深度学习 2:第 2 部分第 13 课
原文:
medium.com/@hiromi_suenaga/deep-learning-2-part-2-lesson-13-43454b21a5d0译者:飞龙
来自 fast.ai 课程的个人笔记。随着我继续复习课程以“真正”理解它,这些笔记将继续更新和改进。非常感谢 Jeremy 和Rachel 给了我这个学习的机会。
图像增强 - 我们将涵盖您可能熟悉的这幅画。然而,您可能之前没有注意到这幅画中有一只鹰。您之前可能没有注意到的原因是这幅画以前没有鹰。同样地,第一张幻灯片上的画以前也没有美国队长的盾牌。
这是一篇很酷的新论文,几天前刚发表,名为Deep Painterly Harmonization,它几乎完全使用了我们将在本课程中学习的技术,只是进行了一些微小的调整。但您可以看到基本思想是将一张图片粘贴在另一张图片上,然后使用某种方法将两者结合起来。这种方法被称为“风格转移”。
在我们讨论之前,我想提一下 William Horton 的这个非常酷的贡献,他将这种随机权重平均技术添加到了 fastai 库中,现在已经全部合并并准备就绪。他写了一整篇关于这个的文章,我强烈建议您查看,不仅因为随机权重平均让您可以从现有的神经网络中获得更高的性能,而且基本上不需要额外的工作(只需向您的 fit 函数添加两个参数:use_swa,swa_start),而且他描述了他构建这个过程以及他如何测试它以及他如何为库做出贡献。所以如果您有兴趣做类似的事情,我认为这很有趣。我认为 William 以前没有建立过这种类型的库,所以他描述了他是如何做到的。
medium.com/@hortonhearsafoo/adding-a-cutting-edge-deep-learning-training-technique-to-the-fast-ai-library-2cd1dba90a49
TrainPhase [2:01]
fastai 库的另一个非常酷的贡献是一个新的训练阶段 API。我将做一件我以前从未做过的事情,那就是我将展示别人的笔记本。之前我没有这样做的原因是因为我没有喜欢到足够好的笔记本,认为值得展示,但 Sylvain 在这里做得非常出色,不仅创建了这个新 API,还创建了一个描述它是什么以及如何工作等等的精美笔记本。背景是,正如大家所知,我们一直在努力更快地训练网络,部分原因是作为这个 Dawn bench 竞赛的一部分,还有一个下周您将了解的原因。我上周在论坛上提到,如果我们有一个更容易尝试不同的学习率调度等的方法,那对我们的实验将非常方便,我提出了我心目中的 API,如果有人能写出来那将非常酷,因为我现在要睡觉了,明天我有点需要它。Sylvain 在论坛上回复说,听起来是一个不错的挑战,24 小时后,它就完成了,而且效果非常酷。我想带您了解一下,因为它将使您能够研究以前没有人尝试过的东西。
这被称为 TrainPhase API,最简单的方法是展示它的示例。这是一个迭代学习率图表,你应该很熟悉。我们在学习率为 0.01 的情况下训练一段时间,然后在学习率为 0.001 的情况下训练一段时间。我实际上想创建一个非常类似于学习率图表的东西,因为大多数训练 ImageNet 的人都使用这种分阶段的方法,而这实际上并不是 fastai 内置的,因为我们通常不建议这样做。但为了复制现有的论文,我想以同样的方式做。因此,与其写一系列不同学习率的 fit、fit、fit 调用,不如能够说在这个学习率下训练 n 个周期,然后在那个学习率下训练 m 个周期。
这就是你如何做到的:
phases = [
TrainingPhase(epochs=1, opt_fn=optim.SGD, lr = 1e-2),
TrainingPhase(epochs=2, opt_fn=optim.SGD, lr = 1e-3)
]
一个阶段是一个具有特定优化器参数的训练期,phases由许多训练阶段对象组成。一个训练阶段对象说明要训练多少个周期,要使用什么优化函数,以及其他我们将看到的东西。在这里,你会看到你刚刚在那张图上看到的两个训练阶段。所以现在,不再调用learn.fit,而是说:
learn.fit_opt_sched(phases)
换句话说,learn.fit与一个具有这些阶段的优化器调度器。大多数传递的参数都可以像往常一样传递给 fit 函数,所以大多数通常的参数都可以正常工作。一般来说,我们只需使用这些训练阶段,你会看到它以一种通常的方式适应。然后当你说plot_lr时,你会看到上面的图表。它不仅绘制学习率,还绘制动量,并且对于每个阶段,它告诉你使用了什么优化器。你可以关闭优化器的打印(show_text=False),你可以关闭动量的打印(show_moms=False),你还可以做其他一些小事情,比如一个训练阶段可以有一个lr_decay参数:
phases = [
TrainingPhase(epochs=1, opt_fn=optim.SGD, lr = 1e-2),
TrainingPhase(
epochs=1,
opt_fn=optim.SGD,
lr = (1e-2,1e-3),
lr_decay=DecayType.LINEAR
),
TrainingPhase(epochs=1, opt_fn=optim.SGD, lr = 1e-3)
]
这里有一个固定的学习率,然后是线性衰减的学习率,然后是放弃这个图像的固定学习率:
lr_i = start_lr + (end_lr - start_lr) * i/n
这可能是一个很好的训练方式,因为我们知道在高学习率下,你可以更好地探索,在低学习率下,你可以更好地微调。逐渐在两者之间滑动可能更好。所以我认为这实际上不是一个坏方法。
你可以使用其他衰减类型,比如余弦:
phases = [
TrainingPhase(epochs=1, opt_fn=optim.SGD, lr = 1e-2),
TrainingPhase(
epochs=1,
opt_fn=optim.SGD,
lr =(1e-2,1e-3),
lr_decay=DecayType.COSINE
),
TrainingPhase(epochs=1, opt_fn=optim.SGD, lr = 1e-3)
]
这可能更有意义,作为一个真正有用的学习率退火形状。
lr_i = end_lr + (start_lr - end_lr)/2 * (1 + np.cos(i * np.pi)/n)
指数,这是一个非常流行的方法:
lr_i = start_lr * (end_lr/start_lr)**(i/n)
多项式并不是非常流行,但实际上在文献中比其他任何方法都要好,但似乎已经被大多数人忽视了。所以多项式是值得注意的。Sylvain 已经为每个曲线给出了公式。因此,使用多项式,你可以选择使用哪个多项式。我相信 p 为 0.9 的多项式是我看到的效果非常好的一个 - FYI。
lr_i = end_lr + (start_lr - end_lr) * (1 - i/n) ** p
如果在 LR 衰减时不提供学习率的元组,那么它将一直衰减到零。如你所见,你可以愉快地从不同的点开始下一个周期。
phases = [
TrainingPhase(epochs=1, opt_fn=optim.SGD, lr = 1e-2),
TrainingPhase(
epochs=1,
opt_fn=optim.SGD,
lr = 1e-2,
lr_decay=DecayType.COSINE
),
TrainingPhase(epochs=1, opt_fn=optim.SGD, lr = 1e-3)
]
SGDR
所以酷的是,现在我们可以仅仅使用这些训练阶段来复制所有我们现有的计划。这里有一个名为phases_sgdr的函数,它使用新的训练阶段 API 来进行 SGDR。
def phases_sgdr(lr, opt_fn, num_cycle,cycle_len,cycle_mult):
phases = [TrainingPhase(epochs = cycle_len/ 20, opt_fn=opt_fn,
lr=lr/100),
TrainingPhase(epochs = cycle_len * 19/20,
opt_fn=opt_fn, lr=lr, lr_decay=DecayType.COSINE)]
for i in range(1,num_cycle):
phases.append(TrainingPhase(epochs=cycle_len*
(cycle_mult**i), opt_fn=opt_fn, lr=lr,
lr_decay=DecayType.COSINE))
return phases
所以你可以看到,如果他按照这个计划运行,这就是它的样子:
他甚至做了我训练时使用非常低的学习率一小段时间然后突然增加并进行几个周期的小技巧,而且这些周期的长度在增加[8:05]。而且这一切都在一个函数中完成。
1cycle
现在我们可以用一个小函数来实现新的 1cycle。
def phases_1cycle(cycle_len,lr,div,pct,max_mom,min_mom):
tri_cyc = (1-pct/100) * cycle_len
return [TrainingPhase(epochs=tri_cyc/2, opt_fn=optim.SGD,
lr=(lr/div,lr), lr_decay=DecayType.LINEAR,
momentum=(max_mom,min_mom),
momentum_decay=DecayType.LINEAR),
TrainingPhase(epochs=tri_cyc/2, opt_fn=optim.SGD,
lr=(lr,lr/div), lr_decay=DecayType.LINEAR,
momentum=(min_mom,max_mom),
momentum_decay=DecayType.LINEAR),
TrainingPhase(epochs=cycle_len-tri_cyc, opt_fn=optim.SGD,
lr=(lr/div,lr/(100*div)),
lr_decay=DecayType.LINEAR,
momentum=max_mom)]
所以如果我们符合这个,我们会得到这个三角形,然后是一个稍微平坦的部分,动量是一个很酷的东西 - 动量有一个动量衰减。在第三个训练阶段,我们有一个固定的动量。所以它同时处理动量和学习率。
区分学习率+ 1cycle
我还没有尝试过的一件事,但我认为会非常有趣的是使用区分学习率和 1cycle 的组合。还没有人尝试过。这将非常有趣。我遇到的唯一一篇使用区分学习率的论文使用了一种称为 LARS 的东西。它被用来通过查看每层的梯度和均值之间的比率并使用该比率自动更改每层的学习率来训练 ImageNet,从而使用非常大的批量大小。他们发现他们可以使用更大的批量大小。这是我看到这种方法使用的唯一其他地方,但是您可以尝试结合区分学习率和不同有趣的调度尝试很多有趣的事情。
您自己的 LR 查找器
现在您可以编写不同类型的 LR finder,特别是因为现在有这个stop_div参数,基本上意味着当损失变得太糟糕时,它将停止训练。
添加的一个有用功能是plot函数中的linear参数。如果您在学习率查找器中使用线性调度而不是指数调度,这是一个好主意,如果您调整到大致正确的区域,那么您可以使用线性来找到确切的区域。然后您可能希望使用线性比例来绘制它。因此,您现在也可以将 linear 传递给 plot。
您可以在每个阶段更改优化器。这比您想象的更重要,因为实际上针对 ImageNet 在非常大的批量大小上快速训练的当前最先进技术实际上是从 RMSProp 开始的,然后他们在第二部分切换到 SGD。因此,这可能是一个有趣的实验,因为至少有一篇论文现在已经表明这样可以很好地工作。再次强调,这是一个尚未被充分认识的问题。
更改数据
然后我发现最有趣的部分是您可以更改您的数据。为什么我们要更改我们的数据?因为您还记得第 1 和第 2 课,您可以在开始时使用小图像,然后稍后使用更大的图像。理论上,您可以使用这种方法更快地训练第一部分,然后记住,如果您将高度减半并将宽度减半,则每层的激活数量就会减少四分之一,因此速度可能会更快。它甚至可能泛化得更好。因此,您现在可以创建几种不同大小,例如,他有 28 和 32 大小的图像。这是 CIFAR10,所以您可以做的事情有限。然后,如果您在调用fit_opt_sched时在data_list参数中传入数据数组,它将在每个阶段使用不同的数据集。
data1 = get_data(28,batch_size)
data2 = get_data(32,batch_size)learn = ConvLearner.from_model_data(ShallowConvNet(), data1)phases = [TrainingPhase(epochs=1, opt_fn=optim.Adam, lr=1e-2,
lr_decay=DecayType.COSINE),
TrainingPhase(epochs=2, opt_fn=optim.Adam, lr=1e-2,
lr_decay=DecayType.COSINE)]learn.fit_opt_sched(phases, data_list=[data1,data2])
这真的很酷,因为我们现在可以像在 DAWN bench 条目中那样使用它,并查看当我们实际上用很少的代码增加大小时会发生什么。那么当我们这样做时会发生什么?答案在 DAWN bench 上对 ImageNet 的训练中。
你可以看到,谷歌用半小时在一组 TPU 上赢得了比赛。最好的非 TPU 集群结果是 fast.ai + 学生在不到 3 小时内击败了拥有 128 台计算机的英特尔,而我们只用了一台计算机。我们还击败了在 TPU 上运行的谷歌,所以使用这种方法,我们已经证明了:
-
最快的 GPU 结果
-
最快的单机结果
-
最快的公开可用基础设施结果
这些 TPU 机架,除非你是谷歌,否则无法使用。而且成本很低(72.54 美元),这个英特尔的成本是 1200 美元的计算成本——他们甚至没有写在这里,但如果你同时使用 128 台计算机,每台有 36 个核心,每台有 140G,那就是你得到的结果,与我们的单个 AWS 实例相比。所以这在我们可以做的事情方面是一种突破。我们可以在一个公开可用的机器上训练 ImageNet,这个成本是 72 美元,顺便说一句,实际上是 25 美元,因为我们使用了一个 spot 实例。我们的学生 Andrew Shaw 建立了整个系统,让我们可以同时运行一堆 spot 实例实验,并且几乎自动化,但 DAWN bench 没有引用我们使用的实际数字。所以实际上是 25 美元,而不是 72 美元。所以这个 data_list 的想法非常重要和有帮助。
CIFAR10 结果
我们的 CIFAR10 结果现在也正式发布了,你可能还记得之前最好的结果是一个多小时。这里的诀窍是使用 1cycle,所以 Sylvain 的训练阶段 API 中的所有东西实际上都是我们用来获得这些顶级结果的东西。另一位 fast.ai 学生 bkj 采用了这个方法,并做了自己的版本,他采用了一个 Resnet18,并在顶部添加了我们学到的 concat pooling,并使用了 Leslie Smith 的 1cycle,所以他上了排行榜。所以前三名都是 fast.ai 的学生,这太棒了。
CIFAR10 成本结果
成本也是一样的——前三名,你可以看到,Paperspace。Brett 在 Paperspace 上运行,得到了最便宜的结果,略胜于 bkj。
所以我认为你可以看到,目前训练更快、更便宜的有趣机会很多都是关于学习率退火、尺寸退火,以及在不同时间使用不同参数进行训练,我仍然认为大家只是触及了表面。我认为我们可以做得更快、更便宜。这对于资源受限的环境中的人们非常有帮助,基本上除了谷歌,也许还有 Facebook。
架构也很有趣,上周我们看了一下简化版本的 darknet 架构。但有一个架构我们还没有谈到,那就是理解 Inception 网络所必需的。Inception 网络实际上非常有趣,因为他们使用了一些技巧使得事情更加高效。我们目前没有使用这些技巧,我觉得也许我们应该尝试一下。最有趣、最成功的 Inception 网络是他们的 Inception-ResNet-v2 网络,其中大部分块看起来像这样:
它看起来很像标准的 ResNet 块,因为有一个恒等连接,还有一个卷积路径,我们把它们加在一起。但实际上并不完全是这样。首先,中间的卷积路径是一个 1x1 卷积,值得思考一下 1x1 卷积实际上是什么。
1x1 卷积
1x1 卷积简单地说,对于输入中的每个网格单元,您基本上有一个向量。1 乘 1 乘滤波器数量的张量基本上是一个向量。对于输入中的每个网格单元,您只需与该张量进行点积。然后,当然,对于我们正在创建的 192 个激活之一,它将是这些向量之一。因此,基本上对网格单元(1,1)进行 192 个点积,然后对网格单元(1,2)或(1,3)等进行 192 个点积。因此,您将得到与输入具有相同网格大小和输出中的 192 个通道的内容。因此,这是一种非常好的方法,可以减少或增加输入的维度,而不改变网格大小。这通常是我们使用 1x1 卷积的方式。在这里,我们有一个 1x1 卷积和另一个 1x1 卷积,然后将它们相加。然后有第三个路径,这第三个路径没有被添加。虽然没有明确提到,但这第三个路径是被连接的。有一种形式的 ResNet 基本上与 ResNet 相同,但我们不使用加号,而是使用连接。这被称为 DenseNet。这只是一个使用连接而不是加法的 ResNet。这是一个有趣的方法,因为这样,身份路径实际上被复制。因此,您可以一直保持这种流动,因此正如我们将在下周看到的那样,这对于分割等需要保留原始像素、第一层像素和第二层像素不变的情况非常有用。
连接而不是添加分支是一件非常有用的事情,我们正在连接中间分支和右侧分支。最右侧的分支正在做一些有趣的事情,首先是 1x1 卷积,然后是 1x7,然后是 7x1。那里发生了什么?所以,那里发生的事情基本上是我们真正想要做的是 7x7 卷积。我们想要做 7x7 卷积的原因是,如果有多个路径(每个路径具有不同的内核大小),那么它可以查看图像的不同部分。最初的 Inception 网络将 1x1、3x3、5x5、7x7 连接在一起或类似的东西。因此,如果我们可以有一个 7x7 滤波器,那么我们可以一次查看图像的很多部分并创建一个非常丰富的表示。因此,Inception 网络的干部,即 Inception 网络的前几层实际上也使用了这种 7x7 卷积,因为您从这个 224x224x3 开始,希望将其转换为 112x112x64。通过使用 7x7 卷积,您可以在每个输出中获得大量信息以获得这些 64 个滤波器。但问题是 7x7 卷积是很费力的。您需要将 49 个内核值乘以每个通道的每个输入像素的 49 个输入。因此,计算量很大。您可能可以在第一层中使用它(也许可以),实际上,ResNet 的第一个卷积就是 7x7 卷积。
但对于《盗梦空间》来说并非如此。它们不使用 7x7 卷积,而是使用 1x7 接着 7x1。因此,基本思想是 Inception 网络或其所有不同版本的基本思想是有许多不同的卷积宽度的独立路径。在这种情况下,概念上的想法是中间路径是 1x1 卷积宽度,右侧路径将是 7 卷积宽度,因此它们正在查看不同数量的数据,然后将它们组合在一起。但我们不希望在整个网络中都使用 7x7 卷积,因为这太耗费计算资源了。
但是如果你考虑一下[23:18],如果我们有一些输入进来,我们有一些我们想要的大滤波器,但它太大了无法处理。我们能做什么?让我们做 5x5。我们可以创建两个滤波器 —— 一个是 1x5,一个是 5x1。我们将前一层的激活传递给 1x5。我们从中取出激活,然后通过 5x1 传递,最后得到一些结果。现在另一端出来了什么?与其将其视为首先我们取激活,然后通过 1x5,然后通过 5x1,不如一起考虑这两个操作,看看一个 5x1 点积和一个 1x5 点积一起做会发生什么?实际上,你可以取一个 1x5 和 5x1,它们的外积将给你一个 5x5。现在你不能通过取这个积来创建任何可能的 5x5 矩阵,但是你可以创建很多 5x5 矩阵。所以这里的基本思想是当你考虑操作的顺序时(如果你对这里的理论更感兴趣,你应该查看 Rachel 的数值线性代数课程,这基本上是关于这个的整个课程)。但从概念上来说,很多时候你想要做的计算实际上比整个 5x5 卷积更简单。在线性代数中我们经常使用的术语是有一些低秩近似。换句话说,1x5 和 5x1 结合在一起 —— 那个 5x5 矩阵几乎和你理想情况下应该计算的 5x5 矩阵一样好。所以在实践中这往往是情况 —— 因为现实世界的本质是现实世界往往比随机性更具结构性。
酷的地方是[26:16],如果我们用 1x7 和 7x1 替换我们的 7x7 卷积,对于每个单元格,它有 14 个输入通道乘以输出通道的点积要做,而 7x7 卷积则有 49 个要做。所以速度会快得多,我们希望它的效果几乎一样好。从定义上来说,它肯定捕捉到了尽可能多的信息宽度。
如果你对这方面的知识感兴趣,特别是在深度学习领域,你可以搜索分解卷积。这个想法是 3 年或 4 年前提出的。它可能已经存在更长时间了,但那是我第一次看到它的时候。结果表明它的效果非常好,Inception 网络广泛使用它。他们实际上在他们的干部中使用它。我们之前谈过,我们倾向于添加-on —— 我们倾向于说这是主干,例如我们有 ResNet34。这是主干,其中包含所有的卷积,然后我们可以添加一个自定义头部,通常是最大池化或全连接层。更好的做法是谈论主干包含两个部分:一个是干部,另一个是主干。原因是进来的东西只有 3 个通道,所以我们希望有一系列操作将其扩展为更丰富的东西 —— 通常是 64 个通道之类的东西。
在 ResNet 中,干部非常简单。它是一个 7x7 步幅 2 卷积,后面跟着一个步幅 2 最大池(如果我记得正确的话)。Inception 有一个更复杂的干部,其中包括多个路径的组合和连接,包括因子化卷积(1x7 和 7x1)。我很感兴趣的是,如果你在 Inception 干部上堆叠一个标准的 ResNet 会发生什么。我认为这将是一个非常有趣的尝试,因为 Inception 干部是一个非常精心设计的东西,以及如何将 3 通道输入转换为更丰富的东西似乎非常重要。而所有这些工作似乎都被抛弃了。我们喜欢 ResNet,它的效果非常好。但是如果我们在 Inception 干部上放置一个密集的网络骨干呢?或者如果我们用标准 ResNet 中的 1x7 和 7x1 因子化卷积替换 7x7 卷积呢?有很多事情我们可以尝试,我认为这将是非常有趣的。所以这是关于潜在研究方向的一些想法。
这就是我小小一堆随机东西部分的内容[29:51]。稍微接近这个实际主题的是图像增强。我将简要谈一下一篇新论文,因为它与我刚刚讨论的内容和我们接下来要讨论的内容有很大联系。这是一篇关于渐进式 GAN 的论文,来自 Nvidia:渐进增长的 GANs 用于提高质量、稳定性和变化。渐进式 GANs 采用了逐渐增加图像大小的想法。这是我所知道的唯一另一个人们实际上逐渐增加图像大小的方向。令我惊讶的是,这篇论文实际上非常受欢迎,知名度很高,而且受欢迎,但是人们还没有将逐渐增加图像大小的基本思想应用到其他地方,这显示了你可以在深度学习研究社区中期望找到的创造力水平。
他们真的回到了 4x4 GAN 开始[31:47]。实际上,他们试图复制 4x4 像素,然后是 8x8(上面左上角的那些)。这是 CelebA 数据集,所以我们试图重新创建名人的图片。然后他们去 16x16,32,64,128,然后 256。他们做的一个非常聪明的事情是,随着尺寸的增加,他们还向网络添加更多层。这有点说得通,因为如果你在做更多的 ResNet 类型的事情,那么你应该能够在每个网格单元大小输出一些有意义的东西,所以你应该能够在其上叠加东西。当他们这样做时,他们做了另一个聪明的事情,他们添加了一个跳过连接,并逐渐改变线性插值参数,使其越来越远离旧的 4x4 网络,朝向新的 8x8 网络。一旦完全移动到新网络,他们就会丢弃那个额外的连接。细节并不太重要,但它使用了我们谈论过的基本思想,逐渐增加图像大小和跳过连接。这是一篇很棒的论文,因为这是一种罕见的情况,好的工程师实际上构建了一些以非常明智的方式工作的东西。现在这并不奇怪,这实际上来自 Nvidia 自己。Nvidia 并不发表很多论文,有趣的是,当他们这样做时,他们构建了一些非常实用和明智的东西。所以我认为这是一篇很棒的论文,如果你想整合我们学到的许多不同的东西,而且没有太多的重新实现,所以这是一个有趣的项目,也许你可以继续研究并找到其他东西。
接下来会发生什么[33:45]。我们最终会升级到 1024x1024,你会看到图像不仅分辨率更高,而且质量更好。所以我要看看你能否猜出以下哪一个是假的:
它们全都是假的。这是下一个阶段。你一直往上走,然后突然爆炸。所以 GANs 和其他东西变得疯狂,你们中的一些人可能在这周看到了这个[34:16]。这个视频刚刚发布,是巴拉克·奥巴马的演讲,让我们来看一下:
正如你所看到的,他们使用这种技术来实际移动奥巴马的脸,就像乔丹·皮尔的脸在移动一样。你现在基本上拥有了所有需要的技术。这是一个好主意吗?
人工智能伦理[35:31]
这是我们谈论最重要的部分,现在我们可以做所有这些事情,我们应该做什么,我们如何考虑?简而言之,我其实不知道。最近,你们中的许多人看到了 spaCy prodigy 公司的创始人在 Explosion AI 做了一个演讲,Matthew 和 Ines,之后我和他们一起吃饭,我们基本上整个晚上都在讨论,辩论,争论我们这样的公司正在构建可以以有害方式使用的工具,这意味着什么。他们是非常深思熟虑的人,我们,我不会说我们没有达成一致意见,我们只是无法得出结论。所以我只是列出一些问题,并指出一些研究,当我说研究时,实际上大部分文献综述和整理工作都是由 Rachel 完成的,所以谢谢 Rachel。
让我先说一下,我们构建的模型通常在某些方面相当糟糕,这些问题并不立即显现[36:52]。除非与你一起构建它们的人是各种各样的人,与你一起使用它们的人也是各种各样的人,否则你不会知道它们有多糟糕。例如,一对出色的研究人员,Timnit Gebru在微软工作,Joy Buolamwini刚从麻省理工学院获得博士学位,他们进行了一项非常有趣的研究,他们查看了一些现成的人脸识别器,其中包括来自 FACE++的一个,这是一家庞大的中国公司,IBM 的,以及微软的,他们寻找了一系列不同类型的人脸。
一般来说,微软的一个特别准确,除非人脸类型恰好是深色皮肤,突然间糟糕了 25 倍。IBM 几乎一半的时间都搞错了。对于这样一个大公司来说,发布一个对世界上大部分人来说都不起作用的产品,不仅仅是技术上的失败。这是对理解需要使用什么样的团队来创建这样的技术以及测试这样的技术,甚至对你的客户是谁的一种深刻失败。你的一些客户有深色皮肤。“我还要补充说,分类器在女性身上的表现都比在男性身上差”(Rachel)。令人震惊。有趣的是,Rachel 前几天在推特上发表了类似的言论,有人说“这是怎么回事?你在说什么?难道你不知道人们很长时间以来一直在制造汽车吗——你是在说你需要女性来制造汽车吗?”Rachel 指出——实际上是的。在汽车安全的大部分历史中,女性在汽车中的死亡风险远远高于男性,因为男性创造了看起来像男性、感觉像男性、尺寸像男性的碰撞测试假人,所以汽车安全实际上没有在女性身材上进行测试。产品管理糟糕,缺乏多样性和理解的失败在我们领域并不新鲜。
“我只是想说,这是在比较男性和女性的影响力”(Rachel)。我不知道为什么每当你在 Twitter 上说这样的话时,Rachel 都要这样说,因为每当你在 Twitter 上说这样的话时,大约有 10 个人会说“哦,你必须比较所有这些其他事情”,好像我们不知道一样。
像微软的人脸识别器或谷歌的语言翻译器这样的我们最好最著名的系统做的其他事情,你把“她是医生。他是护士。”翻译成土耳其语,非常正确——两个代词都变成了 O,因为土耳其语中没有性别代词。反过来,它会变成什么?“他是医生。她是护士。”所以我们在每天使用的工具中内置了这种偏见。而且,人们会说“哦,它只是展示了世界上的东西”,好吧,这个基本断言有很多问题,但正如你所知,机器学习算法喜欢概括。
因为他们喜欢概括,这是你们现在了解技术细节的一个很酷的事情,因为当你看到像 60%的照片中烹饪的人是女性,而他们用来构建这个模型的照片,然后你在另一组照片上运行模型时,84%被选择为烹饪的人是女性,而不是正确的 67%。这对于算法来说是一个非常可以理解的事情,因为它接受了有偏见的输入,并创造了一个更有偏见的输出,因为对于这个特定的损失函数来说,这就是它的结果。这是一种非常常见的模型放大。
这些事情很重要。它的重要性不仅仅体现在尴尬的翻译或黑人照片未被正确分类的方式上。也许也有一些胜利,比如到处可怕的监视,也许对黑人不起作用。“或者会更糟,因为这是可怕的监视,而且是彻头彻尾的种族主义和错误”(Rachel)。但让我们深入一点。尽管我们谈论人类的缺陷,但文明和社会有着长期的历史,创造了层层人类判断,希望避免最可怕的事情发生。有时候,热爱技术的公司会认为“让我们抛弃人类,用技术取代他们”,就像 Facebook 所做的那样。几年前,Facebook 真的摆脱了他们的人类编辑,当时这成为了新闻。他们被算法取代了。现在,当算法将所有内容放在你的新闻源上,而人类编辑却被排除在外时,接下来会发生什么?
接下来发生了很多事情。其中之一是缅甸发生了大规模的可怕种族灭绝。婴儿被从母亲怀里夺走,扔进火里。大规模的强奸、谋杀,整个民族被流放出境。
好吧,我不会说那是因为 Facebook 这样做的,但我要说的是,当这个可怕项目的领导接受采访时,他们经常谈论他们从 Facebook 学到的关于罗辛亚人恶劣动物行为的一切,这些行为需要被扫除。因为算法只是想要给你更多让你点击的东西。如果你被告知这些人不像你,你不认识这些坏人,这里有很多关于坏人的故事,然后你开始点击它们,然后他们会给你更多这些东西。接下来你会发现,你陷入了这个不寻常的循环。人们一直在研究这个问题,比如,我们被告知有几次人们点击我们的 fast.ai 视频,然后推荐给他们的下一个东西是来自 Alex Jones 的阴谋论视频,然后继续下去。因为人类点击那些让我们震惊、惊讶和恐惧的东西。在很多层面上,这个决定产生了不同寻常的后果,我们只是开始理解。再次强调,这并不是说这个特定的后果是因为这一个原因,但说它与此毫无关联显然是在忽视我们所拥有的所有证据和信息。
有意外的后果
关键是要考虑你正在构建什么,以及它可能如何被使用。现在有很多努力投入到人脸识别中,包括我们的课程。我们一直在花费大量时间思考如何识别东西以及它在哪里。有很多很好的理由希望在这方面做得更好,比如改善农业产量、改善医学诊断和治疗规划、改善你的乐高分类机器人系统等等。但它也被广泛用于监视、宣传和虚假信息。再次,问题是我该怎么办?我不完全知道。但至少重要的是要考虑这个问题,谈论这个问题。
失控的反馈循环
有时候你可以做一些非常好的事情。例如,meetup.com 做了一件我认为是非常好的事情的事情,他们早就意识到一个潜在的问题,即更多的男性倾向于参加他们的聚会。这导致他们的协同过滤系统,你现在熟悉正在构建的系统,向男性推荐更多技术内容。这导致更多的男性参加更多的技术内容,从而导致推荐系统向男性推荐更多技术内容。当我们将算法和人类结合在一起时,这种失控的反馈循环是非常常见的。那么 Meetup 做了什么?他们故意做出了向女性推荐更多技术内容的决定,不是因为对世界应该如何的高尚想法,而只是因为这是有道理的。失控的反馈循环是一个 bug——有女性想要参加技术聚会,但当你去参加一个技术聚会,里面全是男性,你就不去了,然后它就会向男性推荐更多,依此类推。因此,Meetup 在这里做出了一个非常强有力的产品管理决策,即不按照算法建议的做。不幸的是,这种情况很少见。大多数这种失控的反馈循环,例如在预测性警务中,算法告诉警察去哪里,很多时候是更多的黑人社区,这些社区最终会涌入更多的警察,导致更多的逮捕,这有助于告诉更多的警察去更多的黑人社区等等。
AI 中的偏见
算法偏见的问题现在非常普遍,随着算法在特定政策决策、司法决策以及日常决策中的广泛使用,这个问题变得越来越严重。其中一些问题实际上是产品管理决策中的人员在最初就应该看到的,这些问题在任何定义下都是没有意义和不合理的。例如,阿贝·龚指出的这些问题——这些问题既用于预审,即谁需要支付保释金,这些人甚至还没有被定罪,也用于判决以及谁获得假释。尽管存在所有的缺陷,这在去年被威斯康星州最高法院维持了。所以你是否必须因为支付不起保释金而留在监狱,你的刑期有多长,你在监狱里待多久取决于你父亲做了什么,你的父母是否离婚,你的朋友是谁,以及你住在哪里。现在事实证明这些算法实际上非常糟糕,最近的一些分析显示它们基本上比随机还要糟糕。但即使公司在这些统计上的相关性上很有信心,有人能想象出一个世界,在那里根据你父亲的行为来决定发生什么吗?
在基本层面上,很多事情显然是不合理的,很多事情只是以这种你可以从经验上看到的方式失败了,这种失控的反馈循环一定发生过,这种过度概括一定发生过。例如,任何领域工作的人都应该准备这些交叉表,使用这些算法。因此,对黑人和白人被告重新犯罪的可能性的预测,我们可以很简单地计算出来。那些被标记为高风险但没有再犯的人中,23.5%是白人,而非洲裔美国人大约是白人的两倍。而那些被标记为低风险但再犯的人中,白人只有非洲裔美国人的一半,而非洲裔美国人只有 28%。这就是这种情况,至少如果你正在使用我们谈论过的技术,并以任何方式将其投入生产,为其他人构建 API,为人们提供培训,或者其他什么——那么至少确保你所做的事情可以被追踪,以便人们知道发生了什么,至少他们是知情的。我认为假设人们是邪恶的,试图破坏社会是错误的。我认为我更愿意从一个假设开始,即如果人们做了愚蠢的事情,那是因为他们不知道更好的方法。所以至少确保他们有这些信息。我发现很少有机器学习从业者考虑他们的界面中应该呈现什么信息。然后我经常会和数据科学家交谈,他们会说“哦,我正在研究的东西对社会没有影响。”真的吗?有很多人认为他们正在做的事情完全毫无意义吗?来吧。人们付钱让你做这件事是有原因的。它会以某种方式影响人们。所以考虑一下这是什么。
在招聘中的责任
我知道的另一件事是很多参与其中的人都在招聘人才,如果你在招聘人才,我想你现在都非常熟悉 fast.ai 的理念,这基本上是这样一个前提,我认为人们总体上并不邪恶,我认为他们需要被告知并拥有工具。因此,我们正在尽可能地为尽可能多的人提供他们需要的工具,特别是我们正在尝试将这些工具交到更广泛人群的手中。因此,如果你参与招聘决策,也许你可以记住这种理念。如果你不仅仅是招聘更广泛的人才,而且还提拔更广泛的人才,并为更广泛的人提供适当的职业管理,除了其他任何事情,你的公司会做得更好。事实证明,更多样化的团队更有创造力,往往比不那么多样化的团队更快更好地解决问题,而且你也可能避免这些糟糕的失误,这在某种程度上对世界是有害的,而在另一层面,如果你被发现,它们可能毁掉你的公司。
IBM 和“死亡计算器”
他们也可以摧毁你,或者至少让你在历史上看起来很糟糕。举几个例子,一个是回到第二次世界大战。IBM 提供了跟踪大屠杀所需的所有基础设施。这些是他们使用的表格,它们有不同的代码 - 犹太人是 8,吉普赛人是 12,毒气室中的死亡是 6,所有这些都记录在这些打孔卡上。现在你可以去博物馆看这些打孔卡,这实际上已经被一位瑞士法官审查过,他说 IBM 的技术支持促进了纳粹分子的任务并促使他们犯下反人类罪行。回顾这些时期的历史,看看当时 IBM 的人们在想什么是很有趣的。当时人们明显在想的是展示技术优势的机会,测试他们的新系统的机会,当然还有他们赚取的巨额利润。当你做了一些事情,即使在某个时候会变成问题,即使你被告知要这样做,这对你个人来说也可能成为问题。例如,大家都记得大众柴油排放丑闻。谁是唯一一个入狱的人?那就是只是在做他的工作的工程师。如果所有这些关于实际上不要搞砸世界的东西还不足以说服你,那么它也可能毁掉你的生活。如果你做了一些事情,结果导致问题,即使有人告诉你要这样做,你绝对可能被追究刑事责任。亚历山大·科根就是那个交出剑桥分析数据的人。他是一位剑桥学者。现在是一位全球著名的剑桥学者,因为他为摧毁民主的基础做出了自己的贡献。这不是我们想要留在历史上的方式。
问题: 在你的一条推特中,你说 dropout 被专利化了[56:50]。我认为这是关于 Google 的 WaveNet 专利。这是什么意思?你能分享更多关于这个主题的见解吗?这意味着我们将来要付费使用 dropout 吗?专利持有人之一是 Geoffrey Hinton。那又怎样?这不是很棒吗?发明就是关于专利的,啦啦啦。我的答案是否定的。专利已经变得疯狂。我们每周讨论的可以被专利化的东西数量会有几十个。很容易想出一个小调整,然后如果你把它变成专利来阻止每个人在接下来的 14 年内使用那个小调整,最终我们就会面临现在的情况,所有东西都以 50 种不同的方式被专利化。然后你会遇到这些专利流氓,他们通过购买大量垃圾专利然后起诉任何无意中做了那件事的人,比如给按钮加上圆角。那么对于我们来说,深度学习中有很多东西被专利化意味着什么?我不知道。
做这个工作的主要人员之一是 Google,而且来自 Google 的人回应这个专利时倾向于认为 Google 这样做是因为他们想要防御性地拥有它,所以如果有人起诉他们,他们可以说不要起诉我们,我们会反诉你,因为我们有所有这些专利。问题是据我所知,他们还没有签署所谓的防御性专利承诺,所以基本上你可以签署一个法律约束文件,说我们的专利组合只会用于防御而不是进攻。即使你相信 Google 的所有管理层永远不会变成专利流氓,你必须记住管理层会变化。给你一个具体的例子,我知道,Google 的最近的 CFO 对 PNL 有更积极的态度,我不知道,也许她会决定他们应该开始变现他们的专利,或者也许做出那个专利的团队可能会被分拆然后卖给另一家公司,最终可能会落入私募股权手中并决定变现专利或其他。所以我认为这是一个问题。最近在法律上有一个从软件专利转向实际上没有任何法律地位的大变化,所以这些可能最终都会被驳回,但现实是,任何不是大公司的人都不太可能有财务能力来抵抗这些庞大的专利流氓。
如果你写代码,就无法避免使用专利的东西。我不会感到惊讶,如果你写的大部分代码都有专利。实际上,有趣的是,最好的做法不是研究专利,因为如果你故意侵犯,惩罚会更严重。所以最好的做法是把手放在耳朵上,唱首歌,然后继续工作。所以关于 dropout 被专利化的事情,忘记我说过的。你不知道那个。你跳过那部分。
风格迁移[1:01:28]
这非常有趣——艺术风格。我们在这里有点复古,因为这实际上是最初的艺术风格论文,后来有很多更新和很多不同的方法,我实际上认为在很多方面最初的方法是最好的。我们也会看一些更新的方法,但我实际上认为最初的方法是一个很棒的方式,即使在之后的一切发展之后。让我们来看看代码。
%matplotlib inline
%reload_ext autoreload
%autoreload 2from fastai.conv_learner import *
from pathlib import Path
from scipy import ndimage
torch.cuda.set_device(3)
torch.backends.cudnn.benchmark=TruePATH = Path('data/imagenet')
PATH_TRN = PATH/'train'm_vgg = to_gpu(vgg16(True)).eval()
set_trainable(m_vgg, False)
这里的想法是我们想要拍摄一只鸟的照片,并且我们想要创作一幅看起来像梵高画了这只鸟的画。顺便说一句,我正在做的很多事情都使用了 ImageNet。你不必为我所做的任何事情下载整个 ImageNet。在files.fast.ai/data中有一个 ImageNet 样本,它有几个 G 的数据,对我们正在做的一切来说应该足够了。如果你想要得到真正出色的结果,你可以获取 ImageNet。你可以从Kaggle下载。定位竞赛实际上包含了所有的分类数据。如果你有空间,最好拥有一份 ImageNet 的副本,因为它随时都会派上用场。
img_fn = PATH_TRN/'n01558993'/'n01558993_9684.JPEG'
img = open_image(img_fn)
plt.imshow(img);
所以我刚从我的 ImageNet 文件夹中拿出了这只鸟,这就是我的鸟:
sz=288trn_tfms,val_tfms = tfms_from_model(vgg16, sz)
img_tfm = val_tfms(img)
img_tfm.shape*(3, 288, 288)*opt_img = np.random.uniform(0, 1, size=img.shape).astype(np.float32)
plt.imshow(opt_img);
我要做的是从这张图片开始:
我将尝试让它越来越像梵高画的鸟的图片。我做的方法实际上非常简单。你们都很熟悉它。我们将创建一个损失函数,我们将称之为f。损失函数将以一张图片作为输入,并输出一个值。如果图像看起来更像梵高画的鸟照片,那么这个值将更低。编写了这个损失函数之后,我们将使用 PyTorch 的梯度和优化器。梯度乘以学习率,我们不会更新任何权重,而是会更新输入图像的像素,使其更像梵高画的鸟的图片。然后我们再次通过损失函数来获取更多的梯度,一遍又一遍地进行。就是这样。所以这与我们解决每个问题的方式是相同的。你们知道我是一个只会一招的人,对吧?这是我的唯一招数。创建一个损失函数,用它来获取一些梯度,乘以学习率来更新某些东西,以前我们总是更新模型中的权重,但今天我们不会这样做。我们将更新输入图像中的像素。但实际上并没有什么不同。我们只是针对输入而不是针对权重来获取梯度。就是这样。我们快要完成了。
让我们做更多的事情。让我们在这里提到,我们的损失函数将有两个额外的输入。一个是鸟的图片。第二个是梵高的一幅艺术作品。通过将它们作为输入,这意味着我们以后可以重新运行这个函数,使其看起来像梵高画的鸟,或者像莫奈画的鸟,或者像梵高画的大型喷气式飞机等。这些将是三个输入。最初,正如我们讨论过的,我们的输入是一些随机噪音。我们从一些随机噪音开始,使用损失函数,获取梯度,使其更像梵高画的鸟,依此类推。
所以我猜我们可以简要讨论的唯一未解决的问题是我们如何计算我们的图像看起来有多像梵高画的这只鸟。让我们将其分为两部分:
内容损失:返回一个值,如果看起来更像这只鸟(不只是任何鸟,而是我们要处理的特定鸟)。
风格损失:如果图像更像 V.G.的风格,则返回一个较低的数字。
有一种非常简单的计算内容损失的方法——我们可以查看输出的像素,将它们与鸟的像素进行比较,计算均方误差,然后相加。所以如果我们这样做,我运行了一段时间。最终我们的图像会变成一只鸟的图像。你应该尝试一下。你应该尝试这个作为练习。尝试使用 PyTorch 中的优化器,从一个随机图像开始,通过使用均方误差像素损失将其转变为另一幅图像。这并不是非常令人兴奋,但这将是第一步。
问题是,即使我们已经有了我们的风格损失函数运行得很好,然后假设我们要做的是将这两者相加,然后其中一个,我们将乘以一些λ来调整风格与内容的比例。假设我们有一个风格损失并选择了一些合理的λ,如果我们使用像素级的内容损失,那么任何使其看起来更像梵高而不是完全像照片、背景、对比度、光照等的东西都会增加内容损失——这不是我们想要的。我们希望它看起来像鸟,但不是以相同的方式。它仍然会有相同位置的两只眼睛,相同的形状等等,但不是相同的表示。所以我们要做的是,这可能会让您震惊,我们要使用一个神经网络!我们将使用 VGG 神经网络,因为那是我去年使用的,我没有时间看其他东西是否有效,所以您可以在这一周自己尝试。
VGG 网络是一个接受输入并将其通过多个层的网络,我将把这些层视为卷积层,显然还有 ReLU,如果是带有批量归一化的 VGG,那么它也有批量归一化。还有一些最大池化等等,但没关系。我们可以做的是,我们可以取其中一个卷积激活,而不是比较这只鸟的像素,我们可以比较这个(由 V.G.绘制的)鸟的 VGG 层 5 激活与我们原始鸟的 VGG 层 5 激活(或第 6 层,第 7 层等)。那么为什么这样更有趣呢?首先,它不会是同一只鸟。它不会完全相同,因为我们不是在检查像素。我们在检查一些后续的激活。那么这些后续的激活包含什么?假设它经过一些最大池化后,它包含一个较小的网格——所以它对事物的位置不那么具体。而不是包含像素颜色值,它们更像是语义的东西,比如这是一种眼球,这是一种毛茸茸的,这是一种明亮的,或者这是一种反射的,或者平放的,或者其他什么。因此,我们希望通过这些层有一定程度的语义特征,如果我们得到一个与这些激活匹配的图片,那么任何匹配这些激活的图片看起来像鸟,但不是相同的鸟的表示。这就是我们要做的。这就是我们的内容损失将是什么。人们通常称之为感知损失,因为在深度学习中,您总是为您做的每件明显的事情创造一个新名称。如果您将两个激活进行比较,您正在进行感知损失。就是这样。我们的内容损失将是感知损失。然后我们将稍后进行风格损失。
让我们从尝试创建一只最初是随机噪音的鸟开始,我们将使用感知损失来创建类似鸟的东西,但不是特定的鸟。我们将从 288x288 开始。因为我们只做一只鸟,所以不会出现 GPU 内存问题。我实际上很失望地意识到我选择了一个相当小的输入图像。尝试使用更大的图像创建一个真正宏伟的作品会很有趣。另一件事要记住的是,如果您要将其投入生产,可以一次处理整个批次。有时人们会抱怨这种方法(Gatys 是主要作者)——Gatys 的风格迁移方法很慢,但我不同意它很慢。只需要几秒钟,您就可以在几秒钟内处理整个批次。
sz=288
所以我们将按照通常的做法将其通过 VGG16 模型的一些转换。记住,转换类有 dunder call 方法(__call__),所以我们可以将其视为一个函数。如果你将一个图像传递给它,那么我们将得到转换后的图像。尽量不要将 fast.ai 和 PyTorch 基础设施视为黑盒,因为它们都设计成非常易于以解耦的方式使用。所以这个转换只是“可调用”的想法(即用括号括起来的东西)来自于 PyTorch,我们完全抄袭了这个想法。所以在 torch.vision 或 fast.ai 中,你的转换只是可调用的。整个转换流水线只是一个可调用的。
trn_tfms,val_tfms = tfms_from_model(vgg16, sz)
img_tfm = val_tfms(img)
img_tfm.shape
'''
(3, 288, 288)
'''
现在我们有了一个 3x288x288 的东西,因为 PyTorch 喜欢通道在前面。正如你所看到的,它已经被转化为一个方形,被归一化为(0,1),所有这些正常的东西。
现在我们正在创建一个随机图像。
opt_img = np.random.uniform(0, 1, size=img.shape).astype(np.float32)
plt.imshow(opt_img);
这是我发现的一件事。试图将这个转化为任何东西的图片实际上非常困难。我发现实际上很难让优化器获得合理的梯度,使其有所作为。就在我以为我要在这门课上耗尽时间并真正让自己尴尬的时候,我意识到关键问题是图片不是这样的。它们更加平滑,所以我稍微模糊了一下,将其转化为以下内容:
opt_img = scipy.ndimage.filters.median_filter(opt_img, [8,8,1])
plt.imshow(opt_img);
我使用了一个中值滤波器——基本上就像一个中值池化。一旦我将其改为这样,它立即开始训练得非常好。你必须做一些微小的调整才能让这些东西工作起来,这有点疯狂,但这里有一个小调整。
所以我们从一个随机图像开始,这个图像至少有一定的平滑度。我发现我的鸟类图像的像素均值大约是这个值的一半,所以我将其除以 2,只是试图让匹配变得更容易一些(我不知道这是否重要)。将其转化为一个变量,因为这个图像,记住,我们将使用优化算法修改这些像素,所以任何涉及损失函数的东西都需要是一个变量。并且,它需要梯度,因为我们实际上是在更新图像。
opt_img = val_tfms(opt_img)/2
opt_img_v = V(opt_img[None], requires_grad=True)
opt_img_v.shape*torch.Size([1, 3, 288, 288])*
所以现在我们有了一个大小为 1 的小批量,3 个通道,288x288 的随机噪声。
m_vgg = nn.Sequential(*children(m_vgg)[:37])
我们将使用,没有特定原因,VGG 的第 37 层。如果你打印出 VGG 网络(你只需输入m_vgg并打印出来),你会看到这是中后期的层。所以我们可以只获取前 37 层并将其转化为一个顺序模型。现在我们有了一个 VGG 的子集,它将输出一些中间层的激活,这就是模型将要做的事情。所以我们可以拿到我们实际的鸟类图像,我们想创建一个大小为一的小批量。记住,如果你在 Numpy 中使用None进行切片,也就是np.newaxis,它会在那个点引入一个新的单位轴。这里,我想创建一个大小为 1 的轴,表示这是一个大小为一的小批量。所以就像我在这里做的一样(opt_img_v = V(opt_img[**None**], requires_grad=**True**))使用None进行切片,在前面得到一个单位轴。然后我们将其转化为一个变量,这个变量不需要更新,所以我们使用VV来表示你不需要为这个变量计算梯度。这将给我们我们的目标激活。
-
我们已经拿到了我们的鸟类图像。
-
将其转化为一个变量
-
将其通过我们的模型传递,以获取第 37 层的激活,这是我们的目标。我们希望我们的内容损失是这组激活。
-
我们将创建一个优化器(我们稍后会回到这个细节)
-
我们将进行多次迭代
-
梯度清零
-
调用一些损失函数
-
损失反向传播()
这就是高层次的版本。我一会儿会回到细节,但关键是我们传入那个随机生成的图像的损失函数——优化图像的变量。因此,我们将该图像传递给我们的损失函数,它将使用损失函数进行更新,而损失函数是通过将我们当前的优化图像通过我们的 VGG 获取中间激活,并将其与目标激活进行比较来计算均方误差损失。我们运行一堆次数,然后将其打印出来。我们有我们的鸟,但没有它的表示形式。
targ_t = m_vgg(VV(img_tfm[None]))
targ_v = V(targ_t)
targ_t.shape
'''
torch.Size([1, 512, 18, 18])
'''
max_iter = 1000
show_iter = 100
optimizer = optim.LBFGS([opt_img_v], lr=0.5)
Broyden–Fletcher–Goldfarb–Shanno(BFGS)
这里有一些新的细节。其中一个是一个奇怪的优化器(optim.LBFGS)。任何完成过某些数学和计算机科学课程的人进入深度学习领域都会发现我们使用像 Adam 和 SGD 这样的东西,并且总是假设该领域的人对计算机科学一无所知,立即说“你们有人尝试过使用 BFGS 吗?”实际上,我们并没有使用来训练神经网络的完全不同类型的优化算法的长期历史。当然,事实上,那些花了几十年研究神经网络的人确实对计算机科学有所了解,结果表明这些技术整体上并不工作得很好。但实际上,这对我们来说会很有效,并且这是一个很好的机会,让那些在学校没有学习过这种类型的优化算法的人了解一个有趣的算法。BFGS(四个不同人的首字母缩写),L 代表有限内存。它是一个优化器,也就是说,有一些损失函数,它将使用一些梯度(并非所有优化器都使用梯度,但我们使用的所有优化器都会)来找到一个方向,并尝试通过调整一些参数使损失函数降低。它只是一个优化器。但它是一种有趣的优化器,因为它在每一步上做的工作比我们习惯的要多一点。具体来说,它的工作方式与我们习惯的方式相同,即我们只是选择一个起点,而在这种情况下,我们选择了一个随机图像,正如你所看到的。像往常一样,我们计算梯度。但我们不仅仅是采取一步,而是实际上在找到梯度的同时,我们还尝试找到二阶导数。二阶导数表示梯度变化的速度。
梯度:函数变化的速度
二阶导数:梯度变化的速度
换句话说,它有多曲折?基本思想是,如果你知道它不太曲折,那么你可能可以跳得更远。但如果它非常曲折,那么你可能不想跳得太远。因此,在更高维度中,梯度被称为雅可比矩阵,而二阶导数被称为海森矩阵。你会经常看到这些词,但它们的意思就是这样。再次强调,数学家们也必须为每件事发明新词。他们就像深度学习研究人员一样——也许有点傲慢。使用 BFGS,我们将尝试计算二阶导数,然后我们将使用它来确定前进的方向和距离——因此,这不是对未知领域的一次疯狂跳跃。
现在的问题是,实际计算 Hessian(二阶导数)几乎肯定不是一个好主意。因为在你要前进的每个可能方向上,对于你测量梯度的每个方向,你还必须在每个方向上计算 Hessian。这变得非常庞大。所以我们不是真的计算它,我们走几步,基本上看一下梯度在每一步变化了多少,然后用那个小函数来近似 Hessian。再次强调,这似乎是一个非常明显的事情,但直到后来有人想到了,这花了相当长的时间。跟踪每一步都需要大量内存,所以别跟踪每一步,只保留最后的十步或二十步。第二部分,就是 L 到 LBFGS。有限内存的 BFGS 意味着保留最后的 10 或 20 个梯度,用它来近似曲率的量,然后用曲率和梯度来估计前进的方向和距离。在深度学习中通常不是一个好主意,有很多原因。这比 Adam 或 SGD 更新更费力,也使用更多内存,当你有一个 GPU 来存储和数亿个权重时,内存就成了一个更大的问题。但更重要的是,小批量是非常颠簸的,所以弄清楚曲率以决定到底要前进多远,有点像我们说的磨亮了粪便(是的,澳大利亚和英国的表达方式,你懂的)。有趣的是,实际上使用二阶导数信息,结果就像是一个吸引鞍点的磁铁。因此,有一些有趣的理论结果基本上说,如果使用二阶导数信息,它实际上会把你引向函数的恶劣平坦区域。所以通常不是一个好主意。
def actn_loss(x):
return F.mse_loss(m_vgg(x), targ_v)*1000
def step(loss_fn):
global n_iter
optimizer.zero_grad()
loss = loss_fn(opt_img_v)
loss.backward()
n_iter+=1
if n_iter%show_iter==0:
print(f'Iteration: n_iter, loss: **{loss.data[0]}**')
return loss
但在这种情况下,我们不是在优化权重,而是在优化像素,所以所有规则都改变了,实际上 BFGS 是有意义的。因为每次它做更多的工作,它是一种不同类型的优化器,PyTorch 中的 API 也有点不同。正如你在这里看到的,当你说optimizer.step时,你实际上传入了损失函数。所以我们的损失函数是调用step,传入一个特定的损失函数,即我们的激活损失(actn_loss)。在循环内部,你不会说 step,step,step。而是看起来像这样。所以有点不同,你可以尝试重写这个来使用 SGD,它仍然会工作。只是会花更长的时间,我还没有尝试过用 SGD,我很想知道它需要多长时间。
n_iter=0
while n_iter <= max_iter:
optimizer.step(partial(step,actn_loss))
'''
Iteration: n_iter, loss: 0.8466196656227112
Iteration: n_iter, loss: 0.34066855907440186
Iteration: n_iter, loss: 0.21001280844211578
Iteration: n_iter, loss: 0.15562333166599274
Iteration: n_iter, loss: 0.12673595547676086
Iteration: n_iter, loss: 0.10863320529460907
Iteration: n_iter, loss: 0.0966048613190651
Iteration: n_iter, loss: 0.08812198787927628
Iteration: n_iter, loss: 0.08170554041862488
Iteration: n_iter, loss: 0.07657770067453384
'''
所以你可以看到损失函数在下降。我们的 VGG 模型第 37 层的激活与目标激活之间的均方误差,记住目标激活是应用于我们的鸟的 VGG。明白了吗?所以现在我们有了一个内容损失。现在,关于这个内容损失,我要说的一件事是我们不知道哪一层会起到最好的作用。所以如果我们能多做一些实验就好了。现在的情况很烦人:
也许我们甚至想使用多个层。所以,与其截断我们想要的层之后的所有层,不如我们能够以某种方式抓取几个层的激活值。现在,我们已经知道一种方法可以在我们做 SSD 时做到这一点,我们实际上编写了一个具有多个输出的网络。记得吗?不同的卷积层,我们吐出了一个不同的oconv东西?但我真的不想去添加到 torch.vision ResNet 模型中,特别是如果以后我想尝试 torch.vision VGG 模型,然后我想尝试 NASNet-A 模型,我不想去修改它们的输出。此外,我希望能够轻松地按需打开和关闭某些激活。所以我们之前简要提到过这个想法,PyTorch 有这些名为 hooks 的奇妙东西。您可以有前向钩子,让您将任何您喜欢的东西插入到计算的前向传递中,或者有后向钩子,让您将任何您喜欢的东西插入到后向传递中。所以我们将创建世界上最简单的前向钩子。
x = val_tfms.denorm(np.rollaxis(to_np(opt_img_v.data),1,4))[0]
plt.figure(figsize=(7,7))
plt.imshow(x);
前向钩子[1:29:42]
这是几乎没有人知道的事情之一,因此几乎在互联网上找到的任何实现风格转移的代码都会有各种可怕的黑客,而不是使用前向钩子。但前向钩子真的很容易。
要创建一个前向钩子,只需创建一个类。该类必须有一个名为hook_fn的东西。您的钩子函数将接收您挂钩的module,前向传递的input和output,然后您可以做任何您喜欢的事情。所以我要做的就是将这个模块的输出存储在某个属性中。就是这样。所以hook_fn实际上可以被称为您喜欢的任何东西,但“hook function”似乎是标准,因为您可以看到,在构造函数中发生的是我在某个属性中存储了m.register_forward_hook的结果(m将是我要挂钩的层),并传入您希望在调用模块的前向方法时调用的函数。当调用其前向方法时,它将调用self.hook_fn,该函数将在名为features的属性中存储输出。
class SaveFeatures():
features=None
def __init__(self, m):
self.hook = m.register_forward_hook(self.hook_fn)
def hook_fn(self, module, input, output):
self.features = output
def close(self):
self.hook.remove()
现在我们可以像以前一样创建一个 VGG。让我们将其设置为不可训练,这样我们就不会浪费时间和内存来计算梯度。让我们遍历并找到所有的最大池层。让我们遍历这个模块的所有子层,如果是一个最大池层,让我们输出索引减 1——这样就会给我最大池之前的层。通常,最大池或步长 2 卷积之前的层是一个非常完整的表示,因为下一层正在改变网格。所以这对我来说是一个很好的地方来获取内容损失。我们在该网格大小上拥有的最语义化、最有趣的内容。这就是为什么我要选择这些索引。
m_vgg = to_gpu(vgg16(True)).eval()
set_trainable(m_vgg, False)
这些是 VGG 中每个最大池之前的最后一层的索引[1:32:30]。
block_ends = [
i-1 for i,o in enumerate(children(m_vgg))
if isinstance(o,nn.MaxPool2d)
]
block_ends
'''
[5, 12, 22, 32, 42]
'''
我要获取32——没有特定的原因,只是尝试其他东西。所以我要说block_ends[3](即 32)。children(m_vgg)[block_ends[3]]会给我 VGG 的第 32 层作为一个模块。
sf = SaveFeatures(children(m_vgg)[block_ends[3]])
然后,如果我调用SaveFeatures构造函数,它会执行:
self.hook = {VGG 的第 32 层}.register_forward_hook(self.hook_fn)
现在,每当我对这个 VGG 模型进行前向传递时,它都会将第 32 层的输出存储在sf.features中。
def get_opt():
opt_img = np.random.uniform(
0, 1,
size=img.shape
).astype(np.float32)
opt_img = scipy.ndimage.filters.median_filter(opt_img, [8,8,1])
opt_img_v = V(val_tfms(opt_img/2)[None], requires_grad=True)
return opt_img_v, optim.LBFGS([opt_img_v])
opt_img_v, optimizer = get_opt()
在这里[1:33:33],我调用了我的 VGG 网络,但我没有将其存储在任何地方。我没有说activations = m_vgg(VV(img_tfm[**None**]))。我调用它,丢弃答案,然后抓取我们在SaveFeatures对象中存储的特征。
m_vgg() — 这是在 PyTorch 中进行前向路径的方法。你不会说 m_vgg.forward(),你只是将其用作可调用。在 nn.module 上使用可调用会自动调用 forward。这就是 PyTorch 模块的工作方式。
所以我们称之为可调用的,最终调用我们的前向钩子,前向钩子将激活存储在 sf.features 中,所以现在我们有了我们的目标变量 — 就像以前一样,但以一种更加灵活的方式。
get_opt 包含了我们之前的相同的 4 行代码[1:34:34]。它只是给我一个要优化的随机图像和一个优化器来优化该图像。
m_vgg(VV(img_tfm[None]))
targ_v = V(sf.features.clone())
targ_v.shape
'''
torch.Size([1, 512, 36, 36])
'''
def actn_loss2(x):
m_vgg(x)
out = V(sf.features)
return F.mse_loss(out, targ_v)*1000
现在我可以继续做完全相同的事情。但现在我将使用不同的损失函数 actn_loss2(激活损失 #2),它不会说 out=m_vgg,再次,它调用 m_vgg 进行前向传递,丢弃结果,并获取 sf.features。所以现在这是我的第 32 层激活,然后我可以在其上执行均方误差损失。你可能已经注意到,最后一个损失函数和这个都乘以了一千。为什么它们乘以一千?这就像所有试图使这个课程不正确的事情。我以前没有使用一千,它就无法训练。今天午餐时间,什么都不起作用。经过几天的尝试让这个东西工作,最终偶然注意到“天哪,损失函数的数字真的很低(如 10E-7)”,我想如果它们不那么低会怎样。所以我将它们乘以一千,然后它开始工作了。那为什么它不起作用呢?因为我们正在使用单精度浮点数,而单精度浮点数并不那么精确。特别是当你得到的梯度有点小,然后你乘以学习率可能也很小,最终得到一个很小的数字。如果它太小,它们可能会被四舍五入为零,这就是发生的事情,我的模型还没有准备好。我相信有比乘以一千更好的方法,但无论如何。它运行得很好。无论你将损失函数乘以多少,因为你关心的只是它的方向和相对大小。有趣的是,这与我们在训练 ImageNet 时所做的事情类似。我们使用了半精度浮点数,因为 Volta 张量核要求如此。如果你想要训练半精度浮点数,实际上你必须将损失函数乘以一个缩放因子。我们使用了 1024 或 512。我认为 fast.ai 现在是第一个具有所有必要技巧以在半精度浮点数中进行训练的库,因此如果你有幸拥有 Volta 或者你可以支付 AWS P3,如果你有一个学习对象,你只需说 learn.half,它现在就会神奇地正确地训练半精度浮点数。它也内置在模型数据对象中,一切都是自动的。我相信没有其他库能做到这一点。
n_iter=0
while n_iter <= max_iter:
optimizer.step(partial(step,actn_loss2))
'''
Iteration: n_iter, loss: 0.2112911492586136
Iteration: n_iter, loss: 0.0902421623468399
Iteration: n_iter, loss: 0.05904778465628624
Iteration: n_iter, loss: 0.04517251253128052
Iteration: n_iter, loss: 0.03721420466899872
Iteration: n_iter, loss: 0.03215853497385979
Iteration: n_iter, loss: 0.028526008129119873
Iteration: n_iter, loss: 0.025799645110964775
Iteration: n_iter, loss: 0.02361033484339714
Iteration: n_iter, loss: 0.021835438907146454
'''
这只是在稍早的层上做同样的事情[1:37:35]。这只是让鸟看起来更像鸟。希望你能理解,较早的层越接近像素。有更多的网格单元,每个单元更小,更小的感受野,更简单的语义特征。所以我们越早得到,它看起来就越像一只鸟。
x = val_tfms.denorm(np.rollaxis(to_np(opt_img_v.data),1,4))[0]
plt.figure(figsize=(7,7))
plt.imshow(x);
sf.close()
事实上,这篇论文有一张很好的图片展示了各种不同的层,并放大到这座房子[1:38:17]。他们试图让这座房子看起来像《星夜》的图片。你可以看到后来,它变得非常混乱,而之前看起来像这座房子。所以这只是在做我们刚刚做的事情。我在我们的学习小组中注意到的一件事是,每当我告诉某人回答一个问题,每当我说去读这篇论文中有一些东西告诉你问题的答案时,总会有一种震惊的表情“读这篇论文?我?”但是说真的,论文已经做了这些实验并绘制了这些图片。论文中有很多东西。这并不意味着你必须读完论文的每一部分。但至少看看图片。所以看看 Gatys 的论文,里面有很好的图片。所以他们已经为我们做了实验,但看起来他们没有深入研究 — 他们只是得到了一些早期的结果。
风格匹配[1:39:29]
我们接下来需要做的是创建风格损失。我们已经有了损失,即它有多像鸟。现在我们需要知道它有多像这幅绘画的风格。我们将做几乎相同的事情。我们将获取某一层的激活。现在问题是,某一层的激活,假设它是一个 5x5 的层(当然没有 5x5 的层,它是 224x224,但我们假装)。这里是一些激活,我们可以获取这些激活,无论是针对我们正在优化的图像还是我们的梵高绘画。让我们看看我们的梵高绘画。这就是它 —《星夜》
style_fn = PATH/'style'/'starry_night.jpg'
style_img = open_image(style_fn)
style_img.shape, img.shape
'''
((1198, 1513, 3), (291, 483, 3))
'''
plt.imshow(style_img);
我从维基百科下载了这幅图像,我想知道为什么加载如此缓慢[1:40:39] — 结果,我下载的维基百科版本是 30,000 x 30,000 像素。他们有这种严肃的画廊品质存档真的很酷。我不知道这个存在。不要试图在上面运行神经网络。完全毁了我的 Jupyter 笔记本。
所以我们可以为我们的梵高图像做到这一点,也可以为我们的优化图像做到这一点。然后我们可以比较这两者,最终我们会创建一幅内容类似于绘画但并非绘画的图像 — 这不是我们想要的。我们想要的是具有相同风格但不是绘画且没有内容的东西。所以我们想要丢弃所有的空间信息。我们不想要创造出一个这里有月亮,这里有星星,这里有教堂的东西。我们不想要任何这些。那么我们如何丢弃所有的特殊信息呢?
在这种情况下,这里有 19 个面 - 19 个切片。所以让我们拿到这个顶部切片,这将是一个 5x5 矩阵。现在,让我们展平它,我们得到一个 25 个元素的长向量。一下子,我们通过展平抛弃了大部分空间信息。现在让我们拿到第二个切片(即另一个通道)并做同样的事情。所以我们有通道 1 展平和通道 2 展平,它们都有 25 个元素。现在,让我们进行点积,我们可以在 Numpy 中用 @ 来做(注:这里是 Jeremy 对我的点积与矩阵乘法问题的回答)。点积将给我们一个数字。那个数字是什么?它告诉我们什么?假设激活在 VGG 网络的中间层附近,我们可能期望其中一些激活是笔触纹理有多强,一些是这个区域有多明亮,一些是这部分是房子的一部分还是圆形的一部分,或者其他部分是这幅画的哪部分有多暗。所以点积基本上是一个相关性。如果这个元素和这个元素都非常正或都非常负,它会给我们一个大结果。另外,如果它们相反,它会给一个小结果。如果它们都接近零,它不会给结果。所以基本上点积是衡量这两个东西有多相似的一个指标。所以如果通道 1 和通道 2 的激活相似,那么它基本上是在说 - 让我们举个例子[1:44:28]。比如第一个是笔触纹理有多强(C1),而另一个是笔触有多倾斜(C2)。
如果细胞(1,1)的 C1 和 C2 同时高,细胞(4,2)也是如此,那么它表明具有纹理的网格单元也倾向于具有对角线。因此,当具有纹理的网格单元也具有对角线时,点积会很高,当它们没有时,点积也不高。所以这就是 C1 @ C2。另外,C1 @ C1 实际上是 2-范数(即 C1 的平方和)。这基本上是在说纹理通道中有多少网格单元是活跃的,以及它们有多活跃。换句话说,C1 @ C1 告诉我们纹理绘画进行了多少。而 C2 @ C2 告诉我们对角线绘画进行了多少。也许 C3 是“颜色是否明亮?”,所以 C3 @ C3 将告诉我们明亮颜色单元有多频繁。
那么我们可以创建一个包含每个点积的 19x19 矩阵[1:47:17]。就像我们讨论过的,数学家们必须给每样东西起个名字,所以这个特定的矩阵,将其展平然后进行所有点积的操作,被称为 Gram 矩阵。
我告诉你一个秘密[1:48:29]。大多数深度学习从业者要么不知道,要么不记得所有这些东西,比如如果他们曾经在大学学过 Gram 矩阵。他们可能忘记了,因为之后他们可能熬夜了。实际上的工作方式是你意识到“哦,我可以创建一个非空间表示,展示通道之间的相关性”,然后当我写论文时,我不得不去问周围的人,“这个东西有个名字吗?” 然后有人会说“这不就是 Gram 矩阵吗?” 你去查一下,确实是。所以不要认为你必须先学习所有的数学。先运用你的直觉和常识,然后再担心数学叫什么,通常是这样。有时候也会反过来,不过不是对我,因为我不擅长数学。
所以这被称为 Gram 矩阵。当然,如果你是一个真正的数学家,非常重要的是你要说得好像你一直知道这是一个 Gram 矩阵,然后你就会说,哦是的,我们只是计算 Gram 矩阵。所以 Gram 矩阵就是这种映射——对角线可能是最有趣的部分。对角线显示哪些通道最活跃,然后非对角线显示哪些通道倾向于一起出现。总的来说,如果两幅图片有相同的风格,那么我们期望某些激活层会有相似的 Gram 矩阵。因为如果我们找到了捕捉很多关于画笔笔触和颜色的东西的激活层,那么仅仅对角线(在 Gram 矩阵中)可能就足够了。这是另一个有趣的作业,如果有人想尝试的话,可以尝试使用 Gatys 的风格迁移,而不是使用 Gram 矩阵,而是只使用 Gram 矩阵的对角线。这只需要改变一行代码。但我还没有看到有人尝试过,也不知道它是否会起作用,但它可能会很好。
“好的,是的,克里斯汀,你已经尝试过了。”“我已经尝试过了,大多数时候都有效,除非你有需要两种风格出现在同一个地方的有趣图片。所以看起来像是一半是草,一半是人群,你需要这两种风格。”(克里斯汀)。很酷,你仍然会做你的作业,但克里斯汀说她会替你做。
def scale_match(src, targ):
h,w,_ = img.shape
sh,sw,_ = style_img.shape
rat = max(h/sh,w/sw); rat
res = cv2.resize(style_img, (int(sw*rat), int(sh*rat)))
return res[:h,:w]
style = scale_match(img, style_img)
plt.imshow(style)
style.shape, img.shape
'''
((291, 483, 3), (291, 483, 3))
'''
这是我们的绘画。我尝试调整绘画的大小,使其与我的鸟类图片大小相同。所以这就是所有这些在做的事情。不管我使用哪一部分,只要它有很多漂亮的风格就可以了。
我像以前一样获取了我的优化器和随机图像:
opt_img_v, optimizer = get_opt()
这一次,我为所有的block_ends调用SaveFeatures,这将给我一个 SaveFeatures 对象的数组——每个模块都会出现在最大池化之前的层中。因为这一次,我想玩弄不同的激活层风格,更具体地说,我想让你来玩。所以现在我有了一个完整的数组。
sfs = [SaveFeatures(children(m_vgg)[idx]) for idx in block_ends]
style_img是我的梵高的绘画。所以我拿我的style_img,通过我的转换来创建我的转换风格图像(style_tfm)。
style_tfm = val_tfms(style_img)
将其转换为一个变量,通过我的 VGG 模块的前向传播,现在我可以遍历所有的 SaveFeatures 对象并获取每组特征。请注意,我调用clone,因为以后,如果我再次调用我的 VGG 对象,它将替换这些内容。我还没有想过这是否有必要。如果你把它拿走了,那没关系。但我只是小心翼翼。现在这是每个block_end层的激活的数组。在这里,你可以看到所有这些形状:
m_vgg(VV(style_tfm[None]))
targ_styles = [V(o.features.clone()) for o in sfs]
[o.shape for o in targ_styles]
'''
[torch.Size([1, 64, 288, 288]),
torch.Size([1, 128, 144, 144]),
torch.Size([1, 256, 72, 72]),
torch.Size([1, 512, 36, 36]),
torch.Size([1, 512, 18, 18])]
'''
你可以看到,能够快速地编写一个列表推导式在你的 Jupyter 玩耍中非常重要。因为你真的希望能够立即看到这是我的通道(64、128、256,...),以及我们期望的网格大小减半(288、144、72...),因为所有这些都出现在最大池化之前。
因此,要进行 Gram MSE 损失,它将是输入的 Gram 矩阵与目标的 Gram 矩阵的 MSE 损失。Gram 矩阵只是x与x转置(x.t())的矩阵乘积,其中 x 简单地等于我已经将批处理和通道轴全部展平的输入。我只有一个图像,所以可以忽略批处理部分——基本上是通道。然后其他所有部分(-1),在这种情况下是高度和宽度,是另一个维度,因为现在将是通道乘以高度和宽度,然后正如我们讨论过的,我们可以将其与其转置进行矩阵乘积。为了归一化,我们将其除以元素的数量(b*c*h*w)——如果我说input.numel(元素的数量)会更优雅,这将是相同的事情。再次,这给我了很小的数字,所以我乘以一个大数字使其变得更合理。所以这基本上就是我的损失。
def gram(input):
b,c,h,w = input.size()
x = input.view(b*c, -1)
return torch.mm(x, x.t())/input.numel()*1e6
def gram_mse_loss(input, target):
return F.mse_loss(gram(input), gram(target))
现在我的风格损失是将我的图像优化,通过 VGG 前向传递,获取所有 SaveFeatures 对象中特征的数组,然后在每一层上调用我的 Gram MSE 损失。这将给我一个数组,然后我只需将它们相加。现在你可以用不同的权重将它们相加,你可以添加子集,或者其他。在这种情况下,我只是获取了所有的。
def style_loss(x):
m_vgg(opt_img_v)
outs = [V(o.features) for o in sfs]
losses = [gram_mse_loss(o, s) for o,s in zip(outs, targ_styles)]
return sum(losses)
像以前一样将其传递给我的优化器:
n_iter=0
while n_iter <= max_iter:
optimizer.step(partial(step,style_loss))
'''
Iteration: n_iter, loss: 230718.453125
Iteration: n_iter, loss: 219493.21875
Iteration: n_iter, loss: 202618.109375
Iteration: n_iter, loss: 481.5616760253906
Iteration: n_iter, loss: 147.41177368164062
Iteration: n_iter, loss: 80.62625122070312
Iteration: n_iter, loss: 49.52326965332031
Iteration: n_iter, loss: 32.36254119873047
Iteration: n_iter, loss: 21.831811904907227
Iteration: n_iter, loss: 15.61091423034668
'''
这里有一张随机图像,风格类似于梵高,我觉得挺酷的。
x = val_tfms.denorm(np.rollaxis(to_np(opt_img_v.data),1,4))[0]
plt.figure(figsize=(7,7))
plt.imshow(x);
再次,Gatys 已经为我们做好了。这里是不同层次的随机图像,风格类似于梵高。所以第一个,你可以看到,激活是简单的几何图形——一点也不有趣。后面的层次更有趣。所以我们有一种怀疑,我们可能想要主要使用后面的层次来进行风格损失,如果我们想要看起来好的话。
我添加了这个SaveFeatures.close,它只是调用self.hook.remove()。记住,我将 hook 存储为self.hook,所以hook.remove()会将其删除。最好将其删除,否则可能会一直使用内存。因此,在最后,我只需遍历每个 SaveFeatures 对象并关闭它:
for sf in sfs: sf.close()
风格转移
风格转移是将内容损失和风格损失加在一起,并加上一些权重。所以没有太多可以展示的。
获取我的优化器,获取我的图像:
opt_img_v, optimizer = get_opt()
我的综合损失是一个特定层次的 MSE 损失,我所有层次的风格损失,将风格损失相加,加到内容损失上,我正在缩放内容损失。实际上,我已经将风格损失缩放为 1E6。所以它们都被精确地缩放了。将它们加在一起。再次,你可以尝试对不同的风格损失进行加权,或者你可以删除其中一些,所以这是最简单的版本。
def comb_loss(x):
m_vgg(opt_img_v)
outs = [V(o.features) for o in sfs]
losses = [gram_mse_loss(o, s) for o,s in zip(outs, targ_styles)]
cnt_loss = F.mse_loss(outs[3], targ_vs[3])*1000000
style_loss = sum(losses)
return cnt_loss + style_loss
训练它:
n_iter=0
while n_iter <= max_iter:
optimizer.step(partial(step,comb_loss))
'''
Iteration: n_iter, loss: 1802.36767578125
Iteration: n_iter, loss: 1163.05908203125
Iteration: n_iter, loss: 961.6024169921875
Iteration: n_iter, loss: 853.079833984375
Iteration: n_iter, loss: 784.970458984375
Iteration: n_iter, loss: 739.18994140625
Iteration: n_iter, loss: 706.310791015625
Iteration: n_iter, loss: 681.6689453125
Iteration: n_iter, loss: 662.4088134765625
Iteration: n_iter, loss: 646.329833984375
'''
x = val_tfms.denorm(np.rollaxis(to_np(opt_img_v.data),1,4))[0]
plt.figure(figsize=(9,9))
plt.imshow(x, interpolation='lanczos')
plt.axis('off');
for sf in sfs: sf.close()
天啊,它看起来真的很好。所以我觉得这很棒。这里的主要要点是,如果你想用神经网络解决问题,你所要做的就是设置一个损失函数,然后优化某些东西。而损失函数是一个较低的数字是你更满意的东西。因为当你优化它时,它会使那个数字尽可能低,它会做你想要它做的事情。所以在这里,Gatys 提出了一个损失函数,当它看起来像我们想要的东西时,它会是一个较小的数字,看起来像我们想要的风格。这就是我们所要做的。
实际上,除了实现了 Gram MSE 损失,这只是 6 行代码,这就是我们的损失函数:
将其传递给我们的优化器,等大约 5 秒钟,我们就完成了。记住,我们可以一次处理一批,所以我们可以等待 5 秒钟,64 个就完成了。所以我认为这真的很有趣,自从这篇论文发表以来,它确实激发了很多有趣的工作。不过对我来说,大部分有趣的工作还没有发生,因为对我来说,有趣的工作是将人类创造力与这些工具结合起来的工作。我还没有看到可以下载或使用的工具,艺术家可以控制并可以以交互方式进行操作。与Google Magenta项目的人交谈很有趣,这是他们的创意人工智能项目,他们在音乐方面所做的一切都是关于这个的。它正在构建音乐家可以实时使用的工具。由于 Magenta 的存在,您将在音乐领域看到更多这样的东西。如果您访问他们的网站,您会看到各种按键,可以实际更改鼓点、旋律、音调等。您肯定会看到 Adobe 或 Nvidia 开始发布一些小型原型并开始这样做,但这种创意人工智能的爆发尚未发生。我认为我们已经拥有了我们所需的所有技术,但没有人将其整合到一起并说“看看我建造的东西,看看人们用我的东西建造的东西”。所以这只是一个巨大的机会领域。
所以我在课堂开始时提到的那篇论文[2:01:16] ——基本上是将美国队长的盾牌添加到任意绘画中使用了这种技术。不过,诀窍是通过一些微小的调整使粘贴的美国队长盾牌能够很好地融入其中。但那篇论文只有几天的历史,所以尝试这个项目将是一个非常有趣的项目,因为您可以使用所有这些代码。它确实利用了这种方法。然后,您可以从使内容图像类似于带有盾牌的绘画开始,然后样式图像可以是不带盾牌的绘画。这将是一个很好的开始,然后您可以看看他们在这篇论文中尝试解决的具体问题,以使其更好。但您现在可以开始。
问题:之前有很多人表达了对 Pyro 和概率编程的兴趣。所以 TensorFlow 现在有了这个 TensorFlow 概率或其他东西。有很多概率编程框架。我认为它们很有趣,但至今未经证明,因为我还没有看到任何使用概率编程系统完成的事情,而没有使用它们更好。基本前提是它允许你创建更多关于你认为世界是如何运作的模型,然后插入参数。所以 20 年前当我还在管理咨询行业工作时,我们经常使用电子表格,然后我们会使用这些蒙特卡洛模拟插件——有一个叫做 At Risk(?),一个叫做 Crystal Ball。我不知道几十年后它们是否还存在。基本上它们让你可以更改电子表格中的一个单元格,说这不是一个具体的值,而实际上代表一个具有这个均值和标准差的值分布,或者它有这个分布,然后你会点击一个按钮,电子表格会从这些分布中随机抽取一千次数字重新计算,并显示你的结果的分布,可能是利润或市场份额或其他什么。那时我们经常使用它们。显然,人们认为电子表格是做这种工作的更明显的地方,因为你可以更自然地看到所有这些,但我不知道。我们将看到。在这个阶段,我希望它能够被证明有用,因为我觉得它非常吸引人,符合我过去经常做的工作。实际上,围绕这种东西有整个实践,他们过去称之为系统动力学,这实际上是建立在这种东西之上的,但它并没有走得太远。
问题:然后有一个关于通用风格转移预训练的问题。我不认为你可以为通用风格进行预训练,但你可以为特定风格的通用照片进行预训练,这就是我们要达到的目标。尽管可能最终会成为一项作业。我还没有决定。但我会做所有的部分。
问题:请让他谈谈多 GPU。哦,是的,我还没有关于那个的幻灯片。我们马上就要谈到了。
在我们开始之前,再分享一张来自 Gatys 论文的有趣图片。他们有更多图片,只是没有适合我的幻灯片,但是有不同的卷积层用于风格。不同的风格和内容比例,这里是不同的图片。显然这不再是梵高的风格,这是一个不同的组合。所以你可以看到,如果你只做风格,你看不到任何图片。如果你做很多内容,但是使用足够低的卷积层,看起来还可以,但背景有点愚蠢。所以你可能想要在中间某个地方。所以你可以尝试一下,做一些实验,但也可以使用论文来帮助指导你。
数学
实际上,我想现在开始研究数学,下周我们将讨论多 GPU 和超分辨率,因为这是来自论文的内容,我真的希望在我们讨论完论文后,你们能阅读论文,并在论坛上提出任何不清楚的问题。但这篇论文中有一部分我想谈谈,讨论如何解释它。所以论文说,我们将得到一个输入图像x,这个小东西通常表示它是一个向量,Rachel,但这个是一个矩阵。我猜它可能是两者之一。我不知道。通常小写粗体字母表示向量,或者带有上箭头的小写字母表示向量。通常大写字母表示矩阵,或者带有两个箭头的小写字母表示矩阵。在这种情况下,我们的图像是一个矩阵。我们基本上将其视为向量,所以也许我们只是在超前一步。
所以我们有一个输入图像x,它可以通过 CNN 的特定层中的滤波器响应(即激活)进行编码。滤波器响应就是激活。希望你们都能理解。CNN 的基本功能就是生成激活层。一个层有一堆滤波器,产生一定数量的通道。今年表示第 L 层有大写 Nl个滤波器。再次强调,这里的大写字母不代表矩阵。所以我不知道,数学符号是如此不一致。所以在第 L 层有 Nl 个不同的滤波器,这意味着也有同样数量的特征图。所以确保你能看到这个字母 Nl 和这个字母是一样的。所以你必须非常小心地阅读字母并认识到它就像啪一下,这个字母和那个字母是一样的。显然,Nl 个滤波器创建了 Nl 个特征图或通道,每个尺寸为 Ml(好吧,我看到这里正在发生展开)。这就像 numpy 符号中的 M[l]。这是第l层。所以 M 是第l层。尺寸是高度乘以宽度——所以我们将其展平。所以第 l 层的响应可以存储在矩阵 F 中(现在l在顶部,出于某种原因)。这不是 f^l,这只是另一个索引。我们只是为了好玩而移动它。这里我们说它是 R 的元素——这是一个特殊的 R,表示实数 N 乘以 M(这表示它的维度是 N 乘以 M)。这非常重要,不要继续。就像 PyTorch 一样,确保你首先理解维度的秩和大小,数学也是一样。这些是你停下来思考为什么是 N 乘以 M 的地方。N 是滤波器的数量,M 是高度乘以宽度。所以你还记得我们做.view(b*c, -1)的时候吗?这就是。所以尝试将代码映射到数学上。所以 F 是x:
如果我对你更友好,我会使用与论文相同的字母。但我太忙于让这个该死的东西运行起来,无法仔细做到这一点。所以你可以回去将其重命名为大写 F。
所以我们将 L 移到顶部是因为我们现在要有更多的索引。在 Numpy 或 PyTorch 中,我们通过方括号索引事物,然后用逗号分隔很多东西。在数学中的方法是用小写字母围绕你的字母——到处都扔上去。所以这里,Fl是 F 的第l层,然后ij是第l层中第i个滤波器在第j位置的激活。所以位置j的大小是 M,即高度乘以宽度的大小。这是容易混淆的事情。通常你会看到一个ij,然后假设它是在图像的高度乘以宽度的位置进行索引,但实际上不是,对吧?它是在通道中对展平图像的第i个滤波器/通道的第j个位置进行索引。它甚至告诉你——它是第l层中展平图像中第j个位置的第i个滤波器/通道。所以除非你理解 F 是什么,否则你将无法进一步阅读论文。这就是为什么这些是你停下来确保你感到舒适的地方。
所以现在,内容损失,我不会花太多时间,但基本上我们只是要检查激活值与预测值的平方[2:12:03]。所以这就是我们的内容损失。风格损失将是类似的,但使用格拉姆矩阵 G:
我真的很想向你展示这个。我觉得这很棒。有时我真的喜欢数学符号中可以做的事情,它们也是你通常可以在 J 和 APL 中做的事情,这种隐式循环正在这里进行。这是在说什么呢?嗯,它在说我的层l中的格拉姆矩阵,对于一个轴上的第i个位置和另一个轴上的第j个位置等于我的 F 矩阵(所以我的展平矩阵)对于该层中的第i个通道与同一层中的第j个通道,然后我将进行求和。我们将取第k个位置并将它们相乘然后将它们全部加起来。所以这正是我们之前计算格拉姆矩阵时所做的事情。所以这里发生了很多事情,因为对我来说,这是非常巧妙的符号 —— 有三个隐式循环同时进行,加上求和中的一个显式循环,然后它们一起工作来为每一层创建这个格拉姆矩阵。所以让我们回去看看你是否能匹配这个。所以所有这一切都同时发生,这非常棒。
就是这样。所以下周,我们将看到一个非常类似的方法,基本上再次进行风格转移,但这次我们实际上会训练一个神经网络来为我们做这件事,而不是进行优化。我们还将看到你可以做同样的事情来进行超分辨率。我们还将回顾一些 SSD 的内容,以及进行一些分割。所以如果你忘记了 SSD,这周可能值得进行一点复习。好的,谢谢大家。下周见。