长短期记忆(Long Short-Term Memory,LSTM) 是 RNN 最重要的变体,也是深度学习 NLP 发展史上的里程碑。在 Transformer 出现之前(2017 年),LSTM 是序列建模的绝对王者。


# 背景:RNN 梯度消失

句子:"我住在法国,...(中间 200 个字)... 所以我会说___"
空格应该填 “法语”

RNN 的问题
需要记住 200 个字前的"法国"。反向传播 时,梯度要连乘 200 次,导致前面的信息传不过来。
如果每次乘 0.9 → 0.9^200 ≈ 0.000000001梯度消失


# LSTM 的结构及接口

LSTM 与 RNN 的接口的不同之处在于,LSTM 还有路径 c。这个 c 称为 记忆单元(或者简称为“单元”),相当于 LSTM 专用的记忆部门。

  • 记忆单元的特点:仅在 LSTM 层内部接收和传递数据,对外部不可见,我们甚至不用考虑它的存在
  • sigma\\sigmasigmoid函数用于求门的开合程度(sigmoid函数的输出范围在0.0 ~ 1.0)

# 输出门 Output Gate

  • 作用:决定输出哪些信息作为隐藏状态 h_th\_t(求出隐藏状态 h_th\_t
  • 公式o_t=sigmaleft(x_tW_x(o)+h_t1W_h(o)+b(o)right)o\_t = \\sigma\\left(x\_t W\_x^{(o)} + h\_{t-1} W\_h^{(o)} + b^{(o)}\\right)

隐藏状态 h_t=o_ttanh(c_t)h\_t=o\_t ⊙ \\tanh(c\_t),记忆单元 c_tc\_t 和隐藏状态 h_th\_t 的关系只是按元素应用 tanh\\tanh 函数。这意味着,记忆单元 c_tc\_t 和隐藏状态 h_th\_t 的_元素个数相同_。

⊙ 表示 阿达玛乘积。即,对应元素的乘积


# 遗忘门 Forget Gate

  • 作用:决定从细胞状态中丢弃哪些信息
  • 公式f_t=sigmaleft(x_tW_x(f)+h_t1W_h(f)+b(f)right)f\_t = \\sigma\\left(x\_t W\_x^{(f)} + h\_{t-1} W\_h^{(f)} + b^{(f)}\\right)

# 输入门 Input Gate

  • 作用:决定更新哪些新信息到细胞状态
  • 公式i_t=sigmaleft(x_tW_x(i)+h_t1W_h(i)+b(i)right)i\_t = \\sigma\\left(x\_t W\_x^{(i)} + h\_{t-1} W\_h^{(i)} + b^{(i)}\\right)

# 新的记忆单元

向记忆单元添加的新信息hatc_t=tanhleft(x_tW_x(g)+h_t1W_h(g)+b(g)right)\\hat{c\_t} = \\tanh\\left(x\_t W\_x^{(g)} + h\_{t-1} W\_h^{(g)} + b^{(g)}\\right)
新的记忆单元c_t=fc_t1+ihatc_tc\_t=f ⊙ c\_{t-1} + i ⊙ \\hat{c\_t}细胞状态 = 遗忘 × 旧细胞 + 输入 × 新候选

hatc_t\\hat{c\_t} 也即上面的 g

这个 tanh 节点的作用不是门,而是将新的信息添加到记忆单元中。因此,它不用 sigmoid 函数 作为激活函数,而是使用 tanh 函数


# LSTM 不会梯度消失

观察记忆单元的反向传播:

记忆单元的反向传播仅流过 +× 节点。

  • + 节点 将上游传来的梯度原样流出,所以梯度没有变化(退化)
  • × 节点 的计算并不是矩阵乘积,而是对应元素的乘积(阿达玛积)

# PyTorch 代码示例

基础 LSTM:

import torch
import torch.nn as nn

class SimpleLSTM(nn.Module):
def __init__(self, input_size, hidden_size, num_layers, num_classes):
super().__init__()
self.hidden_size = hidden_size
self.num_layers = num_layers

    # LSTM 层
    self.lstm = nn.LSTM(input\_size, hidden\_size, 
                       num\_layers=num\_layers,
                       batch\_first=True,
                       dropout=0.3)  # 层间 Dropout
    
    # 全连接层
    self.fc = nn.Linear(hidden\_size, num\_classes)

def forward(self, x):
    # x: (batch\_size, seq\_len, input\_size)
    
    # 初始化隐藏状态
    h0 = torch.zeros(self.num\_layers, x.size(0), self.hidden\_size)
    c0 = torch.zeros(self.num\_layers, x.size(0), self.hidden\_size)
    
    # 前向传播
    out, (hn, cn) = self.lstm(x, (h0, c0))
    # out: (batch, seq\_len, hidden\_size)
    # hn: (num\_layers, batch, hidden\_size)
    
    # 取最后一个时间步的输出
    out = self.fc(out\[:, -1, :\])
    return out

# 使用

model = SimpleLSTM(input_size=100, hidden_size=128,
num_layers=2, num_classes=2)

IMDB 情感分析:

import torch.optim as optim

# 超参数

batch_size = 64
seq_len = 100
learning_rate = 0.001
epochs = 10

# 模型、损失、优化器

model = SentimentBiLSTM(vocab_size=10000, embed_dim=300,
hidden_size=128, num_classes=2)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=learning_rate)

# 训练循环

for epoch in range(epochs):
model.train()
total_loss = 0

for batch\_x, batch\_y in train\_loader:
    # batch\_x: (64, 100), batch\_y: (64,)
    
    optimizer.zero\_grad()
    output = model(batch\_x)
    loss = criterion(output, batch\_y)
    loss.backward()
    
    # 梯度裁剪(防止 LSTM 梯度爆炸)⭐
    torch.nn.utils.clip\_grad\_norm\_(model.parameters(), 1.0)
    
    optimizer.step()
    total\_loss += loss.item()

# 验证
model.eval()
correct = 0
total = 0
with torch.no\_grad():
    for batch\_x, batch\_y in val\_loader:
        output = model(batch\_x)
        predicted = torch.argmax(output, dim=1)
        correct += (predicted == batch\_y).sum().item()
        total += batch\_y.size(0)

val\_acc = correct / total
print(f"Epoch {epoch+1}: Loss={total\_loss:.4f}, Val Acc={val\_acc:.4f}")