循环神经网络(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 数据集的构建函数
由于在本任务中,输入序列的前两位数字为 0 − 9,其组合数是固定的,所以可以穷举所有的前两位数字组合,并在后面默认用0填充到固定长度. 但考虑到数据的多样性,这里对生成的数字序列中的零位置进行随机采样,并将其随机替换成0-9的数字以增加样本的数量.
我们可以通过设置k的数值来指定一条样本随机生成的数字序列数量.当生成某个指定长度的数据集时,会同时生成训练集、验证集和测试集。当k=3时,生成训练集。当k=1时,生成验证集和测试集. 代码实现如下:
import os
import torch
import random
import numpy as np
import torch.nn as nn
# 固定随机种子
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:
# 首先判断是否存在这样的数据文件,不存在就建立一个
if not os.path.exists(f"./datasets/{
length}/"):
os.makedirs(f"./datasets/{
length}")
# 生成长度为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)
实验结果:
generate data to: ./datasets/5/train.txt.
generate data to: ./datasets/5/dev.txt.
generate data to: ./datasets/5/test.txt.
generate data to: ./datasets/10/train.txt.
generate data to: ./datasets/10/dev.txt.
generate data to: ./datasets/10/test.txt.
generate data to: ./datasets/15/train.txt.
generate data to: ./datasets/15/dev.txt.
generate data to: ./datasets/15/test.txt.
generate data to: ./datasets/20/train.txt.
generate data to: ./datasets/20/dev.txt.
generate data to: ./datasets/20/test.txt.
generate data to: ./datasets/25/train.txt.
generate data to: ./datasets/25/dev.txt.
generate data to: ./datasets/25/test.txt.
generate data to: ./datasets/30/train.txt.
generate data to: ./datasets/30/dev.txt.
generate data to: ./datasets/30/test.txt.
generate data to: ./datasets/35/train.txt.
generate data to: ./datasets/35/dev.txt.
generate data to: ./datasets/35/test.txt.
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(