变分自编码器(VAE)详解
Chen Kai Architect

在深度学习的生成模型领域,变分自编码器(Variational Autoencoder, VAE) 作为一种创新的模型架构,展现出了强大的数据生成和潜在表示学习能力。VAE不仅能够有效地压缩和重建输入数据,还能通过学习数据的潜在分布来生成与训练数据高度相似的新样本。其核心在于结合了自编码器和概率图模型的优点,并通过重参数化技巧实现了端到端的可微优化。这使得VAE在图像生成、数据压缩、异常检测等多个应用场景中得到了广泛的应用和认可。本文将深入分析VAE的基本原理、工作机制以及其在实际应用中的优势与挑战,帮助初学者全面掌握这一重要的生成模型。

什么是自动编码器

自动编码器(AutoEncoder)最早作为一种数据压缩技术,它的主要目标是通过神经网络将高维数据压缩为低维表示,并尽可能在压缩的同时保留数据的重要特征。在基本的自动编码器中,主要包含两部分:编码器(Encoder)解码器(Decoder)。编码器负责将输入数据压缩为一个潜在表示(latent representation),即所谓的隐含向量(latent vector)。解码器则尝试从这个潜在表示中重建输入数据,使得输出数据与输入数据尽可能接近。

自动编码器在降维时使用的方式是基于无监督学习,即它不需要对数据进行标注,而是通过输入数据本身来进行训练。经过训练的自动编码器能够学习到数据的特征,并在不丢失太多信息的情况下对数据进行压缩。

自动编码器的特点

  1. 与数据高度相关:自动编码器只能有效压缩与训练数据相似的数据,因为它是通过学习输入数据的特征来压缩信息的。这意味着,如果我们使用人脸数据集训练自动编码器,它在压缩人脸数据时表现良好,但是对于其他类别的数据(例如动物图像)则表现较差。这种特性限制了自动编码器的通用性,但在某些特定领域表现十分出色。
  2. 有损压缩:自动编码器的压缩是有损的,原因在于数据从高维到低维的过程中,不可避免地会丢失部分信息。这种信息丢失取决于网络的复杂性和编码器的结构,虽然模型试图通过解码器恢复丢失的信息,但恢复的精度取决于模型的设计和优化。

自动编码器的应用场景

尽管自动编码器最初是作为数据压缩技术提出的,它如今已经在多个领域获得了广泛应用,主要包括以下几个方面:

  1. 数据去噪:自动编码器在去噪任务中非常有效。通过将含有噪声的数据输入到编码器,并训练解码器生成没有噪声的重建数据,自动编码器可以自动去除噪声。这种去噪自动编码器在图像处理、语音信号处理等领域得到了应用。
  2. 可视化降维:自动编码器能够将高维数据映射到低维空间,并保留数据的核心特征。因此,它常被用于数据降维和可视化。与PCA等传统降维方法不同,自动编码器能够通过非线性映射捕捉数据中的复杂结构。
  3. 生成数据:虽然自动编码器的主要任务是数据压缩和重建,但它也可以用来生成新数据。通过修改编码器输出的隐含向量,解码器可以生成与训练数据相似但不完全相同的新数据。这与生成对抗网络(GAN)有相似之处,但GAN使用的是随机噪声,而自动编码器通过学习得到的隐含向量更加具有解释性和控制性。

自动编码器的工作原理

自动编码器的结构可以用以下两个部分来描述:

  1. 编码器(Encoder):编码器的作用是将输入数据从高维空间映射到一个较小的隐含空间。这通常通过一个深层的神经网络实现。在图像数据中,常见的编码器是卷积神经网络(CNN),它通过提取数据的局部特征逐渐将输入的高维图像压缩成一个低维的特征表示。
  2. 解码器(Decoder):解码器的作用是从编码器输出的隐含表示中重建原始数据。它通过将低维表示重新映射到与输入数据相同的维度,从而恢复出输入数据的近似值。解码器通常与编码器是镜像对称的结构,通过逆向的卷积操作(例如转置卷积)将隐含向量还原成完整的图像。

