《机器学习实战》笔记之二——K-近邻算法

第二章 K-近邻算法

  • K-近邻算法
  • 从文本文件中解析和导入数据
  • 使用Matplotlib创建扩展图
  • 归一化数值

2.1 k-近邻算法概述

工作原理:

存在一个样本数据集合,并且样本集中每个数据都存在标签(即目标变量,哪个类别)。输入没有标签的新数据后,将新数据的每个特征与样本集中数据对应的特征进行比较,然后算法提取样本集中特征最相似数据(最近邻)的分类标签。一般来说,取样本数据集中前k个最相似的数据,最后,选择k个最相似数据中出现次数最多的分类,作为新数据的分类。

三个基本要素:

k-值的选择、距离度量(计算相似的方法)、分类决策规则。

例子:

使用k-近邻算法分类爱情片和动作片。

首先我们如何区分爱情片和动作片,需知道其分别具有哪些特征。直观的感觉动作片打斗场景较多,但也有少量接吻镜头,而爱情片接吻镜头可能比较多,而打斗场景少些,在一部电影中直接以有无这些场景作为特征,显然用来区分两种电影的信息量有点少,难以区分。将打斗场景和接吻镜头分别在两部电影中出现的次数作为特征,信息量稍微多些,容易区分两类电影。

其次,数据说话。首先得有统计了两类电影的打斗场景和接吻镜头的次数,及其电影类型。


未知电影名称具有18次打斗场景,90次接吻镜头,我们将其分为哪类呢?首先需要计算其与样本集中其他电影的距离(此处便是距离度量,可以用欧式距离,曼哈顿距离,cos相似度计算等等)。

按距离递增排序,定K=3,我们知道前3个电影都是爱情片,故将为之电影分为爱情片。

使用python进行k-近邻分类的小例子:

创建模块kNN.py:

#!/usr/bin/env python
# coding=utf-8
from numpy import *
import operator

def createDataSet():
    group  = array([[1.0,1.1],[1.0,1.0],[0,0],[0,0.1]])
    labels = ['A','A','B','B']
    return group, labels
kNN.py模块中,导入包numpy和operator,并创建函数createDataSet(),其有两个返回值,分别是特征值和标签值(目标变量)。

Figure1: group, labels变量


k-近邻算法为代码:

对未知类别属性的数据集中的每个点依次执行以下操作:

  1. 计算已知类别数据集中的点与当前点之间的距离;
  2. 按照距离递增排序;
  3. 选取与当前点距离最小的k个点;
  4. 确定前k个点所在类别的出现频率;
  5. 返回前k个点出现频率最高的类别作为当前点的预测分类;
k-近邻算法代码:
#!/usr/bin/env python
# coding=utf-8
from numpy import *
import operator

def createDataSet():
    group  = array([[1.0,1.1],[1.0,1.0],[0,0],[0,0.1]])
    labels = ['A','A','B','B']
    return group, labels

def classify0(inX, dataSet, labels, k):
    #===========计算距离============
    dataSetSize = dataSet.shape[0]       #shape函数返回array类型的各个维数上的数的个数。对2维来说,即返回行数和列数。
    diffMat = tile(inX, (dataSetSize, 1)) - dataSet #tile()函数如同repeat功能,dataSetSize此处为4,将输入向量inX复制4×1份
    sqDiffMat = diffMat**2
    sqDistances = sqDiffMat.sum(axis=1)
    distances = sqDistances**0.5
    sortedDistIndicies = distances.argsort()#argsort()为numpy包下的函数,返回的是数组值从小到大的索引值
    #===========选择距离最小的k个点==
    classCount = {}
    for i in range(k):
        voteIlabel             = labels[sortedDistIndicies[i]]
        classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1  #先用get()函数将label I在字典里默认值为0,若无直接+1,也即label I的投票+1。
        sortedClassCount       = sorted(classCount.iteritems(), key = operator.itemgetter(1), reverse=True) #按照字典value排序
    return sortedClassCount[0][0]       #排序完了之后返回一个二维列表,[0][0]代表排序最高的那个类别

#if __name__=="__main__":
#    group,labels = createDataSet()
#    target = classify0([0,0],group,labels,3)
#    print target


Figure2: kNN结果

