机器学习实战学习手册——k-NN篇

机器学习实战学习手册——k-NN篇

  • k近邻法(k-nearest neighbor/ k-NN)
    • 1. 算法背景
    • 2. 二维分类模型
      • 2.1 过程示例
        • 2.1.1 数据比较
        • 2.1.2 数据选取及标签提取
    • 3. n 维分类模型
      • 3.1 项目示例: 优化约会网站的配对效果
      • 过程示例
        • 3.1 数据收集
        • 3.2 数据预处理
          • 3.2.1 数据拆分
          • 3.2.2 数据归一化处理
        • 3.3 数据分析
        • 3.4 测试算法
        • 3.5 数据预测
    • 学习总结
    • 参考目录

k近邻法(k-nearest neighbor/ k-NN)

1. 算法背景

k-NN是由Cover T和Hart P在1967年提出的一种基本分类与回归方法。

  • 工作原理

    现有一个样本数据集合(也称训练样本集),样本集中每个数据有对应的分类标签。输入没有标签的新数据后,将新的数据的每个特征与样本集中数据对应的特征进行比较,然后提取样本最相似数据的分类标签。一般只选择样本数据集中前k个最相似的数据,通常k是不大于20的整数(k-近邻算法的命名由来)。最后,统计k个最相似数据中的分类标签频次,把最高频标签作为新数据的分类标签。

机器学习实战学习手册——k-NN篇_第1张图片

2. 二维分类模型

举个简单的例子,我们可以使用k-近邻算法分类一个电影是爱情片还是动作片。

				表2-1 含4个样本的训练集
Film name Fight scenes Kiss scenes Tag
movie_1 1 101 romantic
movie_2 5 89 romantic
movie_3 108 5 action
movie_4 115 8 action

2.1 过程示例

2.1.1 数据比较

  • 理论

上面的例子用肉眼可以很快分类,但如果有100部电影要分类怎么办呢? 这个时候就要加点数学魔法啦,把Fight scenes和Kiss scenes 分别作为x, y轴变量,并用距离公式就可以方便地量化。

假设新电影名称为 newMovie = ( 101, 20 )

∣ m o v i e 1 , n e w M o v i e ∣ = ( 101 − 1 ) 2 + ( 20 − 101 ) 2 = 128.69 | movie_1, newMovie | = \sqrt{(101 - 1)^2 + (20 - 101)^2} \quad = 128.69 movie1,newMovie=(1011)2+(20101)2 =128.69

同理 ∣ m o v i e 2 , n e w M o v i e ∣ = 118.22 | movie_2, newMovie | = 118.22 movie2,newMovie=118.22 ∣ m o v i e 3 , n e w M o v i e ∣ = 18.44 | movie_3, newMovie | = 18.44 movie3,newMovie=18.44 ∣ m o v i e 4 , n e w M o v i e ∣ = 16.55 | movie_4, newMovie | = 16.55 movie4,newMovie=16.55

因此newMovie大概率为romantic movie.

  • Python3 代码示例
# -*- coding: UTF-8 -*-

'''K-NN算法---数据比较示例'''
"""
Function: createTrainingSet
Parameters:
    None
Returns:
	TrainingSet - 训练集
    labes - 分类标签
"""
import numpy as np

# 创建数据集
def createTrainingSet():
    TrainingSet = np.array([[1,101],[5,89],[108,5],[115,8]]) # data
    labels = ['romantic movie','romantic','action movie','action movie'] # tags
    return TrainingSet, Labels

# 运行主函数
if __name__ == '__main__':
    # create 
    SampleGroup, labels = createDataSet()
    # print 
    print(group)
    print(labels)

2.1.2 数据选取及标签提取

假设训练集有n个数据,对于每个newMovie,计算出n个距离并选取前K个距离最小的数据

