【转】变分自编码器的程序实现与对应原理

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的模型大致是这样的:
【转】变分自编码器的程序实现与对应原理_第1张图片

# 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πσ2 1exp(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(PQ)=P(i)lnQ(i)P(i)
这里的KL散度是计算了正态分布和标准正态分布之间的关系。

你可能感兴趣的:(note)