生成模型

生成模型的概念属于概率统计和机器学习,是指一系列用于随机生成可观测数据的模型。简而言之,我们就是要生成的样本和实际的样本尽可能的相似,其主要功能有两个:

  • 学习一个概率分布
  • 生成数据

自动编码器

自动编码器最开始是一种数据压缩的方法,其特点有:

  • 跟数据的相关程度很高:这意味着自动编码器只能压缩与训练数据相似的数据
  • 有损压缩

现在自动编码器主要应用于:

  • 数据去噪
  • 可视化降维
  • 生成数据

8CzFi.png

如图所示自动编码器结构,其主要由两部分组成:编码器(Encoder)和解码器(Decoder),编码器和解码器都可以是任意的模型,主要使用神经网络模型。输入数据经过神经网络降维到一个编码,接着又通过另外一个神经网络去解码得到一个与输入数据一模一样的生成数据。然后通过比较这两个数据,通过最小化他们之间的差异来训练网络中的参数。

训练完成后,拿出解码器,随机传入一个编码,就能生成一个和原始数据差不多的生成数据。


import os

import torch
from torch.autograd import Variable
from torch import nn
from torch.utils.data import DataLoader

from torchvision.datasets import MNIST
from torchvision import transforms as tfs
from torchvision.utils import save_image

im_tfs = tfs.Compose([
tfs.ToTensor(),
tfs.Normalize(0.5,0.5) # 标准化
])

train_set = MNIST('./data', transform=im_tfs,download=True)
train_data = DataLoader(train_set, batch_size=128, shuffle=True)
# 定义网络
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) # 输出的 code 是 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()
)

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

net = autoencoder().cuda()
x = Variable(torch.randn(1, 28*28)).cuda() # batch size 是 1
code, _ = net(x)
print(code.shape)

criterion = nn.MSELoss(size_average=False)
optimizer = torch.optim.Adam(net.parameters(), lr=1e-3)

def to_img(x):
'''
定义一个函数将最后的结果转换回图片
'''
x = 0.5 * (x + 1.)
x = x.clamp(0, 1)
x = x.view(x.shape[0], 1, 28, 28)
return x
# 开始训练自动编码器
for e in range(100):
for batch in train_data:
im,_=batch
im = im.view(im.shape[0], -1)
im = Variable(im).cuda()
# 前向传播
_, output = net(im)
loss = criterion(output, im) / im.shape[0] # 平均
# 反向传播
optimizer.zero_grad()
loss.backward()
optimizer.step()
print('epoch: {}, Loss: {:.4f}'.format(e + 1, loss.item()))
if (e+1) % 20 == 0: # 每 20 次,将生成的图片保存一下

pic = to_img(output.cpu().data)
print(pic.shape)
if not os.path.exists('./simple_autoencoder'):
os.mkdir('./simple_autoencoder')
save_image(pic, './simple_autoencoder/image_{}.png'.format(e + 1))

import matplotlib.pyplot as plt
from matplotlib import cm
from mpl_toolkits.mplot3d import Axes3D


# 可视化结果
view_data = Variable((train_set.train_data[:200].type(torch.FloatTensor).view(-1, 28*28) / 255. - 0.5) / 0.5).cuda()
encode, _ = net(view_data) # 提取压缩的特征值
encode=encode.cpu()
fig = plt.figure(2)
ax = Axes3D(fig) # 3D 图
# x, y, z 的数据值
X = encode.data[:, 0].numpy()
Y = encode.data[:, 1].numpy()
Z = encode.data[:, 2].numpy()
values = train_set.train_labels[:200].numpy() # 标签值
for x, y, z, s in zip(X, Y, Z, values):
c = cm.rainbow(int(255*s/9)) # 上色
ax.text(x, y, z, s, backgroundcolor=c) # 标位子
ax.set_xlim(X.min(), X.max())
ax.set_ylim(Y.min(), Y.max())
ax.set_zlim(Z.min(), Z.max())


