
从上周开始讲述

Alena Harley做了一些非常有趣的事情,她试图找出如果你只用三四百张图像循环GAN会发生什么,我真的很喜欢这些项目,人们只是使用API或其中一个库来进行Google Image Search那里。 我们的一些学生已经创建了一些非常好的库,用于与Google图像API交互,下载他们感兴趣的一些东西,在这种情况下是一些照片和一些彩色玻璃窗。 有300到400张照片,她训练了一些不同的模型 - 这是我特别喜欢的。 正如你所看到的,使用相当少的图像,她可以获得非常漂亮的彩色玻璃效果。 所以我认为这是一个有趣的例子,使用了很少的数据,这些数据很容易获得,她能够很快下载。 如果你有兴趣,可以在论坛上获得更多相关信息。
有趣的是,想知道人们会用这种生成模型提出什么样的东西。 这显然是一种伟大的艺术媒介。 对于伪造品和面包店来说,这显然是一个很好的媒介。 我想知道人们会意识到他们可以用这些生成模型做些什么。 我认为音频将成为下一个重要领域。 也非常互动的类型的东西。 Nvidia刚刚发布了一篇论文,展示了一种互动式的照片修复工具,你只需刷过一个物体,就可以用非常好的深度学习替换它取而代之。 我觉得那些互动工具也很有意思。
超级分辨率 [2:06]
上次,我们通过实际直接优化像素来研究样式转移。 与第二部分中的大部分内容一样,并不是因为我希望你理解样式转换本身,而是直接优化输入并将激活作为损失函数的一部分的想法真的是关键外卖。
因此,有效地看待后续论文是有趣的,不是来自同一个人,而是来自Justin Johnson和斯坦福大学的人们在这些视觉生成模型的序列中接下来的论文。 它实际上做了同样的事情 - 风格转移,但它以不同的方式。 我们不会优化像素,而是回到更熟悉的东西并优化一些权重。 具体来说,我们将训练一个模型,该模型学会拍摄照片并将其翻译成特定艺术作品风格的照片。 因此每个转换网将学习生成一种风格。
现在事实证明,到了那一点,有一个中间点(我实际上认为更有用并将我们带到我们中途)是一种称为超分辨率的东西。 所以我们实际上将从超级分辨率开始 [3:55] 。 因为那时我们将建立在超级分辨率之上,以完成基于转换网络的样式转移。
超分辨率是我们拍摄低分辨率图像(我们将采用72乘72)并将其放大到更大的图像(在我们的例子中为288乘288),试图创建一个看起来尽可能真实的更高分辨率图像。 这是一件具有挑战性的事情,因为在72乘72时,关于很多细节的信息并不多。 很酷的是,我们将采用与视觉模型相关的方式进行操作,视觉模型与输入大小无关,因此你可以完全采用此模型并将其应用于288 x 288图像并获取某些内容这是每侧大四倍,所以比原来大16倍。 通常它甚至在这个级别上工作得更好,因为你真的在更精细的细节中引入了很多细节,你可以真正打印出一些高分辨率的打印件,这些打印件之前很像是像素化的。
笔记本 [5:06]
这很像CSI样式增强,我们将采取看似信息不存在的东西,我们发明它 - 但是转换网将学会以一致的方式发明它有了那里的信息,所以希望它发明了正确的信息。 关于这类问题的一个非常好的事情是我们可以在没有任何标签要求的情况下创建我们自己的数据集,因为我们可以通过对图像进行下采样轻松地从高分辨率图像创建低分辨率图像。 所以我希望你们这些人本周尝试的其他方法是做其他类型的图像到图像的翻译,你可以发明“标签”(你的因变量)。 例如:
- 歪斜 :要么将旋转了90度或更好的东西识别为旋转了5度并将它们拉直。
- 着色 :将一堆图像变成黑白,并学会再次将颜色重新调整。
- 降噪 :也许做一个非常低质量的JPEG保存,并学会把它恢复到应该如何。
- 也许采用16色调的东西,并将其放回更高的调色板。
我认为这些东西都很有趣,因为它们可以用来拍摄你可能在高分辨率之前用旧的数码相机拍摄的照片,或者你可能已经扫描了一些现在已经褪色的旧照片等等。我认为这是能够做到的确非常有用,这是一个很好的项目,因为它与我们在这里所做的非常相似,但是相当不同,你在路上遇到了一些有趣的挑战,我敢肯定。
我将再次使用ImageNet [7:19] 。 你根本不需要使用所有的ImageNet,我恰好让它躺在那里。 你可以从files.fast.ai下载ImageNet的1%样本。 你可以使用你老实说谎的任何一组照片。
matplotlib inline %reload_ext autoreload %autoreload 2
超分辨率数据
**from** **fastai.conv_learner** **import** * **from** **pathlib** **import** Path torch.backends.cudnn.benchmark= **True**
PATH = Path('data/imagenet') PATH_TRN = PATH/'train'
在这种情况下,正如我所说,我们本身并没有真正的标签,所以我只是给所有标签零,这样我们就可以更容易地将它与我们现有的基础设施一起使用。
fnames_full,label_arr_full,all_labels = folder_source(PATH, 'train') fnames_full = ['/'.join(Path(fn).parts[-2:]) **for** fn **in** fnames_full] list(zip(fnames_full[:5],label_arr_full[:5]))
_[('n01440764/n01440764_9627.JPEG', 0),_ _('n01440764/n01440764_9609.JPEG', 0),_ _('n01440764/n01440764_5176.JPEG', 0),_ _('n01440764/n01440764_6936.JPEG', 0),_ _('n01440764/n01440764_4005.JPEG', 0)]_
all_labels[:5]
_['n01440764', 'n01443537', 'n01484850', 'n01491361', 'n01494475']_
现在,因为我指的是一个包含所有ImageNet的文件夹,我当然不想等待所有的ImageNet完成运行一个迭代。 所以在这里,我只是,大多数时候,我会将“保持百分比”( keep_pct )设置为1或2%。 然后我只生成一堆随机数,然后我保留那些小于0.02的数据,这样我就可以快速对我的行进行二次采样。
np.random.seed(42) # keep_pct = 1. _keep_pct = 0.02_ keeps = np.random.rand(len(fnames_full)) < keep_pct fnames = np.array(fnames_full, copy= **False** )[keeps] label_arr = np.array(label_arr_full, copy= **False** )[keeps]
所以我们将使用VGG16 [8:21] 而VGG16是我们在这个类中没有真正看过的东西,但它是一个非常简单的模型,我们采用正常的3通道输入,我们基本上通过一个3x3卷积的数量,然后不时,我们通过2x2 maxpool,然后我们再做3x3卷积,maxpool,等等。 这是我们的支柱。

然后我们不做自适应平均池层。 在其中一些之后,我们像往常一样(或类似的东西)最终得到这个7x7x512网格。 因此,我们做了一些不同的事情,而不是平均汇集,我们将整个事情弄平 - 所以如果内存正确运行,它会向外喷出一个非常长的7x7x512大小的激活向量。 然后将其送入两个全连接层,每个层具有4096个激活,以及一个全连接层,其具有许多类。 所以如果你考虑一下,这里的权重矩阵,它是巨大的7x7x512x4096。 正是由于这个权重矩阵,VGG很快就失宠了 - 因为它占用了大量的内存并且需要大量的计算而且速度非常慢。 这里有很多冗余的东西,因为实际上那些512次激活并不特定于它们所处的7x7网格单元中的哪一个。但是当你在这里拥有每个可能组合的整个权重矩阵时,它会将所有这些单独处理。 因此,这也可能导致泛化问题,因为只有很多权重等等。

我的观点是在每个现代网络中使用的方法,我们在这里进行自适应平均池(在Keras中它被称为全局平均池,在fast.ai中,我们做一个AdaptiveConcatPool),它直接将它输出到512长激活 [11:06] 。 我认为这会丢掉太多的几何体。 所以对我来说,可能正确的答案介于两者之间,并且将涉及某种因素卷积或某种张量分解,这可能是我们中的一些人在未来几个月可能会想到的。 所以现在,无论如何,我们已经从一个极端,即自适应平均汇集到另一个极端,即这个巨大的扁平完全连接层。
关于VGG的一些有趣的事情使它今天仍然有用 [11:59] 。 第一个是在这里有更多有趣的层与大多数现代网络,包括ResNet系列,第一层通常是7x7转换,步幅2或类似的东西。 这意味着我们可以立即丢弃一半的网格大小,因此几乎没有机会使用精细细节,因为我们从不对它进行任何计算。 因此,对于分段或超分辨率模型这样的问题,这是一个问题,因为细节很重要。 我们实际上想恢复它。 然后第二个问题是自适应池化层完全抛弃了最后几个部分中的几何体,这意味着模型的其余部分实际上没有像几何体那样有趣的学习。 因此,对于依赖于位置的事物,任何需要生成模型的任何类型的基于本地化的方法都将变得不那么有效。 所以我希望你在我描述的内容之一就是可能没有一个现有的架构实际上是理想的。 我们可以发明一个新的 。 实际上,我只是尝试在一周内发明一个新的,即将VGG头部连接到ResNet骨干网。 有趣的是,我发现我的分类器实际上比普通的ResNet好一些,但它也有一些更有用的信息。 训练需要花费5或10%的时间,但没有什么值得担心的。 也许我们可以在ResNet中替换这个(7x7转步2),正如我们之前简要讨论过的那样。 这个非常早期的卷积更像是一个Inception词干,它有更多的计算。 我认为这些架构肯定有一些很好的调整空间,因此我们可以构建一些可能更通用的模型。 目前,人们倾向于构建只做一件事的架构。 他们并没有真正想到我在机会方面扔掉了什么,因为这就是出版的运作方式。 你发表了“我已经掌握了这一件事的最新技术,而不是你创造了许多擅长许多东西的东西。
由于这些原因,我们今天将使用VGG,尽管它很古老而且缺少很多很棒的东西 [14:42] 。 我们要做的一件事是使用稍微更现代的版本,这是VGG的一个版本,在所有卷积之后添加了批量规范。 在fast.ai,当你要求VGG网络时,你总是得到批量规范,因为这基本上总是你想要的。 所以这是具有批量规范的VGG。 有16和19,19更大更重,并没有真正做得更好,所以没有人真正使用它。
arch = vgg16 sz_lr = 72
我们将从72 sz_lr 72 LR( sz_lr :尺寸低分辨率)输入。 我们最初将它按比例缩放2倍,批量大小为64,得到2 * 72,144 144输出。 那将是我们的第一阶段。
scale,bs = 2,64 _# scale,bs = 4,32_ sz_hr = sz_lr*scale
我们将为此创建我们自己的数据集,并且非常值得查看fastai.dataset模块并查看其中的内容 [15:45] 。 因为你想要的任何东西,我们可能有一些几乎你想要的东西。 所以在这种情况下,我想要一个数据集,其中我的x是图像,而我的y也是图像。 我们可以从x继承的文件数据集中继承,然后我只是继承了它,我只是复制并粘贴了get_x并将其转换为get_y因此它只是打开一个图像。 现在我有一些东西,其中x是一个图像, y是一个图像,在这两种情况下,我们传入的是一个文件名数组。
**class** **MatchedFilesDataset** (FilesDataset): **def** __init__(self, fnames, y, transform, path): self.y=y **assert** (len(fnames)==len(y)) super().__init__(fnames, transform, path) **def** get_y(self, i): **return** open_image(os.path.join(self.path, self.y[i])) **def** get_c(self): **return** 0
我要做一些数据增强 [16:32] 。 显然,对于所有的ImageNet,我们并不真正需要它,但这主要适用于使用较小数据集来充分利用它的任何人。 RandomDihedral指的是每个可能的90度旋转加上可选的左/右翻转,因此它们是八个对称的二面体组。 通常我们不会对ImageNet图片使用这种转换,因为你通常不会颠倒狗,但在这种情况下,我们不是要试图分类它是狗还是猫,我们只是试图保持一般的结构它。 所以实际上每次可能的翻转对于这个问题都是合理合理的。
aug_tfms = [RandomDihedral(tfm_y=TfmType.PIXEL)]
以通常的方式创建验证集 [17:19] 。 你可以看到我正在使用更低级别的功能 - 一般来说,我只是将它们复制并粘贴在fastai源代码中以找到我想要的位。 所以这里是一个位,它接受一组验证集索引和一个或多个变量数组,并简单地拆分。 在这种情况下,将此( np.array(fnames) )转换为训练和验证集,并将此(第二个np.array(fnames) )转换为训练和验证集,以便为我们提供x和y的。 在这种情况下, x和y是相同的。 我们的输入图像和输出图像是相同的。 我们将使用转换来使其中一个降低分辨率。 这就是为什么这些都是一回事。
val_idxs = get_cv_idxs(len(fnames), val_pct=min(0.01/keep_pct, 0.1)) ((val_x,trn_x),(val_y,trn_y)) = split_by_idx(val_idxs, np.array(fnames), np.array(fnames)) len(val_x),len(trn_x)
_(12811, 1268356)_
img_fn = PATH/'train'/'n01558993'/'n01558993_9684.JPEG'
我们需要做的下一件事是按照惯例创建我们的转换 [18:13] 。 我们将使用tfm_y参数,就像我们对边框所做的那样,而不是使用TfmType.COORD我们将使用TfmType.PIXEL 。 这告诉我们的转换框架你的y值是包含普通像素的图像,所以你对x做的任何事情,你也需要对y做同样的事情。 你需要确保你使用的任何数据增强转换都具有相同的参数。
tfms = tfms_from_model(arch, sz_lr, tfm_y=TfmType.PIXEL, aug_tfms=aug_tfms, sz_y=sz_hr) datasets = ImageData.get_ds(MatchedFilesDataset, (trn_x,trn_y), (val_x,val_y), tfms, path=PATH_TRN) md = ImageData(PATH, datasets, bs, num_workers=16, classes= **None** )
你可以看到可能的转换类型:
- CLASS:我们即将在今天下半年使用细分的分类
- COORD:坐标 - 根本没有转变
- PIXEL
一旦我们有Dataset类和一些x和y训练和验证集。 有一个方便的小方法叫做get datasets( get_ds ),它基本上在所有不同的东西上运行该构造函数,你需要以完全正确的格式返回所需的所有数据集,以传递给ModelData构造函数(在本例中为ImageData构造函数) )。 所以我们有点回到fastai的封面下并从头开始构建它。 在接下来的几个星期里,这一切都将被包装并重构为你可以在fastai中一步完成的事情。 但本课程的重点是学习一些关于幕后工作的内容。
我们之前简要介绍的一点是,当我们拍摄图像时,我们不仅使用数据增强对它们进行变换,而且还将通道尺寸移动到开始,我们减去平均值除以标准差等 [20:08] 。 因此,如果我们希望能够显示那些来自我们的数据集或数据加载器的图片,我们需要对它们进行去规范化。 因此,模型数据对象的( md )数据集( val_ds )具有知道如何执行此操作的denorm函数。 为了方便,我只是给它一个简短的名字:
denorm = md.val_ds.denorm
所以现在我要创建一个可以显示来自数据集的图像的函数,如果你传入的东西说这是一个标准化的图像,那么我们就会对它进行说明。
**def** show_img(ims, idx, figsize=(5,5), normed= **True** , ax= **None** ): **if** ax **is** **None** : fig,ax = plt.subplots(figsize=figsize) **if** normed: ims = denorm(ims) **else** : ims = np.rollaxis(to_np(ims),1,4) ax.imshow(np.clip(ims,0,1)[idx]) ax.axis('off')
x,y = next(iter(md.val_dl)) x.size(),y.size()
_(torch.Size([32, 3, 72, 72]), torch.Size([32, 3, 288, 288]))_
你会在这里看到我们已经传递了尺寸低res( sz_lr )作为我们的变换尺寸和尺寸高res( sz_hr ),因为这是新的,尺寸y参数( sz_y ) [20:58] 。 因此这两个位将变得不同。

在这里你可以看到x和y的两种不同分辨率,对于一大堆鱼来说。
idx=1 fig,axes = plt.subplots(1, 2, figsize=(9,5)) show_img(x,idx, ax=axes[0]) show_img(y,idx, ax=axes[1])

按照惯例, plt.subplots创建我们的两个图,然后我们可以使用回来的不同轴将东西放在彼此旁边。
batches = [next(iter(md.aug_dl)) **for** i **in** range(9)]
然后我们可以看看几个不同版本的数据转换 [21:37] 。 在那里你可以看到它们在所有不同的方向被翻转。
fig, axes = plt.subplots(3, 6, figsize=(18, 9)) **for** i,(x,y) **in** enumerate(batches): show_img(x,idx, ax=axes.flat[i*2]) show_img(y,idx, ax=axes.flat[i*2+1])

型号 [21:48]
让我们创建我们的模型。 我们将有一个小图像进入,我们希望有一个大图像出来。 所以我们需要在这两者之间做一些计算来计算大图像的样子。 基本上有两种方法可以进行计算:
- 我们首先可以进行一些上采样,然后进行一些步骤来进行大量计算。
- 我们可以先做很多步骤来完成所有计算,然后在最后做一些上采样。
我们将选择第二种方法,因为我们希望对较小的东西进行大量计算,因为以这种方式执行它会快得多。 此外,我们在上采样过程中可以利用所有计算。 上采样,我们知道几种可能的方法。 我们可以用:
- 转置或分步跨越的卷积
- 最近邻居上采样后跟1x1转换
在“做大量的计算”部分,我们可以拥有一大堆3x3转换。 但在特殊情况下,似乎ResNet块可能更好,因为输出和输入实际上非常相似。 所以我们真的想要一个流通路径,尽可能少的烦恼,除了做超级分辨率所需的最小量。 如果我们使用ResNet块,那么它们已经具有标识路径。 所以你可以想象那些简单的版本,它采用双线性采样方法,或者它可以通过身份块一直通过,然后在上采样块中,只需学习获取输入的平均值并得到一些不太可怕的东西。
这就是我们要做的事情。 我们将创建具有五个ResNet块的东西,然后对于每个2倍的扩展,我们必须做,我们将有一个上采样块。

