news 2026/10/19 8:53:22

反向传播核心:dX = G Wᵀ 推导与LLM训练FLOPs估算

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
反向传播核心:dX = G Wᵀ 推导与LLM训练FLOPs估算

我最近在整理一份关于LLM训练中FLOPs计算的笔记,前阵子发完第一篇,群里就有朋友来问:你直接给了全连接层前向的计算量公式,但真要自己估算Transformer每一层的计算量,还是容易卡壳,尤其是反向传播里的dX、dW到底怎么流动,完全看不懂。说实话,我自己当初也在这个点上绕了很久。今天就把这个最容易被忽略、却又贯穿整个训练过程的知识点掰开揉碎:反向传播中的dX到底代表什么,它怎么从最后一层沿着计算图往回传,以及标题里这个公式 dX = ∂L/∂X = ∂L/∂Y · ∂Y/∂X = GWᵀ 是怎么来的。这篇文章适合正在学LLM训练基础、想自己手推梯度、或者想搞清楚训练FLOPs估算的朋友,看完之后你至少能独立写出一个线性层的反向传播公式,并且知道它在整个训练里占了多少算力。

1. 先搞清楚dX是什么:反向传播里的“快递员”

1.1 为什么算FLOPs要先聊反向传播

我们平时说训练一个LLM要多少算力,一般都用FLOPs(浮点运算次数)来衡量。但很多教程直接给结论:训练总计算量大约是前向传播的3倍,或者反向传播的计算量是前向的2倍。这个“2倍”是从哪来的?如果你不知道反向传播里到底算了哪些矩阵乘法,这个数字对你来说就是空中楼阁,换个模型结构就不会估了。

实际训练过程分两步。前向传播把输入X经过一层层矩阵运算变成输出,最后算损失L;反向传播则从损失L出发,逐层把梯度传回每一层的参数(权重W和偏置b),然后更新这些参数。关键在于,反向传播不是凭空出现的,它要基于前向传播的中间结果做新的矩阵运算。

以最简单的线性层 Y = XW + b 为例,前向只需要一次矩阵乘法 XW。反向传播就要算两个梯度:dX 和 dW,它们各需要一次矩阵乘法。所以单看这个线性层,反向的计算量就是前向的2倍,加上前向自己,总共是3倍。这就是“训练是前向3倍”说法的来源。Transformer就是一堆矩阵乘法叠起来的,理解了这个,整个训练FLOPs估算的底子也就打牢了。

1.2 dX在计算图里的定位

dX严格写出来是 ∂L/∂X,表示损失L对当前层输入X的偏导。它回答的问题很具体:如果X里的某个元素稍微变大一丁点,最终的损失L会变大还是变小、变化量大概是多少。注意,这里X不是模型参数,它是上一层输出的结果。

那为什么需要它?因为当前层算出来的dX,要当作上游梯度传给前一层。前一层在更新自己的权重时,需要知道“我的输出对损失有多敏感”,而当前层的dX恰好在说这件事:前一层把输出交给当前层,当前层把这个输出对损失的敏感度算出来,然后递回去。所以dX在多层网络里承担的是中转站的职责。

可以类比成快递派送。损失L是最终收货人,每一层是一个中转站。前向传播是“包裹”(数据X)从第一站送到最后一站;反向传播是“签收信息”(梯度)从最后一站往回传。dX就是当前层递给上一层的信息包,上一层的权重梯度dW则依赖这个信息包才能计算出来。没有这个“信息包”,前面的层根本不知道自己该往哪个方向更新。

2. 手推核心公式:dX = ∂L/∂X = GWᵀ

2.1 先统一符号和形状

很多人在公式推导上摔跤,是因为不同教程的符号约定不一样,矩阵乘法方向也不同。我这里统一用更常见的约定:

  • 输入 X 形状为 (N, D_in),N是batch size,D_in是输入维度。
  • 权重 W 形状为 (D_in, D_out)。
  • 偏置 b 形状为 (D_out,)。
  • 输出 Y 形状为 (N, D_out),且 Y = XW + b。
  • G = ∂L/∂Y,形状与Y一致,也是 (N, D_out)。

注意,这里的G通常被称为上游梯度。在PyTorch这类框架里,反向传播时每个张量对象上有一个grad字段,那个就是G。很多新手一上来就找dW,却忽略了这个G的存在,但G恰恰是连接损失和每一层的枢纽。

