HBU_神经网络与深度学习 实验13 循环神经网络:长短期记忆网络的记忆能力实验


一、长短期记忆网络(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 步时,循环单元的内部结构如图10所示.

图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模型的长程依赖能力。


1. 模型构建

在本实验中,我们将使用实验11 循环神经网络:记忆能力实验中定义的Model_RNN4SeqClass模型,并构建 LSTM 算子.只需要实例化 LSTM 算,并传入Model_RNN4SeqClass模型,就可以用 LSTM 进行数字求和实验

(1) LSTM层

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

import torch.nn.functional as F
import torch.nn as nn
import torch

# 声明LSTM和相关参数
class LSTM(nn.Module):
    def OutPut(self, input_size, hidden_size, attr):
        if attr == None:
            Value = torch.zeros(size=[input_size, hidden_size], dtype=torch.float32)
        else:
            Value = torch.tensor(attr, dtype=torch.float32)
        return Value

    def __init__(self, input_size, hidden_size, Wi_attr=None, Wf_attr=None, Wo_attr=None, Wc_attr=None,
                 Ui_attr=None, Uf_attr=None, Uo_attr=None, Uc_attr=None, bi_attr=None, bf_attr=None,
                 bo_attr=None, bc_attr=None):
        super(LSTM, self).__init__()
        self.input_size = input_size
        self.hidden_size = hidden_size

        # 初始化模型参数
        self.W_i = nn.Parameter(self.OutPut(input_size, hidden_size, Wi_attr))
        self.W_f = nn.Parameter(self.OutPut(input_size, hidden_size, Wf_attr))
        self.W_o = nn.Parameter(self.OutPut(input_size, hidden_size, Wo_attr))
        self.W_c = nn.Parameter(self.OutPut(input_size, hidden_size, Wc_attr))
        self.U_i = nn.Parameter(self.OutPut(hidden_size, hidden_size, Ui_attr))
        self.U_f = nn.Parameter(self.OutPut(hidden_size, hidden_size, Uf_attr))
        self.U_o = nn.Parameter(self.OutPut(hidden_size, hidden_size, Uo_attr))
        self.U_c = nn.Parameter(self.OutPut(hidden_size, hidden_size, Uc_attr))
        self.b_i = nn.Parameter(self.OutPut(1, hidden_size, bi_attr))
        self.b_f = nn.Parameter(self.OutPut(1, hidden_size, bf_attr))
        self.b_o = nn.Parameter(self.OutPut(1, hidden_size, bo_attr))
        self.b_c = nn.Parameter(self.OutPut(1, hidden_size, 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
Wi_attr = nn.Parameter(torch.tensor([[0.1, 0.2], [0.1, 0.2]]))
Wf_attr = nn.Parameter(torch.tensor([[0.1, 0.2], [0.1, 0.2]]))
Wo_attr = nn.Parameter(torch.tensor([[0.1, 0.2], [0.1, 0.2]]))
Wc_attr = nn.Parameter(torch.tensor([[0.1, 0.2], [0.1, 0.2]]))
Ui_attr = nn.Parameter(torch.tensor([[0.0, 0.1], [0.1, 0.0]]))
Uf_attr = nn.Parameter(torch.tensor([[0.0, 0.1], [0.1, 0.0]]))
Uo_attr = nn.Parameter(torch.tensor([[0.0, 0.1], [0.1, 0.0]]))
Uc_attr = nn.Parameter(torch.tensor([[0.0, 0.1], [0.1, 0.0]]))
bi_attr = nn.Parameter(torch.tensor([[0.1, 0.1]]))
bf_attr = nn.Parameter(torch.tensor([[0.1, 0.1]]))
bo_attr = nn.Parameter(torch.tensor([[0.1, 0.1]]))
bc_attr = nn.Parameter(torch.tensor([[0.1, 0.1]]))

lstm = LSTM(2, 2, 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)

inputs = torch.tensor([[[1, 0]]], dtype=torch.float32)
hidden_state = lstm(inputs)
print(hidden_state)

代码执行结果:

tensor([[0.0594, 0.0952]], grad_fn=<MulBackward0>)

PyTorch已经内置了LSTM的API torch.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是可学习参数。

这里我们可以将自己实现的SRN和PyTorch内置的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
torch_lstm = nn.LSTM(input_size, hidden_size)
self_lstm = LSTM(input_size, hidden_size)

self_hidden_state = self_lstm(inputs)
torch_outputs, (torch_hidden_state, torch_cell_state) = torch_lstm(inputs)

print("self_lstm hidden_state: ", self_hidden_state.shape)
print("torch_lstm outpus:", torch_outputs.shape)
print("torch_lstm hidden_state:", torch_hidden_state.shape)
print("torch_lstm cell_state:", torch_cell_state.shape)

代码执行结果:

self_lstm hidden_state:  torch.Size([8, 32])
torch_lstm outpus: torch.Size([8, 20, 32])
torch_lstm hidden_state: torch.Size([1, 20, 32])
torch_lstm cell_state: torch.Size([1, 20, 32])

可以看到,自己实现的LSTM由于没有考虑多层因素,因此没有层次这个维度,因此其输出shape为[8, 32].

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

在进行实验时,首先定义输入数据inputs,然后将该数据分别传入PyTorch内置的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 = nn.Parameter(torch.zeros([4 * hidden_size, ]))
torch_lstm = nn.LSTM(input_size, hidden_size, bias=True)

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

Wi_attr = nn.Parameter(chunked_W[0])
Wf_attr = nn.Parameter(chunked_W[1])
Wc_attr = nn.Parameter(chunked_W[2])
Wo_attr = nn.Parameter(chunked_W[3])
Ui_attr = nn.Parameter(chunked_U[0])
Uf_attr = nn.Parameter(chunked_U[1])
Uc_attr = nn.Parameter(chunked_U[2])
Uo_attr = nn.Parameter(chunked_U[3])
bi_attr = nn.Parameter(chunked_b[0])
bf_attr = nn.Parameter(chunked_b[1])
bc_attr = nn.Parameter(chunked_b[2])
bo_attr = 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)
torch_outputs, (torch_hidden_state, _) = torch_lstm(inputs)
print("torch SRN:\n", torch_hidden_state.detach().numpy().squeeze(0))
print("self SRN:\n", self_hidden_state.detach().numpy())

代码执行结果:

torch.Size([10, 40])
torch SRN:
 [[ 0.05112648  0.0069804  -0.03931074  0.08884123  0.1154766  -0.13408035
   0.16033086  0.00135597 -0.063761   -0.2974773 ]
 [ 0.11241535  0.07274596  0.36305282 -0.06277131  0.01287347 -0.15761302
   0.22385652  0.01972566 -0.35233897 -0.20609131]
 [ 0.13069034 -0.03020173 -0.06369952  0.13535677  0.34181935 -0.11440603
   0.10832833  0.04234035  0.08991402 -0.15160468]
 [ 0.0727646   0.15715013  0.06807105  0.07414021  0.3629469  -0.06236503
  -0.11784356  0.00420525 -0.1500205   0.08434851]
 [ 0.07962178  0.01809997 -0.02799227 -0.0978313  -0.08596172 -0.13848482
   0.06129254  0.15295173 -0.14451738 -0.11927365]]
self SRN:
 [[ 0.09405641 -0.14659543 -0.14954016  0.20936163 -0.12826967  0.14749622
   0.00946942  0.19934718 -0.06859785 -0.2767597 ]
 [ 0.17217153  0.16705877 -0.05719085  0.14882168  0.10330293 -0.20432511
   0.13150845  0.03508793 -0.07331903 -0.06966008]]

可以看到,两者的输出基本是一致的。另外,还可以进行对比两者在运算速度方面的差异。代码实现如下:

import time

# 这里创建一个随机数组作为测试数据,数据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
self_lstm = LSTM(input_size, hidden_size)
torch_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')

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

代码执行结果:

self_lstm speed: 0.004340566529168023 s
torch_lstm speed: 0.0008681032392713759 s

可以看到,PyTorch内置的LSTM运行效率远远高于自己实现的LSTM。

(2) 模型汇总

在本节实验中,我们将使用实验11 循环神经网络:记忆能力实验中的Model_RNN4SeqClass作为预测模型,不同在于在实例化时将传入实例化的LSTM层。


2. 模型训练

(1) 训练指定长度的数字预测模型

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

import os
import random
import numpy as np
import torch.utils.data as io

from torch.utils.data import Dataset

def load_data(data_path):
    # 加载训练集
    train_examples = []
    train_path = os.path.join(data_path, "train.txt")
    with open(train_path, "r", encoding="utf-8") as f:
        for line in f.readlines():
            # 解析一行数据,将其处理为数字序列seq和标签label
            items = line.strip().split("\t")
            seq = [int(i) for i in items[0].split(" ")]
            label = int(items[1])
            train_examples.append((seq, label))

    # 加载验证集
    dev_examples = []
    dev_path = os.path.join(data_path, "dev.txt")
    with open(dev_path, "r", encoding="utf-8") as f:
        for line in f.readlines():
            # 解析一行数据,将其处理为数字序列seq和标签label
            items = line.strip().split("\t")
            seq = [int(i) for i in items[0].split(" ")]
            label = int(items[1])
            dev_examples.append((seq, label))

    # 加载测试集
    test_examples = []
    test_path = os.path.join(data_path, "test.txt")
    with open(test_path, "r", encoding="utf-8") as f:
        for line in f.readlines():
            # 解析一行数据,将其处理为数字序列seq和标签label
            items = line.strip().split("\t")
            seq = [int(i) for i in items[0].split(" ")]
            label = int(items[1])
            test_examples.append((seq, label))

    return train_examples, dev_examples, test_examples

class DigitSumDataset(Dataset):
    def __init__(self, data):
        self.data = data

    def __getitem__(self, idx):
        example = self.data[idx]
        seq = torch.tensor(example[0], dtype=torch.int64)
        label = torch.tensor(example[1], dtype=torch.int64)
        return seq, label

    def __len__(self):
        return len(self.data)

class Embedding(nn.Module):
    def __init__(self, num_embeddings, embedding_dim):
        super(Embedding, self).__init__()
        W_attr = nn.init.xavier_uniform_(
            torch.as_tensor(torch.randn([num_embeddings, embedding_dim]), dtype=torch.float32), gain=1.0)
        self.W = nn.Parameter(W_attr)

    def forward(self, inputs):
        # 根据索引获取对应词向量
        embs = self.W[inputs]
        return embs

class Model_RNN4SeqClass(nn.Module):
    def __init__(self, model, num_digits, input_size, hidden_size, num_classes):
        super(Model_RNN4SeqClass, self).__init__()
        # 传入实例化的RNN层,例如SRN
        self.rnn_model = model
        # 词典大小
        self.num_digits = num_digits
        # 嵌入向量的维度
        self.input_size = input_size
        # 定义Embedding层
        self.embedding = Embedding(num_digits, input_size)
        # 定义线性层
        self.linear = nn.Linear(hidden_size, num_classes)

    def forward(self, inputs):
        # 将数字序列映射为相应向量
        inputs_emb = self.embedding(inputs)
        # 调用RNN模型
        hidden_state = self.rnn_model(inputs_emb)
        # 使用最后一个时刻的状态进行数字预测
        logits = self.linear(hidden_state)
        return logits

class Accuracy():
    def __init__(self, is_logist=True):
        """
        输入:
           - is_logist: outputs是logits还是激活后的值
        """

        # 用于统计正确的样本个数
        self.num_correct = 0
        # 用于统计样本的总数
        self.num_count = 0

        self.is_logits = is_logist

    def update(self, outputs, labels):
        """
        输入:
           - outputs: 预测值, shape=[N,class_num]
           - labels: 标签值, shape=[N,1]
        """

        # 判断是二分类任务还是多分类任务,shape[1]=1时为二分类任务,shape[1]>1时为多分类任务
        if outputs.shape[1] == 1:
            if self.is_logist:
                # logits判断是否大于0
                preds = torch.tensor((outputs >= 0), dtype=torch.float32)
            else:
                # 如果不是logits,判断每个概率值是否大于0.5,当大于0.5时,类别为1,否则类别为0
                preds = torch.tensor((outputs >= 0.5), dtype=torch.float32)
        else:
            # 多分类时,使用'torch.argmax'计算最大元素索引作为类别
            preds = torch.argmax(outputs, dim=1)

        # 获取本批数据中预测正确的样本个数
        labels = torch.squeeze(labels, dim=-1)
        batch_correct = torch.sum(torch.tensor(preds == labels, dtype=torch.float32)).numpy()
        batch_count = len(labels)

        # 更新num_correct 和 num_count
        self.num_correct += batch_correct
        self.num_count += batch_count

    def accumulate(self):
        # 使用累计的数据,计算总的指标
        if self.num_count == 0:
            return 0
        return self.num_correct / self.num_count

    def reset(self):
        self.num_correct = 0
        self.num_count = 0

    def name(self):
        return "Accuracy"

class RunnerV3(object):
    def __init__(self, model, optimizer, loss_fn, metric, **kwargs):
        self.model = model
        self.optimizer = optimizer
        self.loss_fn = loss_fn
        self.metric = metric  # 只用于计算评价指标

        # 记录训练过程中的评价指标变化情况
        self.dev_scores = []

        # 记录训练过程中的损失函数变化情况
        self.train_epoch_losses = []  # 一个epoch记录一次loss
        self.train_step_losses = []  # 一个step记录一次loss
        self.dev_losses = []

        # 记录全局最优指标
        self.best_score = 0

    def train(self, train_loader, dev_loader=None, **kwargs):
        # 将模型切换为训练模式
        self.model.train()

        # 传入训练轮数,如果没有传入值则默认为0
        num_epochs = kwargs.get("num_epochs", 0)
        # 传入log打印频率,如果没有传入值则默认为100
        log_steps = kwargs.get("log_steps", 100)
        # 评价频率
        eval_steps = kwargs.get("eval_steps", 0)

        # 传入模型保存路径,如果没有传入值则默认为"best_model.pdparams"
        save_path = kwargs.get("save_path", "best_model.pdparams")

        custom_print_log = kwargs.get("custom_print_log", None)

        # 训练总的步数
        num_training_steps = num_epochs * len(train_loader)

        if eval_steps:
            if self.metric is None:
                raise RuntimeError('Error: Metric can not be None!')
            if dev_loader is None:
                raise RuntimeError('Error: dev_loader can not be None!')

        # 运行的step数目
        global_step = 0

        # 进行num_epochs轮训练
        for epoch in range(num_epochs):
            # 用于统计训练集的损失
            total_loss = 0
            for step, data in enumerate(train_loader):
                X, y = data
                # 获取模型预测
                logits = self.model(X)
                loss = self.loss_fn(logits, y)  # 默认求mean
                total_loss += loss

                # 训练过程中,每个step的loss进行保存
                self.train_step_losses.append((global_step, loss.item()))

                if log_steps and global_step % log_steps == 0:
                    print(
                        f"[Train] epoch: {epoch}/{num_epochs}, step: {global_step}/{num_training_steps}, loss: {loss.item():.5f}")

                # 梯度反向传播,计算每个参数的梯度值
                loss.backward()

                if custom_print_log:
                    custom_print_log(self)

                # 小批量梯度下降进行参数更新
                self.optimizer.step()
                # 梯度归零
                self.optimizer.zero_grad()

                # 判断是否需要评价
                if eval_steps > 0 and global_step != 0 and \
                        (global_step % eval_steps == 0 or global_step == (num_training_steps - 1)):

                    dev_score, dev_loss = self.evaluate(dev_loader, global_step=global_step)
                    print(f"[Evaluate]  dev score: {dev_score:.5f}, dev loss: {dev_loss:.5f}")

                    # 将模型切换为训练模式
                    self.model.train()

                    # 如果当前指标为最优指标,保存该模型
                    if dev_score > self.best_score:
                        self.save_model(save_path)
                        print(
                            f"[Evaluate] best accuracy performence has been updated: {self.best_score:.5f} --> {dev_score:.5f}")
                        self.best_score = dev_score

                global_step += 1

            # 当前epoch 训练loss累计值
            trn_loss = (total_loss / len(train_loader)).item()
            # epoch粒度的训练loss保存
            self.train_epoch_losses.append(trn_loss)

        print("[Train] Training done!")

    # 模型评估阶段,使用'torch.no_grad()'控制不计算和存储梯度
    @torch.no_grad()
    def evaluate(self, dev_loader, **kwargs):
        assert self.metric is not None

        # 将模型设置为评估模式
        self.model.eval()

        global_step = kwargs.get("global_step", -1)

        # 用于统计训练集的损失
        total_loss = 0

        # 重置评价
        self.metric.reset()

        # 遍历验证集每个批次
        for batch_id, data in enumerate(dev_loader):
            X, y = data

            # 计算模型输出
            logits = self.model(X)

            # 计算损失函数
            loss = self.loss_fn(logits, y).item()
            # 累积损失
            total_loss += loss

            # 累积评价
            self.metric.update(logits, y)

        dev_loss = (total_loss / len(dev_loader))
        self.dev_losses.append((global_step, dev_loss))

        dev_score = self.metric.accumulate()
        self.dev_scores.append(dev_score)

        return dev_score, dev_loss

    # 模型评估阶段,使用'torch.no_grad()'控制不计算和存储梯度
    @torch.no_grad()
    def predict(self, x, **kwargs):
        # 将模型设置为评估模式
        self.model.eval()
        # 运行模型前向计算,得到预测值
        logits = self.model(x)
        return logits

    def save_model(self, save_path):
        torch.save(self.model.state_dict(), save_path)

    def load_model(self, model_path):
        model_state_dict = torch.load(model_path)
        self.model.load_state_dict(model_state_dict)

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

# 可以设置不同的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"./datasets/{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 = io.DataLoader(train_set, batch_size=batch_size)
    dev_loader = io.DataLoader(dev_set, batch_size=batch_size)
    test_loader = io.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

(2) 多组训练

接下来,分别进行数据长度为10, 15, 20, 25, 30, 35的数字预测模型训练实验,训练后的runner保存至runners字典中。

lstm_runners = {}

lengths = [10, 15, 20, 25, 30, 35]
for length in lengths:
    runner = train(length)
    lstm_runners[length] = runner

代码执行结果:

(3) 损失曲线展示

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

import matplotlib.pyplot as plt

def plot_training_loss(runner, fig_name, sample_step):
    plt.figure()
    train_items = runner.train_step_losses[::sample_step]
    train_steps = [x[0] for x in train_items]
    train_losses = [x[1] for x in train_items]
    plt.plot(train_steps, train_losses, color='#e4007f', label="Train loss")

    dev_steps = [x[0] for x in runner.dev_losses]
    dev_losses = [x[1] for x in runner.dev_losses]
    plt.plot(dev_steps, dev_losses, color='#f19ec2', linestyle='--', label="Dev loss")

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

    plt.savefig(fig_name)
    plt.show()

for length in lengths:
    runner = lstm_runners[length]
    fig_name = f"./images/6.11_{length}.pdf"
    plot_training_loss(runner, fig_name, sample_step=100)

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





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


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"./datasets/{length}"
    train_examples, dev_examples, test_examples = load_data(data_path)
    test_set = DigitSumDataset(test_examples)
    test_loader = io.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}")

代码执行结果:

Evaluate LSTM with data length 10.
Evaluate LSTM with data length 15.
Evaluate LSTM with data length 20.
Evaluate LSTM with data length 25.
Evaluate LSTM with data length 30.
Evaluate LSTM with data length 35.
[LSTM] length:10, dev_score: 0.9, test_score:  0.90000
[LSTM] length:15, dev_score: 0.92, test_score:  0.92000
[LSTM] length:20, dev_score: 0.85, test_score:  0.85000
[LSTM] length:25, dev_score: 0.25, test_score:  0.25000
[LSTM] length:30, dev_score: 0.76, test_score:  0.72000
[LSTM] length:35, dev_score: 0.23, test_score:  0.14000

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

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

plt.plot(lengths, lstm_dev_scores, '-o', color='#e8609b',  label="LSTM Dev Accuracy")
plt.plot(lengths, lstm_test_scores,'-o', color='#000000', label="LSTM Test Accuracy")

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

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

下列图像展示了LSTM模型与SRN模型在不同长度数据集上的准确度。随着数据集长度的增加,LSTM模型在验证集和测试集上的准确率整体也趋向于降低;同时LSTM模型的准确率高于SRN模型,表明LSTM模型保持长期依赖的能力要优于SRN模型.

LSTM与SRN网络在不同长度数据集上的准确度对比图

(3) LSTM模型门状态和单元状态的变化

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

# 声明LSTM和相关参数
class LSTM(nn.Module):
    def __init__(self, input_size, hidden_size):
        super(LSTM, self).__init__()
        self.input_size = input_size
        self.hidden_size = hidden_size
        # 初始化模型参数
        self.W_i = nn.Parameter(torch.zeros(size=[input_size, hidden_size], dtype=torch.float32))
        self.W_f = nn.Parameter(torch.zeros(size=[input_size, hidden_size], dtype=torch.float32))
        self.W_o = nn.Parameter(torch.zeros(size=[input_size, hidden_size], dtype=torch.float32))
        self.W_c = nn.Parameter(torch.zeros(size=[input_size, hidden_size], dtype=torch.float32))
        self.U_i = nn.Parameter(torch.zeros(size=[hidden_size, hidden_size], dtype=torch.float32))
        self.U_f = nn.Parameter(torch.zeros(size=[hidden_size, hidden_size], dtype=torch.float32))
        self.U_o = nn.Parameter(torch.zeros(size=[hidden_size, hidden_size], dtype=torch.float32))
        self.U_c = nn.Parameter(torch.zeros(size=[hidden_size, hidden_size], dtype=torch.float32))
        self.b_i = nn.Parameter(torch.zeros(size=[1, hidden_size], dtype=torch.float32))
        self.b_f = nn.Parameter(torch.zeros(size=[1, hidden_size], dtype=torch.float32))
        self.b_o = nn.Parameter(torch.zeros(size=[1, hidden_size], dtype=torch.float32))
        self.b_c = nn.Parameter(torch.zeros(size=[1, hidden_size], dtype=torch.float32))

    # 初始化状态向量和隐状态向量
    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):
        batch_size, seq_len, input_size = inputs.shape  # inputs batch_size x seq_len x input_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  

