首页/知识库/多模态学习(三):多模态大模型与统一架构

多模态学习(三):多模态大模型与统一架构

✍️ AI Master📅 创建 2026-04-12📖 18 min 阅读
💡

文章摘要

从图像到文本再到图像,理解跨模态生成的核心技术

1图像描述(Image Captioning)任务概述

图像描述是多模态理解的核心任务之一,目标是为输入图像生成一段自然语言描述。该任务要求模型同时具备视觉感知能力和语言生成能力,本质上是一个跨模态的序列生成问题。经典的数据集包括 MSCOCO(12.3 万张图像,每张 5 条人工标注描述)和 Flickr30k(3.1 万张图像)。图像描述的技术路线经历了从早期模板方法到统计学习,再到深度学习的演进。现代方法普遍采用 Encoder-Decoder 架构,其中 Encoder 负责提取图像的语义特征表示,Decoder 则基于这些特征自回归地生成描述文本。该任务不仅本身具有重要的应用价值,还是许多下游多模态任务(如视觉问答、视觉推理)的基础。

python
# 使用 COCO API 加载和可视化图像描述
from pycocotools.coco import COCO
import matplotlib.pyplot as plt

coco = COCO("annotations/captions_val2017.json")
img_ids = coco.getImgIds()[:3]
for img_id in img_ids:
    img_info = coco.loadImgs(img_id)[0]
    ann_ids = coco.getAnnIds(imgIds=img_id)
    anns = coco.loadAnns(ann_ids)
    captions = [ann["caption"] for ann in anns]
    print(f"Image {img_info['file_name']}:")
    for i, cap in enumerate(captions[:3], 1):
        print(f"  [{i}] {cap}")
python
# 构建图像-文本对的数据加载器
from torch.utils.data import Dataset
from PIL import Image
import torchvision.transforms as T

class CaptionDataset(Dataset):
    def __init__(self, image_dir, annotations, vocab):
        self.image_dir = image_dir
        self.annotations = annotations
        self.vocab = vocab
        self.transform = T.Compose([
            T.Resize(256),
            T.CenterCrop(224),
            T.ToTensor(),
            T.Normalize([0.485, 0.456, 0.406],
                        [0.229, 0.224, 0.225])
        ])

    def __len__(self):
        return len(self.annotations)

    def __getitem__(self, idx):
        ann = self.annotations[idx]
        image = Image.open(f"{self.image_dir}/{ann['image']}")
        image = self.transform(image)
        caption_ids = self.vocab.encode(ann["caption"])
        return image, caption_ids
数据集图像数描述数每张描述数

MSCOCO

123,287

615,435

5

Flickr30k

31,783

158,915

5

Flickr8k

8,092

40,460

5

Conceptual Captions

3.3M

3.3M

1

开始学习时先用 MSCOCO 的验证集做实验,该数据集标注质量高且社区工具链完善。

图像描述与图像分类不同,同一图像可能存在多种正确描述,不要期望模型输出与某个标注完全匹配。

2Encoder-Decoder 架构详解

Encoder-Decoder 架构是图像描述模型的骨干结构。Encoder 通常使用预训练的卷积神经网络(如 ResNet、EfficientNet)或视觉 Transformer(如 ViT)将图像编码为固定维度的特征表示。传统方法将整个图像压缩为一个全局特征向量,但这种方式会丢失空间细节信息。改进后的方法保留空间特征图,输出形状为 (H, W, C) 的张量,其中 H 和 W 是特征图的空间维度,C 是通道数。Decoder 通常基于 LSTM 或 Transformer,它接收 Encoder 输出的视觉特征,并结合上一时刻生成的词元,逐步产生描述文本。Show and Tell 模型是这一架构的奠基性工作,其 Encoder 使用 Inception-v3 提取图像特征,Decoder 使用单层 LSTM 生成文本序列,开创了端到端图像描述的先河。

