Qwen2-VL 视觉编码器源码拆解:从像素到 Token 的完整数据流
2026/8/21 6:54:33 网站建设 项目流程

引言

多模态大模型的视觉编码器通常被视为一个"黑盒"——图像进去,token 出来;但是当我们需要回答"为什么 Qwen2-VL 能处理任意分辨率的图像"或"它和 Swin Transformer V2 的本质差异在哪"等这样细致的问题,我们必须打开这个黑盒,追踪从像素到 token 的每一行代码;

因此本文基于 Qwen2-VL 官方源码,将从数据流、核心模块、框架对比三个层面逐层拆解;

数据流分析

配置文件

我们打开源码的配置文件(通常文件名是包含 Config),看看配置内容是什么:

从配置文件看,可以了解到 Vision Encoder 有 32 层 Transformer,每一层输出 1280 维的特征,并且会映射到 LLM 的维度是 3584 (说明具有投影层),采用的激活函数是 quick-gelu 等信息;下面我们把里面一些关键的信息拿出来做分析:

Quick-gelu

我们可以看到第一个设计点是激活函数的选择,采用的是 quick-gelu,这个激活函数我基本没有接触过,但是从功能上看应该主流大模型都会用这个函数,对应的表达式:

QuickGELU(x) = x * sigmoid(1.702 * x)

我们把两个函数图像拿出来,对比发现基本上是吻合的,最大误差只有0.02,误差 0.02 在神经网络中间层完全可以忽略,但计算速度提升约 30-50%;

很明显从公式上来看,后者的计算明显是更简单,用几乎不可见的精度损失,换显著的速度提升;

token 压缩

Vision Encoder 处理完一张图后,会产生很多 token,这个参数表示把相邻 2×2 = 4 个 token 压成 1 个,减少数量;为什么要压缩 token,原因是 LLM 处理序列的计算复杂度为 O(n²),视觉 token 数量直接决定推理开销,将 4 个 patch 压缩为 1 个 token,能将序列长度降为 1/4,计算量降为约 1/16;一般都是采用 reshape 的方法实现

时间切片

处理视频时,不仅要把画面切成空间 patch,还要把时间切成小段,这个参数表示每 2 帧画面打包成一个时间块,那么这样视频就能和图像用同一套流程处理——图像可以看作只有 1 帧的视频,时间块数为 1;

权重初始化的抖动范围

神经网络刚开始训练时,所有权重不能全是 0,也不能太大,这个值表示初始权重随机抖动的标准差是 0.02;这个步骤很关键,原因是如果初始化太大,训练初期梯度会爆炸,模型学不动;如果全是 0,所有神经元输出一样,模型也学不动;

图片的预处理

接着进入图片的预处理部分,实现的功能是将图片转化成 tensor 的格式送进 Vision Encode;千问在这边有一个小设计—动态分辨率处理,简单来说就是保持原比例进行 resize;传统的做法基本都是直接裁剪成为正方形,这样做宽屏图被压扁了,竖屏图被拉宽了,信息可能会变形;

图像编码

图像 Encoder 部分是架构中的核心的内容上图中的代码是功能的定义,我们看看 forward 函数对这些功能是怎么编排的,可以很直观看出数据的流动方向

def forward( self, hidden_states: torch.Tensor, grid_thw: torch.Tensor, **kwargs: Unpack[TransformersKwargs] ) -> torch.Tensor: r""" grid_thw (`torch.LongTensor` of shape `(num_images, 3)`): The temporal, height and width dimensions of feature shape for each image. Each row contains [t, h, w] values. """ position_ids = get_vision_position_ids(grid_thw, self.spatial_merge_size, kwargs=kwargs) cu_seqlens = get_vision_cu_seqlens(grid_thw, kwargs=kwargs) hidden_states = self.patch_embed(hidden_states) rotary_pos_emb = self.rotary_pos_emb(position_ids) emb = torch.cat((rotary_pos_emb, rotary_pos_emb), dim=-1) position_embeddings = (emb.cos(), emb.sin()) for blk in self.blocks: hidden_states = blk( hidden_states, cu_seqlens=cu_seqlens, position_embeddings=position_embeddings, **kwargs, ) merged_hidden_states = self.merger(hidden_states) return BaseModelOutputWithPooling( last_hidden_state=hidden_states, pooler_output=merged_hidden_states, )

