Py学习  »  机器学习算法

fast.ai 深度学习笔记:第一部分第六课

ApacheCN_飞龙 • 5 年前 • 579 次点击  

2017 年深度学习优化的重点

上周回顾 [2:15]

我们上周深入研究了协同过滤,最后我们在 fast.ai 库中重新创建了EmbeddingDotBias类(column_data.py)。 让我们看一下嵌入的样子(笔记本)。

在学习器learn内部,你可以通过调用learn.model来获取 PyTorch 模型。 @property看起来像常规函数,但在调用它时不需要括号。

@property
def model(self): return self.models.model

learn.modelslearn.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)

然后为ab创建随机权重,它们是我们想要学习的变量,所以设置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_()

  • 计算损失(记住,ab最初设置为随机)
  • 偶尔(每 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:27:57]

  • [1:29:58] 重要的是,这个l_hidden使用一个方形权重矩阵,其大小匹配l_in的输出。 然后hin2将是相同的形状,允许我们在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]。如果我们堆叠x1x2x3,我们将在forward方法中得到c1c2c3。当你想用原始方法训练模型时, 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(),错误就消失了。
  • .viewnp.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 已经存在了几十年之后。 它运作良好,你可以使用更高的学习率,因为它表现良好。


今天看啥 - 高品质阅读平台
本文地址:http://www.jintiankansha.me/t/4xKC1bJt7d
Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/26068
 
579 次点击