MODULE 04 · 多模态

多模态应用:让模型看懂也会创造

多模态模型能同时处理图文、语音、视频等信号。本模块区分「理解」与「生成」,通过 DeepSeek Janus-Pro、Show-o 等开源代表理解统一多模态框架的演进,并用 BLIP、LLaVA、CLIP 写出可运行的看图说话、视觉问答与图文检索代码。

13 节 约 95 分钟 更新于 2026-07

理解与生成

多模态能力可粗分为两类:理解型(VLM,视觉语言模型,看图问答、OCR、图表解析)与生成型(文生图、文生视频、图像编辑)。早期两件事由两套独立模型完成。

近年的趋势是统一架构:用一个模型同时承担理解与生成,共享同一套表征与解码器,显著降低部署与维护成本。

两类任务对视觉表征的诉求截然不同,这也是后面架构设计分歧的根源:

  • 理解任务:需要语义级抽象。一张猫的照片被编码后,模型只需知道「这是一只橘猫趴在键盘上」,像素细节可以丢弃。典型编码器是 CLIP ViT、SigLIP。
  • 生成任务:需要像素级细节。模型必须能从表征还原出毛发纹理与光影,因此常用 VQ Tokenizer(把图像离散化为码本 token)或扩散模型的潜空间。
  • 统一模型:要么让两种表征共存(Janus 路线),要么用一种建模范式硬扛两类任务(Show-o 路线)。

Janus-Pro 统一框架

DeepSeek 的 Janus-Pro 用「解耦视觉编码」的思路:理解任务和生成任务分别走不同的视觉编码器,再汇入同一个语言模型主干。这样理解和生成互不干扰,又共享参数。

具体来说,理解侧用 SigLIP 编码器抽取语义特征,经一个两层 MLP 适配器投影到语言模型的隐空间;生成侧用 VQ Tokenizer 把图像变成离散 token 序列,由语言模型自回归地逐 token 预测。训练分三阶段:先只训适配器对齐、再联合预训练、最后指令微调。

为什么解耦:理解需要语义丰富的表征,生成需要像素级细节。强行共用一个编码器往往是「两头不讨好」,解耦是 Janus 系列的关键设计。

Janus-Pro 的整体架构可概括为「双编码器 + 统一主干 + 双输出」:

Stage 1 适配器 Stage 2 统一预训练 Stage 3 监督微调 理解编码器 SigLIP ViT-L/14 提取语义级视觉特征 生成编码器 VQ Tokenizer 图像离散化为码本 token MLP 适配器(两层) 文本分词器 + Embedding 统一 Transformer DeepSeek-LLM 主干 / 因果注意力 / 共享参数 理解与生成共享同一份权重,经分离 head 输出 文本输出 自回归逐 token 生成回答 图像输出 自回归解码 token → 像素 图像 credit:基于 Janus-Pro 论文 Figure 2 重绘

训练分三阶段推进:Stage 1 只训练 MLP 适配器,冻结编码器和主干,让视觉特征学会「对齐」语言模型的隐空间;Stage 2 解冻全部参数,用大规模图文对做联合预训练,此时理解与生成的 head 共同优化;Stage 3 在少量高质量指令数据上做监督微调,使模型学会遵循对话格式。

Show-o 自回归

Show-o 走另一条路:用自回归 + 离散扩散的混合建模,对文本用自回归、对图像用离散去噪,统一在一个 Transformer 里完成理解与生成。它展示了「一个模型多种生成范式」的可行性。

这类 unified model 的价值在于:推理时可以复用同一份权重,工程上更简洁,也更容易做多模态的联合推理。

多模态架构图解

无论 BLIP、LLaVA 还是 Janus,典型 VLM 的骨架都可以抽象为三块:图像编码器把像素变成视觉 token,文本编码器 / 分词器把文字变成文本 token,融合与对齐层把两路 token 投影到同一隐空间后交给语言模型主干。下图是这条数据通路的全景:

图像输入 224x224 / 336x336 像素 文本输入 问题 / 指令 / 描述 图像编码器 ViT / SigLIP / CLIP patch 切块后输出视觉 token 文本编码器 Tokenizer + Embedding 输出文本 token 向量 融合 / 对齐层 MLP 投影(LLaVA)/ Q-Former(BLIP-2)/ 对比学习(CLIP) 语言模型主干(自回归生成回答)

