RNN(循环神经网络)原理与结构

6 阅读2分钟

1 RNN(循环神经网络)原理与结构

循环神经网络(Recurrent Neural Network, RNN)是一类专门用于处理序列数据(如时间序列、文本、语音等)的深度学习模型。与传统的前馈神经网络不同,RNN在每个时间步都会将前一时刻的隐藏状态(hidden state)作为输入之一,从而能够保留和传递历史信息,捕捉序列内部的时间依赖关系。

1.1 基本计算流程

设输入序列长度为 TT,每个时间步输入为 xtx_t,隐藏状态为 hth_t,输出为 yty_t。RNN 的核心更新公式为:

ht=fh(Wihxt+Whhht1+bh),yt=fo(Whoht+bo),\begin{aligned} h_t &= f_h(W_{ih} x_t + W_{hh} h_{t-1} + b_h),\\ y_t &= f_o(W_{ho} h_t + b_o), \end{aligned}

其中:

  • WihW_{ih} 为输入到隐藏层的权重矩阵,维度为 H×IH \times I
  • WhhW_{hh} 为隐藏到隐藏的权重矩阵,维度为 H×HH \times H
  • WhoW_{ho} 为隐藏到输出的权重矩阵,维度为 O×HO \times H
  • bhb_hbob_o 分别为隐藏层和输出层的偏置项;
  • fhf_hfof_o 分别为隐藏层与输出层的激活函数,常见选择包括 tanh、ReLU、softmax 等。

整个序列的前向计算可视为一个展开的多层网络:

  1. 初始化:通常将 h0h_0 初始化为零向量。
  2. 时间步循环:从 t=1t=1t=Tt=T,依次计算 hth_tyty_t
  3. 输出收集:根据任务需求,输出可以取最后一个时间步的 yTy_T(如分类、回归),也可以取全序列的 (y1,y2,,yT)(y_1, y_2, \dots, y_T)(如序列标注)。

1.2 训练与反向传播

RNN 的训练基于梯度下降,需要通过“反向传播通过时间”(Backpropagation Through Time,BPTT)算法计算梯度:

  1. 损失函数:对全序列或部分时间步的输出计算损失,如均方误差(MSE)或交叉熵(Cross-Entropy)。
  2. 反向展开:将时间维度展开为深度网络,并在展开后的网络上进行反向传播,累积来自每个时间步的梯度。
  3. 梯度更新:按常规方式(SGD、Adam、RMSProp 等)更新参数。由于展开后的网络深度较大,可能出现梯度弥散或梯度爆炸问题。

1.3 长期依赖问题及改进

基础 RNN 在处理长序列时容易出现梯度消失或爆炸,导致模型难以捕捉远距离的依赖。为此,研究者提出了多种改进结构:

  • LSTM(Long Short-Term Memory):引入了输入门、忘记门和输出门,通过门控机制控制信息流动,有效缓解长期依赖问题。
  • GRU(Gated Recurrent Unit):将 LSTM 的输入门和遗忘门合并为重置门和更新门,结构更简洁,性能相当。
  • 带门控的 RNN:在基础 RNN 上添加层归一化、残差连接或门控线性单元(GLU)等技巧,提升稳定性和收敛速度。

下面以 LSTM 单元为例,展示其核心计算:

ft=σ(Wf[ht1,xt]+bf)(遗忘门)it=σ(Wi[ht1,xt]+bi)(输入门)ot=σ(Wo[ht1,xt]+bo)(输出门)c~t=tanh(Wc[ht1,xt]+bc)(候选细胞状态)ct=ftct1+itc~t(更新细胞状态)ht=ottanh(ct)(输出隐藏状态)\begin{aligned} f_t &= \sigma(W_f [h_{t-1}, x_t] + b_f) \quad &\text{(遗忘门)}\\ i_t &= \sigma(W_i [h_{t-1}, x_t] + b_i) \quad &\text{(输入门)}\\ o_t &= \sigma(W_o [h_{t-1}, x_t] + b_o) \quad &\text{(输出门)}\\ \tilde{c}_t &= \tanh(W_c [h_{t-1}, x_t] + b_c) \quad &\text{(候选细胞状态)}\\ c_t &= f_t \odot c_{t-1} + i_t \odot \tilde{c}_t \quad &\text{(更新细胞状态)}\\ h_t &= o_t \odot \tanh(c_t) \quad &\text{(输出隐藏状态)} \end{aligned}

