PaperReading:Qwen series
paperreading
本文字数:4.4k 字 | 阅读时长 ≈ 18 min

PaperReading:Qwen series

paperreading
本文字数:4.4k 字 | 阅读时长 ≈ 18 min

千问是阿里的开源大模型系列,目前基本处于国内开源模型中的第一梯队,下面对 Qwen 系列进行讨论

Qwen

这篇技术报告一共提出了以下几个 Qwen 系列模型: Qwen,Qwen-Chat,Code-Qwen,Code-Qwen-Chat,Math-Qwen-Chat,其中 Chat 模型是经过 RLHF 得到的

1. 分词器

用 BPE 算法,对中英文以及其他语言进行训练,总共的词表为 152K,这里 tokenzier 的压缩率比其他模型的高,意味着更少的 token 可以传递更多的信息

2. 模型结构

模型结构和 LLaMA 很像,但是有以下区别

QwenLM 预训练系列如下

3. 训练

总 tokens: 1.8B,7B 和 14B 分别为 2.2T,2.4T 和 3.0T

训练数据包含公共网络文档,百科全书,书记,代码等等,训练数据是多语种的,中英文占很大比重

使用自回归的方式训练 QwenLM,上下文长度为 2048,在 attention 模块中使用 Flash Attention,使用 AdamW 优化器,$\beta_{1}=0.9$ $\beta_{2}=0.95$ $\epsilon=10^{-8}$,使用余弦退火,最小学习率是峰值的 0.1,使用 BF16 进行训练

在 Attention 的时候使用 NTK 插帧技术对 RoPE 进行插值

Qwen2

本篇技术报告一共发布了四个 dense models,Qwen-0.5B,Qwen-1.5B,Qwen-7B,Qwen-72B,此外还有 57B 的 MoE 模型,所有的模型都在 7T tokens 上训练的

1. 分词器

和 Qwen 相同,包含 151643 个 regular tokens 以及 3 个 control tokens

2. 模型结构

Qwen2 Dense Model

Qwen2 Mixture-of-Experts Model

模型的详细参数如下,其中 Qwen2-57B-A14B 是从 Qwen2-7B 继承而来

3. 训练

总 tokens: 0.5B,1.5B,7B,72B 和 57B-A14B 分别为 12T,7T,7T,7T,4.5T

Qwen2 专注于数据集的 refine 以及 context lengths 的处理

训练数据集

long-context 的训练: 使用 YARN 和 DCA 的训练策略,可以让模型可处理的 token 拓展到 131072 个

Qwen-VL

1. 分词器

添加的特殊 token 一共有 6 个 <img> </img> <box> </box> <ref> </ref>

2. 模型结构

3. 训练

总图文: 一二三阶段分别为 1.4b, 78M, 350K 图文

训练分三阶段,其中经过 multi-task pre-training 的是 Qwen-VL 模型,经过 Supervised Fine-tuning 的是 Qwen-VL-Chat 模型

训练的整个流程如下所示

Qwen2-VL

Qwen2-VL 引入了 Naive Dynamic Resolution 的机制,确保模型可以动态的处理变化的分辨率,并且集成了 Scaling laws

Qwen2-VL 可以理解图片和视频

1. 分词器

2. 模型结构

处理图片和视频均采用 675M 的 ViT,LLM 采用 Qwen2

下面分别介绍这三个模块

Naive Dynamic Resolution

这个模块参考的是 Google DeepMind 的一个工作,叫做 NaViT,这个工作主要是为了解决 ViT 在推理时不能接受不同分辨率输入的问题