数据流:数据进来先被 Patch Embedding,同时加上 2D-RoPE 位置编码,最后传入 Transformer 模块,压缩投影后返回;注意一下,blk 里面并没有 window_size 的字样,也就是注意力采用的大概率是全局注意力;

Patch Embedding 这个步骤我们通常是用卷积直接实现的,切分后直接映射;只需要控制两个变量,分别是 Kernel_size 和 Stride 就可以直接实现

Block

这个就是前面循环的 32 层的单个内部结构,我们可以看看:

很标准的一个Pre-Norm + 残差连接的模块;在初始化定义的时候,我们看到 sdpa,这个全程是Scaled Dot-Product Attention,表示的就是全局注意力机制

我们继续看 VisionAttention 这个模块

代码中有 self.is_causal = False,表示的是采用了双向注意力机制

  • 因果注意力(Causal):每个 token 只能看前面的,不能看后面的(LLM 生成文本时用)

  • 非因果注意力(Non-causal):每个 token 可以看所有其他 token(视觉特征提取时用)

这个部分就是一个常规的 qkv 的构造,和 ViT 的实现基本上是一致的;

我们把上述的内容总结到表格上并且列举出来:

步骤操作输入 shape输出 shape关键源码
预处理resize + normalizePIL Image(3, H, W)image_processing
PatchEmbedConv2d(k=14,s=14)(B,3,H,W)(B, N, 1280)patch_embed
位置编码get_vision_position_idsgrid_thw(N, 2)rotary_pos_emb
32层 BlockAttention + MLP(B, N, 1280)(B, N, 1280)blocks
Merger2×2 压缩 + 投影(B, N, 1280)(B, N/4, 3584)merger

数据流的方向如下图所示:

框架对比

在配置环境的时候,会安装一个库,叫做 Transformer库,transformers 库会预装了几百个模型,因此我们找 Swim-v2 直接搜就可以,代码放下面:

python3 -c "from transformers import Swinv2Model; import inspect; print(inspect.getsourcefile(Swinv2Model))"

注意力的不同

Swim Transformer 的核心的创新就是引入了窗口注意力机制,从而减少了计算量;简单描述窗口注意力机制:把全图切成小窗口,每个 patch 只和窗口内的 patch 算注意力,而不是和全图所有 patch 算;聪明的你发现了,那么不同窗口之间实际上是没有注意力交互的,因此 Swim 提出可以使用滑动窗口的方式解决这个部分的问题;

那为什么 Qwen 敢用全局注意力呢?前面我们有提到PatchMerger方法,因此实际上在送进 LLM 之前我们已经尽量压缩 token 的个数;虽然最后的视觉 token 少,但是用全局注意力换更强的全局感知能力

位置编码的不同

Swin 的注意力只在一个小窗口里算,它需要一个位置编码来告诉模型这 49 个 patch 彼此的相对位置是什么;而 Qwen 的 Vision Encoder 是全局注意力,它不需要知道窗口内的相对位置,它需要知道的是每个 patch 在原图中的绝对二维坐标

归一化的不同

这是Swin V2 论文的核心创新之一,Swin V1 用 Pre-Norm,但模型变大后训练不稳定(深层梯度爆炸),因此 Swin V2 改成了Post-Norm,论文指出 Post-Norm 能抑制特征方差增长,让模型能稳定扩展到 3B 参数

Pre-Norm (Qwen): x + attn(norm(x)) ← 归一化在残差分支前 Post-Norm (Swin): norm(x + attn(x)) ← 归一化在残差分支后

为什么 Qwen 不做这样的处理呢?因为 Qwen 的 Vision Encoder 只有 32 层、大约是 600M 参数,Pre-Norm 足够稳定;这个时候就有人问:为什么 Qwen 不扩展呢?因为多模态模型的瓶颈在 LLM,不在视觉编码器;视觉编码器的任务是"提取特征",LLM 的任务是"理解、推理、生成",因此大头是在后者;

Swin 为什么需要扩展到 3B?因为 Swin 是纯视觉 Backbone,下游任务(检测、分割、分类)的性能直接取决于视觉特征的表达能力,所以会扩展参数;

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询