k近邻法是一种基本的分类和回归方法,是最简单的监督学习算法之一,其本质极其直观:在给定的训练数据集里找距离新的输入实例最近的k个实例,将该k个实例中的多数标签赋给输入实例即可。问题的关键在于三个方面:
样本空间中两个实例点的距离显然是其相似程度的反映,常见模型一般是n维实数向量空间
,多数使用欧式距离,及
distance 中的 p = 2。所以算法程序中此处可设置默认参数;
k的选择对结果影响重大:
一般选用一个比较小的数值,后面根据我们的实际数据作交叉验证。
往往是多数表决,即直接取k个中最多的标签即可。其实应当还有半数表决等更强要求的方法,数据和精力有限暂未谈论。
这三点一说其实理论准备就已经说完了,只是这样的算法每次要计算训练集所有实例到输入的距离,效率过低,这个问题将在下一篇【机器学习算法笔记】02——kd树中聊一聊,这篇基本还是以简单k近邻法实现快速分类为主。以下是主题代码。
1、数据集准备
根据书上内容准备了三个数据集
# 函数1 创建题设数据集
def create_movie_data_set():
group_out = np.array([[1, 101], [5, 89], [108, 5], [115, 8]])
labels_out = np.array(['爱情片', '爱情片', '动作片', '动作片'])
return group_out, labels_out
# 函数2 读取鸾尾花数做实验(弃)
def create_iris_data_set(test_proportion=0.2):
from sklearn import datasets
from sklearn.model_selection import train_test_split
iris_all = datasets.load_iris()
iris_train, iris_test, iris_label_train, iris_label_test
= train_test_split(iris_all.data, iris_all.target,test_size=test_proportion)
return iris_train, iris_test, iris_label_train, iris_label_test
# 函数3 读取书上约会数据实验
def create_dating_data_set(test_proportion=0.2):
from sklearn.model_selection import train_test_split
# 原始数据归一化处理
dating_data = normalization1(np.loadtxt("datingTestSet.txt", delimiter='t', usecols=(0, 1, 2)))
dating_target = np.loadtxt("datingTestSet.txt", delimiter='t', dtype = 'str',usecols=3)
dating_train, dating_test, dating_label_train, dating_label_test
= train_test_split(dating_data, dating_target,test_size=test_proportion)
return dating_train, dating_test, dating_label_train, dating_label_test
sklearn中有
2、kNN算法主体
《机器学习实战》书中内容及目前CSDN中多个高浏览的kNN代码都过于陈旧,明明使用了numpy模块,有好用的boardcast规则不用,偏偏要把一维数组扩到n维占内存毫无意义。
# 函数4 简单k邻近算法
def kNN1(x_in, data_train_in, label_train_in, k, n=2):
"""
:param x_in: 待测试的数据点
:param data_train_in: 训练集
:param label_train_in: 训练集标签
:param k: k近邻法参数
:param n: 距离度量方法选择,默认n=2即计算欧氏距离
:return label_result:测试点的标签
"""
import numpy as np
# 计算各点到目标点的距离,默认n=2即欧氏距离
distance = (((data_train_in - x_in) ** n).sum(axis=1)) ** (1 / n)
distance_sort = distance.argsort() # 求取distance中从小到大的索引值
label_dic = {}
for i in range(k):
# 读取距离最近的k个点的标签
label_vote = label_train_in[distance_sort[i]]
# 将该k个点的标签写入字典
label_dic[label_vote] = label_dic.get(label_vote, 0) + 1
# 读取最近k个点的标签值入字典后,直接求取字典中值最大的健即可
label_result = max(label_dic.items(), key=lambda x: x[1])[0] # 快速求取字典最值
return label_result
对于上述算法中的三个关键点:(1)距离度量默认欧式距离,可根据实际情况更改参数n;(2)k值需调用时输入;(3)分类决策规则为简单的多数表决。
1.0版本的算法应当说还是非常粗糙的,没有容错机制,但通过自己尝试修改能深刻理解算法。
3、归一化函数
算法函数一写完就急不可耐地测试了,结果发现效果很差,才想起来对于各维度数值差异较大的数据一定是要归一化的。
# 函数5 简单直接归一化算法
def normalization1(array_in):
array_in_max = array_in.max(0)
array_in_min = array_in.min(0)
array_out = (array_in-array_in_min)/(array_in_max - array_in_min)
return array_out
这样单独写有个问题就在于对于新数据,每次调用kNN1()前都得先归一化,非常麻烦,暂时搁置,后期更新kNN2时再将normalization直接内置进kNN中。
4、算法效果验证
选取不同的k值,每个k值抽取200次iris数据做验证,
使用经典的iris数据,按照8:2的比例将15个原始数据分为训练集和测试集作初步验证。
k值取1~30,每个k值做200次验证取平均错误率,实现代码如下:
if __name__ == '__main__':
"""直接验证尝试"""
test_result = np.array([])
for k_test in range(1,31):
error_rate = np.array([])
for n in range(200):
data_train, data_test, label_train, label_test = create_iris_data_set(0.2)
len_test = float(len(data_test))
result_error = 0
for i in range(len(data_test)):
label_try = kNN1(data_test[i], data_train, label_train, k_test)
# 检查统计每个输入实例分类结果是否与实际标签一致
if label_try != label_test[i]:
result_error += 1
# 统计每批的错误率
error_rate = np.append(error_rate, float(result_error) / len_test)
test_result = np.append(test_result,np.mean(error_rate))
print(test_result)
# 画图看一看
plt.title("iris_data_kNN_test")
plt.xlabel("k")
plt.ylabel("error_rate")
plt.plot(np.arange(1,31), test_result)
plt.show()
结果如下:
[ 4.65% 4.40% 3.77% 3.80% 3.72% 4.18% 3.72% 3.67% 3.73% 3.30%
3.55% 3.28% 2.58% 3.23% 3.40% 3.58% 3.32% 3.25% 3.62% 3.67%
4.95% 4.30% 4.52% 4.55% 5.22% 5.05% 5.48% 5.27% 5.55% 4.77%]
即当k取13时,分类误差达到最小,约2.58%。
5、收获
- 严格来讲k近邻法并没有弄完,kd树能有效提升计算效率我还没弄好,还得加紧;
- 目前主程序了调用同一数据源的create函数需要多次读取硬盘很没有效率,这是我在前期构建数据时没有提前考虑到的,以后在构建数据来源接口时务必考虑后面要怎么用,根据功能需求调整接口;
- 自己码代码和看别人的成品过程收获是截然不同的,网上可参考资料虽然多,但没有自己真的做过看再多也没用;
- 要善于利用python各类模块的便捷,比如iris数据,我本来是下载了txt放电脑上准备硬盘读取的,但实际著名的sklearn模块都自带了这些经典数据集,如breast_cancer等等,完全不需要自己来回折腾;另一个分割数据集的时候我是先自己用sample功能写了个小函数来拆分原始数据,成功实现后觉得太过麻烦还是换成了sklearn的相应功能,这些小细节有精力了当然自己做更好,但已经有成熟的解决方案时就没有必要非得用自己的。
- numpy操作还是不熟练啊,格式化打印个结果弄了半天。文档看的再细帮助也有限,还是得大量练习,长期浸润其中,Practice make perfect,还是不能着急,毕竟用的时间太短,数组在我手上还开不了花。得想想办法怎么把平常用到的numpy小技巧积累起来。
- 效率得提高,写这么一篇没多少干货的文字都花了不少功夫,得思考思考如何在学习和编程的同时就把思路和收获记下来,否则每周时间这么紧,如果做算法笔记成了累赘那就得不偿失了。
- 这周末要去跑西马,基本上一天半就交代出去了,还有些计划任务都没完成,下周估计得加把劲儿了,希望不要鸽。