# -*- coding: UTF-8 -*-
import numpy as np
import operator
# K-NN算法---数据选取示例
"""
Function: Classifier
Parameters:
    testSet - 分类数据(测试集)
    dataSet - 训练数据(训练集)
    labes - 分类标签
    k - kNN算法参数,选择距离最小的k个点
Returns:
    sortedClassCount[0][0] - 分类结果
"""
def Classifier(testSet, dataSet, labels, k):
    
    dataSetSize = dataSet.shape[0] # 返回dataSet sample 个数
    diffMat = np.tile(testSet, (dataSetSize, 1)) - dataSet  # 计算2维差值
    '''tile 方法产生每行为testSet,共dataSetSize行的array,
    In [9]: tile(inx, (3, 2))
    Out[9]:
    array([[1, 2, 3, 1, 2, 3],
        [1, 2, 3, 1, 2, 3],
        [1, 2, 3, 1, 2, 3]])'''
        
    sqDiffMat = diffMat**2 # 差值平方
    sqDistances = sqDiffMat.sum(axis=1) # sqDiffMat每行求和
    distances = sqDistances**0.5  # sqDiffMat每行(一个分类数据)开方,求每行距离
    sortedDistIndices = distances.argsort() # dataSetSize个距离升序,返回索引列表
    
    # 记录K个数据各标签的频次
    tagsCount = {} # 创建字典
    for i in range(k):
        #取出前k个元素的类别
        labelName = labels[sortedDistIndices[i]]
       
        #计算类别次数
        tagsCount[labelName] = tagsCount.get(labelName,0) + 1
        	#dict.get(key,default=None),返回指定键的值,如果值不在字典中返回默认值。
    
    # 对tagsCount降序排列
    TagHighest = sorted(tagsCount.items(),key=operator.itemgetter(1),reverse=True)
    	#key=operator.itemgetter(1)根据字典的值进行排序, reverse降序排序字典

    return TagHighest[0][0] #返回次数最多的类别,即newMovie的类别

# 运行主函数
if __name__ == '__main__':
    #创建数据集
    TrainingSet, labels = createTrainingSet()
    #测试集
    test = [101,20]
    #kNN分类
    test_class = Classifier( testSet, dataSet, labels, 3)
    #打印分类结果
    print(test_class)

3. n 维分类模型

  • 欧式距离

假设分类集 ∣ T e s t ∣ = m | Test | = m Test=m样本集 ∣ T r a i n ∣ = n |Train| = n Train=n

欧式距离 d i s t a n c e ( T e s t , T r a i n ) = ( T e s t 1 − T r a i n 1 ) 2 + ( T e s t 2 − T r a i n 2 ) 2 + . . . + ( T e s t m − T r a i n n ) 2 distance( Test, Train ) = \sqrt{(Test_1 - Train_1)^2 + (Test_2 - Train_2)^2 + ... + (Test_m - Train_n)^2} \quad distance(Test,Train)=(Test1Train1)2+(Test2Train2)2+...+(TestmTrainn)2

= ∑ i = 0 n ( T e s t i − T r a i n i ) 2 = \sqrt{\sum_{i=0}^n{(Test_i - Train_i)^2}} \quad =i=0n(TestiTraini)2

3.1 项目示例: 优化约会网站的配对效果

源项目地址

  • 数据特征:
    Frequent flyer miles earned per year,
    The percentage of time spent playing video games ,
    Litres of ice cream consumed per week

  • 分类标签:
    [1] didntLike(不喜欢的人)
    [2] smallDoses(魅力一般的人)
    [3] largeDoses(极具魅力的人)

  • 数据格式:

    Frequent flyer miles earned per year The percentage of time spent playing video games Litres of ice cream consumed per week Level
    40920 8.326976 0.953952 largeDoses

过程示例

3.1 数据收集

使用github上爬取的数据datingTestSet.txt

3.2 数据预处理

3.2.1 数据拆分
  • 目标

    把数据分为特征矩阵returnMat 和分类标签矩阵 classLabelVector

  • 代码示例

# -*- coding: UTF-8 -*-

from matplotlib.font_manager import FontProperties
import matplotlib.lines as mlines
import matplotlib.pyplot as plt
import numpy as np
import operator

"""
Function:Classifier

Parameters:
	inX - 用于分类的数据(测试集)
	dataSet - 用于训练的数据(训练集)
	labes - 分类标签
	k - kNN算法参数,选择距离最小的k个点
Returns:
	sortedClassCount[0][0] - 分类结果

Modify:
	2020-08-15
"""
def Classifier(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)
	print(sortedClassCount)
	#返回次数最多的类别,即所要分类的类别
	return sortedClassCount[0][0]

