fast.ai 深度学习笔记(三)

2024-02-11 08:49:08 浏览数 (1)

深度学习 2:第 1 部分第 6 课

原文:medium.com/@hiromi_suenaga/deep-learning-2-part-1-lesson-6-de70d626976c 译者:飞龙 协议:CC BY-NC-SA 4.0

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

第 6 课

[## 2017 年深度学习优化的亮点

目录:深度学习最终是关于找到一个很好泛化的最小值–附加分为…

ruder.io](http://ruder.io/deep-learning-optimization-2017/index.html?source=post_page-----de70d626976c--------------------------------)

上周的回顾[2:15]

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

在一个学习者learn内部,通过调用learn.model可以获得一个 PyTorch 模型本身。@property看起来像一个普通的函数,但当你调用它时不需要括号。

代码语言:javascript复制
@property
def model(self): 
    return self.models.model

learn.modelsCollabFilterModel的一个实例,它是 PyTorch 模型的一个薄包装,允许我们使用“层组”,这是 PyTorch 中没有的概念,fast.ai 使用它来对不同的层组应用不同的学习率。

PyTorch 模型很好地打印出层,包括层名称,这就是我们在代码中称呼它们的方式。

代码语言:javascript复制
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 将摆脱变量,我们将能够直接使用张量。

代码语言:javascript复制
movie_bias = to_np(m.ib(V(topMovieIdx)))

to_np函数将获取一个变量或张量(无论是在 CPU 还是 GPU 上)并返回一个 numpy 数组。Jeremy 的方法[12:03]是除了在他明确需要在 GPU 上运行的时候或者需要它的导数时使用 PyTorch 外,其他情况下都使用 numpy。Numpy 比 PyTorch 存在的时间更长,与其他库如 OpenCV、Pandas 等很好地配合。

关于生产中的 CPU vs. GPU 的问题。建议的方法是在 CPU 上进行推断,因为它更具可扩展性,而且你不需要将事物放入批处理中。你可以通过键入m.cpu()将模型移动到 CPU 上,类似地,通过键入V(topMovieIndex).cpu()将变量移动到 CPU 上(从 CPU 到 GPU 的操作是m.cuda())。如果你的服务器没有 GPU,它将自动在 CPU 上运行推断。要加载在 GPU 上训练的保存模型,请查看torch_imports.py中的这行代码:

代码语言:javascript复制
def load_model(m, p): 
    m.load_state_dict(torch.load(p, map_location=lambda storage, loc: storage))

现在我们有了前 3000 部电影的电影偏差,让我们来看一下评分:

代码语言:javascript复制
movie_ratings = [(b[0], movie_names[i]) for i,b in zip(topMovies,movie_bias)]

zip将允许你同时迭代多个列表。

最差的电影

关于排序键–Python 有itemgetter函数,但普通的lambda只是多了一个字符。

代码语言:javascript复制
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]

最佳电影

代码语言:javascript复制
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)相同)

代码语言:javascript复制
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)
'''

我们将看一下第一个维度“轻松观看 vs. 严肃”(我们不知道它代表什么,但可以通过观察来推测):

代码语言:javascript复制
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)'),
 ...
]
'''

第二个维度“对话驱动 vs. CGI”

代码语言:javascript复制
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)'),
 ...]
