teach.pascalyim.com
目录

ML · 章节 3

第三章 监督分类

在 Kaggle 上打开

引言

在第二章里,我们学习了如何用线性回归和 k 近邻去预测一个连续的目标变量——例如鲍鱼的年龄或房屋的价格。然而在工程实践中,更常见的需求并不是估计一个数值,而是回答一个是非题:这位乘客能否在沉船中幸存?这个肿瘤是良性还是恶性?这封邮件是垃圾邮件吗?这类问题统称为分类问题 (classification),其中目标变量 YY 取离散的、有限多个值。当 Y{0,1}Y \in \{0, 1\} 时,问题被称作二元分类 (binary classification)。

本章将围绕两个迷你数据集展开:titanic_mini(泰坦尼克号生还预测)与 cancer_mini(威斯康星州乳腺癌诊断)。我们将依次介绍朴素贝叶斯、分类指标、概率分数、ROC 曲线、决策树、随机森林、梯度提升以及 LightGBM 与 XGBoost 等业界标杆。

本章的核心问题:当我们说一个分类器"准确率 80%"时,这句话到底有多少信息量?如何区分两个准确率相同但行为迥异的模型?


1. titanic_mini 数据集与经验概率

1.1 数据集结构

titanic_mini 是泰坦尼克号原始数据集的极简版本,仅保留四个二元变量:

列名含义取值
Sex性别0 = 男,1 = 女
Children是否儿童0 = 成人,1 = 儿童
FirstClass头等舱标志0 = 否,1 = 是
Survived目标变量:是否幸存0 = 罹难,1 = 幸存

所有变量都已经过编码并取布尔值,这让我们可以专注于分类算法本身,而不必处理缺失值或类别编码。

1.2 用布尔条件过滤 DataFrame

pandas 通过逐元素的逻辑运算允许我们提取数据子集。三个核心算子分别是按位与 &、按位或 |、按位非 ~,每个条件都必须放在括号里:

# 一等舱的女性乘客 df[(df["Sex"] == 1) & (df["FirstClass"] == 1)] # 不在一等舱的男性乘客 df[(df["Sex"] == 0) & (df["FirstClass"] == 0)]

由于 Survived 是 0/1 编码的,该列的均值正好等于幸存比例——这是一个非常便利的小技巧:

df[(df["Sex"] == 1) & (df["FirstClass"] == 1)]["Survived"].mean()

关键观察:当一个二元变量取值 0/1 时,.mean() 给出经验概率 P^(Y=1)\hat P(Y=1)。这正是后续朴素贝叶斯估计参数的方法。

1.3 经验生还概率

通过手工计算两组乘客的生还率,我们立刻能感受到性别和舱位的巨大影响:一等舱女性的生还率往往超过 95%,而非一等舱男性则不到 20%。这种差异告诉我们:变量 SexFirstClass 携带了大量预测信息——它们正是机器学习模型应该利用的特征。


2. 朴素贝叶斯分类器

2.1 贝叶斯定理

给定观测 xx,分类的核心目标是估计后验概率:

P(Y=yX=x)=P(Y=y)P(X=xY=y)P(X=x)P(Y = y \mid X = x) = \frac{P(Y=y) \, P(X=x \mid Y=y)}{P(X=x)}

由于分母 P(X=x)P(X=x) 与类别 yy 无关,预测时可以省略,于是:

P(Y=yX=x)P(Y=y)P(X=xY=y)P(Y = y \mid X = x) \propto P(Y = y)\, P(X = x \mid Y = y)

2.2 朴素假设

直接估计联合分布 P(X1,,XdY)P(X_1, \dots, X_d \mid Y) 在维度增加时迅速变得不可行。朴素贝叶斯通过一个强假设大幅简化问题:

P(X1,,XdY)=j=1dP(XjY)P(X_1, \dots, X_d \mid Y) = \prod_{j=1}^{d} P(X_j \mid Y)

