吴恩达深度学习作业实战:numpy实现反向传播与神经网络训练 简介面向正在学习吴恩达《深度学习》课程第一部分“神经网络与深度学习”的 Coursera 学员这份资源整理了四周的课后测验与编程作业覆盖逻辑回归、浅层神经网络、深层神经网络与向量化实现并包含选择题答案解析方便按周对照学习。压缩包共 70 个文件大小约 18.64MB以 ipynb 编程作业、py 辅助脚本、pdf 题目解析为主同时含 png、jpg、gif 网络结构图、训练好的 h5 模型文件便于查看运行结果、损失变化和模型保存方式。作业目录按周划分Quiz 和 Programming Assignments 分开收纳可直接运行复现也可快速核对答案。每份代码附有推导笔记或运行截图对理清反向传播、参数更新、梯度下降等关键细节很有帮助。目前已有 2280 人学习下载适合正在上 Coursera 课程、需要完成作业或想动手巩固深度学习的初学者。1. 为什么这套作业比视频本身更值钱吴恩达的深度学习专项课程大家并不陌生但真正拉开差距的从来不是视频讲得有多清楚而是你能不能把课后作业完整跑通。很多人刷完视频觉得懂了一打开编程作业才发现公式里的每一个下标、每一次维度对齐都要自己负责。这套课后作业quiz 加编程作业的价值就在于把看懂的逼成跑通的视频里三分钟讲完的反向传播作业里要用 numpy 一行行写出来并让梯度真正把 loss 降下去。它适合所有想确认自己不是只看了个热闹的人——不管你是刚入门的学生还是工作几年想补基础底子的工程师照着这份作业完整做一遍比多看两遍视频有效得多。2. 作业体系全景四份编程作业的关系与文件结构2.1 第一门课的四份作业到底在练什么这四份作业是层层递进的关系不是并列的四个零散任务。第一份作业是 Python 基础与 numpy 操作解决的问题是用向量化代替 for 循环第二份作业用逻辑回归做猫图二分类建立完整的训练闭环——初始化、前向传播、计算 loss、反向传播、更新参数、预测第三份作业引入单隐层神经网络处理一个非线性的平面数据分类第四份作业把网络扩展到 L 层从零搭建一个通用的深度网络。四份作业连起来恰好覆盖了第一门课的全部知识点。从实际训练效果来看前两份作业是找手感第三份开始进入理解反向传播的阶段第四份则是把前向和反向缓存、维度匹配这类工程细节串起来。多数人卡在第三份和第四份作业原因不是公式看不懂而是代码里的变量名、形状注释和推导过程中的符号对不上。2.2 每个作业文件夹里都有什么每个编程作业都是一个独立的文件夹里面有.ipynb的 Jupyter notebook 文件以及若干辅助.py文件。以第四份作业为例文件夹里会有dnn_utils.py提供 sigmoid、relu 及其导数的实现、testCases.py提供每个函数的测试输入和期望输出、lr_utils.py加载训练数据用和datasets数据目录。理解这些辅助文件的作用很重要因为作业主体要你填写的只有 notebook 里标注# GRADED FUNCTION的代码块其余代码只是用来验证你的实现是否正确。常见的误区是把testCases.py当成标准答案去看实际上它只提供测试用例不包含实现代码。真正要自己写的是反向传播、参数更新这几个核心函数。2.3 环境准备版本选择与启动流程这套作业是早年发布的对库的版本有隐含约束。最稳妥的组合是 Python 3.6 到 3.8、numpy 1.16.x、scipy 1.2.x、matplotlib 3.1.x。新版 numpy 对某些旧 API 有兼容性调整虽然大多数能跑但会出现np.float被移除之类的提示白白浪费时间排查。python -m venv dl_homework source dl_homework/bin/activate pip install numpy1.16.5 scipy1.2.1 matplotlib3.1.3 jupyter h5py2.10.0 jupyter notebook这里有两个参数值得说明。h5py2.10.0是读取数据集datasets/train_catvnoncat.h5用的新版 h5py 在文件读取上没问题但旧代码里有些调用方式会触发警告与其改代码不如直接锁版本。scipy1.2.1是为了兼容lr_utils.py里的load_dataset函数这个函数用到了 scipy 的ndimage.imread接口在更高版本中被移除了如果装了新版 scipy 就会在这里报AttributeError。我自己第一次跑的时候就是被这个报错卡住的所以建议严格按这个版本组合装。启动之后在浏览器里打开 notebook按顺序执行单元格即可。每个作业的 notebook 开头都会有一段import代码如果某个库导入失败优先检查版本而不是去改代码。3. 逻辑回归作业从公式到可运行代码的完整闭环3.1 初始化函数的写法与形状陷阱第二份作业里第一个要填的是initialize_with_zeros。这个函数要做的事情很简单把权重向量 w 初始化为零向量把偏置 b 初始化为 0。难点在于 w 的形状必须和输入特征数匹配而输入特征数由train_set_x_orig决定。def initialize_with_zeros(dim): w np.zeros((dim, 1)) b 0.0 return w, b这里最容易被扣分的点是w的形状。很多人在这一步写成np.zeros(dim)得到的是一个一维数组后面和 X 做矩阵乘法时会触发广播问题结果不会报错但整个梯度计算都是错的。(dim, 1)是列向量这是后续所有矩阵运算能对齐的基础。养成习惯每个数组写完之后先用print(w.shape)验证一下形状比依赖脑子记忆可靠得多。3.2 propagate 函数前向传播与反向传播的公式落地propagate函数是整个逻辑回归作业的核心。它要同时完成前向传播计算 loss以及反向传播计算梯度。公式层面很简单但代码里要注意矩阵乘法的轴。def propagate(w, b, X, Y): m X.shape[1] # 前向传播 A 1 / (1 np.exp(-(np.dot(w.T, X) b))) cost (-1 / m) * np.sum(Y * np.log(A) (1 - Y) * np.log(1 - A)) # 反向传播 dw (1 / m) * np.dot(X, (A - Y).T) db (1 / m) * np.sum(A - Y) grads {dw: dw, db: db} cost np.squeeze(cost) return grads, cost说明一下这里的细节。A的计算对应sigmoid(w^T X b)np.dot(w.T, X)的结果形状是(1, m)加上 b 后广播到每个样本。cost 的计算用了对数损失np.squeeze是为了把 shape 从(1,1)降成标量避免后续操作出现奇怪的广播副作用。反向传播里的dw用的公式是(1/m) * X * (A-Y)^T注意(A-Y)的转置写在np.dot的第二个参数位置这样相乘的维才是(n_features, m) * (m, 1)得到(n_features, 1)正好和 w 的形状一致。这里不需要任何深度学习框架numpy 的矩阵乘法规则就是全部工具。3.3 优化循环与预测函数的边界情况optimize函数把 propagate 包进一个循环迭代num_iterations次。这里有一个容易被忽略的点每一次迭代都要用当前 w、b 计算梯度然后w w - learning_rate * dw顺序不能反。如果先更新 w 再用更新后的 w 算梯度梯度就污染了。预测函数predict的输入是训练好或测试好的数据和参数输出 0 或 1 的预测标签。实际执行时A sigmoid(w^T X b)之后要用A 0.5做阈值判断注意这里的A是概率值不是类别。边界情况是当X只有一列时np.dot(w.T, X)的结果形状会是(1,)而不是(1,1)这时 b的广播行为完全不同所以保持 X 的形状为(特征数, 样本数)是硬性要求。4. 深层网络作业从两层到 L 层的通用实现4.1 初始化He 初始化为什么是默认选项第三份作业用随机初始化第四份作业在前向传播测试时就要求用 He 初始化。原因和激活函数有关当层数加深之后如果权重方差过大信号在传播过程中会爆炸如果过小信号会消失。ReLU 激活下 He 初始化的方差设置为2/n^(l-1)其中n^(l-1)是上一层神经元数量。def initialize_parameters_deep(layer_dims): np.random.seed(3) parameters {} L len(layer_dims) for l in range(1, L): parameters[W str(l)] np.random.randn(layer_dims[l-1], layer_dims[l]) * np.sqrt(2.0 / layer_dims[l-1]) parameters[b str(l)] np.zeros((layer_dims[l], 1)) assert(parameters[W str(l)].shape (layer_dims[l-1], layer_dims[l])) return parameters注意这里W的形状是(上一层节点数, 当前层节点数)和逻辑回归里的(dim, 1)一样的思路。b永远是列向量。最后一个assert是我自己习惯加的一旦后面反向传播出现维度不匹配先检查这里。4.2 前向传播缓存机制的工程价值深层网络的 forward 需要分别写linear_forward和linear_activation_forward。前者计算Z W A_prev b后者先算Z再通过激活函数得到A。其中有一个看起来很啰嗦但极其重要的细节要把A_prev、W、b存进cache字典反向传播时要用。def linear_activation_forward(A_prev, W, b, activation): Z, linear_cache linear_forward(A_prev, W, b) if activation sigmoid: A, activation_cache sigmoid(Z) elif activation relu: A, activation_cache relu(Z) cache (linear_cache, activation_cache) return A, cache为什么要把A_prev存下来因为反向传播计算dA_prev W.T * dZ时需要用到当前层的W和上一层的输出A_prev。如果不缓存前向传播就白跑了只能重算一遍。这个缓存机制就是后来所有深度学习框架里forward和backward接口的原型。写作业的时候把cache完整返回后面调试梯度时能少踩一半的坑。4.3 反向传播dW、db 和 dA_prev 的形状推导反向传播的代码是整份作业里最容易写崩的部分因为每一层的梯度形状都要和参数形状严格对齐。以linear_backward为例def linear_backward(dZ, cache): A_prev, W, b cache m A_prev.shape[1] dW (1 / m) * np.dot(dZ, A_prev.T) db (1 / m) * np.sum(dZ, axis1, keepdimsTrue) dA_prev np.dot(W.T, dZ) return dA_prev, dW, db这里三个梯度分别对应三层信息。dW的形状必须和W一样np.dot(dZ, A_prev.T)的维度是(当前层节点数, m) * (m, 上一层节点数)乘出来正好是(当前层节点数, 上一层节点数)和 W 形状一致。db用np.sum沿 axis1 对 m 个样本求和keepdimsTrue保持列向量形状。dA_prev用np.dot(W.T, dZ)形状是(上一层节点数, 当前层节点数) * (当前层节点数, m)得到(上一层节点数, m)。写这个函数时我自己的经验是不要凭记忆写把每行代码涉及的矩阵形状写在注释里然后逐步推进。shape 对不上时把dZ.shape、A_prev.shape、W.shape都 print 出来看一遍比盯着公式看半小时有效得多。5. 避坑与常见问题五个最容易翻车的地方5.1 维度不匹配导致的静默错误现象代码运行不报错但是训练 loss 完全不下降或者准确率始终在 50% 左右徘徊。原因最常见的场景是把某个矩阵的形状写错比如w初始化为(dim,)而不是(dim, 1)或者dW的计算中矩阵乘法顺序错了导致梯度方向不对。numpy 的广播机制会帮你把错误形状强行计算出来不报错但结果完全是错的。解决在每个关键函数写完的print(xxx.shape)走一遍。重点检查w、dw、dW、db这几个数组的形状是否和参数一致。5.2 梯度爆炸或消失loss 变成 NaN现象训练到某个 iteration 后loss 突然变成nan或者在前几个 iteration 就直接nan。原因学习率过大或者权重初始化方差过大激活值在深层网络中指数级放大。作业里给了learning_rate的设置建议但很多人不听擅自调大后就翻车了。解决把学习率调回作业默认值通常是 0.001 到 0.01 之间如果仍然nan检查初始化是否用了 He 初始化而不是np.random.randn直接乘一个常数。另外np.exp在正向传播里如果输入值过大会溢出得到inf这也会传导到 loss 里。5.3 梯度检查不过数值梯度与解析梯度的差异现象第四份作业里如果做了梯度检查会发现某个参数的数值梯度和解析梯度差异超过阈值。原因反向传播公式里多了个符号错误比如dW和dZ的顺序反了或者db没有除以m。数值梯度是从定义出发算的不会受这些错误影响所以两边差异会非常明显。解决把反向传播的公式再对一遍特别注意(1/m)是否加在了每一项上以及np.dot两个参数的顺序。经验是先在纸上推导一遍再写代码不要边看公式边抄代码容易把符号抄错。5.4 数据集路径问题读取 h5 文件时报错现象lr_utils.load_dataset()报FileNotFoundError或者找不到train_catvnoncat.h5。原因notebook 的工作目录和datasets文件夹不在同一个层级。用 Jupyter 打开文件时默认工作目录是 notebook 所在目录但如果通过jupyter notebook --notebook-dir某个路径启动工作目录会变化。解决启动时直接用cd到作业文件夹再启动jupyter notebook不要用系统的文件管理器直接双击.ipynb文件。5.5 本地全过、提交不过grader 的隐藏检查点现象本地 notebook 运行结果看起来是对的但提交到课程系统后部分测试点不过。原因grader 对函数的输入输出有严格的形状要求甚至包括返回值的类型。比如cost如果是numpy.ndarray而不是标量在某些测试点会报错。解决严格按照作业说明的注释来写返回值尤其是np.squeeze(cost)这类把(1,1)数组变成标量的步骤不要省。另外确保每个函数内的注释不要被删grader 只检测# GRADED FUNCTION下方的代码是否正确。6. 验证作业结果的两个硬核技巧作业跑完不代表你真的理解了还要验证。第一个技巧是用作业自带的testCases.py的期望输出来对齐自己的实现。具体做法是先导入testCases模块调用linear_forward_test_case()拿到输入然后跑自己写的linear_forward把输出和期待输出对比差异小于 1e-8 就说明实现正确。这个方法比靠 loss 曲线判断更可靠因为 loss 低可能是巧合但中间函数的输出和教材完全一致的概率不高。第二个技巧是观察可视化结果而不是只看准确率。第二份作业和第三份作业最后都会画出决策边界或训练曲线这个图能告诉你模型是不是发生了过拟合。比如第三份作业用不同隐藏层节点数训练节点数太少时决策边界是线性的节点数太多时会出现明显的凹陷边界这时候能直观理解超参数的作用。我自己一直到第四份作业写完才养成了一个习惯每次写完一个函数直接打印输入输出的 shape 和三个数值和测试用例的期望值对一遍。从那以后我每次跑这类课程作业都强制走一遍这个流程虽然多花两分钟但省下的调试时间往往是以小时计的。希望帮到你。本文还有配套的精品资源点击获取