Backpropagation¶
约 5878 个字 107 行代码 预计阅读时间 31 分钟
简介¶
反向传播是一种通过链式法则递归计算表达式梯度的方法,广泛应用于神经网络的优化。以下是本节内容的提炼:
-
核心问题 : 给定函数 f(x) ,其中 x 是输入数据的向量,目标是计算函数 f 关于 x 的梯度 \(\nabla f(x)\) 。在神经网络中,函数 f 通常对应于损失函数 L ,输入 x 包含训练数据和神经网络的参数(如权重 W 和偏差 b )。计算这些参数的梯度,是优化模型的关键步骤。
-
链式法则与递归计算 :反向传播基于链式法则,通过递归地计算每层输出相对于输入的导数,最终获取损失函数对各层参数的梯度。这一过程将损失函数的整体影响分解到每个参数上,使得神经网络的训练成为可能
梯度与链式法则¶
简单表达式和理解梯度¶
-
函数定义及其偏导数:
-
考虑一个简单的二元乘法函数 f(x, y) = xy 。
- 对 x 和 y 两个变量分别求偏导数,可以得到:
- 对 x 的偏导数为 y 。
- 对 y 的偏导数为 x 。
解释:
- 导数的意义在于:它表示在某个点附近,函数值相对于某个变量的变化率。
-
导数本质上是函数对于变量变化的敏感度。比如,对于 f(x, y) = xy ,若 x = 4 且 y = -3 ,函数值为 -12,而此时 x 的导数为 -3。这意味着,如果 x 稍微增加一些,函数值会减少,并且减少的量是 x 增量的三倍(由于负号)。同理,如果 y 增加一点,函数值也会增加,并且增加的量是 y 增量的四倍。
-
梯度的概念:
-
梯度是由所有偏导数组成的向量。例如对于 f(x, y) = xy :
- 梯度向量为 [y, x] 。
解释:
-
虽然梯度是一个向量,但人们有时会简单地说“x 上的梯度”或“y 上的梯度”来表示对这两个变量的敏感度。
-
最大值操作的导数:
- 对于 f(x, y) = \max(x, y) :
- 若 x 大于或等于 y ,则对 x 的导数为 1,对 y 的导数为 0;
- 若 y 大于或等于 x ,则对 y 的导数为 1,对 x 的导数为 0。
使用链式法则计算复合表达式¶
为了计算复合表达式的梯度,链式法则通过将复杂表达式拆解为多个简单表达式,并逐步计算每个中间变量的梯度,最终将结果组合起来。以下是对这一过程的详细解析:
考虑一个复合函数 f(x, y, z) = (x + y)z 。这个表达式可以分解成两部分:
- q = x + y
- f = qz
对于中间变量 q = x + y :
- \(\frac{\partial q}{\partial x} = 1\)
- \(\frac{\partial q}{\partial y} = 1\)
对于最终函数 f = qz :
- \(\frac{\partial f}{\partial q} = z\)
- \(\frac{\partial f}{\partial z} = q\)
现在我们关心的是 f 相对于原始输入变量 x 、 y 和 z 的梯度,而不是中间变量 q 的梯度。链式法则告诉我们,计算复合函数的梯度时,可以将各部分梯度相乘:
- \(\frac{\partial f}{\partial x} = \frac{\partial f}{\partial q} \cdot \frac{\partial q}{\partial x} = z \cdot 1 = z\)
- \(\frac{\partial f}{\partial y} = \frac{\partial f}{\partial q} \cdot \frac{\partial q}{\partial y} = z \cdot 1 = z\)
- \(\frac{\partial f}{\partial z} = q = x + y\)
反向传播¶
直观理解¶
反向传播是神经网络训练的核心算法,它通过链式法则有效地计算每个参数对损失函数的影响。为了直观理解反向传播,可以将其视为在网络中逐层传播的梯度信号,类似于门单元之间的“消息传递”
。
- 前向传播与局部计算
在前向传播过程中,每个门单元(如加法门、乘法门等)接收输入并计算两个结果:
- 输出值:这是门单元对输入的直接计算结果。
- 局部梯度:这是门单元的输出对其输入的导数。例如,加法门的局部梯度总是+1,因为加法对输入的变化有线性影响。
这些计算都是局部的,不需要考虑网络的整体结构。
- 反向传播中的链式法则
在反向传播过程中,整个网络的输出梯度被逐步传递回每个门单元。每个门单元接收到从下游传递来的梯度,并将其乘以自己的局部梯度,以计算该门单元的输入变量的梯度,从而得到整个网络的输出对该门单元的每个输入值的梯度。
具体示例:加法门收到了输入 [-2, 5],计算输出是 3。既然这个门是加法操作,那么对于两个输入的局部梯度都是+1。网络的其余部分计算出最终值为-12。在反向传播时将递归地使用链式法则,算到加法门(是乘法门的输入)的时候,知道加法门的输出的梯度是-4。如果网络如果想要输出值更高,那么可以认为它会想要加法门的输出更小一点(因为负号),而且还有一个 4 的倍数。继续递归并对梯度使用链式法则,加法门拿到梯度,然后把这个梯度分别乘到每个输入值的局部梯度(就是让-4 乘以 x 和 y 的局部梯度,x 和 y 的局部梯度都是 1,所以最终都是-4)。可以看到得到了想要的效果:如果 x,y 减小(它们的梯度为负),那么加法门的输出值减小,这会让乘法门的输出值增大。
在反向传播过程中,加法门、乘法门和取最大值门单元的行为可以通过直观的方式理解,这有助于调试神经网络并确保梯度的正确传播。
-
加法门单元 无论输入的数值是多少,梯度都会被平等地分配给所有的输入。这是因为加法操作的局部梯度都是简单的+1,因此输出的梯度不变地传递给每一个输入。因为乘以 1.0 保持不变。
上例中,加法门把梯度 2.00 不变且相等地路由给了两个输入。
-
取最大值门单元 取最大值门单元的行为稍微复杂一些。
它根据输入的值选择将梯度传递给哪个输入。
具体来说,它只将梯度传递给前向传播中值最大的那个输入。因为最大值操作的局部梯度对于最大的输入是 1,对于其他输入则是 0,因此只有最大的输入会接收到梯度。上例中,由于 z 的值比 w 大,所以将梯度传给 z,因此 z 的梯度是 2,w 的梯度是 0 -
乘法门单元 乘法门单元的行为最为复杂。其局部梯度为输入的另一个变量值,并且它们相互交换位置后再相乘,然后通过链式法则乘以上游的梯度。因此,
梯度的大小不仅取决于输入的大小,还取决于上游梯度的大小
。在你的例子中,x
的梯度计算为-4.00 * 2.00 = -8.00
。
非直观影响及其结果。注意一种比较特殊的情况,如果乘法门单元的其中一个输入非常小,而另一个输入非常大,那么乘法门的操作将会不是那么直观:它将会把大的梯度分配给小的输入,把小的梯度分配给大的输入。在线性分类器中,权重和输入是进行点积 \(w^Tx_i\),这 d 说明输入数据的大小对于权重梯度的大小有影响。例如,在计算过程中对所有输入数据样本 \(x_i\) 乘以 1000,那么权重的梯度将会增大 1000 倍,这样就必须降低学习率来弥补。
从反向传播的角度看,数据预处理非常重要。未适当预处理的数据会导致梯度的剧烈波动,从而影响模型的训练过程。例如,如果输入数据过大,权重梯度也会过大,导致学习率的调整。这说明,即使微小的数据变化,也可能对训练过程产生深远的影响。
模块化:Sigmoid 例子¶
考虑以下表达式:
这个表达式描述了一个含有输入 \(\mathbf{x}\) 和权重 \(\mathbf{w}\) 的二维神经元,该神经元使用了 Sigmoid 激活函数。在这里可以将这个函数看作是由多个“门”组成的。
使用 Sigmoid 激活函数的二维神经元的例子如下图所示:
输入为 \([x_0, x_1]\),可学习的权重为 \([w_0, w_1, w_2]\)。神经元对输入数据进行点积运算,然后其激活值被 Sigmoid 函数压缩到 0 到 1 之间。
Sigmoid 函数的梯度推导
在上面的例子中,可以看到一个函数操作的长链条,链条上的门都对 w 和 x 的点积结果进行操作。该函数被称为 Sigmoid 函数 \(\sigma(x)\)。Sigmoid 函数关于其输入的求导可以简化为:
这种简化使得梯度计算更加直接。例如,Sigmoid 函数的输入为 1.0,则前向传播得到的输出为 0.73。根据上面的公式,局部梯度为 \((1 - 0.73) \times 0.73 \approx 0.2\)。这与先前的计算流程相比更为简洁,因此在实际应用中,将这些操作打包进一个单独的门单元是非常有用的。这个神经元的反向传播可以通过以下代码实现:
# 假设一些随机数据和权重
w = [2, -3, -3] # 权重 w0, w1, w2
x = [-1, -2] # 输入 x0, x1
# 前向传播
dot = w[0] * x[0] + w[1] * x[1] + w[2] # 计算加权和 (w0*x0 + w1*x1 + w2)
f = 1.0 / (1 + math.exp(-dot)) # 通过 sigmoid 激活函数得到输出 f
# 反向传播
ddot = (1 - f) * f # 计算 dot 对应的梯度,使用 sigmoid 函数的导数
dx = [w[0] * ddot, w[1] * ddot] # 计算损失函数对输入 x 的梯度
dw = [x[0] * ddot, x[1] * ddot, 1.0 * ddot] # 计算损失函数对权重 w 的梯度
反向传播实践:分段计算¶
假设函数为:
需要强调的是,如果对\(x\) 或 \(y\) 进行微分运算,运算结束后会得到一个巨大而复杂的表达式。然而做如此复杂的运算实际上并无必要,因为我们不需要一个明确的函数来计算梯度,只需知道如何使用反向传播计算梯度即可。下面是构建前向传播的代码模式:
import math
x = 3 # 示例数值
y = -4
# 前向传播
sigy = 1.0 / (1 + math.exp(-y)) # 分子中的 sigmoid(y) #(1)
num = x + sigy # 分子 #(2)
sigx = 1.0 / (1 + math.exp(-x)) # 分母中的 sigmoid(x) #(3)
xpy = x + y # x + y #(4)
xpysqr = xpy ** 2 # (x + y) ^ 2 #(5)
den = sigx + xpysqr # 分母 #(6)
invden = 1.0 / den # 分母的倒数 #(7)
f = num * invden # 最终结果 #(8)
注意在构建代码 s 时创建了多个中间变量,每个都是比较简单的表达式,它们计算局部梯度的方法是已知的。这样计算反向传播就简单了:我们对前向传播时产生每个变量(sigy, num, sigx, xpy, xpysqr, den, invden)进行回传。我们会有同样数量的变量,但是都以 d 开头,用来存储对应变量的梯度。注意在反向传播的每一小块中都将包含了表达式的局部梯度,然后根据使用链式法则乘以上游梯度。对于每行代码,我们将指明其对应的是前向传播的哪部分。
# 回传 f = num * invden
dnum = invden # 对应 num 的梯度 #(8)
dinvden = num # 对应 invden 的梯度 #(8)
# 回传 invden = 1.0 / den
dden = (-1.0 / (den**2)) * dinvden # 对应 den 的梯度 #(7)
# 回传 den = sigx + xpysqr
dsigx = (1) * dden # 对应 sigx 的梯度 #(6)
dxpysqr = (1) * dden # 对应 xpysqr 的梯度 #(6)
# 回传 xpysqr = xpy**2
dxpy = (2 * xpy) * dxpysqr # 对应 xpy 的梯度 #(5)
# 回传 xpy = x + y
dx = (1) * dxpy # 对应 x 的梯度 #(4)
dy = (1) * dxpy # 对应 y 的梯度 #(4)
# 回传 sigx = 1.0 / (1 + math.exp(-x))
dx += ((1 - sigx) * sigx) * dsigx # 注意这里是 +=, 因为有多处对 dx 的贡献 #(3)
# 回传 num = x + sigy
dx += (1) * dnum # 再次对 x 的贡献 #(2)
dsigy = (1) * dnum # 对应 sigy 的梯度 #(2)
# 回传 sigy = 1.0 / (1 + math.exp(-y))
dy += ((1 - sigy) * sigy) * dsigy # 对应 y 的梯度 #(1)
需要注意的一些东西:
对前向传播变量进行缓存:在计算反向传播时,前向传播过程中得到的一些中间变量非常有用。在实际操作中,最好代码实现对于这些中间变量的缓存,这样在反向传播的时候也能用上它们。如果这样做过于困难,也可以(但是浪费计算资源)重新计算它们。
在不同分支的梯度要相加:如果变量 x,y 在前向传播的表达式中出现多次,那么进行反向传播的时候就要非常小心,使用 += 而不是 = 来累计这些变量的梯度(不然就会造成覆写)。这是遵循了在微积分中的多元链式法则,该法则指出如果变量在线路中分支走向不同的部分,那么梯度在回传的时候,就应该进行累加。
用向量化操作计算梯度¶
得到的实际上是雅克比矩阵,这个矩阵可以描述每个输出对输入的影响.雅克比矩阵表示输出向量中每个元素对输入向量中每个元素的偏导数。
考虑一个常见的深度学习操作:矩阵乘法。假设我们有两个矩阵 W 和 X,它们的乘积是一个矩阵 D:\(D = W \cdot X\)
在反向传播中,我们通常需要计算损失函数 L 对 W 和 X 的梯度。这就需要知道 L 对 D 的梯度 \(\frac{\partial L}{\partial D}\),并使用链式法则来计算 \(\frac{\partial L}{\partial W}\) 和 \(\frac{\partial L}{\partial X}\):
维度分析是理解向量化操作梯度计算的关键。假设:
- W 的维度是 [\(n \times p\)]
- X 的维度是 [\(p \times m\)]
- D 的维度是 [\(n \times m\)]
那么,梯度 \(\frac{\partial L}{\partial D}\) 的维度是 [\(n \times m\)] ,而通过链式法则得到的 \(\dfrac{\partial L}{\partial W}\) 和 \(\dfrac{\partial L}{\partial X}\) 的维度分别是:
- \(\dfrac{\partial L}{\partial W} = [n \times p]\)
- \(\dfrac{\partial L}{\partial X} = [p \times m]\)
通过这种方式,我们可以确保计算的正确性和维度匹配。
# 前向传播
W = np.random.randn(5, 10)
X = np.random.randn(10, 3)
D = W.dot(X)
# 假设我们得到了 D 的梯度
dD = np.random.randn(*D.shape) # 和 D 一样的尺寸
dW = dD.dot(X.T) #.T 就是对矩阵进行转置
dX = W.T.dot(dD)
_提示:要分析维度!_注意不需要去记忆 dW 和 dX 的表达,因为它们很容易通过维度推导出来。例如,权重的梯度 dW 的尺寸肯定和权重矩阵 W 的尺寸是一样的,而这又是由 X 和 dD 的矩阵乘法决定的(在上面的例子中 X 和 W 都是数字不是矩阵)。总有一个方式是能够让维度之间能够对的上的。例如,X 的尺寸是 [10x3],dD 的尺寸是 [5x3],如果你想要 dW 和 W 的尺寸是 [5x10],那就要 dD.dot(X.T)。
矩阵相乘反向传播的通用策略
假设我们有如下的矩阵乘法操作:\(Y = X \cdot W\)
其中:
- X 是一个维度为[\(N×D\)] 的输入矩阵。
- W 是一个维度为[\(D×M\)] 的权重矩阵。
- Y 是结果矩阵,维度为[\(N×M\)]。
在反向传播中,我们从损失函数 L 对输出矩阵 Y 的梯度开始,记作 \(\dfrac{\partial L}{\partial Y}\),维度为[N×M]。目标是计算输入矩阵 X 和权重矩阵 W 的梯度,即 \(\dfrac{\partial L}{\partial X}\) 和 \(\dfrac{\partial L}{\partial W}\)。
对于输入矩阵 X 中的元素 \(x_{11}\) :
-
前向传播:在矩阵乘法 \(Y = X \cdot W\) 中,输出矩阵 Y 的第一行第一列的元素 \(y_{11}\) 是 X 的第一行与 W 的第一列的乘积。\(y_{11} = \sum_{k=1}^{D} x_{1k} \cdot w_{k1}\)
-
反向传播:对于元素 \(x_{11}\),我们可以计算其对 \(y_{11}\) 的导数:\(\dfrac{\partial y_{11}}{\partial x_{11}} = w_{11}\)
通过链式法则,损失函数 L 对 \(x_{11}\) 的导数为:\(\dfrac{\partial L}{\partial x_{11}} = \dfrac{\partial y_{11}}{\partial x_{11}} \cdot \dfrac{\partial L}{\partial y_{11}}\)
类似的可以推广到 y 的所有元素,然后就可以得到 y 对 x_11 的导数以及 y 对 x 的导数
在实际应用中,若矩阵 WWW 或 XXX 是稀疏的,可以利用稀疏矩阵乘法优化计算过程,从而提高计算效率。稀疏矩阵的计算可以显著降低内存和计算需求,是在大规模数据集上训练模型时的关键优化手段。
PyTorch 实战:自定义函数的自动求导¶
为什么要自定义函数¶
在 PyTorch 中,求导操作是自动完成的,通过正确的函数嵌套或网络嵌套,即可实现自动求导,无需手动计算复杂的导数。然而,在某些情况下,自动求导并不适用。这通常发生在需要实现某些不可导操作或定制化的计算过程中。为此,PyTorch 提供了扩展 torch.autograd
的功能,使用户可以自定义求导方式。
场景一:不可导操作 有些操作本质上是不可导的,或者其导数在标准库中没有直接实现。例如,你可能希望设计一个特定的激活函数或损失函数,这些函数无法通过现有的自动求导机制处理。
场景二:定制损失函数 有时,为了实现更好的学习效果,你可能会设计一个新的损失函数。此时,必须明确该函数的计算过程和求导方式,以便在反向传播时正确地更新参数。
自动梯度的存储与清理¶
在我们计算关于张量的梯度之前,需要一个地方来存储梯度。 重要的是,我们不会在每次对一个参数求导时都分配新的内存
。 因为我们经常会成千上万次地更新相同的参数,每次都分配新的内存可能很快就会将内存耗尽。 注意,一个标量函数关于向量的梯度是向量,并且与向量具有相同的形状。
- 正确使用
requires_grad=True
在进行反向传播计算时,PyTorch 会自动计算和存储梯度。然而,必须提前告知 PyTorch 哪些张量需要梯度。通过设置 requires_grad=True
,可以确保 PyTorch 在计算时保留梯度信息。如果不设置,则计算过程不会跟踪这些张量的梯度,导致后续的反向传播操作失败或报错。
x = torch.tensor([1.0, 2.0, 3.0]) # 创建输入张量
x.requires_grad_(True) # 启用梯度追踪
y = torch.dot(x, x) # 计算 y = x^2
y.backward() # 反向传播计算梯度
print(x.grad) # 输出梯度
在这个例子中,设置了 x.requires_grad_(True)
,因此 PyTorch 会跟踪 x
的梯度,最终在 y.backward()
调用时可以正确计算并输出 x
的梯度。
- 错误顺序导致的错误
x=torch.tensor([1.0,2.0,3.0])#输入的张量
y=torch.dot(x,x)#进行计算
x.requires_grad_(True)#表示这个张量需要进行求导,否则不会存储其梯度
y.backward()#从最终端开始反向传播
x.grad#显示 x 的梯度
在这个例子中,由于 requires_grad_(True)
在计算之后设置,PyTorch 无法跟踪计算过程中的梯度,因此在调用 y.backward()
时会报错。这表明启用梯度追踪必须在计算之前完成。
- 清除累积的梯度
在默认情况下,PyTorch 会累积梯度,我们需要清除之前的值
x.grad.zero_()
- 非标量输出的梯度计算
当 y
不是标量时,向量 y
关于向量 x
的导数的最自然解释是一个矩阵。 对于高阶和高维的 y
和 x
,求导的结果可以是一个高阶张量。
当输出是非标量时,PyTorch 需要知道如何处理这个张量。通常,最常见的做法是计算批量中每个样本的梯度之和,这也是大多数损失函数的默认行为。
对非标量调用 backward 需要传入一个 gradient 参数,该参数指定微分函数关于 self 的梯度。
# 本例只想求偏导数的和,所以传递一个 1 的梯度是合适
x.grad.zero_() # 清除先前的梯度
y = x * x # 计算 y
# 等价于 y.backward(torch.ones(len(x)))
y.sum().backward() # 计算 y 对 x 的梯度
print(x.grad) # 输出梯度
注意,深度学习中一般对标量求导(主要是评价指标和 Loss),如果损失函数是向量,那么可能导致维度越来越大
求导原理¶
在 PyTorch 中,自动求导的实现基于计算图的概念。计算图是一种有向无环图(DAG)
,其中每个节点代表一个操作(例如加法、乘法等),边表示这些操作的依赖关系。在前向传播过程中,PyTorch 会动态构建这个计算图,并在反向传播时依据计算图来计算梯度。
在前向传播时,PyTorch 会根据操作的顺序构建计算图。每个操作节点都会保存对应的梯度计算方法,这些方法将在反向传播过程中被调用。
反向传播的核心思想是利用链式法则(链式求导)计算梯度。在计算图中,从输出节点开始,梯度会沿着图中边反向传播到每个输入节点,直到所有节点的梯度都被计算出来。
注意,PyTorch 的自动求导本质上是一种数值求导,而不是符号求导。这意味着 PyTorch 依赖于计算图中的每个操作节点存储的梯度计算方法
,而不是解析函数的导数公式。
自定义函数完成求导¶
在 PyTorch 中,我们可以通过自定义 torch.autograd.Function
类,手动实现前向传播和反向传播的过程。这有助于我们更深入地理解反向传播的机制,并且在某些情况下,可以实现更复杂或自定义的操作。
我们想自己定义一个可以反向传播的函数,那么这个函数要定义成类,并且需要继承 torch.autograd.Function
类,同时分别定义 forward
和 backward
方法,这两个方法是实现正向推理和反向传播的关键,我们以一个简单的平方函数为例看看如何实现的
class SquareFunction(torch.autograd.Function):
@staticmethod
def forward(ctx, input):
# 正向传播的逻辑
ctx.save_for_backward(input) # 保存输入,以便在反向传播时使用
return input * input # 计算输出
@staticmethod
def backward(ctx, grad_output):
# 反向传播的逻辑
input, = ctx.saved_tensors # 获取保存的输入
grad_input = grad_output * 2 * input # 计算输入的梯度
print(f"grad_output: {grad_output}") # 输出 grad_output 的值
return grad_input
forward
方法:
ctx.save_for_backward(input)
:在正向传播时,保存输入input
,以便在反向传播时使用。这是实现反向传播所需信息的关键步骤。- 返回值
input * input
:这是正向传播的输出结果。
backward
方法:
ctx.saved_tensors
:获取正向传播中保存的张量,这里是input
。grad_output * 2 * input
:根据链式法则计算梯度。grad_output
是从上游传递过来的梯度,2 * input
是平方函数的导数。
ctx
是“上下文”(context),用于在正向传播和反向传播之间存储信息。这个上下文对象是特别为每次操作或函数调用创建的,可以用来保存用于计算梯度的任何信息
为了使用自定义的 SquareFunction
,我们可以调用其 apply
方法。
# 定义输入张量,并开启自动求导
x = torch.tensor([2.0], requires_grad=True)
# 使用自定义函数进行前向传播
y = SquareFunction.apply(x)
# 进行反向传播
y.backward()
# 打印结果
print(f"x: {x}")
print(f"y: {y}")
print(f"x的梯度: {x.grad}")
在反向传播中,上游传递过来的梯度 grad_output
并不总是等于 2。grad_output
的值取决于计算图中更上层的操作。具体来说,grad_output
是损失函数相对于当前节点的输出的梯度,也就是 \(\frac{\partial L}{\partial y}\)。这个值是由上层节点的梯度计算决定的,而不是固定为某个常数。
在之前的例子中,grad_output
的实际值取决于如何定义和调用 y.backward()
,而不是简单地等于 2。
\(\frac{\partial L}{\partial y}\) 是上游传递过来的梯度,在代码中对应 grad_output
。
\(\frac{\partial y}{\partial x}\) 是当前层的梯度。
小结¶
- 分段计算和链式法则的应用正是反向传播高效性和模块化实现的关键。正如你所说,在深度学习中,我们通常不会手动计算完整的梯度表达式,而是依赖自动微分框架(如 PyTorch)通过分解和链式法则一步步计算局部梯度,最终组合成整体的反向传播过程。
- 下一步,定义和训练神经网络将自然延续这一基础,使用反向传播高效调整网络参数。通过定义前向传播结构、选择合适的损失函数和优化器,你将能够训练模型以最小化损失,从而提高模型的预测能力。至于卷积神经网络(ConvNets),虽然它们引入了空间层次上的复杂性,但核心思想仍然依赖于你已经掌握的反向传播机制。