资讯动态

CS231n作业Softmax分类器全解析:从公式推导到代码实现

发布时间:2026/10/14 14:50:22 来源:尧图企业网站定制
CS231n的Assignment 1光是Softmax这一节就劝退了不少刚入坑深度学习的朋友。我自己当初做这份作业时也是白天调代码、晚上推公式卡在梯度推导上整整两天。回头再看其实Softmax分类器是整个课程里性价比最高的一块内容它把线性分类、损失函数、梯度反传、数值检查、超参数调参这几个核心环节全部串了一遍你把这一个作业吃透后面Assignment 2里的全连接网络、BatchNorm甚至Assignment 3里的Transformer很多概念都是从这里延伸出去的。这篇文章就把我当时做这份作业的完整思路、推导过程和踩坑记录重新梳理一遍给正在啃这道题的同学一个可以照着走的参考。先说清楚这份作业到底是什么。CS231n课程的第一份作业包含KNN、SVM、Softmax、两层神经网络和图像特征实验五个部分其中Softmax分类器要求你独立完成两个版本的损失函数与梯度实现再用梯度检查验证正确性最后在CIFAR-10数据集上通过随机梯度下降训练出一个图像分类器。它适合刚学完Python和NumPy、对机器学习基础概念有初步了解、但还没真正动手写过分类器的读者。做完之后你不仅会掌握Softmax的数学原理更重要的是会建立起从公式到代码的完整映射能力。1. 先拆解作业Softmax这一题到底要你做什么1.1 任务清单全景我拿到作业代码框架后做的第一件事是把TODO注释全部列出来。Softmax部分的核心任务可以拆成四块第一实现softmax_loss_naive函数。输入是训练数据矩阵X形状为N×D、标签y长度为N、权重矩阵W形状为C×D和正则化强度reg输出是损失值loss和梯度dW、db。这个版本允许你用最直观的循环写法一个样本一个样本地算目的是让你把数学公式翻译成代码。第二实现向量化版本softmax_loss_vectorized。同样的功能但要求去掉所有显式循环用NumPy矩阵运算一次性算出所有样本的得分、概率和梯度。作业要求你对比两个版本的结果一致性并统计运行时间差异。第三用数值梯度检查验证分析梯度。作业自带了一个grad_check工具用中心差分法逼近梯度然后和你推导的解析梯度做相对误差比较。误差小于1e-6左右就算通过这一步能精准揪出你在链式法则推导中犯的错。第四用SGD迭代训练在验证集上调节学习率和正则化强度最后在测试集上报告准确率。作业还要求你把每个类别的权重矩阵可视化出来观察模型学到的模板图像。1.2 为什么Softmax值得单独花时间很多初学者会觉得Softmax不就是SVM损失换了个公式吗为什么不直接写神经网络我当初也有这个疑惑但做完之后才理解课程设计者的意图。Softmax分类器是逻辑回归在多分类问题上的推广它的输出是归一化的概率分布这个概率分布直接对接了后面所有深度学习模型里最常用的交叉熵损失。你在Assignment 2里写两层神经网络时最后一层用的还是Softmax 交叉熵写PyTorch时nn.CrossEntropyLoss底层干的事和你在这个作业里手写的一模一样。所以现在把梯度推导搞清楚后面所有作业的损失函数部分你都不会再发怵。另外这份作业的训练管线——数据预处理、分批量采样、前向计算损失、反向算梯度、参数更新、周期性打印损失——就是日后训练任何深度学习模型的最小雏形。你在Softmax里跑通这条流水线后面加隐藏层、加卷积、加注意力机制都只是往这条管线的中间插模块而已。2. Softmax分类器原理从线性得分到概率分布2.1 线性分类器的决策逻辑先把最底层的部分讲透。Softmax分类器本质上还是一个线性分类器它假设每个类别的得分是输入特征的线性函数。对于一张拉平后的图片xCIFAR-10里是3072维第k类的得分是s_k W[k] · x b[k]把所有类别的得分堆叠起来就是s Wx b。这里W是一个C×D矩阵每一行对应一个类别的权重向量。从这个角度看分类器其实是在学习每一类的一个模板得分高低取决于输入图片和哪个模板更相似。作业最后让你可视化的权重图像就是这些模板。这里有个值得注意的点线性分类器要求数据是拉平的一维向量所以CIFAR-10的32×32×3图像会被reshape成3072维。这意味着模型完全丢失了空间结构信息——图片左上角和右下角的像素被一视同仁地当作独立特征。这也是为什么线性分类器在CIFAR-10上只能做到30%-40%的准确率它只能捕捉颜色分布这类全局统计特征根本理解不了猫耳朵在头的两侧这种空间关系。理解这个局限你就能明白神经网络为什么要用卷积来保留空间信息。2.2 Softmax函数与交叉熵损失得分本身是实数范围不定不能直接当概率用。Softmax做的事情就是把得分向量转成概率分布p_k exp(s_k) / Σ_j exp(s_j)这个变换有两个关键性质。第一所有概率都为正且和为1符合概率的公理化定义第二它是一个平滑的软最大值函数得分最高的类别概率最大但其他类别仍然保留非零概率这保留了类别之间的细粒度差异。有了概率分布损失函数用交叉熵来定义。对第i个样本如果它的真实类别是y_i损失是L_i -log(p_{y_i})这个公式的含义很直观真实类别对应的概率越接近1损失越接近0模型越自信地预测错损失越大。注意这里是-log而不是-(y·log(p))的求和形式因为对于单标签分类one-hot编码的标签向量里只有一个位置是1求和之后剩下的就是真实类别那一项的负对数。对所有样本取平均再加上L2正则化项就得到完整损失L (1/N) Σ_i -log(p_{y_i}) λ Σ_{k,j} W[k,j]²正则化项很重要它约束权重不能太大防止模型过分依赖某些特征维度降低过拟合。作业框架里已经帮你把正则化部分写好了你只需要在损失里加上即可。2.3 数值稳定性为什么必须先减去最大值直接按公式计算exp(s_k)有个隐患当某个得分很大时比如s_k 1000exp(1000)在IEEE 754双精度浮点数下直接溢出成inf后续的除法就全乱了。作业里用的数据经过标准化后不太可能出现这种情况但这是一个必须养成的工程习惯。解决办法是Softmax函数的一个数学性质对得分向量同时减去任意常数概率分布不变。证明很简单因为减去的常数在分子分母的指数上同时出现可以约掉p_k exp(s_k - c) / Σ_j exp(s_j - c)工程上最常见的做法是取类别得分里的最大值作为c这样所有指数项的输入都≤0最大的是exp(0)1彻底杜绝溢出。我当时第一次写代码时忘了这个细节后来在调试一个特征值很大的模型时直接看到了NaN损失才意识到这个操作有多重要。提示在实现Softmax时不管数据看起来多正常永远先减最大值。这个习惯能帮你省掉无数排查NaN的时间。3. 梯度推导从损失函数到dW和db3.1 链式法则的手算过程梯度是整个作业最核心也最容易出错的部分。我当初推导时走了不少弯路这里用最直白的方式重新梳理一遍。损失函数对得分s的依赖链是L → p → s。我们先算单样本梯度再推广到矩阵形式。设p是Softmax输出的概率向量长度为Cy是真实类别损失L -log(p_y)。先求∂L/∂s_k。分两种情况讨论。当k y时L -log(p_y)而p_y exp(s_y) / Σ_j exp(s_j)。对exp(s_y)求导是exp(s_y)对分母求和求导也是exp(s_y)用商法则∂L/∂s_y -(1/p_y) · (exp(s_y)·Σ - exp(s_y)·exp(s_y)) / Σ² -(1/p_y) · (p_y - p_y²) p_y - 1当k ≠ y时分子与s_k无关只有分母含s_k∂L/∂s_k -(1/p_y) · (0 - exp(s_y)·exp(s_k)) / Σ² -(1/p_y) · (-p_y · p_k) p_k合并两种情况写成向量形式就是∂L/∂s p - one_hot(y)这个结果非常简洁梯度就是预测概率与one-hot标签的差。直观理解是如果模型预测概率偏高比标签还高梯度为正说明需要降低该类得分反之梯度为负。接下来对s Wx b应用链式法则。单个样本的梯度是∂L/∂W (p - one_hot(y)) ⊗ x 外积形状C×D ∂L/∂b p - one_hot(y) 形状C为什么是外积因为s_k Σ_j W[k,j]·x[j] b[k]所以∂s_k/∂W[k,j] x[j]再乘上∂L/∂s_k并对外积求和的位置取对应项自然得到秩1外积矩阵。3.2 向量化推导把循环变成矩阵运算上面的公式是单样本版本作业要求你处理N个样本。向量化的核心思想是让NumPy一次处理整个矩阵。定义得分矩阵S X W.T b形状为N×C。逐行做Softmax得到概率矩阵P形状同样为N×C。再构造one-hot标签矩阵Y形状为N×C第i行第y_i列是1其余是0。损失部分的向量化是loss -np.mean(np.log(P[np.arange(N), y])) reg * np.sum(W * W)梯度的向量化推导是每个样本梯度求和再平均。单样本梯度(P[i] - Y[i]) ⊗ X[i]的累加可以写成dW (1/N) · X.T (P - Y) 2·reg·W这里X.T形状是D×N(P-Y)形状是N×C矩阵乘得到D×C转置后就是C×D。db更简单对P - Y在样本维度上求和再除以N即可。我当时对照循环版和向量版的结果发现误差在1e-10量级瞬间就理解了为什么矩阵运算能替代循环所谓向量化本质是把对每个样本算外积再累加这个操作重写成批量外积的求和而X.T (P-Y)恰好就是这个批量求和的闭式表达。3.3 数值梯度检查的原理数值梯度检查是验证推导正确性的黄金标准。作业里的grad_check函数用中心差分法dW_num[i,j] ≈ (L(W ε·e_ij) - L(W - ε·e_ij)) / (2ε)其中e_ij是只在位置(i,j)为1的矩阵ε通常取1e-5。这个公式的误差是二阶的O(ε²)比单侧差分精度高得多。相对误差的计算方式是|dW_num - dW_anal| / max(|dW_num|, |dW_anal|)。作业里要求小于1e-6左右实际操作中通常能到1e-8甚至1e-10。我第一次做梯度检查时损失函数算出来和参考值一致但梯度检查死活过不了误差在1e-2量级。排查了半天发现是我在损失里加了正则化但梯度里漏掉了2·reg·W这一项。正则化项会让数值梯度包含这部分贡献而解析梯度没有误差自然很大。所以做检查时要保证损失函数和梯度公式里的每一项严格对应。4. 代码实现从朴素循环到向量化的完整过程4.1 环境准备与数据加载作业环境建议用Python 3.8以上版本核心依赖是NumPy、matplotlib和scikit-learn用于train_test_split。我当时用的是最朴素的配置Anaconda Jupyter Notebook没有GPU也能跑因为这份作业的数据量很小。数据加载部分直接使用课程提供的data_utils工具from cs231n.data_utils import load_CIFAR10 def get_CIFAR10_data(num_training49000, num_validation1000, num_test1000): cifar10_dir cs231n/datasets/cifar-10-batches-py X_train, y_train, X_test, y_test load_CIFAR10(cifar10_dir) # 划分验证集 mask range(num_training, num_training num_validation) X_val X_train[mask] y_val y_train[mask] mask range(num_training) X_train X_train[mask] y_train y_train[mask] # 只取前num_test张作为测试集 mask range(num_test) X_test X_test[mask] y_test y_test[mask] # 拉平并做均值中心化 X_train np.reshape(X_train, (X_train.shape[0], -1)).astype(np.float64) X_val np.reshape(X_val, (X_val.shape[0], -1)).astype(np.float64) X_test np.reshape(X_test, (X_test.shape[0], -1)).astype(np.float64) mean_image np.mean(X_train, axis0) X_train - mean_image X_val - mean_image X_test - mean_image # 加一维偏置把b合并进W X_train np.hstack([X_train, np.ones((X_train.shape[0], 1))]) X_val np.hstack([X_val, np.ones((X_val.shape[0], 1))]) X_test np.hstack([X_test, np.ones((X_test.shape[0], 1))]) return X_train, y_train, X_val, y_val, X_test, y_test这里有个关键工程决策把偏置b合并到权重矩阵W里。做法是给每个样本的特征向量末尾追加一个常数1同时把W的列数加一。这样原来的Wx b就变成了Wx少维护一组参数代码更简洁。作业框架里W的初始形状是(C, D1)正是为了配合这个设计。均值中心化也很关键。CIFAR-10原始像素值在0-255之间均值为约120如果不减去均值exp操作容易接近溢出区间而且梯度更新会比较不稳定。减均值后特征分布以0为中心训练更加平稳。4.2 朴素循环版先把公式翻译成代码softmax_loss_naive的核心逻辑是按样本循环。我当时的实现是这样的def softmax_loss_naive(W, X, y, reg): loss 0.0 dW np.zeros_like(W) num_train X.shape[0] num_classes W.shape[0] for i in range(num_train): scores X[i].dot(W.T) # 形状(1, C) # 数值稳定减去最大值 scores - np.max(scores) probs np.exp(scores) probs / np.sum(probs) loss -np.log(probs[y[i]]) # 梯度p - one_hot外积 dS probs.copy() dS[y[i]] - 1.0 dW dS.reshape(-1, 1) * X[i].reshape(1, -1) loss / num_train loss reg * np.sum(W[:, :-1] * W[:, :-1]) # 偏置列不参与正则化 dW / num_train dW[:, :-1] 2 * reg * W[:, :-1] return loss, dW写这个版本时最容易犯的错误有两个。一是忘记dS[y[i]] - 1.0这一步直接从probs出发做外积这样梯度会整体偏大梯度检查时误差会很明显。二是在算正则化时把偏置列也算了进去严格来说偏置不应该被正则化因为它不控制任何特征的敏感度约束它没有意义。外积那行代码dS.reshape(-1, 1) * X[i].reshape(1, -1)本质就是前面推导里的(p - one_hot(y)) ⊗ x。NumPy的广播机制会把这两个向量自动扩展成矩阵的逐元素乘积得到C×D的梯度贡献。这个版本在全部49000个训练样本上跑一次完整的损失和梯度计算大概需要几秒钟。每次SGD迭代都要算一次几百轮迭代下来就非常痛苦了所以必须做向量化。4.3 向量化版本把循环拍平向量化版本的目标是去掉两个循环外层样本循环和内层类别循环。我当时的实现def softmax_loss_vectorized(W, X, y, reg): num_train X.shape[0] num_classes W.shape[0] scores X.dot(W.T) # (N, C) scores - np.max(scores, axis1, keepdimsTrue) probs np.exp(scores) probs / np.sum(probs, axis1, keepdimsTrue) correct_log_probs -np.log(probs[np.arange(num_train), y]) loss np.sum(correct_log_probs) / num_train loss reg * np.sum(W[:, :-1] * W[:, :-1]) dS probs.copy() dS[np.arange(num_train), y] - 1.0 dW X.T.dot(dS) / num_train dW[:, :-1] 2 * reg * W[:, :-1] return loss, dW向量化的核心就三行probs矩阵一次算完所有样本的概率dS[np.arange(num_train), y] - 1.0用花式索引一次性把每个样本真实类别位置的梯度减1X.T.dot(dS)一次性累加所有样本的外积贡献。对比两个版本输出时我发现损失值和梯度完全一致差异在1e-12量级但运行时间从几秒降到了几十毫秒快了大约50倍。这个速度差距直观说明了向量化的重要性——如果坚持用循环版后面调参时的几百次训练迭代会把人等疯。提示np.max(scores, axis1, keepdimsTrue)里的keepdimsTrue必须加上否则减出来的形状是(N,)而不是(N,1)广播时会对不齐。这个细节我踩过一次报错信息提示dimension mismatch一眼就能看出来。4.4 梯度检查与版本一致性验证写完两个版本后作业要求按顺序做三件事先用grad_check验证naive版的梯度再用随机数据对比两个版本的输出最后对比运行时间。梯度检查我用的代码大致是from cs231n.gradient_check import grad_check_sparse X np.random.randn(10, 4) * 1e-2 y np.random.randint(0, 3, size10) W np.random.randn(3, 4) * 1e-3 f lambda w: softmax_loss_naive(w, X, y, 0.0)[0] grad_numerical grad_check_sparse(f, W, grad_analytical, 10)这里把正则化设成0是为了单独验证数据项梯度排除正则化梯度带来的干扰。等数据项梯度通过后再单独验证带正则化的版本。grad_check_sparse不会检查所有位置而是每轮抽查10个随机位置多次调用覆盖更多位置。相对误差通常落在1e-8到1e-10之间如果某个位置误差超过1e-4基本可以断定解析梯度有bug。我当时排查经验是先检查dS有没有正确减1再检查dW的转置方向X.T.dot(dS)还是dS.T.dot(X)最后检查正则化项有没有漏。5. 训练与超参数调优亲手把准确率拉起来5.1 SGD训练流程的搭建梯度正确后就可以开始训练了。训练代码本身就是一个小型SGD循环from cs231n.classifiers.linear_classifier import LinearClassifier class SoftmaxClassifier(LinearClassifier): def loss(self, X_batch, y_batch, reg): return softmax_loss_vectorized(self.W, X_batch, y_batch, reg) softmax_model SoftmaxClassifier() loss_history softmax_model.train( X_train, y_train, learning_rate1e-2, reg1e-3, num_iters1500, batch_size200, verboseTrue)SGD的原理是每轮迭代随机抽取一小批样本这里200个用这批样本的梯度作为全量梯度估计更新一次参数。为什么要随机抽样而不是用全量数据因为全量49000个样本的梯度计算虽然精确但每轮都算代价太高用200个样本估计的梯度方向虽然带噪声但迭代足够多次后依然能收敛到不错的解而且每轮成本低得多。训练过程中打印的损失值会呈现一个典型的下降曲线初始损失在log(10)≈2.3附近因为10个类别随机猜的期望交叉熵随着迭代降到1.2左右。我当时看到从2.3一路降到1.1才真正理解损失下降这个抽象概念到底长什么样。5.2 学习率与正则化强度的调试作业要求你在验证集上尝试不同的学习率和正则化强度找到最佳组合。我当时用了一个两层嵌套循环来搜索learning_rates [1e-7, 5e-7, 1e-6, 5e-6, 1e-5] regularization_strengths [2e4, 2.5e4, 3e4, 3.5e4, 4e4] results {} best_val -1 best_softmax None for lr in learning_rates: for reg in regularization_strengths: model SoftmaxClassifier() model.train(X_train, y_train, learning_ratelr, regreg, num_iters1500, batch_size200, verboseFalse) y_train_pred model.predict(X_train) train_acc np.mean(y_train_pred y_train) y_val_pred model.predict(X_val) val_acc np.mean(y_val_pred y_val) results[(lr, reg)] (train_acc, val_acc) if val_acc best_val: best_val val_acc best_softmax model这里有个容易困惑的点作业给的初始代码里正则化强度是1e-3这样的量级但很多人的搜索结果里出现2e4、3e4这样的数值。原因在于作业框架里的W初始化时用了较小的scale如果正则化强度太小权重几乎不受约束SGD容易在验证集上过拟合。另一方面如果你的特征做了除以标准差之类的标准化操作正则化强度的最优区间会完全不同。不同预处理方案对应不同的超参区间这是正常现象关键是观察训练集和验证集准确率的差距来判断是否过拟合。我实际跑出来的结果是训练集准确率通常在55%-65%验证集准确率在35%-40%之间。注意训练集比验证集高出一大截这说明线性分类器的容量不够即使在训练集上也无法完全拟合同时也存在一定的过拟合。这就是线性模型的瓶颈——你不能指望一个没有隐藏层的模型在CIFAR-10上达到很高准确率。这个准确率天花板会让你对后面两层神经网络的显著提升有更深的体会。调参过程中的一个实用技巧是把结果打印成表格横轴是学习率纵轴是正则化强度每个格子填验证集准确率。这样能快速看出哪个方向是敏感方向。我自己的经验是学习率是更敏感的超参差一个数量级就可能导致完全不收敛准确率10%或训练震荡正则化强度的容忍度相对宽一些在一个数量级范围内都有还不错的验证集表现。5.3 权重可视化看看模型到底学到了什么训练结束后作业的最后一问是可视化权重。每个类别的权重向量W[k]形状是(D1,)去掉最后的偏置项后reshape回32×32×3然后做归一化显示w softmax_model.W[:, :-1].reshape(10, 32, 32, 3) w_min, w_max w.min(), w.max() w (w - w_min) / (w_max - w_min) # 归一化到[0,1] plt.imshow(np.hstack(w))我做的可视化结果非常有意思飞机类的模板整体呈现蓝色背景因为CIFAR-10里的飞机照片大多以蓝天为背景汽车类模板中心区域有红色调对应车身颜色青蛙类模板整体偏绿。这说明线性分类器确实在学每个类别的平均外观模板——它把属于某个类别的大多数图像在像素空间里做了平均然后用这个平均模板去做匹配。这个可视化直观地印证了2.1节说的问题线性模型的表达力有限它只能表示某个像素位置偏红这种非常粗糙的统计规律无法表达物体可以出现在不同位置只是相对关系固定这种更高级的结构。看到这张模板图对比后面Assignment 1里两层神经网络学到的特征可视化你会更理解模型容量的意义。6. 常见问题与排查技巧实录6.1 损失正确但梯度检查失败这是我在做这份作业时遇到的最典型的bug。现象是损失值和作业提供的参考值完全一致但grad_check报出1e-2量级的误差。排查步骤应该是这样的第一检查正则化项我最开始就是漏了2·reg·W第二检查dS[y[i]] - 1.0是否执行在循环版里写着dS probs然后忘了减1会导致梯度整体偏移第三检查dW的累加方向到底是X.T dS还是dS.T X搞反的话形状可能碰巧对C和D相等时但数值完全错误第四用很小的随机数据比如10个样本、4个特征、3个类别逐步手算一个样本的梯度和代码输出对拍。我后来的习惯是给梯度检查单独写一个小测试用例用一个固定种子的随机数据集手动计算第一行的前几个梯度值写死在测试里。这样每次改动代码后跑一遍测试能立刻发现回归问题。6.2 梯度检查的数值陷阱即使公式完全正确梯度检查本身也有几个坑。第一个是ε的选择1e-5是经验值太大则截断误差主导太小则浮点舍入误差主导。可以尝试把ε从1e-3缩小到1e-7观察相对误差是否先降后升如果是说明数值梯度实现正确。第二个是正则化强度的影响。如果reg设置得很大比如1e4量级正则化梯度项会主导总梯度数据项的小误差可能被淹没。我习惯在reg0时先检查数据项再单独检查正则化项。第三个是权重初始化尺度。如果W的scale很小比如1e-3损失和梯度的绝对值都很小相对误差对绝对误差更敏感反过来如果W太大数值梯度计算中损失的值可能非常大导致溢出。作业里给的随机初始化尺度是经过验证的不要随意改大。6.3 训练不收敛或损失异常训练阶段的常见问题有三种。第一种是损失出现NaN大概率是Softmax的exp没有做减去最大值的操作或者学习率过大导致参数爆炸。我在调参时把学习率设成1e5想着快点收敛结果一迭代损失全是NaN后来才意识到学习率过大相当于每步都跳出了有效区域。第二种是损失下降但准确率原地踏步。这种情况一般是数据预处理有问题比如均值中心化没做好或者标签索引和类别顺序对不上。我遇到过把y从0-9错映射成1-10的老式写法导致索引越界报错倒是很直接但有时Cython包装层的报错信息不清晰容易误判成别的问题。第三种是训练集和验证集准确率都极低10%左右接近随机。这是学习率过小的典型特征SGD在损失曲面上每步挪动太少3000轮都走不到最低点。把学习率调大1-2个数量级通常就能看到损失明显下降。我把这些排查经验整理成一个速查表方便对照现象可能原因排查方向梯度检查误差1e-2正则化梯度缺失确认dW里加了2·reg·W梯度检查误差1e-2one-hot减1遗漏检查dS[y[i]] - 1.0损失为NaNSoftmax未减最大值加入scores - max(scores)损失为NaN学习率过大降低学习率1-2个数量级损失不下降学习率过小增大学习率观察前100轮训练高验证低正则化太弱增大reg看验证集是否提升训练验证都低数据预处理错误检查均值中心化和shape6.4 我再补充几个作业里没明说但很有用的技巧第一点是关于训练轮数的选择。作业默认num_iters1500但如果你在调参阶段每次都跑1500轮整个搜索过程会很漫长。我的做法是先用num_iters300快速筛选出几个可能不错的超参组合再对候选组合跑满1500轮做最终确认。这样总时间能省一半以上。第二点是关于batch_size的影响。作业默认200但如果你发现损失曲线震荡得很厉害可以试着减小batch size比如100看是否更平滑或者增大batch size比如500看能否加速收敛。batch size越小梯度噪声越大像模拟退火里的随机扰动有时候反而能帮模型跳出局部最优。第三点是关于可视化权重的归一化。直接用(w - min) / (max - min)是教科书做法但由于权重中有正有负很多测试者会困惑为什么背景是灰色的。实际上归一化后值0对应原权重的最小值显示为黑色中间值显示为灰色正权重可能显示为亮色。如果你想模拟课程讲义里那种蓝色背景、彩色物体的效果可以用plt.imshow(w.transpose(1,2,0).astype(uint8))但前提是权重尺度接近原始像素这需要额外的scale处理。我当时就是按归一化的简单做法来的足够看清每个类别的主色调模板了。7. 从这份作业延伸出去的想法做完整份作业之后我的一个强烈感受是CS231n的作业设计有一种循序渐进的内在逻辑。Softmax这道题表面是让你实现一个分类器实际是在训练你建立数学公式-向量化代码-数值验证的闭环思维。这个闭环在后面的每一份作业里都会反复出现写两层神经网络时你要推导反向传播写卷积网络时你还要验证梯度调超参数时你依然要做类似的学习率搜索。如果你做完Softmax后还有余力我建议做两个额外实验。第一个是不做均值中心化直接训练对比准确率和收敛速度这个实验能让你直观理解数据预处理的作用。第二个是试着用全量数据而不是SGD做一次梯度下降看看全量梯度下降的收敛轨迹和SGD有什么不同这个对比能帮你加深对为什么需要随机性的理解。还有一个小技巧是把作业里所有实现过的函数softmax_loss_naive、softmax_loss_vectorized、train循环保留成独立的模块文件而不是只写在Jupyter里。后面做Assignment 2和3时你可以复用这里的SGD训练骨架只需要替换损失函数和模型结构。我当时就是靠这个代码复用习惯后面几个作业的起步速度快了很多。最后再分享一点个人体会。我见过不少同学卡在Softmax这题上根本原因不是数学太难而是太想一步到位写最优版本结果被循环、广播、转置这些细节绊住。我的建议是严格按作业要求的顺序来先写naive版让你理解公式再做梯度检查让你验证正确性最后写向量化版让你优化性能。这个顺序本身就是课程设计者留给你的捷径。你自己一步一步踩过来收获的东西远不止一份能跑的代码。

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

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

免费获取报价 →
↑