Machine Learning: 线性回归 — 从数学原理到代码实现的销售预测指南
线性回归是ML的"Hello World"——简单、可解释、效果不差,是所有回归任务的起点。
1. 你将学到
- 线性回归数学原理:假设函数、损失函数(MSE)、梯度下降求解
- Scikit-learn实现:LinearRegression vs SGDRegressor,正则化(Ridge/Lasso/ElasticNet)
- 多元回归与特征解读:系数的经济含义
- 假设检验与诊断:残差分析、Q-Q图、异方差性、多重共线性(VIF)
- Bob的销售预测:用历史广告费+流量数据预测下月销售额
2. 一个电商运营的真实故事
(1) 痛点:广告预算不知道怎么分
Bob每月有200 thousand USD的广告预算,要分配到Google Ads、Facebook、Email三个渠道。直觉告诉他"多投多卖",但上个月Google Ads增加50k USD只带来30k USD额外收入——边际收益递减,但不知道拐点在哪。
(2) 线性回归的解法
线性回归能量化每个渠道对销售额的贡献,找到最优预算分配。
PYTHON
from sklearn.linear_model import LinearRegression
model = LinearRegression()
model.fit(X_train, y_train)
# Each coefficient = marginal contribution per 1k USD ad spend
for channel, coef in zip(channels, model.coef_):
print(f"{channel}: +{coef:.2f}k USD revenue per 1k USD spend")
(3) 收益:ROI提升35%
Bob发现Google Ads系数为0.8(每投1k USD回报0.8k USD),而Email系数为2.5(每投1k USD回报2.5k USD)。重新分配预算后,整体ROI提升35%。
3. 线性回归数学原理
(1) 假设函数与损失函数
线性回归假设输出是输入的线性组合:$\hat{y} = w_1x_1 + w_2x_2 + ... + b$
损失函数用MSE(均方误差):$L = \frac{1}{n}\sum_{i=1}^{n}(y_i - \hat{y}_i)^2$
▶ 示例:手动实现线性回归
PYTHON
import numpy as np
# Gradient descent for linear regression
def linear_regression_gd(X, y, lr=0.01, epochs=1000):
n_samples, n_features = X.shape
w = np.zeros(n_features)
b = 0.0
for epoch in range(epochs):
# Forward pass
y_pred = X @ w + b
# Compute gradients
dw = (2 / n_samples) * (X.T @ (y_pred - y))
db = (2 / n_samples) * np.sum(y_pred - y)
# Update parameters
w -= lr * dw
b -= lr * db
if epoch % 200 == 0:
loss = np.mean((y - y_pred) ** 2)
print(f"Epoch {epoch}: MSE = {loss:.4f}")
return w, b
# Test with simple data
rng = np.random.default_rng(42)
X = rng.uniform(0, 10, (100, 1))
y = 3 * X.squeeze() + 7 + rng.normal(0, 2, 100)
w, b = linear_regression_gd(X, y, lr=0.01, epochs=1000)
print(f"\nLearned: w={w[0]:.2f}, b={b:.2f}")
print(f"True: w=3.00, b=7.00")
输出:
TEXT
📖 仅展示
# 函数定义成功
(2) 梯度下降可视化
sequenceDiagram
participant Init as Parameter Init
participant Fwd as Forward Pass
participant Loss as Compute Loss
participant Grad as Compute Gradient
participant Update as Update Parameters
Init->>Fwd: w=0, b=0
loop Each Epoch
Fwd->>Loss: y_pred = Xw + b
Loss->>Grad: MSE = mean((y - y_pred)²)
Grad->>Update: dw, db = gradients
Update->>Fwd: w -= lr*dw, b -= lr*db
end
4. Scikit-learn实现
(1) LinearRegression vs SGDRegressor
▶ 示例:两种求解方式对比
PYTHON
from sklearn.linear_model import LinearRegression, SGDRegressor
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
import numpy as np
rng = np.random.default_rng(42)
n = 500
X = rng.uniform(0, 100, (n, 3))
y = 50 + 0.8 * X[:, 0] + 1.2 * X[:, 1] - 0.5 * X[:, 2] + rng.normal(0, 5, n)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# Method 1: Normal equation (closed-form, exact)
lr_normal = LinearRegression()
lr_normal.fit(X_train, y_train)
# Method 2: SGD (iterative, for large datasets)
lr_sgd = Pipeline([
("scaler", StandardScaler()),
("sgd", SGDRegressor(max_iter=1000, learning_rate="constant", eta0=0.01, random_state=42)),
])
lr_sgd.fit(X_train, y_train)
print(f"Normal Equation R²: {lr_normal.score(X_test, y_test):.4f}")
print(f"SGD R²: {lr_sgd.score(X_test, y_test):.4f}")
print(f"\nNormal coefficients: {lr_normal.coef_.round(2)}")
print(f"True coefficients: [0.80, 1.20, -0.50]")
输出:
TEXT
📖 仅展示
# 执行成功
| 维度 | LinearRegression (Normal Equation) | SGDRegressor |
|---|---|---|
| 求解方式 | 解析解 $(X^TX)^{-1}X^Ty$ | 梯度下降迭代 |
| 计算复杂度 | O(n³) 特征数 | O(n) 每步 |
| 适用数据量 | 中小(n < 100k) | 大(n > 100k) |
| 精度 | 精确解 | 近似解 |
| 需要标准化 | 否 | 是 |
(2) 正则化:Ridge/Lasso/ElasticNet
▶ 示例:正则化对比
PYTHON
from sklearn.linear_model import Ridge, Lasso, ElasticNet
from sklearn.model_selection import cross_val_score
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
import numpy as np
rng = np.random.default_rng(42)
n, p = 100, 20 # p > relevant features (only 5 matter)
X = rng.standard_normal((n, p))
true_coefs = np.zeros(p)
true_coefs[:5] = [3, -2, 1.5, 0.8, -0.5]
y = X @ true_coefs + rng.normal(0, 1, n)
models = {
"LinearRegression": LinearRegression(),
"Ridge (alpha=1)": Ridge(alpha=1),
"Lasso (alpha=0.1)": Lasso(alpha=0.1),
"ElasticNet (alpha=0.1)": ElasticNet(alpha=0.1, l1_ratio=0.5),
}
for name, model in models.items():
pipe = Pipeline([("scaler", StandardScaler()), ("model", model)])
scores = cross_val_score(pipe, X, y, cv=5, scoring="r2")
pipe.fit(X, y)
nonzero = np.sum(np.abs(pipe.named_steps["model"].coef_) > 0.01)
print(f"{name:25s}: R²={scores.mean():.3f}, Non-zero coefs={nonzero}/{p}")
输出:
TEXT
📖 仅展示
# 执行成功
| 正则化 | 惩罚项 | 效果 | 适用场景 |
|---|---|---|---|
| Ridge (L2) | $\alpha \sum w_i^2$ | 缩小系数,不归零 | 多特征都相关 |
| Lasso (L1) | $\alpha \sum |w_i|$ | 稀疏化,部分系数归零 | 特征选择 |
| ElasticNet | L1 + L2混合 | 稀疏+稳定 | 高相关特征组 |
5. 多元回归与特征解读
▶ 示例:Bob的广告ROI分析
PYTHON
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
import pandas as pd
import numpy as np
rng = np.random.default_rng(42)
n = 200
df = pd.DataFrame({
"google_ads_k": rng.uniform(20, 80, n),
"facebook_ads_k": rng.uniform(10, 50, n),
"email_marketing_k": rng.uniform(5, 30, n),
"traffic_k": rng.uniform(50, 300, n),
})
df["revenue_k"] = (
100
+ 0.8 * df["google_ads_k"]
+ 1.2 * df["facebook_ads_k"]
+ 2.5 * df["email_marketing_k"]
+ 0.15 * df["traffic_k"]
+ rng.normal(0, 10, n)
)
X = df.drop(columns=["revenue_k"])
y = df["revenue_k"]
# Train with standardized features for fair comparison
pipe = Pipeline([("scaler", StandardScaler()), ("model", LinearRegression())])
pipe.fit(X, y)
# Interpret standardized coefficients (importance ranking)
coefs = pipe.named_steps["model"].coef_
importance = pd.DataFrame({
"feature": X.columns,
"std_coef": coefs,
"abs_importance": np.abs(coefs),
}).sort_values("abs_importance", ascending=False)
print("Feature Importance (standardized coefficients):")
print(importance.to_string(index=False))
输出:
TEXT
📖 仅展示
Feature Importance (standardized coefficients):
📌 重点: 标准化后的系数大小直接反映特征重要性。Email系数2.5远大于Google系数0.8,说明每1k USD投入Email渠道的回报是Google的3倍。
6. 回归诊断
(1) 残差分析
▶ 示例:4图回归诊断
PYTHON
import matplotlib.pyplot as plt
import numpy as np
from sklearn.linear_model import LinearRegression
from scipy import stats
rng = np.random.default_rng(42)
X = rng.uniform(0, 100, 200).reshape(-1, 1)
y = 50 + 0.8 * X.squeeze() + rng.normal(0, 5, 200)
model = LinearRegression().fit(X, y)
y_pred = model.predict(X)
residuals = y - y_pred
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
# (1) Residuals vs Fitted (check homoscedasticity)
axes[0, 0].scatter(y_pred, residuals, alpha=0.5, s=15)
axes[0, 0].axhline(0, color="red", linestyle="--")
axes[0, 0].set_xlabel("Fitted Values")
axes[0, 0].set_ylabel("Residuals")
axes[0, 0].set_title("Residuals vs Fitted")
# (2) Q-Q Plot (check normality)
stats.probplot(residuals, plot=axes[0, 1])
axes[0, 1].set_title("Q-Q Plot")
# (3) Scale-Location (check variance)
axes[1, 0].scatter(y_pred, np.sqrt(np.abs(residuals / np.std(residuals))), alpha=0.5, s=15)
axes[1, 0].set_xlabel("Fitted Values")
axes[1, 0].set_ylabel("Scale-Location")
axes[1, 0].set_title("Scale-Location")
# (4) Actual vs Predicted
axes[1, 1].scatter(y, y_pred, alpha=0.5, s=15)
axes[1, 1].plot([y.min(), y.max()], [y.min(), y.max()], "r--")
axes[1, 1].set_xlabel("Actual")
axes[1, 1].set_ylabel("Predicted")
axes[1, 1].set_title("Actual vs Predicted")
plt.tight_layout()
plt.savefig("regression_diagnostics.png", dpi=150)
输出:
TEXT
📖 仅展示
# 执行成功
(2) 多重共线性VIF
▶ 示例:VIF检测
PYTHON
from statsmodels.stats.outliers_influence import variance_inflation_factor
import pandas as pd
import numpy as np
# VIF > 10 indicates severe multicollinearity
rng = np.random.default_rng(42)
n = 100
df = pd.DataFrame({
"ad_spend": rng.uniform(10, 100, n),
"traffic": rng.normal(0, 1, n) * 50 + 500,
"clicks": rng.normal(0, 1, n) * 100 + 1000, # Highly correlated with ad_spend
"revenue": rng.uniform(50, 500, n),
})
features = ["ad_spend", "traffic", "clicks"]
for i, col in enumerate(features):
vif = variance_inflation_factor(df[features].values, i)
print(f"{col:12s}: VIF = {vif:.2f} {'⚠️ HIGH' if vif > 10 else '✅ OK'}")
输出:
TEXT
📖 仅展示
# 执行成功
| 诊断项 | 检查方法 | 正常标准 | 异常处理 |
|---|---|---|---|
| 线性关系 | 残差vs拟合值图 | 随机散布 | 加入多项式项 |
| 正态性 | Q-Q图 | 近似直线 | 对数变换 |
| 同方差性 | Scale-Location图 | 水平带状 | 加权回归 |
| 多重共线性 | VIF | VIF < 10 | 删除/合并特征 |
❓ 常见问题
Q LinearRegression不需要标准化吗?
A 不标准化也能训练,但系数不可比(量纲不同)。标准化后系数大小直接反映特征重要性。Ridge/Lasso/SGD必须标准化。
Q Ridge和Lasso该选哪个?
A 特征多且都可能有贡献→Ridge;需要自动特征选择→Lasso;高相关特征组→ElasticNet。实践中都试,用交叉验证选最优。
Q alpha参数怎么选?
A 用GridSearchCV或RidgeCV/LassoCV自动选。alpha越大正则化越强,系数越接近0。通常在对数尺度搜索:[0.001, 0.01, 0.1, 1, 10, 100]。
Q 回归系数为负怎么解释?
A 负系数表示该特征增加时目标值减少。如"退货率"系数为-5,表示退货率每增加1%,收入减少5k USD。
Q VIF > 10怎么办?
A 删除VIF最高的特征,重新计算。或用PCA降维消除共线性。或用Ridge回归(L2正则化对共线性有天然抵抗力)。
Q 残差不满足正态分布怎么办?
A 尝试对y做对数变换(log(y))。如果残差呈漏斗形(异方差),用加权最小二乘或对y做Box-Cox变换。
📖 小节
- 线性回归假设y是x的线性组合,用MSE损失+梯度下降或正规方程求解
- sklearn统一API:LinearRegression(精确解) vs SGDRegressor(大规模数据)
- 正则化三兄弟:Ridge(L2缩系) / Lasso(L1稀疏) / ElasticNet(L1+L2混合)
- 标准化后系数大小反映特征重要性,直接指导业务决策
- 回归诊断四件套:残差图(线性) / Q-Q图(正态) / Scale-Location(方差) / VIF(共线性)
- 系数的业务解读是线性回归的最大价值——可解释性胜过黑箱模型
📝 作业
- 基础题(难度⭐):用sklearn的LinearRegression预测California Housing数据集,输出R²和各特征系数。提示:
fetch_california_housing()+model.coef_。 - 进阶题(难度⭐⭐):对比Ridge(alpha=0.1/1/10/100)在同一数据上的交叉验证R²和系数大小,观察正则化强度如何影响系数。提示:用Pipeline(StandardScaler+Ridge) + GridSearchCV。
- 挑战题(难度⭐⭐⭐):实现完整回归诊断——训练模型后绘制4图诊断面板,计算VIF,判断是否满足线性回归假设,如果不满足提出改进方案。提示:参考第6节的诊断代码。