HBU-NNDL 实验七 循环神经网络(1)RNN记忆能力实验

目录

第6章 循环神经网络

6.1 循环神经网络的记忆能力实验

6.1.1 数据集构建

6.1.1.1 数据集的构建函数

6.1.1.2 加载数据并进行数据划分

6.1.1.3 构造Dataset类 

6.1.2 模型构建

6.1.2.1 嵌入层

6.1.2.2 SRN层

6.1.2.3 线性层 

6.1.2.4 模型汇总

6.1.3 模型训练

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

6.1.3.2 多组训练

6.1.3.3 损失曲线展示

6.1.4 模型评价

参考《神经网络与深度学习》中的公式(6.50),改进SRN的循环单元,加入隐状态之间的残差连接,并重复数字求和实验。观察是否可以缓解长程依赖问题

心得体会


第6章 循环神经网络

循环神经网络(Recurrent Neural Network,RNN)是一类具有短期记忆能力的神经网络.在循环神经网络中,神经元不但可以接受其他神经元的信息,也可以接受自身的信息,形成具有环路的网络结构.和前馈神经网络相比,循环神经网络更加符合生物神经网络的结构.目前,循环神经网络已经被广泛应用在语音识别、语言模型以及自然语言生成等任务上.

6.1 循环神经网络的记忆能力实验

循环神经网络的一种简单实现是简单循环网络(Simple Recurrent Network,SRN).

令向量x_t\in \mathbb{R}^M表示在时刻tt时网络的输入,h_t\in \mathbb{R}^D 表示隐藏层状态(即隐藏层神经元活性值),则h_t不仅和当前时刻的输入x_t相关,也和上一个时刻的隐藏层状态h_{t-1}相关. 简单循环网络在时刻t的更新公式为

h_t=f(Wx_t+Uh_t+b),h_t=f(Wx_t+Uh_{t-1}+b)

其中h_t为隐状态向量,U\in \mathbb{R}^{D\times D}状态-状态权重矩阵,W\in R^{D\times M}状态-输入权重矩阵,b\in \mathbb{R}^D为偏置向量。

HBU-NNDL 实验七 循环神经网络(1)RNN记忆能力实验_第1张图片

 

6.1.1 数据集构建

我们首先构建不同长度的数字预测数据集DigitSum.

6.1.1.1 数据集的构建函数

由于在本任务中,输入序列的前两位数字为 0 − 9,其组合数是固定的,所以可以穷举所有的前两位数字组合,并在后面默认用0填充到固定长度. 但考虑到数据的多样性,这里对生成的数字序列中的零位置进行随机采样,并将其随机替换成0-9的数字以增加样本的数量.

我们可以通过设置kk的数值来指定一条样本随机生成的数字序列数量.当生成某个指定长度的数据集时,会同时生成训练集、验证集和测试集。当k=3时,生成训练集。当k=1时,生成验证集和测试集. 代码实现如下:

import random
import numpy as np

# 固定随机种子
random.seed(0)
np.random.seed(0)


def generate_data(length, k, save_path):
    if length < 3:
        raise ValueError("The length of data should be greater than 2.")
    if k == 0:
        raise ValueError("k should be greater than 0.")
    # 生成100条长度为length的数字序列,除前两个字符外,序列其余数字暂用0填充
    base_examples = []
    for n1 in range(0, 10):
        for n2 in range(0, 10):
            seq = [n1, n2] + [0] * (length - 2)
            label = n1 + n2
            base_examples.append((seq, label))

    examples = []
    # 数据增强:对base_examples中的每条数据,默认生成k条数据,放入examples
    for base_example in base_examples:
        for _ in range(k):
            # 随机生成替换的元素位置和元素
            idx = np.random.randint(2, length)
            val = np.random.randint(0, 10)
            # 对序列中的对应零元素进行替换
            seq = base_example[0].copy()
            label = base_example[1]
            seq[idx] = val
            examples.append((seq, label))

    # 保存增强后的数据
    with open(save_path, "w", encoding="utf-8") as f:
        for example in examples:
            # 将数据转为字符串类型,方便保存
            seq = [str(e) for e in example[0]]
            label = str(example[1])
            line = " ".join(seq) + "\t" + label + "\n"
            f.write(line)

    print(f"generate data to: {save_path}.")


