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

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
云原生网关 MSE Higress,422元/月
注册配置 MSE Nacos/ZooKeeper,118元/月
简介: fast.ai 深度学习笔记(七)

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

原文:medium.com/@hiromi_suenaga/deep-learning-2-part-2-lesson-14-e0d23c7a0add

译者:飞龙

协议:CC BY-NC-SA 4.0

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

论坛 / 视频

上周的展示


Alena Harley 做了一些非常有趣的事情,她尝试找出如果只对三四百张图片进行循环 GAN 会发生什么,我真的很喜欢这些项目,人们只需使用 API 或其中一个库去谷歌图片搜索。我们的一些学生已经创建了一些非常好的库,用于与谷歌图片 API 进行交互,下载一些他们感兴趣的东西,比如一些照片和一些彩色玻璃窗。有了 300~400 张照片,她训练了几个不同的模型——这是我特别喜欢的。正如你所看到的,用相当少量的图片,她得到了非常漂亮的彩色玻璃效果。所以我认为这是一个有趣的例子,使用相当少量的数据,她能够很快地下载到的数据。如果你感兴趣,论坛上有更多信息。

人们会用这种生成模型想出什么样的东西是很有趣的。这显然是一个很好的艺术媒介。显然也是一个很好的伪造和欺骗媒介。我想知道人们会意识到他们可以用这种生成模型做什么其他类型的事情。我认为音频将成为下一个重要领域。还有非常互动的类型。英伟达刚刚发布了一篇论文,展示了一种互动的照片修复工具,你只需刷过一个物体,它就会用深度学习生成的替代品替换得很好。我认为这种互动工具也会很有趣。

超分辨率[2:06]

实时风格转移和超分辨率的感知损失

上次,我们看了通过直接优化像素来进行风格转移。就像第二部分的大部分内容一样,我并不是想让你理解风格转移本身,而是直接优化输入并使用激活作为损失函数的一种想法,这才是真正的关键点。

因此,有趣的是看到接下来的论文,不是来自同一组人,而是在这些视觉生成模型序列中接下来的一篇来自斯坦福大学的 Justin Johnson 和他的同事。它实际上做了同样的事情——风格转移,但是用了不同的方法。与其优化像素,我们将回到更熟悉的东西,优化一些权重。具体来说,我们将训练一个模型,学习将一张照片转换成某种艺术作品风格的照片。因此,每个卷积网络将学习产生一种风格。

现在,事实证明,要达到那一点,有一个中间点(我认为更有用,可以让我们走一半的路)叫做超分辨率。所以我们实际上要从超分辨率开始[3:55]。因为然后我们将在超分辨率的基础上构建卷积神经网络风格转移的最后部分。

超分辨率是指我们将一个低分辨率图像(我们将采用 72x72)放大到一个更大的图像(在我们的情况下是 288x288),试图创建一个看起来尽可能真实的高分辨率图像。这是一件具有挑战性的事情,因为在 72x72 的情况下,关于很多细节的信息并不多。很酷的是,我们将以一种与视觉模型相似的方式来做,这种方式不受输入大小的限制,因此您完全可以将这个模型应用于 288x288 的图像,得到每边都大四倍的东西,比原始图像大 16 倍。通常在那个级别甚至效果更好,因为您真的在更细节的地方引入了很多细节,您可以真正打印出一个高分辨率的打印品,而之前它看起来相当像素化。

笔记本

这很像 CSI 风格的增强,我们将拿出一些看起来信息不在那里的东西,我们会发明它——但是卷积网络将学会以与已有信息一致的方式发明它,所以希望它发明正确的信息。这种问题的一个非常好的地方是,我们可以创建自己的数据集,而不需要任何标签要求,因为我们可以通过对图像进行降采样轻松地从高分辨率图像创建低分辨率图像。所以我希望你们中的一些人这周尝试做其他类型的图像到图像的转换,你可以发明“标签”(你的因变量)。例如:

  • 去斜:识别已经旋转了 90 度或更好的是旋转了 5 度并将其拉直的东西。
  • 着色:将一堆图像变成黑白,然后学会重新加上颜色。
  • 降噪:也许做一个质量很低的 JPEG 保存,然后学会将其恢复到应该有的样子。
  • 也许将一个 16 色调色板的东西放回到更高的色调色板。

