NNDL 实验七 循环神经网络(3)LSTM的记忆能力实验

    • 6.3 LSTM的记忆能力实验
      • 6.3.1 模型构建
        • 6.3.1.1 LSTM层
        • 6.3.1.2 模型汇总
      • 【思考题1】LSTM与SRN实验结果对比,谈谈看法。(选做)
      • 6.3.2 模型训练
        • 6.3.2.1 训练指定长度的数字预测模型
        • 6.3.2.3 损失曲线展示
      • 6.3.3 模型评价
        • 6.3.3.1 在测试集上进行模型评价
        • 6.3.3.2 模型在不同长度的数据集上的准确率变化图
        • 6.3.3.3 LSTM模型门状态和单元状态的变化
    • 【思考题2】LSTM与SRN在不同长度数据集上的准确度对比,谈谈看法。(选做)
    • 【思考题3】分析LSTM中单元状态和门数值的变化图,并用自己的话解释该图。
    • 全面总结RNN(必做)
    • 循环神经网络总结
    • 参考

6.3 LSTM的记忆能力实验

长短期记忆网络(Long Short-Term Memory Network,LSTM)是一种可以有效缓解长程依赖问题的循环神经网络.LSTM 的特点是引入了一个新的内部状态(Internal State) c ∈ R D c \in \mathbb{R}^D cRD 和门控机制(Gating Mechanism).不同时刻的内部状态以近似线性的方式进行传递,从而缓解梯度消失或梯度爆炸问题.同时门控机制进行信息筛选,可以有效地增加记忆能力.例如,输入门可以让网络忽略无关紧要的输入信息,遗忘门可以使得网络保留有用的历史信息.在上一节的数字求和任务中,如果模型能够记住前两个非零数字,同时忽略掉一些不重要的干扰信息,那么即时序列很长,模型也有效地进行预测.

LSTM 模型在第 t t t 步时,循环单元的内部结构如图6.10所示.


图6.10 LSTM网络的循环单元结构

提醒:为了和代码的实现保存一致性,这里使用形状为 (样本数量 × 序列长度 × 特征维度) 的张量来表示一组样本.

假设一组输入序列为 X ∈ R B × L × M \boldsymbol{X}\in \mathbb{R}^{B\times L\times M} XRB×L×M,其中 B B B为批大小, L L L为序列长度, M M M为输入特征维度,LSTM从从左到右依次扫描序列,并通过循环单元计算更新每一时刻的状态内部状态 C t ∈ R B × D \boldsymbol{C}_{t} \in \mathbb{R}^{B \times D} CtRB×D和输出状态 H t ∈ R B × D \boldsymbol{H}_{t} \in \mathbb{R}^{B \times D} HtRB×D

具体计算分为三步:

(1)计算三个“门”

在时刻 t t t,LSTM的循环单元将当前时刻的输入 X t ∈ R B × M \boldsymbol{X}_t \in \mathbb{R}^{B \times M} XtRB×M与上一时刻的输出状态 H t − 1 ∈ R B × D \boldsymbol{H}_{t-1} \in \mathbb{R}^{B \times D} Ht1RB×D,计算一组输入门 I t \boldsymbol{I}_t It、遗忘门 F t \boldsymbol{F}_t Ft和输出门 O t \boldsymbol{O}_t Ot,其计算公式为

I t = σ ( X t W i + H t − 1 U i + b i ) ∈ R B × D , F t = σ ( X t W f + H t − 1 U f + b f ) ∈ R B × D , O t = σ ( X t W o + H t − 1 U o + b o ) ∈ R B × D , \boldsymbol{I}_{t}=\sigma(\boldsymbol{X}_t\boldsymbol{W}_i+\boldsymbol{H}_{t-1}\boldsymbol{U}_i+\boldsymbol{b}_i) \in \mathbb{R}^{B \times D},\\ \boldsymbol{F}_{t}=\sigma(\boldsymbol{X}_t\boldsymbol{W}_f+\boldsymbol{H}_{t-1}\boldsymbol{U}_f+\boldsymbol{b}_f) \in \mathbb{R}^{B \times D},\\ \boldsymbol{O}_{t}=\sigma(\boldsymbol{X}_t\boldsymbol{W}_o+\boldsymbol{H}_{t-1}\boldsymbol{U}_o+\boldsymbol{b}_o) \in \mathbb{R}^{B \times D}, It=σ(XtWi+Ht1Ui+bi)RB×D,Ft=σ(XtWf+Ht1Uf+bf)RB×D,Ot=σ(XtWo+Ht1Uo+bo)RB×D,

其中 W ∗ ∈ R M × D , U ∗ ∈ R D × D , b ∗ ∈ R D \boldsymbol{W}_* \in \mathbb{R}^{M \times D},\boldsymbol{U}_* \in \mathbb{R}^{D \times D},\boldsymbol{b}_* \in \mathbb{R}^{D} WRM×D,URD×D,bRD为可学习的参数, σ \sigma σ表示Logistic函数,将“门”的取值控制在 ( 0 , 1 ) (0,1) (0,1)区间。这里的“门”都是 B B B个样本组成的矩阵,每一行为一个样本的“门”向量。

