一、K-近邻算法
1.1 k-近邻算法简介
简单的说,K-近邻算法采用测量不同特征值之间的距离的方法进行分类。
1.2 原理
存在一个样本数据集合,也称作训练样本集,并且样本集中每个数据都存在标签,即我们知道样本集中每一数据
与所属分类的对应关系。输入没有标签的新数据后,将新数据的每个特征与样本集中数据对应的特征进行比较,然后算法提取样本集中特征最相似数据(最近邻)的分类标签。选择样本数据集中前k个最相似的数据,这就是k-近邻算法中k的出处,通常k是不大于20的整数。最后,选择k个最相似数据中出现次数最多的分类,作为新数据的分类。
1.3 流程
1.4 实例讲解
确定一部电影是动作片还是爱情片。首先我们需要知道位置电源的打斗镜头和接吻镜头数。计算出样本集中所有电影与位置电影的距离,找出k个距离最近的电影,统计最近的k个电影的电影类型,找出出现最多的电影类型。
假定k=3,则三个最靠近的电影依次是He’s Not Really into Dudes、 Beautiful Woman和California Man。所以可以判定出位置电影是爱情片。
1.5 Python3.x代码实现
#coding=utf-8
_date_ = '2018/10/23 14:44'
_author_ = 'Cxy'
# KNN算法实现
from numpy import * # 导入科学计算包Numpy
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
# print(group)
# print(labels)
'''
分类函数
inX:用于分类的输入向量
dataSet:输入的训练样本集
labels:标签向量
k:用于选择最近邻的数目
'''
def classify0(inX, dataSet, labels, k):
# 距离计算
dataSetSize = dataSet.shape[0] # shape[0]返回dataSet的行数
# tile(A,reps) tile共有2个参数,A指待输入数组,reps则决定A重复的次数
diffMat = tile(inX, (dataSetSize, 1)) - dataSet # 二维特征相减
# print(diffMat)
sqDiffMat = diffMat**2 # 二维特征相减后平方
sqDistances = sqDiffMat.sum(axis=1) # sum()所有元素相加,0列相加,1行相加
distances = sqDistances**0.5 # 开方,计算出距离
# print(distances)
sortedDistIndicies = distances.argsort() # 返回distances中元素从小到大排序后的索引值
# print(sortedDistIndicies)
classCount = {} # 记录类别次数
for i in range(k):
# 提取前k个元素的类别
voteIlabel = labels[sortedDistIndicies[i]]
# print(sortedDistIndicies[i])
# 计算类别出现的次数 dict.get(key,default=None),字典的get()方法,返回指定键的值
classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1
# itemgetter(1)按字典中的键进行排序 itemgetter(0)按字典中的值进行排序 reverse降序排序
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
# print(sortedClassCount)
return sortedClassCount[0][0] # 返回次数最多的类别
group, labels = createDataSet()
print(classify0([0,0], group, labels, 3))
运行结果,如下图所示:
二、使用k-近邻算法改进约会网站配对效果
2.1 背景
海伦一直使用在线约会网站寻找适合自己的约会对象。尽管约会网站会推荐不同的人选,但她没有从中找到喜欢的人。经过一番总结,她发现曾交往过三种类型的人:
不喜欢的人
魅力一般的人
极具魅力的人
海伦收集约会数据已经有了一段时间,她把这些数据存放在文本文件datingTestSet.txt中,每个样本数据占据一行,总共有1000行。海伦的样本主要包含以下3种特征:
每年获得的飞行常客里程数
玩视频游戏所耗时间百分比
每周消费的冰淇淋公升数
前三列为特征,最后一列为标签。
2.2 准备数据:从文本文件中解析数据
# 读取数据
def file2matrix(filename):
fr = open(filename)
arrayOLines = fr.readlines() # 读取文本每行
numberOfLines = len(arrayOLines) # 获取文本行数
returnMat = zeros((numberOfLines, 3)) # 创建零矩阵,文本行数 x 3列
classLabelVector = [] # 类别标签特征
index = 0
for line in arrayOLines:
line = line.strip() # 忽略回车字符
listFromLine = line.split('\t') # 将每行内容进行切分成列表
returnMat[index, :] = listFromLine[0:3] # 将前三列元素存储到矩阵中
# 根据喜欢的程度进行分类,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
datingDataMat, datingLabels = file2matrix('datingTestSet.txt')
print(datingDataMat)
print(datingLabels)
运行结果,如下图所示:
2.3 分析数据:创建散点图
from numpy import * # 导入科学计算包Numpy
import matplotlib.pyplot as plt
# 读取数据
def file2matrix(filename):
fr = open(filename)
arrayOLines = fr.readlines() # 读取文本每行
numberOfLines = len(arrayOLines) # 获取文本行数
returnMat = zeros((numberOfLines, 3)) # 创建零矩阵,文本行数 x 3列
classLabelVector = [] # 类别标签特征
index = 0
for line in arrayOLines:
line = line.strip() # 忽略回车字符
listFromLine = line.split('\t') # 将每行内容进行切分成列表
returnMat[index, :] = listFromLine[0:3] # 将前三列元素存储到矩阵中
# 根据喜欢的程度进行分类,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
datingDataMat, datingLabels = file2matrix('datingTestSet.txt')
print(datingDataMat)
print(datingLabels)
# 数据可视化
fig = plt.figure()
ax = fig.add_subplot(111)
ax.scatter(datingDataMat[:,1], datingDataMat[:, 2],15.0*array(datingLabels), 15.0*array(datingLabels))
plt.show()
运行结果如下图所示:
2.4 准备数据:归一化数值
如上图所示的四组数据,若要计算样本3和4之间的距离,可是用如下方法:
可发现,数值差值最大的数学对计算结果影响最大。但海伦认为这三种特征是同等重要,此时我们通常采用方法是数值归一化,入江取值处理为0到1或-1到1,如下公式所示:
n
e
w
V
a
l
u
e
=
(
o
l
d
V
a
l
u
e
−
m
i
n
)
/
(
m
a
x
−
m
i
n
)
newValue = (oldValue-min) / (max - min)
newValue=(oldValue−min)/(max−min)
其中min和max分别是数据集中的最小特征值和最大特征值。实现代码如下:
from numpy import * # 导入科学计算包Numpy
# 读取数据
def file2matrix(filename):
fr = open(filename)
arrayOLines = fr.readlines() # 读取文本每行
numberOfLines = len(arrayOLines) # 获取文本行数
returnMat = zeros((numberOfLines, 3)) # 创建零矩阵,文本行数 x 3列
classLabelVector = [] # 类别标签特征
index = 0
for line in arrayOLines:
line = line.strip() # 忽略回车字符
listFromLine = line.split('\t') # 将每行内容进行切分成列表
returnMat[index, :] = listFromLine[0:3] # 将前三列元素存储到矩阵中
# 根据喜欢的程度进行分类,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
datingDataMat, datingLabels = file2matrix('datingTestSet.txt')
print(datingDataMat)
print(datingLabels)
# 归一化特征值
def autoNorm(dataSet):
minVals = dataSet.min(0) # 获取每列的最小值
maxVals = dataSet.max(0) # 获取每列的最大值
ranges = maxVals - minVals # 获取最大值减最小值的范围
normDataSet = zeros(shape(dataSet)) # shape(dataSet)返回dataSet的矩阵行列数
m = dataSet.shape[0] # shape[0]返回矩阵的行数
normDataSet = dataSet - tile(minVals, (m, 1)) # 原始值减去最小值
normDataSet = normDataSet/tile(ranges, (m, 1))
return normDataSet, ranges, minVals
normDataSet, ranges, minVals = autoNorm(datingDataMat)
print(normDataSet)
print(ranges)
print(minVals)
运行结果:
2.5 测试算法:验证分类器
用已有数据的90%作为训练样本,其余10%数据去测试分类器,检测分类器的正确率。错误率即给出错误结果的次数除以测试数据的总数。
代码如下:
# KNN算法实现
from numpy import * # 导入科学计算包Numpy
import operator # 导入运算符模块
import matplotlib
import matplotlib.pyplot as plt
'''
# 分类函数
# inX:用于分类的输入向量
# dataSet:输入的训练样本集
# labels:标签向量
# k:用于选择最近邻的数目
def classify0(inX, dataSet, labels, k):
# 距离计算
dataSetSize = dataSet.shape[0] # shape[0]返回dataSet的行数
# tile(A,reps) tile共有2个参数,A指待输入数组,reps则决定A重复的次数
diffMat = tile(inX, (dataSetSize, 1)) - dataSet # 二维特征相减
# print(diffMat)
sqDiffMat = diffMat**2 # 二维特征相减后平方
sqDistances = sqDiffMat.sum(axis=1) # sum()所有元素相加,0列相加,1行相加
distances = sqDistances**0.5 # 开方,计算出距离
# print(distances)
sortedDistIndicies = distances.argsort() # 返回distances中元素从小到大排序后的索引值
# print(sortedDistIndicies)
classCount = {} # 记录类别次数
for i in range(k):
# 提取前k个元素的类别
voteIlabel = labels[sortedDistIndicies[i]]
# print(sortedDistIndicies[i])
# 计算类别出现的次数 dict.get(key,default=None),字典的get()方法,返回指定键的值
classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1
# itemgetter(1)按字典中的键进行排序 itemgetter(0)按字典中的值进行排序 reverse降序排序
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
# print(sortedClassCount)
return sortedClassCount[0][0] # 返回次数最多的类别
# group, labels = createDataSet()
# print(classify0([0,0], group, labels, 3))
# 读取数据
def file2matrix(filename):
fr = open(filename)
arrayOLines = fr.readlines() # 读取文本每行
numberOfLines = len(arrayOLines) # 获取文本行数
returnMat = zeros((numberOfLines, 3)) # 创建零矩阵,文本行数 x 3列
classLabelVector = [] # 类别标签特征
index = 0
for line in arrayOLines:
line = line.strip() # 忽略回车字符
listFromLine = line.split('\t') # 将每行内容进行切分成列表
returnMat[index, :] = listFromLine[0:3] # 将前三列元素存储到矩阵中
# 根据喜欢的程度进行分类,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
datingDataMat, datingLabels = file2matrix('datingTestSet.txt')
print(datingDataMat)
print(datingLabels)
# # 数据可视化
# fig = plt.figure()
# ax = fig.add_subplot(111)
# ax.scatter(datingDataMat[:,1], datingDataMat[:, 2],15.0*array(datingLabels), 15.0*array(datingLabels))
# plt.show()
# 归一化特征值
def autoNorm(dataSet):
minVals = dataSet.min(0) # 获取每列的最小值
maxVals = dataSet.max(0) # 获取每列的最大值
ranges = maxVals - minVals # 获取最大值减最小值的范围
normDataSet = zeros(shape(dataSet)) # shape(dataSet)返回dataSet的矩阵行列数
m = dataSet.shape[0] # shape[0]返回矩阵的行数
normDataSet = dataSet - tile(minVals, (m, 1)) # 原始值减去最小值
normDataSet = normDataSet/tile(ranges, (m, 1))
return normDataSet, ranges, minVals
normDataSet, ranges, minVals = autoNorm(datingDataMat)
print(normDataSet)
print(ranges)
print(minVals)
#
# 计算错误率
def datingClassTest():
# 取所有数据的10%
hoRatio = 0.10
# 打开文件,获取特征矩阵和分类向量
datingDataMat,datingLabels = file2matrix('datingTestSet.txt')
# 数据归一化
normMat, ranges, minVals = autoNorm(datingDataMat)
# 获取normMat的行数
m = normMat.shape[0]
# 测试数据的10%
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(errorCount,numTestVecs)
print("错误率:%f%%" % (errorCount/float(numTestVecs)*100))
datingClassTest()
运行结果如下图所示:
2.6 使用算法:构建完成系统
通过输入某个人的信息 ,程序会给出海伦对对方喜欢的程度。
import numpy as np
import operator
def classify0(inX, dataSet, labels, k):
dataSetSize = dataSet.shape[0]
diffMat = np.tile(inX, (dataSetSize, 1)) - dataSet
sqDiffMat = diffMat**2
sqDistances = sqDiffMat.sum(axis=1)
distances = sqDistances**0.5
sortedDistIndices = distances.argsort()
classCount = {}
for i in range(k):
voteIlabel = labels[sortedDistIndices[i]]
classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
return sortedClassCount[0][0]
def file2matrix(filename):
fr = open(filename)
arrayOLines = fr.readlines() # 读取文本每行
numberOfLines = len(arrayOLines) # 获取文本行数
returnMat = np.zeros((numberOfLines, 3)) # 创建零矩阵,文本行数 x 3列
classLabelVector = [] # 类别标签特征
index = 0
for line in arrayOLines:
line = line.strip() # 忽略回车字符
listFromLine = line.split('\t') # 将每行内容进行切分成列表
returnMat[index, :] = listFromLine[0:3] # 将前三列元素存储到矩阵中
# 根据喜欢的程度进行分类,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
def autoNorm(dataSet):
minVals = dataSet.min(0)
maxVals = dataSet.max(0)
ranges = maxVals - minVals
normDataSet = np.zeros(np.shape(dataSet))
m = dataSet.shape[0]
normDataSet = dataSet - np.tile(minVals, (m, 1))
normDataSet = normDataSet / np.tile(ranges, (m, 1))
return normDataSet, ranges, minVals
def classifyPerson():
# 输出结果列表,分为三种
# resultList = ['not at all', 'in small doses', 'in large doses']
resultList = ['讨厌', '有些喜欢', '非常喜欢']
# 用户输入三维特征
percentTats = float(input("玩视频游戏所耗时间百分比:"))
ffMiles = float(input("每年获得的飞行常客里程数:"))
iceCream = float(input("每周消费的冰激淋公升数:"))
filename = "datingTestSet.txt"
datingDataMat, datingLabels = file2matrix(filename)
normMat, ranges, minVals = autoNorm(datingDataMat)
inArr = np.array([ffMiles, percentTats,iceCream])
norminArr = (inArr - minVals) / ranges
classifierResult = classify0(norminArr, normMat, datingLabels, 3)
print("你可能%s这个人" % (resultList[classifierResult - 1]))
if __name__ == '__main__':
classifyPerson()
运行结果如下图所示:
三、实例:手写体识别
3.1 背景
需要识别的数字已经使用图形处理软件,处理成具有相同的色彩和大小①:宽高是32像素× 32像素的黑白图像,并将图像转换成文本形式,如下图所示:
3.2 步骤1:图像转换为向量
编写函数,循环读出文件的前32行,并将每行的头32个字符值存储在NumPy数组中,最后返回数组。
代码如下:
import numpy as np
def img2vector(filename):
returnVect = np.zeros((1, 1024)) # 创建1x1024零向量
fr = open(filename) # 打开文件
for i in range(32): # 按行读取
lineStr = fr.readline()
for j in range(32): # 每一行读取的前32个元素添加到returnVect中
returnVect[0, 32*i + j] = int(lineStr[j])
return returnVect # 返回1x1024向量
testVector = img2vector('testDigits/0_13.txt')
print(testVector[0, 0:31]) # 测试
运行结果如下图所示:
3.3 测试算法:使用k-近邻算法识别手写数字
将数据输入到分类器,检测分类器的执行效果。编写函数handwritingClassTest()代码,并从os模块中导入函数listdir,其可列出给定目录的文件名。
完成代码:
import numpy as np
import operator
from os import listdir
def classify0(inX, dataSet, labels, k):
dataSetSize = dataSet.shape[0]
diffMat = np.tile(inX, (dataSetSize, 1)) - dataSet
sqDiffMat = diffMat**2
sqDistances = sqDiffMat.sum(axis=1)
distances = sqDistances**0.5
sortedDistIndices = distances.argsort()
classCount = {}
for i in range(k):
voteIlabel = labels[sortedDistIndices[i]]
classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
return sortedClassCount[0][0]
def img2vector(filename):
returnVect = np.zeros((1, 1024)) # 创建1x1024零向量
fr = open(filename) # 打开文件
for i in range(32): # 按行读取
lineStr = fr.readline()
for j in range(32): # 每一行读取的前32个元素添加到returnVect中
returnVect[0, 32*i + j] = int(lineStr[j])
return returnVect # 返回1x1024向量
testVector = img2vector('testDigits/0_13.txt')
print(testVector[0, 0:31]) # 测试
def handwritingClassTest():
hwLabels = [] # 测试机标签列表
trainingFileList = listdir('trainingDigits') # 返回trainingDigits目录下的文件名
m = len(trainingFileList) # 返回文件夹下的文件个数
trainingMat = np.zeros((m, 1024)) # 训练矩阵m x 1024
for i in range(m): # 获取训练集类别
fileNameStr = trainingFileList[i] # 获得文件名
fileStr = fileNameStr.split('.')[0]
classNumStr = int(fileStr.split('_')[0]) # 获取分类标签
hwLabels.append(classNumStr) # 将标签加入列表
trainingMat[i, : ] = img2vector('trainingDigits/%s' % (fileNameStr)) # 将每一个文件的1x1024矩阵装进的traningMa矩阵中
testFileList = listdir('testDigits') # 返回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) # 获取分类结果
print('分类返回结果为%d\t真是结果为%d' % (classifierResult, classNumStr))
if(classifierResult != classNumStr):
errorCount += 1.0
print("总共错了%d个数据\n错误率%f%%" % (errorCount, errorCount/mTest * 100))
handwritingClassTest()
运行结果如下图所示:
四、k-近邻算法缺点
(1)k-近邻算法必须保存全部数据集,如果训练数据集的很大,必须使用大量的存储空间。
(2)由于必须对数据集中的每个数据计算距离值,实际使用时可能非常耗时。
(3)k-近邻算法的另一个缺陷是它无法给出任何数据的基础结构信息,因此我们也无法知晓平均实例样本和典型实例样本具有什么特征。
参考文献:
[1]《python机器学习实战》
[2]https://blog.csdn.net/c406495762/article/category/7029976