月度归档:2020年11月

第1章 NumPy基础

在机器学习和深度学习中,图像、声音、文本等输入数据最终都要转换为数组或矩阵。如何有效进行数组和矩阵的运算?这就需要充分利用NumPy。NumPy是数据科学的通用语言,而且与Pytorch关系非常密切,它是科学计算、深度学习的基石。尤其对Pytorch而言,其重要性更加明显。Pytorch中的Tensor与NumPy非常相似,它们之间可以非常方便地进行转换,掌握NumPy是学好Pytorch的重要基础,故我们把它列为全书第1章。
为什么是NumPy?实际上Python本身含有列表(list)和数组(array),但对于大数据来说,这些结构有很多不足。因列表的元素可以是任何对象,因此列表中所保存的是对象的指针。例如为了保存一个简单的[1,2,3],都需要有3个指针和三个整数对象。对于数值运算来说这种结构显然比较浪费内存和CPU等宝贵资源。 至于array对象,它直接保存数值,和C语言的一维数组比较类似。但是由于它不支持多维,在上面的函数也不多,因此也不适合做数值运算。
NumPy(Numerical Python 的简称)的诞生弥补了这些不足,NumPy提供了两种基本的对象:ndarray(N-dimensional array object)和 ufunc(universal function object)。ndarray是存储单一数据类型的多维数组,而ufunc则是能够对数组进行处理的函数。
NumPy的主要特点:
(1)ndarray,快速和节省空间的多维数组,提供数组化的算术运算和高级的广播功能。
(2)使用标准数学函数对整个数组的数据进行快速运算,而不需要编写循环。
(3)读取/写入磁盘上的阵列数据和操作存储器映像文件的工具。
(4)线性代数,随机数生成,和傅里叶变换的能力。
(5)集成C,C++,Fortran代码的工具。
本章主要内容如下:
 如何生成NumPy数组
 如何存取元素
 NumPy的算术运算
 数组变形
 批量处理
 NumPy的通用函数
 NumPy的广播机制

1.1 生成NumPy数组

NumPy是Python的外部库,不在标准库中。因此,若要使用它,需要先导入NumPy。

导入NumPy后,可通过np.+Tab键,查看可使用的函数,如果对其中一些函数的使用不很清楚,还可以在对应函数+?,再运行,就可很方便的看到如何使用函数的帮助信息。
np.然后按'Tab'键,将出现如下界面:

运行如下命令,便可查看函数abs的详细帮助信息。

NumPy不但强大,而且还非常友好。
接下来我们将介绍NumPy的一些常用方法,尤其是与机器学习、深度学习相关的一些内容。
NumPy封装了一个新的数据类型ndarray(n-dimensional array),它是一个多维数组对象。该对象封装了许多常用的数学运算函数,方便我们做数据处理、数据分析等。如何生成ndarray呢?这里我们介绍生成ndarray的几种方式,如从已有数据中创建、利用random创建、创建特殊多维数组、使用arange函数等。

1.1.1 从已有数据中创建数组

直接对 Python 的基础数据类型(如列表、元组等) 进行转换来生成 ndarray:
(1)将列表转换成 ndarray

(2)嵌套列表可以转换成多维 ndarray

如果把上面示例中的列表换成元组也同样适合。

1.1.2 利用 random 模块生成数组

在深度学习中,我们经常需要对一些参数进行初始化,为了更有效训练模型,提高模型的性能,有些初始化还需要满足一定条件,如满足正态分布或均匀分布等。这里我们介绍几种常用的方法,表1-1列举了 np.random 模块常用的函数。
表1-1 np.random模块常用函数

下面我们来看看一些函数的具体使用:

为了每次生成同一份数据,可以指定一个随机种子,使用shuffle函数打乱生成的随机数。

输出结果:
[[-1.0856306 0.99734545 0.2829785 ]
[-1.50629471 -0.57860025 1.65143654]]
随机打乱后数据:
[[-1.50629471 -0.57860025 1.65143654]
[-1.0856306 0.99734545 0.2829785 ]]

1.1.3 创建特定形状的多维数组

参数初始化时,有时需要生成一些特殊矩阵,如全是 0 或 1 的数组或矩阵,这时我们可以利用np.zeros、np.ones、np.diag来实现,如表1-2所示。
表1-2 NumPy 数组创建函数

下面我们通过几个示例来说明:

有时我们可能需要把生成的数据暂时保存起来,以备后续使用。

输出结果:
[[0.41092437 0.5796943 0.13995076 0.40101756 0.62731701]
[0.32415089 0.24475928 0.69475518 0.5939024 0.63179202]
[0.44025718 0.08372648 0.71233018 0.42786349 0.2977805 ]
[0.49208478 0.74029639 0.35772892 0.41720995 0.65472131]
[0.37380143 0.23451288 0.98799529 0.76599595 0.77700444]]

1.1.4 利用 arange、linspace 函数生成数组

arange 是 numpy 模块中的函数,其格式为:

其中start 与 stop 指定范围,step 设定步长,生成一个 ndarray,start 默认为 0,步长 step 可为小数。Python有个内置函数range功能与此类似。

linspace 也是 numpy 模块中常用的函数,其格式为:

