# 图像分类 Image Classification

图像分类的核心任务:给定一张图像和一组类别标签,设计算法将其中一个标签分配给此图像。

图像在计算机中就是一个巨大的数字网格,每个像素值介于 [0,255] 之间。对于一个 800×600 分辨率的彩色图像,其数据张量为 800 × 600 × 3,因为有 RGB 三个颜色通道(红、绿、蓝)。

语义鸿沟 Semantic Gap:人类看到图像能轻松识别物体,但计算机看到的只是一个巨大的整数矩阵。这个差距就是我们需要跨越的核心问题。

  • 图像分类面临的六大挑战:
    • 视角变化 viewpoint variation:即使物体完全静止,只要相机视角发生微小变化,数据张量就可能完全不同
    • 光照条件 illumination conditions:RGB 像素值是表面材料颜色与光源共同作用的函数,同一物体在不同光线下数值差异巨大
    • 形变 deformation:物体本身具有非刚性,姿态变化导致像素分布改变
    • 遮挡 occlusion:物体被部分遮挡,人类可以通过部分信息做出精确判断,计算机则非常困难
    • 背景杂乱 background clutter:物体与背景混杂在一起,难以分离前景和背景
    • 类内变化 intraclass variation:同一类别内的个体差异本身就很大(比如不同品种、不同颜色的猫)

传统的基于边缘检测器的方法(找到图像边缘 → 提取特征 → 映射成输出类)效果有限,无法应对这些复杂性。

数据驱动方法 Data-driven approach 的核心思路:

  • 收集足够多的图像及其标签的数据集
  • 使用机器学习算法训练分类器——用训练函数接受图像与标签,建立一个将图像与标签关联的模型
  • 在新的图像上测试分类器——创建预测函数,输入测试图像,预测标签并返回

CIFAR-10 训练集示例

def train(images, labels):
    # 记忆数据与标签
    return model

def predict(model, test_images):
    # 为测试图像找到最相近的训练图像,输出该图像标签
    return test_labels

# 最近邻分类器 Nearest Neighbor Classifier

最简单的数据驱动方法:记住所有训练数据,预测时在训练集中找到与测试图像最相似的一张,输出其标签。

我们需要一个距离函数来衡量两张图片的相似程度。

L1 距离(曼哈顿距离 Manhattan Distance):

d1(I1,I2)=pI1pI2pd_1(I_1, I_2) = \sum_p |I_1^p - I_2^p|

对每个像素位置 p,直接求像素差的绝对值并累加。如果两张图完全一样,L1 距离为零;差异越大,距离值越大。

L2 距离(欧氏距离 Euclidean Distance):

d2(I1,I2)=p(I1pI2p)2d_2(I_1, I_2) = \sqrt{\sum_p (I_1^p - I_2^p)^2}

L2 是几何意义上的直线距离。

完整的 Nearest Neighbor 分类器实现:

import numpy as np

class NearestNeighbor:
    def __init__(self):
        pass

    def train(self, X, y):
        """训练就是记住所有训练数据,只需 O(1) 时间"""
        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):
            # 使用 L1 距离:广播计算测试样本与所有训练样本的差值
            distances = np.sum(np.abs(self.Xtr - X[i, :]), axis=1)
            # 找到距离最小的训练样本索引
            min_index = np.argmin(distances)
            # 输出该训练样本的标签
            Ypred[i] = self.ytr[min_index]

        return Ypred

这个分类器的设计哲学是:训练快(O(1)),预测慢(O(N))。但实际应用中我们恰好需要相反的特性——可以接受训练慢一些,但预测必须快,因为用户不会等。

  • L1 与 L2 距离的关键区别:
    • L1 距离对特征值敏感:它的决策边界几乎平行于坐标轴,对图像的旋转会导致 L1 形状变化
    • L2 距离对特征值不敏感:它的决策边界不受坐标轴方向限制,旋转图像后 L2 看不出变化
    • 实际选择取决于特征向量中每个维度的含义——如果各维度有明确的物理意义,L1 可能更好;如果只是通用空间向量,L2 更合适

L1 与 L2 距离对坐标系的敏感度差异


# K-邻近算法 K-Nearest Neighbor

最邻近算法可以自然地扩展为 K 邻近算法:选择距离最近的 K 个邻居进行多数投票,票数最多的类别作为预测结果。

  • 当 K > 1 时,决策边界变得更平滑,对噪声和离群点更鲁棒
  • 图中白色区域是 K 个邻居中平票的区域,无法确定标签——说明这个区域数据不足,适合收集更多数据

不同 K 值下的决策边界(K=1,3,5)

CIFAR-10 数据集上(10 个类别,50000 张训练集,10000 张测试集),KNN 的准确率约为 28%~29%,仅比随机猜测(10%)好一些。

