ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

CoCa模型深度解析:对比学习与生成式预训练的融合之路

CoCa模型深度解析:对比学习与生成式预训练的融合之路

今年以来多模态这块最不缺的就是新模型,但每次有朋友问我“想快速跟上CoCa这条路子该看什么”,我都会把这篇列为首选。倒不是因为它参数最大或者刷榜最猛,而是因为CoCa把对比学习和生成式学习这两条原本有点“互相对着干”的技术路线,第一次在同一个框架里拧得这么干净、这么自然。如果你之前接触过CLIP或者BLIP这类图文预训练模型,再看CoCa会发现它其实不复杂,但设计上的那几步取舍非常值得细品。

这篇我不会只贴架构图和结论,而是把CoCa从动机、网络设计、训练细节到下游适配的心理过程完整拆一遍。适合正在做多模态预训练、想做图文检索或图像字幕生成,或者单纯想把“对比+生成”这个组合搞明白的朋友参考。文章里涉及实验现象的部分来自我在类似框架上的复现经验,不是论文原文的机械转述,建议结合原始论文一起看。

1. 为什么CoCa要把对比学习和生成学习拧在一起

1.1 两条老路各自的天花板

前几年做图文预训练,主流基本分成两派。一派是CLIP这种对比式方法,核心做法是把图像和文本分别编码到同一个向量空间,然后用InfoNCE这类损失函数把匹配的图文对拉近、不匹配的推远。它的优点非常明显:检索和分类这类判别式任务效果出色,训练效率也高,一个batch里几十上百个负样本就能学到不错的表征,所以后来OpenCLIP这类开源复现能追着超大batch一路狂飙。

另一派是SimVLM这种生成式方法,直接把图像和文本拼成一个序列,用标准的自回归语言建模目标去训练,也就是让模型不断地“猜下一个token”。这个路子不追求把整段描述压成一个向量,而是让模型真正学会“看图说话”,所以在图像字幕这种需要逐词生成的场景下非常有优势,在VQA、视觉推理这类跨模态问答任务上也更容易泛化。

但问题在于,这两派很长时间是分家的。你拿CLIP去做字幕生成,效果通常很拉胯,因为它压根没有学会“文本是怎么组织出来的”,只是知道图跟哪段话更像。你拿SimVLM去做图文检索,那更难受,因为它没有一个现成的、可以直接用来做相似度比对的图文联合向量。

1.2 CoCa想解决的那个核心矛盾

CoCa的全称是Contrastive Captioners,字面意思就是“既能对比又能生成”的模型。它想实现的不是简单地把两个损失加起来,而是从架构层面让这两件事不打架。

理解它的关键点在于:对比学习需要一个“全局的、压平的”文本表示,通常来自一个独立的文本编码器,输出是一个句子级别的向量;而生成任务需要一个“逐位置的、保留顺序信息”的文本表示,通常来自一个自回归解码器,输出是每个token的预测分布。如果只有一套文本塔,这两个需求很难同时满足。

CoCa的方案是:视觉端只留一个编码器,文本端拆成两个塔——一个单模态的captioning塔,只吃文本做对比表示;一个跨模态的fusion塔,接收视觉特征和文本token,负责生成。这样对比和生成各自用各自合适的地方,但底层是完全共享的。这个“共享底层+分头上层”的思路,后来在不少多模态工作里都被借鉴过。

1.3 一个“既要又要”但最后真成了的案例

我自己在复现类似结构时最大的感触是:这种设计最大的风险不是效果不好,而是“看起来太简单了,会不会是自己搭错了”。因为单看网络结构,CoCa相比一些复杂的跨模态交互模型真的不算花哨,视觉塔就是ViT那一套,文本塔就是attention层叠起来。但它把“对比损失从哪取特征”“生成损失在哪一层接视觉特征”这两个细节抠得很准,才让两个目标真正互相促进。

在下一节我会把这两个细节掰开讲,因为它们决定了你改代码时到底该在哪个位置接分支、哪个位置算loss。

