RNN的理解

发布于:2024-10-16 ⋅ 阅读:(10) ⋅ 点赞:(0)

如果你不想使用 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 的代码示例:

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_size
        self.hidden_size = hidden_size
        self.layers_size = layers_size
        self.first_batch = first_batch

        self.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_t
                x = h_t
            outputs.append(h_t.unsqueeze(1))
        outputs = torch.cat(outputs, dim=1)
        return outputs, hidden
4. 代码详解
  1. 初始化
 def __init__(self, input_size, hidden_size, layers_size,first_batch=False):
        super(MyRNN, self).__init__()
        self.input_size = input_size
        self.hidden_size = hidden_size
        self.layers_size = layers_size
        self.first_batch = first_batch

        self.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. 前向传播

    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_t
                x = h_t
            outputs.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. 示例运行

# 示例:定义参数并运行模型
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)

运行结果:

输入的数据形状: 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 可以使模型更深,从而有潜力捕捉更复杂的特征,但也可能增加计算开销和训练难度。