VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现

发布于 2024-5-20 12:47
浏览
0收藏

变分自编码器是近些年较火的一个生成模型,我个人认为其本质上仍然是一个概率图模型,只是在此基础上引入了神经网络。本文将就变分自编码器(VAE)进行简单的原理讲解和数学推导。


论文:https://arxiv.org/abs/1312.6114
视频:https://www.bilibili.com/video/BV1op421S7Ep/

引入

高斯混合模型

生成模型,可以简单的理解为生成数据 (不 止 , 但 我 们 暂 且 就 这 么 理 解 它) 。假如现在我们有样本数据,而我们发现这些样本符合正态分布且样本具有充分的代表性,因此我们计算出样本的均值和方差,就能得到样本的概率分布。然后从正态分布中抽样,就能得到样本。这 种 生 成 样 本 的 过 程 就 是 生 成 过 程 。


可是,假如我们的数据长这样

VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区

很显然,它的数据是由两个不同的正态分布构成。我们可以计算出这些样本的概率分布。但是一种更为常见的方法就是将其当作是两个正态分布。我们引入一个隐变量z。


假 设 z 的 取 值 为 0,1 ,如果z为0,我们就从蓝色的概率分布中抽样;否则为1,则从橙色的概率分布中抽样。这就是生成过程。


但是这个隐变量z是什么?它其实就是隐藏特征 训 练 数 据x 的 抽 象 出 来 的 特 征 ,比如,如果x偏小,我们则认为它数据蓝色正太分布,否则为橙色。这个 "偏 小" 就是特征,我们把它的取值为0,1(0代表偏小,1代表偏大)。


那这种模型我们如何取训练它呢?如何去找出这个z呢?一 种 很 直 观 的 方 法 就 是 重 构 代 价 最 小 ,我们希望,给一个训练数据x,由x去预测隐变量z,再由隐变量z预测回x,得到的误差最小。比如假如我们是蓝色正态分布,去提取特征z,得到的z再返回来预测x,结果得到的却是橙色的正态分布,这是不可取的。其模型图如下

VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区

这个模型被称为GMM高斯混合模型

变分自编码器(VAE)

那它和VAE有什么关联呢?其实VAE的模型图跟这个原理差不多。只是有些许改变, 隐 变 量Z 的 维 度 是 连 续 且 高 维 的 , 不 再 是 简 单 的 离 散 分 布 ,因为假如我们生成的是图片,我们需要提取出来的特征明显是要很多的,传统的GMM无法做到。


VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区

VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区

也就是将训练样本x给神经网络,让神经网络计算出均值和协方差矩阵.


取log 的 原 因 是 传 统 的 神 经 网 络 输 出 值 总 是 有 正 有 负 。有了这两个值就可以在对应的高斯分布中采样,得到隐变量z。再让z经过神经网络重构回样本,得到新样本。这就是整个VAE的大致过程了。


再次强调, 训 练 过 程 我 们 希 望 每 次 重 构 的 时 候 , 新 样 本 和 训 练 样 本 尽 可 能 的 相 似。


VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区


为什么协方差会变成0?因为采样具有随机性,也就是存在噪声,噪声是肯定会增加重构的误差的。神经网络为了让误差最小,是肯定让这个随机性越小越好,因为只有这样,才能重构误差最小。


但是我们肯定是希望有随机性的,为什么?因为有随机性,我们才可以生成不同的样本啊!


VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区


所以,有KL散度去衡量两个概率分布的相似性

VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区

KL散度是大于等于0的值,越小则证明越相似

VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区

所以,我们就是两个优化目标 ① 最 小 化 重 构 代 价 ② 最 小 化 上 述 的 散 度

依照这两个条件,建立目标函数,直接梯度下降 其 实 还 需 要 重 参 数 化 , 后 面 会 讲 到 ,刷刷刷地往下降,最终收敛。

下面,我们就对其进行简单的数学推导,并以此推导出目标函数

原理推导

引入目标函数

以VAE的简略图为例

VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区

设我们有N个样本

VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区

VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区

VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区

现在,我们先单独看看里面某一个样本的似然,某个样本记为x

VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区

VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区

VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区

所以左边等于右边

VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区

VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区

VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区

VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区

VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区

VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区

VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区

VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区

VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区

VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区

VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区

按照上面提到的,我们可以把第一步改成

VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区

更一般地,我们把它们写成一起

VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区

由于KL散度是大于等于0的,所以第①项,就被称为变分下界。

VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区

好,现在我们只需要最大化其变分下界(以下省略掉参数)

VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区

VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区

细化目标函数

VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区

先 来 看 KL 散 度

VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区

VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区

VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区

可以分为三部分

VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区

如果你熟悉高斯分布的高阶矩的话,式A和式C完全就是二阶原点矩和中心距,是直接可以的得出答案的。

VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区

VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区

VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区

当然了,其实我们也可以不对其概率分布进行约束,归根究底,其让然是最小重构代价,那么我们的目标函数如果可以充分表达出“最小重构代价”,那么是什么又有何关系呢?

VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区

VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区

可以看到,这就是一个均方差

VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区

重参数化技巧

