2.2 KNN算法实践

2.2.1 KNN算法简单实现-电影分类

2.2.1.1准备数据集

我们可以使用numpy直接创建,代码如下:

import numpy as np

"""
函数说明:创建数据集
Parameters:无
Returns:
    group - 数据集
    labels - 分类标签
"""
def createDataSet():
    #四组二维特征
    group = np.array([[3,104],[2,100],[1,81],[101,10],[99,5],[88,2]])
    #四组特征的标签
    labels = ['爱情片','爱情片','爱情片','动作片','动作片','动作片']
    return group, labels
if __name__ == '__main__':
    #创建数据集
    group, labels = createDataSet()
    #打印数据集
    print(group)
    print(labels)

向量labels包含了每个数据点的标签信息,labels包含的元素个数等于group矩阵行数。

2.2.1.2 k-近邻算法

根据两点距离公式,计算距离,选择距离最小的前k个点,并返回分类结果。

import numpy as np
import operator

"""
函数说明:kNN算法,分类器
Parameters:
    inX - 用于分类的数据(测试集)
    dataSet - 用于训练的数据(训练集)
    labes - 分类标签
    k - kNN算法参数,选择距离最小的k个点
Returns:
    sortedClassCount[0][0] - 分类结果
"""
def classify(inX, dataSet, labels, k):
    #numpy函数shape[0]返回dataSet的行数
    dataSetSize = dataSet.shape[0]
    #在列向量方向上重复inX共1次(横向),行向量方向上重复inX共dataSetSize次(纵向)
    diffMat = np.tile(inX, (dataSetSize, 1)) - dataSet
    #二维特征相减后平方
    sqDiffMat = diffMat**2
    #sum()所有元素相加,sum(0)列相加,sum(1)行相加
    sqDistances = sqDiffMat.sum(axis=1)
    #开方,计算出距离
    distances = sqDistances**0.5
    #返回distances中元素从小到大排序后的索引值
    sortedDistIndices = distances.argsort()
    #定一个记录类别次数的字典
    classCount = {}
    for i in range(k):
        #取出前k个元素的类别
        voteIlabel = labels[sortedDistIndices[i]]
        #dict.get(key,default=None),字典的get()方法,返回指定键的值,如果值不在字典中返回默认值。
        #计算类别次数
        classCount[voteIlabel] = classCount.get(voteIlabel,0) + 1
    #python3中用items()替换python2中的iteritems()
    #key=operator.itemgetter(1)根据字典的值进行排序
    #key=operator.itemgetter(0)根据字典的键进行排序
    #reverse降序排序字典
    sortedClassCount = sorted(classCount.items(),key=operator.itemgetter(1),reverse=True)
    #返回次数最多的类别,即所要分类的类别
    return sortedClassCount[0][0]

【完整代码参考4.KNN_Movie Classify的KNN_Movie Classify _ v1和KNN_Movie Classify_ v2】

对于电影分类这样简单的分类来说,所有的代码自己编写那是没有问题,当数据量增加,算法在复杂度增加,不可能所有的代码都自己编写,可以调用现成的库,对于机器学习,sklearn就是不二之选,下面笔者就来简单介绍一下。

2.2.1.3 KNN实现之sklearn库简介

Scikit learn 也简称sklearn,是机器学习领域当中最知名的python模块之一。sklearn包含了很多机器学习的方式:
 Classification 分类;
 Regression 回归;
 Clustering 非监督分类;
 Dimensionality reduction 数据降维;
 Model Selection 模型选择;
 Preprocessing 数据与处理。

使用sklearn可以很方便地让我们实现一个机器学习算法。一个复杂度算法的实现,使用sklearn可能只需要调用几行API即可。所以学习sklearn,可以有效减少我们特定任务的实现周期。

在安装sklearn之前,需要安装两个库,即numpy+mkl和scipy。不要使用pip3直接进行安装,因为pip3默安装的是numpy,而不是numpy+mkl。第三方库下载地址:http://www.lfd.uci.edu/~gohlke/pythonlibs/。

使用pip3安装好这两个whl文件后,使用如下指令安装sklearn。

conda install scikit-learn
或
pip3 install scikit-learn

【注】如果使用anaconda集成环境,使用上述命令就可以了,安装方法还用,有兴趣的自行上网查找吧。
安装好了之后就可使用sklearn.neighbors模块实现了k-近邻算。

官网文档地址


源码地址链接

我们使用sklearn.neighbors.KNeighborsClassifier就可以是实现上小结,我们实现的k-近邻算法。KNeighborsClassifier函数一共有8个参数。

