ARTICLE DETAIL

资讯详情

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

SimCLR 对比学习实战:在 TinyImageNet 上自监督预训练能否赶上监督学习?

SimCLR 对比学习实战:在 TinyImageNet 上自监督预训练能否赶上监督学习? 简介基于SimCLR自监督学习与监督学习对比的图像分类研究项目面向深度学习图像分类与自监督表征学习方向的开发者可帮助理解在标注数据稀缺时如何借助对比学习训练出有效模型。项目选用TinyImageNet数据集用SimCLR预训练ResNet18并与传统监督学习训练结果比较代码覆盖数据重组模型定义数据加载器训练与测试全流程同时包含可视化配置与数据增强扩展便于复现和二次开发。压缩包共35个文件以Python脚本、SVG图像、txt与md文档为主脚本对应训练流程SVG展示图表md与txt为说明整体约580KB目录结构清晰适合自监督学习入门。目前已有57人学习下载资料包含完整工程目录、实验说明与图表结果可直接用于课程设计、毕业设计或科研工作并为超参数调整与模型改进提供参考。1. 用 SimCLR 重新审视图像分类自监督学习真的能追上监督学习吗图像分类任务里标注数据始终是稀缺资源。TinyImageNet 虽然有 200 类、每类 500 张训练图规模比完整 ImageNet 小一个量级但标注成本一样不低。这个项目把 SimCLR 自监督对比学习算法搬上来先在无标注的 TinyImageNet 上预训练 ResNet18再做线性评估和传统监督学习做一次正面交锋。它解决的核心问题不是“自监督能不能用”而是“在数据量有限、标签昂贵的条件下自监督预训练到底能不能逼近甚至超过端到端监督训练”。对新入门的研究生、算法工程师来说这是一条低成本的验证路径一套代码、一块消费级 GPU就能亲手测出对比学习和监督学习的差距到底在哪层。2. SimCLR 对比学习原理把数据增强变成监督信号2.1 核心机制用正负样本对构造自监督损失SimCLR 的思想并不复杂对同一张图做两次不同的随机增强得到两个视图把这两个视图看作正样本对把同批次里其他样本看作负样本。编码器共享权重提取特征投影头把特征映射到低维空间最后用对比损失拉近正样本对、推远负样本对。整套流程里没有标签参与但通过“哪些图来自同一个源”这一自带信息模型被迫学会忽略颜色抖动、裁剪、翻转这些增强扰动保留真正决定物体身份的语义特征。这里有个新手容易忽略的点投影头不是多余的。SimCLR 论文明确指出对比损失作用在投影头输出的 z 上而不是编码器输出的 h 上。投影头用一个两层的 MLP把高维表示压到 128 维损失函数在这个低维空间里计算。训练完做下游任务时丢弃投影头只用编码器输出。原因是 h 里保留了很多增强相关的细节比如颜色直方图这些信息在对比空间里反而干扰正样本对的聚合投影头相当于一个“过滤器”先把特征里跟增强相关的成分洗掉逼迫编码器保留更本质的结构信息。损失函数用的是 NT-Xent归一化温度标度交叉熵损失。对一个 batch 里的每个正样本对 (i, j)损失会这样计算分子是 z_i 和 z_j 的点积除以温度参数 τ 再取指数分母是所有负样本对的和再取指数。温度 τ 是控制判别难度的超参数值越小softmax 分布越尖锐模型被迫把注意力集中到最难的负样本上值越大分布越平缓训练信号越柔和。SimCLR 原论文在 ImageNet 上验证了 τ0.1 附近效果最好但在 TinyImageNet 这种小数据、小分辨率场景下τ0.1 有时候会让训练不稳定我一般建议从 0.07 到 0.3 之间先扫一遍再决定固定值。SimCLR 对 batch size 极其敏感。因为负样本就是同批次里的所有其他样本batch 越大负样本越丰富对比任务越有挑战性学到特征越泛化。原论文用 4096 的 batch这在实际项目里不现实。好在 TinyImageNet 图像只有 64×64在显存允许的前提下一个 RTX 3090 跑 512 或者 1024 的 batch 是可行的。如果显存不够可以用梯度累积模拟更大的 batch但要注意 SimCLR 论文里有一个被证实的现象batch 小于 256 时性能会显著下降所以这块不能省。对比学习还存在一个“坍塌”风险。如果编码器把所有输入映射到同一个输出点损失会完美优化但学出来的特征毫无意义。常见的诱因是投影头初始化不合理或学习率过大表现为训练曲线正常下降但准确率纹丝不动。检测手段很简单保存一批编码器输出算一下每个维度的标准差如果绝大多数维度标准差趋近于 0说明已经坍塌了。2.2 为什么选 ResNet18在 64×64 分辨率下的平衡点TinyImageNet 的图像分辨率是 64×64这是很多人第一次接触这个数据集时踩的坑——误以为它和 ImageNet 一样是 224×224。分辨率小意味着模型不需要很大的感受野就能覆盖整个物体。ResNet18 用 5 层结构一个 7×7 卷积 stem 加四个 stage理论上最低层输出分辨率是 7×7对于 64×64 输入最后一层 feature map 是 2×2已经逼近极限。再深下去比如 ResNet50在 64×64 输入上并不是不能跑但前几层就会把特征压成很小分辨率梯度传播效率反而下降收益有限。ResNet18 在自监督预训练里还有另一个身份它是被验证得最多的骨干网络之一。SimCLR 原论文做了 ResNet50 的完整实验但社区里有大量基于 ResNet18 跑 TinyImageNet、CIFAR-10 的复现工作。选它意味着遇到问题能搜到大量参考实现这对于做对比研究非常重要。如果你后续想扩大实验把 ResNet18 换成 ResNet34 或 ResNet50只需改一行模型定义数据增强、优化器、损失函数都不用动。这也是做实验设计的通用思路骨架用成熟网络变量留给训练策略。不过要留个心眼ResNet18 在 ImageNet 上的预训练权重是 224×224 输入下学习到的直接拿来在 64×64 上做迁移通常有效但如果做“从零自监督预训练”第一个卷积层的 7×7、stride 2 对 64×64 输入有点太激进。我见过不少人会把 stem 换成 3×3、stride 1再用 MaxPool 做下采样保证初期特征图分辨率不会掉太快。这个改动在 ImageNet 那种大分辨率下无所谓在 TinyImageNet 上影响明显。3. 在 TinyImageNet 上预训练 ResNet18最小可复现的 PyTorch 代码与参数3.1 数据集处理与增强策略两个增强视图的正确打开方式TinyImageNet 下载解压后是 ImageFolder 结构train 目录下 200 个子目录名字是 wnid比如 n01443537。用 PyTorch 的 torchvision.datasets.ImageFolder 能直接读进来但类名是 wnid 字符串评估时要注意映射回真实标签。验证集也需要特殊处理——它默认提供的是 val 目录加 val_annotations.txt必须按注释文件重排成子目录结构否则 ImageFolder 会把整个 val 当成一个类。数据增强是 SimCLR 的灵魂直接决定特征质量。每一张训练图都要生成两个独立的增强视图增强强度要足够大否则正样本对太简单模型学到的是 trivial 的匹配而非语义特征。我常规的增强组合是这样import torchvision.transforms as T # TinyImageNet 图像是 64x64SimCLR 原文用 RandomResizedCrop 裁剪到相同尺寸 simclr_aug T.Compose([ T.RandomResizedCrop(size(64, 64), scale(0.08, 1.0), ratio(0.75, 1.333)), T.RandomHorizontalFlip(p0.5), T.ColorJitter(brightness0.4, contrast0.4, saturation0.4, hue0.1), T.RandomGrayscale(p0.2), T.ToTensor(), T.Normalize(mean[0.4802, 0.4481, 0.3975], std[0.2297, 0.2274, 0.2250]), ]) # 对每张图应用两次生成两个视图x_i 和 x_j 构成正样本对 train_transform T.Compose([ T.RandomApply([simclr_aug], p1.0), ])这段代码里 RandomResizedCrop 的 scale 下限设置到 0.08是 SimCLR 原论文推荐值。scale 太小会让裁剪区域过小模型很难学到全局结构太大会让两个视图过于相似正样本对失去挑战性。Grayscale 的概率 0.2 对应论文里的 color distortion 强度这个值偏低如果你的训练 loss 下降缓慢可以尝试加到 0.5 试试。数据加载器要注意的第二个细节两个视图必须独立生成。常见错误是用同一个 transform 实例对同一张图调用两次但因为随机种子和内部状态的问题两次结果可能在序列化时被缓存。正确姿势是每个视图单独执行一次 transform或者自定义 Dataset 在getitem里返回两个增强后的张量class SimCLRDataset(Dataset): def __init__(self, imagefolder, transform): self.data imagefolder self.transform transform def __getitem__(self, idx): img, label self.data[idx] # label 在预训练阶段可忽略 x_i self.transform(img) x_j self.transform(img) return x_i, x_j这样写还有额外好处未来做半监督或带标签微调时这个 Dataset 可以直接复用改动成本几乎为零。3.2 SimCLR 预训练循环NT-Xent 损失、batch 大小与优化器设置预训练循环的核心是损失函数实现和批次构造。NT-Xent 损失在 PyTorch 中实现时推荐的做法是以整个 batch 为单元构造相似度矩阵然后用交叉熵一次性算出所有正样本对的贡献import torch import torch.nn.functional as F def nt_xent_loss(z_i, z_j, temperature0.1): # 将两个视图的嵌入拼接构成 2N x D 的矩阵 batch_size z_i.shape[0] z torch.cat([z_i, z_j], dim0) # 2N x D # L2 归一化确保余弦相似度 点积 z F.normalize(z, dim1) # 计算 2N x 2N 相似度矩阵 sim_matrix z z.T / temperature # 屏蔽对角线自己与自己和对称位置的重复项 mask torch.eye(2 * batch_size, dtypetorch.bool, devicez.device) sim_matrix sim_matrix.masked_fill(mask, -1e9) # 每个样本的正样本对是它在前半/后半个 batch 中对应的那个 labels torch.cat([ torch.arange(batch_size, 2 * batch_size, devicez.device), torch.arange(0, batch_size, devicez.device) ]) loss F.cross_entropy(sim_matrix, labels) return loss这个实现里正样本对的索引方式值得解释。第 i 个视图和它对应的 j 视图在拼接矩阵里的位置分别落在前半和后半所以把 labels 设定为偏移 batch_size 的索引交叉熵会自动把相似度矩阵的第 i 行当成分布去拟合正确正样本的位置。masked_fill 把对角线置为 -1e9 是为了防止模型学会“自己和自己的相似度最高”这种作弊答案。优化器在 SimCLR 里推荐用 Adam 或带 Nesterov 的 SGD。我个人的经验是在 TinyImageNet 这种小数据集上Adam 的收敛速度明显更快但最终特征质量略低SGD 收敛慢但结果更稳。做对比研究时如果论文基线用的是 SGD那预训练建议跟进同样的优化器类型避免把优化器的差异误判成自监督策略的差异。学习率需要和 batch size 做线性缩放这是 SimCLR 原论文里明确写到的技巧batch 加大一倍学习率跟着加倍。一个从零开始的参考配置optimizer torch.optim.SGD( model.parameters(), lr0.3 * (batch_size / 256), momentum0.9, weight_decay5e-4, nesterovTrue )学习率调度的选择也很讲究。SimCLR 原论文用 cosine annealing 衰减整个训练过程从高学习率平滑降到接近 0。在 TinyImageNet 上训练 100 个 epoch 是常见配置cosine 周期设置成 100。如果提前终止训练特征质量会打折扣因为 cosine 调度在训练后期才真正“沉淀”出好特征。整个预训练模型输出的维度是 512ResNet18 最后一个卷积层的输出通道数投影头把它映射成 128 维空间。参考文献设定投影头用两层 MLP中间带 BN 和 ReLU。训练时检查 loss 数值如果从 4 到 6 附近起步并缓慢下降是正常现象如果一开始就低于 0.5说明 batch 太小或增强太弱模型在走捷径。4. 监督学习基线用同一份数据把公平对比做扎实4.1 构建监督基线常规训练路线与超参数配置监督学习的基线不是随便跑一个 ResNet18 就完事它要和 SimCLR 预训练严格对齐。需要控制的变量至少有三个数据增强策略、训练 epoch 数和优化器类型。如果监督基线用了更强的增强而自监督只用简单增强最后对比出来的差距你分不清是算法带来的还是数据增强带来的。监督学习的标准做法是在训练集上用相对温和的增强测试集上只做 Resize 和 Normalize。TinyImageNet 因为输入是 64×64增强一般就用 RandomCrop带 padding、RandomHorizontalFlip也可以加入轻度的 CutMix 或 MixUp但那是额外技巧基线最好保持朴素。关键控制项是 epoch 数如果 SimCLR 预训练跑了 100 个 epoch监督基线也应该跑够同样的迭代次数。注意二者收敛特性完全不同监督学习在 30 到 50 个 epoch 就到平台期后期多跑的收益极小但为了让“训练总成本”更具可比性我通常会统计两者的总计算量FLOPs 或 GPU 时长在图里标注出来而不是强行把监督训练拉到 100 epoch。opt_sup torch.optim.SGD( model_sup.parameters(), lr0.1, momentum0.9, weight_decay5e-4 ) sched_sup torch.optim.lr_scheduler.CosineAnnealingLR(opt_sup, T_maxepoch) criterion torch.nn.CrossEntropyLoss()这个是经典 ResNet 训练配置的 TinyImageNet 适配版。学习率 0.1 配合 momentum 0.9 是直接的参考起点batch size 建议和预训练保持一致。如果 batch 从 256 调到 512学习率翻倍到 0.2 是合理做法。T_max 设成总 epoch 数会让学习率从 0.1 平滑降到接近零后期微调效果更好比固定学习率训练最后一两个点。监督基线的结果受类别数量的影响很大。TinyImageNet 有 200 类比 CIFAR-10 的 10 类困难一个数量级。训练一个从头开始的 ResNet18top-1 准确率在 50% 到 60% 区间是正常的。如果你的基线跑到 60% 以上大概率是加了外部数据的预训练或做了额外增强说明你的实验设置和论文预期不完全一致。不过更常见的坑是预训练权重没有正确加载。PyTorch 里 model.load_state_dict 在 strictTrue 模式下如果键名不匹配直接报错但很多人会用 strictFalse 硬加载导致模型实际上从随机初始化开始跑。打印一遍加载前后第一个卷积层的权重均值对比一下变化就能确认。4.2 评估协议线性探针与微调的差异公平对比不能只看微调后的准确率。SimCLR 是预训练策略监督学习是端到端训练策略两者在微调阶段的表现差异很大。业界标准的评估方式有两种线性探针linear probe和全模型微调fine-tune。线性探针是在冻结编码器权重的前提下在特征输出后接一个线性分类层只训练这个分类层。它的核心假设是如果预训练学到的好特征只需要一个线性分类器就能取得不错的准确率。这种方式最能体现自监督预训练的特征质量因为它不允许模型在使用预训练权重的同时再修改底层特征来适配任务。class LinearProbe(torch.nn.Module): def __init__(self, backbone, num_classes200, feature_dim512): super().__init__() self.backbone backbone # 冻结骨干网络 for p in self.backbone.parameters(): p.requires_grad False self.fc torch.nn.Linear(feature_dim, num_classes) def forward(self, x): feat self.backbone(x) # ResNet18 默认输出 512 维 return self.fc(feat)这里要注意 ResNet18 的输出维度如果用 torchvision 官方实现model.fc 在 forward 的时候会直接输出类别 logits所以线性探针要先把 model.fc 替换成恒等映射再从倒数第二层取特征。更省事的做法是把 model.fc 摘掉在 forward 里只过卷积部分。另一种评估是微调把预训练权重当作初始化解冻所有层继续训练。这种方式在数据量较少时通常能获得最高准确率但它的缺点是无法区分“预训练提供了好起点”和“微调期间重新学到了好特征”。做对比研究报告时建议两个指标都上线性探针反映表征质量微调反映最终可用性。微调阶段的另一个常见做法是冻结前几层layer1、layer2只更新后几层和分类头。这在目标数据量特别小的时候有效但 TinyImageNet 有 10 万张训练图完全解冻也不至于过拟合我一般直接全量微调把学习率调到预训练阶段的十分之一。评估时还要思考一下最终对比的指标除了 top-1 准确率之外最好加上 top-5 准确率和每个类别的平均召回。TinyImageNet 类间相似度高比如不同的犬种有些模型宏观表现一样但在难分类别上差异明显。必要时可以给出按相似类聚组的混淆矩阵图能直观地看到 SimCLR 和监督学习各自擅长什么。5. SimCLR 与监督学习对比研究的高频坑现象、原因与解决5.1 崩溃loss 降到 0.2准确率却接近随机现象自监督预训练过程中对比损失快速下降让人信心满满。结果线性探针做下来 top-1 只有 1% 到 2%和随机猜差不多。原因模型发生维度坍塌编码器把不同输入映射成了近线性相关的输出。这种情况在 batch size 小于 128、温度系数偏低比如 0.05时尤其容易出现。训练框架不会报警loss 一样在下降但嵌入的方差已经塌掉了。解决先查嵌入的方差。取 64 张验证图过编码器输出特征矩阵对每个维度算标准差如果一大半维度的标准差小于 0.01就可以坐实坍塌。临时补救的方法是把温度从 0.05 调回 0.1学习率砍半重新跑长期来说确保 batch 大于等于 256 或者启用梯度累积并且不要省掉投影头的 BatchNorm。5.2 数据增强顺序不当Normalize 在随机裁剪之前现象代码能跑通loss 曲线平滑但最终特征质量偏低且对不同的随机种子结果起伏很大怎么看都像是训练不稳定。原因把 ToTensor 和 Normalize 放在了 RandomResizedCrop 前面。Normalize 之后像素范围被压缩到大约 -1 到 1再执行 crop 虽然语法不报错但裁剪的边界取样和颜色抖动计算都基于标准化后的分布数值范围和颜色统计已经被改过增强效果被削弱。解决固定增强顺序几何变换RandomResizedCrop、HorizontalFlip→ 颜色变换ColorJitter、RandomGrayscale→ ToTensor → Normalize。这个顺序最好写成配置注释防止其他人改动时无意识调整。此外Normalize 用的 mean/std 要和数据集匹配不要直接抄 ImageNet 的数值TinyImageNet 的 RGB 均值和标准差有专门统计值。5.3 公平性被打破预训练和微调用了同一批数据现象对比实验最后差 2 到 3 个点你反复检查代码却找不到问题直到某天发现验证集上不可思议的高准确率。原因TinyImageNet 的验证集实际上是从训练集中的类别里分出来的如果预训练阶段的数据加载器没有严格只用 train 目录某些自监督增强视图可能来自 val 目录特征里已经“偷看”过下游任务的数据。自监督没有标签不像有监督那样能立刻察觉到过拟合等微调阶段它会把偷看过的信息释放出来。解决在数据集加载阶段做一次目录白名单校验预训练和线性探针分别用不同的 Dataset 实例且线性探针在编码器冻结后严禁再次访问训练集做特征统计比如 BatchNorm 的 running stats 更新要关掉。把数据划分写进实验配置git 记录方便审计。5.4 torchvision 模型的 fc 层维度不匹配现象加载 ResNet18 做自监督预训练时没有报错但最后一次前向传播输出维度是 200 而不是 128或者微调时维度对不上报 size mismatch。原因torchvision 的 resnet18 默认把最后的全连接层设置为 1000 类ImageNet。如果直接在自己的数据集上跑不替换 fc 层模型输出 1000 维与投影头输入 1000 维、线性探针输入 512 维就完全对不上了。解决在加载模型后立即把 fc 层替换成恒等映射或随机初始化的线性层。规范的写法是先用一个 dummy forward 检查输出维度再进训练循环。不要在训练中途才发现维度问题浪费的时间和算力很难找回来。5.5 用微调结果当唯一指标埋没自监督真实价值现象对比报告里监督学习微调 92%自监督微调 90%结论是“自监督略逊于监督学习”。但实际上自监督只用了 10% 的标注数据参与微调监督学习用了 100% 的标注。原因评估协议的设定和问题定义不匹配。这个项目标题是“自监督 vs 监督”但真实讨论的其实是“无标注预训练 少量标注微调”的分支如果不在报告里说明标注数据比例对比就没有意义。解决建议把实验设计成多组对比A 组监督学习用 100% 标签B 组自监督预训练后用 10% 标签微调C 组自监督预训练后用 100% 标签微调。如果 B 组无限接近 C 组那就证明了自监督预训练在标签稀缺场景下的价值这比单纯比“最高准确率谁夺冠”更有说服力。6. 验证预训练特征质量KNN 分类与投影可视化的实用手法训练完的特征质量除了用线性探针衡量还有一个成本更低的即时检验手段K 近邻分类器。用预训练编码器把训练集全部过一遍得到特征向量库然后对每张验证图查它的 K 个近邻如果最近邻的类别高度一致说明特征空间已经形成了清晰的聚类结构。这个方法不需要任何额外训练一张 2080Ti 上跑完 TinyImageNet 全套特征只需十几分钟是可以当作每次实验结束后的例行检查。实践中我会用这个流程预训练完先保存所有训练特征到磁盘方便后续复用对特征做 L2 归一化后存成 .npy。验证时逐个过编码器用余弦相似度算近邻。代码实现从简但要注意 T-SNE 之类的可视化做全局结构展示时采样类别要均衡否则大类会把小类压缩成一团。可视化只适合定性观察定量结论还是以 KNN 准确率和线性探针为准。我的一个习惯是每次跑完预训练不急着做线性探针先输出一张 KNN 准确率随 K比如 K1, 5, 20, 50变化的曲线。如果 K1 的准确率已经接近监督基线的 70%说明特征很锐利如果 K20 反而比 K1 高说明存在聚类但边界模糊。这种观察能快速定位预训练存在的问题避免把时间浪费在后续微调上。跑这项对比研究最值回票价的地方在于它把“自监督学习是否值得投入”从口号变成了可复现的数字。我在实验中还吃过温度系数没有随 batch 联动调整的哑巴亏后来养成一个习惯改 batch 大小就同时改学习率和温度顺序是学习率先按比例缩温度再按经验微调每次只改一个变量。如果你也是初次接触自监督领域建议严格遵守这一条能省下大量定位问题的精力。验证完这套流程你可以再往上搭 MAE、BYOL 或 SwAV 做横向扩展框架不变只是把损失函数替换掉希望这个对比项目的拆解路径能帮到你。本文还有配套的精品资源点击获取
返回列表