Machine Learning: 线性回归 — 从数学原理到代码实现的销售预测指南

线性回归是ML的"Hello World"——简单、可解释、效果不差,是所有回归任务的起点。

1. 你将学到


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) 梯度下降可视化

100%
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变换。

📖 小节


📝 作业

  1. 基础题(难度⭐):用sklearn的LinearRegression预测California Housing数据集,输出R²和各特征系数。提示:fetch_california_housing() + model.coef_
  2. 进阶题(难度⭐⭐):对比Ridge(alpha=0.1/1/10/100)在同一数据上的交叉验证R²和系数大小,观察正则化强度如何影响系数。提示:用Pipeline(StandardScaler+Ridge) + GridSearchCV。
  3. 挑战题(难度⭐⭐⭐):实现完整回归诊断——训练模型后绘制4图诊断面板,计算VIF,判断是否满足线性回归假设,如果不满足提出改进方案。提示:参考第6节的诊断代码。

← 上一课:综合练习 — 入门项目 | 下一课:逻辑回归 →

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