恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
模型集成实战:从软投票到Stacking的提分技巧与避坑指南
首页
资讯中心
/
模型集成实战:从软投票到Stacking的提分技巧与避坑指南
模型集成实战:从软投票到Stacking的提分技巧与避坑指南
发布时间:2026/10/15 12:16:05
天池CV入门赛的记录写到第五篇终于轮到模型集成。前几个任务里我们把baseline跑通换了预训练分类模型又把学习率、增强策略调了一轮能稳定压过的分数基本都压干净了。接下来再想提分最有效的往往不是继续抠参数而是把手里的模型组合起来用——也就是TASK5的主题模型集成。这篇就把我在这个阶段做的完整思路、落地代码和翻车经验一起写出来给同样卡在瓶颈的朋友一个可以照着操作的参考。先说结论集成不是玄学它是一种把多个模型“各自的失误”互相抵消的技术。单个模型训到后期线下验证集分数基本是平的线上又有波动这时候靠集成往往能再往前顶一顶。但集成同样有前提没有足够多样的模型、没有统一的验证集划分、权重搜索做得不严谨都有可能出现“集成了反而掉分”的情况。下面我会按为什么做、怎么做、代码怎么落地、掉点怎么排查、算力不够怎么办这五块展开。1. 为什么之前不集成现在才做单模型走到头的信号是什么1.1 单模型调参收益递减的阶段特征如果你和我一样是按任务顺序做下来的会发现前面的流程很顺利刚开始从0跑到baseline准确率一天一个样换一个大一点的预训练模型线上分数能明显涨再调整学习率、加数据增强又能涨一截。但到某个节点之后训练曲线和验证曲线开始趋平最优epoch反复出现在相近的位置线上分数在很小的区间内抖动怎么调都突破不了那个上限。这个阶段就是典型的“单模型瓶颈”。继续增加训练轮次过拟合风险上升继续调学习率只是在小范围内震荡换更强的数据增强验证集分数可能反而先降。这时候再花两三天去调参性价比已经很低。我个人的判断标准是连续三轮实验验证集指标变化不超过0.1到0.2个百分点就可以把注意力转移到模型集成上。1.2 集成为什么能在分类任务里生效模型集成的核心逻辑很简单多个模型的预测结果综合起来比单个模型更稳。分类任务里每个模型都有自己的偏置和随机性两个模型犯错的位置不会完全重合。A模型判断错的那张图B模型大概率是对的把两者的概率取平均正确信号会被保留下来错误信号则被稀释。可以拿裁判打分来类比一个人打分主观性很强但几个水平相近的裁判各自打分再取平均结果通常更接近真实水平。从统计角度看当模型之间误差的相关性越低平均之后的方差下降就越明显。入门阶段不需要把偏差方差公式推得很细记住这个方向就行集成追求的是“多样化模型之间的互补”而不是简单重复同一个结果。1.3 集成救不了欠拟合也补不了数据问题必须提前泼一盆冷水。集成对已经拟合得不错的模型是提分利器但它不能解决欠拟合。如果单个模型在训练集上准确率只有七成验证集也一直上不去这说明模型容量不够或者数据预处理有问题这时候堆集成只会把几个同样水平的预测平均起来结果仍然是七成。先把单模型质量问题解决再谈集成顺序不能反。另外数据集本身有严重问题比如标注错误、类别严重不均衡集成也无能为力。这些属于数据层面的硬伤应该在前面几个任务里处理掉。2. 三类集成方法的选型逻辑软投票、Blending与Stacking2.1 预测概率平均软投票性价比最高的起点集成里最简单也最稳的做法是把多个模型对每个类别的预测概率直接取平均哪个类别的平均概率高就选哪个。关键点是使用概率而不是最终的0/1标签这就是软投票和硬投票的区别。硬投票是少数服从多数三个模型里两个说A结果就是A。但问题在于说A的两个模型可能概率只有51%说B的模型概率是99%硬投票会丢掉这个置信度信息把原本清晰的结果变得模糊。软投票保留概率数值相当于每个模型按自己的信心程度发言。我建议拿到一组训练好的模型后第一步就做这件事把每个模型的测试集预测概率保存下来直接平均提交一次。这一步几乎不消耗额外训练时间但经常能带来稳定的线下提升。实现代码很短import numpy as np # 假设有3个模型分别对测试集输出了概率矩阵 test_probs_1/2/3 # 每个矩阵形状是 (测试样本数, 类别数) final_probs (test_probs_1 test_probs_2 test_probs_3) / 3.0 predictions np.argmax(final_probs, axis1)2.2 Blending让第二层模型学习组合权重当模型数量变多、性能有差异时直接等权平均不一定最优。Blending的思路是从训练集里留出一部分数据比如10%到20%第一层的模型在留出集上做预测把这些预测概率拼成一列一列的新特征再用一个简单的第二层模型通常是逻辑回归去学习“第一层模型的预测和真实标签之间的关系”。Blending的好处是简单、不容易过拟合因为留出集只参与一次预测第二层模型看到的样本量有限学不到太复杂的规则。缺点则是留出的那部分数据没有参加第一层模型训练对样本量不大的比赛来说有点浪费。它在时间紧张、只有几个现成模型时可以快速得到一个比等权平均更好的组合方式。2.3 Stacking用K折交叉预测生成更完整的特征Stacking可以理解为Blending的K折版本。把训练集切成K份每次用K-1份训练模型、对剩下那1份做预测轮完K个fold后每个训练样本都拿到了一份“没见过该样本的模型”给出的预测概率这些概率拼起来就是OOFOut-of-Fold预测。OOF的分布更接近真实测试场景比单次留出集更稳定。等第一层模型全部跑完第二层模型用OOF概率作为输入特征、原始标签作为目标去训练。这里有一个关键纪律第二层模型必须简单。我最常用的是带L2正则的逻辑回归几乎不会去用XGBoost之类的复杂模型做第二层因为第一层输出的特征维度一般不高信息量也有限复杂模型很容易把OOF里的噪声一并记住线下好看、线上翻车。三种方法的取舍可以看这张表方法数据使用效率实现复杂度过拟合风险适用场景软投票/概率平均高只需测试集概率最低极低所有模型已训练好快速提交Blending中留出部分训练数据低低模型数量少、时间紧张时找组合方式Stacking高通过K折利用全部数据中中取决于第二层模型有充足时间和算力追求更高上限对入门赛来说我这次TASK5实际用到的主要是“多折模型平均加权权重搜索”没有单独做复杂的Stacking因为第一层模型的OOF已经能帮我们找到相当好的融合权重了。3. 实战环节OOF收集、测试时增强与权重搜索的落地代码3.1 训练阶段就为集成打基础统一折划分并保存OOF集成做得严不严谨很大程度上取决于你有没有一份干净的OOF预测。我见过不少新手把“每个模型的验证集准确率”拿出来对比一下然后草率地分配权重这其实是在拿不同来源的数据东拼西凑。正确做法从一开始就固定折划分所有模型共用同一套训练/验证索引。以五折交叉验证为例训练时每个fold把验证集的预测概率保存下来最后汇总成完整的OOF矩阵。这份OOF是后续做权重搜索、判断模型多样性、排查集成问题的统一标尺。核心流程如下import numpy as np import torch import torch.nn.functional as F from sklearn.model_selection import StratifiedKFold def predict_proba(model, loader, devicecuda): model.eval() preds, targets [], [] with torch.no_grad(): for images, labels in loader: images images.to(device) logits model(images) probs F.softmax(logits, dim1).cpu().numpy() preds.append(probs) targets.append(labels.numpy()) return np.concatenate(preds), np.concatenate(targets) # 用固定随机种子统一折划分保证所有模型看到同一份验证集 skf StratifiedKFold(n_splits5, shuffleTrue, random_state2024) oof_probs np.zeros((len(train_dataset), num_classes)) test_probs_folds [] for fold, (train_idx, valid_idx) in enumerate(skf.split(train_dataset, train_labels)): train_loader make_loader(train_dataset, train_idx, is_trainTrue) valid_loader make_loader(train_dataset, valid_idx, is_trainFalse) model build_model(resnet50).to(cuda) # 训练若干epoch每个epoch后记录验证集准确率取最优权重保存 best_acc 0.0 for epoch in range(config.epochs): train_one_epoch(model, train_loader, optimizer, scheduler) acc evaluate(model, valid_loader) if acc best_acc: best_acc acc torch.save(model.state_dict(), ffold{fold}_best.pth) model.load_state_dict(torch.load(ffold{fold}_best.pth)) valid_probs, _ predict_proba(model, valid_loader) oof_probs[valid_idx] valid_probs test_probs_folds.append(predict_proba(model, test_loader)[0]) # 测试集多折预测取平均这是最基础的一组集成 final_test_probs np.mean(test_probs_folds, axis0)代码里的make_loader、build_model是示意函数实际比赛时替换成自己的数据加载和模型构造逻辑就行。重点是每个fold都保存最好的权重并在测试集上单独输出一批预测最后再多折平均。很多情况下哪怕只做“五折平均”这一件事线上分数都会比单模型最优checkpoint要稳。3.2 测试时增强TTA不重训也能白赚一个集成的效果TTA的全称是Test Time Augmentation中文叫测试时增强。它的原理很好理解训练时模型见过各种增强后的图片推理时我也把同一张测试图生成几个增强版本分别预测再取平均相当于让模型“多视角看这张图”。这个技巧不消耗额外训练时间推理成本翻几倍却常常能带来稳定收益。使用TTA有一个基本纪律只能用训练阶段见过的增强方式不能随手放大招。我最常用的TTA组合是原图加水平翻转。有些比赛任务里多尺度缩放也能带来收益但前提是你的训练脚本里本身就用到了多尺度。一个简单实现from torchvision import transforms normalize transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) # 只保留训练阶段见过的变换 base transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), normalize, ]) ttas [ base, transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p1.0), # 确定性水平翻转 transforms.ToTensor(), normalize, ]), ] def predict_with_tta(model, image): model.eval() prob_list [] with torch.no_grad(): for t in ttas: x t(image).unsqueeze(0).to(cuda) prob_list.append(F.softmax(model(x), dim1).cpu().numpy()) return np.mean(prob_list, axis0)注意RandomHorizontalFlip(p1.0)在这里是强制水平翻转等价于调用transforms.functional.hflip。这样写是因为transforms.Compose里不能直接传函数用p1.0比较省事。另外提醒一句TTA里不要塞入随机裁剪、随机旋转这类训练时没见过的大范围扰动否则会让模型看到和训练分布不一致的输入反而拉低效果。3.3 权重搜索从等权平均到最优组合多折平均和TTA都做完之后如果还想把多模型融合得更精细可以用验证集上的OOF概率做权重搜索。思路很简单给每个模型分配一个权重权重和为1然后穷举或优化一个目标函数找到让验证集损失最小的那组权重。我最常配的是scipy.optimize.minimize目标函数用sklearn.metrics.log_loss因为多分类交叉熵对概率分布的质量非常敏感比直接优化准确率更能反映预测置信度。代码如下from sklearn.metrics import log_loss from scipy.optimize import minimize # 假设有3个模型分别存好了OOF概率 model1_oof/model2_oof/model3_oof # 每份形状都是 (训练样本数, 类别数)train_labels 是对应的真实标签 model_oofs [model1_oof, model2_oof, model3_oof] def blend_loss(weights): weights np.abs(weights) # 防止负权重 weights weights / weights.sum() # 归一化 blended sum(w * p for w, p in zip(weights, model_oofs)) return log_loss(train_labels, blended) init np.ones(len(model_oofs)) / len(model_oofs) res minimize(blend_loss, init, methodNelder-Mead) best_weights np.abs(res.x) best_weights best_weights / best_weights.sum() print(最优权重:, best_weights)如果不想引入scipy也可以用最简单的网格搜索权重步长取0.1三个模型就是66种组合两两遍历一遍就能找到接近最优的结果。权重搜索有一个容易踩的坑搜索出来的最优权重是对OOF数据的局部适配千万不要在最终测试集上反复验证后再偷偷调整权重那样相当于用测试集做训练线上会失真。用OOF选一次权重定下来就不改了。4. 集成后分数反而降了最常见的四个坑与排查链路集成不是按下按钮就能涨分我这次TASK5就在一套组合上翻过车线下明明涨了线上反而掉。这里把几个高频原因和排查思路完整写出来方便你对照。4.1 多样性不足看似三个模型其实是一家人在投票我最初做集成时很偷懒把同一个模型用三个随机种子各训练一份然后取平均心想这样总归能涨一点。结果是线下几乎不动。原因是这三个模型共享同一个backbone和同一个预训练权重训练数据的顺序和增强虽然略有差异但学到的特征高度重叠错误也高度相关平均起来抵消不掉多少。排查多样性最直接的方法是看OOF概率之间的相关性把两模型对同一样本的预测向量算相关系数越接近1说明它们越像。我的经验数值是相关系数超过0.9时融合收益很小低于0.85时开始有可感知的提升0.8以下效果明显。如果发现手里几个模型相关度过高与其硬融合不如换一个不同架构的模型或换一个输入分辨率重新训练多样性带来的收益往往比多训一个同构模型大得多。4.2 验证集切分不一致OOF和fold划分完全错位另一个隐蔽的坑是不同模型用了不同的随机种子做折划分。比如模型A用seed 42五折模型B换成了seed 2024五折它们各自的OOF覆盖的训练样本虽然一样但对应关系被打乱了权重搜索时把不同划分下的预测混在一起结果自然不可信。这也是为什么我在前面的代码里反复强调固定随机种子。所有模型必须沿用完全相同的fold划分索引确保OOF矩阵的每一行对应同一个训练样本。如果中途发现已经混了只能重训相关模型没有捷径。这个坑我踩过一次之后现在写训练脚本第一行就把seed固定下来并在保存OOF时用np.save把索引一并存下来方便复查。4.3 第二层模型选得太猛线上和线下的差距来自过拟合做Stacking或Blending时第二层模型的选择直接决定成败。我见过一个很典型的案例朋友用XGBoost当第二层训练集上logloss压得很低线下验证集也很漂亮结果提交后线上反而比单模型差。原因就是XGBoost在OOF几万个样本量和十几个特征维度上依然“学得太狠”把第一层模型的噪声也背了下来。第二层模型请老老实实用逻辑回归正则强度C控制在0.01到1.0之间。如果第一层模型的预测概率本身质量不错逻辑回归能学到的边际信息其实很少它的价值更多是做一个稳健的加权器。别为了“听起来高级”而上复杂模型竞赛里分数不会奖励花哨。4.4 TTA过度增强推理时给了模型没见过的东西TTA翻车也常见。曾有人把随机旋转、随机裁剪、颜色抖动全塞进TTA理由是想让模型更“鲁棒”结果验证集分数当场下滑。原因不复杂模型训练时只见过轻微扰动和水平翻转测试阶段你却给它看旋转45度的图它可能直接输出一个错误分布的概率。TTA的本质是让输入贴近训练分布而不是让模型应付所有视角。我现在的TTA策略非常保守原图、水平翻转、必要时加一个多尺度缩放。如果比赛本身包含垂直翻转或旋转增强训练脚本里同样有这种变换才考虑在TTA里加。验证集上做一个0.1到0.2个百分点的对比测试有提升就保留没有就砍掉绝不靠直觉堆变换。5. 算力有限时的集成策略不追求“全都要”只求有效增益5.1 快照集成训练尾声顺手捞几个checkpoint真到了算力紧张的时候连多训一个模型都很奢侈这时可以试试快照集成Snapshot Ensemble。做法是在一个完整训练过程的最后阶段不只在最优epoch保存权重而是把最后几个epoch的checkpoint都留下来推理时取它们的概率平均。这些checkpoint虽然来自同一个训练轨迹但在训练后期权重会在局部最优附近波动平均之后常常能抵消一部分最后的震荡让线上分数更稳。这个技巧严格说不是标准意义上的快照集成那需要循环学习率配合但入门赛里你直接保存最后几个epoch也能获得类似效果。我有一次在只剩半天时间时把最后8个epoch的checkpoint平均了一下比单取最优checkpoint高了0.15个百分点几乎零成本。5.2 两个好模型够用了第三个加加看加不进去就删很多新手误以为集成就是模型越多越好五个不够就十个。但实际上集成收益随着模型数量增加会迅速衰减。我们这次比赛里单模型加TTA线下提升了约0.2个百分点再加一个不同架构的模型又涨了约0.3个百分点等想加第三个时OOF logloss几乎不动了第四个反而略有回退。我的习惯是每加入一个新模型都要先在OOF上算两组融合结果对比不加它的logloss和加它的logloss如果提升小于0.01就果断放弃。集成是减法艺术不是加法堆砌。省下来的推理时间不如去把前两个模型的权重搜索做得更细。5.3 时间分配建议单模型打磨在前集成收尾在后如果你现在刚进入TASK5我建议把整体时间按8:2分配前80%时间继续打磨单模型质量包括架构选择、增强策略、训练细节最后20%时间留给集成。集成是收尾动作不是逃避单模型调参的借口。一个单模型验证集准确率做到95%比三个90%的模型硬融在一起更靠谱。集成之前的准备工作其实很简单确认所有模型的OOF和测试预测都保存为npy文件确定统一的fold划分和seed记录每个模型的关键配置架构、输入尺寸、增强列表、最佳epoch。把这些信息整理成一个对照表后面做融合、排查问题、写本地推理脚本都会顺手很多。最后再分享一个我自己的习惯每集成一个模型之前先单独把它的OOF概率保存成npy并在验证集上独立测一遍准确率和logloss。集成收益是一种“剩下来的补偿”而不是“模型差的救赎”。把单模型基础打牢OOF数据收干净TASK5之后的分数大概率能进入一个自己都意外的区间。至于更进阶的模型融合技巧那就是另外的故事了。