
简介面向系统学习深度学习、希望借助课后编程作业巩固理论的学习者这份资料围绕吴恩达课程各周作业提供答案与总结适合需要对照思路、查漏补缺的初学者和进阶者。包内共70个文件以55个ipynb交互式笔记本为主配合7个py脚本、6张png图示和2个htm页面便于边运行代码边理解模型搭建、训练与测试流程压缩包约32.82MB。内容覆盖神经网络基础、深层神经网络、超参数调试与正则化、优化算法、卷积神经网络、目标检测、循环序列模型、词嵌入及注意力机制等模块并对前向传播、反向传播、梯度下降、损失函数与优化器等核心知识点作出归纳。已有1009人学习可作为课程作业复盘、实验参数调整和模型评估的参考也能帮助读者把图像识别、语音识别等应用场景中的问题拆解到具体实现步骤。1. 知识地图一份深度学习课后作业的答案其实有四层结构很多人拿到一份深度学习的课后作业答案与配套素材第一反应是对着抄一遍、跑通就关掉页面。这套作业真正的价值不在答案本身而在于它把整个方向拆成了四条互相衔接的主线向量化与矩阵运算打底前向反向推导撑起网络初始化、正则化、优化器把网络训得动卷积与序列结构把网络用到图像和时间序列上。答案只是每条主线的验收标准拿它当模板之前先弄清自己站在哪一层。基础薄弱的人适合顺着做卡在调参阶段的人可以从第三层往回检查。2. 基础层向量化、损失函数与反向传播的推导重点这一层是整套作业里最容易被轻视的部分。很多人觉得逻辑回归太简单直接跳到深层网络结果在后面所有编程题里反复犯同一个错误矩阵形状对不上或者对上了但语义是错的。基础层的作业题看着朴素实际在训练三件事把循环写成矩阵、把公式写成代码、把维度写成习惯。这三件事没过关后面每一周都要付出代价。2.1 向量化把 for 循环换成矩阵之后形状就是一切作业里第一批编程题几乎都会给一个“用 for 循环逐样本计算”的版本然后要求改成向量化实现。改造的核心不是语法而是约定样本按列排一个 $n_x \times m$ 的矩阵 $X$ 表示 $m$ 个样本、每个样本 $n_x$ 维特征。这个约定一旦定下来后面所有公式的形状都能推出来不用死记。import numpy as np def propagate(w, b, X, Y): w: (n_x, 1) 权重列向量 b: 标量偏置 X: (n_x, m) 每个样本占一列 Y: (1, m) 标签同样按列排 m X.shape[1] # 广播b 是标量会自动加到 (1, m) 的每一列上 Z w.T X b # (1, n_x) (n_x, m) - (1, m) A 1 / (1 np.exp(-Z)) # sigmoid逐元素 # 交叉熵注意先乘再取均值不要先取均值 cost -(Y * np.log(A) (1 - Y) * np.log(1 - A)).mean() # 反向dZ 的形状必须和 Z 一致 dZ A - Y # (1, m) dw (X dZ.T) / m # (n_x, m) (m, 1) - (n_x, 1) db np.sum(dZ) / m # 标量 return {dw: dw, db: db}, float(cost)这段代码有三个值得盯住的细节。第一dw的形状必须和w完全一致(n_x, 1)对(n_x, 1)因为后面要用w w - lr * dw直接更新如果写成(1, n_x)numpy 的广播不会报错只会把结果悄悄加错这是最典型的看不清来源的错误。第二cost用的是.mean()而不是.sum()两者差一个 $m$ 倍梯度里也会跟着差学习率看起来“莫名其妙不对”往往就是这里。第三db是标量求和后除 $m$不要保留成(1,1)的数组否则后面累加时会引入多余的维度。提示每写完一个函数先print(w.shape, dw.shape, X.shape)三个形状对不上就先别跑训练。这个习惯能省掉大量调试时间。向量化真正带来的收益在深层网络里才显现。作业里通常会要求对比 for 循环版本和向量化版本的耗时这个对比不是走形式它解释了一个事实深度学习能跑起来靠的是把计算压进 BLAS 这类底层矩阵库而不是靠更快的解释器。理解这一点后面看到mini-batch、看到卷积的im2col就不会觉得是凭空冒出来的技巧。2.2 损失函数与梯度作业里必须自己推一遍的三个式子作业的推导题往往被跳过因为答案就摆在那里。但反向传播的调试能力几乎完全来自手推过一遍。梳理下来有三个式子是绕不开的。式子作用容易写错的地方逻辑回归的 $dZ A - Y$单层输出层的梯度起点把它当成通用公式套到隐藏层交叉熵对 $Z$ 的导数与 sigmoid 求导抵消忘了这两个推导是绑在一起的第 $l$ 层 $dW^{[l]} dZ^{[l]} A^{[l-1]T}/m$参数梯度转置方向反了、漏除 $m$第一处的意义在于$dZ A - Y$ 这个极简形式是 sigmoid 配交叉熵互相抵消的结果换成别的激活函数或别的损失就不成立。很多人在深层网络里对输出层直接写dZ A - Y恰好是对的因为输出层通常就是这套组合但对隐藏层也这么写就错了隐藏层要走链式法则dZ[l] W[l1].T dZ[l1] * g(Z[l])。第二处的关键在于求导顺序。作业里一般要求先算dZ再由dZ推dW、db、dA_prev这个顺序保证了每一步只依赖已知量不会出现“要算 A 得先知道 B”的死循环。手推一遍之后看到代码里的四行反向传播脑子里能自动补出对应的公式而不是把它当成一段黑匣子。第三处是维度校验的落点。dW[l]的形状必须等于W[l]db[l]必须等于b[l]这两个等式在每一层都成立。作业里如果某一层训练不收敛先查这两个形状比查学习率快得多。注意推导时把 $m$ 显式写在分母上不要图省事最后统一除。中间量的形状和量纲一旦模糊后面梯度检验的误差会大到看不出是公式错还是实现错。2.3 深层网络缓存、激活函数与维度检查表深层网络的作业题一般会给一个固定的骨架初始化参数、前向传播逐层缓存A_prev、W、b、Z、计算损失、反向传播、更新参数。骨架不难难在缓存变量用错。反向传播时要用到前向的中间结果如果缓存的是激活后的A而公式需要激活前的Z或者层号错位一位代码能跑梯度却是错的。下面这张检查表可以贴在旁边逐层对一遍再提交。检查项正确形态出错表现缓存键名caches[l] (A_prev, W, b, Z)键名顺序混乱导致取错层号范围前向从 1 到 L反向从 L 到 1反向写成 1 到 L梯度全错激活函数隐藏层 ReLU输出层 sigmoid输出层用 ReLU损失算不出参数字典W1..WL、b1..bL循环里拼接字符串时索引错位激活函数的选择在作业里通常有明确要求但背后的理由值得写下来ReLU 在正区间的导数恒为 1避免了 sigmoid 在深处把梯度压到接近零而输出层用 sigmoid 是因为要做二分类输出要落在 0 到 1 之间并能直接进交叉熵。这两个选择不是惯例是被损失函数和任务形式决定的。维度检查有一个便宜的做法给每一层打印A.shape正常应该是(n[l], m)其中 $m$ 是样本数全程不变。如果某一层 $m$ 变了多半是哪里用了sum而不是sum(axis1, keepdimsTrue)把样本维度压掉了。这个错误在深层网络里特别隐蔽因为 numpy 会自动补维度直到最后算损失时才报形状不匹配而那时已经离出错点很远了。3. 训练层初始化、优化器与调参顺序的作业套路基础层解决的是“算得对”训练层解决的是“训得动”。这一层的作业题往往会给出几组对比实验不同初始化、不同优化器、不同学习率然后要求画出训练损失和验证准确率曲线并解释差异。这些对比如果只是跑完截个图价值很有限真正有价值的是能从曲线形状反推训练出了什么问题。3.1 初始化全零为什么必然出问题He 初始化怎么选把所有权重初始化为零网络在数学上仍然能前向传播但所有隐藏单元会算出完全相同的结果反向传播时梯度也完全相同更新之后依然完全相同。整个隐藏层退化成一个神经元网络的表达能力被彻底浪费。这个结论在作业里通常会用一个对比实验验证全零初始化跑出来的损失曲线几乎是一条平线准确率停在随机猜测附近。随机初始化解决了对称性问题但尺度没解决。权重太小时激活值逐层衰减梯度也跟着衰减权重太大时激活值逐层放大最后溢出。作业里常见的做法是按输入维度缩放def he_init(layer_dims, seed3): layer_dims: [n_x, n_1, ..., n_L]逐层宽度 np.random.seed(seed) # 固定种子保证对比实验可复现 params {} for l in range(1, len(layer_dims)): fan_in layer_dims[l - 1] # 除以 fan_in 的平方根让每层输出的方差大致保持不变 params[fW{l}] np.random.randn(layer_dims[l], fan_in) * np.sqrt(2.0 / fan_in) params[fb{l}] np.zeros((layer_dims[l], 1)) return params这里2.0 / fan_in里的 2 来自 ReLU 只保留正半轴、方差减半的补偿作业里如果用的是 tanh系数会换成1.0 / fan_in。b 初始化为零是安全的因为对称性已经被 W 打破偏置不需要再承担这个职责。种子固定的意义在于对比初始化效果时唯一变量必须只有初始化方式否则随机性会把结论搅乱。提示如果换了初始化之后损失下降反而变慢先确认学习率是否跟着改了。初始化和学习率是耦合的权重量级变了合适的学习率区间也会平移。3.2 优化器动量、RMSProp 到 Adam 各自在修什么作业里会依次实现三种优化器代码量都不大但每一步在修什么问题值得写清楚。优化器核心动作修的问题动量梯度下降用梯度的指数加权平均更新鞍点附近来回震荡、下降方向不稳RMSProp用梯度平方的指数加权平均缩放学习率不同参数方向的尺度差异过大Adam两者结合并做偏差修正前几轮估计不准、冷启动震荡动量做的事是给梯度加惯性连续几轮方向一致的维度越走越快来回震荡的维度互相抵消。RMSProp 做的是给每个参数单独调学习率历史梯度大的参数步子收小历史梯度小的参数步子放大。Adam 把两件事拼起来再加上偏差修正因为指数加权平均在最初几轮会严重偏小不修正的话第一步几乎是原地不动。作业里常见的坑是把beta1、beta2、epsilon的顺序记反。约定是beta1管一阶矩梯度均值beta2管二阶矩梯度平方均值epsilon只用来防止除零一般取1e-8不要为了“让训练稳一点”随手调到1e-3那会把缩放效果直接抹掉。def update_with_adam(params, grads, state, t, lr0.001, beta10.9, beta20.999, eps1e-8): state 里存 v一阶矩和 s二阶矩t 是全局步数从 1 开始 for k in params: state.setdefault(k, {v: 0, s: 0}) v beta1 * state[k][v] (1 - beta1) * grads[d k] s beta2 * state[k][s] (1 - beta2) * (grads[d k] ** 2) # 偏差修正t 很小时v_hat 和 s_hat 才接近真实均值 v_hat v / (1 - beta1 ** t) s_hat s / (1 - beta2 ** t) params[k] - lr * v_hat / (np.sqrt(s_hat) eps) state[k][v], state[k][s] v, st从 1 开始还是从 0 开始会在第一步产生明显差别从 0 开始1 - beta1 ** 0等于 0直接除零。这个细节作业里不会强调但它是很多“第一次更新就变成 NaN”的来源。3.3 梯度检验与调参顺序先动哪个旋钮梯度检验是这里的“后悔药”。数值梯度和解析梯度的相对误差低于1e-7基本可以放心1e-5量级需要留意1e-3以上基本可以判定反向传播写错了。def grad_check(forward, params, X, Y, eps1e-7): forward 返回 (grads, cost)只用于小规模抽查 grads, _ forward(params, X, Y) num {k: np.zeros_like(v) for k, v in params.items()} for k, v in params.items(): for idx in np.ndindex(v.shape): old v[idx] v[idx] old eps _, c_plus forward(params, X, Y) v[idx] old - eps _, c_minus forward(params, X, Y) v[idx] old # 用完必须还原 num[k][idx] (c_plus - c_minus) / (2 * eps) # 分子分母都做归一化避免量级差异干扰判断 diff {k: np.linalg.norm(num[k] - grads[d k]) / (np.linalg.norm(num[k]) np.linalg.norm(grads[d k]) 1e-12) for k in params} return diff这段代码的两个约定比代码本身重要。一是必须关掉随机的部分dropout、数据增广否则同一个参数在c_plus和c_minus两次计算里走的是不同的网络差值没有意义。二是只在少量参数上抽查全量跑一遍的代价是前向传播次数的平方级增长作业里的网络规模撑不住。调参的顺序梳理下来应该固定成先确认梯度检验通过再调学习率然后调网络宽度和深度最后调正则化强度。理由很直接梯度错了后面所有旋钮都在错误的方向上白拧学习率是最敏感的一维它定下来之后其他参数的变化才有可比性网络容量和正则化是一对容量没定之前调正则化只是瞎猜。4. 结构层卷积网络的参数量与序列模型的梯度难题结构层是这套作业里分量最重的部分因为它开始处理真实形状的输入图像是三阶张量文本是变长序列前面那套“样本按列排”的约定要重新推一遍。这一层的作业题有个共同特点——代码能跑通不等于理解必须能手算出参数量和输出尺寸才算真的过。4.1 卷积与池化输出尺寸、参数量必须能手算卷积层的输出尺寸公式是这一层的基础$$ n_{out} \left\lfloor \frac{n_{in} 2p - f}{s} \right\rfloor 1 $$其中 $n_{in}$ 是输入边长$f$ 是卷积核大小$p$ 是填充$s$ 是步长。作业里会给几组参数让人手算然后和代码输出对比。这个练习的价值在于很多形状不匹配的错误其实在写代码之前用纸笔就能发现。参数量是另一个必须手算的量。一个卷积层的参数量是 $f \times f \times c_{in} \times c_{out} c_{out}$前一项是权重后一项是偏置。这里有个反直觉的地方卷积层的参数量与输入图像的尺寸无关。一张 $64 \times 64$ 的图和一张 $256 \times 256$ 的图只要通道数和卷积核一样这一层的参数量完全相同。这个性质是卷积相对全连接的核心优势作业里通常会要求对比两种结构的参数量算完就明白为什么图像任务离不开卷积。结构输入配置参数量卷积层$64\times64\times3$$f3, c_{out}32, p1, s1$$3\times3\times3\times3232896$同尺寸全连接$64\times64\times3$输出 32 维$12288\times3232\approx39$ 万池化层$64\times64\times32$$f2, s2$0池化层没有可学习参数它只做下采样作用是压缩空间尺寸、扩大后续层的感受野。作业里容易写错的是池化的反向传播最大池化只把梯度回传给取到最大值的位置其余位置为零平均池化则把梯度均分到窗口内每个位置。用错会让梯度检验的误差看起来像“小问题”实际是梯度语义错了。注意padding在代码里通常是四元组或分通道补手算时先确认是“四周各补 p”还是“总共补 p”两种约定算出来的尺寸差一倍。4.2 残差与 1×1 卷积深度上去之后为什么还要它们作业里会有一段把网络堆到几十层然后观察训练损失的变化。纯堆叠的深层网络会出现一个奇怪现象层数增加训练损失反而更大。这不是过拟合过拟合的表现是训练损失下降、验证损失上升这里训练损失本身就下不去说明优化难度上来了。残差连接修的就是这个。它把某一层的输入直接加到输出上让网络学习残差 $F(x) H(x) - x$ 而不是完整的 $H(x)$。这样做的好处是如果这一层什么都不学输出等于输入至少不会变差梯度也能沿着跳跃连接直接回传不必穿过一长串非线性变换。1×1 卷积则解决通道维度的压缩与扩张。它的参数量是 $c_{in} \times c_{out}$计算量小作用却很大先降维再卷积能把 3×3 卷积的计算量压下来一个量级。作业里如果给出“瓶颈结构”的配置一般是 1×1 降维、3×3 卷积、1×1 升维三步参数量的账要分开算不能笼统地乘在一起。这两样结构在作业答案里往往只是一段现成代码但它们的动机必须自己讲得出来残差是为了让深层网络可优化1×1 卷积是为了让深层网络算得起。两者一个管梯度一个管算力解决的问题完全不同。4.3 序列模型RNN 的梯度消失与注意力的动机序列部分的作业从最朴素的 RNN 开始前向传播按时间步展开每个时间步复用同一组参数。这种参数共享让模型能处理任意长度的输入但也埋下了梯度消失的根反向传播要沿时间步连乘连乘的因子是权重矩阵和激活函数导数的乘积只要这个乘积长期小于 1梯度传到十几步之前就接近零了。作业里一般会用梯度裁剪来缓解裁剪的做法是给梯度的范数设一个上限超过就按比例缩回去def clip(gradients, max_value): 逐元素裁剪防止单步更新把参数冲出合理区间 clipped {} for k, v in gradients.items(): clipped[k] np.clip(v, -max_value, max_value) return clipped裁剪解决的是梯度爆炸对梯度消失帮助有限。真正针对性的是门控结构用门来决定信息保留多少、丢弃多少让梯度有一条更通畅的回传路径。注意力机制的动机则更直接RNN 把所有历史压进一个固定维度的隐状态序列一长早期信息必然被稀释。注意力让解码的每一步都能直接看到编码器所有时间步的输出按相关性加权路径短了梯度也就没那么容易丢。作业里通常会给一个简化版的注意力计算核心就是查询与键做点积、缩放、归一化、加权求和四步参数不多但每一步的形状要盯紧注意力权重矩阵的形状是“目标长度 × 源长度”转置方向反了不会报错只会让对齐关系整体错乱。5. 易错点五处最容易出错、又最不容易报错的地方调试这部分内容时最难的从来不是报错而是不报错。下面这五处代码会安静地跑完损失也在缓慢下降但结果就是比预期差一截而且找不到原因。5.1 数学与形状类广播、维度与正则化系数易漏点一numpy 广播把形状错误藏起来。为什么不容易发现(n, 1)和(1, m)相加会广播成(n, m)不报错值也有意义只是语义悄悄变了。反向传播里这种错误会让某个梯度多出一维更新时又广播回去表现成“训练能跑但收敛慢”。怎么补在所有涉及形状的赋值后加一行断言例如assert dw.shape w.shape把错误拦在发生的那一行。易漏点二正则化项的系数与求导不一致。为什么不容易发现代价函数里加了lambda / (2m) * sum(W**2)反向里却写成lambda / m * W或者干脆漏掉 2m 的分母损失曲线依然下降只是正则强度悄悄翻倍或减半。怎么补把代价函数和梯度函数并排写逐项对照重点看分母然后用梯度检验验证正则项写错时相对误差会稳定在1e-5到1e-4这个不高不低的区间正好卡在“看起来还行”的位置。易漏点三dropout 开着做梯度检验。为什么不容易发现dropout 每轮随机丢弃单元c_plus和c_minus走的是两个不同的子网络数值梯度本身就是噪声误差看起来只是“有点大”很容易被当成精度问题放过去。怎么补梯度检验前显式把keep_prob置为 1把随机性关掉训练时再打开两套配置分开跑。5.2 工程与调参类模式切换、种子与学习率易漏点四训练模式和评估模式混用。为什么不容易发现带 dropout 或批归一化的网络训练时用随机丢弃和批统计量评估时必须关掉丢弃、改用全局统计量。如果评估时忘了切换验证准确率会明显偏低但代码不报错很容易被误读成“模型过拟合了”。怎么补把模式切换封装成函数训练循环开始和评估开始各调一次不要靠记忆验证集指标每次都在切换之后算。易漏点五随机种子只固定了一处。为什么不容易发现初始化固定了种子但数据打乱、dropout、参数初始化各用各的随机源两次运行结果仍然不同复现失败时以为是算法不稳定。怎么补在训练入口统一设一次种子并确认所有随机操作都走同一个全局随机源对比实验时把种子当成一个必须记录的超参数写进实验日志。提示这五条里前三条用三行断言和一次梯度检验就能拦住后两条靠流程约束。经验上调试时间的大部分花在“能跑但不对”上而不是报错上。6. 复习清单按模块自查的六张表与一个自检脚本复习到这一层最有效的做法不是重读代码而是拿一张清单逐项打勾。下面这张表按模块拆开每一项后面都跟一个可验证的动作不是“掌握了”这种模糊判断。模块必须会做的事自查动作向量化手写逻辑回归前向反向不看答案默写propagate跑一次梯度检验损失与梯度说出 $dZ A - Y$ 的成立条件换成交叉熵 softmax重新推一遍初始化解释全零失效的原因用全零和 He 各跑一次对比损失曲线前 100 轮优化器分清一阶矩和二阶矩故意把beta1和beta2对调观察发散正则化说清 L2 与 dropout 的区别固定其他变量只改lambda看训练与验证的间距卷积手算输出尺寸和参数量随机给三组f/p/s手算后与代码结果对照配套的自检脚本不必复杂核心是把“形状、数值、随机性”三件事变成可执行的断言def sanity_check(params, grads, forward, X, Y): # 1. 形状每个梯度必须和对应参数同形 for k in params: assert grads[d k].shape params[k].shape, f{k} 形状不匹配 # 2. 数值损失必须是有限值不能是 nan 或 inf _, cost forward(params, X, Y) assert np.isfinite(cost), 损失出现 nan/inf先查学习率和 log(0) # 3. 随机性同一种子连跑两次结果必须完全一致 a forward(params, X, Y)[1] b forward(params, X, Y)[1] assert a b, 存在未固定的随机源这三条断言看起来过于基础但它们对应的正是前面五处易错点里最难查的那几类。形状断言拦住广播错误有限值断言拦住除零和溢出一致性断言拦住种子没固定。把它挂在训练入口每次改动之后自动跑一遍比事后翻日志快得多。复习的节奏建议按“一天一模块、三天一轮回”推进第一轮只看表、只做自查动作不重读代码第二轮专攻打勾失败的那几项第三轮把整套作业里最难的两道题从头默写一遍卡住的地方就是真正的薄弱点。我自己的习惯是每次重跑任何一份作业答案之前先把sanity_check跑一遍不管上次是不是刚跑通过。有一次图省事跳过了结果一个广播错误藏在第三层损失曲线看着挺正常白白调了两天学习率。从那以后我每次改动参数或换数据集都强制走一遍这三条断言再决定要不要看曲线。希望帮到你。本文还有配套的精品资源点击获取