
简介一份面向2024年秋季机器学习竞赛的实用指导文档适合具备一定机器学习或计算机视觉基础、希望参与图像分类与历史文物鉴定的学生和研究者。资源围绕“中国古代艺术品朝代预测”任务展开基于3600张书法绘画扫描图像其中900张为AI生成训练集标签含噪声因此专门强调了数据清洗、不均衡样本与长尾问题的处理思路。文档共1个docx文件压缩包大小约554KB内容精炼便于快速查阅。目前已有67人关注学习。资料详细说明了竞赛目标、类别定义、评估指标总体准确率、非AI类F1、AI类F1及权益共享得分以及每日提交限制、截止节点和排行榜规则有助于读者快速掌握赛制、制定建模策略并针对性地优化模型鲁棒性同时建议为数据清洗训练专用模型以进一步处理噪声标签与AI图像干扰。1. 2024秋季机器学习比赛指导中国古代艺术品朝代预测到底在考什么如果你报名了这个方向的比赛先别急着找模型你得先意识到这不是一个普通的图像分类题它把计算机视觉里最折磨人的几个问题全凑齐了。基于图像识别的中国古代艺术品朝代预测本质上是一个「细粒度图像分类 小样本学习 类别极度不平衡」的三重难题。你以为自己在做分类其实是在跟数据较劲。我见过太多参赛者上来就微调 ResNet跑完第一个 epoch 就懵了验证集准确率死活不超过 30%。原因很简单陶瓷、书画、青铜器这些艺术品不同朝代之间的差异有时候比同一朝代不同窑口的差异还小而且存世数量差异悬殊——唐代的器物和清代的器物数量根本不在一个量级。这场比赛真正想考验的不是你调参的手速而是你处理真实脏数据、制定合理训练策略的能力。这篇文章就围绕这个比赛把数据准备、模型选型、训练配置、踩坑记录和提分技巧一条线讲透。适合正在打这场比赛、或者在做类似小样本图像分类项目的人照着复现能少走两周弯路。2. 数据清洗与标签体系构建决定你是 70 分还是 30 分的分水岭2.1 先搞清楚你要预测什么朝代标签的三种粒度比赛给的训练数据通常是「一张图 一个朝代标签」的形式。但这里有一个关键问题朝代标签的粒度到底是什么是「唐、宋、元、明、清」这种大朝代还是「北宋、南宋、明早期、明中期」这种细分阶段这直接决定了你的模型输出层神经元数量也决定了任务难度。我一般会先用脚本统计标签分布看每个类别的样本量。这一步不是走流程而是要回答两个问题类别数有多少最少的类有多少张图如果最少的类只有几十张而最多的有几千张那这就是一个典型的长尾分布后续所有的策略都要围绕这个来设计。import pandas as pd from collections import Counter # 假设训练集标注是 CSV 格式image_name, label df pd.read_csv(train_labels.csv) label_counts Counter(df[label]) print(f总类别数: {len(label_counts)}) print(f样本总量: {sum(label_counts.values())}) print(各类别样本数:) for label, count in label_counts.most_common(): print(f {label}: {count}) # 计算不平衡度 max_count max(label_counts.values()) min_count min(label_counts.values()) print(f\n最大类/最小类比例: {max_count / min_count:.1f} 倍)这段代码的价值在于让你在动手训练之前就对数据状况有数。如果最大类和最小类的比例超过 10 倍就需要走类别重加权或者采样策略。如果最小类别只有几十张图那就要考虑这个类在训练集上能不能学到有效特征——很多时候你会发现表现最差的那一类恰恰就是样本最少的那一类。2.2 图像去重与坏图过滤别让模型在垃圾上学习艺术品数据集里最常见的坑是重复图和近似图。同一件器物可能被多个来源拍了不同角度的照片或者同一个朝代的不同器物风格高度相似。如果不去重模型会疯狂过拟合那些「高频出现的特定图像」而不是学到真正的朝代特征。去重有两个层次精确去重用文件 MD5近似去重用感知哈希。MD5 只能去完全相同的文件但艺术品数据集里更多的是「同一件东西的不同照片」这时候感知哈希更实用。import hashlib import os from PIL import Image import imagehash def compute_md5(file_path): h hashlib.md5() with open(file_path, rb) as f: for chunk in iter(lambda: f.read(4096), b): h.update(chunk) return h.hexdigest() def compute_phash(file_path): img Image.open(file_path).convert(RGB) return imagehash.phash(img, hash_size16) # 示例对一批图像计算 phash image_paths [os.path.join(train_images, f) for f in os.listdir(train_images)] hash_dict {} duplicates [] for path in image_paths: phash compute_phash(path) for existing_path, existing_hash in hash_dict.items(): # 汉明距离越小越相似通常认为小于等于 5 是近似重复 if phash - existing_hash 5: duplicates.append((path, existing_path, phash - existing_hash)) break else: hash_dict[path] phash print(f检测到疑似近似重复图像对: {len(duplicates)} 组) for triplet in duplicates[:10]: print(f {triplet})这里有一个参数值得展开phash - existing_hash计算的是汉明距离阈值取 5 是经验值。对于艺术品这种背景复杂、拍摄角度多变的图阈值太紧会漏掉真重复太松会误杀有细微差别的有效样本。我一般会先跑一遍看输出根据实际情况微调这个值。除此之外坏图也必须处理。有些图像文件本身是损坏的PIL 打开会直接报错零字节文件、截断的 JPEG、CMYK 色彩空间的文件。这些图如果不清理训练到一半就会炸或者在预处理阶段拖慢速度。我的习惯是写一个全量检查脚本把所有打不开的、尺寸异常的图列出来该删的删该重新下载的重新下载。import os from PIL import Image bad_images [] for root, dirs, files in os.walk(train_images): for f in files: path os.path.join(root, f) try: img Image.open(path) img.load() if img.size[0] 64 or img.size[1] 64: bad_images.append((path, size_too_small)) except Exception as e: bad_images.append((path, ferror: {e})) print(f发现坏图 {len(bad_images)} 张) with open(bad_images.txt, w) as f: for path, reason in bad_images: f.write(f{path}\t{reason}\n)尺寸阈值 64 也有讲究。如果输入图本身比 64x64 还小那放大到模型的输入尺寸通常是 224x224 或 384x384之后全是马赛克这种图对朝代预测没有任何正面价值留着只会制造噪声。2.3 类别划分的艺术合并和剔除的边界拿捏真实比赛数据里有些标注是有问题的。比如一个唐代的碗标签写成了「唐代」但它是唐三彩和其他唐代青瓷差距很大。这时候你别无脑直接删先看看是「类内差异大」还是「标注错误」。如果同类内差异大到模型学不出共同特征有几个处理方案一是把差异大的子风格拆成独立类别但这改变了比赛的评价逻辑要谨慎二是对这类用更强的数据增强逼模型提取共性三是统计这个类在验证集上的表现如果准确率一直上不去大概率是这个类自身的问题而不是模型的问题。还有一类是「无意义类别」。比如有些数据集里会有「其他」「未知」这样的兜底标签这种类别的图像内容五花八门什么都有模型永远学不出来。如果比赛允许处理我的做法是先训练一版把所有类都带上看混淆矩阵如果「其他」这个类大量被分到别的类而且其他类也会被误分到「其他」那说明这个类没有区分度剔除掉、在推理阶段也禁止模型预测这个类效果反而更好。3. 预训练模型选型与迁移学习别追新追稳定性3.1 为什么是迁移学习而不是从头训练你对着一万张图从头训练一个 ResNet50效果大概率不如用一个在 ImageNet 上预训练好的模型做微调。原因很简单朝代之间的差异规律是「风格特征」不是「像素特征」。预训练模型已经在海量自然图像上学到了边缘、纹理、形状这些底层表征你需要的只是把高层特征从「识别猫狗」迁移到「识别釉色和纹饰」。而在具体选型上我一般不考虑最新的 ViT 或者大型多模态模型。比赛跑分要的是稳定和可控ViT 在小数据集上容易欠拟合调参空间大、翻车概率高。20 万张以下的数据量ResNet50 或者 EfficientNet-B4 是性价比之王。这两个模型的共同特点是训练技巧成熟、batchnorm 在迁移学习时表现稳定、有一大堆现成的复现经验可以参考。3.2 用两个阶段缓解类别不平衡先从大类别学起类别不平衡在这个比赛里不是一个小问题而是一个常态。面对这种问题业界有一个常见做法叫「两阶段训练」第一阶段只用样本量超过某个阈值的类别训练得到一个「大类别分类器」第二阶段把所有类别都加回来但加载第一阶段的模型权重做初始化。这样做的好处是模型先学会了「朝代之间的通用区别」然后才去学那些小类别的特殊细节不会因为小类别样本太少一开始就被大类别带偏。import torch import torch.nn as nn import torchvision.models as models ckpt torch.load(stage1_resnet50.pth, map_locationcpu) # 第一阶段模型假设是 5 个大类 stage1_num_classes 5 # 第二阶段模型假设全部类别是 12 个 stage2_num_classes 12 model models.resnet50(weightsNone) model.fc nn.Linear(2048, stage1_num_classes) model.load_state_dict(ckpt[model_state_dict]) # 把分类头换掉保留特征提取器权重 old_fc_weight model.fc.weight.data.clone() model.fc nn.Linear(2048, stage2_num_classes) with torch.no_grad(): # 新分类头的初始化用旧分类头的均值向量补全 # 这样比随机初始化收敛快得多 for i in range(stage2_num_classes): if i stage1_num_classes: model.fc.weight.data[i] old_fc_weight[i] else: model.fc.weight.data[i] old_fc_weight.mean(dim0) print(f从阶段1加载完成分类头已扩展为 {stage2_num_classes} 类)这个初始化方式值得细说。直接把新分类头那几行随机初始化会打乱整个模型的初始状态吗不会因为分类头是最后一层但如果不做任何处理新类别的输出值会一开始就偏大或偏小导致 loss 剧烈波动。用旧权重的均值向量来初始化新类别相当于给新类别一个「中等偏保守」的起点训练会更稳。3.3 类别重加权损失函数让模型多看两眼小样本两阶段训练是从数据流角度做文章损失函数层面还可以再叠一层。最常见的做法是在 CrossEntropyLoss 的权重参数里做手脚权重按类别样本量的倒数来设但不要直接用原始倒数——那会让样本量极小的类别权重爆表导致模型疯狂过拟合那几个样本。我常用的平滑方式是weight (1 / sqrt(count))或者weight (max_count / count) ** 0.5这个 0.5 次方是一个比较中庸的选择。你可以在验证集上实际搜索这个幂次0.3 到 0.8 之间通常能找到更好的值。import numpy as np import torch.nn as nn def make_class_weight(label_counts, power0.5): labels sorted(label_counts.keys()) counts np.array([label_counts[l] for l in labels], dtypenp.float32) max_count counts.max() # 用 max_count 做归一化权重范围被压在 [1, (max/min)^0.5] weight (max_count / counts) ** power # 再做一次均值归一化让有效学习率不至于整体漂移 weight weight / weight.mean() return torch.from_numpy(weight).float() class_counts {唐: 1200, 宋: 800, 元: 300, 明: 1500, 清: 2000} class_weight make_class_weight(class_counts, power0.5) print(f类别权重: {class_weight}) criterion nn.CrossEntropyLoss(weightclass_weight, label_smoothing0.1)注意我把label_smoothing0.1也加上了。这个参数容易被忽略但在小数据集上非常管用它不让模型对训练标签过于自信相当于一种正则化。尤其是当某个类的训练样本特别少时没有 label smoothing 的模型会轻易把那个类的 logit 推到非常大导致验证集上稍微有一点噪声就预测错。4. 训练配置与验证策略把每张图的用处都榨干4.1 数据增强的剂量控制艺术品不是普通照片艺术品图像有一个特点主体通常在画面中央背景相对干净但角度、光线、裁剪比例差异很大。这决定了数据增强的写法跟自然图像分类不一样。你不能用大尺度随机裁剪因为有可能把器物的关键纹饰裁掉也不能用力旋转因为器物方向在图片里是有意义的比如口沿纹饰的方向。我常用的增强组合是Resize 到短边 256中心或随机裁剪 224水平随机翻转但有条件见下文轻度色彩抖动随机旋转只给 ±5 度。from torchvision import transforms train_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomCrop(224), transforms.RandomHorizontalFlip(p0.3), transforms.RandomRotation(degrees5), transforms.ColorJitter(brightness0.2, contrast0.15, saturation0.1, hue0.05), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) valid_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])这里有一个「水平翻转概率设为 0.3 而不是 0.5」的细节。为什么因为中国特色艺术品中有大量左右不对称的器物比如带把手的壶、带流的长流瓶如果水平翻转概率太高模型会学到「左右无所谓」这个错觉。但这些器物在真实图像里是有固定的左和右的过度的水平翻转反而制造错误样本。你可能会想验证集能不能也做增强我建议别做验证集用固定的中心裁剪就好。因为增强的随机性会掩盖模型真实水平。如果开了 TTA测试时增强那是另一个话题后面进阶章节会讲。4.2 训练超参数的保守起点从稳定到极致比赛训练常见的一个失误是学习率开得太大。迁移学习里特征提取器已经有预训练权重了它不需要那么大动静你真正需要更新的是分类头和新学习的高层语义特征。所以我的起点是骨干网络学习率 1e-4分类头学习率 1e-3用 AdamW 优化器。如果模型在验证集上 10 个 epoch 没动静再把学习率往上微调如果 loss 开始震荡立即减半。还有一个新手几乎必踩的坑对骨干网络启动 grad cam 之类的梯度裁剪幅度没把握好或者忘了给不同层设置不同的学习率。以下是一个简单的分组学习率实现def get_param_groups(model, lr_backbone1e-4, lr_head1e-3): backbone_params [] head_params [] for name, param in model.named_parameters(): if fc in name or classifier in name: head_params.append(param) else: backbone_params.append(param) return [ {params: backbone_params, lr: lr_backbone}, {params: head_params, lr: lr_head}, ] # 使用示例 model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V1) model.fc nn.Linear(2048, 12) param_groups get_param_groups(model, lr_backbone1e-4, lr_head1e-3) optimizer torch.optim.AdamW(param_groups, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR( optimizer, T_max30, eta_min1e-6 )细心的读者会注意到weight_decay1e-4。这个权重衰减是给整组参数用的但这里有一个被很多实现忽略的问题batchnorm 层的 gamma 和 beta 不应该加权重衰减加了会干扰 BN 的归一化尺度。解决方法是额外做一个分组过滤但为了代码简洁许多比赛方案会直接用一个折中的权重衰减值。如果你发现训练不稳优先检查 BN 层的梯度是否异常。4.3 验证集划分警惕「同源数据泄漏」艺术品图像数据有一个很隐蔽的数据泄漏来源同一件器物、同一个博物馆展柜、同一组公开图集可能出现在训练集和验证集里。如果不去重就划分验证集准确率会虚高 5% 到 15%但这部分分数在测试集上根本拿不到。我的做法是先用感知哈希做一层近似去重把相似度高的图像归到同一组然后按「组」为单位划分训练集和验证集。这样至少能保证验证集里没有训练集的近似复制品。import random from collections import defaultdict # 假设已经用 phash 算好了 group_id存成 image_to_group.json import json with open(image_to_group.json, r) as f: image_to_group json.load(f) group_to_images defaultdict(list) for img, grp in image_to_group.items(): group_to_images[grp].append(img) all_groups list(group_to_images.keys()) random.seed(42) random.shuffle(all_groups) valid_ratio 0.15 valid_group set(all_groups[:int(len(all_groups) * valid_ratio)]) valid_images [] train_images [] for img, grp in image_to_group.items(): if grp in valid_group: valid_images.append(img) else: train_images.append(img) print(f训练集组数: {len(all_groups) - len(valid_group)}, 图像数: {len(train_images)}) print(f验证集组数: {len(valid_group)}, 图像数: {len(valid_images)})这里划验证集的本质不是「随机挑 15% 图像」而是「随机挑 15% 的相似组」。如果你发现验证集和训练集之间存在大量近似重复那你的验证策略就是自欺欺人。后续调参时你会被一个虚高的分数误导反复调出一个在公开测试集上完全失灵的模型。5. 四线程训练中的避坑记录从数据检查到玄学问题5.1 数据加载线程炸了不是你的 bug是内存不够现象训练跑到第 2 个 epochDataLoader 报RuntimeError: DataLoader worker (pid xxx) is killed有时候直接整个进程退出。原因Dataloader 的num_workers开太多或者每张图的预处理太重比如在 transform 里做大量 PIL 操作瞬间内存/显存溢出。解决先把num_workers降到 2 看是否稳定再逐步上调到 CPU 核数的一半。图像解码统一走Image.open() - RGB - resize最短路径不要在 transform 里做numpy - PIL - numpy的反复转换。我见过最翻车的案例是有人把num_workers16怼在 4 核的笔记本上结果数据加载比模型训练还慢而且内存直接爆了。这个参数不是越大越好它受限于你的 CPU 内存带宽和图片解码速度。5.2 验证集精度高、测试集却拉胯典型的域偏移现象本地验证集准确率 92%提交到比赛平台公开测试集只有 75%。原因验证集和测试集的图片来源不同。比赛方在构建测试集时通常会用跟训练集完全不同来源的公开图库这些图在光照、背景、清晰度上分布不一样。你的模型如果过拟合了训练集的「背景和色调」——比如训练集很多图是博物馆官网的白底图测试集却是实拍展柜照片——那验证集分数就解释不了测试集分数。解决这正说明了先做同源去重的必要性。另外对训练图做「风格随机化」增强比如随机调整色温、加模拟玻璃反光、随机灰度化能提高对域偏移的抵抗能力。如果比赛允许外部数据还可以用少量测试集分布相近的图做辅助训练。这种「验证集分数虚高」是比赛里最坑的你调参调的再准方向选错了都会打到棉花上。所以要在验证策略上下狠心宁可验证集吃紧一些也不能让它虚胖。5.3 精度卡在某个值不动loss 在降但指标不涨现象训练 loss 稳步下降但验证集准确率在 80% 附近卡了 10 个 epoch怎么调学习率都没反应。原因数据增强强度不够或者类别数太多、类间距太小。模型已经记住了训练集的「最优解」但在验证集上还差一口气。解决先检查是不是数据增强太轻——把 RandomCrop 改成 ScaleJitter或者加 RandomErasing让模型看到更多「残缺」的样本。第二个方法是把模型输入尺寸从 224 提到 320 或 384这个操作在小数据集上往往能带来 2% 到 5% 的提升代价是显存占用翻倍。# 加大输入尺寸 加随机擦除 train_transform_v2 transforms.Compose([ transforms.Resize((384, 384)), transforms.RandomCrop(320), transforms.RandomHorizontalFlip(p0.3), transforms.RandomRotation(degrees5), transforms.ColorJitter(brightness0.2, contrast0.15, saturation0.1, hue0.05), transforms.RandomErasing(p0.25, scale(0.02, 0.15), ratio(0.3, 3.3)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])随机擦除RandomErasing的scale(0.02, 0.15)意思是擦除区域占图像面积为 2% 到 15%这个比例对艺术品细节非常重要。如果擦的太大模型可能会把整个器物主体擦掉等于制造了一个无法判别的样本。5.4 朝代的严谨性要求被忽略这是一个现实的问题现象模型把「明早期」的器物预测成了「明」或「元」尽管单个 epoch 的准确率还行但看混淆矩阵发现相邻朝代互相串。原因朝代之间的边界本来就不是一个像素级的清晰分界线。很多器物的朝代属性是考古学家的综合判断结果图片本身的信息可能不足以支撑精确的细粒度分类。解决如果比赛允许可以用「多标签软化」方案——不是硬生生的 12 类互斥分类而是把标注转成一个朝代序列的概率分布比如某器物 80% 属于明代、20% 属于元代。这样模型学到的不是「非此即彼」而是「朝代渐变」。这需要在预处理时把 label 编成分布向量而不是一个整数索引。通常的做法是先统计标签共现矩阵找出一批「在样本特征上容易混淆」的朝代对然后手动训练一个次级分类器来区分这两个朝代。这个思路跟分层分类器很像第一层判断大类比如明清 vs 唐宋 vs 高古第二层再细分具体朝代。6. 测试时增强和模型融合最后一公里的提分技巧6.1 TTA把一次预测变成 6 次投票到了提交阶段再好的模型也想在推理时再多榨一点提升。测试时增强Test-Time Augmentation的原理很简单训练时你怎么增强测试时也做几组差不多的扰动——不同的裁剪位置、翻转、小旋转——然后把多次预测取平均。对艺术品这样对细节敏感的任务TTA 的常见配方是中心裁剪、左上裁剪、右下裁剪 水平翻转一共 6 个变体。如果你的模型输入是 320推理时先把图 Resize 到 360再随机裁剪到 320得到多视角预测。import torch import torch.nn.functional as F from torchvision import transforms def predict_with_tta(model, img_tensor, device): img_tensor: 已经 normalize 后的 tensor, shape[C, H, W] model.eval() tta_transforms [ transforms.Compose([transforms.CenterCrop(224)]), transforms.Compose([transforms.CenterCrop(224), transforms.RandomHorizontalFlip(p1.0)]), transforms.Compose([transforms.Resize(256), transforms.FiveCrop(224)]), ] all_preds [] x img_tensor.unsqueeze(0).to(device) with torch.no_grad(): # 方案1: 中心裁剪 pred model(x) all_preds.append(F.softmax(pred, dim1)) # 方案2: 水平翻转不变物体方向的前提 x_flip torch.flip(x, dims[3]) pred model(x_flip) all_preds.append(F.softmax(pred, dim1)) # 方案3: 多尺度——resize成不同大小再center crop for scale in [0.8, 0.9, 1.1, 1.2]: scaled F.interpolate( x, scale_factorscale, modebilinear, align_cornersFalse ) # crop回原尺寸 scaled_final F.interpolate( scaled, size(224, 224), modebilinear, align_cornersFalse ) pred model(scaled_final) all_preds.append(F.softmax(pred, dim1)) avg_pred torch.mean(torch.cat(all_preds, dim0), dim0, keepdimTrue) return avg_pred请务必注意翻转的适用条件如果模型在训练时用了低概率的水平翻转那测试时也要用同样的概率约束不能无脑上翻转。否则前面说过的「带把手的壶」这类不对称器物会被翻转操作坑掉。6.2 多模型融合的稳妥姿势按分数加权而不是平均如果你有多个候选模型——比如一个 ResNet50、一个 EfficientNet-B4、一个 ResNeXt——怎么融合是有讲究的。常见做法是直接把预测概率取平均但这默认了每个模型实力相当。我更建议用「以验证集最优 epoch 为准」的分数加权。import numpy as np # models_predictions: list of 概率矩阵, shape[N_samples, num_classes] # valid_scores: 每个模型在验证集上的准确率 model_preds np.array([pred1, pred2, pred3]) # (3, N, C) valid_scores np.array([0.91, 0.88, 0.90]) # 归一化权重 weights valid_scores / valid_scores.sum() # 加权融合 final_pred np.tensordot(weights, model_preds, axes(0, 0)) final_labels np.argmax(final_pred, axis1)这个融合方法是「验证分数驱动」的但它有一个潜在问题验证分数高的模型未必与测试分布对齐。如果你发现融合后的提交分数反而低于单模型最高分那就不要融合。比赛不是做数学题一切以实测为准。6.3 提交格式检测和训练痕迹清理比赛的最后一步我见到有的参赛者模型分数不错却因为提交文件里标签顺序错了、或者类别名大小写不一致而被判零分。这很可惜。后来我养成了一个习惯写一个脚本把提交文件和后端要求的 CSV 格式严格对齐类别名做一遍精确字符串匹配甚至会把valid_submission.csv的第一行打印出来人工核对。import pandas as pd # 假设模型输出 image_name - predicted_label 映射 submission pd.DataFrame({ image_name: test_images, label: predicted_labels }) # 做一次基础校验 allowed_labels [唐, 宋, 元, 明, 清] # 以比赛给定标签集合为准 assert set(submission[label]).issubset(allowed_labels), 存在非法的标签 # 检查是否有空值 assert submission[label].notna().all(), 存在空标签 submission.to_csv(final_submission.csv, indexFalse) print(submission.head())说到训练痕迹我在打比赛时总喜欢保留两份权重一份是「验证集最优 epoch」的权重一份是「最后一个 epoch」的权重。很多初学者只留最后一个 epoch结果最后的训练过程已经过拟合了验证集分数反而往下掉。正确做法是监控每个 epoch 的验证指标保存最优的并且顺手存一份稍微提前几轮、训练更稳定的版本——这两个模型往往能融合出更好的结果。我自己的习惯是给每个实验跑一个完整的config.yaml存档把数据增强版本、学习率、epoch、批大小一并写进去。因为调过几轮之后你会发现原来某个看似不起眼的设置其实是提升分数的关键。没有配置存档你连后悔药都没得吃。希望这篇文章能帮你在跑这个比赛时少走几条弯路把精力花在真正能提分的地方。本文还有配套的精品资源点击获取