AIGC实战——扩散模型(Diffusion Model)

AIGC实战——扩散模型

    • 0. 前言
    • 1. 去噪扩散概率模型
      • 1.1 Flowers 数据集
      • 1.2 正向扩散过程
      • 1.3 重参数化技巧
      • 1.4 扩散规划
      • 1.5 逆向扩散过程
    • 2. U-Net 去噪模型
      • 2.1 U-Net 架构
      • 2.2 正弦嵌入
      • 2.3 ResidualBlock
      • 2.4 DownBlocks 和 UpBlocks
    • 3. 训练扩散模型
    • 4. 去噪扩散概率模型的采样
    • 5. 扩散模型分析
      • 5.1 生成图像
      • 5.2 调整逆扩散步数
      • 5.3 在图像之间进行插值
    • 小结
    • 系列链接

0. 前言

与生成对抗网络 (Generative Adversarial Network, GAN)一样,扩散模型是过去十年中最有影响力的生成模型技术之一。在许多基准测试中,当前的扩散模型已经超过了以往最先进的 GAN 模型,并迅速成为生成模型的首选。扩散模型的核心理念与其他生成模型,例如去噪自编码器、能量模型等有诸多相似之处。事实上,扩散来源于热力学扩散。同时,基于评分的生成模型领域(即能量模型)也取得了重要的进展,其直接估计对数分布的梯度(也称为评分函数),以训练模型。噪声条件得分网络 (Noise Conditional Score Network, NCSN) 使用多尺度噪声扰动应用于原始数据,以确保模型在低数据密度区域具有良好的性能表现。
扩散模型 (Diffusion Model) 在先前模型的基础上,揭示了扩散模型与基于评分的生成模型之间的联系,并训练了一个能够在多个数据集上与 GAN 相媲美的扩散模型,称为去噪扩散概率模型 (Denoising Diffusion Probabilistic ModelD, DDPM)。本节将介绍去噪扩散概率模型的工作原理。然后,学习如何使用 Keras 构建去噪扩散概率模型。

1. 去噪扩散概率模型

去噪扩散概率模型 (Denoising Diffusion Probabilistic Model, DDPM) 的核心思想是通过一系列小步骤训练一个深度学习模型去除图像中的噪声。如果我们从完全随机的噪声开始,理论上我们能够不断应用该模型,直到获得一幅看上去就像是从训练集中采样出来的图像。
首先,我们准备用于训练去噪扩散概率模型的数据集,然后分别介绍正向(加噪)和逆向(去噪)扩散过程。

1.1 Flowers 数据集

为了训练去噪扩散概率模型,使用 Kaggle 中的花卉数据集 Oxford 102 Flower,其中包含 8000 多张各种花卉的彩色图像。下载数据集后,将花卉图像解压并保存到 ./data 文件夹中。
使用 Kerasimage_dataset_from_directory 函数加载图像,将图像尺寸调整为 64×64,并将像素值缩放到 [0,1] 范围内。将数据集重复五次,以增加训练时长,并将数据分成组进行批处理,其中每组包含 64 张图像。

# 使用 Keras 的 image_dataset_from_directory 函数加载数据集
train_data = utils.image_dataset_from_directory("./data/dataset/train",labels=None,image_size=(IMAGE_SIZE, IMAGE_SIZE),batch_size=None,shuffle=True,seed=42,interpolation="bilinear",
)
# 将像素值缩放到[0, 1]范围内
def preprocess(img):img = tf.cast(img, "float32") / 255.0return imgtrain = train_data.map(lambda x: preprocess(x))
# 将数据集重复五次
train = train.repeat(DATASET_REPETITIONS)
# 将数据集分成组进行批处理,其中每组 64 张图像
train = train.batch(BATCH_SIZE, drop_remainder=True)

数据集中的示例图像如下图所示。

示例图像

获取数据集后,我们继续介绍如何使用正向扩散过程向图像添加噪声。

1.2 正向扩散过程

假设有一幅图像 x 0 x_0 x0,我们希望通过多个步骤(比如 T = 1 , 000 T=1,000 T=1,000 )逐渐破坏此图像,使得最终图像无法与标准的高斯噪声区分开(即 x T x_T xT 的均值为 0,方差为 1)。
可以定义一个函数 q q q,将方差为 β t β_t βt 的少量高斯噪声添加到图像 x t − 1 x_{t-1} xt1 中,生成新图像 x t x_t xt。如果我们反复应用这个函数,就会生成一系列噪声逐渐增加的图像 ( x 0 , . . . , x T ) (x_0, ..., x_T) (x0,...,xT),如下图所示。

正向扩散过程