2.2 链式法则拆解:从L到X的路径

从X到L的路径可以写成:X → Y → L。链式法则告诉我们:

dX = ∂L/∂X = ∂L/∂Y · ∂Y/∂X = G · ∂Y/∂X

问题是,∂Y/∂X不是一个简单的矩阵。Y的每个元素都是X所有元素的函数,所以完整的∂Y/∂X是一个四维张量。理论推导很严谨,但对初学者不友好。我习惯从带下标的分量形式去理解,反而直观很多。

假设我们要算 dX 的第 i 行第 k 列元素,也就是 ∂L/∂X_{i,k}。L通过所有 Y_{i,j} 依赖 X_{i,k},于是:

∂L/∂X_{i,k} = Σ_j ∂L/∂Y_{i,j} · ∂Y_{i,j}/∂X_{i,k} = Σ_j G_{i,j} · ∂Y_{i,j}/∂X_{i,k}

关键在于 ∂Y_{i,j}/∂X_{i,k} 等于多少。从 Y_{i,j} = Σ_m X_{i,m} W_{m,j} 可以看出,只有当 m = k 时,导数才非零,且等于 W_{k,j}。所以:

dX_{i,k} = Σ_j G_{i,j} · W_{k,j}

这一步非常关键。顺着这个下标写下去,(i, k)位置的dX等于对j求和G_{i,j}乘W_{k,j}。这不就是矩阵 G 乘以 W 的转置吗?矩阵乘法要求“左矩阵的列数等于右矩阵的行数”,G 的列数是D_out,W的列数也是D_out,所以要让G能乘上W的转置,举起来就是:

dX = G Wᵀ

整个推导没有绕弯,也没有魔法,就是把偏导定义写开,然后用一次求和浓缩成矩阵乘法。

2.3 为什么是“乘W的转置”而不是“乘W”

这是初学者最常见的困惑。我遇到不止一个朋友问:前向明明是 XW,反向为什么不是 G 除以 W 之类的,或者直接乘 W?

逻辑上可以这么理解:前向传播是把输入从 D_in 维空间映射到 D_out 维空间。反向传播要把梯度从 D_out 维空间传回 D_in 维空间,所以维度必须反过来。G 的形状是 (N, D_out),我们希望最终 dX 的形状是 (N, D_in),因此中间需要乘一个能把 D_out 变成 D_in 的矩阵。W 是 (D_in, D_out),把它转置成 (D_out, D_in),恰好满足: (N, D_out) @ (D_out, D_in) = (N, D_in)。

再看分量形式更有感觉。拿单个输出举例子:如果只有 y1 = x1 w1 + x2 w2,那么损失L对 x1 的梯度是 ∂L/∂y1 · w1,对 x2 的梯度是 ∂L/∂y1 · w2。可以看到,输入位置的梯度等于上游梯度乘上“连接该输入和该输出的权重”。如果有多个输出,就得把所有经过该输入的路径都加起来。转置的作用,就是把这一系列“加总”组织成整齐的矩阵运算。

记住一句大白话:前向是“X 右乘 W”,反向就是“G 右乘 Wᵀ”。只要你在代码里看到线性层 Y = XW + b,那么对应反向传播的 dX 一定长这样。这也会帮你后面省掉大量排查时间。

3. dX 如何和 dW、db 一起协作

3.1 三个梯度的角色

一个线性层在反向传播时,其实要算三个量:

  • dX = G Wᵀ,形状 (N, D_in),传给前一层当作上游梯度。
  • dW = Xᵀ G,形状 (D_in, D_out),用来更新当前层的权重。
  • db = Σ_i G_{i,:},形状 (D_out,),用来更新偏置。

这三者一视同仁,但作用完全不同。dX不参与当前层的参数更新,它是给上一层的“消息”;dW和db才是当前层真正用来调整自己的量。群里有人问:既然dW才是用来更新参数的,那我直接算dW不行吗?不行,因为上一层的dW依赖于我们传过去的dX。没有dX,上一层就断粮了。

我建议初次接触时把这三行公式并排写在一起对照记忆,顺手把形状也标上去。一旦习惯用形状做自检,就不容易写错了。

变量公式形状用途
dXG Wᵀ(N, D_in)传给前一层
dWXᵀ G(D_in, D_out)更新当前层权重
dbΣ_i G_{i,:}(D_out,)更新偏置

