深度学习-神经网络-循环神经网络(二):LSTM【最多200个上下文;1997】 --> GRU【2014】【三者都无法并行计算】

Jordan RNN于1986年提出:《SERIAL ORDER: A PARALLEL DISTRmUTED PROCESSING APPROACH》
Elman RNN于1990年提出:《Finding Structure in Time》
《LSTM原始论文:Long Short-Term Memory》
《GRU原始论文:Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation》

四、LSTM(Long Short-term Memory)长短期记忆结构

LSTM是RNN的升级版,加入了forget、input、output三个步骤,包含3个门,5对参数,两次更新。赋予了RNN选择性记忆的能力,一定程度解决了RNN中Long Term Dependency(长期依赖)的问题。

LSTM 通过刻意的设计来避免长期依赖问题。记住长期的信息在实践中是 LSTM 的默认行为,而非需要付出很大代价才能获得的能力!

由于内部结构相对较复杂, 因此训练效率在同等算力下较传统RNN低很多.

在这里插入图片描述

  • 从左向右,三个sigmoid分别对应三个门:forget,input,output,后面用 f , i , o f,i,o f,i,o 代替。按从左至右顺序:
    1. forget gate: f t = σ ( [ h t − 1 , X t ] × W f + b f ) f_t=σ([h_{t−1},X_t]×W_f+b_f) ft=σ([ht1,Xt]×Wf+bf),forget gate用sigmoid函数激活,得到一个0~1的数,来决定 S t − 1 S_{t−1} St1 的“记忆”留着哪些,忘记哪些。剩下两个gate用法也类似。
    2. input gate: i t = σ ( [ h t − 1 , X t ] × W i + b i ) i_t=σ([h_{t−1},X_t]×W_i+b_i) it=σ([ht1,Xt]×Wi+bi)
    3. 更新细胞状态C: C t = f t ⊗ C t − 1 + i t ⊗ t a n h ( ( [ h t − 1 , X t ] × W c + b c ) C_t=f_t⊗C_{t−1}+i_t⊗tanh(([h_{t−1},X_t]×W_c+b_c) Ct=ftCt1+ittanh(([ht1,Xt]×Wc+bc)
    4. output gate: o t = σ ( [ h t − 1 , X t ] × W o + b o ) o_t=σ([h_{t−1},X_t]×W_o+b_o) ot=σ([ht1,Xt]×Wo+bo)
    5. 最后更新输出h: h t = o t ⊗ t a n h ( C t ) h_t=o_t⊗tanh(C_t) ht=ottanh(Ct)
  • 可以看出 5 5 5 对参数(kernel+bias), 3 3 3 个门, 1 1 1个tanh, 2 2 2 次更新。
    在这里插入图片描述
    在这里插入图片描述
  • Pointwise Operation:对应位置的运算(加法/乘法)【比如:2个3行4列的矩阵的Pointwise Operation就是2个矩阵对应位置的元素分别做运算】
  • Concatenate:矩阵的合并【一般是左右方向(横向)的合并,因为横向一般表示各个特征,每一列代表一个特征。比如A矩阵是4列(4个特征),B矩阵是10列(10个特征),合并后就是一个14列的矩阵(14个特征)】

1、LSTM 的核心思想

在这里插入图片描述在这里插入图片描述
  • LSTM 的关键就是细胞状态,水平线在图上方贯穿运行。
  • 细胞状态类似于传送带。直接在整个链上运行,只有一些少量的线性交互。信息在上面流传保持不变会很容易。
    在这里插入图片描述
  • LSTM 有通过精心设计的称作为“门”的结构来去除或者增加信息到细胞状态的能力。门是一种让信息选择式通过的方法。他们包含一个 sigmoid 神经网络层和一个按位的乘法操作。
    在这里插入图片描述
  • Sigmoid 层输出 0 到 1 之间的数值,描述每个部分有多少量可以通过。0 代表“不许任何量通过”,1 就指“允许任意量通过”!

2、LSTM 三个门(保护和控制细胞状态)

2.1 Forget Gate

  • 在我们 LSTM 中的第一步是决定我们会从细胞状态中丢弃什么信息。这个决定通过一个称为忘记门层完成。该门会读取 h t − 1 h_{t−1} ht1 x t x_t xt ,输出一个在 0 到 1 之间的数值给每个在细胞状态 C T − 1 C_{T-1} CT1 中的数字。 1表示“完全保留”, 0表示“完全舍弃”。
  • 让我们回到语言模型的例子中来基于已经看到的预测下一个词。在这个问题中,细胞状态可能包含当前主语的性别,因此正确的代词可以被选择出来。当我们看到新的主语,我们希望忘记旧的主语。
    在这里插入图片描述
    在这里插入图片描述
    与传统RNN的内部结构计算非常相似, 首先将当前时间步输入 x t x_t xt 与上一个时间步隐含状态 h t − 1 h_{t−1} ht1 拼接, 得到 [ x t , h t − 1 ] [x_t, h_{t-1}] [xt,ht1], 然后通过一个全连接层做变换, 最后通过sigmoid函数进行激活得到 f t f_t ft, 我们可以将 f t f_t ft 看作是门值, 好比一扇门开合的大小程度, 门值都将作用在通过该扇门的张量, 遗忘门门值将作用的上一层的细胞状态上, 代表遗忘过去的多少信息, 又因为遗忘门门值是由 x t x_t xt , h t − 1 h_{t−1} ht1 计算得来的, 因此整个公式意味着根据当前时间步输入和上一个时间步隐含状态 h t − 1 h_{t−1} ht1 来决定遗忘多少上一层的细胞状态所携带的过往信息.

2.2 Input Gate

下一步是确定什么样的新信息被存放在细胞状态中。这里包含两个部分。第一,sigmoid 层称 “输入门层” 决定什么值我们将要更新。然后,一个 tanh 层创建一个新的候选值向量 C ⃗ t \vec{C}_t C t,会被加入到状态中。下一步,我们会讲这两个信息来产生对状态的更新。

在我们语言模型的例子中,我们希望增加新的主语的性别到细胞状态中,来替代旧的需要忘记的主语。
在这里插入图片描述

我们看到输入门的计算公式有两个

  • 第一个公式是产生输入门门值的公式, 它和遗忘门公式几乎相同, 区别只是在于它们之后要作用的目标上. 这个公式意味着输入信息有多少需要进行过滤.
  • 第二个公式是与传统RNN的内部结构计算相同. 对于LSTM来讲, 它得到的是当前的细胞状态, 而不是像经典RNN一样得到的是隐含状态。
    在这里插入图片描述

2.3 细胞状态更新

  • 现在是更新旧细胞状态的时间了, C t − 1 C_{t-1} Ct1 更新为 C t C_t Ct。前面的步骤已经决定了将会做什么,我们现在就是实际去完成。
  • 我们把旧状态与 f t f_t ft 相乘,丢弃掉我们确定需要丢弃的信息。接着加上 i t ∗ C ~ t i_t * \tilde{C}_t itC~t。这就是新的候选值,根据我们决定更新每个状态的程度进行变化。
  • 在语言模型的例子中,这就是我们实际根据前面确定的目标,丢弃旧代词的性别信息并添加新的信息的地方。
    在这里插入图片描述
    细胞更新的结构与计算公式非常容易理解, 这里没有全连接层, 只是将刚刚得到的遗忘门门值与上一个时间步得到的 C t − 1 C_{t-1} Ct1 相乘, 再加上输入门门值与当前时间步得到的未更新 C t C_t Ct 相乘的结果. 最终得到更新后的 C t C_t Ct 作为下一个时间步输入的一部分. 整个细胞状态更新过程就是对遗忘门和输入门的应用.
    在这里插入图片描述

2.4 Output Gate

  • 最终,我们需要确定输出什么值。这个输出将会基于我们的细胞状态,但是也是一个过滤后的版本。首先,我们运行一个 sigmoid 层来确定细胞状态的哪个部分将输出出去。接着,我们把细胞状态通过 tanh 进行处理(得到一个在-1到 1之间的值)并将它和 sigmoid 门的输出相乘,最终我们仅仅会输出我们确定输出的那部分。
  • 在语言模型的例子中,因为他就看到了一个 代词,可能需要输出与一个 动词 相关的信息。例如,可能输出是否代词是单数还是负数,这样如果是动词的话,我们也知道动词需要进行的词形变化。
    在这里插入图片描述
    输出门部分的公式也是两个, 第一个即是计算输出门的门值, 它和遗忘门,输入门计算方式相同. 第二个即是使用这个门值产生隐含状态 h t h_t ht, 他将作用在更新后的细胞状态 C t C_t Ct 上, 并做tanh激活, 最终得到 h t h_t ht 作为下一时间步输入的一部分. 整个输出门的过程, 就是为了产生隐含状态 h t h_t ht.
    在这里插入图片描述

3、LSTM结构单元

在这里插入图片描述
在这里插入图片描述

4、LSTM分步计算演示

在这里插入图片描述

  • memory cell 的行为组合模式
input gateforget gatebehavior
01remember the previous value
11add to the previous value
00erase the value
10overwrite the value
  • 可以看出:输入数据 [ x 1 , x 2 , x 3 ] [x_1,x_2,x_3] [x1,x2,x3]
    • x 1 x_1 x1 表示 Input 的输入值
    • x 2 x_2 x2 用于计算 Input Gate、 Forget Gate 的开启状态$,当
    • x 3 x_3 x3 用于计算 Output Gate 的开启状态$

在这里插入图片描述

import torch.nn as nn
import torch

# # 实例化一个LSTM模型实例
lstm = nn.LSTM(5, 4, 2)  # 定义LSTM的参数含义: (input_size=5:输入X的特征维度(WordEmbedding维度);hidden_size=4:隐藏层神经元数量; num_layers=2:隐藏层的层数)
input = torch.randn(1, 3, 5)  # 初始化一个输入张量【batch_size=1表示当前批次的样本数量;3表示样本序列长度/sequence_lenght;5表示WordEmbedding维度】此处的WordEmbedding维度要与lstm对象的input_size一致
print("input.shape = {0}\ninput = \n{1}".format(input.shape, input))

h0 = torch.randn(2, 3, 4)  # 初始化一个初始隐藏层张量,参数含义:【2表示隐藏层层数num_layers,要与lstm对象的num_layers一致;3表示样本序列长度/sequence_lenght;6表示隐藏层神经元的个数(隐层张量的特征维度的大小)】
c0 = torch.randn(2, 3, 4)  # 初始化细胞初始状态张量,参数含义:【2表示隐藏层层数num_layers,要与lstm对象的num_layers一致;3表示样本序列长度/sequence_lenght;6表示隐藏层神经元的个数(隐层张量的特征维度的大小)】

output, (hn, cn) = lstm(input, (h0, c0))

print("\noutput.shape = {0}\noutput = \n{1}".format(output.shape, output))
print("\nhn.shape = {0}\nhn = \n{1}".format(hn.shape, hn))
print("\ncn.shape = {0}\ncn = \n{1}".format(cn.shape, cn))

打印结果:

input.shape = torch.Size([1, 3, 5])
input = 
tensor([[[ 0.0901,  0.5281,  0.7268, -0.8648, -0.9939],
         [ 0.7097, -0.0189, -0.0289, -0.1919, -0.3205],
         [-0.1177, -0.5505,  0.4584, -0.3086,  1.6261]]])

output.shape = torch.Size([1, 3, 4])
output = 
tensor([[[ 0.1668, -0.5727,  0.2761,  0.1491],
         [-0.0641, -0.3459,  0.0284, -0.0874],
         [-0.1094,  0.0087,  0.1879, -0.0362]]], grad_fn=<StackBackward0>)

hn.shape = torch.Size([2, 3, 4])
hn = 
tensor([[[ 0.2731,  0.1101, -0.4006, -0.1451],
         [-0.0482, -0.2946, -0.1373, -0.0744],
         [ 0.2232, -0.3235,  0.2270,  0.6591]],

        [[ 0.1668, -0.5727,  0.2761,  0.1491],
         [-0.0641, -0.3459,  0.0284, -0.0874],
         [-0.1094,  0.0087,  0.1879, -0.0362]]], grad_fn=<StackBackward0>)

cn.shape = torch.Size([2, 3, 4])
cn = 
tensor([[[ 0.6996,  0.1606, -0.7565, -0.3500],
         [-0.1105, -0.5381, -0.2158, -0.1718],
         [ 1.5367, -0.4904,  0.7705,  2.0752]],

        [[ 0.4081, -1.2695,  0.8239,  0.3038],
         [-0.1320, -1.2552,  0.0630, -0.3515],
         [-0.1959,  0.0180,  0.8571, -0.1648]]], grad_fn=<StackBackward0>)

Process finished with exit code 0

nn.LSTM类初始化主要参数解释:

  • input_size: 输入张量x中特征维度的大小.
  • hidden_size: 隐层张量h中特征维度的大小.
  • num_layers: 隐含层的数量.
  • bidirectional: 是否选择使用双向LSTM, 如果为True, 则使用; 默认不使用.

nn.LSTM类实例化对象主要参数解释:

  • input: 输入张量x.
  • h0: 初始化的隐层张量h.
  • c0: 初始化的细胞状态张量c.

5、LSTM的输入输出含义

5.1 LSTM模型参数含义

通过源代码中可以看到nn.LSTM继承自nn.RNNBase,其初始化函数定义如下

class RNNBase(Module):
	...
    def __init__(self, mode, input_size, hidden_size, num_layers=1, 
    						bias=True, batch_first=False, dropout=0.,idirectional=False):

我们需要关注的参数以及其含义解释如下:

  • input_size – 输入数据的大小,也就是前面例子中每个单词向量的长度(The number of expected features in the input x);
  • hidden_size – 隐藏层的大小(即隐藏层节点数量),输出向量的维度等于隐藏节点数(The number of features in the hidden state h);
  • num_layers – recurrent layer的数量,默认等于1(Number of recurrent layers. E.g., setting num_layers=2 would mean stacking two RNNs together to form a stacked RNN, with the second RNN taking in outputs of the first RNN and computing the final results. Default: 1)。
  • bias – 隐层状态是否带bias,默认为true(If False, then the layer does not use bias weights b_ih and b_hh. Default: True)
  • batch_first – 默认为False,也就是说官方不推荐我们把batch放在第一维,这个CNN有点不同,此时输入输出的各个维度含义为 (seq_length,batch,feature)。当然如果你想和CNN一样把batch放在第一维,可将该参数设置为True。
  • dropout – 如果非0,就在除了最后一层的其它层都插入Dropout层,默认为0。
  • bidirectional – If True, becomes a bidirectional LSTM. Default: False

在这里插入图片描述

output, (hn, cn) = lstm(input, (h0, c0))

5.2 输入数据 input

下面介绍一下输入数据的维度要求(batch_first=False):

输入数据需要按如下形式传入 input, (h_0,c_0)

  • input: 输入数据,即上面例子中的一个句子(或者一个batch的句子),其维度形状为 (seq_len, batch_size, input_size)
    • seq_len: 时间步数序列长度(句子长度),即单词数量,这个是需要固定的。当然假如你的一个句子中只有2个单词,但是要求输入10个单词,这个时候可以用torch.nn.utils.rnn.pack_padded_sequence()或者torch.nn.utils.rnn.pack_sequence()来对句子进行填充或者截断。
    • batch_size:就是你一次传入的句子的数量
    • input_size: 每个单词向量的长度,这个必须和你前面定义的网络结构保持一致
  • h_0:维度形状为 (num_layers * num_directions, batch_size, hidden_size)
    • 结合下图应该比较好理解第一个参数的含义num_layers * num_directions, 即LSTM的层数乘以方向数量。这个方向数量是由前面介绍的bidirectional决定,如果为False,则等于1;反之等于2。
    • batch_size:同上
    • hidden_size: 隐藏层节点数
  • c_0: 维度形状为 (num_layers * num_directions, batch_size, hidden_size),各参数含义和h_0类似。

当然,如果你没有传入(h_0, c_0),那么这两个参数会默认设置为0。

5.3 输出数据 output

  • output: 维度和输入数据类似,只不过最后的feature部分会有点不同,即 (seq_len, batch_size, num_directions * hidden_size)
    • 这个输出tensor包含了LSTM模型最后一层每个time step的输出特征,比如说LSTM有两层,那么最后输出的是 [ h 0 1 , h 1 1 , . . . , h l 1 ] [h_0^1,h_1^1,...,h_l^1] [h01,h11,...,hl1],表示第二层LSTM每个time step对应的输出。
    • 另外如果前面你对输入数据使用了torch.nn.utils.rnn.PackedSequence,那么输出也会做同样的操作编程packed sequence。
    • 对于unpacked情况,我们可以对输出做如下处理来对方向作分离output.view(seq_len, batch, num_directions, hidden_size), 其中前向和后向分别用0和1表示Similarly, the directions can be separated in the packed case.
  • h_n:(num_layers * num_directions, batch, hidden_size),
    • 只会输出最后个time step的隐状态结果(如上图所示)。
    • Like output, the layers can be separated using h_n.view(num_layers, num_directions, batch_size, hidden_size) and similarly for c_n.
  • c_n :(num_layers * num_directions, batch_size, hidden_size),只会输出最后个time step的cell状态结果(如下图所示)。

output:如果num_layer为3,则output只记录最后一层(即,第三层)的输出,shape为(time_step, batch_size, hidden_size * num_directions), 包含每一个时刻的输出特征,与多少层无关。所以整体LSTM的输出是在最后一个time_step时才能得到,才是最完整的最终结果,output最后一个时步的输出为:output[-1,:,:]

对于单层单向的LSTM, 其 h n h_n hn 最后一层输出 h n [ − 1 , : , : ] h_n[-1,:,:] hn[1,:,:],和 output最后一个时步的输出 o u t p u t [ − 1 , : , : ] output[-1,:,:] output[1,:,:] 相等。如果是分类任务的话,就可以把 o u t p u t [ − 1 , : , : ] output[-1, :, :] output[1,:,:] 或者 o u t p u t output output 送到一个分类器分类。

比如:在做文字识别中,先对文本行图片提取feature,如shape为(B, 512, 1, 16)其中512是channel 维度,1是height,16是width,tensor处理为(B, 16, 512)或(16, B, 512)因为lstm要求输入是3D的,CNN的feature是4D的。

那这样,512就是input_size, 16就是seq_len

import torch
import torch.nn as nn

# 数据向量维数10, 隐藏元维度20, 2个lstm层串联(如果是1,可以省略,默认为1)
lstm = nn.LSTM(input_size=10, hidden_size=20, num_layers=2)

# 序列长度seq_len=5, batch_size=3, 数据向量维数=10
input = torch.randn(5, 3, 10)

# 初始化的隐藏元和记忆元,通常它们的维度是一样的
# 2个lstm层,batch_size=3,隐藏元维度20
h0 = torch.randn(2, 3, 20)
c0 = torch.randn(2, 3, 20)
# 这里有2层lstm,output是最后一层lstm的每个词向量对应隐藏层的输出,其与层数无关,只与序列长度相关
# hn,cn是所有层最后一个隐藏元和记忆元的输出
output, (hn, cn) = lstm(input, (h0, c0))

print(output.size(), hn.size(), cn.size())  # 分别是:torch.Size([5, 3, 20]) torch.Size([2, 3, 20]) torch.Size([2, 3, 20])

# 查看一下那几个重要的属性:
print("------------输入--》隐藏------------------------------")
print(lstm.weight_ih_l0.size())  # (4*hidden_size, input_size) for `k = 0`  要学习的是四个权重参数
print(lstm.weight_ih_l1.size())  # (4*hidden_size, num_directions * hidden_size) for k!=0
print(lstm.bias_ih_l0.size())
print(lstm.bias_ih_l1.size())
print("------------隐藏--》隐藏------------------------------")
print(lstm.weight_hh_l0.size())  # (4*hidden_size, hidden_size)
print(lstm.weight_hh_l1.size())
print(lstm.bias_hh_l0.size())
print(lstm.bias_hh_l1.size())
print(output[-1:, :, :].equal(hn[-1:, :, :]), '=======')
'''输出结果为:
torch.Size([5, 3, 20]) torch.Size([2, 3, 20]) torch.Size([2, 3, 20])
------------输入--》隐藏------------------------------
torch.Size([80, 10])
torch.Size([80, 20])
torch.Size([80])
torch.Size([80])
------------隐藏--》隐藏------------------------------
torch.Size([80, 20])
torch.Size([80, 20])
torch.Size([80])
torch.Size([80])
True =======
'''

6、Bi-LSTM

单向的 RNN,是根据前面的信息推出后面的,但有时候只看前面的词是不够的, 可能需要预测的词语和后面的内容也相关,那么此时需要一种机制,能够让模型不仅能够从前往后的具有记忆,还需要从后往前需要记忆。此时双向LSTM就可以帮助我们解决这个问题。

在这里插入图片描述
由于是双向LSTM,所以每个方向的LSTM都会有一个输出,最终的输出会有2部分,所以往往需要concat的操作。

Bi-LSTM即双向LSTM, 它没有改变LSTM本身任何的内部结构, 只是将LSTM应用两次且方向不同, 再将两次得到的LSTM结果进行拼接作为最终输出。

在这里插入图片描述

  • 我们看到图中对"我爱中国"这句话或者叫这个输入序列, 进行了从左到右和从右到左两次LSTM处理, 将得到的结果张量进行了拼接作为最终输出.
  • 这种结构能够捕捉语言语法中一些特定的前置或后置特征, 增强语义关联,但是模型参数和计算复杂度也随之增加了一倍, 一般需要对语料和计算资源进行评估后决定是否使用该结构.

7、LSTM参数数量计算

在这里插入图片描述
实际上这里面有 4 个非线性变换(3 个 门 + 1 个 tanh),每一个非线性变换说白了就是一个两层的全连接网络。重点来了,第一层是 x i x_i xi h i h_i hi 的结合,维度就是 embedding_size + hidden_size,第二层就是输出层,维度为 hidden_size,所以该网络的参数量就是:
( e m b e d d i n g _ s i z e + h i d d e n _ s i z e ) ∗ h i d d e n _ s i z e + h i d d e n _ s i z e (embedding\_size + hidden\_size) * hidden\_size + hidden\_size (embedding_size+hidden_size)hidden_size+hidden_size
一个 cell 有 4 个这样结构相同的网络,那么一个 cell 的总参数量就是直接 × 4:
[ ( e m b e d d i n g _ s i z e + h i d d e n _ s i z e ) ∗ h i d d e n _ s i z e + h i d d e n _ s i z e ] × 4 [(embedding\_size + hidden\_size) * hidden\_size + hidden\_size]×4 [(embedding_size+hidden_size)hidden_size+hidden_size]×4
注意这 4 个权重可不是共享的,都是独立的网络。一般来说,一层 LSTM 的参数量计算公式是:
4 [ ( d h i d d e n + d x ) ⋅ d h i d d e n + d h i d d e n ] 4[(d_{hidden}+d_x)·d_{hidden}+d_{hidden}] 4[(dhidden+dx)dhidden+dhidden]
其中 4 4 4 表示每个cell有4个非线性映射层, d h i d d e n + d x d_{hidden}+d_x dhidden+dx [ h t − 1 , x t ] [h_{t-1},x_t] [ht1,xt]的维度,后面的 + d h +d_h +dh表示bias的数量。
所以,LSTM 层的参数数量只与输入维度 h x h_x hx 和输出维度 h h i d d e n h_{hidden} hhidden 相关,和普通全连接层相同。
那么显而易见,一层双向 LSTM 的参数量就是上述公式 × 2。

import tensorflow as tf

model = tf.keras.Sequential([
    tf.keras.layers.Embedding(1000, 128),
    tf.keras.layers.LSTM(units=64),
    tf.keras.layers.Dense(10)]
)
model.summary()

输出如下:

Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
embedding (Embedding)        (None, None, 128)         128000    
_________________________________________________________________
lstm (LSTM)                  (None, 64)                49408     
_________________________________________________________________
dense (Dense)                (None, 10)                650       
=================================================================
Total params: 178,058
Trainable params: 178,058
Non-trainable params: 0
_________________________________________________________________

可以看到

  • Embedding层的参数量 128000 = 1000 × 128 128000=1000×128 128000=1000×128
  • LSTM 层参数量: 49408 = 4 [ ( d h i d d e n + d x ) ⋅ d h i d d e n + d h i d d e n ] = 4 [ ( 64 + 128 ) × 64 + 64 ] 49408=4[(d_{hidden}+d_x)·d_{hidden}+d_{hidden}]=4[(64+128)×64+64] 49408=4[(dhidden+dx)dhidden+dhidden]=4[(64+128)×64+64]
  • 全连接Dense层参数量: 650 = 64 × 10 + 10 650=64×10+10 650=64×10+10

另外,tf.keras.layers.LSTM() 的默认输出大小为 [batch_size, units],就是只使用最后一个 time step 的输出。假如我们想要得到每个 time step 的输出 ( h 0 , h 1 , . . . h t ) (h_0,h_1,...h_t) (h0,h1,...ht) 和最终的 cell state ( C t ) (C_t) (Ct),那么我们可以指定另外两个参数 return_sequences=True 和 return_state=True:

inputs = tf.random.normal([64, 100, 128])  # [batch_size, seq_length, embedding_size]
whole_seq_output, final_memory_state, final_carry_state = tf.keras.layers.LSTM(64, return_sequences=True, return_state=True)(inputs)
print(f"{whole_seq_output.shape=}")
print(f"{final_memory_state.shape=}")
print(f"{final_carry_state.shape=}")

打印结果:

whole_seq_output.shape=TensorShape([32, 100, 64])  # 100 表示有 100 个词,即 100 个 time step
final_memory_state.shape=TensorShape([32, 64])
final_carry_state.shape=TensorShape([32, 64])

8、为什么LSTM能缓解梯度弥散问题(并不能杜绝梯度弥散问题)

  • 从上述中我们知道, RNN产生梯度消失与梯度爆炸的原因就在于
    ∏ j = k + 1 t ∂ S j ∂ S j − 1 = ∏ j = k + 1 t t a n h ′ ( W x X j + W s S j − 1 + b 1 ) ⋅ W s \prod^t_{j=k+1}\cfrac{\partial S_j}{\partial S_{j-1}}=\prod^t_{j=k+1}tanh'(W_{x}X_{j}+W_{s}S_{j-1}+b_{1})·W_s j=k+1tSj1Sj=j=k+1ttanh(WxXj+WsSj1+b1)Ws
  • 如果我们能够将这一坨东西去掉, 我们的不就解决掉梯度问题了吗。 LSTM通过门机制来解决了这个问题。
    • 遗忘门: f t = σ ( W f ⋅ [ h t − 1 , x t ] + b f ) f_t = \sigma( W_f \cdot [h_{t-1}, x_t] + b_f) ft=σ(Wf[ht1,xt]+bf)
    • 输入门: i t = σ ( W i ⋅ [ h t − 1 , x t ] + b i ) i_t = \sigma(W_i \cdot [h_{t-1}, x_t] + b_i) it=σ(Wi[ht1,xt]+bi)
    • 输出门: o t = σ ( W o ⋅ [ h t − 1 , x t ] + b 0 ) o_t = \sigma(W_o \cdot [h_{t-1}, x_t ] + b_0 ) ot=σ(Wo[ht1,xt]+b0)
    • 当前单元状态: c t = f t ∘ c t − 1 + i t ∘ t a n h ( W c ⋅ [ h t − 1 , x t ] + b c ) c_t = f_t \circ c_{t-1} + i_t \circ tanh(W_c \cdot [h_{t-1}, x_t] + b_c ) ct=ftct1+ittanh(Wc[ht1,xt]+bc)
    • 当前时刻的隐层输出: h t = o t ∘ t a n h ( c t ) h_t = o_t \circ tanh(c_t) ht=ottanh(ct)
  • 由于 S S S(也就是LSTM中的 C C C )是由多个值共同决定,所以:
    ∂ C t ∂ C t − 1 = ∂ C t ∂ f t ∂ f t ∂ h t − 1 ∂ h t − 1 ∂ C t − 1 + ∂ C t ∂ i t ∂ i t ∂ h t − 1 ∂ h t − 1 ∂ C t − 1 + ∂ C t ∂ C ~ t ∂ C ~ t ∂ h t − 1 ∂ h t − 1 ∂ C t − 1 + ∂ C t ∂ C t − 1 = C t − 1 σ ′ ( ⋅ ) W f ∗ o t − 1 t a n h ′ ( C t − 1 ) + C ~ t σ ′ ( ⋅ ) W i ∗ o t − 1 t a n h ′ ( C t − 1 ) + i t tanh ⁡ ′ ( ⋅ ) W C ∗ o t − 1 t a n h ′ ( C t − 1 ) + f t \begin{aligned} \cfrac{\partial C_t}{\partial C_{t-1}} &= \cfrac{\partial C_t}{\partial f_{t}}\cfrac{\partial f_t}{\partial h_{t-1}}\cfrac{\partial h_{t-1}}{\partial C_{t-1}} + \cfrac{\partial C_t}{\partial i_{t}}\cfrac{\partial i_t}{\partial h_{t-1}}\cfrac{\partial h_{t-1}}{\partial C_{t-1}} + \cfrac{\partial C_t}{\partial \widetilde{C}_{t}}\cfrac{\partial \widetilde{C}_t}{\partial h_{t-1}}\cfrac{\partial h_{t-1}}{\partial C_{t-1}} + \cfrac{\partial C_t}{\partial C_{t-1}}\\[2ex] &= C_{t-1}\sigma'(\cdot)W_f*o_{t-1}tanh'(C_{t-1}) \\ &+ \widetilde{C}_t\sigma'(\cdot)W_i*o_{t-1}tanh'(C_{t-1}) \\ &+ i_t\tanh'(\cdot)W_C*o_{t-1}tanh'(C_{t-1}) \\ &+ f_t \end{aligned} Ct1Ct=ftCtht1ftCt1ht1+itCtht1itCt1ht1+C tCtht1C tCt1ht1+Ct1Ct=Ct1σ()Wfot1tanh(Ct1)+C tσ()Wiot1tanh(Ct1)+ittanh()WCot1tanh(Ct1)+ft
  • 由于LSTM中的 ∂ C t ∂ C t − 1 \begin{aligned}\frac{\partial C_t}{\partial C_{t-1}}\end{aligned} Ct1Ct 是由 4项组成,通过参数设置可以避免这4项的值同时都很小或者同时都很大,从而避免梯度弥散或梯度爆炸。
  • 最后一项 f t f_t ft 是 forget gate 的输出值,1表示完全保留旧状态,0表示完全舍弃旧状态,那如果我们把 f t f_t ft设置成1或者是接近于1,那 ∏ j = k + 1 t ∂ C j ∂ C j − 1 \begin{aligned}\prod^t_{j=k+1}\cfrac{\partial C_j}{\partial C_{j-1}}\end{aligned} j=k+1tCj1Cj这一项就有妥妥的梯度了。
  • 因此LSTM是靠着cell结构来保留梯度,forget gate控制了对过去信息的保留程度,如果gate选择保留旧状态,那么梯度就会接近于1,可以缓解梯度消失问题。这里说缓解,是因为LSTM只是在 C t C_t Ct C t − 1 C_{t-1} Ct1 这条路上解决梯度消失问题,而其他路依然存在梯度消失问题。
  • forget gate解决了RNN中的长期依赖问题,不管网络多深,也可以记住之前的信息。
  • 我们注意到, 首先三个门的激活函数是sigmoid, 这也就意味着这三个门的输出要么接近于0 , 要么接近于1。这就使得 ∂ C t ∂ C t − 1 \begin{aligned}\frac{\partial C_t}{\partial C_{t-1}}\end{aligned} Ct1Ct 值范围在0~1之间。在实际参数更新中,可以通过控制bias比较大,使得该值接近于1;在这种情况下,即使通过很多次连乘的操作,梯度也不会消失,仍然可以保留"长距"连乘项的存在。即总可以通过选择合适的参数,在不发生梯度爆炸的情况下,找到合理的梯度方向来更新参数,而且这个方向可以充分地考虑远距离的隐含层信息的传播影响。
  • 这种情况对比RNN就很难实现,因为RNN中远距离的隐层影响要么非常强,要么就非常弱,所以难以找到合适的梯度来优化这些远距离的信息效应。
  • LSTM搞的这么复杂,除了在结构上天然地克服了梯度消失的问题,更重要的是具有更多的参数来控制模型;通过四倍于RNN的参数量,可以更加精细地预测时间序列变量。
  • 在预测过程中,LSTM可以通过一套更复杂的结构来说明深度循环神经网络中,哪些历史信息应该记住,哪些历史信息应该遗忘,以及是否每个阶段都应该有有效的信息被输出!

Khandelwal等人在论文《论文:Sharp Nearby, Fuzzy Far Away: How Neural Language Models Use Context》发现基于单词的LSTM语言模型只能有效地使用 约200个上下文(即使提供更多),并且该单词顺序仅在最后的约50个标记内有效。【Khandelwal et al. (2018) find that a word-based LSTM language model only effectively uses around 200 tokens of context (even if more is provided), and that word order only has an effect within approximately the last 50 tokens.】

五、LSTM变体

1、Peephole Connection

  • Peephole Connection变体的思路很简单,也就是我们让 门层 也会接受细胞状态的输入。其他没有改变。注意output gate接受的是更新后的细胞状态。
    在这里插入图片描述
  • 上面的图例中,我们增加了 peephole 到每个门上,但是许多论文会加入部分的 peephole 而非所有都加。
    在这里插入图片描述

2、Coupled

在这里插入图片描述

  • input gate 与 forget gate公用一个门,其他一样。不同于之前是分开确定什么忘记和需要添加什么新的信息,这里是一同做出决定。我们仅仅会当我们将要输入在当前位置时忘记。我们仅仅输入新的值到那些我们已经忘记旧的信息的那些状态 。

3、GRU(Gated Recurrent Unit)门控循环单元结构

Gated Recurrent Unit (门控循环单元,GRU)是一个改动比较大的变体。这是由 Cho, et al. (2014) 提出。它将忘记门和输入门合成了一个单一的 更新门。同样还混合了细胞状态和隐藏状态,和其他一些改动。最终的模型比标准的 LSTM 模型要简单,也是非常流行的变体。

GRU的优势与缺点:

  • 优势:GRU和LSTM作用相同, 在捕捉长序列语义关联时, 能有效抑制梯度消失或爆炸, 效果都优于传统RNN且计算复杂度相比LSTM要小.
  • 缺点:GRU仍然不能完全解决梯度消失问题, 同时其作用RNN的变体, 有着RNN结构本身的一大弊端, 即不可并行计算, 这在数据量和模型体量逐步增大的未来, 是RNN发展的关键瓶颈.

GRU的结构和计算要比LSTM更简单, 它的核心结构可以分为两个部分去解析:

  • 更新门
  • 重置门
    在这里插入图片描述
  • 和之前分析过的LSTM中的门控一样, 首先计算更新门和重置门的门值, 分别是 z t z_t zt r t r_t rt, 计算方法就是使用 x t x_t xt h t − 1 h_{t-1} ht1 拼接进行线性变换, 再经过sigmoid激活.
  • 之后重置门门值作用在了 h t − 1 h_{t-1} ht1 上, 代表控制上一时间步传来的信息有多少可以被利用.
  • 接着就是使用这个重置后的 h t − 1 h{t-1} ht1 进行基本的RNN计算, 即与 x t x_t xt 拼接进行线性变化, 经过tanh激活, 得到新的 h t h_t ht.
  • 最后更新门的门值 z t z_t zt 会作用在新的 h t h_t ht,而 1-门值 ( 1 − z t 1-z_t 1zt)会作用在 h t − 1 h_{t-1} ht1 上, 随后将两者的结果相加, 得到最终的隐含状态输出 h t h_t ht,
  • 这个过程意味着更新门有能力保留之前的结果, 当门值趋于1时, 输出就是新的 h t h_t ht, 而当门值趋于0时, 输出就是上一时间步的 h t − 1 h_{t-1} ht1.
    在这里插入图片描述
    在这里插入图片描述
import torch
import torch.nn as nn

# 实例化一个GRU模型实例
gru = nn.GRU(5, 6, 2)	# 参数含义: (input_size=5:输入X的特征维度(WordEmbedding维度);hidden_size=6:隐藏层神经元数量; num_layers=2:隐藏层的层数)
input = torch.randn(1, 3, 5)	# 初始化一个输入张量【batch_size=1表示当前批次的样本数量;3表示样本序列长度/sequence_lenght;5表示WordEmbedding维度】此处的WordEmbedding维度要与rnn对象的input_size一致
h0 = torch.randn(2, 3, 6)	# 初始化一个初始隐藏层张量,参数含义:【2表示隐藏层层数num_layers,要与rnn对象的num_layers一致;3表示样本序列长度/sequence_lenght;6表示隐藏层神经元的个数(隐层张量的特征维度的大小)】
output, hn = gru(input, h0)
print("output = \n{0}".format(output))
print("hn = \n{0}".format(hn))
output = 
tensor(
[
	[
		[-0.2097, -2.2225,  0.6204, -0.1745, -0.1749, -0.0460], [-0.3820,  0.0465, -0.4798,  0.6837, -0.7894,  0.5173], [-0.0184, -0.2758,  1.2482,  0.5514, -0.9165, -0.6667]
	]
], grad_fn=<StackBackward>
)

hn =
tensor(
[
	[
		[ 0.6578, -0.4226, -0.2129, -0.3785,  0.5070,  0.4338],[-0.5072,  0.5948,  0.8083,  0.4618,  0.1629, -0.1591], [ 0.2430, -0.4981,  0.3846, -0.4252,  0.7191,  0.5420]
	],
	
	[
		[-0.2097, -2.2225,  0.6204, -0.1745, -0.1749, -0.0460],[-0.3820,  0.0465, -0.4798,  0.6837, -0.7894,  0.5173], [-0.0184, -0.2758,  1.2482,  0.5514, -0.9165, -0.6667]
	]
], grad_fn=<StackBackward>
)

nn.GRU类初始化主要参数解释:

  • input_size: 输入张量x中特征维度的大小.
  • hidden_size: 隐层张量h中特征维度的大小.
  • num_layers: 隐含层的数量.
  • bidirectional: 是否选择使用双向LSTM, 如果为True, 则使用; 默认不使用.

nn.GRU类实例化对象主要参数解释:

  • input: 输入张量x.
  • h0: 初始化的隐层张量h.

六、Pytorch中LSTM和GRU模块使用

1、LSTM介绍

LSTM和GRU都是由torch.nn提供

通过观察文档,可知LSMT的参数,

torch.nn.LSTM(input_size,hidden_size,num_layers,batch_first,dropout,bidirectional)

  1. input_size :输入数据的形状,即embedding_dim
  2. hidden_size:隐藏层神经元的数量,即每一层有多少个LSTM单元
  3. num_layer :即RNN的中LSTM单元的层数
  4. batch_first:默认值为False,输入的数据需要[seq_len,batch,feature],如果为True,则为[batch,seq_len,feature]
  5. dropout:dropout的比例,默认值为0。dropout是一种训练过程中让部分参数随机失活的一种方式,能够提高训练速度,同时能够解决过拟合的问题。这里是在LSTM的最后一层,对每个输出进行dropout
  6. bidirectional:是否使用双向LSTM,默认是False

实例化LSTM对象之后,不仅需要传入数据,还需要前一次的h_0(前一次的隐藏状态)和c_0(前一次memory)

即:lstm(input,(h_0,c_0))

LSTM的默认输出为output, (h_n, c_n)

  1. output(seq_len, batch, num_directions * hidden_size)—>batch_first=False
  2. h_n:(num_layers * num_directions, batch, hidden_size)
  3. c_n: (num_layers * num_directions, batch, hidden_size)

2、LSTM使用示例

假设数据输入为 input ,形状是[10,20],假设embedding的形状是[100,30]

则LSTM使用示例如下:

batch_size =10
seq_len = 20
embedding_dim = 30
word_vocab = 100
hidden_size = 18
num_layer = 2

#准备输入数据
input = torch.randint(low=0,high=100,size=(batch_size,seq_len))
#准备embedding
embedding  = torch.nn.Embedding(word_vocab,embedding_dim)
lstm = torch.nn.LSTM(embedding_dim,hidden_size,num_layer)

#进行mebed操作
embed = embedding(input) #[10,20,30]

#转化数据为batch_first=False
embed = embed.permute(1,0,2) #[20,10,30]

#初始化状态, 如果不初始化,torch默认初始值为全0
h_0 = torch.rand(num_layer,batch_size,hidden_size)
c_0 = torch.rand(num_layer,batch_size,hidden_size)
output,(h_1,c_1) = lstm(embed,(h_0,c_0))
#output [20,10,1*18]
#h_1 [2,10,18]
#c_1 [2,10,18]

输出如下

In [122]: output.size()
Out[122]: torch.Size([20, 10, 18])

In [123]: h_1.size()
Out[123]: torch.Size([2, 10, 18])

In [124]: c_1.size()
Out[124]: torch.Size([2, 10, 18])

通过前面的学习,我们知道,最后一次的h_1应该和output的最后一个time step的输出是一样的

通过下面的代码,我们来验证一下:

In [179]: a = output[-1,:,:]

In [180]: a.size()
Out[180]: torch.Size([10, 18])

In [183]: b.size()
Out[183]: torch.Size([10, 18])
In [184]: a == b
Out[184]:
tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]],
       dtype=torch.uint8)

