简介《深度学习入门基于Python的理论与实现》鱼书的配套源代码面向刚接触深度学习、希望在读理论的同时亲手验证代码的Python学习者解决“书上公式能懂动手实现不知从哪开始”的问题。资源按原书章节组织第2章感知机到第8章深度学习均有对应实现覆盖前向/反向传播、梯度下降、权重初始化、Batch Norm、过拟合与Dropout、卷积神经网络等核心主题除主要模型外还包含MNIST数据加载脚本、若干预训练参数文件便于直接复现书中图表与训练结果。压缩包共67个文件其中Python源码文件占据绝大多数有五十九个另有三个pkl格式的参数文件、两个Markdown说明文档、两张示例图片和一个gitignore配置整体大小仅4.44MB目录模块划分清晰方便按章节查阅。已有2524人浏览学习适合配合教材逐章实践或考前快速回顾算法细节。1. 鱼书代码不是“附赠品”它是把公式变成能跑的网络的最后一公里聊深度学习入门绕不开鱼书——那本封面有一条橙色鱼的《深度学习入门基于Python的理论与实现》。很多人买回书来第一周把前向传播、反向传播的公式推得挺顺到打开配套代码就卡住不是报错就是跑出来的数字和书上对不上。原因很直接这本书的代码是教材型实现它优先把公式结构和计算过程讲清楚而不是优先做性能优化和工程健壮性所以它既值得读又需要你会“读”。这套代码真正适合两类人一类是把书看完、想对照公式逐行确认“原来这一步是这么落地的”的读者另一类是拿它当实验脚手架想快速改数据集、改超参数、跑通自己的对比实验的从业者。它不是拿来直接上生产的但作为深度学习的第一个可运行范本价值很大。这篇文章就沿着“跑起来→读主线→避坑→改成自己的”这条路径把值得投入的点和翻车点一次说清。2. 先让代码在本地跑起来环境版本、目录结构与第一个MNIST实验2.1 鱼书代码的目录结构哪些目录是主线、哪些可以先跳过拿到这套代码后第一件事不是打开某个 py 文件从头读而是先看目录。整套代码按书的章节分成 ch01 到 ch08公共代码收在两个目录里common 放所有章节共享的层、优化器、训练器、工具函数dataset 放数据下载和预处理脚本。这个分层方式是全书代码里最值得学的一点它把“实验逻辑”和“公共组件”拆开了后期你自己做实验也建议照这个结构组织。. ├── ch01 # numpy 基础没有网络结构可以快速扫 ├── ch02 # 感知机与逻辑门几十行一个文件 ├── ch03 # 前向传播与 MNIST 推理 ├── ch04 # 两层网络 数值微分训练 ├── ch05 # 反向传播实现重点章节 ├── ch06 # 优化器、权重初始化、过拟合对策 ├── ch07 # 卷积网络与 im2col 实现 ├── ch08 # 深度网络与精度实验 ├── common # layers.py / optimizer.py / trainer.py 等公共模块 └── dataset # mnist.py 等数据加载脚本会缓存 pkl 文件我的建议是ch01 和 ch02 不用逐行读花半天跑一遍、看输出即可ch03 开始进入主题ch04 到 ch06 是整条主线ch07 是理解卷积实现的关键ch08 可以作为扩展阅读。难点集中在 ch05 的反向传播和 ch07 的 im2col这两章值得多花时间。先跑通再读代码顺序很重要。很多读者一上来就啃 ch05 的反向传播源码被中间变量绕晕其实先跑通一个能出结果的最小例子再回头读代码效率高很多。2.2 环境版本选择Python 版本、numpy 版本与依赖安装鱼书的代码写在 Python 3 早期书中大量使用 numpy 和 matplotlib本身依赖很少。但放在今天环境配置反而是最大的坑。不要用最新版 numpy也不要用 Python 3.13 以上。numpy 2.0 开始移除了不少旧别名鱼书这类教材代码在 numpy 2.x 下跑经常碰到 np.float、np.int 不存在这类报错完全不值得花时间排查。python3.10 -m venv .venv source .venv/bin/activate pip install --upgrade pip pip install numpy1.20,2.0 pip install matplotlib逻辑说明这里特意把 numpy 锁在 1.x 版本段。鱼书代码依赖 numpy 的数组广播、reshape、astype 这些基础能力1.20 到 1.26 之间都能稳定运行而 2.x 的兼容性改动对老教材代码不友好。matplotlib 用当前稳定版即可绘图接口十几年没大变。逻辑解释python3.10 -m venv .venv创建独立虚拟环境避免污染系统 Python后面source .venv/bin/activate激活环境之后所有命令都在这个环境里执行。这个版本组合是我反复折腾后觉得最稳的Python 3.10 对 numpy 1.26 支持良好matplotlib 也不存在 wheel 缺失的问题。如果你机器上只有 Python 3.12也可以直接用但建议先创建虚拟环境再装依赖不要用系统 Python 直接装。2.3 跑通第一个MNIST推理脚本预期输出与复现基线环境装好后先用 ch03 里的 MNIST 推理脚本验证整条链路。第一次运行会触发数据集下载和缓存会等一会儿。跑通之后后续所有实验都会复用缓存数据速度明显变快。cd ch03 python neuralnet_mnist.pyAccuracy:0.9352逻辑说明这个脚本会加载训练好的权重书中预置在 sample_weight.pkl 里对 MNIST 测试集做前向推理并输出识别准确率。0.9352 是书中给的一个参考值只要你的输出在 0.93 上下浮动就是正常的。如果这一步能跑通说明 numpy、matplotlib、数据集、目录路径全都正常后面就不用再怀疑环境问题。参数说明neuralnet_mnist.py内部调用load_mnist(normalizeTrue, one_hot_labelFalse)第一个参数决定是否做归一化改成 False 的话准确率会明显下降第二个参数决定标签是否是 one-hot 形式推理场景用整数标签更直观。如果你第一次跑没有输出准确率而是报连接超时直接看第 4 章避坑部分。3. 按主线读代码两层网络、反向传播、优化器与CNN实现3.1 两层网络是主干从 predict 到损失函数的最小闭环鱼书代码里最值得先读的是 ch04 里的两层网络实现。这个文件把神经网络的最小骨架讲明白了一个输入层、一个隐藏层、一个输出层配上 softmax 和交叉熵损失。不要先去看 ch07 的卷积网络那个结构复杂容易让人忽略最基本的数据流。先把这个网络的代码读通后面所有内容都是在这个骨架上加组件。class TwoLayerNet: def __init__(self, input_size, hidden_size, output_size, weight_init_std0.01): self.params {} self.params[W1] weight_init_std * np.random.randn(input_size, hidden_size) self.params[b1] np.zeros(hidden_size) self.params[W2] weight_init_std * np.random.randn(hidden_size, output_size) self.params[b2] np.zeros(output_size) def predict(self, x): W1, W2 self.params[W1], self.params[W2] b1, b2 self.params[b1], self.params[b2] a1 np.dot(x, W1) b1 z1 sigmoid(a1) a2 np.dot(z1, W2) b2 return softmax(a2)逻辑说明predict就是前向传播数据从输入 x 开始先经过第一个全连接层激活函数用 sigmoid再经过第二个全连接层最后过 softmax 得到概率分布。weight_init_std0.01控制权重初始化的尺度这个参数直接决定了激活值是否会集中在 0 附近影响训练速度。参数说明input_size784对应 MNIST 的 28x28 像素展平后的长度hidden_size是可调的隐藏层维度书中示例一般取 50output_size10对应 0 到 9 十个数字类别。我建议你拿到代码后先把这三个参数打印出来确认 shape 是 (784, 50)、(50, 10)这对后面排查维度报错很有帮助。3.2 两种梯度计算方式数值微分是标尺反向传播才是主力鱼书代码同时提供两种梯度计算方式。numerical_gradient是数值微分定义简单、结果可靠但每算一个参数都要做两次前向传播速度慢到只能用来验证而反向传播通过链式法则一次前向一次反向算出全部梯度速度快但实现复杂、容易写错。书中 ch05 的关键设计就是让这两种方式结果对比打印出来。from common.gradient import numerical_gradient net TwoLayerNet(input_size784, hidden_size50, output_size10) grad_numerical net.numerical_gradient(x_batch, t_batch) grad_backward net.gradient(x_batch, t_batch) for key in grad_numerical.keys(): diff np.abs(grad_numerical[key] - grad_backward[key]).mean() print(key, diff)逻辑说明这段代码对每个参数计算两种梯度的平均绝对差异。如果反向传播实现正确这个差值应该在 1e-8 到 1e-10 量级。如果你改了网络结构或者新写了一个层一定先跑这个对比再上训练数值微分虽然慢但它是检验反向传播写没写对的金标准。参数说明x_batch和t_batch是从训练集里抽出的 mini-batch一般取 100 条。注意numerical_gradient的耗时和参数量、batch 大小都成正比两层网络跑一次大约要几十秒不要拿来和反向传播比速度它只是校验工具。3.3 优化器对比脚本SGD、Momentum、AdaGrad、Adam怎么选怎么看进入 ch06第一件事就是跑optimizer_compare_mnist.py。这个脚本把四种优化器放在同一个数据集、同一个网络结构下训练并把 loss 下降曲线画在一起。它是我见过最直观的优化器对比实验比任何文字都管用。读这个脚本时重点看两处四种优化器分别怎么初始化以及 Trainer 是怎么接优化器的。优化器类名书中对比脚本传参特点随机梯度下降SGDlr0.1实现最简单收敛慢但稳定动量法Momentumlr0.01, momentum0.9加速收敛方向震荡小自适应梯度AdaGradlr0.01对稀疏特征友好后期学习率自动变小自适应动量Adamlr0.001综合表现好实际项目首选逻辑说明表中传参是书中脚本里实际使用的值不是理论默认值。Adam 的初始学习率通常比 SGD 小一个数量级这是它自带学习率调整机制决定的。跑完脚本你会看到 Adam 的 loss 曲线下降最快但最优模型不一定来自 Adam——最终精度还受超参数和数据影响这个脚本的价值在于建立直觉。参数说明trainer.train(lr, epochs, mini_batch_size)里的mini_batch_size在脚本中是 256epochs是 16。想看不同优化器的差距不要先调参直接跑原脚本。我自己的习惯是先把优化器换成 Adam 跑一个快速版本确认网络能收敛再回来细调其它超参数。3.4 卷积层的实现核心im2col 到底做了什么、为什么吃内存到了 ch07鱼书用 im2col 实现卷积层。这是全书代码里最值得反复琢磨的一段。理解 im2col 之前先记住一个结论它把卷积运算变成了矩阵乘法从而直接复用 numpy 的高性能矩阵运算。代价是中间会生成一个很大的矩阵占用内存比原始图像大得多。col, params im2col_collection(input_data, filter_h, filter_w, stride1, pad0)如果只看接口im2col的作用是把形状为 (N, C, H, W) 的输入图像转换成形状为 (N, C * FH * FW, OH * OW) 的二维矩阵。其中 FH、FW 是卷积核的高和宽OH、OW 是输出特征图的高和宽。之后的卷积就变成这个矩阵和权重矩阵的一次np.dot再加上广播后的偏置。代码里实际调用方式是col im2col(x, filter_h, filter_w, stride, pad)返回的 col 再 reshape 成四维。逻辑说明这段设计的巧妙之处在于把“窗口滑动”这个循环操作变成了数据重排。代价也在这一张 28x28 的单通道输入如果卷积核是 5x5im2col 后的矩阵尺寸会膨胀几倍到几十倍。所以跑 ch07 时如果内存吃紧先减小 batch_size不要直接换机器这是代码的固有特性不是 bug。参数说明stride和pad决定输出尺寸公式是OH (H 2*pad - FH)//stride 1。改这两个参数时输出 shape 会变化后续全连接层的输入维度也要跟着改这是初学者最容易漏掉的一步。4. 鱼书代码的常见坑数据集下载失败到结果对不上的排查记录4.1 数据集下载卡住不动或直接报连接错误现象第一次运行neuralnet_mnist.py终端停在 Downloading 的状态等很久也没结束或者直接抛出连接相关的异常程序中断。原因dataset/mnist.py内置了一个默认下载地址而不同网络环境访问该地址的稳定性差异很大不少人就是卡在这一步。代码本身没有问题是网络环境的可达性问题。解决最省事的办法是手动准备数据绕开自动下载。从可正常访问的渠道获取四个原始文件训练图像、训练标签、测试图像、测试标签文件名是train-images-idx3-ubyte.gz这类格式放到dataset目录下再重新运行脚本。mnist.py检测到已有文件后会直接生成缓存 pkl不再发起下载。这个操作对结果没有任何影响数据集内容完全一致只是下载路径变了。提示手动放数据时注意文件后缀mnist.py解压时按固定文件名匹配文件名不对会直接报错这个细节比想象中容易翻车。4.2 numpy 版本导致 np.float、np.int 不存在这类报错现象运行某个脚本时报AttributeError: module numpy has no attribute float或者类似的np.int不存在错误。原因numpy 1.24 起正式移除了np.float、np.int这些别名。鱼书代码本身用得很克制问题多出在读者扩展代码、网上搬运的改版代码里。如果你装了 numpy 2.x这类问题出现频率更高。解决如果只是个别报错把np.float改成float、np.int改成int即可行为几乎一致。如果想彻底免去麻烦按第 2 章的方式装numpy2.0的 1.26.x 版本这是最省心的做法。不要自己写兼容层去劫持 numpy 的别名只会引入更多不确定性。4.3 训练脚本跑完但没有任何图像窗口弹出现象脚本正常结束但预期的曲线图、权重可视化图都没出现程序看起来什么都没做。原因分两种情况。一种是你用的是无桌面环境的服务器或容器matplotlib找不到显示设备直接静默失败或报后端错误另一种是脚本调用plt.show()的时机不对或者显示窗口一闪而过你根本没看到。解决在无桌面的机器上把绘图后端强制设为 Agg并将图片保存到文件python -c import matplotlib; matplotlib.use(Agg)或者在脚本里加plt.savefig(result.png)替代plt.show()。如果你用的是 Jupyter Notebook先执行%matplotlib inline再跑绘图脚本图形会直接嵌在 cell 输出里。优先用保存文件的方式它不依赖任何显示环境排查问题时也更方便逐张翻图。4.4 训练结果和书上的表格对不上甚至每次跑都不一样现象按照书里的代码原样运行得到的准确率比书中记录低或者两次运行结果不同。原因这通常是随机性造成的不是代码写错。鱼书中的一部分训练脚本没有固定随机种子权重初始化、mini-batch 抽取都会引入随机波动。另外load_mnist的参数选择也会影响结果比如归一化开关开关一改训练难度完全不同。解决先检查两处。第一确认load_mnist的调用方式和书里一致重点看normalize、flatten、one_hot_label三个参数第二如果要精确复现某个数字在脚本主逻辑最开始加上np.random.seed(0)。需要说明的是optimizer_compare_mnist.py这类对比脚本本身已经固定了种子跑出来的曲线是可复现的拿它当基准最可靠。如果你的结果和它差很多才需要回头查代码改动。5. 把鱼书代码改造成自己的实验脚手架配置分离、数据替换与训练日志5.1 先做配置分离把学习率、批大小、隐藏层数从脚本里拆出来鱼书的训练脚本把超参数直接写在文件里比如lr 0.1、hidden_size 50这在一两个实验时没问题但当你开始对比多组参数每改一个数字都要翻源码既容易漏改也容易记混。第一步改造就是把这些参数收拢到一个配置对象里。这一步不需要引入任何框架一个字典就够。config { input_size: 784, hidden_size: 50, output_size: 10, learning_rate: 0.1, batch_size: 100, epochs: 20, weight_init_std: 0.01, }逻辑说明把超参数集中定义训练脚本里按config[learning_rate]这种形式引用。这样你要做对比实验时只需要复制一个字典改一两个值不用动训练逻辑。我一般会把不同实验的参数命名清楚比如config_adam、config_dropout每个配置对应一次完整实验。参数说明learning_rate和batch_size是最值得抽出来的两个参数因为它们对训练曲线的影响最大。weight_init_std也建议抽出来配合第 3 章的初始化讨论方便复现不同初始化策略对收敛速度的影响。这个字典后面可以直接序列化成 JSON 或 YAML方便存档但不建议在第一步就引入配置文件解析库先用字典跑通再说。5.2 替换数据集从 MNIST 到自己的表格数据需要改哪几个地方鱼书代码的接口设计其实对换数据比较友好load_mnist返回的就是(训练输入, 训练标签), (测试输入, 测试标签)四件套。换自己的数据本质就是写一个新的加载函数返回同样格式的数据。关键要处理好两个点输入 shape 和标签形态。def load_my_data(features_path, labels_path): # 假设特征文件是 CSV每行一个样本 x np.loadtxt(features_path, delimiter,) t np.loadtxt(labels_path, delimiter,) # 分类任务一般转 one-hot回归任务保持数值 t_onehot np.eye(np.max(t.astype(int)) 1)[t.astype(int)] return (x, t_onehot), (x_test, t_test)逻辑说明这个函数把数据读成 numpy 数组后做了两件关键事。第一如果输入是图片可能需要先做展平和归一化MNIST 的 784 维向量就是这样来的第二标签从整数改成 one-hot 形式np.eye生成单位矩阵再按标签索引取对应行。鱼书的损失函数对 one-hot 和非 one-hot 标签有不同的处理分支如果你保留整数标签损失计算路径会不同跑出来的日志显示数值差很多。参数说明np.eye(N)[idx]这种写法要求标签是从 0 开始的连续整数如果你的类别是字符串编码先映射成整数再做 one-hot。对形状比较敏感的是第一个返回值x两层网络要求二维数组卷积网络要求四维数组 (N, C, H, W)换网络时这里要同步改。5.3 加训练日志与过程可视化不打断训练进程的调试手段鱼书的 Trainer 默认会在每个 epoch 打印 loss但信息量有限。改造训练循环时我建议在附加的日志回调里记录每个 batch 的 loss 和当前学习率而不去改动 Trainer 内部计算梯度的逻辑。这样既保留了原实现的可信度又拿到了足够的调参信息。class SimpleLogger: def __init__(self): self.history [] def log(self, epoch, batch, loss): self.history.append((epoch, batch, loss)) if batch % 100 0: print(fepoch {epoch} batch {batch} loss {loss:.4f})逻辑说明这个日志类独立于训练过程通过回调方式在每个 batch 后记录 losshistory列表最后可以转成 numpy 数组画曲线。不需要分布式框架也不需要改动网络代码。实践中我一般把曲线的横轴设为实际见过的样本数即epoch * 数据总量 batch * batch_size这样不同批量配置的曲线才有可比性。参数说明打印频率batch % 100 0是为了避免输出刷屏batch 总数不足 100 时每轮都会打印。loss:.4f控制显示精度调试初期想看得更细可以改成:.6f。6. 验证反向传播写没写对数值梯度对比这个习惯能省掉一半调试时间每次新写或修改一个层我的习惯是先做一次梯度检查再跑训练。这里的做法是在一个随机初始化的网络里对同一份数据分别调用数值梯度和反向传播如果两者差异在极小范围就认为反向传播实现正确可以放心训练一旦差异很大立刻回去查层实现不要浪费时间猜“是不是学习率不对”。鱼书 ch05 的代码里就带着这个对比逻辑你可以直接把它抽成工具函数放到自己的脚手架里。def check_gradient(net, x_batch, t_batch, threshold1e-7): grad_num net.numerical_gradient(x_batch, t_batch) grad_bp net.gradient(x_batch, t_batch) for key in grad_num.keys(): diff np.abs(grad_num[key] - grad_bp[key]).max() norm np.abs(grad_num[key]).max() 1e-12 rel_error diff / norm flag OK if rel_error threshold else NG print(f{key}: relative error {rel_error:.2e} [{flag}])逻辑说明这里没有比平均差异而是比最大相对误差。因为梯度矩阵里可能存在个别元素极小的情况平均差异会把局部错误稀释掉最大相对误差更能暴露单点错误。norm里加1e-12是为了防止梯度为零时除零报错。相对误差小于 1e-7 基本可以认为实现正确在 1e-4 到 1e-5 之间说明有轻微偏差需要检查超过 1e-3 就一定是层实现有 bug不值得在这条路上继续调参。我自己的教训是有一段时间改了一个带 padding 的卷积层觉得逻辑没问题就直接丢进训练结果 loss 反复横跳。回头跑了这个 check发现把 pad 的位置写错了一格导致边界像素计算偏了几像素。后来凡是新写层先跑 gradient check 成了固定动作。它不难每次也就多花几十秒但能让你从“玄学调参”回到“确定性的调试”。这也是我认为鱼书整套代码里最值得带走并形成习惯的一处设计。希望这个对比函数能帮你少走一段弯路。本文还有配套的精品资源点击获取