Homura's Blog TECHNICAL JOURNAL

2026.11.06

从零开始的学AI生活:2.5-RNN 与 CNN——Transformer 之前的序列模型

第 2.5 章:RNN 与 CNN——Transformer 之前的序列模型


1. 序列模型的共同问题

假设一句话被切成 5 个 token:

x1, x2, x3, x4, x5

经过 embedding 后,每个 \(x_i\) 都是一个向量:

e1, e2, e3, e4, e5

整体形状是:

(seq_len, embedding_dim)

例如:

(5, 768)

序列模型要解决的问题是:

如何让当前位置的表示利用其他位置的信息?

这不是普通 MLP (多层感知机) 最擅长的问题。MLP 更适合固定长度输入;而文本长度会变化,位置之间也有顺序关系。


2. 三种信息流动方式

RNN (循环神经网络)、CNN (卷积神经网络) 和 Attention 的核心差异,可以用“信息如何从一个位置传到另一个位置”来理解。

模型 信息流动方式 直观理解
RNN 从左到右逐步传递隐状态 像接力赛,每一步把记忆传给下一步
CNN 用局部窗口提取模式,层数越深看得越远 像滑动窗口提取局部特征
Attention 每个位置直接连接所有位置 像会议室里每个人都能直接提问其他人

如果位置 1 需要影响位置 5:

RNN:       x1 -> h1 -> h2 -> h3 -> h4 -> h5
CNN:       x1 先进入局部窗口,再经过多层卷积影响 x5
Attention: x1 直接和 x5 计算 attention score

这决定了它们的优缺点。


第一部分:RNN

3. RNN 要解决什么问题

RNN 的全称是 Recurrent Neural Network,循环神经网络。

它的核心思想是:

每读一个 token,就更新一次记忆。

这个记忆叫隐状态。

例如读一句话:

I love transformers

RNN 会依次处理:

读 I            -> 更新记忆 h1
读 love         -> 结合 h1,更新记忆 h2
读 transformers -> 结合 h2,更新记忆 h3

最后的 \(h_3\) 被认为包含了整句话的信息。


4. 隐状态是什么

把第 \(t\) 步的隐状态记作:

\[h_t\]

相关符号:

符号 含义
\(x_t\) 第 \(t\) 个 token 的 embedding
\(h_{t-1}\) 上一步隐状态,也就是旧记忆
\(h_t\) 当前隐状态,也就是新记忆
\(W_x\) 输入权重矩阵
\(W_h\) 记忆传递权重矩阵
\(b\) 偏置
\(\tanh\) 非线性激活函数

示例:

h1 = 读完 x1 后的记忆
h2 = 读完 x1, x2 后的记忆
h3 = 读完 x1, x2, x3 后的记忆

注意:

  • \(h_t\) 不是只依赖当前 token
  • 它依赖从 \(x_1\) 到 \(x_t\) 的整段历史
  • 但历史是通过 \(h_{t-1}\) 逐步压缩进来的

5. RNN 的更新公式

最简单的 RNN 公式:

\[h_t=\tanh(W_x x_t + W_h h_{t-1}+b)\]

这个公式做了四件事:

  1. 用 \(W_x x_t\) 提取当前输入的信息
  2. 用 \(W_h h_{t-1}\) 延续旧记忆
  3. 两者相加
  4. 用 \(\tanh\) 把数值压到 \((-1,1)\),避免数值无限增大

可以把它理解成:

new_memory = tanh(current_input + old_memory)

只不过 current_input 和 old_memory 都要先经过各自的线性变换。


6. RNN 示例

为了便于理解,把维度降到 1。

输入:

\[x_1=1.0,\quad x_2=0.5\]

初始记忆:

\[h_0=0\]

参数:

\[W_x=0.8,\quad W_h=0.6,\quad b=0\]

第 1 步

\[h_1=\tanh(W_x x_1+W_h h_0+b)\]

