文章目录
- 三、模型
- 3.1 架构
- 3.2 双分支编码结构(Dual Encoders)
- 3.3 动量更新的 Key Encoder
- 3.4 字典队列(Dynamic Dictionary Queue)
- 3.5 对比损失(InfoNCE Loss)
- 四、技术分析
- 4.1 动量更新
- 4.2 双分支编码
- 4.3 队列结构
- 五、实验
- 5.1 主实验
- 5.2 消融实验
这是上一篇:【AI模型学习】Moco(上)——自监督学习和对比学习
三、模型
3.1 架构
感觉看懂代码就直接看懂Moco的流程了
for x in loader: # load a minibatch x with N samplesx_q, x_k = aug(x), aug(x) # apply two data augmentationq = encoder_q(x_q) # queries: NxCwith torch.no_grad():k = encoder_k(x_k) # keys: NxC# normalizeq = normalize(q, dim=1)k = normalize(k, dim=1)# compute logits# Einstein sum is more intuitive# positive logits: Nx1l_pos = einsum('nc,nc->n', [q, k]).unsqueeze(-1)# negative logits: NxKl_neg = einsum('nc,ck->nk', [q, queue.clone().detach()])# logits: Nx(1+K)logits = cat([l_pos, l_neg], dim=1)# apply temperaturelogits /= T# labels: positive key indicatorslabels = zeros(N, dtype=torch.long)loss = CrossEntropyLoss(logits, labels)# SGD update: query encoderloss.backward()update(encoder_q)# momentum update: key encoderupdate_momentum(encoder_k, encoder_q)# dequeue and enqueuedequeue_and_enqueue(queue, k)
一行一行详解(含 shape)
for x in loader:
- 从数据加载器中读入一个 mini-batch 的图像。
x.shape = (N, 3, H, W)
- e.g.
(256, 3, 224, 224)
- e.g.
N
是 batch size,H×W
是图像分辨率(224×224)
x_q, x_k = aug(x), aug(x)
-
对每一张图像做两次随机数据增强(crop、flip、color jitter…)
-
生成两个视图(view),分别作为 query 和 key。
x_q.shape = (N, 3, H, W) x_k.shape = (N, 3, H, W)
q = encoder_q(x_q)
-
主编码器处理 query 图像
-
输出:query 表征向量
q.shape = (N, C) # C 是输出特征维度,比如 128
with torch.no_grad(): k = encoder_k(x_k)
-
动量编码器处理 key 图像
-
no_grad()
:这部分不参与反向传播 -
输出:正样本 key 向量
k.shape = (N, C) # 同样是 128 维
q = normalize(q, dim=1)
k = normalize(k, dim=1)
-
所有向量都做 L2 归一化,让它们分布在单位球上
-
相当于把相似度计算变成 cosine similarity
||q[i]|| = ||k[i]|| = 1
l_pos = einsum('nc,nc->n', [q, k]).unsqueeze(-1)
-
计算每对
(q[i], k[i])
的正样本点积(余弦相似度) -
einsum('nc,nc->n')
表示:- 对每一对向量
q[i]
和k[i]
做内积 → 得到长度为N
的向量
- 对每一对向量
-
unsqueeze(-1)
→ shape 变成(N, 1)
l_pos.shape = (N, 1)
l_neg = einsum('nc,ck->nk', [q, queue.clone().detach()])
-
queue
是 shape 为(K, C)
的字典向量矩阵,存着旧的负样本 key(K = 65536) -
每个 query
q[i]
要和所有 queue 中的负样本k⁻[j]
做点积l_neg.shape = (N, K)
-
einsum('nc,ck->nk')
相当于q @ queue.T
logits = cat([l_pos, l_neg], dim=1)
-
把正样本 logit 和所有负样本 logit 拼在一起
logits.shape = (N, 1 + K)
每行代表一个样本和:
- 第 0 列:正样本相似度
- 第 1~K 列:负样本相似度
logits /= T
- 所有 logits 除以温度参数
T
(比如 0.07) - 控制 softmax 的 sharpness,温度越低 → 分布越尖锐
labels = zeros(N, dtype=torch.long)
- 因为正样本都在第 0 位 → 所以标签都是 0
labels.shape = (N,) labels = [0, 0, 0, ..., 0]
loss = CrossEntropyLoss(logits, labels)
- 计算对比损失:将 logits 看作
(N, 1+K)
的分类输出 - 每个样本目标是选出第 0 类(正样本)
loss.backward()
update(encoder_q)
- 正常反向传播 + 优化 query encoder 参数
encoder_k
不参与反向传播!
update_momentum(encoder_k, encoder_q)
-
用如下方式更新
encoder_k
的参数(动量更新):θ_k ← m * θ_k + (1 - m) * θ_q
-
m
是 momentum 值(通常取 0.999) -
保证 encoder_k 的输出更平滑、稳定
dequeue_and_enqueue(queue, k)
-
把当前 batch 的
k
插入 queue 尾部 -
从头部移除等量最旧的负样本
-
queue 是一个大小固定的 FIFO 队列
新 queue.shape = (K, C)
每一轮训练 MoCo 做了什么?
- 从图像生成两个视图 → 编码为
q
和k⁺
- 用当前 queue 中的所有历史 key 作为负样本
- 计算对比损失 InfoNCE
- 更新 encoder_q 参数
- 用动量更新 encoder_k
- 把新生成的
k⁺
加入 queue,移除最旧的 key
3.2 双分支编码结构(Dual Encoders)
功能目的:
将一张图像生成两个不同增强版本,分别输入两个网络编码器,用于构造对比对。
对比学习需要一对“相似”样本作为正样本,MoCo 采用两次数据增强来构造这一对。与 SimCLR 相同,但 MoCo 将两条路径分别送入不同 encoder,以便使用动量更新机制。
结构细节:
原图像 x ∈ [B, H, W, C]→ Data Augmentation₁ → x_q ∈ [B, H, W, C]→ Data Augmentation₂ → x_k ∈ [B, H, W, C]
分别送入两个 encoder:
q = f_q(x_q) ∈ [B, D] ← query encoder(主干)
k = f_k(x_k) ∈ [B, D] ← key encoder(动量更新)
两个 encoder 初始结构完全相同(如 ResNet-50),但参数更新方式不同。
3.3 动量更新的 Key Encoder
保持 key encoder 的稳定性,防止因梯度更新造成的特征漂移,从而保障 queue 中的 key 表征在跨 batch 时的一致性。
- 如果 query 和 key 用同一个 encoder(如 SimCLR),那 key 会随训练波动剧烈;
- 如果是 memory bank(如 InstDisc),则 key 会滞后于 encoder 的当前状态;
- MoCo 折中:key encoder 不反向传播,只做动量追踪。
θ k ← m ⋅ θ k + ( 1 − m ) ⋅ θ q \theta_k \leftarrow m \cdot \theta_k + (1 - m) \cdot \theta_q θk←m⋅θk+(1−m)⋅θq
- θ q \theta_q θq:query encoder 的参数;
- θ k \theta_k θk:key encoder 的参数;
- m m m:动量系数(默认 0.999);
- 更新在每个 step 后进行;
- 不占用额外显存或梯度。
3.4 字典队列(Dynamic Dictionary Queue)
构建一个可跨 batch、随训练动态更新的负样本池。实现小 batch 训练,大字典效果。
- SimCLR 的负样本来自当前 batch,batch 小 → 负样本少;
- InstDisc 用 memory bank,特征滞后;
- MoCo 提出使用 队列(queue)结构:
“先进先出”维护 key 表征,使得负样本库既稳定,又动态更新。
数据流结构:
Queue ∈ [K, D] ← 保存历史 k 的表示(如 65536 个)
每轮训练:- 当前 batch 的 k⁺ ∈ [B, D] 入队;- 最旧的 B 个样本出队。
- 与 encoder 解耦,不直接依赖 batch;
- 队列中的向量来自动量 encoder,具有一致性。
3.5 对比损失(InfoNCE Loss)
利用正负样本对,通过 softmax 分类方式进行判别性训练,让 query 特征学到语义空间。
- 标准的对比损失使用:
- 一个正样本( q q q, k + k^+ k+)
- 多个负样本( k − k^- k−);
- 将“是否为同一个图像增强”看成一个二分类任务(在一堆中找唯一正样本)。
数学定义:
L q = − log exp ( q ⋅ k + / τ ) exp ( q ⋅ k + / τ ) + ∑ k − exp ( q ⋅ k − / τ ) \mathcal{L}_q = -\log \frac{\exp(q \cdot k^+ / \tau)}{\exp(q \cdot k^+ / \tau) + \sum_{k^-} \exp(q \cdot k^- / \tau)} Lq=−logexp(q⋅k+/τ)+∑k−exp(q⋅k−/τ)exp(q⋅k+/τ)
- q q q:query 表征;
- k + k^+ k+:对应 key;
- { k − } \{k^-\} {k−}:从 queue 中取出;
- τ \tau τ:温度参数,调控分布陡峭度;
- 所有向量做 L2 normalize。
最终结构图示意(模块级):
┌────────────┐│ Image x │└────┬───────┘Aug1 │ Aug2│ ▼ ▼┌──────┐ ┌──────┐│ x_q │ │ x_k │└──┬───┘ └──┬───┘▼ ▼┌───────┐ ┌────────┐│ f_q │ │ f_k │ ← momentum updated└──┬────┘ └──┬─────┘▼ ▼q ∈ [B,D] k⁺ ∈ [B,D]│ │└───┐ ┌────┘▼ ▼InfoNCE Loss ◄──── Queue: k⁻ ∈ [K,D]↑└──── enqueue(k⁺), dequeue(oldest)
四、技术分析
4.1 动量更新
Memory Bank 的根本问题:向量更新不一致
在早期的对比学习方法中(如 InstDisc),Memory Bank 是一种常见策略:
- 为每个训练样本维护一个特征向量;
- 每轮训练后,用当前 encoder 输出更新这条向量;
- 用于为下一个 batch 构建负样本集合。
表面看起来:
- 每个样本都有一个最新表示;
- 不依赖 batch 同步,资源效率高;
- 易于实现大规模负样本对比。
但真正的问题在于 —— 特征表示更新是“异步”的
每一轮训练中:
- 当前 batch 中的样本,其 memory 中的表示会被立即更新;
- 而其他所有样本的向量保持不变(也就是说,是由旧 encoder 提取的);
- 当 encoder 发生剧烈更新时,这些“老旧的表示”可能与当前 encoder 的语义空间不一致。
于是,字典中的特征不再处于同一“表征空间”。
这很严重
对比学习的核心目标是:
在一个统一的特征空间中,拉近正样本、推远负样本。
但如果:
- 正样本是由最新 encoder 提取的;
- 而负样本来自几轮之前的 encoder;
- 那么我们实际上是在对齐两个不同时期、不同语义空间的向量。
这会导致:
- 训练目标不准确;
- 对比学习失效或训练极不稳定;
- 特别在 early training 阶段,encoder 学习变化剧烈时,影响尤为显著。
MoCo 的解决方案:
MoCo 使用 动量更新 encoder 取代了 Memory Bank 中的“样本表征”:
- 所有 key 表征由 同一个 encoder(key encoder)生成;
- 且该 encoder 是通过滑动平均方式更新(即动量);
- 所有的 key 来自 encoder 的同一“表征空间”或其缓慢演化版本;
- 因此可以保证:
- 语义空间连续;
- 不会有“key 表征落后 N 轮”的问题。
问题维度 | Memory Bank | MoCo Queue |
---|---|---|
表征来源 | 不同轮次 encoder | 同一动量 encoder |
特征表示一致性 | 不一致 | 连贯、稳定 |
表征更新方式 | 按样本、非同步更新 | batch 更新 queue,encoder 一致 |
对比空间稳定性 | 不同特征“时间错位” | 所有负样本在同一 encoder 空间中 |
导致的问题 | 训练不稳定,损失函数扭曲 | 可稳定训练,支持小 batch |
4.2 双分支编码
MoCo 将对比学习中的两张图像增强( x q x_q xq, x k x_k xk)分别输入两个结构相同但更新策略不同的 encoder:
编码器 | 名称 | 参数更新方式 | 用途 |
---|---|---|---|
f_q(x_q) | Query Encoder | 正常反向传播(BP) | 提取 query 表征 q q q |
f_k(x_k) | Key Encoder | 动量更新(Momentum) | 提取 key 表征 k k k |
这两个 encoder 是两个相互协作、功能分离的网络模块。
为什么要两个 encoder?
核心动机:解决 End-to-End 对比学习的不稳定性
对比:End-to-End 单 encoder(如 SimCLR)
特点 | 描述 |
---|---|
结构 | 所有图像(正/负)都输入同一个 encoder |
正负样本构造 | 来自 batch 的 2N 个图像,两两组合构造对比对 |
特征一致性 | encoder 每个 step 都在变动,特征可能不稳定 |
表征空间 | 每个 batch 的表示都在“漂移” |
训练稳定性依赖于 | 极大的 batch size(保证负样本质量) |
当 encoder 参数不断更新时,正样本和负样本的语义空间都在变,softmax 计算不准确,训练容易崩。
双 encoder
- 保持 key 表征稳定一致
- 支持队列机制(Queue-based Dictionary)
- 解耦表示学习与字典维护
- 小 batch 下性能依然强劲
MoCo 双分支结构 vs SimCLR 单分支结构:对比表
方面 | SimCLR(单 encoder) | MoCo(双 encoder) |
---|---|---|
编码器结构 | 同一个 encoder | Query encoder + Key encoder |
更新方式 | 每轮梯度下降同步更新 | Query 正常更新,Key 动量追踪 |
特征空间一致性 | 每轮变化剧烈 | 表征平稳过渡 |
负样本构造 | 当前 batch 内 | 历史 queue + 当前 batch |
训练稳定性 | 依赖大 batch | 小 batch 可用,收敛稳定 |
表征解耦 | 编码器要兼顾 query 和 key | f_q 负责学习,f_k 负责对比 |
双 encoder = 表征稳定性 + 训练灵活性 + 模块解耦 + 可扩展性
它是 MoCo 架构的第一性原理层级的设计突破,解决了 SimCLR 面临的核心结构性瓶颈。
4.3 队列结构
MoCo 中的 queue(字典)是用来存放历史 key 编码器生成的特征向量( k k k),用于构建负样本池。
每个 query 的正样本来自当前 batch
每个 query 的负样本来自字典队列(由历史 batch 累积构成)
MoCo 中,队列在每个训练 step 中都会更新一次:
-
用 key encoder 计算当前 batch 的 key 表征 k + k^+ k+:
k^+ ∈ [B, D]
-
将这 B 个 key 入队(enqueue):
queue.append(k⁺)
-
将最旧的 B 个 key 出队(dequeue):
queue.pop(B)
保证队列长度始终为固定的 K K K,控制内存与负样本数量平衡。
MoCo 队列机制的本质意义
MoCo 的字典不是“为每个样本建一个空间”,而是“持续构造一个可控、高质量、大规模的负样本集合”。
它结合了:
- Memory Bank 的“跨 batch 表征能力”
- SimCLR 的“新鲜样本”机制
- Momentum Encoder 的“语义一致性”
是 MoCo 成为小 batch 对比学习之王的基础。
五、实验
5.1 主实验
任务 | 实验设置 | 方法比较 | 评价指标 | 结果表现 | 论文位置 |
---|---|---|---|---|---|
ImageNet 线性评估 | MoCo 预训练 + Linear probe | 比较有监督、MoCo、InstDisc、SimCLR | Top-1 Acc (%) | MoCo ResNet-50 达到 60.6%(比 SimCLR 好) | Sec. 4.1 / Table 1 |
小 batch 能力 | batch size = 256(SimCLR 无法运行) | MoCo vs SimCLR | Top-1 Acc (%) | MoCo 不依赖大 batch,性能优异 | Sec. 4.1 |
Downstream 迁移(目标检测) | 用 MoCo 表征初始化 Faster-RCNN | 与有监督预训练比较 | box AP / mask AP | MoCo 略优于有监督(如 38.5 vs 38.2) | Sec. 4.3 / Table 3 |
小数据场景(VOC / COCO) | 微调 MoCo 表征在小数据集 | 与有监督比较 | mAP | MoCo 表征迁移能力更强 | Sec. 4.3 |
5.2 消融实验
实验因素 | 设置对比 | Top-1 Accuracy / 结果趋势 | 实验结论 | 论文位置 / 图表 |
---|---|---|---|---|
是否使用动量 encoder | 使用动量更新 vs 不使用(共享 encoder 参数) | 不用动量:性能严重退化 | 动量更新极大提升表征一致性,是 MoCo 成功的核心结构 | Fig. 5(a), Sec 4.2 |
动量系数 m m m 的大小 | m = 0.5 m = 0.5 m=0.5, 0.9 0.9 0.9, 0.99 0.99 0.99, 0.999 0.999 0.999(默认) | m = 0.999 m = 0.999 m=0.999 时性能最优 | 较大的动量使得 encoder 更新更平滑,特征更稳定 | Fig. 5(b) |
Queue 队列长度 | 4K, 16K, 65K(默认) | 越大性能越好,65K 最优 | 更大的 queue 能提供更多负样本,有效增强判别能力 | Fig. 5© |
是否使用 queue | 使用 queue vs 只用当前 batch | 不使用 queue 性能急剧下降 | queue 是实现跨 batch 对比的核心机制 | Fig. 5(a), 隐含讨论 |
Batch size 大小变化 | 256(MoCo 默认) vs SimCLR 4096 | MoCo 在小 batch 下依然表现良好 | MoCo 不依赖大 batch,工程更友好 | Sec. 4.1 |
Linear probe vs Finetune | Downstream 任务中对 MoCo 表征进行线性分类或微调 | 两者都表现优异 | MoCo 表征迁移性强,支持各种下游任务 | Sec. 4.3 / Table 3 |
预训练 vs 随机初始化 | 用 MoCo 表征初始化目标检测模型 vs 随机权重 | 明显优于随机初始化 | MoCo 表征泛化性强,具备替代有监督 pretrain 的潜力 | Sec. 4.3 |