使用数学公式表示这个更新过程(其中, ϵ t − 1 ϵ_{t-1} ϵt1 是均值为 0,方差为 1 的标准高斯分布):
x t = 1 − β t x t − 1 + β t ϵ t − 1 x_t = \sqrt{1 - β_t} x_{t-1} + \sqrt{β_t} ϵ_{t-1} xt=1βt xt1+βt ϵt1
需要注意的是,我们还对输入图像 x t − 1 x_{t-1} xt1 进行了缩放,以确保输出图像 x t x_t xt 的方差随时间保持不变。这样,如果我们将原始图像 x 0 x_0 x0 归一化为均值为 0 、方差为 1,那么当 T T T 足够大时,通过归纳法, x T x_T xT 将逼近标准高斯分布。
假设 x t − 1 x_{t-1} xt1 均值为 0、方差为 1,那么 1 − β t x t − 1 \sqrt {1 - β_t} x_{t-1} 1βt xt1 的方差将为 1 − β t 1 - β_t 1βt,而 β t ϵ t − 1 \sqrt {β_t} ϵ_{t-1} βt ϵt1 的方差将为 β t β_t βt,根据方差的规则 V a r ( a X ) = a 2 V a r ( X ) Var(aX) = a^2 Var(X) Var(aX)=a2Var(X)。将它们相加,得到一个新的分布 x t x_t xt,它的均值为 0,方差为 1 − β t + β t = 1 1 - β_t + β_t = 1 1βt+βt=1,根据方差的规则 V a r ( X + Y ) = V a r ( X ) + V a r ( Y ) Var(X+Y) = Var(X) + Var(Y) Var(X+Y)=Var(X)+Var(Y) (其中 X X X Y Y Y 是独立的)。因此,如果将 x 0 x_0 x0 归一化为均值为 0、方差为1,那么对于所有的 x t x_t xt,包括最后的图像 x T x_T xT,我们可以保证它们也满足这个条件,即逼近标准高斯分布。这样,我们就能够轻松地对 x T x_T xT 进行采样,然后通过训练好的神经网络模型应用逆扩散过程。换句话说,我们的正向添加噪声过程 q q q 也可以改写为:
q ( x t ∣ x t − 1 ) = N ( x t ; 1 − β t x t − 1 , β t I ) q(xt|x_{t-1}) = \mathcal N(x_t; \sqrt{1 - β_t} xt-1, β_t\mathbf I) q(xtxt1)=N(xt;1βt xt1,βtI)
其中 N \mathcal N N 表示高斯分布, I \mathbf I I 表示单位矩阵。

1.3 重参数化技巧

重新参数化技巧 (Reparameterization Trick) 是一种在不需要经过 t t t 次应用 q q q 的情况下,直接从图像 x 0 x_0 x0 跳转到任意噪声版本图像 x t x_t xt 的方法。
如果我们定义 α t = 1 − β t α_t=1-β_t αt=1βt α ‾ t = ∏ i = 1 t α i \overline α_t=∏_{i=1}^tα_i αt=i=1tαi,那么得到以下形式:
x t = α t x t − 1 + 1 − α t ϵ t − 1 = α t α t − 1 x t − 2 + 1 − α t α t − 1 ϵ t − 2 = . . . = α ‾ t x 0 + 1 − α ‾ t ϵ \begin{equation*} \begin{aligned} x_t &= \sqrt{α_t}x_{t-1} + \sqrt {1-α_t}ϵ_{t-1} \\ &= \sqrt{α_tα_{t-1}}x{t-2} + \sqrt{1-α_tα_{t-1}}ϵ_{t-2} \\ &=...\\ &= \sqrt{\overline \alpha_t}x_0+\sqrt{1-\overline\alpha_t}ϵ \\ \end{aligned} \end{equation*} xt=αt xt1+1αt ϵt1=αtαt1 xt2+1αtαt1 ϵt2=...=αt x0+1αt ϵ
需要注意的是,根据定理:两个高斯分布相加得到新的高斯分布。因此,我们可以从原始图像 x 0 x_0 x0 跳转到前向扩散过程的任何步骤 x t x_t xt。此外,我们可以使用 α ‾ t \overline \alpha_t αt 的值来定义扩散规划 (diffusion schedule),而不是使用原始的 β t β_t βt 值,其中 α ‾ t \overline \alpha_t αt 可以表示为与信号(原始图像 x 0 x_0 x0 )相关的方差,而 1 − α ‾ t 1-\overline α_t 1αt 则是与噪声 ( ϵ ϵ ϵ) 相关的方差。
因此,正向扩散过程 q q q 也可以表达为:
q ( x t ∣ x 0 ) = N ( x t ; α ‾ t x 0 , ( 1 − α ‾ t ) I ) q(x_t|x_0) = \mathcal N(x_t; \sqrt{\overline α_t}x_0, (1-\overline α_t)\mathbf I) q(xtx0)=N(xt;αt x0,(1αt)I)

1.4 扩散规划

需要注意的是,我们可以自由地在每个时间步选择不同的 β t β_t βt,它们不必全部相等。关于 β t β_t βt (或 α ‾ t \overline α_t αt )值如何随时间的变化称为扩散规划 (Di€usion Schedule)。
可以采用线性扩散规划 (linear diffusion schedule) 来定义 β t β_t βt,即 β t β_t βt 随着 t t t 线性增加,从 β 1 = 0.0001 β_1=0.0001 β1=0.0001 增加到 β T = 0.02 β_T=0.02 βT=0.02。这可以确保在噪声处理的早期阶段,我们采取较小的噪声步长,而在图像已经包含大量噪声的后期阶段,采取较大的噪声步长。使用 Keras 实现线性扩散规划。

def linear_diffusion_schedule(diffusion_times):min_rate = 0.0001max_rate = 0.02betas = min_rate + diffusion_times * (max_rate - min_rate)alphas = 1 - betasalpha_bars = tf.math.cumprod(alphas)signal_rates = tf.sqrt(alpha_bars)noise_rates = tf.sqrt(1 - alpha_bars)return noise_rates, signal_ratesT = 1000
# 扩散时间是在 0 和 1 之间相等间隔的步长
diffusion_times = tf.convert_to_tensor([x / T for x in range(T)])
#  将线性扩散规划应用于扩散时间,以获得噪声和信号速率
linear_noise_rates, linear_signal_rates = linear_diffusion_schedule(diffusion_times
)

实践证明,余弦扩散规划优于线性规划,余弦规划根据以下公式计算 α ‾ t \overline α_t αt 值:
α ‾ t = c o s 2 ( t T ⋅ π 2 ) \overline α_t = cos^2(\frac t T·\frac π 2) αt=cos2(Tt2π)
因此,更新后的方程如下(使用三角恒等式 c o s 2 ( x ) + s i n 2 ( x ) = 1 cos^2(x) + sin^2(x) = 1 cos2(x)+sin2(x)=1):
x t = c o s ( t T ⋅ π 2 ) x 0 + s i n ( t T ⋅ π 2 ) ϵ x_t = cos(\frac t T·\frac π 2)x_0 + sin(\frac t T·\frac π 2)ϵ xt=cos(Tt2π)x0+sin(Tt2π)ϵ
以上方程是原始余弦扩散规划的简化版本,可以在其中添加偏移项和比例因子,以防止扩散过程开始时的噪声步长过小。使用 Keras 实现余弦和偏移余弦扩散规划。