三种主流对齐方式的取舍:MLP 投影(LLaVA)最简单,视觉 token 数量多但训练成本低;Q-Former(BLIP-2)用一组可学习查询向量把几百个视觉 token 压缩到 32 个,节省上下文;对比学习(CLIP)不做生成,只把图文拉进同一嵌入空间,是检索场景的基石。

VLM 应用

  • 文档智能:扫描件、表格、票据的结构化抽取。
  • 具身与界面理解:截图转操作、UI 自动化。
  • 内容审核:图文一致性、违规识别。
  • 多模态 RAG:把图片也纳入检索与问答。

选型时的经验法则:只要「描述一张图」用 BLIP 这类轻量模型即可;需要多轮对话、复杂推理(读图表、解截图)时上 LLaVA、Qwen-VL 级别的大参数 VLM;只做「找图」不做「说话」时,CLIP 这类双塔模型速度快一个量级。

VLM 实战:看图说话与 VQA

先装依赖:pip install transformers torch pillow accelerate。第一个例子用 BLIP 做 image captioning(看图说话)。注意流程固定为三步:processor 做图像预处理(缩放、归一化、转 tensor),model.generate 解码,processor.decode 还原文本。

# blip_caption.py 用 BLIP 为单张图片生成英文描述
import torch
from PIL import Image
from transformers import BlipProcessor, BlipForConditionalGeneration

device = "cuda" if torch.cuda.is_available() else "cpu"
model_id = "Salesforce/blip-image-captioning-base"

# processor 负责图像预处理:resize 到 384x384、归一化、转成 pixel_values
processor = BlipProcessor.from_pretrained(model_id)
model = BlipForConditionalGeneration.from_pretrained(model_id).to(device)

image = Image.open("demo.jpg").convert("RGB")
inputs = processor(images=image, return_tensors="pt").to(device)

# beam search 生成,最多 30 个新 token
out = model.generate(**inputs, max_new_tokens=30, num_beams=3)
caption = processor.decode(out[0], skip_special_tokens=True)
print("Caption:", caption)

第二个例子是 VQA(视觉问答):换成 BlipForQuestionAnswering,processor 同时吃图像和问题文本。

# blip_vqa.py 对图片提问,模型给出简短答案
from transformers import BlipProcessor, BlipForQuestionAnswering

vqa_id = "Salesforce/blip-vqa-base"
processor = BlipProcessor.from_pretrained(vqa_id)
model = BlipForQuestionAnswering.from_pretrained(vqa_id).to(device)

question = "how many people are in the picture?"
# 图像与问题一起编码:pixel_values + input_ids
inputs = processor(images=image, text=question, return_tensors="pt").to(device)
out = model.generate(**inputs, max_new_tokens=10)
print("Answer:", processor.decode(out[0], skip_special_tokens=True))

需要中文、多轮对话与复杂推理时,换 LLaVA。它是「CLIP 视觉编码器 + MLP 投影 + Vicuna/Llama 主干」的经典结构,走 chat template 接口:

# llava_chat.py 用 LLaVA-1.5-7B 做开放式图像对话(约需 16GB 显存,fp16)
import torch
from transformers import AutoProcessor, LlavaForConditionalGeneration

model_id = "llava-hf/llava-1.5-7b-hf"
processor = AutoProcessor.from_pretrained(model_id)
model = LlavaForConditionalGeneration.from_pretrained(
    model_id, torch_dtype=torch.float16, device_map="auto"
)

# 对话消息里用 type=image 占位,真实图像在 processor 调用时传入
conversation = [
    {"role": "user", "content": [
        {"type": "image"},
        {"type": "text", "text": "详细描述这张图片,并指出其中的文字内容。"},
    ]},
]
prompt = processor.apply_chat_template(conversation, add_generation_prompt=True)
inputs = processor(images=image, text=prompt, return_tensors="pt").to(model.device)

