今天来看看阿里第一代图文多模态大模型Qwen-VL的技术报告,看看里面有哪些值得学习的地方。Qwen-VL相较于之前的图文多模态大模型多了一个功能:视觉定位,就是可以给出一个框将你想要的地方框出来,算是一个小的创新点吧。
一、引言和背景
LVLMs已经出现了不少的开源工作,但是开源工作整体上还是落后于专有模型。大多数的LVLMs都是以粗粒度的方式感知图像,缺乏图像细粒度感知的能力(包括目标定位和文本读取等)。
因为上面的问题,基于qwen阿里开发了Qwen-VL模型,引入了一个新的视觉编码器和位置感知适配器,并且设计了一个三阶段训练的流程用于优化qwen-vl模型。qwen-vl的特点:性能领先、支持多语言、支持任意交错的图像-文本数据、细粒度的视觉理解。
相关工作中提到了clip、blip、blip2、llava等模型,Qwen-VL工作的意义就是将图像描述、视觉问答、OCR、文档理解和视觉定位能力整合到一个模型中,并且取得了不错的效果。更多是能力上的提升,没有看到学术创新(模型结果、数据构建)。
二、模型架构
在这里插入图片描述

模型结构和一般的vllm一样,包含三部分:语言模型、视觉编码器和视觉语言适配器。
语言模型没什么可说的,就是qwen。
视觉编码器是vit的架构,并且从Openclip 的 ViT-bigG权重开始初始化,训练和推理的过程中图像会被调整到特定的分辨率。视觉编码器通过将图像拆分为步长为14的patch处理,生成图像特征。
视觉语言适配器,是一个随机初始化的单层交叉注意力模块组成,该模块使用一组可训练的向量作为查询向量,将视觉编码器的特征作为key进行交叉注意力操作,将图像特征压缩到256长度的序列。并且将2D绝对位置编码用在交叉注意力机制中,以减轻压缩过程中的位置细节丢失。
图像输入使用 和 标记图像的开始和结束。
边界框的输入,将边界框的值归一化在[0,1000)之间,并转换成特定的字符串格式:“(Xtoplef t, Ytoplef t),(Xbottomright, Ybottomright)”,当作普通的文本进行分词。 和 分别添加在边界框字符串的开头和结尾, 和 标记边界框所引用的内容。
三、训练过程
在这里插入图片描述

训练主要分为三个阶段:两个预训练阶段和一个指令微调阶段。
1、预训练
这一阶段使用和互联网网页抓取的图像-文本对,50亿条数据清洗后剩下14亿数据,其中77.3% 为英文数据,22.7% 为中文数据。
这一阶段冻结语言模型,训练图像编码器和连接层,输入图像调整为224*224的分辨率,batch size为30720(阿里还是有钱),训练50000步,使用15亿数据(之前说剩下14亿数据,这里使用是15亿数据,不清楚是不是错误)。
2、多任务预训练
在这里插入图片描述

第二阶段多任务预训练,加入了高质量、细粒度的图像和文本数据,使用了更大的分辨率和交错的文本-图像数据。如上表所示,在7个任务上对qwen-vl进行训练。数据集的信息和构建方法就不介绍了,感兴趣的可以去看原文。将视觉编码器的分辨率从224 × 224 增加到 448 × 448,以减少图像下采样造成的信息损失。
在这里插入图片描述
在这里插入图片描述

这里使用896×896的分辨率太慢,而使用窗口注意力机制的时候损失会明显升高,所以选择了448 × 448的原始注意力机制。
这个过程整体都参与训练。
3、监督微调
这个过程主要进行指令微调,增强指令跟随和对话能力。数据来自LLM 生成的图像标注或对话数据,这些数据通常只处理单图像对话和推理,且仅限于图像内容理解。通过手动标注、模型生成和策略组合,构建了一个额外的对话数据集,以将定位和多图像理解能力融入 Qwen-VL 模型中。在训练过程中混合了多模态和纯文本对话数据,以确保模型的对话能力具有普遍性。指令微调数据量达到 35 万条。
这一过程冻结视觉编码器。
四、评估
评估这里就不说了,就是qwen-vl的能力优于其他模型。但是不知道为什么没有和llava模型进行对比,后面被llava 1.5在文章中还diss了一下。
五、代码
1、数据处理
query = tokenizer.from_list_format([
{‘image’: ‘https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen-VL/assets/demo.jpeg’},
{‘text’: ‘这是什么’},
])
def from_list_format(self, list_format: List[Dict]):
text = ‘’
num_images = 0
for ele in list_format:
if ‘image’ in ele:
num_images += 1
text += f’Picture {num_images}: ’
text += self.image_start_tag + ele[‘image’] + self.image_end_tag
text += ‘\n’
elif ‘text’ in ele:
text += ele[‘text’]
elif ‘box’ in ele:
if ‘ref’ in ele:
text += self.ref_start_tag + ele[‘ref’] + self.ref_end_tag
for box in ele[‘box’]:
text += self.box_start_tag + ‘(%d,%d),(%d,%d)’ % (box[0], box[1], box[2], box[3]) + self.box_end_tag
else:
raise ValueError("Unsupport element: " + str(ele))
return text
qwen-vl将图片都处理成:Picture 1、Picture 2、Picture 3等字符串格式,并添加上图片的开始和结束token,文本直接拼接,box的ref添加上开始结束符拼接,box坐标从数字整理成字符串格式。
2、模型结构
语言模型的部分就不说了,主要看图像处理的部分。
if past_key_values is None and torch.any(input_ids == self.config.visual[‘image_start_id’]):
bos_pos = torch.where(input_ids == self.config.visual[‘image_start_id’])
eos_pos = torch.where(input_ids == self.config.visual[‘image_start_id’] + 1)
assert (bos_pos[0] == eos_pos[0]).all()
img_pos = torch.stack((bos_pos[0], bos_pos[1], eos_pos[1]), dim=1)
images = []
for i, a, b in img_pos:
image = input_ids[i][a + 1 : b - 1].tolist()
image = image[ : image.index(self.config.visual[‘image_start_id’] + 2)]
images.append(bytes(image).decode(‘utf-8’))

        images = self.visual.encode(images)
        assert images.shape[0] == len(images)
        fake_images = None