3、GRU的使用示例

GRU模块torch.nn.GRU,和LSTM的参数相同,含义相同,具体可参考文档

但是输入只剩下gru(input,h_0),输出为output, h_n

其形状为:

  1. output:(seq_len, batch, num_directions * hidden_size)
  2. h_n:(num_layers * num_directions, batch, hidden_size)

大家可以使用上述代码,观察GRU的输出形式

4、双向LSTM

如果需要使用双向LSTM,则在实例化LSTM的过程中,需要把LSTM中的bidriectional设置为True,同时h_0和c_0使用num_layer*2

观察效果,输出为

batch_size =10 #句子的数量
seq_len = 20  #每个句子的长度
embedding_dim = 30  #每个词语使用多长的向量表示
word_vocab = 100  #词典中词语的总数
hidden_size = 18  #隐层中lstm的个数
num_layer = 2  #多少个隐藏层

input = torch.randint(low=0,high=100,size=(batch_size,seq_len))
embedding  = torch.nn.Embedding(word_vocab,embedding_dim)
lstm = torch.nn.LSTM(embedding_dim,hidden_size,num_layer,bidirectional=True)

embed = embedding(input) #[10,20,30]

#转化数据为batch_first=False
embed = embed.permute(1,0,2) #[20,10,30]
h_0 = torch.rand(num_layer*2,batch_size,hidden_size)
c_0 = torch.rand(num_layer*2,batch_size,hidden_size)
output,(h_1,c_1) = lstm(embed,(h_0,c_0))

