2.5 模型选择 (Model Selection)


文档摘要

2.5 模型选择 (Model Selection) 本节摘要:模型选择要回答的不是"哪个算法最厉害",而是"在我的数据上,哪套配置放到新数据上最稳"。它把三件事串成一条线:先切分数据、再用交叉验证给出可信分数、最后用网格搜索或随机搜索找出好超参数。核心矛盾是过拟合与欠拟合——一个把训练集背得太熟,一个连训练集都没学进去。本节给出诊断这两者、以及一步步逼近平衡点的可操作方法,说白了就是给模型挑一条既不过火、也不偷懒的配置。

2.5 模型选择 (Model Selection)

本节摘要:模型选择要回答的不是"哪个算法最厉害",而是"在我的数据上,哪套配置放到新数据上最稳"。它把三件事串成一条线:先切分数据、再用交叉验证给出可信分数、最后用网格搜索或随机搜索找出好超参数。核心矛盾是过拟合与欠拟合——一个把训练集背得太熟,一个连训练集都没学进去。本节给出诊断这两者、以及一步步逼近平衡点的可操作方法,说白了就是给模型挑一条既不过火、也不偷懒的配置。

上手前先明确

阅读完本节,你应当能够:

  1. 说清模型选择的本质是选"泛化能力"而非"训练集分数"
  2. 区分过拟合与欠拟合的表现和各自成因
  3. 用交叉验证代替单次划分来比较候选模型
  4. 说清网格搜索与随机搜索各自的适用场景和代价
  5. 写出一次完整的超参数调优流程并解读结果
  6. 按"先粗搜、后精搜"的思路安排调参预算

一、模型选择到底在选什么

初学者容易把模型选择理解成"翻算法菜单":线性回归、决策树、支持向量机、随机森林,挨个试一遍,谁分高用谁。这么想没错,但只对了一半。真正的模型选择,是选一个在没见过的新数据上表现最好的组合——算法只是其中一维,还有超参数、特征、预处理方式,都是变量。

这里的核心矛盾,是训练误差和泛化误差的拉扯。把模型想成一个学生:他可以把整本习题集的答案背下来,平时测验满分,可一到高考遇到没见过的题就崩——这叫过拟合,模型把噪声和偶然性也学进去了。反过来,如果他连习题集都没吃透,平时就及不了格,上了考场自然更不行——这叫欠拟合,模型太简单,连规律都没抓住。

模型选择的目标,就是在"背得太死"和"学得太浅"之间找一个平衡点。怎么找?分三步走:先切数据,再量分数,最后调参数。下面按这个顺序展开。

二、先切数据:留出法

第一步最简单也最容易做错。把数据分成训练集和测试集,训练集用来喂模型,测试集假装成"未来上线后的新数据",全程不让模型碰,只在最后评估一次。

from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=42, stratify=y )

几个参数值得记住:test_size 决定测试集比例,小数据集常取 0.2 到 0.3;random_state 固定随机种子,保证每次切分一致、结果能复现;stratify=y 让训练集和测试集保持各类别比例一致,分类任务强烈建议加上,否则小类别可能全被切进某一侧。

留出法的问题也明显:分数受这一刀切在哪影响太大。 数据小了,换个种子分数就漂。更隐蔽的是,如果你反复在同一个测试集上调参、再回来看分数,测试集其实已经"泄题"了——它不再是干净的新数据,你的选择被它带偏了。这就是为什么后面要用交叉验证,把"挑配置"这件事从测试集里剥离出来。

顺带说个切分比例的经验:数据量大时,测试集取 20% 甚至 10% 就够,剩下的都留给训练;数据小时,宁可测试集稍大些,也别让它只剩几十个样本,否则最后那个分数根本不可信。train_test_split 还支持 train_size,两者取一个即可,不用都写。还有 stratify,分类任务强烈建议带上——它保证正负例比例在两边一致,否则小类别可能被整个切进某一侧,训练集里干脆没有它,模型学了个寂寞。

