
简介这份资源是斯坦福CS231N计算机视觉公开课assignment1的完整作业包面向正在学习深度学习与图像分类的初学者及进阶者帮助解决从理论理解到代码实现之间的落地问题。包内共54个文件以py源码、ipynb交互式笔记本、pyc缓存、xml配置及tar、gz、zip等数据集压缩文件为主整体约561.98MB涵盖knn、svm、softmax、two_layer_net、features等核心实验模块并附带cifar-10数据集与提交脚本。已有2003人学习下载说明其在同类作业资料中具备一定参考价值。读者可借助现成答案对照自己的实现理解梯度检查、分类器构建、层模块封装与优化器编写等关键环节同时通过notebook直接运行调试快速定位报错原因适合作为课程作业自查与复习巩固的辅助材料。1. 从一份作业包说起为什么 cs231n assignment1 值得你花一个周末拆完如果你正在自学深度学习大概率经历过这样的阶段视频看了一堆反向传播的公式也能推但真让你从零写一个 Softmax 分类器或者两层神经网络手就停在键盘上了。cs231n 的 assignment1 就是冲着这个断层来的。它不是一份“看懂了就行”的讲义而是一套必须自己动手填代码的作业包覆盖 KNN、线性分类器SVM 和 Softmax、两层全连接网络以及反向传播的底层实现。每个 notebook 里都留了TODO标记配套有单元测试和梯度检查你写没写对跑一下就知道。适合谁刚学完神经网络基础、想用 NumPy 把前向和反向传播亲手推一遍的人也适合工作里调惯了框架、想回头补底层直觉的从业者。这份资源最大的价值在于它逼你把“梯度”这两个字从公式变成代码而不是继续当黑匣子用。2. 环境搭建与数据管线把 CIFAR-10 喂进 NumPy 的手里2.1 依赖选型与目录结构assignment1 的代码是纯 NumPy 实现不依赖 PyTorch 或 TensorFlow这一点很关键——它意味着你不需要配 GPU一台普通笔记本就能跑完。常见做法是建一个干净的虚拟环境把 Python 版本控制在 3.8 到 3.10 之间太新的版本有时会在pickle加载旧数据时出兼容问题。# 创建虚拟环境避免污染系统 Python python -m venv cs231n_env source cs231n_env/bin/activate # Windows 用 cs231n_env\Scripts\activate # 安装核心依赖版本不必锁死但 numpy 建议 1.21 以上 pip install numpy matplotlib jupyter scipy # 启动 notebook jupyter notebook逻辑说明venv隔离环境是为了避免和你机器上已有的科学计算包冲突尤其是numpy的版本。matplotlib用于可视化权重和损失曲线scipy在部分辅助函数里会用到。参数上Python 版本不建议低于 3.7否则 f-string 和类型注解会有问题numpy低于 1.20 时某些矩阵运算的广播行为有差异容易在梯度检查里出现莫名其妙的数值偏差。目录结构上作业包通常包含cs231n/目录存放分类器、层实现、数据加载工具、datasets/目录存放 CIFAR-10 的 pickle 文件、以及若干.ipynb文件。你需要确认datasets/cifar-10-batches-py/下确实有data_batch_1到data_batch_5以及test_batch缺一个都会在加载时报FileNotFoundError。2.2 数据加载与预处理别小看那几行归一化CIFAR-10 有 50000 张训练图、10000 张测试图每张 32x32x3。原始像素值是 0 到 255 的整数直接送进线性分类器会让梯度爆炸或收敛极慢。作业里通常要求做两步减均值、除标准差。import numpy as np import pickle import os def load_cifar10_batch(filepath): 加载单个 CIFAR-10 batch 文件 with open(filepath, rb) as f: # encodinglatin1 是为了兼容 Python 3 读取 Python 2 序列化的数据 datadict pickle.load(f, encodinglatin1) X datadict[data] # shape: (10000, 3072) Y datadict[labels] # shape: (10000,) # 从 (N, 3072) 重塑为 (N, 3, 32, 32)再转置为 (N, 32, 32, 3) X X.reshape(10000, 3, 32, 32).transpose(0, 2, 3, 1).astype(float) Y np.array(Y) return X, Y def preprocess(X_train, X_test): 减均值、除标准差按通道独立计算 mean_image np.mean(X_train, axis0) # shape: (32, 32, 3) X_train - mean_image X_test - mean_image std_image np.std(X_train, axis0) X_train / std_image X_test / std_image return X_train, X_test逻辑说明reshape和transpose的顺序不能错CIFAR-10 原始存储是通道优先channel-first而作业里的可视化函数通常期望通道在后channel-last。encodinglatin1是血泪经验不加这一句在 Python 3 下直接报UnicodeDecodeError。减均值时均值必须从训练集算然后同时应用到训练集和测试集——如果测试集用自己的均值等于提前泄露了测试分布信息梯度检查可能通过但实际泛化会虚高。除标准差同理而且标准差为 0 的像素比如某些角落永远是背景会导致除零常见做法是加一个极小值1e-8兜底。提示预处理后的数据建议用np.save存一份下次直接加载省去每次 pickle 解析的时间。3. 从 KNN 到 Softmax把分类器写成可验证的代码3.1 KNN 的向量化实现与距离矩阵陷阱KNN 在 assignment1 里是热身但它的向量化实现是很多人第一次接触“广播”和“内存换速度”的地方。两层循环版本好写但慢完全向量化版本需要用到(X - X_train)^2 X^2 X_train^2 - 2 * X X_train.T这个展开。def compute_distances_no_loops(X, X_train): 完全向量化的 L2 距离计算 # X: (N, D), X_train: (M, D) X_sq np.sum(X ** 2, axis1, keepdimsTrue) # (N, 1) X_train_sq np.sum(X_train ** 2, axis1) # (M,) cross_term X X_train.T # (N, M) # 广播X_sq X_train_sq - 2 * cross_term dists X_sq X_train_sq - 2 * cross_term # 数值误差可能产生极小负数开方前裁剪 dists np.maximum(dists, 0) return np.sqrt(dists)逻辑说明这个展开式把 O(NMD) 的循环降成了矩阵乘法速度提升几十倍。但坑在于浮点误差——当两个向量非常接近时X_sq X_train_sq - 2 * cross_term可能算出 -1e-12 这样的负数直接sqrt会得到nan。np.maximum(dists, 0)是后悔药必须加。参数上keepdimsTrue是为了让X_sq保持 (N, 1) 形状广播到 (N, M) 时不会出错。如果你用np.sum不加keepdims得到的是 (N,)广播结果会变成 (M, N) 或者直接报错这是新手最常见的翻车点。KNN 的 k 值选择也有讲究。k1 时对噪声极敏感k 太大则决策边界过度平滑。作业里通常让你在验证集上扫一遍常见做法是取 k 在 1 到 20 之间步长 2画一条准确率曲线。CIFAR-10 上 KNN 的准确率天花板大概在 35% 到 40% 之间再调也上不去因为像素级 L2 距离本身就不是好的语义度量。3.2 SVM 与 Softmax 的损失函数 hinge 和 cross-entropy 的数值稳定性线性分类器的核心是损失函数和梯度。SVM 用 hinge lossSoftmax 用 cross-entropy。两者都要处理数值稳定性尤其是 Softmax 的指数运算。def softmax_loss_vectorized(W, X, y, reg): 向量化 Softmax 损失与梯度 num_train X.shape[0] scores X W # (N, C) # 数值稳定减去每行最大值 scores_shifted scores - np.max(scores, axis1, keepdimsTrue) exp_scores np.exp(scores_shifted) probs exp_scores / np.sum(exp_scores, axis1, keepdimsTrue) # 交叉熵损失 correct_logprobs -np.log(probs[np.arange(num_train), y] 1e-12) loss np.sum(correct_logprobs) / num_train 0.5 * reg * np.sum(W ** 2) # 梯度 dscores probs.copy() dscores[np.arange(num_train), y] - 1 dscores / num_train dW X.T dscores reg * W return loss, dW逻辑说明scores_shifted那一步是必须的因为exp(1000)会溢出成inf减去最大值后指数最大是exp(0)1不会溢出。probs[np.arange(num_train), y]是花式索引取出每个样本对应正确类别的概率。 1e-12是防止log(0)变成-inf。梯度里dscores先复制再减 1不能直接改probs否则下一次迭代会用到被污染的probs。reg是正则化强度典型值在 1e-5 到 1e-3 之间太大欠拟合太小过拟合。学习率一般从 1e-3 开始试配合学习率衰减。SVM 的 hinge loss 类似但梯度只在margin 1的样本上非零。作业里会让你实现svm_loss_naive和svm_loss_vectorized然后对比两者结果是否一致。梯度检查用数值梯度中心差分和解析梯度对比相对误差小于 1e-8 算通过。如果误差在 1e-4 量级通常是某个地方少了1/N或者正则化项没加对。注意梯度检查时要把正则化强度调小比如 0.1 甚至 0因为数值梯度对正则化项的敏感度不同容易误判。4. 两层神经网络与反向传播手写每一层的梯度4.1 前向传播的模块化设计两层网络的结构是affine - relu - affine - softmax。作业要求你把每一层拆成独立的函数前向返回缓存反向用缓存算梯度。这种设计模式是后面所有深度学习框架的雏形。def affine_forward(x, w, b): 全连接层前向 N x.shape[0] x_flat x.reshape(N, -1) # 展平兼容卷积层输出 out x_flat w b cache (x_flat, w, b) return out, cache def relu_forward(x): ReLU 前向 out np.maximum(0, x) cache x return out, cache def affine_relu_forward(x, w, b): affine relu 组合前向 a, fc_cache affine_forward(x, w, b) out, relu_cache relu_forward(a) cache (fc_cache, relu_cache) return out, cache逻辑说明x.reshape(N, -1)是为了让全连接层不关心输入是图像还是特征只要总维度对得上就行。cache存的是反向传播需要的中间变量——affine需要输入、权重、偏置来算dW和dbrelu需要输入来算掩码。组合函数affine_relu_forward把两个 cache 打包反向时按相反顺序解包。参数初始化上权重用np.random.randn(D, H) * np.sqrt(2.0 / D)He 初始化偏置置零。如果用0.01这种小常数初始化深层网络会梯度消失如果用1.0激活值会爆炸。4.2 反向传播的链式法则与梯度形状校验反向传播最容易错的是形状。dW的形状必须和W一致db和b一致dx和x一致。作业里的grad_check会帮你验但自己心里要有数。def affine_backward(dout, cache): 全连接层反向 x_flat, w, b cache dx_flat dout w.T # (N, D) dw x_flat.T dout # (D, H) db np.sum(dout, axis0) # (H,) dx dx_flat.reshape(x_flat.shape) # 恢复原始形状 return dx, dw, db def relu_backward(dout, cache): ReLU 反向 x cache dx dout * (x 0) # 小于等于 0 的位置梯度为 0 return dx逻辑说明dx_flat dout w.T来自链式法则dout是 (N, H)w.T是 (H, D)乘出来 (N, D)。dw x_flat.T dout是 (D, N) (N, H) (D, H)。db是把dout按样本维度求和因为偏置对每个样本都加了同一个值。relu_backward里x 0产生布尔掩码乘dout时自动转成 0/1。注意x 0的位置梯度取 0这是 ReLU 的次梯度约定实际影响不大但作业里通常按 0 处理。两层网络的超参数调优是个体力活。隐藏层大小从 50 到 500 扫学习率从 1e-4 到 1e-1 按对数刻度扫正则化强度从 1e-6 到 1e-2 扫。常见做法是先用小规模数据比如 500 张图快速筛掉明显不行的组合再在全量数据上跑最优的几个。CIFAR-10 上两层网络能到 50% 左右的验证准确率再往上就需要卷积了。提示训练时把损失曲线画出来如果损失不降先查学习率是不是太大如果损失降了但验证准确率不涨查正则化是不是太强。5. 避坑与排查那些让梯度检查失败的小细节5.1 梯度检查相对误差偏大现象数值梯度和解析梯度的相对误差在 1e-3 到 1e-2 之间远超 1e-8 的通过线。原因通常有三个一是relu在 0 附近的次梯度选择不一致数值梯度用中心差分时跨越了不可导点二是正则化项在数值梯度里没算对比如0.5 * reg * sum(W^2)的导数应该是reg * W漏了系数三是softmax的1/N归一化在数值梯度里重复计算。解决先把reg设为 0排除正则化干扰再把relu的输入避开 0 附近或者接受 1e-6 量级的误差最后逐层检查从affine单独开始验再组合。5.2 训练损失变成 nan现象迭代几次后损失突然变成nan准确率掉到随机水平。原因学习率太大导致权重爆炸或者softmax的指数溢出。解决把学习率降一个数量级比如从 1e-2 降到 1e-3确认softmax里减了每行最大值检查输入数据有没有nan或inf预处理时除标准差如果遇到 0 会出inf加1e-8兜底。5.3 KNN 跑得太慢现象compute_distances_two_loops在 50000 张训练集上跑一次要几分钟。原因双重循环是 O(NMD)50000500003072 的量级。解决用向量化版本但注意内存——(50000, 50000)的float64矩阵是 20GB普通机器扛不住。常见做法是分批计算每次算 1000 个测试样本对 50000 个训练样本或者用float32省一半内存。作业里通常只要求在小规模子集上验证正确性全量跑 KNN 本身意义不大。5.4 权重可视化一片模糊现象把Wreshape 成图像后看不出任何结构。原因学习率太小或迭代次数不够权重还没学到东西或者正则化太强权重被压得太小。解决先确认损失在下降如果损失曲线平坦调大学习率如果损失降了但权重还是噪声减少正则化强度。正常训练后SVM 的权重图应该能看出类似模板的形状比如马的头、车的轮廓。5.5 验证集准确率波动大现象同样的超参数跑两次结果差好几个百分点。原因权重随机初始化不同或者数据 shuffle 的顺序不同。解决固定随机种子np.random.seed(42)在训练前 shuffle 一次数据并固定顺序。如果波动仍然大说明模型对初始化太敏感试试 He 初始化或者减小隐藏层规模。6. 进阶技巧把 assignment1 的代码变成你的实验底座assignment1 写完之后最可惜的做法是关掉 notebook 再也不看。我一般会把它改成一个可复用的实验框架把cs231n/classifiers/下的linear_classifier.py和neural_net.py抽出来加一个train()函数支持早停和模型保存然后用它跑一些 assignment1 之外的小实验。比如把 CIFAR-10 换成 Fashion-MNIST输入维度从 3072 变成 784其他代码几乎不用改就能验证你对数据管线的理解是不是真的到位。另一个值得做的进阶是手动实现批量归一化Batch Normalization。assignment1 里没有但你可以把它插在affine和relu之间前向算均值和方差反向推梯度。这个练习能让你彻底搞懂 BN 为什么能加速训练以及训练和测试阶段的行为差异。代码量不大但梯度推导比affine复杂是检验反向传播功力的好题目。验证方法上我习惯用一个小 trick把训练集切出 10% 做验证训练集用剩下的 90%跑完看验证准确率和测试准确率的差距。如果差距超过 5 个百分点说明过拟合了加正则化或者减模型容量如果两者都低说明欠拟合加层或者加宽度。这个习惯从那以后我每次调参都强制走一遍省了很多盲目试错的时间。提示把每次实验的超参数、损失曲线、验证准确率记到一个表格里比凭记忆靠谱得多。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。