返回列表

递归神经网络(RNN)详解

发布于 ·

递归神经网络(RNN)详解

什么是递归神经网络?

递归神经网络(Recurrent Neural Network,简称RNN)是一种特殊的神经网络架构,其设计初衷是为了处理具有序列数据的任务。与传统的前馈神经网络不同,RNN的核心特性在于它能够“记住”之前的信息,并在处理当前输入时加以利用。

想象一下,你正在阅读一段文字,你的理解不仅依赖于当前的词语,还受到前面词语的影响。RNN的设计理念正是模拟这种人类处理序列信息的机制。

RNN的核心思想

RNN的关键在于它的“循环”结构。在每个时间步 t,网络会接收两个输入:

  1. 当前时刻的输入 xt

  2. 上一个时刻的隐藏状态 h{t-1}

然后,它会计算出当前时刻的隐藏状态 ht 和输出 yt

ht = f(Wh  h{t-1} + Wx  xt + b)
yt = g(V * ht + c)

其中:

  • Wh, Wx, V 是权重矩阵

  • b, c 是偏置项

  • f()g() 是激活函数(如tanh, ReLU)

这个隐藏状态 h
t 可以看作是当前输入 xt 和之前所有输入历史的一个压缩表示。通过这种方式,RNN能够将过去的信息传递到未来。

可视化结构 (Unrolled RNN):

Input Sequence:    x1   x2   x3   ...   xT
                    |     |     |           |
                   [RNN] [RNN] [RNN] ... [RNN]
                    |     |     |           |
Hidden State:      h0 -> h1 -> h2 -> ... -> hT
                    |     |     |           |
Output Sequence:   y1   y2   y3   ...   yT

在展开图中,我们可以看到每个RNN单元都共享相同的参数 (Wh, Wx, V),这使得模型能够学习序列的模式。

RNN的应用场景

RNN因其处理序列数据的能力,被广泛应用于各种领域:

  • 自然语言处理 (NLP):
* 文本生成: 根据给定的起始词生成连贯的句子。 * 机器翻译: 将一种语言的句子翻译成另一种语言。 * 情感分析: 判断一段文本的情感倾向(正面、负面等)。 * 命名实体识别 (NER): 识别文本中的人名、地名、组织名等。 * 语音识别: 将音频信号转换为文字。
  • 时间序列预测:
* 股票价格预测 * 天气预测 * 能源消耗预测
  • 其他:
* 音乐生成: 创作新的音乐片段。 * 手写识别: 识别手写数字或字符。

RNN的局限性

尽管RNN非常强大,但它也存在一些固有的问题,尤其是在处理长序列时:

1. 梯度消失/爆炸 (Vanishing/Exploding Gradients)

这是RNN面临的最主要挑战之一。当我们使用反向传播算法来更新权重时,梯度会通过时间步进行链式相乘。如果这些权重小于1,梯度会指数级衰减,导致早期时间步的权重几乎无法更新(梯度消失);反之,如果大于1,梯度会指数级增长,导致训练不稳定(梯度爆炸)。这使得RNN难以捕捉长期依赖关系。

2. 无法并行计算

由于每个时间步的隐藏状态都依赖于前一个时间步,因此RNN必须按顺序处理序列。这限制了其在现代GPU/TPU上的并行化能力,使得训练速度相对较慢。

改进:LSTM与GRU

为了克服RNN的局限性,研究者们提出了两种主要的变体:长短期记忆网络(LSTM)和门控循环单元(GRU)。

LSTM (Long Short-Term Memory)

LSTM引入了所谓的“记忆细胞”(cell state)和三个“门”(gate):输入门、遗忘门和输出门。这些门控制着信息在细胞状态中的流动,允许网络选择性地记住或忘记信息,从而有效缓解了梯度消失问题,并能够学习更复杂的长期依赖关系。