我认为这些东西都很有趣,因为它们可以用来处理您以前用糟糕的旧数码相机拍摄的照片,或者您可能已经扫描了一些现在已经褪色的旧照片等。我认为这是一件非常有用的事情,也是一个很好的项目,因为它与我们在这里所做的非常相似,但又有足够的不同,让您在途中遇到一些有趣的挑战,我相信。

我将再次使用 ImageNet。您根本不需要使用所有的 ImageNet,我只是碰巧有它。您可以从 files.fast.ai 下载 ImageNet 的百分之一样本。您可以使用您手头上任何一组图片。

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,VGG16 是我们在这门课程中还没有真正研究过的东西,但它是一个非常简单的模型,我们将采用我们通常的预计是 3 通道输入,然后基本上通过一系列 3x3 的卷积运行它,然后不时地,我们将它通过一个 2x2 的最大池化,然后我们再做一些 3x3 的卷积,最大池化,依此类推。这就是我们的骨干。

然后我们不再使用自适应平均池化层。经过几次操作后,我们像往常一样得到了一个 7x7x512 的网格(或类似的东西)。所以我们不再进行平均池化,而是做一些不同的事情,即将整个东西展平 - 这样就会输出一个大小为 7x7x512 的非常长的激活向量。然后将其馈送到两个全连接层,每个全连接层有 4096 个激活,并且还有一个具有多少类别的全连接层。所以如果你考虑一下,这里的权重矩阵是巨大的 7x7x512x4096。正是因为这个权重矩阵,VGG 很快就不受欢迎了 - 因为它占用了大量内存,需要大量计算,速度非常慢。这里有很多冗余的东西,因为实际上这 512 个激活并不特定于它们在哪个 7x7 网格单元中。但是当你有这里的整个权重矩阵,包含了每种可能的组合,它会将它们都视为独特的。这也可能导致泛化问题,因为有很多权重等等。

我认为现代网络中使用的方法是进行自适应平均池化(在 Keras 中被称为全局平均池化,在 fast.ai 中我们使用自适应连接池),这将直接输出一个 512 维的激活。我认为这样做丢失了太多的几何信息。所以对我来说,可能正确的答案在两者之间,并且可能涉及某种因子卷积或张量分解,也许我们中的一些人可以在未来几个月考虑一下。所以目前,我们已经从自适应平均池化这个极端转向了另一个极端,即这个巨大的扁平化全连接层。

关于 VGG 有一些有趣的事情,使它至今仍然有用[11:59]。第一件事是这里有更多有趣的层,大多数现代网络包括 ResNet 系列,第一层通常是一个 7x7 的卷积,步幅为 2 或类似的。这意味着我们立即丢弃了一半的网格大小,因此几乎没有机会使用细节,因为我们从不对其进行任何计算。这对于分割或超分辨率模型等需要细节的问题是一个问题。我们实际上想要恢复它。然后第二个问题是自适应池化层完全丢弃了最后几个部分的几何信息,这意味着模型的其余部分实际上没有太多有趣的几何学习。因此,对于依赖位置的事物,任何需要生成模型的定位方法都会不太有效。所以我希望你在我描述这些内容时能听到的一件事是,也许现有的架构都不是理想的。我们可以发明一个新的。实际上,我在这一周尝试了发明一个新的,就是将 VGG 头部连接到 ResNet 骨干上。有趣的是,我发现我实际上得到了一个稍微更好的分类器,比普通的 ResNet 好一点,但它也包含了一些更有用的信息。训练时间长了 5 到 10%,但没有什么值得担心的。也许我们可以在 ResNet 中,用我们之前简要讨论过的方式,将这个(7x7 卷积步幅 2)替换为更像 Inception stem 的东西,这样有更多的计算。我认为这些架构肯定有一些小的调整空间,这样我们可以构建一些可能更多功能的模型。目前,人们倾向于构建只能做一件事的架构。他们并没有真正考虑到机会的丢失,因为这就是出版的工作方式。你发表“我在这一件事上达到了最新水平”而不是你创造了一些在很多方面都很擅长的东西。

出于这些原因,今天我们将使用 VGG,尽管它已经过时并且缺少很多很棒的东西[14:42]。不过,我们要做的一件事是使用一个稍微更现代的版本,这是一个在所有卷积层之后添加了批量归一化的 VGG 版本。在 fast.ai 中,当你请求一个 VGG 网络时,你总是得到批量归一化的版本,因为那基本上总是你想要的。所以这是带有批量归一化的 VGG。有 16 和 19,19 更大更重,但实际上并没有做得更好,所以没有人真的使用它。

