上一讲第 1 讲:课程导论 下一讲第 3 讲:正则化与优化

第 2 讲:图像分类与线性分类器

Image Classification with Linear Classifiers —— 数据驱动方法、最近邻分类器与 KNN、线性分类器的三种视角、Softmax 与交叉熵损失

本讲概览:

本讲延续第 1 讲的话题,首先严格定义计算机视觉中最核心的任务——图像分类(Image Classification),随后介绍两种数据驱动(data-driven)的方法:最近邻分类器(Nearest Neighbor)与线性分类器(Linear Classifier)。KNN 部分引出超参数调优(hyperparameter tuning)与交叉验证(cross-validation)等重要概念;线性分类器部分则是通向神经网络的第一块、也是最重要的基石。最后讲解 Softmax 分类器与交叉熵损失函数。

一、图像分类任务的定义

图像分类是计算机视觉的核心任务:给定一张图像和一组预先定义好的标签(labels,例如狗、猫、卡车、飞机等),系统的工作就是从这些候选标签中挑出一个分配给该图像。这个任务对人类来说轻而易举——我们的大脑和认知系统天生就能对图像形成整体性(holistic)的理解并给出标签;但对计算机来说,这完全是另一回事。

在计算机眼中,图像通常用数据的矩阵(matrix)、更一般地说是张量(tensor)来表示。以一张分辨率为 \(800 \times 600\) 的彩色 RGB 图像为例:每个像素的取值范围是 0 到 255(8 比特数据结构),由于有红、绿、蓝三个颜色通道,整张图就是一个 \(800 \times 600 \times 3\) 的张量。老师在此特意回答了学生的提问:为什么像素值在 0–255 之间?因为最流行的 RGB 存储标准是 24 位格式——三个通道各占 8 位(有时是 32 位,多出一个透明度 alpha 通道);每个通道 8 比特能表示 \(2^8 = 256\) 个离散取值,故范围为 0–255。

我们人类对图像的语义理解(“这是一只猫”)与机器看到的数字张量之间的鸿沟,被称为语义鸿沟(semantic gap)。这正是图像分类的根本困难所在。

1.1 为什么图像分类很难:六大挑战

老师在讲义中花了相当长的篇幅,与学生互动逐一列举了使像素值发生剧烈变化、从而干扰机器识别的因素。这些挑战值得逐条深入理解:

老师还特别强调了遮挡(Occlusion)上下文(Context)两个例子:当图像里只能看到一条尾巴和一只小爪子时,单看局部很可能是老虎甚至浣熊;但我们知道场景是客厅的沙发,于是几乎可以断定是猫。这说明人类借助上下文先验轻松完成的推理,对算法而言非常困难。当然,得益于 ImageNet 及后续大规模基准数据集的推动,今天的分类器在这些挑战上已经表现得相当出色——本课程的目标,就是一步一步搭建出构成这些大型模型所需的积木。

1.2 从硬编码规则到数据驱动方法

传统计算机科学算法(例如排序)通常由 if-then-else 规则、for 循环和清晰的流程图构成。那么能否为图像分类硬编码(hard code)一套步骤呢?历史上确有此类尝试:例如先做边缘检测(edge detection),再寻找角点(corner)等重要模式,统计特定类型角点的数量等特征,最后据此映射到输出类别。这类方法在受控、变化很小的图像上有过一些成功,但存在两个致命缺陷:

  1. 难以扩展:每识别一种新物体,就要重新设计一套规则,规则数量随类别数爆炸。
  2. 设计代价高昂:为每个物体找到有效的判别逻辑本身就极其费时费力。

因此机器学习带来了新的范式——数据驱动方法(data-driven approach),它包含三步流程:

  1. 收集数据:收集带标签的图像数据集(十二十年前人们靠搜索引擎和图片搜索来爬取数据,如今已有大量现成数据集);
  2. 训练分类器:实现一个 train(images, labels) 函数,输入训练图像及其标签,输出一个能把图像与标签关联起来的模型;
  3. 预测评估:实现一个 predict(model, test_images) 函数,对从未见过的测试图像预测标签。