KNneighborsClassifier参数说明:
 n_neighbors:默认为5,就是k-NN的k的值,选取最近的k个点。
 weights:默认是uniform,参数可以是uniform、distance,也可以是用户自己定义的函数。uniform是均等的权重,就说所有的邻近点的权重都是相等的。distance是不均等的权重,距离近的点比距离远的点的影响大。用户自定义的函数,接收距离的数组,返回一组维数相同的权重。
 algorithm:快速k近邻搜索算法,默认参数为auto,可以理解为算法自己决定合适的搜索算法。除此之外,用户也可以自己指定搜索算法ball_tree、kd_tree、brute方法进行搜索,brute是蛮力搜索,也就是线性扫描,当训练集很大时,计算非常耗时。kd_tree,构造kd树存储数据以便对其进行快速检索的树形数据结构,kd树也就是数据结构中的二叉树。以中值切分构造的树,每个结点是一个超矩形,在维数小于20时效率高。ball tree是为了克服kd树高纬失效而发明的,其构造过程是以质心C和半径r分割样本空间,每个节点是一个超球体。
 leaf_size:默认是30,这个是构造的kd树和ball树的大小。这个值的设置会影响树构建的速度和搜索速度,同样也影响着存储树所需的内存大小。需要根据问题的性质选择最优的大小。
 metric:用于距离度量,默认度量是minkowski,也就是p=2的欧氏距离(欧几里德度量)。
 p:距离度量公式。在上小结,我们使用欧氏距离公式进行距离度量。除此之外,还有其他的度量方法,例如曼哈顿距离。这个参数默认为2,也就是默认使用欧式距离公式进行距离度量。也可以设置为1,使用曼哈顿距离公式进行距离度量。
 metric_params:距离公式的其他关键参数,这个可以不管,使用默认的None即可。
 n_jobs:并行处理设置。默认为1,临近点搜索并行工作数。如果为-1,那么CPU的所有cores都用于并行工作。

KNeighborsClassifier提供了以一些方法供我们使用,如下所示。
《人工智能》机器学习 - 第2章 KNN算法【分类】(二 算法实战)_数据
有兴趣的朋友请自行查看官方手册吧。

中文手册
英文手册

好了,说了这么多,实际来操作吧。

2.2.1.4优化算法

 库使用
对于KNN简单的实例我们可以自己实现其算法,但是到以后算法越来越复杂,我们不可能每个都自己去实现,我们可以调用已经写好的库,下文就将通过机器学习库sklearn来实现上文的电影分类。

import numpy as np
from sklearn import neighbors 

"""
函数说明:创建数据集
Parameters:
    无
Returns:
    group - 数据集
    labels - 分类标签
"""
def createDataSet():
    #四组二维特征
    group = np.array([[3,104],[2,100],[1,81],[101,10],[99,5],[88,2]])
    #四组特征的标签
    labels = ['爱情片','爱情片','爱情片','动作片','动作片','动作片']
    return group, labels

#第一步:取得knn分类器  
knn = neighbors.KNeighborsClassifier() 

#第二步:创建数据
data , lables = createDataSet()

#第三步:训练数据
knn.fit(data,lables) #导入数据进行训练 

#第四步:预测数据
print(knn.predict([[18,90]]))

【完整代码参考4.KNN_ Movie Classify的KNN_Movie Classify_v3】
是不是很简单,导入包,再调用API就好了,我们现在是学习其原理,还是要自己实现的,到一定阶段之后,我们就会调用已经有的API,节省开发时间和效率。

2.2.1.4算法小结

在电影例子中的特征是2维的,这样的距离度量可以用两点距离公式计算,但是如果是更高维的,我们可以用欧氏距离(也称欧几里德度量),前文已经给出了。

细心的读者可以发现,k-邻近算法好像没有学习过程啊,就是把未知数据和已知数据比较,这样分类,分类器并不会得到百分百正确的结果,我们可以使用多种方法检测分类器的正确率。此外分类器的性能也会受到多种因素的影响,如分类器设置和数据集等。不同的算法在不同数据集上的表现可能完全不同。为了测试分类器的效果,我们可以使用已知答案的数据,当然答案不能告诉分类器,检验分类器给出的结果是否符合预期结果。通过大量的测试数据,我们可以得到分类器的错误率-分类器给出错误结果的次数除以测试执行的总数。错误率是常用的评估方法,主要用于评估分类器在某个数据集上的执行效果。完美分类器的错误率为0,最差分类器的错误率是1.0。同时,我们也不难发现,k-近邻算法没有进行数据的训练,直接使用未知的数据与已知的数据进行比较,得到结果。因此,可以说k-邻近算法不具有显式的学习过程。

根据以上简单的实例,可以总结k-近邻算法的一般流程:
[1]收集数据:可以使用Python进行数据的收集,也可以使用第三方提供的免费或收费的数据。一般来讲,数据放在txt文本文件中,按照一定的格式进行存储,便于解析及处理。
[2]准备数据:使用Python解析、预处理数据。最好是结构化的数据格式。
[3]分析数据:可以使用很多方法对数据进行分析,例如使用Matplotlib将数据可视化。
[4]测试算法:计算错误率。
[5]使用算法:首先需要输入样本数据和结构化的输出结果,然后运行k近邻算法判定输入数据分别属于哪个分类,最后应用对计算出的分类执行后续的处理。

