数据集
WIT(WebImageText)包含4亿文本-图像对,从网络上搜索得到
预训练策略
这张图算是经典永流传了,对于输入的N个图像文本对,用左边矩阵的形式构建出N个正对和N2−N个负对,计算他们的余弦距离,并最小化正对的距离,最大化负对的距离具体的训练策略蛮有意思,他居然用的
# 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
这里面一些比较细节的东西:
- 没有像过去的自监督方法一样用非线性的分类层(从各模态特征层到对比特征层),而是直接用的线性层做到同一空间的映射(按照论文的说法是用起来没差,猜想应该是只在自监督的表征学习里面有用)
- 随机初始化,没有使用预训练模型(数据集大就是牛)
- 唯一的数据增强是随机裁剪
- 温度参数t是可学习的[[如何把一个向量(参数)写成可学习的?]]
放一段网络上的实现代码:
class CLIPModel(nn.Module):
def __init__(
self,
temperature=CFG.temperature,
image_embedding=CFG.image_embedding,
text_embedding=CFG.text_embedding,
):
super().__init__()
self.image_encoder = ImageEncoder()
self.text_encoder = TextEncoder()
self.image_projection = ProjectionHead(embedding_dim=image_embedding)
self.text_projection = ProjectionHead(embedding_dim=text_embedding)
self.temperature = temperature
def forward(self, batch):
# Getting Image and Text Features
image_features = self.image_encoder(batch["image"])
text_features = self.text_encoder(
input_ids=batch["input_ids"], attention_mask=batch["attention_mask"]
)
# Getting Image and Text Embeddings (with same dimension)
image_embeddings = self.image_projection(image_features)
text_embeddings = self.text_projection(text_features)
# Calculating the Loss
# 值得注意的是,这里代码直接做点乘,这应当基于各模态的embedding都已经做过了归一化,即除以了每个向量对应的L2范数,把计算余弦相似度的分母部分做掉了
logits = (text_embeddings @ image_embeddings.T) / self.temperature
# 这一段是计算一个label矩阵,这里与伪代码中有所不同
# 伪代码中的label是一个1xN的矩阵,用每个embedding的索引值表示它的类别
# 这里是分别从两个模态计算各自模态中embedding之间的余弦相似度,再做平均,感觉上核心的意义是类似的:让对角线的数值高,其余位置的数值低
images_similarity = image_embeddings @ image_embeddings.T
texts_similarity = text_embeddings @ text_embeddings.T
targets = F.softmax(
(images_similarity + texts_similarity) / 2 * self.temperature, dim=-1
)
# 计算Loss当然应该分不同的模态分别计算,由于上面定义label的方式不同,这里的loss计算也有所不同,当然targets转置不转置是一样的(其实感觉这个实现更合理一点),理论上按照他图里画的label应该是一个单位矩阵来着
texts_loss = cross_entropy(logits, targets, reduction='none')
images_loss = cross_entropy(logits.T, targets.T, reduction='none')
loss = (images_loss + texts_loss) / 2.0 # shape: (batch_size)
return loss.mean()
def cross_entropy(preds, targets, reduction='none'):
log_softmax = nn.LogSoftmax(dim=-1)
loss = (-targets * log_softmax(preds)).sum(1)
if reduction == "none":
return loss
elif reduction == "mean":
return loss.mean()
推理预测策略
假定推理的任务是对一张图片,判断其类别,CLIP的做法是计算图片的特征向量和文本的特征向量,然后依次计算图片特征和一系列文本特征的余弦相似度,取最像的一个类别
当然,在这种分类任务上需要对类别文本做一定处理,因为训练的时候输入是个句子,在有具体类别的数据集上进行推理时可以构造一个prompt,写成A photo of a {object}的格式。
模型选择
- Image Encoder选用了ResNet50和ViT做backbone,其中ResNet50进行了一些魔改(主要是EfficientNet),ViT魔改的少一点,在具体的实验里主要只是修改了图片划分patch的大小
- Text Encoder改的就更少了,基本上就是加了个LN和初始化技巧
- 值得注意的是他说用了masked self-attention,说是可以保留预训练模型的能力
这里应当注意的是将transformer搬过来做迁移,是只用decoder,且所有的self attention都做mask操作,不与encoder交互(把他丢掉!) - 长度是77,超过的截取,不够的padding
- 值得注意的是他说用了masked self-attention,说是可以保留预训练模型的能力