正如往常一样,它们都将包含卷积层,可能在许多卷之后具有激活函数 [24:37] 。 我喜欢把我的标准卷积块放到一个函数中,这样我就可以更容易地重构它。 我不会担心传递填充,只是将其直接计算为内核大小超过两个。
**def** conv(ni, nf, kernel_size=3, actn= **False** ): layers = [nn.Conv2d(ni, nf, kernel_size, padding=kernel_size//2)] **if** actn: layers.append(nn.ReLU( **True** )) **return** nn.Sequential(*layers)
关于我们的小转换块的一个有趣的事情是没有批量规范,这对于ResNet类型模型来说是非常不寻常的。

https://arxiv.org/abs/1707.02921
没有批量规范的原因是因为我从最近这篇精彩的论文中窃取了想法,这篇论文实际上赢得了最近超级分辨率表现的竞争。 为了看看这篇论文有多好,SRResNet是先前的技术水平,他们在这里所做的就是他们已经放大了一个上采样的网格/围栏。 人力资源是原创的。 你可以在之前的最佳方法中看到,存在大量失真和模糊现象。 或者,在他们的方法中,它几乎是完美的。 因此,本文是一个非常大的进步。 他们称他们的模型为EDSR(增强型深度超分辨率网络),他们做了两件事与以前的标准方法不同:
- 获取ResNet块并丢弃批量规范。 他们为什么要抛弃批量规范? 原因是因为批量规范改变了东西,我们想要一个不会改变东西的直接路径。 所以这里的想法是,如果你不想比你必须更多地输入输入,那么不要强迫它必须计算批量规范参数之类的东西 - 所以扔掉批处理规范。
- 缩放因子(我们很快就会看到)。
**class** **ResSequential** (nn.Module): **def** __init__(self, layers, res_scale=1.0): super().__init__() self.res_scale = res_scale self.m = nn.Sequential(*layers) **def** forward(self, x): **return** x + self.m(x) * self.res_scale
因此,我们将创建一个包含两个卷积的残差块。 正如你在他们的方法中看到的那样,他们在第二次转发之后甚至没有ReLU。 所以这就是我第一次激活的原因。
**def** res_block(nf): **return** ResSequential( [conv(nf, nf, actn= **True** ), conv(nf, nf)], 0.1)
这里有几件有趣的事 [27:10] 。 一个是这种想法,即拥有某种主要的ResNet路径(conv,ReLU,conv),然后通过将其添加回身份将其转换为ReLU块 - 这是我们经常做的事情,我将其考虑到了一个名为ResSequential的微小模块。 它只需要将一堆层放入剩余路径中,将其转换为顺序模型,运行它,然后将其添加回输入。 有了这个小模块,我们现在可以通过包装ResSequential将任何内容(如conv激活转换)转换为ResNet块。
但这并不是我所做的全部,因为通常Res块在其forward只有x + self.m(x) 。 但我也有* self.res_scale 。 什么是res_scale ? res_scale是数字0.1。 为什么会这样? 我不确定是否有人知道。 但简短的回答是,发明批量规范的人最近也做了一篇论文,其中他(我认为)首次展示了在一小时内训练ImageNet的能力。 他这样做的方法就是启动很多机器并使它们并行工作以创建非常大的批量。 现在,通常当你按照N阶段增加批量大小时,你也可以通过N阶来提高学习率。 因此,通常非常大的批量训练也意味着非常高的学习率训练。 他发现,在训练开始时,这些非常大的批量大小为8,000+甚至高达32,000, 他的激活基本上会直接进入无限 。 许多其他人也发现了这一点。 我们实际上发现,当我们在CIFAR和ImageNet竞赛中参加DAWN测试时,我们真的很难充分利用我们试图利用的八个GPU,因为这些较大的批量大小的挑战和服用他们的优势。 基督徒发现的东西是在ResNet块中,如果他将它们乘以小于1的某个数字,比如.1或.2,它确实有助于在开始时稳定训练。 这有点奇怪,因为在数学上,它是相同的。 因为很明显,无论我在这里乘以它,我都可以按相反的数量缩放权重并使用相同的数字。 但我们并不是在处理抽象数学 - 我们正在处理真正的优化问题,不同的初始化,学习率以及其他任何问题。 所以权重消失在无穷大的问题,我想通常实际上是关于计算机在实践中的离散和有限性质。 所以这些小技巧经常会有所不同。
在这种情况下,我们只是根据我们的初始初始化来减少事情。 所以可能有其他方法可以做到这一点。 例如,Nvidia的一些人称之为LARS,我上周简要提到的是一种使用实时计算的判别学习率的方法。 基本上看一下梯度与激活之间的比例,以逐层扩展学习率。 因此他们发现他们不需要这个技巧来大量扩大批量。 也许完全不同的初始化就是必要的。 我提到这个的原因并不是因为我认为很多人可能想要在大量的计算机集群上进行训练,而是我认为很多人想要快速训练模型,这意味着使用高学习率并且理想情况下获得超级收敛。 我认为这些技巧是我们需要能够在更多不同的架构上实现超级融合的技巧等等。 除了莱斯利史密斯之外,除了现在的一些快餐学生之外,没有其他人真正致力于超级融合。 所以这些关于我们如何以非常高的学习率训练的事情,我们将不得不成为那些想出来的人,因为据我所知,没有其他人关心。 所以看一下有关训练ImageNet的文献,我们现在在一小时或更近的时间里训练ImageNet 15分钟,我认为这些文章实际上有一些技巧可以让我们以高学习率训练。 所以这是其中之一。
有趣的是,除了列车ImageNet在一小时的纸上,我见过的唯一另一个地方就是这篇EDSR论文。 这真的很酷,因为赢得比赛的人,我发现他们非常务实,读得很好。 他们实际上必须让事情发挥作用。 因此,本文描述了一种实际上比其他人的方法更好的方法,并且他们做了这些务实的事情,例如抛弃批量规范并使用几乎没有人似乎知道的这个小的缩放因子。 那就是.1来自哪里。
**def** upsample(ni, nf, scale): layers = [] **for** i **in** range(int(math.log(scale,2))): layers += [conv(ni, nf*4), nn.PixelShuffle(2)] **return** nn.Sequential(*layers)
所以基本上我们的超级分辨率ResNet( SrResnet )将进行卷积,从我们的三个通道转到64个通道,只是为了稍微增加空间 [33:25] 。 然后我们实际上有8个而不是5个Res块。 请记住,这些Res块中的每一个都是步幅1,因此网格大小不会改变,滤波器的数量不会改变。 它一直只有64。 我们将再进行一次卷积,然后我们将按照我们要求的规模进行上采样。 然后我添加了一些这是一个批量规范,因为它感觉只是缩放最后一层可能会有所帮助。 然后最终转回到我们想要的三个频道。 所以你可以看到这里有很多大量的计算,然后就像我们描述的那样进行一些上采样。
**class** **SrResnet** (nn.Module): **def** __init__(self, nf, scale): super().__init__() features = [conv(3, 64)] **for** i **in** range(8): features.append(res_block(64)) features += [conv(64,64), upsample(64, 64, scale), nn.BatchNorm2d(64), conv(64, 3)] self.features = nn.Sequential(*features) **def** forward(self, x): **return** self.features(x)
简而言之,正如我现在所倾向的那样,这一切都是通过创建一个包含层的列表来完成的,然后最后变成一个顺序模型,这样我的前向功能就可以了。
这是我们的上采样和上采样有点有趣,因为它没有做两件事(转换或分步跨越卷积或最近邻居上采样后跟1x1转换)。 让我们来谈谈上采样。

这是纸上的图片(实时样式转移和超分辨率的感知损失)。 所以他们说“嘿,我们的做法好多了”,但看看他们的做法。 它里面有文物。 这些只是到处流行,不是吗。 其中一个原因是他们使用转置卷积,我们都知道不使用转置卷积。

这是转换的卷积 [35:39] 。 这是来自这个奇妙的卷积算术论文,也在Theano文档中显示。 如果我们从(蓝色是原始图像)3x3图像到5x5图像(如果我们添加了一层填充,则为6x6),那么所有转置卷积都会使用常规的3x3转换,但它会在白色零像素之间粘贴每一对像素。 这使得输入图像更大,当我们对它进行卷积时,因此给我们更大的输出。 但这显然是愚蠢的,因为当我们到达这里时,例如,九个像素进来,其中八个是零。 所以我们只是浪费了大量的计算。 另一方面,如果我们略微偏离,那么我们的九个中有四个是非零的。 但是,我们只使用一个过滤器/内核,因此根据有多少零进来它不能改变。所以它必须适合两者并且它是不可能的,所以我们最终得到这些工件。

http://deeplearning.net/software/theano/tutorial/conv_arithmetic.html
我们已经学会使其变得更好的一种方法是不在这里放置白色的东西,而是将像素的值复制到这三个位置中的每一个 [36:53] 。 这是最近邻居的上采样。 这当然好一点,但它仍然很糟糕,因为现在我们到达这九个(如上所示),其中4个是完全相同的数字。 当我们跨越一个,那么现在我们完全有不同的情况。 因此,取决于我们的位置,特别是,如果我们在这里,重复次数会少得多:

所以,我们还有这样的问题,即数据中存在浪费的计算和太多的结构,并且它将再次导致工件。 因此,上采样优于转置卷积 - 最好复制它们而不是用零替换它们。 但它还不够好。
相反,我们要做像素洗牌 [37:56] 。 Pixel shuffle是这个子像素卷积神经网络中的一个操作,它有点令人费解,但它有点令人着迷。

我们从输入开始,我们经过一些卷积来创建一些特征映射一段时间,直到最终我们得到具有n [i-1]个特征映射的n [i-1]层。 我们将进行另外3x3转换,我们的目标是从7x7网格单元(我们将要进行3x3升级),因此我们将升级到21x21网格单元。 那么我们能做到的另一种方式是什么呢? 为了简单起见,让我们选择一个面/层 - 所以让我们采取最顶层的过滤器,然后对其进行卷积只是为了看看会发生什么。 我们要做的是我们将使用卷积,其中内核大小(过滤器的数量)比我们需要的大9倍(严格来说)。 因此,如果我们需要64个过滤器,我们实际上将做64次9过滤器。 为什么? 这里,r是比例因子,因此3 2是9,所以这里有九个滤波器来覆盖这些输入层/切片之一。 但我们能做的就是从7x7开始,然后我们把它变成了7x7x9。 我们想要的输出等于7乘3乘7次3.换句话说,这里有相同数量的像素/激活,因为在前一步骤中有激活。 因此,我们可以逐步重新调整这些7x7x9激活以创建7x3乘7x3地图 [40:16] 。 所以我们要做的是我们要在这里拿一个小管(每个网格的左上角),我们将把紫色的一个放在左上角,然后将蓝色放在左上方。右边是浅蓝色,右边是浅蓝色,中间是浅绿色,等等。 因此,左上角的这九个单元中的每一个都将在我们网格的小3x3部分中结束。 然后我们将采用(2,1)并将所有这些9和更多它们带到网格的这些3x3部分,依此类推。 因此,我们将最终在7x3 x 7x3图像中进行这些7x7x9激活中的每一个。
所以首先要认识到的是当然这是在作品的某些定义下工作的,因为我们在这里有一个可学习的卷积,并且它会得到一些梯度,它可以做到最好的工作,它可以填充正确的激活,这样这输出是我们想要的东西。 所以第一步是要意识到这里没有什么特别神奇的东西。 我们可以创建任何我们喜欢的架构。 我们可以随心所欲地移动东西,我们在卷积中的权重将尽我们所能去做。 真正的问题是 - 这是个好主意吗? 对于它来说这是一件更容易的事情,并且它比转换卷积或逐个转换后的逐次取样更灵活吗? 简短的回答是肯定的,简而言之就是这里的卷积发生在低分辨率7x7空间中,这非常有效。 或者,如果我们首先进行上采样然后再进行转换,那么我们的转换将发生在21乘21空间,这是很多计算。 此外,正如我们所讨论的,最近邻上采样版本中存在大量复制和冗余。 事实上,他们实际上在本文中表明,他们有一个后续技术说明,他们提供了更多的数学细节,以确切地说正在做什么工作,并表明这项工作确实更有效率。 这就是我们要做的事情。 对于我们的上采样,我们有两个步骤:
- 3x3转换,r²倍于我们原先想要的频道
- 然后是像素打乱操作,它将每个网格单元格中的所有内容移动到通过这里定位的r网格中的小r 。
所以这里是:

这是一行代码。 这是一个转换器数量为4的滤波器数量,因为我们正在进行两个上采样(2²= 4)。 这是我们的卷积,然后这是我们在PyTorch中内置的像素shuffle。 Pixel shuffle是将每个东西移动到正确位置的东西。 因此,这将通过比例因子2进行上采样。因此,我们需要执行该对数基数2比例时间。 如果比例是4,那么我们将做两次两次两次。 这就是这里的例子。
棋盘图案 [44:19]
大。 你猜怎么着。 这并没有摆脱棋盘图案。 我们仍然有棋盘图案。 所以我确信在极大的愤怒和挫折中,来自Twitter的同一个团队我认为这是回来的时候,他们曾经是一家名为魔术小马的创业公司,Twitter买回来的另一篇论文说好了,这次我们已经摆脱了的棋盘格。

https://arxiv.org/abs/1707.02937
为什么我们还有棋盘? 我们之后仍然有一个棋盘格的原因是,当我们在开始时随机初始化这个卷积内核时,这意味着这里的这个小3x3网格中的这9个像素中的每一个都将完全随机地变化。 但随后下一组3个像素将彼此随机不同,但将与之前3x3部分中的相应像素非常相似。 所以我们将一直重复3x3的事情。 然后,当我们尝试更好地学习时,它从重复的3x3起点开始,这不是我们想要的。 我们实际想要的是这些3x3像素开始时是相同的。 为了使这些3x3像素相同,我们需要为每个滤波器使这9个通道相同。 所以本文的解决方案非常简单。 当我们在开始时随机初始化它时初始化这个卷积时,我们不会完全随机地初始化它。 我们随机初始化一组r²通道,然后将它们复制到另一个r²,因此它们都是相同的。 这样,最初,这些3x3中的每一个都是相同的。 所以这被称为ICNR,这就是我们马上要用的东西。
像素丢失 [46:41]
在我们开始之前,让我们快速看一下。 所以我们得到了这个超级分辨率的ResNet,只需要用大量的ResNet块进行大量的计算,然后进行一些上采样并获得最后三个通道。
然后为了让生活更快,我们将并行运行。 我们想要并行运行它的一个原因是因为Gerardo告诉我们他有6个GPU,这就是他现在的计算机。

所以我确信任何拥有多个GPU的人都有过这方面的经验。 那么我们如何让这些人一起工作呢? 你需要做的就是使用PyTorch模块并使用nn.DataParallel包装它。 完成后,它会将其复制到每个GPU,并自动并行运行。 它可以很好地扩展到两个GPU,可以使用三个GPU,比四个GPU更好,除此之外,表现确实倒退。 默认情况下,它会将其复制到你的所有GPU - 你可以添加一组GPU,否则如果你想避免遇到麻烦,例如,我必须与Yannet共享我的盒子,如果我没有把它放在这里那么她现在会对我大喊大叫或抵制我的班级。 所以这就是你避免遇到Yannet麻烦的方法。
m = to_gpu(SrResnet(64, scale)) m = nn.DataParallel(m, [0,2]) learn = Learner(md, SingleModel(m), opt_fn=optim.Adam) learn.crit = F.mse_loss
这里要注意的一件事是,一旦你这样做,它实际上会修改你的模块 [48:21] 。 因此,如果你现在打印出你的模块,让我们先说它只是一个无穷无尽的顺序,现在你会发现它是一个nn.Sequential嵌入在一个名为Module 。 换句话说,如果你保存了nn.DataParallel东西,然后尝试将它加载回你没有nn.DataParallel东西,它会说它不匹配,因为其中一个嵌入了这个模块属性而另一个则不是。 它甚至可能还取决于你复制到哪些GPU ID。 两种可能的方案:
- 不要保存模块
m,而是保存模块属性m.module因为它实际上是非数据并行位。 - 始终将它放在相同的GPU ID上,然后并行使用数据并每次加载并保存。 这就是我正在使用的。
这对我来说很容易在fast.ai中自动修复,我很快就会这样做,所以它会查找该模块属性并自动处理它。 但就目前而言,我们必须手动完成。 无论如何,了解幕后发生的事情可能很有用。
所以我们得到了我们的模块 [49:46] 。 我发现如果你在volta上运行它会在1080Ti上运行速度提高50或60%,它实际上可以更好地并行化。 有更快的并行化方法,但这是一种非常简单的方法。
我们以通常的方式创造我们的学习器。 我们可以在这里使用MSE丢失,这样就可以将输出像素与我们预期的像素进行比较。 我们可以运行我们的学习速率查找器,我们可以训练它一段时间。
learn.lr_find(start_lr=1e-5, end_lr=10000) learn.sched.plot()
31%|███▏ | 225/720 [00:24<00:53, 9.19it/s, loss=0.0482]

lr=2e-3
learn.fit(lr, 1, cycle_len=1, use_clr_beta=(40,10))
2%|▏ | 15/720 [00:02<01:52, 6.25it/s, loss=0.042] epoch trn_loss val_loss 0 0.007431 0.008192
_[array([0.00819])]_
x,y = next(iter(md.val_dl)) preds = learn.model(VV(x))
以下是我们的意见:
idx=4 show_img(y,idx,normed= **False** )

这是我们的输出。
show_img(preds,idx,normed= **False** );

你可以看到我们设法做的就是训练一个非常先进的残余卷积网络,这个网络学到了蓝色的东西。 这是为什么? 好吧,因为这是我们要求的。 我们说要尽量减少MSE损失。 像素之间的MSE损失实际上最好的方法就是平均像素,即模糊它。 这就是像素丢失不好的原因。 所以我们想要用我们的知觉损失。
show_img(x,idx,normed= **True** );

x,y = next(iter(md.val_dl)) preds = learn.model(VV(x))
show_img(y,idx,normed= **False** )

show_img(preds,idx,normed= **False** );

show_img(x,idx);

感性损失[50:57]
由于感知损失,我们基本上将采用我们的VGG网络,就像我们上周所做的那样,我们将在获得maxpool之前找到块索引。
**def** icnr(x, scale=2, init=nn.init.kaiming_normal): new_shape = [int(x.shape[0] / (scale ** 2))] + list(x.shape[1:]) subkernel = torch.zeros(new_shape) subkernel = init(subkernel) subkernel = subkernel.transpose(0, 1) subkernel = subkernel.contiguous().view(subkernel.shape[0], subkernel.shape[1], -1) kernel = subkernel.repeat(1, 1, scale ** 2) transposed_shape = [x.shape[1]] + [x.shape[0]] + list(x.shape[2:]) kernel = kernel.contiguous().view(transposed_shape) kernel = kernel.transpose(0, 1) **return** kernel
m_vgg = vgg16( **True** ) blocks = [i-1 **for** i,o **in** enumerate(children(m_vgg)) **if** isinstance(o,nn.MaxPool2d)] blocks, [m_vgg[i] **for** i **in** blocks]
_([5, 12, 22, 32, 42],_ _[ReLU(inplace), ReLU(inplace), ReLU(inplace), ReLU(inplace), ReLU(inplace)])_
所以这是相同网格大小的每个块的末端。 如果我们只是将它们打印出来,正如我们所期望的那样,其中每一个都是ReLU模块,因此在这种情况下,最后两个块对我们来说不那么有趣。 那里的网格尺寸足够小,足以使它对超分辨率不那么有用。 所以我们将使用前三个。 为了节省不必要的计算,我们将使用前23层VGG,我们将丢弃其余部分。 我们会把它贴在GPU上。 我们根本不会训练这个VGG模型 - 我们只是用它来比较激活。 所以我们将它保持在eval模式,我们将其设置为不可训练。
vgg_layers = children(m_vgg)[:23] m_vgg = nn.Sequential(*vgg_layers).cuda().eval() set_trainable(m_vgg, **False** )
**def** flatten(x): **return** x.view(x.size(0), -1)
就像上周一样,我们将使用SaveFeatures类进行前向挂钩,以保存每个层的输出激活 [52:07] 。
**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** remove(self): self.hook.remove()
所以现在我们拥有了创建感知损失所需的一切,或者我在这里称之为FeatureLoss类。 我们将传递一个层ID列表,我们希望计算内容丢失的层,以及每个层的权重列表。 我们可以遍历每个层ID并创建一个具有前向挂钩功能的对象来存储激活。 因此,在我们的前进中,我们可以继续使用目标(我们正在尝试创建的高分辨率图像)调用我们模型的前向传递。 我们这样做的原因是因为那将调用该钩子函数并在self.sfs (自我点保存功能)中存储我们想要的激活。 现在我们还需要为我们的转换网络输出做到这一点。 所以我们需要克隆这些,因为否则转换网络将继续前进,只是破坏我已经拥有的东西。 所以现在我们可以为conv网络输出做同样的事情,它是损失函数的输入。 所以现在我们有了两件事我们可以将它们与权重一起压缩,所以我们有输入,目标和权重。 然后我们可以在输入和目标之间进行L1损失并乘以层权重。 我唯一做的另一件事就是我也抓住了像素损失,但是我把它减轻了很多。 大多数人不这样做。 我没有看到这样做的论文,但在我看来,它可能会更好一点,因为你有感知内容丢失激活的东西,但真正最好的水平,它也关心单个像素。 这就是我们的损失函数。
**class** **FeatureLoss** (nn.Module): **def** __init__(self, m, layer_ids, layer_wgts): super().__init__() self.m,self.wgts = m,layer_wgts self.sfs = [SaveFeatures(m[i]) **for** i **in** layer_ids] **def** forward(self, input, target, sum_layers= **True** ): self.m(VV(target.data)) res = [F.l1_loss(input,target)/100] targ_feat = [V(o.features.data.clone()) **for** o **in** self.sfs] self.m(input) res += [F.l1_loss(flatten(inp.features),flatten(targ))*wgt **for** inp,targ,wgt **in** zip(self.sfs, targ_feat, self.wgts)] **if** sum_layers: res = sum(res) **return** res **def** close(self): **for** o **in** self.sfs: o.remove()
我们创建了我们的超级分辨率ResNet,告诉它要扩大多少。
m = SrResnet(64, scale)
然后我们将进行像素打乱的卷积的icnr初始化 [54:27] 。 这是非常无聊的代码,我实际上是从其他人那里偷走的。 简而言之,它只是说好了,你有一些你想要初始化的权重张量x所以我们将把它视为它的形状(即特征数量)除以实际中的比例平方特征。 所以这可能是2²= 4因为我们实际上只想保留一组然后然后将它们复制四次,所以我们将它除以4并且我们创建了那个大小的东西,我们默认初始化kaiming_normal初始化。 然后我们只制作它的scale²副本。 其余的只是一点点移动轴。 因此,这将返回一个新的权重矩阵,其中每个初始化的子内核重复r²或scale ²次。 所以细节并不重要。 这里最重要的是我只是通过查看在像素shuffle之前找到实际的转换层并存储它然后我在其权重矩阵上调用icnr以获得我的新权重矩阵。 然后我将新的权重矩阵复制回该层。
conv_shuffle = m.features[10][0][0] kernel = icnr(conv_shuffle.weight, scale=scale) conv_shuffle.weight.data.copy_(kernel);
正如你所看到的,我在这个练习中遇到了很多麻烦,试图实现所有最佳实践 [56:13] 。 我倾向于做一些极端或另一个极端的事情。 我给你看了一个非常hacky的版本,只是稍微有点工作,或者我去了n度,使它工作得很好。 所以这是一个版本,我声称这几乎是最先进的实现。 这是一场竞赛,或者至少是我重新实施竞赛获胜的方法。 我这样做的原因是因为我认为这是一篇罕见的论文,他们实际上得到了很多正确的细节,我希望你能够感受到让所有细节都正确的感觉。 请记住,获得正确的细节是可怕的模糊混乱和非常精致的结果之间的差异。
m = to_gpu(m)
learn = Learner(md, SingleModel(m), opt_fn=optim.Adam)
t = torch.load(learn.get_model_path('sr-samp0'), map_location= **lambda** storage, loc: storage) learn.model.load_state_dict(t, strict= **False** )
learn.freeze_to(999)
**for** i **in** range(10,13): set_trainable(m.features[i], **True** )
conv_shuffle = m.features[10][2][0] kernel = icnr(conv_shuffle.weight, scale=scale) conv_shuffle.weight.data.copy_(kernel);
所以我们再次开始使用DataParallel [57:14] 。
m = nn.DataParallel(m, [0,2]) learn = Learner(md, SingleModel(m), opt_fn=optim.Adam)
learn.set_data(md)
我们将使用我们的VGG模型将我们的标准设置为FeatureLoss,抓住前几个块,这些是我发现非常好的层权重集。
learn.crit = FeatureLoss(m_vgg, blocks[:3], [0.2,0.7,0.1])
lr=6e-3 wd=1e-7
做一个学习率发现者。
learn.lr_find(1e-4, 0.1, wds=wd, linear= **True** )
1%| | 15/1801 [00:06<12:55, 2.30it/s, loss=0.0965] 12%|█▏ | 220/1801 [01:16<09:08, 2.88it/s, loss=0.42]
learn.sched.plot(n_skip_end=1)

适合它一段时间
learn.fit(lr, 1, cycle_len=2, wds=wd, use_clr=(20,10))
epoch trn_loss val_loss 0 0.04523 0.042932 1 0.043574 0.041242
[array([0.04124])]
learn.save('sr-samp0')
learn.save('sr-samp1')
我试图弄清楚其中一些细节是正确的。 但这是我最喜欢的部分是接下来会发生什么。 现在我们已经完成了规模等于2 - 渐进式调整大小。 因此,逐步调整大小是让我们在DAWN工作台上获得ImageNet训练的最佳单一计算机结果的技巧。 这是从小开始逐梯度大的想法。 我只知道有两篇论文使用了这个想法。 一个是GANs论文的逐步调整,它允许训练一个非常高分辨率的GAN,另一个是EDSR论文。 关于渐进式调整大小的好处不仅仅是你早期的迭代,假设你有2x2更小,快4倍。 你还可以使批量大小增加3或4倍。 但更重要的是,他们会更好地概括,因为你在训练期间为你的模型喂食不同大小的图像。 因此,我们能够像大多数人一样训练ImageNet的一半迭代。 我们的迭代更快,而且它们更少。 因此,渐进式调整大小是特别是如果你从头开始训练(我不太确定它是否对微调转移学习有用,但如果你从头开始训练),你可能几乎一直都想做。
进步调整大小 [59:07]
所以下一步是一直回到顶部并改为4级,32个批量大小,重新启动。 在我这样做之前我保存了模型。

回过头来,这就是为什么在这里重新加载会有点烦恼,因为我现在需要做的是我需要加载我保存的模型。

但是有一个小问题,我现在还有一个上采样层,而不是以前从2x2到4x4。 我的循环现在循环两次,而不是一次。 因此,它增加了额外的转换网和额外的像素随机播放。 那么我如何为不同的网络加载权重呢?

答案是我在PyTorch load_state_dict使用了一个非常方便的东西。 这就是lean.load在幕后调用的内容。 如果我传递此参数strict=False然后它说“好吧,如果你不能填写所有层,只需填写你可以的层。”所以在以这种方式加载模型后,我们将结束在某些东西中,它可以加载到它可以的所有层中,并且新的一个转换层将被随机初始化。

然后我冻结我的所有层,然后解冻上采样部分 [1:00:45] 然后在我新添加的额外层上使用icnr 。 然后我可以继续学习。 那么剩下的就是一样的。
如果你尝试复制此内容,请不要只是从上到下运行。 意识到它涉及到一些跳跃。
learn.load('sr-samp1')
lr=3e-3
learn.fit(lr, 1, cycle_len=1, wds=wd, use_clr=(20,10))
epoch trn_loss val_loss 0 0.069054 0.06638
[array([0.06638])]
learn.save('sr-samp2')
learn.unfreeze()
learn.load('sr-samp2')
learn.fit(lr/3, 1, cycle_len=1, wds=wd, use_clr=(20,10))
epoch trn_loss val_loss 0 0.06042 0.057613
[array([0.05761])]
learn.save('sr1')
learn.sched.plot_loss()

**def** plot_ds_img(idx, ax= **None** , figsize=(7,7), normed= **True** ): **if** ax **is** **None** : fig,ax = plt.subplots(figsize=figsize) im = md.val_ds[idx][0] **if** normed: im = denorm(im)[0] **else** : im = np.rollaxis(to_np(im),0,3) ax.imshow(im) ax.axis('off')
fig,axes=plt.subplots(6,6,figsize=(20,20)) **for** i,ax **in** enumerate(axes.flat): plot_ds_img(i+200,ax=ax, normed= **True** )

x,y=md.val_ds[215]
y=y[ **None** ]
learn.model.eval() preds = learn.model(VV(x[ **None** ])) x.shape,y.shape,preds.shape
_((3, 72, 72), (1, 3, 288, 288), torch.Size([1, 3, 288, 288]))_
learn.crit(preds, V(y), sum_layers= **False** )
[Variable containing: 1.00000e-03 * 1.1935 [torch.cuda.FloatTensor of size 1 (GPU 0)], Variable containing: 1.00000e-03 * 8.5054 [torch.cuda.FloatTensor of size 1 (GPU 0)], Variable containing: 1.00000e-02 * 3.4656 [torch.cuda.FloatTensor of size 1 (GPU 0)], Variable containing: 1.00000e-03 * 3.8243 [torch.cuda.FloatTensor of size 1 (GPU 0)]]
learn.crit.close()
你训练的时间越长,它就越好 [1:01:18] 。 我最终训练了大约10个小时,但如果你不那么耐心,你仍然可以更快地得到非常好的结果。 所以我们可以尝试一下,这就是结果。 左边是我的像素化鸟,右边是上采样版本。 它确实发明了色彩。 但它弄清楚它是什么样的鸟,它知道这些羽毛的意思。 因此,它设想了一组与这些精确像素兼容的羽毛,这是天才。 同样的头部后方。 你无法分辨这些蓝点是什么意思。 但如果你知道这种鸟在这里有一系列的羽毛,你知道它们必须是这样的。 然后你可以弄清楚羽毛是否必须是这样的,当它们像素化时,它们最终会在这些斑点中出现。 因此,鉴于它对这种鸟的确切种类的了解,它实际上是逆向工程,它必须如何创造这种输出。 这太神奇了。 它也从周围的所有迹象中知道这里的区域(背景)几乎肯定是模糊的。 所以它实际上重建了模糊的植被。 如果没有完成所有这些事情,它就不会有如此好的损失函数。 因为最后,它必须匹配激活说“噢,这里有一根羽毛,它看起来很蓬松,而且正朝这个方向发展”等等。
_,axes=plt.subplots(1,2,figsize=(14,7)) show_img(x[ **None** ], 0, ax=axes[0]) show_img(preds,0, normed= **True** , ax=axes[1])

好吧,这让我们结束了超级分辨率 [1:03:18] 。 不要忘记查看问Jeremy什么线程。
问杰里米什么的
问题 :fast.ai和本课程的未来计划是什么? 会有第3部分吗? 如果有第3部分,我真的很想接受它 [1:04:11] 。
杰里米 :我不太确定。 总是很难猜到。 我希望会有某种后续行动。 去年,在第2部分之后,其中一个学生开始了每周一次的读书俱乐部,通过Ian Goodfellow深度学习书,Ian实际上进来并介绍了相当多的章节,并且有人,专家,每个人章节。 这是一个非常酷的部分3.在很大程度上,它将取决于你,社区,提出想法并帮助实现它们,我非常渴望提供帮助。 我有很多想法,但我很担心说出来,因为我不确定哪些会发生,哪些不会发生。 但是,在你想要发生事情的过程中,我所拥有的支持越多,他们发生的可能性就越大。
问题 :你从创业的道路上走出来的经历是什么? 你是一直是企业家还是从一家大公司开始并过渡到创业公司? 你是从学术界到初创公司,创业公司还是学术界? [1:05:13]
杰里米 :不,我绝对不是学术界人士。 我完全是一个假学者。 我从麦肯锡和公司开始,这是一家战略公司,当时我18岁,这意味着我不能真正上大学,所以它并没有真正出现。 然后花了8年时间帮助真正的大公司解决战略问题。 我一直想成为一名企业家,计划只在麦肯锡度过两年,唯一让我生活中真正后悔的事情就是不坚持这个计划而浪费了八年。 所以两年会很完美。 但后来我开始创业,在澳大利亚创办了两家公司。 关于这一点最好的部分是我没有得到任何资金所以我所做的所有钱都是我的,或者决定是我和我的伙伴。 我完全专注于利润和产品以及客户和服务。 虽然我在旧金山找到了,我很高兴我来到这里,所以我们两个来到这里为Kaggle,Anthony和我,并为这家真正的新公司筹集了可观的金额1100万美元。 这真的很有趣,但它也真的让人分心,试图担心扩展和风险投资想要了解你的业务发展计划是什么,而且实际上并没有真正需要实现盈利。 所以我在Enlitic遇到了一些同样的问题,我再次快速筹集了大量资金1500万美元并且引起了很多分心。 我想尝试引导自己的公司,专注于通过出售利润来赚钱,然后将其重新投入公司,它运作得非常好。 因为在五年内我们从5个月内和5年内赚取利润,我们赚取的利润不仅仅是为了支付我们所有人和我们自己的工资,而且还看到我的银行帐户在增长,并且在10年后卖掉了它对于一大笔钱,还不足以让VC兴奋但足以让我再也不用担心钱了。 所以我认为引导一家公司至少是湾区人们似乎并不欣赏这个想法有多好。
问题 :如果你今天25岁,仍然知道你知道你想在哪里使用AI? 你现在正在做什么或者想在接下来的两年里工作 [1:08:10] ?
杰里米 :你应该忽略它的最后一部分。 我甚至都不回答。 无论我在哪里都无所谓。 你应该做的是利用你对你的域名的了解。 因此,我们这样做的主要原因之一是让那些具有招聘,油田调查,新闻,行动主义等方面背景的人能够解决你的问题。 对你来说真正明显的是什么是真正的问题,对你来说,你拥有什么数据以及在哪里找到它是非常明显的。 对于其他所有人而言,这些都是非常困难的。 因此,开始说“哦,我知道现在深入学习,我会去寻找应用它的东西”的人基本上永远不会成功的地方,就像“哦,我已经花了25年时间专门招聘法律公司,我知道关键问题是这件事,我知道这条数据完全解决了它,所以我现在就打算这样做,我已经知道该打电话给谁或者真正开始把它卖给“。 他们往往会赢。 如果你除了学术之外什么都没做,那么更多的可能就是你的爱好和兴趣。 所以每个人都有兴趣爱好。 我要说的主要是请不要专注于为数据科学家建立工具或供软件工程师使用,因为每个数据科学家都知道数据科学家的市场,而只有你了解分析石油调查世界的市场或理解听力学研究或你做的任何事情。
问题 :鉴于你向我们展示了将图像识别中的转移学习应用于NLP,看起来在关注整个ML领域发生的所有发展以及如果你专注于一个领域你可能会错过其他浓度的一些重大进展。 你如何留意整个领域的所有进步,同时还有时间深入挖掘你的特定领域 [1:10:19] ?
杰里米 :是的,那真棒。 我的意思是这是本课程的关键信息之一。 在不同的地方做了很多好的工作,人们非常专业,大多数人都不知道。 如果我能够在开始研究NLP的六个月内获得NLP的最新成果,我认为这更多地讲述了NLP而不是关于我,坦率地说。 这有点像创业的事情。 你选择了你所知道的领域和转移的东西,比如“哦,我们可以用深度学习来解决这个问题”,或者在这种情况下,我们可以用这种计算机视觉理念来解决这个问题。 所以像转学习这样的东西,我敢肯定你有很多机会在其他领域去做塞巴斯蒂安和我在NLP中用NLP分类做的事情。 因此,对你的问题的简短回答是如何保持领先于跟踪我的Twitter收藏的提示,我的方法是在Twitter上关注很多人并将它们放入Twitter的收藏夹中。 从字面上看,每当我遇到一些有趣的东西时,我点击收藏夹。 我这样做有两个原因。 首先是当下一个课程出现时,我会通过我的最爱来找到我想要学习的东西。 第二个是你可以做同样的事情。 然后你深入研究,几乎没关系。 每当我看到一些东西时,我发现它变得非常有趣和重要。 因此,选择一些你认为解决问题的东西,由于某些原因,这个问题实际上是有用的,而且它似乎并不是很受欢迎,这与其他人所做的相反。 其他人都在处理其他人已经在研究的问题,因为他们似乎很受欢迎。 我不太明白这种思路,但似乎很常见。
问题 :深度学习在表格数据上使用是否过度? 什么时候在表格数据 [1:12:46] 上使用DL代替ML更好?
杰里米 :这是一个真正的问题,还是你只是把它放在那里,以便我指出雷切尔托马斯只是写了一篇文章? http://www.fast.ai/2018/04/29/categorical-embeddings/
所以Rachel刚刚写了这篇文章,Rachel和我花了很长时间谈论它,简短的回答是我们认为在表格数据上使用深度学习是很好的。 实际上,在Rachel的Twitter流中出现的所有丰富复杂的重要和有趣的东西,包括从Rohingya种族灭绝到人工智能公司的最新道德违规行为,到目前为止得到社区最多关注和参与的一个问题是关于是否称为表格数据或结构化数据。 所以,是的,问电脑人如何命名,你会得到很多兴趣。 这里有一些非常好的链接来自Instacart和Pinterest以及其他在这方面做得很好的人。 参加数据研究所会议的任何人都会看到Jeremy Stanley关于他们在Instacart所做的非常酷的工作的演讲。
Rachel :在撰写这篇文章时,我非常依赖第1部分的第3课和第4课,你可能对此很熟悉。
Jeremy :Rachel在帖子中问我如何判断你是否应该使用像GBM或随机森林或神经网络这样的决策树集合,我的回答是我还是不知道。 我所知道的任何人都没有以任何特别有意义的方式完成这项研究。 我想,那里有一个问题需要回答。 我的方法是尝试通过fast.ai库尽可能地使这两个东西都可访问,这样你就可以尝试它们并看看哪些有效。 我就是做这个的。
问题 :强化学习流行度近年来逐渐上升。 你对强化学习有什么看法? fast.ai将来会考虑在流行的RL技术中覆盖一些基础 [1:15:21] 吗?
杰里米 :我仍然不相信强化学习。 我认为这是一个有趣的问题需要解决,但我们很清楚我们有一个解决这个问题的好方法。 所以问题在于,它确实是信用问题的延迟。 所以我想学习打乒乓球,我上下移动,三分钟后我发现我是否赢得了乒乓球比赛 - 我采取了哪些行动实际上有用? 所以对我来说,计算输出相对于那些输入的梯度的想法,信用是如此延迟,以至于那些衍生物似乎不是很有趣。 到目前为止,我在这四门课程的每一门课程中都经常提到这个问题。 我总是说同样的话。 我很高兴终于最近有一些结果显示实际上基本上随机搜索通常比强化学习更好,所以基本上发生的事情是资金充足的公司拥有大量的计算能力,将所有这些都强加于强化学习问题并获得好的结果,然后人们说“哦,这是因为强化学习”,而不是大量的计算能力。 或者他们使用非常周到和聪明的算法,如卷积神经网络和蒙特卡罗树搜索的组合,就像他们使用Alpha Go的东西获得了很好的结果,人们错误地说“哦,那是因为强化学习”,当它不是真的强化学习。 所以我对解决这些更通用的优化类型问题非常感兴趣,而不仅仅是预测问题,这就是这些延迟信用问题看起来的样子。 但是我认为我们还没有得到足够好的最佳实践,我已经准备好教学并且说我必须教你这个东西,因为我认为它明年仍然有用。 所以我们会继续观察,看看会发生什么。
超级分辨率网络到风格转移网络 [1:17:57]

我们现在将超级分辨率网络转变为样式传输网络。 我们很快就会做到这一点。 我们基本已经有了一些东西 x是我的输入图像,我将有一些损失函数,我又有了一些神经网络。 这次我们的输入与我们的输出一样大,而不是完成大量计算然后在最后进行上采样的神经网络。 所以我们先做一些下采样。 然后我们的电脑,然后我们的上采样。 所以这是我们要做的第一个改变 - 我们将添加一些下采样,以便在我们的网络前面有一些跨越2个卷积层。 第二个不仅仅是比较yc和x在这里是相同的东西。 所以我们基本上会说我们的输入图像应该在结束时看起来像。 具体来说,我们将通过VGG对其进行比较并在其中一个激活层进行比较来进行比较。 然后它的风格应该看起来像一些我们将要做的绘画,就像我们通过在多个层次上查看Gram矩阵对应的Gatys方法一样。 所以基本上就是这样。 所以这应该是超级直接的。 它真的结合了我们已经完成的两件事。
风格转移网 [1:19:19]
因此所有这些代码都开始相同,除了我们没有高分辨率和低分辨率,我们只有一个256大小。
%matplotlib inline %reload_ext autoreload %autoreload 2
**from** **fastai.conv_learner** **import** * **from** **pathlib** **import** Path torch.cuda.set_device(0)
torch.backends.cudnn.benchmark= **True**
PATH = Path('data/imagenet') PATH_TRN = PATH/'train'
fnames_full,label_arr_full,all_labels = folder_source(PATH, 'train') fnames_full = ['/'.join(Path(fn).parts[-2:]) **for** fn **in** fnames_full] list(zip(fnames_full[:5],label_arr_full[:5]))
_[('n01440764/n01440764_9627.JPEG', 0),_ _('n01440764/n01440764_9609.JPEG', 0),_ _('n01440764/n01440764_5176.JPEG', 0),_ _('n01440764/n01440764_6936.JPEG', 0),_ _('n01440764/n01440764_4005.JPEG', 0)]_
all_labels[:5]
_['n01440764', 'n01443537', 'n01484850', 'n01491361', 'n01494475']_
np.random.seed(42) _# keep_pct = 1._ _# keep_pct = 0.01_ keep_pct = 0.1 keeps = np.random.rand(len(fnames_full)) < keep_pct fnames = np.array(fnames_full, copy= **False** )[keeps] label_arr = np.array(label_arr_full, copy= **False** )[keeps]
arch = vgg16 _# sz,bs = 96,32_ sz,bs = 256,24 _# sz,bs = 128,32_
**class** **MatchedFilesDataset** (FilesDataset): **def** __init__(self, fnames, y, transform, path): self.y=y **assert** (len(fnames)==len(y)) super().__init__(fnames, transform, path) **def** get_y(self, i): **return** open_image(os.path.join(self.path, self.y[i])) **def** get_c(self): **return** 0
val_idxs = get_cv_idxs(len(fnames), val_pct=min(0.01/keep_pct, 0.1)) ((val_x,trn_x),(val_y,trn_y)) = split_by_idx(val_idxs, np.array(fnames), np.array(fnames)) len(val_x),len(trn_x)
(12800, 115206)
img_fn = PATH/'train'/'n01558993'/'n01558993_9684.JPEG'
tfms = tfms_from_model(arch, sz, tfm_y=TfmType.PIXEL) datasets = ImageData.get_ds(MatchedFilesDataset, (trn_x,trn_y), (val_x,val_y), tfms, path=PATH_TRN) md = ImageData(PATH, datasets, bs, num_workers=16, classes= **None** )
denorm = md.val_ds.denorm
**def** show_img(ims, idx, figsize=(5,5), normed= **True** , ax= **None** ): **if** ax **is** **None** : fig,ax = plt.subplots(figsize=figsize) **if** normed: ims = denorm(ims) **else** : ims = np.rollaxis(to_np(ims),1,4) ax.imshow(np.clip(ims,0,1)[idx]) ax.axis('off')
型号 [1:19:30]
我的模型是一样的。 我在这里做的一件事是我根本没有为这个做任何花哨的最佳实践。 部分是因为似乎没有。 与超分辨率的东西相比,这种方法的跟进很少。 我们马上谈谈为什么。 所以你会看到,这看起来更正常。
**def** conv(ni, nf, kernel_size=3, stride=1, actn= **True** , pad= **None** , bn= **True** ): **if** pad **is** **None** : pad = kernel_size//2 layers = [nn.Conv2d(ni, nf, kernel_size, stride=stride, padding=pad, bias= **not** bn)] **if** actn: layers.append(nn.ReLU(inplace= **True** )) **if** bn: layers.append(nn.BatchNorm2d(nf)) **return** nn.Sequential(*layers)
我有批量标准层。 我这里没有缩放因子。
**class** **ResSequentialCenter** (nn.Module): **def** __init__(self, layers): super().__init__() self.m = nn.Sequential(*layers)
**def** forward(self, x): **return** x[:, :, 2:-2, 2:-2] + self.m(x)
**def** res_block(nf): **return** ResSequentialCenter([conv(nf, nf, actn= **True** , pad=0), conv(nf, nf, pad=0)])
我没有像素shuffle - 它只是使用正常的上采样,然后是1x1 conf。所以这更正常。
def upsample(ni,nf): return nn.Sequential(nn.Upsample(scale_factor = 2),conv(ni,nf))
他们在论文中提到的一件事是他们有很多问题,零填充创建工件和他们解决的方式是通过在开始时添加40像素的反射填充。所以我做了同样的事情,然后他们在他们的Res块中使用零填充。现在,如果你的Res块中的卷积中没有填充,那么这意味着你的ResNet的两个部分将不会再加起来,因为你在两个卷积的每一个上都丢失了一个像素。所以我ResSequential已成为ResSequentialCenter并且我已经移除了那些好细胞两侧的最后2个像素。除此之外,这与我们以前的基本相同。
class StyleResnet(nn.Module): def __init __(self): super().__init__() features = [nn.ReflectionPad2d(40),conv(3,32,9),conv(32,64,stride = 2),conv(64,128,stride = 2)] for i in range(5):features .append(res_block(128))feature + = [upsample(128,64),upsample(64,32 ),conv(32,3,9,actn = False)] self.features = nn.Sequential(* features) def forward(self,x):return self.features(x)
风格形象 [1:21:02]
那么我们就可以带来我们的星夜图片了。
style_fn = PATH /'style'/'starry_night.jpg'style_img = open_image(style_fn)style_img.shape
(1198,1513,3)
plt.imshow(style_img);

h,w,_ = style_img.shape rat = max(sz / h,sz / h)res = cv2.resize(style_img,(int(w * rat),int(h * rat)),interpolation = cv2.INTER_AREA )resz_style = res [:sz,-sz:]
我们可以调整它的大小。
plt.imshow(resz_style);

我们可以通过我们的转变来抛弃它
style_tfm,_ = tfms [1](resz_style,resz_style)
style_tfm = np.broadcast_to(style_tfm [ 无 ],(bs,)+ style_tfm.shape)
只是为了让我的大脑更容易处理这个方法,我采用了我们的变换风格图像,在3 x 256 x 256的转换后,我制作了一个小批量。我的批量大小为24 - 24份。它只是简单地做了一些批处理算法,而不用担心一些广播。它们实际上不是24份。我np.broadcast以前基本上假装24件。
style_tfm.shape
(24,3,256,256)
感性损失 [1:21:51]
就像以前一样,我们创建一个VGG,抓住最后一个块。这次我们将使用所有这些层,因此我们将所有内容保持到第43层。
m_vgg = vgg16(真)
blocks = [i-1 for i,o in enumerate(children(m_vgg)) if isinstance(o,nn.MaxPool2d)] blocks,[m_vgg [i] for i in blocks [1:]]
([
5,12,22,32,42 ],[ReLU(inplace),ReLU(inplace),ReLU(inplace),ReLU(inplace)])
vgg_layers = children(m_vgg)[:43] m_vgg = nn.Sequential(* vgg_layers).cuda()。eval() set_trainable(m_vgg, **False** )
def flatten(x):return x.view(x.size(0), - 1)
**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** remove(self): self.hook.remove()
def ct_loss(input,target):返回 F.mse_loss(输入,目标)
def gram(输入):b,c,h,w = input.size()x = input.view(b,c,-1) return torch.bmm(x,x.transpose(1,2))/( C * H * W)* 1E6
def gram_loss(input,target): return F.mse_loss(gram(input),gram(target [:input.size(0)]))
因此,现在我们的综合损失将加上第三个区块的内容损失加上所有具有不同权重的区块的Gram损失。再一次,回到尽可能正常的一切,我已经回到上面使用MSE了。基本上发生的事情是我在正确训练时遇到了很多麻烦。所以我逐渐删除了诡计后的技巧,最终只是“好吧,我只是想让它变得平淡无奇”。
[1:22:37] ,上周的Gram矩阵是错误的。它仅适用于批量大小为1,我们只有一个批量大小,所以这很好。我使用矩阵乘法,这意味着每批次都与其他批次进行比较。实际上,你需要使用批处理矩阵multiple(torch.bmm),它会对每批次进行矩阵乘法运算。所以这是需要注意的。
class CombinedLoss(nn.Module): def __init __(self,m,layer_ids,style_im,ct_wgt,style_wgts): super().__init__() self.m,self.ct_wgt,self.style_wgts = m,ct_wgt,style_wgts self.sfs = [saveFeatures(m [i])for i in layer_ids] m(VV(style_im))self.style_feat = [V(o。 features.data.clone()) 为 ø 在 self.sfs]
def forward(self,input,target,sum_layers = True):self.m(VV(target.data))targ_feat = self.sfs [2] .features.data.clone()self.m(输入)inp_feat = [ o.in self.sfs 中的 o 的特征] res = [ct_loss(inp_feat [2],V(targ_feat))* self.ct_wgt] res + = [gram_loss(inp,targ)* wgt for inp,targ,wgt in zip (inp_feat,self.style_feat,self.style_wgts)] 如果 sum_layers:RES =总和(RES) 返回水库 DEF接近(个体): 用于 Ó 在 self.sfs:o.remove()
所以我有Gram矩阵,我在Gram矩阵之间做MSE损失,我按样式权重加权,所以我创建了ResNet。
m = StyleResnet()m = to_gpu(m)
learn =学习器(md,SingleModel(m),opt_fn = optim.Adam)
我创建了我在VGG网络中传递的组合损失,传递了块ID,传递了变换后的星夜图像,你会看到从这里开始,我用VGG模型向前传递那个星夜图像为了我可以保存它的功能。请注意,现在我不做任何数据扩充非常重要,因为我已经为特定的非扩充版本保存了样式功能。因此,如果我增加它,它可能会产生一些小问题。但这很好,因为我已经完成了所有ImageNet的处理。无论如何,我真的不需要进行数据扩充。
learn.crit = CombinedLoss(m_vgg,blocks [1:],style_tfm,1e4,[0.025,0.275,5。,0.2])
wd=1e-7
learn.lr_find(wds = wd)learn.sched.plot(n_skip_end = 1)
1%|▏| 7/482 [00:04 <05:32,1.43it / s,损失= 2.48e + 04] 53%|█████▎| 254/482 [02:27 <02:12,1.73it / s,损失= 1.13e + 12]

LR = 5E-3
所以我有我的损失函数,我可以继续适应 [1:24:06] 。这里根本没有什么聪明的。
learn.fit(lr,1,cycle_len = 1,wds = wd,use_clr =(20,10))
epoch trn_loss val_loss 0 105.351372 105.833994
[阵列([105.83399])]
learn.save( '风格-2')
的x,y = md.val_ds [201]
learn.model.eval() preds = learn.model(VV(x [ None ]))x.shape,y.shape,preds.shape
((3,256,256),(3,256,256),torch.Size([1,3,256,256]))
最后,我有我的sum_layers=False所以我可以看到每个部分的样子,看到它们是平衡的。我终于可以把它弹出来了
learn.crit(preds,VV(y [ None ]),sum_layers = False)
[变量包含:
53.2221
_[torch.cuda.FloatTensor of size 1 (GPU 0)], Variable containing:_
3.8336
_[torch.cuda.FloatTensor of size 1 (GPU 0)], Variable containing:_
4.0612
_[torch.cuda.FloatTensor of size 1 (GPU 0)], Variable containing:_
5.0639
_[torch.cuda.FloatTensor of size 1 (GPU 0)], Variable containing:_
53.0019
[大小为1的torch.cuda.FloatTensor(GPU 0)]]
learn.crit.close()
_,axes = plt.subplots(1,2,figsize =(14,7))show_img(x [ None ],0,ax = axes [0])show_img(preds,0,ax = axes [1])

所以我提到这应该很简单,但是我花了大约4天的时间,因为我发现这非常繁琐,实际上让它起作用 [1:24:26]。所以,当我终于在早上起床时,我对雷切尔说“猜猜是什么,它训练正确。”雷切尔说:“我从未想过会发生这种情况。”它一直看起来很糟糕,而且它真的是关于正确的内容损失和风格损失的混合以及风格损失层次的混合。最糟糕的是,我需要很长时间才能训练有线电视新闻网,我还不知道在我认为它做得不好之前需要多长时间训练它。我应该再训练一下吗?而且我不知道所有的小细节似乎都没有稍微改变它,但只是它会一直完全崩溃。所以我有点提到这一点,只要记住你在这里看到的最终答案是在我整个星期几乎总是不工作之后,直到最后它终于完成的最后一分钟。即使对于看起来似乎不太可能困难的事情,因为这结合了我们已经有效的两件事。另一个是要小心我们如何解释作者声称的内容。

这种风格转移是如此繁琐 [1:26:10]。在做完之后,它让我想到为什么我会烦恼,因为现在我有一些东西需要几个小时来创建一个可以将任何类型的照片转换成一种特定风格的网络。我似乎不太可能想要任何东西。我认为有用的唯一原因是在视频中做一些艺术性的东西,我想把每一帧变成某种风格。这是一个非常小众的想做的事情。但是当我看着报纸时,桌子上写着“哦,我们比Gatys的方法快了一千倍,这是一个明显毫无意义的事情。”这是一个令人难以置信的误导性事情,因为它忽略了每种风格的所有训练时间,我发现这令人沮丧,因为斯坦福大学这样的团体显然知道更好或者应该更好地了解,但我仍然认为学术界鼓励人们做出这些荒谬的盛大主张。它也完全忽略了这种极其敏感的繁琐的训练过程,因此本文在出版时就被广泛接受了。我记得每个人都在推特上说“哇,你知道这些斯坦福人已经找到了这种方式进行风格转移的速度快了一千倍。”显然有人说这是该领域的顶尖研究人员,显然没有人真正理解它,因为没有人说“我不明白为什么这对我来说非常有用,而且我也试过了,并且让它全部起作用是非常繁琐的。”直到18个月后我终于回到它并且有点像“等一下,这有点愚蠢。“所以这就是答案,我想,为什么避风港的问题’人们对此进行了跟进,以创建真正令人惊叹的最佳实践和更好的方法,例如论文的超分辨率部分。我认为答案是因为它是愚蠢的。所以我认为论文的超分辨率部分显然不是愚蠢的。它已得到改进和改进,现在我们拥有出色的超高分辨率。而且我认为我们可以从降低噪音,大色彩化,大倾角去除,很好的交互式神器去除等方面得出。所以我认为这里有很多很酷的技术。它还利用了我们一直在学习并且越来越好的东西。所以我认为论文的超分辨率部分显然不是愚蠢的。它已得到改进和改进,现在我们拥有出色的超高分辨率。而且我认为我们可以从降低噪音,大色彩化,大倾角去除,很好的交互式神器去除等方面得出。所以我认为这里有很多很酷的技术。它还利用了我们一直在学习并且越来越好的东西。所以我认为论文的超分辨率部分显然不是愚蠢的。它已得到改进和改进,现在我们拥有出色的超高分辨率。而且我认为我们可以从降低噪音,大色彩化,大倾角去除,很好的交互式神器去除等方面得出。所以我认为这里有很多很酷的技术。它还利用了我们一直在学习并且越来越好的东西。
细分 [1:29:13]

最后,我们来谈谈细分。这来自着名的CamVid数据集,它是学术分割数据集的典型示例。基本上你可以看到我们做的是我们从图片开始(它们实际上是这个数据集中的视频帧),我们有一些标签,它们实际上不是颜色 - 每个标签都有一个ID,ID被映射到颜色。所以红色可能是1,紫色可能是2,浅粉红色可能是3,所以所有的建筑都是一类,所有的汽车都是另一类,所有人都是另一类,所有的道路都是另一类,等等。所以我们在这里实际做的是每个像素的多类分类。你可以看到,有时候这种多类分类真的很棘手 - 就像这些分支一样。虽然,有时标签真的不那么好。你可以看到,这非常粗糙。这就是我们要做的事情。
我们将进行分割,因此它很像边框。但是,我们实际上要用它的类来标记每个像素,而不仅仅是在每个东西周围找到一个方框。实际上,它实际上要容易得多,因为它非常适合我们的CNN样式,我们可以创建任何CNN,其中输出是N×M网格,包含从0到C的整数,其中有C类。然后我们可以使用softmax激活的交叉熵损失,我们就完成了。我实际上可以在那里停止课程,你可以使用你在第1课和第2课中学到的完全相同的方法,你会得到一个非常好的结果。所以首先要说的是,这实际上并不是一件非常艰难的事情。但我们会尽力做到这一点。
这样简单 [1:31:26]
让我们从真正简单的方式开始吧。我们将使用Kaggle Carvana比赛,你可以像往常一样使用Kaggle API下载它。
%matplotlib inline %reload_ext autoreload %autoreload 2
**from** **fastai.conv_learner** **import** * **from** **fastai.dataset** **import** * **from** **pathlib** **import** Path **import** **json**
建立
有一个火车文件夹包含一堆图像,这是一个独立变量,一个train_masks文件夹是因变量,它们如下所示。

在这种情况下,就像猫和狗一样,我们要简单而不是进行多类分类,我们将进行二元分类。但当然,多级只是更一般的版本 - 分类交叉熵或二进制类熵。概念上没有差异,因此因变量只是零和1,其他自变量是常规图像。
为了做到这一点,了解汽车的外观真的很有帮助。因为我们真正想做的是弄清楚这是一辆汽车及其方向,并将白色像素放在我们期望汽车基于图片和他们对汽车外观的理解的地方。
PATH = Path('data/carvana') list(PATH.iterdir())
[PosixPath('data / carvana / train_masks.csv'),
PosixPath('data / carvana / train_masks-128'),
PosixPath('data / carvana / sample_submission.csv'),
PosixPath('data / carvana / train_masks_png') ,
PosixPath('data / carvana / train.csv'),
PosixPath('data / carvana / train-128'),
PosixPath('data / carvana / train'),
PosixPath('data / carvana / metadata.csv') ,
PosixPath('data / carvana / tmp'),
PosixPath('data / carvana / models'),
PosixPath('data / carvana / train_masks')]
MASKS_FN = 'train_masks.csv' META_FN = 'metadata.csv' TRAIN_DN = 'train' MASKS_DN ='train_masks'
masks_csv = pd.read_csv(PATH/MASKS_FN) masks_csv.head()

原始数据集也带有这些CSV文件 [1:32:44] 。除了从中获取图像列表之外,我并没有真正使用它们。
meta_csv = pd.read_csv(PATH/META_FN) meta_csv.head()

**def** show_img(im, figsize= **None** , ax= **None** , alpha= **None** ): **if** **not** ax: fig,ax = plt.subplots(figsize=figsize) ax.imshow(im, alpha=alpha) ax.set_axis_off() **return** ax
CAR_ID ='00087a6bd4dc'
列表((PATH / TRAIN_DN).iterdir())[:5]
[PosixPath('data / carvana / train / 5ab34f0e3ea5_15.jpg'),PosixPath('data / carvana / train / de3ca5ec1e59_07.jpg'),PosixPath('data / carvana / train / 28d9a149cb02_13.jpg'),PosixPath('data /carvana/train/36a3f7f77e85_12.jpg'),PosixPath('data / carvana / train / 843763f47895_08.jpg')]
Image.open(PATH / TRAIN_DN / f' {CAR_ID} _01.jpg')。resize((300,200))

列表((PATH / MASKS_DN).iterdir())[:5]
[PosixPath('data / carvana / train_masks / 6c0cd487abcd_03_mask.gif'),PosixPath('data / carvana / train_masks / 351c583eabd6_01_mask.gif'),PosixPath('data / carvana / train_masks / 90fdd8932877_02_mask.gif'),PosixPath('data /carvana/train_masks/28d9a149cb02_10_mask.gif'),PosixPath('data / carvana / train_masks / 88bc32b9e1d9_14_mask.gif')]
Image.open(PATH / MASKS_DN / f' {CAR_ID} _01_mask.gif')。resize((300,200))

汽车ID后面的每张图片都有一张01,02等,其中我已经打印出一辆车的所有16张图片,你可以看到基本上这些数字是一辆车的16个方向 [1:32:58] 。我认为本次比赛中没有人真正使用过这些定位信息。我相信他们都把车的图像分开处理了。
**对于**范围(16)中的i **,** ims = [open_image(PATH / TRAIN_DN / f' {CAR_ID} _ {i + 1:02d} .jpg' )]
fig,axes = plt.subplots(4,4,figsize =(9,6)) for i,ax in enumerate(axes.flat):show_img(ims [i],ax = ax) plt.tight_layout(pad=0.1)

调整大小和转换 [1:33:27]
这些图像非常大 - 超过1000×1000,只需打开JPEG并调整它们的速度很慢。所以我处理了所有这些。OpenCV也无法处理GIF文件,所以我转换了它们。
问题:有人会如何最初获得这些面具进行训练?机械土耳其人什么的 [1:33:48] ?是的,只是很多无聊的工作。可能有一些工具可以帮助你进行一些边缘捕捉,这样人类就可以粗略地完成它,然后只需微调它出错的位。这些标签很贵。因此,我真正想要研究的一件事是深度学习增强型交互式标签工具,因为这显然可以帮助很多人。
我在这里有一个小部分你可以运行,如果你想。你可能想要。它将GIF转换为PNG,因此只需使用PIL打开,然后将其保存为PNG,因为OpenCV没有GIF支持。按照这种方式的惯例,我使用ThreadPool来做,所以我可以利用并行处理。然后还创建一个单独的目录train-128,train_masks-128其中包含128 x 128个已调整大小的版本。
如果你在这个过程中尽早做到这一点,那就是让你保持理智的东西。因此,只要你获得新的数据集,就要认真考虑创建一个更小的版本,以提高生活。每当你发现自己在计算机上等待时,请尝试考虑创建较小版本的方法。
(PATH /'train_masks_png')。mkdir(exist_ok = True)
def convert_img(fn):fn = fn.name Image.open(PATH /'train_masks'/ fn).save(PATH /'train_masks_png'/ f' {fn [: - 4]} .png')
文件=列表((PATH / 'train_masks')iterdir()。) 用的ThreadPoolExecutor(8)如 E:e.map(convert_img,文件)
(PATH /'train_masks-128')。mkdir(exist_ok = True)
def resize_mask(fn):Image.open(fn).resize((128,128))。save((fn.parent.parent)/'train_masks-128'/ fn.name) files = list((PATH /'train_masks_png' )。threaddir()) 与 ThreadPoolExecutor(8)一样 e:e.map(resize_img,files)
(PATH /'train-128')。mkdir(exist_ok = True)
def resize_img(fn):Image.open(fn).resize((128,128))。save((fn.parent.parent)/'train- 128 '/ fn.name) files = list((PATH /'train') )。threaddir()) 与 ThreadPoolExecutor(8)一样 e:e.map(resize_img,files)
所以当你从Kaggle中获取它之后,你可能想要运行这些东西,离开,吃午饭,回来,当你完成后,你将拥有这些较小的目录,我们将在128到128之间使用它们开始。
数据集 [1:35:33]
TRAIN_DN = 'train-128' MASKS_DN = 'train_masks-128' sz = 128 bs = 64
ims = [open_image(PATH / TRAIN_DN / f' {CAR_ID} _ {i + 1:02d} .jpg')for i in range(16)] im_masks = [open_image(PATH / MASKS_DN / f' {CAR_ID} _ { i + 1:02d} _mask.png')for i in range(16)]
所以这是一个很酷的技巧。如果使用相同的轴对象(ax)绘制图像两次,第二次使用alpha,你可能知道计算机视觉世界中的透明度,那么你实际上可以在照片顶部绘制蒙版。所以这里有一个很好的方法可以看到一组中所有车辆的照片顶部的所有面具。
fig,axes = plt.subplots(4,4,figsize =(9,6)) **for** i,ax **in** enumerate(axes.flat): ax = show_img(ims [i],ax = ax)show_img(im_masks [i] [...,0],ax = ax,alpha = 0.5) plt.tight_layout(pad=0.1)

这是我们已经看过两次的MatchedFilesDataset。这是完全相同的代码。这是重要的事情。如果我们在训练中设置了左边的那个,然后验证了右边的图像,那将是一种作弊,因为它是同一辆车。

**class** **MatchedFilesDataset** (FilesDataset): **def** __init__(self, fnames, y, transform, path): self.y=y **assert** (len(fnames)==len(y)) super().__init__(fnames, transform, path) **def** get_y(self, i): **return** open_image(os.path.join(self.path, self.y[i])) **def** get_c(self): **return** 0
x_names = np.array([Path(TRAIN_DN)/o **for** o **in** masks_csv['img']]) y_names = np.array([Path(MASKS_DN)/f' **{o[:-4]}** _mask.png' **for** o **in** masks_csv['img']])
LEN(x_names)// 16 // 5 * 16
_1008_
所以我们使用一组连续的汽车ID,因为每组都是16,我们确保它可以被16整除。所以我们确保我们的验证集包含我们训练集的不同汽车ID。这是你必须要小心的东西。在Kaggle上,它并不是那么糟糕 - 你会知道它,因为你会提交你的结果,与你的验证集相比,你的排行榜会得到一个非常不同的结果。但在现实世界中。你不会知道,直到你把它投入生产并让你的公司破产并失去你的工作。因此,在这种情况下,你可能需要仔细考虑你的验证集。
val_idxs = list(range(1008)) ((val_x,trn_x),(val_y,trn_y)) = split_by_idx(val_idxs, x_names, y_names) len(val_x),len(trn_x)
_(1008, 4080)_
这里我们将使用转换类型分类(TfmType.CLASS) [1:37:03] 。这是基本相同的变换类型像素(TfmType.PIXEL),但如果你想想看,一个像素版本,如果我们旋转那么一点点,我们可能要平均的像素在两者之间,但分类,很明显我们没有。我们使用最近邻居。所以那里有细微的差别。同样,对于分类,光照不起作用,归一化不会引入因变量。
aug_tfms = [RandomRotate(4, tfm_y=TfmType.CLASS), RandomFlip(tfm_y=TfmType.CLASS), RandomLighting(0.05,0.05)] #aug_tfms = []
它们已经是方形图像,因此我们不必进行任何裁剪。
tfms = tfms_from_model(resnet34,sz,crop_type = CropType.NO,tfm_y = TfmType.CLASS,aug_tfms = aug_tfms)datasets = ImageData.get_ds(MatchedFilesDataset,(trn_x,trn_y),(val_x,val_y),tfms,path = PATH) md = ImageData(PATH, datasets, bs, num_workers=8, classes= **None** )
denorm = md.trn_ds.denorm x,y = next(iter(md.aug_dl)) x = denorm(x)
所以在这里你可以看到不同版本的增强图像 - 它们会移动一点,它们会旋转一点,等等。
fig,axes = plt.subplots(5,6,figsize =(12,10)) **for** i,ax **in** enumerate(axes.flat): ax=show_img(x[i], ax=ax) show_img(y[i], ax=ax, alpha=0.5) plt.tight_layout(pad=0.1)

在我们的研究小组中,我收到很多关于如何调试和解决不起作用的问题的问题。除了每次修复问题之外,我从来没有一个很好的答案,因为我这样做的东西一直在做。我只是随时打印出所有内容,然后我搞砸的一件事总是成为我忘记检查的一件事。这种事情越多越好。如果你没有查看所有中间结果,那么你将遇到麻烦。
型号 [1:38:30]
class Empty(nn.Module): def forward(self,x):return x models = ConvnetBuilder(resnet34,0,0,0,custom_head = Empty()) learn = ConvLearner(md, models) learn.summary()
**class** **StdUpsample** (nn.Module): **def** __init__(self, nin, nout): super().__init__() self.conv = nn.ConvTranspose2d(nin, nout, 2, stride=2) self.bn = nn.BatchNorm2d(nout) **def** forward(self, x): **return** self.bn(F.relu(self.conv(x)))
flatten_channel = Lambda(lambda x:x [:,0])
simple_up = nn.Sequential( nn.ReLU(), StdUpsample(512,256), StdUpsample(256,256), StdUpsample(256,256), StdUpsample(256,256), nn.ConvTranspose2d(256, 1, 2, stride=2), flatten_channel )
鉴于我们想要了解汽车的外观,我们可能希望从预训练好的ImageNet网络开始。所以我们将从ResNet34开始。有了ConvnetBuilder,我们可以抓住我们的ResNet34,我们可以添加一个自定义头。自定义头部将会出现多次上升,我们现在要做的事情真的很愚蠢,我们只是要做一个ConvTranspose2d,批量规范,ReLU。
这就是我所说的 - 你们中的任何一个人都可以在没有查看任何笔记本的情况下构建它,或者至少你有以前课程的信息。根本没有新的东西。所以在最后,我们有一个过滤器。现在,这将给我们一些批量大小为1乘128乘128的东西。但我们想要的是批量大小为128乘128的东西。所以我们必须删除那个单位轴,所以我在这里有一个lambda层。 Lambda层非常有用,因为这里没有lambda层,只是通过将其索引为0而没有lambda层来删除单位轴,我必须创建一个带有自定义forward方法的自定义类,依此类推。但是通过创建一个执行一个自定义位的lambda层,我现在可以将它放入Sequential中,这样可以让生活更轻松。
PyTorch人对这种方法很有说服力。Lambda层实际上是fastai库的一部分,不属于PyTorch库的一部分。PyTorch讨论区的人们说“是的,我们可以给人们这个”,“是的,它只是一行代码”,但他们从不鼓励他们经常使用顺序。你去吧
所以这是我们的习惯头 [1:40:36] 。因此,我们将有一个ResNet 34进行下采样,然后是一个非常简单的自定义头,可以非常快速地进行采样,并且希望可以做一些事情。我们将使用阈值为0.5的准确度并打印出指标。
models = ConvnetBuilder(resnet34, 0, 0, 0, custom_head=simple_up) learn = ConvLearner(md, models) learn.opt_fn=optim.Adam learn.crit=nn.BCEWithLogitsLoss() learn.metrics=[accuracy_thresh(0.5)]
learn.lr_find() learn.sched.plot()
94%|█████████▍| 30/32 [00:05<00:00, 5.48it/s, loss=10.6]

lr=4e-2
learn.fit(lr,1,cycle_len=5,use_clr=(20,5))
_epoch trn_loss val_loss <lambda>_ _0 0.124078 0.133566 0.945951_ _1 0.111241 0.112318 0.954912_ _2 0.099743 0.09817 0.957507_ _3 0.090651 0.092375 0.958117_ _4 0.084031 0.086026 0.963243_
_[0.086025625, 0.96324310824275017]_
经过几个迭代,我们的准确率达到了96%。 这是好事 [1:40:56] ? 96%准确好吗? 希望这个问题的答案取决于它。 这是为了什么? 答案是Carvana想要这个,因为他们希望能够拍摄他们的汽车图像并将其剪切并粘贴在异国情调的蒙特卡罗背景或其他任何地方(那是蒙特卡洛的地方,而不是模拟)。 要做到这一点,你需要一个非常好的面具。 你不希望将后视镜留在后面,丢失一个车轮,或者包含一些背景或其他东西。 那看起来很愚蠢。 所以你需要一些非常好的东西。 所以只有96%的像素正确才听起来不太好。 但是在我们看之前我们不会真正知道。 让我们来看看吧。
learn.save('tmp')
learn.load('tmp')
py,ay = learn.predict_with_targs()
ay.shape
_(1008, 128, 128)_
所以我们想要删除正确的版本 [1:41:54]
show_img(ay[0]);

那是96%的准确版本。 因此,当你看到它时,你意识到“哦,是的,96%的像素准确实际上很容易,因为所有的外部位都不是汽车,所有内部位都是汽车,真正有趣的是边缘。 所以我们需要做得更好。
show_img(py[0]>0);

让我们解开,因为到目前为止我们所做的一切都是训练定制头。 让我们做更多。
learn.unfreeze()
learn.bn_freeze( **True** )
lrs = np.array([lr/100,lr/10,lr])/4
learn.fit(lrs,1,cycle_len=20,use_clr=(20,10))
_epoch trn_loss val_loss <lambda>_ _0 0.06577 0.053292 0.972977_ _1 0.049475 0.043025 0.982559_ _2 0.039146 0.035927 0.98337_ _3 0.03405 0.031903 0.986982_ _4 0.029788 0.029065 0.987944_ _5 0.027374 0.027752 0.988029_ _6 0.026041 0.026718 0.988226_ _7 0.024302 0.025927 0.989512_ _8 0.022921 0.026102 0.988276_ _9 0.021944 0.024714 0.989537_ _10 0.021135 0.0241 0.990628_ _11 0.020494 0.023367 0.990652_ _12 0.01988 0.022961 0.990989_ _13 0.019241 0.022498 0.991014_ _14 0.018697 0.022492 0.990571_ _15 0.01812 0.021771 0.99105_ _16 0.017597 0.02183 0.991365_ _17 0.017192 0.021434 0.991364_ _18 0.016768 0.021383 0.991643_ _19 0.016418 0.021114 0.99173_
_[0.021113895, 0.99172959849238396]_
再过一点,我们得到99.1%。 这样好吗? 我不知道。 让我们来看看。
learn.save('0')
x,y = next(iter(md.val_dl)) py = to_np(learn.model(V(x)))
其实没有。 它完全错过了左侧的后视镜,并在右侧错过了很多。 它显然在底部出现了问题。 当我们试图削减它们时,这些事情完全重要,所以它仍然不够好。
ax = show_img(denorm(x)[0]) show_img(py[0]>0, ax=ax, alpha=0.5);

ax = show_img(denorm(x)[0]) show_img(y[0], ax=ax, alpha=0.5);

512x512 [1:42:50]
让我们尝试升级。 好消息是,当我们升级到512乘512时,(确保减少批量大小,因为你的内存不足),这里有更多的信息继续下去所以我们的准确度增加到99.4 %和事情一直在变好。
TRAIN_DN = 'train' MASKS_DN = 'train_masks_png' sz = 512 bs = 16
x_names = np.array([Path(TRAIN_DN)/o **for** o **in** masks_csv['img']]) y_names = np.array([Path(MASKS_DN)/f' **{o[:-4]}** _mask.png' **for** o **in** masks_csv['img']])
((val_x,trn_x),(val_y,trn_y)) = split_by_idx(val_idxs, x_names, y_names) len(val_x),len(trn_x)
_(1008, 4080)_
tfms = tfms_from_model(resnet34, sz, crop_type=CropType.NO, tfm_y=TfmType.CLASS, aug_tfms=aug_tfms) datasets = ImageData.get_ds(MatchedFilesDataset, (trn_x,trn_y), (val_x,val_y), tfms, path=PATH) md = ImageData(PATH, datasets, bs, num_workers=8, classes= **None** )
denorm = md.trn_ds.denorm x,y = next(iter(md.aug_dl)) x = denorm(x)
这是真实的。
fig, axes = plt.subplots(4, 4, figsize=(10, 10)) **for** i,ax **in** enumerate(axes.flat): ax=show_img(x[i], ax=ax) show_img(y[i], ax=ax, alpha=0.5) plt.tight_layout(pad=0.1)

simple_up = nn.Sequential( nn.ReLU(), StdUpsample(512,256), StdUpsample(256,256), StdUpsample(256,256), StdUpsample(256,256), nn.ConvTranspose2d(256, 1, 2, stride=2), flatten_channel )
models = ConvnetBuilder(resnet34, 0, 0, 0, custom_head=simple_up) learn = ConvLearner(md, models) learn.opt_fn=optim.Adam learn.crit=nn.BCEWithLogitsLoss() learn.metrics=[accuracy_thresh(0.5)]
learn.load('0')
learn.lr_find() learn.sched.plot()
85%|████████▌ | 218/255 [02:12<00:22, 1.64it/s, loss=8.91]

lr=4e-2
learn.fit(lr,1,cycle_len=5,use_clr=(20,5))
epoch trn_loss val_loss <lambda> 0 0.02178 0.020653 0.991708 1 0.017927 0.020653 0.990241 2 0.015958 0.016115 0.993394 3 0.015172 0.015143 0.993696 4 0.014315 0.014679 0.99388
[0.014679321, 0.99388032489352751]
learn.save('tmp')
learn.load('tmp')
learn.unfreeze() learn.bn_freeze( **True** )
lrs = np.array([lr/100,lr/10,lr])/4
learn.fit(lrs,1,cycle_len=8,use_clr=(20,8))
epoch trn_loss val_loss mask_acc 0 0.038687 0.018685 0.992782 1 0.024906 0.014355 0.994933 2 0.025055 0.014737 0.995526 3 0.024155 0.014083 0.995708 4 0.013446 0.010564 0.996166 5 0.01607 0.010555 0.996096 6 0.019197 0.010883 0.99621 7 0.016157 0.00998 0.996393
[0.0099797687, 0.99639255659920833]
learn.save('512')
x,y = next(iter(md.val_dl)) py = to_np(learn.model(V(x)))
ax = show_img(denorm(x)[0]) show_img(py[0]>0, ax=ax, alpha=0.5);

ax = show_img(denorm(x)[0]) show_img(y[0], ax=ax, alpha=0.5);

事情变得越来越好但我们仍然有相当多的黑色块状小块。 所以让我们去1024乘1024。
1024x1024 [1:43:17]
所以让我们来1024到1024,批量大小降到4.这是相当高的res现在,并训练更多,99.6,99.8%!
sz = 1024 bs = 4
tfms = tfms_from_model(resnet34, sz, crop_type=CropType.NO, tfm_y=TfmType.CLASS, aug_tfms=aug_tfms) datasets = ImageData.get_ds(MatchedFilesDataset, (trn_x,trn_y), (val_x,val_y), tfms, path=PATH) md = ImageData(PATH, datasets, bs, num_workers=8, classes= **None** )
denorm = md.trn_ds.denorm x,y = next(iter(md.aug_dl)) x = denorm(x) y = to_np(y)
fig, axes = plt.subplots(2, 2, figsize=(8, 8)) **for** i,ax **in** enumerate(axes.flat): show_img(x[i], ax=ax) show_img(y[i], ax=ax, alpha=0.5) plt.tight_layout(pad=0.1)

simple_up = nn.Sequential( nn.ReLU(), StdUpsample(512,256), StdUpsample(256,256), StdUpsample(256,256), StdUpsample(256,256), nn.ConvTranspose2d(256, 1, 2, stride=2), flatten_channel, )
models = ConvnetBuilder(resnet34, 0, 0, 0, custom_head=simple_up) learn = ConvLearner(md, models) learn.opt_fn=optim.Adam learn.crit=nn.BCEWithLogitsLoss() learn.metrics=[accuracy_thresh(0.5)]
learn.load('512')
learn.lr_find() learn.sched.plot()
85%|████████▌ | 218/255 [02:12<00:22, 1.64it/s, loss=8.91]

lr=4e-2
learn.fit(lr,1,cycle_len=2,use_clr=(20,4))
_epoch trn_loss val_loss <lambda>_ _0 0.01066 0.011119 0.996227_ _1 0.009357 0.009696 0.996553_
_[0.0096957013, 0.99655332546385511]_
learn.save('tmp')
learn.load('tmp')
learn.unfreeze() learn.bn_freeze( **True** )
lrs = np.array([lr/100,lr/10,lr])/8
learn.fit(lrs,1,cycle_len=40,use_clr=(20,10))
_epoch trn_loss val_loss mask_acc_ _0 0.015565 0.007449 0.997661_ _1 0.01979 0.008376 0.997542_ _2 0.014874 0.007826 0.997736_ _3 0.016104 0.007854 0.997347_ _4 0.023386 0.009745 0.997218_ _5 0.018972 0.008453 0.997588_ _6 0.013184 0.007612 0.997588_ _7 0.010686 0.006775 0.997688_ _8 0.0293 0.015299 0.995782_ _9 0.018713 0.00763 0.997638_ _10 0.015432 0.006575 0.9978_ _11 0.110205 0.060062 0.979043_ _12 0.014374 0.007753 0.997451_ _13 0.022286 0.010282 0.997587_ _14 0.015645 0.00739 0.997776_ _15 0.013821 0.00692 0.997869_ _16 0.022389 0.008632 0.997696_ _17 0.014607 0.00677 0.997837_ _18 0.018748 0.008194 0.997657_ _19 0.016447 0.007237 0.997899_ _20 0.023596 0.008211 0.997918_ _21 0.015721 0.00674 0.997848_ _22 0.01572 0.006415 0.998006_ _23 0.019519 0.007591 0.997876_ _24 0.011159 0.005998 0.998053_ _25 0.010291 0.005806 0.998012_ _26 0.010893 0.005755 0.998046_ _27 0.014534 0.006313 0.997901_ _28 0.020971 0.006855 0.998018_ _29 0.014074 0.006107 0.998053_ _30 0.01782 0.006561 0.998114_ _31 0.01742 0.006414 0.997942_ _32 0.016829 0.006514 0.9981_ _33 0.013148 0.005819 0.998033_ _34 0.023495 0.006261 0.997856_ _35 0.010931 0.005516 0.99812_ _36 0.015798 0.006176 0.998126_ _37 0.021636 0.005931 0.998067_ _38 0.012133 0.005496 0.998158_ _39 0.012562 0.005678 0.998172_
_[0.0056782686, 0.99817223208291195]_
learn.save('1024')
x,y = next(iter(md.val_dl)) py = to_np(learn.model(V(x)))
ax = show_img(denorm(x)[0]) show_img(py[0][0]>0, ax=ax, alpha=0.5);

ax = show_img(denorm(x)[0]) show_img(y[0,...,-1], ax=ax, alpha=0.5);

show_img(py[0][0]>0);

show_img(y[0,...,-1]);

现在,如果我们看一下面具,它们实际上看起来并不坏。 那看起来很不错。 那我们能做得更好吗? 答案是肯定的,我们可以。
U-Net [1:43:45]
U-Net网络非常壮观。 使用之前的方法,我们预训练好的ImageNet网络一直被压缩到7x7,然后一直扩展到224x224(1024被压缩到比7x7大得多)。 然后再次扩展所有这些意味着它必须以某种方式存储关于小版本中更大版本的所有信息。 实际上,关于更大版本的大部分信息都是在原始图片中。 所以这似乎不是一个很好的方法 - 这种压扁和不会压扁。

因此,U-Net的想法来自于这篇精彩的论文,它在字面上发明了这个特定领域的生物医学图像分割领域。 但实际上,基本上每个Kaggle的胜利者甚至都与分割有关,但最终还是使用了U-Net。 这是Kaggle的每个人都知道这是最好的做法之一,但在更多的学术界,这已经存在了至少几年,很多人仍然没有意识到这是迄今为止最好的做法。

这是基本思路 [1:45:10] 。 在左边是向下路径,在这种情况下,我们从572x572开始,然后将网格大小减半,然后在右边是向上路径,我们将网格大小加倍4倍。 但我们也要做的是,在我们将网格大小减半的每一点上,我们实际上将这些激活复制到向上路径并将它们连接在一起。
你可以在右下方看到,这些红色箭头是最大池化操作,这些绿色箭头是向上采样,然后这些灰色箭头正在复制。 所以我们复制并结束。 换句话说,几个转换后的输入图像被复制到输出,连接在一起,所以现在我们可以使用经历了所有向下和所有向上的所有信息的所有信息,再加上也是输入像素的略微修改版本。 并且从输入像素中略微修改了一个东西,因为它们来自这里。 因此,我们拥有一直向下和向上的所有丰富性,但也有一个稍微不那么粗糙的版本和稍微不那么粗糙的版本,然后是非常简单的版本,它们都可以组合在一起。 那就是U-Net。 这是一个很酷的主意。
我们在carvana-unet笔记本中。 这一切都和以前一样。
%matplotlib inline %reload_ext autoreload %autoreload 2
**from** **fastai.conv_learner** **import** * **from** **fastai.dataset** **import** * **from** **fastai.models.resnet** **import** vgg_resnet50 **import** **json**
torch.backends.cudnn.benchmark= **True**
数据
PATH = Path('data/carvana') MASKS_FN = 'train_masks.csv' META_FN = 'metadata.csv' masks_csv = pd.read_csv(PATH/MASKS_FN) meta_csv = pd.read_csv(PATH/META_FN)
**def** show_img(im, figsize= **None** , ax= **None** , alpha= **None** ): **if** **not** ax: fig,ax = plt.subplots(figsize=figsize) ax.imshow(im, alpha=alpha) ax.set_axis_off() **return** ax
TRAIN_DN = 'train-128' MASKS_DN = 'train_masks-128' sz = 128 bs = 64 nw = 16
TRAIN_DN = 'train' MASKS_DN = 'train_masks_png' sz = 128 bs = 64 nw = 16
**class** **MatchedFilesDataset** (FilesDataset): **def** __init__(self, fnames, y, transform, path): self.y=y **assert** (len(fnames)==len(y)) super().__init__(fnames, transform, path) **def** get_y(self, i): **return** open_image(os.path.join(self.path, self.y[i])) **def** get_c(self): **return** 0
x_names = np.array([Path(TRAIN_DN)/o **for** o **in** masks_csv['img']]) y_names = np.array([Path(MASKS_DN)/f' **{o[:-4]}** _mask.png' **for** o **in** masks_csv['img']])
val_idxs = list(range(1008)) ((val_x,trn_x),(val_y,trn_y)) = split_by_idx(val_idxs, x_names, y_names)
aug_tfms = [RandomRotate(4, tfm_y=TfmType.CLASS), RandomFlip(tfm_y=TfmType.CLASS), RandomLighting(0.05, 0.05, tfm_y=TfmType.CLASS)]
tfms = tfms_from_model(resnet34, sz, crop_type=CropType.NO, tfm_y=TfmType.CLASS, aug_tfms=aug_tfms) datasets = ImageData.get_ds(MatchedFilesDataset, (trn_x,trn_y), (val_x,val_y), tfms, path=PATH) md = ImageData(PATH, datasets, bs, num_workers=16, classes= **None** ) denorm = md.trn_ds.denorm
x,y = next(iter(md.trn_dl))
x.shape,y.shape
_(torch.Size([64, 3, 128, 128]), torch.Size([64, 128, 128]))_
简单的上传
在一开始,我有一个简单的上采样版本,只是为了再次显示非U-net版本。 这次,我将添加一个称为骰子指标的东西。 正如你所看到的那样,骰子与Jaccard或我在U上非常相似。这只是一个微小的差别。 它基本上是与小调整相交的交叉点。 我们打算使用骰子的原因是Kaggle比赛所使用的指标,并且获得高骰子得分比高精度要困难一点,因为它真正关注的是正确像素与像素的重叠。 但它非常相似。
所以在Kaggle比赛中,那些做得好的人得到了大约99.6个骰子,获胜者大约是99.7个骰子。
f = resnet34 cut,lr_cut = model_meta[f]
**def** get_base(): layers = cut_model(f( **True** ), cut) **return** nn.Sequential(*layers)
**def** dice(pred, targs): pred = (pred>0).float() **return** 2\. * (pred*targs).sum() / (pred+targs).sum()
这是我们的标准上传。
**class** **StdUpsample** (nn.Module): **def** __init__(self, nin, nout): super().__init__() self.conv = nn.ConvTranspose2d(nin, nout, 2, stride=2) self.bn = nn.BatchNorm2d(nout) **def** forward(self, x): **return** self.bn(F.relu(self.conv(x)))
这一切都和以前一样。
**class** **Upsample34** (nn.Module): **def** __init__(self, rn): super().__init__() self.rn = rn self.features = nn.Sequential( rn, nn.ReLU(), StdUpsample(512,256), StdUpsample(256,256), StdUpsample(256,256), StdUpsample(256,256), nn.ConvTranspose2d(256, 1, 2, stride=2)) **def** forward(self,x): **return** self.features(x)[:,0]
**class** **UpsampleModel** (): **def** __init__(self,model,name='upsample'): self.model,self.name = model,name **def** get_layer_groups(self, precompute): lgs = list(split_by_idxs(children(self.model.rn), [lr_cut])) **return** lgs + [children(self.model.features)[1:]]
m_base = get_base()
m = to_gpu(Upsample34(m_base)) models = UpsampleModel(m)
learn = ConvLearner(md, models) learn.opt_fn=optim.Adam learn.crit=nn.BCEWithLogitsLoss() learn.metrics=[accuracy_thresh(0.5),dice]
learn.freeze_to(1)
learn.lr_find() learn.sched.plot()
86%|█████████████████████████████████████████████████████████████ | 55/64 [00:22<00:03, 2.46it/s, loss=3.21]

lr=4e-2 wd=1e-7 lrs = np.array([lr/100,lr/10,lr])/2
learn.fit(lr,1, wds=wd, cycle_len=4,use_clr=(20,8))
0%| | 0/64 [00:00<?, ?it/s] epoch trn_loss val_loss <lambda> dice 0 0.216882 0.133512 0.938017 0.855221 1 0.169544 0.115158 0.946518 0.878381 2 0.153114 0.099104 0.957748 0.903353 3 0.144105 0.093337 0.964404 0.915084
[0.09333742126112893, 0.9644036065964472, 0.9150839788573129]
learn.save('tmp')
learn.load('tmp')
learn.unfreeze() learn.bn_freeze( **True** )
learn.fit(lrs,1,cycle_len=4,use_clr=(20,8))
epoch trn_loss val_loss <lambda> dice 0 0.174897 0.061603 0.976321 0.94382 1 0.122911 0.053625 0.982206 0.957624 2 0.106837 0.046653 0.985577 0.965792 3 0.099075 0.042291 0.986519 0.968925
[0.042291240323157536, 0.986519161670927, 0.9689251193924556]
现在我们可以查看我们的骰子指标 [1:48:00] 。 所以你可以看到骰子指标,我们在128x128时得到96.8左右。 所以这不是很好。
learn.save('128')
x,y = next(iter(md.val_dl)) py = to_np(learn.model(V(x)))
show_img(py[0]>0);

show_img(y[0]);

U-net(ish) [1:48:16]
让我们试试U-Net吧。 我称之为U-net(ish),因为按照惯例,我正在创建我自己的有点hacky版本 - 尝试保持与你习惯的类似的东西,并做我觉得有意义的事情。 所以你应该有足够的机会通过查看确切的网格大小来至少使这个更真实的U-net,并看看这里(左上方的转换)大小是如何下降的。 所以他们显然没有添加任何填充,然后有一些裁剪正在进行 - 有一些差异。 但有一件事是因为我想利用转移学习 - 这意味着我不能完全使用U-Net。
所以这是另一个重要的机会,如果你创建U-Net下行路径然后在末尾添加分类器然后在ImageNet上训练它。 你现在已经拥有了ImageNet训练分类器,专门设计为U-Net的良好骨干。 那么你现在应该能够回来并且非常接近赢得这场旧比赛(实际上并不是那么久 - 这是最近的比赛)。 因为之前没有预训练好的网络。 但是如果你想想YOLO v3做了什么,基本上就是这样。 他们创建了一个DarkNet,他们在ImageNet上预训练了它,然后他们用它作为边框的基础。 同样,这种预训练的想法不仅仅是为了分类而是为其他事物而设计 - 这只是没有人做过的事情。 但正如我们所展示的那样,你现在可以在三小时内以25美元的价格训练ImageNet。 如果社区中的人有兴趣这样做,希望我能得到你可以帮助你的学分,所以如果你这样做,设置它的工作并给我一个脚本,我可以为你运行它。 但就目前而言,我们还没有。 所以我们将使用ResNet。
**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** remove(self): self.hook.remove()
所以我们基本上将从get_base [1:50:37] 开始。 Base是我们的基础网络,在第一部分中进行了定义。

所以get_base将会调用f f和f resnet34 。 所以我们要抓住我们的ResNet34和cut_model是我们的网站构建器做的第一件事。 它基本上从自适应池开始删除所有内容,从而使我们回到ResNet34的主干。 因此get_base将为我们提供ResNet34主干。
**class** **UnetBlock** (nn.Module): **def** __init__(self, up_in, x_in, n_out): super().__init__() up_out = x_out = n_out//2 self.x_conv = nn.Conv2d(x_in, x_out, 1) self.tr_conv = nn.ConvTranspose2d(up_in, up_out, 2, stride=2) self.bn = nn.BatchNorm2d(n_out) **def** forward(self, up_p, x_p): up_p = self.tr_conv(up_p) x_p = self.x_conv(x_p) cat_p = torch.cat([up_p,x_p], dim=1) **return** self.bn(F.relu(cat_p))
**class** **Unet34** (nn.Module): **def** __init__(self, rn): super().__init__() self.rn = rn self.sfs = [SaveFeatures(rn[i]) **for** i **in** [2,4,5,6]] self.up1 = UnetBlock(512,256,256) self.up2 = UnetBlock(256,128,256) self.up3 = UnetBlock(256,64,256) self.up4 = UnetBlock(256,64,256) self.up5 = nn.ConvTranspose2d(256, 1, 2, stride=2) **def** forward(self,x): x = F.relu(self.rn(x)) x = self.up1(x, self.sfs[3].features) x = self.up2(x, self.sfs[2].features) x = self.up3(x, self.sfs[1].features) x = self.up4(x, self.sfs[0].features) x = self.up5(x) **return** x[:,0] **def** close(self): **for** sf **in** self.sfs: sf.remove()
**class** **UnetModel** (): **def** __init__(self,model,name='unet'): self.model,self.name = model,name **def** get_layer_groups(self, precompute): lgs = list(split_by_idxs(children(self.model.rn), [lr_cut])) **return** lgs + [children(self.model)[1:]]
然后我们将把ResNet34骨干转为a,我称之为Unet34 [1:51:17] 。 所以它要做的就是保存我们传入的ResNet,然后我们将像以前一样使用前向挂钩将结果保存在第2,第4,第5和第6个块中,如前所述在每个步幅2卷积之前的层。 然后我们将创建一些我们称之为UnetBlock东西。 我们需要告诉UnetBlock多少东西来自上一层我们正在进行上采样,有多少是UnetBlock ,然后我们想要出现多少。 所遇到的金额完全由基础网络所定义 - 无论向下的路径如何,我们都需要这么多层。 所以这有点尴尬。 实际上我们的主人之一,Kerem,实际上创建了一个名为DynamicUnet的东西,你可以在fastai.model.DynamicUnet中找到它,它实际上为你计算了这一切,并自动从你的基础模型创建了整个Unet。 我还想解决一些小问题。 当视频播出时,它肯定会正常工作,我至少会有一个笔记本显示如何使用它,可能还有一个额外的视频。 但是现在你只需要自己完成并自己做。 你可以轻松地看到它,一旦你有了ResNet,你只需输入其名称即可打印出层。 你可以看到每个街区有多少次激活。 或者你可以自动为每个块打印出来。 无论如何,我只是手动完成了这个。

所以UnetBlock就像这样 [1:53:29] :
up_in:这很多都来自上一层x_in:这许多人从向下的路径遇到(因此是x)n_out:我们想要的金额
现在我所做的就是,我接着说,好吧我们将从向上的路径和一定数量的交叉路径创建一定数量的卷积,所以我将它们连接在一起所以让我们分开我们想要的数字是2.所以我们将使用我们的交叉卷积我们的交叉路径并创建数字除以2( n_out//2 )。 然后向上的路径将是一个ConvTranspose2d因为我们想要增加/上采样。 再次在这里,我们将数字除以2( up_out ),然后在最后,我将它们连接在一起。
所以我有一个向上的样本,我有一个交叉卷积,我可以将两者连接在一起。 这就是UnetBlock。 所以这实际上是一个非常容易创建的模块。

然后在我的前进道路上,我需要向UnetBlock的前方传递向上的路径和交叉路径 [1:54:40] 。 向上的道路就是我到目前为止所做的一切。 但是,交叉路径就是我在下行过程中存储的任何激活。 所以当我出现时,它是我需要的最后一组保存的功能。 随着我逐渐向上越来越远,最终它成为第一组功能。
我们可以做一些更多的技巧来使它更好一点,但这是一个很好的东西。 所以简单的上采样方法看起来很糟糕,并且有一个.968的骰子。 除了我们现在拥有这些UnetBlocks之外,其他一切都相同的Unet有一个…
m_base = get_base() m = to_gpu(Unet34(m_base)) models = UnetModel(m)
learn = ConvLearner(md, models) learn.opt_fn=optim.Adam learn.crit=nn.BCEWithLogitsLoss() learn.metrics=[accuracy_thresh(0.5),dice]
learn.summary()
OrderedDict([('Conv2d-1', OrderedDict([('input_shape', [-1, 3, 128, 128]), ('output_shape', [-1, 64, 64, 64]), ('trainable', False), ('nb_params', 9408)])), ('BatchNorm2d-2', OrderedDict([('input_shape', [-1, 64, 64, 64]), ('output_shape', [-1, 64, 64, 64]), ('trainable', False), ('nb_params', 128)])), ('ReLU-3', OrderedDict([('input_shape', [-1, 64, 64, 64]), ('output_shape', [-1, 64, 64, 64]), ('nb_params', 0)])), ('MaxPool2d-4', OrderedDict([('input_shape', [-1, 64, 64, 64]), ('output_shape', [-1, 64, 32, 32]), ('nb_params', 0)])), ('Conv2d-5', OrderedDict([('input_shape', [-1, 64, 32, 32]), ('output_shape', [-1, 64, 32, 32]), ('trainable', False), ('nb_params', 36864)])), ('BatchNorm2d-6', OrderedDict([('input_shape', [-1, 64, 32, 32]), ('output_shape', [-1, 64, 32, 32]), ('trainable', False), ('nb_params', 128)])), ('ReLU-7', OrderedDict([('input_shape', [-1, 64, 32, 32]), ('output_shape', [-1, 64, 32, 32]), ('nb_params', 0)])), ('Conv2d-8', OrderedDict([('input_shape', [-1, 64, 32, 32]), ('output_shape', [-1, 64, 32, 32]), ('trainable', False), ('nb_params', 36864)])), ('BatchNorm2d-9', OrderedDict([('input_shape', [-1, 64, 32, 32]), ('output_shape', [-1, 64, 32, 32]), ('trainable', False), ('nb_params', 128)])), ('ReLU-10', OrderedDict([('input_shape', [-1, 64, 32, 32]), ('output_shape', [-1, 64, 32, 32]), ('nb_params', 0)])), ('BasicBlock-11', OrderedDict([('input_shape', [-1, 64, 32, 32]), ('output_shape', [-1, 64, 32, 32]), ('nb_params', 0)])), ('Conv2d-12', OrderedDict([('input_shape', [-1, 64, 32, 32]), ('output_shape', [-1, 64, 32, 32]), ('trainable', False), ('nb_params', 36864)])), ('BatchNorm2d-13', OrderedDict([('input_shape', [-1, 64, 32, 32]), ('output_shape', [-1, 64, 32, 32]), ('trainable', False), ('nb_params', 128)])), ('ReLU-14', OrderedDict([('input_shape', [-1, 64, 32, 32]), ('output_shape', [-1, 64, 32, 32]), ('nb_params', 0)])), ('Conv2d-15', OrderedDict([('input_shape', [-1, 64, 32, 32]), ('output_shape', [-1, 64, 32, 32]), ('trainable', False), ('nb_params', 36864)])), ('BatchNorm2d-16', OrderedDict([('input_shape', [-1, 64, 32, 32]), ('output_shape', [-1, 64, 32, 32]), ('trainable', False), ('nb_params', 128)])), ('ReLU-17', OrderedDict([('input_shape', [-1, 64, 32, 32]), ('output_shape', [-1, 64, 32, 32]), ('nb_params', 0)])), ('BasicBlock-18', OrderedDict([('input_shape', [-1, 64, 32, 32]), ('output_shape', [-1, 64, 32, 32]), ('nb_params', 0)])), ('Conv2d-19', OrderedDict([('input_shape', [-1, 64, 32, 32]), ('output_shape', [-1, 64, 32, 32]), ('trainable', False), ('nb_params', 36864)])), ('BatchNorm2d-20', OrderedDict([('input_shape', [-1, 64, 32, 32]), ('output_shape', [-1, 64, 32, 32]), ('trainable', False), ('nb_params', 128)])), ('ReLU-21', OrderedDict([('input_shape', [-1, 64, 32, 32]), ('output_shape', [-1, 64, 32, 32]), ('nb_params', 0)])), ('Conv2d-22', OrderedDict([('input_shape', [-1, 64, 32, 32]), ('output_shape', [-1, 64, 32, 32]), ('trainable', False), ('nb_params', 36864)])), ('BatchNorm2d-23', OrderedDict([('input_shape', [-1, 64, 32, 32]), ('output_shape', [-1, 64, 32, 32]), ('trainable', False), ('nb_params', 128)])), ('ReLU-24', OrderedDict([('input_shape', [-1, 64, 32, 32]), ('output_shape', [-1, 64, 32, 32]), ('nb_params', 0)])), ('BasicBlock-25', OrderedDict([('input_shape', [-1, 64, 32, 32]), ('output_shape', [-1, 64, 32, 32]), ('nb_params', 0)])), ('Conv2d-26', OrderedDict([('input_shape', [-1, 64, 32, 32]), ('output_shape', [-1, 128, 16, 16]), ('trainable', False), ('nb_params', 73728)])), ('BatchNorm2d-27', OrderedDict([('input_shape', [-1, 128, 16, 16]), ('output_shape', [-1, 128, 16, 16]), ('trainable', False), ('nb_params', 256)])), ('ReLU-28', OrderedDict([('input_shape', [-1, 128, 16, 16]), ('output_shape', [-1, 128, 16, 16]), ('nb_params', 0)])), ('Conv2d-29', OrderedDict([('input_shape', [-1, 128, 16, 16]), ('output_shape', [-1, 128, 16, 16]), ('trainable', False), ('nb_params', 147456)])), ('BatchNorm2d-30', OrderedDict([('input_shape', [-1, 128, 16, 16]), ('output_shape', [-1, 128, 16, 16]), ('trainable', False), ('nb_params', 256)])), ('Conv2d-31', OrderedDict([('input_shape', [-1, 64, 32, 32]), ('output_shape', [-1, 128, 16, 16]), ('trainable', False), ('nb_params', 8192)])), ('BatchNorm2d-32', OrderedDict([('input_shape', [-1, 128, 16, 16]), ('output_shape', [-1, 128, 16, 16]), ('trainable', False), ('nb_params', 256)])), ('ReLU-33', OrderedDict([('input_shape', [-1, 128, 16, 16]), ('output_shape', [-1, 128, 16, 16]), ('nb_params', 0)])), ('BasicBlock-34', OrderedDict([('input_shape', [-1, 64, 32, 32]), ('output_shape', [-1, 128, 16, 16]), ('nb_params', 0)])), ('Conv2d-35', OrderedDict([('input_shape', [-1, 128, 16, 16]), ('output_shape', [-1, 128, 16, 16]), ('trainable', False), ('nb_params', 147456)])), ('BatchNorm2d-36', OrderedDict([('input_shape', [-1, 128, 16, 16]), ('output_shape', [-1, 128, 16, 16]), ('trainable', False), ('nb_params', 256)])), ('ReLU-37', OrderedDict([('input_shape', [-1, 128, 16, 16]), ('output_shape', [-1, 128, 16, 16]), ('nb_params', 0)])), ('Conv2d-38', OrderedDict([('input_shape', [-1, 128, 16, 16]), ('output_shape', [-1, 128, 16, 16]), ('trainable', False), ('nb_params', 147456)])), ('BatchNorm2d-39', OrderedDict([('input_shape', [-1, 128, 16, 16]), ('output_shape', [-1, 128, 16, 16]), ('trainable', False), ('nb_params', 256)])), ('ReLU-40', OrderedDict([('input_shape', [-1, 128, 16, 16]), ('output_shape', [-1, 128, 16, 16]), ('nb_params', 0)])), ('BasicBlock-41', OrderedDict([('input_shape', [-1, 128, 16, 16]), ('output_shape', [-1, 128, 16, 16]), ('nb_params', 0)])), ('Conv2d-42', OrderedDict([('input_shape', [-1, 128, 16, 16]), ('output_shape', [-1, 128, 16, 16]), ('trainable', False), ('nb_params', 147456)])), ('BatchNorm2d-43', OrderedDict([('input_shape', [-1, 128, 16, 16]), ('output_shape', [-1, 128, 16, 16]), ('trainable', False), ('nb_params', 256)])), ('ReLU-44', OrderedDict([('input_shape', [-1, 128, 16, 16]), ('output_shape', [-1, 128, 16, 16]), ('nb_params', 0)])), ('Conv2d-45', OrderedDict([('input_shape', [-1, 128, 16, 16]), ('output_shape', [-1, 128, 16, 16]), ('trainable', False), ('nb_params', 147456)])), ('BatchNorm2d-46', OrderedDict([('input_shape', [-1, 128, 16, 16]), ('output_shape', [-1, 128, 16, 16]), ('trainable', False), ('nb_params', 256)])), ('ReLU-47', OrderedDict([('input_shape', [-1, 128, 16, 16]), ('output_shape', [-1, 128, 16, 16]), ('nb_params', 0)])), ('BasicBlock-48', OrderedDict([('input_shape', [-1, 128, 16, 16]), ('output_shape', [-1, 128, 16, 16]), ('nb_params', 0)])), ('Conv2d-49', OrderedDict([('input_shape', [-1, 128, 16, 16]), ('output_shape', [-1, 128, 16, 16]), ('trainable', False), ('nb_params', 147456)])), ('BatchNorm2d-50', OrderedDict([('input_shape', [-1, 128, 16, 16]), ('output_shape', [-1, 128, 16, 16]), ('trainable', False), ('nb_params', 256)])), ('ReLU-51', OrderedDict([('input_shape', [-1, 128, 16, 16]), ('output_shape', [-1, 128, 16, 16]), ('nb_params', 0)])), ('Conv2d-52', OrderedDict([('input_shape', [-1, 128, 16, 16]), ('output_shape', [-1, 128, 16, 16]), ('trainable', False), ('nb_params', 147456)])), ('BatchNorm2d-53', OrderedDict([('input_shape', [-1, 128, 16, 16]), ('output_shape', [-1, 128, 16, 16]), ('trainable', False), ('nb_params', 256)])), ('ReLU-54', OrderedDict([('input_shape', [-1, 128, 16, 16]), ('output_shape', [-1, 128, 16, 16]), ('nb_params', 0)])), ('BasicBlock-55', OrderedDict([('input_shape', [-1, 128, 16, 16]), ('output_shape', [-1, 128, 16, 16]), ('nb_params', 0)])), ('Conv2d-56', OrderedDict([('input_shape', [-1, 128, 16, 16]), ('output_shape', [-1, 256, 8, 8]), ('trainable', False), ('nb_params', 294912)])), ('BatchNorm2d-57', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 256, 8, 8]), ('trainable', False), ('nb_params', 512)])), ('ReLU-58', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 256, 8, 8]), ('nb_params', 0)])), ('Conv2d-59', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 256, 8, 8]), ('trainable', False), ('nb_params', 589824)])), ('BatchNorm2d-60', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 256, 8, 8]), ('trainable', False), ('nb_params', 512)])), ('Conv2d-61', OrderedDict([('input_shape', [-1, 128, 16, 16]), ('output_shape', [-1, 256, 8, 8]), ('trainable', False), ('nb_params', 32768)])), ('BatchNorm2d-62', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 256, 8, 8]), ('trainable', False), ('nb_params', 512)])), ('ReLU-63', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 256, 8, 8]), ('nb_params', 0)])), ('BasicBlock-64', OrderedDict([('input_shape', [-1, 128, 16, 16]), ('output_shape', [-1, 256, 8, 8]), ('nb_params', 0)])), ('Conv2d-65', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 256, 8, 8]), ('trainable', False), ('nb_params', 589824)])), ('BatchNorm2d-66', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 256, 8, 8]), ('trainable', False), ('nb_params', 512)])), ('ReLU-67', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 256, 8, 8]), ('nb_params', 0)])), ('Conv2d-68', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 256, 8, 8]), ('trainable', False), ('nb_params', 589824)])), ('BatchNorm2d-69', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 256, 8, 8]), ('trainable', False), ('nb_params', 512)])), ('ReLU-70', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 256, 8, 8]), ('nb_params', 0)])), ('BasicBlock-71', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 256, 8, 8]), ('nb_params', 0)])), ('Conv2d-72', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 256, 8, 8]), ('trainable', False), ('nb_params', 589824)])), ('BatchNorm2d-73', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 256, 8, 8]), ('trainable', False), ('nb_params', 512)])), ('ReLU-74', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 256, 8, 8]), ('nb_params', 0)])), ('Conv2d-75', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 256, 8, 8]), ('trainable', False), ('nb_params', 589824)])), ('BatchNorm2d-76', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 256, 8, 8]), ('trainable', False), ('nb_params', 512)])), ('ReLU-77', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 256, 8, 8]), ('nb_params', 0)])), ('BasicBlock-78', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 256, 8, 8]), ('nb_params', 0)])), ('Conv2d-79', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 256, 8, 8]), ('trainable', False), ('nb_params', 589824)])), ('BatchNorm2d-80', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 256, 8, 8]), ('trainable', False), ('nb_params', 512)])), ('ReLU-81', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 256, 8, 8]), ('nb_params', 0)])), ('Conv2d-82', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 256, 8, 8]), ('trainable', False), ('nb_params', 589824)])), ('BatchNorm2d-83', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 256, 8, 8]), ('trainable', False), ('nb_params', 512)])), ('ReLU-84', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 256, 8, 8]), ('nb_params', 0)])), ('BasicBlock-85', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 256, 8, 8]), ('nb_params', 0)])), ('Conv2d-86', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 256, 8, 8]), ('trainable', False), ('nb_params', 589824)])), ('BatchNorm2d-87', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 256, 8, 8]), ('trainable', False), ('nb_params', 512)])), ('ReLU-88', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 256, 8, 8]), ('nb_params', 0)])), ('Conv2d-89', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 256, 8, 8]), ('trainable', False), ('nb_params', 589824)])), ('BatchNorm2d-90', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 256, 8, 8]), ('trainable', False), ('nb_params', 512)])), ('ReLU-91', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 256, 8, 8]), ('nb_params', 0)])), ('BasicBlock-92', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 256, 8, 8]), ('nb_params', 0)])), ('Conv2d-93', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 256, 8, 8]), ('trainable', False), ('nb_params', 589824)])), ('BatchNorm2d-94', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 256, 8, 8]), ('trainable', False), ('nb_params', 512)])), ('ReLU-95', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 256, 8, 8]), ('nb_params', 0)])), ('Conv2d-96', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 256, 8, 8]), ('trainable', False), ('nb_params', 589824)])), ('BatchNorm2d-97', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 256, 8, 8]), ('trainable', False), ('nb_params', 512)])), ('ReLU-98', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 256, 8, 8]), ('nb_params', 0)])), ('BasicBlock-99', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 256, 8, 8]), ('nb_params', 0)])), ('Conv2d-100', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 512, 4, 4]), ('trainable', False), ('nb_params', 1179648)])), ('BatchNorm2d-101', OrderedDict([('input_shape', [-1, 512, 4, 4]), ('output_shape', [-1, 512, 4, 4]), ('trainable', False), ('nb_params', 1024)])), ('ReLU-102', OrderedDict([('input_shape', [-1, 512, 4, 4]), ('output_shape', [-1, 512, 4, 4]), ('nb_params', 0)])), ('Conv2d-103', OrderedDict([('input_shape', [-1, 512, 4, 4]), ('output_shape', [-1, 512, 4, 4]), ('trainable', False), ('nb_params', 2359296)])), ('BatchNorm2d-104', OrderedDict([('input_shape', [-1, 512, 4, 4]), ('output_shape', [-1, 512, 4, 4]), ('trainable', False), ('nb_params', 1024)])), ('Conv2d-105', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 512, 4, 4]), ('trainable', False), ('nb_params', 131072)])), ('BatchNorm2d-106', OrderedDict([('input_shape', [-1, 512, 4, 4]), ('output_shape', [-1, 512, 4, 4]), ('trainable', False), ('nb_params', 1024)])), ('ReLU-107', OrderedDict([('input_shape', [-1, 512, 4, 4]), ('output_shape', [-1, 512, 4, 4]), ('nb_params', 0)])), ('BasicBlock-108', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 512, 4, 4]), ('nb_params', 0)])), ('Conv2d-109', OrderedDict([('input_shape', [-1, 512, 4, 4]), ('output_shape', [-1, 512, 4, 4]), ('trainable', False), ('nb_params', 2359296)])), ('BatchNorm2d-110', OrderedDict([('input_shape', [-1, 512, 4, 4]), ('output_shape', [-1, 512, 4, 4]), ('trainable', False), ('nb_params', 1024)])), ('ReLU-111', OrderedDict([('input_shape', [-1, 512, 4, 4]), ('output_shape', [-1, 512, 4, 4]), ('nb_params', 0)])), ('Conv2d-112', OrderedDict([('input_shape', [-1, 512, 4, 4]), ('output_shape', [-1, 512, 4, 4]), ('trainable', False), ('nb_params', 2359296)])), ('BatchNorm2d-113', OrderedDict([('input_shape', [-1, 512, 4, 4]), ('output_shape', [-1, 512, 4, 4]), ('trainable', False), ('nb_params', 1024)])), ('ReLU-114', OrderedDict([('input_shape', [-1, 512, 4, 4]), ('output_shape', [-1, 512, 4, 4]), ('nb_params', 0)])), ('BasicBlock-115', OrderedDict([('input_shape', [-1, 512, 4, 4]), ('output_shape', [-1, 512, 4, 4]), ('nb_params', 0)])), ('Conv2d-116', OrderedDict([('input_shape', [-1, 512, 4, 4]), ('output_shape', [-1, 512, 4, 4]), ('trainable', False), ('nb_params', 2359296)])), ('BatchNorm2d-117', OrderedDict([('input_shape', [-1, 512, 4, 4]), ('output_shape', [-1, 512, 4, 4]), ('trainable', False), ('nb_params', 1024)])), ('ReLU-118', OrderedDict([('input_shape', [-1, 512, 4, 4]), ('output_shape', [-1, 512, 4, 4]), ('nb_params', 0)])), ('Conv2d-119', OrderedDict([('input_shape', [-1, 512, 4, 4]), ('output_shape', [-1, 512, 4, 4]), ('trainable', False), ('nb_params', 2359296)])), ('BatchNorm2d-120', OrderedDict([('input_shape', [-1, 512, 4, 4]), ('output_shape', [-1, 512, 4, 4]), ('trainable', False), ('nb_params', 1024)])), ('ReLU-121', OrderedDict([('input_shape', [-1, 512, 4, 4]), ('output_shape', [-1, 512, 4, 4]), ('nb_params', 0)])), ('BasicBlock-122', OrderedDict([('input_shape', [-1, 512, 4, 4]), ('output_shape', [-1, 512, 4, 4]), ('nb_params', 0)])), ('ConvTranspose2d-123', OrderedDict([('input_shape', [-1, 512, 4, 4]), ('output_shape', [-1, 128, 8, 8]), ('trainable', True), ('nb_params', 262272)])), ('Conv2d-124', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 128, 8, 8]), ('trainable', True), ('nb_params', 32896)])), ('BatchNorm2d-125', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 256, 8, 8]), ('trainable', True), ('nb_params', 512)])), ('UnetBlock-126', OrderedDict([('input_shape', [-1, 512, 4, 4]), ('output_shape', [-1, 256, 8, 8]), ('nb_params', 0)])), ('ConvTranspose2d-127', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 128, 16, 16]), ('trainable', True), ('nb_params', 131200)])), ('Conv2d-128', OrderedDict([('input_shape', [-1, 128, 16, 16]), ('output_shape', [-1, 128, 16, 16]), ('trainable', True), ('nb_params', 16512)])), ('BatchNorm2d-129', OrderedDict([('input_shape', [-1, 256, 16, 16]), ('output_shape', [-1, 256, 16, 16]), ('trainable', True), ('nb_params', 512)])), ('UnetBlock-130', OrderedDict([('input_shape', [-1, 256, 8, 8]), ('output_shape', [-1, 256, 16, 16]), ('nb_params', 0)])), ('ConvTranspose2d-131', OrderedDict([('input_shape', [-1, 256, 16, 16]), ('output_shape', [-1, 128, 32, 32]), ('trainable', True), ('nb_params', 131200)])), ('Conv2d-132', OrderedDict([('input_shape', [-1, 64, 32, 32]), ('output_shape', [-1, 128, 32, 32]), ('trainable', True), ('nb_params', 8320)])), ('BatchNorm2d-133', OrderedDict([('input_shape', [-1, 256, 32, 32]), ('output_shape', [-1, 256, 32, 32]), ('trainable', True), ('nb_params', 512)])), ('UnetBlock-134', OrderedDict([('input_shape', [-1, 256, 16, 16]), ('output_shape', [-1, 256, 32, 32]), ('nb_params', 0)])), ('ConvTranspose2d-135', OrderedDict([('input_shape', [-1, 256, 32, 32]), ('output_shape', [-1, 128, 64, 64]), ('trainable', True), ('nb_params', 131200)])), ('Conv2d-136', OrderedDict([('input_shape', [-1, 64, 64, 64]), ('output_shape', [-1, 128, 64, 64]), ('trainable', True), ('nb_params', 8320)])), ('BatchNorm2d-137', OrderedDict([('input_shape', [-1, 256, 64, 64]), ('output_shape', [-1, 256, 64, 64]), ('trainable', True), ('nb_params', 512)])), ('UnetBlock-138', OrderedDict([('input_shape', [-1, 256, 32, 32]), ('output_shape', [-1, 256, 64, 64]), ('nb_params', 0)])), ('ConvTranspose2d-139', OrderedDict([('input_shape', [-1, 256, 64, 64]), ('output_shape', [-1, 1, 128, 128]), ('trainable', True), ('nb_params', 1025)]))])
[o.features.size() **for** o **in** m.sfs]
_[torch.Size([3, 64, 64, 64]),_ _torch.Size([3, 64, 32, 32]),_ _torch.Size([3, 128, 16, 16]),_ _torch.Size([3, 256, 8, 8])]_
learn.freeze_to(1)
learn.lr_find() learn.sched.plot()
0%| | 0/64 [00:00<?, ?it/s]
92%|█████████████████████████████████████████████████████████████████▍ | 59/64 [00:22<00:01, 2.68it/s, loss=2.45]