classify0()函数有4个参数:inX为待分类的输入向量,dataSet为训练样本,labels为训练样本的标签向量,k表示最近邻居的数目。
classify0()函数使用欧式距离公式,计算两个向量点xA和xB之间的距离:

如何测试分类器:

需要引入错误率这个概念:分类器给出错误结果的次数除以测试执行的总数。错误率是常用的评估方法,主要用于评估分类器在某个数据集上的执行效果。

2.2 示例: 使用k-近邻算法改进约会网站的配对效果

在约会网站上使用k-近邻算法:

  1. 收集数据:提供文本。
  2. 准备数据:使用python解析文本文件。
  3. 分析数据:使用Matplotlib画二维扩散图。
  4. 训练算法:此步驟不适用于k-近邻算法。
  5. 测试算法:使用海伦提供的部分数据作为测试样本。测试样本和非测试样本的区别在于:测试样本是已经完成分类的数据,如果预测分类与实际类别不同,则标记为一个错误。
  6. 使用算法:产生简单的命令行程序,然后海伦可以输入一些特征数据以判断对方是否为自己喜欢的类型。

准备数据:

数据放在文本文件datingTestSet.txt中,共1000行,被"\t"隔开为4列,有3种特征:

  • 每年获得的飞行常客里程数。
  • 玩视频游戏所耗时间百分比
  • 每周消费的冰淇淋公升数。

Figure2-2-1: datingTestSet.txt前10行示例


需要将数据转换为后续方便处理的格式,如特征用一个变量表示,目标变量用另一个变量表示,并将枚举类型的目标类别用数字代替。在kNN.py中创建函数,读入数据,输出训练样本和类别标签向量。


#!/usr/bin/env python
# coding=utf-8
import codecs
from numpy import *

def file2matrix(filename):
    with codecs.open(filename) as f:
        arrayOLines = f.readlines()
    numberOfLines   = len(arrayOLines)
    returnMat       = zeros((numberOfLines, 3))
    classLabelVector= []
    index = 0
    like_type = {}
    like_type['largeDoses'] = 3
    like_type['smallDoses'] = 2
    like_type['didntLike']  = 1
    for line in arrayOLines:
        line = line.strip()
        line = line.strip("\n")
        listFromLine = line.split("\t")
        returnMat[index, :] = listFromLine[0:3]
        if listFromLine[-1] in like_type:
            classLabelVector.append(int(like_type[listFromLine[-1]]))    
        else:
            print "like_type error",listFromLine[-1]
            classLabelVector.append(0)
        index+=1
    return returnMat, classLabelVector

Figure2-2-2: 准备数据

分析数据:

使用python工具包Matplotlib创建散点图。

Figure2-2-3: 使用第一列和第二列的属性值画图

Figure2-2-4:使用第一列和第二列的属性值的散点图


Figure2-2-5:使用第二列和第三列的属性值画图

Figure2-2-5:使用第二列和第三列的属性值散点图

准备数据:归一化处理


直接对数据使用欧式距离计算,则会使得高数量级的特征对目标变量影响权重大,但实际上,并非真正如此,对其进行归一化到[0,1)之间,认为其同等重要,故需要对数据进行归一化。如数据如下:

Figure2-2-6: 示例数据

公式:newValue = (oldValue - min)/(max - min)

增加autoNorm()函数:

def autoNorm(dataSet):
    minVals = dataSet.min(0)    #返回每列的最小值,[0, 400, 0.1]
    maxVals = dataSet.max(0)    #返回每列的最大值,[67.134000,1.1]
    ranges  = maxVals - minVals
    normDataSet = zeros(shape(dataSet)) #dataSet所有元素都变为0,即同dataSet数组一样大小的全零矩阵
    m = dataSet.shape[0]                #dataSet行数
    normDataSet = dataSet - tile(minVals,(m,1)) #tile(minVals,(m,1)):将minVals复制为m*1份
    normDataSet = normDataSet/(tile(ranges,(m,1)))#在numpy包中,矩阵除法需要使用函数linalg.solve(matA,matB)
    return normDataSet, ranges, minVals


Figure2-2-7: 归一化

测试算法:

