机器学习实战(一):K-近邻算法
一、K-近邻算法概述
1、工作原理
存在一个样本数据集合,也称作为训练样本集,并且样本集中每个数据都存在标签,即我们知道样本集中每一个数据与所属分类的对应关系。输入没有标签的新数据后,将新的数据的每个特征与样本集中数据对应的特征进行比较,然后算法提取样本最相似数据(最近邻)的分类标签。一般来说,我们只选择样本数据集中前k个最相似的数据,这就是k-近邻算法中k的出处,通常k是不大于20的整数。最后,选择k个最相似数据中出现次数最多的分类,作为新数据的分类。
2、特点
优点:精度高、对异常值不敏感、无数据输入假定
缺点:计算复杂度高、空间复杂度高
适用数据范围:数值型和标称型
3、实战实例:电影类别分类、约会网站配对效果判定、手写数字识别。
k-近邻算法的一般流程:
1)收集数据:可以使用爬虫进行数据的收集,也可以使用第三方提供的免费或收费的数据。一般来讲,数据放在txt文本文件中,按照一定的格式进行存储,便于解析及处理。
2)准备数据:使用Python解析、预处理数据。
3)分析数据:可以使用很多方法对数据进行分析,例如使用Matplotlib将数据可视化。
4)测试算法:计算错误率。
5)使用算法:错误率在可接受范围内,就可以运行k-近邻算法进行分类。
二、实战实例
1、电影类别分类
k-近邻算法步骤如下:
1)计算已知类别数据集中的点与当前点之间的距离;
2)按照距离递增次序排序;
3)选取与当前点距离最小的k个点;
4)确定前k个点所在类别的出现频率;
5)返回前k个点所出现频率最高的类别作为当前点的预测分类。
# 科学计算包 numpy
import numpy as np
# 运算符模块
import collections
"""
函数说明:创建数据集和标签
Returns:
group - 数据集
labels - 分类标签
"""
def createDataSet():
# 四组二维特征
group = np.array([[1, 101], [5, 89], [108, 5], [115, 8]])
# 四组特征的标签
labels = ['爱情片', '爱情片', '动作片', '动作片']
return group, labels
"""
函数说明:KNN算法,分类器
Parames:
intX - 用于分类的数据(测试集)
dataSet - 用于训练的数据(训练集)
labels - 分类标签
K - KNN算法参数,选择距离最小的k个点
Returns:
sortedClassCount[0][0] - 分类结果
"""
def classify0(inX, dataSet, labels, k):
# 计算距离
dist = np.sum((inX - dataSet)**2, axis=1)**0.5
# k个最近的标签
k_labels = [labels[index] for index in dist.argsort()[0:k]]
# 出现次数最多的标签即为最终标签 most_common(1)[0][0] 获取最高频元素的频次
label = collections.Counter(k_labels).most_common(1)[0][0]
return label
if __name__ == '__main__':
# 创建数据集
group, labels = createDataSet()
# 测试集
test = [101, 20]
# KNN分类
test_class = classify0(test, group, labels, 3)
# 打印分类结果
print(test_class)
结果:动作片
"""
函数说明:KNN算法,分类器
k-近邻算法步骤:
1.计算已知类别数据集中的点与当前点之间的距离 欧式距离:[(x2-x1)^2+(y2-y1)^2]**0.5
2.按照距离递增次序排列
3.选取与当前距离最小的k个点
4.确定前k个点所在类别的出现频率
5.返回前k个点出现频率最高的类别作为当前点的预测分类
Parames:
intX - 用于分类的数据(测试集)
dataSet - 用于训练的数据(训练集)
labels - 分类标签
K - KNN算法参数,选择距离最小的k个点
Returns:
sortedClassCount[0][0] - 分类结果
"""
def classify0(inX, dataSet, labels, k):
# numpy函数shape[0]返回dataSet的行数
dataSetSize = dataSet.shape[0]
# 将第dataSetSize行的数据复制,放在测试集后,数据集
diffMat = np.tile(inX, (dataSetSize, 1)) - dataSet
# 二维特征相减后平方
sqDiffMat = diffMat**2
# 所有元素相加 sum(0)列相加 sum(1)行相加
sqDistances = sqDiffMat.sum(axis=1)
# 开方,计算距离
distances = sqDistances**0.5
# 返回distances中元素从小到大排序后的索引值
sortedDistIndex = distances.argsort()
# 一个记录类别次数的字典
classCount = {}
for i in range(k):
# 取出前k个元素类别
voteLabel = labels[sortedDistIndex[i]]
# get(key, default=None), 字典的get()方法, 返回指定键的值, 如果值不在字典中返回默认值
# 计算类别次数
classCount[voteLabel] = classCount.get(voteLabel, 0) + 1
# 降序排列字典
# key=operator.itemgetter(1)根据字典的值进行排序
# key=operator.itemgetter(0)根据字典的键进行排序
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
# 返回次数最多的类别
return sortedClassCount[0][0]
2、约会网站配对效果判定
- 1.实战背景
1)分类:
1.不喜欢的人 didntLike
2.魅力一般的人 smallDoses
3.极具魅力的人 largeDoses
2)样本数据主要包含以下3种特征:
1.每年获得的飞行常客里程数
2.玩视频游戏所消耗时间百分比
3.每周消费的冰淇淋公升数
数据存放在文本文件datingTestSet.txt中,每个样本数据占据一行,总共有1000行 - 2.准备数据:数据分析
将待处理的数据的格式改变为分类器可以接收的格式,将数据分类两部分,即特征矩阵和对应的分类标签向量;创建名为file2matrix的函数,以此来处理输入格式问题
导入数据并将其转换为想要的的格式后,虽然可以浏览文本文件,但并不友好,因此将数据可视化
- 3.分析数据,数据可视化,创建散点图
- 4.准备数据,数据归一化
样本数据:
在处理不同范围的特征值时,通常采用的方法是将数值归一化,将取值范围处理为0到1或者-1到1之间。下面的公式可以将任意取值范围的特征值转化为0到1区间内的值:newValue = (oldValue - min) / (max - min)其中min和max分别是数据集中的最小特征值和最大特征值
顺利将数据归一化了,并且求出了数据的取值范围和数据的最小值 - 5.测试算法,验证分类器
评估算法的正确率,通常我们只提供已有数据的90%作为训练样本来训练分类器,而使用其余的10%数据去测试分类器,检测分类器的正确率,注意随机选择
- 6.使用算法,构建完整可用系统
代码汇总:
import numpy as np
import operator
import matplotlib.lines as mlines
import matplotlib.pyplot as plt
from matplotlib.font_manager import FontProperties
"""
函数说明:KNN算法,分类器
k-近邻算法步骤:
1.计算已知类别数据集中的点与当前点之间的距离 欧式距离:[(x2-x1)^2+(y2-y1)^2]**0.5
2.按照距离递增次序排列
3.选取与当前距离最小的k个点
4.确定前k个点所在类别的出现频率
5.返回前k个点出现频率最高的类别作为当前点的预测分类
Parames:
intX - 用于分类的数据(测试集)
dataSet - 用于训练的数据(训练集)
labels - 分类标签
K - KNN算法参数,选择距离最小的k个点
Returns:
sortedClassCount[0][0] - 分类结果
"""
def classify0(inX, dataSet, labels, k):
# numpy函数shape[0]返回dataSet的行数
dataSetSize = dataSet.shape[0]
# 将第dataSetSize行的数据复制,放在测试集后,数据集
diffMat = np.tile(inX, (dataSetSize, 1)) - dataSet
# 二维特征相减后平方
sqDiffMat = diffMat**2
# 所有元素相加 sum(0)列相加 sum(1)行相加
sqDistances = sqDiffMat.sum(axis=1)
# 开方,计算距离
distances = sqDistances**0.5
# 返回distances中元素从小到大排序后的索引值
sortedDistIndex = distances.argsort()
# 一个记录类别次数的字典
classCount = {}
for i in range(k):
# 取出前k个元素类别
voteLabel = labels[sortedDistIndex[i]]
# get(key, default=None), 字典的get()方法, 返回指定键的值, 如果值不在字典中返回默认值
# 计算类别次数
classCount[voteLabel] = classCount.get(voteLabel, 0) + 1
# 降序排列字典
# key=operator.itemgetter(1)根据字典的值进行排序
# key=operator.itemgetter(0)根据字典的键进行排序
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
# 返回次数最多的类别
return sortedClassCount[0][0]
"""
函数说明:打开并解析文件,对数据进行分类
1不喜欢 2魅力一般 3极具魅力
Parameters:
filename - 文件名
Returns:
returnMat - 矩阵特征
classLabelVector - 分类label向量
步骤:
1.打开文件,得到文件行数
2.创建以0填充的NumPy矩阵
3.循环处理文件中的每行数据
1)使用line.strip()截取所有回车字符
2)使用'\t'分割成一个元素列表
3)取前3个元素,存储到矩阵中,也就是特征矩阵
4)利用索引值-1(最后一列元素),将最后一列存储向量classLabelVector
注意:变量值类型为整型
"""
def file2matrix(filename):
# 1.打开文件,得到文件行数
fr = open(filename, 'r', encoding='utf-8')
arrayLines = fr.readlines()
# lstrip - 截掉字符串左边的空格或指定字符
# 针对有BOM的UTF-8文本,应该去掉BOM,否则后面会引发错误
arrayLines[0] = arrayLines[0].lstrip('\ufeff')
numberOfLines = len(arrayLines)
# 2.创建以0填充的NumPy矩阵
returnMat = np.zeros((numberOfLines, 3))
# 3.循环处理文件中的每行数据
classLabelVector = [] # 分类标签向量
index = 0 # 行的索引值
for line in arrayLines:
line = line.strip() # 1)使用line.strip()截取所有回车字符
listFromLine = line.split('\t') # 2)使用'\t'分割成一个元素列表
returnMat[index, :] = listFromLine[0:3] # 3)取前3个元素,存储到矩阵中,也就是特征矩阵
# 4)利用索引值 - 1(最后一列元素),将最后一列存储向量classLabelVector
# 根据文本标记的喜欢程度进行分类,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
"""
函数说明:可视化数据
Parameters:
datingDataMat - 特征矩阵
datingLabels - 分类label
"""
def showDatas(datingDateMat, datingLabels):
# 设置汉字格式
font = FontProperties(fname=r"c:\windows\fonts\simsun.ttc", size=14)
# 将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))
numberOfLabels = len(datingLabels)
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'每年获得的飞行常客里程数与玩视频游戏所消耗时间占比', FontProperties=font)
axs0_xlabel_text = axs[0][0].set_xlabel(u'每年获得的飞行常客里程数', FontProperties=font)
axs0_ylabel_text = axs[0][0].set_ylabel(u'玩视频游戏所消耗时间占比', 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'每年获得的飞行常客里程数与每周消费的冰激淋公升数', FontProperties=font)
axs1_xlabel_text = axs[0][1].set_xlabel(u'每年获得的飞行常客里程数', FontProperties=font)
axs1_ylabel_text = axs[0][1].set_ylabel(u'每周消费的冰激淋公升数', 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'玩视频游戏所消耗时间占比与每周消费的冰激淋公升数', FontProperties=font)
axs2_xlabel_text = axs[1][0].set_xlabel(u'玩视频游戏所消耗时间占比', FontProperties=font)
axs2_ylabel_text = axs[1][0].set_ylabel(u'每周消费的冰激淋公升数', 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()
"""
函数说明:对数据归一化
Parameters:
dataSet - 特征矩阵
Returns:
normDataSet - 归一化后的特征矩阵
ranges - 数据范围
minVals - 数据最小值
"""
def autoNorm(dataSet):
# 获得数据的最值
minVals = dataSet.min(0)
maxVals = dataSet.max(0)
# 最大值和最小值范围
ranges = maxVals - minVals
# 返回dataSet的矩阵行列数
normDataSet = np.zeros(np.shape(dataSet))
# 行数
row = dataSet.shape[0]
# 原始值减去最小值
normDataSet = dataSet - np.tile(minVals, (row, 1))
# 除以最大和最小值的差得到归一化数据
normDataSet /= np.tile(ranges, (row, 1))
# 返回归一化数据结果,数据范围,最小值
return normDataSet, ranges, minVals
def datingClassTest():
# 打开的文件名
filename = "datingTestSet.txt"
# 将返回的特征矩阵和分类向量分别存储到datingDataMat和datingLabels中
datingDataMat, datingLabels = file2matrix(filename)
# 取所有数据的百分之十
hoRatio = 0.10
# 数据归一化,返回归一化后的矩阵,数据范围,数据最小值
normMat, ranges, minVals = autoNorm(datingDataMat)
# 获得normMat的行数
row = normMat.shape[0]
# 百分之十的测试数据的个数
numTestVecs = int(row * hoRatio)
# 分类错误计数
errorCount = 0.0
for i in range(numTestVecs):
# 前numTestVecs个数据作为测试集,后m-numTestVecs个数据作为训练集
classifierResult = classify0(normMat[i, :], normMat[numTestVecs:row, :],
datingLabels[numTestVecs:row], 4)
print("分类结果:%s\t真实类别:%d" % (classifierResult, datingLabels[i]))
if classifierResult != datingLabels[i]:
errorCount += 1.0
print("错误率:%f%%" % (errorCount / float(numTestVecs) * 100))
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("你可能%s这个人" % (resultList[classifierResult-1]))
if __name__ == '__main__':
# 2.准备数据:数据解析
# filename = "datingTestSet.txt"
# # 打开并处理数据
# datingDataMat, datingLabels = file2matrix(filename)
# print(datingDataMat)
# print(datingLabels)
# 3.分析数据,数据可视化
# filename = "datingTestSet.txt"
# # 打开并处理数据
# datingDataMat, datingLabels = file2matrix(filename)
# showDatas(datingDataMat, datingLabels)
# 4.准备数据,数据归一化
# filename = "datingTestSet.txt"
# # 打开并处理数据
# datingDataMat, datingLabels = file2matrix(filename)
# normDataSet, ranges, minVals = autoNorm(datingDataMat)
# print(normDataSet)
# print(ranges)
# print(minVals)
# 5.测试算法,验证分类器
# datingClassTest()
# 6.使用算法:构建完整可用系统
classifyPerson()
3、手写识别系统
- 1.实战背景
需要识别的数字使用图形处理软件,处理成具有相同的色彩和大小:宽高是32像素x32像素,采用本文格式存储图像;文本格式存储的数字的文件命名格式为:数字的值_该数字的样本序号,数据集分为训练集和测试集
- 2.代码汇总
import numpy as np
import operator
from os import listdir
"""
函数说明:KNN算法,分类器
k-近邻算法步骤:
1.计算已知类别数据集中的点与当前点之间的距离 欧式距离:[(x2-x1)^2+(y2-y1)^2]**0.5
2.按照距离递增次序排列
3.选取与当前距离最小的k个点
4.确定前k个点所在类别的出现频率
5.返回前k个点出现频率最高的类别作为当前点的预测分类
Parames:
intX - 用于分类的数据(测试集)
dataSet - 用于训练的数据(训练集)
labels - 分类标签
K - KNN算法参数,选择距离最小的k个点
Returns:
sortedClassCount[0][0] - 分类结果
"""
def classify0(inX, dataSet, labels, k):
# numpy函数shape[0]返回dataSet的行数
dataSetSize = dataSet.shape[0]
# 将第dataSetSize行的数据复制,放在测试集后,数据集
diffMat = np.tile(inX, (dataSetSize, 1)) - dataSet
# 二维特征相减后平方
sqDiffMat = diffMat**2
# 所有元素相加 sum(0)列相加 sum(1)行相加
sqDistances = sqDiffMat.sum(axis=1)
# 开方,计算距离
distances = sqDistances**0.5
# 返回distances中元素从小到大排序后的索引值
sortedDistIndex = distances.argsort()
# 一个记录类别次数的字典
classCount = {}
for i in range(k):
# 取出前k个元素类别
voteLabel = labels[sortedDistIndex[i]]
# get(key, default=None), 字典的get()方法, 返回指定键的值, 如果值不在字典中返回默认值
# 计算类别次数
classCount[voteLabel] = classCount.get(voteLabel, 0) + 1
# 降序排列字典
# key=operator.itemgetter(1)根据字典的值进行排序
# key=operator.itemgetter(0)根据字典的键进行排序
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
# 返回次数最多的类别
return sortedClassCount[0][0]
"""
函数说明:将32*32的二进制图像转换为1*1024向量
Parameters:
fileName - 文件名
Returns:
returnVect - 返回的二进制图像的*1024向量
"""
def img2Vector(fileName):
# 创建1*1024向量
returnVect = np.zeros((1, 1024))
# 打开文件
fr = open(fileName)
# 按行读取
for i in range(32):
# 读一行数据
lineStr = fr.readline()
# 将每一行的前32个元素一次添加到returnVect中
for j in range(32):
returnVect[0, 32*i+j] = int(lineStr[j])
# 返回转换后的1*1024向量
return returnVect
"""
函数说明:手写数字分类测试
"""
def handWritingClassTest():
# 测试集的Labels
hwLabels = []
# 返回trainingDigits目录下的文件名
trainingFileList = listdir('trainingDigits')
# 返回文件夹下的文件个数
m = len(trainingFileList)
# 初始化训练的Mat矩阵,测试集
trainingMat = np.zeros((m, 1024))
# 从文件命中解析处训练集类型
for i in range(m):
# 获得文件的名字
fileNameStr = trainingFileList[i]
# 获得分类的数字
classNumber = int(fileNameStr.split('_')[0])
# 将获得的类别添加到hwLabels中
hwLabels.append(classNumber)
# 将每一个文件的1*1024数据存储到trainingMat矩阵中
trainingMat[i, :] = img2Vector('trainingDigits/%s' % (fileNameStr))
# 返回testDigits目录下的文件名
testFileList = listdir('testDigits')
# 错误检测计数
errorCount = 0
# 测试数据的数量
mTest = len(testFileList)
# 从文件中解析出测试集的类别并进行分类测试
for i in range(mTest):
# 获得文件的名字
fileNameStr = testFileList[i]
# 获得分类的数字
classNumber = int(fileNameStr.split('_')[0])
# 获得测试集的1x1024向量,用于训练
vectorUnderTest = img2Vector('testDigits/%s' % (fileNameStr))
# 获得预测结果
classifierResult = classify0(vectorUnderTest, trainingMat, hwLabels, 3)
print("分类返回结果为%d\t真实结果为%d" % (classifierResult, classNumber))
if (classifierResult != classNumber):
errorCount += 1.0
print("总共错了%d个数据\n错误率为%f%%" % (errorCount, errorCount / mTest))
if __name__ == '__main__':
handWritingClassTest()
4、总结
优点
- 简单好用,容易理解,精度高,理论成熟,既可以用来做分类也可以用来做回归;
- 可用于数值型数据和离散型数据;
- 训练时间复杂度为O(n);无数据输入假定;
- 对异常值不敏感缺点计算复杂性高;
缺点
- 空间复杂性高;样本不平衡问题(即有些类别的样本数量很多,而其它样本的数量很少);
- 一般数值很大的时候不用这个,计算量太大。但是单个样本又不能太少,否则容易发生误分。
- 最大的缺点是无法给出数据的内在含义