ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

手写浅层神经网络:反向传播与参数调优实战解析

手写浅层神经网络:反向传播与参数调优实战解析 这不是一篇简历模板也不是课程广告而是我花了几个晚上把吴恩达深度学习第一课第三周的课后作业从头到尾啃完以后沉淀出来的一份完整实战记录。浅层神经网络这周正好卡在一个特别微妙的位置前一周是逻辑回归这种“单细胞”模型后一周是深度网络那种“多层大厦”。这周的作业就是让你亲手搭一个单隐藏层神经网络去对一份“平面色块数据”做分类。数据集本身是人为构造的图案像一朵花也像靶盘目标就一个——用自己写的反向传播把两类点分开。我做的过程中最大的感受是作业代码量不大但每一行都有分量。它不是让你调库而是让你把 forward propagation 和 backward propagation 从数学公式手搓成代码再把梯度检查、参数更新、学习率对收敛的影响全部过一遍。如果你之前只是用 TensorFlow 或 PyTorch 调过模型这周作业能帮你把底层那层纱撕开。这篇文章适合谁看准备刷完这门课的初学者、正在复习面试基础的人、想彻底搞懂反向传播的“调包侠”。我会从作业怎么理解开始一直讲到每段代码为什么这么写、哪些地方容易踩坑、实际调参时该怎么观察曲线。所有内容都是我在复现过程中逐步验证过的不是纸上谈兵。1. 作业核心任务拆解平面数据分类到底在做什么1.1 数据和目标从一张“花形图”认识非线性的力量第三周作业开篇给了一份planar_utils.py里面预置了load_planar_dataset()函数。运行后会得到X2x400和Y1x400也就是说有 400 个样本每个样本有 2 个特征。把样本点按特征值画成散点图你会看到红色点和蓝色点分布在一个类似花朵形状的图案里红蓝两色交替出现边界是一条平滑的曲线完全不是直线能切开的。作业的第一步不是写网络而是先让你跑一次逻辑回归。为什么要跑这个因为这是基线对比。你只有先亲眼看到线性模型在这个数据集上的准确率只有 47% 左右后面构造隐藏层神经网络时才能体会到非线性特征提取带来的巨大提升。我记得我跑完逻辑回归后看到决策边界画出来只是一条直线斜着穿过去红色区域和蓝色区域大量错分一下就理解了为什么需要隐藏层。这里顺便提一个新手容易忽略的点Y的维度是(1, m)不是(m,)。这意味着我们在写代码时所有标签相关的 shape 都要保持这种“行向量”风格否则后面广播机制会把维度搞乱。很多人在作业里的报错有一半是维度不匹配导致的这一点我会在后面的问题排查章节详细展开。1.2 作业要求的“手写五步法”从参数初始化到预测集成的完整链条第三周的编程作业在 pdf 里给了明确的函数骨架目标就是让你补全以下几个核心模块初始化参数、前向传播、计算损失、反向传播、更新参数最后封装成一个nn_model()整体调用再对结果进行预测和可视化。这个流程和你在 scikit-learn 里fit一下的感觉完全不同因为每个环节都摊开在你面前。我把这个五步法对应的作业函数列成一张表方便你对号入座步骤函数名作用核心要点1initialize_parameters初始化 W1, b1, W2, b2随机初始化不能全零2forward_propagation计算 Z1, A1, Z2, A2隐藏层用 tanh输出层用 sigmoid3compute_cost交叉熵损失用np.multiply逐元素乘后求和4backward_propagation计算 dW1, db1, dW2, db2链式法则逐层回传5update_parameters梯度下降更新学习率决定步长这五个环节里最核心也最抽象的当然是反向传播。课程视频里 Andrew 用手绘的计算图一步步推导公式看起来也简单但你真正要写成numpy代码时每一步的 shape 都必须对得上。我写的时候经常是先在草稿纸上把(n_x, m)、(n_h, m)、(n_y, m)这些 shape 标出来再对照着写代码这样能少走很多弯路。2. 核心代码逐段精讲每一行 numpy 背后的数学含义2.1 初始化参数为什么 W 必须随机b 可以全零def initialize_parameters(n_x, n_h, n_y): np.random.seed(2) W1 np.random.randn(n_h, n_x) * 0.01 b1 np.zeros((n_h, 1)) W2 np.random.randn(n_y, n_h) * 0.01 b2 np.zeros((n_y, 1)) parameters {W1: W1, b1: b1, W2: W2, b2: b2} return parameters这段代码里面有个坑也是作业里明确让你思考过的问题为什么不把 W1 初始化为全零如果 W1 全零那么在第一次前向传播中隐藏层的所有神经元会收到完全相同的输入信号对应的梯度也完全相同。更新之后它们依然相同整个网络就退化成一个“只有一个神经元在重复计算”的模型多个隐藏单元失去了意义。这就是对称性问题。那为什么乘以 0.01因为如果初始值太大tanh 的输入 Z1 会落在饱和区导数趋近于零梯度更新极慢。作业里这个数据集是 2 个输入特征0.01 这个缩放因子是合理的如果换成更深的网络或者别的激活函数初始化策略还要再调整比如 Xavier 或 He 初始化但那不是这周的内容。这里我还想强调一下np.random.seed(2)。作业预设了随机种子保证每个人跑出来的结果一致。但我在复现时试过把 seed 改成别的值发现最终准确率会有小幅波动在 88% 到 91% 之间。这说明不同起点确实会收敛到不同的局部最优。这个现象本身就很有教学价值。2.2 前向传播从输入到预测的“信息流动”def forward_propagation(X, parameters): W1 parameters[W1] b1 parameters[b1] W2 parameters[W2] b2 parameters[b2] Z1 np.dot(W1, X) b1 A1 np.tanh(Z1) Z2 np.dot(W2, A1) b2 A2 sigmoid(Z2) cache {Z1: Z1, A1: A1, Z2: Z2, A2: A2} return A2, cache前向传播的代码很直白就是矩阵乘法加偏置再经过激活函数。这里需要留意的第一个细节是矩阵乘法的顺序。W1 的 shape 是(n_h, n_x)X 的 shape 是(n_x, m)所以np.dot(W1, X)得到(n_h, m)每一列对应一个样本在所有隐藏神经元上的线性输出。如果你把顺序写反了np.dot(X, W1)在维度上不一定立刻报错但语义完全不对而且后面反向传播时梯度也必然错。第二个细节是为什么隐藏层选 tanh输出层选 sigmoid。tanh 输出范围在(-1, 1)均值接近 0可以看作对输入做了零中心化在浅层网络里通常比 sigmoid 收敛更快。输出层用 sigmoid 是因为这是二分类任务我们要把 logit 压缩到(0, 1)区间才能解释成概率。如果你把 sigmoid 换成了 tanh输出可能出现负数损失函数里的np.log(A2)就直接报错了。第三个细节是cache。作业里要求把中间结果存下来这个 cache 不是给前向传播用的而是给反向传播用的。因为你反向求梯度时要用到A1、Z1这些中间值不缓存的话就得重新前向算一遍白白浪费算力。后来你看到 PyTorch 里的activations缓存机制思想是一脉相承的。2.3 损失函数交叉熵的形状与求和方向def compute_cost(A2, Y, parameters): m Y.shape[1] logprobs np.multiply(np.log(A2), Y) np.multiply((1 - Y), np.log(1 - A2)) cost - np.sum(logprobs) / m cost float(np.squeeze(cost)) return cost逻辑回归那周已经讲过交叉熵的公式这周作业里的实现稍有不同但本质一致。这里我想提醒的是np.multiply和np.dot的区别np.multiply是逐元素相乘np.dot是矩阵乘法。在这个损失函数里A2是(1, m)Y也是(1, m)我们要做的是把“每个样本的预测概率和真实标签”对应位置相乘再求和所以必须用逐元素乘最后再np.sum全部加起来除以样本数得到平均损失。np.squeeze这一步也很重要。因为如果不做loss 的 shape 可能是(1, 1)虽然后面计算没问题但打印出来带一对多余的括号而且在某些 Python 数值运算场景下可能引发类型问题。转成 float 之后后面画 loss 曲线时也更干净。这里有个我在复现时注意到的小细节作业里计算的是均值损失而不是总损失。除以 m 的好处是让损失不受样本量影响而且梯度大小也被归一化学习率的选择就不会因为数据量变大而需要相应放大或缩小。这个习惯在真实项目里也要保持。2.4 反向传播全篇作业最硬核的 10 行代码def backward_propagation(parameters, cache, X, Y): m X.shape[1] W1 parameters[W1] W2 parameters[W2] A1 cache[A1] A2 cache[A2] dZ2 A2 - Y dW2 (1 / m) * np.dot(dZ2, A1.T) db2 (1 / m) * np.sum(dZ2, axis1, keepdimsTrue) dZ1 np.multiply(np.dot(W2.T, dZ2), (1 - np.power(A1, 2))) dW1 (1 / m) * np.dot(dZ1, X.T) db1 (1 / m) * np.sum(dZ1, axis1, keepdimsTrue) grads {dW1: dW1, db1: db1, dW2: dW2, db2: db2} return grads这段代码是精华中的精华很多人就是卡在这里。如果你第一次接触反向传播我建议按下面这个思路把代码映射到数学公式。先看dZ2 A2 - Y。这是交叉熵损失对输出层线性输出 Z2 的导数。这个结果在数学推导里会经过 sigmoid 导数的化简最终恰好等于A2 - Y非常优雅也方便记忆。你可以尝试手动推损失 L 对 A2 求导是- (Y / A2) (1 - Y) / (1 - A2)再乘上 sigmoid 的导数A2 * (1 - A2)化简后正好是A2 - Y。这就是为什么直接相减就行不用再乘激活函数导数。接着看dW2 (1 / m) * np.dot(dZ2, A1.T)。这个公式模式是固定的上一层激活值的转置乘以当前层的误差再除以 m。相当于把所有样本的梯度累加后求均值。db2则是对dZ2在样本维度axis1上求和keepdimsTrue是为了保持(1, 1)的 shape方便广播相加。最绕的是dZ1这一行。np.dot(W2.T, dZ2)把输出层的误差通过权重 W2 回传到隐藏层得到隐藏层线性输出 Z1 的未激活梯度。然后乘上(1 - np.power(A1, 2))这是 tanh 对 Z1 求导的结果。tanh 的导数公式是1 - tanh²(x)而 A1 就是tanh(Z1)所以直接1 - A1²就是逐元素的导数。这行代码把链式法则中“误差从上层传下来”和“本层激活函数的局部梯度”两件事合并在一起理解了它就理解了反向传播的核心。有个常见错误是忘了把dZ2除以 m结果 loss 曲线会剧烈震荡因为梯度被放大了 400 倍。每更新一步参数损失不是下降而是到处乱跳。我用 0.01 学习率试过这种错误版本loss 直接从 0.6 冲到了 100非常恐怖。2.5 参数更新与整体封装把零散函数拼成可训练系统def update_parameters(parameters, grads, learning_rate1.2): W1 parameters[W1] b1 parameters[b1] W2 parameters[W2] b2 parameters[b2] dW1 grads[dW1] db1 grads[db1] dW2 grads[dW2] db2 grads[db2] W1 W1 - learning_rate * dW1 b1 b1 - learning_rate * db1 W2 W2 - learning_rate * dW2 b2 b2 - learning_rate * db2 parameters {W1: W1, b1: b1, W2: W2, b2: b2} return parameters更新参数的代码就是参数减去学习率乘以梯度没有任何花哨操作。这个作业里的默认学习率是 1.2比你在很多框架里常用的 0.01 大得多。为什么可以这么大因为这个数据集只有 400 个样本网络也很小损失曲面相对平滑而且使用的是批量梯度下降每次更新都基于全部样本梯度方向比小批量更稳定所以大学习率也能稳定收敛。在真实大数据集上你用 1.2 大概率直接发散。把上面几个函数串起来的nn_model()是作业的核心调用入口。它做的事情就是循环 num_iterations 次前向传播、计算损失、反向传播、更新参数。循环结束后返回训练好的参数。我在这个函数里额外加了打印 loss 的代码每 1000 次迭代输出一次方便观察收敛趋势。我还做过一个实验把迭代次数从 10000 改成 2000模型准确率只有 67% 左右但 loss 曲线看起来仍在下降。这说明 10000 次迭代对这个网络和数据量来说不算过分如果你用更复杂的数据可能还需要配合早停策略不是单纯堆迭代次数。3. 调参与实验复盘4 个隐藏神经元怎么把准确率从 47% 拉到 90%3.1 隐藏层神经元数量从 1 到 50 的决策边界变化作业里有个可选练习是跑plot_decision_boundary把不同n_h隐藏层神经元数量下的分类效果可视化。我分别试了 1、2、3、4、5、20、50直观感受非常明显。n_h 1时隐藏层只有一个神经元模型能力其实和逻辑回归差不多决策边界仍然是一条直线准确率只有 63% 左右。n_h 2时开始出现弯折但边界还太简陋准确率提升不大。n_h 3时边界平滑了不少已经能大致区分内外两个弧形区域。n_h 4是作业推荐值准确率能到 90.75%决策边界已经非常贴合数据分布的“花朵”形状。n_h继续增加到 20、50训练集准确率会继续上升但一方面计算量变大另一方面很容易过拟合决策边界开始出现不规则的突出尖角。这个实验给我留下的印象特别深因为它直观地展示了模型容量与任务复杂度之间的匹配关系。数据分布本身并不复杂4 个神经元就足够拟合硬上 50 个神经元反而学到了一些奇怪的噪声特征。放到实际场景里这就是欠拟合、正好、过拟合三种状态的可视化教材。3.2 学习率1.2 和 0.01 的差距到底在哪作业里默认学习率是 1.2我特意做了几组对比实验0.01、0.1、0.5、1.2、2.0。结果很有意思。学习率 0.01 时loss 确实在下降但非常缓慢10000 次迭代之后准确率大概只有 80% 左右还没完全收敛。学习率 0.1 时收敛速度加快准确率到 86% 左右。学习率 1.2 时loss 快速下降到 2000 次迭代附近就已经接近收敛最终准确率 90.75%。学习率 2.0 时前几百次迭代 loss 还能下降但随后开始震荡最终准确率反而下滑模型不稳定。这说明在浅层小网络上较大的学习率能加速收敛且不一定坏事关键要看损失曲面和批量大小。但在深度网络或小批量训练场景下过大的学习率很容易跳过最优点甚至直接发散所以工程上通常还是从较小的学习率开始配合学习率衰减或 Adam 这类自适应优化器。我当时做这个实验时是直接修改nn_model()里的learning_rate参数然后在循环里用costs.append(cost)记录每一千次迭代的损失最后用 matplotlib 画出多条 loss 曲线叠在一起。这个对比图强烈推荐你也画一画因为“看曲线”远比“看数字”更能理解学习率的含义。3.3 不同初始化方式的影响只试一次就踩中了对称性问题作业文档里没有要求你去做不同初始化的对比实验但我在复现时快速试了一遍“全零初始化”结果毫不意外地触发了对称性问题。loss 几乎不动准确率维持在一半左右就像模型根本没在学习。原因前面已经说过所有隐藏神经元完全对称梯度相同参数更新后依然对称网络退化成一个宽的逻辑回归。我还试过把 0.01 改成 5.0也就是用很大的随机初始值。结果 forward propagation 时 Z1 的值很容易落在 tanh 饱和区反向传播的梯度被压缩到接近零网络更新极其缓慢loss 下降得很费劲。这个实验让我真正理解初始化不仅是“打破对称性”还要让激活值落在激活函数的敏感区间这和激活函数的选择紧密相关。4. 常见问题与排查技巧实录新手出错率最高的 5 个点4.1 维度不匹配numpy 报错无处不在怎么高效定位我在完成作业过程中遇到过好几种维度不匹配报错最典型的就是矩阵乘法时两个矩阵的 shape 不符合要求。比如np.dot(W2, A1)时如果前一层的输出 A1 维度不对报错信息会提示shapes (1,4) and (2,400) not aligned这时候你就要回头检查前向传播时矩阵乘法的顺序。排查维度问题我有个习惯直接在每一步前向传播前用X.shape、W1.shape、Z1.shape打印出来核对。尤其是从initialize_parameters开始W1应为(n_h, n_x)W2应为(n_y, n_h)任何一步写反后面全是连锁反应。还有一个容易忽略的地方是b1和b2的 shape。b1必须是(n_h, 1)b2必须是(n_y, 1)如果写成了(n_h,)虽然广播有时也能算但在反向传播np.sum(dZ1, axis1, keepdimsTrue)时会得到不同 shape可能引发隐藏 bug因为这些 bug 不报错但结果全错。4.2 激活函数选择错误隐藏层用 sigmoid输出层用 tanh 会怎样作业文档明确说隐藏层用 tanh输出层用 sigmoid。但我也看到有同学把两个都写成了 sigmoid或者把输出层写成了 tanh。前者的问题主要是收敛变慢因为 sigmoid 输出不是零中心的梯度更新效率低后者则是致命的因为 tanh 输出有正有负np.log(A2)里会出现负数直接NaN。如果你在作业里遇到 loss 变成nan优先检查两点一是输出层是否用了 sigmoid二是是否把np.log的输入搞成了负数或 0可以加个np.clip临时兜底但作业里不需要这么复杂改对激活函数就行。4.3 反向传播公式抄错最常见的是忘记除以 m我前面已经提过dW计算时忘记除以 m 是新手很爱犯的错误。症状是 loss 曲线在前几轮剧烈上升甚至冲到几百。很多人这时会怀疑学习率太大但调小学习率也只是减缓发散速度治标不治本。另一个常见错误是dZ1里把np.dot(W2.T, dZ2)写成了np.dot(dZ2, W2.T)。这俩在 shape 上不一定马上报错但结果是完全错误的梯度。我排查这种问题的方法是先用小数据把前向传播手算一遍再用数值梯度法验证反向传播是否正确。虽然作业没要求做梯度检查但自己加一个数值梯度验证函数对理解原理极有帮助。4.4 准确率变化不大可能是循环根本没更新参数有同学写nn_model()的时候for 循环里调用了forward_propagation但忘了把每次update_parameters返回的 parameters 重新赋给当前参数变量。这种情况下网络参数根本没更新loss 曲线是一条水平线准确率恒定在逻辑回归的水平。排查方法很简单打印每轮更新后的 W1 是否发生了变化或者看 loss 是否持续下降。如果 loss 不降先检查参数更新那行代码是不是在循环内、有没有重新赋值。4.5 可视化报错决策边界画不出来通常不是模型问题plot_decision_boundary需要生成一个网格数据然后用模型预测每个点的类别再用contourf绘制区域。有时报错是因为网格点的维度不对或者predict函数里的 forward propagation 没有正确处理单个样本。我在调试时通常先确认X的 shape 是(n_x, m)网格点要堆叠成(n_x, 网格点数)的格式而不是反的。4.6 问题排查速查表下面这个表汇总了我在完成作业和排查同学代码时遇到的典型问题、症状与解决方案你可以直接收藏备用。症状可能原因解决方案loss 为 NaN输出层激活函数写错 / np.log 出现负数确保输出层用 sigmoidloss 不降参数未更新 / 全零初始化检查循环内 update 是否执行loss 剧烈震荡dW 忘记除以 m / 学习率过大核对反向传播公式准确率一直 50% 左右网络退化对称性/单神经元随机初始化、增加隐藏神经元维度报错矩阵乘法顺序或 shape 写错打印每一步的 shape决策边界图异常网格数据维度问题确保 X 和网格点 shape 正确5. 作业之外浅层神经网络代码如何映射到生产级项目5.1 从这份作业里能带到真实工程的习惯很多人觉得课程作业和真实项目差得很远因为实际工作里没人会手写反向传播。但我不这么看。这周的作业虽然只是玩具数据集但代码里体现的几个工程习惯是通用的。第一个习惯是缓存中间结果。作业里的cache就是一个最简单的例子前向传播时把中间值存下来反向传播直接用。真实框架里PyTorch 的autograd会缓存参与计算的张量TensorFlow 的GradientTape也是类似思路。理解了这个缓存机制你在排查“显存占用过高”的问题时就会明白中间激活值缓存是显存开销的大头。第二个习惯是把多维数组的 shape 当作第一优先级。作业里频繁出现维度打印和检查真实项目里我们通常用assert语句在数据进入模型前做校验。数据管道里的 shape 一旦出错模型训练时才会报错那时排查成本已经很高了。提前检查成本极低收益巨大。第三个习惯是把模型封装成可配置的组件。作业里的nn_model()虽然简单但已经把参数初始化、前向、反向、更新封装成了一个完整的函数。真实工程里我们用类把模型定义、前向逻辑、训练循环拆得更开但核心思想一脉相承。5.2 从分类准确率到决策边界模型可解释性的初级形态作业最后一步会画出决策边界这一步放在课程里是“可视化的趣味展示”但在真实项目中决策边界就是模型可解释性的可视化表达。你去给业务方解释一个模型为什么判断某条样本为欺诈最好的方式往往不是贴一堆评估指标而是画一张图展示模型在特征空间里的决策区域。第三周作业里的决策边界还是二维的因为我们只有两个特征。实际项目中特征维度动辄几十上百不可能直接画二维边界但可以借助 t-SNE、PCA 等降维工具把高维特征降到二维后可视化。理解了这个思路你就知道课程里“画边界”这件事并非只是为了好玩而是模型调试和解释的基础功。5.3 寻找“生产级代码的最佳实践标准”一门课给不了全部答案热搜词里有一句是“生产级代码的最佳实践标准是什么”这其实是一个非常大的问题但第三周的作业能给你最初步的答案。你会发现作业代码虽然能跑通但它离生产级标准还很远没有类型检查没有单元测试没有 Logging没有参数校验没有可复现的随机管理虽然设置了 seed也没有模型保存与加载。我在把这份作业代码改编成“更像工程”的版本时做了几件事用dataclasses定义超参数和模型配置把函数改成NeuralNetwork类加入assert校验 shape增加logging记录训练过程把np.random.seed封装成可配置项。这个改动成本不高但代码的健壮性和可读性提升非常明显。如果你已经在工作建议自己也做一次这样的“重构训练”这比看任何框架源码都更能体会代码质量的重要性。6. 复现建议与避坑补充手把手跑通整个 Notebook6.1 环境准备这份作业是基于 Python 3 NumPy Matplotlib 的不需要 GPU。我推荐直接用 Anaconda 创建一个虚拟环境Python 版本 3.7 到 3.10 都行太新的版本有些旧代码依赖可能会出问题。安装numpy、matplotlib、scikit-learn就够用了其中 scikit-learn 主要是用来调用sklearn.linear_model.LogisticRegressionCV跑基线模型。如果你是在 B 站或者其他平台跟着视频学习建议把课程官方的planar_utils.py下载到和 notebook 同一目录。代码里已经包含了load_planar_dataset、plot_decision_boundary、predict等辅助函数不用自己重新造轮子。6.2 一步一步跑通的建议顺序我第一次做这个作业时习惯是先把所有单元格从头到尾运行一遍看看输出结果是什么然后再一点点细读代码。但第二次复现时我改用“边写边跑”的方式先跑通initialize_parameters把W1、b1的 shape 打印出来再写forward_propagation用一个小样本验证输出维度再依次实现损失、反向和更新。这种“模块化验证”的方式比“全部写完再调”高效得多因为你每一步都能确认当前代码没有 bug。到nn_model封装时只要每个子函数都对整体基本不会出大问题。每完成一个函数你都可以用作业自带的测试代码跑一下比如forward_propagation的测试会检查 cache 里的Z1shape 是否为(4, 2)。这些预设测试其实就是最简单的单元测试也是真实工程里“写一点测一点”的雏形。6.3 关于“吴恩达 B 站视频笔记”和“深度学习课本 pdf”的一点建议网上流传的教程资源非常多有不少人会先看各种笔记或者配套书籍比如邱锡鹏老师的《神经网络与深度学习》。我的体会是这周作业最好的伴侣不是厚厚的书而是课程视频和随手可以翻的 Cheat Sheet。先把视频里的计算图和公式看懂再对照作业代码去还原公式比一上来啃大部头更有效率。如果看书我建议重点关注前馈神经网络和反向传播这两章不要急于跳到 CNN、RNN、Transformer。第三周作业本质就是前馈神经网络的一个实例把前馈和 BP 吃透后面所有复杂模型都只是它的变体。热搜里那些关于卷积神经网络、循环神经网络、图神经网络的问题先记在脑子里等把这一周的基础打牢了再去了解信息密度会高很多。6.4 代码之外建议你做的 3 个扩展实验作业本身虽然已经跑通但我强烈建议你在完成基本要求后再做几个小实验它们是进阶理解的钥匙。实验一修改隐藏层神经元数量画出不同n_h的准确率曲线。你会发现准确率不是一直上升的这比单纯完成作业更接近调参的真实感受。实验二修改学习率观察 loss 曲线的变化。这一实验能帮你形成对学习率这个超参数的直觉以后再用框架训练模型时至少能判断模型发散的节奏是“步子太大”还是“梯度算错”。实验三把激活函数从 tanh 改成 ReLU看看收敛速度和准确率会发生什么变化。ReLU 在浅层网络上的表现可能不如 tanh 平滑但你会发现它计算更快而且负区间梯度为零的特性会带来完全不同的行为。这个对比就是以后学习深度网络时最重要的直觉积累之一。最后再分享一个小技巧我在反复跑这个作业时养成了一个非常受用的习惯每次修改参数或代码先清空上一次的缓存变量也就是把“整个 Kernel”重新启动然后从上到下运行一遍。因为 Jupyter Notebook 里如果之前跑过某些变量后面修改代码后这些旧变量可能残留在内存里导致你看到的输出是旧结果新代码根本没生效。另一个技巧是给nn_model手动加一个“访问计数器”比如在循环里打印当前迭代次数和 loss再用%matplotlib inline把损失曲线画出来。这样你可以实时看到训练状态如果 loss 突然跳到非常大立刻停下来改代码不要傻傻等 10000 次循环跑完再观察。说实话这个作业的代码量不大但它带给我最大的收获是那些在框架里一个model.fit()就能完成的事情背后其实是如此清晰和有迹可循的一整套流程。以后再看到“前馈神经网络”和“BP反向传播神经网络拟合曲线”这样的字眼我不再觉得它们陌生因为我已经亲手写过每一个环节。希望这篇文章也能帮你把第三周的路走得更顺。
返回列表