lr=4e-2 wd=1e-7 lrs = np.array([lr/100,lr/10,lr])
learn.fit(lr,1,wds=wd,cycle_len=8,use_clr=(5,8))
_epoch trn_loss val_loss <lambda> dice_ _0 0.12936 0.03934 0.988571 0.971385_ _1 0.098401 0.039252 0.990438 0.974921_ _2 0.087789 0.02539 0.990961 0.978927_ _3 0.082625 0.027984 0.988483 0.975948_ _4 0.079509 0.025003 0.99171 0.981221_ _5 0.076984 0.022514 0.992462 0.981881_ _6 0.076822 0.023203 0.992484 0.982321_ _7 0.075488 0.021956 0.992327 0.982704_
_[0.021955982234979434, 0.9923273126284281, 0.9827044502137199]_
learn.save('128urn-tmp')
learn.load('128urn-tmp')
learn.unfreeze() learn.bn_freeze( **True** )
learn.fit(lrs/4, 1, wds=wd, cycle_len=20,use_clr=(20,10))
0%| | 0/64 [00:00<?, ?it/s] epoch trn_loss val_loss <lambda> dice 0 0.073786 0.023418 0.99297 0.98283 1 0.073561 0.020853 0.992142 0.982725 2 0.075227 0.023357 0.991076 0.980879 3 0.074245 0.02352 0.993108 0.983659 4 0.073434 0.021508 0.993024 0.983609 5 0.073092 0.020956 0.993188 0.983333 6 0.073617 0.019666 0.993035 0.984102 7 0.072786 0.019844 0.993196 0.98435 8 0.072256 0.018479 0.993282 0.984277 9 0.072052 0.019479 0.993164 0.984147 10 0.071361 0.019402 0.993344 0.984541 11 0.070969 0.018904 0.993139 0.984499 12 0.071588 0.018027 0.9935 0.984543 13 0.070709 0.018345 0.993491 0.98489 14 0.072238 0.019096 0.993594 0.984825 15 0.071407 0.018967 0.993446 0.984919 16 0.071047 0.01966 0.993366 0.984952 17 0.072024 0.018133 0.993505 0.98497 18 0.071517 0.018464 0.993602 0.985192 19 0.070109 0.018337 0.993614 0.9852
[0.018336569653853538, 0.9936137114252362, 0.9852004420189631]
0.985! 就像我们把错误减半,其他一切完全相同 [1:55:42] 。 更重要的是,你可以看一下。
learn.save('128urn-0')
learn.load('128urn-0')
x,y = next(iter(md.val_dl)) py = to_np(learn.model(V(x)))
与我们的非Unet等效物相比,这实际上看起来像汽车一样,只是一个blob。 因为试图通过向下和向上的路径来做这件事 - 这只是要求太多。 在其他地方,当我们实际上在每个点提供向下路径像素时,它实际上可以开始创建一些汽车。
show_img(py[0]>0);