代入:

\[h_1=\tanh(0.8*1.0+0.6*0+0)\] \[h_1=\tanh(0.8)\]

结果约为:

\[h_1=0.664\]

第 2 步

\[h_2=\tanh(0.8*x_2+0.6*h_1)\]

代入:

\[h_2=\tanh(0.8*0.5+0.6*0.664)\] \[h_2=\tanh(0.4+0.398)\] \[h_2=\tanh(0.798)\]

结果约为:

\[h_2=0.663\]

这说明:

  • \(h_1\) 依赖 \(x_1\)
  • \(h_2\) 依赖 \(x_2\) 和 \(h_1\)
  • 因此 \(h_2\) 间接保留了 \(x_1\) 的信息

7. RNN 的输出

RNN 每一步都可以接一个输出层:

\[y_t=W_y h_t+b_y\]

如果是语言模型,则:

\[P(x_{t+1}|x_{\le t})=\text{softmax}(y_t)\]

也就是:

读入 x1          -> 预测 x2
读入 x1, x2      -> 预测 x3
读入 x1, x2, x3  -> 预测 x4

训练目标仍然是 next-token prediction:

\[P(x_t|x_{<t})\]

这点和 Transformer 一样,区别只是上下文表示的计算方式。


8. RNN 的参数共享

RNN 在每个时间步使用同一组参数:

W_x, W_h, b

伪代码:

hidden = h0
for token in tokens:
    hidden = update(token, hidden)

这带来两个好处:

  1. 可以处理任意长度序列
  2. 同一个模式在不同位置可以复用同一套规则

例如“否定词 + 形容词”的模式,无论出现在第 3 个位置还是第 30 个位置,理论上都可以用同一组参数识别。


9. RNN 的优点

  1. 天然适合序列
  2. 能处理变长输入
  3. 参数数量与序列长度无关
  4. 适合流式处理,例如实时语音或逐 token 生成
  5. 在历史任务中曾广泛用于语言模型、机器翻译和语音识别

10. RNN 的问题

10.1 长距离依赖困难

例如:

The book that I bought yesterday is on the table.

book 和 is 有语法关联,但中间隔了很多词。

RNN 中,早期信息必须沿隐状态链传递:

\[x_1 \rightarrow h_1 \rightarrow h_2 \rightarrow \dots \rightarrow h_t\]

这会导致:

  • 梯度消失
  • 梯度爆炸
  • 旧信息被新信息覆盖

10.2 难以并行

必须先算 \(h_1\),才能算 \(h_2\):

h1 -> h2 -> h3 -> h4

这不利于充分利用 GPU。

10.3 固定容量记忆

\(h_t\) 是固定长度向量。

序列越长,越难把所有历史信息都压缩进同一个向量。


11. LSTM 和 GRU

为了缓解 RNN 的记忆问题,出现了 LSTM 和 GRU。

11.1 LSTM

LSTM 的全称是 Long Short-Term Memory (长短期记忆网络)。

核心思想:

给记忆加门控,学会决定哪些信息保留、哪些信息遗忘。

它有三种门:

门 作用
forget gate 决定旧记忆遗忘多少
input gate 决定当前输入写入多少
output gate 决定当前记忆输出多少

LSTM 引入了单独的 cell state,比普通 RNN 更适合长序列。

11.2 GRU

GRU 的全称是 Gated Recurrent Unit (门控循环单元)。

它简化了 LSTM,通常有更少参数、更快训练速度。

但 LSTM 和 GRU 仍然是循环结构:

h1 -> h2 -> h3 -> h4

因此仍然难以完全并行,长距离路径仍然是线性长度。


第二部分:CNN

12. CNN 要解决什么问题

CNN 的全称是 Convolutional Neural Network,卷积神经网络。

它最初主要用于图像,因为图像有很强的局部结构:

相邻像素组成边缘
边缘组成纹理
纹理组成局部形状
局部形状组成物体

