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.mzph.cn/news/703149.shtml

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

相关文章

STM32 4位数码管和74HC595

4位数码管 在使用一位数码管的时候,会用到8个IO口,那如果使用4位数码管,难道要使用32个IO口吗?肯定是不行的,太浪费了IO口了。把四个数码管全部接一起共用8个IO口,然后分别给他们一个片选。所以4位数码管共…

✅技术社区项目—JWT身份验证

通用的JWT鉴权方案 JWT鉴权流程 基本流程分三步: ● 用户登录成功之后,后端将生成的jwt返回给前端,然后前端将其保存在本地缓存; ● 之后前端与后端的交互时,都将iwt放在请求头中,比如可以将其放在Http的身份认证的请求头 Author…

C语言编程安全规范

目的 本规范旨在加强编程人员在编程过程中的安全意识,建立编程人员的攻击者思维,养成安全编码的习惯,编写出安全可靠的代码。 2 宏 2.1 用宏定义表达式时,要使用完备的括号 2.2 使用宏时,不允许参数发生变化 3 变量 3.1 所有变量在定义时必须赋初值 变量声明赋予初值,可…

B端系统:导航机制设计,用户体验提升的法宝

Hi,大家好,我是贝格前端工场,从事8年前端开发的老司机。很多B端系统体验不好很大一部分原因在于导航设计的不合理,让用户无所适从,大大降低了操作体验,本文着重分析B端系统的导航体系改如何设计&#xff0c…

$attrs

一、概念 vue官网定义如下: 包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过v-bind="$attrs"传入内部组件——在创建…

抖店是怎么运营做起来的?一文详解抖店的运营逻辑和流程,可收藏

我是王路飞。 很多人都知道现在的抖音有【商城】,进入之后就是一个个的抖音小店了,也知道抖店的红利。 但是抖店具体是怎么运营并且做起来的,就不太清楚了,因此很多新手明明眼馋抖店的红利,却又无从下手。 今天这篇…

Java 中常用的数据结构类 API

目录 常用数据结构API 对应的线程安全的api 高可用衡量标准 常用数据结构API ArrayList: 实现了动态数组,允许快速随机访问元素。 import java.util.ArrayList; LinkedList: 实现了双向链表,适用于频繁插入和删除操作。 import java.util.LinkedLis…

Spring综合漏洞利用工具

Spring综合漏洞利用工具 工具目前支持Spring Cloud Gateway RCE(CVE-2022-22947)、Spring Cloud Function SpEL RCE (CVE-2022-22963)、Spring Framework RCE (CVE-2022-22965) 的检测以及利用,目前仅为第一个版本,后续会添加更多漏洞POC,以及…

逆向茶话会笔记

安卓逆向 用用burp设置代理或者用charles抓包 windows httpopen 类比web站点渗透测试 推荐书 飞虫 安卓大佬不怎么打ctf 喜欢在看雪和吾爱破解 提问环节 q websocket grpc抓包有什么推荐的工具? a 不太了解 游戏安全和llvm 既要逆游戏也要逆外挂 逆游戏入…

发电机测试的常见参数和规格有哪些需要关注?

发电机测试是确保其正常运行和性能的关键步骤。在进行发电机测试时,需要关注一些常见的参数和规格,以确保其安全、高效和可靠的运行。以下是一些需要关注的发电机测试参数和规格: 1. 电压:发电机的输出电压是衡量其性能的重要指标…

【数据结构初阶 7】二叉树:链式二叉树的基本操作实现

文章目录 🌈 Ⅰ 定义二叉树结点🌈 Ⅱ 创建二叉树结点🌈 Ⅲ 遍历二叉树1. 先序遍历2. 中序遍历3. 后序遍历4. 层序遍历 🌈 Ⅳ 销毁二叉树 🌈 Ⅰ 定义二叉树结点 1. 每个结点都由三部分组成 数据域:存储本结…

代码随想录Leetcode474. 一和零

题目&#xff1a; 代码(首刷看解析 2024年2月26日&#xff09; class Solution { public:// 二维 0 1背包int findMaxForm(vector<string>& strs, int m, int n) {// 1 二维 [i]表示 0 的个数&#xff0c;上限m; [j]表示 1 的个数&#xff0c;上限nvector<vector…

大佬推荐的网络安全学习路线(从基础到高级)

说起网络安全&#xff0c;你可能会担心它是一个过时的行业。有人说&#xff0c;网络安全快卷死了&#xff0c;你既要攻又要防&#xff0c;并且随着技术的发展&#xff0c;你还要不断地学习&#xff0c;不在卷中生&#xff0c;就在卷中死。 实际上&#xff0c;随着数字化进程的…

BUGKU-WEB 文件包含

题目描述 题目截图如下&#xff1a; 进入场景看看&#xff1a; 解题思路 你说啥我就干啥&#xff1a;点击一下试试你会想到PHP伪协议这方面去嘛&#xff0c;你有这方面的知识储备吗&#xff1f; 相关工具 解题步骤 查看源码 看到了一点提示信息&#xff1a; ./index.…

Python文件和异常(二)

目录 三、异常 &#xff08;一&#xff09;处理 ZeroDivisionError 异常 &#xff08;二&#xff09;使用 try-except 代码块 &#xff08;三&#xff09;使用异常避免崩溃 &#xff08;四&#xff09;else 代码块 &#xff08;五&#xff09;处理 FileNotFoundError 异常…

什么是web组态?

一、web组态的定义和背景 在深入探讨之前&#xff0c;我们先回顾一下“组态”的定义。在工业自动化领域&#xff0c;组态软件是用于创建监控和数据采集&#xff08;SCADA&#xff09;系统的工具&#xff0c;它允许工程师构建图形界面&#xff0c;实现与各种设备和机器的数据交互…

C++线程同步(下)

多线程同步 概述信号量示例一代码实现运行结果分析示例二开发环境代码实现运行结果分析future和promise示例一实现代码运行结果分析示例二实现代码运行结果示例三实现代码运行结果示例四示例代码运行结果注意附加示例扩展实现代码运行结果注意扩展原子应用场景头文件示例实现代…

【蓝桥杯】拓扑排序

一.拓扑排序 1.定义&#xff1a; 设G&#xff08;V&#xff0c;E&#xff09;是一个具有n个顶点的有向图&#xff0c;V中的顶点序列称为一个拓扑序列&#xff0c;当且仅当满足下列条件&#xff1a;若从顶点到有一条路径&#xff0c;则在顶点序列中顶点必在之前。 2.基本思想…

prime_series_level-1靶场详解

环境搭建 官网https://www.vulnhub.com/entry/prime-1,358/ 直接导入靶机 解题思路 arp-scan -l 确认靶机ip为192.168.236.136 也可以使用nmap扫网段 nmap -sn 192.168.236.0/24 使用nmap扫描靶机开放的端口 nmap -sS -T5 --min-rate 10000 192.168.236.136 -sC -p- &#xf…

【项目部署上线】宝塔部署前端Docker部署后端

【项目部署上线】宝塔部署前端&Docker部署后端 文章目录 【项目部署上线】宝塔部署前端&Docker部署后端1.安装依赖1.1 安装mysql1.2 安装Canal1.3 安装redis1.4 安装rabbitmq1.5 安装nacos 2. 部署前端3. 部署后端 1.安装依赖 1.1 安装mysql docker run -d -p 3306:3…