2.2.2 KNN实战之鸢尾花卉分类

数据集地址:http://archive.ics.uci.edu/ml/datasets/Iris
http://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data

Iris数据集的中文名是安德森鸢尾花卉数据集,英文全称是Anderson’s Iris data set。iris包含150个样本,对应数据集的每行数据。每行数据包含每个样本的四个特征和样本的类别信息,所以iris数据集是一个150行5列的二维表。

通俗地说,iris数据集是用来给花做分类的数据集,每个样本包含了花萼长度、花萼宽度、花瓣长度、花瓣宽度四个特征(前4列),我们需要建立一个分类器,分类器可以通过样本的四个特征来判断样本属于山鸢尾、变色鸢尾还是维吉尼亚鸢尾(这三个名词都是花的品种)。
特征:萼片长度,萼片宽度,花瓣长度,花瓣宽度
(sepal length, sepal width, petal length and petal width)
类别:Iris setosa, Iris versicolor, Iris virginica.

《人工智能》机器学习 - 第2章 KNN算法【分类】(二 算法实战)_人工智能_02

图1

2.2.2.1 KNN实战之鸢尾花卉分类实现

直接上代码。

# -*- coding: utf-8 -*-
import csv#用于处理csv文件
import random#用于随机数
import math
import operator

"""
函数说明:加载数据

Parameters:
  filename - 文件名
  split - 分隔符
  trainingSet - 训练集
  testSet - 测试集
Returns:
	无
"""
def loadDataset(filename, split, trainSet = [], testSet = []):
    with open(filename, 'rt') as csvfile:
        
        #从csv中读取书剑并返回行数
        lines = csv.reader(csvfile)
        
        dataset = list(lines)
        for x in range(len(dataset)-1):
            for y in range(4):
                dataset[x][y] = float(dataset[x][y])
            #保存数据集到训练集和测试集#random.random()返回随机浮点数
            if random.random() < split:
                trainSet.append(dataset[x])
            else:
                #将获得的测试数据放入测试集中
                testSet.append(dataset[x])
				
"""
函数说明:计算距离

Parameters:
    instance1
    instance2
    length - 长度
Returns:
    距离
"""
def euclideanDistance(instance1, instance2, length):
    distance = 0
    for x in range(length):
        #计算距离的平方和
        distance += pow((instance1[x]-instance2[x]), 2)
    return math.sqrt(distance)

"""
函数说明:回K个最近邻

Parameters:
   trainingSet - 训练街
   testInstance 
   k
Returns:
	neighbors 返回k近邻
"""
#
def getNeighbors(trainingSet, testInstance, k):
    distances = []
    length = len(testInstance)-1
    for x in range(len(trainingSet)):
        #testinstance
        dist = euclideanDistance(testInstance, trainingSet[x], length)
        distances.append((trainingSet[x], dist))
        #distances.append(dist)
    ##将邻近距离排序
    distances.sort(key=operator.itemgetter(1))
    neighbors = []
    for x in range(k):
        neighbors.append(distances[x][0])
        return neighbors

"""
函数说明:对k个近邻进行合并

Parameters:
	neighbors - k 近邻
Returns:
	value最大的key
"""
def getResponse(neighbors):
    classVotes = {}
    
    for x in range(len(neighbors)):
        response = neighbors[x][-1]
        if response in classVotes:
            classVotes[response] += 1
        else:
            classVotes[response] = 1
    
    sortedVotes = sorted(classVotes.items(), key=operator.itemgetter(1), reverse=True)
    return sortedVotes[0][0]

"""
函数说明:计算准确率

Parameters:
  testSet - 测试集
  predictions - 预测值 
Returns:
	返回准确率
"""
#计算准确率
def getAccuracy(testSet, predictions):
    correct = 0
    for x in range(len(testSet)):
        if testSet[x][-1] == predictions[x]:
            correct += 1
    return (correct/float(len(testSet)))*100.0

"""
函数说明:主函数

Parameters:
	无
Returns:
	无
"""
def main():
    #prepare data
    trainSet = []#训练数据集
    testSet = []#测试数据集
    split = 0.67#分割的比例
    
    ## step 1: load data
    #加载数据集
    print("step 1: load data...")
    loadDataset('C:/TensorFlow/irisdata.txt', split, trainSet, testSet)
    
    print('Train set: ' + repr(len(trainSet)))
    print('Test set: ' + repr(len(testSet)))
    
    #print(train_X)
    #print(train_Y)
 
    ## step 2: training...
    print("step 2: training...")
    pass

    #generate predictions
    predictions = []
    k = 3
    ## step 3: testing
    print("step 3: testing...")
    for x in range(len(testSet)):
        
        neighbors = getNeighbors(trainSet, testSet[x], k)
        result = getResponse(neighbors)
        
        predictions.append(result)
        #print ('> predicted=' + repr(result) + ', actual=' + repr(testSet[x][-1]) + "\n")
    
    #print('predictions: ' + repr(predictions))
    
    ## step 4: show the result
    print("step 4: show the result...")    
    
    #准确率
    accuracy = getAccuracy(testSet, predictions)
    print('\nAccuracy: ' + repr(accuracy) + '%')