python
# Show and Tell 风格的 Encoder-Decoder 模型
import torch
import torch.nn as nn
import torchvision.models as models

class ShowAndTell(nn.Module):
    def __init__(self, vocab_size, embed_dim, hidden_dim):
        super().__init__()
        # Encoder: 预训练 ResNet-50
        resnet = models.resnet50(weights="IMAGENET1K_V1")
        self.encoder = nn.Sequential(*list(resnet.children())[:-1])
        for param in self.encoder.parameters():
            param.requires_grad = False

        # Decoder: LSTM
        self.embed = nn.Embedding(vocab_size, embed_dim)
        self.lstm = nn.LSTM(embed_dim, hidden_dim, batch_first=True)
        self.fc = nn.Linear(hidden_dim, vocab_size)

    def forward(self, images, captions):
        features = self.encoder(images).squeeze(-1).squeeze(-1)
        embeddings = self.embed(captions)
        h0, c0 = self._init_hidden(features)
        outputs, _ = self.lstm(embeddings, (h0, c0))
        return self.fc(outputs)

    def _init_hidden(self, features):
        h0 = features.unsqueeze(0)
        c0 = torch.zeros_like(h0)
        return h0, c0
python
# 基于 ViT 的视觉 Encoder
import torch.nn as nn
from transformers import ViTModel

class ViTEncoder(nn.Module):
    def __init__(self, model_name="google/vit-base-patch16-224"):
        super().__init__()
        self.vit = ViTModel.from_pretrained(model_name)
        for param in self.vit.parameters():
            param.requires_grad = False

    def forward(self, images):
        outputs = self.vit(pixel_values=images)
        # 使用 [CLS] token 作为全局表示
        cls_token = outputs.last_hidden_state[:, 0, :]
        # 使用所有 patch token 作为空间特征
        patch_tokens = outputs.last_hidden_state[:, 1:, :]
        return cls_token, patch_tokens

# 测试
encoder = ViTEncoder()
dummy = torch.randn(2, 3, 224, 224)
cls_feat, patch_feat = encoder(dummy)
print(f"CLS: {cls_feat.shape}")       # [2, 768]
print(f"Patches: {patch_feat.shape}")  # [2, 196, 768]
Encoder 类型参数量输出维度空间信息

ResNet-50 (全局池化)

25M

(batch, 2048)

丢失

ResNet-101 (特征图)

44M

(batch, 7, 7, 2048)

保留

ViT-Base

86M

(batch, 197, 768)

保留

CLIP-ViT-L/14

307M

(batch, 257, 1024)

保留

冻结 Encoder 参数进行预训练可以大幅减少显存占用和训练时间,待 Decoder 收敛后再微调 Encoder 通常能获得更好的性能。

使用预训练 ViT 时,输入图像必须经过正确的归一化(mean=[0.5,0.5,0.5], std=[0.5,0.5,0.5]),否则特征质量会严重下降。

3注意力机制在 Captioning 中的应用

Show, Attend and Tell 模型首次将注意力机制引入图像描述任务。其核心思想是:生成每个词时,模型不应同等对待整张图像,而应该关注与当前词相关的图像区域。这种软注意力机制计算每个空间位置的权重分布,使得 Decoder 能够动态地聚焦于图像的不同区域。例如生成 "dog" 时关注图像中的狗所在区域,生成 "grass" 时关注草地区域。注意力权重还可以被可视化,帮助理解模型的决策过程。后续工作进一步引入了自顶向下的注意力(Top-Down Attention),使用 Faster R-CNN 检测图像中的目标区域(objects),然后在这些候选区域上计算注意力。这种方法比基于网格的注意力更加精确,因为注意力直接作用于语义上有意义的目标区域,而非任意的特征图像素网格。

