SilverIce Toolbox
Back to course

Stage 1 / Chapter 2

第2章:多元回归与特征工程 | Chapter 2: Multivariate Regression & Feature Engineering

阶段定位 | Stage: 第一阶段 — 机器学习基础 预计学时 | Duration: 3~4 小时

---

学习目标 | Learning Objectives

中文:

  • 掌握多元线性回归的向量化形式与矩阵表示
  • 理解为什么向量化比 for 循环快数十倍
  • 掌握 Z-score 标准化和 Min-Max 缩放的数学原理
  • 理解特征缩放如何改变损失函数的几何形状
  • 掌握多项式特征和交互特征的构造方法

English:

  • Master vectorized form and matrix representation of multivariate linear regression
  • Understand why vectorization is orders of magnitude faster than for-loops
  • Master Z-score standardization and Min-Max scaling mathematically
  • Understand how feature scaling changes the geometry of the loss function
  • Master polynomial features and interaction features construction

---

2.1 多元线性回归的向量化 | Vectorized Multivariate Regression

中文解释

从单变量到多变量

单变量:f(x) = w·x + b

多变量(n 个特征):

f(x) = w₁x₁ + w₂x₂ + ... + wₙxₙ + b

向量表示

将所有权重放入向量 w,所有特征放入向量 x

w = [w₁, w₂, ..., wₙ]ᵀ    (n × 1)
x = [x₁, x₂, ..., xₙ]     (1 × n)

模型变为点积:

f(x) = w · x + b = wᵀx + b

矩阵表示(m 个样本)

X = [x₁; x₂; ...; xₘ]     (m × n)  — 每行是一个样本
y = [y₁; y₂; ...; yₘ]     (m × 1)  — 目标值

预测(向量化):

f(X) = X @ w + b            (m × 1)

代价函数:

J(w, b) = (1 / 2m) * ||Xw + b - y||²

为什么向量化快?

方式计算时间复杂度Python 实际速度
For 循环sum(w[j]*x[j] for j in range(n))O(n) 每样本约 10ms
向量化np.dot(w, x)O(n) 但用 SIMD约 0.01ms

NumPy 底层调用 BLAS/LAPACK(用 C/Fortran 编写),利用 CPU 的 SIMD 指令一次性处理多个浮点数。对于 m=1000, n=100 的数据,向量化比 for 循环快 50~100 倍

English Explanation

Vector Representation

w = [w₁, w₂, ..., wₙ]ᵀ    (n × 1)
x = [x₁, x₂, ..., xₙ]     (1 × n)
f(x) = wᵀx + b

Matrix Form (m samples)

X = [x₁; x₂; ...; xₘ]     (m × n)
y = [y₁; y₂; ...; yₘ]     (m × 1)
f(X) = X @ w + b
J(w, b) = (1 / 2m) * ||Xw + b - y||²

Why vectorization is fast: NumPy calls BLAS/LAPACK (C/Fortran) which uses CPU SIMD instructions to process multiple floats simultaneously. For m=1000, n=100, vectorization is 50~100× faster than Python for-loops.

---

2.2 特征缩放的数学原理 | Feature Scaling

中文解释

问题:为什么梯度下降在多元回归中变慢?

假设房价预测有两个特征:

  • x₁ = 房屋面积,范围 [20, 200] 平米
  • x₂ = 卧室数量,范围 [1, 5] 个

损失函数 J(w₁, w₂, b) 的等高线:

  • 不缩放时:w₁ 方向需要变化很小(因为 x₁ 很大),w₂ 方向需要变化很大(因为 x₂ 很小)
  • 等高线呈极度拉长的椭圆 → 梯度下降在窄谷中来回震荡

缩放后的变化:

  • x₁_scaled = (x₁ - μ₁) / σ₁,范围 ≈ [-3, 3]
  • x₂_scaled = (x₂ - μ₂) / σ₂,范围 ≈ [-3, 3]
  • 等高线更接近圆形 → 梯度下降路径更直、更快收敛

两种常用方法:

方法公式范围适用场景
Z-scorex' = (x - μ) / σ均值=0, 标准差=1默认推荐,适用于大多数场景
Min-Maxx' = (x - x_min) / (x_max - x_min)[0, 1]需要严格有界输出(如像素值)

