基于梯度的方法
前言
上周因为准备毕设以及ECCV开题所以gap了一周,主要做了一些相关的代码阅读和复现(还有摸鱼)。本周会继续进行Multi-Modal Prompt Engineering领域的调研。
什么是Knowledge Guidance
其实这种guidance的做法不是第一次见到了,在之前做生成任务的调研时,文生图就很常用guidance的方法,来将生成器的生成内容引导向一个正确的目标。在文生图中,有通过classifier+gradient的方法来进行约束,还有一些其他的做法不赘述,也就是说,可以通过一个先验的方向来对后续的学习进行一个引导,这当然也可以用于提示词学习!
Prompt-aligned Gradient for Prompt Tuning(ICCV 23/CORR 22)
这是一个2022年的工作,但是在2023年又中了ICCV
Motivation
本文的Motivation主要还是基于CoOp来的,主要有两点。首先看ab图,CoOp存在随着epoch增加严重掉点的问题(本质上应该还是过拟合),然后看cd图,如果不进行数据增强或提供足够的样本(更多shots),CoOp可能无法改进CLIP
此外,还有一个有趣的Motivation是,通过Grad-CAM来可视化时发现,经过CoOp微调过的prompt会导致模型更关注背景而不是前景对象,这和分类任务是背道而驰的(值得注意的是,这篇工作已经默认在分类任务下去做了)
也就是上图中的(b)和(c)的区别。在CLIP中,本身更集中在foreground上的注意力被分散了。
这会导致过拟合现象严重,而且在样本数特别少的时候过拟合的更严重(因为分布偏离的更多)
Contribution
本文提出了一种基于prompt对齐的梯度的引导方法,叫做ProGrad,来应对prompt学习中添加的不正确偏置。ProGrad的主旨是在tuning的过程中进行一种正则化,来确保这一步的tuning不应该和原本的知识产生冲突。对于“原本的知识”,本文给出的例子是zero-shot CLIP给出的预测。具体的,本文通过一个KL散度来将ZSCLIP和本身正在做prompt tuning的预测进行对齐,学习到的方向叫做一般方向(general direction),还有一个是基于交叉熵的方向,成为域特殊方向(domain-specific direction),然后将域特殊方向解构到一个垂直的向量 G ⊥ G_{\perp} G⊥和一个平行的向量 G ∥ G_\Vert G∥。垂直向量并不能覆盖原本的向量,而平行向量要么是和一般方向是相同的,要么是和一般方向相反的。如上面的gradcam图显示,不同方向的的direction都能够起到正则化的作用,而综合起来能够有更好的效果。
综合的Contribution有以下三点:
- ProGrad能够做到很好的精度效果提升与CoOp比
- 能够提升泛化能力与CoCoOp比
- 在域泛化下有很好的效果
具体方法
这部分草稿被删了,现在省略的重新讲一下。最基本的思路就是,将学习的时候的梯度进行结构,首先是由CoOp进行学习的域特殊方向,这个方向是能够加强其在当前数据下的精度的优化方向,但是这可能导致过拟合,所以用一个一般普通的prompt和zero-shot CLIP 学习的logits和原本的logits计算一个KL散度,这个KL散度回传的梯度作为一般方向。公式化的表述如下:
首先有交叉熵损失:
和两个模型的logits的KL散度:
然后将这两个损失转化成相应的回传的梯度,前者对应
G
g
G_g
Gg,后者对应
G
d
G_d
Gd
然后将两个梯度的方向进行比较,这里会分成两种情况,可以参考下图共同理解。第一种情况是夹角小于九十度,也就是余弦相似度为正,此时直接用
G
g
G_g
Gg即CoOp的交叉熵损失进行优化。第二种情况下则是将梯度映射到和KL散度垂直的角度,这样可以保证优化方向不会在冲突的方向上进行。
公式化描述则为
注意其中的
λ
\lambda
λ使得这个修正的程度可调整,
λ
=
1
\lambda=1
λ=1代表映射到垂直方向梯度,
λ
=
0
\lambda=0
λ=0则本方法退化到CoOp
实验部分
主要的核心代码是梯度回传部分
def prograd_backward_and_update(
self, loss_a, loss_b, lambda_=1, names=None
):
# loss_b not increase is okay
# loss_a has to decline
self.model_zero_grad(names)
# get name of the model parameters
names = self.get_model_names(names)
# backward loss_a
self.detect_anomaly(loss_b)
loss_b.backward(retain_graph=True)
# normalize gradient
b_grads = []
for name in names:
for p in self._models[name].parameters():
b_grads.append(p.grad.clone())
# optimizer don't step
for name in names:
self._optims[name].zero_grad()
# backward loss_a
self.detect_anomaly(loss_a)
loss_a.backward()
for name in names:
for p, b_grad in zip(self._models[name].parameters(), b_grads):
# calculate cosine distance
b_grad_norm = b_grad / torch.linalg.norm(b_grad)
a_grad = p.grad.clone()
a_grad_norm = a_grad / torch.linalg.norm(a_grad)
if torch.dot(a_grad_norm.flatten(), b_grad_norm.flatten()) < 0:
p.grad = a_grad - lambda_ * torch.dot(
a_grad.flatten(), b_grad_norm.flatten()
) * b_grad_norm
# optimizer
for name in names:
self._optims[name].step()
kgCoOp
这篇工作的motivation和ProGrad是接近的,主要的工作会在这两者的对比下进行描述
ProGrad中的不足
prograd的方法有个核心的点,是对于冲突的部分采取了丢弃的处理,这会使得在学习的过程中会产生很多无用的信息(其实我个人在理解ProGrad的时候,觉得最主要的问题是,ProGrad的学习是通过和预先构建好prompt的CLIP去做KL散度的,那和coop的motivation提到的一样,既然先验的prompt会导致学习的梯度方向变化,那prompt的设计对于结果是很敏感的,如果是一个很差的prompt,会导致其效果甚至整体变差。
于是KgCoOp的核心思想就是不舍弃原本知识的情况下确保和general knowledge不偏离太多
提出的方法
首先基本的设计还是在CoOp的架构上来进行的,所以这部分内容不再赘述。而在KgCoOp中,主要也是解决了CoOp的过拟合问题。该部分首先先详细的讲了一下CoOp的过拟合现象,在base2new问题上效果很差。然后提出了KgCoOp,基本的思路和ProGrad是类似的
做法出奇的简单,就是在原本的交叉熵损失基础上,加入了一个新的l2范数损失,有
最终有
L
=
L
c
e
+
λ
L
k
g
\mathcal L=\mathcal L_{ce}+\lambda\mathcal L_{kg}
L=Lce+λLkg
注意,这里只是对文本模态的编码,基于zsclip和实际文本输出进行的l2损失。
在kgcoop代码中,其实现直接在CLIP中实现:
# prompt learner: promptsa之后用于构建old_text_features
#prompts_ = [prompt_prefix + " " + name + "." for name in classnames]
temp = CUSTOM_TEMPLATES[cfg.DATASET.NAME]
prompts_ = [temp.format(c.replace("_", " ")) for c in classnames] # choose prompts from temp
print(f"Prompts: {prompts_}")
prompts_ = torch.cat([clip.tokenize(p) for p in prompts_])
prompts_ = prompts_.cuda()
# CustomCLIP: 最终的输出是logits,但是kgCoOp会对这个logits的计算进行修正
def forward(self, image):
prompts = self.prompt_learner()
image_features = self.image_encoder(image.type(self.dtype))
tokenized_prompts = self.tokenized_prompts
text_features = self.text_encoder(prompts, tokenized_prompts)
text_features_old = self.ori_embedding
image_features = image_features / image_features.norm(dim=-1, keepdim=True)
text_features = text_features / text_features.norm(dim=-1, keepdim=True)
logit_scale = self.logit_scale.exp()
logits = logit_scale * image_features @ text_features.t()
cos = torch.nn.CosineSimilarity(dim=1,eps=1e-07)
text_features_old = text_features_old / text_features_old.norm(dim=-1, keepdim=True)
score = cos(text_features,text_features_old)
# calculate the mean of the cosine similarity
# between text_feature and text_feature_old
score = 1.0-torch.mean(score)
return logits, score
# kgcoop.forward_backward(): 基于logits和score进行整体计算
output,score = self.model(image)
loss = F.cross_entropy(output, label)+self.w*score
self.model_backward_and_update(loss)
Self-regulating Prompts: Foundational Model Adaptation without Forgetting (ICCV23)
abstract
这一篇工作是在MaPLe基础上进行推进的,但是整个方法做的就比较复杂了。基本的问题还是一致的,即模型在提高特定任务性能的情况下不损失泛化性。基于此本文提出的自正则化的prompt,本方法主要分为了三个分支
a. 基于Mutual Agreement Maximization的正则化:通过最大化prompt的特征和frozen VLM的特征来实现
b. 基于self-ensemble(自集成)的方法:采用一个基于加权的提示词聚合方法来实现
c. 通过文本多样性来实现正则化:这个Motivation很有意思,作者注意到标签和图像是不对等的,因为一个分类有很多的图像与之对应,但是一个图像不会有很多的label(分类任务中),所以在文本label会欠缺多样性
Contribution
- 通过self-regularization的方法防止了过拟合,提升了泛化性。通过最大化mutual-agreement,同时学习到了task-specific和任务无关的泛化知识(这里我觉得很奇怪,我不认为用于微调的数据能够让模型学到更多的“泛化知识”,而是能够在当前领域下能够不集中在)
- 提出了一种基于加权的self-ensembling的策略,来捕捉互补的特征,同样是能够提高泛化性能