第 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)\]这个公式做了四件事:
- 用 \(W_x x_t\) 提取当前输入的信息
- 用 \(W_h h_{t-1}\) 延续旧记忆
- 两者相加
- 用 \(\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)
这带来两个好处:
- 可以处理任意长度序列
- 同一个模式在不同位置可以复用同一套规则
例如“否定词 + 形容词”的模式,无论出现在第 3 个位置还是第 30 个位置,理论上都可以用同一组参数识别。
9. RNN 的优点
- 天然适合序列
- 能处理变长输入
- 参数数量与序列长度无关
- 适合流式处理,例如实时语音或逐 token 生成
- 在历史任务中曾广泛用于语言模型、机器翻译和语音识别
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]
池化的作用:
- 减少计算量
- 让特征对小幅位移更稳定
- 保留较强的局部信号
23. 感受野 (Receptive Field)
感受野表示:
某个输出位置能看到多大范围的原始输入。
一层 kernel size=3 的卷积只能看 3 个相邻位置。
两层堆叠后,理论上能间接看到更大范围。
层数越多,感受野越大。
这是 CNN 能处理较大图像的原因之一。
24. CNN 的优点
- 局部模式提取能力强
- 参数共享:同一个卷积核在所有位置复用
- 可以并行计算
- 对图像、语音、文本都有效
- 层数越多,感受野越大
- 对小幅平移比较稳定
25. CNN 的问题
- 单层只能看局部窗口
- 长距离依赖需要很多层
- 全局关系不如 Attention 直接
- 强假设“局部相邻信息更重要”
- 对非常长的上下文不如 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
- 多头机制允许同时学习多种关系
- 可以扩展到非常大的模型和数据
代价是:
\[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. 代码
以下代码实现了:
- 计算一个简单 RNN 的隐状态
- 演示
nn.RNN的输入输出形状 - 计算一维卷积
- 演示二维卷积边缘检测
- 用一个序列分类任务比较 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)\) | 复杂度随序列长度平方增长 |