(2)计算内部状态

首先计算候选内部状态:

C ~ t = tanh ⁡ ( X t W c + H t − 1 U c + b c ) ∈ R B × D , \tilde{\boldsymbol{C}}_{t}=\tanh(\boldsymbol{X}_t\boldsymbol{W}_c+\boldsymbol{H}_{t-1}\boldsymbol{U}_c+\boldsymbol{b}_c) \in \mathbb{R}^{B \times D}, C~t=tanh(XtWc+Ht1Uc+bc)RB×D,

其中 W c ∈ R M × D , U c ∈ R D × D , b c ∈ R D \boldsymbol{W}_c \in \mathbb{R}^{M \times D}, \boldsymbol{U}_c \in \mathbb{R}^{D \times D},\boldsymbol{b}_c \in \mathbb{R}^{D} WcRM×D,UcRD×D,bcRD为可学习的参数。

使用遗忘门和输入门,计算时刻 t t t的内部状态:
C t = F t ⊙ C t − 1 + I t ⊙ C ~ t , \boldsymbol{C}_{t} = \boldsymbol{F}_t \odot \boldsymbol{C}_{t-1} + \boldsymbol{I}_{t} \odot \boldsymbol{\tilde{C}}_{t}, Ct=FtCt1+ItC~t,
其中 ⊙ \odot 为逐元素积。

3)计算输出状态
当前LSTM单元状态(候选状态)的计算公式为:
LSTM单元状态向量 C t \boldsymbol{C}_{t} Ct H t \boldsymbol{H}_t Ht的计算公式为
C t = F t ⊙ C t − 1 + I t ⊙ C ~ t , H t = O t ⊙ tanh ( C t ) . \boldsymbol{C}_{t} = \boldsymbol F_t \odot \boldsymbol{C}_{t-1} + \boldsymbol{I}_{t} \odot \boldsymbol{\tilde{C}}_{t},\\ \boldsymbol{H}_{t} = \boldsymbol{O}_{t} \odot \text{tanh}(\boldsymbol{C}_{t}). Ct=FtCt1+ItC~tHt=Ottanh(Ct).

LSTM循环单元结构的输入是 t − 1 t-1 t1时刻内部状态向量 C t − 1 ∈ R B × D \boldsymbol{C}_{t-1} \in \mathbb{R}^{B \times D} Ct1RB×D和隐状态向量 H t − 1 ∈ R B × D \boldsymbol{H}_{t-1} \in \mathbb{R}^{B \times D} Ht1RB×D,输出是当前时刻 t t t的状态向量 C t ∈ R B × D \boldsymbol{C}_{t} \in \mathbb{R}^{B \times D} CtRB×D和隐状态向量 H t ∈ R B × D \boldsymbol{H}_{t} \in \mathbb{R}^{B \times D} HtRB×D。通过LSTM循环单元,整个网络可以建立较长距离的时序依赖关系。

通过学习这些门的设置,LSTM可以选择性地忽略或者强化当前的记忆或是输入信息,帮助网络更好地学习长句子的语义信息。

在本节中,我们使用LSTM模型重新进行数字求和实验,验证LSTM模型的长程依赖能力。

6.3.1 模型构建

在本实验中,我们将使用第6.1.2.4节中定义Model_RNN4SeqClass模型,并构建 LSTM 算子.只需要实例化 LSTM 算,并传入Model_RNN4SeqClass模型,就可以用 LSTM 进行数字求和实验

6.3.1.1 LSTM层

LSTM层的代码与SRN层结构相似,只是在SRN层的基础上增加了内部状态、输入门、遗忘门和输出门的定义和计算。这里LSTM层的输出也依然为序列的最后一个位置的隐状态向量。代码实现如下:

import torch
import torch.nn as nn
import torch.nn.functional as F
# 声明LSTM和相关参数
class LSTM(nn.Module):
    def __init__(self, input_size, hidden_size,
                 Wi_attr=torch.zeros(size=[input_size, hidden_size], dtype=torch.float32),
                 Wf_attr=torch.zeros(size=[input_size, hidden_size], dtype=torch.float32), 
                 Wo_attr=torch.zeros(size=[input_size, hidden_size], dtype=torch.float32), 
                 Wc_attr=torch.zeros(size=[input_size, hidden_size], dtype=torch.float32),
                 Ui_attr=torch.zeros(size=[input_size, hidden_size], dtype=torch.float32),
                 Uf_attr=torch.zeros(size=[input_size, hidden_size], dtype=torch.float32), 
                 Uo_attr=torch.zeros(size=[input_size, hidden_size], dtype=torch.float32), 
                 Uc_attr=torch.zeros(size=[input_size, hidden_size], dtype=torch.float32),
                 bi_attr=torch.zeros(size=[1, hidden_size], dtype=torch.float32),
                 bf_attr=torch.zeros(size=[1, hidden_size], dtype=torch.float32),
                 bo_attr=torch.zeros(size=[1, hidden_size], dtype=torch.float32), 
                 bc_attr=torch.zeros(size=[1, hidden_size], dtype=torch.float32)):
        super(LSTM, self).__init__()
        self.input_size = input_size
        self.hidden_size = hidden_size

        # 初始化模型参数
        self.W_i = torch.nn.Parameter(Wi_attr)
        self.W_f = torch.nn.Parameter(Wf_attr)
        self.W_o = torch.nn.Parameter(Wo_attr)
        self.W_c = torch.nn.Parameter(Wc_attr)
        self.U_i = torch.nn.Parameter(Ui_attr)
        self.U_f = torch.nn.Parameter(Uf_attr)
        self.U_o = torch.nn.Parameter(Uo_attr)
        self.U_c = torch.nn.Parameter(Uc_attr)
        self.b_i = torch.nn.Parameter(bi_attr)
        self.b_f = torch.nn.Parameter(bf_attr)
        self.b_o = torch.nn.Parameter(bo_attr)
        self.b_c = torch.nn.Parameter(bc_attr)
    
    # 初始化状态向量和隐状态向量
    def init_state(self, batch_size):
        hidden_state = torch.zeros(size=[batch_size, self.hidden_size], dtype=torch.float32)
        cell_state = torch.zeros(size=[batch_size, self.hidden_size], dtype=torch.float32)
        return hidden_state, cell_state

    # 定义前向计算
    def forward(self, inputs, states=None):
        # inputs: 输入数据,其shape为batch_size x seq_len x input_size
        batch_size, seq_len, input_size = inputs.shape  
        # 初始化起始的单元状态和隐状态向量,其shape为batch_size x hidden_size
        if states is None:
            states = self.init_state(batch_size)
        hidden_state, cell_state = states

        # 执行LSTM计算,包括:输入门、遗忘门和输出门、候选内部状态、内部状态和隐状态向量
        for step in range(seq_len):
              # 获取当前时刻的输入数据step_input: 其shape为batch_size x input_size
            step_input = inputs[:, step, :]
            # 计算输入门, 遗忘门和输出门, 其shape为:batch_size x hidden_size
            I_gate = F.sigmoid(torch.matmul(step_input, self.W_i) + torch.matmul(hidden_state, self.U_i) + self.b_i)
            F_gate = F.sigmoid(torch.matmul(step_input, self.W_f) + torch.matmul(hidden_state, self.U_f) + self.b_f)
            O_gate = F.sigmoid(torch.matmul(step_input, self.W_o) + torch.matmul(hidden_state, self.U_o) + self.b_o)
            # 计算候选状态向量, 其shape为:batch_size x hidden_size
            C_tilde = F.tanh(torch.matmul(step_input, self.W_c) + torch.matmul(hidden_state, self.U_c) + self.b_c)
            # 计算单元状态向量, 其shape为:batch_size x hidden_size
            cell_state = F_gate * cell_state + I_gate * C_tilde
            # 计算隐状态向量,其shape为:batch_size x hidden_size
            hidden_state = O_gate * F.tanh(cell_state)

        return hidden_state
        

在这里插入图片描述

飞桨框架已经内置了LSTM的API paddle.nn.LSTM,其与自己实现的SRN不同点在于其实现时采用了两个偏置,同时矩阵相乘时参数在输入数据前面,如下公式所示:。

I t = σ ( W i i X t + b i i + U h i H t − 1 + b h i ) F t = σ ( W i f X t + b i f + U h f H t − 1 + b h f ) O t = σ ( W i o X t + b i o + U h o H t − 1 + b h o ) , \boldsymbol{I}_{t}=\sigma(\boldsymbol{W}_{ii}\boldsymbol{X}_t + \boldsymbol{b}_{ii} + \boldsymbol{U}_{hi}\boldsymbol{H}_{t-1}+\boldsymbol{b}_{hi}) \\ \boldsymbol{F}_{t}=\sigma(\boldsymbol{W}_{if}\boldsymbol{X}_t + \boldsymbol{b}_{if}+ \boldsymbol{U}_{hf}\boldsymbol{H}_{t-1}+\boldsymbol{b}_{hf}) \\ \boldsymbol{O}_{t}=\sigma(\boldsymbol{W}_{io}\boldsymbol{X}_t+ \boldsymbol{b}_{io} +\boldsymbol{U}_{ho}\boldsymbol{H}_{t-1}+\boldsymbol{b}_{ho}), It=σ(WiiXt+bii+UhiHt1+bhi)Ft=σ(WifXt+bif+UhfHt1+bhf)Ot=σ(WioXt+bio+UhoHt1+bho),

C ~ t = tanh ⁡ ( W i c X t + b i c + U h c H t − 1 + b h c ) , \tilde{\boldsymbol{C}}_{t}=\tanh(\boldsymbol{W}_{ic}\boldsymbol{X}_t+\boldsymbol{b}_{ic}+\boldsymbol{U}_{hc}\boldsymbol{H}_{t-1}+\boldsymbol{b}_{hc}) , C~t=tanh(WicXt+bic+UhcHt1+bhc),