if fake_images is not None:
hidden_states = hidden_states + images.mean()0
elif images is not None:
for idx, (i, a, b) in enumerate(img_pos):
hidden_states[i][a + 1 : b] = images[idx]
这里是将编码后的图像特征直接替换掉之前的占位特征,和文本embedding进行拼接。
接下来看看图像的编码:
def encode(self, image_paths: List[str]):
images = []
for image_path in image_paths:
if image_path.startswith(“http://”) or image_path.startswith(“https://”):
image = Image.open(requests.get(image_path, stream=True).raw)
else:
image = Image.open(image_path)
image = image.convert(“RGB”)
images.append(self.image_transform(image))
images = torch.stack(images, dim=0)
return self(images)

def forward(self, x: torch.Tensor):
x = x.to(
dtype=self.transformer.get_cast_dtype(),
device=self.transformer.get_cast_device(),
)
# to patches
x = self.conv1(x) # shape = [
, width, grid, grid]
x = x.reshape(x.shape[0], x.shape[1], -1) # shape = [, width, grid ** 2]
x = x.permute(0, 2, 1) # shape = [
, grid ** 2, width]

    x = x + get_abs_pos(self.positional_embedding, x.size(1))

    x = self.ln_pre(x)

    x = x.permute(1, 0, 2)  # NLD -> LND
    x = self.transformer(x)
    x = x.permute(1, 0, 2)  # LND -> NLD

    x = self.attn_pool(x)
    x = self.ln_post(x)
    x = x @ self.proj

图片是经过resize和归一化后输入vit进行编码,vit编码后经过交叉注意力机制、归一化然后投影到embedding维度。接下来看看这个交叉注意力机制的计算:
self.attn_pool = Resampler(
grid_size=int(math.sqrt(n_queries)),
embed_dim=output_dim,
num_heads=output_dim // 128,
kv_dim=width,
norm_layer=norm_layer,
)

def forward(self, x, attn_mask=None):

    pos_embed = get_abs_pos(self.pos_embed, x.size(1))

    x = self.kv_proj(x)
    x = self.ln_kv(x).permute(1, 0, 2)

    N = x.shape[1]
    q = self.ln_q(self.query)
    out = self.attn(
        self._repeat(q, N) + self.pos_embed.unsqueeze(1),
        x + pos_embed.unsqueeze(1),
        x,
        attn_mask=attn_mask)[0]
    return out.permute(1, 0, 2)

交叉注意力机制的计算方法基本和报告中一致,vit的输出加上位置编码后再和query矩阵计算交叉注意力。
之前有很多文章说qwen-vl用的是q-former的架构,这里我看并不是用了q-former的架构,只能说是用了q-former的的思想,都有一个query向量,但是q-former是一个多层的transformer架构,比较复杂(blip2-opt-2.7b包含12层的transformer,感兴趣的可以去看看源码,transformers库也集成了这个模型),这里只是用了一个交叉注意力层,简化了很多,已经有点接近于llava的架构了。llava把query矩阵和交叉注意力都省略了,更简洁,效果也更好。
六、总结
本来看别的技术文章说qwen-vl就是用了q-former的架构,感觉就是阿里财大气粗,用更多的数据把别人的架构又训练了一遍。通过文章和代码来看,qwen-vl在模型结构上还是有一定的创新的,至少简化了模型,和llava已经比较接近了。数据方面,还是羡慕阿里爸爸财大气粗啊,第一阶段15亿的数据,30720的batch size,后面分辨率的提升都是卡啊,希望后面也能去阿里这种大厂,感受一下几百上千张卡训练几十亿数据是什么感觉。

Logo

ModelScope旨在打造下一代开源的模型即服务共享平台,为泛AI开发者提供灵活、易用、低成本的一站式模型服务产品,让模型应用更简单!

更多推荐