3.2 多层网络里的接力赛

假设一个只有两层的网络:输入X,经过第一层权重W1得到Y1,再经过第二层权重W2得到Y2,损失L由Y2计算。

第二层(最后一层)反向传播时,直接用从损失函数那里拿到的 G2 = ∂L/∂Y2,然后算出:

  • dX2 = G2 W2ᵀ
  • dW2 = Y1ᵀ G2

重点来了:因为第一层的输出Y1就是第二层的输入X,所以“第二层的dX”在数学上就等于“第一层的上游梯度G1”。也就是说,G1 = dX2。

接下来第一层反向传播时,用这个G1继续算:

  • dX1 = G1 W1ᵀ
  • dW1 = Xᵀ G1

于是我们看到一条清晰的接力链:最后一层的dX变成上一层的G,依此类推,一层接一层传回最前面。标题里写的“dX代表的是传给前一层来计算dW的上游梯度”,说的正是这件事。所以不要小看dX,它虽然不直接更新参数,却是整个梯度传递回路里最重要的“传令兵”。

4. 用一个小例子把公式落到数值上

4.1 构造一个具体的线性层

光看公式还是有点抽象,我习惯每次推导完都拿一组假数据验算一遍。现在构造一个最简单的例子:batch size N=2,输入维度 Din=3,输出维度 Dout=2。

假设输入 X:

[[1, 2, 3], [4, 5, 6]]

权重 W:

[[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]]

偏置 b 先设成全零,方便展示核心过程。前向传播 Y = XW,手算第一行:

Y[0,0] = 1×0.1 + 2×0.3 + 3×0.5 = 0.1 + 0.6 + 1.5 = 2.2
Y[0,1] = 1×0.2 + 2×0.4 + 3×0.6 = 0.2 + 0.8 + 1.8 = 2.8

第二行同理:Y[1,0]=4×0.1+5×0.3+6×0.5=0.4+1.5+3.0=4.9,Y[1,1]=0.8+2.0+3.6=6.4。得到Y整体是:

[[2.2, 2.8], [4.9, 6.4]]

4.2 反推dX和dW

现在假装从损失函数那边拿到的上游梯度G是:

G = [[0.1, -0.2], [0.3, 0.4]]

这个G代表损失对Y每个元素的敏感度。接下来用公式算dX = G Wᵀ。先把W转置:

Wᵀ = [[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]]

然后:

dX第一行第一列 = 0.1×0.1 + (-0.2)×0.2 = 0.01 - 0.04 = -0.03
dX第一行第二列 = 0.1×0.3 + (-0.2)×0.4 = 0.03 - 0.08 = -0.05
dX第一行第三列 = 0.1×0.5 + (-0.2)×0.6 = 0.05 - 0.12 = -0.07

第二行依葫芦画瓢:0.3×0.1+0.4×0.2=0.03+0.08=0.11,0.3×0.3+0.4×0.4=0.09+0.16=0.25,0.3×0.5+0.4×0.6=0.15+0.24=0.39。得到:

dX = [[-0.03, -0.05, -0.07], [0.11, 0.25, 0.39]]

这个dX的形状是(2, 3),恰好和X一模一样。这就是一个很好的自检信号:如果算出来不是这个形状,说明中间某一步写错了。

再看dW = XᵀG。Xᵀ形状是(3,2),G形状是(2,2),乘出来是(3,2),正好和W同形状。手算其中一两个元素:

dW[0,0] = Xᵀ的第0行(即X的第0列:[1,4])点乘G的第0列([0.1,0.3])= 1×0.1+4×0.3=0.1+1.2=1.3。
dW[1,1] = Xᵀ的第1行(即X的第1列:[2,5])点乘G的第1列([-0.2,0.4])= -0.4+2.0=1.6。

每一步都是有明确下标的,不是瞎蒙。多算几遍之后,你对GWᵀ和XᵀG这两个公式的记忆会非常牢固,因为它们不再是一串符号,而是有实际意义的运算。

4.3 用代码验证一遍

手算完了,再用代码确认一遍,免得自己粗心算错。以下是一个纯NumPy的验证脚本:

import numpy as np X = np.array([[1., 2., 3.], [4., 5., 6.]]) W = np.array([[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]]) G = np.array([[0.1, -0.2], [0.3, 0.4]]) Y = X @ W dX = G @ W.T dW = X.T @ G print("Y shape:", Y.shape) print("dX:\n", dX) print("dX shape:", dX.shape) print("dW:\n", dW) print("dW shape:", dW.shape)

