本章学习目标
- 理解多模态AI的基本概念,区分单模态与多模态模型的差异
- 掌握CLIP、BLIP-2、GPT-4V等主流多模态模型的架构原理
- 学会使用Python加载预训练视觉-语言模型进行图文理解
- 了解多模态融合的三种主要策略:早期融合、晚期融合与交叉注意力
- 能够构建简单的图文检索和图像描述生成应用
- 理解多模态对齐、跨模态表示学习的核心思想
- 认识多模态AI的局限性和未来发展方向
前置知识
- 第11章:神经网络基础 —— 理解前馈网络和反向传播
- 第16章:自然语言处理 —— 掌握Transformer编码器和注意力机制
- 第20章:卷积神经网络 —— 了解CNN的基本结构和卷积操作
- Python基础与PyTorch使用经验
- 了解softmax、交叉熵损失等基本概念
图27-1 多模态AI架构:文本、图像、音频、视频模态的融合与对齐
人类感知世界的方式从来不是单一的 —— 我们同时用眼睛看、耳朵听、大脑综合理解。当你看到一张朋友在海边微笑的照片时,你的大脑同时处理了视觉信息(面孔、表情、海浪)和语义信息("这是我的朋友"、"开心"、"度假")。多模态AI正是模仿这种能力,让机器能够同时处理和关联来自不同来源(文本、图像、音频、视频)的信息。本章将带你从概念到实践,系统认识这一前沿领域。
27.1 什么是多模态AI
多模态 (Multimodal)
指的是两种或两种以上的信息模态。在AI领域,常见的模态包括:
- 文本 (Text) — 自然语言描述、对话、文档、代码
- 图像 (Image) — 照片、图表、截图、医学影像
- 音频 (Audio) — 语音、音乐、环境声、音频事件
- 视频 (Video) — 时序图像序列,包含视觉和听觉双重信息
- 3D/深度 (Depth) — 点云、深度图、3D网格模型
- 触觉/力觉 (Tactile) — 机器人触觉传感器数据
- 传感器数据 (Sensor) — 激光雷达、IMU、GPS等
单模态模型(如纯文本GPT-3、ResNet图像分类器、Whisper语音识别)只处理一种类型的数据。而多模态模型的核心目标是建立跨模态关联——理解"一张猫的图片"和"一只猫"这两个不同模态的表达指向同一概念。
💡 多模态的数学直觉
从数学角度看,多模态融合本质上是将不同模态的表示向量映射到一个共享的语义空间(Shared Semantic Space)。在这个空间中,语义相近的内容(无论来自哪个模态)应当具有相近的向量表示。用公式表达就是:给定图像 \(I\) 和文本 \(T\),我们希望 \(\text{sim}(f_I(I), f_T(T))\) 在语义匹配时最大化。
为什么需要多模态?现实世界的信息天然是多模态的。医生需要同时查看影像(CT/MRI)和文本病历来诊断疾病;电商客服需要理解用户上传的商品图片和文字描述才能准确回答问题;自动驾驶系统需要融合摄像头、激光雷达、高精地图和语音指令才能安全行驶。单一模态无法提供完整的语义理解。
模态的详细分类与特征
每种模态都有独特的数据结构和处理方式。理解这些差异是设计多模态系统的第一步。例如,文本是离散的符号序列,而图像是连续的像素矩阵;音频具有时频双重结构,而视频则在图像基础上增加了时间维度。不同的数据结构决定了它们需要不同的编码器和预处理流程。
| 模态 |
数据结构 |
典型编码器 |
特征维度示例 |
token数量(224×224图像/512词文本) |
| 文本 |
离散符号序列 |
Transformer (BERT/GPT) |
768 / 4096 |
~512 tokens |
| 图像 |
连续像素矩阵 (H×W×C) |
CNN / ViT |
768 / 1024 |
~196-576 patches |
| 音频 |
时频谱图 / 波形 |
Whisper / HuBERT |
512 / 768 |
~3000 frames/30s |
| 视频 |
时序图像帧序列 |
ViT + Temporal Attention |
768 / 1024 |
~数千至数万tokens |
| 3D/点云 |
非结构化点集 (N×3) |
PointNet / PointBERT |
256 / 512 |
~1024-8192 points |
多模态系统的设计核心挑战在于:如何让这些结构迥异的数据在同一个语义空间中对齐。这不仅仅是维度对齐的问题,更是语义层面的对齐——让"猫"这个文字和猫的图像在数学上表示为相似的向量。
多模态AI的发展历程
多模态AI并非突然出现,而是经历了几十年的渐进发展。早期的研究主要集中在图像描述生成(Image Captioning)和视觉问答(VQA)这两个任务上。2021年,OpenAI发布的CLIP标志着多模态AI进入了新的阶段——通过大规模对比学习,模型可以将图像和文本映射到同一空间。2023年,GPT-4V和BLIP-2的出现将多模态AI推向了对话式理解的新高度。
多模态AI关键里程碑
2014-2015
Visual Attention / show-and-tell
图像描述的开端
2017-2019
ViLBERT / VisualBERT
预训练多模态模型
2021
CLIP / DALL-E / ALIGN
对比学习 + 大规模预训练
2022-2023
BLIP-2 / LLaVA / GPT-4V
对话式多模态理解
2024-2025
Gemini / Sora / GPT-4o
原生多模态 + 生成
值得注意的是,多模态AI的发展与大语言模型(LLM)的进步密切相关。早期的多模态模型(如VisualBERT)参数量只有几亿,而如今的GPT-4V、Gemini等模型参数量已达数千亿甚至万亿级别。这种规模的提升使得模型能够处理更复杂的跨模态推理任务,例如理解图像中的幽默、推理场景中物体的因果关系等。
为什么现在是多模态AI的爆发期?
三个关键因素推动了多模态AI的快速发展:
- 大规模配对数据:互联网上存在海量的图文对(如网页图片与Alt文本)、视频与字幕等天然的多模态数据。CLIP正是利用了从互联网爬取的4亿个图文对进行训练。
- 预训练视觉编码器的成熟:ViT、CLIP ViT等视觉编码器已经能够在大规模数据上提取高质量的视觉特征。这使得多模态模型可以站在"巨人的肩膀上",无需从头训练视觉编码器。
- 大语言模型的涌现能力:LLaMA、GPT等大语言模型展现出了强大的推理和泛化能力。通过将视觉信息"注入"这些模型,我们可以直接利用这些能力处理视觉任务,而无需为每个视觉任务单独训练模型。
多模态AI应用场景
图文检索
(Image-Text Retrieval)
单模态 vs 多模态:核心差异
| 维度 |
单模态模型 |
多模态模型 |
| 输入 |
单一类型(仅文本或仅图像) |
多种类型(文本+图像+...) |
| 训练数据 |
单模态标注数据 |
跨模态配对数据 |
| 表示空间 |
单一语义空间 |
共享对齐语义空间 |
| 典型架构 |
Transformer / CNN |
多编码器 + 融合模块 |
| 代表模型 |
GPT-4, ResNet, BERT |
GPT-4V, BLIP-2, Gemini |
27.2 核心架构:视觉编码器与语言模型的融合
多模态大模型的主流架构可以概括为一个经典三段式:视觉编码器 → 投影层/融合模块 → 语言模型。这种架构的关键在于,将视觉信息"翻译"成语言模型能够理解的表示。
为什么是这样的三段式?因为语言模型(如LLaMA、GPT)是目前最强大的"思考引擎",它拥有强大的推理、规划和生成能力。如果我们能让语言模型"看懂"图像,就能直接利用这些能力进行视觉推理。因此,多模态大模型的设计哲学是:让视觉编码器负责"看",让语言模型负责"想",中间用一个轻量级的模块来连接两者。
架构图解
多模态融合架构:视觉编码器与语言模型的融合策略
视觉编码器选型
视觉编码器负责将原始图像转换为特征向量序列。它是多模态模型"看"世界的"眼睛"。不同的视觉编码器有不同的特性,选择合适的编码器对最终性能至关重要。
视觉编码器的核心任务是将二维图像转换为一组特征向量(tokens)。在ViT架构中,一张224×224的图像首先被切分为196个16×16的patch,每个patch通过线性投影映射为一个特征向量。加上一个可学习的[CLS]token后,这些向量被送入标准的Transformer编码器。最终,编码器输出197个(1 CLS + 196 patch)特征向量,每个向量的维度取决于模型大小(如ViT-B/16为768维)。
为什么ViT比CNN更适合多模态?CNN的卷积操作本质上是局部特征提取,而Transformer的自注意力机制可以捕获全局关系。对于多模态任务,模型需要理解图像中不同区域与文本中不同词语的对应关系,这种全局关系建模正是Transformer的优势所在。此外,ViT输出的特征向量序列可以直接与文本token序列拼接,无需额外的特征转换。
- ViT (Vision Transformer):将图像切分为16×16的patch序列,用Transformer编码器处理。这是当前多模态模型的首选架构,因为它天然输出序列特征,便于与文本token对接。
- CLIP ViT:经过图文对比学习预训练的ViT,天然具备语义对齐能力。CLIP ViT的特征更"语义化",因为它在训练时就学会了与文本对齐。
- SigLIP:Google提出的改进CLIP变体,使用sigmoid损失替代softmax,在大规模训练中更稳定,被Gemini系列使用。
- DINOv2:Meta提出的自监督ViT,不依赖文本,但在视觉任务上表现出色。适合需要精细视觉特征的场景。
投影层的设计
投影层是多模态模型中最灵活的部分,它的设计直接决定了视觉信息如何被语言模型"消化"。根据不同的设计思路,投影层可以分为以下几种类型:
1. 线性投影(Linear Projection):最简单的方法,使用一个线性变换将视觉特征维度映射到语言模型的输入维度。优点是计算量小、训练快;缺点是表达能力有限,无法学习复杂的跨模态关系。
2. MLP投影(MLP Adapter):使用多层感知机进行非线性映射。LLaVA采用的方法是两层MLP(视觉维度→4倍中间维度→语言模型维度),中间使用GELU激活函数。相比线性投影,MLP能学习更复杂的映射关系。
3. Q-Former:BLIP-2提出的方法,使用一组可学习的查询向量(通常32个)从视觉特征中提取信息。这些查询向量通过交叉注意力机制与视觉特征交互,然后输出固定数量的特征向量送给语言模型。Q-Former的优势是能够自适应地选择最相关的视觉信息,而非被动地接收所有视觉特征。
4. 交叉注意力(Cross-Attention):在语言模型的每一层中插入交叉注意力模块,让文本token直接与视觉特征交互。这种方法提供了最深层的跨模态交互,但计算量最大。
选择哪种投影层取决于任务需求和资源限制。对于快速原型开发,MLP投影是最佳选择;对于追求最佳性能,交叉注意力值得尝试;对于资源受限的场景,线性投影或Q-Former更为合适。
import torch
import torch.nn as nn
class LinearProjection(nn.Module):
def __init__(self, vision_dim, llm_dim):
super().__init__()
self.proj = nn.Linear(vision_dim, llm_dim)
def forward(self, vision_features):
return self.proj(vision_features)
class MLPProjection(nn.Module):
def __init__(self, vision_dim, llm_dim, hidden_mult=4):
super().__init__()
self.proj = nn.Sequential(
nn.Linear(vision_dim, llm_dim * hidden_mult),
nn.GELU(),
nn.Linear(llm_dim * hidden_mult, llm_dim),
)
def forward(self, vision_features):
return self.proj(vision_features)
class QFormerProjection(nn.Module):
def __init__(self, vision_dim, llm_dim, num_queries=32, num_heads=8):
super().__init__()
self.queries = nn.Parameter(torch.randn(1, num_queries, llm_dim))
self.cross_attn = nn.MultiheadAttention(
embed_dim=llm_dim, num_heads=num_heads,
kdim=vision_dim, vdim=vision_dim, batch_first=True
)
self.norm = nn.LayerNorm(llm_dim)
def forward(self, vision_features):
batch_size = vision_features.shape[0]
queries = self.queries.expand(batch_size, -1, -1)
attended, _ = self.cross_attn(queries, vision_features, vision_features)
return self.norm(attended)
💡 投影层设计的关键原则
无论选择哪种投影层,设计时都需要考虑三个关键因素:(1)维度匹配:视觉编码器输出维度和语言模型输入维度通常不匹配,投影层的首要任务是解决维度不一致问题;(2)信息压缩:视觉特征的token数量通常远多于语言模型能处理的长度,需要进行信息压缩(如Q-Former)或降采样;(3)训练效率:投影层的参数量应该远小于语言模型,以保证训练的可行性。
import torch
from PIL import Image
from transformers import CLIPVisionModel, CLIPImageProcessor
model_name = "openai/clip-vit-base-patch32"
processor = CLIPImageProcessor.from_pretrained(model_name)
model = CLIPVisionModel.from_pretrained(model_name)
model.eval()
image = Image.open("example.jpg")
inputs = processor(images=image, return_tensors="pt")
with torch.no_grad():
outputs = model(**inputs)
print(f"最后隐藏层形状: {outputs.last_hidden_state.shape}")
# [1, 50, 768] -> [batch, 1个CLS + 49个patch, 特征维度]
print(f"池化输出形状: {outputs.pooler_output.shape}")
# [1, 768] -> 全局图像表示
patch_features = outputs.last_hidden_state[:, 1:, :]
print(f"Patch特征形状: {patch_features.shape}")
# [1, 49, 768] -> 7x7=49个视觉token
💡 关键理解:patch embedding
ViT将224×224的图像切成196个16×16的patch,每个patch被线性映射为一个768维向量。加上一个可学习的[CLS]token和位置编码后,序列送入Transformer编码器。这意味着一张图像在多模态模型眼中,本质上是一串"视觉token"——就像文本被分词成word token一样。理解这个类比是理解多模态架构的关键。
投影层:连接视觉与语言
视觉编码器输出的特征维度(如768)通常与语言模型的输入维度(如4096)不一致。投影层(Projection Layer)的作用就是完成这个维度对齐。不同模型采用了不同的投影策略:
- 线性投影:最简单,一个全连接层即可(LLaVA最初方案)。优点是轻量,缺点是表达能力有限。
- MLP投影:两层MLP + GELU激活,略有非线性(LLaVA-1.5方案)。实践中效果好于线性投影,但增加的参数量很少。
- Q-Former:BLIP-2使用可学习查询向量通过交叉注意力从视觉特征中提取信息。这种方法更强大,可以主动选择视觉信息中最相关的部分,但也更复杂。
- C-Abstractor:Honeybee模型提出的卷积投影,通过跨步卷积压缩视觉token数量,在效率和效果之间取得平衡。
import torch
import torch.nn as nn
class VisionProjection(nn.Module):
def __init__(self, vision_dim=768, llm_dim=4096):
super().__init__()
self.projection = nn.Sequential(
nn.Linear(vision_dim, llm_dim),
nn.GELU(),
nn.Linear(llm_dim, llm_dim),
)
def forward(self, vision_features):
return self.projection(vision_features)
vision_proj = VisionProjection(vision_dim=768, llm_dim=4096)
dummy_vision = torch.randn(1, 50, 768)
projected = vision_proj(dummy_vision)
print(f"投影后形状: {projected.shape}") # [1, 50, 4096]
⚠️ 注意:位置编码的处理
当图像patch经过投影后插入文本token序列时,需要为这些视觉token分配位置编码。如果语言模型使用RoPE(旋转位置编码),需要特别处理,因为视觉token和文本token的相对位置关系与纯文本序列不同。LLaVA系列的做法是直接延续文本的位置索引,即视觉token被视为序列开头的特殊token。这种处理虽然简单但在实践中效果不错。
完整推理流程
理解了各组件后,让我们把它们组合起来,看看一个完整的多模态推理是如何进行的。以"描述这张图片"为例:
- 用户输入图像和文本指令 "描述这张图片"
- 视觉编码器将图像转换为50个视觉token(1个CLS + 49个patch)
- 投影层将50个768维向量转换为50个4096维向量
- 文本分词器将指令编码为若干文本token
- 所有token(视觉 + 文本)拼接后送入语言模型
- 语言模型自回归地生成描述文本
import torch
from transformers import CLIPVisionModel, AutoTokenizer, AutoModelForCausalLM
vision_encoder = CLIPVisionModel.from_pretrained("openai/clip-vit-base-patch32")
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b-chat-hf")
llm = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-7b-chat-hf")
projection = VisionProjection(vision_dim=768, llm_dim=4096)
def multimodal_inference(image, text_prompt):
# Step 1: 编码图像
vision_features = vision_encoder(image).last_hidden_state
# Step 2: 投影到LLM空间
visual_tokens = projection(vision_features)
# Step 3: 编码文本
text_tokens = tokenizer(text_prompt, return_tensors="pt")
# Step 4: 拼接视觉和文本token
inputs_embeds = torch.cat([
visual_tokens,
llm.get_input_embeddings()(text_tokens.input_ids)
], dim=1)
# Step 5: LLM生成
outputs = llm.generate(inputs_embeds=inputs_embeds, max_new_tokens=200)
return tokenizer.decode(outputs[0])
27.3 三种融合策略
多模态融合的核心问题是:在模型的哪个阶段、以何种方式将不同模态的信息结合起来?根据融合时机的不同,分为三种主要策略。每种策略都有其适用场景和优缺点。
策略一:早期融合 (Early Fusion)
在模型的最底层就将不同模态的输入拼接在一起,让模型从头开始学习跨模态交互。代表模型:VisualBERT、FLAVA、ViLBERT。这种策略的优势是模型可以学习到最深层的跨模态交互,但缺点是需要大量的配对数据和计算资源。
早期融合的核心思想是将所有模态的token视为同一个序列。例如,对于一张图像和一段描述文本,VisualBERT会将196个图像patch token和若干文本token拼接成一个序列,然后送入标准的Transformer编码器。由于自注意力机制的存在,每个token都可以"关注"其他所有token——图像patch可以关注文本token,文本token也可以关注图像patch。这使得模型能够学习到最深层次的跨模态交互。
早期融合的代价是计算量。Transformer的自注意力复杂度为 \(O(n^2)\),其中 \(n\) 是序列长度。当图像和文本的token拼接后,序列长度显著增加,导致计算量急剧上升。这也是为什么早期融合方案通常只适用于较短的文本和较小的图像输入。
import torch
import torch.nn as nn
class EarlyFusionModel(nn.Module):
def __init__(self, vocab_size, embed_dim=768, n_heads=8, n_layers=6):
super().__init__()
self.text_embedding = nn.Embedding(vocab_size, embed_dim)
self.vision_projection = nn.Linear(768, embed_dim)
self.modality_embedding = nn.Embedding(2, embed_dim)
encoder_layer = nn.TransformerEncoderLayer(
d_model=embed_dim, nhead=n_heads, batch_first=True
)
self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=n_layers)
def forward(self, text_tokens, vision_features):
text_emb = self.text_embedding(text_tokens)
vis_emb = self.vision_projection(vision_features)
text_mod = self.modality_embedding(
torch.zeros(text_emb.size(1), dtype=torch.long, device=text_emb.device)
)
vis_mod = self.modality_embedding(
torch.ones(vis_emb.size(1), dtype=torch.long, device=vis_emb.device)
)
combined = torch.cat([text_emb + text_mod, vis_emb + vis_mod], dim=1)
output = self.transformer(combined)
return output
策略二:晚期融合 (Late Fusion)
各模态独立编码,在最后阶段才进行交互。代表模型:CLIP、ALIGN。CLIP分别编码图像和文本,通过对比损失学习对齐,但两个编码器之间没有直接的注意力交互。这种策略的优势是训练效率高、可以独立优化各模态编码器,但缺点是缺乏深层交互。
晚期融合的最大优势是模块化和效率。由于各模态编码器独立运行,它们可以分别预训练、分别优化。例如,CLIP的视觉编码器可以被直接提取出来用于其他视觉任务(如目标检测、图像分割),无需重新训练。这种模块化设计极大地降低了多模态系统的开发和部署成本。
然而,晚期融合的局限性也很明显。由于模态间没有直接的注意力交互,模型无法学习到细粒度的跨模态关系。例如,对于"红色的猫在蓝色的沙发上"这个描述,晚期融合模型可能分别识别出"红色"、"猫"、"蓝色"、"沙发"这些概念,但无法精确理解它们之间的空间和属性关系。这也是为什么CLIP在组合性描述上的表现不如预期。
import torch
import torch.nn.functional as F
def clip_contrastive_loss(image_features, text_features, temperature=0.07):
image_features = F.normalize(image_features, dim=-1)
text_features = F.normalize(text_features, dim=-1)
logits = torch.matmul(image_features, text_features.T) / temperature
batch_size = image_features.size(0)
labels = torch.arange(batch_size, device=logits.device)
loss_i2t = F.cross_entropy(logits, labels)
loss_t2i = F.cross_entropy(logits.T, labels)
return (loss_i2t + loss_t2i) / 2
策略三:交叉注意力融合 (Cross-Attention)
在Transformer层之间插入交叉注意力模块,让一种模态能够"关注"另一种模态的信息。这是当前多模态大模型最主流的方案,代表模型:BLIP-2、Flamingo、GPT-4V。交叉注意力在效果和效率之间取得了最佳平衡。
import torch
import torch.nn as nn
import math
class CrossAttentionFusion(nn.Module):
def __init__(self, dim=768, n_heads=8):
super().__init__()
self.n_heads = n_heads
self.head_dim = dim // n_heads
self.q_proj = nn.Linear(dim, dim)
self.k_proj = nn.Linear(dim, dim)
self.v_proj = nn.Linear(dim, dim)
self.out_proj = nn.Linear(dim, dim)
self.norm = nn.LayerNorm(dim)
def forward(self, text_features, vision_features):
B, T, D = text_features.shape
V = vision_features.size(1)
Q = self.q_proj(text_features).view(B, T, self.n_heads, self.head_dim).transpose(1, 2)
K = self.k_proj(vision_features).view(B, V, self.n_heads, self.head_dim).transpose(1, 2)
V_feat = self.v_proj(vision_features).view(B, V, self.n_heads, self.head_dim).transpose(1, 2)
attn = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.head_dim)
attn = torch.softmax(attn, dim=-1)
out = torch.matmul(attn, V_feat)
out = out.transpose(1, 2).contiguous().view(B, T, D)
out = self.out_proj(out)
return self.norm(text_features + out)
三种策略对比
| 策略 |
融合时机 |
优点 |
缺点 |
代表模型 |
| 早期融合 |
输入层 |
最灵活,深层交互 |
计算量大,需大量数据 |
VisualBERT, FLAVA |
| 晚期融合 |
输出层 |
高效,可独立优化 |
交互有限 |
CLIP, ALIGN |
| 交叉注意力 |
中间层 |
效果与效率平衡 |
架构复杂度中等 |
BLIP-2, GPT-4V |
✅ 实践建议
如果你在构建自己的多模态模型,MLP投影(晚期融合变体)是最佳起点——简单、高效、效果好。只有在需要更精细的视觉-语言交互时,才考虑引入交叉注意力或Q-Former。
27.4 CLIP:连接视觉与语言的桥梁
CLIP(Contrastive Language-Image Pre-training)是OpenAI于2021年发布的里程碑式工作。它通过在4亿个图文对上进行对比学习,学会了将图像和文本映射到同一语义空间。CLIP之所以重要,是因为它为后续几乎所有多模态大模型提供了视觉编码器的基础。
CLIP的训练目标
CLIP对比学习原理:图像-文本对齐训练过程
CLIP的训练非常直觉:在一个batch中,有N个图文对。模型需要学会让正确匹配的图文对相似度最高,同时让不匹配的对相似度最低。这本质上是一个N选1的分类问题。
\[ \mathcal{L} = -\frac{1}{2N}\sum_{i=1}^{N} \left[\log \frac{e^{\text{sim}(I_i, T_i)/\tau}}{\sum_{j=1}^{N} e^{\text{sim}(I_i, T_j)/\tau}} + \log \frac{e^{\text{sim}(T_i, I_i)/\tau}}{\sum_{j=1}^{N} e^{\text{sim}(T_i, I_j)/\tau}}\right] \]
其中 \(\text{sim}(\cdot,\cdot)\) 为余弦相似度,\(\tau\) 为可学习的温度参数
这个损失函数的设计非常巧妙:分子只计算正确匹配对的相似度,分母计算所有可能对的相似度之和。当损失最小时,正确匹配对的相似度在所有对中占主导地位。温度参数 \(\tau\) 控制了相似度分布的"锐度"——较小的 \(\tau\) 会让分布更尖锐,模型更"自信"。
import torch
from PIL import Image
from transformers import CLIPProcessor, CLIPModel
model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32")
processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")
image = Image.open("cat.jpg")
candidate_labels = ["猫", "狗", "汽车", "飞机", "花"]
text_prompts = [f"一张{label}的照片" for label in candidate_labels]
inputs = processor(
text=text_prompts,
images=image,
return_tensors="pt",
padding=True
)
with torch.no_grad():
outputs = model(**inputs)
logits = outputs.logits_per_image
probs = logits.softmax(dim=-1)
for label, prob in zip(candidate_labels, probs[0]):
print(f" {label}: {prob:.4f}")
💡 Prompt Engineering提升零样本性能
直接使用类别标签(如"猫")作为文本输入效果一般。加上上下文描述(如"一张猫的照片"、"这是一个{label}")可以显著提升准确率。这是因为CLIP在训练时看到的文本通常是完整的句子描述,而非孤立的词语。研究发现,使用集成多个prompt的策略(如"一张{label}的照片"、"{label}的特写"、"一张{label}的图像")可以进一步提升1-2%的准确率。
CLIP的局限性
- 缺乏生成能力:CLIP只能判断图文匹配度,不能生成文本描述或图像。它是一个判别模型而非生成模型。
- 组合性较弱:对于"红色的猫在蓝色的车上"这种组合描述,CLIP的理解有限。它更擅长识别"红色"、"猫"、"车"等独立概念,但对它们的组合关系理解不足。这是对比学习的固有局限——它学习的是全局相似度,而非细粒度的组成关系。
- 文化偏见:训练数据以英文为主,中文场景需要额外适配。某些文化特有的概念可能无法准确理解。
- 空间理解不足:对于"猫在桌子下面"这类空间关系,CLIP经常判断错误。这是因为对比学习的目标是全局语义对齐,而非局部空间关系建模。
- 对对抗攻击敏感:微小的图像扰动(人类不可见)可能改变CLIP的判断结果,这在安全关键场景中是一个隐患。
尽管存在这些局限性,CLIP的影响力是不可否认的。几乎所有的多模态大模型都使用CLIP ViT作为视觉编码器的基础。理解CLIP的工作原理,是理解现代多模态AI系统的第一步。在下一节中,我们将看到BLIP-2如何通过Q-Former来弥补CLIP在生成能力上的不足。
import torch
import torch.nn as nn
from transformers import CLIPModel, CLIPProcessor
class CLIPFeatureExtractor:
def __init__(self, model_name="openai/clip-vit-base-patch32"):
self.device = "cuda" if torch.cuda.is_available() else "cpu"
self.model = CLIPModel.from_pretrained(model_name).to(self.device)
self.processor = CLIPProcessor.from_pretrained(model_name)
self.model.eval()
def get_image_embedding(self, image_path):
image = Image.open(image_path).convert("RGB")
inputs = self.processor(images=image, return_tensors="pt")
inputs = {k: v.to(self.device) for k, v in inputs.items()}
with torch.no_grad():
features = self.model.get_image_features(**inputs)
return features / features.norm(dim=-1, keepdim=True)
def get_text_embedding(self, text):
inputs = self.processor(text=[text], return_tensors="pt", padding=True)
inputs = {k: v.to(self.device) for k, v in inputs.items()}
with torch.no_grad():
features = self.model.get_text_features(**inputs)
return features / features.norm(dim=-1, keepdim=True)
def compute_similarity(self, image_path, text):
img_emb = self.get_image_embedding(image_path)
txt_emb = self.get_text_embedding(text)
return (img_emb @ txt_emb.T).item()
27.5 BLIP-2与视觉语言大模型
BLIP-2(Bootstrapping Language-Image Pre-training 2)是Salesforce提出的高效多模态大模型架构。它的核心创新是Q-Former——一个轻量级的桥梁模块,用可学习的查询向量从冻结的视觉编码器中提取信息,然后传递给冻结的语言模型。这种设计大幅降低了训练成本,同时保持了强大的多模态理解能力。
Q-Former的工作原理
Q-Former包含一组可学习的查询向量(通常32个),它们通过交叉注意力机制从视觉特征中提取信息。这种设计有两个优势:(1)大幅减少了需要训练的参数量——视觉编码器和LLM都被冻结,只训练Q-Former;(2)查询向量可以学到"该关注图像的哪些部分",实现信息的自适应提取。
BLIP-2的两阶段训练
阶段一:视觉-语言表示学习
图文对比学习(ITC)
图文匹配(ITM)
图像描述生成(ITG)
目标:让Q-Former学会提取视觉语义
阶段二:视觉-语言生成学习
连接冻结的LLM
仅训练Q-Former + 投影层
冻结视觉编码器和LLM
目标:让Q-Former学会向LLM传递信息
import torch
from PIL import Image
from transformers import Blip2Processor, Blip2ForConditionalGeneration
processor = Blip2Processor.from_pretrained("Salesforce/blip2-opt-2.7b")
model = Blip2ForConditionalGeneration.from_pretrained(
"Salesforce/blip2-opt-2.7b",
torch_dtype=torch.float16,
device_map="auto"
)
image = Image.open("street_scene.jpg")
inputs = processor(images=image, return_tensors="pt").to(device="cuda", dtype=torch.float16)
generated_ids = model.generate(**inputs, max_new_tokens=100)
caption = processor.batch_decode(generated_ids, skip_special_tokens=True)[0]
print(f"描述: {caption}")
question = "图片中有什么交通工具?"
inputs = processor(images=image, text=question, return_tensors="pt")
inputs = {k: v.to(device="cuda", dtype=torch.float16) for k, v in inputs.items()}
generated_ids = model.generate(**inputs, max_new_tokens=50)
answer = processor.batch_decode(generated_ids, skip_special_tokens=True)[0]
print(f"问题: {question}")
print(f"回答: {answer}")
💡 GPT-4V、Gemini与开源替代
GPT-4V和Gemini是目前最强大的多模态大模型。虽然它们的具体架构未公开,但从技术报告和学术推测来看,都采用了类似的"视觉编码器 + 融合模块 + 大语言模型"架构。GPT-4V支持同时处理多达20张图像,Gemini原生支持图像、音频和视频的混合输入。对于开源替代方案,LLaVA系列(如LLaVA-NeXT)、InternVL、Qwen-VL等都提供了不错的性能。
主流多模态模型对比
| 模型 |
视觉编码器 |
语言模型 |
融合方式 |
参数量 |
| LLaVA-1.5 |
CLIP ViT-L/14 |
Vicuna-7B/13B |
MLP投影 |
7B-13B |
| BLIP-2 |
EVA-CLIP ViT-G |
FlanT5-XXL / OPT-6.7B |
Q-Former |
12B |
| InternVL-2 |
InternViT-6B |
InternLM2-Chat |
MLP投影 |
8B-76B |
| Qwen-VL-Max |
ViT (未公开) |
Qwen-72B |
交叉注意力 |
未公开 |
| GPT-4o |
原生多模态 (未公开) |
GPT-4系列 |
原生融合 |
未公开 |
| Gemini 1.5 Pro |
原生多模态 (未公开) |
Gemini系列 |
原生融合 |
未公开 |
多模态大模型的涌现能力
当模型规模达到一定阈值后,会表现出在小模型中不存在的能力,这种现象被称为"涌现能力"(Emergent Abilities)。在多模态领域,涌现能力尤为显著。例如,GPT-4V能够理解图像中的幽默、讽刺和文化隐喻——这些能力在小模型中完全不存在。它能够从X光片中诊断疾病、从卫星图像中估算GDP、从手绘草图中生成代码。
涌现能力的出现与模型的规模密切相关。研究表明,当多模态模型的参数量超过100亿时,视觉推理能力开始显著提升;超过1000亿时,模型能够进行复杂的跨模态推理,如理解"为什么这张照片是讽刺的?"这类需要文化背景知识的问题。这种非线性的能力提升给模型训练带来了启示:与其精心设计小模型的架构,不如扩大模型规模并提供高质量的多模态数据。
然而,涌现能力也带来了不可预测性。我们无法事先知道模型会"涌现"出什么能力,也无法保证涌现出的能力是安全的。这要求我们在部署大规模多模态模型时,必须进行充分的安全评估和对齐。
27.6 实战:构建图文检索系统
图文检索是多模态AI的经典应用场景:给定一段文字,从图片库中找到最相关的图片(Text→Image Retrieval),或给定一张图片找到最匹配的文字描述(Image→Text Retrieval)。我们利用CLIP的对齐表示来实现一个完整的图文检索系统。
检索系统的评估指标
评估图文检索系统的质量需要使用专门的指标。不同于简单的分类准确率,检索任务的评估需要考虑排名(ranking)——正确结果出现在前几位比出现在后几位更好。以下是图文检索中最常用的评估指标。
Recall@K:在前K个检索结果中,包含至少一个正确结果的比例。例如,Recall@1表示第一个结果就是正确结果的比例,Recall@5表示前5个结果中包含正确结果的比例。这是图文检索中最核心的指标。
MRR(Mean Reciprocal Rank):正确结果排名的倒数的平均值。如果正确结果排在第1位,得分为1;排在第2位,得分为0.5;排在第3位,得分为0.33。MRR越高,说明正确结果的排名越靠前。
mAP(Mean Average Precision):综合考虑所有正确结果的排名位置和数量的指标。它是信息检索领域最全面的评估指标,同时衡量了检索的准确性和完整性。
在实际应用中,通常同时报告Recall@1、Recall@5、Recall@10和mAP。一个好的图文检索系统应该在这些指标上都达到较高水平。
import torch
import numpy as np
from PIL import Image
from pathlib import Path
from transformers import CLIPModel, CLIPProcessor
class CLIPImageRetriever:
def __init__(self, model_name="openai/clip-vit-base-patch32"):
self.device = "cuda" if torch.cuda.is_available() else "cpu"
self.model = CLIPModel.from_pretrained(model_name).to(self.device)
self.processor = CLIPProcessor.from_pretrained(model_name)
self.model.eval()
self.image_paths = []
self.image_features = None
def encode_images(self, image_dir, extensions=("*.jpg", "*.png", "*.jpeg")):
image_dir = Path(image_dir)
self.image_paths = []
features_list = []
for ext in extensions:
self.image_paths.extend(sorted(image_dir.glob(ext)))
print(f"正在编码 {len(self.image_paths)} 张图像...")
for img_path in self.image_paths:
image = Image.open(img_path).convert("RGB")
inputs = self.processor(images=image, return_tensors="pt")
inputs = {k: v.to(self.device) for k, v in inputs.items()}
with torch.no_grad():
features = self.model.get_image_features(**inputs)
features = features / features.norm(dim=-1, keepdim=True)
features_list.append(features.cpu())
self.image_features = torch.cat(features_list, dim=0)
print(f"编码完成,特征矩阵形状: {self.image_features.shape}")
def search_by_text(self, query, top_k=5):
inputs = self.processor(text=[query], return_tensors="pt", padding=True)
inputs = {k: v.to(self.device) for k, v in inputs.items()}
with torch.no_grad():
text_features = self.model.get_text_features(**inputs)
text_features = text_features / text_features.norm(dim=-1, keepdim=True)
similarities = (text_features.cpu() @ self.image_features.T).squeeze(0)
top_indices = similarities.argsort(descending=True)[:top_k]
results = []
for idx in top_indices:
results.append({
"path": str(self.image_paths[idx]),
"score": similarities[idx].item()
})
return results
def search_by_image(self, image_path, top_k=5):
image = Image.open(image_path).convert("RGB")
inputs = self.processor(images=image, return_tensors="pt")
inputs = {k: v.to(self.device) for k, v in inputs.items()}
with torch.no_grad():
query_features = self.model.get_image_features(**inputs)
query_features = query_features / query_features.norm(dim=-1, keepdim=True)
similarities = (query_features.cpu() @ self.image_features.T).squeeze(0)
top_indices = similarities.argsort(descending=True)[1:top_k+1]
results = []
for idx in top_indices:
results.append({
"path": str(self.image_paths[idx]),
"score": similarities[idx].item()
})
return results
retriever = CLIPImageRetriever()
retriever.encode_images("./images")
results = retriever.search_by_text("一只在草地上奔跑的狗", top_k=3)
for r in results:
print(f" {r['score']:.4f} - {r['path']}")
💡 提升检索性能的技巧
- 数据增强:对查询图像做裁剪、翻转等增强,取平均特征可以提升鲁棒性
- Prompt Engineering:文本查询加入"一张...的照片"前缀
- 特征缓存:对于固定图库,编码一次即可,避免重复计算
- 向量索引:大规模场景使用FAISS或Milvus加速检索
- 多尺度检索:对图像进行多尺度裁剪,检索后聚合结果
import numpy as np
from typing import List, Dict
class RetrievalEvaluator:
@staticmethod
def recall_at_k(retrieved: List[List[int]], ground_truth: List[List[int]], k: int) -> float:
total = 0
for retrieved_ids, gt_ids in zip(retrieved, ground_truth):
gt_set = set(gt_ids)
hits = sum(1 for rid in retrieved_ids[:k] if rid in gt_set)
total += 1 if hits > 0 else 0
return total / len(retrieved)
@staticmethod
def mean_reciprocal_rank(retrieved: List[List[int]], ground_truth: List[List[int]]) -> float:
rr_sum = 0
for retrieved_ids, gt_ids in zip(retrieved, ground_truth):
gt_set = set(gt_ids)
for rank, rid in enumerate(retrieved_ids, 1):
if rid in gt_set:
rr_sum += 1.0 / rank
break
return rr_sum / len(retrieved)
@staticmethod
def mean_average_precision(retrieved: List[List[int]], ground_truth: List[List[int]]) -> float:
ap_sum = 0
for retrieved_ids, gt_ids in zip(retrieved, ground_truth):
gt_set = set(gt_ids)
hits = 0
precision_sum = 0
for rank, rid in enumerate(retrieved_ids, 1):
if rid in gt_set:
hits += 1
precision_sum += hits / rank
ap = precision_sum / len(gt_set) if gt_set else 0
ap_sum += ap
return ap_sum / len(retrieved)
def evaluate(self, retrieved: List[List[int]], ground_truth: List[List[int]]) -> Dict:
return {
"Recall@1": self.recall_at_k(retrieved, ground_truth, 1),
"Recall@5": self.recall_at_k(retrieved, ground_truth, 5),
"Recall@10": self.recall_at_k(retrieved, ground_truth, 10),
"MRR": self.mean_reciprocal_rank(retrieved, ground_truth),
"mAP": self.mean_average_precision(retrieved, ground_truth),
}
图像描述生成
图文检索的逆任务是图像描述生成(Image Captioning)——给定一张图像,自动生成自然语言描述。这是多模态AI最直观的应用之一。图像描述生成需要模型同时"理解"图像内容和"生成"自然语言,这比单纯的分类或检索更具挑战性。
图像描述生成经历了从模板方法到深度学习的演变。早期方法使用预定义的模板(如"一个[物体]在[位置][动作]"),生成的描述机械且缺乏细节。现代方法(如BLIP、BLIP-2)利用端到端的编码器-解码器架构,能够生成流畅、详细的描述。更高级的模型甚至可以生成包含推理的描述,例如"图片中有一只猫正在追逐一只蝴蝶,这可能是在花园里"。
from transformers import BlipProcessor, BlipForConditionalGeneration
from PIL import Image
processor = BlipProcessor.from_pretrained("Salesforce/blip-image-captioning-base")
model = BlipForConditionalGeneration.from_pretrained("Salesforce/blip-image-captioning-base")
image = Image.open("dog_park.jpg")
inputs = processor(image, return_tensors="pt")
output = model.generate(**inputs, max_new_tokens=50)
caption = processor.decode(output[0], skip_special_tokens=True)
print(f"无条件描述: {caption}")
inputs = processor(image, "a photo of", return_tensors="pt")
output = model.generate(**inputs, max_new_tokens=50)
caption = processor.decode(output[0], skip_special_tokens=True)
print(f"条件描述: {caption}")
27.7 多模态AI的未来与挑战
多模态AI正处于快速发展期,从"能看能说"向"能理解能推理能行动"演进。以下是该领域的关键趋势和尚未解决的挑战。
前沿趋势
多模态AI发展路线
2021
CLIP / DALL-E
图文对齐与生成
2023
GPT-4V / BLIP-2
对话式多模态理解
2024
Gemini / Sora
原生多模态 + 视频生成
核心挑战
- 幻觉问题:多模态模型可能"看到"图像中不存在的内容,生成看似合理但错误的描述。例如,给模型展示一张空房间里有一张桌子的图片,问"桌子上有几个苹果?"模型可能回答"3个"而非"没有苹果"。
- 空间理解:当前模型在判断物体的精确位置、数量和空间关系方面仍有不足。对于"左边的杯子"、"桌子下面的猫"这类描述,模型经常混淆。
- 长视频理解:处理长时间序列的视频(如电影理解)仍然困难。视频的token数量远超图像,对上下文窗口和计算资源提出巨大挑战。
- 跨文化理解:训练数据的文化偏见可能导致对不同文化的误读。某些文化特有的概念、符号、习俗可能无法被准确理解。
- 计算效率:多模态输入(尤其是视频)的token数量远超纯文本,对推理资源要求很高。一张图像可能产生数百个token,一段视频可能产生数万个token。
⚠️ 多模态幻觉的典型案例
多模态幻觉(Multimodal Hallucination)是当前多模态模型最严重的问题之一。模型可能:(1)描述图像中不存在的物体;(2)错误描述物体的属性(颜色、大小、位置);(3)编造不存在的场景关系。这种幻觉源于语言模型的先验偏见——它在训练数据中见过太多"桌子上有水果"的描述,因此即使看不到也会"脑补"。
新兴应用方向
- 多模态Agent:能够操作电脑界面、浏览网页、使用工具的AI助手。例如,用户可以截图一个网页,让AI自动完成表单填写。
- 世界模型:基于视频预测未来帧,理解物理世界规律。这是实现真正通用智能的关键一步。
- 机器人控制:将多模态理解转化为机器人动作指令。例如,看到"把杯子放到桌子上"的指令,机器人能理解并执行。
- 医疗诊断:综合影像、病历文本、基因数据进行辅助诊断。多模态融合可以提供比单一模态更准确的诊断结果。
- 自动驾驶:融合摄像头、激光雷达、地图数据和语音指令,实现更安全的自动驾驶。
- 内容创作:AI根据文本描述自动生成配图、视频、音乐。DALL-E、Stable Diffusion、Sora等模型已经展现了强大的多模态生成能力。
- 无障碍辅助:为视障人士描述图像内容,为听障人士生成视频字幕,为语言障碍人士提供语音交互。多模态AI正在成为无障碍技术的核心驱动力。
伦理与安全挑战
多模态AI的伦理挑战比纯文本模型更加复杂,因为它们涉及图像、视频等更敏感的数据类型。以下是一些核心的伦理问题:
- 深度伪造(Deepfake):多模态生成模型可以生成高度逼真的虚假图像和视频,被用于欺诈、诽谤和政治操纵。检测和防御深度伪造成为了一个紧迫的研究课题。
- 隐私侵犯:多模态模型可以识别图像中的个人信息(如人脸、车牌、地址),这引发了严重的隐私担忧。欧盟的GDPR和中国的《个人信息保护法》对图像数据的处理有严格规定。
- 偏见与公平性:训练数据中的偏见会被多模态模型放大。例如,某些模型在识别人脸时对深色皮肤的准确率显著低于浅色皮肤,这种不公平性需要通过数据平衡和算法改进来解决。
- 版权问题:多模态生成模型的训练数据通常包含大量受版权保护的图像和文本。DALL-E和Stable Diffusion已经面临多起版权诉讼。如何平衡数据利用和版权保护是一个悬而未决的法律问题。
- 军事应用:多模态AI可以被用于自主武器系统、监控和情报分析。国际社会正在讨论是否应该限制或禁止某些军事用途的多模态AI。
⚠️ 负责任地使用多模态AI
在使用多模态AI时,必须遵循以下原则:(1)不要使用模型生成或传播虚假内容;(2)尊重图像中人物的隐私权;(3)在医疗、法律等高风险领域,AI的输出必须经过人类专家审核;(4)披露AI生成内容,避免误导;(5)持续监控模型的偏见和公平性。负责任地使用多模态AI是每个从业者的义务。
未来研究方向
多模态AI的未来充满了令人兴奋的研究方向。以下是几个最具潜力的方向:
- 原生多模态模型:从"拼接式"多模态(各模态独立编码后融合)向"原生多模态"(从头设计就支持多模态)演进。GPT-4o和Gemini已经展示了原生多模态的可能性。
- 长时间视频理解:当前模型只能处理几分钟的视频,未来需要突破到小时甚至天级别的视频理解能力。这需要新的架构设计和更高效的注意力机制。
- 多模态推理:从简单的感知(识别图像中的物体)向复杂的推理(理解图像中的因果关系、预测未来事件)演进。这需要结合知识图谱、物理模拟等多种技术。
- 具身智能(Embodied AI):将多模态理解与物理世界的交互能力相结合,实现机器人在真实环境中的自主操作。这是通向通用人工智能(AGI)的关键一步。
- 多模态安全对齐:如何确保多模态模型不会被滥用?如何让模型在处理敏感图像时遵守伦理准则?这些安全对齐问题需要新的理论和技术。
- 跨文化多模态理解:不同文化对图像的解读方式不同。未来的多模态模型需要具备文化感知能力,能够根据用户的文化背景调整理解方式。
💡 推荐学习资源
想深入学习多模态AI,推荐以下资源:Hugging Face的Multimodal Course(免费在线课程)、Stanford CS231n(视觉深度学习)、BLIP-2和LLaVA的原始论文。动手实践时,Hugging Face Transformers库提供了最便捷的接口。对于中文社区,Qwen-VL和InternVL提供了优秀的中文多模态能力。
练习题
[1]
使用CLIP模型,对5张不同类别的图像进行零样本分类,比较直接使用类别标签和使用"一张{label}的照片"作为prompt的准确率差异。记录你的实验结果并分析原因。
[2]
修改本章的交叉注意力融合模块,实现一个简单的多模态分类器:输入MNIST图像和对应的数字标签文本,判断图像和文本是否匹配。训练模型并报告准确率。
[3]
构建一个包含至少50张图像的测试图库,使用本章实现的CLIPImageRetriever进行图文检索实验。设计10个不同复杂度的文本查询(如简单描述 vs. 组合描述),评估检索性能并讨论CLIP在组合性描述上的局限性。
[4]
研究论文:阅读BLIP-2原始论文(arXiv:2301.12597),回答以下问题:(1)Q-Former有多少个可学习查询向量?(2)为什么选择冻结视觉编码器和LLM?(3)两阶段训练分别使用了什么预训练目标?
[5]
使用Hugging Face Transformers库加载LLaVA-1.5模型,实现一个交互式多模态聊天程序:用户可以上传图片并提问,模型生成回答。记录模型在不同问题类型(描述性、推理性、计数性)上的表现差异。
章节小结
- 多模态AI让模型能够同时处理文本、图像、音频等多种模态的信息,核心挑战是跨模态语义对齐
- 当前主流架构为"视觉编码器 + 投影层 + 语言模型"三段式,代表模型包括LLaVA、BLIP-2、GPT-4V
- 三种融合策略:早期融合(灵活但昂贵)、晚期融合(高效但交互有限)、交叉注意力(当前最优平衡)
- CLIP通过对比学习在4亿图文对上预训练,成为几乎所有多模态模型的视觉编码器基础
- BLIP-2的Q-Former通过可学习查询向量实现高效的视觉-语言桥接,大幅降低了训练成本
- 图文检索、图像描述生成、视觉问答是多模态AI的三大基础任务
- 多模态幻觉、空间理解、长视频处理是当前面临的主要技术挑战
- 多模态Agent和具身智能是多模态AI的未来发展方向