python
# 软注意力(Soft Attention)机制实现
class SoftAttention(nn.Module):
    def __init__(self, encoder_dim, decoder_dim):
        super().__init__()
        self.encoder_att = nn.Linear(encoder_dim, decoder_dim)
        self.decoder_att = nn.Linear(decoder_dim, decoder_dim)
        self.full_att = nn.Linear(decoder_dim, 1)
        self.relu = nn.ReLU()
        self.softmax = nn.Softmax(dim=1)

    def forward(self, encoder_out, decoder_hidden):
        # encoder_out: (batch, num_pixels, encoder_dim)
        att1 = self.encoder_att(encoder_out)        # (batch, num_pixels, decoder_dim)
        att2 = self.decoder_att(decoder_hidden)      # (batch, decoder_dim)
        att = self.full_att(self.relu(att1 + att2.unsqueeze(1))).squeeze(2)
        alpha = self.softmax(att)  # (batch, num_pixels)
        attention_weighted_encoding = (encoder_out * alpha.unsqueeze(2)).sum(dim=1)
        return attention_weighted_encoding, alpha
python
# 自顶向下注意力(Top-Down Attention)
class TopDownAttention(nn.Module):
    def __init__(self, obj_dim, hidden_dim, num_heads=8):
        super().__init__()
        self.multihead_attn = nn.MultiheadAttention(
            embed_dim=hidden_dim,
            num_heads=num_heads,
            batch_first=True,
            kdim=obj_dim,
            vdim=obj_dim,
        )
        self.obj_proj = nn.Linear(obj_dim, hidden_dim)

    def forward(self, decoder_query, object_features, mask=None):
        key = self.obj_proj(object_features)
        value = object_features
        attended, attn_weights = self.multihead_attn(
            query=decoder_query,
            key=key,
            value=value,
            key_padding_mask=mask,
        )
        return attended, attn_weights

# object_features 来自 Faster R-CNN 的 RoI Align
# shape: (batch, num_objects, obj_dim)
注意力类型特征来源注意力粒度性能 (CIDEr)

无注意力

全局特征

整图

94.0

软注意力

CNN 特征图

网格 (7x7)

105.2

硬注意力

CNN 特征图

网格 (7x7)

100.5

自顶向下注意力

目标检测区域

语义区域

120.1

可视化注意力权重是调试模型的重要手段,可以直观地检查模型是否关注到了正确的图像区域。

硬注意力(Hard Attention)不可导,需要使用 REINFORCE 算法进行策略梯度训练,方差大且训练不稳定,建议优先使用软注意力。

4文本到图像生成(DALL-E / Stable Diffusion)

文本到图像生成是图像描述的逆任务,也是近年来 AI 领域最引人注目的突破之一。DALL-E 由 OpenAI 提出,采用 Transformer 架构将文本和图像统一为离散的 token 序列。它首先使用 VQ-VAE 将图像压缩为离散的视觉 token,然后将文本 token 和视觉 token 拼接后送入 Transformer 进行自回归生成。这种方法概念简洁,但分辨率和质量受限于 VQ-VAE 的压缩能力。DALL-E 2 引入了 CLIP 和扩散模型,通过 CLIP 的文本编码器提取文本的语义嵌入,然后使用先验网络将文本嵌入转换为图像嵌入,最后通过扩散解码器从噪声中逐步生成图像。Stable Diffusion 则进一步降低了计算成本,它在潜空间(Latent Space)而非像素空间进行扩散过程,大幅减少了计算量和显存需求,使得在消费级 GPU 上运行高质量图像生成成为可能。

python
# 使用 Stable Diffusion 生成图像
from diffusers import StableDiffusionPipeline
import torch

# 加载预训练模型
pipe = StableDiffusionPipeline.from_pretrained(
    "runwayml/stable-diffusion-v1-5",
    torch_dtype=torch.float16,
)
pipe = pipe.to("cuda")

# 文本到图像生成
prompt = "a photograph of an astronaut riding a horse"
image = pipe(
    prompt,
    num_inference_steps=50,
    guidance_scale=7.5,
).images[0]
image.save("astronaut_horse.png")
python
# DDPM 扩散过程的核心公式实现
import torch

