鱼书《深度学习入门》代码详解:numpy手写神经网络与反向传播

发布时间:2026/10/19 12:21:01
鱼书《深度学习入门》代码详解:numpy手写神经网络与反向传播 简介这份代码包完整收录《深度学习入门基于Python的理论与实现》一书各章示例程序面向希望边读边敲代码、快速上手深度学习基础实现的初学者。内容从第1章的图像加载与函数绘制、第2章的感知机逻辑门逐步推进到第3章的MNIST手写数字识别及激活函数对比再到第4章神经网络学习与梯度实现第5章包含误差反向传播与梯度确认第6章覆盖权重初始化、Batch Norm、Dropout、超参数优化及多种优化器对比第7、8章则提供简单卷积网络、深度卷积网络和半精度训练等进阶实现。资源共67个文件以59个Python脚本为主体辅以3个pkl已训练模型参数、2个md说明文档和2个png图像文件压缩包约4.44MB目录按章节与common模块组织便于对照原书逐章运行。目前已有2534人学习下载适合作为入门深度学习课程、自学或备课的随书代码伴侣。1. 鱼书《深度学习入门》代码一本老书为什么到现在还值得拆最近我把鱼书《深度学习入门基于Python的理论与实现》的配套代码在两个不同 numpy 版本的环境里完整跑了一遍。跑完之后结论很直接这本老书的代码依然是深度学习 python 入门最好的一份最小实现素材。它不依赖 TensorFlow 或 PyTorch前向传播、反向传播、梯度下降、CNN 的 im2col 全部用最朴素的 numpy 写出来几百行代码就能看到一个数字图像分类任务从随机权重走到接近 95% 精度的全过程。如果你正卡在“理论都懂、代码不会写”或者想找一份能改、能跑、能对照书里公式的基线代码这份资源就是现成的作业模板。2. 跑通环境与数据加载numpy 版本、MNIST 数据与第一次推理2.1 代码结构按章节拆好的最小实现打开资源包第一件事不是读 README而是先把目录结构摸清楚。这本书的配套代码按 ch01 到 ch08 的顺序组织每个章节文件夹里都是相对独立的小脚本。我第一次跑的时候直接进 ch04 找训练脚本结果因为缺了前面的目录上下文光 import 路径就错了两回。下面是最核心的目录结构你拿到资源后先对照检查一遍book-code/ ├── dataset/ # 公共数据集与加载工具 │ ├── mnist.py # load_mnist 的封装核心入口 │ └── mnist.pkl # 序列化缓存首次下载后生成 ├── ch03/ │ ├── neuralnet_mnist.py # 三层神经网络推理与精度验证 │ └── sigmoid.py ├── ch04/ │ ├── two_layer_net.py # 两层网络类loss 与数值梯度 │ └── train_neuralnet.py # mini-batch 训练主循环 ├── ch05/ │ ├── layers.py # ReLU、Sigmoid、Affine、Softmax 层 │ └── train_neuralnet.py # 用反向传播优化的版本 ├── ch06/ │ ├── optimizer_compare_mnist.py # 四种优化器对比 │ └── weight_init_compare.py # 权重初始化对比 └── ch07/ ├── simple_convnet.py ├── im2col.py └── train_convnet.py这段目录信息不是摆设它体现了作者的编排思路dataset 是公共模块ch03 以前的章节用不到数据集ch04 开始做训练ch07 才进入卷积网络。实际使用中如果你的 IDE 没把项目根目录加进 PYTHONPATH直接从 ch04 目录里运行 python train_neuralnet.py大概率会报 ModuleNotFoundError。这个问题后面避坑章节会专门展开。2.2 环境检查先过 numpy 版本这一关这本书写成时 numpy 还在 1.24 之前很多代码里出现 np.float、np.int 这类 dtype 别名。我第一次在新环境执行 ch03 示例脚本第一行随机权重初始化就炸了AttributeError: module numpy has no attribute float。所以拿到代码后的第一步不是装依赖而是先确认当前环境能不能跑这些脚本python -c import sys, numpy; print(sys.version); print(numpy.__version__)如果输出的是 numpy 2.x你可以选择把脚本里所有 np.float 和 np.int 全局替换成 float 和 int也可以锁定 numpy 1.23.5。我个人习惯建一个干净的 Python 3.10 或 3.11 虚拟环境然后装依赖。这本书只依赖 numpy 和 matplotlib没有深度学习框架这反而是最大优势跑不动大模型的机器也能轻松带起来排错范围也被压缩到最小。pip install numpy matplotlib依赖越少出问题的地方就越少。全书一共就用两个第三方库如果装完后还有 import 失败基本就是当前 shell 没有激活正确的虚拟环境或者 pip 装到了系统级环境导致版本冲突。装完依赖后建议先进入 dataset 目录手动执行一次 python mnist.py确认数据能加载再继续往下走。2.3 load_mnist 的三个参数决定后续所有实验的数据形态load_mnist 是整本书出现频率最高的函数它在 dataset/mnist.py 里默认行为是检查本地 mnist.pkl不存在就尝试下载原始数据并序列化。理解它的三个参数基本等于理解了后续所有数据流向from dataset.mnist import load_mnist (x_train, t_train), (x_test, t_test) load_mnist( normalizeTrue, flattenTrue, one_hot_labelFalse, ) print(train:, x_train.shape, t_train.shape) print(test: , x_test.shape, t_test.shape) print(pixel range:, x_train.min(), x_train.max()) print(labels example:, t_train[:8])正常输出是 train: (60000, 784) 和 (60000,)test: (10000, 784) 和 (10000,)像素范围 0.0 到 1.0。normalizeTrue 把 0 到 255 的灰度值统一缩放到 0 到 1避免像素值过大导致梯度震荡flattenTrue 把 28x28 的二维图像拉平成 784 维向量方便全连接层直接做矩阵乘法one_hot_labelFalse 表示标签仍是 0 到 9 的整数。早期我图省事把 one_hot_label 设成 False结果在写交叉熵时得自己处理索引取值反而多写了代码。如果你接下来要做损失函数推导建议直接设 True两种形态在后面的章节都会用到。mnist.pkl 的缓存机制值得留意第一次加载可能触发下载一旦 pkl 生成后续加载都从本地文件恢复。对离线环境你只需要提前把 mnist.pkl 放进 dataset 目录函数检测到文件存在就会跳过下载。这个特性在避坑章会再展开。2.4 加载后的形状自检把问题挡在训练之前数据加载完不要急着跑训练先用几个断言确认形态没有偏差。很多奇怪的训练问题源头只是数据 shape 不符合网络预期assert x_train.shape (60000, 784) assert t_train.shape (60000,) assert x_test.shape (10000, 784) assert t_test.shape (10000,) assert x_train.min() 0.0 and x_train.max() 1.0第一条断言检查训练样本是否被 flatten第二条检查标签是否一维整数数组最后一条检查 normalize 是否真的生效。如果某条断言失败回去检查 load_mnist 的参数配置而不是继续带着坏数据跑模型。这样的自检成本极低能省下后面无数排查时间。做完自检可以先不做训练拿随机初始化的网络跑一次推理预期精度在 0.10 左右十个类别均衡分布随机猜就是十分之一。如果你跑出来精度明显偏离这个值说明权重初始化或数据加载有异常先回头看 2.3 的参数。3. 手写三层神经网络的推理forward 与 softmax 的抄作业路径3.1 网络骨架参数初始化与两层隐层的 forward在 ch03 里作者用字典存参数用一连串 numpy 运算完成推理。很多先学框架的人第一次看这段代码会不习惯因为 PyTorch 里一个 nn.Sequential 就完事了。但恰恰是这种“裸写”让你看清每一层发生了什么。下面是干净版的三层网络初始化与推理import numpy as np def sigmoid(x): return 1 / (1 np.exp(-x)) def softmax(a): c np.max(a) exp_a np.exp(a - c) return exp_a / np.sum(exp_a) def init_network(): network {} network[W1] np.random.randn(784, 100) * 0.01 network[b1] np.zeros(100) network[W2] np.random.randn(100, 100) * 0.01 network[b2] np.zeros(100) network[W3] np.random.randn(100, 10) * 0.01 network[b3] np.zeros(10) return network def predict(network, x): W1, W2, W3 network[W1], network[W2], network[W3] b1, b2, b3 network[b1], network[b2], network[b3] a1 np.dot(x, W1) b1 z1 sigmoid(a1) a2 np.dot(z1, W2) b2 z2 sigmoid(a2) y softmax(np.dot(z2, W3) b3) return y这段代码的维度变化是 784 - 100 - 100 - 10两个隐层都接 sigmoid输出层用 softmax 转成概率分布。乘 0.01 的权重初始化是关键它让激活值落在 sigmoid 的线性区附近如果标准差设成 1深层网络的输出会很快饱和训练时梯度小到几乎不更新。偏置全部初始化为 0 在只有两层隐层的网络里没有问题无需担心对称性问题。实际写代码时最容易错的一步是忘记加偏置项。有人会把 b 直接写进 np.dot 之外就完事后果是当输入 x 的维度不是 (batch, 784) 时广播规则会给出一个看着正常但结果错误的值。我习惯在关键层后面打印 shape确认每一层输出维度对得上再继续往下写。3.2 softmax 数值稳定与 argmax 精度统计softmax 实现里减最大值这一刀容易被忽略exp(a) 在大数值下会溢出减掉数组最大值后再算指数值域就安全了。虽然 MNIST 输入已经归一化到 0 到 1前向传播后 logits 不会太大但养成减最大值的习惯迁移到其他任务时能救命。def softmax(a): c np.max(a, axis1, keepdimsTrue) exp_a np.exp(a - c) return exp_a / np.sum(exp_a, axis1, keepdimsTrue)注意这里有三个改动axis1 是对每个样本独立求最大值keepdimsTrue 是为了保住维度用于广播。如果你直接 np.max(a)得到的是整个 batch 的全局最大值数学上不算错但会让每行概率的分布被一个极端样本带偏。精度统计用 argmax 和均值比较y predict(network, x_test) pred np.argmax(y, axis1) acc np.mean(pred t_test) print(accuracy:, acc)axis1 表示在类别维上取最大值的索引pred 的 shape 是 (10000,)。和 t_test 逐位比较后取 mean就得到测试集上的准确率。随机权重下这个数字稳定在 0.098 到 0.11 之间因为十个类均衡分布瞎猜也能中十分之一。3.3 推理脚本改造成可视化版本把黑匣子变成能看到的图书里的 neuralnet_mnist.py 只输出精度没有可视化。我一般在跑通后加一段 matplotlib 代码把 28x28 的输入图像和预测结果一起打印出来让“输入一张图、输出十个概率”的过程变得直观import matplotlib.pyplot as plt def visualize_prediction(network, x_test, t_test, index0): y predict(network, x_test[index:index 1]) pred np.argmax(y, axis1)[0] plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False plt.imshow(x_test[index].reshape(28, 28), cmapgray) plt.title(f真值: {t_test[index]} 预测: {pred}) plt.show() visualize_prediction(network, x_test, t_test, index0)这里 index 切片 x_test[index:index1] 是为了保持 batch 维直接传 x_test[0] 会让 np.dot 少一个维度矩阵乘法直接报错。rcParams 两行配置解决中文标题方块和负号显示异常两个问题。初期跑这种可视化脚本比只看 loss 曲线更能建立“模型到底在认什么”的直觉。4. 反向传播与训练循环损失、梯度与优化器的更新链路4.1 交叉熵损失数值下限与 mini-batch 平均第 3 章只做推理第 4 章开始训练训练第一步是明确“模型错得有多远”。书里的交叉熵实现有两个细节值得抄一是只取正确类别对应的 log 概率二是除以 batch_size 求平均。实现如下def cross_entropy_error(y, t): if y.ndim 1: t t.reshape(1, t.size) y y.reshape(1, y.size) batch_size y.shape[0] return -np.sum(np.log(y[np.arange(batch_size), t] 1e-7)) / batch_size这里 y 是 softmax 的输出概率矩阵t 是整数标签。y[np.arange(batch_size), t] 的写法等价于“选每一行里标签对应的那个概率”比用循环快得多。加 1e-7 是为防止概率为 0 时 log(0) 变成负无穷导致 loss 显示为 inf。如果你把 one_hot_label 设成 True代码就要改成 t 和 y 逐位相乘再求和两种写法结果一致但整数标签版本更适合推导梯度。这个函数返回的是每个样本损失的平均值而不是总和。损失求平均后梯度大小与 batch_size 解耦你用 batch_size100 和 batch_size10000 训练时学习率量级不需要大幅调整。这是很多初学框架的人容易忽略的点PyTorch 默认的 CrossEntropyLoss 同样是平均损失如果手动实现时忘了除以 batch_sizeloss 曲线会随 batch 大小漂移。4.2 梯度来源数值梯度验证与反向传播提速ch04 的 two_layer_net.py 里梯度来自数值微分对每个参数做微小扰动用中心差分近似导数。这段代码逻辑简单但时间开销非常大两层网络在 MNIST 上跑 10000 次迭代需要算上万次前向传播一小时都跑不完。作者的思路是先用数值梯度验证实现正确再在 ch05 换成反向传播提速。所以我的建议是拿到这份资源ch04 只要看懂数值梯度怎么写后续跑实验直接跳到 ch05/train_neuralnet.py。def numerical_gradient(f, x): h 1e-4 grad np.zeros_like(x) for i in range(x.size): tmp x[i] x[i] tmp h fxh1 f(x) x[i] tmp - h fxh2 f(x) grad[i] (fxh1 - fxh2) / (2 * h) x[i] tmp return gradh 取 1e-4 是经验值太大梯度近似误差大太小浮点数舍入误差大。中心差分比单侧差分误差低一个数量级但计算量翻倍。数值梯度的唯一用途是验证反向传播算出的梯度你在复现时如果发现两者相差超过 1e-4 量级说明某个层的反向推导写错了别急着调学习率。4.3 训练主循环迭代次数、学习率、批大小的一组安全参数训练循环本身不复杂但参数组合非常敏感。下面这段来自 ch04/ch05 的核心循环是这份资源里最值得抄的作业iters_num 10000 train_size x_train.shape[0] batch_size 100 learning_rate 0.1 for i in range(iters_num): batch_mask np.random.choice(train_size, batch_size) x_batch x_train[batch_mask] t_batch t_train[batch_mask] grad network.gradient(x_batch, t_batch) for key in (W1, b1, W2, b2): network.params[key] - learning_rate * grad[key] loss network.loss(x_batch, t_batch) if i % 1000 0: print(fiter {i}, loss {loss:.4f})learning_rate0.1 在 MNIST 归一化后的数据上是安全值loss 会在 10000 次迭代内从约 0.6 附近降到 0.2 附近。如果你把 learning_rate 调到 1.0loss 大概率一开始就震荡到 2 以上这是最常见的训练翻车现场。batch_size100 是书中所有章节的统一配置配合 mini-batch 随机抽取每个 epoch 大约相当于 600 次迭代。如果机器内存吃紧可以把 batch_size 降为 50但学习率也要适当下调否则单批梯度噪声变大收敛曲线更抖。打印间隔在书里是 1000 次一次。实际调试时我一般改成 100 次打印一次方便尽早发现 loss 突变正式训练再改回 1000。另外有个容易误判的细节np.random.choice 每次都随机抽取没有先 shuffle 再切分这意味着同一个样本在一个 epoch 里可能被抽中多次也可能一次都没抽中。这正是 mini-batch SGD 的常见做法体现的是“用随机性近似全样本梯度”的思想别当成 bug。4.4 优化器与权重初始化书中自带的对比实验可以直接复用如果只看 SGD 更新你可能会误以为所有神经网络都这么训练。ch06 的 optimizer_compare_mnist.py 把四种优化器放在同一个问题上做对比是快速建立优化器直觉的最好入口优化器核心思路典型学习率适用场景SGD沿梯度反方向更新0.01~0.1代码最简单适合验证Momentum累积速度减轻震荡0.01收敛更平稳AdaGrad按历史梯度平方缩放0.01稀疏梯度场景Adam一阶矩加二阶矩自适应0.001~0.01默认选择鲁棒性高在同样的两层网络上SGD 收敛最慢Adam 通常最早把 loss 降到 0.3 以下。不过第一遍跑我建议不要直接换 Adam先用 SGD 手动走一遍“前向计算、求梯度、参数更新”的完整链路对梯度的理解会深很多等后面调参时再切 Adam 省时间。权重初始化在 ch06/weight_init_compare.py 里有类似对比。随机权重标准差从 0.01 到 1 的变化在深层网络上会造成激活值要么集中在 0 附近要么饱和到 0/1梯度要么消失要么爆炸。书中推荐的 Xavier 和 He 初始化各有适用激活函数sigmoid 配 XavierReLU 配 He。第 3 章里乘 0.01 之所以可行是因为隐层只有两层换个十层网络再用 0.01 初始化输出层大概率什么都学不会。复现一遍这套对比实验比背十遍理论都管用。5. 避坑与排查五个踩得最深的坑5.1 numpy 版本差异导致变量初始化即崩溃现象运行 ch03 或 ch04 任意脚本在构建网络参数时报 AttributeError: module numpy has no attribute float或类似的 np.int 报错。原因这本书的代码写成时 numpy 还存在 np.float、np.int 这些 dtype 别名numpy 1.24 起正式移除统一改用 Python 原生 float/int 或 np.float64/np.int64。解决全局搜索 np.float 和 np.int替换为 float 和 int。如果是随机权重初始化里写了 np.float(0.01)直接写成 0.01 即可。我一般会在项目根目录执行一次批量替换再逐个脚本验证没有遗漏。如果不想改动源码也可以把 numpy 锁到 1.23.5但这个旧版本对 Python 3.11 的兼容性不稳定不推荐长期使用。5.2 load_mnist 卡在下载阶段不动现象第一次运行 mnist.py终端长时间停在下载状态进度条不动或下载失败退出。原因mnist.py 内部在找不到本地 mnist.pkl 时会尝试从网络获取原始数据集这一步对网络环境敏感容易超时。解决离线或弱网环境直接把 mnist.pkl 文件放到 dataset 目录load_mnist 检测到本地文件存在后跳过下载整个加载过程在本地完成。这也是我的习惯做法任何教材的配套数据集都先单独准备好再跑代码避免把第一次运行的体验浪费在网络等待上。这一步不做后面所有训练脚本都启动不了。5.3 matplotlib 图表中文变方块、负号显示成横杠现象plt.title 里的中文字符显示成一排方块坐标轴的负号显示成类似横杠的符号而不是标准减号。原因matplotlib 默认字体不包含中文字形且默认启用了 Unicode 负号但某些字体不支持该映射。解决在绘图脚本开头加入两行配置plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] FalseLinux 服务器没有 SimHei 时可以换成 Noto Sans CJK SC 或系统中已安装的中文字体名。这两行配置从第 3 章以后的所有图表都会用到建议统一放在一个公共配置函数里而不是每个脚本复制粘贴。5.4 im2col 卷积展开内存暴涨现象ch07 的训练脚本按默认 batch_size 跑没问题把 batch_size 调大后内存占用飙升甚至出现 MemoryError进程被杀。原因im2col 会把每个输入图像块复制展开成一个大矩阵卷积的矩阵乘变快了但中间矩阵大小是 (N * OH * OW, C * FH * FW)。拿 28x28 单通道输入、5x5 卷积核来算单个样本展开约 19600 个元素10000 个样本就是约 1.96 亿个 float64占用内存约 1.6GB这还没算后续矩阵乘法的中间结果。解决严格按书中默认 batch_size100 运行如果验证阶段需要全样本一次性前向改为分块推理。理解 im2col 公式后也可以自己用 np.lib.stride_tricks.as_strided 实现更省内存的版本但新手阶段直接用书里的现成实现就好。5.5 loss 不降或震荡先检查数据归一化和学习率现象训练跑了 5000 次迭代loss 停留在 0.6 到 0.8 之间震荡偶尔跳到 2 以上测试精度始终没有明显增长。原因排在前两位的是输入没有归一化到 0 到 1以及学习率设置过大。如果 normalizeFalse像素值是 0 到 255权重和偏置的梯度数量级被放大同样的学习率下 loss 曲线会剧烈震荡学习率过大也会导致参数更新越过最优区域。解决先把 normalize 改成 True确认 x_train.min() 和 max() 分别在 0 和 1 附近然后把 learning_rate 从 0.1 降到 0.01观察 loss 是否平滑下降。如果还不行固定随机种子排除随机性干扰再逐个排查预处理和超参数。这个方法能解决我遇到过的八成训练不收敛问题。6. 把鱼书代码改成可复用训练模板从脚本到能换数据集的函数最后一个技巧把之前所有循环封装成可复用的训练函数下次换个数据集不用重写主流程def train(network, x_train, t_train, iters10000, batch_size100, lr0.1, log_interval500): history [] for i in range(iters): mask np.random.choice(x_train.shape[0], batch_size) x_batch, t_batch x_train[mask], t_train[mask] grads network.gradient(x_batch, t_batch) for key in network.params: network.params[key] - lr * grads[key] if i % log_interval 0: loss network.loss(x_batch, t_batch) history.append((i, loss)) print(fiter {i}: loss {loss:.4f}) return history封装后只需要换掉 x_train、t_train 和网络结构同样的训练流程就能用在其他图像分类数据上。history 列表留出来画 loss 曲线不用再从 print 输出里抠数据。我个人会把这份代码存成模板连书里的注释一起保留新项目需要做基线实验时直接复制过来改参数。我在跑这个模板时习惯固定随机种子做过一次基准验证两层网络 100 节点隐层SGD、lr0.1、10000 次迭代loss 降到约 0.24测试精度约 95.7%同样参数换成 Adam收敛明显更快。这次验证让我确认了这份代码的适用边界是“两层网络加 MNIST”一旦换更复杂的数据集必须配合归一化、权重初始化和优化器调整不能直接套参数。从那以后我每次拿到一本新书的配套代码都强制先跑通环境再做一行代码改造花 20 分钟做一次基线验证再决定要不要深入读源码。这个习惯让我少走了很多弯路希望也能帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

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

尧图内容编辑团队

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

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

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

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

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

网站改版的5个关键决策

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

获取专属建站方案

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

立即免费咨询