一、RNN基本思想与核心概念
1.1 为什么需要RNN?
传统神经网络(如全连接网络、CNN)无法处理序列数据,因为它们:
无记忆性:每个输入独立处理,忽略序列中元素的时间/顺序关系
固定输入尺寸:无法处理可变长度的序列
无法建模时间依赖:比如语言中"我今天吃了__"的预测需要前面信息
RNN的突破:引入"记忆"概念,让网络可以记住之前的信息来处理当前输入。
1.2 核心思想:参数共享与循环连接
python
传统网络:y_t = f(x_t)
RNN: y_t = f(x_t, h_{t-1})
h:隐藏状态(记忆单元)
t:时间步
每个时间步使用相同的权重参数
二、RNN基本原理与数学推导
2.1 基础RNN结构
python
输入序列:X = [x_1, x_2, ..., x_T]
输出序列:Y = [y_1, y_2, ..., y_T]
隐藏状态:H = [h_1, h_2, ..., h_T]
前向传播公式:
python
# 时间步 t 的计算
h_t = tanh(W_hh * h_{t-1} + W_xh * x_t + b_h) # 更新隐藏状态
y_t = W_hy * h_t + b_y # 输出
# 或者写成矩阵形式
h_t = tanh(
[W_hh | W_xh] * [h_{t-1}; x_t] + b_h
)
y_t = W_hy * h_t + b_y
2.2 展开计算图(Unrolled)
python
时间步1: x1 → h1 = f(W*h0 + U*x1) → y1
↓
时间步2: x2 → h2 = f(W*h1 + U*x2) → y2
↓
时间步3: x3 → h3 = f(W*h2 + U*x3) → y3
W: 隐藏状态权重 (h_t-1 → h_t)
U: 输入权重 (x_t → h_t)
V: 输出权重 (h_t → y_t)
关键:所有时间步共享相同的权重参数 W, U, V
2.3 RNN的三种基本模式
"""
-
一对多 (One-to-Many)
输入:单一样本 → 输出:序列
应用:图像描述生成
-
多对一 (Many-to-One)
输入:序列 → 输出:单一样本
应用:情感分析、文本分类
-
多对多 (Many-to-Many)
输入:序列 → 输出:序列
应用:机器翻译、序列标注
-
编码器-解码器 (Encoder-Decoder)
输入:序列 → 编码 → 解码 → 输出序列
应用:seq2seq任务
"""
三、RNN的详细数学原理
3.1 前向传播完整公式
对于一个包含T个时间步的序列:
python
# 初始化隐藏状态
h_0 = zeros() # 通常初始化为0向量
# 对每个时间步 t = 1 to T
for t in range(1, T+1):
# 组合输入和上一时刻隐藏状态
a_t = W_hh * h_{t-1} + W_xh * x_t + b_h
# 应用激活函数(通常用tanh)
h_t = tanh(a_t)
# 计算输出(如果需要)
o_t = W_hy * h_t + b_y
# 应用输出激活函数(如softmax用于分类)
y_t = softmax(o_t)
3.2 激活函数的选择
python
"""
tanh: 范围(-1, 1),最常用,梯度消失问题较轻
sigmoid: 范围(0, 1),用于门控机制
ReLU: 范围(0, ∞),可能梯度爆炸
"""
3.3 损失函数
对于序列预测任务,总损失是各时间步损失之和:
python
L = Σ_{t=1}^T L_t(y_t, ŷ_t)
其中ŷ_t是真实标签,y_t是预测值。
四、RNN的训练:BPTT算法
4.1 BPTT(Backpropagation Through Time)原理
BPTT是标准反向传播在时间维度上的扩展:
python
# 反向传播计算梯度
def bptt(self, x, y):
"""
1. 前向传播计算所有h_t
2. 计算总损失 L = Σ L_t
3. 反向传播计算梯度
"""
# 损失对W_hy的梯度
dL/dW_hy = Σ_t (dL_t/dy_t * dy_t/dW_hy)
# 损失对W_hh的梯度(复杂!)
dL/dW_hh = Σ_t (dL/dh_t * dh_t/dW_hh)
# 关键:dh_t/dW_hh 依赖于所有之前的时间步
# 需要应用链式法则展开
4.2 梯度计算详细推导
考虑损失对W_hh的梯度:
python
∂L/∂W_hh = Σ_{t=1}^T ∂L/∂h_t * ∂h_t/∂W_hh
但 ∂h_t/∂W_hh 依赖于 h_{t-1}, h_{t-2}, ..., h_1
因此需要展开:
∂h_t/∂W_hh = Σ_{k=1}^t (∂h_t/∂h_k * ∂h_k/∂W_hh)
其中 ∂h_t/∂h_k = Π_{i=k}^{t-1} ∂h_{i+1}/∂h_i
= Π_{i=k}^{t-1} W_hh^T * diag(tanh'(a_i))
4.3 梯度消失与梯度爆炸问题
梯度消失(Vanishing Gradient)
python
# 原因:长期依赖梯度衰减
∂h_t/∂h_k = Π_{i=k}^{t-1} ∂h_{i+1}/∂h_i
= Π_{i=k}^{t-1} W_hh^T * diag(tanh'(a_i))
# 如果 |W_hh| < 1 且 tanh'(a_i) ≤ 1
# 那么随着 (t-k) 增大,乘积趋近于0
梯度爆炸(Exploding Gradient)
python
# 原因:梯度指数增长
# 如果 |W_hh| > 1,梯度会指数爆炸
# 解决方法:梯度裁剪(Gradient Clipping)
if grad_norm > threshold:
grad = grad * (threshold / grad_norm)
五、RNN变体与改进
5.1 LSTM(长短期记忆网络)
python
"""
解决梯度消失问题的经典方案
核心:引入门控机制和细胞状态
"""
# LSTM的三个门
i_t = σ(W_i * [h_{t-1}, x_t] + b_i) # 输入门
f_t = σ(W_f * [h_{t-1}, x_t] + b_f) # 遗忘门
o_t = σ(W_o * [h_{t-1}, x_t] + b_o) # 输出门
# 候选细胞状态
C̃_t = tanh(W_C * [h_{t-1}, x_t] + b_C)
# 更新细胞状态(关键!)
C_t = f_t ⊙ C_{t-1} + i_t ⊙ C̃_t
# 更新隐藏状态
h_t = o_t ⊙ tanh(C_t)
"""
优势:
1. 遗忘门控制记忆保留
2. 细胞状态C_t的线性路径避免梯度消失
3. 门控机制实现长期依赖
"""
5.2 GRU(门控循环单元)
python
"""
LSTM的简化版,只有两个门
计算效率更高
"""
# GRU的两个门
z_t = σ(W_z * [h_{t-1}, x_t]) # 更新门
r_t = σ(W_r * [h_{t-1}, x_t]) # 重置门
# 候选隐藏状态
h̃_t = tanh(W * [r_t ⊙ h_{t-1}, x_t])
# 更新隐藏状态
h_t = (1 - z_t) ⊙ h_{t-1} + z_t ⊙ h̃_t
"""
特点:
1. 更新门决定保留多少旧信息
2. 重置门决定使用多少历史信息
3. 参数比LSTM少,训练更快
"""
5.3 双向RNN(BiRNN)
python
"""
同时考虑过去和未来的上下文
"""
# 前向RNN
h_t_forward = RNN_forward(x_t, h_{t-1}_forward)
# 反向RNN
h_t_backward = RNN_backward(x_t, h_{t+1}_backward)
# 组合两个方向的隐藏状态
h_t = [h_t_forward; h_t_backward]
"""
应用:需要完整上下文的任务
如:命名实体识别、语音识别
"""
六、PyTorch中的RNN实现
6.1 基础RNN层
python
import torch
import torch.nn as nn
# 创建RNN层
rnn = nn.RNN(
input_size=10, # 输入特征维度
hidden_size=20, # 隐藏状态维度
num_layers=2, # RNN层数
batch_first=True, # 输入形状为(batch, seq, feature)
bidirectional=False,# 是否双向
nonlinearity='tanh' # 激活函数
)
# 输入数据
batch_size = 3
seq_len = 5
input_size = 10
x = torch.randn(batch_size, seq_len, input_size)
# 初始隐藏状态
h0 = torch.zeros(2, batch_size, 20) # (num_layers, batch, hidden_size)
# 前向传播
output, hn = rnn(x, h0)
print(f"输出形状: {output.shape}") # (3, 5, 20)
print(f"最后隐藏状态: {hn.shape}") # (2, 3, 20)
6.2 LSTM实现
python
# 创建LSTM层
lstm = nn.LSTM(
input_size=10,
hidden_size=20,
num_layers=2,
batch_first=True,
bidirectional=True # 双向LSTM
)
# 输入
x = torch.randn(batch_size, seq_len, input_size)
# 初始状态(LSTM需要两个状态)
h0 = torch.zeros(4, batch_size, 20) # (num_layers*2, batch, hidden_size)
c0 = torch.zeros(4, batch_size, 20) # 细胞状态
# 前向传播
output, (hn, cn) = lstm(x, (h0, c0))
print(f"双向LSTM输出形状: {output.shape}") # (3, 5, 40) hidden_size*2
7、总结
RNN的核心贡献是引入了时间维度上的记忆机制,使得神经网络能够处理序列数据:
基本结构:通过隐藏状态传递历史信息
训练方法:BPTT算法在时间维度上反向传播
主要问题:梯度消失/爆炸,通过LSTM/GRU缓解
应用场景:NLP、时间序列、语音识别等序列任务
PyTorch实现:提供了RNN、LSTM、GRU等现成模块