前言
本文章将讲解一种最简单的图像分类算法,即K-最近邻算法(K-NearestNeighbor,KNN)。
主要包括KNN算法的算法逻辑以及代码的实现。
一、算法逻辑
1、KNN算法的计算逻辑:
1)给定测试对象,计算它与训练集中每个对象的距离。
2)圈定距离最近的k个训练对象,作为测试对象的邻居。
3)根据这k个近邻对象所属的类别,找到占比最高的那个类别作为测试对象的预测类别。
具体操作如图所示:
2、在KNN算法中,我们发现有两个方面的因素会影响KNN算法的准确度:一个是计算测试对象与训练集中各个对象的距离,另一个因素就是k的选择(k的选择后面小节讲解)。
本小节先讲解距离度量:
(1)曼哈顿距离(Manhattan distance)
假设先只考虑两个点,第一个点的坐标为(x1,y1),第二个点的坐标为(x2,y2),那么,它们之间的曼哈顿距离就是|x1-x2|+|y1-y2|。
曼哈顿距离类似于从街道一端到街道另一端如:
下图中从A到B的三条路线都是曼哈顿距离,且三条距离相同。
(2)欧式距离(Euclidean Metric)
以空间为基准的两点之间的最短距离。还是假设只有两个点,第一个点的坐标为(x1,y1),第二个点的坐标为(x2,y2),那么它们之间的欧式距离就是√((x₁-x₂)²+(y₁-y₂)²)。
欧式距离是空间中的两点之间的直线距离
二、KNN实现MNIST数据分类
1、MNIST数据集
MNIST数据集来自美国国家标准与技术研究所(National Institute of Standards and Technolo,
NIST)。训练集由250个人手写的数字构成,其中50%是高中学生,50%是人口普查的工作人员。测试数据集也是同样比例的手写数字数据。MNIST数据集是一个很经典且很常用的数据集(类似于图像处理中的“Hello World!”)。
我们可以使用PyTorch框架进行MNIST数据集的下载与读取,代码如下:
import torch
import torchvision.datasets as dsets
import numpy as np
batch_size = 100
#Cifar10 dataset
train_dataset = dsets.CIFAR10(root = '/ml/pycifar', #选择数据的根目录
train = True, # 选择训练集
download = True) # 从网络上download图片
test_dataset = dsets.CIFAR10(root = '/ml/pycifar', #选择数据的根目录
train = False, # 选择测试集
download = True) # 从网络上download图片
#加载数据
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)
train_dataset与test_dataset可以返回训练集数据、训练集标签、测试集数据以及测试集标签,训练集数据以及测试集数据都是n×m维的矩阵,这里的n是样本数(行数),m是特征数(列数)。训练数据集包含60000个样本,测试数据集包含10000个样本。在MNIST数据集中,每张图片均由28×28个像素点构成,每个像素点使用一个灰度值表示。在这里,我们将28×28的像素展开为一个一维的行矢量,这些行矢量就是图片数组里的行(每行784个值,或者说每行就代表了一张图片)。训练集标签以及测试标签包含了相应的目标变量,也就是手写数字的类标签(整数0~9)。
2、完善fit方法,fit方法主要是通过训练数据集来训练模型,在Knn类中,我们的实现思路是将训练集的数据与其对应的标签存储于内存中。代码如下:
def fit(self,x_train,y_train):# x_train代表的是训练数据集,而y_train代表的是对应的训练集数据的标签
self.x_train = x_train
self.y_train = y_train
3、完善predict方法,predict方法可用于预测测试集的标签。具体的实现代码与之前的代码类似,只不过输入的参数只有k(代表的是k的选值),dis代表使用的是欧拉公式还是曼哈顿公式,X_test代表的是测试数据集;predict方法返回的是预测的标签集合。代码如下:
def predict(self,k,dis,x_test):
assert dis == 'E'or dis == 'M','dis must E or M'
num_test = x_test.shape[0] #测试样本的数量
labellist = []
'''
使用欧式距离作为距离度量
'''
if (dis == 'E'):
for i in range(num_test):
# 实现欧拉距离公式
distances = np.sqrt(np.sum(((X_train - np.tile(y_test[i], (X_train.shape[0], 1))) ** 2), axis=1))
nearest_k = np.argsort(distances) # 距离由小到大进行排序,并返回index值
topK = nearest_k[:k] # 选取前k个距离
classCount = {}
for i in topK: # 统计每个类别的个数
classCount[X_train[i]] = classCount.get(X_train[i], 0) + 1
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
labellist.append(sortedClassCount[0][0])
return np.array(labellist)
'''
使用曼哈顿距离作为距离度量
'''
if (dis == 'M'):
for i in range(num_test):
# 按照列的方向相加,其实就是行相加
distances = np.sum(np.abs(X_train - np.tile(y_test[i], (X_train.shape[0], 1))), axis=1)
nearest_k = np.argsort(distances)
topK = nearest_k[:k]
classCount = {}
for i in topK:
classCount[X_train[i]] = classCount.get(X_train[i], 0) + 1
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
labellist.append(sortedClassCount[0][0])
return np.array(labellist)
4、归一化:在进行数据加载的时候尝试使用归一化,能够使分类的准确度提升。代码如下:
def getXmean(X_train):
X_train = np.reshape(X_train, (X_train.shape[0], -1)) # 将图片从二维展开为一维
mean_image = np.mean(X_train, axis=0) # 求出训练集所有图片每个像素位置上的平均值
return mean_image
def centralized(X_test,mean_image):
X_test = np.reshape(X_test, (X_test.shape[0], -1)) # 将图片从二维展开为一维
X_test = X_test.astype(np.float64)
X_test -= mean_image # 减去均值图像,实现零均值化
return X_test
5、进行分类并输出准确度,代码如下:
未使用归一化代码:
if __name__ == '__main__':
X_train = train_loader.dataset.data.numpy() # 需要转为numpy矩阵
X_train = X_train.reshape(X_train.shape[0], 28 * 28) # 需要reshape之后才能放入knn分类器
y_train = train_loader.dataset.targets.numpy()
X_test = test_loader.dataset.data[:1000].numpy()
X_test = X_test.reshape(X_test.shape[0], 28 * 28)
y_test = test_loader.dataset.targets[:1000].numpy()
num_test = y_test.shape[0]
y_test_pred = kNN_classify(5, 'E', X_train, y_train, 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))
输出结果:
使用归一化后代码:
if __name__ == '__main__':
X_train = train_loader.dataset.data.numpy() # 需要转为numpy矩阵
mean_image = getXmean(X_train)
X_train = centralized(X_train,mean_image)
y_train = train_loader.dataset.targets.numpy()
X_test = test_loader.dataset.data[:1000].numpy()
X_test = centralized(X_test,mean_image)
y_test = test_loader.dataset.targets[:1000].numpy()
num_test = y_test.shape[0]
y_test_pred = kNN_classify(5, 'M', X_train, y_train, 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))
输出结果:
通过以上得到的准确度可知,归一化能够提升准确度。
三、KNN实现Cifar10数据分类
1、CIfar10数据集
获取Cifar10数据集代码如下:
import numpy as np
import torch
import KNN_Test
from torch.utils.data import DataLoader
import torchvision.datasets as dsets
batch_size = 100
#Cifar10 dataset
train_dataset = dsets.CIFAR10(root = '/ml/pycifar', #选择数据的根目录
train = True, # 选择训练集
download = True) # 从网络上download图片
test_dataset = dsets.CIFAR10(root = '/ml/pycifar', #选择数据的根目录
train = False, # 选择测试集
download = True) # 从网络上download图片
#加载数据
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)
2、查看Cifar10数据集中图片代码如下:
classes = ('plane', 'car', 'bird', 'cat', 'deer','dog', 'frog', 'horse', 'ship', 'truck')
digit = train_loader.dataset.data[0]
import matplotlib.pyplot as plt
plt.imshow(digit,cmap = plt.cm.binary)
plt.show()
print(classes[train_loader.dataset.targets[0]])
3、fit方法、predict方法以及归一化方法同上,这里不再给出代码,读者可以去上一节查看
4、进行分类以及准确度的输出代码如下:
X_train = train_loader.dataset.data
mean_image = getXmean(X_train)
X_train = centralized(X_train,mean_image)
y_train = train_loader.dataset.targets
X_test = test_loader.dataset.data[:100]
X_test = centralized(X_test,mean_image)
y_test = test_loader.dataset.targets[:100]
num_test = len(y_test)
y_test_pred = KNN_Test.kNN_classify(6, 'E', X_train, y_train, 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))
输出结果:
四、模型参数调优
对于KNN算法来说,k就是需要调整的超参数。对于k的调整并没有很好的方法,我们一般会尝试不同的值,看哪个值表现最好就选哪个。有一种更专业的穷举调参方法称为GridSearch,即在所有候选的参数中,通过循环遍历,尝试每一种的可能性,表现最好的参数就是最终的结果。
以下使用交叉验证的数据拆分方法来划分我们的数据集(读者不知道这个方法的话可以去看周志华老师的西瓜书)
1、使用之前所写的KNN分类器,代码如下:
import operator
import torch
import torchvision.datasets as dsets
import numpy as np
batch_size = 100
#Cifar10 dataset
train_dataset = dsets.CIFAR10(root = '/ml/pycifar', #选择数据的根目录
train = True, # 选择训练集
download = True) # 从网络上download图片
test_dataset = dsets.CIFAR10(root = '/ml/pycifar', #选择数据的根目录
train = False, # 选择测试集
download = True) # 从网络上download图片
#加载数据
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)
class Knn:
def __init__(self):
pass
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', 'dis must E or 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)
topK = nearest_k[:k]
classCount = {}
for i in topK:
classCount[self.ytr[i]] = classCount.get(self.ytr[i], 0) + 1
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
labellist.append(sortedClassCount[0][0])
return np.array(labellist)
# 使用曼哈顿公式作为距离度量
if (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)
topK = nearest_k[:k]
classCount = {}
for i in topK:
classCount[self.ytr[i]] = classCount.get(self.ytr[i], 0) + 1
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
labellist.append(sortedClassCount[0][0])
return np.array(labellist)
#归一化
def getXmean(X_train):
X_train = np.reshape(X_train, (X_train.shape[0], -1)) # 将图片从二维展开为一维
mean_image = np.mean(X_train, axis=0) # 求出训练集所有图片每个像素位置上的平均值
return mean_image
def centralized(X_test,mean_image):
X_test = np.reshape(X_test, (X_test.shape[0], -1)) # 将图片从二维展开为一维
X_test = X_test.astype(np.float64)
X_test -= mean_image # 减去均值图像,实现零均值化
return X_test
2、准备测试数据与验证数据,代码如下:
X_train = train_loader.dataset.data
X_train = X_train.reshape(X_train.shape[0],-1)
mean_image = getXmean(X_train)
X_train = centralized(X_train,mean_image)
y_train = train_loader.dataset.targets
y_train = np.array(y_train)
X_test = test_loader.dataset.data
X_test = X_test.reshape(X_test.shape[0],-1)
X_test = centralized(X_test,mean_image)
y_test = test_loader.dataset.targets
y_test = np.array(y_test)
3、将训练数据分成5个部分,每个部分轮流作为验证集,代码如下:
num_folds = 5
k_choices = [1, 3, 5, 8, 10, 12, 15, 20] # k的值一般选择1~20以内
num_training = X_train.shape[0]
X_train_folds = []
y_train_folds = []
indices = np.array_split(np.arange(num_training), indices_or_sections=num_folds) # 把下标分成5个部分
for i in indices:
X_train_folds.append(X_train[i])
y_train_folds.append(y_train[i])
k_to_accuracies = {}
for k in k_choices:
# 进行交叉验证
acc = []
for i in range(num_folds):
x = X_train_folds[0:i] + X_train_folds[i + 1:] # 训练集不包括验证集
x = np.concatenate(x, axis=0) # 使用concatenate将4个训练集拼在一起
y = y_train_folds[0:i] + y_train_folds[i + 1:]
y = np.concatenate(y) # 对label使用同样的操作
test_x = X_train_folds[i] # 单独拿出验证集
test_y = y_train_folds[i]
classifier = Knn() # 定义model
classifier.fit(x, y) # 将训练集读入
# dist = classifier.compute_distances_no_loops(test_x) # 计算距离矩阵
y_pred = classifier.predict(k, 'M', test_x) # 预测结果
# print(y_pred)
accuracy = np.mean(y_pred == test_y) # 计算准确率
acc.append(accuracy)
k_to_accuracies[k] = acc # 计算交叉验证的平均准确率
# 输出准确度
for k in sorted(k_to_accuracies):
for accuracy in k_to_accuracies[k]:
print('k = %d, accuracy = %f' % (k, accuracy))
4、图形化展示k的选取与准确度趋势,代码如下:
import matplotlib.pyplot as plt
for k in k_choices:
accuracies = k_to_accuracies[k]
plt.scatter([k] * len(accuracies), accuracies)
# plot the trend line with error bars that correspond to standard deviation
accuracies_mean = np.array([np.mean(v) for k,v in sorted(k_to_accuracies.items())])
accuracies_std = np.array([np.std(v) for k,v in sorted(k_to_accuracies.items())])
plt.errorbar(k_choices, accuracies_mean, yerr=accuracies_std)
plt.title('Cross-validation on k')
plt.xlabel('k')
plt.ylabel('Cross-validation accuracy')
plt.show()
这个我运行代码跑了很久也没跑出结果,大家可以自己试一下。
总结
对于KNN算法,我们需要理解算法的原理,掌握距离度量的规则,以及归一化的影响。