# 定义生成的数字序列长度
lengths = [5, 10, 15, 20, 25, 30, 35]
for length in lengths:
    # 生成长度为length的训练数据
    save_path = f"./datasets/{length}/train.txt"
    k = 3
    generate_data(length, k, save_path)
    # 生成长度为length的验证数据
    save_path = f"./datasets/{length}/dev.txt"
    k = 1
    generate_data(length, k, save_path)
    # 生成长度为length的测试数据
    save_path = f"./datasets/{length}/test.txt"
    k = 1
    generate_data(length, k, save_path)

6.1.1.2 加载数据并进行数据划分

为方便使用,本实验提前生成了长度分别为5、10、 15、20、25、30和35的7份数据,存放于“./datasets”目录下,读者可以直接加载使用。代码实现如下:

import os
# 加载数据
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

# 设定加载的数据集的长度
length = 5
# 该长度的数据集的存放目录
data_path = f"./datasets/{length}"
# 加载该数据集
train_examples, dev_examples, test_examples = load_data(data_path)
print("dev example:", dev_examples[:2])
print("训练集数量:", len(train_examples))
print("验证集数量:", len(dev_examples))
print("测试集数量:", len(test_examples))


dev example: [([0, 0, 6, 0, 0], 0), ([0, 1, 0, 0, 8], 1)]
训练集数量: 300
验证集数量: 100
测试集数量: 100

6.1.1.3 构造Dataset类 

为了方便使用梯度下降法进行优化,我们构造了DigitSum数据集的Dataset类,函数__getitem__负责根据索引读取数据,并将数据转换为张量。代码实现如下:

from torch.utils.data import Dataset
import torch

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)

6.1.2 模型构建

使用SRN模型进行数字加和任务的模型结构为如图6.4所示.

HBU-NNDL 实验七 循环神经网络(1)RNN记忆能力实验_第2张图片

整个模型由以下几个部分组成:
(1) 嵌入层:将输入的数字序列进行向量化,即将每个数字映射为向量;
(2) SRN 层:接收向量序列,更新循环单元,将最后时刻的隐状态作为整个序列的表示;
(3) 输出层:一个线性层,输出分类的结果. 

6.1.2.1 嵌入层

本任务输入的样本是数字序列,为了更好地表示数字,需要将数字映射为一个嵌入(Embedding)向量。嵌入向量中的每个维度均能用来刻画该数字本身的某种特性。由于向量能够表达该数字更多的信息,利用向量进行数字求和任务,可以使得模型具有更强的拟合能力。

基于索引方式的嵌入层的实现如下:

import torch.nn as nn
import torch

class Embedding(nn.Module):
    def __init__(self, num_embeddings, embedding_dim):
        super(Embedding, self).__init__()
        self.W = nn.init.xavier_uniform_(torch.empty(num_embeddings, embedding_dim),gain=1.0)

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


emb_layer = Embedding(10, 5)
inputs = torch.tensor([0, 1, 2, 3])
emb_layer(inputs)

6.1.2.2 SRN层

简单循环网络的代码实现如下:

import torch
import torch.nn as nn
import torch.nn.functional as F
torch.manual_seed(0)

