3、遗忘门详解:Sigmoid层如何决定丢弃哪些历史信息

好,咱们今天来啃LSTM里最核心的一个部件——遗忘门。

说实话,我刚接触LSTM那会儿,最困惑的就是这个“遗忘”。人脑遗忘是坏事,怎么神经网络还专门设计个门来忘东西?这不是倒退吗?

后来我明白了。你想想看,如果一个人把所有事情都记得清清楚楚,大脑早就炸了。神经网络也一样。它需要学会判断:哪些历史信息已经过时了,哪些还有用。这就是遗忘门的职责。

3.1 遗忘门的核心思想:选择性记忆

遗忘门,说白了就是一个“信息过滤器”。

它的输入有两个:当前时刻的输入 \(x_t\)上一时刻的隐藏状态 \(h_{t-1}\)。输出是一个介于0到1之间的向量 \(f_t\)。

这个 \(f_t\) 会跟细胞状态 \(C_{t-1}\) 做逐元素相乘。乘0的信息,就被彻底丢弃;乘1的信息,就完整保留;乘0.5的信息,就半信半疑地留着。

嗯,这里要注意:遗忘门不是真的“删除”数据,而是通过权重控制信息的保留程度。我在项目中遇到过,很多人把遗忘门理解成二值开关(要么全丢要么全留),这是不对的。它是一个软开关,可以做到“部分遗忘”。

3.2 数学公式:Sigmoid的魔力

遗忘门的数学表达式长这样:

f_t = σ(W_f · [h_{t-1}, x_t] + b_f)

拆开来看:

  • \([h_{t-1}, x_t]\):把上一时刻的隐藏状态和当前输入拼接成一个长向量
  • \(W_f\):遗忘门的权重矩阵,模型要学的参数
  • \(b_f\):偏置项
  • \(\sigma\):Sigmoid激活函数,把输出压到(0,1)区间

为什么用Sigmoid?

我个人习惯把Sigmoid想象成一个“概率阀门”。它的输出天然在0到1之间,非常适合做“保留比例”的控制。如果是ReLU,输出可能到无穷大,没法当比例用。如果是Tanh,输出有正有负,乘到细胞状态上会改变符号,逻辑上说不通。

所以,Sigmoid是遗忘门最自然的选择。没有之一。

3.3 遗忘门的内部工作机制

咱们来走一遍计算流程:

  1. 拼接输入:把 \(h_{t-1}\) 和 \(x_t\) 拼在一起,形成一个更长的向量
  2. 线性变换:用权重矩阵 \(W_f\) 做线性映射,加上偏置 \(b_f\)
  3. Sigmoid激活:把线性变换的结果压到(0,1)区间
  4. 逐元素相乘:用得到的 \(f_t\) 跟旧的细胞状态 \(C_{t-1}\) 相乘

举个例子:

假设 \(C_{t-1} = [5.0, -2.0, 0.0, 3.5]\),遗忘门输出 \(f_t = [0.9, 0.1, 0.5, 0.0]\)。

那么新的细胞状态就是:

C_t = f_t * C_{t-1} = [4.5, -0.2, 0.0, 0.0]

看到了吗?第一个元素保留了90%,第二个只剩10%,第三个砍了一半,第四个被彻底丢弃。

关键理解:遗忘门不是“一刀切”地决定忘记还是记住,而是对每个维度独立做决策。这给了模型极大的灵活性。

3.4 代码实现:从零搭建遗忘门

理论讲完了,咱们上代码。我建议你亲手敲一遍,光看是学不会的。

import numpy as np

def sigmoid(x):
    """Sigmoid激活函数,防止数值溢出"""
    return 1 / (1 + np.exp(-np.clip(x, -500, 500)))

class ForgetGate:
    """遗忘门实现"""
    def __init__(self, hidden_size, input_size):
        # 权重矩阵:输入是 hidden_size + input_size
        # 输出是 hidden_size
        self.W_f = np.random.randn(hidden_size, hidden_size + input_size) * 0.01
        self.b_f = np.zeros((hidden_size, 1))
    
    def forward(self, h_prev, x_t):
        """
        前向传播
        h_prev: 上一时刻隐藏状态, shape (hidden_size, 1)
        x_t: 当前输入, shape (input_size, 1)
        """
        # 1. 拼接输入
        concat = np.vstack((h_prev, x_t))
        
        # 2. 线性变换 + Sigmoid
        f_t = sigmoid(np.dot(self.W_f, concat) + self.b_f)
        
        return f_t

# 使用示例
hidden_size = 4
input_size = 3

forget_gate = ForgetGate(hidden_size, input_size)

h_prev = np.random.randn(hidden_size, 1)
x_t = np.random.randn(input_size, 1)

f_t = forget_gate.forward(h_prev, x_t)
print("遗忘门输出 f_t:")
print(f_t)
print(f"\n输出范围: [{f_t.min():.4f}, {f_t.max():.4f}]")

个人经验:初始化权重时用0.01的小值,别用太大。我刚开始做LSTM时,权重初始化太大,Sigmoid直接饱和,梯度消失得一塌糊涂。后来改成小随机数初始化,训练才正常起来。

3.5 遗忘门的可视化理解

光看数字不够直观,我画了一张图帮你理解遗忘门在整个LSTM单元中的位置和作用。

遗忘门在LSTM单元中的位置 C 遗忘门 σ(W·[h,x] + b) × h_{t-1} x_t C_{t-1} C_t = f_t * C_{t-1} 图例 细胞状态主线 遗忘门(Sigmoid) 逐元素乘法

这张图里,红色框就是遗忘门。它接收两个输入,输出一个0到1之间的向量,然后跟细胞状态做乘法。蓝色虚线是细胞状态的主线,信息沿着这条线流动,遗忘门就是这条线上的“阀门”。

3.6 遗忘门的实际应用场景

我在做时间序列预测时,发现遗忘门有几个典型的使用模式:

场景 遗忘门行为 实际例子
长期依赖 f_t 接近1,保留历史信息 股票预测中保留季度趋势
短期突变 f_t 接近0,丢弃旧信息 新闻事件冲击时快速更新
周期性模式 f_t 周期性变化 天气预测中季节更替
噪声过滤 f_t 取中间值,部分保留 传感器数据中的随机波动

避坑指南:我曾经在一个文本生成项目里,把遗忘门的偏置初始化为0。结果模型训练到一半,梯度爆炸了。后来查资料才发现,遗忘门的偏置通常初始化为1或较大的正值,这样一开始模型倾向于“记住”所有信息,再慢慢学会遗忘。这个小技巧能显著提升训练稳定性。

3.7 遗忘门的梯度流动

最后聊点深入的。遗忘门为什么能解决梯度消失?

关键就在那个乘法操作上。在标准RNN中,梯度要经过多个Tanh层,每次乘一个小于1的导数,梯度越传越小。但在LSTM中,细胞状态的梯度传递路径是:

∂C_t / ∂C_{t-1} = f_t

如果 \(f_t\) 接近1,梯度就能无损地传回去。这就是LSTM能捕捉长期依赖的根本原因。

说白了,遗忘门给梯度开了一条高速公路。只要模型学会把遗忘门打开(输出接近1),梯度就能畅通无阻地往回传。

嗯,这个设计真的非常巧妙。我第一次理解这个机制时,忍不住拍了一下桌子——太漂亮了。


好了,遗忘门就讲到这里。下一节咱们聊输入门,看看新信息是怎么写入细胞状态的。

公众号:蓝海资料掘金营,微信deep3321