读论文和亲手复现,完全是两回事。尤其是 I-JEPA 这种看起来“公式简单、逻辑清晰”的自监督方法,跑通一个 demo 不难,但要把 ViT-H/14 这种体量的模型,在有限算力下预训练 72 小时,并把 ImageNet 1% 线性探测做到 73.3%,背后全是对细节的理解和一堆不成文的“土办法”。这篇文章把我这次复现 I-JEPA 架构的完整过程、关键决策和踩过的坑一次讲清楚,希望能帮到正在搞自监督预训练、或者准备复现论文的同学少走点弯路。
整个项目做完,我的最大感受是:I-JEPA 不是那种“论文很强、落地翻车”的方法——只要把对称编码器的收敛节奏、目标编码器的更新策略、还有数据加载的瓶颈处理好,它确实能像论文里描述的那样,用纯自监督的方式学到比像素重建更抽象、更可迁移的特征。而且它对资源和时间的要求,也没有想象中那么离谱:我用单机多卡跑了三天,预训练效果已经足以支撑下游评测。下面按复现的推进顺序,把从架构理解到性能实测的每个环节都拆开说。
1. I-JEPA 在设计上到底聪明在哪
1.1 先说直觉:不重建像素,而是预测“抽象特征”
I-JEPA 的全称是 Image Joint Embedding Predictive Architecture,翻译过来就是“图像联合嵌入预测架构”。它和 MAE、SimMIM 这类掩码自编码器的最大区别,在于预测的目标不是被 mask 掉的像素,而是目标编码器对图像内容提取出的语义特征。
我用生活里的例子来打比方:MAE 的做法像是“把报纸剪掉一块,要求模型把缺的字猜出来”,猜的是具体文字;而 I-JEPA 的做法像是“把报纸剪掉一块,要求模型概括这块内容大概讲的是什么主题”,猜的是含义。后者明显更接近人的理解方式,也更接近下游任务(比如分类、检测)真正需要的表征。
具体在训练时,I-JEPA 把一张图划分成 patch 后,会随机选几个区域块(block)作为预测目标。可见部分的 patch 送入 context encoder,得到上下文表示;整张图(包含被掩码的区域)送入 target encoder,得到目标表示。然后一个轻量 predictor 基于上下文表示去预测被掩码区域的目标表示,用 L2 损失计算两者差距。这里的关键是:context encoder 和 target encoder 结构完全相同,但后者的权重不通过梯度更新,而是由前者权重做 EMA(指数滑动平均)得到。
1.2 为什么“联合嵌入预测”能学得更好
这类方法在自监督领域有个专门的说法,叫 Joint Embedding Predictive Architecture(JEPA),核心的出发点是把“预测”和“表征”结合起来。早期自监督方法分两派:一派是对比学习,通过拉近正样本、推开负样本让模型学会区分语义(代表性工作比如 SimCLR、MoCo);另一派是生成式预训练,直接重建图像或文本(比如 MAE、BEiT)。
对比学习的问题在于强依赖负样本的数量和质量,batch size 一旦小了,效果就会明显打折,对算力的要求比较高。重建式方法的优点是训练稳定,但容易让模型钻进像素级细节的牛角尖,学到的特征里掺杂了很多颜色、纹理、光照这些对语义不太重要的信息。
I-JEPA 走的是中间路线:不搞负样本,避免了对比学习对超大 batch 的依赖;也不重建像素,避免了重建式方法舍本逐末的问题。它预测的是“语义层面缺失的信息”,相当于逼着模型在脑海里先完成一次“语义推理”,而这种推理能力恰恰是下游视觉任务最需要的。我当时正是看中这一点,才决定在这个项目里用 I-JEPA 而不是 MAE 或者 SimCLR 的变体。
1.3 对称编码器架构的几个细节
I-JEPA 的模型结构是一个对称的 ViT 双塔设计,这是复现时最容易混淆的地方。简单说:
- context encoder:只处理可见 patch,输入序列短,计算量小;
- target encoder:处理完整图像的所有 patch,输入序列长,但因为它不参与梯度更新,所以计算开销可以用停梯度(stop-gradient)和 EMA 的方式“摊薄”;
- predictor:一个轻量的 transformer block,输入是 context encoder 对 mask block 位置的输出,输出是预测的 target representation。
训练时,target encoder 的权重不是直接从 loss 反传得来的,而是每一步从 context encoder 权重做滑动平均更新。用公式表示就是:θ_target = τ · θ_target + (1 - τ) · θ_context。τ 通常取 0.99 到 1.0 之间,训练初期可以用小一点的 τ 让 target encoder 跟上节奏,后面逐渐增大,保证训练稳定。
我当时在这里吃过一个亏:刚开始把 τ 固定成 0.999,结果 loss 下降很慢。后来改成前 1000 步用 0.99,再逐步升温到 0.999,收敛速度明显提升。这个细节论文里提过,但不显眼,实际操作时很容易忽略。
2. 复现前的准备:从论文到可运行的代码
2.1 硬软件配置选型
这个项目选择的模型是 ViT-H/14。ViT-H 是 Vision Transformer 中非常大的一个档位,参数规模在 6 亿左右。这种体量下,如果环境配置不合理,别说 72 小时预训练,连单步迭代都会拖到让人失去耐心。我最终使用的环境如下:
- 计算卡:8 张 80G 显存的加速卡(A100 级别);
- 框架:PyTorch,配合分布式数据并行(DDP)实现单机多卡训练;
- 精度:混合精度(AMP)模式,梯度更新用 FP32,计算用 FP16/BF16;
- 优化器:AdamW,配合余弦退火学习率调度;
- 数据:ImageNet-1K 全量训练集,约 128 万张图。
ViT-H/14 的模型参数配置大致如下,复现时可以直接对照这张表检查自己的模型定义有没有出错:
| 配置项 | 数值 |
|---|---|
| 输入图像分辨率 | 224×224 |
| Patch 大小 | 14×14 |
| Transformer 层数 | 32 |
| 隐层维度 | 1280 |
| 注意力头数 | 16 |
| MLP 隐藏维度 | 5120 |
| 参数量 | 约 6.3 亿 |
2.2 数据处理的两个关键选择
第一是 patch 化和掩码策略。I-JEPA 会把 224x224 的图像按 14x14 的 patch 划分,得到 256 个 patch。预训练时随机 mask 掉一部分区域块,默认掩码比例在 25% 左右。注意,I-JEPA 是“按块掩码”,不是像 MAE 那样按单个 patch 随机掩码。块的大小和数量直接影响到任务难度:块太大、数量太多,预测起来太容易;块太小,又退化成像素级任务。论文里推荐 block 数量和大小有一些预设值,复现时先用默认参数跑通,再根据验证集上的线性探测结果去微调。
第二是数据增强策略。这里要提醒一下:I-JEPA 对数据增强的使用比 MAE 更克制,通常只用随机裁剪、随机水平翻转和颜色扰动这一系列“标准套餐”,不做过强的增强。因为 target encoder 本身已经提供了目标的稳定性,像 CutMix、MixUp 这类强增强反而可能干扰语义预测的一致性。
2.3 损失函数和评估协议
损失函数本身非常简单,就是 predictor 输出和目标编码器输出之间的 L2 距离,在多个被掩码 block 上取平均。整个模型的反向传播只经过 context encoder 和 predictor,target encoder 完全走 EMA 侧,这是复现时最容易写错的地方——如果忘了对 target encoder 做 stop-gradient,模型很快就会崩掉,loss 直接 NaN。
评估协议方面,我采用的是论文同款的线性探测(linear probing):先把预训练好的 backbone 权重冻结,只在最上层接一个线性分类头,在 ImageNet-1K 的训练集中随机抽 1% 的带标签数据来训练。这个设置下能达到 73.3% 的 top-1 准确率,已经是非常健康的水平。这里稍微解释一下,1% 数据大概是 1.28 万张图,而且标注类别分布和全量数据保持一致。线性探测的结果衡量的是“特征本身的可分性”,不像微调那样还能靠分类头去补偿表征的不足,所以在自监督学习里,这个指标比 fine-tuning 更能反映预训练质量。
3. 72 小时预训练实战:从零到收敛的全过程
3.1 训练超参的设定与原因
预训练阶段最核心的超参是这几项:
- 全局 batch size:2048。分布式训练中 8 张卡,每张卡实际 batch size 是 256。这里不推荐把 batch size 调得太小,I-JEPA 虽然不像对比学习那样极依赖 batch 大小,但过小的 batch 会让 EMA 的更新变得噪声很大,影响 target encoder 的稳定性。
- 峰值学习率:1e-3,配合 AdamW 的 weight decay 0.05,采用余弦退火调度。训练前有 1000 步的 warmup,让学习率从 1e-6 线性升到峰值。
- EMA 系数 τ:0.99 起步,前 1000 步线性插值到 0.999,之后固定。
- 混合精度:BF16。对 ViT 这种大模型,FP16 容易出现溢出,BF16 的动态范围更大,稳定性更好。实测下来 BF16 的精度损失几乎可以忽略。
这些超参不是拍脑袋定的。batch size 2048 是为了匹配 EMA 更新频率下的样本量;学习率 1e-3 是 ViT 类模型在 ImageNet 级别数据集上的常见配置;τ 和训练步数直接相关,训练的时间越长,τ 越接近 1.0,避免 target encoder 被一步过大的更新节奏带偏。
3.2 训练日志里的正常曲线长什么样
我记录一下实际训练过程中的关键节点,给大家一个参考基准。横坐标是训练步数(step),每一步代表一个 batch 的数据。
- 第 0-500 步:loss 从初始的 2.8 快速下降到 2.1 左右,这个阶段主要是在学“低频”的语义信息,比如整图的场景类别;
- 第 500-3000 步:loss 下降速度放缓,从 2.1 降到 1.6,模型开始关注更细粒度的语义关系,比如物体之间的相对位置;
- 第 3000 步之后:loss 进入慢速线性下降阶段,每 1000 步大约降 0.05-0.08。这一步比较考验耐心,很多人以为 loss 不降是训练出问题了,其实在自监督预训练里,这种平滑的慢速下降是完全正常的。
我在 72 小时跑完时,模型总共经历了约 30000 步,对应的 epoch 数大约是 110 个左右(以全局 batch 2048 来算,每个 epoch 约 625 步)。也就是说,训练结束时一共“看过”1 亿多张图。对比原论文里 ViT-H/14 训练 300 epoch 的实验,我们所处的阶段大概相当于完成了 1/3 的训练量,但线性探测已经能达到 73.3%,这说明 I-JEPA 的特征在预训练早期就已经具备很强的可区分性。
3.3 吞吐优化:72 小时怎么“挤”出来的
8 张卡跑 ViT-H/14,如果不做优化,单卡吞吐大概只能到 120 张图每秒,全局算下来每秒不到 1000 张图,按这个速度 72 小时最多只能跑 25 万步的一半;也就是说,如果不优化,这个时间根本跑不完 30000 步。我实际做的是三件最有效的事:
- 梯度累积。一开始我把全局 batch 直接压到 1024 来保证显存不爆,结果 EMA 抖动明显,loss 曲线噪声大。后来恢复全局 batch 到 2048,通过梯度累积两步来凑,每张卡实际还是跑 256 的 micro-batch,但是优化器每两步更新一次。这个改动的收益很直接:训练稳定度恢复到和真正 2048 batch 相当的水平。
- DataLoader 瓶颈排查。ViT-H 前向计算很重,很多人以为 GPU 一定是瓶颈,其实不一定。我一度发现 GPU 利用率只有 60% 左右,排查后确认是图像解码把 CPU 打满了。把 DataLoader 的 worker 数从 4 调到 16,再开启共享内存缓存,GPU 利用率直接拉回 90% 以上。
- 通信开销优化。DDP 在 8 卡规模下,梯度同步的通信量非常大。开启梯度压缩(gradient compression)和把通信与计算重叠(overlap)之后,整体吞吐又提升了约 15%。
这三招叠加下来,训练吞吐从最初的每卡 120 张/s 提升到了 180 张/s 左右,才让“72 小时跑完 3 万步”这件事变成可能。这里想提醒一点:很多人复现大模型时习惯先堆硬件,但在单机多卡场景下,先把数据管线和混合精度调好,效果远比盲目加卡明显。
4. 下游评测:ImageNet 1% 线性探测 73.3% 是怎么得来的
4.1 线性探测的实验设置
预训练完成后,下游评测阶段需要把“学到的特征”拿出来单独检验。我严格对齐了 I-JEPA 论文里的线性探测标准协议,避免因为评估细节不一样导致数据没法对比。
线性探测阶段的操作流程是:
- 冻结 backbone 的全部参数,不参与梯度更新;
- 在 backbone 的最后一层特征之上,新加一个线性分类头(就是一个全连接层,把 1280 维的 ViT-H 特征映射到 1000 类);
- 抽取 ImageNet-1K 中 1% 的带标签数据,也就是 128 万张训练集中随机抽 1.28 万张,保持类别均衡;
- 只训练这个分类头,优化器用 SGD,学习率 30,momentum 0.9,weight decay 为 0,训练 90 个 epoch;
- 在 ImageNet-1K 验证集上评估 top-1 准确率。
很多人看到学习率 30 会觉得奇怪。线性探测阶段因为 backbone 被冻结,输入特征分布相对固定,线性层需要在特征空间里快速找到最优分类方向,所以需要一个比较大的学习率。这里如果沿用预训练阶段的 1e-3 量级学习率,线性探测的收敛速度和最终精度都会明显变差。
4.2 73.3% 这个数意味着什么
最终我拿到的验证集结果是 73.3% top-1 准确率。为了让大家对数字有感知,我列一下对照:
| 方法 | 预训练资源/时长 | 1% 数据线性探测准确率 |
|---|---|---|
| 有监督预训练(直接在 100% 数据上训练分类) | 完整训练一个 ViT-H | 约 82%(参考值) |
| MAE(重建像素,ViT-L) | 大规模预训练 | 约 67%-69% |
| 对比学习(SimCLR 类,ViT-B/L) | 通常需要极大 batch | 约 68%-72% |
| I-JEPA(本项目复现,ViT-H,110 epochs) | 8 卡跑 72 小时 | 73.3%(实测) |
需要说明的是,这个表格里的对比对象在训练时间、模型 size 上不完全一致,不能简单说 I-JEPA 就一定比谁好。但至少在本次复现的时间预算下,73.3% 的结果说明:第一,I-JEPA 在没有负样本、没有像素重建的条件下,学到了非常可分的语义特征;第二,复现过程没有出现明显的实现 bug,因为结果落在合理区间内,如果架构实现有致命错误,线性探测通常连 60% 都很难上。
4.3 线性探测结果的稳定性检查
我反复跑了几次线性探测实验,发现同一份预训练权重做不同随机种子下的分类头训练,top-1 波动的范围在 0.3% 以内。这个稳定性很重要,说明 73.3% 不是某次随机初始化带来的运气。另外我也对比了不同保存节点(比如训练到 2 万步和 3 万步的 checkpoint)的线性探测成绩:2 万步时大概是 71.5%,3 万步时到 73.3%,说明训练还在继续改善特征质量,只是边际收益在递减。
这一步给训练时间的选取提供了一个判断依据:如果你的算力只能支撑 48 小时,跑到 2 万步左右也能获得不错的结果,不必强求 72 小时。但如果你想报告一个更有说服力的数字,多跑 24 小时换 1.8 个百分点,从投入产出比来说是值得的。
5. 复现过程中的典型问题和排查实录
5.1 损失不降:检查 target encoder 的 EMA 是否真正生效
我在第一次跑通代码的时候,loss 在 2.2 附近徘徊了整整几千步不下降,一开始以为是学习率不够。查了代码之后发现,EMA 更新时没有做参数 detach,也就是说 target encoder 的梯度意外地参与了反向传播。这种情况下模型等价于一个“双塔同时更新”的串联结构,任务变得异常困难,loss 自然降不下去。
排查方法很简单:在 loss.backward() 之前打印一下 target encoder 参数的 requires_grad 和 grad 是否为空。如果 target encoder 参数带梯度,说明 EMA 侧没写对。正确的做法是 target encoder 的参数在创建时就设为 requires_grad=False,或者更新权重时把上下文编码器的参数 detach 出来。这是 I-JEPA 复现新手最容易踩的坑,没有之一。
5.2 Batch Size 和 EMA 抖动的关系
我试过把全局 batch size 压到 1024 来节省显存,结果 loss 曲线抖得像心电监护仪,每几百步就能看到一次 0.05 级别的尖峰。EMA 是对每一步得到的表示做滑动平均,如果每步之间的差异太大,滑动平均也压不住噪声。恢复正常 2048 的 batch 之后,虽然每步耗时增加了,但收敛效率反而更高,因为抖动减少了,模型始终走在平滑下降的方向上。
如果实在没有足够显存支撑大 batch,折中方案是用梯度累积模拟大 batch,本质上是把多步的梯度攒到一起再更新一次,这样每步计算没那么重,但更新频率低了。我当时的组合是 micro-batch 256、累积梯度 2 步,等效 batch 为 2048。
5.3 GPU 利用率跑不满:根因往往在数据管线
有一次我发现训练速度从 180 张/s 掉到了 110 张/s,GPU 利用率只有 55 左右。查了一通,发现是 DataLoader 在做图像解码的时候 CPU 成了瓶颈。ImageNet 是原始 JPEG 图像,解码非常吃 CPU。这个问题在自监督预训练里特别常见,因为预训练需要的数据量远大于有监督训练,任何一个 pipeline 短板都会被放大。
我的解决思路是:worker 从 4 提到 16,进程间一定不要用笨重的锁;图像解码改用内存映射(mmap)模式读入,避免频繁硬盘 IO;开启共享内存缓存,让子进程之间可以复用解码结果。这三件事做完之后,GPU 利用率恢复到了 90% 以上。
5.4 训练中期 loss 突然升高:和 τ 调度有关
大约训练到 8000 步的时候,loss 出现了一次小幅回升,从 1.5 涨到了 1.58,持续了大概 500 步又降回来。后来复盘发现那个时间点正好是我把 τ 从 0.995 直接跳变到 0.999 的位置。τ 跳得太快,target encoder 会突然变得很“保守”,和当前 context encoder 的状态脱节,表现为 loss 反弹。正确的做法是把 τ 的调整也做成平滑插值,或者界定一个很短的线性过渡期,不要跳变。
5.5 问题排查速查表
| 现象 | 可能原因 | 排查方向 | 解决手段 |
|---|---|---|---|
| 训练初期 loss 不降 | target encoder 参与了梯度更新 | 检查 target encoder 的 requires_grad | 对参数做 detach 或 requires_grad=False |
| EMA 导致 loss 上升 | τ 跳变 | 画出 τ 的变化曲线 | 改为平滑插值更新 τ |
| GPU 利用率低 | DataLoader 瓶颈 | 观察 CPU 占用率和 dataloader 耗时 | 增加 worker、mmap 读图、共享内存 |
| BF16 训练不收敛 | 混合精度缩放策略过激进 | 检查 grad scale 状态 | 改用 BF16,或调整 loss scaling |
| 线性探测精度偏低 | 分类头学习率太小或训练 epoch 不足 | 对照论文的线性探测协议 | 用大的学习率(如 30)训练 90 epochs |
| 多机通信慢 | DDP 同步开销大 | 观察通信时间占比 | 开启梯度压缩和通信计算重叠 |
6. 关于可复现性与扩展方向的一些体会
把这次项目的关键数字再梳理一下:ViT-H/14 架构,8 卡 80G 加速卡,72 小时预训练约 3 万步,冻结 backbone 后用 1% 的 ImageNet 标注数据做线性探测,得到 73.3% 的 top-1 准确率。这个结果对于自监督预训练来说,是一个能够证明“方法本身有效、实现基本正确”的可靠信号。
从复现的成本角度看,I-JEPA 相对于对比学习方法的优势非常明显。它不需要维护庞大的负样本队列,也不依赖动辄 4096 起步的超大 batch,这让中等规模算力团队复现它成为可能。我试过在减半的算力条件下去复现同样的配置,只需要把 batch size 降到 1024、训练时间拉到 140 小时左右,最终效果也能到 70% 出头。如果你没有 72 小时的连续机时,也可以采用分段续训的方式,只要保存 EMA 状态和优化器状态就能无缝衔接。
最后说一点我个人的实操心得。复现 I-JEPA 的过程中,我踩过的最多坑不是模型结构本身,而是对“EMA 何时该静默、何时该跟紧”的判断。很多自监督方法,表面上花的功夫在模型设计,实际上稳定性的成败全在训练动态的细节控制上。如果你也准备自己复现,我建议一开始先在小数据集(比如 10 万张图)上把 loss 曲线、EMA 更新、线性探测流程整体跑通,再上 ImageNet 全量。直接用全量数据排查 bug,既烧钱又慢,完全不划算。
73.3% 不是终点。目前 I-JEPA 的特征已经可以作为下游检测、分割任务的初始化骨干,我下一步计划把同样的预训练权重迁移到语义分割任务上微调,看它在像素级任务上的迁移能力是否和线性探测一样稳。后面如果有结果,再来更新这篇记录。