# 余弦扩散规划(不带偏移和比例因子)
def cosine_diffusion_schedule(diffusion_times):signal_rates = tf.cos(diffusion_times * math.pi / 2)noise_rates = tf.sin(diffusion_times * math.pi / 2)return noise_rates, signal_rates
#  偏移余弦扩散规划调整规划以确保在噪声处理开始时噪声步长不会太小
def offset_cosine_diffusion_schedule(diffusion_times):min_signal_rate = 0.02max_signal_rate = 0.95start_angle = tf.acos(max_signal_rate)end_angle = tf.acos(min_signal_rate)diffusion_angles = start_angle + diffusion_times * (end_angle - start_angle)signal_rates = tf.cos(diffusion_angles)noise_rates = tf.sin(diffusion_angles)return noise_rates, signal_ratescosine_noise_rates, cosine_signal_rates = cosine_diffusion_schedule(diffusion_times)
(offset_cosine_noise_rates, offset_cosine_signal_rates,) = offset_cosine_diffusion_schedule(diffusion_times)

可以计算每个 t t t α ‾ t \overline α_t αt 值,以获取在线性、余弦和偏移余弦扩散规划的每个时间步中通过的信号 ( α ‾ t \overline α_t αt) 和噪声 ( 1 − α ‾ t 1-\overline α_t 1αt)的量,如下图所示。

扩散规划

需要注意的是,在余弦扩散规划中,噪声水平的增加速度比线性扩散规划更慢。余弦扩散规划比线性扩散规划更平滑地向图像添加噪声,这提高了训练效率和生成质量。使用线性和余弦扩散规划在途中添加噪声的效果如下图所示:

扩散规划

1.5 逆向扩散过程

接下来,我们继续介绍逆向扩散过程,构建神经网络 p θ ( x t − 1 ∣ x t ) p_θ(x_{t-1}|x_t) pθ(xt1xt) 可以移除噪声处理,即近似逆向分布 q ( x t − 1 ∣ x t ) q(x_{t-1}|x_t) q(xt1xt)。如果我们能够构建此模型,就可以从 N ( 0 , I ) \mathcal N(0,I) N(0,I) 中采样随机噪声,然后多次应用逆向扩散过程,以生成一幅新的图像。

逆向扩散过程

逆向扩散过程和变分自编码器 (Variational Autoencoder,VAE) 的解码器之间有许多相似之处,这两者都旨在使用神经网络将随机噪声转化为有意义的输出。扩散模型和 VAE 之间的区别在于,在 VAE 中,正向过程(将图像转换为噪声)是模型的一部分(即它是可学习的),而在扩散模型中并没有参数化。
因此,可以在扩散模型中应用与变分自编码器相同的损失函数,原始的 DDM 论文推导了此损失函数的确切形式,并且表明,通过训练一个网络 ϵ θ ϵ_θ ϵθ 来预测已添加到给定图像 x 0 x_0 x0 的噪声 ϵ ϵ ϵ,即可以优化此损失函数。
换句话说,我们对图像 x 0 x_0 x0 进行采样,并通过 t t t 个噪声步骤将其转换为图像 x t = α ‾ t x 0 + ( 1 − α ‾ t ) ϵ x_t=\sqrt {\overline α_t}x_0 + \sqrt {(1-\overline α_t)}ϵ xt=αt x0+(1αt) ϵ。我们将这个新图像和噪声率 α ‾ t \overline α_t αt 提供给神经网络,并要求它预测 ϵ ϵ ϵ,计算预测 ϵ θ ( x t ) ϵθ_(x_t) ϵθ(xt) 与真实 ϵ ϵ ϵ 之间的平方误差的梯度,并使用梯度下降法进行优化。
需要注意的是,扩散模型实际上维护了网络的两个副本:一个是使用梯度下降主动训练的,另一个是指数移动平均 (Exponential Moving Average, EMA) 的网络,它是在先前的训练步骤中对主动训练网络权重进行的平均。EMA 网络不易受到训练过程中的短期波动和峰值的影响,因此在生成方面比被主动训练的网络更加稳健。因此,每当我们想要从网络中生成输出时,都会使用 EMA 网络。模型的训练过程如下所示。

模型训练流程

使用 Keras 实现上述训练步骤。

