多模态ViLT模型下游任务微调原理及代码

前言:

        最近准备搞图文问答VQA, 恰巧多模态任务包括了NLP以及CV领域的知识,由于以前做过的一些项目不知道放到哪了,找起来也很麻烦,这篇论文可以帮助我很好的梳理NLP和CV故事线,对此进行总结,以防自己忘记 。代码比较粗糙准备在下一个版本进行改进。

为啥是这篇文章?

由于本来是学NLP的,所以一开始就接触了有关模型预训练和微调的相关概念。在NLP的世界里,transformer到来之后,出现了Bert模型,这模型可了得,好像大部分任务都可以加载Bert的权重文件并进行微调来达到自己的目的,有点像是一个内核的感觉。之后就开始研究一些CV领域的知识,巧了的就是当我看到了ViT这篇论文的时候,发现这不就是Bert吗!拿到了这个模型的权重之后试着做了分类任务,发现ViT也了得啊。来到了多模态任务时,一头雾水,查了多模态任务的前世今生,也翻了几篇论文,直到看到了这个图。

多模态ViLT模型下游任务微调原理及代码_第1张图片

这啥东西,右边一档就是Bert, 左边一挡就是ViT, 好家伙梦中情model啊, 索性准备复现一下。这问题不就来了吗,这源代码啥玩意啊看不懂,果然GitHub对小白是不友好的,直接上杀手锏抱抱脸,好在抱抱脸上有权重和配置文件,虽然不怎么会用,但是还是决定研究一番,看看这些东西都是什么牛马。

模型及配置文件

原论文刚好有VQA的模型,就不用加分类头了,直接用,git克隆下载下来了看。

多模态ViLT模型下游任务微调原理及代码_第2张图片

 config.json里面除了基本的模型参数还有两个参数:id2label和label2id。这俩东西是做VQA的时候最后输出的label, 字典形式的数字和字符的索引,这俩正好相反。

多模态ViLT模型下游任务微调原理及代码_第3张图片多模态ViLT模型下游任务微调原理及代码_第4张图片

preprocessor_config.json里面是图片模块的配置文件,tokenizer_config.json里面是文本模块的配置文件, bin文件是权重文件,其他的是词表分词器啥的暂时问题不大。

所有下载下来这一大堆这两行代码就把模型和预处理器整来了。

processor = ViltProcessor.from_pretrained("./vilt-b32-finetuned-vqa")
model = ViltForQuestionAnswering.from_pretrained("./vilt-b32-finetuned-vqa") 

debug一下,model是由两块组成:base_model和classifier(主体ViLT+分类头) ,最后输出的向量是3129维,也就是id2label和label2id字典长度。model没什么好说的,当一个黑箱子就行了,知道输入是processor预处理后的文字和图片,输出是3129维取最大的一个就行了,输出通过.logits索引就行了。

还得来看这个processor,其实抱抱脸大部分模型都提供了预处理器,形式都大同小异,基本都是根据上面的config文件调用pre_trained方法来获得。ViLT的processor用的时候必须要传入图像数据和文本数据,基本形式就是这样的。

encoding = processor(image, text, return_tensors="pt")
outputs = model(**encoding)

其中image和text可以是(图片,列表,张量),所以就拟定image是图片列表形式, text是列表形式。把三张图片拼成一个列表,三句问题拼成一个列表,输入processor中,看看encoding是个啥东西?

多模态ViLT模型下游任务微调原理及代码_第5张图片

 原来预处理之后的东西在encoding.data下, 前三个是文本的处理结果,后俩是图片的,细化一下就是input_ids是文本预处理结果,pixel_values是图片预处理结果,其他三个是和模型是否计算注意力有关,这三个看似不重要的地方才是processor的强大之处,输入进去自动识别字符,要不要算注意力,可以说是手撸transformer时候最细节的东西。反正有了processor就不需要我们考虑了。

然后看看demo结果,输入就是这,图片和文字列表

image1 = Image.open('./1.png')
text1 = "what is this people doing?"
image2 = Image.open('./0.PNG')
text2 = "is this people beautiful?"
image3 = Image.open('./2.png')
text3 = "how old is this people?"

text = [text1, text2, text3]
image = [image1, image2, image3]

 输出logits看一下,选3192概率最大的,再把数字转字符串得到结果

多模态ViLT模型下游任务微调原理及代码_第6张图片

 demo做完了,准备做一下微调了该,训练代码依照上述原理就可以成型。