它可以根据输入的指定数据范围以及等份数量,自动生成一个线性等分向量,其中endpoint (包含终点)默认为 True,等分数量num默认为 50。如果将retstep设置为 True,则会返回一个带步长的 ndarray。

值得一提的,这里并没有像我们预期的那样,生成 0.1, 0.2, ... 1.0 这样步长为0.1的 ndarray,这是因为 linspace 必定会包含数据起点和终点,那么其步长则为(1-0) / 9 = 0.11111111。如果需要产生 0.1, 0.2, ... 1.0 这样的数据,只需要将数据起点 0 修改为 0.1 即可。
除了上面介绍到的 arange 和 linspace,NumPy还提供了 logspace 函数,该函数使用方法与 linspace 使用方法一样,读者不妨自己动手试一下。

1.2 获取元素

上节我们介绍了生成ndarray的几种方法,数据生成后,如何读取我们需要的数据?这节我们介绍几种常用方法。

如果对上面这些获取方式还不是很清楚,没关系,下面我们通过图形的方式进一步说明,如图1-1所示,左边为表达式,右边为表达式获取的元素。注意不同的边界,表示不同的表达式。

图1-1 获取多维数组中的元素
获取数组中的部分元素除通过指定索引标签外,还可以使用一些函数来实现,如通过random.choice函数可以从指定的样本中进行随机抽取数据。

打印结果:
随机可重复抽取
[[ 7. 22. 19. 21.]
[ 7. 5. 5. 5.]
[ 7. 9. 22. 12.]]
随机但不重复抽取
[[ 21. 9. 15. 4.]
[ 23. 2. 3. 7.]
[ 13. 5. 6. 1.]]
随机但按制度概率抽取
[[ 15. 19. 24. 8.]
[ 5. 22. 5. 14.]
[ 3. 22. 13. 17.]]

1.3 NumPy的算术运算

在机器学习和深度学习中,涉及大量的数组或矩阵运算,这节我们重点介绍两种常用的运算。一种是对应元素相乘,又称为逐元乘法(element-wise product),运算符为np.multiply(), 或 *。另一种是点积或内积元素,运算符为np.dot()。

1.3.1对应元素相乘

对应元素相乘(element-wise product)是两个矩阵中对应元素乘积。np.multiply 函数用于数组或矩阵对应元素相乘,输出与相乘数组或矩阵的大小一致,其格式如下:

其中x1,x2之间的对应元素相乘遵守广播规则,NumPy的广播规则本章第7小节将介绍。以下我们通过一些示例来进一步说明。

矩阵A和B的对应元素相乘,用图1-2直观表示为:

图1-2 对应元素相乘示意图
NumPy数组不仅可以和数组进行对应元素相乘,也可以和单一数值(或称为标量)进行运算。运算时,NumPy数组每个元素和标量进行运算,其间会用到广播机制(1.7小节将详细介绍)。

[[ 2. 4.]
[-2. 8.]]
[[ 0.5 1. ]
[-0.5 2. ]]
由此,推而广之,数组通过一些激活函数后,输出与输入形状一致。

输入参数X的形状: (2, 3)
激活函数softmoid输出形状: (2, 3)
激活函数relu输出形状: (2, 3)
激活函数softmax输出形状: (2, 3)

1.3.2 点积运算

点积运算(dot product)又称为内积,在NumPy用np.dot表示,其一般格式为:

以下通过一个示例来说明dot的具体使用及注意事项。

[[21 24 27]
[47 54 61]]
以上运算,用图1-3可表示为:

图1-3 矩阵的点积示意图,对应维度的元素个数需要保持一致
如图1-3所示,矩阵X1和矩阵X2进行点积运算,其中X1和X2对应维度(即X1的第2个维度与X2的第1个维度)的元素个数必须保持一致,此外,矩阵X3的形状是由矩阵X1的行数与矩阵X2的列数构成的。

1.4 数组变形

在机器学习以及深度学习的任务中,通常需要将处理好的数据以模型能接受的格式喂给模型,然后模型通过一系列的运算,最终返回一个处理结果。然而,由于不同模型所接受的输入格式不一样,往往需要先对其进行一系列的变形和运算,从而将数据处理成符合模型要求的格式。最常见的是矩阵或者数组的运算,经常会遇到需要把多个向量或矩阵按某轴方向合并,或需要展平(如在卷积或循环神经网络中,在全连接层之前,需要把矩阵展平)。下面介绍几种常用数据变形方法。

1.4.1 更改数组的形状

修改指定数组的形状是 NumPy 中最常见的操作之一,常见的方法有很多,表1-3 列出了一些常用函数。
表1-3 NumPy中改变向量形状的一些函数

下面我们来看一些示例:
(1)reshape

输出结果:
[0 1 2 3 4 5 6 7 8 9]
[[0 1 2 3 4]
[5 6 7 8 9]]
[[0 1]
[2 3]
[4 5]
[6 7]
[8 9]]
[[0 1 2 3 4]
[5 6 7 8 9]]
值得注意的是,reshape 函数支持只指定行数或列数,剩余的设置为-1即可。所指定的行数或列数一定要能被整除(如10不能被3整除),例如上面代码如果修改为arr.reshape(3,-1)将报错误。
(2)resize

输出结果:
[0 1 2 3 4 5 6 7 8 9]
[[0 1 2 3 4]
[5 6 7 8 9]]
(3).T