C t = F t ⋅ C t − 1 + I t ⋅ C ~ t , H t = O t ⋅ tanh ( C t ) . \boldsymbol{C}_{t} = \boldsymbol F_t \cdot \boldsymbol{C}_{t-1} + \boldsymbol{I}_{t} \cdot \boldsymbol{\tilde{C}}_{t},\\ \boldsymbol{H}_{t} = \boldsymbol{O}_{t} \cdot \text{tanh}(\boldsymbol{C}_{t}). Ct=FtCt1+ItC~tHt=Ottanh(Ct).

其中 W ∗ ∈ R M × D , U ∗ ∈ R D × D , b i ∗ ∈ R 1 × D , b h ∗ ∈ R 1 × D \boldsymbol{W}_* \in \mathbb{R}^{M \times D}, \boldsymbol{U}_* \in \mathbb{R}^{D \times D}, \boldsymbol{b}_{i*} \in \mathbb{R}^{1 \times D}, \boldsymbol{b}_{h*} \in \mathbb{R}^{1 \times D} WRM×D,URD×D,biR1×D,bhR1×D是可学习参数。

另外,在Paddle内置LSTM实现时,对于参数 W i i , W i f , W i o , W i c \boldsymbol{W}_{ii}, \boldsymbol{W}_{if}, \boldsymbol{W}_{io}, \boldsymbol{W}_{ic} Wii,Wif,Wio,Wic ,并不是分别申请这些矩阵,而是申请了一个大的矩阵 W i h \boldsymbol{W}_{ih} Wih,将这个大的矩阵分割为4份,便可以得到 W i i , W i f , W i c , W i o \boldsymbol{W}_{ii}, \boldsymbol{W}_{if},\boldsymbol{W}_{ic},\boldsymbol{W}_{io} Wii,Wif,Wic,Wio。 同理,将会得到 W h h \boldsymbol{W}_{hh} Whh, b i h \boldsymbol{b}_{ih} bih b h h \boldsymbol{b}_{hh} bhh.

最后,Paddle内置LSTM API将会返回参数序列向量outputs和最后时刻的状态向量,其中序列向量outputs是指最后一层SRN的输出向量,其shape为[batch_size, seq_len, num_directions * hidden_size];最后时刻的状态向量是个元组,其包含了两个向量,分别是隐状态向量和单元状态向量,其shape均为[num_layers * num_directions, batch_size, hidden_size]。

这里我们可以将自己实现的SRN和Paddle框架内置的SRN返回的结果进行打印展示,实现代码如下。

# 这里创建一个随机数组作为测试数据,数据shape为batch_size x seq_len x input_size
batch_size, seq_len, input_size = 8, 20, 32
inputs = torch.randn(size=[batch_size, seq_len, input_size])

# 设置模型的hidden_size
hidden_size = 32
paddle_lstm = nn.LSTM(input_size, hidden_size)
self_lstm = LSTM(input_size, hidden_size)

self_hidden_state = self_lstm(inputs)
paddle_outputs, (paddle_hidden_state, paddle_cell_state) = paddle_lstm(inputs)

print("self_lstm hidden_state: ", self_hidden_state.shape)
print("paddle_lstm outpus:", paddle_outputs.shape)
print("paddle_lstm hidden_state:", paddle_hidden_state.shape)
print("paddle_lstm cell_state:", paddle_cell_state.shape)

NNDL 实验七 循环神经网络(3)LSTM的记忆能力实验_第1张图片

可以看到,自己实现的LSTM由于没有考虑多层因素,因此没有层次这个维度,因此其输出shape为[8, 32]。同时由于在以上代码使用Paddle内置API实例化LSTM时,默认定义的是1层的单向SRN,因此其shape为[1, 8, 32],同时隐状态向量为[8,20, 32].

接下来,我们可以将自己实现的LSTM与Paddle内置的LSTM在输出值的精度上进行对比,这里首先根据Paddle内置的LSTM实例化模型(为了进行对比,在实例化时只保留一个偏置,将偏置 b i h b_{ih} bih设置为0),然后提取该模型对应的参数,进行参数分割后,使用相应参数去初始化自己实现的LSTM,从而保证两者在参数初始化时是一致的。

在进行实验时,首先定义输入数据inputs,然后将该数据分别传入Paddle内置的LSTM与自己实现的LSTM模型中,最后通过对比两者的隐状态输出向量。代码实现如下:

torch.manual_seed(0)

# 这里创建一个随机数组作为测试数据,数据shape为batch_size x seq_len x input_size
batch_size, seq_len, input_size, hidden_size = 2, 5, 10, 10
inputs = torch.randn(size=[batch_size, seq_len, input_size])

# 设置模型的hidden_size
bih_attr = torch.nn.Parameter(torch.zeros([4*hidden_size, ]))
paddle_lstm = nn.LSTM(input_size, hidden_size,bias=True)

# 获取paddle_lstm中的参数,并设置相应的paramAttr,用于初始化lstm
print(paddle_lstm.weight_ih_l0.T.shape)
chunked_W = torch.split(paddle_lstm.weight_ih_l0.T, split_size_or_sections=10, dim=-1)
chunked_U = torch.split(paddle_lstm.weight_hh_l0.T, split_size_or_sections=10, dim=-1)
chunked_b = torch.split(paddle_lstm.bias_hh_l0.T, split_size_or_sections=10, dim=-1)