有了目标函数,理论上我们直接梯度下降就可以了。然而,别忘了,我们是从中采样出z来。可是我们却是用的神经网络去计算的均值和方差,得到的高斯分布再去采样,这种情况是不可导的。中间都已经出现了一个断层了。神经网络是一层套一层的计算。而采样计算了一层之后,从这一层中去采样新的值,再计算下一层。因此,采样本身是不可导的。

VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区

VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区

VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区

VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区

代码实现

VAE变分自编码器原理解析看这一篇就够了!另附Python代码实现-AI.x社区

效果一般,不晓得论文里面用了什么手段,效果看起来比这个好。(这个结果甚至还是我加了一层隐藏层的)

import torch
from torch.utils.data import DataLoader
from torchvision.datasets import MNIST
from torchvision.transforms import transforms
from  torch import nn
from tqdm import tqdm
import matplotlib.pyplot as plt
class VAE(nn.Module):
    def __init__(self,input_dim,hidden_dim,gaussian_dim):
        super().__init__()
        #编码器
        #隐藏层
        self.fc1=nn.Sequential(
            nn.Linear(in_features=input_dim,out_features=hidden_dim),
            nn.Tanh(),
            nn.Linear(in_features=hidden_dim, out_features=256),
            nn.Tanh(),
        )
        #μ和logσ^2
        self.mu=nn.Linear(in_features=256,out_features=gaussian_dim)
        self.log_sigma=nn.Linear(in_features=256,out_features=gaussian_dim)

        #解码(重构)
        self.fc2=nn.Sequential(
            nn.Linear(in_features=gaussian_dim,out_features=256),
            nn.Tanh(),
            nn.Linear(in_features=256, out_features=512),
            nn.Tanh(),
            nn.Linear(in_features=512,out_features=input_dim),
            nn.Sigmoid() #图片被转为为0,1的值了,故用此函数
        )
    def forward(self,x):
        #隐藏层
        h=self.fc1(x)


        #计算期望和log方差
        mu=self.mu(h)
        log_sigma=self.log_sigma(h)

        #重参数化
        h_sample=self.reparameterization(mu,log_sigma)

        #重构
        recnotallow=self.fc2(h_sample)

        return reconsitution,mu,log_sigma

    def reparameterization(self,mu,log_sigma):
        #重参数化
        sigma=torch.exp(log_sigma*0.5) #计算σ
        e=torch.randn_like(input=sigma,device=device)

        result=mu+e*sigma #依据重参数化技巧可得

        return result
    def predict(self,new_x): #预测
        recnotallow=self.fc2(new_x)

        return reconsitution
def train():

    transformer = transforms.Compose([
        transforms.ToTensor(),
    ]) #归一化
    data = MNIST("./data", transform=transformer,download=True) #载入数据

    dataloader = DataLoader(data, batch_size=128, shuffle=True) #写入加载器

    model = VAE(784, 512, 20).to(device) #初始化模型

    optimer = torch.optim.Adam(model.parameters(), lr=1e-3) #初始化优化器

    loss_fn = nn.MSELoss(reductinotallow="sum") #均方差损失
    epochs = 100 #训练100轮

    for epoch in torch.arange(epochs):
        all_loss = 0
        dataloader_len = len(dataloader.dataset)

        for data in tqdm(dataloader, desc="第{}轮梯度下降".format(epoch)):
            sample, label = data
            sample = sample.to(device)
            sample = sample.reshape(-1, 784) #重塑
            result, mu, log_sigma = model(sample) #预测

            loss_likelihood = loss_fn(sample, result) #计算似然损失

            #计算KL损失
            loss_KL = torch.pow(mu, 2) + torch.exp(log_sigma) - log_sigma - 1

            #总损失
            loss = loss_likelihood + 0.5 * torch.sum(loss_KL)

            #梯度归0并反向传播和更新
            optimer.zero_grad()

            loss.backward()

            optimer.step()
            with torch.no_grad():
                all_loss += loss.item()
        print("函数损失为:{}".format(all_loss / dataloader_len))
        torch.save(model, "./model/VAE.pth")
if __name__ == '__main__':
    #是否有闲置GPU
    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
    #训练
    train()

    #载入模型,预测
    model=torch.load("./model/VAE (1).pth",map_locatinotallow="cpu")
    #预测20个样本
    x=torch.randn(size=(20,20))
    result=model.predict(x).detach().numpy()
    result=result.reshape(-1,28,28)
    #绘图
    for i in range(20):
        plt.subplot(4,5,i+1)
        plt.imshow(result[i])
        plt.gray()
    plt.show()

VAE有很多的变种优化,感兴趣的读者自行查阅。

结束

以上,就是VAE的原理和推导过程了。能力有限,过程并不严谨,如有问题,还望指出。


本文转自 AI生成未来 ,作者:篝火者2312


原文链接:​​https://mp.weixin.qq.com/s/LFmFXA1hFZE8lesSk1XzzQ?poc_token=HMnPSmajmz9QXr5QDZlgSltwD4h5noYPraMmDOfv​

已于2024-5-20 12:50:11修改
收藏
回复
举报
回复
相关推荐