type
status
date
slug
summary
tags
category
icon
password
😀
前后分析下 Qwen2-vl 和 Qwen2.5-vl 的源码,重点在于图片/视频处理部分,最后再统计下前后的区别
 

Q & A

Q:Qwen2-vl 中提到视频动态分辨率的机制是什么

A:通过预先设定好的 min_pixels=100 × 28 × 28 和 max_pixels=16384 × 28 × 28 调整视频的分辨率,逻辑是这样:
  1. 模型有最大能处理的 sequence 长度 max_len,每个 token 对于图片是 28 × 28 的 patch
  1. 通过 max_len * 28 × 28 可以得到模型能处理的最大像素
  1. 在 Qwen2-vl 中视频是固定采用 fps=2,所以一个视频的采用出来的帧数是固定的
  1. 通过 max_pixels 和帧数,可以反推出每帧应该设置为多少分辨率
 

Q:Qwen2.5-vl 中的动态帧率的机制是什么

A:其实就是可以通过预先设定的 fps 或者 nframe 采样视频的帧,确定帧率后,再通过 min_pixels 和 max_pixels 来调整分辨率
论文没有提到的,但是在 github 的 readme.md 提到了:
动态帧率可以理解成,动态分辨率推广到了时间维度。
代码上可以体现在 vision_process.py_read_video_torchcodec func 中,它的作用就是根据入参的 fps 或者 nframe 采样,同时会返回一个 sample_fps 的参数,表示真实采样的 fps(因为入参的 fps 或 nframe 会被 min_frames,max_frames限制,然后被调整),sample_fps 后续应该要用到 m-rope 中,这样就把时间信息放到位置编码中了
 
注:一开始读论文读到这里内容时,以为是模型会根据需要自动确定视频的采用帧率,看完代码发现其实采用帧率还是要根据经验来调整的超参。
 

Qwen2-vl

找到一篇写的非常清晰的源码分析文章,图片/视频处理过程看这篇文章就可以了,这里记录一下重点没理解的东西,再看看位置编码的部分(文章没涉及位置编码的源码)
 

Q:如何统一图像和视频处理的?3D 卷积是用来做什么的?

  • 视频每秒采样两帧,即 2FPS
  • 为了单张图片也可以用 3D 卷积,于是会把单张图片复制成两张,这样统一了图片和视频的处理
  • 两张图片叠加在一起用 3D 卷积,将一个 patch 转换为 patch 的 embedding,这里 patch 的 shape 通常为 (2, 3, 14, 14),它们分别为 (叠加在一起的图片数量, RGB 通道数量,patch size 的 High, patch size 的 width), 卷积核的大小可以理解成 (1280, 2, 3, 14, 14),即它会输出 1280 个通道,每个通道就是 path embedding 的一个维度,所以这里 3D 卷积就是一个线性层把 (2, 3, 14, 14) 的 patch 映射为了 embedding,只是 ViT 里面 patchify 的操作很适合用卷积来实现
引用文章里面的图片
引用文章里面的图片
 

Q:论文中描述的,经过 ViT 之后会把相邻的 2✖️2 的 patch 合并为一个 patch 是如何实现的

 
知乎文章中给的非常形象的图片处理前后的样子
知乎文章中给的非常形象的图片处理前后的样子
  • 处理过程会把相邻的 2✖️2 patch 的 embedding 放到一起,方便后面进一步 token 压缩
  • 在 ViT 之后,会使用一个 4h → h 的单层 MLP 将 4 个 patch 转换为一个 patch,进一步压缩给到 LLM 到 token 数量,这里的单层 MLP 其实就是模态融合的 Adapter 层
 

Q:2D-Rope 是如何实现的

