首页 / 资讯中心 / 文章详情

验证曲线实战:用交叉验证定位超参数与过拟合拐点

验证曲线实战:用交叉验证定位超参数与过拟合拐点 ★ FEATURED ARTICLE
1. 验证曲线到底在解决什么问题调参调到后半夜训练集准确率已经刷到 0.99换到测试集上一跑掉到 0.62——这个画面我见过太多次了。很多人第一反应是模型过拟合了然后凭感觉把树深度砍一半、把正则项加大、把邻居数调大运气好就蒙对了运气不好越调越差。验证曲线Validation Curve就是用来终结这种凭感觉调参的工具它把某一个超参数从最小值到最大值扫一遍每个取值都记录训练集得分和验证集得分再把两条线画在同一张图上让你一眼看出当前模型是欠拟合还是过拟合、这个参数该往哪个方向拧、拧到哪儿该停。它解决的不是模型准不准而是模型复杂度该定在哪。这俩问题完全不同。前者靠一次评估就能回答后者必须扫一个参数区间、观察得分随参数变化的全过程才能回答。适合谁看我觉得有三类人最该把这张图刻进习惯里一是刚学完交叉验证、还在用train_test_split单次划分评估模型的同学二是手上有一堆超参数、靠GridSearchCV盲搜但不知道搜索区间该设多宽的工程师三是模型上线前需要给别人解释为什么选这个参数值的开发者——有一张曲线图比十句我感觉这样更好都有说服力。我自己带过几个做数据方向的同学最常见的误区是把验证曲线和学习曲线混为一谈。前者横轴是超参数取值后者横轴是训练样本数量两者的读法、用途、甚至该在项目哪个阶段画都不一样。下面我会把这两条线的区别、验证曲线的数学含义、三种主流模型的实操调参记录、以及一堆踩过的坑一条条拆开讲。2. 原理拆解一条曲线背后到底在算什么2.1 训练得分与验证得分的双线逻辑验证曲线的本质是一次受控扫描实验。选定一个超参数比如决策树的max_depth给它一个候选集合[1, 2, 3, ..., 20]然后对集合里的每一个取值都用 k 折交叉验证完整训练 k 次模型每次在训练折上算一个得分、在留出的验证折上算一个得分最后把 k 次的训练得分取平均、验证得分取平均得到这个参数取值对应的两个点。扫完整个候选集合就有了两条折线。这里的关键在于控制变量一次只动一个参数其他参数全部固定在当前值。这样做的好处是归因清晰——曲线上升或下降只可能是这一个参数造成的。坏处是参数之间往往存在交互比如树模型里max_depth和min_samples_leaf是耦合的深度调到 12 但叶子最小样本数只有 1曲线照样会翘到天上去。所以验证曲线是局部扫描不是全局寻优这一点必须提前想明白不然容易对着一条单参数曲线得出过于乐观的结论。从数学上看验证曲线上每个点的训练得分是 k 次训练得分的均值train_score(θ) (1/k) · Σ score(f_θ(X_train_i), y_train_i)验证得分同理只是把评估数据换成没参与训练的验证折。注意分母都是 k用的是平均而不是拼接后统一计算这让不同参数取值之间的点具有可比性——因为每折的样本量都是一样的平均值的统计含义稳定。有人图省事用sum或者只看最后一折曲线就会抖得没法看这是很典型的实现错误。2.2 偏差与方差视角下的曲线形状验证曲线的形状其实只有三种典型模式认出来就够用了。第一种训练得分高、验证得分低且中间有明显缺口。随着参数往更复杂的方向走训练得分继续攀升甚至逼近 1.0验证得分却开始下降或原地踏步。这个缺口就是方差也就是过拟合。树模型的深度、多项式特征的阶数、kNN 里越来越小的 k 值都会画出这种形状。看到它调参方向很明确往简单的方向调也就是减小深度、降低阶数、增大 k。第二种两条线都很低且贴得很近。这是欠拟合偏差主导。模型根本没能力捕捉数据里的结构训练集上都没学好验证集自然也好不到哪去。这时候如果还在加正则、还在砍模型容量那就是南辕北辙。正确动作是往复杂的方向调或者干脆换模型、加特征。第三种两条线都高且缺口很小这是我们要的甜点区。实际项目里这条完美曲线很少出现更多是一条先升后降的验证曲线中间有个明显的峰值。这个峰值对应的参数值就是当前数据、当前模型、当前评估指标下的最优解。要注意当前这三个字——换数据集、换特征、换评估指标峰值位置都会移动所以验证曲线不是画一次就完事的它属于每次实验的常规动作。还有个容易被忽略的细节验证得分的曲线往往比训练得分更抖。原因很直白训练得分是在模型见过的数据上算的方差天然小验证得分面对的是分布略有差异的新样本k 折之间的波动会被放大。所以读图时不要死盯某一个点的凸起要看整体趋势必要时把cv从 5 提到 10或者对同一参数重复多次不同随机种子的划分再看均值。2.3 为什么必须用交叉验证而不是单次划分有人会问我把数据按 8:2 切一刀扫参数的时候就用这固定的 20% 当验证集不也能画出曲线吗技术上可以但结论可靠性差很多。单次划分的验证得分本身就有很大的随机性——恰好切到一批简单样本验证得分就虚高切到一批噪声大的样本就虚低。这种随机波动会直接叠加到曲线上导致你看到的峰值可能只是划分运气好换个随机种子峰值就跑到别处去了。交叉验证把这个问题摊平了k 折轮着当验证集每个样本都被评估一次得分是 k 次的平均随机性被显著压低。代价是计算量翻了 k 倍这个下面讲性能优化时再细说。还有一个更隐蔽的问题如果在扫参数之前做了标准化、特征选择、缺失值填充这类依赖数据分布的预处理而且是在全量数据上做的那验证折的信息就已经泄露进了训练过程。曲线会显得异常漂亮验证得分高得不真实实际部署时打回原形。正确做法是把这些预处理包进Pipeline让它在每一折内部独立完成。这个坑我在早期项目里踩过不止一次下面第 5 节会专门展开。3. 动手实操从零画出一条能看懂的验证曲线3.1 数据与模型准备先造一个能稳定复现的实验环境。用一份中等规模、特征维度适中的分类数据噪声别太小否则所有参数都能刷到 0.98曲线平平的一条看不出任何结构不利于理解。这里我用合成数据演示换成你自己的数据把加载部分替换掉即可。import numpy as np import matplotlib.pyplot as plt from sklearn.datasets import make_classification from sklearn.model_selection import validation_curve, StratifiedKFold from sklearn.tree import DecisionTreeClassifier # 固定随机种子保证每次跑出来的图一致 RNG 42 X, y make_classification( n_samples4000, n_features30, n_informative8, n_redundant6, n_classes2, class_sep0.9, flip_y0.08, # 故意加一点标签噪声让曲线更有区分度 random_stateRNG, ) cv StratifiedKFold(n_splits5, shuffleTrue, random_stateRNG)这里几个参数值得多说两句。flip_y0.08是故意把 8% 的标签翻转掉模拟真实数据里的标注错误。如果不加这个噪声模型在很浅的深度就能学到接近完美的规则验证曲线会是一条平线你就失去了观察过拟合的机会。n_redundant制造了一批与有效特征线性相关的冗余列这会让树模型在浅层时表现一般、深层时突然记住噪声曲线的拐点更明显。这类刻意制造的难度在调试阶段特别有用比拿真实数据反复试错效率高得多。3.2 sklearn 的 validation_curve 完整用法与参数解读库里的validation_curve已经把上面说的扫描 交叉验证 求均值全包了一行调用就能拿到训练得分和验证得分两个矩阵形状是(参数取值个数, 折数)。param_range np.arange(1, 21, 1) train_scores, valid_scores validation_curve( estimatorDecisionTreeClassifier(random_stateRNG), XX, yy, param_namemax_depth, param_rangeparam_range, cvcv, scoringaccuracy, n_jobs-1, verbose0, ) train_mean train_scores.mean(axis1) valid_mean valid_scores.mean(axis1) train_std train_scores.std(axis1) valid_std valid_scores.std(axis1) for d, tr, va in zip(param_range, train_mean, valid_mean): print(fmax_depth{d:2} train{tr:.4f} valid{va:.4f} gap{tr - va:.4f})几个参数必须解释清楚因为它们直接决定曲线的可信度param_name必须是 estimator 的构造参数名不是属性的名字写成tree_.max_depth会直接报错。param_range的选取比想象中重要。区间太窄看不到下降段你会误以为参数越大越好区间太宽计算量爆炸且大部分点落在无意义的区域。我的习惯是先粗扫一个宽区间比如深度 1~30找到拐点后再在拐点附近细扫。scoring建议显式指定不依赖默认值。分类任务用accuracy、f1、roc_auc的曲线形状可能完全不同因为不同指标对预测概率的敏感度不一样。类别不平衡时尤其明显用 accuracy 画出来的最优深度换成 f1 可能就偏了。n_jobs-1是提速的关键交叉验证的每一折之间完全独立并行几乎是免费的午餐。但要注意内存数据集很大时并行折数太多会直接把内存吃满。输出里那个gap列是我每次必看的。gap 就是训练得分减验证得分它比两条线本身更直观地反映过拟合程度。gap 从 0.02 涨到 0.25说明模型复杂度的提升已经不再带来泛化收益继续加深只是在记忆噪声。3.3 手写版本完全掌控每一次训练库函数好用但有几个场景绕不过去一是模型没有可直接拿来扫的单一参数比如伪标签策略、样本权重阈值二是你想在每一折内部做记录比如保存每折的模型、记录训练耗时三是你想换个不兼容 sklearn 接口的模型。这时候手写反而更省事逻辑也就三十行。def manual_validation_curve(make_model, param_name, param_range, X, y, cv): 手写版验证曲线返回每个参数取值下的 train/valid 得分列表 results {param_name: [], train: [], valid: [], fit_time: []} # 折的划分只做一次所有参数取值共享保证比较公平 splits list(cv.split(X, y)) for value in param_range: fold_train, fold_valid, fold_time [], [], [] for tr_idx, va_idx in splits: X_tr, X_va X[tr_idx], X[va_idx] y_tr, y_va y[tr_idx], y[va_idx] model make_model(**{param_name: value}) import time t0 time.perf_counter() model.fit(X_tr, y_tr) fold_time.append(time.perf_counter() - t0) fold_train.append(model.score(X_tr, y_tr)) fold_valid.append(model.score(X_va, y_va)) results[param_name].append(value) results[train].append(np.mean(fold_train)) results[valid].append(np.mean(fold_valid)) results[fit_time].append(np.mean(fold_time)) return results这里有两个细节是我从踩坑里学来的。第一折的划分必须在循环外生成一次所有参数取值共用同一套划分。如果每个参数值都重新split一次不同取值面对的验证集不一样得分之间就不可比了曲线会出现莫名其妙的锯齿。validation_curve内部也是这么处理的手写时千万别忘了。第二顺手记录fit_time。训练耗时会随参数显著变化比如 boosting 的树数量后面选参数时不只看精度还要看多花 3 倍训练时间换 0.5 个点精度值不值这个数据在那时候就是决策依据。3.4 美化与解读把曲线画成人能看懂的样子图要能一眼读出信息。我的固定模板是训练得分实线、验证得分虚线各自带半透明置信带±1 标准差最优参数处打一条竖线并标注坐标。fig, ax plt.subplots(figsize(9, 5.5), dpi120) ax.plot(param_range, train_mean, o-, color#1f77b4, label训练得分) ax.fill_between(param_range, train_mean - train_std, train_mean train_std, alpha0.15, color#1f77b4) ax.plot(param_range, valid_mean, s--, color#d62728, label验证得分) ax.fill_between(param_range, valid_mean - valid_std, valid_mean valid_std, alpha0.15, color#d62728) best_idx int(np.argmax(valid_mean)) best_x param_range[best_idx] best_y valid_mean[best_idx] ax.axvline(best_x, linestyle:, colorgray, linewidth1.2) ax.annotate(f最优 max_depth{best_x}\nvalid{best_y:.4f}, xy(best_x, best_y), xytext(best_x 2, best_y - 0.12), fontsize9, arrowpropsdict(arrowstyle-, colorgray, lw1)) ax.set_xlabel(max_depth树的最大深度) ax.set_ylabel(准确率) ax.set_title(决策树 max_depth 的验证曲线5 折交叉验证) ax.set_ylim(0.5, 1.02) ax.grid(alpha0.25) ax.legend(loclower right) plt.tight_layout() plt.show()几个绘图上的小讲究属于图能不能被人读懂的范畴跟技术关系不大但价值不低。ylim一定要手动设不要用默认值——默认会把纵轴压缩到数据的实际范围两条线的差距被视觉放大或缩小容易误判。我一般把下界设在 0.5 左右让过拟合缺口的量级显得真实。置信带用fill_between而不是画十几条折线信息密度更高也更干净。最优点的标注位置要手动调xytext那个偏移量因图而异多试两次就好自动避让有时候会把标注甩到图外面去。读图的时候我习惯按三段式看上升段验证得分随参数增大而提升说明模型容量不够这一段不要停、平台段验证得分基本走平提升趋缓这里是收益递减区、下降段验证得分掉头向下过拟合开始必须停。最优值通常落在平台段的中前部而不是曲线的绝对最高点。原因很简单最高点往往是噪声带来的偶然抖动取平台段靠前的位置泛化更稳模型也更简单。4. 参数选择实战三个典型模型的调参记录4.1 树模型的 max_depth最经典的 S 形决策树的深度是验证曲线的最好教材。深度为 1 时是单层决策桩模型容量极小训练和验证得分都很低两条线几乎重叠——典型的欠拟合。深度涨到 4~6验证得分快速爬升这是模型开始学到真正的特征交互。深度超过 10 之后训练得分继续向 1.0 逼近验证得分却开始缓慢下滑缺口从 0.05 拉大到 0.2 以上。我在这份合成数据上跑出来的结果是最优深度大约在 7 到 9 之间验证得分约 0.86而深度 20 时训练得分 1.00、验证得分跌到 0.79。缺口 0.21这 0.21 就是纯粹的方差成本。值得注意的是验证得分在最优值附近非常平——深度 7、8、9 的差异在 0.005 以内完全在置信带宽度之内。这时候按最简原则选 7 就行没必要为 0.003 的差距多养三层树。还有一个替代路径不限制max_depth改扫min_samples_leaf。这两者控制的是同一件事——模型复杂度——但机制不同。深度是硬性截断叶子样本数是软性约束。数据量大、噪声多的时候我更倾向后者因为它对样本分布的适应性更好不会因为某个分支恰好很深就被整体砍掉。两者都扫一遍看曲线形状选拐点更平滑的那个这是我常用的判断方法。4.2 正则化强度 C对数尺度是关键逻辑回归的C是正则项系数的倒数C 越大正则越弱、模型越复杂。这个参数有一个非常实际的问题它的有效区间通常横跨好几个数量级从 1e-4 到 1e4。如果像扫深度那样用线性刻度np.arange你会看到一条几乎垂直的线——所有变化都挤在最左边一小段右边的点全是平的完全读不出信息。正确做法是用对数刻度param_range np.logspace(-4, 4, 17) # 1e-4 到 1e417 个点等比这样横轴按数量级展开曲线形状立刻变得可读。我在同一份数据上跑出的典型形状是C 从 1e-4 开始两条线都低强正则把权重压得太狠模型近乎瞎猜C 到 1e-2 附近验证得分快速上升C 在 1e-1 到 1e1 之间是平台区C 超过 1e2 之后训练得分继续爬验证得分走平甚至微降。平台区里取一个偏小的值模型更简单、更稳。这里有个很多人不知道的技巧logspace的点数不要太多。我一开始用np.logspace(-4, 4, 100)画出来的曲线锯齿严重因为相邻点的差异小于交叉验证的随机波动整条线就是在噪声里游泳。17 个点每个数量级 2 个点是个不错的起点看清楚了再在目标区间加密。点太多除了浪费时间还会误导你把噪声当成趋势。4.3 kNN 的 n_neighbors 与核方法的 gammakNN 的n_neighbors方向和树深度相反k 越大模型越简单。所以验证曲线的形状是倒过来的——左边k1训练得分 1.00、验证得分很低随着 k 增大验证得分先升后降最优点在中间。k1 时训练得分必然是 1.0因为最近的邻居就是自己这个点在图上永远贴在顶上看着很扎眼但完全正常不用管它。RBF 核的gamma控制单个样本影响范围gamma 越大影响范围越小、边界越弯曲、越容易过拟合同样是扫对数刻度。gamma特别容易画出训练得分一骑绝尘、验证得分先升后急跌的形状而且下降段非常陡。看到陡降说明已经深度过拟合此时的最优点通常离下降起点很近取的时候要保守一点宁可往前挪一格。三个模型的曲线有个共同的实用规律最优点往往不在验证得分的绝对峰值而在峰值左侧更简单的一侧一到两格的位置。这条经验在我做过的项目里命中率相当高原因是峰值本身受随机波动影响而左侧的点既接近峰值性能又有更大的安全边际。用这条规律做初选再配合网格搜索微调比直接取峰值稳得多。4.4 从曲线读出该往哪调我把读图决策整理成一张表实际用的时候基本是照着走的曲线现象判断下一步动作两线都低缺口小于 0.02欠拟合增大模型容量加深、加特征、降正则、换更强模型训练升、验证降缺口持续扩大过拟合降容量减小深度、增大 k、加强正则、加样本两线都高缺口 0.03~0.08基本合适微调邻域参数考虑特征工程再提一档验证曲线剧烈锯齿无明确趋势评估噪声大提高 cv 折数、增大数据量、检查是否有数据泄漏验证得分全程平直参数选错了换一个真正影响复杂度的参数或检查参数是否生效验证得分在某点后断崖下跌数值不稳定检查特征尺度、是否有极端值、学习率是否过大这张表的价值在于把图形翻译成动作。很多人画完曲线就卡在这里——图看懂了但不知道下一步干什么。有了对应关系调参就变成了查表加验证的机械过程效率高得多也不容易陷入反复试同一类参数的死循环。5. 常见坑与排查技巧实录5.1 曲线抖成锯齿看不出趋势这是最高频的问题我在不同项目里遇到过至少四五次原因基本就三类。第一类是验证集太小5 折在几千样本上每折只有几百条得分波动自然大解法是提高折数或者增大数据量。第二类是评估指标本身方差大比如小样本上的 accuracy换成roc_auc或者多次重复划分取平均会稳很多。第三类最隐蔽——某个参数在部分取值下会导致训练不收敛比如学习率过大导致损失爆炸那一折的得分就成了异常值均值被拉偏。这种情况的做法是把每一折的得分打印出来看异常值一眼就能识别而不是只看均值。# 快速诊断打印每折得分的极值差 for i, v in enumerate(param_range): spread valid_scores[i].max() - valid_scores[i].min() if spread 0.08: print(f参数 {v} 的各折得分离散度过大{spread:.3f}检查该取值下是否收敛)阈值的经验值是 0.08超过就值得看一眼。这个方法花不了几秒钟但能省掉大量对着锯齿图瞎猜的时间。5.2 曲线全程平直参数好像没起作用先确认参数真的传进去了。我见过有人把param_name写成C但模型是SGDClassifier它的正则参数叫alpha而不是C代码居然不报错——因为某些版本的封装会静默忽略无效参数。确认方法很简单在param_range里取两个差异极大的值手动训练两次打印模型属性看参数是否真的变了。另一个原因是参数范围设错。比如扫min_samples_leaf却给了[1, 2, 3]在这个区间内模型复杂度几乎没变化曲线当然是平的。范围要覆盖明显欠拟合到明显过拟合的完整区间如果画出来的线是平的八成是范围太窄。5.3 跑得太慢一个区间的要跑半小时validation_curve的总训练次数是len(param_range) × cv。20 个参数点、5 折、每次训练 3 秒就是 5 分钟换成梯度提升这种慢模型半小时很正常。我的提速顺序是这样的先把n_jobs-1加上通常能拿到接近线性的加速再把参数点从 20 减到 10先用粗网格定位大致区间然后在大数据上做子采样探索比如只用 30% 数据画第一版曲线定位拐点再用全量数据在拐点附近细扫。这三步下来多数场景的耗时能压到原来的十分之一以内而且结论基本不变——因为曲线形状主要由数据结构和模型容量决定样本量减到 30% 通常只是让整体分数低一点拐点位置移动很小。注意子采样探索只用于定位拐点最终选定参数值时一定要回到全量数据上复核否则可能因为子样本的分布偏差选错点。5.4 数据泄漏最贵的一个坑这个坑的代价是真金白银。早年做一个分类项目我在画验证曲线前把全量数据做了标准化用全量均值方差然后交给validation_curve做交叉验证。曲线漂亮得离谱验证得分 0.94比不加标准化高了 6 个点当时还以为找到了银弹。结果模型上线后掉到 0.83排查了半天才反应过来标准化的均值方差里包含了每一折验证集的信息等于模型提前偷看了答案。正确做法是把预处理和模型打包成Pipeline让每一折内部独立完成拟合from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LogisticRegression pipe Pipeline([ (scaler, StandardScaler()), (clf, LogisticRegression(max_iter1000)), ]) train_scores, valid_scores validation_curve( estimatorpipe, XX, yy, param_nameclf__C, # Pipeline 内的参数要用 步骤名__参数名 的写法 param_rangenp.logspace(-4, 4, 17), cvcv, scoringroc_auc, n_jobs-1, )那个clf__C的双下划线写法是 Pipeline 的约定第一次用很容易写成C然后报参数不存在的错。同样的道理适用于任何看全体数据才能算出统计量的预处理缺失值填充的中位数、特征选择的统计量、目标编码的映射表统统要放进 Pipeline。判断标准很简单——这一步如果用到了y或者用到了全量X的分布就必须进 Pipeline。5.5 问题速查表把上面这些整理成一张随手可查的表出问题时按行对号入座症状最可能的原因排查动作修复方式报错 param_name 不存在参数名写错或模型不匹配打印model.get_params().keys()用返回的键名原样填写验证得分异常高预处理泄漏检查标准化/填充是否在全量数据上做全部包进 Pipeline曲线锯齿严重折数少或指标方差大打印各折得分的极值差提到 10 折、换 roc_auc、重复多次训练耗时不可接受参数点多 模型慢统计 参数数 × 折数 × 单次训练时间粗扫、子采样、n_jobs-1曲线全程平直参数范围太窄或参数无效取两个极端值手动训练对比扩大范围、确认参数生效最优点在边界上扫描区间没覆盖到看曲线是否在端点仍在上升向该方向扩展 param_range换随机种子最优点跳变数据量不足或噪声大换 3 个种子各画一次增大数据、取平台段而非峰值6. 我的实操心得与进阶用法6.1 验证曲线不该单独看配合学习曲线更准单看验证曲线你只能判断参数该往哪调但没法判断是数据不够还是模型不对。这个信息必须靠学习曲线补上。学习曲线横轴是训练样本量如果两条线在样本量增加后仍然有很大缺口说明需要更多数据如果两条线很快收敛到一个较低的水平说明模型容量或特征表达不够加数据也没用得换模型。我现在的固定流程是先画学习曲线判断瓶颈类型再画验证曲线定参数值。顺序反了会浪费很多时间——在数据严重不足的情况下死磕参数结果往往是把模型调到刚好记住训练集。6.2 和 GridSearchCV 的配合方式验证曲线和网格搜索不是替代关系是先探路后精搜的关系。验证曲线的优势是给出形状让你知道参数和性能的关系曲线长什么样但它是逐参数独立扫描抓不到参数间的交互。所以我的做法是先用验证曲线为每个关键参数确定一个合理区间通常是拐点所在的一个数量级内再把这些区间交给GridSearchCV或RandomizedSearchCV做多参数联合搜索。from sklearn.model_selection import RandomizedSearchCV from scipy.stats import loguniform search_space { max_depth: [6, 7, 8, 9, 10], # 由验证曲线收窄得来 min_samples_leaf: [5, 10, 20, 50], # 由验证曲线收窄得来 max_features: [sqrt, log2, None], } rs RandomizedSearchCV( DecisionTreeClassifier(random_stateRNG), param_distributionssearch_space, n_iter40, # 从 5*4*360 种组合里随机抽 40 组省 1/3 时间 cvcv, scoringroc_auc, random_stateRNG, n_jobs-1, refitTrue, ) rs.fit(X, y) print(rs.best_params_, rs.best_score_)这样做的实际收益很明显不加约束直接做三参数网格搜索搜索空间可能是几千组组合用验证曲线收窄之后降到几十组时间省一个量级而且最终选出的参数更可信因为搜索区间本身是有依据的不是拍脑袋定的。6.3 值得写进个人脚本库的几个封装验证曲线的代码每次都要重复写一遍均值和标准差、画两条线加置信带、标最优点我干脆封装成一个函数塞进个人工具库。封装时加了几个实用选项scalelog自动切换横轴刻度top_k3输出验证得分最高的三个参数值方便对比gap_threshold超过阈值时自动打印过拟合告警。def plot_validation_curve(estimator, X, y, param_name, param_range, cv5, scoringaccuracy, scalelinear, top_k3, figsize(9, 5.5)): from sklearn.model_selection import validation_curve import numpy as np, matplotlib.pyplot as plt tr, va validation_curve( estimator, X, y, param_nameparam_name, param_rangeparam_range, cvcv, scoringscoring, n_jobs-1, ) tr_m, tr_s tr.mean(1), tr.std(1) va_m, va_s va.mean(1), va.std(1) # 输出 top_k 候选 order np.argsort(va_m)[::-1][:top_k] for i in order: gap tr_m[i] - va_m[i] flag -- 缺口偏大注意过拟合 if gap 0.15 else print(f{param_name}{param_range[i]}: valid{va_m[i]:.4f}, gap{gap:.4f}{flag}) fig, ax plt.subplots(figsizefigsize, dpi120) ax.plot(param_range, tr_m, o-, labeltrain) ax.fill_between(param_range, tr_m - tr_s, tr_m tr_s, alpha0.15) ax.plot(param_range, va_m, s--, labelvalid) ax.fill_between(param_range, va_m - va_s, va_m va_s, alpha0.15) if scale log: ax.set_xscale(log) ax.set_xlabel(param_name) ax.set_ylabel(scoring) ax.grid(alpha0.25) ax.legend() plt.tight_layout() return tr, va, fig封装之后新项目的标准动作就变成了三行代码画一张图探索成本极低自然就愿意多画几次。我个人体会是一个分析手段如果每次都要折腾五分钟配置环境用着用着就懒得用了反过来一旦它变成一次函数调用你就会不自觉地把它当成默认流程的一部分。工具顺手程度实实在在决定了它会不会被用起来。6.4 几个不成文的经验数值最后分享一组我攒下来的经验值不是定理但能省下不少试错时间交叉验证折数数据量过万用 5 折几千条用 10 折几百条用留一法或重复 5 次 10 折。折数不是越多越好10 折之后收益递减而计算量线性增长。参数扫描点数粗扫 10~15 个点足够定形状细扫在拐点附近每格 1 个单位。点数超过 30 时曲线上的细节已经小于噪声了纯属浪费。过拟合缺口阈值缺口超过 0.15 我基本就不看这个参数值的性能了直接往简单方向调缺口在 0.03 到 0.08 之间属于健康区间。最优值取值策略优先取验证得分平台段的前三分之一处而不是绝对峰值。同一件事我验证过很多次平台段前部的模型在独立测试集上的表现比峰值点更稳。复核方式定好参数后用完全独立的留出集再评一次这个集从头到尾不参与任何调参和选模。如果留出集得分和验证得分差超过 3 个点说明之前的曲线被某种泄漏污染了得回去重新检查 Pipeline。这套东西跑顺了之后调参这件事会从玄学试错变成一件有据可查、可复现、也能跟别人讲清楚的工作。最直接的变化是当你面对为什么这个模型用深度 8 而不是 15这样的问题时可以打开一张图指着两条线说深度 8 之后验证得分不再提升而训练和验证的缺口从 0.06 拉到了 0.21多出来的容量全部花在了记住噪声上。这比任何形容词都有说服力。
阅读完成 · 觉得有帮助?
咨询建站