二、最近邻分类器(Nearest Neighbor)

最近邻分类器是最简单的一种分类器,也是最容易解释的数据驱动方法。它的两个函数简单到极致:

2.1 L1 距离(曼哈顿距离)

要比较“相似度”,需要定义距离函数(distance function)。最常用的选择之一是 L1 距离,也叫曼哈顿距离(Manhattan distance):把两张图像逐像素相减、取绝对值,再把所有绝对差累加:

$$d_1(I_1, I_2) = \sum_p \left| I_1^p - I_2^p \right|$$

其中 \(p\) 遍历图像中的每个像素(每个 RGB 通道值)。例如把一张测试图与一张训练图做逐像素减法得到差值矩阵,把矩阵中所有元素加总,就得到这两张图的距离值。虽然定义简单,L1 距离在很多应用中都非常实用,本课程后面还会反复回访它。

用 Python/NumPy 实现,整个最近邻分类器只需寥寥几行:

import numpy as np

class NearestNeighbor:
    def train(self, X, y):
        self.Xtr = X          # 记住全部训练数据
        self.ytr = y

    def predict(self, X):
        num_test = X.shape[0]
        Ypred = np.zeros(num_test, dtype=self.ytr.dtype)
        for i in range(num_test):
            distances = np.sum(np.abs(self.Xtr - X[i, :]), axis=1)  # L1 距离
            min_index = np.argmin(distances)                        # 最近的一张
            Ypred[i] = self.ytr[min_index]
        return Ypred
重点

复杂度分析(老师课堂提问):设训练集有 \(n\) 个样本,最近邻的 train\(O(1)\)——因为它什么计算都不做,只是把数据留在内存里;而每个测试样本的 predict\(O(n)\)——必须和全部 \(n\) 张训练图逐一计算距离。这个方向恰好和我们的期望相反:我们希望训练慢一点没关系(可以离线做),但预测必须快。老师打了个生动的比方:如果 ChatGPT 每次回答你的问题都要和互联网上所有可能的答案对比一遍,那要等好几年才能返回结果。这个“训练快、预测慢”的倒挂特性,是最近邻在真实系统中几乎不用的核心原因(虽然确实有利用 GPU 加速最近邻检索的研究,但超出了本课范围)。

2.2 从 1-NN 到 K-近邻(KNN)

可视化最近邻在二维特征空间中的行为:每个训练样本是一个带颜色的点,1-最近邻会把整个空间按“离谁最近”划分成若干区域(即 Voronoi 划分),区域颜色代表了落在该区域的测试样本将被预测成的类别。老师随即指出了图中的问题:一个孤零零的黄色样本(很可能是噪声/离群点 outlier)恰好落在绿色样本群的正中央,结果在绿色区域中间硬生生撑起一大块黄色领地——只因为我们只看一个最近邻

解决办法是把最近邻推广为 K-近邻(K-Nearest Neighbor,KNN):取距离最近的 \(K\) 个训练样本,对它们的标签做多数投票(majority voting)。\(K\) 变大后决策边界明显更平滑、对噪声更鲁棒。但 KNN 的可视化中会出现白色区域——那里来自三个类别的近邻票数相同,无法决出胜负。老师特别指出这其实是个有用的诊断工具:白色(模糊)区域正是值得去补充收集数据的区域;在基因学、医学影像等实际问题中,把特征空间可视化后,若发现某片空间没有好样本或存在歧义,就应主动去采集落在该区域的样本。

2.3 L1 与 L2 距离的深入比较

另一个需要选择的是距离函数。除了 L1,另一个常用选择是 L2 距离(欧氏距离,Euclidean distance):逐像素差取平方求和后再开平方根

$$d_2(I_1, I_2) = \sqrt{\sum_p \left( I_1^p - I_2^p \right)^2}$$