code = Variable(torch.FloatTensor([[1.19, -3.36, 2.06]])).cuda() # 给一个 code 是 (1.19, -3.36, 2.06)
decode = net.decoder(code).cpu()
decode_img = to_img(decode).squeeze()
decode_img = decode_img.data.numpy() * 255
plt.figure(3)
plt.imshow(decode_img.astype('uint8'), cmap='gray') # 生成图片 3

plt.show()
# class conv_autoencoder(nn.Module):
# def __init__(self):
# super(conv_autoencoder, self).__init__()

# self.encoder = nn.Sequential(
# nn.Conv2d(1, 16, 3, stride=3, padding=1), # (b, 16, 10, 10)
# nn.ReLU(True),
# nn.MaxPool2d(2, stride=2), # (b, 16, 5, 5)
# nn.Conv2d(16, 8, 3, stride=2, padding=1), # (b, 8, 3, 3)
# nn.ReLU(True),
# nn.MaxPool2d(2, stride=1) # (b, 8, 2, 2)
# )

# self.decoder = nn.Sequential(
# nn.ConvTranspose2d(8, 16, 3, stride=2), # (b, 16, 5, 5)
# nn.ReLU(True),
# nn.ConvTranspose2d(16, 8, 5, stride=3, padding=1), # (b, 8, 15, 15)
# nn.ReLU(True),
# nn.ConvTranspose2d(8, 1, 2, stride=2, padding=1), # (b, 1, 28, 28)
# nn.Tanh()
# )

# def forward(self, x):
# encode = self.encoder(x)
# decode = self.decoder(encode)
# return encode, decode
# conv_net = conv_autoencoder()
# if torch.cuda.is_available():
# conv_net = conv_net.cuda()
# optimizer = torch.optim.Adam(conv_net.parameters(), lr=1e-3, weight_decay=1e-5)

# # 开始训练自动编码器
# for e in range(40):
# for im, _ in train_data:
# if torch.cuda.is_available():
# im = im.cuda()
# im = Variable(im)
# # 前向传播
# _, output = conv_net(im)
# loss = criterion(output, im) / im.shape[0] # 平均
# # 反向传播
# optimizer.zero_grad()
# loss.backward()
# optimizer.step()

# if (e+1) % 20 == 0: # 每 20 次,将生成的图片保存一下
# print('epoch: {}, Loss: {:.4f}'.format(e+1, loss.data[0]))
# pic = to_img(output.cpu().data)
# if not os.path.exists('./conv_autoencoder'):
# os.mkdir('./conv_autoencoder')
# save_image(pic, './conv_autoencoder/image_{}.png'.format(e+1))

8pM2y.png

8pSjd.png

8pBJQ.png

如图二所示,不同数字通过自动编码器所得编码大概聚集在一起,且通过解编码所得图三大概也能看出是5.

变分自动编码

变分编码器是自动编码器的升级版本,其结构跟自动编码器是类似的,也由编码器和解码器构成。

自动编码器有个问题,就是并不能任意生成图片,因为我们没有办法自己去编码向量,需要通过一张图片输入编码我们才知道得到的编码向量是什么,这时我们就可以通过变分自动编码器来解决这个问题。

其实原理特别简单,只需要在编码过程给它增加一些限制,迫使其生成的编码向量能够粗略的遵循一个标准正态分布,这就是其与一般的自动编码器最大的不同。

这样我们生成一张新图片就很简单了,我们只需要给它一个标准正态分布的随机隐含向量,这样通过解码器就能够生成我们想要的图片,而不需要给它一张原始图片先编码。

综上所述,就是让编码都服从一个分布,这样我们就可以任意取服从该分布的编码。

一般来讲,我们通过 encoder 得到的隐含向量并不是一个标准的正态分布,为了衡量两种分布的相似程度,我们使用 KL divergence,利用其来表示隐含向量与标准正态分布之间差异的 loss,另外一个 loss 仍然使用生成图片与原图片的均方误差来表示。

KL divergence 的公式如下

$$
D{KL} (P || Q) = \sum_{-\infty}^{\infty} p(i) \log \frac{p(i)}{q(i)} dx
$$

