Sutskever 30 #03:记忆为什么会漏,LSTM 怎么堵的?

上一篇留下的问题 上一篇里,我们用一个 vanilla RNN 在 2490 个字符上跑出了 hello world。但那个模型的记忆只有 25 步——再远的东西它就看不到了。 一个自然的想法是:那把记忆加大呢?64 个隐藏单元不够,换 1000 个,换 10000 个。 没用。白板再大也没用。 乘法效应 vanilla RNN 每一步做的是:把旧状态和新输入混在一起,过一个 tanh。这个操作是乘法性质的——旧状态被一个矩阵乘一次、压一次。连续做 30 步,就是连乘 30 次。 $0.9^{30} \approx 0.04$ 哪怕每次只损失 10%,30 步之后只剩 4%。每一步都在把之前写的东西乘以一个小于 1 的数,越乘越淡。白板开到多大都一样。 训练的时候也一样。梯度要从最后一步往回传,告诉前面的参数"你该记住什么"。每往回传一步,信号就乘一次,30 步之后信号几乎为 0。学习信号传不到那么远,模型就学不到"30 步之前发生了什么"。 这就是梯度消失。连乘带来的衰减,是 vanilla RNN 记不远的根本原因。 LSTM:开一条加法的路 LSTM 的做法是,在乘法链旁边开了一条加法通道——cell state。这条通道专门用来存记忆,更新方式是加法:旧记忆保留一部分,加上新信息的一部分。 光有加法通道还不够。如果每一步都把所有新信息加进去,cell state 很快就会被噪音淹没。所以 LSTM 还配了三个开关,控制这条通道上的读写: 三个开关在每一步都问三个问题: 忘记开关(forget gate):旧记忆里哪些要继续留着,哪些可以扔掉? 写入开关(input gate):新信息要不要写进记忆?写多少? 输出开关(output gate):记忆里的东西,这一步要拿多少出来用? 每个开关输出一个 0 到 1 之间的数。1 就是全开,0 就是全关。 代码里长这样: # 忘记开关 f = sigmoid(np.dot(self.Wf, concat) + self.bf) # 写入开关 i = sigmoid(np.dot(self.Wi, concat) + self.bi) # 新信息候选 c_tilde = np.tanh(np.dot(self.Wc, concat) + self.bc) # 更新记忆:旧的留一部分 + 新的写一部分 c_next = f * c_prev + i * c_tilde # 输出开关 o = sigmoid(np.dot(self.Wo, concat) + self.bo) h_next = o * np.tanh(c_next) 关键是这一行: ...

April 16, 2026