Lecture 2: Image Classification & Linear Classifiers
课程概述
本节课是CS231N计算机视觉课程的第二讲,主要内容包括:
- 图像分类任务的定义与挑战
- 数据驱动方法(Data-Driven Approach)
- K近邻算法(K-Nearest Neighbor, KNN)
- 超参数调优策略
- 线性分类器(Linear Classifier)
- Softmax分类器与损失函数
学习目标:
- 理解图像分类的核心挑战
- 掌握数据驱动方法的三步流程
- 理解K近邻算法的工作原理及其局限性
- 学习超参数调优的最佳实践
- 掌握线性分类器的代数、视觉和几何解释
- 理解Softmax分类器和交叉熵损失函数
目录
1. 图像分类任务定义
1.1 什么是图像分类?
图像分类(Image Classification)是计算机视觉中的核心任务之一,其定义为:
- 输入:一张图像
- 输出:从预定义标签集合中选择一个标签
- 目标:为输入图像分配正确的类别标签
示例:给定一张图像和标签集合 {cat, dog, truck, plane, …},系统需要输出”cat”
1.2 语义鸿沟(Semantic Gap)
对人类而言,图像分类是一个极其简单的任务,因为我们的认知系统能够整体理解图像。但对计算机来说,这是一个巨大的挑战:
- 人类视角:看到一只猫
- 计算机视角:看到一个数字矩阵(张量)
图像的数字表示:
图像尺寸:800 × 600 像素
RGB三通道:Red, Green, Blue
数据结构:800 × 600 × 3 的三维张量
像素值范围:0-255(8位数据)
每个像素的RGB值是一个介于0-255之间的整数,这是标准的24位(3×8位)RGB格式。
2. 图像分类的核心挑战
2.1 视角变化(Viewpoint Variation)
问题:即使物体保持静止,仅仅移动相机(平移、旋转)就会导致所有像素值发生变化。
- 对人类:同一个物体
- 对计算机:完全不同的数据点(800×600×3 = 1,440,000个数值全部改变)
2.2 光照变化(Illumination)
问题:RGB像素值是光照条件的函数。
$$\text{Pixel Value} = f(\text{Object Reflectance}, \text{Light Source}, \text{Camera Response})$$
同一物体在不同光照下会产生不同的像素值:
- 明亮阳光下的猫
- 黑暗房间里的猫
- 对人类:都是同一只猫
- 对计算机:完全不同的数值
2.3 其他主要挑战
| 挑战类型 | 描述 | 影响 |
|---|---|---|
| 背景杂乱 | Background Clutter | 目标物体可能与背景混淆 |
| 尺度变化 | Scale Variation | 物体在图像中的大小不一 |
| 遮挡 | Occlusion | 物体的部分被遮挡 |
| 形变 | Deformation | 物体形状变化(如猫的不同姿态) |
| 类内差异 | Intra-class Variation | 同类物体外观差异大(不同品种的猫) |
2.4 遮挡问题示例
即使只能看到猫尾巴和一小部分毛发,人类也能根据上下文(客厅、沙发)推断这是一只猫,而不是老虎或浣熊。
3. 数据驱动方法
3.1 传统方法的局限性
基于规则的方法(如边缘检测):
# 伪代码示例
def recognize_cat(image):
edges = detect_edges(image)
corners = find_corners(edges)
features = extract_features(corners)
if matches_cat_pattern(features):
return "cat"
问题:
- 难以扩展(每个类别都需要手工设计规则)
- 泛化能力差
- 对变化敏感
3.2 数据驱动的三步流程
现代机器学习采用数据驱动方法,包含三个核心步骤:
步骤1:收集数据集
- 收集大量图像及其对应标签
- 示例数据集:ImageNet, CIFAR-10, CIFAR-100
步骤2:训练分类器
def train(images, labels):
"""
使用机器学习算法训练模型
输入:训练图像和标签
输出:训练好的模型
"""
model = build_model()
model.fit(images, labels)
return model
步骤3:评估分类器
def predict(model, test_images):
"""
在新图像上测试模型
输入:模型和测试图像
输出:预测标签
"""
predictions = model.predict(test_images)
return predictions
4. K近邻算法(KNN)
4.1 算法原理
K近邻是最简单的数据驱动方法之一,其核心思想是:相似的输入应该产生相似的输出。
4.2 算法实现
训练阶段:
def train(images, labels):
"""
训练函数:仅仅记忆所有数据
时间复杂度:O(1)
"""
# 将所有训练数据存储在内存中
self.train_images = images
self.train_labels = labels
预测阶段:
def predict(test_image):
"""
预测函数:找到最相似的训练图像
时间复杂度:O(N) - N为训练样本数
"""
# 计算测试图像与所有训练图像的距离
distances = compute_distances(test_image, self.train_images)
# 找到K个最近邻
k_nearest = find_k_smallest(distances, k)
# 投票决定标签
predicted_label = majority_vote(k_nearest)
return predicted_label
4.3 距离度量
L1 距离(曼哈顿距离)
$$d_{L1}(I_1, I2) = \sum{p} |I_1^p – I_2^p|$$
其中 $p$ 遍历所有像素位置。
实现示例:
import numpy as np
def L1_distance(image1, image2):
"""
计算两张图像的L1距离
"""
return np.sum(np.abs(image1 - image2))
特点:
- 对坐标轴旋转敏感
- 适合特征具有明确物理意义的情况
L2 距离(欧氏距离)
$$d_{L2}(I_1, I2) = \sqrt{\sum{p} (I_1^p – I_2^p)^2}$$
def L2_distance(image1, image2):
"""
计算两张图像的L2距离
"""
return np.sqrt(np.sum((image1 - image2) ** 2))
特点:
- 对坐标轴旋转不敏感
- 更常用于一般情况
L1 vs L2 可视化
在2D空间中:
- L1距离:等距线形成菱形(正方形旋转45°)
- L2距离:等距线形成圆形
L1: 到原点距离为r的点集形成正方形
|x| + |y| = r
L2: 到原点距离为r的点集形成圆形
√(x² + y²) = r
4.4 K值的选择
K=1(最近邻):
- 优点:简单直接
- 缺点:对噪声敏感,决策边界不平滑
K>1(K近邻):
- 优点:对噪声更鲁棒
- 缺点:可能出现投票平局
决策边界可视化:
- K=1:决策边界呈锯齿状,容易过拟合
- K=3,5,7:决策边界更平滑
- K过大:可能导致欠拟合
4.5 KNN的优缺点
优点: ✓ 实现简单 ✓ 易于理解 ✓ 训练时间为O(1)
缺点: ✗ 预测时间为O(N),效率低下 ✗ 需要存储所有训练数据 ✗ 基于像素的距离语义意义不强 ✗ 对高维数据效果差(维度灾难)
计算复杂度分析:
| 阶段 | 时间复杂度 | 说明 |
|---|---|---|
| 训练 | O(1) | 仅存储数据 |
| 预测 | O(N) | 需要与所有训练样本比较 |
这与理想情况相反:我们希望训练慢、预测快,因为训练可以离线进行。
4.6 CIFAR-10 实验结果
数据集:
- 10个类别
- 50,000训练图像
- 10,000测试图像
- 图像尺寸:32×32×3
实验设置:
- 使用5折交叉验证
- 测试不同K值(K=1,3,5,7,9,11…)
结果:
- 最佳K值:K=7
- 最佳准确率:~29%
- 随机猜测基准:10%(10个类别)
结论:KNN在CIFAR-10上表现一般,因为像素距离不能很好地捕捉语义相似性。
5. 超参数调优
5.1 什么是超参数?
超参数(Hyperparameters)是在训练开始前需要设定的参数,例如:
- K近邻中的K值
- 距离度量函数(L1, L2, etc.)
- 学习率(后续课程)
- 正则化强度(后续课程)
5.2 设置超参数的方法
方法1:在训练集上选择(❌ 不推荐)
# 错误示例
best_k = None
best_accuracy = 0
for k in [1, 3, 5, 7, 9]:
accuracy = evaluate_on_train(k) # 在训练集上评估
if accuracy > best_accuracy:
best_k = k
best_accuracy = accuracy
问题:
- K=1总是能在训练集上达到100%准确率(记忆训练数据)
- 无法评估泛化能力
方法2:在测试集上选择(❌ 不推荐)
# 错误示例
best_k = None
best_accuracy = 0
for k in [1, 3, 5, 7, 9]:
accuracy = evaluate_on_test(k) # 在测试集上评估
if accuracy > best_accuracy:
best_k = k
best_accuracy = accuracy
问题:
- 相当于”作弊”
- 无法评估模型在真正未见过数据上的表现
- 过拟合测试集
方法3:使用验证集(✓ 推荐)
# 正确示例
# 步骤1:划分数据
train_set, val_set, test_set = split_data(data)
# 步骤2:在验证集上选择超参数
best_k = None
best_val_accuracy = 0
for k in [1, 3, 5, 7, 9]:
model = train(train_set, k)
val_accuracy = evaluate(model, val_set)
if val_accuracy > best_val_accuracy:
best_k = k
best_val_accuracy = val_accuracy
# 步骤3:用最佳超参数在测试集上评估
final_model = train(train_set, best_k)
test_accuracy = evaluate(final_model, test_set)
数据划分示例:
- 训练集:50,000样本(用于训练模型)
- 验证集:10,000样本(用于选择超参数)
- 测试集:10,000样本(用于最终评估)
方法4:交叉验证(✓✓ 最推荐,但计算昂贵)
K折交叉验证:
def cross_validation(data, k_folds=5):
"""
K折交叉验证
"""
fold_size = len(data) // k_folds
accuracies = []
for fold in range(k_folds):
# 划分数据
val_start = fold * fold_size
val_end = (fold + 1) * fold_size
val_set = data[val_start:val_end]
train_set = data[:val_start] + data[val_end:]
# 训练和评估
model = train(train_set)
accuracy = evaluate(model, val_set)
accuracies.append(accuracy)
return np.mean(accuracies)
5折交叉验证示意图:
Fold 1: [Val][Train][Train][Train][Train]
Fold 2: [Train][Val][Train][Train][Train]
Fold 3: [Train][Train][Val][Train][Train]
Fold 4: [Train][Train][Train][Val][Train]
Fold 5: [Train][Train][Train][Train][Val]
优点:
- 更可靠的超参数选择
- 充分利用数据
- 减少随机性影响
缺点:
- 计算成本高(需要训练K次)
- 在深度学习中较少使用(数据集大,训练耗时)
5.3 最佳实践
小数据集:
- 使用交叉验证
大数据集:
- 使用单个验证集
- 依赖经验和直觉
永远不要:
- 在测试集上调整超参数
- 在训练集上评估泛化性能
6. 线性分类器
6.1 参数化方法
与KNN不同,线性分类器是一种参数化方法:
KNN(非参数):
- 训练:存储所有数据
- 预测:遍历所有数据
线性分类器(参数化):
- 训练:学习参数W和b
- 预测:快速计算 $f(x, W, b)$
6.2 数学表示
线性分类器定义为:
$$f(x, W, b) = Wx + b$$
符号说明:
- $x$:输入图像(拉直成向量)
- $W$:权重矩阵
- $b$:偏置向量
- $f(x, W, b)$:输出分数(每个类别的得分)
维度分析(以CIFAR-10为例):
输入图像:32 × 32 × 3 = 3,072 维
输出类别:10 类
x: [3,072 × 1] 列向量
W: [10 × 3,072] 权重矩阵
b: [10 × 1] 偏置向量
输出 = Wx + b: [10 × 1] 分数向量
计算示例:
import numpy as np
# 输入图像(拉直)
x = np.random.randn(3072, 1) # 32x32x3 = 3072
# 权重和偏置
W = np.random.randn(10, 3072) # 10个类别
b = np.random.randn(10, 1)
# 计算分数
scores = W.dot(x) + b # [10, 1]
6.3 偏置项的作用
无偏置:$f(x, W) = Wx$
- 所有分类超平面必须过原点
有偏置:$f(x, W, b) = Wx + b$
- 允许超平面平移
- 提供更大的灵活性
- 对类别不平衡有帮助
示例:假设某个类别的样本都偏向正值,偏置可以调整该类别的基准分数。
6.4 线性分类器的三种解释
6.4.1 代数解释
线性分类器是一个简单的函数:
$$\begin{bmatrix} s_1 \ s2 \ \vdots \ s{10} \end{bmatrix} = \begin{bmatrix} w_1^T \ w2^T \ \vdots \ w{10}^T \end{bmatrix} \begin{bmatrix} x_1 \ x2 \ \vdots \ x{3072} \end{bmatrix} + \begin{bmatrix} b_1 \ b2 \ \vdots \ b{10} \end{bmatrix}$$
每个类别 $c$ 的分数为: $$s_c = w_c^T x + bc = \sum{i=1}^{3072} w_{c,i} x_i + b_c$$
6.4.2 视觉解释
权重矩阵W的每一行是一个类别的模板:
W的第1行 = cat模板(3,072个数字)
W的第2行 = dog模板(3,072个数字)
...
W的第10行 = truck模板(3,072个数字)
预测过程:
- 将输入图像与每个模板做内积
- 内积值表示相似度
- 选择分数最高的类别
CIFAR-10学到的模板可视化:
- Car模板:可能显示汽车的轮廓
- Airplane模板:可能显示飞机的形状
- 每个类别只学习一个模板(这是线性分类器的局限)
6.4.3 几何解释
在高维空间中,线性分类器定义了分类超平面:
2D情况(2个特征):
- 分类边界是直线
- 方程:$w_1x_1 + w_2x_2 + b = 0$
3D情况(3个特征):
- 分类边界是平面
高维情况(3,072个特征):
- 分类边界是超平面
多类别情况:
- K个类别需要K个超平面
- 空间被划分为K个区域
偏置的几何意义:
- 控制超平面到原点的距离
- 允许超平面不经过原点
6.5 线性分类器的局限性
线性分类器无法解决以下问题:
问题1:XOR问题
类别1:象限1和象限3
类别2:象限2和象限4
无法用一条直线分开。
问题2:同心圆问题
类别1:距原点距离在[1, 2]之间的点
类别2:其他点
需要非线性边界。
问题3:多模态问题
类别1:三个分离的区域
类别2:三个区域之间的空间
单个线性分类器只能学习一个模板,无法捕捉类内的多样性。
解决方案(后续课程):
- 使用非线性激活函数
- 堆叠多个线性层形成神经网络
- 使用特征变换
7. Softmax分类器与损失函数
7.1 损失函数的需求
已知线性函数 $f(x, W) = Wx + b$ 能计算分数,现在需要:
问题1:如何评估当前参数W的好坏? 答案:定义损失函数(Loss Function)
问题2:如何找到最优的W? 答案:优化算法(下节课内容)
7.2 损失函数定义
数学表示:
$$L = \frac{1}{N} \sum_{i=1}^{N} L_i(f(x_i, W), y_i)$$
其中:
- $N$:训练样本数量
- $L_i$:第i个样本的损失
- $f(x_i, W)$:模型对第i个样本的预测分数
- $y_i$:第i个样本的真实标签
直观理解:
- 损失函数量化模型的”不开心程度”
- 损失越小,模型越好
- 目标:最小化损失
7.3 Softmax分类器
7.3.1 从分数到概率
线性函数输出的是未归一化的分数(logits):
$$s = f(x, W) = [s_1, s_2, \ldots, s_K]$$
问题:分数没有概率意义(可以是负数,可以很大)
解决:使用Softmax函数转换为概率分布:
$$P(Y=k|X=x_i) = \frac{e^{sk}}{\sum{j=1}^{K} e^{s_j}}$$
Softmax的特性:
- 非负性:$e^{s_k} > 0$
- 归一化:$\sum_{k=1}^{K} P(Y=k|X=x_i) = 1$
- 单调性:分数越高,概率越大
- 可微性:便于梯度下降优化
7.3.2 数值示例
假设某个样本的分数为:
s = [3.2, 5.1, -1.7] # cat, car, frog
真实标签:cat (索引0)
步骤1:指数化
exp(s) = [e^3.2, e^5.1, e^-1.7]
= [24.5, 164.0, 0.18]
步骤2:归一化
sum = 24.5 + 164.0 + 0.18 = 188.68
P(cat) = 24.5 / 188.68 = 0.13
P(car) = 164.0 / 188.68 = 0.87
P(frog) = 0.18 / 188.68 = 0.001
解释:模型认为这张图像是cat的概率为13%(错误的预测!)
7.4 交叉熵损失
7.4.1 定义
对于单个样本,Softmax损失定义为:
$$L_i = -\log P(Y=y_i|X=xi) = -\log \left( \frac{e^{s{y_i}}}{\sum_j e^{s_j}} \right)$$
简化: $$Li = -s{y_i} + \log \sum_j e^{s_j}$$
7.4.2 直觉理解
目标:最大化正确类别的概率 $$\max P(Y=y_i|X=x_i)$$
等价于:最小化负对数概率 $$\min -\log P(Y=y_i|X=x_i)$$
为什么用对数?
- 将乘法转换为加法(数值稳定)
- 将概率(0-1)映射到损失(0-∞)
- 惩罚错误预测更严重
7.4.3 损失范围
理论范围:
- 最小值:0(当 $P(Y=y_i|X=x_i) = 1$ 时)
- 最大值:$\infty$(当 $P(Y=y_i|X=x_i) \to 0$ 时)
初始化检查: 在随机初始化时,所有类别概率应该接近相等:
$$P(Y=k|X=x_i) \approx \frac{1}{C}$$
因此初始损失应该约为:
$$L_i \approx -\log\left(\frac{1}{C}\right) = \log(C)$$
CIFAR-10示例(C=10): $$L_i \approx \log(10) = 2.3$$
调试技巧:如果初始损失不是 $\log(C)$,说明实现有bug!
7.5 交叉熵的信息论解释
7.5.1 与KL散度的关系
真实分布 $p$(one-hot编码): $$p = [0, 0, \ldots, 1, \ldots, 0]$$ (只有正确类别为1,其他为0)
预测分布 $q$(Softmax输出): $$q = [P(Y=1), P(Y=2), \ldots, P(Y=K)]$$
KL散度: $$D_{KL}(p | q) = \sum_k p_k \log \frac{p_k}{q_k}$$
当 $p$ 是one-hot时: $$D{KL}(p | q) = -\log q{y_i}$$
这正是Softmax损失!
7.5.2 与熵的关系
交叉熵: $$H(p, q) = -\sum_k p_k \log q_k$$
关系: $$H(p, q) = H(p) + D_{KL}(p | q)$$
对于one-hot编码的 $p$:
- $H(p) = 0$(完全确定)
- 因此 $H(p, q) = D_{KL}(p | q)$
命名由来:这就是为什么Softmax损失也称为交叉熵损失。
7.6 实现细节
数值稳定性
直接计算 $e^{s_k}$ 可能导致数值溢出。
改进: $$\frac{e^{s_k}}{\sum_j e^{s_j}} = \frac{Ce^{s_k}}{C\sum_j e^{s_j}} = \frac{e^{s_k + \log C}}{\sum_j e^{s_j + \log C}}$$
选择 $C = e^{-\max_j s_j}$:
def softmax_stable(scores):
"""
数值稳定的Softmax实现
"""
# 减去最大值防止溢出
scores_shifted = scores - np.max(scores)
exp_scores = np.exp(scores_shifted)
probs = exp_scores / np.sum(exp_scores)
return probs
def cross_entropy_loss(scores, y):
"""
交叉熵损失
"""
probs = softmax_stable(scores)
loss = -np.log(probs[y])
return loss
核心要点总结
关键概念
-
图像分类的挑战
- 语义鸿沟:人类感知 vs 计算机表示
- 视角、光照、遮挡、形变等变化
-
数据驱动方法
- 收集数据 → 训练模型 → 评估预测
- 替代手工设计规则
-
K近邻算法
- 简单但效率低(训练O(1),预测O(N))
- 距离度量:L1(曼哈顿)vs L2(欧氏)
- 实际应用中效果有限(基于像素距离)
-
超参数调优
- 使用验证集,不要用测试集
- 交叉验证提供更可靠的结果
- 在深度学习中常用单个验证集
-
线性分类器
- 参数化方法:$f(x, W) = Wx + b$
- 三种解释:代数(加权和)、视觉(模板匹配)、几何(超平面)
- 局限性:无法解决非线性可分问题
-
Softmax分类器
- 将分数转换为概率:Softmax函数
- 损失函数:交叉熵(负对数似然)
- 初始化检查:损失应约为 $\log(C)$
重要公式
| 概念 | 公式 |
|---|---|
| L1距离 | $d_{L1}(I_1, I_2) = \sum_p |I_1^p – I_2^p|$ |
| L2距离 | $d_{L2}(I_1, I_2) = \sqrt{\sum_p (I_1^p – I_2^p)^2}$ |
| 线性分类器 | $f(x, W, b) = Wx + b$ |
| Softmax | $P(Y=k|X) = \frac{e^{s_k}}{\sum_j e^{s_j}}$ |
| 交叉熵损失 | $L = -\log P(Y=y_i|X=x_i)$ |
| 总损失 | $L = \frac{1}{N} \sum_{i=1}^N L_i$ |
下节预告
- 优化算法:如何找到最优的W?
- 梯度下降:迭代更新参数
- 反向传播:高效计算梯度
- 神经网络:堆叠多个线性层
参考资料
数据集
- CIFAR-10: https://www.cs.toronto.edu/~kriz/cifar.html
- ImageNet: https://www.image-net.org/
在线工具
- KNN可视化: CS231N课程网站提供交互式demo
相关阅读
- 《Deep Learning》 – Goodfellow et al. (Chapter 5: Machine Learning Basics)
- 《Pattern Recognition and Machine Learning》 – Bishop (Chapter 1)
课程资源
- CS231N课程笔记:https://cs231n.github.io/
- 讲义和作业:课程官网
附录:Python实现示例
完整的KNN实现
import numpy as np
from collections import Counter
class KNearestNeighbor:
def __init__(self):
pass
def train(self, X, y):
"""
训练KNN分类器(仅存储数据)
参数:
- X: 训练数据,shape [N, D]
- y: 训练标签,shape [N,]
"""
self.X_train = X
self.y_train = y
def predict(self, X, k=1, distance_metric='L2'):
"""
预测测试数据的标签
参数:
- X: 测试数据,shape [M, D]
- k: 近邻数量
- distance_metric: 'L1' 或 'L2'
返回:
- y_pred: 预测标签,shape [M,]
"""
if distance_metric == 'L1':
distances = self.compute_L1_distances(X)
else:
distances = self.compute_L2_distances(X)
return self.predict_labels(distances, k)
def compute_L1_distances(self, X):
"""
计算L1距离
"""
num_test = X.shape[0]
num_train = self.X_train.shape[0]
distances = np.zeros((num_test, num_train))
for i in range(num_test):
distances[i, :] = np.sum(np.abs(self.X_train - X[i, :]), axis=1)
return distances
def compute_L2_distances(self, X):
"""
计算L2距离(向量化实现)
"""
# 使用矩阵运算加速
# (a-b)^2 = a^2 + b^2 - 2ab
num_test = X.shape[0]
num_train = self.X_train.shape[0]
test_sum = np.sum(X**2, axis=1, keepdims=True) # [M, 1]
train_sum = np.sum(self.X_train**2, axis=1, keepdims=True).T # [1, N]
cross_term = X @ self.X_train.T # [M, N]
distances = np.sqrt(test_sum + train_sum - 2 * cross_term)
return distances
def predict_labels(self, distances, k=1):
"""
根据距离预测标签
"""
num_test = distances.shape[0]
y_pred = np.zeros(num_test, dtype=self.y_train.dtype)
for i in range(num_test):
# 找到k个最近邻的索引
k_nearest_indices = np.argsort(distances[i])[:k]
# 获取这k个样本的标签
k_nearest_labels = self.y_train[k_nearest_indices]
# 投票
y_pred[i] = Counter(k_nearest_labels).most_common(1)[0][0]
return y_pred
# 使用示例
if __name__ == "__main__":
# 生成模拟数据
np.random.seed(42)
X_train = np.random.randn(100, 3072) # 100个样本,3072维
y_train = np.random.randint(0, 10, 100) # 10个类别
X_test = np.random.randn(20, 3072)
# 训练和预测
knn = KNearestNeighbor()
knn.train(X_train, y_train)
y_pred = knn.predict(X_test, k=5, distance_metric='L2')
print("预测标签:", y_pred)
线性分类器实现
class LinearClassifier:
def __init__(self, num_classes, input_dim):
"""
初始化线性分类器
参数:
- num_classes: 类别数量
- input_dim: 输入维度
"""
# 随机初始化权重(小随机数)
self.W = 0.001 * np.random.randn(num_classes, input_dim)
self.b = np.zeros(num_classes)
def forward(self, X):
"""
前向传播:计算分数
参数:
- X: 输入数据,shape [N, D]
返回:
- scores: 每个类别的分数,shape [N, C]
"""
scores = X @ self.W.T + self.b # [N, C]
return scores
def softmax_loss(self, X, y):
"""
计算Softmax损失
参数:
- X: 输入数据,shape [N, D]
- y: 真实标签,shape [N,]
返回:
- loss: 标量损失值
"""
num_samples = X.shape[0]
scores = self.forward(X) # [N, C]
# 数值稳定的Softmax
scores_shifted = scores - np.max(scores, axis=1, keepdims=True)
exp_scores = np.exp(scores_shifted)
probs = exp_scores / np.sum(exp_scores, axis=1, keepdims=True)
# 计算损失
correct_log_probs = -np.log(probs[range(num_samples), y])
loss = np.sum(correct_log_probs) / num_samples
return loss
def predict(self, X):
"""
预测类别
"""
scores = self.forward(X)
y_pred = np.argmax(scores, axis=1)
return y_pred
# 使用示例
if __name__ == "__main__":
# 模拟CIFAR-10数据
num_train = 1000
num_classes = 10
input_dim = 3072
X_train = np.random.randn(num_train, input_dim)
y_train = np.random.randint(0, num_classes, num_train)
# 初始化分类器
classifier = LinearClassifier(num_classes, input_dim)
# 计算初始损失(应该约为log(10) ≈ 2.3)
initial_loss = classifier.softmax_loss(X_train, y_train)
print(f"初始损失: {initial_loss:.4f}")
print(f"理论值: {np.log(num_classes):.4f}")
学习建议:
- 动手实现KNN和线性分类器
- 在CIFAR-10上运行实验
- 可视化决策边界和学到的权重
- 思考如何改进这些基础方法
下节课预习:
- 梯度下降优化
- 反向传播算法
- 神经网络基础
留言