使用错误率来检验我们的分类器的性能。对于k-近邻算法,我们的主要参数便是三要素:k的选择、距离度量的标准、分类决策的规则。对于已有的数据,将90%作为训练,剩下作为测试。设定k=3,对数据进行归一化各个特征权重一样大使用欧式距离计算,没有其他的变化这样便能够确定错误率,调整训练的数据,调整k值,不使用欧式距离计算相似度,对不同特征采取不同全值等等,都可以使得最终的错误率不一样,为此需要多次实验以得到最好的一组参数使得错误率最小。在kNN.py创建datingClassTest()函数:

def datingClassTest():
    hoRatio = 0.10
    datingDataMat, datingLabels = file2matrix("datingTestSet.txt")
    normMat,ranges,minVals = autoNorm(datingDataMat)
    m = normMat.shape[0]
    numTestVecs = int(m*hoRatio)
    errorCount  = 0.0
    for i in range(numTestVecs):        #对前10%行的数据处理
        classifierResult = classify0(normMat[i,:], normMat[numTestVecs:m, :], datingLabels[numTestVecs:m],3)
        #前10%行的数据作为测试集,并且对测试集中的每一行都进行预测,对比测试集中实际的label
        #后90%行的数据全部作为训练集,每个测试集样本都要跟90%的训练集计算距离,算出最相似的label,
        print "the classifier came back with: %d, the real answer is: %d" %(classifierResult, datingLabels[i])
        if classifierResult!=datingLabels[i]:
            errorCount+=1.0
    print "the total error rate is: %f" % (errorCount/float(numTestVecs))

Figure2-2-8: 计算算法的错误率

使用算法:

当海伦在约会网上找到某个人并输入他的信息时,我们的程序会自动给出她对对方喜欢程度的预测值。在kNN.py创建classifyPerson()函数:

def classifyPerson():
    resultList  = ["not at all","in small doses","in large doses"]
    percentTats = float(raw_input("percentage of time spent playing video games? "))
    ffMiles     = float(raw_input("frequent flier miles earned per year? "))
    iceCream    = float(raw_input("liters of ice cream consumed per year? "))
    inArr       = array([ffMiles, percentTats, iceCream])
    
    datingDataMat, datingLabels = file2matrix("datingTestSet.txt")
    normMat,ranges,minVals = autoNorm(datingDataMat)            #需要对新来的测试集也做归一化,故需要用到ranges和minVals两个变量
    classifierResult       = classify0((inArr-minVals)/ranges, normMat, datingLabels, 3)
    print "You will probably like this person: ",resultList[classifierResult - 1]

Figure2-2-9: 预测

数据链接:

datingTestSet.txt

kNN.py

#!/usr/bin/env python
# coding=utf-8
import codecs
from numpy import *
import operator

def classify0(inX, dataSet, labels, k):
    #===========计算距离============
    dataSetSize = dataSet.shape[0]       #shape函数返回array类型的各个维数上的数的个数。对2维来说,即返回行数和列数。
    diffMat = tile(inX, (dataSetSize, 1)) - dataSet #tile()函数如同repeat功能,dataSetSize此处为4,将输入向量inX复制4×1份
    sqDiffMat = diffMat**2
    sqDistances = sqDiffMat.sum(axis=1)
    distances = sqDistances**0.5
    sortedDistIndicies = distances.argsort()#argsort()为numpy包下的函数,返回的是数组值从小到大的索引值
    #===========选择距离最小的k个点==
    classCount = {}
    for i in range(k):
        voteIlabel             = labels[sortedDistIndicies[i]]
        classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1  #先用get()函数将label I在字典里默认值为0,若无直接+1,也即label I的投票+1。
        sortedClassCount       = sorted(classCount.iteritems(), key = operator.itemgetter(1), reverse=True) #按照字典value排序
    return sortedClassCount[0][0]       #排序完了之后返回一个二维列表,[0][0]代表排序最高的那个类别
    
def file2matrix(filename):
    with codecs.open(filename) as f:
        arrayOLines = f.readlines()
    numberOfLines   = len(arrayOLines)
    returnMat       = zeros((numberOfLines, 3))
    classLabelVector= []
    index = 0
    like_type = {}
    like_type['largeDoses'] = 3
    like_type['smallDoses'] = 2
    like_type['didntLike']  = 1
    for line in arrayOLines:
        line = line.strip()
        line = line.strip("\n")
        listFromLine = line.split("\t")
        returnMat[index, :] = listFromLine[0:3]
        if listFromLine[-1] in like_type:
            classLabelVector.append(int(like_type[listFromLine[-1]]))    
        else:
            print "like_type error",listFromLine[-1]
            classLabelVector.append(0)
        index+=1
    return returnMat, classLabelVector