Wi_attr = torch.nn.Parameter(chunked_W[0])
Wf_attr = torch.nn.Parameter(chunked_W[1])
Wc_attr = torch.nn.Parameter(chunked_W[2])
Wo_attr = torch.nn.Parameter(chunked_W[3])
Ui_attr = torch.nn.Parameter(chunked_U[0])
Uf_attr = torch.nn.Parameter(chunked_U[1])
Uc_attr = torch.nn.Parameter(chunked_U[2])
Uo_attr = torch.nn.Parameter(chunked_U[3])
bi_attr = torch.nn.Parameter(chunked_b[0])
bf_attr = torch.nn.Parameter(chunked_b[1])
bc_attr = torch.nn.Parameter(chunked_b[2])
bo_attr = torch.nn.Parameter(chunked_b[3])
self_lstm = LSTM(input_size, hidden_size, Wi_attr=Wi_attr, Wf_attr=Wf_attr, Wo_attr=Wo_attr, Wc_attr=Wc_attr,
                 Ui_attr=Ui_attr, Uf_attr=Uf_attr, Uo_attr=Uo_attr, Uc_attr=Uc_attr,
                 bi_attr=bi_attr, bf_attr=bf_attr, bo_attr=bo_attr, bc_attr=bc_attr)

# 进行前向计算,获取隐状态向量,并打印展示
self_hidden_state = self_lstm(inputs)
paddle_outputs, (paddle_hidden_state, _) = paddle_lstm(inputs)
print("paddle SRN:\n", paddle_hidden_state.detach().numpy().squeeze(0))
print("self SRN:\n", self_hidden_state.detach().numpy())

NNDL 实验七 循环神经网络(3)LSTM的记忆能力实验_第2张图片
可以看到,两者的输出基本是一致的。另外,还可以进行对比两者在运算速度方面的差异。代码实现如下:

import time

# 这里创建一个随机数组作为测试数据,数据shape为batch_size x seq_len x input_size
batch_size, seq_len, input_size = 8, 20, 32
inputs = paddle.randn(shape=[batch_size, seq_len, input_size])

# 设置模型的hidden_size
hidden_size = 32
self_lstm = LSTM(input_size, hidden_size)
paddle_lstm = nn.LSTM(input_size, hidden_size)

# 计算自己实现的SRN运算速度
model_time = 0
for i in range(100):
    strat_time = time.time()
    hidden_state = self_lstm(inputs)
    # 预热10次运算,不计入最终速度统计
    if i < 10:
        continue
    end_time = time.time()
    model_time += (end_time - strat_time)
avg_model_time = model_time / 90
print('self_lstm speed:', avg_model_time, 's')

# 计算Paddle内置的SRN运算速度
model_time = 0
for i in range(100):
    strat_time = time.time()
    outputs, (hidden_state, cell_state) = paddle_lstm(inputs)
    # 预热10次运算,不计入最终速度统计
    if i < 10:
        continue
    end_time = time.time()
    model_time += (end_time - strat_time)
avg_model_time = model_time / 90
print('paddle_lstm speed:', avg_model_time, 's')

在这里插入图片描述

可以看到,由于Paddle框架的LSTM底层采用了C++实现并进行优化,Paddle框架内置的LSTM运行效率远远高于自己实现的LSTMa。

6.3.1.2 模型汇总

在本节实验中,我们将使用6.1.2.4的Model_RNN4SeqClass作为预测模型,不同在于在实例化时将传入实例化的LSTM层。

【思考题1】LSTM与SRN实验结果对比,谈谈看法。(选做)

LSTM随着数据集序列长度的增加,准确率并没有下降,而SRN随着序列的增加,在序列长度为20左右的时候准确率急速下降,这正是由于SRN和LSTM的区别。

(解释,在之前已经阐述过了)
LSTM相对于SRN,具有输入门、遗忘门、和输出门。
正是由于这些门的存在,才使得LSTM在序列较长的时候仍具有较高的准确率。
在这里插入图片描述
我们将其放大, c t − 1 c_{t-1} ct1也就是上一轮考试(高数)的记忆, h t − 1 h_{t-1} ht1相当于高数的内容,其中高数的内容和线代的输入经过遗忘门,也就是 f t f_{t} ft将其遗忘,就比如高数中的麦克劳林公式和泰勒公式在现代中是用不到的,所以我们需要选择性的将其进行遗忘,而更新门 i t i_{t} it的作用就是选择性的记忆,讲线代和高数中学习到的只是 c t c_{t} ct选择性的记忆,就比如每次新学一个学科就要学习它的历史,而在实际的考试中,我们是使用不到历史的,也就是所谓的重点不考,所以我们需要将其忘记,而最后的输出门则是进行答题了,并不是所有的学习了的都要考,只是老师出题的部分,所以就有输出门进行选择。同时不会从头到尾进行梯度传播,只是部分进行负责梯度传播,所以避免的梯度爆炸,和梯度消失。
而SRN并没有进行选择,及所有的信息,他都记忆下来,毕竟人的脑容量都是有限的,谁也记不住所有东西,不具有选择性,所以当需要记忆的东西很多的时候,人们往往效果较差。

