概述
2024年2月16日,OpenAI发布了视频生成模型Sora,其能基于已有的文本描述、图片和视频生成新的视频,且生成的视频可支持不同尺寸和时长(最长可达1分钟),并可模拟真实物理世界的相关特性(如镜头移动时物体可保持三维透视原理)。Sora生成的视频的视觉效果相比其他已有的视频生成模型有较大的提升。
OpenAI发布的关于Sora的技术报告——《Video Generation Models as World Simulators》对Sora的技术方案、生成效果作了简要的介绍,其核心技术基于Sora项目负责人之一——William Peebles在2023年发表的论文《Scalable Diffusion Models with Transformers》中提出的Diffusion Transformer(可简称为DiT),而Diffusion Transformer则是在扩散模型近几年不断发展的基础上,将扩散模型进一步与Transformer结合,从而在图片生成领域取得了更好的效果。
因此,Sora的成功并不是一蹴而就的,而是基于近几年扩散模型、Transformer等方面不断发展的技术成果。本文对Sora相关技术基于已有的论文和技术报告进行解读,如有不足之处,请指正。
Transformer
《Attention Is All You Need》是Google于2017年发表的一篇经典论文,其开创性的设计了Transformer模型结构,对于序列问题,通过编码器输出隐向量序列,再通过解码器输出目标序列,模型引入了注意力机制,充分挖掘序列各节点之间的深度信息,并通过矩阵计算的并行化加速模型训练和推理性能。Transformer模型结构的提出当初主要为了解决机器翻译问题,但由于其模型结构的通用性,目前已广泛应用于NLP、计算机视觉、计算广告等多个领域,更成为NLP大模型的底层基础结构。
模型结构
Transformer整体模型结构为编码器和解码器(Encoder-Decoder)架构,如图1所示。对于机器翻译问题,编码器将原句词元序列作为输入,输出向量序列,如图1左侧所示;而解码器再将向量序列作为输入,输出翻译后的结果句词元序列,如图1右侧所示。过程中,解码器每步输出一个词元,即第步输出,实际输出时通过Softmax层计算每个词元在当前位置的概率,选取最大概率的词元作为结果。同时,解码器每步计算时,会将之前已输出的结果句词元序列也作为输入,即第步会使用作为输入,论文称之为自回归(Auto-Regressive)。编码器和解码器内部使用了注意力机制、全连接网络和残差连接充分挖掘词元序列的语义信息,论文下面对这些结构进行了详细的介绍。
编码器
编码器在Embedding和位置编码后。编码器由多层组成,每层的结构相同,均又包含两个子层:多头注意力层(Multi-Head Attention)和全连接网络层。编码器中多头注意力层输入的查询、键值对相同,均采用原句词元序列的Embedding和位置编码后的输出,因此实现自注意力(Self Attention)机制,挖掘原句词元序列两两词元之间的相关性。每个子层周围有一个残差连接,通过残差连接避免梯度消失问题,然后进行层归一化,通过层归一化在样本粒度进行归一化,而非批次粒度进行归一化,避免不同句子样本的长短不一问题。另外,为便于残差连接,Embedding和位置编码后的输出(即编码器层的输入)和编码器各子层的输出的维度相等,论文中基线模型值为512。每个子层的上述计算过程可以通过以下公式表示:
其中,表示每个子层的输入,表示多头注意力层或全连接网络层的输出,表示残差连接,表示层归一化。
解码器
解码器也是由多层组成(论文中基线模型层数为6),每层的结构相同,均包含三个子层:带掩码的多头注意力层(Masked Multi-Head Attention)、多头注意力层(Multi-Head Attention)和全连接层,并在每个子层周围有一个残差连接,然后进行层归一化。可以看到解码器和编码器的结构基本相同,不同之处主要有以下几点:
- 增加带掩码的多头注意力层,并将解码器之前已输出的结果句词元序列也作为输入,即第步会使用作为输入,其中掩码的作用是为了在训练阶段中,当第步预测时,只使用训练样本中的,而抹去,保证训练和推理阶段数据的一致性;
- 多头注意力层输入的查询采用带掩码的多头注意力层的输出,键值对采用编码器的输出,因此实现目标注意力(Target Attention)机制,挖掘原句词元序列和结果句词元序列两两词元之间的相关性;
- 解码器的输出通过线性层和Softmax层计算每个词元在当前位置的概率,选取最大概率的词元作为结果。
注意力机制
注意力机制的输入包括查询和多个键值对,通过注意力评分函数计算查询和各个键的权重,基于权重对相应的各个值进行加权求和,从而突出和查询相关的值,实现注意力机制,类比人类观察事物,会着重关注自己感兴趣的事物部分而非整体。
缩放点积注意力
论文中的注意力机制底层实现是缩放点积注意力(Scaled Dot-Product Attention),其结构如图2所示。令多个查询构成矩阵,其中每个查询的维度为,多个键、值对分别构成矩阵、,其中每个键的维度为,每个值的维度为,则图2的计算过程可以通过以下公式表示:
其中,通过和这两个矩阵相乘,实现查询和键两两之间的点积计算,作为点积注意力,并且各点积计算可以并行化加速计算过程。计算后的点积注意力会乘以进行缩放,这是为了避免当较大时,原始点积注意力值也会较大,Softmax函数输出值会落入梯度较小的区域,收敛慢。缩放后的点积注意力通过Softmax函数映射到取值范围为、且和为1的概率空间。最后再通过一个矩阵相乘,得到各个查询下、各个值的加权和。
另外,图中还有一个可选项——掩码,其作用已在解码器部分介绍。
多头注意力
如果仅使用上一节的缩放点积注意力,其查询、键值对在论文中均是词元向量,维度均为,论文中提到,若采用线性层,将查询、键值对映射到不同的维度空间,能够自动挖掘原始输入中的深度信息,带来效果的提升,因此引入了多头注意力层,其结构如图3所示。
多头注意力层分为多层(论文中基线模型层数为8),每个层之间可以并行计算。图3的计算过程可以通过以下公式表示:
其中,对于当前层,先通过三个线性层矩阵、、分别将原始的词元序列转化为向量维度为的查询矩阵、键矩阵和向量维度为的值矩阵,再通过上一节的缩放点积注意力计算各个查询下、各个值的加权和。得到各层向量维度为的输出后,再拼接在一起得到向量维度为的总输出,最后通过一个线性层矩阵将总输出映射为向量维度为的结果输出。论文中基线模型。
注意力计算示例
下面通过如图4所示的示例说明如何计算得到词元之间的注意力,并通过注意力对词元进行加权求和。
图4中,输入即词元向量序列,每一行为一个词元向量。输入使用多个“Self-Attention”进行处理(即多头注意力)。
令词元向量序列中只有两个词元向量和,在第二个头中,通过权重矩阵将其分别映射为和,通过权重矩阵将其分别映射为和,通过权重矩阵将其分别映射为和,然后在点积缩放注意力中,对和计算点积得到和其自身的注意力,对和计算点积得到和的注意力(此处暂不考虑缩放和归一化),再基于注意力对和进行加权求和得到第二个头输出结果中的第行,即:。
图5是原Transformer论文列出的一个多头注意力示例,其中只列出了词元“making”和部分其他词元之间的注意力,不同头的注意力采用不同的颜色。
注意力机制在模型中的应用
这里论文进一步总结了注意力机制在模型中的应用:
- 在编码器部分,多头注意力层输入的查询、键值对相同,均采用原句词元序列的Embedding和位置编码后的输出,因此实现自注意力(Self Attention)机制,挖掘原句词元序列两两词元之间的相关性;
- 在解码器部分,带掩码多头注意力层输入的查询、键值对相同,均采用结果句词元序列的Embedding和位置编码后的输出,因此实现自注意力(Self Attention)机制,挖掘结果句词元序列两两词元之间的相关性;另外,增加掩码,在训练阶段中,当第步预测时,只使用训练样本中的,而在Softmax层的输入被重置为负无强大,使其在Softmax层的输出逼近0,抹去其影响,保证训练和推理阶段数据的一致性;
- 在编码器和解码器之间,查询采用解码器的输入,键值对采用编码器的输出,因此实现目标注意力(Target Attention)机制,挖掘原句词元序列和结果句词元序列两两词元之间的相关性。
逐个位置的前馈神经网络
多头注意力层输出是句子序列各词元的向量序列,维度为。经过残差连接和归一化后,输入前馈神经网络,由于多头注意力层已通过注意力机制考虑句子序列各词元之间的相关性,包含序列信息,因此,前馈神经网络对每个词元的向量作为输入,进行单独处理,论文称之为“Position-Wise”。
前馈神经网络的模型结构可以用以下公式表示:
从中可以看出,其结构就是带有一层隐藏层的全连接网络,输入到隐层的权重矩阵为,偏移向量是,而隐层中每个神经元的激活函数采用ReLU,隐层到输出的权重矩阵为,偏移向量是,输入和输出的维度仍是(512),隐层的维度是(2048)。
Embedding和Softmax
对于输入的词元,使用了统一的Embedding矩阵进行向量化,转化为维度为的向量。Softmax层不再详述。
位置编码
对于一个句子,如果构造该句子的词元本身不变,只是词元顺序变化,那么句子所表达的含义也会变化,因此需要考虑词元在句子中的位置。在Transformer中,输入的词元序列在Embedding计算后,还会对每个词元在序列中的位置进行编码,充分考虑位置信息。
词元位置编码后输出的向量维度也是,和词元通过Embedding层后输出的向量维度相同,这样便于两者直接相加作为编码器层的输入。那么如何将长短不一的句子中的词元位置编码为固定维度的向量,论文中采用的方法是正余弦函数,公式如下:
其中,表示位置编码,表示当前计算的是第个位置的编码向量,和分别表示该位置编码向量中第和第个值。由于论文中的向量维度值为512,因此,取值范围为,即将每个位置向量的维度分为256组,第组的两个值分别使用波长为的正余弦函数对位置进行计算,映射到。也就是说,随着的取值,正余弦函数的波长范围在和,只要句子中词元序列的长度不超过,通过上述方法计算的位置编码能保证唯一。
论文中解释采用这种位置编码的原因,一是可以将将长短不一的句子中的词元位置编码为固定维度的向量,二是可以将离散的位置值转化为连续值,三是可以借助正余弦函数的特性,对于某个偏移量,可以基于通过线性变换快速计算出相对位置的,即:
其中,。
为什么采用自注意力机制
论文在这部分,针对输入向量序列为、输出向量序列为、其中和这一类序列问题场景,对自注意力机制,和RNN、CNN在计算性能上作比较。考察每一层的计算量、其中串行操作量(多个串行操作可以并行,以此来衡量并行度),以及数据在网络中进行前向推理和反向梯度更新遍历的最长路径这三种指标,分析结果如图6所示。其中:
- 在每一层的计算量上,自注意力机制需要计算序列中任意两个节点之间的相关性,因此计算复杂度是,而RNN需要依赖序列中前序节点的输出,因此计算复杂度是,CNN需要进行卷积层计算,因此若卷积核宽度为,则计算复杂度是。对于向量维度远大于序列长度的场景(比如论文中的向量维度是512,若句子的词元数量远小于512),则自注意力机制的计算复杂度更优;
- 在串行操作量上,由于RNN需要依赖序列中前序节点的输出,因此计算复杂度是,而自注意力机制和CNN均是;
- 在网络遍历最长路径上,同样由于RNN需要依赖序列中前序节点的输出,因此计算复杂度是,而自注意力机制是。
从中可以看出,自注意力机制可以并行地计算两两节点之间的相关性,从而提升模型训练和推理效率。
Vision Transformer(ViT)
起初Transformer被用于NLP领域,而Google于2021年发表的论文《An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale》提出了Vision Transformer(可简称为ViT),其参考NLP中将句子拆分为词元(Token)向量序列作为Transformer的输入,将图片拆分为块(Patch)向量序列作为Transformer的输入,并将Transformer用于图片分类识别任务,取得了很好的效果,并论证了Transformer在计算机视觉领域也满足Scaling Law,即模型规模越大,数据集越大,其效果越好。
方法
ViT的整体架构如图7所示,其核心结构采用Transformer的编码器。
在输入的处理上,令图片为,其中为图片高,为图片宽,为图片像素的通道数。ViT将图片划分为多个块,每一个块被称为一个Patch,而图片的块,可以类比于句子中的词元。令每个块的长和宽均为,块的数量为,则。
类比NLP中将句子转化为词元向量序列,ViT将图片转化为块向量序列。令块向量序列为,序列的长度即块的数量,为,序列中的每个块使用向量()表示,向量中的每个元素表示块中某个像素某个通道的值,因此,向量的维度为。块向量序列可表示为:
将块向量序列作为Transformer的输入,而Transformer中隐向量的维度为,因此通过矩阵将每个块的原始向量线性映射为维向量。块向量序列可进一步表示为:
类似于BERT在词元序列中增加特殊词元“[class]”,ViT在上述块向量序列的头部增加特殊的维向量,该向量最终经过Transformer编码器的输出作为图片的表征。块向量序列可进一步表示为:
和原始Transformer类似,ViT也会对块序列中的各位置进行位置编码,原块序列长度为,增加后,块序列长度为,各位置编码的向量维度也为,令所有个位置的位置编码为。因此,最终ViT的输入可以表示为:
获得输入后,将输入Transformer的编码器,如图7左侧所示。Transformer的编码器结构如图7右侧所示。编码器共有层,对于第层,将上一层的输出作为该层的输入,进行层归一化(Layer Normalization)后再输入多头自注意力层(Multi-Head Self-Attention),多头自注意力层的输出再与输入进行残差连接得到,上述操作可表示为:
其中,表示层归一化,表示多头自注意力层。
进行层归一化后再输入多层感知机,多层感知机的输出再与输入进行残差连接得到第层的输出,上述操作可表示为:
其中,表示多层感知机,每个多层感知机包含两层,使用GELU作为激活函数。
在输出的处理上,将编码器最后一层输出的第一个块(即对应编码器输入中的)的编码,再进行一次层归一化,得到图片表征向量:
综上,ViT的整体网络结构可表示为:
Transformer编码器中多头注意力层的实现细节之前已介绍,此处不再详述。
在ViT的预训练和微调阶段,ViT会在上述网络结构的基础上,增加分类头,分类头的输入是,输出是图片各分类的概率。预训练阶段的分类头是包含一个隐层的多层感知机,微调阶段的分类头是线性映射矩阵。
效果
ViT通过使用不同的超参配置,构建了三种规模的模型,分别是ViT-Base、ViT-Large、ViT-Huge,如图8所示。三个模型的规模逐渐增长,编码器的层数从12增大到32,编码器中向量的维度从768增大到1280,模型参数量从8千万增大到6亿。
各模型在各分类任务上的准确率如图9所示。从中可以看出,同一个模型(ViT-L/16)在较大数据集(JFT-300M)上预训练的效果优于在较小数据集(ImageNet-21k)上预训练的效果,同一个预训练数据集(JFT-300M)下较大模型(ViT-H/14)的效果优于较小模型的效果(ViT-L/16),同一个预训练数据集(JFT-300M)下ViT(ViT-H/14和ViT-L/16)的效果优于之前基于卷积神经网络的SOTA模型BiT-L。上述结果论证了Transformer在计算机视觉领域也满足Scaling Law,即模型规模越大,数据集越大,其效果越好。
DDPM
扩散模型(Diffusion Model)是继GAN、VAE后的一种生成式模型,而目前在文生图领域比较流行的工具,如OpenAI的DALL-E系列、Stability AI的Stable Diffusion等,均是以上述扩散模型为基础,不断进行算法优化、迭代,取得了令人惊艳的效果。
扩散模型于2015年在论文《Deep Unsupervised Learning Using Nonequilibrium Thermodynamics》中被提出,并于2020年在论文《Denoising Diffusion Probabilistic Models》中被改进、用于图片生成。《Denoising Diffusion Probabilistic Models》中提出的扩散模型算法被称为DDPM。
正向扩散过程
令原始图片样本为,其满足分布。定义前向扩散过程,在步内,每步给样本增加一个小的满足高斯分布的噪声,从而产生个带噪声的样本,整个过程为一个一阶马尔可夫过程,只与有关,可用以下公式表示:
其中,表示给定时,的条件概率,即均值为、方差为的高斯分布,集合用于控制每步的噪声大小。进一步给定时,整个马尔科夫过程的条件概率为各步条件概率的连乘,可用以下公式表示:
正向扩散过程可由图10从右到左的过程表示,其中为原始图片,随着每步增加噪声,图片逐渐变得模糊。
对于上述正向扩散过程,可进一步令,且,则可用以下公式表示:
即是在的基础上,增加一个满足高斯分布的噪声,循环递归,即可进一步推导为在的基础上,增加一个满足高斯分布的噪声。这里使用了高斯分布的一个特性,即两个高斯分布合并后仍是一个高斯分布,例如分布和,合并后的分布为。
反向扩散过程
以上介绍了正向扩散过程,即图10从右到左,对原始图片逐步增加噪声,如果将过程逆向,即图1从左到右,那么就能从满足高斯分布的噪音逐步还原原始图片样本,这就是基于扩散模型生成图片的基本思想,即从到的每一步,在给定时,根据条件概率采样求解,直至最终得到。
而当正向扩散过程每步增加的噪声很小时,反向扩散过程的条件概率也可以认为满足高斯分布,但实际上,我们不能直接求解该条件概率,因为直接求解需要整体数据集合。除直接求解外,另一个方法是训练一个模型近似预估上述条件概率,可用以下公式表示:
从到的每一步,通过模型,输入和,预测的高斯分布的均值和方差,基于预测值,可以从的高斯分布中进行采样,从而得到的一个可能取值,如此循环,直至最终得到的一个可能取值。通过上述反向扩散过程,即可以实现从一个满足高斯分布的随机噪声,生成一张图片。而由于每次预测均是从一个概率密度函数中进行采样,因此,可以保证生成图片的多样性。
更进一步,论文进一步将模型预测的均值和方差转化为预测噪声,并推导出和的关系:
因此,可表示为:
论文将固定为常量,通过模型预测,并使用上述公式的概率密度函数进行采样,从降噪得到,这也是论文标题中“Denosing”的由来。
模型结构
DDPM中预测的模型基于OpenAI于2017年发布的一个U-Net形式的网络结构PixelCNN++。U-Net于2015年在论文《U-Net: Convolutional Networks for Biomedical Image Segmentation》中发布,起初主要用于医学图像的切割,目前作为常用的去噪结构,广泛应用于扩散模型中。
U-Net的网络结构如图11所示,因整体结构形似字母U而得名,U型左侧是4层编码器层,对图片进行降维,U型右侧是4层解码器层,对图片进行升维。编码器的每一层,先是连续的两个卷积层(卷积核维度为3×3)和ReLU层,再接一个池化层进行下采样,然后输入下一层,卷积层的通道数逐层加倍,例如,第一层的输入是572×572的单通道图片,经过两个卷积核维度为3×3、通道为64、无padding的卷积层,输出张量维度分别为570×570×64、568×568×64,再经过一个维度为2×2的最大池化层后,输出张量维度为284×284×64,如此循环,最后一层输出的张量维度为32×32×512。在编码器层和解码器层之间的中间层,经过两个卷积层(卷积核维度为3×3、通道为1024)和ReLU层,输出的张量维度为28×28×1024。解码器的每一层,和编码器类似,也先是连续的两个卷积层(卷积核维度为3×3)和ReLU层,和编码器不同的是,解码器从下层到上层,通过一个上卷积层(卷积核维度为3×3)进行上采样(长、宽维度加倍,但通道缩小),同时,解码器每层的输入除上一层上采样的输出外,还包括同层编码器输出的裁剪。例如,解码器第一层的输入,包括中间层上采样的输出,张量维度为56×56×512,和同层编码器输出的裁剪,张量维度为56×56×512,合并后的张量维度为56×56×1024,经过两个卷积核维度为3×3、通道为512的卷积层,输出张量维度分别为54×54×512、52×52×512。解码器最后一层的输出,再通过一个1×1的卷积层,将原先的64通道映射为指定的通道,因为原始U-Net用于图像切割,即对图像每个像素做分类,所以有多少个分类,即有多少个最终的通道。
而PixelCNN++于2017年在论文《PixelCNN++: Improving the PixelCNN with Discretized Logistic Mixture Likelihood and Other Modifications》中发布,其网络结构如图12所示。图中,矩形区块对应于U-Net中的编码器或解码器层,共3个编码器层、3个解码器层。在每个编码器或解码器中,PixelCNN++在原U-Net两个卷积层的基础上,增加了一个残差连接。DDPM进一步进行网络结构的改进,包括:使用Group Normalization进行归一化;在残差卷积块后增加自注意力层;使用Transformer中Sinusoidal Position Embedding对步数编码成Embedding向量作为模型输入。
DDPM的代码开源,代码地址是:github.com/hojonathanh…,其深度学习框架采用Tensorflow,计算资源采用Google Cloud TPU v3-8。diffusion_tf/models/unet.py中定义了网络结构,核心代码如下所示(增加了部分注释):
with tf.variable_scope(name, reuse=reuse):
# Timestep embedding
# 将步数t编码成Embedding向量
with tf.variable_scope('temb'):
temb = nn.get_timestep_embedding(t, ch)
temb = nn.dense(temb, name='dense0', num_units=ch * 4)
temb = nn.dense(nonlinearity(temb), name='dense1', num_units=ch * 4)
assert temb.shape == [B, ch * 4]
# Downsampling
# 多层编码器层
hs = [nn.conv2d(x, name='conv_in', num_units=ch)]
for i_level in range(num_resolutions):
with tf.variable_scope('down_{}'.format(i_level)):
# Residual blocks for this resolution
# 构造编码器层,残差卷积块+自注意力层,并进行下采样
for i_block in range(num_res_blocks):
h = resnet_block(
hs[-1], name='block_{}'.format(i_block), temb=temb, out_ch=ch * ch_mult[i_level], dropout=dropout)
if h.shape[1] in attn_resolutions:
h = attn_block(h, name='attn_{}'.format(i_block), temb=temb)
hs.append(h)
# Downsample
if i_level != num_resolutions - 1:
hs.append(downsample(hs[-1], name='downsample', with_conv=resamp_with_conv))
# Middle
# 中间层,残差卷积块+自注意力层+残差卷积块
with tf.variable_scope('mid'):
h = hs[-1]
h = resnet_block(h, temb=temb, name='block_1', dropout=dropout)
h = attn_block(h, name='attn_1'.format(i_block), temb=temb)
h = resnet_block(h, temb=temb, name='block_2', dropout=dropout)
# Upsampling
# 多层解码器层
for i_level in reversed(range(num_resolutions)):
with tf.variable_scope('up_{}'.format(i_level)):
# Residual blocks for this resolution
# 构造解码器层,残差卷积块+自注意力层,并进行上采样
for i_block in range(num_res_blocks + 1):
h = resnet_block(tf.concat([h, hs.pop()], axis=-1), name='block_{}'.format(i_block),
temb=temb, out_ch=ch * ch_mult[i_level], dropout=dropout)
if h.shape[1] in attn_resolutions:
h = attn_block(h, name='attn_{}'.format(i_block), temb=temb)
# Upsample
if i_level != 0:
h = upsample(h, name='upsample', with_conv=resamp_with_conv)
assert not hs
# End
# 最后再经过一个卷积层输出
h = nonlinearity(normalize(h, temb=temb, name='norm_out'))
h = nn.conv2d(h, name='conv_out', num_units=out_ch, init_scale=0.)
assert h.shape == x.shape[:3] + [out_ch]
return h
其中,残差卷积块的代码如下(增加了部分注释):
def resnet_block(x, *, temb, name, out_ch=None, conv_shortcut=False, dropout):
B, H, W, C = x.shape
if out_ch is None:
out_ch = C
with tf.variable_scope(name):
h = x
# 对图片进行归一化和非线性转化
h = nonlinearity(normalize(h, temb=temb, name='norm1'))
# 对图片进行卷积
h = nn.conv2d(h, name='conv1', num_units=out_ch)
# add in timestep embedding
# 对步数t的embedding向量进行非线性转化,并合并至图片
h += nn.dense(nonlinearity(temb), name='temb_proj', num_units=out_ch)[:, None, None, :]
# 对合并图片和步数后的输入再进行归一化和非线性转化,并再进行卷积
h = nonlinearity(normalize(h, temb=temb, name='norm2'))
h = tf.nn.dropout(h, rate=dropout)
h = nn.conv2d(h, name='conv2', num_units=out_ch, init_scale=0.)
# 对两次卷积后的输出和原始输入进行残差连接
if C != out_ch:
if conv_shortcut:
x = nn.conv2d(x, name='conv_shortcut', num_units=out_ch)
else:
x = nn.nin(x, name='nin_shortcut', num_units=out_ch)
assert x.shape == h.shape
print('{}: x={} temb={}'.format(tf.get_default_graph().get_name_scope(), x.shape, temb.shape))
return x + h
自注意力层的代码如下(即Transformer中的缩放点积注意力层):
def attn_block(x, *, name, temb):
B, H, W, C = x.shape
with tf.variable_scope(name):
h = normalize(x, temb=temb, name='norm')
q = nn.nin(h, name='q', num_units=C)
k = nn.nin(h, name='k', num_units=C)
v = nn.nin(h, name='v', num_units=C)
w = tf.einsum('bhwc,bHWc->bhwHW', q, k) * (int(C) ** (-0.5))
w = tf.reshape(w, [B, H, W, H * W])
w = tf.nn.softmax(w, -1)
w = tf.reshape(w, [B, H, W, H, W])
h = tf.einsum('bhwHW,bHWc->bhwc', w, v)
h = nn.nin(h, name='proj_out', num_units=C, init_scale=0.)
assert h.shape == x.shape
print(tf.get_default_graph().get_name_scope(), x.shape)
return x + h
训练采样
训练
通过模型预测误差,损失函数采用均方误差(MSE,Mean-Squared Error):
模型训练的目标即最小化上述损失函数,即使模型预测出的噪声和真实噪声尽可能接近。
训练算法如图13所示,采用梯度下降算法,循环下述过程直至模型收敛:
- 对于样本,从中随机采样步数;
- 从高斯分布中采样真实噪声;
- 根据样本和真实噪声,使用前面推导出的公式计算第步正向扩散后带噪声的图片;
- 根据带噪声的图片和步数,使用模型预测噪声,即;
- 根据真实噪声和预测噪声计算损失函数的梯度,即;
- 根据梯度和学习率超参更新模型参数。
采样
采样算法如图14所示,过程如下:
- 从高斯分布中采样完全噪声图片;
- 循环步,步数从到1,直至计算得到,生成最终的图片,对于其中的某一步:
- 根据带噪声的图片和步数,使用模型预测噪声;
- 前面已推导出概率密度函数满足高斯分布,使用公式由噪声计算均值;
- 对于上述概率密度函数,指定方差为常量,根据该分布进行采样,从得到,即。
Improved DDPM
2021年OpenAI发表的论文《Improved Denoising Diffusion Probabilistic Models》,对DDPM算法进行改进,包括噪声Schedule采用余弦函数、对方差进行学习(原DDPM算法将方差固定为常量)等。
改进
噪声Schedule采用余弦函数
原始DDPM算法,使用公式计算第步正向扩散后带噪声的图片,公式中的,,即,表示每步噪声的大小,原始DDPM算法,令随线性增长,从增长到,改进的DDPM算法,令随的变化采用余弦函数:
两种噪声Schedule下,随的变化曲线如图15所示,相比线性函数,余弦函数的下降相对较平缓,因而相对较小,加噪相对较慢,不会过快地对原始图片加入过多的噪声。
Improved DDPM的代码开源,代码地址是:github.com/openai/impr…,其深度学习框架采用PyTorch。训练和采样分别执行以下脚本:
# 训练脚本
python scripts/image_train.py --data_dir path/to/images $MODEL_FLAGS $DIFFUSION_FLAGS $TRAIN_FLAGS
# 采样脚本
python scripts/image_sample.py --model_path /path/to/model.pt $MODEL_FLAGS $DIFFUSION_FLAGS
其中,MODEL_FLAGS、DIFFUSION_FLAGS、TRAIN_FLAGS分别表示模型结构(U-Net)、扩散过程和训练的配置,而基线模型(DDPM)的配置如下:
MODEL_FLAGS="--image_size 64 --num_channels 128 --num_res_blocks 3"
DIFFUSION_FLAGS="--diffusion_steps 4000 --noise_schedule linear"
TRAIN_FLAGS="--lr 1e-4 --batch_size 128"
如果采用余弦函数作为噪声Schedule,可以将DIFFUSION_FLAGS中的noise_schedule设置为cosine,而相应的计算噪声的代码在improved_diffusion/gaussian_diffusion.py中,如下:
def get_named_beta_schedule(schedule_name, num_diffusion_timesteps):
"""
Get a pre-defined beta schedule for the given name.
The beta schedule library consists of beta schedules which remain similar
in the limit of num_diffusion_timesteps.
Beta schedules may be added, but should not be removed or changed once
they are committed to maintain backwards compatibility.
"""
if schedule_name == "linear":
# Linear schedule from Ho et al, extended to work for any number of
# diffusion steps.
scale = 1000 / num_diffusion_timesteps
beta_start = scale * 0.0001
beta_end = scale * 0.02
return np.linspace(
beta_start, beta_end, num_diffusion_timesteps, dtype=np.float64
)
elif schedule_name == "cosine":
return betas_for_alpha_bar(
num_diffusion_timesteps,
lambda t: math.cos((t + 0.008) / 1.008 * math.pi / 2) ** 2,
)
else:
raise NotImplementedError(f"unknown beta schedule: {schedule_name}")
# 入参alpha_bar即公式推导中的f(t)
def betas_for_alpha_bar(num_diffusion_timesteps, alpha_bar, max_beta=0.999):
"""
Create a beta schedule that discretizes the given alpha_t_bar function,
which defines the cumulative product of (1-beta) over time from t = [0,1].
:param num_diffusion_timesteps: the number of betas to produce.
:param alpha_bar: a lambda that takes an argument t from 0 to 1 and
produces the cumulative product of (1-beta) up to that
part of the diffusion process.
:param max_beta: the maximum beta to use; use values lower than 1 to
prevent singularities.
"""
betas = []
for i in range(num_diffusion_timesteps):
t1 = i / num_diffusion_timesteps
t2 = (i + 1) / num_diffusion_timesteps
betas.append(min(1 - alpha_bar(t2) / alpha_bar(t1), max_beta))
return np.array(betas)
对方差进行学习
原始DDPM算法,满足高斯分布的概率密度函数中的方差被固定为常量,其中直接取值。改进的DDPM算法通过模型对也进行了学习,这样可以使用更少的步数、且生成更高质量的图片。
具体如何学习呢?DDPM的论文已推导的取值在和之间,而,图16表示了和之间的关系,从中可见,除了外,其他取值下,和近似相等,所以原始DDPM算法将方差直接取值,而改进的DDPM算法设计了中间向量,由模型预测,并将表示为:
原始DDPM算法的损失函数为:
其中并不包含,因此改进的DDPM算法设计了新的损失函数为:
而的定义如下:
损失函数中被设置为0.001,以减少对的影响。另外梯度更新时,部分不更新涉及的参数,只更新涉及的参数。 如果需要对方差进行学习,可以将训练脚本参数MODEL_FLAGS中的learn_sigma设置为True,这样,模型结构(U-Net)的输出维度增加,增加的部分作为,相关代码在improved_diffusion/script_util.py的create_model方法中,如下:
return UNetModel(
in_channels=3,
model_channels=num_channels,
# 模型结构(U-Net)的输出维度增加,增加的部分作为v
out_channels=(3 if not learn_sigma else 6),
num_res_blocks=num_res_blocks,
attention_resolutions=tuple(attention_ds),
dropout=dropout,
channel_mult=channel_mult,
num_classes=(NUM_CLASSES if class_cond else None),
use_checkpoint=use_checkpoint,
num_heads=num_heads,
num_heads_upsample=num_heads_upsample,
use_scale_shift_norm=use_scale_shift_norm,
)
同时,反向扩散生成图片时,由模型预测方差的代码在improved_diffusion/gaussian_diffusion.py的p_mean_variance方法中,如下:
# 模型预测
model_output = model(x, self._scale_timesteps(t), **model_kwargs)
if self.model_var_type in [ModelVarType.LEARNED, ModelVarType.LEARNED_RANGE]:
assert model_output.shape == (B, C * 2, *x.shape[2:])
# 按列拆分模型输出,后半部分作为v
model_output, model_var_values = th.split(model_output, C, dim=1)
if self.model_var_type == ModelVarType.LEARNED:
model_log_variance = model_var_values
model_variance = th.exp(model_log_variance)
else:
# 按公式exp(v·log(β_t)+(1-v)·log(β_t))计算方差
min_log = _extract_into_tensor(
self.posterior_log_variance_clipped, t, x.shape
)
max_log = _extract_into_tensor(np.log(self.betas), t, x.shape)
# The model_var_values is [-1, 1] for [min_var, max_var].
frac = (model_var_values + 1) / 2
model_log_variance = frac * max_log + (1 - frac) * min_log
model_variance = th.exp(model_log_variance)
而代码如何调整损失函数的计算,在下一节介绍。
训练时采用Importance Sampling
论文进一步发现,将损失函数替换为或后,损失函数取值随着训练迭代变化的曲线比较波动,不易收敛,如图17所示。包含多项,每项对应一个步数,且每项的取值量纲差别较大,如图18所示,而原始DDPM算法训练时随机采样步数,因此论文认为每次训练迭代随机采样步数、并进而计算量纲差别大的导致或的波动。论文通过训练时采用Importance Sampling来解决上述波动问题。Importance Sampling中,可表示为以下公式:
无法提前求解,且在训练过程中会变化,因此,论文对的每一项保留最新的10个取值,并在训练过程中动态更新。训练初期,仍是随机采样步数,直至所有的均有10个取值,再采用Importance Sampling。从图3可以看出,经过Importance Sampling后的随着训练迭代变化的曲线比较平滑,且损失最小。
如果需要使用Importance Sampling后的作为损失函数,可以将训练脚本参数DIFFUSION_FLAGS中的use_kl设置为True、TRAIN_FLAGS中的schedule_sampler设置为loss-second-moment。Importance Sampling的相关代码在improved_diffusion/resample.py的LossSecondMomentResampler类中,如下:
class LossSecondMomentResampler(LossAwareSampler):
def __init__(self, diffusion, history_per_term=10, uniform_prob=0.001):
self.diffusion = diffusion
self.history_per_term = history_per_term
self.uniform_prob = uniform_prob
self._loss_history = np.zeros(
[diffusion.num_timesteps, history_per_term], dtype=np.float64
)
self._loss_counts = np.zeros([diffusion.num_timesteps], dtype=np.int)
def weights(self):
# 训练初期,仍是随机采样步数
if not self._warmed_up():
return np.ones([self.diffusion.num_timesteps], dtype=np.float64)
# 根据L_t的历史值计算p_t
weights = np.sqrt(np.mean(self._loss_history ** 2, axis=-1))
weights /= np.sum(weights)
weights *= 1 - self.uniform_prob
weights += self.uniform_prob / len(weights)
return weights
def update_with_all_losses(self, ts, losses):
for t, loss in zip(ts, losses):
if self._loss_counts[t] == self.history_per_term:
# Shift out the oldest loss term.
self._loss_history[t, :-1] = self._loss_history[t, 1:]
self._loss_history[t, -1] = loss
else:
self._loss_history[t, self._loss_counts[t]] = loss
self._loss_counts[t] += 1
def _warmed_up(self):
return (self._loss_counts == self.history_per_term).all()
使用作为损失函数的相关代码在improved_diffusion/gaussian_diffusion.py的_vb_terms_bpd方法中,如下:
def _vb_terms_bpd(
self, model, x_start, x_t, t, clip_denoised=True, model_kwargs=None
):
"""
Get a term for the variational lower-bound.
The resulting units are bits (rather than nats, as one might expect).
This allows for comparison to other papers.
:return: a dict with the following keys:
- 'output': a shape [N] tensor of NLLs or KLs.
- 'pred_xstart': the x_0 predictions.
"""
true_mean, _, true_log_variance_clipped = self.q_posterior_mean_variance(
x_start=x_start, x_t=x_t, t=t
)
out = self.p_mean_variance(
model, x_t, t, clip_denoised=clip_denoised, model_kwargs=model_kwargs
)
kl = normal_kl(
true_mean, true_log_variance_clipped, out["mean"], out["log_variance"]
)
kl = mean_flat(kl) / np.log(2.0)
decoder_nll = -discretized_gaussian_log_likelihood(
x_start, means=out["mean"], log_scales=0.5 * out["log_variance"]
)
assert decoder_nll.shape == x_start.shape
decoder_nll = mean_flat(decoder_nll) / np.log(2.0)
# At the first timestep return the decoder NLL,
# otherwise return KL(q(x_{t-1}|x_t,x_0) || p(x_{t-1}|x_t))
output = th.where((t == 0), decoder_nll, kl)
return {"output": output, "pred_xstart": out["pred_xstart"]}
效果
针对上述改进,论文在ImageNet 64×64和CIFAR-10这两个数据集上分别进行消融实验以验证各改进的有效性,如图19和图20所示。
其中,有效性指标使用了NLL和FID。NLL(Negative Log Likelihood)等价于损失函数,NLL越小,说明生成图像与真实图像的分布越接近。FID(Fréchet Inception Distance)是另一种用于图像生成质量评估的指标,它可以评估生成图像与真实图像之间的相似度。FID指标的计算方法是使用Inception-v3模型对生成图像和真实图像进行特征提取,并计算两个特征分布之间的Fréchet距离。FID越小,说明生成图像与真实图像越相似。从实验结果上看,噪声Schedule采用余弦函数、对方差进行学习并且训练时损失函数采用Importance Sampling后的,NLL最低,但FID较高,而噪声Schedule采用余弦函数、对方差进行学习并且训练时损失函数采用,在NLL、FID上都能取得较小的值。
另外,论文还和其他基于似然预估的模型进行了对比实验,如图21所示。优化后的DDPM虽然在NLL和FID上还不是SOTA,但相对也是较优的效果,仅次于基于Transformer的网络结构。
加速
DDPM在生成图片时需要从完全噪声开始执行多步降噪操作,而每步操作均需要将当前步带噪声的图片作为输入由模型预测噪声,导致生成图片需要较多的步骤和计算量。论文也采用了《Denoising Diffusion Implicit Models》提出的采样方法——DDIM,减少步数。
Classifier Guidance
Improved DDPM虽然对DDPM进行了改进,但在一些大数据集上(如ImageNet 256×256)生成图片的实验效果(FID)仍是低于GAN。因此,OpenAI继续对DDPM进行改进,在2021年随后又发表了论文《Diffusion Models Beat Gans on Image Synthesis》,在模型结构上进一步优化,同时引入Classifier Guidance技术,通过图片分类器的梯度调节反向扩散过程,在尽量保持图片生成多样性的前提下,提升准确性,从而在多个数据集的实验效果(FID)上超过了GAN,实现了SOTA。论文将改进后的模型称为ADM(Ablated Diffusion Model)。
改进
网络结构
DDPM和Improved DDPM中的模型均使用U-Net,ADM在其网络结构的基础上,进一步增加以下数项改进:
- 增加网络结构的宽度和深度;
- 在注意力机制上,DDPM原先只在16×16这一层增加单头注意力层(缩放点积注意力),而ADM在32×32、16×16、8×8各层均增加了多头注意力层;
- 在上下采样上,DDPM原先在下采样使用池化或卷积层、在上采样使用插值或卷积层,而ADM使用残差卷积块;
ADM的代码开源,代码地址是:github.com/openai/guid…,其是在Improved DDPM的代码基础上进行修改。网络结构定义的相关代码在guided-diffusion/unet.py的UNetModel类中,例如,ADM使用残差块卷积进行下采样的代码如下:
if level != len(channel_mult) - 1:
# 除编码器最后一层外的其他层,需要进行下采样输出到下一层
out_ch = ch
self.input_blocks.append(
TimestepEmbedSequential(
# 若标记resblock_updown为True,则使用残差卷积块进行下采样
ResBlock(
ch,
time_embed_dim,
dropout,
out_channels=out_ch,
dims=dims,
use_checkpoint=use_checkpoint,
use_scale_shift_norm=use_scale_shift_norm,
# 设置残差卷积块中需进行下采样
down=True,
)
if resblock_updown
else Downsample(
ch, conv_resample, dims=dims, out_channels=out_ch
)
)
)
ch = out_ch
input_block_chans.append(ch)
ds *= 2
self._feature_size += ch
自适应组归一化
ADM还使用了自适应组归一化(Adaptive Group Normalization,AdaGN),组归一化如图22最右侧所示,即对一个图片样本的所有像素,按通道分组进行归一化,而自适应归一化可表示为以下公式:
其中,是残差卷积块中第一个卷积层的输出,、分别是步数和图片分类的Embedding向量经过线性层后的投影。自适应归一化的代码如下所示:
# 经过第一个卷积层的输出
h = self.in_layers(x)
......
out_norm, out_rest = self.out_layers[0], self.out_layers[1:]
# 取y_s和y_b
scale, shift = th.chunk(emb_out, 2, dim=1)
# 按y_s * GroupNorm(h) + y_b进行自适应组归一化
h = out_norm(h) * (1 + scale) + shift
# 经过第二个卷积层输出
h = out_rest(h)
论文通过实验发现,使用自适应组归一化能够进一步优化FID。
Classifier Guidance
除了在网络结构上精心设计和优化外,GAN还在有条件(已知图片类别)的图片生成中大量使用了图片类别信息。基于此,ADM一方面在自适应组归一化中引入图片类别的Embedding向量作为模型输入,另一方面设计了Classifier Guidance机制,通过引入一个分类器指导反向扩散过程:预先使用带噪声的图片训练分类器实现对类别的预测;在逐步反向扩散生成图片时,DDPM在每一步基于扩散模型预测噪声和方差,并由公式计算得到,即得到了高斯分布的均值和方差,在此基础上,ADM使用分类器输出对数的梯度对均值进行调整,并使用调整均值后的高斯分布进行采样得到,均值调整公式如下所示:
其中,系数被称为Guidance Scale,论文通过实验发现随着的增加,生成图片的质量会提升,但多样性会减少。引入Classifier Guidance后的采样算法步骤如图23所示。
ADM中使用的分类器网络结构和扩散模型网络结构近似,均采用U-Net,但只使用编码器层和中间层,而没有解码器层,另外,由于分类器的目标是预测类别,因此类别没有作为输入。分类器网络结构定义的相关代码在guided-diffusion/unet.py的UNetModel类中。
在分类器和扩散模型训练完成后,便可使用其进行图片采样。采样时使用分类器输出梯度对均值进行调整的代码在guided-diffusion/gaussian_diffusion.py的condition_mean方法中,如下所示:
def condition_mean(self, cond_fn, p_mean_var, x, t, model_kwargs=None):
"""
Compute the mean for the previous step, given a function cond_fn that
computes the gradient of a conditional log probability with respect to
x. In particular, cond_fn computes grad(log(p(y|x))), and we want to
condition on y.
This uses the conditioning strategy from Sohl-Dickstein et al. (2015).
"""
# 使用分类器输出分类器梯度
gradient = cond_fn(x, self._scale_timesteps(t), **model_kwargs)
# 根据分类器梯度、均值和方差计算新均值
new_mean = (
p_mean_var["mean"].float() + p_mean_var["variance"] * gradient.float()
)
return new_mean
而其中使用分类器输出分类器梯度的cond_fn方法代码如下所示:
def cond_fn(x, t, y=None):
assert y is not None
with th.enable_grad():
x_in = x.detach().requires_grad_(True)
# 分类器输出分类结果
logits = classifier(x_in, t)
log_probs = F.log_softmax(logits, dim=-1)
selected = log_probs[range(len(logits)), y.view(-1)]
# 计算梯度并返回
return th.autograd.grad(selected.sum(), x_in)[0] * args.classifier_scale
效果
论文在多个数据集上对ADM(仅做网络结构优化)、ADM-G(同时引入Classifier Guidance机制)和其他模型进行了对比实验,从FID指标上,ADM、特别是ADM-G超过了GAN,实现了SOTA。
Classifier-Free Guidance
在Classifier Guidance机制被提出后,紧接着Google于2021年发表了论文《Classifier-Free Diffusion Guidance》。这篇论文指出Classifier Guidance仍存在以下不足:一是Classifier Guidance需要额外训练分类器,二是Classifier Guidance会导致基于梯度的对抗攻击,欺骗FID、IS这类基于分类器的评估指标。因此,这篇论文提出了一种不需要训练分类器、但仍可以基于类别信息指导反向扩散过程的机制——Classifier-Free Guidance。
之前ADM等扩散模型可使用类别信息进行有条件的图片生成,由模型基于和类别预测噪声,或是不使用类别信息进行无条件的图片生成,由模型仅基于预测噪声,但这两种情况需要分别训练模型,而Classifier-Free Guidance的思想是在模型训练时,按一定比例丢弃类别信息,使得模型能够同时学习有条件的图片生成和无条件的图片生成,这样在采样生成图片时,由同一个模型预测和,并使用两者的差值等价替换分类器输出的梯度对进行调整,调整公式如下:
再基于调整后的计算均值,并从高斯分布中采样得到。
CLIP Guidance
Classifier Guidance使用类别信息指导反向扩散过程,那是否可以使用除类别外的其他信息指导反向扩散过程?2022年发表的论文《More Control for Free! Image Synthesis with Semantic Diffusion Guidance》就尝试使用了其他信息,其中包括在多模态领域应用比较广泛的CLIP模型。
CLIP模型包括两部分,图片编码器和文本编码器,其中为图片,为文本。训练阶段,采用对比学习,使得正确图片、文本对的点积尽可能大,错误图片、文本对的点积尽可能小。因此,在推理阶段,可以进行文本和图片相关性的比较。关于CLIP模型的详细介绍,可以阅读原论文《Learning Transferable Visual Models From Natural Language Supervision》或《AIGC系列-CLIP论文阅读笔记》。
在Classifier Guidance中可以使用CLIP模型替换分类器,对于,使用的梯度调整,公式如下所示:
和Classifier Guidance中的分类器类似,需使用带噪声的图片和文本对训练CLIP模型以获得正确的梯度。
GLIDE
在上述工作的基础上,OpenAI于2022年发表了论文《GLIDE: Towards Photorealistic Image Generation and Editing with Text-Guided Diffusion Models》,其中发布了GLIDE(Guided Language to Image Diffusion for Generation and Editing)模型,用于基于文本的图片生成。图25是使用GLIDE模型基于文本生成的图片。
基于文本的图片生成
一般的扩散模型从随机采样的高斯噪声开始,不能生成特定的图片,而GLIDE在已有扩散模型的基础上,使用文本信息指导扩散过程,对于带噪声的图片和文本,能够通过模型预测,从而逐步降噪,实现了基于文本的图片生成。
具体实现上,GLIDE基于ADM模型,但模型参数和训练数据规模更大,模型参数达到35亿。GLIDE先将文本转化为长度为的token序列,再通过Transformer输出文本的Embedding向量,最后使用文本Embedding向量替换原ADM模型输入中的类别Embedding向量。另外,文本Embedding向量还会经过投影与注意力层中的、拼接在一起,通过注意力机制指导扩散过程。
GLIDE的代码开源,代码地址是:github.com/openai/guid…。通过Transformer输出文本的Embedding向量作为模型输入的代码在glide_text2im/text2im_model.py中,如下所示:
def forward(self, x, timesteps, tokens=None, mask=None):
hs = []
emb = self.time_embed(timestep_embedding(timesteps, self.model_channels))
if self.xf_width:
text_outputs = self.get_text_emb(tokens, mask)
xf_proj, xf_out = text_outputs["xf_proj"], text_outputs["xf_out"]
emb = emb + xf_proj.to(emb)
else:
xf_out = None
h = x.type(self.dtype)
for module in self.input_blocks:
h = module(h, emb, xf_out)
hs.append(h)
h = self.middle_block(h, emb, xf_out)
for module in self.output_blocks:
h = th.cat([h, hs.pop()], dim=1)
h = module(h, emb, xf_out)
h = h.type(x.dtype)
h = self.out(h)
return h
注意力层拼接文本Embedding向量的代码在glide_text2im/unet.py中,如下所示:
class QKVAttention(nn.Module):
"""
A module which performs QKV attention. Matches legacy QKVAttention + input/ouput heads shaping
"""
def __init__(self, n_heads):
super().__init__()
self.n_heads = n_heads
def forward(self, qkv, encoder_kv=None):
"""
Apply QKV attention.
:param qkv: an [N x (H * 3 * C) x T] tensor of Qs, Ks, and Vs.
:return: an [N x (H * C) x T] tensor after attention.
"""
bs, width, length = qkv.shape
assert width % (3 * self.n_heads) == 0
ch = width // (3 * self.n_heads)
q, k, v = qkv.reshape(bs * self.n_heads, ch * 3, length).split(ch, dim=1)
if encoder_kv is not None:
assert encoder_kv.shape[1] == self.n_heads * ch * 2
ek, ev = encoder_kv.reshape(bs * self.n_heads, ch * 2, -1).split(ch, dim=1)
# 拼接文本Embedding向量到k
k = th.cat([ek, k], dim=-1)
# 拼接文本Embedding向量到v
v = th.cat([ev, v], dim=-1)
scale = 1 / math.sqrt(math.sqrt(ch))
weight = th.einsum(
"bct,bcs->bts", q * scale, k * scale
) # More stable with f16 than dividing afterwards
weight = th.softmax(weight.float(), dim=-1).type(weight.dtype)
a = th.einsum("bts,bcs->bct", weight, v)
return a.reshape(bs, -1, length)
Classifier-Free Guidance
GLIDE也使用了Classifier-Free Guidance机制,只是将类别替换为文本,因此,对模型所预测噪声进行调整的公式如下:
GLIDE在上一节已训练得到基于文本的模型、可预测的基础上,对模型进行微调,将20%的文本Token序列替换成空序列,从而使得模型在具备预测的基础上,能够进一步预测,从而在采样时,能够基于调整后的噪声降噪生成图片。
CLIP Guidance
GLIDE也使用了CLIP Guidance机制,但从实验结果上,其效果不如Classifier-Free Guidance。
Stable Diffusion
Stable Diffusion是由Stability AI发布的开源文生图、图生图工具,其GitHub地址为:github.com/Stability-A…。Stable Diffusion基于LDM(Latent Diffusion Model),介绍LDM的论文《High-Resolution Image Synthesis with Latent Diffusion Models》于2021年发表。LDM相较于DDPM,在隐向量空间进行反向扩散过程,而不是在原始图片上,从而能够减少训练资源、提升推理速度。《The Illustrated Stable Diffusion》是一篇介绍Stable Diffusion的经典博客,以下内容是对这篇博客的翻译和精简。
整体架构
Stable Diffusion的整体架构如图26所示,其先在隐向量空间中进行反向扩散过程得到图片的Embedding向量,再通过图片解码器得到最终的图片,该流程包含以下核心组件:
- Text Encoder,将文本转化为Embedding向量,其中包含文本的语义信息;
- Image Information Creator,Stable Diffusion的核心,使用扩散模型,基于文本Embedding向量通过逐步的反向扩散过程,生成相应的图片Embedding向量,反向扩散步数默认50到100;反向扩散过程在图片的隐向量空间进行,并不是在图片的原始像素空间,这样相比于原先的扩散模型可以减少计算量;
- Image Decoder,基于图片Embedding向量生成图片。
Stable Diffusion各组件的模型细节:
- Text Encoder,使用CLIP的文本编码器,输入文本,输出77个Token的Embedding向量,每个Token的Embedding向量的维度为768;
- Image Infomation Creator,使用扩散模型,网络结构为UNet,输入文本Embedding向量、步数Embedding向量、带噪声的图片Embedding向量(64×64、4通道的张量),输出图片Embedding向量(64×64、4通道的张量);
- Image Decoder,使用自编码器模型中的解码器部分,输入图片Embedding向量(64×64、4通道的张量),输出生成的图片(512×512、3通道的张量)。
Image Infomation Creator在反向扩散过程中逐步去噪的是图片Embedding向量(隐向量),如果将每步去噪后的图片Embedding向量直接输入Image Decoder并输出相应的图片,可以看到初始的Embedding向量对应完全噪声,中间的Embedding向量对应的图片逐渐清晰,直至得到最终生成的图片,如图27所示。
模型训练
Stable Diffusion基于LDM(Latent Diffusion Model),LDM论文中的模型结构如图28所示,和上一节的Stable Diffusion整体架构图基本一致,其中表示原始图片,表示生成的图片,、分别表示自编码器的编码器和解码器(已预训练),通过得到的Embedding向量,基于进行正向扩散过程,逐步增加噪声得到,将作为训练样本,进行LDM模型的训练,LDM模型的输出仍是预测噪声,损失函数为:
扩散模型训练过程可以参见DDPM算法中的介绍。模型训练完成、进行采样生成时,基于完全噪声进行反向扩散,通过模型逐步预测噪声并降噪,得到最终的图片Embedding向量,再通过得到。
基于已预训练的自编码器,使用编码器生成带噪声的图片Embedding向量用于扩散模型训练,并使用解码器基于扩散模型采样、生成的图片Embedding向量解码得到图片的流程如图29所示。
基于文本生成
Stable Diffusiion在图片生成过程中引入文本信息作控制,先使用CLIP的文本编码器将文本转化为Embedding向量,而扩散模型所使用网络结构UNet由多层ResNet组成,在每层ResNet输出的后面,增加一个Attention层,将前序ResNet的输出,和文本Embedding向量作为该Attention层的输入,通过注意力机制基于文本调整前序ResNet的输出,再输入到下一个ResNet中,整体流程如图30所示。
用公式描述该过程,Attention层可用以下公式表示:
其中表示前序ResNet的输出,表示文本经过CLIP文本编码器后的Embedding向量,、、表示Attention层中用于线性投影的参数矩阵,即将文本的Embedding向量作为注意力机制中的和,将前序ResNet的输出作为注意力机制中的。若进一步了解注意力机制,可以参见Transformer部分的介绍。
Diffusion Transformer(DiT)
2022年由William Peebles和谢赛宁发表的论文《Scalable Diffusion Models with Transformers》提出了Diffusion Transformer(可简称为DiT)。原先的扩散模型中,噪声预测的模型主要使用U-Net,而DiT的创新点是在LDM的基础上,使用Transformer替换U-Net,并论证了DiT在图片生成领域也满足Scaling Law。目前,William Peebles是OpenAI Sora项目的负责人之一,而Sora的技术报告指出DiT是其核心技术之一。
方法
DiT整体基于LDM,在隐空间中进行逆向扩散过程。在逆向扩散过程的每一步,将带噪声的隐空间图片和条件信息(当前扩散步数和图片类别)作为模型输入,由模型预测噪声和方差(DiT参考了Improved DDPM算法,既预测噪声,也预测方差),然后基于预测的噪声和方差对带噪声的隐空间图片进行采样、去噪。和LDM不同之处是,LDM中的预测模型采用U-Net,DiT中的预测模型采用Transformer。以下主要介绍DiT如何在隐空间中使用Transformer预测噪声和方差。
按块切分图片
和ViT类似,DiT也将图片按块进行切分。因为LDM是在图片编码压缩(如原始图片长、宽为256,通道数为3,编码压缩后的图片长、宽为32,通道数为4)后的隐空间(Latent Space)进行扩散过程,所以DiT实际是在编码压缩后的隐空间图片上按块进行切分,如图31所示,令编码压缩后的图片长和宽均为、每个像素的通道数为,块的长和宽均为,则块序列的长度。和ViT类似,每个块通过一个线性映射被转化为维向量,且每个块的维向量也会叠加其相应的位置编码。
编码器设计
隐空间图片被按块切分、转化为块向量序列后,DiT再将块向量序列和条件信息(包括扩散过程当前的步数、图片类别以及图片的自然语言描述)的Embedding向量输入层Transfomer的编码器,每层编码器为一个Block,如图32所示。
DiT的Block在原Transformer编码器结构的基础上进行升级,共设计了4种结构:
- In-Context conditioning,即对原Transformer编码器结构不做改动,只是将扩散过程当前的步数、图片类别的Embedding向量追加到块向量序列的后面;
- Cross-attention block,扩散过程当前的步数、图片类别的Embedding向量不再追加到块向量序列的后面,而是将两者拼接在一起后,作为一个独立的长度为2的条件信息向量序列,并在原Transformer编码器的多头自注意力层和多层感知机之间,增加一个多头交叉注意力层,多头自注意力层中的、、均取自块向量序列,而多头交叉注意力层和LDM中的使用方式类似,其中的取自块向量序列,、取自条件信息向量序列;增加多头交叉注意力层会增加约15%的计算量;
- Adaptive layer norm(adaLN) block,在原Transformer编码器的层归一化后增加自适应层归一化(adaptive layer norm,adaLN);将扩散过程当前的步数、图片类别的Embedding向量拼接在一起,作为一个新的多层感知机的输入,由其预测原Transformer编码器中多头自注意力层和多层感知机的自适应层归一化的缩放和偏移参数、和、;
- adaLN-Zero block,将自适应层归一化的参数初始化为0,即初始时残差连接模块等价为Identity函数;另外,在原Transformer编码器中多头自注意力层和多层感知机的输出后再增加一层缩放,并由预测自适应层归一化参数的多层感知机预测上述缩放参数和。
论文通过实验论证adaLN-Zero block在上述4种结构中的效果最好,因此,后续如无特别说明,DiT的Block均采用adaLN-Zero block。DiT的代码地址是:github.com/facebookres…,其中,adaLN-Zero block的代码如下所示:
class DiTBlock(nn.Module):
"""
A DiT block with adaptive layer norm zero (adaLN-Zero) conditioning.
"""
def __init__(self, hidden_size, num_heads, mlp_ratio=4.0, **block_kwargs):
super().__init__()
self.norm1 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
self.attn = Attention(hidden_size, num_heads=num_heads, qkv_bias=True, **block_kwargs)
self.norm2 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
mlp_hidden_dim = int(hidden_size * mlp_ratio)
approx_gelu = lambda: nn.GELU(approximate="tanh")
self.mlp = Mlp(in_features=hidden_size, hidden_features=mlp_hidden_dim, act_layer=approx_gelu, drop=0)
self.adaLN_modulation = nn.Sequential(
nn.SiLU(),
nn.Linear(hidden_size, 6 * hidden_size, bias=True)
)
def forward(self, x, c):
shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = self.adaLN_modulation(c).chunk(6, dim=1)
x = x + gate_msa.unsqueeze(1) * self.attn(modulate(self.norm1(x), shift_msa, scale_msa))
x = x + gate_mlp.unsqueeze(1) * self.mlp(modulate(self.norm2(x), shift_mlp, scale_mlp))
return x
解码器设计
最后一层Block的输出也是各个块的维向量,通过线性映射将各个块的维向量转化为维向量,即转化后向量的每个元素对应到块中每个像素、每个通道的噪声或方差,然后合并各个块通过解码器输出的向量,得到输入模型的带噪声的隐空间图片每个像素、每个通道的噪声和方差,然后基于Improved DDPM算法中的采样方法,对带噪声的隐空间图片进行降噪。
效果
DiT通过使用不同的超参配置,构建了四种规模的模型,分别是DiT-S、DiT-B、DiT-L、DiT-XL,如图33所示。四个模型的规模逐渐增长,Block的层数从12增大到28,Block中向量的维度从384增大到1152,模型计算量从1.4Gflops增大到29.1Gflops。
论文评估模型规模(S、B、L、XL)和块的大小(8、4、2)对图片生成效果的影响,如图34所示。图34中的7张图表,横坐标为训练步数,纵坐标为FID值(值越小,图片生成效果越好)。图34中第一行3张图表为块的大小分别是8、4、2时,各规模模型的FID值,说明块的大小固定时,模型规模越大,图片生成效果越好,第二行4张图表为模型规模分别是S、B、L、XL时,各块大小的FID值,说明模型的规模固定时,块的大小越小,图片生成效果越好。
不同模型规模和块的大小下生成图片的对比如图35所示。对于图中红框部分,模型规模从左到右逐渐增大(S、B、L、XL),块的尺寸从上到下逐渐减少(8、4、2,即块向量序列从上到下逐渐增大),从中可以直观地感受到,从左上到右下,模型越大,块越小,生成的图片效果越好,但计算量也越大。
基于类别的图片生成各模型对比如图36和图37所示,DiT-XL实现了SOTA。
Sora
2024年2月15日,OpenAI发布了Sora的技术报告《Video Generation Models as World Simulators》,其中对Sora的技术原理做了简要的介绍。Sora是一个生成式模型,可以基于文本描述、图片和视频生成新的视频或图片,其底层技术类似DiT,在隐空间进行逆向扩散过程,基于条件信息使用Transformer对带噪声的隐空间视频进行噪声预测,并去噪,最后通过解码器生成像素空间的清晰视频。
方法
按块切分视频
视频压缩网络
在LDM、DiT中,通过编码器将像素空间的图片编码压缩至隐空间,在隐空间进行逆向扩散过程,生成隐空间图片,再通过解码器将隐空间图片解码还原成像素空间的图片。
视频可以认为是由多帧图片组成,因此和LDM、DiT类似,Sora也使用编码器将像素空间的视频编码压缩至隐空间,压缩至隐空间的视频同样具有原视频空间和时间的信息,对于视频的每帧图片,类似LDM、DiT,将其压缩至隐空间图片,构成隐空间的空间信息,而隐空间的各帧图片序列,构成隐空间的时间信息。
同样和LDM、DiT类似,Sora也使用解码器将生成的隐空间视频解码还原至像素空间。
按块切分视频
类似DiT对隐空间图片按块切分,将隐空间图片转化为块(Patch)向量序列,隐空间视频也是由多个隐空间图片构成,因此也可以将隐空间视频的各帧隐空间图片按块切分,得到各隐空间图片的块向量序列,再合并各隐空间图片的块向量序列,得到隐空间视频的块向量序列,该块向量序列同样具有原视频空间和时间的信息。
类似NLP中,通过将句子切分为词元向量序列后,可以在Transformer中处理最大词元向量序列长度内任意长度的句子,Sora中通过将隐空间视频切分为块向量序列后,也可以处理并生成任意分辨率和播放时长的的视频。
视频生成
Sora本质是一个扩散模型,报告中并没有详细介绍其细节,只是介绍其使用类似DiT的技术方案,并给出了如图39所示的示例。关于DiT的细节,可以参见上一章节的详细介绍。另外,和DiT类似,Sora也论证了其满足Scaling Law,即模型规模越大、计算量越大,其生成的视频效果越好。
整体架构
在《复刻Sora有多难?一张图带你读懂Sora的技术路径》这篇文章中,通过图40描述Sora的整体架构,该架构与类似DiT,在LDM的基础上,将扩散过程中预测噪声的模型由U-Net替换为Transformer。
效果
可变的分辨率、播放时长和宽高比
得益于将视频按块切分为块向量序列,相比过去的方法只能生成固定分辨率和播放时长(且较短)的视频,Sora可以生成不同分辨率、不同播放时长和不同宽高比的视频。
文生视频
为了训练Sora能够基于文本生成视频,需要大量带有文本描述的视频作为训练数据集。Sora采用类似OpenAI文生图模型——DALL-E3中的方法,先训练文本生成模型,为训练集中的视频生成文本描述。另外,Sora在文生视频时,先将用户输入的简短提示输入ChatGPT,由ChatGPT生成详细的符合SORA规范的文本描述,再将详细的文本描述输入Sora,从而能够生成符合用户提示的高质量的视频。
以下是文生视频的一个例子的截图。用户提示为“a woman wearing blue jeans and a white-shirt taking a pleasant stroll in Johanesburg, South Africa during a beautiful sunset”(一位身穿蓝色牛仔裤和白色衬衫的女人在南非约翰内斯堡美丽的日落中愉快地散步),而所生成的视频中均准确体现了“日落”、“蓝色牛仔裤”、“白色衬衫”、“散步”等描述。
图生视频、视频生视频
除了基于文本生成视频,Sora还可以基于图片、视频生成视频,基于视频生成视频时,Sora可以:
- 对已有视频按时序进行前向和后向扩展,并可生成一段无限循环的视频(如绕圈的骑行视频);
- 基于文本描述对视频进行编辑(如将汽车行驶的环境由山林变成丛林);
- 将两个不同的视频拼接在一起,两个视频之间进行平滑的过渡。
文生图
将图片看作只有一帧的视频,Sora也支持生成2048x2048内任意尺寸的图片,以下是文生图的一个例子的截图。用户提示为“Vibrant coral reef teeming with colorful fish and sea creatures”(充满多彩鱼类和海洋生物的珊瑚礁)。
世界模拟能力
Sora还具备模拟真实物理世界的能力,包括:
- 当镜头移动时,视频中的物体变化符合三维透视原理;
- 保持视频的连贯性,即使某个物体在视频中被遮挡,该物体在遮挡前后能够保持连贯性;
- 模拟动作对环境的影响,例如当一个人咬汉堡时,会在汉堡上留下咬痕。
Sora还能够模拟数字世界,例如,若文本描述中包含“Minecraft”(游戏我的世界),则Sora能够生成该游戏玩家视角的视频。
局限
Sora的技术报告也说明了其存在的一些局限,比如Sora并不能总是准确模拟真实物理世界,一个例子是玻璃杯掉落时,没有准确模拟玻璃的破裂。
参考文献
- 《Attention Is All You Need》
- 《An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale》
- 《U-Net: Convolutional Networks for Biomedical Image Segmentation》
- 《PixelCNN++: Improving the PixelCNN with Discretized Logistic Mixture Likelihood and Other Modifications》
- 《Deep Unsupervised Learning Using Nonequilibrium Thermodynamics》
- 《Denoising Diffusion Probabilistic Models》
- 《Improved Denoising Diffusion Probabilistic Models》
- 《Denoising Diffusion Implicit Models》
- 《Diffusion Models Beat Gans on Image Synthesis》
- 《Classifier-Free Diffusion Guidance》
- 《More Control for Free! Image Synthesis with Semantic Diffusion Guidance》
- 《GLIDE: Towards Photorealistic Image Generation and Editing with Text-Guided Diffusion Models》
- 《The Illustrated Stable Diffusion》
- 《High-Resolution Image Synthesis with Latent Diffusion Models》
- 《Scalable Diffusion Models with Transformers》
- 《Video Generation Models as World Simulators》
- 《复刻Sora有多难?一张图带你读懂Sora的技术路径》