#datingDataMat, datingLabels = kNN.file2matrix("datingTestSet.txt")

def autoNorm(dataSet):
    minVals = dataSet.min(0)    #返回每列的最小值,[0, 400, 0.1]
    maxVals = dataSet.max(0)    #返回每列的最大值,[67.134000,1.1]
    ranges  = maxVals - minVals
    normDataSet = zeros(shape(dataSet)) #dataSet所有元素都变为0,即同dataSet数组一样大小的全零矩阵
    m = dataSet.shape[0]                #dataSet行数
    normDataSet = dataSet - tile(minVals,(m,1)) #tile(minVals,(m,1)):将minVals复制为m*1份
    normDataSet = normDataSet/(tile(ranges,(m,1)))#在numpy包中,矩阵除法需要使用函数linalg.solve(matA,matB)
    return normDataSet, ranges, minVals
#normMat,ranges,minVals = kNN.autoNorm(datingDataMat)
    
def datingClassTest():
    hoRatio = 0.10
    datingDataMat, datingLabels = file2matrix("datingTestSet.txt")
    normMat,ranges,minVals = autoNorm(datingDataMat)
    m = normMat.shape[0]
    numTestVecs = int(m*hoRatio)
    errorCount  = 0.0
    for i in range(numTestVecs):        #对前10%行的数据处理
        classifierResult = classify0(normMat[i,:], normMat[numTestVecs:m, :], datingLabels[numTestVecs:m],3)
        #前10%行的数据作为测试集,并且对测试集中的每一行都进行预测,对比测试集中实际的label
        #后90%行的数据全部作为训练集,每个测试集样本都要跟90%的训练集计算距离,算出最相似的label,
        print "the classifier came back with: %d, the real answer is: %d" %(classifierResult, datingLabels[i])
        if classifierResult!=datingLabels[i]:
            errorCount+=1.0
    print "the total error rate is: %f" % (errorCount/float(numTestVecs))
#kNN.datingClassTest()

def classifyPerson():
    resultList  = ["not at all","in small doses","in large doses"]
    percentTats = float(raw_input("percentage of time spent playing video games? "))
    ffMiles     = float(raw_input("frequent flier miles earned per year? "))
    iceCream    = float(raw_input("liters of ice cream consumed per year? "))
    inArr       = array([ffMiles, percentTats, iceCream])
    
    datingDataMat, datingLabels = file2matrix("datingTestSet.txt")
    normMat,ranges,minVals = autoNorm(datingDataMat)            #需要对新来的测试集也做归一化,故需要用到ranges和minVals两个变量
    classifierResult       = classify0((inArr-minVals)/ranges, normMat, datingLabels, 3)
    print "You will probably like this person: ",resultList[classifierResult - 1]

2.3示例: 手写识别系统

此处数据使用UCI机器学习资料库http://archive.ics.uci.edu/ml中”手写数字数据集的光学识别“的部分数据。

Figure2-3-1: 数据集

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

trainingDigits目录包含1934个例子,每个数字样本大概有200个。作为训练集。

testDigits目录包含946个例子。作为测试集。同训练集没有重叠。

Figure2-3-2: 数字0的手写数字的32×32维二进制数据

为使用数据,首先需要将32×32维的二进制数据转换为1×1024的向量,以便后续处理。

图像转为向量的函数img2vector():

from numpy import *

def  img2vector(filename):
    returnVect = zeros((1,1024))#(1,1024)元组作为zeros()函数的第一个参数,创建二维数组
    fr = open(filename)
    for i in range(32):
        lineStr = fr.readline()
        for j in range(32):
            returnVect[0,32*i+j] = int(lineStr[j]) #因为一个0或者一个1作为一个字符串,共32个,直接取lineStr[j]即可
    return returnVect

Figure2-3-3: img2vector()函数效果

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

kNN.py:

# -*- coding: utf-8 -*-
"""
Created on Thu Sep  3 17:29:21 2015

@author: shifeng
"""
from numpy import *
from os import listdir
import operator
import time

