反向传播从公式到代码:吴恩达ex4作业Python实现与梯度检验

发布时间:2026/10/20 7:55:41
反向传播从公式到代码:吴恩达ex4作业Python实现与梯度检验 吴恩达机器学习这门课的编程练习做到 ex4 的时候一半人会被 Backpropagation 反向传播劝退——不是公式看不懂而是看懂了公式却不知道代码从哪一行开始写。这篇文章就是我用 Python/NumPy 完整实现 ex4 的记录覆盖数据加载、前向传播、代价函数、反向传播、数值梯度检验、训练与隐藏层可视化并给出文末可直接运行的完整代码。适合正在刷这门课、想把 Octave/MATLAB 思路迁移到 Python 的读者也适合想搞清楚反向传播到底在算什么的人。1. 这一题的难点在哪公式、维度、还有环境的切换1.1 题面要求被很多人低估了ex4 表面上是实现反向传播实际要做的事情比名字多得多你要自己完成正则化代价函数、前向传播、输出层误差、隐藏层误差、梯度累积、参数正则化梯度最后还要写一个数值梯度检验来证明你的梯度没算错。课程作业说明文档里给的参考输出是两组数字用课程提供的权重不带正则化的代价约 0.287629带 lambda1 的正则化后约 0.383770。我当年看到这两个数字的时候毫无概念直到自己实现完才发现这就是一个非常有效的全网验证点——你的代价函数如果返回这两个数说明前半段写对了返回不对后面反向传播再漂亮也没有意义。很多人习惯先看别人博客再动手结果发现大家贴的代码维度五花八门有人把 theta 写成行向量有人把 y 直接当数字往公式里带。这门课原始作业是面向 Octave/MATLAB 的矩阵下标从 1 开始而 NumPy 从 0 开始光这个转换就能埋下十几个 bug。1.2 核心矩阵维度一张表事先对齐我强烈建议动手前先把所有变量和维度列出来你的代码里每一行 reshape、每一次矩阵乘法都要能对应回这张表。这是整个 ex4 我最想强调的一步。变量维度说明X5000 x 4005000 个 20x20 像素手写数字按行展开y5000 x 1标签取值 1 到 10其中 10 表示数字 0Theta125 x 401输入层 400 偏置单元映射到隐藏层 25 个单元Theta210 x 26隐藏层 25 偏置单元映射到输出层 10 个单元a1m x 401给 X 左边拼接一列 1 之后的激活值z2m x 25a1 与 Theta1 转置相乘隐藏层加权输入a2m x 26sigmoid(z2) 之后再拼接一列 1z3m x 10a2 与 Theta2 转置相乘输出层加权输入h / a3m x 10sigmoid(z3)即预测输出注意偏置单元的处理方式前向传播时a1 和 a2 都要手动拼接一列 1反向传播时Theta2 的第一列会被跳过。这张表里最容易出错的就是 a2 的维度它比 z2 多一列偏置。1.3 我的整体策略公式先翻译成矩阵运算再谈优化从 Octave 迁到 Python我的做法是先把课程里的向量化公式逐行翻译成 NumPy 写法先不求优化只求每一步中间变量的 shape 都能对上。等前向传播和代价函数验证通过再写反向传播反向传播写完先做小网络梯度检验最后才接 scipy 优化器跑全量数据。这个过程虽然慢但每一步都有验证点不会出现代码跑通了但结果全错的绝望局面。2. 数据加载与预处理先用 .mat 文件和 one-hot 标签热身2.1 用 scipy.io.loadmat 读取课程原始数据课程原始数据是 ex4data1.mat 和 ex4weights.mat前者是 5000 个样本后者是课程训练好的权重。Python 里没有 MATLAB 的 load 命令需要用 scipy 读import numpy as np from scipy.io import loadmat data loadmat(ex4data1.mat) X data[X] # shape: (5000, 400) y data[y] # shape: (5000, 1) weights loadmat(ex4weights.mat) Theta1_ref weights[Theta1] # shape: (25, 401) Theta2_ref weights[Theta2] # shape: (10, 26)这里有两个细节一是确保 ex4data1.mat 和代码在同一目录否则路径错位直接报错二是 loadmat 读出来的 y 是列向量而不是一维数组后面做 one-hot 之前最好先 ravel 一下。我建议先把 X.shape、y.shape、X 的数值范围打印出来X 是 0 到 255 之间的灰度值y 里面只有 1 到 10 这十个整数。2.2 y 标签的坑10 代表数字 0这是所有从 Octave 迁移到 Python 的人都会撞上的第一个坑。课程数据里数字 0 的标签是 10不是 0。one-hot 编码的时候不能直接 np.eye(10)[y]因为 y 里最大是 10而矩阵索引最大是 9。正确做法是先减 1Y np.eye(10)[y.ravel().astype(int) - 1]这样 y10 会落到第 9 列代表数字 0y1 落到第 0 列代表数字 1。我把 y 的取值、减 1 后的索引、对应的真实数字这三者列在一起之后每写一次 one-hot 就对照一遍y 原始值减 1 后索引对应数字101212.........9891090如果你用 loss 一直不降、准确率卡在 90% 左右回来检查这一行八成是索引错位。2.3 先画几个样本确认数据没读歪在写任何网络逻辑之前我建议先做一次可视化确认读进来的矩阵确实是一张张手写数字图。写一个通用的 display_data 函数把矩阵每行 reshape 成 20x20 再拼成网格import matplotlib.pyplot as plt def display_data(X, rowsNone, colsNone): m, n X.shape side int(np.sqrt(n)) if rows is None: rows int(np.floor(np.sqrt(m))) if cols is None: cols int(np.ceil(m / rows)) pad 1 canvas -np.ones((pad rows * (side pad), pad cols * (side pad))) idx 0 for i in range(rows): for j in range(cols): if idx m: break max_val np.max(np.abs(X[idx])) canvas[pad i * (side pad): pad (i 1) * (side pad), pad j * (side pad): pad (j 1) * (side pad)] \ X[idx].reshape(side, side) / max_val idx 1 plt.imshow(canvas, cmapgray)调用 display_data(X[:100, :]) 如果能看到 10x10 的手写数字网格说明数据加载和 reshape 都对。这个函数后面还有用——隐藏层可视化也靠它。3. 前向传播与代价函数先让网络会算数3.1 为什么必须把权重压平成一维向量scipy.optimize.minimize 要求初始参数 x0 是一个一维数组而 Theta1 是 25x401、Theta2 是 10x26不能直接传两个矩阵进去。所以整个 ex4 里最反复出现的操作就是打包和解包def flatten_params(Theta1, Theta2): return np.concatenate([Theta1.ravel(), Theta2.ravel()]) def unflatten_params(nn_params): Theta1 nn_params[:25 * 401].reshape(25, 401) Theta2 nn_params[25 * 401:].reshape(10, 26) return Theta1, Theta2这里 25*401 是硬编码一般做题没问题但更稳妥的方式是从 input_layer_size、hidden_layer_size、num_labels 这三个变量现场算出切分点。我自己的习惯是在代价函数开头先算好theta1_shape (hidden_layer_size, input_layer_size 1) theta2_shape (num_labels, hidden_layer_size 1) theta1_end theta1_shape[0] * theta1_shape[1] Theta1 nn_params[:theta1_end].reshape(theta1_shape) Theta2 nn_params[theta1_end:].reshape(theta2_shape)这样以后改网络结构不用回头改魔数。3.2 随机初始化全零权重会毁掉整个训练另一种常见的错误是拿全零初始化。全零矩阵看起来无害但神经网络的隐藏层会因此对称——所有隐藏单元接收相同的梯度更新后依然相同网络退化成一个线性模型。ex4 要求用随机初始化课程里给了一个经验公式def rand_init_weights(l_in, l_out): eps_init np.sqrt(6) / np.sqrt(l_in l_out) return np.random.uniform(-eps_init, eps_init, size(l_out, l_in 1))注意这里函数签名里的 l_out 是输出层单元数返回的矩阵是 l_out 行、l_in1 列多的那一列是给偏置的。初始权重范围大约在 -0.12 到 0.12 之间。为什么值不能太大因为 sigmoid 在很大或很小的输入下会饱和梯度趋近于零网络几乎学不动。3.3 前向传播的矩阵写法前向传播的本质是一层层做加权求和 激活。用矩阵运算一次算完全部样本不需要写 for 循环def sigmoid(z): return 1 / (1 np.exp(-z)) m X.shape[0] a1 np.hstack([np.ones((m, 1)), X]) z2 a1 Theta1.T a2 np.hstack([np.ones((m, 1)), sigmoid(z2)]) z3 a2 Theta2.T h sigmoid(z3)这里 a1 Theta1.T 的写法对应课程里的 Theta1 * a1Octave 中乘法的方向注意不要写成 Theta1 a1.T那样得到的是 25x401维度完全乱掉。a2 拼接偏置列之后才能和 Theta2 相乘得到 m x 10 的输出。3.4 代价函数交叉熵 正则化输出层有 10 个单元所以每个样本的代价是这 10 个单元上的交叉熵之和再对所有样本取平均J_base np.sum(-Y * np.log(h) - (1 - Y) * np.log(1 - h)) / m reg (lamb / (2 * m)) * (np.sum(Theta1[:, 1:] ** 2) np.sum(Theta2[:, 1:] ** 2)) J J_base reg两处细节一是 np.sum 直接把矩阵所有元素求和对应课程公式里的双重求和二是正则项从第 1 列开始取 Theta[:, 1:]把偏置列排除在外因为偏置不参与正则化惩罚。验证这一部分是否正确有一个非常省事的方法用课程提供的 ex4weights 权重加载数据后算一下代价。如果我算出的无正则代价接近 0.287629、lambda1 时接近 0.383770说明前向传播、代价函数、one-hot 编码全部正确。这个验证点我每次写 ex4 都会做比自己对着公式干瞪眼高效得多。4. 反向传播的向量化实现从 delta_3 一路倒推到 Theta_14.1 输出层误差delta_3 h - Y反向传播的第一步是计算输出层误差。很多刚接触的人会把这一步写成 delta3 (h - Y) * sigmoid_gradient(z3)这其实是错的。输出层的误差定义为代价函数对 z3 的偏导而交叉熵损失配上 sigmoid 输出之后导数恰好化简成 h - Y不需要再乘一次 sigmoid 梯度。这里的数学推导可以自己花两分钟算一遍对单个输出单元sigmoid 的导数 h(1-h) 会把交叉熵求导后的 1/(h(1-h)) 抵消掉剩下的正好是 h-Y。所以反向传播的第一步就是一行delta3 h - Y这一步错误会在梯度检验环节彻底暴露差得不是一点半点。4.2 隐藏层误差最容易丢掉偏置列的地方隐藏层误差要把输出层误差往回传同时乘上隐藏层的 sigmoid 梯度delta2 (delta3 Theta2[:, 1:]) * sigmoid_gradient(z2)这里的关键是 Theta2[:, 1:]。原因有两层第一Theta2 有 10 行 26 列第一列对应的是 a2 的偏置单元第二反向传播到隐藏层时误差只应该传播到隐藏层的 25 个真实单元偏置单元没有输入权重不需要也不需要接收误差。如果这里误写成 Theta2结果会差 26 和 25 的维度错位报错反而会让你庆幸最怕的是维度恰好凑对但值错了比如某些写法里把偏置误差也算了进去。注意 sigmoid_gradient 必须是逐元素相乘所以在代码里是 * 而不是 。sigmoid_gradient 的实现是def sigmoid_gradient(z): return sigmoid(z) * (1 - sigmoid(z))4.3 梯度累积为什么矩阵乘法就是所有样本的累加课程公式里每个样本的梯度贡献是 delta 外积 a然后对所有样本求和再除以 m。用矩阵运算可以一步到位因为矩阵乘法本身就是行与列各自相乘再求和Theta1_grad (delta2.T a1) / m Theta2_grad (delta3.T a2) / m这里 a1 和 a2 都是包含偏置列的形式所以 Theta1_grad 是 25x401Theta2_grad 是 26? 不对Theta2_grad 应该是 10x26。检查一下delta3.T 是 10xma2 是 mx26乘积是 10x26正确。Theta1_grad 是 25x401正确。只要维度一一对上反向传播的主体就完成了。4.4 正则化梯度的加回时机正则化梯度要加在反向传播主体算完之后而且同样排除偏置列Theta1_grad[:, 1:] (lamb / m) * Theta1[:, 1:] Theta2_grad[:, 1:] (lamb / m) * Theta2[:, 1:]这里有一个高频 bug把偏置列也加了正则化或者忘了除以 m。加回时机也重要必须在除以 m 之后的基线上加否则梯度会被放大 m 倍。判断方法很简单把 lamb 设成 0梯度检验应该完全通过再设 lamb1如果正则化梯度写对了梯度检验也应该通过。来总结一下反向传播的全部代码逻辑它最终应该是一个统一的函数输入一维参数 nn_params输出 (cost, grad)def nn_cost_function(nn_params, input_layer_size, hidden_layer_size, num_labels, X, y, lamb): Theta1 nn_params[:hidden_layer_size * (input_layer_size 1)].reshape( hidden_layer_size, input_layer_size 1) Theta2 nn_params[hidden_layer_size * (input_layer_size 1):].reshape( num_labels, hidden_layer_size 1) m X.shape[0] Y np.eye(num_labels)[y.ravel().astype(int) - 1] # 前向传播 a1 np.hstack([np.ones((m, 1)), X]) z2 a1 Theta1.T a2 np.hstack([np.ones((m, 1)), sigmoid(z2)]) z3 a2 Theta2.T h sigmoid(z3) # 代价 cost np.sum(-Y * np.log(h) - (1 - Y) * np.log(1 - h)) / m cost (lamb / (2 * m)) * (np.sum(Theta1[:, 1:] ** 2) np.sum(Theta2[:, 1:] ** 2)) # 反向传播 delta3 h - Y delta2 (delta3 Theta2[:, 1:]) * sigmoid_gradient(z2) Theta1_grad (delta2.T a1) / m Theta2_grad (delta3.T a2) / m Theta1_grad[:, 1:] (lamb / m) * Theta1[:, 1:] Theta2_grad[:, 1:] (lamb / m) * Theta2[:, 1:] grad np.concatenate([Theta1_grad.ravel(), Theta2_grad.ravel()]) return cost, grad这个函数的设计非常重要把代价函数和梯度打包在一起返回后面 scipy 优化器只要声明 jacTrue 就能直接用不用额外写一个独立的梯度函数。5. 数值梯度检验训练前给反向传播做一次体检5.1 数值梯度的原理反向传播公式推导容易实现也容易但混合在一起很难肉眼看出对不对。数值梯度检验就是绕开推导用最朴素的导数定义来验证梯度对参数向量里每个位置给 theta 加一个极小量 epsilon再减一个极小量算两次代价中间差分近似梯度num_grad[i] (cost_plus - cost_minus) / (2 * epsilon)这里 epsilon 通常取 1e-4。为什么不取更小比如 1e-8因为浮点数精度有限epsilon 太小会让 (cost_plus - cost_minus) 被舍入误差吃掉。1e-4 是有限差分和舍入误差之间的一个平衡点。5.2 用一个小网络快速验证而不是直接跑全量数据直接在 5000 个样本、上万维参数上做数值梯度检验会慢到怀疑人生因为每算一次梯度循环要跑一万多次代价函数。课程的做法是构造一个小的调试网络比如输入层 3、隐藏层 5、输出层 3数据规模 m5参数总量小得多。我迁移的时候直接照搬这个思路def debug_initialize_weights(fan_out, fan_in): return np.sin(np.arange(1, fan_out * (fan_in 1) 1)).reshape( fan_out, fan_in 1) / 10 def check_nn_gradients(lamb0): input_layer_size, hidden_layer_size, num_labels 3, 5, 3 m 5 Theta1 debug_initialize_weights(hidden_layer_size, input_layer_size) Theta2 debug_initialize_weights(num_labels, hidden_layer_size) X debug_initialize_weights(m, input_layer_size - 1) y 1 np.mod(np.arange(1, m 1), num_labels) nn_params np.concatenate([Theta1.ravel(), Theta2.ravel()]) cost, grad nn_cost_function( nn_params, input_layer_size, hidden_layer_size, num_labels, X, y, lamb) num_grad np.zeros_like(nn_params) eps 1e-4 for i in range(len(nn_params)): theta_plus nn_params.copy() theta_plus[i] eps theta_minus nn_params.copy() theta_minus[i] - eps cost_plus, _ nn_cost_function( theta_plus, input_layer_size, hidden_layer_size, num_labels, X, y, lamb) cost_minus, _ nn_cost_function( theta_minus, input_layer_size, hidden_layer_size, num_labels, X, y, lamb) num_grad[i] (cost_plus - cost_minus) / (2 * eps) diff np.linalg.norm(num_grad - grad) / ( np.linalg.norm(num_grad) np.linalg.norm(grad)) return diff课程文档里给出的正常结果大约是 1e-9 量级。如果 diff 在 1e-3 或更大说明反向传播写错了不要急着训练。5.3 四个高频翻车点数值梯度检验几乎能定位所有反向传播错误但前提是你知道怎么看结果。我整理了几个自己踩过、也看别人踩过的典型情况现象根本原因解决办法diff 在 1e-3 以上且集中在固定位置delta2 忘了去掉 Theta2 的偏置列检查 Theta2[:, 1:]diff 在 1e-2 级别误差比较均匀梯度没有除以 m检查 Theta1_grad / mdiff 很大cost 本身也异常one-hot 编码错位或 y 没 ravel打印 Y 的行和看对应标签正则化后 diff 变大正则化梯度把偏置列也加了检查 Theta[:, 1:] 后再加另一个建议先做 lamb0 的梯度检验通过后再把 lamb 设为 1 做一次。两次都通过反向传播基本可以放心。6. 训练与隐藏层可视化看网络从随机权重里学到了什么6.1 用 scipy.optimize.minimize 接上我们的代价函数课程原始作业用 fmincg 优化参数Python 里我一般用 scipy.optimize.minimizemethod 选 TNC 或 L-BFGS-B 都能稳定收敛。我实测下来 TNC 对这个题目更省心迭代 500 次以内就能得到不错的效果。调用方式非常直接from scipy.optimize import minimize input_layer_size 400 hidden_layer_size 25 num_labels 10 lamb 1 init_theta1 rand_init_weights(input_layer_size, hidden_layer_size) init_theta2 rand_init_weights(hidden_layer_size, num_labels) init_params np.concatenate([init_theta1.ravel(), init_theta2.ravel()]) res minimize( funlambda nn_params: nn_cost_function( nn_params, input_layer_size, hidden_layer_size, num_labels, X, y, lamb), x0init_params, jacTrue, methodTNC, options{maxiter: 500} ) nn_params res.x Theta1, Theta2 unflatten_params(nn_params)这里 fun 用一个 lambda 把 nn_cost_function 包装成只接受 nn_params 一个参数的函数这样 minimize 调用起来干净。只要函数返回 (cost, grad) 且 jacTrue优化器会自动用我们算好的梯度。6.2 训练集准确率95% 左右是正常水平训练完成后用前向传播算一次预测argmax 找出每个样本概率最大的输出单元再加 1 还原成 1 到 10 的标签m X.shape[0] a1 np.hstack([np.ones((m, 1)), X]) z2 a1 Theta1.T a2 np.hstack([np.ones((m, 1)), sigmoid(z2)]) z3 a2 Theta2.T h sigmoid(z3) pred (np.argmax(h, axis1) 1).reshape(-1, 1) acc np.mean(pred y) * 100 print(fTraining accuracy: {acc:.2f}%)我跑了几次准确率基本都在 95% 上下和课程提供的参考权重在训练集上的表现接近。如果你的准确率只有 90% 左右优先怀疑 one-hot 编码和 y 的维度如果只有 80% 甚至更低回头检查代价函数的正则化项lambda 取 1 时不应该有这么大的偏差。6.3 隐藏层权重可视化最直观的成就感训练完成之后我想推荐一个很多教程不会专门讲但非常有价值的步骤可视化隐藏层权重。取 Theta1 去掉第一列偏置列后的部分每一行是一个隐藏单元对应的 400 维输入权重reshape 成 20x20 就是一张这个隐藏单元在识别什么模式的图display_data(Theta1[:, 1:])如果训练正常这 25 张图看起来会像是各种方向的边缘、斑块和笔画组合而不是一团均匀的噪声。看到这一步你会直观理解神经网络自动学习特征这句话到底是什么意思——它没有专门被告诉数字 0 是中间空心的圆而是从数据中自己长出了能区分类别的局部模式。6.4 完整可运行代码最后把完整代码贴在这里直接复制到一个 .py 文件把 ex4data1.mat 和 ex4weights.mat 放在同目录下就能跑。我习惯把显示函数、网络函数、训练流程分开但不做过度的面向对象封装因为课程练习阶段函数式写法更容易和公式一一对应。import numpy as np import matplotlib.pyplot as plt from scipy.io import loadmat from scipy.optimize import minimize def sigmoid(z): return 1 / (1 np.exp(-z)) def sigmoid_gradient(z): return sigmoid(z) * (1 - sigmoid(z)) def rand_init_weights(l_in, l_out): eps_init np.sqrt(6) / np.sqrt(l_in l_out) return np.random.uniform(-eps_init, eps_init, size(l_out, l_in 1)) def nn_cost_function(nn_params, input_layer_size, hidden_layer_size, num_labels, X, y, lamb): theta1_end hidden_layer_size * (input_layer_size 1) Theta1 nn_params[:theta1_end].reshape( hidden_layer_size, input_layer_size 1) Theta2 nn_params[theta1_end:].reshape( num_labels, hidden_layer_size 1) m X.shape[0] Y np.eye(num_labels)[y.ravel().astype(int) - 1] # 前向传播 a1 np.hstack([np.ones((m, 1)), X]) z2 a1 Theta1.T a2 np.hstack([np.ones((m, 1)), sigmoid(z2)]) z3 a2 Theta2.T h sigmoid(z3) # 代价函数含正则化 cost np.sum(-Y * np.log(h) - (1 - Y) * np.log(1 - h)) / m cost (lamb / (2 * m)) * (np.sum(Theta1[:, 1:] ** 2) np.sum(Theta2[:, 1:] ** 2)) # 反向传播 delta3 h - Y delta2 (delta3 Theta2[:, 1:]) * sigmoid_gradient(z2) Theta1_grad (delta2.T a1) / m Theta2_grad (delta3.T a2) / m # 正则化梯度排除偏置列 Theta1_grad[:, 1:] (lamb / m) * Theta1[:, 1:] Theta2_grad[:, 1:] (lamb / m) * Theta2[:, 1:] grad np.concatenate([Theta1_grad.ravel(), Theta2_grad.ravel()]) return cost, grad def display_data(X, rowsNone, colsNone): m, n X.shape side int(np.sqrt(n)) if rows is None: rows int(np.floor(np.sqrt(m))) if cols is None: cols int(np.ceil(m / rows)) pad 1 canvas -np.ones((pad rows * (side pad), pad cols * (side pad))) idx 0 for i in range(rows): for j in range(cols): if idx m: break max_val np.max(np.abs(X[idx])) canvas[pad i * (side pad): pad (i 1) * (side pad), pad j * (side pad): pad (j 1) * (side pad)] \ X[idx].reshape(side, side) / max_val idx 1 plt.imshow(canvas, cmapgray) def debug_initialize_weights(fan_out, fan_in): return np.sin(np.arange(1, fan_out * (fan_in 1) 1)).reshape( fan_out, fan_in 1) / 10 def check_nn_gradients(lamb0): input_layer_size, hidden_layer_size, num_labels 3, 5, 3 m 5 Theta1 debug_initialize_weights(hidden_layer_size, input_layer_size) Theta2 debug_initialize_weights(num_labels, hidden_layer_size) X debug_initialize_weights(m, input_layer_size - 1) y 1 np.mod(np.arange(1, m 1), num_labels) nn_params np.concatenate([Theta1.ravel(), Theta2.ravel()]) cost, grad nn_cost_function( nn_params, input_layer_size, hidden_layer_size, num_labels, X, y, lamb) num_grad np.zeros_like(nn_params) eps 1e-4 for i in range(len(nn_params)): theta_plus nn_params.copy() theta_plus[i] eps theta_minus nn_params.copy() theta_minus[i] - eps cost_plus, _ nn_cost_function( theta_plus, input_layer_size, hidden_layer_size, num_labels, X, y, lamb) cost_minus, _ nn_cost_function( theta_minus, input_layer_size, hidden_layer_size, num_labels, X, y, lamb) num_grad[i] (cost_plus - cost_minus) / (2 * eps) diff np.linalg.norm(num_grad - grad) / ( np.linalg.norm(num_grad) np.linalg.norm(grad)) print(fGradient diff: {diff:.2e}) return diff def main(): data loadmat(ex4data1.mat) X data[X] y data[y] weights loadmat(ex4weights.mat) Theta1_ref weights[Theta1] Theta2_ref weights[Theta2] input_layer_size 400 hidden_layer_size 25 num_labels 10 lamb 1 # 用参考权重验证代价函数 ref_params np.concatenate([Theta1_ref.ravel(), Theta2_ref.ravel()]) cost_no_reg, _ nn_cost_function( ref_params, input_layer_size, hidden_layer_size, num_labels, X, y, 0) cost_with_reg, _ nn_cost_function( ref_params, input_layer_size, hidden_layer_size, num_labels, X, y, lamb) print(fCost without reg: {cost_no_reg:.6f}) print(fCost with reg: {cost_with_reg:.6f}) # 梯度检验 check_nn_gradients(lamb0) check_nn_gradients(lamb1) # 随机初始化并训练 init_theta1 rand_init_weights(input_layer_size, hidden_layer_size) init_theta2 rand_init_weights(hidden_layer_size, num_labels) init_params np.concatenate([init_theta1.ravel(), init_theta2.ravel()]) res minimize( funlambda nn_params: nn_cost_function( nn_params, input_layer_size, hidden_layer_size, num_labels, X, y, lamb), x0init_params, jacTrue, methodTNC, options{maxiter: 500} ) nn_params res.x theta1_end hidden_layer_size * (input_layer_size 1) Theta1 nn_params[:theta1_end].reshape( hidden_layer_size, input_layer_size 1) Theta2 nn_params[theta1_end:].reshape( num_labels, hidden_layer_size 1) # 预测与准确率 m X.shape[0] a1 np.hstack([np.ones((m, 1)), X]) z2 a1 Theta1.T a2 np.hstack([np.ones((m, 1)), sigmoid(z2)]) z3 a2 Theta2.T h sigmoid(z3) pred (np.argmax(h, axis1) 1).reshape(-1, 1) print(fTraining accuracy: {np.mean(pred y) * 100:.2f}%) # 可视化隐藏层 display_data(Theta1[:, 1:]) plt.title(Hidden layer visualization) plt.show() if __name__ __main__: main()跑这个脚本的时候梯度检验会花一点时间因为参数虽然不多但也要逐个循环。看到两个 diff 都落在 1e-9 量级就可以安心等训练了。我自己的体会是ex4 最大的收获不是让准确率达到 95%而是亲手把链式法则变成可以逐行调试的矩阵代码。如果你也在复习这门课建议把梯度检验这一步当成必做项——它比任何博客里的标准答案都更能证明你真的理解了反向传播。

关于本文作者

来自尧图内容编辑团队

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

尧图内容编辑团队

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

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

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

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

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

网站改版的5个关键决策

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

获取专属建站方案

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

立即免费咨询