show_img(y[0]);

最后,我们将m.close删除那些占用GPU内存的sfs.features 。
m.close()
512x512 [1:56:26]
转到较小的批量,更大的尺寸
sz=512 bs=16
tfms = tfms_from_model(resnet34, sz, crop_type=CropType.NO, tfm_y=TfmType.CLASS, aug_tfms=aug_tfms) datasets = ImageData.get_ds(MatchedFilesDataset, (trn_x,trn_y), (val_x,val_y), tfms, path=PATH) md = ImageData(PATH, datasets, bs, num_workers=4, classes= **None** ) denorm = md.trn_ds.denorm
m_base = get_base() m = to_gpu(Unet34(m_base)) models = UnetModel(m)
learn = ConvLearner(md, models) learn.opt_fn=optim.Adam learn.crit=nn.BCEWithLogitsLoss() learn.metrics=[accuracy_thresh(0.5),dice]
learn.freeze_to(1)
**learn.load('128urn-0')**
learn.fit(lr,1,wds=wd, cycle_len=5,use_clr=(5,5))
epoch trn_loss val_loss <lambda> dice 0 0.071421 0.02362 0.996459 0.991772 1 0.070373 0.014013 0.996558 0.992602 2 0.067895 0.011482 0.996705 0.992883 3 0.070653 0.014256 0.996695 0.992771 4 0.068621 0.013195 0.996993 0.993359
[0.013194938530288046, 0.996993034604996, 0.993358936574724]
你可以看到骰子系数真的上升 [1:56:30] 。 所以请注意,我正在加载128x128版本的网络。 我们再次进行这种渐进式调整大小的技巧,这样我们就可以获得.993。
learn.save('512urn-tmp')
learn.unfreeze() learn.bn_freeze( **True** )
learn.load('512urn-tmp')
learn.fit(lrs/4,1,wds=wd, cycle_len=8,use_clr=(20,8))
epoch trn_loss val_loss <lambda> dice 0 0.06605 0.013602 0.997 0.993014 1 0.066885 0.011252 0.997248 0.993563 2 0.065796 0.009802 0.997223 0.993817 3 0.065089 0.009668 0.997296 0.993744 4 0.064552 0.011683 0.997269 0.993835 5 0.065089 0.010553 0.997415 0.993827 6 0.064303 0.009472 0.997431 0.994046 7 0.062506 0.009623 0.997441 0.994118
[0.009623114736602894, 0.9974409020136273, 0.9941179137381296]
然后解冻到达.994。
learn.save('512urn')
learn.load('512urn')
x,y = next(iter(md.val_dl)) py = to_np(learn.model(V(x)))
你可以看到,它现在看起来很不错。
show_img(py[0]>0);