def classify0(inX, dataSet, labels, k):
    #===========计算距离============
    dataSetSize = dataSet.shape[0]       #shape函数返回array类型的各个维数上的数的个数。对2维来说,即返回行数和列数。
    diffMat = tile(inX, (dataSetSize, 1)) - dataSet #tile()函数如同repeat功能,dataSetSize此处为4,将输入向量inX复制4×1份
    sqDiffMat = diffMat**2
    sqDistances = sqDiffMat.sum(axis=1)
    distances = sqDistances**0.5
    sortedDistIndicies = distances.argsort()#argsort()为numpy包下的函数,返回的是数组值从小到大的索引值
    #===========选择距离最小的k个点==
    classCount = {}
    for i in range(k):
        voteIlabel             = labels[sortedDistIndicies[i]]
        classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1  #先用get()函数将label I在字典里默认值为0,若无直接+1,也即label I的投票+1。
        sortedClassCount       = sorted(classCount.iteritems(), key = operator.itemgetter(1), reverse=True) #按照字典value排序
    return sortedClassCount[0][0]       #排序完了之后返回一个二维列表,[0][0]代表排序最高的那个类别
    
def  img2vector(filename):
    returnVect = zeros((1,1024))#(1,1024)元组作为zeros()函数的第一个参数,创建二维数组
    fr = open(filename)
    for i in range(32):
        lineStr = fr.readline()
        for j in range(32):
            returnVect[0,32*i+j] = int(lineStr[j]) #因为一个0或者一个1作为一个字符串,共32个,直接取lineStr[j]即可
    return returnVect

def handwritingClassTest():
    start_time = time.time()
    hwLabels = []
    #以往利用python os.walk()函数遍历某个文件夹下的文件,这里直接用listdir()更为方便,其返回以文件名为字符串的一个列表
    trainingFileList = listdir("trainingDigits") 
    m = len(trainingFileList)
    trainingMat = zeros((m,1024))           #同训练集一样大小的全0矩阵
    for i in range(m):
        fileNameStr  = trainingFileList[i]  #某个文件的文件名,如0_0.txt
        fileStr      = fileNameStr.split(".")[0]
        classNumStr  = int(fileStr.split("_")[0])
        hwLabels.append(classNumStr)                    #将某个类别存起来,以便和trainingMat对应起来,作为训练集的labels
        trainingMat[i, :] = img2vector("trainingDigits/%s" % fileNameStr)  #调用函数img2vector每行放一个1每行放一个1×1024的向量
        
    testFileList = listdir("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("testDigits/%s" % fileNameStr)
        classifierResult = classify0(vectorUnderTest, trainingMat, hwLabels, 3)
        if i <10:
            print "the classifier came back with: %d, the real answer is: %d" % (classifierResult, classNumStr)
        if classifierResult != classNumStr:
            errorCount += 1.0
    print "\nthe total number of errors is: %d" % errorCount
    print "\nthe total error rate is: %f" % (errorCount/float(mTest))
    end_time = time.time()        
    print "All time is ", end_time - start_time
    


Figure2-3-4: 测试算法

算法效率:

每个测试集样本计算1934次距离运算,每个距离运算1024个浮点运算,936个测试集样本,共计936×1934×1024次浮点运算。需2.1M空间存训练集。

2.4 k-近邻算法优缺点

听闻hr会问一些机器学习算法的优缺点及对比其他算法的优势等等。故除了熟悉具体算法流程,对其特点也需注意。当然,在熟悉其算法后,其特点特性也自然而然的熟悉了。此处也是书中提到的章节小节。

在数据量不是很大时,是作为最简单最有效的算法。

k-近邻算法是基于实例的学习,使用算法必须有接近实际数据的训练样本数。

缺点:

  • k-近邻算法对每个测试集样本都使用了一次全部的训练集,第一若是训练集大,需要较大的存储空间,这一点倒不是什么问题,现在处理的数据基本上上G,主要是第二点,因为对每个测试集样本都需要使用一次全部的训练集得到最短的k个距离值,那么计算必然非常耗时。
  • k-近邻算法无法给出任何数据的基础结构信息。无法知晓平均实例样本和典型实例样本具有怎样的特征。


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值