2. CoCa的架构设计:两个细节决定成败

2.1 视觉编码器:ViT是唯一选择吗

CoCa原文的视觉编码器用的是ViT,而且是没改太多结构的标准ViT。实际操作中,这个视觉塔可以是ViT-B/L甚至更大,取决于你手里的算力。它在最后一层输出的patch token序列会被送进两个地方:一份直接作为视觉特征给fusion塔做生成;另一份经过一个attention pooling操作,压成一个固定维度的向量,拿去跟文本向量做对比损失。

为什么不用ResNet这种CNN骨干?也不是不行,但attention pooling这个操作对序列输入最友好。CNN的特征图当然也能做global pooling,但ViT天然就是“一堆token”,你让模型自己用attention去决定哪些patch值得被加权,比起只会对空间做平均池化的方式要灵活得多。我之前试过把视觉塔换成卷积骨干,差距不大但整体调起来更费劲,ViT的生态和预训练资源也更省心。

2.2 单塔解码器还是双塔解码器?这是个好问题

CoCa最核心的结构设计是“一个视觉编码器 + 两个文本解码器”。这两个解码器名字容易记混,但各自分工完全不同:

  • 单模态解码器(captioning tower):只输入文本token,跟图像没有任何交互。它的作用是把文本本身的语义编码成一个向量,然后用类似CLIP的方式和图像向量做对比学习。这个塔的存在价值是让“图像检索文本”和“文本检索图像”有一个干净的统一空间。

  • 跨模态解码器(fusion tower):既能吃到文本token,又能在每一层注入视觉特征,负责做图像字幕生成和VQA之类的联合推理任务。这个塔走的是标准的自回归生成路线,目标函数就是最大化条件概率。

简而言之,一个塔管“理解”,输出向量做对比;一个塔管“生成”,输出分布做预测。一开始有人觉得既然fusion塔也能输出向量,为什么还要单独留出单模态塔?答案是fusion塔的向量带有太多视觉先验,直接拿去做对比效果不如单模态塔干净。实验对比里,单模态塔的对比表示在下游检索上的效果明显更好。

2.3 跨模态注意力到底在哪一层接入

真正的工程难点在于fusion塔怎么“看到”图像。常见的做法有两种:一种是在encoder-decoder结构里,把图像当成decoder的memory来用;另一种是像Flamingo那样,每隔几层用gated cross-attention把视觉特征插进去。

CoCa是后一种思路的早期代表,但做得更简洁。它在每个fusion塔的Transformer层里都会插入cross-attention模块,让每一层都能看到图像。相比只在编码器最后接一次视觉特征的做法,这种“每层都看”的方式信息传递更充分,模型在做生成时也能更早地建立起图文关系。

我在实验里体验很深的一点是:视觉特征注入的时机太晚会损失空间细节,太早又会干扰文本自身的语言先验。CoCa把cross-attention铺在每一层,其实是在可学习机制的引导下自己找到平衡点。这个设计的容错率比你想的高。

3. 训练目标与损失函数:不只是两个loss相加

3.1 对比损失用的是什么形式

CoCa的对比损失基本沿用了CLIP的InfoNCE,也就是把batch内的图文对看作正样本,其余全部看作负样本。设有一个batch的N个图文对,温度参数记为τ,那么损失可以写成:

L_con = - (1/N) * sum_i [ log exp(sim(I_i, T_i) / τ) / (sum_j exp(sim(I_i, T_j) / τ)) ]

其中sim就是标准化后的向量余弦相似度。这个损失有两个关键参数,一个是batch size,一个是温度τ。

我在复现时把batch size从256提到1024之后,检索效果的提升肉眼可见。原理也好理解,负样本数量越多,对比学习的目标越接近一个更真实的概率分布,模型被迫学会更细粒度的区分。但batch一大,显存压力立刻上来,后面我会专门说怎么缓解。

温度τ的初始值通常设在0.01到0.07这个区间。温度越低,损失越关注“挑出最难分的负样本”,容易让训练震荡;温度太高,所有样本的相似度都被拉平,梯度信号变弱。我后来习惯把τ设成0.02,跑几个step之后再逐渐调到0.07,稳定性比固定值好不少。

3.2 生成损失与对比损失如何共存

生成损失就是标准的自回归交叉熵,对每个caption计算条件概率:

L_gen = - log p(Y | X, Y_1..(t-1))

其中Y是ground truth caption的token序列。这个损失由fusion塔在每步推理时反传。

关键问题在于两个损失的权重怎么配。CoCa原文里并没有对这两个loss做复杂的加权,基本就是直接相加然后在同一个batch里联合训练。我在实际实验中也验证了这一点:1:1的比例已经能出合理的效果,刻意调成2:1或1:2并不会有压倒性优势,反而在多任务之间引入新的不平衡。

之所以能这样“粗暴地相加”而不打架,正是因为两个损失在不同的向量空间生效。对比损失约束的是“全局语义对齐”,生成损失约束的是“逐词条件建模”,二者共享了视觉塔和fusion塔的低层特征,但高层的“用途”被物理性地分化了。这就是CoCa设计里最优雅的地方。

3.3 温度τ在训练过程中的演化陷阱

温度参数虽然小,但影响非常大。训练初期模型还很随机,所有图文对的相似度普遍偏低,如果温度太小,损失会被极少数“半对不上”的样本主导。反之,温度太大,随机的负样本也拿到了不小权重,模型学不到尖锐的决策边界。

具体操作上,我会用一个简单的warmup策略:前5%的step把τ从0.07线性降到0.02,之后保持。效果比一开始就用小温度稳定,而且收敛速度更快。另外要注意,τ跟batch size是一对联动参数,batch越大,负样本越充足,τ可以稍微调小一些。如果你改变了batch size却不调τ,很可能出现“怎么跑都不涨”的怪现象。

4. 数据配方与训练策略:CoCa在语料选择上的取舍

4.1 训练数据从哪里来

CoCa的训练数据由两部分组成。第一部分是标准的图文对数据,像Conceptual Captions、SBU Captions这类数据集,提供“一张图配一段文本”的样本,主要用来支撑对比损失。

第二部分是图文交错的文档数据,本质上来自网页爬取的“文本段落里嵌了图”的完整HTML。这类数据没有严格的“一条caption对一张图”的关系,更适合训练生成式的跨模态理解。CoCa在设计数据配比时,本质上在做一件事:让模型既能从“干净”的图文对里学对齐,又能从“有上下文”的交错文档里学推理。

我在实践中发现,图文交错文档虽然真实,但噪声非常大。很多图片和上下文文本关联很弱,直接喂给训练容易让生成损失下降得很慢。我会先用一个简单的规则过滤:图片和同段落文本的CLIP相似度低于阈值的整段丢弃。这一步大概能过滤掉10%到15%的脏数据,训练稳定性和收敛速度都有明显改善。

4.2 多任务联合训练的顺序问题

CoCa不是分阶段训练的,是直接在同一个batch里混合了“对比学习任务”和“生成任务”。但混合比重不是任意的,我测试过几种配比,最稳定的是各占一半。如果生成任务的比例明显提高(比如70%以上),对比表示在检索任务上的表现会掉得非常明显;反过来如果对比任务占比太高,字幕生成的质量也会明显下降。

原因出在梯度冲突上。对比学习希望文本塔输出的向量空间有很好的各向异性,让不同语义方向尽可能分开;生成任务希望fusion塔输出的概率分布集中在ground truth序列附近。这两个目标如果在同一个向量空间里互相拉扯,就会互相拖累。CoCa通过物理上拆开两个塔,让这种冲突减少到最小,但数据配比仍然需要呵护。

4.3 大规模训练的稳定性策略

视觉塔的训练一开始特别容易不稳定。我在第一次复制类似架构时,直接把预训练ViT参数的lr设成了和新模块一样的值,结果跑了几千step就loss爆炸。后来把视觉塔的lr调成其他模块的十分之一,问题才消失。

