fast.ai 深度学习笔记(六)(1)

简介: fast.ai 深度学习笔记(六)

深度学习 2:第 2 部分第 12 课

原文:medium.com/@hiromi_suenaga/deep-learning-2-part-2-lesson-12-215dfbf04a94

译者:飞龙

协议:CC BY-NC-SA 4.0

来自 fast.ai 课程的个人笔记。随着我继续复习课程以“真正”理解它,这些笔记将继续更新和改进。非常感谢 JeremyRachel 给了我这个学习的机会。


生成对抗网络(GANs)

视频 / 论坛

非常炙手可热的技术,但绝对值得成为课程中前沿深度学习部分的一部分,因为它们并不完全被证明对任何事情都有用,但它们几乎到了那个地步,肯定会成功。我们将专注于那些在实践中肯定会有用的事情,有许多领域可能会被证明有用,但我们还不知道。所以我认为它们在实践中肯定会有用的领域是你在幻灯片左侧看到的那种东西 —— 例如将绘画转化为渲染图片。这来自于两天前刚刚发布的一篇论文,所以目前正在进行非常活跃的研究。

从上一堂课 [1:04]: 我们的多样性研究员 Christine Payne 拥有斯坦福大学的医学硕士学位,因此她对构建医学语言模型感兴趣。我们在第四课中简要提到过的一件事,但上次并没有详细讨论的是,你实际上可以种子一个生成式语言模型,这意味着你已经在某个语料库上训练了一个语言模型,然后你将从该语言模型生成一些文本。你可以通过输入一些词来开始,告诉它“这是用来创建语言模型中隐藏状态的前几个词,请从这里生成”。Christine 做了一些聪明的事情,她用一个问题作为种子,重复这个问题三次,然后让它从那里生成。她向语言模型输入了许多不同的医学文本,并输入了下面看到的问题:


Jeremy 发现这个有趣的地方是,对于没有医学硕士学位的人来说,这几乎是一个可信的答案。但它与现实完全没有关系。他认为这是一种有趣的伦理和用户体验困境。Jeremy 参与了一个名为 doc.ai 的公司,该公司试图做很多事情,但最终提供一个应用程序供医生和患者使用,可以帮助他们解决医疗问题。他一直在对团队中的软件工程师说,请不要尝试使用 LSTM 或其他东西创建生成模型,因为它们会擅长创造听起来令人印象深刻但实际上是错误建议的东西 —— 就像政治评论员或终身教授可以以极大的权威说废话一样。所以他认为这是一个非常有趣的实验。如果你做了一些有趣的实验,请在论坛、博客、Twitter 上分享。让人们知道并受到了了不起的人的关注。

CIFAR10 [5:26]

让我们谈谈 CIFAR10,原因是今天我们将看一些更基础的 PyTorch 内容,以构建这些生成对抗模型。目前没有关于 GAN 的 fastai 支持 - 很快就会有,但目前还没有,所以我们将从头开始构建许多模型。我们已经有一段时间没有进行严肃的模型构建了。在课程的第一部分中,我们看了 CIFAR10,并构建了一个准确率约为 85%的模型,训练时间约为几个小时。有趣的是,现在正在进行一项竞赛,看看谁能最快地训练 CIFAR10(DAWN),目标是将准确率提高到 94%。看看我们是否能构建一个能够达到 94%准确率的架构,因为这比我们之前的尝试要好得多。希望通过这样做,我们将学到一些关于创建良好架构的东西,这对于今天研究 GANs 将会很有用。此外,这也很有用,因为 Jeremy 在过去几年深入研究了关于不同类型 CNN 架构的论文,并意识到这些论文中的许多见解并没有被广泛利用,显然也没有被广泛理解。因此,他想向您展示如果我们能利用其中一些理解会发生什么。

cifar10-darknet.ipynb [7:17]

这个笔记本被称为darknet,因为我们将要查看的特定架构与 darknet 架构非常接近。但在这个过程中,您会发现 darknet 架构并不是整个 YOLO v3 端到端的东西,而只是他们在 ImageNet 上预训练用于分类的部分。这几乎就像您可以想到的最通用的简单架构,因此它是实验的一个很好的起点。因此,我们将其称为“darknet”,但它并不完全是那样,您可以对其进行调整以创建绝对不是 darknet 的东西。它实际上只是几乎任何现代基于 ResNet 的架构的基础。

CIFAR10 是一个相当小的数据集[8:06]。图像大小仅为 32x32,这是一个很好的数据集,因为:

  • 与 ImageNet 不同,您可以相对快速地对其进行训练
  • 相对较少的数据
  • 实际上很难识别这些图像,因为 32x32 太小了,很难看清楚发生了什么。

这是一个被低估的数据集,因为它很老。谁愿意使用小而古老的数据集,当他们可以利用整个服务器房间来处理更大的数据时。但这是一个非常好的数据集,值得关注。

继续导入我们通常使用的东西,我们将尝试从头开始构建一个网络来训练这个[8:58]。

%matplotlib inline
%reload_ext autoreload
%autoreload 2
from fastai.conv_learner import *
PATH = Path("data/cifar10/")
os.makedirs(PATH,exist_ok=True)

对于那些对广播和 PyTorch 基本技能不是 100%自信的人来说,一个非常好的练习是弄清楚 Jeremy 是如何得出这些stats数字的。这些数字是 CIFAR10 中每个通道的平均值和标准差。尝试确保您可以重新创建这些数字,并查看是否可以在不超过几行代码的情况下完成(不使用循环!)。