if __name__ == '__main__':
    main()

【完整代码参考5.KNN_Iris_ Classify下的KNN_Iris_Classify_ v1的KNN_Iris_v1.0】
笔者对改代码进行了再次优化,完整代码如下所示。

"""
Please note, this code is only for python 3+. If you are using python 2+, please modify the code accordingly.
"""
"""
# @Date     : 2018-09-08
# @Author   : BruceOu
# @Language : Python3.6
"""
# -*- coding: utf-8 -*-
import csv#用于处理csv文件
import random#用于随机数
import operator
import numpy as np

"""
函数说明:加载数据

Parameters:
  filename - 文件名
  split - 分隔符
  trainingSet - 训练集
  testSet - 测试集
Returns:
	无
"""
def loadDataset(filename, split, trainSet = [], testSet = []):
    with open(filename, 'rt') as csvfile:
        
        #从csv中读取数据并返回行数
        lines = csv.reader(csvfile)
        
        dataset = list(lines)
        for x in range(len(dataset)-1):
            for y in range(4):
                dataset[x][y] = float(dataset[x][y])
            #保存数据集到训练集和测试集#random.random()返回随机浮点数
            if random.random() < split:
                trainSet.append(dataset[x])
            else:
                #将获得的测试数据放入测试集中
                testSet.append(dataset[x])

"""
函数说明:kNN算法,分类器

Parameters:
	inX - 用于分类的数据(测试集)
	dataSet - 用于训练的数据(训练集)
	labes - 分类标签
	k - kNN算法参数,选择距离最小的k个点
Returns:
	sortedClassCount[0][0] - 分类结果
"""
def classify(inX, dataSet, labels, k):
	#numpy函数shape[0]返回dataSet的行数
	dataSetSize = dataSet.shape[0]
	
	#在列向量方向上重复inX共1次(横向),行向量方向上重复inX共dataSetSize次(纵向)
	diffMat = np.tile(inX, (dataSetSize, 1)) - dataSet
	
	#二维特征相减后平方
	sqDiffMat = diffMat**2
	
	#sum()所有元素相加,sum(0)列相加,sum(1)行相加
	sqDistances = sqDiffMat.sum(axis=1)
	
	#开方,计算出距离
	distances = sqDistances**0.5
	
	#返回distances中元素从小到大排序后的索引值
	sortedDistIndices = distances.argsort()
	
	#定一个记录类别次数的字典
	classCount = {}
	for i in range(k):
		#取出前k个元素的类别
		voteIlabel = labels[sortedDistIndices[i]]
		#dict.get(key,default=None),字典的get()方法,返回指定键的值,如果值不在字典中返回默认值。
		#计算类别次数
		classCount[voteIlabel] = classCount.get(voteIlabel,0) + 1
	#python3中用items()替换python2中的iteritems()
	#key=operator.itemgetter(1)根据字典的值进行排序
	#key=operator.itemgetter(0)根据字典的键进行排序
	#reverse降序排序字典
	sortedClassCount = sorted(classCount.items(),key=operator.itemgetter(1),reverse=True)
	
	#返回次数最多的类别,即所要分类的类别
	return sortedClassCount[0][0]

"""
函数说明:计算准确率

Parameters:
  testSet - 测试集
  predictions - 预测值 
Returns:
	返回准确率
"""
#计算准确率
def getAccuracy(testSet, predictions):
    correct = 0
    for x in range(len(testSet)):
        if testSet[x][-1] == predictions[x]:
            correct += 1
    return (correct/float(len(testSet)))*100.0

"""
函数说明:分割数据

Parameters:
    dataSet - 数据集
Returns:
    data_X - 特征数据集
    data_Y - 标签数据集
"""
def segmentation_Data(dataSet):
    
    #得到文件行数
    Lines = len(dataSet)
    
    #返回的NumPy矩阵,解析完成的数据:4列
    data_X = np.zeros((Lines,4))
    data_Y = []
    for x in range(Lines):
        data_X[x,:] = dataSet[x][0:4]
        data_Y.append(dataSet[x][-1])
    
    return data_X, data_Y


