0 前言
信息的载体,除了文字,还有图像、视频、声音等,另外,人类还可以根据触觉捕捉到信息,动植物也能通过温度、湿度、磁场来捕捉外界的环境变化,这些都是不同的信息模态,而所谓多模态模型,指的是能处理多种信息载体的模型。当前,多模态模型的使用已经成为了 AI 开发工程师的一项重要能力,但限于技术条件,目前只有文字、图像、视频、声音等模态的信息有比较成熟的模型,我们需要掌握文生图/视频、图/视频生文(视觉问答)、图生图等模型的原理。
阅读本文之前,需要读者具备 Transformer、CNN(如ResNet、VGG等)的基础。
1 ViT
ViT 诞生于2020年,由 Google 团队提出,它是将 Transformer 结构应用在图像分类任务上,虽然之前也有其他团队开发了基于 Transformer 的视觉模型,但是 ViT 因为其模型“简单”且效果好,可扩展性强(模型越大效果越好),成为了 Transformer 在CV领域应用的里程碑著作,也引爆了后续相关研究。
1.1 模型结构
模型结构如下图所示:
1.2 核心思想
ViT(Vision Transformer)的核心创新是将图像视为序列数据处理,而非传统的二维网格数据。具体步骤包括:
- 图像分块:将输入图像(如224×224像素)划分为固定大小的非重叠小块(如16×16像素),得到多个图像块(Patches)。
- 线性嵌入:每个块展平为向量后,通过线性投影映射到固定维度(如768维),形成类似NLP中的"词嵌入"序列。
- 位置编码:为每个块添加可学习的位置嵌入,保留空间信息(因Transformer本身无法感知顺序)。
- Class Token:在序列开头添加一个可学习的分类标记(类似BERT的[CLS]),最终输出用于分类任务。
1.3 代码
和 ResNet 类似,ViT 也是有多种变体的,典型变体如下:
- ViT-B/16、ViT-B/32:基础版(Base),参数量约86M,Patch尺寸分别为16×16和32×32。
- ViT-L/16、ViT-L/32:大型版(Large),参数量约307M,适合高精度任务。
- ViT-H/14:超大型版(Huge),参数量632M,Patch尺寸14×14,需极大数据和算力支持
接下来我们实现一下ViT-B/16。
先把需要的包导入进来:
import torch
import torch.nn as nn
from einops import rearrange
然后是图像块嵌入层(PatchEmbedding):
class PatchEmbedding(nn.Module):
"""图像分块嵌入模块(核心组件)"""
def __init__(self, img_size=224, patch_size=16, in_chans=3, embed_dim=768):
super().__init__()
self.img_size = (img_size, img_size)
self.patch_size = (patch_size, patch_size)
self.num_patches = (self.img_size[0] // self.patch_size[0]) * (self.img_size[1] // self.patch_size[1])
# 使用卷积层实现高效分块,并通过输出通道数来调整嵌入维度,等效于线性投影
self.proj = nn.Conv2d(
in_chans,
embed_dim, # 输出通道数与嵌入维度一致
kernel_size=patch_size, # 卷积核的大小为 patch_size
stride=patch_size # 卷积核的步长也为 patch_size
)
def forward(self, x):
# [batch, channels, H, W] -> [batch, embed_dim, num_patches_h, num_patches_w]
x = self.proj(x)
# 展平并转置维度: [batch, embed_dim, num_patches_h, num_patches_w] -> [batch, N, embed_dim],N是 patch 的数量
x = rearrange(x, 'b c h w -> b (h w) c')
return x
下面是 ViT 块,其实就是 Transformer 的编码器子层,熟悉 Transformer 结构的同学一看就懂:
class ViTBlock(nn.Module):
"""Transformer编码器基础模块(ViT核心单元)"""
def __init__(self, dim=768, num_heads=12, mlp_ratio=4.0, dropout=0.1):
super().__init__()
self.norm1 = nn.LayerNorm(dim)
self.attn = nn.MultiheadAttention(dim, num_heads, dropout=dropout, batch_first=True)
self.norm2 = nn.LayerNorm(dim)
self.mlp = nn.Sequential(
nn.Linear(dim, int(dim * mlp_ratio)),
nn.GELU(),
nn.Dropout(dropout),
nn.Linear(int(dim * mlp_ratio), dim),
nn.Dropout(dropout)
)
def forward(self, x):
# 残差连接 + 层归一化 + 多头注意力
attn_output, _ = self.attn(self.norm1(x), self.norm1(x), self.norm1(x))
x = x + attn_output
# 残差连接 + 层归一化 + MLP
x = x + self.mlp(self.norm2(x))
return x
最后是堆叠 ViTBlock,并把 PatchEmbedding 层加入进来,于此同时,实现 Class Token,代码如下:
class ViT_B(nn.Module):
def __init__(self,
img_size=224,
num_classes=1000,
in_chans=3,
patch_size=16,
embed_dim=768,
depth=12,
num_heads=12,
mlp_ratio=4.0,
dropout=0.1,
emb_dropout=0.1):
super().__init__()
# 1. 分块嵌入
self.patch_embed = PatchEmbedding(
img_size=img_size,
patch_size=patch_size,
in_chans=in_chans,
embed_dim=embed_dim
)
num_patches = self.patch_embed.num_patches # 获取图像块的数量
# 2. 分类token和位置编码
self.cls_token = nn.Parameter(torch.zeros(1, 1, embed_dim))
self.pos_embed = nn.Parameter(torch.zeros(1, num_patches + 1, embed_dim)) # num_patches + 1,加1表示有一个分类token
self.pos_drop = nn.Dropout(emb_dropout)
# 3. Transformer编码器堆叠
self.blocks = nn.Sequential(*[
ViTBlock(
dim=embed_dim,
num_heads=num_heads,
mlp_ratio=mlp_ratio,
dropout=dropout
) for _ in range(depth)
])
# 4. 分类头
self.norm = nn.LayerNorm(embed_dim)
self.head = nn.Linear(embed_dim, num_classes)
# 初始化权重
nn.init.trunc_normal_(self.cls_token, std=0.02)
nn.init.trunc_normal_(self.pos_embed, std=0.02)
def forward(self, x):
# 分块嵌入 [B, C, H, W] -> [B, num_patches, embed_dim]
x = self.patch_embed(x)
# 添加分类token
cls_tokens = self.cls_token.expand(x.shape[0], -1, -1) # 将分类token按照 batch_size 扩展
x = torch.cat((cls_tokens, x), dim=1)
# 添加位置编码
x = self.pos_drop(x + self.pos_embed)
# 通过Transformer编码器
x = self.blocks(x)
# 取分类token对应的输出
x = self.norm(x)
cls_token_final = x[:, 0]
# 将分类token对应的输出,输入到分类头中,获取图片的特征并返回
return self.head(cls_token_final)
最后是输出测试用例:
# 实例化ViT-B/16模型(关键参数)
model = ViT_B(
img_size=224, # 输入图像尺寸
num_classes=1000, # 分类类别数
patch_size=16, # 图像块的尺寸
embed_dim=768, # 特征维度(ViT-B标准)
depth=12, # Transformer层数
num_heads=12, # 注意力头数
mlp_ratio=4.0, # MLP扩展比例
dropout=0.1, # 普通dropout
emb_dropout=0.1 # 嵌入层dropout
)
# 示例输入(batch_size=4, 3通道,224x224图像)
input_tensor = torch.randn(4, 3, 224, 224)
output = model(input_tensor) # 输出形状: [4, 1000]
print(output.shape)
输出:
torch.Size([4, 1000])
2 CLIP
CLIP(Contrastive Language-Image Pre-training)是由OpenAI于2021年提出的多模态预训练模型,通过对比学习将图像与文本映射到同一语义空间,实现跨模态理解。
2.1 对比学习机制
- CLIP通过对比损失函数训练模型:通过图像编码器和文本编码器,将图像和文本嵌入到同一空间中(共享空间),在共享嵌入空间中,若图像和文本描述的是同一事物,则最大化两个特征向量的相似度(余弦相似度),若描述的不是同一事物,则最小化两个向量的相似度。
- 训练数据:使用4亿个互联网图文对(WebImageText数据集),覆盖广泛视觉概念。
2.2 核心原理与架构
- 双编码器架构
- 图像编码器:支持ResNet或Vision Transformer(ViT),提取图像特征向量。
- 文本编码器:基于Transformer,将文本转换为向量,文本特征向量的维度,与图像特征向量的维度一致。
- 两个编码器输出的特征,经L2归一化后计算相似度,确保跨模态可比性。
- Clip 结构如下(左边为训练过程,右边为推理过程):
- 推理时,根据任务设定提示模板,例如图像分类任务,那么提示词模板可以设置成
a photo of a {object}
,可以把 ImageNet 中的1000个类别的名称当成 object 填进去,得到 1000 个条提示词,输入到文本编码器得到1000个向量,然后把图像输入到图像编码器,得到图像特征向量,最后将图像特征向量与1000个文本特征向量比较余弦相似度,最相似的文本向量对应的类别名称,就是这个图像的类别。
2.3 关键技术突破
零样本学习(Zero-Shot Learning)
- 无需微调:直接通过文本描述分类图像。例如,输入“一张狗的照片”,模型计算图像特征与所有类别文本特征的相似度,选择最高匹配类别。
- 灵活适配:分类标签可动态扩展,不受预定义类别限制,可以是100个类别,也可以是1000个类别,甚至更多。
高效的损失函数设计
- 代码:
import torch.nn.functional as F def contrastive_loss(image_emb, text_emb, tau=0.07): # tau 是温度系数 # 归一化嵌入向量 image_emb = F.normalize(image_emb, dim=-1) text_emb = F.normalize(text_emb, dim=-1) # 计算相似度矩阵 logits_per_image = (image_emb @ text_emb.T) / tau logits_per_text = (text_emb @ image_emb.T) / tau # 标签:对角线为正样本 labels = torch.arange(len(image_emb)).to(image_emb.device) # 对称交叉熵损失 loss_image = F.cross_entropy(logits_per_image, labels) # 图像到文本的损失 loss_text = F.cross_entropy(logits_per_text, labels) # 文本到图像的损失 return (loss_image + loss_text) / 2
- 对称损失设计思路:跨模态对齐,同时优化
图像→文本
和文本→图像
的匹配损失,提升对齐效果。 - 温度系数τ:τ值小(如 0.01),相似度差异被放大,则模型更关注困难负样本,τ值大(如 0.5),则差异被 “平滑” 掉了,避免过度自信。初始值设为
0.07
,根据训练动态调整(如随训练步数衰减)。
2.4 简单代码实现
先把需要的包导进来:
import torch
import torch.nn as nn
from torchvision.models import resnet50
from einops import rearrange
接下来是图像编码器,有 ResNet 或 ViT 两种实现方式,ViT 可以使用我们上一节实现的 ViT,这里我们把 ResNet 的编码器实现一下:
from torchvision.models import resnet50
class ResNetEncoder(nn.Module):
""" 改进版ResNet-50(CLIP调整版) """
def __init__(self, output_dim=512):
super().__init__()
base = resnet50()
# 移除原分类头,替换为CLIP的适配结构
self.backbone = nn.Sequential(*list(base.children())[:-2])
self.attnpool = nn.Sequential(
nn.AdaptiveAvgPool2d(1),
nn.Flatten(),
nn.Linear(2048, output_dim)
)
def forward(self, x):
features = self.backbone(x) # [B, 2048, 7, 7]
return self.attnpool(features) # [B, 512]
class ImageEncoder(nn.Module):
def __init__(self, arch='ViT-B/32', output_dim=512):
super().__init__()
if 'ViT' in arch:
self.encoder = ViT_B(
num_classes=output_dim,
patch_size=int(arch.split('/')[-1]),
depth=12,
)
elif 'RN' in arch:
self.encoder = ResNetEncoder(output_dim=output_dim)
else:
raise ValueError(f"Unsupported architecture: {arch}")
def forward(self, x):
features = self.encoder(x)
return features
接下来是文本编码器:
class TextEncoder(nn.Module):
def __init__(self, vocab_size=10000, embed_dim=512):
super().__init__()
self.token_embed = nn.Embedding(vocab_size, embed_dim)
self.pos_embed = nn.Parameter(torch.randn(1, 77, embed_dim))
self.transformer = nn.TransformerEncoder(
nn.TransformerEncoderLayer(d_model=embed_dim, nhead=8),
num_layers=6
)
self.ln = nn.LayerNorm(embed_dim)
def forward(self, x):
# [B, 77] -> [B, 77, 512]
x = self.token_embed(x) + self.pos_embed
x = self.transformer(x)
return self.ln(x[:, 0]) # 取首字符特征
最后是模型整合:
class CLIP(nn.Module):
def __init__(self):
super().__init__()
self.image_encoder = ImageEncoder(arch='ViT-B/32')
self.text_encoder = TextEncoder()
self.logit_scale = nn.Parameter(torch.tensor(2.6592)) # 可学习温度系数
# 特征投影层(对齐维度)
self.image_proj = nn.Linear(512, 512)
self.text_proj = nn.Linear(512, 512)
def encode_image(self, image):
features = self.image_encoder(image)
return F.normalize(self.image_proj(features), dim=-1)
def encode_text(self, text):
features = self.text_encoder(text)
return F.normalize(self.text_proj(features), dim=-1)
def forward(self, image, text):
image_features = self.encode_image(image)
text_features = self.encode_text(text)
# 计算相似度矩阵
logit_scale = self.logit_scale.exp()
logits = logit_scale * image_features @ text_features.t()
return logits
我们写一个测试用例,看看模型是否存在bug:
# 4. 使用示例
if __name__ == "__main__":
device = "cuda" if torch.cuda.is_available() else "cpu"
model = CLIP().to(device)
# 模拟输入(实际需预处理)
image = torch.randn(2, 3, 224, 224).to(device) # 2张图片
text = torch.randint(0, 10000, (3, 77)).to(device) # 3段文本
# 前向计算
logits = model(image, text)
print("图像-文本相似度矩阵:\n", logits.detach().cpu().numpy())
输出:
图像-文本相似度矩阵:
[[-0.7128611 -0.9151645 -0.86004055]
[ 0.04123104 -0.04031402 -0.02805367]]
3 VAE
变分自编码器 VAE(Variational Autoencoder)是一种深度生成模型,结合了自编码器(Autoencoder)和概率图模型的思想,由Kingma和Welling于2013年提出,其核心目标是通过学习数据的潜在分布(latent distribution),实现数据生成、重构和特征提取。与传统自编码器不同,VAE引入概率编码,将输入数据映射为潜在空间的概率分布(如高斯分布),而非确定性向量。
3.1 核心组件
- 编码器(Encoder):将输入数据 x x x 映射到潜在空间,输出分布参数(均值 μ \mu μ 和方差 σ \sigma σ)。
- 解码器(Decoder):从潜在变量 z z z 采样,重构生成数据 x ′ x' x′,目标是逼近原始输入。
- 潜在空间(Latent Space):低维连续空间,用于捕捉数据的隐含特征,通常假设服从标准正态分布 N ( 0 , I ) N(0, I) N(0,I)。
- 模型架构:
图中 ϵ \epsilon ϵ 是为标准高斯噪声,它是随机变量,服从正态分布;无论训练还是推理, z z z 都不是编码器直接生成的,编码器只会生成 μ \mu μ 和 σ \sigma σ,然后通过 z = μ + σ ⊙ ϵ z = \mu + \sigma \odot \epsilon z=μ+σ⊙ϵ 生成 z z z。
3.2 损失函数
VAE(变分自编码器)的损失函数是其核心设计,由两部分组成:重构损失(Reconstruction Loss) 和 KL散度(Kullback-Leibler Divergence)。这两部分共同作用,确保模型既能准确重建输入数据,又能学习到结构化的潜在空间。以下是详细解析:
VAE的损失函数可表示为:
L VAE = L reconstruction + β ⋅ D KL ( q ( z ∣ x ) ∥ p ( z ) ) \mathcal{L}_{\text{VAE}} = \mathcal{L}_{\text{reconstruction}} + \beta \cdot D_{\text{KL}}(q(z|x) \parallel p(z)) LVAE=Lreconstruction+β⋅DKL(q(z∣x)∥p(z))
其中:
- L reconstruction \mathcal{L}_{\text{reconstruction}} Lreconstruction 是重构损失;
- D KL D_{\text{KL}} DKL 是KL散度,衡量两个分布的差异;
- β \beta β 是平衡两部分的超参数(默认 β = 1 \beta=1 β=1)。
(1)重构损失(Reconstruction Loss)
作用:衡量解码器重建输入数据 x x x 的能力,确保生成的 x ^ \hat{x} x^ 与原始数据尽可能接近。
常见形式:
- 均方误差(MSE):适用于连续数据(如图像像素值):
L MSE = 1 n ∑ i = 1 n ( x i − x ^ i ) 2 \mathcal{L}_{\text{MSE}} = \frac{1}{n} \sum_{i=1}^{n} (x_i - \hat{x}_i)^2 LMSE=n1i=1∑n(xi−x^i)2 - 二元交叉熵(BCE):适用于二值数据(如二值化图像):
L BCE = − ∑ i = 1 n [ x i log ( x ^ i ) + ( 1 − x i ) log ( 1 − x ^ i ) ] \mathcal{L}_{\text{BCE}} = -\sum_{i=1}^{n} \left[ x_i \log(\hat{x}_i) + (1 - x_i) \log(1 - \hat{x}_i) \right] LBCE=−i=1∑n[xilog(x^i)+(1−xi)log(1−x^i)]
选择依据:数据特性决定损失类型(连续数据用MSE,二值数据用BCE)。
(2)KL散度(KL Divergence)
作用:正则化潜在空间,强制编码器输出的分布 q ( z ∣ x ) q(z|x) q(z∣x)(通常是高斯分布)接近先验分布 p ( z ) p(z) p(z)(标准正态分布 N ( 0 , I ) \mathcal{N}(0, I) N(0,I)),即让 q ( z ∣ x ) q(z|x) q(z∣x) 往正态分布的方向靠拢。
KL散度用于衡量两个分布的距离,KL散度越小,说明两个分布越接近,数学形式(下式是连续分布的KL散度计算公式,我们不需要知道怎么推出来的):
D KL ( q ( z ∣ x ) ∥ p ( z ) ) = 1 2 ∑ i = 1 d ( μ i 2 + σ i 2 − 1 − log σ i 2 ) D_{\text{KL}}(q(z|x) \parallel p(z)) = \frac{1}{2} \sum_{i=1}^{d} \left( \mu_i^2 + \sigma_i^2 - 1 - \log \sigma_i^2 \right) DKL(q(z∣x)∥p(z))=21i=1∑d(μi2+σi2−1−logσi2)
其中 d d d 是潜在变量维度, μ i \mu_i μi 和 σ i \sigma_i σi 是编码器输出的均值和方差。
效果:
- μ i → 0 \mu_i \to 0 μi→0:潜在分布中心向原点靠拢;
- σ i → 1 \sigma_i \to 1 σi→1:潜在分布形状接近标准高斯(球形)。
(3)两部分损失的平衡
- 重构损失主导:模型更关注重建细节,但潜在空间可能不连续,导致生成样本缺乏多样性(例如插值时生成扭曲图像)。
- KL散度主导:潜在空间更规整,但重建质量下降(生成图像模糊)。
- 超参数 β \beta β 的作用:调整 β > 1 \beta >1 β>1 可增强正则化,强制潜在空间更接近标准正态分布; β < 1 \beta <1 β<1 则侧重重建精度。
(4) 重参数化
重参数化技巧(Reparameterization Trick):
解决采样 z ∼ q ( z ∣ x ) z \sim q(z|x) z∼q(z∣x) 不可导的问题,将随机性转移到外部变量 ϵ ∼ N ( 0 , I ) \epsilon \sim \mathcal{N}(0, I) ϵ∼N(0,I):
z = μ + σ ⊙ ϵ z = \mu + \sigma \odot \epsilon z=μ+σ⊙ϵ
这里的 μ \mu μ 和 σ \sigma σ 是潜空间向量的均值和标准差,通过它们可以确保梯度反向传播。损失计算示例(PyTorch代码):
def loss_function(recon_x, x, mu, logvar): # 重构损失(BCE示例) BCE = F.binary_cross_entropy(recon_x, x.view(-1, 784), reduction='sum') # KL散度(闭合解) KLD = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp()) return BCE + KLD
(5)总结
VAE的损失函数通过重构精度与潜在空间正则化的双目标优化,实现了数据生成与结构化表示的平衡:
- 重构损失 → 逼真还原细节;
- KL散度 → 构造连续、可插值的潜在空间,支撑可控生成。
3.3 VAE的主要应用场景
图像生成与重构
- 生成新图像(如人脸、手写数字):支持潜在空间插值实现连续变化,即把已有图像对应的潜空间向量 z z z 保存下来,然后插值获得新的 z z z,然后输入到解码器获得新的图像。
- 图像修复与去噪:从损坏或噪声数据中恢复清晰图像,即把损坏的图像输入到编码器,得到 μ \mu μ 和 σ \sigma σ,然后随机生成 ϵ \epsilon ϵ,进而得到潜空间向量 z z z,再把 z z z 输入到解码器实现数据恢复,应用于医学影像处理。训练时仅使用正常数据,推理时根据验证集确定误差阈值(如95%分位数)
- 数据增强:从标准正态分布 N ( 0 , I ) \mathcal{N}(0, I) N(0,I) 中随机采样潜在变量 z z z,然后输入到解码器生成新样本,以扩充训练集,提升模型泛化能力(如医学图像稀缺场景);也可以在潜在空间的两点间线性插值,生成语义连续的过渡样本(如人脸表情渐变)。
异常检测
- 正常数据在潜在空间中聚集于高概率区域(接近标准正态分布中心),异常数据因偏离该区域,解码器重构时会产生显著误差。工业质检中,缺陷产品图像的重构误差远高于正常产品,因此可以计算输入数据重建误差 ∥ x − x ^ ∥ 2 \left \| x-\hat{x} \right \| ^2 ∥x−x^∥2 ,误差大于阈值则为异常。适用领域:金融欺诈检测、工业设备故障诊断。
数据压缩与恢复
- 压缩时输入 x x x 经编码器得到分布参数 μ μ μ 和 σ σ σ 实现数据降维,恢复时用 z = μ + ϵ ⋅ σ z=μ+ϵ⋅σ z=μ+ϵ⋅σ 实现数据重建,其中 ϵ ϵ ϵ 通过标准正太分布采样(测试时可直接用 μ μ μ 避免随机性,不同的 ϵ ϵ ϵ 采样会使重建结果在细节(如纹理、边缘)上存在微小差异)。
4 Diffusion Model
Diffusion Model(扩散模型)是一种基于概率图模型和统计物理学原理的生成式人工智能模型,通过模拟数据的渐进噪声化与去噪过程生成高质量样本。其核心思想源于非平衡热力学,通过系统的扩散与逆扩散过程学习数据分布,广泛应用于图像、音频、文本等多模态生成任务。
4.1 核心原理:前向扩散与逆向生成
前向扩散(Forward Diffusion):
将原始数据(如图像)通过马尔可夫链逐步添加高斯噪声,每一步的加噪公式为:
q ( x t ∣ x t − 1 ) = N ( x t ; 1 − β t x t − 1 , β t I ) q(\mathbf{x}_t | \mathbf{x}_{t-1}) = \mathcal{N}(\mathbf{x}_t; \sqrt{1-\beta_t} \mathbf{x}_{t-1}, \beta_t \mathbf{I}) q(xt∣xt−1)=N(xt;1−βtxt−1,βtI)
N ( … ) \mathcal{N}(\dots) N(…): 表示一个多元高斯分布,分号;
通常用于分隔随机变量和分布的参数,后面第一个参数是均值,第二个参数是协方差矩阵,这里协方差矩阵是对角阵,说明维度间不相关。简言之,上式表示 x t \mathbf{x}_t xt 服从一个均值为 1 − β t x t − 1 \sqrt{1-\beta_t} \mathbf{x}_{t-1} 1−βtxt−1、标准差为 β t \sqrt{\beta_t} βt 的正态分布,其中 β t \beta_t βt 控制噪声强度, T T T 步后数据退化为纯高斯噪声 x T ∼ N ( 0 , I ) \mathbf{x}_T \sim \mathcal{N}(0, \mathbf{I}) xT∼N(0,I)。从 x t − 1 \mathbf{x}_{t-1} xt−1 生成 x t \mathbf{x}_t xt 并不是完全随机的,均值 1 − β t x t − 1 \sqrt{1-\beta_t} \mathbf{x}_{t-1} 1−βtxt−1 表示 x t \mathbf{x}_t xt 的中心点朝着缩小的 x t − 1 \mathbf{x}_{t-1} xt−1 靠近,缩放因子 1 − β t \sqrt{1-\beta_t} 1−βt 保留了大部分 x t − 1 \mathbf{x}_{t-1} xt−1 的信息。 β t \beta_t βt 控制着这一步要添加的噪声量,通常很小(如 0.01),因此大部分信号保留,只添加少量噪声。 β 1 \beta_1 β1, β 2 \beta_2 β2, … , β t \beta_t βt 这样的一个序列被称为 variance schedule 或者 noise schedule。
在缩放的 x t − 1 \mathbf{x}_{t-1} xt−1 基础上,我们从均值为 0、方差为 β t \beta_t βt 的各向同性高斯分布中采样噪声 ϵ ∼ N ( 0 , I ) \boldsymbol{\epsilon} \sim \mathcal{N}(0, \mathbf{I}) ϵ∼N(0,I) 并添加进去。
最终生成步骤可以理解为:
x t = 1 − β t x t − 1 + β t ϵ \mathbf{x}_t = \sqrt{1-\beta_t} \mathbf{x}_{t-1} + \sqrt{\beta_t} \boldsymbol{\epsilon} xt=1−βtxt−1+βtϵ
这正是从定义 N ( x t ; 1 − β t x t − 1 , β t I ) \mathcal{N}(\mathbf{x}_t; \sqrt{1-\beta_t} \mathbf{x}_{t-1}, \beta_t \mathbf{I}) N(xt;1−βtxt−1,βtI) 中采样 x t \mathbf{x}_t xt 的过程。关键性质:从上面的推导来看,任意时刻 t t t 的数据可直接从 x 0 \mathbf{x}_0 x0 采样:
x t = α ˉ t x 0 + 1 − α ˉ t ϵ , α ˉ t = ∏ i = 1 t ( 1 − β i ) \mathbf{x}_t = \sqrt{\bar{\alpha}_t} \mathbf{x}_0 + \sqrt{1-\bar{\alpha}_t} \boldsymbol{\epsilon}, \quad \bar{\alpha}_t = \prod_{i=1}^t (1-\beta_i) xt=αˉtx0+1−αˉtϵ,αˉt=i=1∏t(1−βi)
逆向生成(Reverse Process):
训练神经网络(如U-Net)学习从噪声中逐步恢复数据。逆过程定义为:
p θ ( x t − 1 ∣ x t ) = N ( x t − 1 ; μ θ ( x t , t ) , Σ θ ( x t , t ) ) p_\theta(\mathbf{x}_{t-1} | \mathbf{x}_t) = \mathcal{N}(\mathbf{x}_{t-1}; \boldsymbol{\mu}_\theta(\mathbf{x}_t, t), \boldsymbol{\Sigma}_\theta(\mathbf{x}_t, t)) pθ(xt−1∣xt)=N(xt−1;μθ(xt,t),Σθ(xt,t))
其中,均值 μ θ ( x t , t ) \boldsymbol{\mu}_\theta(\mathbf{x}_t, t) μθ(xt,t) 的尺寸(shape)和 x t \mathbf{x}_{t} xt 完全一致,按下式计算:
μ θ = 1 α t ( x t − β t 1 − α ˉ t ϵ θ ( x t , t ) ) \boldsymbol{\mu}_\theta = \frac{1}{\sqrt{\alpha_t}} \left( \mathbf{x}_t - \frac{\beta_t}{\sqrt{1-\bar{\alpha}_t}} \boldsymbol{\epsilon}_\theta(\mathbf{x}_t, t) \right) μθ=αt1(xt−1−αˉtβtϵθ(xt,t))
这里 α t = 1 − β t \alpha_t = 1 - \beta_t αt=1−βt, ϵ θ \boldsymbol{\epsilon}_\theta ϵθ 是神经网络预测的噪声(稍后介绍完模型架构后就能理解),这样就可以直接预测噪声 ϵ θ \boldsymbol{\epsilon}_\theta ϵθ 而非均值 μ θ \boldsymbol{\mu}_\theta μθ,简化了优化目标。协方差的计算可以根据DDPM(Denoising Diffusion Probabilistic Models)的策略, Σ θ ( x t , t ) = σ t 2 I \boldsymbol{\Sigma}_\theta(\mathbf{x}_t, t) = \sigma_t^2 \mathbf{I} Σθ(xt,t)=σt2I,其中 σ t \sigma_t σt 取预定义值:
- Option 1: σ t 2 = β t \sigma_t^2 = \beta_t σt2=βt(前向过程方差)
- Option 2: σ t 2 = β ~ t = 1 − α ˉ t − 1 1 − α ˉ t β t \sigma_t^2 = \tilde{\beta}_t = \frac{1-\bar{\alpha}_{t-1}}{1-\bar{\alpha}_t} \beta_t σt2=β~t=1−αˉt1−αˉt−1βt
实验表明两者效果接近,固定方差可减少训练不稳定性和计算复杂度。
过程示意图
从右到左为前向扩散,从左到右为逆向生成。
从图中可以看到,扩散的时候,每一步只对当前数据做微小改动(保留大部分信息 + 添加少量各向同性高斯噪声),但经过 T 步(T 通常很大)后,原始数据就被完全破坏成了随机噪声。而逆向生成,则是逆转这个过程。
4.2 模型架构:改进的U-Net网络
扩散模型的前向过程是固定且确定性的数学过程,通过预定义的马尔可夫链逐步向数据添加高斯噪声,该过程仅依赖预设的噪声参数 noise schedule,即 β t {\beta_t} βt,无需任何神经网络或可训练参数。
逆向过程使用的是改进的U-Net结构(就是图像分割的那个U-Net,具体改进措施这里不展开,只需要记住,改进之后,输入的不只有图像,还有时间步),它的输入是噪声图像 x t x_t xt 和时间步 t t t,输出的是残差噪声 ε θ ( x t , t ) ε_θ(x_t, t) εθ(xt,t),噪声与输入图像维度相同。
拿到噪声后,计算均值 μ θ ( x t , t ) \boldsymbol{\mu}_\theta(\mathbf{x}_t, t) μθ(xt,t) 和标准差 Σ θ ( x t , t ) \boldsymbol{\Sigma}_\theta(\mathbf{x}_t, t) Σθ(xt,t),然后生成新样本:
x t − 1 ∼ N ( μ θ ( x t , t ) , Σ θ ( x t , t ) ) x_{t-1} \sim \mathcal{N}(\boldsymbol{\mu}_\theta(\mathbf{x}_t, t), \boldsymbol{\Sigma}_\theta(\mathbf{x}_t, t)) xt−1∼N(μθ(xt,t),Σθ(xt,t))
所有时间步 t 共享同一模型参数 θ θ θ,而非为每个 t 训练独立模型,这是扩散模型的核心设计,显著降低了计算成本。
4.3 数学基础:优化目标与损失函数
损失函数:通过变分下界(ELBO)最大化对数似然,推导出简化目标:
L = E t , x 0 , ϵ [ ∥ ϵ − ϵ θ ( x t , t ) ∥ 2 ] \mathcal{L} = \mathbb{E}_{t, \mathbf{x}_0, \boldsymbol{\epsilon}} \left[ \| \boldsymbol{\epsilon} - \boldsymbol{\epsilon}_\theta(\mathbf{x}_t, t) \|^2 \right] L=Et,x0,ϵ[∥ϵ−ϵθ(xt,t)∥2]
其中 ϵ \boldsymbol{\epsilon} ϵ 为前向过程真实噪声, ϵ ∼ N ( 0 , I ) \boldsymbol{\epsilon} \sim \mathcal{N}(0, I) ϵ∼N(0,I), ϵ θ \boldsymbol{\epsilon}_\theta ϵθ 为模型预测的噪声。E t , x 0 , ϵ \mathbb{E}_{t, \mathbf{x}_0, \boldsymbol{\epsilon}} Et,x0,ϵ 表示对三个随机变量的联合期望(符号不理解没关系,太复杂了),训练目标简化为噪声预测的均方误差(MSE),即对每个样本的每个时间步算一次MSE,然后取平均。
噪声调度策略:
- 线性调度: β t \beta_t βt 随 t t t 线性增加,实现简单但可能生成质量受限。
- 余弦调度: β t = cos ( t T ⋅ π 2 ) \beta_t = \cos\left(\frac{t}{T} \cdot \frac{\pi}{2}\right) βt=cos(Tt⋅2π),在中间阶段加速加噪,保留更多细节,适合复杂数据。
4.4 训练与推理流程
训练过程:
训练并不是严格按照损失函数的表达式来计算损失函数,而是蒙特卡洛模拟:- 从训练集分布 q ( x 0 ) q(\mathbf{x}_0) q(x0) 中采样数据 x 0 \mathbf{x}_0 x0, 从均匀分布中,采样时间步,即 t ∼ Uniform [ 1 , T ] t \sim \text{Uniform}[1, T] t∼Uniform[1,T]。
- 生成噪声 ϵ ∼ N ( 0 , I ) \boldsymbol{\epsilon} \sim \mathcal{N}(0, I) ϵ∼N(0,I) 和加噪样本 x t = α ˉ t x 0 + 1 − α ˉ t ϵ \mathbf{x}_t = \sqrt{\bar{\alpha}_t} \mathbf{x}_0 + \sqrt{1-\bar{\alpha}_t} \boldsymbol{\epsilon} xt=αˉtx0+1−αˉtϵ。
- 输入 x t \mathbf{x}_t xt 和 t t t 至U-Net,预测噪声 ϵ θ \boldsymbol{\epsilon}_\theta ϵθ。
- 计算 ϵ \boldsymbol{\epsilon} ϵ 和 ϵ θ \boldsymbol{\epsilon}_\theta ϵθ 的 MSE 损失并反向传播更新参数。
也就是说,并非所有样本都参与了损失函数计算,也不是每个时间步都参与了计算。
推理过程(采样):
- 初始化随机噪声 x T ∼ N ( 0 , I ) \mathbf{x}_T \sim \mathcal{N}(0, \mathbf{I}) xT∼N(0,I)。
- 从 t = T t=T t=T 到 t = 1 t=1 t=1 逐步去噪:
x t − 1 = 1 α t ( x t − β t 1 − α ˉ t ϵ θ ( x t , t ) ) + σ t z , z ∼ N ( 0 , I ) \mathbf{x}_{t-1} = \frac{1}{\sqrt{\alpha_t}} \left( \mathbf{x}_t - \frac{\beta_t}{\sqrt{1-\bar{\alpha}_t}} \boldsymbol{\epsilon}_\theta(\mathbf{x}_t, t) \right) + \sigma_t \mathbf{z}, \quad \mathbf{z} \sim \mathcal{N}(0, \mathbf{I}) xt−1=αt1(xt−1−αˉtβtϵθ(xt,t))+σtz,z∼N(0,I)
其中 σ t \sigma_t σt 控制随机性(DDPM引入随机噪声;DDIM则为确定性采样)。 - 输出 x 0 \mathbf{x}_0 x0 为生成结果。
4.5 应用与前沿发展
- 图像生成:DALL·E 2、Stable Diffusion 支持文本到图像生成,效果逼真。
- 图像编辑:GLIDE 实现局部修复、风格迁移。
- 跨模态生成:扩散模型扩展至视频(生成连续帧)、音频(音乐合成)、分子结构设计等领域。
Diffusion Model 凭借训练稳定性(避免GAN的模式崩溃)和生成质量优势,成为生成式AI的主流框架,其核心挑战在于采样速度慢与计算成本高。
5 总结
本文是为了给后续讲解多模态模型打基础的,其中 VAE 模型和 Diffusion 模型涉及的数学公式较多,如果理解不了就算了,但模型的结构,推理过程要理解。