图像生成模型 Stable Diffusion|CVPR ’22 Oral

项目地址:https://ommer-lab.com/research/latent-diffusion-models/

试玩: https://huggingface.co/spaces/stabilityai/stable-diffusion

High-Resolution Image Synthesis with Latent Diffusion Models

Stable Diffusion 是一个“文本到图像”的人工智能模型。近日,Stable AI 公司向公众开放了它的预训练模型权重。当输入一个文字描述时,Stable Diffusion 可以生成 512×512 像素的图像,这些图像如相片般真实,反映了文字描述的场景。

这个项目先是经历了早期的代码发布,而后又向研究界有限制地发布了模型权重,现在模型权重已经向公众开放。对于最新版本,任何人都可以在为普通消费者设计的硬件上下载和使用 Stable Diffusion。该模型不仅支持文本到图像的生成,而且还支持图像到图像的风格转换和放大。与之一同发布的还有 DreamStudio 测试版,这是一个用于该模型的 API 和 Web 用户界面。

Stable AI 公司表示:

“Stable Diffusion 是一个文本到图像的模型,它将使数十亿人在几秒钟内创造出令人惊叹的艺术。它在速度和质量上的突破意味着它可以在消费者级的 GPU 上运行。这将允许研究人员和公众在一系列条件下运行它,并使图像生成普及化。我们期待着有围绕这个模型和其他模型的开放生态系统出现,以真正探索潜伏空间的边界。”

Latent Diffusion 模型(LDM)是 Stable Diffusion 模型建立的一种图像生成方法。LDM 通过在潜伏表示空间(latent representation space)中迭代“去噪”输入来创建图像,然后将表示解码为完整的图像,这与其他著名的图像合成技术,如生成对抗网络(GAN)和 DALL-E 采用的自动回归方法不同。最近的 IEEE/CVF 计算机视觉和模式识别会议(CVPR)上有一篇关于 LDM 的论文,它是由慕尼黑路德维希-马克西米利安大学的机器视觉和学习研究小组创建的。今年早些时候,InfoQ 也报道的另一个基于扩散的图片生成 AI 是谷歌的 Imagen 模型。

Stable Diffusion 可以支持众多的操作。与 DALL-E 类似,它可以生成一个高质量的图像,并使其完全符合所需图像的文字描述。我们也可以使用一个直观的草图和所需图像的文字描述,从而创建一个看起来很真实的图像。类似的“图像到图像”的能力可以在 Meta AI 的 Make-A-Scene 模型中找到,该模型刚发布不久。

一些人公开分享了 Stable Diffusion 创建的照片的例子,Stable AI 的首席开发人员 Katherine Crowson 也在 Twitter 上分享了许多照片。毫无疑问,基于人工智能的图片合成技术将对艺术家和艺术界产生影响,这令一些观察家感到担忧。值得注意的是,在 Stable Diffusion 发布的同一周,一幅由人工智能生成的作品在科罗拉多州博览会的艺术竞赛中获得了最高荣誉。

Stable Diffusion 的源代码可以在 GitHub 上查阅。

试玩地址: https://huggingface.co/spaces/stabilityai/stable-diffusion

Contribution

  • Diffusion model是一种likelihood-based的模型,相比GAN可以取得更好的生成效果。然而该模型是一种自回归模型,需要反复迭代计算,因而训练和推理都十分昂贵。本文提出一种diffusion的过程改为在latent space上做的方法,从而大大减少计算复杂度,同时也能达到十分不错的生成效果。( “democratizing” research on DMs),在unconditional image synthesis, inpainting, super-resolution都能表现不错~
  • 相比于其它进行压缩的方法,本文的方法可以生成更细致的图像,并且在高分辨率(风景图之类的,最高达10242px都无压力)的生成也表现得很好。
  • 提出了cross-attention的方法来实现多模态训练,使得class-condition, text-to-image, layout-to-image也可以实现。
方法We condition LDMs either via concatenation or by a
more general cross-attention mechanism.

整体框架如图,先训练好一个AutoEncoder(包括一个encoder和decoder)。因此,我们可以利用encoder压缩后的数据做diffusion操作,再用decoder恢复即可。

  • Autoencoder训练: L1/L2loss来作为重建损失,用GAN来做对抗攻击?,用KL loss来把latent space拉到正态分布,防止搜索空间过大
  • 用了encoder降维后,就可以使用latent space diffusion了~ 具体扩散过程其实没有变,只不过现在扩散和重建的目标为latent space的向量了。Diffusion model具体实现为 time-conditional UNet。

为了引入conditioning的信息,提出了domain specific encoder τθ(y)不同模态的(比如text, class, image…)转成中间表达(intermediate representation),再利用cross-attention来嵌入到UNet中去。

Experiments