'''

绘图

代码语言:javascript复制
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]

第二篇论文讨论了分类嵌入。图 1 的标题应该听起来很熟悉,因为它们讨论了实体嵌入层等效于一个独热编码后跟着一个矩阵乘法。

他们做的有趣的事情是,他们用神经网络训练的实体嵌入替换了每个分类变量,然后将其输入到梯度提升机(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]

  • 一个虚假任务比另一个更好吗?尚未研究清楚。
  • 直觉上,我们希望有一个任务可以帮助机器学习你关心的关系类型。
  • 在计算机视觉中,人们使用一种虚假任务的类型是应用不真实和不合理的数据增强。
  • 如果你想不出很好的虚假任务,只需使用糟糕的任务——令人惊讶的是你需要的很少。
  • 自动编码器 - 它最近赢得了一场保险索赔竞赛。拿一个单一的政策,通过神经网络运行它,并让它重建自己(确保中间层的激活少于输入变量)。基本上,这是一个输入=输出的任务,作为一个虚假任务效果惊人。

在计算机视觉中,您可以训练猫和狗,并将其用于 CT 扫描。也许它对语言/NLP 也有效!(未来研究)

Rossmann

  • 笔记本中添加了正确使用测试集的方法。
  • 有关更详细的解释,请参见机器学习课程。
  • apply_cats(joined_test, joined) 用于确保测试集和训练集具有相同的分类代码。
  • 跟踪包含每个连续列的均值和标准差的mapper,并将相同的mapper应用于测试集。
  • 不要依赖 Kaggle 公共板块 - 依赖您自己精心创建的验证集。

查看 Rossmann 的一个好的Kernel

  • 周日对销售的影响

在店铺关闭前后销售有所增长。第三名获奖者在开始任何分析之前删除了关闭的店铺行。

除非您首先分析以确保您所做的是正确的 - 不要触碰您的数据。

Vim 技巧

  • :tag ColumnarModelData将带您到类定义处
  • ctrl ]将带您到光标下的定义
  • ctrl t返回
  • *查找光标下的内容的用法
  • 您可以使用:tabn:tabp在选项卡之间切换,使用:tabe <filepath>可以添加一个新选项卡;使用常规的:q:wq关闭一个选项卡。如果将:tabn:tabp映射到 F7/F8 键,您可以轻松地在文件之间切换。

在 ColumnarModelData 内部

慢慢地,曾经只是“魔术”的东西开始变得熟悉起来。正如您所看到的,get_learner返回Learner,这是 fast.ai 概念,它包装了数据和 PyTorch 模型:

MixedInputModel内部,您可以看到它是如何创建我们现在更多了解的Embedding的。nn.ModuleList用于注册一系列层。我们将在下周讨论BatchNorm,但是其他部分,我们之前已经见过。

同样,我们现在了解了forward函数中发生的事情。

  • 使用第i个分类变量调用嵌入层,并将它们全部连接在一起
  • 通过 dropout 处理
  • 逐个遍历我们的线性层,称之为,应用 relu 和 dropout
  • 然后最终的线性层大小为 1
  • 如果传入y_range,则应用 sigmoid 并将输出拟合在一个范围内(我们上周学到的)

随机梯度下降 - SGD

为了确保我们完全熟悉 SGD,我们将使用它来学习*y = ax b*。如果我们可以用 2 个参数解决问题,我们可以使用相同的技术来解决 1 亿个参数。

代码语言:javascript复制
# 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)。

回归 - 目标输出是一个实数或一整个实数向量 分类 - 目标输出是一个类标签

代码语言:javascript复制
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 可以为他做到这一点:

代码语言:javascript复制
x, y = gen_fake_data(10000, 3., 8.) 
x,y = V(x),V(y)

然后为ab创建随机权重,它们是我们想要学习的变量,因此设置requires_grad=True

代码语言:javascript复制
a = V(np.random.randn(1), requires_grad=True) 
b = V(np.random.randn(1), requires_grad=True)

然后设置学习率,并进行 10000 个完全梯度下降的周期(不是 SGD,因为每个周期将查看所有数据):

代码语言:javascript复制
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]

我们实际上需要做微积分,但其他方面应该看起来类似:

代码语言:javascript复制
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 与我们已经学过的内容没有区别。

一些例子:

  • SwiftKey
  • Andrej Karpathy LaTex 生成器

具有单隐藏层的基本 NN

所有形状都是激活(激活是通过 relu、矩阵乘积等计算得到的数字)。箭头是层操作(可能不止一个)。查看机器学习课程第 9-11 课,从头开始创建这个。

具有单个密集隐藏层的图像 CNN

我们将在下周更详细地介绍如何展平一个层,但主要方法被称为“自适应最大池化”——在高度和宽度上取平均值,将其转换为向量。

这里没有显示batch_size维度和激活函数(例如 relu,softmax)

使用字符 1 和 2 预测字符 3[1:18:04]

我们将为 NLP 实现这个。

  • 输入可以是一个独热编码字符(向量长度=唯一字符数)或一个整数,并通过使用嵌入层假装它是独热编码。
  • 与 CNN 的区别在于然后 char 2 输入被添加。

未显示层操作;记住箭头代表层操作

让我们实现这个,没有 torchtext 或 fast.ai 库,这样我们就可以看到。

  • set将返回所有唯一字符。
代码语言:javascript复制
text = open(f'{PATH}nietzsche.txt').read()
print(text[:400])
'''
'PREFACEnnnSUPPOSING that Truth is a woman--what then? Is there not groundnfor suspecting that all philosophers, in so far as they have beenndogmatists, have failed to understand women--that the terriblenseriousness and clumsy importunity with which they have usually paidntheir addresses to Truth, have been unskilled and unseemly methods fornwinning 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
'''
  • 总是好的为填充放置一个空字符或空字符。
代码语言:javascript复制
chars.insert(0, "")

将每个字符映射到唯一 ID,并将唯一 ID 映射到字符

代码语言:javascript复制
char_indices = dict((c, i) for i, c in enumerate(chars))
indices_char = dict((i, c) for i, c in enumerate(chars))

现在我们可以用它的 ID 来表示文本:

代码语言:javascript复制
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]

代码语言:javascript复制
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的第三个参数)

代码语言:javascript复制
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]

代码语言:javascript复制
n_hidden = 256 
n_fac = 42
  • n_hiddein-图表中的“#激活”。
  • n_fac-嵌入矩阵的大小。

这是上一个图表的更新版本。请注意,现在箭头是彩色的。所有具有相同颜色的箭头将使用相同的权重矩阵。这里的想法是,一个字符不会根据它在序列中是第一个、第二个还是第三个项目而具有不同的含义(语义上或概念上),因此将它们视为相同。

代码语言:javascript复制
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())只是为了使这三行相同,以便稍后更容易放入循环中。
代码语言:javascript复制
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)中返回。

代码语言:javascript复制
m = Char3Model(vocab_size, n_fac).cuda()
  • 我们创建一个标准的 PyTorch 模型(不是Learner
  • 因为它是一个标准的 PyTorch 模型,不要忘记.cuda
代码语言:javascript复制
it = iter(md.trn_dl)
*xs,yt = next(it)
t = m(*V(xs))
  • iter来获取一个迭代器
  • next返回一个小批量
  • “变量化”xs张量,并将其通过模型-这将给我们一个包含预测的 512x85 张量(批量大小*独特字符)
代码语言:javascript复制
opt = optim.Adam(m.parameters(), 1e-2)
  • 创建一个标准的 PyTorch 优化器-需要传入一个要优化的列表,该列表由m.parameters()返回
代码语言:javascript复制
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]

代码语言:javascript复制
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返回最大值的索引。

代码语言:javascript复制
get_next('y. ')
'''
'T'
'''
get_next('ppl')
'''
'e'
'''
get_next(' th')
'''
'e'
'''
get_next('and')
'''
' '
'''

让我们创建我们的第一个 RNN[1:37:45]

我们可以简化上一个图表如下:

使用 1 到 n-1 个字符预测第 n 个字符

让我们实现这个。这次,我们将使用前 8 个字符来预测第 9 个。这是如何创建输入和输出的,就像上次一样:

代码语言:javascript复制
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)。

代码语言:javascript复制
val_idx = get_cv_idxs(len(idx)-cs-1)
md = ColumnarModelData.from_arrays('.', val_idx, xs, y, bs=512)

创建模型[1:43:03]

代码语言:javascript复制
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 个。随着网络变得更深,它们变得更难训练。

代码语言:javascript复制
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)。

代码语言:javascript复制
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循环,还会编写线性输入层。

代码语言:javascript复制
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]
代码语言:javascript复制
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 个。

测试模型

代码语言:javascript复制
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.RNNJeremysRNN”,而不查看 PyTorch 源代码。

多输出[1:55:31]

从最后一个图表中,我们可以进一步简化,将字符 1 视为字符 2 到 n-1 相同。你会注意到三角形(输出)也移动到循环内部,换句话说,我们在每个字符之后创建一个预测。

使用字符 1 到 n-1 预测字符 2 到 n

我们可能想要这样做的原因之一是我们之前看到的冗余:

代码语言:javascript复制
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 的预测。

代码语言:javascript复制
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]])
'''