老师用“到原点等距离的点的轨迹”来直观对比两者:

更关键的差别在于坐标系旋转(等价于换一组特征基):如果把特征轴旋转,L1 距离的值会完全改变,而 L2 距离保持不变——L1 对特征的坐标系/取值敏感,L2 则对各向同性。由此得出实用准则:

若输入特征是各自有明确含义、需要保留其独立信息的(例如特征向量中每个维度对应一个具体变量),L1 更合适;若特征比较任意(例如纯粹的像素值,坐标系本身没有特别意义),L2 更合适。这一差异也体现在决策边界上:用 L1 的 KNN 边界大多与坐标轴平行(对特征敏感),用 L2 则得到更平滑的边界。

2.4 超参数与调优方法

为什么要讲 KNN?除了它是最简单的数据驱动方法,更重要的原因是它天然引出超参数(hyperparameters)这一核心概念:运行算法前必须人为敲定的选择。KNN 就有两个超参数——近邻数 \(K\)距离函数(L1 还是 L2)。超参数的最优取值高度依赖具体数据集和问题,因此需要系统的调优(hyperparameter tuning)流程。老师逐一评点了三种候选方案:

  1. 选训练集上表现最好的超参数:绝对不行。对最近邻而言 \(K=1\) 永远在训练集上 100% 正确(自己就是自己的最近邻),等于什么也没学到;
  2. 选测试集上表现最好的超参数:同样不可取,而且“永远不要这么做”——这相当于作弊。你只是挑出了恰好在这份测试集上表现好的参数,完全无法说明模型在其他未见数据上的泛化(generalization)能力;
  3. 从训练数据中划出验证集(validation set):正确做法。在训练子集上训练,在验证集上搜索/优化超参数,选定最优超参数后再在测试集上一次性评估。

单验证集也有隐忧:验证集通常小得多,可能不足以代表整体数据分布。于是更可靠的做法是K 折交叉验证(K-fold cross-validation):把训练数据切成 \(K\) 份(课上以 5 份为例),每一轮轮流让其中一份充当验证集,跑 5 次、取 5 次验证精度的平均,以此评估每个候选超参数,最后用最优设置在测试集上评估。老师同时坦率说明:在大规模深度学习中,5 倍训练成本往往难以承受,因此实践中常用“经验直觉 + 单一验证集”;但在视觉之外的小规模场景,学术论文通常要求交叉验证这类统计框架来保证结果可复现。

2.5 在 CIFAR-10 上的实验结果

CIFAR-10 是本课程作业会反复使用的数据集:10 个类别,若干训练图像与测试图像。用最近邻检索测试图像的前 10 个最近邻并可视化,可以立刻看出问题:例如第 4 行的测试图是青蛙,但最近的训练图却像一只——原因在于距离是在原始像素上计算的,两者背景、色调相近,逐像素看确实“很像”。五折交叉验证扫 \(K\) 的曲线显示 \(K=7\) 附近精度最好,约 28%–29%。这看起来不高,但考虑到 10 分类问题随机猜只有 10% 的精度,KNN 确实“学到了东西”,只是改进空间巨大。

另一个令人印象深刻的例子:把一张图整体向右平移一个像素(人眼完全看不出区别),它与原图的像素距离却和它与两张完全不同照片的距离一样大。结论:基于原始像素值的距离度量并不是好的选择,实践中我们从不在真实任务上直接使用像素级 KNN 来做图像分类——更好用的方法正是接下来要讲的线性分类器与后续课程中的神经网络。

拓展

“像素平移一个像素、L2 距离却剧变”这一现象,正是后续课程中卷积神经网络(CNN)要解决的核心问题:卷积核在整幅图上共享权重并滑动扫描,天然对平移具有近似不变性。此外,KNN“预测慢”的缺陷在现代研究中也有补救,例如用 GPU 加速的近似最近邻检索库(FAISS 等)把高维向量的近邻查询做到毫秒级——这类方法被广泛用于向量数据库与检索增强生成(RAG)系统中。KNN 作为最朴素的“非参数、免训练”基线,其思想在嵌入检索(如 CLIP 的零样本分类、开源词汇检测)中依然活跃。

