简介:面向正在学习吴恩达《深度学习》课程第一部分“神经网络与深度学习”的 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 numpy==1.16.5 scipy==1.2.1 matplotlib==3.1.3 jupyter h5py==2.10.0 jupyter notebook这里有两个参数值得说明。h5py==2.10.0是读取数据集datasets/train_catvnoncat.h5用的,新版 h5py 在文件读取上没问题,但旧代码里有些调用方式会触发警告,与其改代码不如直接锁版本。scipy==1.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, axis=1, keepdims=True) 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沿 axis=1 对 m 个样本求和,keepdims=True保持列向量形状。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 和三个数值,和测试用例的期望值对一遍。从那以后我每次跑这类课程作业都强制走一遍这个流程,虽然多花两分钟,但省下的调试时间往往是以小时计的。希望帮到你。
本文还有配套的精品资源,点击获取