← Back to Learning Notes

Lecture 8:循环神经网络与LSTM

Chinese-language Computer Science 408 note: Lecture 8:循环神经网络与LSTM. Original source available where applicable.

On this page

This note is currently available in Chinese. The original wording, formulas and diagrams are preserved to avoid an unverified technical translation.

描述:本节从序列概率与语言模型出发,整理文本预处理、RNN 状态递推、困惑度、梯度裁剪和序列任务类型;随后完整推导 LSTM 的输入门、遗忘门、输出门、候选记忆、记忆单元和隐藏状态,并说明 LSTM-Attention 与编码器-解码器架构。

8.1 序列数据

8.1.1 定义与例子

序列数据中的样本按时间、位置或逻辑顺序排列,当前数据通常与前面观察到的数据相关,而不能简单视为相互独立。

课件给出的例子包括:

  • 电影评分会随时间变化:获奖后可能上升,奖项影响淡化后又会变化。
  • 连续看过很多好电影后,观众期望提高,后续评分标准可能改变。
  • 贺岁档、暑期档等季节性因素会影响电影数据。
  • 导演或演员的负面报道可能使评分下降。
  • 音乐、文本和视频内部都具有连续结构。
  • 大地震后出现若干较小余震,也体现时间依赖。

8.1.2 联合概率的链式展开

设依次观察到:

$$ X_1,X_2,\ldots,X_T, $$

它们的联合分布为:

$$ p(x_1,x_2,\ldots,x_T). $$

由条件概率:

$$ p(a,b)=p(a)p(b\mid a)=p(b)p(a\mid b), $$

可得链式法则:

$$ p(x_1,\ldots,x_T) =p(x_1) \prod_{t=2}^{T} p(x_t\mid x_1,\ldots,x_{t-1}). $$

该公式不要求变量独立,但完整条件历史会随 $t$ 增长,直接建模很困难。

8.1.3 自回归模型

自回归模型使用序列自身的过去值预测当前或未来值:

$$ p(x_t\mid x_1,\ldots,x_{t-1}). $$

可以把历史压缩成特征函数:

$$ p(x_t\mid x_1,\ldots,x_{t-1}) =p!\left(x_t\mid f(x_1,\ldots,x_{t-1})\right). $$

常见简化有两类:

  1. 有限阶马尔可夫模型:只使用最近 $\tau$ 个观测。

$$ p(x_t\mid x_1,\ldots,x_{t-1}) \approx p(x_t\mid x_{t-\tau},\ldots,x_{t-1}). $$

  1. 潜变量模型:用隐藏状态 $h_t$ 概括历史信息,再基于 $h_t$ 预测。

NOTE 自回归说明“用自身过去预测未来”;马尔可夫假设限制回看的窗口;潜变量模型则学习一个历史摘要。三者相关,但不是同一个概念。

8.2 文本预处理

课件使用《时间机器》(Time Machine)文本演示四个步骤:

  1. 下载并读取文本。
  2. 用正则表达式清理非字母字符并统一为小写。
  3. 按词或字符切分 token。
  4. 统计 token 频率并构造词表。

下面把课件中被分页和水印打断的代码整理为可读、可解析版本:

import collections
import re

from d2l import torch as d2l


d2l.DATA_HUB["time_machine"] = (
    d2l.DATA_URL + "timemachine.txt",
    "090b5e7e70c295757f55df93cb0a180b9691891a",
)


def read_time_machine():
    """读取并规范化《时间机器》文本。"""
    with open(d2l.download("time_machine"), "r") as file:
        lines = file.readlines()
    return [
        re.sub("[^A-Za-z]+", " ", line).strip().lower()
        for line in lines
    ]


def tokenize(lines, token="word"):
    """按单词或字符切分文本行。"""
    if token == "word":
        return [line.split() for line in lines]
    if token == "char":
        return [list(line) for line in lines]
    raise ValueError(f"未知令牌类型:{token}")


def count_corpus(tokens):
    """统计 token 频率。"""
    if not tokens:
        return collections.Counter()
    if isinstance(tokens[0], list):
        tokens = [token for line in tokens for token in line]
    return collections.Counter(tokens)