输出结果:
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
[[ 0 4 8]
[ 1 5 9]
[ 2 6 10]
[ 3 7 11]]
(4)ravel

输出结果:
[[0 1 2]
[3 4 5]]
按照列优先,展平
[0 3 1 4 2 5]
按照行优先,展平
[0 1 2 3 4 5]
(5)flatten
把矩阵转换为向量,这种需求经常出现在卷积网络与全连接层之间。

输出结果:
[[4. 0. 8. 5.]
[1. 0. 4. 8.]
[8. 2. 3. 7.]]
[4. 0. 8. 5. 1. 0. 4. 8. 8. 2. 3. 7.]
(6)squeeze
这是一个重要用来降维的函数,把矩阵中含1的维度去掉。在Pytorch中还有一种与之相反的操作,torch.unsqueeze这个后面将介绍。

(7)transpose
对高维矩阵进行轴对换,这个在深度学习中经常使用,比如把图片表示颜色的RGB顺序,改为GBR的顺序。

1.4.2 合并数组

合并数组也是最常见的操作之一,表1-4列举了常用的用于数组或向量合并的方法。
表1-4 NumPy 数组合并方法

[说明]
①append、concatnate以及stack都有一个 axis 参数,用于控制数组合并是按行还是按列。
②对于append和concatnate,待合并的数组必须有相同的行数或列数(满足一个即可)。
③stack、hstack、dstack待合并的数组必须具有相同的形状( shape)。
下面选择一些常用函数进行说明。
(1)append
合并一维数组

合并多维数组

输出结果:
按行合并后的结果
[[0 1]
[2 3]
[0 1]
[2 3]]
合并后数据维度 (4, 2)
按列合并后的结果
[[0 1 0 1]
[2 3 2 3]]
合并后数据维度 (2, 4)
(2)concatenate
沿指定轴连接数组或矩阵

输出结果:
[[1 2]
[3 4]
[5 6]]
[[1 2 5]
[3 4 6]]
(3)stack
沿指定轴堆叠数组或矩阵

输出结果:
[[[1 2]
[3 4]]

[[5 6]
[7 8]]]

1.5 批量处理

在深度学习中,由于源数据都比较大,所以通常需要采用批处理。如利用批量来计算梯度的随机梯度法(SGD),就是一个典型应用。深度学习的计算一般比较复杂,加上数据量一般比较大,如果一次处理整个数据,往往出现资源瓶颈。为了更有效的计算,一般将整个数据集分成小批量。与处理整个数据集的另一个极端是每次处理一条记录,这种方法也不科学,一次处理一条记录无法充分发挥GPU、NumPy平行处理优势。因此,实际使用中往往采用批量处理(mini-batch)。
如何把大数据拆分成多个批次呢?可采用如下步骤:
(1)得到数据集
(2)随机打乱数据
(3)定义批大小
(4)批处理数据集
以下我们通过一个示例来具体说明:

最后5行结果:
第9500批次,该批次的数据之和:17.63702580438092
第9600批次,该批次的数据之和:-1.360924607368387
第9700批次,该批次的数据之和:-25.912226239266445
第9800批次,该批次的数据之和:32.018136957835814
第9900批次,该批次的数据之和:2.9002576614446935
【说明】
批次从0开始,所以最后一个批次是9900。

1.6 通用函数

NumPy提供了两种基本的对象,即ndarray和ufunc对象。前面我们介绍了ndarray,本节将介绍NumPy的另一个对象通用函数(ufunc),ufunc是universal function的缩写,它是一种能对数组的每个元素进行操作的函数。许多ufunc函数都是用c语言级别实现的,因此它们的计算速度非常快。此外,它们比math模块中函数更灵活。math模块的输入一般是标量,但NumPy中函数可以是向量或矩阵,而利用向量或矩阵可以避免使用循环语句,这点在机器学习、深度学习中非常重要。表1-5为NumPy常用的几个通用函数。
表1-5 NumPy几个常用通用函数

(1)math与numpy函数的性能比较:

打印结果
math.sin: 0.5169950000000005
numpy.sin: 0.05381199999999886
由此可见,numpy.sin比math.sin快近10倍。
(2)循环与向量运算比较:
充分使用Python的NumPy库中的内建函数(built-in function),实现计算的向量化,可大大提高运行速度。NumPy库中的内建函数使用了SIMD指令。如下使用的向量化要比使用循环计算速度快得多。如果使用GPU,其性能将更强大,不过NumPy不支持GPU。Pytorch支持GPU,第5章将介绍Pytorch如何使用GPU来加速算法。

输出结果
dot = 250215.601995
for loop----- Computation time = 798.3389819999998ms
dot = 250215.601995
verctor version---- Computation time = 1.885051999999554ms
从运行结果上来看,使用for循环的运行时间大约是向量运算的400倍。因此,深度学习算法中,一般都使用向量化矩阵运算。

1.7 广播机制

