第十一章 多模态大模型:视觉编码、跨模态对齐与训练¶
多模态模型要解决三个问题:不同模态如何表示,如何对齐到可交互空间,如何让语言模型利用视觉信息生成答案。图片不是“另一种文本”;分辨率、空间位置、OCR、颜色和对象关系都会影响视觉 token。

图 11-1 视觉编码器产生视觉特征,连接模块将其映射到语言模型可使用的表示。
11.1 ViT:把图像变成 token 序列¶

图 11-2 图像尺寸和 patch 大小共同决定视觉序列长度与计算成本。 先建立直觉。 ViT 把图像切成固定大小 patch,将每块展平并线性投影为视觉 token,再像文本 token 一样使用 Transformer。分辨率和 patch 大小共同决定序列长度。 读这一节时,先不要急着记缩写,而要不断追问:输入是什么,经过了哪些可观察变换,输出又怎样被验证。
把过程拆开看:
- 图像规范化与缩放。
- patchify 得到 N 个小块。
- 线性投影并加位置。
- Transformer 编码视觉关系。
最小例子。 224×224 图像、patch=16 时有 14×14=196 个 patch;分辨率翻倍会让 patch 数约变为四倍。 例子越小,越容易手算、打印中间量并判断实现是否偏离定义。
容易踩坑。 高分辨率只改输入尺寸却不处理位置编码、不同宽高强行拉伸、通道顺序和归一化不匹配,都会损伤效果。 排错时应保存失败输入和版本,先定位最早出现异常的步骤,再决定是否调整模型或超参数。
动手任务:用 reshape/permute 手写 patchify,再重组图像验证无信息丢失。 完成后不要只保存最终输出,还要保存代码、配置、中间张量或检索结果,以及你对结果的解释。
给定 \(H\times W\times C\) 图像,以 \(P\times P\) 切块,patch 数为 \(N=HW/P^2\)。每个 patch 展平后线性投影到隐藏维,加位置编码,再送入 Transformer Encoder。Patch 越小,空间细节越多,但视觉 token 数和注意力成本快速增加。
import torch
from torch import nn
class PatchEmbedding(nn.Module):
def __init__(self, image_size=224, patch_size=16, in_ch=3, dim=768):
super().__init__()
if image_size % patch_size:
raise ValueError("image_size 必须能被 patch_size 整除")
self.proj = nn.Conv2d(in_ch, dim, kernel_size=patch_size,
stride=patch_size)
self.n_patches = (image_size // patch_size) ** 2
def forward(self, images):
x = self.proj(images) # (B,D,H/P,W/P)
return x.flatten(2).transpose(1, 2) # (B,N,D)
卷积核大小和步长都等于 patch 大小,等价于对每个不重叠 patch 做共享线性投影。真实模型还会处理任意分辨率、动态切块或多尺度特征。
11.2 CLIP:图文对比学习¶
从问题出发。 CLIP 用图像编码器和文本编码器把配对样本映射到同一空间,通过批内对比学习拉近匹配对、推远不匹配对。 先把名词放到一边,沿着输入、状态变化和输出走一遍;只有能指出证据落在哪一步,概念才算真正掌握。
沿数据流逐步检查:
- 分别编码并归一化。
- 计算批内相似度矩阵。
- 图到文与文到图双向交叉熵。
- 学习温度调节分布。
用小数据走一遍。 batch 为 N 时得到 N×N logits,对角线是正样本,其他位置作为批内负样本。 先在纸面预测结果,再让程序打印中间状态;预测与运行不一致的地方,正是需要继续追查的知识缺口。
这里最容易出现的误解。 批中重复语义被当假负样本、只训练单向损失、忘记归一化、文本模板差异,都会影响检索。 出现异常时先冻结样本、配置和环境,从最靠近输入的环节开始验证;不要同时更换模型、数据和超参数。
小实验:实现 CLIP loss,验证交换图文顺序后双向损失含义。 提交物应包含预期、运行证据和失败记录;只有别人能按同样步骤复现,结果才具有学习价值。
CLIP 分别编码一批图片与文本,把向量归一化,计算批内相似度矩阵。正确图文对位于对角线,损失同时做 image-to-text 与 text-to-image 分类。
import torch.nn.functional as F
def clip_loss(image_features, text_features, logit_scale):
image = F.normalize(image_features, dim=-1)
text = F.normalize(text_features, dim=-1)
logits = logit_scale.exp() * image @ text.T
labels = torch.arange(len(image), device=image.device)
return (F.cross_entropy(logits, labels) +
F.cross_entropy(logits.T, labels)) / 2
批内其他样本充当负例,因此 batch 组成很重要;重复/近义 caption 会产生“假负例”。CLIP 学到共享表征,适合检索和零样本分类,但本身不是能长篇回答的生成式视觉语言模型。
11.3 BLIP/BLIP-2 与 Q-Former¶
先看它解决什么。 BLIP 统一理解与生成目标,BLIP-2 用 Q-Former 在冻结视觉编码器与冻结语言模型之间提取少量查询表示,降低端到端训练成本。 理解的标准不是会复述定义,而是能画出数据流、预测中间结果,并设计一个让错误暴露出来的检查。
可以把实现分成以下环节:
- 视觉编码器产生图像特征。
- 可学习 query 通过 Q-Former 读取。
- 投影到语言模型输入空间。
- 用图文目标训练连接。
一个可以手算的例子。 Q-Former 的 query 数固定,可把大量视觉 patch 压缩成较少表示,但压缩也可能丢失细粒度信息。 这里故意不用大模型或大数据,因为小输入能把每个轴、分数和状态变化完整暴露出来。
排错时先看这些地方。 把 Q-Former 说成简单线性层、冻结模块仍处于错误训练模式、视觉与语言 tokenizer 混淆,都会导致实现偏差。 修复不能止于‘这次跑通’,还要把失败样本变成自动测试,防止同类问题在下一版本重新出现。
现在动手:画出 BLIP-2 三个模块的梯度流,标注冻结与训练参数。 把关键断言写进测试,并在 README 说明怎样运行、怎样判断正确、当前实现还不支持什么。
BLIP 同时面向理解与生成,并通过 captioner/filter 改善噪声图文数据。BLIP-2 冻结已有视觉编码器和大语言模型,用轻量 Q-Former 跨越模态差距。可学习 query token 从视觉特征抽取与语言相关的信息,再连接到 LLM。
冻结大模块降低训练成本,但连接模块必须适配两端的表示与尺度。理解 Q-Former 的关键是“少量查询向量主动读取视觉特征”,而不是把所有视觉 patch 原样塞入语言模型。
11.4 LLaVA 两阶段训练¶
抓住这一节的主线。 LLaVA 典型路线先训练视觉—语言投影层,再用图文指令数据联合微调,让语言模型学会在对话中使用视觉 token。 遇到新术语,先找它在系统中的位置:它读取什么、保存什么、改变什么,以及失败时会留下什么信号。
真正动手时按这个顺序走:
- 预训练投影对齐特征。
- 构造多轮图文指令。
- 按阶段解冻部分模块。
- 用多能力评测和人工检查。
先做最小实验。 图像占位 token 在模板中的位置必须与视觉特征插入位置一致;标签通常只覆盖 assistant 文本。 把随机性固定并保留中间量,这个例子就能成为后续优化时的回归基线。
别被表面现象带偏。 指令数据答案由模型生成却不清洗、图像与文本错配、阶段二学习率过大破坏视觉特征,都会造成幻觉。 先区分定义错误、实现错误、数据问题和性能瓶颈。四类问题需要的证据不同,不能靠盲目调参混在一起处理。
验证任务:为一条图文多轮样本画出最终 token 序列和 label mask。 实验结束后写三句话:观察到了什么、这些证据支持什么结论、还有哪种解释尚未排除。
经典 LLaVA 结构使用 CLIP ViT 作为视觉编码器,通过投影层连接语言模型。第一阶段主要训练投影层,使图像特征与语言嵌入对齐;第二阶段用视觉指令数据做指令微调,使模型学会问答和对话。
两阶段的目的不同:对齐阶段解决“视觉特征如何进入语言空间”,指令阶段解决“如何按任务使用视觉信息”。若只做第二阶段且初始化不当,训练更难稳定;若只做对齐,模型未必会遵循复杂视觉指令。
现代 VLM 可能使用任意分辨率、视觉 token 压缩、窗口注意力、原生多模态预训练和视频/文档增强,不能把所有模型都等同于 LLaVA。
11.5 数据构建¶
先把概念落到可观察对象上。 多模态数据至少包含媒体、消息、任务类型、来源与许可。还要考虑 OCR、版面、语言、重复、隐私和可能造成偏见的分布。 这一节的重点是建立因果链。先说明问题,再看计算或流程,最后用可重复实验验证结论。
把抽象概念还原成操作:
- 验证文件可读与配对。
- 按内容与感知哈希去重。
- 清洗 OCR/标注。
- 按任务和难度分层切分。
把它缩小到能逐项检查。 同一图片配多个近似问题若跨训练/测试切分,会形成视觉泄漏;应按图像簇而非单条样本切分。 如果这个小例子还不能解释清楚,扩大数据只会让错误更难发现。
需要特别守住的边界。 只检查文本重复、EXIF 泄漏隐私、低质量自动 caption 当真值、版权不明图片入库,都会带来风险。 保留完整 trace 比猜原因更重要。找到第一次偏离预期的位置,通常比分析最终错误输出更高效。
本节练习:设计多模态数据卡,含图像来源、许可、分辨率、OCR 质量、语言和任务标签。 除代码外,请保留一份结果说明,标明环境、随机种子、输入规模和你主动检查过的边界。
多模态样本至少包含媒体、消息、任务类型和来源。图片与文本必须真正对应;OCR 文档要保留阅读顺序、页面和区域;VQA 要防止仅凭问题语言猜答案。敏感图像、人物隐私、版权和未成年人数据需要专门治理。
下游数据可覆盖描述、OCR、图表、定位、比较、多图和拒答。随机切分可能让同一视频帧或同一文档页面跨集合,导致泄漏,应按原始媒体分组。
11.6 微调 Qwen-VL 类模型的通用步骤¶
先建立直觉。 微调视觉语言模型要以官方 processor 和 chat template 为准,先确认模型怎样插入视觉 token,再决定冻结、LoRA 目标和像素预算。 读这一节时,先不要急着记缩写,而要不断追问:输入是什么,经过了哪些可观察变换,输出又怎样被验证。
把过程拆开看:
- processor 同时处理媒体与文本。
- 检查输入 id、pixel values 与 grid。
- 只在目标文本上算 loss。
- 按视觉长度分桶并控制 OOM。
最小例子。 一张高分辨率图可能产生远多于普通图的视觉 token,batch 按样本数相同也会有巨大显存差异。 例子越小,越容易手算、打印中间量并判断实现是否偏离定义。
容易踩坑。 仅给语言层加 LoRA 却期望视觉对齐改变、视觉占位符重复、训练推理预处理不同、padding mask 错误,都会失败。 排错时应保存失败输入和版本,先定位最早出现异常的步骤,再决定是否调整模型或超参数。
动手任务:对三个分辨率样本打印视觉 token 数、总长度和峰值显存,制定 batch 规则。 完成后不要只保存最终输出,还要保存代码、配置、中间张量或检索结果,以及你对结果的解释。
- 按官方 processor 和 chat template 编码图像与消息。
- 决定冻结视觉编码器、连接层和 LLM 的哪些部分。
- 对 assistant token 计算损失,屏蔽系统、用户和视觉占位 token。
- 控制像素/视觉 token 上限,按长度分桶,防止极端样本 OOM。
- 使用 LoRA 时明确目标模块是否覆盖视觉、投影或语言部分。
- 分任务评估,不只看综合平均分。
多模态显存除了模型参数,还受像素分辨率和视觉 token 影响。同一 batch 中一张超大图可能显著抬高峰值。先记录每样本视觉 token,再决定动态 batch。
11.7 评估¶
从问题出发。 多模态评估不能只用一个综合分数。要分别测感知、OCR、图表、空间、知识、推理、grounding、拒答和幻觉,并核对评测协议。 先把名词放到一边,沿着输入、状态变化和输出走一遍;只有能指出证据落在哪一步,概念才算真正掌握。
沿数据流逐步检查:
- 为每类能力准备独立样本。
- 统一提示和解码。
- 自动指标与人工审阅结合。
- 记录图像许可和错误类型。
用小数据走一遍。 VQA exact match 对同义表达敏感,OCR 字符错误率不反映回答解释;不同任务需要不同指标。 先在纸面预测结果,再让程序打印中间状态;预测与运行不一致的地方,正是需要继续追查的知识缺口。
这里最容易出现的误解。 测试图像训练泄漏、只看英文、裁剪导致答案丢失、用无视觉 baseline 仍能答对而未识别,都会高估视觉能力。 出现异常时先冻结样本、配置和环境,从最靠近输入的环节开始验证;不要同时更换模型、数据和超参数。
小实验:加入遮蔽图像或打乱图文配对的对照,判断模型是否真正使用视觉。 提交物应包含预期、运行证据和失败记录;只有别人能按同样步骤复现,结果才具有学习价值。
自动指标包括分类准确率、VQA exact match、OCR 字符错误率、caption CIDEr 等,但开放回答还需事实性和人评。特别测试:不存在对象、遮挡、小字、旋转、颜色、空间关系、多图指代和诱导问题。
视觉幻觉指模型描述图中不存在内容。缓解手段包括更好的视觉对齐数据、区域/grounding 监督、拒答样本和基于检测/OCR 的外部验证,但无法彻底消除。
11.8 常见错误¶
先看它解决什么。 多模态常见错误来自三种错位:媒体与文本错位、视觉 token 与位置错位、训练与推理模板错位。定位时先检查输入而不是先调优化器。 理解的标准不是会复述定义,而是能画出数据流、预测中间结果,并设计一个让错误暴露出来的检查。
可以把实现分成以下环节:
- 可视化实际送入的图像。
- 解码文本 token 与占位符。
- 核对标签 mask。
- 比较单样本前向与生成。
一个可以手算的例子。 如果图像预处理后上下颠倒或颜色通道错误,模型再大也无法稳定回答;把 tensor 还原成图像是最便宜的检查。 这里故意不用大模型或大数据,因为小输入能把每个轴、分数和状态变化完整暴露出来。
排错时先看这些地方。 只检查 loss、在数据加载器中静默跳过坏图、把所有错误归因于‘多模态幻觉’,都会延迟定位。 修复不能止于‘这次跑通’,还要把失败样本变成自动测试,防止同类问题在下一版本重新出现。
现在动手:建立五类故障注入:错图、空图、过大图、占位符缺失和 OCR 噪声,并记录系统行为。 把关键断言写进测试,并在 README 说明怎样运行、怎样判断正确、当前实现还不支持什么。
- 图像归一化或色彩通道不匹配预训练配置。
- 视觉占位 token 数与视觉特征数不一致。
- 标签 mask 错误,模型被训练预测用户问题或图像占位。
- 只用语言可猜的数据,模型表面指标高却忽略图像。
- 训练图像过于同质,真实照片/文档分布下失效。
练习:实现 PatchEmbedding 并验证 token 数;用随机特征计算 CLIP loss,观察正确配对相似度提高时损失变化;构造一个无需看图即可回答的 VQA 数据集,说明其评估缺陷。
本章配套代码¶
下面的脚本与正文使用相同符号。建议先在代码中打印形状和中间量,再运行断言;若依赖尚未安装,至少先阅读入口函数、输入输出和测试部分。
examples/11_vit_patchify.py:ViT patchify 与逆变换。
本章端到端实验:把知识变成可复现证据¶
本实验不是把本章代码重新抄一遍,而是把概念、实现、测试和解释串成一个小型工程。请新建独立目录,保存 README.md、环境文件、源代码、测试、运行日志和结果图。README 至少说明任务、输入输出、运行命令、预期现象、已知限制和复现条件。
实验步骤¶
- 用 reshape/permute 手写 patchify,再重组图像验证无信息丢失。
- 实现 CLIP loss,验证交换图文顺序后双向损失含义。
- 画出 BLIP-2 三个模块的梯度流,标注冻结与训练参数。
- 为一条图文多轮样本画出最终 token 序列和 label mask。
- 设计多模态数据卡,含图像来源、许可、分辨率、OCR 质量、语言和任务标签。
- 对三个分辨率样本打印视觉 token 数、总长度和峰值显存,制定 batch 规则。
每完成一步,先写下预期,再运行代码。若结果与预期不一致,不要覆盖旧日志;建立 failures.md,记录现象、假设、证据、修复和回归测试。这样得到的不是一次性 Demo,而是一份能证明你真正理解本章内容的实验档案。
验收标准¶
- 全新环境能够按照 README 从头运行,依赖和随机种子已记录。
- 关键函数至少有正常、边界和错误输入三类测试;涉及数值计算时检查有限值与合理误差。
- 结果包含一个基线和至少一个受控改动,能够说明变化来自哪里。
- 日志保留输入规模、耗时、内存或显存、软件版本和失败样本。
- 结论区分“实验直接证明的事实”“根据事实做出的推断”和“仍未验证的猜想”。
本章自测¶
- 不看正文,用自己的话解释“ViT 把图像切成固定大小 patch,将每块展平并线性投影为视觉 token,再像文本 token 一样使用 Transformer”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“CLIP 用图像编码器和文本编码器把配对样本映射到同一空间,通过批内对比学习拉近匹配对、推远不匹配对”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“BLIP 统一理解与生成目标,BLIP-2 用 Q-Former 在冻结视觉编码器与冻结语言模型之间提取少量查询表示,降低端到端训练成本”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“LLaVA 典型路线先训练视觉—语言投影层,再用图文指令数据联合微调,让语言模型学会在对话中使用视觉 token”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“多模态数据至少包含媒体、消息、任务类型、来源与许可”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“微调视觉语言模型要以官方 processor 和 chat template 为准,先确认模型怎样插入视觉 token,再决定冻结、LoRA 目标和像素预算”,并给出一个可以证伪的测试。
回答时先画图或写形状,再给结论。若只能说出术语而不能给出最小例子、边界条件和验证方法,说明这一节仍需要回到代码中重做。