class DiffusionModel(models.Model):def __init__(self):super().__init__()self.normalizer = layers.Normalization()self.network = unetself.ema_network = models.clone_model(self.network)self.diffusion_schedule = offset_cosine_diffusion_scheduledef compile(self, **kwargs):super().compile(**kwargs)self.noise_loss_tracker = metrics.Mean(name="n_loss")@propertydef metrics(self):return [self.noise_loss_tracker]def denormalize(self, images):images = self.normalizer.mean + images * self.normalizer.variance**0.5return tf.clip_by_value(images, 0.0, 1.0)def denoise(self, noisy_images, noise_rates, signal_rates, training):if training:network = self.networkelse:network = self.ema_networkpred_noises = network([noisy_images, noise_rates**2], training=training)pred_images = (noisy_images - noise_rates * pred_noises) / signal_ratesreturn pred_noises, pred_imagesdef reverse_diffusion(self, initial_noise, diffusion_steps):num_images = initial_noise.shape[0]step_size = 1.0 / diffusion_stepscurrent_images = initial_noisefor step in range(diffusion_steps):diffusion_times = tf.ones((num_images, 1, 1, 1)) - step * step_sizenoise_rates, signal_rates = self.diffusion_schedule(diffusion_times)pred_noises, pred_images = self.denoise(current_images, noise_rates, signal_rates, training=False)next_diffusion_times = diffusion_times - step_sizenext_noise_rates, next_signal_rates = self.diffusion_schedule(next_diffusion_times)current_images = (next_signal_rates * pred_images + next_noise_rates * pred_noises)return pred_imagesdef generate(self, num_images, diffusion_steps, initial_noise=None):if initial_noise is None:initial_noise = tf.random.normal(shape=(num_images, IMAGE_SIZE, IMAGE_SIZE, 3))generated_images = self.reverse_diffusion(initial_noise, diffusion_steps)generated_images = self.denormalize(generated_images)return generated_imagesdef train_step(self, images):# 首先对图像进行归一化处理,使其均值为 0,方差为 1images = self.normalizer(images, training=True)# 采样噪声以匹配输入图像的形状noises = tf.random.normal(shape=(BATCH_SIZE, IMAGE_SIZE, IMAGE_SIZE, 3))# 采样随机扩散时间diffusion_times = tf.random.uniform(shape=(BATCH_SIZE, 1, 1, 1), minval=0.0, maxval=1.0)# 根据余弦扩散规划生成噪声和信号率noise_rates, signal_rates = self.diffusion_schedule(diffusion_times)# 将信号和噪声权重应用于输入图像,以生成带有噪声的图像noisy_images = signal_rates * images + noise_rates * noiseswith tf.GradientTape() as tape:# 通过要求网络预测噪声并撤消加噪操作(使用提供的噪声率和信号率),对带噪声的图像进行去噪pred_noises, pred_images = self.denoise(noisy_images, noise_rates, signal_rates, training=True)# 计算预测噪声与真实噪声之间的损失(平均绝对误差)noise_loss = self.loss(noises, pred_noises)  # used for traininggradients = tape.gradient(noise_loss, self.network.trainable_weights)# 针对损失函数使用梯度下降优化网络权重self.optimizer.apply_gradients(zip(gradients, self.network.trainable_weights))self.noise_loss_tracker.update_state(noise_loss)for weight, ema_weight in zip(self.network.weights, self.ema_network.weights):# 应用梯度下降之后,EMA网络的权重将更新为现有 EMA 权重和经过梯度下降步骤训练的网络权重的加权平均值ema_weight.assign(EMA * ema_weight + (1 - EMA) * weight)return {m.name: m.result() for m in self.metrics}def test_step(self, images):images = self.normalizer(images, training=False)noises = tf.random.normal(shape=(BATCH_SIZE, IMAGE_SIZE, IMAGE_SIZE, 3))diffusion_times = tf.random.uniform(shape=(BATCH_SIZE, 1, 1, 1), minval=0.0, maxval=1.0)noise_rates, signal_rates = self.diffusion_schedule(diffusion_times)noisy_images = signal_rates * images + noise_rates * noisespred_noises, pred_images = self.denoise(noisy_images, noise_rates, signal_rates, training=False)noise_loss = self.loss(noises, pred_noises)self.noise_loss_tracker.update_state(noise_loss)return {m.name: m.result() for m in self.metrics}

2. U-Net 去噪模型

接下来,我们使用 U-Net 作为去噪模型架构,以预测添加到给定图像中的噪声。

2.1 U-Net 架构

DDPM 使用了 U-Net 体系结构,下图展示了该网络的结构,并给出了通过网络时张量的形状。

U-Net 架构

与变分自编码器 (Variational Autoencoder, VAE) 类似,U-Net 由两部分组成:1) 下采样部分,输入图像在空间上逐渐缩小,但通道逐渐增加;2) 上采样部分,潜表示在空间上逐渐扩大,而通道数逐渐减少。然而,与 VAE 不同的是,网络的上采样和下采样部分之间还存在跳跃连接。VAE 是顺序的,数据从输入流经网络传递到输出,一层接一层;而 U-Net 不同,因为跳跃连接允许信息绕过网络的某些部分直接流向后面的网络层。
U-Net 架构中,输出与输入具有相同的形状,在扩散模型中,添加到图像中的噪声与图像本身的形状完全相同,因此 U-Net 成为去噪扩散概率模型网络架构的自然选择。
使用 Keras 中构建 U-Net 架构。

# U-Net 的第一个输入是我们希望去噪的图像
noisy_images = layers.Input(shape=(IMAGE_SIZE, IMAGE_SIZE, 3))
# 图像经过一个 Conv2D 层,增加通道的数量
x = layers.Conv2D(32, kernel_size=1)(noisy_images)
# U-Net 的第二个输入是噪声方差(标量值)
noise_variances = layers.Input(shape=(1, 1, 1))
# 使用正弦嵌入对其进行编码
noise_embedding = layers.Lambda(sinusoidal_embedding)(noise_variances)
# 将该嵌入复制到空间维度上,以匹配输入图像的大小
noise_embedding = layers.UpSampling2D(size=IMAGE_SIZE, interpolation="nearest")(noise_embedding)
# 两个输入流在通道维度上进行连接
x = layers.Concatenate()([x, noise_embedding])
# skips 列表用于保存连接到下游 UpBlock 层的 DownBlock 层的输出
skips = []
# 张量通过一系列的 DownBlock 层,减小图像的大小,同时增加通道的数量
x = DownBlock(32, block_depth=2)([x, skips])
x = DownBlock(64, block_depth=2)([x, skips])
x = DownBlock(96, block_depth=2)([x, skips])
# 张量通过两个 ResidualBlock 层,并保持图像大小和通道数不变
x = ResidualBlock(128)(x)
x = ResidualBlock(128)(x)
# 张量通过一系列的 UpBlock 层,增加图像的大小,同时减少通道的数量。跳跃连接用于输入下采样时对应的 DownBlock 层输出
x = UpBlock(96, block_depth=2)([x, skips])
x = UpBlock(64, block_depth=2)([x, skips])
x = UpBlock(32, block_depth=2)([x, skips])
# 最后一个 Conv2D 层将通道数减少为 3,得到 RGB 图像
x = layers.Conv2D(3, kernel_size=1, kernel_initializer="zeros")(x)
# 构建 U-Net 模型,以噪声图像和噪声方差作为输入,并输出预测的噪声图像
unet = models.Model([noisy_images, noise_variances], x, name="unet")

