文章目录
- 1 AE(自编码器)
- 1.1 自编码器干什么用的
- 1.2 自编码器的架构图、输入输出、训练方法
- 1.3 常见应用
- 1.4 代码示例:图片的压缩存储和复原
- 2 VAE(变分自编码器)
- 2.1 概述
- 2.2 AE存在的问题:隐空间不平滑,无法生成全新图像
- 2.3 VAE如何解决上述问题
- 举例说明
- 2.4 VAE架构图
- 2.5 理解架构图中几个点
- 2.5.1 编码器(Encoder)层的输出是什么?
- 2.5.2 为什么要让编码器预测 log σ^2,而不是直接预测正太分布中的标准差 σ ?
- 2.5.3 编码器的输出经过了哪些操作再输入到解码器的?
- 1\. 编码器的输出:
- 2. **计算标准差 σ \sigma σ**
- 3. **采样过程:重参数化技巧**
- 4\. 将隐变量 z 输入解码器
- 2.5.4 为什么需要噪声ϵ?
- 1. **引入随机性,增加生成样本的多样性**
- 2. **使模型可微分,便于反向传播**
- 3. **实现隐空间的连续性**
- 4. **避免过拟合**
- 总结:
- 2.5.5 VAE的解码器是把Z复原成跟输入一模一样的图像,还是复原成类似的图像?
- 1. **目标是生成相似的图像,而不是一模一样的图像**
- 2. **引入随机性增加了多样性**
- 3. **隐空间的连续性**
- 2.6 VAE训练的原理和损失函数
- 2.6.1 背景知识:KL散度
- 1. **KL散度是什么?**
- 2. **KL散度的直观理解**
- 3. **KL散度的特点**
- 总结:
- 2.6.2 VAE的训练过程与损失函数
- 1. **VAE 的训练步骤**:
- 2. **损失函数的组成**:
- 3. **总损失函数**:
- 4. **反向传播与优化**:
- 总结:
- 2.6.3 备注:VAE中KL loss公式的推导过程
- 1. **KL散度定义**
- 2 高斯分布(也就是正态分布)的概率密度函数
- 3 **将高斯分布代入KL散度公式**
- 4 **对数部分化简**
- 5 **化简KL散度公式**
- 6 **整合所有项**
- 2.7 VAE的全套代码(源自CMU-11785,VAE手写数字生成)
- 2.7.1 ipynb代码文件流程概述和结果截图
- 2.7.2 VAE模型架构和损失函数代码
1 AE(自编码器)
1.1 自编码器干什么用的
自编码器主要用于无监督学习
的任务。它的目的是通过神经网络学习数据的有效低维表示(编码)
,然后使用该编码重建原始数据
。自编码器通常用于特征学习、数据压缩和去噪等任务。
1.2 自编码器的架构图、输入输出、训练方法
模型结构如下图所示:
输入
:一维向量(由图像拉平而成),即原始图像被转换为一维向量形式,以便输入到自编码器的编码器部分。
Encoder
:全连接层。使用全连接层对输入数据进行压缩和特征提取,编码器的目标是将高维的输入数据映射到低维的隐含表示(即“压缩表示”)。
Decoder
:全连接层。同样由全连接层构成,负责从编码器生成的低维表示中重建出高维的原始数据,解码器的目的是复原原始输入数据。
输出
:一维向量(reshape成二维向量,就是图片了)
损失函数与训练方法
:自编码器的训练是通过最小化【输入向量】与【输出向量】的均方误差(MSE loss),这样可以迫使模型在编码器中学习如何提取重要特征,并在解码器中学习如何准确地重建输入数据。
1.3 常见应用
数据降维、压缩
:减少存储量、减少计算量。图像去噪
:使用去噪自编码器(DAE),通过向输入图像添加噪声,并训练自编码器去重构原始的无噪声图像,可以有效地消除噪声。数据生成
:变分自编码器(VAE)是一种生成模型,可以用来生成与训练数据分布相似的新数据。异常检测
:自编码器可以用来检测数据中的异常点。通过训练自编码器重构正常数据,任何重构误差较大的数据点可能就是异常点。这个方法广泛应用于工业设备故障检测、网络入侵检测等领域。数据填补
:自编码器可以用于数据缺失值的填补。通过训练自编码器重构完整数据,可以用潜在空间的表示来推断并填补缺失的数据。
1.4 代码示例:图片的压缩存储和复原
如下代码说明:
- 训练一个简单的自编码器对MNIST手写数字进行压缩表示。
- 单个手写数字图片的信息量:28*28=784个0~255的整数。
- 压缩表示的信息量:16个浮点数。
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
# 检测是否有可用的 GPU
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")
# 定义自编码器类
class Autoencoder(nn.Module):
def __init__(self):
super(Autoencoder, self).__init__()
# 编码器部分
self.encoder = nn.Sequential(
nn.Linear(784, 128),
nn.ReLU(),
nn.Linear(128, 64),
nn.ReLU(),
nn.Linear(64, 32),
nn.ReLU(),
nn.Linear(32, 16) # 压缩到16维
)
# 解码器部分
self.decoder = nn.Sequential(
nn.Linear(16, 32),
nn.ReLU(),
nn.Linear(32, 64),
nn.ReLU(),
nn.Linear(64, 128),
nn.ReLU(),
nn.Linear(128, 784) # 重建回原始784维
)
def forward(self, x):
z = self.encoder(x) # 编码
y = self.decoder(z) # 解码
return y
# 处理 MNIST 数据集的 transforms
transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,))])
# 加载 MNIST 数据集
train_dataset = datasets.MNIST(root='./data', train=True, transform=transform, download=True)
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
# 初始化模型、损失函数和优化器
model = Autoencoder().to(device)
criterion = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=0.0005)
# 开始训练
num_epochs = 20
for epoch in range(num_epochs):
running_loss = 0.0
for data in train_loader:
imgs, _ = data # 不需要标签,只需要图像
imgs = imgs.view(imgs.size(0), -1).to(device)
outputs = model(imgs)
loss = criterion(outputs, imgs)
optimizer.zero_grad()
loss.backward()
optimizer.step()
running_loss += loss.item()
print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {running_loss/len(train_loader):.4f}')
print("训练完成!")
# 测试模型的效果
import matplotlib.pyplot as plt
# 从训练集中取一批数据进行测试
dataiter = iter(train_loader)
images, _ = next(dataiter)
images_flat = images.view(images.size(0), -1).to(device)
# 获取模型的重建结果
reconstructed = model(images_flat).view(-1, 1, 28, 28).cpu().detach() # 转回CPU以便显示
# 显示原始图像和重建图像对比
fig, axes = plt.subplots(2, 6, figsize=(12, 4))
for i in range(6):
# 显示原始图像
axes[0, i].imshow(images[i].view(28, 28), cmap='gray')
axes[0, i].set_title("Original")
axes[0, i].axis('off')
# 显示重建图像
axes[1, i].imshow(reconstructed[i].view(28, 28), cmap='gray')
axes[1, i].set_title("Reconstructed")
axes[1, i].axis('off')
plt.show()
2 VAE(变分自编码器)
2.1 概述
时间:2013.12
论文:Auto-Encoding Variational
作者:荷兰阿姆斯特丹大学,2人
成就:ICLR 2024首个时间检验奖
- 获奖评语:概率建模是对世界进行推理的最基本方式之一。这篇论文率先将深度学习与可扩展概率推理(通过所谓的重新参数化技巧摊销均值场变分推理)相结合,从而催生了变分自动编码器 (VAE)。这项工作的持久价值源于其优雅性。用于开发 VAE 的原理加深了我们对深度学习和概率建模之间相互作用的理解,并引发了许多后续有趣的概率模型和编码方法的开发。这篇论文对于深度学习和生成模型领域产生了重大影响。
2.2 AE存在的问题:隐空间不平滑,无法生成全新图像
非生成式模型
:自编码器的主要目标是将输入数据压缩到低维空间(编码),再从该低维空间重构原始数据(解码)。虽然它可以很好地复原数据,但无法生成训练集之外的全新数据,因其编码空间是离散且不连续的。
编码空间(隐空间)不平滑
:在AE的隐空间中,相邻的编码点并没有表现出平滑的变化。例如,两个图像的编码点虽然距离很近,但解码器生成的图像可能差异巨大。因此,AE的隐空间缺乏结构化。
过拟合
:自编码器往往仅仅记住了训练数据中样本与其编码的对应关系,无法有效地对编码空间中未见过的区域生成合理的输出,这导致AE在生成新样本时的表现不佳。
如下图,在满月与半月中间点的编码空间位置,其输入解码器后,其输出图像可能完全是四不像。
而变分自编码器(VAE)则旨在解决这一问题,通过使隐空间平滑化,使其成为一种生成式模型。
如下图所示,经过训练的VAE在隐空间中相邻的点输入解码器后,能够生成连续渐变的图像。
2.3 VAE如何解决上述问题
在自编码器(AE)中,我们让模型学习从图片中提取一些重要特征,把这些特征用几个数字表示。这就像我们把一张复杂的图片简化成一组特征数字,模型通过这组数字尽量还原原始图片。
但是,变分自编码器(VAE)在此基础上有了改进。它不仅要让模型学会提取特征,还要让这些特征不是固定的数字,而是每个特征都有一个范围(分布),这样模型能学会图片的“模糊性”或者“多样性”。
具体来说,在VAE中,模型会为每个特征生成两个值:一个是这个特征的平均值(大概的值),另一个是这个特征的变化范围(标准差)。然后,模型从这个范围里随机挑选一个值,作为这个特征的表示。这就让隐空间中的点更加连续,相似的图片会被编码到隐空间的相邻位置,这样通过这些相邻的特征可以生成相似的图片。
在训练过程中,我们通过两个目标让模型学会这些:
- 重构目标:模型生成的图片要尽量和原始图片相似;
- 正态分布目标:我们希望模型输出的特征符合某种规律(通常是标准正态分布),这样模型学会的是数据背后的通用规律,而不是仅仅记住图片本身。
通过这两点,模型就能学习到更有用的隐空间表达,并且这些表达是有“弹性”的,能够生成具有多样性但仍然相似的图片。
总结就是:AE学的是固定的特征,VAE学的是带有变化范围的特征,从而让模型不仅能还原图片,还能生成具有变化性的图片。
举例说明
比如说,当我们训练一个用于人脸生成的自编码器(AE)和变分自编码器(VAE)模型时,假设我们将隐空间的维度(即编码器输出的向量长度)设为6,AE的做法是将人脸压缩为6个特征,每个特征对应一个具体的数值(即特征提取的结果)。而在VAE中,模型不仅会将人脸编码为6个特征,还会为每个特征生成一个概率分布,即每个特征不再是一个固定的数值,而是一个带有变化范围的分布。这种分布能够让模型生成具有更多变化的图像,同时保持图像的相似性。如下图:
2.4 VAE架构图
(备注:上图有2处地方写错了,我已经校正了)
VAE的本质是让编码器(encoder)学会为输入数据生成一组正态分布的参数
(均值
μ
\mu
μ 和方差
σ
2
\sigma^2
σ2),这些正态分布可以看作是对输入数据在隐空间中的概率表示
。这些分布的作用是让相似的输入数据在隐空间中映射到相邻或连续的区域,同时允许通过采样生成具有一定随机性和多样性的隐变量 (Z)。每个隐变量 (Z) 是从这些正态分布中随机采样的结果,之后由解码器(decoder)将其还原为相应的数据(如图像)。
因此,这些正态分布不仅表示了输入数据的潜在特征,还赋予了模型在生成任务中的多样性和连续性,使得模型在生成新样本时能够从隐空间中进行有意义的采样。
2.5 理解架构图中几个点
2.5.1 编码器(Encoder)层的输出是什么?
- 输出内容
编码器的输出主要包括两部分:
- **均值向量 ** μ \mu μ:表示输入数据在隐空间中的中心点,每个维度对应隐空间中的一个特征。均值向量用于确定数据在隐空间中的位置。
- 对数方差向量 log σ 2 \log\sigma^2 logσ2:表示输入数据的方差分布,通过取对数的形式保证方差非负。该向量用于表示数据在隐空间中的不确定性,即该输入数据在不同维度上的变动范围。
- 输出维度
这两个向量的长度由隐空间的维度决定,假设隐空间的维度是 d d d,那么输出的 μ \mu μ 和 log σ 2 \log \sigma^2 logσ2 的长度都是 d d d。
隐空间的维度是模型设定的超参数,通常可以设为 6、10、100 等。例如,假设隐空间的维度为 6,那么编码器输出的会是 6 个均值和 6 个方差。
- 输出的意义
编码器输出的均值和方差共同构成了一个正态分布,用于描述输入数据在隐空间中的分布特征。这一分布使得VAE具备生成模型的能力,通过从隐空间中不同位置采样 Z Z Z,可以生成具有多样性但依然保留输入数据主要特征的新样本。
2.5.2 为什么要让编码器预测 log σ^2,而不是直接预测正太分布中的标准差 σ ?
- 标准差必须非负:标准差 σ \sigma σ 是用于衡量数据分布广度的度量,表示数据点与均值的平均距离,因此它必须是非负的。如果直接让编码器预测 σ \sigma σ,我们需要在输出层增加非负约束,例如使用 ReLU 激活函数。然而,这种做法会引入额外的复杂性,并且可能导致数值不稳定。
- 预测 log σ 2 \log\sigma^2 logσ2 的优势:
- 预测范围更广: log σ 2 \log\sigma^2 logσ2 的值可以为正或负,因此没有非负的限制,使得模型的输出范围更灵活,优化过程更加自由。
- 转换简单:虽然预测的是 log σ 2 \log\sigma^2 logσ2,但可以简单的通过指数运算 σ = exp ( 1 2 log σ 2 ) \sigma=\exp\left(\frac{1}{2}\log\sigma^2\right) σ=exp(21logσ2) 轻松将其转换为标准差 σ \sigma σ,并且这种转换确保了得到的 σ \sigma σ 值始终为正值。
- 数值稳定性:直接预测 σ \sigma σ 可能会在训练过程中导致数值不稳定(例如梯度更新时可能过大或过小)。使用 log σ 2 \log\sigma^2 logσ2 可以避免这种问题,因为对数操作控制了输出值的范围,使优化过程更加平稳。
总结:
让编码器预测 log σ 2 \log \sigma^2 logσ2 而不是直接预测 σ \sigma σ,主要是为了保证标准差的非负性,同时使得输出更加灵活、数值稳定性更强,并简化了对标准差的处理。
2.5.3 编码器的输出经过了哪些操作再输入到解码器的?
编码器的输出经过以下几个步骤处理后,才会作为输入传递给解码器:
1. 编码器的输出:
编码器会为输入数据生成两个关键的输出:
- 均值向量 μ \mu μ:表示输入数据在隐空间中的平均位置。
- 对数方差向量 log σ 2 \log \sigma^2 logσ2:表示每个隐变量在隐空间中的分布宽度(不确定性)。
2. 计算标准差 σ \sigma σ
为了从对数方差 log σ 2 \log \sigma^2 logσ2 中得到标准差 σ \sigma σ,我们需要对其进行指数运算:
σ = exp ( 1 2 log σ 2 ) \sigma = \exp\left(\frac{1}{2} \log \sigma^2\right) σ=exp(21logσ2)
这一步将对数方差转换为标准差,从而确保每个维度的标准差始终为正。
3. 采样过程:重参数化技巧
为了使得VAE的采样过程可微分(以便通过反向传播训练模型),VAE使用了重参数化技巧(Reparameterization Trick)。具体步骤如下:
- 从标准正态分布 N ( 0 , 1 ) \mathcal{N}(0, 1) N(0,1) 中采样一个随机噪声 ϵ \epsilon ϵ。
- 通过公式计算隐变量 Z Z Z:
Z = μ + σ × ϵ Z = \mu + \sigma \times \epsilon Z=μ+σ×ϵ
其中, ϵ \epsilon ϵ 是一个从标准正态分布中采样的噪声。这样,通过 μ \mu μ 和 σ \sigma σ 来控制生成的 Z Z Z,确保每次采样都带有一定的随机性。
4. 将隐变量 z 输入解码器
最终计算得到的隐变量 Z Z Z 是一个经过随机采样后的隐空间表示,它结合了输入数据的均值 μ \mu μ 和不确定性 σ \sigma σ。这个隐变量 Z Z Z 会作为解码器的输入,通过解码器生成与原始数据类似的输出(如图像、音频等)。
2.5.4 为什么需要噪声ϵ?
噪声 ϵ \epsilon ϵ 在VAE中是非常关键的一部分,原因如下:
1. 引入随机性,增加生成样本的多样性
- VAE的目标是从隐空间中采样生成与原始输入相似的样本。通过添加噪声 ϵ \epsilon ϵ,我们确保了每次从正态分布中采样的隐变量 Z Z Z 都会带有一定的随机性。
- 这种随机性能够使模型生成具有一定变化的样本,而不是每次都生成完全相同的输出。通过 ϵ \epsilon ϵ,我们可以在相同的均值 μ \mu μ 和标准差 σ \sigma σ 上生成不同的隐变量 Z Z Z,从而提高生成样本的多样性。
2. 使模型可微分,便于反向传播
- 在普通的自编码器中,编码器直接输出隐空间的点。但是在VAE中,由于我们需要从隐空间中的正态分布采样,而采样本身是一个非确定性的过程,不能直接用于反向传播。
- 为了解决这个问题,VAE采用了重参数化技巧,即将采样过程重写为 Z = μ + σ × ϵ Z = \mu + \sigma \times \epsilon Z=μ+σ×ϵ,其中 ϵ \epsilon ϵ 是从标准正态分布 N ( 0 , 1 ) \mathcal{N}(0, 1) N(0,1) 中采样的噪声。这样,采样的随机性被隔离到了 ϵ \epsilon ϵ,而 μ \mu μ 和 σ \sigma σ 是可微的,可以通过反向传播进行优化。这个技巧确保了VAE在训练过程中可以正常计算梯度并更新参数。
3. 实现隐空间的连续性
- 噪声 ϵ \epsilon ϵ 帮助确保隐空间的连续性,即相似的输入样本在隐空间中会被映射到相邻的区域。
- 通过在正态分布上引入噪声,模型可以在隐空间中生成连续的表示。这样,模型不仅能够对训练样本进行重构,还可以通过在隐空间中进行插值生成新的样本。例如,在人脸生成模型中,隐变量 Z Z Z 可以表示不同的人脸特征,噪声确保了这些特征的平滑过渡。
4. 避免过拟合
- 在没有噪声的情况下,模型可能会记住输入数据,导致过拟合。而通过引入噪声,模型在训练过程中学习的是数据的分布特征,而不仅仅是记住每个样本本身。
- 这使得模型能够泛化到未见过的样本上,从而提高了模型的生成能力。
总结:
噪声 ϵ \epsilon ϵ 的作用是确保VAE中的随机性和多样性,同时通过重参数化技巧保持可微分性。它帮助模型生成更为丰富和多样的样本,并确保模型在训练过程中能够进行正常的梯度更新,保持隐空间的连续性和生成过程的泛化能力。
2.5.5 VAE的解码器是把Z复原成跟输入一模一样的图像,还是复原成类似的图像?
解码器在变分自编码器(VAE)中的目标是将隐变量 Z Z Z 还原成与输入类似的图像,而不是完全一模一样的图像。其背后的原因和设计初衷有以下几个方面:
1. 目标是生成相似的图像,而不是一模一样的图像
- 变分自编码器的核心思想是学习输入数据的概率分布,并通过隐空间中的变量 Z Z Z 来生成具有相似特征的样本。因此,解码器通过隐变量 Z Z Z 重构的图像应该与输入图像相似,但由于引入了随机性(通过噪声 ϵ \epsilon ϵ 的采样),生成的图像不一定与输入图像完全一模一样。
- 这一设计的目的是为了让模型具备生成能力,而不仅仅是重构能力。解码器可以通过隐空间中的不同点生成带有变化的图像,从而具备生成多样化数据的能力。
2. 引入随机性增加了多样性
- 编码器通过预测均值 μ \mu μ 和方差 σ \sigma σ,生成隐变量 Z Z Z,其中引入了从正态分布中采样的噪声 ϵ \epsilon ϵ。这使得每次生成的 Z Z Z 都带有一定的随机性。因此,即使对相同的输入数据,生成的图像可能会有细微差异。
- 这种随机性允许模型生成多种不同但相似的图像,而不是仅仅将输入图像逐字逐句地“记住”并一模一样地重现。
3. 隐空间的连续性
- VAE的隐空间是连续的,意味着在隐空间中相邻的点生成的图像会有连续的变化。因此,解码器通过 Z Z Z 来重构图像时,生成的结果会和输入图像有相似的特征,但由于隐空间中的采样过程,生成图像是相似但不完全相同的。
2.6 VAE训练的原理和损失函数
2.6.1 背景知识:KL散度
KL散度(Kullback-Leibler Divergence),简称 KL散度,是用来衡量两个概率分布之间差异的一种方法。它常用于机器学习中,用来比较模型预测的概率分布和真实概率分布之间的差异。在变分自编码器(VAE)中,KL散度用于度量编码器生成的隐空间分布与标准正态分布之间的差异。
1. KL散度是什么?
KL散度是一种非对称的测量方式,它告诉我们一个分布 q ( z ∣ x ) q(z|x) q(z∣x) 相对于另一个参考分布 p ( z ) p(z) p(z) 有多么不同。它可以理解为:“如果我们使用分布 q q q 来近似分布 p p p,会损失多少信息?”
KL散度的公式如下:
D KL ( q ( z ∣ x ) ∣ ∣ p ( z ) ) = ∑ z q ( z ∣ x ) log q ( z ∣ x ) p ( z ) D_{\text{KL}}(q(z|x) || p(z)) = \sum_{z} q(z|x) \log \frac{q(z|x)}{p(z)} DKL(q(z∣x)∣∣p(z))=z∑q(z∣x)logp(z)q(z∣x)
2. KL散度的直观理解
KL散度可以理解为“信息损失”的度量,表示了如果我们用分布 q ( z ∣ x ) q(z|x) q(z∣x) 来替代 p ( z ) p(z) p(z),我们会错过多少信息。
- 当 q ( z ∣ x ) q(z|x) q(z∣x) 和 p ( z ) p(z) p(z) 完全相同(即两个分布一致时),KL散度为 0,表示没有任何信息损失。
- 当 q ( z ∣ x ) q(z|x) q(z∣x) 和 p ( z ) p(z) p(z) 越不相同时,KL散度越大,表示信息损失越多,也就是说这两个分布的差异越大。
举例子:
- 假设你认为今天会下雨的概率是 0.9,而实际的天气预报给出的下雨概率是 0.2。如果你按照你的预测去准备雨具,那么你实际上是错误地评估了这个概率,犯了一个错误。KL散度在这里就是衡量你预测的分布(即 0.9 下雨)和实际的天气分布(0.2 下雨)之间的差异。
- 例子:抛硬币
假设我们有一枚不均匀的硬币:
- 真实分布 ( P ):正面朝上的概率为 0.9,反面为 0.1。
- 近似分布 ( Q ):我们误以为硬币是公平的,即正反面概率都是 0.5。
计算KL散度:
D K L ( P ∥ Q ) = 0.9 log ( 0.9 0.5 ) + 0.1 log ( 0.1 0.5 ) D_{KL}(P \| Q) = 0.9 \log\left(\frac{0.9}{0.5}\right) + 0.1 \log\left(\frac{0.1}{0.5}\right) DKL(P∥Q)=0.9log(0.50.9)+0.1log(0.50.1)
计算得:
D K L ( P ∥ Q ) = 0.9 × 0.847 + 0.1 × ( − 1.609 ) = 0.762 − 0.161 = 0.601 D_{KL}(P \| Q) = 0.9 \times 0.847 + 0.1 \times (-1.609) = 0.762 - 0.161 = 0.601 DKL(P∥Q)=0.9×0.847+0.1×(−1.609)=0.762−0.161=0.601
这表示,如果我们用 ( Q ) 来近似 ( P ),每次抛硬币平均会损失约 0.601 个 nats 的信息(以自然对数为底)。
3. KL散度的特点
- 非对称性:KL散度不是对称的,也就是说 D KL ( q ∣ ∣ p ) ≠ D KL ( p ∣ ∣ q ) D_{\text{KL}}(q || p) \neq D_{\text{KL}}(p || q) DKL(q∣∣p)=DKL(p∣∣q)。因此,它不是一个真正的“距离”度量,只能告诉我们一个分布相对于另一个分布的差异有多大。
- 非负性:KL散度总是大于或等于0,也就是说:
D KL ( q ∣ ∣ p ) ≥ 0 D_{\text{KL}}(q || p) \geq 0 DKL(q∣∣p)≥0
当且仅当两个分布完全相同时,KL散度才为 0。
总结:
KL散度衡量的是两个概率分布之间的差异。在VAE中,KL散度确保编码器生成的隐变量分布尽可能接近标准正态分布,帮助模型在隐空间中生成结构化、连续性的表示,同时保证生成的样本具有多样性和泛化能力。
2.6.2 VAE的训练过程与损失函数
变分自编码器(VAE)的训练过程与传统自编码器类似,但增加了一些关键的步骤以确保模型能够学习到隐空间的分布特征。VAE 的训练目标是最小化重构误差和隐空间分布与标准正态分布之间的差异。具体来说,VAE 的训练依赖于两个关键的损失函数:重构损失 和 KL 散度损失。
1. VAE 的训练步骤:
- 编码:输入数据通过编码器,被编码为均值向量 μ \mu μ 和对数方差向量 log σ 2 \log \sigma^2 logσ2。
- 重参数化技巧:通过重参数化技巧,使用均值 μ \mu μ 和标准差 σ \sigma σ,以及从标准正态分布中采样的噪声 ϵ \epsilon ϵ 生成隐变量 Z = μ + σ × ϵ Z = \mu + \sigma \times \epsilon Z=μ+σ×ϵ。
- 解码:隐变量 Z Z Z 传入解码器,解码器尝试将其重构为与输入数据相似的输出。
2. 损失函数的组成:
VAE 的损失函数由两个部分组成:
2.1 重构损失(Reconstruction Loss)
- 这部分损失衡量模型生成的图像与输入图像的相似程度。通常使用像素级别的差异来衡量,例如使用均方误差(MSE)或二元交叉熵(BCE)作为损失函数。
- 如果输入图像为 x x x,解码器生成的图像为 x ^ \hat{x} x^,那么重构损失为:
Reconstruction Loss = E q ( z ∣ x ) [ log p ( x ∣ z ) ] \text{Reconstruction Loss} = \mathbb{E}_{q(z|x)}[\log p(x|z)] Reconstruction Loss=Eq(z∣x)[logp(x∣z)]
- 直观上,这部分损失的目标是让解码器生成的图像尽可能接近输入图像,确保解码器能够有效地还原信息。
2.2 KL 散度损失(KL Divergence Loss)
- 在 VAE 中,我们希望编码器输出的分布接近标准正态分布,所以用KL散度进行约束。计算公式如下(推导过程见下附录):
KL Loss = D KL ( q ( z ∣ x ) ∣ ∣ p ( z ) ) = 1 2 ∑ i = 1 d ( μ i 2 + σ i 2 − log σ i 2 − 1 ) \text{KL Loss} = D_{\text{KL}}(q(z|x) || p(z)) = \frac{1}{2} \sum_{i=1}^{d} \left( \mu_i^2 + \sigma_i^2 - \log \sigma_i^2 - 1 \right) KL Loss=DKL(q(z∣x)∣∣p(z))=21i=1∑d(μi2+σi2−logσi2−1)
3. 总损失函数:
VAE 的总损失函数是重构损失和 KL 散度损失的加和:
VAE Loss = Reconstruction Loss + KL Loss \text{VAE Loss} = \text{Reconstruction Loss} + \text{KL Loss} VAE Loss=Reconstruction Loss+KL Loss
- 重构损失 确保生成的样本与输入相似。
- KL 散度损失 确保编码器生成的隐空间分布与标准正态分布接近。
4. 反向传播与优化:
- VAE 使用反向传播和梯度下降来最小化上述损失函数。通过调整编码器和解码器的参数,模型逐步学会如何有效编码输入数据,同时保持隐空间中的连续性和可解释性。
总结:
VAE 的训练过程通过最小化两部分损失函数来进行:重构损失 用于确保生成图像与输入图像相似,KL 散度损失 用于让隐空间的分布接近标准正态分布。通过这两个目标的结合,VAE 能够学会如何生成多样化但相似的图像,同时保持生成空间的结构化。
2.6.3 备注:VAE中KL loss公式的推导过程
1. KL散度定义
在VAE中,KL散度衡量编码器输出的隐变量分布 q ( z ∣ x ) q(z|x) q(z∣x) 与先验分布 p ( z ) p(z) p(z) 之间的差异。KL散度的定义公式为:
D KL ( q ( z ∣ x ) ∣ ∣ p ( z ) ) = ∫ q ( z ∣ x ) log q ( z ∣ x ) p ( z ) d z D_{\text{KL}}(q(z|x) || p(z)) = \int q(z|x) \log \frac{q(z|x)}{p(z)} dz DKL(q(z∣x)∣∣p(z))=∫q(z∣x)logp(z)q(z∣x)dz
在VAE中:
- q ( z ∣ x ) q(z|x) q(z∣x) 是编码器输出的后验分布,通常假设为一个高斯分布 N ( μ , σ 2 ) \mathcal{N}(\mu, \sigma^2) N(μ,σ2);
- p ( z ) p(z) p(z) 是标准正态分布 N ( 0 , 1 ) \mathcal{N}(0, 1) N(0,1)。
我们需要计算这两个高斯分布之间的KL散度。
2 高斯分布(也就是正态分布)的概率密度函数
首先,我们写出高斯分布的概率密度函数:
- 对于 q ( z ∣ x ) = N ( μ , σ 2 ) q(z|x) = \mathcal{N}(\mu, \sigma^2) q(z∣x)=N(μ,σ2),概率密度函数为:
q ( z ∣ x ) = 1 2 π σ 2 exp ( − ( z − μ ) 2 2 σ 2 ) q(z|x) = \frac{1}{\sqrt{2\pi\sigma^2}} \exp\left(-\frac{(z - \mu)^2}{2\sigma^2}\right) q(z∣x)=2πσ21exp(−2σ2(z−μ)2)
- 对于 p ( z ) = N ( 0 , 1 ) p(z) = \mathcal{N}(0, 1) p(z)=N(0,1),概率密度函数为:
p ( z ) = 1 2 π exp ( − z 2 2 ) p(z) = \frac{1}{\sqrt{2\pi}} \exp\left(-\frac{z^2}{2}\right) p(z)=2π1exp(−2z2)
3 将高斯分布代入KL散度公式
将高斯分布的概率密度函数代入KL散度的定义公式:
D KL ( q ( z ∣ x ) ∣ ∣ p ( z ) ) = ∫ 1 2 π σ 2 exp ( − ( z − μ ) 2 2 σ 2 ) log 1 2 π σ 2 exp ( − ( z − μ ) 2 2 σ 2 ) 1 2 π exp ( − z 2 2 ) d z D_{\text{KL}}(q(z|x) || p(z)) = \int \frac{1}{\sqrt{2\pi\sigma^2}} \exp\left(-\frac{(z - \mu)^2}{2\sigma^2}\right) \log \frac{\frac{1}{\sqrt{2\pi\sigma^2}} \exp\left(-\frac{(z - \mu)^2}{2\sigma^2}\right)}{\frac{1}{\sqrt{2\pi}} \exp\left(-\frac{z^2}{2}\right)} dz DKL(q(z∣x)∣∣p(z))=∫2πσ21exp(−2σ2(z−μ)2)log2π1exp(−2z2)2πσ21exp(−2σ2(z−μ)2)dz
4 对数部分化简
对数部分可以分为两部分:
log 1 2 π σ 2 exp ( − ( z − μ ) 2 2 σ 2 ) 1 2 π exp ( − z 2 2 ) = log 1 σ 2 − ( z − μ ) 2 2 σ 2 + z 2 2 \log \frac{\frac{1}{\sqrt{2\pi\sigma^2}} \exp\left(-\frac{(z - \mu)^2}{2\sigma^2}\right)}{\frac{1}{\sqrt{2\pi}} \exp\left(-\frac{z^2}{2}\right)} = \log \frac{1}{\sqrt{\sigma^2}} - \frac{(z - \mu)^2}{2\sigma^2} + \frac{z^2}{2} log2π1exp(−2z2)2πσ21exp(−2σ2(z−μ)2)=logσ21−2σ2(z−μ)2+2z2
5 化简KL散度公式
代入上面的化简结果后,KL散度公式变为:
D KL ( q ( z ∣ x ) ∣ ∣ p ( z ) ) = ∫ q ( z ∣ x ) ( log 1 σ 2 − ( z − μ ) 2 2 σ 2 + z 2 2 ) d z D_{\text{KL}}(q(z|x) || p(z)) = \int q(z|x) \left( \log \frac{1}{\sqrt{\sigma^2}} - \frac{(z - \mu)^2}{2\sigma^2} + \frac{z^2}{2} \right) dz DKL(q(z∣x)∣∣p(z))=∫q(z∣x)(logσ21−2σ2(z−μ)2+2z2)dz
这个公式可以进一步拆解为三项:
- 第一项:
∫ q ( z ∣ x ) log 1 σ 2 d z = − 1 2 log σ 2 \int q(z|x) \log \frac{1}{\sqrt{\sigma^2}} dz = -\frac{1}{2} \log \sigma^2 ∫q(z∣x)logσ21dz=−21logσ2
- 第二项:
∫ q ( z ∣ x ) ( − ( z − μ ) 2 2 σ 2 ) d z \int q(z|x) \left(- \frac{(z - \mu)^2}{2\sigma^2} \right) dz ∫q(z∣x)(−2σ2(z−μ)2)dz
由于这是高斯分布的期望,已知 E q ( z ∣ x ) [ ( z − μ ) 2 ] = σ 2 \mathbb{E}_{q(z|x)}[(z - \mu)^2] = \sigma^2 Eq(z∣x)[(z−μ)2]=σ2,所以这一项化简为:
− 1 2 - \frac{1}{2} −21
- 第三项:
∫ q ( z ∣ x ) z 2 2 d z \int q(z|x) \frac{z^2}{2} dz ∫q(z∣x)2z2dz
这可以拆解为:
1 2 E q ( z ∣ x ) [ z 2 ] = 1 2 ( μ 2 + σ 2 ) \frac{1}{2} \mathbb{E}_{q(z|x)}[z^2] = \frac{1}{2} (\mu^2 + \sigma^2) 21Eq(z∣x)[z2]=21(μ2+σ2)
6 整合所有项
将所有项整合后,KL散度的最终结果为:
D KL ( N ( μ , σ 2 ) ∣ ∣ N ( 0 , 1 ) ) = 1 2 ( μ 2 + σ 2 − log σ 2 − 1 ) D_{\text{KL}}(\mathcal{N}(\mu, \sigma^2) || \mathcal{N}(0, 1)) = \frac{1}{2} \left( \mu^2 + \sigma^2 - \log \sigma^2 - 1 \right) DKL(N(μ,σ2)∣∣N(0,1))=21(μ2+σ2−logσ2−1)
2.7 VAE的全套代码(源自CMU-11785,VAE手写数字生成)
【variational_autoencoder.ipynb】
2.7.1 ipynb代码文件流程概述和结果截图
- 引入与准备工作
- 安装与导入PyTorch及相关库:确保环境准备好,包括必要的库和依赖,尤其在Google Colab等环境下需要安装特定的依赖。
- 导入数据集(MNIST):加载手写数字数据集MNIST,并对其进行预处理,为模型训练做好准备。
- 参数设置
- 设置VAE模型所需的超参数,例如隐空间维度、学习率等。这些参数控制模型的训练行为和最终的生成效果。
- VAE模型定义
- 编码器(Encoder):将输入图像编码为隐空间表示(即潜在向量),并生成均值和方差向量,作为隐变量的分布。
- 解码器(Decoder):从隐空间表示还原出原始图像,完成数据的重构。
- VAE类整合:将编码器和解码器封装为一个整体VAE模型,并定义其前向传播过程。
- 训练模型
- 使用MNIST数据集训练VAE模型,优化器设置为Adam,并且通过重构损失和KL散度损失来指导模型学习隐空间的分布和生成能力。
- 绘制训练曲线
- 使用Matplotlib展示训练过程中损失函数的变化,帮助可视化模型的收敛情况。
-
- 加载预训练模型
- 提供了加载预训练模型的选项,避免从头开始训练模型。如果有预训练的VAE模型,可以直接加载并进行推理或测试。
- 测试模型与评估
-
在测试集上评估模型的表现,切换到评估模式,并对模型生成的图像与真实图像的相似性进行对比。
-
重建图像的可视化
- 可视化模型在测试集上的重建效果,即对给定的手写数字输入,模型如何在隐空间中找到对应的表示,并生成相似的图像。
- 隐空间插值
- 演示在隐空间中对两个潜在向量进行线性插值,生成从一个图像逐渐过渡到另一个图像的效果。
- 从先验分布中采样
- 演示如何从标准正态分布中随机采样潜在向量,经过VAE解码器生成新图像。这展示了VAE作为生成模型的能力。
- 2D隐空间可视化
- 如果VAE的隐空间维度为2,可以将隐空间中的样本可视化,并展示潜在变量与重建图像之间的对应关系。
2.7.2 VAE模型架构和损失函数代码
class Encoder(nn.Module):
def __init__(self):
super(Encoder, self).__init__()
c = capacity # 基础的通道数量,模型的容量参数
# 第一个卷积层,将输入图像从1通道转换为c通道,输出尺寸为(c, 14, 14)
self.conv1 = nn.Conv2d(in_channels=1, out_channels=c, kernel_size=4, stride=2, padding=1) # out: c x 14 x 14
# 第二个卷积层,将特征从c通道转换为2c通道,输出尺寸为(c*2, 7, 7)
self.conv2 = nn.Conv2d(in_channels=c, out_channels=c*2, kernel_size=4, stride=2, padding=1) # out: c*2 x 7 x 7
# 全连接层,用于生成潜在空间的均值向量
self.fc_mu = nn.Linear(in_features=c*2*7*7, out_features=latent_dims)
# 全连接层,用于生成潜在空间的对数方差向量
self.fc_logvar = nn.Linear(in_features=c*2*7*7, out_features=latent_dims)
def forward(self, x):
x = F.relu(self.conv1(x))
x = F.relu(self.conv2(x))
x = x.view(x.size(0), -1) # 展平多通道特征图
x_mu = self.fc_mu(x)
x_logvar = self.fc_logvar(x)
return x_mu, x_logvar # 返回均值和对数方差
class Decoder(nn.Module):
def __init__(self):
super(Decoder, self).__init__()
c = capacity # 基础的通道数量
self.fc = nn.Linear(in_features=latent_dims, out_features=c*2*7*7) # 全连接层,将潜在向量映射回特征图的尺寸
self.conv2 = nn.ConvTranspose2d(in_channels=c*2, out_channels=c, kernel_size=4, stride=2, padding=1) # 反卷积层,将特征从2c通道转换为c通道
self.conv1 = nn.ConvTranspose2d(in_channels=c, out_channels=1, kernel_size=4, stride=2, padding=1) # 反卷积层,将特征从c通道转换回1通道(即恢复到图像的大小)
def forward(self, x):
x = self.fc(x) # 将全连接层的输出调整为合适的卷积输入形状
x = x.view(x.size(0), capacity*2, 7, 7) # 展平为批量多通道特征图
x = F.relu(self.conv2(x)) # 反卷积层并应用ReLU激活函数
x = torch.sigmoid(self.conv1(x)) # 最后一层使用Sigmoid激活(因为重构损失使用的是二元交叉熵)
return x
class VariationalAutoencoder(nn.Module):
def __init__(self):
super(VariationalAutoencoder, self).__init__()
self.encoder = Encoder() # 初始化编码器
self.decoder = Decoder() # 初始化解码器
def forward(self, x):
# 使用编码器生成潜在空间的均值和对数方差
latent_mu, latent_logvar = self.encoder(x)
# 通过重参数化技巧采样潜在变量
latent = self.latent_sample(latent_mu, latent_logvar)
# 解码器将潜在变量解码为重构的图像
x_recon = self.decoder(latent)
return x_recon, latent_mu, latent_logvar
def latent_sample(self, mu, logvar):
if self.training:
# 使用重参数化技巧
std = logvar.mul(0.5).exp_() # 计算标准差
eps = torch.empty_like(std).normal_() # 从标准正态分布中采样噪声
return eps.mul(std).add_(mu) # 返回重新参数化的潜在向量
else:
return mu # 推理时直接返回均值作为潜在变量
def vae_loss(recon_x, x, mu, logvar):
# 重构损失:计算像素级的二元交叉熵,用于衡量重构图像与原始图像的相似性
recon_loss = F.binary_cross_entropy(recon_x.view(-1, 784), x.view(-1, 784), reduction='sum')
# KL散度:衡量编码器生成的潜在分布与标准正态分布之间的差异
kldivergence = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp())
# 返回总损失:重构损失 + KL散度损失
return recon_loss + variational_beta * kldivergence # variational_beta 是一个可调的超参数
损失函数的输入参数备注:
recon_x
:解码器生成的重构图像,用于计算重构损失。x
:原始输入图像,用于与重构图像比较,计算像素级的二元交叉熵(BCE)损失。mu
:潜在空间的均值,用于计算KL散度,衡量潜在空间分布与标准正态分布之间的差异。logvar
:潜在空间的对数方差,用于计算KL散度,控制潜在空间的不确定性。