超参数 Hyperparameters 是由用户设定的变量(如 K 值、距离函数的选择),模型无法从数据中自动学到。如何选择超参数是机器学习中的关键问题。

  • 四种超参数设置策略(递进式推理):
    1. 选择在训练集上表现最好的超参数:模型会"记住"训练数据,训练准确率始终接近 100%,但严重过拟合,无法泛化到新数据
    2. 选择在测试集上表现最好的超参数:这是"作弊"行为——测试集信息泄露到超参数选择中,我们无法知道算法在新数据上的真实表现
    3. 将数据分为训练集 train、验证集 validation、测试集 test:在训练集上用不同超参数训练,在验证集上评估并选出最佳超参数,最后仅在测试集上运行一次得到最终结果。测试集是一种极其宝贵的资源,在最后一步之前永远不要碰它。这个方法的难点在于需要选择合适的验证集划分
    4. K-Fold 交叉验证 Cross-Validation:将训练数据折叠成 K 个褶皱,每个褶皱轮流充当一次验证集,其余 K-1 个做训练集。训练 K 次后取平均精度。最后用最佳超参数在全部训练数据上重新训练,在测试集上评估一次。这可以产生更可靠的结果,但在大数据集上计算负担很重,深度学习实践中不常用
# 交叉验证示例:在 CIFAR-10 上为 KNN 寻找最佳 K 值
# 假设 Xtr_rows 和 Ytr 是训练数据
Xval_rows = Xtr_rows[:1000, :]    # 取前 1000 个作为验证集
Yval = Ytr[:1000]
Xtr_rows = Xtr_rows[1000:, :]     # 剩余作为训练集
Ytr = Ytr[1000:]

validation_accuracies = []
for k in [1, 3, 5, 10, 20, 50, 100]:
    # 对每个 k 值评估验证集精度
    nn = NearestNeighbor()
    nn.train(Xtr_rows, Ytr)
    Yval_predict = nn.predict(Xval_rows, k=k)
    acc = np.mean(Yval_predict == Yval)
    validation_accuracies.append((k, acc))

实验结果表明 CIFAR-10 上最优 K 值约为 7

交叉验证中不同 K 值的分类精度

K-Fold 交叉验证示意图

KNN 的三大缺陷

  • 预测时间极慢:训练 O(1),预测 O(N),与实际需求完全相反。我们关注的是测试效率,用户不会为了分类一张图片等上几分钟
  • 像素距离不等于语义距离:从人眼来看四张完全不同的图片,它们的 L2 像素距离可能完全相同——背景、姿态、光照的不同导致逐像素比较彻底失效
  • 维度灾难 Curse of Dimensionality:在高维空间中,距离的概念变得反直觉——所有点看起来都很远,最近邻失去意义。要密集覆盖高维空间,所需样本数以指数级增长

但 KNN 仍然是一个理解数据驱动方法和超参数调优的绝佳起点。


# 线性分类器 Linear Classifier

线性分类器是整个神经网络和卷积网络的基础构件。大规模神经网络实质上就是这些基础单元一层层堆叠起来的。

# 核心公式

f(x,W)=Wx+bf(x, W) = Wx + b

  • x:输入图像,展开为列向量。CIFAR-10 图像为 32×32×3 = 3072 维
  • W:权重矩阵 Weights,又称为参数。输出是 10 个类别的得分,所以 W 的维度为 10 × 3072
  • b:偏置向量 Bias,维度为 10 × 1,表示数据独立的类别偏好值(例如数据集中猫的图像比狗多,则猫对应的偏置会更大)
  • f(x, W):输出是一个 10 维向量,每个元素对应一个类别的得分 score

W 的每一行对应一个类别的分类器。线性分类器的几何本质是:在 3072 维空间中找到一个超平面,将不同类别区分开来

如果没有偏置 b,每条分界线都必须穿过原点,分类将失去灵活性——比如当某个类别的所有样本都落在第一象限时,穿过原点的直线无法很好地分离它们。

# 技巧:将 W 和 b 合并

在 x 向量的末尾添加一个常数维度 1,同时将 b 作为新的一列附加到 W 中:

f(x,W)=Wxf(x, W) = Wx

这样公式更简洁,实现时也更方便。

偏置技巧:将 b 合并入 W

# 模板匹配视角 Template Matching

线性分类器还可以从另一个角度理解:

  • W 的每一行可以重新排列成与输入图像相同的大小(32×32×3),想象成一张"模板"图像
  • 线性分类就是使用内积来比较输入图像与每个类别的模板,找到最相似的那个
  • 分类器为例:蓝色通道(水和天空)有许多正权重,而红色和绿色通道多为负权重——这恰好是一张蓝色大海背景下船只轮廓的模糊模板

CIFAR-10 学习到的权重模板

# 图像预处理

在输入分类器之前,建议将像素值从 [0, 255] 归一化到 [-1, 1] 范围,有助于训练稳定。

# 线性分类器的局限性

