动态噪声重塑:基于时间嵌入U-Net的PyTorch扩散模型实战​

本文基于PyTorch框架帮助读者从零开始理解和实现扩散模型,并给出一个基于时间嵌入U-Net模型的基础型扩散模型实战开发案例。

简介

在我最近发表的几篇文章中,我谈到了生成式深度学习算法,这些算法大多与文本生成任务有关。所以,我认为现在转向图像生成的生成算法研究会很有趣。我们知道,如今已经有很多专门用于生成图像的深度学习模型,例如自动编码器、变分自动编码器(VAE)、生成对抗网络(GAN)和神经风格迁移(NST)。

在本文中,我想讨论所谓的扩散模型 ——这是深度学习领域中对图像生成影响最大的模型之一。该算法的想法最早是在2015年由Sohl-Dickstein等人撰写的题为《使用非平衡热力学的深度无监督学习》的论文中提出的【引文1】。他们的框架随后由Ho等人在2020年的论文《去噪扩散概率模型》中进一步开发【引文2】。DDPM后来被OpenAI和Google改编以开发DALLE-2和Imagen,我们知道这些模型具有生成高质量图像的令人印象深刻的能力。

扩散模型的工作原理

一般来说,扩散模型的工作原理是从噪声中生成图像。我们可以把它想象成一个艺术家将画布上的一抹颜料变成一幅美丽的艺术品。为了做到这一点,首先需要训练扩散模型。训练模型需要遵循两个主要步骤,即前向扩散和后向扩散。

图1.正向和反向扩散过程【引文3】

如上图所示,前向扩散是一个将高斯噪声迭代地应用于原始图像的过程。我们不断添加噪声,直到图像完全无法识别,此时我们可以说图像现在位于潜在空间中。与自动编码器和GAN中的潜在空间通常比原始图像的维度低不同,DDPM中的潜在空间保持与原始图像完全相同的维度。这个噪声过程遵循马尔可夫链的原理,这意味着时间步t处的图像仅受时间步t-1的影响。前向扩散被认为很容易,因为我们基本上只是一步一步地添加一些噪声。

第二个训练阶段称为反向扩散,我们的目标是一点一点地去除噪声,直到获得清晰的图像。这个过程遵循逆马尔可夫链的原理,其中时间步长t-1的图像只能基于时间步长t的图像获得。这样的去噪过程非常困难,因为我们需要猜测哪些像素是噪声,哪些像素属于实际图像内容。因此,我们需要采用神经网络模型来实现这一点。

DDPM使用U-Net作为反向扩散深度学习架构的基础。但是,我们不需要使用原始的U-Net模型【引文4】,而是需要对其进行一些修改,以使其更适合我们的任务。稍后,我将在MNIST手写数字数据集【引文5】上训练此模型,看看它是否可以生成类似的图像。

好了,以上就是目前你需要了解的有关扩散模型的所有基本概念。在接下来的内容中,我们将从头开始实现扩散模型算法,从而使读者更深入地了解有关细节。

PyTorch实现

我们将首先导入所需的模块。如果你还不熟悉下面的导入,那么告诉你torch和torchvision都是我们用于准备模型和数据集的库。同时,matplotlib和tqdm将帮助我们显示图像和进度条。

复制

# Codeblock 1
import matplotlib.pyplot as plt
import torch
import torch.nn as nn

from torch.optim import Adam
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
from tqdm import tqdm
由于模块已导入,接下来要做的是初始化一些配置参数。详细信息请参阅下面的Codeblock 2。
# Codeblock 2
IMAGE_SIZE = 28 #(1)
NUM_CHANNELS = 1 #(2)

BATCH_SIZE = 2
NUM_EPOCHS = 10
LEARNING_RATE = 0.001

NUM_TIMESTEPS = 1000 #(3)
BETA_START = 0.0001 #(4)
BETA_END = 0.02 #(5)
TIME_EMBED_DIM = 32 #(6)
DEVICE = torch.device("cuda" if torch.cuda.is_available else "cpu") #(7)
DEVICE
# Codeblock 2 Output
device(type='cuda')1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.

在代码行#(1)和#(2)中,我设置IMAGE_SIZE和NUM_CHANNELS两个常量分别为28和1,这些数字是从MNIST数据集中的图像维度获得的。变量BATCH_SIZE,NUM_EPOCHS和LEARNING_RATE非常简单,所以我不需要进一步解释它们。

在代码行#(3),变量NUM_TIMESTEPS表示前向和后向扩散过程中的迭代次数。时间步长0是图像处于原始状态的情况(图1中最左边的图像)。在这种情况下,由于我们将此参数设置为1000,时间步长数999将是图像完全无法识别的情况(上面图1中最右边的图像)。重要的是要记住,时间步长的选择涉及模型准确性和计算成本之间的权衡。如果我们为分配一个较小的值NUM_TIMESTEPS,推理时间会更短,但由于模型在后向扩散阶段细化图像的步骤较少,因此生成的图像可能不是很好。另一方面,增加NUM_TIMESTEPS会减慢推理过程,但我们可以预期输出图像具有更好的质量,这要归功于逐步的去噪过程,从而可以实现更精确的重建。