也就是说,给定类别 YY,各特征之间相互独立

关键点 — 朴素假设:这个假设几乎在现实中从未严格成立(一等舱里的女性比例自然偏高,性别与舱位并不独立)。然而即使假设被违反,朴素贝叶斯往往依然给出令人惊讶的好结果,是一个非常稳健、训练极快的基线模型。

2.3 最大后验决策

预测时,我们选择使后验最大的类别:

y^=argmaxy  P(Y=y)j=1dP(XjY=y)\hat y = \arg\max_y \; P(Y=y) \prod_{j=1}^{d} P(X_j \mid Y=y)

这被称作 MAP 估计 (Maximum A Posteriori)。

2.4 伯努利朴素贝叶斯

当所有特征都是二元变量时(正如 titanic_mini),合适的条件分布是伯努利分布:

P(Xj=1Y=y)=θjy,P(Xj=0Y=y)=1θjyP(X_j = 1 \mid Y = y) = \theta_{j \mid y}, \qquad P(X_j = 0 \mid Y = y) = 1 - \theta_{j \mid y}

对于一个观测 x{0,1}dx \in \{0, 1\}^d

P(X=xY=y)=j=1dθjyxj(1θjy)1xjP(X = x \mid Y = y) = \prod_{j=1}^{d} \theta_{j \mid y}^{x_j} \, (1 - \theta_{j \mid y})^{1 - x_j}

参数 θjy\theta_{j \mid y} 通过频率估计,并使用拉普拉斯平滑避免零概率:

θ^jy=Nj=1,y+αNy+2α\hat\theta_{j \mid y} = \frac{N_{j=1, y} + \alpha}{N_y + 2\alpha}

其中 α\alphasklearn 中的 alpha 参数,默认为 1。

2.5 在 sklearn 中的实现

from sklearn.naive_bayes import BernoulliNB model = BernoulliNB(alpha=1.0) model.fit(X_train, y_train) y_hat = model.predict(X_test)

完整的训练流水线只需四行:拆分、构造、训练、预测。如此简单的接口正是 sklearn 设计哲学的体现——所有模型共享 .fit() / .predict() API。


3. 分类评价指标

3.1 为什么 MAE/RMSE 不再适用?

第二章的回归指标(MAE、RMSE、R2R^2)在分类中失去了语义。把"罹难"与"幸存"分别编码为 0 和 1 时,两者之间的"距离"并无物理意义;MAPE 在 y=0y=0 时还会出现除零错误。我们需要全新的、基于计数的指标。

3.2 准确率

准确率 (accuracy) 是最直观的指标:

Accuracy=正确预测的样本数样本总数\text{Accuracy} = \frac{\text{正确预测的样本数}}{\text{样本总数}}

在 0/1 编码下,可以等价地写成:

Accuracy=11ni=1ny^iyi\text{Accuracy} = 1 - \frac{1}{n} \sum_{i=1}^{n} |\hat y_i - y_i|
from sklearn.metrics import accuracy_score accuracy_score(y_test, y_hat)

但准确率有一个致命缺陷:在类别失衡的数据上极具误导性。如果 95% 的邮件不是垃圾邮件,那么"全部预测为非垃圾"的模型也能取得 95% 的准确率,却毫无实用价值。

3.3 混淆矩阵

为了细化分析,我们引入混淆矩阵 (confusion matrix):

预测 0预测 1真实 0TNFP真实 1FNTP\begin{array}{c|cc} & \text{预测 } 0 & \text{预测 } 1 \\ \hline \text{真实 } 0 & TN & FP \\ \text{真实 } 1 & FN & TP \end{array}

四个量分别是:真阴性 TNTN、假阳性 FPFP、假阴性 FNFN、真阳性 TPTP。所有后续的指标都建立在它们之上。

from sklearn.metrics import confusion_matrix confusion_matrix(y_test, y_hat)

3.4 精确率、召回率与 F1