6.3.2 模型训练

6.3.2.1 训练指定长度的数字预测模型

本节将基于RunnerV3类进行训练,首先定义模型训练的超参数,并保证和简单循环网络的超参数一致. 然后定义一个train函数,其可以通过指定长度的数据集,并进行训练. 在train函数中,首先加载长度为length的数据,然后实例化各项组件并创建对应的Runner,然后训练该Runner。同时在本节将使用4.5.4节定义的准确度(Accuracy)作为评估指标,代码实现如下:


# 训练轮次
num_epochs = 500
# 学习率
lr = 0.001
# 输入数字的类别数
num_digits = 10
# 将数字映射为向量的维度
input_size = 32
# 隐状态向量的维度
hidden_size = 32
# 预测数字的类别数
num_classes = 19
# 批大小 
batch_size = 8
# 模型保存目录
save_dir = "./"

# 可以设置不同的length进行不同长度数据的预测实验
def train(length):
    print(f"\n====> Training LSTM with data of length {length}.")
    np.random.seed(0)
    random.seed(0)
    torch.manual_seed(0)
    # 加载长度为length的数据
    data_path = f"./{length}"
    train_examples, dev_examples, test_examples = load_data(data_path)
    train_set, dev_set, test_set = DigitSumDataset(train_examples), DigitSumDataset(dev_examples), DigitSumDataset(test_examples)
    train_loader = torch.utils.data.DataLoader(train_set, batch_size=batch_size)
    dev_loader = torch.utils.data.DataLoader(dev_set, batch_size=batch_size)
    test_loader = torch.utils.data.DataLoader(test_set, batch_size=batch_size)
    # 实例化模型
    base_model = LSTM(input_size, hidden_size)
    model = Model_RNN4SeqClass(base_model, num_digits, input_size, hidden_size, num_classes) 
    # 指定优化器
    optimizer = torch.optim.Adam(lr=lr, params=model.parameters())
    # 定义评价指标
    metric = Accuracy()
    # 定义损失函数
    loss_fn = torch.nn.CrossEntropyLoss()
    # 基于以上组件,实例化Runner
    runner = RunnerV3(model, optimizer, loss_fn, metric)

    # 进行模型训练
    model_save_path = os.path.join(save_dir, f"best_lstm_model_{length}.pdparams")
    runner.train(train_loader, dev_loader, num_epochs=num_epochs, eval_steps=100, log_steps=100, save_path=model_save_path)

    return runner

NNDL 实验七 循环神经网络(3)LSTM的记忆能力实验_第3张图片

6.3.2.3 损失曲线展示

分别画出基于LSTM的各个长度的数字预测模型训练过程中,在训练集和验证集上的损失曲线,代码实现如下:

# 画出训练过程中的损失图
for length in lengths:
    runner = lstm_runners[length]
    fig_name = f"./6.11_{length}.pdf"
    plot_training_loss(runner, fig_name, sample_step=100)

NNDL 实验七 循环神经网络(3)LSTM的记忆能力实验_第4张图片

图6.11 LSTM在不同长度数据集训练损失变化图

图6.11展示了LSTM模型在不同长度数据集上进行训练后的损失变化,同SRN模型一样,随着序列长度的增加,训练集上的损失逐渐不稳定,验证集上的损失整体趋向于变大,这说明当序列长度增加时,保持长期依赖的能力同样在逐渐变弱. 同图6.5相比,LSTM模型在序列长度增加时,收敛情况比SRN模型更好。

6.3.3 模型评价

6.3.3.1 在测试集上进行模型评价

使用测试数据对在训练过程中保存的最好模型进行评价,观察模型在测试集上的准确率. 同时获取模型在训练过程中在验证集上最好的准确率,实现代码如下:

lstm_dev_scores = []
lstm_test_scores = []
for length in lengths:
    print(f"Evaluate LSTM with data length {length}.")
    runner = lstm_runners[length]
    # 加载训练过程中效果最好的模型
    model_path = os.path.join(save_dir, f"best_lstm_model_{length}.pdparams")
    runner.load_model(model_path)
    
    # 加载长度为length的数据
    data_path = f"./{length}"
    train_examples, dev_examples, test_examples = load_data(data_path)
    test_set = DigitSumDataset(test_examples)
    test_loader = torch.utils.data.DataLoader(test_set, batch_size=batch_size)

    # 使用测试集评价模型,获取测试集上的预测准确率
    score, _ = runner.evaluate(test_loader)
    lstm_test_scores.append(score)
    lstm_dev_scores.append(max(runner.dev_scores))

for length, dev_score, test_score in zip(lengths, lstm_dev_scores, lstm_test_scores):
    print(f"[LSTM] length:{length}, dev_score: {dev_score}, test_score: {test_score: .5f}")

NNDL 实验七 循环神经网络(3)LSTM的记忆能力实验_第5张图片

6.3.3.2 模型在不同长度的数据集上的准确率变化图

接下来,将SRN和LSTM在不同长度的验证集和测试集数据上的准确率绘制成图片,以方面观察。

import matplotlib.pyplot as plt

