从神经元到反向传播:重建神经网络基础的完整学习路径

📅 发布时间:2026/10/20 20:09:29
从神经元到反向传播:重建神经网络基础的完整学习路径
去年年底那阵子我把各种CNN示例代码跑得飞起模型能出结果、准确率也能看但一被问到“卷积核到底为什么能提取边缘”“反向传播的梯度是怎么一层层流回去的”这类问题我就只能支支吾吾。这让我意识到一个尴尬的事实我的神经网络基础是空的。跑再多项目也只是在照着菜谱写菜食材为什么这样处理、火候为什么这样调我完全说不上来。于是我把CNN按了暂停从头过了一遍神经网络基础。这篇记录就是从零重建神经网络地基的完整笔记——从神经元、激活函数、损失函数到反向传播、优化器、权重初始化最后再到一个手写数字识别实验把我踩过的坑和想明白的事情都写下来。1. 为什么我决定先啃完神经网络基础再碰卷积网络1.1 看着代码能跑其实全是空中楼阁我最早学深度学习的方式估计跟很多人一样找一份成熟的开源项目改改数据、调调参数能出效果就算学会。有一阵子我特别膨胀觉得自己已经“会”CNN了。直到某天和朋友聊到一个很普通的问题为什么分类任务最后用交叉熵而不是均方误差我当时第一反应是“大家都这么用”但再往深一层想为什么大家都这么用我没法给出一个让人信服的解释。朋友当时说了一句话让我印象很深“你要是能把模型为什么不收敛的原因从一个公式推到另一个公式那才是真会了。”从那天起我意识到自己一直在用黑盒却不敢拆开黑盒。后来我尝试去读一些经典模型的源码发现里面几十行代码藏着大量工程细节比如权重初始化方式、学习率调度策略、数据归一化的顺序每一个都是前人踩了无数坑才定下来的。你不懂背后的神经网络基础就只能靠运气去调参。1.2 我给自己的学习地图先搭骨架再填血肉这次我给自己定了一条很朴素的学习路线顺序是单个人工神经元输入怎么加权求和、偏置做什么用激活函数为什么必须是非线性几个经典激活函数的优缺点损失函数如何衡量“差多少”不同类型任务该选谁多层网络与反向传播梯度到底怎么从损失函数流回每一个参数优化器与初始化梯度有了之后怎么更新参数才高效最后才回到CNN卷积、池化、感受野这些概念如何在前面这些基础上生长出来。这条路线其实非常老派但效果出乎意料地好。它是按“前向传播—反向传播—参数更新—网络结构演化”这条逻辑串起来的每一步都能回答上一步的“为什么”。比如先搞懂层次网络在做什么再去看卷积为什么不用全连接就自然明白“参数共享”“局部连接”是在解决什么问题。1.3 学习材料怎么选别一上来就买大部头一开始我也踩过“教材完美主义”的坑买过几本经典教材前几章看完就搁置了。后来我调整了策略每个概念至少看三个来源然后要求自己用一段话对着空气讲清楚。视频类的课程用于建立直觉尤其是图解反向传播的动画很有帮助博客和笔记用来补公式推导这类资料通常已经把“书中跳跃的步骤”展开了真正的教科书用来查缺补漏哪里不懂翻哪里而不是从头硬啃。我自己的经验是如果看完一个概念能默写出一张流程图并能解释每一步上游和下游分别是什么这个知识点才算初步吃透。下面几个章节就是我按这个标准重新“啃下来”的内容。2. 神经网络的三个积木神经元、激活函数与损失函数2.1 神经元它不只是一次加权求和大家见过最多的是这个公式(y f(\sum_i w_i x_i b))。一个神经元做的事是先对输入做线性组合再经过一个非线性函数。我刚开始觉得这很普通直到自己亲手算了一遍才明白那个偏置 (b) 是关键。偏置可以理解为神经元的一条“出厂设置”。没有它无论权重如何调整当输入全为零的时候输出永远为零模型的拟合能力会大打折扣。我给学生时代的一个朋友打比方权重是“这件事有多影响你”偏置是“你本来是什么态度”。同样一堆信息不同偏置的神经元会给出不同的起点反应这对训练多样化的特征非常重要。一个神经元本身只能画一条直线或者严格地说只能划分一个线性边界。真正让神经网络强大起来的是把这些神经元堆叠成层并在层与层之间塞进非线性激活函数。2.2 激活函数到底在“激活”什么如果网络里全是线性变换那么无论堆多少层整体仍然是一次线性变换。这就好比把多张透明胶片叠在一起每张只做简单偏移叠再厚也变不成一个弯曲的形状。激活函数就是负责“把直线掰弯”的工具让网络有能力拟合复杂边界。在我学习的过程中三个经典激活函数值得一个个吃透Sigmoid把输出压到0到1之间早期网络很爱用。但它的导数在两端趋近于0很容易导致梯度消失而且输出不是零均值会拖慢收敛。Tanh把输出压到-1到1之间零均值特性比Sigmoid好。但两端饱和的问题依然存在。ReLU正区间梯度恒为1负区间直接置0。计算简单、收敛快是当前很多网络的基础选择。缺点也很明显一旦输入变成负数梯度就变成0这个神经元可能“死”掉。我第一次看到ReLU的“死亡”问题时感到很抽象后来亲手搭了一个网络发现当学习率设置过大时损失直接变NaN检查参数发现大量神经元的输出恒为负数这就叫“神经元死亡”。从那以后我对学习率、初始化这类看起来像“玄学”的东西再也不敢随便拍脑袋。2.3 损失函数为什么决定网络的学习方向损失函数是整个网络“努力的方向标”。方向标设错了结构再精巧也很难训出好效果。对于回归任务均方误差MSE天然直观预测值与真实值差得越远惩罚越大。但把它挪到分类任务上就会出问题。分类任务输出的是“属于每个类别的概率”我们期望的是概率分布尽量接近真实标签的分布此时用交叉熵更合适。原因在于交叉熵对概率差的梯度形式比较友好。用Sigmoid输出配合MSE时靠后的层误差会被Sigmoid导数压得很小导致前面层几乎学不到东西。我自己做了一个小实验写出损失对最后输出的导数分别看MSE和交叉熵在预测严重错误时的数值。发现交叉熵在预测严重错误时会给一个很大的梯度逼着网络快速纠正而MSE配合Sigmoid时梯度会被压得又平又缓。这也是为什么现在主流图像分类模型的标配是“Softmax输出 交叉熵损失”。3. 反向传播算法手推一遍才真正理解的梯度流动3.1 从链式法则到计算图反向传播一度是我最畏惧的部分因为一听“梯度流动”就觉得复杂。直到我把神经网络当成一张计算图才真正放松下来。正向传播就是按图从输入算到输出反向传播则是按相反方向用链式法则把损失函数对每个参数的梯度算出来。关键心法是每个节点只需要关心两件事——它自己关于输入的梯度以及它输出的梯度如何被上游传下来的因子作用。更直白的说梯度是“乘法”流过去的。损失对某个权重的梯度等于“损失对输出的梯度 × 输出对加权和的梯度 × 加权和对该权重的梯度”这三个因子相乘就完事。很多人觉得反向传播需要背一堆公式其实不需要。它是一个局部规则每个节点独立计算自己的“局部梯度”然后利用链式法则乘起来。这也是为什么即使是非常复杂的网络只要能拆成一个个基本运算单元反向传播都能一以贯之地工作。3.2 手算一个微型网络看梯度怎么一步一步更新我建议所有人不依赖任何框架手算一遍单个神经元。当时我用的例子是这样输入 (x1)初始权重 (w0.5)偏置 (b0.1)目标 (y1)激活函数用Sigmoid损失用MSE。先算前向[ z 0.5 \times 1 0.1 0.6 ] [ a \frac{1}{1 e^{-0.6}} \approx 0.6457 ] [ L \frac{1}{2}(0.6457 - 1)^2 \approx 0.0627 ]然后反向[ \frac{\partial L}{\partial a} a - y -0.3543 ] [ \frac{\partial a}{\partial z} a(1-a) \approx 0.2288 ] [ \frac{\partial z}{\partial w} x 1,\quad \frac{\partial z}{\partial b} 1 ]于是[ \frac{\partial L}{\partial w} -0.3543 \times 0.2288 \approx -0.0811 ] [ \frac{\partial L}{\partial b} -0.3543 \times 0.2288 \approx -0.0811 ]如果学习率取0.1权重更新后[ w 0.5 - 0.1 \times (-0.0811) 0.5081 ] [ b 0.1 - 0.1 \times (-0.0811) 0.1081 ]再把新参数代回去输出 (a) 会变大一点损失确实下降了。这一步走完我才真正明白“梯度下降”为什么是“下降”每次更新都沿着让损失减小的方向迈一小步。扩展到三层网络时只是把同样的法则重复多次。对权重 (W_1) 的梯度要经过输出层、隐层、激活函数一层层乘回来中间每一个因子都不能漏。我在纸上从 (W_2) 推到 (W_1)整整写了两页写完以后再看框架里的.backward()不再觉得那是黑魔法。3.3 第一次亲眼看到“梯度消失”手推完微型网络后我顺手算了一个五层全连接网络的前两层梯度发现数值小得惊人。原因在于每一层Sigmoid的导数最大才0.25多个0.25连乘之后浅层梯度会呈现指数级缩小。这就是“梯度消失”的直观来源不是算法错了而是数学上连乘必然导致的结果。我当时打印出各层梯度的平均值看到从输出层到输入层数值从 (10^{-1}) 一路掉到 (10^{-8})那种震动比看十篇博客都深刻。这也是后来ReLU以及残差结构出现的重要原因——它们都在想办法保持梯度的“通路”。4. 优化器、学习率与权重初始化训练不收敛的锅到底归谁4.1 三种梯度下降的取舍当我们有了每个参数的梯度下一步就是决定“怎么更新参数”。这里第一个岔路口就是梯度下降的三种形态全量梯度下降用整个数据集计算一次梯度方向最准但数据量大时慢得无法接受随机梯度下降每个样本算一次梯度就更新快但吵得厉害损失曲线会剧烈抖动小批量梯度下降取一小批样本算梯度再更新既有一定稳定性又比较高效是最常见的做法。我自己的体会是小批量大小本身就是超参数。批次太小梯度噪声大批次太大每个epoch更新次数少收敛变慢而且占用内存高。工程上常用32、64、128这些数值没有绝对的对错要在自己的数据规模上试一试。4.2 学习率调参实录大小学习率的两类惨案学习率可以说是所有超参数里最值得花时间调试的一个。我做过一次对比实验固定其他条件不变只改学习率学习率设为0.5时训练损失先下降然后剧烈震荡最后直接变成NaN。原因是步子迈得太大参数跳到了数值不稳定的区域碰到NaN后整个模型就废了。学习率设为0.00001时损失倒是稳定但下降速度慢得让人怀疑人生。训练了同样多的epoch准确率比正常的低一截而且看着曲线上“努力爬行”的样子你会明白什么叫做欠拟合。经过几轮尝试我最后把学习率调到0.001附近模型顺利收敛。经验是先按数量级去扫学习率比如从0.1、0.01、0.001、0.0001四档开始确定大致范围后再做细分。看到损失曲线像心电图一样乱跳别急着加正则化先看看是不是学习率太大。4.3 权重初始化为什么全零开局必翻车如果把所有权重初始化为0会发生一个很隐蔽的问题同一层所有神经元在第一次前向时输出完全相同反向时梯度也完全相同它们会永远保持对称无法分化成不同的特征检测器。换句话说这层有100个神经元实际只相当于1个神经元在学习模型的能力被白白浪费。稍微好一点的做法是随机初始化但随机范围同样要讲究。范围太大激活值容易进入饱和区梯度消失范围太小信号传不过几层就衰减没了。早期一种经典策略是根据本层神经元数量控制随机范围让每层输出的方差尽量保持不变。后来有人针对ReLU做了改进把初始化范围进一步放大因为ReLU会把一半的负区间归零需要更强的信号才能维持前向传播。我的实测经验是用默认的初始化策略大多数情况都能正常工作但如果你自己手写了一个“看起来更帅”的自定义初始化一定要先在极小网络上验证梯度数值别等到训练跑飞了才回头查。4.4 优化器的进化从普通梯度到动量再到自适应把梯度算出来以后“怎么用”还能再优化。普通梯度下降每次只根据这一步的梯度更新很容易在峡谷地形中来回震荡。动量的思路是把历史梯度方向当作惯性像一个小球从山坡滚下来遇到坑不会完全停住而是借助惯性冲出去。这个类比相当准确我当时用一个二次函数地形画图验证过动量确实能显著加快收敛。后来出现的自适应方法则进一步按参数的历史梯度大小调整更新步长频繁更新且梯度大的参数步长自动收小稀疏更新的参数步长自动放大。这类方法对学习率的敏感度比普通梯度下降低很多所以成了很多任务的默认选择。不过它并不总是最优如果你的任务比较经典、数据量也不大普通SGD配合动量配合精心调过的学习率有时候收敛到更好的局部最优点。我现在的选择策略很简单场景优先选择快速验证想法、调试模型自适应优化器学习率0.001起步图像分类、经典数据集、追求更高精度SGD配合动量结合学习率衰减稀疏特征、大规模参数自适应类方法表现会更省心5. 从全连接走向卷积CNN到底在解决什么问题5.1 全连接层在图像任务上的两个硬伤当我终于理解了全连接网络之后再回头看CNN很多设计就变得顺理成章了。全连接层处理图像时会遇到两个致命的尴尬。第一个是参数爆炸。一张普通的手写数字图片只有28×28像素展平后也有784个输入。如果隐藏层设128个神经元单这一层就有约10万个参数。到了更大尺寸的图片比如224×224全连接层几乎无法承担这样的计算量。第二个更加本质展平操作把像素的二维空间关系彻底打散了。图像里“相邻像素往往相关、远处像素往往无关”这个极其重要的先验信息被全连接层当作普通特征列表处理它看不到哪里是左边、哪里是右边。就好比你把一张照片剪成碎片再随机排列丢给人看人类也很难认出内容。CNN的提出就是针对这两个问题给出的结构性答案。5.2 卷积是怎么“看”图像的局部连接与权值共享卷积层的核心做法是让每个神经元只连接输入的一小块区域这块区域的尺寸就是卷积核大小。比如一个 (3\times3) 的卷积核只看输入图像对应位置周围9个像素。每个卷积核通过滑动窗口的方式扫过整张图同一组权重在不同位置重复使用这就是“权值共享”。我理解权值共享时用了一个很朴素的类比它像一把固定的金属印章在整个图像上到处盖章。不管印章落在图像左上角还是右下角用的都是同一个图案。所以一个卷积核能检测的是一种局部的视觉模式比如垂直边缘、水平边缘、颜色变化等。图像中任何位置出现这种模式都会被同一个“印章”捕获。这样做同时解决了参数爆炸问题一个 (3\times3) 卷积核只有9个权重加上偏置才10个参数。即使一个卷积层有几十个卷积核参数数量也远远小于全连接层。5.3 卷积层、池化层与感受野直觉层面的理解卷积层通常会配上两个概念填充和步长。填充的意思是在图像外围补一圈0让卷积核也能覆盖到边缘像素步长则是每次滑动窗口走几步步长越大输出特征图越小相当于抽稀。我一开始总是混淆这两个后来记住一句话填充管“边缘信息要不要保留”步长管“输出分辨率压缩多少”。池化层的作用则被很多人低估。最大池化就是在一个小区域内取最大值它带来的核心好处不是省算力而是让特征对细微的位移更鲁棒。图像里的目标稍微挪动几个像素池化之后的结果往往变化不大。每次池化还会让下一层的每块区域对应输入上更大范围也就是感受野扩大。深层卷积之所以能识别出“眼睛”“轮子”这种较大尺度的概念正是因为感受野越来越大能看到的信息越来越多。5.4 为什么CNN天然适合图像数据把前面的内容串起来CNN在图像上表现好的原因就很清楚了局部性利用相邻像素相关的先验只用少量参数捕捉局部模式平移不变性同一套卷积核扫遍全图目标出现在哪个位置都能被同样处理层次性浅层学到边缘、纹理等局部细节深层逐步组合成部件和整体概念这和人脑视觉皮层的处理方式有异曲同工之处。我当时做了个很幼稚的验证取一个训练好的边缘检测卷积核把它对应的权重画成热力图看它是不是真的在“寻找”某种方向的边界。果然不同卷积核的响应图确实各有侧重。那一刻之前抽象的概念终于落地了。6. 一次手写数字识别的完整训练记录跑通与踩坑复盘6.1 实验环境与数据准备先定好评估标准学习完基础后我决定用一个小而完整的数据集来验证自己的理解选来选去用了公开的手写数字数据集。它的好处很明显数据量适中、类别明确、图像小非常适合做实验。我在搭建实验时给自己定了几条纪律数据归一化到0到1之间避免大数值输入拖慢收敛划分训练集和验证集并固定随机种子保证对比可复现用统计准确率作为主要评估指标同时记录每个epoch的训练损失和验证损失。这些看起来琐碎但它们决定了实验结论是否可信。比如不做归一化模型训练时间会明显变长不固定随机种子每次结果波动很大你就很难判断改动到底有没有效果。6.2 基线模型先让训练跑起来再说我的基线模型是一个很常规的小型CNN两层卷积加池化最后接一个全连接层输出10个类别。优化器从自适应开始慢慢调初始学习率0.001批次大小32训练约10轮。第一次训练其实很顺利验证准确率很快爬到98%以上。可越是这样顺利我越觉得不能只停留在“跑通了”于是故意把一些参数改成“教科书上的错误用法”看看自己能不能排查出问题。结果这一折腾才有了一次印象深刻的踩坑经历。6.3 一次“训练不收敛”的完整排查链路我把学习率从0.001直接调到0.5其他条件不变结果训练损失一开始下降得飞快到第3个epoch左右突然变成NaN。当时我把这个现象记录为一次排查任务整个链路如下观察现象训练损失前几轮下降之后跳变到NaN验证全部为0。检查数据先确认数据集没有损坏标签没有错位归一化也正常。数据没问题。检查模型结构确认最后一层的输出维度等于类别数损失函数和激活函数匹配。也没问题。检查超参数发现学习率设置过大。参数更新幅度过大进入数值溢出区域损失变成NaN。最简验证我只用8张图片过拟合把学习率改回0.001模型迅速记住这批数据证明模型本身有学习能力。修复把学习率从0.5调回0.001训练正常收敛。排查完毕后我的收获很大所谓不收敛要先分清是数据问题、模型问题还是超参数问题而不是一股脑地换模型。最有效的手段是“最小化实验”——用极少数据让模型过拟合如果这个都做不到说明模型或数据管道有根本性问题。6.4 参数调整前后的对比学到了什么我把三次实验的结果放在一起做了一张让人印象深刻的对比表配置学习率训练结果验证准确率配置10.5损失震荡后NaN基本无效配置20.01损失缓慢下降有微震荡约85%配置30.001损失平稳下降98%以上这三组实验很好的说明了在模型结构足够合理的前提下学习率对结果的影响有多么显著。我也尝试过把卷积核数量翻倍、把全连接层砍掉发现准确率变化不大这让我意识到对于这类简单的数据集模型的瓶颈可能不在容量而在训练配置和数据处理细节。还有一个小发现验证损失开始回升但准确率还在上涨时往往就是快要过拟合的预警。这时候与其盲调不如先固定一个早停策略保存验证准确率最高的那一轮模型。这次系统过完神经网络基础再回头看CNN整个知识框架终于立起来了。我不再觉得卷积是某个框架帮我封装好的“魔法函数”而是一种基于图像先验的、可解释的参数共享结构。最后分享一个一直在用的小技巧学完任何一个概念强制自己用不超过三句话讲给一个不懂技术的人听如果讲得通就说明你大概真懂了如果讲不通别急着往下学先把那个卡点解决了。写这篇记录本身就是我的“讲给外行人听”的练习效果比我预期要好得多。