arch = vgg16
sz_lr = 72

我们将从 72x72 的 LR(sz_lr:低分辨率大小)输入开始。我们将首先通过 64 的批次大小将其放大 2 倍,以获得 2 * 72,即 144x144 的输出。这将是我们的第一阶段。

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

我将进行一些数据增强。显然,对于所有的 ImageNet,我们并不真正需要它,但这主要是为了任何使用较小数据集的人能够充分利用它。RandomDihedral指的是每个可能的 90 度旋转加上可选的左/右翻转,因此它们是八个对称的二面角群。通常我们不会对 ImageNet 图片使用这种转换,因为你通常不会把狗颠倒过来,但在这种情况下,我们并不是试图分类它是狗还是猫,我们只是试图保持它的一般结构。因此,实际上对于这个问题来说,每个可能的翻转都是一个相当明智的事情。

aug_tfms = [RandomDihedral(tfm_y=TfmType.PIXEL)]

以通常的方式创建一个验证集。你可以看到我使用了一些更低级别的函数——一般来说,我只是从 fastai 源代码中复制和粘贴它们,找到我想要的部分。这里有一个部分,它接受一个验证集索引数组和一个或多个变量数组,然后简单地分割。在这种情况下,这个(np.array(fnames))分成一个训练和验证集,这个(第二个np.array(fnames))分成一个训练和验证集,给我们我们的xy。在这种情况下,xy是相同的。我们的输入图像和输出图像是相同的。我们将使用转换使它们中的一个分辨率较低。这就是为什么它们是相同的东西。

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'

接下来我们需要像往常一样创建我们的转换。我们将使用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)

你可以看到你得到的可能的转换类型:

  • 分类:我们将在今天的下半部分使用分割
  • 坐标:坐标——没有任何转换
  • 像素

一旦我们有了Dataset类和一些xy的训练和验证集。有一个方便的小方法叫做获取数据集(get_ds),它基本上运行构造函数,返回你需要的所有数据集,以恰好正确的格式传递给 ModelData 构造函数(在这种情况下是ImageData构造函数)。所以我们有点回到了 fastai 的内部,从头开始构建。在接下来的几周里,这一切都将被整合和重构成你可以在 fastai 中一步完成的东西。但这个类的目的是为了学习一些关于内部的知识。

