一次关于 CNN → RNN → Transformer 的深度技术考古,不只是“是什么”,更是“为什么”。


引言:为什么是这三者?

如果你在 2026 年回望深度学习的发展史,会发现一条清晰的脉络——感知(Perception)→ 记忆(Memory)→ 注意(Attention)

这三个词不仅是三类网络的本质,也是智能体处理信息的三个递进阶段:

阶段 核心架构 解决的本质问题 关键词
感知 CNN 如何从原始信号中提取层次化特征 局部感受野、权值共享
记忆 RNN 如何在时间维度上积累和传递信息 隐状态、门控机制
注意 Transformer 如何在全局范围内动态聚焦相关信息 自注意力、并行化

这不是简单的“新取代旧”,而是每次范式跃迁都在解决前一个架构的根本性局限。下面我们逐一拆解。

阅读导航

你可以按下面路径阅读:

  • 想先把主线搞懂:优先看第一章到第五章(架构演进)
  • 想直接提升 AI 实操效率:重点看第六章(原理到实践)
  • 想快速回顾关键参数:看附录A(速查表)

第一章 CNN:空间上的归纳偏置

1.1 一个根本性的问题

假设你要识别一张 224×224 的猫片。最朴素的全连接网络怎么做?

1
2
输入维度: 224 × 224 × 3 = 150,528
第一层 1024 个神经元 → 150,528 × 1024 = 1.54 亿参数

仅仅第一层就有 1.5 亿参数。这不仅意味着计算灾难,更意味着模型可以自由地将图像中任意两个像素关联——但我们知道,图像的语义是局部的:猫耳朵和猫耳朵旁边的像素关系密切,和猫脚旁的像素关系不大。

CNN 的核心洞察就一句话:空间局部性是一种先验知识,应该被编码进架构而非让模型自己学。

1.2 卷积操作:从数学到代码

数学定义

二维卷积的严格定义:

(IK)(i,j)=mnI(im,jn)K(m,n)(I \ast K)(i, j) = \sum_{m} \sum_{n} I(i-m, j-n) \cdot K(m, n)

公式说明:输入图像 I 与卷积核 K 在位置 (i,j) 的卷积结果,等于 K 在 I 上滑动时的加权求和。

但在深度学习中,我们实际用的是互相关(cross-correlation)

O(i,j)=m=0kh1n=0kw1I(i+m,j+n)K(m,n)O(i, j) = \sum_{m=0}^{k_h-1} \sum_{n=0}^{k_w-1} I(i+m, j+n) \cdot K(m, n)

公式说明:输出特征图 O 在位置 (i,j) 的值,等于输入 I 在对应位置的 kH×kW 窗口与卷积核 K 的逐元素相乘再求和。

变量说明

  • I:输入特征图(Input Feature Map)
  • K:卷积核(Kernel)
  • O:输出特征图(Output Feature Map)
  • k_h, k_w:卷积核的高和宽

代码抽象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def conv2d(input, kernel, stride=1, padding=0):
"""
最朴素的二维卷积实现
input: (C_in, H, W)
kernel: (C_in, kH, kW)
"""
C_in, H, W = input.shape
C_in, kH, kW = kernel.shape

# Padding
if padding > 0:
input = pad(input, padding)

# 计算输出尺寸
H_out = (H + 2*padding - kH) // stride + 1
W_out = (W + 2*padding - kW) // stride + 1
output = zeros(H_out, W_out)

# 滑动窗口 —— 这就是"卷积"的全部秘密
for i in range(0, H_out):
for j in range(0, W_out):
# 提取局部感受野
receptive_field = input[
:,
i*stride : i*stride + kH,
j*stride : j*stride + kW
]
# 逐元素相乘再求和
output[i, j] = sum(receptive_field * kernel)

return output

这段代码揭示了 CNN 的两个核心设计原则:

  1. 局部感受野(Local Receptive Field):每次只看 kH × kW 的窗口,而非整张图
  2. 权值共享(Weight Sharing):同一个 kernel 在所有位置滑动——同一个特征检测器在空间各处复用

1.3 多通道卷积:特征图的堆叠

单个卷积核只能检测一种模式。实际的卷积层用 C_out 个卷积核,每个核的维度是 (C_in, kH, kW):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def conv2d_multi_channel(input, kernels, bias):
"""
input: (C_in, H, W)
kernels: (C_out, C_in, kH, kW)
bias: (C_out,)
output: (C_out, H_out, W_out)
"""
C_out = kernels.shape[0]
output = []

for c in range(C_out):
# 每个输出通道 = 所有输入通道的卷积之和 + 偏置
feature_map = bias[c]
for ch_in in range(input.shape[0]):
feature_map += conv2d(input[ch_in], kernels[c, ch_in])
output.append(feature_map)

return stack(output)

关键理解:输出特征图的每个位置,是输入在对应位置局部区域内所有通道的加权组合。第 c 个输出通道的卷积核 kernels[c] 就定义了"我在找什么模式"。

1.4 池化:空间不变性的来源

卷积保留了空间信息,但有时我们希望模型对微小的位移和形变不敏感:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def max_pool2d(input, pool_size=2, stride=2):
"""
input: (C, H, W)
"""
C, H, W = input.shape
H_out = (H - pool_size) // stride + 1
W_out = (W - pool_size) // stride + 1
output = zeros(C, H_out, W_out)

for i in range(H_out):
for j in range(W_out):
region = input[
:,
i*stride : i*stride + pool_size,
j*stride : j*stride + pool_size
]
# 取最大值 —— 只保留最强烈的信号
output[:, i, j] = region.reshape(C, -1).max(axis=1)

return output

池化做了一件很暴力但很有效的事:把 2×2 的区域压缩成一个值。这意味着模型不再关心"猫耳朵精确在哪个像素",而只关心"这片区域有没有猫耳朵的特征"。

1.5 残差连接:CNN 的成人礼

2015 年,ResNet 提出了一个简单到让人怀疑的问题:如果网络应该学到 F(x),为什么不让它学 F(x) = H(x) - x 的残差?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class ResidualBlock:
def __init__(self, channels):
self.conv1 = Conv2d(channels, channels, 3, padding=1)
self.bn1 = BatchNorm2d(channels)
self.conv2 = Conv2d(channels, channels, 3, padding=1)
self.bn2 = BatchNorm2d(channels)

def forward(self, x):
identity = x # 保存输入

# 主路径:两层卷积
out = relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out))

# 核心操作:残差相加
out = out + identity # ← 这一行改变了深度学习的轨迹
out = relu(out)

return out

为什么 out = out + identity 如此重要?

考虑反向传播时的梯度流。假设损失函数 L 对 out 的梯度为 ∂L/∂out,那么:

Lx=Loutoutx=Lout(F(x)x+1)\frac{\partial L}{\partial x} = \frac{\partial L}{\partial \mathrm{out}} \cdot \frac{\partial \mathrm{out}}{\partial x} = \frac{\partial L}{\partial \mathrm{out}} \cdot \left(\frac{\partial F(x)}{\partial x} + 1\right)

公式说明:残差连接的梯度由两部分组成——变换 F(x) 的梯度 + 恒等映射的梯度(那个关键的 +1)。

那个 +1 意味着:即使 F(x) 的梯度消失了,恒等映射的梯度通路依然畅通。这就是 ResNet 可以训练 152 层甚至 1000+ 层的根本原因。

1.6 CNN 的本质局限

CNN 做对了很多事,但有一个根本性的盲点:它假设数据具有空间局部性和平移不变性。这对于图像是成立的,但对于序列数据——一段文字、一帧帧画面、一个个时间步的信号——这些假设失效了。

序列数据的关键特性是:当前时刻的含义取决于之前所有时刻。"我爱你"和"你爱我"词完全一样,但意思完全不同。CNN 的滑动窗口无法自然地建模这种长程依赖。

这就引出了 RNN。


第二章 RNN:时间上的展开

2.1 一个新问题:时间维度

处理一个句子 [x₁, x₂, …, xₜ],我们需要的不是一个空间上的滑动窗口,而是一个沿时间步依次处理、并维护内部状态的计算单元。

2.2 Vanilla RNN:最朴素的循环

数学定义

