开坑前言:大一在读新生开启了自己的机器学习之旅,从接触到现在已经有快两个月了,现在回过头为夯实基础,从开始看起《机器学习实战》这本书,即使是最简单的KNN算法,详细看过作者用python实现的代码后还能感觉到不少收获。开这个坑希望能分享一些经验给到更多一同在学习这本书的朋友,共同进步。
若本文有漏洞或者希望交流的朋友可以和我私信,欢迎大家指正。
本系列笔记正式开始:
故事引入:
首先!有一只小牛在网上冲浪,它正在找伴侣,于是在一个约会网站上搜啊搜。
由于约会网站很是发达也有些缺点,这只小牛在这个网站上找到了1000个可叫来约会的对象,但是对于这些对象只知道三个信息,分别是身高,体重,颜值(满分为10分)。
于是小牛把这1000个对象的信息储存在一个表格里,小牛和这1000个对象逐个叫来约会……
过了N年,小牛和1000个对象一一约会完毕。每个对象小牛都给出了一个喜爱程度,1是不喜爱,2是一般般,3是特别喜爱。
约会完以后,约会网站又给小牛分配了1000个新对象,但是这个小牛不想再通过约会来知道这些对象是不是他喜爱的了,他想通过约会网站给出的下一个对象的三个信息,来预估自己喜不喜欢下一个对象(如果不喜欢或者一般就不去约会了 )。
小牛根据之前1000个老对象的三个信息,和约会得出的喜不喜欢,建立了一个三维坐标轴,这三维分别是身高 体重 颜值,通过这三个维度,点出了每个对象在三维坐标系中所在的位置。
而每个对象都有对应的喜欢程度,而喜欢程度都对应一个颜色,不喜欢的就是灰色,一般般的就是蓝色,特别喜欢的就是红色,小牛由此建立了一个装载了1000个老对象的三维坐标空间。
小牛把新对象一个个放进坐标系中进行比较,根据三个信息确定了他们在空间里的位置,计算了新对象和所有老对象在空间坐标系中的距离。
再找到离这个新对象最近的10个老对象,看看他们是什么颜色的,如果距离最近的老对象们大多是红色的,那么新对象一定也差不到哪去,小牛笃定这个新对象一定是值得约会的!如果最近的老对象们大多是黑色的,小牛笃定这个新对象肯定很差劲,于是就根本不打算和这个差劲的新对象约会。
是不是非常简单易懂呀,实际上就是对于训练集(老对象们)建立了一组三维空间坐标系,根据几何距离的关系来对测试样本(新对象)的类别做出判断。
而KNN的算法是在本故事的基础上将问题上升到了n维空间中,而故事里边寻找的距离新对象最近的老对象的数量,指代的就是KNN中的K,如果K=1,算法就叫最近邻,K=3,算法就叫3近邻。
这里将书本上算法实现流程码在下面仅供参考:
对未知类别属性的数据集中的每个点依次执行以下操作:
(1)计算已知类别数据集中的点与当前点之间的距离
(2)按照距离递增次序排序
(3)选取与当前点距离最小的k个点
(4)确定前K个点所在类别的出现频率
(5)返回前K个点出现频率最高的类别作为当前点的预测分类
下面给出《机器学习实战》中,对于datingTestSet2.txt这一数据集实现绘制数据分析散点图和使用KNN算法预判类别功能的代码,(运行于版本最新的Python3.9):
这个代码需要结合书本来看,也就是说该代码对于有书的同学帮助较大。它是在原代码的基础上进行了优化和整合,将我注释的代码和源代码一一对应和比较更容易看懂~
这里给出各个函数对应的功能方便查阅:
- file_matrix 读入数据集
- classify0 实现KNN算法
- vasual_map 绘制数据分析图
- auto_norm 对数据进行归一化
- dating_class_test 划分测试集和训练集,检测准确率
- create_dataset 无视即可
- img_vector 列向量化手写图片
- handwriting_recognize 手写字KNN识别模块
from numpy import *
import operator
import matplotlib.pyplot as plt
import os
def file_matrix(filename):
fr = open(filename)
# open()函数用于打开一个文件,创建一个file对象,相关的方法才可以调用它进行读写
array_in_line = fr.readlines()
# file.readlines([size]) :返回包含size行的列表, size 未指定则返回全部行
number_of_lines = len(array_in_line)
# Python len() 方法返回对象(字符、列表、元组等)长度或项目个数。
return_mat = zeros((number_of_lines, 3))
# return_mat为len行,一行三元素的由零组成的数组,视为初始化样本数组
class_label_vector = []
index = 0
for line in array_in_line:
line = line.strip()
# 去除该行的首尾空格
list_from_line = line.split('\t')
# \t为制表符,意为分割有空格分隔开的各元素
return_mat[index, :] = list_from_line[0:3]
# 注意,这里只取0 1 2个元素,不包括第三个元素,该数组即为读入的数据
class_label_vector.append(int(list_from_line[-1]))
# -1为最后一个元素,是每个样本的标签值
# 注:必须有int注明为整数变量,否则将读入字符串型
index += 1
return return_mat, class_label_vector
def create_dataset():
group = array([[1.0, 1.1], [1.0, 1.0], [0, 0], [0, 0.1]])
labels = ['A', 'A', 'B', 'B']
return group, labels
def classify0(in_x, dataset, labels, kling):
dataset_size = dataset.shape[0]
# 获取训练集的大小
diff_mat = tile(in_x, (dataset_size, 1)) - dataset
# 将样本in_x按照dataset的大小纵向复制,(行,列),减去dataset代表各维度相减
sq_diff_mat = diff_mat**2
# 样本与测试集的各维度之差做平方
sq_distances = sq_diff_mat.sum(axis=1)
# axis=1代表跨行,axis=0代表跨列,这里表示计算样本与训练样本的各维度距离平方和
distances = sq_distances**0.5
# 计算样本与所有训练样本的距离
sorted_dist_index = distances.argsort()
# 返回对所有距离从小到大排序的索引值
class_count = {}
for i in range(kling):
vote_label = labels[sorted_dist_index[i]]
class_count[vote_label] = class_count.get(vote_label, 0) + 1
# get(key, default=None)
# 参数
# key -- 字典中要查找的键。
# default -- 如果指定键的值不存在时,返回该默认值。
# 这里是选择距离最小的k个点
sorted_class_count = sorted(class_count.items(), key=operator.itemgetter(1), reverse=True)
# sorted是对是所有可迭代的对象进行排序操作,items(原iteritems)是将字典以列表的形式返回,itemgetter是返回对象第一个域的值,reverse=Ture是是数据
return sorted_class_count[0][0]
# 最后返回排序后的第一个点
def vasual_map(datamat, datalabels):
fig = plt.figure()
# 创建自定义图像
ax = fig.add_subplot(1, 2, 1)
ay = fig.add_subplot(1, 2, 2)
# add_subplot(x,y,z) 表示将图像划分为x行y列,此子图占据从左到右从上到下的第z个位置
ax.scatter(datamat[:, 1], datamat[:, 2], 15.0*array(datalabels), 15.0*array(datalabels))
ay.scatter(datamat[:, 0], datamat[:, 1], 15.0*array(datalabels), 15.0*array(datalabels))
# 使用数据矩阵的第二列第三列数据来构建ax子图像,并利用datalabels储存的不同标签,在散点图上绘制了色彩不等,尺寸不同的点
plt.show()
def auto_norm(data_set):
min_vals = data_set.min(0)
max_vals = data_set.max(0)
# min(0)返回该矩阵中每一列的最小值 min(1)返回该矩阵中每一行的最小值 最后返回的均是行向量的形式
ranges = max_vals - min_vals
norm_data_set = zeros(shape(data_set))
# 初始化norm_data_set作为最终返回的处理过后的数据集
m = data_set.shape[0]
# shape[0]表示矩阵的行数 shape[1]表示矩阵的列数
norm_data_set = data_set - tile(min_vals, (m, 1))
norm_data_set = norm_data_set/tile(ranges, (m, 1))
# tile(A,reps) 若reps为(a,b) 表示重构后的数组为min_vals的第一维上重复a次,在第二维上重复b次
return norm_data_set, ranges, min_vals
def dating_class_test():
test_ratio = 0.1
# 选取的测试集的比例
dating_data_mat, dating_labels = file_matrix('datingTestSet2.txt')
# 读取数据集
norm_mat, ranges, min_vals = auto_norm(dating_data_mat)
# 将数据集进行归一化
m = norm_mat.shape[0]
# 取值数据集的行数,即样本的总数
num_test_vecs = int(m*test_ratio)
# 选取的测试集总数
error_count = 0
# 这个变量用于计量分类器判断错误的数量
for i in range(num_test_vecs):
classify_result = classify0(norm_mat[i, :], norm_mat[num_test_vecs:m, :], dating_labels[num_test_vecs:m], 3)
if classify_result != dating_labels[i]:
error_count += 1.0
print("the classifier came back with: %d, the real answer is: %d" % (classify_result, dating_labels[i]))
print("the total error rate is: %f" % (error_count/float(num_test_vecs)))
# 输出比对结果(错误率)
def img_vector(filename):
final_vector = zeros((1, 1024))
# 初始化最终函数返回的矩阵
fr = open(filename)
# 打开文件
for i in range(32):
line_str = fr.readline()
# 读取文件的第i行。每次读取一行后会记录,在下一次使用该语句使读取上一次的下一行。
for j in range(32):
final_vector[0, 32*i+j] = int(line_str[j])
# 读取文件的第i行第j个字符
return final_vector
def handwriting_recognize():
hwlabels = []
# 测试集的标识
trainfilelist = os.listdir('trainingDigits')
# trainfilelist为trainingDigits文件夹下每个元素的列表
num_train = len(trainfilelist)
# 该列表的总元素的数目
training_mat = zeros((num_train, 1024))
for i in range(num_train):
filenamestr = trainfilelist[i]
filestr = filenamestr.split('.')[0]
# 读取文件的名字
fileclassnum = filestr.split('_')[0]
# 读取文件的标签
hwlabels.append(fileclassnum)
training_mat[i, :] = img_vector("trainingDigits/%s" % filenamestr)
# 获取样本向量化后的列向量
testfilelist = os.listdir('testDigits')
error_count = 0.0
num_test = len(testfilelist)
for i in range(num_test):
filenamestr = testfilelist[i]
filestr = filenamestr.split('.')[0]
test_label = filestr.split('_')[0]
test_vector = img_vector("testDigits/%s" % filenamestr)
classify_result = classify0(test_vector, training_mat, hwlabels, 3)
if classify_result != test_label:
error_count += 1
print("the handwriting recognize total error rate is: %f" % (error_count/float(num_test)))
dating_class_test()
handwriting_recognize()
附上书上用代码绘制出的两个散点图: