系列三收官。前面几课在案例平台上立起了分类、回归、聚类三类模型,但每个模型都还是”实验室状态”——参数是拍的、评估是单次的、没有正式的验收。这一课把它们做成可交付的状态,完成里程碑 M2:论文领域分类器的最终验收。
前置阅读:系列三前四课、深度学习课程 10:模型评估与部署闭环(评估闭环的深度学习版本,思路相通)。
系统调参:网格、随机与分层搜索
以领域分类的逻辑回归管道为例,把调参从”手动试几个值”升级成”定义搜索空间”:
from sklearn.model_selection import GridSearchCV
param_grid = {
"tfidf__max_features": [10000, 20000, 40000],
"tfidf__ngram_range": [(1, 1), (1, 2)],
"tfidf__min_df": [3, 5, 10],
"clf__C": [0.1, 1, 10, 100],
}
search = GridSearchCV(pipe, param_grid, cv=5,
scoring="f1_macro", n_jobs=-1, verbose=1)
search.fit(train["text_all"], train["field"])
网格 × 5 折 × 上面这个空间 = 360 次训练,文本数据上可能要跑几小时。两个提速手段:
随机搜索(RandomizedSearchCV):在指定分布中抽取参数组合,用 n_iter 控制试验预算。它可以在同样预算下探索更多不同取值,但效果取决于搜索空间和参数分布,不能用维度数直接保证接近最优。比较搜索方法时,应保持试验预算与交叉验证划分一致。
分层搜索:先粗后细。第一轮对数刻度扫 C ∈ [0.01, 100] 找数量级,第二轮在最优值附近线性细分。特征参数(ngram、min_df)先单独定,再调模型参数——特征和模型参数联动搜索的组合爆炸不值得。
调参结果全部进实验记录(研究方法的纪律),包括没赢的参数组合——它们证明了搜索的充分性。
学习曲线:诊断该加数据还是该换模型
分数上不去时,先别急着换模型,画学习曲线诊断问题性质:
from sklearn.model_selection import learning_curve
sizes, train_scores, val_scores = learning_curve(
search.best_estimator_, train["text_all"], train["field"],
train_sizes=np.linspace(0.1, 1.0, 8), cv=5, scoring="f1_macro")
两条线提供诊断线索,需要结合标签质量、特征覆盖与数据分布验证:
- 训练分 ≈ 验证分,都低:可能是欠拟合,也可能是标签噪声或训练失败。先检查数据和优化,再试特征或模型复杂度;不能仅凭这张图断言加数据无效。
- 训练分 ≫ 验证分:可能过拟合,也可能切分分布不同。正则化和更多有代表性的数据是候选方案,是否有效还需新一轮验证。
- 两条线收敛到同一水平且平缓:当前模型+特征的天花板到了,继续投入的边际收益递减,该去改进数据质量或重新定义任务。
学习曲线是”下一步往哪使劲”的决策工具。没有它,调参就是玄学。
最终评估:封存测试集只摸一次
系列三所有模型迭代都在训练集交叉验证上进行,现在到了验收时刻:拿出从第 2 课就封存、整个调参过程从未碰过的测试集,跑一次最终评估:
import numpy as np
from sklearn.metrics import classification_report, f1_score
final_model = search.best_estimator_
pred = final_model.predict(test["text_all"])
report = classification_report(test["field"], pred, output_dict=True)
# 同时输出置信度区间的自助法估计
rng = np.random.default_rng(42)
labels = sorted(train["field"].unique())
f1s = []
for _ in range(1000):
idx = rng.integers(0, len(test), size=len(test))
f1s.append(f1_score(test["field"].iloc[idx], pred[idx],
labels=labels, average="macro", zero_division=0))
print(f"宏 F1: {report['macro avg']['f1-score']:.3f}, "
f"95% CI: [{np.percentile(f1s, 2.5):.3f}, {np.percentile(f1s, 97.5):.3f}]")
这段代码依赖前文训练好的 search、train 和封存的 test,不是独立脚本。百分位区间一般不对称,报告时保留上下界;它估计的是固定模型在当前测试分布下的采样波动,不包括重新训练的随机性。若同一论文有多个版本或存在作者/时间相关性,应按组或时间设计重采样,不能直接假定每一行独立。小类别在 bootstrap 中可能缺失,必须固定标签集合并说明处理规则。
模型选择报告:M2 的核心交付物
下面是待填写的模型选择报告模板,不含实测成绩。用同一切分评估候选模型,保存每次运行的配置与原始输出,再填入表格:
| 模型 | 宏 F1 (CV) | 训练耗时 | 推理延迟 | 可解释性 | 结论 |
|---|---|---|---|---|---|
| TF-IDF + 逻辑回归 | 待测 | 待测 | 待测 | 可查看系数 | 待评估 |
| TF-IDF + LinearSVC | 待测 | 待测 | 待测 | 决策分数不是概率 | 待评估 |
| 元数据 + HGB | 待测 | 待测 | 待测 | 可另做特征分析 | 待评估 |
选择逻辑要写清楚:若平台需要概率做低置信降级,就比较概率校准、覆盖率与错误率,不能只比较 F1。LinearSVC 可以增加校准器,但会增加验证与计算成本;逻辑回归也不自动保证概率已校准。没有实际结果时,不预先宣布某个模型入选。
M2 验收清单
- 封存测试集最终评估完成,宏 F1 带置信区间。
- 模型选择报告归档(含未入选方案的分数与淘汰理由)。
- 模型序列化保存(
joblib.dump),README 写清加载方式与数据版本依赖(m1.0)。 - 置信度阈值的服务水平(覆盖率/准确率权衡)写进文档。
git tag m2-classifier。- 已知局限列出:相邻领域混淆、小类 F1 偏低、Embedding 改进路径留给系列四。
踩坑排查
| 症状 | 原因 | 处理 |
|---|---|---|
| 网格搜索跑了通宵 | 空间太大 × 全量数据 | 先子集粗扫,再全量精调 |
| 调参后测试分反降 | 测试集被反复使用导致过拟合 | 封存测试集只在验收时用 |
| 最优参数贴着搜索边界 | 边界设窄了 | 沿该方向扩展范围重搜 |
| CV 分数与最终测试分差很多 | 切分分布漂移 | StratifiedKFold;检查时间泄漏 |
| 换台机器结果变了 | 随机性未固定 | random_state 全链路固定 |
| joblib 加载后预测不一致 | 保存时环境版本不同 | 保存时记录 sklearn 版本 |
作品集证据
M2 是求职作品集里”经典机器学习”的完整证据链:特征工程(含泄漏审查)→ 多模型对比 → 系统调参 → 学习曲线诊断 → 带置信区间的最终评估 → 模型选择报告。面试官追问任何一环,你都有产出物可以展示。
练习
- 对领域分类器做两轮分层搜索,记录每轮的搜索空间与最优值变化。
- 画三个模型的学习曲线,分别诊断它们处于哪种状态并给出下一步动作。
- 用自助法为最终模型算 95% 置信区间,写一句话结论。
- 写一份完整的模型选择报告,包含至少一个”分数更高但没入选”的案例及理由。
面试常问
Q:学习曲线怎么用? 训练/验证分数随数据量的两条曲线:同低→高偏差(加特征换模型);差距大→高方差(加正则或加数据,看验证线斜率是否还在升)。它回答”下一步该投数据还是投模型复杂度”。
Q:为什么模型选择不能只看分数? 分数之外还有需求约束(要不要概率输出)、成本约束(训练/推理耗时)、运维约束(可解释性、体积)。需求匹配优先——略低分数但满足置信度需求的模型,优于高分黑箱。
Q:怎么防止调参过拟合测试集? 训练/验证/测试三分:调参只看交叉验证,测试集封存到最后验收用一次;验收不满意需要迭代时,重新切分或收集新数据,不要在同一测试集上反复。
Q:随机搜索为什么常常不输网格搜索? 超参对结果的影响高度不均——有效的维度往往只有一两个。随机搜索在相同预算下对每一维的探索更密,命中有效维度的概率反而高于稀疏网格(Bergstra & Bengio 的经典结论)。
完成上述验收后,才能将你的 M2 标记为达成。 下一课进入系列四:多模态科研内容理解 01:PDF 全文解析与结构化抽取。
💬 评论
这套方案在哪一步卡住了?也欢迎补充你的实现方式或踩坑经验。