"""
函数说明:主函数

Parameters:
	无
Returns:
	无
"""
def main():
    #prepare data
    trainSet = []#训练数据集
    testSet = []#测试数据集
    split = 0.67#分割的比例
    
    ## step 1: load data
    #加载数据集
    print("step 1: load data...")
    loadDataset('C:/TensorFlow/irisdata.txt', split, trainSet, testSet)
    
    #数据集分割
    train_X,train_Y = segmentation_Data(trainSet)
    test_X,test_Y = segmentation_Data(testSet)
    
    print('Train set: ' + repr(len(trainSet)))
    print('Test set: ' + repr(len(testSet)))
    
    #print(train_X)
    #print(train_Y)
 
    ## step 2: training...
    print("step 2: training...")
    pass

    #generate predictions
    predictions = []
    k = 3
	
    ## step 3: testing
    print("step 3: testing...")
    for x in range(len(testSet)):
        
        result = classify(test_X[x], train_X, train_Y, k)
        
        predictions.append(result)
        #print ('> predicted=' + repr(result) + ', actual=' + repr(testSet[x][-1]) + "\n")
    
    #print('predictions: ' + repr(predictions))
    
    ## step 4: show the result
    print("step 4: show the result...")    
    
    #准确率
    accuracy = getAccuracy(testSet, predictions)
    print('\nAccuracy: ' + repr(accuracy) + '%')

if __name__ == '__main__':
    main()

【完整代码参考5.KNN_Iris_ Classify下的KNN_Iris_Classify_ v1的KNN_Iris_v1.1】
结果如下所示。

《人工智能》机器学习 - 第2章 KNN算法【分类】(二 算法实战)_机器学习_03

2.2.2.2 KNN实战之鸢尾花卉分类-调用sklearn库

前文是根据KNN算法,我们自己一步一步实现鸢尾花卉分类,在这里,我们还是可以调用sklearn来进行鸢尾花卉分类,直接上代码吧。

from sklearn import neighbors
from sklearn import datasets# 引入datasets
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score

#第一步:读取数据集
iris = datasets.load_iris()# 获取所需数据集

print(iris)

#第二步:分离数据
# X = features
X = iris.data
# Y = label
Y = iris.target

X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=.6)

#第三步KNN分类
#初始化分类器
knn = neighbors.KNeighborsClassifier()

#训练
knn.fit(X_train, Y_train)

#第四步:预测数据
predictedLabel = knn.predict(X_test)
# 获得预测准确率
print(accuracy_score(Y_test, predictedLabel))
print("predictedLabel is :")
print(predictedLabel)

【完整代码参考5.KNN_Iris _ Classify下的KNN_Iris _ Classify_v2】
笔者在代码中已经进行了详细的注释,在这里我就不在细说代码了。

2.2.3 KNN实战之手写数字识别

本节我们一步步地构造使用 k 近邻分类器的手写识别系统。为了简单起见,这里构造的系统只能识别数字 0 到 9,每个数字大约有200个样本。需要识别的数字已经使用图形处理软件,处理成具有相同的色彩和大小 1:宽高是 32 像素 x 32 像素的黑白图像。尽管采用文本格式存储图像不能有效地利用内存空间,但是为了方便理解,我们还是将图像转换为文本格式。

数据集下载地址

《人工智能》机器学习 - 第2章 KNN算法【分类】(二 算法实战)_人工智能_04

图2
与此同时,这些文本格式存储的数字的文件命名也很有特点,格式为:数字的值_该数字的样本序号。

《人工智能》机器学习 - 第2章 KNN算法【分类】(二 算法实战)_人工智能_05

图3
【注】该数据集合修改自 "手写数字数据集的光学识别" 一文中的数据集合,该文登载于 2010 年 10 月 3 日的 UCI 机器学习资料库中 http://archive.ics.uci.edu/ml。作者是土耳其伊斯坦布尔海峡大学计算机工程系的 E. Alpaydin 与 C. Kaynak。

数据库解压后digits 目录下有两个文件夹,分别是:
 trainingDigits:训练数据,1934 个文件,每个数字大约 200 个文件。
 testDigits:测试数据,946 个文件,每个数字大约 100 个文件。

每个文件中存储一个手写的数字,文件的命名类似 0_7.txt,第一个数字 0 表示文件中的手写数字是 0,后面的 7 是个序号。

和鸢尾花卉分类识别一样,还是先用我们自己的KNN,最后再调用sklearn实现手写数字的识别。好了我们开始吧。

2.2.3.1准备数据:将图像转换为测试向量

为了使用前面两个例子的分类器,我们必须将图像格式化处理为一个向量。我们将把一个32x32的二进制图像矩阵转换为1x1024的向量,这样前两节使用的分类器就可以处理数字图像信息了。

我们首先编写一段函数img2vector,将图像转换为向量:该函数创建1x1024的NumPy数组,然后打开给定的文件,循环读出文件的前32行,并将每行的头32个字符值存储在NumPy数组中,最后返回数组。

import numpy as np
def img2vector(filename):
    rows = 32
    cols = 32
    
    #创建1x1024零向量
    imgVector = np.zeros((1, rows * cols))
	
    #打开文件,读取每行内容
    fr = open(filename)
	
    #按行读取
    for r in range(rows):
        #读一行数据
        lineStr = fr.readline()
		
        #每一行的前32个元素依次添加到returnVect中
        for c in range(cols):
            imgVector[0, rows * r + c] = int(lineStr[c])
	
    #返回转换后的1x1024向量