自动编码器的典型结构

典型的自动编码器网络结构如下:

  • 输入层:接收原始输入数据(如图像的像素)。
  • 编码器层:通过若干层神经网络将输入数据映射到隐含向量。隐含向量的维度通常远小于输入数据的维度。
  • 解码器层:将隐含向量重建为与输入数据相同的维度。
  • 输出层:输出重建后的数据,与原始数据进行比较以计算误差。

这种结构的目标是通过优化使输入和输出的差异最小化,常用的误差度量方法有均方误差(MSE)和二元交叉熵(BCE)。通过最小化误差,自动编码器学习到一种有效的压缩表示。

PyTorch 中的自动编码器实现

我们可以通过一个简单的多层感知器(MLP)实现自动编码器。首先定义编码器部分,它会将输入数据压缩到低维空间;然后定义解码器部分,将压缩后的隐含向量解码为原始输入数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import torch
import torch.nn as nn

class AutoEncoder(nn.Module):
def __init__(self):
super(AutoEncoder, self).__init__()
# 编码器部分
self.encoder = nn.Sequential(
nn.Linear(28*28, 128),
nn.ReLU(True),
nn.Linear(128, 64),
nn.ReLU(True),
nn.Linear(64, 12),
nn.ReLU(True),
nn.Linear(12, 3) # 压缩为3维的隐含向量
)
# 解码器部分
self.decoder = nn.Sequential(
nn.Linear(3, 12),
nn.ReLU(True),
nn.Linear(12, 64),
nn.ReLU(True),
nn.Linear(64, 128),
nn.ReLU(True),
nn.Linear(128, 28*28),
nn.Tanh() # 输出范围为 -1 到 1
)

def forward(self, x):
x = self.encoder(x)
x = self.decoder(x)
return x

这个自动编码器首先通过编码器部分将28x28的图像数据压缩为一个3维的隐含向量,然后通过解码器将其还原成原始的28x28图像。值得注意的是,最后一层使用了 Tanh() 激活函数,因为输入的图像数据被标准化到 [-1, 1] 之间,因此输出也需要保持在这个范围内。

卷积自动编码器(Convolutional AutoEncoder)

除了使用全连接层,卷积层(Convolutional Layer)也可以用于实现自动编码器,尤其在处理图像数据时,卷积层能更好地捕捉图像中的局部特征。卷积自动编码器的编码器使用卷积操作来提取图像特征,而解码器则使用反卷积(Transposed Convolution)来恢复图像。下面是一个卷积自动编码器的简单实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class ConvAutoEncoder(nn.Module):
def __init__(self):
super(ConvAutoEncoder, self).__init__()
# 编码器部分
self.encoder = nn.Sequential(
nn.Conv2d(1, 16, 3, stride=2, padding=1), # 输出尺寸:16x14x14
nn.ReLU(True),
nn.Conv2d(16, 8, 3, stride=2, padding=1), # 输出尺寸:8x7x7
nn.ReLU(True),
nn.Conv2d(8, 8, 3, stride=2, padding=1), # 输出尺寸:8x4x4
nn.ReLU(True)
)
# 解码器部分
self.decoder = nn.Sequential(
nn.ConvTranspose2d(8, 8, 3, stride=2, padding=1), # 输出尺寸:8x7x7
nn.ReLU(True),
nn.ConvTranspose2d(8, 16, 3, stride=2, padding=1), # 输出尺寸:16x14x14
nn.ReLU(True),
nn.ConvTranspose2d(16, 1, 3, stride=2, padding=1, output_padding=1), # 输出尺寸:1x28x28
nn.Tanh()
)

def forward(self, x):
x = self.encoder(x)
x = self.decoder(x)
return x

在这里,编码器部分通过卷积层逐步减少图像的空间分辨率,而解码器部分使用 ConvTranspose2d 逐步恢复图像的尺寸。Tanh() 用于将最终输出限制在 [-1, 1] 范围内。

什么是变分自编码器(VAE)?

