一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情。
一、KNN算法设计
- 读入训练数据
- 从训练数据中分割出测试数据
- 可视化训练数据集
- 使用训练数据训练KNN
- 使用测试数据对KNN进行评估
- 对不同的变量对KNN的性能影响进行探讨
实验一
在西瓜数据集上使用KNN分类
对西瓜数据集进行简单的分析,结果如下:
特征 | 数据特征 | 角色 |
---|---|---|
编号 | 离散 | 编号 |
密度 | 连续 | 特征 |
含糖率 | 连续 | 特征 |
好瓜 | 离散 | 标签 |
因此,我选择密度
和含糖率
作为特征,通过KNN模型预测好瓜
标签。
实验二
在Wine数据集上使用KNN分类
这些数据是对意大利同一地区种植的葡萄酒进行化学分析的结果,这些葡萄酒来自三个不同的品种。该分析确定了三种葡萄酒中每种葡萄酒中含有的13种成分的数量。我们需要通过这些数据对葡萄酒的类别进行分类。
数据集属性概况:
RangeIndex: 178 entries, 0 to 177
Data columns (total 14 columns):
0 Alcohol 178 non-null float64
1 Malic acid 178 non-null float64
2 Ash 178 non-null float64
3 Alcalinity of ash 178 non-null float64
4 Magnesium 178 non-null int64
5 Total phenols 178 non-null float64
6 Flavanoid 178 non-null float64
7 Nonflavanoid phenols 178 non-null float64
8 Proanthocyanins 178 non-null float64
9 Color intensity 178 non-null float64
10 Hue 178 non-null float64
11 OD280/OD315 of diluted wines 178 non-null float64
12 Proline 178 non-null int64
13 category 178 non-null int64
dtypes: float64(11), int64(3)
memory usage: 19.5 KB
复制代码
该数据集包含13个特征,没有空缺数据,全部特征都是连续,且总共有3个类别。
二、KNN模型核心代码
-
导入所需库
import pandas as pd import numpy as np import matplotlib.pyplot as plt 复制代码
在本次实验中,我选择pandas1作为读取数据集的主要工具,选择numpy2加速主要的数学运算,选择matplotlib3进行数据可视化分析。
-
定义KNN分类器 KNNClassifier
-
定义
__init__()
以初始化分类器class KNNClassifier: def __init__(self, X: pd.DataFrame, Y: pd.DataFrame, k: int): # X : batches * features self.X = X self.Y = Y self.k = k ... 复制代码
其中
X
代表训练特征,Y
代表训练标签,k
代表判断时取最近的k
个点进行决策。 -
定义
visualization()
方法以可视化数据集class KNNClassifier: ... def visualization(self) -> None: self.X.plot.scatter(x=0, y=1, c=self.Y, colormap='viridis') plt.show() ... 复制代码
此处使用
pandas.Dataframe
中封装的scatter
方法,指定绘图坐标分别为两个特征值,点的颜色以数据点标签决定,由此对数据集进行可视化分析。扫描二维码关注公众号,回复: 13769245 查看本文章例如,在西瓜数据集上的可视化效果如下(已划分10%作为测试集,此处是训练集数据):
针对高维特征,只能选取前两个维度进行可视化。
-
定义
predict()
核心方法以对目标数据进行预测class KNNClassifier: ... def predict(self, X, mode="euclidean"): # 步骤 1:将新数据的每个特征与样本集中的数据对应的特征进行比较 all_distances = np.apply_along_axis(self.distance, 1, X, mode, self.X) # 步骤 2:提取样本集中特征最相似的 K 个数据的分类标签 k_smallest = all_distances.argpartition(self.k, axis=1)[:, :self.k] # 步骤 3:统计这 K 个标签中出现次数最多的类别,作为新数据的类别 labels = np.apply_along_axis(self.select_by_index, 1, k_smallest, self.Y) result = np.apply_along_axis(self.find_most_common, 1, labels) return result @staticmethod def select_by_index(index: np.ndarray, x: pd.DataFrame): return x.values[index] @staticmethod def find_most_common(x): # x: 1 dim array return np.bincount(x).argmax() @staticmethod def distance(target, mode, train): ... 复制代码
在这段代码中,主体函数是
predict()
,此外,还有三个辅助函数,分别是select_by_index()
,find_most_common()
和distance()
。此处我进行了多处优化,具体创新点如下:
-
避免使用
for-loop
以加快运行速度为什么在这里需要定义三个函数来完成预测过程呢?众所周知,在
python
中执行代码的效率是相当低下的,这个特点在for
循环上体现的尤为明显4。因此,很多python
库都通过pyi
内置了C语言实现5。Numpy2也使用C语言对运行效率进行优化,因此,调用Numpy2的接口比在python
中使用for-loop
的运行效率要高出不少。==在这段代码中,我没有使用任何for-loop==,核心代码全部使用Numpy2自带向量化接口实现,因此运行效率比python-oriented-code
要高出不少。这也是这段代码中包含两个辅助函数的原因。 -
使用
argpartition
而不是argsort
为什么选择
argpartition
呢?主要是内部实现时时间复杂度上的区别6。对于argpartition
,其复杂度在最坏情况下也只是 ,而argsort
的时间复杂度为
-
-
定义
distance()
方法以灵活选择距离函数并支持向量化class KNNClassifier: ... @staticmethod def distance(target, mode, train): # train batches * features # target 1 * features # In function: All 1 batch def euclidean(x, y): # 1 # return np.sqrt(np.dot(x, x) - 2 * np.dot(x, y) + np.dot(y, y)) def ManHaDun(x, y): return np.abs(x - y).sum() def Cosine(a, b): if a.shape != b.shape: raise RuntimeError("array {} shape not match {}".format(a.shape, b.shape)) if a.ndim == 1: a_norm = np.linalg.norm(a) b_norm = np.linalg.norm(b) elif a.ndim == 2: a_norm = np.linalg.norm(a, axis=1, keepdims=True) b_norm = np.linalg.norm(b, axis=1, keepdims=True) else: raise RuntimeError("array dimensions {} not right".format(a.ndim)) similiarity = np.dot(a, b.T) / (a_norm * b_norm) dist = 1. - similiarity return dist func = { "euclidean": euclidean, "ManHaDun": ManHaDun, "Cosine": Cosine, }[mode] return np.apply_along_axis(func, 1, train, target) 复制代码
在这段代码中,我实现了三个距离函数,分别是
距离函数 对应实现 欧氏距离 euclidean()
曼哈顿距离 ManHaDun()
余弦距离 Cosine()
此处我进行了多处优化,具体优化点如下:
-
复用
euclidean(x, y)
计算结果欧氏距离一般计算公式为:
但是我在此处使用的公式为其展开形式
此公式中红色部分在计算欧氏距离时会多次使用,因此,使用此公式可以充分利用numpy的缓存机制,减少不必要的重复运算量。
-
借用字典映射,实现灵活调整不同距离函数
因为要使用numpy接口调用此函数完成距离运算操作,如果分别定义三个距离函数并分别调用,将会在多处引入三个if-else语句以判断具体使用哪个距离函数,此处使用一个字典,巧妙地统一了距离函数的调用方式而又不失灵活性。
-
-
三、实验数据及结果分析
-
在西瓜数据集上使用KNN
-
导入所需库
import pandas as pd from Model import KNNClassifier from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report 复制代码
此处导入刚刚编写的
KNNClassifier
,sklearn
中的训练集分割工具以及分类报告函数进行准确率计算。 -
读取数据集并分割训练集和测试集
df = pd.read_csv("kmeansdata.csv") names = ["Bad", "Good"] X_train, X_test, y_train, y_test = train_test_split(df[["m", "h"]], df["v"], test_size=0.1, random_state=1) 复制代码
此处读入西瓜数据集,并选定特征
m
和h
-
KNN 模型训练,可视化,预测与准确率计算
knn = KNNClassifier(X_train, y_train, k=3) knn.visualization() predict = knn.predict(X_test) print("Predict Result on WaterMelon Dataset:") print(classification_report(y_test, predict, target_names=names)) 复制代码
-
可视化结果
-
准确率报告
Predict Result on WaterMelon Dataset: precision recall f1-score support Bad 1.00 1.00 1.00 1 Good 1.00 1.00 1.00 1 accuracy 1.00 2 macro avg 1.00 1.00 1.00 2 weighted avg 1.00 1.00 1.00 2 复制代码
轻松达到100%准确率!
-
-
-
在Wines数据集上使用KNN
-
导入所需库
import pandas as pd from Model import KNNClassifier from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report from tqdm import tqdm 复制代码
在这次实验中,因为数据集较大,所以引入tqdm7加入进度条可视化程序执行进度。
-
读取数据集并分割训练集与测试集
df = pd.read_csv("wine.data") names = [ "Class1", "Class2", "Class3", ] X_train, X_test, y_train, y_test = train_test_split(df.iloc[:, 1:], df.iloc[:, 0], test_size=0.1, random_state=1) knn = KNNClassifier(X_train, y_train, k=3) 复制代码
根据数据集的特征以10%的比率分为测试集和训练集。
-
对数据集进行可视化
knn.visualization() 复制代码
从图中可以看出,此数据集比西瓜数据集复杂,各类别交错,且分类平面不明显,存在很多离散点。因此,可以估计到KNN的分类准确度不会很高。
-
针对
k
值和不同距离函数对准确率的影响进行可视化分析k = list(range(1, 20, 2)) for mode in ["euclidean", "ManHaDun", "Cosine"]: acc = [] for i in tqdm(k, desc=f"Computing k varies using mode = {mode}"): knn.set_k(i) predict = knn.predict(X_test, mode=mode) accuracy = classification_report(y_test, predict, target_names=names, output_dict=True)["accuracy"] acc.append(accuracy) plt.plot(k, acc, label=mode) plt.legend() plt.show() 复制代码
此处考量k值从1到150,距离函数分别选用欧氏距离,曼哈顿距离,以及余弦距离,并把结果以折线图的形式绘制在图片上,实验输出如下:
Computing k varies using mode = euclidean: 100%|██████████| 15/15 [00:00<00:00, 27.85it/s] Computing k varies using mode = ManHaDun: 100%|██████████| 15/15 [00:00<00:00, 39.03it/s] Computing k varies using mode = Cosine: 100%|██████████| 15/15 [00:00<00:00, 18.20it/s] 复制代码
可视化结果如下:
可以看到,k值并不是越大越好。当 时,分类准确率较好,当 时,分类准确率明显呈下降趋势
-
四、总结及心得体会
- 在简单的数据集(如西瓜数据集)上,其拥有较少的特征和较明显的分类平面,对这种数据集,KNN的分类效果较好,在西瓜数据集上更是能打到100%准确率。
- 在复杂的数据集(如Wines数据集)上,其拥有较多的特征,且复杂的分类平面,对于这种数据集,KNN的分类效果较差,在Wines数据集上最高只能达到93%的准确率。
- 根据对不同k值和不同距离函数的可视化分析,我们可以看到,k值并不是越大越好。当 时,分类准确率较好,当 时,分类准确率明显呈下降趋势。同时余弦距离函数的准确率最高,但是在 时其准确率下降最快。这可能是因为余弦距离考虑到两个向量之间的夹角关系,比其他两种距离函数更适合KNN分类。
- 使用C接口实现Python程序比使用
Python-based-coding
效率更高。 - 掌握了一些简单的数据可视化方法,学会使用一些简单的matplotlib库中有关pyplot的函数,利用简单的数据可视化方法将大量的数据转化成图片,极大地简化了我们对结果数据的分析和比对,能够更轻易的获得一些结果上的规律和结论。
五、对本实验过程及方法、手段的改进建议
- 数据集可视化时,对高维特征粗暴选取前两个维度进行可视化分析8会丢失其他维度的特征信息,此处可以选择降维方法,例如PCA9等,把高维特征投影到二维平面上以进行可视化分析。
- 可以尝试更加复杂的数据集。
- 可以尝试考量更多距离函数。
六、References
- pandas.pydata.org/↩
- numpy.org/↩
- matplotlib.org/↩
- stackoverflow.com/questions/8…↩
- docs.python.org/3/c-api/int…↩
- [python - How do I get indices of N maximum values in a NumPy array? - Stack Overflow](python - How do I get indices of N maximum values in a NumPy array? - Stack Overflow)↩
- tqdm/tqdm: A Fast, Extensible Progress Bar for Python and CLI↩
- 针对高维特征,只能选取前两个维度进行可视化。↩
- en.wikipedia.org/wiki/Princi…↩