return imgVector

然后,可以代码后面输入测试img2vector函数:

testVector = img2vector('digits/testDigits/0_1.txt')
testVector[0,0:31]

运行结果如下。

array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 1.,
       0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])

《人工智能》机器学习 - 第2章 KNN算法【分类】(二 算法实战)_KNN_06

图4
上图很清楚的看到txt文档中的数据和矩阵之间的转换。

2.2.3.2加载数据

前面是将图片数据转化为矩阵,接下来就是将图片加载进来。看代码吧。

def loadDataSet():
    
    ## step 1: Getting training set
    print("---Getting training set...")
    
    dataSetDir = 'C:/TensorFlow/'
    #返回trainingDigits目录下的文件名
    trainingFileList = os.listdir(dataSetDir + 'trainingDigits') # load the training set
    
    #返回文件夹下文件的个数
    numSamples = len(trainingFileList)
 
    # 初始化样本数据矩阵(numSamples*1024)
    train_x = np.zeros((numSamples, 1024))
    train_y = []
    
    #从文件名中解析出训练集的类别
    for i in range(numSamples):
        #获得文件的名字
        filename = trainingFileList[i]
        
        ##将每一个文件的1x1024数据存储到train_x矩阵中
        train_x[i, :] = img2vector(dataSetDir + 'trainingDigits/%s' % filename) 
        
        #获得分类的数字,也就是分类标签
        label = int(filename.split('_')[0]) # return 1
        #将获得的类别添加到train_y中
        train_y.append(label)
 
    ## step 2: Getting testing set
    print("---Getting testing set...")
    #返回testDigits目录下的文件名
    testingFileList = os.listdir(dataSetDir + 'testDigits') # load the testing set
    
    #返回文件夹下文件的个数
    numSamples = len(testingFileList)
    
    # 初始化测试样本数据矩阵(numSamples*1024)
    test_x = np.zeros((numSamples, 1024))
    test_y = []
    
    for i in range(numSamples):
        #获得文件的名字
        filename = testingFileList[i]
 
        #将每一个文件的1x1024数据存储到test_x矩阵中
        test_x[i, :] = img2vector(dataSetDir + 'testDigits/%s' % filename) 
 
        #获得分类的数字,也就是分类标签
        label = int(filename.split('_')[0]) # return 1
        #将获得的类别添加到test_y中
        test_y.append(label)
 
    return train_x, train_y, test_x, test_y

这个函数就是将文件中的数据读取并加载进来。

2.2.3.3分析数据

k-近邻(k-NN)算法我们在理论学习部分已经有所了解,本节内容将实现这个算法的核心部分:计算“距离”。

当我们有一定的样本数据和这些数据所属的分类后,输入一个测试数据,我们就可以根据算法得出该测试数据属于哪个类别,此处的类别为0-9十个数字,就是十个类别。

算法实现过程

  1. 计算已知类别数据集中的点与当前点之间的距离;
  2. 按照距离递增次序排序;
  3. 选取与当前点距离最小的k个点;
  4. 确定前k个点所在类别的出现频率;
  5. 返回前k个点出现频率最高的类别作为当前点的预测分类。

算法实现为函数 classify(),函数的参数包括:
1. inX:用于分类的输入向量
2. dataSet:输入的训练样本集
3. labels:样本数据的类标签向量
4. k:用于选择最近邻居的数目

我们继续添加代码:

import operator
def classify(inX, dataSet, labels, k):
	#numpy函数shape[0]返回dataSet的行数
	dataSetSize = dataSet.shape[0]
	
	#在列向量方向上重复inX共1次(横向),行向量方向上重复inX共dataSetSize次(纵向)
	diffMat = np.tile(inX, (dataSetSize, 1)) - dataSet
	
	#二维特征相减后平方
	sqDiffMat = diffMat**2
	
	#sum()所有元素相加,sum(0)列相加,sum(1)行相加
	sqDistances = sqDiffMat.sum(axis=1)
	
	#开方,计算出距离
	distances = sqDistances**0.5
	
	#返回distances中元素从小到大排序后的索引值
	sortedDistIndices = distances.argsort()
	
	#定一个记录类别次数的字典
	classCount = {}
	for i in range(k):
		#取出前k个元素的类别
		voteIlabel = labels[sortedDistIndices[i]]
		#dict.get(key,default=None),字典的get()方法,返回指定键的值,如果值不在字典中返回默认值。
		#计算类别次数
		classCount[voteIlabel] = classCount.get(voteIlabel,0) + 1
	#python3中用items()替换python2中的iteritems()
	#key=operator.itemgetter(1)根据字典的值进行排序
	#key=operator.itemgetter(0)根据字典的键进行排序
	#reverse降序排序字典
	sortedClassCount = sorted(classCount.items(),key=operator.itemgetter(1),reverse=True)
	
	#返回次数最多的类别,即所要分类的类别
	return sortedClassCount[0][0]

