Skip to content

多模态代码补充指南

本文档定位:补充 Ch22 多模态基础文档中缺失的代码实操部分——ViT 手写实现、CLIP 手写实现、预训练 CLIP 模型下载与使用。

前置阅读:先看完 22_强化学习/05_多模态基础.md 理解概念,再回来读代码。


📖 阅读优先级

等级章节说明
🔴 可跑通ViT 手写 + CLIP 手写代码完整,可在本地跑
🟡 理解即可预训练 CLIP 使用知道怎么下载和调用模型
🟢 了解即可扩散模型/DALL-E2Ch22 已有概念,无新代码

一、知识点对照: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

OPC 超级个体实战指南