plt.plot(lengths, srn_dev_scores, '-o', color='#E20079',  label="SRN Dev Accuracy")
plt.plot(lengths, srn_test_scores,'-o', color='#946279', label="SRN Test Accuracy")
plt.plot(lengths, lstm_dev_scores, '-o', color='#8E004D',  label="LSTM Dev Accuracy")
plt.plot(lengths, lstm_test_scores,'-o', color='#3D3D3F', label="LSTM Test Accuracy")

#绘制坐标轴和图例
plt.ylabel("loss", fontsize='x-large')
plt.xlabel("step", fontsize='x-large')
plt.legend(loc='lower left')

fig_name = "./6.12.pdf"
plt.savefig(fig_name)
plt.show()

#NNDL 实验七 循环神经网络(3)LSTM的记忆能力实验_第6张图片

6.3.3.3 LSTM模型门状态和单元状态的变化

LSTM模型通过门控机制控制信息的单元状态的更新,这里可以观察当LSTM在处理一条数字序列的时候,相应门和单元状态是如何变化的。首先需要对以上LSTM模型实现代码中,定义相应列表进行存储这些门和单元状态在每个时刻的向量。

import torch
import torch.nn as nn
import torch.nn.functional as F
# 声明(LSTM)和相关参数
class LSTM(nn.Module):
    def __init__(self, input_size, hidden_size,
                 Wi_attr=torch.zeros(size=[input_size, hidden_size], dtype=torch.float32),
                 Wf_attr=torch.zeros(size=[input_size, hidden_size], dtype=torch.float32), 
                 Wo_attr=torch.zeros(size=[input_size, hidden_size], dtype=torch.float32), 
                 Wc_attr=torch.zeros(size=[input_size, hidden_size], dtype=torch.float32),
                 Ui_attr=torch.zeros(size=[input_size, hidden_size], dtype=torch.float32),
                 Uf_attr=torch.zeros(size=[input_size, hidden_size], dtype=torch.float32), 
                 Uo_attr=torch.zeros(size=[input_size, hidden_size], dtype=torch.float32), 
                 Uc_attr=torch.zeros(size=[input_size, hidden_size], dtype=torch.float32),
                 bi_attr=torch.zeros(size=[1, hidden_size], dtype=torch.float32),
                 bf_attr=torch.zeros(size=[1, hidden_size], dtype=torch.float32),
                 bo_attr=torch.zeros(size=[1, hidden_size], dtype=torch.float32), 
                 bc_attr=torch.zeros(size=[1, hidden_size], dtype=torch.float32)):
        super(LSTM, self).__init__()
        self.input_size = input_size
        self.hidden_size = hidden_size

        # 初始化模型参数
        self.W_i = torch.nn.Parameter(Wi_attr)
        self.W_f = torch.nn.Parameter(Wf_attr)
        self.W_o = torch.nn.Parameter(Wo_attr)
        self.W_c = torch.nn.Parameter(Wc_attr)
        self.U_i = torch.nn.Parameter(Ui_attr)
        self.U_f = torch.nn.Parameter(Uf_attr)
        self.U_o = torch.nn.Parameter(Uo_attr)
        self.U_c = torch.nn.Parameter(Uc_attr)
        self.b_i = torch.nn.Parameter(bi_attr)
        self.b_f = torch.nn.Parameter(bf_attr)
        self.b_o = torch.nn.Parameter(bo_attr)
        self.b_c = torch.nn.Parameter(bc_attr)
    
    # 初始化状态向量和隐状态向量
    def init_state(self, batch_size):
        hidden_state = torch.zeros(size=[batch_size, self.hidden_size], dtype=torch.float32)
        cell_state = torch.zeros(size=[batch_size, self.hidden_size], dtype=torch.float32)
        return hidden_state, cell_state


    # 定义前向计算
    def forward(self, inputs, states=None):
        # inputs: 输入数据,其shape为batch_size x seq_len x input_size
        batch_size, seq_len, input_size = inputs.shape

        # 初始化起始的单元状态和隐状态向量,其shape为batch_size x hidden_size
        if states is None:
            states = self.init_state(batch_size)
        hidden_state, cell_state = states

    
        # 定义相应的门状态和单元状态向量列表
        self.Is = []
        self.Fs = []
        self.Os = []
        self.Cs = []
        # 初始化状态向量和隐状态向量
        cell_state = torch.zeros(size=[batch_size, self.hidden_size], dtype=torch.float32)
        hidden_state = torch.zeros(size=[batch_size, self.hidden_size], dtype=torch.float32)

        # 执行LSTM计算,包括:隐藏门、输入门、遗忘门、候选状态向量、状态向量和隐状态向量
        for step in range(seq_len):
            input_step = inputs[:, step, :]
            I_gate = F.sigmoid(torch.matmul(input_step, self.W_i) + torch.matmul(hidden_state, self.U_i) + self.b_i)
            F_gate = F.sigmoid(torch.matmul(input_step, self.W_f) + torch.matmul(hidden_state, self.U_f) + self.b_f)
            O_gate = F.sigmoid(torch.matmul(input_step, self.W_o) + torch.matmul(hidden_state, self.U_o) + self.b_o)
            C_tilde = F.tanh(torch.matmul(input_step, self.W_c) + torch.matmul(hidden_state, self.U_c) + self.b_c)
            cell_state = F_gate * cell_state + I_gate * C_tilde
            hidden_state = O_gate * F.tanh(cell_state)
            # 存储门状态向量和单元状态向量
            self.Is.append(I_gate.detach().numpy().copy())
            self.Fs.append(F_gate.detach().numpy().copy())
            self.Os.append(O_gate.detach().numpy().copy())
            self.Cs.append(cell_state.detach().numpy().copy())
        return hidden_state
        # 实例化模型