三、线性分类器:深度学习最重要的积木

与最近邻“背下所有数据”的做法不同,线性分类器是一种参数化(parametric)方法:我们要学习一组参数/权重 \(W\),把输入图像直接映射为各类别的输出。函数 \(f(x) = Wx + b\) 的输出通常是图像对每个类别的隶属评分(membership scores)——分数越高,模型越认为图像属于该类。

3.1 代数视角:\(f(x) = Wx + b\)

以 CIFAR-10 为例做维度推演(老师逐步推导):

$$f(x) = Wx + b$$

讲课时为了便于手算,把例子简化为 \(2 \times 2\) 的小图像(4 个像素)和 cat、dog、ship 三个类别:\(x\) 是 \(4 \times 1\) 向量,\(W\) 是 \(3 \times 4\) 矩阵,\(b\) 是 \(3 \times 1\) 向量,\(W\) 的每一行作用于 \(x\) 得到对应类别的一个分数。整个模型的信息全部压缩在 \(W\) 和 \(b\) 里——训练完成后,训练集本身就可以丢掉了,这正是线性分类器预测极快(一次矩阵乘法)的原因。

重点

偏置 \(b\) 的作用:\(b\) 是一个与输入无关的常量偏移,用来微调每个类别评分的“基准线”,使各类别的分数更好地彼此错开。它的重要性在几何视角中看得最清楚(见下节):如果没有 \(b\),所有决策边界都被强制穿过坐标原点,这显然不合理;有了 \(b\),决策边界才能自由平移,得到更可靠、更灵活的分类函数。另一个等价的理解是:分类正确类别的分数需要比错误类别的分数高出一定“安全边际”时,b 参与设定这一边际。

3.2 模板视角:W 的每一行是一张“类别模板”

\(W\) 是 \(10 \times 3072\) 的矩阵,把它的每一行重新折叠回 \(32 \times 32 \times 3\) 的形状,就得到该行对应类别的一张模板(template):评分过程等价于“输入图像与每个类别的模板做逐元素乘加”——图像与某模板越匹配,该类得分越高。在 CIFAR-10 上训练出的真实模板非常有意思:例如“汽车”那一行竟然隐约浮现出一张汽车正面的样子——而这一切仅仅由一个线性分类器完成。同时也能看到线性模型的局限:每个类别只有一个模板,若类内外观差异大(比如正面脸和侧面脸),单一平均模板就显得模糊。

3.3 几何视角:高维空间中的超平面

把图像看作 3072 维空间中的一个点,线性分类器做的事是在这个空间中寻找把一个类别与其他类别分开的直线;在高维空间里,这些分界线推广为超平面(hyperplanes)。每个类别的分数 \(W_i \cdot x + b_i\) 在空间中定义了一个线性函数,分数为零的界面就是该类的决策边界。这里再次呼应了偏置项的作用:没有 \(b\),所有超平面都必须经过原点;有了 \(b\),超平面才能平移到数据真正需要的位置。

3.4 线性分类器做不到的事

线性分类器虽然重要,但能力有明确上限——有些数据天生线性不可分。老师给出三个经典反例(以二维空间为例):

解决之道正是把线性函数堆叠起来构成神经网络:线性分类器是几乎所有深度学习的最重要构件,流行的神经网络架构里“线性函数无处不在”——但中间还必须加入其他成分(下一讲会讲到的非线性激活函数等),才能拟合上述复杂边界。

拓展

