用Python和NumPy手写神经网络:反向传播原理与手写数字识别

发布时间:2026/10/13 19:11:30
用Python和NumPy手写神经网络:反向传播原理与手写数字识别 如果你也翻过几页机器学习教材又因为满屏的矩阵符号和“从零开始搭建PyTorch”的标题而被劝退那这本《Python神经网络编程》可能就是你一直在找的那本书。作者塔里克·拉希德Tariq Rashid的写作思路非常明确不依赖任何深度学习框架只用Python基础库加NumPy和SciPy亲手写一个能识别手写数字的神经网络。我读完之后最大的感受是它把“反向传播”这个劝退大户拆成了连初中生都能看懂的直观步骤。这篇文章就是我的完整读书笔记包含对每一章核心内容的拆解、我复现代码时踩过的坑以及做完调参实验之后的一些真实体会。无论你是刚入门Python的开发者还是想深入理解深度学习底层原理的学习者这篇文章都应该能帮到你。1. 这本书是什么以及为什么我推荐每个入门者都读一遍1.1 一本不依赖任何框架的神经网络入门书市面上讲神经网络的资料成千上万但绝大多数要么是纯理论PPT要么是某个框架的API教程。这本书走的是第三条路让你从最底层亲手构建一个完整的神经网络。全书的核心代码大概只有200行左右你不需要安装TensorFlow不需要装PyTorch只要有一个能跑NumPy的Python环境就足够了。书的正文分成三大块第一部分解释神经网络的工作原理包括权重、激活函数、误差和梯度下降这些核心概念第二部分进入实战用MNIST手写数字数据集训练一个识别器并给出了完整代码第三部分是对模型的改进建议和更深入的思考。整本书读下来你会发现自己不光是“会调库”而是真的能理解每一行代码在数学上对应什么操作。作者在书里反复强调一个观点不要盲目复制别人的代码自己动手写一遍才能真正建立直觉。这句话贯穿全书。每章后面的练习题也都是围绕这个目标设计的比如“如果只有两个输入节点分类结果会怎样变化”“试试把学习率调成1.0会发生什么”这类实操性问题而不是空泛的思考题。1.2 正确的读法先看思路再敲代码最后补数学我自己的阅读过程比较波折第一遍通读用了大约三个晚上第二遍对照代码逐行运行又花了一个周末第三遍回头自己推导公式时才觉得真正读透了。总结下来这本书的正确打开方式可以分为三步。第一步是通读不需要在数学公式上死磕只要在脑子里建立起“神经网络是什么、训练在做什么”的整体画面。第二步是照书敲代码注意是所有代码一个字符都不跳。敲的过程中你会自然遇见很多“哦原来这里要这样处理”的细节。第三步才是回头补数学把书里的加权求和、sigmoid导数、误差反向传播这些公式亲手推一遍你会发现它们没有想象中复杂。有一点必须提醒第二章是全书的心脏一定不能跳读。书里的神经网络最核心的“训练”逻辑全部集中在这里第三章的MNIST实战只是套用一个已经构建好的框架。如果第二章只是草草翻过后面大概率会卡住。2. 神经网络的骨架输入层、隐藏层、输出层到底在算什么2.1 一次前向传播的计算过程我最初看书名以为会涉及很多晦涩的数学推导结果第二章开头就用一个“2-3-2”的小网络把整个计算过程写成了表格手把手教你算了一遍。这个设计真的很聪明先用最简单、规模最小的网络建立直觉再推广到真正的784-100-10结构。所谓前向传播就是把输入信号从输入层逐层向后传直到输出层的过程。以MNIST手写数字为例输入层有784个节点对应一张28x28灰度图的每个像素值隐藏层可以选100个节点输出层有10个节点分别代表数字0到9。每一层之间的连接都有权重信号从上一层传到下一层时先做加权求和再经过激活函数得到这一层节点的输出。用矩阵写更简洁。设输入向量为I输入层到隐藏层的权重矩阵为W那么隐藏层的输入就是X W · I经过sigmoid激活函数后得到隐藏层输出H sigmoid(X)。同理输出层的输入就是W2 · H再经过一次激活就得到最终输出。书中用矩阵形式写出来之后整个前向传播就三行代码的事。实际敲的时候我踩过一次比较经典的坑矩阵点乘时维度对不上。这里记住一个规律前一层有多少个节点权重矩阵的列数就是多少后一层有多少个节点权重矩阵的行数就是多少。方向写反NumPy会直接报错。2.2 sigmoid函数在干什么压缩、光滑、求导简单为什么每层都要跟一个激活函数如果只是多层线性变换的叠加那不管堆多少层本质上还是线性变换表达能力非常有限。sigmoid函数S(x) 1/(1e^(-x))在这里承担的任务是把任意大小的加权求和结果压缩到(0,1)区间同时给网络引入非线性让它能拟合更复杂的模式。书里给了很多直观理解的方式。你可以把sigmoid想象成一根平滑的S形管道非常大的正数进去出口接近1非常大的负数进去出口接近00进去出口正好是0.5。这种平滑特性非常关键因为我们后面要用梯度信息调整权重如果激活函数是阶跃函数那除了台阶处以外的所有位置梯度都是0网络根本学不动。还有一个特别实用的原因sigmoid的导数形式极其简洁。对sigmoid求导得到的恰好是O(1-O)其中O是sigmoid的输出值。这意味着我们在反向传播时甚至不需要重新算复杂的导函数只要拿着当前节点的输出值一减一乘就拿到了导数。书里把这个性质称为“服务梯度下降的完美激活函数”这个说法很贴切。2.3 权重初始化的坑全零初始化的对称性问题很多第一次接触神经网络的人会想既然权重是要学出来的那干脆全部初始化为0好了。书里专门解释了为什么这是个大坑。如果同一层所有节点的初始权重完全一样那它们接收到的输入相同、输出相同、误差也相同更新后权重依然相同。也就是说这一层无论设置多少个节点实际上都退化成了一个节点网络失去了表达能力。正确的做法是随机初始化。书中的实现方式是使用正态分布生成一组随机权重均值设为0标准差设为1除以传入节点数的平方根。这样设置的目的是控制初始加权求和的规模避免输入值太大直接进入sigmoid的饱和区。饱和区附近导数接近0梯度几乎消失训练一开始就会卡死。后来我在实验里也验证了这一点把初始权重标准差调大到几倍网络准确率立刻下降而且训练初期的收敛速度明显变慢。3. 反向传播没想象中难——书里的推导路径其实是这样的3.1 误差分派一个最朴素的“错多少按比例摊”反向传播是很多人的噩梦但这本书的推导路径真的把门槛降到了很低。它的核心思路从“误差”两个字开始输出层的每个节点都会算出一个输出值和目标值一比较就得到该节点的误差。问题是输出层的误差只能告诉我们“这两个节点错了”但应该怪谁呢是怪我前面某个隐藏层节点还是怪某个具体权重书里的处理方法是足够朴素的按连接权重的比例把误差回传。如果输出层节点a和隐藏层节点b之间的权重比较大那b就要为a的误差承担更多责任反之承担少一些。这就是“反向传播”四个字最本质的含义——沿着连接的反方向把误差摊还给每个节点。我当时看完这个解释立刻觉得之前的恐惧很没必要。整个推导过程没有复杂的公式只需要做几组简单的乘法。书中甚至专门列了一张表一步一步用手算的方式演示了误差如何在2-3-2网络里从输出层传回隐藏层。你可以完全跟着算一遍整个过程不超过十分钟但收获非常大。3.2 梯度下降沿着最陡的下坡迈步知道每个节点“错了多少”还不够我们更需要知道“该往哪个方向调整权重”。这个概念被无数资料包装得高深莫测但书里用了一个非常好的比喻想象你站在山坡上雾气很浓只能感受到脚底下的坡度。梯度下降做的就是感受当前点的最陡下坡方向然后朝那个方向迈一小步。反复迈步最终就能走下山谷也就是找到让误差最小的权重组合。每次走多长就是学习率。书里强调了一件事步长太大可能跳过最低点步长太小又走得极慢甚至卡在半路。训练开始前先猜测一个学习率再根据效果调整是最常见的做法。这个我后面实验部分会详细说。权重更新公式最终呈现为新权重 旧权重 - 学习率 × 当前点的梯度。为什么是减号因为梯度本身指向函数值上升最快的方向我们要找的是误差变小的方向所以必须反着走。这个“为什么是减号”书里专门标注了非常贴心。3.3 权重更新公式逐项拆解完整写出来输出层到隐藏层的权重更新是这样的新权重 旧权重 - 学习率 × 误差 × sigmoid导数 × 当前节点的输入每一项都有明确的含义。误差表示这个权重连接造成的错误有多大误差越大更新幅度越大sigmoid导数表示当前神经元处于什么状态如果输出接近0或1导数接近0更新就会很慢最后乘上的输入信号表示来自前一层节点的信号有多强信号越强的连接越需要为误差负责。我用一个生活化的类比理解它一位老师给一份试卷打回修改意见权重更新就是学生根据每道错题的难度、自己的薄弱程度和这道题占的分数比例决定在哪个知识点上花更多时间。误差代表丢了多少分导数代表这个知识点还有多大提升空间输入信号代表这道题是否值得重点投入。三者相乘就是一个合理的“整改力度”。这样拆开看反向传播就不再是玄学了。4. 从零写一个手写数字识别器核心代码与数据处理细节4.1 一个极简的神经网络类书里给出的代码框架非常精简。我用Python重新整理后核心就是一个类包含初始化、训练、查询三个方法import numpy as np import scipy.special class NeuralNetwork: def __init__(self, input_nodes, hidden_nodes, output_nodes, learning_rate): self.inodes input_nodes self.hnodes hidden_nodes self.onodes output_nodes self.lr learning_rate self.wih np.random.normal(0.0, pow(self.hnodes, -0.5), (self.hnodes, self.inodes)) self.who np.random.normal(0.0, pow(self.onodes, -0.5), (self.onodes, self.hnodes)) self.activation_function lambda x: scipy.special.expit(x) def train(self, inputs_list, targets_list): inputs np.array(inputs_list, ndmin2).T targets np.array(targets_list, ndmin2).T hidden_inputs np.dot(self.wih, inputs) hidden_outputs self.activation_function(hidden_inputs) final_inputs np.dot(self.who, hidden_outputs) final_outputs self.activation_function(final_inputs) output_errors targets - final_outputs hidden_errors np.dot(self.who.T, output_errors) self.who self.lr * np.dot( (output_errors * final_outputs * (1.0 - final_outputs)), np.transpose(hidden_outputs)) self.wih self.lr * np.dot( (hidden_errors * hidden_outputs * (1.0 - hidden_outputs)), np.transpose(inputs)) def query(self, inputs_list): inputs np.array(inputs_list, ndmin2).T hidden_inputs np.dot(self.wih, inputs) hidden_outputs self.activation_function(hidden_inputs) final_inputs np.dot(self.who, hidden_outputs) final_outputs self.activation_function(final_inputs) return final_outputs初始化方法里有两个容易忽略的细节。一是权重矩阵的形状wih的形状是(隐藏层节点数, 输入层节点数)who的形状是(输出层节点数, 隐藏层节点数)方向反了就会在np.dot时报错。二是np.random.normal的标准差用了pow(self.hnodes, -0.5)也就是1/sqrt(隐藏层节点数)这正是前面第2章讲过的控制初始信号规模的技巧。train方法的前半段和query完全一样都是前向传播后半段才是核心分成计算输出误差、把误差回传到隐藏层、逐层更新权重三步。这段代码的写法非常规整每一行都能和上一章的数学公式对应起来。我在自己的电脑上手动输入这段代码时遇到最多的错误就是漏写.T转置导致矩阵乘法维度不匹配。如果你想完全复现建议对照着书里对每个数组.shape的标注先打印出每个中间变量的形状。4.2 MNIST数据解析与归一化灰度、CSV、标签MNIST是一个经典的手写数字数据集包含60000张训练图片和10000张测试图片每张都是28x28的灰度图。官方提供的CSV格式很简单每行第一个数字是图片的标签后面784个数是像素灰度值范围0到255。数据进入网络之前需要做两件事。第一是归一化。书里没有直接用像素值除以255完事而是做了一个小偏移像素灰度除以255再乘以0.99最后再加上0.01。也就是说原本的0变成0.01原本的255变成1.0。为什么要留这0.01因为sigmoid在极端靠近0和1的位置导数非常小而输入为0还会让梯度更新时乘上一个0导致该条连接永远无法更新。这个细节书里特意加粗提醒值得留意。第二是目标值的编码。输出层的10个节点代表数字0到9训练时目标向量只有目标数字对应位置为0.99其余全部填充0.01。这里同样不直接用1和0背后的原因和输入归一化是一致的sigmoid的输出永远到不了1也到不了0如果目标设为1和0网络就必须把权重推向无穷大才能无限逼近这个目标值。书里将0.99和0.01称为“目标值的软边界”这个说法很形象。数据处理部分的参考代码大致是这样import numpy as np data_file open(mnist_train.csv) data_list data_file.readlines() data_file.close() for record in data_list: all_values record.split(,) inputs (np.asfarray(all_values[1:]) / 255.0 * 0.99) 0.01 targets np.zeros(output_nodes) 0.01 targets[int(all_values[0])] 0.99 n.train(inputs, targets)测试阶段则是对测试数据的每一行做同样的预处理用query方法拿到输出再用np.argmax选出最大的输出索引和真实标签比较。4.3 实测结果训练一轮和训练五轮的差距我用默认参数跑了一遍784个输入节点、100个隐藏节点、10个输出节点、学习率0.3。只训练1个epoch时测试集准确率大概在93%左右训练5个epoch后准确率能到97%附近。这个提升幅度非常说明问题即便同样一套网络结构多轮迭代能让权重持续微调准确率也顺势上去。我顺手把训练轮数拉到10、20、50发现准确率不再单调上升而是稳定在97%到97.5%之间个别时候还有轻微回落。这个现象就是后面要说的过拟合前兆网络在训练集上越来越好但对测试集的泛化能力几乎到顶了。书中也提到继续增加训练轮数的边际收益很小真正值得关注的是数据质量、网络结构和学习率这三者的配合。5. 入门之后的事学习率、隐藏层节点数和我的调参实验5.1 学习率对比0.01学不动0.3刚好1.0会震荡书里提到学习率是“需要亲手试出来的参数”我于是做了一个小实验保持其他条件不变只改学习率。结果如下学习率训练5个epoch后的测试集准确率直观感受0.01约89%收敛极慢明显欠拟合0.1约96%表现正常略微保守0.3约97%效果最好推荐值1.0约95%训练曲线明显震荡步子太大损失值上下跳动学习率太小的问题很好理解权重每次只挪一点点5个epoch根本不够它收敛到位。学习率太大会导致步子跨过最优点又弹回来损失值震荡甚至越震荡越偏离。从这个表可以直观看出0.3确实是这本书默认值背后比较稳健的选择。5.2 隐藏层节点数3到200的变化隐藏层节点数决定这个三层的网络能拟合多复杂的函数。我把隐藏层节点数分别设成3、10、100、200固定学习率0.3、训练5个epoch隐藏层节点数测试集准确率单个epoch耗时粗略3约82%很短10约94%较短100约97%中等200约97%几乎无提升明显变长节点数太少网络能区分的模式太少实际表现接近一个线性分类器节点数到100之后准确率基本进入平台期再翻倍只会增加训练时间对结果帮助不大。这个实验直接帮我校准了“模型容量”这个概念——并非容量越大越好够用且不拖慢训练才是更实际的选择。5.3 自己画图的识别实验预处理才是重头戏训练完模型后书里鼓励读者去做一件有意思的事用自己的笔迹测试模型。我也试了发现真正的难点不在网络而在图像预处理。MNIST是黑底白字画布是28x28而我自己用绘图工具画的图通常是白底黑字、尺寸也大得多。处理流程大致是这几步用PIL把图片读取为灰度图缩放到28x28反色再按同样的公式归一化到0.01到1.0之间。核心代码片段from PIL import Image import numpy as np img Image.open(my_digit_7.png).convert(L) img img.resize((28, 28)) img_array np.asarray(img) # 如果原图是白底黑字需要反色成黑底白字 img_array 255 - img_array inputs (img_array / 255.0 * 0.99) 0.01 outputs n.query(inputs) print(np.argmax(outputs))我实测的结果是写得很规整的数字识别基本正确字迹潦草、位置偏移、笔画太细的识别率掉得很快。这说明训练数据里手写风格的分布与我的笔迹存在明显差异也直观体会到数据预处理和输入分布一致性有多重要。如果你也想复现这个实验建议把图片居中裁剪让数字主体尽量占满28x28的中央区域否则模型很容易把大片空白当背景学进去。6. 把这本书读薄又读厚边界认知与下一步学习路径6.1 这本书没讲什么以及为什么没讲也是优点读完后我去翻了目录确认这本书确实没有讲卷积神经网络没有讲Dropout、批归一化、各种优化器也没有任何关于PyTorch或TensorFlow的API。初看会觉得内容不够“现代”但正是这种克制让它成为一本优秀的入门书。原因是它把“神经网络是什么”这个核心问题用最少的依赖完整回答了。如果你先接触的是PyTorch你会把网络定义成一堆层和张量操作很容易忽略每个层里的权重到底在做什么而这本书记录的是手工更新权重的那一瞬间让你真正看见“学习”发生在哪里。有了这个底层画面再去学任何框架都只是换一套语法。6.2 读完可以做的三个小扩张如果不想读完就停在97%的准确率我有三个方向可以推荐难度递增但都建立在书里的知识之上。第一个是给网络加上第二个隐藏层变成四层结构。这需要自己处理两层之间的误差回传把书里的推导逻辑多套一层就行。第二个是把sigmoid换成ReLU观察训练速度的变化。ReLU的导数更简单、计算更快换过去之后你会发现同样的epoch数下收敛特性有明显不同。第三个是换数据集把MNIST换成Fashion-MNIST或CIFAR-10的简化版看看同一个全连接网络在不同数据上的表现有什么差异。这三个实验做完你很容易意识到“网络结构设计”为什么是深度学习里如此关键的话题。6.3 关于“写出来才算真懂”的阅读建议书的后记里有一句话让我印象很深大意是“读完代码和真正写出代码之间的距离比想象中要大得多”。我这次的体验也完全如此。前两遍看书我自认为逻辑都清楚直到把代码里的每一个shape、每一步矩阵乘法都对上公式之后才敢说我真正读完了这本书。所以我最后的建议很简单无论是学习哪本技术书都不要只看作者的结论。拿到代码之后先原样运行一遍再故意改坏一个参数比如把学习率调成0.005或者把权重初始化标准差放大十倍观察模型的反应。这是个特别好的“排错练习”它能帮助你真正建立对参数和结构的直觉。毕竟神经网络不是靠读懂的是靠一遍遍写和改才慢慢长在脑子里的。

关于本文作者

来自尧图内容编辑团队

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

尧图内容编辑团队

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

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

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

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

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

网站改版的5个关键决策

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

获取专属建站方案

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

立即免费咨询