show_img(y[0]);

m.close()
1024x1024 [1:56:53]
下载批量大小为4,大小为1024。
sz=1024 bs=4
tfms = tfms_from_model(resnet34, sz, crop_type=CropType.NO, tfm_y=TfmType.CLASS) datasets = ImageData.get_ds(MatchedFilesDataset, (trn_x,trn_y), (val_x,val_y), tfms, path=PATH) md = ImageData(PATH, datasets, bs, num_workers=16, classes= **None** ) denorm = md.trn_ds.denorm
m_base = get_base() m = to_gpu(Unet34(m_base)) models = UnetModel(m)
learn = ConvLearner(md, models) learn.opt_fn=optim.Adam learn.crit=nn.BCEWithLogitsLoss() learn.metrics=[accuracy_thresh(0.5),dice]
加载我们刚刚用512保存的内容。
learn.load('512urn')
learn.freeze_to(1)
learn.fit(lr,1, wds=wd, cycle_len=2,use_clr=(5,4))
epoch trn_loss val_loss <lambda> dice 0 0.007656 0.008155 0.997247 0.99353 1 0.004706 0.00509 0.998039 0.995437
[0.005090427414942828, 0.9980387706605215, 0.995437301104031]
这让我们达到.995。
learn.save('1024urn-tmp')
learn.load('1024urn-tmp')
learn.unfreeze() learn.bn_freeze( **True** )
lrs = np.array([lr/200,lr/30,lr])
learn.fit(lrs/10,1, wds=wd,cycle_len=4,use_clr=(20,8))
epoch trn_loss val_loss <lambda> dice 0 0.005688 0.006135 0.997616 0.994616 1 0.004412 0.005223 0.997983 0.995349 2 0.004186 0.004975 0.99806 0.99554 3 0.004016 0.004899 0.99812 0.995627
[0.004898778487196458, 0.9981196409180051, 0.9956271404784823]
learn.fit(lrs/10,1, wds=wd,cycle_len=4,use_clr=(20,8))
epoch trn_loss val_loss <lambda> dice 0 0.004169 0.004962 0.998049 0.995517 1 0.004022 0.004595 0.99823 0.995818 2 0.003772 0.004497 0.998215 0.995916 3 0.003618 0.004435 0.998291 0.995991
[0.004434524739663753, 0.9982911745707194, 0.9959913929776539]
解冻带我们去…我们称之为.996。
learn.sched.plot_loss()