由于这些数据相当小,我们可以使用比通常更大的批量大小,并且这些图像的大小为 32[9:46]。

classes = (
    'plane', 'car', 'bird', 
    'cat', 'deer', 'dog', 'frog', 
    'horse', 'ship', 'truck'
)
stats = (
    np.array([ 0.4914 ,  0.48216,  0.44653]), 
    np.array([ 0.24703,  0.24349,  0.26159])
)
num_workers = num_cpus()//2
bs=256
sz=32

变换[9:57],通常我们有这一套标准的 side_on 变换,用于普通物体的照片。我们不会在这里使用这个,因为这些图像太小了,尝试将一个 32x32 的图像稍微旋转会引入很多块状失真。人们倾向于使用的标准变换是随机水平翻转,然后我们在每一侧添加 4 个像素(尺寸除以 8)的填充。一个非常有效的方法是,默认情况下 fastai 不会添加黑色填充,而许多其他库会这样做。Fastai 会取现有照片的最后 4 个像素,翻转并反射它,我们发现使用反射填充会得到更好的结果。现在我们有了 40x40 的图像,在训练中,这组变换将随机选择 32x32 的裁剪,所以我们会有一点变化但不会太多。因此我们可以使用正常的from_paths来获取我们的数据。

tfms = tfms_from_stats(
    stats, sz, 
    aug_tfms=[RandomFlip()], 
    pad=sz//8
)
data = ImageClassifierData.from_paths(
    PATH, 
    val_name='test', 
    tfms=tfms, 
    bs=bs
)

现在我们需要一个架构,我们将创建一个适合在一个屏幕上显示的架构[11:07]。这是从头开始的。我们正在使用预定义的Conv2dBatchNorm2dLeakyReLU模块,但我们没有使用任何块或其他东西。整个东西都在一个屏幕上,所以如果你曾经想知道我是否能理解一个现代的高质量架构,绝对可以!让我们来学习这个。

def conv_layer(ni, nf, ks=3, stride=1):
    return nn.Sequential(
        nn.Conv2d(
            ni, nf, 
            kernel_size=ks, 
            bias=False, 
            stride=stride,
            padding=ks//2
        ),
        nn.BatchNorm2d(nf, momentum=0.01),
        nn.LeakyReLU(negative_slope=0.1, inplace=True)
    )