线性分类器每类只能学习一个模板,无法处理多模态数据。以奇偶像素计数分类为例:蓝色类别在平面上占据两个相反的象限——没有办法绘制一条单独的直线来同时覆盖两个离散的蓝色区域

以下三类问题线性分类器无法解决:

  • 奇偶问题(类别按奇偶数分布在两个相反的象限)
  • 同心圆问题(内外圆分属两类)
  • 多模态分布(同一类分布在空间中不相邻的多个区域)

这些局限正是后续引入神经网络和非线性激活函数的原因。


# 损失函数 Loss Function

现在的问题是:如何量化 W 的好坏? 损失函数用于衡量分类器的"糟糕程度"。我们的目标是找到一个 W 使得损失函数值最小。

# 多分类 SVM 损失 Multiclass SVM Loss(Hinge Loss)

Li=jyimax(0,sjsyi+Δ)L_i = \sum_{j \neq y_i} \max(0, s_j - s_{y_i} + \Delta)

  • syis_{y_i}:正确类别的得分
  • sjs_j:错误类别的得分
  • Δ\Delta:安全间隔 margin,通常取 1

直观理解:我们希望正确类别的得分比所有错误类别高出至少 Δ\Delta。如果某个错误类别的得分与正确类别差距小于 Δ\Delta,则产生损失;如果差距足够大(大于 Δ\Delta),则损失为零。这个损失被称为 Hinge Loss,因为它的形状像合页一样在 Δ\Delta 处弯折。

多分类 SVM 损失示意图

# Softmax 分类器(交叉熵损失 Cross-Entropy Loss)

将得分转化为概率分布:

  1. 指数化:取 eske^{s_k},确保所有值 > 0
  2. 归一化:除以所有指数之和 jesj\sum_j e^{s_j},使所有概率之和为 1

P(Y=kX=xi)=eskjesjP(Y=k|X=x_i) = \frac{e^{s_k}}{\sum_j e^{s_j}}

例如模型输出 [cat: 0.13, car: 0.87, frog: 0.00],意味着模型认为这张图是猫的概率为 13%,是车的概率为 87%,是青蛙的概率为 0%。

损失函数直接取负对数似然 Negative Log Likelihood

Li=logP(Y=yiX=xi)L_i = -\log P(Y=y_i|X=x_i)

本质是在做最大似然估计 Maximum Likelihood Estimation——我们希望正确类别的概率越大越好。

# 交叉熵与 KL 散度的关系

H(P,Q)=xP(x)logQ(x)H(P, Q) = -\sum_x P(x) \log Q(x)

DKL(PQ)=xP(x)logP(x)Q(x)D_{KL}(P \parallel Q) = \sum_x P(x) \log \frac{P(x)}{Q(x)}

H(P,Q)=H(P)+DKL(PQ)H(P, Q) = H(P) + D_{KL}(P \parallel Q)

  • P 是真实概率分布,Q 是模型预测的概率分布
  • H(P) 是真实分布的熵(常数,真实标签固定不变)
  • 交叉熵 = KL 散度 + 常数,所以最小化交叉熵就是在最小化 KL 散度,让预测分布逼近真实分布

SVM vs Softmax 损失函数对比

# 两个实用的 Debug 问题

Q1:Softmax 损失函数的最大值是多少?

理论上为无穷大。当正确类别的概率趋近于 0 时,log(0)-\log(0) \to \infty。实际中由于数值精度限制,概率不会精确为零,但可以非常大。

Q2:初始化时所有权重为小随机数,所有 sis_i 近似相等,损失函数的值是多少?

所有类别概率相等,P=1/CP = 1/C

Li=log(1/C)=log(C)L_i = -\log(1/C) = \log(C)

当 C = 10 时,Li=log(10)2.3L_i = \log(10) \approx 2.3。这是一个很有用的 sanity check:训练刚开始时如果 loss 偏离这个值太多,说明实现中很可能有 bug。

# 正则化 Regularization

完整损失函数 = 数据损失 Data Loss + 正则化项 Regularization

L=1NiLi+λR(W)L = \frac{1}{N} \sum_i L_i + \lambda R(W)

  • 数据损失:让模型拟合训练数据,最小化预测误差
  • 正则化项:惩罚复杂模型,鼓励简单权重,防止过拟合
  • λ\lambda:平衡两个目标的超参数

常见的正则化方式:

  • L2 正则化R(W)=klWk,l2R(W) = \sum_k \sum_l W_{k,l}^2,鼓励权重分散到所有维度,惩罚个别过大的权重值
  • L1 正则化R(W)=klWk,lR(W) = \sum_k \sum_l |W_{k,l}|,鼓励稀疏权重
  • Elastic Net:L1 + L2 的结合

# 声明

本blog由Yumengmeng基于2025春季李飞飞斯坦福CS231n计算机视觉课程的视频内容结合Claude Code抓取网上开源笔记进行美化与排版,仅供个人复习使用。