在现代深度模型中,线性层(常写作 nn.Linear / Dense,即 \(y = Wx + b\))依然是 Transformer、ResNet 等架构中数量最多的构件之一:Transformer 的注意力头输出投影、FFN 层、各种预测头,本质上都是线性分类器/线性映射。可以说,第 2 讲的 \(f(x) = Wx + b\) 是从“一个分类器”到“一整个网络”的第一步。此外,理论上任何连续函数都能被若干线性层与非线性激活的堆叠逼近(万能逼近定理),这解释了为什么“线性不可分”并不是终点。

四、损失函数:量化“这个 W 有多差”

有了 \(f(x) = Wx + b\),剩下的问题是如何挑选“好”的 \(W\)。为此需要定义损失函数(loss function),也叫目标函数(objective function):它以当前的分类器为输入,输出一个量化“分类器有多糟糕”的数值——即模型在训练数据分数上的“不满意度”。给定数据集 \(\{(x_i, y_i)\}_{i=1}^{N}\),其中 \(x_i\) 是图像,\(y_i\) 是对应标签,损失通常定义为逐样本损失 \(L_i\) 的平均:

$$L = \frac{1}{N} \sum_i L_i \left( f(x_i, W),\; y_i \right)$$

定义好损失后,还需要高效地调整 W 使损失最小化——这个步骤叫优化(optimization),是下一讲的主题。本讲剩下的时间集中在如何定义 \(L_i\)。

4.1 Multiclass SVM(多类支持向量机)与 hinge loss

历史上 CS231N 的经典损失函数之一是多类 SVM 的合页损失(hinge loss)。对样本 \((x_i, y_i)\),正确类别得分为 \(s_{y_i} = f(x_i, W)_{y_i}\),其定义为:

$$L_i = \sum_{j \neq y_i} \max \left( 0,\; s_j - s_{y_i} + \Delta \right)$$

含义是:对每个错误类别 \(j\),只有当它的分数比正确类别分数高出超过安全边际 \(\Delta\)(常取 1)时才累计惩罚;换一种说法,它要求正确类别比所有错误类别至少高出 \(\Delta\) 分,否则差距每缩小一分,损失就增加一分。\(\max(0, \cdot)\) 的形状像一扇合页,故名 hinge。例如三类别得分为 cat/car/frog = \(3.2 / 5.1 / -1.7\) 且真实标签为 car 时,cat 项贡献 \(\max(0, 3.2 - 5.1 + 1) = 0\)(已拉开足够差距),frog 项贡献 \(\max(0, -1.7 - 5.1 + 1) = 0\),故 \(L_i = 0\),这是一组“完美”的分数;反之若正确类分数不占优,损失为正。本季课程字幕中老师选择了直接重点讲授 Softmax 分类器,hinge loss 在备份幻灯片中供课后自学;两者可以并列成一张对照表:

Multiclass SVM (hinge)Softmax(本讲重点)
对分数做什么直接比较分数差(\(s_j - s_{y_i}\))先指数化、再归一化为概率
损失形式\(\max(0,\; s_j - s_{y_i} + \Delta)\)\(-\log P(y_i \mid x_i)\)
输出含义无界的原始分数可解释为类别的概率分布
最小/最大损失最小 0;损失有上界最小 0;损失无上界(可达无穷)

4.2 Softmax 分类器(本讲核心)

老师用 cat/car/frog 三个类别的例子逐步推导。设当前某组 W 对一张猫图输出的原始分数(scores)为:

cat: 3.2    car: 5.1    frog: -1.7

这些分数是线性函数的直接输出,无界且不可控。最好的处理办法是把它们变成概率——即“该图像属于类别 \(k\)”的概率 \(P(y = k \mid x_i)\)。转换分两步,就是著名的 Softmax 函数

  1. 指数化:对每个分数取 \(e\) 的幂。指数函数保证输出恒为正(概率必须非负),同时把分数间的差距指数级放大;
  2. 归一化:除以所有指数化结果之和,使各概率加和为 1。
$$P(y = k \mid x_i) = \frac{e^{s_k}}{\sum_j e^{s_j}} \qquad \left( s_k = f(x_i, W)_k \right)$$