2D-Rope 逻辑上还是简单的 - q 或者 k 向量分成两半,一半施加 x 轴的 1D-Rope,另外一半施加 y 轴(height)的 1D-Rope。
这里核心就是怎么获取 patch 的 position id,即下面这个 func 的功能
notion image
代码层面上,获取每个 patch position id,就可以套到1D-Rope 上面了,以前面一个 4✖️4 patch 的图片为例,它的 H 和 W 的位置 id 就是:
那么它拉平之后就是 , (0, 1) 就表示这个 patch 下一半的向量施加位置为 0 的位置编码,一半施加位置为 1 的位置编码
 

Q:M-Rope 是如何实现的

debug 看源码的结果,和论文/代码注释是有出入的,论文/注释的表述的方法有个重点 —— 从 text 下的 1D-Rope 来看,一张图片应该只占一个 token,即 position id 只会增加 1,(这里理解错了,论文/代码注释表述确实是下面讲述的逻辑)从代码结果来看,一张图片占多少 postition 等于 max(t, h, w)的大小
 
先说逻辑:
假设 sequence 是这样的: “[text_token1][image][text_token2]” (这里先忽略隔离 image 的 vision_start, vision_end token) ,假设 image patchify (经过 vision encoder)之后是一个 2*3 的 shape,一共算 6 个 token 给到 LLM,[text_token1] 位置 id 14,那实际 qwen2-vl 代码中,m-rope 对以上的位置编码分别是:
  • [text_token1]: (14, 14, 14)
  • [image]: [15 15 15], [15 15 16], [15 15 17], [15 16 15], [15 16 16], [15 16 17],
  • [text_token2]: [18 18 18]
逻辑是这样的:
  • 对于时间维度的 position id
    • 同一个 image 内的 token 都是一样的,且不递增
    • 但是对于在 image 后面的 text token 会增加 max(t, h, w) 个 step,这里 h w 就是 image 的 path 在高宽维度的 shape
  • 对于 H, W 维度
    • image token 就会随 patch 的位置而变化,这里比较好理解
    • text token 的 position 是和时间维度的 id 是一样的
我的评价:太不优雅了,但是为了 text 部分保持 1D-Rope 的折中方案;并且对于图片前后的 text 位置编码会差很多,不符合直觉
 
这里的伪代码可以总结成:
 
 

Qwen2.5-vl

2.5 相比于 2 的涉及代码的改动主要:
  1. 动态帧率
  1. M-Rope 增加时间信息
动态帧率在开头已经总结过了,比较简单,这里就分析下 M-Rope 是如何融入时间信息的
相比 Qwen2-vl 的源码,主要区别在 这几行代码,它们改写后和 2 对比:
2.5 只是在计算 vision 部分的 position_ids 时,乘上了 second_per_grid_t * self.config.vision_config.tokens_per_second
在解释 2.5 的逻辑之前,先回顾一下 2 的 t_index 的结果:假设 video fps=2 采样后,给到 LLM 部分一共 4 张图片(视频一共四秒,采样 8 张图片,3D卷积会把两张合成一张),那么这四张图片对应 token 的 position_ids 分别是 [0, 1, 2, 3] (实际上还会加上起始位置,但就先假设这个视频从位置 0 开始)
那 2.5 的逻辑是:每秒之间间隔的 position 长度应该是一样的,就是拿tokens_per_second这个参数控制的,是一个超参,它表示的是 “你想用多少个 position 表示一秒”,假设为 12,另外一个参数:second_per_grid_t=temporal_patch_size / fps 其中 temporal_patch_size 就是 3D 卷积的时间维度通道数,固定为 2,fps 为视频的采用帧率可以动态变化的,假设为 8,那么second_per_grid_t=2/8=0.25 表示每张图片占 0.25 秒,tmp_factor=0.25*12=3,就得到每张图片占用的 position 为 3,就从原来的每张图片占用 postition 1 变成了 3,这样就加入了时间信息了。
 
注:Qwen2.5-vl 的配置中 tokens_per_second 设置为 2,另外这变量着实有点误导性,它像是表述每秒占用多少个 token,改成 position_per_second 感觉更合适,因为同一张图片下所有 patch 的 token 的 t_index 是一样的。