RNN的理解

server/2024/10/18 22:26:40/

如果你不想使用 PyTorch 内置的 nn.RNN 模块,而是希望自己从头实现一个基础的 RNN 以更深入地理解其内部机制,你可以手动实现 RNN 的前向传播和反向传播。下面是一个详细的实现示例,包括中文注释说明。

手动实现基础 RNN

1. 基础 RNN 结构

在基础的 RNN 中,我们需要实现以下部分:

  1. 隐藏状态更新公式
  2. 输出计算公式
  3. 梯度计算和参数更新
2. RNN 的数学公式
  1. 隐藏状态更新
    在这里插入图片描述

    其中:

    • ( h_t ):时间步 ( t ) 的隐藏状态
    • ( x_t ):时间步 ( t ) 的输入
    • ( W_{ih} ):输入到隐藏层的权重矩阵
    • ( W_{hh} ):隐藏层到隐藏层的权重矩阵
    • ( b_h ):隐藏层的偏置
    • ( \tanh ):激活函数
2. 实现代码

以下是一个完整的手动实现 RNN 的代码示例:

python">import torch
import torch.nn.functional as F
class MyRNN(nn.Module):def __init__(self, input_size, hidden_size, layers_size,first_batch=False):super(MyRNN, self).__init__()self.input_size = input_sizeself.hidden_size = hidden_sizeself.layers_size = layers_sizeself.first_batch = first_batchself.w_in = []self.w_hh = []self.bh = []for layers in range(self.layers_size):self.w_in.append(torch.randn([hidden_size, input_size])*0.01)self.w_hh.append(torch.randn([hidden_size, hidden_size])*0.01)self.bh.append(torch.zeros(hidden_size))input_size = hidden_size # 除了第一层,输入与隐层相等self.w_in = nn.ParameterList([nn.Parameter(w) for w in self.w_in])self.w_hh = nn.ParameterList([nn.Parameter(w) for w in self.w_hh])self.bh = nn.ParameterList([nn.Parameter(b) for b in self.bh])def forward(self, inputs, hidden=None):if self.first_batch:batch_size, seq_len, _ = inputs.size()else:seq_len, batch_size, _ = inputs.size()inputs = inputs.transpose(0, 1)if hidden is None:hidden = [torch.zeros(batch_size, self.hidden_size) for _ in range(self.layers_size)]outputs = []print(inputs.shape)for t in range(seq_len):x = inputs[:, t, :]for layer in range(self.layers_size):h_pre = hidden[layer]h_t = torch.tanh(torch.mm(x, self.w_in[layer].t()) + torch.mm(h_pre, self.w_hh[layer].t()) + self.bh[layer])hidden[layer] = h_tx = h_toutputs.append(h_t.unsqueeze(1))outputs = torch.cat(outputs, dim=1)return outputs, hidden
4. 代码详解
  1. 初始化
python"> def __init__(self, input_size, hidden_size, layers_size,first_batch=False):super(MyRNN, self).__init__()self.input_size = input_sizeself.hidden_size = hidden_sizeself.layers_size = layers_sizeself.first_batch = first_batchself.w_in = []self.w_hh = []self.bh = []for layers in range(self.layers_size):self.w_in.append(torch.randn([hidden_size, input_size])*0.01)self.w_hh.append(torch.randn([hidden_size, hidden_size])*0.01)self.bh.append(torch.zeros(hidden_size))input_size = hidden_size # 除了第一层,输入与隐层相等self.w_in = nn.ParameterList([nn.Parameter(w) for w in self.w_in])self.w_hh = nn.ParameterList([nn.Parameter(w) for w in self.w_hh])self.bh = nn.ParameterList([nn.Parameter(b) for b in self.bh])
  • W_ih:输入到隐藏层的权重矩阵
  • W_hh:隐藏层到隐藏层的权重矩阵
  • bh:隐藏层的偏置项
  1. 前向传播
python">def forward(self, inputs, hidden=None):if self.first_batch:batch_size, seq_len, _ = inputs.size()else:seq_len, batch_size, _ = inputs.size()inputs = inputs.transpose(0, 1)if hidden is None:hidden = [torch.zeros(batch_size, self.hidden_size) for _ in range(self.layers_size)]outputs = []for t in range(seq_len):x = inputs[:, t, :]for layer in range(self.layers_size):h_pre = hidden[layer]h_t = torch.tanh(torch.mm(x, self.w_in[layer].t()) + torch.mm(h_pre, self.w_hh[layer].t()) + self.bh[layer])hidden[layer] = h_tx = h_toutputs.append(h_t.unsqueeze(1))outputs = torch.cat(outputs, dim=1)if self.first_batch:# 保证输入与输出一致outputs = outputs.transpose(0, 1)return outputs, hidden
  • x:当前时间步的输入
  • hidden:更新后的隐藏状态
  • torch.tanh:激活函数

