一、实验介绍
本实验介绍了一个简单的循环神经网络(RNN)模型,并探讨了梯度裁剪在模型训练中的应用。
在前馈神经网络中,信息的传递是单向的,这种限制虽然使得网络变得更容易学习,但在一定程度上也减弱了神经网络模型的能力.在生物神经网络中,神经元之间的连接关系要复杂得多.前馈神经网络可以看作一个复杂的函数,每次输入都是独立的,即网络的输出只依赖于当前的输入.但是在很多现实任务中, 网络的输出不仅和当前时刻的输入相关,也和其过去一段时间的输出相关.比如一个有限状态自动机,其下一个时刻的状态(输出)不仅仅和当前输入相关,也和当前状态(上一个时刻的输出)相关.此外,前馈网络难以处理时序数据,比如视频、语音、文本等.时序数据的长度一般是不固定的,而前馈神经网络要求输入和输出的维数都是固定的,不能任意改变.因此,当处理这一类和时序数据相关 的问题时,就需要一种能力更强的模型. 循环神经网络(Recurrent Neural Network,RNN)是一类具有短期记忆能力的神经网络. 在循环神经网络中,神经元不但可以接受其他神经元的信息,也可以接受自身的信息,形成具有环路的网络结构.和前馈神经网络相比,循环神经网络更加符合生物神经网络的结构.循环神经网络已经被广泛应用在语音识别、语言模型以及自然语言生成等任务上.循环神经网络的参数学习可以通过随时间反向传播算法[Werbos, 1990]来学习.随时间反向传播算法即按照时间的逆序将错误信息一步步地往前传递.
二、实验环境
本系列实验使用了PyTorch深度学习框架,相关操作如下:
1. 配置虚拟环境
代码语言:javascript复制conda create -n DL python=3.7
代码语言:javascript复制conda activate DL
代码语言:javascript复制pip install torch==1.8.1 cu102 torchvision==0.9.1 cu102 torchaudio==0.8.1 -f https://download.pytorch.org/whl/torch_stable.html
代码语言:javascript复制conda install matplotlib
代码语言:javascript复制 conda install scikit-learn
2. 库版本介绍
软件包 | 本实验版本 | 目前最新版 |
---|---|---|
matplotlib | 3.5.3 | 3.8.0 |
numpy | 1.21.6 | 1.26.0 |
python | 3.7.16 | |
scikit-learn | 0.22.1 | 1.3.0 |
torch | 1.8.1 cu102 | 2.0.1 |
torchaudio | 0.8.1 | 2.0.2 |
torchvision | 0.9.1 cu102 | 0.15.2 |
三、实验内容
0. 导入必要的工具包
代码语言:javascript复制import torch
1. 数据处理
与之前的模型有所不同,循环神经网络引入了隐藏状态和时间步两个新概念。当前时间步的隐藏状态由当前时间的输入与上一个时间步的隐藏状态一起计算出。
根据隐藏状态的计算公式,需要计算两次矩阵乘法和三次加法才能得到当前时刻的隐藏状态。这里通过代码说明: 该计算公式等价于将当前时刻的输入与上一个时间步的隐藏状态做拼接,将两个权重矩阵做拼接,然后对两个拼接后的结果做矩阵乘法。此处展示省略了偏置项。
代码语言:javascript复制# X为模拟的输入,H为模拟的隐藏状态,在实际情况时要更复杂一些
X, W_xh = torch.normal(0, 1, (3, 1)), torch.normal(0, 1, (1, 4))
H, W_hh = torch.normal(0, 1, (3, 4)), torch.normal(0, 1, (4, 4))
torch.matmul(X, W_xh) torch.matmul(H, W_hh)
上面是按照公式计算得到的结果,下面是拼接后计算得到的结果,两个结果完全相同
代码语言:javascript复制torch.matmul(torch.cat((X, H), 1), torch.cat((W_xh, W_hh), 0))
X
是一个形状为(3, 1)的张量,表示输入。W_xh
是一个形状为(1, 4)的张量,表示输入到隐藏状态的权重。H
是一个形状为(3, 4)的张量,表示隐藏状态。W_hh
是一个形状为(4, 4)的张量,表示隐藏状态到隐藏状态的权重。
2. rnn
定义了一个名为rnn
的函数,用于执行循环神经网络的前向传播,在函数内部,通过遍历输入序列的每个时间步,逐步计算隐藏状态和输出。
def rnn(inputs, state, params):
# inputs的形状:(时间步数量,批量大小,词表大小)
W_xh, W_hh, b_h, W_hq, b_q = params
H = state
outputs = []
# X的形状:(批量大小,词表大小)
for X in inputs:
H = torch.tanh(torch.mm(X, W_xh) torch.mm(H, W_hh) b_h)
Y = torch.mm(H, W_hq) b_q
outputs.append(Y)
return torch.cat(outputs, dim=0), (H,)
- 参数:
inputs
是一个形状为(时间步数量,批量大小,词表大小)的张量,表示输入序列。state
是一个形状为(批量大小,隐藏状态大小)的张量,表示初始隐藏状态。params
是一个包含了模型的参数的列表,包括W_xh
、W_hh
、b_h
、W_hq
和b_q
。
- 对于每个时间步,
- 使用
tanh
激活函数来更新隐藏状态 - 根据更新后的隐藏状态,计算输出Y
- 将输出添加到outputs列表中
- 使用
- 使用
torch.cat
函数将输出列表合并成一个张量,返回合并后的张量和最后一个隐藏状态(H,)
。
测试
代码语言:javascript复制 inputs=torch.rand(10,3,50)
params=[torch.rand((50,50)),torch.rand((50,50)),torch.rand((3,50)),torch.rand((50,60)),torch.rand((3,60))]
state=torch.rand((3,50))
output=rnn(inputs,state,params)
print(output)
inputs
是一个形状为(10, 3, 50)的随机张量,表示模拟的输入序列params
是一个包含了随机参数的列表,与rnn
函数中的参数对应state
是一个形状为(3, 50)的随机张量,表示初始隐藏状态- 调用
rnn
函数 - 打印输出结果
output
3. grad_clipping
在循环神经网络的训练中,当时间步较大时,可能导致数值不稳定, 例如梯度爆炸或梯度消失,所以一个很重要的步骤是梯度裁剪。通过下面的函数,梯度范数永远不会超过给定的阈值, 并且更新后的梯度完全与的原始方向对齐。
代码语言:javascript复制def grad_clipping(net, theta):
if isinstance(net, nn.Module):
params = [p for p in net.parameters() if p.requires_grad]
else:
params = net.params
norm = torch.sqrt(sum(torch.sum((p.grad ** 2)) for p in params))
if norm > theta:
for param in params:
param.grad[:] *= theta / norm
函数接受两个参数:net和theta。该函数首先根据net的类型获取需要梯度更新的参数,然后计算所有参数梯度的平方和的平方根,并将其与阈值theta进行比较。如果超过阈值,则对参数梯度进行裁剪,使其不超过阈值。
4. 代码整合
代码语言:javascript复制# 导入必要的工具包
import torch
# # X为模拟的输入,H为模拟的隐藏状态,在实际情况时要更复杂一些
# X, W_xh = torch.normal(0, 1, (3, 1)), torch.normal(0, 1, (1, 4))
# H, W_hh = torch.normal(0, 1, (3, 4)), torch.normal(0, 1, (4, 4))
# # torch.matmul(X, W_xh) torch.matmul(H, W_hh)
# #
# # torch.matmul(torch.cat((X, H), 1), torch.cat((W_xh, W_hh), 0))
def rnn(inputs, state, params):
# inputs的形状:(时间步数量,批量大小,词表大小)
W_xh, W_hh, b_h, W_hq, b_q = params
H = state
outputs = []
# X的形状:(批量大小,词表大小)
for X in inputs:
H = torch.tanh(torch.mm(X, W_xh) torch.mm(H, W_hh) b_h)
Y = torch.mm(H, W_hq) b_q
outputs.append(Y)
return torch.cat(outputs, dim=0), (H,)
def grad_clipping(net, theta):
if isinstance(net, nn.Module):
params = [p for p in net.parameters() if p.requires_grad]
else:
params = net.params
norm = torch.sqrt(sum(torch.sum((p.grad ** 2)) for p in params))
if norm > theta:
for param in params:
param.grad[:] *= theta / norm
if __name__ == '__main__':
inputs=torch.rand(10,3,50)
params=[torch.rand((50,50)),torch.rand((50,50)),torch.rand((3,50)),torch.rand((50,60)),torch.rand((3,60))]
state=torch.rand((3,50))
output=rnn(inputs,state,params)
print(output)
使用随机生成的输入数据和参数进行模型的测试。测试结果显示,RNN模型能够正确计算隐藏状态和输出结果,并且通过梯度裁剪可以有效控制梯度的大小,提高模型的稳定性和训练效果。