class ResLayer(nn.Module):
    def __init__(self, ni):
        super().__init__()
        self.conv1=conv_layer(ni, ni//2, ks=1)
        self.conv2=conv_layer(ni//2, ni, ks=3)
    def forward(self, x): 
        return x.add_(self.conv2(self.conv1(x)))
class Darknet(nn.Module):
    def make_group_layer(self, ch_in, num_blocks, stride=1):
        return [conv_layer(ch_in, ch_in*2,stride=stride)] + \
               [(ResLayer(ch_in*2)) for i in range(num_blocks)]
    def __init__(self, num_blocks, num_classes, nf=32):
        super().__init__()
        layers = [conv_layer(3, nf, ks=3, stride=1)]
        for i,nb in enumerate(num_blocks):
            layers += self.make_group_layer(nf, nb, stride=2-(i==1))
            nf *= 2
        layers += [
            nn.AdaptiveAvgPool2d(1), 
            Flatten(), 
            nn.Linear(nf, num_classes)
        ]
        self.layers = nn.Sequential(*layers)
    def forward(self, x): 
        return self.layers(x)

架构的基本起点是说它是一堆堆叠的层,一般来说会有一种层次结构[11:51]。在最底层,有像卷积层和批量归一化层这样的东西,但任何时候你有一个卷积,你可能会有一些标准的顺序。通常会是:

  1. 卷积
  2. 批量归一化
  3. 一个非线性激活(例如 ReLU)

我们将从确定我们的基本单元是什么开始,并在一个函数(conv_layer)中定义它,这样我们就不必担心保持一致性,这将使一切变得更简单。

Leaky Relu [12:43]:


Leaky ReLU 的梯度(其中x < 0)会有所变化,但通常是 0.1 或 0.01 左右。其背后的想法是,当你处于负区域时,你不会得到一个零梯度,这会使更新变得非常困难。实践中,人们发现 Leaky ReLU 在较小的数据集上更有用,在大数据集上不太有用。但有趣的是,在YOLO v3论文中,他们使用了 Leaky ReLU,并从中获得了很好的性能。它很少会使事情变得更糟,通常会使事情变得更好。所以如果你需要创建自己的架构,Leaky ReLU 可能不错作为默认选择。

你会注意到我们在conv_layer中没有定义 PyTorch 模块,我们只是使用nn.Sequential[14:07]。如果你阅读其他人的 PyTorch 代码,你会发现这是一个被低估的东西。人们倾向于将一切都写成 PyTorch 模块,带有__init__forward,但如果你想要的只是一系列按顺序排列的东西,将其作为Sequential会更简洁易懂。

残差块 [14:40]:如前所述,大多数现代网络中通常有多个层次的单元,我们现在知道 ResNet 中这个单元层次结构的下一个级别是 ResBlock 或残差块(参见ResLayer)。回顾我们上次做 CIFAR10 时,我们过于简化了(有点作弊)。我们将x输入,经过一个conv,然后将其加回到x中输出。在真正的 ResBlock 中,有两个这样的块。当我们说conv时,我们将其作为conv_layer的快捷方式(卷积,批量归一化,ReLU)。


这里有一个有趣的观点是这些卷积中的通道数量。我们有一些 ni 进来(一些输入通道/滤波器的数量)。Darknet 团队设置的方式是,他们让每一个这些 Res 层输出与进来的相同数量的通道,Jeremy 喜欢这样做,这就是为什么他在ResLayer中使用它,因为这样会让生活更简单。第一个卷积将通道数量减半,然后第二个卷积再将其加倍。所以你有一个漏斗效应,64 个通道进来,通过第一个卷积压缩到 32 个通道,然后再次提升到 64 个通道输出。

问题:为什么LeakyReLU中要使用inplace=True?谢谢你的提问!很多人忘记了这一点或者不知道这一点,但这是一个非常重要的内存技巧。如果你想一下,这个conv_layer,它是最底层的东西,所以基本上我们的 ResNet 一旦全部组装起来,就会有很多conv_layer。如果你没有inplace=True,它会为 ReLU 的输出创建一个完全独立的内存块,这样就会分配一大堆完全不必要的内存。另一个例子是ResLayer中的原始forward看起来像这样:

def forward(self, x): 
    return x + self.conv2(self.conv1(x))

希望你们中的一些人记得在 PyTorch 中几乎每个函数都有一个下划线后缀版本,告诉它在原地执行。+等同于addadd的原地版本是add_,这样可以减少内存使用量:

def forward(self, x): 
    return x.add_(self.conv2(self.conv1(x)))

这些都是非常方便的小技巧。Jeremy 一开始忘记了inplace=True,但他不得不将批量大小降低到非常低的数量,这让他发疯了——然后他意识到那个部分缺失了。如果你使用了 dropout,你也可以这样做。以下是需要注意的事项:

  • Dropout
  • 所有激活函数
  • 任何算术操作

问题:在 ResNet 中,为什么conv_layer中的偏置通常设置为 False?在Conv之后,紧接着是BatchNorm。记住,BatchNorm对于每个激活有 2 个可学习参数——你要乘以的东西和你要添加的东西。如果我们在Conv中有偏置,然后在BatchNorm中再添加另一件事,那就是在添加两件事,这完全没有意义——这是两个权重,一个就够了。所以如果在Conv之后有一个BatchNorm,你可以告诉BatchNorm不要包括添加部分,或者更简单的方法是告诉Conv不要包括偏置。这没有特别的危害,但是会占用更多内存,因为它需要跟踪更多的梯度,所以最好避免。

另一个小技巧是,大多数人的conv_layer都有填充作为参数。但一般来说,你应该能够很容易地计算填充。如果卷积核大小为 3,那么显然每边会有一个单位的重叠,所以我们需要填充 1。或者,如果卷积核大小为 1,那么我们就不需要任何填充。所以一般来说,卷积核大小“整数除以 2”就是你需要的填充。有时会有一些调整,但在这种情况下,这个方法非常有效。再次尝试简化我的代码,让计算机为我计算东西,而不是我自己去做。


另一个关于这两个conv_layer的事情:我们有这个瓶颈的想法(减少通道然后再增加),还有要使用的卷积核大小。第一个有 1 乘 1 的Conv。1 乘 1 卷积实际上发生了什么?如果我们有一个 4 乘 4 的网格,有 32 个滤波器/通道,我们将进行 1 乘 1 卷积,卷积的核看起来像中间的那个。当我们谈论卷积核大小时,我们从来没有提到最后一部分——但假设它是 1 乘 1 乘 32,因为这是输入和输出的滤波器的一部分。卷积核被放在黄色的第一个单元上,我们得到这 32 个深度位的点积,这给了我们第一个输出。然后我们将其移动到第二个单元并得到第二个输出。所以对于网格中的每个点,都会有一堆点积。这使我们能够以任何我们想要的方式改变通道维度。我们创建了ni//2个滤波器,我们将有ni//2个点积,基本上是输入通道的不同加权平均值。通过非常少的计算,它让我们添加了这个额外的计算和非线性步骤。这是一个很酷的技巧,利用这些 1 乘 1 卷积,创建这个瓶颈,然后再用 3 乘 3 卷积拉出来——这将充分利用输入的 2D 特性。否则,1 乘 1 卷积根本不利用这一点。


这两行代码,里面没有太多内容,但这是一个对你对正在发生的事情的理解和直觉的很好的测试——为什么它有效?为什么张量秩是对齐的?为什么维度都很好地对齐?为什么这是一个好主意?它到底在做什么?这是一个很好的东西来调整。也许在 Jupyter Notebook 中创建一些小的实例,自己运行一下,看看输入和输出是什么。真正感受一下。一旦你这样做了,你就可以尝试不同的东西。

这篇真正被低估的论文是这篇Wide Residual Networks。这篇论文非常简单,但他们做的是围绕这两行代码进行调整:

  • 如果我们用ni*2代替ni//2会怎样?
  • 如果我们添加conv3呢?

他们提出了一种简单的符号表示来定义这两行代码可能的样子,并展示了许多实验。他们展示的是,在 ResNet 中普遍采用的减少通道数量的瓶颈方法可能不是一个好主意。实际上,根据实验结果,绝对不是一个好主意。因为这样可以创建非常深的网络。创建 ResNet 的人因为创建了 1001 层网络而变得特别有名。但是 1001 层的问题在于,你无法在完成第 1 层之前计算第 2 层。你无法在完成计算第 2 层之前计算第 3 层。所以是顺序的。GPU 不喜欢顺序。所以他们展示的是,如果层数较少但每层计算量更大——一个简单的方法是去掉//2,没有其他改变:


在家里试试吧。尝试运行 CIFAR 看看会发生什么。甚至乘以 2 或者摆弄一下。这样可以让你的 GPU 做更多的工作,这非常有趣,因为绝大多数关于不同架构性能的论文实际上从来没有计算运行一个批次需要多长时间。他们说“这个需要每批次 X 个浮点运算”,但他们从来没有真正费心像一个合格的实验者那样运行它,找出它是快还是慢。现在很有名的很多架构结果都很慢,占用大量内存,完全没用,因为研究人员从来没有费心看看它们是否快,实际上看看它们是否适合正常批次大小的内存。所以 Wide ResNet 论文之所以不同在于它实际上计算了运行所需的时间,YOLO v3 论文也做了同样的发现。他们可能错过了 Wide ResNet 论文,因为 YOLO v3 论文得出了很多相同的结论,但 Jeremy 不确定他们是否引用了 Wide ResNet 论文,所以他们可能不知道所有这些工作已经完成。看到人们实际上在计时并注意到什么是有意义的是很好的。

问题:你对 SELU(缩放指数线性单元)有什么看法?[29:44] SELU 主要用于全连接层,它允许你摆脱批量归一化,基本思想是,如果你使用这种不同的激活函数,它是自归一化的。自归一化意味着它将始终保持单位标准差和零均值,因此你不需要批量归一化。它实际上并没有取得什么进展,原因是因为它非常挑剔 — 你必须使用非常特定的初始化,否则它就不会以完全正确的标准差和均值开始。很难将其用于诸如嵌入之类的东西,如果你这样做,那么你必须使用一种特定类型的嵌入初始化,这对嵌入来说是没有意义的。你做了所有这些工作,很难搞对,最终如果你搞对了,有什么意义呢?好吧,你成功摆脱了一些并没有真正伤害你的批量归一化层。有趣的是 SELU 论文 — 人们注意到它的主要原因是因为它是由 LSTM 的发明者创建的,而且它有一个巨大的数学附录。所以人们认为“一个名人的大量数学 — 必定很棒!”但实际上,Jeremy 没有看到任何人使用它来获得任何最先进的结果或赢得任何比赛。

Darknet.make_group_layer包含一堆ResLayer[31:28]。group_layer将会有一些通道/滤波器进入。我们将通过使用标准的conv_layer来使进入的通道数量加倍。可选地,我们将通过使用步幅为 2 来减半网格大小。然后我们将做一系列的 ResLayers — 我们可以选择多少个(2、3、8 等),因为记住 ResLayers 不会改变网格大小,也不会改变通道数量,所以你可以添加任意数量而不会造成任何问题。这将使用更多的计算和内存,但除此之外你可以添加任意数量。因此,group_layer最终将使通道数量加倍,因为初始卷积使通道数量加倍,取决于我们传入的stride,如果我们设置stride=2,它也可能减半网格大小。然后我们可以做一系列 Res 块的计算,任意数量。

定义我们的Darknet,我们将传入类似这样的东西[33:13]:

m = Darknet([1, 2, 4, 6, 3], num_classes=10, nf=32)
m = nn.DataParallel(m, [1,2,3])

这意味着创建五个组层:第一个将包含 1 个额外的 ResLayer,第二个将包含 2 个,然后是 4 个,6 个,3 个,我们希望从 32 个滤波器开始。第一个 ResLayers 将包含 32 个滤波器,只会有一个额外的 ResLayer。第二个将会使滤波器数量翻倍,因为每次有一个新的组层时我们都会这样做。所以第二个将有 64 个,然后 128 个,256 个,512 个,就这样。几乎整个网络将由这些层组成,记住,每个组层在开始时也有一个卷积。所以在这之前,我们将在一开始有一个卷积层,在最后我们将执行标准的自适应平均池化,展平,并在最后创建一个线性层来生成最终的类别数量。总结一下,一个端有一个卷积,自适应池化和另一个端有一个线性层,中间是这些组层,每个组层由一个卷积层和n个 ResLayers 组成。

自适应平均池化:Jeremy 多次提到过这个,但他还没有看到任何代码,任何示例,任何地方使用自适应平均池化。他看到的每一个都像nn.AvgPool2d(n)这样写,其中n是一个特定的数字-这意味着它现在与特定的图像大小绑定在一起,这绝对不是您想要的。所以大多数人仍然认为特定的架构与特定的大小绑定在一起。当人们认为这样时,这是一个巨大的问题,因为这会严重限制他们使用更小的尺寸来启动建模或使用更小的尺寸进行实验的能力。

Sequential:创建架构的一个好方法是首先创建一个列表,在这种情况下,这是一个只有一个conv_layer的列表,然后make_group_layer返回另一个列表。然后我们可以用+=将该列表附加到前一个列表中,并对包含AdaptiveAvnPool2d的另一个列表执行相同操作。最后,我们将调用所有这些层的nn.Sequential。现在forward只是self.layers(x)


这是如何使您的架构尽可能简单的好方法。有很多可以摆弄的地方。您可以将ni的除数参数化,使其成为您传入的数字,以传入不同的数字-也许是乘以 2。您还可以传入一些可以改变内核大小或改变卷积层数量的参数。Jeremy 有一个版本,他将为您运行,其中实现了 Wide ResNet 论文中的所有不同参数,因此他可以摆弄看看哪些效果好。


lr = 1.3
learn = ConvLearner.from_model_data(m, data)
learn.crit = nn.CrossEntropyLoss()
learn.metrics = [accuracy]
wd=1e-4
%time learn.fit(
    lr, 1, 
    wds=wd, 
    cycle_len=30, 
    use_clr_beta=(20, 20, 0.95, 0.85)
)

一旦我们有了这个,我们可以使用ConvLearner.from_model_data来获取我们的 PyTorch 模块和模型数据对象,并将它们转换为一个学习器。给它一个标准,如果我们喜欢,可以添加一个指标,然后我们可以拟合并开始。

问题:您能解释一下自适应平均池化吗?将其设置为 1 是如何工作的?当我们进行平均池化时,通常情况下,假设我们有 4x4,然后进行avgpool((2, 2))。这将创建一个 2x2 的区域(下方的蓝色),并取这四个的平均值。如果我们传入stride=1,下一个是 2x2(绿色),然后取平均值。这就是正常的 2x2 平均池化。如果我们没有填充,那么输出将是 3x3。如果我们想要 4x4,我们可以添加填充。


如果我们想要 1x1 呢?那么我们可以说avgpool((4,4), stride=1),这将在黄色中进行 4x4 并对整体进行平均,结果为 1x1。但这只是一种方法。与其说池化滤波器的大小,为什么不说“我不在乎输入网格的大小。我总是想要一个一个”。这就是你说adap_avgpool(1)的地方。在这种情况下,你不说池化滤波器的大小,而是说我们想要的输出大小。我们想要的是一个一个。如果你放一个单独的整数n,它会假设你的意思是n乘以n。在这种情况下,一个 4x4 网格的自适应平均池化与平均池化(4,4)相同。如果是一个 7x7 的网格进来,它将与平均池化(7,7)相同。这是相同的操作,只是以一种方式表达,无论输入是什么,我们都希望得到那个大小的输出。

DAWNBench:让我们看看我们的简单网络与这些最新技术结果相比如何。Jeremy 已经准备好命令了。我们已经将所有这些东西放入一个简单的 Python 脚本中,他修改了一些他提到的参数,创建了一个他称之为wrn_22网络,它并不存在,但根据 Jeremy 的实验,它对我们讨论的参数进行了一些改变。它有一堆很酷的东西,比如:

  • 莱斯利·史密斯的一个周期
  • 半精度浮点实现


这将在 AWS p3 上运行,它有 8 个 GPU 和 Volta 架构的 GPU,这些 GPU 对半精度浮点有特殊支持。Fastai 是第一个实际将 Volta 优化的半精度浮点集成到库中的库,所以你只需learn.half()就可以自动获得支持。它也是第一个集成一个周期的库。

实际上,这是使用 PyTorch 的多 GPU 支持。由于有八个 GPU,它实际上会启动八个单独的 Python 处理器,每个处理器都会训练一点,然后最后将梯度更新传回主进程,主进程将把它们全部整合在一起。所以你会看到很多进度条一起弹出。

你可以看到这种方式训练三到四秒。而在之前,当 Jeremy 早些时候训练时,他每个时代要花 30 秒。所以用这种方式,我们可以训练东西大约快 10 倍,这很酷。

检查状态

完成了!我们达到了 94%,用时 3 分 11 秒。之前的最新技术是 1 小时 7 分钟。折腾这些参数,学习这些架构实际上是如何工作的,而不仅仅是使用开箱即用的东西,值得吗?哇哦。我们刚刚使用了一个公开可用的实例(我们使用了一个 spot 实例,所以花费了我们每小时 8 美元——3 分钟 40 美分)来从头开始训练,比以往任何人都要快 20 倍。所以这是最疯狂的最新技术结果之一。我们看到了很多,但这个结果真的让人大吃一惊。这在很大程度上要归功于调整这些架构参数,主要是关于使用莱斯利·史密斯的一个周期。提醒一下它在做什么,对于学习率,它创建了一个向上的路径,与向下的路径一样长,所以它是真正的三角形循环学习率(CLR)。像往常一样,你可以选择 x 和 y 的比例(即起始 LR/峰值 LR)。在


在这种情况下,我们选择了 50 作为比率。所以我们从更小的学习率开始。然后它有一个很酷的想法,你可以说你的 epochs 的百分之几是从三角形底部一直下降到几乎为零 - 这是第二个数字。所以 15%的批次花在从我们的三角形底部进一步下降。


这不是一个周期所做的唯一事情,我们还有动量。动量从 0.95 到 0.85。换句话说,当学习率很低时,我们使用很大的动量,当学习率很高时,我们使用很少的动量,这很有道理,但在 Leslie Smith 在论文中展示之前,Jeremy 从未见过有人这样做。这是一个非常酷的技巧。你现在可以通过在 fastai 中使用use-clr-beta参数来使用它(Sylvain 的论坛帖子),你应该能够复制最先进的结果。你可以在自己的计算机上或者 paper space 上使用它,唯一得不到的是多 GPU 部分,但这样训练会更容易一些。


问题make_group_layer包含步幅等于 2,这意味着第一层的步幅为 1,其他所有层的步幅为 2。背后的逻辑是什么?通常我见过的步幅是奇数。步幅要么是 1,要么是 2。我认为你在考虑卷积核大小。所以步幅=2 意味着我跨越两个,这意味着你的网格大小减半。所以我认为你可能在步幅和卷积核大小之间混淆了。如果步幅为 1,网格大小不会改变。如果步幅为 2,那么会改变。在这种情况下,因为这是 CIFAR10,32x32 很小,我们不会经常减半网格大小,因为很快我们就会用完单元格。这就是为什么第一层的步幅为 1,这样我们不会立即减小网格大小。这是一种很好的做法,因为这就是为什么我们一开始在大网格上没有太多计算Darknet([1, 2, 4, 6, 3], …)。我们可以从大网格上开始,然后随着网格变小,逐渐增加更多的计算,因为网格越小,计算所需的时间就越少。

生成对抗网络(GAN)[48:49]

我们将讨论生成对抗网络,也称为 GAN,具体来说,我们将专注于沃瑟斯坦 GAN 论文,其中包括后来创建 PyTorch 的 Soumith Chintala。沃瑟斯坦 GAN(WGAN)受到了深度卷积生成对抗网络论文的重大影响,Soumith 也参与其中。这是一篇非常有趣的论文。很多内容看起来像这样:


好消息是你可以跳过那些部分,因为还有一个看起来像这样的部分:


很多论文都有一个理论部分,似乎完全是为了满足审稿人对理论的需求。但 WGAN 论文并非如此。理论部分实际上很有趣 - 你不需要了解它就能使用它,但如果你想了解一些很酷的想法,并看到为什么选择这种特定算法的思考过程,那绝对是迷人的。在这篇论文出来之前,Jeremy 不认识任何研究其基础数学的人,所以每个人都必须学习这些数学知识。这篇论文做了很好的工作,列出了所有的要点(你需要自己阅读一些内容)。所以如果你对深入研究某篇论文背后更深层次的数学感兴趣,想看看学习它是什么感觉,我会选择这篇,因为在那个理论部分结束时,你会说“我现在明白他们为什么要设计这种算法了。”

GAN 的基本思想是它是一个生成模型[51:23]。它将创建句子、创建图像或生成一些东西。它将尝试创建一些很难区分生成的东西和真实的东西的东西。因此,生成模型可以用于换脸视频——目前发生的深度伪造和虚假色情非常有争议。它可以用来伪造某人的声音。它可以用来伪造对医学问题的回答——但在这种情况下,它并不是真正的伪造,它可以是对医学问题的生成回答,实际上是一个好的回答,因此你在生成语言。例如,你可以为图像生成标题。因此,生成模型有许多有趣的应用。但一般来说,它们需要足够好,例如,如果你要用它自动为凯丽·费舍尔在下一部星球大战电影中的新场景而她已经不在了,你想尝试生成一个看起来一样的图像,那么它必须欺骗星球大战的观众,让他们认为“好吧,那看起来不像奇怪的凯丽·费舍尔——那看起来像真正的凯丽·费舍尔。或者如果你试图生成对医学问题的回答,你希望生成的英语读起来流畅清晰,并且听起来有权威和意义。生成对抗网络的思想是我们不仅要创建一个生成模型来创建生成的图像,还要创建一个第二个模型,它将尝试挑选哪些是真实的,哪些是生成的(我们将称之为“假的”)。因此,我们有一个生成器,它将创建我们的虚假内容,还有一个鉴别器,它将努力变得擅长识别哪些是真实的,哪些是假的。因此,将有两个模型,它们将是对抗性的,意味着生成器将努力不断提高欺骗鉴别器认为假的是真实的能力,而鉴别器将努力不断提高区分真实和虚假的能力。因此,它们将正面交锋。这基本上就像 Jeremy 刚刚描述的那样[54:14]:

  • 我们将在 PyTorch 中构建两个模型
  • 我们将创建一个训练循环,首先说鉴别器的损失函数是“你能分辨真实和虚假吗,然后更新那个的权重。
  • 我们将为生成器创建一个损失函数,即“你能生成一些能欺骗鉴别器的东西并从中更新权重。
  • 然后我们将循环几次并看看会发生什么。

查看代码[54:52]

笔记本

GAN 有很多不同的用途。我们将做一些有点无聊但易于理解的事情,而且甚至可能的是我们将从无中生成一些图片。我们只是让它画一些图片。具体来说,我们将让它画卧室的图片。希望你有机会在这一周内使用自己的数据集玩耍。如果你选择一个非常多样化的数据集,比如 ImageNet,然后让 GAN 尝试创建 ImageNet 的图片,它往往做得不太好,因为你想要的图片不够清晰。所以最好给它,例如,有一个名为CelebA的数据集,其中包含名人的脸部图片,这对 GAN 非常有效。你可以生成真实但实际上不存在的名人脸。卧室数据集也是一个不错的选择——同一种类型的图片。

有一个叫做 LSUN 场景分类数据集的东西。

from fastai.conv_learner import *
from fastai.dataset import *
import gzip

下载 LSUN 场景分类数据集卧室类别,解压缩它,并将其转换为 jpg 文件(脚本文件夹在dl2文件夹中):

curl 'http://lsun.cs.princeton.edu/htbin/download.cgi?tag=latest&category=bedroom&set=train' -o bedroom.zip
unzip bedroom.zip
pip install lmdb
python lsun-data.py {PATH}/bedroom_train_lmdb --out_dir {PATH}/bedroom

这在 Windows 上没有经过测试 - 如果不起作用,您可以使用 Linux 框来转换文件,然后复制它们。或者,您可以从 Kaggle 数据集中下载这个 20%的样本。

PATH = Path('data/lsun/')
IMG_PATH = PATH/'bedroom'
CSV_PATH = PATH/'files.csv'
TMP_PATH = PATH/'tmp'
TMP_PATH.mkdir(exist_ok=True)

在处理我们的数据时,通过 CSV 路线会更容易。因此,我们生成一个包含我们想要的文件列表和一个虚假标签“0”的 CSV,因为我们实际上根本没有这些标签。一个 CSV 文件包含卧室数据集中的所有内容,另一个包含随机的 10%。这样做很好,因为这样我们在实验时大多数时间可以使用样本,因为即使只是读取列表也需要很长时间,因为有超过一百万个文件。

files = PATH.glob('bedroom/**/*.jpg')
with CSV_PATH.open('w') as fo:
    for f in files: 
        fo.write(f'{f.relative_to(IMG_PATH)},0\n')
        # Optional - sampling a subset of files
CSV_PATH = PATH/'files_sample.csv'
files = PATH.glob('bedroom/**/*.jpg')
with CSV_PATH.open('w') as fo:
    for f in files:
        if random.random()<0.1: 
            fo.write(f'{f.relative_to(IMG_PATH)},0\n')

这看起来非常熟悉。这是在 Jeremy 意识到顺序模型更好之前。因此,如果将这与以前的顺序模型的卷积块进行比较,这里有更多的代码行数——但它做的事情是一样的,卷积,ReLU,批量归一化。

class ConvBlock(nn.Module):
    def __init__(self, ni, no, ks, stride, bn=True, pad=None):
        super().__init__()
        if pad is None: 
            pad = ks//2//stride
        self.conv = nn.Conv2d(
            ni, no, 
            ks, stride, 
            padding=pad, 
            bias=False
        )
        self.bn = nn.BatchNorm2d(no) if bn else None
        self.relu = nn.LeakyReLU(0.2, inplace=True)
    def forward(self, x):
        x = self.relu(self.conv(x))
        return self.bn(x) if self.bn else x

我们要做的第一件事是构建一个鉴别器。鉴别器将接收一幅图像作为输入,并输出一个数字。如果它认为这幅图像是真实的,那么这个数字应该更低。当然,“它为什么输出一个更低的数字”这个问题不会出现在架构中,这将在损失函数中。所以我们所要做的就是创建一个接收图像并输出数字的东西。这些代码的很多部分都是从这篇论文的原始作者那里借来的,所以一些命名方案与我们习惯的不同。但它看起来与我们之前的很相似。我们从卷积(conv,ReLU,批量归一化)开始。然后我们有一堆额外的卷积层——这不会使用残差,所以它看起来与之前非常相似,有一堆额外的层,但这些将是卷积层而不是残差层。最后,我们需要添加足够的步幅为 2 的卷积层,使网格大小减小到不大于 4x4。所以它将继续使用步幅 2,将大小除以 2,并重复直到我们的网格大小不大于 4。这是一个非常好的方法,可以创建网络中所需的任意数量的层,以处理任意大小的图像并将它们转换为固定的已知网格大小。

问题:GAN 是否需要比狗和猫或 NLP 等更多的数据?还是可以相提并论?老实说,我有点尴尬地说我不是 GAN 的专家从业者。我在第一部分教授的东西是我很高兴地说我知道如何做这些事情的最佳方式,所以我可以展示像我们刚刚在 CIFAR10 中所做的那样的最新结果,有一些学生的帮助。我在 GAN 方面一点也不行,所以我不太确定你需要多少。总的来说,似乎需要相当多,但请记住我们在狗和猫方面不需要太多的原因是因为我们有一个预训练模型,我们可以利用预训练的 GAN 模型并微调它们,可能。据我所知,我认为没有人这样做过。这可能是人们考虑和实验的一个非常有趣的事情。也许人们已经这样做了,有一些文献我们还没有接触到。我对 GAN 的主要文献有一些了解,但并不是全部,所以也许我错过了关于 GAN 中迁移学习的一些内容。但这可能是不需要太多数据的诀窍。

问题:是单周期学习率和动量退火加上八个 GPU 并行训练在半精度下的巨大加速?只有消费级 GPU 才能进行半精度计算吗?另一个问题,为什么从单精度到半精度的计算速度提高了 8 倍,而从双精度到单精度只提高了 2 倍?好的,所以 CIFAR10 的结果,从单精度到半精度并不是提高了 8 倍。从单精度到半精度大约快了 2 到 3 倍。NVIDIA 声称张量核心的 flops 性能,在学术上是正确的,但在实践中是没有意义的,因为这真的取决于你需要什么调用来做什么事情——所以半精度大约提高了 2 到 3 倍。所以半精度有所帮助,额外的 GPU 有所帮助,单周期有很大帮助,然后另一个关键部分是我告诉你的参数调整。所以仔细阅读 Wide ResNet 论文,识别他们在那里发现的东西的类型,然后编写一个你刚刚看到的架构的版本,使我们可以轻松地调整参数,整夜不眠地尝试每种可能的不同核大小、核数、层组数、层组大小的组合。记住,我们做了一个瓶颈,但实际上我们更倾向于扩大,所以我们增加了大小,然后减小了,因为这更好地利用了 GPU。所以所有这些结合在一起,我会说单周期也许是最关键的,但每一个都导致了巨大的加速。这就是为什么我们能够在 CIFAR10 的最新技术上取得 30 倍的改进。我们对其他事情有一些想法——在这个 DAWN 基准完成之后,也许我们会尝试更进一步,看看是否可以在某一天打破一分钟。那将很有趣。

class DCGAN_D(nn.Module):
    def __init__(self, isize, nc, ndf, n_extra_layers=0):
        super().__init__()
        assert isize % 16 == 0, "isize has to be a multiple of 16"
        self.initial = ConvBlock(nc, ndf, 4, 2, bn=False)
        csize,cndf = isize/2,ndf
        self.extra = nn.Sequential(*[
            ConvBlock(cndf, cndf, 3, 1)
            for t in range(n_extra_layers)
        ])
        pyr_layers = []
        while csize > 4:
            pyr_layers.append(ConvBlock(cndf, cndf*2, 4, 2))
            cndf *= 2; csize /= 2
        self.pyramid = nn.Sequential(*pyr_layers)
        self.final = nn.Conv2d(cndf, 1, 4, padding=0, bias=False)
    def forward(self, input):
        x = self.initial(input)
        x = self.extra(x)
        x = self.pyramid(x)
        return self.final(x).mean(0).view(1)

所以这是我们的鉴别器。关于架构需要记住的重要事情是它除了有一些输入张量大小和秩,以及一些输出张量大小和秩之外,什么也不做。正如你所看到的,最后一个卷积层只有一个通道。这与我们通常的做法不同,因为通常我们的最后一层是一个线性块。但我们这里的最后一层是一个卷积块。它只有一个通道,但它的网格大小大约是 4x4(不超过 4x4)。所以我们将输出(假设是 4x4),4x4x1 张量。然后我们计算平均值。所以它从 4x4x1 变成一个标量。这有点像最终的自适应平均池化,因为我们有一个通道,我们取平均值。这有点不同——通常我们首先进行平均池化,然后通过一个全连接层来得到我们的输出。但这里是得到一个通道,然后取平均值。Jeremy 怀疑如果我们按照正常方式做会更好,但他还没有尝试过,他也没有足够好的直觉来知道是否漏掉了什么——但如果有人想要尝试在自适应平均池化层和一个具有单个输出的全连接层之后添加一个,那将是一个有趣的实验。

这就是一个鉴别器。假设我们已经有了一个生成器——有人说“好的,这里有一个生成卧室的生成器。我希望你建立一个模型,可以找出哪些是真实的,哪些是假的”。我们将拿取数据集,并标记一堆来自生成器的假卧室图像,以及 LSUN 数据集中真实卧室的一堆图像,然后在每个图像上贴上 1 或 0。然后我们将尝试让鉴别器区分出差异。所以这将是足够简单的。但我们还没有得到一个生成器。我们需要建立一个。我们还没有讨论损失函数——我们将假设有一个损失函数可以做到这一点。

fast.ai 深度学习笔记(六)(2)https://developer.aliyun.com/article/1482697

相关文章
|
4天前
|
机器学习/深度学习 人工智能 算法
【AI】从零构建深度学习框架实践
【5月更文挑战第16天】 本文介绍了从零构建一个轻量级的深度学习框架tinynn,旨在帮助读者理解深度学习的基本组件和框架设计。构建过程包括设计框架架构、实现基本功能、模型定义、反向传播算法、训练和推理过程以及性能优化。文章详细阐述了网络层、张量、损失函数、优化器等组件的抽象和实现,并给出了一个基于MNIST数据集的分类示例,与TensorFlow进行了简单对比。tinynn的源代码可在GitHub上找到,目前支持多种层、损失函数和优化器,适用于学习和实验新算法。
59 2
|
6天前
|
机器学习/深度学习 人工智能 算法
AI大咖说-关于深度学习的一点思考
周志华教授探讨深度学习的成效,指出其关键在于大量数据、强大算力和训练技巧。深度学习依赖于函数可导性、梯度下降及反向传播算法,尽管硬件和数据集有显著进步,但核心原理保持不变。深度意味着增加模型复杂度,相较于拓宽,加深网络更能增强泛函表达能力,促进表示学习,通过逐层加工处理和内置特征变换实现抽象语义理解。周志华教授还提到了非神经网络的深度学习方法——深度森林。5月更文挑战第12天
30 5
|
6天前
|
机器学习/深度学习 人工智能 算法
构建高效AI系统:深度学习优化技术解析
【5月更文挑战第12天】 随着人工智能技术的飞速发展,深度学习已成为推动创新的核心动力。本文将深入探讨在构建高效AI系统中,如何通过优化算法、调整网络结构及使用新型硬件资源等手段显著提升模型性能。我们将剖析先进的优化策略,如自适应学习率调整、梯度累积技巧以及正则化方法,并讨论其对模型训练稳定性和效率的影响。文中不仅提供理论分析,还结合实例说明如何在实际项目中应用这些优化技术。
|
6天前
|
机器学习/深度学习 敏捷开发 人工智能
吴恩达 x Open AI ChatGPT ——如何写出好的提示词视频核心笔记
吴恩达 x Open AI ChatGPT ——如何写出好的提示词视频核心笔记
29 0
|
6天前
|
机器学习/深度学习 人工智能 算法
【AI 初识】讨论深度学习和机器学习之间的区别
【5月更文挑战第3天】【AI 初识】讨论深度学习和机器学习之间的区别
|
6天前
|
机器学习/深度学习 自然语言处理 PyTorch
fast.ai 深度学习笔记(三)(4)
fast.ai 深度学习笔记(三)(4)
25 0
|
6天前
|
机器学习/深度学习 算法 PyTorch
fast.ai 深度学习笔记(三)(3)
fast.ai 深度学习笔记(三)(3)
34 0
|
6天前
|
机器学习/深度学习 编解码 自然语言处理
fast.ai 深度学习笔记(三)(2)
fast.ai 深度学习笔记(三)(2)
38 0
|
6天前
|
机器学习/深度学习 PyTorch 算法框架/工具
fast.ai 深度学习笔记(三)(1)
fast.ai 深度学习笔记(三)(1)
41 0
|
6天前
|
索引 机器学习/深度学习 计算机视觉
fast.ai 深度学习笔记(四)(3)
fast.ai 深度学习笔记(四)
47 0

热门文章

最新文章