接下来,需要使用新的LSTM模型,重新实例化一个runner,本节使用序列长度为10的模型进行此项实验,因此需要加载序列长度为10的模型。

# 实例化模型
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="./images/6.13_I.pdf")
# 遗忘门
Fs = runner.model.rnn_model.Fs
plot_tensor(inputs, Fs, save_path="./images/6.13_F.pdf")
# 输出门
Os = runner.model.rnn_model.Os
plot_tensor(inputs, Os, save_path="./images/6.13_O.pdf")
# 单元状态
Cs = runner.model.rnn_model.Cs
plot_tensor(inputs, Cs, save_path="./images/6.13_C.pdf", vmin=-5, vmax=5)

代码执行结果:

predict result: 13

下列图像展示了当LSTM处理序列数据[6, 7, 0, 0, 1, 0, 0, 0, 0, 0]的过程中单元状态和门数值的变化图,其中横坐标为输入数字,纵坐标为相应门或单元状态向量的维度,颜色的深浅代表数值的大小。可以看到,当输入门遇到不同位置的数字0时,保持了相对一致的数值大小,表明对于0元素保持相同的门控过滤机制,避免输入信息的变化给当前模型带来困扰;当遗忘门遇到数字1后,遗忘门数值在一些维度上变小,表明对某些信息进行了遗忘;随着序列的输入,输出门和单元状态在某些维度上数值变小,在某些维度上数值变大,表明输出门在根据信息的重要性选择信息进行输出,同时单元状态也在保持着对文本预测重要的一些信息.



