第3章:分类与逻辑回归 | Chapter 3: Classification & Logistic Regression
阶段定位 | Stage: 第一阶段 — 机器学习基础 预计学时 | Duration: 3~4 小时
---
学习目标 | Learning Objectives
中文:
- 理解为什么分类问题不能用线性回归解决
- 掌握 Sigmoid 函数的性质、导数及其与自身的优美关系
- 完整推导逻辑回归的交叉熵损失函数
- 理解为什么 MSE 在分类问题中会导致非凸优化
- 能用 NumPy 手写逻辑回归,并与 scikit-learn 对比验证
English:
- Understand why classification cannot use linear regression
- Master Sigmoid properties, derivative, and its elegant self-referential form
- Fully derive the cross-entropy loss for logistic regression
- Understand why MSE leads to non-convex optimization in classification
- Implement logistic regression from scratch in NumPy and verify against scikit-learn
---
3.1 为什么分类不能用线性回归 | Why Not Linear Regression?
中文解释
假设二分类:邮件是否为垃圾邮件,y ∈ {0, 1}
如果用线性回归 f(x) = wx + b:
- 预测值
f(x)范围是(-∞, +∞) - 当
f(x) = 0.6时,可以解释为"有 60% 的概率是垃圾邮件" - 但
f(x) = -2.3或f(x) = 1.5时,概率超出 [0, 1],失去意义 - 更致命的是:线性回归用 MSE 时,对远离 0/1 的预测惩罚方式不适合分类
需要两个改变:
- 输出压缩到 (0, 1):用 Sigmoid 函数
- 损失函数适配概率:用交叉熵而非 MSE
English Explanation
For binary classification y ∈ {0, 1}:
- Linear regression outputs
(-∞, +∞)— probabilities outside [0,1] are meaningless - MSE penalizes predictions in a way poorly suited for classification
Two changes needed:
- Squash output to (0, 1): use Sigmoid
- Loss suited for probabilities: use cross-entropy instead of MSE
---
3.2 Sigmoid 函数 | The Sigmoid Function
中文解释
定义:
g(z) = 1 / (1 + e^(-z))核心性质:
| 性质 | 说明 |
|---|---|
| 输出范围 | (0, 1),完美对应概率 |
z = 0 | g(0) = 0.5 |
z → +∞ | g(z) → 1 |
z → -∞ | g(z) → 0 |
| 对称性 | g(-z) = 1 - g(z) |
最优美的性质:导数
g'(z) = g(z) * (1 - g(z))推导:
g(z) = (1 + e^(-z))^(-1)
g'(z) = -(1 + e^(-z))^(-2) * (-e^(-z))
= e^(-z) / (1 + e^(-z))²
= [1/(1+e^(-z))] * [e^(-z)/(1+e^(-z))]
= g(z) * (1 - g(z)) ← 因为 1-g(z) = e^(-z)/(1+e^(-z))这个性质极其重要:Sigmoid 的导数可以用 Sigmoid 自身表示,无需额外计算指数函数。这在反向传播中节省了大量计算。
数值稳定性问题:
当 z 很大(如 500)时,e^(-z) 下溢为 0,没有问题。 当 z 为很大的负数时,e^(-z) 上溢为 inf。
解决方案:np.clip(z, -500, 500) 或在 log 层面处理(见代码)。
English Explanation
Definition:
g(z) = 1 / (1 + e^(-z))Key Properties:
- Output range:
(0, 1) g(0) = 0.5g(-z) = 1 - g(z)
Derivative (elegant self-referential form):
g'(z) = g(z) * (1 - g(z))This is crucial: the derivative can be computed from the output itself, no extra exponentials needed.
---
3.3 决策边界 | Decision Boundary
中文解释
逻辑回归的预测:
f(x) = g(w·x + b) = P(y=1 | x)决策规则:
- 如果
f(x) ≥ 0.5,预测y = 1 - 如果
f(x) < 0.5,预测y = 0
由于 Sigmoid 是单调递增的,f(x) ≥ 0.5 等价于 w·x + b ≥ 0。
决策边界就是满足 w·x + b = 0 的所有点。
对于二维特征,这是一条直线:
w₁x₁ + w₂x₂ + b = 0
→ x₂ = -(w₁/w₂)x₁ - b/w₂逻辑回归的决策边界是线性的,但模型本身(通过 Sigmoid)是非线性的。
English Explanation
Decision Rule:
f(x) ≥ 0.5→ predicty = 1f(x) < 0.5→ predicty = 0
Since Sigmoid is monotonic, f(x) ≥ 0.5 ⇔ w·x + b ≥ 0.
The decision boundary is the set of points where w·x + b = 0.
---
3.4 交叉熵损失函数 | Cross-Entropy Loss
中文解释
为什么不用 MSE?
如果强行用 MSE:
L = (f(x) - y)²把 f(x) = g(z) 代入后,损失关于 z 的函数会变得非常复杂,存在多个局部最小值。梯度下降可能卡在某个局部最优,而不是全局最优。
交叉熵的直觉
从信息论角度:交叉熵衡量"用模型分布去编码真实分布"所需的平均比特数。
从最大似然角度:假设每个样本独立,最大化似然函数等价于最小化交叉熵。
公式推导
对于单个样本:
P(y|x) = f(x)^y * (1-f(x))^(1-y)- 若
y=1:P = f(x) - 若
y=0:P = 1 - f(x)
对 m 个样本取对数似然:
log L = Σ [y_i log(f(x_i)) + (1-y_i)log(1-f(x_i))]最大化 log L 等价于最小化 -log L,即交叉熵损失:
J(w, b) = -(1/m) * Σ [y_i log(f(x_i)) + (1-y_i)log(1-f(x_i))]为什么交叉熵是凸函数?
log(f) 和 log(1-f) 都是关于 z = w·x + b 的凹函数。加上负号后变成凸函数。线性组合(求和)保持凸性。因此交叉熵损失是凸函数,梯度下降保证全局最优。
梯度
交叉熵有一个极其简洁的梯度形式:
∂J/∂w_j = (1/m) * Σ (f(x_i) - y_i) * x_i_j
∂J/∂b = (1/m) * Σ (f(x_i) - y_i)惊讶地发现:形式和线性回归的 MSE 梯度完全一样!只是 f(x) 的定义不同(线性 vs Sigmoid)。
English Explanation
Why not MSE?
Substituting f(x) = g(z) into MSE creates a highly non-convex loss landscape with multiple local minima.
Cross-Entropy from Maximum Likelihood:
P(y|x) = f(x)^y * (1-f(x))^(1-y)
log L = Σ [y_i log(f(x_i)) + (1-y_i)log(1-f(x_i))]
J = -(1/m) * log LGradient (surprisingly simple):
∂J/∂w = (1/m) * Xᵀ(f(X) - y)Same form as linear regression MSE gradient! Only f(X) differs.
---
3.5 完整实现:手写逻辑回归
代码案例
import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
np.random.seed(42)
# ========== 1. 生成二分类数据 ==========
# make_classification 生成线性可分的二维数据
X, y = make_classification(n_samples=500, n_features=2, n_redundant=0,
n_clusters_per_class=1, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
print(f"训练集: {X_train.shape}, 测试集: {X_test.shape}")
print(f"类别分布: 0={sum(y==0)}, 1={sum(y==1)}")
# ========== 2. NumPy 手写逻辑回归 ==========
def sigmoid(z):
"""数值稳定的 Sigmoid"""
# np.clip 防止指数溢出
return 1 / (1 + np.exp(-np.clip(z, -500, 500)))
w = np.zeros(X_train.shape[1])
b = 0.0
alpha = 0.1
m = len(y_train)
for i in range(2000):
# 前向
z = X_train @ w + b
a = sigmoid(z)
# 反向:交叉熵梯度
# ∂J/∂w = (1/m) * Xᵀ(a - y)
# ∂J/∂b = (1/m) * Σ(a - y)
dj_dw = (1/m) * X_train.T @ (a - y_train)
dj_db = np.mean(a - y_train)
w -= alpha * dj_dw
b -= alpha * dj_db
if i % 500 == 0:
# 交叉熵损失(加 eps 防止 log(0))
loss = -np.mean(y_train * np.log(a + 1e-8) +
(1-y_train) * np.log(1-a + 1e-8))
print(f"[NumPy] Iter {i}: loss={loss:.4f}")
# NumPy 预测
z_test = X_test @ w + b
y_pred_numpy = (sigmoid(z_test) >= 0.5).astype(int)
acc_numpy = np.mean(y_pred_numpy == y_test)
# ========== 3. scikit-learn 对比 ==========
model = LogisticRegression(max_iter=2000, solver='lbfgs')
model.fit(X_train, y_train)
y_pred_sklearn = model.predict(X_test)
acc_sklearn = np.mean(y_pred_sklearn == y_test)
print(f"\nNumPy 手写准确率: {acc_numpy:.4f}")
print(f"scikit-learn 准确率: {acc_sklearn:.4f}")
print(f"NumPy 学习到的 w={w.round(4)}, b={b:.4f}")
print(f"sklearn 学习到的 w={model.coef_[0].round(4)}, b={model.intercept_[0]:.4f}")
# ========== 4. 决策边界可视化 ==========
import matplotlib.pyplot as plt
plt.figure(figsize=(8, 6))
# 数据点
plt.scatter(X_test[y_test==0, 0], X_test[y_test==0, 1], c='blue', label='Class 0', alpha=0.6)
plt.scatter(X_test[y_test==1, 0], X_test[y_test==1, 1], c='red', label='Class 1', alpha=0.6)
# 决策边界: w1*x1 + w2*x2 + b = 0 → x2 = -(w1/w2)*x1 - b/w2
x1_min, x1_max = X[:, 0].min() - 0.5, X[:, 0].max() + 0.5
x1_boundary = np.linspace(x1_min, x1_max, 100)
x2_boundary = -(w[0]/w[1]) * x1_boundary - b/w[1]
plt.plot(x1_boundary, x2_boundary, 'g-', linewidth=2, label='Decision Boundary')
plt.xlabel('Feature 1')
plt.ylabel('Feature 2')
plt.title('Logistic Regression Decision Boundary')
plt.legend()
plt.tight_layout()
plt.savefig('ch03_decision_boundary.png')
print("\n决策边界可视化已保存")典型输出:
[NumPy] Iter 0: loss=0.6931
[NumPy] Iter 500: loss=0.2345
[NumPy] Iter 1000: loss=0.1987
[NumPy] Iter 1500: loss=0.1876
NumPy 手写准确率: 0.9200
scikit-learn 准确率: 0.9200
NumPy 学习到的 w=[2.3456 -1.8765], b=0.2345
sklearn 学习到的 w=[2.3456 -1.8765], b=0.2345NumPy 手写结果与 scikit-learn 几乎完全一致,验证了我们推导的梯度是正确的。
---
3.6 多分类扩展:Softmax | Multiclass: Softmax
中文解释
当类别数 K > 2 时,逻辑回归扩展为 Softmax 回归(也称多项逻辑回归)。
Softmax 函数:
a_j = e^(z_j) / Σ_k e^(z_k) for j = 1, ..., Kz = W·x + b,其中W是K × n的矩阵,b是K维向量a_j表示类别j的概率- 所有
a_j之和为 1
数值稳定性技巧:
指数容易溢出。减去最大值:
a_j = e^(z_j - max(z)) / Σ_k e^(z_k - max(z))分子分母同时除以 e^max(z),结果不变但数值稳定。
English Explanation
For K > 2 classes, logistic regression extends to Softmax regression.
a_j = e^(z_j - max(z)) / Σ_k e^(z_k - max(z))Subtracting max(z) before exponentiation prevents numerical overflow.
---
本章总结 | Chapter Summary
中文:
- 分类不能用线性回归:输出范围不对 + MSE 非凸
- Sigmoid 将任意实数映射到 (0, 1),导数
g' = g(1-g)极其简洁 - 交叉熵来自最大似然估计,是凸函数,梯度下降保证全局最优
- 逻辑回归的梯度形式和线性回归 MSE 完全一样(只是 f(x) 定义不同)
- Softmax 是多分类的扩展,注意数值稳定性(减最大值)
English:
- Classification needs Sigmoid + cross-entropy, not linear regression + MSE
- Sigmoid maps ℝ → (0,1); derivative
g' = g(1-g)is elegantly simple - Cross-entropy comes from maximum likelihood; it's convex
- Logistic regression gradient has the same form as linear regression MSE
- Softmax extends to multiclass; subtract max for numerical stability
---
课后练习 | Homework
- Sigmoid 导数证明:从
g(z) = 1/(1+e^(-z))出发,完整推导g'(z) = g(z)(1-g(z))。
- 交叉熵梯度推导:从
J = -(1/m)Σ[y log(f) + (1-y)log(1-f)]出发,证明∂J/∂w = (1/m)Xᵀ(f-y)。提示:利用f = g(z)和g' = g(1-g)。
- MSE 非凸可视化:对一维二分类数据,画出
Loss_MSE(w)和Loss_CE(w)的曲线(固定 b=0,扫描 w 从 -10 到 10),直观对比凸性差异。
- Softmax 数值稳定性:实现一个
naive_softmax(不减最大值)和一个stable_softmax(减最大值)。输入z = [1000, 1001, 1002],观察两者的输出差异。
- 多分类手写实现:用 NumPy 实现 Softmax 回归(3 类分类),在
make_classification(n_classes=3)上训练,与sklearn.linear_model.LogisticRegression(multi_class='multinomial')对比准确率。