"""
函数说明:打开并解析文件,对数据进行分类:1代表不喜欢,2代表魅力一般,3代表极具魅力

Parameters:
	filename - 文件名
Returns:
	returnMat - 特征矩阵
	classLabelVector - 分类Label向量

Modify:
	2020-09-11
"""
def file2matrix(filename):
	#打开文件,此次应指定编码,
    
    fr = open(filename,'r',encoding = 'utf-8')
	#读取文件所有内容
    arrayOLines = fr.readlines()
    #针对有BOM的UTF-8文本,应该去掉BOM,否则后面会引发错误。
    arrayOLines[0] = arrayOLines[0].lstrip('\ufeff')
	#得到文件行数
    numberOfLines = len(arrayOLines)
	#返回的NumPy矩阵,解析完成的数据:numberOfLines行,3列
    returnMat = np.zeros((numberOfLines,3))
	#返回的分类标签向量
    classLabelVector = []
	#行的索引值
    index = 0

    for line in arrayOLines:
		#s.strip(rm),当rm空时,默认删除空白符(包括'\n','\r','\t',' ')
        line = line.strip()
		#使用s.split(str="",num=string,cout(str))将字符串根据'\t'分隔符进行切片。
        listFromLine = line.split('\t')
		#将数据前三列提取出来,存放到returnMat的NumPy矩阵中,也就是特征矩阵
        returnMat[index,:] = listFromLine[0:3]
		#根据文本中标记的喜欢的程度进行分类,1代表不喜欢,2代表魅力一般,3代表极具魅力   
		# 对于datingTestSet2.txt  最后的标签是已经经过处理的 标签已经改为了1, 2, 3
        if listFromLine[-1] == 'didntLike':
            classLabelVector.append(1)
        elif listFromLine[-1] == 'smallDoses':
            classLabelVector.append(2)
        elif listFromLine[-1] == 'largeDoses':
            classLabelVector.append(3)
        index += 1
    return returnMat, classLabelVector

运行主函数

# 数据处理
if __name__ == '__main__':
    filename = "datingTestSet.txt"
    datingTestMat, datingLables = file2matrix(filename)
    print(datingTestMat)
    print(datingLables)

在Console内输入

runfile('xxx.py文件路径', wdir='工作文件夹路径(读入文件所在文件夹)')

即可得到特征矩阵returnMat 和分类标签矩阵 classLabelVector

机器学习实战学习手册——k-NN篇_第2张图片

3.2.2 数据归一化处理

​为了方便后续数据处理,同时使程序运行时收敛加快,我们把分散的数据统一转化到更小的区间内。统计学上称此过程为归一化。

​如把 4092014488 分别转化为 0.448325350.15873259

是不是感觉数据更集中了呢~

通过(1) 式:

t r a n s f o r m e d = ( o r i g i n a l − M i n V a l u e ) / ( M a x V a l u e − M i n V a l u e ) transformed=(original-MinValue)/(MaxValue-MinValue) transformed=(originalMinValue)/(MaxValueMinValue)

MaxValue = 1,MinValue = 0

在输入层,用(1)式将训练集数据换算为 [ 0, 1 ] 区间的值

在输出层,再用(1)式换算回初始值。

  • 代码示例
"""
Function:
	autoNorm - 数据归一化[-1,1]
Parameters:
	dataSet - 特征矩阵
Returns:
	normDataSet - 归一化后的特征矩阵
	ranges - 数据范围
	minVals - 数据最小值

Modify:
	2020-09-11
"""
def autoNorm(dataSet):
	#获得数据的最小值
	minVals = dataSet.min(0)
	maxVals = dataSet.max(0)
	#最大值和最小值的范围
	ranges = maxVals - minVals
	#shape(dataSet)返回dataSet的矩阵行列数
	normDataSet = np.zeros(np.shape(dataSet))
	#返回dataSet的行数
	m = dataSet.shape[0]
	#原始值减去最小值
	normDataSet = dataSet - np.tile(minVals, (m, 1))
	#除以最大和最小值的差,得到归一化数据
	normDataSet = normDataSet / np.tile(ranges, (m, 1))
	#返回归一化数据结果,数据范围,最小值
	return normDataSet, ranges, minVals

3.3 数据分析

  • 使用Matplotlib将数据可视化