3. 示例运行

python"># 示例:定义参数并运行模型
input_size = 3
hidden_size = 8
num_layers = 1
batch_size = 5
seq_len = 4# 初始化自定义RNN
model = MyRNN(input_size, hidden_size, num_layers, first_batch=False)# 生成随机输入
# input_seq = torch.randn(batch_size, seq_len, input_size)
input_seq = torch.randn(seq_len, batch_size, input_size)# 执行前向传播
output, hidden = model(input_seq)print(f"输入的数据形状: {input_seq.shape}")
print("输出维度:", output.shape)
print("输出:", output.shape)

运行结果:

python">输入的数据形状: torch.Size([4, 5, 3])
输出维度: torch.Size([5, 4, 8])
输出: torch.Size([5, 4, 8])

4. 对RNN网络结构问题的思考

  1. 参数的初始化为什么要乘以 0.01?
    乘以 0.01 是一种简化的初始化策略,主要目的是让权重矩阵的初始值较小,避免梯度爆炸或梯度消失等问题。一方面,在深度网络,随着层数的加深,梯度可能会在反向传播时不断增大,导致梯度爆炸,使得模型训练不稳定。将初始权重设置得较小(例如乘以 0.01)可以在一定程度上减缓这种现象。乘以 0.01 也是一种实现的简化方式,避免了复杂的权重初始化方法。

  2. 在前向传播中,外层循环是对输入序列长度,内层循环是对网络层数,那么为什么不能交换呢?
    seq_len 表示输入序列的时间步长度。在每个时间步上,RNN 的计算是依赖于前一个时间步的隐藏状态。为了让时间步之间的数据流通,逐个时间步进行计算,就保证当前时间步的输入和前一个时间步的隐藏状态能够正常传递。这种设计是符合 RNN 的时间序列计算机制的。

    RNN 的这种时间步间依赖性是其核心特点,必须按时间顺序处理。

  • 2.1 时间依赖:每个时间步的输出依赖于前一个时间步的隐藏状态。因此必须先按时间顺序处理每个时间步,这样才能确保前一个时间步的隐藏状态正确地传递给下一个时间步。如果先循环层数,再循环时间步,时间步之间的依赖被破坏(具体而言就是: 每一层的 h_pre 只是在当前层的时间步中起作用,但在跨层的时候,没有正确的隐藏状态传递给下一时间步。也就是说,在循环第一个时间步时,已经丢失了前一个时间步的隐藏状态。)

  • 2.2 层的串联
    在每个时间步内,所有层应该是串联关系,但你是在整个时间序列结束后再计算下一个层,这样就会导致第二层无法获得第一层的输出,因为第一层的计算还没有完成。在每个时间步内,必须依次通过所有层的计算,将输出逐层传递下去。
    时间步之间存在时序上的依赖关系,当前时间步的计算需要使用前一个时间步的隐藏状态。因此,时间步的顺序在计算中非常重要。而层数之间是空间上的依赖,每一层的输出作为下一层的输入,但它不涉及到前后时间步的状态传递。

  • 3 代码中为什么要使用unsqueeze扩展outputs的第一个维度?
    RNN 模型的输出一般是形状为 (seq_len, batch_size, hidden_size) 的三维张量:

    • seq_len 表示序列的长度,即时间步数;
    • batch_size 表示批处理的大小;
    • hidden_size 表示每个时间步的隐藏状态的维度

注意:first_batch=True时候,输入输出是[Batch_size, seq_len,…]
若first_batch=False时候,输入输出是[seq_len, Batch_size, …]

当我们循环遍历时间步时,每个时间步的隐藏状态 h_t 都是一个二维的张量 (batch_size, hidden_size),如果直接将它添加到 outputs 列表中,会造成最后合并时的维度不匹配。unsqueeze(1) 这个操作的作用就是在第 1 维(即时间步维度的位置)上插入一个新的维度,使 h_t 的形状从 (batch_size, hidden_size) 变成 (batch_size, 1, hidden_size)。这样做之后,多个时间步的隐藏状态拼接在一起后,才能形成一个完整的三维张量,符合 RNN 的输出格式。

  • 4 RNN中的数学公式可以是多样的,除了我实现的这种,还有别的
    在这里插入图片描述在同一个权重矩阵 W中,将输入 𝑥 和前一隐藏状态 组合在一起进行处理。这种表达方式的背后逻辑是,将输入和前一隐藏状态拼接成一个向量,然后对它们应用同一个权重矩阵。

5. 关于一些参数的学习

num_layers 参数

