一、AE 自编码器
自编码器模型结构图
编码器网络可以将原始高维网络转换为潜在的低维代码
解码器网络可以从低维代码中恢复原始数据,并且可能具有越来越大的输出层
自编码器针对从代码重建数据进行了显式优化。一个好的中间表示不仅可以捕获潜在变量,而且有利于完整的解压缩过程。
变分编码器和自动编码器的区别就在于:传统自动编码器的隐变量z的分布是不知道的,因此我们无法采样得到新的z,也就无法通过解码器得到新的x。
AE的缺点:映射空间不连续,无规则,无界
VAE将每组数据编码为一个分布
二、AEs 正则自编码器
1. 去噪自编码器
在输入数据时加入噪声,强化特征提取能力
2. 稀疏自编码器
dropout
3. 对抗式自编码器
与GAN网络结合
三、VAE 变分自编码器
变分自编码器模型结构图
1. 数学知识
1.1 贝叶斯公式(Bayes Rule)
公式表述为:
p(z∣x)=p(x)p(x,z)=p(x)p(x∣z)p(z)(1)
1.2 KL散度
K−L 散度又被称为相对熵(relative entropy),是对两个概率分布间差异的非对称性度量。
假设p(x) ,q(x)是随机变量上的两个概率分布,
在离散随机变量的情况下,相对熵的定义为:
KL((p(x)∣∣q(x)))=∑p(x)logq(x)p(x)(2)
在连续随机变量的情况下,相对熵的定义为:
KL((p(x)∣∣q(x)))=∫p(x)logq(x)p(x)dx(3)
1.3 EM算法
EM算法(期望最大算法)是一种迭代算法,用于含有隐变量的概率参数模型的最大似然估计或极大后验概率估计。具体思想如下:
EM算法的核心思想非常简单,分为两步:Expection-Step和Maximization-Step
E-Step主要通过观察数据和现有模型来估计参数,然后用这个估计的参数值来计算似然函数的期望值
M-Step是寻找似然函数最大化时对应的参数。由于算法会保证在每次迭代之后似然函数都会增加,所以函数最终会收敛。
公式表述为:
lnpθ(x)=KL(q(z)∣∣pθ(z∣x))+∫q(z)lnq(z)pθ(x,z)dz(4)
=KL(q(z)∣∣pθ(z∣x))+Lθ(q,x)
E-step:用来固定θ,求 q(z):
qt+1(z)=argmaxqLθt(q,x)(5)
如果pθ(z∣x)) 可以得出,则 q(z) 就等于pθ(z∣x)) ,如果不能得出,就利用变分推断近似估计 q(z)
M-step:用来固定 q(z) ,求 θ:
θt+1=argmaxθLθ(qt+1,x)(6)
1.4 变分推断
参数估计:根据样本中提供的相关信息,对总体分布中的未知参数z进行估值
使用贝叶斯估计对未知参数估值:
p(z∣x)=p(x)p(x,z)=p(x)p(x∣z)p(z)(上述公式1)
其中, p(x∣z) 为极大似然估计, p(z) 为最大后验估计。然而贝叶斯估计中的分母 p(x) 一般得不出结果,只能找一个与之相似的函数 $q(z) \approx p(z|x) $ 代替,并通过K−L散度求出函数间的近似程度。
目标函数:
minKL((q(z)∣∣p(z∣x)))(2)
则:
KL((q(z)∣∣p(z∣x)))=∫q(z)logp(z∣x)q(z)dz=∫q(z)logp(x,z)q(z)dz+lnp(x)(3)
lnp(x)=KL((q(z)∣∣p(z∣x)))+∫q(z)logq(z)p(x,z)dz=KL((q(z)∣∣p(z∣x)))+L(q)(4)
因为 lnp(x)是与z无关的常量,并不改变,所以要求的目标函数minKL((q(z)∣∣p(z∣x))) 相当于maxL(q) ,而变分贝叶斯学习可以通过 q(z) 的迭代实现 L(q) 的最大化,即ELBO变分下界,则:
maxL(q)=∫q(z)logp(x,z)dz−∫q(z)logq(z)dz(5)
平均场理论:变分分布q(z)可以因式分解为M个互不相交的部分
q(z)=i=I∏Mqi(6)
则:
ELBO=maxL(q)=∫q(z)logq(z)p(x,z)dz(7)
=∫zi=1∏Mqi[logp(x,z)−logk=1∏Mqk]dz
=∫zqj[i=j∏Mqilogp(x,z)]dz−∫zi=0∏Mqik=1∑Mlogqkdz
分别对➖左右进行计算
Left=∫zqj[i=j∏Mqilogp(x,z)]dz(8)
=∫zjqj[∫zi=ji=j∏qilogp(x,z)dzi=j]dzj
=∫zjqjEi=j[logp(x,z)]dzj
Right=∫zi=0∏Mqik=1∑Mlogqkdz+C1(9)
=∫zjqjlogqjdzji=j∏∫ziqidzi+C1
=∫zjqjlogqjdzj+C1
其中:C1=−∫z∏i=0Mqi∑i=jlogqjdz
同时,令logp~(x,zj)=Ei=j[logp(x,z)]+C2 (C2用于归一化,等式左边对一个概率取对数,因此需要保证概率的性质)
则ELBO最终化为:
ELBO=∫zjqjEi=j[logp(x,z)]dzj−∫zjqjlogqjdzj−C1(10)
=∫zjqjlogqjp~(x,zj)dzj+C3
=−KL(qj∣∣p~(x,zj))+C3
则当qj→p~(x,zj)时,ELBO最大,即L(q)取最大值
logqj(zj)=logp~(x,zj)(11)
=Ei=j[logp(x,z)]+C2
则:
qj(zj)=exp(Ei=j[logp(x,z)]+C2)(12)
∫zjqj(zj)dzj=exp(C2)∫zjexp(Ei=j[logp(x,z)])dzj
可以求出:
C2=log∫zjexp(Ei=j[logp(x,z)])dzj1(13)
返回带入公式12,得出:
qj(zj)=∫zjexp(Ei=j[logp(x,z)])dzjexp(Ei=j[logp(x,z)])(14)
2. 公式推导
参数 | 含义 | 数学表示 | 含义 |
---|---|---|---|
x | 观测数据 | qϕ(z∣x)/qϕz | 使用变分参数 ϕ 来近似真实后验 pθ(z∣x) |
z | 隐变量 | pθ(z∣x) | 给定数据 x 后隐变量 z 的真实后验分布 |
ϕ | 变分参数(近似后验分布的参数) | pθ(x,z) | 生成数据 x 和隐变量 z 的联合概率分布 |
θ | 生成模型的参数 | pθ(x∣z) | 给定隐变量 z 后数据 x 的条件概率分布 |
在变分自编码器(VAE)的推导过程中,我们使用变分推断来近似真实后验分布,并且通过优化目标函数来学习生成模型的参数。
推导过程
-
目标:最大化数据的对数似然
变分自编码器的目标是最大化观测数据 x 的对数似然,即:
logpθ(x)(1)
由于计算 pθ(x) 直接计算非常困难,我们通过引入隐变量 z 和变分推断来简化。 -
引入隐变量
根据隐变量的定义,我们可以将对数似然分解为:
logpθ(x)=log∫zpθ(x,z)dz(2) -
应用变分下界(ELBO)
为了优化这个目标,我们引入变分分布 qϕ(z∣x) 来近似真实后验分布 pθ(z∣x)。通过引入变分分布,我们可以使用变分下界(Evidence Lower Bound, ELBO)来逼近对数似然。ELBO 的推导如下:
logpθ(x)=log∫zpθ(x,z)dz(3)
引入变分分布 qϕ(z∣x):
logpθ(x)=log∫zqϕ(z∣x)qϕ(z∣x)pθ(x,z)dz(4)使用对数的变换性质:
logpθ(x)=log[∫zqϕ(z∣x)qϕ(z∣x)pθ(x,z)dz]应用对数-期望的变换:
logpθ(x)≥Eqϕ(z∣x)[logpθ(x,z)−logqϕ(z∣x)](5)这个期望值被称为变分下界(ELBO):
ELBO=Eqϕ(z∣x)[logpθ(x,z)−logqϕ(z∣x)](6)
其中:
ELBO=Eqϕ(z∣x)[logpθ(x∣z)]−KL(qϕ(z∣x)∣∣pθ(z))(7)
这里,KL 散度(Kullback-Leibler divergence)是:
KL(qϕ(z∣x)∣∣pθ(z))=∫zqϕ(z∣x)logpθ(z)qϕ(z∣x)dz(8) -
优化目标
最终的目标是最大化 ELBO,从而间接最大化对数似然。通过优化变分参数 ϕ 和生成参数 θ,使得 ELBO 最大化,即可获得最优的模型参数。
-
总结
- 近似后验 qϕ(z∣x):通过变分推断来近似真实后验。
- 真实后验 pθ(z∣x):目标是通过优化得到近似。
- 生成模型 pθ(x,z):模型定义生成数据的过程。
- 条件概率分布 pθ(x∣z):表示给定隐变量 z 的情况下数据 x 的分布。
VAE 的关键在于利用 ELBO 来进行模型优化,通过最大化 ELBO 来逼近真实对数似然,从而获得高效的生成模型。
以下为基于pytorch的代码实现:
vae.py
import torch
from torch import nn
import torch.nn.functional as F
class VAE(nn.Module):
def __init__(self):
super(VAE, self).__init__()
# 编码器所用的结构
self.fc1 = nn.Linear(784, 200)
self.fc2_mu = nn.Linear(200, 20) # 用于生成高斯分布的均值
self.fc2_log_std = nn.Linear(200, 20) # 用于生成高斯分布的方差,且为方便计算默认方差是经过log函数处理的。
# 解码器所用的结构
self.fc3 = nn.Linear(20, 200)
self.fc4 = nn.Linear(200, 784)
def encoder(self, x):
h1 = F.relu(self.fc1(x))
mu = self.fc2_mu(h1) # 生成均值
logvar = self.fc2_log_std(h1) # 生成经过log处理的方差
return mu, logvar
def decoder(self, z):
h3 = F.relu(self.fc3(z))
recon = torch.sigmoid(self.fc4(h3)) # 之所以用sigmoid是因为本例用到的图像默认像素值为0-1之间。
return recon
def reparametrize(self, mu, logvar):
var = torch.exp(logvar) # 因为生成的方差是经过log处理的,所以真正要用到方差的时候要再把它经过exp处理一下。
eps = torch.randn_like(var) # 在标准正态分布中采样
z = mu + eps * var # 获得抽取的z
return z
def forward(self, x):
mu, logvar = self.encoder(x)
z = self.reparametrize(mu, logvar)
recon = self.decoder(z)
return recon, mu, logvar # 返回重构的图,均值,log后的方差
def loss_function(self, recon, x, mu, logvar):
recon_loss = F.mse_loss(recon, x, reduction="sum")
kl_loss = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp())
loss = recon_loss + kl_loss
return loss
vae_main.py
import torch
from torch import optim
from torch.autograd import Variable
from torch.utils.data import DataLoader
import torch.nn.functional as F
from torchvision import transforms
from torchvision.utils import save_image
from torchvision.datasets import MNIST
import os
import datetime
from vae import VAE
if not os.path.exists('./vae_img'):
os.mkdir('./vae_img')
def to_img(x):
x = x.clamp(0, 1) # torch.clamp(input,min,max) 把输入的张量加紧到指定区间内
x = x.view(x.size(0), 1, 28, 28) # batch,channel,w,h
return x
num_epochs = 300
batch_size = 128
img_transform = transforms.Compose([
transforms.ToTensor()
# transforms.Normalize((0.5,0.5,0.5),(0.5,0.5,0.5))
])
dataset = MNIST('./data', transform=img_transform, download=True)
datalodader = DataLoader(dataset, batch_size=batch_size, shuffle=True)
start_time = datetime.datetime.now()
model = VAE()
if torch.cuda.is_available():
print('cuda is ok!')
model = model.to('cuda')
else:
print('cuda is no!')
# loss_function = VAE.loss_function
optimizer = optim.Adam(model.parameters(), lr=1e-4)
for epoch in range(num_epochs):
model.train()
train_loss = 0
for batch_idx, data in enumerate(datalodader):
img, _ = data
img = img.view(img.size(0), -1) # 把图像拉平
img = Variable(
img) # tensor不能求导,variable能(其包含三个参数,data:存tensor数据,grad:保留data的梯度,grad_fn:指向function对象,用于反向传播的梯度计算)但我印象中好像tensor可以求梯度 见13讲
img = (img.cuda() if torch.cuda.is_available() else img)
optimizer.zero_grad()
recon_batch, mu, logvar = model(img)
# 计算损失函数
recon_loss = F.mse_loss(recon_batch, img, reduction="sum") # use "mean" may have a bad effect on gradients
kl_loss = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp())
# kl_loss = torch.sum(kl_loss)
loss = recon_loss + kl_loss
# loss = VAE.loss_function(recon_batch, img, mu, logvar)
loss.backward()
train_loss += loss.item()
optimizer.step()
# if batch_idx % 100 == 0:
# end_time = datetime.datetime.now()
# print('Train Epoch: {} [{}/{}({:.0f}%] Loss:{:.6f} time:{:.2f}s'.format(
# epoch,
# batch_idx * len(img),
# len(datalodader.dataset),
# (batch_idx * len(img)) / (len(datalodader.dataset)),
# loss.item() / len(img),
# (end_time - start_time).seconds
# ))
print('====> Epoch: {} Average loss: {:.4f}'.format(
epoch, train_loss / len(datalodader.dataset)
))
if epoch % 2 == 0:
# 生成图像
if torch.cuda.is_available():
device = 'cuda'
else:
device = 'cpu'
z = torch.randn(batch_size, 20).to(device)
out = model.decoder(z).view(-1, 1, 28, 28)
# print(out)
# print(type(out))
save_image(out, './vae_img/sample-{}.png'.format(epoch))
# 重构图像
# recon_batch = model.encoder(img).to(device)
save = to_img(recon_batch.cpu().data)
# print(save)
# print(type(save))
save_image(save, './vae_img/image_{}.png'.format(epoch))
torch.save(model.state_dict(), './vae_weight/vae.pth')
实现结果如下:
image.png(原生) 对比 sample.png(生成)