
简介资源为一份使用Python与NumPy从零实现的全连接神经网络代码包面向希望理解深度学习基础原理的初学者与数据科学爱好者解决入门阶段对反向传播、梯度下降、激活函数与损失函数等核心概念难以落地的问题。压缩包共7个文件全部为py脚本体积仅4KB按功能拆分清晰涵盖激活函数定义、随机权重初始化、前向传播计算、均方误差损失、反向传播权重更新、训练循环与预测等完整封装模块结构紧凑便于逐行阅读与二次修改。目前已有4211人学习参考。通过这份极简实现读者可清晰观察网络从随机权重开始、经多轮迭代逐步逼近目标输出的全过程直观理解链式法则在反向传播中的实际运用掌握多层感知器的搭建逻辑并能为后续转向TensorFlow、PyTorch等成熟框架打下扎实的数学与工程基础。1. 简单全连接神经网络 python 实现为什么从零手写反而更值得做很多朋友学深度学习第一步接触的就是全连接神经网络。但大多数人是在 PyTorch 或 Keras 里点两下就搭好了真正被问到梯度是怎么传回去的时基本只能说出一个反向传播的名字。这个标题要解决的问题很直接不依赖任何深度学习框架用纯 Python 和 NumPy 把全连接神经网络的训练过程一行行写出来让它在自己的电脑上真正跑起来。我接触过的场景无非三种课程设计或者实验报告要求提交纯 Python 实现面试手撕代码时被问到反向传播以及那些调包调了很久、始终觉得网络是个黑匣子的入门者。下面就从数学推导到可运行代码再到训练分类器时最常见的几个坑一路讲清楚。2. 全连接网络的数学结构前向传播和反向传播到底在算什么2.1 神经元、权重与偏置一个全连接层就是一次矩阵乘法全连接网络这个名字的由来就是相邻两层的每个神经元之间都有连接。假设输入层有 d 个特征隐藏层有 h 个神经元那么输入和隐藏层之间的权重矩阵 W 的形状就是 d×h偏置 b 是长度 h 的向量。一个神经元的输出是输入向量的加权和加上偏置再经过激活函数。把所有神经元合在一起看这一层就是一个矩阵乘法加一个偏置加法在 Python 里就是 np.dot(X, W) b。用代码表达会更清楚。假设我们有 4 个样本、3 个特征隐藏层 5 个神经元import numpy as np X np.random.randn(4, 3) # 4个样本3个特征 W np.random.randn(3, 5) * 0.1 # 输入维度3 - 隐藏层维度5 b np.zeros(5) z np.dot(X, W) b # 线性变换形状是 (4, 5) a 1 / (1 np.exp(-z)) # sigmoid 激活逐元素操作这里 np.dot(X, W) 是前向传播的核心它把 X 的每一行一个样本与 W 的每一列一个隐藏神经元对所有输入的权重做内积结果形状是 (4, 5)。偏置 b 在加法时会自动广播到每个样本上这是 NumPy 的广播机制手写时非常省事。真正需要记住的是无论网络多少层每一层都在做线性变换加激活这两件事层数越多非线性表达能力越强。在 Python 里实现全连接网络最难的不是写循环而是把每个张量的形状搞清楚。我见过很多新手在 np.dot 的时候把维度的顺序写反结果要么报错要么得到完全错误的结果。判断标准就一条上一层输出的第一维是样本数第二维是上一层神经元数下一层权重的第一维必须等于上一层神经元数第二维是下一层神经元数。2.2 损失函数与反向传播链式法则把误差拆回每个权重反向传播要解决的问题是当预测结果和真实标签有差距时这个差距应该由哪些权重来负责、各自负责多少。一切从损失函数开始。分类问题最常用的损失是交叉熵但为了把代码写得直观入门阶段用均方误差也是可以的。均方误差对输出层的导数非常干净刚好是预测值减真实值。假设网络只有输出层和一层隐藏层。记损失为 L输出层权重为 W2隐藏层权重为 W1。根据链式法则L 对 W2 的梯度等于 L 对输出 z2 的梯度乘以 z2 对 W2 的梯度其中 z2 a1·W2 b2。而 a1 又是上一层的激活输出。这种一层一层往回推的过程就是反向传播名字的来源误差从输出层出发沿着计算路径反向流动每经过一个节点就乘以对应的局部导数。在实现上反向传播通常分三步。第一步用前向传播时缓存下来的中间结果 z1、a1、z2第二步计算输出层误差 delta2第三步把 delta2 通过 W2 传回隐藏层得到 delta1然后分别对 W1、W2、b1、b2 求梯度。这里最关键的体会是前向传播缓存了什么反向传播才能对什么求导。很多手写实现跑不通就是因为漏存了某层的 z 或者 a导致反向传播时没有地方取用。我一般会把反向传播写成和 forward 对称的步骤每一层都在 forward 里保存一份当时的样子backward 里再按相反顺序取用。这虽然会占一点内存但换来的可读性和排错效率非常值。缓存的东西就三样每层的线性输出 z、激活输出 a、以及输入的原始样本 X。反向传播里最容易写错的是转置。计算 dW2 时要用 a1.T 去乘 delta2而不是直接 a1 乘 delta2。原因是梯度矩阵的每个元素是损失对某个权重的偏导权重 W2 的形状是 (hidden_dim, output_dim)梯度形状必须和它一致。a1 的形状是 (m, hidden_dim)delta2 是 (m, output_dim)要让两者相乘得到 (hidden_dim, output_dim)只能把 a1 转置成 (hidden_dim, m)。这个规则可以总结成一句话权重梯度的形状永远和权重本身一样哪个维度对不上就转置哪个矩阵。2.3 激活函数怎么选sigmoid、tanh 还是 ReLU全连接网络里激活函数决定了神经元怎么响应。最经典的是 sigmoid输出范围在 0 到 1 之间天然适合二分类的输出层。但它有个明显的毛病输入绝对值很大时导数趋近于 0也就是梯度饱和多层网络训练起来很慢。tanh 把输出范围压到 -1 到 1均值更接近 0收敛通常比 sigmoid 快。ReLU 则更简单粗暴负数直接置 0正数原样通过计算快、梯度不容易消失但可能让神经元永久死掉。对于手写入门我的建议是隐藏层优先选 tanh 或者 ReLU输出层根据任务选。二分类输出层用 sigmoid多分类用 softmax。为了简单也可以先用 sigmoid 加 one-hot 标签凑合但严格说 softmax 才是正路。第 3 章里我会用 sigmoid 做隐藏层因为它的导数形式最简单方便验证反向传播的正确性真正跑手写数字分类时再换成 tanh 或者 ReLU。换激活函数只需要改 forward 里的函数和 backward 里的导数其余代码完全不用动这也是把网络写成类的好处。3. 用 numpy 从零写一个三层全连接网络直接能跑的最小代码3.1 环境准备python 安装与 numpy 库安装开始前确保机器上有能用的 Python 环境。很多新手卡在第一步装完 Python 后命令行输入 python 没反应多半是安装时没勾选 Add Python to PATH也就是环境变量没配置好。在 Windows 上安装时勾选 Add Python 3.x to PATHLinux 上一般自带macOS 用 Homebrew 装也可以。装完验证一下python --version pip install numpy python -c import numpy; print(numpy.__version__)第一行确认 Python 版本第二行安装唯一的第三方依赖 NumPy第三行验证安装结果。全连接网络的手写实现只依赖 NumPy不需要 PyTorch 也不需要 TensorFlow。如果 pip 安装很慢可以换成pip install numpy -i https://pypi.tuna.tsinghua.edu.cn/simple这样的国内镜像源这是常规操作。装好后把导入写在第一行import numpy as np。这个库提供了我们需要的数组运算、随机数初始化、矩阵乘法尤其是 np.random.randn 和 np.dot。注意如果你用的是 Anacondanumpy 通常已经内置了只需要确认环境激活正确。不需要为了这个项目单独建虚拟环境全局环境完全够用。3.2 网络类骨架初始化、前向、反向、更新一把梭接下来是核心代码。我把网络写成最简单的类包含三个方法init负责构建网络结构forward 计算输出backward 计算梯度并更新参数。这里为了减少概念负担直接使用全批量梯度下降每一步都用全部训练样本计算梯度。import numpy as np class SimpleFCNet: def __init__(self, input_dim, hidden_dim, output_dim, lr0.5): self.lr lr self.W1 np.random.randn(input_dim, hidden_dim) * 0.5 self.b1 np.zeros(hidden_dim) self.W2 np.random.randn(hidden_dim, output_dim) * 0.5 self.b2 np.zeros(output_dim) def sigmoid(self, x): return 1 / (1 np.exp(-x)) def sigmoid_deriv(self, x): s self.sigmoid(x) return s * (1 - s) def forward(self, X): self.z1 np.dot(X, self.W1) self.b1 self.a1 self.sigmoid(self.z1) self.z2 np.dot(self.a1, self.W2) self.b2 self.a2 self.sigmoid(self.z2) return self.a2 def backward(self, X, y): m X.shape[0] delta2 (self.a2 - y) * self.sigmoid_deriv(self.z2) dW2 np.dot(self.a1.T, delta2) / m db2 np.sum(delta2, axis0) / m delta1 np.dot(delta2, self.W2.T) * self.sigmoid_deriv(self.z1) dW1 np.dot(X.T, delta1) / m db1 np.sum(delta1, axis0) / m self.W1 - self.lr * dW1 self.b1 - self.lr * db1 self.W2 - self.lr * dW2 self.b2 - self.lr * db2 def train(self, X, y, epochs10000, print_every1000): for epoch in range(epochs): output self.forward(X) loss np.mean((output - y) ** 2) self.backward(X, y) if epoch % print_every 0: print(fepoch {epoch}, loss {loss:.6f})这段代码里的 sigmoid_deriv 是对前向传播中保存的 z 求导而不是对 a 求导这是一个关键细节。sigmoid 的导数是 s(z)·(1-s(z))这里的 z 必须用 forward 里缓存的 z1、z2。如果误用了激活后的 a 去算梯度方向就会出错。另一个细节是除以 m因为用的是全批量梯度下降每个权重的梯度是全部样本梯度的平均不除 m 的话学习率就得依赖批量大小来调不同批量下的最优学习率对不上。初始化时 W1、W2 用 np.random.randn 乘以 0.5让权重落在比较小的范围。如果不乘 0.5randn 的标准差是 1输入经过多层线性组合后进入 sigmoid 的 z 很容易跑到正负十几的区间sigmoid 在那里已经完全饱和梯度趋近于 0参数更新不动。乘以 0.5 后 z 落在一个相对温和的区间梯度能正常传输。3.3 用 XOR 做最小验证四个样本让网络学出非线性神经网络最经典的入门验证是异或问题输入是 00、01、10、11输出是 0、1、1、0。这个映射是线性不可分的单层感知机永远学不会但带一层隐藏层的全连接网络可以学。把上面代码和数据拼在一起X np.array([[0, 0], [0, 1], [1, 0], [1, 1]], dtypefloat) y np.array([[0], [1], [1], [0]], dtypefloat) net SimpleFCNet(input_dim2, hidden_dim4, output_dim1, lr0.5) net.train(X, y, epochs20000, print_every5000) print(预测结果:) print(np.round(net.forward(X), 3))我实际跑过很多次lr0.5、hidden_dim4、20000 轮时 loss 能降到 0.001 左右输出的四舍五入结果正好是 0、1、1、0。如果 loss 下不去第一件事不是加层数而是看学习率。XOR 这种小数据把 lr 调大到 1.0 也常见。为什么这个小实验值得做因为它能一次性验证三点前向传播是不是真的实现了非线性变换反向传播的梯度方向对不对以及参数更新逻辑有没有写反。任何一个地方出错XOR 的 loss 都会停在 0.25 附近那正好是四个样本平均 loss 的随机水平。3.4 把训练循环改成批处理小批量梯度下降怎么嵌入现有代码上面的 train 方法每次都用所有样本算梯度手写数字集有 1797 个样本时没问题但如果数据量到几万全批量梯度下降每一步都算全部样本计算量很大。常见的做法是改成小批量梯度下降每次从训练集里抽一小批比如 32 或 64 个样本算这批的梯度并更新一次。修改其实很小只需要在 train 里加一层按 batch 切分数据的循环def train_batch(self, X, y, epochs100, batch_size32, print_every10): m X.shape[0] for epoch in range(epochs): perm np.random.permutation(m) X_shuffled X[perm] y_shuffled y[perm] for start in range(0, m, batch_size): end start batch_size X_batch X_shuffled[start:end] y_batch y_shuffled[start:end] self.forward(X_batch) self.backward(X_batch, y_batch) if epoch % print_every 0: loss np.mean((self.forward(X) - y) ** 2) print(fepoch {epoch}, loss {loss:.6f})注意 backward 里已经没有输出参数了因为 forward 已经把 self.a2 算好并缓存住backward 直接用缓存。每次 epoch 开始时打乱数据顺序可以避免模型学到样本顺序里的虚假规律。这是全连接网络训练循环里的一个标准动作后面第 4 章会基于这个版本继续改。4. 让网络学会分类手写数字数据预处理与训练循环4.1 数据准备用 scikit-learn 自带的手写数字集做基准XOR 只能证明网络能学习真正的实用场景是多分类。这里最方便的数据集是 scikit-learn 自带的 load_digits1797 张 8×8 的手写数字图每张图展开成 64 维向量标签是 0 到 9。它不需要额外下载安装好 scikit-learn 后一行代码就能加载。如果你不想装 scikit-learn也可以自己准备一个小的 CSV 文件但 load_digits 作为验证基准最省事。from sklearn.datasets import load_digits from sklearn.model_selection import train_test_split digits load_digits() X digits.data.astype(float) y digits.target X X / 16.0 # 像素值范围是0到16缩放到0~1之间 def one_hot(labels, num_classes10): result np.zeros((labels.shape[0], num_classes)) result[np.arange(labels.shape[0]), labels] 1 return result Y one_hot(y) X_train, X_test, Y_train, Y_test train_test_split(X, Y, test_size0.2, random_state42) print(X_train.shape, X_test.shape)这段代码做了三件事。第一把像素值除以 16 归一化到 [0, 1]避免输入范围太大导致梯度不稳。第二把标签从整数 09 转成 one-hot 向量数字 3 变成[0,0,0,1,0,0,0,0,0,0]这样每个输出神经元对应一个类别。第三按 8:2 划分训练集和测试集test_size0.2random_state42 是为了让结果可复现。这里最容易犯的错是忘记转 float整数数组在归一化时会被截断成 0。4.2 网络结构调整输出层改成 10 个神经元前面的 SimpleFCNet 输出维度是 1多分类需要把 output_dim 改成 10输出一个 10 维向量每个维度代表模型对该类别的置信程度。训练时预测结果和 one-hot 标签的差异通过损失函数体现。用均方误差在入门阶段完全可以但更标准的做法是交叉熵。这里为了复用第 3 章的代码我继续用均方误差只是把输出层改成 sigmoid这不影响理解全连接网络的实现逻辑。net SimpleFCNet(input_dim64, hidden_dim32, output_dim10, lr0.1) net.train_batch(X_train, Y_train, epochs200, batch_size32, print_every50)这里 hidden_dim 选了 32。为什么不是越大越好隐藏层越大模型能拟合的函数越复杂但在数据量只有 1797 的情况下隐藏层到 128 以上容易过拟合训练集 loss 降得很低、测试集准确率却不再上升。32 是一个对这个小数据集友好、训练速度也可接受的起点。如果你想验证这一规律可以把 hidden_dim 分别设成 8、32、128对比最终测试准确率这会让你直观感受到容量和过拟合的关系。学习率从第 3 章的 0.5 降到 0.1是因为小批量梯度下降的单次梯度更加嘈杂每批样本的梯度方向和全批量不完全一致步子太大容易震荡。这一改动背后没有玄学主要靠观察 loss 曲线来判断如果前 50 轮的 loss 在反复跳动而不是平滑下降就把学习率再除以 10。每个数据集的最优学习率都需要自己试这是手写网络时要接受的常态。4.3 测试集评估准确率从哪一行代码开始算训练结束后用训练好的参数跑一遍测试集把输出层每个样本的 10 个值里最大的那个当作预测类别和真实类别比较。pred net.forward(X_test) pred_labels np.argmax(pred, axis1) true_labels np.argmax(Y_test, axis1) accuracy np.mean(pred_labels true_labels) print(ftest accuracy: {accuracy:.4f})np.argmax 沿 axis1 取每个样本 10 个输出中最大值所在的索引这个索引正好是类别编号。准确率计算是预测对的数量除以总样本数。在我常用参数下这个简单网络在测试集上大概能做到 90% 到 93% 的准确率具体数值取决于初始化随机种子。很多人第一次把准确率算出来后会惊讶一个手写的、没有框架支持的全连接网络居然就能识别手写数字。但要注意训练时 loss 降得很低不代表测试准确率高这就要说到第 5 章的坑了。4.4 超参数的边界学习率、隐藏层大小、训练轮数之间怎么权衡手调这些超参数是理解神经网络最有价值的部分。学习率太大梯度更新一步迈过最优点loss 可能震荡甚至变成 nan太小训练半天 loss 缓慢下降。隐藏层大小和训练轮数之间也存在张力隐藏层越大模型越容易过拟合训练集于是你要减少轮数或者加正则隐藏层越小模型可能欠拟合loss 降不到该有的水平。我的经验是先固定一个不极端的值比如 hidden_dim32、lr0.1跑 200 轮看 loss 曲线形状。如果 loss 在一两百轮后还在快速下降说明轮数不够或者学习率偏小如果 loss 在前 20 轮就冲上去则说明学习率太大。还有一种更稳妥的做法把学习率设成 0.1、0.01、0.001 三档各跑一遍画三条 loss 曲线对比很快就能看出哪个区间最合适。后面我会专门讲怎么验证反向传播本身对不对因为一旦遇到所有超参数都不收敛问题大概率不在超参数而在梯度写错了。注意不要在训练循环里用测试集计算 loss 来指导训练过程那等价于把测试集泄漏给模型。测试集只在训练结束后用来评估一次。5. 全连接网络避坑指南5 个让我翻过车的常见问题5.1 学习率太大loss 一路飙到 nan现象训练刚开始几轮loss 输出就变成 nan或者从 0.2 跳到好几万之后所有输出都是 nan。原因学习率过大参数更新步子跨得太远导致某些权重的值变得极大下一轮前向传播算出来的中间值超出 sigmoid 的有效区间梯度也变成极大的数反复叠加后数值溢出成 inf最终变成 nan。这在手写网络里很常见因为全批量梯度下降的梯度是平均值如果某个样本的误差很大平均值的绝对值也可能很大。解决把学习率往小了调。我的经验是每次除以 10 来试0.5 出问题就换 0.1再不行就 0.01。同时检查特征是否做了归一化未归一化的输入和较大的学习率叠加是 nan 最常见的情侣组合。另外在代码里加一行 print 观察 loss 开始异常的位置能快速判断是第几轮出的事。还有一种防御式写法如果 loss 变成 nan直接重新初始化并调低学习率而不是硬着头皮继续跑。5.2 权重初始化不当训练几百轮 loss 还是纹丝不动现象loss 一直稳定在某个值附近比如 XOR 的 0.25不管跑多少轮都不下降但参数更新明明在执行。原因权重初始值太大比如直接用 np.random.randn(input_dim, hidden_dim) 而不做缩放。前向传播时 z 的方差会逐层放大sigmoid 的输入跑到饱和区导数接近 0反向传播算出来的梯度也接近 0。参数虽然每次都在减一个很小的数但几乎等于没更新loss 自然一动不动。这属于梯度消失的一种只不过来源不是网络层数深而是初始化不当。解决把随机初始化乘以一个小于 1 的缩放系数比如 0.1 或 0.5。更规范的做法是用 Xavier 初始化权重标准差设为sqrt(2 / (fan_in fan_out))在 Python 里可以写成np.random.randn(fan_in, fan_out) * np.sqrt(2 / (fan_in fan_out))。这个公式的目标是让每层输出的方差在向前传播和向后传播时都保持稳定。我手写网络时除非刻意验证否则都会用这个初始化。5.3 sigmoid 在深层网络中梯度消失换 ReLU 是最直接的后悔药现象隐藏层加到 3 层以上loss 在初期下降后迅速停滞或者根本降不动。浅层网络用得好好的 sigmoid堆到深层就不行了。原因sigmoid 导数的最大值只有 0.25而且输入稍微偏离 0输出就更快地接近 1 或 0。反向传播的误差每经过一层 sigmoid就要乘一个 0.25 以下的小数三层之后就衰减到原来的百分之几到不了靠近输入层的权重那些层几乎学不到东西。这不是代码 bug而是激活函数的数学性质。解决把隐藏层的激活函数换成 ReLU导数在正半轴恒为 1梯度可以顺畅地穿过很多层。换法很简单forward 里的self.a1 np.maximum(0, self.z1)backward 里对应的导数为(self.z1 0).astype(float)。不过 ReLU 也有自己的坑比如神经元死亡所以实际常用 Leaky ReLU 或者 tanh。对手写的小网络来说tanh 是一个比 sigmoid 稳妥、比 ReLU 少脾气的中庸选择。5.4 输入没有归一化一条特征主导了整个学习过程现象训练 loss 一直在下降但测试准确率很低或者 loss 曲线震荡得厉害。检查代码逻辑没发现任何问题。原因如果某个特征的取值范围是 0 到 16另一个是 0 到 1前向传播时大数值特征对应的权重梯度也会偏大网络会把几乎所有注意力放在这个特征上忽略其他信息。这就是不同量纲的特征在竞争量纲大的特征从一开始就占了优势。解决在把数据送进网络之前对每个特征做标准化减去均值除以标准差。对于像素类数据也可以像第 4 章那样简单除以最大值归一到 [0, 1]。标准化后每个特征的量纲一致梯度在各个方向上的量级接近训练会平稳很多。如果你观察 loss 曲线发现它在几个固定值之间反复横跳先检查标准化再检查学习率。5.5 one-hot 标签写错准确率卡在 90% 上不去还找不到原因现象测试集准确率停在 90% 左右loss 却已经降得很低。检查数据、网络、训练循环都没有明显 bug但准确率就是上不去。原因10 个类别的输出层用 sigmoid 加均方误差模型其实是在独立判断每个类别是或不是这和多分类输出层本来应该用 softmax 有理论上的差别但更常见的翻车点是对 one-hot 理解不深标签从 0 开始如果按 110 编码类别 10 就永远没有对应神经元类别 0 会被错误映射到第 1 个神经元。另一个翻车点是在计算准确率时 np.argmax 的轴用错把每一列都当成一个类别去取最大值了。解决首先打印 one-hot 后前 5 行的形状和数值确认第 i 个样本的标签 3 对应向量下标 3 处是 1。然后检查np.argmax(pred, axis1)的 axis 参数axis1 表示对每一行取最大值索引axis0 则会得到每个类别上最像的样本语义完全反了。最后如果你的最终目标是认真做多分类输出层建议换成 softmax 加交叉熵代码改动很小但对分类任务的收敛性质会好很多。6. 用梯度检查验证反向传播给自己留一张后悔药6.1 数值梯度法用定义直接算导数不依赖任何推导反向传播代码写错是很常见的事尤其是手写网络转置、维度、缓存这些环节任何一处出错结果都是训练不收敛或收敛到错误结果。但问题在于不收敛有很多原因学习率、初始化、数据都可能导致同样的表象。如果你不确定反向传播对不对有一个不依赖任何人经验的方法数值梯度检查。它的原理来自导数的定义。对某个参数 w它的梯度大约等于w 加一个小量时的损失减去 w 减一个小量时的损失除以两倍小量。这个小量通常取 1e-5。这个计算方式很慢每次只能校验一个参数但它完全独立于链式法则只依赖损失函数本身所以可以用来验证解析梯度的正确性。实现下来大概是def numeric_gradient(loss_fn, param, eps1e-5): grad np.zeros_like(param) it np.nditer(param, flags[multi_index], op_flags[readwrite]) while not it.finished: idx it.multi_index old param[idx] param[idx] old eps loss_plus loss_fn() param[idx] old - eps loss_minus loss_fn() param[idx] old grad[idx] (loss_plus - loss_minus) / (2 * eps) it.iternext() return grad这个函数遍历参数里的每一个元素对每个元素单独加一个小扰动算两次损失。注意函数末尾必须恢复 param 里的原值否则下一次迭代会污染后面的计算。使用方式先向前传播一次拿到解析梯度再调用 numeric_gradient 拿到数值梯度两者逐元素比较。相对误差的计算公式是diff np.abs(grad_analytic - grad_numeric) denom np.abs(grad_analytic) np.abs(grad_numeric) print(np.max(diff / denom))相对误差在 1e-4 以内说明反向传播基本可信1e-2 以上说明肯定有 bug。我一般会在网络刚写完时用一个很小的随机数据和很小的网络比如 input_dim2, hidden_dim3, output_dim2跑一遍梯度检查确认 W1、W2、b1、b2 四个参数全部通过再正式训练真实数据。这套流程救过我很多次有一次写循环神经网络时延迟项的行列顺序写反了全靠梯度检查定位到具体层。虽然这个标题下的全连接网络结构简单但用数值梯度验证解析梯度这个习惯几乎可以无成本平移到之后的任何网络实现里。希望这个技巧能帮你在手写网络的路上少走点弯路也省掉那些对着不收敛的 loss 干瞪眼的夜晚。本文还有配套的精品资源点击获取