# SRN模型
class SRN(nn.Module):
    def __init__(self, input_size,  hidden_size, W_attr=None, U_attr=None, b_attr=None):
        super(SRN, self).__init__()
        # 嵌入向量的维度
        self.input_size = input_size
        # 隐状态的维度
        self.hidden_size = hidden_size
        # 定义模型参数W,其shape为 input_size x hidden_size
        if W_attr==None:
            W=torch.zeros(size=[input_size, hidden_size], dtype=torch.float32)
        else:
            W=torch.tensor(W_attr,dtype=torch.float32)
        self.W = torch.nn.Parameter(W)
        # 定义模型参数U,其shape为hidden_size x hidden_size
        if U_attr==None:
            U=torch.zeros(size=[hidden_size, hidden_size], dtype=torch.float32)
        else:
            U=torch.tensor(U_attr,dtype=torch.float32)
        self.U = torch.nn.Parameter(U)
        # 定义模型参数b,其shape为 1 x hidden_size
        if b_attr==None:
            b=torch.zeros(size=[1, hidden_size], dtype=torch.float32)
        else:
            b=torch.tensor(b_attr,dtype=torch.float32)
        self.b = torch.nn.Parameter(b)

    # 初始化向量
    def init_state(self, batch_size):
        hidden_state = torch.zeros(size=[batch_size, self.hidden_size], dtype=torch.float32)
        return hidden_state

    # 定义前向计算
    def forward(self, inputs, hidden_state=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 hidden_state is None:
            hidden_state = self.init_state(batch_size)

        # 循环执行RNN计算
        for step in range(seq_len):
            # 获取当前时刻的输入数据step_input, 其shape为 batch_size x input_size
            step_input = inputs[:, step, :]
            # 获取当前时刻的隐状态向量hidden_state, 其shape为 batch_size x hidden_size
            hidden_state = F.tanh(torch.matmul(step_input, self.W) + torch.matmul(hidden_state, self.U) + self.b)
        return hidden_state



## 初始化参数并运行
U_attr = [[0.0, 0.1], [0.1,0.0]]
b_attr = [[0.1, 0.1]]
W_attr=[[0.1, 0.2], [0.1,0.2]]

srn = SRN(2, 2, W_attr=W_attr, U_attr=U_attr, b_attr=b_attr)

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

hidden_state tensor([[0.3177, 0.4775]], grad_fn=)

PyTorch框架内置了SRN的API torch.nn.RNN


## 初始化参数并运行
U_attr = [[0.0, 0.1], [0.1,0.0]]
b_attr = [[0.1, 0.1]]
W_attr=[[0.1, 0.2], [0.1,0.2]]

srn = SRN(2, 2, W_attr=W_attr, U_attr=U_attr, b_attr=b_attr)

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

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

# 设置模型的hidden_size
hidden_size = 32
torch_srn = nn.RNN(input_size, hidden_size)
self_srn = SRN(input_size, hidden_size)

self_hidden_state = self_srn(inputs)
torch_outputs, torch_hidden_state = torch_srn(inputs)

print("self_srn hidden_state: ", self_hidden_state.shape)
print("torch_srn outpus:", torch_outputs.shape)
print("torch_srn hidden_state:", torch_hidden_state.shape)

hidden_state tensor([[0.3177, 0.4775]], grad_fn=)
self_srn hidden_state:  torch.Size([8, 32])
torch_srn outpus: torch.Size([8, 20, 32])
torch_srn hidden_state: torch.Size([1, 20, 32])

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

# 计算自己实现的SRN运算速度
model_time = 0
for i in range(100):
    strat_time = time.time()
    out = self_srn(inputs)
    if i < 10:
        continue
    end_time = time.time()
    model_time += (end_time - strat_time)
avg_model_time = model_time / 90
print('self_srn speed:', avg_model_time, 's')

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

self_srn speed: 0.00018852551778157552 s
torch_srn speed: 8.865197499593099e-05 s

可以看出Pytorch框架实现的要快10倍左右

6.1.2.3 线性层 

线性层会将最后一个时刻的隐状态向量H_L\in \mathbb{R}^{B\times D}进行线性变换,输出分类的对数几率(Logits)为:

Y=H_LW_o+b_o

其中W_o\in \mathbb{R}^{D\times 19},b_o\in \mathbb{R}^{19}为可学习的权重矩阵和偏置。

在分类问题的实践中,我们通常只需要模型输出分类的对数几率(Logits),而不用输出每个类的概率。这需要损失函数可以直接接收对数几率来损失计算。

线性层直接使用torch.nn.Linear算子。

6.1.2.4 模型汇总

在定义了每一层的算子之后,我们定义一个数字求和模型Model_RNN4SeqClass,该模型会将嵌入层、SRN层和线性层进行组合,以实现数字求和的功能.

具体来讲,Model_RNN4SeqClass会接收一个SRN层实例,用于处理数字序列数据,同时在__init__函数中定义一个Embedding嵌入层,其会将输入的数字作为索引,输出对应的向量,最后会使用torch.nn.Linear定义一个线性层。

forward函数中,调用上文实现的嵌入层、SRN层和线性层处理数字序列,同时返回最后一个位置的隐状态向量。代码实现如下:

# 基于RNN实现数字预测的模型
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

# 实例化一个input_size为4, hidden_size为5的SRN
srn = SRN(4, 5)
# 基于srn实例化一个数字预测模型实例
model = Model_RNN4SeqClass(srn, 10, 4, 5, 19)
# 生成一个shape为 2 x 3 的批次数据
inputs = torch.tensor([[1, 2, 3], [2, 3, 4]])
# 进行模型前向预测
logits = model(inputs)
print(logits)

tensor([[-0.0460,  0.0124, -0.0386,  0.0905,  0.2844,  0.4236,  0.2840,  0.4246,
         -0.0323, -0.4017, -0.2120,  0.3045, -0.0029, -0.2223, -0.3427, -0.4185,
         -0.3775, -0.0907,  0.2453],
        [-0.0460,  0.0124, -0.0386,  0.0905,  0.2844,  0.4236,  0.2840,  0.4246,
         -0.0323, -0.4017, -0.2120,  0.3045, -0.0029, -0.2223, -0.3427, -0.4185,
         -0.3775, -0.0907,  0.2453]], grad_fn=)

 

6.1.3 模型训练

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

基于RunnerV3类进行训练,只需要指定length便可以加载相应的数据。设置超参数,使用Adam优化器,学习率为 0.001,实例化模型,使用第4.5.4节定义的Accuracy计算准确率。使用Runner进行训练,训练回合数设为500。代码实现如下:

import os
import random
import torch
import numpy as np
from metric import Accuracy
from Runner import RunnerV3
# 训练轮次
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 SRN with data of length {length}.")
    # 加载长度为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 = DataLoader(train_set, batch_size=batch_size)
    dev_loader = DataLoader(dev_set, batch_size=batch_size)
    test_loader = DataLoader(test_set, batch_size=batch_size)
    # 实例化模型
    base_model = SRN(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 = nn.CrossEntropyLoss()

    # 基于以上组件,实例化Runner
    runner = RunnerV3(model, optimizer, loss_fn, metric)

    # 进行模型训练
    model_save_path = os.path.join(save_dir, f"best_srn_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

6.1.3.2 多组训练

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

srn_runners = {}

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

部分运行结果:

====> Training SRN with data of length 10.

[Train] epoch: 489/500, step: 18600/19000, loss: 0.05225
[Evaluate]  dev score: 0.50000, dev loss: 2.47189
[Train] epoch: 492/500, step: 18700/19000, loss: 0.05124
[Evaluate]  dev score: 0.51000, dev loss: 2.48123
[Train] epoch: 494/500, step: 18800/19000, loss: 0.05769
[Evaluate]  dev score: 0.50000, dev loss: 2.47833
[Train] epoch: 497/500, step: 18900/19000, loss: 0.04340
[Evaluate]  dev score: 0.50000, dev loss: 2.47491
[Evaluate]  dev score: 0.51000, dev loss: 2.47137
[Train] Training done!

====> Training SRN with data of length 15.

[Train] epoch: 489/500, step: 18600/19000, loss: 0.11159
[Evaluate]  dev score: 0.42000, dev loss: 3.65576
[Train] epoch: 492/500, step: 18700/19000, loss: 1.34253
[Evaluate]  dev score: 0.35000, dev loss: 4.08752
[Train] epoch: 494/500, step: 18800/19000, loss: 0.29696
[Evaluate]  dev score: 0.25000, dev loss: 4.61034
[Train] epoch: 497/500, step: 18900/19000, loss: 0.12404
[Evaluate]  dev score: 0.41000, dev loss: 3.44996
[Evaluate]  dev score: 0.42000, dev loss: 3.38767
[Train] Training done!

====> Training SRN with data of length 20.

[Train] epoch: 489/500, step: 18600/19000, loss: 0.94918
[Evaluate]  dev score: 0.28000, dev loss: 3.03929
[Train] epoch: 492/500, step: 18700/19000, loss: 1.78990
[Evaluate]  dev score: 0.30000, dev loss: 2.91098
[Train] epoch: 494/500, step: 18800/19000, loss: 0.58229
[Evaluate]  dev score: 0.34000, dev loss: 2.88835
[Train] epoch: 497/500, step: 18900/19000, loss: 0.83346
[Evaluate]  dev score: 0.28000, dev loss: 2.91086
[Evaluate]  dev score: 0.26000, dev loss: 3.06699
[Train] Training done!

====> Training SRN with data of length 25.

[Train] epoch: 489/500, step: 18600/19000, loss: 1.25829
[Evaluate]  dev score: 0.10000, dev loss: 5.29650
[Train] epoch: 492/500, step: 18700/19000, loss: 0.49421
[Evaluate]  dev score: 0.10000, dev loss: 5.00213
[Train] epoch: 494/500, step: 18800/19000, loss: 0.48700
[Evaluate]  dev score: 0.13000, dev loss: 5.15761
[Train] epoch: 497/500, step: 18900/19000, loss: 0.51467
[Evaluate]  dev score: 0.13000, dev loss: 5.27542
[Evaluate]  dev score: 0.12000, dev loss: 5.33683
[Train] Training done!

====> Training SRN with data of length 30.

[Train] epoch: 489/500, step: 18600/19000, loss: 0.12149
[Evaluate]  dev score: 0.18000, dev loss: 4.28667
[Train] epoch: 492/500, step: 18700/19000, loss: 0.64496
[Evaluate]  dev score: 0.19000, dev loss: 4.32516
[Train] epoch: 494/500, step: 18800/19000, loss: 0.36210
[Evaluate]  dev score: 0.22000, dev loss: 4.37720
[Train] epoch: 497/500, step: 18900/19000, loss: 0.97052
[Evaluate]  dev score: 0.20000, dev loss: 4.29592
[Evaluate]  dev score: 0.12000, dev loss: 4.48994
[Train] Training done!

====> Training SRN with data of length 35.

[Train] epoch: 489/500, step: 18600/19000, loss: 1.99054
[Evaluate]  dev score: 0.05000, dev loss: 4.47362
[Train] epoch: 492/500, step: 18700/19000, loss: 1.63049
[Evaluate]  dev score: 0.07000, dev loss: 4.44207
[Train] epoch: 494/500, step: 18800/19000, loss: 1.58705
[Evaluate]  dev score: 0.07000, dev loss: 4.48929
[Train] epoch: 497/500, step: 18900/19000, loss: 1.63524
[Evaluate]  dev score: 0.03000, dev loss: 4.50457
[Evaluate]  dev score: 0.06000, dev loss: 4.08931
[Train] Training done!

6.1.3.3 损失曲线展示

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

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 = srn_runners[length]
    fig_name = f"./images/6.6_{length}.pdf"
    plot_training_loss(runner, fig_name, sample_step=100)

                                L=10                                                                L=15

HBU-NNDL 实验七 循环神经网络(1)RNN记忆能力实验_第3张图片HBU-NNDL 实验七 循环神经网络(1)RNN记忆能力实验_第4张图片 

                                L=20                                                                L=25

HBU-NNDL 实验七 循环神经网络(1)RNN记忆能力实验_第5张图片 HBU-NNDL 实验七 循环神经网络(1)RNN记忆能力实验_第6张图片 

                                L=30                                                                        L=35 

HBU-NNDL 实验七 循环神经网络(1)RNN记忆能力实验_第7张图片 HBU-NNDL 实验七 循环神经网络(1)RNN记忆能力实验_第8张图片 

 

6.1.4 模型评价

在模型评价时,加载不同长度的效果最好的模型,然后使用测试集对该模型进行评价,观察模型在测试集上预测的准确度. 同时记录一下不同长度模型在训练过程中,在验证集上最好的效果。代码实现如下。

srn_dev_scores = []
srn_test_scores = []
for length in lengths:
    print(f"Evaluate SRN with data length {length}.")
    runner = srn_runners[length]
    # 加载训练过程中效果最好的模型
    model_path = os.path.join(save_dir, f"best_srn_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 = DataLoader(test_set, batch_size=batch_size)

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

for length, dev_score, test_score in zip(lengths, srn_dev_scores, srn_test_scores):
    print(f"[SRN] length:{length}, dev_score: {dev_score}, test_score: {test_score: .5f}")


import matplotlib.pyplot as plt

plt.plot(lengths, srn_dev_scores, '-o', color='#e4007f',  label="Dev Accuracy")
plt.plot(lengths, srn_test_scores,'-o', color='#f19ec2', label="Test Accuracy")

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

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

Evaluate SRN with data length 10.
Evaluate SRN with data length 15.
Evaluate SRN with data length 20.
Evaluate SRN with data length 25.
Evaluate SRN with data length 30.
Evaluate SRN with data length 35.
[SRN] length:10, dev_score: 0.58, test_score:  0.49000
[SRN] length:15, dev_score: 0.45, test_score:  0.48000
[SRN] length:20, dev_score: 0.34, test_score:  0.24000
[SRN] length:25, dev_score: 0.13, test_score:  0.10000
[SRN] length:30, dev_score: 0.23, test_score:  0.11000
[SRN] length:35, dev_score: 0.12, test_score:  0.08000

HBU-NNDL 实验七 循环神经网络(1)RNN记忆能力实验_第9张图片

 上图展示了SRN模型在不同长度数据训练出来的最好模型在验证集和测试集上的表现。可以看到,随着序列长度的增加,验证集和测试集的准确度整体趋势是降低的,这同样说明SRN模型保持长期依赖的能力在不断降低.

参考《神经网络与深度学习》中的公式(6.50),改进SRN的循环单元,加入隐状态之间的残差连接,并重复数字求和实验。观察是否可以缓解长程依赖问题

残差连接即:y = H(x,WH) + X

隐状态添加残差连接就是将

  hidden_state = F.tanh(torch.matmul(step_input, self.W) + torch.matmul(hidden_state, self.U) + self.b)

改为

 hidden_state =hidden_state + F.tanh(torch.matmul(step_input, self.W) + torch.matmul(hidden_state, self.U) + self.b)

Evaluate SRN with data length 10.
Evaluate SRN with data length 15.
Evaluate SRN with data length 20.
Evaluate SRN with data length 25.
Evaluate SRN with data length 30.
Evaluate SRN with data length 35.
[SRN] length:10, dev_score: 0.97, test_score:  0.96000
[SRN] length:15, dev_score: 0.98, test_score:  0.96000
[SRN] length:20, dev_score: 0.94, test_score:  0.96000
[SRN] length:25, dev_score: 0.98, test_score:  0.97000
[SRN] length:30, dev_score: 0.97, test_score:  0.96000
[SRN] length:35, dev_score: 0.87, test_score:  0.92000 

HBU-NNDL 实验七 循环神经网络(1)RNN记忆能力实验_第10张图片

 可以看出,加了残差之后,准确率提升很高。

心得体会

循环网络可以看作是在时间维度上权值共享的神经网络。

 简单循环神经网络和隐马尔可夫链大致无异,通过U状态-状态转移矩阵,和W状态输入矩阵。

由于在神经元中的运算和变量在不同时刻是相同的,因此循环神经网络理论上可以看作是同一神经网络被无限复制的结果,极大的降低了计算量。循环神经网络在不同的位置共享参数,从而使有限的参数处理任意长度的序列。如果我们在每个时间点都有一个单独的参数,我们不但不能泛化到训练时没有见过序列长度,也不能再时间上共享不同序列长度和不同位置的统计强度。
 

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