out = model.generate(**inputs, max_new_tokens=256, do_sample=False)
# 只解码新生成的部分,跳过输入 prompt
answer = processor.decode(out[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True)
print(answer)
显存不够怎么办:加 load_in_4bit=True(需 bitsandbytes)可把 LLaVA-7B 压到约 5GB;或直接用 BLIP 系列,base 版 CPU 也能跑,只是慢。

BLIP-2 实战:视觉问答(VQA)

BLIP 虽轻量但只能看图说话和简单 VQA,无法做多轮对话。升级方案是 BLIP-2LLaVA。BLIP-2 用 Q-Former 把视觉 token 压缩后喂给冻结的语言模型,推理成本远低于全参数 VLM;LLaVA 则把 CLIP 编码通过 MLP 直接投影到大语言模型,适合需要长回答、多轮对话的场景。

先装依赖:pip install transformers torch pillow accelerate。下面的 BLIP-2 代码做视觉问答:加载一张图片,提问,模型返回简短答案。注意 processor 同时吃图片和问题文本,model.generate 解码后 decode 还原答案:

# blip2_vqa.py 用 BLIP-2 做视觉问答(VQA)
import torch
from PIL import Image
from transformers import Blip2Processor, Blip2ForConditionalGeneration

device = "cuda" if torch.cuda.is_available() else "cpu"
model_id = "Salesforce/blip2-opt-2.7b"

# processor 负责图像预处理 + 文本分词,一步完成
processor = Blip2Processor.from_pretrained(model_id)
# 用 bfloat16 降低显存,约需 6GB
model = Blip2ForConditionalGeneration.from_pretrained(
    model_id, torch_dtype=torch.bfloat16, device_map="auto"
)

# 1. 加载图片
image = Image.open("demo.jpg").convert("RGB")

# 2. 提出问题 -- 答案通常为简短短语或单词
question = "How many people are in this image and what are they doing?"

# 3. 编码(图像 pixel_values + 文本 input_ids)
inputs = processor(images=image, text=question, return_tensors="pt").to(device, torch.bfloat16)

# 4. 生成答案 -- max_new_tokens 控制回答长度
with torch.no_grad():
    generated_ids = model.generate(**inputs, max_new_tokens=50, do_sample=False)

# 5. 解析答案
answer = processor.decode(generated_ids[0], skip_special_tokens=True)
print(f"Q: {question}")
print(f"A: {answer.strip()}")

批量处理多张图片时,可以把图片路径列表循环处理,每张图片独立提问。下面是一个完整批处理示例,同时演示中英文切换:

# blip2_batch_vqa.py 批量 VQA:遍历图库对每张图片回答 3 个问题
import torch
from pathlib import Path
from PIL import Image
from transformers import Blip2Processor, Blip2ForConditionalGeneration

device = "cuda" if torch.cuda.is_available() else "cpu"
model_id = "Salesforce/blip2-opt-2.7b"
processor = Blip2Processor.from_pretrained(model_id)
model = Blip2ForConditionalGeneration.from_pretrained(
    model_id, torch_dtype=torch.bfloat16, device_map="auto"
)

# 问题列表:前两个英文、第三个中文(BLIP-2 对英文更准)
questions = [
    "What objects are in this image?",
    "Is this photo taken indoors or outdoors?",
    "What is the dominant color in this picture?",
]

# 遍历图库
for img_path in sorted(Path("gallery").glob("*"))[:10]:
    if img_path.suffix.lower() not in (".jpg", ".png", ".jpeg"):
        continue
    image = Image.open(img_path).convert("RGB")
    print(f"\n--- {img_path.name} ---")
    for q in questions:
        inputs = processor(images=image, text=q, return_tensors="pt").to(device, torch.bfloat16)
        with torch.no_grad():
            ids = model.generate(**inputs, max_new_tokens=30, do_sample=False)
        ans = processor.decode(ids[0], skip_special_tokens=True)
        print(f"  Q: {q}\n  A: {ans.strip()}")
LLaVA vs BLIP-2 选型建议:简单 VQA(答案是单词或短句)用 BLIP-2 更快更省显存;需要多轮对话、推理、中文流畅回答时上 LLaVA-1.5 或 Qwen-VL。BLIP-2 的 Q-Former 压缩 token 数量后延迟更低,适合批量离线处理。

CLIP 图文检索实战

CLIP 是双塔结构:图像塔与文本塔各自输出一个向量,训练目标是让配对图文的余弦相似度最大、非配对最小。推理时不做生成,只做「编码 + 算相似度」,因此可以离线预编码整个图库,检索是毫秒级的。

下图是图文检索的端到端数据流 -- 每一步的输入输出形状已标注,注意所有向量最终都会做 L2 归一化:

图片 224x224x3 CLIP 图像编码器 ViT-B/32 · 12 层 图像向量 1 x 512-dim 查询文本 N tokens CLIP 文本编码器 12 层 Transformer 文本向量 1 x 512-dim 余弦相似度 sim = I·T / (|I|·|T|) 归一化后等价于点积 TopK 检索结果

在实际部署中,图像向量可以离线预计算并存入向量数据库(如 FAISS),查询时只需运行文本编码器即可毫秒级返回结果。下图从工程视角拆解离线与在线两条路径的分工:

离线:图库预编码 N 张图片 gallery/*.jpg 图像塔编码 N x 512 矩阵,L2 归一化 在线:文本查询 查询文本 a photo of a cat 文本塔编码 1 x 512 向量,L2 归一化 余弦相似度 + TopK sims = text_emb @ img_emb.T

下面的代码先批量编码一个文件夹的图片,再用一句话检索出最相关的 3 张。核心细节是先 L2 归一化,再做矩阵乘法,此时点积就等于余弦相似度:

# clip_search.py 用 CLIP 做文搜图:编码图库、余弦相似度、TopK 检索
import torch
from pathlib import Path
from PIL import Image
from transformers import CLIPModel, CLIPProcessor

device = "cuda" if torch.cuda.is_available() else "cpu"
model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32").to(device).eval()
processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")

# 1. 批量编码图库(离线只需做一次,结果可存成 .pt 文件)
paths = sorted(Path("gallery").glob("*.jpg"))
images = [Image.open(p).convert("RGB") for p in paths]

with torch.no_grad():
    img_inputs = processor(images=images, return_tensors="pt").to(device)
    img_emb = model.get_image_features(**img_inputs)          # [N, 512]
    img_emb = img_emb / img_emb.norm(dim=-1, keepdim=True)  # L2 归一化

# 2. 编码查询文本(CLIP 对英文模板更敏感,建议加 a photo of 前缀)
query = "a photo of a cat sleeping on a keyboard"
with torch.no_grad():
    txt_inputs = processor(text=[query], return_tensors="pt", padding=True).to(device)
    txt_emb = model.get_text_features(**txt_inputs)
    txt_emb = txt_emb / txt_emb.norm(dim=-1, keepdim=True)

# 3. 归一化后点积即余弦相似度,取 TopK
sims = (txt_emb @ img_emb.T).squeeze(0)                     # [N]
topk = sims.topk(k=min(3, len(paths)))
for score, idx in zip(topk.values.tolist(), topk.indices.tolist()):
    print(f"{score:.4f}  {paths[idx].name}")

反过来「图搜文」也是同一套逻辑:把候选标签列表送进文本塔,与单张图片的向量算相似度,就得到零样本分类器。这正是 CLIP 论文标题里 zero-shot 的含义:

# clip_zeroshot.py 用 CLIP 做零样本图像分类
labels = ["cat", "dog", "bird", "car"]
prompts = [f"a photo of a {l}" for l in labels]

with torch.no_grad():
    inputs = processor(text=prompts, images=Image.open("demo.jpg"),
                       return_tensors="pt", padding=True).to(device)
    outputs = model(**inputs)
    # logits_per_image 已含温度系数缩放,softmax 得到分类概率
    probs = outputs.logits_per_image.softmax(dim=-1).squeeze(0)

for label, p in zip(labels, probs.tolist()):
    print(f"{label}: {p:.2%}")
规模化提示:图库超过 10 万张时,把归一化后的向量存入向量数据库(FAISS、Milvus)做近似最近邻检索。中文场景可换 Chinese-CLIP(OFA-Sys/chinese-clip-vit-base-patch16),接口完全一致。

多模态 RAG 实战

前面的章节独立介绍了 CLIP 检索和 VLM 问答。把它们串起来就构成了一个多模态 RAG 系统:用户提问 → CLIP 检索相关图片 → 把图片传给 VLM 做看图回答。下面是一个可运行的端到端脚本,先装依赖:pip install transformers torch pillow accelerate

完整流程分四步:① 用 CLIP 对图库建立向量索引;② 接收用户查询文本,检索最相关图片;③ 把检索结果图片传给 LLaVA(或 BLIP-2)做看图问答;④ 返回回答。核心代码:

# multimodal_rag.py 多模态 RAG:CLIP 检索 + LLaVA 看图问答,端到端流水线
import torch
from pathlib import Path
from PIL import Image
from transformers import (
    CLIPModel, CLIPProcessor,
    Blip2Processor, Blip2ForConditionalGeneration,
)

device = "cuda" if torch.cuda.is_available() else "cpu"

# ====== 1. 初始化模型 ======
clip_model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32").to(device).eval()
clip_processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")

# VLM 选 BLIP-2(省显存)或 LLaVA(更强推理)
vlm_id = "Salesforce/blip2-opt-2.7b"
vlm_processor = Blip2Processor.from_pretrained(vlm_id)
vlm_model = Blip2ForConditionalGeneration.from_pretrained(
    vlm_id, torch_dtype=torch.bfloat16, device_map="auto"
)

# ====== 2. 建立图库向量索引 ======
image_dir = Path("gallery")
paths = sorted([p for p in image_dir.glob("*") if p.suffix.lower() in (".jpg", ".png")])

def build_index(paths):
    """遍历图库,CLIP 编码后 L2 归一化,返回向量矩阵和路径列表"""
    images = [Image.open(p).convert("RGB") for p in paths]
    with torch.no_grad():
        inputs = clip_processor(images=images, return_tensors="pt").to(device)
        emb = clip_model.get_image_features(**inputs)
        emb = emb / emb.norm(dim=-1, keepdim=True)
    return emb, paths

print(f"索引 {len(paths)} 张图片...")
img_emb, img_paths = build_index(paths)

# ====== 3. 检索相关图片 ======
def retrieve(query, top_k=3):
    """文本查询检索 TopK 最相关图片"""
    with torch.no_grad():
        txt_in = clip_processor(text=[query], return_tensors="pt", padding=True).to(device)
        txt_emb = clip_model.get_text_features(**txt_in)
        txt_emb = txt_emb / txt_emb.norm(dim=-1, keepdim=True)
    sims = (txt_emb @ img_emb.T).squeeze(0)
    topk = sims.topk(k=min(top_k, len(img_paths)))
    return [(img_paths[i], s.item()) for s, i in zip(topk.values, topk.indices)]

# ====== 4. 看图问答 ======
def answer_with_image(image_path, question):
    """把检索到的图片传给 VLM,结合问题生成回答"""
    image = Image.open(image_path).convert("RGB")
    with torch.no_grad():
        inputs = vlm_processor(images=image, text=question, return_tensors="pt").to(device, torch.bfloat16)
        ids = vlm_model.generate(**inputs, max_new_tokens=50, do_sample=False)
    return vlm_processor.decode(ids[0], skip_special_tokens=True).strip()

# ====== 5. 端到端 RAG ======
def rag_query(user_question):
    """用户提问 → 检索图片 → 看图回答"""
    results = retrieve(user_question, top_k=1)
    if not results:
        return "未找到相关图片。"
    best_path, score = results[0]
    print(f"[检索] 最相关图片: {best_path.name} (相似度 {score:.4f})")
    prompt = f"Question: {user_question}\nBased on the image, answer:"
    return answer_with_image(best_path, prompt)

# 交互式查询循环
print("\n多模态 RAG 就绪。输入问题,输入 quit 退出。")
while True:
    q = input("\n>>> ").strip()
    if q.lower() in ("quit", "exit", "q"):
        break
    print(f"回答: {rag_query(q)}")
升级建议:① 换 LLaVA 代替 BLIP-2 做 VQA,回答更精准但显存翻倍;② 用 FAISS 替代暴力检索,十亿级图库毫秒响应;③ 把向量索引缓存为 .pt 文件避免每次启动重编码;④ 引入 reranker(ColPali 等)对 TopK 结果二次排序,提升召回精度。

训练数据

多模态模型的瓶颈常在配对数据:图文对、指令-图像对、思维链标注。构建时常用「现有数据集 + 合成标注(用强模型给弱任务造样本)」的组合,并注意去重与偏见控制。

常见公开数据源:LAION-5B(网络爬取图文对,量大但噪声高)、COCO Captions(人工标注,质量高但只有 12 万图)、LLaVA-Instruct(用 GPT-4 从 COCO 标注合成的指令对话)。一个实用做法是先用 CLIP 分数过滤爬取数据,把图文相似度低于阈值的样本直接丢弃。

若你想把这类能力接到可执行的流程里,建议接着看 AI Agent 编排 模块。

动手练习

以下练习按难度递进,每个都有明确的验收标准。建议在本地准备 20 到 50 张自己的照片作为素材。

练习一:批量看图说话。基于上文 blip_caption.py,写一个脚本遍历 gallery/ 目录下所有 jpg/png 图片,为每张生成英文描述,结果写入 captions.json(键为文件名,值为描述)。要求用 torch.no_grad() 包裹推理,并按每批 8 张做 batch 处理而不是逐张循环。

  • 验收标准:30 张图片全部生成非空描述;batch 版本比逐张版本快 2 倍以上(用 time.time 计时对比);JSON 文件可被 json.load 正常读回。

练习二:搭一个可检索图库。结合练习一与 clip_search.py:先用 CLIP 把整个图库编码并保存为 embeddings.pt(含向量矩阵与文件名列表),再写一个 search(query, k) 函数,输入中文或英文描述、返回 TopK 文件名与相似度分数。加一个简单的命令行循环让用户连续输入查询。

  • 验收标准:图库编码只在首次运行时执行,之后直接加载 .pt 文件(二次启动应在 3 秒内可查询);用「戴帽子的人」「食物特写」等 5 个不同查询各检索一次,Top1 结果人工判断至少 4 次相关;相似度分数按降序输出。

练习三:VQA 质检器。用 BLIP-VQA 对练习一生成的每条 caption 做交叉验证:把 caption 改写成一个 yes/no 问题(如 caption 是 a dog on the beach,就问 is there a dog in the picture?),若模型回答 no 则把该样本标记为可疑。输出一份 suspect.txt 列出所有可疑文件名。

  • 验收标准:脚本能对全部图片跑完不报错;人为混入 3 张与 caption 无关的图片后,至少 2 张被标记为可疑;正常样本误报率低于 20%。

完成三个练习后,你就拥有了一条「生成描述、建索引、质检」的完整多模态数据流水线,这正是多模态 RAG 系统的雏形。

练习四:搭一个多模态客服 Demo。基于上方 multimodal_rag.py,把图库限定为 5 张自己准备的产品图(护肤品、电子产品、服饰等任选),然后编写 3 个客户问题,每条问题通过 CLIP 检索最匹配的产品图,再利用 BLIP-2 或 LLaVA 对图片做视觉问答,把检索路径 + 回答打印出来。

要求实现一个 customer_service(query) 函数,输入客户自然语言问题,内部依次完成检索与看图问答,最终返回字符串答案。同时统计检索命中次数与回答质量。

  • 产品图准备:5 张 jpg/png 图片,放在 products/ 目录下,每张图片包含一个可辨认的产品主体。建议选实物拍照而非网图,以模拟真实场景。
  • 客户问题设计:至少 3 个问题,覆盖不同产品类别。例如:「充电宝是哪款?」「有没有美白精华?」「哪件衣服是纯棉的?」。问题中必须包含产品相关关键词。
  • 验收标准:3 个问题中至少 2 个的 CLIP 检索 Top1 确实是正确产品图(人工判断);BLIP-2 对正确图片的回答与图片内容一致(不可答非所问);customer_service() 函数返回结果中包含检索文件名和模型回答。

这个练习本质上就是多模态 RAG 的最小化产品级实现:前端接收到用户自然语言后,后端自动检索产品图库并结合 VLM 做视觉确认,返回可阅读的客服回复。

已复制。