简介:这份CS231N计算机视觉公开课assignment1配套资料,面向深度学习与视觉方向入门学习者,提供作业原题、参考答案与数据集的完整练习包,便于在本地Jupyter环境复现实验并对照自查。压缩包共54个文件、约561.98MB,以交互式笔记本和Python源码模块为主,另含CIFAR-10压缩数据包及少量配置、脚本和说明文档,覆盖从代码运行到结果提交的环节。已有2006人学习下载。内容围绕KNN、SVM、Softmax、两层神经网络与特征提取实验展开,涉及损失函数、梯度推导、超参数交叉验证和图像特征工程,并包含分类器、层实现、优化器与梯度检查代码。帮助读者建立图像分类任务的整体认识并积累可复用的实验经验,适合按作业顺序调试、比对答案并理解输出,也可用于课程复习、实验排错及衔接后续作业。
1. 零基础也能读懂的 cs231n assignment1:它到底在练什么
假设你面前摊着六万张 32×32 的小图片,有人问你:这张糊成一团的图,到底是猫还是狗?你的第一反应大概是翻出几张长得最像的来对比。cs231n 这门深度学习公开课的 assignment1,练的就是把“找最像的”这件直觉,写成计算机能执行的代码。
它不是什么神秘项目,而是一份带骨架、带测试、带数据集的编程作业:给你一堆小图,让你从最笨的“最近邻”分类器开始,一路写到线性分类器,最后手写一个两层神经网络,看着准确率从 0.28 一点点爬上去。整个过程不需要 GPU,一台普通笔记本就能跑完。
适合谁?会一点点 Python 循环和数组、数学已经忘得差不多、但想真正搞懂“模型是怎么学会分类的”的人。如果你只是背过“交叉熵”“梯度下降”这些词,却从没自己算过一次梯度,那这份作业正好补上这块。
2. 为什么值得动手:从“看得懂公式”到“写得出代码”
2.1 深度学习入门最缺的不是数学,是手上有感觉
我见过太多人卡在同一个地方:视频看得懂,公式推得动,一到自己写就懵。原因很简单——看别人推导是被动接受,自己动手写是主动重建,中间隔着一道很宽的沟。
举个具体例子。你大概听过“梯度就是损失函数对参数的偏导”这句话。听上去没什么难度,但当你真的要在dW[:, y[i]] -= X[i]这一行里决定该加还是该减、该对哪个类别操作时,你会发现脑子里那点概念根本不够用。这时候唯一的办法就是:写一遍,跑一遍,让数值梯度检查告诉你错在哪。
assignment1 的设计恰好踩在这个点上。它不要求你先啃完一本优化教材,而是先给你一个能跑的最小实现,再让你用数值梯度去验证解析梯度。验证不过,你就得回去一行行看,这个过程比看十遍视频都管用。零基础的人最怕的是“不知道自己在干什么”,而这份作业每一步都有测试文件告诉你:对了,或者错了。
还有一点很关键:它的反馈特别快。kNN 写完,几秒钟就能看到准确率;SVM 写完,损失曲线立刻画出来。这种即时反馈对新手极其重要,因为你能明确感觉到“我改的这行代码,真的影响结果了”。深度学习里很多东西是玄学,但这份作业里几乎没有玄学,全是能对上账的加减乘除。
2.2 assignment1 的五块内容,按难度排一排
这份作业不是让你一口气吃完的,它分了五块,难度大致递增。我按新手体感排了个顺序,你可以照着走:
| 顺序 | 练习 | 核心要写的东西 | 新手大概耗时 |
|---|---|---|---|
| 1 | kNN | 距离矩阵 + 投票预测 | 3~4 小时 |
| 2 | SVM | 合页损失 + 梯度 | 4~6 小时 |
| 3 | Softmax | 交叉熵 + 数值稳定 | 3~5 小时 |
| 4 | 两层神经网络 | 前向传播 + 反向传播 + 训练循环 | 6~8 小时 |
| 5 | 图像特征 | 手工特征拼接 + 调参 | 2~3 小时 |
第一块 kNN 几乎不需要数学,你只要会算两点之间的距离、会数票就行。它的作用不是让你学会一个有用的模型(kNN 在实际里基本没人用),而是让你熟悉整套流程:数据怎么加载、训练集测试集怎么分、准确率怎么算。
第二、三块的 SVM 和 Softmax 是真正的分水岭。从这里开始,你要接触“损失函数”和“梯度”这两个词的实际含义。很多人在这卡住,不是因为它难,而是因为第一次见到“我要对每个错误类别都算一次惩罚”这种写法,脑子转不过弯。
第四块的两层神经网络是整份作业的重点。前面几块都是线性模型,到这一步你才第一次加上隐藏层和 ReLU 激活函数,第一次看到“非线性”带来的准确率跳变。第五块图像特征反而轻松,属于给你一个“如果不死磕神经网络,还能怎么提分”的备选答案。
入门的话建议严格按这个顺序走,别跳。我见过有人嫌 kNN 太简单,直接去写神经网络,结果连dists矩阵的维度都搞不清,反而更慢。
2.3 跑完它,你手上会多出什么
说点实在的。跑完这五块,你会得到三样东西。
第一样是一套自己写的 numpy 实现。不是调包,是从头算矩阵乘法、从头算梯度。你会清楚地知道W的形状为什么是(3072, 10),为什么偏置b是 10 维而不是 3072 维。这些细节在调包的时候永远学不到。
第二样是调参的肌肉记忆。学习率从 1e-7 试到 1e-2,正则化强度从 0 试到 1e-2,你会亲眼看到学习率太大会让损失炸成nan,太大会让准确率卡在 0.1 不动。这种“手感”是看教程看不来的。
第三样是对“过拟合”的直观理解。当你把训练集准确率调到 0.55、验证集只有 0.29 的时候,你会瞬间明白为什么要有验证集、为什么要加正则化。这比背十遍定义有用得多。
3. 动手之前:环境、依赖和第一次跑通
3.1 一份新手够用的环境清单
先说结论:不需要显卡,不需要装深度学习框架,一台能开浏览器的电脑就够了。整份作业只依赖 numpy 和 matplotlib,外加一个 Jupyter 用来跑 notebook。
我建议用虚拟环境把依赖隔离起来,别直接装在系统 Python 里,不然以后装别的包容易打架。下面是创建环境并装依赖的命令:
# 创建一个独立的 Python 环境,名字随便取,这里叫 cs231n python -m venv cs231n_env # 激活环境:macOS / Linux 用下面这条 source cs231n_env/bin/activate # Windows 用下面这条 # cs231n_env\Scripts\activate # 装作业需要的最小依赖 pip install numpy matplotlib jupyter装完之后,运行jupyter notebook就能在浏览器里打开交互式界面。这里解释一下每个包的作用:numpy负责所有矩阵运算,作业里几乎每一行都离不开它;matplotlib用来画损失曲线和权重可视化图,调参的时候全靠它;jupyter提供一个能分块运行、随时看中间结果的编辑环境,新手排查问题特别方便。
版本方面不用太纠结,Python 3.7 以上、numpy 1.x 以上就够。真要说有什么注意事项,就是别用太老的 Python 2,语法差异会让你在抄代码的时候莫名报错。
3.2 把骨架跑起来的正确顺序
作业的目录结构大概是这样的:一个cs231n/文件夹放核心代码,一个datasets/放数据,几个.ipynb文件是每一步的练习入口。你不需要自己建项目,拿到骨架之后按下面的顺序走就行。
第一步,先别急着写代码,把数据加载跑通。打开 kNN 那个 notebook,找到加载 CIFAR-10 数据集的那几行:
from cs231n.data_utils import load_CIFAR10 # 把图片读进来,返回四个数组:训练图、训练标签、测试图、测试标签 X_train, y_train, X_test, y_test = load_CIFAR10('cs231n/datasets/cifar-10-batches-py') print(X_train.shape) # (50000, 32, 32, 3) print(y_train.shape) # (50000,) print(X_test.shape) # (10000, 32, 32, 3)这几行代码的意思是:训练集有五万张 32×32 的彩色图,测试集有一万张。注意图片的原始形状是三维的,但后面所有计算都要把它拉平成一行 3072 个数字(32×32×3),所以紧接着会看到这样两行:
# 把每张图从 32x32x3 拉平成一行 3072 维的向量 X_train = np.reshape(X_train, (X_train.shape[0], -1)) X_test = np.reshape(X_test, (X_test.shape[0], -1)) print(X_train.shape) # (50000, 3072)-1在这里的意思是“这一维你自己算”,numpy 会把 32×32×3 自动乘成 3072。这个技巧在作业里会反复出现,看到别慌。
第二步,把数据切出一份验证集。这一步很多人会漏掉,直接拿测试集调参,后面会吃大亏。标准做法是从训练集里切出 1000 到 5000 张当验证集,用来选超参数,测试集留到最后只用一次。
第三步,跑一个“全预测成同一类”的傻瓜基线。听起来很蠢,但它能告诉你随机猜的准确率大概是 0.1(十个类别),如果后面你写出来的模型还不如这个,说明代码有 bug。
3.3 用一段最小代码看懂整个流程
在写任何分类器之前,先在脑子里过一遍整条链路:训练数据进来,算它和测试样本的距离,挑最近的几个,看它们是什么类,投票。就这么四步。
我把它压成一段完整可跑的最小代码,你可以先复制到一个新的 notebook 单元格里跑一下,不用管它是不是用到了完整的类结构:
import numpy as np # 假装我们有 5 张测试图、100 张训练图,每张图 3072 维 Xtr = np.random.randn(100, 3072) ytr = np.random.randint(0, 10, 100) Xte = np.random.randn(5, 3072) # 第一步:算距离,用最朴素的广播写法 # Xte[:, None, :] 形状是 (5, 1, 3072),Xtr[None, :, :] 形状是 (1, 100, 3072) # 两者相减后求和开方,得到 (5, 100) 的距离矩阵 dists = np.sqrt(((Xte[:, None, :] - Xtr[None, :, :]) ** 2).sum(axis=2)) print(dists.shape) # (5, 100) # 第二步:每张测试图取最近的 k=3 个训练样本 k = 3 nearest = np.argsort(dists, axis=1)[:, :k] # (5, 3),存的是训练样本的下标 # 第三步:投票,选出出现次数最多的类别 for i in range(5): labels = ytr[nearest[i]] pred = np.bincount(labels, minlength=10).argmax() print(f'第 {i} 张图预测为类别 {pred}')这段代码里最值得琢磨的是Xte[:, None, :] - Xtr[None, :, :]这一句。None的作用是在中间插一个长度为 1 的维度,让两个数组的形状能对齐成(5, 100, 3072),然后 numpy 会自动把每一对样本相减。这就是所谓“广播”,作业里向量化的核心技巧就是它。一开始看不懂没关系,先把形状打印出来,对着形状看几遍就通了。
至于np.argsort,它返回的是从小到大排序后的下标,不是值本身。取前 k 个就是取最近邻。np.bincount则是数每个类别出现了几次,argmax挑出次数最多的那个。这三行就是 kNN 的全部逻辑,作业里你只是要把它写得更规范、支持任意 k、并且跑在真实数据上。
跑通这段,你就已经跨过了这道坎最难的一步:知道自己在处理什么形状的数据。
4. 五个练习逐个拆:从最近邻到两层神经网络
4.1 kNN:先写一个最笨但能出分的分类器
kNN 的作业要求你实现两个函数:一个算距离,一个做预测。距离有两种写法,一种是双重循环,一种是完全向量化。新手一定要先写循环版,因为它最直白,出错也最容易定位。
class KNearestNeighbor(object): def __init__(self): pass def train(self, X, y): # kNN 没有真正的"训练",只是把训练数据原封不动存下来 self.X_train = X self.y_train = y def compute_distances_two_loops(self, X): num_test = X.shape[0] num_train = self.X_train.shape[0] # 结果是一个 (测试样本数, 训练样本数) 的矩阵 dists = np.zeros((num_test, num_train)) for i in range(num_test): for j in range(num_train): diff = X[i] - self.X_train[j] # 3072 维的差向量 dists[i, j] = np.sqrt(np.sum(diff ** 2)) return dists这段代码的逻辑很直白:对每一张测试图,和每一张训练图算一次欧氏距离,填进矩阵对应位置。两个参数X和self.X_train的形状分别是(num_test, 3072)和(num_train, 3072),返回的dists是(num_test, num_train)。
这个循环版在真实数据上会很慢。500 张测试图对 5000 张训练图,要跑将近一分钟。所以作业后面会要求你写向量化版本,把两层循环干掉。我的建议是:先让循环版跑出结果,确认准确率在 0.28 左右,再去优化速度。如果跳过了这步,向量化写错的时候,你都不知道是逻辑错了还是形状错了。
预测函数的写法更简单:
def predict_labels(self, dists, k=1): num_test = dists.shape[0] y_pred = np.zeros(num_test) for i in range(num_test): # argsort 拿到距离从小到大的下标,取前 k 个 closest_y = self.y_train[np.argsort(dists[i])[:k]] # bincount 数票,argmax 挑出票数最多的类别 y_pred[i] = np.argmax(np.bincount(closest_y)) return y_predk=1就是最近邻,k=5就是五个邻居投票。你会发现 k 取值不同,准确率会晃来晃去,这就是超参数。作业会让你试几个不同的 k,画出准确率曲线,你会看到 k 太小容易受噪声影响,k 太大又把远处的样本也算进来,中间有个甜点。
这里有个新手常犯的错:np.bincount如果遇到某个类别一次都没出现,返回数组长度会变短,导致下标错位。加上minlength=10就稳了。这个小细节在测试文件里不会直接告诉你,但你如果发现准确率异常低,八成是这里。
4.2 SVM 与 Softmax:把“打分”翻译成“损失”
从这一节开始,你要接触真正的“学习”了。核心思路只有一句话:模型给每个类别打一个分,我们希望正确类别的分比错误类别的分高出一截,差多少就罚多少。
SVM 用的是合页损失(hinge loss)。写成代码是这样的:
def svm_loss_naive(W, X, y, reg): # W: (3072, 10) 权重矩阵,X: (N, 3072) 一批样本,y: (N,) 正确标签 dW = np.zeros(W.shape) num_classes = W.shape[1] num_train = X.shape[0] loss = 0.0 for i in range(num_train): scores = X[i].dot(W) # 这张图对 10 个类别的打分 correct = scores[y[i]] # 正确类别的得分 for j in range(num_classes): if j == y[i]: continue # 正确类别自己不用罚 margin = scores[j] - correct + 1.0 # 错误类得分 - 正确类得分 + 安全边距 if margin > 0: # 只有没拉开差距才罚 loss += margin dW[:, j] += X[i] # 错误类的权重想让它降分 dW[:, y[i]] -= X[i] # 正确类的权重想让它升分 loss /= num_train dW /= num_train loss += reg * np.sum(W * W) # L2 正则,防止权重长得太大 dW += 2 * reg * W return loss, dW几个参数要说明白。W是权重矩阵,3072 行对应每个像素位置,10 列对应十个类别。reg是正则化强度,取值越大,模型越保守。margin那个+1.0是安全边距,意思是正确类别的分至少要比错误类别高 1 分,高不到就按差距罚款。
Softmax 换了个思路:先把打分变成概率,然后让正确类别的概率尽量接近 1,用交叉熵衡量差距。写法上有个坑必须注意——直接对打分取exp会溢出成inf,所以要先减去最大值:
def softmax_loss_naive(W, X, y, reg): loss = 0.0 dW = np.zeros_like(W) num_train = X.shape[0] for i in range(num_train): scores = X[i].dot(W) scores -= np.max(scores) # 关键:减最大值,防止 exp 溢出 exp_scores = np.exp(scores) probs = exp_scores / np.sum(exp_scores) # 归一化成概率 loss -= np.log(probs[y[i]]) # 正确类别概率越低,罚得越狠 # 反向传播:概率向量里,正确类减 1,其余保持 probs[y[i]] -= 1 dW += np.outer(X[i], probs) loss /= num_train dW /= num_train loss += reg * np.sum(W * W) dW += 2 * reg * W return loss, dW这段代码里scores -= np.max(scores)那一行是血泪经验换来的。如果不减,当某个打分是 800 的时候,exp(800)直接变成inf,然后inf / inf得到nan,整个损失就废了。减去最大值不改变概率分布,但把数值压到了安全范围。这个技巧在后面写神经网络的时候还要用一次。
写完这两块,作业里会要求你做梯度检查。原理很简单:把某个权重加一点点,看损失变化多少,跟解析梯度对比,误差应该在 1e-8 以内。检查不过,基本都是dW里少乘了、少除了,或者正则项的导数忘了加。
4.3 两层神经网络:第一次看到“学习”发生
前面三个模型全是线性的,本质上是给每个类别学一个模板,然后把图片往上套。这种方法的天花板大概在 0.4 左右。想再往上,就得加非线性。
两层网络的思路是:先用第一层把 3072 维压到一个更小的隐藏维度(比如 100),过一下 ReLU 把负数清零,再用第二层从这 100 维映射到 10 个类别。多了这么一层,模型就能学到“边缘 + 组合”这种更复杂的模式。
class TwoLayerNet(object): def __init__(self, input_size, hidden_size, num_classes, std=1e-3): self.params = {} # 第一层:输入 3072 维,压到 hidden_size 维 self.params['W1'] = std * np.random.randn(input_size, hidden_size) self.params['b1'] = np.zeros(hidden_size) # 第二层:hidden_size 维映射到 10 个类别 self.params['W2'] = std * np.random.randn(hidden_size, num_classes) self.params['b2'] = np.zeros(num_classes) def loss(self, X, y=None, reg=0.0): W1, b1 = self.params['W1'], self.params['b1'] W2, b2 = self.params['W2'], self.params['b2'] # 前向:线性 + ReLU hidden = np.maximum(0, X.dot(W1) + b1) scores = hidden.dot(W2) + b2 if y is None: return scores # softmax 交叉熵,和上一节一样先减最大值 scores -= np.max(scores, axis=1, keepdims=True) exp_scores = np.exp(scores) probs = exp_scores / np.sum(exp_scores, axis=1, keepdims=True) num_train = X.shape[0] loss = -np.sum(np.log(probs[np.arange(num_train), y])) / num_train loss += reg * (np.sum(W1 * W1) + np.sum(W2 * W2)) # 反向:从 softmax 往回推 dscores = probs.copy() dscores[np.arange(num_train), y] -= 1 dscores /= num_train grads = {} grads['W2'] = hidden.T.dot(dscores) + 2 * reg * W2 grads['b2'] = np.sum(dscores, axis=0) dhidden = dscores.dot(W2.T) dhidden[hidden <= 0] = 0 # ReLU 的导数:负数位置梯度归零 grads['W1'] = X.T.dot(dhidden) + 2 * reg * W1 grads['b1'] = np.sum(dhidden, axis=0) return loss, gradshidden_size是你自己定的,100 到 500 都行,越大模型越强但也越容易过拟合。std是初始化权重的标准差,太小会导致所有神经元行为一样,太大会让激活值爆炸,1e-3 是个稳妥的起点。
写完这个类,作业会用一个小批量梯度下降的循环来训练它:每次拿 200 张图,算损失和梯度,按W -= learning_rate * dW更新,重复几千次。你会看到损失从 2.3 慢慢降到 1.5 左右,训练集准确率爬到 0.5 上下。第一次看到自己写的代码让准确率动起来,感觉是很好的。
这里最容易翻车的地方是dhidden[hidden <= 0] = 0这一行。ReLU 的导数在正数区是 1、负数区是 0,忘记清掉负数位置的梯度,训练就会莫名其妙地失败——损失不降反升,也不会报错。遇到这种情况,先检查这一行。
4.4 图像特征:给不想死磕的一步捷径
最后一块作业很有意思:它让你在原始像素之外,再试试手工设计的特征,比如方向梯度直方图(HOG)加上颜色直方图。这些特征在上古时代是主流,现在虽然被卷积网络取代了,但对新手来说是个很好的对照实验。
作业的做法是把图片切成小格子,统计每个格子里梯度方向的分布,再和颜色分布拼起来,得到一个比 3072 更短的向量。用这个向量去训练 SVM 或 Softmax,准确率能从 0.4 提到 0.5 左右。
这一步不需要写太多新代码,主要是理解和调参。它给你的启示是:模型再笨,特征好的话也能出成绩;反过来,特征不行,模型再花哨也白搭。这个判断力在后面做真实项目的时候很关键。
5. 新手最容易踩的五个坑:从现象反推原因
5.1 损失变成 nan 或者 inf
现象:训练刚开始几步,损失值突然从 2.3 跳到nan,后面所有准确率都是 0。
原因:九成是exp溢出。softmax 里如果没有减去每行的最大值,当打分稍微大一点(几十就够),np.exp结果就会超出浮点数范围。另一种可能是学习率设太大,一步更新把权重推到了极端值。
解决:先检查 softmax 里有没有scores -= np.max(scores, axis=1, keepdims=True)这一行。有的话,把学习率从 1e-2 降到 1e-3 或 1e-4 再试。还不行就打印一下更新前的权重绝对值,如果已经上万,说明初始化标准差设太大了,改回 1e-3。
5.2 梯度检查永远过不了
现象:数值梯度和解析梯度的相对误差在 0.1 以上,反复改都没用。
原因:通常是正则项的导数漏了。loss += reg * np.sum(W * W)对应的导数应该是2 * reg * W,很多人只写了reg * W或者干脆忘了这一项。另一种是忘了除以num_train,损失除了但梯度没除。
解决:先把reg设成 0 再检查一次。如果误差立刻降到 1e-8,那就是正则项导数的问题。如果还是不过,就单独检查某一个权重元素的梯度,用(loss_plus - loss_minus) / (2 * h)手算一个值,和解析梯度对比,看差在哪个量级。
5.3 向量化之后准确率暴跌
现象:循环版跑出来 0.28,改成向量化后掉到 0.1,也就是随机猜的水平。
原因:向量化的广播写错了方向。比如算距离的时候忘记开方、或者对错了轴。axis参数写错是最高频的问题,axis=1和axis=0的结果形状完全不同,但代码不会报错,只是悄悄算出错误答案。
解决:在向量化版本里加一句assert np.allclose(dists_vectorized, dists_loops, atol=1e-6),拿小批量数据跟循环版逐元素对比。一旦对不上,就打印几个具体位置的数值,看是整体偏移还是个别位置错。这个方法看起来笨,但比盯着代码看快十倍。
5.4 拿测试集调参,分数虚高
现象:你在测试集上试了十几种学习率,挑出最好的那个,报告准确率 0.52。过两天重跑一遍,换个随机种子,只有 0.46。
原因:测试集被当成了验证集用,模型间接“见过”了测试数据。这种分数是虚的,换个数据就崩。
解决:从训练集里切出 1000 到 5000 张当验证集,所有调参都在验证集上做。测试集只在最后跑一次,跑完就不要再改任何东西。这个习惯我现在每次做实验都强制走一遍,因为血泪经验告诉我,虚高的分数比低分更能骗人。
5.5 Jupyter 里改了代码但结果不变
现象:明明改了cs231n/classifiers/k_nearest_neighbor.py,重新运行单元格,结果还是老样子。
原因:Python 的模块导入是带缓存的,Jupyter 内核不会自动重新加载你改过的.py文件。第一次 import 之后,后面调用的都是内存里那份旧代码。
解决:改完外部文件,点一下内核的 Restart,或者用importlib.reload手动重载。更省事的办法是在 notebook 开头放这几行:
import importlib import cs231n.classifiers.k_nearest_neighbor as knn_module importlib.reload(knn_module)养成改完外部文件就重启内核的习惯,能省掉大量“我明明改了”的自我怀疑。
6. 做完 assignment1 之后:下一步学什么
跑完这五块,很多人会陷入一个空窗期:知道 kNN、SVM、softmax、两层网络是怎么回事了,但下一步该往哪走?我给一条我实际走过的路径,你可以照着排。
第一步,先把 assignment1 的向量化版本全部写干净。循环版能跑通只是及格,向量化才是真正理解 numpy 广播的时候。特别是svm_loss_vectorized和softmax_loss_vectorized,写完对着梯度检查跑一遍,误差在 1e-9 以内再往下走。这一步大概花两三天,但后面所有代码都靠这个基础。
第二步,往 assignment2 走,重点看三样东西:批量归一化、Dropout、卷积层。前两个是防止过拟合的常用手段,你在 assignment1 里已经被“训练集 0.55、验证集 0.29”折磨过了,正好有动机去理解它们。卷积层则是从全连接走向图像识别的关键一步,理解了卷积核在滑动的过程中共享权重,你才算真正入门计算机视觉。
第三步,把同样的模型用 PyTorch 重写一遍。为什么要重写?因为你需要体会“框架帮你做了什么”。手写过一遍dhidden[hidden <= 0] = 0之后,再看到nn.ReLU(),你会知道它在背后干了什么,而不是把它当成魔法。这个对照过程对建立信心特别重要。
第四步,找一个自己的小数据集练手。不要一上来就做大的,几百张图片、三五个类别就够。把数据加载、划分、训练、评估这条链路完整走一遍,你会遇到 assignment1 里没出现过的问题:图片尺寸不一致、类别不平衡、标签有噪声。这些问题才是真实项目的常态。
我给个简单的路线对照表,你按自己的节奏挑:
| 阶段 | 重点 | 建议投入 |
|---|---|---|
| 巩固 | 向量化 + 梯度检查 | 2~3 天 |
| 进阶 | 批量归一化、Dropout、卷积 | 1~2 周 |
| 迁移 | 用框架重写 assignment1 的模型 | 3~5 天 |
| 实战 | 自选小数据集完整跑一遍 | 1~2 周 |
有个习惯我想单独说一句。我第一次做完这份作业的时候,图快,梯度检查全靠“看起来差不多”糊弄过去,结果到 assignment2 写反向传播时彻底翻车,一个axis写错,调了整整两天才发现问题出在最早那层。从那以后我每次写完任何带梯度的代码,都强制先跑一遍数值梯度检查,误差不过 1e-7 绝不往下写。
这份作业最大的价值不是教会你几个模型,而是让你养成“先验证再推进”的习惯。慢慢来,别跳步,希望帮到你。
本文还有配套的精品资源,点击获取