简介:经典日系教材《深度学习入门:基于Python的理论与实现》(即“鱼书”)的全章节配套源码,面向零基础起步的深度学习初学者和想对照原著动手复现实验的开发者,覆盖从感知机、神经网络到卷积网络与深度网络的完整学习链路。压缩包共67个文件,以59个Python脚本为核心,辅以3个pkl预训练参数、2个Markdown说明文档、2张图像示例及gitignore配置,整体仅4.44MB,轻量易用。目前已有2534人浏览学习。代码按原书章节ch01至ch08清晰分目录,包含MNIST手写数字识别、梯度下降与误差反向传播、权重初始化对比、Batch Norm、Dropout、多种优化器比较、简单卷积网络与深度卷积网络等实验脚本,并附带训练好的预训练参数文件,可直接加载运行并可视化中间结果。读者能边读书边跑代码,直观理解每章算法的实现细节与调参效果,是系统掌握深度学习原理和Python底层实现的高质量辅助资源。1. 深度学习入门,为什么很多人卡在“看不懂代码”上很多人学深度学习,不是被数学吓退的,而是卡在第一步:书翻开了,Python也装好了,结果看到的代码不是一行行能照着敲的完整例子,而是一堆半截代码。这种感觉就像学游泳,教练在岸上把动作讲得头头是道,你一下水还是呛水。鱼书《深度学习入门:基于Python的理论与实现》的配套代码,就是那块能让你先下水扑腾的浮板。这本书的特别之处在于:它从头到尾不用 TensorFlow、PyTorch 这类现成框架,而是用 Python 和 NumPy 一行一行把神经网络写出来。配套代码则是把书里每一章的示例都整理成能直接运行的程序,从最基础的手写数字识别,到两层神经网络、反向传播,再到卷积神经网络,全部按章节排列好。也就是说,你不光是在读理论,而是真能亲眼看到电脑从一堆像素里学会认数字的全过程。这份资源最适合两类人:一类是 Python 刚入门、想搞懂深度学习到底在干什么的零基础新手;另一类是看了不少理论文章,但从来没自己跑通过一个训练程序的人。如果你是这两种,接下来跟着我从零开始,先把第一个例子跑起来。2. 跑起来之前:先搞懂鱼书代码在解什么题2.1 深度学习在干嘛:先记住“看图猜数字”这一个画面如果你只记住一件事,那就记住:深度学习处理的最典型问题就是“给一张图片,输出一个答案”。鱼书整本书的核心例子是 MNIST 手写数字识别,数据是 28×28 像素的灰度图片,每张图里写了一个 0 到 9 之间的数字,你的任务是让程序看完像素之后,说出一句“这张图是 7”。听起来不难,但难在“程序怎么知道什么是 7”。人眼看到 7,知道它有一条横线加一条斜线;机器看到的只是一串数字,比如某个像素亮度是 0,另一个像素亮度是 255。深度学习要做的,是让计算机自己从几万张带答案的例子里总结出一套判断规则。这套规则在鱼书里不是装在某个现成框架里的黑匣子,而是用矩阵乘法一步步算出来的。所以你学这份代码时,心里始终要有一个画面:前面是一个 784 维的像素向量,中间经过几层计算,最后出来 10 个数字,代表“是 0 的概率、是 1 的概率……”。谁最大,就预测成谁。后面第三章、第五章的所有代码,都是在把这个画面变成一行行能跑的 Python 语句。2.2 鱼书代码包里有什么:先认识四个关键位置下载下来的代码不是一堆散文件,而是按章节组织的目录。我第一次打开时也发懵,后来发现只需要先认准四个位置就能开始。fishbook/ ├── ch01/ # 第一章:Python 基础,入门先看 ├── ch02/ # 第二章:感知机,理解神经网络的雏形 ├── ch03/ # 第三章:神经网络,第一个完整的推理程序在这 │ ├── neuralnet_mnist.py # 用预训练权重做手写数字识别 │ └── sample_weight.pkl # 已经训练好的权重文件 ├── ch04/ # 第四章:两层神经网络的实现与训练 ├── ch05/ # 第五章:误差反向传播,深度学习的关键 ├── ch06/ # 第六章:各种优化技巧 ├── ch07/ # 第七章:卷积神经网络 CNN ├── ch08/ # 第八章:深度学习实战 ├── common/ # 公共函数库,各章共用 │ ├── functions.py │ ├── gradient.py │ └── layers.py └── dataset/ └── mnist.py # MNIST 数据集的下载与读取新手最容易犯的错,是把 ch03 里的某个文件单独拷出来运行。别这么做,因为代码里大量使用了from dataset.mnist import load_mnist这种跨目录导入,必须保持整体目录结构。你只需要在命令行里进入fishbook这个根目录,然后运行对应章节的文件即可。2.3 零基础环境准备:Python 装好、三件套装齐这份代码对环境要求极低,不需要 GPU、不需要装深度学习框架,只需要 Python 和三个第三方库。但我还是建议你先建一个独立环境,这相当于给自己留一颗后悔药:环境弄坏了,删掉重建就行,不会影响系统里其他项目。# 用 conda 创建独立环境,命名为 deeplearn,指定 Python 3.8 conda create -n deeplearn python=3.8 # 激活这个环境 conda activate deeplearn # 安装鱼书代码用到的三个第三方库 pip install numpy matplotlib pillow这是我的血泪经验:鱼书代码是几年前写的,当时主流的 Python 版本比较老。用 Python 3.8 跑兼容性最好,代码基本不用改。装完库之后,你可以验证一下环境是否正常,在命令行里敲一句:python -c "import numpy, matplotlib, PIL; print('环境OK')"能看到环境OK就说明三件套都装好了。注意不要装一堆乱七八糟的深度学习框架插件,入门阶段纯 CPU 加 NumPy 完全够用。软件层面装得越少,后面出问题的概率越低。2.4 为什么这份代码不依赖深度学习框架你可能会问:既然现在大家都在用框架写神经网络,为什么还要学一份老代码?答案恰恰是:这份代码的价值就在于它“老”,老到每一行都自己算。如果你直接上手 PyTorch,一个nn.Linear(784, 50)就把一层网络封装好了,你根本看不到里面发生了什么。而鱼书代码里同一层是这样写的:把输入和权重做np.dot点积,再逐元素加上偏置,再过一个激活函数。每一步都是显式的数学运算,你就知道“哦,原来所谓的一层网络,本质上就是一次矩阵乘法加一个偏移量”。这对于零基础入门来说特别重要。因为深度学习里的梯度下降、反向传播这些概念,用框架一句话就能调用,但新手完全不知道背后是什么,遇到报错也没法排查。先把这份不用框架的代码跑通,相当于给自己补上了地基。之后再学任何框架,看到的都是“哦,这就是把之前手写的步骤封装起来了”,而不是面对一个完全陌生的黑匣子。3. 第一个例子:把 MNIST 手写数字识别跑通3.1 第一步先不写代码:看看 MNIST 数据长什么样MNIST 是深度学习入门的经典数据集,训练集有 6 万张、测试集有 1 万张 28×28 的灰度图片。所谓 28×28,就是横向 28 个像素、纵向 28 个像素,每个像素的亮度用 0 到 255 的整数表示。0 是全黑,255 是全白。我们先跑一小段代码,把数据读进来,看看它的形状:# 从 dataset 目录导入 MNIST 的加载函数 from dataset.mnist import load_mnist # 同时读入训练集和测试集 (x_train, t_train), (x_test, t_test) = load_mnist( flatten=True, # 把 28x28 的图片展平成 784 个像素的一维向量 normalize=False # 先不做归一化,保持 0-255 的原始亮度值 ) print(x_train.shape) # (60000, 784) 训练集有 6 万张图 print(t_train.shape) # (60000,) 每张图对应的数字标签load_mnist是鱼书代码自带的函数,负责下载或读取数据集。flatten=True表示把二维图片拉成一维向量,因为神经网络的输入通常是向量。normalize=False暂时不做归一化,等会儿推理的时候我们再打开。如果你运行这段代码时卡在下载,先别急,第四章会专门讲怎么处理。这一步的关键是让你理解:所谓“一张图片”,在程序里就是一个 784 维的数字数组;所谓“标签”,就是一个 0 到 9 的整数。深度学习要学的,就是从这 784 个数字到那个整数的映射关系。3.2 加载一张图片,把它显示出来光看 shape 没有实感,我们把第一张图真的显示出来,你会第一次亲眼看到“数据长什么样”。import numpy as np from PIL import Image # 取训练集的第一张图 img = x_train[0] # 把 784 个像素还原成 28 行 28 列 img = img.reshape(28, 28) # 转换成 PIL 图片对象并保存到当前目录 pil_img = Image.fromarray(np.uint8(img)) pil_img.save("first_number.png") print("这张图的标签是:", t_train[0])这段代码里,reshape(28, 28)是把一维数组恢复成二维形状,这样我们才能用图片的方式查看它。np.uint8是因为 PIL 库要求像素值是无符号 8 位整数,正好对应 0 到 255 的范围。运行之后,去当前目录找first_number.png这张图片,用看图软件打开它。我第一次看到的时候还挺惊讶的——原来所谓的手写数字数据集,就是这种不太工整、但人眼能认出来的数字图片。保存这张图还有一个好处,就是后面你换了其他图片输入时,能直观对比“程序眼中的输入”和“人眼看到的图”之间的差别。3.3 用预训练权重跑一次推理现在进入正题:用已经训练好的权重,对测试集做预测。这一步不涉及训练,只是把写好的参数代入神经网络,计算输出。import pickle import numpy as np from dataset.mnist import load_mnist def sigmoid(x): """把任意数值压缩到 0 到 1 之间""" return 1 / (1 + np.exp(-x)) def softmax(a): """把一组数值变成概率分布,所有值加起来等于 1""" c = np.max(a) # 减去最大值,防止 exp 指数溢出 exp_a = np.exp(a - c) return exp_a / np.sum(exp_a) def get_data(): """读取测试集,并做归一化:把 0-255 缩放到 0-1""" (x_train, t_train), (x_test, t_test) = load_mnist( normalize=True, flatten=True, one_hot_label=False ) return x_test, t_test def init_network(): """从 sample_weight.pkl 读取已训练好的权重和偏置""" with open("sample_weight.pkl", "rb") as f: network = pickle.load(f) return network def predict(network, x): """用神经网络做一次前向传播,返回 10 个类别的概率""" W1, b1 = network['W1'], network['b1'] W2, b2 = network['W2'], network['b2'] W3, b3 = network['W3'], network['b3'] # 第一层:加权求和 + 偏置 + sigmoid 激活 a1 = np.dot(x, W1) + b1 z1 = sigmoid(a1) # 第二层:同样操作 a2 = np.dot(z1, W2) + b2 z2 = sigmoid(a2) # 第三层:输出层,用 softmax 转成概率 a3 = np.dot(z2, W3) + b3 y = softmax(a3) return y这段代码看着长,其实每个函数只干一件事。get_data里把normalize设为True,是因为归一化能加快计算,让每个像素值都落在 0 到 1 之间。init_network用 pickle 读取权重文件,这个.pkl文件是别人训练好之后保存下来的,我们直接拿来做推理,省去了自己训练的时间。注意predict里的矩阵乘法:x是 784 维向量,W1是 784×50 的矩阵,np.dot(x, W1)算出来的结果是 50 维向量,再加上偏置b1。这就是第三章里反复强调的“层”的本质。3.4 跑一遍完整验证:预测一万张图,算准确率有了上面的函数,我们就可以对测试集做完整的准确率统计:x_test, t_test = get_data() network = init_network() accuracy_cnt = 0 for i in range(len(x_test)): y = predict(network, x_test[i]) # 对第 i 张图做预测 p = np.argmax(y) # 取概率最大的类别 if p == t_test[i]: accuracy_cnt += 1 print("准确率:", accuracy_cnt / len(x_test))np.argmax(y)返回概率最大那个位置的索引。如果最大概率在索引 7 处,就说明模型认为这张图是数字 7。把预测结果和真实标签t_test[i]比一比,相同就累加计数。最终用累计数除以测试集总量,得出准确率。我第一次跑出来准确率大约是 0.93,也就是 93% 左右。这个成绩并不惊艳,但要知道,这是用一个只有三层、总共才几万个参数的神经网络实现的,而且代码全部是 Python 写的。你亲手跑出了这个数字之后,再回头看理论,感受会完全不同。最后给你讲一个小优化:上面的代码是一张一张处理,比较慢。改成一次处理一批图片会快很多,这就是鱼书后面反复提到的 batch(批)概念:batch_size = 100 accuracy_cnt = 0 for i in range(0, len(x_test), batch_size): x_batch = x_test[i:i+batch_size] y_batch = predict(network, x_batch) p_batch = np.argmax(y_batch, axis=1) accuracy_cnt += np.sum(p_batch == t_test[i:i+batch_size]) print("准确率:", accuracy_cnt / len(x_test))区别在于,原来每次输入一张 784 维向量,现在每次输入 100 张组成的二维数组。NumPy 的矩阵运算在批量处理时效率更高,因为底层把循环交给了经过优化的 C 语言库。你先记住这个结论,第四章实现训练过程时会反复用到。4. 避坑指南:新手跑鱼书代码最容易翻车的 5 个地方4.1 现象:提示 No module named 'dataset'现象:你运行neuralnet_mnist.py,报错ModuleNotFoundError: No module named 'dataset'。原因:Python 导入包时,会按sys.path里的路径去查找dataset这个目录。如果你直接用编辑器打开了 ch03 目录下的文件,或者把文件单独拷到了别的地方,Python 就找不到上一级的dataset文件夹。解决:保持目录结构,从fishbook根目录运行。如果实在要在子目录里运行,也可以手动把父目录加入搜索路径:import sys, os sys.path.append(os.pardir) # 把当前目录的上一级加入搜索路径os.pardir就是..,也就是父目录。加了这一句之后,from dataset.mnist import load_mnist就能被找到。我自己的习惯是偷懒,永远在根目录下用命令行运行,这样任何代码都不会出现路径问题。4.2 现象:卡在下载 MNIST 数据集,进度条不动现象:第一次调用load_mnist时,终端一直显示下载状态,等了好几分钟没有反应,甚至直接报连接错误。原因:MNIST 原始数据集托管在国外的服务器上,国内访问时速度很不稳定。load_mnist第一次运行会先检查本地有没有缓存文件,没有就联网下载。解决:最省心的办法是手动下载数据集文件,放到dataset目录下,再重新运行。load_mnist发现本地存在对应文件后就不会再去联网了。如果你下载下来的文件是压缩包形式,保持原始文件名即可,代码会自动识别和缓存为.pkl格式。我第一次就是卡在这里,后来看到有个说法是“把下载这一步替换成国内镜像源”。具体做法是打开dataset/mnist.py,在里面找到原始下载地址那一行,替换成可用的镜像地址。改的时候注意只改 URL,别动其他逻辑。这一步做完,之后所有章节都能顺利读取数据,再也不用等待下载。4.3 现象:print 出来一长串数字,根本看不懂现象:你想看看图片数据是什么样的,于是print(x_train[0]),结果终端刷出一大串 0 和 255 的数字,完全看不出所以然。原因:x_train[0]本来就是 784 个像素值组成的数组,直接打印当然是一串数字,人眼很难从 784 个数字里看出图片内容。解决:数据分析时少看原始值,多看形状和可视化结果。先用print(x_train.shape)确认维度,再用上一章的reshape和Image.save把图片保存出来看。如果是在命令行里,也可以这样快速预览:print(x_train[0].reshape(28, 28))终端会把数字按 28 行 28 列排开,虽然很粗糙,但能隐约看出数字轮廓。这个习惯很重要:以后你处理任何新数据,第一件事永远是打印shape,而不是打印内容。4.4 现象:代码能跑但特别慢,等半天不出结果现象:第三章的推理还好,到了第四章训练时,跑了很久进度条还在原地,你不知道它是死机了还是正常工作中。原因:鱼书代码是纯 Python 实现,没有调用 GPU 加速。训练涉及几万次迭代,每轮迭代又要跑几百张图片的前向和反向计算,CPU 上确实会慢。但这不代表程序死了,而是本来就需要时间。解决:调试阶段先缩小数据量和训练轮数。第四章训练代码里有iters_num(迭代次数)这个变量,先把它从 10000 改成 1000,甚至 200,确认整个流程能跑通、loss 值在下降,再把轮数加回去。我的习惯是先让程序“能跑”,再让程序“跑得快”,别一上来就追求完整训练效果。4.5 现象:改了参数之后,准确率反而下降现象:你想试试调参,把learning_rate从 0.1 改成 0.5,结果损失值乱跳,准确率反而不如默认配置。原因:调参不是越大越好。学习率太大,参数会在最优值附近来回震荡,甚至发散;学习率太小,收敛又慢得让人绝望。隐藏层神经元数量改多了,在小训练集上也很容易过拟合。解决:调参要一次只动一个变量,并且按数量级来试。比如学习率先试 0.1、0.01、0.001 三档,哪个 loss 曲线最平滑就用哪个。建议你每次调参都记一个表:参数试过的值效果学习率0.1 / 0.01 / 0.0010.1 震荡,0.001 太慢,0.01 最优隐藏层大小50 / 100 / 200100 精度略高,但训练更慢调参在入门阶段看起来像玄学,其实就是控制变量加记录日志。你踩过一次坑,后面再调任何参数都会习惯性地先看 loss 曲线。5. 读懂核心代码:三层神经网络的前向传播是怎么算出来的5.1 先把神经网络想成一条流水线很多入门教程一上来就画复杂的神经网络结构图,新手看了头皮发麻。我建议你把三层网络想成一条流水线,原料就是一张 784 维像素向量,它要经过三道工序。第一道工序:把这 784 个像素和 50 组权重做加权求和,每组权重相当于一个“特征探测器”,有的负责探测横线,有的负责探测圆弧。第二道工序:把第一层的 50 个输出,再经过 100 组权重,组合成更抽象的特征。第三道工序:把上一层的 100 个结果映射成 10 个数字,每个数字代表“这是几”的概率。每一道工序做的事情都一样:加权求和、加偏置、过激活函数。差别只在于输入输出的维度不同。理解了这条流水线,再看代码就不会乱了。5.2 逐行拆解 predict 函数我们回到第三章的predict,把它当成解剖对象。假设输入x是一张 784 维的图片:def predict(network, x): # 先去取出每一层的权重 W 和偏置 b W1, b1 = network['W1'], network['b1'] W2, b2 = network['W2'], network['b2'] W3, b3 = network['W3'], network['b3'] # 第一层:矩阵乘法 + 偏置 a1 = np.dot(x, W1) + b1 z1 = sigmoid(a1) # 第二层:重复同样的操作 a2 = np.dot(z1, W2) + b2 z2 = sigmoid(a2) # 第三层:输出层,换成 softmax 得到概率 a3 = np.dot(z2, W3) + b3 y = softmax(a3) return ynp.dot(x, W1)就是矩阵乘法。x的形状是(784,),W1的形状是(784, 50),结果就是(50,)的一维数组,代表这一层 50 个节点的得分。加上b1这个 50 维的偏置向量后,每个节点的分数再传入sigmoid,压缩到 0 到 1 之间,作为这一层最终的输出。第二层同理,输入从 784 变成了 50,权重形状是(50, 100),输出是 100 维。第三层的权重形状是(100, 10),输出是 10 维。你可以这样记:每一层的权重形状,永远是“上一层的输出维度 × 这一层的节点数”。层权重形状偏置形状输出形状输入→第1层(784, 50)(50,)(50,)第1层→第2层(50, 100)(100,)(100,)第2层→输出层(100, 10)(10,)(10,)这个表你对照代码看一遍,整段predict就再也没有神秘感了。它本质上就是三次乘法和加法,中间穿插两个激活函数。5.3 sigmoid、ReLU、softmax:三个函数各管一件事鱼书第三章里出现频率最高的三个函数,新手容易搞混它们的使用场景。我把它们拆开讲清楚:sigmoid的公式是1 / (1 + exp(-x)),作用是压缩到 0 到 1 之间。它用于隐藏层,特点是非常平滑,但有个缺点:当输入绝对值很大时,输出几乎贴到 0 或 1,梯度会变得很小,这就是所谓的梯度消失问题。鱼书第三章用它,是因为它好理解;到第五章之后,你会看到更多代码改用 ReLU。ReLU的公式是max(0, x),简单粗暴:负数一律变成 0,正数保持不变。它训练速度快,因为没有 sigmoid 那种饱和区间,现在大多数网络都在隐藏层用它。鱼书第五章之后,sigmoid基本只在理论演示里出现,实际代码多用ReLU。softmax只用于最后一层,作用是把任意一组实数变成一组加起来等于 1 的概率。注意它有一个细节:计算前先减去这批数据里的最大值,防止exp指数溢出。这就是为什么softmax函数里第一行是c = np.max(a)。三个函数使用位置的区别,你现在不用背,但要知道一个原则:隐藏层用 ReLU 用的多,输出层做分类任务用 softmax。这个记忆点足够支撑你读完前六章。5.4 为什么“推理”和“训练”必须分开看学到这里,你可能会有一个疑问:predict 函数里只是算了一遍加权求和,那网络的权重 W 是从哪来的?答案是:别人用训练过程算出来的。推理前向传播,指的是输入数据顺着流水线往前流,最终得到预测概率。这一过程不需要标签,不需要算损失,只需要有权重就能算。程序做的事就是上文的矩阵乘法,没什么好神秘的。训练反向传播,则是拿着有标签的数据,先做一次前向传播得到预测结果,然后和真实标签比较算出误差,再从输出层往回一层一层地修改权重,让下次预测更准。鱼书第四章会手写实现这个训练过程,包括梯度下降更新参数。新手最容易犯的错,是还没跑通推理,就急着看训练代码,结果被反向传播里的一堆公式劝退。我建议你严格按这个顺序来:先把推理代码反复读到能默写,再进入训练部分。前向传播搞明白了,反向传播不过就是它的“逆过程”,顺带还涉及微积分里的链式法则,但鱼书会用手写代码帮你绕开数学推导。6. 下一步学什么:从“跑通代码”到“改得动代码”6.1 三个递进练法,把代码变成自己的跑通第一个例子只算入门,真正学进大脑靠的是动手改。我推荐三个递进的练法,难度逐级上升。练法一:改超参数。把第四章two_layer_net.py里的学习率改成0.01、0.001,隐藏层大小改成100,然后重新训练,看 loss 曲线的变化。这一步让你亲身体会参数如何影响训练结果。练法二:换激活函数。把隐藏层的sigmoid全部替换成ReLU,再次训练。你会发现代码只要改一行,训练中的 loss 下降趋势就不一样了。这就是为什么后面章节要专门讲激活函数的选择。练法三:改网络结构。把两层网络改成三层,或者把隐藏层节点从 50 改成 200,再对比训练时间和准确率。这个练习能让你理解“深度”到底带来了什么。每改一次,都建议把结果记成一张表:练习内容改哪里观察什么学习率训练代码里的 learning_rate收敛速度、loss 是否震荡隐藏层大小two_layer_net 里的 hidden_size训练耗时和最终准确率激活函数两层之间的 sigmoid 换成 ReLU训练曲线的平滑程度网络层数在 predict 中多加一层表达能力是否变强6.2 之后的学习路径建议如果你把第三章和第四章都跑通了,下一站是第五章“误差反向传播”。这一章是全书最硬核的部分,但你不需要背公式,只需要跟着代码里的layers.py走一遍正向和反向的计算流程,理解每一层保存了什么、更新了什么。之后再进入第七章 CNN,你会发现卷积神经网络的整体结构你早就见过了,无非是多了一些卷积层和池化层。至于要不要尽快去学 PyTorch、TensorFlow,我的建议是暂缓。鱼书这份代码的价值在于脱离框架、看见本质。等你能不看答案就自己写出一层网络的前向传播时,再转框架会顺畅得多。那时候你会觉得框架里nn.Linear只是一个封装好的工具,而你知道盒子里的齿轮是怎么转的。说起来,我从那以后每次拿到一份新代码,都强制自己先跑通、再改参数、再读源码,这个顺序帮我少踩了一半的坑。希望你也能从这个最小例子开始,一步步把鱼书从第三章走到第八章,体会到那种“原来我也能让电脑认数字”的掌控感。希望帮到你。本文还有配套的精品资源,点击获取
简介:经典日系教材《深度学习入门:基于Python的理论与实现》(即“鱼书”)的全章节配套源码,面向零基础起步的深度学习初学者和想对照原著动手复现实验的开发者,覆盖从感知机、神经网络到卷积网络与深度网络的完整学习链路。压缩包共67个文件,以59个Python脚本为核心,辅以3个pkl预训练参数、2个Markdown说明文档、2张图像示例及gitignore配置,整体仅4.44MB,轻量易用。目前已有2534人浏览学习。代码按原书章节ch01至ch08清晰分目录,包含MNIST手写数字识别、梯度下降与误差反向传播、权重初始化对比、Batch Norm、Dropout、多种优化器比较、简单卷积网络与深度卷积网络等实验脚本,并附带训练好的预训练参数文件,可直接加载运行并可视化中间结果。读者能边读书边跑代码,直观理解每章算法的实现细节与调参效果,是系统掌握深度学习原理和Python底层实现的高质量辅助资源。1. 深度学习入门,为什么很多人卡在“看不懂代码”上很多人学深度学习,不是被数学吓退的,而是卡在第一步:书翻开了,Python也装好了,结果看到的代码不是一行行能照着敲的完整例子,而是一堆半截代码。这种感觉就像学游泳,教练在岸上把动作讲得头头是道,你一下水还是呛水。鱼书《深度学习入门:基于Python的理论与实现》的配套代码,就是那块能让你先下水扑腾的浮板。这本书的特别之处在于:它从头到尾不用 TensorFlow、PyTorch 这类现成框架,而是用 Python 和 NumPy 一行一行把神经网络写出来。配套代码则是把书里每一章的示例都整理成能直接运行的程序,从最基础的手写数字识别,到两层神经网络、反向传播,再到卷积神经网络,全部按章节排列好。也就是说,你不光是在读理论,而是真能亲眼看到电脑从一堆像素里学会认数字的全过程。这份资源最适合两类人:一类是 Python 刚入门、想搞懂深度学习到底在干什么的零基础新手;另一类是看了不少理论文章,但从来没自己跑通过一个训练程序的人。如果你是这两种,接下来跟着我从零开始,先把第一个例子跑起来。2. 跑起来之前:先搞懂鱼书代码在解什么题2.1 深度学习在干嘛:先记住“看图猜数字”这一个画面如果你只记住一件事,那就记住:深度学习处理的最典型问题就是“给一张图片,输出一个答案”。鱼书整本书的核心例子是 MNIST 手写数字识别,数据是 28×28 像素的灰度图片,每张图里写了一个 0 到 9 之间的数字,你的任务是让程序看完像素之后,说出一句“这张图是 7”。听起来不难,但难在“程序怎么知道什么是 7”。人眼看到 7,知道它有一条横线加一条斜线;机器看到的只是一串数字,比如某个像素亮度是 0,另一个像素亮度是 255。深度学习要做的,是让计算机自己从几万张带答案的例子里总结出一套判断规则。这套规则在鱼书里不是装在某个现成框架里的黑匣子,而是用矩阵乘法一步步算出来的。所以你学这份代码时,心里始终要有一个画面:前面是一个 784 维的像素向量,中间经过几层计算,最后出来 10 个数字,代表“是 0 的概率、是 1 的概率……”。谁最大,就预测成谁。后面第三章、第五章的所有代码,都是在把这个画面变成一行行能跑的 Python 语句。2.2 鱼书代码包里有什么:先认识四个关键位置下载下来的代码不是一堆散文件,而是按章节组织的目录。我第一次打开时也发懵,后来发现只需要先认准四个位置就能开始。fishbook/ ├── ch01/ # 第一章:Python 基础,入门先看 ├── ch02/ # 第二章:感知机,理解神经网络的雏形 ├── ch03/ # 第三章:神经网络,第一个完整的推理程序在这 │ ├── neuralnet_mnist.py # 用预训练权重做手写数字识别 │ └── sample_weight.pkl # 已经训练好的权重文件 ├── ch04/ # 第四章:两层神经网络的实现与训练 ├── ch05/ # 第五章:误差反向传播,深度学习的关键 ├── ch06/ # 第六章:各种优化技巧 ├── ch07/ # 第七章:卷积神经网络 CNN ├── ch08/ # 第八章:深度学习实战 ├── common/ # 公共函数库,各章共用 │ ├── functions.py │ ├── gradient.py │ └── layers.py └── dataset/ └── mnist.py # MNIST 数据集的下载与读取新手最容易犯的错,是把 ch03 里的某个文件单独拷出来运行。别这么做,因为代码里大量使用了from dataset.mnist import load_mnist这种跨目录导入,必须保持整体目录结构。你只需要在命令行里进入fishbook这个根目录,然后运行对应章节的文件即可。2.3 零基础环境准备:Python 装好、三件套装齐这份代码对环境要求极低,不需要 GPU、不需要装深度学习框架,只需要 Python 和三个第三方库。但我还是建议你先建一个独立环境,这相当于给自己留一颗后悔药:环境弄坏了,删掉重建就行,不会影响系统里其他项目。# 用 conda 创建独立环境,命名为 deeplearn,指定 Python 3.8 conda create -n deeplearn python=3.8 # 激活这个环境 conda activate deeplearn # 安装鱼书代码用到的三个第三方库 pip install numpy matplotlib pillow这是我的血泪经验:鱼书代码是几年前写的,当时主流的 Python 版本比较老。用 Python 3.8 跑兼容性最好,代码基本不用改。装完库之后,你可以验证一下环境是否正常,在命令行里敲一句:python -c "import numpy, matplotlib, PIL; print('环境OK')"能看到环境OK就说明三件套都装好了。注意不要装一堆乱七八糟的深度学习框架插件,入门阶段纯 CPU 加 NumPy 完全够用。软件层面装得越少,后面出问题的概率越低。2.4 为什么这份代码不依赖深度学习框架你可能会问:既然现在大家都在用框架写神经网络,为什么还要学一份老代码?答案恰恰是:这份代码的价值就在于它“老”,老到每一行都自己算。如果你直接上手 PyTorch,一个nn.Linear(784, 50)就把一层网络封装好了,你根本看不到里面发生了什么。而鱼书代码里同一层是这样写的:把输入和权重做np.dot点积,再逐元素加上偏置,再过一个激活函数。每一步都是显式的数学运算,你就知道“哦,原来所谓的一层网络,本质上就是一次矩阵乘法加一个偏移量”。这对于零基础入门来说特别重要。因为深度学习里的梯度下降、反向传播这些概念,用框架一句话就能调用,但新手完全不知道背后是什么,遇到报错也没法排查。先把这份不用框架的代码跑通,相当于给自己补上了地基。之后再学任何框架,看到的都是“哦,这就是把之前手写的步骤封装起来了”,而不是面对一个完全陌生的黑匣子。3. 第一个例子:把 MNIST 手写数字识别跑通3.1 第一步先不写代码:看看 MNIST 数据长什么样MNIST 是深度学习入门的经典数据集,训练集有 6 万张、测试集有 1 万张 28×28 的灰度图片。所谓 28×28,就是横向 28 个像素、纵向 28 个像素,每个像素的亮度用 0 到 255 的整数表示。0 是全黑,255 是全白。我们先跑一小段代码,把数据读进来,看看它的形状:# 从 dataset 目录导入 MNIST 的加载函数 from dataset.mnist import load_mnist # 同时读入训练集和测试集 (x_train, t_train), (x_test, t_test) = load_mnist( flatten=True, # 把 28x28 的图片展平成 784 个像素的一维向量 normalize=False # 先不做归一化,保持 0-255 的原始亮度值 ) print(x_train.shape) # (60000, 784) 训练集有 6 万张图 print(t_train.shape) # (60000,) 每张图对应的数字标签load_mnist是鱼书代码自带的函数,负责下载或读取数据集。flatten=True表示把二维图片拉成一维向量,因为神经网络的输入通常是向量。normalize=False暂时不做归一化,等会儿推理的时候我们再打开。如果你运行这段代码时卡在下载,先别急,第四章会专门讲怎么处理。这一步的关键是让你理解:所谓“一张图片”,在程序里就是一个 784 维的数字数组;所谓“标签”,就是一个 0 到 9 的整数。深度学习要学的,就是从这 784 个数字到那个整数的映射关系。3.2 加载一张图片,把它显示出来光看 shape 没有实感,我们把第一张图真的显示出来,你会第一次亲眼看到“数据长什么样”。import numpy as np from PIL import Image # 取训练集的第一张图 img = x_train[0] # 把 784 个像素还原成 28 行 28 列 img = img.reshape(28, 28) # 转换成 PIL 图片对象并保存到当前目录 pil_img = Image.fromarray(np.uint8(img)) pil_img.save("first_number.png") print("这张图的标签是:", t_train[0])这段代码里,reshape(28, 28)是把一维数组恢复成二维形状,这样我们才能用图片的方式查看它。np.uint8是因为 PIL 库要求像素值是无符号 8 位整数,正好对应 0 到 255 的范围。运行之后,去当前目录找first_number.png这张图片,用看图软件打开它。我第一次看到的时候还挺惊讶的——原来所谓的手写数字数据集,就是这种不太工整、但人眼能认出来的数字图片。保存这张图还有一个好处,就是后面你换了其他图片输入时,能直观对比“程序眼中的输入”和“人眼看到的图”之间的差别。3.3 用预训练权重跑一次推理现在进入正题:用已经训练好的权重,对测试集做预测。这一步不涉及训练,只是把写好的参数代入神经网络,计算输出。import pickle import numpy as np from dataset.mnist import load_mnist def sigmoid(x): """把任意数值压缩到 0 到 1 之间""" return 1 / (1 + np.exp(-x)) def softmax(a): """把一组数值变成概率分布,所有值加起来等于 1""" c = np.max(a) # 减去最大值,防止 exp 指数溢出 exp_a = np.exp(a - c) return exp_a / np.sum(exp_a) def get_data(): """读取测试集,并做归一化:把 0-255 缩放到 0-1""" (x_train, t_train), (x_test, t_test) = load_mnist( normalize=True, flatten=True, one_hot_label=False ) return x_test, t_test def init_network(): """从 sample_weight.pkl 读取已训练好的权重和偏置""" with open("sample_weight.pkl", "rb") as f: network = pickle.load(f) return network def predict(network, x): """用神经网络做一次前向传播,返回 10 个类别的概率""" W1, b1 = network['W1'], network['b1'] W2, b2 = network['W2'], network['b2'] W3, b3 = network['W3'], network['b3'] # 第一层:加权求和 + 偏置 + sigmoid 激活 a1 = np.dot(x, W1) + b1 z1 = sigmoid(a1) # 第二层:同样操作 a2 = np.dot(z1, W2) + b2 z2 = sigmoid(a2) # 第三层:输出层,用 softmax 转成概率 a3 = np.dot(z2, W3) + b3 y = softmax(a3) return y这段代码看着长,其实每个函数只干一件事。get_data里把normalize设为True,是因为归一化能加快计算,让每个像素值都落在 0 到 1 之间。init_network用 pickle 读取权重文件,这个.pkl文件是别人训练好之后保存下来的,我们直接拿来做推理,省去了自己训练的时间。注意predict里的矩阵乘法:x是 784 维向量,W1是 784×50 的矩阵,np.dot(x, W1)算出来的结果是 50 维向量,再加上偏置b1。这就是第三章里反复强调的“层”的本质。3.4 跑一遍完整验证:预测一万张图,算准确率有了上面的函数,我们就可以对测试集做完整的准确率统计:x_test, t_test = get_data() network = init_network() accuracy_cnt = 0 for i in range(len(x_test)): y = predict(network, x_test[i]) # 对第 i 张图做预测 p = np.argmax(y) # 取概率最大的类别 if p == t_test[i]: accuracy_cnt += 1 print("准确率:", accuracy_cnt / len(x_test))np.argmax(y)返回概率最大那个位置的索引。如果最大概率在索引 7 处,就说明模型认为这张图是数字 7。把预测结果和真实标签t_test[i]比一比,相同就累加计数。最终用累计数除以测试集总量,得出准确率。我第一次跑出来准确率大约是 0.93,也就是 93% 左右。这个成绩并不惊艳,但要知道,这是用一个只有三层、总共才几万个参数的神经网络实现的,而且代码全部是 Python 写的。你亲手跑出了这个数字之后,再回头看理论,感受会完全不同。最后给你讲一个小优化:上面的代码是一张一张处理,比较慢。改成一次处理一批图片会快很多,这就是鱼书后面反复提到的 batch(批)概念:batch_size = 100 accuracy_cnt = 0 for i in range(0, len(x_test), batch_size): x_batch = x_test[i:i+batch_size] y_batch = predict(network, x_batch) p_batch = np.argmax(y_batch, axis=1) accuracy_cnt += np.sum(p_batch == t_test[i:i+batch_size]) print("准确率:", accuracy_cnt / len(x_test))区别在于,原来每次输入一张 784 维向量,现在每次输入 100 张组成的二维数组。NumPy 的矩阵运算在批量处理时效率更高,因为底层把循环交给了经过优化的 C 语言库。你先记住这个结论,第四章实现训练过程时会反复用到。4. 避坑指南:新手跑鱼书代码最容易翻车的 5 个地方4.1 现象:提示 No module named 'dataset'现象:你运行neuralnet_mnist.py,报错ModuleNotFoundError: No module named 'dataset'。原因:Python 导入包时,会按sys.path里的路径去查找dataset这个目录。如果你直接用编辑器打开了 ch03 目录下的文件,或者把文件单独拷到了别的地方,Python 就找不到上一级的dataset文件夹。解决:保持目录结构,从fishbook根目录运行。如果实在要在子目录里运行,也可以手动把父目录加入搜索路径:import sys, os sys.path.append(os.pardir) # 把当前目录的上一级加入搜索路径os.pardir就是..,也就是父目录。加了这一句之后,from dataset.mnist import load_mnist就能被找到。我自己的习惯是偷懒,永远在根目录下用命令行运行,这样任何代码都不会出现路径问题。4.2 现象:卡在下载 MNIST 数据集,进度条不动现象:第一次调用load_mnist时,终端一直显示下载状态,等了好几分钟没有反应,甚至直接报连接错误。原因:MNIST 原始数据集托管在国外的服务器上,国内访问时速度很不稳定。load_mnist第一次运行会先检查本地有没有缓存文件,没有就联网下载。解决:最省心的办法是手动下载数据集文件,放到dataset目录下,再重新运行。load_mnist发现本地存在对应文件后就不会再去联网了。如果你下载下来的文件是压缩包形式,保持原始文件名即可,代码会自动识别和缓存为.pkl格式。我第一次就是卡在这里,后来看到有个说法是“把下载这一步替换成国内镜像源”。具体做法是打开dataset/mnist.py,在里面找到原始下载地址那一行,替换成可用的镜像地址。改的时候注意只改 URL,别动其他逻辑。这一步做完,之后所有章节都能顺利读取数据,再也不用等待下载。4.3 现象:print 出来一长串数字,根本看不懂现象:你想看看图片数据是什么样的,于是print(x_train[0]),结果终端刷出一大串 0 和 255 的数字,完全看不出所以然。原因:x_train[0]本来就是 784 个像素值组成的数组,直接打印当然是一串数字,人眼很难从 784 个数字里看出图片内容。解决:数据分析时少看原始值,多看形状和可视化结果。先用print(x_train.shape)确认维度,再用上一章的reshape和Image.save把图片保存出来看。如果是在命令行里,也可以这样快速预览:print(x_train[0].reshape(28, 28))终端会把数字按 28 行 28 列排开,虽然很粗糙,但能隐约看出数字轮廓。这个习惯很重要:以后你处理任何新数据,第一件事永远是打印shape,而不是打印内容。4.4 现象:代码能跑但特别慢,等半天不出结果现象:第三章的推理还好,到了第四章训练时,跑了很久进度条还在原地,你不知道它是死机了还是正常工作中。原因:鱼书代码是纯 Python 实现,没有调用 GPU 加速。训练涉及几万次迭代,每轮迭代又要跑几百张图片的前向和反向计算,CPU 上确实会慢。但这不代表程序死了,而是本来就需要时间。解决:调试阶段先缩小数据量和训练轮数。第四章训练代码里有iters_num(迭代次数)这个变量,先把它从 10000 改成 1000,甚至 200,确认整个流程能跑通、loss 值在下降,再把轮数加回去。我的习惯是先让程序“能跑”,再让程序“跑得快”,别一上来就追求完整训练效果。4.5 现象:改了参数之后,准确率反而下降现象:你想试试调参,把learning_rate从 0.1 改成 0.5,结果损失值乱跳,准确率反而不如默认配置。原因:调参不是越大越好。学习率太大,参数会在最优值附近来回震荡,甚至发散;学习率太小,收敛又慢得让人绝望。隐藏层神经元数量改多了,在小训练集上也很容易过拟合。解决:调参要一次只动一个变量,并且按数量级来试。比如学习率先试 0.1、0.01、0.001 三档,哪个 loss 曲线最平滑就用哪个。建议你每次调参都记一个表:参数试过的值效果学习率0.1 / 0.01 / 0.0010.1 震荡,0.001 太慢,0.01 最优隐藏层大小50 / 100 / 200100 精度略高,但训练更慢调参在入门阶段看起来像玄学,其实就是控制变量加记录日志。你踩过一次坑,后面再调任何参数都会习惯性地先看 loss 曲线。5. 读懂核心代码:三层神经网络的前向传播是怎么算出来的5.1 先把神经网络想成一条流水线很多入门教程一上来就画复杂的神经网络结构图,新手看了头皮发麻。我建议你把三层网络想成一条流水线,原料就是一张 784 维像素向量,它要经过三道工序。第一道工序:把这 784 个像素和 50 组权重做加权求和,每组权重相当于一个“特征探测器”,有的负责探测横线,有的负责探测圆弧。第二道工序:把第一层的 50 个输出,再经过 100 组权重,组合成更抽象的特征。第三道工序:把上一层的 100 个结果映射成 10 个数字,每个数字代表“这是几”的概率。每一道工序做的事情都一样:加权求和、加偏置、过激活函数。差别只在于输入输出的维度不同。理解了这条流水线,再看代码就不会乱了。5.2 逐行拆解 predict 函数我们回到第三章的predict,把它当成解剖对象。假设输入x是一张 784 维的图片:def predict(network, x): # 先去取出每一层的权重 W 和偏置 b W1, b1 = network['W1'], network['b1'] W2, b2 = network['W2'], network['b2'] W3, b3 = network['W3'], network['b3'] # 第一层:矩阵乘法 + 偏置 a1 = np.dot(x, W1) + b1 z1 = sigmoid(a1) # 第二层:重复同样的操作 a2 = np.dot(z1, W2) + b2 z2 = sigmoid(a2) # 第三层:输出层,换成 softmax 得到概率 a3 = np.dot(z2, W3) + b3 y = softmax(a3) return ynp.dot(x, W1)就是矩阵乘法。x的形状是(784,),W1的形状是(784, 50),结果就是(50,)的一维数组,代表这一层 50 个节点的得分。加上b1这个 50 维的偏置向量后,每个节点的分数再传入sigmoid,压缩到 0 到 1 之间,作为这一层最终的输出。第二层同理,输入从 784 变成了 50,权重形状是(50, 100),输出是 100 维。第三层的权重形状是(100, 10),输出是 10 维。你可以这样记:每一层的权重形状,永远是“上一层的输出维度 × 这一层的节点数”。层权重形状偏置形状输出形状输入→第1层(784, 50)(50,)(50,)第1层→第2层(50, 100)(100,)(100,)第2层→输出层(100, 10)(10,)(10,)这个表你对照代码看一遍,整段predict就再也没有神秘感了。它本质上就是三次乘法和加法,中间穿插两个激活函数。5.3 sigmoid、ReLU、softmax:三个函数各管一件事鱼书第三章里出现频率最高的三个函数,新手容易搞混它们的使用场景。我把它们拆开讲清楚:sigmoid的公式是1 / (1 + exp(-x)),作用是压缩到 0 到 1 之间。它用于隐藏层,特点是非常平滑,但有个缺点:当输入绝对值很大时,输出几乎贴到 0 或 1,梯度会变得很小,这就是所谓的梯度消失问题。鱼书第三章用它,是因为它好理解;到第五章之后,你会看到更多代码改用 ReLU。ReLU的公式是max(0, x),简单粗暴:负数一律变成 0,正数保持不变。它训练速度快,因为没有 sigmoid 那种饱和区间,现在大多数网络都在隐藏层用它。鱼书第五章之后,sigmoid基本只在理论演示里出现,实际代码多用ReLU。softmax只用于最后一层,作用是把任意一组实数变成一组加起来等于 1 的概率。注意它有一个细节:计算前先减去这批数据里的最大值,防止exp指数溢出。这就是为什么softmax函数里第一行是c = np.max(a)。三个函数使用位置的区别,你现在不用背,但要知道一个原则:隐藏层用 ReLU 用的多,输出层做分类任务用 softmax。这个记忆点足够支撑你读完前六章。5.4 为什么“推理”和“训练”必须分开看学到这里,你可能会有一个疑问:predict 函数里只是算了一遍加权求和,那网络的权重 W 是从哪来的?答案是:别人用训练过程算出来的。推理前向传播,指的是输入数据顺着流水线往前流,最终得到预测概率。这一过程不需要标签,不需要算损失,只需要有权重就能算。程序做的事就是上文的矩阵乘法,没什么好神秘的。训练反向传播,则是拿着有标签的数据,先做一次前向传播得到预测结果,然后和真实标签比较算出误差,再从输出层往回一层一层地修改权重,让下次预测更准。鱼书第四章会手写实现这个训练过程,包括梯度下降更新参数。新手最容易犯的错,是还没跑通推理,就急着看训练代码,结果被反向传播里的一堆公式劝退。我建议你严格按这个顺序来:先把推理代码反复读到能默写,再进入训练部分。前向传播搞明白了,反向传播不过就是它的“逆过程”,顺带还涉及微积分里的链式法则,但鱼书会用手写代码帮你绕开数学推导。6. 下一步学什么:从“跑通代码”到“改得动代码”6.1 三个递进练法,把代码变成自己的跑通第一个例子只算入门,真正学进大脑靠的是动手改。我推荐三个递进的练法,难度逐级上升。练法一:改超参数。把第四章two_layer_net.py里的学习率改成0.01、0.001,隐藏层大小改成100,然后重新训练,看 loss 曲线的变化。这一步让你亲身体会参数如何影响训练结果。练法二:换激活函数。把隐藏层的sigmoid全部替换成ReLU,再次训练。你会发现代码只要改一行,训练中的 loss 下降趋势就不一样了。这就是为什么后面章节要专门讲激活函数的选择。练法三:改网络结构。把两层网络改成三层,或者把隐藏层节点从 50 改成 200,再对比训练时间和准确率。这个练习能让你理解“深度”到底带来了什么。每改一次,都建议把结果记成一张表:练习内容改哪里观察什么学习率训练代码里的 learning_rate收敛速度、loss 是否震荡隐藏层大小two_layer_net 里的 hidden_size训练耗时和最终准确率激活函数两层之间的 sigmoid 换成 ReLU训练曲线的平滑程度网络层数在 predict 中多加一层表达能力是否变强6.2 之后的学习路径建议如果你把第三章和第四章都跑通了,下一站是第五章“误差反向传播”。这一章是全书最硬核的部分,但你不需要背公式,只需要跟着代码里的layers.py走一遍正向和反向的计算流程,理解每一层保存了什么、更新了什么。之后再进入第七章 CNN,你会发现卷积神经网络的整体结构你早就见过了,无非是多了一些卷积层和池化层。至于要不要尽快去学 PyTorch、TensorFlow,我的建议是暂缓。鱼书这份代码的价值在于脱离框架、看见本质。等你能不看答案就自己写出一层网络的前向传播时,再转框架会顺畅得多。那时候你会觉得框架里nn.Linear只是一个封装好的工具,而你知道盒子里的齿轮是怎么转的。说起来,我从那以后每次拿到一份新代码,都强制自己先跑通、再改参数、再读源码,这个顺序帮我少踩了一半的坑。希望你也能从这个最小例子开始,一步步把鱼书从第三章走到第八章,体会到那种“原来我也能让电脑认数字”的掌控感。希望帮到你。本文还有配套的精品资源,点击获取