展示一些可用的任务:

  • layout-to-image 输入bounding box输出图像。
  • text-to-image输入文本,输出图像。
  • 输入bounding box输出图像。
  • 输入文本,输出图像
    • 输入landscape输出高分辨率的风景图。
    • 超分辨率
    • inpainting (图像修复/编辑)

    效率对比。大概时间上缩短为1/3~ 并且,FID的值更小。

    Diffusion Model 综述

    Diffusion Models: A Comprehensive Survey of Methods and Applications来自加州大学&Google Research的Ming-Hsuan Yang、北京大学崔斌实验室以及CMU、UCLA、蒙特利尔Mila研究院等众研究团队,首次对现有的扩散生成模型(diffusion model)进行了全面的总结分析,从diffusion model算法细化分类、和其他五大生成模型的关联以及在七大领域中的应用等方面展开,最后提出了diffusion model的现有limitation和未来的发展方向

    论文:https://arxiv.org/abs/2209.00796

    摘自:AI科技评论

    github链接:https://github.com/YangLing0818/Diffusion-Models-Papers-Survey-Taxonomy(扩散模型论文汇总)

    介绍

    扩散模型(diffusion models)是深度生成模型中新的SOTA。扩散模型在图片生成任务中超越了原SOTA:GAN,并且在诸多应用领域都有出色的表现,如计算机视觉,NLP、波形信号处理、多模态建模、分子图建模、时间序列建模、对抗性净化等。此外,扩散模型与其他研究领域有着密切的联系,如稳健学习、表示学习、强化学习。然而,原始的扩散模型也有缺点,它的采样速度慢,通常需要数千个评估步骤才能抽取一个样本;它的最大似然估计无法和基于似然的模型相比;它泛化到各种数据类型的能力较差。如今很多研究已经从实际应用的角度解决上述限制做出了许多努力,或从理论角度对模型能力进行了分析。然而,现在缺乏对扩散模型从算法到应用的最新进展的系统回顾。为了反映这一快速发展领域的进展,我们对扩散模型进行了首个全面综述。我们设想我们的工作将阐明扩散模型的设计考虑和先进方法,展示其在不同领域的应用,并指出未来的研究方向。此综述的概要如下图所示:

    尽管diffusion model在各类任务中都有着优秀的表现,它仍还有自己的缺点,并有诸多研究对diffusion model进行了改善。为了系统地阐明diffusion model的研究进展,我们总结了原始扩散模型的三个主要缺点,采样速度慢,最大化似然差、数据泛化能力弱,并提出将的diffusion models改进研究分为对应的三类:采样速度提升、最大似然增强和数据泛化增强。我们首先说明改善的动机,再根据方法的特性将每个改进方向的研究进一步细化分类,从而清楚的展现方法之间的联系与区别。在此我们仅选取部分重要方法为例, 我们的工作中对每类方法都做了详细的介绍,内容如图所示:

    在分析完三类扩散模型后,我们将介绍其他的五种生成模型GAN,VAE,Autoregressive model, Normalizing flow, Energy-based model。考虑到扩散模型的优良性质,研究者们已经根据其特性将diffusion model与其他生成模型结合,所以为了进一步展现diffusion model 的特点和改进工作,我们详细地介绍了diffusion model和其他生成模型的结合的工作并阐明了在原始生成模型上的改进之处。Diffusion model在诸多领域都有着优异的表现,并且考虑到不同领域的应用中diffusion model产生了不同的变形,我们系统地介绍了diffusion model的应用研究,其中包含如下领域:计算机视觉,NLP、波形信号处理、多模态建模、分子图建模、时间序列建模、对抗性净化。对于每个任务,我们定义了该任务并介绍利用扩散模型处理任务的工作,我们将本项工作的主要贡献总结如下:

    • 新的分类方法:我们对扩散模型和其应用提出了一种新的、系统的分类法。具体的我们将模型分为三类:采样速度增强、最大似然估计增强、数据泛化增强。进一步地,我们将扩散模型的应用分为七类:计算机视觉,NLP、波形信号处理、多模态建模、分子图建模、时间序列建模、对抗性净化。
    • 全面的回顾:我们首次全面地概述了现代扩散模型及其应用。我们展示了每种扩散模型的主要改进,和原始模型进行了必要的比较,并总结了相应的论文。对于扩散模型的每种类型的应用,我们展示了扩散模型要解决的主要问题,并说明它们如何解决这些问题。
    • 未来研究方向:我们对未来研究提出了开放型问题,并对扩散模型在算法和应用方面的未来发展提供了一些建议。

    扩散模型基础

    生成式建模的一个核心问题是模型的灵活性和可计算性之间的权衡。扩散模型的基本思想是正向扩散过程来系统地扰动数据中的分布,然后通过学习反向扩散过程恢复数据的分布,这样就了产生一个高度灵活且易于计算的生成模型。

    1.Denoising Diffusion Probabilistic Models(DDPM)

    一个DDPM由两个参数化马尔可夫链组成,并使用变分推断以在有限时间后生成与原始数据分布一致的样本。前向链的作用是扰动数据,它根据预先设计的噪声进度向数据逐渐加入高斯噪声,直到数据的分布趋于先验分布,即标准高斯分布。反向链从给定的先验开始并使用参数化的高斯转换核,学习逐步恢复原数据分布。用表示原始数据及其分布,则前向链的分布是可由下式表达:

    这说明前向链是马尔可夫过程,是加入t步噪音后的样本,是事先给定的控制噪声进度的参数。当 趋于1时,可以近似认为服从标准高斯分布。当很小时,逆向过程的转移核可以近似认为也是高斯的:

    我们可以将变分下界作为损失函数进行学习:

    2.Score-Based Generative Models(SGM)

    上述DDPM可以视作SGM的离散形式。SGM构造一个随机微分方程(SDE)来平滑的扰乱数据分布,将原始数据分布转化到已知的先验分布:

    和一个相应的逆向SDE,来将先验分布变换回原始数据分布:

    因此,要逆转扩散过程并生成数据,我们需要的唯一信息就是在每个时间点的分数函数。利用score-matching的技巧我们可以通过如下损失函数来学习分数函数:

    对两种方法的进一步介绍和两者关系的介绍请参见我们的文章。原始扩散模型的三个主要缺点,采样速度慢,最大化似然差、数据泛化能力弱。最近许多研究都在解决这些缺点,因此我们将改进的扩散模型分为三类:采样速度提升、最大似然增强和数据泛化增强。在接下来的三、四、五节我们将对这三类模型进行详细的介绍。

    采样加速方法

    在应用时,为了让新样本的质量达到最佳,扩散模型往往需要进行成千上万步计算来获取一个新样本。这限制了diffusion model的实际应用价值,因为在实际应用时,我们往往需要产生大量的新样本,来为下一步处理提供材料。研究者们在提高diffusion model采样速度上进行了大量的研究。我们对这些研究进行了详细的阐述。我们将其细化分类为三种方法:Discretization Optimization,Non-Markovian Process,Partial Sampling。

    1.Discretization Optimization

    方法优化求解diffusion SDE的方法。因为现实中求解复杂SDE只能使用离散解来逼近真正的解,所以该类方法试图优化SDE的离散化方法,在保证样本质量的同时减少离散步数。SGM 提出了一个通用的方法来求解逆向过程,即对前向和后向过程采取相同的离散方法。如果给定了前向SDE的离散方式:

    那么我们就可以以相同的方式离散化逆向SDE:

    这种方法比朴素DDPM效果略好一点。进一步,SGM向SDE求解器中加入了一个矫正器,从而让每一步生成的样本都有正确的分布。在求解的每一步,求解器给出一个样本后,矫正器都使用马尔可夫链蒙特卡罗方法来矫正刚生成的样本的分布。实验表明向求解器中加入矫正器比直接增加求解器的步数效率更高。

    2.Non-Markovian Process方法突破了原有Markovian Process的限制,其逆过程的每一步可以依赖更多以往的样本来进行预测新样本,所以在步长较大时也能做出较好的预测,从而加速采样过程。其中主要的工作DDIM,不再假设前向过程是马尔可夫过程,而是服从如下分布:

    DDIM的采样过程可以视为离散化的神经常微分方程,其采样过程更高效,并且支持样本的内插。进一步的研究发现DDIM可以视作流形上扩散模型PNDM的特例。3.Partial Sampling方法通过在generation process中忽略一部分的时间节点,而只使用剩下的时间节点来生成样本,直接减少了采样时间。例如,Progressive Distillation从训练好的扩散模型中蒸馏出效率更高的扩散模型。对于训练好的一个扩散模型,Progressive Distillation会从新训练一个扩散模型,使新的扩散模型的一步对应于训练好的扩散模型的两步,这样新模型就可以省去老模型一半的采样过程。具体算法如下:

    不断循环这个蒸馏过程就能让采样步骤指数级下降。

    最大似然估计加强

    扩散模型在最大似然估计的表现差于基于似然函数的生成模型,但最大化似然估计在诸多应用场景都有重要意义,比如图片压缩, 半监督学习, 对抗性净化。由于对数似然难以直接计算,研究主要集中在优化和分析变分下界(VLB)。我们对提高扩散模型最大似然估计的模型进行了详细的阐述。我们将其细化分类为三类方法:Objectives Designing,Noise Schedule Optimization,Learnable Reverse Variance。

    1.Objectives Designing方法利用扩散 SDE推倒出生成数据的对数似然与分数函数匹配的损失函数的关系。这样通过适当设计损失函数,就可以最大化 VLB 和对数似然。Song et al. 证明了可以设计损失函数的权重函数,使得plug-in reverse SDE 生成样本的似然函数值小于等于损失函数值,即损失函数是似然函数的上界。分数函数拟合的损失函数如下:

    我们只需将权重函数设为扩散系数g(t)即可让损失函数成为似然函数的VLB,即:

    2.Noise Schedule Optimization通过设计或学习前向过程的噪声进度来增大VLB。VDM证明了当离散步数接近无穷时,损失函数完全由信噪比函数SNR(t)的端点决定:

    那么在离散步数接近无穷时,可以通过学习信噪比函数SNR(t)的端点最优化VLB,而通过学习信噪比函数中间部分的函数值来实现模型其他方面的改进。

    3.Learnable Reverse Variance方法学习反向过程的方差,从而较少拟合误差,可以有效地最大化VLB。Analytic-DPM证明,在DDPM和DDIM中存在反向过程中的最优期望和方差:

    使用上述公式和训练好的分数函数,在给定前向过程的条件下,最优的VLB可以近似达到。

    数据泛化增强

    扩散模型假设数据存在于欧几里得空间,即具有平面几何形状的流形,并添加高斯噪声将不可避免地将数据转换为连续状态空间,所以扩散模型最初只能处理图片等连续性数据,直接应用离散数据或其他数据类型的效果较差。这限制了扩散模型的应用场景。数个研究工作将扩散模型推广到适用于其他数据类型的模型,我们对这些方法进行了详细地阐释。我们将其细化分类为两类方法:Feature Space Unification,Data-Dependent Transition Kernels。1.Feature Space Unification方法将数据转化到统一形式的latent space,然后再latent space上进行扩散。LSGM提出将数据通过VAE框架先转换到连续的latent space 上后再在其上进行扩散。这个方法的难点在于如何同时训练VAE和扩散模型。LSGM表明由于潜在先验是intractable的,分数匹配损失不再适用。LSGM直接使用VAE中传统的损失函数ELBO作为损失函数,并导出了ELBO和分数匹配的关系:

    该式在忽略常数的意义下成立。通过参数化扩散过程中样本的分数函数,LSGM可以高效的学习和优化ELBO。

    2.Data-Dependent Transition Kernels方法根据数据类型的特点设计diffusion process 中的transition kernels,使扩散模型可以直接应用于特定的数据类型。D3PM为离散型数据设计了transition kernel,可以设为lazy random-walk,absorbing state等。GEODIFF为3D分子图数据设计了平移-旋转不变的图神经网络,并且证明了具有不变性的初分布和transition kernel可以导出具有不变性的边缘分布。假设是一个平移-旋转变换,如:

    那么生成的样本分布也有平移-旋转不变性:

    和其他生成模型的联系

    在下面的每个小节中,我们首先介绍其他五类重要的生成模型,并分析它们的优势和局限性。然后我们介绍了扩散模型是如何与它们联系起来的,并说明通过结合扩散模型来改进这些生成模型。VAE,GAN,Autoregressive model, Normalizing flow, Energy-based model和扩散模型的联系如下图所示:

    1. DDPM可以视作层次马尔可夫VAE(hierarchical Markovian VAE)。但DDPM和一般的VAE也有区别。DDPM作为VAE,它的encoder和decoder都服从高斯分布、有马尔科夫行;其隐变量的维数和数据维数相同;decoder的所有层都共用一个神经网络。
    2. DDPM可以帮助GAN解决训练不稳定的问题。因为数据是在高维空间中的低维流形中,所以GAN生成数据的分布和真实数据的分布重合度低,导致训练不稳定。扩散模型提供了一个系统地增加噪音的过程,通过扩散模型向生成的数据和真实数据添加噪音,然后将加入噪音的数据送入判别器,这样可以高效地解决GAN无法训练、训练不稳定的问题。
    3. Normalizing flow通过双射函数将数据转换到先验分布,这样的作法限制了Normalizing flow的表达能力,导致应用效果较差。类比扩散模型向encoder中加入噪声,可以增加Normalizing flow的表达能力,而从另一个视角看,这样的做法是将扩散模型推广到前向过程也可学习的模型。
    4. Autoregressive model在需要保证数据有一定的结构,这导致设计和参数化自回归模型非常困难。扩散模型的训练启发了自回归模型的训练,通过特定的训练方式避免了设计的困难。
    5. Energy-based model直接对原始数据的分布建模,但直接建模导致学习和采样都比较困难。通过使用扩散恢复似然,模型可以先对样本加入微小的噪声,再从有略微噪声的样本分布来推断原始样本的分布,使的学习和采样过程更简单和稳定。

    扩散模型的应用

    在本节中,我们分别介绍了扩散模型在计算机视觉、自然语言处理、波形信号处理、多模态学习、分子图生成、时间序列以及对抗学习等七大应用方向中的应用,并对每类应用中的方法进行了细分并解析。例如在计算机视觉中可以用diffusion model进行图像补全修复(RePaint):

    在多模态任务中可以用diffusion model进行文本到图像的生成(GLIDE):

    还可以在分子图生成中用diffusion model进行药物分子和蛋白质分子的生成(GeoDiff):

    应用分类汇总见表:

    未来研究方向

    1. 应用假设再检验。我们需要检查我们在应用中普遍接受的假设。例如,实践中普遍认为扩散模型的前向过程会将数据转换为标准高斯分布,但事实并非如此,更多的前向扩散步骤会使最终的样本分布与标准高斯分布更接近,与采样过程一致;但更多的前向扩散步骤也会使估计分数函数更加困难。理论的条件很难获得,因此在实践中操作中会导致理论和实践的不匹配。我们应该意识到这种情况并设计适当的扩散模型。
    2. 从离散时间到连续时间。由于扩散模型的灵活性,许多经验方法可以通过进一步分析得到加强。通过将离散时间的模型转化到对应的连续时间模型,然后再设计更多、更好的离散方法,这样的研究思路有前景。
    3. 新的生成过程。扩散模型通过两种主要方法生成样本:一是离散化反向扩散 SDE,然后通过离散的反向 SDE 生成样本;另一个是使用逆过程中马尔可夫性质对样本逐步去噪。然而,对于一些任务,在实践中很难应用这些方法来生成样本。因此,需要进一步研究新的生成过程和视角。
    4. 泛化到更复杂的场景和更多的研究领域。虽然目前diffusion model已经应用到多个场景中,但是大多数局限于单输入单输出的场景,将来可以考虑将其应用到更复杂的场景,比如text-to-audiovisual speech synthesis。也可以考虑和更多的研究领域相结合。

    part2:【扩散模型笔记整理】从DDPM到Imagen

    扩散模型(Diffusion Model)

    1. 概述

    • 如图所示,扩散模型分两个过程:扩散(diffusion, 从x0到xT的过程逐步加入噪声)和去噪(denoise, 从xT到x0逐步去噪)。训练的时候,需要利用扩散加噪来生成训练样本;推理的时候,输入一个噪音,逐步去噪输出原始信号(比如图像、语音)。

    参考文献:Denoising Diffusion Probabilistic Models

    2. 扩散和去噪(Diffusion&Denoise)

    • 首先介绍一下高斯分布的表达,记作X∽N(μ,σ2):

    扩散过程每一步都加入一个方差为βt∈(0,1)的高斯噪声可以用马尔科夫链来表示:

    • 这里的βt是一个0到1的等比序列(β0=0),此时表示原始图像;第T步的时候,βt=1,表示标准高斯噪声N∽(0,I)。因而实际上扩散过程是一个从原始图像变为标准高斯分布的过程。加噪和高斯采样等价,无非就是改变了一下高斯采样的均值中心点。
    • 实际训练的时候,我们可以直接用下面的公式一次性算出某一步的加噪图片作为训练素材,无需逐步迭代。
    • 去噪过程和扩散过程反过来:从一张随机采样的高斯噪声图片逐步去噪得到我们想要生成的图像。表达式:
    • 去噪过程,需要用模型预测加入的高斯噪声,得到原始的无噪声的图像。上式表示,利用模型算出原始第n步的未加噪图像,实质上只要算出均值和方差,再做一个采样得到原始图像。而为了算出μ,我们需要预测出噪声ϵ,反推出原始图像的均值中心,方差项可以由网络预测也可以取常数(前者效果好)。下节将介绍模型的训练和推理过程。

    3. 训练和采样(Training&Sampling)

    • 训练其实就是扩散过程,而采样其实就是去噪过程。
    • 算法如上图所示,训练training的过程实际上是随机采第t步的加噪图像,输入带噪图片以及步数t,模型预测噪声ϵ,模型训练目标:预测噪声与实际加入噪声的误差越小越好。
    • 采样sampling的过程(生成过程)为:将有噪声的图像(第一张图像为随机采样的高斯分布噪声)减去模型预测的噪声(噪声前面的其它参数可以由上面加噪的过程反向推导出来)不断把噪声去掉以恢复出原始的图像。
    • 方差项σ也可以由模型来预测。

    参考文献: Improved Denoising Diffusion Probabilistic Models

    引导扩散模型(Guided Diffusion)

    前文已经讲述扩散模型的原理,然而我们随机输入一张高斯噪声显然不能按照人的意愿生成我们想要的内容,因而需要额外的引导guidance以得到我们需要的图像。一种想法是使用外部模型(分类器or广义的判别器)的输出作为引导条件来指导扩散模型的去噪过程,从而得到我们想要的输出;还有一种则比较直观一些:我们直接把我们想要的引导条件condition也作为模型输入的一部分,从而让扩散模型见到这个条件后就可以直接生成我们想要的内容。

    下文将讲解classifier guidance和semantic guidance diffusion model(后者包括前者,前者是比较简单的一个应用),除此之外,由于额外的判别器会拖慢推理速度,因此后来有人提出了 classifier-free guidance diffusion model来替代前面的那种方案,也即把条件作为模型的输入,直接生成我们需要的图像。

    1. Classifier Guidance Diffusion Model

    • 这种方法不用额外训练扩散模型,直接在原有训练好的扩散模型上,通过外部的分类器来引导生成期望的图像。唯一需要改动的地方其实只有sampling过程中的高斯采样的均值,也即采样过程中,期望噪声图像的采样中心越靠近判别器引导的条件越好。
    • 上图总结了采样算法。Algorithm 1和 Algorithm 2其实是等价的(1是直接预测均值和方差,2是预测噪声的误差)。直接看Algorithm 1可知,实质上改变的只有高斯分布的均值中心,将扩散方向“引导”成我们想要的内容。具体而言,用分类模型pϕ对生成的图片进行分类,得到预测分数与目标类别的交叉熵,将其对带噪图像求梯度用梯度引导下一步的生成采样。(实际使用的时候,需要把这个分类器也在带噪数据额外训练一下)
    • 因为我们实际使用的模型预测的是噪音,实际计算为Algorithm 2,可以由1推导而来。(具体推导过程可以参考文献)

    参考文献:Diffusion Models Beat GANs on Image Synthesis

    2. Semantic Guidance Diffusion

    • 介绍完前面的 classifier guidance后,显然我们可以把分类器替换成其它任意的判别器,也即更换引导条件,从而实现利用不同的语义信息来指导扩散模型的去噪过程。比如说,我们可以实现text-guidance和image-guidance等。
    • 实质上就是把classifier guidance的条件推广,表达为:
    • Fϕ表示就是新的引导条件,这里展示的是分类的,其实也可以换成相似度之类的分数指标。具体可以有以下的例子:图像引导、文本引导、图像+文本引导。


    参考文献:More Control for Free! Image Synthesis with Semantic Diffusion Guidance

    3. Classifier-Free Guidance Diffusion

    • 正如前文提到的,额外引入一个网络来指导,推理的时候比较复杂(扩散模型需要反复迭代,每次迭代都需要额外算一个分数)。然而,直接将引导条件作为模型的输入,直到Classifier-Free Diffusion Guidance被提出前似乎效果也一般般。Classifier-Free Diffusion Guidance这篇文章的贡献就是提出了一个等价的结构替换掉了外部的判别器,从而可以直接用一个扩散模型来做条件生成任务。
    • 实际做法只是改变了模型输入的内容,有conditional(除了随机高斯噪声输入外,把引导信息的embedding也加进来)和unconditional 的 sample输入。两种输入都会被送到同一个diffusion model从而让其能够具有无条件和有条件生成的能力。得到这两种输入的输出后,就可以用来引导扩散模型进行训练。
    • 回忆一下前面的 classifier guidance的噪音更新方式:
    • 实质上,这个classifier-free用另一个近似的等价结构替换掉了后面那一项:
    • 其中,ϵθ(xt,y) 表示conditional的输入,而ϵθ(xt)则表示unconditional输入,用这两项之差乘以一个系数来替换掉原来的那项。至于为什么可以这么直接替换,其实可以用贝叶斯公式推导而来:
    • 因而,实际上这个过程就训练了一个 implicit classifier,从而移除外部的分类器。

    参考文献:Classifier-Free Diffusion Guidance

    GLIDE

    • 这篇文章主要就是用到了前面所说的classifier-free扩散模型,只不过把输入的condition换成了文本信息,从而实现文本生成图像,此外还利用diffusion model实现了超分辨率。一些效果展示如下,可以看到,其实已经可以生成一些比较逼真的图片了。
    • 具体可以表达为:
    • 这里无非就是把原来的label y换成了 caption,实际上就是运用了足够量的image-text pair从而可以把caption当作是某种程度上的label。(随机替换为空序列以实现unconditional的训练方式)
    动图封面
    • 由于此时的生成图像质量一般般,文章也提供了图像编辑的方式(具体操作为:将选中区域mask掉,将图像也作为一个condition连同文本输入到模型中去):

    DALL·E 2

    概况

    • 第一版DALL·E用的是GAN+CLIP重排序的结构。
    • DALL·E 2可以把diffusion model和CLIP结合在一起,生成效果十分惊艳,可以直接去官网浏览一下。DALL·E 2 (openai.com)
    • 包括prior网络用于将caption转换为CLIP image embedding,一个decoder把image embedding作为condition来生成图像。prior有两种:一种是autoregressive model、一种是diffusion model(后者效果更好一些);decoder就是diffusion model。总之,这里相比前面的变化主要在于加入了prior,以及把condition换成了CLIP的embedding。

    Decoder

    • 具体而言,把CLIP image embedding作为condition输入到diffusion model中,同时把CLIP image embedding映射成4个额外的tokens接到GLIDE text encoder的输出。
    • 除了用于生成图像的diffusion model,这部分还有2个额外用于超分辨率的diffusion model,生成高清图像。

    Prior

    这部分的内容是为了将caption y转换为 CLIP image embedding,以用于后面decoder的图像生成。

    • 一种是auto-regressive model,将image embedding转换为一串离散的编码,并且基于condition caption y自回归地预测。(这里不一定要condition on caption(GLIDE的方法——额外用一个Transformer处理caption),也可以condition on CLIP text embedding)。此外,这里还用到了PCA来降维,降低运算复杂度。
    • 一种是diffusion model。这是一个decoder-only Transformer,输入是encoded text+CLIP text embedding+noised CLIP image embedding+额外token(类似class embedding)输入,其输出一个unnoised CLIP image embedding(取那个额外的embedding)。

    Variations

    • 这部分是为了给一张图,生成相似的图像。做法很简单:用CLIP把图像编码,把这个CLIP image embedding作为condition引导decoder生成图像。除此之外,还可以对2张图像的CLIP embedding进行插值,以实现风格迁移。( spherical interpolation 几何球面线性插值)。这里证明了CLIP语义空间的可解释性

    量化结果

    • 本文方法又称unCLIP(其实本质上就是把CLIP生成的embedding进行decode),相比GLIDE有小幅的提高。

    Paper List

    1. (DDPM) Denoising Diffusion Probabilistic Models. NIPS 20. (Diffusion and deep-learning-based 图像生成开山之作)
    2. More Control for Free! Image Synthesis with Semantic Diffusion Guidance. arXiv 21. (对DDIM进行了推广,引入了一般形式的判别器引导)
    3. Denoising Diffusion Implicit Models. ICLR 21. (提出了一种新的sampling的方法,可以通过改变eta来skip一些step,进而达到加速sampling的目的)
    4. Improved denoising diffusion probabilistic models. ICML 21.
    5. Classifier-Free Diffusion Guidance. NIPSW 21. (引入了等价结构替代了分类器引导)
    6. GLIDE: Towards Photorealistic Image Generation and Editing with Text-Guided Diffusion Models. ICML 22.
    7. Hierarchical Text-Conditional Image Generation with CLIP Latents. NIPS 22 在投. (DALL-E 2)
    8. Photorealistic Text-to-Image Diffusion Models with Deep Language Understanding. NIPS 22 在投. (Imagen, SOTA)
    9. High-Resolution Image Synthesis with Latent Diffusion Models. CVPR 22. (隐空间LDM)

    扩散模型DDPM

    摘自:https://zhuanlan.zhihu.com/p/563661713

    “What I cannot create, I do not understand.” — Richard Feynman

    https://github.com/xiaohu2015/nngen/tree/main/models/diffusion_models

    论文:https://arxiv.org/abs/2006.11239

    近段时间最火的方向无疑是基于文本用AI生成图像,继OpenAI在2021提出的文本转图像模型DALLE之后,越来越多的大公司卷入这个方向,如谷歌在今年相继推出了ImagenParti。一些主流的文本转图像模型如DALL·E 2,stable-diffusion和Imagen采用了扩散模型Diffusion Model)作为图像生成模型,这也引发了对扩散模型的研究热潮。相比GAN来说,扩散模型训练更稳定,而且能够生成更多样的样本,OpenAI的论文Diffusion Models Beat GANs on Image Synthesis也证明了扩散模型能够超越GAN。简单来说,扩散模型包含两个过程:前向扩散过程反向生成过程,前向扩散过程是对一张图像逐渐添加高斯噪音直至变成随机噪音,而反向生成过程是去噪音过程,我们将从一个随机噪音开始逐渐去噪音直至生成一张图像,这也是我们要求解或者训练的部分。扩散模型与其它主流生成模型的对比如下所示:

    目前所采用的扩散模型大都是来自于2020年的工作DDPM: Denoising Diffusion Probabilistic Models,DDPM对之前的扩散模型(具体见Deep Unsupervised Learning using Nonequilibrium Thermodynamics)进行了简化,并通过变分推断(variational inference)来进行建模,这主要是因为扩散模型也是一个隐变量模型(latent variable model),相比VAE这样的隐变量模型,扩散模型的隐变量是和原始数据是同维度的,而且推理过程(即扩散过程)往往是固定的。这篇文章将基于DDPM详细介绍扩散模型的原理,并给出具体的代码实现和分析。

    扩散模型原理

    扩散模型包括两个过程:前向过程(forward process)反向过程(reverse process),其中前向过程又称为为扩散过程(diffusion process),如下图所示。无论是前向过程还是反向过程都是一个参数化的马尔可夫链(Markov chain),其中反向过程可以用来生成数据,这里我们将通过变分推断来进行建模和求解。

    扩散过程

    扩散过程是指的对数据逐渐增加高斯噪音直至数据变成随机噪音的过程。对于原始数据

    ,总共包含T步的扩散过程的每一步都是对上一步得到的数据xt-1按如下方式增加高斯噪音:

    这里{βt}t=1~T为每一步所采用的方差,它介于0~1之间。对于扩散模型,我们往往称不同step的方差设定为variance schedule或者noise schedule,通常情况下,越后面的step会采用更大的方差,即满足β1<β2<⋯<βT。在一个设计好的variance schedule下,的如果扩散步数T足够大,那么最终得到的xT就完全丢失了原始数据而变成了一个随机噪音。 扩散过程的每一步都生成一个带噪音的数据xt,整个扩散过程也就是一个马尔卡夫链

    另外要指出的是, 扩散过程往往是固定的, 即采用一个预先定义好的variance schedule, 比 如DDPM就采用一个线性的variance schedule。扩散过程的一个重要特性是我们可以直接基 于原始数据 \(\mathbf{x}0\) 来对任意 \(t\)步的 \(\mathbf{x}_t\) 进行采样: \(\mathbf{x}_t \sim q\left(\mathbf{x}_t \mid \mathbf{x}_0\right)\) 。这里定义 \(\alpha_t=1-\beta_t\) 和 \(\bar{\alpha}_t=\prod{i=1}^t \alpha_i\) , 通过重参数技巧(和VAE类似), 那么有:

    上述推到过程利用了两个方差不同的高斯分布\(\mathcal{N}\left(\mathbf{0}, \sigma_1^2 \mathbf{I}\right)\) 和 \(\mathcal{N}\left(\mathbf{0}, \sigma_2^2 \mathbf{I}\right)\) 相加等于一个新的高斯分 布 \(\mathcal{N}\left(\mathbf{0},\left(\sigma_1^2+\sigma_2^2\right) \mathbf{I}\right)\) 。反重参数化后, 我们得到:
    \[
    q\left(\mathbf{x}_t \mid \mathbf{x}_0\right)=\mathcal{N}\left(\mathbf{x}_t ; \sqrt{\bar{\alpha}_t} \mathbf{x}_0,\left(1-\bar{\alpha}_t\right) \mathbf{I}\right)
    \]
    扩散过程的这个特性很重要。首先, 我们可以看到 \(\mathbf{x}_t\) 其实可以看成是原始数据 \(\mathbf{x}_0\) 和随机噪音 \(\epsilon\) 的线性组合, 其中\(\sqrt{\bar{\alpha}_t}\) 和 \(\sqrt{1-\bar{\alpha}_t}\) 为组合系数, 它们的平方和等于 1 , 我们也可以称两者分别 为 signal_rate 和 noise_rate (见https://keras.io/examples/generative/ddim/#diffusionschedule和Variational Diffusion Models)。更近一步地,我们可以基于 \(\bar{\alpha}_t\) 而不是 \(\beta_t\) 来定义 noise schedule (见Improved Denoising Diffusion Probabilistic Models所设计的cosine schedule), 因为这样处理更直接, 比如我们直接将 \(\bar{\alpha}_T\) 设定为一个接近0的值, 那么就可以保 证最终得到的 \(\mathbf{x}_T\) 近似为一个随机噪音。其次, 后面的建模和分析过程将使用这个特性。

    反向过程

    扩散过程是将数据噪音化,那么反向过程就是一个去噪的过程,如果我们知道反向过程的每一步的真实分布q(xt−1|xt),那么从一个随机噪音xT∼N(0,I)开始,逐渐去噪就能生成一个真实的样本,所以反向过程也就是生成数据的过程

    估计分布 \(q\left(\mathbf{x}{t-1} \mid \mathbf{x}_t\right)\) 需要用到整个训练样本, 我们可以用神经网络来估计这些分布。这里, 我们将反向过程也定义为一个马尔卡夫链, 只不过它是由一系列用神经网络参数化的高斯分布来组成:

    \[p\theta\left(\mathbf{x}{0: T}\right)=p\left(\mathbf{x}_T\right) \prod{t=1}^T p_\theta\left(\mathbf{x}{t-1} \mid \mathbf{x}_t\right) \quad p\theta\left(\mathbf{x}{t-1} \mid \mathbf{x}_t\right)=\mathcal{N}\left(\mathbf{x}{t-1} ; \boldsymbol{\mu}\theta\left(\mathbf{x}_t, t\right), \mathbf{\Sigma}\theta\left(\mathbf{x}t, t\right)\right)\]

    这里 \(p\left(\mathbf{x}_T\right)=\mathcal{N}\left(\mathbf{x}_T ; \mathbf{0}, \mathbf{I}\right)\), 而 \(p\theta\left(\mathbf{x}{t-1} \mid \mathbf{x}_t\right)\) 为参数化的高斯分布, 它们的均值和方差由训练的网络 \(\boldsymbol{\mu}\theta\left(\mathbf{x}t, t\right)\) 和 \(\boldsymbol{\Sigma}\theta\left(\mathbf{x}t, t\right)\) 给出。实际上, 扩散模型就是要得到这些训练好的网络, 因为它们构 成了最终的生成模型。虽然分布 \(q\left(\mathbf{x}{t-1} \mid \mathbf{x}t\right)\) 是不可直接处理的, 但是加上条件\(\mathbf{x}_0\) 的后验分布 \(q\left(\mathbf{x}{t-1} \mid \mathbf{x}t, \mathbf{x}_0\right)\) 却是可处理的, 这里有:

    \[q\left(\mathbf{x}{t-1} \mid \mathbf{x}t, \mathbf{x}_0\right)=\mathcal{N}\left(\mathbf{x}{t-1} ; \tilde{\boldsymbol{\mu}}\left(\mathbf{x}_t, \mathbf{x}_0\right), \tilde{\beta}_t \mathbf{I}\right)
    \]

    下面我们来具体推导这个分布,首先根据贝叶斯公式,我们有:

    由于扩散过程的马尔卡夫链特性,我们知道分布

    ,而由前面得到的扩散过程特性可知:

    所以,我们有:

    这里的 \(C\left(\mathbf{x}t, \mathbf{x}_0\right)\) 是一个和 \(\mathbf{x}{t-1}\) 无关的部分,所以省略。根据高斯分布的概率密度函数定义和上 述结果 (配平方),我们可以得到后验分布 \(q\left(\mathbf{x}t \mid \mathbf{x}{t-1}, \mathbf{x}0\right)\) 的均值和方差:


    可以看到方差是一个定量 (扩散过程参数固定),而均值是一个依赖 \(\mathbf{x}_0\) 和 \(\mathbf{x}_t\) 的函数。这个分布将 会被用于推导扩散模型的优化目标。

    优化目标

    上面介绍了扩散模型的扩散过程和反向过程,现在我们来从另外一个角度来看扩散模型:如果我们把中间产生的变量看成隐变量的话,那么扩散模型其实是包含T个隐变量的隐变量模型(latent variable model),它可以看成是一个特殊的Hierarchical VAEs(见Understanding Diffusion Models: A Unified Perspective):

    相比VAE来说,扩散模型的隐变量是和原始数据同维度的,而且encoder(即扩散过程)是固定的。既然扩散模型是隐变量模型,那么我们可以就可以基于变分推断来得到variational lower boundVLB,又称ELBO)作为最大化优化目标,这里有:

    这里最后一步是利用了Jensen’s inequality(不采用这个不等式的推导见博客What are Diffusion Models?),对于网络训练来说,其训练目标为VLB取负

    我们近一步对训练目标进行分解可得:

    可以看到最终的优化目标共包含 \(T+1\) 项,其中 \(L_0\) 可以看成是原始数据重建,优化的是负对数似然, \(L_0\) 可以用估计的 \(\mathcal{N}\left(\mathbf{x}0 ; \boldsymbol{\mu}\theta\left(\mathbf{x}1, 1\right), \mathbf{\Sigma}\theta\left(\mathbf{x}1, 1\right)\right)\) 来构建一个离散化的decoder来计算(见 DDPM论文3.3部分);而 \(L_T\) 计算的是最后得到的噪音的分布和先验分布的KL散度,这个KL散度没有训练参数,近似为 0 ,因为先验 \(p\left(\mathbf{x}_T\right)=\mathcal{N}(\mathbf{0}, \mathbf{I})\) 而扩散过程最后得到的随机噪音 \(q\left(\mathbf{x}_T \mid \mathbf{x}_0\right)\) 也近似为 \(\mathcal{N}(\mathbf{0}, \mathbf{I})\) ;而 \(L{t-1}\) 则是计算的是估计分布 \(p_\theta\left(\mathbf{x}{t-1} \mid \mathbf{x}_t\right)\) 和真实后验分布 \(q\left(\mathbf{x}{t-1} \mid \mathbf{x}_t, \mathbf{x}_0\right)\) 的KL散度,这里希望䇝们估计的去噪过程和依赖真实数据的去噪过程近似一致:

    之所以前面我们将 \(p_\theta\left(\mathbf{x}{t-1} \mid \mathbf{x}_t\right)\) 定义为一个用网络参数化的高斯分布 \(\mathcal{N}\left(\mathbf{x}{t-1} ; \boldsymbol{\mu}\theta\left(\mathbf{x}_t, t\right), \mathbf{\Sigma}\theta\left(\mathbf{x}t, t\right)\right)\), 是因为要匹配的后验分布 \(q\left(\mathbf{x}{t-1} \mid \mathbf{x}t, \mathbf{x}_0\right)\)也是一个高斯分布。对 于训练目标 \(L_0\) 和 \(L{t-1}\) 来说, 都是希望得到训练好的网络 \(\boldsymbol{\mu}\theta\left(\mathbf{x}_t, t\right)\) 和 \(\boldsymbol{\Sigma}\theta\left(\mathbf{x}t, t\right)\) (对于 \(L_0, t=1\) )。DDPM对 \(p\theta\left(\mathbf{x}{t-1} \mid \mathbf{x}_t\right)\) 做了近一步简化, 采用周定的方差: \(\boldsymbol{\Sigma}\theta\left(\mathbf{x}t, t\right)=\sigma_t^2 \mathbf{I}\), 这里的 \(\sigma_t^2\) 可以 设定为 \(\beta_t\) 或者 \(\tilde{\beta}_t\) (这其实是两个极端, 分别是上限和下限, 也可以采用可训练的方差, 见论文 Improved Denoising Diffusion Probabilistic Models 和Analytic-DPM: an Analytic Estimate of the Optimal Reverse Variance in Diffusion Probabilistic Models)。这里假定 \(\sigma_t^2=\tilde{\beta}_t\), 那么:

    \[q\left(\mathbf{x}{t-1} \mid \mathbf{x}t, \mathbf{x}_0\right)=\mathcal{N}\left(\mathbf{x}{t-1} ; \tilde{\boldsymbol{\mu}}\left(\mathbf{x}t, \mathbf{x}_0\right), \sigma_t^2 \mathbf{I}\right) p\theta\left(\mathbf{x}{t-1} \mid \mathbf{x}_t\right)=\mathcal{N}\left(\mathbf{x}{t-1} ; \boldsymbol{\mu}_\theta\left(\mathbf{x}_t, t\right), \sigma_t^2 \mathbf{I}\right)
    \]
    对于两个高斯分布的KL散度, 其计算公式为(具体推导见生成模型之VAE):

    那么就有:

    那么优化目标即\(L{t-1}\)为:

    从上述公式来看, 我们是希望网络学习到的均值 \(\boldsymbol{\mu}\theta\left(\mathbf{x}_t, t\right)\) 和后验分布的均值 \(\tilde{\boldsymbol{\mu}}\left(\mathbf{x}_t, \mathbf{x}_0\right)\) 一致。不 过DDPM发现预测均值并不是最好的选择。根据前面得到的扩散过程的特性, 我们有:

    \(\mathbf{x}{\mathbf{t}}\left(\mathbf{x}_{\mathbf{0}}, \epsilon\right)=\sqrt{\bar{\alpha}_t} \mathbf{x}_0+\sqrt{1-\bar{\alpha}_t \epsilon} \quad \text { where } \epsilon \sim \mathcal{N}(\mathbf{0}, \mathbf{I})
    \)

    将这个公式带入上述优化目标,可以得到:

    近一步地, 我们对 \(\boldsymbol{\mu}\theta\left(\mathbf{x}{\mathbf{t}}\left(\mathbf{x}0, \epsilon\right), t\right)\) 也进行重参数化, 变成:

    \[\boldsymbol{\mu}\theta\left(\mathbf{x}{\mathbf{t}}\left(\mathbf{x}_0, \epsilon\right), t\right)=\frac{1}{\sqrt{\alpha_t}}\left(\mathbf{x}_t\left(\mathbf{x}_0, \epsilon\right)-\frac{\beta_t}{\sqrt{1-\bar{\alpha}_t}} \epsilon\theta\left(\mathbf{x}t\left(\mathbf{x}_0, \epsilon\right), t\right)\right)\]

    这里的 \(\epsilon\theta\) 是一个基于神经网络的拟合函数, 这意味着我们由原来的预测均值而换成预测噪音 \(\epsilon\) 。我们将上述等式带入优化目标, 可以得到:

    DDPM近一步对上述目标进行了简化, 即去掉了权重系数, 变成了: \(L{t-1}^{\text {simple }}=\mathbb{E}{\mathbf{x}_0, \epsilon \sim \mathcal{N}(0, \mathrm{I})}\left[\left|\epsilon-\epsilon\theta\left(\sqrt{\bar{\alpha}_t} \mathbf{x}_0+\sqrt{1-\bar{\alpha}_t} \epsilon, t\right)\right|^2\right]\) 这里的 \(t\) 在 \([1, \mathrm{~T}]\) 范围内取值(如前所述, 其中取 1 时对应 \(L_0\) )。由于去掉了不同 \(t\)的权重系数, 所以这个简化的目标其实是VLB优化 目标进行了 reweight。从DDPM的对比实验结果来看, 预测噪音比预测均值效果要好, 采用简 化版本的优化目标比VLB目标效果要好:

    虽然扩散模型背后的推导比较复杂,但是我们最终得到的优化目标非常简单,就是让网络预测的噪音和真实的噪音一致。DDPM的训练过程也非常简单,如下图所示:随机选择一个训练样本->从1-T随机抽样一个t->随机产生噪音-计算当前所产生的带噪音数据(红色框所示)->输入网络预测噪音->计算产生的噪音和预测的噪音的L2损失->计算梯度并更新网络。

    一旦训练完成,其采样过程也非常简单,如上所示:我们从一个随机噪音开始,并用训练好的网络预测噪音,然后计算条件分布的均值(红色框部分),然后用均值加标准差乘以一个随机噪音,直至t=0完成新样本的生成(最后一步不加噪音)。不过实际的代码实现和上述过程略有区别(见https://github.com/hojonathanho/diffusion/issues/5:先基于预测的噪音生成,并进行了clip处理(范围[-1, 1],原始数据归一化到这个范围),然后再计算均值。我个人的理解这应该算是一种约束,既然模型预测的是噪音,那么我们也希望用预测噪音重构处理的原始数据也应该满足范围要求。

    模型设计

    前面我们介绍了扩散模型的原理以及优化目标,那么扩散模型的核心就在于训练噪音预测模型,由于噪音和原始数据是同维度的,所以我们可以选择采用AutoEncoder架构来作为噪音预测模型。DDPM所采用的模型是一个基于residual block和attention block的U-Net模型。如下所示:

    U-Net属于encoder-decoder架构,其中encoder分成不同的stages,每个stage都包含下采样模块来降低特征的空间大小(H和W),然后decoder和encoder相反,是将encoder压缩的特征逐渐恢复。U-Net在decoder模块中还引入了skip connection,即concat了encoder中间得到的同维度特征,这有利于网络优化。DDPM所采用的U-Net每个stage包含2个residual block,而且部分stage还加入了self-attention模块增加网络的全局建模能力。 另外,扩散模型其实需要的是个噪音预测模型,实际处理时,我们可以增加一个time embedding(类似transformer中的position embedding)来将timestep编码到网络中,从而只需要训练一个共享的U-Net模型。具体地,DDPM在各个residual block都引入了time embedding,如上图所示。

    代码实现

    最后,我们基于PyTorch框架给出DDPM的具体实现,这里主要参考了三套代码实现:

    首先,是time embeding,这里是采用Attention Is All You Need中所设计的sinusoidal position embedding,只不过是用来编码timestep:

    # use sinusoidal position embedding to encode time step (https://arxiv.org/abs/1706.03762)   
    def timestep_embedding(timesteps, dim, max_period=10000):
        """
        Create sinusoidal timestep embeddings.
        :param timesteps: a 1-D Tensor of N indices, one per batch element.
                          These may be fractional.
        :param dim: the dimension of the output.
        :param max_period: controls the minimum frequency of the embeddings.
        :return: an [N x dim] Tensor of positional embeddings.
        """
        half = dim // 2
        freqs = torch.exp(
            -math.log(max_period) * torch.arange(start=0, end=half, dtype=torch.float32) / half
        ).to(device=timesteps.device)
        args = timesteps[:, None].float() * freqs[None]
        embedding = torch.cat([torch.cos(args), torch.sin(args)], dim=-1)
        if dim % 2:
            embedding = torch.cat([embedding, torch.zeros_like(embedding[:, :1])], dim=-1)
        return embedding

    由于只有residual block才引入time embedding,所以可以定义一些辅助模块来自动处理,如下所示:

    # define TimestepEmbedSequential to support `time_emb` as extra input
    class TimestepBlock(nn.Module):
        """
        Any module where forward() takes timestep embeddings as a second argument.
        """
    
        @abstractmethod
        def forward(self, x, emb):
            """
            Apply the module to `x` given `emb` timestep embeddings.
            """
    
    
    class TimestepEmbedSequential(nn.Sequential, TimestepBlock):
        """
        A sequential module that passes timestep embeddings to the children that
        support it as an extra input.
        """
    
        def forward(self, x, emb):
            for layer in self:
                if isinstance(layer, TimestepBlock):
                    x = layer(x, emb)
                else:
                    x = layer(x)
            return x

    这里所采用的U-Net采用GroupNorm进行归一化,所以这里也简单定义了一个norm layer以方便使用:

    # use GN for norm layer
    def norm_layer(channels):
        return nn.GroupNorm(32, channels)

    U-Net的核心模块是residual block,它包含两个卷积层以及shortcut,同时也要引入time embedding,这里额外定义了一个linear层来将time embedding变换为和特征维度一致,第一conv之后通过加上time embedding来编码time:

    # Residual block
    class ResidualBlock(TimestepBlock):
        def __init__(self, in_channels, out_channels, time_channels, dropout):
            super().__init__()
            self.conv1 = nn.Sequential(
                norm_layer(in_channels),
                nn.SiLU(),
                nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1)
            )
            
            # pojection for time step embedding
            self.time_emb = nn.Sequential(
                nn.SiLU(),
                nn.Linear(time_channels, out_channels)
            )
            
            self.conv2 = nn.Sequential(
                norm_layer(out_channels),
                nn.SiLU(),
                nn.Dropout(p=dropout),
                nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1)
            )
    
            if in_channels != out_channels:
                self.shortcut = nn.Conv2d(in_channels, out_channels, kernel_size=1)
            else:
                self.shortcut = nn.Identity()
    
    
        def forward(self, x, t):
            """
            `x` has shape `[batch_size, in_dim, height, width]`
            `t` has shape `[batch_size, time_dim]`
            """
            h = self.conv1(x)
            # Add time step embeddings
            h += self.time_emb(t)[:, :, None, None]
            h = self.conv2(h)
            return h + self.shortcut(x)

    这里还在部分residual block引入了attention,这里的attention和transformer的self-attention是一致的:

    # Attention block with shortcut
    class AttentionBlock(nn.Module):
        def __init__(self, channels, num_heads=1):
            super().__init__()
            self.num_heads = num_heads
            assert channels % num_heads == 0
            
            self.norm = norm_layer(channels)
            self.qkv = nn.Conv2d(channels, channels * 3, kernel_size=1, bias=False)
            self.proj = nn.Conv2d(channels, channels, kernel_size=1)
    
        def forward(self, x):
            B, C, H, W = x.shape
            qkv = self.qkv(self.norm(x))
            q, k, v = qkv.reshape(B*self.num_heads, -1, H*W).chunk(3, dim=1)
            scale = 1. / math.sqrt(math.sqrt(C // self.num_heads))
            attn = torch.einsum("bct,bcs->bts", q * scale, k * scale)
            attn = attn.softmax(dim=-1)
            h = torch.einsum("bts,bcs->bct", attn, v)
            h = h.reshape(B, -1, H, W)
            h = self.proj(h)
            return h + x

    对于上采样模块和下采样模块,其分别可以采用插值和stride=2的conv或者pooling来实现:

    # upsample
    class Upsample(nn.Module):
        def __init__(self, channels, use_conv):
            super().__init__()
            self.use_conv = use_conv
            if use_conv:
                self.conv = nn.Conv2d(channels, channels, kernel_size=3, padding=1)
    
        def forward(self, x):
            x = F.interpolate(x, scale_factor=2, mode="nearest")
            if self.use_conv:
                x = self.conv(x)
            return x
    
    # downsample
    class Downsample(nn.Module):
        def __init__(self, channels, use_conv):
            super().__init__()
            self.use_conv = use_conv
            if use_conv:
                self.op = nn.Conv2d(channels, channels, kernel_size=3, stride=2, padding=1)
            else:
                self.op = nn.AvgPool2d(stride=2)
    
        def forward(self, x):
            return self.op(x)

    上面我们实现了U-Net的所有组件,就可以进行组合来实现U-Net了:

     The full UNet model with attention and timestep embedding
    class UNetModel(nn.Module):
        def __init__(
            self,
            in_channels=3,
            model_channels=128,
            out_channels=3,
            num_res_blocks=2,
            attention_resolutions=(8, 16),
            dropout=0,
            channel_mult=(1, 2, 2, 2),
            conv_resample=True,
            num_heads=4
        ):
            super().__init__()
    
            self.in_channels = in_channels
            self.model_channels = model_channels
            self.out_channels = out_channels
            self.num_res_blocks = num_res_blocks
            self.attention_resolutions = attention_resolutions
            self.dropout = dropout
            self.channel_mult = channel_mult
            self.conv_resample = conv_resample
            self.num_heads = num_heads
            
            # time embedding
            time_embed_dim = model_channels * 4
            self.time_embed = nn.Sequential(
                nn.Linear(model_channels, time_embed_dim),
                nn.SiLU(),
                nn.Linear(time_embed_dim, time_embed_dim),
            )
            
            # down blocks
            self.down_blocks = nn.ModuleList([
                TimestepEmbedSequential(nn.Conv2d(in_channels, model_channels, kernel_size=3, padding=1))
            ])
            down_block_chans = [model_channels]
            ch = model_channels
            ds = 1
            for level, mult in enumerate(channel_mult):
                for _ in range(num_res_blocks):
                    layers = [
                        ResidualBlock(ch, mult * model_channels, time_embed_dim, dropout)
                    ]
                    ch = mult * model_channels
                    if ds in attention_resolutions:
                        layers.append(AttentionBlock(ch, num_heads=num_heads))
                    self.down_blocks.append(TimestepEmbedSequential(*layers))
                    down_block_chans.append(ch)
                if level != len(channel_mult) - 1: # don't use downsample for the last stage
                    self.down_blocks.append(TimestepEmbedSequential(Downsample(ch, conv_resample)))
                    down_block_chans.append(ch)
                    ds *= 2
            
            # middle block
            self.middle_block = TimestepEmbedSequential(
                ResidualBlock(ch, ch, time_embed_dim, dropout),
                AttentionBlock(ch, num_heads=num_heads),
                ResidualBlock(ch, ch, time_embed_dim, dropout)
            )
            
            # up blocks
            self.up_blocks = nn.ModuleList([])
            for level, mult in list(enumerate(channel_mult))[::-1]:
                for i in range(num_res_blocks + 1):
                    layers = [
                        ResidualBlock(
                            ch + down_block_chans.pop(),
                            model_channels * mult,
                            time_embed_dim,
                            dropout
                        )
                    ]
                    ch = model_channels * mult
                    if ds in attention_resolutions:
                        layers.append(AttentionBlock(ch, num_heads=num_heads))
                    if level and i == num_res_blocks:
                        layers.append(Upsample(ch, conv_resample))
                        ds //= 2
                    self.up_blocks.append(TimestepEmbedSequential(*layers))
    
            self.out = nn.Sequential(
                norm_layer(ch),
                nn.SiLU(),
                nn.Conv2d(model_channels, out_channels, kernel_size=3, padding=1),
            )
    
        def forward(self, x, timesteps):
            """
            Apply the model to an input batch.
            :param x: an [N x C x H x W] Tensor of inputs.
            :param timesteps: a 1-D batch of timesteps.
            :return: an [N x C x ...] Tensor of outputs.
            """
            hs = []
            # time step embedding
            emb = self.time_embed(timestep_embedding(timesteps, self.model_channels))
            
            # down stage
            h = x
            for module in self.down_blocks:
                h = module(h, emb)
                hs.append(h)
            # middle stage
            h = self.middle_block(h, emb)
            # up stage
            for module in self.up_blocks:
                cat_in = torch.cat([h, hs.pop()], dim=1)
                h = module(cat_in, emb)
            return self.out(h)

    对于扩散过程,其主要的参数就是timesteps和noise schedule,DDPM采用范围为[0.0001, 0.02]的线性noise schedule,其默认采用的总扩散步数为1000

    # beta schedule
    def linear_beta_schedule(timesteps):
        scale = 1000 / timesteps
        beta_start = scale * 0.0001
        beta_end = scale * 0.02
        return torch.linspace(beta_start, beta_end, timesteps, dtype=torch.float64)

    我们定义个扩散模型,它主要要提前根据设计的noise schedule来计算一些系数,并实现一些扩散过程和生成过程:

    class GaussianDiffusion:
        def __init__(
            self,
            timesteps=1000,
            beta_schedule='linear'
        ):
            self.timesteps = timesteps
            
            if beta_schedule == 'linear':
                betas = linear_beta_schedule(timesteps)
            elif beta_schedule == 'cosine':
                betas = cosine_beta_schedule(timesteps)
            else:
                raise ValueError(f'unknown beta schedule {beta_schedule}')
            self.betas = betas
                
            self.alphas = 1. - self.betas
            self.alphas_cumprod = torch.cumprod(self.alphas, axis=0)
            self.alphas_cumprod_prev = F.pad(self.alphas_cumprod[:-1], (1, 0), value=1.)
            
            # calculations for diffusion q(x_t | x_{t-1}) and others
            self.sqrt_alphas_cumprod = torch.sqrt(self.alphas_cumprod)
            self.sqrt_one_minus_alphas_cumprod = torch.sqrt(1.0 - self.alphas_cumprod)
            self.log_one_minus_alphas_cumprod = torch.log(1.0 - self.alphas_cumprod)
            self.sqrt_recip_alphas_cumprod = torch.sqrt(1.0 / self.alphas_cumprod)
            self.sqrt_recipm1_alphas_cumprod = torch.sqrt(1.0 / self.alphas_cumprod - 1)
            
            # calculations for posterior q(x_{t-1} | x_t, x_0)
            self.posterior_variance = (
                self.betas * (1.0 - self.alphas_cumprod_prev) / (1.0 - self.alphas_cumprod)
            )
            # below: log calculation clipped because the posterior variance is 0 at the beginning
            # of the diffusion chain
            self.posterior_log_variance_clipped = torch.log(self.posterior_variance.clamp(min =1e-20))
            
            self.posterior_mean_coef1 = (
                self.betas * torch.sqrt(self.alphas_cumprod_prev) / (1.0 - self.alphas_cumprod)
            )
            self.posterior_mean_coef2 = (
                (1.0 - self.alphas_cumprod_prev)
                * torch.sqrt(self.alphas)
                / (1.0 - self.alphas_cumprod)
            )
        
        # get the param of given timestep t
        def _extract(self, a, t, x_shape):
            batch_size = t.shape[0]
            out = a.to(t.device).gather(0, t).float()
            out = out.reshape(batch_size, *((1,) * (len(x_shape) - 1)))
            return out
        
        # forward diffusion (using the nice property): q(x_t | x_0)
        def q_sample(self, x_start, t, noise=None):
            if noise is None:
                noise = torch.randn_like(x_start)
    
            sqrt_alphas_cumprod_t = self._extract(self.sqrt_alphas_cumprod, t, x_start.shape)
            sqrt_one_minus_alphas_cumprod_t = self._extract(self.sqrt_one_minus_alphas_cumprod, t, x_start.shape)
    
            return sqrt_alphas_cumprod_t * x_start + sqrt_one_minus_alphas_cumprod_t * noise
        
        # Get the mean and variance of q(x_t | x_0).
        def q_mean_variance(self, x_start, t):
            mean = self._extract(self.sqrt_alphas_cumprod, t, x_start.shape) * x_start
            variance = self._extract(1.0 - self.alphas_cumprod, t, x_start.shape)
            log_variance = self._extract(self.log_one_minus_alphas_cumprod, t, x_start.shape)
            return mean, variance, log_variance
        
        # Compute the mean and variance of the diffusion posterior: q(x_{t-1} | x_t, x_0)
        def q_posterior_mean_variance(self, x_start, x_t, t):
            posterior_mean = (
                self._extract(self.posterior_mean_coef1, t, x_t.shape) * x_start
                + self._extract(self.posterior_mean_coef2, t, x_t.shape) * x_t
            )
            posterior_variance = self._extract(self.posterior_variance, t, x_t.shape)
            posterior_log_variance_clipped = self._extract(self.posterior_log_variance_clipped, t, x_t.shape)
            return posterior_mean, posterior_variance, posterior_log_variance_clipped
        
        # compute x_0 from x_t and pred noise: the reverse of `q_sample`
        def predict_start_from_noise(self, x_t, t, noise):
            return (
                self._extract(self.sqrt_recip_alphas_cumprod, t, x_t.shape) * x_t -
                self._extract(self.sqrt_recipm1_alphas_cumprod, t, x_t.shape) * noise
            )
        
        # compute predicted mean and variance of p(x_{t-1} | x_t)
        def p_mean_variance(self, model, x_t, t, clip_denoised=True):
            # predict noise using model
            pred_noise = model(x_t, t)
            # get the predicted x_0: different from the algorithm2 in the paper
            x_recon = self.predict_start_from_noise(x_t, t, pred_noise)
            if clip_denoised:
                x_recon = torch.clamp(x_recon, min=-1., max=1.)
            model_mean, posterior_variance, posterior_log_variance = \
                        self.q_posterior_mean_variance(x_recon, x_t, t)
            return model_mean, posterior_variance, posterior_log_variance
            
        # denoise_step: sample x_{t-1} from x_t and pred_noise
        @torch.no_grad()
        def p_sample(self, model, x_t, t, clip_denoised=True):
            # predict mean and variance
            model_mean, _, model_log_variance = self.p_mean_variance(model, x_t, t,
                                                        clip_denoised=clip_denoised)
            noise = torch.randn_like(x_t)
            # no noise when t == 0
            nonzero_mask = ((t != 0).float().view(-1, *([1] * (len(x_t.shape) - 1))))
            # compute x_{t-1}
            pred_img = model_mean + nonzero_mask * (0.5 * model_log_variance).exp() * noise
            return pred_img
        
        # denoise: reverse diffusion
        @torch.no_grad()
        def p_sample_loop(self, model, shape):
            batch_size = shape[0]
            device = next(model.parameters()).device
            # start from pure noise (for each example in the batch)
            img = torch.randn(shape, device=device)
            imgs = []
            for i in tqdm(reversed(range(0, timesteps)), desc='sampling loop time step', total=timesteps):
                img = self.p_sample(model, img, torch.full((batch_size,), i, device=device, dtype=torch.long))
                imgs.append(img.cpu().numpy())
            return imgs
        
        # sample new images
        @torch.no_grad()
        def sample(self, model, image_size, batch_size=8, channels=3):
            return self.p_sample_loop(model, shape=(batch_size, channels, image_size, image_size))
        
        # compute train losses
        def train_losses(self, model, x_start, t):
            # generate random noise
            noise = torch.randn_like(x_start)
            # get x_t
            x_noisy = self.q_sample(x_start, t, noise=noise)
            predicted_noise = model(x_noisy, t)
            loss = F.mse_loss(noise, predicted_noise)
            return loss

    其中几个主要的函数总结如下:

    • q_sample:实现的从x0到xt扩散过程;
    • q_posterior_mean_variance:实现的是后验分布的均值和方差的计算公式;
    • predict_start_from_noiseq_sample的逆过程,根据预测的噪音来生成x0;
    • p_mean_variance:根据预测的噪音来计算pθ(xt−1|xt)的均值和方差;
    • p_sample:单个去噪step;
    • p_sample_loop:整个去噪音过程,即生成过程。

    扩散模型的训练过程非常简单,如下所示:

    # train
    epochs = 10
    
    for epoch in range(epochs):
        for step, (images, labels) in enumerate(train_loader):
            optimizer.zero_grad()
            
            batch_size = images.shape[0]
            images = images.to(device)
            
            # sample t uniformally for every example in the batch
            t = torch.randint(0, timesteps, (batch_size,), device=device).long()
            
            loss = gaussian_diffusion.train_losses(model, images, t)
            
            if step % 200 == 0:
                print("Loss:", loss.item())
                
            loss.backward()
            optimizer.step()
    这里我们以mnist数据简单实现了一个mnist-demo,下面是一些生成的样本:

    对生成过程进行采样,如下所示展示了如何从一个随机噪音生层一个手写字体图像:

    另外这里也提供了CIFAR10数据集的demo:ddpm_cifar10,不过只训练了200epochs,生成的图像只是初见成效。

    小结

    相比VAE和GAN,扩散模型的理论更复杂一些,不过其优化目标和具体实现却并不复杂,这其实也让人感叹:一堆复杂的数据推导,最终却得到了一个简单的结论。要深入理解扩散模型,DDPM只是起点,后面还有比较多的改进工作,比如加速采样的DDIM以及DDPM的改进版本DDPM+和DDPM++。注:本人水平有限,如有谬误,欢迎讨论交流。

    参考

    • Denoising Diffusion Probabilistic Models
    • Understanding Diffusion Models: A Unified Perspective
    • https://spaces.ac.cn/archives/9119
    • https://keras.io/examples/generative/ddim/
    • What are Diffusion Models?
    • https://cvpr2022-tutorial-diffusion-models.github.io/
    • https://github.com/openai/improved-diffusion
    • https://huggingface.co/blog/annotated-diffusion
    • https://github.com/lucidrains/denoising-diffusion-pytorch
    • https://github.com/hojonathanho/diffusion