本次学习的是k近邻算法,首先是理论部分,主要是学习李航的统计学习方法,最后是代码实战,有电影的分类,约会网站再到手写识别系统,主要是学习机器学习实战。
同时本人因为python学的不好,在代码实现部分会穿插一些python的知识点,适合初学者学习。附上知识点链接:实现knn时用到的python知识点
那么,我们开始吧!
目录
sklearn.neighbors.KNeighborsClassifier-识别手写数据
k近邻法
k近邻算法是一种基本的分类回归的方法。此方法的输入为实例的特征向量,对应特征空间上的点,输出为输入的类别,可以取多类。
k近邻的基本步骤是,假定给定一个训练的数据集,其中的实例类别已定,分类时,对于新的实例,根据其k个最近邻的训练实例的类别,通过多数表决等方式进行预测,因此不具备显式的学习过程,k近邻法实际上利用训练数据对特征向量空间进行划分,并作为其分类模型。因此,此方法的三个基本要素是:k的选择、距离的度量、分类决策规则。
优缺点
优点
- 简单好用,容易理解,精度高,理论成熟,既可以用来做分类也可以用来做回归;
- 可用于数值型数据和离散型数据;
- 训练时间复杂度为O(n);无数据输入假定;
- 对异常值不敏感。
缺点:
- 计算复杂性高;空间复杂性高;
- 样本不平衡问题(即有些类别的样本数量很多,而其它样本的数量很少);
- 一般数值很大的时候不用这个,计算量太大。但是单个样本又不能太少,否则容易发生误分。
- 最大的缺点是无法给出数据的内在含义。
那么针对方法的三个要素,我们一个个来看:
距离度量
可使用的距离是欧式距离,但也可以使用其他的距离如Lp距离和minkowski距离。接下来是看一下距离公式:
当p=2时,为欧式距离:
p=1,为曼哈顿距离:
p=无穷,为切比雪夫距离。
距离的分类,给一个学习的链接,讲的不错:几种距离分类
接下来给一个《统计学习方法》上的例子,以便更好的理解:
k值的选择
k值的选择对于k近邻法非常重要
如果k选择的越小,则相当于在较小的领域中对训练实例进行预测,学习的近似误差会减小,只有与输入实例相近的训练实例才会对结果起到预测作用,但缺点是估计误差会增大,意味着整体模型变得复杂,更加偏向于训练数据,容易产生过拟合。
如果k选择的越大,意味着在较大的邻域中进行预测,优点是可以减少估计误差,但整体模型更简单,意味着会使预测发生错误。
所以开始的时候会设置一个较小的k值,然后通过交叉验证法来选取最优的k值。
关于近似误差与估计误差,如果不懂可以查看这个链接:近似误差和估计误差的区别
分类决策规则
此部分就不多说了,就是查看k个近邻中,多数的是哪一类,分类决策是多数表决。
快速k近邻搜索之kd树
k近邻最简单的实现方法是线性扫描,这时要计算输入的实例与每一个训练实例的距离,当训练集过大时,计算十分耗时,所以为了提高k近邻的搜索的效率,可以考虑特殊的结构来存储训练数据,以减少计算距离的时间。
简单言之。我们只需要找到离待分类近的点,所以离得较远的点我们可以不用去计算,所以,需要提前找离得较近的点。
构造kd树
构造算法如下,个人觉得《统计学习方法》中文字描述的较为抽象,有了例子可以更好的理解:
结合例子,可以更好的理解:
搜索kd树
还是具体的算法文字描述,仔给出例子:
搜索的例子部分,书上讲的不太清除,给一个很清楚的例子的链接:kd树的搜索
knn的实现
电影分类
众所周知,电影可以按照题材来分类,那么题材本身如何定义,怎么判别电影的题材,相同题材有哪些共同特征,这个例子,我们来使用k-近邻算法来实现动作片和爱情片的分类,有人曾统计过电影的打斗镜头数量和接吻镜头数量。我们通过一张图和一张表来看一下:
我们将通过k-近邻方法的计算来求最后一个电影的具体题材。
首先是导入数据部分:
from numpy import *
import operator
def createDataSet():
group = array([[3.0,104.0],[2.0,100.0],[1.0,81.0],[101.0,10.0],[99.0,5.0],[98.0,2.0]])
labels = ['爱情片','爱情片','爱情片','动作片','动作片','动作片']
return group,labels
接下来是knn算法步骤:
knn算法步骤
对于未知类别属性的数据集中的每个点依次执行以下的操作:
1、计算已知类别数据集中的点与当前点之间的距离;
2、按照距离递增数据依次排序
3、选取与当前点距离最小的k个点;
4、确定前k个点所在类别的出现频率;
5、返回前k个点出现的频率最高的类别作为当前点的预测分类
代码如下
def classify0(inx,dataSet,labels,k):
dataSetSize = dataSet.shape[0]#返回数据集一维的维数
diffMat = tile(inx,(dataSetSize,1)) - dataSet#待分类数据重复dataSetSize行1列,再与原来已经分类好的数据相减
sqDiffMat = diffMat**2#每个数求平方
sqDistances = sqDiffMat.sum(axis=1)#二维列表中的元素按行相加
distances = sqDistances**0.5#元素求平方根
sortedDistIndicies = distances.argsort()#数值大小排序后,其索引的顺序
classCount={}
for i in range(k):#获取k个距离最小的标签
voteIlabel = labels[sortedDistIndicies[i]]
classCount[voteIlabel] = classCount.get(voteIlabel,0)+1#查找该键值,如果不存在,则返回默认值0
sortedClassCount = sorted(classCount.items(),key=operator.itemgetter(1),reverse = True)#以键为可迭代对象,以值为比较对象,按降序排列
return sortedClassCount[0][0]
最后是测试部分,测试分类结果:
group,labels=createDataSet()
classify0([18.0,90.0],group,labels,3)
结果如上所示。
约会网站
在此部分会用到两个数据集,可以在我的资源中找到,粉丝可以免费下载。
数据集包含三个特征:
每年获得的飞行常客里程数
玩视频游戏所耗的时间百分比
每周消费的冰淇凌公升数
三种类型的标签:
不喜欢的人
魅力一般的人
极具魅力的人
首先是从文本中解析数据:
def file2matrix(filename):
fr = open(filename)
arrayOLines = fr.readlines()#读取文件的每一行
numberOfLines = len(arrayOLines)#得到文件的行数
returnMat = zeros((numberOfLines,3))#创建numberOfLines行3列的二维数组,中间括号为[]也可
classLabelVector = []#创建数组用于存储标签
index = 0
for line in arrayOLines:
line = line.strip()#删除字符串头尾的字符,注意只能删除开头和结尾,中间的字符不能删除
listFromLine = line.split('\t')#以指定字符分割字符串
returnMat[index,0:3] = listFromLine[0:3]#将每行前三个赋给returnMat[index]
classLabelVector.append(listFromLine[-1])#最后一个元素添加到标签数组
index += 1
return returnMat,classLabelVector
我们来看一下解析之后的结果:
相比于文字来了解数据,图形化的方式更能直观的展现数据:
import matplotlib
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(111)
ax.scatter(datingDataMat[:,1],datingDataMat[:,2])
plt.show()
%matplotlib inline
使用颜色对每个标签进行区分,同时对两两的特征,画出图形:
from matplotlib.font_manager import FontProperties
def huahua(datingDataMat,datingLabels,axis):
zhfont = FontProperties(fname='C:\Windows\Fonts\simsun.ttc',size=12)
fig = plt.figure()
plt.figure(figsize=(8, 5), dpi=80)
ax = plt.subplot(111)
datingLabels = array(datingLabels)
idx_1 = where(datingLabels == '1')
p1 = ax.scatter(datingDataMat[idx_1,axis[0]],datingDataMat[idx_1,axis[1]],marker = '*',color = 'r',label='1',s=10)
idx_2 = where(datingLabels == '2')
p2 = ax.scatter(datingDataMat[idx_2,axis[0]],datingDataMat[idx_2,axis[1]],marker = 'o',color ='g',label='2',s=20)
idx_3 = where(datingLabels == '3')
p3 = ax.scatter(datingDataMat[idx_3,axis[0]],datingDataMat[idx_3,axis[1]],marker = '+',color ='b',label='3',s=30)
ax.legend((p1, p2, p3), (u'不喜欢', u'魅力一般', u'极具魅力'), loc=2, prop=zhfont)
plt.show()
return 0
以下是运行结果:
我们提取数据集中的一段数据查看一下:
使用其中两组计算欧式距离:
我们会发现,数据差值最大的属性对计算结果影响较大,但是对于特征而言,三个特征同等重要,因此不同特征值间因为取值大小而影响结果不应该存在。
因此需要采用数值归一化的方法,将数值取值范围处理为0~1或者-1~1之间,具体计算公式如下:
max是此特征中的最大取值,min是最小取值。
具体算法如下:
def autoNorm(dataSet):
minVals = dataSet.min(0)#提取每列的最小元素
maxVals = dataSet.max(0)#提取每列的最大元素
ranges = maxVals-minVals#相减
normDataSet = zeros(shape(dataSet))#生成一个形状和dataSet一样的,元素全为0的数组
m = dataSet.shape[0]#数据集一维的维数,二维也就是行数
normDataSet = dataSet - tile(minVals,(m,1))#初始值减去最小值
normDataSet = normDataSet/tile(ranges,(m,1))#初始值减去最小值除最大值减去最小值
return normDataSet,ranges,minVals#返回新的数据集,每列最大值减去最小值,每列最小值
运行结果如下:
最后来看下结果:
datingDataMat, datingLabels = file2matrix('D:\python-txt\datingTestSet.txt')
hoRatio = 0.10
normMat, ranges, minVals = autoNorm(datingDataMat)
m = normMat.shape[0]
numTestVecs = int(m * hoRatio)
errorCount = 0.0
for i in range(numTestVecs):
classifierResult = classify0(normMat[i,:], normMat[numTestVecs:m,:],datingLabels[numTestVecs:m], 4)
#print("分类结果:%d\t真实类别:%d" % (classifierResult, datingLabels[i]))
if classifierResult != datingLabels[i]:
errorCount += 1.0
print("错误率:%f%%" %(errorCount/float(numTestVecs)*100))
手写识别系统
这里构造的系统是识别数字0~9,数据集是.txt文件,文件是32*32的黑白图像,有俩组文件trainingDigits和testDigits,每个数字有两百个样本,其中文件名为0_100.txt,0表示数字,100表示0的第100个样本,样本集可以在我的资源里面下载,粉丝免费。
样本的例子如下:
首先我们需要将图形格式化处理为一个向量,我们将把32*32转换为1*1024的向量。
def img2vector(filename):
returnVect = zeros((1,1024))#生成1行1024列的全0的列表
fr = open(filename)
for i in range(32):
lineStr = fr.readline()#获取文件的1行
for j in range(32):
returnVect[0,32*i+j] = int(lineStr[j])#递归获取元素并赋值
return returnVect
手敲算法-识别手写数字
from os import listdir
def handwritingClassTest():
hwLabels = []#存储每个文件代表的数字
trainingFileList = listdir('D:/python-txt/trainingDigits')
m = len(trainingFileList)#文件夹中的文件个数
trainingMat = zeros((m,1024))
for i in range(m):
fileNameStr = trainingFileList[i]#获取第一个文件名
fileStr = fileNameStr.split('.')[0]#按’.‘分割文件名,并获取分割后的第一个元素
classNumStr = int(fileStr.split('_')[0])#获取以’_‘后的第一个元素
hwLabels.append(classNumStr)
trainingMat[i,:] = img2vector('D:/python-txt/trainingDigits/%s'%fileNameStr)
testFileList = listdir('D:/python-txt/testDigits')
errorCount = 0.0
mTest = len(testFileList)
for i in range(mTest):
fileNameStr = testFileList[i]#获取第一个文件名
fileStr = fileNameStr.split('.')[0]#按’.‘分割文件名,并获取分割后的第一个元素
classNumStr = int(fileStr.split('_')[0])#获取以’_‘后的第一个元素
vectorUnderTest = img2vector('D:/python-txt/testDigits/%s'%fileNameStr)
classifierResult = classify0(vectorUnderTest,trainingMat,hwLabels,3)
#print('分类结果是%d,真实的结果是%d'%(classifierResult,classNumStr))
if(classifierResult!=classNumStr):
errorCount +=1.0
print('错误个数:%d'%errorCount)
print("错误率:%f%%" %(errorCount/float(mTest)*100))
来看一下运行的结果:
sklearn.neighbors.KNeighborsClassifier-识别手写数据
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提供了以一些方法供我们使用:
from sklearn.neighbors import KNeighborsClassifier as kNN
def handwritingClassTest_sk_knn():
#测试集的Labels
hwLabels = []
#返回trainingDigits目录下的文件名
trainingFileList = listdir('D:/python-txt/trainingDigits')
#返回文件夹下文件的个数
m = len(trainingFileList)
#初始化训练的Mat矩阵,测试集
trainingMat = zeros((m, 1024))
#从文件名中解析出训练集的类别
for i in range(m):
#获得文件的名字
fileNameStr = trainingFileList[i]
#获得分类的数字
classNumber = int(fileNameStr.split('_')[0])
#将获得的类别添加到hwLabels中
hwLabels.append(classNumber)
#将每一个文件的1x1024数据存储到trainingMat矩阵中
trainingMat[i,:] = img2vector('D:/python-txt/trainingDigits/%s' % (fileNameStr))
#构建kNN分类器
neigh = kNN(n_neighbors = 3, algorithm = 'auto')
#拟合模型, trainingMat为测试矩阵,hwLabels为对应的标签
neigh.fit(trainingMat, hwLabels)
#返回testDigits目录下的文件列表
testFileList = listdir('D:/python-txt/testDigits')
#错误检测计数
errorCount = 0.0
#测试数据的数量
mTest = len(testFileList)
#从文件中解析出测试集的类别并进行分类测试
for i in range(mTest):
#获得文件的名字
fileNameStr = testFileList[i]
#获得分类的数字
classNumber = int(fileNameStr.split('_')[0])
#获得测试集的1x1024向量,用于训练
vectorUnderTest = img2vector('D:/python-txt/testDigits/%s' % (fileNameStr))
#获得预测结果
# classifierResult = classify0(vectorUnderTest, trainingMat, hwLabels, 3)
classifierResult = neigh.predict(vectorUnderTest)
#print("分类返回结果为%d\t真实结果为%d" % (classifierResult, classNumber))
if(classifierResult != classNumber):
errorCount += 1.0
print("总共错了%d个数据\n错误率为%f%%" % (errorCount, errorCount/mTest * 100))
运行结果:
到此,knn的学习就结束啦!
参考书籍:
李航《统计学习方法》
《机器学习实战》