这不会使我们的模型更准确,但我们可以更有效地训练它。

代码语言:javascript复制
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]:

代码语言:javascript复制
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(),错误就会消失。
  • .viewnp.reshape相同。-1表示它需要多长。
代码语言:javascript复制
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))移到构造函数中。
代码语言:javascript复制
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不会导致我们的激活平均增加或减少。

一个很好的能做到这一点的矩阵被称为单位矩阵:

我们可以用单位矩阵覆盖随机初始化的隐藏-隐藏权重:

代码语言:javascript复制
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 译者:飞龙 协议:CC BY-NC-SA 4.0

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

第 7 课

第 1 部分的主题是:

  • 使用深度学习进行分类和回归
  • 识别和学习最佳和已建立的实践
  • 重点是分类和回归,即预测“一件事”(例如一个数字,少量标签)

课程的第 2 部分:

  • 重点是生成建模,这意味着预测“很多事情” — 例如,在神经翻译中创建句子,图像字幕或问题回答,同时创建图像,例如风格转移,超分辨率,分割等等。
  • 不是那么多的最佳实践,而是从最近的可能尚未完全测试的论文中更多的推测。

Char3Model 的回顾

提醒:RNN 在任何方面都不是不同或不寻常或神奇的 — 只是一个标准的全连接网络。

标准全连接网络

  • 箭头代表一个或多个层操作 —— 一般来说是线性后跟一个非线性函数,本例中是矩阵乘法后跟 relutanh
  • 相同颜色的箭头表示使用完全相同的权重矩阵。
  • 与以前的一个细微差别是第二层和第三层有输入进来。我们尝试了两种方法 —— 将这些输入连接或添加到当前激活中。
代码语言:javascript复制
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,我们免费获得了权重矩阵和偏置向量。
  • 为了解决第一个椭圆中没有橙色箭头的问题,我们发明了一个空矩阵
代码语言:javascript复制
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 循环
代码语言:javascript复制
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

代码语言:javascript复制
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 的展开版本的大小将有一百万层(椭圆)。一百万层全连接网络将非常占用内存,因为为了进行链式规则,我们必须在每个批次中乘以一百万层,同时记住所有一百万个梯度。
  • 为了避免这种情况,我们告诉它不时忘记它的历史。我们仍然可以记住状态(隐藏矩阵中的值)而不必记住如何到达那里的一切。