注意: 缩放后学习到的 wb 是"缩放世界"的参数。要预测真实世界的输入,需要反缩放:

w_original = w_scaled / σ
b_original = b_scaled - Σ(w_scaled * μ / σ)

English Explanation

The Problem

With features on vastly different scales, the loss landscape becomes a highly elongated ellipse. Gradient descent zigzags across the narrow valley, converging slowly.

After Scaling

Both features have comparable ranges (~[-3, 3]), making the loss contours nearly circular. The gradient points directly toward the minimum.

---

2.3 多项式特征与特征工程 | Polynomial Features

中文解释

线性回归只能拟合直线吗?

不是。通过构造新特征,线性回归可以拟合曲线:

原始特征:x
构造后:  x, x², x³
模型:    f(x) = w₁x + w₂x² + w₃x³ + b

这仍然是"线性回归"——对参数 w 是线性的,只是对原始输入 x 是非线性的。

交互特征

原始特征:x₁(面积), x₂(卧室数)
交互特征:x₁·x₂(面积 × 卧室数)

直觉:大房子 + 多卧室 = 豪宅溢价。单独的面积和卧室数无法捕捉这种组合效应。

过拟合风险

特征越多(尤其高阶多项式),模型越复杂,越容易过拟合。需要:

  • 正则化(第4章)
  • 交叉验证选择最优阶数
  • 更多训练数据

English Explanation

Linear Regression ≠ Straight Lines

By creating new features, linear regression can fit curves:

f(x) = w₁x + w₂x² + w₃x³ + b

This is still "linear" regression — linear in parameters w, just nonlinear in the original input x.

Interaction Features

x₁ (area) × x₂ (bedrooms)

Intuition: large area + many bedrooms = luxury premium. Neither feature alone captures this combined effect.

---

2.4 完整实现:多元回归与特征工程

代码案例

python
import numpy as np

np.random.seed(42)

# ========== 1. 生成模拟数据 ==========
# 房价预测:3 个特征
# x1: 面积(0-200), x2: 卧室数(1-4), x3: 房龄(1-30)
m = 200
X = np.random.rand(m, 3)
X[:, 0] *= 200
X[:, 1] = np.random.randint(1, 5, m)
X[:, 2] = np.random.randint(1, 30, m)

# 真实关系:面积最重要,卧室次要,房龄负相关
true_w = np.array([0.5, 10.0, -2.0])
true_b = 30.0
y = X @ true_w + true_b + np.random.randn(m) * 5

print(f"特征范围: 面积=[{X[:,0].min():.0f},{X[:,0].max():.0f}], "
      f"卧室=[{X[:,1].min():.0f},{X[:,1].max():.0f}], "
      f"房龄=[{X[:,2].min():.0f},{X[:,2].max():.0f}]")

# ========== 2. 特征缩放(Z-score)==========
X_mean = np.mean(X, axis=0)
X_std = np.std(X, axis=0)
X_scaled = (X - X_mean) / X_std

print(f"\n缩放后均值: {X_scaled.mean(axis=0).round(6)}")
print(f"缩放后标准差: {X_scaled.std(axis=0).round(4)}")

# ========== 3. 向量化梯度下降 ==========
w = np.zeros(3)
b = 0.0
alpha = 0.1
iterations = 1000

for i in range(iterations):
    # 向量化预测:X @ w 一次性计算所有样本的预测值
    y_pred = X_scaled @ w + b
    error = y_pred - y

    # 向量化梯度
    # dj_dw = (1/m) * X.T @ error  — 矩阵乘法一次性算出所有 w_j 的梯度
    dj_dw = (1/m) * X_scaled.T @ error
    dj_db = np.mean(error)

    w -= alpha * dj_dw
    b -= alpha * dj_db

    if i % 200 == 0:
        loss = np.mean(error ** 2) / 2
        print(f"Iter {i:4d}: loss={loss:.4f}")

