资讯动态

BYOL自监督学习解析:拒绝负样本,用动量更新对抗表征坍塌

发布时间:2026/10/19 8:35:47 来源:尧图企业网站定制
BYOLBootstrap Your Own Latent这篇工作出来的时候我第一反应是“这怕不是个BUG”。当时自监督学习几乎被对比学习统治SimCLR、MoCo那一套都在强调负样本的重要性业内默认的共识是没有负样本做约束网络一定会把所有样本压到一个点上也就是所谓的表征坍塌representation collapse。然后BYOL横空出世告诉所有人我不用负样本我用一个预测器加一个慢更新的目标分支照样能把表征学出来甚至在ImageNet线性评测上超过了SimCLR。这篇文章不是论文精读更像是我自己从原理到复现、再到现在理解BYOL的一份完整笔记。我会把里面最关键的设计掰开揉碎讲明白为什么自监督需要负样本、BYOL凭什么敢扔掉负样本、它的两个分支到底怎么协作、动量更新为什么是灵魂、训练里哪些地方容易翻车。看完之后你能理清BYOL的原理也能在心里大概感知到它背后的设计范式而不是只记几个名词。1. 自监督学习遇到了哪两堵墙负样本依赖和特征坍塌1.1 对比学习的基本玩法拉近正样本推远负样本要理解BYOL的价值得先看它出现之前大多数人怎么玩自监督。自监督学习的本质是没有人工标签我们要构造一个“代理任务”pretext task让模型从数据自己产生监督信号。对比学习大概是这几年来最成功的一条路线它的思想概括起来就是同一张图片做两次不同的数据增强得到两个正样本它们的表征应该尽可能接近同时不同图片的表征应该彼此分开。于是损失函数就变成了一个InfoNCE或者类似的形式核心结构是[ L -\log \frac{\exp(\text{sim}(z_i, z_j)/\tau)}{\sum_{k1}^{N} \exp(\text{sim}(z_i, z_k)/\tau)} ]分母里的 (z_k) 有一个比较重要的社会功能——它是一个“排斥项”逼着模型不把不同样本的表征挤到一起。这个分母里的样本就是所谓的负样本它们可能来自同一个batch里的其他图片也可能来自一个独立维护的特征队列。在SimCLR里batch size直接决定了负样本数量所以SimCLR被诟病吃显存属实是大batch怪物MoCo则是用动量队列把负样本累积起来数量可以做得很大。1.2 负样本的“隐性账单”显存、吞吐、还有模型盲区负样本听起来简单真的做起来你会发现有一堆麻烦事。首当其冲的是显存压力。你想让负样本足够多一般会直接加大batch size一个batch塞几百上千张图每个样本还要跑一遍编码器得到表征显存和算力都不是小数。有人可能会说那我不加大batch我从缓存队列里取负样本呢MoCo就是这么做的能缓解一部分压力。但还有一个更微妙的问题负样本的质量并不稳定。当你从数据流中取样作为负样本时你的负样本是随机的。一个batch里面可能大量负样本都是“易分”的模型的判别压力就小如果负样本里出现了跟正样本非常相似的其他物体模型又容易被带偏。更本质的问题在于对比学习学到的表征严重依赖负样本的“多样性”如果你负样本分布不够广你学到的特征可能只在某些维度上有区分能力其他维度压根不敏感。这里还有一个说法叫“维度坍塌”dimensional collapse模型学会了在若干维度上均匀分布又把其他维度压缩掉从判别角度看它对正负样本区分得不错但学到的表示有冗余、信息不完整下游任务的泛化性能会受影响。1.3 特征坍塌自监督里最怕的那个幽灵什么叫特征坍塌简单说模型把所有输入都映射到同一个输出损失值变得很低但表征完全失去了区分能力。在对比学习里因为有一个负样本排斥项在兜底模型很难陷入这种全坍塌状态。最极端的坍塌是“完全坍塌”所有样本的表征变成一个常数还有一种更隐蔽的“部分坍塌”某些维度上模型没有任何区分度。一个很有意思的现象是如果你在BYOL模型里只保留online分支和target分支去掉predictor去掉动量更新也就是两个分支共享权重同步更新然后只优化两者的余弦相似度那么模型会迅速坍塌——所有样本的输出向量变成同一个方向损失直接逼近最优值但特征已经废了。BYOL的作者并没有绕开这个问题他们承认去掉负样本以后坍塌在理论上仍然是一个危险项。他们靠的是架构上的精巧设计把坍塌的风险压到了一个极低的水平而不是彻底从数学上消除它。2. BYOL的核心设计不用负样本凭什么能稳住2.1 架构总览online分支和target分支的“师徒”关系BYOL的框架其实不复杂。它同时存在两个主干网络一个叫online分支一个叫target分支。online分支我们通常叫“学生”target分支叫“老师”这是我个人觉得最直观的理解方式。student网络更新得快teacher网络更新得慢student的目标就是去预测teacher输出什么然后teacher再根据student的进度慢慢调整自己。具体来说student和teacher的输入不是同一张图而是同一张图经过不同数据增强后的两个视角view。student吃第一个视角teacher吃第二个视角。整体流程大致是图片经过随机裁剪、颜色扰动等增强得到 (v) 和 (v)分别送入online分支和target分支。online分支经过encoder (f_\theta)、projector (g_\theta) 得到投影 (z_\theta)接着过一个predictor (q_\theta) 得到预测 (q_\theta(z_\theta))。target分支经过 (f_\xi) 和 (g_\xi) 得到 (z\xi)。损失就是 (q\theta(z_\theta)) 和 (z_\xi) 的余弦相似度。这里比较重要的是online分支里多了一个predictor而target分支没有。为什么必须是online有、target没有这个细节到后面细讲它不是一个无意义的对称性破坏而是整个防坍塌机制里不可缺少的一环。2.2 三重机制防止坍塌predictor、慢更新、停止梯度我一直拿“师徒”这个类比来想BYOLstudent要用自己的视角去想象teacher在另一个视角下看到的东西。teacher不能太抢镜也不能完全同步于是有了三重保险。保险之一target分支的梯度是停掉的也就是对target不进行梯度更新它只作为回归目标被使用。因为如果我们同时对student和teacher做梯度更新两侧都在互相追逐训练非常容易变成“两个人对着镜子赛跑”最终两个人一起摔倒坍塌。停掉梯度以后student永远在朝一个相对固定的目标努力teacher只通过慢速移动来更新“标准答案”。保险之二teacher参数通过EMA指数滑动平均更新。也就是说teacher并不是不动而是用一个很低的动量系数把student的参数一点点“平滑”过来[ \xi \leftarrow \tau \xi (1-\tau) \theta ]这个 (\tau) 一般取0.99到0.999。(\tau)越大teacher更新得越慢student像是面对一个“更老练但反应迟钝”的师傅。这个设计在概念上与MoCo的动量队列一脉相承但在BYOL里更直接——更新的不是队列而是目标网络的权重本身。保险之三就是那个predictor。predictor的存在是为了“打破对称性”它让online分支的输出空间和target分支的输出空间不必完全对齐student的任务变成“预测”而非“拷贝”。我曾经看过一个很形象的解释如果没有predictor两边的特征都会朝着彼此靠近最终在表征空间里陷入一个共同的退化点有了predictor以后student的输出要经过一次非线性变换再去和target对齐这给表征空间里的流动引入了额外的自由度坍塌的通道被堵住了不少。2.3 损失函数对称余弦相似度背后的隐语BYOL的损失函数公式写出来不吓人[ L 2 - 2 \cdot \langle q_\theta(z_\theta), z_\xi \rangle ]这里的 (\langle \cdot, \cdot \rangle) 是L2归一化后的余弦相似度由于两个向量都是单位向量余弦相似度等于它们的点积。目标就是最大化这个点积推到极限就是两个向量完全同向损失为0。但注意这个公式只是单向的。实际在训练中输入的两个视角是会调换位置再算一次的也就是说第一次(v) 进online(v) 进target第二次(v) 进online(v) 进target然后把两个方向的损失加起来。对称化处理看起来只是增加了一次计算但它真正的作用是让模型的训练目标变得均衡——即使某个视角被增强得比较“狠”或者比较“轻”模型都不会单边偏向。在实际实现里我会建议先把它写成单向的验证forward和loss都能跑通再补成对称形式这样如果出了nan或者维度错误排查起来更快。2.4 为什么“预测未来”比“判别过去”更稳对比学习本质上在做判别判断两个样本是否来自同一种数据分布。这种学习方式很容易把特征的判别性拉满但不一定把特征的表征性representational quality拉满。BYOL做的事情更像是一个回归任务student的输出要尽可能接近teacher的输出。这种“匹配式”目标天然更平滑不会像对比损失那样有一个排斥项让梯度变得非常尖锐。有人可能会质疑如果没有负样本直接最大化student和teacher的一致性模型随便输出一个常数不也能让损失为0吗没错这正是理论上的漏洞。但实验表明乳化组合里既有structure又有dynamicspredictor的非线性、数据增强的随机性、EMA的慢更新、以及两个分支之间的相互制衡加在一起构成了一种“动态均衡”。模型一旦想朝着全常数方向坍塌student和teacher因为输入视角是不同的加上teacher更新得慢沿途的梯度信号会先把student拉回来。BYOL不是用数学公式说明了“不会坍塌”而是用实验证明了“在很多数据集上和不同超参数下它就是不塌”。所以如果你在某个特殊任务上强行套BYOL还是需要自己监控表征的秩和分布是否有坍缩迹象不能想当然地觉得它百毒不侵。3. 从零复现BYOL核心模块与工程注意点3.1 网络搭建backbone projector predictor要跑出可用的BYOLEncoder部分一般选ResNet-50不带分类头。输入图像过encoder得到2048维特征ResNet-50最后一层平均池化后然后接一个MLP projector。这里有个常见的理解差异其实论文里的projector是在“训练结束后会被丢弃”的只保留backbone的表征给下游任务用。Projector的推荐结构是import torch import torch.nn as nn import torch.nn.functional as F class MLP(nn.Module): def __init__(self, in_dim, hidden_dim4096, out_dim256): super().__init__() self.layers nn.Sequential( nn.Linear(in_dim, hidden_dim), nn.BatchNorm1d(hidden_dim), nn.ReLU(inplaceTrue), nn.Linear(hidden_dim, out_dim), nn.BatchNorm1d(out_dim), ) def forward(self, x): return self.layers(x)Predictor略微简单些它不是输出到256维就算完而是输出维度保持不变还是256维只是中间过一层class PredictionMLP(nn.Module): def __init__(self, in_dim256, hidden_dim4096): super().__init__() self.layers nn.Sequential( nn.Linear(in_dim, hidden_dim), nn.BatchNorm1d(hidden_dim), nn.ReLU(inplaceTrue), nn.Linear(hidden_dim, in_dim), ) def forward(self, x): return self.layers(x)有个容易踩的坑是BatchNorm放在哪个位置。论文的projector是每个线性层后面都接一个BN包括最后输出层后面的BN而predictor内部也用了BN。一个不太起眼但其实很重要的原因在于BN的统计量本身也会随着batch变化而变化它天然又在暗搓搓做一些“归一化”的工作对防止坍塌有正向作用。训练时一定要开train模式如果靠惯性把模型切到eval模式去训练你大概率会得到很差的结果因为BN统计量不再随梯度更新而平滑调整了。3.2 数据增强BYOL比较依赖强增强自监督学习里数据增强就是“标签生成器”。BYOL对增强的依赖程度不亚于SimCLR。论文里用的是SimCLR风格的增强组合随机裁剪加缩放、随机颜色抖动、随机灰度化、高斯模糊、太阳能化solarize。其中随机裁剪和颜色抖动的强度比较关键。我试下来有一个体感BYOL对颜色扰动异常敏感。如果你把颜色抖动的强度调得太低模型很快就表现出坍塌倾向——这里说的坍塌不一定是完全常数可能是指表征的有效维度下降、loss不再健康下降、下游评测表现变差。颜色扰动其实是在“模拟同物体的不同外观”相当于给teacher的视角和student的视角制造足够大的语义鸿沟让student学的东西更鲁棒。这里给一个参考增强配置随机裁剪区域比例在0.08到1.0之间长宽比变化0.75到1.333颜色抖动强度0.4高斯模糊概率0.5灰度化概率0.2solarize概率0.1。实际调参的时候还是得按自己的数据集来试特别是医学影像或者红外图像这类光谱比较单一的数据过度颜色扰动反而会引入太多噪声。3.3 优化器与动量调度控制teacher“衰老”的节奏BYOL经典的配置是LARS优化器因为它在大批量训练下不会像SGD那样出现明显的性能衰减学习率采用cosine schedule基础学习率随batch size做线性缩放。如果你用的是小batch比如256以下直接上SGD带momentum、weight decay也能跑只是最后的精读效果会比用LARS略差。我个人经常看到有人拿Adam来跑BYOL并不是说不行而是Adam和BN在大规模自监督场景下的配合偶尔会不稳定。真要用Adam建议学习率降低一个数量级再试。动量系数 (\tau) 也不是全程固定的而是从 (\tau_00.996) 开始训练中逐步升到1[ \tau 1 - (1-\tau_0) \cdot (\cos(\pi \frac{k}{K}) 1)/2 ]也就是说训练初期teacher动得快一些让student能追上训练后期teacher几乎冻结student在其中慢慢精修。我自己跑小规模数据集时倒是经常直接用固定的0.99或者0.995效果也还行但如果你想复现论文并做对比最好还是把余弦动量调度加上因为早期学习阶段teacher动得快确实能帮student更快找到有效特征方向。3.4 完整伪代码用50行看清BYOL整体流程这里我给一个精简版PYTORCH风格的伪代码集中展示每一步的顺序。实际工程里你还要加上分布式、AMP、EMA的register_buffer或者额外的权重副本管理但逻辑骨架是这几十行# online_encoder: backbone projector # target_encoder: backbone projector for x in dataloader: v1 augment(x) v2 augment(x) # 对称loss准备 z1_online predictor(online_encoder(v1)) z2_online predictor(online_encoder(v2)) with torch.no_grad(): z1_target target_encoder(v1) z2_target target_encoder(v2) loss 2 - 2 * F.cosine_similarity(z1_online, z2_target, dim-1).mean() loss 2 - 2 * F.cosine_similarity(z2_online, z1_target, dim-1).mean() loss loss / 2 loss.backward() optimizer.step() # EMA更新target_encoder with torch.no_grad(): for param_online, param_target in zip(online_encoder.parameters(), target_encoder.parameters()): param_target.data tau * param_target.data (1 - tau) * param_online.data有一个实现细节提醒下target分支的参数不能直接等于online的拷贝你得准备两份独立的网络实例但不用给target单独加载优化器。很多初学者会忘了with torch.no_grad()导致target分支也会累积梯度而EMA更新本身又不是基于梯度的于是整个流程语义就乱了。target分支虽然不参与反向传播但这个分支的推理计算图默认会保存在内存里即使torch.no_grad()不加损失计算完以后target分支权重也不会更新——这就有个隐患内存占用很高容易OOM。所以规范的做法是显式包一层torch.no_grad()省的后续排查起来很痛苦。3.5 Batch Size 和显存BYOL 对资源要求的真实感受SimCLR动辄需要几千的batch size才能出好效果BYOL相对亲民但也不是小玩具。论文里主要实验用1024的batch size训练1000个epoch。在小数据集上比如CIFAR-10这类256的batch size也能work只是精度会差一些。从我复现的经历来看BYOL对batch size的容忍度确实比对对比学习更高。为什么因为对比学习里的负样本来自batch内batch小了负样本数量不足正负样本比失衡BYOL没有负样本这个概念它的约束只来自两个视角的一致性所以batch的规模对精准度的影响不如对统计量稳定性的影响大。BN的统计量在小batch上抖得厉害projector和predictor里的BN本身又是防坍塌的助力所以如果你的GPU只能跑到batch 64我建议不要硬怼ResNet-50换个轻量backbone或者把输入分辨率降到160附近这样BN统计量能更稳一些。4. 常见问题与排查技巧实录4.1 训练几个epoch之后loss不下降反而上升我先说一下我的排查顺序。先确认loss是不是还在一个正常量级BYOL的loss正常情况一般在0到2之间因为最大余弦距离是2。如果loss高于2检查是不是把类似MSELoss那种不加归一化的损失拿进来了。如果loss一开始下降然后又反弹优先怀疑学习率和EMA动量之间的配合learning rate过大时online分支更新太猛EMA的target还没跟上student和teacher之间出现了“振荡追逐”表现为loss波动。还有一个很多新手容易忽略的地方——数据增强里的random crop要配合random resize。如果你的数据增强里用了固定resize再做crop两个视角的覆盖区域可能高度重合目标过于简单loss前期降得很快但到了后面几乎不再变化而且训练出来的表征泛化能力一般。这个我之前遇到过把增强改成随机裁剪加缩放后loss曲线形态有了明显改善。4.2 表征真的坍塌时怎么检测很多人问怎么判断我的模型是正常训练还是已经在坍塌边缘了方法很简单把每个batch的表征收集起来做一个矩阵然后计算它的奇异值分布或者协方差矩阵的秩。正常训练时表征矩阵的奇异值通常有一个比较“宽”的分布说明不同维度都在承载信息坍塌时奇异值会快速衰减成只有前几个大尾巴甚至变成一个极尖的分布。实际操作中你可以在validation集上每次跑一个batch把online分支输出的特征拼起来对特征矩阵做SVD。如果你发现模型有效秩小于10那大概率已经开始崩了。这时候去检查predictor是不是被不小心删掉了、EMA动量是不是设成了0.99以下、数据增强是不是太弱基本能找回原因。4.3 分布式训练和BN引起的抖动BYOL的projector和predictor都依赖BN。分布式训练时每个进程有自己的BN统计量如果数据被均分到不同卡上BN统计量是各自算各自的这会导致student和target对同一个batch的统计特性感知不一致。实践中有人用SyncBN同步批归一化来解决。特别是在多卡训练时建议第一件事就把BN换掉或同步否则你的loss曲线稳定性和最终效果都会受到明显影响。这个坑我是亲手踩过单卡跑得好好的一上多卡性能就变了排查到最后就是BN没同步。4.4 小数据集上BYOL是不是一定合适很多人在小数据集上跑BYOL结果发现效果甚至不如下游监督学习就开始怀疑这个方法。说实话自监督学习本来就是数据饥饿的它在无标签大数据上才有明显优势。如果你的数据集只有几千张图BYOL更多时候是在学习增强策略而不是捕获丰富的语义类别。我之前在一个大概1万张图的小数据集上跑过linear probe精度还行但和强监督的ResNet比还是差一些。这种场景下我更推荐拿预训练好的自监督模型做微调而不是从零自监督预训练。4.5 给下游任务做迁移时的一些技巧BYOL训练完成后我们一般只用encoder部分projector和predictor都扔掉。下游任务有两种常见用法一是把所有网络参数冻结只在上面接一个线性分类器用这个小线性头来评估特征质量也就是linear probe二是在下游任务上做全参数微调自监督预训练相当于做了一个非常好的初始化。我做迁移实验时有个习惯先用linear probe做一个快速摸底看特征的线性可分程度。如果linear probe表现不错说明表征质量已经相当高如果linear probe不好但微调之后暴涨说明特征可能要“重新线性化”那大概率是预训练任务和数据分布有些错位需要通过微调来校正。5. 我对BYOL的理解和它留下的设计遗产BYOL最让我觉得漂亮的地方在于它把“学习表征”这件事从判别问题变成了预测问题。它没有硬性抵抗坍塌而是给模型一个自己的工作机制让一个慢信号去牵引一个快信号让模型自己找到一致的、稳定的表征方向。这种思路在之后很多方法里都能看到影子。比如后来的SimSiam直接把EMA都砍了只用stop-gradient和predictor发现也能work进一步说明负样本也并非必须只要网络结构内部存在一种不对称性就能形成抗坍塌的动态约束。还有一个发展方向是让两个分支“主动交换信息”比如某些蒸馏类方法会引入teacher的soft label和BYOL在精神上也是相通的。我现在做很多无标签数据处理任务时BYOL仍然是我优先考虑的一个baseline。它不用费劲维护负样本队列不用卡一个巨大batch size数据增强调好之后可以安安静静跑完整个预训练。回想起最早看到BYOL的时候我甚至怀疑是实验代码写错了才得到这个结果但后来自己动手把那些模块一个个仔细实现过后才真正体会到一个好的自监督框架靠的不是把工程细节叠到极致而是在正确的抽象层次上做了一件简单、优雅、有效的事。我自己踩过几次坍塌的坑以后现在更相信这一点你自己亲手把online分支的梯度走得越顺、把EMA调得越稳你就越能理解BYOL为什么能用这么朴素的方式达到这么好的表现。如果你也想在工程里快速试一把自监督预训练完全可以从BYOL开始。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