通过门控机制,LSTM 能够选择性地保留或忘记信息,使得模型在处理长序列时更加稳定。

2 序列到序列模型(Seq2Seq)

序列到序列(Sequence to Sequence, Seq2Seq)模型最早应用于机器翻译任务,其核心思想是:将一个可变长度的输入序列映射为另一个可变长度的输出序列。典型结构由编码器(Encoder)和解码器(Decoder)两部分组成:

  1. 编码器:逐步读取输入序列,将其压缩为一个固定长度的上下文向量(context vector)。
  2. 解码器:根据上下文向量,以自回归方式生成输出序列。

2.1 基本架构

  • Encoder:一个多层 RNN(如 LSTM/GRU),从 t=1t=1t=Tint=T_{in} 读取输入 xtx_t,并最终输出隐藏状态 hTinh_{T_{in}}(或多层的状态集合)。
  • Context Vector:通常取编码器最后一个时间步的隐藏状态或其线性变换,作为定长向量 cc
  • Decoder:另一个多层 RNN,初始状态由上下文向量 cc(或映射后的初始隐藏状态)提供。解码器在每一时刻接收上一时刻生成的 yt1y_{t-1}(训练时可使用真实标签,称为 Teacher Forcing),并输出当前时刻 yty_t

2.2 注意力机制(Attention)

固定长度的上下文向量在长序列或信息密集场景中可能成为瓶颈。注意力机制为解码器在每一步动态计算与编码器所有隐藏状态的加权和,从而获得更加丰富的上下文信息。核心计算:

et,s=score(ht1dec,hsenc)alphat,s=exp(et,s)s=1Tinexp(et,s)ct=s=1Tinαt,shsenc\begin{aligned} e_{t,s} &= \text{score}(h^{dec}_{t-1}, h^{enc}_s) \\ alpha_{t,s} &= \frac{\exp(e_{t,s})}{\sum_{s'=1}^{T_{in}} \exp(e_{t,s'})} \\ c_t &= \sum_{s=1}^{T_{in}} \alpha_{t,s} \cdot h^{enc}_s \end{aligned}

其中,score 函数可以是点积、可学习的前馈网络或双线性形式。最终,解码器将上下文向量 ctc_t 与自身隐藏状态拼接或融合后进行生成。注意力机制在机器翻译、文本摘要、图像描述等任务中大幅提升了性能。

3 实际案例:基于 Seq2Seq 的多步时间序列预测

下面通过一个端到端示例,演示如何使用 TensorFlow/Keras 构建并训练一个 Seq2Seq 模型,进行多步时间序列预测。示例目标:根据过去 48 小时传感器采集的温度数据,预测未来 24 小时的温度变化。

3.1 数据集与预处理

  1. 数据来源:假设 CSV 文件 sensor_temperature.csv 包含两列:timestamptemperature

  2. 时间索引:将 timestamp 转为 pandas 的 DatetimeIndex,按小时对齐。缺失值使用线性插值。

  3. 归一化:为了加快收敛并稳定训练,对温度序列做标准化: x=xμσx' = \frac{x - \mu}{\sigma} 其中 μ,σ\mu, \sigma 分别为训练集上的均值和标准差。

  4. 滑动窗口构建:定义输入长度 Lin=48L_{in}=48,输出长度 Lout=24L_{out}=24。遍历序列,构造样本对:

    Xi=[xi,xi+1,,xi+Lin1],Yi=[xi+Lin,xi+Lin+1,,xi+Lin+Lout1].X_i = [x_i, x_{i+1}, \dots, x_{i+L_{in}-1}],\\ Y_i = [x_{i+L_{in}}, x_{i+L_{in}+1}, \dots, x_{i+L_{in}+L_{out}-1}].
  5. 数据拆分:按时间顺序将前 80% 样本作为训练集,后 20% 作为验证集。

import pandas as pd
import numpy as np

# 读取与插值
df = pd.read_csv('sensor_temperature.csv', parse_dates=['timestamp'], index_col='timestamp')
df = df.resample('1H').mean().interpolate()

# 提取序列并标准化
series = df['temperature'].values
mu, sigma = series.mean(), series.std()
series_norm = (series - mu) / sigma

