diff --git a/15.md b/15.md index 1f4becfbd556b7607dd49f996f515853030cd551..d1db1fd4e03e3d05fc451805112bd6f1ccebbb47 100644 --- a/15.md +++ b/15.md @@ -516,3 +516,285 @@ alice_5_nearest_neighbors 我们正在实现我们的 K 最近邻分类器。 在接下来的两节中,我们将把它放在一起并评估其准确性。 +## 实现分类器 + +现在我们准备基于多个属性实现 K 最近邻分类器。 到目前为止,我们只使用了两个属性,以便可视化。 但通常预测将基于许多属性。 这里是一个例子,显示了多个属性可能比两个更好。 + +### 钞票检测 + +这次我们来看看,预测钞票(例如 20 美元钞票)是伪造还是合法的。 研究人员根据许多单个钞票的照片,为我们汇集了一套数据集:一些是假冒的,一些是合法的。 他们从每张图片中计算出一些数字,使用这门课中我们无需担心的技术。 所以,对于每一张钞票,我们知道了一些数字,它们从钞票的照片以及它的类别(是否是伪造的)中计算。 让我们把它加载到一个表中,并看一下。 + +```py +banknotes = Table.read_table('banknote.csv') +banknotes +``` + + +| WaveletVar | WaveletSkew | WaveletCurt | Entropy | Class | +| --- | --- | --- | --- | --- | +| 3.6216 | 8.6661 | -2.8073 | -0.44699 | 0 | +| 4.5459 | 8.1674 | -2.4586 | -1.4621 | 0 | +| 3.866 | -2.6383 | 1.9242 | 0.10645 | 0 | +| 3.4566 | 9.5228 | -4.0112 | -3.5944 | 0 | +| 0.32924 | -4.4552 | 4.5718 | -0.9888 | 0 | +| 4.3684 | 9.6718 | -3.9606 | -3.1625 | 0 | +| 3.5912 | 3.0129 | 0.72888 | 0.56421 | 0 | +| 2.0922 | -6.81 | 8.4636 | -0.60216 | 0 | +| 3.2032 | 5.7588 | -0.75345 | -0.61251 | 0 | +| 1.5356 | 9.1772 | -2.2718 | -0.73535 | 0 | + +(省略了 1362 行) + +让我们看看,前两个数值是否告诉了我们,任何钞票是否伪造的事情。这里是散点图: + +```py +color_table = Table().with_columns( + 'Class', make_array(1, 0), + 'Color', make_array('darkblue', 'gold') +) +banknotes = banknotes.join('Class', color_table) +banknotes.scatter('WaveletVar', 'WaveletCurt', colors='Color') +``` + +非常有趣! 这两个测量值看起来对于预测钞票是否伪造有帮助。 然而,在这个例子中,你现在可以看到蓝色的簇和金色的簇之间有一些重叠。 这表示基于这两个数字,很难判断钞票是否合法。 不过,您可以使用 K 最近邻分类器来预测钞票的合法性。 + +花点时间想一想:假设我们使用`k = 11`(是假如)。 图中的哪些部分会得到正确的结果,哪些部分会产生错误? 决定边界是什么样子? + +数据中显示的规律可能非常乱。 例如,如果使用与图像不同的一对测量值,我们可以得到以下结果: + +```py +banknotes.scatter('WaveletSkew', 'Entropy', colors='Color') +``` + +似乎存在规律,但它是非常复杂。 尽管如此, K 最近邻分类器仍然可以使用,并将有效地“发现”规律。 这说明了机器学习有多强大:它可以有效地利用规律,我们不曾预料到它,或者我们打算将其编入计算机。 + +### 多个属性 + +到目前为止,我一直假设我们有两个属性,可以用来帮助我们做出预测。如果我们有两个以上呢?例如,如果我们有 3 个属性呢? + +这里有一个很酷的部分:你也可以对这个案例使用同样的想法。你需要做的所有事情,就是绘制一个三维散点图,而不是二维的。你仍然可以使用 K 最近邻分类器,但现在计算 3 维而不是 2 维距离,它还是有用。可以,很酷! + +事实上,2 或 3 没有什么特别之处。如果你有 4 个属性,你可以使用 4 维的 K 最近邻分类器。 5 个属性?在五维空间里工作。没有必要在这里停下来!这一切都适用于任意多的属性。你只需在非常高维的空间中工作。它变得有点奇怪 - 不可能可视化,但没关系。计算机算法推广得很好:你需要的所有事情,就是计算距离的能力,这并不难。真是亦可赛艇! + +```py +ax = plt.figure(figsize=(8,8)).add_subplot(111, projection='3d') +ax.scatter(banknotes.column('WaveletSkew'), + banknotes.column('WaveletVar'), + banknotes.column('WaveletCurt'), + c=banknotes.column('Color')); +``` + +真棒!只用 2 个属性,两个簇之间有一些重叠(这意味着对于重叠中的一些点,分类器必然犯一些错误)。但是当我们使用这三个属性时,两个簇几乎没有重叠。换句话说,使用这 3 个属性的分类器比仅使用 2 个属性的分类器更精确。 + +这是分类中的普遍现象。每个属性都可能会给你提供新的信息,所以更多的属性有时可以帮助你建立一个更好的分类器。当然开销是,现在我们必须收集更多的信息来衡量每个属性的值,但是如果这个开销显着提高了我们的分类器的精度,那么它可能非常值得。 + +综上所述:你现在知道如何使用 K 最近邻分类,预测是与否的问题的答案,基于一些属性值,假设你有一个带有样本的训练集,其中正确的预测已知。总的路线图是这样的: + +找出一些属性,你认为可能帮助你预测问题的答案。 +收集一组训练样本,其中你知道属性值以及正确预测。 +为了预测未来,测量属性的值,然后使用 K 最近邻分类来预测问题的答案。 + +### 多维距离 + +我们知道如何在二维空间中计算距离。 如果我们在坐标`(x0, y0)`处有一个点,而在`(x1, y1)`处有另一个点,则它们之间的距离是: + +![](http://latex.codecogs.com/gif.latex?D%20%3D%20%5Csqrt%7B%28x_0-x_1%29%5E2%20+%20%28y_0-y_1%29%5E2%7D) + +在三维空间中,点是`(x0, y0, z0)`和`(x1, y1, z1)`,它们之间的距离公式为: + +![](http://latex.codecogs.com/gif.latex?D%20%3D%20%5Csqrt%7B%28x_0-x_1%29%5E2%20+%20%28y_0-y_1%29%5E2%20+%20%28z_0-z_1%29%5E2%7D) + +在 N 维空间中,东西有点难以可视化,但我想你可以看到公式是如何推广的:我们总结每个独立坐标差的平方,然后取平方根。 + +在最后一节中,我们定义了函数`distance`返回两点之间距离。 我们在二维中使用它,但好消息是函数并不关心有多少维! 它只是将两个坐标数组相减(无论数组有多长),求差值的平方并加起来,然后取平方根。 我们不必更改代码就可以在多个维度上工作。 + +```py +def distance(point1, point2): + """Returns the distance between point1 and point2 + where each argument is an array + consisting of the coordinates of the point""" + return np.sqrt(np.sum((point1 - point2)**2)) +``` + +我们在这个新的数据集上使用它。 `wine`表含有 178 种不同的意大利葡萄酒的化学成分。 这些类别是葡萄品种,称为品种。 有三个类别,但我们只看看是否可以把第一类和其他两个类别分开。 + +```py +wine = Table.read_table('wine.csv') + +# For converting Class to binary + +def is_one(x): + if x == 1: + return 1 + else: + return 0 + +wine = wine.with_column('Class', wine.apply(is_one, 0)) +wine +``` + + +| Class | Alcohol | Malic Acid | Ash | Alcalinity of Ash | Magnesium | Total Phenols | Flavanoids | Nonflavanoid phenols | Proanthocyanins | Color Intensity | Hue | OD280/OD315 of diulted wines | Proline | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 1 | 14.23 | 1.71 | 2.43 | 15.6 | 127 | 2.8 | 3.06 | 0.28 | 2.29 | 5.64 | 1.04 | 3.92 | 1065 | +| 1 | 13.2 | 1.78 | 2.14 | 11.2 | 100 | 2.65 | 2.76 | 0.26 | 1.28 | 4.38 | 1.05 | 3.4 | 1050 | +| 1 | 13.16 | 2.36 | 2.67 | 18.6 | 101 | 2.8 | 3.24 | 0.3 | 2.81 | 5.68 | 1.03 | 3.17 | 1185 | +| 1 | 14.37 | 1.95 | 2.5 | 16.8 | 113 | 3.85 | 3.49 | 0.24 | 2.18 | 7.8 | 0.86 | 3.45 | 1480 | +| 1 | 13.24 | 2.59 | 2.87 | 21 | 118 | 2.8 | 2.69 | 0.39 | 1.82 | 4.32 | 1.04 | 2.93 | 735 | +| 1 | 14.2 | 1.76 | 2.45 | 15.2 | 112 | 3.27 | 3.39 | 0.34 | 1.97 | 6.75 | 1.05 | 2.85 | 1450 | +| 1 | 14.39 | 1.87 | 2.45 | 14.6 | 96 | 2.5 | 2.52 | 0.3 | 1.98 | 5.25 | 1.02 | 3.58 | 1290 | +| 1 | 14.06 | 2.15 | 2.61 | 17.6 | 121 | 2.6 | 2.51 | 0.31 | 1.25 | 5.05 | 1.06 | 3.58 | 1295 | +| 1 | 14.83 | 1.64 | 2.17 | 14 | 97 | 2.8 | 2.98 | 0.29 | 1.98 | 5.2 | 1.08 | 2.85 | 1045 | +| 1 | 13.86 | 1.35 | 2.27 | 16 | 98 | 2.98 | 3.15 | 0.22 | 1.85 | 7.22 | 1.01 | 3.55 | 1045 | + +前两种葡萄酒都属于第一类。为了找到它们之间的距离,我们首先需要一个只有属性的表格: + +```py +wine_attributes = wine.drop('Class') +distance(np.array(wine_attributes.row(0)), np.array(wine_attributes.row(1))) +31.265012394048398 +``` + +中的最后一个葡萄酒是第零类。它与第一个葡萄酒的距离是: + +```py +distance(np.array(wine_attributes.row(0)), np.array(wine_attributes.row(177))) +506.05936766351834 +``` + +这也太大了! 让我们做一些可视化,看看第一类是否真的看起来不同于第零类。 + +```py +wine_with_colors = wine.join('Class', color_table) +wine_with_colors.scatter('Flavanoids', 'Alcohol', colors='Color') +``` + +蓝点(第一类)几乎完全与金点分离。 这表明了,为什么两种第一类葡萄酒之间的距离小于两个不同类别葡萄酒之间的距离。 我们使用不同的一对属性,也可以看到类似的现象: + +```py +wine_with_colors.scatter('Alcalinity of Ash', 'Ash', colors='Color') +``` + +但是对于不同的偶对,图像更加模糊。 + +```py +wine_with_colors.scatter('Magnesium', 'Total Phenols', colors='Color') +``` + +让我们来看看,是否可以基于所有的属性来实现一个分类器。 之后,我们会看到它有多准确。 + +### 实现计划 + +现在是时候编写一些代码来实现分类器了。 输入是我们要分类的一个点。 分类器的原理是,找到训练集中的 K 个最近邻点。 所以,我们的方法将会是这样: + +找出最接近的 K 个点,即训练集中与点最相似的 K 个葡萄酒。 + +看看这些 K 个邻居的类别,并取大多数,找到最普遍的葡萄酒类别。 用它作为我们对点的预测。 + +所以这将指导我们的 Python 代码的结构。 + +```py +def closest(training, p, k): + ... + +def majority(topkclasses): + ... + +def classify(training, p, k): + kclosest = closest(training, p, k) + kclosest.classes = kclosest.select('Class') + return majority(kclosest) +``` + +### 实现步骤 1 + +为了为肾病数据实现第一步,我们必须计算点到训练集中每个患者的距离,按照距离排序,并取出训练集中最接近的 K 个患者。 + +这就是我们在上一节中使用对应 Alice 的点所做的事情。 我们来概括一下这个代码。 我们将在这里重新定义`distance`,只是为了方便。 + +```py +def distance(point1, point2): + """Returns the distance between point1 and point2 + where each argument is an array + consisting of the coordinates of the point""" + return np.sqrt(np.sum((point1 - point2)**2)) + +def all_distances(training, new_point): + """Returns an array of distances + between each point in the training set + and the new point (which is a row of attributes)""" + attributes = training.drop('Class') + def distance_from_point(row): + return distance(np.array(new_point), np.array(row)) + return attributes.apply(distance_from_point) + +def table_with_distances(training, new_point): + """Augments the training table + with a column of distances from new_point""" + return training.with_column('Distance', all_distances(training, new_point)) + +def closest(training, new_point, k): + """Returns a table of the k rows of the augmented table + corresponding to the k smallest distances""" + with_dists = table_with_distances(training, new_point) + sorted_by_distance = with_dists.sort('Distance') + topk = sorted_by_distance.take(np.arange(k)) + return topk +``` + +让我们看看它如何在我们的葡萄酒数据上工作。 我们只要取第一个葡萄酒,在所有葡萄酒中找到最近的五个邻居。 请记住,由于这个葡萄酒是数据集的一部分,因此它自己是最近的邻居。 所以我们应该预计看到,它在列表顶端,后面是其他四个。 + +首先让我们来提取它的属性: + +```py +special_wine = wine.drop('Class').row(0) +``` + +现在让我们找到它的五个最近邻: + +```py +closest(wine, special_wine, 5) +``` + +| Class | Alcohol | Malic Acid | Ash | Alcalinity of Ash | Magnesium | Total Phenols | Flavanoids | Nonflavanoid phenols | Proanthocyanins | Color Intensity | Hue | OD280/OD315 of diulted wines | Proline | Distance | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 1 | 14.23 | 1.71 | 2.43 | 15.6 | 127 | 2.8 | 3.06 | 0.28 | 2.29 | 5.64 | 1.04 | 3.92 | 1065 | 0 | +| 1 | 13.74 | 1.67 | 2.25 | 16.4 | 118 | 2.6 | 2.9 | 0.21 | 1.62 | 5.85 | 0.92 | 3.2 | 1060 | 10.3928 | +| 1 | 14.21 | 4.04 | 2.44 | 18.9 | 111 | 2.85 | 2.65 | 0.3 | 1.25 | 5.24 | 0.87 | 3.33 | 1080 | 22.3407 | +| 1 | 14.1 | 2.02 | 2.4 | 18.8 | 103 | 2.75 | 2.92 | 0.32 | 2.38 | 6.2 | 1.07 | 2.75 | 1060 | 24.7602 | +| 1 | 14.38 | 3.59 | 2.28 | 16 | 102 | 3.25 | 3.17 | 0.27 | 2.19 | 4.9 | 1.04 | 3.44 | 1065 | 25.0947 | + +好的! 第一行是最近邻,这是它自己 - `Distance`中值为零,和预期一样。 所有五个最近邻都属于第一类,这与我们先前的观察结果一致,即第一类葡萄酒集中在某些维度。 + +### 实现步骤 2 和 3 + +接下来,我们需要获取最近邻的“最大计数”,并把我们的点分配给大多数的相同类别。 + +```py +def majority(topkclasses): + ones = topkclasses.where('Class', are.equal_to(1)).num_rows + zeros = topkclasses.where('Class', are.equal_to(0)).num_rows + if ones > zeros: + return 1 + else: + return 0 + +def classify(training, new_point, k): + closestk = closest(training, new_point, k) + topkclasses = closestk.select('Class') + return majority(topkclasses) +classify(wine, special_wine, 5) +1 +``` + +如果将`special_wine`改为数据集中的最后一个,我们的分类器是否能够判断它在第零类中嘛? + +```py +special_wine = wine.drop('Class').row(177) +classify(wine, special_wine, 5) +0 +``` + +是的! 分类器弄对了。 + +但是我们还不知道它对于所有其它葡萄酒如何,而且无论如何我们都知道,测试已经属于训练集的葡萄酒可能过于乐观了。 在本章的最后部分,我们将葡萄酒分为训练集和测试集,然后测量分类器在测试集上的准确性。