揉扁搓圆transformer架构:反向传播算法详解 深度学习一大特色就是能够根据给定数据和自己的预判结果进行自我调整然后让自己的预判结果跟实际数据越来越接近。前面我们研究的损失函数用于判断模型的输出结果与实际结果的“接近层度”反向传播算法就是调整模型内部参数然后让模型的输出在损失函数的评判下使得输出结果与实际数据的“接近层度”越来越大也就是让损失函数的数值越来越小。如果你了解微积分那么你就会了解通过求导来求函数极值。如果一个单变量函数yf(x)我们想要找到x’使得f(x’)是整个函数的最小值那么基本算法是先使用任意一点x0,然后计算在点x0处的导数。如果导数值为0那么此时x0就是函数的极大值或者是极小值到底是哪一种情况我们需要做进一步的判断但无论如何x0一定是函数的极值点。如果在x0处求导后结果大于0那意味着往x0的右边挪动会使得f(x)取值增大,向x0左边挪懂就能让f(x)取值减小由于我们求最小值因此在这种情况下就需要向x0左边挪动“一小步”。同理如果在x0处求导结果小于0那意味着向x0左边挪动“一小步”会让函数取值增大向右边挪动“一小步”就会让f(x)取值减小。这里的“一小步”到底如何量化也是大模型算法的一个要点我们后面会详细研究。反向传播算法的目的之一就是确定“向左”还是“向右”挪动。虽然原理与上面的单变量函数相同但是大模型拥有数千亿乃至万亿参数因此它的处理就要比上面的描述复杂得多我们看看基于大模型反向传播算法的执行步骤1.前向传播。该步骤跟我们前面描述单变量函数类似由于大模型可以看做是一个多变量函数因此输入对应的就是一个含有多个变量的向量通常成为tensor,通过输入数据得到模型的判断结果:F(X).由于大模型是一层连着一层构成的因此前向传播时我们需要逐层计算上一层的输出会成为下一层的输入例如当前层属于第l层那么此时的计算为:其中a(l-1)是上一层的输出结果Wl 表示上一层与当前层参数直接的连接参数。例如第l-1层有10个神经元第l层有20个神经元l-1层的第一个神经元与l层的神经元都有连接那么连接参数就是20个由于l-1层每个神经元都跟l层所有神经元有连接于是这些连接参数就可以使用一个10X20的矩阵来表示这些参数就对应要求导确定调整方向的参数。在计算完当前层的输出结果后我们还需要使用激活函数对当前输出做一次非线性变化:对于大模型而言上面使用的激活函数f通常是GeLU。当计算传导到最后一层我们获得最后一层的输出结果后就能将输出结果和实际要对比的数据传入损失函数进行计算由此得到模型输出跟实际结果的“差距”。2.反向传播求导。模型内部可调整的参数其实就是模型内部一层层神经元前后连接的参数。前面我们看到前后两层神经元之间的连接参数可以使用矩阵来表示因此我们把模型看做一个函数的话那么结合内部参数就可以表达成 L F(W1, W2…Wn)。反向传播求导的核心原则是链式法则加深损失L依赖于输出a, 同时a又通过参数w可以调整控制那么要调整L就需要调整a但调整a又得调整w于是根据链式法则就有:对于大模型而言这条反向传播的链条非常长。正如夜长梦多一旦链条长了出错的概率就很大后面我们会看到如何使用一系列算法控制这个长链条计算过程中出现的偏差。我们针对当前第l层来拆解具体的数学步骤2.1.计算当前层的输出误差流如果我们当前位于第l层由于是反向传播那么我们就已经知道l1层反向传回来的梯度数值d^(l1).从前面前向传播的过程我们知道第l1层的输入来自于第l层的输出进过一次激活函数。因此当前l层的梯度计算就是上图中符号“一个圆圈内部一个点”表示两个向量逐元素相乘。这一步叫“将误差通过激活函数反向传播”2.2.由于两层神经元直接的连接参数就是模型内部要修改的关键参数因此我们需要针对连接参数W进行修正对于权重W^l也就是第l层于第l1层直接的连接参数它的修改步骤如下:每一层除了连接参数外还有一个针对该层的修正参数b^l,我们也要针对其进行修正:2.3.我们需要将梯度继续向前传播也就是从第l层向第l-1成传播以便修正第l-1层连接的参数3.利用反向传播算出来的梯度使用各种优化器SGD, Adam更新参数:上面描述的算法步骤包含多个数学参数看起来很乱我们走一段具体代码来理解上面的算法步骤。我们将使用代码模拟一个两层神经网络每层只包含两个神经元然后走一遍向前传播和向后传播的过程执行上面的算法步骤。首先我们先设计一个拥有两层每层两个节点的网络:importnumpyasnp #设置随机种子np.random.seed(42)#网络结构与参数# 输入特征2个样本每个样本2维特征Xnp.array([[0.5,0.3],[0.2,0.8]])# shape:(2,2)批量大小2Y_truenp.array([[0.8,0.2],[0.6,0.4]])# 真实标签 shape:(2,2)# 第一层输入-隐藏:2个输入2个神经元W1np.random.randn(2,2)*0.5#shape(2,2)b1np.random.randn(2,1)*0.5#shape(2,1)# 第二层隐藏-输出:2个输入2个神经元W2np.random.randn(2,2)*0.5#shape(2,2)b2np.random.randn(2,1)*0.5#shape(2,1)# 激活函数sigmoid defsigmoid(z):return1/(1np.exp(-z))defsigmoid_derivative(a):returna*(1-a)上面代码对应的网络结构如下:从上图我们可以看出输入 W^1 [ [0.25,0.32],[-0.07, 0.76]], b1[-0.12,-0.12]; W^2[[0.79,-0.23],[0.38,0.27]], b2[-0.23, 0.23]. 接下来我们看看如何执行前向传播:print( 前向传播 )# 输入层激活值 a0X.T#shape(2,2)每一列是一个样本 # 第一层线性变换 z1np.dot(W1,a0)b1 #shape(2,2)a1sigmoid(z1)#shape(2,2)# 第二层线性变换 z2np.dot(W2,a1)b2 #shape(2,2)a2sigmoid(z2)#shape(2,2)#损失函数均方误差(MSE)Lnp.mean((a2-Y_true.T)**2)print(fa0 (输入激活值):\n{a0}\n)print(fz1 (隐藏层线性输入):\n{z1}\n)print(fa1 (隐藏层激活值):\n{a1}\n)print(fz2 (输出层线性输入):\n{z2}\n)print(fa2 (输出层激活值):\n{a2}\n)print(fL (损失): {L:.6f}\n)上面代码运行后给出结果如下:前向传播a0(输入激活值):[[0.50.2][0.30.8]]z1(隐藏层线性输入):[[-0.01363779-0.12271099][0.273308130.55691232]]a1(隐藏层激活值):[[0.49659060.46936069][0.567904870.63573781]]z2(输出层线性输入):[[0.378317240.382845][-0.19537192-0.17057832]]a2(输出层激活值):[[0.593467180.5945591][0.451311790.45745852]]L(损失):0.027286通过上面代码我们可以看到a1其实是a0经过第一层神经元与第二层神经元的链路上的参数在加上链路末尾的调整参数b1后的结果z1是输入第二层神经元时执行一次激活函数后的结果。我们通过如下图像能比较好的理解这个过程:从上图可以看到输入信号a0[0] 0.5,经过第一层第一个节点与第二层第一个节点的链路后用输入信号乘以链路权重也就是 0.50.25,然后第一层第二个元素的输入值a0[1]0.3这个数值经过第一层第二个元素与第二层第一个元素的链路后信号乘以链路上的参数也就是0.3-0.15, 两个输入到第二层第一个元素的信号数值加总再加上第二层第一个神经元的调整参数b0[0]-0.12于是得到输入第二层第一个元素的加总信号值为 z1[0] 0.5 * 0.25 0.3 * -0.15 -0.12 0.125, 这个数值在经过一次激活变成第二层第一个元素的输出信号a1[0]也就是a1[0] sigmoid(z1[0]) 0.479,第二层第二个神经元的输出数值可以此类推。接下来我们看看反向传播的算法原理。反向传播其实是链式法则求导的具体实现。从前向传播我们可以看到如下的逻辑链条:从上面路径可以看到最终损失函数的计算中链路参数W0[0][0]发挥了作用。我们想要判断如何调整W0[0][0]才能让损失函数的结果降到最小那么就需要基于损失函数来针对W0[0][0]求导。问题在于W0[0][0]并不是直接作用于损失函数而是通过层层迭代传递来影响损失函数。W0[0][0]先促进Z1[0]也就是第二层第一个神经元输入信号的数值的创建(Z1[0]中的1表示神经网络第2层由于网络层的下标计数以0开始最开始那层为第0层所以1表示第二层网络层其中的0表示第一层中第1个神经元),然后第二层第一个神经元将输入信号数值经过激活函数sigmoid转换后成为它的输出信号值a1[0]最后这个值参与了损失函数的计算。将上面前向传播的链路转换为数学公式如下:上面公式中有:它对应的就是激活函数sigmod然后const对应的就是b0[0],也就是调整常量。在使用反向传播时首先针对最外层的a1[0]也就是第二层第一个神经元的输出信号进行求导:由于a1[0]是第一层两个神经元的输出信号经过对应链路抵达第二层神经元的信号加总后所得的z1[0]经过激活函数sigmoid后所得的结果因此a1[0]是z1[0]函数的结果于是针对z1[0]在基于sigmoid函数上进行求导:最后Z1[0]的形成有一部分是第一层第一个神经元输出的信号a0[0]经过链路权重W0[0]后贡献的(第一层第二个神经元输出的信号a0[1]也通过链路权重W0[1]进行了贡献只是我们只查看W0[0]的偏导所以可以忽略它).于是z1[0]又是W0[0]的函数结果于是基于z1[0]的基础上针对w0[0]进行求导:最后我们把这三部分相乘就得到损失函数针对参数w0[0]这点取值上的导数:上面结果就是损失函数针对参数w0[0]处切线的方向,要想通过调整w0[0]来降低损失函数的结果那么就需要根据这个方向反向调整w0[0]的数值这里的“反向调整”里面的文章大了去了也是后续我们需要特别深入详解的地方在这里我们先简单的认为反向调整就是乘以一个预先固定的参数也叫学习率即可最后我们看反向传播的代码实现:print( 反向传播 )#1.输出层误差 δ2# dL/da2(a2-Y)(因为MSE导数)dL_da2(a2-Y_true.T)#shape(2,2)# f(z2)sigmoid_derivative(a2)delta2dL_da2*sigmoid_derivative(a2)#shape(2,2)print(fδ2 (输出层误差, dL/dz2):\n{delta2}\n)#2.输出层参数梯度 dW2,db2 # dW2δ2*a1^TdW2np.dot(delta2,a1.T)#shape(2,2)# db2sum(δ2,axis1,keepdimsTrue)# 对样本维度求和 db2np.sum(delta2,axis1,keepdimsTrue)#shape(2,1)print(fdW2 (输出层权重梯度):\n{dW2}\n)print(fdb2 (输出层偏置梯度):\n{db2}\n)#3.隐藏层误差 δ1# dL/da1W2^T*δ2dL_da1np.dot(W2.T,delta2)#shape(2,2)# f(z1)sigmoid_derivative(a1)delta1dL_da1*sigmoid_derivative(a1)#shape(2,2)print(fδ1 (隐藏层误差, dL/dz1):\n{delta1}\n)#4.隐藏层参数梯度 dW1,db1 dW1np.dot(delta1,a0.T)#shape(2,2)db1np.sum(delta1,axis1,keepdimsTrue)#shape(2,1)print(fdW1 (隐藏层权重梯度):\n{dW1}\n)print(fdb1 (隐藏层偏置梯度):\n{db1}\n)#参数更新演示learning_rate0.1W1_newW1-learning_rate*dW1 b1_newb1-learning_rate*db1 W2_newW2-learning_rate*dW2 b2_newb2-learning_rate*db2print( 参数更新 )print(更新后的 W1:\n,W1_new)print(更新后的 b1:\n,b1_new)print(更新后的 W2:\n,W2_new)print(更新后的 b2:\n,b2_new)可以仔细对应上面代码和前面的逻辑分析就能看到每一步实现都对应前面描述的链式法则。上面代码运行后给出的结果如下:反向传播δ2(输出层误差,dL/dz2):[[-0.04982891-0.00131158][0.06223220.01426064]]dW2(输出层权重梯度):[[-0.02536017-0.0291319][0.037597310.044408]]db2(输出层偏置梯度):[[-0.05114049][0.07649285]]δ1(隐藏层误差,dL/dz1):[[-0.01348773-0.00109167][-0.000549150.00077933]]dW1(隐藏层权重梯度):[[-0.0069622-0.00491965][-0.000118710.00045872]]db1(隐藏层偏置梯度):[[-0.0145794][0.00023019]]参数更新更新后的W1:[[0.2490533-0.06864019][0.323856140.76146906]]更新后的 b1:[[-0.11561875][-0.1170915]]更新后的W2:[[0.792142420.38663055][-0.238496920.26683922]]更新后的 b2:[[-0.2265948][-0.24051416]]