为了更深入地理解 U-Net,我们还需要了解四个概念:噪声方差的正弦嵌入,ResidualBlockDownBlockUpBlock

2.2 正弦嵌入

正弦嵌入 (Sinusoidal embedding) 最早由 Vaswani 等人提出,其核心思想是将标量值(噪声方差)转换为一个独特的高维向量,能够在网络下游提供更复杂的表示。原始论文根据这一思想将句子中的离散位置编码为向量;NeRF 将其扩展到连续值。
具体来说,一个标量值 x x x 编码如下:
γ ( x ) = ( s i n ( 2 π e 0 f x ) , . . . , s i n ( 2 π e ( L − 1 ) f x ) , c o s ( 2 π e 0 f x ) , . . . , c o s ( 2 π e ( L − 1 ) f x ) ) γ(x) = (sin(2πe^{0f}x), ..., sin(2πe^{(L-1)f}x), cos(2πe^{0f}x), ..., cos(2πe^{(L-1)f}x)) γ(x)=(sin(2πe0fx),...,sin(2πe(L1)fx),cos(2πe0fx),...,cos(2πe(L1)fx))
其中使用 L = 16 L=16 L=16 作为所需噪声嵌入长度的一半, f = l n ( 1000 ) ( L − 1 ) f=\frac {ln(1000)}{(L-1)} f=(L1)ln(1000) 作为频率的最大缩放因子,在这种情况下,嵌入模式如下图所示:

正弦嵌入

编写这个嵌入函数,将一个噪声方差标量值转换为长度为 32 的向量。

