1.53.Auto-Encoders和Variational AutoEncoders(VAE)
1.53.1.无监督学习
1.53.2.Auto-Encoders
1.53.3.How to Train?
1.53.4.Auto-Encoders的训练
1.53.5.PCA降维和Auto-Encoders降维的比较
1.53.6.Auto-Encoders变种
1.53.6.1.Denoising AutoEncoders,去噪 AutoEncoders
1.53.6.2.Dropout AutoEncoders
1.53.6.3.Adversarial AutoEncoders,对抗AutoEncoders
1.53.6.4.q分布和p分布
1.53.6.5.对 KL(p◎q)的直观理解
1.53.6.6.Maximize Likelihood(极大似然)
1.53.6.7.Minimize KL Divergence (最小化KL散度)
1.53.6.8.在q(z)和p(z)之间怎么计算KL?
1.53.7.Variational AutoEncoders
1.53.8.AutoEncoder的PyTorch实现
1.53.9.Variational AutoEncoders
1.53.10.参考文章
1.53.Auto-Encoders和Variational AutoEncoders(VAE)
1.53.1.无监督学习
无监督学习常见的两种类型是:数据集变换和聚类。
数据集变换,就是创建数据集新的表示算法,与数据的原始表示相比,新的表示可能更容易被人或其他机器学习算法所理解。
常见的应用有降维,就是对许多特征表示的高维数据,找到该数据的一种新方法,用较少的特征就可以概括其重要特征。另一个应用就是找到”构成”数据的各个组成部分,比如对文本文档的关键字提取。
聚类,就是将数据划分成不同的组,每组包含相似的物项。
1.53.2.Auto-Encoders
自编码AutoEncoder是一种无监督学习的算法,他利用反向传播算法,让目标等于输入值。什么意思呢,下面举个例子:
有一个神经网络,它在做的事情是,输入一张图片,通过一个Encoder神经网络,输出一个比较”浓缩的”feature map。之后将这个feature map通过一个Decoder网络,结果又将这张图片还原回去了。
你也可以这么理解,整个Encoder + Decoder是一个神经网络,中间的code只是暂存的数据。
感觉就像是,现在有一锅红糖水,你不停的煮它,最终水都被煮干了,只剩下红糖,这个红糖就是上图的”Code”。然后你再向红糖里面注水、加热,结果又还原回了一锅红糖水。
假设上线的神经网络展开如下图所示,可以看出,图片经过了一个压缩,再解压的工序。当压缩的时候,原有的图片质量被缩减。解压时,用信息量少却包含所有关键信息的文件恢复出了原本的图片。为什么要这样做呢?
因为有时候神经网络要接受大量的输入信息,比如说输入信息是高清图片时,信息量可能高达上千万,让神经网络直接从上千万信息中学习是很吃力的。所以,为何不压缩一下,提取原图片中最具代表性的信息,缩减信息量,再把缩减后的信息带入进行网络学习。这样学习起来就轻松多了。
在无监督学习中,Auto-Encoders的目标是重建自己,它是一个特殊的全连接层,输入和输出的维度是一样的,这样能保证自己能够重建。中间有一个neck(脖子),这样既可以升维也可以降维,这里降到两维的好处是,二维的图片是可视化的,不仅已经降维,而且在空间中还保证了语义的相关性(通过无监督的聚类可以发现)。
1.53.3.How to Train?
1.53.4.Auto-Encoders的训练
下面Loss Function,如果输入是binary的,即01值,那么就用第一种。
如果是实数作为输入,Loss function就是第二种。
②MSE
1.53.5.PCA降维和Auto-Encoders降维的比较
PCA在高维数据中寻找方差最大的方向,只选择方差最大的轴。然而,主成分分析(PCA)的线性性对可提取的特征维度的种类有很大的限制。
PCA V.S. AutoEncoder
同样都是降维,PCA和AutoEncoder谁的效果更好呢?
首先从直觉上分析,PCA本质上是线性的变换,所以它是有局限性的。而AutoEncoder是基于DNN的,由于有activation function的存在,所以可以进行非线性变换,使用范围更广。
下图展示了MNIST数据集分别经过PCA和AutoEncoder 降维再还原 后的效果。第二行是使用AutoEncoder的方法,可以看到几乎没什么太大变化;而第四行的图片很多都变得非常模糊了,说明PCA的效果是不如AutoEncoder的。
Auto-Encoders比PCA降维的效果要好。
1.53.6.Auto-Encoders变种
1.53.6.1.Denoising AutoEncoders,去噪 AutoEncoders
如果只在像素级别的重建,便不能发现一些更加深层次的特征,网路可能会记住一些特征,为了防止这种情况出现,我们可以在原输入图片后加入随机噪声累加到原图片上。
Vincent在2008的论文(http://www.iro.umontreal.ca/~lisa/publications2/index.php/publications/show/217)中提出了AutoEncoder的改良版----dA,论文标题叫”Extracting and Composing Robust Features”,译成中文就是”提取、编码出具有鲁棒性的特征”
首先我们考虑,为什么会产生这样的变种AutoEncoder。如果我们仅仅只是在像素级别对一张图片进行Encode,然后再重建,这样就无法发现更深层次的信息,很有可能会导致网络记住了一些特征。为了防止这种情况产生,我们可以给输入图片加一些噪声,比方说生成和图片同样大小的高斯分布的数据,然后和图像的像素值相加(见下图)。如果这样都能重建原来的图片,意味着这个网络能从这些混乱的信息中发现真正有用的特征,此时的Code才能代表输入图片的"精华"。
关键是,这样胡乱给原始图片加噪声真的科学吗?Vincent从大脑认知角度给了解释。Paper中说到:
人类具有认识被阻挡的破损图像的能力,源于我们高等的联想记忆感受机能。
就是说,我们能以多种形式去记忆(比如图像、声音),所以即便是数据破损丢失,我们也能回想起来。
1.53.6.2.Dropout AutoEncoders
其实这个没什么特别的,平时我们不论是CNN还是RNN几乎都会用到Dropout。据说Dropout是当时Hilton在给学生上课的时候提到的,用来帮助提升神经网路训练效果的小Trick。具体关于Dropout的讲解可以看我的这篇文章:https://wmathor.com/index.php/archives/1377/
在训练的时候随机对某些连接进行断开(通过将该连接的w设置为0),那么将会迫使网络尽可能地提升还存在的连接的表征能力,降低对多个神经元的依赖程度。绿色图的x轴是训练时loss的Dropout率,y是loss, 当为1时表示全部断开,loss最大,然而当dropout率为0时,右边蓝色图的y轴(test的acc)并不是最大,说明dropout率为0.2时在一定程度上可以缓解过拟合现象。
1.53.6.3.Adversarial AutoEncoders,对抗AutoEncoders
在AutoEncoder中可能存在这样一个问题,图片经过Encode之后的vector并不符合我们希望的分布(例如高斯分布),他的分布很有可能如下图所示。这其实是令我们不太满意的(虽然我并不知道Code满足分布到底有重要,但是既然别人认为很重要那就重要吧),那么有什么解决办法呢?
由University of Toronto、Google Brain和OpenAI合作的文章Adversarial Autoencoders(AAE)(https://arxiv.org/pdf/1511.05644.pdf)提出了一个使用Autoencoder进行对抗学习的idea,某种程度上对上述问题提供了一些新思路
AAE的核心其实就是利用GAN的思想,利用一个生成器G和一个判别器D进行对抗学习,以区分Real data和Fake data。具体思路是这样的,我现在需要一个满足p(z)概率分布的 向量,但是 实际上满足q(z)分布。那么我就首先生成一个满足p(z)分布的z’向量,打上Real data的标签,然后将z向量打上Fake data(服从q(z)分布)。由于这里的p(z)可以是我们定义的任何一个概率分布,因此整个对抗学习的过程实际上是可以认为是通过调整Encoder不断让其产生的数据的概率分布q(z)接近我们预定的p(z).
在原始的AutoEncoders中,没有呈现出原有数据的分布,有可能生成的数据是一样的,Adversarial AutoEncoders额外的添加了一个Discriminator(鉴别器),我们希望生成的Z符合真实的Z‘的分布,将真实的和生成的都送到鉴别器计算差距,如果属于希望的分布就输出为1,否则输出为0。
1.53.6.4.q分布和p分布
假设p和q都服从
1.53.6.5.对 KL(p◎q)的直观理解:
1.53.6.6.Maximize Likelihood(极大似然)
1.53.6.7.Minimize KL Divergence (最小化KL散度)
Evidence Lower BOund
1.53.6.8.在q(z)和p(z)之间怎么计算KL?
1.53.7.Variational AutoEncoders
前面的各种AutoEncoder都是将输入数据转换为vector,其中每个维度代表学习到的数据。而Variational AutoEncoders(VAE)提供了一种概率分布的描述形式,VAE中Encoder描述的是每个潜在属性的概率分布,而不是直接输出一个值。
举例来说,假设我们已经在一个AutoEncoder上训练了一个6维的vector,这个6维的vector将学习面部的一些属性,例如肤色、是否戴眼镜等。
在上面的示例中,我们使用单个值来描述输入图像的潜在属性。但是,我们可能更喜欢将每个潜在属性表示为一个范围。VAE就可以实现这个功能,如下图所示:
通过这种方法,我们现在将给定输入的每个潜在属性表示为概率分布。从状态解码(Decode)时,我们将从每个潜在状态分布中随机采样以生成向量来作为解码器的输入:
Sample()不可微
再参数化技巧
Too Complex!
生成模型,通过学习得到每一个特征的分布情况。
1.53.8.AutoEncoder的PyTorch实现
其实AutoEncoder就是非常简单的DNN。在encoder中神经元随着层数的增加逐渐变少,也就是降维的过程。而在decoder中神经元随着层数的增加逐渐变多,也就是升维的过程
# -*- coding: UTF-8 -*-
import torch
"""
安装pytorch的可视化工具——visdom:
安装并更新,不更新不能正常使用,会卡在download script。
pip install visdom -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install --upgrade visdom
"""
import visdom
from torch.utils.data import DataLoader
from torchvision import transforms, datasets
from torch import nn, optim
"""
其实AutoEncoder就是非常简单的DNN。在encoder中神经元随着层数的增加逐渐变少,也就是降维的过程。而在decoder
中神经元随着层数的增加逐渐变多,也就是升维的过程。
"""
class AE(nn.Module):
def __init__(self):
super(AE, self).__init__()
self.encoder = nn.Sequential(
# [b, 784] => [b, 256]
nn.Linear(784, 256),
nn.ReLU(),
# [b, 256] => [b, 64]
nn.Linear(256, 64),
nn.ReLU(),
# [b, 64] => [b, 20]
nn.Linear(64, 20),
nn.ReLU()
)
self.decoder = nn.Sequential(
# [b, 20] => [b, 64]
nn.Linear(20, 64),
nn.ReLU(),
# [b, 64] => [b, 256]
nn.Linear(64, 256),
nn.ReLU(),
# [b, 256] => [b, 784]
nn.Linear(256, 784),
nn.Sigmoid()
)
"""
上面代码都是基本操作,有一个地方需要特别注意,在decoder网络中,最后跟的不是ReLU而是Sigmoid函数,
因为我们想要将图片打印出来看一下,而使用的数据集是MNIST,所以要将tensor里面的值最终都压缩到0-1之间。
"""
def forward(self, x):
"""
:param [b, 1, 28, 28]:
:return [b, 1, 28, 28]:
"""
batchsz = x.size(0)
# flatten
x = x.view(batchsz, -1)
# encoder
x = self.encoder(x)
# decoder
x = self.decoder(x)
# reshape
x = x.view(batchsz, 1, 28, 28)
return x
def main():
mnist_train = datasets.MNIST('mnist', train=True, transform=transforms.Compose([
transforms.ToTensor()
]), download=True)
mnist_train = DataLoader(mnist_train, batch_size=32, shuffle=True)
mnist_test = datasets.MNIST('mnist', train=False, transform=transforms.Compose([
transforms.ToTensor()
]), download=True)
mnist_test = DataLoader(mnist_test, batch_size=32)
epochs = 1000
lr = 1e-3
model = AE()
# 由于input是0-1之间的实数,所以Loss functionMSE
criteon = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=lr)
print(model)
"""
在通常(监督学习)情况下,我们需要将网络的输出output和训练集的label进行对比,计算loss。
但AutoEncoder是无监督学习,不需要label,我们只需要将网络的输出output和网络的输入input进行对比,计算loss即可
"""
viz = visdom.Visdom(use_incoming_socket=False)
for epoch in range(epochs):
# 不需要label,所以用一个占位符"_"代替
for batchidx, (x, _) in enumerate(mnist_train):
x_hat = model(x)
loss = criteon(x_hat, x)
# backprop
optimizer.zero_grad()
loss.backward()
optimizer.step()
if epoch % 10 == 0:
print(epoch, 'loss:', loss.item())
x, _ = iter(mnist_test).next()
with torch.no_grad():
x_hat = model(x)
viz.images(x, nrow=8, win='x', opts=dict(title='x'))
viz.images(x_hat, nrow=8, win='x_hat', opts=dict(title='x_hat'))
if __name__ == '__main__':
main()
得到的效果如下图所示,普通的AutoEncoder还是差了一点,可以看到很多图片已经看不清具体代表的数字了。
1.53.9.Variational AutoEncoders
最主要关注一下定义网络的部分:
Encode以后的变量h要分成两半儿,利用h.chunk(num, dim)实现,num表示要分成几块,dim值表示在什么维度上进行。然后随机采样出标准正态分布的数据,用和对其进行变换。这里的kld指的是KL Divergence,它是Loss的一部分:
# -*- coding: UTF-8 -*-
import torch
import visdom
import numpy as np
from torch import nn, optim
from torch.utils.data import DataLoader
from torchvision import transforms, datasets
class VAE(nn.Module):
def __init__(self):
super(VAE, self).__init__()
# [b, 784] => [b, 20]
# u: [b, 10]
# sigma: [b, 10]
self.encoder = nn.Sequential(
# [b, 784] => [b, 256]
nn.Linear(784, 256),
nn.ReLU(),
# [b, 256] => [b, 64]
nn.Linear(256, 64),
nn.ReLU(),
# [b, 64] => [b, 20]
nn.Linear(64, 20),
nn.ReLU()
)
self.decoder = nn.Sequential(
# [b, 10] => [b, 64]
nn.Linear(10, 64),
nn.ReLU(),
# [b, 64] => [b, 256]
nn.Linear(64, 256),
nn.ReLU(),
# [b, 256] => [b, 784]
nn.Linear(256, 784),
nn.Sigmoid()
)
'''
Encode以后的变量h要分成两半儿,利用h.chunk(num, dim)实现,num表示要分成几块,dim值表示在什么维度
上进行。然后随机采样出标准正态分布的数据,用 和 对其进行变换。
'''
def forward(self, x):
"""
:param [b, 1, 28, 28]:
:return [b, 1, 28, 28]:
"""
batchsz = x.size(0)
# flatten
x = x.view(batchsz, -1)
# encoder
# [b, 20] including mean and sigma
q = self.encoder(x)
# [b, 20] => [b, 10] and [b, 10]
mu, sigma = q.chunk(2, dim=1)
# reparameterize trick, epsilon~N(0, 1)
q = mu + sigma * torch.randn_like(sigma)
# decoder
x_hat = self.decoder(q)
# reshape
x_hat = x_hat.view(batchsz, 1, 28, 28)
# KL
kld = 0.5 * torch.sum(
torch.pow(mu, 2) +
torch.pow(sigma, 2) -
torch.log(1e-8 + torch.pow(sigma, 2)) - 1
) / (batchsz * 28 * 28)
return x_hat, kld
def main():
mnist_train = datasets.MNIST('mnist', train=True, transform=transforms.Compose([
transforms.ToTensor()
]), download=True)
mnist_train = DataLoader(mnist_train, batch_size=32, shuffle=True)
mnist_test = datasets.MNIST('mnist', train=False, transform=transforms.Compose([
transforms.ToTensor()
]), download=True)
mnist_test = DataLoader(mnist_test, batch_size=32)
epochs = 1000
lr = 1e-3
model = VAE()
criteon = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=lr)
print(model)
"""
如果出现:Visdom python client failed to establish socket to get messages from the server. This feature is optional
and can be disabled by initializing Visdom with `use_incoming_socket=False`, which will prevent waiting for this request to timeout.
解决办法:
vis = visdom.Visdom(use_incoming_socket=False)
"""
viz = visdom.Visdom(use_incoming_socket=False)
for epoch in range(epochs):
# 不需要label,所以用一个占位符"_"代替
for batchidx, (x, _) in enumerate(mnist_train):
x_hat, kld = model(x)
loss = criteon(x_hat, x)
if kld is not None:
elbo = loss + 1.0 * kld
loss = elbo
# backprop
optimizer.zero_grad()
loss.backward()
optimizer.step()
if epoch % 10 == 0:
print(epoch, 'loss:', loss.item(), 'kld', kld.item())
x, _ = iter(mnist_test).next()
with torch.no_grad():
x_hat, kld = model(x)
viz.images(x, nrow=8, win='x', opts=dict(title='x'))
viz.images(x_hat, nrow=8, win='x_hat', opts=dict(title='x_hat'))
if __name__ == '__main__':
main()
输出结果:
VAE(
(encoder): Sequential(
(0): Linear(in_features=784, out_features=256, bias=True)
(1): ReLU()
(2): Linear(in_features=256, out_features=64, bias=True)
(3): ReLU()
(4): Linear(in_features=64, out_features=20, bias=True)
(5): ReLU()
)
(decoder): Sequential(
(0): Linear(in_features=10, out_features=64, bias=True)
(1): ReLU()
(2): Linear(in_features=64, out_features=256, bias=True)
(3): ReLU()
(4): Linear(in_features=256, out_features=784, bias=True)
(5): Sigmoid()
)
)
Setting up a new session...
Without the incoming socket you cannot receive events from the server or register event handlers to your Visdom client.
0 loss: 0.05882733315229416 kld 0.005957949906587601
10 loss: 0.04458034038543701 kld 0.00900172907859087
20 loss: 0.046602990478277206 kld 0.008918660692870617
30 loss: 0.04872169345617294 kld 0.009532256051898003
40 loss: 0.04016539081931114 kld 0.009037637151777744
50 loss: 0.044585954397916794 kld 0.009513779543340206
1.53.10.参考文章
https://www.cnblogs.com/pengzhonglian/p/12159970.html
https://blog.csdn.net/h__ang/article/details/90720579
https://www.cnblogs.com/jeshy/p/11204300.html
https://blog.csdn.net/z_feng12489/article/details/88851163
https://blog.csdn.net/fengdu78/article/details/104337519
https://blog.csdn.net/sleepinghm/article/details/105142959
https://www.cnblogs.com/yongjieShi/p/8371549.html
https://www.freesion.com/article/959958354/
https://blog.csdn.net/u013517182/article/details/93046229
https://blog.csdn.net/qq_41882866/article/details/108200444