代码语言:javascript复制
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 的包装器已经有了一些东西,您可以在每个路径中有一个训练路径和验证路径,并且每个路径中有一个或多个文本文件,其中包含一堆文本,这些文本被连接在一起用于您的语言模型。

代码语言:javascript复制
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]。
  • 这样做的另一个好处是,似乎更现实地拥有一个验证集,它不是文本行的随机洗牌集,而是完全独立于语料库的一部分。
  • 当您进行语言模型时,您实际上不需要单独的文件。您可以有多个文件,但它们最终会被连接在一起。
代码语言:javascript复制
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:我们将其重命名为csn_fac:嵌入的大小,n_hidden:我们隐藏状态的大小
  • 我们没有单独的测试集,所以我们将只使用验证集进行测试
  • TorchText 每次都会稍微随机化bptt的长度。它并不总是给我们确切的 8 个字符;有 5%的概率,它会将其减半并添加一个小的标准偏差,使其略大或略小于 8。我们不能对数据进行洗牌,因为它需要是连续的,所以这是引入一些随机性的一种方式。
  • 问题:每个小批次的大小是否保持恒定?是的,我们需要用h权重矩阵进行矩阵乘法,因此小批次的大小必须保持恒定。但是序列长度可以改变,没有问题。
  • len(md.trn_dl):数据加载器的长度(即有多少个小批次),md.nt:标记的数量(即词汇表中有多少个唯一的东西)
  • 一旦运行LanguageModelData.from_text_filesTEXT将包含一个名为vocab的额外属性。TEXT.vocab.itos是词汇表中唯一项目的列表,TEXT.vocab.stoi是从每个项目到数字的反向映射。
代码语言:javascript复制
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上进行。
代码语言:javascript复制
m = CharSeqStatefulRnn(md.nt, n_fac, 512).cuda() 
opt = optim.Adam(m.parameters(), 1e-3)
fit(m, md, 4, opt, F.nll_loss)

让我们通过拆解 RNN 来获得更多见解

我们移除了nn.RNN的使用,并用nn.RNNCell替换。PyTorch 源代码如下。您应该能够阅读和理解(注意:它们不会连接输入和隐藏状态,而是将它们相加 - 这是我们的第一种方法):

代码语言:javascript复制
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

代码语言:javascript复制
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

www.wildml.com/2015/10/recurrent-neural-network-tutorial-part-4-implementing-a-grulstm-rnn-with-python-and-theano/

  • 通常,输入会乘以一个权重矩阵以创建新的激活h,并立即添加到现有的激活中。这里不是这样发生的。
  • 输入进入,它不仅仅被添加到先前的激活中,而是先前的激活被r(重置门)乘以,r的值为 0 或 1。
  • r的计算如下 - 一些权重矩阵的矩阵乘法和我们先前隐藏状态和新输入的连接。换句话说,这是一个小型的单隐藏层神经网络。它也通过 sigmoid 函数传递。这个小型神经网络学会了确定要记住隐藏状态的多少(也许在看到句号字符时全部忘记 - 新句子的开始)。
  • z门(更新门)确定要使用(隐藏状态的新输入版本)的程度,以及要保持隐藏状态与之前相同的程度。

colah.github.io/posts/2015-08-Understanding-LSTMs/

  • 线性插值
代码语言:javascript复制
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代码的样子,我们利用这个新模型如下:

代码语言:javascript复制
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中返回一个矩阵元组(与隐藏状态完全相同的大小):

代码语言:javascript复制
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]

代码语言:javascript复制
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返回优化器。
代码语言:javascript复制
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时,现在可以传递LayerOptimizercallbacks
  • 在这里,我们使用余弦退火回调 —— 需要一个LayerOptimizer对象。它通过更改lo对象内的学习率来进行余弦退火。
  • 概念:创建一个余弦退火回调,它将更新层优化器lo中的学习率。一个周期的长度等于len(md.trn_dl) —— 一个周期中有多少个小批次就是数据加载器的长度。由于它正在进行余弦退火,它需要知道多久重置一次。您可以以通常的方式传递cycle_mult。我们甚至可以自动保存我们的模型,就像我们在Learner.fit中使用cycle_save_name一样。
  • 我们可以在训练、周期或批处理的开始时进行回调,也可以在训练、周期或批处理的结束时进行回调。
  • 它已用于CosAnneal(SGDR),和解耦权重衰减(AdamW),随时间变化的损失图等。

测试[59:55]

代码语言:javascript复制
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 数据以图像格式可在此处获取

代码语言:javascript复制
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 区域。
代码语言:javascript复制
data = get_data(32,bs)

lr=1e-2

来自我们的学生 Kerem Turgutlu 的这个笔记本:

代码语言:javascript复制
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 中以将这些注册为属性。
代码语言:javascript复制
learn = ConvLearner.from_model_data(SimpleNet([32*32*3, 40,10]), data)
  • 现在我们提高一个 API 级别 - 而不是调用 fit 函数,我们从一个自定义模型创建一个 learn 对象。ConfLearner.from_model_data 接受标准的 PyTorch 模型和模型数据对象。