class Vocab:
    """把 token 映射为整数下标。"""

    def __init__(self, tokens=None, min_freq=0, reserved_tokens=None):
        if tokens is None:
            tokens = []
        if reserved_tokens is None:
            reserved_tokens = []

        counter = count_corpus(tokens)
        self.token_freqs = sorted(
            counter.items(), key=lambda item: item[1], reverse=True
        )

        self.idx_to_token = ["<unk>"] + list(reserved_tokens)
        self.token_to_idx = {
            token: idx for idx, token in enumerate(self.idx_to_token)
        }

        for token, freq in self.token_freqs:
            if freq < min_freq:
                break
            if token not in self.token_to_idx:
                self.idx_to_token.append(token)
                self.token_to_idx[token] = len(self.idx_to_token) - 1

    def __len__(self):
        return len(self.idx_to_token)

    def __getitem__(self, tokens):
        if not isinstance(tokens, (list, tuple)):
            return self.token_to_idx.get(tokens, 0)
        return [self[token] for token in tokens]


lines = read_time_machine()
tokens = tokenize(lines, token="word")
vocab = Vocab(tokens)

WARNING · 课件代码纠错 清洗表达式应为 [^A-Za-z]+;应先 strip() 再 lower();分词判断变量是 token 而不是 tolen;词频应从 counter.items() 排序。这些 OCR 或原代码拼写问题已在上面修正。

8.3 语言模型

8.3.1 目标

语言模型估计 token 序列的联合概率:

$$ p(x_1,x_2,\ldots,x_T)

\prod_{t=1}^{T} p(x_t\mid x_1,\ldots,x_{t-1}). $$

训练时常把序列错开一位:输入前缀,预测下一个 token。

8.3.2 基于计数的估计

设语料总 token 数为 $n$,$n(x)$ 是 token $x$ 的出现次数,$n(x,x')$ 是连续二元组 $(x,x')$ 的出现次数。

单个 token 的最大似然估计为:

$$ \hat p(x)=\frac{n(x)}{n}. $$

二元组概率可写为:

$$ \hat p(x,x') =\hat p(x)\hat p(x'\mid x) =\frac{n(x)}{n}\frac{n(x,x')}{n(x)}. $$

对于三元序列:

$$ \hat p(x,x',x'') =\frac{n(x)}{n} \frac{n(x,x')}{n(x)} \frac{n(x,x',x'')}{n(x,x')}. $$

8.3.3 $n$ 元语法与马尔可夫假设

若序列很长,完整历史条件概率难以可靠估计,可以采用有限阶马尔可夫假设。

**一元语法(unigram)**假设各 token 独立:

$$ p(x_1,x_2,x_3,x_4) \approx p(x_1)p(x_2)p(x_3)p(x_4). $$

计数形式为:

$$ \frac{n(x_1)}{n} \frac{n(x_2)}{n} \frac{n(x_3)}{n} \frac{n(x_4)}{n}. $$

**二元语法(bigram)**只依赖前一个 token:

$$ p(x_1,x_2,x_3,x_4) \approx p(x_1) p(x_2\mid x_1) p(x_3\mid x_2) p(x_4\mid x_3). $$

计数形式为:

$$ \frac{n(x_1)}{n} \frac{n(x_1,x_2)}{n(x_1)} \frac{n(x_2,x_3)}{n(x_2)} \frac{n(x_3,x_4)}{n(x_3)}. $$

更一般的 $n$ 元语法使用前 $n-1$ 个 token。阶数越高,条件越精细,但组合数量和稀疏性也越严重,实际还需要平滑或神经语言模型。

8.4 循环神经网络 RNN

8.4.1 潜变量自回归

RNN 用隐藏状态表示到当前为止的历史。课件以生成顺序表示为:

$$ p(h_t\mid h_{t-1},x_{t-1}), $$

以及:

$$ p(x_t\mid h_t,x_{t-1}). $$

标准确定性 RNN 把状态更新写为:

$$ \boldsymbol h_t =\phi!\left( W_{hh}\boldsymbol h_{t-1} +W_{xh}\boldsymbol x_t +\boldsymbol b_h \right). $$

输出分数为:

$$ \boldsymbol o_t =W_{ho}\boldsymbol h_t+\boldsymbol b_o. $$

再对 $\boldsymbol o_t$ 做 Softmax,可得到下一个 token 的概率分布。

8.4.2 与 MLP 的区别

如果去掉 $\boldsymbol h_{t-1}$,每个时间步只依赖当前输入,模型退化为逐位置共享参数的前馈网络。

RNN 的关键是:

  • 当前状态同时依赖当前输入与前一状态。
  • 同一组参数在所有时间步共享。
  • 隐藏状态沿时间传播,充当历史摘要。

8.4.3 “你好,世界!”图的正确时序

课件图将观察序列写为:

你  好  ,  世  界  !

对应目标应错开一个位置:

好  ,  世  界  !  <结束>

即读入 $x_t$ 后,用 $h_t$ 预测 $x_{t+1}$。

WARNING · 课件表述纠错 图中文字“当前时刻的输出是为了预测当前时刻的输入”与图中的错位标签不一致。标准自回归语言模型使用当前及过去信息预测下一个 token;目标 token 在前向计算时不能作为当前输入泄漏给模型。

8.5 交叉熵与困惑度

8.5.1 平均交叉熵

设真实序列为 $x_1,\ldots,x_n$,平均负对数似然为:

$$ \mathcal L =-\frac{1}{n} \sum_{t=1}^{n} \log p(x_t\mid x_1,\ldots,x_{t-1}). $$

平均交叉熵越小,模型给真实 token 分配的概率越大。

8.5.2 困惑度

困惑度(Perplexity,PPL)定义为:

$$ \operatorname{PPL}=\exp(\mathcal L). $$

它可直观理解为模型在每一步面对的“有效平均候选数”:

  • $\operatorname{PPL}=1$ 表示模型对每一步都把概率 1 分给真实 token,是理想情况。
  • 在大小为 $V$ 的词表上,均匀随机预测的困惑度为 $V$。
  • 困惑度越小通常越好,但只有在相同 token 化方式、词表和测试集上比较才有意义。

NOTE 困惑度没有固定的“最差有限值”。若模型给真实 token 的概率趋近 0,负对数似然与困惑度都可趋于无穷大。

8.6 时间反向传播与梯度裁剪

RNN 在 $T$ 个时间步上展开后,反向传播会形成长度为 $O(T)$ 的雅可比矩阵乘积链,容易产生梯度消失或梯度爆炸。

梯度裁剪主要限制梯度爆炸。若总梯度向量为 $\boldsymbol g$,阈值为 $\theta$,全局范数裁剪为:

$$ \boldsymbol g \leftarrow \min!\left(1,\frac{\theta}{\lVert\boldsymbol g\rVert}\right) \boldsymbol g. $$

因此:

  • 当 $\lVert\boldsymbol g\rVert\le\theta$ 时,梯度不变。
  • 当 $\lVert\boldsymbol g\rVert>\theta$ 时,方向不变,范数缩放为 $\theta$。

WARNING 梯度裁剪主要防止爆炸,不能从根本上解决梯度消失。

8.7 RNN 的输入输出形式

结构 输入/输出关系 典型任务
one-to-one 一个输入,一个输出 普通分类或回归
one-to-many 一个输入,生成多个输出 条件文本生成、图像描述
many-to-one 多个输入,一个输出 文本分类、序列分类
many-to-many 多个输入,多个输出 序列标注、问答、机器翻译

课件图中重点标出了:

  • one-to-many:文本生成。
  • many-to-one:文本分类。
  • many-to-many:问答、机器翻译。

many-to-many 还可分为输入输出同步对齐和编码完整输入后再逐步解码两种情况。

8.8 LSTM 基础

长短期记忆网络(Long Short-Term Memory,LSTM)是一种带门控记忆单元的 RNN,用于改善普通 RNN 对长程依赖的学习能力。

每个时间步接收:

  • 当前输入 $X_t$。
  • 前一隐藏状态 $H_{t-1}$。
  • 前一记忆单元状态 $C_{t-1}$。

每个时间步输出:

  • 当前隐藏状态 $H_t$。
  • 当前记忆单元状态 $C_t$。

LSTM 的四条控制支路是:

  1. 遗忘门 $F_t$。
  2. 输入门 $I_t$。
  3. 候选记忆 $\widetilde C_t$。
  4. 输出门 $O_t$。

课件图中的 “Special Neuron: 4 inputs, 1 output” 强调了一个 LSTM 单元内部由多路输入和门控共同决定输出,而不是单一的普通神经元变换。

8.9 LSTM 三个门

下面沿用课件的行向量记号。$\sigma$ 是 Sigmoid,输出范围为 $(0,1)$;$\odot$ 表示逐元素乘法。

8.9.1 遗忘门

遗忘门决定旧记忆 $C_{t-1}$ 保留多少:

$$ F_t =\sigma!\left( X_tW_{xf}+H_{t-1}W_{hf}+b_f \right). $$

  • $F_t$ 接近 1:尽量保留对应旧记忆。
  • $F_t$ 接近 0:尽量遗忘对应旧记忆。

8.9.2 输入门

输入门决定候选新信息写入多少:

$$ I_t =\sigma!\left( X_tW_{xi}+H_{t-1}W_{hi}+b_i \right). $$

  • $I_t$ 接近 1:更多写入候选记忆。
  • $I_t$ 接近 0:较少写入候选记忆。

8.9.3 输出门

输出门决定当前记忆暴露给隐藏状态多少:

$$ O_t =\sigma!\left( X_tW_{xo}+H_{t-1}W_{ho}+b_o \right). $$

  • $O_t$ 接近 1:更多输出。
  • $O_t$ 接近 0:更少输出。

WARNING · 课件符号纠错 课件第 6-8 页部分文字把 $I_t$ 叫作遗忘门、把 $F_t$ 叫作输入门,与其权重下标和最终总公式冲突。标准记号是:$F_t$ 为 forget gate,$I_t$ 为 input gate;本笔记统一采用标准记号。

8.10 候选记忆

候选记忆表示当前时间步可能写入的新信息:

$$ \widetilde C_t =\tanh!\left( X_tW_{xc}+H_{t-1}W_{hc}+b_c \right). $$

它通常位于 $(-1,1)$,但不会全部直接写入记忆单元,而是受输入门 $I_t$ 调节。

课件中的含义可以整理为:

  • $\widetilde C_t$:当前输入和历史状态共同生成的候选新内容。
  • $I_t\odot\widetilde C_t$:实际准备写入的内容。
  • 候选内容可能大量采用,也可能几乎不用。

8.11 记忆单元更新

LSTM 的核心更新公式为:

$$ C_t =F_t\odot C_{t-1} +I_t\odot\widetilde C_t. $$

两项分别表示:

  1. $F_t\odot C_{t-1}$:保留的旧记忆。
  2. $I_t\odot\widetilde C_t$:写入的新记忆。

极端情况:

  • $F_t=1,I_t=0$:只保留旧记忆。
  • $F_t=0,I_t=1$:丢弃旧记忆,主要采用新候选。
  • $F_t=0,I_t=0$:旧记忆和新候选都不保留。
  • 两门均取中间值:按维度混合旧记忆与新信息。

8.11.1 梯度为什么更稳定

记忆单元采用加性更新:

$$ C_t=F_t\odot C_{t-1}+\cdots, $$

沿记忆通路有:

$$ \frac{\partial C_t}{\partial C_{t-1}}=F_t. $$

若需要长期保留信息,模型可学习令有关维度的 $F_t$ 接近 1,使梯度沿近似线性的记忆通路传播,减轻普通 RNN 反复矩阵乘法造成的快速衰减。

门控中的 Sigmoid 与候选记忆中的 $\tanh$ 也把数值限制在合理范围。

NOTE LSTM 是“缓解”长序列中的梯度消失和爆炸,不保证在任意序列、初始化和训练设置下彻底消除问题;实际仍常配合梯度裁剪。

8.12 隐藏状态

当前隐藏状态为:

$$ H_t=O_t\odot\tanh(C_t). $$

其中:

  • $\tanh(C_t)$ 把记忆内容压到 $(-1,1)$。
  • $O_t$ 控制哪些记忆维度向外输出。
  • $H_t$ 既传给下一时间步,也可供当前预测头使用。

记忆单元 $C_t$ 与隐藏状态 $H_t$ 的区别:

状态 主要作用 更新方式
$C_t$ 长期记忆通路 遗忘旧记忆并加上候选新记忆
$H_t$ 当前可见表示/短期输出 对 $C_t$ 做 $\tanh$ 后受输出门控制

8.13 LSTM 总公式

$$ \begin{aligned} I_t&=\sigma(X_tW_{xi}+H_{t-1}W_{hi}+b_i),\ F_t&=\sigma(X_tW_{xf}+H_{t-1}W_{hf}+b_f),\ O_t&=\sigma(X_tW_{xo}+H_{t-1}W_{ho}+b_o),\ \widetilde C_t&=\tanh(X_tW_{xc}+H_{t-1}W_{hc}+b_c),\ C_t&=F_t\odot C_{t-1}+I_t\odot\widetilde C_t,\ H_t&=O_t\odot\tanh(C_t). \end{aligned} $$

记忆方法:

忘多少旧信息:F_t
写多少新信息:I_t
新信息是什么:C~_t
当前记忆是什么:C_t
向外显示多少:O_t
当前隐藏输出:H_t

8.14 LSTM 代码实现

下面保留课件“同时计算四个门”的结构,修正分页造成的拼写和缩进错误,并采用与公式一致的门顺序:输入门、遗忘门、候选记忆、输出门。

import torch
import torch.nn as nn


class LSTMCell(nn.Module):
    """单个 LSTM 时间步。"""

    def __init__(self, input_size, hidden_size):
        super().__init__()
        self.input_size = input_size
        self.hidden_size = hidden_size

        # 右乘约定:
        # x @ weight_ih: [batch, input] @ [input, 4 * hidden]
        # h @ weight_hh: [batch, hidden] @ [hidden, 4 * hidden]
        self.weight_ih = nn.Parameter(
            torch.empty(input_size, 4 * hidden_size)
        )
        self.weight_hh = nn.Parameter(
            torch.empty(hidden_size, 4 * hidden_size)
        )
        self.bias = nn.Parameter(torch.empty(4 * hidden_size))
        self.reset_parameters()

    def reset_parameters(self):
        nn.init.xavier_uniform_(self.weight_ih)
        nn.init.xavier_uniform_(self.weight_hh)
        nn.init.zeros_(self.bias)

    def forward(self, x, state):
        """
        x: [batch_size, input_size]
        state: (h_prev, c_prev),均为 [batch_size, hidden_size]
        """
        h_prev, c_prev = state

        gates = x @ self.weight_ih + h_prev @ self.weight_hh + self.bias

        # 顺序:输入门、遗忘门、候选记忆、输出门。
        i_gate, f_gate, g_gate, o_gate = gates.chunk(4, dim=1)

        i_gate = torch.sigmoid(i_gate)
        f_gate = torch.sigmoid(f_gate)
        g_gate = torch.tanh(g_gate)
        o_gate = torch.sigmoid(o_gate)

        c_next = f_gate * c_prev + i_gate * g_gate
        h_next = o_gate * torch.tanh(c_next)
        return h_next, c_next


class LSTM(nn.Module):
    """用自定义 LSTMCell 处理完整序列。"""

    def __init__(self, input_size, hidden_size):
        super().__init__()
        self.lstm_cell = LSTMCell(input_size, hidden_size)
        self.hidden_size = hidden_size

    def forward(self, x, initial_state=None):
        """
        x: [seq_len, batch_size, input_size]
        initial_state: (h0, c0)

        返回:
        outputs: [seq_len, batch_size, hidden_size]
        (h_last, c_last): 最终状态
        """
        seq_len, batch_size, _ = x.shape
        outputs = []

        if initial_state is None:
            h = x.new_zeros(batch_size, self.hidden_size)
            c = x.new_zeros(batch_size, self.hidden_size)
        else:
            h, c = initial_state

        for t in range(seq_len):
            h, c = self.lstm_cell(x[t], (h, c))
            outputs.append(h.unsqueeze(0))

        outputs = torch.cat(outputs, dim=0)
        return outputs, (h, c)

代码中的形状关系:

$$ [B,D_x]@[D_x,4D_h] +[B,D_h]@[D_h,4D_h] \rightarrow[B,4D_h]. $$

再把最后一维等分成四个 $[B,D_h]$ 张量。

8.15 LSTM 与 Attention

8.15.1 动机

LSTM 可以把历史压入隐藏状态,但很长序列中的早期关键信息仍可能被最终状态弱化。

注意力机制不要求所有时间步贡献相同,而是:

  1. 保存每个时间步的隐藏状态 $h_t$。
  2. 计算每个 $h_t$ 对当前任务的重要性分数。
  3. 用 Softmax 把分数转换为归一化权重。
  4. 对隐藏状态加权求和,得到上下文向量。

它广泛用于时间序列预测、自然语言处理和编码器-解码器模型。

8.15.2 相似度/重要性分数

课件给出一种简化加性打分:

$$ e_t =v^{\mathrm T}\tanh(W_hh_t). $$

若需要让注意力依赖当前查询 $q$,更一般地可写为:

$$ e_t =v^{\mathrm T}\tanh(W_hh_t+W_qq+b). $$

8.15.3 Softmax 权重

$$ \alpha_t =\frac{\exp(e_t)} {\sum_{k=1}^{T}\exp(e_k)}. $$

因此:

$$ \alpha_t>0, \qquad \sum_{t=1}^{T}\alpha_t=1. $$

8.15.4 上下文向量

$$ h_{\mathrm{attention}} =\sum_{t=1}^{T}\alpha_th_t. $$

网络结构为:

输入时间序列
   ↓
LSTM:生成 h_1, h_2, ..., h_T
   ↓
Attention:打分并归一化为 α_1, ..., α_T
   ↓
上下文向量 h_attention
   ↓
输出层:生成最终预测

8.16 LSTM-Attention 数值例子

8.16.1 数据与隐藏状态

课件给出的前五个时间步数据为:

时间步 数据值 假设隐藏状态
1 100 0.5
2 102 0.7
3 98 0.4
4 105 0.8
5 103 0.6

假设注意力分数为:

$$ (e_1,e_2,e_3,e_4,e_5) =(0.1,0.3,0.2,0.4,0.2). $$

8.16.2 课件的示意权重

课件把权重近似写为:

$$ (\alpha_1,\alpha_2,\alpha_3,\alpha_4,\alpha_5) =(0.1,0.2,0.2,0.3,0.2). $$

按这组示意权重计算:

$$ \begin{aligned} h_{\mathrm{attention}} &=0.1\times0.5 +0.2\times0.7 +0.2\times0.4\ &\quad+0.3\times0.8 +0.2\times0.6\ &=0.63. \end{aligned} $$

8.16.3 按公式得到的正确 Softmax

对上述分数实际执行 Softmax,约为:

$$ (0.173,0.211,0.191,0.233,0.191). $$

相应上下文向量约为:

$$ h_{\mathrm{attention}} \approx0.612. $$

WARNING · 例题纠错 课件的 $(0.1,0.2,0.2,0.3,0.2)$ 虽然和为 1,但不是分数 $(0.1,0.3,0.2,0.4,0.2)$ 的真实 Softmax;所以 $0.63$ 只能视为使用课件手工示意权重得到的结果。

课件随后假设模型预测第 6 个时间步为 104。这个数还需要输出层参数才能从上下文向量得到,不能只凭 $h_{\mathrm{attention}}=0.63$ 直接推出;它是示意性的最终预测。

8.17 编码器与解码器

8.17.1 基本职责

一个模型可以分成两部分:

  • 编码器(Encoder):读取输入并生成中间表示、特征或状态。
  • 解码器(Decoder):读取编码状态以及可选的解码器输入,生成目标输出。

课件的通用流程图是:

输入 → 编码器 → 状态 → 解码器 → 输出
                         ↑
                    解码器输入

WARNING · 课件文字纠错 “编码器处理输出,解码器生成输出”中的前一个“输出”应为“输入”。

8.17.2 CNN 中的对应关系

课件用图像分类网络作类比:

  • 前面的卷积层负责特征提取,可看作编码器。
  • 最后的输出层把特征映射为类别,可看作简单解码头。

在更复杂的生成任务中,解码器通常不只是一个 Softmax 层,而会逐步产生结构化输出。

8.17.3 RNN 中的对应关系

课件的 this movie is great 图表示:

  1. Embedding 把每个词转换为向量。
  2. LSTM 编码完整输入序列。
  3. 最后隐藏状态概括文本。
  4. Dense 层根据该向量输出结果。

这是 many-to-one 分类结构。对于机器翻译等 sequence-to-sequence 任务,解码器会接收编码器状态,并结合前一目标 token 逐步生成输出序列。

8.18 编码器-解码器代码骨架

课件使用一个通用包装器连接编码器和解码器:

import torch.nn as nn


class EncoderDecoder(nn.Module):
    """编码器-解码器架构的基类。"""

    def __init__(self, encoder, decoder, **kwargs):
        super().__init__(**kwargs)
        self.encoder = encoder
        self.decoder = decoder

    def forward(self, enc_x, dec_x, *args):
        enc_outputs = self.encoder(enc_x, *args)
        dec_state = self.decoder.init_state(enc_outputs, *args)
        return self.decoder(dec_x, dec_state)

数据流为:

  1. encoder(enc_x) 生成编码器输出。
  2. decoder.init_state(...) 把编码器输出转换为解码初始状态。
  3. decoder(dec_x, dec_state) 根据解码器输入和状态生成结果。

注意力模型中,enc_outputs 往往保留所有时间步,而不只保留最后一个向量;解码器可在每一步对这些状态计算注意力。

8.19 易错点

  • 序列联合概率必须按条件概率链式展开,不能在没有独立假设时直接写成边缘概率乘积。
  • 自回归模型使用过去值预测当前或未来,马尔可夫模型只是把依赖限制为有限阶。
  • 一元语法假设 token 独立;二元语法只依赖前一个 token;阶数提高会加重稀疏性。
  • 语言模型输入和标签错开一位,当前状态预测下一个 token,不应读取要预测的 token。
  • 困惑度是平均交叉熵的指数;均匀随机基线等于词表大小,且比较时必须统一 token 化与数据集。
  • 梯度裁剪防止梯度爆炸,不会自动解决梯度消失。
  • 普通 RNN 的隐藏状态包含循环依赖;去掉前一隐藏状态后不再具有时序记忆。
  • LSTM 中 $F_t$ 是遗忘门,$I_t$ 是输入门;课件部分文字交换了两者。
  • 候选记忆 $\widetilde C_t$ 不是最终记忆,必须经过输入门后才能写入 $C_t$。
  • $C_t$ 是长期记忆通路,$H_t$ 是受输出门控制的当前可见状态,两者不能混用。
  • LSTM 缓解而非绝对消除长序列梯度问题。
  • 注意力权重必须由 Softmax 正规化;课件数值例子的手工权重不是其给定分数的真实 Softmax。
  • 上下文向量只是输出层输入,若没有输出层参数,不能由它直接推出最终预测值 104。
  • 编码器处理输入,解码器生成输出;序列解码器通常还需要自己的输入 token。

相关知识

8.20 本节复盘

  • 序列数据具有顺序依赖,联合概率可按条件概率链式展开。
  • 自回归模型用历史预测未来;马尔可夫模型限制历史窗口;潜变量模型用状态概括历史。
  • 文本预处理包括清洗、按词/字符切分、计数与词表映射。
  • $n$ 元语法用有限历史估计语言概率,神经语言模型则学习连续隐藏状态。
  • RNN 用 $h_t=\phi(W_{hh}h_{t-1}+W_{xh}x_t+b_h)$ 递推历史表示,并预测下一 token。
  • 困惑度等于平均负对数似然的指数,越低通常越好;梯度裁剪限制梯度全局范数。
  • LSTM 用遗忘门保留旧记忆、输入门写入候选记忆、输出门控制隐藏状态。
  • LSTM 核心公式是 $C_t=F_t\odot C_{t-1}+I_t\odot\widetilde C_t$ 与 $H_t=O_t\odot\tanh(C_t)$。
  • LSTM 的加性记忆通路改善长程梯度传播,但仍可能需要裁剪和合理训练设置。
  • Attention 对各时间步隐藏状态打分、Softmax 归一化并加权求和,使模型动态选择重要历史。
  • 编码器把输入变为状态,解码器结合该状态与自己的输入生成输出;注意力可让解码器访问全部编码状态。