概述

循环神经网络(Recurrent Neural Network, RNN)是一类专门用于处理序列数据的神经网络。与前馈神经网络不同,RNN 具有隐藏状态,能够记住之前的信息并将其用于当前时刻的计算。1

RNN 的核心思想

RNN 的”循环”体现在:同一个网络层在每个时间步被重复使用,权重共享使得网络能够处理任意长度的序列。

时间步 t-1      时间步 t      时间步 t+1
    │              │              │
    ▼              ▼              ▼
┌────────┐    ┌────────┐    ┌────────┐
│  x_{t-1}│    │  x_t   │    │ x_{t+1}│
└────┬───┘    └────┬───┘    └────┬───┘
     │             │             │
     ▼             ▼             ▼
┌─────────┐   ┌─────────┐   ┌─────────┐
│ h_{t-1} │   │   h_t    │   │ h_{t+1} │
└─────────┘   └─────────┘   └─────────┘

RNN 的结构

基本 RNN 单元

RNN 的核心方程:

其中:

  • :时刻 的输入
  • :时刻 的隐藏状态
  • :输入到隐藏的权重矩阵
  • :隐藏到隐藏的权重矩阵(循环权重)
  • :非线性激活函数(如

PyTorch 实现

import torch
import torch.nn as nn
 
class SimpleRNN(nn.Module):
    """基础RNN实现"""
    def __init__(self, input_size, hidden_size, output_size):
        super().__init__()
        self.hidden_size = hidden_size
        
        # 输入到隐藏
        self.W_xh = nn.Linear(input_size, hidden_size)
        # 隐藏到隐藏(循环连接)
        self.W_hh = nn.Linear(hidden_size, hidden_size, bias=False)
        # 隐藏到输出
        self.W_hy = nn.Linear(hidden_size, output_size)
        
        self.tanh = nn.Tanh()
    
    def forward(self, x, h_prev=None):
        """
        Args:
            x: (batch_size, input_size) 或 (batch_size, seq_len, input_size)
            h_prev: (batch_size, hidden_size) 初始隐藏状态
        Returns:
            output: (batch_size, output_size)
            h: (batch_size, hidden_size)
        """
        if h_prev is None:
            h_prev = torch.zeros(x.size(0), self.hidden_size, device=x.device)
        
        # RNN 核心方程
        h = self.tanh(self.W_xh(x) + self.W_hh(h_prev))
        y = self.W_hy(h)
        
        return y, h

多层 RNN

class MultiLayerRNN(nn.Module):
    """多层RNN"""
    def __init__(self, input_size, hidden_size, num_layers, output_size, dropout=0.0):
        super().__init__()
        self.num_layers = num_layers
        
        self.rnn_layers = nn.ModuleList()
        self.rnn_layers.append(nn.RNN(input_size, hidden_size, batch_first=True))
        
        for _ in range(num_layers - 1):
            self.rnn_layers.append(nn.RNN(hidden_size, hidden_size, batch_first=True))
        
        self.dropout = nn.Dropout(dropout)
        self.fc = nn.Linear(hidden_size, output_size)
    
    def forward(self, x, h0=None):
        """
        Args:
            x: (batch_size, seq_len, input_size)
        Returns:
            output: (batch_size, seq_len, output_size)
            h_n: (num_layers, batch_size, hidden_size)
        """
        if h0 is None:
            h0 = [None] * self.num_layers
        
        h_n = []
        for i, layer in enumerate(self.rnn_layers):
            x, h = layer(x, h0[i])
            if i < self.num_layers - 1:
                x = self.dropout(x)
            h_n.append(h)
        
        # 只取最后一个时间步的输出
        output = self.fc(x)
        
        return output, torch.stack(h_n)

前向传播

单步前向传播

对于单个时间步

def rnn_step_forward(x_t, h_prev, params):
    """
    单步前向传播
    
    Args:
        x_t: (hidden_size,) 当前输入
        h_prev: (hidden_size,) 上一时刻隐藏状态
        params: 参数字典
    Returns:
        h: (hidden_size,) 当前隐藏状态
        cache: 用于反向传播的缓存
    """
    W_xh = params['W_xh']
    W_hh = params['W_hh']
    b = params['b']
    
    # 循环连接的核心
    h_raw = np.dot(W_xh, x_t) + np.dot(W_hh, h_prev) + b
    h = np.tanh(h_raw)  # 激活函数
    
    cache = (x_t, h_prev, h_raw, W_xh, W_hh)
    return h, cache

序列前向传播

def rnn_forward(x, h0, params):
    """
    完整序列的前向传播
    
    Args:
        x: (seq_len, input_size) 输入序列
        h0: (hidden_size,) 初始隐藏状态
    Returns:
        h: (seq_len, hidden_size) 所有时间步的隐藏状态
        cache: 反向传播所需的缓存
    """
    seq_len, input_size = x.shape
    hidden_size = h0.shape[0]
    
    h = np.zeros((seq_len, hidden_size))
    cache = []
    
    h_prev = h0
    for t in range(seq_len):
        h[t], cache_t = rnn_step_forward(x[t], h_prev, params)
        h_prev = h[t]
        cache.append(cache_t)
    
    return h, cache

反向传播 Through Time(BPTT)

梯度推导

RNN 的反向传播称为 BPTT(Backpropagation Through Time),因为需要将误差沿着时间反向传播。

设损失函数为 ,对每个参数求偏导:

梯度消失与梯度爆炸

核心问题:BPTT 中需要计算雅可比矩阵的乘积

梯度消失:当 时,梯度指数衰减

梯度爆炸:当 时,梯度指数增长

BPTT 实现

def rnn_step_backward(dh_next, cache):
    """
    单步反向传播
    
    Args:
        dh_next: (hidden_size,) 从下一时间步传来的梯度
        cache: 前向传播时保存的缓存
    Returns:
        dx: (input_size,) 对输入的梯度
        dh_prev: (hidden_size,) 对上一时刻隐藏状态的梯度
        grads: 权重梯度字典
    """
    x_t, h_prev, h_raw, W_xh, W_hh = cache
    
    # tanh 的梯度: d(tanh(x))/dx = 1 - tanh²(x)
    dh_raw = dh_next * (1 - h_raw**2)
    
    # 权重梯度
    dW_xh = np.outer(dh_raw, x_t)
    dW_hh = np.outer(dh_raw, h_prev)
    db = dh_raw
    
    # 对输入和上一隐藏状态的梯度
    dx = np.dot(W_xh.T, dh_raw)
    dh_prev = np.dot(W_hh.T, dh_raw)
    
    grads = {'W_xh': dW_xh, 'W_hh': dW_hh, 'b': db}
    return dx, dh_prev, grads
 
 
def bptt_forward_backward(x, h0, params, target):
    """
    完整的 BPTT
    
    Args:
        x: (seq_len, input_size) 输入序列
        h0: (hidden_size,) 初始隐藏状态
        params: 参数字典
        target: (seq_len, output_size) 目标序列
    Returns:
        loss: 损失值
        grads: 梯度字典
    """
    seq_len = x.shape[0]
    
    # 前向传播
    h, forward_cache = rnn_forward(x, h0, params)
    
    # 计算损失(简化:MSE)
    loss = np.sum((h - target)**2) / 2
    
    # 初始化梯度
    grads = {k: np.zeros_like(v) for k, v in params.items()}
    
    # 初始化 dh_next(从损失对最终隐藏状态的梯度)
    dh_next = h[-1] - target[-1]  # 假设MSE损失
    
    # 反向传播(从最后一个时间步到第一个)
    for t in reversed(range(seq_len)):
        cache = forward_cache[t]
        dx, dh_prev, step_grads = rnn_step_backward(dh_next, cache)
        
        # 累加梯度
        for k in grads:
            grads[k] += step_grads.get(k, 0)
        
        # 继续反向传播到更早的时间步
        dh_next = dh_prev
    
    return loss, grads

梯度问题与解决方案

梯度裁剪

防止梯度爆炸的最简单有效的方法:

def clip_gradients(grads, max_norm=5.0):
    """梯度裁剪"""
    total_norm = 0.0
    for g in grads.values():
        total_norm += np.sum(g**2)
    total_norm = np.sqrt(total_norm)
    
    clip_coef = max_norm / (total_norm + 1e-6)
    if clip_coef < 1:
        for k in grads:
            grads[k] *= clip_coef
    
    return grads

梯度裁剪的 PyTorch 实现

# 方法1:按范数裁剪
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
 
# 方法2:按值裁剪
torch.nn.utils.clip_grad_value_(model.parameters(), clip_value=1.0)

截断 BPTT(Truncated BPTT)

处理超长序列的实用技巧:

class TruncatedBPTTRNN(nn.Module):
    """截断BPTT的RNN"""
    def __init__(self, input_size, hidden_size, output_size, seq_len=50):
        super().__init__()
        self.rnn = nn.RNN(input_size, hidden_size, batch_first=True)
        self.fc = nn.Linear(hidden_size, output_size)
        self.seq_len = seq_len
    
    def forward(self, x, h=None):
        """
        处理超长序列,使用截断BPTT
        """
        batch_size, total_len, input_size = x.shape
        
        # 分段处理
        h_cur = h
        outputs = []
        
        for start in range(0, total_len, self.seq_len):
            end = min(start + self.seq_len, total_len)
            segment = x[:, start:end, :]
            
            out, h_cur = self.rnn(segment, h_cur)
            
            # detach 梯度,防止反向传播到之前的段
            h_cur = h_cur.detach()
            
            outputs.append(out)
        
        # 合并所有段的输出
        output = torch.cat(outputs, dim=1)
        return self.fc(output), h_cur

RNN 的应用场景

任务类型输入输出典型应用
Many-to-One序列单个值情感分析、文本分类
One-to-Many单个值序列图像描述生成
Many-to-Many序列序列机器翻译、语音识别
同步 Many-to-Many序列序列帧级视频分类

情感分类示例

class SentimentRNN(nn.Module):
    """基于RNN的情感分类器"""
    def __init__(self, vocab_size, embedding_dim, hidden_size, num_classes=2):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, embedding_dim)
        self.rnn = nn.RNN(embedding_dim, hidden_size, batch_first=True)
        self.fc = nn.Linear(hidden_size, num_classes)
    
    def forward(self, x):
        """
        Args:
            x: (batch_size, seq_len) 词索引序列
        Returns:
            logits: (batch_size, num_classes)
        """
        # 词嵌入
        embedded = self.embedding(x)  # (batch, seq_len, embed_dim)
        
        # RNN 前向传播
        output, hidden = self.rnn(embedded)  # output: (batch, seq_len, hidden)
        
        # 取最后一个时间步的隐藏状态
        final_hidden = output[:, -1, :]  # (batch, hidden)
        
        # 分类
        logits = self.fc(final_hidden)
        return logits

RNN 的局限性

1. 长期依赖问题

RNN 难以学习相距很远的时间步之间的依赖关系,因为梯度在反向传播时会指数衰减。

2. 训练困难

  • 梯度爆炸:需要梯度裁剪
  • 梯度消失:需要特殊架构(LSTM、GRU)

3. 并行化困难

序列的时序依赖使得 GPU 并行化效率不高。

4. 表达能力有限

标准 RNN 的隐藏状态容量有限,无法记住大量信息。


从 RNN 到 LSTM

LSTM(Long Short-Term Memory)通过引入门控机制解决了 RNN 的长期依赖问题。详见 LSTM 详解

LSTM 的关键创新

  • 细胞状态(Cell State):信息传输的”传送带”
  • 门控机制:遗忘门、输入门、输出门
  • 短期/长期记忆分离:更好地处理长期依赖

参考


相关阅读

Footnotes

  1. Goodfellow, I., Bengio, Y., & Courville, A. (2016). Deep Learning. MIT Press.