从零实现前馈神经网络:Python手写反向传播与训练全流程

发布时间:2026/10/26 23:03:53
从零实现前馈神经网络:Python手写反向传播与训练全流程 1. 从零搭建前馈神经网络为什么我不推荐一上来就用框架很多人学神经网络第一步就是import torch或者from tensorflow import keras然后跑一个MNIST手写数字识别看到准确率98%就觉得自己学会了。我刚开始也是这么干的后来发现一个问题当网络不收敛、梯度爆炸、loss变成NaN的时候我完全不知道从哪里下手排查。因为框架把一切都封装好了我根本不知道反向传播到底在算什么。所以这个系列我决定用纯Python加NumPy从零把前馈神经网络搭一遍。这一篇是第六部分重点放在完整的前馈神经网络实现包括前向传播、反向传播、参数更新、以及一个能跑通的训练循环。核心关键词就两个Python和神经网络。适合已经了解基本Python语法、知道什么是矩阵乘法、但还没亲手写过反向传播的读者。如果你已经用过PyTorch但说不清loss.backward()背后发生了什么这篇也值得一看。我用的环境很简单Python 3.8以上NumPy装好就行。如果你还没装NumPy命令行里pip install numpy一条命令搞定。不需要GPU不需要CUDA一台普通笔记本就能跑完所有代码。整个实现大概两百行左右但我会把每一行的意图都讲清楚让你看完能自己默写出一个能训练的前馈网络。2. 前馈神经网络的核心结构拆解2.1 为什么选择全连接前馈网络作为入门前馈神经网络Feedforward Neural Network是最基础的神经网络结构信息从输入层单向流向输出层没有循环、没有反馈。它的数学形式非常干净每一层做的就是输出 激活函数(输入 × 权重 偏置)。没有卷积的权值共享没有RNN的时间展开所有复杂度都集中在矩阵乘法和链式求导上。我选它作为从零实现的第一站原因有三个。第一它的反向传播推导可以手算你能在白板上把梯度公式推出来这对建立直觉至关重要。第二它的代码结构最清晰前向和反向几乎是对称的写完之后你再看CNN或者LSTM会发现它们只是在“层”的层面上做了替换。第三它能解决很多实际问题比如简单的分类、回归、甚至一些特征工程之后的预测任务不是玩具。注意不要觉得前馈网络“太简单”就跳过。我见过太多人直接上CNN做图像分类结果连全连接层的梯度都推不明白调参全靠试。基础打牢了后面学卷积神经网络、循环神经网络、图神经网络都是水到渠成的事。2.2 网络层数与神经元数量的选择逻辑假设我们要做一个二分类任务输入特征是4维的输出是1个概率值。一个典型的结构是输入层4个节点隐藏层1有8个节点隐藏层2有4个节点输出层1个节点。为什么这么选隐藏层的节点数没有严格的数学公式但有一些经验规则可以参考。隐藏层节点数通常取输入维度和输出维度之间的某个值常见做法是取2的幂次比如8、16、32、64。隐藏层层数从1层开始试如果欠拟合再加层。对于这个4维输入的二分类问题8和4的隐藏层已经足够再多就容易过拟合。我试过用128个节点的单隐藏层训练集准确率100%但验证集只有70%出头典型的过拟合。激活函数的选择也有讲究。隐藏层用ReLU因为它的导数在正区间恒为1不会出现梯度消失而且计算极快。输出层用Sigmoid因为二分类需要输出0到1之间的概率。如果是多分类输出层换成Softmax。这些选择不是随意的背后都有梯度传播的考量后面讲反向传播的时候你会看到原因。2.3 参数初始化为什么全零初始化是灾难权重初始化是很多人忽略的一步。如果你把所有权重初始化为0那么同一层的所有神经元在前向传播时输出完全相同反向传播时梯度也完全相同更新之后还是完全相同。这意味着无论隐藏层有多少个神经元它们都在学同一个东西网络退化成线性模型。正确的做法是随机初始化。我常用的是He初始化也叫Kaiming初始化专门配合ReLU激活函数。具体做法是权重从均值为0、标准差为sqrt(2 / 输入维度)的正态分布中采样。偏置初始化为0。为什么是2/n而不是1/n因为ReLU把一半的输入置零了方差需要乘2来补偿。这个细节在何恺明2015年的论文里有详细推导实测下来用He初始化比用小的随机数收敛快很多。import numpy as np def he_init(input_dim, output_dim): std np.sqrt(2.0 / input_dim) W np.random.randn(input_dim, output_dim) * std b np.zeros((1, output_dim)) return W, b这段代码里input_dim是上一层神经元数量output_dim是当前层神经元数量。np.random.randn生成标准正态分布乘以std调整方差。偏置用np.zeros初始化为0形状是(1, output_dim)方便后面广播加法。3. 前向传播与反向传播的完整实现3.1 前向传播数据如何从输入流到输出前向传播就是数据从输入层经过每一层的线性变换和激活函数最终到达输出层。假设第l层的权重是W[l]偏置是b[l]激活函数是f那么第l层的输出A[l] f(Z[l])其中Z[l] A[l-1] × W[l] b[l]。输入层的A[0]就是原始特征矩阵X。我习惯把每一层的中间结果缓存下来因为反向传播需要用到Z和A。缓存的内容包括每一层的Z、每一层的A、以及每一层的输入A_prev。这些在反向传播时直接取用不用重新计算。def forward(X, params): caches [] A X L len(params) // 2 # 参数是成对的 W,b for l in range(1, L): A_prev A W params[fW{l}] b params[fb{l}] Z np.dot(A_prev, W) b A np.maximum(0, Z) # ReLU caches.append((A_prev, W, b, Z)) # 最后一层用Sigmoid W params[fW{L}] b params[fb{L}] Z np.dot(A, W) b A 1.0 / (1.0 np.exp(-Z)) caches.append((A_prev, W, b, Z)) return A, caches这里有个细节循环里range(1, L)只处理隐藏层最后一层单独处理因为激活函数不同。np.maximum(0, Z)就是ReLU比np.where(Z 0, Z, 0)快一些。Sigmoid用1/(1exp(-Z))直接算注意当Z很大时exp(-Z)会趋近0不会溢出当Z很小时exp(-Z)会很大可能溢出但NumPy会返回inf1/(1inf)等于0结果正确。3.2 反向传播链式法则的代码落地反向传播是整个神经网络最核心也最容易写错的部分。它的本质就是链式法则从损失函数开始逐层往回求梯度。对于二分类的交叉熵损失输出层的梯度有一个非常简洁的形式dZ[L] A[L] - Y。这个结果不是巧合是Sigmoid加交叉熵的导数恰好抵消了推导过程在之前的文章里详细写过。隐藏层的梯度计算稍微复杂一点dZ[l] (dA[l] * relu_derivative(Z[l]))其中dA[l] dZ[l1] × W[l1].T。然后dW[l] A[l-1].T × dZ[l] / mdb[l] sum(dZ[l]) / m。除以m是因为我们对整个batch的损失取了平均。def backward(Y, caches, params): grads {} L len(caches) m Y.shape[0] # 输出层 A_prev, W, b, Z caches[-1] A 1.0 / (1.0 np.exp(-Z)) dZ A - Y grads[fdW{L}] np.dot(A_prev.T, dZ) / m grads[fdb{L}] np.sum(dZ, axis0, keepdimsTrue) / m dA_prev np.dot(dZ, W.T) # 隐藏层反向 for l in reversed(range(1, L)): A_prev, W, b, Z caches[l-1] dZ dA_prev * (Z 0) # ReLU导数 grads[fdW{l}] np.dot(A_prev.T, dZ) / m grads[fdb{l}] np.sum(dZ, axis0, keepdimsTrue) / m dA_prev np.dot(dZ, params[fW{l}].T) return gradsZ 0返回布尔数组乘以dA_prev就实现了ReLU的导数正区间为1负区间为0。注意dA_prev在循环中不断被覆盖从后往前传播。keepdimsTrue保证db的形状是(1, output_dim)和b一致方便后续减法。3.3 参数更新与学习率的选择参数更新就是梯度下降W W - learning_rate * dWb b - learning_rate * db。学习率是最重要的超参数没有之一。太大震荡不收敛太小收敛慢。我一般从0.01开始试如果loss震荡就降到0.001如果loss下降太慢就升到0.05。def update_params(params, grads, learning_rate): L len(params) // 2 for l in range(1, L1): params[fW{l}] - learning_rate * grads[fdW{l}] params[fb{l}] - learning_rate * grads[fdb{l}] return params实测下来对于这个4维输入的二分类问题学习率0.1能很快收敛大概500个epoch就能到95%以上的准确率。但学习率不是固定的后面可以加学习率衰减每100个epoch乘以0.9效果更稳。4. 完整训练流程与实操记录4.1 数据准备与预处理我用一个模拟数据集来演示生成1000个样本每个样本4个特征标签由特征的线性组合加噪声决定。这样做的原因是真实数据往往需要清洗和标准化而模拟数据可以让我们专注于网络本身。np.random.seed(42) m 1000 X np.random.randn(m, 4) true_w np.array([[1.5], [-2.0], [0.5], [1.0]]) true_b 0.3 logits np.dot(X, true_w) true_b prob 1.0 / (1.0 np.exp(-logits)) Y (prob 0.5).astype(float)这里Y是0或1的标签形状(1000, 1)。注意true_w的形状是(4, 1)和网络输出层维度一致。生成之后我习惯检查一下正负样本比例如果严重不平衡需要调整这里大概各占一半没问题。提示实际项目中输入特征一定要做标准化均值0方差1。否则不同量纲的特征会导致梯度更新方向偏向数值大的特征收敛很慢。标准化用(X - X.mean(axis0)) / X.std(axis0)一行搞定。4.2 训练循环与损失监控训练循环就是前向、计算损失、反向、更新重复若干轮。我每100个epoch打印一次loss和准确率观察收敛情况。def compute_loss(A, Y): m Y.shape[0] epsilon 1e-8 loss -np.sum(Y * np.log(A epsilon) (1 - Y) * np.log(1 - A epsilon)) / m return loss def train(X, Y, layers_dims, learning_rate0.1, epochs2000): params {} for l in range(1, len(layers_dims)): params[fW{l}], params[fb{l}] he_init(layers_dims[l-1], layers_dims[l]) losses [] for i in range(epochs): A, caches forward(X, params) loss compute_loss(A, Y) grads backward(Y, caches, params) params update_params(params, grads, learning_rate) if i % 100 0: pred (A 0.5).astype(float) acc np.mean(pred Y) losses.append(loss) print(fEpoch {i}, Loss: {loss:.4f}, Acc: {acc:.4f}) return params, lossesepsilon是防止log(0)出现NaN。layers_dims是列表比如[4, 8, 4, 1]表示输入4维两个隐藏层分别8和4输出1维。训练2000轮每100轮打印一次。实测下来大概300轮左右loss就降到0.1以下准确率超过95%。4.3 训练结果分析与可视化训练完成后我把loss曲线画出来横轴是epoch纵轴是loss。如果曲线平滑下降说明学习率合适如果震荡说明学习率偏大如果下降很慢说明学习率偏小或者初始化有问题。import matplotlib.pyplot as plt params, losses train(X, Y, [4, 8, 4, 1], learning_rate0.1, epochs2000) plt.plot(range(0, 2000, 100), losses) plt.xlabel(Epoch) plt.ylabel(Loss) plt.title(Training Loss Curve) plt.show()画图的时候如果横坐标太密集可以用plt.xticks(rotation45)旋转标签或者用plt.locator_params(axisx, nbins10)减少刻度数量。这些是小技巧但能让图表更易读。5. 常见问题与排查技巧实录5.1 Loss变成NaN的三种原因与修复Loss变NaN是最常见的问题我遇到过至少三种原因。第一种是学习率太大导致参数更新步长过大权重爆炸Z变成infSigmoid输出0或1log(0)就是负无穷。修复方法是降低学习率从0.1降到0.01甚至0.001。第二种是输入数据没有标准化某些特征数值极大导致Z溢出。修复方法是标准化输入。第三种是log(0)没有加epsilon修复方法是在np.log里面加一个很小的数。注意如果loss已经变成NaN不要指望它自己恢复。必须停止训练检查数据、学习率、初始化然后重新开始。我一般会在训练循环里加一个判断if np.isnan(loss): print(Loss is NaN, stopping); break。5.2 梯度消失与梯度爆炸的判断方法梯度消失的表现是靠近输入层的权重几乎不更新loss下降非常慢准确率卡在某个值上不去。梯度爆炸的表现是loss剧烈震荡甚至变成NaN权重数值变得极大。判断方法很简单在反向传播之后打印每一层梯度的范数np.linalg.norm(grads[fdW{l}])。如果某一层的梯度范数接近0就是消失如果超过1000就是爆炸。对于前馈网络梯度消失通常发生在层数较多且使用Sigmoid激活函数时。解决方案是换ReLU或者用Batch Normalization。梯度爆炸可以用梯度裁剪dW np.clip(dW, -5, 5)把梯度限制在[-5, 5]之间。我实测下来对于这个4维输入的小网络梯度裁剪基本用不上但层数增加到10层以上就很有必要。5.3 过拟合与欠拟合的快速诊断过拟合的判断标准是训练集准确率很高比如99%但验证集准确率明显低比如80%。欠拟合是两者都低。诊断方法很简单把数据分成训练集和验证集训练过程中同时监控两个准确率。现象训练集准确率验证集准确率原因解决方案过拟合99%80%模型太复杂数据太少加正则化、Dropout、减少层数或节点数欠拟合70%68%模型太简单特征不够加层数、加节点、加特征正常95%93%模型容量合适继续训练或调学习率我试过在隐藏层加L2正则化就是在loss里加上lambda * sum(W**2)lambda取0.01。效果是训练集准确率从100%降到96%验证集从80%升到90%明显缓解了过拟合。Dropout也可以但实现起来稍微麻烦一点需要在训练时随机置零一部分神经元测试时乘以保留概率。5.4 参数不更新的排查清单有时候训练半天loss不动参数也不更新。我整理了一个排查清单按顺序检查学习率是否为0检查learning_rate变量。梯度是否全为0打印grads[dW1]看看。数据标签是否全为同一类np.unique(Y)检查。前向传播是否出错单独跑一次forward看输出。权重初始化是否全为0检查he_init是否被正确调用。最常见的是第3条标签全0或全1导致网络学不到东西。我有一次用模拟数据忘了加噪声标签完全由线性组合决定网络很快就学到100%但换成真实数据就不行了。所以数据检查永远是第一步。6. 从零实现到框架使用的过渡建议写完这个从零实现之后你再看PyTorch或者TensorFlow的代码会发现它们做的事情一模一样nn.Linear就是W和bforward就是前向传播loss.backward()就是反向传播optimizer.step()就是参数更新。区别只是框架帮你处理了GPU加速、自动微分、以及各种层的封装。我的建议是先用NumPy手写一遍确保你能推导出每一层的梯度公式能解释为什么输出层用Sigmoid加交叉熵能说清楚He初始化的方差为什么是2/n。然后再用框架复现同样的网络对比两者的结果是否一致。如果一致说明你真的懂了如果不一致回去检查你的实现。这个从零实现的前馈网络虽然简单但它是理解更复杂网络的基础。卷积神经网络只是在全连接层前面加了卷积层和池化层循环神经网络只是在全连接层之间加了时间维度的循环图神经网络只是在聚合邻居信息时用了不同的权重矩阵。核心的反向传播逻辑没有变。最后分享一个小技巧如果你在调试梯度的时候不确定自己算得对不对可以用数值梯度检验。对某个参数W[i][j]计算(loss(W[i][j]epsilon) - loss(W[i][j]-epsilon)) / (2*epsilon)和反向传播算出来的梯度对比误差应该在1e-7以内。这个方法虽然慢但能帮你定位到具体哪一层的梯度算错了。我每次写新的层都会用这个方法验证一遍比盲目调参高效得多。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询