去年组队参加了一场数据挖掘挑战赛B题给的任务是做岩石样本图像分类。说实话一开始我们完全没把这个题放在心上——图像分类嘛加载一个预训练ResNet跑通baseline再调调参最坏三天也该出结果了。结果第一次提交就把我们打醒了准确率只有80.8%连预期的底线都没碰到。更难受的是我们根本说不清楚问题出在模型、数据还是评价方式上。后面两周我们把整条流程推倒重来从数据清洗、样本分组、类别平衡到骨干网络选型、微调策略、TTA和模型集成最后又把可解释性分析补齐才把成绩拉到了93.2%。这篇文章就是那次比赛的完整复盘。无论你是准备参加数据挖掘竞赛还是想把深度学习用到岩矿鉴定、地质调查这类图像识别场景这篇内容里都有可以直接抄作业的步骤和细节。1. 赛题拆解这题最大的坑不在“不会训模型”1.1 业务背景为什么岩石识别需要模型这个赛题不是凭空想出来的。在地质调查、矿产勘查、油气钻井这些场景里岩芯和岩屑的鉴定一直是工作量最大的环节之一。传统做法是专业人员用肉眼、放大镜或者偏光显微镜根据岩石的颜色、结构、构造、矿物组成给出分类结论。这个方法可靠但效率上限非常明显一个熟练人员一天能看几十到一两百块样本已经不错而且结果受人的经验和状态影响很大。近些年图像识别技术逐步被引入岩矿鉴定流程主要用来做辅助筛选——先让模型把明显能区分的样本分掉再把疑难样本交给人工复核整体效率能提升不少。比赛就是把这类实际问题抽象成了一个标准的监督图像分类任务数据是已经标注好的岩石图像目标是训练模型尽可能正确判断每一张图属于哪个岩石类别。1.2 数据分布里藏着三个典型难题训练集大概有一万两千张图像覆盖十二个岩石类别。光看数字没感觉真正开始训练之后问题很快就暴露出来了。第一是类别不均衡严重。花岗岩和玄武岩这两个类别加起来占了总量接近一半而大理岩、石英岩这类样本只有不到百分之五。如果不做任何处理模型训练出来会天然偏向大类别。第二是图像风格非常混杂。有的图是手机拍摄带比例尺的有的图是岩芯扫描仪出来的有的图带着标签纸、黑色背景还有的图在自然光下拍摄、阴影很明显。模型很容易把背景风格当成分类依据而不是岩石本身的纹理。第三是类间相似度高。玄武岩和辉绿岩、辉长岩和闪长岩、安山岩和玄武岩肉眼看上去几乎一样。岩石分类本身存在连续过渡不同岩石之间的矿物成分比例并不是跳跃变化的这给分类器带来了先天困难。也是因为这个原因后面我们始终没有只盯一个总分而是反复在看混淆矩阵。如果只关心排行榜上的单一数字很多真实问题都会被漂亮的总分掩盖掉。1.3 评价指标决定策略方向初赛排行榜用的是总体准确率但这个指标对少数类特别不友好。一个模型只要把花岗岩和玄武岩认好其他类别全部猜错准确率也能到40%以上再加上一点运气少数类偶尔猜对几个分数就更好看了。我们内部从一开始就定为宏观F1作为主指标总体准确率作为参考。原因很简单——不想交出那种“大部分类别都废掉”的模型。从最终提交的分析报告来看评委也是按分类别表现逐项看的混淆矩阵里少数类如果全军覆没整体分数再高也很难说服人。这个判断直接影响了后续所有策略在损失函数上做类别加权在推理阶段对类别先验概率做阈值调整本质上都是为了平衡这个评价目标。2. 数据工程阶段训练前我们先把一万多张图彻底清理了一遍2.1 三步清洗去重、除糊、去背景模型训练之前先把数据彻底清洗一遍这一步看似基础实际上对结果的贡献比后面任何调参都大。第一步去重。同一个岩石块往往存在多张角度非常接近的照片直接用感知哈希计算图像指纹把相似度高于95%的图去掉只保留一张。这一轮大约删掉了百分之二三的数据。不处理的话模型会对同一块岩石的训练样本过拟合验证集上看着挺好碰上没见过的岩石块就原形毕露。第二步筛模糊。用拉普拉斯算子计算图像方差高方差意味着边缘清晰低方差代表图像偏糊。我们统计了一下正常图片的响应基本在800以上低于300的就是明显模糊图这些图直接剔除。模糊图对纹理类任务伤害特别大因为岩石识别的核心信息就是纹理和颗粒结构一张糊掉的图等于把模型的眼睛蒙上了。第三步排查纯背景图。有些图像可能因为拍摄失误只有岩芯托盘、黑布背景或比例尺卡片。这个用亮度直方图来处理很直观正常岩石图的像素直方图通常分布相对均匀纯背景图会在某个亮度区间出现严重尖峰。这类图不需要人去看写个脚本就能筛掉。2.2 用baseline模型反向挖错标数据清理之后还有一类问题更隐蔽——标注可能本身是错的。我们的做法是先训练一个简单的baseline模型然后让它对整份训练集做交叉预测把所有“模型置信度很高但预测类别和标注不一致”的样本抽出来人工逐张复核。为什么专门看高置信度错分样本因为普通错分有可能是模型能力不足但高置信度错分大概率是数据标注和图像内容真的对不上模型本身并没有错。最终我们抽出来两百多张样本人工复核发现了几十张明显标错的图大多发生在相近岩石类别之间比如辉绿岩标成玄武岩、黑云母片岩标成角闪岩。把这些标注修正之后验证集准确率直接提升了接近1%。这个操作比花时间试任何新模型结构都划算也是我强烈建议所有做分类题的人优先做的事情。2.3 数据增强不是开得越猛越好数据增强策略我们迭代了两版。第一版比较激进旋转、翻转、裁剪、颜色抖动、随机擦除、CutMix全部打开结果验证集反而下降了。原因现在想得很清楚岩石图像的核心辨识信息在纹理和结构而不是颜色本身。过强的光照扰动会让模型去学伪影过强的CutMix会把不同类别的岩石纹理生硬拼接产生大量不自然的中间状态模型反而学不到干净的特征边界。第二版我们收得比较克制最终保留这些增强水平翻转和90°旋转必开岩石没有方向性旋转不会破坏语义随机裁剪范围限制在0.85到1.0之间避免裁掉关键构造颜色抖动只做轻微幅度不让色调和饱和度跑太远片麻岩、大理岩这类纹理条带明显、颗粒感强的类别不做大尺度缩放防止层理或条纹被压缩变形。增强的最终原则只有一句让模型看到更多拍摄角度和光照差异而不是看更多扭曲的岩石纹理。2.4 分组划分随机划分会高估模型实力这是整个项目里最容易忽略、也最影响判断的一步。数据里同一个岩石块会产生多张图像如果直接按图像随机划分训练集和验证集同一个“石块”的照片可能同时出现在两边。模型相当于提前见过验证集同类纹理验证分数自然会虚高。第一版随机划分时验证集跑到89%改成按岩石块分组划分之后直接掉到85%以下。数字不好看但我们知道后面那个才是真实水平。真实使用场景里模型要面对的是完全没见过的岩石块而不是同一块岩石的不同角度。所以划分时坚持按样本编号前缀把同属一个岩石块的图像归到同一组整组划分。后面所有模型对比、实验决策都基于这个更严格的分割方式避免了大量虚假的“提升”。3. 模型选型与训练策略为什么最后是ResNet34加EfficientNet-B33.1 预训练到底该不该用这个题目里岩石图像和自然图像差异不小我们一开始也纠结要不要从零训练怕ImageNet预训练权重反而变成干扰。专门做了对比实验从零训练ResNet34验证集准确率大概在76%而且需要更多轮次才能稳定加载ImageNet预训练权重后做常规微调同样轮次下能到88%以上。差距非常明显。原因也好解释岩石图像虽然和自然图像不完全一样但底层特征仍然是边缘、纹理、颜色梯度这些都是预训练模型已经学得很扎实的部分。迁移学习的本质就是把这些通用浅层特征直接带过来我们只需要让深层去适应岩石领域特有的结构组合。所以结论干脆利落这个场景下预训练一定要用但真正拉开差距的是微调策略。3.2 骨干网络对比测试在固定增强策略和分组划分条件下我们跑了几个常见backbone做对比模型参数量验证集准确率训练时长单卡约备注ResNet1811M86.1%2小时收敛快后期略微欠拟合ResNet3421M88.9%3小时综合表现最好ResNet5025M87.6%4小时相比34提升有限显存占用大EfficientNet-B312M88.2%5小时吞吐低一些但上限更高ViT-Tiny5M79.4%4小时数据量不够明显吃亏结论很清晰在1.2万张图像这个数据规模下ResNet34性价比最高。EfficientNet-B3在叠加更长训练和TTA之后能再追一点但单卡时间成本也更高。ViT这类强依赖大数据量的结构小数据集上表现一般不建议硬上。最终提交用的方案是ResNet34和EfficientNet-B3双模型集成兼顾性能和稳定性。3.3 核心训练参数与选择理由最终训练配置大致如下损失函数交叉熵开启标签平滑smoothing设为0.07。标签平滑能防止模型对训练集过于自信针对类间模糊的任务效果明显。优化器AdamW骨干网络学习率2e-4分类头学习率6e-4weight decay 0.05。学习率调度前5个epoch线性warmup之后cosine退火总训练轮次60到70轮。数据归一化使用ImageNet的均值和标准差因为骨干加载的是ImageNet预训练权重输入分布保持一致很重要。混合精度训练开启AMP显存占用下降约四分之一训练速度提升约三成。这些参数都不是凭感觉定的。骨干参数已有预训练基础分类头是随机初始化的两者必须用不同学习率。warmup是为了让随机初始化的分类头在初期稳定下来cosine退火则在后期慢慢收敛避免大学习率在小数据集上反复震荡。3.4 冻结-解冻两阶段微调微调路径我们分两步走偏保守但很稳。第一步冻结整个骨干网络只训练分类头训练15到20轮。这个阶段的作用是让随机初始化的分类头先适应已有特征相当于给分类器一个热身期。第二步解冻所有层做全模型微调。解冻时学习率整体降为原来的一半优先维持已经稳定的底层特征再用较慢的速度调整深层参数。这个策略在小数据集上几乎不会翻车因为如果从一开始就全模型微调预训练特征很容易在前期被随机分类头的剧烈梯度带偏。经过这两步模型在按组划分的验证集上能到90%以上。对比第一版“加载预训练直接全量微调”的84%提升很明显。4. 从82%到93%三个被反复验证过的精度拉升操作4.1 错标修正数据层面的隐形提分错标挖掘做了两轮。第一轮用的是单模型交叉预测第二轮换了更稳的方法把ResNet34和EfficientNet-B3两个模型在训练集上的预测结果取交集只找两个模型都高置信度判错、而且错成同一类别的样本。这种“双模型一致错分”策略大大减少了人工复核的噪音。第二轮回又修正了十几张标注数量不多但每一张都实实在在反映在混淆矩阵上。总的体会是每一次数据清洗带来的提升都比同等的模型调参更稳定。模型结构改动可能带来一两个点的波动而且方向不确定数据修正则是实打实往正确方向走几乎不存在过拟合风险。4.2 类别加权与推理阈值调整类别不均衡的处理方法分两处。训练时给每个类别的损失单独乘一个权重系数权重与类别样本数呈反比关系但做截断处理。比如某个类别只占总量的3%原始反比权重算下来会是30倍左右必然导致过拟合我们把它限制在最高3倍以内。推理阶段还做了一次阈值调整按训练集里各类别的先验占比对预测概率做小幅度的平移。通俗解释就是训练数据里天然就少的类别模型输出概率也会偏低推理时给一点补偿让少数类的召回率回升。代价是多数类的准确率略微下降但整体算账是划算的因为目标是宏观F1均衡而不是一个好看但偏科的总准确率。4.3 TTA与双模型集成后期提升最明显的一步是多尺度TTA。训练时图像resize到288×288推理时同时用256×256、288×288、320×320三个尺度外加水平翻转对五次预测结果取平均。TTA带来的提升大约在1.2个百分点。原理不复杂不同尺度下模型能看到不同范围的纹理细节平均之后预测更稳健。另一个提升点来自模型集成。把ResNet34和EfficientNet-B3的logits直接做平均而不是先过softmax再平均因为logits平均保留了更多概率信息。这两个模型结构差异大错误模式不完全相关集成后准确率又涨了约1%。最终提交用的就是双模型TTA融合的版本。5. 可解释性工作告诉评委“模型不是瞎猜”5.1 Grad-CAM可视化把注意力画出来图像分类最容易收到的质疑是“模型是不是按背景、比例尺甚至像素噪声在分类”。为了自证我们用Grad-CAM做可视化。核心思路是通过目标类别的梯度找出模型在最后一层卷积特征图上最看重的区域再叠加回原图。核心逻辑示意如下def grad_cam(model, tensor, target_idx): model.eval() features [] def hook_fn(module, input, output): features.append(output) handle model.backbone.register_forward_hook(hook_fn) output model(tensor) handle.remove() model.zero_grad() score output[0][target_idx] score.backward() # 实际实现中取的是最后一层卷积特征图对应的梯度 grads get_last_conv_grad(model) weights grads.mean(dim(2, 3), keepdimTrue) cam (weights * features[0].squeeze(0)).sum(dim0).relu() return cam上面是示意代码实际实现时要根据模型结构取到正确的特征图位置。可视化做了全类别抽样发现模型确实是在关注岩石本身的特征。比如玄武岩样本的热力集中在气孔和斑晶周围角闪岩样本的热力集中在柱状解理纹理附近大理岩则集中在条带和晶粒边界。这些结果直接放进了答辩PPT说服力比任何指标都强。5.2 混淆矩阵里的矿物学规律怎么讲最终模型的混淆矩阵里仍然存在一些顽固错误主要集中在玄武岩、辉绿岩、安山岩之间。我们的处理方式不是回避而是从地质学角度正面解释。玄武岩和辉绿岩都是基性火山岩矿物组成和肉眼结构非常接近人工在野外鉴定时也经常需要借助薄片观察才能区分。模型把它们混淆本质上是复现了领域内的客观难度而不是模型学坏了。这个说法评委接受度很高因为说明我们对分类任务的业务逻辑有理解而不只是会跑模型。另一个例子是角闪岩和黑云母片岩两类都以暗色矿物为主结构相似确实容易误判。我们把这些难分样本统一归到“共性错误”板块没有试图掩盖。坦白讲把解释性工作做在前面比评委现场质疑时再临时想理由要踏实得多。5.3 鲁棒性实验找到部署前的短板为了验证模型在真实条件下的表现我们对测试图做了一组破坏性实验加高斯噪声、随机旋转、降低亮度分别看精度下降幅度。高斯噪声对结果影响不大说明模型对传感器噪声有较强容忍度亮度降低影响明显更大说明模型对光照条件比较敏感。这个结论直接推动了预处理改进在推理阶段增加一步亮度归一化把每张图的亮度先拉伸到相对稳定的区间再输入模型。赛后我们用真实岩芯照片测试时这个改动的价值再次体现出来。很多时候简单实验暴露的问题比复杂模型带来的提升更值得关注。6. 工程化细节决定比赛后期能否睡好觉的那些小事6.1 目录结构和配置管理比赛周期不长如果代码随手乱写后面改参数、换模型、跑对比实验会非常痛苦。我们从一开始就按工程项目的习惯组织代码rock-classifier/ ├── configs/ # 各实验的yaml配置 │ ├── baseline.yaml │ └── final.yaml ├── data/ │ ├── raw/ # 原始图像 │ ├── processed/ # 清洗后的图像 │ └── splits/ # 分组划分索引 ├── src/ │ ├── data/ # 数据读取、增强、划分 │ ├── models/ # 模型定义 │ ├── train.py │ └── inference.py ├── outputs/ │ ├── checkpoints/ │ ├── logs/ │ └── vis/ └── scripts/ # 辅助脚本所有超参数写进yaml配置文件不散落在代码各处。这样每一组对比实验只需要改配置、跑脚本结果也容易复现。日志里记录每个epoch的准确率、损失、当前学习率以及对应的配置文件名确保事后能倒查每个数字是怎么来的。6.2 固定随机种子与断点续训实验结果可复现是很重要但经常被忽略的工程点。我们固定了Python、NumPy、PyTorch的随机种子DataLoader的shuffle随机种子也一并固定。这样同一个配置重复跑两遍结果几乎一致不再出现“昨天跑出90%今天跑出88%”的玄学问题。训练脚本做了完整的断点续训支持每个epoch结束保存模型权重、optimizer状态和lr scheduler状态到checkpoint。假如训练到40轮机器重启可以从第39轮继续而不是从头再来。最后两周我们每天跑好几组实验这个能力的价值在每一次赶时间的时候都会被放大。6.3 显存不足的常规解法手头显卡显存不算宽裕16G显存跑EfficientNet-B3、batch size 64会直接爆。解决办法是梯度累积把batch size降到32每两个batch累积一次梯度等效batch size保持64。模型规模和等效批大小不变只是训练时间略长。同时全程开启AMP混合精度显存占用再降一档。如果还不够就继续缩小单卡batch size、加大累积步数。有一点需要注意使用梯度累积时原来的学习率不要随意改动因为等效batch size并没有变。另外开启梯度累积后要确认BatchNorm的实现方式支持小batch累积训练否则显存省下来了模型精度反而可能受损。6.4 推理端的几个小坑提交阶段整理推理脚本时有三个踩过坑的地方值得单独说。第一类别映射必须固定。训练脚本里类别列表的排序和推理脚本必须完全一致否则logits和类别名对不上预测结果全乱。我们直接用类别名的字典序映射并把映射表保存成文件随权重一起提交。第二TTA要在推理端单独写。很多框架的训练增强和推理增强是分开的千万别直接把训练时的随机裁剪搬到推理时用。推理TTA我们用固定尺寸的center crop加翻转每次输入大小保持和训练主尺度一致。第三预处理必须和训练阶段完全对齐。归一化的均值、方差、resize尺寸哪怕差一个像素都会在集成时产生不必要的偏差。这些参数直接从配置文件读取不要手写两遍也不要凭记忆复制。最后说一点个人体会如果把这次比赛重来一遍我会从第一天就按“数据工程—分组划分—可解释性—工程可复现”的框架推进而不是先急着把模型跑起来。模型结构在固定规模数据集上能拉开的空间越来越小真正决定成绩的往往是那些看起来很琐碎的数据细节和实验流程。这份复盘里的每一步都不是什么独家技巧但一件件做扎实结果自然会上来。
阅读完成 · 觉得有帮助?