第一代 Qwen-VL 2023.08-2023.10
参考:
Qwen-VL看这一篇就够了
当时大多数的 LVLMs 都是以粗粒度 的方式感知图像,缺乏图像细粒度感知 的能力(包括目标定位 和文本读取 等)。基于当时的问题,Qwen 团队引入了一个新的视觉编码器 和位置感知适配器 ,并且设计了一个三阶段训练的流程用于优化 Qwen-VL 模型。Qwen-VL 的特点:性能领先、支持多语言、支持任意交错的 “图像-文本” 数据 、细粒度的视觉理解 (例如 OCR)。Qwen-VL 相较于之前的图文多模态大模型 多了一个功能:视觉定位,就是可以给出一个框 将你想要的地方框出来。
模型结构
通常一个多模态 “视觉—语言” 模型包含三个结构:语言模型、视觉编码器和 “视觉—语言” 适配器。
Qwen-VL 整个模型参数大致在 1.9B + 0.08B + 7.7B = 9.6B 的参数数量。
语言模型:Qwen-7B 大语言模型;
视觉编码器:ViT 的架构,参数量在 1.9B ,并且从开源项目 openclip 的 ViT-bigG 权重开始初始化,训练和推理的过程中图像会被调整到特定的分辨率,也就是拆成 14x14 像素的 patch 块;
(位置级)视觉语言适配器:一个随机权重初始化 的单层交叉注意力模块 组成,参数量在 0.08B 。
该模块使用一组可训练的向量(意思就是在训练中张量数值会改变,且梯度会流向这个向量) 作为 query 向量,将视觉编码器的特征 作为 key 进行交叉注意力操作,将图像特征压缩到 256 长度的序列。并且将 2D 绝对位置编码用在交叉注意力机制中,以减轻压缩过程中的位置细节丢失。
模型输入和输出 图像输入:<img>
和 </img>
标记图像的开始和结束。图片通过视觉编码器 和(位置级)视觉语言适配器 模块,得到一个定长的特征序列。为了和文字输入区别,图片特征前后分别加上 <img>
和 </img>
。
边界框输出:将边界框的值归一化在 [0,1000)
之间,并转换成特定的字符串格式 "(X_top_left, Y_top_left), (X_bottom_right, Y_bottom_right)"
,<box>
和 </box>
分别添加在边界框字符串的开头和结尾。
内容输出:<ref>
和 </ref>
标记边界框所引用的内容。
例如,某个任务的提示词:
1 <img >coyo700m/1 .jpg </img >Generate the caption in English with grounding:
Qwen-VL 的回答如下:
1 Beautiful shot of <ref>bees</ref><box>(661 ,612 ),(833 ,812 )</box><box>(120 ,555 ),(265 ,770 )</box> gathering nectars from <ref>an apricot flower</ref><box>(224 ,13 ),(399 ,313 ) </box><eos>
模型处理视觉信息的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 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]
Q:image = image[ : image.index(self.config.visual['image_start_id'] + 2)]
这段代码的作用?
A:因为模型在输入图片时,有时会预留更多 token 空间来填充图像信息,例如 padding 和 filter ,这就导致图像本身可能变长。这些填充的 token 确实是图像的一部分,但是输入到视觉编码器中产生干扰,因此需要额外再加一行代码对这些填充 token 做进一步过滤。
模型训练过程 第一阶段:预训练过程 使用互联网网页抓取的 ”图像—文本“ 对,50 亿条数据清洗后剩下 14 亿数据,其中 77.3% 为英文数据,22.7% 为中文数据。这一阶段冻结语言模型 ,训练视觉编码器和视觉语言适配器,输入图像调整为 224x224 的分辨率(按照每 14 像素分割后得到 16x16=256 个 patch),batch size 为 30720 ,训练 50000 步,使用 15 亿数据。
第二阶段:多任务预训练 加入了高质量、细粒度的图像和文本数据,使用了更大的分辨率和交错的 ”图像—文本“ 数据。在 7 个任务上对 Qwen-VL 进行训练。将视觉编码器的分辨率从 224x224 增加到 448x448,以减少图像下采样造成的信息损失。这一过程没有冻结任何模块。
第三阶段:监督微调 数据来自 LLM 生成 的图像标注或对话数据,这些数据通常只处理单图像对话和推理 ,且仅限于图像内容理解。
通过手动标注、模型生成和策略组合 ,构建了一个额外的对话数据集,以将定位和多图像理解能力 融入 Qwen-VL 模型中。在训练过程中混合了多模态和纯文本对话数据 ,以确保模型的对话能力具有普遍性。
指令微调数据量达到 35 万条。这一过程冻结视觉编码器。
模型代码应用 图片和文本的加载 1 2 3 4 query = tokenizer.from_list_format([ {'image' : 'https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen-VL/assets/demo.jpeg' }, {'text' : '这是什么' }, ])
图像到字符串的转换 Qwen-VL 将图片都处理成:“Picture 1”、“Picture 2”、“Picture 3” 等字符串 格式,并添加上图片的开始和结束 token ,文本直接拼接,box 的 ref 添加上开始结束符 拼接,box 坐标从数字整理成字符串格式 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 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
图像的编码 1 2 3 4 5 6 7 8 9 10 11 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)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 def forward (self, x: torch.Tensor ): x = x.to( dtype=self .transformer.get_cast_dtype(), device=self .transformer.get_cast_device(), ) x = self .conv1(x) x = x.reshape(x.shape[0 ], x.shape[1 ], -1 ) x = x.permute(0 , 2 , 1 ) x = x + get_abs_pos(self .positional_embedding, x.size(1 )) x = self .ln_pre(x) x = x.permute(1 , 0 , 2 ) x = self .transformer(x) x = x.permute(1 , 0 , 2 ) x = self .attn_pool(x) x = self .ln_post(x) x = x @ self .proj
图片是经过 resize 和归一化后输入 ViT 进行编码,ViT 编码后经过交叉注意力机制、归一化然后投影到 embedding 维度。
1 2 3 4 5 6 7 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, )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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 )