资讯动态

深度学习入门实践:从零实现神经网络与误差反向传播

发布时间:2026/8/12 9:43:12 来源:尧图企业网站定制
1. 项目概述从零开始的深度学习实践笔记最近几年深度学习的热度一直居高不下从图像识别到自然语言处理再到各种智能应用它几乎无处不在。但对于很多想入门的朋友来说那些充斥着复杂公式和抽象概念的教科书常常让人望而却步。我自己在刚开始学习时也深有体会直到遇到了斋藤康毅的《深度学习入门基于Python的理论与实现》也就是大家常说的“鱼书”。这本书最大的魅力在于它不满足于仅仅告诉你“是什么”而是手把手带你用代码实现“为什么”和“怎么做”。这份学习笔记就是我啃完这本经典教材后的实践总结和心得沉淀。它不是一个简单的读书摘要而是一个以Python、Numpy、Matplotlib为核心工具从感知机到误差反向传播法一步步搭建并理解神经网络的全过程记录。我的目标很明确把书中的理论变成可运行、可调试、可修改的代码并在过程中把那些容易卡壳的“黑箱”操作彻底搞明白。无论你是刚刚接触Python编程的新手还是对神经网络原理感到困惑的初学者这份笔记都希望能为你提供一条清晰、可复现的实践路径让你真正理解深度学习是如何从一行行简单的代码中构建起来的。2. 核心学习路径与工具栈解析2.1 为什么选择“鱼书”作为起点市面上深度学习入门书籍很多但“鱼书”的定位非常独特。它摒弃了直接调用TensorFlow或PyTorch等高级框架的做法选择从最底层的数学原理和Numpy数组操作开始。这种“自底向上”的学习方式初期看似缓慢但根基打得极为扎实。当你亲手用矩阵乘法实现全连接层用循环实现误差反向传播时你对神经网络中数据流动、梯度计算的理解是直接调用model.fit()所无法比拟的。这就像学开车一开始就用自动挡固然轻松但理解了手动挡的离合、换挡机制后你对“驾驶”这件事的掌控感会完全不同。这本书正是那个带你理解“手动挡”原理的绝佳教练。2.2 工具链搭建Python环境与核心库工欲善其事必先利其器。一个稳定、隔离的Python环境是后续所有实验的基础。我强烈建议使用Anaconda来管理环境它能很好地解决包依赖冲突这个世界性难题。1. 创建专属虚拟环境打开Anaconda PromptWindows或终端Mac/Linux执行以下命令创建一个名为dl_study的虚拟环境并指定Python版本为3.8这是一个在兼容性上比较稳妥的版本conda create -n dl_study python3.8 conda activate dl_study2. 安装核心三件套在激活的dl_study环境中安装我们最核心的三个库pip install numpy matplotlib这里有一个非常重要的细节库版本的兼容性。根据网络上的常见反馈特别是某些特定环境如32位系统或与其他库如Gradio搭配时Numpy 2.x版本可能会带来一些意想不到的兼容性问题例如报错AttributeError: module numpy has no attribute arange或requires numpy~1.0。为了最大限度地保证与“鱼书”代码以及绝大多数现有教程的兼容性我建议锁定安装Numpy 1.x的稳定版本pip install numpy1.24.3 matplotlibMatplotlib的安装通常比较顺利但如果遇到诸如Process finished with exit code -1066598273 (0xc06d007f)这类诡异错误多半是底层图形后端或依赖库的问题。可以尝试安装更完整的版本或指定后端pip install matplotlib --upgrade # 或者在代码中尝试使用非交互式后端 import matplotlib matplotlib.use(Agg) # 在导入pyplot之前设置3. 验证与编辑器选择安装完成后可以写一个简单的脚本来验证import numpy as np import matplotlib.pyplot as plt print(fNumPy version: {np.__version__}) print(fMatplotlib version: {matplotlib.__version__}) # 测试基本功能 arr np.arange(10) print(arr) plt.plot(arr, arr**2) plt.title(Test Plot) plt.savefig(test.png) # 使用savefig避免可能出现的GUI窗口问题 print(Basic test passed!)关于代码编辑器VSCode是当前非常流行的选择配合Python插件和Jupyter扩展可以实现代码编写、调试和笔记记录的无缝衔接。在VSCode中记得将左下角的Python解释器选择为我们刚创建的dl_study环境这样就能确保运行环境正确。3. 基础概念代码化从感知机到神经网络3.1 感知机神经网络的起源砖块感知机是理解一切复杂模型的起点。它接收多个输入产生一个输出0或1本质是一个二分类器。书中通过AND、OR、NAND门电路的例子生动展示了如何通过手动设置权重w和偏置b来实现逻辑运算。关键实现与理解import numpy as np def AND(x1, x2): x np.array([x1, x2]) w np.array([0.5, 0.5]) b -0.7 tmp np.sum(w*x) b if tmp 0: return 0 else: return 1 print(AND(0,0), AND(1,0), AND(0,1), AND(1,1)) # 输出: 0 0 0 1注意这里的核心是np.sum(w*x)它实现了加权求和。偏置b的作用可以理解为“激活门槛”调整它就能改变神经元被激活的难易程度。手动设置参数虽然直观但也引出了核心问题对于复杂的、无法直观理解的模式比如识别猫狗图片我们如何自动找到合适的w和b这就是神经网络和深度学习要解决的核心——自动学习参数。3.2 引入激活函数从阶跃到平滑感知机使用的阶跃函数输出非0即1不连续且不可导这导致它无法使用强大的梯度下降法来学习。神经网络的重大改进之一就是引入了平滑的激活函数如sigmoid和ReLU。Sigmoid函数实现与理解def sigmoid(x): return 1 / (1 np.exp(-x)) # 测试 x np.array([-1.0, 0.0, 1.0, 2.0]) print(sigmoid(x)) # 输出大约为[0.26894142, 0.5, 0.73105858, 0.88079708]sigmoid函数将任意实数映射到(0,1)区间输出平滑可导。但这里有一个实操中极易忽略的数值稳定性问题当输入x是一个很大的负数时np.exp(-x)可能会溢出变成无穷大虽然Python/NumPy能处理但可能导致计算警告或精度损失。一个更稳健的实现是def sigmoid_stable(x): # 对正负输入分别处理避免溢出 return np.where(x 0, 1 / (1 np.exp(-x)), np.exp(x) / (1 np.exp(x)))这个细节在后续构建多层网络、处理真实数据时非常重要它能保证计算的稳定性。3.3 搭建三层神经网络前向传播结合加权和、偏置与激活函数我们就可以搭建一个简单的三层输入层、隐藏层、输出层神经网络进行前向传播。def init_network(): 初始化一个简单的3层网络权重参数 network {} network[W1] np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]]) # 2x3 network[b1] np.array([0.1, 0.2, 0.3]) network[W2] np.array([[0.1, 0.4], [0.2, 0.5], [0.3, 0.6]]) # 3x2 network[b2] np.array([0.1, 0.2]) network[W3] np.array([[0.1, 0.3], [0.2, 0.4]]) # 2x2 network[b3] np.array([0.1, 0.2]) return network def forward(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) a3 np.dot(z2, W3) b3 y a3 # 输出层通常不使用激活函数对于回归任务 return y # 使用 network init_network() x np.array([1.0, 0.5]) y forward(network, x) print(y) # 输出两个数值这段代码清晰地展示了数据流动输入x - 加权和a - 激活z - 下一层。这里全部使用np.dot进行矩阵乘法这是神经网络计算的核心。务必理解每一层权重矩阵W的形状W1的形状是(2,3)因为它连接了2个输入节点和3个隐藏层节点。掌握这种形状的对应关系是调试神经网络代码的关键。4. 神经网络的学习机制损失函数与梯度下降4.1 量化“不好”损失函数的作用网络有了参数是随机初始化的输出结果自然没有意义。如何衡量网络输出的“好坏”这就需要损失函数。对于回归问题预测连续值常用均方误差对于分类问题如图像识别常用交叉熵误差。均方误差实现def mean_squared_error(y, t): y: 预测值 t: 真实标签one-hot形式 return 0.5 * np.sum((y-t)**2) # 示例假设图像分类数字“2”是正确答案 t np.array([0, 0, 1, 0, 0, 0, 0, 0, 0, 0]) # one-hot编码索引2为1 y np.array([0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]) # 网络预测概率 print(mean_squared_error(y, t)) # 损失值损失函数的值越大说明预测越不准。我们的目标就是通过调整网络参数让这个损失值尽可能小。4.2 寻找下山的路梯度下降法原理梯度下降法是神经网络学习的引擎。梯度是一个向量指向函数值增加最快的方向。那么梯度的反方向就是函数值下降最快的方向。通过不断将参数向梯度反方向移动一小步步长由学习率控制我们就能逐渐找到损失函数的极小值点。数值微分与梯度计算 “鱼书”为了清晰理解概念首先介绍了通过极限定义计算导数数值微分的方法def numerical_diff(f, x): 中心差分法比前向差分精度更高 h 1e-4 # 不宜过小避免浮点数精度问题 return (f(xh) - f(x-h)) / (2*h)然后利用数值微分可以计算损失函数关于某个参数的梯度偏导数。例如对于一个简单的函数f(x0, x1) x0**2 x1**2在点(3.0, 4.0)处的梯度计算如下def function_2(x): return x[0]**2 x[1]**2 def numerical_gradient(f, x): 计算函数f在点x处的梯度 h 1e-4 grad np.zeros_like(x) # 生成和x形状相同的全零数组 for idx in range(x.size): tmp_val x[idx] # 计算f(xh) x[idx] tmp_val h fxh1 f(x) # 计算f(x-h) x[idx] tmp_val - h fxh2 f(x) # 计算偏导数 grad[idx] (fxh1 - fxh2) / (2*h) # 恢复x的原始值 x[idx] tmp_val return grad print(numerical_gradient(function_2, np.array([3.0, 4.0]))) # 输出[6. 8.] 这与解析解 2*x 的结果一致。这个numerical_gradient函数是理解后续误差反向传播法的基石。它告诉我们对于多变量函数梯度是由每个变量方向上的偏导数组成的向量。4.3 实现梯度下降更新有了梯度我们就可以实现最简单的梯度下降更新def gradient_descent(f, init_x, lr0.01, step_num100): 梯度下降法 f: 要求梯度的函数 init_x: 初始参数 lr: 学习率 step_num: 迭代次数 x init_x.copy() # 避免修改原数组 for i in range(step_num): grad numerical_gradient(f, x) x - lr * grad # 向梯度反方向更新 # 可以打印每次迭代的x和损失观察下降过程 # print(fstep {i}: x{x}, f(x){f(x)}) return x # 测试寻找f(x)x0^2x1^2的最小值点显然是(0,0) init_x np.array([-3.0, 4.0]) result gradient_descent(function_2, init_x, lr0.1, step_num20) print(result) # 输出应非常接近 [0., 0.]关键心得学习率lr的选择至关重要。太大可能导致在最小值点附近震荡甚至发散太小则学习速度过慢。通常需要根据具体问题通过实验调整。上述例子中lr0.1是合适的如果设为lr1.0更新步长过大参数可能会在(0,0)两边来回跳跃。5. 神经网络的灵魂误差反向传播法数值微分虽然直观但计算效率极低尤其是对于百万、千万参数的现代网络。误差反向传播法利用链式法则通过一次前向传播和一次反向传播就能高效地计算出损失函数关于所有参数的梯度。5.1 计算图与链式法则“鱼书”用计算图的方式将反向传播讲解得非常透彻。以最简单的z x * y为例前向传播给定x,y计算z。反向传播求梯度已知上游传来的梯度∂L/∂zL是最终损失根据链式法则∂L/∂x ∂L/∂z * ∂z/∂x ∂L/∂z * y。同理∂L/∂y ∂L/∂z * x。在反向传播中每个节点运算只需要实现两个方法forward()和backward()。backward()接收上游梯度计算并传递本地梯度。5.2 实现基础层ReLU和Affine我们来实现两个最基础的层ReLU激活层和Affine全连接层。1. ReLU层实现class Relu: def __init__(self): self.mask None def forward(self, x): self.mask (x 0) # 记录输入中不大于0的位置 out x.copy() out[self.mask] 0 # 不大于0的输出置0 return out def backward(self, dout): dout: 上游传来的梯度 ReLU的导数为输入0时是1输入0时是0。 因此反向传播时只需将上游梯度dout在原输入0的位置置零。 dout[self.mask] 0 dx dout return dxmask的作用是记录在前向传播中哪些输入是小于等于0的在反向传播时直接将对应位置的梯度归零。这是实现函数导数逻辑的关键。2. Affine层全连接层实现 这是核心中的核心实现了Y X·W B。class Affine: def __init__(self, W, b): self.W W self.b b self.x None self.dW None # 权重W的梯度 self.db None # 偏置b的梯度 def forward(self, x): self.x x # 保存输入反向传播时要用 out np.dot(x, self.W) self.b return out def backward(self, dout): dout: 上游传来的梯度形状通常为 (batch_size, out_size) 根据矩阵求导法则 dX dout · W^T dW X^T · dout dB 对dout在batch维度求和因为偏置B是广播加上的 dx np.dot(dout, self.W.T) self.dW np.dot(self.x.T, dout) self.db np.sum(dout, axis0) # 沿batch轴求和 return dx必须理解的计算self.dW np.dot(self.x.T, dout)。假设输入x形状为(N, D)权重W形状为(D, H)输出out形状为(N, H)。那么上游梯度dout形状也是(N, H)。根据多元微积分损失L对W的梯度∂L/∂W等于x^T · (∂L/∂out)这正是代码中的计算。axis0在np.sum(dout, axis0)中表示沿着第0维batch维求和因为偏置b是在每个特征维度上被加到所有样本上的。5.3 组合成网络TwoLayerNet将实现好的层组合起来并实现Softmax-with-Loss层结合Softmax和交叉熵损失便于梯度计算我们就得到了一个完整的可训练的两层神经网络。from collections import OrderedDict 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) # 构建层 self.layers OrderedDict() # 有序字典保证层的前后顺序 self.layers[Affine1] Affine(self.params[W1], self.params[b1]) self.layers[Relu1] Relu() self.layers[Affine2] Affine(self.params[W2], self.params[b2]) self.lastLayer SoftmaxWithLoss() # 最后一层是损失层 def predict(self, x): 前向传播推理 for layer in self.layers.values(): x layer.forward(x) return x def loss(self, x, t): 计算损失前向传播至损失层 y self.predict(x) return self.lastLayer.forward(y, t) def accuracy(self, x, t): 计算精度 y self.predict(x) y np.argmax(y, axis1) # 取概率最大的索引作为预测类别 if t.ndim ! 1 : t np.argmax(t, axis1) # 如果t是one-hot转为类别索引 accuracy np.sum(y t) / float(x.shape[0]) return accuracy def gradient(self, x, t): 计算梯度核心 # 前向传播 self.loss(x, t) # 反向传播 dout 1 # 初始上游梯度损失对自身的导数为1 dout self.lastLayer.backward(dout) layers list(self.layers.values()) layers.reverse() # 将层顺序反转用于反向传播 for layer in layers: dout layer.backward(dout) # 从各层收集梯度 grads {} grads[W1] self.layers[Affine1].dW grads[b1] self.layers[Affine1].db grads[W2] self.layers[Affine2].dW grads[b2] self.layers[Affine2].db return grads这个TwoLayerNet类封装了网络的全部功能初始化、预测、计算损失和精度以及最关键的计算梯度gradient方法。gradient方法内部依次调用了损失层和各层的backward函数将梯度从最终损失一路传播回最初的输入并收集了每一层参数的梯度。6. 实战训练在MNIST数据集上验证理论最终需要实践检验。我们使用深度学习界的“Hello World”数据集——MNIST手写数字数据集来训练我们刚刚实现的两层网络。6.1 数据加载与预处理“鱼书”提供了便利的数据加载函数。数据预处理包括归一化将像素值从0-255缩放到0.0-1.0和标签的one-hot编码。# 假设已从本书官网下载mnist.pkl数据集并存在当前目录 import pickle import gzip def load_mnist(): 加载MNIST数据集 with gzip.open(mnist.pkl.gz, rb) as f: dataset pickle.load(f, encodinglatin1) return (dataset[0][0], dataset[0][1]), (dataset[1][0], dataset[1][1]), (dataset[2][0], dataset[2][1]) (x_train, t_train), (x_test, t_test) load_mnist()[0], load_mnist()[1] # 只取训练和测试集 # 预处理 x_train, x_test x_train / 255.0, x_test / 255.0 # 归一化 # 标签one-hot编码简易版 def to_one_hot(y, num_classes10): return np.eye(num_classes)[y.astype(int)] t_train_one_hot to_one_hot(t_train) t_test_one_hot to_one_hot(t_test)6.2 训练循环与超参数设置现在我们实例化网络并设置训练循环。network TwoLayerNet(input_size784, hidden_size50, output_size10) # 超参数 iters_num 10000 # 迭代次数 train_size x_train.shape[0] batch_size 100 # 小批量大小 learning_rate 0.1 train_loss_list [] train_acc_list [] test_acc_list [] # 平均每个epoch的迭代次数 iter_per_epoch max(train_size / batch_size, 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_one_hot[batch_mask] # 计算梯度 grad network.gradient(x_batch, t_batch) # 更新参数最基础的SGD for key in (W1, b1, W2, b2): network.params[key] - learning_rate * grad[key] # 记录学习过程 if i % 100 0: loss network.loss(x_batch, t_batch) train_loss_list.append(loss) # 计算并记录精度每过一个epoch计算一次 if i % iter_per_epoch 0: train_acc network.accuracy(x_train, t_train) test_acc network.accuracy(x_test, t_test) train_acc_list.append(train_acc) test_acc_list.append(test_acc) print(fepoch {i/iter_per_epoch:.0f} | train acc: {train_acc:.4f}, test acc: {test_acc:.4f})6.3 可视化与结果分析训练完成后使用Matplotlib绘制损失和精度曲线直观观察学习过程。import matplotlib.pyplot as plt # 绘制损失变化 plt.figure(figsize(12, 4)) plt.subplot(1, 2, 1) plt.plot(range(len(train_loss_list)), train_loss_list) plt.xlabel(Iteration (x100)) plt.ylabel(Loss) plt.title(Training Loss) # 绘制精度变化 plt.subplot(1, 2, 2) epochs range(len(train_acc_list)) plt.plot(epochs, train_acc_list, labeltrain acc) plt.plot(epochs, test_acc_list, labeltest acc, linestyle--) plt.xlabel(Epoch) plt.ylabel(Accuracy) plt.ylim(0, 1.0) plt.legend() plt.title(Training Test Accuracy) plt.tight_layout() plt.savefig(mnist_training_result.png) # 保存图片避免GUI问题 plt.show()一个成功的训练过程会显示训练损失持续下降并逐渐平缓训练精度和测试精度同步上升最终测试精度能达到**97%**左右。如果测试精度远低于训练精度可能是过拟合如果两者都很低可能是模型能力不足或学习率设置不当。7. 常见问题、调试技巧与扩展思考在实际动手实现上述代码的过程中你几乎一定会遇到各种错误和疑惑。下面是我在学习和实践中总结的一些典型问题及解决方法。7.1 维度错误与形状调试这是新手最常遇到的问题几乎占所有错误的80%。ValueError: shapes (a,b) and (c,d) not aligned这类错误让人头疼。黄金法则时刻关注并打印每个变量的shape。在前向传播的每一步都清楚数据形状的变化。调试技巧在关键函数如Affine.forward的开头添加print(x.shape, self.W.shape)。确保矩阵乘法的维度匹配(N, D) dot (D, H) - (N, H)。常见坑点偏置b的加法利用了广播机制。b的形状应为(H,)与(N, H)的输出相加时会自动广播到每个样本。7.2 梯度验证确保反向传播正确自己实现的梯度计算反向传播是否正确一个极其重要的验证方法是梯度检查用数值微分计算出的梯度与反向传播计算出的梯度进行对比。def gradient_check(network, x_batch, t_batch): grad_backprop network.gradient(x_batch, t_batch) # 反向传播梯度 # 对每个参数进行数值微分 for key in network.params: param network.params[key] grad_numerical numerical_gradient(lambda w: network.loss(x_batch, t_batch), param) # 计算相对误差 diff np.abs(grad_backprop[key] - grad_numerical).sum() relative_error diff / (np.abs(grad_backprop[key]).sum() np.abs(grad_numerical).sum()) print(f{key}: relative error {relative_error}) if relative_error 1e-4: print(f OK) else: print(f Error too large!)如果相对误差在1e-4以内通常认为反向传播实现是正确的。务必在正式训练前进行梯度检查它能帮你排除掉反向传播实现中的逻辑错误。7.3 训练不收敛或效果差如果训练后精度远低于预期比如低于90%可以从以下几个方面排查学习率这是首要怀疑对象。尝试将其调小如0.01, 0.001或使用学习率衰减策略。权重初始化代码中使用的是高斯随机初始化weight_init_std0.01。如果这个值设置得过大或过小可能导致梯度消失或爆炸。对于使用ReLU的网络He初始化std sqrt(2.0 / n_in)通常是更好的选择。数据预处理确认是否进行了归一化/255.0。未归一化的输入0-255会导致梯度巨大训练不稳定。批量大小batch_size过小如1会导致更新噪声太大过大则可能内存不足且收敛慢。100是一个常用的起始值。网络容量隐藏层神经元数量hidden_size过少如10模型可能欠拟合无法捕捉复杂模式。可以尝试增加到100或200。7.4 从“鱼书”走向更广阔的世界完成“鱼书”的实践意味着你已经掌握了深度学习的“内功”。接下来可以转向现代框架用PyTorch或TensorFlow重写一遍这个网络你会发现框架帮你自动完成了梯度计算让你能更专注于模型架构和实验。探索更优优化器将代码中的随机梯度下降SGD替换为Adam、RMSprop等自适应优化器通常能获得更快的收敛速度和更好的效果。增加网络深度尝试实现三层、四层甚至更深的网络并观察性能变化。同时需要关注梯度消失问题考虑使用Batch Normalization层。尝试卷积神经网络CNN对于图像数据CNN是更强大的工具。可以基于本书理解的基础去学习并实现简单的CNN如LeNet-5来处理MNIST精度可以轻松提升到99%以上。整个学习过程最宝贵的不是记住了多少公式而是建立了“前向传播计算输出反向传播计算梯度梯度下降更新参数”这一核心闭环的直观感受。当你下次看到复杂的ResNet、Transformer架构时你会明白它们无非是这个基本框架下通过更精巧的模块如残差连接、自注意力搭建起来的。这份从零构建的理解将是你在深度学习领域持续探索最坚实的底气。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价