简介:CS231n作业assignment1完整解决方案面向深度学习与计算机视觉入门者,尤其适合自学斯坦福CS231n课程、需要对照答案调试代码的学习者。压缩包内含作业原题、参考答案及官方CIFAR-10数据集,覆盖KNN、SVM、Softmax、两层神经网络和特征提取等多个经典实验模块,并提供collect_submission等配套脚本,可直接用于检查作业格式与结果提交。资源共54个文件,以17个Python脚本和7个Jupyter Notebook为核心代码,内含逐步实现与可视化输出;另有pyc缓存、xml工程配置、shell脚本、数据压缩包等辅助内容,整体大小561.98MB,目录结构清晰,便于按模块逐项学习。目前已有2003人学习下载,适合希望系统完成assignment1并深入理解反向传播、损失函数与参数调优等实现细节的初学者使用。
1. cs231n作业assignment1:从像素到十类图像分类的第一道完整代码旅程
第一次看到CS231n课程里的cs231n作业assignment1代码框架时,我盯着compute_distances_two_loops这个函数名愣了很久。这个作业要求你从零手写kNN、SVM、Softmax和一个两层神经网络,在CIFAR-10的十个类别上跑出分类正确率。它几乎不依赖任何深度学习框架,全靠Numpy和手写反向传播,所以特别适合刚学完Python、想验证自己是否真正理解梯度的开发者。做完这整个作业,你会获得两样东西:一是对损失、梯度、学习率这些概念有了能摸到的体感;二是建好一套训练、验证、测试的评测闭环,之后做简单图像分类任务可以直接复用这套流程。
2. 数据准备与kNN:先把评测闭环跑通,再做花哨模型
2.1 CIFAR-10的数据装载与预处理:为什么要先缩小子集
写这个作业,第一个要过的不是算法,而是数据流。作业框架用data_utils.py里的load_CIFAR10读取cifar-10-batches-py目录下的批数据。返回的训练集是5万张32x32x3的彩色图片,测试集是1万张。做kNN和线性分类器时,我建议只取训练集前5000张、测试集前500张,不是因为偷懒,而是kNN的预测阶段要做大量距离计算,全量数据会让交叉验证慢到让你怀疑人生。等后面跑两层神经网络,再考虑使用更大规模的数据。
import numpy as np from cs231n.data_utils import load_CIFAR10 cifar10_dir = 'cs231n/datasets/cifar-10-batches-py' X_train, y_train, X_test, y_test = load_CIFAR10(cifar10_dir) # 作业常用做法:缩小训练和测试规模,避免kNN距离矩阵过大 num_training = 5000 mask = list(range(num_training)) X_train = X_train[mask] y_train = y_train[mask] num_test = 500 mask = list(range(num_test)) X_test = X_test[mask] y_test = y_test[mask] # 把图像张量展平成行向量,每个样本长度 32*32*3 = 3072 X_train = np.reshape(X_train, (X_train.shape[0], -1)) X_test = np.reshape(X_test, (X_test.shape[0], -1))这段代码做的事情很直白:取前5000个训练样本、前500个测试样本,再把四维张量(N, 32, 32, 3)压成(N, 3072)的矩阵。-1让Numpy自动算出最终维度。作业里所有分类器都约定“每一行是一个样本,每一列是一个特征维度”,这个约定贯穿后面的SVM、Softmax和两层网络,所以一开始就要把数据形态摆正。
注意:
load_CIFAR10返回的数组已经是float64。如果你在自己项目里直接读原始图像文件,记得先做astype(np.float64),否则uint8类型的像素在距离计算里会溢出或产生截断误差。
这里没有做均值减法,是因为kNN对全体的平移不敏感,而且把均值计算留在每个分类器里能避免在交叉验证时提前看到全训练集的统计信息。真正需要均值减法的是接下来的线性分类器,我会在对应章节再处理。
2.2 kNN距离矩阵:从两重循环到零循环的三版实现
kNN的核心是计算每个测试样本和每个训练样本之间的欧氏距离。作业提供了三个函数签名,让你从最直观的两重循环写起,再逐步向量化。最朴素的那一版长这样:
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): # 逐样本欧氏距离 dists[i, j] = np.sqrt(np.sum((X[i] - self.X_train[j]) ** 2)) return dists这是最不容易写错的一版,也是后面验证向量化正确性的基准。只要这个版本能通过作业自带的正确性测试,说明你的训练集拷贝、数据形状和对距离的定义都没问题。接下来可以把它改成零循环版本,用平方展开把逐样本距离变成矩阵运算:
def compute_distances_no_loop(self, X): # 欧氏距离平方 = ||x||^2 + ||y||^2 - 2xy X_sqnorms = np.sum(X ** 2, axis=1, keepdims=True) train_sqnorms = np.sum(self.X_train ** 2, axis=1) cross = X.dot(self.X_train.T) dists = np.sqrt(np.maximum(X_sqnorms + train_sqnorms - 2 * cross, 0)) return dists这里X的每行是一个测试样本,self.X_train.T每列是一个训练样本,矩阵乘法的结果恰好是测试样本与训练样本的内积。为什么最后要做np.maximum(..., 0)?因为浮点误差可能让理论上等于0的距离变成很小的负数,开方后会产生nan,后面argsort排序时会把nan排在最后,导致预测标签莫名其妙出错。这个防“玄学”的小地方,很多人翻过车。
得到距离矩阵后,预测标签时取最近的k个训练样本的标签,做多数投票:
def predict_labels(self, dists, k=1): num_test = dists.shape[0] y_pred = np.zeros(num_test) for i in range(num_test): closest_y = self.y_train[np.argsort(dists[i])[:k]] # bincount统计每个标签出现次数,argmax取出现最多的 y_pred[i] = np.bincount(closest_y).argmax() return y_predargsort返回的是距离从小到大的训练样本下标,:k截取最近的k个。np.bincount本身就是为标签投票准备的,它统计0到最大标签之间每个整数出现的次数,argmax取次数最多的标签。作业默认k=1,通常能跑通,但正确率只在三成上下,几乎不能算“分类成功”。
距离矩阵的形状是(num_test, num_train),500个测试样本对5000个训练样本就是250万个浮点数,约20MB内存。如果把训练集切成20000张,这个矩阵会膨胀到3GB以上,直接挤爆内存。所以我通常把kNN的样本量控制在5000以内,这既是时间上的妥协,也是内存上的必须。
2.3 五折交叉验证选k:把测试集留到最后再用
kNN没有训练过程,所以调k就是整个模型调参。正确姿势是交叉验证:把训练集切5份,每次拿4份当训练、1份当验证,轮流计算5个正确率,再对每个候选k取平均和方差。作业框架里正好要求做5折:
num_folds = 5 X_train_folds = np.array_split(X_train, num_folds) y_train_folds = np.array_split(y_train, num_folds) k_choices = [1, 3, 5, 8, 10, 12, 15, 20, 50, 100] k_to_accuracies = {} for k in k_choices: accuracies = [] for f in range(num_folds): # 当前折留作验证,其余折拼接成训练 X_tr = np.concatenate([X_train_folds[i] for i in range(num_folds) if i != f]) y_tr = np.concatenate([y_train_folds[i] for i in range(num_folds) if i != f]) X_va = X_train_folds[f] y_va = y_train_folds[f] classifier = KNearestNeighbor() classifier.train(X_tr, y_tr) dists = classifier.compute_distances_no_loop(X_va) y_pred = classifier.predict_labels(dists, k=k) accuracies.append(np.mean(y_pred == y_va)) k_to_accuracies[k] = accuraciesnp.array_split切出来的是原数组的视图,不是拷贝,所以循环里反复拼接不会复制底层数据。每次循环里的classifier.train(X_tr, y_tr)只是保存引用,真正的计算发生在compute_distances_no_loop,所以在5000个训练样本时,这轮交叉验证依然可以接受。
候选k的取值,我习惯在1到100之间按倍数拉开,因为小k对噪声更敏感,大k后正确率趋于平缓。交叉验证结束后,画出每个k的5次正确率点图,看均值最高的区域;如果均值接近,优先选更小的k,因为它能保留更多局部信息。最后用选出的k在留出的测试集上跑一次,得到最终正确率。使用交叉验证而不是单次验证集,是因为kNN的正确率在小k时的方差很大,5次平均能把这个波动展示出来。你甚至会看到:k从1增到10,正确率先升后略降;当k到50之后,距离被平均掉,边界越来越平滑,正确率反而不如小k。这个观察能帮你建立对偏差-方差权衡的具体感知。
3. 线性分类器:SVM和Softmax的梯度实现与调参
3.1 损失函数对比:多类SVM与Softmax的训练行为差异
作业里的线性分类器将一张图片展平后的3072维向量作为输入,乘上形状为(3072, 10)的权重矩阵,得到10个类别的打分。SVM用的合页损失只要求正确类别的分数比错误类别高出一个固定间隔,不关心分数绝对值;Softmax则把分数转成概率,用交叉熵衡量预测分布和真实标签分布之间的差距。
用一个具体例子说明差异。假设某张猫图,正确类“猫”得分3,“狗”得分2.5,SVM的间隔1时损失为0,因为正确类已经高出0.5;但Softmax仍然会认为模型不够好,它希望正确类别的概率尽量接近1。因此Softmax会把训练过程看作持续压缩错误类的概率,而SVM只要满足间隔就停止优化。这个差异的直接后果是:Softmax通常需要更小的学习率或更长的训练迭代,否则前几轮就可能看到loss出现明显波动。
在训练线性分类器之前,我建议先对输入做均值减法:用训练集算出一个平均脸,训练集和测试集都减去这个平均值,这样每个特征维度都围绕0分布,梯度下降更容易收敛。
mean_image = np.mean(X_train, axis=0) X_train_centered = X_train - mean_image X_test_centered = X_test - mean_image注意只能减去训练集算出的均值,测试集要用同一个均值去减,不能各自算各自的。
3.2 向量化SVM损失与梯度:正确处理正确类的负累加
SVM损失最稳妥的做法是先写循环版,确保数值正确后再改成向量化。我这里直接给出向量化最终版,适合已经会循环版的读者对照:
def svm_loss_vectorized(W, X, y, reg): num_train = X.shape[0] scores = X.dot(W) # (N, C) correct_class_scores = scores[np.arange(num_train), y][:, np.newaxis] # delta = 1.0 是作业里的默认间隔 margins = np.maximum(0, scores - correct_class_scores + 1.0) margins[np.arange(num_train), y] = 0 # 正确类不参与损失 loss = np.sum(margins) / num_train + 0.5 * reg * np.sum(W * W) binary = np.zeros_like(margins) binary[margins > 0] = 1 # 每个错误类贡献 +1 binary[np.arange(num_train), y] = -np.sum(binary, axis=1) dW = X.T.dot(binary) / num_train + reg * W return loss, dW梯度部分最容易出错的是binary矩阵的最后一列:对每个样本来说,所有 margin > 0 的错误类都会在对应类别的梯度上累加一个该样本的特征向量,而正确类的位置需要累加所有这些错误类贡献的和并取负。那行binary[np.arange(num_train), y] = -np.sum(binary, axis=1)就是在做这件事。写错这行,梯度方向就会是错的,正确率会一直卡在随机以下。
在这个实现里,正则化项用的是0.5 * reg * sum(W^2),求导后得到reg * W,所以损失和梯度里都带有reg*W。作业里的reg通常取1e-3到1e-1之间。调参时先固定reg,再动学习率,能减少同时调两个参数的干扰。
3.3 Softmax的数值稳定性与梯度公式
Softmax损失函数是-log(p_y),其中概率由exp(score)归一化得到。如果分数很大,exp(100)直接溢出成inf,loss变成nan。解决方法是先减掉每行最大值,再做指数,这不会改变最终概率分布:
def softmax_loss_vectorized(W, X, y, reg): num_train = X.shape[0] scores = X.dot(W) scores -= np.max(scores, axis=1, keepdims=True) # 数值稳定 exp_scores = np.exp(scores) probs = exp_scores / np.sum(exp_scores, axis=1, keepdims=True) correct_log_probs = -np.log(probs[np.arange(num_train), y]) loss = np.sum(correct_log_probs) / num_train + 0.5 * reg * np.sum(W * W) dscores = probs.copy() dscores[np.arange(num_train), y] -= 1 dscores /= num_train dW = X.T.dot(dscores) + reg * W return loss, dWSoftmax梯度公式可以写成(P - label_onehot) / N,其中P是每个样本在每个类别上的预测概率,label_onehot是真实标签的 one-hot 向量。代码里的三段式处理:先拷贝probs,再只在正确类位置减1,最后除以样本数,就是完整的解析梯度。注意np.max(scores, axis=1, keepdims=True)没有参与梯度回传,因为它只是一个平移操作,对概率分布求导不变。
3.4 用数值梯度核对解析实现:做一次能省两小时
写完解析梯度后,必做的一件事是拿数值梯度对拍。作业框架里有eval_numerical_gradient,它用中心差分近似梯度:
from cs231n.gradient_check import eval_numerical_gradient def f(W): return svm_loss_vectorized(W, X_train_small, y_train_small)[0] loss, grad = svm_loss_vectorized(W, X_train_small, y_train_small, reg) grad_num = eval_numerical_gradient(f, W) def rel_error(a, b): return np.max(np.abs(a - b) / (np.max(np.abs(a)) + np.max(np.abs(b)))) print(rel_error(grad, grad_num))相对误差公式里,分母用max(|a|)+max(|b|)而不是普通范数,是为了避免某一边接近0时误差被放大。作业惯例是相对误差小于1e-7视为通过,小于1e-5基本也可以接受,但如果还在1e-2量级,就一定是解析梯度实现有问题。最常见的出错点是SVM里binary矩阵的正确类负累加、以及Softmax里dscores /= num_train放的位置。
提示:做梯度检查时应该用很小的样本数,比如10个样本、3个类别,这样数值梯度也能跑得飞快。用全量训练集做梯度检查,等于是给排查错误按钮加上了慢动作特效。
3.5 训练循环与调参顺序:先找学习率,再调正则强度
线性分类器的训练循环非常简洁:小批量采样、算损失和梯度、梯度下降更新权重:
W = np.random.randn(D, C) * 0.001 # 小随机初始化 for epoch in range(num_iters): idx = np.random.choice(num_train, batch_size, replace=False) X_batch = X_train_centered[idx] y_batch = y_train[idx] loss, dW = svm_loss_vectorized(W, X_batch, y_batch, reg) W -= learning_rate * dW这里D=3072,C=10,batch_size我常用200,num_iters取2000左右。调参顺序我总结为三步:先把reg固定在一个中性值如1e-3,学习率从0.1开始按十倍往下扫;找到一个能让 loss 在2000次迭代内稳定下降的学习率;再用 log 搜索扫 reg,学习率不动只调正则。学习率过大的征兆是loss在头几步直接跳成 nan;过小的征兆是loss下降斜率几乎平行于 x 轴。要注意的是,SVM和Softmax对学习率的敏感度不同,切换模型后不能直接沿用旧参数,至少要把学习率往下调半个数量级重新试。
4. 两层神经网络与图像特征:从线性到非线性的关键一跃
4.1 两层网络的结构与反向传播:用掩码实现ReLU的梯度
两层网络结构是:输入层、全连接加ReLU、隐藏层、全连接、输出10个分数。它相比线性分类器多了一个非线性隐藏层,能够对输入特征做简单的组合。作业里的TwoLayerNet所有前向和反向都写在loss函数里,返回(loss, grads)。初始化部分有一个容易被忽视的坑:如果权重初始化得过小,ReLU输出全是0,反向传播梯度为0;如果初始化过大,隐藏层输出饱和,loss可能震荡。我习惯用标准差1e-4的高斯随机初始化隐藏层权重,偏置保持0:
self.params['W1'] = std * np.random.randn(input_size, hidden_size) self.params['b1'] = np.zeros(hidden_size) self.params['W2'] = std * np.random.randn(hidden_size, output_size) self.params['b2'] = np.zeros(output_size)前向和反向的核心流程如下。注意我缓存了z1和a1,反向时用z1 > 0作为ReLU的掩码:
z1 = X.dot(W1) + b1 a1 = np.maximum(0, z1) scores = a1.dot(W2) + b2 # softmax loss 的前向,先取每行最大分数做数值稳定 exp_scores = np.exp(scores - np.max(scores, axis=1, keepdims=True)) probs = exp_scores / np.sum(exp_scores, axis=1, keepdims=True) loss = -np.log(probs[np.arange(N), y]).sum() / N loss += 0.5 * reg * (np.sum(W1 * W1) + np.sum(W2 * W2)) # softmax 反向:dscores = (P - onehot) / N dscores = probs dscores[np.arange(N), y] -= 1 dscores /= N # 第二层梯度 dW2 = a1.T.dot(dscores) db2 = np.sum(dscores, axis=0) # ReLU 反向:梯度只流过激活值大于0的位置 da1 = dscores.dot(W2.T) dz1 = da1 * (z1 > 0) # 第一层梯度 dW1 = X.T.dot(dz1) db1 = np.sum(dz1, axis=0) grads['W1'] = dW1 + reg * W1 grads['W2'] = dW2 + reg * W2这里有一个维度检查技巧:正向传播里X是(N, D),W1是(D, H),a1是(N, H),W2是(H, C)。反向传播时,dW2的维度应该和W2一样,所以用a1.T.dot(dscores);da1的维度应该和a1一样,所以用dscores.dot(W2.T)。如果形状对不上,NumPy会直接抛广播错误,反而容易检查;最怕形状恰好能乘但方向弄反。
4.2 图像特征:为什么HOG加色彩直方图能明显提升正确率
作业在两遍网络之后会引导你做特征工程,把每个样本从3072维像素换成几百维的手动特征。框架里已经提供了 HOG 和 HSV 色彩直方图的实现,你只需要调用并拼接特征向量:
from cs231n.features import hog_feature, color_histogram_hsv num_color_bins = 10 hog_feat = hog_feature(X_train, hog_params) color_feat = color_histogram_hsv(X_train, num_color_bins) X_train_feats = np.hstack([hog_feat, color_feat]) X_test_feats = np.hstack([hog_feat_test, color_feat_test])HOG 统计图像局部梯度方向分布,对边缘和纹理非常敏感;HSV色彩直方图统计图像整体颜色分布,对颜色类别很敏感。两者拼起来后,模型看到的不再是每个像素孤立强度,而是更有语义的局部描述。我在作业里用原始像素训练同一个两层网络,正确率在0.4左右徘徊;换成特征后可以上到0.55以上。原因很简单:没有卷积网络时,线性层很难从原始像素中凭空学会“边缘不变性”,而手动特征把这种先验直接喂给了模型。
4.3 训练两层网络的实用参数区间与验证
作业中隐藏层大小一般取50或100。隐藏层太小,非线性表达能力不够;太大则参数量暴涨,训练时间拉长。我建议先固定50,跑通再往上加。小批量大小取200,学习率集中在1e-3到1e-2,正则强度在1e-3附近。训练2000次迭代后,正确率可以达到0.45-0.50;如果换成特征输入,0.55-0.60是常见范围。
下面这个训练循环可以直接套用:
net = TwoLayerNet(input_size=D, hidden_size=50, output_size=10) for epoch in range(num_iters): idx = np.random.choice(num_train, batch_size, replace=False) X_batch = X_train[idx] y_batch = y_train[idx] loss, grads = net.loss(X_batch, y_batch, reg=0.01) for param in net.params: net.params[param] -= learning_rate * grads[param]注意学习率不能沿用线性分类器的值,因为两层网络的梯度量级更大,第一次跑建议从0.001开始试。loss 应该在前500次迭代有明显下降,之后进入平台。如果loss在某个区间反复横跳,可以把学习率减半;如果loss持续下降但正确率不涨,大概率是数据预处理或特征选型不对。
我习惯每500次迭代打印一次loss和当前batch的训练正确率。如果训练正确率在上升而验证正确率不涨,是过拟合,需要加大reg;如果两者一起横盘,是学习率太小或隐藏层节点太少;如果loss在初始值附近震荡,是学习率太大。这些诊断规则比单纯看最终正确率更能帮你判断下一步动作。
| 模型 | 常见正确率区间(5000训练子集) | 训练耗时 | | kNN | 0.28~0.35 | 无训练但预测慢 | | 线性SVM | 0.36~0.43 | 快 | | 两层网络(原始像素) | 0.45~0.50 | 快 | | 两层网络(HOG+颜色特征) | 0.55~0.60 | 快 |
这个表格是我自己复现时的经验范围,不同初始化、不同数据切片会有几个百分点的波动。如果你跑出来的数字低很多,先检查数据预处理和梯度检查,而不是怀疑模型能力。
5. 避坑指南:cs231n作业assignment1里最常见的五个翻车现场
5.1 梯度数值核对总是不通过
现象:rel_error一直停在1e-2量级,不管怎么调整学习率都无法收敛。
原因:数值梯度用的是中心差分,不容易出错。问题几乎都出在解析梯度里:SVM的binary矩阵忘了在正确类做负累加,或者Softmax里忘了除以num_train,又或者正则化梯度的reg*W被加到了错误的变量上。
解决:先写循环版解析梯度,用10个样本和3个类别做梯度检查,通过后再改成向量化版本。如果向量化版本和循环版对不上,就一步步替换矩阵乘法,每替换一行就检查一次。另外加一个简单自检:打印dW.shape,它必须和W.shape一致;如果一致但对不上数值,就检查margins[np.arange(num_train), y] = 0这一行有没有写到正确的位置。
5.2 kNN预测奇慢,训练集只有5000张也要跑几十秒
现象:测试集只有500个样本,compute_distances_two_loops要跑很久,交叉验证时几乎等不到结果。
原因:Python的双层for循环每次只算一个距离,500*5000等于250万个距离,每个距离都经过Python解释器,慢是正常的。
解决:立刻换no_loop向量化版本。先通过np.allclose校验两个距离矩阵一致,再把交叉验证里的调用全部切成no_loop。如果仍然慢,检查是否在循环里反复调用了self.X_train来取训练矩阵的副本;把train = self.X_train提到循环外能明显减少属性查找和潜在拷贝。
5.3 SVM损失第一轮就接近零,后面不变化
现象:第一次训练SVM,loss初始值只有个位数的小数,甚至接近0,之后整个训练过程正确率基本不变。
原因:权重初始化用了np.random.randn(D, C)没有乘小系数,导致初始分数很大。分数都远大于0,SVM的间隔被轻松满足,margin几乎全为0,所有样本的损失都是0,梯度也是0,模型直接锁死。
解决:把初始化改成* 0.001或* 1e-3,让初始分数尽量接近0。在训练前打印一次loss:如果loss为0,先检查 margin 计算是否正确,再检查初始化标准差是否过大。合理的初始loss应该在C-1到C附近,因为初始 margin 大约为1,错误类的期望 margin 接近1。
5.4 Softmax的loss在早期变成nan
现象:第一次跑Softmax,第一轮迭代就输出nan或inf,训练中断。
原因:最常见的是没有做scores -= np.max(scores, axis=1, keepdims=True),分数一旦超过710,exp(710)就会溢出成inf。另一个原因是学习率太大,权重一步跳太远,下一轮loss直接 nan。
解决:在softmax前显式减去每行最大值,确认数值稳定后再调学习率。当 nan 出现在第10轮而不是第1轮,优先怀疑学习率;当 nan 出现在第1轮,优先怀疑数值稳定性。如果已经加了最大值减法仍然nan,把学习率降到当前的十分之一再试。
5.5 交叉验证用测试集做选择,最后换数据就崩
现象:反复在测试集上试不同k、reg、learning_rate,每次测试集分数都在涨,但把模型拿到另一批同分布数据上正确率掉了超过10个点。
原因:测试集已经被你用来调参了,信息通过超参数选择泄漏进了模型,测试集失去评估泛化能力的作用。
解决:从第一次写代码开始就把测试集锁起来。正确的顺序是:先把测试集放到一边,在训练集上分折验证,选定超参数,再用全训练集训练最终模型,最后在测试集上跑一次并且只记录结果。如果你已经这么做了,那么看到的正确率才是可信的。
6. 拿到0.5之后:逼着自己多做三件小事
当两层网络在测试集上到了0.5甚至0.55,作业的核心目标已经达成。这时候建议多做三件小事,把这次作业的收获变成之后做简单图像分类能直接复用的习惯。
第一件是把训练过程可视化。loss曲线横轴用迭代次数,纵轴用每轮平均loss;曲线持续下降但有波动是正常的。再把训练正确率和验证正确率画在同一张图里,如果两者的距离越拉越大,说明过拟合在加剧,这时优先增大正则强度或扩充数据,而不是继续堆训练步数。
第二件是看混淆矩阵。用sklearn.metrics.confusion_matrix把验证集上的预测结果和真实标签统计成矩阵,通常CIFAR-10里猫和狗经常互相认错。看到这类系统性错误后,改进方向就变成了“增加特定类别的数据”或“调整特征”。这比只看正确率有用得多。
第三件是保存权重并做可视化。把第一层权重矩阵W1的每一行reshape回32x32x3并打印成小图,你会看见训练出的滤波器带上了边缘和色块结构。这个习惯能帮你在更大模型上判断“网络到底在学什么”。
一个我自己的教训:曾经为了赶时间,在交叉验证还没跑完的情况下直接拿测试集调参,最后测试集分数很好看,但换到另一份同分布数据后正确率掉了近10个点。从那以后我把测试集当成不可触碰的最终考卷,所有探索都在验证集上完成。如果你也正在写cs231n作业assignment1,做完kNN后先别急着进入下一章,刻意把交叉验证和测试集的边界守住,后面所有模型都会受益。希望帮到你。
本文还有配套的精品资源,点击获取