LSTM的关键组件:

  • 记忆细胞 (Cell State): 一条贯穿整个网络的主线,负责长期记忆。

  • 遗忘门 (Forget Gate): 决定哪些信息应该从细胞状态中被丢弃。

  • 输入门 (Input Gate): 决定哪些新信息应该被存储在细胞状态中。

  • 输出门 (Output Gate): 决定基于细胞状态,输出什么值。

GRU (Gated Recurrent Unit)

GRU是LSTM的一种简化版本,它结合了LSTM的遗忘门和输入门的功能,并移除了独立的细胞状态。GRU通常比LSTM更简单、更快,并且在许多任务上表现相当甚至更好。

GRU的关键组件:

  • 重置门 (Reset Gate): 决定是否忽略过去的隐藏状态。

  • 更新门 (Update Gate): 决定多少过去的隐藏状态应该保留,以及多少新的候选隐藏状态应该被引入。

代码示例 (PyTorch)

下面是一个简单的RNN示例,用于文本分类。

```python
import torch
import torch.nn as nn
import torch.optim as optim

假设我们有一些简单的数据

文本序列 (例如: ['hello', 'world', 'how', 'are', 'you'])

text
sequence = [['hello', 'world'], ['how', 'are', 'you']]

对应的标签 (例如: [0, 1])

labels = [0, 1]

词汇表

vocab = {'<PAD>': 0, '<UNK>': 1, 'hello': 2, 'world': 3, 'how': 4, 'are': 5, 'you': 6} vocabsize = len(vocab) embeddingdim = 10 hiddendim = 20 numlayers = 1

数据预处理

def texttoindices(text, vocab): return [vocab.get(word, vocab['<UNK>']) for word in text]

填充序列

maxlen = max(len(seq) for seq in textsequence) paddedsequences = [] for seq in textsequence: indices = texttoindices(seq, vocab) paddedindices = indices + [vocab['<PAD>']] * (maxlen - len(indices)) paddedsequences.append(paddedindices)

转换为 PyTorch tensors

X = torch.tensor(paddedsequences, dtype=torch.long) y = torch.tensor(labels, dtype=torch.long)

定义 RNN 模型

class SimpleRNN(nn.Module): def init(self, vocab
size, embeddingdim, hiddendim, outputdim, nlayers): super(SimpleRNN, self).init() self.embedding = nn.Embedding(vocabsize, embeddingdim) self.rnn = nn.RNN(embeddingdim, hiddendim, nlayers, batchfirst=True) self.fc = nn.Linear(hiddendim, outputdim) def forward(self, x): embedded = self.embedding(x) output, hidden = self.rnn(embedded) # output: (batchsize, seqlen, hiddendim), hidden: (nlayers, batchsize, hiddendim) # 取最后一个时间步的隐藏状态作为序列表示 # 或者对所有时间步的输出求平均 # 这里我们取最后一个时间步的隐藏状态 # hidden[-1] 表示最后一层的最后一个时间步的隐藏状态 # hidden.squeeze(0) 移除 nlayers 维度 (因为我们只有一层) out = hidden.squeeze(0) prediction = self.fc(out) return prediction

实例化模型

model = SimpleRNN(vocab
size, embeddingdim, hiddendim, 2, numlayers) # 假设二分类任务

损失函数和优化器

criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=0.001)

训练循环

epochs = 100 for epoch in range(epochs): model.train() optimizer.zero
grad() outputs = model(X) loss = criterion(outputs, y) loss.backward() optimizer.step() if (epoch+1) % 20 == 0: print(f'Epoch [{epoch+1}/{epochs}], Loss: {loss.item():.4f}')

简单测试

model.eval() with torch.nograd(): testtext = ['how', 'are'] testindices = texttoindices(testtext, vocab) testpadded = testindices + [vocab['<PAD>']] * (maxlen - len(testindices)) Xtest = torch.tensor([testpadded], dtype=torch.long) prediction = model(Xtest) predictedclass = torch.argmax(prediction, dim=