1. 先把这场比赛在考什么搞明白
1.1 拆开标题里的三个词
我报名这次暑假赛之前,先花了一个晚上把标题里的三个词拆开看:开悟、AIArena、暑假赛。这三个词各自代表一种约束,理解错了,努力方向就会偏。
开悟是平台属性,意味着它带着明显的教学和引导色彩——官方一般会给任务说明、基线代码、数据说明文档,甚至给一些预备知识清单。这一点跟纯粹的工业比赛不一样,工业比赛常常只给你一堆数据和一句"预测目标值",剩下全靠自己猜。有引导的好处是上手快,坏处是很多人会直接复制官方基线然后原地不动,最后卡在一个不上不下的分数上。
AIArena是竞技属性,核心就是排行榜。排行榜意味着你的模型要在一个你看不见的测试集上跑出分数,也意味着你没法靠"我本地验证集99%"来自我安慰。这个属性直接决定了后面所有的技术选择:必须防过拟合,必须做稳定的验证切分,必须留出提交次数。
暑假赛是时间属性。周期短、集中、连续,通常两三周到一个多月。这个约束最容易被新手忽略,但它其实决定了你的策略应该是"快速迭代多个可复现的实验"而不是"死磕一个模型调到底"。
把这三层想明白之后,我给自己定了一个原则:先保证有一个能稳定提交的基线,再谈提分。这个原则在后面救了我至少三次。
1.2 我的三条线打法:基线线、提分线、认知线
参加这类比赛,最容易出现的问题是"东一榔头西一棒子"——今天看到别人用卷积网络提了分,赶紧换;明天看到有人用数据增强又提了分,又赶紧加。结果是实验记录一团乱,最后连哪个改动有效都说不清。
我给自己划了三条并行的线。
基线线的任务只有一个:跑通。数据能读进来、模型能训起来、结果能提交上去、榜单上有个分数。哪怕这个分数很低,只要流程是通的,基线线的任务就算完成。我在这次比赛里用的基线是一个两层全连接网络,验证集准确率比瞎猜好不了多少,但它让我在开赛第二天就完成了第一次提交。
提分线的任务是找"性价比最高的那几刀"。注意是"性价比最高",不是"效果最好"。有些改动能提0.5个点但要重训八小时,有些改动能提0.3个点只要改三行代码,后者明显更值得先做。我给自己定了个粗略的优先级:数据层面的改动 > 训练策略层面的改动 > 模型结构层面的改动 > 集成层面的改动。
认知线最容易被忽略,但对我个人价值最大。我每天收工前会花十分钟写一句复盘:今天试了什么、结果如何、为什么。这个习惯看着很土,但在比赛后半程,当我需要对十几个实验做取舍的时候,这份记录就是唯一可靠的东西。
1.3 四十天的时间节奏怎么排
时间分配上我踩过坑。第一次参赛的时候我前十天全在"学习理论",等到要写代码了发现手生,又花五天重新捡起来。这次我换了个更实用的排法,把学习和动手交替起来。
| 阶段 | 大致天数 | 主要目标 | 交付物 |
|---|---|---|---|
| 热身期 | 第1-5天 | 环境跑通、看完任务说明、跑通官方基线 | 一次成功提交 |
| 基线期 | 第6-12天 | 搭自己的训练脚本,固定验证集 | 可复现的训练代码 |
| 提分期 | 第13-28天 | 数据增强、结构调整、调参 | 实验记录表 |
| 冲刺期 | 第29-38天 | 模型融合、TTA、结果稳定化 | 最终提交方案 |
| 复盘期 | 第39-40天 | 整理代码与笔记 | 可复用的项目模板 |
这张表真正有用的地方不是天数,而是每个阶段都必须有明确交付物。没有交付物的阶段,时间一定会被浪费掉。我个人最看重的是"基线期",因为一个可复现的训练脚本,价值远大于一次偶然的高分。
2. 赛前准备:环境、数学、资料三件事
2.1 环境配置的核心是先算清楚显存
深度学习环境这件事,新手最容易陷入的误区是"先把框架装上再说"。我的建议是先反推:手头有多少算力,再决定用什么框架、什么模型规模。
显存估算有个粗略但好用的公式。以训练为例,显存占用大致等于:
参数量 × 4字节(权重) + 参数量 × 4字节(梯度) + 参数量 × 8字节(Adam的两个动量) + 激活值
也就是说,用Adam训练一个1000万参数的小模型,光优化器相关就要占掉大约160MB,加上激活值往往要到几百MB。这只是保守下限,实际能不能跑起来还要看batch size和输入分辨率。我一般会先把batch size设成8或者16试一次,跑得动再往上加。
环境创建我习惯用conda,隔离得干净,出问题直接删掉重建:
conda create -n kaiwu python=3.10 -y conda activate kaiwu pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121 pip install numpy pandas matplotlib scikit-learn tqdm tensorboard这里有两个细节值得说。第一,Python版本不要追最新,3.10左右是最稳的区间,太新的版本经常遇到某个依赖还没出对应轮子,编译到一半报错。第二,框架和驱动版本要对应,我遇到过驱动版本偏低导致装完框架torch.cuda.is_available()返回False的情况,查了两个小时才发现问题在驱动。
装完之后一定要写三行验证代码:
import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0) if torch.cuda.is_available() else "CPU only")这三行的意义在于:在写任何模型代码之前,先把"是环境问题还是代码问题"这个变量排除掉。后面调参调到怀疑人生的时候,你会感谢自己做了这一步。
至于用本地机器还是用平台提供的在线环境,我的判断标准很简单:如果数据集规模在几个GB以内、模型参数量在千万级以下,本地跑就够了;如果数据量很大、或者本地显卡显存不足,就直接用平台环境,别在环境上耗时间。比赛期间时间是最贵的资源。
2.2 数学部分只需要一个"最小必要集"
很多人一提到深度学习的数学就打退堂鼓,其实打比赛用到的数学远比想象中少。我自己总结的最小必要集只有三块:泛化误差、反向传播、偏差方差。
先说自己感受最深的泛化误差。训练集上的误差叫经验误差,测试集上的误差叫泛化误差,两者之间的差距叫泛化间隙。比赛里所有的技术动作,本质上都是在做同一件事:让泛化间隙变小。数据增强是让模型见过更多变化、正则化是限制模型复杂度、早停是防止模型在训练集上跑太远。想明白这一层,就不会再盲目堆技巧了。
反向传播我一开始是被推导过程吓住的,后来发现抓住一句话就够了:它是链式法则的高效实现。前向传播算出预测值和损失,反向就是把损失对每一层参数的偏导从后往前一层层乘回来。真正需要理解的不是公式本身,而是它的两个推论:一是靠近输入的层梯度容易变小,这就是深层网络难训的根源;二是学习率太大时梯度更新会震荡,因为乘出来的梯度链被放大了。
偏差方差这一对概念解释了很多现象。模型太小、容量不足,偏差高,表现是训练集和验证集都差,这叫欠拟合;模型太大、数据太少,方差高,表现是训练集很好但验证集差,这叫过拟合。这两个诊断结论直接决定你下一步该加参数还是该加正则,判断错了会浪费好几天。我在热身期就遇到过这个问题,验证集准确率一直卡在六成左右,我第一反应是模型不够强,加了一层又一层,结果毫无起色。后来把训练集准确率打出来一看,训练集也只有六成多——这明显是欠拟合,该做的是把学习率调大、把训练轮数加够,而不是加层数。
2.3 资料清单和它们的正确打开方式
资料这块我的态度是:不要贪多,但要知道每份资料是干什么用的。
吴恩达的课程适合打地基,讲得慢但清楚,尤其是关于正则化、优化算法、诊断模型的几节,属于看一遍就能用上的内容。缺点是作业偏简单,光看完课不动手,写代码时还是会卡。
《深度学习入门:基于Python的理论与实现》这类书,价值在于从零实现一遍前向和反向。很多人觉得"有框架了为什么还要手写",我的实际体会是:手写一遍之后,你对"张量形状不对"这类报错的敏感度会提升一个档次,因为你知道每一步的形状是怎么来的。
邱锡鹏那本《神经网络与深度学习》更偏理论体系,适合在遇到"为什么这么做"的问题时去查,不适合从头到尾啃。动手深度学习那本更偏工程实践,代码多,适合边看边敲。
至于MATLAB那边的神经网络工具箱,我个人的用法是拿它做快速验证。工具箱里拖几个模块就能搭出一个前馈网络,用来验证一个想法值不值得投入时间,比写Python快得多。真要打比赛,长期还是得回到代码里,因为可定制性差太多了。
工业视觉方向还有专门的深度学习工具,比如Halcon的深度学习模块,它的优势是训练和部署一体化,做缺陷检测这类落地项目非常省事。但比赛阶段用它意义不大,因为比赛的评测接口是固定的。我把它列进学习清单,是为将来做工程项目留的后路。
提示:资料看得再多,也不如把一个数据集从头到尾跑三遍。每跑一遍你会注意到之前完全没留意的问题。
3. 网络结构选型:全连接、卷积、循环到底怎么用
3.1 前馈网络和BP:最容易被跳过的那个基线
前馈神经网络,也就是常说的多层感知机,结构简单到一句话能说完:输入层、若干隐藏层、输出层,层与层之间全连接。它的训练靠的就是反向传播加梯度下降。
我建议每个人都用MLP先跑一个基线,理由有两层。第一层是工程上的:MLP的代码最短,出问题的概率最低,跑通它意味着数据管道、训练循环、验证逻辑、提交格式都没问题了。第二层是认知上的:MLP的参数量你算得清楚,因此你知道模型容量到底有多大。
举个具体例子。输入是28×28的灰度图,展平后是784维,接一个256维隐藏层,再输出10类:
- 第一层参数:784 × 256 + 256 = 200,960
- 第二层参数:256 × 10 + 10 = 2,570
- 合计约20.4万
一个20万参数的小网络,在几万条数据上的表现基本就是"能跑但不惊艳"。这个认知很重要,因为它给了你一个参照系:后面换成卷积网络后参数量降到了几万,效果反而更好,你就能直观理解"参数量不等于能力"。
用MLP有几个实操注意点:
- 输入一定要归一化。像素值从0-255压到0-1,损失曲线会平顺很多,不归一化很容易出现第一轮损失就炸掉的情况。
- 权重初始化别用全零。全零会导致同一层的神经元梯度完全相同,网络退化成只有几个有效单元。用默认的Kaiming或者Xavier初始化就行。
- 随机种子要固定。同一份代码两次跑出不同结果,排查起来非常痛苦。
3.2 图像任务里为什么卷积网络能压住全连接
"图像处理为什么用CNN不用前馈网络"这个问题,我一开始的答案很敷衍——书上都说CNN好。后来自己算了一遍参数量,才算真的理解。
还是28×28的输入。如果用全连接,第一层每个神经元都要连784个输入,256个神经元就是20万个参数。而一个3×3的卷积核,即使输出32个通道,参数量也只有3 × 3 × 1 × 32 + 32 = 320个。差了三个数量级。
但参数量少只是表象,真正的差别在归纳偏置。图像有两个天然性质:局部相关和平移不变。相邻像素关系密切,一个特征出现在左上角还是右下角,本质是同一个特征。卷积通过局部连接和权重共享,把这两条先验直接写进了结构里。全连接网络没有这个先验,它必须从数据里学出"左上角的角和右下角的角是同一回事",这需要大量样本。
我用一张表概括一下两者的差别:
| 维度 | 前馈网络(MLP) | 卷积网络(CNN) |
|---|---|---|
| 参数规模 | 随输入尺寸平方增长 | 与输入尺寸基本无关 |
| 空间结构 | 展平后丢失邻接关系 | 保留二维邻接关系 |
| 平移不变性 | 需要从数据中学 | 结构自带 |
| 小数据集表现 | 容易过拟合 | 相对更稳 |
| 适合的场景 | 表格数据、低维特征 | 图像、语音谱、时序信号 |
卷积网络的经典入门结构是LeNet-5这类小网络:两个卷积层加池化,再接全连接分类头。它的层数很浅,但在手写数字这种任务上表现相当好。我建议第一次搭CNN就从这种结构开始,别一上来就堆ResNet,因为你还没建立"每加一层会发生什么"的直觉。
搭CNN时我踩过的坑集中在形状上:
- 卷积分辨率和通道数要一起看。经过几次下采样后,特征图会变得很小,这时候再加池化可能直接就变成1×1了。
- 全连接层之前的展平维度一定要算准。我最常犯的错就是这里,解决办法是在展平前打印一次形状。
- 批归一化的位置。一般放在卷积之后、激活之前,用起来收敛会明显更快。
顺便说一下ViT和EfficientNetV2这类相对新的结构。ViT把图像切成patch当作序列处理,在数据量大的时候表现优异,但数据量小的时候往往不如CNN,因为它缺少卷积的那套先验,需要更多数据来补。EfficientNetV2则在精度和速度之间做了比较好的平衡,做图像分类时可以直接拿来当骨干网。我的建议是:比赛初期先用自己搭的小CNN建立基线,中期再考虑换骨干网,因为换骨干会引入一堆新的超参,需要重新调。
3.3 序列任务里的循环网络和它的替代选项
如果任务涉及序列——文本、时序信号、音频帧——那单靠卷积就不太够了,需要能记住"上文"的结构。
循环神经网络的核心思想是维护一个隐藏状态,每一步都把它和当前输入一起送进网络,更新后再传给下一步。它在结构上天然适合变长序列。但基础的RNN有个硬伤:梯度消失。序列一长,反向传播时梯度连乘很多次,很快就趋近于零,模型学不到长距离依赖。
LSTM通过门控机制缓解了这个问题:遗忘门决定丢掉多少旧信息,输入门决定写入多少新信息,输出门决定暴露多少给下一步。这套机制让LSTM在长序列上稳定很多,代价是参数量翻了几倍,训练也更慢。
现在的实际选择通常是这样:
- 序列不长、追求速度:一维卷积就能做得不错,把序列当一维图像处理。
- 序列较长、需要长距离依赖:LSTM或者GRU。
- 数据量大、有充足算力:注意力机制那一类结构,效果通常更好,但对数据和算力要求更高。
还有一个方向值得一提,就是把图结构引进来。如果数据本身带有关系结构(比如节点之间的连接关系),图神经网络会比单纯堆卷积或循环层更合适。我这次比赛没用到,但在看别人的方案时注意到,处理关系型数据的队伍里用图结构的不少。这条线可以作为暑假之后的延伸方向,慢慢啃。
3.4 选型拿不准时看这张表
实战中我基本是靠这张表做第一轮决策的,能省掉大量犹豫时间:
| 任务形态 | 首选结构 | 备选 | 主要风险 |
|---|---|---|---|
| 表格/低维特征 | MLP | 梯度提升树 | 特征未归一化导致不收敛 |
| 二维图像分类 | 小CNN | 预训练骨干网 | 输入尺寸与下采样不匹配 |
| 长序列建模 | LSTM/GRU | 一维卷积 | 序列过长导致训练慢 |
| 关系型数据 | 图结构网络 | 手工特征+MLP | 图构造方式不对,白做 |
| 数据极少 | 预训练+微调 | 强增强+小模型 | 直接训练必然过拟合 |
这张表的用法是"先定方向,再调细节"。方向错了,调参调到天亮也没用。
4. 一次完整实操:从读数据到提交榜单
4.1 数据检查:先看,再动手
我现在养成了一个习惯:拿到数据的前半小时不写任何模型代码,只看数据。这一步看起来浪费时间,实际上是最省时间的一步。
要看的东西包括:类别分布是否均衡、图像尺寸是否统一、有没有损坏文件、标签有没有明显错误。我这次就发现训练集里有几十张图尺寸和其他样本不一样,如果直接送进网络,会因为维度不匹配直接报错,排查起来又是半小时。提前发现,统一resize就解决了。
数据划分上,我的做法是先用固定随机种子切出验证集,一般取训练数据的15%到20%。如果类别不均衡,就分层抽样,保证验证集的类别分布和训练集一致。这个细节很重要,因为验证集分布偏了,你会得到错误的反馈信号,据此做的所有调参决策都是错的。
import numpy as np from sklearn.model_selection import train_test_split SEED = 42 X_train, X_val, y_train, y_val = train_test_split( X, y, test_size=0.15, random_state=SEED, stratify=y )4.2 一个可以反复复用的训练脚本骨架
比赛期间我的经验是:训练脚本要写得"笨"一点,但必须稳定。花哨的写法在调参阶段会变成负担。下面这个骨架我用了整个暑假,改动很少。
import torch import torch.nn as nn from torch.utils.data import DataLoader, TensorDataset from tqdm import tqdm def set_seed(seed=42): torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) np.random.seed(seed) def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss, correct, total = 0.0, 0, 0 for x, y in tqdm(loader, leave=False): x, y = x.to(device), y.to(device) optimizer.zero_grad() out = model(x) loss = criterion(out, y) loss.backward() optimizer.step() total_loss += loss.item() * x.size(0) correct += (out.argmax(1) == y).sum().item() total += x.size(0) return total_loss / total, correct / total @torch.no_grad() def evaluate(model, loader, criterion, device): model.eval() total_loss, correct, total = 0.0, 0, 0 for x, y in loader: x, y = x.to(device), y.to(device) out = model(x) loss = criterion(out, y) total_loss += loss.item() * x.size(0) correct += (out.argmax(1) == y).sum().item() total += x.size(0) return total_loss / total, correct / total有了这两个函数,主循环就只剩十几行:每轮训练、每轮验证、记录最好的那一版权重。
关键点在于"保存验证集最优权重",而不是保存最后一轮的。训练轮数给多了以后,模型一定会在某个点之后开始过拟合,保存最后一轮等于主动放弃了前面最好的状态。我这次比赛里,验证集最优出现在第23轮,而我一共训了40轮,如果不做这个保存,分数会低一截。
4.3 超参数的取值逻辑,不是玄学
超参调参我以前是靠感觉,后来发现其实有比较清晰的逻辑链条。
学习率是最关键的一个。它太大,损失会震荡甚至发散;太小,收敛慢到让人绝望。我的做法是先做一次粗略的扫描,取值在1e-1到1e-4之间跨数量级试几个,看哪一段损失下降最快,再在这个区间内细化。经验值是:配合Adam,1e-3是个很稳的起点;用SGD的话,1e-2左右更常见。
批大小跟学习率是联动的。批大小翻倍,学习率大致也可以翻倍,这个缩放关系在多数任务上成立。批大小大的好处是训练稳定、吞吐高,坏处是泛化性有时会略差。小批大小噪声大一些,反而常常能带来更好的泛化。
训练轮数不要靠猜,通过观察验证集曲线来定。我一般会先设一个偏大的值(比如50轮),然后看验证集在第几轮开始不再提升。
| 参数 | 常见起点 | 表现不对时的信号 | 调整方向 |
|---|---|---|---|
| 学习率 | 1e-3(Adam) | 损失震荡或长时间不降 | 先降一个数量级 |
| 批大小 | 32/64 | 显存不足或梯度噪声太大 | 结合学习率一起调 |
| 权重衰减 | 1e-4 | 训练准确率远高于验证准确率 | 适当加大 |
| 训练轮数 | 30-50 | 验证损失开始回升 | 加早停 |
| 数据增强强度 | 中等 | 训练准确率上不去 | 减弱增强 |
早停这个技巧值得单独说。它的实现很简单:记录验证集最优分数,如果连续N轮没有提升就停掉训练。它同时解决了两个问题——省时间和防过拟合。我在比赛后期把早停的容忍轮数设成7轮,效果比较稳。
4.4 提分手段的性价比排序
比赛到中期,你会发现可选的提分手段一大堆,这时候排序就变得非常重要。我的排序大致是这样,从最推荐到最不推荐:
第一档:数据增强和验证集质量。这两件事几乎零成本,收益却很直接。图像任务里,随机裁剪、水平翻转、轻微色彩抖动就能带来一两个点的提升。验证集切分做得干净,你的所有后续判断才有依据。
第二档:训练策略优化。学习率调度(比如余弦退火)、warmup、标签平滑,这些改动通常只要几行代码。
第三档:换更强的模型结构。比如从小CNN换到预训练骨干网。收益可能不小,但代价是重新调参,而且推理时间会变长。
第四档:测试时增强和模型融合。也就是TTA和多模型投票。这类手段能提分,但成本高——TTA会让推理时间翻几倍,多模型融合需要训好几个模型,训练时间成本翻倍。
第五档:伪标签、自训练这类半监督手段。这类方法有一定门槛,做不好反而掉分,建议等前面的都做扎实了再碰。
注意:永远不要同时改两个变量。一次只改一个,跑完看结果,记录,再动下一步。同时改两个,你永远不知道是哪个起了作用。这是我在比赛早期浪费最多时间的地方。
4.5 提交记录表和复盘
排行榜上的分数是最诚实的反馈,但如果不做记录,你很快就会忘记"第7次提交那个0.83是怎么来的"。我用的表格很简单,但收益极大:
| 序号 | 改动内容 | 验证集分数 | 榜单分数 | 结论 |
|---|---|---|---|---|
| 1 | 基线MLP | 0.71 | 0.70 | 跑通流程 |
| 2 | 换小CNN | 0.85 | 0.84 | 结构有效 |
| 3 | 加随机裁剪 | 0.87 | 0.86 | 增强有效 |
| 4 | 学习率改1e-3 | 0.88 | 0.87 | 小幅提升 |
| 5 | 换骨干网 | 0.90 | 0.89 | 明显提升 |
| 6 | 加TTA | 0.91 | 0.91 | 提升有限但值得 |
这张表最重要的作用是防止重复劳动。人很容易在焦虑的时候反复试已经试过的东西,尤其是隔了几天之后。
5. 踩坑实录和排查速查表
5.1 损失不下降,按这个顺序查
这是我整个暑假遇到最多的问题,后来我固定了一套排查顺序,基本能在半小时内定位。
- 先看数据。把一批样本可视化出来,确认图像内容和标签是对应的。我就遇到过标签整体错位一位的情况,这个错很隐蔽,因为模型依然能训出一个看似合理的数字,只是分数上不去。
- 确认输入归一化。灰度图像除以255、彩色图像按通道做标准化。归一化没做的时候,损失经常在第一轮就变成nan。
- 把学习率降一个数量级。如果损失从稳定变成下降,说明学习率太大。
- 检查标签编码。分类任务里,类别标签必须是0到N-1的整数,损失函数要匹配。用交叉熵却传了one-hot标签,会得到一个很小但几乎不变的损失值。
- 用一个极小的子集过拟合。拿20条数据训100轮,如果模型不能在训练集上接近100%准确率,那说明代码有结构性问题,跟超参无关。这招非常好用,能在几分钟内区分"模型有问题"和"数据太难"。
- 检查是否忘了
optimizer.zero_grad()。忘了清零梯度会导致梯度累加,训练直接崩掉。 - 检查验证逻辑和训练逻辑是否一致。忘记
model.eval()会让批归一化和dropout在验证时行为错误,验证分数会莫名其妙地低。
5.2 过拟合和欠拟合,看两条曲线就够了
这个问题我用一张对比讲清楚。训练集和验证集两条损失曲线,是诊断模型状态最直接的工具。
训练损失下降、验证损失也下降,且两者差距小——健康状态,继续训。 训练损失一直降、验证损失先降后升——典型过拟合。处理方式:加数据增强、加权重衰减、加dropout、减小模型、加早停。 训练损失和验证损失都很高、都降得很慢——欠拟合。处理方式:加大模型容量、提高学习率、延长训练、减少正则强度。
我这次比赛中期就卡在第二种状态上。验证集分数到了一个点就不动了,训练集却还在往上走。当时的应对是加了两项增强:随机旋转和随机擦除,验证集分数又上去了一点。这个经验说明,过拟合不是模型的问题,是数据多样性的问题,从数据侧解决往往比从模型侧解决更有效。
5.3 环境和显存上的坑,都是可以提前规避的
**显存不足(OOM)**是高频问题。我的排查顺序是:先看是不是batch size太大,减半试试;再看是不是验证阶段忘了用torch.no_grad(),验证时也在建计算图,显存会涨得很快;最后看是不是数据加载器积压,把num_workers从0开始试。
训练速度慢有时候不是显卡的问题,而是数据加载成了瓶颈。判断方法很简单:看GPU利用率,如果长期低于50%,瓶颈大概率在数据侧。解决办法是把数据预处理提前做一次,存成numpy数组或者缓存文件,训练时直接读。
随机种子不固定导致结果不可复现,这个坑的代价是巨大的。我现在的做法是在脚本入口处就把torch、numpy、random三个种子全设一遍,并且在训练日志里把种子值打出来。
版本不匹配造成的报错往往很迷惑。我遇到过某个依赖版本升级之后,加载模型权重报键名不匹配的情况。现在我的习惯是:环境跑通之后马上导出依赖清单,后面所有实验都在同一个环境里跑。
pip freeze > requirements_lock.txt这份锁定的清单,在换机器或者重装环境的时候能省下大量时间。
5.4 报错速查表
| 现象 | 大概率原因 | 处理方式 |
|---|---|---|
| 损失变成nan | 学习率过大 / 未归一化 / 除零 | 降学习率,检查数据范围 |
| 维度不匹配报错 | 卷积后展平维度算错 | 展平前打印形状 |
| 验证分数异常低 | 未调用eval模式 | 验证前加model.eval() |
| GPU用不上 | 驱动或框架版本不匹配 | 重建环境,跑三行验证代码 |
| 训练速度波动大 | 数据加载慢 | 预处理缓存,调整num_workers |
| 多次运行结果不同 | 随机种子未固定 | 统一设置torch/numpy/random种子 |
| 提交格式报错 | 列名或行数不符 | 对照样例文件逐列核对 |
这张表里的每一条我都至少踩过一次。尤其是最后一条,提交格式错误在比赛最后阶段是致命的,因为提交次数往往有限。我的做法是:第一次提交前,先写一个校验脚本,检查行数、列名、取值范围是否与样例一致。这个脚本写一次,后面每次提交前跑一遍,能避免绝大部分低级失误。
5.5 比赛节奏和心态上的两个坑
第一个坑是过早追求高分。开赛第一周就想冲榜,结果基础没打牢,后面反复返工。我现在更认同的顺序是:流程正确优先于分数,分数稳定优先于分数最高。
第二个坑是最后阶段大改方案。我在比赛倒数第二天动过一次模型结构,结果验证集分数上去了,提交之后榜单分数反而掉了。原因很可能是验证集和测试集分布有差异,改动引入了对验证集的过拟合。这个教训让我定了一条规矩:最后三天只做能大幅提升稳定性的改动,比如固定种子多次运行取平均,不做结构性的探索。
如果提交次数有限,我的分配策略是:前期用掉三分之一做探索,中期用掉三分之一做验证,最后留三分之一做保守提交。很多队伍最后发现没有提交次数了,只能眼睁睁看着排名掉下去。
最后再分享一个小技巧。整个比赛期间,我把所有实验目录都按"日期+改动+分数"命名,比如0820_cnn_crop_0887。听起来是很土的习惯,但当你需要对十几个实验做取舍、或者想回头复现某个两周前的实验时,这套命名法就是救命的。
至于这个暑假真正学到了什么,我的答案不是某一项技术,而是一套把想法快速变成可验证实验的工作流:先固定变量,一次只改一个,记录结果,定期复盘。这套流程放在这次比赛里能提分,放在任何需要试错的事情上也一样管用。顺便提一句,这次比赛带出来的代码模板我做了整理,打算拿它去跑几个自己感兴趣的小项目——复现一个数字识别的demo,试试把序列模型迁移到传感器数据上,看看换个数据集之后哪些超参需要重调。这些在比赛期间没时间做的事,正好留到开学后慢慢折腾。