资讯动态

cs231n Assignment1 实战指南:用NumPy从零实现神经网络

发布时间:2026/10/21 2:57:56 来源:尧图企业网站定制
简介本资源是斯坦福大学CS231n《计算机视觉中的卷积神经网络》课程Assignment 1的完整实践包面向深度学习初学者与计算机视觉入门者聚焦KNN、SVM、Softmax分类器及两层全连接网络的原理实现与调参训练。压缩包共54个文件含17个核心Python源码如layers.py、solver.py、7个Jupyter Notebookknn.ipynb、two_layer_net.ipynb等、7个编译缓存文件及配套数据集cifar-10-python.tar、data_batch_*等另有Shell脚本、Markdown说明与IDE配置文件整体结构清晰开箱即用。资源大小为561.98MB已支持AnacondaJupyter环境直接运行无需额外下载依赖或手动解压数据。目前已有2005人学习下载读者可一次性获得作业题目、参考实现、完整数据集、提交工具collect_submission.py及可视化辅助模块vis_utils.py显著降低环境配置门槛加速从理论到代码落地的学习闭环。1. 这不是一份“交完就扔”的作业cs231n assignment1 是你亲手搭起第一个神经网络的脚手架想象一下你刚学完反向传播的数学推导公式写满三页草稿纸但心里还是发虚——那些 ∂L/∂W 真的能在内存里跑起来吗梯度真的会按链式法则一层层回传而不是在某处突然炸成 nan 或缩成 0 吗cs231n assignment1 就是那个“必须亲手拧紧每一颗螺丝”的实战关卡。它用纯 NumPy 实现 Softmax 分类器、两层全连接网络、带 Dropout 和 BatchNorm 的完整训练流程不依赖 PyTorch 的自动求导黑匣子逼你把 forward、backward、weight update 拆成可调试、可断点、可 print 的 Python 函数。它适合所有想甩掉“调包侠”标签的人新手靠它建立对 loss、grad、update 的肌肉记忆老手拿它当探针去验证自己对优化器行为、初始化策略、数值稳定性的直觉是否成立。这不是课程配套练习这是你深度学习工程能力的第一次压力测试——跑通不难但想让 train_acc 55%、val_acc 48%、loss 曲线平滑下降每一步都得亲手校准。2. 环境与数据准备从零拉起一个可复现的 NumPy 深度学习沙盒2.1 依赖安装与版本锁定为什么 conda pinned requirements 是血泪经验cs231n assignment1 对环境极其敏感。我见过太多人卡在numpy 1.24的np.float64类型变更上——softmax函数里np.exp(x - np.max(x))因为广播规则微调直接返回全 0也有人用matplotlib 3.8导致visualize_grid报AxesSubplot属性错误。正确做法是严格复刻原始环境# 创建干净环境推荐 conda避免 pip 混乱 conda create -n cs231n-a1 python3.7 conda activate cs231n-a1 # 安装课程指定版本来自 assignment1 原始 repo 的 requirements.txt pip install numpy1.19.5 matplotlib3.3.4 pillow8.1.0提示不要跳过python3.7。assignment1 的data_utils.py里大量使用urllib.request.urlretrieve下载 CIFAR-10而新版 urllib 在 Python 3.8 中对 SSL 上下文处理更严格容易触发URLError: urlopen error [SSL: CERTIFICATE_VERIFY_FAILED]。用 3.7 可绕过此坑且与课程原始运行环境完全一致。2.2 数据集下载与结构解析CIFAR-10 不只是 50000 张图assignment1 使用 CIFAR-10 数据集但它的加载逻辑藏在cs231n/data_utils.py中。关键点在于数据不是以原始 PNG/JPG 形式存在而是序列化为 Python pickle 格式。当你运行get_CIFAR10_data()时它会检查cs231n/datasets/cifar-10-batches-py/是否存在若不存在自动从官方服务器下载cifar-10-python.tar.gz约 170MB解压后将 5 个训练 batchdata_batch_1 ~ data_batch_5和 1 个测试 batchtest_batch合并为X_train,y_train,X_test,y_test最关键的预处理对图像数据执行X_train X_train.astype(np.float64)并除以 255.0 归一化但不减均值、不除标准差——这与后续作业不同是 assignment1 的设计特征。目录结构必须严格如下否则data_utils.py会报FileNotFoundErrorcs231n/ ├── data_utils.py # 加载核心逻辑 ├── datasets/ │ └── cifar-10-batches-py/ # 解压后的文件夹含 data_batch_1~5, test_batch, batches.meta ├── classifiers/ │ ├── __init__.py │ ├── softmax.py # Softmax 分类器实现 │ └── neural_net.py # 两层神经网络实现 ├── layers.py # 全连接、ReLU、Softmax loss 层 └── solver.py # 训练循环控制器2.3 验证数据加载三行代码确认你的沙盒已就绪别急着跑训练先用最小代码验证数据流是否通畅import numpy as np from cs231n.data_utils import get_CIFAR10_data # 加载数据首次运行会触发下载 data get_CIFAR10_data(num_training49000, num_validation1000, num_test1000) print(X_train shape:, data[X_train].shape) # 应为 (49000, 32, 32, 3) print(y_train shape:, data[y_train].shape) # 应为 (49000,) print(Class names:, data[class_names]) # [plane, car, ..., truck]如果输出X_train shape: (49000, 32, 32, 3)恭喜——你的数据管道已打通。此时data[X_train]是float64类型的四维数组每个像素值在[0.0, 1.0]区间这是后续所有层计算的起点。3. 核心模块拆解从 softmax.py 到 neural_net.py 的逐层实现逻辑3.1 Softmax 分类器理解loss与grad如何共生于同一函数cs231n/classifiers/softmax.py中的softmax_loss_naive是 assignment1 的第一道硬坎。它不追求速度只求逻辑清晰。我们来拆解其loss和grad计算如何耦合def softmax_loss_naive(W, X, y, reg): # W: (D, C), X: (N, D), y: (N,) N X.shape[0] loss 0.0 dW np.zeros_like(W) # 初始化梯度矩阵 for i in range(N): # Step 1: 计算第 i 个样本的得分向量 scores X[i].dot(W) # (C,) # Step 2: 数值稳定化减去最大值防止 exp 溢出 scores - np.max(scores) # Step 3: 计算 softmax 概率 exp_scores np.exp(scores) probs exp_scores / np.sum(exp_scores) # (C,) # Step 4: 计算单样本 loss交叉熵 correct_logprob -np.log(probs[y[i]]) loss correct_logprob # Step 5: 计算单样本梯度 dW关键 # 对于正确类别 jy[i]dL/dW[:,j] X[i] * (probs[j] - 1) # 对于错误类别 j≠y[i]dL/dW[:,j] X[i] * probs[j] for j in range(W.shape[1]): if j y[i]: dW[:, j] X[i] * (probs[j] - 1) else: dW[:, j] X[i] * probs[j] # Step 6: 平均 loss 加正则项 loss / N loss 0.5 * reg * np.sum(W * W) # Step 7: 平均梯度 加正则梯度 dW / N dW reg * W return loss, dW逻辑说明与参数意义reg是 L2 正则化强度如1e-3它惩罚大权重防止过拟合。0.5 * reg * np.sum(W*W)是正则 lossreg * W是正则梯度scores - np.max(scores)是数值稳定化的黄金操作避免np.exp(1000)这类溢出梯度计算dW[:, j] X[i] * (probs[j] - (jy[i]))是核心(probs[j] - 1)当j是正确类否则probs[j]—— 这正是交叉熵对 softmax 输出的导数dW / N是因为 loss 是平均值梯度也需平均。注意这个函数是“naive”版时间复杂度 O(N×C)仅用于验证逻辑。后续softmax_loss_vectorized用广播机制优化到 O(NC)但梯度本质相同。3.2 两层神经网络neural_net.py中的前向与反向传播链cs231n/classifiers/neural_net.py实现了一个带 ReLU 激活的两层全连接网络。其loss函数是整个 assignment1 的心脏它串联了layers.py中的原子操作def loss(self, X, yNone): # X: (N, D), y: (N,) or None (预测模式) W1, b1 self.params[W1], self.params[b1] # (D, H), (H,) W2, b2 self.params[W2], self.params[b2] # (H, C), (C,) # --- Forward Pass --- # Layer 1: affine - ReLU a1, cache_a1 affine_forward(X, W1, b1) # a1 X W1 b1 h1, cache_h1 relu_forward(a1) # h1 max(0, a1) # Layer 2: affine - softmax loss scores, cache_scores affine_forward(h1, W2, b2) # scores h1 W2 b2 if y is None: return scores # 预测模式只返回得分 # --- Compute Loss --- loss, dscores softmax_loss(scores, y) # dscores: (N, C), 损失对 scores 的梯度 # --- Backward Pass --- # Layer 2 backward dh1, dW2, db2 affine_backward(dscores, cache_scores) # dh1: (N, H) # Layer 1 backward (ReLU - affine) da1 relu_backward(dh1, cache_h1) # da1: (N, H) dX, dW1, db1 affine_backward(da1, cache_a1) # dX: (N, D), 通常丢弃 # --- Regularization --- loss 0.5 * self.reg * (np.sum(W1*W1) np.sum(W2*W2)) dW1 self.reg * W1 dW2 self.reg * W2 grads {W1: dW1, b1: db1, W2: dW2, b2: db2} return loss, grads关键设计点cache_*是前向过程中的中间变量如X, W1, b1供反向时复用避免重复计算softmax_loss返回dscores即损失对最后一层输出的梯度这是反向传播的起点affine_backward和relu_backward是layers.py中定义的原子反向函数它们接收dout上游梯度和cache返回dx, dW, db梯度累加顺序不可逆必须先算dh1再用dh1算da1最后算dW1/db1。任何一步中断梯度链就断裂。3.3 Solver 训练器solver.py如何把 loss 函数变成可收敛的模型solver.py是 assignment1 的“胶水”模块它封装了完整的训练循环。其train()方法核心逻辑如下def train(self): num_train self.X_train.shape[0] iterations_per_epoch max(num_train // self.batch_size, 1) num_iterations self.num_epochs * iterations_per_epoch for t in range(num_iterations): # 1. Mini-batch sampling batch_mask np.random.choice(num_train, self.batch_size) X_batch self.X_train[batch_mask] y_batch self.y_train[batch_mask] # 2. Forward backward pass loss, grads self.model.loss(X_batch, y_batch) self.loss_history.append(loss) # 3. Update parameters (SGD with momentum) for p, w in self.model.params.items(): dw grads[p] config self.optim_configs[p] next_w, next_config sgd_momentum(w, dw, config) # 核心更新函数 self.model.params[p] next_w self.optim_configs[p] next_config # 4. Epoch end: compute train/val accuracy if t % iterations_per_epoch 0: train_acc self.check_accuracy(self.X_train, self.y_train) val_acc self.check_accuracy(self.X_val, self.y_val) self.train_acc_history.append(train_acc) self.val_acc_history.append(val_acc)参数说明与调试技巧self.batch_size默认 200太小导致梯度噪声大太大显存不够但 assignment1 是 CPU 训练主要影响收敛速度sgd_momentum是带动量的 SGDconfig中的momentum默认 0.9learning_rate默认1e-3——这是 assignment1 最易调参的两个超参check_accuracy每 epoch 跑一次但注意它用的是全量训练集所以train_acc会随 epoch 缓慢上升而val_acc才是泛化能力指标如果val_acc卡在 40% 不动大概率是learning_rate太大loss 振荡或太小收敛极慢建议在1e-3~5e-3间网格搜索。4. 避坑指南我在 assignment1 上踩过的 5 个真实翻车现场4.1 现象softmax_loss_naive返回lossinf或nan原因scores - np.max(scores)未在每个样本内独立执行而是对整个scores矩阵取了最大值。例如scores是(N, C)np.max(scores)返回标量但若未用axis1会导致所有样本减同一个值部分样本仍可能溢出。解决确保scores - np.max(scores, axis1, keepdimsTrue)keepdimsTrue保持维度以便广播。4.2 现象neural_net训练时loss初期下降随后突然飙升至1e10原因W1,W2初始化过大如np.random.randn(D,H)未缩放导致第一层a1 X W1 b1输出极大ReLU后h1极大第二层scores h1 W2 b2溢出softmax计算失败。解决严格使用课程初始化W1 np.random.randn(D, H) * weight_scale其中weight_scale1e-3或1e-2。weight_scale是 assignment1 的隐藏超参调它比调lr更有效。4.3 现象val_acc始终低于train_acc15% 以上且不随 epoch 改善原因忘记在Solver.train()中对验证集做与训练集相同的预处理。X_val加载后是[0,255]整数但X_train已归一化为[0.0,1.0]直接喂给模型会导致数值范围错乱。解决检查get_CIFAR10_data()返回的data[X_val]是否已除以 255.0。若未处理在Solver.__init__()中手动补self.X_val self.X_val.astype(np.float64) / 255.0。4.4 现象sgd_momentum更新后W的范数持续增大最终nan原因dW未除以batch_size。softmax_loss和neural_net.loss中的梯度dW是 mini-batch 的和但sgd_momentum函数默认将其当作平均梯度处理。若dW是 sumlr * dW就会爆炸。解决确认loss函数中dW / NN 是 batch_size已执行。neural_net.py的loss函数末尾有dW1 self.reg * W1但前面必须有dW1 / self.batch_size—— 这个除法常被忽略。4.5 现象visualize_grid显示的权重图全黑或全白原因W1是(D, H)D307232×32×3但visualize_grid期望输入是(H, D)或(H, 32, 32, 3)。直接传W1.T会错位。解决正确 reshapew self.params[W1].T.reshape(-1, 3, 32, 32).transpose(0,2,3,1)再传入visualize_grid(w)。transpose(0,2,3,1)将(N, C, H, W)转为(N, H, W, C)以匹配 matplotlib。5. 二次开发实战如何把 assignment1 改造成你的个人深度学习实验台5.1 添加 Batch Normalization三步注入现代训练技巧assignment1 原生不支持 BatchNorm但它是提升小网络性能的关键。我们可以在neural_net.py的loss函数中插入# 在 forward pass 中Layer 1 后添加 BN a1, cache_a1 affine_forward(X, W1, b1) # 新增BN forward a1_bn, cache_bn batchnorm_forward(a1, gamma1, beta1, bn_param1) # gamma1/beta1 是新参数 h1, cache_h1 relu_forward(a1_bn) # 在 backward pass 中Layer 1 backward 前添加 BN backward dh1 relu_backward(dh1, cache_h1) # 注意dh1 现在是 BN 输入的梯度 da1_bn, dgamma1, dbeta1 batchnorm_backward(dh1, cache_bn) # 新增 da1, dW1, db1 affine_backward(da1_bn, cache_a1) # 输入改为 da1_bn关键改造点bn_param1是字典{mode: train}训练时需在Solver.train()中设为train预测时设为testgamma1,beta1是可学习参数需加入self.params并在Solver的optim_configs中初始化如gamma1 np.ones(H)batchnorm_forward/backward需自行实现或从layers.py补充——其核心是计算mu np.mean(x, axis0),var np.var(x, axis0)再标准化。5.2 替换优化器用 Adam 替代 SGD Momentum 的 5 行代码sgd_momentum是基础但 Adam 更鲁棒。只需修改solver.py中的update_rule# 替换 sgd_momentum 调用为 adam next_w, next_config adam(w, dw, config) # config 需含 m, v, t # adam 函数放在 solver.py 顶部 def adam(w, dw, configNone): if config is None: config {} config.setdefault(learning_rate, 1e-3) config.setdefault(beta1, 0.9) config.setdefault(beta2, 0.999) config.setdefault(epsilon, 1e-8) config.setdefault(m, np.zeros_like(w)) config.setdefault(v, np.zeros_like(w)) config.setdefault(t, 0) config[t] 1 config[m] config[beta1] * config[m] (1 - config[beta1]) * dw config[v] config[beta2] * config[v] (1 - config[beta2]) * (dw**2) m_hat config[m] / (1 - config[beta1]**config[t]) v_hat config[v] / (1 - config[beta2]**config[t]) next_w w - config[learning_rate] * m_hat / (np.sqrt(v_hat) config[epsilon]) return next_w, config效果对比在相同learning_rate1e-3下Adam 通常比 SGD Momentum 快 2~3 倍收敛且对lr不敏感——这是你后续实验的“后悔药”。5.3 可视化增强用 TensorBoard 记录 loss/acc 曲线无需改模型assignment1 是纯 NumPy但你可以用tensorboardX轻量级 TensorBoard 接口记录训练过程# 在 solver.py 开头 from tensorboardX import SummaryWriter writer SummaryWriter(runs/cs231n_a1) # 在 train() 循环中epoch 结束时 writer.add_scalar(Loss/train, loss, t) writer.add_scalar(Accuracy/train, train_acc, t) writer.add_scalar(Accuracy/val, val_acc, t) # 记录权重分布诊断梯度消失 writer.add_histogram(Weights/W1, self.model.params[W1], t)运行tensorboard --logdirruns/cs231n_a1即可在浏览器查看动态曲线。这比plt.plot()更适合长期实验追踪。从那以后我每次启动新实验都强制走一遍tensorboard日志初始化 git commit -m init: baseline run。不是为了留痕而是因为 assignment1 教会我可复现性不是终点而是你每天开工的第一行代码。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