精确率 (precision) 衡量"阳性预测的可靠性":

Precision=TPTP+FP\text{Precision} = \frac{TP}{TP + FP}

召回率 (recall) 衡量"对真实阳性的覆盖程度":

Recall=TPTP+FN\text{Recall} = \frac{TP}{TP + FN}

F1 分数 是两者的调和平均:

F1=2PrecisionRecallPrecision+Recall\text{F1} = 2 \cdot \frac{\text{Precision} \cdot \text{Recall}}{\text{Precision} + \text{Recall}}

如何选择? 当假阳性代价高(比如把好邮件误判为垃圾邮件)时,关注 precision;当假阴性代价高(比如漏诊癌症)时,关注 recall;如果两者同等重要,使用 F1。

3.5 分类报告

classification_report 一次性给出每个类别的 precision、recall、F1 和支持样本数:

from sklearn.metrics import classification_report print(classification_report(y_test, y_hat))

这是面试与项目汇报中最常用的诊断工具。


4. 概率分数与决策阈值

4.1 predict_proba

许多分类器(朴素贝叶斯、逻辑回归、随机森林……)实际上输出的是正类的概率,而非直接的类标签:

p^=P(Y=1X)\hat p = P(Y = 1 \mid X)
probas = model.predict_proba(X_test) # shape (n, 2) y_score = probas[:, 1] # 正类概率

4.2 决策阈值

从概率到类别需要一个阈值 t[0,1]t \in [0, 1]