其实本质上来讲,ViT 可以接受任意分辨率的输入,但是因为位置编码的问题,所以不能直接使用,需要进行一些处理

  1. ViT理论上可以推理任意分辨率吗?理论上,ViT 的 patch embedding 和主干 Transformer 都不限制输入 patch 数,只要显存够,序列多长都可以。但是ViT 的**位置编码(positional embedding)**通常是训练时以固定尺寸(比如 14×14=196 个 patch)初始化和学习的。推理时,如果输入 patch 数量变了,就要插值 position embedding(比如把 14×14 插值成 16×16)。插值虽然能凑用,但会带来“语义漂移”,模型泛化能力会变差,尤其是分辨率跨度较大、长宽比变化时,模型性能大概率明显下降。

  2. ViT为何不能直接支持原生分辨率推理?ViT 只在特定分辨率训练过,比如 224×224,模型学到的空间结构、感知能力都被限定在这个分辨率和 patch 布局下。推理时直接用原生分辨率,意味着输入 patch 布局和训练时完全不同,很多位置编码参数没被充分学习过,模型难以泛化。实验事实:ViT 直接用大/小分辨率推理,或者输入不同长宽比,准确率、鲁棒性会大幅下降(详见 ViT/DeiT 论文附录和许多 benchmark 对比)。

  3. 为什么NaViT/Patch n’ Pack训练方法能解决这个问题?NaViT从一开始就支持任意分辨率、长宽比训练,通过Packing和灵活的位置编码(factorized/fractional 2D embedding),模型在训练时见过各种分辨率/长宽比的图片,空间表征能力被极大提升。位置编码被设计得可以泛化到新尺寸,而不是只会“插值凑合”。所以,NaViT在推理时用原生分辨率,性能和鲁棒性远高于普通ViT(具体可见NaViT和ViT在ImageNet、ObjectNet、ImageNet-A等多分辨率benchmark上的对比)。

  4. 举个实际例子:ViT 训练分辨率 224×224,推理 256×256 时,需要把 1D/2D position embedding 插值,性能通常会掉 3~5 个点甚至更多。NaViT 训练过程中本身就暴露了各种分辨率,模型天然适应多样输入,推理时可直接用原生分辨率,性能很稳。

  5. 一句话总结/本质区别:普通 ViT 在推理时虽然理论上能处理任意分辨率,但实际上没有泛化能力,依赖插值位置编码,效果差。NaViT 专门为多分辨率/长宽比设计了训练机制,模型和位置编码都能自适应多样输入,推理原生分辨率才真正 work。

下面是一个伪代码

batch_images = [img1, img2, img3, ...]  # 多张原图
packed_tokens = []
image_slices = []
max_seq_len = 512

for img in batch_images:
    # 1. patchify(假设是16×16的patch)
    patches = patchify(img, patch_size=16)
    # 2. 用patch embedding卷积生成token
    tokens = patch_embedding(patches)  # shape: [num_patches, emb_dim]
    # 3. 记录每张图token的起止索引
    start_idx = len(packed_tokens)
    packed_tokens.extend(tokens)
    end_idx = len(packed_tokens)
    image_slices.append((start_idx, end_idx))

# 4. padding补齐
while len(packed_tokens) < max_seq_len:
    packed_tokens.append(pad_token)

# 5. 生成attention mask(让不同图片的token互不注意,pad token也mask掉)

# 6. 输入到Transformer
output = transformer(packed_tokens, attention_mask)

Multimodal Rotary Position Embedding (M-RoPE)

在多模态大模型时代,如何让模型理解文本、图片、视频等不同输入的位置信息,是 Transformer 架构面临的重要挑战。传统的大模型,比如 LLM(大语言模型)中用的是一维旋转位置编码(1D-RoPE),这种方式只能编码序列(如文本)的位置信息,对于图片和视频这样拥有二维甚至三维空间结构的数据就不够用了。

M-RoPE的创新点

M-RoPE(Multimodal Rotary Position Embedding,多模态旋转位置编码)应运而生。它的核心思想,就是将传统RoPE的位置编码从一维拆解为三维,分别对应:

不同模态下的M-RoPE怎么用?

M-RoPE带来了什么好处?

伪代码

def m_rope_embedding(token, mode):
    if mode == "text":
        pos_id = token_idx  # 1D
        embedding = rope_1d(pos_id)
    elif mode == "image":
        temporal_id = 0
        height_id = row_idx
        width_id = col_idx
        embedding = rope_3d(temporal_id, height_id, width_id)
    elif mode == "video":
        temporal_id = frame_idx
        height_id = row_idx
        width_id = col_idx
        embedding = rope_3d(temporal_id, height_id, width_id)
    return embedding

