隐藏层结构与变长序列训练

一、RNN 隐藏层层数的理解

1. 基本概念区分

  • 时间步(Time Step):处理序列中每个元素(如句子中的词),是横向展开。
  • 隐藏层(Hidden Layer):指网络在深度方向上的堆叠层数,是纵向堆叠。

✅ 注意:多个时间步 ≠ 多个隐藏层!

2. 单层 RNN(1 层)

  • 每个时间步接收输入 xtx_txt​ 和上一时刻隐藏状态 ht−1h_{t-1}ht−1​,输出当前隐藏状态 hth_tht​:
    ht=tanh(Wxhxt+Whhht−1+bh) h_t = \text{tanh}(W_{xh} x_t + W_{hh} h_{t-1} + b_h) ht​=tanh(Wxh​xt​+Whh​ht−1​+bh​)
  • 输出形状(PyTorch):
    • 输入:(T, B, D)
    • output(所有时间步最后一层输出):(T, B, H)
    • h_n(最终隐藏状态):(1, B, H)

3. 多层 RNN(L 层,L > 1)

  • 结构:第 lll 层以第 l−1l-1l−1 层在同一时间步的隐藏状态为输入。
  • 每层都有独立参数和初始隐藏状态。
  • 前向传播示例(L=2):
    h_t^(1) = RNN1(x_t, h_{t-1}^(1))
    h_t^(2) = RNN2(h_t^(1), h_{t-1}^(2))
    
  • 输出形状:
    • output:仍为 (T, B, H)(仅顶层所有时间步输出)
    • h_n:(L, B, H)(包含每一层的最终隐藏状态)

4. 第二层的输入与初始化

  • 输入来源:第一层在当前时间步的隐藏状态 ht(1)h_t^{(1)}ht(1)​,不是原始输入。
  • 初始化隐藏状态:第二层有自己独立的 h0(2)h_0^{(2)}h0(2)​,通常默认为零或可学习。
  • 数据流:
    x_t → [Layer 1] → h_t^(1) → [Layer 2] → h_t^(2)
          ↑ h_{t-1}^(1)         ↑ h_{t-1}^(2)
    

二、变长序列能否在一个 batch 中训练?

❓ 问题

“一个批次 8 个样本,但时间步长度不同,可以训练吗?”

✅ 答案

可以,但必须使用特殊处理方法。标准 RNN 要求 batch 内序列等长。


三、推荐方案:Padding + Packing(PyTorch 标准做法)

步骤详解

  1. Padding(填充)
    将 batch 中所有序列补零至该 batch 最长序列长度。

    • 例:长度 [5, 3, 7] → 统一 pad 到 7。
  2. 记录真实长度
    保存 lengths = [5, 3, 7]。

  3. 按长度降序排序
    PyTorch 的 pack_padded_sequence 要求序列按长度从大到小排列。

  4. Packing(打包)
    使用 pack_padded_sequence() 压缩序列,跳过 padding 部分的 RNN 计算。

  5. RNN 前向传播
    直接传入 packed sequence。

  6. Unpacking(可选)
    如需完整 output,用 pad_packed_sequence() 解包。

优势

  • ✅ 避免 pad token 干扰隐藏状态(如最后 h 不被 0 污染)
  • ✅ 节省计算资源(不处理无效位置)
  • ✅ 支持 GPU 并行加速

PyTorch 完整示例

import torch
import torch.nn as nn
from torch.nn.utils.rnn import pack_padded_sequence, pad_packed_sequence, pad_sequence

# 构造变长序列(batch=3)
seqs = [torch.randn(5, 10), torch.randn(3, 10), torch.randn(7, 10)]
lengths = [5, 3, 7]

# 1. 按长度降序排序
sorted_lengths, perm_idx = torch.sort(torch.tensor(lengths), descending=True)
sorted_seqs = [seqs[i] for i in perm_idx]

# 2. Padding 成 tensor (max_len, batch, feat)
padded = pad_sequence(sorted_seqs, batch_first=False)  # shape: (7, 3, 10)

# 3. Packing
packed = pack_padded_sequence(padded, sorted_lengths, batch_first=False)

# 4. RNN 处理
rnn = nn.LSTM(10, 20, batch_first=False)
packed_out, (h_n, c_n) = rnn(packed)

# 5. 解包(如需要)
output, out_lengths = pad_packed_sequence(packed_out, batch_first=False)

损失函数处理

  • 在计算 loss 时,应忽略 padding 位置:
    criterion = nn.CrossEntropyLoss(ignore_index=0)  # 假设 pad_id = 0
    

四、常见误区澄清

误区正确理解
“第二层 RNN 接收原始输入”❌ 接收的是第一层的隐藏状态
“多层共享一个初始隐藏状态”❌ 每层有独立 h0(l)h_0^{(l)}h0(l)​
“output 包含所有层输出”❌ 仅包含最后一层
“可以直接输入变长序列”❌ 必须 pad + pack 或单样本处理
“padding 会影响 RNN 结果”✅ 若不 packing,会污染隐藏状态;若 packing,则无影响

五、总结对比表

项目单层 RNN多层 RNN(L 层)变长序列处理
隐藏状态初始化1 个 h0h_0h0​L 个独立 h0(l)h_0^{(l)}h0(l)​每层均可初始化
第 l 层输入(l≥2)—第 l−1 层同时间步输出同左
output 形状(T, B, H)(T, B, H)(顶层)打包后自动对齐
h_n 形状(1, B, H)(L, B, H)正确反映各层最终状态
是否支持变长 batch❌(需处理)❌(需处理)✅ 通过 pad + pack

更多推荐