chapter 02 / classical-ml · 预计学习时间 120-150 分钟
第 1 章预测的是连续值(房价)。现在改成预测类别:邮件是不是垃圾?肿瘤是良性还是恶性?标签 $y \in \{0, 1\}$。
能不能直接用线性回归,输出 $>0.5$ 算类别 1?理论上可以,实际很糟糕:① 输出值域是 $(-\infty, +\infty)$,没有概率解释,「预测值 3.7」是什么意思?② MSE 对远离边界的「过于正确」的点也施加损失,一个极端但分类正确的样本会把决策边界拉歪。
我们需要两个新零件:把输出压缩成概率的函数,和与概率匹配的损失函数。这两个零件——sigmoid/softmax 和交叉熵——会一路用到 GPT:LLM 的预训练损失就是 softmax + 交叉熵,只是类别从 2 个变成约 10 万个(词表里每个 token 是一类)。这一章的推导是第 7 章的直接地基。
保留线性骨架 $z = \mathbf{w}^\top\mathbf{x} + b$(这个 $z$ 有个会用一辈子的名字:logit),再套一个挤压函数:
$$\sigma(z) = \frac{1}{1 + e^{-z}}, \qquad \hat{p} = \sigma(\mathbf{w}^\top\mathbf{x} + b) = P(y{=}1 \mid \mathbf{x})$$sigmoid 把任意实数单调地压进 $(0,1)$:$z=0$ 时输出 0.5(最不确定),$z$ 越大越接近 1。它的导数有个优美的形式(推导:对 $\sigma(z)=(1+e^{-z})^{-1}$ 用链式法则后整理):
$$\sigma'(z) = \sigma(z)\,(1 - \sigma(z))$$损失函数不拍脑袋,走第 1 章学过的最大似然路线。单个样本 $y \in \{0,1\}$ 服从伯努利分布,两种情况可以合写成一个式子:
$$P(y \mid \mathbf{x}) = \hat{p}^{\,y}\,(1-\hat{p})^{\,1-y}$$全数据集的似然是各样本连乘。取对数(连乘变求和,且不改变最优点),再取负号变成「越小越好」的损失,除以 $n$ 取平均:
$$L = -\frac{1}{n}\sum_{i=1}^n \Big[ y^{(i)} \ln \hat{p}^{(i)} + (1 - y^{(i)}) \ln (1 - \hat{p}^{(i)}) \Big]$$这就是二元交叉熵(Binary Cross-Entropy)。直觉检查:若真实 $y=1$,损失是 $-\ln\hat{p}$——预测 $\hat{p}\to 1$ 损失趋于 0,$\hat{p}\to 0$ 损失爆炸到无穷。对自信的错误施以无界惩罚,这正是分类任务想要的性格。
现在求梯度。对 $z$ 用链式法则:$\frac{\partial L_i}{\partial z} = \frac{\partial L_i}{\partial \hat{p}}\cdot\frac{\partial \hat{p}}{\partial z}$。代入 $\sigma' = \sigma(1-\sigma)$,一串看似复杂的项神奇地相消(强烈建议手推一次),剩下:
$$\frac{\partial L_i}{\partial z} = \hat{p}^{(i)} - y^{(i)} \qquad\Longrightarrow\qquad \nabla_\mathbf{w} L = \frac{1}{n}\sum_i \big(\hat{p}^{(i)} - y^{(i)}\big)\,\mathbf{x}^{(i)}$$$K$ 类时,让模型输出 $K$ 个 logit $z_1,\dots,z_K$,用 softmax 归一化成概率分布:
$$\hat{p}_k = \frac{e^{z_k}}{\sum_{j=1}^{K} e^{z_j}}, \qquad L = -\ln \hat{p}_{y}\ \text{(只罚正确类别的概率)}$$几个工程上必须知道的细节:
换一种完全不同的世界观:不学权重,学一连串问题。「面积 > 90㎡?」→ 是 →「房龄 < 10 年?」→ …… 每个内部节点是一次特征切分,每个叶子是一个预测。它天生处理非线性、不在乎特征量纲、人类可读。
贪心地选让子节点最纯的切分。两种纯度度量:
$$\text{熵:} H(S) = -\sum_k p_k \log_2 p_k \qquad\qquad \text{基尼:} G(S) = 1 - \sum_k p_k^2$$都在「各类均匀混合」时最大、「只剩一类」时为 0。信息增益 = 父节点熵 − 子节点熵的加权平均;CART 算法用基尼(无对数,算得快),效果与熵几乎无差。对切分点 $t$ 的搜索就是:对每个特征的每个候选阈值,算一遍加权纯度,取最优——你将在实验室里亲眼看到这种「轴对齐切分」画出的矩形拼图边界。
同一份数据,三种算法看到的世界完全不同。逻辑回归只能画直线;kNN 的边界跟着数据走但锯齿斑驳;决策树只会画横平竖直的矩形。必做实验:①月牙数据下对比三者;②XOR 数据让逻辑回归当场翻车(线性模型的极限,第 3 章神经网络的出场理由);③树深调到 12 看过拟合、调回 2 看欠拟合;④kNN 的 k=1 vs k=15 对比(方差 vs 偏差)。
点击画布放点 · 背景色块 = 模型对该区域的预测类别 · 训练集准确率实时显示
「三个臭皮匠」的两种科学化方式,方向相反:
对训练集做 $B$ 次有放回抽样(bootstrap),各训练一棵不剪枝的深树,预测时投票。$B$ 个方差为 $\sigma^2$、两两相关系数 $\rho$ 的估计器取平均后,方差变为:
$$\rho\sigma^2 + \frac{1-\rho}{B}\sigma^2$$第二项随 $B$ 消失,但第一项卡在 $\rho\sigma^2$——树之间越相关,平均的收益越有限。随机森林的点睛之笔由此而来:每次分裂只从随机抽的 $\sqrt{d}$ 个特征里选,强行让树彼此「想得不一样」,把 $\rho$ 压下去。这就是「多样性比个体强度更重要」的数学表达。
反过来用一串浅树(弱学习器),每棵专门修正前面所有树的残余错误。第 $m$ 步的精妙视角:把当前模型的预测 $F_{m-1}(x)$ 看作「参数」,对它求损失的负梯度 $r_i = -\frac{\partial L(y_i, F)}{\partial F}\big|_{F_{m-1}}$,训练一棵小树去拟合这个负梯度,然后 $F_m = F_{m-1} + \eta \cdot \text{tree}_m$。MSE 下负梯度恰好就是残差 $y - F(x)$,所以直觉版叙事是「每棵树学习上一轮的残差」——但负梯度视角才是它叫「梯度提升」的原因:这是在函数空间里做梯度下降。
XGBoost(2014)把这套做到工业极致:目标函数二阶泰勒展开(牛顿法而非单纯梯度)、对叶子权重加 L2 正则、缺失值自动学习默认方向、直方图加速与并行。在中小规模表格数据上,「XGBoost/LightGBM 优于神经网络」直到 2026 年仍是基本事实——别看完本课程就觉得万物皆深度学习。
可分数据有无数条分隔直线,哪条最好?SVM 的回答:离两边最近样本都最远的那条——最大化间隔(margin)。几何推导:点到超平面 $\mathbf{w}^\top\mathbf{x}+b=0$ 的距离是 $\frac{|\mathbf{w}^\top\mathbf{x}+b|}{\|\mathbf{w}\|}$,约定支持向量处 $|\mathbf{w}^\top\mathbf{x}+b|=1$,则间隔为 $\frac{2}{\|\mathbf{w}\|}$。最大化间隔等价于:
$$\min_{\mathbf{w},b}\ \frac{1}{2}\|\mathbf{w}\|^2 \quad \text{s.t.}\quad y^{(i)}(\mathbf{w}^\top\mathbf{x}^{(i)} + b) \ge 1,\ \forall i \qquad (y \in \{-1,+1\})$$凸二次规划,有全局最优。现实数据有噪声,引入松弛变量 $\xi_i \ge 0$ 允许个别点越界,代价由超参 $C$ 控制(软间隔,1995)。用拉格朗日乘子法转成对偶问题后,发生两件大事:
$$\max_{\alpha}\ \sum_i \alpha_i - \frac{1}{2}\sum_{i,j} \alpha_i \alpha_j y^{(i)} y^{(j)} \langle \mathbf{x}^{(i)}, \mathbf{x}^{(j)} \rangle$$# 四种世界观在两个数据集上的对决
import numpy as np
from sklearn.datasets import make_moons, make_circles
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.svm import SVC
datasets = {
"moons": make_moons(n_samples=500, noise=0.25, random_state=0),
"circles": make_circles(n_samples=500, noise=0.1, factor=0.4, random_state=0),
}
models = {
"LogReg(线性)": LogisticRegression(),
"Tree(d=4)": DecisionTreeClassifier(max_depth=4),
"RandomForest": RandomForestClassifier(n_estimators=200),
"GradBoost": GradientBoostingClassifier(), # 浅树串联
"SVM-RBF": SVC(kernel="rbf", C=1.0, gamma="scale"),
}
for dname, (X, y) in datasets.items():
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.3, random_state=0)
print(f"\n== {dname} ==")
for mname, m in models.items():
acc = m.fit(Xtr, ytr).score(Xte, yte) # 注意:评测用测试集!
print(f" {mname:14s} {acc:.3f}")
# 预期:moons/circles 上 LogReg 明显落后(线性边界的极限),
# RBF-SVM 与集成方法接近满分。把 noise 调大再跑——排名会怎么变?