learn.save('1024urn')
learn.load('1024urn')
x,y = next(iter(md.val_dl)) py = to_np(learn.model(V(x)))
如你所见,这实际上看起来很好 [1:57:17] 。 准确度方面,为99.82%。 你可以看到这看起来像你可以用来削减的东西。 我认为,在这一点上,我们可以做一些小的调整来达到.997但是真的关键是我想,这可能只是为了做一些平滑或者一点点的后期处理。 你可以去看看Carvana获奖者的博客,看看其中的一些技巧,但正如我所说,我们在.996的位置和获胜者的.997之间的区别,不是很重要。 真的,只有Unet本身就能解决这个问题。
show_img(py[0]>0);

show_img(y[0]);

回到边框 [1:58:15]
好的,就是这样。 我想提到的最后一件事现在是回到边框,因为你可能还记得,我说我们的边框模型在小物体上仍然表现不佳。 所以希望你能猜到我要去哪里,这就是对于边框模型,记住我们如何在不同的网格单元中输出模型的输出。 而那些早期的网格尺寸不是很好的。 我们该如何解决? U-Net吧! 让我们有一个交叉连接的向上路径。 那么我们就打算做一个U-Net,然后将它们输出来。 因为现在那些更精细的网格单元具有该路径的所有信息,该路径,该路径以及用于杠杆的路径。 当然,这是深度学习,这意味着你不能写一篇论文,说我们只是使用U-Net作为边框。 你必须发明一个新词,所以这被称为特征金字塔网络或FPN。 这在RetinaNet论文中使用,它是在早期专门关于FPN的论文中创建的。 如果内存服务正常,他们会简单地引用U-Net论文,但它们听起来有点让人觉得有些人可能会觉得有些微不足道。 但实际上,FPN是U-Nets。
我没有向你展示它的实现,但这将是一个有趣的事情,也许对我们中的一些人来说,我知道有些学生一直试图让它在论坛上运作良好。 所以是的,尝试有趣的事情。 因此,我认为在本课程以及我提到的其他内容之后要考虑的一些事情是使用FPN,也可能尝试使用Kerem的DynamicUnet。 它们都是有趣的东西。
所以你们这些人已经完成了14节课,我现在正在和你说话。 所以我很抱歉。 谢谢你忍受我。 我认为你会发现很难找到真正了解他们的人,就像你一样训练神经网络和练习。 你很容易高估所有这些人的能力,并低估你的能力。 所以我要说的主要是请练习。 只因为你没有这个不变的东西让你现在每周一晚上回到这里。 失去这种势头很容易。 所以找到保持它的方法。组织一个研究小组,一个读书小组,或与一些朋友聚在一起工作,或者做更多的事情,而不仅仅是决定我要继续研究X.除非你是一个超级积极的人,每当你决定做某事,它发生了。不是我。就像我知道的那样,为了发生某些事情,我不得不说“是的,大卫。 10月份,我绝对会教授这门课程“然后就好了我更好地写了一些材料。这是我能让事情发生的唯一方法。所以我们在论坛上有一个很棒的社区。如果人们有想法让它变得更好,请告诉我。如果你认为你可以提供帮助,如果你想创建一些新的论坛或以某种不同的方式或其他方式主持,请告诉我。你总能PM我和那里’很多项目也通过GitHub进行 - 很多东西。所以我希望看到你们所有人回到这里,并且非常感谢你们加入我的旅程。