LSTM中单元状态和门数值的变化图


二、实验Q&A

EX1

对比LSTM与SRN实验结果,谈谈看法。

实验11 循环神经网络:记忆能力实验 一、3. (3) 损失曲线展示
显然,长短期记忆网络在Dev Loss上的准确率要明显优于循环神经网络,在Train Loss上的整体趋势表现也比循环神经网络要好很多。猜测造成上述情况的原因是循环神经网络中老生常谈的长程依赖问题

EX2

对比LSTM与SRN在不同长度数据集上的准确度,谈谈看法。

两者整体均呈下降趋势,在长程依赖问题仍严重影响数据集长度为25时LSTM的训练结果的情况下,相对应的准确率也比SRN的要高。需要注意的是,由于LSTM参数多,在数据集长度较小的时候,LSTM的准确率要比SRN的高,随着数据集长度的变化,LSTM的准确率和SRN的差距将逐渐减小,最终被SRN所反超。

EX3

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

遗忘门、输入门和输出门用来控制网络前面的遗忘了多少,现在输入了多少,单元状态又要输出多少。数值变化图的横向是序列,纵向是每个序列不同门的向量,向量不同的颜色代表了取值的大小,可以看出6、7序列的取值要和后面明显不同,而后面的则相差不大,有不同就是对前面序列一些维度的遗忘的变化。

EX4

总结循环神经网络。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值