fast.ai 深度学习笔记(三)(1)https://developer.aliyun.com/article/1482896
让我们创建我们的第一个 RNN[1:37:45]
我们可以简化上一个图表如下:
使用 1 到 n-1 个字符预测第 n 个字符
让我们实现这个。这次,我们将使用前 8 个字符来预测第 9 个。这是如何创建输入和输出的,就像上次一样:
cs = 8 c_in_dat = [[idx[i+j] for i in range(cs)] for j in range(len(idx)-cs)] c_out_dat = [idx[j+cs] for j in range(len(idx)-cs)] xs = np.stack(c_in_dat, axis=0) y = np.stack(c_out_dat) xs[:cs,:cs] ''' array([[40, 42, 29, 30, 25, 27, 29, 1], [42, 29, 30, 25, 27, 29, 1, 1], [29, 30, 25, 27, 29, 1, 1, 1], [30, 25, 27, 29, 1, 1, 1, 43], [25, 27, 29, 1, 1, 1, 43, 45], [27, 29, 1, 1, 1, 43, 45, 40], [29, 1, 1, 1, 43, 45, 40, 40], [ 1, 1, 1, 43, 45, 40, 40, 39]]) ''' y[:cs] ''' array([ 1, 1, 43, 45, 40, 40, 39, 43]) '''
请注意,它们是重叠的(即 0-7 预测 8,1-8 预测 9)。
val_idx = get_cv_idxs(len(idx)-cs-1) md = ColumnarModelData.from_arrays('.', val_idx, xs, y, bs=512)
创建模型[1:43:03]
class CharLoopModel(nn.Module): # This is an RNN! def __init__(self, vocab_size, n_fac): super().__init__() self.e = nn.Embedding(vocab_size, n_fac) self.l_in = nn.Linear(n_fac, n_hidden) self.l_hidden = nn.Linear(n_hidden, n_hidden) self.l_out = nn.Linear(n_hidden, vocab_size) def forward(self, *cs): bs = cs[0].size(0) h = V(torch.zeros(bs, n_hidden).cuda()) for c in cs: inp = F.relu(self.l_in(self.e(c))) h = F.tanh(self.l_hidden(h+inp)) return F.log_softmax(self.l_out(h), dim=-1)
大部分代码与以前相同。您会注意到forward
函数中有一个for
循环。
双曲正切(Tanh)[1:43:43]
这是一个偏移的 sigmoid 函数。在隐藏状态到隐藏状态的转换中使用双曲正切是常见的,因为它可以阻止其飞得太高或太低。对于其他目的,relu 更常见。
现在这是一个相当深的网络,因为它使用 8 个字符而不是 2 个。随着网络变得更深,它们变得更难训练。
m = CharLoopModel(vocab_size, n_fac).cuda() opt = optim.Adam(m.parameters(), 1e-2) fit(m, md, 1, opt, F.nll_loss) set_lrs(opt, 0.001) fit(m, md, 1, opt, F.nll_loss)
添加 vs.连接
现在我们将尝试为self.l_hidden(**h+inp**)
[1:46:04]尝试其他方法。原因是输入状态和隐藏状态在质上是不同的。输入是字符的编码,h 是一系列字符的编码。因此,将它们相加,我们可能会丢失信息。让我们改为连接它们。不要忘记更改输入以匹配形状(n_fac+n_hidden
而不是n_fac
)。
class CharLoopConcatModel(nn.Module): def __init__(self, vocab_size, n_fac): super().__init__() self.e = nn.Embedding(vocab_size, n_fac) self.l_in = nn.Linear(n_fac+n_hidden, n_hidden) self.l_hidden = nn.Linear(n_hidden, n_hidden) self.l_out = nn.Linear(n_hidden, vocab_size) def forward(self, *cs): bs = cs[0].size(0) h = V(torch.zeros(bs, n_hidden).cuda()) for c in cs: inp = torch.cat((h, self.e(c)), 1) inp = F.relu(self.l_in(inp)) h = F.tanh(self.l_hidden(inp)) return F.log_softmax(self.l_out(h), dim=-1)
这带来了一些改进。
使用 PyTorch 的 RNN[1:48:47]
PyTorch 将自动为我们编写for
循环,还会编写线性输入层。
class CharRnn(nn.Module): def __init__(self, vocab_size, n_fac): super().__init__() self.e = nn.Embedding(vocab_size, n_fac) **self.rnn = nn.RNN(n_fac, n_hidden)** self.l_out = nn.Linear(n_hidden, vocab_size) def forward(self, *cs): bs = cs[0].size(0) h = V(torch.zeros(1, bs, n_hidden)) inp = self.e(torch.stack(cs)) **outp,h = self.rnn(inp, h)** return F.log_softmax(self.l_out(**outp[-1]**), dim=-1)
- 出于以后会变得明显的原因,
self.rnn
将返回不仅输出,还有隐藏状态。 - PyTorch 中的一个微小差异是
self.rnn
会将一个新的隐藏状态附加到张量上,而不是替换(换句话说,它会在图表中返回所有省略号)。我们只想要最后一个,所以我们做outp[-1]
m = CharRnn(vocab_size, n_fac).cuda() opt = optim.Adam(m.parameters(), 1e-3) ht = V(torch.zeros(1, 512,n_hidden)) outp, hn = m.rnn(t, ht) outp.size(), hn.size() ''' (torch.Size([8, 512, 256]), torch.Size([1, 512, 256])) '''
在 PyTorch 版本中,隐藏状态是一个秩为 3 的张量h = V(torch.zeros(1, bs, n_hidden)
(在我们的版本中,它是秩为 2 的张量)[1:51:58]。我们以后会学到更多关于这个,但事实证明你可以有第二个向后运行的 RNN。这个想法是它会更好地找到向后的关系——它被称为“双向 RNN”。你也可以有一个 RNN 馈送到一个 RNN,这被称为“多层 RNN”。对于这些 RNN,你将需要张量中的额外轴来跟踪额外层的隐藏状态。现在,我们只有 1 个,然后返回 1 个。
测试模型
def get_next(inp): idxs = T(np.array([char_indices[c] for c in inp])) p = m(*VV(idxs)) i = np.argmax(to_np(p)) return chars[i] def get_next_n(inp, n): res = inp for i in range(n): c = get_next(inp) res += c inp = inp[1:]+c return res get_next_n('for thos', 40) ''' 'for those the same the same the same the same th' '''
这次,我们循环n
次,每次调用get_next
,每次我们将我们的输入替换为删除第一个字符并添加我们刚预测的字符。
对于有趣的作业,尝试编写自己的nn.RNN
“JeremysRNN
”,而不查看 PyTorch 源代码。
多输出[1:55:31]
从最后一个图表中,我们可以进一步简化,将字符 1 视为字符 2 到 n-1 相同。你会注意到三角形(输出)也移动到循环内部,换句话说,我们在每个字符之后创建一个预测。
使用字符 1 到 n-1 预测字符 2 到 n
我们可能想要这样做的原因之一是我们之前看到的冗余:
array([[40, 42, 29, 30, 25, 27, 29, 1], [42, 29, 30, 25, 27, 29, 1, 1], [29, 30, 25, 27, 29, 1, 1, 1], [30, 25, 27, 29, 1, 1, 1, 43], [25, 27, 29, 1, 1, 1, 43, 45], [27, 29, 1, 1, 1, 43, 45, 40], [29, 1, 1, 1, 43, 45, 40, 40], [ 1, 1, 1, 43, 45, 40, 40, 39]])
这次我们可以通过采用不重叠的字符集来使其更有效。因为我们正在进行多输出,对于输入字符 0 到 7,输出将是字符 1 到 8 的预测。
xs[:cs,:cs] ''' array([[40, 42, 29, 30, 25, 27, 29, 1], [ 1, 1, 43, 45, 40, 40, 39, 43], [33, 38, 31, 2, 73, 61, 54, 73], [ 2, 44, 71, 74, 73, 61, 2, 62], [72, 2, 54, 2, 76, 68, 66, 54], [67, 9, 9, 76, 61, 54, 73, 2], [73, 61, 58, 67, 24, 2, 33, 72], [ 2, 73, 61, 58, 71, 58, 2, 67]]) ''' ys[:cs,:cs] ''' array([[42, 29, 30, 25, 27, 29, 1, 1], [ 1, 43, 45, 40, 40, 39, 43, 33], [38, 31, 2, 73, 61, 54, 73, 2], [44, 71, 74, 73, 61, 2, 62, 72], [ 2, 54, 2, 76, 68, 66, 54, 67], [ 9, 9, 76, 61, 54, 73, 2, 73], [61, 58, 67, 24, 2, 33, 72, 2], [73, 61, 58, 71, 58, 2, 67, 68]]) '''
这不会使我们的模型更准确,但我们可以更有效地训练它。
class CharSeqRnn(nn.Module): def __init__(self, vocab_size, n_fac): super().__init__() self.e = nn.Embedding(vocab_size, n_fac) self.rnn = nn.RNN(n_fac, n_hidden) self.l_out = nn.Linear(n_hidden, vocab_size) def forward(self, *cs): bs = cs[0].size(0) h = V(torch.zeros(1, bs, n_hidden)) inp = self.e(torch.stack(cs)) outp,h = self.rnn(inp, h) return F.log_softmax(self.l_out(outp), dim=-1)
请注意,我们不再做outp[-1]
,因为我们想保留所有这些。但其他一切都是相同的。一个复杂性[2:00:37]是我们想要像以前一样使用负对数似然损失函数,但它期望两个秩为 2 的张量(两个矢量的小批量)。但在这里,我们有秩为 3 的张量:
- 8 个字符(时间步)
- 84 个概率
- 对于 512 个小批量
让我们编写一个自定义损失函数[2:02:10]:
def nll_loss_seq(inp, targ): sl,bs,nh = inp.size() targ = targ.transpose(0,1).contiguous().view(-1) return F.nll_loss(inp.view(-1,nh), targ)
F.nll_loss
是 PyTorch 的损失函数。- 展平我们的输入和目标。
- 转置前两个轴,因为 PyTorch 期望 1.序列长度(多少个时间步),2.批量大小,3.隐藏状态本身。
yt.size()
是 512 乘以 8,而sl, bs
是 8 乘以 512。 - 当你做像“transpose”这样的事情时,PyTorch 通常不会实际洗牌内存顺序,而是保留一些内部元数据来处理它,就好像它被转置了。当你转置一个矩阵时,PyTorch 只是更新元数据。如果你看到一个错误说“这个张量不连续”,在它后面加上
.contiguous()
,错误就会消失。 .view
与np.reshape
相同。-1
表示它需要多长。
fit(m, md, 4, opt, null_loss_seq)
记住fit(...)
是 fast.ai 实现训练循环的最低级抽象。因此,除了md
是包装测试集、训练集和验证集的模型数据对象之外,所有参数都是标准的 PyTorch 东西。
问题[2:06:04]: 现在我们在循环内部放了一个三角形,我们需要更大的序列大小吗?
- 如果我们有一个短序列像 8 这样,第一个字符没有任何依据。它从零开始的空隐藏状态。
- 我们将学习如何避免这个问题下周。
- 基本思想是“为什么我们每次都要将隐藏状态重置为零?”(见下面的代码)。如果我们可以以某种方式排列这些小批量,使得下一个小批量正确连接起来,代表尼采作品中的下一个字母,那么我们可以将
h = V(torch.zeros(1, bs, n_hidden))
移到构造函数中。
class CharSeqRnn(nn.Module): def __init__(self, vocab_size, n_fac): super().__init__() self.e = nn.Embedding(vocab_size, n_fac) self.rnn = nn.RNN(n_fac, n_hidden) self.l_out = nn.Linear(n_hidden, vocab_size) def forward(self, *cs): bs = cs[0].size(0) **h = V(torch.zeros(1, bs, n_hidden))** inp = self.e(torch.stack(cs)) outp,h = self.rnn(inp, h) return F.log_softmax(self.l_out(outp), dim=-1)
梯度爆炸 [2:08:21]
self.rnn(inp, h)
是一个循环,一遍又一遍地应用相同的矩阵乘法。如果那个矩阵乘法倾向于每次增加激活,我们实际上是将其乘以 8 次 — 我们称之为梯度爆炸。我们希望确保初始的l_hidden
不会导致我们的激活平均增加或减少。
一个很好的能做到这一点的矩阵被称为单位矩阵:
我们可以用单位矩阵覆盖随机初始化的隐藏-隐藏权重:
m.rnn.weight_hh_l0.data.copy_(torch.eye(n_hidden))
这是由 Geoffrey Hinton 等人在 2015 年介绍的(一种初始化修正线性单元循环网络的简单方法) — 在 RNN 存在几十年后。它效果非常好,你可以使用更高的学习率,因为它表现良好。
深度学习 2:第 1 部分第 7 课
原文:
medium.com/@hiromi_suenaga/deep-learning-2-part-1-lesson-7-1b9503aff0c
译者:飞龙
来自 fast.ai 课程的个人笔记。随着我继续复习课程以“真正”理解它,这些笔记将继续更新和改进。非常感谢 Jeremy 和Rachel 给了我这个学习的机会。
第 7 课
第 1 部分的主题是:
- 使用深度学习进行分类和回归
- 识别和学习最佳和已建立的实践
- 重点是分类和回归,即预测“一件事”(例如一个数字,少量标签)
课程的第 2 部分:
- 重点是生成建模,这意味着预测“很多事情” — 例如,在神经翻译中创建句子,图像字幕或问题回答,同时创建图像,例如风格转移,超分辨率,分割等等。
- 不是那么多的最佳实践,而是从最近的可能尚未完全测试的论文中更多的推测。
Char3Model 的回顾
提醒:RNN 在任何方面都不是不同或不寻常或神奇的 — 只是一个标准的全连接网络。
标准全连接网络
- 箭头代表一个或多个层操作 —— 一般来说是线性后跟一个非线性函数,本例中是矩阵乘法后跟
relu
或tanh
- 相同颜色的箭头表示使用完全相同的权重矩阵。
- 与以前的一个细微差别是第二层和第三层有输入进来。我们尝试了两种方法 —— 将这些输入连接或添加到当前激活中。
class Char3Model(nn.Module): def __init__(self, vocab_size, n_fac): super().__init__() self.e = nn.Embedding(vocab_size, n_fac) # The 'green arrow' from our diagram self.l_in = nn.Linear(n_fac, n_hidden) # The 'orange arrow' from our diagram self.l_hidden = nn.Linear(n_hidden, n_hidden) # The 'blue arrow' from our diagram self.l_out = nn.Linear(n_hidden, vocab_size) def forward(self, c1, c2, c3): in1 = F.relu(self.l_in(self.e(c1))) in2 = F.relu(self.l_in(self.e(c2))) in3 = F.relu(self.l_in(self.e(c3))) h = V(torch.zeros(in1.size()).cuda()) h = F.tanh(self.l_hidden(h+in1)) h = F.tanh(self.l_hidden(h+in2)) h = F.tanh(self.l_hidden(h+in3)) return F.log_softmax(self.l_out(h))
- 通过使用
nn.Linear
,我们免费获得了权重矩阵和偏置向量。 - 为了解决第一个椭圆中没有橙色箭头的问题,我们发明了一个空矩阵
class CharLoopModel(nn.Module): # This is an RNN! def __init__(self, vocab_size, n_fac): super().__init__() self.e = nn.Embedding(vocab_size, n_fac) self.l_in = nn.Linear(n_fac, n_hidden) self.l_hidden = nn.Linear(n_hidden, n_hidden) self.l_out = nn.Linear(n_hidden, vocab_size) def forward(self, *cs): bs = cs[0].size(0) h = V(torch.zeros(bs, n_hidden).cuda()) for c in cs: inp = F.relu(self.l_in(self.e(c))) h = F.tanh(self.l_hidden(h+inp)) return F.log_softmax(self.l_out(h), dim=-1)
- 几乎相同,除了
for
循环
class CharRnn(nn.Module): def __init__(self, vocab_size, n_fac): super().__init__() self.e = nn.Embedding(vocab_size, n_fac) self.rnn = nn.RNN(n_fac, n_hidden) self.l_out = nn.Linear(n_hidden, vocab_size) def forward(self, *cs): bs = cs[0].size(0) h = V(torch.zeros(1, bs, n_hidden)) inp = self.e(torch.stack(cs)) outp,h = self.rnn(inp, h) return F.log_softmax(self.l_out(outp[-1]), dim=-1)
- PyTorch 版本 —
nn.RNN
将创建循环并跟踪h
。 - 我们使用白色部分来预测绿色字符 —— 这似乎是浪费的,因为下一部分与当前部分大部分重叠。
- 然后我们尝试在多输出模型中将其分割为不重叠的部分:
- 在这种方法中,我们在处理每个部分后丢弃了我们的
h
激活,并开始了一个新的激活。为了在下一部分中使用第一个字符来预测第二个字符,它除了默认激活外没有其他信息。让我们不要丢弃h
。
有状态的 RNN
class CharSeqStatefulRnn(nn.Module): def __init__(self, vocab_size, n_fac, bs): self.vocab_size = vocab_size super().__init__() self.e = nn.Embedding(vocab_size, n_fac) self.rnn = nn.RNN(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))
- 构造函数中的一个额外行。
self.init_hidden(bs)
将self.h
设置为一堆零。 - 问题 #1 — 如果我们简单地执行
self.h = h
,并在一个包含一百万个字符的文档上进行训练,那么 RNN 的展开版本的大小将有一百万层(椭圆)。一百万层全连接网络将非常占用内存,因为为了进行链式规则,我们必须在每个批次中乘以一百万层,同时记住所有一百万个梯度。 - 为了避免这种情况,我们告诉它不时忘记它的历史。我们仍然可以记住状态(隐藏矩阵中的值)而不必记住如何到达那里的一切。
def repackage_var(h): return ( Variable(h.data) if type(h) == Variable else tuple(repackage_var(v) for v in h) )
- 从
Variable
h
中取出张量(记住,张量本身没有任何历史概念),并从中创建一个新的Variable
。新变量具有相同的值,但没有操作历史,因此当它尝试反向传播时,它将在那里停止。 forward
将处理 8 个字符,然后通过 8 个层进行反向传播,跟踪隐藏状态中的值,但会丢弃其操作历史。这被称为时间反向传播(bptt)。- 换句话说,在
for
循环之后,只需丢弃操作历史并重新开始。因此,我们保留了我们的隐藏状态,但没有保留我们的隐藏状态历史。 - 不要通过太多层进行反向传播的另一个很好的理由是,如果您有任何梯度不稳定性(例如,梯度爆炸或梯度消失),您拥有的层数越多,网络训练就越困难(速度更慢,弹性更差)。
- 另一方面,更长的
bptt
意味着您能够明确捕获更长的记忆和更多状态。 - 皱纹#2[16:00] - 如何创建小批量。我们不想一次处理一个部分,而是一次并行处理一堆。
- 当我们第一次开始研究 TorchText 时,我们谈到了它如何创建这些小批量。
- Jeremy 说我们拿一整个由尼采的全部作品或所有 IMDB 评论连接在一起的长文档,将其分成 64 个相等大小的块(不是大小为 64 的块)。
- 对于一个长度为 6400 万字符的文档,每个“块”将是 100 万个字符。我们将它们堆叠在一起,现在按
bptt
拆分它们 - 1 个小批次由 64 个bptt
矩阵组成。 - 第二块(第 100 万个字符)的第一个字符可能在一个句子的中间。但没关系,因为这只会在每一百万个字符中发生一次。
问题:这种数据集的数据增强?[20:34]
没有已知的好方法。最近有人通过进行数据增强赢得了一个 Kaggle 竞赛,随机插入不同行的部分 - 这样的方法可能在这里有用。但最近没有任何最先进的 NLP 论文在进行这种数据增强。
问题:我们如何选择 bptt 的大小?[21:36]
有几件事需要考虑:
- 第一点是小批量矩阵的大小为
bs
(块数)乘以bptt
,因此您的 GPU RAM 必须能够容纳嵌入矩阵。因此,如果您遇到 CUDA 内存不足错误,您需要减少其中一个。 - 如果您的训练不稳定(例如,您的损失突然飙升到 NaN),那么您可以尝试减少您的
bptt
,因为您的层较少,梯度不会爆炸。 - 如果速度太慢[22:44],尝试减少你的
bptt
,因为它会一次执行一个步骤。for
循环不能并行化(对于当前版本)。最近有一种叫做 QRNN(准循环神经网络)的东西,它可以并行化,我们希望在第二部分中介绍。 - 所以选择满足所有这些条件的最高数字。
有状态的 RNN 和 TorchText[23:23]
在使用期望数据符合特定格式的现有 API 时,您可以将数据更改为符合该格式,也可以编写自己的数据集子类来处理您的数据已经存在的格式。两者都可以,但在这种情况下,我们将把我们的数据放在 TorchText 已经支持的格式中。Fast.ai 对 TorchText 的包装器已经有了一些东西,您可以在每个路径中有一个训练路径和验证路径,并且每个路径中有一个或多个文本文件,其中包含一堆文本,这些文本被连接在一起用于您的语言模型。
from torchtext import vocab, data from fastai.nlp import * from fastai.lm_rnn import * PATH='data/nietzsche/' TRN_PATH = 'trn/' VAL_PATH = 'val/' TRN = f'{PATH}{TRN_PATH}' VAL = f'{PATH}{VAL_PATH}' %ls {PATH} ''' models/ nietzsche.txt trn/ val/ ''' %ls {PATH}trn ''' trn.txt '''
- 复制了尼采文件,粘贴到训练和验证目录中。然后从训练集中删除最后 20%的行,并删除验证集中除最后 20%之外的所有内容[25:15]。
- 这样做的另一个好处是,似乎更现实地拥有一个验证集,它不是文本行的随机洗牌集,而是完全独立于语料库的一部分。
- 当您进行语言模型时,您实际上不需要单独的文件。您可以有多个文件,但它们最终会被连接在一起。
TEXT = data.Field(lower=True, tokenize=list) bs=64; bptt=8; n_fac=42; n_hidden=256 FILES = dict(train=TRN_PATH, validation=VAL_PATH, test=VAL_PATH) md = LanguageModelData.from_text_files(PATH, TEXT, **FILES, bs=bs, bptt=bptt, min_freq=3) len(md.trn_dl), md.nt, len(md.trn_ds), len(md.trn_ds[0].text) ''' (963, 56, 1, 493747) '''
- 在 TorchText 中,我们创建了一个叫做
Field
的东西,最初Field
只是关于如何进行文本预处理的描述。 lower
- 我们告诉它将文本转换为小写tokenize
- 上次,我们使用了一个在空格上分割的函数,给我们一个单词模型。这次,我们想要一个字符模型,所以使用list
函数来对字符串进行标记化。记住,在 Python 中,list('abc')
将返回['a','b','c']
。bs
:批次大小,bptt
:我们将其重命名为cs
,n_fac
:嵌入的大小,n_hidden
:我们隐藏状态的大小- 我们没有单独的测试集,所以我们将只使用验证集进行测试
- TorchText 每次都会稍微随机化
bptt
的长度。它并不总是给我们确切的 8 个字符;有 5%的概率,它会将其减半并添加一个小的标准偏差,使其略大或略小于 8。我们不能对数据进行洗牌,因为它需要是连续的,所以这是引入一些随机性的一种方式。 - 问题:每个小批次的大小是否保持恒定?是的,我们需要用
h
权重矩阵进行矩阵乘法,因此小批次的大小必须保持恒定。但是序列长度可以改变,没有问题。 len(md.trn_dl)
:数据加载器的长度(即有多少个小批次),md.nt
:标记的数量(即词汇表中有多少个唯一的东西)- 一旦运行
LanguageModelData.from_text_files
,TEXT
将包含一个名为vocab
的额外属性。TEXT.vocab.itos
是词汇表中唯一项目的列表,TEXT.vocab.stoi
是从每个项目到数字的反向映射。
class CharSeqStatefulRnn(nn.Module): def __init__(self, vocab_size, n_fac, bs): self.vocab_size = vocab_size super().__init__() self.e = nn.Embedding(vocab_size, n_fac) self.rnn = nn.RNN(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))
- 问题 #3:Jeremy 在说小批次大小保持恒定时对我们撒谎了。最后一个小批次很可能比其他小批次短,除非数据集恰好可以被
bptt
乘以bs
整除。这就是为什么我们要检查self.h
的第二维是否与输入的bs
相同。如果不相同,将其设置回零,并使用输入的bs
。这发生在周期结束和周期开始时(将其设置回完整的批次大小)。 - 问题 #4:最后一个问题是关于 PyTorch 的一个小问题,也许有人可以友好地尝试通过 PR 来修复它。损失函数不喜欢接收一个三维张量(即三维数组)。它们不应该不喜欢接收一个三维张量(按序列长度、批次大小和结果计算损失 - 因此您可以为两个初始轴的每个计算损失)。对于二维或四维张量可以工作,但对于三维张量不行。
.view
将三维张量重塑为二维的-1
(必要时尽可能大)乘以vocab_size
。TorchText 自动将目标展平,因此我们不需要为实际值这样做(当我们在第 4 课看到一个小批次时,我们注意到它被展平了。Jeremy 说我们以后会了解原因,现在就是时候了)。- PyTorch(截至 0.3 版),
log_softmax
要求我们指定我们要对 softmax 进行的轴(即我们要将其求和为 1 的轴)。在这种情况下,我们希望在最后一个轴dim = -1
上进行。
m = CharSeqStatefulRnn(md.nt, n_fac, 512).cuda() opt = optim.Adam(m.parameters(), 1e-3) fit(m, md, 4, opt, F.nll_loss)
fast.ai 深度学习笔记(三)(3)https://developer.aliyun.com/article/1482898