CNN 的做法是:

用一个小的滑动窗口,在整张图上反复提取相同类型的局部模式。

这个小窗口叫卷积核。


13. 卷积核是什么

假设有一个一维序列:

x = [1, 0, -1, 2, 1]

定义一个长度为 3 的卷积核:

kernel = [0.25, 0.5, 0.25]

它像一个权重模板:

前一个位置权重:0.25
当前位置权重:0.50
后一个位置权重:0.25

卷积就是让这个模板从左到右滑动。

每个位置都计算:

加权求和

14. 一维卷积计算

输入:

x = [1, 0, -1, 2, 1]

卷积核:

w = [0.25, 0.5, 0.25]

偏置:

b = 0

第一个窗口

[1, 0, -1]

计算:

\[y_1=0.25*1+0.5*0+0.25*(-1)+0\] \[y_1=0\]

第二个窗口

[0, -1, 2]

计算:

\[y_2=0*0+0.5*(-1)+0.25*2\] \[y_2=0\]

第三个窗口

[-1, 2, 1]

计算:

\[y_3=0.25*(-1)+0.5*2+0.25*1\] \[y_3=1.0\]

最终:

y = [0, 0, 1.0]

这个输出叫特征图。


15. 一维卷积公式

\[y_t=\sum_{k=0}^{K-1}w_k x_{t+k}+b\]

各项含义:

符号 含义
\(x_t\) 第 \(t\) 个位置的输入
\(w_k\) 卷积核中第 \(k\) 个权重
\(K\) 卷积核大小
\(b\) 偏置
\(y_t\) 第 \(t\) 个输出特征

如果 \(K=3\),当前位置会同时看到:

前一个位置
当前位置
后一个位置

16. 文本 CNN

对文本来说,输入不是单个数字,而是每个 token 的 embedding 向量。

假设:

序列长度 = 5
embedding_dim = 768
kernel size = 3

一个卷积核每次看:

3 个连续 token

它可能学到:

n-gram 模式
短语特征
局部搭配

例如:

"not good"
"very good"
"machine learning"

这些模式都出现在连续几个 token 内,适合 CNN 捕捉。


17. 多通道输入

图像通常是多通道的。

例如 RGB 图片:

shape = (3, 高, 宽)

3 表示:

R 通道
G 通道
B 通道

卷积核也要同时在通道维度上计算。

对文本来说,embedding 维度就像通道:

shape = (seq_len, embedding_dim)

卷积核会在 embedding 维度和局部 token 窗口上做加权求和。


18. 多个卷积核

一个卷积核只能提取一种模式。

为了同时提取多种特征,会使用多个卷积核。

例如:

输入 shape = (seq_len, embedding_dim)
卷积核数量 = 256
kernel size = 3

输出大致是:

(新的 seq_len, 256)

可以理解为:

256 个不同的模式检测器

有的核可能关注否定词。

有的核可能关注形容词。

有的核可能关注标点结构。


19. 二维卷积

图像通常是二维的。

假设输入是一个 \(5\times5\) 的灰度图:

1 1 1 0 0
1 1 1 0 0
1 1 1 0 0
0 0 0 0 0
0 0 0 0 0

使用一个 \(3\times3\) 卷积核:

1 0 -1
1 0 -1
1 0 -1

这个核可以检测垂直边缘。

每个窗口的计算方式是:

对应位置相乘
再全部相加

例如左上角的 \(3\times3\) 窗口:

1 1 1
1 1 1
1 1 1

与卷积核:

1 0 -1
1 0 -1
1 0 -1

逐元素相乘:

1 0 -1
1 0 -1
1 0 -1

求和:

\[1+0+(-1)+1+0+(-1)+1+0+(-1)=0\]

如果窗口左边是 1、右边是 0:

1 1 0
1 1 0
1 1 0

卷积结果会是一个较大的正数,表示检测到了从左到右变暗的边缘。


