VAE原理
我们知道,对于生成模型而言,主流的理论模型可以分为隐马尔可夫模型HMM、朴素贝叶斯模型NB和高斯混合模型GMM,而VAE的理论基础就是高斯混合模型。
什么是高斯混合模型呢?就是说,任何一个数据的分布,都可以看作是若干高斯分布的叠加。
如图所示,上面黑色线即为高斯混合分布的例子,如果把该条线拆分可获得若干条浅蓝色曲线(高斯分布)的叠加。有趣的是,当拆分数量达到512时,其叠加的分布相对于原始分布而言误差就非常小了。
然后,我们可以利用这一理论模型去考虑如何给数据进行编码。一种最直接的思路是,直接用每一组高斯分布的参数作为一个编码值实现编码。
这里m表示每一条高斯分布的曲线,每采样一个m,则会获得对应的高斯分布,对于这条曲线的分布函数P(x)而言,它可以表示为以下公式:
上述的这种编码方式是非常简单粗暴的,它对应的是我们之前提到的离散的、有大量失真区域的编码方式。于是我们需要对目前的编码方式进行改进,使得它成为连续有效的编码。
现在我们的编码规则换成连续的变量z,同时,我们假设z服从正态分布(这里可以假设任意分布,需根据数据来定)
对于每一个分布函数z,都会有对应方差和均值,两者决定了高斯分布的形状和范围。然后,累加所有可能的z就获得了连续状态下的P(x)。
这里P(z)是已知的关于z的高斯分布,而P(x|z)是未知的每个z的高斯分布⭐⭐⭐
实际上我们求解P(x|z)就是求解关于z分布所对应的所有的 和。这通常是一个很难求解的过程,因此可以使用神经网络来建模。
———————————————————⭐⭐⭐————————————————————
构建一个编码器Encoder如下,它可以求出一个数据分布q(x|z),来表示关于x的数据分布,用于推进P(x|z)的求解:
然后,构建一个解码器Decoder如下 ,它可以求解 和两个参数,即等价于求解P(x|z):
关于Encoder这部分内容就比较偏数学了,实际上就是推导公式引入一个新的变量 q(x|z),通过在给定q(x|z)然后优化P(x|z)使其输出值足够高。这样做可以使算法获得更好的性质,更易于求解,同时也导出了VAE的损失函数。总的流程图如下:
———————————————————⭐⭐⭐————————————————————
VAE损失函数
下面,对于要实现VAE模型来说,损失函数是关键,这里简单推导一下损失函数。
损失函数包含重构损失+KL散度两部分
1. KL散度
前面我们引入了和,前者表示VAE模拟出的数据分布情况,后者表示数据真实的分布,接下来,根据两者之间的差异性,可以导出一个KL散度公式。
KL散度标准公式定义如下
针对两种分布情况,,可以简化上述公式如下(复制来的,可以忽略)
以上是KL散度的标准公式,可以不看。
前面我们已经讨论了VAE是关于高斯混和模型的讨论。因此,我们定义是从标准正态分布中采样的,即,那么,
不难看出,p1就是原始数据的真实分布,p2就是VAE模拟出的数据分布。
2. 重构损失
重构损失就是重构结果和原始数据之间的误差,可以用BCE、MSE、L1等等多种损失函数。
总结一下,总的损失函数定义如下:
———————————————————⭐⭐⭐————————————————————
VAE实现
附上python代码(包含中文注释)
import torch
import torchvision
from torch import nn
from torch import optim
import torch.nn.functional as F
from torch.autograd import Variable
from torch.utils.data import DataLoader
from torchvision import transforms
from torchvision.utils import save_image
from torchvision.datasets import MNIST
import os
# 图像转为二维可视化
def to_img(x):
x = x.clamp(0, 1)
x = x.view(x.size(0), 1, 28, 28)
return x
# 定义VAE类
class VAE(nn.Module):
def __init__(self):
super(VAE, self).__init__()
INOUT_num = 784 # 输入(输出)大小
hidden_num1 = 400 # 隐藏层大小
hidden_num2 = 20 # 隐变量大小
self.fc1 = nn.Linear(INOUT_num, hidden_num1) # (编码) 全连接层
self.fc21 = nn.Linear(hidden_num1, hidden_num2) # (编码) 计算 mean
self.fc22 = nn.Linear(hidden_num1, hidden_num2) # (编码) 计算 logvar
self.fc3 = nn.Linear(hidden_num2, hidden_num1) # (解码) 隐藏层
self.fc4 = nn.Linear(hidden_num1, INOUT_num) # (解码) 输出层
def encode(self, x):
# 全连接层
hidden1 = self.fc1(x)
# relu层
h1 = F.relu(hidden1)
# 计算mean
mu = self.fc21(h1)
# 计算var
logvar = self.fc22(h1)
return mu, logvar
def reparametrize(self, mu, logvar):
std = logvar.mul(0.5).exp_() # mul是乘法的意思,然后exp_是求e的次方并修改原数值 所有带"—"都是inplace的 意思就是操作后 原数也会改动
if torch.cuda.is_available():
eps = torch.cuda.FloatTensor(std.size()).normal_() # 在cuda中生成一个std.size()的张量,标准正态分布采样,类型为FloatTensor
else:
eps = torch.FloatTensor(std.size()).normal_() # 生成一个std.size()的张量,正态分布,类型为FloatTensor
eps = Variable(eps) # Variable是torch.autograd中很重要的类。它用来包装Tensor,将Tensor转换为Variable之后,可以装载梯度信息。
repar = eps.mul(std).add_(mu)
return repar
def decode(self, z):
# 隐藏层
hidden2 = self.fc3(z)
# relu层
h3 = F.relu(hidden2)
# 隐藏层
hidden3 = self.fc4(h3)
# sigmoid层
output = F.sigmoid(hidden3)
return output
def forward(self, x):
mu, logvar = self.encode(x) # 编码
z = self.reparametrize(mu, logvar) # 重新参数化成正态分布
decodez = self.decode(z) # 解码
return decodez, mu, logvar
def loss_function(recon_x, x, mu, logvar):
"""
recon_x: generating images
x: origin images
mu: latent mean
logvar: latent log variance
"""
BCE = reconstruction_function(recon_x, x) # MSE loss
KLD = -0.5 * torch.sum(logvar + 1 - mu.pow() - logvar.exp())
# KL divergence
return BCE + KLD
if __name__ == '__main__':
# 创建路径
if not os.path.exists('./vae_img'):
os.mkdir('./vae_img')
# VAE参数设置
num_epochs = 30
batch_size = 128
learning_rate = 1e-3
# 定义数据格式
img_transform = transforms.Compose([
transforms.ToTensor() # 将原始的PILImage格式或者numpy.array格式的数据格式化为可被pytorch快速处理的张量类型。
# transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
]) # orchvision.transforms是pytorch中的图像预处理包。一般用Compose把多个步骤整合到一起:
# 加载数据
dataset = MNIST('./data', transform=img_transform, download=True)
dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=True)
model = VAE() # 实例化VAE
if torch.cuda.is_available():
model.cuda()
reconstruction_function = nn.MSELoss(size_average=False) # 定义损失函数,可修改其他
optimizer = optim.Adam(model.parameters(), lr = learning_rate)
#开始训练
for epoch in range(num_epochs):
model.train()
train_loss = 0
for batch_idx, data in enumerate(dataloader):
img, _ = data
img = img.view(img.size(0), -1)
img = Variable(img)
if torch.cuda.is_available():
img = img.cuda()
optimizer.zero_grad()
recon_batch, mu, logvar = model(img)
loss = loss_function(recon_batch, img, mu, logvar)
loss.backward()
train_loss += loss.item()
optimizer.step()
if batch_idx % 100 == 0:
print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
epoch,
batch_idx * len(img),
len(dataloader.dataset), 100. * batch_idx / len(dataloader),
loss.item() / len(img)))
print('====> Epoch: {} Average loss: {:.4f}'.format(
epoch, train_loss / len(dataloader.dataset)))
if epoch % 10 == 0:
save = to_img(recon_batch.cpu().data)
save_image(save, './vae_img/image_{}.png'.format(epoch))
torch.save(model.state_dict(), './vae.pth')
Encoder这部分的内容是VAE最为巧妙的地方,是灵魂所在,感兴趣的小伙伴可以深入探究下,附上相关链接:【学习笔记】生成模型——变分自编码器。以上内容如有错误,欢迎指出积极讨论!