目录
一、实验介绍
二、实验环境
1. 配置虚拟环境
2. 库版本介绍
三、实验内容
0. 导入必要的工具包
1. 数据处理
2. rnn
测试
3. grad_clipping
4. 代码整合
经验是智慧之父,记忆是智慧之母。
——谚语
本实验介绍了一个简单的循环神经网络(RNN)模型,并探讨了梯度裁剪在模型训练中的应用。
ChatGPT:
循环神经网络(Recurrent Neural Network,RNN)是一种在序列数据上进行建模和处理的神经网络模型。与传统的前馈神经网络(Feedforward Neural Network)不同,RNN具有循环连接,使其能够捕捉到序列数据中的时间依赖关系。
RNN的主要特点是它可以接受任意长度的输入序列,并且可以通过记忆先前的信息来影响后续的输出。这种记忆能力使得RNN在处理序列数据方面非常有效,如自然语言处理(NLP)、语音识别、机器翻译等任务。
RNN模型中的基本组件是循环单元(Recurrent Unit),最常见的是长短期记忆网络(Long Short-Term Memory,LSTM)和门控循环单元(Gated Recurrent Unit,GRU)。这些循环单元通过将当前输入与前一个时间步的隐藏状态结合起来,产生当前时间步的输出和新的隐藏状态。这种循环结构允许信息在网络中传递和更新,并保持对序列中的长期依赖关系的记忆。
在训练RNN模型时,通常使用反向传播算法和梯度下降优化算法来更新网络参数,以使模型能够更好地适应输入序列的特征。
需要注意的是,虽然RNN在处理序列数据方面具有优势,但也存在一些问题,如长期依赖问题和梯度消失/爆炸问题。为了应对这些问题,研究人员提出了一些改进的RNN变体,如带有门控机制的LSTM和GRU,以及更先进的模型,如变换器(Transformer)模型。
本系列实验使用了PyTorch深度学习框架,相关操作如下:
conda create -n DL python=3.7
conda activate DL
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
conda install matplotlib
conda install scikit-learn
软件包 | 本实验版本 | 目前最新版 |
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 |
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))
X
是一个形状为(3, 1)的张量,表示输入。W_xh
是一个形状为(1, 4)的张量,表示输入到隐藏状态的权重。H
是一个形状为(3, 4)的张量,表示隐藏状态。W_hh
是一个形状为(4, 4)的张量,表示隐藏状态到隐藏状态的权重。 定义了一个名为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
激活函数来更新隐藏状态torch.cat
函数将输出列表合并成一个张量,返回合并后的张量和最后一个隐藏状态 (H,)
。 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
在循环神经网络的训练中,当时间步较大时,可能导致数值不稳定, 例如梯度爆炸或梯度消失,所以一个很重要的步骤是梯度裁剪。通过下面的函数,梯度范数永远不会超过给定的阈值, 并且更新后的梯度完全与的原始方向对齐。
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进行比较。如果超过阈值,则对参数梯度进行裁剪,使其不超过阈值。
# 导入必要的工具包
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模型能够正确计算隐藏状态和输出结果,并且通过梯度裁剪可以有效控制梯度的大小,提高模型的稳定性和训练效果。