文本塔的初始化也很重要。如果直接用随机初始化的Transformer,要跑很久才能学到基本的语言结构。更推荐的方式是加载一个已经训练好的文本模型作为caption塔和fusion塔的共同初始化,这样生成损失下降得更快,对比损失的语义空间也更平滑。

最后是batch size和显存的博弈。前面说batch size对对比学习很重要,但显存往往是瓶颈。我的方案是梯度累积,设一个很小的实际batch,累积16步甚至32步模拟大batch的效果。配合混合精度训练,实际效果可以逼近大batch训练,显存需求却能降一半以上。

5. 下游任务适配:CoCa在一堆任务上的真实表现

5.1 零样本分类与图文检索

CoCa在零样本分类和图文检索上表现得非常接近CLIP,有时候还略强一点。这背后的逻辑不复杂:因为对比损失的设计本身就是从CLIP继承来的,所以判别式任务上的优势保留得很完整。

我在评估一个微调版本时,在ImageNet零样本分类上跟同参数的CLIP做了对比,CoCa大概领先了1到2个百分点。幅度不算夸张,但考虑到CoCa同时还在搞定字幕生成任务,这种“不拖后腿”的意义就大了。图文检索里的零样本 recall@1,CoCa在Flickr30K和COCO上的数字也非常可观。如果你之前只用CLIP做检索,切换到CoCa不需要改任何上层逻辑,因为它输出的图像和文本向量纬度、相似度函数完全兼容。

5.2 图像字幕生成与VQA

字幕生成是CoCa相对CLIP最显著的增量。它在COCO Caption任务上能接近当时专门做字幕生成的模型,而这在一个统一架构里是比较少见的高水平。我在本地测试时跑了一段caption,模型对图片主体的描述很准确,有时候还能带上颜色、数量这些细粒度信息,说明fusion塔的逐层cross-attention确实把视觉细节喂进了语言模型的每一层。

VQA类的视觉问答任务里,CoCa可以把图像和问题拼在一段输入里,直接交给fusion塔生成答案。效果比把VQA当成分类问题来做的方式要好,因为生成式解码天然能处理更开放的答案空间。它的天花板主要受语言模型本身的容量限制,换一个更大的文本塔,VQA效果会随之提升。

5.3 冻结微调 vs 全量微调

CoCa最灵活的属性是“哪里都能冻”。你可以冻结整个视觉塔和caption塔,只训练fusion塔,这在数据量很小的下游任务里特别有用。因为fusion塔本身接受了跨模态生成训练,冻结其他部分时它依然有不错的图文对齐和语言生成能力。

全量微调的效果在数据量充足时通常更好,但需要特别小心不要“灾难性遗忘”。如果下游数据非常单一只包含某一类图像(比如全是医学影像),全量微调后模型对自然图像的检索能力会掉得很快。这时候我会选择冻结caption塔,只微调视觉塔和fusion塔,保住对比表示的同时让生成部分适应新域。

6. 复现路上的坑:我踩过的几个具体问题

6.1 文本tokenizer不一致导致的对齐问题

CoCa对文本的tokenizer要求比较严格。caption塔和fusion塔理论上应该共用同一个分词方式,因为它们吃的是同样的文本输入。如果你为了省事在caption塔用了A tokenizer,在fusion塔用了B tokenizer,对比特征和生成特征会在一开始就不在一个“坐标系”里,导致两个损失同时下降但检索效果纹丝不动。

更隐蔽的问题是中文场景下分词table不统一。英文的BPE在处理中文时会把每个汉字切成独立token,这种分词方式对生成任务影响不算大,但对“整句向量”的对比学习来说损失了很多语义信息。我在做中文数据复现时,宁可多花时间找合适的中文tokenizer,也不愿直接用英文BPE硬跑。

6.2 cross-attention权重不收敛

有个特别容易犯的错是cross-attention初始化得太“激进”。如果你把视觉特征直接以很大的初始权重注入语言模型,最开始几个step的loss可能会居高不下,因为语言模型还没有适应“眼前多了一堆乱七八糟的图像token”。

