https://www.colabug.com/3704322.html
本文是结合博客 变分自编码器(一):原来是这么一回事 和变分自编码VAE的PyTorch实现 VAE in PyTorch ,对VAE进行理解。
import os
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision
from torchvision import transforms
from torchvision.utils import save_image
# Device configuration
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# Create a directory if not exists
sample_dir = 'samples'
if not os.path.exists(sample_dir):
os.makedirs(sample_dir)
导入PyTorch和TorchVision,并根据实际情形看使用GPU还是CPU。建立samples文件夹放置图片。
image_size = 784 # 这个是输入图片的长宽像素之积,MNIST图片是28*28的,所以是784
h_dim = 400 # 计算均值方差的神经网络是一个含有两个全连接层和一个ReLU层的网络。这里是第一个全连接层的输出神经元个数
z_dim = 20 # 这里是第二个全连接层的输出神经元个数
num_epochs = 15 # 迭代次数
batch_size = 128 # 批处理样本时每批的个数
learning_rate = 1e-3 # 学习速率
# MNIST dataset
dataset = torchvision.datasets.MNIST(root='../../data',
train=True,
transform=transforms.ToTensor(),
download=True)
# Data loader
data_loader = torch.utils.data.DataLoader(dataset=dataset,
batch_size=batch_size,
shuffle=True)
# VAE model
class VAE(nn.Module):
def __init__(self, image_size=784, h_dim=400, z_dim=20):
super(VAE, self).__init__()
self.fc1 = nn.Linear(image_size, h_dim)
self.fc2 = nn.Linear(h_dim, z_dim)
self.fc3 = nn.Linear(h_dim, z_dim)
self.fc4 = nn.Linear(z_dim, h_dim)
self.fc5 = nn.Linear(h_dim, image_size)
def encode(self, x): # 编码器
h = F.relu(self.fc1(x)) # 计算第一个全连接层,然后用ReLU整流
return self.fc2(h), self.fc3(h) # 对均值和方差(实际是$logsigma^2$)都计算第二个全连接层
def reparameterize(self, mu, log_var): # 重参数化
std = torch.exp(log_var/2) # 这一步是将方差的对数转换为标准差。
eps = torch.randn_like(std) # 生成一个跟std同样大小的取自标准正态分布的随机数。
return mu + eps * std # 根据标准正态分布的采样得到之前正态分布中的采样。*对应元素乘
def decode(self, z): # 解码器,也是两个全连接层,中间有个ReLU整流,最后有个Sigmoid层。
h = F.relu(self.fc4(z))
return F.sigmoid(self.fc5(h))
def forward(self, x): # 前向计算
mu, log_var = self.encode(x) # 对输入变量进行编码,得到隐变量所满足的正态分布的均值和方差
z = self.reparameterize(mu, log_var) # 重参数化
x_reconst = self.decode(z) # 解码器
return x_reconst, mu, log_var
有几个注意点:
(1) 对每个输入的x都生成一个正态分布。
(2) 计算方差的神经网络实际计算的不是方差,而是它的对数,即 l o g ( s i g m a 2 ) log(sigma^2) log(sigma2)。
为什么是方差的对数?
(3) 重参数reparameterize的作用:得到隐参量z的正态分布后,还要对其采样得到离散值。但这个“采样”操作是不可导的,因此没法应用于梯度下降的训练过程。而根据公式:
1 2 π σ 2 exp ( − ( z − μ ) 2 2 σ 2 ) d z = 1 2 π exp [ − 1 2 ( z − μ σ ) 2 ] d ( z − μ σ ) \frac{1}{\sqrt{2\pi \sigma^2}}\exp(-\frac{(z-\mu)^2}{2\sigma^2})dz = \frac{1}{\sqrt{2\pi}}\exp[-\frac{1}{2}(\frac{z-\mu}{\sigma})^2]d(\frac{z-\mu}{\sigma}) 2πσ21exp(−2σ2(z−μ)2)dz=2π1exp[−21(σz−μ)2]d(σz−μ)
对 d ( z − μ σ ) d(\frac{z-\mu}{\sigma}) d(σz−μ)求导,得到 1 σ d z \frac{1}{\sigma}dz σ1dz,对了!!!
可以看出变量 ϵ = z − μ σ \epsilon=\frac{z-\mu}{\sigma} ϵ=σz−μ服从标准正态分布。因此,从原正态分布中采样一个 z z z,就等同于从标准正态分布中采样一个 ϵ \epsilon ϵ,然后让 z = μ + ϵ ∗ σ z=\mu+\epsilon*\sigma z=μ+ϵ∗σ。这样,就可以不用把采样这个操作加入到反向传播中,而只需要将结果放进来即可。
VAE中参与反向传播的是哪些部分?
model = VAE().to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
# Start training
for epoch in range(num_epochs):
for i, (x, _) in enumerate(data_loader):
# Forward pass
x = x.to(device).view(-1, image_size)
x_reconst, mu, log_var = model(x) # 前向计算,得到新的变量x,以及正态分布的均值和方差
# Compute reconstruction loss and kl divergence
# For KL divergence, see Appendix B in VAE paper or http://yunjey47.tistory.com/43
reconst_loss = F.binary_cross_entropy(x_reconst, x, size_average=False) # 计算输入变量和新变量之间的二进制交叉熵
kl_div = - 0.5 * torch.sum(1 + log_var - mu.pow(2) - log_var.exp()) # 计算KL散度
# Backprop and optimize
loss = reconst_loss + kl_div # 总损失等于交叉熵和KL散度之和
optimizer.zero_grad() # 梯度置零
loss.backward() # 反向传播
optimizer.step() # 步进
if (i+1) % 10 == 0:
print ("Epoch[{}/{}], Step [{}/{}], Reconst Loss: {:.4f}, KL Div: {:.4f}"
.format(epoch+1, num_epochs, i+1, len(data_loader), reconst_loss.item(), kl_div.item()))
with torch.no_grad():
# Save the sampled images
z = torch.randn(batch_size, z_dim).to(device)
out = model.decode(z).view(-1, 1, 28, 28)
save_image(out, os.path.join(sample_dir, 'sampled-{}.png'.format(epoch+1)))
# Save the reconstructed images
out, _, _ = model(x)
x_concat = torch.cat([x.view(-1, 1, 28, 28), out.view(-1, 1, 28, 28)], dim=3)
save_image(x_concat, os.path.join(sample_dir, 'reconst-{}.png'.format(epoch+1)))
注意:
总损失函数等于交叉熵和KL散度。KL散度的计算公式是:
D K L ( P ∣ ∣ Q ) = ∑ P ( i ) l n P ( i ) Q ( i ) D_{KL}(P||Q)=\sum{P(i) ln \frac{P(i)}{Q(i)}} DKL(P∣∣Q)=∑P(i)lnQ(i)P(i)
这里的KL散度是计算了正态分布和标准正态分布之间的关系。