In [135]: output.size()
Out[135]: torch.Size([20, 10, 36])

In [136]: h_1.size()
Out[136]: torch.Size([4, 10, 18])

In [137]: c_1.size()
Out[137]: torch.Size([4, 10, 18])

在单向LSTM中,最后一个time step的输出的前hidden_size个和最后一层隐藏状态h_1的输出相同,那么双向LSTM呢?

双向LSTM中:

output:按照正反计算的结果顺序在第2个维度进行拼接,正向第一个拼接反向的最后一个输出

hidden state:按照得到的结果在第0个维度进行拼接,正向第一个之后接着是反向第一个

  1. 前向的LSTM中,最后一个time step的输出的前hidden_size个和最后一层向前传播h_1的输出相同

    • 示例:

    • #-1是前向LSTM的最后一个,前18是前hidden_size个
      In [188]: a = output[-1,:,:18]  #前项LSTM中最后一个time step的output
      
      In [189]: b = h_1[-2,:,:]  #倒数第二个为前向
      
      In [190]: a.size()
      Out[190]: torch.Size([10, 18])
      
      In [191]: b.size()
      Out[191]: torch.Size([10, 18])
      
      In [192]: a == b
      Out[192]:
      tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
              [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
              [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
              [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
              [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
              [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
              [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
              [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
              [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
              [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]],
             dtype=torch.uint8)
      
  2. 后向LSTM中,最后一个time step的输出的后hidden_size个和最后一层后向传播的h_1的输出相同

    • 示例

    • #0 是反向LSTM的最后一个,后18是后hidden_size个
      In [196]: c = output[0,:,18:]  #后向LSTM中的最后一个输出
      
      In [197]: d = h_1[-1,:,:] #后向LSTM中的最后一个隐藏层状态
      
      In [198]: c == d
      Out[198]:
      tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
              [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
              [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
              [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
              [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
              [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
              [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
              [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
              [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
              [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]],
             dtype=torch.uint8)
      

5、LSTM和GRU的使用注意点

  1. 第一次调用之前,需要初始化隐藏状态,如果不初始化,默认创建全为0的隐藏状态
  2. 往往会使用LSTM or GRU 的输出的最后一维的结果,来代表LSTM、GRU对文本处理的结果,其形状为[batch, num_directions*hidden_size]
    1. 并不是所有模型都会使用最后一维的结果
    2. 如果实例化LSTM的过程中,batch_first=False,则output[-1] or output[-1,:,:]可以获取最后一维
    3. 如果实例化LSTM的过程中,batch_first=True,则output[:,-1,:]可以获取最后一维
  3. 如果结果是(seq_len, batch_size, num_directions * hidden_size),需要把它转化为(batch_size,seq_len, num_directions * hidden_size)的形状,不能够不是view等变形的方法,需要使用output.permute(1,0,2),即交换0和1轴,实现上述效果
  4. 使用双向LSTM的时候,往往会分别使用每个方向最后一次的output,作为当前数据经过双向LSTM的结果
    • 即:torch.cat([h_1[-2,:,:],h_1[-1,:,:]],dim=-1)
    • 最后的表示的size是[batch_size,hidden_size*2]
  5. 上述内容在GRU中同理

6、LSTM模型构建代码

class LSTMmodel(nn.Module):
    def __init__(self):
        super(LSTMmodel,self).__init__()
        self.hidden_size = 64
        self.embedding_dim = 200
        self.num_layer = 2
        self.bidriectional = True
        self.bi_num = 2 if self.bidriectional else 1
        self.dropout = 0.5
        #以上部分为超参数,可以自行修改

        self.embedding = nn.Embedding(len(ws),self.embedding_dim,padding_idx=ws.PAD) #[N,300]
        self.lstm = nn.LSTM(self.embedding_dim,self.hidden_size,self.num_layer,bidirectional=True,dropout=self.dropout)
        #使用两个全连接层,中间使用relu激活函数
        self.fc = nn.Linear(self.hidden_size*self.bi_num,20)
        self.fc2 = nn.Linear(20,2)


    def forward(self, x):
        x = self.embedding(x)
        x = x.permute(1,0,2) #进行轴交换
        h_0,c_0 = self.init_hidden_state(x.size(1))
        _,(h_n,c_n) = self.lstm(x,(h_0,c_0))

        #只要最后一个lstm单元处理的结果,这里多去的hidden state
        out = torch.cat([h_n[-2, :, :], h_n[-1, :, :]], dim=-1)
        out = self.fc(out)
        out = F.relu(out)
        out = self.fc2(out)
        return F.log_softmax(out,dim=-1)

    def init_hidden_state(self,batch_size):
        h_0 = torch.rand(self.num_layer * self.bi_num, batch_size, self.hidden_size).to(device)
        c_0 = torch.rand(self.num_layer * self.bi_num, batch_size, self.hidden_size).to(device)
        return h_0,c_0

七、RNN自定义模型【新闻主题四分类任务】

以一段新闻报道中的文本描述内容为输入, 使用模型帮助我们判断它最有可能属于哪一种类型的新闻, 这是典型的文本分类问题, 我们这里假定每种类型是互斥的, 即文本描述有且只有一种类型.

# 导入相关的torch工具包
import torch
import torchtext
# 导入torchtext.datasets中的文本分类任务
from torchtext.datasets import text_classification
import os
# 导入必备的torch模型构建工具
import torch.nn as nn
import torch.nn.functional as F
# 导入torch中的数据加载器方法
from torch.utils.data import DataLoader
# 导入时间工具包
import time
# 导入数据随机划分方法工具
from torch.utils.data.dataset import random_split

# 一、通过torchtext获取数据
# 定义数据下载路径, 当前路径的data文件夹
load_data_path = "./data"
# 如果不存在该路径, 则创建这个路径
if not os.path.isdir(load_data_path):
    os.mkdir(load_data_path)

# 选取torchtext中的文本分类数据集'AG_NEWS'即新闻主题分类数据, 保存在指定目录下
# 并将数值映射后的训练和验证数据加载到内存中
train_dataset, test_dataset = text_classification.DATASETS['AG_NEWS'](root=load_data_path)

# 二: 构建带有Embedding层的文本分类模型
# 指定BATCH_SIZE的大小
BATCH_SIZE = 16

# 进行可用设备检测, 有GPU的话将优先使用GPU
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

class TextSentiment(nn.Module):
    """文本分类模型"""

    def __init__(self, vocab_size, embed_dim, num_class):
        """
        description: 类的初始化函数
        :param vocab_size: 整个语料包含的不同词汇总数
        :param embed_dim: 指定词嵌入的维度
        :param num_class: 文本分类的类别总数
        """
        super().__init__()
        # 实例化embedding层, sparse=True代表每次对该层求解梯度时, 只更新部分权重.
        self.embedding = nn.Embedding(vocab_size, embed_dim, sparse=True)
        # 实例化线性层, 参数分别是embed_dim和num_class.
        self.fc = nn.Linear(embed_dim, num_class)
        # 为各层初始化权重
        self.init_weights()

    def init_weights(self):
        """初始化权重函数"""
        # 指定初始权重的取值范围数
        initrange = 0.5
        # 各层的权重参数都是初始化为均匀分布
        self.embedding.weight.data.uniform_(-initrange, initrange)
        self.fc.weight.data.uniform_(-initrange, initrange)
        # 偏置初始化为0
        self.fc.bias.data.zero_()

    def forward(self, text):
        """
        :param text: 文本数值映射后的结果
        :return: 与类别数尺寸相同的张量, 用以判断文本类别
        """
        # 获得embedding的结果embedded
        # >>> embedded.shape
        # (m, 32) 其中m是BATCH_SIZE大小的数据中词汇总数
        embedded = self.embedding(text)
        # 接下来我们需要将(m, 32)转化成(BATCH_SIZE, 32)
        # 以便通过fc层后能计算相应的损失
        # 首先, 我们已知m的值远大于BATCH_SIZE=16,
        # 用m整除BATCH_SIZE, 获得m中共包含c个BATCH_SIZE
        c = embedded.size(0) // BATCH_SIZE
        # 之后再从embedded中取c*BATCH_SIZE个向量得到新的embedded
        # 这个新的embedded中的向量个数可以整除BATCH_SIZE
        embedded = embedded[:BATCH_SIZE * c]
        # 因为我们想利用平均池化的方法求embedded中指定行数的列的平均数,
        # 但平均池化方法是作用在行上的, 并且需要3维输入
        # 因此我们对新的embedded进行转置并拓展维度
        embedded = embedded.transpose(1, 0).unsqueeze(0)
        # 然后就是调用平均池化的方法, 并且核的大小为c
        # 即取每c的元素计算一次均值作为结果
        embedded = F.avg_pool1d(embedded, kernel_size=c)
        # 最后,还需要减去新增的维度, 然后转置回去输送给fc层
        return self.fc(embedded[0].transpose(1, 0))


# 对数据进行batch处理
def generate_batch(batch):
    """
    description: 生成batch数据函数
    :param batch: 由样本张量和对应标签的元组组成的batch_size大小的列表
                  形如:
                  [(label1, sample1), (lable2, sample2), ..., (labelN, sampleN)]
    return: 样本张量和标签各自的列表形式(张量)
             形如:
             text = tensor([sample1, sample2, ..., sampleN])
             label = tensor([label1, label2, ..., labelN])
    """
    # 从batch中获得标签张量
    label = torch.tensor([entry[0] for entry in batch])
    # 从batch中获得样本张量
    text = [entry[1] for entry in batch]
    text = torch.cat(text)
    # 返回结果
    return text, label



# 三: 构建训练函数
def train(train_data):
    """模型训练函数"""
    # 初始化训练损失和准确率为0
    train_loss = 0
    train_acc = 0

    # 使用数据加载器生成BATCH_SIZE大小的数据进行批次训练
    # data就是N多个generate_batch函数处理后的BATCH_SIZE大小的数据生成器
    data = DataLoader(train_data, batch_size=BATCH_SIZE, shuffle=True, collate_fn=generate_batch)

    # 对data进行循环遍历, 使用每个batch的数据进行参数更新
    for i, (text, cls) in enumerate(data):
        # 设置优化器初始梯度为0
        optimizer.zero_grad()
        # 模型输入一个批次数据, 获得输出
        output = model(text)
        # 根据真实标签与模型输出计算损失
        loss = criterion(output, cls)
        # 将该批次的损失加到总损失中
        train_loss += loss.item()
        # 误差反向传播
        loss.backward()
        # 参数进行更新
        optimizer.step()
        # 将该批次的准确率加到总准确率中
        train_acc += (output.argmax(1) == cls).sum().item()

    # 调整优化器学习率
    scheduler.step()

    # 返回本轮训练的平均损失和平均准确率
    return train_loss / len(train_data), train_acc / len(train_data)


# 四: 构建验证函数
def valid(valid_data):
    """模型验证函数"""
    # 初始化验证损失和准确率为0
    loss = 0
    acc = 0

    # 和训练相同, 使用DataLoader获得训练数据生成器
    data = DataLoader(valid_data, batch_size=BATCH_SIZE, collate_fn=generate_batch)
    # 按批次取出数据验证
    for text, cls in data:
        # 验证阶段, 不再求解梯度
        with torch.no_grad():
            # 使用模型获得输出
            output = model(text)
            # 计算损失
            loss = criterion(output, cls)
            # 将损失和准确率加到总损失和准确率中
            loss += loss.item()
            acc += (output.argmax(1) == cls).sum().item()

    # 返回本轮验证的平均损失和平均准确率
    return loss / len(valid_data), acc / len(valid_data)


if "__name__" == "__main__":

    # 五、实例化模型
    # 获得整个语料包含的不同词汇总数
    VOCAB_SIZE = len(train_dataset.get_vocab())
    # 指定词嵌入维度
    EMBED_DIM = 32
    # 获得类别总数
    NUN_CLASS = len(train_dataset.get_labels())
    # 实例化模型
    model = TextSentiment(VOCAB_SIZE, EMBED_DIM, NUN_CLASS).to(device)

    # 六、进行模型训练和验证
    # 指定训练轮数
    N_EPOCHS = 10

    # 定义初始的验证损失
    min_valid_loss = float('inf')

    # 选择损失函数, 这里选择预定义的交叉熵损失函数
    criterion = torch.nn.CrossEntropyLoss().to(device)
    # 选择随机梯度下降优化器
    optimizer = torch.optim.SGD(model.parameters(), lr=4.0)
    # 选择优化器步长调节方法StepLR, 用来衰减学习率
    scheduler = torch.optim.lr_scheduler.StepLR(optimizer, 1, gamma=0.9)

    # 从train_dataset取出0.95作为训练集, 先取其长度
    train_len = int(len(train_dataset) * 0.95)

    # 然后使用random_split进行乱序划分, 得到对应的训练集和验证集
    sub_train_, sub_valid_ = random_split(train_dataset, [train_len, len(train_dataset) - train_len])

    # 开始每一轮训练
    for epoch in range(N_EPOCHS):
        # 记录概论训练的开始时间
        start_time = time.time()
        # 调用train和valid函数得到训练和验证的平均损失, 平均准确率
        train_loss, train_acc = train(sub_train_)
        valid_loss, valid_acc = valid(sub_valid_)

        # 计算训练和验证的总耗时(秒)
        secs = int(time.time() - start_time)
        # 用分钟和秒表示
        mins = secs / 60
        secs = secs % 60

        # 打印训练和验证耗时,平均损失,平均准确率
        print('Epoch: %d' % (epoch + 1), " | time in %d minutes, %d seconds" % (mins, secs))
        print(f'\tLoss: {train_loss:.4f}(train)\t|\tAcc: {train_acc * 100:.1f}%(train)')
        print(f'\tLoss: {valid_loss:.4f}(valid)\t|\tAcc: {valid_acc * 100:.1f}%(valid)')

八、Pytorch创建RNN、LSTM、GRU神经网络模型【人名所属国家分类器】

在这里插入图片描述
以一个人名为输入, 使用模型帮助我们判断它最有可能是来自哪一个国家的人名, 这在某些国际化公司的业务中具有重要意义, 在用户注册过程中, 会根据用户填写的名字直接给他分配可能的国家或地区选项, 以及该国家或地区的国旗, 限制手机号码位数等等.

数据下载地址: https://download.pytorch.org/tutorial/data.zip

数据文件预览:

- data/
    - names/
        Arabic.txt
        Chinese.txt
        Czech.txt
        Dutch.txt
        English.txt
        French.txt
        German.txt
        Greek.txt
        Irish.txt
        Italian.txt
        Japanese.txt
        Korean.txt
        Polish.txt
        Portuguese.txt
        Russian.txt
        Scottish.txt
        Spanish.txt
        Vietnamese.txt

Chiness.txt预览:

Ang
Au-Yong
Bai
Ban
Bao
Bei
Bian
Bui
Cai
Cao
Cen
Chai
Chaim
Chan
Chang
Chao
Che
Chen
Cheng

整个案例的实现可分为以下五个步骤:

  • 第一步: 导入必备的工具包.

    • python版本使用3.6.x, pytorch版本使用1.3.1
  • 第二步: 对data文件中的数据进行处理,满足训练要求.

    • 定义数据集路径并获取常用的字符数量.
    • 字符规范化之unicode转Ascii函数unicodeToAscii.
    • 构建一个从持久化文件中读取内容到内存的函数readLines.
    • 构建人名类别(所属的语言)列表与人名对应关系字典
    • 将人名转化为对应onehot张量表示函数lineToTensor
  • 第三步: 构建RNN模型(包括传统RNN, LSTM以及GRU).

    • 构建传统的RNN模型的类class RNN.
    • 构建LSTM模型的类class LSTM.
    • 构建GRU模型的类class GRU.
  • 第四步: 构建训练函数并进行训练.

    • 从输出结果中获得指定类别函数categoryFromOutput.
    • 随机生成训练数据函数randomTrainingExample.
    • 构建传统RNN训练函数trainRNN.
    • 构建LSTM训练函数trainLSTM.
    • 构建GRU训练函数trainGRU.
    • 构建时间计算函数timeSince.
    • 构建训练过程的日志打印函数train.得到损失对比曲线和训练耗时对比图.
  • 第五步: 构建评估函数并进行预测.

    • 构建传统RNN评估函数evaluateRNN.
    • 构建LSTM评估函数evaluateLSTM.
    • 构建GRU评估函数evaluateGRU.
    • 构建预测函数predict.
# 从io中导入文件打开方法
from io import open
# 帮助使用正则表达式进行子目录的查询
import glob
import os
# 用于获得常见字母及字符规范化
import string
import unicodedata
# 导入随机工具random
import random
# 导入时间和数学工具包
import time
import math
# 导入torch工具
import torch
# 导入nn准备构建模型
import torch.nn as nn
# 引入制图工具包
import matplotlib.pyplot as plt

data_path = "./data/names/"  # 下载地址:https://download.pytorch.org/tutorial/data.zip

# 第一步: 对data文件中的数据进行处理,满足训练要求
# 1.1 获取常用的字符数量
all_letters = string.ascii_letters + " .,;'"  # 获取所有常用字符包括字母和常用标点
n_letters = len(all_letters)  # 获取常用字符数量
print("n_letter:", n_letters)


# 1.2 字符规范化之unicode转Ascii函数【关于编码问题我们暂且不去考虑,我们认为这个函数的作用就是去掉一些语言中的重音标记。如: Ślusàrski ---> Slusarski】
def unicodeToAscii(s):
    return ''.join(
        c for c in unicodedata.normalize('NFD', s)
        if unicodedata.category(c) != 'Mn' and c in all_letters
    )


s = "Ślusàrski"
a = unicodeToAscii(s)
print("a = {0}".format(a))  # a = Slusarski


# 1.3 构建一个从持久化文件中读取内容到内存的函数
def readLines(filename):  # 从文件中读取每一行加载到内存中形成列表
    names = open(filename, encoding='utf-8').read().strip().split('\n')  # 打开指定文件并读取所有内容, 使用strip()去除两侧空白符, 然后以'\n'进行切分
    return [unicodeToAscii(name) for name in names]  # 对应每一个names列表中的名字进行Ascii转换, 使其规范化.最后返回一个名字列表


filename = data_path + "Chinese.txt"  # filename是数据集中某个具体的文件, 我们这里选择Chinese.txt
results = readLines(filename)
print("results[:20] = {0}".format(results[:20]))  # results[:20] = ['Ang', 'AuYong', 'Bai', 'Ban', 'Bao', 'Bei', 'Bian', 'Bui', 'Cai', 'Cao', 'Cen', 'Chai', 'Chaim', 'Chan', 'Chang', 'Chao', 'Che', 'Chen', 'Cheng', 'Cheung']

# 1.4 构建人名类别(所属的语言)列表与人名对应关系字典【构建的category_names形如:{"English":["Lily", "Susan", "Kobe"], "Chinese":["Zhang San", "Xiao Ming"]}】
category_names_dict = {}
all_categories_list = []  # all_categories形如: ["English",...,"Chinese"]
# 读取指定路径下的txt文件, 使用glob,path中可以使用正则表达式
for filename in glob.glob(data_path + '*.txt'):
    category = os.path.splitext(os.path.basename(filename))[0]  # 获取每个文件的文件名, 就是对应的名字类别
    all_categories_list.append(category)  # 将其逐一装到all_categories列表中
    names = readLines(filename)  # 然后读取每个文件的内容,形成名字列表
    category_names_dict[category] = names  # 按照对应的类别,将名字列表写入到category_names字典中
categories_size = len(all_categories_list)  # 查看类别总数
print("categories_size = {0}, all_categories_list = {1}".format(categories_size, all_categories_list))
print("category_names_dict['Italian'][:5] = {0}".format(category_names_dict['Italian'][:5]))  # 随便查看其中的一些内容【category_names_dict['Italian'][:5] = ['Abandonato', 'Abatangelo', 'Abatantuono', 'Abate', 'Abategiovanni']】


# 1.5 将人名转化为对应onehot张量表示
# 将字符串(单词粒度)转化为张量表示,如:"ab" --->
# tensor([[[1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
#          0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
#          0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
#          0., 0., 0., 0., 0., 0.]],

#        [[0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
#          0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
#          0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
#          0., 0., 0., 0., 0., 0.]]])
# 将人名转化为对应onehot张量表示, 参数name是输入的人名
def nameToTensor(name):
    name_tensor = torch.zeros(len(name), 1, n_letters)  # 首先初始化一个0张量, 它的形状(len(name), 1, n_letters),代表人名中的每个字母用一个(1*n_letters)的张量表示.
    for li, letter in enumerate(name):  # 遍历这个人名中的每个字符索引和字符
        name_tensor[li][0][all_letters.find(letter)] = 1  # 使用字符串方法find找到每个字符在all_letters中的索引,它也是我们生成onehot张量中1的索引位置
    return name_tensor


print("nameToTensor('Bai') = {0}".format(nameToTensor('Bai')))


# 第二步: 构建RNN模型
# 2.1 构建传统的RNN模型(使用nn.RNN构建完成传统RNN使用类)
class RNN(nn.Module):
    # 初始化函数中有4个参数,【input_size:代表输入张量的最后一维尺寸;hidden_size:代表RNN的隐层最后一维尺寸;output_size:代表最后线性层的输出维度;num_layers:代表RNN网络的层数】
    def __init__(self, input_size, hidden_size, output_size, num_layers=1):
        super(RNN, self).__init__()
        self.input_size = input_size
        self.hidden_size = hidden_size
        self.output_size = output_size
        self.num_layers = num_layers
        self.rnn = nn.RNN(input_size, hidden_size, num_layers)  # 实例化预定义的nn.RNN, 它的三个参数分别是input_size, hidden_size, num_layers
        self.namear = nn.Linear(hidden_size, output_size)  # 实例化全连接层, 这个线性层用于将nn.RNN的输出维度转化为指定的输出维度
        self.softmax = nn.LogSoftmax(dim=-1)  # 实例化nn中预定的Softmax层, 用于从输出层获得类别结果

    # 完成传统RNN中的主要逻辑【input代表输入张量, 它的形状是1*n_letters;hidden代表RNN的隐层张量, 它的形状是self.num_layers*1*self.hidden_size】
    def forward(self, input, hidden):
        input = input.unsqueeze(0)  # 因为预定义的nn.RNN要求输入维度一定是三维张量, 因此在这里使用unsqueeze(0)扩展一个维度
        # print("input.shape = {0},hidden.shape = {1}".format(input.shape, hidden.shape))
        rnn_output, hidden = self.rnn(input, hidden)  # 将input和hidden输入到传统RNN的实例化对象中,如果num_layers=1, rnn_output恒等于hidden
        namear_output = self.namear(rnn_output)  # 将从rnn中获得的结果通过线性变换转化为指定的输出维度
        softmax_output = self.softmax(namear_output)  # 将线性变换后的输出结果通过softmax返回分类结果
        return softmax_output, hidden  # 返回softmax结果,每次的softmax被下一次覆盖;hidden作为后续rnn的输入

    # 初始化隐层张量:初始化一个(self.num_layers, 1, self.hidden_size)形状的0张量
    def initHidden(self):
        hidden_tensor = torch.zeros(self.num_layers, 1, self.hidden_size)
        return hidden_tensor


# 2.2 使用nn.LSTM构建完成LSTM使用类
class LSTM(nn.Module):
    # 初始化函数中有4个参数,【input_size:代表输入张量的最后一维尺寸;hidden_size:代表LSTM的隐层最后一维尺寸;output_size:代表最后线性层的输出维度;num_layers:代表LSTM网络的层数】
    def __init__(self, input_size, hidden_size, output_size, num_layers=1):
        super(LSTM, self).__init__()
        self.input_size = input_size
        self.hidden_size = hidden_size
        self.output_size = output_size
        self.num_layers = num_layers
        self.lstm = nn.LSTM(input_size, hidden_size, num_layers)  # 实例化预定义的nn.LSTM, 它的三个参数分别是input_size, hidden_size, num_layers
        self.namear = nn.Linear(hidden_size, output_size)  # 实例化全连接层, 这个线性层用于将nn.LSTM的输出维度转化为指定的输出维度
        self.softmax = nn.LogSoftmax(dim=-1)  # 实例化nn中预定的Softmax层, 用于从输出层获得类别结果

    # 完成LSTM中的主要逻辑【input代表输入张量, 它的形状是1*n_letters;hidden代表RNN的隐层张量, 它的形状是self.num_layers*1*self.hidden_size;cell代表LSTM中的细胞状态张量】
    def forward(self, input, hidden, cell):
        input = input.unsqueeze(0)  # 因为预定义的nn.LSTM要求输入维度一定是三维张量, 因此在这里使用unsqueeze(0)扩展一个维度
        lstm_output, (hidden, cell) = self.lstm(input, (hidden, cell))  # 将input, hidden以及初始化的cell传入lstm中
        namear_output = self.namear(lstm_output)  # 将从lstm中获得的结果通过线性变换转化为指定的输出维度
        softmax_output = self.softmax(namear_output)  # 将线性变换后的输出结果通过softmax返回分类结果
        return softmax_output, hidden, cell  # 最后返回处理后的softmax_output, 同时返回hidden, cell作为后续lstm的输入

    # 初始化隐层张量:不仅初始化hidden还要初始化细胞状态cell, 它们形状相同"""
    def initHiddenAndCell(self):
        cell_tensor = hidden_tensor = torch.zeros(self.num_layers, 1, self.hidden_size)
        return hidden_tensor, cell_tensor


# 2.3 使用nn.GRU构建完成传统RNN使用类【GRU与传统RNN的外部形式相同, 都是只传递隐层张量, 因此只需要更改预定义层的名字】
class GRU(nn.Module):
    # 初始化函数中有4个参数,【input_size:代表输入张量的最后一维尺寸;hidden_size:代表GRU的隐层最后一维尺寸;output_size:代表最后线性层的输出维度;num_layers:代表GRU网络的层数】
    def __init__(self, input_size, hidden_size, output_size, num_layers=1):
        super(GRU, self).__init__()
        self.input_size = input_size
        self.hidden_size = hidden_size
        self.output_size = output_size
        self.num_layers = num_layers
        self.gru = nn.GRU(input_size, hidden_size, num_layers)  # 实例化预定义的nn.GRU, 它的三个参数分别是input_size, hidden_size, num_layers
        self.namear = nn.Linear(hidden_size, output_size)  # 实例化全连接层, 这个线性层用于将nn.RNN的输出维度转化为指定的输出维度
        self.softmax = nn.LogSoftmax(dim=-1)  # 实例化nn中预定的Softmax层, 用于从输出层获得类别结果

    # 完成GRU中的主要逻辑【input代表输入张量, 它的形状是1*n_letters;hidden代表GRU的隐层张量, 它的形状是self.num_layers*1*self.hidden_size】
    def forward(self, input, hidden):
        input = input.unsqueeze(0)  # 因为预定义的nn.RNN要求输入维度一定是三维张量, 因此在这里使用unsqueeze(0)扩展一个维度
        gru_output, hidden = self.gru(input, hidden)  # 将input和hidden输入到GRU的实例化对象中
        namear_output = self.namear(gru_output)  # 将从gru中获得的结果通过线性变换转化为指定的输出维度
        softmax_output = self.softmax(namear_output)  # 将线性变换后的输出结果通过softmax返回分类结果
        return softmax_output, hidden

    # 初始化隐层张量:初始化一个(self.num_layers, 1, self.hidden_size)形状的0张量
    def initHidden(self):
        hidden_tensor = torch.zeros(self.num_layers, 1, self.hidden_size)
        return hidden_tensor


# 第三步: 测试模型
# 3.1 实例化参数
input_size = n_letters  # 因为是onehot编码, 输入张量最后一维的尺寸就是n_letters
hidden_size = 128  # 定义隐层的最后一维尺寸大小
output_size = categories_size  # 输出尺寸为语言类别总数n_categories
num_layer = 1  # num_layer使用默认值, num_layers = 1
# 3.2 输入参数
input = nameToTensor('B').squeeze(0)  # 假如我们以一个字母B作为RNN的首次输入, 它通过nameToTensor转为张量【因为我们的nameToTensor输出是三维张量, 而RNN类需要的二维张量,因此需要使用squeeze(0)降低一个维度】
cell = hidden = torch.zeros(1, 1, hidden_size)  # 初始化一个三维的隐层0张量, 也是初始的细胞状态张量
# 3.3 实例化模型
rnn = RNN(input_size, hidden_size, output_size)
lstm = LSTM(input_size, hidden_size, output_size)
gru = GRU(input_size, hidden_size, output_size)
# 3.4 调用模型
rnn_output, next_hidden = rnn(input, hidden)
print("\n\nrnn_output.shape = {0}, \nrnn_output = {1}\nnext_hidden.shape = {2}, \nnext_hidden[:,:,:5] = {3}".format(rnn_output.shape, rnn_output, next_hidden.shape, next_hidden[:, :, :5]))
lstm_output, next_hidden, cell = lstm(input, hidden, cell)
print("\n\nlstm_output.shape = {0}, \nlstm_output = {1}\nnext_hidden.shape = {2}, \nnext_hidden[:,:,:5] = {3}\ncell.shape = {2}, \ncell[:,:,:5] = {3}".format(rnn_output.shape, rnn_output, next_hidden.shape, next_hidden[:, :, :5], cell.shape, cell[:, :, :5]))
gru_output, next_hidden = gru(input, hidden)
print("\n\ngru_output.shape = {0}, \ngru_output = {1}\nnext_hidden.shape = {2}, \nnext_hidden[:,:,:5] = {3}".format(gru_output.shape, gru_output, next_hidden.shape, next_hidden[:, :, :5]))


# 第四步: 工具函数
# 4.1 从输出结果中获得指定类别函数, 参数为输出张量output
def categoryFromOutput(output):
    top_values, top_indexs = output.topk(1)  # 调用topk()函数,从输出张量中返回最大的值及其索引对象,从而得到类别信息
    category_index = top_indexs[0].item()  # 从top_indexs对象中取出索引的值
    category_name = all_categories_list[category_index]  # 从前面已经构造好的all_categories数组中根据索引值获得对应语言类别
    return category_name, category_index  # 返回语言类别和索引值


category, category_index = categoryFromOutput(rnn_output)
print("category = {0}----category_index = {1}".format(category, category_index))


# 4.2 随机生成训练数据
def randomTrainingExample():
    category_name_random = random.choice(all_categories_list)  # 首先使用random.choice()方法从all_categories随机选择一个类别
    names = category_names_dict[category_name_random]  # 然后在通过category_names字典取category_name_random类别对应的名字列表
    name = random.choice(names)  # 之后再从名字列表names中随机取一个名字
    category_tensor = torch.tensor([all_categories_list.index(category_name_random)], dtype=torch.long)  # 接着将这个类别在所有类别列表中的索引封装成tensor, 得到类别张量category_tensor
    name_tensor = nameToTensor(name)  # 最后, 将随机取到的名字通过函数 nameToTensor 转化为onehot张量表示
    return category_name_random, category_tensor, name, name_tensor


for i in range(10):  # 随机取出十个进行结果查看
    category, category_tensor, name, name_tensor = randomTrainingExample()
    print("category = {0}, category_tensor = {1}, name = {2}".format(category, category_tensor, name))
print("name_tensor = \n{0}".format(name_tensor))


# 4.3 时间计算函数【获得每次打印的训练耗时, since是训练开始时间】
def timeSince(since):
    now = time.time()  # 获得当前时间
    s = now - since  # 获得时间差,就是训练耗时
    m = math.floor(s / 60)  # 将秒转化为分钟, 并取整
    s -= m * 60  # 计算剩下不够凑成1分钟的秒数
    return '%dm %ds' % (m, s)  # 返回指定格式的耗时


period = timeSince(time.time() - 10 * 60)  # 假定模型训练开始时间是10min之前
print("测试:period = {0}".format(period))

# 第五步: 构建训练函数
criterion = nn.NLLLoss()  # 定义损失函数为nn.NLLLoss,因为RNN的最后一层是nn.LogSoftmax, 两者的内部计算逻辑正好能够吻合.
learning_rate = 0.005  # 设置学习率为0.005


# 5.1 构建传统RNN训练函数【参数category_tensor代表类别的张量表示, 相当于训练数据的标签;name_tensor代表名字的张量表示, 相当于对应训练数据的特征数据值】
def trainRNN(category_tensor, name_tensor):
    hidden_tensor = rnn.initHidden()  # 初始化对象rnn的隐层张量
    rnn.zero_grad()  # 然后将模型结构中的梯度归0
    # 开始进行训练,训练结束后得到最后一次的softmax_output、hidden_tensor
    for i in range(name_tensor.size()[0]):  # 将训练数据name_tensor的每个字符逐个传入rnn之中, 得到最终结果
        softmax_output, hidden_tensor = rnn(name_tensor[i], hidden_tensor)  # 返回的softmax_output被下一次的softmax_output覆盖,hidden_tensor作为下一个时间步的rnn的输入
    loss = criterion(softmax_output.squeeze(0), category_tensor)  # 因为我们的rnn对象由nn.RNN实例化得到, 最终输出形状是三维张量, 为了满足于category_tensor进行对比计算损失, 需要减少第一个维度, 这里使用squeeze()方法
    loss.backward()  # 损失进行反向传播
    for p in rnn.parameters():  # 显示地更新模型中所有的参数【此处没有用自动参数更新函数】
        p.data.add_(-learning_rate, p.grad.data)  # 原地覆盖:将“参数p”+“参数p的梯度×学习率”的结果来更新参数p
    return softmax_output, loss.item()  # 返回结果和损失的值


# 5.2 构建LSTM训练函数【参数category_tensor代表类别的张量表示, 相当于训练数据的标签;name_tensor代表名字的张量表示, 相当于对应训练数据的特征数据值】
def trainLSTM(category_tensor, name_tensor):
    hidden, cell = lstm.initHiddenAndCell()  # 初始化对象rnn的隐层张量
    lstm.zero_grad()  # 然后将模型结构中的梯度归0
    # 开始进行训练,训练结束后得到最后一次的softmax_output、hidden、cell
    for i in range(name_tensor.size()[0]):  # 将训练数据name_tensor的每个字符逐个传入rnn之中, 得到最终结果
        softmax_output, hidden, cell = lstm(name_tensor[i], hidden, cell)  # 返回的softmax_output被下一次的softmax_output覆盖,hidden、cell作为下一个时间步的lstm的输入
    loss = criterion(softmax_output.squeeze(0), category_tensor)
    loss.backward()  # 损失进行反向传播
    for p in lstm.parameters():  # 显示地更新模型中所有的参数【此处没有用自动参数更新函数】
        p.data.add_(-learning_rate, p.grad.data)  # 原地覆盖:将“参数p”+“参数p的梯度×学习率”的结果来更新参数p
    return softmax_output, loss.item()  # 返回结果和损失的值


# 5.3 构建GRU训练函数【参数category_tensor代表类别的张量表示, 相当于训练数据的标签;name_tensor代表名字的张量表示, 相当于对应训练数据的特征数据值】
def trainGRU(category_tensor, name_tensor):
    hidden_tensor = gru.initHidden()  # 初始化对象rnn的隐层张量
    gru.zero_grad()  # 然后将模型结构中的梯度归0
    # 开始进行训练,训练结束后得到最后一次的softmax_output、hidden_tensor
    for i in range(name_tensor.size()[0]):  # 将训练数据name_tensor的每个字符逐个传入rnn之中, 得到最终结果
        softmax_output, hidden_tensor = gru(name_tensor[i], hidden_tensor)  # 返回的softmax_output被下一次的softmax_output覆盖,hidden_tensor作为下一个时间步的gru的输入
    loss = criterion(softmax_output.squeeze(0), category_tensor)  # 因为我们的rnn对象由nn.RNN实例化得到, 最终输出形状是三维张量, 为了满足于category_tensor进行对比计算损失, 需要减少第一个维度, 这里使用squeeze()方法
    loss.backward()  # 损失进行反向传播
    for p in gru.parameters():  # 显示地更新模型中所有的参数【此处没有用自动参数更新函数】
        p.data.add_(-learning_rate, p.grad.data)  # 原地覆盖:将“参数p”+“参数p的梯度×学习率”的结果来更新参数p
    return softmax_output, loss.item()  # 返回结果和损失的值


# 第六步: 构建训练过程
n_iters = 1000  # 设置训练迭代次数
print_every = 50  # 设置结果的打印间隔【每隔50轮打印一次数据】
plot_every = 10  # 设置绘制损失曲线上的制图间隔【每隔10轮采集一次绘图数据】


# 训练过程的日志打印函数, 参数train_type_fn代表选择哪种模型训练函数, 如trainRNN、trainLSTM、trainGRU
def train(train_type_fn):
    all_losses = []  # 每个制图间隔损失保存列表
    start = time.time()  # 获得训练开始时间戳
    current_loss = 0  # 设置初始间隔损失为0
    for iter in range(1, n_iters + 1):  # 从1开始进行训练迭代, 共n_iters次
        category, category_tensor, name, name_tensor = randomTrainingExample()  # 通过randomTrainingExample函数随机获取一组训练数据和对应的类别
        output, loss = train_type_fn(category_tensor, name_tensor)  # 将训练数据和对应类别的张量表示传入到train函数中
        current_loss += loss  # 计算制图间隔中的总损失
        if iter % print_every == 0:  # 如果迭代数能够整除打印间隔
            guess, guess_i = categoryFromOutput(output)  # 取该迭代步上的output通过categoryFromOutput函数获得对应的类别和类别索引
            correct = '✓' if guess == category else '✗ (%s)' % category  # 然后和真实的类别category做比较, 如果相同则打对号, 否则打叉号.
            print('iter = %d %d%%----timeSince(start)= (%s)----loss = %.4f----name = %s / guess = %s----is_correct = %s' % (iter, iter / n_iters * 100, timeSince(start), loss, name, guess, correct))  # 打印迭代步, 迭代步百分比, 当前训练耗时, 损失, 该步预测的名字, 以及是否正确
        if iter % plot_every == 0:  # 如果迭代数能够整除制图间隔
            all_losses.append(current_loss / plot_every)  # 将保存该间隔中的平均损失到all_losses列表中
            current_loss = 0  # 间隔损失重置为0
    return all_losses, int(time.time() - start)  # 返回对应的总损失列表和训练耗时


# 第七步: 开始训练传统RNN, LSTM, GRU模型并制作对比图
# 7.1 调用train函数, 分别进行RNN, LSTM, GRU模型的训练
# 并返回各自的全部损失, 以及训练耗时用于制图
all_losses1, period1 = train(trainRNN)
all_losses2, period2 = train(trainLSTM)
all_losses3, period3 = train(trainGRU)

# 7.2 绘制损失对比曲线, 训练耗时对比柱张图
plt.figure(0)  # 创建画布0
# 绘制损失对比曲线
plt.plot(all_losses1, label="RNN")
plt.plot(all_losses2, color="red", label="LSTM")
plt.plot(all_losses3, color="orange", label="GRU")
plt.legend(loc='upper left')

plt.figure(1)  # 创建画布1
x_data = ["RNN", "LSTM", "GRU"]
y_data = [period1, period2, period3]
plt.bar(range(len(x_data)), y_data, tick_label=x_data)  # 绘制训练耗时对比柱状图
plt.show()


# 第八步: 构建评估函数【评估函数, 和训练函数逻辑相同, 参数是name_tensor代表名字的张量表示】
# 8.1 构建传统RNN评估函数
def evaluateRNN(name_tensor):
    hidden = rnn.initHidden()  # 初始化隐层张量
    for i in range(name_tensor.size()[0]):  # 将评估数据name_tensor的每个字符逐个传入rnn之中
        output, hidden = rnn(name_tensor[i], hidden)
    return output.squeeze(0)  # 获得输出结果


# 8.2 构建LSTM评估函数
def evaluateLSTM(line_tensor):
    hidden, cell = lstm.initHiddenAndCell()  # 初始化隐层张量和细胞状态张量
    for i in range(line_tensor.size()[0]):  # 将评估数据line_tensor的每个字符逐个传入lstm之中
        output, hidden, cell = lstm(line_tensor[i], hidden, cell)
    return output.squeeze(0)


# 8.3 构建GRU评估函数
def evaluateGRU(name_tensor):
    hidden = gru.initHidden()
    for i in range(name_tensor.size()[0]):  # 将评估数据name_tensor的每个字符逐个传入gru之中
        output, hidden = gru(name_tensor[i], hidden)
    return output.squeeze(0)


name = "Bai"
name_tensor = nameToTensor(name)

rnn_output = evaluateRNN(name_tensor)
lstm_output = evaluateLSTM(name_tensor)
gru_output = evaluateGRU(name_tensor)
print("rnn_output:", rnn_output)
print("lstm_output:", lstm_output)
print("gru_output:", gru_output)


# 第九步: 构建预测函数【预测函数, 参数input_name:代表输入的名字字符串, 参数evaluate_fn:代表评估的模型函数;参数n_predictions:代表需要取最有可能的top个】
def predict(input_name, evaluate_fn, n_predictions=3):
    print('\n> %s' % input_name)  # 首先打印输入
    # 以下操作的相关张量不进行求梯度
    with torch.no_grad():
        output = evaluate_fn(nameToTensor(input_name))  # 使输入的名字转换为张量表示, 并使用evaluate函数获得预测输出
        top_values, top_indexes = output.topk(n_predictions)     # 从预测的输出中取前3个最大的值及其索引()
        predictions = []  # 创建盛装结果的列表
        for i in range(n_predictions):  # 遍历n_predictions
            value = top_values[0][i].item()   # 从top_values中取出的output值
            category_index = top_indexes[0][i].item()  # 取出索引并找到对应的类别
            print('(%.2f) %s' % (value, all_categories_list[category_index]))   # 打印ouput的值, 和对应的类别
            predictions.append([value, all_categories_list[category_index]])    # 将结果装进predictions中
        return predictions

for evaluate_fn in [evaluateRNN, evaluateLSTM, evaluateGRU]:
    print("-"*50)
    predictions01 = predict('Dovesky', evaluate_fn)
    predictions02 = predict('Jackson', evaluate_fn)
    predictions03 = predict('Satoshi', evaluate_fn)

打印结果:

n_letter: 57
a = Slusarski
results[:20] = ['Ang', 'AuYong', 'Bai', 'Ban', 'Bao', 'Bei', 'Bian', 'Bui', 'Cai', 'Cao', 'Cen', 'Chai', 'Chaim', 'Chan', 'Chang', 'Chao', 'Che', 'Chen', 'Cheng', 'Cheung']
categories_size = 18, all_categories_list = ['Arabic', 'Chinese', 'Czech', 'Dutch', 'English', 'French', 'German', 'Greek', 'Irish', 'Italian', 'Japanese', 'Korean', 'Polish', 'Portuguese', 'Russian', 'Scottish', 'Spanish', 'Vietnamese']
category_names_dict['Italian'][:5] = ['Abandonato', 'Abatangelo', 'Abatantuono', 'Abate', 'Abategiovanni']
nameToTensor('Bai') = tensor([[[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
          0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0.,
          0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
          0., 0., 0., 0., 0., 0.]],

        [[1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
          0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
          0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
          0., 0., 0., 0., 0., 0.]],

        [[0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0.,
          0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
          0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
          0., 0., 0., 0., 0., 0.]]])


rnn_output.shape = torch.Size([1, 1, 18]), 
rnn_output = tensor([[[-2.8661, -2.8582, -2.9480, -2.9104, -2.8761, -2.8454, -2.8940,
          -2.8492, -2.9057, -2.8037, -2.8347, -2.8989, -2.8913, -2.8549,
          -2.9412, -2.8947, -3.0215, -2.9551]]], grad_fn=<LogSoftmaxBackward>)
next_hidden.shape = torch.Size([1, 1, 128]), 
next_hidden[:,:,:5] = tensor([[[ 0.1841,  0.0722, -0.0517, -0.0985,  0.0389]]],
       grad_fn=<SliceBackward>)


lstm_output.shape = torch.Size([1, 1, 18]), 
lstm_output = tensor([[[-2.8661, -2.8582, -2.9480, -2.9104, -2.8761, -2.8454, -2.8940,
          -2.8492, -2.9057, -2.8037, -2.8347, -2.8989, -2.8913, -2.8549,
          -2.9412, -2.8947, -3.0215, -2.9551]]], grad_fn=<LogSoftmaxBackward>)
next_hidden.shape = torch.Size([1, 1, 128]), 
next_hidden[:,:,:5] = tensor([[[-0.0018, -0.0170,  0.0134,  0.0299, -0.0115]]],
       grad_fn=<SliceBackward>)
cell.shape = torch.Size([1, 1, 128]), 
cell[:,:,:5] = tensor([[[-0.0018, -0.0170,  0.0134,  0.0299, -0.0115]]],
       grad_fn=<SliceBackward>)


gru_output.shape = torch.Size([1, 1, 18]), 
gru_output = tensor([[[-2.8254, -2.8348, -2.9732, -2.8324, -2.7727, -2.8912, -2.8643,
          -2.8390, -2.8996, -2.9456, -2.9254, -2.8220, -2.9603, -2.9446,
          -2.8960, -2.9835, -2.8761, -2.9742]]], grad_fn=<LogSoftmaxBackward>)
next_hidden.shape = torch.Size([1, 1, 128]), 
next_hidden[:,:,:5] = tensor([[[ 0.0380, -0.0449, -0.0123,  0.0079,  0.0166]]],
       grad_fn=<SliceBackward>)
category = Italian----category_index = 9
category = Greek, category_tensor = tensor([7]), name = Kalogeria
category = French, category_tensor = tensor([5]), name = Voclain
category = Spanish, category_tensor = tensor([16]), name = Ybarra
category = Irish, category_tensor = tensor([8]), name = O'Halloran
category = Greek, category_tensor = tensor([7]), name = Comino
category = Korean, category_tensor = tensor([11]), name = Ngai
category = Irish, category_tensor = tensor([8]), name = Lennon
category = Arabic, category_tensor = tensor([0]), name = Koury
category = Vietnamese, category_tensor = tensor([17]), name = Chung
category = Russian, category_tensor = tensor([14]), name = Otov
name_tensor = 
tensor([[[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
          0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
          0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
          0., 0., 0., 0., 0., 0.]],

        [[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
          0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
          0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
          0., 0., 0., 0., 0., 0.]],

        [[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0.,
          0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
          0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
          0., 0., 0., 0., 0., 0.]],

        [[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
          0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
          0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
          0., 0., 0., 0., 0., 0.]]])
测试:period = 10m 0s

iter = 100 10%----timeSince(start)= (0m 0s)----loss = 2.9856----name = Bell / guess = Irish----is_correct =(Scottish)
iter = 200 20%----timeSince(start)= (0m 0s)----loss = 2.9410----name = Schoonraad / guess = French----is_correct =(Dutch)
iter = 300 30%----timeSince(start)= (0m 0s)----loss = 2.7686----name = Yuasa / guess = Japanese----is_correct =iter = 400 40%----timeSince(start)= (0m 1s)----loss = 2.9545----name = Salomon / guess = Japanese----is_correct =(Polish)
iter = 500 50%----timeSince(start)= (0m 1s)----loss = 2.7721----name = Kools / guess = Greek----is_correct =(Dutch)
iter = 600 60%----timeSince(start)= (0m 1s)----loss = 3.0601----name = Kinnison / guess = Dutch----is_correct =(English)
iter = 700 70%----timeSince(start)= (0m 2s)----loss = 2.8069----name = Bertsimas / guess = French----is_correct =(Greek)
iter = 800 80%----timeSince(start)= (0m 2s)----loss = 3.0697----name = Mccourt / guess = French----is_correct =(English)
iter = 900 90%----timeSince(start)= (0m 2s)----loss = 2.9705----name = Vinton / guess = Korean----is_correct =(English)
iter = 1000 100%----timeSince(start)= (0m 2s)----loss = 2.8315----name = Sciacchitano / guess = Chinese----is_correct =(Italian)

iter = 100 10%----timeSince(start)= (0m 0s)----loss = 2.9202----name = Steuben / guess = Russian----is_correct =(German)
iter = 200 20%----timeSince(start)= (0m 0s)----loss = 2.9407----name = Jameson / guess = Russian----is_correct =(English)
iter = 300 30%----timeSince(start)= (0m 1s)----loss = 2.9750----name = Tansho / guess = Russian----is_correct =(Japanese)
iter = 400 40%----timeSince(start)= (0m 1s)----loss = 2.9542----name = Yamawaki / guess = Dutch----is_correct =(Japanese)
iter = 500 50%----timeSince(start)= (0m 2s)----loss = 2.9519----name = Martin / guess = Dutch----is_correct =(Scottish)
iter = 600 60%----timeSince(start)= (0m 2s)----loss = 2.8210----name = Patsyna / guess = Dutch----is_correct =(Russian)
iter = 700 70%----timeSince(start)= (0m 2s)----loss = 2.8637----name = Braden / guess = Dutch----is_correct =(Irish)
iter = 800 80%----timeSince(start)= (0m 3s)----loss = 2.9544----name = Ribeiro / guess = Greek----is_correct =(Portuguese)
iter = 900 90%----timeSince(start)= (0m 3s)----loss = 2.8512----name = Pyrchenkov / guess = Greek----is_correct =(Russian)
iter = 1000 100%----timeSince(start)= (0m 4s)----loss = 2.8863----name = Bonhomme / guess = Dutch----is_correct =(French)

iter = 100 10%----timeSince(start)= (0m 0s)----loss = 2.8053----name = Fotopoulos / guess = English----is_correct =(Greek)
iter = 200 20%----timeSince(start)= (0m 0s)----loss = 2.7995----name = Pahlke / guess = English----is_correct =(German)
iter = 300 30%----timeSince(start)= (0m 1s)----loss = 2.8021----name = Antonopoulos / guess = English----is_correct =(Greek)
iter = 400 40%----timeSince(start)= (0m 1s)----loss = 2.8211----name = Lyon / guess = English----is_correct =(French)
iter = 500 50%----timeSince(start)= (0m 1s)----loss = 2.8197----name = Uzunov / guess = English----is_correct =(Russian)
iter = 600 60%----timeSince(start)= (0m 2s)----loss = 2.8837----name = Tong / guess = English----is_correct =(Chinese)
iter = 700 70%----timeSince(start)= (0m 2s)----loss = 2.9241----name = Taidhg / guess = English----is_correct =(Irish)
iter = 800 80%----timeSince(start)= (0m 3s)----loss = 2.8355----name = Favager / guess = English----is_correct =(French)
iter = 900 90%----timeSince(start)= (0m 3s)----loss = 2.8069----name = Gavrilopoulos / guess = English----is_correct =(Greek)
iter = 1000 100%----timeSince(start)= (0m 4s)----loss = 2.8909----name = O'Mahoney / guess = English----is_correct =(Irish)

rnn_output: tensor([[-2.8367, -2.7073, -3.1202, -2.7890, -3.0393, -2.8151, -3.0920, -2.9447, -2.9071, -2.7546, -2.8206, -2.7822, -2.8891, -2.9345, -2.8056, -2.9625, -3.0591, -2.8892]], grad_fn=<SqueezeBackward1>)
lstm_output: tensor([[-2.9428, -2.8900, -2.8169, -2.7529, -2.9216, -2.8817, -2.9201, -2.7735, -2.8706, -2.9447, -2.8947, -2.8616, -2.9547, -2.9307, -2.8692, -2.9436, -2.8546, -3.0433]], grad_fn=<SqueezeBackward1>)
gru_output: tensor([[-2.8713, -2.9555, -2.9238, -2.8854, -2.7455, -2.8857, -2.7851, -2.8328, -2.9240, -2.9653, -2.8776, -2.8990, -2.8710, -2.9643, -2.8889, -3.0186, -2.8719, -2.8967]], grad_fn=<SqueezeBackward1>)

--------------------------------------------------

> Dovesky
(-2.66) Dutch
(-2.74) Russian
(-2.78) French

> Jackson
(-2.71) Dutch
(-2.74) Korean
(-2.76) Irish

> Satoshi
(-2.73) Chinese
(-2.75) Dutch
(-2.77) Italian
--------------------------------------------------

> Dovesky
(-2.76) Dutch
(-2.77) Greek
(-2.82) Czech

> Jackson
(-2.76) Greek
(-2.76) Dutch
(-2.81) Czech

> Satoshi
(-2.75) Dutch
(-2.77) Greek
(-2.82) Czech
--------------------------------------------------

> Dovesky
(-2.68) English
(-2.79) German
(-2.83) Greek

> Jackson
(-2.68) English
(-2.81) German
(-2.84) Greek

> Satoshi
(-2.71) English
(-2.77) German
(-2.84) Greek

Process finished with exit code 0

损失对比曲线(n_iters =100000):
在这里插入图片描述
损失对比曲线分析:

  • 模型训练的损失降低快慢代表模型收敛程度,
  • 由图可知, 传统RNN的模型收敛情况最好, 然后是GRU, 最后是LSTM, 这是因为: 我们当前处理的文本数据是人名, 他们的长度有限, 且长距离字母间基本无特定关联, 因此无法发挥改进模型LSTM和GRU的长距离捕捉语义关联的优势.
  • 所以在以后的模型选用时, 要通过对任务的分析以及实验对比, 选择最适合的模型.

训练耗时对比图:
在这里插入图片描述
训练耗时对比图分析:

  • 模型训练的耗时长短代表模型的计算复杂度,
  • 由图可知, 也正如我们之前的理论分析, 传统RNN复杂度最低, 耗时几乎只是后两者的一半, 然后是GRU, 最后是复杂度最高的LSTM.

模型选用一般应通过实验对比, 并非越复杂或越先进的模型表现越好, 而是需要结合自己的特定任务, 从对数据的分析和实验结果中获得最佳答案.

九、LSTM案例

1、Tensorflow2-LSTMCell案例(构建每一个cell及memorycell)-imdb数据集【电影评论情感二分类】

import os

os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
import tensorflow as tf
import numpy as np
from tensorflow import keras
from tensorflow.keras import layers
import time

tf.random.set_seed(22)
np.random.seed(22)

assert tf.__version__.startswith('2.')

batch_size = 500  # 每次训练500个句子

total_words = 10000  # the most frequest words
max_review_len = 80  # 设置句子长度,如果有的句子的长度不到80则补齐,如果有的句子超过80则截断
embedding_len = 100  # 每个单词转为向量后的向量维度

# 一、获取数据集
(X_train, Y_train), (X_val, Y_val) = keras.datasets.imdb.load_data(num_words=total_words)
print('X_train[0] = {0},\nY_train[0] = {1}'.format(X_train[0], Y_train[0]))
print('X_train.shpae = {0},Y_train.shpae = {1}------------type(X_train) = {2},type(Y_train) = {3}'.format(X_train.shape, Y_train.shape, type(X_train), type(Y_train)))

# 二、数据处理
# 2.1 # 设置句子统一长度
X_train = keras.preprocessing.sequence.pad_sequences(X_train, maxlen=max_review_len)  # 设置句子长度    [b, 80]
X_val = keras.preprocessing.sequence.pad_sequences(X_val, maxlen=max_review_len)  # 设置句子长度
print('X_train.shpae = {0},Y_train.shpae = {1},tf.reduce_max(Y_train) = {2},tf.reduce_min(Y_train) = {3}'.format(X_train.shape, Y_train.shape, tf.reduce_max(Y_train), tf.reduce_min(Y_train)))
# 2.1 处理训练集为batch模型
db_train = tf.data.Dataset.from_tensor_slices((X_train, Y_train))
db_train = db_train.shuffle(1000).batch(batch_size, drop_remainder=True)  # 通过 drop_remainder=True 把 最后一个不满足batch_size大小的batch丢弃掉
db_val = tf.data.Dataset.from_tensor_slices((X_val, Y_val))
db_val = db_val.batch(batch_size, drop_remainder=True)  # 通过 drop_remainder=True 把 最后一个不满足batch_size大小的batch丢弃掉
print('db_train = {0},len(db_train) = {1}'.format(db_train, len(db_train)))


class MyRNN(keras.Model):
    def __init__(self, output_dim):
        super(MyRNN, self).__init__()
        # ***********************************************************memoryCell***********************************************************
        # [b, 64, 64]
        # memoryCell用于保存上一次的隐藏状态值C_{t-1}以及上一次的输出值h_{t-1}、,用于计算本次的C_{t-1}以及h_t时的输入值
        # 使用多个memoryCell串联即实现Deep的作用
        self.memoryCell01 = [tf.zeros([batch_size, output_dim]), tf.zeros([batch_size, output_dim])]  # 初始化memoryCell01,维度为  [b, 64, 64]
        self.memoryCell02 = [tf.zeros([batch_size, output_dim]), tf.zeros([batch_size, output_dim])]  # 初始化memoryCell02,维度为  [b, 64, 64]
        # ***********************************************************Embedding***********************************************************
        # 将每一个句子(维度为[80,1],80表示每个句子包含的word数量,1表示1个word)变换为wordEmbedding(维度为[80,100],80表示每个句子包含的word数量,100表示每个wordEmbedding的维度)
        # [b, 80, 1] => [b, 80, 100]
        # input_dim:表示输入维度,即设定词库总单词数量;b
        # input_length:表示每个句子统一长度(包含的单词数量);80
        # output_dim:表示输出维度,即每个单词转为向量后的向量维度;100
        self.embedding = layers.Embedding(input_dim=total_words, input_length=max_review_len, output_dim=embedding_len)
        # ***********************************************************RNNCell Layer***********************************************************
        # [b, 80, 100]=>[b, 64]
        self.rnn_cell01 = layers.LSTMCell(output_dim, dropout=0.2)  # output_dim: dimensionality of the output space. 隐藏状态的维度;dropout 防止过拟合
        self.rnn_cell02 = layers.LSTMCell(output_dim, dropout=0.2)
        # ***********************************************************全连接层***********************************************************
        # [b, 64] => [b, 1]
        self.outlayer = layers.Dense(1)

    def call(self, inputs, training=None):
        """
        net(x) net(x, training=True) :train mode
        net(x, training=False): test mode
        :param inputs: [b, 80, 1]
        :param training:
        :return:
        """
        # ***********************************************************Embedding***********************************************************
        # embedding: [b, 80, 1] => [b, 80, 100]
        wordEmbeddings = self.embedding(inputs)  # inputs 为1个batch的句子文本
        print('\nwordEmbeddings.shape = {0}, wordEmbeddings = {1}'.format(wordEmbeddings.shape, wordEmbeddings))
        # rnn cell compute
        # ***********************************************************RNNCell Layer***********************************************************
        # [b, 80, 100] => [b, 1, 64],每个句子都从降维:[80, 100]=>[1, 64]
        memoryCell01 = self.memoryCell01
        memoryCell02 = self.memoryCell02
        wordEmbedding_index = 0
        for wordEmbedding in tf.unstack(wordEmbeddings, axis=1):  # wordEmbedding: [b, 100],将每个句子中的80个单词展开,即按读取该句子的时间轴展开
            # 隐含状态:out01/out02: [b, 64]
            # h_t = x_t×w_{xh}+h_{t-1}×w_{hh};其中:x_t=wordEmbedding;h_{t-1}=memoryCell01;输出值h_t = out01
            # memoryCell01保存2个值:第一个值是隐藏状态C_t,第二个值是隐藏状态输出值h_t
            out01, memoryCell01 = self.rnn_cell01(wordEmbedding, memoryCell01, training=training)  # 用输出值更新memoryCell01;   training=True 表示模式是训练模式,dropout功能有效,默认是True
            # 将rnn_cell01的输出值out01传入下一个rnn_cell02提升RNNCell Layer的提取能力
            # memoryCell01保存2个值:第一个值是隐藏状态C_t,第二个值是隐藏状态输出值h_t
            out02, memoryCell02 = self.rnn_cell02(out01, memoryCell02, training=training)  # 用输出值更新memoryCell02; training=True 表示模式是训练模式,dropout功能有效,默认是True
            if wordEmbedding_index == 0:
                print('wordEmbedding.shape = {0}, wordEmbedding = {1}'.format(wordEmbedding.shape, wordEmbedding))
                print('out01.shape = {0}, memoryCell01[0].shape = {1}, out01 = {2}'.format(out01.shape, memoryCell01[0].shape, out01))
                print('out02.shape = {0}, memoryCell02[0].shape = {1}, out02 = {2}'.format(out02.shape, memoryCell02[0].shape, out02))
            wordEmbedding_index += 1
        # ***********************************************************全连接层***********************************************************
        # out: [b, 1, 64] => [b, 1, 1]
        out_logit = self.outlayer(out02)  # out02代表了每个句子的语义信息的提取
        print('out_logit.shape = {0}, out_logit = {1}'.format(out_logit.shape, out_logit))
        out_prob = tf.sigmoid(out_logit)  # p(y is pos|x)
        print('out_prob.shape = {0}, out_prob = {1}, {2}'.format(out_prob.shape, out_prob, '\n'))
        return out_prob


def main():
    output_dim = 64     # 设定输出的隐藏状态维度  [b, 100] => [b,64]
    epochs = 4
    t0 = time.time()
    network = MyRNN(output_dim)
    # 不需要设置from_logits=True,因为MyRNN()中已经设定了激活函数层 out_prob = tf.sigmoid(X)
    # metrics=['accuracy']表示打印测试数据
    network.compile(optimizer=keras.optimizers.Adam(0.001),
                    loss=tf.losses.BinaryCrossentropy(),
                    metrics=['accuracy'])
    print('\n***********************************************************训练network:开始***********************************************************')
    network.fit(db_train, epochs=epochs, validation_data=db_val)
    print('***********************************************************训练network:结束***********************************************************')
    print('\n***********************************************************评估network(其实训练时已经评估):开始***********************************************************')
    network.evaluate(db_val)  # 评估模型
    print('***********************************************************评估network(其实训练时已经评估):结束***********************************************************')
    t1 = time.time()
    print('total time cost:', t1 - t0)


if __name__ == '__main__':
    main()

打印结果:

X_train[0] = [1, 14, 22, 16, 43, 530, 973, 1622, 1385, 65, 458, 4468, 66, 3941, 4, 173, 36, 256, 5, 25, 100, 43, 838, 112, 50, 670, 2, 9, 35, 480, 284, 5, 150, 4, 172, 112, 167, 2, 336, 385, 39, 4, 172, 4536, 1111, 17, 546, 38, 13, 447, 4, 192, 50, 16, 6, 147, 2025, 19, 14, 22, 4, 1920, 4613, 469, 4, 22, 71, 87, 12, 16, 43, 530, 38, 76, 15, 13, 1247, 4, 22, 17, 515, 17, 12, 16, 626, 18, 2, 5, 62, 386, 12, 8, 316, 8, 106, 5, 4, 2223, 5244, 16, 480, 66, 3785, 33, 4, 130, 12, 16, 38, 619, 5, 25, 124, 51, 36, 135, 48, 25, 1415, 33, 6, 22, 12, 215, 28, 77, 52, 5, 14, 407, 16, 82, 2, 8, 4, 107, 117, 5952, 15, 256, 4, 2, 7, 3766, 5, 723, 36, 71, 43, 530, 476, 26, 400, 317, 46, 7, 4, 2, 1029, 13, 104, 88, 4, 381, 15, 297, 98, 32, 2071, 56, 26, 141, 6, 194, 7486, 18, 4, 226, 22, 21, 134, 476, 26, 480, 5, 144, 30, 5535, 18, 51, 36, 28, 224, 92, 25, 104, 4, 226, 65, 16, 38, 1334, 88, 12, 16, 283, 5, 16, 4472, 113, 103, 32, 15, 16, 5345, 19, 178, 32],
Y_train[0] = 1
X_train.shpae = (25000,),Y_train.shpae = (25000,)------------type(X_train) = <class 'numpy.ndarray'>type(Y_train) = <class 'numpy.ndarray'>
X_train.shpae = (25000, 80),Y_train.shpae = (25000,),tf.reduce_max(Y_train) = 1,tf.reduce_min(Y_train) = 0
db_train = <BatchDataset shapes: ((500, 80), (500,)), types: (tf.int32, tf.int64)>len(db_train) = 50

***********************************************************训练network:开始***********************************************************
Epoch 1/4

wordEmbeddings.shape = (500, 80, 100), wordEmbeddings = Tensor("my_rnn/embedding/embedding_lookup/Identity:0", shape=(500, 80, 100), dtype=float32)
wordEmbedding.shape = (500, 100), wordEmbedding = Tensor("my_rnn/unstack:0", shape=(500, 100), dtype=float32)
out01.shape = (500, 64), memoryCell01[0].shape = (500, 64), out01 = Tensor("my_rnn/lstm_cell/mul_3:0", shape=(500, 64), dtype=float32)
out02.shape = (500, 64), memoryCell02[0].shape = (500, 64), out02 = Tensor("my_rnn/lstm_cell_1/mul_3:0", shape=(500, 64), dtype=float32)
out_logit.shape = (500, 1), out_logit = Tensor("my_rnn/dense/BiasAdd:0", shape=(500, 1), dtype=float32)
out_prob.shape = (500, 1), out_prob = Tensor("my_rnn/Sigmoid:0", shape=(500, 1), dtype=float32), 


wordEmbeddings.shape = (500, 80, 100), wordEmbeddings = Tensor("my_rnn/embedding/embedding_lookup/Identity:0", shape=(500, 80, 100), dtype=float32)
wordEmbedding.shape = (500, 100), wordEmbedding = Tensor("my_rnn/unstack:0", shape=(500, 100), dtype=float32)
out01.shape = (500, 64), memoryCell01[0].shape = (500, 64), out01 = Tensor("my_rnn/lstm_cell/mul_3:0", shape=(500, 64), dtype=float32)
out02.shape = (500, 64), memoryCell02[0].shape = (500, 64), out02 = Tensor("my_rnn/lstm_cell_1/mul_3:0", shape=(500, 64), dtype=float32)
out_logit.shape = (500, 1), out_logit = Tensor("my_rnn/dense/BiasAdd:0", shape=(500, 1), dtype=float32)
out_prob.shape = (500, 1), out_prob = Tensor("my_rnn/Sigmoid:0", shape=(500, 1), dtype=float32), 

50/50 [==============================] - ETA: 0s - loss: 0.6419 - accuracy: 0.6000
wordEmbeddings.shape = (500, 80, 100), wordEmbeddings = Tensor("my_rnn/embedding/embedding_lookup/Identity:0", shape=(500, 80, 100), dtype=float32)
wordEmbedding.shape = (500, 100), wordEmbedding = Tensor("my_rnn/unstack:0", shape=(500, 100), dtype=float32)
out01.shape = (500, 64), memoryCell01[0].shape = (500, 64), out01 = Tensor("my_rnn/lstm_cell/mul_3:0", shape=(500, 64), dtype=float32)
out02.shape = (500, 64), memoryCell02[0].shape = (500, 64), out02 = Tensor("my_rnn/lstm_cell_1/mul_3:0", shape=(500, 64), dtype=float32)
out_logit.shape = (500, 1), out_logit = Tensor("my_rnn/dense/BiasAdd:0", shape=(500, 1), dtype=float32)
out_prob.shape = (500, 1), out_prob = Tensor("my_rnn/Sigmoid:0", shape=(500, 1), dtype=float32), 

50/50 [==============================] - 20s 244ms/step - loss: 0.6402 - accuracy: 0.6018 - val_loss: 0.3965 - val_accuracy: 0.8207
Epoch 2/4
50/50 [==============================] - 9s 187ms/step - loss: 0.3500 - accuracy: 0.8469 - val_loss: 0.3740 - val_accuracy: 0.8362
Epoch 3/4
50/50 [==============================] - 9s 185ms/step - loss: 0.2635 - accuracy: 0.8947 - val_loss: 0.4112 - val_accuracy: 0.8321
Epoch 4/4
50/50 [==============================] - 10s 193ms/step - loss: 0.2153 - accuracy: 0.9161 - val_loss: 0.4534 - val_accuracy: 0.8260
***********************************************************训练network:结束***********************************************************

***********************************************************评估network(其实训练时已经评估):开始***********************************************************
50/50 [==============================] - 2s 50ms/step - loss: 0.4534 - accuracy: 0.8260
***********************************************************评估network(其实训练时已经评估):结束***********************************************************
total time cost: 50.509249687194824

Process finished with exit code 0

2、Tensorflow2-LSTM案例-imdb数据集【电影评论情感二分类】

import os

os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
import tensorflow as tf
import numpy as np
from tensorflow import keras
from tensorflow.keras import layers
import time

tf.random.set_seed(22)
np.random.seed(22)

assert tf.__version__.startswith('2.')

batch_size = 500  # 每次训练500个句子

total_words = 10000  # the most frequest words
max_review_len = 80  # 设置句子长度,如果有的句子的长度不到80则补齐,如果有的句子超过80则截断
embedding_len = 100  # 每个单词转为向量后的向量维度

# 一、获取数据集
(X_train, Y_train), (X_val, Y_val) = keras.datasets.imdb.load_data(num_words=total_words)
print('X_train[0] = {0},\nY_train[0] = {1}'.format(X_train[0], Y_train[0]))
print('X_train.shpae = {0},Y_train.shpae = {1}------------type(X_train) = {2},type(Y_train) = {3}'.format(X_train.shape, Y_train.shape, type(X_train), type(Y_train)))

# 二、数据处理
# 2.1 # 设置句子统一长度
X_train = keras.preprocessing.sequence.pad_sequences(X_train, maxlen=max_review_len)  # 设置句子长度    [b, 80]
X_val = keras.preprocessing.sequence.pad_sequences(X_val, maxlen=max_review_len)  # 设置句子长度
print('X_train.shpae = {0},Y_train.shpae = {1},tf.reduce_max(Y_train) = {2},tf.reduce_min(Y_train) = {3}'.format(X_train.shape, Y_train.shape, tf.reduce_max(Y_train), tf.reduce_min(Y_train)))
# 2.1 处理训练集为batch模型
db_train = tf.data.Dataset.from_tensor_slices((X_train, Y_train))
db_train = db_train.shuffle(1000).batch(batch_size, drop_remainder=True)  # 通过 drop_remainder=True 把 最后一个不满足batch_size大小的batch丢弃掉
db_val = tf.data.Dataset.from_tensor_slices((X_val, Y_val))
db_val = db_val.batch(batch_size, drop_remainder=True)  # 通过 drop_remainder=True 把 最后一个不满足batch_size大小的batch丢弃掉
print('db_train = {0},len(db_train) = {1}'.format(db_train, len(db_train)))


class MyRNN(keras.Model):
    def __init__(self, output_dim):
        super(MyRNN, self).__init__()
        # ***********************************************************Embedding***********************************************************
        # transform text to embedding representation
        # 将每一个句子(维度为[80,1],80表示每个句子包含的word数量,1表示1个word)变换为wordEmbedding(维度为[80,100],80表示每个句子包含的word数量,100表示每个wordEmbedding的维度)
        # [b, 80, 1] => [b, 80, 100]
        # input_dim:表示输入维度,即设定词库总单词数量;b
        # input_length:表示每个句子统一长度(包含的单词数量);80
        # output_dim:表示输出维度,即每个单词转为向量后的向量维度;100
        self.embedding = layers.Embedding(input_dim=total_words, input_length=max_review_len, output_dim=embedding_len)
        # ***********************************************************RNN神经网络结构:SimpleRNN 表示SimpleRNN连接层***********************************************************
        # [b, 80, 100]=>[b, 64]
        self.rnn = keras.Sequential([
            # output_dim: dimensionality of the output space. 隐藏状态的维度;dropout 防止过拟合
            # return_sequences: Boolean. Whether to return the last output in the output sequence, or the full sequence.
            # unroll: Boolean (default False). If True, the network will be unrolled, else a symbolic loop will be used.
            # Unrolling can speed-up a RNN, although it tends to be more memory-intensive. Unrolling is only suitable for short sequences.
            layers.LSTM(output_dim, dropout=0.5, return_sequences=True, unroll=True),
            layers.LSTM(output_dim, dropout=0.5, unroll=True)
        ])
        # ***********************************************************全连接层***********************************************************
        # [b, 64] => [b, 1]
        self.outlayer = layers.Dense(1)

    def call(self, inputs, training=None):
        """
        net(x) net(x, training=True) :train mode
        net(x, training=False): test
        :param inputs: [b, 80]
        :param training:
        :return:
        """
        # ***********************************************************Embedding***********************************************************
        # embedding: [b, 80, 1] => [b, 80, 100]
        x_wordEmbeddings = self.embedding(inputs)  # inputs 为1个batch的句子文本
        print('\nx_wordEmbeddings.shape = {0}, x_wordEmbeddings = {1}'.format(x_wordEmbeddings.shape, x_wordEmbeddings))
        # ***********************************************************RNN神经网络结构计算***********************************************************
        out = self.rnn(x_wordEmbeddings)  # x: [b, 80, 100] => [b, 64]
        print('out.shape = {0}, out = {1}'.format(out.shape, out))
        out_logit = self.outlayer(out)  # 隐含状态=>0/1   out: [b, 64] => [b, 1]
        print('out_logit.shape = {0}, out_logit = {1}'.format(out_logit.shape, out_logit))
        out_prob = tf.sigmoid(out_logit)  # p(y is pos|x)
        print('out_prob.shape = {0}, out_prob = {1}, {2}'.format(out_prob.shape, out_prob, '\n'))
        return out_prob


def main():
    output_dim = 64     # 设定输出的隐藏状态维度  [b, 100] => [b,64]
    epochs = 4
    t0 = time.time()
    network = MyRNN(output_dim)
    # 不需要设置from_logits=True,因为MyRNN()中已经设定了激活函数层 out_prob = tf.sigmoid(X)
    # metrics=['accuracy']表示打印测试数据
    network.compile(optimizer=keras.optimizers.Adam(0.001),
                    loss=tf.losses.BinaryCrossentropy(),
                    metrics=['accuracy'])
    print('\n***********************************************************训练network:开始***********************************************************')
    network.fit(db_train, epochs=epochs, validation_data=db_val)
    print('***********************************************************训练network:结束***********************************************************')
    print('\n***********************************************************评估network(其实训练时已经评估):开始***********************************************************')
    network.evaluate(db_val)  # 评估模型
    print('***********************************************************评估network(其实训练时已经评估):结束***********************************************************')
    t1 = time.time()
    print('total time cost:', t1 - t0)


if __name__ == '__main__':
    main()

打印结果:

X_train[0] = [1, 14, 22, 16, 43, 530, 973, 1622, 1385, 65, 458, 4468, 66, 3941, 4, 173, 36, 256, 5, 25, 100, 43, 838, 112, 50, 670, 2, 9, 35, 480, 284, 5, 150, 4, 172, 112, 167, 2, 336, 385, 39, 4, 172, 4536, 1111, 17, 546, 38, 13, 447, 4, 192, 50, 16, 6, 147, 2025, 19, 14, 22, 4, 1920, 4613, 469, 4, 22, 71, 87, 12, 16, 43, 530, 38, 76, 15, 13, 1247, 4, 22, 17, 515, 17, 12, 16, 626, 18, 2, 5, 62, 386, 12, 8, 316, 8, 106, 5, 4, 2223, 5244, 16, 480, 66, 3785, 33, 4, 130, 12, 16, 38, 619, 5, 25, 124, 51, 36, 135, 48, 25, 1415, 33, 6, 22, 12, 215, 28, 77, 52, 5, 14, 407, 16, 82, 2, 8, 4, 107, 117, 5952, 15, 256, 4, 2, 7, 3766, 5, 723, 36, 71, 43, 530, 476, 26, 400, 317, 46, 7, 4, 2, 1029, 13, 104, 88, 4, 381, 15, 297, 98, 32, 2071, 56, 26, 141, 6, 194, 7486, 18, 4, 226, 22, 21, 134, 476, 26, 480, 5, 144, 30, 5535, 18, 51, 36, 28, 224, 92, 25, 104, 4, 226, 65, 16, 38, 1334, 88, 12, 16, 283, 5, 16, 4472, 113, 103, 32, 15, 16, 5345, 19, 178, 32],
Y_train[0] = 1
X_train.shpae = (25000,),Y_train.shpae = (25000,)------------type(X_train) = <class 'numpy.ndarray'>type(Y_train) = <class 'numpy.ndarray'>
X_train.shpae = (25000, 80),Y_train.shpae = (25000,),tf.reduce_max(Y_train) = 1,tf.reduce_min(Y_train) = 0
db_train = <BatchDataset shapes: ((500, 80), (500,)), types: (tf.int32, tf.int64)>len(db_train) = 50
WARNING:tensorflow:Layer lstm will not use cuDNN kernel since it doesn't meet the cuDNN kernel criteria. It will use generic GPU kernel as fallback when running on GPU
WARNING:tensorflow:Layer lstm_1 will not use cuDNN kernel since it doesn't meet the cuDNN kernel criteria. It will use generic GPU kernel as fallback when running on GPU

***********************************************************训练network:开始***********************************************************
Epoch 1/4

x_wordEmbeddings.shape = (500, 80, 100), x_wordEmbeddings = Tensor("my_rnn/embedding/embedding_lookup/Identity:0", shape=(500, 80, 100), dtype=float32)
out.shape = (500, 64), out = Tensor("my_rnn/sequential/lstm_1/lstm_cell_1/mul_319:0", shape=(500, 64), dtype=float32)
out_logit.shape = (500, 1), out_logit = Tensor("my_rnn/dense/BiasAdd:0", shape=(500, 1), dtype=float32)
out_prob.shape = (500, 1), out_prob = Tensor("my_rnn/Sigmoid:0", shape=(500, 1), dtype=float32), 


x_wordEmbeddings.shape = (500, 80, 100), x_wordEmbeddings = Tensor("my_rnn/embedding/embedding_lookup/Identity:0", shape=(500, 80, 100), dtype=float32)
out.shape = (500, 64), out = Tensor("my_rnn/sequential/lstm_1/lstm_cell_1/mul_319:0", shape=(500, 64), dtype=float32)
out_logit.shape = (500, 1), out_logit = Tensor("my_rnn/dense/BiasAdd:0", shape=(500, 1), dtype=float32)
out_prob.shape = (500, 1), out_prob = Tensor("my_rnn/Sigmoid:0", shape=(500, 1), dtype=float32), 

50/50 [==============================] - ETA: 0s - loss: 0.6580 - accuracy: 0.5754
x_wordEmbeddings.shape = (500, 80, 100), x_wordEmbeddings = Tensor("my_rnn/embedding/embedding_lookup/Identity:0", shape=(500, 80, 100), dtype=float32)
out.shape = (500, 64), out = Tensor("my_rnn/sequential/lstm_1/lstm_cell_1/mul_319:0", shape=(500, 64), dtype=float32)
out_logit.shape = (500, 1), out_logit = Tensor("my_rnn/dense/BiasAdd:0", shape=(500, 1), dtype=float32)
out_prob.shape = (500, 1), out_prob = Tensor("my_rnn/Sigmoid:0", shape=(500, 1), dtype=float32), 

50/50 [==============================] - 21s 235ms/step - loss: 0.6566 - accuracy: 0.5772 - val_loss: 0.4175 - val_accuracy: 0.8122
Epoch 2/4
50/50 [==============================] - 10s 199ms/step - loss: 0.3870 - accuracy: 0.8297 - val_loss: 0.3594 - val_accuracy: 0.8421
Epoch 3/4
50/50 [==============================] - 10s 206ms/step - loss: 0.2956 - accuracy: 0.8808 - val_loss: 0.3779 - val_accuracy: 0.8359
Epoch 4/4
50/50 [==============================] - 10s 198ms/step - loss: 0.2560 - accuracy: 0.8976 - val_loss: 0.3988 - val_accuracy: 0.8343
***********************************************************训练network:结束***********************************************************

***********************************************************评估network(其实训练时已经评估):开始***********************************************************
50/50 [==============================] - 2s 50ms/step - loss: 0.3988 - accuracy: 0.8343
***********************************************************评估network(其实训练时已经评估):结束***********************************************************
total time cost: 53.280513763427734

Process finished with exit code 0

3、Tensorflow2-LSTM案例-imdb数据集【电影评论情感二分类】【子词数值映射】

子词数值映射

  • 文本的子词数值映射是文本数值映射的拓展,基本的文本数值映射是将每个英文单词映射成一个词汇,如hello映射成数值1,而文本子词映射可能将hello拆分成子词如:hell和o,分别映射成对应的数值34和6,这样相当于将hello映射成34和6。
  • 文本子词数值映射在英文文本生成任务中使用频繁,因为子词的多样组合使得模型对不同的文本输入更加友好,不再局限于训练语料大小。在TFDS中,对该文本语料都使用了该技巧。
import os

os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
import time
import tensorflow as tf
import numpy as np
import tensorflow_datasets as tfds

tf.random.set_seed(22)
np.random.seed(22)

assert tf.__version__.startswith('2.')

batch_size = 500  # 每次训练500个句子

total_words = 10000  # the most frequest words
max_review_len = 80  # 设置句子长度,如果有的句子的长度不到80则补齐,如果有的句子超过80则截断
embedding_len = 100  # 每个单词转为向量后的向量维度

# 一、获取数据集
# 使用tfds加载数据集,其中参数with_info=True代表返回数据相关信息,包括数据进行数值映射的方式等。【参数as_supervised=True, 代表返回的数据为监督数据即需要将data和对应的label以一个元组的方式(data, label)返回】
dataset, info = tfds.load('imdb_reviews/subwords8k', with_info=True, as_supervised=True)

# 返回的dataset对象是一个字典形式,直接取train和test作为训练集和验证集
train_dataset, test_dataset = dataset['train'], dataset['test']
encoder = info.features['text'].encoder # 从dataset的info对象中可以获得子词数值映射器或称为子词数值编码器(encoder)
print('从子词数值映射器中取出它映射过的非重复子词数量: Vocabulary size = {}'.format(encoder.vocab_size))# 从子词数值映射器中取出它映射过的非重复子词数量
sample_string = 'Hello TensorFlow.'# 样例字符串hello tensorflow
encoded_string = encoder.encode(sample_string)# 使用encoder进行映射(编码)并查看结果
print('Encoded string = {}'.format(encoded_string))
original_string = encoder.decode(encoded_string)# 使用对应的方法decode进行解码并查看结果
print('The original string = "{}"'.format(original_string))
assert original_string == sample_string# 断定解码字符串与原样例字符串相同
# 原来两个单词会映射成5个数值【我们将每个数值进行解码查看,发现它们都是原单词的子词(原单词的一部分)】
for index in encoded_string:
    print('{} ----> {}'.format(index, encoder.decode([index])))

打印结果:

从子词数值映射器中取出它映射过的非重复子词数量: Vocabulary size = 8185
Encoded string = [4025, 222, 6307, 2327, 4043, 2120, 7975]
The original string = "Hello TensorFlow."
4025 ----> Hell
222 ----> o 
6307 ----> Ten
2327 ----> sor
4043 ----> Fl
2120 ----> ow
7975 ----> .

完整代码:

import os

os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
os.environ["CUDA_VISIBLE_DEVICES"] = '-1'  # 使用CPU计算

import time
import tensorflow as tf
import numpy as np
from tensorflow.keras import layers
import matplotlib.pyplot as plt
import tensorflow_datasets as tfds

tf.random.set_seed(22)
np.random.seed(22)

# 一、获取数据集
# 使用tfds加载数据集,其中参数with_info=True代表返回数据相关信息,包括数据进行数值映射的方式等。【参数as_supervised=True, 代表返回的数据为监督数据即需要将data和对应的label以一个元组的方式(data, label)返回】
dataset, info = tfds.load('imdb_reviews/subwords8k', with_info=True, as_supervised=True)

# 返回的dataset对象是一个字典形式,直接取train和test作为训练集和验证集
train_dataset, test_dataset = dataset['train'], dataset['test']
encoder = info.features['text'].encoder  # 从dataset的info对象中可以获得子词数值映射器或称为子词数值编码器(encoder)
print('train_dataset = {0},len(train_dataset) = {1}'.format(train_dataset, len(train_dataset)))
print('test_dataset = {0},len(test_dataset) = {1}'.format(test_dataset, len(test_dataset)))
print('Vocabulary size: {}'.format(encoder.vocab_size))

# 二、数据处理:批次化数据
BUFFER_SIZE = 10000  # 定义shuffle时的缓存大小
BATCH_SIZE = 200  # 定义训练批次大小
train_dataset = train_dataset.shuffle(BUFFER_SIZE)  # 打乱训练数据
train_dataset = train_dataset.padded_batch(batch_size=BATCH_SIZE, drop_remainder=True)  # 训练集划分批次,并做批次内最大长度补齐,使每个批次输入都是一个矩阵
test_dataset = test_dataset.padded_batch(batch_size=BATCH_SIZE, drop_remainder=True)  # 验证集划分批次(不需要打乱),并做批次内最大长度补齐,使每个批次输入都是一个矩阵

model = tf.keras.Sequential([
    tf.keras.layers.Embedding(input_dim=encoder.vocab_size, output_dim=50),  # output_dim:表示输出维度,即每个单词转为向量后的向量维度;50
    tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(units=30)),  # units:表示输出维度,即每个单词转为向量后的向量维度;100
    tf.keras.layers.Dense(units=20, activation='relu'),  # units:表示输出维度
    tf.keras.layers.Dense(units=1, activation='sigmoid')  # units:表示输出维度
])
model.summary()


# 根据训练历史日志和对应的评估指标绘制曲线
def plot_graphs(history, metric):
    plt.plot(history.history[metric])  # 绘制训练曲线
    plt.plot(history.history['val_' + metric], '')  # 绘制验证曲线
    plt.xlabel("Epochs")  # 横坐标为epochs
    plt.ylabel(metric)  # 纵坐标为对应指标
    plt.legend([metric, 'val_' + metric])  # 图中曲线说明框
    plt.show()


def main():
    output_dim = 64  # 设定输出的隐藏状态维度  [b, 100] => [b,64]
    epochs = 4
    t0 = time.time()
    # 不需要设置from_logits=True,因为MyLSTM()中已经设定了激活函数层 out_prob = tf.sigmoid(X)
    # metrics=['accuracy']表示打印测试数据
    model.compile(optimizer=tf.keras.optimizers.Adam(1e-4),
                  loss=tf.keras.losses.BinaryCrossentropy(from_logits=True),
                  metrics=['accuracy'])
    # 训练
    print('=' * 100, '训练network:开始', '=' * 100)
    history = model.fit(train_dataset, epochs=epochs, validation_data=test_dataset, validation_steps=30)
    print('=' * 100, '训练network:结束', '=' * 100)
    # 评估
    print('=' * 100, '评估network(其实训练时已经评估):开始', '=' * 100)
    test_loss, test_acc = model.evaluate(test_dataset)  # 评估模型
    print('Test Loss = {0}, Test Accuracy = {1}'.format(test_loss, test_acc))
    print('=' * 100, '评估network(其实训练时已经评估):结束', '=' * 100)
    t1 = time.time()
    print('total time cost:', t1 - t0)
    # 画图
    print('=' * 100, '进行准确率和损失的绘制:开始', '=' * 100)
    # 进行准确率和损失的绘制
    plot_graphs(history, 'accuracy')
    plot_graphs(history, 'loss')
    print('=' * 100, '进行准确率和损失的绘制:结束', '=' * 100)


if __name__ == '__main__':
    main()

4、Pytorch-LSTM案例-情感分类数据集【利用训练好的GloVe词向量初始化词嵌入层权重】

# -*- coding: utf-8 -*-
# pip install torch
# pip install torchtext
# python -m spacy download en_core_web_sm
# python -m spacy download en_core_web_md
# https://github.com/explosion/spacy-models
# 安装spacy:pip --default-timeout=10000 install https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-2.3.1/en_core_web_sm-2.3.1.tar.gz
# 在torchtext中使用spacy时,由于field的默认属性是tokenizer_language=‘en’,所以需要安装en_core_web_md:pip --default-timeout=10000 install https://github.com/explosion/spacy-models/releases/download/en_core_web_md-2.3.1/en_core_web_md-2.3.0.tar.gz
import numpy as np
import torch
from torch import nn, optim
from torchtext import data, datasets

print('GPU:', torch.cuda.is_available())
torch.manual_seed(123)

# 一、获取情感分类数据集
TEXT = data.Field(tokenize='spacy')
LABEL = data.LabelField(dtype=torch.float)
train_data, val_data = datasets.IMDB.splits(TEXT, LABEL)
print('len(train_data) = {0}'.format(len(train_data)))
print('len(val_data) = {0}'.format(len(val_data)))
print('train_data.examples[15].text = {0}'.format(train_data.examples[15].text))
print('train_data.examples[15].label = {0}'.format(train_data.examples[15].label))

# word2vec, glove
TEXT.build_vocab(train_data, max_size=10000, vectors='glove.6B.100d')
LABEL.build_vocab(train_data)

batchsz = 30
device = torch.device('cuda')
train_iterator, val_iterator = data.BucketIterator.splits(
    (train_data, val_data),
    batch_size=batchsz,
    device=device
)


# 二、构建LSTM神经网络结构
class MyLSTM(nn.Module):
    def __init__(self, vocab_size, embedding_dim, hidden_dim):
        super(MyLSTM, self).__init__()
        self.embedding = nn.Embedding(num_embeddings=vocab_size, embedding_dim=embedding_dim)  # [b, 1] => [b, 100]  需要编码的单词数量为vocab_size,每个单词编码为一个维度为embedding_dim的vector
        self.lstm = nn.LSTM(input_size=embedding_dim, hidden_size=hidden_dim, num_layers=2, bidirectional=True, dropout=0.5)  # [b, 100] => [b, 256]  embedding_dim为输入的vector维度,hidden_dim为latent层的维度,num_layers表示神经网络的层数
        self.fc = nn.Linear(in_features=hidden_dim * 2, out_features=1)  # [b, 256*2] => [b, 1]
        self.dropout = nn.Dropout(0.5)

    def forward(self, X):
        X = self.embedding(X)  # [seq, b, 1] => [seq, b, 100]
        embedding = self.dropout(X)
        output, (hidden, cell) = self.lstm(embedding)  # output: [seq, b, hid_dim*2];   hidden/h&cell/c: [num_layers*2, b, hid_dim]
        hidden = torch.cat([hidden[-2], hidden[-1]], dim=1)  # [num_layers*2, b, hid_dim] => 2 of [b, hid_dim] => [b, hid_dim*2]
        hidden = self.dropout(hidden)
        out = self.fc(hidden)  # [b, hid_dim*2] => [b, 1]
        return out


# 三、实例化LSTM
lstm = MyLSTM(len(TEXT.vocab), 100, 256)

# 四、初始化WordEmbedding
pretrained_embedding = TEXT.vocab.vectors
print('pretrained_embedding:', pretrained_embedding.shape)
lstm.embedding.weight.data.copy_(pretrained_embedding)  # 利用已经训练好的GloVede的embedding替代原来的embedding
print('embedding layer inited.')

optimizer = optim.Adam(lstm.parameters(), lr=1e-3)
criteon = nn.BCEWithLogitsLoss().to(device)
lstm.to(device)


# 准确率
def binary_acc(preds, y):
    preds = torch.round(torch.sigmoid(preds))
    correct = torch.eq(preds, y).float()
    acc = correct.sum() / len(correct)
    return acc


# 八、训练
def train(lstm, iterator, optimizer, criteon):
    avg_acc = []
    lstm.train()
    for batch_index, batch in enumerate(iterator):
        pred = lstm(batch.text).squeeze(1)  # [seq, b] => [b, 1] => [b]
        loss = criteon(pred, batch.label)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        acc = binary_acc(pred, batch.label).item()  # Training过程中的准确度
        avg_acc.append(acc)
        if batch_index % 10 == 0:
            print('batch_index = {0}, acc = {1}'.format(batch_index, acc))
    avg_acc = np.array(avg_acc).mean()
    print('avg acc:', avg_acc)


def eval(lstm, iterator, criteon):
    avg_acc = []
    lstm.eval()
    with torch.no_grad():   # 不需要计算梯度
        for batch in iterator:
            # [b, 1] => [b]
            pred = lstm(batch.text).squeeze(1)
            loss = criteon(pred, batch.label)
            acc = binary_acc(pred, batch.label).item()
            avg_acc.append(acc)
    avg_acc = np.array(avg_acc).mean()
    print('>>test--avg_acc = {0}'.format(avg_acc))


for epoch in range(10):
    eval(lstm, val_iterator, criteon)
    train(lstm, train_iterator, optimizer, criteon)

十、GRU 案例

1、Tensorflow2-GRUCell案例(构建每一个cell及memorycell)-imdb数据集【电影评论情感二分类】

import os

os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
import tensorflow as tf
import numpy as np
from tensorflow import keras
from tensorflow.keras import layers
import time

tf.random.set_seed(22)
np.random.seed(22)

assert tf.__version__.startswith('2.')

batch_size = 500  # 每次训练500个句子

total_words = 10000  # the most frequest words
max_review_len = 80  # 设置句子长度,如果有的句子的长度不到80则补齐,如果有的句子超过80则截断
embedding_len = 100  # 每个单词转为向量后的向量维度

# 一、获取数据集
(X_train, Y_train), (X_val, Y_val) = keras.datasets.imdb.load_data(num_words=total_words)
print('X_train[0] = {0},\nY_train[0] = {1}'.format(X_train[0], Y_train[0]))
print('X_train.shpae = {0},Y_train.shpae = {1}------------type(X_train) = {2},type(Y_train) = {3}'.format(X_train.shape, Y_train.shape, type(X_train), type(Y_train)))

# 二、数据处理
# 2.1 # 设置句子统一长度
X_train = keras.preprocessing.sequence.pad_sequences(X_train, maxlen=max_review_len)  # 设置句子长度    [b, 80]
X_val = keras.preprocessing.sequence.pad_sequences(X_val, maxlen=max_review_len)  # 设置句子长度
print('X_train.shpae = {0},Y_train.shpae = {1},tf.reduce_max(Y_train) = {2},tf.reduce_min(Y_train) = {3}'.format(X_train.shape, Y_train.shape, tf.reduce_max(Y_train), tf.reduce_min(Y_train)))
# 2.1 处理训练集为batch模型
db_train = tf.data.Dataset.from_tensor_slices((X_train, Y_train))
db_train = db_train.shuffle(1000).batch(batch_size, drop_remainder=True)  # 通过 drop_remainder=True 把 最后一个不满足batch_size大小的batch丢弃掉
db_val = tf.data.Dataset.from_tensor_slices((X_val, Y_val))
db_val = db_val.batch(batch_size, drop_remainder=True)  # 通过 drop_remainder=True 把 最后一个不满足batch_size大小的batch丢弃掉
print('db_train = {0},len(db_train) = {1}'.format(db_train, len(db_train)))


class MyRNN(keras.Model):
    def __init__(self, output_dim):
        super(MyRNN, self).__init__()
        # ***********************************************************memoryCell***********************************************************
        # [b, 64]
        # memoryCell用于保存上一次的隐藏状态值h_{t-1},用于计算本次的h_t时的输入值
        # 使用多个memoryCell串联即实现Deep的作用
        self.memoryCell01 = [tf.zeros([batch_size, output_dim])]  # 初始化memoryCell01,维度为  [b, 64]
        self.memoryCell02 = [tf.zeros([batch_size, output_dim])]  # 初始化memoryCell02,维度为  [b, 64]
        # ***********************************************************Embedding***********************************************************
        # 将每一个句子(维度为[80,1],80表示每个句子包含的word数量,1表示1个word)变换为wordEmbedding(维度为[80,100],80表示每个句子包含的word数量,100表示每个wordEmbedding的维度)
        # [b, 80, 1] => [b, 80, 100]
        # input_dim:表示输入维度,即设定词库总单词数量;b
        # input_length:表示每个句子统一长度(包含的单词数量);80
        # output_dim:表示输出维度,即每个单词转为向量后的向量维度;100
        self.embedding = layers.Embedding(input_dim=total_words, input_length=max_review_len, output_dim=embedding_len)
        # ***********************************************************RNNCell Layer***********************************************************
        # [b, 80, 100]=>[b, 64]
        self.rnn_cell01 = layers.GRUCell(output_dim, dropout=0.2)  # output_dim: dimensionality of the output space. 隐藏状态的维度;dropout 防止过拟合
        self.rnn_cell02 = layers.GRUCell(output_dim, dropout=0.2)
        # ***********************************************************全连接层***********************************************************
        # [b, 64] => [b, 1]
        self.outlayer = layers.Dense(1)

    def call(self, inputs, training=None):
        """
        net(x) net(x, training=True) :train mode
        net(x, training=False): test mode
        :param inputs: [b, 80, 1]
        :param training:
        :return:
        """
        # ***********************************************************Embedding***********************************************************
        # embedding: [b, 80, 1] => [b, 80, 100]
        wordEmbeddings = self.embedding(inputs)  # inputs 为1个batch的句子文本
        print('\nwordEmbeddings.shape = {0}, wordEmbeddings = {1}'.format(wordEmbeddings.shape, wordEmbeddings))
        # rnn cell compute
        # ***********************************************************RNNCell Layer***********************************************************
        # [b, 80, 100] => [b, 1, 64],每个句子都从降维:[80, 100]=>[1, 64]
        memoryCell01 = self.memoryCell01
        memoryCell02 = self.memoryCell02
        wordEmbedding_index = 0
        for wordEmbedding in tf.unstack(wordEmbeddings, axis=1):  # wordEmbedding: [b, 100],将每个句子中的80个单词展开,即按读取该句子的时间轴展开
            # 隐含状态:out01/out02: [b, 64]
            # h_t = x_t×w_{xh}+h_{t-1}×w_{hh};其中:x_t=wordEmbedding;h_{t-1}=memoryCell01;输出值h_t = out01
            # memoryCell01保存2个值:第一个值是隐藏状态C_t,第二个值是隐藏状态输出值h_t
            out01, memoryCell01 = self.rnn_cell01(wordEmbedding, memoryCell01, training=training)  # 用输出值更新memoryCell01;   training=True 表示模式是训练模式,dropout功能有效,默认是True
            # 将rnn_cell01的输出值out01传入下一个rnn_cell02提升RNNCell Layer的提取能力
            # memoryCell01保存2个值:第一个值是隐藏状态C_t,第二个值是隐藏状态输出值h_t
            out02, memoryCell02 = self.rnn_cell02(out01, memoryCell02, training=training)  # 用输出值更新memoryCell02; training=True 表示模式是训练模式,dropout功能有效,默认是True
            if wordEmbedding_index == 0:
                print('wordEmbedding.shape = {0}, wordEmbedding = {1}'.format(wordEmbedding.shape, wordEmbedding))
                print('out01.shape = {0}, memoryCell01[0].shape = {1}, out01 = {2}'.format(out01.shape, memoryCell01[0].shape, out01))
                print('out02.shape = {0}, memoryCell02[0].shape = {1}, out02 = {2}'.format(out02.shape, memoryCell02[0].shape, out02))
            wordEmbedding_index += 1
        # ***********************************************************全连接层***********************************************************
        # out: [b, 1, 64] => [b, 1, 1]
        out_logit = self.outlayer(out02)  # out02代表了每个句子的语义信息的提取
        print('out_logit.shape = {0}, out_logit = {1}'.format(out_logit.shape, out_logit))
        out_prob = tf.sigmoid(out_logit)  # p(y is pos|x)
        print('out_prob.shape = {0}, out_prob = {1}, {2}'.format(out_prob.shape, out_prob, '\n'))
        return out_prob


def main():
    output_dim = 64     # 设定输出的隐藏状态维度  [b, 100] => [b,64]
    epochs = 4
    t0 = time.time()
    network = MyRNN(output_dim)
    # 不需要设置from_logits=True,因为MyRNN()中已经设定了激活函数层 out_prob = tf.sigmoid(X)
    # metrics=['accuracy']表示打印测试数据
    network.compile(optimizer=keras.optimizers.Adam(0.001),
                    loss=tf.losses.BinaryCrossentropy(),
                    metrics=['accuracy'])
    print('\n***********************************************************训练network:开始***********************************************************')
    network.fit(db_train, epochs=epochs, validation_data=db_val)
    print('***********************************************************训练network:结束***********************************************************')
    print('\n***********************************************************评估network(其实训练时已经评估):开始***********************************************************')
    network.evaluate(db_val)  # 评估模型
    print('***********************************************************评估network(其实训练时已经评估):结束***********************************************************')
    t1 = time.time()
    print('total time cost:', t1 - t0)


if __name__ == '__main__':
    main()

打印结果:

X_train[0] = [1, 14, 22, 16, 43, 530, 973, 1622, 1385, 65, 458, 4468, 66, 3941, 4, 173, 36, 256, 5, 25, 100, 43, 838, 112, 50, 670, 2, 9, 35, 480, 284, 5, 150, 4, 172, 112, 167, 2, 336, 385, 39, 4, 172, 4536, 1111, 17, 546, 38, 13, 447, 4, 192, 50, 16, 6, 147, 2025, 19, 14, 22, 4, 1920, 4613, 469, 4, 22, 71, 87, 12, 16, 43, 530, 38, 76, 15, 13, 1247, 4, 22, 17, 515, 17, 12, 16, 626, 18, 2, 5, 62, 386, 12, 8, 316, 8, 106, 5, 4, 2223, 5244, 16, 480, 66, 3785, 33, 4, 130, 12, 16, 38, 619, 5, 25, 124, 51, 36, 135, 48, 25, 1415, 33, 6, 22, 12, 215, 28, 77, 52, 5, 14, 407, 16, 82, 2, 8, 4, 107, 117, 5952, 15, 256, 4, 2, 7, 3766, 5, 723, 36, 71, 43, 530, 476, 26, 400, 317, 46, 7, 4, 2, 1029, 13, 104, 88, 4, 381, 15, 297, 98, 32, 2071, 56, 26, 141, 6, 194, 7486, 18, 4, 226, 22, 21, 134, 476, 26, 480, 5, 144, 30, 5535, 18, 51, 36, 28, 224, 92, 25, 104, 4, 226, 65, 16, 38, 1334, 88, 12, 16, 283, 5, 16, 4472, 113, 103, 32, 15, 16, 5345, 19, 178, 32],
Y_train[0] = 1
X_train.shpae = (25000,),Y_train.shpae = (25000,)------------type(X_train) = <class 'numpy.ndarray'>type(Y_train) = <class 'numpy.ndarray'>
X_train.shpae = (25000, 80),Y_train.shpae = (25000,),tf.reduce_max(Y_train) = 1,tf.reduce_min(Y_train) = 0
db_train = <BatchDataset shapes: ((500, 80), (500,)), types: (tf.int32, tf.int64)>len(db_train) = 50

***********************************************************训练network:开始***********************************************************
Epoch 1/4

wordEmbeddings.shape = (500, 80, 100), wordEmbeddings = Tensor("my_rnn/embedding/embedding_lookup/Identity:0", shape=(500, 80, 100), dtype=float32)
wordEmbedding.shape = (500, 100), wordEmbedding = Tensor("my_rnn/unstack:0", shape=(500, 100), dtype=float32)
out01.shape = (500, 64), memoryCell01[0].shape = (500, 64), out01 = Tensor("my_rnn/gru_cell/add_3:0", shape=(500, 64), dtype=float32)
out02.shape = (500, 64), memoryCell02[0].shape = (500, 64), out02 = Tensor("my_rnn/gru_cell_1/add_3:0", shape=(500, 64), dtype=float32)
out_logit.shape = (500, 1), out_logit = Tensor("my_rnn/dense/BiasAdd:0", shape=(500, 1), dtype=float32)
out_prob.shape = (500, 1), out_prob = Tensor("my_rnn/Sigmoid:0", shape=(500, 1), dtype=float32), 


wordEmbeddings.shape = (500, 80, 100), wordEmbeddings = Tensor("my_rnn/embedding/embedding_lookup/Identity:0", shape=(500, 80, 100), dtype=float32)
wordEmbedding.shape = (500, 100), wordEmbedding = Tensor("my_rnn/unstack:0", shape=(500, 100), dtype=float32)
out01.shape = (500, 64), memoryCell01[0].shape = (500, 64), out01 = Tensor("my_rnn/gru_cell/add_3:0", shape=(500, 64), dtype=float32)
out02.shape = (500, 64), memoryCell02[0].shape = (500, 64), out02 = Tensor("my_rnn/gru_cell_1/add_3:0", shape=(500, 64), dtype=float32)
out_logit.shape = (500, 1), out_logit = Tensor("my_rnn/dense/BiasAdd:0", shape=(500, 1), dtype=float32)
out_prob.shape = (500, 1), out_prob = Tensor("my_rnn/Sigmoid:0", shape=(500, 1), dtype=float32), 

50/50 [==============================] - ETA: 0s - loss: 0.6669 - accuracy: 0.5711
wordEmbeddings.shape = (500, 80, 100), wordEmbeddings = Tensor("my_rnn/embedding/embedding_lookup/Identity:0", shape=(500, 80, 100), dtype=float32)
wordEmbedding.shape = (500, 100), wordEmbedding = Tensor("my_rnn/unstack:0", shape=(500, 100), dtype=float32)
out01.shape = (500, 64), memoryCell01[0].shape = (500, 64), out01 = Tensor("my_rnn/gru_cell/add_3:0", shape=(500, 64), dtype=float32)
out02.shape = (500, 64), memoryCell02[0].shape = (500, 64), out02 = Tensor("my_rnn/gru_cell_1/add_3:0", shape=(500, 64), dtype=float32)
out_logit.shape = (500, 1), out_logit = Tensor("my_rnn/dense/BiasAdd:0", shape=(500, 1), dtype=float32)
out_prob.shape = (500, 1), out_prob = Tensor("my_rnn/Sigmoid:0", shape=(500, 1), dtype=float32), 

50/50 [==============================] - 23s 265ms/step - loss: 0.6657 - accuracy: 0.5727 - val_loss: 0.4395 - val_accuracy: 0.7975
Epoch 2/4
50/50 [==============================] - 11s 217ms/step - loss: 0.3770 - accuracy: 0.8304 - val_loss: 0.3646 - val_accuracy: 0.8424
Epoch 3/4
50/50 [==============================] - 10s 204ms/step - loss: 0.2573 - accuracy: 0.8946 - val_loss: 0.4230 - val_accuracy: 0.8372
Epoch 4/4
50/50 [==============================] - 10s 209ms/step - loss: 0.2107 - accuracy: 0.9183 - val_loss: 0.4979 - val_accuracy: 0.8305
***********************************************************训练network:结束***********************************************************

***********************************************************评估network(其实训练时已经评估):开始***********************************************************
50/50 [==============================] - 3s 59ms/step - loss: 0.4979 - accuracy: 0.8305
***********************************************************评估network(其实训练时已经评估):结束***********************************************************
total time cost: 57.23774862289429

Process finished with exit code 0

2、Tensorflow2-GRU案例-imdb数据集【电影评论情感二分类】

import os

os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
import tensorflow as tf
import numpy as np
from tensorflow import keras
from tensorflow.keras import layers
import time

tf.random.set_seed(22)
np.random.seed(22)

assert tf.__version__.startswith('2.')

batch_size = 500  # 每次训练500个句子

total_words = 10000  # the most frequest words
max_review_len = 80  # 设置句子长度,如果有的句子的长度不到80则补齐,如果有的句子超过80则截断
embedding_len = 100  # 每个单词转为向量后的向量维度

# 一、获取数据集
(X_train, Y_train), (X_val, Y_val) = keras.datasets.imdb.load_data(num_words=total_words)
print('X_train[0] = {0},\nY_train[0] = {1}'.format(X_train[0], Y_train[0]))
print('X_train.shpae = {0},Y_train.shpae = {1}------------type(X_train) = {2},type(Y_train) = {3}'.format(X_train.shape, Y_train.shape, type(X_train), type(Y_train)))

# 二、数据处理
# 2.1 # 设置句子统一长度
X_train = keras.preprocessing.sequence.pad_sequences(X_train, maxlen=max_review_len)  # 设置句子长度    [b, 80]
X_val = keras.preprocessing.sequence.pad_sequences(X_val, maxlen=max_review_len)  # 设置句子长度
print('X_train.shpae = {0},Y_train.shpae = {1},tf.reduce_max(Y_train) = {2},tf.reduce_min(Y_train) = {3}'.format(X_train.shape, Y_train.shape, tf.reduce_max(Y_train), tf.reduce_min(Y_train)))
# 2.1 处理训练集为batch模型
db_train = tf.data.Dataset.from_tensor_slices((X_train, Y_train))
db_train = db_train.shuffle(1000).batch(batch_size, drop_remainder=True)  # 通过 drop_remainder=True 把 最后一个不满足batch_size大小的batch丢弃掉
db_val = tf.data.Dataset.from_tensor_slices((X_val, Y_val))
db_val = db_val.batch(batch_size, drop_remainder=True)  # 通过 drop_remainder=True 把 最后一个不满足batch_size大小的batch丢弃掉
print('db_train = {0},len(db_train) = {1}'.format(db_train, len(db_train)))


class MyRNN(keras.Model):
    def __init__(self, output_dim):
        super(MyRNN, self).__init__()
        # ***********************************************************Embedding***********************************************************
        # transform text to embedding representation
        # 将每一个句子(维度为[80,1],80表示每个句子包含的word数量,1表示1个word)变换为wordEmbedding(维度为[80,100],80表示每个句子包含的word数量,100表示每个wordEmbedding的维度)
        # [b, 80, 1] => [b, 80, 100]
        # input_dim:表示输入维度,即设定词库总单词数量;b
        # input_length:表示每个句子统一长度(包含的单词数量);80
        # output_dim:表示输出维度,即每个单词转为向量后的向量维度;100
        self.embedding = layers.Embedding(input_dim=total_words, input_length=max_review_len, output_dim=embedding_len)
        # ***********************************************************RNN神经网络结构:SimpleRNN 表示SimpleRNN连接层***********************************************************
        # [b, 80, 100]=>[b, 64]
        self.rnn = keras.Sequential([
            # output_dim: dimensionality of the output space. 隐藏状态的维度;dropout 防止过拟合
            # return_sequences: Boolean. Whether to return the last output in the output sequence, or the full sequence.
            # unroll: Boolean (default False). If True, the network will be unrolled, else a symbolic loop will be used.
            # Unrolling can speed-up a RNN, although it tends to be more memory-intensive. Unrolling is only suitable for short sequences.
            layers.GRU(output_dim, dropout=0.5, return_sequences=True, unroll=True),
            layers.GRU(output_dim, dropout=0.5, unroll=True)
        ])
        # ***********************************************************全连接层***********************************************************
        # [b, 64] => [b, 1]
        self.outlayer = layers.Dense(1)

    def call(self, inputs, training=None):
        """
        net(x) net(x, training=True) :train mode
        net(x, training=False): test
        :param inputs: [b, 80]
        :param training:
        :return:
        """
        # ***********************************************************Embedding***********************************************************
        # embedding: [b, 80, 1] => [b, 80, 100]
        x_wordEmbeddings = self.embedding(inputs)  # inputs 为1个batch的句子文本
        print('\nx_wordEmbeddings.shape = {0}, x_wordEmbeddings = {1}'.format(x_wordEmbeddings.shape, x_wordEmbeddings))
        # ***********************************************************RNN神经网络结构计算***********************************************************
        out = self.rnn(x_wordEmbeddings)  # x: [b, 80, 100] => [b, 64]
        print('out.shape = {0}, out = {1}'.format(out.shape, out))
        out_logit = self.outlayer(out)  # 隐含状态=>0/1   out: [b, 64] => [b, 1]
        print('out_logit.shape = {0}, out_logit = {1}'.format(out_logit.shape, out_logit))
        out_prob = tf.sigmoid(out_logit)  # p(y is pos|x)
        print('out_prob.shape = {0}, out_prob = {1}, {2}'.format(out_prob.shape, out_prob, '\n'))
        return out_prob


def main():
    output_dim = 64
    epochs = 4
    t0 = time.time()
    network = MyRNN(output_dim)
    # 不需要设置from_logits=True,因为MyRNN()中已经设定了激活函数层 out_prob = tf.sigmoid(X)
    # metrics=['accuracy']表示打印测试数据
    network.compile(optimizer=keras.optimizers.Adam(0.001),
                    loss=tf.losses.BinaryCrossentropy(),
                    metrics=['accuracy'])
    print('\n***********************************************************训练network:开始***********************************************************')
    network.fit(db_train, epochs=epochs, validation_data=db_val)
    print('***********************************************************训练network:结束***********************************************************')
    print('\n***********************************************************评估network(其实训练时已经评估):开始***********************************************************')
    network.evaluate(db_val)  # 评估模型
    print('***********************************************************评估network(其实训练时已经评估):结束***********************************************************')
    t1 = time.time()
    print('total time cost:', t1 - t0)


if __name__ == '__main__':
    main()

打印结果:

X_train[0] = [1, 14, 22, 16, 43, 530, 973, 1622, 1385, 65, 458, 4468, 66, 3941, 4, 173, 36, 256, 5, 25, 100, 43, 838, 112, 50, 670, 2, 9, 35, 480, 284, 5, 150, 4, 172, 112, 167, 2, 336, 385, 39, 4, 172, 4536, 1111, 17, 546, 38, 13, 447, 4, 192, 50, 16, 6, 147, 2025, 19, 14, 22, 4, 1920, 4613, 469, 4, 22, 71, 87, 12, 16, 43, 530, 38, 76, 15, 13, 1247, 4, 22, 17, 515, 17, 12, 16, 626, 18, 2, 5, 62, 386, 12, 8, 316, 8, 106, 5, 4, 2223, 5244, 16, 480, 66, 3785, 33, 4, 130, 12, 16, 38, 619, 5, 25, 124, 51, 36, 135, 48, 25, 1415, 33, 6, 22, 12, 215, 28, 77, 52, 5, 14, 407, 16, 82, 2, 8, 4, 107, 117, 5952, 15, 256, 4, 2, 7, 3766, 5, 723, 36, 71, 43, 530, 476, 26, 400, 317, 46, 7, 4, 2, 1029, 13, 104, 88, 4, 381, 15, 297, 98, 32, 2071, 56, 26, 141, 6, 194, 7486, 18, 4, 226, 22, 21, 134, 476, 26, 480, 5, 144, 30, 5535, 18, 51, 36, 28, 224, 92, 25, 104, 4, 226, 65, 16, 38, 1334, 88, 12, 16, 283, 5, 16, 4472, 113, 103, 32, 15, 16, 5345, 19, 178, 32],
Y_train[0] = 1
X_train.shpae = (25000,),Y_train.shpae = (25000,)------------type(X_train) = <class 'numpy.ndarray'>type(Y_train) = <class 'numpy.ndarray'>
X_train.shpae = (25000, 80),Y_train.shpae = (25000,),tf.reduce_max(Y_train) = 1,tf.reduce_min(Y_train) = 0
db_train = <BatchDataset shapes: ((500, 80), (500,)), types: (tf.int32, tf.int64)>len(db_train) = 50

***********************************************************训练network:开始***********************************************************
WARNING:tensorflow:Layer gru will not use cuDNN kernel since it doesn't meet the cuDNN kernel criteria. It will use generic GPU kernel as fallback when running on GPU
WARNING:tensorflow:Layer gru_1 will not use cuDNN kernel since it doesn't meet the cuDNN kernel criteria. It will use generic GPU kernel as fallback when running on GPU
Epoch 1/4

x_wordEmbeddings.shape = (500, 80, 100), x_wordEmbeddings = Tensor("my_rnn/embedding/embedding_lookup/Identity:0", shape=(500, 80, 100), dtype=float32)
out.shape = (500, 64), out = Tensor("my_rnn/sequential/gru_1/gru_cell_1/add_319:0", shape=(500, 64), dtype=float32)
out_logit.shape = (500, 1), out_logit = Tensor("my_rnn/dense/BiasAdd:0", shape=(500, 1), dtype=float32)
out_prob.shape = (500, 1), out_prob = Tensor("my_rnn/Sigmoid:0", shape=(500, 1), dtype=float32), 


x_wordEmbeddings.shape = (500, 80, 100), x_wordEmbeddings = Tensor("my_rnn/embedding/embedding_lookup/Identity:0", shape=(500, 80, 100), dtype=float32)
out.shape = (500, 64), out = Tensor("my_rnn/sequential/gru_1/gru_cell_1/add_319:0", shape=(500, 64), dtype=float32)
out_logit.shape = (500, 1), out_logit = Tensor("my_rnn/dense/BiasAdd:0", shape=(500, 1), dtype=float32)
out_prob.shape = (500, 1), out_prob = Tensor("my_rnn/Sigmoid:0", shape=(500, 1), dtype=float32), 

50/50 [==============================] - ETA: 0s - loss: 0.6784 - accuracy: 0.5487
x_wordEmbeddings.shape = (500, 80, 100), x_wordEmbeddings = Tensor("my_rnn/embedding/embedding_lookup/Identity:0", shape=(500, 80, 100), dtype=float32)
out.shape = (500, 64), out = Tensor("my_rnn/sequential/gru_1/gru_cell_1/add_319:0", shape=(500, 64), dtype=float32)
out_logit.shape = (500, 1), out_logit = Tensor("my_rnn/dense/BiasAdd:0", shape=(500, 1), dtype=float32)
out_prob.shape = (500, 1), out_prob = Tensor("my_rnn/Sigmoid:0", shape=(500, 1), dtype=float32), 

50/50 [==============================] - 26s 304ms/step - loss: 0.6775 - accuracy: 0.5501 - val_loss: 0.4695 - val_accuracy: 0.7734
Epoch 2/4
50/50 [==============================] - 11s 223ms/step - loss: 0.4109 - accuracy: 0.8152 - val_loss: 0.3752 - val_accuracy: 0.8306
Epoch 3/4
50/50 [==============================] - 11s 213ms/step - loss: 0.2942 - accuracy: 0.8750 - val_loss: 0.3690 - val_accuracy: 0.8438
Epoch 4/4
50/50 [==============================] - 11s 228ms/step - loss: 0.2450 - accuracy: 0.9005 - val_loss: 0.3863 - val_accuracy: 0.8379
***********************************************************训练network:结束***********************************************************

***********************************************************评估network(其实训练时已经评估):开始***********************************************************
50/50 [==============================] - 3s 57ms/step - loss: 0.3863 - accuracy: 0.8379
***********************************************************评估network(其实训练时已经评估):结束***********************************************************
total time cost: 62.06642150878906

Process finished with exit code 0

Deep Learning与Structure Learning的结合

在这里插入图片描述




参考资料:
全面理解LSTM网络及输入,输出,hidden_size等参数
LSTM输入输出理解
pytorch-LSTM的输入和输出尺寸
理解Pytorch中LSTM的输入输出参数含义
理解 LSTM 网络
深度学习原理:循环神经网络RNN和LSTM网络结构、结构变体(peephole,GRU)、前向传播公式以及TF实现简单解析
The Unreasonable Effectiveness of Recurrent Neural Networks
Illustrated Guide to Recurrent Neural Networks
The Vanishing Gradient Problem
Lecture 15: Exploding and Vanishing Gradients
Why LSTMs Stop Your Gradients From Vanishing: A View from the Backwards Pass
LSTM如何解决梯度消失或爆炸的?
为什么相比于RNN,LSTM在梯度消失上表现更好?
LSTM系列的梯度问题
Illustrated Guide to LSTM’s and GRU’s: A step by step explanation
如何计算 LSTM 的参数量

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值