我们使用欧氏距离公式,计算两个向量点 和 之间的距离:
《人工智能》机器学习 - 第2章 KNN算法【分类】(二 算法实战)_数据_07
例如,点(0, 0)与(1, 2)之间的距离计算为:
《人工智能》机器学习 - 第2章 KNN算法【分类】(二 算法实战)_sklearn_08
如果数据集存在4个特征值,则点 (1, 0, 0, 1) 与 (7, 6, 9, 4) 之间的距离计算为:
《人工智能》机器学习 - 第2章 KNN算法【分类】(二 算法实战)_KNN_09

计算完所有点之间的距离后,可以对数据按照从小到大的次序排序。然后,确定前 k 个距离最小元素所在的主要分类,输入 k 总是正整数;最后,将 classCount 字典分解为元组列表,然后使用程序第二行导入运算符模块的 itemgetter 方法,按照第二个元素的次序对元组进行排序。此处的排序为逆序,即按照从最大到最小次序排序,最后返回发生频率最高的元素标签。

到现在为止,我们已经构造了第一个分类器,使用这个分类器可以完成很多分类任务。从这个实例出发,构造使用分类算法将会更加容易。

2.2.3.4测试算法:使用 k 近邻算法识别手写数字

我们已经将数据处理成分类器可以识别的格式。接下来,我们将这些数据输入到分类器,检测分类器的执行效果。在写入这些代码之前,我们必须确保将from os import listdir写入文件的起始部分,这段代码的主要功能是从os模块中导入函数 listdir,它可以列出给定目录的文件名。

 测试的步骤:
1.读取训练数据到向量(手写图片数据),从数据文件名中提取类别标签列表(每个向量对应的真实的数字)
2.读取测试数据到向量,从数据文件名中提取类别标签
3.执行KNN算法对测试数据进行测试,得到分类结果
4.与实际的类别标签进行对比,记录分类错误率
5.打印每个数据文件的分类数据及错误率作为最终的结果。

def testHandWritingClass():
    ## step 1: load data
    print("step 1: load data...")
    train_x, train_y, test_x, test_y = loadDataSet()
 
    ## step 2: training...
    print("step 2: training...")
    pass
 
    ## step 3: testing
    print("step 3: testing...")
    numTestSamples = test_x.shape[0]
    matchCount = 0
    
    for i in range(numTestSamples):
        predict = classify(test_x[i], train_x, train_y, 3)
        print("Really Lable: %d \t KNN Lable :%d" % (test_y[i],predict))
        if predict == test_y[i]:
            matchCount += 1
    accuracy = float(matchCount) / numTestSamples
 
    ## step 4: show the result
    print("step 4: show the result...")    
    print("总共错了%d个数据\n" % (numTestSamples-matchCount))
    print('准确率是: %.2f%%' % (accuracy * 100))

上面的代码中,将trainingDigits目录中的文件内容存储在列表中,然后可以得到目录中有多少文件,并将其存储在变量m中。接着,代码创建一个m行1024列的训练矩阵,该矩阵的每行数据存储一个图像。我们可以从文件名中解析出分类数字。该目录下的文件按照规则命名,如文件9_45.txt的分类是9,它是数字9的第45个实例。然后我们可以将类代码存储在hwLabels向量中,使用前面讨论的img2vector函数载入图像。在下一步中,我们对testDigits目录中的文件执行相似的操作,不同之处是我们并不将这个目录下的文件载入矩阵中,而是使用classify()函数测试该目录下的每个文件。

最后,我们输入handwritingClassTest(),测试该函数的输出结果。
《人工智能》机器学习 - 第2章 KNN算法【分类】(二 算法实战)_数据_10
【完整代码参看6.HandWritingClassify下的HandWritingClassify_v1】
【注】笔者在这里只显示了部分数据。

k-近邻算法识别手写数字数据集,正确率98.94%。改变变量k的值、修改函数testHandwritingClass随机选取训练样本、改变训练样本的数目,都会对k近邻算法的错误率产生影响,感兴趣的话可以改变这些变量值,观察错误率的变化。

好了,接下来就是Sklearn来实现。

2.2.3.5调用Sklearn的API实现 k 近邻算法识别手写数字

对于使用Sklearn的API实现 k 近邻算法识别手写数字的方法和自己实现KNN的算法不同的地方主要就是neighbors的KNeighborsClassifier算法,其余的基本一样,请读者朋友自行比较吧。

# -*- coding: UTF-8 -*-
import numpy as np
from sklearn import neighbors
import os
from sklearn.metrics import accuracy_score