代码语言:javascript复制
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()
代码语言:javascript复制
%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 核的滤波器。当有多个滤波器时,输出将具有额外的维度。
代码语言:javascript复制
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 最大池化相同的效果)
代码语言:javascript复制
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,因此它将删除最后两层。
  • 这个模型被称为“完全卷积网络” - 每一层都是卷积的,除了最后一层。
代码语言:javascript复制
learn.lr_find(end_lr=100)
learn.sched.plot()
  • lr_find 尝试的默认最终学习率是 10。如果在那一点上损失仍在变好,您可以通过指定 end_lr 来覆盖。
代码语言:javascript复制
%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 中,层定义和神经网络定义是相同的。每当您有一个层时,您可以将其用作神经网络,当您有一个神经网络时,您可以将其用作层。

代码语言:javascript复制
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 时,您真的不想丢弃整个部分。
代码语言:javascript复制
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 开始通过对输入进行归一化。我们真正想要做的是对所有层进行这样的操作,而不仅仅是对输入。
代码语言:javascript复制
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之后

消融研究

这是一个尝试打开和关闭模型不同部分以查看哪些部分产生哪些影响的过程,原始批量归一化论文中没有进行任何有效的消融。因此,缺失的一点是刚刚提出的这个问题——批量归一化放在哪里。这个疏忽导致了很多问题,因为原始论文实际上没有将其放在最佳位置。自那时以来,其他人已经弄清楚了这一点,当 Jeremy 向人们展示代码时,实际上放在更好位置的人们会说他的批量归一化放错了位置。

  • 尽量在每一层上都使用批量归一化。
  • 不要停止对数据进行归一化,这样使用您的数据的人就会知道您是如何对数据进行归一化的。其他库可能无法正确处理预训练模型的批量归一化,因此当人们开始重新训练时可能会出现问题。
代码语言:javascript复制
class ConvBnNet(nn.Module):
    def __init__(self, layers, c):
        super().__init__()
        self.conv1 = nn.Conv2d(3, 10, kernel_size=5, stride=1, padding=2)
        self.layers = nn.ModuleList([
            BnLayer(layers[i], layers[i   1])
            for i in range(len(layers) - 1)
        ])
        self.out = nn.Linear(layers[-1], c)

    def forward(self, x):
        x = self.conv1(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)
  • 代码的其余部分类似——使用BnLayer而不是ConvLayer
  • 在开始时添加了一个单个卷积层,试图接近现代方法。它具有更大的内核大小和步幅为 1。基本思想是我们希望第一层具有更丰富的输入。它使用 5x5 区域进行卷积,这使它可以尝试在该 5x5 区域中找到更有趣更丰富的特征,然后输出更大的输出(在这种情况下,是 10x5x5 个滤波器)。通常是 5x5 或 7x7,甚至是 11x11 卷积,输出相当多的滤波器(例如 32 个滤波器)。
  • 由于padding = kernel_size — 1 / 2stride=1,输入大小与输出大小相同——只是有更多的滤波器。
  • 这是尝试创建更丰富的起点的好方法。

深度批量归一化

让我们增加模型的深度。我们不能只添加更多的步幅为 2 的层,因为每次都会将图像的大小减半。相反,在每个步幅为 2 的层之后,我们插入一个步幅为 1 的层。

代码语言:javascript复制
class ConvBnNet2(nn.Module):
    def __init__(self, layers, c):
        super().__init__()
        self.conv1 = nn.Conv2d(3, 10, kernel_size=5, stride=1, padding=2)
        self.layers = nn.ModuleList([
            BnLayer(layers[i], layers[i 1])
            for i in range(len(layers) - 1)
        ])
        self.layers2 = nn.ModuleList([
            BnLayer(layers[i 1], layers[i   1], 1)
            for i in range(len(layers) - 1)
        ])
        self.out = nn.Linear(layers[-1], c)

    def forward(self, x):
        x = self.conv1(x)
        for l,l2 in zip(self.layers, self.layers2):
            x = l(x)
            x = l2(x)
        x = F.adaptive_max_pool2d(x, 1)
        x = x.view(x.size(0), -1)
        return F.log_softmax(self.out(x), dim=-1)

