K最近邻(k-Nearest Neighbor,KNN)分类算法,是一个理论上比较成熟的方法,也是最简单的机器学习算法之一。该方法的思路是:如果一个样本在特征空间中的k个最相似(即特征空间中最邻近)的样本中的大多数属于某一个类别,则该样本也属于这个类别。
举例来说,我们要根据三角形更像什么(跟哪种图形离得更近),预测三角形的类别。我们找到三个离它最近的邻居:两个菱形和一个圆。菱形的数量多于圆,因此我们预测三角形的类别为菱形。
KNN的优点是
1. 简单,易于理解,易于实现,无需估计参数,无需训练;
2. 适合对稀有事件进行分类;
3. 特别适合于多分类问题(multi-modal,对象具有多个类别标签), kNN比SVM的表现要好
缺点也很明显
1. 当样本不平衡时,如一个类的样本容量很大,而其他类样本容量很小时,有可能导致当输入一个新样本时,该样本的K个邻居中大容量类的样本占多数。 该算法只计算“最近的”邻居样本,某一类的样本数量很大,那么或者这类样本并不接近目标样本,或者这类样本很靠近目标样本。无论怎样,数量并不能影响运行结果。
2. 该方法的另一个不足之处是计算量较大,因为对每一个待分类的文本都要计算它到全体已知样本的距离,才能求得它的K个最近邻点。
3. 可理解性差,无法给出像决策树那样的规则。
我们以手写识别为例,用KNN算法来预测数字的正确率。KNN算法通常需要求两个目标之间的距离,一般情况下都是欧氏距离,就是两个点坐标之间的直线距离。有些情况也会用到曼哈顿距离、余弦距离。这里我们将数字的二位数组转变为一维向量,距离就是向量之间的距离,也就是欧氏距离。
我们用到的数据集来自机器学习实战,有2000个例子,共10个数字,每个数字有大约200个样本。每个例子的名称的第一个数字就是它表示的数字,它是由32*32的矩阵表示,大概是这样:
代码和数据放在github上: https://github.com/ColinFred/KNN
首先,写一个函数,将每一个32*32的矩阵变成1*24的一维向量。
import numpy as np
from os import listdir
def img2vector(filename):
"""
将图片转化为向量形式。即将二维数组变为一维数组
"""
returnVect = np.zeros((1, 1024))
fr = open(filename)
for i in range(32):
lineStr = fr.readline()
for j in range(32):
returnVect[0, 32 * i + j] = int(lineStr[j])
return returnVectdef img2vector(filename):
然后,将所有的数据都做这样的处理,并且从名称中提取出它表示的数字作为标签
def handwritingClassTest():
"""
数据预处理
"""
hwLabels = []
trainingFileList = listdir('trainingDigits') # 加载训练集
m = len(trainingFileList)
trainingMat = np.zeros((m, 1024)) # 用来存放训练集各个向量
for i in range(m):
fileNameStr = trainingFileList[i] # 文件名称
fileStr = fileNameStr.split('.')[0] # 将 .txt 字符串剔除
classNumStr = int(fileStr.split('_')[0]) # 提取出这个文件所代表的数字
hwLabels.append(classNumStr) # 将数字放入标签集合中
trainingMat[i, :] = img2vector('trainingDigits/%s' % fileNameStr) # 将向量形式的数字文件存入trainingMat中
return trainingMat, hwLabels # 返回值是训练集的属性和标签
将所有的数据随机划分为两组,一组作为训练集,一组作为测试集
from sklearn.model_selection import train_test_split # 随机划分训练集和测试集
X, y = handwritingClassTest()
x_train, x_test, y_train, y_test = train_test_split(X, y, random_state=0)
print("training dataset: {0}".format(x_train.shape[0]))
print("testing dataset: {0}".format(x_test.shape[0]))
print("each sample has {0} features".format(x_train.shape[1]))
# training dataset: 1450
# testing dataset: 484
# each sample has 1024 features
函数:sklearn.model_selection.train_test_split
随机划分训练集和测试集
train_test_split是交叉验证中常用的函数,功能是从样本中随机的按比例选取train data和testdata,形式为:
X_train, X_test, y_train, y_test = cross_validation.train_test_split(train_data, train_target, test_size=0.4, random_state=0)
参数解释:
train_data:所要划分的样本特征集
train_target:所要划分的样本结果
test_size:样本占比,如果是整数的话就是样本的数量
random_state:是随机数的种子。
利用sklearn中的 KNeighborsClassifier 分类器对训练集进行训练,并用测试集测试其准确率。
from sklearn.neighbors import KNeighborsClassifier # 引入k近邻算法
estimator = KNeighborsClassifier() # 目前都使用默认参数
estimator.fit(x_train, y_train) # 开始训练
y_predicted = estimator.predict(x_test) # 开始预测
accuracy = np.mean(y_test == y_predicted) * 100 # 准确率
print("the accuracy is {0:.1f}%".format(accuracy))
# the accuracy is 96.7%
我们把数据集分为训练集和测试集,用训练集训练算法,在测试集上评估效果。倘若碰巧走运,测试集很简单,我们就会觉得算法表现很出色。反之,我们可能会怀疑算法很糟糕。也许由于我们一时不走运,就把一个其实很不错的算法给无情抛弃了,这岂不是很可惜。
交叉检验能解决上述一次性测试所带来的问题。既然只切一次有问题,那就多切几次,多进行几次实验。每次切分时,都要保证这次得到的训练集和测试集与上次不一样,还要确保每条数据都只能用来测试一次。算法描述如下。
1. 将整个大数据集分为几个部分。
2. 对于每一部分执行以下操作:
2.1 将其中一部分作为当前测试集
2.2 用剩余部分训练算法
2.3 在当前测试集上测试算法
3. 记录每次得分及平均得分。
4. 在上述过程中,每条数据只能在测试集中出现一次,以减少(但不能完全规避)运气成分
为了避免一次性测试的运气问题,我们引入交叉检验
from sklearn.model_selection import cross_val_score
scores = cross_val_score(estimator, X, y, scoring='accuracy')
average_accuacy = np.mean(scores) * 100
print("the average accuracy is {0:.1f}%".format(average_accuacy))
# the average accuracy is 94.7%
函数:sklearn.cross_validation.cross_val_score
调用形式是:sklearn.cross_validation.cross_val_score(estimator, X, y=None, scoring=None, cv=None,n_jobs=1, verbose=0, fit_params=None, pre_dispatch=’2*n_jobs’)
返回值就是对于每次不同的的划分raw data时,在test data上得到的分类的准确率。
参数解释:
estimator: 是不同的分类器,可以是任何的分类器。比如支持向量机分类器:estimator = svm.SVC(kernel=’linear’, C=1)
cv: 代表不同的cross validation的方法。如果cv是一个int值,并且如果提供了rawtarget参数,那么就代表使用StratifiedKFold分类方式;如果cv是一个int值,并且没有提供rawtarget参数,那么就代表使用KFold分类方式;也可以给定它一个CV迭代策略生成器,指定不同的CV方法。
scoring: 默认Nnoe,准确率的算法,可以通过score_func参数指定;如果不指定的话,是用estimator默认自带的准确率算法。
近邻算法有多个参数,最重要的是选取多少个近邻作为预测依据。 scikit-learn管这个参数叫n_neighbors。
我们看看n_neighbors对算法准确率的影响。我们对它从1-20逐个取值,用图表来显示结果。
avg_scores = []
all_scores = []
parameter_values = list(range(1, 21))
for n_neighbors in parameter_values:
estimator = KNeighborsClassifier(n_neighbors=n_neighbors)
scores = cross_val_score(estimator, X, y, scoring='accuracy')
avg_scores.append(np.mean(scores))
all_scores.append(scores)
# 使用图来分析一下n_neighbors的不同取值与分类正确率的关系
from matplotlib import pyplot as plt
plt.figure(figsize=(32, 20))
plt.plot(parameter_values, avg_scores, '-o', linewidth=5, markersize=24)
plt.show()
可以看到,当n_neighbors=3的时候,KNN的准确率最高。当n_neighbors=2的时候,会出现很明显的偏差。