☰
Python Machine Learning 第6章实战指南:模型评估与超参数调优的最佳实践
2026/9/29 7:53:43 网站建设 项目流程
  • 示例工程
  • 机器学习
  • 深度学习

【免费下载链接】python-machine-learning-book-2nd-edition

The "Python Machine Learning (2nd edition)" book code repository and info resource

项目地址:https://gitcode.com/gh_mirrors/py/python-machine-learning-book-2nd-edition
点击查看免费下载

本指南以《Python Machine Learning(第2版)》第6章为基础,围绕机器学习建模中最关键的一环——如何科学地评估模型性能并调优超参数——展开。文中所有代码均来自本仓库 code/ch06/ch06.py 与 ch06.ipynb,并以威斯康星乳腺癌(WDBC)数据集为实战对象。读完本文,你将掌握 Pipeline 流水线、k-fold 交叉验证、学习曲线与验证曲线、网格搜索与嵌套交叉验证、混淆矩阵与 ROC 曲线,以及类别不平衡问题的处理手段,构建出一套可复用的模型评估与调优工作流。

第6章导览

第6章聚焦于一个几乎所有机器学习项目都会遇到的问题:模型在训练集上表现好,不代表泛化能力强。因此,需要一套严谨的方法论来回答"我的模型到底行不行、参数怎么调、指标怎么选"。本章的完整脉络如下(与 ch06.py 顶部的章节大纲一一对应):

  • 用 Pipeline 精简工作流:加载 Breast Cancer Wisconsin 数据集、在 Pipeline 中组合转换器(transformer)与估计器(estimator);
  • 用 k-fold 交叉验证评估模型性能:holdout 方法回顾、k-fold 交叉验证;
  • 用学习曲线与验证曲线调试算法:诊断偏差(bias)与方差(variance)问题、处理过拟合与欠拟合;
  • 用网格搜索微调模型:超参数网格搜索、嵌套交叉验证进行算法选择;
  • 考察不同的性能评估指标:混淆矩阵、精确率与召回率优化、ROC 曲线、多分类评分指标;
  • 处理类别不平衡(class imbalance);
  • 小结。

本仓库为本章提供了两种可执行载体:ch06.ipynb(Jupyter Notebook,可逐步执行并内嵌图表输出)与 ch06.py(纯 Python 脚本,可用任意文本编辑器查看)。此外 tests/test_notebooks.py 中的test_ch06会通过jupyter nbconvert --execute完整执行该 notebook,保证示例代码可复现。

环境准备与运行方式

安装 Jupyter Notebook

本章示例以 Jupyter Notebook 为推荐载体,可以逐格执行代码并把所有输出(含绘图)集中在一份文档中。安装方式有两种:

  • 使用 Anaconda 发行版时,执行conda install jupyter notebook;
  • 使用 pip 时,执行pip install jupyter notebook(详见 code/ch01/README.md 的环境说明)。

安装后进入code/ch06目录,执行jupyter notebook,浏览器会打开一个窗口(默认运行于http://localhost:8888/),在其中选择ch06.ipynb即可逐格运行。

依赖库与版本

运行本章代码需要的核心 Python 包及其最低版本(按 code/ch01/README.md 的记录)为:

  • NumPy >= 1.12.1
  • SciPy >= 0.19.0
  • scikit-learn >= 0.18.1
  • matplotlib >= 2.0.2
  • pandas >= 0.20.1

notebook 的环境记录显示其作者使用 CPython 3.7.1 + scikit-learn 0.21.3 运行通过,因此建议使用 Python 3 环境并保证上述版本不低于所列数值。

用 Pipeline 精简建模工作流

加载 Breast Cancer Wisconsin 数据集

本章采用威斯康星乳腺癌诊断(WDBC)数据集。按 code/ch06/wdbc.names.txt 的说明,该数据集包含 569 个样本、32 个属性(ID、诊断标签、30 个实数特征),类别分布为 357 个良性(benign)、212 个恶性(malignant),无缺失值。30 个特征是从细针穿刺(FNA)细胞核数字图像中计算的半径、纹理、周长、面积、平滑度、紧凑度、凹度、凹点、对称性、分形维数这 10 个量的均值、标准误与"最差值"(worst)三组统计量。

ch06.py 中从 UCI 仓库读取数据,若在线源临时不可用,可改用仓库内自带的本地副本wdbc.data:

df = pd.read_csv('https://archive.ics.uci.edu/ml/' 'machine-learning-databases' '/breast-cancer-wisconsin/wdbc.data', header=None) # 若 UCI 仓库暂时不可用,取消下一行注释以从本地路径加载: # df = pd.read_csv('wdbc.data', header=None)

随后把特征与标签分离,并用LabelEncoder把字符串标签M(恶性)/B(良性)编码为数值:

X = df.loc[:, 2:].values # 第 2 列之后为 30 维特征 y = df.loc[:, 1].values # 第 1 列为诊断标签 le = LabelEncoder() y = le.fit_transform(y) le.classes_ # array(['B', 'M']) le.transform(['M', 'B']) # array([1, 0])

注意编码结果:恶性(M)编码为 1,良性(B)编码为 0,这一约定在后续混淆矩阵与 ROC 的分析中扮演"正类"定义的角色。

按分层抽样的方式切分训练集与测试集(保持类别比例,保证训练/测试集中恶性样本占比一致):

X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.20, stratify=y, random_state=1)