运行结果里的dX和手算完全一致,形状也正确。真正在框架里写模型时,你未必会手写这个矩阵乘法,因为自动微分帮我们搞定了,但理解底层原理还是很有必要。以后你要是想做算子融合、写自定义层、或者手推FLOPs,都会需要这种肌肉记忆。

5. 从dX看LLM训练里的几个大坑

5.1 维度不匹配:最常见的报错

我见过最多的反向传播报错是矩阵乘法维度对不上。比如有人把dX写成G乘W,结果W形状是(D_in, D_out),G是(N, D_out),一乘就报错。因为 (N, D_out) @ (D_in, D_out) 根本乘不了,除非N恰好等于D_in。

解决这类报错的核心心法只有一个:dX的形状必须等于X的形状。你拿到一个梯度公式,先别管数学细节,直接检查最终形状。如果dX形状是 (N, D_out),而X是 (N, D_in),那这个公式100%有问题。用形状自检,几乎能秒杀一半的反向传播bug。

同理,dW的形状必须等于W的形状。W是 (D_in, D_out),所以dW也必须是 (D_in, D_out)。XᵀG正好满足,因为Xᵀ是(D_in, N),G是(N, D_out)。这一招在跟队友review代码时特别好用,不用逐行看逻辑,先看形状全对,再谈其他。

5.2 搞反左乘还是右乘

另一个高频坑是转置方向写反,或者把左右顺序搞混。原因在于:如果前向约定是Y = WX(输入X在右边,但在某些教材里输入写在左边),那么反向公式就会变成dX = WᵀG,dW = G Xᵀ,和我们这里的结论正好镜像。

所以我在文章开头就强调:必须固定自己的符号约定。只要你的前向写成Y = XW + b,那么反向就永远是:

  • dX = G Wᵀ,G在左,Wᵀ在右
  • dW = Xᵀ G,Xᵀ在左,G在右

如果哪天你在别人的代码里看到dX = Wᵀ G,别急着说别人错了,先看他前向是怎么写的。把约定统一到自己的框架之后,再去理解别人的代码就轻松很多,只需要做个左右镜像而已。

5.3 理解Transformer里的反向传播结构

真正到了LLM训练,网络里不只有线性层,还有多头注意力、LayerNorm、Embedding等。它们各自的dX公式和线性层不太一样,但排列思路完全一致:从上层接住梯度,再算“传给下一层”的梯度。

比如多头注意力里的线性投射层,本质还是线性层,直接套上面的公式没问题。但注意力内部的softmax,它的反向传播就不是简单的矩阵乘转置了,需要把softmax的雅可比矩阵算出来,再做类似链式传播。这一块我打算放到后面讲Attention的FLOPs时再展开。这里先打下一个重要认知:dX的传播形态在不同算子里长得不一样,但角色永远相同,都是从后往前递梯度。

在估算LLM训练的FLOPs时,有一个实用结论:Transformer里矩阵乘法占了绝对大头,而每个线性层反向传播需要算dX和dW,所以反向这部分大致是前向的2倍。如果你细算到每一个张量,还会发现dX和dW各一次矩阵乘法,前向一次,加起来就是3倍。这也解释了为什么训练大模型时,反向传播的计算量如此可观。

6. 几个实战经验与自查技巧

6.1 用“形状守恒”自查公式

每次写完一个反向传播的公式,我第一件事不是看推导过程,而是看形状对不对。这个方法简单到有点蠢,但极其有效。给自己定三条铁律:

  • dX 的形状必须和 X 一致。
  • dW 的形状必须和 W 一致。
  • db 的形状必须和 b 一致。

如果不一致,别想了,必然是转置、左右顺序、或者求和维度错了一个。等以后再接触CNN、RNN、GNN,这套形状守恒法依然通用。我在写自定义算子时,就是这么挨个检查形状,很少出过岔子。

6.2 数值梯度校验:最靠谱的调试手段

公式写对了不代表代码写对了。你要是在实现自定义层的时候,反向传播结果怎么都不对,最推荐的方式是数值梯度校验。原理很简单:利用导数的定义,给某个参数加一个极小的扰动 ε,看损失变化量除以ε,是否约等于你手写的梯度值。