例子:文本、图片、视频的M-RoPE编码

1. 文本(Text)

假设有一句话:“Hello world”,分词后有5个token,index分别是0~4。对每个token:temporal_id = 0height_id = 0width_id = token的位置(0, 1, 2, 3, 4),实际实现里,因为是文本,通常三个id都一样,也可以都用token的位置。M-RoPE实际效果等价于传统的1D-RoPE。

2. 图片(Image)

假设输入是一张 $3\times 4$ 的patch网格图片(3行4列),共12个patch。对每个patch的token:temporal_id = 0(因为不是视频,没有时间维),height_id = 行号(0, 1, 2)width_id = 列号(0, 1, 2, 3),比如patch在第2行第3列(行号从0开始),那么temporal_id = 0height_id = 1width_id = 2

col=0 col=1 col=2 col=3
(0,0,0) (0,0,1) (0,0,2) (0,0,3)
(0,1,0) (0,1,1) (0,1,2) (0,1,3)
(0,2,0) (0,2,1) (0,2,2) (0,2,3)

所有patch的temporal_id都为0,只有height和width随位置变化。

3. 视频(Video)

假设有2帧视频,每帧图片patch网格为 $3\times 4$。对于第1帧(frame_idx = 0):temporal_id = 0height_id = 行号width_id = 列号,对于第2帧(frame_idx = 1):temporal_id = 1height_id = 行号width_id = 列号,比如第2帧的第2行第3列patch:temporal_id = 1height_id = 1width_id = 2,每个视频patch token都有唯一的(temporal, height, width)三元组。

假如我们要为所有token生成M-RoPE编码,编码过程简化伪代码如下:

def get_mrope_ids(mode, token_idx, row_idx=None, col_idx=None, frame_idx=None):
    if mode == "text":
        # 文本序列
        return (token_idx, token_idx, token_idx)  # 或全部用token_idx
    elif mode == "image":
        # 图片patch
        return (0, row_idx, col_idx)
    elif mode == "video":
        # 视频patch
        return (frame_idx, row_idx, col_idx)

总结

每个token输入transformer前,都会赋予这样一个三元组编号,分别用RoPE编码,然后合成最终的位置embedding。这就是M-RoPE的实质。

import torch
import torch.nn as nn

# --------- 1. 假设的原始输入(文本、图片、视频) -------------
text_tokens = torch.randint(1000, (6,))  # 文本:假设已经分词为6个token
image = torch.randn(3, 48, 64)  # 图片:单张图片,shape [3, 48, 64],3通道,48×64像素
video = torch.randn(2, 3, 48, 64)  # 视频:2帧,每帧[3, 48, 64]

# --------- 2. patch/token embedding ----------------------------
# (A) 文本 token embedding
vocab_size = 1000
embed_dim = 48
text_embedding = nn.Embedding(vocab_size, embed_dim)
text_token_emb = text_embedding(text_tokens)  # shape [6, 48]

# (B) 图片 patch embedding
patch_size = 16
patch_embedding = nn.Conv2d(3, embed_dim, kernel_size=patch_size, stride=patch_size)
img_patches = patch_embedding(image.unsqueeze(0))  # [1, 48, H', W']
H_p, W_p = img_patches.shape[2], img_patches.shape[3]
img_token_emb = img_patches.flatten(2).squeeze(0).transpose(0, 1)  # [num_patches, 48]

# (C) 视频 patch embedding
video_patches = patch_embedding(video.view(-1, 3, 48, 64))  # [2, 48, H', W']
T = video_patches.shape[0]
video_token_emb = video_patches.flatten(2).transpose(1, 2).reshape(-1, embed_dim)  # [T*num_patches, 48]
H_vp, W_vp = video_patches.shape[2], video_patches.shape[3]


# --------- 3. 生成M-RoPE的位置ID -------------------------------
# text_token_emb: [6, 48]  # 文本
# img_token_emb: [12, 48]  # 图片
# video_token_emb: [24, 48]  # 视频

