原文:
zh.annas-archive.org/md5/844a6ce45a119d3197c33a6b5db2d7b1
译者:飞龙
第二部分:深度学习基础算法
在这一部分,我们将探索所有基础深度学习算法。我们首先直观地理解每个算法,然后深入研究其数学基础。我们还将学习如何在 TensorFlow 中实现每个算法。
本节包括以下章节:
-
第三章,梯度下降及其变种
-
第四章,使用 RNN 生成歌词
-
第五章,改进 RNN
-
第六章,解密卷积网络
-
第七章,学习文本表示
第三章:梯度下降及其变体
梯度下降是最流行和广泛使用的优化算法之一,是一种一阶优化算法。一阶优化意味着我们只计算一阶导数。正如我们在第一章中看到的,深度学习简介,我们使用梯度下降计算损失函数相对于网络权重的一阶导数以最小化损失。
梯度下降不仅适用于神经网络,它还用于需要找到函数最小值的情况。在本章中,我们将深入探讨梯度下降,从基础知识开始,并学习几种梯度下降算法的变体。有各种各样的梯度下降方法用于训练神经网络。首先,我们将了解随机梯度下降(SGD)和小批量梯度下降。然后,我们将探讨如何通过动量加速梯度下降以达到收敛。本章后期,我们将学习如何使用各种算法(如 Adagrad、Adadelta、RMSProp、Adam、Adamax、AMSGrad 和 Nadam)以自适应方式执行梯度下降。我们将使用简单的线性回归方程,并看看如何使用各种梯度下降算法找到线性回归成本函数的最小值。
在本章中,我们将学习以下主题:
-
解析梯度下降
-
梯度下降与随机梯度下降的区别
-
动量和 Nesterov 加速梯度
-
梯度下降的自适应方法
解析梯度下降
在深入细节之前,让我们先了解基础知识。数学中的函数是什么?函数表示输入和输出之间的关系。我们通常用表示一个函数。例如,
表示一个以
为输入并返回
为输出的函数。它也可以表示为
。
这里,我们有一个函数,,我们可以绘制并查看函数的形状:
函数的最小值称为函数的最小值。正如您在上图中看到的, 函数的最小值位于 0 处。前述函数称为凸函数,在这种函数中只有一个最小值。当存在多个最小值时,函数称为非凸函数。如下图所示,非凸函数可以有许多局部最小值和一个全局最小值,而凸函数只有一个全局最小值:
通过观察 函数的图表,我们可以很容易地说它在
处有其最小值。但是,如何在数学上找到函数的最小值?首先,让我们假设 x = 0.7。因此,我们处于一个 x = 0.7 的位置,如下图所示:
现在,我们需要走向零,这是我们的最小值,但我们如何达到它?我们可以通过计算函数的导数,,来达到它。因此,关于
,函数的导数为:
由于我们在 x = 0.7 处,并将其代入前述方程,我们得到以下方程:
计算导数后,我们根据以下更新规则更新 的位置:
如我们在下图中所见,我们最初位于 x = 0.7 处,但在计算梯度后,我们现在处于更新位置 x = -0.7。然而,这并不是我们想要的,因为我们错过了我们的最小值 x = 0,而是达到了其他位置:
为了避免这种情况,我们引入了一个称为学习率的新参数,,在更新规则中。它帮助我们减慢梯度下降的步伐,以便我们不会错过最小点。我们将梯度乘以学习率,并更新
的值,如下所示:
假设 ;现在,我们可以写出以下内容:
正如我们在下图中看到的那样,在将更新后的 x 值的梯度乘以学习率后,我们从初始位置 x = 0.7 下降到 x = 0.49:
然而,这仍然不是我们的最优最小值。我们需要进一步下降,直到达到最小值,即 x = 0。因此,对于一些 n 次迭代,我们必须重复相同的过程,直到达到最小点。也就是说,对于一些 n 次迭代,我们使用以下更新规则更新 x 的值,直到达到最小点:
好的,为什么在前面的方程中有一个减号?也就是说,为什么我们要从x中减去?为什么不能将它们加起来,将我们的方程变为
?
这是因为我们在寻找函数的最小值,所以我们需要向下。如果我们将x加上,那么我们在每次迭代时都会向上移动,无法找到最小值,如下图所示:
![]() | ![]() |
---|---|
![]() | ![]() |
因此,在每次迭代中,我们计算y相对于x的梯度,即,将梯度乘以学习率,即
,然后从x值中减去它以获得更新后的x值,如下所示:
通过在每次迭代中重复此步骤,我们从成本函数向下移动,并达到最小点。正如我们在下图中所看到的,我们从初始位置 0.7 向下移动到 0.49,然后从那里到达 0.2。
然后,在多次迭代之后,我们达到了最小点,即 0.0:
当我们达到函数的最小值时,我们称之为收敛。但问题是:我们如何知道我们已经达到了收敛?在我们的例子中,,我们知道最小值是 0。因此,当我们达到 0 时,我们可以说我们找到了最小值,即我们已经达到了收敛。但是我们如何在数学上表达 0 是函数
的最小值呢?
让我们仔细观察下面的图表,显示了x在每次迭代中的变化。正如您可能注意到的那样,第五次迭代时x的值为 0.009,第六次迭代时为 0.008,第七次迭代时为 0.007。正如您所见,第五、六和七次迭代之间几乎没有什么差异。当x在迭代中的值变化很小时,我们可以得出我们已经达到了收敛:
好了,但这一切有什么用?我们为什么要找到一个函数的最小值?当我们训练模型时,我们的目标是最小化模型的损失函数。因此,通过梯度下降,我们可以找到成本函数的最小值。找到成本函数的最小值给我们提供了模型的最优参数,从而可以获得最小的损失。一般来说,我们用来表示模型的参数。以下方程称为参数更新规则或权重更新规则:
在这里,我们有以下内容:
-
是模型的参数
-
是学习率
-
是梯度
我们根据参数更新规则多次迭代更新模型的参数,直到达到收敛。
在回归中执行梯度下降
到目前为止,我们已经理解了梯度下降算法如何找到模型的最优参数。在本节中,我们将了解如何在线性回归中使用梯度下降,并找到最优参数。
简单线性回归的方程可以表达如下:
因此,我们有两个参数,和
。现在,我们将看看如何使用梯度下降找到这两个参数的最优值。
导入库
首先,我们需要导入所需的库:
import warnings
warnings.filterwarnings('ignore')
import random
import math
import numpy as np
from matplotlib import pyplot as plt
%matplotlib inline
准备数据集
接下来,我们将生成一些随机数据点,有500
行和2
列(x和y),并将它们用于训练:
data = np.random.randn(500, 2)
正如你所看到的,我们的数据有两列:
print data[0]
array([-0.08575873, 0.45157591])
第一列表示值:
print data[0,0]
-0.08575873243708057
第二列表示值:
print data[0,1]
0.4515759149158441
我们知道简单线性回归方程的表达如下:
因此,我们有两个参数,和
。我们将这两个参数都存储在名为
theta
的数组中。首先,我们将theta
初始化为零,如下所示:
theta = np.zeros(2)
函数theta[0]
表示的值,而函数
theta[1]
表示的值:
print theta
array([0., 0.])
定义损失函数
线性回归的均方误差(MSE)如下所示:
这里, 是训练样本的数量,
是实际值,
是预测值。
上述损失函数的实现如下所示。我们将data
和模型参数theta
输入损失函数,返回均方误差(MSE)。请记住,data[,0]
具有一个 值,而
data[,1]
具有一个 值。类似地,
theta [0]
的值为m
,而theta[1]
的值为 。
让我们定义损失函数:
def loss_function(data,theta):
现在,我们需要获取 和
的值:
m = theta[0]
b = theta[1]
loss = 0
我们对每个迭代执行此操作:
for i in range(0, len(data)):
现在,我们得到 和
的值:
x = data[i, 0]
y = data[i, 1]
然后,我们预测 的值:
y_hat = (m*x + b)
这里,我们按照方程 (3) 计算损失:
loss = loss + ((y - (y_hat)) ** 2)
然后,我们计算均方误差:
mse = loss / float(len(data))
return mse
当我们将随机初始化的data
和模型参数theta
输入loss_function
时,会返回均方损失,如下所示:
loss_function(data, theta)
1.0253548008165727
现在,我们需要最小化这个损失。为了最小化损失,我们需要计算损失函数 关于模型参数
和
的梯度,并根据参数更新规则更新参数。首先,我们将计算损失函数的梯度。
计算损失函数的梯度
损失函数关于参数 的梯度如下:
损失函数关于参数 的梯度如下:
我们定义了一个名为compute_gradients
的函数,该函数接受输入参数data
和theta
,并返回计算得到的梯度:
def compute_gradients(data, theta):
现在,我们需要初始化梯度:
gradients = np.zeros(2)
然后,我们需要保存数据点的总数N
:
N = float(len(data))
现在,我们可以得到 和
的值:
m = theta[0]
b = theta[1]
我们对每个迭代执行相同操作:
for i in range(0, len(data)):
然后,我们得到 和
的值:
x = data[i, 0]
y = data[i, 1]
现在,我们按照方程 (4) 计算损失关于 的梯度:
gradients[0] += - (2 / N) * x * (y - (( m* x) + b))
然后,我们根据方程 (5) 计算损失相对于 的梯度:
gradients[1] += - (2 / N) * (y - ((theta[0] * x) + b))
我们需要添加epsilon
以避免零除错误:
epsilon = 1e-6
gradients = np.divide(gradients, N + epsilon)
return gradients
当我们输入我们随机初始化的data
和theta
模型参数时,compute_gradients
函数返回相对于 即
,以及相对于
即
的梯度,如下所示:
compute_gradients(data,theta)
array([-9.08423989e-05, 1.05174511e-04])
更新模型参数
现在我们已经计算出梯度,我们需要根据我们的更新规则更新我们的模型参数,如下所示:
因为我们在theta[0]
中存储了 ,在
theta[1]
中存储了 ,所以我们可以写出我们的更新方程如下:
正如我们在前一节中学到的,仅在一个迭代中更新梯度不会导致我们收敛到成本函数的最小值,因此我们需要计算梯度并对模型参数进行多次迭代更新。
首先,我们需要设置迭代次数:
num_iterations = 50000
现在,我们需要定义学习率:
lr = 1e-2
接下来,我们将定义一个名为loss
的列表来存储每次迭代的损失:
loss = []
在每次迭代中,我们将根据我们的参数更新规则从方程 (8) 计算并更新梯度:
theta = np.zeros(2)
for t in range(num_iterations):
#compute gradients
gradients = compute_gradients(data, theta)
#update parameter
theta = theta - (lr*gradients)
#store the loss
loss.append(loss_function(data,theta))
现在,我们需要绘制loss
(Cost
)函数:
plt.plot(loss)
plt.grid()
plt.xlabel('Training Iterations')
plt.ylabel('Cost')
plt.title('Gradient Descent')
下图显示了损失(Cost)随训练迭代次数减少的情况:
因此,我们学会了梯度下降可以用来找到模型的最优参数,然后我们可以用它来最小化损失。在下一节中,我们将学习梯度下降算法的几种变体。
梯度下降与随机梯度下降的对比
我们使用我们的参数更新方程 (1) 多次更新模型参数,直到找到最优参数值。在梯度下降中,为了执行单个参数更新,我们遍历训练集中的所有数据点。因此,每次更新模型参数时,我们都要遍历训练集中的所有数据点。仅在遍历训练集中的所有数据点后更新模型参数使得梯度下降非常缓慢,并且会增加训练时间,特别是当数据集很大时。
假设我们有一个包含 100 万数据点的训练集。我们知道,我们需要多次更新模型的参数才能找到最优参数值。因此,即使是执行单次参数更新,我们也需要遍历训练集中的所有 100 万数据点,然后更新模型参数。这无疑会使训练变慢。这是因为我们不能仅通过单次更新找到最优参数;我们需要多次更新模型参数才能找到最优值。因此,如果我们对训练集中的每个参数更新都进行 100 万次数据点的迭代,这肯定会减慢我们的训练速度。
因此,为了应对这一情况,我们引入了随机梯度下降(SGD)。与梯度下降不同,我们无需等待在训练集中迭代所有数据点后更新模型参数;我们只需在遍历训练集中的每一个数据点后更新模型参数。
由于我们在随机梯度下降中在遍历每个单独数据点后更新模型参数,相比梯度下降,它将更快地学习到模型的最优参数,从而缩短训练时间。
SGD 的作用是什么?当我们有一个巨大的数据集时,使用传统的梯度下降方法,我们只在遍历完整个数据集中的所有数据点后更新参数。因此,在整个数据集上进行多次迭代后,我们才能达到收敛,并且显然这需要很长时间。但是,在随机梯度下降中,我们在遍历每个单独的训练样本后更新参数。也就是说,我们从第一个训练样本开始就学习到寻找最优参数,这有助于相比传统的梯度下降方法更快地达到收敛。
我们知道,周期指的是神经网络查看整个训练数据的次数。因此,在梯度下降中,每个周期我们执行参数更新。这意味着,在每个周期结束后,神经网络看到整个训练数据。我们按以下方式每个周期执行参数更新:
然而,在随机梯度下降中,我们无需等到每个周期完成后才更新参数。也就是说,我们不需要等到神经网络看到整个训练数据后才更新参数。相反,我们在每个周期中从看到单个训练样本开始就更新网络的参数:
下图显示了梯度下降和随机梯度下降如何执行参数更新并找到最小成本。图中心的星号符号表示我们最小成本的位置。正如您所见,随机梯度下降比普通梯度下降更快地达到收敛。您还可以观察到随机梯度下降中梯度步骤的振荡;这是因为我们在每个训练样本上更新参数,因此与普通梯度下降相比,SGD 中的梯度步骤变化频繁:
还有另一种称为小批量梯度下降的梯度下降变体。它吸取了普通梯度下降和随机梯度下降的优点。在 SGD 中,我们看到我们为每个训练样本更新模型的参数。然而,在小批量梯度下降中,我们不是在每个训练样本迭代后更新参数,而是在迭代一些数据点的批次后更新参数。假设批量大小为 50,这意味着我们在迭代 50 个数据点后更新模型的参数,而不是在迭代每个单独数据点后更新模型的参数。
下图显示了 SGD 和小批量梯度下降的轮廓图:
这些梯度下降类型之间的差异如下:
-
梯度下降:在训练集中迭代所有数据点后更新模型参数
-
随机梯度下降:在训练集中迭代每个单独数据点后更新模型的参数
-
小批量梯度下降:在训练集中迭代n个数据点后更新模型参数
对于大型数据集,小批量梯度下降优于普通梯度下降和 SGD,因为小批量梯度下降的表现优于其他两种方法。
小批量梯度下降的代码如下所示。
首先,我们需要定义minibatch
函数:
def minibatch(data, theta, lr = 1e-2, minibatch_ratio = 0.01, num_iterations = 1000):
接下来,我们将通过将数据长度乘以minibatch_ratio
来定义minibatch_size
:
minibatch_size = int(math.ceil(len(data) * minibatch_ratio))
现在,在每次迭代中,我们执行以下操作:
for t in range(num_iterations):
接下来,选择sample_size
:
sample_size = random.sample(range(len(data)), minibatch_size)
np.random.shuffle(data)
现在,基于sample_size
对数据进行抽样:
sample_data = data[0:sample_size[0], :]
计算sample_data
相对于theta
的梯度:
grad = compute_gradients(sample_data, theta)
在计算了给定小批量大小的抽样数据的梯度后,我们按以下方式更新模型参数theta
:
theta = theta - (lr * grad)
return theta
基于动量的梯度下降
在本节中,我们将学习两种新的梯度下降变体,称为动量和 Nesterov 加速梯度。
带有动量的梯度下降
我们在 SGD 和小批量梯度下降中遇到了问题,因为参数更新中存在振荡。看看下面的图表,展示了小批量梯度下降是如何达到收敛的。正如您所见,梯度步骤中存在振荡。振荡由虚线表示。您可能注意到,它朝一个方向迈出梯度步骤,然后朝另一个方向,依此类推,直到达到收敛:
这种振荡是由于我们在迭代每个n个数据点后更新参数,更新方向会有一定的变化,从而导致每个梯度步骤中的振荡。由于这种振荡,很难达到收敛,并且减慢了达到收敛的过程。
为了缓解这个问题,我们将介绍一种称为动量的新技术。如果我们能够理解梯度步骤达到更快收敛的正确方向,那么我们可以使我们的梯度步骤在那个方向导航,并减少不相关方向上的振荡;也就是说,我们可以减少采取不导致收敛的方向。
那么,我们该如何做呢?基本上,我们从前一梯度步骤的参数更新中获取一部分,并添加到当前梯度步骤中。在物理学中,动量在施加力后使物体保持运动。在这里,动量使我们的梯度保持朝向导致收敛的方向运动。
如果您看一下以下方程,您会看到我们基本上是从上一步的参数更新中取参数更新,,并将其添加到当前梯度步骤,
。我们希望从前一个梯度步骤中获取多少信息取决于因子,即,
,以及学习率,用
表示:
在前述方程中, 被称为速度,它加速梯度朝向收敛方向的更新。它还通过在当前步骤中添加来自上一步参数更新的一部分,来减少不相关方向上的振荡。
因此,具有动量的参数更新方程如下表达:
通过这样做,使用动量进行小批量梯度下降有助于减少梯度步骤中的振荡,并更快地达到收敛。
现在,让我们来看看动量的实现。
首先,我们定义momentum
函数,如下所示:
def momentum(data, theta, lr = 1e-2, gamma = 0.9, num_iterations = 1000):
然后,我们用零初始化vt
:
vt = np.zeros(theta.shape[0])
下面的代码用于每次迭代覆盖范围:
for t in range(num_iterations):
现在,我们计算相对于theta
的gradients
:
gradients = compute_gradients(data, theta)
接下来,我们更新vt
为:
vt = gamma * vt + lr * gradients
现在,我们更新模型参数theta
,如下所示:
theta = theta - vt
return theta
Nesterov 加速梯度
动量法的一个问题是可能会错过最小值。也就是说,当我们接近收敛(最小点)时,动量的值会很高。当动量值在接近收敛时很高时,实际上动量会推动梯度步长变高,并且可能错过实际的最小值;也就是说,当动量在接近收敛时很高时,它可能会超出最小值,如下图所示:
为了克服这一问题,Nesterov 引入了一种新的方法,称为Nesterov 加速梯度(NAG)。
Nesterov 动量背后的基本动机是,我们不是在当前位置计算梯度,而是在动量将带我们到的位置计算梯度,我们称之为前瞻位置。
那么,这到底意味着什么呢?在带动量的梯度下降部分,我们了解到以下方程:
上述方程告诉我们,我们基本上是用前一步参数更新的一部分来推动当前的梯度步长vt
到一个新位置,这将帮助我们达到收敛。然而,当动量很高时,这个新位置实际上会超过最小值。
因此,在使用动量进行梯度步进并到达新位置之前,如果我们理解动量将带我们到哪个位置,我们就可以避免超出最小值。如果我们发现动量会带我们到实际错过最小值的位置,那么我们可以减缓动量并尝试达到最小值。
但是我们如何找到动量将带我们到的位置呢?在方程*(2)*中,我们不是根据当前梯度步长vt
计算梯度,而是根据vt
的前瞻位置计算梯度。术语1
基本上告诉我们我们下一个梯度步长的大致位置,我们称之为前瞻位置。这给了我们一个关于下一个梯度步长将在哪里的想法。
因此,我们可以根据 NAG 重新编写我们的方程vt
如下:
我们更新我们的参数如下:
使用上述方程更新参数可以通过在梯度步骤接近收敛时减慢动量来防止错过最小值。Nesterov 加速方法的实现如下。
首先,我们定义NAG
函数:
def NAG(data, theta, lr = 1e-2, gamma = 0.9, num_iterations = 1000):
然后,我们用零初始化vt
的值:
vt = np.zeros(theta.shape[0])
对于每次迭代,我们执行以下步骤:
for t in range(num_iterations):
现在,我们需要计算相对于的梯度:
gradients = compute_gradients(data, theta - gamma * vt)
然后,我们将vt
更新为:
vt = gamma * vt + lr * gradients
现在,我们将模型参数theta
更新为:
theta = theta - vt
return theta
梯度下降的自适应方法
在本节中,我们将学习几种梯度下降的自适应版本。
使用 Adagrad 自适应设置学习率
当我们构建深度神经网络时,会有很多参数。参数基本上是网络的权重,所以当我们构建具有多层的网络时,会有很多权重,比如。我们的目标是找到所有这些权重的最优值。在我们之前学到的所有方法中,网络参数的学习率是一个常见值。然而,Adagrad(自适应梯度的简称)会根据参数自适应地设置学习率。
经常更新或梯度较大的参数将具有较慢的学习率,而更新不频繁或梯度较小的参数也将具有较慢的学习率。但是我们为什么要这样做?这是因为更新不频繁的参数意味着它们没有被充分训练,所以我们为它们设置较高的学习率;而频繁更新的参数意味着它们已经足够训练,所以我们为它们设置较低的学习率,以防止超出最小值。
现在,让我们看看 Adagrad 如何自适应地改变学习率。之前,我们用表示梯度。为简单起见,在本章中,我们将用
代表梯度。因此,参数
在迭代
时的梯度可以表示为:
因此,我们可以用来重新编写我们的更新方程,作为梯度符号表示如下:
现在,对于每次迭代,要更新参数
,我们将学习率除以参数
的所有先前梯度的平方和,如下所示:
在这里, 表示参数
所有先前梯度的平方和。我们添加
只是为了避免除以零的错误。通常将
的值设置为一个小数。这里出现的问题是,为什么我们要把学习率除以所有先前梯度的平方和?
我们了解到,具有频繁更新或高梯度的参数将具有较慢的学习率,而具有不频繁更新或小梯度的参数也将具有较高的学习率。
总和,,实际上缩放了我们的学习率。也就是说,当过去梯度的平方和值较高时,我们基本上将学习率除以一个高值,因此我们的学习率将变得较低。类似地,如果过去梯度的平方和值较低,则我们将学习率除以一个较低的值,因此我们的学习率值将变高。这意味着学习率与参数的所有先前梯度的平方和成反比。
在这里,我们的更新方程表示如下:
简而言之,在 Adagrad 中,当先前梯度值高时,我们将学习率设置为较低的值,当过去梯度值较低时,我们将学习率设置为较高的值。这意味着我们的学习率值根据参数的过去梯度更新而变化。
现在我们已经学习了 Adagrad 算法的工作原理,让我们通过实现它来加深我们的知识。Adagrad 算法的代码如下所示。
首先,定义AdaGrad
函数:
def AdaGrad(data, theta, lr = 1e-2, epsilon = 1e-8, num_iterations = 10000):
定义名为gradients_sum
的变量来保存梯度和,并将它们初始化为零:
gradients_sum = np.zeros(theta.shape[0])
对于每次迭代,我们执行以下步骤:
for t in range(num_iterations):
然后,我们计算损失对theta
的梯度:
gradients = compute_gradients(data, theta)
现在,我们计算梯度平方和,即:
gradients_sum += gradients ** 2
之后,我们计算梯度更新,即:
gradient_update = gradients / (np.sqrt(gradients_sum + epsilon))
现在,更新theta
模型参数,使其为 :
theta = theta - (lr * gradient_update)
return theta
然而,Adagrad 方法存在一个缺点。在每次迭代中,我们都会累积和求和所有过去的平方梯度。因此,在每次迭代中,过去平方梯度值的总和将会增加。当过去平方梯度值的总和较高时,分母中会有一个较大的数。当我们将学习率除以一个非常大的数时,学习率将变得非常小。因此,经过几次迭代后,学习率开始衰减并变成一个无限小的数值——也就是说,我们的学习率将单调递减。当学习率降至一个非常低的值时,收敛需要很长时间。
在下一节中,我们将看到 Adadelta 如何解决这个缺点。
采用 Adadelta 方法摒弃学习率
Adadelta 是 Adagrad 算法的增强版。在 Adagrad 中,我们注意到学习率减小到一个非常低的数字的问题。虽然 Adagrad 能够自适应地学习学习率,但我们仍然需要手动设置初始学习率。然而,在 Adadelta 中,我们根本不需要学习率。那么,Adadelta 算法是如何学习的呢?
在 Adadelta 中,我们不是取所有过去的平方梯度的总和,而是设定一个大小为 的窗口,并仅从该窗口中取过去的平方梯度的总和。在 Adagrad 中,我们取所有过去的平方梯度的总和,并导致学习率减小到一个低数字。为了避免这种情况,我们只从一个窗口内取过去的平方梯度的总和。
如果 是窗口大小,则我们的参数更新方程如下所示:
然而,问题在于,虽然我们仅从一个窗口内取梯度,,但在每次迭代中将窗口内所有梯度进行平方并存储是低效的。因此,我们可以采取梯度的运行平均值,而不是这样做。
我们通过将先前的梯度的运行平均值 和当前梯度
相加来计算迭代 t 的梯度的运行平均值
:
不仅仅是取运行平均值,我们还采取梯度的指数衰减运行平均值,如下所示:
在这里, 被称为指数衰减率,类似于我们在动量中看到的那个——用于决定从前一次梯度的运行平均值中添加多少信息。
现在,我们的更新方程如下所示:
为了简化符号,让我们将记作
,这样我们可以将前一个更新方程重写为如下形式:
根据前述方程,我们可以推断如下:
如果你观察前一个方程中的分母,我们基本上在计算迭代过程中梯度的均方根,,因此我们可以简单地用以下方式写出:
通过将方程 (13) 代入方程 (12),我们可以写出如下:
然而,在我们的方程中仍然有学习率,,术语。我们如何摆脱它?我们可以通过使参数更新的单位与参数相符来实现。正如你可能注意到的那样,
和
的单位并不完全匹配。为了解决这个问题,我们计算参数更新的指数衰减平均值,
,正如我们在方程 (10) 中计算了梯度的指数衰减平均值,
。因此,我们可以写出如下:
它类似于梯度的 RMS,,类似于方程 (13)。我们可以将参数更新的 RMS 写成如下形式:
然而,参数更新的 RMS 值,,是未知的,即
是未知的,因此我们可以通过考虑直到上一个更新,
,来近似计算它。
现在,我们只需用参数更新的 RMS 值取代学习率。也就是说,我们在方程 (14) 中用取代
,并写出如下:
将方程 (15) 代入方程 (11),我们的最终更新方程如下:
现在,让我们通过实现了解 Adadelta 算法。
首先,我们定义AdaDelta
函数:
def AdaDelta(data, theta, gamma = 0.9, epsilon = 1e-5, num_iterations = 1000):
然后,我们将E_grad2
变量初始化为零,用于存储梯度的运行平均值,并将E_delta_theta2
初始化为零,用于存储参数更新的运行平均值,如下所示:
# running average of gradients
E_grad2 = np.zeros(theta.shape[0])
#running average of parameter update
E_delta_theta2 = np.zeros(theta.shape[0])
每次迭代,我们执行以下步骤:
for t in range(num_iterations):
现在,我们需要计算相对于theta
的gradients
:
gradients = compute_gradients(data, theta)
然后,我们可以计算梯度的运行平均值:
E_grad2 = (gamma * E_grad2) + ((1\. - gamma) * (gradients ** 2))
在这里,我们将计算delta_theta
,即,:
delta_theta = - (np.sqrt(E_delta_theta2 + epsilon)) / (np.sqrt(E_grad2 + epsilon)) * gradients
现在,我们可以计算参数更新的运行平均值,:
E_delta_theta2 = (gamma * E_delta_theta2) + ((1\. - gamma) * (delta_theta ** 2))
接下来,我们将更新模型参数theta
,使其变为:
theta = theta + delta_theta
return theta
通过 RMSProp 克服 Adagrad 的限制
与 Adadelta 类似,RMSProp 被引入来解决 Adagrad 的学习率衰减问题。因此,在 RMSProp 中,我们如下计算梯度的指数衰减运行平均值:
而不是取所有过去梯度的平方和,我们使用这些梯度的运行平均值。这意味着我们的更新方程如下:
建议将学习率设置为
0.9
。现在,我们将学习如何在 Python 中实现 RMSProp。
首先,我们需要定义RMSProp
函数:
def RMSProp(data, theta, lr = 1e-2, gamma = 0.9, epsilon = 1e-6, num_iterations = 1000):
现在,我们需要用零来初始化E_grad2
变量,以存储梯度的运行平均值:
E_grad2 = np.zeros(theta.shape[0])
每次迭代时,我们执行以下步骤:
for t in range(num_iterations):
然后,我们计算相对于theta
的gradients
:
gradients = compute_gradients(data, theta)
接下来,我们计算梯度的运行平均值,即,:
E_grad2 = (gamma * E_grad2) + ((1\. - gamma) * (gradients ** 2))
现在,我们更新模型参数theta
,使其变为:
theta = theta - (lr / (np.sqrt(E_grad2 + epsilon)) * gradients)
return theta
自适应矩估计
自适应矩估计,简称Adam,是优化神经网络中最常用的算法之一。在阅读有关 RMSProp 的内容时,我们了解到,为了避免学习率衰减问题,我们计算了平方梯度的运行平均值:
RMSprop 的最终更新方程如下所示:
类似于此,在 Adam 中,我们还计算平方梯度的运行平均值。然而,除了计算平方梯度的运行平均值外,我们还计算梯度的运行平均值。
梯度的运行平均值如下所示:
平方梯度的运行平均值如下所示:
由于许多文献和库将 Adam 中的衰减率表示为 ,而不是
,我们也将使用
来表示 Adam 中的衰减率。因此,在方程式 (16) 和 (17) 中,
和
分别表示梯度运行平均值和平方梯度的指数衰减率。
因此,我们的更新方程式变为以下形式:
梯度运行平均值和平方梯度的运行平均值基本上是这些梯度的第一和第二时刻。也就是说,它们分别是我们梯度的均值和未中心化方差。为了符号简洁起见,让我们将 表示为
,将
表示为
。
因此,我们可以将方程 (16) 和 (17) 重写如下:
我们首先将初始时刻估计设置为零。也就是说,我们用零初始化 和
。当初始估计设置为 0 时,即使经过多次迭代后它们仍然非常小。这意味着它们会偏向 0,特别是当
和
接近 1 时。因此,为了抵消这种影响,我们通过仅将它们除以
来计算
和
的偏差校正估计,如下所示:
在这里, 和
分别是
和
的偏差校正估计。
因此,我们的最终更新方程式如下:
现在,让我们了解如何在 Python 中实现 Adam。
首先,让我们定义 Adam
函数如下:
def Adam(data, theta, lr = 1e-2, beta1 = 0.9, beta2 = 0.9, epsilon = 1e-6, num_iterations = 1000):
然后,我们用 zeros
初始化第一时刻 mt
和第二时刻 vt
:
mt = np.zeros(theta.shape[0])
vt = np.zeros(theta.shape[0])
每次迭代,我们执行以下步骤:
for t in range(num_iterations):
接下来,我们计算相对于 theta
的 gradients
:
gradients = compute_gradients(data, theta)
然后,我们更新第一时刻 mt
,使其为 :
mt = beta1 * mt + (1\. - beta1) * gradients
接下来,我们更新第二矩vt
,使其为:
vt = beta2 * vt + (1\. - beta2) * gradients ** 2
现在,我们计算偏差校正估计的mt
,即:
mt_hat = mt / (1\. - beta1 ** (t+1))
接下来,我们计算偏差校正估计的vt
,即:
vt_hat = vt / (1\. - beta2 ** (t+1))
最后,我们更新模型参数theta
,使其为:
theta = theta - (lr / (np.sqrt(vt_hat) + epsilon)) * mt_hat
return theta
Adamax – 基于无穷范数的 Adam
现在,我们将看一个叫Adamax的 Adam 算法的一个小变体。让我们回忆一下 Adam 中的二阶矩方程:
正如您从上述方程看到的那样,我们将梯度按当前和过去梯度的范数的倒数进行缩放(
范数基本上是值的平方):
而不仅仅是有,我们能将它推广到
范数吗?一般情况下,当我们有大
范数时,我们的更新会变得不稳定。然而,当我们设置
值为
,即
时,
方程变得简单且稳定。我们不仅仅是对梯度参数化,
,我们还对衰减率,
,进行参数化。因此,我们可以写出以下内容:
当我们设置限制时,趋向于无穷大,然后我们得到以下最终方程:
您可以查阅本章末尾列出的进一步阅读部分的论文,了解这是如何推导出来的。
我们可以将上述方程重写为一个简单的递归方程,如下所示:
计算类似于我们在自适应动量估计部分看到的,因此我们可以直接写出以下内容:
通过这样做,我们可以计算偏差校正估计的:
因此,最终的更新方程变为以下形式:
为了更好地理解 Adamax 算法,让我们逐步编写代码。
首先,我们定义Adamax
函数,如下所示:
def Adamax(data, theta, lr = 1e-2, beta1 = 0.9, beta2 = 0.999, epsilon = 1e-6, num_iterations = 1000):
然后,我们用零来初始化第一时刻mt
和第二时刻vt
:
mt = np.zeros(theta.shape[0])
vt = np.zeros(theta.shape[0])
对于每次迭代,我们执行以下步骤:
for t in range(num_iterations):
现在,我们可以计算关于theta
的梯度,如下所示:
gradients = compute_gradients(data, theta)
然后,我们计算第一时刻mt
,如下所示:
mt = beta1 * mt + (1\. - beta1) * gradients
接下来,我们计算第二时刻vt
,如下所示:
vt = np.maximum(beta2 * vt, np.abs(gradients))
现在,我们可以计算mt
的偏差校正估计,即:
mt_hat = mt / (1\. - beta1 ** (t+1))
更新模型参数theta
,使其为:
theta = theta - ((lr / (vt + epsilon)) * mt_hat)
return theta
使用 AMSGrad 的自适应矩估计
Adam 算法的一个问题是有时无法达到最优收敛,或者它达到次优解。已经注意到,在某些情况下,Adam 无法实现收敛,或者达到次优解,而不是全局最优解。这是由于指数移动平均梯度。记得我们在 Adam 中使用梯度的指数移动平均来避免学习率衰减的问题吗?
然而,问题在于由于我们采用梯度的指数移动平均,我们错过了不经常出现的梯度信息。
为了解决这个问题,AMSGrad 的作者对 Adam 算法进行了微小的修改。回想一下我们在 Adam 中看到的二阶矩估计,如下所示:
在 AMSGrad 中,我们使用稍微修改过的的版本。我们不直接使用
,而是取直到前一步的
的最大值,如下所示:
保留了由于指数移动平均而保留信息梯度,而不是逐步淘汰。
因此,我们的最终更新方程式如下所示:
现在,让我们了解如何在 Python 中编写 AMSGrad。
首先,我们定义AMSGrad
函数,如下所示:
def AMSGrad(data, theta, lr = 1e-2, beta1 = 0.9, beta2 = 0.9, epsilon = 1e-6, num_iterations = 1000):
然后,我们初始化第一时刻mt
、第二时刻vt
以及修改后的vt
,即vt_hat
,均为zeros
,如下所示:
mt = np.zeros(theta.shape[0])
vt = np.zeros(theta.shape[0])
vt_hat = np.zeros(theta.shape[0])
对于每次迭代,我们执行以下步骤:
for t in range(num_iterations):
现在,我们可以计算关于theta
的梯度:
gradients = compute_gradients(data, theta)
然后,我们计算第一时刻mt
,如下所示:
mt = beta1 * mt + (1\. - beta1) * gradients
接下来,我们更新第二时刻vt
,如下所示:
vt = beta2 * vt + (1\. - beta2) * gradients ** 2
在 AMSGrad 中,我们使用稍微修改过的的版本。我们不直接使用
,而是取直到前一步的
的最大值。因此,
的实现如下:
vt_hat = np.maximum(vt_hat,vt)
在这里,我们将计算mt
的偏差校正估计,即:
mt_hat = mt / (1\. - beta1 ** (t+1))
现在,我们可以更新模型参数theta
,使其为:
theta = theta - (lr / (np.sqrt(vt_hat) + epsilon)) * mt_hat
return theta
Nadam – 将 NAG 添加到 ADAM 中
Nadam 是 Adam 方法的另一个小扩展。正如其名称所示,在这里,我们将 NAG 合并到 Adam 中。首先,让我们回顾一下我们在 Adam 中学到的内容。
我们按以下方式计算第一和第二时刻:
然后,我们计算第一和第二时刻的偏差校正估计,如下:
我们 Adam 的最终更新方程式表达如下:
现在,我们将看看 Nadam 如何修改 Adam 以使用 Nesterov 动量。在 Adam 中,我们计算第一时刻如下:
我们将这个第一时刻更改为 Nesterov 加速动量。也就是说,我们不再使用先前的动量,而是使用当前的动量,并将其用作前瞻:
我们无法像在 Adam 中计算偏差校正估计那样在这里计算,因为这里的来自当前步骤,而
来自后续步骤。因此,我们改变偏差校正估计步骤如下:
因此,我们可以将我们的第一时刻方程重写为以下形式:
因此,我们的最终更新方程变为以下形式:
现在让我们看看如何在 Python 中实现 Nadam 算法。
首先,我们定义nadam
函数:
def nadam(data, theta, lr = 1e-2, beta1 = 0.9, beta2 = 0.999, epsilon = 1e-6, num_iterations = 500):
然后,我们用零初始化第一时刻mt
和第二时刻vt
:
mt = np.zeros(theta.shape[0])
vt = np.zeros(theta.shape[0])
接下来,我们将beta_prod
设置为1
:
beta_prod = 1
对于每次迭代,我们执行以下步骤:
for t in range(num_iterations):
然后,我们计算相对于theta
的梯度:
gradients = compute_gradients(data, theta)
然后,我们计算第一时刻mt
,使其为:
mt = beta1 * mt + (1\. - beta1) * gradients
现在,我们可以更新第二时刻vt
,使其为:
vt = beta2 * vt + (1\. - beta2) * gradients ** 2
现在,我们计算beta_prod
,即:
beta_prod = beta_prod * (beta1)
接下来,我们计算mt
的偏差校正估计,使其为:
mt_hat = mt / (1\. - beta_prod)
然后,我们计算gt
的偏差校正估计,使其为:
g_hat = grad / (1\. - beta_prod)
从这里开始,我们计算vt
的偏差校正估计,使其为:
vt_hat = vt / (1\. - beta2 ** (t))
现在,我们计算mt_tilde
,使其为:
mt_tilde = (1-beta1**t+1) * mt_hat + ((beta1**t)* g_hat)
最后,我们通过使用来更新模型参数
theta
:
theta = theta - (lr / (np.sqrt(vt_hat) + epsilon)) * mt_hat
return theta
通过这样做,我们学习了用于训练神经网络的各种流行的梯度下降算法变体。执行包含所有回归变体的完整代码的 Jupyter Notebook 可以在bit.ly/2XoW0vH
找到。
摘要
我们从学习什么是凸函数和非凸函数开始本章。然后,我们探讨了如何使用梯度下降找到函数的最小值。我们学习了梯度下降通过计算最优参数来最小化损失函数。后来,我们看了 SGD,其中我们在迭代每个数据点之后更新模型的参数,然后我们学习了小批量 SGD,其中我们在迭代一批数据点之后更新参数。
继续前进,我们学习了如何使用动量来减少梯度步骤中的振荡并更快地达到收敛。在此之后,我们了解了 Nesterov 动量,其中我们不是在当前位置计算梯度,而是在动量将我们带到的位置计算梯度。
我们还学习了 Adagrad 方法,其中我们为频繁更新的参数设置了低学习率,对不经常更新的参数设置了高学习率。接下来,我们了解了 Adadelta 方法,其中我们完全放弃了学习率,而是使用梯度的指数衰减平均值。然后,我们学习了 Adam 方法,其中我们使用第一和第二动量估计来更新梯度。
在此之后,我们探讨了 Adam 的变体,如 Adamax,我们将 Adam 的范数泛化为
,以及 AMSGrad,我们解决了 Adam 达到次优解的问题。在本章的最后,我们学习了 Nadam,其中我们将 Nesterov 动量整合到 Adam 算法中。
在下一章中,我们将学习一种最广泛使用的深度学习算法之一,称为循环神经网络(RNNs),以及如何使用它们来生成歌词。
问题
通过回答以下问题来回顾梯度下降:
-
SGD 与普通梯度下降有什么区别?
-
解释小批量梯度下降。
-
我们为什么需要动量?
-
NAG 背后的动机是什么?
-
Adagrad 如何自适应地设置学习率?
-
Adadelta 的更新规则是什么?
-
RMSProp 如何克服 Adagrad 的局限性?
-
定义 Adam 的更新方程。
进一步阅读
更多信息,请参考以下链接:
-
用于在线学习和随机优化的自适应次梯度方法,作者:John Duchi 等,
www.jmlr.org/papers/volume12/duchi11a/duchi11a.pdf
-
Adadelta:一种自适应学习率方法,作者:Matthew D. Zeiler,
arxiv.org/pdf/1212.5701.pdf
-
Adam:一种用于随机优化的方法,作者:Diederik P. Kingma 和 Jimmy Lei Ba,
arxiv.org/pdf/1412.6980.pdf
-
Adam 及其扩展算法的收敛性研究,作者:Sashank J. Reddi, Satyen Kale, 和 Sanjiv Kumar,
openreview.net/pdf?id=ryQu7f-RZ
-
将 Nesterov 动量整合到 Adam 中,作者:Timothy Dozat,
cs229.stanford.edu/proj2015/054_report.pdf
第四章:使用 RNN 生成歌词
在普通前馈神经网络中,每个输入都是独立的。但是对于序列数据集,我们需要知道过去的输入以进行预测。序列是一组有序的项目。例如,一个句子是一个单词序列。假设我们想要预测句子中的下一个单词;为此,我们需要记住之前的单词。普通的前馈神经网络无法预测出正确的下一个单词,因为它不会记住句子的前面单词。在这种需要记住先前输入的情况下(在这种情况下,我们需要记住先前输入以进行预测),为了进行预测,我们使用递归神经网络(RNNs)。
在本章中,我们将描述如何使用 RNN 对序列数据集进行建模以及如何记住先前的输入。我们将首先研究 RNN 与前馈神经网络的区别。然后,我们将检查 RNN 中的前向传播是如何工作的。
继续,我们将研究时域反向传播(BPTT)算法,该算法用于训练 RNN。随后,我们将讨论梯度消失和爆炸问题,在训练递归网络时会出现这些问题。您还将学习如何使用 TensorFlow 中的 RNN 生成歌词。
在本章末尾,我们将研究不同类型的 RNN 架构,以及它们在各种应用中的使用。
在本章中,我们将学习以下主题:
-
递归神经网络
-
RNN 中的前向传播
-
时域反向传播
-
梯度消失和爆炸问题
-
使用 RNN 生成歌词
-
不同类型的 RNN 架构
引入 RNNs
太阳在 ____ 中升起。
如果我们被要求预测前面句子中的空白处,我们可能会说东方。为什么我们会预测东方是正确的词汇在这里?因为我们读了整个句子,理解了上下文,并预测东方是一个适合完成句子的合适词汇。
如果我们使用前馈神经网络来预测空白,它将不能预测出正确的单词。这是因为在前馈网络中,每个输入都是独立的,并且它们仅基于当前输入进行预测,它们不记住前面的输入。
因此,网络的输入将仅仅是前一个空白处的单词,这个单词是 the。仅凭这个单词作为输入,我们的网络无法预测出正确的单词,因为它不知道句子的上下文,也就是说它不知道前面一组单词来理解句子的语境并预测合适的下一个单词。
这就是我们使用 RNN 的地方。它们不仅基于当前输入预测输出,还基于先前的隐藏状态。为什么它们必须基于当前输入和先前的隐藏状态来预测输出呢?为什么不能只使用当前输入和先前的输入呢?
这是因为前一个输入仅存储有关前一个单词的信息,而前一个隐藏状态将捕捉到网络迄今所见的句子中所有单词的上下文信息。基本上,前一个隐藏状态 acts like a memory,并捕捉句子的上下文。有了这个上下文和当前输入,我们可以预测相关的单词。
例如,让我们拿同样的句子 The sun rises in the ____. 举例。如下图所示,我们首先将单词 the 作为输入传递,然后将下一个单词 sun 作为输入;但与此同时,我们也传递了上一个隐藏状态,。因此,每次传递输入单词时,我们也会传递前一个隐藏状态作为输入。
在最后一步,我们传递单词 the,同时传递前一个隐藏状态 ,它捕捉到了网络迄今为止所见的单词序列的上下文信息。因此,
充当了记忆,存储了网络已经看到的所有先前单词的信息。有了
和当前输入单词 (the),我们可以预测出相关的下一个单词:
简言之,RNN 使用前一个隐藏状态作为记忆,捕捉并存储网络迄今所见的上下文信息(输入)。
RNN 广泛应用于涉及序列数据的用例,如时间序列、文本、音频、语音、视频、天气等等。它们在各种自然语言处理(NLP)任务中被广泛使用,如语言翻译、情感分析、文本生成等。
前馈网络和 RNN 之间的区别
RNN 和前馈网络的比较如下图所示:
正如您可以在前面的图表中观察到的那样,RNN 在隐藏层中包含一个循环连接,这意味着我们使用前一个隐藏状态与输入一起来预测输出。
仍然感到困惑吗?让我们看看下面展开的一个 RNN 版本。但等等,什么是 RNN 的展开版本?
这意味着我们展开网络以完成一个完整的序列。假设我们有一个包含 个单词的输入句子;那么我们将有
到
层,每层对应一个单词,如下图所示:
如您在前图中所见,在时间步 ,基于当前输入
和先前的隐藏状态
预测输出
。同样,在时间步
,基于当前输入
和先前的隐藏状态
,预测
。这就是 RNN 的工作原理;它利用当前输入和先前的隐藏状态来预测输出。
循环神经网络的前向传播
让我们看看循环神经网络如何使用前向传播来预测输出;但在我们深入探讨之前,让我们熟悉一下符号:
前述图示明了以下:
-
表示输入到隐藏层的权重矩阵
-
表示隐藏到隐藏层的权重矩阵
-
表示隐藏到输出层的权重矩阵
在时间步 ,隐藏状态
可以计算如下:
也就是说,时间步 t 的隐藏状态 = tanh([输入到隐藏层权重 x 输入] + [隐藏到隐藏层权重 x 先前隐藏状态])。
在时间步 ,输出可以如下计算:
也就是说,时间步 t 的输出 = softmax(隐藏到输出层权重 x 时间步 t 的隐藏状态)。
我们还可以如下图所示表示循环神经网络(RNN)。正如您所看到的,隐藏层由一个 RNN 块表示,这意味着我们的网络是一个 RNN,并且先前的隐藏状态用于预测输出:
下图显示了 RNN 展开版本中前向传播的工作原理:
我们用随机值初始化初始隐藏状态 。如前图所示,输出
基于当前输入
和先前的隐藏状态(即初始隐藏状态)
使用以下公式预测:
类似地,看看输出 是如何计算的。它使用当前输入
和先前的隐藏状态
:
因此,在前向传播中,为了预测输出,RNN 使用当前输入和先前的隐藏状态。
为了明确起见,让我们看看如何在 RNN 中实现前向传播以预测输出:
- 通过从均匀分布中随机抽取,初始化所有权重
,
,和
:
U = np.random.uniform(-np.sqrt(1.0 / input_dim), np.sqrt(1.0 / input_dim), (hidden_dim, input_dim))
W = np.random.uniform(-np.sqrt(1.0 / hidden_dim), np.sqrt(1.0 / hidden_dim), (hidden_dim, hidden_dim))
V = np.random.uniform(-np.sqrt(1.0 / hidden_dim), np.sqrt(1.0 / hidden_dim), (input_dim, hidden_dim))
- 定义时间步长的数量,这将是我们输入序列的长度
:
num_time_steps = len(x)
- 定义隐藏状态:
hidden_state = np.zeros((num_time_steps + 1, hidden_dim))
- 用零初始化初始隐藏状态
:
hidden_state[-1] = np.zeros(hidden_dim)
- 初始化输出:
YHat = np.zeros((num_time_steps, output_dim))
- 对于每个时间步长,我们执行以下操作:
for t in np.arange(num_time_steps):
#h_t = tanh(UX + Wh_{t-1})
hidden_state[t] = np.tanh(U[:, x[t]] + W.dot(hidden_state[t - 1]))
# yhat_t = softmax(vh)
YHat[t] = softmax(V.dot(hidden_state[t]))
通过时间反向传播
我们刚刚学习了 RNN 中的前向传播如何工作以及如何预测输出。现在,我们计算损失 ,在每个时间步
上,以确定 RNN 预测输出的效果。我们使用交叉熵损失作为我们的损失函数。时间步
处的损失
可以如下给出:
这里, 是实际输出,而
是时间步长为
时的预测输出。
最终损失是所有时间步长上的损失之和。假设我们有 层;那么,最终损失可以如下给出:
如下图所示,最终损失是所有时间步长上损失的总和:
我们计算了损失,现在我们的目标是最小化损失。我们如何最小化损失?我们可以通过找到 RNN 的最优权重来最小化损失。正如我们所学的,RNN 中有三个权重:输入到隐藏的权重 ,隐藏到隐藏的权重
,以及隐藏到输出的权重
。
我们需要找到所有这三个权重的最优值以最小化损失。我们可以使用我们喜爱的梯度下降算法来找到最优权重。我们首先计算损失函数相对于所有权重的梯度,然后根据权重更新规则更新权重,如下所示:
如果您不想理解梯度计算背后的数学,可以跳过接下来的几节。但是,这将有助于您更好地理解循环神经网络中的 BPTT 工作原理。
首先,我们计算损失相对于最终层 的梯度,即
,以便我们可以在接下来的步骤中使用它。
正如我们所学的,时间步骤 处的损失
可以表示如下:
因为我们知道:
我们可以写成:
因此,损失 相对于
的梯度变为:
现在,我们将学习如何逐个计算损失相对于所有权重的梯度。
针对隐藏到输出权重 V 的梯度
首先,让我们回顾一下前向传播涉及的步骤:
假设 ,将其代入方程 (2),我们可以重写上述步骤如下:
在预测输出 后,我们处于网络的最终层。由于我们正在进行反向传播,即从输出层到输入层,我们的第一个权重将是
,即隐藏到输出层的权重。
我们已经看到,最终损失是所有时间步长上的损失之和,类似地,最终梯度是所有时间步长上梯度的总和:
因此,我们可以写成:
回顾我们的损失函数,;我们不能计算相对于的梯度
直接来自
,因为其中没有
项。因此,我们应用链式法则。回顾前向传播方程;在
中有一个
项:
,其中
首先,我们计算损失对的偏导数,然后从
中计算对
的偏导数。从
中,我们可以计算对
的导数。
因此,我们的方程如下:
由于我们知道,损失函数对
的梯度可以计算如下:
将方程*(4)代入方程(3)*,我们可以写成以下形式:
为了更好地理解,让我们逐个从前述方程中取出每个项并逐个计算:
根据方程*(1),我们可以将的值代入前述方程(6)*中,如下所示:
现在,我们将计算项。因为我们知道
,计算
给出 softmax 函数的导数:
softmax 函数的导数可以表示如下:
将方程*(8)代入方程(7)*,我们可以写成以下形式:
因此,最终方程如下:
现在,我们可以将方程*(9)代入方程(5)*:
因为我们知道,我们可以写成:
将前述方程代入方程*(10)*,我们得到我们的最终方程,即损失函数对的梯度如下:
对隐藏到隐藏层权重 W 的梯度
现在,我们将计算损失相对于隐藏到隐藏层权重 的梯度。与
类似,最终的梯度是所有时间步长上梯度的总和:
因此,我们可以写成:
首先,让我们计算损失的梯度 对
的导数,即
。
我们不能直接从中计算 对
的导数,因为其中没有
项。因此,我们使用链式法则计算损失对
的梯度。让我们重新回顾前向传播方程:
首先,我们计算损失 对
的偏导数;然后,从
开始,计算对
的偏导数;然后,从
开始,我们可以计算对 W 的导数,如下所示:
现在,让我们计算损失的梯度 对
的导数,即
。因此,我们再次应用链式法则,得到以下结果:
如果您看看前述等式,我们如何计算项 ?让我们回顾一下
的方程:
正如您在前述等式中所看到的那样,计算 取决于
和
,但
并非常数;它再次是一个函数。因此,我们需要计算其相对于该函数的导数。
然后方程变为:
下图显示了计算 ;我们可以注意到
如何依赖于
:
现在,让我们计算损失函数的梯度,即关于
的梯度。因此,我们再次应用链式法则,得到以下结果:
在前述方程中,我们无法直接计算。回顾方程
:
正如您所观察到的,计算取决于一个函数
,而
再次是取决于函数
的函数。如下图所示,为了计算关于
的导数,我们需要遍历直到
,因为每个函数彼此依赖:
这可以用下图来形象地表示:
这适用于任何时间步长的损失;比如说,。因此,我们可以说,要计算任何损失
,我们需要遍历到
,如下图所示:
这是因为在循环神经网络中,时间的隐藏状态取决于时间
的隐藏状态,这意味着当前隐藏状态始终依赖于先前的隐藏状态。
因此,任何损失可以如下图所示地计算:
因此,我们可以写出损失函数关于
的梯度如下:
在前述方程中,前述方程中的总和意味着所有隐藏状态
的总和。在前述方程中,
可以使用链式法则计算。因此,我们可以说:
假设j=3和k=0;那么,前述方程变为:
将方程*(12)代入方程(11)*将得到以下结果:
我们知道最终损失是所有时间步长上损失的总和:
将方程 (13) 代入前述方程,我们得到以下结果:
在前述方程中,我们有两个求和,其中:
-
暗示了所有时间步长上损失的总和
-
是隐藏状态的总和
因此,我们计算损失关于 W 的梯度的最终方程为:
现在,我们将逐一看如何计算上述方程中的每个术语。从方程 (4) 和方程 (9),我们可以说:
让我们看下一个术语:
我们知道隐藏状态 的计算为:
的导数为
,因此我们可以写成:
让我们来看最后一个术语 。我们知道隐藏状态
的计算如下:
。因此,损失
关于
的导数为:
将所有计算出的项代入方程 (15),我们得到了关于损失梯度 关于
的最终方程如下:
关于隐藏层权重输入的梯度 U
计算损失函数对 的梯度与
相同,因为这里我们也是对
进行顺序导数。类似于
,为了计算任何损失
关于
的导数,我们需要沿着一直回到
。
计算损失关于 的梯度的最终方程如下。正如你所注意到的那样,它基本上与方程 (15) 相同,只是我们有项
而不是显示为
:
我们已经在前一节中看到如何计算前两项。
让我们看看最终项 。我们知道隐藏状态
计算如下,
。因此,
对
的导数推导如下:
因此,我们对损失 对
的最终梯度方程可以写成如下形式:
梯度消失和梯度爆炸问题
我们刚刚学习了 BPTT 的工作原理,看到了如何计算 RNN 中所有权重的损失梯度。但在这里,我们将遇到一个称为梯度消失和梯度爆炸的问题。
在计算损失对 和
的导数时,我们看到我们必须遍历直到第一个隐藏状态,因为每个隐藏状态
都依赖于其前一个隐藏状态
。
例如,损失 对
的梯度给出如下:
如果你看一下前述方程中的项 ,我们无法计算导数
关于 对
的直接推导。正如我们所知,
是一个依赖于
和
的函数。因此,我们也需要计算对
的导数。即使
也是一个依赖于
和
的函数。因此,我们还需要计算对
的导数。
如下图所示,为了计算 的导数,我们需要一直追溯到初始隐藏状态
,因为每个隐藏状态都依赖于其前一个隐藏状态:
因此,为了计算任何损失 ,我们需要一直回溯到初始隐藏状态。
状态 ,因为每个隐藏状态都依赖于其前一个隐藏状态。假设我们有一个具有 50 层的深度递归网络。为了计算损失
,我们需要一直回溯到
,如下图所示:
所以,问题究竟出在哪里?在向初始隐藏状态反向传播时,我们丢失了信息,RNN 将不能完美地反向传播。
记得 吗?每次向后移动时,我们计算
的导数。tanh 的导数被限制在 1. 我们知道,当两个介于 0 和 1 之间的值相乘时,结果将会更小。我们通常将网络的权重初始化为一个小数。因此,当我们在反向传播时乘以导数和权重时,实质上是在乘以较小的数。
当我们在向后移动时,每一步乘以较小的数,我们的梯度变得无限小,导致计算机无法处理的数值;这被称为梯度消失问题。
回顾我们在 关于隐藏层到隐藏层权重 W 的梯度 部分看到的关于损失的梯度方程式:
正如你所看到的,我们在每个时间步长上乘以权重和 tanh 函数的导数。这两者的重复乘法会导致一个很小的数,从而引起梯度消失问题。
梯度消失问题不仅出现在 RNN 中,还出现在其他使用 sigmoid 或 tanh 作为激活函数的深层网络中。因此,为了克服这个问题,我们可以使用 ReLU 作为激活函数,而不是 tanh。
然而,我们有一种称为长短期记忆(LSTM)网络的 RNN 变体,它可以有效地解决梯度消失问题。我们将在第五章 RNN 的改进 中看看它是如何工作的。
同样地,当我们将网络的权重初始化为非常大的数时,在每一步中梯度会变得非常大。在反向传播时,我们在每个时间步长上乘以一个大数,导致梯度爆炸。这被称为梯度爆炸问题。
梯度裁剪
我们可以使用梯度裁剪来避免梯度爆炸问题。在这种方法中,我们根据向量范数(比如 L2 范数)来归一化梯度,并将梯度值裁剪到某个范围内。例如,如果我们将阈值设为 0.7,那么我们保持梯度在 -0.7 到 +0.7 的范围内。如果梯度值超过 -0.7,我们将其更改为 -0.7;同样地,如果超过 0.7,我们将其更改为 +0.7。
假设 是损失函数 L 关于 W 的梯度:
首先,我们使用 L2 范数对梯度进行归一化,即 。如果归一化的梯度超过了定义的阈值,我们更新梯度如下:
使用 RNN 生成歌词
我们已经学习了关于 RNN 的足够知识;现在,让我们看看如何使用 RNN 生成歌词。为此,我们简单地构建一个字符级 RNN,也就是说,在每个时间步,我们预测一个新字符。
让我们考虑一个小句子,What a beautiful d。
在第一个时间步,RNN 预测一个新字符 a。句子将更新为 What a beautiful da.。
在下一个时间步,它预测一个新字符 y,句子变成了 What a beautiful day.。
这样,我们每个时间步预测一个新字符并生成一首歌曲。除了每次预测一个新字符外,我们还可以每次预测一个新单词,这称为词级 RNN。为简单起见,让我们从字符级 RNN 开始。
但是,RNN 如何在每个时间步预测一个新字符呢?假设在时间步 t=0 时,我们输入一个字符 x。现在 RNN 根据给定的输入字符 x 预测下一个字符。为了预测下一个字符,它会预测我们词汇表中所有字符成为下一个字符的概率。一旦得到这个概率分布,我们根据这个概率随机选择下一个字符。有点糊涂吗?让我们通过一个例子更好地理解这个过程。
例如,如下图所示,假设我们的词汇表包含四个字符 L, O, V, 和 E;当我们将字符 L 作为输入时,RNN 计算词汇表中所有单词成为下一个字符的概率:
因此,我们得到概率 [0.0, 0.9, 0.0, 0.1],对应词汇表中的字符 [L,O,V,E]。通过从这个概率分布中抽样来预测下一个字符,给输出增加了一些随机性。
在下一个时间步上,我们将上一时间步预测的字符和先前的隐藏状态作为输入,预测下一个字符,如下图所示:
因此,在每个时间步上,我们将上一时间步预测的字符和先前的隐藏状态作为输入,并预测下一个字符,如下所示:
正如您在前面的图中所见,在时间步 t=2,V 作为输入传递,并预测下一个字符为 E。但这并不意味着每次将字符 V 作为输入发送时都应始终返回 E 作为输出。由于我们将输入与先前的隐藏状态一起传递,RNN 记住了到目前为止看到的所有字符。
因此,先前的隐藏状态捕捉了前面输入字符的精髓,即 L 和 O。现在,使用此前的隐藏状态和输入 V,RNN 预测下一个字符为 E。
在 TensorFlow 中实现
现在,我们将看看如何在 TensorFlow 中构建 RNN 模型来生成歌词。该数据集以及本节中使用的完整代码和逐步说明可以在 GitHub 上的 bit.ly/2QJttyp
获取。下载后,解压缩档案,并将 songdata.csv
放在 data
文件夹中。
导入所需的库:
import warnings
warnings.filterwarnings('ignore')
import random
import numpy as np
import tensorflow as tf
tf.logging.set_verbosity(tf.logging.ERROR)
import warnings
warnings.filterwarnings('ignore')
数据准备
读取下载的输入数据集:
df = pd.read_csv('data/songdata.csv')
让我们看看我们的数据集中有什么:
df.head()
前述代码生成如下输出:
我们的数据集包含约 57,650 首歌曲:
df.shape[0]
57650
我们有约 643
位艺术家的歌词:
len(df['artist'].unique())
643
每位艺术家的歌曲数量如下所示:
df['artist'].value_counts()[:10]
Donna Summer 191
Gordon Lightfoot 189
George Strait 188
Bob Dylan 188
Loretta Lynn 187
Cher 187
Alabama 187
Reba Mcentire 187
Chaka Khan 186
Dean Martin 186
Name: artist, dtype: int64
平均每位艺术家有约 89
首歌曲:
df['artist'].value_counts().values.mean()
89
我们在 text
列中有歌词,因此我们将该列的所有行组合起来,并将其保存为名为 data
的变量中的 text
,如下所示:
data = ', '.join(df['text'])
让我们看看一首歌的几行:
data[:369]
"Look at her face, it's a wonderful face \nAnd it means something special to me \nLook at the way that she smiles when she sees me \nHow lucky can one fellow be? \n \nShe's just my kind of girl, she makes me feel fine \nWho could ever believe that she could be mine? \nShe's just my kind of girl, without her I'm blue \nAnd if she ever leaves me what could I do, what co"
由于我们正在构建字符级 RNN,我们将数据集中所有唯一字符存储在名为 chars
的变量中;这基本上就是我们的词汇表:
chars = sorted(list(set(data)))
将词汇表大小存储在名为 vocab_size
的变量中:
vocab_size = len(chars)
由于神经网络只接受数字输入,因此我们需要将词汇表中的所有字符转换为数字。
我们将词汇表中的所有字符映射到它们的对应索引,形成一个唯一的数字。我们定义了一个 char_to_ix
字典,其中包含所有字符到它们索引的映射。为了通过字符获取索引,我们还定义了 ix_to_char
字典,其中包含所有索引到它们相应字符的映射:
char_to_ix = {ch: i for i, ch in enumerate(chars)}
ix_to_char = {i: ch for i, ch in enumerate(chars)}
如您在下面的代码片段中所见,字符 's'
在 char_to_ix
字典中映射到索引 68
:
print char_to_ix['s']
68
类似地,如果我们将 68
作为输入给 ix_to_char
,那么我们得到相应的字符,即 's'
:
print ix_to_char[68]
's'
一旦我们获得字符到整数的映射,我们使用独热编码将输入和输出表示为向量形式。独热编码向量 基本上是一个全为 0 的向量,除了对应字符索引位置为 1。
例如,假设 vocabSize
是 7
,而字符 z 在词汇表中的第四个位置。那么,字符 z 的独热编码表示如下所示:
vocabSize = 7
char_index = 4
print np.eye(vocabSize)[char_index]
array([0., 0., 0., 0., 1., 0., 0.])
如您所见,我们在对应字符的索引位置有一个 1,其余值为 0。这就是我们将每个字符转换为独热编码向量的方式。
在以下代码中,我们定义了一个名为 one_hot_encoder
的函数,该函数将根据字符的索引返回一个独热编码向量:
def one_hot_encoder(index):
return np.eye(vocab_size)[index]
定义网络参数
接下来,我们定义所有网络参数:
- 定义隐藏层中的单元数:
hidden_size = 100
- 定义输入和输出序列的长度:
seq_length = 25
- 定义梯度下降的学习率:
learning_rate = 1e-1
- 设置种子值:
seed_value = 42
tf.set_random_seed(seed_value)
random.seed(seed_value)
定义占位符
现在,我们将定义 TensorFlow 的占位符:
- 输入和输出的
placeholders
定义如下:
inputs = tf.placeholder(shape=[None, vocab_size],dtype=tf.float32, name="inputs")
targets = tf.placeholder(shape=[None, vocab_size], dtype=tf.float32, name="targets")
- 定义初始隐藏状态的
placeholder
:
init_state = tf.placeholder(shape=[1, hidden_size], dtype=tf.float32, name="state")
- 定义用于初始化 RNN 权重的
initializer
:
initializer = tf.random_normal_initializer(stddev=0.1)
定义前向传播
让我们定义涉及 RNN 的前向传播,数学表达如下:
和
是隐藏层和输出层的偏置项,简单起见,在前面的方程中我们没有添加它们。前向传播可以实现如下:
with tf.variable_scope("RNN") as scope:
h_t = init_state
y_hat = []
for t, x_t in enumerate(tf.split(inputs, seq_length, axis=0)):
if t > 0:
scope.reuse_variables()
#input to hidden layer weights
U = tf.get_variable("U", [vocab_size, hidden_size], initializer=initializer)
#hidden to hidden layer weights
W = tf.get_variable("W", [hidden_size, hidden_size], initializer=initializer)
#output to hidden layer weights
V = tf.get_variable("V", [hidden_size, vocab_size], initializer=initializer)
#bias for hidden layer
bh = tf.get_variable("bh", [hidden_size], initializer=initializer)
#bias for output layer
by = tf.get_variable("by", [vocab_size], initializer=initializer)
h_t = tf.tanh(tf.matmul(x_t, U) + tf.matmul(h_t, W) + bh)
y_hat_t = tf.matmul(h_t, V) + by
y_hat.append(y_hat_t)
对输出应用 softmax
并获取概率:
output_softmax = tf.nn.softmax(y_hat[-1])
outputs = tf.concat(y_hat, axis=0)
计算交叉熵损失:
loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=targets, logits=outputs))
将 RNN 的最终隐藏状态存储在 hprev
中。我们使用这个最终隐藏状态来进行预测:
hprev = h_t
定义 BPTT
现在,我们将执行 BPTT,使用 Adam 作为优化器。我们还将进行梯度裁剪以避免梯度爆炸问题:
- 初始化 Adam 优化器:
minimizer = tf.train.AdamOptimizer()
- 使用 Adam 优化器计算损失的梯度:
gradients = minimizer.compute_gradients(loss)
- 设置梯度裁剪的阈值:
threshold = tf.constant(5.0, name="grad_clipping")
- 裁剪超过阈值的梯度并将其带入范围内:
clipped_gradients = []
for grad, var in gradients:
clipped_grad = tf.clip_by_value(grad, -threshold, threshold)
clipped_gradients.append((clipped_grad, var))
- 使用裁剪后的梯度更新梯度:
updated_gradients = minimizer.apply_gradients(clipped_gradients)
开始生成歌曲
开始 TensorFlow 会话并初始化所有变量:
sess = tf.Session()
init = tf.global_variables_initializer()
sess.run(init)
现在,我们将看看如何使用 RNN 生成歌词。RNN 的输入和输出应该是什么?它是如何学习的?训练数据是什么?让我们逐步理解这一解释,并附上代码。
我们知道在 RNN 中,在时间步 预测的输出将作为下一个时间步的输入;也就是说,每个时间步,我们需要将前一个时间步预测的字符作为输入。因此,我们以同样的方式准备我们的数据集。
例如,请看下表。假设每一行代表一个不同的时间步;在时间步 ,RNN 预测一个新字符 g 作为输出。这将作为下一个时间步
的输入发送。
然而,如果你注意到时间步 中的输入,我们从输入
o
中删除了第一个字符,并在序列末尾添加了新预测的字符 g。为什么要删除输入中的第一个字符?因为我们需要保持序列长度。
假设我们的序列长度是八;将新预测的字符添加到序列中会将序列长度增加到九。为了避免这种情况,我们从输入中移除第一个字符,同时将前一个时间步的新预测字符添加进来。
类似地,在输出数据中,我们也会在每个时间步删除第一个字符,因为一旦预测了新字符,序列长度就会增加。为了避免这种情况,在每个时间步从输出中删除第一个字符,如下表所示:
现在,我们将看看如何准备我们的输入和输出序列,类似于前面的表格。
定义一个名为 pointer
的变量,它指向数据集中的字符。我们将 pointer
设置为 0
,这意味着它指向第一个字符:
pointer = 0
定义输入数据:
input_sentence = data[pointer: pointer + seq_length]
这是什么意思?使用指针和序列长度切片数据。假设 seq_length
是 25
,指针是 0
。它将返回前 25 个字符作为输入。因此,data[pointer:pointer + seq_length]
返回以下输出:
"Look at her face, it's a "
定义输出,如下所示:
output_sentence = data[pointer + 1: pointer + seq_length + 1]
我们将输出数据切片,从输入数据移动一个字符。因此,data[pointer + 1:pointer + seq_length + 1]
返回以下内容:
"ook at her face, it's a w"
正如你所看到的,我们在前一句中添加了下一个字符并删除了第一个字符。因此,每次迭代时,我们增加指针并遍历整个数据集。这就是我们为训练 RNN 获取输入和输出句子的方法。
就像我们学到的那样,RNN 仅接受数字作为输入。一旦我们切片了输入和输出序列,我们使用之前定义的 char_to_ix
字典获取相应字符的索引:
input_indices = [char_to_ix[ch] for ch in input_sentence]
target_indices = [char_to_ix[ch] for ch in output_sentence]
使用我们之前定义的 one_hot_encoder
函数将索引转换为 one-hot 编码向量:
input_vector = one_hot_encoder(input_indices)
target_vector = one_hot_encoder(target_indices)
这 input_vector
和 target_vector
成为训练 RNN 的输入和输出。现在,让我们开始训练。
hprev_val
变量存储了我们训练的 RNN 模型的最后隐藏状态,我们用它来进行预测,并将损失存储在名为loss_val
的变量中:
hprev_val, loss_val, _ = sess.run([hprev, loss, updated_gradients], feed_dict={inputs: input_vector,targets: target_vector,init_state: hprev_val})
我们训练模型进行n次迭代。训练后,我们开始进行预测。现在,我们将看看如何进行预测并使用我们训练的 RNN 生成歌词。设置sample_length
,即我们想要生成的句子(歌曲)的长度:
sample_length = 500
随机选择输入序列的起始索引:
random_index = random.randint(0, len(data) - seq_length)
选择具有随机选择索引的输入句子:
sample_input_sent = data[random_index:random_index + seq_length]
正如我们所知,我们需要将输入作为数字进行馈送;将所选输入句子转换为索引:
sample_input_indices = [char_to_ix[ch] for ch in sample_input_sent]
记住,我们将 RNN 的最后隐藏状态存储在hprev_val
中。我们使用它来进行预测。我们通过从hprev_val
复制值来创建一个名为sample_prev_state_val
的新变量。
sample_prev_state_val
用作进行预测的初始隐藏状态:
sample_prev_state_val = np.copy(hprev_val)
初始化用于存储预测输出索引的列表:
predicted_indices = []
现在,对于t
在sample_length
的范围内,我们执行以下操作并为定义的sample_length
生成歌曲:
将sampled_input_indices
转换为 one-hot 编码向量:
sample_input_vector = one_hot_encoder(sample_input_indices)
将sample_input_vector
和初始隐藏状态sample_prev_state_val
馈送给 RNN,并获得预测。我们将输出概率分布存储在probs_dist
中:
probs_dist, sample_prev_state_val = sess.run([output_softmax, hprev],
feed_dict={inputs: sample_input_vector,init_state: sample_prev_state_val})
使用由 RNN 生成的概率分布随机选择下一个字符的索引:
ix = np.random.choice(range(vocab_size), p=probs_dist.ravel())
将新预测的索引ix
添加到sample_input_indices
中,并从sample_input_indices
中删除第一个索引以保持序列长度。这将形成下一个时间步的输入:
sample_input_indices = sample_input_indices[1:] + [ix]
存储所有预测的chars
索引在predicted_indices
列表中:
predicted_indices.append(ix)
将所有的predicted_indices
转换为它们对应的字符:
predicted_chars = [ix_to_char[ix] for ix in predicted_indices]
将所有的predicted_chars
组合起来,并保存为text
:
text = ''.join(predicted_chars)
在每 50,000^(th)次迭代时打印预测文本:
print ('\n')
print (' After %d iterations' %(iteration))
print('\n %s \n' % (text,))
print('-'*115)
增加pointer
和iteration
:
pointer += seq_length
iteration += 1
在初始迭代中,您可以看到 RNN 生成了随机字符。但是在第 50,000^(th)次迭代中,它开始生成有意义的文本:
After 0 iterations
Y?a6C.-eMSfk0pHD v!74YNeI 3YeP,h- h6AADuANJJv:HA(QXNeKzwCjBnAShbavSrGw7:ZcSv[!?dUno Qt?OmE-PdY wrqhSu?Yvxdek?5Rn'Pj!n5:32a?cjue ZIj
Xr6qn.scqpa7)[MSUjG-Sw8n3ZexdUrLXDQ:MOXBMX EiuKjGudcznGMkF:Y6)ynj0Hiajj?d?n2Iapmfc?WYd BWVyB-GAxe.Hq0PaEce5H!u5t: AkO?F(oz0Ma!BUMtGtSsAP]Oh,1nHf5tZCwU(F?X5CDzhOgSNH(4Cl-Ldk? HO7 WD9boZyPIDghWUfY B:r5z9Muzdw2'WWtf4srCgyX?hS!,BL GZHqgTY:K3!wn:aZGoxr?zmayANhMKJsZhGjpbgiwSw5Z:oatGAL4Xenk]jE3zJ?ymB6v?j7(mL[3DFsO['Hw-d7htzMn?nm20o'?6gfPZhBa
NlOjnBd2n0 T"d'e1k?OY6Wwnx6d!F
----------------------------------------------------------------------------------------------
After 50000 iterations
Hem-:]
[Ex" what
Akn'lise
[Grout his bring bear.
Gnow ourd?
Thelf
As cloume
That hands, Havi Musking me Mrse your leallas, Froking the cluse (have: mes.
I slok and if a serfres me the sky withrioni flle rome.....Ba tut get make ome
But it lives I dive.
[Lett it's to the srom of and a live me it's streefies
And is.
As it and is me dand a serray]
zrtye:"
Chay at your hanyer
[Every rigbthing with farclets
[Brround.
Mad is trie
[Chare's a day-Mom shacke?
, I
-------------------------------------------------------------------------------------------------
不同类型的 RNN 架构
现在我们已经了解了 RNN 的工作原理,我们将看看基于输入和输出数量的不同类型的 RNN 架构。
一对一架构
在一对一架构中,单个输入映射到单个输出,时间步t的输出作为下一个时间步的输入。我们已经在最后一节中看到了这种架构,用于使用 RNN 生成歌曲。
例如,在文本生成任务中,我们将当前时间步生成的输出作为下一个时间步的输入,以生成下一个单词。这种架构在股票市场预测中也被广泛使用。
下图展示了一对一的 RNN 架构。如您所见,时间步t预测的输出被发送为下一个时间步的输入:
多对一架构
一个多对一架构,顾名思义,接受一个输入序列并将其映射到单个输出值。多对一架构的一个流行示例是情感分类。一句话是一个单词序列,因此在每个时间步,我们将每个单词作为输入传递,并在最终时间步预测输出。
假设我们有一个句子:Paris is a beautiful city. 如下图所示,在每个时间步,一个单词作为输入传递,并且在最终时间步预测句子的情感:
多对多架构
在多对多架构中,我们将任意长度的输入序列映射到任意长度的输出序列。这种架构已被用于各种应用中。多对多架构的一些流行应用包括语言翻译、对话机器人和音频生成。
假设我们正在将一句英语句子转换成法语。考虑我们的输入句子:What are you doing? 如下图所示,它将映射为Que faites vous:
总结
我们首先介绍了什么是 RNN,以及 RNN 与前馈网络的区别。我们了解到 RNN 是一种特殊类型的神经网络,广泛应用于序列数据;它根据当前输入和先前的隐藏状态预测输出,隐藏状态充当记忆,存储到目前为止网络所见到的信息序列。
我们学习了 RNN 中的前向传播工作原理,然后详细推导了用于训练 RNN 的 BPTT 算法。接着,我们通过在 TensorFlow 中实现 RNN 来生成歌词。在本章末尾,我们了解了 RNN 的不同架构,如一对一、一对多、多对一和多对多,它们被用于各种应用中。
在下一章中,我们将学习关于 LSTM 单元的内容,它解决了 RNN 中的梯度消失问题。我们还将学习不同变体的 RNN。
问题
尝试回答以下问题:
-
RNN 与前馈神经网络有什么区别?
-
RNN 中的隐藏状态是如何计算的?
-
递归网络有什么用途?
-
梯度消失问题是如何发生的?
-
什么是爆炸梯度问题?
-
梯度裁剪如何缓解爆炸梯度问题?
-
不同类型的 RNN 架构有哪些?
进一步阅读
参考以下链接了解更多关于 RNN 的内容:
-
循环神经网络(RNN)和长短期记忆(LSTM)网络基础,作者 Alex Sherstinsky,
arxiv.org/pdf/1808.03314.pdf
-
利用 TensorFlow 进行手写生成的 RNN,基于 Alex Graves 的用循环神经网络生成序列,
github.com/snowkylin/rnn-handwriting-generation