4.2 循环神经网络:按时步展开的串行调度


文档摘要

4.2 循环神经网络:按时步展开的串行调度 本节摘要:CNN 用权值共享押注空间局部性,RNN 用同一招押注时间依赖:同一个单元在序列的每个时步反复执行,隐藏状态像传送带一样把历史信息带向未来。本节讲清状态传递与按时间展开的调度结构,门控单元(LSTM 与 GRU)如何缓解长序列的记忆衰减,并用影评情感分类与用电负荷预测两个案例落地。RNN 的串行本质也埋下它的局限——这正是 4.5 节注意力登场的伏笔。 本节能力清单 阅读完本节,你应当能够: 手推简单 RNN 单元的状态更新公式,解释"同一权重在所有时步复用"; 说清 LSTM 三个门各自管什么,以及梯度消失在时间轴上的成因; 在影评数据上用 Embedding 加 LSTM 搭情感分类器并跑通;

4.2 循环神经网络:按时步展开的串行调度

本节摘要:CNN 用权值共享押注空间局部性,RNN 用同一招押注时间依赖:同一个单元在序列的每个时步反复执行,隐藏状态像传送带一样把历史信息带向未来。本节讲清状态传递与按时间展开的调度结构,门控单元(LSTM 与 GRU)如何缓解长序列的记忆衰减,并用影评情感分类与用电负荷预测两个案例落地。RNN 的串行本质也埋下它的局限——这正是 4.5 节注意力登场的伏笔。

本节能力清单

阅读完本节,你应当能够:

  1. 手推简单 RNN 单元的状态更新公式,解释"同一权重在所有时步复用";
  2. 说清 LSTM 三个门各自管什么,以及梯度消失在时间轴上的成因;
  3. 在影评数据上用 Embedding 加 LSTM 搭情感分类器并跑通;
  4. 把时序预测组织成"窗口到下一步"的监督学习形式并完成训练。

状态传递:把历史装进一个向量

序列数据的核心难点是变长与依赖:一句话十几个词,负荷曲线几千个点,关键信息可能出现在任意位置。RNN 的方案是用一个固定长度的隐藏状态作为"运行摘要":每读一个时步,用当前输入与上一步的状态算出新的状态。简单单元的更新只有一行——新状态等于激活函数作用在权重乘输入加权重乘旧状态上:

import tensorflow as tf import numpy as np # 手写一步简单 RNN,看清"一行公式"的调度本质 h = tf.zeros([2, 4]) # 批次 2,状态 4 维,初始为零 Wx = tf.random.normal([3, 4]) * 0.5 # 输入到状态的权重 Wh = tf.random.normal([4, 4]) * 0.5 # 状态到状态的权重(所有时步同一份) seq = tf.random.normal([2, 5, 3]) # 批次 2,5 个时步,每步 3 维特征 for t in range(seq.shape[1]): x_t = seq[:, t, :] h = tf.tanh(x_t @ Wx + h @ Wh) # 同一公式,权重全序列复用 print(f"step {t}: state norm {tf.norm(h).numpy():.3f}") # 输出示例: # step 0: state norm 0.912 # step 1: state norm 1.034 # step 2: state norm 0.877 # ... # 关键观察:Wx 与 Wh 在循环外只定义一次—— # 展开后是 5 个相同的单元首尾相接,权重逐时步共享

权重复用带来的训练问题是梯度要沿时间回放:损失对第 1 步权重的梯度要穿过第 2、3、4 步的链式乘积。每乘一次状态转移矩阵,梯度就乘一个系数——系数持续小于 1 就是梯度消失,远处的信号传不回来,RNN 因此记不住长程依赖。

门控:给传送带装闸口

LSTM 的思路是给状态传送带装三个可学习的闸口:遗忘门决定旧状态保留多少、输入门决定新信息写入多少、输出门决定状态暴露多少。细胞状态单独走一条近似线性的通道,梯度沿它回传时衰减慢得多。GRU 是简化款,把三个门合并成两个,参数少四分之一,效果多数场景相当。

# 三种单元的形状与参数量对比 x = tf.random.normal([2, 10, 8]) # 批次 2,10 个时步,8 维特征 simple = tf.keras.layers.SimpleRNN(16, return_sequences=True) lstm = tf.keras.layers.LSTM(16, return_sequences=True) gru = tf.keras.layers.GRU(16, return_sequences=True) s = tf.keras.Sequential([tf.keras.layers.Input(shape=(10, 8)), tf.keras.layers.SimpleRNN(16)]) l = tf.keras.Sequential([tf.keras.layers.Input(shape=(10, 8)), tf.keras.layers.LSTM(16)]) g = tf.keras.Sequential([tf.keras.layers.Input(shape=(10, 8)), tf.keras.layers.GRU(16)]) print("SimpleRNN:", s.count_params(), "LSTM:", l.count_params(), "GRU:", g.count_params()) # 输出:SimpleRNN: 401 LSTM: 1601 GRU: 1217 # 状态 16 维时:SimpleRNN 每层 401 参数, # LSTM 是它的四倍(三套门加一套细胞候选), # GRU 介于两者之间

