前言
笔者写下此系列文章是希望在复习人工智能相关知识同时为想学此技术的人提供一定帮助。
图源网络,所有者可随时联系笔者删除。
代码不代表全部实现,只是为展示模型的关键结构。
与CNN不同,RNN被设计用来处理序列数据。它通过在网络的隐藏层中引入循环,使网络能够保留前一个状态的信息,并将这些信息用于当前状态的计算。这种设计使RNN特别适合处理语言翻译、自然语言处理、语音识别等需要理解数据序列中时间相关性的任务。
正文
RNN的多层结构
我们知道CNN模型的多层结构大致是这样的
代码上看,对于如下的两层卷积一层全连接的简单CNN模型定义如下:
def __init__(self):super(SimpleCNN, self).__init__()# 第一个卷积层, 输入通道数1, 输出通道数16, 卷积核大小3x3self.conv1 = nn.Conv2d(1, 16, kernel_size=3, stride=1, padding=1)# 第二个卷积层, 输入通道数16, 输出通道数32, 卷积核大小3x3self.conv2 = nn.Conv2d(16, 32, kernel_size=3, stride=1, padding=1)# 最大池化层, 使用2x2窗口进行下采样self.pool = nn.MaxPool2d(kernel_size=2, stride=2)# 全连接层, 32 * 7 * 7 输入特征数, 10输出特征数 (10类分类,28 X 28)self.fc = nn.Linear(32 * 7 * 7, 10)
前向传播时,我们只需要如下操作(Pytorch,TF会完成后向传播)
def forward(self, x):# 通过第一个卷积层后激活x = F.relu(self.conv1(x))# 通过池化层x = self.pool(x)# 通过第二个卷积层后激活x = F.relu(self.conv2(x))# 通过池化层x = self.pool(x)# 展平特征图以供全连接层使用x = x.view(-1, 32 * 7 * 7)# 通过全连接层x = self.fc(x)return x
符合直觉,再来看看RNN我们会怎么做。
def __init__(self, input_size, hidden_size, num_layers, output_size):super(SimpleRNN, self).__init__()self.hidden_size = hidden_sizeself.num_layers = num_layers# RNN 层self.rnn = nn.RNN(input_size, hidden_size, num_layers, batch_first=True)# 输出层self.fc = nn.Linear(hidden_size, output_size)
nn.RNN
是 PyTorch 中的 RNN 层,我们设置它的输入尺寸、隐藏状态尺寸、层数,以及是否批量处理输入数据。
def forward(self, x):# 初始化隐藏状态h0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size)# 前向传播RNNout, _ = self.rnn(x, h0)# 取最后一时间步out = out[:, -1, :]out = self.fc(out)return out
全连接层 (self.fc
) 用于 RNN 的最后一个时间步的隐藏状态转换为输出。
最后一个时间步的隐藏状态是序列中的最后一个元素或时刻,每一个时间步都有其对应的数据输入。在循环神经网络中,网络会逐个时间步地处理整个序列,为每一个时间步生成一个输出。
对于很多任务来说,通常只需要序列的最终状态,因为在循环神经网络中,每个时间步的隐藏状态包含了到目前为止的序列信息,因此最后一个时间步的隐藏状态上包含了整个序列的信息。
从架构图上看,它是这样来堆叠多层的。
RNN 变种之GRU门控循环单元
GRU的提出主要是为了解决RNN在长距离依赖问题上的挑战,这个问题也被称为梯度消失或梯度爆炸问题,具体的说,如下为RNN隐藏状态梯度的通项公式,当时间步数T较大或者时间步t较小,将产生梯度衰减和梯度爆炸问题,不利于训练模型。
为了解决这些问题,GRU引入了两个关键的门控机制:更新门(update gate)和重置门(reset gate)。
-
更新门用于控制前一个状态到当前状态的信息量。它决定了有多少之前的信息需要保留,以及有多少新的信息需要加入。通过这种方式,GRU能够在必要时保留长期信息,从而有效地缓解梯度消失问题。
- zt 是时刻 t 的更新门,用于决定保留多少旧状态信息。
- Wz 是更新门的权重参数。
- σ 表示sigmoid函数,将输入压缩到0和1之间,以便作为门控信号。
- [ht−1,xt] 表示ht−1和xt 的连接。
-
重置门则控制了多少之前的信息需要忘记,这使得模型能够根据新的输入丢弃不相关的状态信息。这有助于模型更好地处理输入序列中的变化,使其对于序列中的重要事件更加敏感。
- rt 是时刻 t 的重置门,用于决定有多少过去的信息需要被忘记。
- Wr 是重置门的权重参数。
- [ht−1,xt] 表示ht−1和xt 的连接。
候选隐藏状态(Candidate Hidden State)
候选隐藏状态提供了一种可能的新状态,其基于当前的输入 xt 和通过重置门调整过的前一时刻的隐藏状态rt∗ht−1得出,候选隐藏状态的计算考虑了当前输入和经重置门修改后的上一时刻隐藏状态。如果重置门接近0,那么旧的信息会被忽略,候选状态几乎完全基于当前的输入,允许模型在需要的时候快速忘记无关的过去信息。
- h~t 是时刻 t 的候选隐藏状态,它包含了在当前时刻可能需要加入到真正隐藏状态中的新信息。
- W 是候选隐藏状态的权重参数。
- rt∗ht−1 表示重置门控制后的隐藏状态,其中的 ∗ 表示逐元素乘法。
- tanhtanh 函数帮助将数据压缩到 −1 和 1之间,帮助控制梯度流。
最终隐藏状态(Final Hidden State)
最终隐藏状态是根据更新门的输出决定的,它决定了从上一时刻隐藏状态 ht−1 到当前时刻 t 的隐藏状态 ht 应该保留多少信息,以及候选隐藏状态 h~t 应该贡献多少新信息,通过更新门,GRU在候选隐藏状态(代表了新信息)和前一隐藏状态(代表了旧信息)之间做权衡。如果更新门值接近1,意味着保留更多旧信息;如果接近0,意味着采纳更多新信息。
- ht 是时刻 t 的最终隐藏状态,它通过更新门 zt 来融合之前的隐藏状态 ht−1 和当前的候选隐藏状态 h~t。
- 这一步骤使GRU能够在保留长期依赖信息的同时,也加入新的信息,解决了传统RNN中的梯度消失问题。
由于现在框架发展成熟,调用只需如下,分别提供了两种主流框架代码:
import tensorflow as tfmodel = tf.keras.Sequential([# 输入数据维度是 (None, 10, 64)# None代表批次大小,10代表序列长度,64代表每个时间步的特征维度tf.keras.layers.GRU(256, return_sequences=True, input_shape=(10, 64)),tf.keras.layers.GRU(128),tf.keras.layers.Dense(10, activation='softmax')
])model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])
model.summary()
两个GRU层来处理序列数据,其中第一个GRU层返回整个序列的输出供下一个GRU层使用,Dense层分类。
import torch
import torch.nn as nnclass GRUNet(nn.Module):def __init__(self):super(GRUNet, self).__init__()self.gru1 = nn.GRU(input_size=64, hidden_size=256, num_layers=1, batch_first=True)self.gru2 = nn.GRU(input_size=256, hidden_size=128, num_layers=1, batch_first=True)self.fc = nn.Linear(128, 10)def forward(self, x):# x shape [batch_size, sequence_length, feature_size]out, _ = self.gru1(x)out, _ = self.gru2(out)# 取序列的最后一个时间步out = out[:, -1, :]out = self.fc(out)return outmodel = GRUNet()
print(model)
两个GRU层和一个全连接层,全连接层10分类。