上周回顾 [2:15]
我们上周深入研究了协同过滤,最后我们在 fast.ai 库中重新创建了EmbeddingDotBias
类(column_data.py
)。 让我们看一下嵌入的样子(笔记本)。
在学习器learn
内部,你可以通过调用learn.model
来获取 PyTorch 模型。 @property
看起来像常规函数,但在调用它时不需要括号。
@property
def model(self): return self.models.model
learn.models
是learn.models
的一个实例,它是PyTorch模型的一个薄包装器,它允许我们使用“层组”,这不是PyTorch中可用的概念,而fast.ai使用它将不同的学习率应用于不同的层集(层组)。
PyTorch模型很好地打印出层,包括层名,这就是我们在代码中称之为的层名。
m=learn.model; m
EmbeddingDotBias (
(u): Embedding(671, 50)
(i): Embedding(9066, 50)
(ub): Embedding(671, 1)
(ib): Embedding(9066, 1)
)
m.ib
是指项目偏差的嵌入层 - 在我们的例子中是电影偏见。 PyTorch模型和层的好处是我们可以将它们称为函数。 因此,如果你想获得预测,则调用m(...)
并传入变量。
层需要变量而不是张量,因为它需要跟踪导数 - 这就是V(...)
将张量转换为变量的原因。PyTorch 0.4 将摆脱变量,我们将能够直接使用张量。
movie_bias = to_np(m.ib(V(topMovieIdx)))
to_np
函数将接受变量或张量(无论是在 CPU 还是 GPU 上)并返回 numpy 数组。 Jeremy 的方法 [12:03] 是将 numpy 用于一切,除非他明确需要在 GPU 上运行某些东西或者需要它的导数 - 在这种情况下他使用 PyTorch。 Numpy 比 PyTorch 的使用时间更长,并且可以与 OpenCV,Pandas
等其他库一起使用。
有关生产中 CPU 与 GPU 的问题。 建议的方法是对 CPU 进行推理,因为它更具可扩展性,你无需批量生产。 你可以通过键入m.cpu()
将模型移动到 CPU 上,类似于对变量键入V(topMovieIndex).cpu()
(从 CPU 到 GPU 是m.cuda()
)。如果你的服务器没有 GPU ,它会自动在 CPU 上运行推理。 要加载在 GPU 上训练过的已保存模型,请查看torch_imports.py
以下代码torch_imports.py
:
def load_model(m, p): m.load_state_dict(torch.load(p, map_location=lambda storage, loc: storage))
现在我们对前 3000 部电影有了偏差,让我们来看看收视率:
movie_ratings = [(b[0], movie_names[i]) for i,b in zip(topMovies,movie_bias)]
zip
将允许你同时迭代多个列表。
最糟糕的电影
对于有序的键 - Python 有itemgetter
函数,但普通的lambda
只多了一个字符。
sorted(movie_ratings, key=lambda o: o[0])[:15]
'''
[(-0.96070349, 'Battlefield Earth (2000)'),
(-0.76858485, 'Speed 2: Cruise Control (1997)'),
(-0.73675376, 'Wild Wild West (1999)'),
(-0.73655486, 'Anaconda (1997)'),
...]
'''
sorted(movie_ratings, key=itemgetter(0))[:15]
最好的电影
sorted(movie_ratings, key=lambda o: o[0], reverse=True)[:15]
'''
[(1.3070084, 'Shawshank Redemption, The (1994)'),
(1.1196285, 'Godfather, The (1972)'),
(1.0844109, 'Usual Suspects, The (1995)'),
(0.96578616, "Schindler's List (1993)"),
...]
'''
嵌入的解释 [18:42]
每部电影有 50 个嵌入,很难看到 50 维空间,所以我们将它变成一个三维空间。 我们可以使用几种技术来压缩维度:主成分分析(PCA)(Rachel 的计算线性代数课程详细介绍了这一点 - 几乎与奇异值分解(SVD)相同)
movie_emb = to_np(m.i(V(topMovieIdx)))
movie_emb.shape
# (3000, 50)
from sklearn.decomposition import PCA
pca = PCA(n_components=3)
movie_pca = pca.fit(movie_emb.T).components_
movie_pca.shape
# (3, 3000)
我们将看看第一个维度“轻松与严肃”(我们不知道它代表什么但可以通过观察它们来推测):
fac0 = movie_pca[0]
movie_comp = [(f, movie_names[i]) for f,i in zip(fac0, topMovies)]
sorted(movie_comp, key=itemgetter(0), reverse=True)[:10]
sorted(movie_comp, key=itemgetter(0), reverse=True)[:10]
'''
[(0.06748189, 'Independence Day (a.k.a. ID4) (1996)'),
(0.061572548, 'Police Academy 4: Citizens on Patrol (1987)'),
(0.061050549, 'Waterworld (1995)'),
(0.057877172, 'Rocky V (1990)'),
...
]
'''
sorted(movie_comp, key=itemgetter(0))[:10]
'''
[(-0.078433245, 'Godfather: Part II, The (1974)'),
(-0.072180331, 'Fargo (1996)'),
(-0.071351372, 'Pulp Fiction (1994)'),
(-0.068537779, 'Goodfellas (1990)'),
...
]
'''
第二个维度“对话驱动与 CGI”
fac1 = movie_pca[1]
movie_comp = [(f, movie_names[i]) for f,i in zip(fac1, topMovies)]
sorted(movie_comp, key=itemgetter(0), reverse=True)[:10]
'''
[(0.058975246, 'Bonfire of the Vanities (1990)'),
(0.055992026, '2001: A Space Odyssey (1968)'),
(0.054682467, 'Tank Girl (1995)'),
(0.054429606, 'Purple Rose of Cairo, The (1985)'),
...]
'''
sorted(movie_comp, key=itemgetter(0))[:10]
'''
[(-0.1064609, 'Lord of the Rings: The Return of the King, The (2003)'),
(-0.090635143, 'Aladdin (1992)'),
(-0.089208141, 'Star Wars: Episode V - The Empire Strikes Back (1980)'),
(-0.088854566, 'Star Wars: Episode IV - A New Hope (1977)'),
...]
'''
情节
idxs = np.random.choice(len(topMovies), 50, replace=False)
X = fac0[idxs]
Y = fac1[idxs]
plt.figure(figsize=(15,15))
plt.scatter(X, Y)
for i, x, y in zip(topMovies[idxs], X, Y):
plt.text(x,y,movie_names[i], color=np.random.rand(3)*0.7, fontsize=11)
plt.show()
当你说learn.fit
时会发生什么?
类别变量的实体嵌入 [24:42]
第二篇论文谈论类别嵌入。 图一的标题应该听起来很熟悉,因为它们讨论了实体嵌入层如何等效于单热编码,然后是矩阵乘法。
他们做的有趣的事情是,他们采用由神经网络训练的实体嵌入,用学习的实体嵌入替换每个类别变量,然后将其输入到梯度增强机(GBM),随机森林(RF)和 KNN 中 - 这减少了某些误差,几乎与神经网络(NN)一样好。 这是一种很好的方式,可以在你的组织中提供神经网络的强大功能,而不必强迫其他人学习深度学习,因为他们可以继续使用他们当前使用的东西并使用嵌入作为输入。 GBM 和 RF 的训练比 NN 快得多。
他们还绘制了德国的州的嵌入,有趣的是(正如 Jeremy 所说的那样“令人费解”)类似于实际的地图。
他们还绘制了物理空间和嵌入空间中商店的距离 - 这显示出美丽而清晰的相关性。
一周的天数或一年中的几个月之间似乎也存在相关性。 可视化嵌入可能很有趣,因为它向你显示你期望看到的内容或你未看到的内容。
关于 Skip-Gram 生成嵌入的问题 [31:31]
Skip-Gram 特定于 NLP。 将未标记的问题转变为标记问题的好方法是“发明”标签。 Word2Vec 的方法是选取 11 个单词的句子,删除中间单词,并用随机单词替换它。 然后他们将标签 1 给原句,标签 0 给假的句子,并建立了一个机器学习模型来查找假的句子。 因此,他们现在可以将嵌入用于其他目的。 如果你将它实现为单个矩阵乘法(浅模型)而不是深度神经网络,你可以非常快速地训练 - 缺点是它是一个预测性较低的模型,但优点是你可以训练一个非常大的数据集,更重要的是,最终的嵌入具有线性特征 ,允许我们很好地加,减或绘制。 在 NLP 中,我们应该超越 Word2Vec 和 Glove(即基于线性的方法),因为这些嵌入不太具有预测性。 最先进的语言模型使用深度 RNN。
要学习任何类型的特征空间,你需要标记数据或者需要发明虚假任务 [35:45]
- 一个虚假任务比另一个好吗? 还没有很好的研究。
- 直观地说,我们想要一个任务,帮助机器学习你关心的各种关系。
- 在计算机视觉中,人们使用的一种虚假任务是应用虚幻和不合理的数据增强。
- 如果你不能提出很棒的虚假任务,那就去使用糟糕的任务 - 你需要的很少,这通常是令人惊讶的。
- 自编码器 [38:10] - 它最近赢得了保险索赔竞赛。 采取单一策略,通过神经网络运行,并让它重建自己(确保中间层的激活少于输入变量)。 基本上,这是一个任务,其输入等于输出,作为一个假任务它有效,令人惊讶。
在计算机视觉中,你可以在猫狗上训练并将其用于 CT 扫描。 也许它可能适用于语言/ NLP! (未来的研究)
Rossmann [41:04]
- 正确使用测试集的方法已添加到笔记本中。
- 对于更详细的说明,请参阅机器学习课程。
apply_cats(joined_test, joined)
用于确保测试集和训练集具有相同的类别编号。- 跟踪包含每个连续列的平均值和标准差的
mapper
,并将相同的mapper
应用于测试集。 - 不要依赖 Kaggle 公共版 - 依靠你自己精心设计的验证集。
为 Rossmann 寻找一个好的核心
- 周日对销售的影响
商店关闭前后的销售额有所增长。 第三名获胜者在开始任何分析之前删除了关闭的商店的行。
不要触碰你的数据,除非你首先分析来看看做什么好 - 没有假设。
Vim技巧 [49:12]
:tag ColumnarModelData
访问类定义ctrl + ]
访问光标下内容的定义ctrl + t
返回*
找到光标下的内容的用法- 你可以使用
:tabn
和:tabp
在选项卡之间切换,使用:tabe <filepath>
可以添加新选项卡;并使用常规:q
或:wq
关闭一个标签。 如果将:tabn
和:tabp
映射到F7 / F8
键,则可以轻松地在文件之间切换。
ColumnarModelData内部 [51:01]
慢慢地,但是必然,过去只是“神奇”的东西,开始看起来很熟悉。 如你所见, get_learner
返回Learner
,它是包装数据和 PyTorch 模型的 fast.ai 概念:
在MixedInputModel
内部,你可以看到,它如何创建我们现在更了解的Embedding。
nn.ModuleList用于注册层的列表。 我们将在下周讨论
BatchNorm`,但我们之前已经看过了其余部分。
同样,我们现在了解forward
函数发生了什么。
- 使用第
i
个类别变量调用嵌入层并将它们连接在一起 - 将其通过 Dropout
- 浏览每个线性层,调用它,应用 relu 和 dropout
- 然后最终线性层的大小为 1
- 如果
y_range
,则应用 sigmoid 并将输出拟合到一个范围内(我们上周学到的)
随机梯度下降 - SGD [59:56]
为了确保我们完全掌握 SGD,我们将用它来学习y = ax + b
。 如果我们可以用 2 个参数解决问题,我们可以使用相同的技巧来解决 1 亿个参数。
# Here we generate some fake data
def lin(a,b,x): return a*x+b
def gen_fake_data(n, a, b):
x = s = np.random.uniform(0,1,n)
y = lin(a,b,x) + 0.1 * np.random.normal(0,3,n)
return x, y
x, y = gen_fake_data(50, 3., 8.)
plt.scatter(x,y, s=8); plt.xlabel("x"); plt.ylabel("y");
首先,我们需要一个损失函数。 这是一个回归问题,因为输出是连续输出,最常见的损失函数是均方误差(MSE)。
回归 - 目标输出是实数或整数实数
分类 - 目标输出是类标签
def mse(y_hat, y): return ((y_hat - y) ** 2).mean()
def mse_loss(a, b, x, y): return mse(lin(a,b,x), y)
y_hat
- 预测
我们将制作 10,000 多个假数据并将它们转换为 PyTorch 变量,因为 Jeremy 不喜欢计算导数,而 PyTorch 可以为他做到:
x, y = gen_fake_data(10000, 3., 8.)
x,y = V(x),V(y)
然后为a
和b
创建随机权重,它们是我们想要学习的变量,所以设置requires_grad=True
。
a = V(np.random.randn(1), requires_grad=True)
b = V(np.random.randn(1), requires_grad=True)
然后设置学习率并完成 10000 个全量梯度下降的迭代(不是 SGD,因为每个迭代将查看所有数据):
learning_rate = 1e-3
for t in range(10000):
# Forward pass: compute predicted y using operations on Variables
loss = mse_loss(a,b,x,y)
if t % 1000 == 0: print(loss.data[0])
# Computes the gradient of loss with respect to all Variables with requires_grad=True.
# After this call a.grad and b.grad will be Variables holding the gradient
# of the loss with respect to a and b respectively
loss.backward()
# Update a and b using gradient descent; a.data and b.data are Tensors,
# a.grad and b.grad are Variables and a.grad.data and b.grad.data are Tensors
a.data -= learning_rate * a.grad.data
b.data -= learning_rate * b.grad.data
# Zero the gradients
a.grad.data.zero_()
b.grad.data.zero_()
- 计算损失(记住,
a
和b
最初设置为随机) - 偶尔(每 1000 个迭代),打印出损失
loss.backward()
将使用requires_grad=True
计算所有变量的梯度,并填写.grad
属性- 将
a
更新来减去LR * grad
(.data
访问变量内部的张量) - 当有多个损失函数,或许多输出层对梯度有贡献时,PyTorch 会将它们加在一起。 因此,你需要告诉何时将梯度设置回零(
_
中的zero_()
表示变量原地更改)。 - 最后 4 行代码包含在
optim.SGD.step
函数中
让我们只用 Numpy(没有 PyTorch) [1:07:01]
我们实际上必须做微分,但其他一切看起来应该相似:
x, y = gen_fake_data(50, 3., 8.)
a_guess,b_guess = -1., 1.
mse_loss(y, a_guess, b_guess, x)
lr=0.01
def upd():
global a_guess, b_guess
y_pred = lin(a_guess, b_guess, x)
dydb = 2 * (y_pred - y)
dyda = x*dydb
a_guess -= lr*dyda.mean()
b_guess -= lr*dydb.mean()
只是为了好玩,你可以使用matplotlib.animation.FuncAnimation
来制作动画:
提示:Fast.ai AMI 没有附带ffmpeg
。所以如果你看到KeyError: 'ffmpeg'
- 运行
print(animation.writers.list())
并打印出可用的MovieWriters
列表 - 如果
ffmpeg
不在其中,就安装它 。
循环神经网络 - RNN [1:09:16]
让我们学习如何写尼采这样的哲学。 这类似于我们在第 4 课中学到的语言模型,但这一次,我们将一次完成一个字符。 RNN 与我们已经学到的没什么不同。
一些例子:
具有单个隐藏层的基本 NN
所有形状都是激活(激活是由 relu,矩阵乘法等计算的数字)。 箭头是层操作(可能不止一个)。 查看机器学习课程 9-11,从头开始创建。
具有单密集隐层的图像 CNN
我们将在下周介绍,如何把层展开,但主要方法称为“自适应最大池” - 我们在高度和宽度上进行平均,并将其转换为向量。
batch_size
维度和激活函数(例如 relu,softmax)未在此处显示
使用字符 1 和 2 预测字符 3 [1:18:04]
我们将为 NLP 实现这个。
- 输入可以是单热编码字符(向量的长度等于唯一字符的数量),或单个整数,并假设它由嵌入层进行单热编码。
- 与 CNN 的不同之处在于添加了字符 2 输入。
层操作未显示;记住箭头代表层操作
让我们在没有torchtext
或 fast.ai 库的情况下来实现,以便我们可以看到。
set
将返回所有唯一字符。
text = open(f'{PATH}nietzsche.txt').read()
print(text[:400])
'PREFACE\n\n\nSUPPOSING that Truth is a woman--what then? Is there not ground\nfor suspecting that all philosophers, in so far as they have been\ndogmatists, have failed to understand women--that the terrible\nseriousness and clumsy importunity with which they have usually paid\ntheir addresses to Truth, have been unskilled and unseemly methods for\nwinning a woman? Certainly she has never allowed herself '
chars = sorted(list(set(text)))
vocab_size = len(chars)+1
print('total chars:', vocab_size)
# total chars: 85
- 添加
null
或空字符来填充,总是很好。
chars.insert(0, "\0")
将每个字符映射到唯一 ID,以及唯一 ID 到字符。
char_indices = dict((c, i) for i, c in enumerate(chars))
indices_char = dict((i, c) for i, c in enumerate(chars))
现在我们可以使用其 ID 来表示文本:
idx = [char_indices[c] for c in text]
idx[:10]
# [40, 42, 29, 30, 25, 27, 29, 1, 1, 1]
问题:基于字符的模型与基于单词的模型 [1:22:30]
- 通常,你希望将字符级模型和单词级模型组合在一起(例如,用于翻译)。
- 当词汇表包含不常用的单词时,字符级模型很有用 - 单词级模型将仅视为“未知”。 当你看到之前没有见过的单词时,可以使用字符级模型。
- 在它们之间还有一种称为字节对编码(BPE)的东西,它查看 n-gram 字符。
创建输入 [1:23:48]
cs = 3
c1_dat = [idx[i] for i in range(0, len(idx)-cs, cs)]
c2_dat = [idx[i+1] for i in range(0, len(idx)-cs, cs)]
c3_dat = [idx[i+2] for i in range(0, len(idx)-cs, cs)]
c4_dat = [idx[i+3] for i in range(0, len(idx)-cs, cs)]
注意c1_dat[n+1] == c4_dat[n]
因为我们步长为 3( range
的第三个参数)
x1 = np.stack(c1_dat)
x2 = np.stack(c2_dat)
x3 = np.stack(c3_dat)
y = np.stack(c4_dat)
x
是我们的输入,y
是我们的目标值。
建立模型 [1:26:08]
n_hidden = 256
n_fac = 42
n_hiddin
- 图中的“n_hiddin
”。n_fac
- 嵌入矩阵的大小。
这是上图的更新版本。 请注意,现在箭头已着色。 具有相同颜色的所有箭头,将使用相同的权重矩阵。 这里的想法是,字符不具有不同的含义(语义上或概念上),这取决于它是序列中的第一个,第二个还是第三个项目,因此对它们的处理方式相同。
class Char3Model(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)
self.l_hidden = nn.Linear(n_hidden, n_hidden)
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))
- [1:29:58] 重要的是,这个
l_hidden
使用一个方形权重矩阵,其大小匹配l_in
的输出。 然后h
和in2
将是相同的形状,允许我们在self.l_hidden(h+in2)
看到它们的总和 V(torch.zeros(in1.size()).cuda())
只是使三行相同,以便以后更容易放入for
循环。
md = ColumnarModelData.from_arrays('.', [-1], np.stack([x1,x2,x3], axis=1), y, bs=512)
我们将复用ColumnarModelData
[1:32:20]。如果我们堆叠x1
, x2
和x3
,我们将在forward
方法中得到c1
,c2
,c3
。当你想用原始方法训练模型时,
ColumnarModelData.from_arrays
会派上用场,你放入[x1, x2, x3]
,你将在def forward(self, c1, c2, c3)
取回来。
m = Char3Model(vocab_size, n_fac).cuda()
- 我们创建了一个标准的 PyTorch 模型(不是
Learner
) - 因为它是标准的 PyTorch 模型,所以不要忘记
.cuda
it = iter(md.trn_dl)
*xs,yt = next(it)
t = m(*V(xs)
iter
返回了一个迭代器next
返回一个小批量- 把
xs
张量变成“变量”,并使其通过模型 - 这将给我们 512x85 张量,它包含预测(批量大小乘唯一字符)
opt = optim.Adam(m.parameters(), 1e-2)
- 创建一个标准的 PyTorch 优化器 - 你需要传递一个要优化的东西列表,由
m.parameters()
返回
fit(m, md, 1, opt, F.nll_loss)
set_lrs(opt, 0.001)
fit(m, md, 1, opt, F.nll_loss)
- 我们没有找到学习率查找器和 SGDR,因为我们没有使用
Learner
,所以我们需要手动进行学习率退货(将 LR 设置得稍低)
测试模型 [1:35:58]
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]
此函数需要三个字符并返回模型预测的第四个字符。注意:np.argmax
返回最大值的索引。
get_next('y. ')
# 'T'
get_next('ppl')
# 'e'
get_next(' th')
# 'e'
get_next('and')
# ' '
让我们创建我们的第一个 RNN [1:37:45]
我们可以简化上面的图表如下:
使用字符 1 到 n-1 预测字符
让我们实现它。 这次,我们将使用前 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。 通常在隐藏状态中使用 tanh 来隐藏状态转换,因为它会阻止它飞得太高或太低。出于其他目的,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)
添加与连接
我们现在将为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)
这提供了一些改进。
RNT 与 PyTorch [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))
(在我们的版本中,它是二阶张量) [1:51:58]。 我们稍后会详细了解它,但事实证明你可以拥有反向的第二个 RNN。 我们的想法是找到反向的关系会更好 - 它被称为“双向 RNN”。 你也可以向 RNN 提供 RNN
的输出,称为“多层 RNN”。 对于这些 RNN,你将需要张量中的额外轴,来跟踪其他层的隐藏状态。 现在,我们只有一层。
测试模型
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] 是,我们想要像以前一样使用负对数似然损失函数,但它期望两个二阶张量(两个小批量向量)。 但在这里,我们有三阶张量:
- 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 期望第一维:序列长度(多少时间步长),第二维:批量大小,第三位:隐藏状态本身。
yt.size()
是 512 乘 8,而sl, bs
是 8 乘 512。 - 当你执行“转置”之类的操作时,PyTorch 通常不会实际调整内存顺序,而是保留一些内部元数据来将其视为转置。当你转置矩阵时,PyTorch 只会更新元数据。如果你看到错误“此张量不连续”,请在其后添加
.contiguous()
,错误就消失了。 .view
与np.reshape
相同。-1
表示应该是的东西。
fit(m, md, 4, opt, null_loss_seq)
请记住, fit(...)
是实现训练循环的最低级别 fast.ai抽象。 所以所有参数都是标准的 PyTorch,除了md
,它是我们的模型数据对象,它包装了测试集,训练集和验证集。
问题 [2:06:04]:既然我们在循环中放了一个三角形,我们需要更大的序列大小吗?
- 如果我们有一个像 8 这样的短序列,那么第一个字符就没有可依照的东西。它以空的隐藏状态来开始。
- 我们将在下周学习如何避免这个问题。
- 基本思想是“为什么我们每次都要将隐藏状态重置为零?”(参见下面的代码)。 如果我们能够以某种方式排列这些小批量,以便下一个小批量正确连接,来表示 Nietsche 作品中的下一个字母,那么我们可以将
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 年引入(初始化 ReLU 循环网络的一种简便方法) — 在 RNN 已经存在了几十年之后。 它运作良好,你可以使用更高的学习率,因为它表现良好。