return_sequences 参数决定输出形态:True 时每个时步都吐状态(做序列到序列、或堆叠多层 LSTM 时用);False 时只吐最后一个时步的状态(做序列级分类够用)。

案例一:影评情感分类

用 keras 内置的 IMDb 影评数据(已预转成整数词序列)。管道是:Embedding 把词编号变稠密向量,LSTM 聚合时序信息,末层出情感分数:

vocab = 10000 (xtr, ytr), (xte, yte) = tf.keras.datasets.imdb.load_data(num_words=vocab) xtr = tf.keras.preprocessing.sequence.pad_sequences(xtr[:5000], maxlen=200) xte = tf.keras.preprocessing.sequence.pad_sequences(xte[:1000], maxlen=200) print(xtr.shape, xtr[0][:8]) # 输出:(5000, 200) [ 1 14 22 16 43 530 97 1146] # 变长评论统一补零到 200 步,词被编码成整数编号 clf = tf.keras.Sequential([ tf.keras.layers.Input(shape=(200,)), tf.keras.layers.Embedding(vocab, 32), # 编号变 32 维稠密向量 tf.keras.layers.LSTM(32), # 只取末时步状态 tf.keras.layers.Dense(16, activation="relu"), tf.keras.layers.Dense(1, activation="sigmoid"), ]) clf.compile(optimizer="adam", loss="binary_crossentropy", metrics=["accuracy"]) h = clf.fit(xtr, ytr, validation_data=(xte, yte), epochs=4, batch_size=64, verbose=0) print(f"val acc {h.history['val_accuracy'][-1]:.3f}") # 参考输出:val acc 0.832 # 四轮内到八成上下的验证准确率——LSTM 对短评足够胜任

Embedding 层的本质是一张可训练的查表:词编号到向量的映射矩阵随训练一起更新,语义相近的词会学到相近的向量——它其实是词的"结构先验"的参数化。

案例二:用电负荷预测

时序预测要先把单条曲线切成"输入窗口到预测目标"的监督样本。造一段带周期与趋势的负荷曲线,窗口 24 步预测下一步:

t = np.arange(3000) load = 100 + 20 * np.sin(t / 12.0) + t * 0.01 + np.random.randn(3000) * 3 # 周期加趋势加噪声 def make_windows(series, w=24): xs, ys = [], [] for i in range(len(series) - w): xs.append(series[i:i + w]) ys.append(series[i + w]) xs = np.array(xs, dtype="float32")[..., None] # 加特征轴:(N, 24, 1) ys = np.array(ys, dtype="float32") return xs, ys Xw, yw = make_windows(load) cut = int(len(Xw) * 0.8) fore = tf.keras.Sequential([ tf.keras.layers.Input(shape=(24, 1)), tf.keras.layers.LSTM(32), tf.keras.layers.Dense(1), ]) fore.compile(optimizer="adam", loss="mse") fore.fit(Xw[:cut], yw[:cut], validation_data=(Xw[cut:], yw[cut:]), epochs=5, batch_size=64, verbose=0) pred = fore.predict(Xw[cut:cut + 5], verbose=0).flatten() print("pred :", np.round(pred, 1)) print("truth:", np.round(yw[cut:cut + 5], 1)) # 输出示例: # pred : [125.4 125.9 126.6 127.1 127.8] # truth: [124.8 125.3 126.2 127.0 127.5] # 五步预测与真值逐点贴近——周期、趋势都被窗口捕捉

窗口化是时序监督学习的通用前置工序,注意它必须在标准化之后、管道化之前完成(2.2 节提过:别让 from_tensor_slices 把时间轴切碎)。

串行的代价与出路的预告

RNN 的调度死结在时步之间:第 t 步必须等第 t-1 步的状态就绪才能开工,序列多长调度链就多长,GPU 的并行能力用不上。长短序列的处理成本线性增长且无法并行展开——这不是工程优化能绕开的,是结构本身的天花板。4.5 节的注意力机制正面回答这个问题:取消逐步传递,让每个位置直接与其他所有位置打分交互,全部时步一次并行算完。

⚠️ 常见坑:LSTM 堆多层时忘了给中间层开 return_sequences=True,下一层收到的不是序列而是单向量,形状报错或模型退化。口诀:中间层全开 True,最后一层按需。

💡 关键直觉:RNN 是"同一个单元反复上岗"的调度结构——参数省了,但时间上的串行依赖是刚性成本。判断用不用它,先量序列长度与实时性预算。

本节要点回顾

  • 一行状态公式:同一权重全时步复用,展开即相同单元首尾相接。
  • 梯度沿时间回放:连乘衰减导致长程记忆难,这是结构病不是调参病。
  • 门控三件套:LSTM 三门管遗忘、写入、暴露;GRU 简化但多数够用。
  • 两个案例:影评分类用 Embedding 加 LSTM;负荷预测先窗口化再 LSTM。
  • 串行天花板:时步间刚性依赖限制并行,长序列的正解在注意力。

下节转向无标签数据的家族:自编码器。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U