简介这份资源面向深度学习入门者与希望夯实神经网络底层原理的开发者用纯Python与numpy从零实现一个简单的全连接神经网络多层感知器帮助读者摆脱对高层框架的依赖真正理解前向传播、反向传播与权重更新的计算过程。压缩包共7个文件全部为py脚本整体约4KB涵盖网络结构定义、公共数据处理、自研算法、预测与数据生成等模块结构轻量便于逐文件阅读与调试。资源围绕sigmoid激活函数、均方误差损失、链式法则求梯度等核心知识点展开读者可据此搭建可训练、可预测的完整流程并在此基础上尝试调整隐藏层规模、学习率与训练轮数观察模型表现变化。目前已有4213人学习下载适合作为课程实验、自学练手或进一步学习TensorFlow、PyTorch等框架前的过渡练习。1. 从零手写一个全连接神经网络一份能跑通的 Python 实现拆解很多人第一次接触深度学习都是从「调包」开始的——几行sklearn或torch代码就能跑出一个模型但反向传播到底怎么算、梯度从哪来、权重怎么更新脑子里始终是一团浆糊。这份「简单全连接神经网络 Python 实现」的资源走的是另一条路不依赖任何深度学习框架只用numpy从零把前向传播、损失函数、反向传播、参数更新全部手写一遍。它解决的不是「训一个高精度模型」的问题而是「让你真正看懂神经网络内部在干什么」的问题。适合刚学完机器学习基础、想动手验证公式的在校学生也适合工作几年、一直调包但想补底层原理的工程师。下面我按自己拆包复现的顺序把这份实现讲透。2. 网络结构设计与前向传播三层结构怎么定、矩阵维度怎么对拿到一份从零实现的全连接网络第一件事不是急着跑而是先搞清楚它的网络结构是怎么定义的。这份实现采用的是最经典的「输入层 → 一个隐藏层 → 输出层」三层结构隐藏层用 Sigmoid 或 ReLU 激活输出层根据任务类型选择 Softmax多分类或恒等映射回归。结构虽然简单但它是理解深层网络的地基——把三层的矩阵维度对齐了堆到十层只是循环的问题。2.1 权重初始化为什么不能全零以及 Xavier 的适用边界权重初始化是很多人翻车的第一站。如果你把所有权重初始化为 0那么同一层所有神经元的输出完全相同反向传播时梯度也完全相同网络永远学不到东西——这就是所谓的「对称性破缺失败」。这份实现里我一般会改成小随机数初始化常见做法是用np.random.randn乘以一个缩放因子。import numpy as np def init_params(n_input, n_hidden, n_output): # 使用缩放后的正态分布初始化避免初始输出过大导致 Sigmoid 饱和 # 缩放因子 0.01 是经验值输入维度大时可改用 sqrt(1/n_input) W1 np.random.randn(n_input, n_hidden) * 0.01 b1 np.zeros((1, n_hidden)) W2 np.random.randn(n_hidden, n_output) * 0.01 b2 np.zeros((1, n_output)) return W1, b1, W2, b2这里W1的形状是(n_input, n_hidden)b1是(1, n_hidden)偏置用零初始化是安全的因为权重已经打破了对称性。缩放因子取 0.01 是个保守值如果你的输入特征维度很大比如上千维0.01 会让第一层输出过小梯度消失这时候应该换成 Xavier 初始化即缩放因子取sqrt(1/n_input)。反过来如果隐藏层用 ReLUXavier 会偏小更适合 He 初始化sqrt(2/n_input)。这份实现默认 Sigmoid所以 0.01 够用但你要知道边界在哪。2.2 前向传播矩阵乘法与激活函数的串联前向传播的本质就是「线性变换 非线性激活」的反复堆叠。输入数据X形状为(n_samples, n_input)经过第一层权重得到隐藏层预激活值Z1再经激活函数得到A1最后经第二层得到输出Z2。def sigmoid(z): # 限制 z 的范围防止 exp 溢出这是数值稳定的常见做法 z np.clip(z, -500, 500) return 1 / (1 np.exp(-z)) def forward(X, W1, b1, W2, b2): # 第一层线性 激活 Z1 np.dot(X, W1) b1 # 形状 (n_samples, n_hidden) A1 sigmoid(Z1) # 第二层线性输出多分类时后面接 Softmax Z2 np.dot(A1, W2) b2 # 形状 (n_samples, n_output) return Z1, A1, Z2np.dot(X, W1)这一步要求X的列数等于W1的行数也就是n_input必须对齐这是最常见的维度报错来源。Z2是未经过 Softmax 的 logits训练时交给损失函数处理预测时再取argmax。把前向传播写成函数的好处是反向传播时可以直接复用Z1、A1这些中间变量不用重新算一遍。3. 损失函数与反向传播梯度推导和代码逐行对应前向传播只是把数据流过去真正让网络「学习」的是反向传播。这一章是整份实现的核心也是最多人卡住的地方——公式看得懂代码对不上。我按「损失 → 输出层梯度 → 隐藏层梯度 → 参数更新」的顺序把推导和代码一一对应讲清楚。3.1 交叉熵损失与 Softmax 的梯度合并多分类任务用交叉熵损失配合 Softmax 输出。单独看 Softmax 的导数很复杂但和交叉熵合并后对 logits 的梯度会化简成一个非常干净的形式dZ2 (Y_pred - Y_true) / n_samples。这个化简是反向传播里最漂亮的一步也是为什么分类任务偏爱交叉熵的原因。def softmax(z): # 减去每行最大值防止 exp 溢出这是标准数值稳定技巧 z z - np.max(z, axis1, keepdimsTrue) exp_z np.exp(z) return exp_z / np.sum(exp_z, axis1, keepdimsTrue) def compute_loss_and_grad_output(Z2, Y_true): n_samples Z2.shape[0] Y_pred softmax(Z2) # 交叉熵损失加 1e-8 防止 log(0) loss -np.sum(Y_true * np.log(Y_pred 1e-8)) / n_samples # Softmax 交叉熵合并后的梯度形式极简 dZ2 (Y_pred - Y_true) / n_samples return loss, dZ2Y_true是 one-hot 编码形状和Y_pred一致。dZ2的形状是(n_samples, n_output)直接就是第二层线性输出的梯度。注意这里除以了n_samples意味着损失是 batch 平均梯度也是平均梯度这样学习率就不用随 batch size 频繁调整。3.2 隐藏层梯度链式法则的代码落地拿到dZ2后往回推第二层权重梯度dW2、偏置梯度db2再穿过激活函数得到dZ1最后得到dW1、db1。链式法则在这里体现得淋漓尽致。def backward(X, Z1, A1, W2, dZ2): # 第二层参数梯度 dW2 np.dot(A1.T, dZ2) # (n_hidden, n_output) db2 np.sum(dZ2, axis0, keepdimsTrue) # 梯度回传到隐藏层 dA1 np.dot(dZ2, W2.T) # (n_samples, n_hidden) # Sigmoid 导数A1 * (1 - A1) dZ1 dA1 * A1 * (1 - A1) # 第一层参数梯度 dW1 np.dot(X.T, dZ1) # (n_input, n_hidden) db1 np.sum(dZ1, axis0, keepdimsTrue) return dW1, db1, dW2, db2A1.T的转置是为了让维度匹配A1是(n_samples, n_hidden)dZ2是(n_samples, n_output)转置后相乘得到(n_hidden, n_output)正好和W2形状一致。db2用np.sum沿样本维度求和因为偏置对每个样本都加了同一个值梯度要累加。dZ1里的A1 * (1 - A1)是 Sigmoid 的导数如果你把激活函数换成 ReLU这里就要改成dA1 * (Z1 0)这是换激活函数时最容易漏改的地方。3.3 参数更新与训练循环学习率怎么定、batch 怎么切参数更新就是最朴素的梯度下降W W - lr * dW。学习率lr是最关键的超参数太大震荡不收敛太小收敛慢。这份实现默认用全批量梯度下降数据量小时没问题数据量大了要改成 mini-batch。def train(X, Y, n_hidden, epochs, lr): n_input X.shape[1] n_output Y.shape[1] W1, b1, W2, b2 init_params(n_input, n_hidden, n_output) losses [] for i in range(epochs): Z1, A1, Z2 forward(X, W1, b1, W2, b2) loss, dZ2 compute_loss_and_grad_output(Z2, Y) dW1, db1, dW2, db2 backward(X, Z1, A1, W2, dZ2) # 梯度下降更新 W1 - lr * dW1 b1 - lr * db1 W2 - lr * dW2 b2 - lr * db2 losses.append(loss) if i % 100 0: print(fepoch {i}, loss {loss:.4f}) return W1, b1, W2, b2, losses学习率我一般从 0.1 开始试Sigmoid 网络用 0.1 到 0.5 都算常见ReLU 网络可以更小。epochs设 1000 到 5000 看损失曲线如果损失下降很慢就调大学习率如果上下跳动就调小。n_hidden是隐藏层神经元数量简单数据集 16 到 64 够用太大会过拟合太小欠拟合。训练循环里每 100 轮打印一次损失方便观察收敛趋势。4. 避坑与排查维度报错、梯度消失、过拟合的实战记录从零实现神经网络报错信息往往不如框架友好很多问题要靠自己排查。这一章记录几个我踩过的坑按「现象 → 原因 → 解决」写清楚你遇到类似问题时可以对照。4.1 矩阵维度不匹配最常见的 ValueError现象运行np.dot(X, W1)时报ValueError: shapes not aligned。原因X的列数和W1的行数不一致通常是数据预处理时特征维度变了或者W1初始化时n_input传错。解决在forward函数开头加一行assert X.shape[1] W1.shape[0]让报错提前到维度检查阶段而不是等到矩阵乘法才崩。同时检查数据加载时是否做了 reshape 或转置。4.2 损失不下降学习率与初始化的双重嫌疑现象训练几百轮后损失几乎不变或者变成nan。原因损失变nan通常是学习率太大导致梯度爆炸或者log(0)没加保护损失不变可能是学习率太小或者权重初始化全零导致对称性没打破。解决先检查np.log里有没有加1e-8再把学习率降到 0.01 试。如果还不行打印初始权重看是不是全零。另外 Sigmoid 在输入绝对值大时会饱和梯度接近零这时候换 ReLU 或调小初始化缩放因子。4.3 训练集准确率高但测试集低过拟合的识别与缓解现象训练损失持续下降训练集准确率到 95% 以上但测试集准确率只有 60% 左右。原因隐藏层神经元太多、训练轮数太多网络把训练数据的噪声也记住了。解决先减小n_hidden比如从 128 降到 32再减少epochs配合早停策略——每隔若干轮在验证集上评估验证损失开始上升就停。如果数据量允许加 L2 正则化在损失里加0.5 * reg_lambda * (sum(W1**2) sum(W2**2))梯度里对应加reg_lambda * W。4.4 激活函数换 ReLU 后梯度全零死亡 ReLU 问题现象把 Sigmoid 换成 ReLU 后部分神经元输出恒为 0梯度也恒为 0参数再也不更新。原因ReLU 在输入为负时梯度为零如果学习率太大某些神经元权重被更新到一直输出负值就「死亡」了。解决降低学习率或者改用 Leaky ReLU负半轴给一个小斜率如 0.01。反向传播里 ReLU 的导数要写成dA1 * (Z1 0)注意是Z1不是A1写错会导致梯度计算完全错误。4.5 数值溢出exp 计算的隐藏炸弹现象Softmax 或 Sigmoid 计算时出现RuntimeWarning: overflow encountered in exp随后损失变nan。原因np.exp对大的正数会溢出比如exp(1000)直接变成inf。解决Softmax 里减去每行最大值代码里已经做了Sigmoid 里用np.clip限制输入范围。这两个操作不改变数学结果但能保证数值稳定是从零实现时必须养成的习惯。5. 进阶技巧从手写实现到框架对照的验证方法手写实现跑通之后最有价值的动作是拿它和成熟框架对照验证自己的梯度算得对不对。我一般用「梯度数值检验」这个方法对某个参数加一个极小扰动用损失变化除以扰动得到数值梯度再和反向传播算出的解析梯度对比相对误差小于1e-6就说明推导和代码都没问题。def gradient_check(X, Y, W1, b1, W2, b2, epsilon1e-6): # 以 W1 的第一个元素为例做数值梯度检验 Z1, A1, Z2 forward(X, W1, b1, W2, b2) loss, dZ2 compute_loss_and_grad_output(Z2, Y) dW1, _, _, _ backward(X, Z1, A1, W2, dZ2) analytic_grad dW1[0, 0] # 数值梯度扰动 W1[0,0] W1_plus W1.copy(); W1_plus[0, 0] epsilon W1_minus W1.copy(); W1_minus[0, 0] - epsilon loss_plus compute_loss_and_grad_output(forward(X, W1_plus, b1, W2, b2)[2], Y)[0] loss_minus compute_loss_and_grad_output(forward(X, W1_minus, b1, W2, b2)[2], Y)[0] numeric_grad (loss_plus - loss_minus) / (2 * epsilon) diff abs(analytic_grad - numeric_grad) / max(abs(analytic_grad) abs(numeric_grad), 1e-8) print(f解析梯度 {analytic_grad:.8f}, 数值梯度 {numeric_grad:.8f}, 相对误差 {diff:.2e})这个检验只对单个参数做全量做太慢但足以暴露链式法则里的符号错误或转置错误。相对误差在1e-7量级是正常的如果到1e-3以上基本可以确定反向传播某一步写错了。另一个进阶方向是加动量或 Adam 优化器把W - lr * dW换成带动量的更新收敛速度会明显提升但前提是基础梯度已经验证正确。从那以后我每次手写反向传播都强制先跑一遍梯度检验再开始训练这个习惯帮我省下了大量对着损失曲线发呆的时间。希望这份拆解能帮你把这份实现真正跑起来、看明白。本文还有配套的精品资源点击获取