变分自编码器(Variational Autoencoder, VAE) 是一种生成模型,旨在通过学习数据的潜在表示(latent representation)来生成与训练数据分布相似的新数据。它结合了自编码器(AutoEncoder)概率图模型(Probabilistic Graphical Models)的优点,通过概率推断的方式来优化模型,使得在生成新的数据时具有更高的灵活性。

VAE的主要创新在于它通过引入随机性和概率推断来增强自编码器的生成能力。传统的自编码器无法控制生成数据的类型,也不能进行有效的采样。而VAE通过潜在空间的随机采样和隐含变量分布的建模,能够从任意的正态分布中生成样本,这使得它能够生成多样化且逼真的数据。

VAE的基本架构

VAE的结构类似于自动编码器,但在潜在变量的处理上进行了扩展。VAE通过概率分布来描述输入数据,并且能够生成新的样本,而不仅仅是重建输入。它主要由两个部分组成:

  1. 编码器(Encoder):将输入数据映射到潜在空间,并输出潜在变量的分布参数(通常为均值 和对数方差 )。这些分布参数描述了潜在变量的概率分布,帮助我们通过采样生成新的数据。
  2. 解码器(Decoder):从潜在空间采样潜在变量,并使用这些采样的隐含表示来重建输入数据。解码器的目标是最大限度地生成与输入数据相似的输出数据。

这个结构使得VAE不仅能重建输入数据,还能通过潜在变量生成新的、未见过的样本。

自编码器与VAE的区别

尽管VAE和自编码器的结构在编码器-解码器部分非常相似,但它们的目标和工作方式有着显著的区别。

  1. 目标和任务不同
    • 自编码器(AutoEncoder) 的目标是学习数据的压缩表示,主要用于特征提取、降维、数据去噪等任务。自编码器通过最小化输入数据与重建数据之间的差异来优化模型。这意味着它更像是一个压缩工具,用于重建与输入数据相似的输出。
    • VAE 的目标是生成新数据。它不仅要重建输入数据,还需要通过潜在变量的分布生成全新的数据。VAE通过概率分布和采样来生成与训练数据分布相似的全新数据样本。
  2. 潜在空间处理方式不同
    • 自编码器 直接将输入数据映射到一个确定的潜在空间向量,因此每个输入对应一个固定的隐含表示。虽然这种方式可以有效地进行数据压缩,但它无法通过潜在向量生成新的数据。
    • VAE 引入了概率建模。VAE不再直接输出确定的隐含表示,而是输出潜在变量的分布参数(均值 和对数方差 )。通过这些分布参数,VAE可以从潜在空间中采样生成隐含表示。这种采样过程为生成新数据提供了随机性和多样性。
  3. 潜在变量的生成方式不同
    • 自编码器 中的潜在空间是通过训练数据学到的固定空间,因此它只能压缩和重建训练数据,无法生成新的样本。
    • VAE 通过在训练过程中学到的潜在分布来生成隐含向量 ,并且通过采样从潜在空间中生成新样本。这使得VAE能够在不同的潜在变量范围内生成多种数据样本,不再局限于仅重建训练数据。

VAE的数学基础

潜在变量模型

VAE假设数据 由潜在变量 生成,且 服从先验分布 。生成过程可以表示为:

其中: - 是潜在变量的先验分布,通常选择标准正态分布 。 - 是生成分布,表示给定 时生成 的概率。

直接计算 的后验分布 通常是不可行的,因此VAE引入了变分推断,通过引入一个近似后验分布 来近似 。这个近似分布通常也是一个高斯分布,其均值和方差由编码器输出:

这里, 分别是潜在变量 的均值和方差,它们是通过编码器网络从输入数据 中学习到的。通过这种方式,VAE不再直接求解 ,而是通过近似分布 进行推断。

证据下界(ELBO)

VAE的目标是最大化证据下界(ELBO):

其中: - 第一项 是重建误差,衡量生成数据与真实输入数据之间的相似性。它表示解码器从潜在变量 中生成数据的质量。 - 第二项 是KL散度,衡量近似后验分布与先验分布的差异。通过最小化KL散度,我们可以确保潜在变量的分布接近于标准正态分布。

