简介:这份资料是吴恩达深度学习课程的课后作业答案与总结合集,面向正在系统学习深度学习、希望巩固理论并提升编程实践能力的学生与从业者。内容覆盖神经网络与深度学习基础、深层网络优化与超参数调试、结构化机器学习项目、卷积神经网络、序列模型等模块,涉及前向传播、反向传播、梯度下降、激活函数、损失函数、优化算法以及CNN、RNN、LSTM、注意力机制等核心知识点。压缩包共70个文件,以55个ipynb交互式笔记本为主,辅以7个py脚本、6张png图示和2个htm页面,整体约32.82MB,便于按周次与主题对照练习。目前已有1008人学习下载。通过研读答案与总结,读者可理清各周作业的解题思路,掌握模型搭建、参数调整与性能评估流程,并将图像识别、自然语言处理等案例迁移到自身项目中。
1. 深度学习课后作业的知识地图:从反向传播到序列模型
很多人学深度学习时会有一种错觉:视频看懂了,公式推了一遍,但一到自己写代码就卡壳。这门课的课后作业恰好卡在这个痛点上——它不考你背没背下激活函数的导数,而是逼你从零实现一遍前向传播和反向传播,亲手调通一个能跑起来的网络。梳理下来会发现,整个作业体系其实是一条很清晰的主线:先用 NumPy 手写底层算子,理解梯度是怎么流动的;再过渡到框架,把注意力放到模型结构和调参上;最后进入序列建模,处理文本和时间序列这类变长输入。适合谁?适合已经看完视频、但代码能力跟不上理论的人,也适合考前想快速把知识点串成线的人。这份答案与总结的价值不在于给你一个可以抄的结果,而在于它把每个模块的“为什么这么写”和“容易在哪翻车”都摊开了。
2. 参数初始化与正则化:作业里最容易翻车的两个细节
2.1 初始化不是玄学,是方差控制
作业里第一次让你手动实现初始化时,很多人会直接写np.random.randn() * 0.01,然后发现网络要么不收敛,要么梯度爆炸。这里的关键在于:初始化的方差要和网络深度、激活函数匹配。作业中涉及的几种方式,梳理下来可以归为三类:
| 初始化方式 | 适用场景 | 核心公式 | 常见问题 |
|---|---|---|---|
| 零初始化 | 仅用于偏置项 | 全部置零 | 权重全零会导致对称性无法打破 |
| 随机初始化 | 浅层网络 | 乘以小常数 | 常数选不好,深层网络直接失效 |
| He 初始化 | ReLU 激活 | 方差为 2/n | 作业中深层网络的标准答案 |
| Xavier 初始化 | tanh/sigmoid | 方差为 1/n | 对 ReLU 效果不如 He |
作业里有一个细节容易被忽略:当你把初始化乘数从 0.01 改成 1 时,成本曲线会从一条平线变成剧烈震荡。这不是代码写错了,而是方差过大导致激活值饱和。补的方法很简单——在训练前打印每一层激活值的均值和方差,如果某一层方差突然掉到 1e-8 量级,说明初始化或激活函数选错了。
2.2 正则化:L2 和 Dropout 的代码差异
L2 正则化在作业里是直接加在成本函数上的,代码大概长这样:
# L2 正则化成本计算 def compute_cost_with_regularization(A3, Y, parameters, lambd): m = Y.shape[1] cross_entropy_cost = compute_cost(A3, Y) # 累加所有层的权重平方和 L2_cost = 0 for l in range(1, L): L2_cost += np.sum(np.square(parameters["W" + str(l)])) L2_cost = (lambd / (2 * m)) * L2_cost return cross_entropy_cost + L2_cost这里的lambd是超参数,作业里通常让你试 0.1、0.3、0.7 几个值。注意分母是2*m,不是m,这是为了求导后系数干净。反向传播时对应的梯度项是(lambd/m) * W,别漏乘。
Dropout 的代码更绕一些,因为训练和测试的行为不一样:
# 训练时应用 Dropout def forward_propagation_with_dropout(X, parameters, keep_prob=0.5): # 生成随机掩码,小于 keep_prob 的保留 D1 = np.random.rand(A1.shape[0], A1.shape[1]) < keep_prob A1 = A1 * D1 A1 = A1 / keep_prob # 缩放,保证期望不变 ...最容易错的地方是测试阶段忘了关掉 Dropout,或者训练时忘了除以keep_prob。作业里有一个对比实验:不加缩放时,训练集准确率很高但测试集崩得厉害,加上缩放后两者才接近。这个细节在考试里经常以“为什么 Dropout 要除以 keep_prob”的形式出现。
提示:如果你在作业里看到成本曲线在训练后期反而上升,先检查正则化系数是不是太大了,而不是急着调学习率。
3. 优化算法:从批量梯度下降到 Adam 的复习重点
3.1 三种梯度下降的代码骨架
作业里要求你分别实现批量梯度下降、随机梯度下降和小批量梯度下降。三者的区别不在模型结构,而在每次更新用多少样本。批量梯度下降每次用全部 m 个样本,代码最直观但速度慢;随机梯度下降每次用 1 个样本,速度快但震荡严重;小批量在两者之间取平衡,作业里通常设 batch size 为 64 或 128。
# 小批量梯度下降的一轮迭代 for i in range(num_batches): # 从打乱后的数据中取一个 batch minibatch_X = shuffled_X[:, i*batch_size:(i+1)*batch_size] minibatch_Y = shuffled_Y[:, i*batch_size:(i+1)*batch_size] # 前向传播、计算成本、反向传播、更新参数 ...这里有一个血泪经验:作业里如果忘了在每轮 epoch 之前打乱数据,小批量梯度下降会退化成一种奇怪的顺序更新,成本曲线会出现周期性抖动。补的方法是在每个 epoch 开始时调用np.random.permutation重新排列索引。
3.2 Momentum、RMSProp 与 Adam 的参数对比
作业里让你手动实现这三种优化器,目的是让你看清它们到底在改什么。梳理下来,核心差异在更新公式:
| 优化器 | 更新方向 | 关键超参数 | 作业中的典型值 |
|---|---|---|---|
| Momentum | 累积历史梯度 | beta | 0.9 |
| RMSProp | 除以梯度平方的指数平均 | beta | 0.999 |
| Adam | 两者结合 + 偏差修正 | beta1, beta2 | 0.9, 0.999 |
Adam 的代码里有一个容易漏掉的步骤:偏差修正。因为初始时 v 和 s 都是零,前几次更新会偏向零,所以需要除以(1 - beta^t)。作业里如果漏掉这一步,前几个迭代的成本下降会明显偏慢。补的方法是在循环里维护一个t计数器,每次迭代加一。
注意:Adam 虽然默认好用,但在某些小数据集上,不加正则化的 Adam 会比带动量的 SGD 更容易过拟合。作业里有一个对比实验,你可以自己跑一遍看成本曲线的分叉点。
4. 序列模型:RNN、LSTM 与词嵌入的易漏点
4.1 RNN 的前向传播与梯度裁剪
序列模型的作业通常从最基础的 RNN 单元开始。一个 RNN 单元的前向传播包含两步:计算隐藏状态和输出。代码骨架如下:
# 单个时间步的 RNN 前向传播 def rnn_cell_forward(xt, a_prev, parameters): Wax = parameters["Wax"] Waa = parameters["Waa"] Wya = parameters["Wya"] ba = parameters["ba"] by = parameters["by"] # 计算新的隐藏状态 a_next = np.tanh(np.dot(Waa, a_prev) + np.dot(Wax, xt) + ba) # 计算当前步的输出 yt_pred = softmax(np.dot(Wya, a_next) + by) return a_next, yt_pred这里最容易忽略的是梯度裁剪。RNN 的反向传播沿时间步展开,梯度会指数级增长或消失。作业里通常要求你实现一个clip函数,把梯度限制在[-5, 5]之间。如果不做裁剪,训练到几十个时间步后,成本会突然变成 NaN。补的方法是在反向传播后、更新参数前,对每个梯度调用np.clip(grad, -maxValue, maxValue)。
4.2 LSTM 的门控机制与作业实现细节
LSTM 比 RNN 多了三个门:遗忘门、更新门、输出门。作业里会让你手动实现一个 LSTM 单元,代码量比 RNN 大不少。梳理下来,关键点在于门之间的激活函数选择:
| 门 | 激活函数 | 作用 | 作业中的常见错误 |
|---|---|---|---|
| 遗忘门 | sigmoid | 决定丢弃多少旧记忆 | 忘记加偏置项 |
| 更新门 | sigmoid | 决定写入多少新信息 | 和候选记忆的 tanh 搞混 |
| 输出门 | sigmoid | 决定输出多少记忆 | 忘记乘 tanh(记忆) |
作业里有一个细节:候选记忆用的激活函数是 tanh,不是 sigmoid。如果你写错了,训练时损失会下降得很慢。补的方法是对照公式表逐项检查,尤其是c_next = ft * c_prev + it * c_candidate这一行。
4.3 词嵌入与余弦相似度
词嵌入部分的作业通常让你加载预训练的词向量,然后计算相似度。核心代码是余弦相似度:
# 计算两个词向量的余弦相似度 def cosine_similarity(u, v): dot = np.dot(u, v) norm_u = np.sqrt(np.sum(u**2)) norm_v = np.sqrt(np.sum(v**2)) return dot / (norm_u * norm_v)这里容易漏的是归一化。如果直接算点积,长向量的相似度会虚高。作业里会让你找“国王 - 男人 + 女人”最接近的词,如果不做归一化,结果会偏向高频词。补的方法是在计算前先对每个向量做 L2 归一化。
提示:词嵌入作业里经常出现“某个词不在词表里”的情况,作业代码通常用零向量代替。如果你发现相似度全是零,先检查词表加载路径和大小写。
5. 查漏补缺:一份可以照着走的复习清单
复习到这个阶段,最怕的是“感觉都会,一写就错”。下面这份清单是我自己反复踩坑后整理出来的,按模块列出最容易翻车的点和对应的检查方法。
| 模块 | 易漏点 | 检查方法 |
|---|---|---|
| 初始化 | He 初始化方差公式记反 | 打印每层激活值方差,应在 1 附近 |
| 正则化 | Dropout 测试阶段没关 | 测试时设 keep_prob=1 |
| 优化器 | Adam 漏掉偏差修正 | 前 5 次迭代成本下降应明显 |
| RNN | 梯度裁剪阈值设太大 | 阈值超过 10 基本等于没裁 |
| LSTM | 候选记忆用 sigmoid | 对照公式,tanh 才对 |
| 词嵌入 | 余弦相似度没归一化 | 先算范数再算点积 |
除了表格里的点,还有三个习惯能帮你省下大量调试时间。第一,每次写完前向传播,先用一个极小数据集(比如 2 个样本)跑一遍,确认输出形状和预期一致。第二,反向传播写完后,用数值梯度检验(grad_check)验证一遍,作业里通常提供了这个函数,别跳过。第三,训练前先打印所有可训练参数的形状和初始化后的均值方差,如果某一层方差是 0 或者 NaN,后面不用跑了。
# 数值梯度检验的调用示例 def gradient_check_n(parameters, gradients, X, Y, epsilon=1e-7): # 把参数展平成一维向量 parameters_values = dictionary_to_vector(parameters) grad = gradients_to_vector(gradients) num_parameters = parameters_values.shape[0] J_plus = np.zeros((num_parameters, 1)) J_minus = np.zeros((num_parameters, 1)) gradapprox = np.zeros((num_parameters, 1)) for i in range(num_parameters): # 对每个参数加/减 epsilon,计算成本 thetaplus = np.copy(parameters_values) thetaplus[i][0] += epsilon J_plus[i] = forward_propagation_n(X, Y, vector_to_dictionary(thetaplus)) # 同理计算 J_minus ... # 比较数值梯度和反向传播梯度 numerator = np.linalg.norm(grad - gradapprox) denominator = np.linalg.norm(grad) + np.linalg.norm(gradapprox) difference = numerator / denominator return difference < 1e-7这个检验跑一次大概几十秒,但能帮你省下几个小时的瞎调参。从那以后我每次写完反向传播都强制走一遍梯度检验,哪怕作业里没要求。希望这份梳理能帮你在复习时少走一点弯路。
本文还有配套的精品资源,点击获取