我们之前简要看到的是,当我们输入图像时,我们不仅要进行数据增强,还要将通道维度移到开头,我们要减去平均值除以标准差等。所以如果我们想要显示那些从我们的数据集或数据加载器中出来的图片,我们需要对它们进行反归一化。所以模型数据对象(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]))
'''

你会看到我们传入了低分辨率大小(sz_lr)作为我们的转换大小,高分辨率大小(sz_hr)作为,这是新的东西,大小 y 参数(sz_y)。所以这两部分将得到不同的大小。

在这里,您可以看到我们的xy的两种不同分辨率,用于一大堆鱼。

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]

让我们创建我们的模型。我们将有一个小图像输入,并且我们希望有一个大图像输出。因此,我们需要在这两者之间进行一些计算,以计算大图像会是什么样子。基本上有两种方法来进行这种计算:

  • 我们首先可以进行一些上采样,然后进行一些步幅为 1 的层来进行大量计算。
  • 我们可以首先进行大量步幅为 1 的层来进行所有计算,然后最后进行一些上采样。

我们将选择第二种方法,因为我们想在较小的东西上进行大量计算,因为这样做速度更快。此外,在上采样过程中,我们可以利用所有这些计算。上采样,我们知道有几种可能的方法可以做到这一点。我们可以使用:

  • 转置或分数步幅卷积
  • 最近邻上采样,然后是 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 类型的模型来说是非常不寻常的。


arxiv.org/abs/1707.02921

没有批量归一化的原因是因为我从这篇最近的出色论文中窃取了一些想法,这篇论文实际上赢得了最近的超分辨率性能比赛。要看看这篇论文有多好,SRResNet 是之前的最先进技术,他们在这里所做的是他们已经放大到了一个上采样的网格/围栏。HR 是原始的。您可以看到在以前的最佳方法中,存在大量的失真和模糊。或者,在他们的方法中,几乎完美。因此,这篇论文是一个真正的重大进步。他们称其模型为 EDSR(增强深度超分辨率网络),并且他们与以前的标准方法有两点不同:

  1. 拿起 ResNet 块并丢弃批量归一化。为什么要丢弃批量归一化?原因是因为批量归一化会改变东西,而我们希望有一个不改变东西的良好直通路径。因此,这里的想法是,如果您不想对输入进行更多操作,那么就不要强迫它计算诸如批量归一化参数之类的东西-所以丢弃批量归一化。
  2. 缩放因子(我们很快会看到)。
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 路径(卷积,ReLU,卷积),然后通过将其添加回到身份来将其转换为 ReLU 块——我们经常这样做,以至于我将其提取出来成为一个名为 ResSequential 的小模块。它简单地将您想要放入残差路径的一堆层转换为顺序模型,运行它,然后将其添加回输入。有了这个小模块,我们现在可以通过将其包装在 ResSequential 中,将任何东西,比如卷积激活卷积,转换为一个 ResNet 块。

但这并不是我正在做的全部,因为通常一个 Res 块在它的forward中只有x + self.m(x)。但我还加上了* self.res_scale。什么是res_scaleres_scale是数字 0.1。为什么要有它?我不确定有人完全知道。但简短的答案是,发明批量归一化的那个人最近还发表了一篇论文,他在其中首次展示了在不到一个小时内训练 ImageNet 的能力。他是如何做到的呢?他启动了大量的机器,并让它们并行工作,以创建非常大的批量大小。通常情况下,当你将批量大小增加N倍时,你也会相应地增加N倍的学习率。所以通常情况下,非常大的批量大小训练也意味着非常高的学习率训练。他发现,当使用这些非常大的批量大小,如 8000+甚至高达 32000 时,在训练开始时,他的激活基本上会直接变为无穷大。很多其他人也发现了这一点。我们在 DAWN bench 上参加 CIFAR 和 ImageNet 比赛时也发现了这一点,我们很难充分利用我们试图利用的八个 GPU,因为这些更大批量大小和利用它们的挑战。Christian 发现的一件事是,在 ResNet 块中,如果他将它们乘以小于 1 的某个数字,比如 0.1 或 0.2,这确实有助于在开始时稳定训练。这有点奇怪,因为从数学上讲,它是相同的。因为显然,无论我在这里乘以什么,我只需按相反的数量缩放权重,就可以得到相同的数字。但我们不是在处理抽象的数学——我们在处理真实的优化问题,不同的初始化、学习率和其他因素。所以权重消失到无穷大的问题,我想通常主要是关于计算机在实践中的离散和有限性质的一部分。因此,通常这种小技巧可以起到关键作用。

在这种情况下,我们只是根据我们的初始初始化来调整事物。所以可能还有其他方法可以做到这一点。例如,Nvidia 的一些人提出的一种叫做 LARS 的方法,我上周简要提到过,这是一种实时计算的判别学习率方法。基本上是通过查看梯度和激活之间的比率来按层缩放学习率。因此,他们发现他们不需要这个技巧来大幅增加批量大小。也许只需要不同的初始化就足够了。我提到这一点的原因并不是因为我认为你们中很多人可能想要在大型计算机集群上进行训练,而是因为我认为你们中很多人想要快速训练模型,这意味着使用高学习率,并且理想情况下实现超级收敛。我认为这些技巧是我们需要能够在更多不同的架构等方面实现超级收敛的技巧。除了 Leslie Smith 之外,没有其他人真正致力于超级收敛,现在只有一些 fastai 学生在做这些事情。因此,关于如何以非常非常高的学习率进行训练的问题,我们将不得不自己去解决,因为据我所知,其他人还没有关心这个问题。因此,查看围绕在一个小时内训练 ImageNet 的文献,或者最近有现在在 15 分钟内训练 ImageNet 的文献,我认为,这些论文实际上有一些技巧可以让我们以高学习率训练事物。这就是其中之一。

有趣的是,除了在一个小时内训练 ImageNet 的论文中提到过之外,我唯一看到这个提到的地方是在这篇 EDSR 论文中。这真的很酷,因为赢得比赛的人,我发现他们非常务实和博学。他们实际上必须让事情运转起来。因此,这篇论文描述了一种方法,实际上比任何其他方法都要好,他们做了这些务实的事情,比如放弃批量归一化,使用几乎没有人知道的这个小缩放因子。所以这就是 0.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 个通道,只是为了稍微丰富一下空间。然后我们实际上有 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 卷积,但它在每对像素之间插入白色零像素。这使得输入图像变大,当我们在其上运行这个卷积时,因此会给我们一个更大的输出。但这显然很愚蠢,因为当我们到达这里时,例如,从九个像素中进入的八个是零。所以我们只是浪费了大量的计算。另一方面,如果我们稍微偏离,那么我们九个中有四个是非零的。但是,我们只有一个滤波器/核来使用,所以它不能根据进入的零的数量而改变。所以它必须适用于两者,这是不可能的,所以我们最终得到这些伪像。

deeplearning.net/software/theano/tutorial/conv_arithmetic.html

我们学到的一种方法是不要在这里放白色的东西,而是将像素的值复制到这三个位置中的每一个[36:53]。所以这是最近邻上采样。这当然好一点,但仍然相当糟糕,因为现在当我们到达这九个(如上所示)时,其中有 4 个是完全相同的数字。当我们移动一个时,现在我们有了完全不同的情况。所以取决于我们在哪里,特别是,如果我们在这里,重复会少得多:

所以再次,我们有这样一个问题,即存在浪费的计算和数据中的太多结构,这将再次导致伪像。因此,上采样比转置卷积更好——最好复制它们而不是用零替换它们。但这仍然不够好。

因此,我们将进行像素洗牌[37:56]。像素洗牌是这个次像素卷积神经网络中的一个操作,有点令人费解,但却很迷人。


使用高效的次像素卷积神经网络进行实时单图像和视频超分辨率

我们从输入开始,经过一些卷积一段时间,直到最终到达第*n[i-1]*层,其中有 n[i-1]个特征图。我们将进行另一个 3x3 卷积,我们的目标是从一个 7x7 的网格单元(我们将进行一个 3x3 的放大),所以我们将扩展到一个 21x21 的网格单元。那么我们还有另一种方法可以做到这一点吗?为了简化,让我们只选择一个面/层-所以让我们取最顶部的滤波器,只对其进行卷积,看看会发生什么。我们要做的是使用一个卷积,其中卷积核大小(滤波器数量)比我们需要的大九倍(严格来说)。所以如果我们需要 64 个滤波器,实际上我们要做的是 64 乘以 9 个滤波器。为什么?这里,r 是比例因子,所以 3²是 9,这里有九个滤波器来覆盖这些输入层/切片中的一个。但我们可以做的是,我们从 7x7 开始,然后将其转换为 7x7x9。我们想要的输出等于 7 乘以 3 乘以 7 乘以 3。换句话说,这里的像素/激活数量与上一步的激活数量相同。所以我们可以重新洗牌这些 7x7x9 的激活,以创建这个 7x3 乘以 7x3 的地图。所以我们要做的是,我们要取这里的一个小管道(所有网格的左上角),我们要把紫色的放在左上角,然后把蓝色的放在右边,淡蓝色的放在右边,稍微深一点的放在最左边的中间,绿色的放在中间,依此类推。所以这些九个单元中的每一个在左上角,它们最终会出现在我们网格的小 3x3 部分中。然后我们要取(2,1)并将所有这 9 个移动到网格的这个 3x3 部分,依此类推。所以我们最终会在 7x3 乘以 7x3 的图像中有每一个这些 7x7x9 的激活。

所以首先要意识到的是,当然这在某种定义下是有效的,因为我们这里有一个可学习的卷积,它将得到一些梯度,这些梯度将尽力填充正确的激活,使得输出是我们想要的东西。所以第一步是意识到这里没有什么特别神奇的地方。我们可以创建任何我们喜欢的架构。我们可以随意移动事物,我们想要的方式,我们的卷积中的权重将尽力做到我们要求的一切。真正的问题是——这是一个好主意吗?这是一个更容易做的事情,也是一个更灵活的事情,比转置卷积或上采样后再进行一对一卷积更好吗?简短的答案是是的,原因很简单,因为这里的卷积发生在低分辨率的 7x7 空间中,这是相当高效的。否则,如果我们首先进行上采样,然后再进行卷积,那么我们的卷积将发生在 21x21 的空间中,这是很多计算。此外,正如我们讨论过的,最近邻上采样版本中存在很多复制和冗余。实际上,他们在这篇论文中展示了这一点,事实上,我认为他们有一个后续的技术说明,其中提供了更多关于正在进行的工作的数学细节,并展示了这种方式确实更有效。所以这就是我们要做的。对于我们的上采样,我们有两个步骤:

  1. 3x3 卷积,比我们最初想要的通道数多r²倍
  2. 然后是一个像素洗牌操作,将每个网格单元中的所有内容移动到遍布其中的小r乘以r的网格中。

所以这就是:

这只是一行代码。这是一个卷积,输入数量到输出数量乘以四,因为我们正在进行一个比例为 2 的上采样(2²=4)。这是我们的卷积,然后这里是我们的像素洗牌,它内置在 PyTorch 中。像素洗牌是将每个东西移动到正确位置的东西。因此,这将通过一个比例因子为 2 进行上采样。所以我们需要做对数以 2 为底的比例次数。如果比例是四,那么我们将做两次,以便两次两次。这就是这里的上采样所做的事情。

棋盘格模式[44:19]

太好了。猜猜看。这并没有消除棋盘格模式。我们仍然有棋盘格模式。所以我相信在极度愤怒和沮丧的情况下,来自 Twitter 团队的同一团队,我认为这是在他们被 Twitter 收购之前的一个创业公司叫做魔术小马,他们再次回来,发表了另一篇论文,说好吧,这次我们消除了棋盘格。


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]。所以如果你现在打印出你的模块,比如以前它只是一个无限的顺序,现在你会发现它是一个嵌入在一个名为Module的模块内部的nn.Sequential。换句话说,如果你保存了一个nn.DataParallel的东西,然后尝试将其加载到一个没有nn.DataParallel的东西中,它会说它不匹配,因为其中一个嵌入在这个 Module 属性内部,而另一个没有。甚至可能取决于你将其复制到的 GPU ID。两种可能的解决方案:

  1. 不要保存模块m,而是保存模块属性m.module,因为那实际上是非数据并行位。
  2. 始终将其放在相同的 GPU ID 上,然后使用数据并行,并每次加载和保存。这就是我使用的方法。

这对我来说很容易在 fast.ai 中自动修复,我很快就会做到,这样它就会自动查找那个模块属性并自动处理。但是现在,我们必须手动操作。了解背后发生的事情可能很有用。

所以我们有了我们的模块[49:46]。我发现如果你在 1080Ti 上运行,它会比较快 50%或 60%,如果你在 volta 上运行,它实际上会并行化得更好。有更快的并行化方式,但这是一个超级简单的方式。

我们以通常的方式创建我们的学习器。我们可以在这里使用 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);

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


相关实践学习
部署Stable Diffusion玩转AI绘画(GPU云服务器)
本实验通过在ECS上从零开始部署Stable Diffusion来进行AI绘画创作,开启AIGC盲盒。
相关文章
|
20天前
|
人工智能 测试技术 API
AI计算机视觉笔记二十 九:yolov10竹签模型,自动数竹签
本文介绍了如何在AutoDL平台上搭建YOLOv10环境并进行竹签检测与计数。首先从官网下载YOLOv10源码并创建虚拟环境,安装依赖库。接着通过官方模型测试环境是否正常工作。然后下载自定义数据集并配置`mycoco128.yaml`文件,使用`yolo detect train`命令或Python代码进行训练。最后,通过命令行或API调用测试训练结果,并展示竹签计数功能。如需转载,请注明原文出处。
|
20天前
|
JSON 人工智能 数据格式
AI计算机视觉笔记二十六:YOLOV8自训练关键点检测
本文档详细记录了使用YOLOv8训练关键点检测模型的过程。首先通过清华源安装YOLOv8,并验证安装。接着通过示例权重文件与测试图片`bus.jpg`演示预测流程。为准备训练数据,文档介绍了如何使用`labelme`标注工具进行关键点标注,并提供了一个Python脚本`labelme2yolo.py`将标注结果从JSON格式转换为YOLO所需的TXT格式。随后,通过Jupyter Notebook可视化标注结果确保准确性。最后,文档展示了如何组织数据集目录结构,并提供了训练与测试代码示例,包括配置文件`smoke.yaml`及训练脚本`train.py`,帮助读者完成自定义模型的训练与评估。
|
5天前
|
机器学习/深度学习 人工智能 自然语言处理
探索AI的未来:深度学习与自然语言处理的融合
【9月更文挑战第22天】本文旨在探讨AI技术中深度学习与自然语言处理的结合,以及它们如何共同推动未来技术的发展。我们将通过实例和代码示例,深入理解这两种技术如何相互作用,以及它们如何影响我们的生活和工作。
23 4
|
16天前
|
机器学习/深度学习 人工智能 自然语言处理
探索AI的奥秘:深度学习与神经网络
【9月更文挑战第11天】本文将深入探讨人工智能的核心领域——深度学习,以及其背后的神经网络技术。我们将从基础理论出发,逐步深入到实践应用,揭示这一领域的神秘面纱。无论你是AI领域的初学者,还是有一定基础的开发者,都能在这篇文章中获得新的启示和理解。让我们一起踏上这场探索之旅,揭开AI的神秘面纱,体验深度学习的魅力。
|
20天前
|
人工智能 并行计算 PyTorch
AI计算机视觉笔记十八:Swin Transformer目标检测环境搭建
本文详细记录了Swin Transformer在AutoDL平台上的环境搭建与训练过程。作者从租用GPU实例开始,逐步介绍了虚拟环境的创建、PyTorch安装、mmcv及mmdetection的配置,并解决了安装过程中遇到的各种问题,如cython版本冲突等。最后,通过修改代码实现目标检测结果的保存。如需了解更多细节或获取完整代码,请联系作者。原文链接:[原文链接](请在此处插入原文链接)。
|
20天前
|
机器学习/深度学习 人工智能 PyTorch
AI计算机视觉笔记三十二:LPRNet车牌识别
LPRNet是一种基于Pytorch的高性能、轻量级车牌识别框架,适用于中国及其他国家的车牌识别。该网络无需对字符进行预分割,采用端到端的轻量化设计,结合了squeezenet和inception的思想。其创新点在于去除了RNN,仅使用CNN与CTC Loss,并通过特定的卷积模块提取上下文信息。环境配置包括使用CPU开发板和Autodl训练环境。训练和测试过程需搭建虚拟环境并安装相关依赖,执行训练和测试脚本时可能遇到若干错误,需相应调整代码以确保正确运行。使用官方模型可获得较高的识别准确率,自行训练时建议增加训练轮数以提升效果。
|
20天前
|
人工智能 开发工具 计算机视觉
AI计算机视觉笔记三十:yolov8_obb旋转框训练
本文介绍了如何使用AUTODL环境搭建YOLOv8-obb的训练流程。首先创建虚拟环境并激活,然后通过指定清华源安装ultralytics库。接着下载YOLOv8源码,并使用指定命令开始训练,过程中可能会下载yolov8n.pt文件。训练完成后,可使用相应命令进行预测测试。
|
20天前
|
人工智能 PyTorch 算法框架/工具
AI计算机视觉笔记二十二:基于 LeNet5 的手写数字识别及训练
本文介绍了使用PyTorch复现LeNet5模型并检测手写数字的过程。通过搭建PyTorch环境、安装相关库和下载MNIST数据集,实现了模型训练与测试。训练过程涉及创建虚拟环境、安装PyTorch及依赖库、准备数据集,并编写训练代码。最终模型在测试集上的准确率达到0.986,满足预期要求。此项目为后续在RK3568平台上部署模型奠定了基础。
|
20天前
|
人工智能 并行计算 测试技术
AI计算机视觉笔记三十一:基于UNetMultiLane的多车道线等识别
该项目基于开源数据集 VIL100 实现了 UNetMultiLane,用于多车道线及车道线类型的识别。数据集中标注了六个车道的车道线及其类型。项目详细记录了从环境搭建到模型训练与测试的全过程,并提供了在 CPU 上进行训练和 ONNX 转换的代码示例。训练过程约需 4 小时完成 50 个 epoch。此外,还实现了视频检测功能,可在视频中实时识别车道线及其类型。
|
20天前
|
传感器 人工智能 算法
AI计算机视觉笔记二十七:YOLOV8实现目标追踪
本文介绍了使用YOLOv8实现人员检测与追踪的方法。通过为每个人员分配唯一ID,实现持续追踪,并可统计人数,适用于小区或办公楼出入管理。首先解释了目标检测与追踪的区别,接着详细描述了使用匈牙利算法和卡尔曼滤波实现目标关联的过程。文章提供了基于IOU实现追踪的具体步骤,包括环境搭建、模型加载及追踪逻辑实现。通过示例代码展示了如何使用YOLOv8进行实时视频处理,并实现人员追踪功能。测试结果显示,该方法在实际场景中具有较好的应用潜力。