fast.ai 深度学习笔记(六)(1)https://developer.aliyun.com/article/1482694
生成器
生成器也是一种架构,本身不会做任何事情,直到我们有损失函数和数据。但张量的秩和大小是什么?生成器的输入将是一个随机数向量。在论文中,他们称之为“先验”。有多大?我们不知道。这个想法是不同的一堆随机数将生成一个不同的卧室。因此,我们的生成器必须将一个向量作为输入,通过顺序模型,将其转换为一个秩为 4 的张量(没有批量维度的秩为 3)-高度乘以宽度乘以 3。因此,在最后一步,nc
(通道数)最终将变为 3,因为它将创建一个大小为 3 的通道图像。
class DeconvBlock(nn.Module): def __init__(self, ni, no, ks, stride, pad, bn=True): super().__init__() self.conv = nn.ConvTranspose2d( ni, no, ks, stride, padding=pad, bias=False ) self.bn = nn.BatchNorm2d(no) self.relu = nn.ReLU(inplace=True) def forward(self, x): x = self.relu(self.conv(x)) return self.bn(x) if self.bn else x class DCGAN_G(nn.Module): def __init__(self, isize, nz, nc, ngf, n_extra_layers=0): super().__init__() assert isize % 16 == 0, "isize has to be a multiple of 16" cngf, tisize = ngf//2, 4 while tisize!=isize: cngf*=2; tisize*=2 layers = [DeconvBlock(nz, cngf, 4, 1, 0)] csize, cndf = 4, cngf while csize < isize//2: layers.append(DeconvBlock(cngf, cngf//2, 4, 2, 1)) cngf //= 2; csize *= 2 layers += [ DeconvBlock(cngf, cngf, 3, 1, 1) for t in range(n_extra_layers) ] layers.append(nn.ConvTranspose2d(cngf, nc, 4, 2, 1, bias=False)) self.features = nn.Sequential(*layers) def forward(self, input): return F.tanh(self.features(input))
问题:在 ConvBlock 中,为什么批量归一化在 ReLU 之后(即self.bn(self.relu(...))
)?我通常期望先进行 ReLU,然后批量归一化,这实际上是 Jeremy 认为有意义的顺序。我们在 darknet 中使用的顺序是 darknet 论文中使用的顺序,所以每个人似乎对这些事情有不同的顺序。事实上,大多数人对 CIFAR10 有一个不同的顺序,即批量归一化→ReLU→卷积,这是一种奇特的思考方式,但事实证明,对于残差块来说,这通常效果更好。这被称为“预激活 ResNet”。有一些博客文章中,人们已经尝试了不同顺序的事物,似乎这很大程度上取决于特定数据集以及您正在处理的内容,尽管性能差异很小,除非是为了比赛,否则您不会在意。
反卷积
因此,生成器需要从一个向量开始,最终得到一个秩为 3 的张量。我们还不知道如何做到这一点。我们需要使用一种称为“反卷积”的东西,PyTorch 称之为转置卷积-相同的东西,不同的名称。反卷积是一种增加网格大小而不是减小网格大小的东西。因此,像所有事物一样,在 Excel 电子表格中最容易看到。
这是一个卷积。我们开始,假设有一个单通道的 4x4 网格单元。让我们通过一个单输出滤波器的 3x3 核心。所以我们有一个输入通道,一个滤波器核心,如果我们不添加任何填充,最终会得到 2x2。记住,卷积只是核心和适当网格单元的乘积的总和。所以这是我们标准的 3x3 卷积一个通道一个滤波器。
现在的想法是我们想要朝相反的方向发展。我们想要从我们的 2x2 开始,我们想要创建一个 4x4。具体来说,我们想要创建与我们开始的相同的 4x4。我们想通过使用卷积来实现这一点。我们如何做到这一点?
如果我们有一个 3x3 卷积,那么如果我们想要创建一个 4x4 输出,我们将需要创建这么多填充:
因为有这么多填充,我们最终会得到 4x4。所以假设我们的卷积滤波器只是一堆零,那么我们可以通过进行这个减法来计算每个单元格的错误:
然后我们可以通过对这些错误的绝对值求和来获得绝对值之和(L1 损失):
现在我们可以使用优化,在 Excel 中称为“求解器”来进行梯度下降。所以我们将设置总单元格等于最小值,然后尝试通过改变我们的滤波器来减少我们的损失。你可以看到它提出了一个滤波器,使得结果几乎像数据一样。它并不完美,一般来说,你不能假设反卷积可以完全创建出你想要的完全相同的东西,因为这里没有足够的。因为滤波器中有 9 个东西,结果中有 16 个东西。但它做出了一个相当不错的尝试。所以这就是反卷积的样子 - 在一个 2x2 的网格单元上进行步长为 1 的 3x3 反卷积。
问题: 创建一个鉴别器来识别假新闻和真实新闻有多难?你不需要任何特殊的东西 - 那只是一个分类器。所以你可以使用之前课程和第 4 课的 NLP 分类器。在这种情况下,没有生成部分,所以你只需要一个数据集,其中说这些是我们认为是假新闻的东西,这些是我们认为是真实新闻的东西,它应该工作得非常好。据我们所知,如果你尝试,你应该得到和其他人一样好的结果 - 它是否足够实用,Jeremy 不知道。在这个阶段,你能做的最好的事情可能是生成一种分类,说这些东西看起来相当可疑,基于它们的写作方式,然后一些人可以去核实它们。NLP 分类器和 RNN 不能核实事实,但它可以识别这些是以那种高度通俗的风格写成的,通常假新闻就是这样写的,所以也许这些值得关注。这可能是你在不依赖某种外部数据源的情况下所能希望的最好的结果。但重要的是要记住,鉴别器基本上只是一个分类器,你不需要任何特殊的技术,超出我们已经学会的 NLP 分类的范围。
ConvTranspose2d
在 PyTorch 中进行反卷积,只需说:
nn.ConvTranspose2d(ni, no, ks, stride, padding=pad, bias=False)
ni
: 输入通道的数量no
: 输出通道的数量ks
: 卷积核大小
它被称为 ConvTranspose 的原因是因为事实证明这与卷积的梯度计算是相同的。这就是为什么他们这样称呼它。
可视化
deeplearning.net/software/theano/tutorial/conv_arithmetic.html
左边的是我们刚刚看到的进行 2x2 反卷积。如果有一个步长为 2,那么你不仅在外面周围有填充,而且你实际上还需要在中间放填充。它们实际上并不是这样实现的,因为这样做很慢。在实践中,你会以不同的方式实现它们,但所有这些都是在幕后发生的,所以你不必担心。我们之前已经讨论过这个卷积算术教程,如果你对卷积仍然不熟悉,并且想要熟悉反卷积,这是一个很好的网站。如果你想看这篇论文,它是A guide to convolution arithmetic for deep learning。
DeconvBlock
看起来与 ConvBlock
几乎相同,只是多了一个 Transpose
。我们像以前一样进行卷积 → relu → 批量归一化,它有输入滤波器和输出滤波器。唯一的区别是步长为 2 意味着网格大小会加倍而不是减半。
问题:nn.ConvTranspose2d
和 nn.Upsample
似乎做着相同的事情,即从上一层扩展网格大小(高度和宽度)。我们可以说 nn.ConvTranspose2d
总是优于 nn.Upsample
吗,因为 nn.Upsample
仅仅是调整大小并用零或插值填充未知部分吗?不,不能。在 distill.pub 上有一篇名为 反卷积和棋盘伪影 的出色互动论文指出,我们现在正在做的事情极其不理想,但好消息是其他人都在这样做。
看一下这里,你能看到这些棋盘伪影吗?这些都来自实际论文,基本上他们注意到每一篇关于生成模型的论文都有这些棋盘伪影,他们意识到这是因为当您使用大小为三的内核的步幅 2 卷积时,它们会重叠。因此,一些网格单元会获得两倍的激活。
因此,即使您从随机权重开始,最终也会得到一个棋盘状的伪影。所以你越深入,情况就越糟。他们的建议没有那么直接,Jeremy 发现对于大多数生成模型,上采样更好。如果你使用 nn.Upsample
,基本上是在做池化的相反操作 —— 它说让我们用四个(2x2)网格单元替换这一个。有许多方法可以进行上采样 —— 一种方法是将所有内容复制到这四个单元格中,另一种方法是使用双线性或双三次插值。有各种技术可以尝试创建平滑的上采样版本,您可以在 PyTorch 中选择任何一种。如果您进行了 2x2 的上采样,然后正常的 3x3 卷积,这是另一种与 ConvTranspose 相同的操作方式 —— 它将网格大小加倍,并对其进行一些卷积运算。对于生成模型,这几乎总是效果更好。在 distil.pub 的出版物中,他们指出也许这是一个好方法,但他们没有直接说出来,而 Jeremy 会直接说出来。话虽如此,对于 GANS,他还没有取得太大的成功,他认为可能需要一些调整才能使其正常工作。问题在于在早期阶段,它没有产生足够的噪音。他尝试过使用上采样的版本,您可以看到噪音看起来并不是很嘈杂。下周当我们研究风格转移和超分辨率时,您将看到 nn.Upsample
真正发挥作用。
生成器,我们现在可以从向量开始。我们可以决定并说好,让我们不把它看作一个向量,而实际上是一个 1x1 的网格单元,然后我们可以将其转换为 4x4,然后是 8x8 等等。这就是为什么我们必须确保它是一个合适的倍数,以便我们可以创建出正确大小的东西。正如您所看到的,它正在做与之前完全相反的事情。它每次使单元格大小增加 2,直到达到我们想要的一半大小,然后最后我们再添加 n
个,步幅为 1。然后我们再添加一个 ConvTranspose 最终得到我们想要的大小,然后我们完成了。最后我们通过一个 tanh
,这将强制我们处于零到一的范围内,因为当然我们不希望输出任意大小的像素值。因此,我们有一个生成器架构,它输出一个给定大小的图像,具有正确数量的通道,值在零到一之间。
在这一点上,我们现在可以创建我们的模型数据对象。这些东西需要一段时间来训练,所以我们将其设置为 128x128(只是一个更快的便利方式)。因此,这将是输入的大小,但然后我们将使用转换将其转换为 64x64。
最近有更多的进展,试图将其提高到高分辨率大小,但它们仍然倾向于要求批量大小为 1 或大量的 GPU。所以我们试图做一些可以用单个消费者 GPU 完成的事情。这是一个 64x64 卧室的例子。
bs,sz,nz = 64,64,100 tfms = tfms_from_stats(inception_stats, sz) md = ImageClassifierData.from_csv( PATH, 'bedroom', CSV_PATH, tfms=tfms, bs=128, skip_header=False, continuous=True ) md = md.resize(128) x,_ = next(iter(md.val_dl)) plt.imshow(md.trn_ds.denorm(x)[0]);
将它们全部放在一起
我们将几乎所有事情都手动完成,所以让我们继续创建我们的两个模型 - 我们的生成器和鉴别器,正如你所看到的它们是 DCGAN,换句话说,它们是出现在这篇论文中的相同模块。值得回头看一下 DCGAN 论文,看看这些架构是什么,因为假定当你阅读 Wasserstein GAN 论文时,你已经知道这一点。
netG = DCGAN_G(sz, nz, 3, 64, 1).cuda() netD = DCGAN_D(sz, 3, 64, 1).cuda()
问题:如果我们想要在 0 到 1 之间的值,我们不应该使用 sigmoid 吗?像往常一样,我们的图像已经被归一化为范围从-1 到 1,因此它们的像素值不再在 0 到 1 之间。这就是为什么我们希望值从-1 到 1,否则我们将无法为鉴别器提供正确的输入。
所以我们有一个生成器和一个鉴别器,我们需要一个返回“先验”向量(即一堆噪音)的函数。我们通过创建一堆零来实现这一点。nz
是z
的大小 - 在我们的代码中经常看到一个神秘的字母,那是因为那是他们在论文中使用的字母。这里,z
是我们噪音向量的大小。然后我们使用正态分布生成 0 到 1 之间的随机数。这需要是一个变量,因为它将参与梯度更新。
def create_noise(b): return V(torch.zeros(b, nz, 1, 1).normal_(0, 1)) preds = netG(create_noise(4)) pred_ims = md.trn_ds.denorm(preds) fig, axes = plt.subplots(2, 2, figsize=(6, 6)) for i,ax in enumerate(axes.flat): ax.imshow(pred_ims[i])
这里是创建一些噪音并生成四个不同噪音片段的示例。
def gallery(x, nc=3): n,h,w,c = x.shape nr = n//nc assert n == nr*nc return ( x.reshape(nr, nc, h, w, c) .swapaxes(1,2) .reshape(h*nr, w*nc, c) )
我们需要一个优化器来更新我们的梯度。在 Wasserstein GAN 论文中,他们告诉我们使用 RMSProp:
我们可以很容易地在 PyTorch 中做到这一点:
optimizerD = optim.RMSprop(netD.parameters(), lr = 1e-4) optimizerG = optim.RMSprop(netG.parameters(), lr = 1e-4)
在论文中,他们建议使用学习率为 0.00005(5e-5
),我们发现1e-4
似乎有效,所以我们将其增加了一点。
现在我们需要一个训练循环:
为了更容易阅读
训练循环将经过我们选择的一些时代(这将是一个参数)。记住,当你手动完成所有事情时,你必须记住所有手动步骤:
- 当你训练模型时,你必须将模块设置为训练模式,并在评估时将其设置为评估模式,因为在训练模式下,批量归一化更新会发生,丢失会发生,在评估模式下,这两个事情会被关闭。
- 我们将从我们的训练数据加载器中获取一个迭代器
- 我们将看看我们需要经过多少步,然后我们将使用
tqdm
给我们提供一个进度条,然后我们将经过那么多步。
论文中算法的第一步是更新鉴别器(在论文中,他们称鉴别器为“评论家”,w
是评论家的权重)。所以第一步是训练我们的评论家一点点,然后我们将训练我们的生成器一点点,然后我们将回到循环的顶部。论文中的内部for
循环对应于我们代码中的第二个while
循环。
现在我们要做的是我们现在有一个随机的生成器。所以我们的生成器将生成看起来像噪音的东西。首先,我们需要教我们的鉴别器区分噪音和卧室之间的区别 - 你希望这不会太难。所以我们只是按照通常的方式做,但有一些小调整:
- 我们将获取一小批真实卧室照片,这样我们就可以从迭代器中获取下一批,将其转换为变量。
- 然后我们将计算损失——这将是鉴别器认为这看起来假的程度(“真实的看起来假吗?”)。
- 然后我们将创建一些假图像,为此我们将创建一些随机噪音,并将其通过我们的生成器,这个阶段它只是一堆随机权重。这将创建一个小批量的假图像。
- 然后我们将通过与之前相同的鉴别器模块来获取该损失(“假的看起来有多假?”)。记住,当你手动做所有事情时,你必须在循环中将梯度归零(
netD.zero_grad()
)。如果你忘记了这一点,请回到第 1 部分课程,我们从头开始做所有事情。 - 最后,总鉴别器损失等于真实损失减去假损失。
所以你可以在这里看到:
他们没有谈论损失,实际上他们只谈论了一个梯度更新。
在 PyTorch 中,我们不必担心获取梯度,我们只需指定损失并调用loss.backward()
,然后鉴别器的optimizer.step()
。有一个关键步骤,即我们必须将 PyTorch 模块中的所有权重(参数)保持在-0.01 和 0.01 的小范围内。为什么?因为使该算法工作的数学假设仅适用于一个小球。了解为什么这样是有趣的数学是有趣的,但这与这篇论文非常相关,了解它不会帮助你理解其他论文,所以只有在你感兴趣的情况下才去学习。Jeremy 认为这很有趣,但除非你对 GANs 非常感兴趣,否则这不会是你在其他地方会重复使用的信息。他还提到,在改进的 Wasserstein GAN 出现后,有更好的方法来确保你的权重空间在这个紧密球内,即惩罚梯度过高,所以现在有稍微不同的方法来做这个。但这行代码是关键贡献,它是使 Wasserstein GAN 成功的关键:
for p in netD.parameters(): p.data.clamp_(-0.01, 0.01)
在这之后,我们有一个可以识别真实卧室和完全随机糟糕生成的图像的鉴别器。现在让我们尝试创建一些更好的图像。所以现在将可训练的鉴别器设置为 false,将可训练的生成器设置为 true,将生成器的梯度归零。我们的损失再次是生成器的fw
(鉴别器)应用于一些更多的随机噪音。所以这与之前完全相同,我们对噪音进行生成,然后将其传递给鉴别器,但这次,可训练的是生成器,而不是鉴别器。换句话说,在伪代码中,更新的是θ,即生成器的参数。它接受噪音,生成一些图像,尝试弄清楚它们是假的还是真实的,并使用这些梯度来更新生成器的权重,而不是之前我们是根据鉴别器来获取梯度,并使用 RMSProp 和 alpha 学习率来更新我们的权重。
def train(niter, first=True): gen_iterations = 0 for epoch in trange(niter): netD.train(); netG.train() data_iter = iter(md.trn_dl) i,n = 0,len(md.trn_dl) with tqdm(total=n) as pbar: while i < n: set_trainable(netD, True) set_trainable(netG, False) d_iters = ( 100 if (first and (gen_iterations < 25) or (gen_iterations % 500 == 0)) else 5 ) j = 0 while (j < d_iters) and (i < n): j += 1; i += 1 for p in netD.parameters(): p.data.clamp_(-0.01, 0.01) real = V(next(data_iter)[0]) real_loss = netD(real) fake = netG(create_noise(real.size(0))) fake_loss = netD(V(fake.data)) netD.zero_grad() lossD = real_loss-fake_loss lossD.backward() optimizerD.step() pbar.update() set_trainable(netD, False) set_trainable(netG, True) netG.zero_grad() lossG = netD(netG(create_noise(bs))).mean(0).view(1) lossG.backward() optimizerG.step() gen_iterations += 1 print( f'Loss_D {to_np(lossD)}; Loss_G {to_np(lossG)}; ' + f'D_real {to_np(real_loss)}; Loss_D_fake {to_np(fake_loss)}' )
你会发现鉴别器被训练ncritic次(上面代码中的 d_iters),他们将其设置为 5,每次我们训练生成器一次。论文中谈到了这一点,但基本思想是如果鉴别器还不知道如何区分,那么让生成器变得更好是没有意义的。这就是为什么我们有第二个 while 循环。这里是 5:
d_iters = ( 100 if (first and (gen_iterations < 25) or (gen_iterations % 500 == 0)) else 5 )
实际上,稍后的论文中添加的内容或者可能是补充材料是,不时地在开始时,您应该在鉴别器上执行更多步骤,以确保鉴别器是有能力的。
torch.backends.cudnn.benchmark=True
让我们为一个时代进行训练:
train(1, False)0%| | 0/1 [00:00<?, ?it/s] 100%|██████████| 18957/18957 [19:48<00:00, 10.74it/s] Loss_D [-0.67574]; Loss_G [0.08612]; D_real [-0.1782]; Loss_D_fake [0.49754] 100%|██████████| 1/1 [19:49<00:00, 1189.02s/it]
然后让我们创建一些噪音,这样我们就可以生成一些示例。
fixed_noise = create_noise(bs)
但在此之前,将学习率降低 10 倍,并再进行一次训练:
set_trainable(netD, True) set_trainable(netG, True) optimizerD = optim.RMSprop(netD.parameters(), lr = 1e-5) optimizerG = optim.RMSprop(netG.parameters(), lr = 1e-5) train(1, False) ''' 0%| | 0/1 [00:00<?, ?it/s] 100%|██████████| 18957/18957 [23:31<00:00, 13.43it/s] Loss_D [-1.01657]; Loss_G [0.51333]; D_real [-0.50913]; Loss_D_fake [0.50744] 100%|██████████| 1/1 [23:31<00:00, 1411.84s/it] '''
然后让我们使用噪音传递给我们的生成器,然后通过我们的反标准化将其转换回我们可以看到的东西,然后绘制它:
netD.eval(); netG.eval(); fake = netG(fixed_noise).data.cpu() faked = np.clip(md.trn_ds.denorm(fake),0,1) plt.figure(figsize=(9,9)) plt.imshow(gallery(faked, 8));
我们有一些卧室。这些不是真实的卧室,有些看起来并不像卧室,但有些看起来很像卧室,这就是想法。这就是 GAN。最好的方法是将 GAN 视为一种基础技术,你可能永远不会像这样使用它,但你会以许多有趣的方式使用它。例如,我们将使用它来创建一个循环 GAN。
问题:为什么要特别使用 RMSProp 作为优化器,而不是 Adam 等等?我不记得论文中有明确讨论过这个问题。我不知道这是实验性的还是理论上的原因。看看论文中是怎么说的。
通过实验,我发现 Adam 和 WGAN 不仅效果更差 - 它导致生成器训练失败。
来自 WGAN 论文:
最后,作为一个负面结果,我们报告说当使用基于动量的优化器(如 Adam [8](具有β1>0))对评论者进行训练时,WGAN 训练有时会变得不稳定,或者当使用高学习率时。由于评论者的损失是非平稳的,基于动量的方法似乎表现更差。我们确定动量可能是一个潜在原因,因为随着损失的增加和样本变得更糟,Adam 步骤和梯度之间的余弦通常变为负值。这种余弦为负值的唯一情况是在这些不稳定的情况下。因此,我们转而使用 RMSProp [21],它被认为在非平稳问题上表现良好
问题: 在训练过程中,检测过拟合的一个合理方法是什么?或者在训练结束后评估这些 GAN 模型的性能的一个方法是什么?换句话说,训练/验证/测试集的概念如何转化为 GANs [1:41:57]?这是一个很棒的问题,很多人开玩笑说 GANs 是唯一不需要测试集的领域,人们利用这一点编造东西并说看起来很棒。GANs 存在一些著名的问题,其中之一被称为模式崩溃。模式崩溃发生在你查看卧室时,结果发现只有三种卧室,每个可能的噪声向量都映射到这三种卧室中的一种。你查看画廊,结果发现它们都是相同的东西或者只有三种不同的东西。模式崩溃很容易看到,如果崩溃到一个很小的模式数量,比如 3 或 4。但如果模式崩溃到 10,000 种模式怎么办?因此,只有 10,000 种可能的卧室,所有的噪声向量都崩溃到这些卧室。你不太可能在我们刚刚看到的画廊视图中看到,因为在 10,000 种卧室中很少会有两个相同的卧室。或者如果每个卧室基本上是输入的直接副本 —— 它基本上记住了一些输入。这可能正在发生吗?事实是,大多数论文在检查这些问题方面做得不好,有时甚至根本不检查。因此,我们如何评估 GANs 甚至也许我们应该真正正确地评估 GANs 是一个现在还不够广泛理解的问题。一些人正在努力推动。Ian Goodfellow 是最著名的深度学习书籍的第一作者,也是 GANs 的发明者,他一直在发送持续的推文提醒人们测试 GANs 的重要性。如果你看到一篇声称有异常 GAN 结果的论文,那么这绝对值得关注。他们是否谈到了模式崩溃?他们是否谈到了记忆化?等等。
问题: GANs 可以用于数据增强吗 [1:45:33]?是的,绝对可以使用 GAN 进行数据增强。你应该吗?我不知道。有一些论文尝试使用 GANs 进行半监督学习。我还没有找到任何特别引人注目的论文,在广泛研究的真正有趣的数据集上展示出最先进的结果。我有点怀疑,原因是在我的经验中,如果用合成数据训练模型,神经网络将变得极其擅长识别你合成数据的具体问题,并且最终学到的将是这些问题。还有很多其他方法可以做半监督模型,效果很好。有一些地方可以工作。例如,你可能还记得 Otavio Good 在第一部分的缩放卷积网络中创建的那个奇妙的可视化,其中显示了字母通过 MNIST,他,至少在那个时候,是自动遥控汽车比赛中的第一名,他使用合成增强数据训练了他的模型,基本上是拿真实的汽车绕着赛道行驶的视频,然后添加了虚假的人和虚假的其他汽车。我认为这样做效果很好,因为 A. 他有点天才,B. 因为我认为他有一个明确定义的小子集需要处理。但总的来说,使用合成数据真的很难。我尝试过几十年使用合成数据和模型(显然不包括 GANs,因为它们是相当新的),但总的来说,这很难做到。非常有趣的研究问题。
Cycle GAN [1:41:08]
我们将使用 cycle GAN 将马变成斑马。您也可以使用它将莫奈的印刷品转变为照片,或将优胜美地夏季的照片转变为冬季。
这将非常简单,因为它只是一个神经网络。我们要做的就是创建一个包含大量斑马照片的输入,并将每个照片与等价的马照片配对,然后训练一个从一个到另一个的神经网络。或者您可以对每幅莫奈的画做同样的事情——创建一个包含该地点照片的数据集……哦等等,这不可能,因为莫奈绘制的地方已经不存在了,也没有确切的斑马版本的马……这将如何运作?这似乎违背了我们对神经网络能做什么以及它们如何做的一切认知。
所以某种方式,这些伯克利的人创造了一个模型,可以将马变成斑马,尽管没有任何照片。除非他们出去画马并拍摄前后照片,但我相信他们没有。那么他们是如何做到的呢?这有点天才。
我知道目前正在进行最有趣的 cycle GAN 实践的人是我们的学生 Helena Sarin。她是我所知道的唯一一位 cycle GAN 艺术家。
以下是她更多令人惊叹的作品,我觉得非常有趣。我在这堂课开始时提到,GANs 属于尚未出现的东西,但它们几乎已经到位了。在这种情况下,世界上至少有一个人正在使用 GANs(具体来说是 cycle GANs)创作美丽而非凡的艺术作品。至少我知道有十几个人正在用神经网络进行有趣的创意工作。创意人工智能领域将会大幅扩展。
这是基本的技巧。这是来自 cycle GAN 论文。我们将有两幅图像(假设我们正在处理图像)。关键是它们不是配对的图像,所以我们没有一组马和等价斑马的数据集。我们有一堆马,一堆斑马。拿一匹马X,拿一匹斑马Y。我们将训练一个生成器(他们在这里称之为“映射函数”),将马变成斑马。我们将称之为映射函数G,并创建一个将斑马变成马的映射函数(也称为生成器),我们将称之为F。我们将创建一个鉴别器,就像以前一样,它将尽可能地识别真假马,我们将称之为Dx。另一个鉴别器,它将尽可能地识别真假斑马,我们将称之为Dy。这是我们的起点。
使这个工作的关键[1:51:27] - 所以我们在这里生成一个损失函数(Dx和Dy)。我们将创建一个叫做循环一致性损失的东西,它说当你用生成器将你的马变成斑马后,检查我是否能识别它是真实的。我们将我们的马变成斑马,然后尝试将那只斑马再变回我们开始的同一匹马。然后我们将有另一个函数,它将检查这匹马是否与原始马相似,这匹马是完全由这只斑马Y生成的,不知道x的任何信息。所以想法是,如果你生成的斑马看起来一点也不像原始马,你就没有机会将其变回原始马。因此,将x-hat与x进行比较的损失会非常糟糕,除非你能进入Y再出来,如果你能够创建一个看起来像原始马的斑马,那么你可能能够做到这一点。反之亦然 - 将你的斑马变成一个假马,检查你是否能识别它,然后尝试将其变回原始斑马并检查它是否看起来像原始的。
注意F(斑马到马)和G(马到斑马)正在做两件事。它们都将原始马变成斑马,然后将斑马再变回原始马。所以只有两个生成器。没有一个单独的生成器用于反向映射。你必须使用用于原始映射的相同生成器。这就是循环一致性损失。我认为这是天才。这种事情甚至可能存在的想法。老实说,当这一点出现时,我从未想过我甚至可以尝试解决这个问题。它似乎如此明显地不可能,然后你可以像这样解决它的想法 - 我只是觉得这太聪明了。
看这篇论文中的方程式是很好的,因为它们是很好的例子 - 它们写得相当简单,不像一些瓦瑟斯坦 GAN 论文那样,那些是很多理论证明和其他东西。在这种情况下,它们只是列出了正在发生的事情的方程式。你真的想要达到一个可以阅读并理解它们的程度。
所以我们有一匹马X和一只斑马Y。对于一些映射函数G,这是我们的马到斑马映射函数,然后有一个 GAN 损失,这是我们已经熟悉的一部分,它说我们有一匹马,一只斑马,一个假斑马识别器和一个马斑马生成器。损失就是我们之前看到的 - 我们能够从我们的斑马中画出一只斑马并识别它是真实的还是假的。然后拿一匹马变成一只斑马并识别它是真实的还是假的。然后做一减另一个(在这种情况下,它们里面有一个对数,但对数并不是非常重要)。这就是我们刚刚看到的东西。这就是为什么我们先做了瓦瑟斯坦 GAN。这只是一个标准的数学形式的 GAN 损失。
问题:所有这些听起来很像将一种语言翻译成另一种语言,然后再翻译回原来的语言。GANs 或任何等效物已经尝试过翻译吗?来自论坛的论文。回到我所知道的 — 通常在翻译中,你需要这种配对的输入(即平行文本 — “这是这个英语句子的法语翻译”)。最近有几篇论文显示了在没有配对数据的情况下创建高质量翻译模型的能力。我还没有实施它们,我不理解我没有实施的任何东西,但它们很可能在做同样的基本想法。我们将在本周内研究一下,并回复您。
循环一致性损失:所以我们有一个 GAN 损失,接下来是循环一致性损失。基本思想是我们从我们的马开始,使用我们的斑马生成器创建一匹斑马,然后使用我们的马生成器创建一匹马,并将其与原始马进行比较。这个双线与 1 是 L1 损失 — 差异的绝对值的和。否则,如果这是 2,那么它将是 L2 损失,即平方差的和。
我们现在知道这个波浪线的想法是从我们的马抓取一匹马。这就是我们所说的从分布中取样。有各种各样的分布,但在这些论文中,我们最常用的是经验分布,换句话说,我们有一些数据行,抓取一行。所以这里,它是说从数据中抓取一些东西,我们将称那个东西为x。为了重新概括:
- 从我们的马图片中,抓取一匹马
- 将其变成斑马
- 将其转换回马
- 将其与原始图像进行比较并求绝对值的和
- 也对斑马进行同样的操作
- 然后将两者相加
这就是我们的循环一致性损失。
完整目标
现在我们得到了我们的损失函数,整个损失函数取决于:
- 我们的马生成器
- 一个斑马生成器
- 我们的马识别器
- 我们的斑马识别器(又名鉴别器)
我们将加起来:
- 用于识别马的 GAN 损失
- 用于识别斑马的 GAN 损失
- 我们两个生成器的循环一致性损失
我们这里有一个 lambda,希望我们现在对这个想法有点习惯了,当你有两种不同的损失时,你可以加入一个参数,这样你可以将它们乘以一个相同的比例。我们在定位时也对我们的边界框损失与分类器损失做了类似的事情。
然后对于这个损失函数,我们将尝试最大化鉴别器的辨别能力,同时最小化生成器的辨别能力。因此,生成器和鉴别器将面对面地对抗。当你在论文中看到这个 min max 时,基本上意味着在你的训练循环中,一个东西试图让某事变得更好,另一个东西试图让某事变得更糟,有很多方法可以做到,但最常见的是你会在两者之间交替。你经常会在数学论文中看到这个被简称为 min-max。所以当你看到 min-max 时,你应该立即想到对抗训练。
实施循环 GAN
让我们看看代码。我们将要做一些几乎闻所未闻的事情,那就是我开始查看别人的代码,但并没有对整个东西感到恶心,然后自己重新做。我实际上说我相当喜欢这个,我喜欢它到足以向我的学生展示。这是代码的来源,这是一个为循环 GAN 创建原始代码的人之一,他们创建了一个 PyTorch 版本。我不得不稍微整理一下,但实际上它还是相当不错的。这个酷的地方是,你现在将看到几乎所有 fast.ai 的部分,或者其他相关的 fast.ai 部分,是由其他人以不同的方式编写的。所以你将看到他们如何处理数据集、数据加载器、模型、训练循环等等。
你会发现有一个cgan
目录,这基本上几乎是原始的,只是做了一些清理,我希望有一天能提交为 PR。它是以一种不幸地使它与他们作为脚本使用的方式过于连接的方式编写的,所以我稍微整理了一下,以便我可以将其用作模块。但除此之外,它还是相当相似的。
from fastai.conv_learner import * from fastai.dataset import * from cgan.options.train_options import *
所以cgan
是他们从 github 仓库复制的代码,做了一些小的改动。cgan
迷你库的设置方式是,它假设配置选项是被传递到像脚本一样。所以他们有TrainOptions().parse
方法,我基本上传入一个脚本选项的数组(我的数据在哪里,有多少线程,我想要丢弃吗,我要迭代多少次,我要怎么称呼这个模型,我要在哪个 GPU 上运行)。这给我们一个opt
对象,你可以看到它包含了什么。你会看到它包含了一些我们没有提到的东西,那是因为它对我们没有提到的其他所有东西都有默认值。
opt = TrainOptions().parse(['--dataroot', '/data0/datasets/cyclegan/horse2zebra', '--nThreads', '8', '--no_dropout', '--niter', '100', '--niter_decay', '100', '--name', 'nodrop', '--gpu_ids', '2'])
所以我们不再使用 fast.ai 的东西,我们将主要使用 cgan 的东西。
from cgan.data.data_loader import CreateDataLoader from cgan.models.models import create_model
我们首先需要的是一个数据加载器。这也是一个很好的机会,让你再次练习使用你选择的编辑器或 IDE 浏览代码的能力。我们将从CreateDataLoader
开始。你应该能够找到符号或在 vim 标签中直接跳转到CreateDataLoader
,我们可以看到它创建了一个CustomDatasetDataLoader
。然后我们可以看到CustomDatasetDataLoader
是一个BaseDataLoader
。我们可以看到它将使用标准的 PyTorch DataLoader,这很好。我们知道如果要使用标准的 PyTorch DataLoader,你需要传递一个数据集,我们知道数据集是包含长度和索引器的东西,所以当我们查看CreateDataset
时,它应该会这样做。
这里是CreateDataset
,这个库不仅仅是循环 GAN - 它处理对齐和不对齐的图像对。我们知道我们的图像对是不对齐的,所以我们要使用UnalignedDataset
。
正如预期的那样,它有__getitem__
和__len__
。对于长度,A 和 B 是我们的马和斑马,我们有两组,所以较长的那个将是DataLoader
的长度。__getitem__
将会:
- 随机从我们的两匹马和斑马中抓取一些东西
- 用 Pillow(PIL)打开它们
- 通过一些转换运行它们
- 然后我们可以把马变成斑马,或者把斑马变成马,所以有一些方向
- 返回我们的马、斑马、马的路径和斑马的路径
希望你能看到这看起来与 fast.ai 所做的事情非常相似。当涉及到转换和性能时,fast.ai 显然做了更多,但请记住,这是为这个特定事情的研究代码,他们做了这么多工作,这是相当酷的。
data_loader = CreateDataLoader(opt) dataset = data_loader.load_data() dataset_size = len(data_loader) dataset_size ''' 1334 '''
我们有一个数据加载器,所以我们可以将我们的数据加载到其中[2:06:17]。这将告诉我们其中有多少个小批次(这是 PyTorch 数据加载器的长度)。
下一步是创建一个模型。同样的想法,我们有不同类型的模型,我们将要做一个循环 GAN。
这是我们的CycleGANModel
。CycleGANModel
中有相当多的内容,所以让我们逐步找出将要使用的内容。在这个阶段,我们只是调用了初始化器,所以当我们初始化它时,它将会定义两个生成器,一个用于我们的马,一个用于斑马。它有一种方法来生成一组假数据,然后我们将获取我们的 GAN 损失,正如我们所讨论的,我们的循环一致性损失是一个 L1 损失。他们将使用 Adam,显然对于循环 GAN,他们发现 Adam 效果很好。然后我们将为我们的马判别器、斑马判别器和生成器各自创建一个优化器。生成器的优化器将包含马生成器和斑马生成器的参数,所有这些都在一个地方。
因此,初始化器将设置我们需要的所有不同网络和损失函数,并将它们存储在这个model
中[2:08:14]。
model = create_model(opt)
然后打印出并向我们展示我们拥有的 PyTorch 模型。看到他们正在使用 ResNets,你会发现 ResNets 看起来非常熟悉,所以我们有卷积、批量归一化、Relu。InstanceNorm
基本上与批量归一化相同,但它是针对一幅图像应用的,区别并不特别重要。你可以看到他们正在做反射填充,就像我们一样。当你尝试像这样从头开始构建所有东西时,这是很多工作,你可能会忘记 fast.ai 自动为你做的一些好事。你必须手动完成所有这些工作,最终只能得到其中的一部分。所以随着时间的推移,希望很快,我们将把所有这些 GAN 内容整合到 fast.ai 中,这将变得简单而容易。
我们有我们的模型,记住模型包含损失函数、生成器、判别器,所有这些都在一个方便的地方[2:09:32]。我已经复制、粘贴并稍微重构了他们代码中的训练循环,这样我们就可以在笔记本中运行它。所以这个应该看起来很熟悉。一个循环用于遍历每个 epoch,一个循环用于遍历数据。在这之前,我们设置了dataset
。实际上这不是一个 PyTorch 数据集,我认为这是他们稍微令人困惑地用来谈论他们的组合数据,我们称之为模型数据对象——他们需要的所有数据。用tqdm
循环遍历它,以获得进度条,这样我们就可以看看模型中发生了什么。
total_steps = 0 for epoch in range(opt.epoch_count, opt.niter + opt.niter_decay+1): epoch_start_time = time.time() iter_data_time = time.time() epoch_iter = 0 for i, data in tqdm(enumerate(dataset)): iter_start_time = time.time() if total_steps % opt.print_freq == 0: t_data = iter_start_time - iter_data_time total_steps += opt.batchSize epoch_iter += opt.batchSize model.set_input(data) model.optimize_parameters() if total_steps % opt.display_freq == 0: save_result = total_steps % opt.update_html_freq == 0 if total_steps % opt.print_freq == 0: errors = model.get_current_errors() t = (time.time() - iter_start_time) / opt.batchSize if total_steps % opt.save_latest_freq == 0: print( 'saving the latest model(epoch %d,total_steps %d)' % (epoch, total_steps) ) model.save('latest') iter_data_time = time.time() if epoch % opt.save_epoch_freq == 0: print( 'saving the model at the end of epoch %d, iters %d' % (epoch, total_steps) ) model.save('latest') model.save(epoch) print( 'End of epoch %d / %d \t Time Taken: %d sec' % ( epoch, opt.niter + opt.niter_decay, time.time() - epoch_start_time ) ) model.update_learning_rate()
set_input
[2:10:32]:这是一种与 fast.ai 中所做的不同方法。这很巧妙,它相当特定于循环 GAN,但基本上在这个模型内部的想法是,我们将进入我们的数据并获取适当的数据。我们要么将马转换为斑马,要么将斑马转换为马,取决于我们选择的方式,A
要么是马要么是斑马,反之亦然。如果需要,将其放在适当的 GPU 上,然后获取适当的路径。因此,模型现在有一批马和一批斑马。
现在我们优化参数[2:11:19]。这样看起来很好。你可以看到每一步。首先,尝试优化生成器,然后尝试优化马判别器,然后尝试优化斑马判别器。zero_grad()
是 PyTorch 的一部分,以及step()
。因此,有趣的部分是实际执行生成器反向传播的部分。
这里是[2:12:04]。让我们跳到关键部分。这里有我们刚刚在论文中看到的所有公式。让我们拿一匹马生成一只斑马。现在让我们使用鉴别器来看看我们是否能够判断它是假的还是真的(pred_fake
)。然后让我们将其放入我们之前设置的损失函数中,以基于该预测获得 GAN 损失。然后让我们以相反的方向做同样的事情,使用相反的鉴别器,然后再次通过损失函数。然后让我们做循环一致性损失。再次,我们拿我们创建的假的东西,尝试将其转回原始状态。让我们使用之前创建的循环一致性损失函数将其与真实原始状态进行比较。这里是那个 lambda - 所以有一些权重我们使用了,实际上我们只是使用了他们在选项中建议的默认值。然后对相反的方向做同样的事情,然后将它们全部加在一起。然后进行反向步骤。就是这样。
所以我们可以为第一个鉴别器做同样的事情[2:13:50]。因为基本上所有的工作现在都已经完成了,这里要做的事情就少得多了。就是这样。我们不会一步步走过来,但基本上是我们已经看到的相同的基本东西。
所以optimize_parameters()
正在计算损失并执行优化器步骤。不时保存并打印一些结果。然后不时更新学习率,所以他们在这里也有一些学习率退火的机制。有点像 fast.ai,他们有这个调度器的概念,你可以用它来更新你的学习率。
对于那些对更好地理解深度学习 API、更多地为 fast.ai 做贡献,或者在一些不同的后端中创建自己版本的一些东西感兴趣的人,看看第二个 API 是很酷的,它涵盖了一些类似的东西的一些子集,以便了解他们是如何解决这些问题的,以及相似之处/不同之处是什么。
def show_img(im, ax=None, figsize=None): if not ax: fig,ax = plt.subplots(figsize=figsize) ax.imshow(im) ax.get_xaxis().set_visible(False) ax.get_yaxis().set_visible(False) return ax def get_one(data): model.set_input(data) model.test() return list(model.get_current_visuals().values()) model.save(201) test_ims = [] for i,o in enumerate(dataset): if i>10: break test_ims.append(get_one(o)) def show_grid(ims): fig,axes = plt.subplots(2,3,figsize=(9,6)) for i,ax in enumerate(axes.flat): show_img(ims[i], ax); fig.tight_layout() for i in range(8): show_grid(test_ims[i])
我们训练了一段时间,然后我们可以随便拿几个例子,这里有它们[2:15:29]。这里有马、斑马,然后再变回马。
我花了大约 24 小时来训练它,所以它有点慢[2:16:39]。我知道 Helena 经常在 Twitter 上抱怨这些事情花费的时间有多长。我不知道她是如何在这些事情上如此高效的。
# !wget https://people.eecs.berkeley.edu/~taesung_park/CycleGAN/datasets/horse2zebra.zip
我还要提到昨天刚出来的另一件事[2:16:54]:
现在有一种多模态的无监督图像到图像的翻译。所以你现在基本上可以从这只狗创建不同的猫。
这不仅仅是创建你想要的输出的一个例子,而是创建多个例子。这是昨天或前天才出来的。我觉得这很惊人。所以你可以看到这项技术是如何发展的,我认为在音乐、语音、写作方面,或者为艺术家创造工具方面,可能有很多机会。
深度学习 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
fast.ai 深度学习笔记(六)(3)https://developer.aliyun.com/article/1482699