NumPy的Universal functions 中要求输入的数组shape是一致的,当数组的shape不相等的时候,则会使用广播机制。不过,调整数组使得shape一样,需满足一定规则,否则将出错。这些规则可归结为以下四条:
(1)让所有输入数组都向其中shape最长的数组看齐,shape中不足的部分都通过在前面加1补齐;
如:a:2x3x2 b:3x2,则b向a看齐,在b的前面加1:变为:1x3x2
(2)输出数组的shape是输入数组shape的各个轴上的最大值;
(3)如果输入数组的某个轴和输出数组的对应轴的长度相同或者其长度为1时,这个数组能够用来计算,否则出错;
(4)当输入数组的某个轴的长度为1时,沿着此轴运算时都用(或复制)此轴上的第一组值。
广播在整个NumPy中用于决定如何处理形状迥异的数组;涉及算术运算包括(+,-,*,/…)。这些规则说的很严谨,但不直观,下面我们结合图形与代码进一步说明:
目的:A+B
其中A为4x1矩阵,B为一维向量 (3,)
要相加,需要做如下处理:
(1)根据规则1,B需要向看齐,把B变为(1,3)
(2)根据规则2,输出的结果为各个轴上的最大值,即输出结果应该为(4,3)矩阵
那么A如何由(4,1)变为(4,3)矩阵?B如何由(1,3)变为(4,3)矩阵?
3)根据规则4,用此轴上的第一组值(要主要区分是哪个轴),进行复制(但在实际处理中不是真正复制,否则太耗内存,而是采用其它对象如ogrid对象,进行网格处理)即可,
详细处理如图1-4所示。

图1-4 NumPy广播规则示意图
代码实现

运行结果
A矩阵的形状:(4, 1),B矩阵的形状:(3,)
C矩阵的形状:(4, 3)
[[ 0 1 2]
[10 11 12]
[20 21 22]
[30 31 32]]

1.8 小结

本章主要介绍了NumPy模块的常用操作,尤其涉及对矩阵的操作,这些操作在后续程序中经常使用。NumPy内容很丰富,这里只列了一些主要内容,如果你想了解更多内容,可登录NumPy官网:http://www.numpy.org/

3.4 优化RNN


RNN擅长处理序列数据,尤其当后面数据与前面数据有依赖关系时。不过,如果这种依赖涉及长依赖的话,使用RNN的效果将受到较大影响,因长依赖就意味着时间步就要大,而根据梯度的关联规则,会发现累乘会导致激活函数导数的累乘,如果取tanh或sigmoid函数作为激活函数的话,那么必然是一堆小数在做乘法,结果就是越乘越小。随着时间序列的不断深入,小数的累乘就会导致梯度越来越小直到接近于0,这就是“梯度消失“现象。实际使用中,会优先选择tanh函数,原因是tanh函数相对于sigmoid函数来说梯度较大,收敛速度更快且引起梯度消失更慢。通常减缓梯度消失的几种方法:
1、选取更好的激活函数,如Relu激活函数。ReLU函数的左侧导数为0,右侧导数恒为1,这就避免了“梯度消失“的发生。但大于1的导数容易导致“梯度爆炸“,但设定合适的阈值可以解决这个问题。
2、加入BN层,其优点包括可加速收敛、控制过拟合,可以少用或不用Dropout和正则、降低网络对初始化权重不敏感,且能允许使用较大的学习率等。
3、改变传播结构,LSTM结构可以有效解决这个问题。接下来我们将介绍LSTM相关内容。
这些方法中,LSTM比其他几种方法效果要好,LSTM的核心思想就是增加一条记忆以往信息的传送带,这条传送带通过各种更新信息相加,而从有效避免梯度消失问题。
LSTM成功用于许多应用(如语言建模,手势预测,用户建模)。 LSTM基于记忆的序列建模架构非常有影响力——它启发了许多最新的改进方法,例如Transformers。

3.4.1 LSTMCell

1.RNN与LSTM的关系图

 

图1-14 RNN与LSTM的关系
从图1-14 可知,RNN对应LSTM中方框部分,方框部分的信息就是当前状态信息,该信息将通过输入门之后加入到传送带C中。