base_model = LSTM(input_size, hidden_size)
model = Model_RNN4SeqClass(base_model, num_digits, input_size, hidden_size, num_classes) 
# 指定优化器
optimizer = torch.optim.Adam(lr=lr, params=model.parameters())
# 定义评价指标
metric = Accuracy()
# 定义损失函数
loss_fn = torch.nn.CrossEntropyLoss()
# 基于以上组件,重新实例化Runner
runner = RunnerV3(model, optimizer, loss_fn, metric)

length = 10
# 加载训练过程中效果最好的模型
model_path = os.path.join(save_dir, f"best_lstm_model_{length}.pdparams")
runner.load_model(model_path)
import seaborn as sns

def plot_tensor(inputs, tensor,  save_path, vmin=0, vmax=1):
    tensor = np.stack(tensor, axis=0)
    tensor = np.squeeze(tensor, 1).T

    plt.figure(figsize=(16,6))
    # vmin, vmax定义了色彩图的上下界
    ax = sns.heatmap(tensor, vmin=vmin, vmax=vmax) 
    ax.set_xticklabels(inputs)
    ax.figure.savefig(save_path)


# 定义模型输入
inputs = [6, 7, 0, 0, 1, 0, 0, 0, 0, 0]
X = torch.tensor(inputs.copy())
X = X.unsqueeze(0)
# 进行模型预测,并获取相应的预测结果
logits = runner.predict(X)
predict_label = torch.argmax(logits, dim=-1)
print(f"predict result: {predict_label.numpy()[0]}")

# 输入门
Is = runner.model.rnn_model.Is
plot_tensor(inputs, Is, save_path="./6.13_I.pdf")
# 遗忘门
Fs = runner.model.rnn_model.Fs
plot_tensor(inputs, Fs, save_path="./6.13_F.pdf")
# 输出门
Os = runner.model.rnn_model.Os
plot_tensor(inputs, Os, save_path="./6.13_O.pdf")
# 单元状态
Cs = runner.model.rnn_model.Cs
plot_tensor(inputs, Cs, save_path="./6.13_C.pdf", vmin=-5, vmax=5)

NNDL 实验七 循环神经网络(3)LSTM的记忆能力实验_第7张图片
NNDL 实验七 循环神经网络(3)LSTM的记忆能力实验_第8张图片
NNDL 实验七 循环神经网络(3)LSTM的记忆能力实验_第9张图片
NNDL 实验七 循环神经网络(3)LSTM的记忆能力实验_第10张图片

【思考题2】LSTM与SRN在不同长度数据集上的准确度对比,谈谈看法。(选做)

通过实验结果我们可以发现,SRN在长度为20左右的时候出现了问题,准确率大幅度下降,达到了大脑容量记忆的极限了。
LSTM准确率依旧维持在一个稳定的范围内,及LSTM比SRN更聪明一点,能够记住自己想要的东西。而不是全盘以及,撑死脑容量。

【思考题3】分析LSTM中单元状态和门数值的变化图,并用自己的话解释该图。

色阶图中,颜色的深浅代表着数值的大小,,输入门大小为0时,颜色差不多相近大小近似一致,表明对于0元素进行过滤,过滤掉不需要的信息,遗忘门为1的地方,数值变小,表示对不需要的信息进行遗忘,随着序列的输入,输出门和单元状态在某些维度上数值变小,在某些维度上数值变大,表示输出们对输出的信息进行选择,输出重要的信息。

全面总结RNN(必做)

NNDL 实验七 循环神经网络(3)LSTM的记忆能力实验_第11张图片

循环神经网络总结

从最初SRN简单循环神经网络,例如hello,通过h得出e一直到o。结果求导的时候,发现序列长度太长的话,要么导数爆炸,要么导数为无限小,总之求导不合适,结果人们就想到了梯度截断的方法,求导是解决了,但是由于其网络训练太过于麻烦,为了解决其长程依赖问题,发明了LSTM和GRU神经网络,更加符合人记忆的时候的大脑,果真时代都是随着人们的需求在发展的,例如其中的遗忘门,人们在成长的过程中,总是会忘掉一些不重要的东西,而通过实验我们也验证了梯度爆炸问题以及使用梯度截断的效果,和LSTM与SRN在不同长度训练下的效果,结果显示,LSTM并没有在长度变长的情况下,准确率大幅度下降,可以发现很好的解决了长程问题。

参考

NNDL实验六 下
长短期记忆神经网络(LSTM)介绍以及简单应用分析
自己的理论课作业

你可能感兴趣的:(rnn,lstm,深度学习)