一、背景
海伦女士一直使用在线约会网站寻找适合自己的约会对象。尽管约会网站会推荐不同的任选,但她并不是喜欢每一个人。经过一番总结,她发现自己交往过的人可以进行如下分类
- 不喜欢的人
- 魅力一般的人
- 极具魅力的人
海伦收集约会数据已经有了一段时间,她把这些数据存放在文本文件datingTestSet.txt中,每个样本数据占据一行,总共有1000行。
海伦收集的样本数据主要包含以下3种特征:
- 每年获得的飞行常客里程数
- 玩视频游戏所消耗时间百分比
- 每周消费的冰淇淋公升数
二、准备数据:数据的解析
在将上述特征数据输入到分类器前,必须将待处理的数据的格式改变为分类器可以接收的格式。在knn_test.py文件中创建名为file2matrix的函数,以此来处理输入格式问题。 将datingTestSet.txt放到与kn_test.py相同目录下,编写代码如下
1 # -*- coding: UTF-8 -*-
2 import numpy as np
3
4 """
5 函数说明:打开并解析文件,对数据进行分类:1代表不喜欢,2代表魅力一般,3代表极具魅力
6
7 Parameters:
8 filename - 文件名
9 Returns:
10 returnMat - 特征矩阵
11 classLabelVector - 分类Label向量
12 """
13
14
15 def file2matrix(filename):
16 # 打开文件
17 fr = open(filename)
18 # 读取文件所有内容
19 arrayOLines = fr.readlines()
20 # 得到文件行数
21 numberOfLines = len(arrayOLines)
22 # 返回的NumPy矩阵,解析完成的数据:numberOfLines行,3列
23 returnMat = np.zeros((numberOfLines, 3))
24 # 返回的分类标签向量
25 classLabelVector = []
26 # 行的索引值
27 index = 0
28 for line in arrayOLines:
29 # s.strip(rm),当rm空时,默认删除空白符(包括'
','
',' ',' ')
30 line = line.strip()
31 # 使用s.split(str="",num=string,cout(str))将字符串根据' '分隔符进行切片。
32 listFromLine = line.split(' ')
33 # 将数据前三列提取出来,存放到returnMat的NumPy矩阵中,也就是特征矩阵
34 returnMat[index, :] = listFromLine[0:3]
35 # 根据文本中标记的喜欢的程度进行分类,1代表不喜欢,2代表魅力一般,3代表极具魅力
36 if listFromLine[-1] == 'didntLike':
37 classLabelVector.append(1)
38 elif listFromLine[-1] == 'smallDoses':
39 classLabelVector.append(2)
40 elif listFromLine[-1] == 'largeDoses':
41 classLabelVector.append(3)
42 index += 1
43 return returnMat, classLabelVector
44
45
46 """
47 函数说明:main函数
48
49 Parameters:
50 无
51 Returns:
52 无
53 """
54 if __name__ == '__main__':
55 # 打开的文件名
56 filename = "datingTestSet.txt"
57 # 打开并处理数据
58 datingDataMat, datingLabels = file2matrix(filename)
59 print(datingDataMat)
60 print(datingLabels)
三、分析数据:数据可视化
1 # -*- coding: UTF-8 -*-
2
3 from matplotlib.font_manager import FontProperties
4 import matplotlib.lines as mlines
5 import matplotlib.pyplot as plt
6 import numpy as np
7
8 """
9 函数说明:打开并解析文件,对数据进行分类:1代表不喜欢,2代表魅力一般,3代表极具魅力
10
11 Parameters:
12 filename - 文件名
13 Returns:
14 returnMat - 特征矩阵
15 classLabelVector - 分类Label向量
16 """
17
18
19 def file2matrix(filename):
20 # 打开文件
21 fr = open(filename)
22 # 读取文件所有内容
23 arrayOLines = fr.readlines()
24 # 得到文件行数
25 numberOfLines = len(arrayOLines)
26 # 返回的NumPy矩阵,解析完成的数据:numberOfLines行,3列
27 returnMat = np.zeros((numberOfLines, 3))
28 # 返回的分类标签向量
29 classLabelVector = []
30 # 行的索引值
31 index = 0
32 for line in arrayOLines:
33 # s.strip(rm),当rm空时,默认删除空白符(包括'
','
',' ',' ')
34 line = line.strip()
35 # 使用s.split(str="",num=string,cout(str))将字符串根据' '分隔符进行切片。
36 listFromLine = line.split(' ')
37 # 将数据前三列提取出来,存放到returnMat的NumPy矩阵中,也就是特征矩阵
38 returnMat[index, :] = listFromLine[0:3]
39 # 根据文本中标记的喜欢的程度进行分类,1代表不喜欢,2代表魅力一般,3代表极具魅力
40 if listFromLine[-1] == 'didntLike':
41 classLabelVector.append(1)
42 elif listFromLine[-1] == 'smallDoses':
43 classLabelVector.append(2)
44 elif listFromLine[-1] == 'largeDoses':
45 classLabelVector.append(3)
46 index += 1
47 return returnMat, classLabelVector
48
49
50 """
51 函数说明:可视化数据
52
53 Parameters:
54 datingDataMat - 特征矩阵
55 datingLabels - 分类Label
56 Returns:
57 无
58 """
59
60
61 def showdatas(datingDataMat, datingLabels):
62 # 设置汉字格式
63 font = FontProperties(fname=r"/Library/Fonts/Songti.ttc", size=14)
64 # 将fig画布分隔成1行1列,不共享x轴和y轴,fig画布的大小为(13,8)
65 # 当nrow=2,nclos=2时,代表fig画布被分为四个区域,axs[0][0]表示第一行第一个区域
66 fig, axs = plt.subplots(nrows=2, ncols=2, sharex=False, sharey=False, figsize=(13, 8))
67
68 numberOfLabels = len(datingLabels)
69 LabelsColors = []
70 for i in datingLabels:
71 if i == 1:
72 LabelsColors.append('black')
73 if i == 2:
74 LabelsColors.append('orange')
75 if i == 3:
76 LabelsColors.append('red')
77 # 画出散点图,以datingDataMat矩阵的第一(飞行常客例程)、第二列(玩游戏)数据画散点数据,散点大小为15,透明度为0.5
78 axs[0][0].scatter(x=datingDataMat[:, 0], y=datingDataMat[:, 1], color=LabelsColors, s=15, alpha=.5)
79 # 设置标题,x轴label,y轴label
80 axs0_title_text = axs[0][0].set_title(u'每年获得的飞行常客里程数与玩视频游戏所消耗时间占比', FontProperties=font)
81 axs0_xlabel_text = axs[0][0].set_xlabel(u'每年获得的飞行常客里程数', FontProperties=font)
82 axs0_ylabel_text = axs[0][0].set_ylabel(u'玩视频游戏所消耗时间占', FontProperties=font)
83 plt.setp(axs0_title_text, size=9, weight='bold', color='red')
84 plt.setp(axs0_xlabel_text, size=7, weight='bold', color='black')
85 plt.setp(axs0_ylabel_text, size=7, weight='bold', color='black')
86
87 # 画出散点图,以datingDataMat矩阵的第一(飞行常客例程)、第三列(冰激凌)数据画散点数据,散点大小为15,透明度为0.5
88 axs[0][1].scatter(x=datingDataMat[:, 0], y=datingDataMat[:, 2], color=LabelsColors, s=15, alpha=.5)
89 # 设置标题,x轴label,y轴label
90 axs1_title_text = axs[0][1].set_title(u'每年获得的飞行常客里程数与每周消费的冰激淋公升数', FontProperties=font)
91 axs1_xlabel_text = axs[0][1].set_xlabel(u'每年获得的飞行常客里程数', FontProperties=font)
92 axs1_ylabel_text = axs[0][1].set_ylabel(u'每周消费的冰激淋公升数', FontProperties=font)
93 plt.setp(axs1_title_text, size=9, weight='bold', color='red')
94 plt.setp(axs1_xlabel_text, size=7, weight='bold', color='black')
95 plt.setp(axs1_ylabel_text, size=7, weight='bold', color='black')
96
97 # 画出散点图,以datingDataMat矩阵的第二(玩游戏)、第三列(冰激凌)数据画散点数据,散点大小为15,透明度为0.5
98 axs[1][0].scatter(x=datingDataMat[:, 1], y=datingDataMat[:, 2], color=LabelsColors, s=15, alpha=.5)
99 # 设置标题,x轴label,y轴label
100 axs2_title_text = axs[1][0].set_title(u'玩视频游戏所消耗时间占比与每周消费的冰激淋公升数', FontProperties=font)
101 axs2_xlabel_text = axs[1][0].set_xlabel(u'玩视频游戏所消耗时间占比', FontProperties=font)
102 axs2_ylabel_text = axs[1][0].set_ylabel(u'每周消费的冰激淋公升数', FontProperties=font)
103 plt.setp(axs2_title_text, size=9, weight='bold', color='red')
104 plt.setp(axs2_xlabel_text, size=7, weight='bold', color='black')
105 plt.setp(axs2_ylabel_text, size=7, weight='bold', color='black')
106 # 设置图例
107 didntLike = mlines.Line2D([], [], color='black', marker='.',
108 markersize=6, label='didntLike')
109 smallDoses = mlines.Line2D([], [], color='orange', marker='.',
110 markersize=6, label='smallDoses')
111 largeDoses = mlines.Line2D([], [], color='red', marker='.',
112 markersize=6, label='largeDoses')
113 # 添加图例
114 axs[0][0].legend(handles=[didntLike, smallDoses, largeDoses])
115 axs[0][1].legend(handles=[didntLike, smallDoses, largeDoses])
116 axs[1][0].legend(handles=[didntLike, smallDoses, largeDoses])
117 # 显示图片
118 plt.show()
119
120
121 """
122 函数说明:main函数
123
124 Parameters:
125 无
126 Returns:
127 无
128 """
129 if __name__ == '__main__':
130 # 打开的文件名
131 filename = "datingTestSet.txt"
132 # 打开并处理数据
133 datingDataMat, datingLabels = file2matrix(filename)
134 showdatas(datingDataMat, datingLabels)
四、准备数据:数据归一化
如下给出了四组样本,如果想要计算样本3和样本4之间的距离,可以使用欧拉公式计算。
我们很容易发现,上面方程中数字差值最大的属性对计算结果的影响最大,也就是说,每年获取的飞行常客里程数对于计算结果的影响将远远大于表2.1中其他两个特征-玩视频游戏所耗时间占比和每周消费冰淇淋公斤数的影响。而产生这种现象的唯一原因,仅仅是因为飞行常客里程数远大于其他特征值。但海伦认为这三种特征是同等重要的,因此作为三个等权重的特征之一,飞行常客里程数并不应该如此严重地影响到计算结果。
在处理这种不同取值范围的特征值时,我们通常采用的方法是将数值归一化,如将取值范围处理为0到1或者-1到1之间。下面的公式可以将任意取值范围的特征值转化为0到1区间内的值:
newValue = (oldValue - min) / (max - min)
五、测试算法:验证分类器
为了测试分类器效果,编写代码如下:
1 # -*- coding: UTF-8 -*-
2 import numpy as np
3 import operator
4
5 """
6 函数说明:kNN算法,分类器
7
8 Parameters:
9 inX - 用于分类的数据(测试集)
10 dataSet - 用于训练的数据(训练集)
11 labes - 分类标签
12 k - kNN算法参数,选择距离最小的k个点
13 Returns:
14 sortedClassCount[0][0] - 分类结果
15 """
16
17
18 def classify0(inX, dataSet, labels, k):
19 # numpy函数shape[0]返回dataSet的行数
20 dataSetSize = dataSet.shape[0]
21 # 在列向量方向上重复inX共1次(横向),行向量方向上重复inX共dataSetSize次(纵向)
22 diffMat = np.tile(inX, (dataSetSize, 1)) - dataSet
23 # 二维特征相减后平方
24 sqDiffMat = diffMat ** 2
25 # sum()所有元素相加,sum(0)列相加,sum(1)行相加
26 sqDistances = sqDiffMat.sum(axis=1)
27 # 开方,计算出距离
28 distances = sqDistances ** 0.5
29 # 返回distances中元素从小到大排序后的索引值
30 sortedDistIndices = distances.argsort()
31 # 定一个记录类别次数的字典
32 classCount = {}
33 for i in range(k):
34 # 取出前k个元素的类别
35 voteIlabel = labels[sortedDistIndices[i]]
36 # dict.get(key,default=None),字典的get()方法,返回指定键的值,如果值不在字典中返回默认值。
37 # 计算类别次数
38 classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1
39 # python3中用items()替换python2中的iteritems()
40 # key=operator.itemgetter(1)根据字典的值进行排序
41 # key=operator.itemgetter(0)根据字典的键进行排序
42 # reverse降序排序字典
43 sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
44 # 返回次数最多的类别,即所要分类的类别
45 return sortedClassCount[0][0]
46
47
48 """
49 函数说明:打开并解析文件,对数据进行分类:1代表不喜欢,2代表魅力一般,3代表极具魅力
50
51 Parameters:
52 filename - 文件名
53 Returns:
54 returnMat - 特征矩阵
55 classLabelVector - 分类Label向量
56
57 """
58
59
60 def file2matrix(filename):
61 # 打开文件
62 fr = open(filename)
63 # 读取文件所有内容
64 arrayOLines = fr.readlines()
65 # 得到文件行数
66 numberOfLines = len(arrayOLines)
67 # 返回的NumPy矩阵,解析完成的数据:numberOfLines行,3列
68 returnMat = np.zeros((numberOfLines, 3))
69 # 返回的分类标签向量
70 classLabelVector = []
71 # 行的索引值
72 index = 0
73 for line in arrayOLines:
74 # s.strip(rm),当rm空时,默认删除空白符(包括'
','
',' ',' ')
75 line = line.strip()
76 # 使用s.split(str="",num=string,cout(str))将字符串根据' '分隔符进行切片。
77 listFromLine = line.split(' ')
78 # 将数据前三列提取出来,存放到returnMat的NumPy矩阵中,也就是特征矩阵
79 returnMat[index, :] = listFromLine[0:3]
80 # 根据文本中标记的喜欢的程度进行分类,1代表不喜欢,2代表魅力一般,3代表极具魅力
81 if listFromLine[-1] == 'didntLike':
82 classLabelVector.append(1)
83 elif listFromLine[-1] == 'smallDoses':
84 classLabelVector.append(2)
85 elif listFromLine[-1] == 'largeDoses':
86 classLabelVector.append(3)
87 index += 1
88 return returnMat, classLabelVector
89
90
91 """
92 函数说明:对数据进行归一化
93
94 Parameters:
95 dataSet - 特征矩阵
96 Returns:
97 normDataSet - 归一化后的特征矩阵
98 ranges - 数据范围
99 minVals - 数据最小值
100 """
101
102
103 def autoNorm(dataSet):
104 # 获得数据的最小值
105 minVals = dataSet.min(0)
106 maxVals = dataSet.max(0)
107 # 最大值和最小值的范围
108 ranges = maxVals - minVals
109 # shape(dataSet)返回dataSet的矩阵行列数
110 normDataSet = np.zeros(np.shape(dataSet))
111 # 返回dataSet的行数
112 m = dataSet.shape[0]
113 # 原始值减去最小值
114 normDataSet = dataSet - np.tile(minVals, (m, 1))
115 # 除以最大和最小值的差,得到归一化数据
116 normDataSet = normDataSet / np.tile(ranges, (m, 1))
117 # 返回归一化数据结果,数据范围,最小值
118 return normDataSet, ranges, minVals
119
120
121 """
122 函数说明:分类器测试函数
123
124 Parameters:
125 无
126 Returns:
127 normDataSet - 归一化后的特征矩阵
128 ranges - 数据范围
129 minVals - 数据最小值
130
131 """
132
133
134 def datingClassTest():
135 # 打开的文件名
136 filename = "datingTestSet.txt"
137 # 将返回的特征矩阵和分类向量分别存储到datingDataMat和datingLabels中
138 datingDataMat, datingLabels = file2matrix(filename)
139 # 取所有数据的百分之十
140 hoRatio = 0.10
141 # 数据归一化,返回归一化后的矩阵,数据范围,数据最小值
142 normMat, ranges, minVals = autoNorm(datingDataMat)
143 # 获得normMat的行数
144 m = normMat.shape[0]
145 # 百分之十的测试数据的个数
146 numTestVecs = int(m * hoRatio)
147 # 分类错误计数
148 errorCount = 0.0
149
150 for i in range(numTestVecs):
151 # 前numTestVecs个数据作为测试集,后m-numTestVecs个数据作为训练集
152 classifierResult = classify0(normMat[i, :], normMat[numTestVecs:m, :],
153 datingLabels[numTestVecs:m], 4)
154 print("分类结果:%d 真实类别:%d" % (classifierResult, datingLabels[i]))
155 if classifierResult != datingLabels[i]:
156 errorCount += 1.0
157 print("错误率:%f%%" % (errorCount / float(numTestVecs) * 100))
158
159
160 """
161 函数说明:main函数
162
163 Parameters:
164 无
165 Returns:
166 无
167
168 """
169 if __name__ == '__main__':
170 datingClassTest()
六、使用算法:构建完整可用系统
我们可以给海伦一个小段程序,通过该程序海伦会在约会网站上找到某个人并输入他的信息。程序会给出她对男方喜欢程度的预测值。
1 # -*- coding: UTF-8 -*-
2
3 import numpy as np
4 import operator
5
6 """
7 函数说明:kNN算法,分类器
8
9 Parameters:
10 inX - 用于分类的数据(测试集)
11 dataSet - 用于训练的数据(训练集)
12 labes - 分类标签
13 k - kNN算法参数,选择距离最小的k个点
14 Returns:
15 sortedClassCount[0][0] - 分类结果
16 """
17
18
19 def classify0(inX, dataSet, labels, k):
20 # numpy函数shape[0]返回dataSet的行数
21 dataSetSize = dataSet.shape[0]
22 # 在列向量方向上重复inX共1次(横向),行向量方向上重复inX共dataSetSize次(纵向)
23 diffMat = np.tile(inX, (dataSetSize, 1)) - dataSet
24 # 二维特征相减后平方
25 sqDiffMat = diffMat ** 2
26 # sum()所有元素相加,sum(0)列相加,sum(1)行相加
27 sqDistances = sqDiffMat.sum(axis=1)
28 # 开方,计算出距离
29 distances = sqDistances ** 0.5
30 # 返回distances中元素从小到大排序后的索引值
31 sortedDistIndices = distances.argsort()
32 # 定一个记录类别次数的字典
33 classCount = {}
34 for i in range(k):
35 # 取出前k个元素的类别
36 voteIlabel = labels[sortedDistIndices[i]]
37 # dict.get(key,default=None),字典的get()方法,返回指定键的值,如果值不在字典中返回默认值。
38 # 计算类别次数
39 classCount[voteIlabel] = classCount.get(voteIlabel, 0) + 1
40 # python3中用items()替换python2中的iteritems()
41 # key=operator.itemgetter(1)根据字典的值进行排序
42 # key=operator.itemgetter(0)根据字典的键进行排序
43 # reverse降序排序字典
44 sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
45 # 返回次数最多的类别,即所要分类的类别
46 return sortedClassCount[0][0]
47
48
49 """
50 函数说明:打开并解析文件,对数据进行分类:1代表不喜欢,2代表魅力一般,3代表极具魅力
51
52 Parameters:
53 filename - 文件名
54 Returns:
55 returnMat - 特征矩阵
56 classLabelVector - 分类Label向量
57 """
58
59
60 def file2matrix(filename):
61 # 打开文件
62 fr = open(filename)
63 # 读取文件所有内容
64 arrayOLines = fr.readlines()
65 # 得到文件行数
66 numberOfLines = len(arrayOLines)
67 # 返回的NumPy矩阵,解析完成的数据:numberOfLines行,3列
68 returnMat = np.zeros((numberOfLines, 3))
69 # 返回的分类标签向量
70 classLabelVector = []
71 # 行的索引值
72 index = 0
73 for line in arrayOLines:
74 # s.strip(rm),当rm空时,默认删除空白符(包括'
','
',' ',' ')
75 line = line.strip()
76 # 使用s.split(str="",num=string,cout(str))将字符串根据' '分隔符进行切片。
77 listFromLine = line.split(' ')
78 # 将数据前三列提取出来,存放到returnMat的NumPy矩阵中,也就是特征矩阵
79 returnMat[index, :] = listFromLine[0:3]
80 # 根据文本中标记的喜欢的程度进行分类,1代表不喜欢,2代表魅力一般,3代表极具魅力
81 if listFromLine[-1] == 'didntLike':
82 classLabelVector.append(1)
83 elif listFromLine[-1] == 'smallDoses':
84 classLabelVector.append(2)
85 elif listFromLine[-1] == 'largeDoses':
86 classLabelVector.append(3)
87 index += 1
88 return returnMat, classLabelVector
89
90
91 """
92 函数说明:对数据进行归一化
93
94 Parameters:
95 dataSet - 特征矩阵
96 Returns:
97 normDataSet - 归一化后的特征矩阵
98 ranges - 数据范围
99 minVals - 数据最小值
100 """
101
102
103 def autoNorm(dataSet):
104 # 获得数据的最小值
105 minVals = dataSet.min(0)
106 maxVals = dataSet.max(0)
107 # 最大值和最小值的范围
108 ranges = maxVals - minVals
109 # shape(dataSet)返回dataSet的矩阵行列数
110 normDataSet = np.zeros(np.shape(dataSet))
111 # 返回dataSet的行数
112 m = dataSet.shape[0]
113 # 原始值减去最小值
114 normDataSet = dataSet - np.tile(minVals, (m, 1))
115 # 除以最大和最小值的差,得到归一化数据
116 normDataSet = normDataSet / np.tile(ranges, (m, 1))
117 # 返回归一化数据结果,数据范围,最小值
118 return normDataSet, ranges, minVals
119
120
121 """
122 函数说明:通过输入一个人的三维特征,进行分类输出
123
124 Parameters:
125 无
126 Returns:
127 无
128 """
129
130
131 def classifyPerson():
132 # 输出结果
133 resultList = ['讨厌', '有些喜欢', '非常喜欢']
134 # 三维特征用户输入
135 precentTats = float(input("玩视频游戏所耗时间百分比:"))
136 ffMiles = float(input("每年获得的飞行常客里程数:"))
137 iceCream = float(input("每周消费的冰激淋公升数:"))
138 # 打开的文件名
139 filename = "datingTestSet.txt"
140 # 打开并处理数据
141 datingDataMat, datingLabels = file2matrix(filename)
142 # 训练集归一化
143 normMat, ranges, minVals = autoNorm(datingDataMat)
144 # 生成NumPy数组,测试集
145 inArr = np.array([precentTats, ffMiles, iceCream])
146 # 测试集归一化
147 norminArr = (inArr - minVals) / ranges
148 # 返回分类结果
149 classifierResult = classify0(norminArr, normMat, datingLabels, 3)
150 # 打印结果
151 print("你可能%s这个人" % (resultList[classifierResult - 1]))
152
153
154 """
155 函数说明:main函数
156
157 Parameters:
158 无
159 Returns:
160 无
161 """
162 if __name__ == '__main__':
163 classifyPerson()