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


循环神经网络(Recurrent Neural Network,RNN)是一类具有短期记忆能力的神经网络.
在循环神经网络中,神经元不但可以接受其他神经元的信息,也可以接受自身的信息,形成具有环路的网络结构.
和前馈神经网络相比,循环神经网络更加符合生物神经网络的结构.
目前,循环神经网络已经被广泛应用在语音识别、语言模型以及自然语言生成等任务上.
在这里插入图片描述
简单循环网络在参数学习时存在长程依赖问题,很难建模长时间间隔(Long Range)的状态之间的依赖关系。
为了测试简单循环网络的记忆能力,本节构建一个【数字求和任务】进行实验。
数字求和任务的输入是一串数字,前两个位置的数字为0-9,其余数字随机生成(主要为0),预测目标是输入序列中前两个数字的加和。图6.3展示了长度为10的数字序列.

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

循环神经网络的一种简单实现是简单循环网络(Simple Recurrent Network,SRN)
在这里插入图片描述
简单循环网络在参数学习时存在长程依赖问题,很难建模长时间间隔(Long Range)的状态之间的依赖关系。为了测试简单循环网络的记忆能力,本节构建一个数字求和任务进行实验。

数字求和任务的输入是一串数字,前两个位置的数字为0-9,其余数字随机生成(主要为0),预测目标是输入序列中前两个数字的加和。图6.3展示了长度为10的数字序列.
在这里插入图片描述
如果序列长度越长,准确率越高,则说明网络的记忆能力越好.因此,我们可以构建不同长度的数据集,通过验证简单循环网络在不同长度的数据集上的表现,从而测试简单循环网络的长程依赖能力.

6.1.1 数据集构建

构建不同长度的数字预测数据集DigitSum

6.1.1.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 加载数据并进行数据划分

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类

from torch.utils.data import Dataset,DataLoader
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="int64")
        label = torch.tensor(example[1], dtype="int64")
        return seq, label

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

6.1.2 模型构建

在这里插入图片描述

6.1.2.1 嵌入层

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)

运行结果:

tensor([[0.3177, 0.4775]], grad_fn=<TanhBackward0>) 
  • 自定义简单循环网络
  • pytorch内置了SRN的API paddle.nn.SimpleRNN
## 初始化参数并运行
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)

运行结果:

self_srn hidden_state:  torch.Size([8, 32])
paddle_srn outpus: torch.Size([8, 20, 32])
paddle_srn hidden_state: torch.Size([1, 20, 32]) 

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

将自己实现的SRN和pytorch框架内置的SRN返回的结果进行打印展示

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

# 设置模型的hidden_size

torch_srn = nn.RNN(input_size, hidden_size, bias=False)

# 获取torch_srn中的参数,并设置相应的paramAttr,用于初始化SRN
W_attr = torch_srn.weight_ih_l0.T
U_attr = torch_srn.weight_hh_l0.T
self_srn = SRN(input_size, hidden_size, W_attr=W_attr, U_attr=U_attr)

# 进行前向计算,获取隐状态向量,并打印展示
self_hidden_state = self_srn(inputs)
torch_outputs, torch_hidden_state = torch_srn(inputs)
print("pytorch的SRN:\n", torch_hidden_state.detach().numpy().squeeze(0))
print("自己的SRN:\n", self_hidden_state.detach().numpy())
 pytorch的SRN:
 [[-0.7879561   0.30351916 -0.8180367  -0.6886782  -0.04480647  0.21398577
   0.7151095  -0.6950615   0.15764976  0.44665754]
 [ 0.09500529 -0.20703138  0.41871697 -0.96180415 -0.67065084  0.3635481
   0.9330359  -0.6328799   0.29817006  0.45230556]
 [ 0.37487072  0.50830954 -0.70687103  0.37766257  0.08774418  0.3540142
  -0.3860005   0.4884959   0.440492    0.27764338]
 [-0.4694892   0.03494496 -0.8948485   0.68928754 -0.06903987 -0.11051217
  -0.23742953 -0.6187381  -0.15267557  0.5742124 ]
 [-0.05243767 -0.00631889  0.15062073 -0.5604651  -0.01922686  0.20319594
   0.7351512  -0.4121003   0.07413491  0.6317186 ]]
自己的SRN:
 [[ 0.12710479  0.10965557  0.30339125 -0.8991724   0.17633936 -0.30394837
  -0.49593294  0.84559697  0.8441264   0.06168123]
 [ 0.01825176 -0.31220886 -0.09947876 -0.19136088 -0.10779942 -0.09303827
   0.33987308  0.0207502  -0.7328755   0.4166944 ]]

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

import time

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

# 实例化模型
self_srn = SRN(input_size, hidden_size)
torch_srn = nn.RNN(input_size, hidden_size)

# 计算自己实现的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.0003568015407986111 s
paddle_srn speed: 0.0001598037100897895 s 

可以看到,由于PyTorch内部相关算子由C++实现,PyTorch框架实现的SRN的运行效率显著高于自己实现的SRN效率。

6.1.2.3 线性层

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

6.1.2.4 模型汇总