# (A) 文本
text_len = text_token_emb.shape[0]
temporal_ids_text = torch.arange(text_len)
height_ids_text = torch.arange(text_len)
width_ids_text = torch.arange(text_len)

# (B) 图片
num_img_patches = H_p * W_p
temporal_ids_img = torch.zeros(num_img_patches, dtype=torch.long)
height_ids_img = torch.arange(H_p).repeat_interleave(W_p)
width_ids_img = torch.arange(W_p).repeat(H_p)

# (C) 视频
num_vid_patches = T * H_vp * W_vp
temporal_ids_vid = torch.arange(T).repeat_interleave(H_vp * W_vp)
height_ids_vid = height_ids_img.repeat(T)
width_ids_vid = width_ids_img.repeat(T)

# --------- 4. M-RoPE编码实现(见前例,也可封装成函数) ---------------
def rotary_embedding(pos_ids, dim):
    inv_freq = 1.0 / (10000 ** (torch.arange(0, dim, 2).float() / dim))
    sinusoid_inp = torch.einsum("i,j->ij", pos_ids.float(), inv_freq)
    emb = torch.cat([torch.sin(sinusoid_inp), torch.cos(sinusoid_inp)], dim=-1)
    return emb

def mrope_embedding(temporal_ids, height_ids, width_ids, dim):
    sub_dim = dim // 3  ### 每个维度占用的维度数
    temp_emb = rotary_embedding(temporal_ids, sub_dim)
    h_emb = rotary_embedding(height_ids, sub_dim)
    w_emb = rotary_embedding(width_ids, dim - 2*sub_dim)
    pos_emb = torch.cat([temp_emb, h_emb, w_emb], dim=-1)
    return pos_emb

# --------- 5. 获得最终的token + 位置编码 -------------------------
text_pos_emb = mrope_embedding(temporal_ids_text, height_ids_text, width_ids_text, embed_dim)
text_final_emb = text_token_emb + text_pos_emb  # [6, 48]

img_pos_emb = mrope_embedding(temporal_ids_img, height_ids_img, width_ids_img, embed_dim)
img_final_emb = img_token_emb + img_pos_emb  # [num_img_patches, 48]

vid_pos_emb = mrope_embedding(temporal_ids_vid, height_ids_vid, width_ids_vid, embed_dim)
vid_final_emb = video_token_emb + vid_pos_emb  # [T*num_vid_patches, 48]

# --------- 6. 拼成Transformer输入或分别处理 ------------------------
# 你可以将三种类型的final_emb拼在一起形成一个长序列送入Transformer(记得为不同模态分配不同的起始position偏移)。
# 或者根据任务需要分别送入不同模型分支。
print("文本序列输入 shape:", text_final_emb.shape)
print("图片序列输入 shape:", img_final_emb.shape)
print("视频序列输入 shape:", vid_final_emb.shape)

Unified Image and Video Understanding

在 Qwen2-VL 多模态大模型中,图片和视频是一起训练的,这样模型既能懂图片,也能理解视频内容。

对于视频:

对于图片:

此外,为了让训练既高效又能处理长视频,每段视频的输入都会动态调整分辨率,确保总的token数量不超过16384,这样既节省了显存,也保证了模型能看到较长的视频。

3. 训练

采用三阶段训练,LLM 来自 Qwen2,Vison encoder 来自 DFN(fixed position embedding 用 RoPE-2D 代替),下面的 token 不仅包含文本 token,还包含图片 token

第一阶段:只训练视觉编码器(ViT)

第二阶段:端到端联合训练(解冻所有参数)

第三阶段:冻结ViT,仅微调LLM(instruction tuning)

三阶段训练是为了让模型先建立强健的视觉表征,再实现端到端跨模态对齐,最后通过单独微调语言部分提升指令理解和实际应用能力,各阶段目标清晰、互不干扰,能显著提升最终多模态模型性能和泛化能力。

训练使用的是 ChatML 的数据形式

bounding box 形式

5月 06, 2025
4月 06, 2025
ufw