ht=tanh(Whhht1+Wxhxt+bh)h_t = \tanh(W_{hh} h_{t-1} + W_{xh} x_t + b_h)

yt=Whyht+byy_t = W_{hy} h_t + b_y

公式说明:

  • h_t:时刻 t 的隐状态(hidden state),是网络的"记忆"
  • W_hh:隐状态到隐状态的权重矩阵
  • W_xh:输入到隐状态的权重矩阵
  • b_h:偏置项

代码抽象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class VanillaRNN:
def __init__(self, input_size, hidden_size):
self.W_xh = randn(hidden_size, input_size) * 0.01
self.W_hh = randn(hidden_size, hidden_size) * 0.01
self.b_h = zeros(hidden_size)
self.W_hy = randn(output_size, hidden_size) * 0.01
self.b_y = zeros(output_size)

def forward(self, inputs):
"""
inputs: list of vectors, each (input_size,)
返回每个时间步的输出和最终隐状态
"""
h = zeros(self.hidden_size) # 初始隐状态 h_0
outputs = []

for x_t in inputs:
# 隐状态更新:当前输入 + 上一步记忆 → 新记忆
h = tanh(self.W_xh @ x_t + self.W_hh @ h + self.b_h)

# 输出:从隐状态解码
y_t = self.W_hy @ h + self.b_y
outputs.append(y_t)

return outputs, h

核心设计h = tanh(W_xh @ x_t + W_hh @ h + self.b_h) 这一行。

注意 W_hh @ h——这是隐状态对自身的递归依赖。把 RNN 沿时间步展开,你会看到一个所有时间步共享相同参数的极深网络

1
2
3
x_1 → [RNN] → x_2 → [RNN] → x_3 → [RNN] → ... → x_T → [RNN]
↑ ↑ ↑ ↑
h_1 h_2 h_3 h_T

所有 [RNN] 是同一个函数,同一组参数。这就是"权值共享"在时间维度的体现——CNN 在空间上共享参数,RNN 在时间上共享参数。

2.3 梯度消失:RNN 的阿喀琉斯之踵

BPTT(Backpropagation Through Time)中,梯度沿时间步回传:

Lh0=LhTt=1Ththt1=LhTt=1T(Whhdiag(tanh(zt)))\frac{\partial L}{\partial h_0} = \frac{\partial L}{\partial h_T} \prod_{t=1}^{T} \frac{\partial h_t}{\partial h_{t-1}} = \frac{\partial L}{\partial h_T} \prod_{t=1}^{T} \left( W_{hh}^{\top} \cdot \operatorname{diag}(\tanh'(z_t)) \right)

公式说明:初始时刻 h₀ 的梯度等于最终时刻 hₜ 的梯度,乘以所有中间步骤梯度的连乘。

问题出在那个连乘。tanh 的导数最大值为 1,实际中通常远小于 1。如果 W_hh 的最大特征值 λ_max < 1,那么梯度以 O(λ_max^T) 的速度指数衰减。一个 100 步的序列,如果 λ_max = 0.9,梯度会缩小到 0.9^100 ≈ 2.66 × 10^(-5)。

物理意义:模型无法学习跨越数十步的依赖关系。处理"我出生在[…30个词…]的北京人"这种长距依赖时,梯度早就消失了。

2.4 LSTM:用门控拯救梯度

1997 年,Hochreiter 和 Schmidhuber 提出了 LSTM。核心思想:不要让信息直接流过 tanh,而是用可学习的门来控制信息的流动

三道门

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class LSTMCell:
def __init__(self, input_size, hidden_size):
# 注意:所有门共享输入,但各有自己的参数
# 遗忘门参数
self.W_f = [randn(hidden_size, hidden_size), randn(hidden_size, input_size)]
self.b_f = ones(hidden_size) # 偏置初始化为1!遗忘门默认"记住"

# 输入门参数
self.W_i = [randn(hidden_size, hidden_size), randn(hidden_size, input_size)]
self.b_i = zeros(hidden_size)

# 候选记忆参数
self.W_c = [randn(hidden_size, hidden_size), randn(hidden_size, input_size)]
self.b_c = zeros(hidden_size)

# 输出门参数
self.W_o = [randn(hidden_size, hidden_size), randn(hidden_size, input_size)]
self.b_o = zeros(hidden_size)

def forward(self, x_t, h_prev, c_prev):
"""
x_t: (input_size,) 当前输入
h_prev: (hidden_size,) 上一步隐状态
c_prev: (hidden_size,) 上一步细胞状态(这是LSTM新增的)
"""

# 遗忘门:哪些旧记忆应该被丢弃?
f_t = sigmoid(self.W_f[0] @ h_prev + self.W_f[1] @ x_t + self.b_f)

# 输入门:哪些新信息应该被写入?
i_t = sigmoid(self.W_i[0] @ h_prev + self.W_i[1] @ x_t + self.b_i)

# 候选记忆:新信息的内容是什么?
c_tilde = tanh(self.W_c[0] @ h_prev + self.W_c[1] @ x_t + self.b_c)

# 细胞状态更新:旧记忆 × 遗忘门 + 新信息 × 输入门
c_t = f_t * c_prev + i_t * c_tilde
# ↑ 丢弃部分旧记忆 ↑ 写入部分新信息
# 这是 LSTM 的核心公式

# 输出门:从细胞状态中选择性输出
o_t = sigmoid(self.W_o[0] @ h_prev + self.W_o[1] @ x_t + self.b_o)

# 隐状态:输出门过滤后的细胞状态
h_t = o_t * tanh(c_t)

return h_t, c_t

为什么 LSTM 能缓解梯度消失?

关键在细胞状态的更新 c_t = f_t * c_prev + i_t * c_tilde。反向传播时:

ctct1=ft\frac{\partial c_t}{\partial c_{t-1}} = f_t

公式说明:细胞状态的梯度直接等于遗忘门 f_t 的值,而不是像 Vanilla RNN 那样是多个矩阵的连乘。

如果遗忘门 f_t ≈ 1(网络学会了"记住"),那么梯度几乎无损地沿细胞状态通路回传。与 Vanilla RNN 的 W_hh · tanh’(·) 不同,这里的梯度传递是加法性的,不是乘法性的。

这就是为什么 LSTM 的偏置 b_f 初始化为 1 而非 0——在训练初期,遗忘门倾向于打开,确保梯度流通。

2.5 GRU:LSTM 的极简版

2014 年,Cho 等人提出了 GRU,把 LSTM 的四组参数简化为三组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class GRUCell:
def __init__(self, input_size, hidden_size):
# 重置门:决定在计算候选隐状态时,忽略多少过去的隐状态
self.W_r = [randn(hidden_size, hidden_size), randn(hidden_size, input_size)]
self.b_r = zeros(hidden_size)

# 更新门:决定在新隐状态中,保留多少旧隐状态
self.W_z = [randn(hidden_size, hidden_size), randn(hidden_size, input_size)]
self.b_z = zeros(hidden_size)

# 候选隐状态
self.W_h = [randn(hidden_size, hidden_size), randn(hidden_size, input_size)]
self.b_h = zeros(hidden_size)

def forward(self, x_t, h_prev):
# 重置门
r_t = sigmoid(self.W_r[0] @ h_prev + self.W_r[1] @ x_t + self.b_r)

# 更新门(同时承担了LSTM中遗忘门和输入门的角色)
z_t = sigmoid(self.W_z[0] @ h_prev + self.W_z[1] @ x_t + self.b_z)

# 候选隐状态(用重置门调制旧隐状态)
h_tilde = tanh(
self.W_h[0] @ (r_t * h_prev) + self.W_h[1] @ x_t + self.b_h
)

# 插值:z_t 控制 "旧" 与 "新" 的混合比例
h_t = (1 - z_t) * h_prev + z_t * h_tilde
# ↑ 保留部分旧状态 ↑ 加入部分新状态

return h_t

GRU 的 h_t = (1 - z_t) * h_prev + z_t * h_tilde 和 LSTM 的 c_t = f_t * c_prev + i_t * c_tilde 异曲同工——都是加法性的梯度通路。区别在于 GRU 没有独立的细胞状态 c_t,隐状态 h_t 同时承担了记忆和输出的功能。

2.6 双向 RNN 与序列到序列

双向 RNN

单向 RNN 只看到过去。但在很多任务中,理解当前词需要上下文(过去和未来):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class BiRNN:
def __init__(self, input_size, hidden_size):
self.rnn_forward = RNN(input_size, hidden_size)
self.rnn_backward = RNN(input_size, hidden_size)

def forward(self, inputs):
# 前向:从左到右
h_forward = [h_1, h_2, ..., h_T]

# 后向:从右到左
h_backward = [h_T, h_{T-1}, ..., h_1]

# 拼接:每个位置同时拥有"过去"和"未来"的信息
h_bi = [concat(h_f, h_b) for h_f, h_b in zip(h_forward, reversed(h_backward))]

return h_bi

Seq2Seq:编码器-解码器架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Seq2Seq:
def __init__(self, input_size, hidden_size, output_size):
self.encoder = LSTM(input_size, hidden_size)
self.decoder = LSTM(output_size, hidden_size)
self.output_proj = Linear(hidden_size, output_size)

def forward(self, source_seq, target_seq):
# 编码:把整个源序列压缩成最终的隐状态
_, (h_final, c_final) = self.encoder(source_seq)

# 解码:用编码器的最终状态初始化,逐步生成目标序列
h, c = h_final, c_final
outputs = []

for t in range(len(target_seq)):
# 解码器的每一步输入是上一步的输出(推理时)或教师强制(训练时)
y_t, (h, c) = self.decoder(target_seq[t-1], h, c)
outputs.append(self.output_proj(y_t))

return outputs

Seq2Seq 的根本瓶颈在于:编码器必须将所有信息压缩到固定维度的隐状态中。对于长句子,这个"信息瓶颈"导致早期信息被后期信息覆盖。

2.7 注意力机制:从补丁到核心

2015 年,Bahdanau 等人在机器翻译中提出了注意力机制来缓解信息瓶颈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class BahdanauAttention:
def __init__(self, hidden_size):
self.W_query = Linear(hidden_size, hidden_size)
self.W_key = Linear(hidden_size, hidden_size)
self.V = Linear(hidden_size, 1)

def forward(self, query, encoder_outputs):
"""
query: (hidden_size,) 解码器当前隐状态
encoder_outputs: (seq_len, hidden_size) 编码器所有时间步的输出
"""
# 计算注意力分数:query 与每个 encoder_output 的相关性
scores = []
for key in encoder_outputs:
score = self.V(tanh(
self.W_query(query) + self.W_key(key)
))
scores.append(score)

# Softmax 归一化
attention_weights = softmax(scores)

# 加权求和
context = sum(w * output for w, output in zip(attention_weights, encoder_outputs))

return context, attention_weights

注意力机制的思想是:不再强制把所有信息塞进一个向量,而是在每一步解码时动态地"回头看"编码器的所有输出,决定现在该关注哪里

这个思想,即将成为下一个范式的基石。

2.8 RNN 的本质局限

RNN 系列解决了序列建模问题,但有两个根本性缺陷:

  1. 串行性:h_t 依赖 h_{t-1},无法并行化。处理长度为 T 的序列,至少需要 O(T) 的串行步。
  2. 长程依赖的天花板:即使有 LSTM/GRU 的门控机制,对于数千步的长序列,梯度衰减仍然是不可忽视的问题。

这两个问题,Transformer 同时解决了。


第三章 Transformer:注意力的终极形态

3.1 “Attention Is All You Need”——标题就是全部宣言

2017 年,Vaswani 等人的论文标题本身就是论点:你不需要循环,不需要卷积,注意力就是你所需要的一切。

Transformer 的核心问题是:能否在不使用递归结构的情况下,建模序列中任意两个位置之间的关系?

答案是自注意力(Self-Attention)。

3.2 自注意力:从 QKV 说起

直觉

对于序列中的每个位置 i,自注意力机制问三个问题:

  • Query(查询):我在找什么?
  • Key(键):我有什么可以匹配的?
  • Value(值):一旦匹配上了,我能提供什么信息?

数学定义

给定输入序列 X ∈ R^(n×d)(n 个 token,每个 d 维),先通过三个线性变换得到 Q、K、V:

Q=XWQ,K=XWK,V=XWVQ = X W^Q, \quad K = X W^K, \quad V = X W^V

公式说明:输入的每个 token 分别通过三个不同的权重矩阵,投影成查询向量 Q、键向量 K 和值向量 V。

其中 W^Q, W^K ∈ R(d×d_k),WV ∈ R^(d×d_v)。

然后:

Attention(Q,K,V)=softmax(QKdk)V\operatorname{Attention}(Q, K, V) = \operatorname{softmax}\left(\frac{QK^{\top}}{\sqrt{d_k}}\right) V

公式说明:注意力 = softmax(Q × K 的转置 / √d_k) × V。先算 Q 和 K 的相似度,归一化后加权求和 V。

代码实现——从逐元素到矩阵

第一步:单个头的注意力

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def scaled_dot_product_attention(Q, K, V, mask=None):
"""
Q: (..., seq_len_q, d_k)
K: (..., seq_len_k, d_k)
V: (..., seq_len_v, d_v) 注意: seq_len_k == seq_len_v
"""
d_k = Q.shape[-1]

# 第一步:计算注意力分数
# Q @ K^T → (..., seq_len_q, seq_len_k)
# 每个 (i,j) 元素 = 第 i 个 query 和第 j 个 key 的点积
scores = Q @ K.transpose(-2, -1) / sqrt(d_k)

# 为什么除以 sqrt(d_k)?
# 当 d_k 很大时,点积的方差也大(\mathcal{O}(d_k)),导致 softmax 饱和
# 除以 sqrt(d_k) 使方差归一化到 \mathcal{O}(1)

# 第二步:掩码(用于解码器,防止看到未来信息)
if mask is not None:
scores = scores.masked_fill(mask == 0, -1e9)

# 第三步:softmax → 注意力权重
# 沿 key 维度归一化,每个 query 的权重和为 1
attention_weights = softmax(scores, dim=-1)

# 第四步:加权求和
# (..., seq_len_q, seq_len_k) @ (..., seq_len_k, d_v) → (..., seq_len_q, d_v)
output = attention_weights @ V

return output, attention_weights

让我们用一个具体例子来理解。假设有一个 4 词序列,d_k = 3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Q = [[0.5, 0.2, 0.1],   ← token "我" 的查询向量
[0.3, 0.8, 0.4], ← token "爱" 的查询向量
[0.1, 0.3, 0.9], ← token "北京" 的查询向量
[0.7, 0.1, 0.3]] ← token "。" 的查询向量

K = [[0.4, 0.1, 0.2], ← token "我" 的键向量
[0.2, 0.7, 0.3], ← token "爱" 的键向量
[0.1, 0.2, 0.8], ← token "北京" 的键向量
[0.6, 0.1, 0.1]] ← token "。" 的键向量

scores = Q @ K^T / sqrt(3) =
我 爱 北京 。
我 [0.15, 0.12, 0.05, 0.27]
爱 [0.16, 0.37, 0.25, 0.19]
北京 [0.09, 0.22, 0.49, 0.12]
。 [0.24, 0.12, 0.08, 0.28]

softmax 后,"北京"这个 token 对自身的注意力权重最高(0.49),这是合理的——自注意力让每个词首先"注意到"自己。同时"爱"也给了"北京"较高权重(0.25),因为这可能是动宾关系。

3.3 多头注意力:并行的多视角

单个注意力头只能学习一种关联模式。多头注意力让模型同时维护多组 QKV:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class MultiHeadAttention:
def __init__(self, d_model, n_heads):
self.d_model = d_model
self.n_heads = n_heads
self.d_k = d_model // n_heads # 每个头的维度

# 每个头有独立的 QKV 投影
self.W_q = Linear(d_model, d_model) # 实际是 n_heads 个 (d_model, d_k) 的拼接
self.W_k = Linear(d_model, d_model)
self.W_v = Linear(d_model, d_model)
self.W_o = Linear(d_model, d_model) # 输出投影

def forward(self, query, key, value, mask=None):
batch_size = query.shape[0]
seq_len = query.shape[1]

# 第一步:线性投影并分头
# (batch, seq_len, d_model) → (batch, n_heads, seq_len, d_k)
Q = self.W_q(query).view(batch_size, seq_len, self.n_heads, self.d_k).transpose(1, 2)
K = self.W_k(key).view(batch_size, seq_len, self.n_heads, self.d_k).transpose(1, 2)
V = self.W_v(value).view(batch_size, seq_len, self.n_heads, self.d_k).transpose(1, 2)

# 第二步:每个头独立计算注意力
attn_output, attn_weights = scaled_dot_product_attention(Q, K, V, mask)

# 第三步:拼接所有头的输出
# (batch, n_heads, seq_len, d_k) → (batch, seq_len, d_model)
attn_output = attn_output.transpose(1, 2).contiguous().view(batch_size, seq_len, self.d_model)

# 第四步:输出投影
output = self.W_o(attn_output)

return output, attn_weights

为什么多头比单头好? 类比人类阅读:我们理解一个句子时,同时关注语法结构、语义关联、指代关系等多个维度。每个注意力头可以专注于一种关联模式:

  • 头 1 可能学会了主谓关系
  • 头 2 可能学会了形容词修饰关系
  • 头 3 可能学会了长距离指代
  • ……

3.4 位置编码:没有递归,如何知道顺序?

自注意力是置换不变的(permutation invariant)——打乱输入顺序,注意力权重的模式会变,但计算方式不变。这意味着模型本身不知道 token 的位置。

Transformer 用位置编码注入顺序信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class PositionalEncoding:
def __init__(self, d_model, max_len=5000):
self.pe = zeros(max_len, d_model)

position = arange(0, max_len).unsqueeze(1) # (max_len, 1)

# 用不同频率的 sin/cos 函数
div_term = exp(arange(0, d_model, 2) * -(log(10000.0) / d_model))

self.pe[:, 0::2] = sin(position * div_term) # 偶数维度用 sin
self.pe[:, 1::2] = cos(position * div_term) # 奇数维度用 cos

def forward(self, x):
"""
x: (batch, seq_len, d_model)
"""
return x + self.pe[:x.shape[1]] # 直接加到输入上

为什么是 sin/cos?

关键性质:对于任意固定偏移 k,PE(pos+k) 可以表示为 PE(pos) 的线性函数。这意味着模型可以通过学习线性变换来推断相对位置。

更深层的原因:不同维度使用不同频率(从 2π 到 2π × 10000),低维度编码局部位置关系(相邻 token),高维度编码全局位置关系(段落级别)。

3.5 完整的 Transformer Block

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class TransformerBlock:
def __init__(self, d_model, n_heads, d_ff, dropout=0.1):
# 子层 1:多头自注意力
self.attention = MultiHeadAttention(d_model, n_heads)
self.norm1 = LayerNorm(d_model)
self.dropout1 = Dropout(dropout)

# 子层 2:前馈网络
self.ffn = Sequential(
Linear(d_model, d_ff), # 扩展:d_model → 4*d_model
ReLU(),
Linear(d_ff, d_model), # 压缩:4*d_model → d_model
)
self.norm2 = LayerNorm(d_model)
self.dropout2 = Dropout(dropout)

def forward(self, x, mask=None):
# 子层 1:Pre-Norm 变体(现代实现常用)
# LayerNorm → Multi-Head Attention → Residual
attn_output, _ = self.attention(
self.norm1(x), self.norm1(x), self.norm1(x), mask
)
x = x + self.dropout1(attn_output)

# 子层 2:LayerNorm → FFN → Residual
ffn_output = self.ffn(self.norm2(x))
x = x + self.dropout2(ffn_output)

return x

几个值得注意的设计细节

  1. FFN 的扩展比:中间层维度 d_ff 通常是 d_model 的 4 倍。这个"先扩后缩"的结构让网络可以在高维空间中做非线性变换。

  2. Layer Norm 的位置:原始论文用 Post-Norm(x = norm(x + sublayer(x))),但 Pre-Norm(x = x + sublayer(norm(x)))在训练深层模型时更稳定,因为残差通路更干净。

  3. 残差连接:和 ResNet 一样,每个子层都有残差连接,确保梯度的加法性通路。

3.6 编码器-解码器架构

完整的 Transformer 用于机器翻译时,包含编码器和解码器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
class Encoder:
def __init__(self, n_layers, d_model, n_heads, d_ff):
self.layers = [TransformerBlock(d_model, n_heads, d_ff) for _ in range(n_layers)]
self.norm = LayerNorm(d_model)

def forward(self, x, mask=None):
for layer in self.layers:
x = layer(x, mask)
return self.norm(x)


class DecoderBlock:
"""解码器多了一个交叉注意力子层"""
def __init__(self, d_model, n_heads, d_ff, dropout=0.1):
# 子层 1:自注意力(带因果掩码)
self.self_attention = MultiHeadAttention(d_model, n_heads)
self.norm1 = LayerNorm(d_model)

# 子层 2:交叉注意力(query 来自解码器,key/value 来自编码器)
self.cross_attention = MultiHeadAttention(d_model, n_heads)
self.norm2 = LayerNorm(d_model)

# 子层 3:FFN
self.ffn = Sequential(Linear(d_model, d_ff), ReLU(), Linear(d_ff, d_model))
self.norm3 = LayerNorm(d_model)

def forward(self, x, encoder_output, self_mask=None, cross_mask=None):
# 自注意力:只能看已经生成的 token
x = x + self.self_attention(self.norm1(x), self.norm1(x), self.norm1(x), self_mask)[0]

# 交叉注意力:可以看编码器的所有输出
x = x + self.cross_attention(self.norm2(x), encoder_output, encoder_output, cross_mask)[0]

# FFN
x = x + self.ffn(self.norm3(x))

return x


class Transformer:
def __init__(self, src_vocab_size, tgt_vocab_size, d_model=512,
n_heads=8, n_layers=6, d_ff=2048):
self.src_embed = Embedding(src_vocab_size, d_model) + PositionalEncoding(d_model)
self.tgt_embed = Embedding(tgt_vocab_size, d_model) + PositionalEncoding(d_model)
self.encoder = Encoder(n_layers, d_model, n_heads, d_ff)
self.decoder = [DecoderBlock(d_model, n_heads, d_ff) for _ in range(n_layers)]
self.output_proj = Linear(d_model, tgt_vocab_size)

def forward(self, src, tgt):
# 编码
src_embedded = self.src_embed(src)
encoder_output = self.encoder(src_embedded)

# 解码
tgt_embedded = self.tgt_embed(tgt)

# 因果掩码:下三角矩阵,防止看到未来
seq_len = tgt.shape[1]
causal_mask = tril(ones(seq_len, seq_len)).unsqueeze(0).unsqueeze(0)

x = tgt_embedded
for layer in self.decoder:
x = layer(x, encoder_output, self_mask=causal_mask)

# 投影到词表
logits = self.output_proj(x)

return logits

3.7 Transformer 为什么能取代 RNN?

维度 RNN Transformer
并行性 必须串行(O(T) 步) 完全并行(O(1) 步)
长程依赖 受限于门控机制 自注意力直接连接任意两位置
计算复杂度 O(T · d²) O(T² · d)
参数效率 参数少(共享参数) 参数多(大量投影矩阵)

关键洞察:Transformer 用 O(T²) 的计算换来了 O(1) 的路径长度。对于序列中的任意两个位置,信息传递只需要一步自注意力。而 RNN 需要 O(T) 步递归传递。

代价:自注意力的 O(T²) 复杂度意味着处理超长序列时显存和计算量爆炸。这催生了后续的稀疏注意力、线性注意力等改进。


第四章 后 Transformer 时代:三条分化路径

4.1 BERT:双向编码器

2018 年,Devlin 等人提出了 BERT,只用 Transformer 的编码器部分:

1
2
3
4
5
6
7
训练目标 1:Masked Language Model(MLM)
输入: "我 [MASK] 北京"
目标: 预测 [MASK] = "爱"

训练目标 2:Next Sentence Prediction(NSP)
输入: 句子A + [SEP] + 句子B
目标: B 是否是 A 的下一句?

BERT 的核心贡献是证明了:在大规模无标注语料上预训练的双向编码器,可以为几乎所有 NLP 任务提供强大的特征表示。fine-tune 只需加一个简单的分类头。

4.2 GPT:自回归解码器

2018-2020 年,OpenAI 沿着另一条路径——只用解码器,做自回归语言模型:

1
2
3
4
5
6
训练目标:Next Token Prediction
输入: "我 爱"
目标: 预测下一个 token = "北京"

输入: "我 爱 北京"
目标: 预测下一个 token = "天安门"

GPT 的哲学是:预测下一个 token 这个单一目标,足以驱动模型学会语法、语义、常识、推理……甚至在一定程度上学会"思考"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class GPTModel:
def __init__(self, vocab_size, d_model, n_heads, n_layers, d_ff, max_seq_len):
self.token_embed = Embedding(vocab_size, d_model)
self.pos_embed = Embedding(max_seq_len, d_model) # 可学习的位置编码
self.layers = [DecoderOnlyBlock(d_model, n_heads, d_ff) for _ in range(n_layers)]
self.norm = LayerNorm(d_model)
self.head = Linear(d_model, vocab_size, bias=False)

# 权重共享:embedding 和输出投影共享参数
self.head.weight = self.token_embed.weight

def forward(self, input_ids):
B, T = input_ids.shape

# Token embedding + Position embedding
tok_emb = self.token_embed(input_ids) # (B, T, d_model)
pos_emb = self.pos_embed(arange(T)) # (T, d_model)
x = tok_emb + pos_emb

# 因果掩码
mask = tril(ones(T, T)).view(1, 1, T, T)

# Transformer 层
for layer in self.layers:
x = layer(x, mask)

x = self.norm(x)
logits = self.head(x) # (B, T, vocab_size)

return logits

4.3 规模化的力量:Scaling Laws

2020 年,Kaplan 等人发现了大语言模型的 Scaling Laws:

L(N)(NcN)αNL(N) \approx \left(\frac{N_c}{N}\right)^{\alpha_N}

公式说明:损失 L 随参数量 N 呈幂律下降。N_c 是常数,α_N ≈ 0.076 是缩放指数。

变量说明

  • L:交叉熵损失
  • N:模型参数量
  • α_N ≈ 0.076:缩放指数
  • N_c ≈ 8.8 × 10^13:常数

这意味着:损失随参数量呈幂律下降。只要持续增大模型和数据,性能就会持续提升——不需要架构创新。

这解释了为什么 GPT-4、Claude、Gemini 等模型的核心架构与 2017 年的 Transformer 并无根本区别。真正改变的是规模:参数从 6500 万(原始 Transformer)到数千亿,训练数据从数 GB 到数 TB。

4.4 架构微创新:在 Transformer 框架内优化

虽然核心架构未变,但现代大模型在细节上做了大量优化:

改进 内容 意义
RoPE 用旋转矩阵编码相对位置 外推性更好,支持更长上下文
GQA 多个 query 头共享 key/value 头 减少 KV cache,降低推理成本
SwiGLU 用 Swish+GLU 替换 ReLU 训练更稳定,性能更好
Flash Attention 分块计算注意力,减少 HBM 访问 2-4x 训练加速
KV Cache 缓存已计算的 K、V 自回归推理时避免重复计算

RoPE:旋转位置编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class RotaryPositionalEmbedding:
def __init__(self, d_model, max_seq_len=8192, base=10000):
# 计算每个维度的旋转频率
inv_freq = 1.0 / (base ** (arange(0, d_model, 2) / d_model))
self.inv_freq = inv_freq

def forward(self, x, seq_len):
"""
x: (batch, n_heads, seq_len, d_k)
"""
# 生成旋转角度
t = arange(seq_len)
freqs = outer(t, self.inv_freq) # (seq_len, d_k/2)

# 构造旋转矩阵的等价复数表示
emb = concat([freqs, freqs], dim=-1) # (seq_len, d_k)
cos_emb = cos(emb)
sin_emb = sin(emb)

# 应用旋转:将 x 的相邻维度视为复数对,乘以 e^{iθ}
x1, x2 = x[..., :d_k//2], x[..., d_k//2:]
rotated = concat([
x1 * cos_emb - x2 * sin_emb,
x1 * sin_emb + x2 * cos_emb
], dim=-1)

return rotated

RoPE 的巧妙之处在于:两个位置 m 和 n 的 query-key 内积只依赖于相对位置 m-n,而不是绝对位置。这使得模型天然具有平移等变性。

GQA:分组查询注意力

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class GroupedQueryAttention:
"""
标准 MHA: n_heads 个 query, n_heads 个 key, n_heads 个 value
GQA: n_heads 个 query, n_kv_heads 个 key/value (n_kv_heads < n_heads)
MQA: n_heads 个 query, 1 个 key, 1 个 value (GQA 的极端情况)
"""
def __init__(self, d_model, n_heads, n_kv_heads):
self.n_heads = n_heads
self.n_kv_heads = n_kv_heads
self.n_groups = n_heads // n_kv_heads # 每组多少个 query 头
self.d_k = d_model // n_heads

self.W_q = Linear(d_model, n_heads * self.d_k)
self.W_k = Linear(d_model, n_kv_heads * self.d_k)
self.W_v = Linear(d_model, n_kv_heads * self.d_k)
self.W_o = Linear(n_heads * self.d_k, d_model)

def forward(self, x):
B, T, D = x.shape

Q = self.W_q(x).view(B, T, self.n_heads, self.d_k).transpose(1, 2)
K = self.W_k(x).view(B, T, self.n_kv_heads, self.d_k).transpose(1, 2)
V = self.W_v(x).view(B, T, self.n_kv_heads, self.d_k).transpose(1, 2)

# 扩展 K、V 以匹配 Q 的头数
K = K.repeat_interleave(self.n_groups, dim=1) # (B, n_heads, T, d_k)
V = V.repeat_interleave(self.n_groups, dim=1) # (B, n_heads, T, d_k)

output = scaled_dot_product_attention(Q, K, V)
output = output.transpose(1, 2).view(B, T, -1)
return self.W_o(output)

GQA 的动机很简单:推理时,KV Cache 的大小与 n_kv_heads × d_k × seq_len 成正比。减少 KV 头数直接减少显存占用和内存带宽需求,对长上下文推理至关重要。


第五章 范式之间的回响

5.1 三次跃迁的共同逻辑

回顾 CNN → RNN → Transformer 的发展,有一个共同逻辑:

每次新范式的出现,都是因为前一个范式的归纳偏置(inductive bias)与任务需求不匹配。

  • CNN 假设数据具有空间局部性和平移不变性 → 适合图像,不适合序列
  • RNN 假设数据具有时间局部性和因果性 → 适合序列,但无法并行且长程依赖受限
  • Transformer 几乎不做假设(最少的归纳偏置)→ 最灵活,但需要更多数据来学习偏置

这解释了为什么 Transformer 需要"大力出奇迹":它放弃了 CNN 和 RNN 内置的先验知识,转而用海量数据和参数来从数据中学到这些模式。

5.2 融合的趋势

当前最前沿的研究正在重新引入归纳偏置:

  • Vision Transformer (ViT):把 Transformer 用在图像上,但用分块(patch)操作引入了类似 CNN 的局部性
  • ConvNeXt:用 Transformer 的训练技巧改造 CNN,性能匹敌 ViT
  • Mamba/SSM:用状态空间模型引入类似 RNN 的递归结构,在长序列上比 Transformer 更高效
  • 混合架构:CNN 提取局部特征 + Transformer 建模全局关系,在视觉和多模态任务中广泛使用

5.3 下一个范式?

2024 年以来,几个值得关注的趋势:

  1. 状态空间模型(SSM):Mamba 等模型试图用选择性状态空间替代注意力,实现线性复杂度的序列建模
  2. Diffusion + Transformer:DiT 用 Transformer 替换 U-Net 中的卷积,成为图像生成的标准架构
  3. MoE(Mixture of Experts):不是架构替代,而是扩展策略——用稀疏激活的专家网络在不增加推理成本的情况下扩大参数量

无论下一个范式是什么,理解 CNN、RNN、Transformer 的设计哲学和局限性,都是理解未来架构的必要前提。因为每一次范式跃迁,本质上都是在回应同一个问题:

如何用更少的先验假设、更高效的计算方式,建模数据中更复杂的依赖关系?


第六章 从原理到实践:如何更高效地使用 AI

理解了 CNN 的感受野、RNN 的记忆门控、Transformer 的注意力机制,这些知识不应该只停留在"了解"的层面。当你真正理解了这些架构的工作原理,你就能像调试电路一样调试你和 AI 的协作流程——知道信号在哪里衰减,注意力应该指向哪里,记忆如何被有效利用。

这一章不是泛泛而谈的"AI 使用技巧",而是从前五章的架构原理出发,推导出一套有底层逻辑支撑的 AI 使用方法论。尤其是在当今 Vibe Coding(氛围编程)的浪潮中,理解这些原理能让你的效率产生质变。


6.1 从 Transformer 的注意力机制理解 Prompt 工程

原理回溯:注意力是稀缺资源

回看第三章的自注意力公式:

Attention(Q,K,V)=softmax(QKdk)V\operatorname{Attention}(Q, K, V) = \operatorname{softmax}\left(\frac{QK^{\top}}{\sqrt{d_k}}\right) V

公式回顾:注意力 = softmax(Q × K 的转置 / √d_k) × V

softmax 的核心性质是权重归一化——所有注意力权重之和为 1。这意味着:注意力是零和博弈。给一个 token 更多注意力,必然意味着给其他 token 更少。

这就是为什么 Prompt 中塞入过多无关信息会导致输出质量下降——不是因为模型"看不懂",而是因为注意力被稀释了

实践原则:控制注意力分布

原则 1:信噪比决定输出质量

1
2
3
4
5
6
7
8
9
10
11
12
❌ 低信噪比 Prompt:
"帮我写一个 Python 的 Web 服务器,要用 Flask,要有用户登录功能,
要连接数据库,要处理错误,要有日志,要美观,要安全,要高效,
顺便帮我写个 README,再加个 Dockerfile,对了还要单元测试……"

✅ 高信噪比 Prompt:
"用 Flask 写一个 Web 服务器,核心功能:
1. 用户注册/登录(JWT 认证)
2. SQLite 数据库(SQLAlchemy ORM)
3. 全局错误处理 + 日志记录

只输出代码,不解释。"

第二个 Prompt 更好,不是因为它更短,而是因为每个 token 都携带了高密度的任务信号。在自注意力计算中,"核心功能"这三个字会把注意力权重集中在紧随其后的列表上,而非分散到无数个"要"字上。

原则 2:位置编码效应——开头和结尾权重最高

Transformer 的位置编码让模型对序列的首尾位置有天然的敏感度。在长 Prompt 中,最关键的信息应该放在:

  • 开头:任务定义、角色设定(这会影响后续所有 token 的 Query 向量)
  • 结尾:约束条件、输出格式(这是模型在生成前最后"看到"的信息)
1
2
3
4
5
6
7
8
9
10
✅ 结构化 Prompt 模板:

[角色/任务定义] ← 开头:锚定注意力
你是一个 Python 后端专家。

[详细上下文]
...

[约束与格式] ← 结尾:收束注意力
只输出代码。使用 Python 3.10+。不要解释。

原则 3:Few-shot 是在训练一个临时的小网络

Few-shot examples 本质上是在通过 in-context learning 调整注意力模式。每一个示例都在告诉模型:“输入长这样时,注意力应该这样分配”。示例之间的格式一致性至关重要——不一致的格式会让模型在应该关注内容的时候去关注格式差异。

1
2
3
4
5
6
7
8
9
❌ 格式不一致的 Few-shot:
输入: 翻译"hello" → 输出: 你好
Input: translate "world" → Output: 世界
请翻译 "cat"

✅ 格式一致的 Few-shot:
输入: "hello" → 输出: 你好
输入: "world" → 输出: 世界
输入: "cat" →

格式一致的示例让模型可以复用同一个注意力模式,而非为每个示例学习新的模式。


6.2 从 RNN 的记忆机制理解上下文窗口管理

原理回溯:梯度消失的隐喻

第二章中我们推导了 RNN 的梯度消失问题:当序列过长时,早期信息的梯度指数衰减:

Lh0=LhTt=1Ththt10\frac{\partial L}{\partial h_0} = \frac{\partial L}{\partial h_T} \prod_{t=1}^{T} \frac{\partial h_t}{\partial h_{t-1}} \approx 0

公式回顾:初始时刻 h₀ 的梯度 ≈ 最终时刻 hₜ 的梯度 × 所有中间步梯度的连乘 → 趋近于 0

Transformer 用自注意力取代了递归,但上下文窗口的有限性创造了一种新的"梯度消失"——当对话超过上下文长度时,早期信息被截断或压缩,效果等同于梯度消失。

实践原则:像 LSTM 管理细胞状态一样管理上下文

原则 1:关键信息必须"加法性"地传递

LSTM 用 c_t = f_t * c_prev + i_t * c_new 的加法性更新解决了梯度消失。在长对话中,我们也需要加法性地维护关键信息

1
2
3
4
5
6
7
8
9
10
11
❌ 乘法性传递(依赖模型"记住"):
[第1轮] 用户: 我的项目用的是 React 18 + TypeScript + Vite
...
[第15轮] 用户: 帮我写一个新组件
(模型可能已经"忘记"了技术栈)

✅ 加法性传递(显式维护):
每次开始新任务时,在 Prompt 开头加入:
"技术栈: React 18 + TypeScript + Vite + Tailwind CSS
项目结构: src/components/, src/hooks/, src/utils/
代码风格: 函数组件 + 自定义 Hook,不使用 class"

这就是为什么 System Prompt 如此重要——它在每次对话的最开头锚定了最关键的信息,相当于 LSTM 中遗忘门始终为 1 的细胞状态。

原则 2:上下文压缩 = 信息瓶颈

Seq2Seq 架构的信息瓶颈告诉我们:把长信息压缩到固定维度的向量中,必然丢失细节。同理,当你把一个 500 行的文件粘贴到对话中,模型只能分配有限的注意力给每一行。

更好的做法是只传递相关的上下文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
❌ 传递整个文件:
"这是我 2000 行的 app.tsx,帮我修改第 1347 行的按钮样式"

✅ 传递局部上下文 + 结构信息:
"项目使用 Next.js 14 App Router。
需要修改的组件位于 src/components/Header.tsx:

```tsx
// 第 20-35 行
export function NavButton({ label, href }: NavButtonProps) {
return (
<Link href={href} className="px-4 py-2 bg-blue-500 text-white rounded">
{label}
</Link>
)
}

请将按钮样式改为 ghost 风格(透明背景 + 边框)。"

1
2
3
4
5
6
7
8
9
10
11
12

**原则 3:像残差连接一样,保持信息的恒等通路**

ResNet 的核心洞见是 `out = F(x) + x`——即使变换层学不到有用的东西,信息也能通过恒等通路无损传递。在 AI 协作中,这意味着:

**永远保留一份"源文件"作为 identity mapping。** 不要让 AI 连续修改同一个文件 10 次而不做版本记录——就像深层网络没有残差连接会导致信息退化一样,连续修改会导致代码偏离原始意图。

```bash
# 每次重大修改前,创建一个"残差连接"
cp src/app.tsx src/app.tsx.backup.v3
# 或者用 Git
git commit -m "refactor: extract NavButton before style change"

6.3 从 CNN 的感受野理解任务分解

原理回溯:层次化特征提取

CNN 的伟大之处在于层次化的感受野

1
2
3
4
5
第1层卷积: 感受野 3×3  → 检测边缘
第2层卷积: 感受野 5×5 → 检测纹理
第3层卷积: 感受野 7×7 → 检测部件
...
第N层卷积: 感受野覆盖全图 → 检测物体

低层看细节,高层看整体。这不是任意的设计选择,而是从局部到全局的必然递进——你不可能在没检测到边缘的情况下直接检测物体。

实践原则:Vibe Coding 的层次化策略

在 Vibe Coding 中,最常见的错误是试图一步到位——给 AI 一个模糊的大需求,然后期望它一次生成完整的系统。这就像用一个巨大的全连接层替代所有卷积层:理论上可能,实际上参数灾难。

原则 1:从局部到全局,逐层构建

1
2
3
4
5
6
7
8
9
10
11
12
13
第1层(边缘检测)— 确定技术选型和数据模型
"我需要构建一个任务管理应用。
技术栈: Next.js 14 + Prisma + PostgreSQL
请先只设计数据库 Schema 和 API 路由结构"

第2层(纹理组合)— 实现核心业务逻辑
"基于上面的 Schema,实现 CRUD API 的代码"

第3层(部件识别)— 构建前端组件
"为任务列表实现 React 组件,使用上面的 API"

第4层(整体检测)— 集成和样式
"将组件组合成完整页面,添加 Tailwind 样式和响应式布局"

原则 2:每层有独立的"卷积核"——单一职责的 Prompt

CNN 的每个卷积核只检测一种模式。同样,每个 Prompt 应该只有一个明确的关注点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
❌ 一个核检测所有模式:
"写一个完整的用户系统,包括注册、登录、权限管理、
邮箱验证、密码重置、个人资料编辑、头像上传"

✅ 每个核只检测一种模式:

Prompt 1: "实现用户注册 API(POST /api/auth/register),
接收 email + password,返回 JWT"

Prompt 2: "实现登录 API(POST /api/auth/login),
接收 email + password,返回 JWT + refresh token"

Prompt 3: "实现 JWT 验证中间件,
从 Authorization header 提取并验证 token"

原则 3:权值共享——抽象和复用

CNN 在空间上共享卷积核参数。在代码生成中,设计模式(Design Patterns)就是编程的权值共享——用同一个模式处理不同但结构相似的问题:

1
2
3
4
5
6
7
8
9
10
11
12
"以下是我们项目中 API 路由的标准模式:

```typescript
// 标准 API 路由模板
export async function GET(request: Request) {
try {
const data = await prisma.model.findMany({ where: { userId } });
return Response.json(data);
} catch (error) {
return Response.json({ error: 'Fetch failed' }, { status: 500 });
}
}

请按照这个模式,为 Task 模型实现 GET/POST/PUT/DELETE 四个路由。"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

这样做的好处是:模型不需要为每个路由"学习"新的错误处理和响应格式——就像卷积核在不同位置复用参数一样,设计模式在不同场景中复用逻辑。

---

### 6.4 从多头注意力理解多角度审查

#### 原理回溯:多头 = 多视角

第三章中,多头注意力让模型同时关注不同维度的关联:

- 头 1:语法结构
- 头 2:语义关联
- 头 3:指代关系

单一视角的注意力会遗漏信息。同样,**单一视角的代码审查也会遗漏问题**。

#### 实践原则:用多个"注意力头"审查 AI 输出

当你让 AI 生成了一段代码,不要只从一个角度审查。模仿多头注意力,用**至少三个视角**检查:

审查头 1 — 功能正确性:
“这段代码是否能正确实现需求?边界条件是否处理?”

审查头 2 — 安全性:
“是否存在 SQL 注入、XSS、未授权访问等安全风险?”

审查头 3 — 可维护性:
“代码是否符合项目既有的设计模式?命名是否一致?
是否过度工程或工程不足?”

1
2
3

甚至可以直接让 AI 扮演不同的"头":

"请分别从以下三个角色审查这段代码:

  1. 安全工程师 — 找出所有安全漏洞
  2. 性能工程师 — 找出所有性能瓶颈
  3. 代码审查员 — 找出所有可维护性问题

每个角色独立输出发现,最后汇总。"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

---

### 6.5 从 Scaling Laws 理解迭代策略

#### 原理回溯:幂律下降

第四章的 Scaling Laws 告诉我们:

$$
L(N) \approx \left(\frac{N_c}{N}\right)^{\alpha_N}, \quad \alpha_N \approx 0.076
$$

> 公式回顾:损失 L 随参数量 N 呈幂律下降,α_N ≈ 0.076 是缩放指数。

损失随参数量呈幂律下降,但**边际收益递减**——从 1M 到 10M 参数的提升,远大于从 100B 到 110B 的提升。

#### 实践原则:投入的边际收益也是递减的

**原则 1:用最小的 Prompt 达到 80% 的效果**

不要花 30 分钟精心打磨一个 Prompt。80% 的效果来自 20% 的指令。先快速得到一个 80 分的结果,然后**针对性补强**:

第1轮: “写一个 React 表单组件” → 得到 80 分的代码
第2轮: “加上表单验证” → 补强到 90 分
第3轮: “加上加载状态和错误提示” → 补强到 95 分

1
2
3
4
5
6
7

这比一次性写出完美 Prompt 更高效,因为**每一轮的反馈都告诉你下一轮应该把注意力集中在哪里**。

**原则 2:换模型 = 换参数规模**

Scaling Laws 也意味着:当你用 GPT-3.5 级别的模型无法完成任务时,换用 GPT-4 级别模型的效果提升,往往大于在 Prompt 上花几个小时优化的效果提升。

策略决策树:

  1. 简单任务(格式化、翻译、小修改)→ 快速模型(快+便宜)
  2. 中等任务(写新功能、调试)→ 中等模型
  3. 复杂任务(架构设计、多文件重构)→ 最强模型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

**原则 3:数据质量 > 模型规模 > Prompt 技巧**

Scaling Laws 的另一个启示:模型规模和训练数据量是性能的两大驱动力。映射到使用层面:

- **数据质量**(你提供的上下文是否准确、完整)> **模型选择**(GPT-4 还是 Claude)> **Prompt 技巧**(措辞是否精妙)

一个精确的需求描述 + 中等模型,胜过一个模糊的需求 + 最强模型。

---

### 6.6 从编码器-解码器架构理解人机协作

#### 原理回溯:编码器理解,解码器生成

Transformer 的编码器负责"理解"源序列,解码器负责"生成"目标序列。两者通过交叉注意力连接——解码器在每一步生成时,都会通过交叉注意力"回头看"编码器的输出。

#### 实践原则:你是编码器,AI 是解码器

在 Vibe Coding 中,最优的人机分工是:

你(编码器)→ 理解需求,定义约束,设计架构
AI(解码器)→ 根据你的编码,生成具体实现

交叉注意力 → 你对 AI 输出的审查和反馈,
引导 AI 的下一步生成聚焦到正确位置

1
2
3

**常见误区:让 AI 同时做编码器和解码器**

❌ “帮我从零开始设计并实现一个电商系统”
→ AI 既是编码器又是解码器,缺乏你的领域知识作为交叉注意力


你: “电商系统需求:[详细需求文档]
技术约束:[明确的技术选型]
数据模型:[你设计的 Schema]
请实现订单模块的 API 层代码”
→ 你完成了编码器的工作,AI 只负责解码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

**原则:你的领域知识是交叉注意力的 Key/Value**

在 Transformer 中,解码器通过交叉注意力获取编码器的表示。在 AI 协作中,**你的领域知识就是 AI 的交叉注意力来源**。你提供的业务规则、代码约定、架构决策,都在告诉 AI:"在生成每一个 token 时,应该关注什么"。

如果你不给 AI 提供这些 Key/Value,它只能依赖预训练数据中的"通用知识"——这些知识可能正确,也可能与你的具体场景不匹配。

---

### 6.7 从残差连接理解渐进式重构

#### 原理回溯:信息无损通路

ResNet 的 `out = F(x) + x` 保证了即使变换 F 是退化的,原始信息 x 也能无损传递。这就是"渐进式修改"的数学基础。

#### 实践原则:每次只改一个"变换层"

❌ 大爆炸重构:
“把这个 jQuery 项目重写成 React + TypeScript + Next.js”

✅ 渐进式重构(每步保持 identity mapping):

Step 1: 引入构建工具,不改任何业务代码
→ out = build_tool(x) + x, F ≈ 0, out ≈ x

Step 2: 把一个页面从 jQuery 迁移到 React
→ out = react_migration(x) + x, 只改一个页面

Step 3: 逐步迁移其他页面
→ 每次都是 F(x) + x,不是整体替换

Step 4: 引入 TypeScript(类型标注)
→ out = types(x) + x,行为不变,只是加了类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

每一步都保证:如果新引入的 F 出了问题,你可以回退到 x(原始代码)。这就是残差连接在工程实践中的对应物——**可回退的渐进式变更**。

---

### 6.8 从 FFN 的扩展比理解思维空间

#### 原理回溯:先扩展,再压缩

Transformer 的 FFN 子层把维度从 d_model 扩展到 4 × d_model,再压缩回 d_model:

```python
FFN(x) = W2 · ReLU(W1 · x + b1) + b2
# W1: (d_model, 4*d_model) → 扩展
# W2: (4*d_model, d_model) → 压缩

直觉是:在高维空间中做非线性变换,比在低维空间中更容易找到好的解

实践原则:先发散,再收敛

在 Vibe Coding 中,当你面对一个复杂问题时:

1
2
3
4
5
6
7
8
9
发散阶段(扩展到 4x 空间):
"列出实现这个功能的至少 5 种不同方案,包括优缺点"

收敛阶段(压缩回 1x 空间):
"基于上面的分析,选择最佳方案并实现"

这比直接说"帮我实现 XXX"更好,因为:
1. 扩展阶段让模型在更大的解空间中搜索
2. 收敛阶段利用了扩展阶段的信息来做更好的决策

同样适用于调试:

1
2
发散: "这段代码可能出 bug 的原因有哪些?列出所有可能性"
收敛: "根据错误日志 [具体日志],最可能的原因是哪一个?请修复"

6.9 Vibe Coding 的完整工作流:架构原理的综合应用

把以上所有原则组合起来,一个高效的 Vibe Coding 工作流应该是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
┌─────────────────────────────────────────────────────────┐
│ 你(编码器) │
│ ┌─────────────┐ ┌──────────────┐ ┌───────────────┐ │
│ │ 需求定义 │ │ 架构设计 │ │ 约束条件 │ │
│ │ (Query) │ │ (Key/Value) │ │ (Attention │ │
│ │ │ │ │ │ Mask) │ │
│ └──────┬──────┘ └──────┬───────┘ └──────┬────────┘ │
│ │ │ │ │
│ └────────────────┼──────────────────┘ │
│ │ │
│ 交叉注意力 │
│ │ │
│ ▼ │
│ AI(解码器) │
│ ┌─────────────────────────────────────────────────────┐│
│ │ 层次化生成(CNN 式感受野递增) ││
│ │ Schema → API → 组件 → 样式 ││
│ │ ││
│ │ 每步单一职责(一个卷积核一个模式) ││
│ │ 渐进式修改(残差连接保证可回退) ││
│ │ 先发散后收敛(FFN 扩展-压缩) ││
│ └─────────────────────────────────────────────────────┘│
│ │ │
│ ▼ │
│ 多头审查 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 功能头 │ │ 安全头 │ │ 可维护头 │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │ │
│ ▼ │
│ 迭代优化 │
│ (Scaling Laws: 边际收益递减, │
│ 优先补强最薄弱环节) │
└─────────────────────────────────────────────────────────┘

具体操作清单

阶段 原理来源 操作
任务定义 注意力分布 开头定义角色和目标,结尾写约束
上下文管理 LSTM 记忆机制 System Prompt 维护核心信息(加法性传递)
任务分解 CNN 感受野 从局部到全局,逐层构建
Prompt 设计 卷积核权值共享 单一职责 + 设计模式复用
代码生成 编码器-解码器 人做编码器(理解),AI 做解码器(生成)
代码审查 多头注意力 多视角独立审查
调试排查 FFN 扩展比 先发散(列举可能性),再收敛(定位修复)
重构优化 残差连接 渐进式变更,每步可回退
模型选择 Scaling Laws 任务复杂度匹配模型能力,数据质量优先
迭代节奏 边际收益递减 快速 80 分 → 针对性补强到 95 分

6.10 一个完整的实战案例

让我们用上面的方法论,实际操作一个 Vibe Coding 任务:构建一个 Markdown 博客系统

Step 1:编码器阶段(你的工作)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 项目定义(Query - 决定"我在找什么")

## 技术栈
- Next.js 14 App Router + TypeScript
- MDX for content
- Tailwind CSS + shadcn/ui
- Vercel 部署

## 核心功能
1. Markdown 文章列表(分页)
2. 文章详情页(支持代码高亮)
3. 标签筛选
4. 暗色模式

## 数据模型(Key/Value - 提供交叉注意力)
Post {
slug: string
title: string
date: string
tags: string[]
content: MDX
}

## 约束(Attention Mask - 限制注意力范围)
- 不使用数据库,MDX 文件作为数据源
- 不需要评论功能
- 不需要用户认证

Step 2:层次化生成(CNN 式递进)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Prompt 1(感受野: 数据层):
"基于上面的数据模型,实现 MDX 文件的读取和解析工具函数。
要求:
1. 从 /content/posts/ 目录读取 .mdx 文件
2. 解析 frontmatter(title, date, tags)
3. 导出 getAllPosts() 和 getPostBySlug() 函数
只输出这一个文件。"

Prompt 2(感受野: API 层):
"基于上一步的工具函数,实现 Next.js 的路由:
1. /api/posts — 返回文章列表
2. /api/posts/[slug] — 返回文章详情
只输出 API 路由文件。"

Prompt 3(感受野: 组件层):
"实现文章列表组件 PostList 和文章详情组件 PostDetail。
使用 shadcn/ui 的 Card 组件。
代码高亮使用 rehype-pretty-code。
只输出这两个组件文件。"

Prompt 4(感受野: 页面层):
"将组件组合成页面:
1. / — 首页,显示文章列表
2. /posts/[slug] — 文章详情页
3. /tags/[tag] — 标签筛选页
实现暗色模式切换。
只输出页面文件和 layout。"

Step 3:多头审查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
"请从以下三个角度审查我项目中的代码:

1. 安全审查:检查是否有路径遍历风险(slug 参数直接拼接文件路径)
2. 性能审查:MDX 解析是否应该缓存?静态生成还是动态渲染?
3. 可维护性:组件拆分是否合理?类型定义是否完整?

输出格式:
### 安全问题
- [问题] → [修复建议]

### 性能问题
- [问题] → [修复建议]

### 可维护性问题
- [问题] → [修复建议]"

Step 4:渐进式优化(残差连接)

1
2
3
4
5
6
7
"当前代码工作正常。请做以下优化,每次只改一处:

1. 为 getPostBySlug 添加路径遍历防护(slug 只允许字母数字和连字符)
2. 为文章列表添加 generateStaticParams 静态生成
3. 为 MDX 内容添加 readingTime 计算

每项优化独立,不影响其他代码。"

每步都是 out = F(x) + x——改动是 F(x),原有代码是 x,合在一起就是渐进增强。


6.11 最后一层:当原理成为直觉

回看全文的脉络:

  • CNN 教会我们:从局部到全局,层次化地处理复杂问题
  • RNN 教会我们:维护好记忆,关键信息必须加法性地传递
  • Transformer 教会我们:注意力是稀缺资源,每一行 Prompt 都应该有存在的理由
  • ResNet 教会我们:渐进式变更,永远保留回退的通路
  • 多头注意力 教会我们:用多个视角审查,避免单点盲区
  • Scaling Laws 教会我们:数据质量 > 模型规模 > 技巧,边际收益递减

这些不是牵强的类比,而是同一套信息处理原则在不同层面的体现。当你真正理解了注意力权重如何分配、梯度如何流动、信息如何压缩与传递,你和 AI 的协作就不再停留在"写 Prompt"的层面——你在设计信息流

这才是最大化使用 AI 的本质:不是学会说什么,而是理解你说的每一句话,在模型的计算图中会走多远。


附录A:架构参数对比(速查)

模型 年份 参数量 层数 隐维度 注意力头 关键创新
AlexNet 2012 61M 8 ReLU, Dropout, GPU训练
VGGNet 2014 138M 19 小卷积核堆叠
ResNet-152 2015 60M 152 残差连接
LSTM 1997 门控机制
Transformer 2017 65M 6 512 8 自注意力
BERT-Large 2018 340M 24 1024 16 双向预训练
GPT-2 2019 1.5B 48 1600 25 大规模自回归
GPT-3 2020 175B 96 12288 96 Few-shot learning
LLaMA-2 70B 2023 70B 80 8192 64 GQA, RoPE, SwiGLU
LLaMA-3 405B 2024 405B 126 16384 128 大规模 GQA

本文后半部分写于 2026 年 4 月。当 Vibe Coding 成为常态,理解底层原理不再是学术奢侈,而是工程必需。