
1. 第三周到底在解决什么问题先说个很多人看完课程视频后的共同感受前两周还在讲单神经元、逻辑回归觉得“这不就是线性分类器换了个马甲吗”到了第三周突然神经网络变得有“层次感”了。第三周的核心任务是从单个神经元过渡到“一层神经元”也就是真正意义上的浅层神经网络。吴恩达在这一周里反复强调的其实是三件事神经元之间怎么排列、激活函数到底该选谁、梯度是怎么一层一层传回去的。单神经元本质上是逻辑回归的参数化表达它只能学出线性的决策边界。而浅层神经网络引入了一个中间层隐藏层等于在输入和输出之间插入了一个“特征再加工车间”。输入先被隐藏层重新组合成更高阶的特征再由输出层基于这些特征做决策。这就是第三周和前面内容的分水岭。很多人看完第三周的数学推导会觉得前向传播好理解反向传播那些偏导公式算到后面就晕了。我一开始也是这样。但后来我意识到第三周课程设计的精妙之处在于它故意把隐藏层神经元个数限制在一层让你有足够精力把前向、反向传播的每一步都亲手推一遍而不是依赖深度学习的框架帮你自动求导。这门课的目标不是让你背公式而是让你建立“参数如何通过梯度被更新”的直觉。这一周的内容也是后续课程的基础第四周的深层神经网络本质上就是第三周结构的纵向堆叠。分类任务的网络结构设计、二分类和多分类的输出层差异、隐藏层节点数量的选择这些问题都能在第三周找到思考原点。所以把这一周吃透后面才会顺畅得多。2. 从网络结构开始理解输入、隐藏层、输出层2.1 符号约定与数学表示吴恩达在第三周花了不少时间统一符号这部分看起来枯燥但恰恰是后续所有推导的基础。这里把最核心的几个符号重新捋一遍因为搞混其中一个下标反向传播基本就推不下去了。输入特征记为 x维度是 n_x也就是单个样本的特征数。隐藏层有 n_h 个神经元输出层有 n_y 个神经元对于二分类 n_y 1。权重矩阵 W^{[1]} 的维度是 (n_h, n_x)偏置 b^{[1]} 的维度是 (n_h, 1)。输出层的权重 W^{[2]} 维度是 (n_y, n_h)偏置 b^{[2]} 维度是 (n_y, 1)。注意权重矩阵的行数等于当前层的神经元个数列数等于上一层的神经元个数。这个约定和很多教材相反刚接触时特别容易把 W 的维度记反。我的习惯是每次写代码前先在纸上标一遍维度这比硬背公式更可靠。输入层的“神经元”其实不叫神经元它就是特征本身不做任何计算。隐藏层名字的由来也简单它的输出不直接对外可见而是作为中间表征喂给下一层。这里要留意的是隐藏层的每个神经元并不是分别对着输入做一次逻辑回归那么简单它们在同一时刻接收相同的输入但各自有不同的权重和偏置所以在输入空间里它们各自从不同的角度去“观察和组合”特征。2.2 前向传播到底做了几次“投影”前向传播过程本质上是对输入数据做两次连续的仿射变换加非线性映射。用公式写出来就是$$Z^{[1]} W^{[1]} X b^{[1]}$$$$A^{[1]} g^{[1]}(Z^{[1]})$$$$Z^{[2]} W^{[2]} A^{[1]} b^{[2]}$$$$A^{[2]} \sigma(Z^{[2]})$$第一行和第三行是线性变换第二行和第四行是激活函数带来的非线性变换。如果没有激活函数两层线性变换叠加之后仍然是一次线性变换那浅层神经网络和单个逻辑回归就没有本质区别了。这也是第三周课程中反复强调的那句话非线性激活函数是神经网络能够逼近复杂函数的根本原因。生活化的类比是只有线性变换的网络就像只会直走的机器人无论你怎么组合它还是在直线上做组合而激活函数让机器人可以转弯路径立马丰富起来。前向传播的向量化其实没有想象中复杂。课程里用大写 X 代表整个训练集的特征矩阵维度是 (n_x, m)m 是样本数。此时 Z^{[1]} 的维度变成 (n_h, m)每一列对应一个样本的隐藏层输出整个前向过程还是那四个公式只是 X 的列代表样本替换了之前单样本的向量。我个人的建议是先手推单样本版本再对照矩阵版本看一遍逻辑是一致的只是视野从单个样本切到了整个批次。3. 激活函数选型不能只盯住 sigmoid3.1 各种激活函数的对比与直觉第三周的系统梳理很有价值因为很多人从入门到实战使用的激活函数可能就一个 ReLU但为什么它能打哪些场景要换成别的这需要有清晰认知。sigmoid输出范围 (0,1)教科书里最常见的激活函数逻辑回归默认用它。但它有两个硬伤一是当输入绝对值较大时导数趋近于零梯度容易消失二是输出不是零中心的这会导致后续层的输入总是正的权重更新时容易出现“锯齿效应”收敛变慢。第三周用它主要因为输出层做二分类时需要给出一个 0 到 1 之间的概率这时候 sigmoid 是不可替代的。tanh输出范围 (-1,1)它是零中心的所以收敛通常比 sigmoid 快。吴恩达也明确说了隐藏层默认用 tanh 往往好于 sigmoid。但它同样存在饱和区梯度消失的问题。ReLUx 小于 0 输出 0大于 0 输出 x。好处是正区间梯度恒为 1不会衰减计算也快是目前隐藏层最常用的选择。坏处是“神经元死亡”如果某个神经元的输入长期为负它的梯度就是 0权重很难再更新这个神经元相当于废了。Leaky ReLU 就是针对这个问题的小修小补给负区间一个很小的斜率比如 0.01让梯度不至于完全断掉。第三周里做选择题的一个基本结论是二分类输出层选 sigmoid隐藏层首选 ReLU也可以先用 tanh 对比效果。不要一上来就堆花活网络到第四周才加深先保证基础配置能稳定训练。3.2 为什么隐藏层必须是非线性激活这个问题值得多讲几句因为它在面试里出现的频率极高。假设隐藏层激活函数是线性的那整个网络的输出可以写成输入的线性函数。具体推一下A^{[1]} W^{[1]} X b^{[1]}Z^{[2]} W^{[2]} A^{[1]} b^{[2]}把 A^{[1]} 代入后得到 W^{[2]} W^{[1]} X W^{[2]} b^{[1]} b^{[2]}这仍然是 X 的线性函数。既然等价于一个单层线性模型堆再多的层也只是在浪费算力。只有引入非线性激活每一层才能对输入空间做“非线性的扭曲”。多个非线性层的叠加才能逐步把原始空间的数据映射到适合分类的特征空间。这也是说明第三周课程价值的一个关键点它不只是讲“怎么搭一个两层网络”而是解释“为什么网络能比单层强”。3.3 激活函数导数表做反向传播编程练习时导数表达式是直接要写进代码里的。我把第三周涉及到的激活函数导数整理成了一张速查表平时做作业和项目都会反复用激活函数函数表达式导数表达式sigmoida 1 / (1 e^{-z})dz a(1 - a)tanha tanh(z)dz 1 - a^2ReLUa max(0, z)dz z 0 ? 1 : 0Leaky ReLUa max(0.01z, z)dz z 0 ? 1 : 0.01这里有个常见陷阱ReLU 在 z 恰好等于 0 时不可导。实践中的处理方式是直接令导数为 0。统计上 z 恰好等于 0 的概率极低不必纠结数学严格性这是工程和数学的常见妥协课程里也默认这么处理。4. 反向传播把偏导链一条条拆给你看4.1 梯度从输出层往回走的三个关键公式第三周的课程把反向传播公式给的比较密集如果不自己推一遍很容易记混。我的经验是先把“最后一层”搞明白然后反向逐层套同一个模式。输出层以二分类为例损失函数是交叉熵$$L -[y \log \hat{y} (1-y) \log (1-\hat{y})]$$输出层激活函数是 sigmoid记 a^{[2]} \hat{y}则输出层梯度为$$dz^{[2]} a^{[2]} - y$$这个结果极其优雅输出层的初始梯度就是“预测值减真实值”而不需要真的去对 sigmoid 求导绕一圈。这是交叉熵损失和 sigmoid 组合在一起的数学魔法。自己推一遍会更有印象先对 L 求关于 a^{[2]} 的偏导再乘上 sigmoid 导数 a^{[2]}(1 - a^{[2]})两项一约就剩 a^{[2]} - y。紧接着求 W^{[2]} 和 b^{[2]} 的梯度注意要把维度对齐写清楚$$dW^{[2]} \frac{1}{m} dz^{[2]} A^{[1]T}$$$$db^{[2]} \frac{1}{m} \sum_{i1}^{m} dz^{ 2 }$$这里的 A^{[1]T} 是隐藏层输出的转置维度上要匹配 dW^{[2]} 的 (1, n_h)。如果你在编程作业里遇到 shape 不匹配的报错基本就是这里少转置了一次。然后需要把梯度继续往上一层的传播那就要先算出隐藏层那部分的初始梯度$$dz^{[1]} W^{[2]T} dz^{[2]} \odot g^{[1]}(Z^{[1]})$$这里 \odot 是逐元素相乘g 是隐藏层激活函数的导数。整体逻辑就是输出层回传的梯度先经过 W^{[2]} 的“转置映射”回到隐藏层的线性输出上再乘上激活函数的局部导数作为这一层参数的梯度来源。到这里就形成了一个清晰循环每一层拿到“从后面传来的梯度”结合本层的激活函数导数和前层的激活输出就能求出本层的 dW、db 以及继续向前传的梯度。第三周只让大家推两层也就是让你完整体验一次这个循环第四周开始就是机械重复这个模式。4.2 为什么反向传播里要乘 W 的转置很多人初次看到 dz^{[1]} W^{[2]T} dz^{[2]} 时都会愣一下为什么是转置这里从维度推导最容易理解。z^{[1]} 是隐藏层每个神经元的线性输出维度是 (n_h, 1)。损失对 z^{[1]} 的每个分量求偏导时需要把输出层的信息“分摊”到这个 n_h 个分量上。W^{[2]} 的维度是 (n_y, n_h)转置后变成 (n_h, n_y)再乘 dz^{[2]}维度 n_y × 1结果正好是 (n_h, 1)。这个转置其实就是在做“梯度按权重反向分配”每一个隐藏层神经元从输出神经元那里接收到多少误差取决于它们之间的连接权重。我自己的理解方式是把矩阵乘法看成一次“加权求和”z^{[1]}_j 影响所有输出神经元的输入所以它的梯度是“所有输出神经元梯度乘以对应权重”的和。想通这一点后你就不会再对着转置符号发怵了。4.3 向量化实现的踩坑点第三周编程作业里最让人烦躁的就是把 for 循环去掉改成向量化的过程。这里分享几个我踩过的坑。第一求和维度要对。db 的梯度需要对所有样本求平均但 numpy 的 sum 默认会打平所有维度。计算 db^{[1]} 时要用 axis1 并 keepdimsTrue保持形状是 (n_h, 1)不然后面广播会出现隐蔽的 bug。第二逐元素乘积不要写成矩阵乘法。dz^{[1]} 算式中那个逐元素乘是很多人矩阵乘法和逐元素乘法不分导致结果全错的根源。记住一个口诀带 \odot 的是逐元素正常写的是矩阵乘法。第三前向缓存要存够信息。反向传播需要 Z^{[1]}、A^{[1]}、Z^{[2]} 这些中间结果所以前向传播里必须把每层线性输出和激活输出都存下来。课程提供的结构就是这样设计的自己写代码时不要为了省内存而跳过缓存调试时会非常痛苦。5. 随机初始化零初始化为什么不可行5.1 对称性问题第三周课程里有一个非常经典的实验把所有权重初始化为 0然后训练看看会发生什么。结果是无论怎么迭代隐藏层的所有神经元输出完全相同梯度更新也完全相同。原因在于反向传播时 dz^{[1]} 的分量依赖于 W^{[2]} 同一行的权重。如果 W^{[1]} 初始全零那么 z^{[1]} 全零A^{[1]} 全是同一个值反向传给每个隐藏层神经元的梯度也完全一样于是 W^{[1]} 的每一行同步更新。神经网络退化成只有一个“有效神经元”的模型隐藏层再宽也没意义。生活类比如果一群学生起点完全相同、接受完全相同的反馈、每步动作一致那他们永远没法发展出不同的解题策略最后学出来的结果是同一个人的复制品。随机初始化的作用就是打破这种对称性让每个神经元从不同的起点开始有机会发展出不同的功能。5.2 初始化值的尺度怎么选随机初始化也不是随便乱来。吴恩达提醒过权重初始化过大z 的绝对值会很大激活函数容易进入饱和区梯度就变小训练变慢甚至停滞。初始化过小又可能让信号在层层传播过程中被过度缩放。第三周给出的常规做法是W 用 0.01 乘以标准正态分布随机数b 直接初始化为 0 即可。核心逻辑是让 z 的初始分布不要太大保持在激活函数导数较大的区间附近。我也补充一下我对第三周课程笔记的理解这个 0.01 是针对浅层网络的经验值。到第四周深层网络还会引入更精细的 Xavier 或 He 初始化其缩放因子与层宽度相关。因为网络越深累积的缩放效应越严重浅层的时候可以先不焦虑这个问题。实操心得初始化直接影响你是否能“看到”损失下降。如果训练一开始 loss 完全不动先检查权重是不是初始化为 0 了如果 loss 一直是 NaN检查是不是初始化值太大导致梯度爆炸。6. 编程作业里最容易让人抓狂的几个问题6.1 shape 不匹配第三周作业报错里出现频率最高的就是 shape 不匹配。核心原因基本就是矩阵维度约定没想清楚。建议把各层参数的维度写在一张纸上再开始写代码X: (n_x, m)W^{[1]}: (n_h, n_x)b^{[1]}: (n_h, 1)Z^{[1]}: (n_h, m)W^{[2]}: (n_y, n_h)b^{[2]}: (n_y, 1)A^{[2]}: (n_y, m)写代码前先用 assert 检查一遍 shape比如 assert(W1.shape (n_h, n_x))这个小习惯能省下大量调试时间。6.2 损失函数震荡不下降如果你看到损失曲线像锯齿一样反复跳先检查学习率是否过大。第三周的作业里数据集不大正常情况下损失应该平滑下降。还有个隐蔽问题在计算 dz^{[2]} 时如果你手写 sigmoid 导数很容易出错建议直接使用 a^{[2]} - y。这也是为什么吴恩达在课程里特意推导这个简化形式目的就是帮你避开繁琐的中间项。6.3 训练精度高但测试精度低第三周课程给的数据集比较小训练精度很高而测试精度低往往是过拟合的前兆。浅层网络参数数量不多但参数多了之后同样会过拟合。这里先不用急着上正则化可以先观察测试集精度有没有持续恶化后面课程会专门讲如何处理。第三周主要目标是把整个训练链路跑通精度不理想先别慌找到原因比追一波高精度更重要。7. 搭建一个二分类浅层网络的最小完整实现课程用 Octave 风格讲解但我在本地复现时用的是 Python NumPy完整手写了前向传播、反向传播和梯度下降。这里给出一份可以直接跑通的最小实现骨架方便你对照课程公式检查理解。import numpy as np def initialize_parameters(n_x, n_h, n_y): 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)) return {W1: W1, b1: b1, W2: W2, b2: b2} def forward_propagation(X, params): W1, b1, W2, b2 params[W1], params[b1], params[W2], params[b2] Z1 np.dot(W1, X) b1 A1 np.tanh(Z1) Z2 np.dot(W2, A1) b2 A2 1 / (1 np.exp(-Z2)) cache {Z1: Z1, A1: A1, Z2: Z2, A2: A2} return A2, cache def compute_cost(A2, Y): m Y.shape[1] return -np.sum(Y * np.log(A2) (1 - Y) * np.log(1 - A2)) / m def backward_propagation(X, Y, params, cache): m X.shape[1] W2 params[W2] A1, A2 cache[A1], cache[A2] Z1 cache[Z1] dz2 A2 - Y dW2 np.dot(dz2, A1.T) / m db2 np.sum(dz2, axis1, keepdimsTrue) / m dz1 np.dot(W2.T, dz2) * (1 - A1**2) # tanh 的导数 dW1 np.dot(dz1, X.T) / m db1 np.sum(dz1, axis1, keepdimsTrue) / m grads {dW1: dW1, db1: db1, dW2: dW2, db2: db2} return grads def update_params(params, grads, learning_rate1.2): params[W1] - learning_rate * grads[dW1] params[b1] - learning_rate * grads[db1] params[W2] - learning_rate * grads[dW2] params[b2] - learning_rate * grads[db2] return paramstanh 的导数写成 1 - A1**2就是利用前向传播缓存好的 A1不用再保存 Z1 之外的额外数组非常省事。这就是为什么前向传播要缓存激活值反向传播可以直接复用。训练循环也很简单核心就是三行前向传播、算梯度、更新参数。建议每 1000 次迭代打印一次损失观察是否在稳定下降。8. 第三周学完后你应该能回答这些问题第三周课程结束后可以先做一次自查。如果能不看笔记把下面几题说清楚那说明这一周是真的学到位了。为什么两层网络比单层逻辑回归表达能力强因为非线性激活函数让复合函数不再是线性的。隐藏层选什么激活函数首选 ReLUtanh 也可以。输出层做二分类用 sigmoid。反向传播的输出层初始梯度为什么是 a-y因为交叉熵损失和 sigmoid 导数的约分结果。为什么权重不能全零初始化对称性会导致所有神经元同步更新网络退化。梯度下降里学习率太大或太小分别会怎样太大容易震荡或发散太小收敛极慢。向量化时 torch 和 numpy 的维度检查需要注意什么每步都算清楚行数和列数起步时写 assert 帮助快速定位。这些问题虽然基础却是后续第四周深层网络、调参、优化算法的地基。很多人急着往后赶进度忽略了第三周的“为什么”到了 Batch Norm、残差网络里遇到“玄学”问题回头才发现很多困惑的根源在第三周就埋下了。9. 我的几条实战建议课程作业能跑通只是第一步真正内化还需要自己动手改几个变量观察结果变化。我在学完第三周后做了几个小实验收获比单纯做作业大得多。第一个实验是把隐藏层神经元个数从 3 改成 10你会发现决策边界立刻精细了很多同时也开始出现一点过拟合的趋势。这能帮助你直观理解模型容量这个概念。第二个实验是故意把权重初始化放大到 1.0你会发现训练速度肉眼可见地变慢损失下降变得困难直观感受一下饱和区梯度消失的威力。以后再看到初始化的问题就不会觉得这是玄学了。第三个实验是换成只有输入层和输出层的结构也就是去掉隐藏层你会发现分类效果明显变差这刚好印证了非线性隐藏层是神经网络能力的关键来源。第三个实验如果能做出来你对第三周内容的理解就已经超出绝大多数只看视频的初学者了。很多人学深度学习就是一路追视频、跑代码但缺少“故意弄坏再修好”的过程。那些此时慢下来的人反而在后来的课程里走得最快。