learn = ConvLearner.from_model_data((ConvBnNet2([10, 20, 40, 80, 160], 10), data)
%time learn.fit(1e-2, 2)

'''
A Jupyter Widget
[ 0.       1.53499  1.43782  0.47588]                       
[ 1.       1.28867  1.22616  0.55537]                       

CPU times: user 1min 22s, sys: 34.5 s, total: 1min 56s
Wall time: 58.2 s
'''
%time learn.fit(1e-2, 2, cycle_len=1)
'''
A Jupyter Widget
[ 0.       1.10933  1.06439  0.61582]                       
[ 1.       1.04663  0.98608  0.64609]                       

CPU times: user 1min 21s, sys: 32.9 s, total: 1min 54s
Wall time: 57.6 s
'''

准确率与之前相同。现在深度为 12 层,即使对于批量归一化来说也太深了。可以训练 12 层深的卷积网络,但开始变得困难。而且似乎并没有太多帮助。

ResNet

代码语言:javascript复制
class ResnetLayer(BnLayer):
    def forward(self, x): 
        return x   super().forward(x)
class Resnet(nn.Module):
    def __init__(self, layers, c):
        super().__init__()
        self.conv1 = nn.Conv2d(3, 10, kernel_size=5, stride=1, padding=2)
        self.layers = nn.ModuleList([
            BnLayer(layers[i], layers[i 1])
            for i in range(len(layers) - 1)
        ])
        self.layers2 = nn.ModuleList([
            ResnetLayer(layers[i 1], layers[i   1], 1)
            for i in range(len(layers) - 1)
        ])
        self.layers3 = nn.ModuleList([
            ResnetLayer(layers[i 1], layers[i   1], 1)
            for i in range(len(layers) - 1)
        ])
        self.out = nn.Linear(layers[-1], c)

    def forward(self, x):
        x = self.conv1(x)
        for l,l2,l3 in zip(self.layers, self.layers2, self.layers3):
            x = l3(l2(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)
  • ResnetLayer继承自BnLayer并覆盖forward
  • 然后添加一堆层,使其深度增加 3 倍,仍然可以很好地训练,只是因为x super().forward(x)
代码语言:javascript复制
learn = ConvLearner.from_model_data(Resnet([10, 20, 40, 80, 160], 10), data)wd=1e-5%time learn.fit(1e-2, 2, wds=wd)
'''
A Jupyter Widget
[ 0.       1.58191  1.40258  0.49131]                       
[ 1.       1.33134  1.21739  0.55625]                       

CPU times: user 1min 27s, sys: 34.3 s, total: 2min 1s
Wall time: 1min 3s
'''
%time learn.fit(1e-2, 3, cycle_len=1, cycle_mult=2, wds=wd)
'''
A Jupyter Widget
[ 0.       1.11534  1.05117  0.62549]                       
[ 1.       1.06272  0.97874  0.65185]                       
[ 2.       0.92913  0.90472  0.68154]                        
[ 3.       0.97932  0.94404  0.67227]                        
[ 4.       0.88057  0.84372  0.70654]                        
[ 5.       0.77817  0.77815  0.73018]                        
[ 6.       0.73235  0.76302  0.73633]                        

CPU times: user 5min 2s, sys: 1min 59s, total: 7min 1s
Wall time: 3min 39s
'''
%time learn.fit(1e-2, 8, cycle_len=4, wds=wd)
'''
A Jupyter Widget
[ 0.       0.8307   0.83635  0.7126 ]                        
[ 1.       0.74295  0.73682  0.74189]                        
[ 2.       0.66492  0.69554  0.75996]                        
[ 3.       0.62392  0.67166  0.7625 ]                        
[ 4.       0.73479  0.80425  0.72861]                        
[ 5.       0.65423  0.68876  0.76318]                        
[ 6.       0.58608  0.64105  0.77783]                        
[ 7.       0.55738  0.62641  0.78721]                        
[ 8.       0.66163  0.74154  0.7501 ]                        
[ 9.       0.59444  0.64253  0.78106]                        
[ 10.        0.53      0.61772   0.79385]                    
[ 11.        0.49747   0.65968   0.77832]                    
[ 12.        0.59463   0.67915   0.77422]                    
[ 13.        0.55023   0.65815   0.78106]                    
[ 14.        0.48959   0.59035   0.80273]                    
[ 15.        0.4459    0.61823   0.79336]                    
[ 16.        0.55848   0.64115   0.78018]                    
[ 17.        0.50268   0.61795   0.79541]                    
[ 18.        0.45084   0.57577   0.80654]                    
[ 19.        0.40726   0.5708    0.80947]                    
[ 20.        0.51177   0.66771   0.78232]                    
[ 21.        0.46516   0.6116    0.79932]                    
[ 22.        0.40966   0.56865   0.81172]                    
[ 23.        0.3852    0.58161   0.80967]                    
[ 24.        0.48268   0.59944   0.79551]                    
[ 25.        0.43282   0.56429   0.81182]                    
[ 26.        0.37634   0.54724   0.81797]                    
[ 27.        0.34953   0.54169   0.82129]                    
[ 28.        0.46053   0.58128   0.80342]                    
[ 29.        0.4041    0.55185   0.82295]                    
[ 30.        0.3599    0.53953   0.82861]                    
[ 31.        0.32937   0.55605   0.82227]                    

CPU times: user 22min 52s, sys: 8min 58s, total: 31min 51s
Wall time: 16min 38s
'''

ResNet 块

return x super().forward(x)

y = x f(x)

其中x是来自上一层的预测,y是来自当前层的预测。重新排列公式,我们得到:公式重新排列

f(x) = y − x

差异y − x残差。残差是迄今为止我们计算的错误。这意味着尝试找到一组卷积权重,试图填补我们偏离的量。换句话说,我们有一个输入,我们有一个函数试图预测错误(即我们偏离的量)。然后我们将输入的错误预测量相加,然后再添加另一个错误预测量,然后重复这个过程,逐层放大到正确答案。这基于一种称为boosting的理论。

  • 完整的 ResNet 在将其添加回原始输入之前进行了两次卷积(我们这里只做了一次)。
  • 在每个块x = l3(l2(l(x)))中,其中一层不是ResnetLayer而是一个带有stride=2的标准卷积——这被称为“瓶颈层”。ResNet 不是卷积层,而是我们将在第 2 部分中介绍的不同形式的瓶颈块。

ResNet 2 [01:59:33]

在这里,我们增加了特征的大小并添加了 dropout。

代码语言:javascript复制
class Resnet2(nn.Module):
    def __init__(self, layers, c, p=0.5):
        super().__init__()
        self.conv1 = BnLayer(3, 16, stride=1, kernel_size=7)
        self.layers = nn.ModuleList([
            BnLayer(layers[i], layers[i 1])
            for i in range(len(layers) - 1)
        ])
        self.layers2 = nn.ModuleList([
            ResnetLayer(layers[i 1], layers[i   1], 1)
            for i in range(len(layers) - 1)
        ])
        self.layers3 = nn.ModuleList([
            ResnetLayer(layers[i 1], layers[i   1], 1)
            for i in range(len(layers) - 1)
        ])
        self.out = nn.Linear(layers[-1], c)
        self.drop = nn.Dropout(p)

    def forward(self, x):
        x = self.conv1(x)
        for l,l2,l3 in zip(self.layers, self.layers2, self.layers3):
            x = l3(l2(l(x)))
        x = F.adaptive_max_pool2d(x, 1)
        x = x.view(x.size(0), -1)
        x = self.drop(x)
        return F.log_softmax(self.out(x), dim=-1)
        
learn = ConvLearner.from_model_data(Resnet2([**16, 32, 64, 128, 256**], 10, 0.2), data)
wd=1e-6
%time learn.fit(1e-2, 2, wds=wd)
%time learn.fit(1e-2, 3, cycle_len=1, cycle_mult=2, wds=wd)
%time learn.fit(1e-2, 8, cycle_len=4, wds=wd)
log_preds,y = learn.TTA()
preds = np.mean(np.exp(log_preds),0)
metrics.log_loss(y,preds), accuracy(preds,y)
'''
(0.44507397166057938, 0.84909999999999997)
'''

85%是 2012 年或 2013 年 CIFAR 10 的最新技术。如今,它已经达到了 97%,因此还有改进的空间,但所有都基于这些技术:

  • 更好的数据增强方法
  • 更好的正则化方法
  • 对 ResNet 进行一些调整

问题[02:01:07]:我们可以将“训练残差”方法应用于非图像问题吗?是的!但是它已经被其他地方忽略了。在 NLP 中,“transformer 架构”最近出现,并被证明是翻译的最新技术,并且其中有一个简单的 ResNet 结构。这种一般方法称为“跳过连接”(即跳过一层的想法),在计算机视觉中经常出现,但似乎没有其他人多使用,尽管它与计算机视觉无关。好机会!

狗与猫 [02:02:03]

回到狗和猫。我们将创建 resnet34(如果您对尾随数字的含义感兴趣,请参阅这里——只是不同的参数)。

代码语言:javascript复制
PATH = "data/dogscats/"
sz = 224
arch = resnet34  # <-- Name of the function 
bs = 64
m = arch(pretrained=True) # Get a model w/ pre-trained weight loaded
m
'''
ResNet(
  (conv1): Conv2d (3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
  (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True)
  (relu): ReLU(inplace)
  (maxpool): MaxPool2d(kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), dilation=(1, 1))
  (**layer1**): Sequential(
    (0): BasicBlock(
      (conv1): Conv2d (64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True)
      (relu): ReLU(inplace)
      (conv2): Conv2d (64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True)
    )
    (1): BasicBlock(
      (conv1): Conv2d (64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True)
      (relu): ReLU(inplace)
      (conv2): Conv2d (64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True)
    )
    (2): BasicBlock(
      (conv1): Conv2d (64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True)
      (relu): ReLU(inplace)
      (conv2): Conv2d (64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True)
    )
  )
  (layer2): Sequential(
    (0): BasicBlock(
      (conv1): Conv2d (64, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True)
      (relu): ReLU(inplace)
      (conv2): Conv2d (128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True)
      (downsample): Sequential(
        (0): Conv2d (64, 128, kernel_size=(1, 1), stride=(2, 2), bias=False)
        (1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True)
      )
    )
    (1): BasicBlock(
      (conv1): Conv2d (128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True)
      (relu): ReLU(inplace)
      (conv2): Conv2d (128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True)
    )
    (2): BasicBlock(
      (conv1): Conv2d (128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True)
      (relu): ReLU(inplace)
      (conv2): Conv2d (128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True)
    )
    (3): BasicBlock(
      (conv1): Conv2d (128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True)
      (relu): ReLU(inplace)
      (conv2): Conv2d (128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True)
    )
  )
  ...
  (avgpool): AvgPool2d(kernel_size=7, stride=7, padding=0, ceil_mode=False, count_include_pad=True)
  (fc): Linear(in_features=512, out_features=1000)
)
'''

我们的 ResNet 模型具有 Relu → BatchNorm。TorchVision 使用 BatchNorm → Relu。有三个不同版本的 ResNet 在流传,最好的是 PreAct (arxiv.org/pdf/1603.05027.pdf)。

  • 目前,最后一层有数千个特征,因为 ImageNet 有 1000 个特征,所以我们需要摆脱它。
  • 当您使用 fast.ai 的ConvLearner时,它会为您删除最后两层。fast.ai 用自适应平均池化和自适应最大池化替换AvgPool2d,并将两者连接在一起。
  • 对于这个练习,我们将做一个简单版本。
代码语言:javascript复制
m = nn.Sequential(*children(m)[:-2], 
    nn.Conv2d(512, 2, 3, padding=1), 
    nn.AdaptiveAvgPool2d(1), 
    Flatten(), 
    nn.LogSoftmax()
)
  • 删除最后两层
  • 添加一个只有 2 个输出的卷积。
  • 进行平均池化然后进行 softmax
  • 最后没有线性层。这是产生两个数字的不同方式——这使我们能够进行 CAM!
代码语言:javascript复制
tfms = tfms_from_model(arch, sz, aug_tfms=transforms_side_on, max_zoom=1.1)
data = ImageClassifierData.from_paths(PATH, tfms=tfms, bs=bs)
learn = ConvLearner.from_model_data(m, data)
learn.freeze_to(-4)
learn.fit(0.01, 1)
learn.fit(0.01, 1, cycle_len=1)
  • ConvLearner.from_model是我们之前学到的——允许我们使用自定义模型创建 Learner 对象。
  • 然后冻结除了我们刚刚添加的层之外的所有层。

类激活图(CAM)[02:08:55]

我们选择一个特定的图像,并使用一种称为 CAM 的技术,询问模型哪些部分的图像被证明是重要的。

它是如何做到的?让我们逆向工作。它是通过生成这个矩阵来做到的:

大数字对应于猫。那么这个矩阵是什么?这个矩阵简单地等于特征矩阵feat乘以py向量的值:

代码语言:javascript复制
f2=np.dot(np.rollaxis(feat,0,3), py)
f2-=f2.min()
f2/=f2.max()
f2

py 向量是预测,表示“我对这是一只猫有 100%的信心”。feat 是最终卷积层(我们添加的Conv2d层)输出的值(2×7×7)。如果我们将feat乘以py,我们会得到所有第一个通道的值,而第二个通道的值为零。因此,它将返回与猫对应的部分的最后一个卷积层的值。换句话说,如果我们将feat乘以[0, 1],它将与狗对应。

代码语言:javascript复制
sf = SaveFeatures(m[-4])
py = m(Variable(x.cuda()))
sf.remove()

py = np.exp(to_np(py)[0]); py
'''
array([ 1.,  0.], dtype=float32)
'''
feat = np.maximum(0, sf.features[0])
feat.shape

换句话说,在模型中,卷积层之后唯一发生的事情是平均池化层。平均池化层将 7×7 的网格平均化,计算出每个部分有多少“像猫”。然后,我们将“猫样”矩阵调整大小为与原始猫图像相同的大小,并叠加在顶部,然后你就得到了热图。

您可以在家中使用这种技术的方法是:

  1. 当您有一幅大图像时,您可以在一个快速小的卷积网络上计算这个矩阵。
  2. 放大具有最高值的区域
  3. 仅在该部分重新运行

由于时间不够,我们很快跳过了这部分,但我们将在第 2 部分中学习更多关于这种方法的内容。

“Hook”是让我们要求模型返回矩阵的机制。register_forward_hook要求 PyTorch 每次计算一个层时运行给定的函数 - 类似于每次计算一个层时发生的回调。在以下情况下,它保存了我们感兴趣的特定层的值:

代码语言:javascript复制
class SaveFeatures():
    features=None
    def __init__(self, m): 
        self.hook = m.register_forward_hook(self.hook_fn)
    def hook_fn(self, module, input, output): 
        self.features = to_np(output)
    def remove(self): 
        self.hook.remove()

Jeremy 的问题[02:14:27]:“您对深度学习的探索”和“如何跟上从业者的重要研究”

“如果您打算参加第 2 部分,您应该掌握我们在第 1 部分学到的所有技术”。以下是您可以做的一些事情:

  1. 至少观看每个视频 3 次。
  2. 确保您可以重新创建笔记本而无需观看视频 - 可能使用不同的数据集来使其更有趣。
  3. 密切关注论坛上的最新论文和最新进展。
  4. 坚持不懈,继续努力!

0 人点赞