$$
D{KL} (P || Q) = \int_{-\infty}^{\infty} p(x) \log \frac{p(x)}{q(x)} dx
$$

重参数

很明显,对于变分自动编码器的输出向量做积分或求和是十分麻烦的,我们不如直接生成两个向量:一个表示均值,一个表示标准差,那么我们所需要的编码向量直接用一个标准正态分布先乘上标准差再加上均值就行了。这时候尽量让均值接近0,标准差接近1,也就不需要再去计算KL divergence。
$$
\begin{aligned}
DKL(P\Vert Q)&=\int_{-\infty}^{\infty}p(x)\log\frac{p(x)}{q(x)}dx\\
&=\int_{-\infty}^{\infty}p(x)\log p(x)-p(x)\log q(x)dx
\end{aligned}
$$

$$
\begin{aligned}
\int_{-\infty}^{\infty}p(x)\log p(x)dx&=\int_{-\infty}^{\infty}p(x)\log (\frac{1}{\sqrt{2\pi}\sigma_1}\exp(-\frac{(x-\mu_1)^2}{2\sigma_1^2}))dx\\
&=-\frac{1}{2}\log(2\pi\sigma_1^2)\int p(x)dx
+\int p(x)(-\frac{(x-\mu_1)^2}{2\sigma_1^2})dx\\
&=-\frac{1}{2}\log(2\pi\sigma_1^2)-\frac{\int p(x)x^2dx-\int p(x)2x\mu_1dx+\int p(x)\mu_1^2dx}{2\sigma_1^2}\\
&=-\frac{1}{2}\log(2\pi\sigma_1^2)-\frac{(\mu_1^2+\sigma_q^2)-(2\mu_1\times \mu_1)+u_1^2}{2\sigma_1^2}\\
&=-\frac{1}{2}[1+\log(2\pi \sigma_1^2)]
\end{aligned}
$$

$$
\begin{aligned}
\int_{-\infty}^{\infty}p(x)\log q(x)dx&=\int_{-\infty}^{\infty}p(x)\log (\frac{1}{\sqrt{2\pi}\sigma_2}\exp(-\frac{(x-\mu_2)^2}{2\sigma_2^2}))dx\\
&=-\frac{1}{2}\log(2\pi\sigma_2^2)\int p(x)dx
+\int p(x)(-\frac{(x-\mu_2)^2}{2\sigma_1^2})dx\\
&=-\frac{1}{2}\log(2\pi\sigma_2^2)-\frac{\int p(x)x^2dx-\int p(x)2x\mu_2dx+\int p(x)\mu_2^2dx}{2\sigma_2^2}\\
&=-\frac{1}{2}\log(2\pi\sigma_2^2)-\frac{(\mu_1^2+\sigma12^2)-(2\mu_2\times \mu_1)+u_2^2}{2\sigma_2^2}\\
&=-\frac{1}{2}\log(2\pi \sigma_2^2)-\frac{\sigma_1^2+(\mu_1-\mu_2)^2}{2\sigma_2^2}
\end{aligned}
$$

$$
\begin{aligned}
D{KL} (P || Q)&=-\frac{1}{2}[1+\log(2\pi \sigma_1^2)]-[-\frac{1}{2}\log(2\pi \sigma_2^2)-\frac{\sigma_1^2+(\mu_1-\mu_2)^2}{2\sigma_2^2}]\\
&=\log\frac{\sigma_2}{\sigma_1}+\frac{\sigma_1^2+(\mu_1-\mu_2)^2}{2\sigma_2^2}-\frac{1}{2}
\end{aligned}
$$

80uoM.png

那么我们的总体loss就等于$MSE+KLD$

reconstruction_function=nn.MSELoss()
def loss_function(recon_x, x, mu, logvar):
"""
recon_x: generating images
x: origin images
mu: latent mean
logvar: latent log variance
"""
MSE = reconstruction_function(recon_x, x)
# kld = 0.5 * sum(1 + log(sigma^2) - mu^2 - sigma^2)
KLD_element = mu.pow(2).add_(logvar.exp()).mul_(-1).add_(1).add_(logvar)
KLD = torch.sum(KLD_element).mul_(-0.5)
# KL divergence
return MSE + KLD