class DiffusionProcess:
    def __init__(self, num_steps=1000):
        self.num_steps = num_steps
        beta = torch.linspace(1e-4, 0.02, num_steps)
        self.alpha = 1.0 - beta
        self.alpha_bar = torch.cumprod(self.alpha, dim=0)

    def add_noise(self, x0, t):
        noise = torch.randn_like(x0)
        sqrt_alpha_bar = torch.sqrt(self.alpha_bar[t])
        sqrt_one_minus_alpha_bar = torch.sqrt(1 - self.alpha_bar[t])
        return sqrt_alpha_bar * x0 + sqrt_one_minus_alpha_bar * noise, noise

    def sample(self, model, xT, text_embed):
        x = xT
        for t in reversed(range(self.num_steps)):
            noise_pred = model(x, t, text_embed)
            # 简化的反向采样步骤
            x = self._reverse_step(x, noise_pred, t)
        return x
模型年份架构分辨率核心创新

DALL-E

2021

Transformer + VQ-VAE

256x256

统一 token 序列

DALL-E 2

2022

CLIP + 扩散模型

1024x1024

先验网络 + 解码器

Stable Diffusion

2022

潜空间扩散

512x512

潜空间压缩

DALL-E 3

2023

扩散 + LLM 提示增强

1024x1024

理解复杂提示

guidance_scale 控制生成结果与文本提示的匹配程度,值越大越忠于提示但可能降低多样性,推荐范围 5.0 到 10.0。

扩散模型的推理速度较慢,50 步采样在消费级 GPU 上通常需要数秒,实时应用场景可考虑 LCM(Latent Consistency Model)加速到 4-8 步。

5评估指标(BLEU / CIDEr / SPICE)

图像描述的质量评估是一个复杂问题,因为同一图像可以有多种同样正确的描述。BLEU 最初用于机器翻译,通过计算 n-gram 精确匹配度来评估生成文本,但它过于关注精确匹配,对同义替换不够鲁棒。CIDEr(Consensus-based Image Description Evaluation)是专门为图像描述设计的指标,它使用 TF-IDF 加权的 n-gram 匹配,并且考虑了多个人工标注之间的一致性作为归一化基准,这使得它能够更好地评估描述是否抓住了图像的共识性内容。SPICE 则更进一步,它将描述解析为场景图(Scene Graph),比较主语、谓语和宾语等语义元素,对语义相似但措辞不同的描述更加宽容。实际研究中通常同时报告多个指标以获得全面的评估视角。

python
# 计算 BLEU 和 CIDEr 分数
from pycocoevalcap.bleu.bleu import Bleu
from pycocoevalcap.cider.cider import Cider
from pycocoevalcap.tokenizer.ptbtokenizer import PTBTokenizer

# 准备数据格式
gts = {"img1": [{"caption": "a dog playing in the snow"}]}
res = {"img1": [{"caption": "a dog is playing in white snow"}]}

# Tokenize
tokenizer = PTBTokenizer()
gts = tokenizer.tokenize(gts)
res = tokenizer.tokenize(res)

# BLEU
bleu_scorer = Bleu(4)
bleu, _ = bleu_scorer.compute_score(gts, res)
for i, score in enumerate(bleu, 1):
    print(f"BLEU-{i}: {score:.4f}")

# CIDEr
cider_scorer = Cider()
cider, _ = cider_scorer.compute_score(gts, res)
print(f"CIDEr: {cider:.4f}")
python
# 使用 SPICE 进行语义级评估
from pycocoevalcap.spice.spice import Spice

# SPICE 需要 Java 环境
spice_scorer = Spice()
score, _ = spice_scorer.compute_score(gts, res)
print(f"SPICE: {score:.4f}")

# CLIPScore: 基于 CLIP 的无参考评估
import clip
from PIL import Image
import torch.nn.functional as F

