☰
视觉特征如何变成语言?Mini-InternVL2-4B-DA-DriveLM的Pixel Shuffle与MLP连接器实现原理
2026/10/19 15:34:55 网站建设 项目流程

【免费下载链接】Mini-InternVL2-4B-DA-DriveLM

项目地址:https://ai.gitcode.com/OpenGVLab/Mini-InternVL2-4B-DA-DriveLM
点击查看免费下载

Mini-InternVL2-4B-DA-DriveLM 是 OpenGVLab 面向自动驾驶领域微调的多模态大模型,它如何让"看得懂"的图像真正"说得出来"?答案藏在一座精巧的桥里:视觉特征先经过Pixel Shuffle(像素重排降采样),再由一个MLP 连接器映射成语言模型能理解的词嵌入空间。本文带你快速读懂这座"视觉→语言"桥梁的完整实现。

1. 先看清整体架构:三块拼图的分工 🧩

模型由三个部分组成,配置文件 config.json 中可以看到全貌:

组件模型关键参数
视觉编码器(ViT)InternVisionModelimage_size: 448、patch_size: 14、hidden_size: 1024
连接器Pixel Shuffle + MLPdownsample_ratio: 0.5、ps_version: "v2"
语言模型(LLM)Phi3ForCausalLMhidden_size: 3072、32 层

一次推理的数据流是这样的:

图片(448×448) → ViT 输出 32×32 个 patch 特征(1024维) → Pixel Shuffle:2×2 邻域拼接 → 16×16 个特征(4096维) → MLP 连接器:4096 → 3072 维 → 替换文本中的 <IMG_CONTEXT> 占位符 → Phi-3 语言模型生成文字回答

关键洞察:ViT 和 LLM 是两个"语言不通"的模型。ViT 吐出的特征向量,LLM 完全看不懂。连接器就是翻译官,而 Pixel Shuffle 在翻译前先做了一次"压缩排版"。

2. 为什么必须先降采样?从 1024 个 token 到 256 个

448×448 的图片按 14×14 切 patch,会得到 32×32 =1024 个patch 特征。如果每个 patch 都变成一个视觉 token 塞给 LLM,序列会非常长,注意力开销平方级上涨,还会挤占用户问题和回答的上下文空间。

Mini-InternVL 的解法是:把空间分辨率减半、通道数加倍。降采样比例由 config.json 中的downsample_ratio: 0.5控制,每张图片最终对应多少个 token,在 modeling_internvl_chat.py 中一眼可见:

self.num_image_token = int((image_size // patch_size) ** 2 * (config.downsample_ratio ** 2)) # (448 // 14)**2 * 0.5**2 = 1024 * 0.25 = 256

也就是说,每张 448×448 图片只消耗 256 个 token,且信息并没有被插值丢掉——它被"打包"进了更宽的向量里。

3. Pixel Shuffle 实现原理:零参数、零插值的纯重排 🔁

Pixel Shuffle 的全部逻辑集中在 modeling_internvl_chat.py 的pixel_shuffle方法:

def pixel_shuffle(self, x, scale_factor=0.5): n, w, h, c = x.size() x = x.view(n, w, int(h * scale_factor), int(c / scale_factor)) x = x.permute(0, 2, 1, 3).contiguous() x = x.view(n, int(h * scale_factor), int(w * scale_factor), int(c / (scale_factor * scale_factor))) if self.ps_version == 'v1': warnings.warn("In ps_version 'v1', the height and width have not been swapped back, " 'which results in a transposed image.') else: x = x.permute(0, 2, 1, 3).contiguous() return x

拆开看它在做什么(以本模型的 32×32×1024 输入为例):

  1. reshape:把高度维拆成16 × 2,同时把 1024 个通道看作512 × 2;
  2. permute:调整维度顺序,让 2×2 邻域内的 4 个 patch 在内存中排到一起;
  3. view:合并后得到16×16×4096—— 每个新位置的特征,正是原来 2×2 小方块里 4 个 patch 特征按行优先顺序首尾拼接。

它的巧妙之处:

  • 纯view/permute操作:不经过任何矩阵乘法和插值,没有可学习参数,开销几乎为零;
  • 信息无损:4 个 patch 的 4×1024 = 4096 维信息一个不少地保留在新 token 中,空间细节只是被"压缩打包"了;
  • 空间对应关系正确:第 (i, j) 个新 token 精确对应原图 2×2 的方块,后续 LLM 能建立"这个 token 负责图像哪一块"的映射。

ps_version 的小坑:v1 与 v2 的区别 ⚠️

注意方法末尾的分支。ps_version: "v1"时跳过最后一步permute(0, 2, 1, 3),结果就是宽和高没有换回去,输出特征图相当于被转置(横向信息错放到纵向)。本仓库的 config.json 中配置为"ps_version": "v2",即会执行这次交换,保证空间对应关系正确。如果你加载旧版本权重或旧配置,需要留意这个参数。

4. 特征提取的完整流程:extract_feature 逐步拆解

上面两步被组织在 modeling_internvl_chat.py 的extract_feature中,逐行对应:

vit_embeds = self.vision_model(pixel_values=..., ...).last_hidden_state vit_embeds = vit_embeds[:, 1:, :] # ① 去掉 CLS token,只留 32×32=1024 个 patch 特征 h = w = int(vit_embeds.shape[1] ** 0.5) # ② 推出 32×32 网格 vit_embeds = vit_embeds.reshape(vit_embeds.shape[0], h, w, -1) vit_embeds = self.pixel_shuffle(vit_embeds, scale_factor=self.downsample_ratio) # ③ 重排为 16×16×4096 vit_embeds = vit_embeds.reshape(vit_embeds.shape[0], -1, vit_embeds.shape[-1]) # ④ 展平成 256×4096 vit_embeds = self.mlp1(vit_embeds) # ⑤ MLP 连接器 → 256×3072

其中select_layer: -1(见 config.json)表示取 ViT 最后一层输出;若设为正整数,则取对应中间层(源码中hidden_states[self.select_layer]的分支),这是 InternVL 系列常用的消融手段。

5. MLP 连接器:把 4096 维"翻译"进 LLM 词汇空间

降采样后的特征还是 4096 维,而 Phi-3 的隐藏维度是 3072。真正的"语言翻译"由mlp1完成,定义见 modeling_internvl_chat.py:

self.mlp1 = nn.Sequential( nn.LayerNorm(vit_hidden_size * int(1 / self.downsample_ratio) ** 2), # 4096 nn.Linear(vit_hidden_size * int(1 / self.downsample_ratio) ** 2, llm_hidden_size), # 4096 → 3072 nn.GELU(), nn.Linear(llm_hidden_size, llm_hidden_size) # 3072 → 3072 )

结构是经典的LayerNorm → Linear → GELU → Linear:

  • 输入 4096 =1024 × (1/0.5)²,正好是 Pixel Shuffle 拼接后的维度,两者是"上下游"关系;
  • 输出 3072,与 config.json 中llm_config.hidden_size: 3072完全对齐,可以直接和文本词嵌入并列;
  • 没有加投影层数上的花活,保持轻量,符合 Mini-InternVL "口袋模型"的定位。

6. 最后一公里:视觉嵌入如何"替换"进文本序列

连接器输出的 256 个特征向量,如何进入对话?看 modeling_internvl_chat.py 的generate:

vit_embeds = self.extract_feature(pixel_values) input_embeds = self.language_model.get_input_embeddings()(input_ids) selected = (input_ids == self.img_context_token_id) # 找到 <IMG_CONTEXT> 位置 input_embeds[selected] = vit_embeds.reshape(-1, C) # 原地替换成视觉特征

机制很直白:

  1. chat接口把问题中的<image>展开为<img>+ 256 个<IMG_CONTEXT>× 图片块数 +</img>(见 modeling_internvl_chat.py);
  2. 这些特殊 token 来自 added_tokens.json,其中<IMG_CONTEXT>对应词表 id32013(词表大小 32020);
  3. 编码时,所有<IMG_CONTEXT>位置的文本词嵌入被视觉特征整体覆盖(* 0.0 + vit_embeds的技巧保证梯度不回流到原嵌入,见 modeling_internvl_chat.py);
  4. 于是 Phi-3 看到的序列是:... <img> [256 个视觉向量] </img> 用户的问题 ...,随后逐词生成回答。

顺带一提:得益于动态分辨率(dynamic_image_size: true、max_dynamic_patch: 12),长宽比不规则的图会被切成多块 448×448 的 tile(最多 12 块 + 1 张缩略图),每块都贡献 256 个 token,这也是该模型能看清道路细节、小字标识的关键。

7. 快速体验:跑通一次图文对话 🚀

使用transformers >= 4.37.2,按 README.md 的 Quick Start 即可:

from transformers import AutoModel, AutoTokenizer import torch path = 'OpenGVLab/Mini-InternVL2-4B-DA-DriveLM' model = AutoModel.from_pretrained(path, torch_dtype=torch.bfloat16, low_cpu_mem_usage=True, trust_remote_code=True).eval().cuda() tokenizer = AutoTokenizer.from_pretrained(path, trust_remote_code=True, use_fast=False) question = '<image>\nPlease describe the image shortly.' response = model.chat(tokenizer, pixel_values, question, dict(max_new_tokens=1024, do_sample=True))

图片预处理(load_image/dynamic_preprocess)的完整代码见 README.md。

8. 核心文件与参数速查表

内容位置
Pixel Shuffle 实现modeling_internvl_chat.py
特征提取流程 extract_featuremodeling_internvl_chat.py
MLP 连接器 mlp1 定义modeling_internvl_chat.py
占位符替换逻辑modeling_internvl_chat.py
连接器超参(downsample_ratio、ps_version等)config.json
连接器超参定义configuration_internvl_chat.py
ViT 结构(24 层、hidden 1024)modeling_intern_vit.py
<IMG_CONTEXT>等新增 tokenadded_tokens.json
推理示例代码README.md

9. 小结

回到开头的问题——视觉特征如何变成语言?

  • Pixel Shuffle用一次无参数的张量重排,把 1024 个 1024 维 patch 特征打包成 256 个 4096 维特征,空间减半、通道翻倍,信息无损;
  • MLP 连接器(LayerNorm + 两层 Linear + GELU)再把 4096 维投影进 Phi-3 的 3072 维嵌入空间;
  • 最后通过<IMG_CONTEXT>占位符替换,视觉向量与文本词嵌入并肩进入 LLM,模型自然输出"看得懂"的文字。

这套"重排 + 轻 MLP"的连接器设计,正是 Mini-InternVL2-4B-DA-DriveLM 以不到 5% 的参数拿到 90% 性能的秘密之一,也是理解 InternVL 系列多模态对齐机制的最佳切入点。

【免费下载链接】Mini-InternVL2-4B-DA-DriveLM

项目地址:https://ai.gitcode.com/OpenGVLab/Mini-InternVL2-4B-DA-DriveLM
点击查看免费下载
上一篇:零拷贝的魔法:video-compress-cj 如何用 Surface 直通硬件编解码,大幅降低延迟
下一篇:在Slack、Discord、微信里继续指挥同一个AI:Kiro Crew接入10大消息平台教程(无需暴露公网端口)

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

立即咨询