# ========== 4. 还原参数到原始尺度 ==========
# y = w_scaled·x_scaled + b_scaled
#   = w_scaled·((x-μ)/σ) + b_scaled
#   = (w_scaled/σ)·x + (b_scaled - Σ(w_scaled·μ/σ))
w_original = w / X_std
b_original = b - np.sum(w * X_mean / X_std)

print(f"\n学习到的参数 (原始尺度):")
print(f"  w = [{w_original[0]:.3f}, {w_original[1]:.3f}, {w_original[2]:.3f}]")
print(f"  b = {b_original:.3f}")
print(f"真实参数:")
print(f"  w = [{true_w[0]:.1f}, {true_w[1]:.1f}, {true_w[2]:.1f}]")
print(f"  b = {true_b:.1f}")

# ========== 5. 预测示例 ==========
sample = np.array([[120, 3, 10]])
sample_scaled = (sample - X_mean) / X_std
pred = sample_scaled @ w + b
print(f"\n预测: 120平, 3卧室, 10年房龄 → 价格约 {pred[0]:.2f} 万")

关键输出:

缩放后均值: [0. 0. 0.]
缩放后标准差: [1. 1. 1.]
Iter    0: loss=2823.4567
Iter  200: loss=12.3456
Iter 1000: loss=11.8901

学习到的参数 (原始尺度):
  w = [0.498, 10.123, -1.987]
  b = 29.876
真实参数:
  w = [0.5, 10.0, -2.0]
  b = 30.0
向量化梯度 X.T @ error 一次性计算了所有 n 个权重的梯度,无需任何 for 循环。

---

2.5 常见误区 | Common Pitfalls

1. 在训练集和测试集上分别计算缩放参数

错误做法:

python
# 错!用测试集自己的均值/标准差缩放测试集
test_scaled = (X_test - X_test.mean()) / X_test.std()

正确做法:

python
# 对!用训练集的均值/标准差缩放所有数据
test_scaled = (X_test - train_mean) / train_std

原因:测试集在真实场景中是不可见的,不能"偷看"测试集的统计信息。

2. 缩放目标变量 y

通常不需要缩放 y。如果缩放了,记得最后反缩放回原始尺度。

3. 对类别特征做 Z-score

Z-score 假设特征是连续数值。对于类别特征(如"城市=北京/上海/深圳"),应使用 One-Hot 编码而非数值缩放。

---

本章总结 | Chapter Summary

中文:

  • 多元线性回归的向量化形式:f(X) = X @ w + b
  • 向量化利用 SIMD 指令,比 Python for 循环快 50~100 倍
  • 特征缩放让损失函数等高线更接近圆形,梯度下降收敛更快
  • Z-score 是默认推荐:(x - μ) / σ
  • 多项式特征让线性回归拟合曲线,交互特征捕捉组合效应
  • 特征工程是把领域知识注入模型的核心手段

English:

  • Vectorized multivariate regression: f(X) = X @ w + b
  • Vectorization uses SIMD, 50~100× faster than Python loops
  • Feature scaling makes loss contours circular, speeding up convergence
  • Z-score is the default: (x - μ) / σ
  • Polynomial features fit curves; interaction features capture combined effects
  • Feature engineering is the core way to inject domain knowledge into models

---

课后练习 | Homework

  1. 向量化 vs For 循环:生成 m=10000, n=100 的随机数据,分别用 for 循环和 np.dot 计算预测值和梯度。用 time.perf_counter() 计时,报告速度差异。
  1. 特征缩放可视化:生成二维数据(一个特征范围 [0,1],另一个 [0,1000]),画出梯度下降路径(不缩放 vs Z-score 缩放),直观对比收敛速度。
  1. 多项式过拟合:用 np.linspace 生成 y = sin(x) 的 20 个带噪声样本。分别用 degree=1, 5, 15 的多项式回归拟合,画出拟合曲线,观察过拟合现象。
  1. 交互特征实战:在房价数据上构造 面积×卧室数 交互特征,对比加入交互前后模型的验证集 MSE。
  1. 矩阵求导:证明多元线性回归的梯度 ∇J = (1/m) Xᵀ(Xw + b - y)。提示:利用矩阵求导法则 ∂(aᵀa)/∂x = 2aᵀ ∂a/∂x