20. Padding

如果不在边缘补零,序列会变短。

一维例子:

输入长度 5
kernel size 3
输出长度 3

如果想保持长度不变,可以在左右补零:

[0, 1, 0, -1, 2, 1, 0]

这叫 padding。

常见选项:

  • padding=0:不补零
  • padding=1:左右各补 1 个零
  • padding="same":尽量保持输出长度不变

21. Stride

stride 是窗口每次移动几步。

如果 stride=1:

每次移动 1 个位置

如果 stride=2:

每次移动 2 个位置

输出长度会更短。

stride 越大:

  • 计算量越小
  • 输出尺寸越小
  • 保留的位置信息越粗

22. 池化

池化用来降低序列或图像尺寸。

常见的是 max pooling。

假设卷积输出:

[0, 0, 1.0]

做窗口大小为 2 的 max pooling:

max(0, 0) -> 0
max(1.0)    -> 1.0

结果:

[0, 1.0]

池化的作用:

  1. 减少计算量
  2. 让特征对小幅位移更稳定
  3. 保留较强的局部信号

23. 感受野 (Receptive Field)

感受野表示:

某个输出位置能看到多大范围的原始输入。

一层 kernel size=3 的卷积只能看 3 个相邻位置。

两层堆叠后,理论上能间接看到更大范围。

层数越多,感受野越大。

这是 CNN 能处理较大图像的原因之一。


24. CNN 的优点

  1. 局部模式提取能力强
  2. 参数共享:同一个卷积核在所有位置复用
  3. 可以并行计算
  4. 对图像、语音、文本都有效
  5. 层数越多,感受野越大
  6. 对小幅平移比较稳定

25. CNN 的问题

  1. 单层只能看局部窗口
  2. 长距离依赖需要很多层
  3. 全局关系不如 Attention 直接
  4. 强假设“局部相邻信息更重要”
  5. 对非常长的上下文不如 Attention 灵活

第三部分:RNN、CNN 与 Attention 的关系

26. RNN、CNN、Attention 对比

特性 RNN CNN Attention
核心机制 循环更新隐状态 滑动窗口卷积 Q/K/V 全局匹配
参数共享 时间步共享 位置共享 Q/K/V 投影共享
长距离路径 线性长度 随层数逐渐扩大 一步直达
并行性 差 好 好
显存复杂度 \(O(n)\) \(O(n)\) \(O(n^2)\)
局部模式 较弱 强 可学习
全局依赖 容易遗忘 需要堆叠层数 直接建模
典型用途 早期序列建模、语音、流式处理 图像、语音、文本分类 Transformer、LLM

其中 \(n\) 是序列长度。


27. 为什么 Transformer 选择 Attention

Attention 的优势:

  1. 每个位置可以直接访问任意其他位置
  2. 计算可以高度并行
  3. 长距离依赖路径长度为 1
  4. 多头机制允许同时学习多种关系
  5. 可以扩展到非常大的模型和数据

代价是:

\[O(n^2)\]

因此现代 LLM 会做很多优化,例如:

  • FlashAttention
  • Grouped-Query Attention
  • Sliding Window Attention
  • KV cache
  • 稀疏注意力

这些优化不改变核心思想:

用 Q 和 K 的相似度动态决定应该关注哪些 V。


28. 三个公式放在一起看

RNN 的上下文来自:

\[h_t=f(x_t,h_{t-1})\]

CNN 的上下文来自:

\[y_t=\sum_k w_k x_{t+k}\]

Attention 的上下文来自:

\[z_i=\sum_j \alpha_{ij}v_j\]

关键区别:

  • RNN 的记忆传递路径是固定的递归链
  • CNN 的窗口和卷积核权重是局部、预定义的
  • Attention 的权重 \(\alpha_{ij}\) 由输入动态计算

因此 Attention 可以理解为:

根据当前内容动态生成权重,再对不同位置做加权求和。


