反向传播与特征提取:神经网络到底在学什么
反向传播与特征提取:神经网络到底在学什么
在深度学习普及之前,特征工程决定了项目的上限。SIFT、HOG、TF-IDF 这些手工描述子凝聚了领域知识,但也带来了明显的瓶颈:对数据分布敏感,难以捕获高阶非线性关系,而且特征设计和模型训练是割裂的——设计者没法根据最终任务的反馈来调整表示本身。
神经网络把特征的定义权交给了优化过程。它并没有 magically “发现”更好的特征,只是让损失函数来决定什么算“好”。反向传播的真正作用,是把任务目标逆向传回每一层,参数更新只是这个过程的副产品。换句话说,网络学到的表示,是任务梯度、数据分布和网络结构自带的假设(比如卷积的平移不变性)共同作用的结果。下文会用严格的矩阵求导和可运行代码,验证梯度是怎么逐层塑造特征的。
从标量到矩阵:梯度流的直觉基础
理解反向传播得从标量链式法则开始:若 z = g(y)、y = f(x),则 dz/dx = (dz/dy) · (dy/dx)。这本质上就是把全局变化率拆成局部敏感度的级联。变量升维到向量时,思想没变,只是“导数”变成了 Jacobian 矩阵。
这里有个容易混淆的点:分母布局下,∂y/∂x 的第一维度与分母 x 对齐;只有当 x 是向量时,梯度 ∂L/∂x 的形状才和 x 完全一致。比如 x ∈ ℝⁿ、y ∈ ℝᵐ,∂y/∂x ∈ ℝⁿˣᵐ,第 j 行第 i 列是 ∂yᵢ/∂xⱼ。此时链式法则写作 ∂z/∂x = (∂y/∂x)ᵀ ∂z/∂y。这个形式自然保证了梯度形状和参数一致,不用额外 reshape。这很关键。
在计算图里,梯度从损失节点逆向流动,每一步都是上游梯度和当前操作 Jacobian 转置的乘积。需要澄清的是,∂L/∂W 不是“W 对 L 的贡献”,而是 L 对 W 的敏感度,指向 W 空间里让损失增长最快的方向。反向传播就是利用这个方向,把任务目标逐层投影到中间表示上,逼着隐藏单元编码对当前任务有用的信息。下面用一个两层全连接网络完整推导一遍,再给出 PyTorch 对照代码。
完整矩阵求导与代码验证
考虑两层全连接网络:h = ReLU(XW₁),ŷ = hW₂,损失 L = ½‖ŷ − y‖²₂ / b(b 为 batch size)。为了聚焦权重梯度对特征的塑造,这里省略了偏置。加 bias 只是在 ∂L/∂W₁、∂L/∂W₂ 里多一项由 δ 在 batch 维求和得到的梯度,不影响核心机制。以下推导严格遵循分母布局,所有梯度 shape 和对应参数一致。
前向传播
X ∈ ℝ^{b×d}, W₁ ∈ ℝ^{d×h} → h_pre = XW₁ ∈ ℝ^{b×h}, h = ReLU(h_pre) ∈ ℝ^{b×h}
W₂ ∈ ℝ^{h×c} → ŷ = hW₂ ∈ ℝ^{b×c}
反向传播
令 δ_out = ∂L/∂ŷ = (ŷ − y)/b ∈ ℝ^{b×c}
∂L/∂W₂ = hᵀ δ_out ∈ ℝ^{h×c}
δ_h = δ_out W₂ᵀ ∈ ℝ^{b×h}
δ_pre = δ_h ⊙ I(h_pre > 0) ∈ ℝ^{b×h}
∂L/∂W₁ = Xᵀ δ_pre ∈ ℝ^{d×h}
推导的关键在于:∂L/∂W₁ 不仅依赖输入 X,还通过 δ_pre 携带了任务目标 y 的信息。正是这一项把输出端的监督信号逆向注入第一层权重,逼着 W₁ 学习对当前任务有用的特征变换。
import torch
# 初始化(固定种子确保可复现)
torch.manual_seed(42)
b, d, h, c = 32, 784, 256, 10
X = torch.randn(b, d)
y = torch.randn(b, c)
W1 = torch.randn(d, h, requires_grad=True)
W2 = torch.randn(h, c, requires_grad=True)
# 前向 + 自动微分
h_pre = X @ W1
h = torch.relu(h_pre)
y_hat = h @ W2
loss = 0.5 * ((y_hat - y) ** 2).mean()
loss.backward()
# 手推梯度(分母布局)
delta_out = (y_hat - y) / b
grad_W2_manual = h.T @ delta_out # (h, c)
delta_h = delta_out @ W2.T # (b, h)
delta_pre = delta_h * (h_pre > 0).float() # (b, h)
grad_W1_manual = X.T @ delta_pre # (d, h)
# 数值校验
assert torch.allclose(W1.grad, grad_W1_manual, atol=1e-5)
assert torch.allclose(W2.grad, grad_W2_manual, atol=1e-5)
print("Gradient check passed!")
这里有个坑:代码里 delta_out 除以 b,是因为 loss 用了 .mean() 而不是 .sum(),这和推导里的 1/b 因子严格对应。差一个 b 倍,特征就学歪了。这是分母布局下 shape 正确但数值错误的典型陷阱。
实战陷阱与特征验证
想象一个负责检测图像左上角边缘的神经元。如果训练标签只关心画面中心的目标,梯度会持续压低该神经元的权重——不是因为数据里没有边缘,而是因为任务不需要它。这就是“梯度塑造特征”最直观的体现:特征的存在与否,取决于它对损失的贡献,而非数据本身的统计特性。
但事情没这么简单。即使推导无误,实践中仍有多个环节会阻断这个过程。首先是 ReLU 死区:若 W₁ 初始化不当或学习率过大,h_pre 长期为负,δ_pre 恒为零,∂L/∂W₁ 失去任务信号。在标准 SGD 下,实践中通常视为失效,复活需要额外的扰动机制。其次是对称初始化灾难:若同一层内所有神经元权重初始值完全相同,且后续层未打破对称性,所有神经元就会接收相同梯度,特征空间直接坍缩成单一方向。另外,loss 用 mean 还是 sum,代码里必须和推导一致,否则梯度幅值错误,间接扭曲特征学习的尺度。
验证特征是否真由任务驱动,不能只看训练损失下降。一个基础的 sanity check 是随机标签对照实验:用相同架构和数据,但把 y 换成随机噪声训练。如果中间层激活模式和真实任务显著不同(比如用线性 probing 分类器检测其对原始输入的预测能力,注意要冻结主干只训练探针),就说明任务梯度确实影响了特征学习。但这只能证明任务相关性,至于影响得好不好,还得看下游任务表现。更进一步,可以监控 W₁ 的奇异值谱演化:任务相关的特征学习通常会让奇异值从均匀分布逐渐变得有结构,向少数主导方向集中。这些手段能把抽象的“梯度塑造”变成可观测的实证指标。
总结与延伸
这意味着,神经网络学到的特征,本质是损失函数、数据分布和网络先验共同定义的任务驱动的压缩表示。它既不是数据的内在结构,也不是脱离任务的通用知识。预训练模型的通用性,目前学界仍有争论,较为主流的看法是部分源于 pretext task 梯度在大规模数据上的累积,同时也受数据多样性、架构假设及优化过程本身带来的正则效果影响。当 downstream task 提供足够强的监督信号时,这些特征仍会被重新校准。特征的“好坏”永远相对于任务而言,脱离优化目标讨论特征质量没有意义。
不过,这个框架的有效性依赖于梯度信号的可靠性。在标签噪声、域偏移或极端类别不平衡等场景下,梯度可能塑造出看似合理实则误导的特征,这时候光靠改架构很难根治。理解梯度如何塑造特征,不光是为了用好现有模型,更是为了识别它的失效边界。
如果想继续深入,可以关注这几个方向:对比损失、三元组损失等非交叉熵目标怎么通过不同的梯度结构引导特征解耦;注意力机制里 query/key/value 的梯度流和全连接层有什么不同,以及对长程依赖建模的影响;低秩或稀疏约束下,梯度信号还能不能完整传递任务信息。这些方向如果做出来,我们就不只是调参,而是能主动设计特征空间的结构。