三、再量分数:交叉验证选型

交叉验证的思路上一节讲过:把训练数据再切 K 折,轮换当验证集,取平均分。它在这里的用途是比较候选模型——不用等到最后碰测试集,就能给出一个相对可信的排名。

from sklearn.model_selection import cross_val_score from sklearn.tree import DecisionTreeClassifier from sklearn.linear_model import LogisticRegression for name, model in [("决策树", DecisionTreeClassifier()), ("逻辑回归", LogisticRegression(max_iter=1000))]: scores = cross_val_score(model, X_train, y_train, cv=5) print(name, scores.mean())

看平均分的同时,别忘了看分数的波动。两个模型平均分都是 0.90,一个五折是 0.88 到 0.92,另一个是 0.80 到 1.00,前者显然更稳,更值得信任。只看均值、不看方差,是选型时常见的偷懒。

选型这件事上,我更倾向先把复杂模型和简单模型都拉进来跑一轮 CV 兜底——有时候一个调得好的线性模型,分数不输没调过的树模型,而它解释性更好、跑得更快。别默认"更复杂的算法一定更好",先让交叉验证说话。

想进一步诊断过拟合,可以让 cross_validate 同时返回训练集和验证集分数:

from sklearn.model_selection import cross_validate res = cross_validate(model, X_train, y_train, cv=5, return_train_score=True) print(res["train_score"].mean(), res["test_score"].mean())

训练分和验证分之间的差,就是过拟合的体温计:训练分逼近满分、验证分低一大截,说明模型在背题;两个都低,说明还没学会。选型时不光挑验证分高的,还该挑这个差小的。

四、最后调参数:网格搜索与随机搜索

选定了算法,剩下的超参数怎么定?一个个手试太低效,用搜索工具让交叉验证自动帮你跑。scikit-learn 提供了两个主力:

  • 网格搜索:把所有超参数的可能取值排成一张网格,穷举每一种组合,逐个用交叉验证打分,取最高分那组。它的保证是——网格范围内一定找到最优。
  • 随机搜索:不穷举,而是在超参数空间里随机抽固定数量的组合来试。它不保证最优,但算力花得少,且能覆盖更广的范围。
from sklearn.model_selection import GridSearchCV from sklearn.ensemble import RandomForestClassifier grid = { "n_estimators": [50, 100, 200], "max_depth": [None, 5, 10], } search = GridSearchCV(RandomForestClassifier(random_state=42), grid, cv=5, scoring="f1", n_jobs=-1) search.fit(X_train, y_train) print(search.best_params_) print(search.best_score_)

跑完后有三个属性最常用:best_params_ 是最佳超参数组合,best_score_ 是它在交叉验证上的平均分,best_estimator_ 是已经用最佳参数在整个训练集上重新训练好的模型,可以直接拿去预测。

两种搜索怎么选,看这张表:

维度 网格搜索 随机搜索
搜索方式 穷举所有组合 随机抽样固定数量
是否保证最优 网格内保证 不保证,通常接近
计算量 随组合数爆炸 可控,只取决于抽样数
适合场景 参数少、范围小 参数多、范围大
连续参数 只能取离散点 可用分布采样

一个很实用的组合拳:先用随机搜索在宽范围里粗扫一轮,摸清哪个方向有戏,再用网格搜索在小范围里精扫。 这比一上来就上密集网格划算得多——密集网格的大部分组合其实是浪费的。

两个参数再补一句。n_jobs=-1 让搜索用满所有 CPU 核,能成倍提速;refit=True(默认)会在找到最佳参数后用整个训练集重新训练一个最终模型,存进 best_estimator_ 供你直接拿去预测。还要分清两个分数:best_score_ 是交叉验证上的平均分,把 best_estimator_ 放到测试集上得到的分数才是对未来表现的估计,别把前者当成上线依据。

五、过拟合与欠拟合:诊断和对策