29. 代码

以下代码实现了:

  1. 计算一个简单 RNN 的隐状态
  2. 演示 nn.RNN 的输入输出形状
  3. 计算一维卷积
  4. 演示二维卷积边缘检测
  5. 用一个序列分类任务比较 RNN 和 CNN
#!/usr/bin/env python
"""Chapter 2.5: hands-on RNN and CNN examples."""

from __future__ import annotations

import argparse
import math
import random

import torch
from torch import nn
import torch.nn.functional as F
from torch.utils.data import DataLoader, TensorDataset


def demo_manual_rnn() -> None:
    print("=== 1. manual RNN: h_t = tanh(0.8*x_t + 0.6*h_{t-1}) ===")
    inputs = [1.0, 0.5, 1.2]
    hidden = 0.0

    for step, value in enumerate(inputs, start=1):
        old_hidden = hidden
        hidden = math.tanh(0.8 * value + 0.6 * old_hidden)
        print(
            f"step {step} | x={value:.3f} | old h={old_hidden:+.6f} | "
            f"new h={hidden:+.6f}"
        )


def demo_pytorch_rnn() -> None:
    print("\n=== 2. PyTorch nn.RNN shapes ===")
    model = nn.RNN(input_size=3, hidden_size=4, batch_first=True)
    inputs = torch.randn(2, 5, 3)
    outputs, final_hidden = model(inputs)

    print(f"input shape:        {tuple(inputs.shape)}")
    print(f"all outputs shape:  {tuple(outputs.shape)}")
    print(f"final hidden shape: {tuple(final_hidden.shape)}")
    print("meaning: (batch=2, seq_len=5, input_dim=3) -> (batch=2, seq_len=5, hidden_dim=4)")


def demo_conv1d() -> None:
    print("\n=== 3. manual Conv1d example ===")
    inputs = torch.tensor([[[1.0, 0.0, -1.0, 2.0, 1.0]]])
    weight = torch.tensor([[[0.25, 0.50, 0.25]]])
    bias = torch.tensor([0.0])
    output = F.conv1d(inputs, weight, bias=bias, stride=1, padding=0)

    print(f"input shape:  {tuple(inputs.shape)}")
    print(f"weight shape: {tuple(weight.shape)}")
    print(f"output shape: {tuple(output.shape)}")
    print(f"input values:  {inputs[0, 0].tolist()}")
    print(f"kernel values: {weight[0, 0].tolist()}")
    print(f"output values: {output[0, 0].tolist()}")


def demo_conv2d_edge_detector() -> None:
    print("\n=== 4. Conv2d edge detector ===")
    image = torch.tensor(
        [
            [
                [
                    [1.0, 1.0, 1.0, 0.0, 0.0],
                    [1.0, 1.0, 1.0, 0.0, 0.0],
                    [1.0, 1.0, 1.0, 0.0, 0.0],
                    [0.0, 0.0, 0.0, 0.0, 0.0],
                    [0.0, 0.0, 0.0, 0.0, 0.0],
                ]
            ]
        ]
    )
    vertical_edge_kernel = torch.tensor(
        [
            [
                [
                    [1.0, 0.0, -1.0],
                    [1.0, 0.0, -1.0],
                    [1.0, 0.0, -1.0],
                ]
            ]
        ]
    )
    output = F.conv2d(image, vertical_edge_kernel)

    print(f"image shape:  {tuple(image.shape)}")
    print(f"kernel shape: {tuple(vertical_edge_kernel.shape)}")
    print(f"output shape: {tuple(output.shape)}")
    print("feature map:")
    for row in output[0, 0]:
        print(" ".join(f"{value:+.1f}" for value in row.tolist()))


