From 53c89eb3a19d8299f9fd4c1b2926d1d731973feb Mon Sep 17 00:00:00 2001 From: bushuhui Date: Thu, 16 Dec 2021 19:03:42 +0800 Subject: [PATCH] Improve knn classification --- 2_knn/knn_classification.ipynb | 70 +++++++++++++++++++-------------- 2_knn/knn_test_data.pdf | Bin 0 -> 9022 bytes 2_knn/knn_train_data.pdf | Bin 0 -> 10374 bytes 3 files changed, 41 insertions(+), 29 deletions(-) create mode 100644 2_knn/knn_test_data.pdf create mode 100644 2_knn/knn_train_data.pdf diff --git a/2_knn/knn_classification.ipynb b/2_knn/knn_classification.ipynb index 686830d..ae6ea5d 100644 --- a/2_knn/knn_classification.ipynb +++ b/2_knn/knn_classification.ipynb @@ -62,7 +62,7 @@ "source": [ "### 1.1 距离计算\n", "\n", - "要度量空间中点距离的话,有好几种度量方式,比如常见的曼哈顿距离计算、欧式距离计算等等。不过通常 KNN 算法中使用的是欧式距离。这里只是简单说一下,拿二维平面为例,二维空间两个点的欧式距离计算公式如下:\n", + "要度量空间中点距离的话,有好几种度量方式,比如常见的曼哈顿距离计算、欧式距离计算等等。不过通常 kNN 算法中使用的是欧式距离。这里只是简单说一下,拿二维平面为例,二维空间两个点的欧式距离计算公式如下:\n", "$$\n", "d = \\sqrt{(x_2 - x_1)^2 + (y_2 - y_1)^2}\n", "$$\n", @@ -72,7 +72,7 @@ "d(p, q) = \\sqrt{ (p_1-q_1)^2 + (p_1-q_1)^2 + ... + (p_n-q_n)^2 } = \\sqrt{ \\sum_{i=1,n} (p_i-q_i)^2}\n", "$$\n", "\n", - "这样我们就明白了如何计算距离。kNN 算法最简单粗暴的就是将 `预测点` 与 `所有点` 距离进行计算,然后保存并排序,选出前面 k 个值看看哪些类别比较多。" + "kNN 算法最简单粗暴的就是将 `预测点` 与 `所有点` 距离进行计算,然后保存并排序,选出前面 k 个值看看哪些类别比较多。" ] }, { @@ -82,7 +82,7 @@ "\n", "## 2. 机器学习的思维模型\n", "\n", - "针对kNN方法的提出机器学习的思维模型,在给定问题的情况下,是如何思考并解决机器学习问题\n", + "针对kNN方法从原理、算法、到实现,可以得出机器学习的思维模型,在给定问题的情况下,是如何思考并解决机器学习问题。\n", "\n", "![machine learning - methodology](images/ml_methodology.png)\n", "\n", @@ -112,7 +112,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 2, "metadata": {}, "outputs": [ { @@ -146,7 +146,7 @@ "import numpy as np\n", "import matplotlib.pyplot as plt\n", "\n", - "# data generation\n", + "# 生成模拟数据\n", "np.random.seed(314)\n", "\n", "data_size1 = 100\n", @@ -158,7 +158,7 @@ "y2 = [1 for _ in range(data_size2)]\n", "\n", "\n", - "# all sample data\n", + "# 合并生成全部数据\n", "x = np.concatenate((x1, x2), axis=0)\n", "y = np.concatenate((y1, y2), axis=0)\n", "\n", @@ -167,7 +167,7 @@ "x = x[shuffled_index]\n", "y = y[shuffled_index]\n", "\n", - "# split train & test\n", + "# 分割训练与测试数据\n", "split_index = int(data_size_all*0.7)\n", "x_train = x[:split_index]\n", "y_train = y[:split_index]\n", @@ -175,12 +175,14 @@ "y_test = y[split_index:]\n", "\n", "\n", - "# plot data\n", + "# 绘制结果\n", "plt.scatter(x_train[:,0], x_train[:,1], c=y_train, marker='.')\n", "plt.title(\"train data\")\n", + "plt.savefig(\"knn_train_data.pdf\")\n", "plt.show()\n", "plt.scatter(x_test[:,0], x_test[:,1], c=y_test, marker='.')\n", "plt.title(\"test data\")\n", + "plt.savefig(\"knn_test_data.pdf\")\n", "plt.show()\n" ] }, @@ -209,38 +211,31 @@ "import operator\n", "\n", "def knn_distance(v1, v2):\n", + " \"\"\"计算两个多维向量的距离\"\"\"\n", " return np.sum(np.square(v1-v2))\n", "\n", "def knn_vote(ys):\n", - " method = 1\n", - " \n", - " # method 1\n", - " if method == 1:\n", - " vote_dict = {}\n", + " \"\"\"根据ys的类别,挑选类别最多一类作为输出\"\"\"\n", + " vote_dict = {}\n", " for y in ys:\n", " if y not in vote_dict.keys():\n", " vote_dict[y] = 1\n", " else:\n", " vote_dict[y] += 1\n", + " \n", + " method = 1\n", + " \n", + " # 方法1 - 使用排序的方法\n", + " if method == 1:\n", " sorted_vote_dict = sorted(vote_dict.items(), \\\n", " #key=operator.itemgetter(1), \\\n", " key=lambda x:x[1], \\\n", " reverse=True)\n", - " \n", " return sorted_vote_dict[0][0]\n", " \n", - " # method 2\n", + " # 方法2 - 使用循环遍历找到类别最多的一类\n", " if method == 2:\n", - " maxv = 0\n", - " maxk = 0\n", - " \n", - " vote_dict = {}\n", - " for y in ys:\n", - " if y not in vote_dict.keys():\n", - " vote_dict[y] = 1\n", - " else:\n", - " vote_dict[y] += 1\n", - " \n", + " maxv = maxk = 0 \n", " for y in np.unique(ys):\n", " if maxv < vote_dict[y]:\n", " maxv = vote_dict[y]\n", @@ -248,6 +243,14 @@ " return maxk\n", " \n", "def knn_predict(x, train_x, train_y, k=3):\n", + " \"\"\"\n", + " 针对给定的数据进行分类\n", + " 参数\n", + " x - 输入的待分类样本\n", + " train_x - 训练数据的样本\n", + " train_y - 训练数据的标签\n", + " k - 最近邻的样本个数\n", + " \"\"\"\n", " dist_arr = [knn_distance(x, train_x[j]) for j in range(len(train_x))]\n", " sorted_index = np.argsort(dist_arr)\n", " top_k_index = sorted_index[:k]\n", @@ -255,8 +258,7 @@ " return knn_vote(ys)\n", " \n", "\n", - "#a = knn_predict(x_train[0], x_train, y_train)\n", - "\n", + "# 对每个样本进行分类\n", "y_train_est = [knn_predict(x_train[i], x_train, y_train) for i in range(len(x_train))]\n", "print(y_train_est)" ] @@ -275,6 +277,7 @@ } ], "source": [ + "# 计算训练数据的精度\n", "n_correct = 0\n", "for i in range(len(x_train)):\n", " if y_train_est[i] == y_train[i]:\n", @@ -298,6 +301,7 @@ } ], "source": [ + "# 计算测试数据的精度\n", "y_test_est = [knn_predict(x_test[i], x_train, y_train, 3) for i in range(len(x_test))]\n", "n_correct = 0\n", "for i in range(len(x_test)):\n", @@ -317,7 +321,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -325,19 +329,26 @@ "import operator\n", "\n", "class KNN(object):\n", - "\n", " def __init__(self, k=3):\n", + " \"\"\"对象构造函数,参数为:\n", + " k - 近邻个数\"\"\"\n", " self.k = k\n", "\n", " def fit(self, x, y):\n", + " \"\"\"拟合给定的数据,参数为:\n", + " x - 样本的特征;y - 样本的标签\"\"\"\n", " self.x = x\n", " self.y = y\n", " return self\n", "\n", " def _square_distance(self, v1, v2):\n", + " \"\"\"计算两个样本点的特征空间距离,参数为:\n", + " v1 - 样本点1;v2 - 样本点2\"\"\"\n", " return np.sum(np.square(v1-v2))\n", "\n", " def _vote(self, ys):\n", + " \"\"\"投票算法,参数为:\n", + " ys - k个近邻样本的类别\"\"\"\n", " ys_unique = np.unique(ys)\n", " vote_dict = {}\n", " for y in ys:\n", @@ -349,6 +360,7 @@ " return sorted_vote_dict[0][0]\n", "\n", " def predict(self, x):\n", + " \n", " y_pred = []\n", " for i in range(len(x)):\n", " dist_arr = [self._square_distance(x[i], self.x[j]) for j in range(len(self.x))]\n", diff --git a/2_knn/knn_test_data.pdf b/2_knn/knn_test_data.pdf new file mode 100644 index 0000000000000000000000000000000000000000..56aad6184604dda2696b487848ad070fec925ef1 GIT binary patch literal 9022 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 Jt7oc@_#gj|ONamf literal 0 HcmV?d00001 diff --git a/2_knn/knn_train_data.pdf b/2_knn/knn_train_data.pdf new file mode 100644 index 0000000000000000000000000000000000000000..fe9887c2c1f22a95a5d85f7251ebd54997a74922 GIT binary patch literal 10374 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Ȼ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 zul