目录
一、理论基础
KNN算法的原理已在上一篇博客详细论述,在这里不再展开。在之前的部分,我们实现了基于二维坐标点的分类,这是图像分类中的一个简单示例。现在,我们将进一步扩展这个概念,应用于更复杂的图像数据集,以实现实际的图像分类任务。
1. 图像分类
图像可以根据多种标准和特性进行分类。不同的分类方法反映了图像的不同属性和用途。常见的图像类型有彩色图像、灰度图像和二值图像。
2. 实现思路
图像分类的任务就是预测给定图像属于各类标签的的可能性,根据这些输出,模型将选择概率最高的标签作为预测结果。
3. 图像预处理
在使用算法前,一般都需要先对图像进行预处理,包括:归一化、灰度变换、滤波变换等等。
二、数据集准备
MNIST数据集包含来自大约250个不同人手写的数字,其中一半是美国人口调查局的员工,另一半是美国高中生。这些数字经过归一化和中心对齐处理。
1. 数据集结构
每张图像都是固定的28x28像素大小的灰度图像。并且,每张图像都有一个与之对应的标签,标签是0到9的数字,表示图像中手写数字的实际值。
训练集:包含60,000个样本,这些样本用于训练模型。
测试集:包含10,000个样本,这些样本用于测试模型的性能。
2. 加载数据集
在PyTorch中,可以直接使用torchvision.datasets.MNIST
来加载MNIST手写数字数据集。在这里,设置batch_size = 100,即每次迭代中使用的样本数量为100。需要注意的是:在加载数据前,需要将图像转化为张量,确保数据与PyTorch库的兼容性,使得可以方便地在图像上应用各种算法和操作。
MNIST_dataset_loader.py
import torch
from torch.utils.data import DataLoader
import torchvision.datasets as dsets
import torchvision.transforms as transforms
#指定每次训练迭代的样本数量
batch_size = 100
transform = transforms.ToTensor() #将图片转化为PyTorch张量
train_dataset = dsets.MNIST(root= '指定路径/pymnist',
train= True,
transform=transforms.ToTensor(),
download=True)
test_dataset = dsets.MNIST(root= '指定路径/pymnist',
train= False,
transform=transforms.ToTensor(),
download=True)
#加载数据
train_loader = torch.utils.data.DataLoader(dataset= train_dataset,
batch_size=batch_size,
shuffle= True)
test_loader = torch.utils.data.DataLoader(dataset=test_dataset,
batch_size=batch_size,
shuffle= True)
通过打印训练和测试数据集的数据和标签的尺寸,输出数据集及其对应标签的维度信息,这对于数据结构的把握非常重要。
print("train data:",train_dataset.train_data.size())
print("train labels:",train_dataset.train_labels.size())
print("test data:",test_dataset.test_data.size())
print("test labels:",test_dataset.test_labels.size())
输出结果为:
train data: torch.Size([60000, 28, 28])
train labels: torch.Size([60000])
test data: torch.Size([10000, 28, 28])
test labels: torch.Size([10000])
3. 图像可视化
从第100个训练图像(因为索引从0开始),并且打印出该图像对应的标签:
MNIST_show.py
import matplotlib.pyplot as plt
import MNIST_dataset_loader
train = MNIST_dataset_loader.train_loader.dataset.train_data[99]
plt.imshow(train, cmap=plt.cm.binary)
plt.show()
print(MNIST_dataset_loader.train_loader.dataset.train_labels[99])
输出结果:
tensor(1)
![]() |
图1 MNIST数据集中第100个训练图像 |
三、数据预处理
1. 为什么要进行图像预处理?
如果我们对图像没有进行预处理,而是直接使用KNN分类器来验证MNIST数据集的准确性,得到的结果为:
KNNClassifier.py
import numpy as np
import operator
# KNN分类器构建
class KNNClassifier:
def __init__(self):
self.Xtr = None
self.ytr = None
def fit(self, X_train, y_train):
self.Xtr = X_train
self.ytr = y_train
def predict(self, k, dis, X_test):
assert dis == 'E' or dis == 'M' # E代表欧氏距离, M代表曼哈顿距离
num_test = X_test.shape[0]
labellist = []
if dis == 'E':
for i in range(num_test):
distances = np.sqrt(np.sum(((self.Xtr - np.tile(X_test[i], (self.Xtr.shape[0],1))) ** 2), axis=1))
nearest_k = np.argsort(distances)[:k]
classCount = {self.ytr[i]: 0 for i in nearest_k}
for i in nearest_k:
classCount[self.ytr[i]] += 1
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
labellist.append(sortedClassCount[0][0])
elif dis == 'M':
for i in range(num_test):
distances = np.sum(np.abs(self.Xtr - np.tile(X_test[i], (self.Xtr.shape[0], 1))), axis=1)
nearest_k = np.argsort(distances)[:k]
classCount = {self.ytr[i]: 0 for i in nearest_k}
for i in nearest_k:
classCount[self.ytr[i]] += 1
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
labellist.append(sortedClassCount[0][0])
return np.array(labellist)
test_KNN.py
import numpy as np
import MNIST_dataset_loader
from KNNClassifier import KNNClassifier
if __name__ == '__main__':
X_train = MNIST_dataset_loader.train_loader.dataset.train_data.numpy() # 转化为numpy
X_train = X_train.reshape(X_train.shape[0], 28 * 28)
y_train = MNIST_dataset_loader.train_loader.dataset.train_labels.numpy()
X_test = MNIST_dataset_loader.test_loader.dataset.test_data[:1000].numpy()
X_test = X_test.reshape(X_test.shape[0], 28 * 28)
y_test = MNIST_dataset_loader.test_loader.dataset.test_labels[:1000].numpy()
num_test = y_test.shape[0]
knn = KNNClassifier()
knn.fit(X_train, y_train)
y_test_pred = knn.predict(5, 'M', X_test)
num_correct = np.sum(y_test_pred == y_test)
accuracy = float(num_correct) / num_test
print('Got %d / %d correct => accuracy: %f' % (num_correct, num_test, accuracy))
运行结果如下:
Got 368 / 1000 correct => accuracy: 0.368000
在这个任务中,分类器在1000个样本的测试集上只能正确分类了368个样本,从而得到了36.8%的准确率。(当然如果选择不同的k值和距离度量会带来不同的结果,但总体的准确率都不会太高。)KNN算法在计算距离时对特征的尺度非常敏感。如果图像的尺寸或像素值范围(亮度或颜色深度)不统一,可能会导致距离计算偏向于尺度较大的特征。如:将所有图像归一化到同一尺寸和/或将像素值标准化到同一范围(如0到1),可以确保不同的特征对距离的贡献均衡,从而使KNN分类器更公平、更准确。
2. 图像归一化
归一化是数据预处理中的一个关键步骤,尤其是在处理图像数据时。它有助于将图像数据的数值范围调整到一个标准化的尺度。通常,归一化可以按照缩放方式分为两类:
2.1 均值方差归一化
2.1.1 定义
均值方差归一化(也称为标准化或Z-score标准化)是一种广泛使用的数据预处理技术,特别是在机器学习和统计建模中。这种方法涉及调整特征的尺度,使其具有零均值和单位方差,这有助于许多算法的性能,特别是那些依赖于数据距离度量的算法,如:KNN算法,以及涉及梯度下降的训练方法,如:神经网络。
2.1.2 实现思路及计算步骤
均值方差归一化将原始数据先减去均值,然后除以标准差,可以得到均值为0,标准差为1的分布(并非一定为正态)。
-
减去均值:首先从每个数据点减去整个数据集的均值。这一步是为了中心化数据,将数据的中心移至原点,这样做可以消除数据原本可能存在的偏移。
-
除以标准差:然后将上一步得到的结果除以数据集的标准差。这一步是为了规模化数据,使得数据集的标准差为1。这样,数据集中的每个特征都将在同一尺度上,避免某些特征因变化范围大而对结果产生过大影响。
计算步骤:对于一个 M*N 的二维图像进行均值方差归一化时,每个像素点都将根据整个图像的像素值均值和标准差进行调整。
-
计算均值(Mean):首先计算整个图像的平均像素值,假设图像的像素值合集为
,其中
,
。
-
-
其中,
指图像中所有像素值的平均值;
和
分别是图像的行数和列数。
-
计算方差(Variance):接着,计算图像像素值的标准差。
-
-
其中,
指方差(各数据与其平均数之差的平方和的平均数)。
-
计算标准差(Standard Deviation):然后,计算图像像素值相对于均值的标准差。
-
-
其中,
指标准差(方差的平方根)。
-
均值方差归一化公式:将每个像素值转换为具有零均值和单位方差的新值。
这里,指原始数据点的像素值;
是归一化后数据的像素值。
2.1.3 代码实现
在Python中,可以使用NumPy库来非常简单地实现这一过程:
import numpy as np
# 假设data是一个NumPy数组,包含了我们需要标准化的数据
data = np.array([1, 2, 3, 4, 5])
# 计算均值
mean = np.mean(data)
# 计算标准差
std = np.std(data)
# 进行标准化
normalized_data = (data - mean) / std
print("原始数据:", data)
print("标准化数据:", normalized_data)
2.2 最值归一化
2.2.1 定义
最值归一化(也称为Min-Max Scaling)是一种常用的数据预处理技术,用于将特征的尺度调整到一个指定的范围内,通常是0到1之间,或者是-1到1之间。这种方法特别适用于不假定数据遵循正态分布的情况下。
2.2.2 实现思路及计算步骤
最值归一化通过将特征的实际范围(最小值到最大值)缩放到一个标准范围内,通常是[0,1]或者[-1,1]。这使得不同特征之间的比较和权重更加公平和一致。
计算步骤:对于一个 M*N 的二维8图像,最值归一化将每个像素的值重新缩放,使所有像素值均位于0到1或者-1到1之间。
或者:
2.2.3 代码实现
在Python中,也可以使用NumPy库来非常简单地实现这一过程:
import numpy as np
# [0,1]最值归一化公式实现
def min_max_scaling(image):
min_val = np.min(image)
max_val = np.max(image)
scaled_image = (image - min_val) / (max_val - min_val)
return scaled_image
# 假设 image 是一个 M*N 的二维图像矩阵
image = np.array([[100, 150, 100], [120, 100, 130], [110, 120, 90]])
scaled_image = min_max_scaling(image)
print("Scaled Image:\n", scaled_image)
或者:
import numpy as np
#[-1,1]最值归一化公式实现
def min_max_mean_scaling(image):
min_val = np.min(image)
max_val = np.max(image)
mean_val = np.mean(image)
scaled_image = (image - mean_val) / (max_val - min_val)
return scaled_image
# 假设 image 是一个 M*N 的二维图像矩阵
image = np.array([[100, 150, 100], [120, 100, 130], [110, 120, 90]])
scaled_image = min_max_mean_scaling(image)
print("Scaled Image:\n", scaled_image)
四、代码改进
KNN算法是基于距离度量(如欧氏距离或曼哈顿距离)来确定每个测试点的“邻居”,因此确保所有特征具有相似的尺度是至关重要的。对于KNN来说,由于它对数据的尺度敏感,选择均值方差归一化通常是更好的选择。
1. KNN分类器
下面是在先前的代码基础上添加了均值方差归一化模块的实现过程:
KNNClassifier.py
import numpy as np
import operator
# KNN分类器构建
class KNNClassifier:
--snip--
def standardize_image(image): #均值方差归一化
mean = np.mean(image)
std = np.std(image)
return (image - mean) / std
2. 图像可视化
MNIST_show.py
import matplotlib.pyplot as plt
import numpy as np
import MNIST_dataset_loader
from KNNClassifier import standardize_image
# 图像(归一化)
train = MNIST_dataset_loader.train_loader.dataset.train_data[:1000].numpy()
digit_01 = train[33]
digit_02 = standardize_image(digit_01)
plt.imshow(digit_01, cmap=plt.cm.binary)
plt.show()
plt.imshow(digit_02, cmap=plt.cm.binary)
plt.show()
print(MNIST_dataset_loader.train_loader.dataset.train_labels[33])
print("Before standardization: mean = {}, std = {}".format(np.mean(digit_01), np.std(digit_01)))
print("After standardization: mean = {}, std = {}".format(np.mean(digit_02), np.std(digit_02)))
输出结果:
tensor(9)
Before standardization: mean = 27.007653061224488, std = 70.88341925375865
After standardization: mean = 5.890979314337566e-17, std = 1.0
![]() | ![]() |
图2 第34个训练图像(归一化前) | 图2 第34个训练图像(归一化后) |
虽然归一化前后的图像在视觉上似乎并无明显的差异,但通过打印归一化前后的像素值平均值和标准差可以发现标准化后,数据的平均值为5.890979314337566e-17,接近0(浮点数计算的微小误差,这在数值计算中可以视为0),这是标准化的预期结果,旨在将数据的均值中心化到0;标准差为1.0,确保数据的尺度一致。
3. 验证效果
test_KNN.py
import numpy as np
from KNNClassifier import KNNClassifier,standardize_image
import MNIST_dataset_loader
if __name__ == '__main__':
#训练数据
X_train = MNIST_dataset_loader.train_loader.dataset.train_data.numpy() #转化为numpy
X_train = X_train.reshape(X_train.shape[0], 28 * 28)
X_train = standardize_image(X_train) #均值方差归一化处理
y_train = MNIST_dataset_loader.train_loader.dataset.train_labels.numpy()
#测试数据
X_test = MNIST_dataset_loader.test_loader.dataset.test_data[:1000].numpy()
X_test = X_test.reshape(X_test.shape[0], 28 * 28)
X_test = standardize_image(X_test)
y_test = MNIST_dataset_loader.test_loader.dataset.test_labels[:1000].numpy()
num_test = y_test.shape[0]
knn = KNNClassifier()
knn.fit(X_train,y_train)
# y_test_pred = kNN_classify(5,'M',X_train,y_train,X_test)
y_test_pred = knn.predict(5,'M',X_test)
num_correct = np.sum(y_test_pred == y_test)
accuracy = float(num_correct) / num_test
print('Got %d / %d correct => accuracy: %f' % (num_correct,num_test,accuracy))
运行结果如下:
Got 950 / 1000 correct => accuracy: 0.950000
改进后的KNN算法在MNIST测试集上正确识别了1000个样本中的950个,模型的准确率为95%。这是一个很高的准确率(相比未归一化处理),通常表明分类器表现得相当好。
五、参考资料
[1]魏溪含, 涂铭, & 张修鹏. (2020). 深度学习与图像识别: 原理与实践. 北京: 机械工业出版社。