调参调不动时,先别急着加算力,回头判断一下模型处在哪一端。

现象 训练集表现 验证集表现 成因 对策
过拟合 很高 明显偏低 模型太复杂、数据太少、噪声多 加正则、减复杂度、加数据、早停
欠拟合 偏低 也偏低 模型太简单、特征不够、没训够 换更强模型、加特征、多迭代

判断的关键,是同时看训练集和验证集两个分数。如果训练集接近满分、验证集一塌糊涂,是过拟合,模型"背题"了;如果两个都低,是欠拟合,模型"没学会"。只盯一个分数是看不出端倪的。

拿决策树的 max_depth 做个实验最直观。深度从 1 一点点加到 30,训练分数会一路涨到接近满分,可验证分数先升后降,在某个中等深度到达顶点之后开始往下掉。那个顶点,就是复杂度最合适的点,调参本质上就是找到它。所以过拟合不是"模型变坏了",而是"复杂度越过了拐点"。

from sklearn.tree import DecisionTreeClassifier for d in [1, 3, 5, 10, None]: m = DecisionTreeClassifier(max_depth=d, random_state=42) m.fit(X_train, y_train) print(d, m.score(X_train, y_train), m.score(X_test, y_test))

跑完这几行,你会亲眼看到训练分和测试分随着深度拉开的剪刀差,比任何文字都更有说服力。

⚠️ 常见坑:把测试集当验证集反复用。每次你在测试集上看分数再回去改模型,测试集就泄漏了一点信息,几轮之后它就不再干净,最终上线分数会比测试分数难看。正确做法是:验证交给交叉验证,测试集只在最后碰一次。

⚠️ 常见坑:调参范围拍脑袋定得太宽或太窄。太宽,算力白白烧掉;太窄,最优解可能在边界外。先跑一次默认参数拿到基线,再围绕它放缩范围。

💡 关键直觉:模型选择不是一次性的"选一个",而是"先圈范围、再逼近"的迭代。粗搜定方向,精搜定落点,测试集最后只验收一次,这条纪律能省掉大量返工。

六、时间序列与分组数据:常规交叉验证会失效

前面所有交叉验证都默认一个前提——样本之间相互独立、可以任意打乱。有两种数据打破了这个前提,硬套常规 K 折会得到虚高甚至完全错误的结论。

一种是时间序列。今天的数据和昨天强相关,随机切折会让模型"用未来预测过去",等于作弊。正确做法是按时间顺序切:只用过去的数据训练,去预测未来的数据,也就是时序交叉验证。另一种是分组数据,比如同一用户的多条记录。如果同一用户的样本散落在训练折和验证折里,模型只要记住用户是谁就能"猜对",分数虚高。这时要用分组交叉验证,保证同一组的样本不跨折。

判断标准一句话:如果样本不是独立同分布,先想清楚"泄漏的通道"在哪,再选对应的划分策略。 这两类数据的评估细节,留到后续实践章再展开。记住一条总原则就好:数据的结构决定了你能用哪种划分方式,先看清数据长什么样,再决定怎么切、怎么评,别拿标准 K 折硬套一切。

要点串联

  • 模型选择选的是泛化能力:不是训练集分数最高,而是新数据上最稳的那个组合。
  • 过拟合是"背题":训练集满分、验证集崩,对策是正则化、降复杂度、加数据。
  • 欠拟合是"没学会":训练验证都低,对策是换强模型、加特征。
  • 留出法是起步:切一次训练测试,但分数受切分运气影响,还容易反复偷看测试集。
  • 交叉验证是选型主力:看平均分更要看波动,波动小才可信。
  • 网格搜索穷举、随机搜索抽样:参数少用网格,参数多用随机,先粗后精最划算。
  • 测试集全程只碰一次:反复用测试集调参会泄漏信息,最终分数会失真。

下一节我们把"预处理 + 模型"打包成管道,让交叉验证和调参不再担心数据泄漏。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U