2.LSTMCell整体架构图
LSTM 通过精心设计的称作为“门”的结构来去除或者增加信息到传送带C状态的能力。门是一种让信息选择式通过的方法。他们通过含激活 sigmoid 神经网络层形成门(如图1-15中等信息进行过滤。

图1-15 LSTM架构图
LSTM三个门的主要功能:

输入门对当前信息C ̃_t进行过滤,其表示式为:

2.LSTMCell的详细结构

图1-16 LSTM的详细内部结构图
为简明起见,图1-16中假设输入为3个元素的向量,隐藏状态及传送信息都是2个元素的向量。隐藏状态与输入拼接成5个元素的向量。实际应用中输入、状态一般加上批量等维度,如[batch,input_size],如果在LSTM模块中还需加上序列长度,形如[batch,seg_len,input_size]。multiplication是指哈达玛积,S表示Softmoid,T表示Tanh。

3.4.2 LSTMCell

LSTMCell这是LSTM的一个单元,如图1-15所示。该单元可以用PyTorch的 nn.LSTMCell模块来实现,该模块构建LSTM中的一个Cell,同一层会共享这一个Cell,但要手动处理每个时刻的迭代计算过程。如果要建立多层的LSTM,就要建立多个nn.LSTMCell。
1 .构造方法
构造方法和nn.RNNcell类似,依次传入feature_len和hidden_len,因为这只是一个计算单元,所以不涉及层数。
2. forward方法
回顾一下nn.RNNCell的forward方法,它是:
ht=nn.rnncell(x,ht-1)
即上一时刻的输出ht−1经nn.RNNCell前向计算得到这一时刻的ht。
对于nn.LSTMCell也是类似,但因为LSTM计算单元中还涉及对上一时刻的记忆Ct−1的使用,所以是
ht,ct=lstmcell(xt,(ht-1,ct-1))
因为输入xt只是t时刻的输入,不涉及seq_len,所以其shape是[batch,feature_len]
而ht和Ct在这里只是t时刻本层的隐藏单元和记忆单元,不涉及num_layers,所以其shape是 [batch,hidden_len]

3.4.3 一层的例子

每个时刻传入新的输入xt和上一时刻的隐藏单元ht−1和记忆单元Ct−1,并把这两个单元更新。

3.4.4 两层的例子

在最底下一层l0层和上面的例子一样,上层还需要接受下层的输出ht作为当前输入,然后同样是依赖本层上一时刻的h和C更新本层的h和C。注意LSTM单元的输入输出结构,向上层传递的是h而不是C。

3.4.5 PyTorch的LSTM模块

https://zhuanlan.zhihu.com/p/139617364
1、多层LSTM的网络架构

图1-17 多层LSTM架构图
2、LSTM模块

3、输入
input(seq_len, batch, input_size)
参数有:
seq_len:序列长度,在NLP中就是句子长度,一般都会用pad_sequence补齐长度
batch:每次喂给网络的数据条数,在NLP中就是一次喂给网络多少个句子
input_size:特征维度,和前面定义网络结构的input_size一致

图1-18 LSTM的输入格式
如果LSTM的参数 batch_first=True,则要求输入的格式是:input(batch, seq_len, input_size)
4、隐含层
LSTM有两个输入是 h0 和 c0,可以理解成网络的初始化参数,用随机数生成即可。

参数:
num_layers:隐藏层数
num_directions:如果是单向循环网络,则num_directions=1,双向则num_directions=2
batch:输入数据的batch
hidden_size:隐藏层神经元个数
注意,如果我们定义的input格式是:
input(batch, seq_len, input_size)
则H和C的格式也是要变的:

5、输出
LSTM的输出是一个tuple,如下:
output,(ht, ct) = net(input)
output: 最后一个状态的隐藏层的神经元输出
ht:最后一个状态的隐含层的状态值
ct:最后一个状态的隐含层的遗忘门值
output的默认维度是:

和input的情况类似,如果我们前面定义的input格式是:
input(batch, seq_len, input_size)
则ht和ct的格式也是要变的:

3.5 LSTM实现文本生实例

本实例使用2014人民日报上的一些新闻,使用PyTorch提供的nn.LSTM模型,根据提示字符串预测给定长度的语句。整个处理与3.3小节基本相同,构建模型时稍有不同,LSTM有两个变量h和c,RNN只要一个h。

3.6 GRU结构

https://blog.csdn.net/Jerr__y/article/details/58598296
LSTM门比较多,状态有C和H,其中C就相当于中间变量一样,输出只有H。有关LSTM的改进方案比较多,其中比较著名的变种 GRU(Gated Recurrent Unit ),这是由 Cho, et al. (2014) 提出。在 GRU 中,如 下图所示,只有两个门:重置门(reset gate)和更新门(update gate)。同时在这个结构中,把细胞状态和隐藏状态进行了合并。最后模型比标准的 LSTM 结构要简单,而且这个结构后来也非常流行。

图1-19 GRU模型结构图
其中,r_t 表示重置门,z_t表示更新门。重置门决定是否将之前的状态(h_(t-1))忘记。当r_t趋于 0 的时候,前一个时刻的状态信息 h_(t-1)将被忘掉,隐藏状态 h_t会被重置为当前输入的信息。更新门决定是否要将隐藏状态更新为新的状态 h ̃_t (更新门的作用相当于合并了 LSTM 中的遗忘门和输入门)。
和 LSTM 比较一下:
(1) GRU 少一个门,同时少了传送带状态 C_t。
(2) 在 LSTM 中,通过遗忘门和输入门控制信息的保留和传入;GRU 则通过重置门来控制是否要保留原来隐藏状态的信息,但是不再限制当前信息的传入。
(3) 在 LSTM 中,虽然得到了新的传送带状态 C_t,但是它仅仅作为一个中间变量,不直接输出,而是需要经过一个过滤的处理:
h_t=O_t 〖⊗tanh⁡(C〗_t)
同样,在 GRU 中, 虽然 (2) 中也得到了新的隐藏状态h_t, 但是还不能直接输出,而是通过更新门来控制最后的输出:
h_t=(1-z_t )⊗h_(t-1)+z_t⊗h ̃_t
保留以往隐藏状态(h_(t-1))和保留当前隐藏状态(h ̃_t)之间是一种互斥关系,这点从上面这个公式也可说明。如果z_t 越接近1,则(1-z_t )就越接近于0;反之,也成立。

3.7 biRNN结构

1.biRNN的网络架构
图1-20 为biRNN的网络架构图,从图中还可以看出,对于一个序列,一次向前遍历得到左LSTM,向后遍历得到右LSTM。隐层矢量直接通过拼接(concat)得到,最终损失函数直接相加,具体如下图。

图1-20 biRNN模型结构图
2、biRNN的不足
(1)biRNN模型的前向和后向LSTM模型是分开训练的,故它不是真正双向学习的模型,即biRNN无法同时向前和向后学习。
(2)biRNN如果层数多了,将出现自己看到自己的情况。下图中第2行第2列中的A|CD,此结果是第一层Bi LSTM的B位置输出内容,包括正向A和反向CD,然后直接拼接就得到A| CD。下图中第3行第2列中的ABCD, 此结果为正向BCD和反向AB | D拼接而成,当前位置需要预测的是B,但B已经在这里ABCD出现了。所以对于Bi LSTM,只要层数增加,会有一个“看到你自己”的问题。

 

图1-21 多层biRNN将自己看到自己示意图

利用RNN生成中文语句实例

本实例使用2014人民日报上的一些新闻(约28万条,60M),使用PyTorch提供的nn.rnn模型,根据提示字符串预测给定长度的语句。

数据下载

提取码:6l9e

3.3.1 模型架构

图1-13 RNN实例模型架构图

3.3.2 导入需要的模块

3.3.3 定义预处理函数

使用torch.utils.data生成可迭代的数据集。

3.3.4 定义模型

根据图1-13构建模型,以Embedding为输入层,隐含节点数为256,共两层。为何使用Embedding层作为输入层,而不使用One-hot编码作为输入层?Embedding输入层,除可以有效压缩维度空间外(与以one-hot编码为输入层),更重要的是Embedding层在整个迭代过程中参与学习。

3.3.5 定义训练模型函数

为便于管理,这里参数传入采用argparse方式。

3.3.6 设置参数

这里参数设置运行环境为jupyter notebook ,如果在命令行运行需要做一些改动。

3.3.7 运行模型

3.3.8 练习

1.把Emedding层改为One-hot层
2.目前学习率为固定,把固定改为动态(如与迭代次数相关关联),查看损失值的变化
3.修改学习率及迭代次数等,比较损失值的变化。
4.使用LSTM或GRU模型
5.使用GPT或BERT模型

3.2 循环神经网络语言模型

神经网络语言模型(NNLM)对统计语言模型而言(如n-gram),前进了一大步。它用词嵌入代替词索引,把词映射到低维向量,用神经网络计算代替词组的统计频率计算,从而有效避免维度灾难、增强了词的表现力和模型的泛化力。不过NNLM除计算效率问题外,主要还是基于n-gram的思想,虽然对n有所扩充,但是本质上仍然是使用神经网络编码的n-gram模型,而且对输入数据要求固定长度(一般取5-10),这严重影响模型的性能和泛化能力,所以,如何打破n的"桎梏"一直是人们追求的方向。
循环神经网络(RNN)(尤其其改进版本LSTM)序列语言模型的提出,较好的解决了如何学习到长距离的依赖关系的问题,接下来将从多个角度、多个层次详细介绍RNN。

3.2.1 RNN结构

图1-8是循环神经网络的经典结构,从图中可以看到输入x、隐含层、输出层等,这些与传统神经网络类似,不过自循环W却是它的一大特色。这个自循环直观理解就是神经元之间还有关联,这是传统神经网络、卷积神经网络所没有的。

图1-8 循环神经网络的结构
其中U是输入到隐含层的权重矩阵,W是状态到隐含层的权重矩阵,s为状态,V是隐含层到输出层的权重矩阵。图1-8比较抽象,将它展开成图1-9,就更好理解。

图1-9循环神经网络的展开结构
这是一个典型的Elman循环神经网络,从图7-2不难看出,它的共享参数方式是各个时间节点对应的W、U、V都是不变的,这个机制就像卷积神经网络的过滤器机制一样,通过这种方法实现参数共享,同时大大降低参数量。
图1-9中隐含层不够详细,把隐含层再细化就可得图1-10。

图1-10 循环神经网络Cell的结构图
这个网络在每一时间t有相同的网络结构,假设输入x为n维向量,隐含层的神经元个数为m,输出层的神经元个数为r,则U的大小为n×m维;W是上一次的s_(t-1)作为这一次输入的权重矩阵,大小为m×m维;V是连输出层的权重矩阵,大小为m×r维。而x_t、s_t 和o_t 都是向量,它们各自表示的含义如下:
x_t是时刻t的输入;
s_t是时刻t的隐层状态。它是网络的记忆。s_t基于前一时刻的隐层状态和当前时刻的输入进行计算,
函数f通常是非线性的,如tanh或者ReLU。s_(t-1)为前一个时刻的隐藏状态,其初始化通常为0;
o_t是时刻t的输出。例如,如想预测句子的下一个词,它将会是一个词汇表中的概率向量,o_t=softmax(Vs_t);
s_t认为是网络的记忆状态,s_t可以捕获之前所有时刻发生的信息。输出o_t的计算仅仅依赖于时刻t的记忆。
图1-10是RNN Cell的结构图,我们可以放大这个图,看看其内部结构,如图1-11所示。

图1-11 RNN 的Cell的内部结构
在图1-11中,把隐含状态s_(t-1) (假设为一个4维向量)
和输入x_t(假设为一个3维向量)拼接成一个7维向量,这个7维向量构成全连接神经网络的输入层,输出为4个节点的向量,激活函数为f,输入与输出间的权重矩阵的维度为[7,4]。这样式(1.6)可写成如下形式:

3.2.2 RNNCell代码实现

这里用Python实现(1.8)、(1.9)式,为简便起见,这里不考虑偏移量b。
1、定义激活函数

2、定义状态及输入值,并进行拼接。

3、计算输出值

3.2.3 多层RNNCell

上节我们介绍了单个RNN的结构(即RNNCell),有了这个基础之后,我们就可对RNNCell进行拓展。RNN可以横向拓展(增加时间步或序列长度),也可纵向拓展成多层循环神经网络,如图1-12所示。

图1-12多层循环神经网络
图1-12中,序列长度为T,网络层数为3。接下来为帮助大家更好的理解,接下来我们用PyTorch实现一个2层RNN。

3.2.4 多层RNNCell的PyTorch代码实现

Pytorch提供了两个版本的循环神经网络接口,单元版的输入是每个时间步,或循环神经网络的一个循环,而封装版的是一个序列。下面我们从简单的封装版torch.nn.RNN开始,其一般格式为:

由图7-3 可知,RNN状态输出a_t的计算公式为:

nn.RNN函数中的参数说明如下:
input_size : 输入x的特征数量。
hidden_size : 隐含层的特征数量。
num_layers : RNN的层数。
nonlinearity : 指定非线性函数使用tanh还是relu。默认是tanh。
bias : 如果是False,那么RNN层就不会使用偏置权重 b_i和b_h,默认是True
batch_first : 如果True的话,那么输入Tensor的shape应该是(batch, seq, feature),输出也是这样。默认网络输入是(seq, batch, feature),即序列长度、批次大小、特征维度。
dropout : 如果值非零(该参数取值范围为0~1之间),那么除了最后一层外,其它层的输出都会加上一个dropout层,缺省为零。
bidirectional : 如果True,将会变成一个双向RNN,默认为False。

函数nn.RNN()的输入包括特征及隐含状态,记为(x_t 、h_0),输出包括输出特征及输出隐含状态,记为(output_t 、h_n)。
其中特征值x_t的形状为(seq_len, batch, input_size), h_0的形状为(num_layers * num_directions, batch, hidden_size),其中num_layers为层数,num_directions方向数,如果取2表示双向(bidirectional,),取1表示单向。
output_t的形状为(seq_len, batch, num_directions * hidden_size), h_n的形状为(num_layers * num_directions, batch, hidden_size)。
为使大家对循环神经网络有个直观理解,下面先用Pytorch实现简单循环神经网络,然后验证其关键要素。
首先建立一个简单循环神经网络,输入维度为10,隐含状态维度为20,单向两层网络。

因输入节点与隐含层节点是全连接,根据输入维度、隐含层维度,可以推算出相关权重参数的维度,w_ih应该是20x10,w_hh是20x20, b_ih和b_hh都是hidden_size。以下我们通过查询weight_ih_l0、weight_hh_l0等进行验证。

RNN网络已搭建好,接下来将输入(x_t 、h_0)传入网络,根据网络配置及网络要求,我们生成输入数据。输入特征长度为100,批量大小为32,特征维度为10的张量。隐含状态按网络要求,其形状为(2,32,20)。

将输入数据传入RNN网络,将得到输出及更新后隐含状态值。根据以上规则,输出output的形状应该是(100,32,20),隐含状态的输出形状应该与输入的形状一致。

其结果与我们设想的完全一致。

3 神经网络语言模型

  n-gram语言模型简单明了,解释性较好,但有几个较大缺陷:
1、维度灾难,随着n值越大,单词组合数指数级增长,由此带来相关联合或条件概率大量为0;
2、因n一般不能超过3或4,这影响模型利用单词更多邻居信息;
3、n-gram使用单词组合的频度作为计算基础,这需要提前计算,且无法泛化到相似语句或相似单词的情况。
接下来将介绍的神经网络语言模型(NNLM),可有效避免n-gram的这些不足,NNLM使用哪些方法或技术来解决这些问题的呢?请看下节内容。

3.1 神经网络语言模型

  Yoshua Bengio团队在2003年在论文《A Neural Probabilistic Language Model》中提出神经网络语言模型(NNLM),可以说是后续神经网络语言模型的鼻祖,其创新点有以下几点,这些亮点也是它避免n-gram模型不足的重要方法。
1、使用词嵌入(word Embedding)代替单词索引;
2、使用神经网络计算概率
当然,这个NNLM还有很多不足,其中整个模型因使用softmax,tanh等激活函数,在面对较大的语料库时(如词汇量在几万、几百万、甚至更多)计时效率很低,而且模型有点繁琐不够简练,后续我们将介绍一些改进模型。

3.1.1 神经网络语言模型(NNLM)

Yoshua Bengio团队提出的这个NNLM的架构图1-2所示。

图1-2 神经网络架构
假设该模型训练的语料库的词汇量为|V|,语料库中每个单词w_i转换成词向量的大小维度为m。把每个单词w_i转换为词嵌入的矩阵为C,其形状为|V|xm。其过程如图1-3所示。

图1-3 通过矩阵C把词索引转换为词嵌入

整个网络架构用表达式表示:
y=b+Wx+U tanh(d+Hx)
其中Wx表示输入层与输出层有直接联系(图1-2中的虚线部分),如果不要这个链接,直接设置W为0即可,b是输出层的偏置向量,d是隐层的偏置向量,里面的x即是单词到特征向量的映射,计算如下:
x=(C(w_(t-1) ),C(w_(t-2) ),⋯,C(w_(t-n+1)))
其中C是一个矩阵,其形状为|V|xm
假设隐层的神经元个数为h,那么整个模型的参数可以细化为θ = (b, d, W, U, H, C)。下面各参数含义及形状:
b是词向量x到输出层的偏移量,维度为|V|
W是词向量x到输出层的权重矩阵,维度为|V|x(n−1)m
d是隐含层的偏移量,维度为h
H是输入x到隐含层的权重矩阵,形状为hx(n-1)m
U是隐含层到输出层的权重矩阵,形状为|V|xh

网络的第一层(输入层)是将C(w_(t-1) ),C(w_(t-2) ),⋯,C(w_(t-n+1))这已知的n-1和单词的词向量首尾相连拼接起来,形成(n-1)m的向量x。
网络的第二层(隐藏层)直接用d+Hx计算得到,d是一个偏置项。之后,用tanh作为激活函数。
网络的第三层(输出层)一共有|V|个节点,最后使用softmax函数将输出值y归一化成概率。
最后,用随机梯度下降法把这个模型优化出来就可以了。

3.1.2 NNLM的PyTorch实现

这样用一个简单实例,实现3.1.1节的计算过程。
1、导入需要的库或模块

2、定义语料库及预处理函数

3、构建模型

4、训练模型

5、运行结果
Epoch: 1000 cost = 0.113229
Epoch: 2000 cost = 0.015353
Epoch: 3000 cost = 0.004546
Epoch: 4000 cost = 0.001836
Epoch: 5000 cost = 0.000853
[['我喜欢苹果'], ['我爱运动'], ['我讨厌老鼠']] -> ['苹果', '运动', '老鼠']

3.1.3词嵌入特征

  在NNLM语言模型中,通过神经网络的线性/非线性转换以及激活函数(例如softmax)得到似然条件概率值。同时因为保存了隐层的系数矩阵,因此可以得到每个文本序列以及每个词的词嵌入(Word Embedding)。词嵌入一般通过神经网络能学习语料库中的一些特性或知识, 如捕获词之间的多种相似性。图1-4为在某语料库上训练得到的一个简单词嵌入矩阵,从这个特征我们可以看出,有些词是相近的,如男与女,国王与王后,苹果与橘子等,这些相似性是从语料库学习得到。如何从语料库中学习这些特征或知识?人们研究出多种有效方法,其中最著名的就是Word2vec。
另外我们可以把词嵌入这些特性,通过迁移方法,应用到下游项目中。

图1-4 词嵌入特征示意图
与从语料库中学习词嵌入类似,在视觉处理领域中,也是通过学习图像,把图像特征转换为编码,整个过程如下图1-5所示。只不过在视觉处理中我们一般不把学到的向量为词嵌入,而往往称之为编码。

图1-5 把图像转换为编码示意图

3.1.4 word2vec简介

  词嵌入(word Embedding)最早由 Hinton 于 1986 年提出的,可以克服独热表示的缺点。解决词汇与位置无关问题,可以通过计算向量之间的距离(欧式距离、余弦距离等)来体现词与词的相似性。其基本想法是直接用一个普通的向量表示一个词,此向量为:
[0.792, -0.177, -0.107, 0.109, -0.542, ...],常见维度50或100。用这种方式表示的向量,“麦克”和“话筒”的距离会远远小于“麦克”和“天气”的距离。
词嵌入表示的优点是解决了词汇与位置无关问题,不足是学习过程相对复杂且受训练语料的影响很大。训练这种向量表示的方法较多,常见的有LSA、PLSA、LDA、Word2Vec等,其中Word2Vec是Google在2013年开源的一个工具,Word2Vec是一款用于词向量计算的工具,同时也是一套生成词向量的算法方案。Word2Vec算法的背后是一个浅层神经网络,其网络深度仅为3层,所以,严格说Word2Vec并非深度学习范畴。但其生成的词向量在很多任务中都可以作为深度学习算法的输入,因此,在一定程度上可以说Word2Vec技术是深度学习在NLP领域的基础。训练Word2Vec主要有以下两种模型来训练得到:
1、CBOW模型
CBOW模型包含三层,输入层、映射层和输出层。其架构如图1-6。CBOW模型中的w(t)为目标词,在已知它的上下文w(t-2),w(t-1),w(t+1),w(t+2)的前提下预测词w(t)出现的概率,即:p(w/context(w))。 目标函数为:

图1-6 CBOW模型
CBOW模型训练其实就是根据某个词前后若干词来预测该词,这其实可以看成是多分类。最朴素的想法就是直接使用softmax来分别计算每个词对应的归一化的概率。但对于动辄十几万词汇量的场景中使用softmax计算量太大,于是需要用一种二分类组合形式的hierarchical softmax,即输出层为一棵二叉树。
2、Skip-gram模型
Skip-gram模型同样包含三层,输入层,映射层和输出层。其架构如图1-7。Skip-Gram模型中的w(t)为输入词,在已知词w(t)的前提下预测词w(t)的上下文w(t-2),w(t-1),w(t+1),w(t+2),条件概率写为:p(context(w)/w)。目标函数为:


图1-7 Skip-gram模型