对上面的例子(为便于理解,用与课上同型的数字演示:\(\exp(3.2) \approx 24.5\),\(\exp(5.1) \approx 164.0\),\(\exp(-1.7) \approx 0.18\),总和 \(\approx 188.7\)):

$$P(\text{cat}) \approx \frac{24.5}{188.7} \approx 0.13 \qquad P(\text{car}) \approx \frac{164.0}{188.7} \approx 0.87 \qquad P(\text{frog}) \approx \frac{0.18}{188.7} \approx 0.001$$

语义非常清晰:当前这组参数 \(W\) 认为“这张图是猫”的概率只有约 13%(0.13)——而真实标签是猫,所以这组 \(W\) 显然不好,需要通过优化去修正。原始的、未归一化的分数本身也有专名——logits(未归一化的对数概率)。老师指出这一框架与逻辑回归(logistic regression)完全一致,多类别情形就是多项逻辑回归(multinomial logistic regression);在 CS229 等课程中它也以极大似然估计(maximum likelihood estimation)的名字出现——同一个算法,不同的名字。

4.3 交叉熵损失:从“最大化正确概率”推导

目标很自然:我们希望最大化正确类别的概率(把 0.13 推向 1)。但所有优化目标习惯上写成最小化形式,于是分两步变换:

  1. 取负号:把最大化问题变成最小化问题;
  2. 取对数(log):概率连乘会数值爆炸/下溢,取 log 把乘法变加法、让数值更可控。

由此得到 Softmax 分类的损失函数——负对数似然(negative log likelihood),即交叉熵损失(cross-entropy loss)

$$L_i = -\log P(y = y_i \mid x_i) = -\log \left( \frac{e^{s_{y_i}}}{\sum_j e^{s_j}} \right)$$

老师进一步给出同一损失的两种等价高级视角(这部分讲得非常细致,值得完整记录):

三条路线殊途同归:极大似然 = 最小化 KL 散度 = 最小化交叉熵 = \(-\log P\)(正确类别)。

重点

课堂两道思考题(务必掌握)

问题一:\(L_i\) 的最小值和最大值是多少?最小值为 0:当正确类别概率趋于 1 时,\(-\log(1) = 0\)。最大值为正无穷:当正确类别概率趋于 0 时,\(-\log(p) \to +\infty\)。也就是说交叉熵损失无上界——一个极差的 W 会被罚得极重。(对比:hinge loss 有上界,SVM 分数拉到足够大后损失封顶为 0。)

问题二:训练刚开始时 \(W\) 几乎是随机初始化的,各类分数大致相等,Softmax 的 \(L_i\) 等于多少?分数相等意味着每个类别的概率都约为 \(1/C\)(\(C\) 为类别数),于是 \(L_i = -\log(1/C) =\) \(\log C\)。若 \(C = 10\),则 \(L_i = \ln 10 \approx\) \(2.3\)。这个数字非常有用:如果训练初期你的交叉熵损失明显偏离 \(\log C\),说明初始化或实现可能有问题——这是实践中常用的“健全性检查(sanity check)”。

拓展

Softmax + 交叉熵至今仍是几乎所有多分类深度模型的默认输出配置:从 CNN 分类器、目标检测器的类别预测头,到大语言模型最后预测下一个 token 的词表分布(词表可大到数万类),用的都是本讲的这一个公式。数值实现上,直接计算 \(e^s\) 容易溢出,标准技巧是先减去最大分数(logit 减 max)再做指数化;工程上还常把 softmax 与交叉熵合并为 cross_entropy_loss(logits, labels) 一个算子以获得更稳定的梯度(“softmax + NLL 合并”)。此外,“温度”参数(logits 除以 \(T\) 再 softmax)、标签平滑(把 one-hot 轻微软化)等变体,都是这一基础框架上的发展。

五、本讲要点回顾

上一讲第 1 讲:课程导论 下一讲第 3 讲:正则化与优化