K近邻法实现数据分类完整实验~

1,097 阅读9分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情

一、KNN算法设计

  1. 读入训练数据
  2. 从训练数据中分割出测试数据
  3. 可视化训练数据集
  4. 使用训练数据训练KNN
  5. 使用测试数据对KNN进行评估
  6. 对不同的变量对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模型核心代码

  1. 导入所需库

    import pandas as pd
    import numpy as np
    import matplotlib.pyplot as plt
    

    在本次实验中,我选择pandas1作为读取数据集的主要工具,选择numpy2加速主要的数学运算,选择matplotlib3进行数据可视化分析。

  2. 定义KNN分类器 KNNClassifier

    1. 定义 __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个点进行决策。

    2. 定义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方法,指定绘图坐标分别为两个特征值,点的颜色以数据点标签决定,由此对数据集进行可视化分析

      例如,在西瓜数据集上的可视化效果如下(已划分10%作为测试集,此处是训练集数据):

      image.png

      针对高维特征,只能选取前两个维度进行可视化。

    3. 定义 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()

      此处我进行了多处优化,具体创新点如下:

      1. 避免使用for-loop以加快运行速度

        为什么在这里需要定义三个函数来完成预测过程呢?众所周知,在python中执行代码的效率是相当低下的,这个特点在for循环上体现的尤为明显4。因此,很多python库都通过pyi内置了C语言实现5。Numpy2也使用C语言对运行效率进行优化,因此,调用Numpy2的接口比在python中使用for-loop的运行效率要高出不少。==在这段代码中,我没有使用任何for-loop==,核心代码全部使用Numpy2自带向量化接口实现,因此运行效率比python-oriented-code要高出不少。这也是这段代码中包含两个辅助函数的原因。

      2. 使用argpartition而不是argsort

        为什么选择argpartition呢?主要是内部实现时时间复杂度上的区别6。对于argpartition,其复杂度在最坏情况下也只是O(n)O(n),而argsort的时间复杂度为O(klogk)O(k\log k)

    4. 定义 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()

      此处我进行了多处优化,具体优化点如下:

      1. 复用 euclidean(x, y) 计算结果

        欧氏距离一般计算公式为:

        d=(k=1mxkixkj2)d = \left( \sum_{k=1}^m \left | x_{ki} - x_{kj} \right |^2 \right)

        但是我在此处使用的公式为其展开形式

        d=k=1m(xki22×xki×xkj+xkj2)d = \sum_{k=1}^m\left( \red{ x_{ki}^2} - 2\times x_{ki}\times x_{kj}+ \red {x_{kj}^2} \right)

        此公式中红色部分在计算欧氏距离时会多次使用,因此,使用此公式可以充分利用numpy的缓存机制,减少不必要的重复运算量。

      2. 借用字典映射,实现灵活调整不同距离函数

        因为要使用numpy接口调用此函数完成距离运算操作,如果分别定义三个距离函数并分别调用,将会在多处引入三个if-else语句以判断具体使用哪个距离函数,此处使用一个字典,巧妙地统一了距离函数的调用方式而又不失灵活性。

三、实验数据及结果分析

  1. 在西瓜数据集上使用KNN

    1. 导入所需库

      import pandas as pd
      from Model import KNNClassifier
      from sklearn.model_selection import train_test_split
      from sklearn.metrics import classification_report
      

      此处导入刚刚编写的KNNClassifiersklearn中的训练集分割工具以及分类报告函数进行准确率计算。

    2. 读取数据集并分割训练集和测试集

      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)
      

      此处读入西瓜数据集,并选定特征mh

    3. 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))
      
      1. 可视化结果

        image.png

      2. 准确率报告

      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%准确率!

  2. 在Wines数据集上使用KNN

    1. 导入所需库

      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加入进度条可视化程序执行进度。

    2. 读取数据集并分割训练集与测试集

      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%的比率分为测试集和训练集。

    3. 对数据集进行可视化

      knn.visualization()
      

      image.png

      从图中可以看出,此数据集比西瓜数据集复杂,各类别交错,且分类平面不明显,存在很多离散点。因此,可以估计到KNN的分类准确度不会很高。

    4. 针对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]
      

      可视化结果如下:

      image.png

      可以看到,k值并不是越大越好。当1k101\le k \le 10时,分类准确率较好,当k10k \ge 10时,分类准确率明显呈下降趋势

四、总结及心得体会

  1. 在简单的数据集(如西瓜数据集)上,其拥有较少的特征和较明显的分类平面,对这种数据集,KNN的分类效果较好,在西瓜数据集上更是能打到100%准确率。
  2. 在复杂的数据集(如Wines数据集)上,其拥有较多的特征,且复杂的分类平面,对于这种数据集,KNN的分类效果较差,在Wines数据集上最高只能达到93%的准确率。
  3. 根据对不同k值和不同距离函数的可视化分析,我们可以看到,k值并不是越大越好。当1k101\le k \le 10时,分类准确率较好,当k10k \ge 10时,分类准确率明显呈下降趋势。同时余弦距离函数的准确率最高,但是在k10k \ge 10时其准确率下降最快。这可能是因为余弦距离考虑到两个向量之间的夹角关系,比其他两种距离函数更适合KNN分类。
  4. 使用C接口实现Python程序比使用Python-based-coding效率更高。
  5. 掌握了一些简单的数据可视化方法,学会使用一些简单的matplotlib库中有关pyplot的函数,利用简单的数据可视化方法将大量的数据转化成图片,极大地简化了我们对结果数据的分析和比对,能够更轻易的获得一些结果上的规律和结论。

五、对本实验过程及方法、手段的改进建议

  1. 数据集可视化时,对高维特征粗暴选取前两个维度进行可视化分析8会丢失其他维度的特征信息,此处可以选择降维方法,例如PCA9等,把高维特征投影到二维平面上以进行可视化分析。
  2. 可以尝试更加复杂的数据集。
  3. 可以尝试考量更多距离函数。

六、References

Footnotes

  1. pandas.pydata.org/

  2. numpy.org/ 2 3 4

  3. matplotlib.org/

  4. stackoverflow.com/questions/8…

  5. docs.python.org/3/c-api/int…

  6. [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)

  7. tqdm/tqdm: A Fast, Extensible Progress Bar for Python and CLI

  8. 针对高维特征,只能选取前两个维度进行可视化。

  9. en.wikipedia.org/wiki/Princi…