fast.ai 深度学习笔记(三)(2)https://developer.aliyun.com/article/1482897
让我们通过拆解 RNN 来获得更多见解
我们移除了nn.RNN
的使用,并用nn.RNNCell
替换。PyTorch 源代码如下。您应该能够阅读和理解(注意:它们不会连接输入和隐藏状态,而是将它们相加 - 这是我们的第一种方法):
def RNNCell(input, hidden, w_ih, w_hh, b_ih, b_hh): return F.tanh(F.linear(input, w_ih, b_ih) + F.linear(hidden, w_hh, b_hh))
关于tanh
的问题[44:06]:正如我们上周所看到的,tanh
强制值在-1 和 1 之间。由于我们一遍又一遍地乘以这个权重矩阵,我们担心relu
(因为它是无界的)可能会有更多的梯度爆炸问题。话虽如此,您可以指定RNNCell
使用不同的nonlineality
,其默认值为tanh
,并要求其使用relu
。
class CharSeqStatefulRnn2(nn.Module): def __init__(self, vocab_size, n_fac, bs): super().__init__() self.vocab_size = vocab_size self.e = nn.Embedding(vocab_size, n_fac) self.rnn = nn.RNNCell(n_fac, n_hidden) self.l_out = nn.Linear(n_hidden, vocab_size) self.init_hidden(bs) def forward(self, cs): bs = cs[0].size(0) if self.h.size(1) != bs: self.init_hidden(bs) outp = [] o = self.h for c in cs: o = self.rnn(self.e(c), o) outp.append(o) outp = self.l_out(torch.stack(outp)) self.h = repackage_var(o) return F.log_softmax(outp, dim=-1).view(-1, self.vocab_size) def init_hidden(self, bs): self.h = V(torch.zeros(1, bs, n_hidden))
for
循环回来并将线性函数的结果附加到列表中 - 最终将它们堆叠在一起。- 实际上,fast.ai 库确实正是为了使用 PyTorch 不支持的正则化方法而这样做的。
门控循环单元(GRU)[46:44]
在实践中,没有人真正使用RNNCell
,因为即使使用tanh
,梯度爆炸仍然是一个问题,我们需要使用较低的学习率和较小的bptt
来训练它们。因此,我们所做的是用类似GRUCell
替换RNNCell
。
- 通常,输入会乘以一个权重矩阵以创建新的激活
h
,并立即添加到现有的激活中。这里不是这样发生的。 - 输入进入
h˜
,它不仅仅被添加到先前的激活中,而是先前的激活被r
(重置门)乘以,r
的值为 0 或 1。 r
的计算如下 - 一些权重矩阵的矩阵乘法和我们先前隐藏状态和新输入的连接。换句话说,这是一个小型的单隐藏层神经网络。它也通过 sigmoid 函数传递。这个小型神经网络学会了确定要记住隐藏状态的多少(也许在看到句号字符时全部忘记 - 新句子的开始)。z
门(更新门)确定要使用h˜
(隐藏状态的新输入版本)的程度,以及要保持隐藏状态与之前相同的程度。
colah.github.io/posts/2015-08-Understanding-LSTMs/
- 线性插值
def GRUCell(input, hidden, w_ih, w_hh, b_ih, b_hh): gi = F.linear(input, w_ih, b_ih) gh = F.linear(hidden, w_hh, b_hh) i_r, i_i, i_n = gi.chunk(3, 1) h_r, h_i, h_n = gh.chunk(3, 1) resetgate = F.sigmoid(i_r + h_r) inputgate = F.sigmoid(i_i + h_i) newgate = F.tanh(i_n + resetgate * h_n) return newgate + inputgate * (hidden - newgate)
上面是GRUCell
代码的样子,我们利用这个新模型如下:
class CharSeqStatefulGRU(nn.Module): def __init__(self, vocab_size, n_fac, bs): super().__init__() self.vocab_size = vocab_size self.e = nn.Embedding(vocab_size, n_fac) self.rnn = nn.GRU(n_fac, n_hidden) self.l_out = nn.Linear(n_hidden, vocab_size) self.init_hidden(bs) def forward(self, cs): bs = cs[0].size(0) if self.h.size(1) != bs: self.init_hidden(bs) outp,h = self.rnn(self.e(cs), self.h) self.h = repackage_var(h) return F.log_softmax(self.l_out(outp), dim=-1).view(-1, self.vocab_size) def init_hidden(self, bs): self.h = V(torch.zeros(1, bs, n_hidden))
结果,我们可以将损失降低到 1.36(RNNCell
为 1.54)。在实践中,GRU 和 LSTM 是人们使用的。
将所有内容放在一起:长短期记忆[54:09]
LSTM 中还有一个称为“单元状态”的状态(不仅仅是隐藏状态),因此如果使用 LSTM,必须在init_hidden
中返回一个矩阵元组(与隐藏状态完全相同的大小):
from fastai import sgdr n_hidden=512 class CharSeqStatefulLSTM(nn.Module): def __init__(self, vocab_size, n_fac, bs, nl): super().__init__() self.vocab_size,self.nl = vocab_size,nl self.e = nn.Embedding(vocab_size, n_fac) self.rnn = nn.LSTM(n_fac, n_hidden, nl, dropout=0.5) self.l_out = nn.Linear(n_hidden, vocab_size) self.init_hidden(bs) def forward(self, cs): bs = cs[0].size(0) if self.h[0].size(1) != bs: self.init_hidden(bs) outp,h = self.rnn(self.e(cs), self.h) self.h = repackage_var(h) return F.log_softmax(self.l_out(outp), dim=-1).view(-1, self.vocab_size) def init_hidden(self, bs): self.h = (V(torch.zeros(self.nl, bs, n_hidden)), V(torch.zeros(self.nl, bs, n_hidden)))
代码与 GRU 相同。添加的一件事是dropout
,它在每个时间步之后进行 dropout 并将隐藏层加倍 - 希望它能够学到更多并且在这样做时更具弹性。
回调(特别是 SGDR)没有 Learner 类[55:23]
m = CharSeqStatefulLSTM(md.nt, n_fac, 512, 2).cuda() lo = LayerOptimizer(optim.Adam, m, 1e-2, 1e-5)
- 创建标准的 PyTorch 模型后,我们通常会做类似
opt = optim.Adam(m.parameters(), 1e-3)
的事情。相反,我们将使用 fast.ai 的LayerOptimizer
,它接受一个优化器optim.Adam
,我们的模型m
,学习率1e-2
,以及可选的权重衰减1e-5
。 LayerOptimizer
存在的一个关键原因是进行差分学习率和差分权重衰减。我们需要使用它的原因是 fast.ai 内部的所有机制都假定您有其中之一。如果要在不使用 Learner 类的代码中使用回调或 SGDR,您需要使用这个。lo.opt
返回优化器。
on_end = lambda sched, cycle: save_model(m, f'{PATH}models/cyc_{cycle}') cb = [CosAnneal(lo, len(md.trn_dl), cycle_mult=2, on_cycle_end=on_end)] fit(m, md, 2**4-1, lo.opt, F.nll_loss, callbacks=cb)
- 当我们调用
fit
时,现在可以传递LayerOptimizer
和callbacks
。 - 在这里,我们使用余弦退火回调 —— 需要一个
LayerOptimizer
对象。它通过更改lo
对象内的学习率来进行余弦退火。 - 概念:创建一个余弦退火回调,它将更新层优化器
lo
中的学习率。一个周期的长度等于len(md.trn_dl)
—— 一个周期中有多少个小批次就是数据加载器的长度。由于它正在进行余弦退火,它需要知道多久重置一次。您可以以通常的方式传递cycle_mult
。我们甚至可以自动保存我们的模型,就像我们在Learner.fit
中使用cycle_save_name
一样。 - 我们可以在训练、周期或批处理的开始时进行回调,也可以在训练、周期或批处理的结束时进行回调。
- 它已用于
CosAnneal
(SGDR),和解耦权重衰减(AdamW),随时间变化的损失图等。
测试[59:55]
def get_next(inp): idxs = TEXT.numericalize(inp) p = m(VV(idxs.transpose(0,1))) r = torch.multinomial(p[-1].exp(), 1) return TEXT.vocab.itos[to_np(r)[0]] def get_next_n(inp, n): res = inp for i in range(n): c = get_next(inp) res += c inp = inp[1:]+c return resprint(get_next_n('for thos', 400)) ''' for those the skemps), or imaginates, though they deceives. it should so each ourselvess and new present, step absolutely for the science." the contradity and measuring, the whole!* *293\. perhaps, that every life a values of blood of intercourse when it senses there is unscrupulus, his very rights, and still impulse, love? just after that thereby how made with the way anything, and set for harmless philos '''
- 在第 6 课中,当我们测试
CharRnn
模型时,我们注意到它一遍又一遍地重复。在这个新版本中使用的torch.multinomial
处理了这个问题。p[-1]
用于获取最终输出(三角形),exp
用于将对数概率转换为概率。然后我们使用torch.multinomial
函数,根据给定的概率给出一个样本。如果概率是[0, 1, 0, 0],并要求它给我们一个样本,它将始终返回第二个项目。如果是[0.5, 0, 0.5],它将 50%的时间给出第一个项目,50%的时间给出第二个项目(多项分布的评论) - 要尝试训练基于字符的语言模型,可以尝试在不同损失水平上运行
get_next_n
,以了解其外观。上面的示例是 1.25,但在 1.3 时,它看起来像一团垃圾。 - 当您在玩弄 NLP 时,特别是像这样的生成模型,并且结果还可以但不是很好时,请不要灰心,因为这意味着您实际上非常非常接近成功!
返回计算机视觉:CIFAR 10 [1:01:58]
CIFAR 10 是学术界中一个古老而著名的数据集 —— 在 ImageNet 之前,有 CIFAR 10。它在图像数量和大小方面都很小,这使得它既有趣又具有挑战性。您可能会处理成千上万张图像,而不是一百五十万张图像。此外,我们正在研究的许多内容,比如在医学成像中,我们正在查看一个肺结节的特定区域,您可能最多查看 32x32 像素。
它也运行得很快,因此最好测试一下您的算法。正如 Ali Rahini 在 NIPS 2017 中提到的,Jeremy 担心许多人在深度学习中没有进行精心调整和深思熟虑的实验,而是他们投入大量的 GPU 和 TPU 或大量的数据,然后认为一天就够了。在像 CIFAR 10 这样的数据集上测试您的算法的许多版本是很重要的,而不是像 ImageNet 那样需要几周的时间。尽管人们倾向于抱怨 MNIST,但它也适用于研究和实验。
CIFAR 10 数据以图像格式可在此处获取
from fastai.conv_learner import * PATH = "data/cifar10/" os.makedirs(PATH,exist_ok=True) classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck') stats = (np.array([ 0.4914 , 0.48216, 0.44653]), np.array([ 0.24703, 0.24349, 0.26159])) def get_data(sz,bs): tfms = tfms_from_stats(stats, sz, aug_tfms=[RandomFlipXY()], pad=sz//8) return ImageClassifierData.from_paths(PATH, val_name='test', tfms=tfms, bs=bs) bs=256
classes
— 图像标签stats
— 当我们使用预训练模型时,可以调用tfms_from_model
,它会创建必要的转换,将我们的数据集转换为基于原始模型中每个通道的均值和标准差的归一化数据集。由于我们正在从头开始训练模型,因此需要告诉它我们数据的均值和标准差以进行归一化。确保您可以计算每个通道的均值和标准差。tfms
— 对于 CIFAR 10 数据增强,人们通常会进行水平翻转和在边缘周围添加黑色填充,并在填充图像内随机选择 32x32 区域。
data = get_data(32,bs) lr=1e-2
来自我们的学生 Kerem Turgutlu 的这个笔记本:
class SimpleNet(nn.Module): def __init__(self, layers): super().__init__() self.layers = nn.ModuleList([ nn.Linear(layers[i], layers[i + 1]) for i in range(len(layers) - 1) ]) def forward(self, x): x = x.view(x.size(0), -1) for l in self.layers: l_x = l(x) x = F.relu(l_x) return F.log_softmax(l_x, dim=-1)
nn.ModuleList
- 每当您在 PyTorch 中创建一组层时,您必须将其包装在ModuleList
中以将这些注册为属性。
learn = ConvLearner.from_model_data(SimpleNet([32*32*3, 40,10]), data)
- 现在我们提高一个 API 级别 - 而不是调用
fit
函数,我们从一个自定义模型创建一个learn
对象。ConfLearner.from_model_data
接受标准的 PyTorch 模型和模型数据对象。
learn, [o.numel() for o in learn.model.parameters()] ''' (SimpleNet( (layers): ModuleList( (0): Linear(in_features=3072, out_features=40) (1): Linear(in_features=40, out_features=10) ) ), [122880, 40, 400, 10]) ''' learn.summary() ''' OrderedDict([('Linear-1', OrderedDict([('input_shape', [-1, 3072]), ('output_shape', [-1, 40]), ('trainable', True), ('nb_params', 122920)])), ('Linear-2', OrderedDict([('input_shape', [-1, 40]), ('output_shape', [-1, 10]), ('trainable', True), ('nb_params', 410)]))]) ''' learn.lr_find() learn.sched.plot()
%time learn.fit(lr, 2) ''' A Jupyter Widget [ 0\. 1.7658 1.64148 0.42129] [ 1\. 1.68074 1.57897 0.44131] CPU times: user 1min 11s, sys: 32.3 s, total: 1min 44s Wall time: 55.1 s ''' %time learn.fit(lr, 2, cycle_len=1) ''' A Jupyter Widget [ 0\. 1.60857 1.51711 0.46631] [ 1\. 1.59361 1.50341 0.46924] CPU times: user 1min 12s, sys: 31.8 s, total: 1min 44s Wall time: 55.3 s '''
通过一个具有 122,880 个参数的简单单隐藏层模型,我们实现了 46.9%的准确率。让我们改进这一点,并逐渐构建一个基本的 ResNet 架构。
CNN [01:12:30]
- 让我们用一个卷积模型替换一个全连接模型。全连接层只是做一个点积。这就是为什么权重矩阵很大(3072 个输入 * 40 = 122880)。我们没有有效地使用参数,因为输入中的每个像素都有不同的权重。我们想要做的是一组具有特定模式的 3x3 像素(即卷积)。
- 我们将使用一个 3x3 核的滤波器。当有多个滤波器时,输出将具有额外的维度。
class ConvNet(nn.Module): def __init__(self, layers, c): super().__init__() self.layers = nn.ModuleList([ nn.Conv2d(layers[i], layers[i + 1], kernel_size=3, stride=2) for i in range(len(layers) - 1) ]) self.pool = nn.AdaptiveMaxPool2d(1) self.out = nn.Linear(layers[-1], c) def forward(self, x): for l in self.layers: x = F.relu(l(x)) x = self.pool(x) x = x.view(x.size(0), -1) return F.log_softmax(self.out(x), dim=-1)
- 用
nn.Conv2d
替换nn.Linear
- 前两个参数与
nn.Linear
完全相同 - 输入特征的数量和输出特征的数量 kernel_size=3
,滤波器的大小stride=2
将使用每隔一个 3x3 区域,这将使每个维度的输出分辨率减半(即具有与 2x2 最大池化相同的效果)
learn = ConvLearner.from_model_data(ConvNet([3, 20, 40, 80], 10), data) learn.summary() ''' OrderedDict([('Conv2d-1', OrderedDict([('input_shape', [-1, 3, 32, 32]), ('output_shape', [-1, 20, 15, 15]), ('trainable', True), ('nb_params', 560)])), ('Conv2d-2', OrderedDict([('input_shape', [-1, 20, 15, 15]), ('output_shape', [-1, 40, 7, 7]), ('trainable', True), ('nb_params', 7240)])), ('Conv2d-3', OrderedDict([('input_shape', [-1, 40, 7, 7]), ('output_shape', [-1, 80, 3, 3]), ('trainable', True), ('nb_params', 28880)])), ('AdaptiveMaxPool2d-4', OrderedDict([('input_shape', [-1, 80, 3, 3]), ('output_shape', [-1, 80, 1, 1]), ('nb_params', 0)])), ('Linear-5', OrderedDict([('input_shape', [-1, 80]), ('output_shape', [-1, 10]), ('trainable', True), ('nb_params', 810)]))]) '''
ConvNet([3, 20, 40, 80], 10)
- 从 3 个 RGB 通道开始,20、40、80 个特征,然后预测 10 个类别。AdaptiveMaxPool2d
- 这是一个线性层后面的内容,通过这种方式,你可以从 3x3 降到 10 个类别中的一个预测,并且现在已经成为最先进算法的标准。在最后一层,我们进行一种特殊类型的最大池化,您需要指定输出激活分辨率,而不是要池化的区域有多大。换句话说,在这里我们进行 3x3 最大池化,相当于 1x1 的自适应最大池化。x = x.view(x.size(0), -1)
-x
的形状是特征的数量乘以 1 乘以 1,因此它将删除最后两层。- 这个模型被称为“完全卷积网络” - 每一层都是卷积的,除了最后一层。
learn.lr_find(end_lr=100) learn.sched.plot()
lr_find
尝试的默认最终学习率是 10。如果在那一点上损失仍在变好,您可以通过指定end_lr
来覆盖。
%time learn.fit(1e-1, 2) ''' A Jupyter Widget [ 0\. 1.72594 1.63399 0.41338] [ 1\. 1.51599 1.49687 0.45723] CPU times: user 1min 14s, sys: 32.3 s, total: 1min 46s Wall time: 56.5 s ''' %time learn.fit(1e-1, 4, cycle_len=1) ''' A Jupyter Widget [ 0\. 1.36734 1.28901 0.53418] [ 1\. 1.28854 1.21991 0.56143] [ 2\. 1.22854 1.15514 0.58398] [ 3\. 1.17904 1.12523 0.59922] CPU times: user 2min 21s, sys: 1min 3s, total: 3min 24s Wall time: 1min 46s '''
- 准确率在 60%左右稳定下来。考虑到它使用约 30,000 个参数(与 122k 参数的 47%相比)
- 每个时期的时间大约相同,因为它们的架构都很简单,大部分时间都花在内存传输上。
重构 [01:21:57]
通过创建 ConvLayer
(我们的第一个自定义层)简化 forward
函数。在 PyTorch 中,层定义和神经网络定义是相同的。每当您有一个层时,您可以将其用作神经网络,当您有一个神经网络时,您可以将其用作层。
class ConvLayer(nn.Module): def __init__(self, ni, nf): super().__init__() self.conv = nn.Conv2d(ni, nf, kernel_size=3, stride=2, padding=1) def forward(self, x): return F.relu(self.conv(x))
padding=1
- 当进行卷积时,图像的每一侧都会缩小 1 个像素。因此,它不是从 32x32 到 16x16,而实际上是 15x15。padding
将添加一个边框,以便我们可以保留边缘像素信息。对于大图像来说,这不是一个大问题,但当缩小到 4x4 时,您真的不想丢弃整个部分。
class ConvNet2(nn.Module): def __init__(self, layers, c): super().__init__() self.layers = nn.ModuleList([ ConvLayer(layers[i], layers[i + 1]) for i in range(len(layers) - 1) ]) self.out = nn.Linear(layers[-1], c) def forward(self, x): for l in self.layers: x = l(x) x = F.adaptive_max_pool2d(x, 1) x = x.view(x.size(0), -1) return F.log_softmax(self.out(x), dim=-1)
- 与上一个模型的另一个不同之处是
nn.AdaptiveMaxPool2d
没有任何状态(即没有权重)。因此,我们可以将其作为一个函数F.adaptive_max_pool2d
调用。
BatchNorm [1:25:10]
- 最后一个模型,当我们尝试添加更多层时,我们遇到了训练困难。我们遇到训练困难的原因是,如果使用更大的学习率,它会变成 NaN,如果使用更小的学习率,它将花费很长时间,无法正确探索 - 因此它不具有弹性。
- 为了使其具有弹性,我们将使用一种称为批量归一化的东西。 BatchNorm 大约两年前出现,自那时以来,它已经发生了很大变化,因为它突然使训练更深的网络变得非常容易。
- 我们可以简单地使用
nn.BatchNorm
,但为了了解它,我们将从头开始编写。 - 平均来看,权重矩阵不太可能导致激活不断变小或不断变大。保持它们在合理的范围内很重要。因此,我们从零均值标准差为 1 开始通过对输入进行归一化。我们真正想要做的是对所有层进行这样的操作,而不仅仅是对输入。
class BnLayer(nn.Module): def __init__(self, ni, nf, stride=2, kernel_size=3): super().__init__() self.conv = nn.Conv2d( ni, nf, kernel_size=kernel_size, stride=stride, bias=False, padding=1 ) self.a = nn.Parameter(torch.zeros(nf,1,1)) self.m = nn.Parameter(torch.ones(nf,1,1)) def forward(self, x): x = F.relu(self.conv(x)) x_chan = x.transpose(0,1).contiguous().view(x.size(1), -1) if self.training: self.means = x_chan.mean(1)[:,None,None] self.stds = x_chan.std (1)[:,None,None] return (x-self.means) / self.stds *self.m + self.a
- 计算每个通道或每个滤波器的均值和每个通道或每个滤波器的标准差。然后减去均值并除以标准差。
- 我们不再需要归一化我们的输入,因为它是按通道归一化的,或者对于后续层,它是按滤波器归一化的。
- 事实证明这还不够,因为 SGD 是固执的。如果 SGD 决定要使矩阵整体变大/变小,那么做
(x=self.means) / self.stds
是不够的,因为 SGD 会撤消它,并尝试在下一个小批次中再次执行。因此,我们将添加两个参数:a
- 加法器(初始值为零)和m
- 乘法器(初始值为 1)用于每个通道。 Parameter
告诉 PyTorch 可以将这些作为权重进行学习。- 为什么这样做?如果要扩展该层,它不必扩展矩阵中的每个值。如果要将其全部上移或下移一点,它不必移动整个权重矩阵,它们只需移动这三个数字
self.m
。直觉:我们正在对数据进行归一化,然后我们说您可以使用远少于实际需要的参数来移动和缩放它,而不是移动和缩放整套卷积滤波器。在实践中,它允许我们增加学习速率,增加训练的弹性,并且允许我们添加更多层并仍然有效地进行训练。 - 批量归一化的另一件事是正则化,换句话说,您通常可以减少或删除辍学或权重衰减。原因是每个小批次将具有不同的均值和不同的标准差与上一个小批次不同。因此它们不断变化,以微妙的方式改变滤波器的含义,起到噪声(即正则化)的作用。
- 在真实版本中,它不使用这个批次的均值和标准差,而是采用指数加权移动平均标准差和均值。
**if** self.training
- 这很重要,因为当您通过验证集时,您不希望更改模型的含义。有一些类型的层实际上对网络的模式敏感,无论它是处于训练模式还是评估/测试模式。当我们为 MovieLens 实现迷你网络时,存在一个错误,即在验证期间应用了辍学 - 这已经得到修复。在 PyTorch 中,有两种这样的层:辍学和批量归一化。nn.Dropout
已经进行了检查。- 在 fast.ai 中的关键区别是,这些均值和标准差在训练模式下会得到更新,而在其他库中,只要您说“我在训练”,无论该层是否可训练,这些均值和标准差就会立即得到更新。对于预训练网络来说,这是一个糟糕的主意。如果您有一个针对批量归一化中这些均值和标准差的特定值进行预训练的网络,如果更改它们,就会改变这些预训练层的含义。在 fast.ai 中,默认情况下,如果您的层被冻结,它将不会触及这些均值和标准差。一旦您解冻它,它将开始更新它们,除非您设置
learn.bn_freeze=True
。实际上,这在处理与预训练模型非常相似的数据时似乎经常效果更好。 - 您应该在哪里放置批量归一化层?我们稍后会详细讨论,但现在,在
relu
之后
fast.ai 深度学习笔记(三)(4)https://developer.aliyun.com/article/1482899