ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

双层神经网络矩阵乘法与反向传播维度详解

双层神经网络矩阵乘法与反向传播维度详解 我一直觉得深度学习入门阶段最难的部分不是“会跑代码”而是“知道代码里的矩阵乘法到底在干什么”。之前我把一个双层神经网络在 MNIST 上跑到了98%的准确率代码写得挺顺但朋友问我“W1 为什么是 784×128 而不是 128×784转置位置换一下又会怎样”时我忽然发现自己并不能把每一步矩阵运算的含义讲清楚。这篇记录就是想把这个基础问题彻底拆开双层神经网络里的矩阵运算是什么形状、为什么是这些形状、每个乘法代表什么语义以及反向传播时梯度矩阵是怎么跟着这些形状一步步传回去的。适合已经写过一点神经网络代码、但面对“维度推导”还会犯迷糊的读者。1. 双层神经网络里矩阵到底在算什么1.1 先约定一套贯穿全文的维度符号讨论双层神经网络之前先统一符号。这里说的双层是指“一个隐藏层 一个输出层”的标准 MLP数据流是X → h XW1 b1 → a activation(h) → s aW2 b2 → loss我会用下面这组形状贯穿全文X形状 (N, D)N 是样本数D 是每个样本的特征维度也就是输入维度。W1形状 (D, H)把 D 维输入映射到 H 维隐藏层。b1形状 (H,)每个隐藏神经元一个偏置。h形状 (N, H)隐藏层的线性加权结果。a形状 (N, H)激活函数作用后的隐藏层输出。W2形状 (H, C)C 是输出类别数比如手写数字就是 10。b2形状 (C,)。s形状 (N, C)也叫 logits是网络最后的原始得分。这套约定是线性代数教材里最常用的写法样本矩阵 X 在前权重矩阵 W 在后。为什么这样约定下一节细说。1.2 为什么数据在前、权重在后而不是反过来新手最容易卡住的问题就是XW1 为什么能成立反过来为什么不行。矩阵乘法的合法性只有一个硬性条件左矩阵的列数必须等于右矩阵的行数。X 的形状是 (N, D)W1 的形状是 (D, H)中间那个 D 正好等于 D所以乘积结果是 (N, H)。这个“中间对齐”的 D 不是巧合它就是每个样本的特征维度也是每个隐藏神经元的输入连接数。把一行样本1, D和 W1 的一列权重D, 1做内积得到的就是第 n 个样本在第 h 个隐藏神经元上的加权和。这个解释在 3.1 节还会再展开。这里要特别提醒一个容易混淆的点PyTorch 的 nn.Linear 层里weight 的形状实际上不是 (D, H)而是 (H, D)也就是 out_features 在前in_features 在后。框架里前向计算是 X weight.T bias。我在学习早期就被这个差异坑过纸上推导用 XW看框架代码却是 XW.T两边经常对不上。我的建议是自己推导时坚持 XW 这套线性代数写法看框架源码时记得把 weight 转置回来两套约定不要混着写。1.3 一行一列背后样本与特征的组织方式理解矩阵运算前提是先理解矩阵每一行每一列代表什么。X 的每一行是一个样本每一列是一个特征维度。比如某一行是 [0.2, 0.5, 0.1, 0.9]意思是这个样本在 4 个特征上的取值。W1 的第 h 列是第 h 个隐藏神经元的全部权重这一列有 D 个数分别对应 D 个输入特征的权重系数。h[n, h] 这个元素的意思就是第 n 个样本的所有特征值按权重 w1[:, h] 做加权求和再加偏置 b1[h]得到第 n 个样本在第 h 个隐藏神经元上的线性响应。一旦把这个“一行是一个样本、一列是一组权重或一组特征”的图像建立起来后面所有梯度公式的维度都不需要死记硬背只要盯住“每个参数的形状必须和梯度形状一致”这一条就能自动检查对错。2. 矩阵乘法的三重身份累加、空间变换、特征提取矩阵乘法在双层神经网络里不是一种孤立操作它至少有三种理解方式。我建议初学者三种都掌握因为不同问题下最好用的视角不同。2.1 加权求和视角每个神经元就是在给特征打分第一种视角最朴素矩阵乘法本质上包含了一大批内积运算。一个隐藏神经元做的事情就是给输入特征做加权求和。举个例子假设一个神经元的权重是 [0.2, -0.5, 0.8]输入样本是 [0.4, 0.1, 0.6]那么这个神经元的输出忽略偏置就是0.2 × 0.4 (-0.5) × 0.1 0.8 × 0.6 0.51这很像一个线性打分器收入高给正权重负债高给负权重加起来判断一个人信用好不好。神经网络训练的本质就是不断调整这些权重让打分结果越来越符合任务需求。W1 的每一列都是一套独立的打分标准H 个隐藏神经元就是 H 种不同的打分视角。2.2 线性变换视角向量经过矩阵乘法会被旋转和拉伸第二种视角更几何X W1 是把每个 D 维向量线性映射到 H 维空间。说得具体一点一个矩阵乘法对向量施加的变换可以拆解为旋转、缩放和投影的组合。举一个二维例子矩阵 [[0, -1], [1, 0]] 乘以任意二维向量相当于把这个向量逆时针旋转 90 度矩阵 [[2, 0], [0, 0.5]] 则是把 x 方向拉伸 2 倍、y 方向压缩一半。神经网络里的 W1 也是一个线性变换只不过维度更大而且矩阵里的数字不是人设计的是训练过程中让梯度一点点调整出来的。训练结束后W1 代表的线性变换恰好把输入数据映射到一种“更好分类”的形态。这个视角对理解深层网络尤其重要第 6 章还会回来展开。2.3 特征提取视角第一层矩阵做特征重组第二层矩阵做决策第三种视角是前两种的升华。只盯着“累加”或“旋转”都还不够。在双层神经网络这个具体场景里W1 的语义更像“特征重组”原始特征往往是杂乱、冗余、不直接可分的W1 通过线性组合把它们重组成 H 个中间特征。这 H 个特征在训练中会自动朝着“对最终任务最有利”的方向调整。而 W2 是在这个新特征空间里做最终决策相当于一个线性分类器。为什么需要这样做因为原始空间的线性边界往往不够用。比如一维数据 [0.3, 12.7] 你很难用一个点把它们完美分开但如果经过第一层变换后它们在隐藏空间里分布得更分散、更有规律第二层做线性划分就容易多了。所以第一层矩阵负责“把数据变好分”第二层矩阵负责“分类”这就是两者语义分工的实质。3. 前向传播的逐层拆解h XW1 b1 的计算过程理论说得再多不如实际算一遍。这一节用一个微型网络把前向传播的每一步落到具体数字上。3.1 XW1一批样本如何同时跑完隐藏层所有神经元假设输入有 2 个样本每个样本 3 个特征也就是 X 的形状是 (2, 3)隐藏层有 4 个神经元那么 W1 的形状就是 (3, 4)。具体数值如下X形状 2×3样本特征1特征2特征3样本1123样本2456W1形状 3×4隐藏单元1隐藏单元2隐藏单元3隐藏单元4输入特征10.1-0.20.30.4输入特征20.50.6-0.70.8输入特征3-0.91.00.2-0.3b1 是 (4,) 的向量 [0.1, -0.1, 0.2, -0.2]。X W1 的每一行都是“该样本分别和 W1 每一列做内积”再加上 b1 之后得到的 h 是样本隐藏单元1隐藏单元2隐藏单元3隐藏单元4样本1-1.53.9-0.30.9样本2-2.48.1-0.93.6比如样本1在隐藏单元2上的计算就是1×(-0.2) 2×0.6 3×1.0 (-0.1) 3.9。这个数字的含义是样本1在第二号隐藏神经元上的线性加权得分。细节之处在于矩阵乘法同时完成了 N×H 次内积运算而不是在代码里写 for 循环。这也是 GPU 加速的基础一次矩阵乘把一批样本的所有隐藏单元计算全部做完了。3.2 偏置 b 为什么能广播维度不同也能相加的秘密很多人在手写代码时会遇到一个疑问h 的中间结果是 (N, H)b1 是 (H,)形状不一样为什么能直接相加答案不是“h 自动变成了 b1 的形状”而是 NumPy 等计算库里的广播机制维度为 (N, H) 的矩阵加一个维度为 (H,) 的向量相当于把 b1 复制到每一行逐行相加。每个隐藏神经元有自己独立的偏置但所有样本共享同一组偏置。偏置的作用是平移。如果没有 b1那么当输入特征全为 0 的时候神经元的输出恒为 0。有了偏置神经元可以在输入为 0 时也保持一个非零基线。你可以把偏置理解为“这个神经元的默认倾向”正值表示在没有信息时倾向于激活负值表示倾向于抑制。3.3 激活函数矩阵之后的那一步为什么不能省矩阵乘法加偏置永远都是线性变换。无论多少个线性变换叠加最终等价于一个大的线性变换。想要表达非线性关系必须在层与层之间加入激活函数。常用的 ReLU 函数是 max(0, x)对 h 逐元素操作。上面例子中的 h 经过 ReLU 后得到 a样本隐藏单元1隐藏单元2隐藏单元3隐藏单元4样本103.900.9样本208.103.6可以看到h 里为负的值全部被清零为正的值保持不变。这一步打断了一层层矩阵连乘的线性叠加让网络有能力拟合非线性函数。从计算图的角度看激活函数是逐元素操作不改变矩阵形状。它的梯度也很简单ReLU 的导数是 0输入为负或 1输入为正输入为 0 的位置按 0 处理就好。这个简单性也是它被广泛使用的原因之一。3.4 一个小数值例子把公式落到具体数据上继续上面的例子。a 的形状是 (2, 4)也就是 2 个样本、4 个隐藏特征。第二层权重 W2 的形状应该是 (4, 2)把隐藏特征映射到 2 个输出类别。假设W2形状 4×2类别1类别2隐藏单元10.5-1.0隐藏单元20.70.2隐藏单元3-0.40.6隐藏单元41.00.3b2 [0.0, -0.5]。那么 s a W2 b2形状是 (2, 2)样本类别1得分类别2得分样本13.630.55样本29.272.20以样本1为例类别1得分 0×0.5 3.9×0.7 0×(-0.4) 0.9×1.0 0 3.63。看到没激活后清零的单元直接不参与第二层计算而隐藏单元2和4贡献了主要信息。这个微型例子走完前向传播的形状变化是X(2,3) → W1(3,4) → h(2,4) → a(2,4) → W2(4,2) → s(2,2)。每一层的维度都严丝合缝任何一步形状对不上整个网络都走不通。4. 反向传播中梯度矩阵的维度推导这一块最容易把转置搞反前向传播理解后反向传播是双层神经网络真正的难点也是矩阵运算含义最深的环节。大部分人在这里第一次被“为什么梯度公式里有转置”折磨。4.1 反向传播的起点损失函数对 s 的梯度回归到只有一层的情况。假设输出层做了 softmax并使用交叉熵损失。可以证明在 softmax 交叉熵的组合下损失 L 对输出得分 s 的梯度有一个极其简洁的形式ds s_softmax - y_onehot这里 s_softmax 是 (N, C) 的概率矩阵y_onehot 是 (N, C) 的独热标签矩阵。ds 的形状也是 (N, C)。如果你现在没用到分类任务把它当做一个抽象的“损失对 logits 的梯度”即可形状就是 (N, C)。反向传播要做的事情很简单把 dL/ds 这个梯度沿着计算图一层一层“传回去”依次算出 dW2、db2、da、dh、dW1、db1。4.2 核心模板y XW b 的三个梯度长什么样在继续推导之前我先给一个可以反复使用的模板这是整个反向传播的骨架假设 y X W b其中X 形状 (N, K)W 形状 (K, M)b 形状 (M,)y 形状 (N, M)已知损失 L 对 y 的梯度 dL/dy形状 (N, M)。那么dL/dW X.T (dL/dy)形状 (K, M)dL/dX (dL/dy) W.T形状 (N, K)dL/db 对 dL/dy 按行求和形状 (M,)这个模板可以套用到网络中任何一层线性变换。需要理解的是它为什么长这样尤其是第一行公式的由来。4.3 为什么是 X.T dL/dh 而不是 dL/dh X.T这一节是全文最该反复看的部分。先推导 y XW b 的一个元素。y[n, m] sum_k X[n, k] * W[k, m] b[m]。根据链式法则L 对某个权重 W[k, m] 的偏导是dL/dW[k, m] sum_n (dL/dy[n, m]) * (dy[n, m] / dW[k, m])对 y[n, m] 这个式子来说它对 W[k, m] 的偏导其实就是 X[n, k]因为只有这一项包含 W[k, m]且系数是 X[n, k]。所以dL/dW[k, m] sum_n X[n, k] * dL/dy[n, m]这个式子的形状是 (K, M)。现在看 X.T 的形状是 (K, N)dL/dy 的形状是 (N, M)两者相乘正好是 (K, M)而且乘法定义就是结果矩阵的 (k, m) 元素等于 X.T 的第 k 行也就是 X 的第 k 列和 dL/dy 的第 m 列做内积这恰好就是 sum_n X[n, k] * dL/dy[n, m]。所以 X.T dL/dy 不是靠死记的它就是“按求和公式写出来的矩阵形式”。如果反过来写成 dL/dy.T X形状是 (M, N)和 W 本来应该是 (K, M) 的形状对不上语义上就错了。同理dL/dy W.T 的推导是L 对 X[n, k] 的梯度是 sum_m W[k, m] * dL/dy[n, m]写成矩阵形式就是 dL/dyN, M乘以 W.TM, K得到 (N, K)。这地方的顿悟点是转置不是随便加的转置存在的唯一目的是把求和维度和被求导维度对齐让最终梯度形状和参数形状一致。记住这句话反向传播的维度推导就再也不会错了。4.4 逐层反向从 s 一路求到 W1有了模板双层网络的反向传播就是按顺序套模板加链式法则。已知 ds形状 N×CdW2 a.T ds形状 (H, C)和 W2 对齐。db2 ds.sum(axis0)形状 (C,)和 b2 对齐。da ds W2.T形状 (N, H)这是梯度穿过线性层回流到激活函数的输出。dh da * (h 0)这是 ReLU 的逐元素梯度传导把大于 0 的位置保留梯度小于等于 0 的位置置零。dW1 X.T dh形状 (D, H)和 W1 对齐。db1 dh.sum(axis0)形状 (H,)和 b1 对齐。整个链条的形状变化如下步骤表达式输入形状输出形状1ds(N, C)(N, C)2a.T ds(H, N) (N, C)(H, C)3ds.sum(axis0)(N, C)(C,)4ds W2.T(N, C) (C, H)(N, H)5da * (h 0)(N, H) 逐元素(N, H)6X.T dh(D, N) (N, H)(D, H)7dh.sum(axis0)(N, H)(H,)每步的输出形状都能和下个步骤需要的输入对上这就是反向传播的“维度护城河”。4.5 一个 30 行代码验证整个链路以下是一个完整的双层网络前向和反向的 NumPy 核心代码不包含训练循环但足够直观import numpy as np np.random.seed(0) N, D, H, C 8, 4, 5, 3 X np.random.randn(N, D) y np.random.randint(0, C, size(N,)) y_onehot np.eye(C)[y] W1 np.random.randn(D, H) * 0.01 b1 np.zeros(H) W2 np.random.randn(H, C) * 0.01 b2 np.zeros(C) # 前向 h X W1 b1 a np.maximum(0, h) s a W2 b2 # softmax 交叉熵 exp_s np.exp(s - s.max(axis1, keepdimsTrue)) probs exp_s / exp_s.sum(axis1, keepdimsTrue) loss -np.mean(np.sum(y_onehot * np.log(probs 1e-8), axis1)) # 反向 ds (probs - y_onehot) / N dW2 a.T ds db2 ds.sum(axis0) da ds W2.T dh da * (h 0) dW1 X.T dh db1 dh.sum(axis0) # 形状自检 assert dW1.shape W1.shape, fdW1 {dW1.shape} ! W1 {W1.shape} assert dW2.shape W2.shape, fdW2 {dW2.shape} ! W2 {W2.shape} assert db1.shape b1.shape assert db2.shape b2.shape这段代码如果跑通说明链路的形状理解已经到位了。5. 矩阵形状错了会怎样从报错信息到 debug 思路理论推导正确不代表实践不会犯错。我在这部分踩过的坑每一个都值得单独记录。5.1 最常见错误把梯度公式里的转置放错位置一次实验中我把 dW1 写成了 dW1 dh.T X而不是 X.T dh。乍一看没问题X 形状是 (N, D)dh 形状是 (N, H)那么 dh.T X 的形状是 (H, D)。dW1 应该是 (D, H) 没错但 (H, D) 也是合法形状程序不会报错训练也能跑loss 也会下降。问题是反向传播的数学方向完全错了权重更新算是在按错误梯度走最终验证准确率一直上不去卡在 70% 左右。当时我查了很久最后逐行打印梯度形状才发现问题。这个案例的教训是“形状合法”只是必要条件不是充分条件。梯度的形状可以和参数对齐但数值可能完全错误。所以写反向传播时不能只满足于不报错最好再用数值梯度验一次。5.2 偏置梯度直接用了 ds 而不是 sum另一个很常见的错误是我早期写 W2 的偏置梯度时直接用 db2 ds因为觉得“ds 就是损失对 s 的梯度s 里包含 b2”。但 b2 的形状是 (C,)而 ds 的形状是 (N, C)。一个偏置被 N 个样本共享反向传播时损失对它的梯度理应是所有样本的梯度之和也就是 axis0 上的求和。如果直接把 ds 当作 db2接下来用这个错误形状去做参数更新要么触发广播导致更新逻辑混乱要么形状不匹配直接报错。只要意识到广播机制的存在这个错误就很容易避免b2 在前向中广播到了 N 行那么在反向中就必须把 N 个梯度汇总回一行。5.3 混淆了公式里的 W 和框架里的 weight还有一个几乎每个迁移到 PyTorch 的人都会碰到的问题线性层的 weight 参数形状是 (out_features, in_features)。我在一个项目里直接套公式写 X W1结果发现输入 (N, 128) 和 weight (784, 128) 乘不起来因为 X 和 weight 的内维对不上。后来才发现 PyTorch 的前向公式是 X W1.T。这里我给新手的建议是所有用框架实现的模块先去源码里确认 weight 的存放顺序。一个 shape 断言就能省下两个小时。5.4 五个步骤的 debug 检查清单我在踩过足够多坑以后整理了一套自己的检查流程每次手写网络都会过一遍前向传播从 X 开始把每一层的输入输出形状写进注释形状不符合预期就立刻停止。反向传播中的每个梯度变量先写一行 assert grad.shape param.shape确保梯度和参数形状对齐。训练前用一个固定随机种子跑一个 mini-batch计算一次数值梯度和解析梯度对比。数值梯度实现很简单对参数加一个小扰动 epsilon计算 loss 差值的近似梯度两者应该非常接近。确认公式中的 W 和框架权重的转置关系。凡是看到 weight.shape 为 (out, in) 的自动在心中翻译成公式里的 W.T。训练过程中如果 loss 出现明显异常先检查前向最后几层的数值范围比如 softmax 之前是否溢出再检查反向梯度是否出现 NaN。这套流程帮我抓住了至少五次“看起来能跑但实际上错了”的 bug比盲目调参有用得多。6. 从两层到更深网络这套矩阵视角为什么能继续用理解两层网络之后你会发现更深的网络并没有引入新的矩阵运算种类只是把同样的运算重复了更多次。6.1 深层网络只是不断重复“线性 非线性”一个 L 层网络的第 l 层公式永远是z_l a_{l-1} W_l b_l a_l f(z_l)其中 a_0 就是输入 X。这和双层网络的前向公式完全一样唯一的区别是 a_{l-1} 不再是原始输入而是上一层学习到的特征表示。反向传播也一样。第 l 层的梯度计算依然是dW_l a_{l-1}.T dz_l da_{l-1} dz_l W_l.T所以两层网络是理解一切深层结构的基石这也是我在学习时先死磕两层的原因。很多人一上来就搭 ResNet、Transformer遇到梯度消失就束手无策其实就是没弄清楚最基础的矩阵回传链条。6.2 隐藏层的几何意义数据点云的变形第 2 章提到过的“线性变换视角”在深层网络中更值得反复体会。想象 X 的 N 行是 N 个数据点它们散布在高维空间里。第一层矩阵乘法是对这些点做一次旋转、缩放、投影激活函数再把一部分点“按到”坐标轴上相当于施加了一个非线性折叠。每一次“矩阵乘法 激活”都会让数据点的分布发生一次形变。训练的目标就是让最后一层输出分布变得“好分”。经典例子是 XOR 问题四个点 (0,0)、(1,1)、(0,1)、(1,0)类别分别是 0、0、1、1在二维空间里无法用一条直线分开。但经过一个带有两个隐藏神经元的隐藏层和 ReLU 之后这四个点可以被映射到一个新的二维空间在这个空间里线性可分。你完全可以用两层矩阵运算在纸上推一遍这个过程推完会对“隐藏层在干什么”有一种完全不同的理解。6.3 矩阵维度在高级结构里依然是核心思想到了 Transformer 时代核心的 self-attention 计算依然是一系列矩阵乘法Q K.T 计算查询和键的相似度经过 softmax 之后再 V 进行加权求和。这里的每个乘法背后依然遵循“左矩阵的每一行是一组查询右矩阵的每一列是一组键或一组值”的解读方式。所以不要觉得“矩阵运算含义”是入门话题不值得深究。维度背后站着的永远是语义一个维度是样本数一个维度是特征数一个维度是类别数一个维度是序列长度。当你把每个矩阵乘法的维度都翻译成一句话调试深度模型就不会再靠猜了。我现在写代码的习惯是每写一句矩阵乘法先默念三个东西——左矩阵的行是什么左矩阵的列是什么右矩阵的列是什么。把这三个问题想清楚形状就永远不会错梯度推导也更像是套模板而不是做玄学。如果你正在被两层网络的转置和维度折磨先别急着往下学更花哨的模型把 XW1、ReLU、XW2 这条链的每一步用手算一遍、用 NumPy 验证一遍。这个根基打牢了后面所有深度网络对你来说都只是“同样的矩阵运算换了个名词而已”。
返回列表