简介:这份资源面向正在学习斯坦福CS231N计算机视觉公开课、需要完成assignment1编程作业的深度学习入门者,提供从题目到答案的完整参考。包内共54个文件,以ipynb交互式笔记本、py源码与pyc缓存为主,辅以tar、gz、zip等格式的CIFAR-10数据集分片,以及xml、iml等IDE工程配置和sh、cmd脚本,压缩包约561.98MB。已有2007人学习下载。内容覆盖knn、svm、softmax、two_layer_net与features等作业模块,既保留原始题目笔记本,也附带完成后的答案版本,并集成网上下载的数据集,可直接在Anaconda的Jupyter环境中打开运行。读者可对照参考实现理解分类器构建、梯度检查与特征提取等关键环节,借助solver、layers、optim等模块梳理代码结构,适合边做边查、快速定位思路,减少环境与数据准备上的重复摸索。
1. 从一道“反向传播手推题”说起:这场作业到底考什么
很多人第一次打开 cs231n 作业 assignment1,会以为它只是“跑通几个 notebook”。真正拉开差距的,是第一道反向传播手推题:给你一个由乘法、加法、ReLU、softmax 组成的计算图,要求你写出每个局部梯度并链式相乘。这道题不写代码,却直接决定你后面 SVM、Softmax、两层网络的梯度实现能不能一次过。assignment1 考的不是调库能力,而是“你能不能把梯度从损失函数一路推回每个权重矩阵”。拿分逻辑很朴素:概念题拿稳、向量化写对、梯度检验通过。适合已经学过一点深度学习、但一写 backward 就翻车的人,也适合准备面试时被问“手推 softmax 梯度”的备考者。这一份作业的考点分布,基本就是深度学习入门面试的高频题库。
2. 考点分布:四个模块的分值与拿分优先级
2.1 模块拆解与分值权重
assignment1 的内容可以拆成四块:KNN、线性分类器(SVM 与 Softmax)、两层全连接网络、以及贯穿始终的向量化与梯度推导。按“投入产出比”排,线性分类器和两层网络是绝对重点,KNN 是理解距离度量的引子,向量化则是隐形加分项——它不单独出题,却决定你每一题的运行效率和代码质量。
| 模块 | 核心考点 | 拿分优先级 | 常见形式 |
|---|---|---|---|
| KNN | L1/L2 距离、k 值影响、交叉验证 | 中 | 填空 + 简答 |
| 线性分类器 | SVM hinge loss、Softmax 交叉熵、解析梯度 | 高 | 推导 + 代码 |
| 两层网络 | 前向传播、反向传播、ReLU 梯度 | 高 | 代码 + 梯度检验 |
| 向量化 | 去循环、广播机制、矩阵维度对齐 | 高(隐形) | 贯穿各题 |
这张表的意思是:如果你时间紧,先把线性分类器和两层网络的 backward 写熟,再回头补 KNN 的交叉验证细节。面试里被问得最多的也是前两块。
2.2 真题演练:KNN 的 k 值与交叉验证
题目:KNN 中 k 取 1 和取很大分别会怎样?为什么必须用交叉验证选 k?
答题思路:先说极端。k=1 时模型对每个训练点都“记住”,训练集上几乎零误差,但测试时对噪声极度敏感,一个离群点就能把预测带偏,这是典型的过拟合。k 取很大时,预测会被大量邻居“平均化”,决策边界变得平滑,偏差上升、方差下降,极端情况下所有点都被判成多数类。所以 k 是偏差-方差权衡的旋钮。
再说为什么用交叉验证:k 是超参数,不能拿测试集调,否则测试集信息泄漏,评估结果虚高。标准做法是把训练集切成若干折,对每个候选 k 在验证折上算准确率,取平均最高的那个,最后用全部训练集重训、只在测试集上评估一次。面试官最想听的是“超参数只能在验证集上选”这句话,以及你能说清 k 小过拟合、k 大方差低的因果。
提示:KNN 还有一个常被追问的点——距离度量。L1 对异常值更鲁棒,L2 对大差异更敏感,答题时顺带提一句会显得你理解得不浅。
2.3 真题演练:SVM hinge loss 与梯度
题目:写出多类 SVM 的 hinge loss,并说明什么时候梯度为零。
答题思路:多类 SVM 的损失是对每个样本,把所有“错误类得分减正确类得分加间隔”的正部分加起来。用代码表达更清楚:
import numpy as np def svm_loss_naive(W, X, y, reg): # W: (D, C),X: (N, D),y: (N,) num_train = X.shape[0] loss = 0.0 dW = np.zeros_like(W) for i in range(num_train): scores = X[i].dot(W) # 当前样本对各类的得分 correct = scores[y[i]] # 正确类得分 for j in range(W.shape[1]): if j == y[i]: continue margin = scores[j] - correct + 1.0 # 间隔设为 1 if margin > 0: # 只有违反间隔才计损失 loss += margin dW[:, j] += X[i] # 错误类梯度 dW[:, y[i]] -= X[i] # 正确类梯度 loss /= num_train loss += reg * np.sum(W * W) # L2 正则 dW /= num_train dW += 2 * reg * W return loss, dW这段代码的关键在if margin > 0:只有错误类得分“逼近甚至超过”正确类(差距小于间隔 1)时,这一项才贡献损失和梯度;一旦正确类领先超过间隔,梯度就是零,这个样本对更新没有影响。参数上,reg控制正则强度,间隔 1 是惯例取值,改它等价于缩放得分尺度。面试里如果让你口述,就抓住“max(0, ...) 的分段性质决定梯度是否为零”这一句。
2.4 真题演练:Softmax 梯度为什么是“概率减标签”
题目:Softmax 交叉熵对得分的梯度为什么可以写成“预测概率减去 one-hot 标签”?
答题思路:这是 assignment1 里最值得背下来的结论。设得分向量为 s,softmax 后第 j 类概率为 p_j,交叉熵损失只对正确类取负对数。对正确类求导,链式展开后得到 p_j - 1;对错误类求导得到 p_j。合起来就是“概率向量减去 one-hot 向量”。这个形式极其干净,也是它比 SVM 更常用的原因之一——梯度天然有界,数值更稳。
答题时补一句数值稳定性:实际实现要先减去每行最大值再取指数,否则 exp 容易溢出。这一点在作业里是隐藏考点,面试官问“你实现 softmax 时踩过什么坑”,答这个就是血泪经验。
3. 高频考点:向量化、广播与梯度检验三件套
3.1 向量化:把双重循环改成矩阵运算
assignment1 几乎每道代码题都要求“不用循环”。以 SVM 损失为例,向量化版本靠的是把“每个样本对每个类”的得分一次性算出来,再用掩码处理间隔:
def svm_loss_vectorized(W, X, y, reg): num_train = X.shape[0] scores = X.dot(W) # (N, C) 一次算完 correct = scores[np.arange(num_train), y].reshape(-1, 1) margins = np.maximum(0, scores - correct + 1.0) margins[np.arange(num_train), y] = 0 # 正确类自身不计损失 loss = np.sum(margins) / num_train + reg * np.sum(W * W) mask = (margins > 0).astype(float) # 违反间隔的位置为 1 mask[np.arange(num_train), y] = -np.sum(mask, axis=1) # 正确类汇总负梯度 dW = X.T.dot(mask) / num_train + 2 * reg * W return loss, dW讲解重点在mask的两步:先把所有违反间隔的位置标 1,再把正确类那一列改成“该行违反次数的负值”。这样一次矩阵乘法X.T.dot(mask)就同时完成了错误类加梯度、正确类减梯度。参数上np.arange(num_train)是行索引,配合y做花式索引,这是 NumPy 里最常考也最容易写错的写法。向量化写对,运行时间能从分钟级降到毫秒级,这就是作业里反复强调它的原因。
3.2 广播机制:维度对齐是翻车重灾区
广播的规则是“从尾部维度开始对齐,相等或其一为 1 才能广播”。在两层网络里,偏置b的形状是(1, H),要和(N, H)的隐藏层相加,靠的就是广播。常见翻车是:把b写成(N, H)去加,结果不报错但语义错了;或者反向传播时忘了对偏置求和沿样本维度axis=0聚合。
答题模板可以这样组织:先写形状注释,再写运算,最后检查“哪些维度被广播了、反向时要不要 sum 回去”。面试官问“广播什么时候会悄悄出错”,你就答“形状恰好兼容但语义不对,比如把偏置当成逐样本向量”,这是很能体现工程直觉的答案。
3.3 梯度检验:数值梯度是唯一的后悔药
解析梯度写完,必须用数值梯度对一遍。中心差分公式是(f(x+h) - f(x-h)) / (2h),相对误差控制在 1e-7 量级算通过,1e-4 以下可接受,再大就说明 backward 写错了。
def eval_numerical_gradient(f, x, h=1e-5): grad = np.zeros_like(x) it = np.nditer(x, flags=['multi_index']) while not it.finished: idx = it.multi_index old = x[idx] x[idx] = old + h fxph = f(x) x[idx] = old - h fxmh = f(x) x[idx] = old grad[idx] = (fxph - fxmh) / (2 * h) # 中心差分 it.iternext() return grad这段代码逐元素扰动求数值梯度,h不能太大(截断误差)也不能太小(舍入误差),1e-5 是常用折中。梯度检验只在调试时开,正式训练要关掉,因为它慢得离谱。作业里它出现的意义就是:让你在提交前有一次“后悔药”可吃。
注意:梯度检验前先把正则项调小或关掉,否则正则梯度的小误差会掩盖真正的实现错误,这是很多人卡住半天的地方。
4. 易错点:五个让分数悄悄溜走的坑
4.1 错在把正确类也算进 hinge loss
错在哪:写 SVM 损失时,循环里没有跳过j == y[i],把正确类自己的得分也算了间隔。为什么错:正确类减自己加 1 恒为正,会凭空多出一项常数损失,梯度也跟着错。怎么答对:循环里显式continue,向量化版本里把正确类那一列置零,两处都要检查。
4.2 错在 softmax 不减最大值直接 exp
错在哪:直接np.exp(scores),分数稍大就溢出成 inf,损失变 nan。为什么错:exp 增长极快,浮点表示范围有限。怎么答对:先沿类别维度减每行最大值再取指数,数学上等价、数值上稳定,答题时主动提这一点是加分项。
4.3 错在反向传播漏掉 ReLU 的掩码
错在哪:两层网络 backward 时,把上游梯度直接传过 ReLU,忘了乘“前向输入是否大于零”的掩码。为什么错:ReLU 在负半轴梯度为零,漏乘等于把死神经元的梯度也放过去了。怎么答对:前向时缓存x > 0的布尔掩码,反向时逐元素相乘,这是固定套路。
4.4 错在正则梯度少乘系数
错在哪:L2 正则损失写reg * sum(W*W),梯度却只写reg * W。为什么错:对 W 平方求导会掉出因子 2,漏掉就系统性偏小。怎么答对:损失和梯度成对写,写完立刻用梯度检验对一遍,这类错误一验就现形。
4.5 错在交叉验证时动了测试集
错在哪:调 k、调学习率时顺手看了测试集准确率,反复挑最好的。为什么错:测试集一旦参与选择,评估就不再无偏,分数虚高。怎么答对:所有超参数只在训练集内部切出的验证折上选,测试集只在最后评估一次,这条纪律面试里也要说清楚。
5. 答题节奏:从推导到代码的通用模板
5.1 一套可复用的四步答题法
面对 assignment1 里任何一道“实现损失和梯度”的题,我习惯走四步。第一步写形状:把 W、X、y、b 的维度标在草稿上,维度对了思路就顺了一半。第二步写前向:先不管效率,用最直白的循环把损失算出来,确保逻辑对。第三步推梯度:对每个参与运算的量问“它对损失的偏导是什么”,链式乘上去,注意 max、exp 这类非线性点的分段性质。第四步向量化加检验:把循环换成矩阵运算,再用数值梯度对一遍。
这套顺序的价值在于:先保证对,再保证快,最后保证稳。很多人一上来就想向量化,结果维度对不上、掩码写错,debug 时间反而更长。面试时如果被要求现场写,也按这个顺序口述,面试官能清楚看到你的思路层次。
| 步骤 | 产出 | 检查点 |
|---|---|---|
| 写形状 | 维度注释 | 尾部维度能否对齐 |
| 写前向 | 循环版损失 | 损失量级是否合理 |
| 推梯度 | 解析梯度 | 非线性点是否分段 |
| 向量化+检验 | 矩阵版代码 | 相对误差是否达标 |
5.2 面试口述时的取舍
如果面试只给你三分钟讲清两层网络的反向传播,不要从链式法则讲起,直接从“缓存了什么、每层梯度怎么来”切入:前向缓存输入、权重和 ReLU 掩码;反向从损失对得分的梯度出发,先过 softmax(概率减标签),再乘第二层权重转置得到隐藏层梯度,过 ReLU 掩码,最后乘第一层输入转置得到第一层权重梯度,偏置沿样本维度求和。每一句都落到具体张量,比背公式有说服力得多。
从那以后我每次准备这类作业或面试,都强制走一遍“形状—前向—梯度—检验”的流程,哪怕题再简单也不跳步,因为翻车几乎都发生在自以为熟的地方。希望帮到你。
本文还有配套的精品资源,点击获取