- 什么是 RNN? - 理解其核心思想和应用场景。
- 为什么需要 LSTM 和 GRU? - 了解 RNN 的致命弱点(梯度消失/爆炸)及其解决方案。
- TensorFlow 中的 RNN 层 - 介绍 Keras 提供的 RNN 相关层。
- 实战项目:基于 RNN 的文本生成 - 一个完整、可运行的例子,从数据预处理到模型训练和文本生成。
- 进阶技巧与最佳实践 - 包括状态管理、双向 RNN、嵌入层等。
什么是 RNN?
核心思想
传统的神经网络(如 Dense 层)处理的是固定长度的独立样本,它们没有“记忆”能力,无法处理序列数据。

RNN 的核心思想是引入了状态,也称为隐藏状态 或记忆,在每个时间步,RNN 不仅接收当前时刻的输入,还会接收上一个时刻的隐藏状态,并将当前的输入和上一个状态结合起来,生成新的隐藏状态和输出。
这种结构使得 RNN 能够“之前的信息,非常适合处理序列数据,
- 自然语言处理: 句子、文章、机器翻译
- 时间序列分析: 股票价格、天气预测
- 语音识别: 音频信号序列
简单的 RNN 在 TensorFlow 中的表示
TensorFlow 的 Keras API 让构建 RNN 变得异常简单,最基础的 RNN 层是 tf.keras.layers.SimpleRNN。
import tensorflow as tf # 创建一个 SimpleRNN 层 # units: 输出空间的维度(即隐藏状态的维度) # return_sequences: 是否返回完整的输出序列(每个时间步都输出),还是只返回最后一个时间步的输出 simple_rnn = tf.keras.layers.SimpleRNN(units=32, return_sequences=True, input_shape=(timesteps, input_features))
为什么需要 LSTM 和 GRU?
简单 RNN 的短板
尽管 SimpleRNN 的概念很优雅,但它在实践中有一个致命的弱点:梯度消失/爆炸问题。

当序列很长时,在反向传播过程中,梯度需要通过很多时间步进行传递,这会导致梯度变得极其小(消失)或极其大(爆炸),使得模型无法有效学习到序列中早期信息与后期输出之间的长距离依赖关系。
在一个长句子中,模型很难理解句首的词对句末词性的影响。
LSTM 和 GRU:解决方案
为了解决这个问题,研究者们提出了更复杂的 RNN 单元,最著名的是 LSTM (Long Short-Term Memory, 长短期记忆网络) 和 GRU (Gated Recurrent Unit, 门控循环单元)。
它们通过精巧的“门控机制”(如遗忘门、输入门、输出门)来控制信息的流动,允许模型有选择地记忆或遗忘信息,从而更好地捕捉长距离依赖关系。

