多模态代码补充指南
本文档定位:补充 Ch22 多模态基础文档中缺失的代码实操部分——ViT 手写实现、CLIP 手写实现、预训练 CLIP 模型下载与使用。
前置阅读:先看完
22_强化学习/05_多模态基础.md理解概念,再回来读代码。
📖 阅读优先级
| 等级 | 章节 | 说明 |
|---|---|---|
| 🔴 可跑通 | ViT 手写 + CLIP 手写 | 代码完整,可在本地跑 |
| 🟡 理解即可 | 预训练 CLIP 使用 | 知道怎么下载和调用模型 |
| 🟢 了解即可 | 扩散模型/DALL-E2 | Ch22 已有概念,无新代码 |
一、知识点对照:Ch22 vs Ch26
| 知识点 | Ch22 多模态基础 | Ch26 新增 |
|---|---|---|
| ViT | ✅ 概念(切图+Transformer) | 🔴 手写代码 286行 |
| CLIP | ✅ 概念(对比学习匹配) | 🔴 手写代码 469行 |
| 预训练 CLIP | ❌ 未涉及 | 🔴 模型下载+使用 |
| 中文 CLIP | ❌ 未涉及 | 🔴 chinese-clip 权重 |
| 扩散模型 | ✅ 概念 | ❌ 无新代码 |
| DALL-E2 | ✅ 概念 | ❌ 无新代码 |
二、ViT 手写实现(code-1-vit.py)
🟡【P1 看注释就行】 ViT 的核心就是把图片切成 patch 然后送进 Transformer。以下是最核心的 PatchEmbedding 和 位置编码部分。
2.1 Patch Embedding
python
class PatchEmbedding(nn.Module):
"""把图片切成固定大小的 patch,展平并投射到 d_model 维度"""
def __init__(self, d_model, img_size, patch_size, n_channels):
super().__init__()
# 用 Conv2d 实现 patch 切分:
# kernel_size = patch_size, stride = patch_size
# → 相当于把图片切成不重叠的 patch,每个 patch 展平后线性投影
self.linear_project = nn.Conv2d(
n_channels, d_model,
kernel_size=patch_size, stride=patch_size
)
def forward(self, x):
# (B, C, H, W) → (B, d_model, P_col, P_row)
x = self.linear_project(x)
# (B, d_model, P_col, P_row) → (B, d_model, P) → (B, P, d_model)
x = x.flatten(2).transpose(1, 2)
return x🟡【理解即可】 PatchEmbedding 的输出形状是 (batch_size, num_patches, d_model)——和 Transformer 处理文本时的 (batch_size, seq_len, d_model) 完全一致。这就是 ViT 的巧妙之处。
2.2 位置编码
python
class PositionalEncoding(nn.Module):
def __init__(self, d_model, max_seq_length):
super().__init__()
self.cls_token = nn.Parameter(torch.randn(1, 1, d_model)) # 分类 token
pe = torch.zeros(max_seq_length, d_model)
for pos in range(max_seq_length):
for i in range(d_model):
if i % 2 == 0:
pe[pos][i] = np.sin(pos / (10000 ** (i / d_model)))
else:
pe[pos][i] = np.cos(pos / (10000 ** ((i - 1) / d_model)))
self.register_buffer('pe', pe.unsqueeze(0))
def forward(self, x):
# 拼接 cls_token + 位置编码
cls_token = self.cls_token.expand(x.shape[0], -1, -1)
x = torch.cat([cls_token, x], dim=1)
x = x + self.pe
return x训练流程:
bash
# 在 MNIST 上训练 ViT 分类器
python code-1-vit.py
# → 输出:MNIST 测试集上的分类准确率三、CLIP 手写实现(code-2-clip.py)
🟡【P1 看注释就行】 CLIP 包含两个编码器(文本 + 图像),通过对比学习让匹配的图文对在向量空间中靠近。
3.1 文本编码器
python
# 文本端:用 Transformer Encoder 编码文本
class TextEncoder(nn.Module):
def __init__(self, vocab_size, width, max_seq_length, num_layers):
# vocab_size: 词表大小
# width: 嵌入维度(d_model)
# max_seq_length: 最大序列长度
# num_layers: Transformer 层数
self.token_embedding = nn.Embedding(vocab_size, width)
self.positional_embedding = PositionalEmbedding(width, max_seq_length)
self.transformer = nn.ModuleList([
TransformerBlock(width) for _ in range(num_layers)
])
def forward(self, text):
# text: (batch_size, seq_len) → token IDs
x = self.token_embedding(text)
x = self.positional_embedding(x)
for block in self.transformer:
x = block(x)
# 取 [EOS] 位置的特征作为文本向量
return x[range(x.shape[0]), text.argmax(dim=-1)]3.2 图像编码器
python
# 图像端:用 ViT 编码图像
class ImageEncoder(nn.Module):
def __init__(self, width, img_size, patch_size, n_channels, num_layers):
self.patch_embedding = PatchEmbedding(width, img_size, patch_size, n_channels)
self.positional_embedding = PositionalEmbedding(width, ...)
self.transformer = nn.ModuleList([
TransformerBlock(width) for _ in range(num_layers)
])
def forward(self, image):
x = self.patch_embedding(image)
x = self.positional_embedding(x)
for block in self.transformer:
x = block(x)
# 取 [CLS] 位置的特征作为图像向量
return x[:, 0, :]3.3 对比学习损失
python
class CLIPModel(nn.Module):
def __init__(self, text_encoder, image_encoder):
self.text_encoder = text_encoder
self.image_encoder = image_encoder
# 投影层:将两个编码器的输出映射到同一维度空间
self.text_projection = nn.Linear(width, projection_dim)
self.image_projection = nn.Linear(width, projection_dim)
def forward(self, text, image):
# 分别编码文本和图像
text_features = self.text_projection(self.text_encoder(text))
image_features = self.image_projection(self.image_encoder(image))
# 归一化
text_features = text_features / text_features.norm(dim=-1, keepdim=True)
image_features = image_features / image_features.norm(dim=-1, keepdim=True)
# 计算相似度矩阵
logits = text_features @ image_features.T * temperature
return logits
# 损失函数:交叉熵,让匹配的图文对相似度高,不匹配的相似度低
def clip_loss(logits_per_text):
labels = torch.arange(len(logits_per_text))
text_loss = F.cross_entropy(logits_per_text, labels)
image_loss = F.cross_entropy(logits_per_text.T, labels)
return (text_loss + image_loss) / 2训练流程:
bash
# 需要从 datasets 库加载配对数据
python code-2-clip.py
# → 输出:训练过程中的 loss 变化四、预训练 CLIP 模型使用
🟡【理解即可】 自己训练 CLIP 需要大量算力。实际使用直接下载预训练模型。
4.1 原始 CLIP(OpenAI)
python
# 需要安装:pip install git+https://github.com/openai/CLIP.git
import clip
import torch
# 加载预训练模型(自动下载权重)
model, preprocess = clip.load("ViT-B/32")
# 使用
image = preprocess(image).unsqueeze(0)
text = clip.tokenize(["一只猫", "一条狗"])
with torch.no_grad():
logits_per_image, _ = model(image, text)
probs = logits_per_image.softmax(dim=-1)
print(probs) # → 输出各类别概率4.2 中文 CLIP(Chinese-CLIP)
Ch26 资料中提供了中文 CLIP 权重:
python
# chinese-clip-vit-base-patch16/ 目录下的模型可以直接加载
from transformers import CLIPModel, CLIPProcessor
model = CLIPModel.from_pretrained("./chinese-clip-vit-base-patch16/")
processor = CLIPProcessor.from_pretrained("./chinese-clip-vit-base-patch16/")
# 使用
image = Image.open("festival.jpg")
inputs = processor(
text=["春节", "端午节", "中秋节"],
images=image,
return_tensors="pt"
)
outputs = model(**inputs)
print(outputs.logits_per_image.softmax(dim=-1))五、对 Agent 开发的实际意义
和多模态基础文档的判断一致——了解就好,用到时再回来看代码。
| 技术 | 什么时候需要 | 怎么用 |
|---|---|---|
| ViT | 需要自定义图像分类 | 直接调 torchvision.models.vit_b_16(pretrained=True) |
| CLIP | 需要图文匹配/图片搜索 | 直接调 clip.load("ViT-B/32") |
| 中文 CLIP | 需要处理中文图片场景 | 调 chinese-clip-vit-base-patch16 权重 |
| 扩散模型 | 需要生成图片 | 调 Stable Diffusion API / DALL-E API |