CS231n这门课的Spring 2020版Assignment1,做到SVM Classifier这一节的时候,很多人第一次意识到“原来我连一个简单的线性分类器都没真正写明白”。作业目标很直接:在CIFAR-10数据集上,从零实现一个多类线性SVM分类器,包括Hinge Loss的计算、解析梯度推导、梯度数值验证,以及用SGD把权重训练出来,最后对测试图片做分类。听起来不复杂,但真正动手时,无数细节会冒出来:为什么损失里有max(0,·)?为什么梯度里要分正确类和其他类?为什么学习率差一点结果就天差地别?这些坑如果不踩一遍,后面学Softmax、两层神经网络也会根基不稳。
这篇文章适合正在做CS231n作业的人、想系统补一遍线性分类器原理的人,以及那些作业做完了但想回头看“当时我到底在算什么”的人。我会沿着作业的完整流程走一遍,把每个关键选择的理由、每个容易翻车的细节、以及我实际验证过的调试经验都写清楚,希望能帮你少走几小时弯路。
1. 先从整体看:Assignment1里SVM在哪个位置
1.1 整个Assignment1的作业地图
Spring 2020版的Assignment1一共四个任务,按顺序是:KNN分类器、SVM线性分类器、Softmax线性分类器、两层神经网络。当时课程设计者显然是想让同学们先接触最简单的非参数方法(KNN),然后转向参数化方法(SVM和Softmax),最后用两层神经网络收尾,形成一个递进关系。
SVM这条线的位置非常关键。它在KNN之后出现,意味着你已经知道“用像素距离做分类”有多笨拙:KNN在CIFAR-10上即便调好参数,测试准确率也就是20%~30%左右,而且预测时要遍历全部训练样本,慢得不行。SVM登场后,你用训练数据学出一个权重矩阵W,预测时只做一次矩阵乘法和argmax,速度快了几个数量级,准确率也能稳稳到30%~40%。
这个对比本身就是作业想教给你的第一课:分类器不只有“记忆训练样本”这一条路,更好的思路是“学习一个从像素到类别的映射函数”。
1.2 为什么用SVM而不是直接上神经网络
很多初学会问:既然最终目标是深度学习,为什么不直接跳过SVM去学CNN?这个问题我当时也问过自己。但实际做完之后我才明白,线性SVM其实是理解神经网络的最小必要模型。
一个线性分类器做的事情是 f(x) = Wx + b,其中W是权重矩阵,x是输入图像拉成的向量。神经网络无非是在这外面套了好几层非线性变换。如果你连“损失函数如何驱动W更新”都没亲手实现过,后面看反向传播会像看天书。Hinge Loss的梯度只有两种值:要么是0,要么是某个累加量,逻辑非常简单。把这个梯度的来龙去脉吃透了,再回头看Softmax的交叉熵损失、再看两层的反向传播,会发现很多概念都是一脉相承的。
另外还有一个现实原因:CS231n作业里提供了完整的代码框架,包括数据加载、图像预处理、数值梯度检查工具等。你只需要在关键位置填上损失和梯度实现。但这个“只填几十行代码”的过程,恰恰是整个作业最考验人的地方,因为框架不会告诉你哪一步该向量化、哪一步该用mask矩阵,这些都要你自己摸索。
1.3 本次SVM作业与后续作业的联系
做SVM时你会在CIFAR-10上完成一个完整的训练闭环:定义损失 -> 算梯度 -> 更新权重 -> 循环迭代 -> 评估准确率。这个闭环在Assignment2的卷积神经网络、Assignment3的Transformer里会反复出现。而且作业里要求你做的“网格搜索超参数”,在后面的作业里也经常要用到。
还有一个更直接的联系:做完SVM之后,你会马上做Softmax。两者的数据准备、训练逻辑几乎一模一样,唯一的变化是损失函数从Hinge Loss换成交叉熵。如果你把SVM这一步做得足够扎实,做Softmax可能只需要改一个损失函数再加上数值稳定处理。很多同学后期速度快,是因为SVM阶段已经把工程框架吃透了。
2. 核心原理拆解:多类SVM的损失与梯度
2.1 多类Hinge Loss怎么读
先约定符号。训练集有N张图片,每张图片x_i被拉成长度为D的向量。类别数是C,在CIFAR-10里C=10。我们要学一个权重矩阵W,形状是 C x D,还有偏置b,形状是 C x 1。对于第i张图片,每个类别会得到一个得分 s_j = W_j·x_i + b_j,其中j遍历所有类别。
对于这张图片的正确类别y_i,我们希望它的得分 s_{y_i} 比其他所有类别的得分都要高。多类SVM的损失定义为:
L_i = Σ_{j≠y_i} max(0, s_j - s_{y_i} + Δ)
这里的Δ是margin,课程作业里默认设成1。这个式子的意思是:对于每个错误类别,如果它的得分比正确类得分还高,或者说差距没超过margin,我们就要惩罚;如果它比正确类低出至少Δ,那这个错误类别的“坏影响”就是0,不产生损失。
用一个生活化的比方:假设班级里有一个“正确答案”同学,得分越高越好。你要求“错误答案”同学的得分最多只能比正确答案低一点点(低于margin),低得越多越安全。如果某个错误答案居然压过了正确答案,或者差距不够大,那就要记一笔账。SVM训练的本质就是不断让正确类的得分“碾压”所有错误类。
2.2 为什么选max(0,·)这种形式
Hinge Loss用max(0,·),有几个关键理由。第一,它天然稀疏:对于大量样本,正确类和其他类的得分差距已经很安全,损失和梯度都是0,这时候它们对W的更新没有贡献,相当于模型在“忽略”这些已经做对的样本。这种稀疏性让SVM的训练带有一种“只关注最难样本”的特性。
第二,它对“已经分类正确的样本”不产生额外的推动。软间隔SVM的哲学是“只要边界合适就好,不需要把所有样本都推到离边界无穷远”。如果换成一个处处有梯度的损失函数,比如平方损失,那模型会花大量算力去优化那些已经做得不错的样本,反而容易过拟合。
第三,max(0,·)在0点不可导,但在实际实现中我们通常选取次梯度。作业里其实不需要处理这个数学细节,因为代码实现时只需要判断条件是否满足,大于0就取梯度,否则就是0。
提示:margin Δ的取值固定为1时,正则化强度λ实际上控制了“间隔偏好”和“边界宽度”的平衡。Δ越大,要求正确类和错误类的得分差越大,模型越难满足,决策边界越保守。
2.3 正则化:不希望W只看一张图
如果损失只包含上面的L_i,模型会出现一个严重问题:它会把训练样本记得死死的,权重w的范数变得很大,对噪声也特别敏感。为了缓解过拟合,作业里在损失函数中加上了L2正则项:
L = (1/N) * Σ_i L_i + λ * Σ_k W_k^2
正则项的本质是“惩罚权重过大”。为什么权重过大会导致过拟合?可以这样想:如果W的某个分量特别大,那么输入x中对应的像素稍微变化一点,得分就会剧烈波动。这相当于模型在“放大镜下”看待像素的微小差异,很容易把噪声也当作模式。加上正则项后,模型被迫用更小的权重、更平滑的边界去拟合训练数据。
实际调参时你会发现,λ的选择对最终准确率有非常直接的影响。λ太小,训练集准确率可能很高但测试集上掉下来;λ太大,训练集准确率会明显受限,因为模型太“保守”了。CIFAR-10上线性SVM比较合适的λ大概在1e-3到1e-2这个量级,但具体值还是要通过网格搜索确认。
2.4 梯度推导:让W知道往哪走
这里是最容易卡住的地方。我们需要计算的是损失函数对W的偏导。因为每个样本i只影响它自己的那部分损失,我们先看单个样本的梯度,再把所有样本的梯度累加。
对于第i个样本,设正确类得分为 s_{y_i},错误类别j的得分为 s_j。如果 s_j - s_{y_i} + 1 > 0,那么这个错误类别“激活”了损失。此时:
- 从 s_j 的角度看,损失对 s_j 的偏导是 +1
- 从 s_{y_i} 的角度看,因为 s_j - s_{y_i} + 1 中 s_{y_i} 带负号,所以损失对 s_{y_i} 的偏导是 -1
又因为 s_j = W_j·x_i,所以损失对W_j的梯度是 x_i(转置后),对W_{y_i}的梯度是 -x_i。
把所有激活的错误类别加起来,设正确类之外有m个类别被激活(即它们的得分与正确类得分的差不超过margin),那么这一样本对正确类权重W_{y_i}的梯度贡献是 -m * x_i,对每个被激活的错误类别权重W_j的梯度贡献是 +1 * x_i,对未被激活的类别梯度为0。
这个逻辑用代码写起来非常简洁:
def svm_loss_vectorized(W, X, y, reg): num_train = X.shape[0] num_classes = W.shape[0] scores = X.dot(W.T) # 形状 (N, C) correct_class_scores = scores[np.arange(num_train), y].reshape(-1, 1) margins = np.maximum(0, scores - correct_class_scores + 1) margins[np.arange(num_train), y] = 0 # 正确类自身不计入损失 loss = np.sum(margins) / num_train loss += reg * np.sum(W * W) # 计算梯度 binary = margins binary[margins > 0] = 1 row_sum = np.sum(binary, axis=1) binary[np.arange(num_train), y] -= row_sum dW = binary.T.dot(X) / num_train + 2 * reg * W return loss, dW我看很多初学者在算梯度时,喜欢先对每个类别循环,再对每个样本循环。其实正确的直觉是:对每个样本,先找出哪些错误类别“越界”了,然后所有越界类别共享同一个gradient方向(=xi的方向),而正确类别要吸收负梯度 = 越界类别数的相反数。批量计算时,我们用矩阵操作把每个样本的越界情况记录下来,再乘上X,就得到了dW。
数值梯度验证是作业的标准动作。做法是在某个很小的epsilon下,用 (L(w+eps) - L(w-eps)) / (2*eps) 近似梯度,再跟解析梯度比较相对误差。这个验证通过一遍,基本可以确认损失和梯度实现没有低级错误。需要注意:正则项也要算进梯度里,否则你验证半天都会差一项。
3. 实操过程:从数据准备到训练闭环
3.1 数据加载与预处理
作业刚开始会让你加载CIFAR-10数据集。这里数据已经提前处理成numpy数组,训练集是50000x3072,测试集是10000x3072。3072 = 32323,对应3通道彩色图像按RGB排列后拉平的向量。
加载后第一件事就是均值减法。你计算训练集所有样本的每个像素均值,得到一个3072维的均值向量,然后在训练集和测试集上都减去这个均值。这一步非常关键,因为减去均值后,数据的中心在原点附近,线性分类器学起来更容易收敛。如果不做均值减法,你会看到损失下降异常缓慢,准确率也上不去,因为模型还要额外花力气去补偿像素的整体偏移。
还有一个小细节:作业里通常会把图像像素值从[0,255]缩放到[0,1]吗?实际上CS231n的SVM作业并没有强制做归一化,因为均值减法之后数值范围已经是[-127.5,127.5]这个量级,配合合适的学习率(比如1e-3到1e-2)是可以工作的。但如果你用的是原始0-255像素,学习率要相应调大,否则更新步长太小。我个人习惯在减去均值后再除以标准差,这样能让损失面更“圆”,调参更加鲁棒。
数据划分方面,作业会要求从训练集里切出一部分作为验证集,通常是50000中取5000做val,剩下45000做train。验证集的用途是挑选超参数,等超参数定下来后,最后才在测试集上跑一次报告最终精度。一定不要用测试集调参,这是作业反复强调的红线。
3.2 朴素实现:先写对再写快
作业框架一开始会让你用双重循环实现损失函数。虽然它慢,但它逻辑直白,非常适合对照公式检查。我建议哪怕你已经有能力写向量化版本,也先在草稿纸上或者注释里把双重循环版本写一遍,因为它是你后面调试向量化实现的“参照物”。
伪代码如下:
for i in range(num_train): scores_i = X[i].dot(W.T) correct_score = scores_i[y[i]] for j in range(num_classes): if j == y[i]: continue margin = scores_i[j] - correct_score + 1 if margin > 0: loss += margin dW[j] += X[i] dW[y[i]] -= X[i]这个版本有两点值得注意。第一,loss最后要除以N并加上正则项,但梯度累加时我们先把每个样本的贡献加起来,最后统一除以N再加正则梯度。第二,在双重循环里,每个样本的dW[y[i]]会被多次减法更新,每个错误类别j则单独加一次,这跟数学推导完全对应。
写完朴素版后,用作业提供的gradient_check工具验证一次。如果数值梯度跟解析梯度的相对误差在1e-5以下(有的版本要求1e-6),就说明实现对了。我第一次做的时候,正则项忘记在数值验证里也加,导致误差一直卡在1e-2级别,折腾了半小时才反应过来。
3.3 数值梯度验证为什么可靠
数值梯度的思想很简单:梯度是损失函数在权重空间某个点上的变化率。如果解析梯度算的是对的,那么用极限定义去逼近得到的结果应该非常接近。作业封装好的check_gradient函数通常会随机选几个点,对每个点生成一个小扰动,计算 (f(theta + h) - f(theta - h)) / (2h)。中心差分比单边差分更准确,误差是O(h^2)。
这个验证最大的价值是把你从“公式推导错误”中拯救出来。很多人算梯度时会把符号写反,比如把正确类的负梯度写成正梯度,数值验证能立刻暴露这个问题。唯一要留意的是,验证时如果某个样本的loss正好落在hinge的“断点”附近,max(0,·)的次梯度定义可能导致数值梯度与解析梯度稍有偏差,这时可以换一个随机初始化的W重新验证。
注意:数值梯度验证时,如果相对误差在1e-2左右,往往是实现了但符号反了;如果在1e-5左右,基本可以说梯度实现正确。
3.4 向量化实现:真正理解矩阵运算
双重循环版本在45000训练样本、10个类别上跑一次,可能就要零点几秒,而训练过程要跑几百次迭代,那会慢到怀疑人生。向量化的目标是把所有样本的损失和梯度用矩阵乘法一次性算出来。
向量化的核心就两行:先算出所有样本对所有类别的得分矩阵scores,形状(N, C),然后用广播机制把正确类别得分扩展到每一列;然后算margins。梯度部分,构建一个跟margins同形状的binary矩阵,激活位置为1,接着对行求和,再把每个样本正确类位置的数值变成负的行求和数。这样,每一行都表示这个样本的“权重分配”:正确类分到负数,越界类别分到正数,其他为0。最后用binary.T.dot(X)累加所有样本的贡献。
我测过一次,向量化版本比双重循环快30到100倍,而且代码更简洁。很多初学者一开始觉得向量化难,主要是没把“每一行表示一个样本的权重分配方案”这个中间结果想清楚。一旦想明白了,后面写Softmax的梯度也会非常顺手。
3.5 SGD训练循环与学习率
作业要求用完全批量梯度下降还是小批量SGD?我记得Assignment1里给了SGD的框架,batch size默认是200。SGD的核心是:不用全部45000个样本算梯度,而是每次随机抽一小批,用这一批的梯度近似全量梯度。
训练循环长得像这样:
learning_rate = 1e-3 num_iters = 1500 batch_size = 200 for it in range(num_iters): idx = np.random.choice(num_train, batch_size) X_batch = X_train[idx] y_batch = y_train[idx] loss, grad = svm_loss_vectorized(W, X_batch, y_batch, reg) W -= learning_rate * grad if it % 100 == 0: train_acc = evaluate(W, X_train, y_train) val_acc = evaluate(W, X_val, y_val)学习率是最难调的超参数。它太大,损失会发散,表现为loss直接变成NaN或者震荡剧烈;它太小,损失下降像蜗牛,到训练结束精度仍然很低。我个人的经验是先试几个相差10倍的学习率(1e-4、1e-3、1e-2),看哪个在100次迭代内能让损失稳定下降,然后在那个量级附近再细化搜索。
SGD的随机性意味着每次跑的结果不完全一样,但同一组超参数下最终精度波动不会太大。如果某次运行得到特别差的结果,先别急着调参,重新跑一次可能就正常了。作业里还要求记录loss的变化曲线,通常你会看到loss前期下降很快,后面逐渐平缓,这是正常的收敛形态。
3.6 训练完怎么检查:权重可视化
作业里有一个很直观的可视化步骤:把训练好的W每一行重新reshape成32x32x3,然后显示出来,就能看到每个类别学到的“模板”。比如模型学到的“飞机”权重模板可能带有蓝色背景,“车”的模板可能更亮一些,“青蛙”的模板可能带着绿色调。因为CIFAR-10的像素是RGB三通道,可视化时你把每一行W加回均值向量,再归一化到[0,255]范围内显示即可。
看到权重模板的那一刻,你对“线性分类器到底学到了什么”会有非常直观的理解。它没有学到复杂的物体结构,它学到的只是每个类别在像素空间中的平均模式。这也是为什么线性SVM在CIFAR-10上只能做到40%左右的原因,因为图像本质上是线性不可分的。
4. 调参实验与结果分析
4.1 学习率与正则化强度的网格搜索
作业要求你在验证集上搜索超参数。常见的做法是把learning_rate和reg各取几个值,比如learning_rate在[1e-7, 5e-6, 1e-5, 5e-4]里选,reg在[1e-3, 1e-2, 1e-1, 0.5]里选,两两组合训练。但要注意:SVM作业里的学习率其实会因为数据预处理方式不同而有巨大差异。如果你做了标准化,学习率通常取1e-2到1e-1;如果你只做了均值减法,学习率1e-3左右更安全。
我建议先固定reg=1e-2,只搜学习率;找到学习率的大致量级后,再固定这个学习率搜reg。这种“坐标下降”式搜索比全网格搜索高效很多,因为不同超参数的影响不是完全独立的。
4.2 观察损失曲线判断收敛
训练时每100轮打印一次loss、train_acc、val_acc。合格的loss曲线应该是稳步下降的,而且最终train_acc和val_acc的差距不能太大。如果train_acc很高但val_acc明显低,说明过拟合,应该增大reg。如果两个acc都低,说明欠拟合,可能是学习率太小或者迭代不够。如果loss来回震荡,说明学习率偏大,应该调小。
这里有个容易被忽略的点:SGD的batch size会影响梯度噪声,从而影响可用的学习率。batch size越大,梯度估计越准,可以适当调大学习率;batch size越小,梯度噪声越大,学习率要小一些。作业默认batch size=200,在这个设置下,learning_rate=1e-2配合标准化预处理表现不错;如果用的是原始像素,最好把学习率降到1e-4左右。
4.3 最终精度预期:不要抱不切实际的期望
线性SVM在CIFAR-10上的水平,经过好好调参,训练集准确率大概在40%~45%,验证集大概在35%~40%,测试集能到35%~38%就非常不错了。很多初学者会不甘心,试图用更大迭代次数或更极端的超参数把测试精度冲到40%以上。但线性模型的capacity就那么多,再挣扎也很难突破45%。
这个结果本身是有教育意义的:它告诉你,在图像分类任务中,直接从原始像素做线性分类是有天花板。真正让精度发生质变的是特征工程(比如HOG特征、颜色直方图)以及后面的非线性神经网络。你在这个作业里体验到的“调参带来几个百分点的提升”,会帮助你建立对模型容量的直觉。
4.4 与KNN的对比
做完SVM后,作业会让你比较SVM和KNN的精度与速度。KNN在CIFAR-10上调整k值后最多能到28%左右,而SVM轻松到38%+。更关键的是预测速度:KNN需要把测试样本和所有训练样本算距离,预测一张图可能要遍历45000个样本;SVM只需要做一次矩阵乘法,预测10000张测试图也就几秒钟。
这个对比让我当时印象很深:一个模型哪怕精度稍微高点,如果每次预测都这么慢,实际部署根本不可用。这也是为什么作业要在开头放KNN,目的就是让你亲身体验为什么业界不用KNN做大规模图像分类。
4.5 一个常见的误解:SVM是“支持向量机”吗
线性SVM的作业实现的是软间隔多类SVM的简化版,但你并没有在代码里显式找到“支持向量”。它其实是用梯度下降优化了SVM的目标函数,等价于训练了一个带Hinge Loss的线性模型。真正的支持向量是那些落在margin边界附近、对决策边界有影响的训练样本。在作业的SGD实现里,支持向量的概念是隐式的:它们就是对loss贡献不为0的那些样本。
如果将来你去看SVM的原始优化理论,会发现有KKT条件、对偶求解等完全不同的推导路线。但CS231n课程刻意选择了“损失函数+梯度下降”这条路线,因为它跟深度学习的训练方式完全一致,这才是这门课的核心目的。
5. 常见问题与排查技巧实录
5.1 损失变成NaN
NaN几乎是每个做这个作业的人都会遇到的第一个问题。原因通常是学习率设得太大,导致权重更新后得分变得巨大,Hinge Loss里的得分差值溢出,或者梯度数值爆炸。解决办法很简单:调低学习率。如果学习率已经很低还是NaN,检查一下是不是输入数据里有NaN值,或者标准化时除的是0(某个通道标准差为0)。
还有一个不太容易想到的坑:如果你的梯度实现里把正则项梯度写成了2 * reg * W,但在损失里用了np.sum(W*W),这没问题。但如果你误把正则项梯度写成reg * W,数值梯度验证会通过不了,但训练时不太会NaN,而是精度很低。所以看到NaN先怀疑学习率,看到精度异常再回头检查梯度公式。
5.2 梯度检查永远过不了
数值梯度验证不过,常见有三种原因。第一种,解析梯度里漏了正则项,或者正则项梯度写错。第二种,梯度的符号搞反了,比如把 dW[y[i]] 写成了 += X[i] 而不是 -= X[i]。第三种,检查时选的W维度太小,数值梯度因为浮点精度问题匹配不上,这种情况可以增大W的scale(乘以10)再试。
我踩过的一个很隐蔽的坑是:在向量化实现里,我用margins本身作为binary矩阵,但margins里存的可能是小数(比如0.5),而我需要的是0/1标记。必须单独用一个 >0 的布尔矩阵来标记激活位置,不能复用margins。如果直接用margins当梯度系数,数值梯度检查也过不了。
下面是问题排查速查表:
| 现象 | 可能原因 | 优先排查动作 |
|---|---|---|
| loss=NaN | 学习率太大或梯度爆炸 | 学习率降到1e-5试一次 |
| 梯度检查相对误差1e-2 | 梯度符号错或漏正则项 | 逐项对比朴素实现 |
| 训练集准确率低 | 学习率太小或迭代不够 | 增大学习率或增大迭代次数 |
| 训练集高验证集低 | 过拟合,reg太小 | 增大reg |
| loss震荡不下降 | 学习率偏大 | 学习率减半再试 |
| 精度稳定但上不去 | 模型容量天花板 | 换特征或加深模型 |
5.3 学习率怎么找比较稳
我推荐一个非常省事的方法:先不训练,只观察初始loss和梯度。在第一次迭代时,loss应该接近设定值附近。然后你用一个“安全”的学习率比如1e-3,跑100次迭代,画出loss下降曲线。如果loss在前几十次迭代下降明显,说明学习率可用。如果loss几乎不动,就调大10倍再试。如果loss爆炸,就调小10倍。用这种“对数尺度搜索”,两三次就能定位到合适量级,比盲目网格搜索高效得多。
5.4 记住:不要在测试集上调参
这个问题我见很多人犯过。他们为了刷高测试精度,频繁在测试集上评估修改模型。这个做法的恶果是,最后你报告的数字是“被调参污染”的数字,不能反映模型泛化能力。作业里明确要求:所有超参数选择都要在验证集上做,最后只在测试集上跑一次。
一个实际经验:你把学习率、reg、迭代次数确定后,可以在验证集上多跑几次取平均,因为SGD随机性会导致单次结果有波动。最后再用最好的超参数组合在训练集+验证集的全量数据上重新训练一遍(如果作业允许),再到测试集上评估。这个流程跟真实竞赛里的做法一致,能最大程度保证结果的可靠性。
5.5 一个调试小技巧:过拟合单个样本
当你把SVM从零实现完但不确定整个训练流程是否正常时,一个很有效的测试是:只让模型过拟合10个训练样本。如果模型能做到在这10个样本上准确率100%(通常几十次迭代就行),说明损失、梯度、更新过程基本没有问题。如果连10个样本都过拟合不了,那一定是某个环节出了错。这个技巧在后面的神经网络作业里也特别有用,建议养成本能。
结尾:这门作业带给我的东西
做完了CS231n Spring 2020 Assignment1的SVM Classifier之后,我自己最大的收获不是“我会算Hinge Loss了”,而是终于建立起了“损失函数 -> 梯度 -> 参数更新”这个闭环的心智模型。以前看论文里写“我们用梯度下降优化目标函数”,总觉得这句话轻飘飘,真正自己动手写过一遍才发现,里面的细节多得惊人:数据预处理方式会改变可用的学习率范围,向量化实现要时刻警惕维度变化,正则化强度一个数量级的差别就能让验证集准确率波动好几个点。
如果你正在做这个作业,我的建议是不要急着抄代码,哪怕你是参考别人实现,也一定要在自己的笔记本上把每一行代码对应的数学含义写出来。尤其是那个binary矩阵的构造逻辑,你彻底想明白它,就等于理解了一半线性分类器。另外,作业做完后非常推荐把SVM改成Softmax,你会看到两者的代码结构几乎完全一样,只是损失和梯度变了一下,这个对比能让你对损失函数的作用有更深的认识。
最后留一个小技巧:做完SVM后,你可以把CIFAR-10的原始像素换成一阶的图像特征(比如色彩直方图加梯度直方图),再用同一个向量化SVM训练,你会发现准确率能一下子冲到45%以上。这个实验能最直观地让你理解“数据表达”和“模型能力”的关系,也为后面学CNN时“特征自动学习”的价值做了一次很好的铺垫。