写在前面的话
本文记录的是 美国 Peter Harrington 所著的《机器学习实战》中的学习笔记。但这本书是2013年出版的,因而有些代码方法已经过时,并且书中是使用的Python2,现在有了更加方便简洁的方法来实现,所以后需对各种算法的介绍会分两次进行,第一次使用书上原本的代码来实现,但会将它使用Python3重写。第二次会使用简便的新方法来实现,一般都是调用sklearn库。学习的整个过程中,也查阅了一些论文和博文。为了方便理解,对主要代码都进行了注释,建议搭配着书一起看。
好了,废话不多说,开始介绍第一个算法吧!
k-近邻算法
1、简介
K-近邻(K-nearest neighbor)是一种常用的有监督学习分类算法,于 1968 年由Hart 与 Cover 首次提出。其基本原理为:
-
存在一个样本数据集合, 也称作训练样本集, 并且样本集中每个数据都存在标签, 即我们知道样本集中每一数据与所属分类的对应关系。
-
输入没有标签的新数据后, 将新数据的每个特征与样本集中数据对应的特征进行比较, 然后算法提取样本集中特征最相似数据 (最近邻) 的分类标签。
-
一般来说, 我们只选择样本数据集中前k个最相似的数据, 这就是k-近邻算法中k的出处, 通常k是不大于20的整数。
-
最后, 选择k个最相似数据中出现次数最多的分类, 作为新数据的分类。
也就是输入无标签的待分类样本,在训练集中找到与新输入的样本相似度最高的 K 个,在这 K 个样本里出现最多的类别即为新到来的样本的所属类别。从其名称中可简单认为是寻找最近的 K 个邻居。其具体原理也很好理解:通过计算待分类的数据点与已知数据集中的所有数据点之间的距离,取其中距离最小的前 K 个点,按照“少数服从多数”的原则,将该数据点划分到出现最多的类别。如下图,当K 取 5,对点 Xu即选择与其最近的 5 个点,由于其中 4 个点都被归为 w1 类,故在 K=5 时点 Xu也被归为 w1 类。
K 近邻算法的核心在于找到实例的邻居,而其与邻居之间的距离可用于度量它们之间的相似程度。通常情况下,K 近邻模型的特征空间是 n 维实数向量空间,常用的距离度量表示法有欧式距离、曼哈顿距离、夹角余弦等。这里主要介绍书上所使用的方法——欧氏距离。
欧式距离是一种最常用的距离度量方法,它源于欧式空间中两点的距离,又被称作欧几里得度量。
2、使用流程
k-近邻算法的一般流程如下:
(1) 收集数据:可以使用任何方法。
(2) 准备数据:距离计算所需要的数值, 最好是结构化的数据格式。
(3) 分析数据:可以使用任何方法。
(4) 训练算法:此步骤不适用于k-近邻算法。
(5) 测试算法:计算错误率。
(6) 使用算法:首先需要输入样本数据和结构化的输出结果, 然后运行k-近邻算法判定输入数据分别属于哪个分类, 最后应用对计算出的分类执行后续的处理。
3、代码实现
k-近邻算法:
def classify0(inX, dataSet, labels, k): # k 近邻算法
dataSetSize = dataSet.shape[0] #计算dataSet有多少行,这里的0维指x行---标签向量的元素数目和矩阵 dataSet 的行数相同
# 计算距离---以数据 group = np.array([[1.0, 1.1], [1.0, 1.0], [0, 0], [0, 0.1]])
# labels = ['A', 'A', 'B', 'B'] 为例,
# 并假设即将用于分类的输入数据 inX = [0,1],此时dataSetSize=4:
# np.tile(inX, (dataSetSize, 1)):先将要进行分类的数据向量按要求复制成可以和数据集进行减法运算的形式---将inX纵向复制4次:
# 0, 1 dataSet:1.0, 1.1
# 0, 1 1.0, 1.0
# 0, 1 0,0
# 0, 1 0, 0.1
# np.tile(inX, (dataSetSize, 1)) - dataSet:
# -1.0, -0.1
# -1.0, 0
# 0, 1.0
# 0, 0.9
# diffMat**2:
# 1.0, 0.01
# 1.0, 0
# 0, 1.0
# 0, 0.81
# sqDiffMat.sum(axis=1):
# 1.01
# 1.0
# 1.0
# 0.81
# sqDistances**0.5:
# 1.005
# 1.0
# 1.0
# 0.9
# 得出了输入数据[0,1]与数据集中的四个数据分别的距离
diffMat = np.tile(inX, (dataSetSize, 1)) - dataSet #生成和dataSet一样大小的数组,其每行的成员(x,y)都是inX的x,y减dataSet对应行的x,y
sqDiffMat = diffMat**2 #对每个成员做平方操作
sqDistances = sqDiffMat.sum(axis=1) # 把每行的两个值相加---axis=1:将一个矩阵的每一 行 向量相加,axis=0:将一个矩阵的每一 列 向量相加
distances = sqDistances**0.5 # 再把相加的值开方,就算出了距离
sortedDistIndicies = distances.argsort() #将数据 从小到大 排序,返回 的是 数据在原列表中的 索引
classCount = {}
#选择距离最小的 k 个点
for i in range(k):
voteIlabel = labels[sortedDistIndicies[i]] #获得数据集中距离最小的数据所对应的标签
classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1 #将标签作为 key 存入 字典,其value为 该标签在循环中出现的次数。
# 以上诉为例,取 k=3,则classCount为 {B:2,A:1}
#排序
#sorted可对迭代类型排序
#operator是python的一个模块,其中itemgetter函数可以获得对象不同维度的数据,参数为维度的索引值
#比如[('B', 2), ('A', 1)],那么op.itemgetter(1)就是以第2维来排序,即以后面的数字来排序。
#reverse是否反转,默认排序结果是从小到大,这里想要的是 从大到小。
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
return sortedClassCount[0][0] #这里取第0行第0列,即出现次数最多的 标签
介绍两个例子:约会网站的配对优化 和 手写数字识别
3.1 约会网站配对优化
3.1.1 导入模块
在整个代码中,我们会用到以下模块:
import numpy as np
import operator
from os import listdir
import matplotlib
import matplotlib.pyplot as plt
3.1.2 准备数据
所有的数据存放在文本文件datingTestSet2.txt中,在将文件中的特征数据输入到分类器之前,必须先将待处理数据的格式改变为可接受的格式。有下面的文本数据样例可知,每个样本数据占一行,并且每行的每个数据是以制表符来进行划分的,因而可按此规则将其进行切割。file2matrix函数输入的是文件名字符串,输出的为 训练样本矩阵 和 类标签向量。
def file2matrix(filename):
love_dictionary = {'largeDoses':3, 'smallDoses':2, 'didntLike':1} #三个类别:非常喜欢,有点喜欢,不喜欢
fr = open(filename)
arrayOLines = fr.readlines() # 按 行 读取数据
numberOfLines = len(arrayOLines) #get the number of lines in the file
returnMat = np.zeros((numberOfLines, 3)) #创建一个 numberOfLines行,3列的矩阵,初始化为0.
classLabelVector = [] #用于存放第四列的的值
index = 0
for line in arrayOLines: #处理读到的每一行数据
line = line.strip() #截取掉 两端 的回车和空格
listFromLine = line.split('\t') #因为输入的数据每行的每个数据是以制表符分隔的,所以以此对数据进行切割,得到一个元素列表
returnMat[index, :] = listFromLine[0:3] #截取元素列表的 前3个 数据存储到 矩阵中。returnMat[index,:]表示对于returnMat这个二维矩阵,行取index所在的行,而列取全部列
if(listFromLine[-1].isdigit()):
classLabelVector.append(int(listFromLine[-1])) #将元素列表的最后一行以 整型 数据的形式存储到向量classLabelVector中
else:
classLabelVector.append(love_dictionary.get(listFromLine[-1]))
index += 1 #行数加1
return returnMat, classLabelVector
读取到数据后,我们可以将数据用散点图画出来,以便观察。
def showdatas(dataSet, labels):
# 使用该函数确定图的位置。fig 是图像对象,ax 是坐标轴对象
# fig, ax = plt.subplots(1,3),其中参数1和3分别代表子图的行数和列数,一共有 1x3 个子图像。函数返回一个figure图像和子图ax的array列表。
# fig, ax = plt.subplots(1,3,1),最后一个参数1代表第一个子图。
# 如果想要设置子图的宽度和高度可以在函数内加入figsize值
# fig, ax = plt.subplots(1,3,figsize=(15,7)),这样就会有1行3个15x7大小的子图
# sharex,sharey:所有的子图是否共用 x轴,y轴
fig, axs = plt.subplots(nrows=2, ncols=2, sharex=False, sharey=False, figsize=(13,8)) #画布大小 2行2列
LabelsColors = []
#把1,2,3分别赋予黑,黄和红,并对应存储在Labels Colors列表中。标签集中的1,2,3分别代表魅力程度 {'largeDoses':3, 'smallDoses':2, 'didntLike':1}
for i in labels:
if i == 1:
LabelsColors.append('black')
elif i == 2:
LabelsColors.append('orange')
elif i == 3:
LabelsColors.append('red')
# 要显示中文字体,则需要添加如下两行代码
plt.rcParams['font.sans-serif'] = ['SimHei'] #显示中文标签
plt.rcParams['axes.unicode_minus'] = False #用来正常显示 负号
#第一块画布,位置为(0,0),画出以飞行常客路程为x轴(即dataSet第一列数据), 以玩游戏为y轴(即dataSet第二列数据)
# 颜色为对应数字代表的颜色
axs[0,0].scatter(x = dataSet[:,0], y = dataSet[:,1], color = LabelsColors, s = 15)
#设置x轴和y轴的标题
axs[0,0].set_title("每年获得的飞行常客里程数与玩视频游戏所消耗时间占比")
axs[0,0].set_xlabel("每年获得的飞行常客里程数")
axs[0,0].set_ylabel("玩视频游戏所消耗时间占比")
#第二块画布,画出以飞行常客路程为x轴(即dataSet第一列数据),以冰激凌为y轴(即dataSet第三列数据)
axs[0, 1].scatter(x = dataSet[:, 0], y = dataSet[:, 2], color = LabelsColors, s=15)
# 设置x轴和y轴的标题
axs[0, 1].set_title("每年获得的飞行常客里程数与每周冰激凌消费量")
axs[0, 1].set_xlabel("每年获得的飞行常客里程数")
axs[0, 1].set_ylabel("每周消费冰激凌量")
#第三块画布, 画出以玩游戏为x轴(即dataSet第二列数据), 以冰激凌为y轴(即dataSet第三列数据)
axs[1, 0].scatter(x = dataSet[:, 1], y = dataSet[:, 2], color = LabelsColors, s=15)
# 设置x轴和y轴的标题
axs[1, 0].set_title("玩游戏所消耗的时间占比与每周消费冰激凌量")
axs[1, 0].set_xlabel("玩游戏所消耗的时间占比")
axs[1, 0].set_ylabel("每周消费冰激凌量")
plt.savefig("data.png") # 保存图片的操作要在 show 图片之前,因为show图片之后,就会产生一张新的图像,此时再保存就是一个空白的图像了
plt.show()
散点图效果,其中,红色表示极具魅力,橘色表示魅力一般,黑色表示不喜欢:
因为每个数据的取值范围不同,所以要将数据进行归一化处理,这里将特征值的取值范围转化到0-1的区间内。
def autoNorm(dataSet): #归一化特征值。归一化处理公式:newValue=(oldValue-min)/(max-min)
minVals = dataSet.min(0)
maxVals = dataSet.max(0)
ranges = maxVals - minVals
normDataSet = np.zeros(np.shape(dataSet)) #shape取ndarray dataSet的大小(维度),然后创建一个一样大小的ndarray,并以0初始化。
m = dataSet.shape[0] #获得矩阵第一维的大小(长度),即1000行
normDataSet = dataSet - np.tile(minVals, (m, 1))
normDataSet = normDataSet/np.tile(ranges, (m, 1)) #element wise divide
return normDataSet, ranges, minVals
3.1.3 测试算法
我们从整个数据中随机选取 10% 的数据作为测试集,其余的作为训练集。
def datingClassTest(): #使用 数据集 进行 分类 测试
hoRatio = 0.10 #hold out 10%---预留的数据用来测试分类器
datingDataMat, datingLabels = file2matrix('datingTestSet2.txt') #datingDataMat存放文本数据的前3列,datingLabels存放第四列
normMat, ranges, minVals = autoNorm(datingDataMat)
m = normMat.shape[0] #算出0维大小,一共1000条
numTestVecs = int(m*hoRatio) #预留数据的后面数据用来训练样本。预留数据用来和这些训练样本比较,计算距离
errorCount = 0.0
for i in range(numTestVecs): #测试分类器---训练的就是从文本文件中读到的部分数据,即让程序知道 哪个数据 对应 哪个标签
classifierResult = classify0(normMat[i, :], normMat[numTestVecs:m, :], datingLabels[numTestVecs:m], 3)
#classifierResult是使用分类器测出来的结果,拿此结果和真实结果(datingLabels[i])对比,如果一样表示预测正确。
#normMat[i,:],表示norMat的第i行,第i取所有(:冒号表示所有)
#normMat[numTestVecs:m,:]表示从训练数据位置numTestVecs开始到最后一条数据m为止
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))) #统计错误率
print(errorCount)
运行结果如下:
可以看到,图中最后一行的分类出现了错误,但整体情况中,错误的并不多
在100个测试集中,总共出现了5次错误,错误率为5%,因而该方法拟合效果较好。
3.1.4 使用算法
接下来,就可以使用该算法来进行配对使用了。
def classifyPerson(): # 直接输入数据进行 分类
resultList = ['not at all', 'in small doses', 'in large doses']
percentTats = float(input("percentage of time spent playing video games?"))
ffMiles = float(input("frequent flier miles earned per year?"))
iceCream = float(input("liters of ice cream consumed per week?"))
datingDataMat, datingLabels = file2matrix('datingTestSet2.txt')
normMat, ranges, minVals = autoNorm(datingDataMat) #归一化 数据集
inArr = np.array([ffMiles, percentTats, iceCream, ]) #构造一个列表
classifierResult = classify0((inArr - minVals)/ranges, normMat, datingLabels, 3) # k近邻
print("You will probably like this person: %s" % resultList[classifierResult - 1])
运行该函数,按要求输入数据:
-
玩视频游戏所耗时间百分比
-
每年获得的飞行常客里程数
-
每周消费的冰淇淋公斤数
通过算法得出,这个人对你有很大的魅力!
3.2 手写数字识别
识别一个32*32的二进制黑白数字图像,在使用分类器之前,要将图像格式化为一个向量。这里将其转化为一个1*1024的向量。
# 手写识别---图片为 32*32 的 黑白 图像
def img2vector(filename): #将图像转化为向量
returnVect = np.zeros((1, 1024)) #创建 1*1024 的Numpy数组
fr = open(filename)
for i in range(32): #循环读出文件的 前32行
lineStr = fr.readline()
for j in range(32): #将每行的头32个字符值存储在Numpy数组中
returnVect[0, 32*i+j] = int(lineStr[j])
return returnVect
测试算法,训练集存放在文件夹trainingDigits中,测试集存放在文件夹testDigits中。
def handwritingClassTest():
hwLabels = []
trainingFileList = listdir('trainingDigits') #load the training set--- 获取训练集文件夹下的文件目录
m = len(trainingFileList) #获得文件个数
trainingMat = np.zeros((m, 1024))
for i in range(m): #从文件名解析分类的数字
fileNameStr = trainingFileList[i]
fileStr = fileNameStr.split('.')[0] #take off .txt
classNumStr = int(fileStr.split('_')[0])
hwLabels.append(classNumStr) #图片向量列表对应的 数字 标签
trainingMat[i, :] = img2vector('trainingDigits/%s' % fileNameStr) # trainingMat的第i行的所有列(:)存储 该图片的向量
testFileList = listdir('testDigits') #iterate through the test set
errorCount = 0.0
mTest = len(testFileList)
for i in range(mTest):
fileNameStr = testFileList[i]
fileStr = fileNameStr.split('.')[0] #take off .txt
classNumStr = int(fileStr.split('_')[0])
vectorUnderTest = img2vector('testDigits/%s' % fileNameStr)
classifierResult = classify0(vectorUnderTest, trainingMat, hwLabels, 3) #通过得到的训练集列表trainingMat进行k近邻算法
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)))
运行结果:
错误率:
可见,错误率只有1.05%。
总结
k近邻算法的精度是属于较高的,但是其计算复杂度较高,算法的执行效率也不高,且当数据集很大时,要占据大量的存储空间,此外,由于必须对数据集中的每个数据都计算欧氏距离,所以实际使用时可能会非常耗时。