"""
Functions: 
	showDatas - 显示数据表格
Parameters:
	datingDataMat - 特征矩阵
	datingLabels - 分类Label
Returns:
	无
Modify:
	2020-09-11
"""
def showDatas(datingDataMat, datingLabels):
	#将fig画布分隔成1行1列,不共享x轴和y轴,fig画布的大小为(13,8)
	#当nrow=2,nclos=2时,代表fig画布被分为四个区域,axs[0][0]表示第一行第一个区域
	fig, axs = plt.subplots(nrows=2, ncols=2,sharex=False, sharey=False, figsize=(13,8))

    # 设置数据点颜色
	LabelsColors = []
	for i in datingLabels:
		if i == 1:
			LabelsColors.append('black')
		if i == 2:
			LabelsColors.append('orange')
		if i == 3:
			LabelsColors.append('red')
	#画出散点图,以datingDataMat矩阵的第一(飞行常客例程)、第二列(玩游戏)数据画散点数据,散点大小为15,透明度为0.5
	axs[0][0].scatter(x=datingDataMat[:,0], y=datingDataMat[:,1], color=LabelsColors,s=15, alpha=.5)
	#设置标题,x轴label,y轴label
	axs0_title_text = axs[0][0].set_title(u'Frequent-flier miles earned per year and The percentage of time spent playing video games',FontProperties=font)
	axs0_xlabel_text = axs[0][0].set_xlabel(u'Frequent-flier miles earned per year',FontProperties=font)
	axs0_ylabel_text = axs[0][0].set_ylabel(u'The percentage of time spent playing video games',FontProperties=font)
	plt.setp(axs0_title_text, size=9, weight='bold', color='red')  
	plt.setp(axs0_xlabel_text, size=7, weight='bold', color='black')  
	plt.setp(axs0_ylabel_text, size=7, weight='bold', color='black') 

	#画出散点图,以datingDataMat矩阵的第一(飞行常客例程)、第三列(冰激凌)数据画散点数据,散点大小为15,透明度为0.5
	axs[0][1].scatter(x=datingDataMat[:,0], y=datingDataMat[:,2], color=LabelsColors,s=15, alpha=.5)
	#设置标题,x轴label,y轴label
	axs1_title_text = axs[0][1].set_title(u'Frequent-flier miles earned per year and Litres of ice cream consumed per week',FontProperties=font)
	axs1_xlabel_text = axs[0][1].set_xlabel(u'Frequent-flier miles earned per year',FontProperties=font)
	axs1_ylabel_text = axs[0][1].set_ylabel(u'Litres of ice cream consumed per week',FontProperties=font)
	plt.setp(axs1_title_text, size=9, weight='bold', color='red')  
	plt.setp(axs1_xlabel_text, size=7, weight='bold', color='black')  
	plt.setp(axs1_ylabel_text, size=7, weight='bold', color='black') 

	#画出散点图,以datingDataMat矩阵的第二(玩游戏)、第三列(冰激凌)数据画散点数据,散点大小为15,透明度为0.5
	axs[1][0].scatter(x=datingDataMat[:,1], y=datingDataMat[:,2], color=LabelsColors,s=15, alpha=.5)
	#设置标题,x轴label,y轴label
	axs2_title_text = axs[1][0].set_title(u'The percentage of time spent playing video games and Litres of ice cream consumed per week',FontProperties=font)
	axs2_xlabel_text = axs[1][0].set_xlabel(u'The percentage of time spent playing video games',FontProperties=font)
	axs2_ylabel_text = axs[1][0].set_ylabel(u'Litres of ice cream consumed per week',FontProperties=font)
	plt.setp(axs2_title_text, size=9, weight='bold', color='red')  
	plt.setp(axs2_xlabel_text, size=7, weight='bold', color='black')  
	plt.setp(axs2_ylabel_text, size=7, weight='bold', color='black') 
	#设置图例
	didntLike = mlines.Line2D([], [], color='black', marker='.',
                      markersize=6, label='didntLike')
	smallDoses = mlines.Line2D([], [], color='orange', marker='.',
	                  markersize=6, label='smallDoses')
	largeDoses = mlines.Line2D([], [], color='red', marker='.',
	                  markersize=6, label='largeDoses')
	#添加图例
	axs[0][0].legend(handles=[didntLike,smallDoses,largeDoses])
	axs[0][1].legend(handles=[didntLike,smallDoses,largeDoses])
	axs[1][0].legend(handles=[didntLike,smallDoses,largeDoses])
	#显示图片
	plt.show()
  • 运行主函数
# 数据可视化
if __name__ == '__main__':
    filename = "datingTestSet.txt"
    datingTestMat, datingLables = file2matrix(filename)
    showDatas(datingTestMat, datingLables)
  • 结果分析

机器学习实战学习手册——k-NN篇_第3张图片
可以看到3类特征两两组合得到的数据分布图

从图中可以看出,Helen 最喜欢工作(飞行出差)娱乐两不误的人(要求可多了哈哈)

3.4 测试算法

一个算法设计好了,那如何判断它的准确性呢?这就要用到大量的训练集来训练分类器,再通过测试集,统计测试集的分类结果,检查算法是否能高效地解决分类问题。

  • 样本集(训练集 + 测试集)

    ​ 训练集:90 %

    ​ 测试集:10%

  • 代码示例