model, preprocess = clip.load("ViT-B/32")
image = preprocess(Image.open("photo.jpg")).unsqueeze(0)
text = clip.tokenize(["a dog playing in the snow"])

with torch.no_grad():
    img_feat = model.encode_image(image)
    txt_feat = model.encode_text(text)
    score = F.cosine_similarity(img_feat, txt_feat)
    print(f"CLIPScore: {score.item():.4f}")
指标评估维度取值范围对同义词敏感度

BLEU-4

n-gram 精确匹配

0-100

METEOR

带同义词的 n-gram

0-1

CIDEr

TF-IDF 加权 n-gram

0-100+

SPICE

场景图语义匹配

0-1

CIDEr 是目前图像描述领域最常用的主指标,优化 CIDEr 通常也能改善其他指标。

BLEU 对词汇变化非常敏感,即使生成描述与参考描述语义完全相同,只要用词不同也会得到很低的分数。

6可控图像生成

可控生成是文本到图像生成的进阶方向,目标是让模型不仅理解文本提示,还能精确控制生成图像的多个属性,如风格、构图、颜色、空间布局等。ControlNet 通过在预训练扩散模型中注入可训练的条件分支,实现了在不破坏原始模型能力的情况下添加额外的控制信号。这些控制信号可以是边缘图(Canny)、深度图、人体姿态关键点(OpenPose)或分割掩码等。Adapter 方法则更加轻量,它在模型的各个层之间插入小型适配模块,通过微调这些适配模块来实现条件控制,而不需要修改原始模型的权重。Prompt 工程也是可控生成的重要手段,通过精心设计文本提示词,可以引导模型生成特定风格、构图和内容的图像。负面提示词(Negative Prompt)则允许用户明确指定不希望在生成结果中出现的内容。

python
# 使用 ControlNet 进行姿态控制生成
from diffusers import (
    StableDiffusionControlNetPipeline,
    ControlNetModel,
    UniPCMultistepScheduler,
)
from controlnet_aux import OpenposeDetector
from PIL import Image
import torch

# 加载 ControlNet
controlnet = ControlNetModel.from_pretrained(
    "lllyasviel/control_v11p_sd15_openpose",
    torch_dtype=torch.float16,
)
pipe = StableDiffusionControlNetPipeline.from_pretrained(
    "runwayml/stable-diffusion-v1-5",
    controlnet=controlnet,
    torch_dtype=torch.float16,
)
pipe.scheduler = UniPCMultistepScheduler.from_config(pipe.scheduler.config)
pipe = pipe.to("cuda")

# 提取姿态并生成图像
pose_detector = OpenposeDetector.from_pretrained("lllyasviel/ControlNet")
input_image = Image.open("person.jpg")
pose = pose_detector(input_image)
image = pipe(
    "a beautiful portrait, 4k quality",
    image=pose,
    num_inference_steps=20,
).images[0]
python
# 提示词工程:构建高质量提示
class PromptBuilder:
    def __init__(self):
        self.quality_terms = [
            "masterpiece", "best quality", "highres", "detailed"
        ]
        self.style_terms = {
            "photo": "photorealistic, natural lighting, 85mm lens",
            "anime": "anime style, cel shading, vibrant colors",
            "painting": "oil painting, impressionist style, canvas texture",
        }
        self.negative_default = (
            "worst quality, low quality, blurry, deformed, "
            "bad anatomy, extra limbs, watermark"
        )

    def build(self, subject, style="photo"):
        parts = self.quality_terms.copy()
        parts.append(self.style_terms.get(style, ""))
        parts.append(subject)
        return ", ".join(parts), self.negative_default

builder = PromptBuilder()
pos, neg = builder.build("a cat sitting on a windowsill", "photo")
print(f"Positive: {pos}")
print(f"Negative: {neg}")
控制方法控制信号训练开销控制精度

ControlNet

边缘/深度/姿态

中等

T2I-Adapter

边缘/深度/草图

中高

IP-Adapter

