深度学习四大基石:反向传播、CNN、Embedding 与注意力
深度学习四大基石:反向传播、CNN、Embedding 与注意力
如果把深度学习比作一座大厦,那么反向传播、卷积神经网络(CNN)、词嵌入(Embedding)和注意力机制就是支撑起这座大厦的四根承重柱。无论后来的模型多么庞大、多么"智能",追根溯源都绕不开这四个基础概念。本文尝试用尽量直白的语言,把这四块基石讲透彻——不追求覆盖所有数学细节,而是讲清楚"为什么这样设计"和"这样设计解决了什么问题"。
🧠 层层抽象:多层网络与反向传播
单层网络的天花板:只会画直线
一个最简单的神经元(感知机)做的事情其实很朴素:把输入按权重加权求和,再套一个非线性函数,判断结果落在哪一边。几何上看,一个单层神经元本质上是在特征空间里画一条"分界线"(更高维时是一个超平面),凡是落在线一侧的判为类别A,另一侧判为类别B。
这带来一个著名的局限:单层网络只能解决线性可分的问题。历史上最典型的反例是异或(XOR)问题——四个点 (0,0)→0、(0,1)→1、(1,0)→1、(1,1)→0,无论怎么画一条直线,都不可能把两类点正确分开。这曾经在上世纪60年代末给感知机研究泼了一盆冷水,导致神经网络研究一度陷入低谷。
多层带来的转机:逐层抽象
解决办法是"叠层"。如果在输入和输出之间插入一个隐藏层,网络就能先用第一层画出若干条直线,把空间切割成若干个区域,再用第二层把这些区域重新组合,就能拟合出任意复杂的非线性边界。这就是多层感知机(MLP)能解决XOR问题、并且理论上可以逼近任意连续函数(万能近似定理)的原因。
更重要的是,多层结构天然带来了"层层抽象"的能力。以图像识别为例:
这种从低级特征到高级语义的层层抽象,正是深度学习"深度"二字的含义所在——层数越多,理论上能表达的抽象层级就越丰富。当然层数不是越多越好,层数一多就会带来梯度消失/爆炸、训练难度陡增等新问题,这也是后来ResNet残差连接等技术要解决的问题。
反向传播:网络怎么知道该怎么调整
网络结构解决了"能不能表达复杂函数"的问题,但还有一个问题:给定成千上万个参数(权重),网络怎么知道往哪个方向调整才能让预测更准?答案就是反向传播算法(Backpropagation),本质是链式法则在多层复合函数上的巧妙应用。
整个过程可以拆成两个阶段:
前向传播(Forward Pass):输入数据从第一层开始,逐层做"加权求和 + 非线性激活"的运算,一路传到输出层,得到预测值。然后用损失函数(Loss Function)衡量预测值和真实标签之间的差距,得到一个标量的"误差"。
反向传播(Backward Pass):这是关键。我们希望知道"每一个权重对最终误差的贡献有多大",也就是误差对每个权重的偏导数(梯度)。由于网络是层层复合的函数,根据微积分的链式法则,输出层的误差可以一层一层往回"传":先算出误差对最后一层输出的梯度,再乘以最后一层对倒数第二层输出的梯度,依次往前传递,最终得到误差对每一层每一个权重的梯度。因为梯度是从输出往输入反向流动的,所以叫"反向传播"。
拿到梯度之后,配合梯度下降(Gradient Descent)之类的优化算法,把每个权重沿着"能让误差变小"的方向挪动一小步(步长由学习率控制)。重复"前向传播算误差 → 反向传播算梯度 → 更新权重"这个循环成千上万次,网络的预测就会越来越准。
下面用一段简化的 numpy 伪代码演示一个只有一个隐藏层的网络如何做反向传播(为了让链式法则的过程一目了然,激活函数用简单的 sigmoid):
import numpy as np
def sigmoid(x):
return 1 / (1 + np.exp(-x))
def sigmoid_derivative(a):
# a 是sigmoid的输出,导数可以直接用输出表示
return a * (1 - a)
# 前向传播
def forward(X, W1, b1, W2, b2):
z1 = X @ W1 + b1
a1 = sigmoid(z1) # 隐藏层输出
z2 = a1 @ W2 + b2
a2 = sigmoid(z2) # 最终预测
cache = (X, z1, a1, z2, a2)
return a2, cache
# 反向传播:链式法则逐层往回传误差
def backward(y_true, W2, cache):
X, z1, a1, z2, a2 = cache
# 第一步:输出层的误差梯度(损失对a2的偏导 * a2对z2的偏导)
d_loss_a2 = a2 - y_true # 均方误差简化后的梯度
d_a2_z2 = sigmoid_derivative(a2)
delta2 = d_loss_a2 * d_a2_z2 # 输出层的"误差信号"
# 第二步:用delta2反推W2、b2的梯度
dW2 = a1.T @ delta2
db2 = delta2.sum(axis=0)
# 第三步:把误差信号通过W2传回隐藏层(链式法则的关键一步)
d_loss_a1 = delta2 @ W2.T
d_a1_z1 = sigmoid_derivative(a1)
delta1 = d_loss_a1 * d_a1_z1 # 隐藏层的"误差信号"
# 第四步:反推W1、b1的梯度
dW1 = X.T @ delta1
db1 = delta1.sum(axis=0)
return dW1, db1, dW2, db2
# 训练循环:不断前向算误差、反向算梯度、更新参数
def train_step(X, y_true, W1, b1, W2, b2, lr=0.1):
a2, cache = forward(X, W1, b1, W2, b2)
dW1, db1, dW2, db2 = backward(y_true, W2, cache)
W1 -= lr * dW1
b1 -= lr * db1
W2 -= lr * dW2
b2 -= lr * db2
return W1, b1, W2, b2可以看到,backward 函数里的核心思路就是:先算出"输出层错在哪、错多少"(delta2),再顺着连接权重 W2 把这份"责任"分摊到隐藏层的每个神经元上(delta1),层数再多也是同样的套路不断往前传递。这正是"链式法则"在工程上的具体体现——不需要对每一层单独推导公式,只需要知道"上一层的误差信号 × 连接权重 × 本层激活函数导数"就能算出这一层的误差信号,一路传到输入层为止。
理解了这个机制,也就能理解为什么深层网络训练早期会遇到梯度消失问题:如果每一层的导数都小于1,经过很多层连续相乘后梯度会指数级衰减,导致靠近输入层的参数几乎学不到东西——这也是ReLU激活函数、残差连接、批归一化等技术被发明出来的直接原因。
👁️ 计算机如何"看":卷积神经网络 CNN
为什么不能直接把图片拉平塞进全连接网络
一张224×224的彩色图片有224×224×3约15万个像素值。如果直接把它拉平成一个向量丢进全连接网络,第一层如果有1000个神经元,仅这一层就有1.5亿个参数,不但训练代价惊人,还完全忽略了图像最重要的一个特性——空间局部性:一个像素和它周围的像素密切相关,和图片另一角的像素基本无关。全连接网络对空间位置一视同仁,既浪费参数,又学不到"局部模式"这种关键先验。卷积神经网络正是针对这个问题设计的。
卷积核:一个滑动的"特征探测器"
卷积操作的核心是一个很小的矩阵,叫卷积核(Kernel/Filter),常见尺寸是3×3或5×5。这个小矩阵会在整张图片上滑动,每滑到一个位置,就把卷积核里的数值和图片上对应区域的像素值做逐元素相乘再求和,得到一个输出值。滑完整张图,就得到一张新的"特征图"(Feature Map)。
关键的巧妙之处在于:同一个卷积核在整张图片上是共享参数的(权重共享)。这意味着:如果一个卷积核学会了"识别垂直边缘",它在图片左上角能识别垂直边缘,挪到右下角照样能识别——不需要为每个位置单独学一套参数。这既大幅减少了参数量,也符合"同一种视觉模式可能出现在图片任何位置"的常识(平移不变性)。
一个网络里通常会有很多个卷积核并行工作,各自学习探测不同的模式:有的对垂直边缘敏感,有的对水平边缘敏感,有的对某种颜色过渡敏感……这些卷积核的输出堆叠在一起,就构成了这一层的输出。
从边缘到物体:层层递进的特征提取
和前面讲的"层层抽象"呼应,CNN的多层结构让它天然形成一条从简单到复杂的特征提取流水线:
这种层级结构不是人为设计出来的规则,而是网络在训练数据上自动学出来的——这也是深度学习相比传统人工设计特征(如SIFT、HOG)最大的优势:特征本身也是学出来的。
池化:压缩信息、扩大感受野
卷积层之间通常会插入池化层(Pooling),最常见的是最大池化(Max Pooling):把特征图切成一个个小块(比如2×2),每块只保留最大值,其余丢弃。池化的作用主要有两个:
感受野:一层看到的"视野"有多大
感受野(Receptive Field)指的是输出特征图上一个点,对应到原始输入图像上,是由哪一块区域的像素共同决定的。第一层卷积核只能看到3×3这样的局部区域,感受野很小;但随着层数加深,后面每一层的一个点都是综合了前面很多个"局部区域"的结果,感受野会像滚雪球一样越滚越大。到了网络的最深层,一个输出点的感受野可能已经覆盖了输入图像的绝大部分甚至全部区域——这就是为什么深层网络能够"看到"物体的整体结构,而不仅仅是局部纹理。池化层由于会压缩尺寸,也是感受野快速扩大的重要推手。
下面用numpy手写一个最基础的二维卷积操作,帮助建立直观理解:
import numpy as np
def conv2d(image, kernel, stride=1):
"""
对单通道灰度图做一次简单卷积(不做padding)
image: 形状 (H, W) 的二维数组
kernel: 形状 (kh, kw) 的卷积核
"""
H, W = image.shape
kh, kw = kernel.shape
out_h = (H - kh) // stride + 1
out_w = (W - kw) // stride + 1
output = np.zeros((out_h, out_w))
for i in range(0, out_h):
for j in range(0, out_w):
row_start = i * stride
col_start = j * stride
# 取出当前卷积核覆盖的图像区域
region = image[row_start:row_start + kh, col_start:col_start + kw]
# 逐元素相乘再求和,这就是一次"卷积"
output[i, j] = np.sum(region * kernel)
return output
# 一个经典的垂直边缘检测卷积核(Sobel算子的简化版)
vertical_edge_kernel = np.array([
[-1, 0, 1],
[-1, 0, 1],
[-1, 0, 1]
])
# 一个简单的最大池化实现
def max_pool2d(feature_map, pool_size=2, stride=2):
H, W = feature_map.shape
out_h = (H - pool_size) // stride + 1
out_w = (W - pool_size) // stride + 1
output = np.zeros((out_h, out_w))
for i in range(out_h):
for j in range(out_w):
row_start = i * stride
col_start = j * stride
region = feature_map[row_start:row_start + pool_size, col_start:col_start + pool_size]
output[i, j] = np.max(region)
return output把卷积核换成不同的数值,就能探测出不同的模式——这也是为什么说CNN的训练过程,本质上就是在自动"学习"一套最适合当前任务的卷积核参数。
🔤 词变成数字:Embedding 与向量空间
为什么必须把词变成数字
神经网络本质上是一堆矩阵乘法和非线性函数的组合,它只认识数字,不认识"苹果""国王"这样的文字符号。最直接的想法是给词表里每个词分配一个独一无二的编号,再用One-Hot编码——一个词对应一个只有一位是1、其余全是0的超长向量。但这样做有两个致命问题:
Embedding(词嵌入)就是为了解决这个问题而生的:把每个词映射成一个低维(通常是几十到几百维)的稠密向量,并且让这个向量空间里的几何距离能够反映词与词之间的语义关系。
训练直觉:分布式语义假设
Embedding是怎么学出来的?背后依赖一个语言学假设,叫分布式语义假设(Distributional Hypothesis),通俗说就是:"观其伴,知其义"——一个词的含义,很大程度上由经常出现在它周围的词决定。如果两个词经常出现在相似的上下文中,那么它们的含义大概率是相似的。
举个例子:"我今天喝了一杯___"这句话里,空格处填"咖啡""茶""果汁"都很自然,填"石头""椅子"就很突兀。如果在海量文本中统计,"咖啡""茶""果汁"这几个词出现的上下文高度重合,那么一个训练目标是"根据上下文预测中心词"(或反过来"根据中心词预测上下文")的模型,自然会把这几个词的向量学得彼此靠近——因为只有这样,模型才能用相近的向量在相近的上下文里做出相近的预测。这正是经典的word2vec(Skip-gram / CBOW)背后的训练思路。
用一段高度简化的伪代码来表达Skip-gram的核心思想(根据中心词预测周围词):
import numpy as np
def sigmoid(x):
return 1 / (1 + np.exp(-x))
def skipgram_training_step(center_word_id, context_word_id,
center_embeddings, context_embeddings,
vocab_size, lr=0.01):
"""
center_embeddings: 形状 (vocab_size, dim),每个词的"中心词"向量
context_embeddings: 形状 (vocab_size, dim),每个词的"上下文词"向量
目标:让 中心词向量 与 真实上下文词向量 的点积尽量大,
与随机采样的"负样本"词向量的点积尽量小。
"""
v_center = center_embeddings[center_word_id]
v_context = context_embeddings[context_word_id]
# 正样本:真实共现的词对,希望相似度(点积)越大越好
pos_score = sigmoid(np.dot(v_center, v_context))
grad_pos = (pos_score - 1) * v_context
# 负样本:随机采样几个"不该出现在这个上下文里"的词
negative_ids = np.random.choice(vocab_size, size=5)
grad_neg_total = np.zeros_like(v_center)
for neg_id in negative_ids:
v_neg = context_embeddings[neg_id]
neg_score = sigmoid(np.dot(v_center, v_neg))
grad_neg_total += neg_score * v_neg
context_embeddings[neg_id] -= lr * neg_score * v_center
# 用梯度更新中心词向量和上下文词向量(简化的梯度下降)
grad_center = grad_pos + grad_neg_total
center_embeddings[center_word_id] -= lr * grad_center
context_embeddings[context_word_id] -= lr * (pos_score - 1) * v_center
return center_embeddings, context_embeddings在数以亿计的这种"正样本拉近、负样本推远"的调整之后,词向量空间会自动"沉淀"出丰富的语义结构——同义词、近义词会聚集在相近的区域,不同词性、不同语义类别的词会分布在空间的不同角落。
向量空间里的语义代数:国王 - 男人 + 女人 ≈ 女王
Embedding最令人惊艳的性质是,词向量之间的加减运算,竟然能对应真实世界的语义关系。经典的例子是:
vector("国王") - vector("男人") + vector("女人") ≈ vector("女王")
直觉上可以这样理解:"国王"和"女王"的差别,主要就体现在"性别"这个维度上,而"男人"和"女人"的向量差,恰好也主要编码了"性别"这个语义方向。因为Embedding是在海量真实语料里,通过大量类似"国王统治王国""女王统治王国""男人是父亲""女人是母亲"这样的上下文共现关系训练出来的,性别这个语义特征就会被模型不知不觉地编码成向量空间里一个相对稳定的"方向"。当我们做"国王 - 男人"这个减法时,相当于把"国王"里"性别=男"的成分抵消掉,只留下"王室、统治者"这些共性语义;再加上"女人"的向量,相当于把"性别=女"的成分加回去,结果自然就落在"女王"附近。
类似的例子还有很多,比如"北京 - 中国 + 法国 ≈ 巴黎"(首都关系)、"游泳 - 游泳者 + 弹钢琴 ≈ 钢琴家"(动作与执行者的关系)。这些看似"智能"的代数运算,本质上都是分布式语义假设在向量空间里的自然推论——语义相近或存在某种规律关系的词,在训练数据里会展现出相似的上下文分布模式,这种规律性最终会以几何结构(方向、距离)的形式沉淀在向量空间里。
需要说明的是,这里讲的是Embedding本身"是什么、为什么有效"的原理;而拿到这些向量之后如何高效存储、如何在海量向量里做近似最近邻检索、如何构建向量数据库支撑RAG检索等工程问题,不是本文的重点,可以参考本知识库中专门讲向量数据库的文章。
现代的词向量早已从静态的word2vec/GloVe演进到基于Transformer的上下文相关Embedding(同一个词在不同句子中会有不同的向量表示),但"用向量空间的几何结构编码语义关系"这一底层逻辑至今没有变过。
🎯 注意力机制:AI 学会"划重点"
核心问题:理解一个词,该看上下文里的哪些词
考虑这样一句话:"他把钥匙放在桌子上,因为它太重了。" "它"指代的是"钥匙"还是"桌子"?人类几乎瞬间就能判断出更可能指"钥匙"——这种"理解一个词的含义,需要参考句子中其他相关的词"的需求,在处理长句子、长文档时尤为关键,相关的线索可能出现在几十个词之外。
早期的RNN/LSTM按顺序逐词处理文本,把之前所有信息压缩进一个固定长度的"隐藏状态"向量里再往后传,句子一长,越靠前的信息就越容易被"稀释"或遗忘,这就是注意力机制要解决的核心问题:让模型在处理某个词时,能够直接"回头看"整个上下文里所有的词,并自动判断哪些词更相关、更需要重点关注,而不是依赖一个逐步累积、容易遗忘的中间状态。
Query / Key / Value 的直觉:一场信息检索
理解Self-Attention最好用的比喻是"图书馆检索"。假设你(当前正在处理的词)想知道该重点参考哪些资料:
检索过程是:拿你的Query和每一本书的Key做对比,算出一个"匹配度"分数——越匹配分数越高;然后用这些匹配度分数(归一化成加起来等于1的权重)对所有书的Value做加权平均,匹配度高的书内容占的比重更大,匹配度低的书内容占比很小甚至可以忽略。这个加权平均的结果,就是Query(这个词)最终"融合了上下文信息"之后的新表示。
在Self-Attention里,句子中的每一个词都会同时扮演三个角色:它会生成自己的Query(去查询别的词),也会生成自己的Key和Value(被别的词查询)。这样一来,每个词都能根据自己的"问题",从全句所有词(包括自己)里挑出最相关的信息,重新组装出一个融合了上下文的新向量表示。这就是Self-Attention中"Self"的含义——查询者和被查询者来自同一个序列。
一个简化的注意力权重计算示例
import numpy as np
def softmax(x):
x = x - np.max(x, axis=-1, keepdims=True) # 数值稳定性处理
exp_x = np.exp(x)
return exp_x / np.sum(exp_x, axis=-1, keepdims=True)
def simple_self_attention(X, Wq, Wk, Wv):
"""
X: 形状 (seq_len, d_model),句子中每个词的初始向量表示
Wq, Wk, Wv: 分别把X投影成 Query、Key、Value 的权重矩阵
"""
Q = X @ Wq # (seq_len, d_k) 每个词的"问题"
K = X @ Wk # (seq_len, d_k) 每个词的"标签"
V = X @ Wv # (seq_len, d_v) 每个词的"内容"
d_k = K.shape[-1]
# 第一步:算出每个词的Query与所有词的Key的匹配度(点积衡量相似度)
scores = Q @ K.T / np.sqrt(d_k) # 除以sqrt(d_k)防止数值过大导致softmax梯度消失
# 第二步:把匹配度分数变成"加起来等于1"的注意力权重
attention_weights = softmax(scores) # 形状 (seq_len, seq_len)
# 第三步:用注意力权重对所有词的Value做加权求和
output = attention_weights @ V # 每个词都得到融合了上下文的新表示
return output, attention_weights举个直观的例子:如果句子是"猫 追 老鼠 因为 它 饿 了",处理"它"这个词时,理想情况下"它"生成的Query应该和"猫"的Key算出较高的匹配分数,于是attention_weights里"它→猫"这一项的权重会明显高于"它→老鼠"或"它→追",最终"它"融合上下文之后的新向量,就会带有更多"猫"的语义信息。这正是注意力机制"划重点"能力的体现——不需要人工规则,模型通过训练自动学会该在什么场景下把注意力分配给哪些词。
实际的Transformer会用"多头注意力"(Multi-Head Attention),也就是并行跑多组独立的Query/Key/Value投影,让不同的"头"学会关注不同类型的关系(比如有的头专注于指代消解,有的头专注于语法结构),最后把多个头的结果拼接起来,得到更丰富的表示。
🏗️ Transformer:改变一切的架构
前面讲的注意力机制,正是2017年那篇著名论文《Attention Is All You Need》提出的Transformer架构的核心引擎。这里不再重复注意力的底层原理(本知识库中的LLM基础一文已有详细展开),只从"架构演进"的视角,快速梳理Transformer相对CNN/RNN的关键升级。
整体结构一览:一个标准Transformer模块通常由四个部件堆叠而成——多头自注意力层(负责让每个位置聚合全局上下文信息)、位置编码(Positional Encoding,给原本不带顺序信息的注意力机制补上"词在句子中排第几"的信息)、前馈网络(Feed-Forward Network,对每个位置的表示做非线性的进一步加工)、以及贯穿始终的残差连接与层归一化(保证深层堆叠时梯度能够顺畅回传,训练更稳定)。多个这样的模块堆叠起来,就构成了完整的Encoder或Decoder。
相比RNN的优势——可并行:RNN处理序列必须严格按时间步一个接一个算,前一步不算完后一步无法开始,训练时天然难以并行,长序列训练极慢。Transformer完全抛弃了这种递归结构,一个序列里所有位置的注意力计算可以同时展开,天然适配GPU大规模并行运算,这也是它能训练到今天动辄千亿参数规模的重要前提之一。
相比RNN的优势——长距离依赖:RNN里两个相隔很远的词,信息要经过很多个时间步的层层传递才能相互影响,链条一长信息就容易衰减或丢失。而Self-Attention让任意两个位置之间只需要"一步"就能直接建立联系,无论两个词隔多远,理论上的路径长度都是O(1),这从根本上缓解了长距离依赖的问题。
相比CNN的优势——感受野:前文提到CNN要靠堆叠很多层才能让感受野覆盖整张图/整个序列,而Self-Attention每一层天然就是"全局感受野"——第一层就能让某个位置看到序列中所有其他位置,不需要靠堆层数去换取全局视野。
正是这几点优势的叠加,让Transformer迅速取代RNN/CNN成为NLP乃至多模态领域的主流骨干架构,后续的BERT、GPT系列、直到今天的各种大语言模型,基本都是在这套架构上做增量创新。