"""
Function:
	datingClassTest - 分类器测试函数
取百分之十的数据作为测试数据,检测分类器的正确性
Parameters:
	无
Returns:
	无

Modify:
	2020-08-17
"""
def datingClassTest():
	#打开的文件名
	filename = "datingTestSet.txt"
	#将返回的特征矩阵和分类向量分别存储到datingDataMat和datingLabels中
	datingDataMat, datingLabels = file2matrix(filename)
	#取所有数据的百分之十
	hoRatio = 0.10
	#数据归一化,返回归一化后的矩阵,数据范围,数据最小值
	normMat, ranges, minVals = autoNorm(datingDataMat)
	#获得normMat的行数
	m = normMat.shape[0]
	#百分之十的测试数据的个数
	numTestVecs = int(m * hoRatio)
	#分类错误计数
	errorCount = 0.0

	for i in range(numTestVecs):
		#前numTestVecs个数据作为测试集,后m-numTestVecs个数据作为训练集
		classifierResult = classify0(normMat[i,:], normMat[numTestVecs:m,:], 
			datingLabels[numTestVecs:m], 4)
		print("分类结果:%s\t真实类别:%d" % (classifierResult, datingLabels[i]))
		if classifierResult != datingLabels[i]:
			errorCount += 1.0
	print("错误率:%f%%" %(errorCount/float(numTestVecs)*100))

"""
Function:
	通过输入一个人的三维特征,进行分类输出

Parameters:
	无
Returns:
	无

Modify:
	2020-09-11
"""

运行主函数

"""
函数说明:main函数

Parameters:
	无
Returns:
	无

Modify:
	2017-03-24
"""
if __name__ == '__main__':
	datingClassTest()

机器学习实战学习手册——k-NN篇_第4张图片

从图中可以看出,分类器的错误率为3.00%。为了进一步检查算法准确性,还可以通过改变K值、增加样本集容量等方法,计算错误率

3.5 数据预测

  • 封装

    算法的训练和测试都完成啦,可以简单地把算法封装一下

def classifyPerson():
	#输出结果
	resultList = ['讨厌','有些喜欢','非常喜欢']
	#三维特征用户输入
	precentTats = float(input("玩视频游戏所耗时间百分比:"))
	ffMiles = float(input("每年获得的飞行常客里程数:"))
	iceCream = float(input("每周消费的冰激淋公升数:"))
	#打开的文件名
	filename = "datingTestSet.txt"
	#打开并处理数据
	datingDataMat, datingLabels = file2matrix(filename)
	#训练集归一化
	normMat, ranges, minVals = autoNorm(datingDataMat)
	#生成NumPy数组,测试集
	inArr = np.array([ffMiles, precentTats, iceCream])
	#测试集归一化
	norminArr = (inArr - minVals) / ranges
	#返回分类结果
	classifierResult = classify0(norminArr, normMat, datingLabels, 3)
	#打印结果
	print("You may %s this guy" % (resultList[classifierResult-1]))

现在就可以来真枪实战地使用它了。

  • 预测

假设我们从交友网站上爬取到一位男士的数据为(12, 66666, 0.6),

使用分类器 classifyPerson ( ),预测分类结果

The percentage of time spent playing video games:12

Frequent-flier miles earned per year:66666

Litres of ice cream consumed per week:0.6
[(1, 3)]
You may didntLike this guy

是不是挺有趣的呢哈哈~ 有机会做个妹子版的分类器,我相信已经有人行动起来了( 窥伺.jpg )

学习总结

  • 知识补充
  1. 机器学习

    基础:Coursera上吴恩达大师的——《机器学习》

    进阶:周志华大大的西瓜书——《机器学习》

  2. Python与数据科学

    Udemy的Online Course——《Python for Data Science and Machine Learning Bootcamp》

    搭配《Python机器学习》和《Hands-On Machine Learning with Scikit-Learn and TensorFlow》

  3. 数学知识一点都不能落下啊,特别是统计学的知识,恶补走起

    基础:《深入浅出统计学》

    基础:《An Introduction to Statistical Learning》

    公开课《可汗学院统计学》

  • 实战练习

    实战真的是太太太重要了,数据读写、格式、调试啥的,不去反复练习而是一味看书看视频看别人的代码,结果就是永!远!不!会!用!

------------未待完续,博主正在努力肝手册-----------

参考目录

  1. Jack-Cu的《机器学习实战》笔记
  2. ApacheCN的《机器学习实战》
  3. 异常检测下有关数据挖掘的优秀答主——微调

你可能感兴趣的:(机器学习,深度学习与数据挖掘实战,机器学习)