如果你在复现BLIP的时候仔细读过那段模型结构图估计会对“CLS → Encoder → Decoder”这条链路印象很深。三个词看似平平无奇但恰恰是理解这篇论文为什么能同时把“理解”和“生成”两类任务揉进同一个框架里的钥匙。这篇我打算直接从一个复现代码的人视角把CLS这个token在三种不同分支里的真实行为、Encoder和Decoder的关注力差异、以及三个损失函数怎么把这一套结构训起来这件事掰开揉碎讲清楚。1. 从任务割裂到统一BLIP想解决的那个别扭问题先花点篇幅把BLIP出场时的背景说清楚因为它决定了一切结构设计的动机。1.1 当时的视觉语言预训练是什么状态在BLIP之前视觉语言预训练VLP大体分两派。一派做“理解型”任务比如图文检索、图文匹配这类模型主干是Encoder架构输入图文再输出特征或得分另一派做“生成型”任务比如图像描述、视觉问答里的开放生成模型主干是Decoder架构输入视觉特征再自回归吐出文本token。问题在于这两派模型的骨干网络不通用。理解派模型迁移到生成任务效果很差因为它的训练目标里没有生成能力生成派模型反过来做理解特征表达又不够细粒度。而且当时很多方法在预训练阶段用了公开爬取的图文对数据里存在大量噪声——文本和图像相关度低的样本会让模型学到错误的对齐信号导致下游效果进一步打折。所谓统一说穿了就三件事模型结构能不能同时输出“用于区分的特征”和“用于生成的token”训练目标能不能把对比学习、匹配判别、语言建模放在一个模型里一起优化数据质量能不能在训练过程中自我校正。BLIP当时踩的是一个很实在的坑——别的模型总是告诉你“在这个任务上我们赢了”却没告诉你换个任务你得把网络拆掉重拼。BLIP想做到的是拆掉重拼的成本为零。1.2 MED架构的出现一个模型三种形态BLIP给出的方案是MED全称Multimodal Mixture of Encoder-Decoder多模态混合编解码结构。它同时包含三种分支单模态编码器分支、图像文本联合编码器分支、图像文本联合解码器分支。这三个分支共享一部分权重而不是各干各的独立网络。这个设计最微妙的一点是它用“共享Transformer层”的方式让同一个模型既有Encoder的双向注意力来提取全局语义又有Decoder的单向注意力来生成文本。论文里最核心的那张架构图画的就是这三个分支如何在同一个主干上做切换。而贯穿这三个分支的一个关键符号就是CLS。很多人读论文时把CLS当成ViT里常见的那个分类token觉得“哦就是加在序列开头做个特征汇聚嘛”。但如果你去看实现代码和训练逻辑会对CLS有更复杂的认知它在三个分支中扮演的角色、传递的信息粒度、以及它在每个分支里要不要被丢弃都是有讲究的。在动手写代码前我建议先把这三个词固定下来CLS是“信息汇聚点”Encoder是“交互加工区”Decoder是“逐步生成器”。整篇论文和技术实现都在围绕这三者的协作关系展开。2. CLS的三种角色一个token在三个分支里完全不重样下面进入正题从代码视角逐一拆解CLS在BLIP三个分支中的真实行为。这是我认为理解BLIP结构最关键的环节也是很多复现代码时最容易被带偏的地方。2.1 视觉编码器分支CLS是图像全局特征的出口BLIP的视觉编码器用的是ViT或者ResNet作为backbone。以ViT为例输入图像会先切成patch然后线性映射成patch embedding再加一个可学习的CLS token拼在序列最前面一起送入标准的Transformer Encoder层。在这个分支里CLS不参与具体的局部patch语义判断它只是每层Transformer通过自注意力不断聚合所有patch token信息的容器。到最后一层CLS对应位置的输出向量就是整张图像的全局表示。BLIP在做ITCImage-Text Contrastive损失计算时正是从视觉编码器取出这个CLS向量和文本编码器输出的文本表示做对比学习拉近匹配图文对的距离推远不匹配对。这里有一个容易忽略的实现细节如果你用同样一份ViT权重去做所谓的“共享视觉主干”那在计算ITC损失时视觉CLS一定不能直接拿去做下游分类任务。它训练时被拉向“与文本全局特征对齐”的子空间和纯视觉自监督训练得到的CLS含义不同。BLIP作者在论文里提到通过加权token交互来增强表征实际代码中常表现为对CLS输出做额外的LayerNorm和线性投影后再进对比损失这个投影是不参与下游生成的。从工程角度看这个CLS更像一个专用的“对比学习接头”。2.2 图文联合编码器分支CLS是匹配二分类的判官第二个分支图像与文本拼接后一起进入Encoder。结构上就是把图像特征序列和文本token序列在序列维度拼起来在文本开头放一个独立的CLS token一起经过交叉注意力层做双向交互。这个CLS和视觉编码器里的那个CLS不是同一个至少输入来源不同。联合编码器里的CLS更像“融合特征的总调度”。论文中的ITMImage-Text Matching任务就是从这个分支的CLS输出接一个二分类头判断输入的图文对是否匹配。如果你复现时打印CLS的输出维度会发现一个问题如果图像端64个patch文本端有20个token那拼接后的序列长度是85含CLS。这个CLS要在双向注意力里看到图像patch和文本token的全部内容才能做出“匹配/不匹配”的二分类判断。实际操作中ITM损失用的是Hard Negative Mining策略会刻意构造“难负例”让CLS去硬扛。所以这个分支的CLS本质上是整个ED中训练难度最大的角色。2.3 图文联合解码器分支CLS在这里基本不干活到了第三个分支视觉特征作为cross-attention的KV输入文本token序列在Decoder里做自回归生成。这时候序列开头通常也会放一个[BOS]或CLS但由于解码器用的是带因果掩码的自注意力序列开头token的注意力范围受限它的表征质量并不高。真正用于逐个token预测的核心是Decoder对每个目标token位置的输出。也就是说在Decoder分支里CLS更像是“占位符”负责启动生成流程而不是生成质量的关键。如果你把BLIP的LMLanguage Modeling损失实现打开看会发现计算loss时通常是忽略序列开头这个token的它的隐状态不会作为预测目标。理解这一点很解压同一个词根CLS在不同形态的Transformer中承担的任务天差地别。很多文章讲到BLIP只说“CLS是分类token”这种简化放在联合编码器分支勉强成立放在解码器分支就站不住脚了。这也是我从代码阅读中得到的一个强烈体会永远先看某个token在具体分支中的掩码范围再判断它的作用。3. Encoder侧的发生过程双向注意力如何让图文真正交互理解了CLS的三重身份我们再往细看Encoder侧到底怎么做特征交互。BLIP不只是一个结构创新它在输入构造和注意力机制上也做了很多嵌入式细节。3.1 输入构造图像patch和文本token怎么拼在一起代码层面一张图会被resize到固定分辨率比如224x224然后切成14x14的patch。对ViT-B/16来说序列长度是196每个patch映射为768维向量加上位置编码。文本侧通过WordPiece或BPE分词得到token序列也映射到相同维度。在联合编码器分支它们会拼成一个长序列[CLS, 图patch1, 图patch2, ..., 图patchN, 文token1, 文token2, ...]。Transformer自注意力在这个长序列上任意交互图像token能看到文本token文本token也能看到图像token这就是双向注意力。有一个重要的点就是图片patch这侧通常会额外加一个模态类型嵌入文本token也加对应的模态类型嵌入以便Transformer区分当前token来自哪种模态。虽然ViT本身已经有位置编码了但模态嵌入能让模型更快学到差异。BLIP的代码中embedding层包含token embedding、位置embedding、模态embedding三部分缺一个都会让训练收敛明显变慢。这是很多复现版和原版指标差距大的常见原因不是模型改坏了是输入构造少了东西。3.2 自注意力里的“跨模态流动”如何被建模联合编码器里的交互不靠额外模块靠的是纯自注意力。每个图像patch可以attend到所有其他patch和所有文本token文本token同样如此。这种信任机制在大模型时代我们说过很多次就是让两个模态的信息在同一个特征空间做soft对齐。但BLIP相比早期多模态模型不一样的地方在于它的联合编码器分支不是从零训练的而是用预训练的ViT和BERT做初始化再在图文数据上微调。所以它的收敛速度和最终效果在早期VLP工作里都属于第一梯队。尤其实操时的体会是初始化用ViT-B/16和BERT-Base对比从头训练在图文检索的R1上大概能高出四五个点。这说明跨模态交互很吃单模态初始化的底子。3.3 为什么联合编码器要保留完整双向注意力部分方法为了加快跨模态对齐会在浅层用单模态编码、深层再拼起来做交互。BLIP选择从头到尾都保持双向全交互好处是语义融合充分坏处是计算量大。论文里给的解释是统一特征提取和交互对于同时做理解和生成才能保持一致性。我的看法是在预训练阶段数据规模还不足以支撑深层分离式设计时全交互是稳妥的选择。实际操作中如果你考虑并行效率和显存会发现图像token数196加上文本token几十个对Transformer来说并不算大负担。ViT-B的序列长度在224分辨率下就是几十万次乘加运算完全可接受。真正不满意的点在于物体多、图像细节多的场景196个patch不够细BLIP后来的一些优化工作也往高分辨率继续卷了。这就是后话了。4. Decoder侧的自回归生成为什么CLS留在编码器解码器靠shifted embedding理解与生成的区别在于Decoder的目标是给定视觉信息逐词生成文本。它不能让当前词看到未来词所以必须用因果掩码。本节的要点是为什么解码器里不依赖CLS来启动预测以及代码里到底怎么处理生成序列。4.1 因果掩码和cross-attention的组合逻辑BLIP的Decoder分支每个解码层包含两部分注意力先是自注意力mask成因果形式只看自己和之前的token然后是cross-attentionQ来自解码器当前tokenKV来自视觉编码器的输出序列。这样每个文本token在生成时都能融入图像语义但文本侧不泄漏未来信息。如果你看过实现代码会看到Decoder分支的forward里视觉特征通过一个cross-attention层插入而不是像Fusion Encoder那样直接用双向注意力混合。这是“Encode”和“Decode”最本质的分界线Encode是双向全交互Decode是单向生成式交互。4.2 所谓的shifted embedding在做什么文本生成任务里常见一种处理训练时输入序列整体右移一位作为decoder输入让当前位置的输入是前一个真实token把当前位置的真实token作为预测目标。这就是shifted embedding的由来。BLIP的LM损失就沿用了这种做法。训练时输入到解码器的文本序列是右移后的真值token比如“a cat on the grass”变成“[BOS] a cat on the grass”作为输入目标是“a cat on the grass [EOS]”。推理时每步用已生成token序列作为输入采样下一个token再把新token拼回输入序列。这解释了为什么在实践中如果你把CLS token硬塞进解码器的输入开头然后不加右移效果会大幅下降。解码器是严格按时间顺序来的开头位置放CLS但预测目标却是第二个token等于强制模型“预测自己已经看到的东西”训练与推理不一致。BLIP的实现里严格遵循右移逻辑CLS只作为BOS占位不承担语义判断。如果你复现时loss掉不下去先检查你的输入是否右移了。4.3 预训练阶段的LM目标真是“懂图像才能生成”的试金石Decoder的LM损失本质是一个标准语言模型目标只不过条件是图像特征。这就迫使模型不能只做检索式匹配而是要真正“理解”图像内容并组织成通顺描述。BLIP把这种能力直接用于下游图像描述生成任务效果在相当长时间里都是SOTA级别的。我自己的复现体感是visual feature质量直接决定生成上限。如果你只做了ITC/ITM训练没有单独把LM训练到位生成文本会很生硬、丢主语或者瞎编属性。BLIP把这三个损失放在一起训练形成结构上的互相促进ITC让特征对齐ITM让特征能区分LM让特征能描述。三个目标解决了三个不同维度的能力这也是后面五个损失函数协同的铺垫。5. 三个损失函数怎么让CLS、Encoder、Decoder协同收敛BLIP的核心结构跑通之后训练目标的重要性就浮出来了。我见过不少朋友把MED结构搭得很像那么回事但loss怎么配比、什么时候冻结哪个分支完全是凭感觉。这一节从ITC、ITM、LM各自的角色和联动逻辑说起。5.1 ITC拉近匹配特征推远不匹配特征ITC即Image-Text Contrastive Loss做法和CLIP一致从视觉编码器取CLS从文本编码器取文本CLS或最后一层平均计算两者cosine相似度再用InfoNCE损失拉近正样本、推远负样本。BLIP的做法是在batch内构造负样本比如一个batch有32个图文对那么某张图的负样本就是其他31个文本。一般认为ITC的梯度会同时更新图像编码器和文本编码器让它们学会把共享语义映射到同一个子空间。BLIP在实现中为了过滤数据噪声还会计算一个相似度分数来对样本加权。这种加权不是直接丢弃低分样本而是让低分样本的梯度贡献更小相当于是软性去噪。5.2 ITM训练一个更严格的匹配判别器ITM是二分类任务输入是联合编码器分支的完整图文拼接CLS输出过一个线性层输出匹配概率。这个任务比ITC更难因为它要求模型基于细粒度的token级交互来判断匹配而不是仅凭全局特征相似。值得一提的Hard Negative Mining构造负样本对时不是随便拉一个不匹配的文本而是选那些与当前图有部分共享语义但整体不搭配的文本作为负例。比如某张图上有“一只猫和一只狗”硬负例会选描述道“只有一只猫”的文本。通过这种负例CLS才能学会抓住细微差别匹配判别的鲁棒性才会强。5.3 LM把语义对齐变成生成能力LM是Decoder分支的autoregressive loss在每个step计算交叉熵优化的是给定图像条件下生成正确文本的概率。它与ITC/ITM的区别是后面两者做的是判别式任务LM是生成式任务它需要模型真的把图像信息组织成语言输出。这三个loss在BLIP里是相加的原文中LM的权重通常会设置得比ITC/ITM略高一些具体数值可以在一些公开配置里看到。一个合理的解释是生成任务对模型容量的要求最高如果权重太低模型倾向走捷径只做检索式匹配不学真实生成。实践上如果发现生成描述重复度高或空泛把LM权重适当加大是一个直接有效的调法。5.4 三个损失如何反向协同同一套CLS不同梯度流很多第一次接触BLIP的人会有疑问同一个视觉编码器CLS同时被ITC拉向对齐、被ITM作为匹配判据、被Decoder的cross-attention作为KV来源这三个梯度会不会打架实测下来基本不会因为ITC是全局对比ITM是配对判别LM是生成三个损失在特征空间优化的方向并不冲突。ITC提供一个松散的对齐先验ITM收紧判别边界LM迫使表征信息完备。这个结构到今天还被许多工作沿用不是偶然的。6. 复现BLIP实战源码阅读顺序和注意力掩码避坑清单最后这部分我想把代码实践里踩过的坑整理一下帮助读者从“看懂论文”过渡到“跑通代码”。6.1 建议的github源码阅读顺序拿到一份BLIP实现后的推荐路径是先看懂config文件和模型初始化逻辑再找visual_encoder的定义搞清楚ViT和文本编码器分别是什么结构然后看forward_image_text函数注意自注意力掩码的类型最后再去看loss计算部分。如果一开始就一头扎进train函数看数据加载和优化器容易迷失。真正信息密度最大的函数就是forward_text、forward_image、forward_image_text和forward_image_text_decoder。把这四个函数对照三幅结构图看逻辑会清晰很多。6.2 注意力掩码最容易出错且最难排查的地方我说一个真实经历。有次复现BLIP发现预训练loss在迭代了一两万步后突然上涨训练震荡剧烈。我查遍了学习率、数据顺序都没找到问题。最后逐层对比才知道是在Decoder分支的cross-attention里我无意间用了全连接mask而不是因果mask导致当前token看到了未来token的信息。生成任务中这种泄漏的后果是训练阶段模型看起来很会“预测”但验证时因无法看到未来而性能暴跌。排查方式其实很简单自己写个小样本测试case打印每一步生成的结果看是否存在提前输出后续内容的迹象。出现这种情况基本就是mask配错了。BLIP对self-attention和cross-attention分别维护不同的mask前者是下三角后者是全1矩阵千万别为了省事共用一套。6.3 数据加载时的文本清洗与长度截断影响复现效果的另一大因素是文本数据质量。BLIP训练用的图文对文本需要做标准化去掉多余空格、统一小写、处理特殊字符。文本最大长度一般截断到30个token或35个token太长的描述尾部噪声很大Decoder预测起来也吃力。在数据加载时实现随机采样负样本的难度也不低。ITM负样本要按批次做shuffle还需要排除正样本索引否则会出现“标签错了”的漏洞直接影响ITC和ITM的稳定性。很多复现版本跑出来结果偏低本质上不是结构问题而是负样本采样没做好。6.4 训练资源与收敛观察的体感BLIP的常见配置是batch_size 64或128训练数据集规模大概在千万级图文对。我自己在单机多卡跑的时候显存主要消耗在视觉编码器分支和联合编码器分支预训练初期ITC和LM会先下降ITM会在负样本构造得好时快速收敛之后再缓慢下降。如果ITM一直在0.5附近徘徊先检查负样本逻辑。如果资源有限可以先固定视觉编码器、只训文本侧和联合交互层等ITM收敛到一定水平再解冻视觉侧。这样能显著减少显存开销但会牺牲最终效果上限属于“实验跑通”和“完整复现”的折中选择。做迁移学习时这种渐进解冻策略反而更稳具体可以根据你的设备来权衡。收尾的一个实战建议如果你准备上手复现BLIP我建议先把前三张结构图配着四个forward函数过一遍再碰训练脚本别看一行代码就先跑。CLS在不同分支的角色差异、Decoder的因果掩码、三个loss的梯度流这三个点想通了BLIP的80%内容就抓住了。之后想往ImageCaptioning、VQA、图文检索这些下游应用走也基本是在这套骨架上换任务头的事。BLIP不是一个“多任务硬拼”的方法它的功夫在于让理解与生成两个方向共享一套参数还能互不干扰这一点从结构到loss都值得反复琢磨。