第4章:多模态大模型原理
核心定位:理解文本-图像等多模态交互的核心技术(CLIP、ViT、LLaVA)
边界约束:
- ✅ 包含:CLIP 对比学习、ViT 架构、LLaVA 连接器、多模态推理实战
- ❌ 不包含:Transformer 基础机制(已在 Part 2 第1章)、对比学习基础理论(已在 Part 3 第4章)
目录
- 多模态的直觉理解:图像作为"外语"
- [统一 Token 化:Omni 模型的基石](#统一 Token 化:Omni 模型的基石)
- [视觉编码器:Vision Transformer (ViT)](#视觉编码器:Vision Transformer (ViT))
- 图文对齐:CLIP
- 多模态大模型架构:LLaVA
- [视频理解:Video as Frames](#视频理解:Video as Frames)
- 实战:多模态理解应用
- [2025视角:Connector vs Native Multimodal](#2025视角:Connector vs Native Multimodal)
- 总结与展望
一、多模态的直觉理解:图像作为"外语"
1.1 Token Space Alignment:为什么图像可以被视为"外语"
想象你是一个只懂中文的语言模型(LLM)。现在,有人拿着一张图片,用一种你从未见过的语言("图像语")向你描述。你该怎么办?
核心挑战 :LLM 只理解文本 Token,而图像是像素矩阵。就像中文和英文一样,它们是两个完全不同的"语言空间"。
解决方案:跨模态对齐(Cross-Modal Alignment)
┌─────────────┐ ┌─────────────┐
│ 图像空间 │ │ 文本空间 │
│ (像素矩阵) │ │ (Token 序列) │
│ │ │ │
│ [255, 0] │ │ "一只猫" │
│ [128, 64] │ │ "在草地上" │
│ [...] │ │ "躺着" │
└──────┬──────┘ └──────┬──────┘
│ │
│ 通过对齐训练 │
│ (CLIP、LLaVA 等) │
▼ ▼
┌────────────────────────────────────────────┐
│ 共享语义空间 (Shared Latent Space) │
│ │
│ "猫" ≈ [0.8, -0.3, 0.5, ...] │
│ 🐱 ≈ [0.82, -0.28, 0.51, ...] │
│ │
│ 距离很近 → 语义相似! │
└────────────────────────────────────────────┘
核心思想:
- 图像编码器:将图像翻译成向量(就像把英文翻译成中文)
- 文本编码器:将文本也翻译成向量
- 对齐训练 :让描述同一事物的图像和文本在向量空间中靠近
1.2 数学本质:余弦相似度
假设我们有一张猫的图片 I I I 和文本 "a photo of a cat" T T T。
编码过程 :
v image = E vision ( I ) ∈ R d \mathbf{v}{\text{image}} = E{\text{vision}}(I) \in \mathbb{R}^d vimage=Evision(I)∈Rd
v text = E text ( T ) ∈ R d \mathbf{v}{\text{text}} = E{\text{text}}(T) \in \mathbb{R}^d vtext=Etext(T)∈Rd
相似度计算 (余弦相似度):
sim ( v image , v text ) = v image ⋅ v text ∥ v image ∥ ∥ v text ∥ ∈ [ − 1 , 1 ] \text{sim}(\mathbf{v}{\text{image}}, \mathbf{v}{\text{text}}) = \frac{\mathbf{v}{\text{image}} \cdot \mathbf{v}{\text{text}}}{\|\mathbf{v}{\text{image}}\| \|\mathbf{v}{\text{text}}\|} \in [-1, 1] sim(vimage,vtext)=∥vimage∥∥vtext∥vimage⋅vtext∈[−1,1]
- 接近 1:高度相关(图片确实是猫)
- 接近 0:无关(图片可能是狗)
- 接近 -1:负相关(实际应用中较少见)
直觉理解:
- 就像在高维空间中测量两个向量的夹角
- 夹角越小,语义越相似
二、统一 Token 化:Omni 模型的基石
核心定位:理解 GPT-4o 时代的 "Omni" 理念 - 如何让 LLM 像处理文本一样处理图像、音频和视频。
2.1 从连续到离散:为什么需要 Token 化
核心问题:语言模型只理解离散的 Token(如文字),但图像、音频是连续信号。
文本:天生离散
"我爱猫" → ["我", "爱", "猫"] → [101, 203, 456] (Token IDs)
✅ LLM 可以直接处理
图像:连续信号
[[[255, 0, 128], [64, 32, 200], ...]] # 像素矩阵
❌ LLM 无法直接处理 → 需要转换为离散 Token
音频:连续信号
[0.023, -0.145, 0.089, ...] # 波形采样点
❌ LLM 无法直接处理 → 需要转换为离散 Token
Omni 模型的核心思想:将所有模态统一到离散 Token 空间,让 LLM 用同一套机制处理所有信息。
2.2 视觉 Token 化:VQ-VAE(Vector Quantized Variational AutoEncoder)
VQ-VAE 是将连续图像转换为离散 Token 的核心技术,广泛应用于 DALL-E、Parti 等生成模型。
2.2.1 VQ-VAE 核心原理
架构流程:
┌───────────────────────────────────────────────────────────────┐
│ VQ-VAE 架构 │
└───────────────────────────────────────────────────────────────┘
输入图像 编码器 量化器
[256×256×3] Codebook
│ ┌──────────┐
│ │ e₀=[...] │
▼ │ e₁=[...] │
┌─────────┐ ┌──────────┐ │ e₂=[...] │
│ │ │ │ 连续编码 │ ... │
│ 原始 │───────>│ Encoder │ [32×32×256] │ e₈₁₉₁=[..]│
│ 图像 │ │ (CNN) │ │ └─────┬────┘
│ │ │ │ │ │
└─────────┘ └──────────┘ │ │
▼ ▼
查找最近的 Codebook 向量
│
▼
离散 Token 序列
[142, 783, 45, 892, ...]
[1024 个 Token IDs]
│
▼
┌──────────────┐
│ Decoder │
│ (CNN) │───────> 重建图像
└──────────────┘ [256×256×3]
工作流程:
- Encoder :将图像 ( 256 × 256 × 3 ) (256 \times 256 \times 3) (256×256×3) 压缩为低分辨率特征图 ( 32 × 32 × 256 ) (32 \times 32 \times 256) (32×32×256)
- Quantization :将每个特征向量 ( 256 维 ) (256维) (256维) 映射到最近的 Codebook 向量
- 离散化 :得到 32 × 32 = 1024 32 \times 32 = 1024 32×32=1024 个离散 Token
- Decoder:从离散 Token 重建图像(训练时用于优化)
2.2.2 Codebook 的工作机制
Codebook:预定义的向量字典,类似于"视觉词汇表"。
python
import torch
import torch.nn as nn
import torch.nn.functional as F
class VectorQuantizer(nn.Module):
"""VQ-VAE 的量化层"""
def __init__(self, num_embeddings=8192, embedding_dim=256):
"""
Args:
num_embeddings: Codebook 大小(词汇表大小)
embedding_dim: 每个向量的维度
"""
super().__init__()
self.num_embeddings = num_embeddings
self.embedding_dim = embedding_dim
# Codebook: [8192, 256] - 8192 个 256 维的向量
self.embedding = nn.Embedding(num_embeddings, embedding_dim)
self.embedding.weight.data.uniform_(-1/num_embeddings, 1/num_embeddings)
def forward(self, z):
"""
Args:
z: 编码器输出 [B, H, W, D] 例如 [B, 32, 32, 256]
Returns:
quantized: 量化后的特征 [B, H, W, D]
token_ids: 离散 Token IDs [B, H, W]
"""
B, H, W, D = z.shape
# 1. 展平空间维度: [B, H, W, D] -> [B*H*W, D]
z_flat = z.reshape(-1, D)
# 2. 计算与所有 Codebook 向量的距离
# |z - e|² = |z|² + |e|² - 2z·e
distances = (
torch.sum(z_flat**2, dim=1, keepdim=True) + # [B*H*W, 1]
torch.sum(self.embedding.weight**2, dim=1) - # [8192]
2 * torch.matmul(z_flat, self.embedding.weight.t()) # [B*H*W, 8192]
)
# 3. 找到最近的 Codebook 向量(贪心匹配)
token_ids = torch.argmin(distances, dim=1) # [B*H*W]
# 4. 查表获取量化后的向量
quantized_flat = self.embedding(token_ids) # [B*H*W, D]
# 5. 恢复空间维度
quantized = quantized_flat.view(B, H, W, D) # [B, H, W, D]
token_ids = token_ids.view(B, H, W) # [B, H, W]
# 6. Straight-through estimator(训练技巧)
# 前向传播:使用量化后的向量
# 反向传播:梯度直接传给编码器
quantized = z + (quantized - z).detach()
return quantized, token_ids
# 使用示例
if __name__ == "__main__":
vq = VectorQuantizer(num_embeddings=8192, embedding_dim=256)
# 模拟编码器输出
z = torch.randn(2, 32, 32, 256) # batch=2, 图像编码为 32×32 的特征图
quantized, token_ids = vq(z)
print(f"输入: {z.shape}")
print(f"量化后: {quantized.shape}")
print(f"Token IDs: {token_ids.shape}")
print(f"Token 范围: [{token_ids.min()}, {token_ids.max()}]")
# 输出:
# 输入: torch.Size([2, 32, 32, 256])
# 量化后: torch.Size([2, 32, 32, 256])
# Token IDs: torch.Size([2, 32, 32])
# Token 范围: [0, 8191]
为什么这样有效?
图像: [256×256×3] → Encoder → [32×32×256] → VQ → [32×32] Token IDs
↑
每个位置选择 8192 个候选中最匹配的 Token
最终:图像被表示为 1024 个离散 Token(就像 1024 个单词!)
2.2.3 视觉 Token 化的直观理解
原始图像 VQ-VAE Token 化 文本类比
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 🐱 │ → │ [142, 783] │ ≈ │ "一只猫" │
│ │ │ [45, 892] │ │ │
│ │ │ [234, 1023] │ │ │
└─────────────┘ └─────────────┘ └─────────────┘
像素矩阵 离散 Token 文本 Token
现在,LLM 可以用同样的 Transformer 处理这两种 Token!
2.3 音频 Token 化:Audio Codec
音频的 Token 化与图像类似,但使用专门的音频编解码器。
2.3.1 常用音频 Codec
1. EnCodec(Meta,2022)
- 将音频压缩为离散 Token
- 支持多种码率(1.5-12 kbps)
- 应用:AudioLM、MusicGen
2. SoundStream(Google,2021)
- 高质量音频压缩
- 应用:AudioPaLM
架构流程(以 EnCodec 为例):
┌────────────────────────────────────────────────────────────────┐
│ EnCodec 音频 Token 化 │
└────────────────────────────────────────────────────────────────┘
原始音频波形 编码器 量化器
[1秒 @ 24kHz] Codebook
= 24000 采样点 ┌──────────┐
│ │ e₀=[...] │
▼ │ e₁=[...] │
┌─────────┐ ┌──────────┐ │ ... │
│ 音频 │───────>│ Conv │ 压缩特征 │ e₁₀₂₃=[..]│
│ 波形 │ │ Encoder │ [75, 128] └─────┬────┘
└─────────┘ └──────────┘ │ │
▼ ▼
量化为离散 Token
│
▼
[523, 12, 945, 234, ...]
[75 个 Token / 秒]
│
▼
┌──────────────┐
│ Decoder │
│ (Conv) │───────> 重建音频
└──────────────┘
关键参数:
- 降采样倍率:320倍(24000 Hz → 75 Token/秒)
- Codebook 大小:1024(10 位)
- 压缩率:1 秒音频 ≈ 75 个 Token
2.3.2 音频 Token 化示例代码
python
"""
使用 EnCodec 进行音频 Token 化
需要: pip install encodec
"""
import torch
import torchaudio
from encodec import EncodecModel
from encodec.utils import convert_audio
# 1. 加载预训练的 EnCodec 模型
model = EncodecModel.encodec_model_24khz()
model.set_target_bandwidth(6.0) # 设置目标码率 6 kbps
# 2. 加载音频文件
wav, sr = torchaudio.load("audio.wav")
# 3. 转换为模型需要的格式(24kHz, mono)
wav = convert_audio(wav, sr, model.sample_rate, model.channels)
wav = wav.unsqueeze(0) # [1, channels, time]
# 4. 编码为离散 Token
with torch.no_grad():
encoded_frames = model.encode(wav)
# 5. 提取 Token IDs
# encoded_frames 是一个列表,每个元素包含 [codes, scale]
# codes: [batch, num_quantizers, time]
codes = encoded_frames[0][0] # [1, num_quantizers, time]
print(f"原始音频: {wav.shape[2]} 采样点 ({wav.shape[2]/model.sample_rate:.2f} 秒)")
print(f"Token 序列: {codes.shape}")
print(f"压缩率: {wav.shape[2] / codes.shape[2]:.1f}x")
# 输出示例:
# 原始音频: 48000 采样点 (2.00 秒)
# Token 序列: torch.Size([1, 8, 150])
# 压缩率: 320.0x
# 6. 解码回音频(验证)
with torch.no_grad():
reconstructed = model.decode(encoded_frames)
print(f"重建音频: {reconstructed.shape}")
2.4 统一 Token 空间:Omni 模型的实现
GPT-4o / Gemini 1.5 的推测架构:
┌────────────────────────────────────────────────────────────┐
│ Unified Token Space │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 文本 │ │ 图像 │ │ 音频 │ │
│ │ "你好" │ │ 🖼️ │ │ 🔊 │ │
│ └──────┬──────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ Text Tokenizer VQ-VAE Encoder Audio Codec │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ [1024, 2045] [256000+142] [264000+523] │
│ │ │ │ │
│ └────────────────┴──────────────────┘ │
│ │ │
│ ▼ │
│ Unified Transformer (GPT-4o) │
│ - 词汇表: [0, 300000) │
│ [0, 256k): 文本 Token │
│ [256k, 264k): 视觉 Token (VQ-VAE) │
│ [264k, 300k): 音频 Token (Codec) │
│ - 无需投影层,原生统一处理 │
└────────────────────────────────────────────────────────────┘
核心优势:
-
真正的 Any-to-Any
输入: [文本 Token] + [图像 Token] + [音频 Token] 输出: [文本 Token] 或 [图像 Token] 或 [音频 Token] -
无信息瓶颈
- 不需要投影层(LLaVA 的瓶颈)
- 每层 Transformer 都能处理跨模态信息
-
统一训练范式
- 所有模态使用相同的预训练目标(Next Token Prediction)
- 自然支持模态间的细粒度交互
2.5 实战:构建简易的视觉 Token 化器
python
"""
使用预训练的 VQGAN 进行图像 Token 化
需要: pip install taming-transformers-rom1504
"""
import torch
from omegaconf import OmegaConf
from taming.models.vqgan import VQModel
from PIL import Image
import torchvision.transforms as transforms
class ImageTokenizer:
"""图像 Token 化器(基于 VQGAN)"""
def __init__(self, config_path, checkpoint_path):
# 加载配置和模型
config = OmegaConf.load(config_path)
self.model = VQModel(**config.model.params)
state_dict = torch.load(checkpoint_path, map_location="cpu")["state_dict"]
self.model.load_state_dict(state_dict, strict=False)
self.model.eval()
# 图像预处理
self.transform = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(256),
transforms.ToTensor(),
transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])
])
def encode(self, image_path):
"""将图像编码为离散 Token"""
# 加载并预处理图像
image = Image.open(image_path).convert("RGB")
x = self.transform(image).unsqueeze(0) # [1, 3, 256, 256]
# 编码
with torch.no_grad():
z = self.model.encode(x) # 连续特征
_, _, [_, _, indices] = self.model.quantize(z) # 量化
# indices: [1, 16*16] = [1, 256] (16x16 个 Token)
return indices.squeeze(0).cpu().numpy()
def decode(self, token_ids):
"""从 Token 重建图像"""
with torch.no_grad():
z_q = self.model.quantize.get_codebook_entry(
torch.tensor(token_ids).unsqueeze(0),
shape=(1, 16, 16, 256)
)
reconstructed = self.model.decode(z_q)
# 转为 PIL 图像
img = reconstructed.squeeze(0).permute(1, 2, 0).cpu().numpy()
img = ((img + 1) / 2 * 255).clip(0, 255).astype('uint8')
return Image.fromarray(img)
# 使用示例
if __name__ == "__main__":
tokenizer = ImageTokenizer(
config_path="vqgan_config.yaml",
checkpoint_path="vqgan.ckpt"
)
# 编码
token_ids = tokenizer.encode("cat.jpg")
print(f"Token 序列: {token_ids}")
print(f"Token 数量: {len(token_ids)}")
print(f"Token 范围: [{token_ids.min()}, {token_ids.max()}]")
# 解码(验证)
reconstructed = tokenizer.decode(token_ids)
reconstructed.save("cat_reconstructed.jpg")
关键点:
- 图像 ( 256 × 256 ) (256 \times 256) (256×256) → 16 × 16 = 256 16 \times 16 = 256 16×16=256 个 Token
- 每个 Token 来自大小为 8192 的 Codebook
- 压缩率: ( 256 × 256 × 3 ) (256 \times 256 \times 3) (256×256×3) 像素 → 256 个 Token(约 768:1)
三、视觉编码器:Vision Transformer (ViT)
详见 [Part 2 第1章] Transformer 核心机制。本章仅讲解 ViT 如何将 Transformer 应用于图像。
3.1 核心思想:图像是 16×16 的单词
问题 :Transformer 处理一维序列,但图像是二维的 ( H × W ) (H \times W) (H×W)。
ViT 的解决方案:把图像切成小方块(Patches),像处理单词一样处理它们。
python
# 1. 原始图像
image = [224, 224, 3] # 高×宽×通道
# 2. 切成 Patches (每块 16×16)
num_patches = (224 // 16) * (224 // 16) = 14 * 14 = 196
patches = split_image(image, patch_size=16) # [196, 16, 16, 3]
# 3. 展平每个 Patch
patch_vectors = patches.reshape(196, 16*16*3) # [196, 768]
# 4. 线性投影到 Embedding 维度
embeddings = Linear(768, 768)(patch_vectors) # [196, 768]
# 5. 加入位置编码(告诉模型每个 Patch 的位置)
position_embeddings = learnable_params([196, 768])
final_input = embeddings + position_embeddings
# 6. 喂给 Transformer!
类比:
- NLP:一句话 = ["我", "爱", "猫"] → 3 个 Token
- ViT:一张图 = [Patch₁, Patch₂, ..., Patch₁₉₆] → 196 个 Token
3.2 ViT 代码实现
python
import torch
import torch.nn as nn
class PatchEmbedding(nn.Module):
"""将图像切分成 Patches 并映射到 Embedding 空间"""
def __init__(self, img_size=224, patch_size=16, in_channels=3, embed_dim=768):
super().__init__()
self.n_patches = (img_size // patch_size) ** 2
# 使用卷积实现分块+投影(最高效的方式)
# kernel_size=patch_size, stride=patch_size 实现非重叠分块
self.proj = nn.Conv2d(in_channels, embed_dim,
kernel_size=patch_size, stride=patch_size)
def forward(self, x):
# x: [batch, 3, 224, 224]
x = self.proj(x) # [batch, 768, 14, 14]
x = x.flatten(2) # [batch, 768, 196]
x = x.transpose(1, 2) # [batch, 196, 768]
return x
class VisionTransformer(nn.Module):
def __init__(self, img_size=224, patch_size=16, embed_dim=768,
num_heads=12, num_layers=12, num_classes=1000):
super().__init__()
# 1. Patch Embedding
self.patch_embed = PatchEmbedding(img_size, patch_size, 3, embed_dim)
# 2. CLS Token(类似 BERT 的 [CLS],用于分类)
self.cls_token = nn.Parameter(torch.zeros(1, 1, embed_dim))
# 3. Position Embedding
self.pos_embed = nn.Parameter(
torch.zeros(1, 1 + self.patch_embed.n_patches, embed_dim)
)
# 4. Transformer Encoder(详见 Part 2 第1章)
encoder_layer = nn.TransformerEncoderLayer(
d_model=embed_dim, nhead=num_heads,
activation='gelu', batch_first=True
)
self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)
# 5. Classification Head
self.head = nn.Linear(embed_dim, num_classes)
def forward(self, x):
B = x.shape[0]
# Patch Embedding
x = self.patch_embed(x) # [B, 196, 768]
# 添加 CLS Token
cls_tokens = self.cls_token.expand(B, -1, -1) # [B, 1, 768]
x = torch.cat((cls_tokens, x), dim=1) # [B, 197, 768]
# 添加位置编码
x = x + self.pos_embed
# Transformer Encoder
x = self.encoder(x)
# 取 CLS Token 进行分类
cls_output = x[:, 0] # [B, 768]
logits = self.head(cls_output) # [B, 1000]
return logits
# 测试
if __name__ == "__main__":
model = VisionTransformer()
dummy_img = torch.randn(2, 3, 224, 224) # 批量大小=2
output = model(dummy_img)
print(f"输入: {dummy_img.shape}, 输出: {output.shape}")
# 输入: torch.Size([2, 3, 224, 224]), 输出: torch.Size([2, 1000])
关键点:
- Patch Embedding:用卷积高效实现分块
- CLS Token:全局特征聚合(可选,有些 ViT 用全局平均池化)
- 位置编码 :ViT 通常使用可学习的位置编码(与 Transformer 的正弦编码不同)
四、图文对齐:CLIP
CLIP (Contrastive Language-Image Pre-training) 是 OpenAI 2021 年提出的突破性工作,通过对比学习让图像和文本在同一空间中对齐。
4.1 核心机制:对比学习(Contrastive Learning)
详见 [Part 3 第4章] 对比学习详解。本节仅讲解 CLIP 的具体实现。
训练数据:4 亿个(图像,文本)对,从互联网爬取。
训练目标:
- 正样本对 ( I i , T i ) (I_i, T_i) (Ii,Ti):相似度最大化
- 负样本对 ( I i , T j ) i ≠ j (I_i, T_j)_{i \neq j} (Ii,Tj)i=j:相似度最小化
python
import torch
import torch.nn.functional as F
def clip_loss(image_embeddings, text_embeddings, temperature=0.07):
"""
CLIP 的 InfoNCE 损失
Args:
image_embeddings: [N, D] - N 张图像的特征向量
text_embeddings: [N, D] - N 个文本的特征向量
temperature: 温度系数,控制 softmax 分布的平滑度
"""
# 1. 归一化(确保余弦相似度计算正确)
image_embeddings = F.normalize(image_embeddings, dim=-1)
text_embeddings = F.normalize(text_embeddings, dim=-1)
# 2. 计算相似度矩阵 [N, N]
# logits[i, j] = sim(image_i, text_j)
logits = (image_embeddings @ text_embeddings.T) / temperature
# 3. 对角线是正样本,其余是负样本
labels = torch.arange(len(logits)).to(logits.device)
# 4. 双向损失(图像→文本 + 文本→图像)
loss_i2t = F.cross_entropy(logits, labels) # 图像查文本
loss_t2i = F.cross_entropy(logits.T, labels) # 文本查图像
loss = (loss_i2t + loss_t2i) / 2
return loss
数学表达 (图像→文本方向):
L i → t = − log exp ( sim ( I i , T i ) / τ ) ∑ j = 1 N exp ( sim ( I i , T j ) / τ ) \mathcal{L}{i \to t} = -\log \frac{\exp(\text{sim}(I_i, T_i) / \tau)}{\sum{j=1}^N \exp(\text{sim}(I_i, T_j) / \tau)} Li→t=−log∑j=1Nexp(sim(Ii,Tj)/τ)exp(sim(Ii,Ti)/τ)
直觉解释:
- 分子:正样本对的相似度(越大越好)
- 分母:所有样本的相似度(正样本应该远大于负样本)
- τ \tau τ (温度):越小,模型对难负样本越敏感
4.2 CLIP 的实际使用
零样本图像分类(Zero-shot Classification)
python
from transformers import CLIPProcessor, CLIPModel
from PIL import Image
import requests
# 1. 加载预训练的 CLIP 模型
model_name = "openai/clip-vit-base-patch32"
model = CLIPModel.from_pretrained(model_name)
processor = CLIPProcessor.from_pretrained(model_name)
# 2. 准备图像
url = "http://images.cocodataset.org/val2017/000000039769.jpg"
image = Image.open(requests.get(url, stream=True).raw)
# 3. 定义候选类别(用自然语言描述!)
candidates = [
"a photo of a cat",
"a photo of a dog",
"a photo of a bird",
"a photo of remote controls" # 图中实际有遥控器
]
# 4. 编码
inputs = processor(text=candidates, images=image,
return_tensors="pt", padding=True)
# 5. 前向传播
outputs = model(**inputs)
logits_per_image = outputs.logits_per_image # [1, 4]
probs = logits_per_image.softmax(dim=1) # 转为概率
# 6. 输出结果
print("候选类别:", candidates)
print("匹配概率:", probs.detach().numpy()[0])
# 预期输出(实际图片是两只猫和一些遥控器):
# 候选类别: ['a photo of a cat', 'a photo of a dog', 'a photo of a bird', 'a photo of remote controls']
# 匹配概率: [0.85, 0.02, 0.01, 0.12] (猫的概率最高)
关键优势:
- 零样本能力:不需要专门训练分类器,直接用文本描述类别
- 灵活性:可以随时改变候选类别,无需重新训练
4.3 CLIP 的应用场景
- 零样本图像分类(如上例)
- 图文检索(详见第五节实战)
- 多模态搜索:输入文字搜图片,或输入图片搜相似图片
- 图像生成引导:Stable Diffusion、DALL-E 使用 CLIP 引导生成
五、多模态大模型架构:LLaVA
LLaVA (Large Language and Vision Assistant) 是当前最流行的开源多模态大模型架构,设计理念简单优雅。
5.1 架构设计:三个组件
┌────────────────────────────────────────────────┐
│ │
│ 输入: 图像 + 文本指令 │
│ "请描述这张图片" │
│ │
└────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ 1️⃣ Vision Encoder (CLIP ViT-L/14) │
│ - 冻结参数,不训练 │
│ - 输出: [576, 1024] 视觉 Token │
└────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ 2️⃣ Projection Layer (投影层) │
│ - 可训练的 MLP: 1024 → 4096 维 │
│ - 将视觉特征映射到 LLM 的 Token 空间 │
│ - 输出: [576, 4096] "伪装"成文本 Token │
└────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ 3️⃣ LLM (Vicuna-7B / LLaMA-7B) │
│ - 处理: [视觉 Token] + [文本 Token] │
│ - 生成: "这张图片显示了..." │
└─────────────────────────────────────────────────┘
核心思想:
- Vision Encoder:提取视觉特征(使用预训练的 CLIP)
- Projection Layer :桥接视觉和语言空间(关键创新)
- LLM:理解并生成文本
5.2 Projection Layer:Token Space Alignment 的实现
问题:
- CLIP ViT 输出维度:1024
- LLaMA-7B Token 维度:4096
解决方案:简单的 MLP
python
class ProjectionLayer(nn.Module):
"""将视觉特征投影到 LLM 的 Token Embedding 空间"""
def __init__(self, vision_dim=1024, llm_dim=4096):
super().__init__()
# 两层 MLP
self.proj = nn.Sequential(
nn.Linear(vision_dim, llm_dim),
nn.GELU(),
nn.Linear(llm_dim, llm_dim)
)
def forward(self, vision_features):
# vision_features: [B, N, 1024] (N=576 个视觉 Token)
# 输出: [B, N, 4096]
return self.proj(vision_features)
# 使用示例
proj = ProjectionLayer()
vision_tokens = torch.randn(1, 576, 1024) # CLIP 输出
llm_tokens = proj(vision_tokens) # [1, 576, 4096]
# 现在可以与文本 Token 拼接!
text_tokens = torch.randn(1, 20, 4096) # 文本 Token
combined = torch.cat([llm_tokens, text_tokens], dim=1) # [1, 596, 4096]
为什么有效?
- 视觉特征经过投影后,"伪装"成了 LLM 可以理解的 Token
- LLM 就像在处理一段"特殊语言"(图像语),但使用相同的 Transformer 机制
5.3 LLaVA 的两阶段训练
这种两阶段训练法已成为行业标准。
阶段一:特征对齐预训练(Feature Alignment Pre-training)
训练策略:
- 🔒 冻结:Vision Encoder + LLM
- 🔥 训练:仅 Projection Layer
数据:CC3M(300 万图像-标题对)
目的:让 Projection Layer 学会将视觉特征映射到 LLM 能理解的空间
python
# 伪代码
for image, caption in dataset:
# 1. 提取视觉特征(冻结)
vision_features = vision_encoder(image) # [B, 576, 1024]
# 2. 投影到 LLM 空间(训练)
visual_tokens = projection_layer(vision_features) # [B, 576, 4096]
# 3. 拼接文本 Token(冻结)
text_tokens = llm.tokenize(caption)
combined_tokens = concat([visual_tokens, text_tokens])
# 4. 语言建模损失(仅反向传播到 Projection Layer)
loss = llm.forward(combined_tokens, labels=caption)
loss.backward() # 只更新 Projection Layer 的参数
阶段二:视觉指令微调(Visual Instruction Tuning)
训练策略:
- 🔒 冻结:Vision Encoder
- 🔥 训练:Projection Layer + LLM
数据:高质量视觉指令数据(如 LLaVA-Instruct-150K)
示例数据:
json
{
"image": "beach.jpg",
"conversations": [
{
"role": "human",
"content": "这张图片中发生了什么?"
},
{
"role": "assistant",
"content": "这张图片展示了一个美丽的海滩日落场景。天空呈现出橙色和紫色的渐变,海浪轻柔地拍打着沙滩。远处可以看到几只海鸥在飞翔。整体氛围宁静而祥和。"
}
]
}
目的:
- 提升多模态推理能力
- 学会遵循视觉相关的指令
- 生成更详细、准确的描述
5.4 其他连接器方案:Perceiver Resampler (Flamingo/IDEFICS)
LLaVA 使用简单的 MLP,但有些模型使用更复杂的连接器。
Perceiver Resampler(Flamingo 架构):
python
class PerceiverResampler(nn.Module):
"""
使用交叉注意力压缩视觉 Token
"""
def __init__(self, num_queries=64, vision_dim=1024, llm_dim=4096, depth=6):
super().__init__()
# 可学习的 Query Token
self.queries = nn.Parameter(torch.randn(num_queries, llm_dim))
# 多层交叉注意力
self.layers = nn.ModuleList([
nn.MultiheadAttention(llm_dim, num_heads=16)
for _ in range(depth)
])
def forward(self, vision_features):
# vision_features: [B, 576, 1024]
# 1. 先投影到 LLM 维度
vision_features = nn.Linear(1024, 4096)(vision_features) # [B, 576, 4096]
# 2. 使用固定数量的 Queries 提取信息
B = vision_features.size(0)
queries = self.queries.unsqueeze(0).expand(B, -1, -1) # [B, 64, 4096]
# 3. 多层交叉注意力
for layer in self.layers:
queries, _ = layer(
query=queries.transpose(0, 1), # [64, B, 4096]
key=vision_features.transpose(0, 1), # [576, B, 4096]
value=vision_features.transpose(0, 1)
)
queries = queries.transpose(0, 1) # [B, 64, 4096]
# 输出: [B, 64, 4096] (从 576 压缩到 64 个 Token!)
return queries
对比:
| 方案 | 输出 Token 数 | 复杂度 | 代表模型 |
|---|---|---|---|
| MLP (LLaVA) | 576 | 低 | LLaVA, Qwen-VL |
| Perceiver Resampler | 64 | 中 | Flamingo, IDEFICS |
| Q-Former (BLIP-2) | 32 | 高 | BLIP-2, InstructBLIP |
权衡:
- 更多 Token:保留更多视觉细节,但增加 LLM 计算量
- 更少 Token:计算高效,但可能丢失细节
六、视频理解:Video as Frames
核心定位:理解多模态模型如何处理视频 - 最常用的方法是将视频视为一系列静态图像。
6.1 视频的本质:时序图像序列
核心思想:视频 = 连续的图像帧 + 时间维度
视频文件 (30 FPS, 10秒)
↓
300 个图像帧
↓
抽帧策略 (降低计算成本)
↓
选择关键帧 (例如: 每秒 1 帧)
↓
10 个图像 Token 序列
↓
输入到多模态模型
6.2 视频抽帧策略
常见策略:
-
均匀抽帧(Uniform Sampling)
pythondef uniform_sample_frames(video_path, num_frames=8): """均匀抽取 N 帧""" cap = cv2.VideoCapture(video_path) total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) # 计算采样间隔 indices = np.linspace(0, total_frames-1, num_frames, dtype=int) frames = [] for idx in indices: cap.set(cv2.CAP_PROP_POS_FRAMES, idx) ret, frame = cap.read() if ret: frames.append(frame) cap.release() return frames # 使用示例 # 10秒视频(300帧) → 抽取 8 帧 → 每 37 帧抽一次 frames = uniform_sample_frames("video.mp4", num_frames=8) -
FPS 固定抽帧(Fixed FPS)
pythondef sample_by_fps(video_path, target_fps=1): """按固定 FPS 抽帧(例如每秒 1 帧)""" cap = cv2.VideoCapture(video_path) original_fps = cap.get(cv2.CAP_PROP_FPS) # 计算每隔多少帧抽一次 frame_interval = int(original_fps / target_fps) frames = [] frame_idx = 0 while True: ret, frame = cap.read() if not ret: break if frame_idx % frame_interval == 0: frames.append(frame) frame_idx += 1 cap.release() return frames # 使用示例 # 30 FPS 视频 → 每 30 帧抽一次 → 1 FPS frames = sample_by_fps("video.mp4", target_fps=1) -
关键帧检测(Keyframe Detection)
- 基于场景变化检测(Scene Change Detection)
- 检测帧间差异,保留变化显著的帧
- 更智能,但计算成本更高
6.3 视频 Token 化:两种范式
范式 1:连接器方案(LLaVA-Video)
┌──────────────────────────────────────────────────────────┐
│ LLaVA-Video 架构 │
└──────────────────────────────────────────────────────────┘
视频输入 (10秒, 30 FPS)
↓
抽取 8 帧 (Uniform Sampling)
↓
┌───────┬───────┬───────┬─────┬───────┐
│ Frame │ Frame │ Frame │ ... │ Frame │
│ 1 │ 2 │ 3 │ │ 8 │
└───┬───┴───┬───┴───┬───┴─────┴───┬───┘
│ │ │ │
▼ ▼ ▼ ▼
┌────────────────────────────────────┐
│ Vision Encoder (CLIP ViT) │
│ 每帧 → [576, 1024] │
└────────────┬───────────────────────┘
▼
8 × [576, 1024] = [4608, 1024]
↓
┌────────────────────────────────────┐
│ Projection Layer │
│ [4608, 1024] → [4608, 4096] │
└────────────┬───────────────────────┘
▼
┌────────────────────────────────────┐
│ LLM (处理 4608 个视觉 Token) │
│ + 文本 Token │
│ → 生成视频描述 │
└────────────────────────────────────┘
关键点:
- 每帧独立编码(无时序建模)
- 拼接所有帧的 Token(线性增长)
- 依赖 LLM 的自注意力学习时序关系
范式 2:原生统一方案(GPT-4o)
视频 → VQ-VAE 编码器(3D 卷积)→ 时空离散 Token
↓
直接输入统一 Transformer
(视觉 Token 本身包含时间信息)
6.4 实战:使用 LLaVA-Video 理解视频
python
"""
使用 Video-LLaVA 进行视频问答
需要: pip install transformers accelerate opencv-python
"""
import torch
import cv2
import numpy as np
from transformers import VideoLlavaForConditionalGeneration, AutoProcessor
from PIL import Image
def load_video_frames(video_path, num_frames=8):
"""从视频中均匀抽取 N 帧"""
cap = cv2.VideoCapture(video_path)
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
indices = np.linspace(0, total_frames-1, num_frames, dtype=int)
frames = []
for idx in indices:
cap.set(cv2.CAP_PROP_POS_FRAMES, idx)
ret, frame = cap.read()
if ret:
# OpenCV 读取的是 BGR,转为 RGB
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
frames.append(Image.fromarray(frame))
cap.release()
return frames
# 1. 加载模型
model_id = "LanguageBind/Video-LLaVA-7B"
model = VideoLlavaForConditionalGeneration.from_pretrained(
model_id,
torch_dtype=torch.float16,
device_map="auto"
)
processor = AutoProcessor.from_pretrained(model_id)
# 2. 加载视频并抽帧
video_path = "cooking.mp4"
frames = load_video_frames(video_path, num_frames=8)
print(f"抽取了 {len(frames)} 帧")
# 3. 准备问题
prompt = "USER: <video>\nDescribe what's happening in this video.\nASSISTANT:"
# 4. 处理输入
inputs = processor(
text=prompt,
images=frames, # Video-LLaVA 使用 images 参数处理视频帧
return_tensors="pt"
).to("cuda")
# 5. 生成回答
with torch.inference_mode():
generated_ids = model.generate(
**inputs,
max_new_tokens=150,
do_sample=False
)
# 6. 解码输出
output = processor.decode(generated_ids[0], skip_special_tokens=True)
answer = output.split("ASSISTANT:")[-1].strip()
print(f"\n视频: {video_path}")
print(f"问题: Describe what's happening in this video.")
print(f"回答: {answer}")
# 预期输出示例:
# "This video shows a person cooking in a kitchen. They start by chopping vegetables,
# then heat oil in a pan, add the vegetables, and stir-fry them. The person appears
# to be preparing a healthy meal."
6.5 视频理解的挑战与优化
挑战 1:Token 数量爆炸
- 问题:8 帧 × 576 Token/帧 = 4608 Token(接近某些模型的上下文限制)
- 解决方案:
- 使用 Perceiver Resampler 压缩(576 → 64 Token/帧)
- 减少抽帧数量(牺牲时序细节)
挑战 2:缺乏真正的时序建模
- 问题:独立编码每帧,无法捕捉连续动作
- 解决方案:
- 使用 3D 卷积(C3D、I3D)
- 时序 Transformer(TimeSformer)
- 原生多模态模型(GPT-4o)
挑战 3:长视频处理
- 问题:10 分钟视频抽帧后仍有数百帧
- 解决方案:
- 分段处理(每 30 秒一段)
- 层次化采样(先粗采样定位关键片段,再细采样)
- 使用长上下文模型(Gemini 1.5:1M Token)
6.6 视频理解的应用场景
-
视频摘要生成
pythonprompt = "Summarize the main events in this video in 3 sentences." -
时间戳定位
pythonprompt = "At what timestamp does the person start cooking? Answer in format MM:SS." -
动作识别
pythonprompt = "What actions does the person perform in this video? List them step by step." -
视频问答
pythonprompt = "How many people appear in this video?"
七、实战:多模态理解应用
7.1 使用开源模型:LLaVA 图像问答
python
"""
使用 Hugging Face 的 LLaVA-1.5-7B 进行图像理解
需要: pip install transformers accelerate pillow
"""
from transformers import AutoProcessor, LlavaForConditionalGeneration
from PIL import Image
import requests
import torch
# 1. 加载模型和处理器
model_id = "llava-hf/llava-1.5-7b-hf"
model = LlavaForConditionalGeneration.from_pretrained(
model_id,
torch_dtype=torch.float16,
device_map="auto" # 自动分配 GPU/CPU
)
processor = AutoProcessor.from_pretrained(model_id)
# 2. 准备图像和问题
url = "https://www.ilankelman.org/stopsigns/australia.jpg"
image = Image.open(requests.get(url, stream=True).raw)
prompt = "USER: <image>\nWhat's the content of this image?\nASSISTANT:"
# 3. 处理输入
inputs = processor(text=prompt, images=image, return_tensors="pt").to("cuda")
# 4. 生成回答
with torch.inference_mode():
generated_ids = model.generate(
**inputs,
max_new_tokens=100,
do_sample=False
)
# 5. 解码输出
output = processor.decode(generated_ids[0], skip_special_tokens=True)
answer = output.split("ASSISTANT:")[-1].strip()
print("图像:", url)
print("问题:", "What's the content of this image?")
print("回答:", answer)
# 预期输出:
# "This image shows a red stop sign at a street intersection in Australia.
# The sign features white text in English and additional text in Chinese characters."
7.2 实战:构建本地图文检索引擎
使用 CLIP 构建一个简单的图片搜索引擎。
python
import os
import torch
import glob
from PIL import Image
from transformers import CLIPProcessor, CLIPModel
import numpy as np
class ImageSearchEngine:
"""基于 CLIP 的图文检索引擎"""
def __init__(self, model_id="openai/clip-vit-base-patch32"):
self.device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"使用设备: {self.device}")
# 加载 CLIP 模型
self.model = CLIPModel.from_pretrained(model_id).to(self.device)
self.processor = CLIPProcessor.from_pretrained(model_id)
# 索引数据
self.image_paths = []
self.image_features = None
def index_images(self, image_dir):
"""为指定目录下的所有图片建立特征索引"""
# 1. 收集图片路径
extensions = ['*.jpg', '*.jpeg', '*.png', '*.webp']
for ext in extensions:
self.image_paths.extend(glob.glob(os.path.join(image_dir, ext)))
print(f"找到 {len(self.image_paths)} 张图片,开始建立索引...")
# 2. 批量提取特征
all_features = []
batch_size = 32
for i in range(0, len(self.image_paths), batch_size):
batch_paths = self.image_paths[i:i+batch_size]
images = []
# 加载图片
for path in batch_paths:
try:
images.append(Image.open(path).convert("RGB"))
except Exception as e:
print(f"读取失败 {path}: {e}")
continue
if not images:
continue
# 提取特征
with torch.no_grad():
inputs = self.processor(images=images, return_tensors="pt",
padding=True).to(self.device)
features = self.model.get_image_features(**inputs)
# 归一化(用于余弦相似度)
features = features / features.norm(p=2, dim=-1, keepdim=True)
all_features.append(features.cpu())
print(f"已索引: {min(i+batch_size, len(self.image_paths))}/{len(self.image_paths)}")
# 3. 合并所有特征
self.image_features = torch.cat(all_features)
print("索引完成!")
def search(self, query_text, top_k=5):
"""使用文本搜索图片"""
if self.image_features is None:
raise ValueError("请先调用 index_images() 建立索引")
# 1. 编码查询文本
with torch.no_grad():
inputs = self.processor(text=[query_text], return_tensors="pt",
padding=True).to(self.device)
text_features = self.model.get_text_features(**inputs)
text_features = text_features / text_features.norm(p=2, dim=-1, keepdim=True)
# 2. 计算相似度(余弦相似度)
# [1, D] @ [N, D]^T = [1, N]
similarities = (text_features.cpu() @ self.image_features.T).squeeze(0)
# 3. 获取 Top-K
values, indices = similarities.topk(top_k)
results = []
for val, idx in zip(values, indices):
results.append({
'path': self.image_paths[idx],
'score': val.item()
})
return results
# 使用示例
if __name__ == "__main__":
# 1. 创建搜索引擎并建立索引
engine = ImageSearchEngine()
engine.index_images("./my_photos") # 替换为你的图片文件夹
# 2. 搜索
queries = [
"a dog playing in the park",
"sunset at the beach",
"a person reading a book"
]
for query in queries:
print(f"\n查询: '{query}'")
results = engine.search(query, top_k=3)
for i, result in enumerate(results, 1):
print(f" {i}. {result['path']} (相似度: {result['score']:.4f})")
输出示例:
找到 1523 张图片,开始建立索引...
已索引: 32/1523
已索引: 64/1523
...
索引完成!
查询: 'a dog playing in the park'
1. ./my_photos/IMG_2023.jpg (相似度: 0.8752)
2. ./my_photos/IMG_1845.jpg (相似度: 0.8231)
3. ./my_photos/IMG_2091.jpg (相似度: 0.7963)
7.3 实战:使用 GPT-4V 进行高级视觉理解
python
"""
调用 GPT-4V API 进行图像理解
需要: pip install openai
"""
from openai import OpenAI
import base64
client = OpenAI(api_key="your-api-key")
def encode_image(image_path):
"""将图片编码为 base64"""
with open(image_path, "rb") as image_file:
return base64.b64encode(image_file.read()).decode('utf-8')
def analyze_image(image_path, question):
"""使用 GPT-4V 分析图像"""
base64_image = encode_image(image_path)
response = client.chat.completions.create(
model="gpt-4o", # 或 "gpt-4-turbo"
messages=[
{
"role": "user",
"content": [
{
"type": "text",
"text": question
},
{
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{base64_image}",
"detail": "high" # 高分辨率模式
}
}
]
}
],
max_tokens=500
)
return response.choices[0].message.content
# 使用示例
if __name__ == "__main__":
# 1. 图像描述
description = analyze_image(
"chart.png",
"详细描述这张图表,包括类型、趋势和关键数据点"
)
print("图表分析:", description)
# 2. OCR + 结构化输出
ocr_result = analyze_image(
"receipt.jpg",
"提取这张收据中的所有信息,以 JSON 格式输出,包括:商家名称、日期、商品列表、总金额"
)
print("收据信息:", ocr_result)
# 3. 视觉推理
reasoning = analyze_image(
"scene.jpg",
"这张图片中有哪些潜在的安全隐患?请列举并解释"
)
print("安全分析:", reasoning)
八、当前视角:Connector vs Native Multimodal
核心定位:深入理解两种多模态架构范式的本质区别 - Connector(连接器)是"外挂眼睛",Native(原生)是"全身神经系统"。
8.1 架构范式对比:眼睛 vs 神经系统
Connector 方案(LLaVA):外挂的"眼睛"
┌──────────────────────────────────────────────────────┐
│ Connector 架构 (LLaVA) │
└──────────────────────────────────────────────────────┘
┌─────────────┐
│ 大脑 │
│ (LLM) │ ← 只懂"语言"
└──────┬──────┘
↑
投影层
(翻译器)
↑
┌──────────────┐
│ 眼睛 │
│ (CLIP ViT) │ ← 只懂"视觉"
└──────┬───────┘
↑
🖼️ 图像
本质:两个独立训练的系统,通过"翻译层"勉强沟通
类比:给只会中文的人配一个英语翻译
工作流程:
-
视觉编码器(CLIP ViT):独立预训练,冻结参数
- 训练数据:4亿图文对(CLIP 数据集)
- 目标:图文对比学习
- 输出:1024 维视觉特征
-
投影层(Projection):桥接层,唯一可训练
- 作用:将 1024 维视觉特征"伪装"成 4096 维文本 Token
- 训练数据:少量(30万-150万)图文对
- 挑战:必须在有限数据下完成"翻译"任务
-
语言模型(LLM):独立预训练,微调
- 训练数据:数万亿 Token 的纯文本
- 目标:语言建模
- 问题:从未在预训练中"见过"真实图像
Native 方案(GPT-4o):原生的"神经系统"
┌──────────────────────────────────────────────────────┐
│ Native 架构 (GPT-4o) │
└──────────────────────────────────────────────────────┘
┌─────────────────┐
│ 统一大脑 │
│ (Transformer) │
│ │
│ 从出生就同时 │
│ "看""听""说" │
└────────┬────────┘
↑
统一 Token 流
↑
┌──────────────────┼──────────────────┐
│ │ │
🖼️ 图像 📝 文本 🔊 音频
(VQ-VAE) (BPE) (Codec)
│ │ │
└──────────────────┴──────────────────┘
所有模态共享同一词汇表
本质:从零开始,用多模态数据联合训练的单一系统
类比:从小在双语环境长大的人,天生就会中英文
工作流程:
-
统一 Token 化:所有模态转为离散 Token
文本: "猫" → Token ID 1024 图像: 🐱 → Token ID 256142 音频: 喵 → Token ID 264523 -
统一 Transformer:单一模型处理所有 Token
- 训练数据:混合数据(文本 + 图像 + 音频 + ...)
- 训练目标:统一的 Next Token Prediction
- 优势:每层都能学习跨模态交互
-
无需桥接层:所有模态天生在同一空间
- 无信息瓶颈
- 无需"翻译"
- 自然支持模态混合
8.2 核心差异:深入技术对比
| 维度 | Connector (LLaVA) | Native (GPT-4o) |
|---|---|---|
| 训练范式 | 🔧 组装式:先单模态 → 后拼接 | 🌱 原生式:从零多模态联合训练 |
| Token 空间 | 🔀 分离后对齐 : - CLIP: R 1024 \mathbb{R}^{1024} R1024 - LLM: R 4096 \mathbb{R}^{4096} R4096 - 投影层强行对齐 | ✨ 天然统一 : - 所有模态共享同一词汇表 - [0, 300k) 包含文本/图像/音频 |
| 信息流动 | 🚧 单向受限 : 视觉 → 投影层 → LLM 投影层是瓶颈(576 Token) | 🌊 全向流动 : 任意模态可在任意层交互 无瓶颈 |
| 细粒度交互 | ❌ 浅层交互 : - 只有 LLM 的自注意力能跨模态 - 视觉编码器完全不知道文本 | ✅ 深层融合 : - 每层 Transformer 都跨模态 - 图像、文本、音频互相"理解" |
| 长上下文 | 📏 受限 : - 8K Token(LLaMA-7B) - 视觉 Token 占大头(576×N 帧) | 🚀 超长 : - 128K+ (GPT-4o) - 1M Token (Gemini 1.5) |
| 模态数量 | 🔢 受限 : - 通常只支持文本+图像 - 添加新模态需要新的编码器+投影层 | ∞ 无限扩展 : - 文本、图像、音频、视频统一 - 添加新模态只需扩展词汇表 |
| 训练成本 | 💰 低 : - 100万美元级别 - 只训练投影层+微调 LLM | 💸 极高 : - 数亿美元级别 - 从零训练整个模型 |
| 推理效率 | ⚡ 高 : - 7B-13B 参数 - 可本地部署 | 🐢 低 : - 数百 B 参数(推测) - 只能 API 调用 |
8.3 能力对比:实际场景测试
场景 1:细粒度视觉推理
任务:图中左下角的咖啡杯是什么颜色?
Connector(LLaVA)表现:
问:图中左下角的咖啡杯是什么颜色?
答:图片中有一个咖啡杯,它是白色的。
问题:无法精确定位"左下角",因为:
- CLIP ViT 只输出全局特征(576 个 patch)
- 投影层压缩后,空间信息进一步损失
- LLM 难以理解精确的空间位置
Native(GPT-4o)表现:
问:图中左下角的咖啡杯是什么颜色?
答:左下角的咖啡杯是深蓝色,杯身上有白色的品牌logo。
优势:
- 原生视觉编码保留更多空间信息
- 统一 Transformer 能在多层细化空间理解
- 无信息瓶颈
场景 2:跨模态推理
任务:看图说话,并解释为什么这张图令人感动。
Connector(LLaVA)表现:
答:图片显示了一位母亲抱着她的孩子。这可能令人感动,
因为它展示了母爱。
特点:
- 描述准确但浅层
- 缺乏情感细节(表情、姿态、背景)
- 推理链较短
Native(GPT-4o)表现:
答:图片中,一位母亲正紧紧拥抱着她的孩子,两人都闭着眼睛,
脸上洋溢着幸福的微笑。阳光从窗户洒进来,温暖地照在她们
身上。这张图令人感动,因为:
1. 肢体语言:紧密的拥抱传递出深深的爱
2. 面部表情:满足且安心的微笑
3. 光线氛围:暖色调烘托出温馨感
这种母子间的纯粹情感连接触动人心。
优势:
- 多层次细节观察
- 视觉-情感-语言深度融合
- 推理链更长更连贯
场景 3:视频+音频理解
任务:分析这段钢琴演奏视频,评价演奏者的技巧。
Connector(LLaVA-Video)表现:
答:无法完成任务
原因:
- 需要单独的音频编码器(增加复杂度)
- 视频帧和音频 Token 如何同步?
- 投影层如何处理三模态(视频+音频+文本)?
Native(GPT-4o)表现:
输入:[视频帧 Token] + [音频 Token] + [文本 Token]
答:演奏者展现了出色的技巧:
- 视觉观察:手指动作流畅,踏板使用精准
- 听觉分析:音色饱满,节奏稳定,强弱对比明显
- 综合评价:这是一场高水平的演出
优势:
- Any-to-Any 原生支持
- 统一 Token 空间无需复杂工程
8.4 类比理解:两种架构的本质
Connector:拼接汽车
┌─────────┐ ┌──────────┐ ┌─────────┐
│ 自行车 │ → │ 改装套件 │ → │ 电动车 │
│ 引擎 │ │ (投影层) │ │ (能跑) │
└─────────┘ └──────────┘ └─────────┘
优点:
✅ 便宜:利用现成部件
✅ 快速:组装即可上路
缺点:
❌ 性能受限:引擎和电池不匹配
❌ 效率低:能量在转换中损失
❌ 扩展难:加装音响系统很麻烦
Native:原生电动车
┌─────────────────────────────────────┐
│ 特斯拉 (Tesla) │
│ - 电池、引擎、控制系统一体化设计 │
│ - 从零开始为电动优化 │
│ - 软硬件深度集成 │
└─────────────────────────────────────┘
优点:
✅ 性能强:专为目标设计
✅ 效率高:无能量转换损失
✅ 扩展易:添加功能只需软件升级
缺点:
❌ 昂贵:研发成本数十亿美元
❌ 耗时:需要多年迭代
8.5 未来趋势预测
短期(1-2年):Connector 仍是主流
- ✅ 开源社区持续优化 LLaVA 类模型
- ✅ Qwen-VL、InternVL 等国产方案成熟
- ✅ 企业优先选择成本低、易部署的方案
中期(2-3年):Native 开始普及
- 🚀 新一代闭源模型进一步提升能力
- 🚀 开源社区尝试小规模 Native 模型(7B-13B)
- 🚀 Omni 模型成为高端应用标配
长期(3年以上):Native 完全主导
- 🌟 训练成本降低(更高效的算法)
- 🌟 开源 Native 模型性能追上 Connector
- 🌟 Connector 方案逐渐被淘汰(就像单模态模型淘汰传统 CV/NLP pipeline)
8.6 选型建议(最新版)
┌──────────────────────────────────────────────────────┐
│ 决策树 │
└──────────────────────────────────────────────────────┘
你的预算是否 > $10万/年?
│
├─ 否 → 用 Connector (LLaVA, Qwen-VL)
│ - 开源免费
│ - 可本地部署
│ - 适合:研究、原型、中小企业
│
└─ 是 → 你需要顶级性能吗?
│
├─ 是 → Native (GPT-4o, Claude 3.5)
│ - API 调用
│ - 适合:高价值应用(医疗、金融)
│
└─ 否 → 混合方案
- 简单任务:Connector (自部署)
- 复杂任务:Native (API)
- 适合:成本敏感的生产环境
具体场景推荐:
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 学术研究 | LLaVA-1.5-7B | 开源、可复现、社区活跃 |
| 产品原型 | Qwen-VL-Chat | 中文友好、部署简单 |
| 内容审核 | Connector 自部署 | 隐私保护、低延迟 |
| 医疗诊断 | GPT-4o / Claude 3.5 | 最高精度、可解释性强 |
| 长视频分析 | Gemini 1.5 Pro | 1M Token 上下文 |
| 实时语音交互 | GPT-4o | Any-to-Any 原生支持 |
8.7 实战:对比测试两种架构
python
"""
对比 Connector (LLaVA) 和 Native (GPT-4o) 在同一任务上的表现
"""
from transformers import LlavaForConditionalGeneration, AutoProcessor
from openai import OpenAI
from PIL import Image
import base64
import torch
# ========== Connector 方案 (LLaVA) ==========
def test_connector(image_path, question):
model_id = "llava-hf/llava-1.5-7b-hf"
model = LlavaForConditionalGeneration.from_pretrained(
model_id, torch_dtype=torch.float16, device_map="auto"
)
processor = AutoProcessor.from_pretrained(model_id)
image = Image.open(image_path)
prompt = f"USER: <image>\n{question}\nASSISTANT:"
inputs = processor(text=prompt, images=image, return_tensors="pt").to("cuda")
with torch.inference_mode():
generated_ids = model.generate(**inputs, max_new_tokens=200)
output = processor.decode(generated_ids[0], skip_special_tokens=True)
return output.split("ASSISTANT:")[-1].strip()
# ========== Native 方案 (GPT-4o) ==========
def test_native(image_path, question):
client = OpenAI(api_key="your-api-key")
with open(image_path, "rb") as f:
base64_image = base64.b64encode(f.read()).decode('utf-8')
response = client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "user",
"content": [
{"type": "text", "text": question},
{
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{base64_image}",
"detail": "high"
}
}
]
}],
max_tokens=200
)
return response.choices[0].message.content
# ========== 对比测试 ==========
if __name__ == "__main__":
test_cases = [
{
"image": "complex_scene.jpg",
"question": "请详细描述图片右上角的物体,并解释它为什么重要。"
},
{
"image": "chart.png",
"question": "分析这张图表的趋势,并给出投资建议。"
},
{
"image": "medical_scan.jpg",
"question": "识别图中的异常区域,并评估严重程度(仅供参考)。"
}
]
for i, test in enumerate(test_cases, 1):
print(f"\n{'='*60}")
print(f"测试 {i}: {test['question']}")
print(f"{'='*60}")
print("\n[Connector - LLaVA]")
connector_answer = test_connector(test['image'], test['question'])
print(connector_answer)
print("\n[Native - GPT-4o]")
native_answer = test_native(test['image'], test['question'])
print(native_answer)
print(f"\n{'='*60}\n")
预期观察:
- 细节丰富度:Native > Connector
- 推理深度:Native > Connector
- 空间理解:Native > Connector
- 响应速度:Connector > Native(本地部署)
- 成本:Connector < Native
九、总结与展望
9.1 核心知识点回顾
| 技术 | 核心思想 | 关键创新 |
|---|---|---|
| ViT | 图像分块 → Transformer | 证明 Transformer 可处理视觉 |
| CLIP | 对比学习对齐图文 | 零样本能力,跨模态检索 |
| LLaVA | 投影层连接视觉和语言 | 简单高效,易于训练 |
| Native Multimodal | 统一 Token 空间 | 更强交互,更长上下文 |
9.2 多模态技术演进路线
2017: Transformer 诞生(纯文本)
↓
2020: ViT 证明 Transformer 可处理图像
↓
2021: CLIP 实现图文对齐(对比学习)
↓
2023: LLaVA 连接 LLM 和视觉(投影层方案)
↓
SOTA: GPT-4V/Gemini 原生多模态(端到端训练)
↓
当前: Omni 模型成标配(图文音视频统一)
9.3 未来趋势
-
Any-to-Any 模型
- 输入:图/文/音/视频
- 输出:图/文/音/视频
- 代表:GPT-4o(实时语音对话 + 视觉)
-
具身智能(Embodied AI)
- 多模态 + 机器人控制
- 感知(视觉)+ 理解(语言)+ 行动(控制)
- 代表:RT-2、PaLM-E
-
更长上下文
- 处理完整电影、长文档
- Gemini 1.5:1M Token(约 1 小时视频)
-
更高效的训练
- 小模型 + 大数据 > 大模型 + 小数据
- LoRA、QLoRA 等高效微调技术
9.4 学习资源
论文:
- ViT: An Image is Worth 16x16 Words
- CLIP: Learning Transferable Visual Models
- LLaVA: Visual Instruction Tuning
- GPT-4o: Omni Technical Report
代码:
- LLaVA: https://github.com/haotian-liu/LLaVA
- CLIP: https://github.com/openai/CLIP
- Video-LLaVA: https://github.com/PKU-YuanGroup/Video-LLaVA
实践建议:
- 先用 CLIP 熟悉图文对齐
- 尝试部署 LLaVA-1.5-7B(本地 GPU)
- 使用 GPT-4o/Gemini API 体验原生多模态
- 尝试处理简单视频(抽帧方案)
下一步:详见 [Part 6 第4章] 多模态模型评估,学习如何评估多模态模型的能力。