num_layers 参数在 RNN 中的作用是控制模型的层数,也就是说,num_layers 决定了有多少个 RNN 层堆叠在一起。每一层 RNN 都会处理前一层的输出,形成更深层次的特征抽取。

具体说明
5-1. num_layers 的作用
  • 控制层数num_layers 设定了 RNN 堆叠的层数。例如,num_layers=1 表示模型只有一层 RNN,num_layers=2 表示模型有两层 RNN,依此类推。
  • 多层 RNN 的优势:多层 RNN 可以更好地捕捉复杂的时间序列特征和长距离依赖。每一层 RNN 会从上一层的输出中提取更高级的特征。
5-2. 如何理解
  • 单层 RNN
    如果 num_layers=1,RNN 只包含一层处理时间步数据的神经网络。每个时间步的输入会直接传递到这一层,隐藏状态也直接从这一层输出。

    输入序列 -> RNN层 -> 输出
    
  • 多层 RNN
    如果 num_layers=2,RNN 包含两层堆叠的神经网络。第一层的输出会作为第二层的输入进行处理。这种层次结构允许网络捕捉更复杂的特征。

    输入序列 -> RNN层1 -> RNN层2 -> 输出
    

5. 总结

  • hidden_size 是隐藏层的维度,控制每个时间步的隐藏状态的大小。
  • num_layers 代表 RNN 的层数,每层都可以捕捉不同层次的特征。
  • num_layers 参数在 RNN 中用来指定模型的层数。
    • 更高的 num_layers 可以使模型更深,从而有潜力捕捉更复杂的特征,但也可能增加计算开销和训练难度。

http://www.ppmy.cn/server/130462.html

相关文章

C++,STL 028(24.10.11)

内容&#xff1a;利用sort排序算法来对deque容器进行排序。 代码&#xff1a; #include <iostream> #include <deque> #include <algorithm> // 标准算法头文件using namespace std;void printDeque(const deque<int> &d) {for (deque<int>…

主线程与各工作线程的分工

1. 主线程&#xff1a; 配置读取资源的申请 原子变量的初始化&#xff1a;_Atomic 变量&#xff0c;C11标准引入&#xff1b;其初始化非线程安全&#xff0c;要顺序执行设置工作线程的全局启动开关ON工作线程的创建退出前&#xff0c;设置工作线程的全局启动开关OFF&#xff0…

C/C++进阶(一)--内存管理

更多精彩内容..... &#x1f389;❤️播主の主页✨&#x1f618; Stark、-CSDN博客 本文所在专栏&#xff1a; 学习专栏C语言_Stark、的博客-CSDN博客 其它专栏&#xff1a; 数据结构与算法_Stark、的博客-CSDN博客 ​​​​​​项目实战C系列_Stark、的博客-CSDN博客 座右铭&a…

[uni-app]小兔鲜-07订单+支付

订单模块 基本信息渲染 import type { OrderState } from /services/constants import type { AddressItem } from ./address import type { PageParams } from /types/global/** 获取预付订单 返回信息 */ export type OrderPreResult {/** 商品集合 [ 商品信息 ] */goods: …

【Python】操作列表

Python是一种功能强大的编程语言&#xff0c;它提供了丰富的操作列表的方法。列表是一种有序、可变的数据类型&#xff0c;可以存储任意类型的元素。下面是一些常用的操作列表的方法&#xff1a; 1. 创建列表&#xff1a;可以使用方括号 [] 或者 list() 函数来创建一个列表。例…

threejs-基础材质设置

一、介绍 主要内容&#xff1a;基础材质(贴图、高光、透明、环境、光照、环境遮蔽贴图) 主要属性&#xff1a; side: three.DoubleSide, //设置双面 color: 0xffffff, //颜色 map: texture, //纹理 transparent: true, // 透明度 aoMap: aoTexture, //ao贴图 aoMapIntensity: 1…

“探索端智能,加速大模型应用” 火山引擎边缘智能x扣子技术沙龙圆满落幕!

9月21日&#xff0c;火山引擎边缘智能扣子技术沙龙在上海圆满落地&#xff0c;沙龙以“探索端智能&#xff0c;加速大模型应用”为主题&#xff0c;边缘智能、扣子、地瓜机器人以及上海交通大学等多位重磅嘉宾出席&#xff0c;从多维视角探讨 AI、 AIoT、端侧大模型等技术与发展…

12.3 Linux_进程间通信_信号机制

概述 什么是信号&#xff1a; 信号是在软件层次上对中断机制的模拟&#xff08;软中断&#xff09;&#xff0c;是一种异步通信方式。 进程对信号的响应方式&#xff1a; 缺省方式&#xff1a;根据默认行为响应信号忽略信号&#xff1a;不响应信号捕捉信号&#xff1a;根据…