核心目标与重点
在深入代码之前,我们先明确一下本章的重点。目标是掌握循环神经网络的核心原理、经典变体结构,以及在文本序列任务中的实战开发流程。理解 RNN 的循环计算机制是关键,同时要学会使用 TensorFlow/Keras 搭建基础 RNN 与 LSTM 模型,完成实际的文本分类任务。
循环神经网络核心原理
为什么需要 RNN
传统的前馈神经网络(如 CNN、全连接网络)通常假设输入和输出是相互独立的。但在处理自然语言文本、语音信号或时间序列数据时,这种假设就不成立了。序列数据的核心特点是当前时刻的信息和之前时刻的信息紧密相关。
循环神经网络通过引入隐藏状态,可以存储历史信息,从而有效捕捉序列数据的上下文依赖关系。
RNN 的循环计算机制
RNN 的核心结构是循环核。它的本质是一个带有自连接的神经元结构。循环核会在每一个时间步接收输入数据和上一个时间步的隐藏状态,计算当前时间步的输出和新的隐藏状态。
计算过程大致分为三步:
- 初始化隐藏状态 h₀,通常设置为全零向量。
- 对每个时间步 t,计算当前隐藏状态 hₜ = tanh(Wₓₕxₜ + Wₕₕhₜ₋₁ + bₕ)。
- 根据隐藏状态计算当前时间步输出 yₜ = Wₕᵧhₜ + bᵧ。
⚠️ 注意:基础 RNN 存在梯度消失或梯度爆炸问题。它无法有效捕捉长序列的依赖关系,因此实际应用中更多使用其变体模型。
import tensorflow as tf
from tensorflow.keras.layers import SimpleRNN
# 定义基础 RNN 层
# units: 隐藏状态维度,return_sequences: 是否返回所有时间步输出
rnn_layer = SimpleRNN(units=64, return_sequences=True, input_shape=(10, 20))
# 模拟输入:批次大小 32,序列长度 10,每个时间步特征维度 20
input_seq = tf.random.normal(shape=(32, 10, 20))
# 执行 RNN 计算
output_seq = rnn_layer(input_seq)
print("RNN 输出形状:", output_seq.shape)
# 输出形状 (32, 10, 64)
RNN 的梯度问题与改进方向
基础 RNN 在处理长序列时,梯度在反向传播过程中会随着时间步的增加而指数级衰减或膨胀。这会导致模型无法学习到长距离的依赖关系。
为了解决这个问题,研究者提出了两种经典的 RNN 变体:长短期记忆网络(LSTM) 和 门控循环单元(GRU)。它们通过引入门控机制,来控制信息的遗忘和更新,从而有效缓解梯度消失问题。
经典 RNN 变体——长短期记忆网络(LSTM)
LSTM 是最常用的 RNN 变体,由 Hochreiter & Schmidhuber 于 1997 年提出。它通过输入门、遗忘门和输出门的协同作用,实现对历史信息的选择性记忆和遗忘。
LSTM 的门控机制解析
LSTM 的每个循环核内部包含三个关键门控和一个细胞状态:
- 遗忘门:决定哪些历史信息需要被丢弃。通过 sigmoid 函数输出 0~1 之间的数值,0 表示完全遗忘,1 表示完全保留。
- 输入门:决定哪些新信息需要被加入到细胞状态中。分为两步,先通过 sigmoid 函数筛选信息,再通过 tanh 函数生成候选信息。
- 输出门:决定当前细胞状态中哪些信息需要输出作为隐藏状态。通过 sigmoid 函数筛选,再与 tanh 处理后的细胞状态相乘得到输出。
- 细胞状态:LSTM 的核心记忆单元,负责存储长序列的历史信息,通过门控机制实现信息的更新和传递。
LSTM 层的代码实现
from tensorflow.keras.layers import LSTM
# 定义 LSTM 层
# return_state: 是否返回最终的隐藏状态和细胞状态
lstm_layer = LSTM(units=128, return_sequences=False, return_state=True, input_shape=(10, 20))
# 执行 LSTM 计算
output, final_hidden_state, final_cell_state = lstm_layer(input_seq)
print("LSTM 输出形状:", output.shape)
# 输出形状 (32, 128)
print("最终隐藏状态形状:", final_hidden_state.shape)
# 形状 (32, 128)
print("最终细胞状态形状:", final_cell_state.shape)
# 形状 (32, 128)
实战:基于 LSTM 的文本分类任务
任务介绍与数据集准备
本次实战任务是情感分类。我们将使用 IMDB 电影评论数据集。这个数据集包含 50000 条标注为'正面'或'负面'的电影评论。我们的目标是搭建 LSTM 模型,实现对评论情感倾向的自动判断。
步骤如下:
- 加载 IMDB 数据集,限制词汇表大小为 10000,序列长度统一为 200。
- 将文本序列转换为整数索引序列,超出长度的截断,不足的补零。
- 划分训练集和测试集,训练集 25000 条,测试集 25000 条。
from tensorflow.keras.datasets import imdb
from tensorflow.keras.preprocessing.sequence import pad_sequences
# 1. 加载数据集
vocab_size = 10000
max_seq_len = 200
(x_train, y_train), (x_test, y_test) = imdb.load_data(num_words=vocab_size)
# 2. 序列填充与截断
x_train = pad_sequences(x_train, maxlen=max_seq_len, padding="post", truncating="post")
x_test = pad_sequences(x_test, maxlen=max_seq_len, padding="post", truncating="post")
print("训练集形状:", x_train.shape) # (25000, 200)
print("测试集形状:", x_test.shape) # (25000, 200)
搭建 LSTM 文本分类模型
本次模型结构分为三层:嵌入层、LSTM 层、全连接分类层。
- 嵌入层将整数索引转换为稠密向量,解决文本稀疏问题。
- LSTM 层捕捉文本序列的上下文依赖。
- 全连接层通过 sigmoid 函数输出情感分类结果。
from tensorflow.keras import Sequential
from tensorflow.keras.layers import Embedding, Dense
# 定义模型
embedding_dim = 128
model = Sequential([
# 嵌入层:input_dim=词汇表大小,output_dim=嵌入维度,input_length=序列长度
Embedding(input_dim=vocab_size, output_dim=embedding_dim, input_length=max_seq_len),
# LSTM 层:128 个隐藏单元,加入 Dropout 防止过拟合
LSTM(units=128, dropout=0.2, recurrent_dropout=0.2),
# 全连接分类层:输出 1 个值,sigmoid 激活
Dense(units=1, activation="sigmoid")
])
# 查看模型结构
model.summary()
模型编译与训练
接下来我们进行模型的编译与训练。选择 Adam 优化器,二分类交叉熵损失函数,评估指标为准确率。设置批次大小 64,训练轮数 5 轮,使用 10% 的训练数据作为验证集。
# 1. 编译模型
model.compile(optimizer="adam", loss="binary_crossentropy", metrics=["accuracy"])
# 2. 训练模型
batch_size = 64
epochs = 5
history = model.fit(
x_train, y_train,
batch_size=batch_size,
epochs=epochs,
validation_split=0.1
)
# 3. 评估模型
test_loss, test_acc = model.evaluate(x_test, y_test)
print(f"测试集准确率:{test_acc:.4f}")
模型优化技巧
在实际开发中,我们还可以尝试一些优化手段来提升效果:
- 预训练词向量:使用 Word2Vec 或 GloVe 替换随机初始化的嵌入层,提升文本特征表示能力。
- 双向 LSTM:同时捕捉文本的正向和反向上下文依赖。
- 早停法(EarlyStopping):当验证集损失不再下降时停止训练,防止过拟合。
双向 LSTM 层的代码示例:
from tensorflow.keras.layers import Bidirectional
# 替换原 LSTM 层为双向 LSTM
Bidirectional(LSTM(units=128, dropout=0.2, recurrent_dropout=0.2))
早停法的代码示例:
from tensorflow.keras.callbacks import EarlyStopping
# 定义早停回调函数
early_stopping = EarlyStopping(monitor="val_loss", patience=2, restore_best_weights=True)
# 在训练时加入回调
model.fit(x_train, y_train, callbacks=[early_stopping])
门控循环单元(GRU)简介
GRU 是 LSTM 的简化版本。它将遗忘门和输入门合并为更新门,同时取消了细胞状态,直接使用隐藏状态传递信息。GRU 的参数数量比 LSTM 更少,训练速度更快。在很多场景下,GRU 可以取得和 LSTM 相当的效果。
from tensorflow.keras.layers import GRU
# 定义 GRU 层
gru_layer = GRU(units=128, return_sequences=True, input_shape=(10, 20))
gru_output = gru_layer(input_seq)
print("GRU 输出形状:", gru_output.shape)
本章总结
循环神经网络通过隐藏状态存储历史信息,能够有效处理序列数据的上下文依赖关系。LSTM 引入门控机制,解决了基础 RNN 的梯度消失问题,是处理长序列任务的核心模型。在文本分类等序列任务中,LSTM 结合嵌入层可以取得良好效果,双向 LSTM 和早停法等技巧能进一步优化模型性能。