# 滑动窗口函数
def create_sequences(data, L_in, L_out):
    X, Y = [], []
    for i in range(len(data) - L_in - L_out + 1):
        X.append(data[i:i+L_in])
        Y.append(data[i+L_in:i+L_in+L_out])
    return np.array(X), np.array(Y)

L_in, L_out = 48, 24
X, Y = create_sequences(series_norm, L_in, L_out)

# 拆分训练/验证集
split = int(0.8 * len(X))
X_train, Y_train = X[:split], Y[:split]
X_val, Y_val       = X[split:], Y[split:]

3.2 模型搭建

采用经典的编码器-解码器结构:

  • 编码器:单层 LSTM,隐藏单元数 64。
  • 解码器:RepeatVector 将上下文向量复制为输出序列长度,后接单层 LSTM(64 单元)与 TimeDistributed(Dense(1))。
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Input, LSTM, RepeatVector, TimeDistributed, Dense

# 输入层
encoder_inputs = Input(shape=(L_in, 1), name='encoder_inputs')
# 编码器 LSTM
encoder_lstm, state_h, state_c = LSTM(64, return_state=True, name='encoder_lstm')(encoder_inputs)
encoder_states = [state_h, state_c]

# 解码器输入:重复编码器输出
decoder_inputs = RepeatVector(L_out, name='repeat_vector')(state_h)
# 解码器 LSTM
decoder_lstm = LSTM(64, return_sequences=True, name='decoder_lstm')
decoder_outputs = decoder_lstm(decoder_inputs, initial_state=encoder_states)
# 时间分布的全连接层
decoder_dense = TimeDistributed(Dense(1), name='time_distributed')
decoder_outputs = decoder_dense(decoder_outputs)

# 定义模型
model = Model(encoder_inputs, decoder_outputs)
model.compile(optimizer='adam', loss='mse')
model.summary()

3.2.1 超参数与训练策略

  • 学习率:Adam 默认 lr=0.001
  • 批次大小:32;
  • 训练轮次:50 次;
  • EarlyStopping:监控验证集损失,patience=5,以防过拟合。
from tensorflow.keras.callbacks import EarlyStopping

callbacks = [EarlyStopping(monitor='val_loss', patience=5, restore_best_weights=True)]
history = model.fit(
    X_train[..., np.newaxis], Y_train[..., np.newaxis],
    validation_data=(X_val[..., np.newaxis], Y_val[..., np.newaxis]),
    epochs=50,
    batch_size=32,
    callbacks=callbacks
)

3.3 训练结果与评估

  1. 收敛曲线:绘制训练与验证损失随轮次变化曲线,以判断过拟合或欠拟合。

  2. 定量指标:采用均方根误差(RMSE)、平均绝对百分比误差(MAPE)评估预测性能:

    RMSE=1Ni=1N(y^iyi)2,MAPE=100%Ni=1Ny^iyiyi.\text{RMSE} = \sqrt{\frac{1}{N} \sum_{i=1}^N (\hat{y}_i - y_i)^2},\\ \text{MAPE} = \frac{100\%}{N} \sum_{i=1}^N \left|\frac{\hat{y}_i - y_i}{y_i}\right|.
  3. 可视化对比:随机选取若干样本,绘制真实值与预测值对比曲线。

import matplotlib.pyplot as plt

# 收敛曲线
plt.figure(); plt.plot(history.history['loss'], label='train'); plt.plot(history.history['val_loss'], label='val'); plt.legend(); plt.title('Loss Curve')

# 随机样本可视化
def plot_sample(idx):
    true = Y_val[idx]
    pred = model.predict(X_val[idx:idx+1])[0,...,0]
    plt.figure(); plt.plot(true, label='True'); plt.plot(pred, label='Pred'); plt.legend(); plt.title(f'Sample {idx} Prediction')

plot_sample(0)
plot_sample(5)

3.4 性能优化与拓展

  • 加入注意力机制:在解码器每一步对编码器隐藏状态加权。
  • 双向编码器:使用双向 LSTM 捕捉过去和未来上下文。
  • 多层堆叠:增加 LSTM 层数以提升表达能力。
  • 混合模型:结合卷积神经网络(CNN)进行特征提取。
  • 超参数搜索:使用网格搜索(Grid Search)或贝叶斯优化(Bayesian Optimization)寻找最佳超参数。

致谢

感谢阅读!如有疑问或建议,欢迎讨论。