递归神经网络(RNN)详解
什么是递归神经网络?
递归神经网络(Recurrent Neural Network,简称RNN)是一种特殊的神经网络架构,其设计初衷是为了处理具有序列数据的任务。与传统的前馈神经网络不同,RNN的核心特性在于它能够“记住”之前的信息,并在处理当前输入时加以利用。
想象一下,你正在阅读一段文字,你的理解不仅依赖于当前的词语,还受到前面词语的影响。RNN的设计理念正是模拟这种人类处理序列信息的机制。
RNN的核心思想
RNN的关键在于它的“循环”结构。在每个时间步 t,网络会接收两个输入:
- 当前时刻的输入
xt
- 上一个时刻的隐藏状态
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)
这个隐藏状态
ht 可以看作是当前输入
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因其处理序列数据的能力,被广泛应用于各种领域:
*
文本生成: 根据给定的起始词生成连贯的句子。
*
机器翻译: 将一种语言的句子翻译成另一种语言。
*
情感分析: 判断一段文本的情感倾向(正面、负面等)。
*
命名实体识别 (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'])
textsequence = [['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}
vocab
size = len(vocab)
embeddingdim = 10
hidden
dim = 20
numlayers = 1
数据预处理
def text
toindices(text, vocab):
return [vocab.get(word, vocab['<UNK>']) for word in text]
填充序列
max
len = max(len(seq) for seq in textsequence)
padded
sequences = []
for seq in textsequence:
indices = text
toindices(seq, vocab)
padded
indices = indices + [vocab['<PAD>']] * (maxlen - len(indices))
padded
sequences.append(paddedindices)
转换为 PyTorch tensors
X = torch.tensor(padded
sequences, dtype=torch.long)
y = torch.tensor(labels, dtype=torch.long)
定义 RNN 模型
class SimpleRNN(nn.Module):
def init(self, vocabsize, embedding
dim, hiddendim, output
dim, nlayers):
super(SimpleRNN, self).
init()
self.embedding = nn.Embedding(vocab
size, embeddingdim)
self.rnn = nn.RNN(embedding
dim, hiddendim, n
layers, batchfirst=True)
self.fc = nn.Linear(hidden
dim, outputdim)
def forward(self, x):
embedded = self.embedding(x)
output, hidden = self.rnn(embedded) # output: (batch
size, seqlen, hidden
dim), hidden: (nlayers, batch
size, hiddendim)
# 取最后一个时间步的隐藏状态作为序列表示
# 或者对所有时间步的输出求平均
# 这里我们取最后一个时间步的隐藏状态
# hidden[-1] 表示最后一层的最后一个时间步的隐藏状态
# hidden.squeeze(0) 移除 n
layers 维度 (因为我们只有一层)
out = hidden.squeeze(0)
prediction = self.fc(out)
return prediction
实例化模型
model = SimpleRNN(vocabsize, embedding
dim, hiddendim, 2, num
layers) # 假设二分类任务
损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
训练循环
epochs = 100
for epoch in range(epochs):
model.train()
optimizer.zerograd()
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.no
grad():
testtext = ['how', 'are']
test
indices = textto
indices(testtext, vocab)
test
padded = testindices + [vocab['<PAD>']] * (max
len - len(testindices))
X
test = torch.tensor([testpadded], dtype=torch.long)
prediction = model(X
test)
predictedclass = torch.argmax(prediction, dim=