资讯详情

手写深度学习代码:从感知机到CNN的Python工程实践

📅 2026/10/19 12:20:38 | 华诺云谱 👁 阅读
手写深度学习代码:从感知机到CNN的Python工程实践
简介鱼书《深度学习入门基于python的理论与实现》的配套示例代码面向正在阅读这本书的深度学习初学者以及需要快速搭建多层感知机、卷积网络实验环境的Python开发者。压缩包内共67个文件以Python源码为主包含59个.py脚本、3个已训练好的.pkl参数文件、2个Markdown说明文档、2张Lena测试图片整体体积约4.44MB结构清晰。资源按书中的ch01至ch08组织覆盖MNIST手写数字识别、误差反向传播、数值梯度、权重初始值比较、批归一化、Dropout、超参数优化、简单卷积网络与深度卷积网络等实验同时提供common公共模块和dataset数据加载脚本方便直接运行与二次修改。已有2534人学习下载适合随书逐章对照练习也可作为深度学习中常用Python代码的查漏补缺参考。1. 鱼书《深度学习入门基于Python的理论与实现》代码从手写感知机到 CNN 的完整工程想入门深度学习的人多半被框架的一行代码出模型劝退过模型是个黑匣子梯度算得对不对全靠玄学调参完全靠猜。鱼书《深度学习入门基于Python的理论与实现》这套配套代码走的是完全相反的路——全工程不依赖任何深度学习框架不挂任何插件从感知机到卷积神经网络每一层激活函数、每一条梯度公式都用 Python 和 NumPy 手写出来。它把深度学习到底在算什么拆成了能逐行读、能打断点、能动手改的普通程序适合想真正弄懂前向传播、反向传播和梯度下降原理的开发者也适合拿这套代码当实验脚手架做二次开发。2. 工程结构与技术选型按章组织的零框架代码库2.1 目录结构ch01 到 ch08 各自承载的实验先把工程骨架摸清楚。这份代码的目录严格按书的章节展开从 ch01 到 ch08外加两个共享目录 common 和 dataset整体结构如下├── ch01/ # 感知机、NumPy 基础语法 ├── ch02/ # AND / OR / NAND / XOR 逻辑门 ├── ch03/ # 三层神经网络前向传播、MNIST 识别 ├── ch04/ # 两层神经网络、数值微分求梯度 ├── ch05/ # 误差反向传播法训练 ├── ch06/ # 优化器、权重初始化、BatchNorm、Dropout ├── ch07/ # 卷积神经网络、卷积核可视化 ├── ch08/ # 深度卷积网络 ├── common/ # 公共模块函数、层、优化器、训练器 └── dataset/ # MNIST 数据集加载脚本想快速体会各章实验的差异可以按这张表挑脚本先跑。目录承载内容值得先跑的脚本ch02用感知机实现逻辑门xor_gate.pych03MNIST 推理、三层前向传播neuralnet_mnist.pych04数值微分、两层网络训练train_neuralnet.pych05反向传播训练、计算图示例layer_naive.pych06优化器对比、过拟合对策optimizer_compare_mnist.pych07卷积与池化实现train_convnet.pych08深度卷积网络训练train_deepnet.pycommon 是这套代码真正的复用核心。functions.py 放激活函数和损失函数gradient.py 放数值微分和梯度下降layers.py 把乘法层、加法层、ReLU、Affine、SoftmaxWithLoss、Convolution、Pooling 全部封装成带 forward 和 backward 的类optimizer.py 实现了 SGD、Momentum、AdaGrad、Adam 四种更新规则trainer.py 把训练循环封装成通用类ch06 之后的高层实验基本靠它驱动。理解这套代码优先把 common 下这三个文件吃透其余章节脚本只是不同的数据流组合。2.2 技术选型为什么只用 Python NumPy不碰框架这套代码刻意不引入任何深度学习框架原因很实在。框架把反向传播做成了编译好的黑匣子你调用 loss.backward() 就能拿到梯度但梯度是怎么沿计算图一层层传回来的框架不会告诉你。鱼书把每个算子拆成带 forward 和 backward 的类就是为了让你亲手把链式法则完整走一遍。其次NumPy 的向量化计算天然贴合批量数据一次 forward 处理 100 张 MNIST 图片就是几个矩阵乘法性能完全够用。最后是成本问题——MNIST 只有 28x28 灰度图、6 万训练样本CPU 上训练一个两层网络不过几分钟根本用不上 GPU 和分布式能力。这套选型决定了它的运行方式NumPy 负责数值计算matplotlib 负责画损失曲线和卷积核可视化除此之外没有第三个运行时依赖。这也是它二次开发价值高的原因——任何一层都可以单独抽出来替换成自己的实现不用处理框架版本和底层接口的兼容问题。2.3 环境跑通安装依赖、加载 MNIST 与第一个脚本环境准备非常简单Python 3.x 环境下装两个包就够pip install numpy matplotlibMNIST 数据由 dataset/mnist.py 的 load_mnist() 函数统一管理负责下载、解压、归一化和 one-hot 编码第一次调用会在 dataset 目录下生成 mnist.pkl 缓存。这个函数是所有章节共用的数据入口参数含义值得记一下(x_train, t_train), (x_test, t_test) load_mnist( normalizeTrue, flattenTrue, one_hot_labelTrue)normalizeTrue 会把像素值从 0~255 缩放到 0~1flattenTrue 把每张 28x28 图拉平成 784 维向量one_hot_labelTrue 把标签转成 10 维 one-hot 向量。代码里经常出现的 784 和 10 这两个数字就是从这儿来的。第一个验证环境的脚本是 ch03/neuralnet_mnist.py它加载预训练权重对测试集做推理并打印准确率python ch03/neuralnet_mnist.py能跑到 90% 以上的识别准确率说明环境、数据、权重文件三个环节都通了之后就可以按章节顺序把 train_neuralnet.py、train_convnet.py 一路跑下去。提示首次运行如果卡在数据下载多半是网络问题手动把 mnist.pkl 放进 dataset 目录即可具体做法见第 5 章排查。3. 核心模块拆解一个前向传播从进入到返回的完整链路3.1 激活函数实现sigmoid 的数值稳定与 softmax 的防溢出common/functions.py 是所有网络层共用的地基。sigmoid 和 softmax 这两个函数最基础实现也最有讲究import numpy as np def sigmoid(x): return 1 / (1 np.exp(-x)) def softmax(x): if x.ndim 2: x x.T x x - np.max(x, axis0) y np.exp(x) / np.sum(np.exp(x), axis0) return y.T x x - np.max(x) return np.exp(x) / np.sum(np.exp(x))sigmoid 就是标准公式输入一个实数输出 0~1 之间的概率值中间层的隐藏激活全靠它。softmax 的关键在减最大值这一步如果不减 np.max(x)当输入分数达到几百时np.exp(x) 会直接溢出成 inf结果全是 nan。这也是手写深度学习最常见的数值稳定性问题之一。注意 softmax 对二维输入做了转置处理按每一列也就是每个样本分别减去该列的最大值最后再转置回来。这个写法是为了避免对 batch 中所有样本统一减最大值——每个样本的分布不同必须分开归一化。一维输入则直接处理。3.2 损失函数交叉熵误差的 batch 化处理分类任务的损失函数是交叉熵误差鱼书里的实现同时兼容 one-hot 标签和数字标签两种形式def cross_entropy_error(y, t): if y.ndim 1: t t.reshape(1, t.size) y y.reshape(1, y.size) if t.size y.size: t t.argmax(axis1) batch_size y.shape[0] return -np.sum(np.log(y[np.arange(batch_size), t] 1e-7)) / batch_size第一段处理单样本输入统一升成二维。第二段是关键技巧如果 t 是 one-hot 编码它的形状和 y 一样就先通过 argmax 转回数字标签如果 t 本来就是数字标签就直接用。然后通过 y[np.arange(batch_size), t] 这个花式索引取出每个样本对应类别的预测概率。加 1e-7 是为了防止 y 里出现 0 导致 log(0) 变成负无穷。最后除以 batch_size 做平均得到的是每个样本的平均损失这样不管 batch 取 100 还是 1000损失值都在同一量级便于比较。这套兼容写法在二次开发时可以直接抄不用自己区分标签格式。3.3 TwoLayerNet预测、损失、精度串起完整链路ch04 和 ch05 里的 two_layer_net.py 是整本书的模型骨架一个两层神经网络类完整展示了前向传播的链路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 y softmax(a2) return y def loss(self, x, t): y self.predict(x) return cross_entropy_error(y, t) def accuracy(self, x, t): y self.predict(x) y np.argmax(y, axis1) t np.argmax(t, axis1) return np.sum(y t) / float(x.shape[0])权重用 np.random.randn 生成标准正态分布随机数再乘上 weight_init_std0.01 缩小数值偏置初始化为零。输入 x 的形状是 (batch_size, 784)W1 是 (784, hidden_size)dot 之后得到 (batch_size, hidden_size)经过 sigmoid 再到第二层最终 softmax 输出 (batch_size, 10)。整个过程没有任何框架介入每个矩阵的 shape 都是看的见算得清的。accuracy 方法里把预测和真实标签都做 argmax 取最大概率的类别然后比较相等个数除以样本数。这个方法是评估模型最直接的手段后面训练循环里每个 epoch 都会用到。4. 反向传播实战梯度计算与参数更新的完整实现4.1 计算图层封装MulLayer 与 ReLU 的局部梯度反向传播的核心是把每个算子看成一个带 forward 和 backward 的层。ch05/layer_naive.py 里的乘法层是最简单的例子class MulLayer: def __init__(self): self.x None self.y None def forward(self, x, y): self.x x self.y y return x * y def backward(self, dout): dx dout * self.y dy dout * self.x return dx, dyforward 里把输入存下来backward 里用链式法则计算局部梯度对 x 的梯度等于传入梯度乘以 y对 y 的梯度等于传入梯度乘以 x。这个先存输入、后算梯度的模式是所有网络层的标准写法。ReLU 层的实现更典型它用 mask 记录正向传播时哪些位置的输入小于等于 0反向传播时把这些位置的梯度直接清零class ReLU: def __init__(self): self.mask None def forward(self, x): self.mask (x 0) out x.copy() out[self.mask] 0 return out def backward(self, dout): dout[self.mask] 0 return doutReLU 在 x 0 时导数恒为 1在 x 0 时导数为 0所以 backward 不需要任何计算只靠 forward 阶段记录的 mask 就能完成梯度传递。这也是 ReLU 比 sigmoid 计算量小的原因——sigmoid 反向传播要做两次浮点运算ReLU 只做一次布尔索引。mask 的内存开销和输入一样大但在 MNIST 规模下完全可接受。4.2 gradient 方法误差反向传播法替代数值微分ch04 的 numerical_gradient 用差分近似梯度对每个参数要做两次前向传播两层网络几千个参数就要跑几千次慢得没法训练。ch05 的 two_layer_net.py 里用反向传播重写了 gradient 方法一次前向、一次反向就拿到所有梯度def gradient(self, x, t): W1, W2 self.params[W1], self.params[W2] b1, b2 self.params[b1], self.params[b2] grads {} batch_num x.shape[0] # forward a1 np.dot(x, W1) b1 z1 sigmoid(a1) a2 np.dot(z1, W2) b2 y softmax(a2) # backward dy (y - t) / batch_num grads[W2] np.dot(z1.T, dy) grads[b2] np.sum(dy, axis0) da1 np.dot(dy, W2.T) dz1 sigmoid_grad(a1) * da1 grads[W1] np.dot(x.T, dz1) grads[b1] np.sum(dz1, axis0) return gradsbackward 部分最关键的就两行。dy (y - t) / batch_num 是 SoftmaxWithLoss 层的反向传播结果交叉熵损失对 softmax 输入的导数恰好就是 y - t这是 softmax 和交叉熵配对使用带来的数学简化。得到 dy 后W2 的梯度是 z1 的转置乘 dyW1 的梯度则把误差再往上一层传先乘 W2 的转置得到中间梯度 da1再用 sigmoid_grad 乘上激活函数的局部梯度。sigmoid_grad 在 functions.py 里定义为 (1.0 - sigmoid(x)) * sigmoid(x)即 sigmoid 导数的标准公式。注意反向传播的矩阵乘法方向全是转置过的这一点写错 shape 会直接报错报错信息通常会提示维度对不上对照前向传播的 shape 逐步检查是最快的排查方式。4.3 训练循环batch 采样、参数更新与 loss 收敛ch04 和 ch05 的 train_neuralnet.py 结构几乎一样区别只在用 numerical_gradient 还是 gradient。核心训练循环如下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]每轮迭代从 6 万训练样本里随机抽 100 个形成 mini-batch算这批数据的梯度然后沿负梯度方向更新四个参数。np.random.choice 不指定 replace 参数默认有放回抽样所以每轮 batch 都不一样这就是随机梯度下降中的随机来源。两个版本跑起来差别非常大对比项ch04 数值微分ch05 反向传播单次迭代耗时秒级毫秒级计算次数每个参数 2 次前向1 次前向 1 次反向精度有限差分近似解析解适用场景梯度验证实际训练一般建议第一次跑 ch04 的版本用 iterations100 体验一下速度感受差分的笨重然后切到 ch05。训练结束后打印 loss应该能看到从 2.3 左右一路降到 0.1 以下测试集 accuracy 能到 90% 以上。5. 避坑指南手写深度学习代码的五个高频翻车点5.1 MNIST 数据下载失败mnist.pkl 放不进去的玄学现象运行任何用到 load_mnist 的脚本卡在下载阶段最后报 URLError 或超时。原因dataset/mnist.py 在本地没有 mnist.pkl 时会尝试从网络拉取。网络受限的环境下这个请求经常失败而且脚本重试逻辑很弱报错信息也不够明确容易让人误以为是代码问题。解决手动找一份 mnist.pkl 放进 dataset 目录。load_mnist 加载时优先检查本地缓存只要文件在且格式对就不会再走网络。放好后重新跑脚本跳过下载直接进入处理流程。5.2 NumPy 版本太新np.float 与 np.math 的兼容报错现象import 或运行时报 AttributeError提示 module numpy has no attribute float 或者 no attribute math。原因这套代码部分脚本沿用了较老写法np.float、np.int 等别名在 NumPy 1.24 被移除np.math 在 1.25 之后也没了。新装环境的 NumPy 默认版本大概率踩中。解决报错文件里把 np.float 改成 floatnp.float32 改成 np.float32 本身不受影响np.math 改成 math 并补 import math。不想改代码的话装 NumPy 1.23.x 也可以规避但不建议为了跑老代码锁死版本改那两处更干净。5.3 数值微分慢到怀疑人生别拿 ch04 梯度跑全量数据现象ch04 的 train_neuralnet.py 把 iters_num 设成 10000跑了一小时 loss 还没降到底。原因数值微分对每个参数都要做两次前向传播。两层网络约 784 乘以 50 加 50 乘以 10参数量四万多一轮迭代就是八万多次前向10 轮迭代直接百万级计算CPU 根本扛不住。解决先用 iters_num100 验证流程能跑通真正训练切换到 ch05 的 gradient 版本。也可以保留 ch04 的 numerical_gradient 方法只在小 batch 上对比验证梯度正确性训练交给反向传播。这套分工原本就是书里设计的节奏。5.4 loss 变 nan、accuracy 卡在 10%归一化与学习率现象训练循环里 loss 前几轮正常突然变成 nan或者从头到尾 accuracy 都在 10% 附近抖动和随机猜差不多。原因两类情况。loss 变 nan 通常是学习率太大参数更新步长过大导致梯度爆炸accuracy 卡死在 10% 大概率是输入没归一化原始像素值 0~255 和初始权重相乘后激活值饱和梯度传不回去。解决学习率从 0.1 往下降一档试 0.01确认 load_mnist 时传了 normalizeTrue没有的话手动 x_train x_train / 255.0。排查时可以打印第一层激活值的分布如果大量集中在 0 或 1 附近就是饱和了。注意这两个问题在 ch04 和 ch05 的脚本里默认配置是对的基本是改参数或改代码时引入的。5.5 im2col 展开后内存爆炸CNN 实验的血泪经验现象跑 ch07 的 train_convnet.py内存占用直线飙升轻则卡顿重则直接 OOM 被杀。原因卷积层的核心实现 im2col 会把图像数据展开成大矩阵。一张 28x28 的图经过 5x5 卷积核和填充展开后数据量膨胀几十倍batch 再取大一点内存立刻失控。这个展开是一次性载入内存的没有流式处理。解决调小 batch_size从默认值往 16、8 降或者把 iters_num 缩短只验证链路。如果只是验证卷积实现是否正确可以先跑 ch07/gradient_check.py 这个小脚本它在极小规模上做数值梯度对比内存压力小得多。理解 im2col 的展开逻辑后再决定是不是要自己写更省内存的滑动窗口版本。6. 二次开发把鱼书代码改造成自己的实验工具箱拿到这份代码别急着跑完就扔它最值钱的地方在于每个模块都能单独抽出来组合成自己的实验。第一个改造建议建立梯度验证流程。用 ch04 的 numerical_gradient 当基准对比 ch05 的 gradient 输出两者误差在 1e-4 量级就说明反向传播写对了。我一般会在网络结构改完、开训之前先跑一遍这个对比几秒钟就能拦住大部分梯度 bug。代码可以复用 ch07 里的 gradient_check.py它已经内置了权重初始化、前向、数值梯度对比的完整框架把网络类换掉就行。第二个改造建议换掉手写更新规则。ch06 的 common/optimizer.py 提供了 SGD、Momentum、AdaGrad、Adam 四个类接口统一是 update(params, grads)。训练循环里原来四行手动更新可以替换成optimizer Adam(lr0.01) for i in range(iters_num): # ... 采样和 forward grad network.gradient(x_batch, t_batch) optimizer.update(network.params, grad)换优化器只需改一个类名这就把 ch04 到 ch06 的实验串联起来了能直观看到 Adam 相比 SGD 的收敛速度差异。第三个改造建议是从两层走向多层。common/multi_layer_net.py 已经实现了可配置层数的网络通过 hidden_size_list 参数指定每层神经元数量配合 common/trainer.py 的 Trainer 类可以大幅简化训练代码。想做数据集扩展、加正则项或自定义损失函数只需要在 common/functions.py 或 common/layers.py 里新增类网络结构不用大改。从那以后我每次改网络结构都强制走一遍梯度对比脚本数值微分和反向传播对不上就绝不开训。这套代码教会我的就是这个习惯——你把它当黑匣子它就会用各种姿势翻车你把它拆开看它反而是最老实的教材。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