模型微调

我这个微调不是真正意义的微调,还是用三个样本进行训练,V1.0版本主要实现了:

1. 固定格式的图片和文字输入

2. 如何通过自定义Dataset输入实现batch输入

3. 完成整体训练框架并使用GPU加速训练

"""

ViLT Model train V1.0

"""

from torch import nn, optim
from torch.utils.data import DataLoader
from torch.utils.data.dataset import Dataset
import torch
from torchvision import transforms
from transformers import ViltProcessor, ViltForQuestionAnswering
from PIL import Image

image1 = Image.open('./1.png').resize((384, 384))
text1 = "what is this people doing?"
label1 = "sitting"
image2 = Image.open('./0.PNG').resize((384, 384))
text2 = "is this people beautiful? "
label2 = "yes"
image3 = Image.open('./2.png').resize((384, 384))
text3 = "how old is this people?"
label3 = "30"

# 数据需要满足如下形式
text = [text1, text2, text3]
image = [image1, image2, image3]
label = [label1, label2, label3]

processor = ViltProcessor.from_pretrained("./vilt-b32-finetuned-vqa")  # VILT模型预处理器
model = ViltForQuestionAnswering.from_pretrained("./vilt-b32-finetuned-vqa").cuda()  # VILT预训练模型
loss_function = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.classifier.parameters(), lr=0.001)


class MyData(Dataset):
    def __init__(self, text, image, label):
        super(MyData, self).__init__()
        self.text = text
        self.image = image
        self.label = label

    # Dataset不能返回图片形式, 所以将图片先变为tensor
    def __getitem__(self, item):
        text = self.text[item]
        image = self.image[item]
        transform = transforms.ToTensor()
        image = transform(image)
        label = self.label[item]

        return text, image, label

    def __len__(self):
        return len(self.label)


#
dataset = MyData(text, image, label)
dataloader = DataLoader(dataset, batch_size=3, shuffle=True)
#


for epoch in range(10):
    model.train()
    train_loss = 0.0
    val_loss = 0.0
    train_acc = 0.0
    val_acc = 0.0
    # 预处理器需要的其中一种是列表形式, 将图像文本标签全部转换
    for i, (text, image, label) in enumerate(dataloader):
        text_list = list(text)
        label = list(label)
        # 图片完全可以直接输入, 为了做batch暂时想到的一种解决方案
        image_list = []
        label_list = []

        for idx in label:
            each_label = model.config.label2id[idx]
            label_list.append(each_label)

        for n in range(len(image[:, 0, 0])):
            each_image = image[n, :, :]
            image_list.append(each_image)

        labels = torch.tensor(label_list)
        # 做batch就是有很多维度不匹配的坑, padding=True就是文本的坑, 自己做padding的话attention_mask就很复杂
        encoding = processor(image_list, text_list, return_tensors="pt", padding=True)
        # 文本图片标签to_device, 文本和图片特征全部存放在encoding.data里面
        labels = labels.cuda()
        for feature, data in encoding.data.items():
            encoding.data[feature] = data.cuda()

        outputs = model(**encoding)
        logits = outputs.logits  # [batch_size, 3129]

        optimizer.zero_grad()
        loss = loss_function(logits, labels)
        loss.backward()
        optimizer.step()
        train_loss += loss.item() * len(label)
        ret, predictions = torch.max(logits.data, 1)
        correct_counts = predictions.eq(labels.data.view_as(predictions))
        acc = torch.mean(correct_counts.type(torch.FloatTensor))
        train_acc += acc.item() * len(label)

        # for k in range(len(logits[:, 0])):
        #     idx = logits[k, :].unsqueeze(dim=0).argmax(1).item()
        #     answer = model.config.id2label[idx]
        #     print("Predicted answer:", answer)
    avg_train_loss = train_loss / len(dataset)
    avg_train_acc = train_acc / len(dataset)
    print(avg_train_loss, avg_train_acc)

 总结改进

1. 做batch时由于维度不匹配的问题强行将图片resize成384*384,感觉还是有影响的。由于抱抱脸的processor还是非常强大,因为做batch维度和数据类型的限制,导致图片预处理可能会有bug。还得研究咋回事。

2. 下载数据集做微调和测试

3. 代码写的太繁琐,理论上应该有简洁的办法,就是暂时没找到。

你可能感兴趣的:(python,人工智能)