class RNNClassifier(nn.Module):
    def __init__(self, input_dim: int, hidden_dim: int, num_classes: int) -> None:
        super().__init__()
        self.rnn = nn.RNN(input_dim, hidden_dim, batch_first=True)
        self.classifier = nn.Linear(hidden_dim, num_classes)

    def forward(self, inputs: torch.Tensor) -> torch.Tensor:
        _, final_hidden = self.rnn(inputs)
        return self.classifier(final_hidden.squeeze(0))


class CNNClassifier(nn.Module):
    def __init__(self, input_dim: int, num_channels: int, num_classes: int) -> None:
        super().__init__()
        self.conv = nn.Conv1d(input_dim, num_channels, kernel_size=3, padding=1)
        self.activation = nn.ReLU()
        self.pooling = nn.AdaptiveAvgPool1d(1)
        self.classifier = nn.Linear(num_channels, num_classes)

    def forward(self, inputs: torch.Tensor) -> torch.Tensor:
        # Text tensors are (batch, seq_len, features), Conv1d wants
        # (batch, features, seq_len).
        features = inputs.transpose(1, 2)
        features = self.pooling(self.activation(self.conv(features))).squeeze(-1)
        return self.classifier(features)


def make_sequence_dataset(
    sample_count: int,
    sequence_length: int,
    seed: int,
) -> tuple[torch.Tensor, torch.Tensor]:
    """Create a toy binary sequence classification dataset.

    Label 1 sequences contain the consecutive pattern 0 -> 1.
    Label 0 sequences contain the consecutive pattern 2 -> 3.
    """
    rng = random.Random(seed)
    token_ids: list[list[int]] = []
    labels: list[int] = []

    for index in range(sample_count):
        label = index % 2
        sequence = [rng.choice([0, 2]) for _ in range(sequence_length)]
        position = rng.randrange(0, sequence_length - 1)
        if label == 1:
            sequence[position] = 0
            sequence[position + 1] = 1
        else:
            sequence[position] = 2
            sequence[position + 1] = 3
        token_ids.append(sequence)
        labels.append(label)

    inputs = F.one_hot(torch.tensor(token_ids, dtype=torch.long), num_classes=4).float()
    targets = torch.tensor(labels, dtype=torch.long)
    return inputs, targets


def train_classifier(
    model: nn.Module,
    loader: DataLoader,
    device: torch.device,
    epochs: int,
    learning_rate: float,
) -> list[float]:
    optimizer = torch.optim.AdamW(model.parameters(), lr=learning_rate)
    loss_fn = nn.CrossEntropyLoss()
    losses: list[float] = []

    for _ in range(epochs):
        model.train()
        epoch_losses: list[float] = []
        for inputs, targets in loader:
            inputs = inputs.to(device)
            targets = targets.to(device)
            optimizer.zero_grad(set_to_none=True)
            logits = model(inputs)
            loss = loss_fn(logits, targets)
            loss.backward()
            optimizer.step()
            epoch_losses.append(loss.item())
        losses.append(sum(epoch_losses) / len(epoch_losses))

    return losses


@torch.inference_mode()
def evaluate_classifier(model: nn.Module, loader: DataLoader, device: torch.device) -> float:
    correct = 0
    total = 0
    model.eval()
    for inputs, targets in loader:
        inputs = inputs.to(device)
        targets = targets.to(device)
        predictions = model(inputs).argmax(dim=1)
        correct += (predictions == targets).sum().item()
        total += targets.numel()
    return correct / total


