引言
在学习KNN算法的时候,对其算法实现的具体细节非常感兴趣,经过查询和学习后,对KNN算法的原理进行了简单的还原,以便学习以及回顾时的理解。
在阅读之前,具备机器学习最基本的知识,对KNN有一定的了解。本文章会对各方面进行相对基础和详细的讲述。
KNN算法步骤
在实现KNN算法之前,让我们来回顾一下算法的具体步骤。
K- nn的工作原理可以通过以下算法来解释:
步骤1:选择K个邻居
步骤2:计算K个邻居的欧几里得距离
步骤3:根据计算出的欧几里得距离取最近的K个邻居
步骤4:在这K个邻居中,统计每个类别的数据点数量
步骤5:将新数据点分配给邻居数量最大的类别
步骤6:模型准备完毕
准备工作
我们知道,在KNN的分类过程中,计算的是离散点之间的距离,因此向量的各种计算是必不可少的,我们首先来将向量的各种运算进行函数定义。
由于这部分代码相对简单,这里便不再进行过多的笔墨阐述。
def vector_add(v, w):
"""
将两个向量按分量相加
输入:v, w是两个长度相同的数字向量
输出:包含v和w中相应元素相加的向量
"""
return [v_i + w_i for v_i, w_i in zip(v,w)]
def vector_subtract(v, w):
"""
分别减去两个向量
输入:v, w是两个长度相同的数字向量
输出:包含v和w中相应元素的相减的向量
"""
return [v_i - w_i for v_i, w_i in zip(v,w)]
def vector_sum(vectors):
"""
以组件方式添加向量列表
输入:vectors是一个相同长度的向量列表
输出:包含向量中相应元素相加的向量
"""
sum = vectors[0]
for i in range(1, len(vectors)):
sum = vector_add(sum, vectors[i])
return sum
def scalar_multiply(c, v):
"""
用一个向量乘以一个标量
输入:
C:标量
V:一个数字向量
输出:包含v的元素乘以c的值的向量
"""
return [c * v_i for v_i in v]
def dot(v, w):
"""
计算两个向量的点积
输入:v和w是两个长度相同的数字向量
输出:v与w的点积,定义为
v_1 * w_1 +…+ v_n * w_n
"""
return sum(v_i * w_i for v_i, w_i in zip(v, w))
def sum_of_squares(v):
"""
计算向量中各元素的平方和
输入:v是一个数字向量
输出:v中所有元素的平方和,定义为
v_1 * v_1 +…+ v_n * v_n
"""
return dot(v, v)
import math
def magnitude(v):
"""
计算向量的大小、长度或欧氏范数
输入:一个数字向量
输出:矢量的大小
"""
return math.sqrt(sum_of_squares(v))
def squared_distance(v, w):
"""
计算两个向量之间距离的平方
输入:v和w是两个长度相同的数字向量
输出:v和w之间距离的平方
"""
return sum_of_squares(vector_subtract(v, w))
def distance(v, w):
"""
计算两个向量之间的欧氏距离
输入:v和w是两个长度相同的数字向量
输出:v和w之间的欧氏距离
"""
return math.sqrt(squared_distance(v, w))
def mean(v):
"""
计算一个数字向量的均值
输入:v是一个数字向量
输出: v的平均值
"""
return sum(v) / len(v)
算法实现
majority_vote_weighted函数
在使用这个函数时,输入为已经按照由小到大排列的每个点与目标点之间距离数值的列表以及排序后相对应的标签。如:a,b,c三个点和目标点的距离分别为1,3,2,标签类型分别为优,劣,优。则相对应的labels为[优,优,劣],distances为[1, 2, 3]。
函数的返回值为判定后的目标点标签类型。(目标点即要预估的点)。
整个函数的代码如下(为了便于阅读与代码的搬运使用,注解写在代码注释中):
from collections import Counter, defaultdict
import math, random
def majority_vote_weighted(labels, distances):
# 赋予每个点的距离相应的权重,以此点到目标点的距离的倒数作为权重
weights = [1/x for x in distances]
# 创建空字典用于存储各个标签类型的总权重
label_weight = defaultdict(float)
# 计算每个标签的总权重
for i, l in enumerate(labels):
label_weight[l] = label_weight[l] + weights[i]
# 统计各个种类标签的点的个数
counts = Counter(labels)
# 计算每个标签的平均权重作为最终比较判断的指标
for l in label_weight:
label_weight[l] = label_weight[l] / counts[l]
# 找出平均权重值最大的标签类型的值
max_weight = max(label_weight.values())
# 将所有最大值对应的标签添入列表中
candidates = []
for l in label_weight:
if label_weight[l] == max_weight:
candidates.append(l)
# 如果只有一个合适的标签,直接预测为此标签。若不是,则随机预测为一个标签
if len(candidates) == 1:
return candidates[0]
else:
return random.choice(candidates)
knn_classify函数
使用majority_vote_weighted()通过KNN算法对new_point进行分类
输入中K是用于分类的邻居数,Labeled_points是一组训练实例,每个实例应该是一对(点,标签)的格式,New_point是分类下的新实例;一个点就是一串数字
输出Label为new_point的分类标签。
def knn_classify(k, labeled_points, new_point):
"""
使用majority_vote_weighted()通过KNN算法对new_point进行分类
输入:
K:用于分类的邻居数
Labeled_points:一组训练实例,每个实例应该是一对(点,标签)
New_point:分类下的新实例;一个点就是一串数字
输出:
Label: new_point的分类标签
"""
labels = []
distances = []
# 根据与目标点的距离对元素进行排序
by_distances = sorted(labeled_points,
key=lambda point_label: distance(point_label[0], new_point))
# 将标签和点数据按排序顺序存储在两个列表中
for i in by_distances:
d = distance(i[0], new_point)
if d:
distances.append(d)
labels.append(i[1])
# 选出最优预测标签
return majority_vote_weighted(labels[:k], distances[:k])
至此简易的KNN算法还原完成。
模型的应用与评估
数据准备
此处使用鸢尾花部分数据集。
准备knn_classify函数中的labeled_points参数和new_point参数的集合new_points列表。
import pandas as pd
iris_df = pd.read_csv("Iris.csv")
X = iris_df.iloc[:, 1:-1]
y = iris_df.Species
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2, random_state = 42)
labeled_points = []
for i in range(X_train.shape[0]):
point = X_train.iloc[i]
label = y_train.iloc[i]
labeled_points.append((list(point), label))
new_points = []
for i in range(X_test.shape[0]):
new_points.append(list(X_test.iloc[i]))
模型评估
此处使用回归率,精确率和F1分数进行测评。
def evaluation(k):
print("When K is {}".format(k))
y_pred = []
for new_point in new_points:
lab = knn_classify(k, labeled_points, new_point)
y_pred.append(lab)
y_pred_series = pd.Series(y_pred)
y_test.index = range(y_test.shape[0])
print("The classification accuracy: ", sum(y_test == y_pred_series)/len(y_test))
print()
tp = sum((y_test == 'Iris-versicolor') & (y_pred_series == 'Iris-versicolor'))
fn = sum((y_test == 'Iris-versicolor') & (y_pred_series != 'Iris-versicolor'))
fp= sum((y_test != 'Iris-versicolor') & (y_pred_series == 'Iris-versicolor'))
tn = sum((y_test != 'Iris-versicolor') & (y_pred_series != 'Iris-versicolor'))
print("recall for class 'Iris-versicolor' = " + str(tp / (tp + fn)))
print("precision for class 'Iris-versicolor' = " + str(tp / (tp + fp)))
print("F1 Score for class 'Iris-versicolor' = " + str(2 * tp /(2 * tp + fp + fn)))
print()
tp = sum((y_test == 'Iris-setosa') & (y_pred_series == 'Iris-setosa'))
fn = sum((y_test == 'Iris-setosa') & (y_pred_series != 'Iris-setosa'))
fp= sum((y_test != 'Iris-setosa') & (y_pred_series == 'Iris-setosa'))
tn = sum((y_test != 'Iris-setosa') & (y_pred_series != 'Iris-setosa'))
print("recall for class 'Iris-setosa' = " + str(tp / (tp + fn)))
print("precision for class 'Iris-setosa' = " + str(tp / (tp + fp)))
print("F1 Score for class 'Iris-setosa' = " + str(2 * tp /(2 * tp + fp + fn)))
print()
tp = sum((y_test == 'Iris-virginica') & (y_pred_series == 'Iris-virginica'))
fn = sum((y_test == 'Iris-virginica') & (y_pred_series != 'Iris-virginica'))
fp= sum((y_test != 'Iris-virginica') & (y_pred_series == 'Iris-virginica'))
tn = sum((y_test != 'Iris-virginica') & (y_pred_series != 'Iris-virginica'))
print("recall for class 'Iris-virginica' = " + str(tp / (tp + fn)))
print("precision for class 'Iris-virginica' = " + str(tp / (tp + fp)))
print("F1 Score for class 'Iris-virginica' = " + str(2 * tp /(2 * tp + fp + fn)))
print("\n\n\n")
for k in range(5, 21):
evaluation(k)
评估结果及分析
结果
由于预测的是5到20的K值,文本体量太大,这里摘取关键部分进行展示。
When K is 8 The classification accuracy: 1.0 recall for class 'Iris-versicolor' = 1.0 precision for class 'Iris-versicolor' = 1.0 F1 Score for class 'Iris-versicolor' = 1.0 recall for class 'Iris-setosa' = 1.0 precision for class 'Iris-setosa' = 1.0 F1 Score for class 'Iris-setosa' = 1.0 recall for class 'Iris-virginica' = 1.0 precision for class 'Iris-virginica' = 1.0 F1 Score for class 'Iris-virginica' = 1.0 When K is 9 The classification accuracy: 0.9666666666666667 recall for class 'Iris-versicolor' = 1.0 precision for class 'Iris-versicolor' = 0.9 F1 Score for class 'Iris-versicolor' = 0.9473684210526315 recall for class 'Iris-setosa' = 1.0 precision for class 'Iris-setosa' = 1.0 F1 Score for class 'Iris-setosa' = 1.0 recall for class 'Iris-virginica' = 0.9090909090909091 precision for class 'Iris-virginica' = 1.0 F1 Score for class 'Iris-virginica' = 0.9523809523809523 When K is 10 The classification accuracy: 0.9666666666666667 recall for class 'Iris-versicolor' = 1.0 precision for class 'Iris-versicolor' = 0.9 F1 Score for class 'Iris-versicolor' = 0.9473684210526315 recall for class 'Iris-setosa' = 1.0 precision for class 'Iris-setosa' = 1.0 F1 Score for class 'Iris-setosa' = 1.0 recall for class 'Iris-virginica' = 0.9090909090909091 precision for class 'Iris-virginica' = 1.0 F1 Score for class 'Iris-virginica' = 0.9523809523809523
分析
在K=8之前,模型各方面的表现都很好,但在K=8之后,所有指标逐渐萎缩。
K=8之后指标逐渐收窄,可能是因为在某一点之后,增加邻居数量并不能使模型泛化得更好,反而可能引入一些噪声。
然而,较小的k可能会使模型对噪声和局部波动更敏感,从而在训练数据上表现良好,但在以前未见过的数据上可能会出现欠拟合。
因此,在妥协中,K=8是更好的选择。
结语
本文所呈现的只是一个简单易懂的最基本意识流KNN算法,旨在将其中算法基本机制讲述清楚,便于对KNN算法的进一步理解。其中诸多超参数,不同形式数据的处理转换等均未涉及,感兴趣的读者可自行实现,也欢迎各位在评论区进行讨论。如有犯错之处,感谢指出,定第一时间进行改正。