接下来,BETA_START(代码行#(4))和BETA_END(代码行#(5))变量分别用于控制每个时间步添加的高斯噪声量,而TIME_EMBED_DIM(代码行#(6))用于确定用于存储时间步信息的特征向量长度。最后,在第#(7)行,如果Pytorch检测到我们的机器上安装了GPU,我将分配“cuda”给变量。我强烈建议你在GPU上运行此项目,因为训练扩散模型的计算成本很高。除了上述参数之外,为NUM_TIMESTEPS、BETA_START和BETA_END设定的值均直接采用自DDPM论文【引文2】。

完整的实现过程可分为几个步骤:构建U-Net模型、准备数据集、定义扩散过程的噪声调度程序、训练和推理。我们将在以下小节中讨论每个阶段。

U-Net架构:时间嵌入

正如我之前提到的,扩散模型的基础是U-Net。之所以使用这种架构,是因为它的输出层适合表示图像,这绝对是有道理的,因为它最初是为图像分割任务引入的。下图显示了原始U-Net架构的样子。

图2.【引文4】中提出的原始U-Net模型

然而,有必要修改一下此架构,以便它也能考虑时间步长信息。不仅如此,由于我们只使用MNIST数据集,我们还需要使模型更小。只需记住深度学习中的惯例,即更简单的模型通常对简单任务更有效。

下图中我展示了经过修改的整个U-Net模型。在这里你可以看到时间嵌入张量在每个阶段都被注入到模型中,稍后将通过逐元素求和来完成,从而使模型能够捕获时间步长信息。接下来,我们不会像原始U-Net那样将每个下采样和上采样阶段重复四次,而是在本例中只重复两次。此外,值得注意的是,下采样阶段的堆栈也称为编码器,而上采样阶段的堆栈通常称为解码器。

图3.针对我们的扩散任务修改后的U-Net模型【引文3】

现在,让我们开始构建架构,创建一个用于生成时间嵌入张量的类,其思想类似于Transformer中的位置嵌入。有关详细信息,请参阅下面的Codeblock 3。

复制

# Codeblock 3
class TimeEmbedding(nn.Module):
 def forward(self):
 time = torch.arange(NUM_TIMESTEPS, device=DEVICE).reshape(NUM_TIMESTEPS, 1) #(1)
 print(f"time		: {time.shape}")

 i = torch.arange(0, TIME_EMBED_DIM, 2, device=DEVICE)
 denominator = torch.pow(10000, i/TIME_EMBED_DIM)
 print(f"denominator	: {denominator.shape}")

 even_time_embed = torch.sin(time/denominator) #(1)
 odd_time_embed = torch.cos(time/denominator) #(2)
 print(f"even_time_embed	: {even_time_embed.shape}")
 print(f"odd_time_embed	: {odd_time_embed.shape}")

 stacked = torch.stack([even_time_embed, odd_time_embed], dim=2) #(3)
 print(f"stacked		: {stacked.shape}")
 time_embed = torch.flatten(stacked, start_dim=1, end_dim=2) #(4)
 print(f"time_embed	: {time_embed.shape}")

 return time_embed1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.

上述代码中,我们基本上要创建一个大小为NUM_TIMESTEPS×TIME_EMBED_DIM(1000×32)的张量,其中该张量的每一行都包含时间步长信息。稍后,1000个时间步长中的每一个都将由长度为32的特征向量表示。张量本身的值是根据图4中的两个方程获得的。在上面的Codeblock 3中,这两个方程分别在第#(1)和#(2)行实现,每个方程都形成一个大小为1000×16的张量。接下来,使用第#(3)行和#(4)行的代码将这些张量组合起来。

在这里,我还打印出了上述代码块中完成的每个步骤,以便你更好地理解TimeEmbedding类中实际执行的操作。如果你仍想了解有关上述代码的更多解释,请随时阅读我之前关于Transformer的博客文章,你可以通过本文末尾的链接访问该文章。单击链接后,你可以一直向下滚动到“Positional Encoding”部分。

图4. Transformer论文【引文6】中的正弦位置编码公式

现在,让我们使用以下测试代码检查是否TimeEmbedding类正常工作。结果输出显示它成功生成了一个大小为1000×32的张量,这正是我们之前所期望的。

复制

# Codeblock 4
time_embed_test = TimeEmbedding()
out_test = time_embed_test()
# Codeblock 4 Output
time : torch.Size([1000, 1])
denominator : torch.Size([16])
even_time_embed : torch.Size([1000, 16])
odd_time_embed : torch.Size([1000, 16])
stacked : torch.Size([1000, 16, 2])
time_embed : torch.Size([1000, 32])1.2.3.4.5.6.7.8.9.10.

U-Net架构:DoubleConv

如果仔细观察修改后的架构,你会发现我们实际上得到了许多重复的模型,例如下图中黄色框中突出显示的模型。

图5.黄色框内完成的流程将在DoubleConv课堂上实现【引文3】

这五个黄色框具有相同的结构,它们由两个卷积层组成,在执行第一个卷积操作后立即注入时间嵌入张量。因此,我们现在要做的是创建另一个名为DoubleConv的类来重现此结构。查看下面的代码块5a和5b以了解我如何做到这一点。

复制

# Codeblock 5a
class DoubleConv(nn.Module):
 def __init__(self, in_channels, out_channels): #(1)
 super().__init__()

 self.conv_0 = nn.Conv2d(in_channels=in_channels, #(2)
 out_channels=out_channels, 
 kernel_size=3, 
 bias=False, 
 padding=1)
 self.bn_0 = nn.BatchNorm2d(num_features=out_channels) #(3)

 self.time_embedding = TimeEmbedding() #(4)
 self.linear = nn.Linear(in_features=TIME_EMBED_DIM, #(5)
 out_features=out_channels)

 self.conv_1 = nn.Conv2d(in_channels=out_channels, #(6)
 out_channels=out_channels, 
 kernel_size=3, 
 bias=False, 
 padding=1)
 self.bn_1 = nn.BatchNorm2d(num_features=out_channels) #(7)

 self.relu = nn.ReLU(inplace=True) #(8)1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.

上述方法__init__()中的两个输入参数使我们可以灵活地配置输入和输出通道的数量(#(1));这样一来,DoubleConv类可以通过调整输入参数来实例化所有五个黄色框。顾名思义,这里我们初始化了两个卷积层(行#(2)和#(6)),每个卷积层后跟一个批量归一化层和一个ReLU激活函数。请记住,两个归一化层需要单独初始化(行#(3)和#(7)),因为它们每个都有自己可训练的归一化参数。同时,ReLU激活函数应该只初始化一次(#(8)),因为它不包含任何参数,因此可以在网络的不同部分多次使用。在代码行#(4),我们初始化之前创建的TimeEmbedding层,该层稍后将连接到标准线性层(#(5))。该线性层负责调整时间嵌入张量的维度,以便可以以逐元素的方式将得到的输出与第一个卷积层的输出相加。

现在,让我们看一下下面的Codeblock 5b,以更好地理解该DoubleConv块的运行流程。在这部分代码中,你可以看到forward()方法接受两个输入:原始图像x和时间步长信息t,如第#(1)行所示。我们首先使用第一个Conv-BN-ReLU序列处理图像(#(2–4))。这种Conv-BN-ReLU结构通常用于基于CNN的模型,即使插图没有明确显示批量标准化和ReLU层。除了图像之外,我们还从相应图像的嵌入张量(#(5))中获取第t个时间步长信息并将其传递给线性层(#(6))。我们仍然需要使用第#(7)行的代码扩展结果张量的维度,然后在第#(8)行执行逐元素求和。最后,我们使用第二个Conv-BN-ReLU序列(#(9–11))处理结果张量。

复制

# Codeblock 5b
 def forward(self, x, t): #(1)
 print(f'images			: {x.size()}')
 print(f'timesteps		: {t.size()}, {t}')

 x = self.conv_0(x) #(2)
 x = self.bn_0(x) #(3)
 x = self.relu(x) #(4)
 print(f'
after first conv	: {x.size()}')

 time_embed = self.time_embedding()[t] #(5)
 print(f'
time_embed		: {time_embed.size()}')

 time_embed = self.linear(time_embed) #(6)
 print(f'time_embed after linear	: {time_embed.size()}')

 time_embed = time_embed[:, :, None, None] #(7)
 print(f'time_embed expanded	: {time_embed.size()}')

 x = x + time_embed #(8)
 print(f'
after summation		: {x.size()}')

 x = self.conv_1(x) #(9)
 x = self.bn_1(x) #(10)
 x = self.relu(x) #(11)
 print(f'after second conv	: {x.size()}')

 return x1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.

为了查看我们的DoubleConv类实现是否正常工作,我们将使用下面的Codeblock 6对其进行测试。在这里,我想模拟此块的第一个实例,它对应于图5中最左边的黄色框。为此,我们需要将in_channels和out_channels参数分别设置为1和64(#(1))。接下来,我们初始化两个输入张量,即x_test和t_test。其中,x_test张量的大小为2×1×28×28,表示一批大小为28×28的两个灰度图像(#(2))。请记住,这只是一个赋予了随机数值的虚拟张量,它将在训练阶段的后期被来自MNIST数据集的实际图像替换。同时,t_test是一个包含相应图像的时间步长数的张量(#(3))。此张量的值在0到NUM_TIMESTEPS(1000)之间随机选择。请注意,此张量的数据类型必须是整数,因为这些数字将用于索引,如Codeblock 5b中的第#(5)行所示。最后,在代码行#(4)我们将x_test和t_test张量传递给double_conv_test层。

顺便说一句,print()在运行以下代码之前,我重新运行了前面的代码块并删除了函数,以便输出结果看起来更整洁一些。

复制

# Codeblock 6
double_conv_test = DoubleConv(in_channels=1, out_channels=64).to(DEVICE) #(1)

x_test = torch.randn((BATCH_SIZE, NUM_CHANNELS, IMAGE_SIZE, IMAGE_SIZE)).to(DEVICE) #(2)
t_test = torch.randint(0, NUM_TIMESTEPS, (BATCH_SIZE,)).to(DEVICE) #(3)

out_test = double_conv_test(x_test, t_test) #(4)
# Codeblock 6 Output
images : torch.Size([2, 1, 28, 28]) #(1)
timesteps : torch.Size([2]), tensor([468, 304], device='cuda:0') #(2)

after first conv : torch.Size([2, 64, 28, 28]) #(3)

time_embed : torch.Size([2, 32]) #(4)
time_embed after linear : torch.Size([2, 64])
time_embed expanded : torch.Size([2, 64, 1, 1]) #(5)

after summation : torch.Size([2, 64, 28, 28]) #(6)
after second conv : torch.Size([2, 64, 28, 28]) #(7)1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.

原始输入张量的形状可以在上面的输出的第#(1)和#(2)行看到。具体来说,在第#(2)行,我还打印出了我们随机选择的两个时间步。在这个例子中,我们假设x张量中的两个图像在输入网络之前已经用来自第468和第304个时间步的噪声级别进行了噪声处理。我们可以看到,在经过第一个卷积层(#(3)行)后,图像张量x的形状变为2×64×28×28。同时,我们的时间嵌入张量的大小变为2×32(#(4)行),这是通过从大小为1000×32的原始嵌入中提取第468行和第304行获得的。为了能够执行元素级求和(#(6)行),我们需要将32维时间嵌入向量映射到64维并扩展它们的轴,从而得到大小为2×64×1×1的张量(#(5)行),以便可以将其广播到2×64×28×28张量。求和完成后,我们将张量传递到第二个卷积层,此时张量维度完全不会改变(#(7)行)。

U-Net架构:编码器

到目前为止,既然我们已经成功实现了DoubleConv块,那么接下来要做的就是实现所谓的DownSample块。在下面的图6中,这对应于红色框内的部分。

图6.网络中以红色突出显示的部分是所谓的DownSample块【引文3】

DownSample块的目的是减少图像的空间维度,但需要注意的是,它同时增加了通道数。为了实现这一点,我们可以简单地堆叠一个DoubleConv块和一个最大池化操作。在这种情况下,池化使用2×2内核大小,步长为2,导致图像的空间维度是输入的两倍。此块的实现可以在下面的Codeblock 7中看到。

复制

# Codeblock 7
class DownSample(nn.Module):
 def __init__(self, in_channels, out_channels): #(1)
 super().__init__()

 self.double_conv = DoubleConv(in_channels=in_channels, #(2)
 out_channels=out_channels)
 self.maxpool = nn.MaxPool2d(kernel_size=2, stride=2) #(3)

 def forward(self, x, t): #(4)
 print(f'original		: {x.size()}')
 print(f'timesteps		: {t.size()}, {t}')

 convolved = self.double_conv(x, t) #(5)
 print(f'
after double conv	: {convolved.size()}')

 maxpooled = self.maxpool(convolved) #(6)
 print(f'after pooling		: {maxpooled.size()}')

 return convolved, maxpooled #(7)1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.

在这里,我将__init__()方法设置为采用输入和输出通道的数量,以便我们可以使用它来创建图6中突出显示的两个DownSample块,而无需将它们写入单独的类(#(1))。接下来,DoubleConv和最大池化层分别在第#(2)和#(3)行初始化。请记住,由于DoubleConv块接受图像x和相应的时间步t作为输入,我们还需要设置此DownSample块的forward()方法,以便使其也接受它们两个参数(#(4))。然后,当double_conv层处理两个张量时,x和t中包含的信息被组合在一起,输出被存储在名为convolved(#(5))的变量中。之后,我们现在实际上在第1行使用最大池化操作执行下采样(#(6)),产生一个名为maxpooled的张量。值得注意的是,convolved和maxpooled张量都将被返回,这基本上是因为我们稍后会把maxpooled带入下一个下采样阶段,而convolved张量将通过跳过连接直接传输到解码器中的上采样阶段。

现在,我们使用下面的Codeblock 8来测试该DownSample类。这里使用的输入张量与Codeblock 6中的完全相同。根据结果输出,我们可以看到池化操作成功地将DoubleConv块的输出从2×64×28×28(#(1))转换为2×64×14×14(#(2)),这表明我们的DownSample类正常工作。

复制

# Codeblock 8
down_sample_test = DownSample(in_channels=1, out_channels=64).to(DEVICE)

x_test = torch.randn((BATCH_SIZE, NUM_CHANNELS, IMAGE_SIZE, IMAGE_SIZE)).to(DEVICE)
t_test = torch.randint(0, NUM_TIMESTEPS, (BATCH_SIZE,)).to(DEVICE)

out_test = down_sample_test(x_test, t_test)
# Codeblock 8 Output
original : torch.Size([2, 1, 28, 28])
timesteps : torch.Size([2]), tensor([468, 304], device='cuda:0')

after double conv : torch.Size([2, 64, 28, 28]) #(1)
after pooling : torch.Size([2, 64, 14, 14]) #(2)1.2.3.4.5.6.7.8.9.10.11.12.13.

U-Net架构:解码器

我们需要在解码器中引入所谓的UpSample块,它负责将中间层中的张量恢复到原始图像维度。为了保持对称结构,UpSample块的数量必须与DownSample块的数量相匹配。观察下面的图7,就可以看到两个UpSample块的位置。

图7.蓝色框内的组件就是所谓的UpSample块【引文3】

由于两个UpSample块的结构相同,我们可以为它们初始化一个类,就像DownSample我们之前创建的类一样。请查看下面的Codeblock 9,了解我如何实现它。

复制

# Codeblock 9
class UpSample(nn.Module):
 def __init__(self, in_channels, out_channels):
 super().__init__()

 self.conv_transpose = nn.ConvTranspose2d(in_channels=in_channels, #(1)
 out_channels=out_channels, 
 kernel_size=2, stride=2) #(2)
 self.double_conv = DoubleConv(in_channels=in_channels, #(3)
 out_channels=out_channels)

 def forward(self, x, t, connection): #(4)
 print(f'original		: {x.size()}')
 print(f'timesteps		: {t.size()}, {t}')
 print(f'connection		: {connection.size()}')

 x = self.conv_transpose(x) #(5)
 print(f'
after conv transpose	: {x.size()}')

 x = torch.cat([x, connection], dim=1) #(6)
 print(f'after concat		: {x.size()}')

 x = self.double_conv(x, t) #(7)
 print(f'after double conv	: {x.size()}')

 return x1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.

在该__init__()方法中,我们使用nn.ConvTranspose2d对空间维度进行上采样(#(1))。内核大小和步长都设置为2,这样输出将是原来的两倍(#(2))。接下来,DoubleConv将使用块来减少通道数,同时结合来自时间嵌入张量的时间步长信息(#(3))。

这个UpSample类的流程比DownSample类稍微复杂一些。如果我们仔细看看这个架构,我们会发现我们还有一个直接来自编码器的跳过连接。因此,除了原始图像x和时间步长之外,我们还需要forward()方法接受另一个参数t,即残差张量connection(#(4))。我们在这个方法中做的第一件事是用转置卷积层处理原始图像(#(5))。事实上,这个层不仅对空间大小进行了上采样,而且还同时减少了通道数。然而,得到的张量随后以通道方式直接连接(#(6)),使其看起来好像没有执行任何通道减少。重要的是要知道,此时这两个张量只是连接在一起,这意味着来自两者的信息尚未结合。我们最终将这些连接的张量输入到double_conv层(#(7)),允许它们通过卷积层内的可学习参数相互共享信息。

下面的Codeblock 10展示了我如何测试该类UpSample。需要传入的张量的大小是根据第二个上采样块(即图7中最右边的蓝色框)设置的。

复制

# Codeblock 10
up_sample_test = UpSample(in_channels=128, out_channels=64).to(DEVICE)

x_test = torch.randn((BATCH_SIZE, 128, 14, 14)).to(DEVICE)
t_test = torch.randint(0, NUM_TIMESTEPS, (BATCH_SIZE,)).to(DEVICE)
connection_test = torch.randn((BATCH_SIZE, 64, 28, 28)).to(DEVICE)

out_test = up_sample_test(x_test, t_test, connection_test)1.2.3.4.5.6.7.8.

在下面的结果输出中,如果我们将输入张量(#(1))与最终张量形状(#(2))进行比较,我们可以清楚地看到通道数成功地从128减少到64,同时空间维度从14×14增加到28×28。这实际上意味着,我们的UpSample类现在可以在主U-Net架构中使用了。

复制

original : torch.Size([2, 128, 14, 14]) #(1)
timesteps : torch.Size([2]), tensor([468, 304], device='cuda:0')
connection : torch.Size([2, 64, 28, 28])

after conv transpose : torch.Size([2, 64, 28, 28])
after concat : torch.Size([2, 128, 28, 28])
after double conv : torch.Size([2, 64, 28, 28]) #(2)1.2.3.4.5.6.7.

U-Net架构:将所有组件整合在一起

一旦创建了所有U-Net组件,我们接下来要做的就是将它们包装成一个类。请参阅下面的代码块11a和11b了解详细信息。

复制

# Codeblock 11a
class UNet(nn.Module):
 def __init__(self):
 super().__init__()

 self.downsample_0 = DownSample(in_channels=NUM_CHANNELS, #(1)
 out_channels=64)
 self.downsample_1 = DownSample(in_channels=64, #(2)
 out_channels=128)

 self.bottleneck = DoubleConv(in_channels=128, #(3)
 out_channels=256)

 self.upsample_0 = UpSample(in_channels=256, #(4)
 out_channels=128)
 self.upsample_1 = UpSample(in_channels=128, #(5)
 out_channels=64)

 self.output = nn.Conv2d(in_channels=64, #(6)
 out_channels=NUM_CHANNELS,
 kernel_size=1)1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.

你可以在上面的__init__()方法中看到,我们初始化两个下采样(#(1-2))和两个上采样(#(4-5))块;其中,输入和输出通道的数量根据图中所示的架构设置。实际上,还有两个我还没有解释的额外组件,即瓶颈层bottleneck(#(3))和输出层output(#(6))。前者本质上只是一个DoubleConv块,它充当编码器和解码器之间的主要连接。查看下面的图8,以查看网络的哪些组件属于瓶颈层bottleneck。接下来,输出层是一个标准卷积层,负责将上一个UpSampling阶段生成的64通道图像转换为仅1通道。此操作使用大小为1×1的内核完成,这意味着,它结合所有通道的信息,同时在每个像素位置独立操作。

图8.瓶颈层(模型的下部)充当U-Net编码器和解码器之间的主要桥梁【引文3】

我想,以下代码块中的整个U-Net的forward()方法应该是非常简单的,因为我们在这里所做的基本上是将张量从一层传递到另一层——只是不要忘记在下采样和上采样块之间包含跳过连接。

复制

# Codeblock 11b
 def forward(self, x, t): #(1)
 print(f'original		: {x.size()}')
 print(f'timesteps		: {t.size()}, {t}')

 convolved_0, maxpooled_0 = self.downsample_0(x, t)
 print(f'
maxpooled_0		: {maxpooled_0.size()}')

 convolved_1, maxpooled_1 = self.downsample_1(maxpooled_0, t)
 print(f'maxpooled_1		: {maxpooled_1.size()}')

 x = self.bottleneck(maxpooled_1, t)
 print(f'after bottleneck	: {x.size()}')

 upsampled_0 = self.upsample_0(x, t, convolved_1)
 print(f'upsampled_0		: {upsampled_0.size()}')

 upsampled_1 = self.upsample_1(upsampled_0, t, convolved_0)
 print(f'upsampled_1		: {upsampled_1.size()}')

 x = self.output(upsampled_1)
 print(f'final output		: {x.size()}')

 return x1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.

现在,让我们通过运行以下测试代码来看看我们是否正确构建了上面的U-Net类。

复制

# Codeblock 12
unet_test = UNet().to(DEVICE)

x_test = torch.randn((BATCH_SIZE, NUM_CHANNELS, IMAGE_SIZE, IMAGE_SIZE)).to(DEVICE)
t_test = torch.randint(0, NUM_TIMESTEPS, (BATCH_SIZE,)).to(DEVICE)

out_test = unet_test(x_test, t_test)
# Codeblock 12 Output
original : torch.Size([2, 1, 28, 28]) #(1)
timesteps : torch.Size([2]), tensor([468, 304], device='cuda:0')

maxpooled_0 : torch.Size([2, 64, 14, 14]) #(2)
maxpooled_1 : torch.Size([2, 128, 7, 7]) #(3)
after bottleneck : torch.Size([2, 256, 7, 7]) #(4)
upsampled_0 : torch.Size([2, 128, 14, 14])
upsampled_1 : torch.Size([2, 64, 28, 28])
final output : torch.Size([2, 1, 28, 28]) #(5)1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.

在上面的输出中我们可以看到,两个下采样阶段成功地将原始大小为1×28×28(#(1))的张量分别转换为64×14×14(#(2))和128×7×7(#(3))。然后,该张量通过瓶颈层,导致其通道数扩展到256,而不会改变空间维度(#(4))。最后,我们对张量进行两次上采样,最终将通道数缩小到1(#(5))。根据这个输出,我们的模型似乎已经可以运行正常了。因此,它现在可以用于我们的扩散任务了。

数据集准备

由于我们已经成功创建了整个U-Net架构,接下来要做的就是准备MNIST手写数字数据集。在实际加载它之前,我们需要首先使用Torchvision中的transforms.Compose()方法定义预处理步骤,如Codeblock13中#(1)行所示。这里我们做两件事:将图像转换为PyTorch张量,这也会将像素值从0-255缩放到0-1(#(2)),并对其进行规范化,以便最终像素值介于-1和1之间(#(3))。

接下来,我们使用datasets.MNIST()下载数据集。在本例中,我们将从训练数据中获取图像,因此我们使用train=True(#(5))。不要忘记将我们之前初始化的变量transform传递给transform参数(transform=transform),以便它在加载图像时自动预处理图像(#(6))。最后,我们需要使用DataLoader加载来自mnist_dataset的图像(#(7))。注意,我用于输入参数的参数,为的是在每次迭代中从数据集中随机挑选BATCH_SIZE(2)张图像。

复制

# Codeblock 13
transform = transforms.Compose([ #(1)
 transforms.ToTensor(), #(2)
 transforms.Normalize((0.5,), (0.5,)) #(3)
])

mnist_dataset = datasets.MNIST( #(4)
 root='./data', 
 train=True, #(5)
 download=True, 
 transform=transform #(6)
)

loader = DataLoader(mnist_dataset, #(7)
 batch_size=BATCH_SIZE,
 drop_last=True, 
 shuffle=True)1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.

在下面的代码块中,我尝试从数据集中加载一批图像。在每次迭代中,loader都会提供图像和相应的标签,因此我们需要将它们存储在两个单独的变量中:images和labels。

复制

# Codeblock 14
images, labels = next(iter(loader))

print('images		:', images.shape)
print('labels		:', labels.shape)
print('min value	:', images.min())
print('max value	:', images.max())1.2.3.4.5.6.7.

我们可以在下面的结果输出中看到,images张量的大小为2×1×28×28(#(1)),表示已成功加载两张大小为28×28的灰度图像。在这里我们还可以看到labels张量的长度为2,与加载的图像数量相匹配(#(2))。请注意,在这种情况下,标签将被完全忽略。我的计划是,我只希望模型从整个训练数据集中生成它之前看到的任何数字,甚至不知道它实际上是什么数字。最后,此输出还显示预处理工作正常,因为像素值现在介于-1和1之间。

复制

# Codeblock 14 Output
images : torch.Size([2, 1, 28, 28]) #(1)
labels : torch.Size([2]) #(2)
min value : tensor(-1.)
max value : tensor(1.)
如果你想看看我们刚刚加载的图像是什么样子,请运行以下代码。
# Codeblock 15 
plt.imshow(images[0].squeeze(), cmap='gray')
plt.show()1.2.3.4.5.6.7.8.9.

图9.Codeblock 15的输出【引文3】

噪音调度器

在本节中,我们将讨论如何执行前向和后向扩散,该过程本质上涉及在每个时间步长上一点一点地添加或消除噪声。有必要知道,我们基本上希望在所有时间步长上噪声量均匀;其中,在前向扩散中,图像应该在时间步长1000时完全充满噪声,而在后向扩散中,我们必须在时间步长0时获得完全清晰的图像。因此,我们需要一些东西来控制每个时间步长的噪声量。在本节后面,我将实现一个名为NoiseScheduler的类来执行此操作。这可能是本文中数学知识涉及最多的部分,因为我将在这里展示许多方程式。但不要担心,因为我们将专注于实现这些方程式,而不是讨论数学推导。

现在,让我们看一下图10中的方程式,我将在下面的NoiseScheduler类的__init__()方法中实现它们。

图10.我们需要在类NoiseScheduler的__init__()方法中实现的方程【引文3】

复制

# Codeblock 16a
class NoiseScheduler:
 def __init__(self):
 self.betas = torch.linspace(BETA_START, BETA_END, NUM_TIMESTEPS) #(1)
 self.alphas = 1. - self.betas
 self.alphas_cum_prod = torch.cumprod(self.alphas, dim=0)
 self.sqrt_alphas_cum_prod = torch.sqrt(self.alphas_cum_prod)
 self.sqrt_one_minus_alphas_cum_prod = torch.sqrt(1. - self.alphas_cum_prod)1.2.3.4.5.6.7.8.

上述代码通过创建多个数字序列来工作,它们基本上都由BETA_START(0.0001)、BETA_END(0.02)和NUM_TIMESTEPS(1000)控制。我们需要实例化的第一个序列是betas本身,它是使用torch.linspace()完成的(#(1))。它本质上的作用是生成一个长度为1000的一维张量,从0.0001到0.02,其中此张量中的每个元素都对应一个时间步。每个元素之间的间隔是均匀的,这使我们能够在所有时间步长中生成均匀的噪声量。有了这个betas张量,我们然后根据图10中的四个方程计算alphas、alphas_cum_prod、sqrt_alphas_cum_prod和
sqrt_one_minus_alphas_cum_prod。稍后,这些张量将作为在扩散过程中如何产生或消除噪声的基础。

扩散通常以顺序方式进行。然而,前向扩散过程是确定性的,因此我们可以将原始方程推导为封闭形式,这样我们就可以在特定的时间步长中获得噪声,而不必从头开始迭代添加噪声。下图11显示了前向扩散的封闭形式,其中x0表示原始图像,而epsilon(ϵ)表示由随机高斯噪声组成的图像。我们可以将此方程视为加权组合,其中我们根据时间步长确定的权重将清晰图像和噪声组合在一起,从而得到具有特定噪声量的图像。

图11.正向扩散过程的封闭形式【引文3】

该方程的实现可以在Codeblock 16b中看到。在forward_diffusion()方法中,x0和ϵ表示为original和noise。这里你需要记住这两个输入变量是图像,而sqrt_alphas_cum_prod_t和
sqrt_one_minus_alphas_cum_prod_t是标量。因此,我们需要调整这两个标量的形状(代码行#(1)和#(2)),以便可以执行#(3)行中的操作。变量noisy_image将是此函数的输出,我想这个名字是不言自明的。

复制

# Codeblock 16b
 def forward_diffusion(self, original, noise, t):
 sqrt_alphas_cum_prod_t = self.sqrt_alphas_cum_prod[t]
 sqrt_alphas_cum_prod_t = sqrt_alphas_cum_prod_t.to(DEVICE).view(-1, 1, 1, 1) #(1)

 sqrt_one_minus_alphas_cum_prod_t = self.sqrt_one_minus_alphas_cum_prod[t]
 sqrt_one_minus_alphas_cum_prod_t = sqrt_one_minus_alphas_cum_prod_t.to(DEVICE).view(-1, 1, 1, 1) #(2)

 noisy_image = sqrt_alphas_cum_prod_t * original + sqrt_one_minus_alphas_cum_prod_t * noise #(3)

 return noisy_image1.2.3.4.5.6.7.8.9.10.11.

现在,我们来谈谈反向扩散。事实上,这个比正向扩散稍微复杂一些,因为我们需要三个方程。在我向你展示这些方程之前,让我先向你展示一下有关代码实现。请参阅下面的代码块16c。

复制

# Codeblock 16c
 def backward_diffusion(self, current_image, predicted_noise, t): #(1)
 denoised_image = (current_image - (self.sqrt_one_minus_alphas_cum_prod[t] * predicted_noise)) / self.sqrt_alphas_cum_prod[t] #(2)
 denoised_image = 2 * (denoised_image - denoised_image.min()) / (denoised_image.max() - denoised_image.min()) - 1 #(3)

 current_prediction = current_image - ((self.betas[t] * predicted_noise) / (self.sqrt_one_minus_alphas_cum_prod[t])) #(4)
 current_prediction = current_prediction / torch.sqrt(self.alphas[t]) #(5)

 if t == 0: #(6)
 return current_prediction, denoised_image

 else:
 variance = (1 - self.alphas_cum_prod[t-1]) / (1. - self.alphas_cum_prod[t]) #(7)
 variance = variance * self.betas[t] #(8)
 sigma = variance ** 0.5
 z = torch.randn(current_image.shape).to(DEVICE)
 current_prediction = current_prediction + sigma*z

 return current_prediction, denoised_image1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.

在推理阶段的后期,该backward_diffusion()方法将在一个循环中调用,该循环迭代NUM_TIMESTEPS(1000)次,从t=999开始,继续执行t=998,一直到t=0为止。此函数负责根据current_image(前一个去噪步骤生成的图像)、predicted_noise(U-Net在上一步中预测的噪声)和时间步长信息t三个参数迭代地从图像中去除噪声(#(1))。在每次迭代中,使用图12中所示的公式进行噪声消除,在代码块16c中,这对应于代码行#(4-5)。

图12.用于从图像中去除噪声的方程【引文3】

只要我们还没有达到t=0,我们就会根据图13中的方程计算方差(代码行#(7-8))。然后,该方差将用于引入另一个受控噪声,以模拟反向扩散过程中的随机性,因为图12中的噪声消除方程是确定性近似。这本质上也是我们在达到t=0(代码行#(6))后不再计算方差的原因,因为我们不再需要添加更多噪声,因为图像已经完全清晰了。

图13.用于计算引入受控噪声的方差的方程【引文3】

current_prediction与旨在估计前一个时间步长的图像(xt-1)不同,张量的目标denoised_image是重建原始图像(x0)。由于这些不同的目标,我们需要一个单独的方程来计算denoised_image,如下图14所示。方程本身的实现写在第#(2-3)行。

图14.重建原始图像的方程【引文3】

现在,让我们测试一下上面创建的NoiseScheduler类。在下面的代码块中,我实例化一个NoiseScheduler对象并打印出与其关联的属性,这些属性都是使用图10中的公式根据存储在betas属性中的值计算得出的。请记住,这些张量的实际长度是NUM_TIMESTEPS(1000),但在这里我只打印出前6个元素。

复制

# Codeblock 17
noise_scheduler = NoiseScheduler()

print(f'betas				: {noise_scheduler.betas[:6]}')
print(f'alphas				: {noise_scheduler.alphas[:6]}')
print(f'alphas_cum_prod			: {noise_scheduler.alphas_cum_prod[:6]}')
print(f'sqrt_alphas_cum_prod		: {noise_scheduler.sqrt_alphas_cum_prod[:6]}')
print(f'sqrt_one_minus_alphas_cum_prod	: {noise_scheduler.sqrt_one_minus_alphas_cum_prod[:6]}')
# Codeblock 17 Output
betas : tensor([1.0000e-04, 1.1992e-04, 1.3984e-04, 1.5976e-04, 1.7968e-04, 1.9960e-04])
alphas : tensor([0.9999, 0.9999, 0.9999, 0.9998, 0.9998, 0.9998])
alphas_cum_prod : tensor([0.9999, 0.9998, 0.9996, 0.9995, 0.9993, 0.9991])
sqrt_alphas_cum_prod : tensor([0.9999, 0.9999, 0.9998, 0.9997, 0.9997, 0.9996])
sqrt_one_minus_alphas_cum_prod : tensor([0.0100, 0.0148, 0.0190, 0.0228, 0.0264, 0.0300])1.2.3.4.5.6.7.8.9.10.11.12.13.14.

上面的输出表明我们的__init__()方法按预期工作。接下来,我们将测试forward_diffusion()方法。如果回看一下图16b,你将看到forward_diffusion()接受三个输入:原始图像、噪声图像和时间步长数。我们只需使用我们之前加载的MNIST数据集中的图像作为第一个输入(#(1)),并使用大小完全相同的随机高斯噪声作为第二个输入(#(2))。为此,可以运行下面的Codeblock 18来查看这两个图像是什么样子。

复制

# Codeblock 18
image = images[0] #(1)
noise = torch.randn_like(image) #(2)

plt.imshow(image.squeeze(), cmap='gray')
plt.show()
plt.imshow(noise.squeeze(), cmap='gray')
plt.show()1.2.3.4.5.6.7.8.

图15.用作原始图像(左)和噪声图像(右)的两幅图像(其中,左侧的图像与我之前在图9【引文3】中展示的图像相同)

由于我们已经准备好了图像和噪声,接下来我们需要做的就是将它们传递给forward_diffusion()方法。我实际上尝试多次运行下面的Codeblock19:t=50、100、150等等,直到t=300。你可以在图16中看到,随着参数的增加,图像变得不那么清晰。在这种情况下,当t设置为999时,图像将完全被噪声填充。

复制

# Codeblock 19
noisy_image_test = noise_scheduler.forward_diffusion(image.to(DEVICE), noise.to(DEVICE), t=50)

plt.imshow(noisy_image_test[0].squeeze().cpu(), cmap='gray')
plt.show()1.2.3.4.5.

图16. t=50、100、150等时刻正向扩散过程的结果,直到t=300【引文3】

不幸的是,我们无法测试该backward_diffusion()方法,因为此过程需要我们对U-Net模型进行训练。所以,我们现在就跳过这部分。我将向你展示如何在稍后的推理阶段实际使用此功能。

训练

由于U-Net模型、MNIST数据集和噪声调度程序已准备就绪,我们现在可以准备一个函数进行训练。在此之前,我在下面的Codeblock 20中实例化了模型和噪声调度程序。

复制

# Codeblock 20
model = UNet().to(DEVICE)
noise_scheduler = NoiseScheduler()1.2.3.

整个训练过程在Codeblock 21中显示的train()函数中实现。在执行任何操作之前,我们首先初始化优化器和损失函数,在本例中我们分别使用Adam和MSE(#(1-2))。我们基本上想要做的是训练模型,使其能够预测输入图像中包含的噪声,稍后,预测的噪声将用作后向扩散阶段去噪过程的基础。要实际训练模型,我们首先需要使用#(6)行处的代码执行前向扩散。此噪声化过程将使用#(4)行处生成的随机噪声在images量上完成(#(3))。接下来,我们为t(#(5))取0到NUM_TIMESTEPS(1000)之间的随机数,这主要是因为我们希望我们的模型能够看到不同噪声水平的图像,以此作为提高泛化能力的一种方法。生成噪声图像后,我们将它与所选的t(#(7))一起传递给U-Net模型。此处的输入对模型很有用,因为它表示图像中的当前噪声水平。最后,我们之前初始化的损失函数负责计算实际噪声与原始图像的预测噪声之间的差异(#(8))。因此,这次训练的目标基本上是使预测噪声尽可能与我们在第#(4)行生成的噪声相似。

复制

# Codeblock 21
def train():
 optimizer = Adam(model.parameters(), lr=LEARNING_RATE) #(1)
 loss_function = nn.MSELoss() #(2)
 losses = []

 for epoch in range(NUM_EPOCHS):
 print(f'Epoch no {epoch}')

 for images, _ in tqdm(loader):

 optimizer.zero_grad()

 images = images.float().to(DEVICE) #(3)
 noise = torch.randn_like(images) #(4)
 t = torch.randint(0, NUM_TIMESTEPS, (BATCH_SIZE,)) #(5)

 noisy_images = noise_scheduler.forward_diffusion(images, noise, t).to(DEVICE) #(6)
 predicted_noise = model(noisy_images, t) #(7)
 loss = loss_function(predicted_noise, noise) #(8)

 losses.append(loss.item())
 loss.backward()
 optimizer.step()

 return losses1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.

现在,让我们使用下面的代码块运行上述训练函数。坐下来放松一下,等待训练完成。就我而言,我使用了开启了Nvidia GPU P100的Kaggle Notebook,大约需要45分钟才能完成。

复制

# Codeblock 22
losses = train()1.2.

如果我们看一下损失图,似乎我们的模型学习得很好,因为价值通常会随着时间的推移而下降,早期阶段下降迅速,后期阶段趋势更稳定(但仍在下降)。所以,我认为我们可以在推理阶段的后期期待好的结果。

复制

# Codeblock 23
plt.plot(losses)1.2.

图17.损失值如何随着训练的进行而减少【引文3】

推理

此时,我们已经训练了模型,因此我们现在可以对其进行推理。查看下面的Codeblock 24以了解我如何实现该inference()函数。

复制

# Codeblock 24
def inference():

 denoised_images = [] #(1)

 with torch.no_grad(): #(2)
 current_prediction = torch.randn((64, NUM_CHANNELS, IMAGE_SIZE, IMAGE_SIZE)).to(DEVICE) #(3)

 for i in tqdm(reversed(range(NUM_TIMESTEPS))): #(4)
 predicted_noise = model(current_prediction, torch.as_tensor(i).unsqueeze(0)) #(5)
 current_prediction, denoised_image = noise_scheduler.backward_diffusion(current_prediction, predicted_noise, torch.as_tensor(i)) #(6)

 if i%100 == 0: #(7)
 denoised_images.append(denoised_image)

 return denoised_images1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.

在标有#(1)的行中,我初始化了一个空列表,该列表将用于每100个时间步存储一次去噪结果(#(7))。这将使我们能够稍后看到反向扩散的进行方式。实际的推理过程封装在torch.no_grad()(#(2))内。请记住,在扩散模型中,我们从完全随机的噪声中生成图像,我们假设这些图像最初是在t=999时。为了实现这一点,我们可以简单地使用torch.randn(),如#(3)行所示。在这里,我们初始化一个大小为64×1×28×28的张量,表示我们即将同时生成64张图像。接下来,我们编写一个for从999开始向后迭代到0的循环(#(4))。在这个循环中,我们将当前图像和时间步作为训练后的U-Net的输入,并让它预测噪声(#(5))。然后在#(6)行执行实际的反向扩散。在迭代结束时,我们应该得到类似于我们数据集中的新图像。现在,让我们在下面的代码块中调用该inference()函数。

复制

# Codeblock 25
denoised_images = inference()1.2.

推理完成后,我们现在可以看到生成的图像是什么样子。下面的Codeblock 26用于显示我们刚刚生成的前42张图像。

复制

# Codeblock 26
fig, axes = plt.subplots(ncols=7, nrows=6, figsize=(10, 8))

counter = 0

for i in range(6):
 for j in range(7):
 axes[i,j].imshow(denoised_images[-1][counter].squeeze().detach().cpu().numpy(), cmap='gray') #(1)
 axes[i,j].get_xaxis().set_visible(False)
 axes[i,j].get_yaxis().set_visible(False)
 counter += 1

plt.show()1.2.3.4.5.6.7.8.9.10.11.12.13.

图18.在MNIST手写数字数据集【引文3】上训练的扩散模型生成的图像

如果我们看一下上面的代码块,你会看到#(1)行的索引器[-1]表示我们只显示来自最后一次迭代(对应于时间步长0)的图像。这就是你在图18中看到的图像都没有噪音的原因。我承认这可能不是最好的结果,因为并非所有生成的图像都是有效的数字。——但是,这反而表明这些图像不仅仅是原始数据集的重复。

这里我们还可以使用下面的Codeblock 27可视化反向扩散过程。你可以在图19中的结果输出中看到,我们最初从完全随机的噪声开始,随着我们向右移动,噪声逐渐消失。

复制

# Codeblock 27
fig, axes = plt.subplots(ncols=10, figsize=(24, 8))

sample_no = 0
timestep_no = 0

for i in range(10):
 axes[i].imshow(denoised_images[timestep_no][sample_no].squeeze().detach().cpu().numpy(), cmap='gray')
 axes[i].get_xaxis().set_visible(False)
 axes[i].get_yaxis().set_visible(False)
 timestep_no += 1

plt.show()1.2.3.4.5.6.7.8.9.10.11.12.13.

图19.时间步900、800、700等…直到时间步0时图像的样子【引文3】

结论

你可以基于本文提供的线路,进一步进行很多方面的研究。首先,如果你想要更好的结果,你可能需要调整Codeblock 2中的参数配置。其次,除了我们在下采样和上采样阶段使用的卷积层堆栈之外,还可以通过实现注意层来改进U-Net模型。这样做并不能保证你获得更好的结果,尤其是对于像本文所使用的这样的简单数据集,但绝对值得一试。第三,如果你想挑战自己,你还可以尝试使用更复杂的数据集。

在实际应用中,扩散模型实际上可以做很多事情。最简单的一个可能是数据增强。使用扩散模型,我们可以轻松地从特定的数据分布中生成新图像。例如,假设我们正在进行一个图像分类项目,但类别中的图像数量不平衡。为了解决这个问题,我们可以从少数类别中取出图像并将它们输入到扩散模型中。通过这样做,我们可以要求训练后的扩散模型从该类别中生成任意数量的样本。

好了,以上就是有关扩散模型的理论和实现的全部内容。感谢阅读,希望你今天能学到一些新知识!

你可以通过此链接访问该项目中使用的代码。这里还有我之前关于自动编码器、变分自动编码器(VAE)、神经风格迁移(NST)和Transformer的文章的链接。

参考

【1】Jascha Sohl-Dickstein等人,《利用非平衡热力学进行深度无监督学习》,Arxiv

【2】Jonathan Ho等人,《去噪扩散概率模型》,Arxiv

【3】Olaf Ronneberger等人,《U-Net:用于生物医学图像分割的卷积网络》,Arxiv

【4】Yann LeCun等人,《MNIST手写数字数据库》,Creative Commons Attribution-Share Alike 3.0许可证。

【5】Ashish Vaswani等人,《注意力就是你所需要的一切》,Arxiv

译者介绍

朱先忠,51CTO社区编辑,51CTO专家博客、讲师,潍坊一所高校计算机教师,自由编程界老兵一枚。

原文标题:The Art of Noise,作者:Muhammad Ardi

展开阅读全文

更新时间:2025-04-30

标签:步长   噪声   张量   卷积   模型   时间   引文   方程   实战   图像   代码   动态   科技

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020- All Rights Reserved. Powered By bs178.com 闽ICP备11008920号
闽公网安备35020302034844号

Top