机器学习的学习与实现——k近邻

本次学习的是k近邻算法,首先是理论部分,主要是学习李航的统计学习方法,最后是代码实战,有电影的分类,约会网站再到手写识别系统,主要是学习机器学习实战。

同时本人因为python学的不好,在代码实现部分会穿插一些python的知识点,适合初学者学习。附上知识点链接:实现knn时用到的python知识点

那么,我们开始吧!

目录

k近邻法

优缺点

距离度量

k值的选择

分类决策规则

快速k近邻搜索之kd树

构造kd树

搜索kd树

knn的实现

电影分类

约会网站

手写识别系统

手敲算法-识别手写数字

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的学习就结束啦!

参考书籍:

李航《统计学习方法》

《机器学习实战》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值