LSTM实现中的错误与修复 | AI生成和翻译

Home 2025.09

明白了——这是一个清晰的端到端说明,展示了这个从零实现的LSTM脚本在做什么,以及那些会破坏训练的细微错误及其修复方法。我将保持线性结构便于阅读(不使用宽表格)。

代码试图实现的功能

你正在用NumPy实现一个极简的字符/词级LSTM语言模型:

  1. 设置与维度
  1. 参数初始化
  1. 前向传播(每个序列) 对于每个时间步:
  1. 损失函数
  1. 反向传播(通过LSTM的BPTT)
  1. 训练循环

关键问题(这些问题会静默地破坏学习)

1) 反向传播中 hC 的索引偏移错误

forward中,你首先压入初始状态:

但在backward(...)中,你使用h[t]C[t],好像它们就是h_tC_t。其实不是;它们偏移了1。

修复(简单的经验法则):

所以在for t in reversed(range(T)):循环内部:

你当前的代码行:

C_prev = C[t - 1]

对于t==0是错误的(会绕到最后一个元素),并且总体上偏移了1。它必须是:

C_prev = C[t]       # 先前的细胞状态
# 并使用 C_t = C[t+1] 作为"当前"

并且,任何你使用h[t]意图表示当前隐藏状态的地方,都应改为h[t+1]

2) 几个门控的导数计算错误

你有时重复应用了非线性函数而不是其导数,或者忘记了导数标志。

3) 使用 h[0] / C[0] 来调整 dh_nextdC_next 的大小

你需要的是当前 h/C(序列末尾)的形状,而不是初始的零向量。使用:

dh_next = np.zeros_like(h[-1])
dC_next = np.zeros_like(C[-1])

4) 交叉熵的数值稳定性

如果softmax内部进行了截断/加epsilon处理,那么loss += -np.mean(np.log(outputs[t]) * targets[t])是没问题的。如果没有,添加一个小的epsilon:

eps = 1e-12
loss += -np.sum(targets[t] * np.log(outputs[t] + eps))

5) 训练稳定性调整


反向传播循环的修正核心代码

这个草图展示了索引和导数的修正。它假设f[t], i[t], g[t], o[t]是它们非线性函数的输出(如你的前向传播中所示):

T = len(outputs)
dh_next = np.zeros_like(h[-1])
dC_next = np.zeros_like(C[-1])

for t in reversed(range(T)):
    # 使用正确索引的简短变量名
    y_hat = outputs[t]                 # softmax 输出
    y_true = targets[t]
    h_t   = h[t+1]
    C_t   = C[t+1]
    C_tm1 = C[t]

    # 输出层
    dv = y_hat.copy()
    dv[np.argmax(y_true)] -= 1
    W_v_d += dv @ h_t.T
    b_v_d += dv

    # 反向传播到 h
    dh = W_v.T @ dv
    dh += dh_next

    # 输出门
    do = dh * np.tanh(C_t)
    do *= o[t] * (1 - o[t])
    W_o_d += do @ z[t].T
    b_o_d += do

    # 细胞状态
    dC = dC_next + dh * o[t] * (1 - np.tanh(C_t)**2)

    # 候选门 (g), 输入门 (i), 遗忘门 (f)
    dg = dC * i[t]
    dg *= (1 - g[t]**2)               # 因为 g[t] = tanh(preact)

    di = dC * g[t]
    di *= i[t] * (1 - i[t])

    df = dC * C_tm1
    df *= f[t] * (1 - f[t])

    # 累积门控权重/偏置梯度
    W_g_d += dg @ z[t].T;  b_g_d += dg
    W_i_d += di @ z[t].T;  b_i_d += di
    W_f_d += df @ z[t].T;  b_f_d += df

    # 传播到 z 并分割到 h_{t-1} 部分
    dz = (W_f.T @ df) + (W_i.T @ di) + (W_g.T @ dg) + (W_o.T @ do)
    dh_next = dz[:hidden_size, :]
    dC_next = dC * f[t]

较小但重要的细节与建议


你现在应该立即进行的修复总结

  1. 索引:在反向传播中,将每个h[t]替换为h[t+1],每个C[t]替换为C[t+1],并设置C_prev = C[t]
  2. 导数

    • 对细胞状态路径使用(1 - tanh(C_t)**2)
    • 对σ门使用gate * (1 - gate),因为你存储的是门控输出。
    • 对候选门使用(1 - g[t]**2),因为g[t]=tanh(preact)
  3. 初始化:将b_f设置为约1.0
  4. 学习率:降至1e-2(或切换到Adam)。
  5. 稳定性:在log中添加eps

进行这些更改后,你的损失应该开始以更合理的方式下降。如果你愿意,可以粘贴你更新后的backward(...)代码,我会帮你检查一下。


Back Donate