VAE的目标是通过优化ELBO,使重建误差最小化,同时让潜在变量的分布尽可能接近标准正态分布。另外ELBO也可以如下推导:

由此可以推导出

变分推断的步骤

通过最大化ELBO,我们可以将近似后验分布 逼近真实的后验分布 。整个推断过程可以总结为以下几步:

  1. 定义近似后验分布:首先,我们通过编码器从输入数据 中学习出潜在变量的近似后验分布 。这一步的输出是潜在变量 的均值 和方差 ,或者常见的是对数方差

  2. 采样潜在变量 :为了进行反向传播,我们需要对潜在变量 进行采样。然而,采样操作本质上是一个非确定性过程,无法进行梯度传递。为了解决这个问题,VAE引入了重参数化技巧(Reparameterization Trick)。具体来说,我们将潜在变量 表示为均值和标准差的函数,并加入一个标准正态分布的随机噪声

    这种方法将采样操作分解为可微的部分()和非可微的噪声部分(),从而允许对采样过程进行梯度计算和反向传播。

  3. 计算ELBO:接下来,我们计算ELBO的两部分:

    • 使用采样的潜在变量 通过解码器生成数据,并计算重建误差,即
    • 计算KL散度 ,衡量近似后验分布与先验分布之间的距离。
  4. 优化模型:通过最大化ELBO,模型同时优化解码器的重建能力以及潜在变量的分布逼近标准正态分布的程度。这可以通过标准的反向传播和梯度下降法来完成。

重参数化技巧(Reparameterization Trick)

为什么需要重参数化?

在VAE中,潜在变量 是通过采样得到的,即 。直接对采样过程进行反向传播会导致梯度无法传递,因为采样过程是一个随机且不可微的操作。为了解决这个问题,引入了重参数化技巧,将采样过程转化为一个可微的函数。

重参数化的数学表达

重参数化将随机变量 表示为确定性函数与独立噪声变量 的组合:

其中: - 是编码器输出的均值。 - 是编码器输出的标准差。 - 是从标准正态分布中采样的噪声。 - 表示逐元素相乘。

通过这种方式, 的随机性被隔离在 中,而模型参数 影响的是确定性部分,从而使得梯度能够通过 传播。

重参数化的优势

  1. 梯度可传递:由于 是一个确定性函数的输出,梯度可以通过 传递回编码器的参数。
  2. 优化效率:减少了梯度估计的方差,提高了训练的稳定性和效率。
  3. 灵活性:适用于多种分布,尤其是高斯分布,易于实现和扩展。

VAE的具体实现

下面通过一个简单的PyTorch实现,展示VAE的编码器、重参数化和解码器的具体操作。

定义编码器和解码器

编码器将输入数据 映射到潜在空间,输出潜在变量的均值 和对数方差 。通过以下步骤实现:

  1. 输入数据:输入数据 (如28x28的MNIST图像)被展平成一个784维的向量。
  2. 隐藏层:通过线性变换和ReLU激活函数,提取数据的特征。
  3. 输出层:通过两个独立的线性层分别输出潜在变量的均值 和对数方差

解码器根据潜在变量 重建输入数据,通过以下步骤实现:

  1. 隐藏层:通过线性变换和ReLU激活函数,提取潜在变量的特征。
  2. 输出层:通过线性变换和Sigmoid激活函数,生成重建后的数据 ,确保输出在范围内。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import torch
import torch.nn as nn
import torch.nn.functional as F

# 编码器网络
class Encoder(nn.Module):
def __init__(self, input_dim, hidden_dim, latent_dim):
super(Encoder, self).__init__()
self.fc1 = nn.Linear(input_dim, hidden_dim)
self.fc_mu = nn.Linear(hidden_dim, latent_dim) # 输出均值
self.fc_logvar = nn.Linear(hidden_dim, latent_dim) # 输出对数方差

def forward(self, x):
h = F.relu(self.fc1(x))
mu = self.fc_mu(h)
logvar = self.fc_logvar(h)
return mu, logvar