具体做法,选择网络里某一个参数 θ,分别计算:

  • loss(θ + ε),loss(θ - ε)
  • 数值梯度 ≈ (loss(θ+ε) - loss(θ-ε)) / (2ε)

然后和你的解析梯度对比,如果相对误差在 1e-6 左右,说明公式和实现基本正确。这个技巧在实现自定义反向传播的时候可以帮你省下大量时间,我在自己的项目里基本每次都会跑一遍,确保自定义算子的反向传播真的可靠。

实践中还要注意:ε 不能取得太大也不能太小,太小会有浮点舍入误差,太大则会让导数近似失真。一般取 1e-5 到 1e-6 比较合适,而且最好随机抽几个参数检验,别只测一个。

6.3 用dX的范数观察梯度异常

最后分享一个调LLM时的经验:训练刚起步时,可以每步都打印中间层的dX范数。如果某一层的dX突然变成NaN或者剧烈膨胀,往往意味着前向传播有问题,或者上游梯度爆炸了。

dX的行为能反映很多信息。因为它是所有前层梯度的起点,dX如果异常,后面所有层的dW都会受影响,模型参数更新就会乱套。所以在我自己搭训练pipeline时,会在关键层打点观察dX的范数变化,一旦异常能快速定位到是哪一层出的问题。这个方法在debug大型模型时是非常实用的经验,比直接看loss曲线直观得多。

说到底,dX就是在反向传播里“接力传递”的那个梯度。它顶着数学上吓人的偏导符号,本质上只是“损失对输入的敏感度”。把这个东西彻底想明白了,后面再去看Transformer的梯度流、手推FLOPs、甚至做分布式训练里的通信压缩,都会顺畅很多。下一步我准备写Attention层的反向传播梯度链和FLOPs细化,到时候还会再提到这个GWᵀ的套路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 8:52:54

YOLO11融合EfficientVIM:遥感火灾烟雾检测实战

做遥感火灾烟雾检测的人,应该都被同一个坑绊倒过:图像里明明飘着一片浓烟,模型就是检测不出来,却把云、雾、山体阴影挨个框了一遍。我第一次把通用检测器直接扔到无人机俯拍数据上时,mAP50只有不到30%,一开…

作者头像 李华
网站建设 2026/10/19 8:52:39

雾天目标检测:CEM颜色增强模块提升多模态融合mAP

雾天跑目标检测,我相信做过多模态融合的人都体会过那种无力感。晴天测试集上mAP刷到漂亮,一进雾天直接腰斩,尤其小目标几乎是“团灭”。上个月我在某项目里把YOLOv13这个内部多模态版本拉到真实雾天场景做验证,漏检率一度高到让我…

作者头像 李华
网站建设 2026/10/19 8:52:39

open-code-review:代码审查流程、自动化工具与质量提升实践指南

项目标题: "open-code-review"项目正文: open-code-review 是一个关于代码审查(Code Review)的开源项目,强调通过系统化的代码审查流程与自动化工具,提升代码质量、减少线上缺陷、促进团队知识共享。关键词: open-code-…

作者头像 李华
网站建设 2026/10/19 8:52:33

SQL注入变形记:数字型、字符型与搜索型注入原理与防御

SQL注入这个话题,我在不同的安全社区里聊过很多次,每次都能遇到新人问同一个问题:明明学了各种注入技巧,但真拿到一个目标时,还是不知道该怎么下手。多数教程喜欢列一堆payload让人背,却很少讲清楚一个关键…

作者头像 李华
网站建设 2026/10/19 8:52:29

Spark任务报错排查与性能优化:从OOM、Shuffle到数据倾斜的实战指南

这些年调试Spark任务,我听得最多的话就是“代码在本地跑得好好的,一上集群就崩”。屏幕上的英文报错一长串,有的是Timeout,有的是OOM,有的是压根不知道哪里来的Exception。大多数时候,问题不在业务逻辑&…

作者头像 李华
网站建设 2026/10/19 8:50:32

开源代码审查工具open-code-review:设计原理与实战部署

我一直在想一个问题:为什么很多团队明明上了CI、跑了测试、做了自动化扫描,线上还是会出现那些本可以被人类肉眼拦住的低级 bug?后来我想明白了,自动化工具擅长的是“已知问题的重复检查”,而代码审查解决的是“未知问…

作者头像 李华