文章目录
kNN需要向量才能算距离 ,所以必须把图片从 (32, 32, 3) 展平成 (3072,) 向量。每个3072维向量就是一张图片的所有像素信息排成一行。
类比:就像把一张照片从左到右、从上到下扫描一遍,变成一串数字,然后比较两串数字的相似程度。
整体流程:图像文件 → 加载成数组 → 采样减少数量 → 展平成向量 → 计算距离
一、数据流程(5步走)
第1步:原始数据是什么?
CIFAR-10数据集:5万个训练图片 + 1万个测试图片,每张图片32×32像素,RGB彩色。
存储格式 :存在pickle文件里,每个文件存1万张图片。图片被压扁成一串数字:(10000, 3072),3072 = 32×32×3(宽×高×RGB)。
第2步:加载数据(把文件读成数组)
代码 (cs231n/data_utils.py):
python
def load_CIFAR_batch(filename):
with open(filename, 'rb') as f:
datadict = load_pickle(f)
X = datadict['data'] # 形状 (10000, 3072) - 一串数字
Y = datadict['labels'] # 10000个标签
# 重新整理成图片格式
X = X.reshape(10000, 3, 32, 32) # 变成 (10000, 3, 32, 32)
X = X.transpose(0,2,3,1) # 变成 (10000, 32, 32, 3)
X = X.astype("float") # 转成浮点数
return X, Y
代码解释:
reshape(10000, 3, 32, 32):把3072个数字重新排列,变成3个通道、每个32×32的图片transpose(0,2,3,1):调整维度顺序,从(通道,高,宽)变成(高,宽,通道),这样Matplotlib才能正确显示astype("float"):把整数像素值(0-255)转成浮点数,方便计算
加载完整数据集:
python
def load_CIFAR10(ROOT):
xs, ys = [], []
# 加载5个训练批次
for b in range(1,6):
X, Y = load_CIFAR_batch(f'data_batch_{b}')
xs.append(X) # 每个X形状 (10000, 32, 32, 3)
ys.append(Y)
# 合并所有训练数据
Xtr = np.concatenate(xs) # (50000, 32, 32, 3)
Ytr = np.concatenate(ys) # (50000,)
# 加载测试数据
Xte, Yte = load_CIFAR_batch('test_batch')
return Xtr, Ytr, Xte, Yte
结果:
- 训练集:5个文件合并 →
(50000, 32, 32, 3)(5万张图片) - 测试集:1个文件 →
(10000, 32, 32, 3)(1万张图片)
第3步:采样(减少数量,加快实验)
代码 (knn.ipynb):
python
# 采样训练数据
num_training = 5000
mask = list(range(num_training))
X_train = X_train[mask] # 从50000采样到5000
y_train = y_train[mask]
# 采样测试数据
num_test = 500
mask = list(range(num_test))
X_test = X_test[mask] # 从10000采样到500
y_test = y_test[mask]
代码解释:
list(range(5000)):生成索引[0,1,2,...,4999]X_train[mask]:用索引切片,只取前5000个样本- 为什么采样? 数据太多算得慢,先拿一部分试试效果
结果:
- 训练集:
(5000, 32, 32, 3) - 测试集:
(500, 32, 32, 3)
第4步:展平成向量(最关键!)
代码 (knn.ipynb):
python
# Reshape the image data into rows
X_train = np.reshape(X_train, (X_train.shape[0], -1))
X_test = np.reshape(X_test, (X_test.shape[0], -1))
print(X_train.shape, X_test.shape)
# 输出: (5000, 3072) (500, 3072)
代码解释:
X_train.shape[0]:第一维大小,即5000(样本数)-1:自动计算,-1 = 32×32×3 = 3072reshape(5000, -1):保持5000行,把后面的维度展平成一列
第5步:计算L2距离
现在每张图片都是向量了,可以算距离了:
代码 (knn.ipynb):
python
classifier = KNearestNeighbor()
classifier.train(X_train, y_train) # 只是保存数据,不训练
dists = classifier.compute_distances_two_loops(X_test)
# dists 形状: (500, 5000)
# dists[i, j] = 第i个测试图片和第j个训练图片的距离
距离计算代码 (k_nearest_neighbor.py):
python
def compute_distances_two_loops(self, X):
num_test = X.shape[0] # 500
num_train = self.X_train.shape[0] # 5000
dists = np.zeros((num_test, num_train))
for i in range(num_test):
for j in range(num_train):
# 计算L2距离:对应位置相减、平方、求和、开根号
dists[i, j] = np.sqrt(np.sum((X[i] - self.X_train[j]) ** 2))
return dists
代码解释:
X[i]:第i个测试样本,形状(3072,)self.X_train[j]:第j个训练样本,形状(3072,)(X[i] - self.X_train[j]):对应位置相减,形状(3072,)** 2:每个元素平方np.sum(...):求和,得到一个数np.sqrt(...):开根号,得到距离
距离公式 : d = ∑ k = 1 3072 ( x k − y k ) 2 d = \sqrt{\sum_{k=1}^{3072}(x_k - y_k)^2} d=∑k=13072(xk−yk)2 ,就是两个向量对应位置差的平方和再开根号。
二、图片如何被"压扁"?用例子说明
例子1:2×2的小图片(理解原理)
假设有一张2×2像素的RGB图片,形状是 (2, 2, 3):
python
# 原始图片 (2, 2, 3)
image = [
# 第1行
[[100, 150, 200], # 像素(0,0): R=100, G=150, B=200
[110, 160, 210]], # 像素(0,1): R=110, G=160, B=210
# 第2行
[[120, 170, 220], # 像素(1,0): R=120, G=170, B=220
[130, 180, 230]] # 像素(1,1): R=130, G=180, B=230
]
展平过程:
python
# 用reshape展平
vector = np.reshape(image, (-1,))
# 结果: [100, 150, 200, 110, 160, 210, 120, 170, 220, 130, 180, 230]
# ↑像素(0,0)↑ ↑像素(0,1)↑ ↑像素(1,0)↑ ↑像素(1,1)↑
规律 :按行优先顺序,从左到右、从上到下,把每个像素的RGB值依次排列。
例子2:32×32的真实图片
一张32×32的RGB图片,形状是 (32, 32, 3):
python
# 原始图片 (32, 32, 3)
image = [
# 第1行(32个像素)
[[R00, G00, B00], [R01, G01, B01], ..., [R0_31, G0_31, B0_31]],
# 第2行(32个像素)
[[R10, G10, B10], [R11, G11, B11], ..., [R1_31, G1_31, B1_31]],
# ...
# 第32行(32个像素)
[[R31_0, G31_0, B31_0], ..., [R31_31, G31_31, B31_31]]
]
展平过程:
python
vector = np.reshape(image, (-1,))
# 结果: [R00, G00, B00, R01, G01, B01, ..., R0_31, G0_31, B0_31,
# R10, G10, B10, R11, G11, B11, ..., R1_31, G1_31, B1_31,
# ...
# R31_0, G31_0, B31_0, ..., R31_31, G31_31, B31_31]
长度计算:32行 × 32列 × 3通道 = 3072个数字
三、reshape的工作原理
reshape不改变数据,只改变排列方式:
python
# 原始数据在内存中是一串连续的数字
# reshape只是告诉NumPy如何解释这串数字
# 例子:12个数字
data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
# 解释成 2×2×3 的图片
image = np.reshape(data, (2, 2, 3))
# [[[1, 2, 3], [4, 5, 6]],
# [[7, 8, 9], [10, 11, 12]]]
# 解释成 12 的向量(展平)
vector = np.reshape(image, (-1,))
# [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
# 数据没变,只是形状变了!
关键理解:
- 数据在内存中就是一串连续的数字
reshape只是改变如何解释这串数字- 从
(32, 32, 3)变成(3072,),数据本身没变,只是从"三维表格"变成了"一维列表"
四、实际代码演示
python
import numpy as np
# 创建一张2×2的RGB图片(随机值)
image = np.random.randint(0, 255, (2, 2, 3))
print("原始图片形状:", image.shape) # (2, 2, 3)
print("原始图片:\n", image)
# [[[100 150 200]
# [110 160 210]]
# [[120 170 220]
# [130 180 230]]]
# 展平
vector = np.reshape(image, (-1,))
print("\n展平后形状:", vector.shape) # (12,)
print("展平后:", vector)
# [100 150 200 110 160 210 120 170 220 130 180 230]
# 验证:可以还原回去
image2 = np.reshape(vector, (2, 2, 3))
print("\n还原后是否相同:", np.array_equal(image, image2)) # True
32×32图片的展平过程(可视化)
原始图片 (32, 32, 3):
┌─────────────────────────┐
│ [R G B] [R G B] ... │ ← 第1行,32个像素
│ [R G B] [R G B] ... │ ← 第2行,32个像素
│ ... │
│ [R G B] [R G B] ... │ ← 第32行,32个像素
└─────────────────────────┘
↓ reshape ↓
展平向量 (3072,):
[R G B R G B ... R G B R G B ... ... R G B ... R G B]
↑第1行↑ ↑第2行↑ ↑第32行↑
结果:每张图片变成一个3072维的向量(一串3072个数字)。
数据形状变化表
| 步骤 | 训练集 | 测试集 | 说明 |
|---|---|---|---|
| 文件里 | (10000, 3072) | (10000, 3072) | 压扁的数字 |
| 加载后 | (50000, 32, 32, 3) | (10000, 32, 32, 3) | 图片格式 |
| 采样后 | (5000, 32, 32, 3) | (500, 32, 32, 3) | 减少数量 |
| 展平后 | (5000, 3072) | (500, 3072) | 向量格式 |
| 距离矩阵 | - | (500, 5000) | 测试×训练 |
为什么是3072维?
- 32 × 32 = 1024(像素数)
- 1024 × 3 = 3072(每个像素有RGB三个通道)
- 所以每张图片展平后是3072个数字