"""
函数说明:将32x32的二进制图像转换为1x1024向量。

Parameters:filename - 文件名
Returns:returnVect - 返回的二进制图像的1x1024向量
"""
def img2vector(filename):
    rows = 32
    cols = 32
    
    #创建1x1024零向量
    imgVector = np.zeros((1, rows * cols))
	
    #打开文件,读取每行内容
    fr = open(filename)
	
    #按行读取
    for r in range(rows):
        #读一行数据
        lineStr = fr.readline()
		
        #每一行的前32个元素依次添加到returnVect中
        for c in range(cols):
            imgVector[0, rows * r + c] = int(lineStr[c])
	
    #返回转换后的1x1024向量
    return imgVector

# load dataSet
def loadDataSet():
    
    ## step 1: Getting training set
    print("---Getting training set...")
    
    dataSetDir = 'C:/TensorFlow/'
    #返回trainingDigits目录下的文件名
    trainingFileList = os.listdir(dataSetDir + 'trainingDigits') # load the training set
    
    #返回文件夹下文件的个数
    numSamples = len(trainingFileList)
 
    # 初始化样本数据矩阵(numSamples*1024)
    train_x = np.zeros((numSamples, 1024))
    train_y = []
    
    #从文件名中解析出训练集的类别
    for i in range(numSamples):
        #获得文件的名字
        filename = trainingFileList[i]
        
        ##将每一个文件的1x1024数据存储到train_x矩阵中
        train_x[i, :] = img2vector(dataSetDir + 'trainingDigits/%s' % filename) 
        
        #获得分类的数字,也就是分类标签
        label = int(filename.split('_')[0]) # return 1
        #将获得的类别添加到train_y中
        train_y.append(label)
 
    ## step 2: Getting testing set
    print("---Getting testing set...")
    #返回testDigits目录下的文件名
    testingFileList = os.listdir(dataSetDir + 'testDigits') # load the testing set
    
    #返回文件夹下文件的个数
    numSamples = len(testingFileList)
    
    # 初始化测试样本数据矩阵(numSamples*1024)
    test_x = np.zeros((numSamples, 1024))
    test_y = []
    
    for i in range(numSamples):
        #获得文件的名字
        filename = testingFileList[i]
 
        #将每一个文件的1x1024数据存储到test_x矩阵中
        test_x[i, :] = img2vector(dataSetDir + 'testDigits/%s' % filename) 
 
        #获得分类的数字,也就是分类标签
        label = int(filename.split('_')[0]) # return 1
        #将获得的类别添加到test_y中
        test_y.append(label)
 
    return train_x, train_y, test_x, test_y

"""
函数说明:手写数字分类测试

Parameters:
	无
Returns:
	无
"""
def testHandWritingClass():
    ## step 1: load data
    print("step 1: load data...")
    train_x, train_y, test_x, test_y = loadDataSet()
 
    ## step 2: training...
    print("step 2: training...")
    pass
 
    ## step 3: testing
    print("step 3: testing...")
    numTestSamples = test_x.shape[0]
    matchCount = 0
    
    #构建kNN分类器
    #knn = kNN(n_neighbors = 3, algorithm = 'auto')
    knn = neighbors.KNeighborsClassifier(n_neighbors = 3)

    #拟合模型, train_x为训练矩阵,train_y为对应的标签
    knn.fit(train_x, train_y)
    
    #预测数据
    predict = knn.predict(test_x)
    
    for i in range(numTestSamples):
        
        print("Really Lable: %d \t KNN Lable :%d" % (test_y[i],predict[i]))
        if predict[i] == test_y[i]:
            matchCount += 1.0
    accuracy = float(matchCount) / numTestSamples
    ## step 4: show the result
    print("step 4: show the result...")  
    print("总共错了%d个数据\n" % (numTestSamples-matchCount))
    
    # 获得预测准确率
    # http://scikit-learn.org/stable/modules/generated/sklearn.metrics.accuracy_score.html
    #方法一
    # print(accuracy_score(test_y, predict))
    
    #方法二
    print('准确率是: %.2f%%' % (accuracy * 100))

if __name__ == '__main__':
	testHandWritingClass()

最后的结果如下所示。
《人工智能》机器学习 - 第2章 KNN算法【分类】(二 算法实战)_数据_11

【完整代码参看6.KNN_HandWritingClassify下的KNN_HandWritingClassify_v2】
最后,总结一下。k-近邻算法是分类数据最简单有效的算法。k-近邻算法是基于实例的学习,使用算法时我们必须有接近实际数据的训练样本数据。k-近邻算法必须保存全部数据集,如果训练数据集很大,必须使用大量的存储空间。此外,由于必须对数据集中的每个数据计算距离值实际使用是可能非常耗时。是否存在一种算法减少存储空间和计算时间的开销呢?k决策树就是k近邻算法的优化版,可以节省大量的计算开销。

参考文献:
KNeighborsClassifier API链接

Nearest Neighbors 链接

Nearest Neighbors API汇总

Nearest Neighbors Classification 实例

本章附件
点击进入