VQ-VAE的简明介绍:量子化自编码器
By 苏剑林 | 2019-06-24 | 379986位读者 |印象中很早之前就看到过VQ-VAE,当时对它并没有什么兴趣,而最近有两件事情重新引起了我对它的兴趣。一是VQ-VAE-2实现了能够匹配BigGAN的生成效果(来自机器之心的报道);二是我最近看一篇NLP论文《Unsupervised Paraphrasing without Translation》时发现里边也用到了VQ-VAE。这两件事情表明VQ-VAE应该是一个颇为通用和有意思的模型,所以我决定好好读读它。
模型综述 #
VQ-VAE(Vector Quantised - Variational AutoEncoder)首先出现在论《Neural Discrete Representation Learning》,跟VQ-VAE-2一样,都是Google团队的大作。
有趣却玄虚 #
作为一个自编码器,VQ-VAE的一个明显特征是它编码出的编码向量是离散的,换句话说,它最后得到的编码向量的每个元素都是一个整数,这也就是“Quantised”的含义,我们可以称之为“量子化”(跟量子力学的“量子”一样,都包含离散化的意思)。
明明整个模型都是连续的、可导的,但最终得到的编码向量却是离散的,并且重构效果看起来还很清晰(如文章开头的图),这至少意味着VQ-VAE会包含一些有意思、有价值的技巧,值得我们学习一番。不过,读了原论文之后,总感觉原论文写得有点难懂。这种难懂不是像ON-LSTM原论文那样的晦涩难懂,而是有种“故弄玄虚”的感觉。
首先,你读完整篇论文就会明白,VQ-VAE其实就是一个AE(自编码器)而不是VAE(变分自编码器),我不知道作者出于什么目的非得用概率的语言来沾VAE的边,这明显加大了读懂这篇论文的难度。其次,VQ-VAE的核心步骤之一是Straight-Through Estimator,这是将隐变量离散化后的优化技巧,在原论文中没有稍微详细的讲解,以至于必须看源码才能更好地知道它说啥。最后,论文的核心思想也没有很好地交代清楚,给人的感觉是纯粹在介绍模型本身而没有介绍模型思想。
PixelCNN #
要追溯VQ-VAE的思想,就不得不谈到自回归模型。可以说,VQ-VAE做生成模型的思路,源于PixelRNN、PixelCNN之类的自回归模型,这类模型留意到我们要生成的图像,实际上是离散的而不是连续的。以cifar10的图像为例,它是32×32大小的3通道图像,换言之它是一个32×32×3的矩阵,矩阵的每个元素是0~255的任意一个整数,这样一来,我们可以将它看成是一个长度为32×32×3=3072的句子,而词表的大小是256,从而用语言模型的方法,来逐像素地、递归地生成一张图片(传入前面的所有像素,来预测下一个像素),这就是所谓的自回归方法:
p(x)=p(x1)p(x2|x1)…p(x3n2|x1,x2,…,x3n2−1)
其中p(x1),p(x2|x1),…,p(x3n2|x1,x2,…,x3n2−1)每一个都是256分类问题,只不过所依赖的条件有所不同。
PixelRNN、PixelCNN网上都有一定的资料介绍了,这里不再赘述,我感觉其实也可以蹭着Bert的热潮,去搞个PixelAtt(Attention)来做它。自回归模型的研究主要集中在两方面:一方面是如何设计这个递归顺序,使得模型可以更好地生成采样,因为图像的序列不是简单的一维序列,它至少是二维的,更多情况是三维的,这种情况下你是“从左往右再从上到下”、“从上到下再从左往右”、“先中间再四周”或者是其他顺序,都很大程度上影响着生成效果;另一方面是研究如何加速采样过程。在我读到的文献里,自回归模型比较新的成果是ICLR 2019的工作《Generating High Fidelity Images with Subscale Pixel Networks and Multidimensional Upscaling》。
自回归的方法很稳妥,也能有效地做概率估计,但它有一个最致命的缺点:慢。因为它是逐像素地生成的,所以要每个像素地进行随机采样,上面举例的cifar10已经算是小图像的,目前做图像生成好歹也要做到128×128×3的才有说服力了吧,这总像素接近5万个(想想看要生成一个长度为5万的句子),真要逐像素生成会非常耗时。而且这么长的序列,不管是RNN还是CNN模型都无法很好地捕捉这么长的依赖。
原始的自回归还有一个问题,就是割裂了类别之间的联系。虽然说因为每个像素是离散的,所以看成256分类问题也无妨,但事实上连续像素之间的差别是很小的,纯粹的分类问题捕捉到这种联系。更数学化地说,就是我们的目标函数交叉熵是−logpt,假如目标像素是100,如果我预测成99,因为类别不同了,那么pt就接近于0,−logpt就很大,从而带来一个很大的损失。但从视觉上来看,像素值是100还是99差别不大,不应该有这么大的损失。
VQ-VAE #
针对自回归模型的固有毛病,VQ-VAE提出的解决方案是:先降维,然后再对编码向量用PixelCNN建模。
降维离散化 #
看上去这个方案很自然,似乎没什么特别的,但事实上一点都不自然。
因为PixelCNN生成的离散序列,你想用PixelCNN建模编码向量,那就意味着编码向量也是离散的才行。而我们常见的降维手段,比如自编码器,生成的编码向量都是连续性变量,无法直接生成离散变量。同时,生成离散型变量往往还意味着存在梯度消失的问题。还有,降维、重构这个过程,如何保证重构之后出现的图像不失真?如果失真得太严重,甚至还比不上普通的VAE的话,那么VQ-VAE也没什么存在价值了。
幸运的是,VQ-VAE确实提供了有效的训练策略解决了这两个问题。
最邻近重构 #
在VQ-VAE中,一张n×n×3的图片x先被传入一个encoder中,得到连续的编码向量z:
z=encoder(x)
这里的z是一个大小为d的向量。另外,VQ-VAE还维护一个Embedding层,我们也可以称为编码表,记为
E=[e1,e2,…,eK]
这里每个ei都是一个大小为d的向量。接着,VQ-VAE通过最邻近搜索,将z映射为这K个向量之一:
z→ek,k=argminj‖z−ej‖2
我们可以将z对应的编码表向量记为zq,我们认为zq才是最后的编码结果。最后将zq传入一个decoder,希望重构原图ˆx=decoder(zq)。
整个流程是:
xencoder→z最邻近→zqdecoder→ˆx
这样一来,因为zq是编码表E中的向量之一,所以它实际上就等价于1,2,…,K这K个整数之一,因此这整个流程相当于将整张图片编码为了一个整数。
当然,上述过程是比较简化的,如果只编码为一个向量,重构时难免失真,而且泛化性难以得到保证。所以实际编码时直接用多层卷积将x编码为m×m个大小为d的向量:
z=(z11z12…z1mz21z22…z2m⋮⋮⋱⋮zm1zm2…zmm)
也就是说,z的总大小为m×m×d,它依然保留着位置结构,然后每个向量都用前述方法映射为编码表中的一个,就得到一个同样大小的zq,然后再用它来重构。这样一来,zq也等价于一个m×m的整数矩阵,这就实现了离散型编码。
自行设计梯度 #
我们知道,如果是普通的自编码器,直接用下述loss进行训练即可:
‖x−decoder(z)‖22
但是,在VQ-VAE中,我们用来重构的是zq而不是z,那么似乎应该用这个loss才对:
‖x−decoder(zq)‖22
但问题是zq的构建过程包含了argmin,这个操作是没梯度的,所以如果用第二个loss的话,我们没法更新encoder。
换言之,我们的目标其实是‖x−decoder(zq)‖22最小,但是却不好优化,而‖x−decoder(z)‖22容易优化,但却不是我们的优化目标。那怎么办呢?当然,一个很粗暴的方法是两个都用:
‖x−decoder(z)‖22+‖x−decoder(zq)‖22
但这样并不好,因为最小化‖x−decoder(z)‖22并不是我们的目标,会带来额外的约束。
VQ-VAE使用了一个很精巧也很直接的方法,称为Straight-Through Estimator,你也可以称之为“直通估计”,它最早源于Benjio的论文《Estimating or Propagating Gradients Through Stochastic Neurons for Conditional Computation》,在VQ-VAE原论文中也是直接抛出这篇论文而没有做什么讲解。但事实上直接读这篇原始论文是一个很不友好的选择,还不如直接读源代码。
事实上Straight-Through的思想很简单,就是前向传播的时候可以用想要的变量(哪怕不可导),而反向传播的时候,用你自己为它所设计的梯度。根据这个思想,我们设计的目标函数是:
‖x−decoder(z+sg[zq−z])‖22
其中sg是stop gradient的意思,就是不要它的梯度。这样一来,前向传播计算(求loss)的时候,就直接等价于decoder(z+zq−z)=decoder(zq),然后反向传播(求梯度)的时候,由于zq−z不提供梯度,所以它也等价于decoder(z),这个就允许我们对encoder进行优化了。
顺便说一下,基于这个思想,我们可以为很多函数自己自定义梯度,比如x+sg[relu(x)−x]就是将relu(x)的梯度定义为恒为1,但是在误差计算时又跟relu(x)本身等价。当然,用同样的方法我们可以随便指定一个函数的梯度,至于有没有实用价值,则要具体任务具体分析了。
维护编码表 #
要注意,根据VQ-VAE的最邻近搜索的设计,我们应该期望zq和z是很接近的(事实上编码表E的每个向量类似各个z的聚类中心出现),但事实上未必如此,即使‖x−decoder(z)‖22和‖x−decoder(zq)‖22都很小,也不意味着zq和z差别很小(即f(z1)=f(z2)不意味着z1=z2)。
所以,为了让zq和z更接近,我们可以直接地将‖z−zq‖22加入到loss中:
‖x−decoder(z+sg[zq−z])‖22+β‖z−zq‖22
除此之外,还可以做得更仔细一些。由于编码表(zq)相对是比较自由的,而z要尽力保证重构效果,所以我们应当尽量“让zq去靠近z”而不是“让z去靠近zq”,而因为‖zq−z‖22的梯度等于对zq的梯度加上对z的梯度,所以我们将它等价地分解为
‖sg[z]−zq‖22+‖z−sg[zq]‖22
第一项相等于固定z,让zq靠近z,第二项则反过来固定zq,让z靠近zq。注意这个“等价”是对于反向传播(求梯度)来说的,对于前向传播(求loss)它是原来的两倍。根据我们刚才的讨论,我们希望“让zq去靠近z”多于“让z去靠近zq”,所以可以调一下最终的loss比例:
‖x−decoder(z+sg[zq−z])‖22+β‖sg[z]−zq‖22+γ‖z−sg[zq]‖22
其中γ<β,在原论文中使用的是γ=0.25β。
(注:还可以用滑动平均的方式更新编码表,详情请看原论文。)
拟合编码分布 #
经过上述一大通设计之后,我们终于将图片编码为了m×m的整数矩阵了,由于这个m×m的矩阵一定程度上也保留了原来输入图片的位置信息,所以我们可以用自回归模型比如PixelCNN,来对编码矩阵进行拟合(即建模先验分布)。通过PixelCNN得到编码分布后,就可以随机生成一个新的编码矩阵,然后通过编码表E映射为3维的实数矩阵zq(行*列*编码维度),最后经过deocder得到一张图片。
一般来说,现在的m×m比原来的n×n×3要小得多,比如我在用CelebA数据做实验的时候,原来128×128×3的图可以编码为32×32的编码而基本不失真,所以用自回归模型对编码矩阵进行建模,要比直接对原始图片进行建模要容易得多。
个人的复现 #
这是自己用Keras实现的VQ-VAE(Python 2.7 + Tensorflow 1.8 + Keras 2.2.4,其中模型部分参考了这个):
这个脚本的正文部分只包含VQ-VAE的编码和重构(文章开头的图就是笔者用这个脚本重构的,可见重构效果还可以),没有包含用PixelCNN建模先验分布。不过最后的注释那里包含了一个用Attention来建模先验分布的例子,用Attention建模先验分布后,随机采样的效果如下:
效果图一定程度上表明这样的随机采样是可行的,但是这样的生成效果不能说很好。我用PixelAtt而不是PixelCNN的原因是在我的复现里PixelCNN效果比PixelAtt还差得多,所以PixelAtt是有一定优势的,但缺点是PixelAtt太耗显存,容易OOM。不过我个人的复现不够好也不意味着这套方法不够好,可能是我没调好的原因,也能使网络不够深之类的。我个人是比较看好这种离散化的编码研究的。
最后的总结 #
到此,总算把VQ-VAE用自己认为比较好的方式讲清楚了。纵观全文,其实没有任何VAE的味道,所以我说它其实就是一个AE,一个编码为离散型向量的AE。它能重构出比较清晰的图像,则是因为它编码时保留了足够大的feature map~
如果弄懂了VQ-VAE,那么它新出的2.0版本也就没什么难理解的了,VQ-VAE-2相比VQ-VAE几乎没有本质上的技术更新,只不过把编码和解码都分两层来做了(一层整体,一层局部),从而使得生成图像的模糊感更少(相比至少是少很多了,但其实你认真看VQ-VAE-2的大图,还是有略微的模糊感的)。
不过值得肯定的是,VQ-VAE整个模型还是挺有意思,离散型编码、用Straight-Through的方法为梯度赋值等新奇特点,非常值得我们认真学习,能加深我们对深度学习的模型和优化的认识(梯度你都能设计了,还担心设计不好模型吗?)。
转载到请包括本文地址:https://spaces.ac.cn/archives/6760
更详细的转载事宜请参考:《科学空间FAQ》
如果您还有什么疑惑或建议,欢迎在下方评论区继续讨论。
如果您觉得本文还不错,欢迎分享/打赏本文。打赏并非要从中获得收益,而是希望知道科学空间获得了多少读者的真心关注。当然,如果你无视它,也不会影响你的阅读。再次表示欢迎和感谢!
如果您需要引用本文,请参考:
苏剑林. (Jun. 24, 2019). 《VQ-VAE的简明介绍:量子化自编码器 》[Blog post]. Retrieved from https://spaces.ac.cn/archives/6760
@online{kexuefm-6760,
title={VQ-VAE的简明介绍:量子化自编码器},
author={苏剑林},
year={2019},
month={Jun},
url={\url{https://spaces.ac.cn/archives/6760}},
}
July 31st, 2023
苏神,这里还是没有完全理解为啥采用自回归就能有效避免自编码中模糊的问题。自编码我的理解是q(z|x)求隐变量,类似于求空间维度。自回归是q(x0|x1),求时间维度,也可以空间维度,自己恢复自己。难道就是因为采用自己的信息恢复自己比采用隐变量的信息恢复自己更好吗?
如果是这样,那LVAE中,采用多层隐变量,为啥也能避免模糊?当然这个LVAE我也看到了有人说多层VAE叠加相当于一个AR模型,但是一直没想通,是咋相等的。
总而言之,就是让自编码向着自回归靠拢,能解决模糊问题,但这个咋理解呀?
自回归能比较精确建立上下文依赖,有了上下文依赖,说明分布的建模比较精确,从而更有可能解决模糊问题。
August 14th, 2023
苏神您好,请教一下原paper中的目标函数L是不是有问题。L=logp(x|zq(x))+||sg[ze(x)]−e||22+β||ze(x)−sg[e]||22, 其中 logp(x|zq) 这一项log likelihood前面是不是少了个负号,因为应该最大化log likelihood。
嗯嗯,第一项少写了个负号。
October 12th, 2023
你好,请问用整数直接代表离散化向量的合理性在哪里呢,离散化的向量维度高,但是向量之间是有欧式距离关系的,比如1号向量可能与48号向量的距离比2号更近,如果先验分布的产生需要依赖这个信息,直接用数字不就显得草率了吗。
整数的背后也是一个向量。
November 1st, 2023
苏神,好奇VQ-VAE是怎么学到不在样本中的离散编码的意义的呢。比如AE,给月牙和满月的图片,它无法插值出半月的图片,因为没有学到半月对应的编码;VAE能学到是因为它加了噪声,覆盖到了潜在空间的一个范围而不是一个点。对于VQ-VAE,它是如何学习到不在训练集中的离散编码的意义,或者说它怎么建立两个离散编码组合之间的联系?
November 10th, 2023
很久以前看一个NAS算法的代码,里面大概有这样的操作x+(x_q-x).detach(),当时看了半天才明白是为了反向的时候给正常的梯度同时前向时候保持量化的状态。今天看了这篇博客才明白原来这就是STE(STE的论文我确实看不懂。。。)
November 16th, 2023
苏神,请问离散的空间里想要融合两张图片应该怎么操作?先平均 z 再做 KNN 得到 z_q?
VQ-VAE并不是一个生成模型,更加不是一个实随机向量到数据的生成模型,它无法提供有效的图片融合方案。你说的这个会有一点效果,但生成的图片质量往往会明显下降。
我尝试过两种方案,一种是先平均再knn,一种是先knn再平均
最终结果和在像素空间简单的图像加权没区别
December 1st, 2023
一个实践性的问题是:如何确定m和d的大小?
比如,在mnist 10 digits的任务中:
- 如果只给几十个整数的表示空间,是否类似于聚类,某些整数表示的简单整合就对应了digit的分类结果?
- 如果整数的空间很大则更接近一般的连续编码?
1、d只要不太小,结果就不会有明显变化;
2、m太小会影响重构清晰度,从我个人的实验结果来说,对于图片一般m是原始图片的大小除以4就是极限了。
January 25th, 2024
想问下苏神,送进解码器的zq是编码表E中的连续向量还是向量的整数索引呢?
前向传播是前者,反向传播是量化前的编码器输出。
January 28th, 2024
[...]https://www.spaces.ac.cn/archives/6760[...]
February 15th, 2024
今天看了VQ-VAE的原始论文,看到「还需要用PixelCNN建模先验分布」的时候觉得很奇怪。
看了这篇文章恍然大悟:果然VQ-VAE并没有有效地迫使先验分布逼近均匀分布。
如果它真的做到了让先验分布逼近均匀分布,那么应该可以随便从均匀分布中采样一个z,就能生成出完整、像样(coherent)的图像来。
实际上这样做只能生成局部像样的图像,整体还是乱的。也就是说,z的先验分布中还有许多结构,离「均匀」还差得远。
如果编码器、解码器能把这些结构给学习到,那么z的大小可以再减小许多(无论是k还是m),也就是达到更好的图像压缩效果。
VQ-VAE的问题确实如此,它只是一个离散化的AE,并非一个VAE。
但z的大小,不见得可以进一步减少很多,参考我今天刚写的 https://kexue.fm/archives/9984 ,其实基于离散化的方式去做无损压缩是非常困难的,有损的话则没有好的loss可用。