深度学习基础重修:从手算前向传播、梯度下降到反向传播

📅 发布时间:2026/10/19 14:42:11
深度学习基础重修:从手算前向传播、梯度下降到反向传播
1. 为什么我要“重修”深度学习基础先交代一下背景。我算是半路出家做AI应用的之前用现成框架跑过不少模型分类、生成、微调都摸过但说实话对模型内部发生的事情一直停留在“调参侠”阶段。前向传播、梯度下降、反向传播这几个词每个都认识合在一起就懵。上次硬着头皮啃教材公式抄了一整页合上书就忘典型的“背会了没学会”。这次决定换个思路重修不抄公式、不背定义先把每个概念用大白话还原本质再用手算和代码把过程走一遍。标题里的“日记”不是噱头我是真的在用一个新手的心态从头推理遇到卡壳的地方就记下来琢磨明白再往下走。如果你也是那种会用框架但说不清梯度从哪来、反向传播为什么要“反向”的人这篇笔记就是写给你看的。我不保证把数学讲得多优雅但保证每句话你都能读得下去读完之后能自己动手复现一遍最基础的过程。2. 前向传播让数据走一遍流水线2.1 先从一张最简单的网络图说起我这次没有再去看那种画了几十层的大网络拓扑图而是自己动手画了一个极简的网络输入层两个神经元隐藏层两个神经元输出层一个神经元。就这五条连线。为什么先从这么小的开始因为前向传播这件事本质特别朴素把输入数据从第一层往最后一层“推”一遍每一层都做两件事——加权求和、过激活函数。仅此而已。之前学不懂的原因就是一上来就面对几千个参数的大网络注意力全被规模绕晕了反而看不穿这个基本操作。拿真实世界做个类比一家工厂生产一个产品原材料进第一道工序加工完传给第二道再加工最终出成品。每一道工序有自己的“配方”权重和“验收标准”激活函数。前向传播就是完整地跑一遍流水线看看成品到底是啥。模型在推理时比如预测一个结果做的本质就是这个输入进去流一遍得到输出。2.2 手算一个前向传播把数字摁明白我这次特意找了最简单的例子手算确保每一步数字都对得上。假设输入x1 0.5x2 0.1可以理解成某个样本的两个特征权重全部取小随机数w11 0.3, w12 0.6, b1 0.1w21 0.2, w22 0.4, b2 0.2输出层权重v1 0.5, v2 0.8, b3 0.3隐藏层激活函数用Sigmoid输出层不用激活这是回归任务的做法输出直接是数值隐藏层第一个神经元的输入z1 0.5 x 0.3 0.1 x 0.6 0.1 0.31过Sigmoida1 sigmoid(0.31) 0.5768隐藏层第二个神经元z2 0.5 x 0.2 0.1 x 0.4 0.2 0.34过Sigmoida2 sigmoid(0.34) 0.5842输出层神经元过加权求和不过激活pred 0.5768 x 0.5 0.5842 x 0.8 0.3 0.2884 0.4674 0.3 1.0558如果真实标签是1.0那这个模型前向传播下来的误差就是0.0558看起来不大但这只是恰好选了运气好的随机数。再多算几个样本就知道权重初始化和数据分布碰在一起预测值经常离目标十万八千里这时候就需要后面两个东西登场梯度下降和反向传播。手算的意义不在于算得快而在于建立“数据流过网络”的体感。我在草稿纸上画了好几次这张图每次算完都拿Python代码对一遍确认没有算错这种双重确认给了我很大的底气。2.3 为什么要有激活函数如果网络里只做加权求和多层叠加后本质还是一个线性变换换句话说堆多少层都等价于一层模型的表达能力约等于零。激活函数的作用是给网络引入非线性让它能拟合曲线、分类边界这些不是直线的模式。Sigmoid把一切实数压缩到0和1之间好处是输出范围稳定坏处是饱和区梯度接近0反向传播时容易造成梯度消失这也是后来ReLU这类函数流行的原因。ReLU简单粗暴负数直接压成0正数保持不变计算快梯度在正半轴恒为1大大缓解了梯度消失问题。选型建议就一条隐藏层优先ReLU及其变体输出层看你任务——回归任务用线性二分类用Sigmoid多分类用Softmax。这不是玄学每个选择背后都是数学性质和任务目标的匹配。3. 梯度下降下山的路怎么走3.1 损失函数就像一张地形图前向传播跑完模型给出了预测值接着要拿预测值和真实值对比算出一个“错得有多离谱”的分数这个分数就是损失。常用的均方误差公式是L (1/2)(pred - y)²为什么要加那个1/2主要是为了求导的时候把平方项的2抵消掉算是约定俗成的习惯省得每次导数都要带个系数。虽然不影响最优解的位置但是能让公式干净不少。把网络的全部参数权重和偏置当成自变量损失作为因变量就会在高维空间形成一个曲面。最优解就是曲面上最低的那个点。梯度下降做的就是站在曲面的某个位置看看哪个方向是向下最陡的方向沿着那个方向迈一小步然后重复这个过程直到走到一个低处。生活化理解就是摸黑下山你看不见整个山的全貌只能感受脚底下的坡度每次挑最陡的坡往下走。运气好能走到谷底运气不好可能被困在半山腰的小洼地局部最优也可能在山谷两边来回震荡振荡。3.2 梯度到底是个啥怎么求梯度这个概念以前总觉得玄后来我用“做菜放盐”来类比就通了菜咸了说明盐放多了要么减少盐要么增加水你总要知道往哪个方向调整才能让味道往“正好”那边走。梯度的每一维就是“这个参数对损失的影响方向和大小”。数学上梯度就是损失函数对每个参数的偏导数组成的向量。比如有两个参数w1、w2就要算 ∂L/∂w1 和 ∂L/∂w2。参数更新的标准写法是w_new w_old - 学习率 x 梯度注意这里的减号如果梯度是正的说明增大这个参数会让损失变大所以要往相反方向减如果梯度是负的就反过来加。这一减一加参数就在朝损失下降的方向移动。3.3 学习率步子迈多大才合适学习率是梯度下降里最需要手调的参数没有之一。我复现时试过三组学习率效果截然不同学习率0.01慢慢悠悠地下降几十轮过去才看到明显效果训练时间长学习率1.0第一步直接跳飞损失爆炸到几百完全废了学习率0.1前期冲得快后期在最优值附近来回震荡但整体能收敛这让我彻底明白学习率太小训练慢到没法用学习率太大参数直接跨过最优解损失不减反增。实际工程里一般会用学习率调度先用大的快速下降再逐渐调小精细收敛或者用带动量的优化器Adam就是带自适应学习率的经典选择。4. 反向传播误差倒着传回去4.1 链式法则就是把“快递”一层一层拆开前向传播是数据从输入流向输出反向传播则是误差从输出流向输入沿途计算每个参数对损失的梯度。为什么非得“反着来”因为网络是一层套一层的复合函数想求第1层的权重对最终损失的贡献必须借助链式法则一层层往回拆。链式法则本质不复杂复合函数 f(g(x)) 对x求导等于 f(g(x)) 乘以 g(x)。拆快递一样先拆外层输出层的导数再拆里层隐藏层的导数每个包裹里面还套着包裹。反向传播做的就是把误差最外层的结果一层层往内传导每经过一层、每个参数就顺便拿一份“梯度快递”。4.2 接着前面的数字完整手算一遍反向传播继续用刚才那个网络的参数和预测结果。真实标签y1.0预测pred1.0558。第一步算输出层的误差贡献。损失 L 0.5 x (pred - y)² 0.5 x 0.0558² ≈ 0.00156。∂L/∂pred pred - y 0.0558。这就是输出神经元收到的“误差信号”。接下来它要分别对v1、v2、b3求导∂L/∂v1 (pred - y) x a1 0.0558 x 0.5768 ≈ 0.0322∂L/∂v2 0.0558 x 0.5842 ≈ 0.0326∂L/∂b3 0.0558第二步误差传给隐藏层。隐藏层第一个神经元a1对输出有贡献所以它要承接一部分输出层误差∂L/∂a1 (pred - y) x v1 0.0558 x 0.5 0.0279再往下走一层要考虑激活函数Sigmoid的导数。Sigmoid导数有个好记的公式sigmoid(z) x (1 - sigmoid(z))。a1 0.5768所以 sigmoid(z1) 0.5768 x (1 - 0.5768) 0.2441。∂L/∂z1 ∂L/∂a1 x sigmoid(z1) 0.0279 x 0.2441 ≈ 0.00681然后才能求对w11的梯度∂L/∂w11 ∂L/∂z1 x x1 0.00681 x 0.5 ≈ 0.00341到这里w11这个参数就有了更新方向w11_new 0.3 - 学习率 x 0.00341。同理可以算w12、w21、w22以及两个偏置b1、b2。整个过程走完一遍我最大的感悟是反向传播没有高深莫测就是“链式法则的工程化应用”。你不需要在脑子里同时处理所有公式只需要从输出往输入一步步推每一步都是简单的一元复合函数求导。4.3 计算图与自动微分手算能帮忙理解原理但真实框架里你肯定不会手动算梯度——框架用的是“反向模式的自动微分”它和你刚才手算的过程在逻辑上完全一致先走一遍前向传播把每层的中间值都存下来再反向按链式法则把梯度算出来。这就像你记了每道工序的半成品后面发现问题能直接溯源到是哪道工序出的问题。PyTorch里的backward()做的就是这件事TensorFlow里的GradientTape同理。理解了这个底层逻辑你就不会把一个框架当黑魔法它只是替你背了一遍链式法则。5. 从头到脚复现一遍我的实操记录5.1 用NumPy写一个小MLP我不建议一辈子只用框架新手最值得做的一件事用Numpy写一个两层的小网络把上面那个手算过程自动化然后拿它训练一个能用的模型。代码不长但是每一步都看得见。import numpy as np def sigmoid(x): return 1 / (1 np.exp(-x)) def sigmoid_deriv(a): return a * (1 - a) # 造一点线性可分的数据 np.random.seed(42) X np.array([[0,0],[0,1],[1,0],[1,1]], dtypefloat) y np.array([0, 1, 1, 0], dtypefloat) # 异或问题 # 初始化 n_in, n_hidden, n_out 2, 4, 1 W1 np.random.randn(n_in, n_hidden) * 0.5 b1 np.zeros((1, n_hidden)) W2 np.random.randn(n_hidden, n_out) * 0.5 b2 np.zeros((1, n_out)) lr 0.5 for epoch in range(10000): # 前向传播 z1 X W1 b1 a1 sigmoid(z1) z2 a1 W2 b2 pred sigmoid(z2) # 二分类用sigmoid输出 # 损失 loss np.mean((pred - y.reshape(-1, 1)) ** 2) # 反向传播 d_pred (pred - y.reshape(-1, 1)) / len(X) d_z2 d_pred * sigmoid_deriv(pred) d_W2 a1.T d_z2 d_b2 np.sum(d_z2, axis0, keepdimsTrue) d_a1 d_z2 W2.T d_z1 d_a1 * sigmoid_deriv(a1) d_W1 X.T d_z1 d_b1 np.sum(d_z1, axis0, keepdimsTrue) # 更新 W2 - lr * d_W2 b2 - lr * d_b2 W1 - lr * d_W1 b1 - lr * d_b1 if epoch % 1000 0: print(fepoch {epoch}, loss {loss:.6f})跑出来的损失会一路往下掉说明前向传播和反向传播配合着奏效了。如果你看到loss不减反增或者卡在某个值不动基本就是学习率或者初始化的问题后面一节我详细说。5.2 我踩过的坑损失不降的三个排查方向学这个的时候我踩过不少坑列几个最有代表性的第一个坑是激活函数用错。比如输出层用了Sigmoid但任务其实是回归导致损失永远降不到理想范围。这属于“选型错误”不是优化问题。我后来每条经验都是先确认输出层的激活函数和损失函数跟任务匹配再谈调参。第二个坑是学习率设置不当。我有一次把学习率设成1.0直接爆炸loss一秒变nan。排查时用了一个笨办法每几轮打印一次权重和梯度的均值看到梯度的数量级是几百立刻意识到是学习率太大。后来我给自己定了个习惯新数据集先拿0.01跑一轮看看loss初始值大概什么水平再根据下降速度调节。第三个坑是忘记做数据归一化。输入特征的量级差距太大的时候大数值特征会直接主导梯度小数值特征学不动。归一化之后训练速度快了很多稳定性也好了就像做菜时所有食材都切成差不多大小的丁好入味。5.3 梯度消失为什么深层网络难训练前面提过Sigmoid的饱和区梯度会趋于0。如果网络层数多了反向传播时梯度每一层都乘一个小数从输出传到输入梯度会指数级缩小前面的层几乎收不到有效的更新信号。这就是梯度消失。我在复现三层网络时试过用Sigmoid当隐藏层激活第三层最靠近输入的层的权重基本不动一眼看去训练好像成功了其实只是后面两层在硬扛。换成ReLU后这种情况明显改善。ReLU在正数部分导数恒为1反向传播时梯度穿过它不会被缩小前面的层也能正常学习了。另外还有一个常见问题是局部最优解和鞍点。高维损失空间里鞍点比局部最优更常见在鞍点附近梯度接近0训练看起来很“平”但其实不是到了好地方。带动量的优化器Momentum能帮助冲过这种平坦区域这就是为什么工程上大家普遍用Adam它的自适应学习率和动量机制就是为了应对这些地形问题。6. 重修之后我的几个新认识6.1 三个概念其实是一件事的三个视角这一轮学完我最大的认知提升是前向传播、梯度下降、反向传播不是三个独立的知识点。前向传播定义了模型在某个参数下的输出梯度下降定义了如何通过“参数-损失”的地形往低处走反向传播则是一个高效算出“该往哪个方向走”的工具。它们环环相扣少了任何一个都没法完成训练。之前总觉得训练模型是在“调参数”现在我更愿意说训练是在“用误差反推参数的世界观”。你允许误差从输出往输入传让每个参数知道自己应该朝哪个方向调整这个机制就是深度学习的发动机。6.2 知其所以然后调参才叫调参以前跟着网上教程折腾模型时遇到loss曲线不收敛我只能凭感觉随机改动学习率、隐藏层宽度等配置经常越调越乱。现在起码有了排查的路数先确认前向传播的数值输出是否合理再看损失下降的速率是否正常最后根据梯度的数量级判断学习率的方向。这套排查顺序说白了就是“由上到下、由前到后”的工程化思维跟手算反传时“从输出往输入推”的思路如出一辙。理解了原理之后你获得的不是公式而是一套处理模型问题的直觉。6.3 一些实用的学习建议如果你也在学这块我给你几个实操性建议第一一定自己手算一次全流程数字不必复杂但每一步的过程要完整用纸笔或用表格都行。我当初在表格里一行行写下每个中间变量的数值前后核对了好几遍才真正把链路打通。第二手算完一定要拿Numpy复现一遍重点不是训练多好的模型而是对照手算验证梯度公式正确。如果手算和代码跑出来的梯度相差几个量级多半是链式法则哪一步漏了一个乘项。第三遇到loss不降的问题时不要上来就调参。先打印一下中间层输出的数值范围和梯度范围建立“看到什么数值、说明哪里有问题”的经验库比瞎猜高效得多。这批重修日记写到这最基础的一章算告一段落了。下一块我想接着把损失函数的可选择范围、不同优化器的设计动机以及一个小型图像分类任务从零到一的完整实现串一遍到时候有新的踩坑体验再拿出来接着跟大家分享。前向传播、梯度下降和反向传播这三板斧虽然入门但最终所有花哨的模型和技巧底子都在这里。