解决方案有两种。一种是初始化cross-attention的参数时,把输出投影矩阵设得极小,让它一开始接近恒等映射,然后慢慢放开。另一种是给cross-attention加一个可学习的门控系数,初始为0,训练中自己决定注入多少视觉信息。这两种做法在视觉语言模型里都很常见,CoCa那代模型直接硬训,没引进门控,但我们在自己的实现里引入门控后稳定性提升了很多。

6.3 分布式训练里batch size对不上

复现过程中最容易发现的隐蔽bug是:你以为自己的batch是1024,实际上因为分布式并行和数据均衡的关系,真实有效batch只有512甚至更小。这会导致对比损失的行动轨迹完全不同于论文里的设置,模型跑很久也不见提升。

排查方法是把每个GPU上的batch size、梯度累积步数、GPU数量三个数字乘起来,去跟论文里的设置对比。我遇到过某个分布式框架会在每个step自动做一次all-reduce,实际上不需要手动累积梯度,结果就是我额外乘了一个累积步数,batch size虚高一倍,训练曲线看起来正常但性能上不去。

7. 系列后续展望:CoCa之后还有什么可以玩

7.1 把CoCa当作预训练骨干的更多可能

CoCa这种“对比+生成”的框架,最有价值的属性是它产出的视觉特征既适合判别也适合生成,这就让它非常适合当作其他任务的初始化骨干。比如做图像编辑、文生图或者多模态对话系统,你不用另起炉灶重新预训练,加载CoCa权重后接一个任务头就能获得不错的下限。

特别是文生图领域,如果你需要一个文本编码器来把你的prompt编码成条件向量,CoCa里的单模态caption塔本身就是一个训练得极其充分的文本编码器,比直接用语言模型的效果更贴合视觉语义对齐的需求。我最近在这条路上做过尝试,效果不错,后续有空可以单独写一篇讲讲。

7.2 从CoCa到后续模型的一条清晰路线

CoCa是2022年前后的工作,但它几乎把所有重要的设计点都占全了。后来的很多模型要么继承它的双塔思路,要么优化它的跨模态交互方式。比如部分工作把两个文本塔合并成一个并引入双向注意力,用一个非对称的语义空间同时服务对比和生成。也有一些工作把温度τ直接纳入了可学习参数,省去了人为设定的烦恼。

如果你完全吃透了CoCa,再去啃这类后续模型会轻松很多,因为它们的基本盘就是CoCa那套“共享视觉塔 + 分化文本塔”的思路。阅读源码时只要盯住一句话:“哪个分支负责对比,哪个分支负责生成,它们之间有没有共享权重”,整张网络图基本就能画出来了。

7.3 数据配比和多语言扩展

CoCa在英文语料上表现很好,但直接挪到中文或小语种上会出现一个问题:图文交错的文档数据极其稀缺。这会让生成任务缺乏足够的训练信号,fusion塔学不到中文的跨模态生成能力。我的经验是:人工构造“图文交替段落”比想象中有效。把中文caption拆成“第一句描述全局,第二句描述物体,第三句描述关系”这种伪交错格式,明显能帮模型更快适应。

多语言扩展的核心同时在于tokenizer和语料。没有对应语言的高质量图文对,任何模型都白搭;有语料但tokenizer切词太碎,对比损失的效果也会被稀释得很厉害。这些坑往往要比改模型结构更花时间,但一旦处理好了,效果提升非常直接。

我自己在复现CoCa并踩过这些坑之后,最大的感触是:模型结构反而不是最难的部分,数据配比、温度参数、batch size和tokenizer这些“周边设置”才是拉开差距的地方。如果你正打算动手尝试,我建议先别急着上超大模型,把一个中等规模的CoCa完整跑通,把上面这些问题都摸一遍,再谈扩展。这样遇到问题才有足够清晰的排查线索,而不是被一堆变量同时影响时完全无从下手。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进