CLIP
Learning Transferable Visual Models From Natural Language Supervision
Clip, 全称为Contrastive Language-Image Pre-training,即利用图文对进行对比学习从而进行预训练的方法,文章表述的核心是用文本作为监督信号来训练可迁移的视觉模型。和这篇出发点相同(即作为可迁移的视觉模型)但是纯利用视觉特征的,可以参考dinov2。
模型很简单,一个图像特征提取器,一个文本特征提取器,分别获取特征,然后进行对比学习,伪代码如下:
# image_encoder - ResNet or Vision Transformer
# text_encoder - CBOW or Text Transformer
# I[n, h, w, c] - minibatch of aligned images
# T[n, l] - minibatch of aligned texts
# W_i[d_i, d_e] - learned proj of image to embed
# W_t[d_t, d_e] - learned proj of text to embed
# t - learned temperature parameter
# extract feature representations of each modality
I_f = image_encoder(I) #[n, d_i]
T_f = text_encoder(T) #[n, d_t]
# joint multimodal embedding [n, d_e]
I_e = l2_normalize(np.dot(I_f, W_i), axis=1)
T_e = l2_normalize(np.dot(T_f, W_t), axis=1)
# scaled pairwise cosine similarities [n, n]
logits = np.dot(I_e, T_e.T) * np.exp(t)
# symmetric loss function
labels = np.arange(n)
loss_i = cross_entropy_loss(logits, labels, axis=0)
loss_t = cross_entropy_loss(logits, labels, axis=1)
loss = (loss_i + loss_t)/2
- 图像特征处理
图像$[n,h,w,c]$
Image Patching图像分块
将连续的二维图像进行离散化,分割成一系列固定大小,不重叠的小方块。例如224x224的图像,分割为16x16大小的图像块,则获得14x14=196个图像块。
Flatten & Linear Projection展平和线性投影
每个二维图像块是16x16x3,展平后是768,然后通过一个可学习的线性投影层,将其映射到模型预设的、更具语义意义的隐藏维度中(Embedding Dimension),如768。
Position&Class Embeding位置编码和类别编码
为了让模型理解每个图像块在原始图片中的空间位置,必须为每个视觉次元注入位置信息。此外,ViT还借鉴了BERT思想,在序列的最前面加入一个额外的、可学习的[class] Token
B = paddle.shape(x)[0]
x = self.patch_embed(x)
# 实际拆开是:
self.proj = nn.Conv2D(in_chans, embed_dim, kernel_size=patch_size, stride=patch_size, bias_attr=False)
# 分块和线性投影一个卷积就完成了,然后单独flatten
x = self.proj(x).flatten(2).transpose((0, 2, 1))
class_embedding = self.class_embedding.expand((B, -1, -1))
# cls + patch
x = paddle.concat((class_embedding, x), axis=1)
x = x + self.positional_embedding
# 对位置进行随机dropout
x = self.pos_drop(x)
# LN 把每个 token 的特征值调整到 “均值 0、方差 1” 的范围,让后续 Transformer 块的训练更稳定、收敛更快(Transformer 对数值范围很敏感,LN 是标配)。
x = self.norm_pre(x)
# 然后就可以输入Transformer了
- 文本特征处理
文本$[n,l]$
FullTokenizer分词
包括BasicTokenizer和WordPieceTokenizer
Token_to_id转换成词汇表id
补充信息id
# text_id 前补cls id,结尾补end id,最后补padding id,1是cls id,2是END id,0是pad_id
id = [1] + id[:max_len_stu] + [2] + [0] * max(0,(max_len_stu - len(id)))
Word Embedding词嵌入+Pos Embedding位置嵌入+Sent_Embedding句子嵌入
self.word_emb = nn.Embedding(
d_vocab,
d_emb,
weight_attr=paddle.ParamAttr(name=append_name(name, 'word_embedding'), initializer=self.initializer))
self.pos_emb = nn.Embedding(
d_pos,
d_emb,
weight_attr=paddle.ParamAttr(name=append_name(name, 'pos_embedding'), initializer=self.initializer))
self.sent_emb = nn.Embedding(
d_sent,
d_emb,
weight_attr=paddle.ParamAttr(name=append_name(name, 'sent_embedding'), initializer=self.initializer))
# src_id 维度:[batch_size, seq_len] ,词嵌入后维度是[batch_size, seq_len, d_emb]
# 词嵌入本质是一个查表操作,同理位置嵌入和句子嵌入也都是查表
embedded = self.word_emb(src_ids) + self.pos_emb(pos_ids) + self.sent_emb(sent_id_stu)
问题:只有单个句子是否还需要句子嵌入?
句子嵌入是为序列中每个 token(词 / 字)赋予 “所属句子标识” 的嵌入层,核心作用是让模型感知token 的句子归属,本质是 “句子级别的特征编码”。
比如 “问答任务”(问题 + 答案)、“自然语言推理”(前提 + 假设),此时sent_id_stu中:问题的所有 token 的句子 ID=0,答案的所有 token 的句子 ID=1;句子嵌入矩阵[2, d_emb]中,ID=0 对应 “问题句” 的特征向量,ID=1 对应 “答案句” 的特征向量。
单句场景(如文本分类):
此时所有 token 的句子 ID=0,d_sent=1,句子嵌入矩阵退化为[1, d_emb](只有一行向量)。
句子嵌入的本质是解决 “模型无法区分 token 所属句子” 的问题,具体价值:
- 多句场景:明确 token 的句子边界(比如 BERT 的 Next Sentence Prediction 任务依赖句子嵌入);
- 单句场景:给序列添加 “全局句子特征”(可理解为 “整个句子的公共偏置”),辅助模型捕捉句子级别的整体信息。
如果你的模型是Transformer/BERT 类架构,或任务有潜在的 “句子级全局特征需求”,建议保留句子嵌入,原因有:①参数成本极低,收益稳定。任务的句子级特征(比如情感分析中,“积极句子” 的全局偏置会偏向正向,“消极句子” 偏向负向)。②模型架构的兼容性
特征映射
np.dot(I_f,W_i)和np.dot(T_f,W_t)的作用是将图像和文本特征映射到相同的维度d_e归一化
使用l2_normalize 将特征向量归一化为单位向量。目的: 归一化后,两个向量的点积直接等于它们的余弦相似度 (Cosine Similarity)。这消除了向量模长的影响,只关注方向的一致性。
- 计算logits
np.dot(I_e,T_e.T)计算得到的余弦相似度的值域范围是[-1,1]
np.exp(t) ,温度缩放,用于在训练过程中动态调整softmax的温度,帮助模型更好收敛。
- 对称对比损失函数
labels = np.arange(n) ,输出为array([0, 1, 2, ...,n-1]),分别代表标签
loss_i = cross_entropy_loss(logits, labels, axis=0)
交叉熵公式:$CE(p,q)=-\sum^K_{k=1}p(k)\cdot log(q(k))$
$p(k)$真是标签的概率分布(由于是分类问题,所以是one-hot形式,仅匹配的类别为1,其余为0),q(k) 模型预测的概率分布(经softmax归一化后的相似度)。
所以展开后,就退化为了$CE(i,logits)=-log(\frac{exp(logits[i])}{\sum^{n}_{k=1}exp(logits[k])})$,i代表真实标签是第i个样本匹配,$p(k)=0$的全被消去,公式只保留了$p(k)=1$的情况,这就有两个问题:
- 为什么交叉熵$p(k)$在前,$q(k)$ 在后?
可以参考我的博客**<<网络结构解密>>,核心的思想是:信息熵是所有信息量的期望,信息熵越小,信息量越小。从编码层面表示:最短平均编码长度**。编码方案完美时,最短平均编码长度。编码方案完美就是指一个事件结果的出现概率越低,对齐编码的bit长度就越长。
基于此,假设我们用一个错误的分布q,对随机变量编码,获得的就不是最短平均编码长度,真实的每个字符出现的概率还是一致,即$p(k)$,而信息量的预估或者说编码错了,即**$-logq(k)$**
二者的差即为多出来的平均编码长度,或者说两个概率分布的差异,即为相对熵= 交叉熵-真实熵,即$D_{KL}(P||Q) = -\sum p(x_i)\cdot(q(x_i)-p(x_i))$,由于真实熵是固定的,所以可以用交叉熵替代相对熵,交叉熵最小则相对熵最小
为什么可以用交叉熵替代相对熵,交叉熵趋于0的时候,相对熵不是0,能表示两个分布一致吗?
对于分类任务,真实标签的分布 P 是固定的 one-hot 分布,其熵 (H(P)=0)
- 为什么用softmax归一化?它相比于普通的归一化的优势是?
神经网络的最后一层输出通常被称为 Logits(未归一化的对数概率),这些值通常是实数范围 (−∞,+∞)。而交叉熵的log(q(x))需要q(x)是预测概率,所以要求模型输出必须满足:1非负性2所有输出的值的和必须等于1.
Softmax:$S(z_i) = \frac{e^{z_i}}{\sum_j e^{z_i}}$,首先是满足的。
其次是优势:相比于普通归一化如$\frac{x_i}{\sum_{x_i}}$或min-max归一化$x’=\frac{x-min(X)}{max(X)-min(X)}$
1.处理负数的能力。普通归一化无法处理负数,
2.拉大差距(马太效应)。不是简单地按比例缩放,而是通过指数增长,显著放大最大值,抑制小值。
3.梯度形式简单,且无梯度消失问题。梯度公式为:$\frac{\partial L}{\partial z_i} = a_i -y_i$,其中$a_i$:模型预测第i类的概率,$y_i$:真实标签,$z_i$:logits,梯度的大小直接取决于预测偏差。这也是深度学习框架(TensorFlow、PyTorch)中,会将“Softmax + 交叉熵”实现为一个统一层(如 nn.CrossEntropyLoss)的核心原因——既简化计算,又提升数值稳定性。 这个梯度的推导很妙,可以自己去搜索。核心需要理解的一点是:$\frac{\partial L}{\partial z_i} = \sum^C_{j=1}\frac{\partial L}{\partial a_j}\cdot\frac{\partial{a_j}}{\partial z_i}$,为什么要求和?因为$z_i$会对每个$a_j$的数值造成影响(softmax求和的影响),所以反向传播每个$a_j$也会对$z_i$造成影响。
**cross_entropy_loss(logits, labels, axis=0)**的原理
logits:模型未经过softmax激活的原始输出;
labels:真实标签,支持两种格式 1 one-hot编码,和logits维度完全一致;2 稀疏标签(整数),shape比ligits少axis维度,框架会自动转one-hot。
axis:指定分类维度,在该轴上计算softmax并求和交叉熵。
为什么是对称交叉熵损失?
就像谈恋爱相亲一样:
- 单向 Loss (I→T): 只是问男生(图片)“这几个女生(文本)里你最喜欢谁?”
- 对称 Loss: 不仅问男生,还要问女生(文本)“这几个男生(图片)里你最喜欢谁?”
只有当男生选择了女生,且女生也选择了男生(双向奔赴),这个匹配在 CLIP 的特征空间里才是最稳固、最准确的。
线上应用代码
# 单机多卡训练的话,假设为8卡,数据是分布在8张卡上的,8张卡上的存在一个列表
emb_img_stu_list = []
emb_txt_stu_list = []
# 把每个进程的都收集过来
dist.all_gather(emb_img_stu_list, emb_img_stu)
dist.all_gather(emb_txt_stu_list, emb_txt_stu)
emb_img_stu_all=paddle.concat(x=emb_img_stu_list, axis=0)
emb_txt_stu_all=paddle.concat(x=emb_txt_stu_list, axis=0)
# (b,C) *(C,B)
logits_stu_img = emb_img_stu @ emb_txt_stu_all.t()
logits_stu_txt = emb_txt_stu @ emb_img_stu_all.t()
bs_img = logits_stu_img.shape[0]
bs_txt = logits_stu_txt.shape[0]
# 获取当前是哪个进程
rank = dist.get_rank()
# 构造标签
# 举例:rank=0(第 1 张卡):0+0=0~7;rank=1(第 2 张卡):0+8×1=8~15;rank=2:0+8×2=16~23…… 以此类推;
img_labels = (paddle.arange(bs_img) + bs_img * rank).astype('int')
text_labels = (paddle.arange(bs_txt) + bs_txt * rank).astype('int')
# bs * Bs bs * 1
img_loss_constrastive = paddle.mean(nn.functional.softmax_with_cross_entropy(
logits_stu_img / 0.07, img_labels.reshape([-1, 1])))
text_loss_constrastive = paddle.mean(nn.functional.softmax_with_cross_entropy(
logits_stu_txt / 0.07, text_labels.reshape([-1, 1])))
loss_constrastive = (img_loss_constrastive + text_loss_constrastive) * 0.5
BLIP2
Bootstrapping Language-Image Pre-training with Frozen Image Encoders and Large Language Models
BLIP1就不提了,我们直接从BLIP2开始。
BLIP2的预训练核心就分2步:①固定Vit,进行视觉和语言的表征学习 ②固定LLM模型,学习从图像生成文本
我们做图文对齐,核心关注的就是第一步。BLIP2进行图文对齐的核心就是设计了Q-Former(Querying Transformer),采用一组可学习的查询向量从冻结的图像编码器中提取视觉特征,通过强制qformer学习和文本最相关的视觉表达,强制模型只提取那些对生成文本最有用的视觉特征。它把图片变成了LLM能“消化”的精简特征序列。
第一步不连接 LLM。Q-Former 拿着图片特征和对应的文本描述进行训练。它学习如何提取出的视觉特征能最好地匹配文本。(通俗理解就是QFormer先把图像翻译成文本描述,然后再一起喂到LLM就行,这样ViT和LLM都不需要重新训练)。
Q-Former主要参考了Bert的enconder,总共12层,偶数层增加了一个ca层,奇数层不增加。Q-Former通过bert-base预训练模型初始化,SA是共享权重的,CA随机初始化,CA层只对图像起作用。图中颜色相同代表权重共享。
图像特征提取用vit,使用了ViT-L/14 from CLIP、ViT-G/14 from EVA-CLIP预训练模型,移除ViT最后一层,使用倒数第二层特征。这里vit图像输入的分辨率是224,可以考虑换成sam的vit,输入分辨率是1024,可以提取更丰富的图像特征。
设计了一个可学习的Query embeddings作为输入,大小为【b,32,768】,query通过SA(self attention)进行自我交互(让每个token学习自己应该关注什么),学习从图片里边提取哪些token;通过CA(cross attention)和图像特征进行交互,进行decoder,同时和文本分支的sa共享权重,实现了和文本进行交互。
PS: 作者源码中用一个Bert模型来实现QFormer,通过魔改BertLayer实现通过条件判断来确定走image transformer分支还是text-transformer分支。感兴趣的同学可以深入看一下源码,核心逻辑位于: lavis/models/blip2_models/Qformer.BertLayer
- 为什么要移除ViT最后一层,使用倒数第二层特征?
- ViT 各层特征的核心差异
- ViT 的最后一层(输出层):经过预训练任务(如 CLIP 的图文匹配、分类任务)优化,特征高度偏向 “任务目标”。比如 CLIP 的 ViT 最后一层特征,已经被压缩成适配 “图文对齐分类” 的向量,语义信息集中但丢失了很多图像细节。
- 倒数第二层(编码层末尾):属于 “通用特征层”,未经过最终任务的压缩映射。它保留了图像的局部结构(如物体边缘、纹理)、全局布局(如物体位置关系)等细粒度信息,维度更高、表达更灵活。
- 适配 Qformer 的多模态交互需求
Qformer 需要的是 “能和文本语义联动的图像特征”,而非 “已经定型的分类特征”。
- 倒数第二层特征的通用性更强,能更好地和 Qformer 的 Query 进行交互 ——Query 可以从中抓取不同维度的视觉信息(比如文本提到 “红色杯子”,Query 能从倒数第二层特征中定位 “红色” 和 “杯子形状” 的相关特征)。
- 若用最后一层特征,其表达已经被预训练任务 “固化”,灵活性不足,难以适配后续多模态融合的细粒度对齐需求(比如文本和图像的局部信息匹配)。
- 避免信息过度压缩
ViT 的最后一层通常会将前面的高维特征映射到低维向量(如 CLIP 的 512 维),过程中会丢失大量细节。
- 倒数第二层特征维度更高(如 ViT-L/14 的倒数第二层维度可能达 1024 或 2048),包含的图像信息更完整。
- 这对后续 Qformer 学习 “图文互译” 式的多模态信息至关重要 —— 更丰富的细节能让 Query 更精准地建立文本语义和图像特征的对应关系。
为了进行表征学习, 设计了3个任务,分别是:图文匹配(ITM:Image-Text Matching)、图文对比学习(ITC:Image-text Contrastive)**、根据图像生成文本(ITG:Image-text Generation)。其中ITG是为了第二阶段做准备,我们不需要生成只考虑对齐的话,其实只用前两个也行。ITG 并非无用,只是在 “仅需对齐” 的场景下性价比不高。**
这样的话,其实相比clip,使用blip2的优点主要在于:①ViT无需重新训练,极大减少了训练参数,提升了训练速度。②Q-Former自身也作为Text-Encoder,比较轻量化 ③和LiT的思想相似。
- LiT是什么?
LiT: Zero-Shot Transfer with Locked-image text Tuning。
- 提出LiT方法:论文提出了一种名为”Locked-image Tuning”(LiT)的对比调优方法,用于将预训练的图像模型转换为支持零样本迁移学习的视觉-语言模型。
- 核心思想:LiT通过锁定预训练的图像模型,仅训练文本模型来学习与图像表示对齐的文本表示,从而实现高效的零样本迁移。
实验发现锁定的预训练图像模型和未锁定的文本模型能够达到最好的效果。
所以BLIP2的第一步作为第一步就是和LiT的思想不谋而合的。
- ITC
和clip基本相同,此过程query和文本互相看不见,没有交互,避免了query直接从文本学习信息,如果query直接从文本学习,这样query就学不到图像信息,对比学习也就没有意义了。举个例子,图片是一只猫,文本描述是:”一只猫”。query可以直接从文本描述去学习,没办法保证它是从图像中学习到的相关信息。
整体通路类似如下:
QFormer其实可以看做包含两个Transformer-图像Transformer和文本Transformer,只不过SA是共用的。
Image Transformer:query token自己SA,然后和image emb进行CA,然后自己linear;
Text Transformer: text token自己SA,然后自己linear。
- ITM
图文匹配是一个分类任务,判断图像和文本描述是一对,是为1,否则为0。
分类需要正负样本,采样策略为在一个batch内寻找负样本。
文本构造:[正样本,正样本,难负样本]
图像构造:**[正样本,难负样本,正样本]**
对应:**[匹配1,不匹配0,不匹配0]**
为什么要3份?
除了正样本那份,男生确认女生不适合自己,和女生确认男生不适合自己,都要有!
采样的办法?
根据相似度分数选取难负样本,根据i2t选取文本难负样本,根据t2i选取图像难负样本。对角线置0,剩余的按相似度分数采样(相似度越高,被采样的概率越大,越是难负样本)。采样获取难负样本的idx,然后再从batch里去取对应的样本特征。
获取样本集合后如何融合?
整体过程如下:
具体Attention交互细节:
query emb 和text emb 就像夫妻,只在晚上睡在一块(concat)做事(SA)**,然后白天分开**自己干自己的事,query emb去CA和linear,text emb做自己的linear,做完事再睡一块,这就是一个Encoder Block,重复N天,看彼此是否合适。
为什么这个时候可以互相看见?
简单来说,ITC(Image-Text Contrastive)是为了“高效检索”,而 ITM(Image-Text Matching)是为了“精准校验”。
这两者在模型架构和目标上的根本差异,决定了为什么 ITC 必须“隔离”,而 ITM 必须“互看”。
以下是详细的深度解析:
架构模式的区别:双塔 vs. 融合
- ITC (Image-Text Contrastive) = 双塔模式 (Dual Encoder)
- 机制:图像进一个编码器,文本进另一个编码器。在编码过程中,两者互不干扰。 只有在最后生成了两个全局向量(Embedding)后,才通过点积(Dot Product)来计算相似度。
- 为什么不能互看? 如果在编码时图像就能看到文本(Cross-Attention),那么图像的特征向量就会随着输入的文本不同而改变。
- 后果:你就无法预先计算好图像的特征存入数据库了。每次来一个新的文本搜索,你都得把数据库里几亿张图重新和这个文本一起送进模型算一遍,计算量是 O(N2)O(N2) 甚至是 O(N×M)O(N×M),这在实际应用中是不可能的。
- 目的:学习一个通用的特征空间,让相似的图文在空间里靠得近。
- ITM (Image-Text Matching) = 融合模式 (Fusion Encoder / Cross-Encoder)
- 机制:也就是你提供的文本中描述的。图像特征(或Query)和文本特征被送入同一个注意力层(Attention),并且Mask 是双向可见的。
- 为什么必须互看? ITM 是一个二分类任务(是/否),它需要判断细节。比如文本说是“一只戴红帽子的黑狗”,ITC 可能只能判断出“有狗、有红色”,但 ITM 需要通过“互看”来确认那顶红帽子是不是真的戴在那只黑狗头上,而不是路人的红衣服。这需要深度的细粒度交互。
- 代价:计算昂贵。所以通常只对 ITC 筛选出来的 Top-K 个结果(Hard Negatives)进行 ITM 重排序。
真实代码
# 对比损失 i2t和t2i的维度都是(b,B)
conloss, i2t, t2i = self.con_loss(image_feats, text_feat)
# 把不同进程的输入都收集起来
src_id_stu_world = self.concat_all_gather(src_id_stu)
sent_id_world = self.concat_all_gather(sent_id)
img_emb_world = self.concat_all_gather(img_emb_todo)
bs = img_emb_todo.shape[0]
# 获取当前进程的排名
rank = dist.get_rank()
# 下面整体是选取难负样本的逻辑
weights_i2t = F.softmax(i2t) + 1e-4
# 先取维度为(b,b)的,即当前分布式的样本,对角线置0,对角线都是正样本,所以不能选取,所以置0
# 原地修改,所以weights_i2t的维度还是(b,B)
# 这样做的目的是负样本能在整个Batch中选取
weights_i2t[:, rank * bs: rank * bs + bs].fill_diagonal_(0)
weights_t2i = F.softmax(t2i) + 1e-4
weights_t2i[:, rank * bs: rank * bs + bs].fill_diagonal_(0)
# logger.info(weights_i2t)
# 寻找难负样本
txt_embeds_neg = []
sent_embeds_neg = []
for b in range(bs):
# 直接按照概率分布来采样,这个样子的话,正样本概率为0,难负样本概率最大,选取idx
neg_idx = paddle.multinomial(weights_i2t[b], 1).item()
# 获取难负样本,难负样本也是从B里选取的
txt_embeds_neg.append(src_id_stu_world[neg_idx])
# 对应的句子id也取出来
sent_embeds_neg.append(sent_id_world[neg_idx])
# 输出维度为(bs,max_seq_len)的tensor
txt_embeds_neg = paddle.stack(txt_embeds_neg)
sent_embeds_neg = paddle.stack(sent_embeds_neg)
# 图像同理也选取难负样本
image_embeds_neg = []
for b in range(bs):
neg_idx = paddle.multinomial(weights_t2i[b], 1).item()
image_embeds_neg.append(img_emb_world[neg_idx])
image_embeds_neg = paddle.stack(image_embeds_neg)
#文本 分别是正常的,正常的,难负样本
# axis 默认为0,在batch_size这个维度concat
text_all = paddle.concat([src_id_stu, src_id_stu, txt_embeds_neg])
sent_all = paddle.concat([sent_id, sent_id, sent_embeds_neg])
# query是从image features中提取特征,因此直接复制三份就行
# (3B,32,768)
query_all = self.query_tokens.expand([text_all.shape[0], -1, -1])
# (3B,32)
query_sentid_all = paddle.repeat_interleave(self.query_tokens_sentid, text_all.shape[0], 0)
# 图像,分别是正常的,难负样本,正常的
# 一一对应就是匹配,不匹配,不匹配
# (3B, B,1,256)
image_embeds_all = paddle.concat([img_emb_todo, image_embeds_neg, img_emb_todo])
# 返回hidden [B,32+x,768]
qformer_encoder = self.qformer(query_all, query_sentid_all, image_embeds_all, text_all, sent_all, txt='fusion')
# [B,32,768]
vl_embeddings = qformer_encoder[:, :query_all.shape[1], :]
# [B,32,2]
vl_output = self.itm_proj(vl_embeddings)
# [B,2]
logits = paddle.mean(vl_output, 1)
# [正样本,负样本,负样本]
itm_labels = paddle.concat([paddle.ones([bs], dtype='int64'), paddle.zeros([2 * bs], dtype='int64')], 0)
itm_loss = paddle.mean(nn.functional.softmax_with_cross_entropy(logits, itm_labels.reshape([-1, 1])))
- ITG
该部分训练q-former从图像生成文本(Q-Former右半部分),此时q-former变成了decoder,ca层也不用。
transfomer在decoder时,需要对token进行mask。text tokens解码是,输入【DEC】作为解码标记,然后逐token进行解码,还未解码的token先被mask住,此时的token可以看到query的信息(图文对比学习中的query embedding和图像交互得到的key和value值,Q-Former左半部分)和之前解码过的text token,但query看不到text tokens的信息。即解码时文本可以看到图像信息和解码过的文本信息,但是图像看不到文本,解码时看着文本和图像一起进行解码,这样模型预测的文本可以从图像中学习到信息。
浅聊下第二步:
通过第一步我们得到一个训练好的QFormer,这个QFormer能够实现将图片转为一个32x768(用32个token来表征图像)。第二步的任务是让预训练的LLM模型能够理解上述的图片表征,从而借助LLM强大的知识库来实现问答、推理等任务。也就是说,这一阶段我们需要通过训练来赋予图片token能被LLM理解的语义。
这一步的训练比较简单。固定image encoder与预训练的LLM模型,仅训练QFormer和新增的一个投影层。训练任务为language modeling。最终实现QFormer输出的图片表征(论文称之为soft visual prompt)变成LLM能看懂的样子。
FILIP
FINE-GRAINED INTERACTIVE LANGUAGE-IMAGE PRE-TRAINING
Clip是整张图片和整行文本做匹配,所以图片中长尾的、形态较小的可能会被忽略。
一种解决办法是利用ROI Detector对ROI区域进行提取,然后和文本进行在线交互,即可实现细粒度匹配。
然而其问题是需要一个较好的ROI Detector,并且计算复杂度较高,最重要的事,对于在线应用比如图搜是个比较大的负担。
FILIP想到的是使用迟交互,交互操作后移,使得前面可以正常刷库。
在CLIP无论是图片编码器还是文本编码器,均对同一个图片/文本只产出一个特征向量,通过计算余弦相似度计算其图文相似性,显然这是一种全局(Global)的相似度计算方式。而在FILIP中,采用ViT和Text Transformer可以对每个图片token和文本token产出『专属』的embedding(可以认为是每个模态的细粒度局部信息)。
传统方法全局特征的相似性来建模跨模态(视觉和文本)交互,但这种方法常常会丢失重要信息。FILIP则通过细粒度的交互机制来克服这一限制:cross-modal late interaction 跨模态后期交互
最大值选取的核心意义(为什么不直接算平均值 / 求和?): 抓 “关键匹配”,忽略无效噪声。假设有一个猫的文字token,图片token依次有:猫、桌子、背景,相似度分别为0.9,0.2,0.3,取最大值说明有一个token匹配值到了0.9,可认为匹配上了,桌子、背景可以忽略,如果取平均值:$(0.9+0.2+0.3)/3=0.47$,匹配信号反而被拉低了。
CLIP维度:图片:$[B,L_I,d]$ , 文本:$[B,L_T,d]^T$ ,$L$维度上取mean,最后是$[B,d]\cdot [B,d]^T = [B,B]$
FILIP维度:图片:$[BL_I,d]$ x 文本:$[BL_T,d]^T$ = $[BL_I,BL_T]$ = $[B,L_I,B,L_T]$,依次横向(axis=3)取max,纵向(axis=1)取mean,这是整张图片对整行文本的最终相似度,对应图中的$S_{i,j}^I$;纵向取最大值,横向取mean,这是整行文本对整张图片的最终相似度,对应图片中的$S_{i,j}^T$。最后再整个构成$[B,B]$用来计算损失。
在线上应用时候,需要对每张图片都进行刷特征并且存入正排库,刷特征的时候需要对图片每个patch的特征都进行落盘,以便在线上进行交互时候使用。线上交互时可以计算和,然后以其平均值作为最终相似度(Query-图片搜索应用直接用即可)。显然,由于需要对图片patch特征都进行落盘到正排库,需要非常大量的正排存储资源,在实际落地的过程中也许会碰到一定的困难,需要进行工程上的优化。
如果是图搜(文搜图),则存储资源变为原来的$L_I$倍。
实际应用代码:
def simi(self, rep1, rep2):
batch_size1, n_token1, feat_dim = rep1.shape
_, n_token2, _ = rep2.shape
out = rep1.reshape([-1, feat_dim]) @ rep2.reshape([-1, feat_dim]).t()
# 重新调整形状以对应batch和token的维度
out = out.reshape([batch_size1, n_token1, -1, n_token2])
# 按行取最大值
out = out.max(axis=3) # 在第4个维度(n_token2)上取最大值
# 对这些最大值求平均
out = out.mean(axis=1) # 在第2个维度(n_token1)上取平均
return out
def forward(self, emb_img_token, emb_txt_token):
"""loss"""
emb_img_list = []
emb_txt_list = []
dist.all_gather(emb_img_list, emb_img_token)
dist.all_gather(emb_txt_list, emb_txt_token)
emb_img_all=paddle.concat(x=emb_img_list, axis=0)
emb_txt_all=paddle.concat(x=emb_txt_list, axis=0)
sim_i2t = self.simi(emb_img_token, emb_txt_all)
sim_t2i = self.simi(emb_txt_token, emb_img_all)
rank = dist.get_rank()
bs_img = emb_img_token.shape[0]
bs_txt = emb_txt_token.shape[0]
img_labels = (paddle.arange(bs_img) + bs_img * rank).astype('int')
text_labels = (paddle.arange(bs_txt) + bs_txt * rank).astype('int')
img_loss_constrastive = paddle.mean(nn.functional.softmax_with_cross_entropy(
sim_i2t / 0.07, img_labels.reshape([-1, 1])))
text_loss_constrastive = paddle.mean(nn.functional.softmax_with_cross_entropy(
sim_t2i / 0.07, text_labels.reshape([-1, 1])))
loss_constrastive = (img_loss_constrastive + text_loss_constrastive) * 0.5
return loss_constrastive
实际作为一个辅助损失去训练,也是可以有提升的。
Citation
About Me
个人博客:月源
知乎文章:月源
公众号:月源的算法仙蛊屋