ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

机器学习模型评估:留出法原理、实操与常见坑

机器学习模型评估:留出法原理、实操与常见坑 给团队里做机器学习的新同学讲评估方法时我通常会先问一个问题你如何判断训练出来的模型在真实场景里能打最常见的回答是——“用留出法啊把数据切开留一部分当测试集跑一下指标不就知道了。”可大多数人只能说到这一步追问下去切分比例怎么定、要不要分层、随机种子怎么留、同一组数据能不能同时出现在两边往往就开始支支吾吾。机器学习里的模型评估方案不少留出法holdout是最基础也最容易被想当然使用的一种。它看上去就是把数据集切成两块但背后的边界条件其实相当多。我打算从原理、实操和翻车案例三个角度把它彻底讲透既说清楚它到底在估什么也把实际项目里容易踩进去的坑一个个揪出来。1. 训练成绩不等于实战能力留出法想解决的核心问题1.1 过拟合模型很可能只是在“背答案”早年做模型时大家特别爱看训练集上的损失曲线降到几乎为0的时候团队群里一片欢呼结果模型换一批新数据就一塌糊涂。这不是玄学而是模型的容量和训练机制决定的。模型在训练时能做的最偷懒的一件事就是把训练样本本身“记下来”尤其是当模型容量足够大、样本量不够多时这种记忆很容易发生。打个比方决策树不限制深度训练集上可以做到100%正确测试集上却未必能到80%这就是过拟合。我习惯拿学生考试做类比。平时练习册翻来覆去地刷模拟考次次高分但高考真题里把条件稍微改一改就发现这个学生其实是在背题而不是真的理解了解题思路。训练集就是那本练习册测试集就是一份没做过的模拟卷。模型评估要做的就是设计一份真正没做过的模拟卷检验模型是理解了规律还是只记住了答案。1.2 泛化误差才是我们真正关心的指标机器学习建模的终极目标是让模型在未知样本上表现好而不是在旧样本上“回忆”。用统计的话说我们要的是泛化误差模型在一个服从真实数据分布的任意新样本上损失函数的期望。但泛化误差没法直接算因为我们不可能拿到所有数据。留出法的思路很朴实把手里已有的样本随机分成两部分一部分用来训练另一部分完全不让它参与训练把这个“留出来”的部分当作未知数据的代理。测试集上的表现就是对泛化误差的一次抽样估计。值得强调的是一次切分得到的精度或AUC只是一个点估计不是一个确定的值。因为切分带有随机性得到的指标天然有波动。很多人把留出法跑出的0.92当作模型真实水平这是理解偏差。更严谨的说法是在当前的切分方式下测试集给出了一个泛化水平的估计这个估计可能有正负几个百分点的误差。后面我会专门讲这个波动到底能有多大。1.3 为什么留出法能存在这么久留出法被广泛使用不是因为它完美而是因为它极其务实。实现简单、几乎零计算成本、一次训练就能给出评估结果而且在数据量足够大时单次切分的随机波动可以被压到很小。可以说它是机器学习里“性价比”最高的评估起点。很多经典教材比如周志华的《机器学习》把“模型评估与选择”单独列了一章留出法就放在最前面再用它引出交叉验证和自助法。它几乎是一切评估思想的骨架。理解了留出法再理解K折交叉验证就很容易——交叉验证不过是把“留出”这一步重复了K次而已。2. 标准留出法流程切分、比例、分层、种子一个都不能少2.1 先搞清楚训练集、验证集、测试集三者的分工很多资料把训练集、验证集、测试集混着说实际工作中会因此翻车。它们的定位是完全不同的训练集用来拟合模型参数。验证集用来做超参数选择、提前停止、特征选择等调优决策。测试集最终评估泛化能力不参与任何训练和调参过程。如果数据量有限只有训练集和测试集很容易出现的情况是反复用测试集调参调着调着测试集就“变脏”了因为调参者会根据测试结果不断改变模型测试集的信息间接参与了建模。这就是“三层数据”在正式流程中存在的意义。标准的留出式评估会先留出一块测试集锁死然后把剩下的数据继续按比例拆成训练集和验证集。2.2 切分比例怎么定提到切分比例网上答案最多的是“7:3”“8:2”但比这个参数更重要的是绝对样本量。切分比例本身没有绝对最优取决于数据规模、模型复杂度和评估指标的稳定性。我自己的经验是这样的数据量在万级别以下测试集可以取20%到30%但更推荐优先考虑交叉验证数据量在十万到百万级测试集取10%到20%通常足够数据量达到百万甚至更大测试集哪怕只取1%绝对样本也很多评估指标依然稳定。为什么因为我们真正关心的是测试集里有多少个有效样本支撑评估而不是占比多少。假设二分类真实AUC是0.8测试集只有50个样本出来的结果可能围绕0.8大幅震荡而测试集有5000个样本时单次切分估算的波动就小得多。所以决定测试集大小的第一位标准是“测试集里能提供多少个有效样本”而不是那个比例数字。2.3 分层采样保持类别比例尤其对不平衡数据随机切分在理论上没问题前提是样本独立同分布且类别分布均匀。现实中的数据往往不是这样正样本可能只占5%。如果完全不控制切分后训练集里可能只剩下个别正样本模型根本学不到正类特征或者测试集里正样本太少指标算出来没有意义。解决办法是分层采样。sklearn的train_test_split提供了stratify参数可以按类别标签比例切分from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy )传入stratifyy之后切分出来的训练集和测试集里正负样本的比例会尽量接近原始数据集。这算是最基础的一步但很多人就是会漏掉。2.4 随机种子让“随机切分”变成“可复现切分”train_test_split的本质是先洗牌再切分。如果不固定random_state每次运行代码都会得到不同的训练/测试划分就会出现一种很尴尬的情况你跑两次实验模型参数一样、数据一样评估指标却不同。这在对比调优时影响很大因为你无法判断指标差异来自模型改动还是切分运气。固定random_state42不只是为了复现论文结果。在项目迭代中固定种子能让所有方案在同一份测试集上比较公平很多。但要注意种子固定不等于结果就是真实水平。你可以在一个调优周期内固定种子但模型定稿前最好用多个不同的种子各切分几次看指标平均值和波动范围而不是只相信一个幸运的种子。2.5 什么时候不能直接随机切分随机切分有一个前提样本之间是独立同分布的。如果你的数据里存在分组结构例如同一个用户、同一台设备、同一个门店的多条记录情况就变了。它们之间并不独立模型很容易通过同一分组的历史记录“猜到”测试集中该分组的标签。这时候随机切分看似没毛病实际已经造成泄漏。常见处理办法是按照实体ID先分组再把整个组切到训练侧或测试侧。sklearn里有GroupShuffleSplit、LeaveOneGroupOut这些工具。时序数据也一样直接用随机切分不合适应该按时间先后切分前一段做训练后一段做测试模拟真实上线时“用历史预测未来”的流程。3. 留出法最容易被忽视的四个坑3.1 坑一切分后的预处理顺序错误把测试集信息泄漏进训练集先切分再预处理这个原则说起来简单实际操作中却经常被忽略。最常见的场景是读取数据后先对全量特征做标准化或归一化再做train_test_split。这种写法的危害在于标准化计算用的均值和标准差来自“训练集测试集”的全体数据测试集的信息通过这两个统计数据进入了训练流程。模型训练时已经间接“看”到了测试集的分布特征。正确的做法是把数据变换器先在训练集上fit得到均值和标准差再transform训练集和测试集或者直接用sklearn的Pipeline让预处理在交叉验证流程里自动按折进行。下面这段是错误示范# 错误示范全量标准化后再切分 scaler StandardScaler() X_scaled scaler.fit_transform(X) X_train, X_test, y_train, y_test train_test_split( X_scaled, y, test_size0.2, random_state42 )正确写法from sklearn.preprocessing import StandardScaler from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test)特征选择、缺失值填充、降维也一样。任何需要从数据里统计数据分布的步骤都必须严格限定在训练集内完成然后原样套用到测试集。这条规则一旦被打破后面所有评估结果都不可信。3.2 坑二类别不平衡时没有做分层小众类别被“随机”弄丢假设有一个二分类任务正样本只占2%。数据集总共1万条正样本200条。随机切分在概率上当然不会完全均匀测试集可能只分到10条正样本甚至极端情况为零。测试集里正样本为0AUC、F1这类指标就完全废了。分层采样能保证比例基本一致但比例一致也不等于万无一失。如果数据再少一些分层后测试集里依然只有个位数正样本即使比例正确抽样误差依然很大。碰到这种极端不平衡正确的思考方向是要么增大测试集的总样本量要么干脆采用K折交叉验证把评估结果平均起来而不是指望一次留出法给出可信答案。有时候我甚至会专门从正样本中单独划分一部分进去测试集人为保证测试集至少有一定数量的正样本再配合分层采样。这在“正样本本来就稀缺”的场景下是合理的工程做法但需要把规则写清楚避免样本选择偏差。3.3 坑三测试集样本量太小指标波动到没有参考价值很多同学分配数据集时只关心比例觉得“20%的测试集已经很多了”但没有意识到真正决定评估可信度的是测试集里的绝对样本量。可以用一个最简单的统计模型说明这一点。假设模型真实精度是0.85测试集大小是n那么测试集上观测到的正确样本数近似服从以0.85×n为均值、以sqrt(n×0.85×0.15)为标准差的二项分布。换算成精度指标标准差是sqrt(p(1-p)/n)。当n30、100、1000时这个标准差分别约为0.065、0.036、0.011也就是说95%参考区间宽度大约分别是正负13%、正负7%、正负2%。所以当你在一个只有几百条样本的数据集上按8:2切分得到大约100条测试样本0.86和0.79两个版本的模型可能根本没有显著差异。必须先估算一下指标波动区间再判断“模型提升了”这个结论成不成立。这也是我一直建议小数据集优先考虑交叉验证而不是一次留出法的原因。3.4 坑四反复更换随机种子等于对测试集做过拟合我见过一种非常隐蔽的坏习惯模型调参调不出来时有人会换random_state重跑一遍看哪个种子的指标好看就留下哪个。这在本质上是一种“种子上位”比固定一个种子更危险。因为你实际上是在多个切分中选了一个对当前模型最友好的测试集测试集已经不再是未知样本的干净代理评估结果被严重虚高。可能有人辩解说“只是换了个随机种子没什么大不了”。实际上这种行为等于在评估环节加入了人工挑选。正确做法有两种一是固定种子但明确指出评估结果依赖这一次切分二是在模型确定后用多个不同种子跑多次报告指标均值和标准差用波动范围来展示真实水平。后一种方式信息量大得多也更接近留出法评估应有的严谨性。4. 留出法不是万能钥匙和交叉验证、自助法的适配边界4.1 为什么交叉验证通常更稳K折交叉验证做的事情很简单把数据分成K份每次拿K-1份训练、1份验证轮流K次最后把K次验证结果平均。相比留出法的一次切分交叉验证相当于做了多次留出法提供的是平均值和方差受切分运气的影响小得多。尤其在小数据集上交叉验证几乎是标准操作。假设总共只有500条样本留出法留出100条做测试单次结果波动会很大而五折交叉验证可以轮换验证每次验证集都不同最后指标稳定很多。代价也很明确训练K次模型计算成本翻倍。4.2 留出法反而更适合的场景交叉验证很好但不是所有时候都合适。留出法有一些交叉验证替代不了的场景第一数据规模极大时。百万级数据上单次切分出10%测试集就有十万条样本评估置信度已经足够而五折交叉验证要把训练量放大五倍完全没有必要。第二训练成本很高时。深度学习大模型尤其是预训练模型微调或大Batch训练单次训练都要以小时计K折会直接拉长整个实验周期。先用留出法快速看趋势最后有问题再上交叉验证是更务实的选择。第三时序和分组场景中。时序预测天然要求按时间切分不能用随机切分也不适合普通K折随机抽样因为未来的数据不能被“偷”到训练集里。分组场景则需要按组切分而留出法里的GroupShuffleSplit就是很好用的方案。第四贴近真实上线环境。很多推荐系统和风控模型线上只会用历史数据去预测未来新样本。用“历史段训练、最近段测试”这种留出式切分反而比K折更贴近实际部署。4.3 一个更推荐的组合姿势外层留出内层交叉验证把留出法和交叉验证对立起来是完全没必要的。成熟的项目流程往往是两者配合使用先在最外层留出一份终极测试集锁死不动专门用来做最终验收然后在剩下的训练数据上用交叉验证来完成超参数搜索和模型选择最后把选定的模型在终极测试集上跑一次得到泛化能力的最终估计。这种做法能同时拿到两个好处。交叉验证保证了调参阶段的稳定性每一份数据都能被用作验证免去留出法一次切分的运气成分终极测试集则保证了最外层评估的“清白”因为整个调参过程永远不会碰它。很多工业界的模型训练流水线本质都是这个结构只是在工程上被封装成了更复杂的形式。评估方式评估稳定性训练开销典型适用场景留出法单次估计波动较大1次训练大样本、快速迭代、贴近线上时序流程K折交叉验证多次平均稳定性好K次训练中小样本、超参调优、模型选型自助法样本重复抽样对不均衡数据友好采样数×训练小样本、稀有事件、需要稳定置信区间自助法值得一提它通过有放回抽样构造训练集未被抽到的样本做测试特别适合小样本场景缺点是实现比较复杂实际项目中用得不算多。理解它的价值就行。5. 一次翻车复盘同一组用户被随机切分后指标直接失真5.1 问题现场前几年做电商用户复购预测数据是按“用户-日期-行为”组织的明细。一个用户可能有好几行分别对应不同月的浏览、加购、购买记录。目标变量是“未来30天内是否复购”。当时项目的流程很常规把这批明细样本丢进train_test_split留下20%当测试集训练完一看AUC 0.83大家都很满意模型直接上线。结果真实的线上AUC只有0.71。5.2 排查链路排查过程大致是这样先怀疑特征把线上特征分布拉出来和训练时对比差异不大然后又怀疑模型过拟合可训练集和测试集的表现差距也没大到异常最后把目光收回到数据划分手滑查了一下发现同一个用户在训练集里和测试集里同时大量出现。问题很清楚随机切分把同一个用户的不同行为记录打散到了两边。模型在训练时已经见过同一个ID的历史行为测试时看到另一部分行为就能很容易“顺藤摸瓜”猜到标签。这是一个典型的分组泄漏而表面上的AUC只是一个虚假繁荣。用代码模拟一下当时的问题from sklearn.model_selection import train_test_split # 错误做法直接对明细数据随机切分 X_train, X_test, y_train, y_test train_test_split( df[features], df[label], test_size0.2, random_state42 )5.3 修复方式正确做法是让同一组用户完整地落在同一侧可以用GroupShuffleSplit或者干脆按用户ID先分组再对用户做随机抽样from sklearn.model_selection import GroupShuffleSplit gss GroupShuffleSplit(n_splits1, test_size0.2, random_state42) for train_idx, test_idx in gss.split(df[features], df[label], groupsdf[user_id]): X_train, X_test df[features].iloc[train_idx], df[features].iloc[test_idx] y_train, y_test df[label].iloc[train_idx], df[label].iloc[test_idx]修好后重新评估AUC从0.83掉到了0.72和线上真实表现对上了。虽然数字难看但这才是模型的真实水平。那个0.83从头到尾都是一个幻觉。5.4 我后来固定下来的检查习惯这件事之后我在每次用留出法评估前都会强制回答三个问题数据的真实粒度是什么同一实体是否会被切到两边数据切分之前有没有任何步骤使用了全量数据的统计信息如果只用一次切分评估指标的置信区间大概多宽能不能支撑“模型有效”这个结论这三个问题能拦住绝大多数离谱的评估结果。留出法本身非常简单复杂的地方全在这些容易被忽略的边界条件上。也是因为这样我始终建议团队里做评估实验时先把“数据是怎么切开的”写清楚这比模型用了什么结构重要得多。
返回列表