y^={1若 p^t0否则\hat y = \begin{cases} 1 & \text{若 } \hat p \ge t \\ 0 & \text{否则} \end{cases}

sklearn 默认 t=0.5t = 0.5,但这并非神圣不可侵犯。改变阈值会重新平衡 precision 与 recall:

  • 提高阈值 → 阳性预测变少 → precision 上升、recall 下降;
  • 降低阈值 → 阳性预测变多 → recall 上升、precision 下降。
t = 0.7 y_hat = (y_score >= t).astype(int)

阈值的选择应由业务成本驱动。例如在癌症筛查中,宁可让一些良性肿瘤进入复检,也不能漏掉一个恶性病例——此时合适的阈值通常远低于 0.5。


5. ROC 曲线与 AUC

5.1 阈值无关的评估

固定阈值评估只能反映模型在某个工作点的表现。ROC 曲线 通过遍历所有可能的阈值,给出与阈值选择无关的整体评价。

5.2 TPR 与 FPR

对每个阈值 tt,定义:

TPR(t)=TP(t)TP(t)+FN(t)(即 recall)\text{TPR}(t) = \frac{TP(t)}{TP(t) + FN(t)} \quad \text{(即 recall)} FPR(t)=FP(t)FP(t)+TN(t)\text{FPR}(t) = \frac{FP(t)}{FP(t) + TN(t)}

ROC 曲线把 (FPR(t),TPR(t))\big(\text{FPR}(t), \text{TPR}(t)\big) 作为点画出来,阈值从 1 扫到 0。

5.3 解读

  • (0,0)(0, 0):阈值极高,没有阳性预测;
  • (1,1)(1, 1):阈值极低,全部预测为阳性;
  • 对角线 TPR=FPR\text{TPR} = \text{FPR}:随机分类器;
  • 曲线越靠近左上角,模型越好。

5.4 AUC

AUC (Area Under Curve) 是 ROC 曲线下方的面积,取值于 [0,1][0, 1]

AUC=01TPR(FPR1(u))du\text{AUC} = \int_0^1 \text{TPR}(\text{FPR}^{-1}(u)) \, du

AUC 的概率解释:随机抽取一个正样本和一个负样本,AUC 等于"模型给正样本打分高于负样本的概率"。

经验性的判读标尺:

  • AUC0.5\text{AUC} \approx 0.5:随机;
  • 0.70.80.7 \sim 0.8:可用;
  • 0.80.90.8 \sim 0.9:良好;
  • >0.9> 0.9:优秀。
from sklearn.metrics import roc_curve, roc_auc_score y_score = model.predict_proba(X_test)[:, 1] fpr, tpr, thresholds = roc_curve(y_test, y_score) auc = roc_auc_score(y_test, y_score)

注意:ROC 与 AUC 必须以概率分数计算,而不是以离散的预测标签计算。


6. 决策树

6.1 直观图景

决策树通过一连串关于特征的"是非问题"将样本路由到叶子节点。每一次分裂都试图让子节点变得更纯——理想情况下每个叶子只包含一个类别。

6.2 基尼指数

对于二元目标 Y{0,1}Y \in \{0, 1\}基尼不纯度 (Gini impurity) 定义为:

G=2p(1p),p=P(Y=1)G = 2p(1-p), \quad p = P(Y = 1)

其性质:

  • p=0.5p = 0.5GG 取最大值 0.50.5(最不纯);
  • p=0p = 0p=1p = 1G=0G = 0(完全纯)。

6.3 选择切分变量

在节点 DD 上,对每个候选变量 XX

  1. XX 的取值划分 DD 为子集 DkD_k
  2. 计算每个子集的基尼 GkG_k
  3. 计算加权平均: G(YX)=kDkDGkG(Y \mid X) = \sum_k \frac{|D_k|}{|D|} \, G_k
  4. 选择使 G(YX)G(Y \mid X) 最小的变量进行分裂。

6.4 数值变量的切分

对于 cancer_mini 中的连续变量 textureradius 等,分裂规则形如 XtX \le t。算法会枚举所有可能的阈值(通常取相邻取值的中点),选择令子节点不纯度最小化的 tt。这就是为什么决策树 对连续变量自动进行离散化,无需用户手动分箱。

6.5 停止准则

  • 完全纯净G=0G = 0,节点变成叶子;
  • 无增益:所有候选切分都不能降低基尼;
  • 结构限制max_depth(最大深度)、min_samples_split(分裂最小样本数)、min_samples_leaf(叶子最小样本数)。

这些超参数控制过拟合:树越深越容易记忆训练集中的噪声。

6.6 用 sklearn 训练与可视化

from sklearn.tree import DecisionTreeClassifier, plot_tree, export_text model = DecisionTreeClassifier(max_depth=3) model.fit(X_train, y_train) plot_tree(model, feature_names=X.columns, class_names=["0", "1"], filled=True, rounded=True, impurity=True) print(export_text(model, feature_names=list(X.columns)))

plot_tree 给出图形树,每个节点显示分裂规则、基尼值、样本数和类别分布;export_text 输出文本规则。

决策树的最大优势:可解释性。一棵深度为 3 的树可以直接讲给业务方听:"如果是儿童 → 幸存;否则如果是女性 → 幸存……"。


7. 交叉验证

单次 train/test 划分的评估结果会受到划分随机性的影响。KK 折交叉验证 (cross-validation) 把数据切成 KK 份,轮流让每一份充当测试集,最后取平均:

from sklearn.model_selection import cross_val_score scores = cross_val_score(model, X, y, cv=10, scoring="accuracy") print(scores.mean(), scores.std())

通过遍历 max_depth,配合 cross_val_score,可以画出"验证准确率—深度"曲线,找到偏差—方差权衡的最优点。


8. 集成方法

单棵决策树的方差大、对噪声敏感。集成学习 (ensemble learning) 通过组合多棵树解决这一问题。

8.1 随机森林

随机森林在两个层面引入随机性:

  1. Bootstrap 采样:每棵树用一个有放回的随机样本训练;
  2. 特征子采样:每个节点只在随机选出的一部分变量中找最佳切分。

预测时分类取多数投票,回归取平均。

from sklearn.ensemble import RandomForestClassifier model = RandomForestClassifier(n_estimators=200, max_depth=5) model.fit(X_train, y_train)

主要超参数:n_estimators(树的数量)、max_depthmax_features(每节点候选变量数)、min_samples_leafbootstrap。增大 n_estimators 通常稳定模型而不会引发过拟合。

8.2 梯度提升

梯度提升 (Gradient Boosting) 与随机森林截然不同。它顺序地构造一系列浅树,每棵新树修正前面所有树的累计残差

最终预测 = 累加的逐步修正,每步修正本身是一棵简单的树。

学习率 (learning_rate) 控制每次修正的强度,通常推荐"小学习率 + 多树"的组合。

from sklearn.ensemble import GradientBoostingClassifier model = GradientBoostingClassifier( n_estimators=200, learning_rate=0.05, max_depth=5 )
对比维度随机森林梯度提升
树间关系独立序列依赖
训练可并行必须顺序
聚合投票/平均累加修正
过拟合风险较高(需调 learning rate)

8.3 LightGBM 与 XGBoost

LightGBMXGBoost 是梯度提升的工业级实现,在 Kaggle 表格数据竞赛中长期占据榜首。

LightGBM 的关键创新是按叶生长 (leaf-wise):每次只展开能最大降低损失的那片叶子,而非按深度逐层扩张。这让它训练更快、树更紧凑。

from lightgbm import LGBMClassifier model = LGBMClassifier(n_estimators=300, learning_rate=0.05, verbosity=-1)

XGBoost 在标准梯度提升基础上加入 L1/L2 显式正则、近似切分算法、二阶 Taylor 展开等多项优化,对过拟合具有更强的控制力。

from xgboost import XGBClassifier model = XGBClassifier(n_estimators=300, learning_rate=0.05, max_depth=3)

XGBoost 还支持 GPU 加速,只需设置 tree_method="gpu_hist"

cancer_mini 上对四种集成模型作 10 折交叉验证比较,通常会得到非常接近的高准确率(0.95 左右)。这说明在干净的小数据集上,模型选择不是瓶颈;而在大型、含噪声的真实数据上,LightGBM/XGBoost 的工程优化才会真正体现价值。


练习

  1. 经验概率:在 titanic_mini 上,分别计算"一等舱女性"和"非一等舱男性"两个子群体的生还率,比较两个值。
  2. 朴素贝叶斯基线:用 BernoulliNB 训练 titanic_mini 分类器,输出 accuracy_scoreclassification_report,并解释回归指标(MAE/RMSE)为何不适用。
  3. 手算 TPR(0.7):给定 y_testy_score,在阈值 t=0.7t = 0.7 下手动计算 TPTPFPFPFNFNTNTN,由此得到 TPR(0.7)TPR(0.7)FPR(0.7)FPR(0.7);用 roc_curve 画出完整 ROC 曲线,并把上述点叠加到图上。
  4. 基尼计算:在 titanic_mini 上分别按 SexChildrenFirstClass 切分,计算切分后的加权基尼,找出基尼最小的变量,验证它就是 DecisionTreeClassifier 选作根节点的变量。
  5. 可视化决策树:在 titanic_minicancer_mini 上各训练一棵 max_depth=3 的决策树,用 plot_treeexport_text 显示结构与规则。
  6. 深度调优:遍历 max_depth ∈ {1, 2, …, 29},用 10 折交叉验证记录平均准确率,画出曲线并找出最优深度。讨论欠拟合与过拟合区间。
  7. 集成大比拼:在 cancer_mini 上以 10 折交叉验证比较 RandomForest、GradientBoosting、LightGBM、XGBoost 四种模型的准确率均值与标准差。
  8. 阈值调优:从 BernoulliNBpredict_proba,在 t{0.3,0.5,0.7}t \in \{0.3, 0.5, 0.7\} 下分别评估 precision、recall、F1,讨论业务场景(漏判恶性肿瘤 vs 误判良性肿瘤)下应如何取舍阈值。

拓展阅读