在定义了每一层的算子之后,我们定义一个数字求和模型Model_RNN4SeqClass,该模型会将嵌入层、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.4391,  0.0797,  0.0528, -0.2097, -0.1545,  0.1211, -0.3110,  0.0738,
          0.2136, -0.1762,  0.3715, -0.0294,  0.2029,  0.4429, -0.1367,  0.2430,
         -0.1279, -0.0653, -0.0758],
        [-0.2591,  0.0797,  0.0528, -0.2097, -0.1545,  0.1211, -0.3110,  0.0738,
          0.1935, -0.1762,  0.3715, -0.0294,  0.2029,  0.4429, -0.1367,  0.2430,
         -0.1279, -0.0653, -0.0758]], grad_fn=<AddmmBackward0>)

6.1.3 模型训练

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

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

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.long())  # 默认求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.long()).item()
            # 累积损失
            total_loss += loss

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

        dev_loss = (total_loss / len(dev_loader))
        dev_score = self.metric.accumulate()

        # 记录验证集loss
        if global_step != -1:
            self.dev_losses.append((global_step, dev_loss))
            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):
        state_dict = torch.load(model_path)
        self.model.load_state_dict(state_dict)

Accuracy:

class Accuracy():
    def __init__(self, is_logist=True):
        # 用于统计正确的样本个数
        self.num_correct = 0
        # 用于统计样本的总数
        self.num_count = 0

        self.is_logist = is_logist

    def update(self, outputs, labels):

        # 判断是二分类任务还是多分类任务,shape[1]=1时为二分类任务,shape[1]>1时为多分类任务
        if outputs.shape[1] == 1:  # 二分类
            outputs = torch.squeeze(outputs, dim=-1)
            if self.is_logist:
                # logist判断是否大于0
                preds = torch.tensor((outputs >= 0), dtype=torch.float32)
            else:
                # 如果不是logist,判断每个概率值是否大于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)).cpu().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"
# 训练轮次
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
srn_runners = {}

6.1.3.2 多组训练

srn_runners = {}

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

运行结果:

[Evaluate]  dev score: 0.13000, dev loss: 3.86728
[Train] epoch: 476/500, step: 18100/19000, loss: 2.55978
[Evaluate]  dev score: 0.14000, dev loss: 3.94202
[Train] epoch: 478/500, step: 18200/19000, loss: 1.88343
[Evaluate]  dev score: 0.10000, dev loss: 3.91543
[Train] epoch: 481/500, step: 18300/19000, loss: 1.63347
[Evaluate]  dev score: 0.14000, dev loss: 3.84488
[Train] epoch: 484/500, step: 18400/19000, loss: 2.11149
[Evaluate]  dev score: 0.11000, dev loss: 3.87420
[Train] epoch: 486/500, step: 18500/19000, loss: 1.73132
[Evaluate]  dev score: 0.14000, dev loss: 3.85581
[Train] epoch: 489/500, step: 18600/19000, loss: 1.52330
[Evaluate]  dev score: 0.16000, dev loss: 3.89372
[Train] epoch: 492/500, step: 18700/19000, loss: 1.39440
[Evaluate]  dev score: 0.12000, dev loss: 3.93903
[Train] epoch: 494/500, step: 18800/19000, loss: 1.79961
[Evaluate]  dev score: 0.15000, dev loss: 3.89620
[Train] epoch: 497/500, step: 18900/19000, loss: 1.08969
[Evaluate]  dev score: 0.11000, dev loss: 4.00258
[Evaluate]  dev score: 0.12000, dev loss: 4.04619
[Train] Training done!

6.1.3.3 损失曲线展示

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)

k=10:
在这里插入图片描述

k=15:
在这里插入图片描述
k=20:
在这里插入图片描述
k=25:
在这里插入图片描述
k=30:
在这里插入图片描述
k=35:
在这里插入图片描述
上图展示了在6个数据集上的损失变化情况,数据集的长度分别为10、15、20、25、30和35. 从输出结果看,随着数据序列长度的增加,虽然训练集损失逐渐逼近于0,但是验证集损失整体趋向越来越大,这表明当序列变长时,SRN模型保持序列长期依赖能力在逐渐变弱,越来越无法学习到有用的知识.

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}")

运行结果:

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.41, test_score:  0.22000
[SRN] length:15, dev_score: 0.2, test_score:  0.18000
[SRN] length:20, dev_score: 0.18, test_score:  0.13000
[SRN] length:25, dev_score: 0.12, test_score:  0.17000
[SRN] length:30, dev_score: 0.17, test_score:  0.05000
[SRN] length:35, dev_score: 0.18, test_score:  0.07000 

接下来,将SRN在不同长度的验证集和测试集数据上的表现,绘制成图片进行观察

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()

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

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

将代码一行改为:

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 

在这里插入图片描述
加了残差之后,准确率提升很高,有效缓解了长程依赖问题。

总结

通过本次实验发现改进SRN的循环单元,加入隐状态之间的残差连接,准确率大幅升高,有效缓解了长程依赖问题。对循环神经网络的模型结构的嵌入层和SRN层有了比较深的了解,RNN的一些计算图还需要好好研究并理解。

参考文章

https://www.cnblogs.com/hbuwyg/p/16617681.html
https://blog.csdn.net/qq_38975453/article/details/126521361

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值