ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

脏标签清洗与无预训练图像分类:从置信度到ResNet-18实战

脏标签清洗与无预训练图像分类:从置信度到ResNet-18实战 简介针对2024年秋季机器学习最终竞赛的指导文档面向具备机器学习与计算机视觉基础的学生和研究者聚焦古代中国书法与绘画扫描图像的朝代分类任务涉及数据清洗、噪声标签处理与类别不均衡等关键问题。资源包为1个docx文档压缩后仅554KB内容精炼却覆盖完整包含赛题说明、任务定义、评估指标、提交规则与比赛时间节点并特别提示训练集中混有900张AI生成图像且部分标签有噪声建议先训练专用模型进行数据清洗。文档还解释了总体准确率、非AI类F1、AI类F1及长尾数据权益共享评分的权重设计帮助参赛者理解排名机制并制定优化策略。目前已有67人学习该资料适合希望快速获取比赛全貌并着手实践的参赛者阅读参考。1. 一场不让用预训练权重的图像分类赛先看清六类目标和脏标签2024 年秋季这场机器学习决赛表面是六分类图像识别真正卡人的地方却不在模型强弱而在标签噪声3600 张扫描图里混了 900 张 AI 生成图其中一半被随机贴上了唐、宋、元、明、清的错误朝代标签。也就是说你拿到的 3200 张训练图里有约 450 张真人书画和约 450 张 AI 图都标在 0 到 4 这五个类里5 号 AI 类反而只有 450 张。这个设定决定了整场比赛的走向——谁先把脏数据洗干净谁就能在准确率、F1 和 Stake 共享三项评分里同时占便宜。适合已经有深度学习基础、想用一次完整竞赛流程把数据清洗方法练扎实的人。2. 数据观察与脏标签定位先从统计里找出 900 张 AI 图的藏身处我拿到比赛包的第一件事不是搭模型而是把 CSV 和图像目录摊开搞清楚每个类到底有多少张图、标签分布长什么样。这场比赛的难点前置在数据端规则里也说得很明白标签是故意制造的噪声只有一半 AI 图被标成了 5另一半被塞进了 0 到 4 的随机类。所以训练集里 5 这个类天然是少数派而且真实朝代类目里混着大量错标样本不先做这一步后面所有评估都会被脏标签带偏。2.1 先看分布再动手把 3200 张训练图摊开第一步是读 CSV看类别分布。常见做法是先用 pandas 把训练集和测试集的目录、标签、文件名理清楚再决定后面怎么划分验证集。import pandas as pd import numpy as np df pd.read_csv(train.csv) print(df[label].value_counts().sort_index())这段代码输出的就是一个六类计数表。按比赛规则推算0 到 4 这五个类每类大约 540 张真实图 450 张 混入的错标 AI 图约 90 张5 类大约 450 张。注意这里的数字不是官方给的而是按“900 张 AI 图、一半被随机贴错”这条规则推出来的实际以你解压后的数据为准。这个分布说明一个问题类别不算极度不均衡但 5 类比真实类别少 20% 左右而且 0 到 4 里的 90 张错标图会在训练时把模型往错误方向拉。接下来划分验证集。我一般会做分层划分保证验证集和训练集的类别比例一致避免验证集里某个类恰好特别少导致评估分数虚高或虚低。from sklearn.model_selection import train_test_split train_df, val_df train_test_split( df, test_size0.1, stratifydf[label], random_state42 ) train_df.to_csv(train_split.csv, indexFalse) val_df.to_csv(val_split.csv, indexFalse)stratify 参数按原始标签比例抽样这是划分验证集的关键尤其在这种类别不平衡的场景下。random_state 固定成 42 也是习惯保证后面每次重跑代码划分结果一致。这里有个容易被忽视的点验证集里同样混着错标的 AI 图用这个验证集评估出的准确率本身就是被污染的数字所以后面第 5 章的清洗策略要覆盖到验证集上。2.2 用一次弱监督循环定位疑似 AI 图面对一堆没标错的标签直接看 CSV 看不出什么名堂。我一般会用一次很轻量的训练让模型输出每张训练图的置信度再利用“真实书画和 AI 生成图在纹理、边缘、色彩分布上有系统性差异”这个前提把置信度低的样本当作疑似 AI 图捞出来。这一步不需要强模型弱一点反而更好模型太强会把脏标签硬学进去。import torch import torchvision.transforms as T from torch.utils.data import Dataset, DataLoader from PIL import Image class ArtDataset(Dataset): def __init__(self, csv_path, img_dir, size224): self.df pd.read_csv(csv_path) self.img_dir img_dir self.transform T.Compose([ T.Resize((size, size)), T.ToTensor(), T.Normalize([0.5] * 3, [0.5] * 3) ]) def __len__(self): return len(self.df) def __getitem__(self, idx): row self.df.iloc[idx] img Image.open(f{self.img_dir}/{row[id]}.png).convert(RGB) return self.transform(img), row[label]数据集类把图像统一缩放到 224×224做一次标准化后返回图像和标签。图像原始是 512×512比赛规则允许自由选分辨率预处理时降采样到 224 能明显缩短单轮训练时间且对这种“看风格和纹理”的任务来说分辨率损失不大。Normalize 用 0.5 均值和 0.5 方差是 ImageNet 之外的常见选择因为没有预训练权重用哪套标准化参数对结果影响有限。训练这个探测模型时我用一个浅层 ResNet只训练 10 到 15 轮并且把每张图的最大 softmax 概率记录到磁盘上。记录方式是每轮结束对整批训练集做一次前向推理保存 logits 而不是直接保存标签这样之后可以随时换阈值重新清洗不用再跑一遍模型。2.3 置信度分布阈值从哪里来拿到所有训练样本的最大概率后画直方图是判断是否存在“低置信度群体”的最直接办法。如果数据真的混入了大量 AI 图直方图通常会出现一个双峰一峰是高置信度的真实朝代图另一峰是置信度普遍偏低的疑似 AI 图。import matplotlib.pyplot as plt max_probs all_logits.max(axis1) plt.hist(max_probs, bins50) plt.xlabel(max probability) plt.ylabel(count) plt.show()这段代码把每张图的最高类别概率画成直方图。观察双峰之间的谷底谷底附近的概率值就是清洗阈值的一个合理起点。我一般会先选 0.5 到 0.6 之间的值把低于阈值的样本重标为 5或者直接从训练集里剔除。注意这里的方法不是玄学本质上是用模型的不确定性来代理标签噪声错标的样本由于特征和标签不匹配模型很难给出高置信度。阈值不要选太高否则会误伤大量真实朝代图把原本能用的训练数据也扔掉了。清洗逻辑会在第 4 章完整展开。3. 模型选型与训练配置没有预训练权重时的 ResNet-18 路线比赛规则里有一条红线任何部分都不得使用预训练模型权重。这条规则直接影响选型——你没法用 ImageNet 上训练好的 ResNet 做迁移学习所有参数都得从零开始学。这反而让选型变简单了模型不需要太大因为数据集只有 3200 张图模型越大越容易过拟合也不建议太小因为朝代特征的区分度很微妙线条、印章、纸张纹理、构图习惯这些信息需要足够的容量去捕捉。3.1 为什么是 ResNet-18中小数据集的无预训练首选ResNet-18 在这个场景下是个平衡点。残差结构让梯度在 18 层里还能顺畅回传训练比 VGG 稳参数规模又比 ResNet-50 小一个量级在没有预训练的情况下不会因为数据量不足而崩掉。规则限制下从零训练的模型可参照的路线不多常见做法是把输入缩到 224×224 或 256×256用 ResNet-18 做 backbone最后接一个 6 类全连接层。from torchvision.models import resnet18 model resnet18(weightsNone, num_classes6) model model.cuda()重点在 weightsNone这表示完全随机初始化不使用任何预训练权重。用 torchvision 构造模型虽然方便但要警惕手滑把 weights 写成默认值那就踩了规则红线。num_classes6 对应唐、宋、元、明、清、AI 六个类别。模型前向输出是 6 维 logits后面接 CrossEntropyLoss 或者带标签平滑的变体都可以。3.2 数据增强与训练循环组合别贪多数据增强在这个任务里是一把双刃剑。真实书画的结构信息很强比如竖轴的落款位置、卷轴的题跋方向、印章的排列这些是判别朝代的重要线索。旋转角度过大或随机裁切比例过大会把这些线索直接抹掉。我一般只用水平翻转、小角度旋转±10 度以内、小幅缩放和颜色抖动组合数控制在五到六种以内。train_transform T.Compose([ T.Resize((224, 224)), T.RandomHorizontalFlip(p0.5), T.RandomRotation(degrees10), T.ColorJitter(brightness0.2, contrast0.2), T.ToTensor(), T.Normalize([0.5] * 3, [0.5] * 3) ])训练循环按标准流程走SGD 配合 momentum或者 AdamW 配合 cosine 学习率调度都可行。没有预训练权重时学习率不需要像微调那样压得很低3e-4 到 1e-3 是常见范围。batch size 看显存定224×224 输入下 ResNet-18 用 64 到 128 都合理。from torch.optim import SGD from torch.optim.lr_scheduler import CosineAnnealingLR from torch.nn import CrossEntropyLoss criterion CrossEntropyLoss() optimizer SGD(model.parameters(), lr0.01, momentum0.9, weight_decay5e-4) scheduler CosineAnnealingLR(optimizer, T_max20) for epoch in range(20): model.train() for imgs, labels in train_loader: imgs, labels imgs.cuda(), labels.cuda() optimizer.zero_grad() logits model(imgs) loss criterion(logits, labels) loss.backward() optimizer.step() scheduler.step()SGD 在这个数据规模下表现稳定weight_decay 用 5e-4 控制过拟合。CosineAnnealingLR 的 T_max 设为总轮数让学习率在最后几轮降到接近零有助于收敛到更平缓的极小值。如果显存吃紧可以加梯度累积或者把分辨率降到 192但一般 ResNet-18 在 224×224 下不会太吃紧。训练日志里至少要记录每轮的 train loss 和 val accuracy方便后面看清洗策略是否生效。3.3 实验记录的习惯四个维度缺一不可到这一步你已经能跑通一个完整的训练流程了。但比赛要求提交的代码必须可复现模型权重要能重放出测试集预测结果所以从第一轮实验开始就要强制记录四样东西数据划分的随机种子、数据增强配置、优化器超参数、验证集评估结果。我习惯用一个表格把每次实验记成一行字段包括模型、分辨率、增强策略、是否清洗、val acc、val F1。没有这个记录后面调三四个版本就分不清哪个配置跑出的是什么结果了。4. 两阶段的清洗与重训把噪声标签变成可用监督信号数据清洗是这场比赛的核心得分手段。规则已经点明TAs 也专门提示——把噪声和不均衡数据处理好比换一个更强的模型更有用。我的做法是分两阶段走先用带脏标签的数据训练一个粗模型用它的置信度定位疑似 AI 图再把疑似样本重标为 5用清洗后的数据重新训练最终模型。这个思路不复杂但阈值怎么定、清洗几轮、重标还是剔除每个决定都会影响最后几个点的分数。4.1 阶段一先让模型“告诉”你噪声在哪第一阶段模型不需要追求高准确率它的唯一任务是产出每张训练图的置信度。我用第 3 章配置训练 10 到 15 轮训练集直接用原始 CSV不做任何清洗然后对训练集全体做一次前向推理把每张图的 6 类概率存成 numpy 数组。这一步的作用是把模型当作一个噪声探测器真实朝代图和被错标的 AI 图在特征空间里并不重叠模型对错标样本的预测往往会犹豫最大概率偏低。model.eval() all_logits [] with torch.no_grad(): for imgs, _ in train_loader: logits model(imgs.cuda()) all_logits.append(logits.cpu().numpy()) all_logits np.concatenate(all_logits, axis0) np.save(train_logits.npy, all_logits)这段代码把训练集的 logits 全部保存下来之后无论怎么改阈值、怎么重标都不用重新跑模型直接在这个数组上做操作。注意这里的 train_loader 必须不启用 shuffle并且要和原始 CSV 的行顺序一一对应否则后面重标时索引会错位。保存 logits 而不是直接保存预测标签是为了保留软信息——一个样本的最大概率是 0.45 和 0.85在清洗决策上的意义完全不同。4.2 阶段二低置信度样本重标为 AI还是直接剔除拿到 logits 后用最大概率做二值筛选。低于阈值的样本有两种处理方式重标为 5或者直接从训练集里删掉。我一般先试重标因为 5 类本身只有 450 张把疑似 AI 图并进去可以让模型学到更完整的 AI 图特征如果重标后验证集 F1 反而下降再试剔除。max_probs all_logits.max(axis1) low_conf_mask max_probs 0.5 df_clean df.copy() df_clean.loc[low_conf_mask, label] 5 df_clean.to_csv(train_clean.csv, indexFalse) print(f清洗样本数: {low_conf_mask.sum()}) print(df_clean[label].value_counts().sort_index())这段代码把置信度低于 0.5 的样本全部重标为 AI 类。清洗的数量可以从打印信息里直接看到正常预期在一百到三百张之间。如果清洗出来的样本数接近 450说明模型把大量错标 AI 图找出来了方向正确如果只有几十张说明阈值设得太严或者第一阶段模型学习能力不够需要把训练轮数加长再试。4.3 迭代还是不迭代一次清洗的边界清洗完一轮之后我通常会拿清洗后的训练集重新训练一个模型然后看验证集上非 AI 类的 F1 是否提升。提升明显说明阈值选得合适提升不明显甚至下降问题可能出在阈值太高误伤了真实朝代图或者第一轮模型本身太弱筛选结果不可靠。常见的做法是把这个流程迭代两轮但我不建议超过两轮。原因是模型会把自己的误差带进第二轮清洗——第二轮模型的筛选结果会延续第一轮模型的偏见边缘样本可能在两轮之间反复横跳形成伪标签的自我强化。我一般第一轮用较宽的阈值先捞掉明显错标样本第二轮把阈值提高 0.05 到 0.1只清理那些两轮都被判定为低置信度的稳健噪声样本。这样做的好处是保留了一部分有争议的样本让训练集保持多样性避免清洗过度导致数据量骤减。5. 五条高频翻车记录与排查步骤这个比赛从数据到评估都有坑很多看起来合理的操作会在看不见的地方把分数拖低。下面五条是我自己复现这个资源时反复踩过的问题每一条都按现象、原因、解决的顺序整理好供你做同样流程时对照排查。5.1 类别不平衡不洗数据时模型全押在多数类上现象训练集里 0 到 4 类各约 540 张5 类只有 450 张不处理直接训练模型在验证集上对 4 类和 0 类的预测概率偏高混淆矩阵显示预测结果大量集中到样本量最大的几个朝代类。原因CrossEntropyLoss 对样本量天然敏感。5 类少 20%模型为了压低整体 loss倾向把不确定样本判给多数类AI 类 F1 掉到很低的水平。解决先用第 4 章的清洗策略把错标样本重标为 5让 5 类数量补上来如果补完后还是偏给损失函数加类别权重权重按类别样本数反比计算或者用 WeightedRandomSampler 让采样器每轮从少数类里多抽几次。5.2 数据增强过头朝代特征被旋转抹掉了现象加了 RandomRotation90 度和 RandomResizedCrop 后训练 loss 降得很快但验证集准确率卡住不动甚至下降。原因书法和绘画的布局有方向性横卷和竖轴的落款、印章位置是判别朝代的重要线索。旋转 90 度等于把这类信息随机破坏了模型只能靠颜色和纹理做判断而这些特征在不同朝代的区分度远不如结构信息。解决把旋转角度限制在 ±10 度裁切改成 Resize 加少量缩放0.8 到 1.0保留完整的画幅边界。水平翻转保留因为书画图像左右翻转不会破坏朝代特征。改完后验证集准确率会有一个明显的回升。5.3 验证集划分不当本地 90 分线上 65 分现象本地验证集准确率接近 90%提交到 Kaggle 只有 65%怀疑模型过拟合但找不到原因。原因验证集是从原始 CSV 直接随机划分的里面混着大量错标 AI 图。本地评估时这些样本的标签是错的模型预测出“错误”的答案反而被当作正确线上测试集标签是干净的同样预测就变成了错分。本地分数虚高来自验证集标签噪声。解决验证集也要走清洗流程。先拿到训练模型的 logits把验证集里低置信度样本的标签也重标为 5再计算准确率和 F1。另一个做法是分两次划分第一次划出验证集后单独统计验证集里的疑似噪声比例如果超过 10%重新划分或考虑用多折交叉验证。5.4 随机种子没固定同一份代码两次结果差两个点现象同样的代码和参数跑两次输出的验证集准确率差了 1.5 到 2 个点提交到 Kaggle 的分数也飘忽不定。原因DataLoader 的 worker 进程、数据增强、模型初始化都依赖全局随机种子不固定的话每次训练的数据顺序和增强结果都不同小数据集下这种波动会被放大。解决在训练脚本开头固定 python、numpy、torch 三套种子DataLoader 里设置 generator 并固定 worker 的 seed最后提交前重跑一次完整流程验证两次测试集预测的差异小于千分之三。比赛规则明确要求结果可复现这一步不是可选项。5.5 测试集预测和本地验证不一致CSV 行序丢了现象本地验证集效果不错但提交后分数骤降检查 CSV 发现测试集的 id 顺序被打乱了。原因测试集预测时用了带 shuffle 的 DataLoader或者测试集读入顺序经过排序和加载时不一致导致提交的 label 和测试集原始 id 对应不上。解决预测码里用 batch_size 为 1、shuffleFalse 的 DataLoader按原始 CSV 的行顺序预测最后用 read_csv 读回测试集合并 id 和预测标签后再保存。我在第 6 章会给出一个最小可用的导出模板。6. 决赛前的验证清单与 CSV 提交把最后一公里走稳到比赛后半程模型训练已经不是主要矛盾提交物的一致性和可复现性才是决定生死的地方。这里分享三个我每次提交前都会强制走一遍的步骤都是从实际翻车里换来的习惯。6.1 CSV 导出与最小正确性检查测试集预测的 CSV 格式在规则里没有给死但 demo code 提供了一个模板。我一般会保持两列——id 和 label行顺序和测试集原始顺序一致任何额外列都不要加。import pandas as pd test_df pd.read_csv(test.csv) preds predict_all(model, test_loader) # 保持 test_loader 不开 shuffle sub pd.DataFrame({id: test_df[id], label: preds}) sub.to_csv(submission.csv, indexFalse) print(sub.shape) print(sub[label].value_counts().sort_index())提交前检查三件事shape 是否等于 400 行label 是否全部落在 0 到 5 的整数范围0 到 4 类的预测数量是否和训练集分布大致接近。如果预测结果里某个类占了 80%说明模型倾斜严重再检查第 5 章的类别平衡问题。日常我还会额外打印一张混淆矩阵确认验证集上哪些类互相混淆最厉害这个信息在做最后调阈值时很有用。6.2 固定随机种子与可复现三件套规则里白纸黑字写着提交的代码和模型权重必须能重放出测试集预测。我平时养成的习惯是每个实验副本都保存三样东西完整的 config 配置模型、分辨率、lr、epoch、清洗阈值、数据增强开关、训练日志每轮的 loss 曲线和验证指标、以及随机种子。提交前至少完整重跑一遍训练和预测确认两次生成的 submission.csv 完全一致。如果两次有微小差异检查是不是 DataLoader 里漏了固定 worker seed。def set_seed(seed42): import random import numpy as np import torch random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed)这个 seed 函数放在训练脚本最顶部调用一次就够。注意固定 seed 不能保证跨平台完全一致但同一台机器同一环境下足以满足复现要求。Stake 共享指标计算周期和提交规则偶尔会更新所以在比赛最后几天我每天只提交一次提交前先去 Kaggle 看当天分数和排行榜变化而不是频繁消耗每天 10 次的提交额度。6.3 Stake 共享别为 10% 的权重打乱主线最后简单说下 Stake 共享指标一张测试图如果只有 3 个人预测正确这 3 人各拿 1/3 的 stake累积后按比例换算成得分超过 10% 的部分会被截断。这个机制本质是奖励那些能把长尾难点题做对的人但权重只有 10%主线仍然在准确率和两类 F1 上。我的策略是模型主线不动只在最后用一个小模型集成去专门看那些高置信度但类别分布异常的样本如果模型对某张图非常有信心且预测的朝代在常识上成立就保留如果普遍犹豫维持主模型预测。这一项不值得为它专门改阈值牺牲 30% 的准确率去换 10% 的 stake账算不过来。从那以后我每次提交前都强制走一遍这套流程查测试集行序、固定随机种子重跑、核对 CSV label 分布、看一眼验证集混淆矩阵。这些动作看着琐碎却实实在在拦住了我至少三次因行序错乱和 seed 漂移导致的低分提交。希望帮到你。本文还有配套的精品资源点击获取
返回列表