简介:本资源是斯坦福大学CS231n《计算机视觉中的卷积神经网络》课程Assignment 1的完整实践包,面向深度学习初学者与计算机视觉入门者,聚焦KNN、SVM、Softmax分类器及两层全连接网络的原理实现与调参训练。压缩包共54个文件,含17个核心Python源码(如layers.py、solver.py)、7个Jupyter Notebook(knn.ipynb、two_layer_net.ipynb等)、7个编译缓存文件及配套数据集(cifar-10-python.tar、data_batch_*等),另有Shell脚本、Markdown说明与IDE配置文件,整体结构清晰,开箱即用。资源大小为561.98MB,已支持Anaconda+Jupyter环境直接运行,无需额外下载依赖或手动解压数据。目前已有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 python=3.7 conda activate cs231n-a1 # 安装课程指定版本(来自 assignment1 原始 repo 的 requirements.txt) pip install numpy==1.19.5 matplotlib==3.3.4 pillow==8.1.0提示:不要跳过python=3.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 个训练 batch(data_batch_1 ~ data_batch_5)和 1 个测试 batch(test_batch)合并为X_train,y_train,X_test,y_test;最关键的预处理:对图像数据执行X_train = X_train.astype(np.float64)并除以 255.0 归一化,但不减均值、不除标准差——这与后续作业不同,是 assignment1 的设计特征。目录结构必须严格如下(否则data_utils.py会报FileNotFoundError):cs231n/ ├── 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_training=49000, num_validation=1000, num_test=1000) 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(关键!) # 对于正确类别 j=y[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)是正则 loss,reg * W是正则梯度;scores -= np.max(scores)是数值稳定化的黄金操作,避免np.exp(1000)这类溢出;梯度计算dW[:, j] += X[i] * (probs[j] - (j==y[i]))是核心:(probs[j] - 1)当j是正确类,否则probs[j]—— 这正是交叉熵对 softmax 输出的导数;dW /= N是因为 loss 是平均值,梯度也需平均。注意:这个函数是“naive”版,时间复杂度 O(N×C),仅用于验证逻辑。后续softmax_loss_vectorized用广播机制优化到 O(N+C),但梯度本质相同。3.2 两层神经网络:neural_net.py中的前向与反向传播链cs231n/classifiers/neural_net.py实现了一个带 ReLU 激活的两层全连接网络。其loss函数是整个 assignment1 的心脏,它串联了layers.py中的原子操作:def loss(self, X, y=None): # 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是带动量的 SGD,config中的momentum默认 0.9,learning_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返回loss=inf或nan原因:scores -= np.max(scores)未在每个样本内独立执行,而是对整个scores矩阵取了最大值。例如scores是(N, C),np.max(scores)返回标量,但若未用axis=1,会导致所有样本减同一个值,部分样本仍可能溢出。解决:确保scores -= np.max(scores, axis=1, keepdims=True),keepdims=True保持维度以便广播。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_scale=1e-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是 sum,lr * dW就会爆炸。解决:确认loss函数中dW /= N(N 是 batch_size)已执行。neural_net.py的loss函数末尾有dW1 += self.reg * W1,但前面必须有dW1 /= self.batch_size—— 这个除法常被忽略。4.5 现象:visualize_grid显示的权重图全黑或全白原因:W1是(D, H),D=3072(32×32×3),但visualize_grid期望输入是(H, D)或(H, 32, 32, 3)。直接传W1.T会错位。解决:正确 reshape:w = 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',预测时设为'test';gamma1,beta1是可学习参数,需加入self.params并在Solver的optim_configs中初始化(如gamma1 = np.ones(H));batchnorm_forward/backward需自行实现或从layers.py补充——其核心是计算mu = np.mean(x, axis=0),var = np.var(x, axis=0),再标准化。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, config=None): 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_rate=1e-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 --logdir=runs/cs231n_a1,即可在浏览器查看动态曲线。这比plt.plot()更适合长期实验追踪。从那以后我每次启动新实验,都强制走一遍tensorboard日志初始化 +git commit -m "init: baseline run"。不是为了留痕,而是因为 assignment1 教会我:可复现性不是终点,而是你每天开工的第一行代码。希望帮到你。本文还有配套的精品资源,点击获取
简介:本资源是斯坦福大学CS231n《计算机视觉中的卷积神经网络》课程Assignment 1的完整实践包,面向深度学习初学者与计算机视觉入门者,聚焦KNN、SVM、Softmax分类器及两层全连接网络的原理实现与调参训练。压缩包共54个文件,含17个核心Python源码(如layers.py、solver.py)、7个Jupyter Notebook(knn.ipynb、two_layer_net.ipynb等)、7个编译缓存文件及配套数据集(cifar-10-python.tar、data_batch_*等),另有Shell脚本、Markdown说明与IDE配置文件,整体结构清晰,开箱即用。资源大小为561.98MB,已支持Anaconda+Jupyter环境直接运行,无需额外下载依赖或手动解压数据。目前已有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 python=3.7 conda activate cs231n-a1 # 安装课程指定版本(来自 assignment1 原始 repo 的 requirements.txt) pip install numpy==1.19.5 matplotlib==3.3.4 pillow==8.1.0提示:不要跳过python=3.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 个训练 batch(data_batch_1 ~ data_batch_5)和 1 个测试 batch(test_batch)合并为X_train,y_train,X_test,y_test;最关键的预处理:对图像数据执行X_train = X_train.astype(np.float64)并除以 255.0 归一化,但不减均值、不除标准差——这与后续作业不同,是 assignment1 的设计特征。目录结构必须严格如下(否则data_utils.py会报FileNotFoundError):cs231n/ ├── 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_training=49000, num_validation=1000, num_test=1000) 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(关键!) # 对于正确类别 j=y[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)是正则 loss,reg * W是正则梯度;scores -= np.max(scores)是数值稳定化的黄金操作,避免np.exp(1000)这类溢出;梯度计算dW[:, j] += X[i] * (probs[j] - (j==y[i]))是核心:(probs[j] - 1)当j是正确类,否则probs[j]—— 这正是交叉熵对 softmax 输出的导数;dW /= N是因为 loss 是平均值,梯度也需平均。注意:这个函数是“naive”版,时间复杂度 O(N×C),仅用于验证逻辑。后续softmax_loss_vectorized用广播机制优化到 O(N+C),但梯度本质相同。3.2 两层神经网络:neural_net.py中的前向与反向传播链cs231n/classifiers/neural_net.py实现了一个带 ReLU 激活的两层全连接网络。其loss函数是整个 assignment1 的心脏,它串联了layers.py中的原子操作:def loss(self, X, y=None): # 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是带动量的 SGD,config中的momentum默认 0.9,learning_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返回loss=inf或nan原因:scores -= np.max(scores)未在每个样本内独立执行,而是对整个scores矩阵取了最大值。例如scores是(N, C),np.max(scores)返回标量,但若未用axis=1,会导致所有样本减同一个值,部分样本仍可能溢出。解决:确保scores -= np.max(scores, axis=1, keepdims=True),keepdims=True保持维度以便广播。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_scale=1e-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是 sum,lr * dW就会爆炸。解决:确认loss函数中dW /= N(N 是 batch_size)已执行。neural_net.py的loss函数末尾有dW1 += self.reg * W1,但前面必须有dW1 /= self.batch_size—— 这个除法常被忽略。4.5 现象:visualize_grid显示的权重图全黑或全白原因:W1是(D, H),D=3072(32×32×3),但visualize_grid期望输入是(H, D)或(H, 32, 32, 3)。直接传W1.T会错位。解决:正确 reshape:w = 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',预测时设为'test';gamma1,beta1是可学习参数,需加入self.params并在Solver的optim_configs中初始化(如gamma1 = np.ones(H));batchnorm_forward/backward需自行实现或从layers.py补充——其核心是计算mu = np.mean(x, axis=0),var = np.var(x, axis=0),再标准化。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, config=None): 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_rate=1e-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 --logdir=runs/cs231n_a1,即可在浏览器查看动态曲线。这比plt.plot()更适合长期实验追踪。从那以后我每次启动新实验,都强制走一遍tensorboard日志初始化 +git commit -m "init: baseline run"。不是为了留痕,而是因为 assignment1 教会我:可复现性不是终点,而是你每天开工的第一行代码。希望帮到你。本文还有配套的精品资源,点击获取