- LSTM: 功能更强大,参数更多,计算量也更大。
- GRU: 是 LSTM 的一个简化版本,参数更少,计算效率更高,在很多任务上表现与 LSTM 相当。
在大多数现代应用中,我们都会优先选择 LSTM 或 GRU。
TensorFlow 中的 RNN 层
Keras 提供了丰富的 RNN 层,你可以在模型中自由组合使用。
| 层名 | 描述 | 常用参数 |
|---|---|---|
tf.keras.layers.SimpleRNN |
基础 RNN 层,通常不直接用于长序列。 | units, return_sequences |
tf.keras.layers.LSTM |
长短期记忆网络,强大的序列建模能力。 | units, return_sequences, return_state |
tf.keras.layers.GRU |
门控循环单元,LSTM 的轻量级替代品。 | units, return_sequences, return_state |
tf.keras.layers.Bidirectional |
将 RNN 层包装成双向 RNN,同时处理过去和未来的信息。 | layer (传入一个 RNN 层实例) |
tf.keras.layers.Embedding |
将整数索引(如单词 ID)转换为密集向量(词嵌入)。 | input_dim (词汇表大小), output_dim (嵌入维度) |
return_sequences 和 return_state 的区别:
return_sequences=False(默认): 只返回最后一个时间步的输出(形状为(batch_size, units))。return_sequences=True: 返回所有时间步的输出(形状为(batch_size, timesteps, units)),当你需要堆叠多个 RNN 层时,必须将前一层的return_sequences设为True。return_state=True: 除了返回输出外,还会返回最后一个隐藏状态,这在需要将一个模型的最终状态作为另一个模型的初始状态时非常有用(如 Encoder-Decoder 架构)。
实战项目:基于 LSTM 的文本生成
我们将训练一个 LSTM 模型来模仿莎士比亚的写作风格,给定一段文本,模型将预测下一个字符,从而能够生成新的、类似风格的文本。
步骤 1: 导入库和下载数据
import tensorflow as tf
import numpy as np
import os
import time
# 下载数据集
path_to_file = tf.keras.utils.get_file('shakespeare.txt', 'https://storage.googleapis.com/download.tensorflow.org/data/shakespeare.txt')
步骤 2: 数据预处理
我们需要将文本转换成数字,因为神经网络只能处理数字。
# 读取文本
text = open(path_to_file, 'rb').read().decode(encoding='utf-8')
# 创建字符到数字的映射
vocab = sorted(set(text))
char2idx = {u:i for i, u in enumerate(vocab)}
idx2char = np.array(vocab)
# 将文本转换为整数序列
text_as_int = np.array([char2idx[c] for c in text])
# 创建训练样本和标签
# 我们使用一个固定的长度 seq_length 来创建序列
seq_length = 100
examples_per_epoch = len(text)//(seq_length+1)
# 创建数据集
char_dataset = tf.data.Dataset.from_tensor_slices(text_as_int)
sequences = char_dataset.batch(seq_length+1, drop_remainder=True)
# 将序列分割成输入和标签
def split_input_target(chunk):
input_text = chunk[:-1]
target_text = chunk[1:]
return input_text, target_text
dataset = sequences.map(split_input_target)
# 创建批次数据
BATCH_SIZE = 64
BUFFER_SIZE = 10000
dataset = dataset.shuffle(BUFFER_SIZE).batch(BATCH_SIZE, drop_remainder=True)
# 检查一下数据
for input_example, target_example in dataset.take(1):
print("Input :", repr(''.join(idx2char[input_example[0].numpy()])))
print("Target:", repr(''.join(idx2char[target_example[0].numpy()])))
步骤 3: 构建模型
我们将使用 Embedding 层、一个 LSTM 层和一个 Dense 层。
# 嵌入维度
embedding_dim = 256
# RNN 单元数量
rnn_units = 1024
def build_model(vocab_size, embedding_dim, rnn_units, batch_size):
model = tf.keras.Sequential([
tf.keras.layers.Embedding(vocab_size, embedding_dim,
batch_input_shape=[batch_size, None]),
tf.keras.layers.LSTM(rnn_units,
return_sequences=True,
stateful=True,
recurrent_initializer='glorot_uniform'),
tf.keras.layers.Dense(vocab_size)
])
return model
vocab_size = len(vocab)
model = build_model(
vocab_size=vocab_size,
embedding_dim=embedding_dim,
rnn_units=rnn_units,
batch_size=BATCH_SIZE)
步骤 4: 配置训练过程
我们需要定义损失函数和优化器,由于我们处理的是整数形式的标签,SparseCategoricalCrossentropy 是一个很好的选择。
def loss(labels, logits):
return tf.keras.losses.sparse_categorical_crossentropy(labels, logits, from_logits=True)
model.compile(optimizer='adam', loss=loss)
# 检查模型输出
for input_example_batch, target_example_batch in dataset.take(1):
example_batch_predictions = model(input_example_batch)
print(example_batch_predictions.shape, "# (batch_size, sequence_length, vocab_size)")
步骤 5: 训练模型
为了能随时生成文本,我们使用 ModelCheckpoint 回调函数来保存模型的权重。
# 训练轮数
EPOCHS = 20
# 保存检查点的目录
checkpoint_dir = './training_checkpoints'
# 检查点的前缀
checkpoint_prefix = os.path.join(checkpoint_dir, "ckpt_{epoch}")
checkpoint_callback = tf.keras.callbacks.ModelCheckpoint(
filepath=checkpoint_prefix,
save_weights_only=True)
history = model.fit(dataset, epochs=EPOCHS, callbacks=[checkpoint_callback])
步骤 6: 文本生成
训练完成后,我们可以用它来生成新的文本。
# 重建具有不同 batch_size 的模型
model = build_model(vocab_size, embedding_dim, rnn_units, batch_size=1)
# 加载最后保存的权重
model.load_weights(tf.train.latest_checkpoint(checkpoint_dir))
model.build(tf.TensorShape([1, None]))
# 生成文本的函数
def generate_text(model, start_string):
# 评估步骤(生成文本)
# 要生成的字符数量
num_generate = 1000
# 将起始字符串转换为数字(向量)
input_eval = [char2idx[s] for s in start_string]
input_eval = tf.expand_dims(input_eval, 0)
# 存储生成的文本
text_generated = []
# 温度可以用来控制生成文本的随机性
temperature = 1.0
model.reset_states()
for i in range(num_generate):
predictions = model(input_eval)
# 移除批次的维度
predictions = tf.squeeze(predictions, 0)
# 用温度缩预测
predictions = predictions / temperature
predicted_id = tf.random.categorical(predictions, num_samples=1)[-1,0].numpy()
# 将预测的字符作为下一个模型的输入
input_eval = tf.expand_dims([predicted_id], 0)
text_generated.append(idx2char[predicted_id])
return (start_string + ''.join(text_generated))
print(generate_text(model, start_string=u"ROMEO: "))
进阶技巧与最佳实践
a) 使用词嵌入
在上面的例子中,我们直接将字符 ID 作为输入,对于更复杂的 NLP 任务(如机器翻译),通常使用 tf.keras.layers.Embedding 层将单词 ID 转换为密集的、低维度的向量,这些向量能够捕捉单词之间的语义关系。
b) 双向 RNN
有时,一个时间步的输出不仅依赖于它之前的信息,也依赖于之后的信息,在词性标注任务中,一个词的词性可能需要结合它前后的词来确定。
双向 RNN 通过将一个 RNN 层正向处理,另一个 RNN 层反向处理,然后将它们的输出连接起来,来实现这一点。
model = tf.keras.Sequential([
tf.keras.layers.Embedding(vocab_size, 64),
tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(64, return_sequences=True)), # return_sequences=True 是为了下一层
tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(32)),
tf.keras.layers.Dense(64, activation='relu'),
tf.keras.layers.Dense(1, activation='sigmoid')
])
c) 状态管理
在训练时,我们通常在每个 epoch 开始时重置 RNN 的状态,但在推理(生成文本)时,我们可能希望模型保持其状态,以延续上一次的“记忆”,这就是为什么在生成文本的模型中我们设置了 stateful=True,并在每次生成前调用 model.reset_states()。
d) 处变长序列
在真实场景中,序列的长度往往是可变的。tf.keras.layers.Masking 或 tf.keras.layers.Embedding 层的 mask_zero=True 参数可以告诉 RNN 忽略填充值(通常是 0),从而正确处理变长序列。
这个教程为你提供了一个坚实的起点,RNN 及其变体(LSTM, GRU)是深度学习领域处理序列数据的基石,掌握它们,你就能应对大量有趣且富有挑战性的问题。