def compare_rnn_cnn(device: torch.device, epochs: int) -> None:
    print("\n=== 5. toy sequence classification: RNN vs CNN ===")
    inputs, targets = make_sequence_dataset(sample_count=800, sequence_length=16, seed=1337)
    split = 640
    train_dataset = TensorDataset(inputs[:split], targets[:split])
    test_dataset = TensorDataset(inputs[split:], targets[split:])
    train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
    test_loader = DataLoader(test_dataset, batch_size=64)

    models: dict[str, nn.Module] = {
        "RNN": RNNClassifier(input_dim=4, hidden_dim=16, num_classes=2).to(device),
        "CNN": CNNClassifier(input_dim=4, num_channels=16, num_classes=2).to(device),
    }

    for name, model in models.items():
        losses = train_classifier(model, train_loader, device, epochs, learning_rate=0.01)
        accuracy = evaluate_classifier(model, test_loader, device)
        parameter_count = sum(parameter.numel() for parameter in model.parameters())
        print(
            f"{name} | final loss {losses[-1]:.4f} | "
            f"test accuracy {accuracy:.4f} | parameters {parameter_count:,}"
        )


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--epochs", type=int, default=100)
    parser.add_argument("--seed", type=int, default=1337)
    parser.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
    args = parser.parse_args()

    torch.manual_seed(args.seed)
    device = torch.device(args.device)

    demo_manual_rnn()
    demo_pytorch_rnn()
    demo_conv1d()
    demo_conv2d_edge_detector()
    compare_rnn_cnn(device, args.epochs)
    print(f"\ndevice: {device}")


if __name__ == "__main__":
    main()

代码中:

\[h_t=\tanh(0.8x_t+0.6h_{t-1})\]

输入:

[1.0, 0.5, 1.2]

输出会逐步打印:

h1
h2
h3

nn.RNN 形状

PyTorch 的 RNN 输入通常是:

(batch_size, seq_len, input_size)

输出是:

(batch_size, seq_len, hidden_size)

最后一个隐状态是:

(1, batch_size, hidden_size)

一维卷积示例

代码构造输入:

[1, 0, -1, 2, 1]

卷积核:

[0.25, 0.5, 0.25]

输出:

[0, 0, 1.0]

二维卷积边缘检测

代码构造一个 \(5\times5\) 图像,并使用垂直边缘卷积核:

1 0 -1
1 0 -1
1 0 -1

输出特征图会在图像左侧亮、右侧暗的位置产生较大响应。


RNN vs CNN

代码构造两类序列:

类别 1:包含连续模式 0 -> 1
类别 0:包含连续模式 2 -> 3

填充 token 只使用 `0` 和 `2`,避免随机序列里同时出现两类模式,保证标签没有歧义。

模型需要判断序列属于哪一类。

RNN 模型:

one-hot token
-> nn.RNN
-> last hidden state
-> linear classifier

CNN 模型:

one-hot token
-> Conv1d(kernel_size=3)
-> ReLU
-> average pooling
-> linear classifier

这个任务有明显的局部模式,因此 CNN 通常学习得很快;RNN 也能学习,但训练通常更慢。


符号与名词详解

符号 / 名词 含义
RNN Recurrent Neural Network,循环神经网络
hidden state 隐状态,RNN 中携带历史信息的记忆向量
\(h_t\) 第 \(t\) 步隐状态
\(x_t\) 第 \(t\) 个 token 的 embedding
\(W_x\) 输入权重矩阵
\(W_h\) 记忆传递权重矩阵
LSTM Long Short-Term Memory,带门控的 RNN
GRU Gated Recurrent Unit,LSTM 的简化版本
BPTT Backpropagation Through Time,RNN 的反向传播方式
梯度消失 反向传播时梯度逐层变小,远处参数难以学习
梯度爆炸 反向传播时梯度逐层变大,训练不稳定
CNN Convolutional Neural Network,卷积神经网络
convolution 卷积,用共享权重窗口扫描输入
kernel / filter 卷积核,一组可学习的局部权重
feature map 特征图,卷积输出结果
channel 通道,例如 RGB 的 3 个颜色通道
padding 边缘补零,控制输出长度
stride 卷积窗口每次滑动的步长
pooling 池化,对局部区域取最大值或平均值
receptive field 感受野,输出位置能看到的输入范围
long-range dependency 长距离依赖,序列中相隔很远的位置之间的关系
\(O(n)\) 复杂度随序列长度线性增长
\(O(n^2)\) 复杂度随序列长度平方增长