参考图像

Regional Prompt

文本区域分割

无训练

使用 ControlNet 时,建议先将 guidance_scale 设为 7.0 左右,controlnet_conditioning_scale 设为 1.0,然后根据效果微调。

ControlNet 的模型版本需要与基础扩散模型版本匹配,混用不同版本(如 SD 1.5 的 ControlNet 与 SDXL 的基础模型)会导致结果异常。

7Transformers + Diffusers 实战

Hugging Face 的 Transformers 和 Diffusers 库为多模态模型提供了统一的接口和丰富的预训练模型。Transformers 库包含了 CLIP、BLIP、Flamingo 等视觉语言模型的实现,而 Diffusers 库则提供了 Stable Diffusion、ControlNet、IP-Adapter 等生成模型的推理和训练接口。两个库共享相同的模型配置和权重管理方式,使得构建端到端的多模态流水线变得非常便捷。BLIP-2 是一个代表性的视觉语言模型,它通过 Q-Former 模块在冻结的图像编码器和语言模型之间建立桥梁,仅训练少量参数即可实现零样本的图像理解和生成能力。在实际工程中,合理使用梯度检查点(Gradient Checkpointing)、混合精度训练(Mixed Precision)和 LoRA 等优化技术,可以在有限的计算资源下训练高质量的多模态模型。

python
# 使用 BLIP-2 进行零样本图像描述
from transformers import Blip2Processor, Blip2ForConditionalGeneration
from PIL import Image
import torch

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("scene.jpg")
inputs = processor(images=image, return_tensors="pt").to("cuda", torch.float16)
generated_ids = model.generate(**inputs, max_new_tokens=50)
caption = processor.batch_decode(generated_ids, skip_special_tokens=True)
print(f"Generated: {caption[0].strip()}")

# 带提示的问题生成
question = "What color is the sky?"
inputs_qa = processor(
    images=image, text=question, return_tensors="pt"
).to("cuda", torch.float16)
out = model.generate(**inputs_qa, max_new_tokens=20)
answer = processor.batch_decode(out, skip_special_tokens=True)
print(f"Q: {question}  A: {answer[0].strip()}")
python
# 使用 LoRA 微调 Stable Diffusion
from diffusers import StableDiffusionPipeline, UNet2DConditionModel
from peft import LoraConfig, get_peft_model

# 加载基础模型
pipe = StableDiffusionPipeline.from_pretrained(
    "runwayml/stable-diffusion-v1-5"
)
unet = pipe.unet

# 配置 LoRA
lora_config = LoraConfig(
    r=16,
    lora_alpha=32,
    target_modules=["to_q", "to_k", "to_v", "to_out.0"],
    lora_dropout=0.1,
    bias="none",
)
lora_unet = get_peft_model(unet, lora_config)
lora_unet.print_trainable_parameters()
# trainable params: 8,838,144 || all params: 859,520,964
# trainable%: 1.03%

# 训练循环中使用梯度检查点节省显存
unet.enable_gradient_checkpointing()
pipe.enable_xformers_memory_efficient_attention()

# 保存 LoRA 权重
lora_unet.save_pretrained("lora-sd-v1", safe_serialization=True)
技术显存节省速度影响适用场景

梯度检查点

40-60%

慢 20%

大模型训练

混合精度 (FP16)

50%

快 1.5-3x

推理/训练

LoRA

仅 1-5% 可训练

无影响

高效微调

xFormers 注意力

30%

快 1.3x

长序列训练

微调时优先尝试 LoRA,它只需要训练原始模型 1-5% 的参数,效果接近全参数微调且显存开销大幅降低。

BLIP-2 的 opt-2.7b 版本需要约 6GB 显存进行推理(FP16),opt-6.7b 版本需要约 14GB,请根据硬件条件选择合适大小的模型。

继续你的 AI 学习之旅

浏览更多 AI 知识库文章,或者探索 GitHub 上的优质 AI 项目