QwenVL系列多模态模型学习笔记_第1篇

第一代 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 的参数数量。

  1. 语言模型:Qwen-7B 大语言模型;

  2. 视觉编码器:ViT 的架构,参数量在 1.9B ,并且从开源项目 openclip 的 ViT-bigG 权重开始初始化,训练和推理的过程中图像会被调整到特定的分辨率,也就是拆成 14x14 像素的 patch 块;

  3. (位置级)视觉语言适配器:一个随机权重初始化单层交叉注意力模块组成,参数量在 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
# 确保只在第一次 forward 时处理视觉信息(past_key_values is None 表示不是缓存推理时)
# 图像 token 是以特殊的 image_start_id 开始,检测是否存在
if past_key_values is None and torch.any(input_ids == self.config.visual['image_start_id']):

# 找到图像 token 的边界位置
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:(batch_idx, start_idx, end_idx)
img_pos = torch.stack((bos_pos[0], bos_pos[1], eos_pos[1]), dim=1)
images = []
for i, a, b in img_pos:

# 截取 patch token(跳过起始和终止标志)
image = input_ids[i][a + 1 : b - 1].tolist()
# 截取图片结束 token
image = image[ : image.index(self.config.visual['image_start_id'] + 2)]
# 解码为 utf-8 图像数据(说明 image token 实际是原始图片字节的编码)
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

# 将图像嵌入写入对应位置 a+1 : b 的 hidden state(对应 patch tokens)
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: # 每个 ele 都是字典
if 'image' in ele:
# 图片处理成这样的字符串,再加上图像自身、开始和结束的 tokens
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:
# 如果是定位框,先考虑有没有参考对象
# 如果有的话,先添加参考对象自身字符串、开始和结束的 tokens
# 没有的话,添加定位框自身字符串、开始和结束的 tokens
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(),
)
# 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 维度。

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)

QwenVL系列多模态模型学习笔记_第1篇
http://example.com/2025/06/25/QwenVL系列多模态模型学习笔记-第1篇/
作者
Jinbiao Zhu
发布于
2025年6月25日
许可协议