完整代码及数据地址:https://github.com/cqulun123/Machine-Learning-in-Action
0 问题背景介绍
我的朋友海伦一直使用在线约会网站寻找适合自己的约会对象。尽管约会网站会推荐不同的人选,但她并不是喜欢每一个人。经过一番总结,她发现曾交往过三种类型的人:
不喜欢的人
魅力一般的人
极具魅力的人
尽管发现了上述规律,但海伦依然无法将约会网站推荐的匹配对象归入恰当的分类。她觉得可以在周一到周五约会那些魅力一般的人,而周末则更喜欢与那些极具魅力的人为伴。海伦希望我们的分类软件可以更好地帮助她将匹配对象划分到确切的分类中。此外海伦还收集了一些约会网站未曾记录的数据信息,她认为这些数据更有助于匹配对象的归类 。
1 在约会网站上使用k-近邻算法的步骤
(1) 收集数据:提供文本文件。(2) 准备数据:使用Python解析文本文件。
(3) 分析数据:使用Matplotlib画二维扩散图。
(4) 训练算法:此步骤不适用于k-近邻算法。
(5) 测试算法:使用海伦提供的部分数据作为测试样本。测试样本和非测试样本的区别在于:测试样本是已经完成分类的数据,如果预测分类与实际类别不同,则标记为一个错误。
(6) 使用算法:产生简单的命令行程序,然后海伦可以输入一些特征数据以判断对方是否为自己喜欢的类型。
2 准备数据:从文本文件中解析数据
海伦收集约会数据已经有了一段时间,她把这些数据存放在文本文件 datingTestSet2.txt中,每个样本数据占据一行,总共有1000行。海伦的样本主要包含以下3种特征: 每年获得的飞行常客里程数
玩视频游戏所耗时间百分比
每周消费的冰淇淋公升数
在将上述特征数据输入到分类器之前,必须将待处理数据的格式改变为分类器可以接受的格式。 代码如下:
def file_to_matrix(filename):
"""
函数作用:从文件中读入训练数据,并存储为矩阵
:param filename:文件名字符串
:return:训练样本矩阵和类标签向量
"""
# 打开文件
fr = open(filename)
# 读取文件内容
array_lines = fr.readlines()
# 得到文件行数
number_of_lines = len(array_lines)
# 返回解析后的数据
return_mat = np.zeros((number_of_lines, 3))
# 定义类标签向量
class_label_vector = []
# 行索引值
index = 0
for line in array_lines:
# 去掉 回车符号
line = line.strip()
# 用\t分割每行数据
list_from_line = line.split('\t')
# 选取前3个元素,将它们存储到特征矩阵中
return_mat[index, :] = list_from_line[0:3]
# 把该样本对应的标签放至标签向量,顺序与样本集对应。
class_label_vector.append(int(list_from_line[-1]))
index += 1
return return_mat, class_label_vector
3 分析数据:使用 Matplotlib 创建散点图
首先我们使用Matplotlib制作原始数据的散点图 ,代码如下:
if __name__ == '__main__':
dating_data_mat, dating_labels = file_to_matrix('datingTestSet2.txt')
fig = plt.figure()
ax = fig.add_subplot(111)
ax.scatter(dating_data_mat[:, 1], dating_data_mat[:, 2])
plt.xlabel("玩游戏所耗时间百分比")
plt.ylabel("每周消费的冰淇淋公升数")
plt.show()
if __name__ == '__main__':
dating_data_mat, dating_labels = file_to_matrix('datingTestSet2.txt')
fig = plt.figure()
ax = fig.add_subplot(111)
ax.scatter(dating_data_mat[:, 1], dating_data_mat[:, 2], 15.0 * np.array(dating_labels),
15.0 * np.array(dating_labels))
plt.xlabel("玩游戏所耗时间百分比")
plt.ylabel("每周消费的冰淇淋公升数")
plt.show()
为了显示书中图2-5的效果,写的代码如下,效果图如图3所示:
if __name__ == '__main__':
dating_data_mat, dating_labels = file_to_matrix('datingTestSet2.txt')
fig = plt.figure()
ax = fig.add_subplot(111)
dating_labels = np.array(dating_labels)
idx_1 = np.where(dating_labels == 1)
p1 = ax.scatter(dating_data_mat[idx_1, 0], dating_data_mat[idx_1, 1], marker='o', color='r', label='不喜欢', s=30)
idx_2 = np.where(dating_labels == 2)
p2 = ax.scatter(dating_data_mat[idx_2, 0], dating_data_mat[idx_2, 1], marker='o', color='g', label='魅力一般', s=45)
idx_3 = np.where(dating_labels == 3)
p3 = ax.scatter(dating_data_mat[idx_3, 0], dating_data_mat[idx_3, 1], marker='o', color='b', label='极具魅力', s=60)
plt.legend(loc='upper left')
plt.xlabel("每年获取的飞行常客历程数")
plt.ylabel("玩游戏所耗时间百分比")
plt.show()
4 准备数据:归一化数值
在处理这种不同取值范围的特征值时,我们通常采用的方法是将数值归一化,如将取值范围处理为0到1或者-1到1之间。 这里,将取值范围处理为0到1,具体代码如下:
def auto_norm(data_set):
"""
该函数可以自动将数字特征值转化为0到1的区间,即归一化训练数据
"""
# 获取数据集中每列的最小数值
min_vals = data_set.min(0)
# 获取数据集中每列的最大数值
max_vals = data_set.max(0)
# 最大值与最小的差值
ranges = max_vals - min_vals
# 创建一个全0矩阵,用于存放归一化后的数据
norm_data_set = np.zeros(np.shape(data_set))
# 返回data_set的行数
m = data_set.shape[0]
# 原始数据值减去最小值
norm_data_set = data_set - np.tile(min_vals, (m, 1))
# 除以最大和最小值的差值,得到归一化数据
norm_data_set = norm_data_set / np.tile(ranges, (m, 1))
# 返回归一化数据,最大值与最小的差值,每列的最小数值
return norm_data_set, ranges, min_vals
5 测试算法:作为完整程序验证分类器
上节我们已经将数据按照需求做了处理,本节我们将测试分类器的效果,如果分类器的正确率满足要求,海伦就可以使用这个软件来处理约会网站提供的约会名单了。 测试分类器效果 ,代码如下:
def dating_class_test():
# 将数据集中90%用于训练,10%的数据留作测试用
ho_ratio = 0.10
# 将返回的特征矩阵和分类向量分别存储到dating_data_mat和dating_labels中
dating_data_mat, dating_labels = file_to_matrix('datingTestSet2.txt')
# 返回归一化数据,最大值与最小的差值,每列的最小数值
norm_mat, ranges, min_vals = auto_norm(dating_data_mat)
# 获得norm_mat的行数
m = norm_mat.shape[0]
# 10%的测试数据的个数
num_test_vecs = int(m * ho_ratio)
# 分类错误计数
error_count = 0.0
for i in range(num_test_vecs):
# 前numTestVecs个数据作为测试集,后m-numTestVecs个数据作为训练集
classifier_result = classify0(norm_mat[i, :], norm_mat[num_test_vecs:m, :], dating_labels[num_test_vecs:m], 3)
print("the classifier came back with: %d, the real answer is: %d" % (classifier_result, dating_labels[i]))
if (classifier_result != dating_labels[i]): error_count += 1.0
# 错误率
print("the total error rate is: %f" % (error_count / float(num_test_vecs)))
执行分类器测试程序,我们将得到下面的输出结果:
if __name__ == '__main__':
dating_class_test()
6 使用算法:构建完整可用系统
上面我们已经在数据上对分类器进行了测试,现在终于可以使用这个分类器为海伦来对人们分类。我们会给海伦一小段程序,通过该程序海伦会在约会网站上找到某个人并输入他的信息。程序会给出她对对方喜欢程度的预测值,代码如下:
def classify_person():
# 类标签列表
result_list = ['not at all', 'in small doses', 'in large doses']
# 用户输入不同特征值
precent_tats = float(input("percentage of time spent playing video games?"))
ff_miles = float(input("frequent flier miles earned per year?"))
ice_cream = float(input("liters of ice cream consumedper year?"))
# 打开的文件名并处理数据
dating_data_mat, dating_labels = file_to_matrix("datingTestSet2.txt")
# 归一化训练集
norm_mat, ranges, min_vals = auto_norm(dating_data_mat)
# 创建测试集数组
in_arr = np.array([precent_tats, ff_miles, ice_cream])
# 归一化测试集
norm_in_arr = (in_arr - min_vals) / ranges
# 返回分类结果
classifier_result = classify0(norm_in_arr, norm_mat, dating_labels, 3)
# 输出结果
print("You will probably like this person: ", result_list[classifier_result - 1])
为了解程序的实际运行效果,输入如下命令:
if __name__ == '__main__':
classify_person()
完整代码:
# encoding: utf-8
"""
@author:max bay
@version: python 3.6
@time: 2018/5/5 22:04
"""
import numpy as np
import operator
def classify0(input_data, data_set, labels_set, k):
"""
函数作用:使用k-近邻算法将每组数据划分到某个类中
:param input_data:用于分类的输入数据(测试集)
:param data_set:输入的训练样本集
:param labels_set:训练样本标签
:param k:用于选择最近邻居的数目,即kNN算法参数,选择距离最小的k个点
:return:返回分类结果
"""
# data_set.shape[0]返回训练样本集的行数
data_set_size = data_set.shape[0]
# 在列方向上重复input_data,1次,行方向上重复input_data,data_set_size次
diff_mat = np.tile(input_data, (data_set_size, 1)) - data_set
# diff_mat:输入样本与每个训练样本的差值,然后对其每个x和y的差值进行平方运算
sq_diff_mat = diff_mat ** 2
# 按行进行累加,axis=1表示按行。
sq_distances = sq_diff_mat.sum(axis=1)
# 开方运算,求出距离
distances = sq_distances ** 0.5
# 返回distances中元素从小到大排序后的索引值
sorted_dist_indices = distances.argsort()
# 定一个字典:统计类别次数
class_count = {}
for i in range(k):
# 取出前k个元素的类别
vote_index_label = labels_set[sorted_dist_indices[i]]
# 统计类别次数
class_count[vote_index_label] = class_count.get(vote_index_label, 0) + 1
# 把分类结果进行降序排序,然后返回得票数最多的分类结果
sorted_class_count = sorted(class_count.items(), key=operator.itemgetter(1), reverse=True)
return sorted_class_count[0][0]
def file_to_matrix(filename):
"""
函数作用:从文件中读入训练数据,并存储为矩阵
:param filename:文件名字符串
:return:训练样本矩阵和类标签向量
"""
# 打开文件
fr = open(filename)
# 读取文件内容
array_lines = fr.readlines()
# 得到文件行数
number_of_lines = len(array_lines)
# 返回解析后的数据
return_mat = np.zeros((number_of_lines, 3))
# 定义类标签向量
class_label_vector = []
# 行索引值
index = 0
for line in array_lines:
# 去掉 回车符号
line = line.strip()
# 用\t分割每行数据
list_from_line = line.split('\t')
# 选取前3个元素,将它们存储到特征矩阵中
return_mat[index, :] = list_from_line[0:3]
# 把该样本对应的标签放至标签向量,顺序与样本集对应。
class_label_vector.append(int(list_from_line[-1]))
index += 1
return return_mat, class_label_vector
def auto_norm(data_set):
"""
该函数可以自动将数字特征值转化为0到1的区间,即归一化训练数据
"""
# 获取数据集中每列的最小数值
min_vals = data_set.min(0)
# 获取数据集中每列的最大数值
max_vals = data_set.max(0)
# 最大值与最小的差值
ranges = max_vals - min_vals
# 创建一个全0矩阵,用于存放归一化后的数据
norm_data_set = np.zeros(np.shape(data_set))
# 返回data_set的行数
m = data_set.shape[0]
# 原始数据值减去最小值
norm_data_set = data_set - np.tile(min_vals, (m, 1))
# 除以最大和最小值的差值,得到归一化数据
norm_data_set = norm_data_set / np.tile(ranges, (m, 1))
# 返回归一化数据,最大值与最小的差值,每列的最小数值
return norm_data_set, ranges, min_vals
def dating_class_test():
# 将数据集中90%用于训练,10%的数据留作测试用
ho_ratio = 0.10
# 将返回的特征矩阵和分类向量分别存储到dating_data_mat和dating_labels中
dating_data_mat, dating_labels = file_to_matrix('datingTestSet2.txt')
# 返回归一化数据,最大值与最小的差值,每列的最小数值
norm_mat, ranges, min_vals = auto_norm(dating_data_mat)
# 获得norm_mat的行数
m = norm_mat.shape[0]
# 10%的测试数据的个数
num_test_vecs = int(m * ho_ratio)
# 分类错误计数
error_count = 0.0
for i in range(num_test_vecs):
# 前numTestVecs个数据作为测试集,后m-numTestVecs个数据作为训练集
classifier_result = classify0(norm_mat[i, :], norm_mat[num_test_vecs:m, :], dating_labels[num_test_vecs:m], 3)
print("the classifier came back with: %d, the real answer is: %d" % (classifier_result, dating_labels[i]))
if (classifier_result != dating_labels[i]): error_count += 1.0
# 错误率
print("the total error rate is: %f" % (error_count / float(num_test_vecs)))
def classify_person():
# 类标签列表
result_list = ['not at all', 'in small doses', 'in large doses']
# 用户输入不同特征值
precent_tats = float(input("percentage of time spent playing video games?"))
ff_miles = float(input("frequent flier miles earned per year?"))
ice_cream = float(input("liters of ice cream consumedper year?"))
# 打开的文件名并处理数据
dating_data_mat, dating_labels = file_to_matrix("datingTestSet2.txt")
# 归一化训练集
norm_mat, ranges, min_vals = auto_norm(dating_data_mat)
# 创建测试集数组
in_arr = np.array([precent_tats, ff_miles, ice_cream])
# 归一化测试集
norm_in_arr = (in_arr - min_vals) / ranges
# 返回分类结果
classifier_result = classify0(norm_in_arr, norm_mat, dating_labels, 3)
# 输出结果
print("You will probably like this person: ", result_list[classifier_result - 1])
if __name__ == '__main__':
# dating_class_test()
classify_person()
7 参考资料
[1] 机器学习实战
[2] 机器学习实战笔记(Python实现)-02-k近邻算法(kNN)
[3] Python3《机器学习实战》学习笔记(一):k-近邻算法(史诗级干货长文)
[4] 浅谈Python数据可视化——使用Matplotlib创建散点图
后记:在画图过程中,matplotlib中文出现乱码,本文参考的《python matplotlib 中文显示参数设置 》一文,完美解决。