在 Pipeline 中组合转换器与估计器

许多实际场景中,特征标准化、降维、模型训练必须按固定顺序执行。Pipeline 将这一串步骤封装为一个整体,fit/predict调用会自动贯穿所有环节,避免数据泄露(例如:缩放器只在训练数据上拟合,再对测试数据做同样的变换)。ch06.py 构造了"标准化 → PCA 降维 → 逻辑回归"的流水线:

pipe_lr = make_pipeline(StandardScaler(), PCA(n_components=2), LogisticRegression(random_state=1)) pipe_lr.fit(X_train, y_train) y_pred = pipe_lr.predict(X_test) print('Test Accuracy: %.3f' % pipe_lr.score(X_test, y_test))

make_pipeline会自动为每个步骤生成小写类名加序号的命名(如standardscaler、pca、logisticregression),后面网格搜索中就会使用这类命名来引用具体参数(见下文svc__C的写法)。使用 06_01.png 可以帮助直观理解 Pipeline 的两阶段流程:先对训练数据执行fit(含缩放、降维、学习)得到预测模型,再对测试数据调用predict输出类标签。

用 k-fold 交叉验证评估模型性能

holdout 方法回顾

简单的 holdout 方法把数据划分为训练集、验证集、测试集:用训练集训练模型,用验证集反复调整超参数,最后用从未参与训练的测试集评估最终性能。它的局限在于:一次随机划分的结果方差较大,评估结论对"哪部分数据恰好进入训练集"过于敏感。

k-fold 交叉验证

k-fold 交叉验证把训练数据等分成 k 个不相交的折(fold),轮流把其中一折作为验证集、其余 k-1 折作为训练集,共训练 k 次,最终以 k 次验证分数的均值与标准差来报告性能。06_02.png 展示了 holdout 划分与验证集在调参中的角色。

ch06.py 先用手写循环展示逐折过程,再用cross_val_score一行完成等价逻辑:

# 手写循环版:逐折打印类别分布与准确率 kfold = StratifiedKFold(n_splits=10, random_state=1).split(X_train, y_train) scores = [] for k, (train, test) in enumerate(kfold): pipe_lr.fit(X_train[train], y_train[train]) score = pipe_lr.score(X_train[test], y_train[test]) scores.append(score) print('Fold: %2d, Class dist.: %s, Acc: %.3f' % (k+1, np.bincount(y_train[train]), score)) print('\nCV accuracy: %.3f +/- %.3f' % (np.mean(scores), np.std(scores))) # cross_val_score 版:一行完成 10 折交叉验证 scores = cross_val_score(estimator=pipe_lr, X=X_train, y=y_train, cv=10, n_jobs=1) print('CV accuracy scores: %s' % scores) print('CV accuracy: %.3f +/- %.3f' % (np.mean(scores), np.std(scores)))

关键参数说明:

  • cv=10:折数 k。k 越大,单次训练数据越多、评估越稳定,但计算成本线性上升;StratifiedKFold保证每一折的类别比例与整体一致,对类别不平衡的数据尤为重要;
  • n_jobs=1:并行核数,可设为-1使用全部 CPU 核以加速;
  • 与普通KFold相比,分层 k-fold(StratifiedKFold)在分类任务中能显著降低因随机划分导致的评估波动。

用学习曲线与验证曲线调试算法

用学习曲线诊断偏差与方差

