SilverIce Toolbox
Back to course

Stage 1 / Chapter 3

第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.3f(x) = 1.5 时,概率超出 [0, 1],失去意义
  • 更致命的是:线性回归用 MSE 时,对远离 0/1 的预测惩罚方式不适合分类

需要两个改变:

  1. 输出压缩到 (0, 1):用 Sigmoid 函数
  2. 损失函数适配概率:用交叉熵而非 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:

  1. Squash output to (0, 1): use Sigmoid
  2. 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 = 0g(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.5
  • g(-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 → predict y = 1
  • f(x) < 0.5 → predict y = 0

Since Sigmoid is monotonic, f(x) ≥ 0.5w·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=1P = f(x)
  • y=0P = 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 L

Gradient (surprisingly simple):

∂J/∂w = (1/m) * Xᵀ(f(X) - y)

Same form as linear regression MSE gradient! Only f(X) differs.

---

3.5 完整实现:手写逻辑回归

代码案例

python
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.2345
NumPy 手写结果与 scikit-learn 几乎完全一致,验证了我们推导的梯度是正确的。

---

3.6 多分类扩展:Softmax | Multiclass: Softmax

中文解释

当类别数 K > 2 时,逻辑回归扩展为 Softmax 回归(也称多项逻辑回归)。

Softmax 函数:

a_j = e^(z_j) / Σ_k e^(z_k)    for j = 1, ..., K
  • z = W·x + b,其中 WK × n 的矩阵,bK 维向量
  • 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

  1. Sigmoid 导数证明:从 g(z) = 1/(1+e^(-z)) 出发,完整推导 g'(z) = g(z)(1-g(z))
  1. 交叉熵梯度推导:从 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)
  1. MSE 非凸可视化:对一维二分类数据,画出 Loss_MSE(w)Loss_CE(w) 的曲线(固定 b=0,扫描 w 从 -10 到 10),直观对比凸性差异。
  1. Softmax 数值稳定性:实现一个naive_softmax(不减最大值)和一个stable_softmax(减最大值)。输入 z = [1000, 1001, 1002],观察两者的输出差异。
  1. 多分类手写实现:用 NumPy 实现 Softmax 回归(3 类分类),在 make_classification(n_classes=3) 上训练,与 sklearn.linear_model.LogisticRegression(multi_class='multinomial') 对比准确率。