import os

import torch
from torch.autograd import Variable
import torch.nn.functional as F
from torch import nn
from torch.utils.data import DataLoader

from torchvision.datasets import MNIST
from torchvision import transforms as tfs
from torchvision.utils import save_image
im_tfs = tfs.Compose([
tfs.ToTensor(),
tfs.Normalize(0.5,0.5) # 标准化
])

train_set = MNIST('./data', transform=im_tfs)
train_data = DataLoader(train_set, batch_size=128, shuffle=True)
class VAE(nn.Module):
def __init__(self):
super(VAE, self).__init__()

self.fc1 = nn.Linear(784, 400)
self.fc21 = nn.Linear(400, 20) # mean
self.fc22 = nn.Linear(400, 20) # var
self.fc3 = nn.Linear(20, 400)
self.fc4 = nn.Linear(400, 784)

def encode(self, x):
h1 = F.relu(self.fc1(x))
return self.fc21(h1), self.fc22(h1)

def reparametrize(self, mu, logvar):
std = logvar.mul(0.5).exp_()
eps = torch.FloatTensor(std.size()).normal_()
if torch.cuda.is_available():
eps = Variable(eps.cuda())
else:
eps = Variable(eps)
return eps.mul(std).add_(mu)

def decode(self, z):
h3 = F.relu(self.fc3(z))
return F.tanh(self.fc4(h3))

def forward(self, x):
mu, logvar = self.encode(x) # 编码
z = self.reparametrize(mu, logvar) # 重新参数化成正态分布
return self.decode(z), mu, logvar # 解码,同时输出均值方差
net = VAE() # 实例化网络
if torch.cuda.is_available():
net = net.cuda()
x, _ = train_set[0]
x = x.view(x.shape[0], -1)
if torch.cuda.is_available():
x = x.cuda()
x = Variable(x)
_, mu, var = net(x)
print(mu)

reconstruction_function = nn.MSELoss(size_average=False)

def loss_function(recon_x, x, mu, logvar):
"""
recon_x: generating images
x: origin images
mu: latent mean
logvar: latent log variance
"""
MSE = reconstruction_function(recon_x, x)
# loss = 0.5 * sum(1 + log(sigma^2) - mu^2 - sigma^2)
KLD_element = mu.pow(2).add_(logvar.exp()).mul_(-1).add_(1).add_(logvar)
KLD = torch.sum(KLD_element).mul_(-0.5)
# KL divergence
return MSE + KLD

optimizer = torch.optim.Adam(net.parameters(), lr=1e-3)

def to_img(x):
'''
定义一个函数将最后的结果转换回图片
'''
x = 0.5 * (x + 1.)
x = x.clamp(0, 1)
x = x.view(x.shape[0], 1, 28, 28)
return x
for e in range(100):
for im, _ in train_data:
im = im.view(im.shape[0], -1)
im = Variable(im)
if torch.cuda.is_available():
im = im.cuda()
recon_im, mu, logvar = net(im)
loss = loss_function(recon_im, im, mu, logvar) / im.shape[0] # 将 loss 平均
optimizer.zero_grad()
loss.backward()
optimizer.step()
print('epoch: {}, Loss: {:.4f}'.format(e + 1, loss.item()))
if (e + 1) % 20 == 0:

save = to_img(recon_im.cpu().data)
if not os.path.exists('./vae_img'):
os.mkdir('./vae_img')
save_image(save, './vae_img/image_{}.png'.format(e + 1))

x, _ = train_set[0]
x = x.view(x.shape[0], -1)
if torch.cuda.is_available():
x = x.cuda()
x = Variable(x)
_, mu, _ = net(x)
print(mu)

8YQSJ.png

可以明显看出变分自动编码的输出图片比自动编码器输出的图片更清晰,但其仍然使用MESLoss为loss函数,所以生成的图片会有模糊现象。