def sinusoidal_embedding(x):frequencies = tf.exp(tf.linspace(tf.math.log(1.0),tf.math.log(1000.0),NOISE_EMBEDDING_SIZE // 2,))angular_speeds = 2.0 * math.pi * frequenciesembeddings = tf.concat([tf.sin(angular_speeds * x), tf.cos(angular_speeds * x)], axis=3)return embeddings

2.3 ResidualBlock

DownBlockUpBlock 都使用了 ResidualBlock 层,我们已经学习了残差块的构建方法,在本节进行简单回顾。
残差块 (ResidualBlock) 是包含跳跃连接的一组神经网络层,将残差块输入添加到输出中。残差块可以用于构建更深的网络,学习更复杂的模式,而不会受到梯度消失和退化问题的严重影响。梯度消失问题是指随着网络深度增加,传播到较深层的梯度变得非常小,因此学习非常缓慢。退化问题是指随着神经网络变得更深,它们不一定比浅层网络更准确,准确率可能会在某个深度达到饱和,然后迅速退化。
He 等人在 ResNet 论文中引入了残差块,通过在网络层周围添加一个跳跃连接,模块有选择地绕过复杂的权重更新,并简单地通过恒等映射,使得网络可以在不牺牲梯度大小或网络准确性的情况下进行深度训练。
ResidualBlock 如下图所示,在某些残差块中,还需要在跳跃连接上使用核大小为 1Conv2D 层,以使通道数与块的其余部分保持一致。

残差块

使用 Keras 实现 ResidualBlock块

def ResidualBlock(width):def apply(x):input_width = x.shape[3]# 检查输入的通道数是否与该块预期输出的通道数匹配,如果不匹配,则在跳跃连接上添加一个额外的 Conv2D 层,以使通道数与 ResidualBlock 块的其余部分保持一致if input_width == width:residual = xelse:residual = layers.Conv2D(width, kernel_size=1)(x)# 应用 BatchNormalization 层x = layers.BatchNormalization(center=False, scale=False)(x)# 应用两个 Conv2D 层x = layers.Conv2D(width, kernel_size=3, padding="same", activation=activations.swish)(x)x = layers.Conv2D(width, kernel_size=3, padding="same")(x)# 将 ResidualBlock 输入添加到输出中,以获得 ResidualBlock 块的最终输出x = layers.Add()([x, residual])return xreturn apply

2.4 DownBlocks 和 UpBlocks

每个连续的 DownBlock 通过 block_depth (本节所用模型中为 2 )个 ResidualBlocks 增加通道数,同时还在最后应用了一个 AveragePooling2D 层,以将图像的尺寸减半。每个 ResidualBlock 都被添加到一个列表中,用于连接到 U-NetUpBlock 层作为跳跃连接。
UpBlock 首先应用一个 UpSampling2D 层,通过双线性插值将图像的尺寸扩大一倍。每个连续的 UpBlock 通过 block_depthResidualBlocks 减少通道数,同时还通过 U-Net 中的跳跃连接与 DownBlocks 的输出进行串联。该过程如下图所示。

DownBlock 和 UpBlock

使用 Keras 实现 DownBlockUpBlock

def DownBlock(width, block_depth):def apply(x):x, skips = xfor _ in range(block_depth):# DownBlock 使用 ResidualBlock 增加图像的通道数x = ResidualBlock(width)(x)# 将每个 ResidualBlock 保存到一个列表 (skips) 中,供 UpBlock 使用skips.append(x)# 最后,使用 AveragePooling2D 层将图像的尺寸减半x = layers.AveragePooling2D(pool_size=2)(x)return xreturn applydef UpBlock(width, block_depth):def apply(x):x, skips = x# UpBlock 以一个 UpSampling2D 层开始,将图像的尺寸扩大一倍x = layers.UpSampling2D(size=2, interpolation="bilinear")(x)for _ in range(block_depth):# 将当前输出与对应的 DownBlock 层的输出通过 Concatenate 层连接在一起x = layers.Concatenate()([x, skips.pop()])# 通过 UpBlock 时,使用 ResidualBlock 减少图像的通道数x = ResidualBlock(width)(x)return xreturn apply

3. 训练扩散模型

创建、编译并拟合去噪扩散概率模型。

# 实例化模型
ddm = DiffusionModel()
# 使用训练集计算归一化统计信息
ddm.normalizer.adapt(train)
# 使用 AdamW 优化器(类似于 Adam,但具有权重衰减,有助于稳定训练过程)和均方绝对误差损失函数编译模型
ddm.compile(optimizer=optimizers.experimental.AdamW(learning_rate=LEARNING_RATE, weight_decay=WEIGHT_DECAY),loss=losses.mean_absolute_error,
)
# 在 50 个 epochs 上拟合模型
ddm.fit(train, epochs=EPOCHS)

4. 去噪扩散概率模型的采样

为了从训练好的模型中采样图像,我们需要应用逆扩散 (reverse diffusion) 过程,也就是说,我们需要从随机噪声开始,并使用模型逐步消除噪声,直到得到一张清晰的花朵图片。
扩散模型被训练用于预测给定噪声图像中添加的总噪声量,而不仅仅是在添加噪声过程的最后一个时间步中添加的噪声。但是,我们不希望一次性完全消除噪声,通过一次性从完全随机的噪声中预测图像显然并不可行。我们希望能够模仿正向过程,逐步地在多个步骤中消除预测的噪声,以允许模型根据预测结果进行调整。
为了生成逼真图像,我们可以使用两个步骤从 x t x_t xt 跳转到 x t − 1 x_{t-1} xt1。首先,使用模型的噪声预测来计算原始图像 x 0 x_0 x0 的估计值,然后将预测的噪声重新应用于该图像,得到 x t − 1 x_{t-1} xt1 作为下一步迭代的图像,如下图所示。

图像生成过程

多次重复此过程,最终得到对 x 0 x_0 x0 的估计,得到一个高质量样本。实践中,可以自由选择迭代的步数,且不必与训练噪声过程中的时间步数(本节构建的模型中为 1000 )相同,反向迭代的步数可以较小,本节中,我们使用 20 个迭代步数。可以使用以下数学方程描述此过程:
x t − 1 = α ‾ t − 1 ( x t − 1 − α ‾ t ϵ θ ( t ) ( x t ) α ‾ t ) ⏟ p r e d i c t e d x 0 + 1 − α ‾ t − 1 − σ t 2 ϵ θ ( t ) ( x t ) ⏟ d i r e c t i o n p o i n t i n t o x t + σ t ϵ t ⏟ r a n d o m n o i s e x_{t-1} = \overline α_{t-1} \underbrace {(\frac {x_t-\sqrt{1 - \overlineα_t} ϵ_θ^{(t)}(x_t)} {\sqrt {\overline \alpha_t}})}_{predicted\ x_0} +\underbrace{ \sqrt{1 - \overline α_{t-1} - σ_t^2} ϵ_θ(t)(x_t)}_{direction\ pointin\ to\ x_t} + \underbrace {σ_t ϵ_t}_{random\ noise} xt1=αt1predicted x0 (αt xt1αt ϵθ(t)(xt))+direction pointin to xt 1αt1σt2 ϵθ(t)(xt)+random noise σtϵt
方程右侧括号内的第一项是使用网络预测的噪声计算得到的估计图像 x 0 x_0 x0。然后,我们将其乘以 t − 1 t-1 t1 时的信号率 α ‾ t − 1 \sqrt {\overline α_{t-1}} αt1 ,并再一次使用预测噪声(通过乘以 t − 1 t-1 t1 时的噪声率 1 − α ‾ t − 1 − σ t 2 \sqrt{1 - \overline α_{t-1} - σ_t^2} 1αt1σt2 进行缩放);并添加额外的高斯随机噪声 σ t ϵ t {σ_t ϵ_t} σtϵt,其中 σ t σ_t σt 用于决定生成过程的随机性有多高。
当对于所有 t t t σ t = 0 σ_t = 0 σt=0 时,模型称为去噪扩散隐式模型 (Denoising Diffusion Implicit Model, DDIM),使用 DDIM,生成过程完全是确定性的,也就是说,相同的随机噪声输入将始终产生相同的输出。这样我们就可以得到从潜空间到像素空间的明确定义的映射关系。
接下来,我们将构建 DDIM 以使生成过程具有确定性。实现 DDIM 采样过程(逆扩散):

    def reverse_diffusion(self, initial_noise, diffusion_steps):num_images = initial_noise.shape[0]step_size = 1.0 / diffusion_stepscurrent_images = initial_noise# 在固定的步数(例如 20 步)内生成观测样本for step in range(diffusion_steps):# 所有扩散时间都设为 1,即逆扩散过程开始时diffusion_times = tf.ones((num_images, 1, 1, 1)) - step * step_size# 噪声率和信号率根据扩散规划进行计算noise_rates, signal_rates = self.diffusion_schedule(diffusion_times)# 使用 U-Net 预测噪声,从而可以计算去噪图像的估计pred_noises, pred_images = self.denoise(current_images, noise_rates, signal_rates, training=False)# 扩散时间减少一步next_diffusion_times = diffusion_times - step_size# 计算新的噪声率和信号率next_noise_rates, next_signal_rates = self.diffusion_schedule(next_diffusion_times)# 根据 t-1 扩散规划重新应用预测的噪声到预测的图像,计算 t-1 时的图像current_images = (next_signal_rates * pred_images + next_noise_rates * pred_noises)# 完成 20 步后,返回最终的 x 预测图像 x0return pred_images

5. 扩散模型分析

接下来,我们学习如何使用训练好的模型进行三种不同的操作:生成新图像,测试逆扩散步数对生成图像质量的影响,以及在潜空间中两个图像之间进行插值。

5.1 生成图像

为了使用训练后的模型生成图像样本,我们只需运行逆扩散过程,并确保在最后对输出进行反归一化处理(即将像素值恢复到 [0, 1] 范围内):

class DiffusionModel(models.Model):...def denormalize(self, images):# 生成初始噪声图像images = self.normalizer.mean + images * self.normalizer.variance**0.5return tf.clip_by_value(images, 0.0, 1.0)def denoise(self, noisy_images, noise_rates, signal_rates, training):if training:network = self.networkelse:network = self.ema_networkpred_noises = network([noisy_images, noise_rates**2], training=training)def generate(self, num_images, diffusion_steps, initial_noise=None):# 生成初始噪声图像if initial_noise is None:initial_noise = tf.random.normal(shape=(num_images, IMAGE_SIZE, IMAGE_SIZE, 3))# 应用逆扩散过程generated_images = self.reverse_diffusion(initial_noise, diffusion_steps)# 网络输出的图像均值为 0,方差为 1,因此需要重新应用从训练数据计算得出的均值和方差来执行反归一化处理generated_images = self.denormalize(generated_images)return generated_images

在下图中,可以看到训练过程中扩散模型的生成的图像样本。

生成过程

5.2 调整逆扩散步数

我们还可以测试调整逆扩散步数对图像质量的影响。直觉上,扩散过程中采取的步数越多,图像生成的质量越高。

扩散步数

在上图中可以看到,随着逆扩散步数的增加,生成的质量确实得到了改善。最初的噪声样本中,模型只能预测出一个模糊的颜色块,随着步数的增加,模型能够改善并锐化其生成图像。然而,生成图像所需的时间与逆扩散步数成正比,因此需要在生成质量和生成速度之间进行权衡。

5.3 在图像之间进行插值

在变分自编码器中,我们可以在高斯潜空间的两点之间进行插值,以在像素空间中平滑地过渡图像。在扩散模型,我们使用球面插值方法,确保方差保持恒定,同时将两个高斯噪声图像混合在一起。具体而言,每个步骤的初始噪声图像由 a sin ⁡ ( π 2 t ) + b cos ⁡ ( π 2 t ) a \sin(\frac π2t)+b \cos(\frac π2t) asin(2πt)+bcos(2πt) 确定,其中 t t t01 之间平滑变化, a a a b b b 是我们希望进行插值的两个随机采样的高斯噪声张量,插值结果如下图所示。

图像插值

小结

本节中,我们介绍了最近最先进的生成模型之一,扩散模型。介绍了去噪扩散概率模型 (Denoising Diffusion Probabilistic Model, DDPM),并利用去噪扩散隐式模型 (Denoising Diffusion Implicit Model, DDIM) 的思想,使生成过程具备完全的确定性。扩散模型由前向扩散过程和逆扩散过程组成,前向扩散过程通过一系列小步骤向训练数据添加噪声,而逆扩散过程中模型的目标是预测添加的噪声。

系列链接

AIGC实战——生成模型简介
AIGC实战——深度学习 (Deep Learning, DL)
AIGC实战——卷积神经网络(Convolutional Neural Network, CNN)
AIGC实战——自编码器(Autoencoder)
AIGC实战——变分自编码器(Variational Autoencoder, VAE)
AIGC实战——使用变分自编码器生成面部图像
AIGC实战——生成对抗网络(Generative Adversarial Network, GAN)
AIGC实战——WGAN(Wasserstein GAN)
AIGC实战——条件生成对抗网络(Conditional Generative Adversarial Net, CGAN)
AIGC实战——自回归模型(Autoregressive Model)
AIGC实战——改进循环神经网络
AIGC实战——像素卷积神经网络(PixelCNN)
AIGC实战——归一化流模型(Normalizing Flow Model)
AIGC实战——能量模型(Energy-Based Model)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/264017.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

流计算之Flink

文章目录 概要有界无界流集群JobManagerTaskManagersTasks 和算子链Task Slots 和资源 小结 概要 Apache Flink 是一个框架和分布式处理引擎,用于在无边界和有边界数据流上进行有状态的计算。Flink 能在所有常见集群环境中运行,并能以内存速度和任意规模…

【C++】类与对象——友元,内部类,匿名对象

类与对象 1 友元1.1 概念:1.2 友元函数1.3 友元类 2 内部类概念:特性:举例: 3 匿名对象Thanks♪(・ω・)ノ谢谢阅读!!!下一篇文章见!!&am…

mac下使用jadx反编译工具

直接执行步骤: 1.创建 jadx目录 mkdir jadx2.将存储库克隆到目录 git clone https://github.com/skylot/jadx.git 3. 进入 jadx目录 cd jadx 4.执行编译 等待片刻 ./gradlew dist出现这个就代表安装好了。 5.最后找到 jadx-gui 可执行文件,双击两下…

【C语言】指针初阶

正文开始之前,我们要记住一个东西就是:地址指针 目录 一、指针的解释二、指针变量和地址1、取地址操作符2、指针变量和解引用操作1、指针变量2、拆解指针类型3、解引用操作符4、注意事项 3、指针变量的大小4、指针的解引用5、void*指针 三、指针的运算1、…

蓝桥杯备战刷题(自用)

1.被污染的支票 #include <iostream> #include <vector> #include <map> #include <algorithm> using namespace std; int main() {int n;cin>>n;vector<int>L;map<int,int>mp;bool ok0;int num;for(int i1;i<n;i){cin>>nu…

Spring事务模板及afterCommit存在的坑

大家好&#xff0c;我是墨哥&#xff08;隐墨星辰&#xff09;。今天的内容来源于两个线上问题&#xff0c;主要和大家聊聊为什么支付系统中基本只使用事务模板方法&#xff0c;而不使用声明式事务Transaction注解&#xff0c;以及使用afterCommit()出现连接未按预期释放导致的…

JAVA算法和数据结构

一、Arrays类 1.1 Arrays基本使用 我们先认识一下Arrays是干什么用的&#xff0c;Arrays是操作数组的工具类&#xff0c;它可以很方便的对数组中的元素进行遍历、拷贝、排序等操作。 下面我们用代码来演示一下&#xff1a;遍历、拷贝、排序等操作。需要用到的方法如下 public…

APP的UI自动化demo(appium+java)

文章目录 appium连接手机java代码实现-第一版第二版-接入testng和隐式等待显示等待 appium连接手机 准备工作 1、查看连接手机模拟器是否连接成功&#xff0c;获取设备名称 执行命令&#xff1a;adb devices 2、查看android内核版本号—>paltformVersion 执行命令&#xf…

springBoot整合Redis(一、Jedis操作Redis)

在springboot环境下连接redis的方法有很多&#xff0c;首先最简单的就是直接通过jedis类来连接&#xff0c;jedis类就相当于是redis的客户端表示。 但是因为现在比较常用的是&#xff1a;StringRedisTemplate和RedisTemplate&#xff0c;所以jedis只做简单的介绍。 一、Jedis…

2023 龙蜥操作系统大会演讲实录:《兼容龙蜥的云原生大模型数据计算系统——πDataCS》

本文主要分三部分内容&#xff1a;第一部分介绍拓数派公司&#xff0c;第二部分介绍 πDataCS 产品&#xff0c;最后介绍 πDataCS 与龙蜥在生态上的合作。 杭州拓数派科技发展有限公司&#xff08;简称“拓数派”&#xff0c;英文名称“OpenPie”&#xff09;是国内基础数据计…

文献阅读:Transformers are Multi-State RNNs

文献阅读&#xff1a;Transformers are Multi-State RNNs 1. 内容简介2. 方法介绍 1. 基础回顾 1. RNN2. Transformer 2. Transformer解构 1. MSRNN2. Transformer 3. TOVA 1. 现有转换策略2. TOVA 3. 实验考察 & 结论 1. 实验设计2. 实验结果 1. LM2. 长文本理解3. 文本生…

仗剑天涯路 侠气传千古《有翡》湖北热血开播

由吴锦源执导&#xff0c;赵丽颖、王一博领衔主演&#xff0c;张慧雯、陈若轩、孙坚、周洁琼、张昕宇、冷纪元主演的古装武侠剧《有翡》&#xff0c;将于2月25日晚19:30登陆湖北卫视长江剧场。该剧改编自Priest小说《有匪》&#xff0c;讲述了南北朝年间&#xff0c;各方势力盘…

modbus-tcp协议详解

本文参考&#xff1a;Modbus协议中文版【完整版】.pdf&#xff0c;加上自己的理解的记录&#xff0c;该文章主要讲modbus-TCP协议。&#xff08;文档下载链接&#xff1a;【免费】modbus协议中文详细解释文档资源-CSDN文库&#xff09; 本系列文章分为三章&#xff1a; 1.mod…

Stable Diffusion 绘画入门教程(webui)-ControlNet(深度Depth)

上篇文章介绍了线稿约束&#xff0c;这篇文章介绍下深度Depth 文章目录 一、选大模型二、写提示词三、基础参数设置四、启用ControlNet 顾名思义&#xff0c;就是把原图预处理为深度图&#xff0c;而深度图可以区分出图像中各元素的远近关系&#xff0c;那么啥事深度图&#xf…

【README 小技巧】在项目README.md 中展示发布到使用的JDK 版本

在项目README.md 中展示发布到使用的JDK 版本 <a target"_blank" href"https://www.oracle.com/java/technologies/javase/jdk11-archive-downloads.html"><img src"https://img-home.csdnimg.cn/images/20230724024159.png?origin_urlhtt…

分布式知识整理

分布式锁 以商场系统超卖现象举例 超卖现象一 现象&#xff1a; 商品卖出数量超出了库存数量。 产生原因&#xff1a; 扣减库存的动作在程序中进行&#xff0c;在程序中计算剩余库存&#xff0c;在并发场景下&#xff0c;导致库存计算错误。 代码复现 es.shutdown(); cycl…

智慧应急与物联网相结合:物联网技术如何提升智慧应急响应能力

目录 一、引言 二、智慧应急与物联网技术的结合 三、物联网技术提升智慧应急响应能力的途径 四、物联网技术在智慧应急中的应用案例 五、物联网技术在智慧应急中面临的挑战与解决方案 挑战一&#xff1a;技术标准与规范不统一 解决方案&#xff1a; 挑战二&#xff1a;…

Linux之JAVA环境配置jdkTomcatMySQL

目录 一. 安装jdk 1.1 查询是否有jdk 1.2 解压 1.3 配置环境变量 二. 安装Tomcat&#xff08;开机自启动&#xff09; 2.1 解压 2.2 启动tomcat 2.3 防火墙设置 2.4 创建启动脚本&#xff08;设置自启动&#xff0c;服务器开启即启动&#xff09; 三. MySQL安装&#xff08;…

国漫年番成趋势?但只有这5部最值得看

自从《斗罗大陆》动画爆火之后&#xff0c;越来越多国漫都开始以年番形式播出&#xff0c;每周都能追自己喜欢的动画也是观众们所期待的。但其实年番对制作公司的要求很高&#xff0c;如果技术跟不上难免出现质量下滑的问题。今天就带大家盘点一下目前在播的最值得看的5部国漫年…

查看navicat保存的数据库连接密码

背景 经常使用navicat的朋友可能会碰到忘记数据库连接密码的情况&#xff0c;自然会想到navicat连接配置中就保存了密码。 个人经验&#xff0c;按以下步骤可查看密码明文 本人在mac上使用的navicat版本 1&#xff0c;导出connection_local.ncx 点击OK导出保存为connection_l…