学习曲线绘制"训练集大小 → 训练/验证准确率"的关系,是诊断偏差-方差问题的标准工具。对同一个固定模型(此处的pipe_lr只做标准化 + L2 逻辑回归,不再降维),按不同规模的训练子集重复 10 折交叉验证,并绘制均值与标准差带:

pipe_lr = make_pipeline(StandardScaler(), LogisticRegression(penalty='l2', random_state=1)) train_sizes, train_scores, test_scores = learning_curve( estimator=pipe_lr, X=X_train, y=y_train, train_sizes=np.linspace(0.1, 1.0, 10), # 10% 到 100% 的训练样本比例 cv=10, n_jobs=1) train_mean = np.mean(train_scores, axis=1) train_std = np.std(train_scores, axis=1) test_mean = np.mean(test_scores, axis=1) test_std = np.std(test_scores, axis=1)

随后的绘图代码(ch06.py)用蓝色实线画训练准确率、绿色虚线画验证准确率,并用fill_between画出 ±1 标准差带:

  • 高偏差(欠拟合):训练与验证准确率都很低且彼此接近,曲线趋于平坦——说明模型容量不足,增加数据量也难有明显改善;
  • 高方差(过拟合):训练准确率远高于验证准确率,且二者之间存在宽大的"沟"——说明模型记住了训练数据,需要正则化、更多数据或更简单的模型;
  • 健康形态:两条曲线随样本增多逐渐收敛并趋于一致。

用验证曲线处理过拟合与欠拟合

验证曲线固定训练数据、扫描单个超参数,帮助定位正则化强度的合适区间。本章扫描逻辑回归的 L2 正则化系数C(C 越小正则化越强):

param_range = [0.001, 0.01, 0.1, 1.0, 10.0, 100.0] train_scores, test_scores = validation_curve( estimator=pipe_lr, X=X_train, y=y_train, param_name='logisticregression__C', # Pipeline 内步骤参数用 "步骤名__参数名" 引用 param_range=param_range, cv=10)

注意param_name='logisticregression__C':当估计器被包在 Pipeline 中时,scikit-learn 用步骤名__参数名的双下划线语法定位内部步骤的参数。绘图时使用plt.xscale('log')把横轴设为对数刻度(ch06.py),因为 C 的取值跨越多个数量级。解读方法:

  • C 过小(强正则化)时两者准确率同时走低 → 欠拟合;
  • C 过大(弱正则化)时训练准确率高、验证准确率回落 → 过拟合;
  • 选择验证准确率最高且与训练准确率差距最小的 C 区间。

用网格搜索微调模型

通过网格搜索调优超参数

网格搜索(GridSearchCV)在预定义的参数组合上执行交叉验证,自动选出使评分最优的参数组合。本章对"标准化 + SVM"的 Pipeline 同时搜索线性核与 RBF 核两种配置:

pipe_svc = make_pipeline(StandardScaler(), SVC(random_state=1)) param_range = [0.0001, 0.001, 0.01, 0.1, 1.0, 10.0, 100.0, 1000.0] param_grid = [{'svc__C': param_range, 'svc__kernel': ['linear']}, {'svc__C': param_range, 'svc__gamma': param_range, 'svc__kernel': ['rbf']}] gs = GridSearchCV(estimator=pipe_svc, param_grid=param_grid, scoring='accuracy', cv=10, n_jobs=-1) gs = gs.fit(X_train, y_train) print(gs.best_score_) print(gs.best_params_)

要点解读:

  • param_grid传的是列表而非字典:两个字典分别代表"线性核只调 C"与"RBF 核同时调 C 和 gamma"两组搜索空间,二者取并集,避免为线性核浪费 gamma 的搜索组合;
  • scoring='accuracy'指定优化目标(下文会换成自定义评分器);
  • cv=10表示每组参数做 10 折交叉验证,n_jobs=-1使用全部 CPU 并行;
  • 结果对象暴露best_score_(交叉验证最优得分)与best_params_(最优参数组合)。

得到最优参数后,直接使用gs.best_estimator_评估测试集即可:

clf = gs.best_estimator_ print('Test accuracy: %.3f' % clf.score(X_test, y_test))

ch06.py 特别注释说明:无需手动clf.fit(X_train, y_train),因为GridSearchCV默认refit=True,会在搜索结束后自动用最优参数在完整训练集上重新拟合best_estimator_。

用嵌套交叉验证进行算法选择

网格搜索在训练集上做交叉验证选参数,如果再用同一份数据报告最终性能,会引入"优化偏差"——测试结果被参数选择过程"泄漏"。嵌套交叉验证(nested cross-validation)把调参与评估分层:外层 k 折负责评估泛化性能,内层折负责参数搜索。

gs = GridSearchCV(estimator=pipe_svc, param_grid=param_grid, scoring='accuracy', cv=2) scores = cross_val_score(gs, X_train, y_train, scoring='accuracy', cv=5) print('CV accuracy: %.3f +/- %.3f' % (np.mean(scores), np.std(scores)))

这里cross_val_score的外层cv=5把数据切成 5 份,每一份轮流做验证,而其内部的GridSearchCV用cv=2在剩余数据上搜索参数。同样的流程可以比较不同算法——ch06.py 用DecisionTreeClassifier搜索max_depth的取值,与 SVM 的嵌套交叉验证得分对比,从而在同一评估协议下完成算法选择。06_07.png 直观展示了这种"外层循环评估 + 内层循环调参"的嵌套结构。

考察不同的性能评估指标

阅读混淆矩阵

仅看准确率可能被类别不平衡掩盖。混淆矩阵按"真实类别 × 预测类别"列出四种计数。先在训练集上拟合 Pipeline 并在测试集上预测:

pipe_svc.fit(X_train, y_train) y_pred = pipe_svc.predict(X_test) confmat = confusion_matrix(y_true=y_test, y_pred=y_pred) print(confmat)

由于此前LabelEncoder把良性编码为 0、恶性编码为 1,在默认排序(0 在前)下矩阵的解读为(结合 ch06.py 的结论):模型正确分类了 71 个良性样本(真阴性,TN)与 40 个恶性样本(真阳性,TP),同时把 1 个良性误判为恶性(假阳性,FP),把 2 个恶性误判为良性(假阴性,FN)。

若希望真阴性显示在矩阵右下角、真阳性显示在左上角,可显式传入labels参数调整顺序:

confmat = confusion_matrix(y_true=y_test, y_pred=y_pred, labels=[1, 0])

优化分类模型的精确率与召回率

在"恶性乳腺肿瘤"这类场景中,漏诊(假阴性)的代价极高,单纯优化准确率并不合理。精确率(precision)、召回率(recall)与 F1 分数分别刻画"预测为正类中有多少是真的"、"正类中有多少被找出"及二者的调和平均:

print('Precision: %.3f' % precision_score(y_true=y_test, y_pred=y_pred)) print('Recall: %.3f' % recall_score(y_true=y_test, y_pred=y_pred)) print('F1: %.3f' % f1_score(y_true=y_test, y_pred=y_pred))

默认以标签 1(恶性)为正类。若要改变正类定义,或把 F1 作为网格搜索的优化目标,可用make_scorer包装自定义评分器:

scorer = make_scorer(f1_score, pos_label=0) c_gamma_range = [0.01, 0.1, 1.0, 10.0] param_grid = [{'svc__C': c_gamma_range, 'svc__kernel': ['linear']}, {'svc__C': c_gamma_range, 'svc__gamma': c_gamma_range, 'svc__kernel': ['rbf']}] gs = GridSearchCV(estimator=pipe_svc, param_grid=param_grid, scoring=scorer, cv=10, n_jobs=-1) gs = gs.fit(X_train, y_train) print(gs.best_score_) print(gs.best_params_)

绘制 ROC 曲线

ROC 曲线以假阳性率(FPR)为横轴、真阳性率(TPR)为纵轴,反映分类器在所有可能阈值下的判别能力,曲线下面积(AUC)是阈值无关的综合度量。本章用 3 折分层交叉验证重复绘制并取均值曲线:

pipe_lr = make_pipeline(StandardScaler(), PCA(n_components=2), LogisticRegression(penalty='l2', random_state=1, C=100.0)) X_train2 = X_train[:, [4, 14]] # 只取两个特征便于可视化 cv = list(StratifiedKFold(n_splits=3, random_state=1).split(X_train, y_train)) mean_tpr = 0.0 mean_fpr = np.linspace(0, 1, 100) for i, (train, test) in enumerate(cv): probas = pipe_lr.fit(X_train2[train], y_train[train]).predict_proba(X_train2[test]) fpr, tpr, thresholds = roc_curve(y_train[test], probas[:, 1], pos_label=1) mean_tpr += interp(mean_fpr, fpr, tpr) mean_tpr[0] = 0.0 roc_auc = auc(fpr, tpr) plt.plot(fpr, tpr, label='ROC fold %d (area = %0.2f)' % (i+1, roc_auc))

绘图部分(ch06.py)还会叠加三条参考线:

  • random guessing:对角线(虚线),代表完全随机猜测,AUC = 0.5;
  • mean ROC:三折 TPR 的均值曲线及其 AUC(黑色虚线);
  • perfect performance:经过 (0,1) 与 (1,1) 的阶梯线(点线),代表理想分类器。

曲线越向左上角弯曲、AUC 越接近 1.0,说明模型能在低假阳性率下取得高真阳性率;predict_proba输出概率后由roc_curve扫描所有阈值生成曲线,因此该指标不依赖某个固定决策阈值。

多分类评分指标

当面对多于两个类别时,需要说明如何聚合各类别的指标。ch06.py 展示了用average参数控制聚合方式的示例:

pre_scorer = make_scorer(score_func=precision_score, pos_label=1, greater_is_better=True, average='micro')

average的常见取值包括:'micro'(把所有类别的 TP/FP 汇总后计算,受大类样本数影响)、'macro'(各类别指标先算后平均,平等对待每个类别)、'weighted'(按各类样本占比加权平均)。选择哪种聚合方式取决于业务上是否要求小类别与大众类获得同等关注。

处理类别不平衡

现实数据中正负样本常常严重失衡。本章演示了"多数类欠采样 + 少数类过采样"的思路:先构造一个极端不平衡的子集(保留全部 357 个良性样本、只取 40 个恶性样本),然后观察一个"永远预测为多数类"的基线模型会得到多高的准确率,借此说明准确率在类别不平衡下具有误导性:

X_imb = np.vstack((X[y == 0], X[y == 1][:40])) y_imb = np.hstack((y[y == 0], y[y == 1][:40])) y_pred = np.zeros(y_imb.shape[0]) # 全部预测为良性 np.mean(y_pred == y_imb) * 100 # 基线"准确率"依旧很高

随后用自助法(bootstrap)对少数类做有放回重采样(过采样),直到两类样本数一致:

print('Number of class 1 samples before:', X_imb[y_imb == 1].shape[0]) X_upsampled, y_upsampled = resample(X_imb[y_imb == 1], y_imb[y_imb == 1], replace=True, n_samples=X_imb[y_imb == 0].shape[0], random_state=123) print('Number of class 1 samples after:', X_upsampled.shape[0]) X_bal = np.vstack((X[y == 0], X_upsampled)) y_bal = np.hstack((y[y == 0], y_upsampled))

参数说明:replace=True表示有放回抽样,n_samples指定目标样本数(这里设为多数类数量,使两类完全均衡),random_state固定随机种子保证可复现。重采样后再次计算"全部预测为良性"的准确率,会明显下降,从而暴露单纯用准确率评估不平衡数据的陷阱——正确做法是配合本章前面的精确率、召回率、F1、ROC/AUC 等指标综合评估。

小结

第6章建立了一套完整的模型评估与超参数调优工作流:用 Pipeline 封装数据变换与模型训练以防止数据泄露;用分层 k-fold 交叉验证获得稳健的性能估计;用学习曲线与验证曲线定位偏差/方差问题并确定正则化强度;用网格搜索与嵌套交叉验证在避免优化偏差的前提下寻找最优参数、比较算法;再用混淆矩阵、精确率/召回率/F1、ROC/AUC 从不同业务视角评估模型;最后针对类别不平衡问题给出重采样与指标选择的处理建议。

本章所有代码均可在 ch06.py 与 ch06.ipynb 中直接运行验证,数据集本地副本位于 code/ch06/wdbc.data,数据说明见 code/ch06/wdbc.names.txt。这套方法同样适用于本仓库第7章及后续章节中的分类与深度学习模型,是构建可信赖机器学习系统的基石。

  • 示例工程
  • 机器学习
  • 深度学习

【免费下载链接】python-machine-learning-book-2nd-edition

The "Python Machine Learning (2nd edition)" book code repository and info resource

项目地址:https://gitcode.com/gh_mirrors/py/python-machine-learning-book-2nd-edition
点击查看免费下载

相关推荐

上一篇:PaddleNLP 中 FNet 模型建模详解:基于傅里叶变换的无注意力编码器实现
下一篇:颠覆传统标注体验:让屏幕交互效率提升10倍的开源工具ppInk

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询