1. VAE 变分自编码的网络板块
食用前闲聊:
我个人感觉,VAE 从概率分布的 潜空间 上去处理图片。不严谨的说,GAN在细节的编辑和还原能力上可能相对于VAE来说强很多。俩都是 无监督学习哈。 VAE 呢,胜在有稳定的输出、可解释性和特征表示能力。或许是处于这个原因又或者是怎么原因(蛐蛐一下,可能是某个大聪明突然爆了个idea,对这个latent 进行高斯加噪和去噪,于是乎就有了 SD。这段纯属自己 yy),被用到了 扩散模型生成板块的核心组件里。
GAN 和 VAE 训练的对比:
VAE 在训练的过程中 更关注于 Encoder 和 Decoder 中间的 Latent 的分布,对最终的成像是有一定程度的容错。
GAN 的输出更关注于重建后的图片整体,毕竟 GAN 的损失有俩嘛。一个是 判别器带来的 损失,一个是 生成器的输入输出的损失,我记得没错我当时自己写的时候用的MSE吧,太久了忘记了哈哈。
1.1 VAE基础:概率生成模型的理论框架
概念的东西自己去看博客或者知乎懒得说了
理论推导:
- 一文理解变分自编码器(VAE) - 知乎
这篇文章直搜能出,写的太多了属实是看的恼火 - VAE中的KL散度-Kullback Leibler divergence - 知乎
我喜欢看这个,直接推导。 - Building a Variational Autoencoder (VAE) from Scratch in Under 5 Minutes using PyTorch
这个视频我觉得配的图很好,能很好的看明白VAE的潜空间表示什么意思
核心思想与数学基础
变分自编码器(Variational Autoencoder, VAE)是基于变分推断(Variational Inference)的生成模型,通过建立观测数据x与潜在变量z之间的概率映射关系,实现数据生成与特征解耦。其核心目标在于构建参数化的概率生成模型:
关键技术解析
KL散度解析
先验分布P(z)通常假设为标准正态分布N(0,I),KL散度项可解析计算:实际实现时采用logvar代替σ²,避免平方运算的数值不稳定性
# 典型KL损失计算
kl_loss = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp())1.2 VAE网络架构设计
模型结构的配置文件:
if __name__ == '__main__':
# 新的配置结构示例
configs = {
'down': [
('ResnetBlock', [64, False]), # (out_channels, downsample)
('ResnetBlock', [128, True]),
('ResnetBlock', [256, True]),
],
'mid': {
'mu': [256, 48, 1, 1, 0], # Conv2d参数
'sigma': [256, 48, 1, 1, 0] # Conv2d参数
},
'up': [
('UpBlock', [128]), # 上采样块参数 (out_channels)
('Conv2d', [128, 128, 3, 1, 1]),
('UpBlock', [64]),
('Conv2d', [64, 64, 3, 1, 1]),
('Conv2d', [64, 3, 3, 1, 1]),
('Sigmoid', [])
]
}基础模块:
# 新增ResnetBlock模块
class ResnetBlock(nn.Module):
def __init__(self, in_channels, out_channels, downsample=False):
super().__init__()
stride = 2 if downsample else 1
self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1)
self.bn1 = nn.InstanceNorm2d(out_channels, affine=True)
self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1)
self.bn2 = nn.InstanceNorm2d(out_channels, affine=True)
self.relu = nn.ReLU(inplace=True)
self.shortcut = nn.Sequential()
if in_channels != out_channels or downsample:
self.shortcut = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride),
nn.InstanceNorm2d(out_channels)
)
def forward(self, x):
identity = self.shortcut(x)
x = self.relu(self.bn1(self.conv1(x)))
x = self.bn2(self.conv2(x))
x += identity
return self.relu(x)1.2.1 编码器(Encoder)
采用卷积神经网络实现特征提取,典型结构包含:
class Encoder(nn.Module):
def __init__(self, configs):
super().__init__()
layers = []
current_channels = 3 # 默认输入通道
# 解析配置结构
for block_type, params in configs['down']:
if block_type == 'ResnetBlock':
layers.append(ResnetBlock(current_channels, *params))
current_channels = params[0]
elif block_type == 'Downsample':
layers.append(nn.MaxPool2d(*params))
else:
layer = getattr(nn, block_type)(*params)
if 'Conv' in block_type:
current_channels = params[1]
layers.append(layer)
self.layer = nn.Sequential(*layers)
def forward(self, x):
return self.layer(x)1.2.2 概率空间映射
使用1x1卷积实现特征到概率参数的转换:
设计要点:
- 并行输出μ和logvar避免计算冲突
- logvar限制输出范围(-∞, +∞)保证数值有效性
# 生成 latent
def reparameterize(self, mu, sigmoid):
std = torch.exp(0.5*sigmoid)
eps = torch.randn_like(std)
return mu + eps * std1.2.3 解码器(Decoder)
通过转置卷积实现空间上采样:
class Decoder(nn.Module):
def __init__(self, configs):
super().__init__()
layers = []
current_channels = configs['init_channels']
# 解析上采样配置
for block_type, params in configs['up']:
if block_type == 'UpBlock':
out_channels = params[0]
# 转置卷积部分
layers.append(nn.ConvTranspose2d(
current_channels, out_channels,
kernel_size=3, stride=2, padding=1, output_padding=1
))
# layers.append(nn.BatchNorm2d(out_channels))
layers.append(nn.ReLU())
# 后接3x3卷积
layers.append(nn.Conv2d(out_channels, out_channels, 3, padding=1))
layers.append(nn.BatchNorm2d(out_channels))
layers.append(nn.ReLU())
current_channels = out_channels
else:
layer = getattr(nn, block_type)(*params)
layers.append(layer)
self.layer = nn.Sequential(*layers)
# nn.Sigmoid
def forward(self, x):
return self.layer(x)