# 解码器网络
class Decoder(nn.Module):
def __init__(self, latent_dim, hidden_dim, output_dim):
super(Decoder, self).__init__()
self.fc3 = nn.Linear(latent_dim, hidden_dim)
self.fc4 = nn.Linear(hidden_dim, output_dim)

def forward(self, z):
h = F.relu(self.fc3(z))
return torch.sigmoid(self.fc4(h))

定义VAE模型

通过重参数化技巧,将随机采样过程转化为确定性函数与噪声变量的组合,使得梯度能够通过采样过程传播。步骤为:

  • 计算标准差,确保标准差为正。
  • 采样噪声,从标准正态分布中采样。
  • 生成潜在变量,实现了可微分的采样过程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class VAE(nn.Module):
def __init__(self, input_dim=784, hidden_dim=400, latent_dim=20):
super(VAE, self).__init__()
self.encoder = Encoder(input_dim, hidden_dim, latent_dim)
self.decoder = Decoder(latent_dim, hidden_dim, input_dim)

def reparameterize(self, mu, logvar):
"""
重参数化技巧:
z = mu + sigma * epsilon
其中 epsilon ~ N(0, 1)
"""
std = torch.exp(0.5 * logvar) # 计算标准差
eps = torch.randn_like(std) # 从标准正态分布采样 epsilon
return mu + std * eps # 生成潜在变量 z

def forward(self, x):
mu, logvar = self.encoder(x) # 编码器输出均值和对数方差
z = self.reparameterize(mu, logvar) # 重参数化生成 z
recon_x = self.decoder(z) # 解码器重建输入
return recon_x, mu, logvar

定义损失函数

VAE的损失函数包括重建误差和KL散度:

  1. 重建误差(BCE):衡量重建数据 与原始数据 之间的差异,采用二元交叉熵。
  2. KL散度(KLD):衡量近似后验分布 与先验分布 的差异,鼓励潜在变量分布接近先验分布。
1
2
3
4
5
6
7
8
def loss_function(recon_x, x, mu, logvar):
"""
VAE的损失函数包括重建误差和KL散度
"""
BCE = F.binary_cross_entropy(recon_x, x, reduction='sum') # 重建误差
# KL散度
KLD = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp())
return BCE + KLD

训练过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def train_vae(model, dataloader, optimizer, epochs=10):
model.train()
for epoch in range(1, epochs + 1):
train_loss = 0
for batch_idx, (data, _) in enumerate(dataloader):
data = data.view(-1, 784) # 展平图像
optimizer.zero_grad()
recon_batch, mu, logvar = model(data)
loss = loss_function(recon_batch, data, mu, logvar)
loss.backward()
train_loss += loss.item()
optimizer.step()

print(f'Epoch {epoch}, Average loss: {train_loss / len(dataloader.dataset):.4f}')

测试过程

1
2
3
4
5
6
7
8
9
10
def test_vae(model, dataloader):
model.eval()
test_loss = 0
with torch.no_grad():
for data, _ in dataloader:
data = data.view(-1, 784)
recon_batch, mu, logvar = model(data)
test_loss += loss_function(recon_batch, data, mu, logvar).item()

print(f'Test set loss: {test_loss / len(dataloader.dataset):.4f}')

主函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from torch.utils.data import DataLoader
from torchvision import datasets, transforms

def main():
# 数据加载与预处理
transform = transforms.ToTensor()
train_dataset = datasets.MNIST('./data', train=True, download=True, transform=transform)
test_dataset = datasets.MNIST('./data', train=False, download=True, transform=transform)
train_loader = DataLoader(train_dataset, batch_size=128, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=128, shuffle=False)

# 模型初始化
model = VAE()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)

# 训练与测试
for epoch in range(1, 11):
train_vae(model, train_loader, optimizer, epochs=1)
test_vae(model, test_loader)

if __name__ == "__main__":
main()
  • Post title:变分自编码器(VAE)详解
  • Post author:Chen Kai
  • Create time:2024-02-11 18:00:00
  • Post link:https://www.chenk.top/变分自编码器(VAE)详解/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.
 Comments