深度学习笔记(十四)---自动编码器

自动编码器:

自动编码器最开始是作为一种数据压缩方法,同时还可以在卷积网络中进行逐层预训练,但是随后更多结构复杂的网络,比如 resnet 的出现使得我们能够训练任意深度的网络,自动编码器就不再使用在这个方面,随着生成对抗模型而出现的,使用自动编码器生成数据。

自动编码器的一般结构如下

由上面的图片,我们能够看到,第一部分是编码器(encoder),第二部分是解码器(decoder),编码器和解码器都可以是任意的模型,通常我们可以使用神经网络作为我们的编码器和解码器,输入的数据经过神经网络降维到一个编码,然后又通过另外一个神经网络解码得到一个与原始数据一模一样的生成数据,通过比较原始数据和生成数据,希望他们尽可能接近,所以最小化他们之间的差异来训练网络中编码器和解码器的参数。

当训练完成之后,我们如何生成数据呢?非常简单,我们只需要拿出解码器的部分,然后随机传入 code,就可以通过解码器生成各种各样的数据

下面我们使用 mnist 数据集来说明一个如何构建一个简单的自动编码器(pytorch实现)由于没有服务器,不能使用cuda加速,更新次数只有十次,可以看到运行效果。

#导入包
import os
import torch
from torchvision import transforms as tfs 
from torchvision.datasets import MNIST
from torchvision.utils import save_image
from torch.autograd import Variable
from torch import nn
from torch.utils.data import DataLoader
"""view返回一个有相同数据但大小不同的tensor。
返回的tensor必须有与原tensor相同的数据和相同数目的元素,但可以有不同的大小。"""
def to_img(x):
    '''
    定义一个函数将最后的结果转换回图片
    '''
    x = 0.5 * (x + 1.)
    x = x.clamp(0, 1)
    x = x.view(-1, 3, 28, 28)
    return x

#用Compose把多个步骤整合到一起
im_tfs = tfs.Compose([
    tfs.ToTensor(),
    tfs.Lambda(lambda x: x.repeat(3,1,1)),#匿名函数,没有实际的用途,
    #repeat:沿着指定的维度重复tensor
    tfs.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) # 标准化
])

#读取数据
train_set = MNIST('./mnist',train=True,download=True,transform=im_tfs)
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()
x = Variable(torch.randn(1,28*28)) #batch size是1
code, _ = net(x)
print(code.shape)

criterion = nn.MSELoss(reduction='sum')
optimizer = torch.optim.Adam(net.parameters(),lr=1e-3)



# 开始训练自动编码器
for e in range(10):
    for im, _ in train_data:
        im = im.view(-1, 28*28)
        im = Variable(im)
        # 前向传播
        _, output = net(im)
        loss = criterion(output, im) / im.shape[0] # 平均
        # 反向传播
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    
    if (e+1) % 2 == 0: # 每 2 次,将生成的图片保存一下
        print('epoch: {}, Loss: {:.4f}'.format(e + 1, loss.item()))
        pic = to_img(output.cpu().data)
        if not os.path.exists('./chapter6_GAN/simple_autoencoder'):
            os.mkdir('./chapter6_GAN/simple_autoencoder')
        save_image(pic, './chapter6_GAN/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)
encode, _ = net(view_data)    # 提取压缩的特征值
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())
plt.show()

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

变分自动编码器

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

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

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

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

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

KL divergence 的公式如下

重参数

为了避免计算 KL divergence 中的积分,我们使用重参数的技巧,不是每次产生一个隐含向量,而是生成两个向量,一个表示均值,一个表示标准差,这里我们默认编码之后的隐含向量服从一个正态分布的之后,就可以用一个标准正态分布先乘上标准差再加上均值来合成这个正态分布,最后 loss 就是希望这个生成的正态分布能够符合一个标准正态分布,也就是希望均值为 0,方差为 1。所以标准的变分自动编码器如下

下面贴出pytorch实现的变分自动编码器:

import os
import torch
from torch.autograd import Variable
import torch.nn.functional as F
from torch import nn
from torchvision.datasets import MNIST
from torchvision import transforms as tfs 
from torchvision.utils import save_image
from torch.utils.data import DataLoader

def to_img(x):
    '''
    定义一个函数将最后的结果转换回图片
    '''
    x = 0.5 * (x + 1.)
    x = x.clamp(0, 1)
    x = x.view(-1, 3, 28, 28)
    return x

im_tfs = tfs.Compose([
    tfs.ToTensor(),
    tfs.Lambda(lambda x: x.repeat(3,1,1)),
    tfs.Normalize([0.5,0.5,0.5],[0.5,0.5,0.5])
])

train_set = MNIST('./mnist',download=True,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)
        self.fc22 = nn.Linear(400,20)
        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_()
        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()

x, _ =train_set[0]
x = Variable(torch.randn(1,28*28)) #batch size是1
_,mu,var = net(x)
#print(mu.shape)

reconstruction_function = nn.MSELoss(size_average=False)

def loss_function(recon_x, x, mu, logvar):
    
    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)


for e in range(10):
    for im, _ in train_data:
        im = im.view(-1, 28*28)
        im = Variable(im)
        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()

    if (e + 1) % 2 == 0:
        print('epoch: {}, Loss: {:.4f}'.format(e + 1, loss.item()))
        save = to_img(recon_im.cpu().data)
        if not os.path.exists('./chapter6_GAN/vae_img'):
            os.mkdir('./chapter6_GAN/vae_img')
        save_image(save,'./chapter6_GAN/vae_img/image_{}.png'.format(e+1))

程序运行结果:

变分自动编码器虽然比一般的自动编码器效果要好,而且也限制了其输出的编码 (code) 的概率分布,但是它仍然是通过直接计算生成图片和原始图片的均方误差来生成 loss,这个方式并不好。

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值