# 十五、分类 [David Wagner](https://en.wikipedia.org/wiki/David_A._Wagner) 是这一章的主要作者。 机器学习是一类技术,用于自动寻找数据中的规律,并使用它来推断或预测。你已经看到了线性回归,这是一种机器学习技术。本章介绍一个新的技术:分类。 分类就是学习如何根据过去的例子做出预测。我们举了一些例子,告诉我们什么是正确的预测,我们希望从这些例子中学习,如何较好地预测未来。以下是在实践中分类的一些应用领域: + 他们有一些每个订单的信息(例如,它的总值,订单是否被运送到这个客户以前使用过的地址,是否与信用卡持有人的账单地址相同)。他们有很多过去的订单数据,他们知道哪些过去的订单是欺诈性的,哪些不是。他们想要学习规律,这将帮助他们预测新订单到达时,这些新订单是否有欺诈行为。 + 在线约会网站希望预测:这两个人合适吗?他们有很多数据,他们过去向顾客推荐一些东西,它们就知道了哪个是成功的。当新客户注册时,他们想预测谁可能是他们的最佳伴侣。 + 医生想知道:这个病人是否患有癌症?根据一些实验室测试的结果,他们希望能够预测特定患者是否患有癌症。基于一些实验室测试的测量结果,以及他们是否最终发展成癌症,并且由此他们希望尝试推断,哪些测量结果倾向于癌症(或非癌症)特征,以便能够准确地诊断未来的患者。 + 政客们想预测:你打算为他们投票吗?这将帮助他们将筹款工作集中在可能支持他们的人身上,并将动员工作集中在投票给他们的人身上。公共数据库和商业数据库有大多数人的大量信息,例如,他们是否拥有房屋或房租;他们是否住在富裕的社区还是贫穷的社区;他们的兴趣和爱好;他们的购物习惯;等等。政治团体已经调查了一些选民,并找到了他们计划投票的人,所以他们有一些正确答案已知的例子。 所有这些都是分类任务。请注意,在每个例子中,预测是一个是与否的问题 - 我们称之为二元分类,因为只有两个可能的预测。 在分类任务中,我们想要进行预测的每个个体或情况都称为观测值。我们通常有很多观测值。每个观测值具有多个已知属性(例如,亚马逊订单的总值,或者选民的年薪)。另外,每个观测值都有一个类别,这是对我们关心的问题(例如欺骗与否,或者是否投票)的回答。 当亚马逊预测订单是否具有欺诈性时,每个订单都对应一个单独的观测值。每个观测值都有几个属性:订单的总值,订单是否被运送到此客户以前使用的地址等等。观测值类别为 0 或 1,其中 0 意味着订单不是欺诈,1 意味着订单是欺诈性的。当一个客户生成新的订单时,我们并没有观察到这个订单是否具有欺诈性,但是我们确实观察了这个订单的属性,并且我们会尝试用这些属性来预测它的类别。 分类需要数据。它涉及到发现规律,并且为了发现规律,你需要数据。这就是数据科学的来源。特别是,我们假设我们可以获得训练数据:一系列的观测数据,我们知道每个观测值的类别。这些预分类的观测值集合也被称为训练集。分类算法需要分析训练集,然后提出一个分类器:用于预测未来观测值类别的算法。 分类器不需要是完全有用的。即使准确度低于 100%,它们也可以是有用的。例如,如果在线约会网站偶尔会提出不好的建议,那没关系;他们的顾客已经预期,在他们找到真爱之前需要遇见许多人。当然,你不希望分类器犯太多的错误,但是不必每次都得到正确的答案。 ## 最近邻 在本节中,我们将开发最近邻分类方法。 如果一些代码神秘,不要担心,现在只要把注意力思路上。 在本章的后面,我们将看到如何将我们的想法组织成执行分类的代码。 ### 慢性肾病 我们来浏览一个例子。 我们将使用收集的数据集来帮助医生诊断慢性肾病(CKD)。 数据集中的每一行都代表单个患者,过去接受过治疗并且诊断已知。 对于每个患者,我们都有一组血液测试的测量结果。 我们希望找到哪些测量结果对诊断慢性肾病最有用,并根据他们的血液检查结果,开发一种方法,将未来的患者分类为“CKD”或“非 CKD”。 ```py ckd = Table.read_table('ckd.csv').relabeled('Blood Glucose Random', 'Glucose') ckd ``` | Age | Blood Pressure | Specific Gravity | Albumin | Sugar | Red Blood Cells | Pus Cell | Pus Cell clumps | Bacteria | Glucose | Blood Urea | Serum Creatinine | Sodium | Potassium | Hemoglobin | Packed Cell Volume | White Blood Cell Count | Red Blood Cell Count | Hypertension | Diabetes Mellitus | Coronary Artery Disease | Appetite | Pedal Edema | Anemia | Class | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | 48 | 70 | 1.005 | 4 | 0 | normal | abnormal | present | notpresent | 117 | 56 | 3.8 | 111 | 2.5 | 11.2 | 32 | 6700 | 3.9 | yes | no | no | poor | yes | yes | 1 | | 53 | 90 | 1.02 | 2 | 0 | abnormal | abnormal | present | notpresent | 70 | 107 | 7.2 | 114 | 3.7 | 9.5 | 29 | 12100 | 3.7 | yes | yes | no | poor | no | yes | 1 | | 63 | 70 | 1.01 | 3 | 0 | abnormal | abnormal | present | notpresent | 380 | 60 | 2.7 | 131 | 4.2 | 10.8 | 32 | 4500 | 3.8 | yes | yes | no | poor | yes | no | 1 | | 68 | 80 | 1.01 | 3 | 2 | normal | abnormal | present | present | 157 | 90 | 4.1 | 130 | 6.4 | 5.6 | 16 | 11000 | 2.6 | yes | yes | yes | poor | yes | no | 1 | | 61 | 80 | 1.015 | 2 | 0 | abnormal | abnormal | notpresent | notpresent | 173 | 148 | 3.9 | 135 | 5.2 | 7.7 | 24 | 9200 | 3.2 | yes | yes | yes | poor | yes | yes | 1 | | 48 | 80 | 1.025 | 4 | 0 | normal | abnormal | notpresent | notpresent | 95 | 163 | 7.7 | 136 | 3.8 | 9.8 | 32 | 6900 | 3.4 | yes | no | no | good | no | yes | 1 | | 69 | 70 | 1.01 | 3 | 4 | normal | abnormal | notpresent | notpresent | 264 | 87 | 2.7 | 130 | 4 | 12.5 | 37 | 9600 | 4.1 | yes | yes | yes | good | yes | no | 1 | | 73 | 70 | 1.005 | 0 | 0 | normal | normal | notpresent | notpresent | 70 | 32 | 0.9 | 125 | 4 | 10 | 29 | 18900 | 3.5 | yes | yes | no | good | yes | no | 1 | | 73 | 80 | 1.02 | 2 | 0 | abnormal | abnormal | notpresent | notpresent | 253 | 142 | 4.6 | 138 | 5.8 | 10.5 | 33 | 7200 | 4.3 | yes | yes | yes | good | no | no | 1 | | 46 | 60 | 1.01 | 1 | 0 | normal | normal | notpresent | notpresent | 163 | 92 | 3.3 | 141 | 4 | 9.8 | 28 | 14600 | 3.2 | yes | yes | no | good | no | no | 1 | (省略了 148 行) 一些变量是类别(像“异常”这样的词),还有一些是定量的。 定量变量都有不同的规模。 我们将要通过眼睛比较和估计距离,所以我们只选择一些变量并在标准单位下工作。 之后我们就不用担心每个变量的规模。 ```py ckd = Table().with_columns( 'Hemoglobin', standard_units(ckd.column('Hemoglobin')), 'Glucose', standard_units(ckd.column('Glucose')), 'White Blood Cell Count', standard_units(ckd.column('White Blood Cell Count')), 'Class', ckd.column('Class') ) ckd ``` | Hemoglobin | Glucose | White Blood Cell Count | Class | | --- | --- | --- | --- | | -0.865744 | -0.221549 | -0.569768 | 1 | | -1.45745 | -0.947597 | 1.16268 | 1 | | -1.00497 | 3.84123 | -1.27558 | 1 | | -2.81488 | 0.396364 | 0.809777 | 1 | | -2.08395 | 0.643529 | 0.232293 | 1 | | -1.35303 | -0.561402 | -0.505603 | 1 | | -0.413266 | 2.04928 | 0.360623 | 1 | | -1.28342 | -0.947597 | 3.34429 | 1 | | -1.10939 | 1.87936 | -0.409356 | 1 | | -1.35303 | 0.489051 | 1.96475 | 1 | (省略了 148 行) 我们来看两列,(病人的血液中)血红蛋白水平和血糖水平(一天中的随机时间;没有专门为血液测试禁食)。 我们将绘制一个散点图来显示两个变量之间的关系。 蓝点是 CKD 患者; 金点是非 CKD 的患者。 什么样的医学检验结果似乎表明了 CKD? ```py color_table = Table().with_columns( 'Class', make_array(1, 0), 'Color', make_array('darkblue', 'gold') ) ckd = ckd.join('Class', color_table) ckd.scatter('Hemoglobin', 'Glucose', colors='Color') ``` 假设爱丽丝是不在数据集中的新患者。 如果我告诉你爱丽丝的血红蛋白水平和血糖水平,你可以预测她是否有 CKD 嘛? 确实看起来可以! 您可以在这里看到非常清晰的规律:右下角的点代表没有 CKD 的人,其余的倾向于有 CKD 的人。 对于人来说,规律是显而易见的。 但是,我们如何为计算机编程来自动检测这种规律? ### 最近邻分类器 我们可能寻找很多种模式,还有很多分类算法。但是我会告诉你一个算法,它拥有令人惊讶的效果。它被称为最近邻分类。这是它的思路。如果我们有爱丽丝的血红蛋白和血糖数值,我们可以把她放在这个散点图的某个地方;血红蛋白是她的`x`坐标,血糖是她的`y`坐标。现在,为了预测她是否有 CKD,我们在散点图中找到最近的点,检查它是蓝色还是金色;我们预测爱丽丝应该接受与该患者相同的诊断。 换句话说,为了将 Alice 划分为 CKD 与否,我们在训练集中找到与 Alice “最近”的患者,然后将该患者的诊断用作对 Alice 的预测。直觉上,如果散点图中的两个点彼此靠近,那么相应的测量结果非常相似,所以我们可能会预计,他们(更可能)得到相同的诊断。我们不知道 Alice 的诊断,但是我们知道训练集中所有病人的诊断,所以我们在训练集中找到与 Alice 最相似的病人,并利用病人的诊断来预测 Alice 的诊断。 在下图中,红点代表爱丽丝。它与距离它最近的点由一条黑线相连,即训练集中最近邻。该图由一个名为`show_closest`的函数绘制。它需要一个数组,代表 Alice 点的`x和`y`坐标。改变它们来查看最近的点如何改变!特别注意最近的点是蓝色,以及金色的时候。 ```py # In this example, Alice's Hemoglobin attribute is 0 and her Glucose is 1.5. alice = make_array(0, 1.5) show_closest(alice) ``` 因此,我们的最近邻分类器是这样工作的: + 找到训练集中离新点最近的点。 + 如果最近的点是“CKD”点,则将新点划分为“CKD”。如果最近的点是“非 CKD”点,则将新点划分为“非 CKD”。 散点图表明这个最近邻分类器应该相当准确。右下角的点倾向于接受“非 CKD”的诊断,因为他们的最近邻是一个金点。其余的点倾向于接受“CKD”诊断,因为他们的最近邻是蓝点。所以这个例子中,最近邻策略似乎很好地捕捉了我们的直觉。 ## 决策边界 有时一种分类器可视化的实用方法是,绘制出分类器预测“CKD”的几种属性,以及预测“非 CKD”的几种。我们最终得到两者之间的边界,边界一侧的点将被划分为“CKD”,而另一侧的点将划分为“非 CKD”。这个边界称为决策边界。每个不同的分类器将有不同的决策边界;决策边界只是一种方法,用于可视化分类器实用什么标准来对点分类。 例如,假设爱丽丝的点坐标是`(0, 1.5)`。注意最近邻是蓝色的。现在尝试减少点的高度(`y`坐标)。你会看到,在`y = 0.95`左右,最近邻从蓝色变为金色。 ```py alice = make_array(0, 0.97) show_closest(alice) ``` 这里有数百个未分类的新点,都是红色的。 每个红点在训练集中都有一个最近邻(与之前的蓝点和金点相同)。对于一些红点,你可以很容易地判断最近邻是蓝色还是金色。对于其他点来说,通过眼睛来做出决定更为棘手。那些是靠近决策边界的点。 但是计算机可以很容易地确定每个点的最近邻。那么让我们将我们的最近邻分类器应用于每个红点: 对于每个红点,它必须找到训练集中最近的点;它必须将红点的颜色改变为最近邻的颜色。 结果图显示哪些点将划分为“CKD”(全部为蓝色),或者“非 CKD”(全部为黄金)。 决策边界是分类器从将红点转换为蓝色变成金色的地方。 ## KNN 然而,两个类别的分类并不总是那么清晰。例如,假设我们不用血红蛋白水平而是看白细胞计数。看看会发生什么: ```py ckd.scatter('White Blood Cell Count', 'Glucose', colors='Color') ``` 如您所见,非 CKD 个体都聚集在左下角。大多数 CKD 患者在该簇的上方或右侧,但不是全部。上图左下角有一些 CKD 患者(分散在金簇中的少数蓝点表示)。这意味着你不能从这两个检测结果确定,某些人是否拥有 CKD。 如果提供爱丽丝的血糖水平和白细胞计数,我们可以预测她是否患有慢性肾病嘛?是的,我们可以做一个预测,但是我们不应该期望它是 100% 准确的。直觉上,似乎存在预测的自然策略:绘制 Alice 在散点图中的位置;如果她在左下角,则预测她没有 CKD,否则预测她有 CKD。 这并不完美 - 我们的预测有时是错误的。 (请花点时间思考一下,会把哪些患者弄错?)上面的散点图表明,CKD 患者的葡萄糖和白细胞水平有时与没有 CKD 的患者相同,因此任何分类器都是不可避免地会对他们做出错误的预测。 我们可以在计算机上自动化吗?那么,最近邻分类器也是一个合理的选择。花点时间思考一下:它的预测与上述直觉策略的预测相比如何?他们什么时候会不同? 它的预测与我们的直觉策略非常相似,但偶尔会做出不同的预测。特别是,如果爱丽丝的血液检测结果恰好把她放在左下角的一个蓝点附近,那么这个直观的策略就可能预测“非 CKD”,而最近邻的分类器会预测“CKD”。 最近邻分类器有一个简单的推广,修正了这个异常。它被称为 K 最近邻分类器。为了预测爱丽丝的诊断,我们不仅仅查看靠近她的一个邻居,而是查看靠近她的三个点,并用这三个点中的每一个点的诊断来预测艾丽丝的诊断。特别是,我们将使用这 3 个诊断中的大部分值作为我们对 Alice 诊断的预测。当然,数字 3 没有什么特别之处:我们可以使用 4 或 5 或更多。 (选择一个奇数通常是很方便的,所以我们不需要处理相等)。一般来说,我们选择一个数字`k`,而我们对 Alice 的预测诊断是基于训练集中最接近爱丽丝的`k`个点。直观来说,这些是血液测试结果与爱丽丝最相似的`k`个患者,因此使用他们的诊断来预测爱丽丝的诊断似乎是合理的。 ## 训练和测试 我们最近的邻居分类器有多好?要回答这个问题,我们需要知道我们的分类有多正确。如果患者患有慢性肾脏疾病,那么我们的分类器有多可能将其选出来呢? 如果病人在我们的训练集中,我们可以立即找到。我们已经知道病人位于什么类别,所以我们可以比较我们的预测和病人的真实类别。 但是分类器的重点在于对未在训练集中的新患者进行预测。我们不知道这些病人位于什么类别,但我们可以根据分类器做出预测。如何知道预测是否正确? 一种方法是等待患者之后的医学检查,然后检查我们的预测是否与检查结果一致。用这种方法,当我们可以说我们的预测有多准确的时候,它就不再能用于帮助病人了。 相反,我们将在一些真实类别已知的病人上尝试我们的分类器。然后,我们将计算分类器正确的时间比例。这个比例将作为我们分类器准确预测的所有新患者的比例的估计值。这就是所谓的测试。 ## 过于乐观的“测试” 训练集提供了一组非常吸引人的患者,我们在它们上测试我们的分类器,因为我们可以知道训练集中每个患者的分类。 但是,我们要小心,如果我们走这条道路,前面就会有隐患。一个例子会告诉我们为什么。 假设我们使用 1 邻近分类器,根据血糖和白细胞计数来预测患者是否患有慢性肾病。 ```py ckd.scatter('White Blood Cell Count', 'Glucose', colors='Color') ``` 之前,我们说我们预计得到一些分类错误,因为在左下方有一些蓝色和金色的点。 但是训练集中的点,也就是已经在散点图上的点呢?我们会把它们误分类吗? 答案是否。请记住,1 最近邻分类寻找训练集中离被分类点最近的点。那么,如果被分类的点已经在训练集中,那么它在训练集中的最近邻就是它自己!因此它将被划分为自己的颜色,这将是正确的,因为训练集中的每个点都已经被正确着色。 换句话说,如果我们使用我们的训练集来“测试”我们的 1 邻近分类器,分类器将以 100% 的几率内通过测试。 任务完成。多好的分类器! 不,不是。正如我们前面提到的,左下角的一个新点很容易被误分类。 “100% 准确”是一个很好的梦想,而它持续。 这个例子的教训是不要使用训练集来测试基于它的分类器。 ### 生成测试集 在前面的章节中,我们看到可以使用随机抽样来估计符合一定标准的总体中的个体比例。不幸的是,我们刚刚看到训练集不像所有患者总体中的随机样本,在一个重要的方面:我们的分类器正确猜测训练集中的个体,比例高于总体中的个体。 当我们计算数值参数的置信区间时,我们希望从一个总体中得到许多新的随机样本,但是我们只能访问一个样本。我们通过从我们的样本中自举重采样来解决这个问题。 我们将使用一个类似的想法来测试我们的分类器。我们将从原始训练集中创建两个样本,将其中一个样本作为我们的训练集,另一个用于测试。 所以我们将有三组个体: + 训练集,我们可以对它进行任何大量的探索来建立我们的分类器 + 一个单独的测试集,在它上面测试我们的分类器,看看分类的正确比例是多少 + 个体的底层总体,我们不了解它;我们的希望是我们的分类器对于这些个体也会成功,就像我们的测试集一样。 如何生成训练和测试集?你猜对了 - 我们会随机选择。 `ckd`有 158 个个体。让我们将它们随机的一半用于训练,另一半用于测试。为此,我们将打乱所有行,把前 79 个作为训练集,其余的 79 个用于测试。 ```py shuffled_ckd = ckd.sample(with_replacement=False) training = shuffled_ckd.take(np.arange(79)) testing = shuffled_ckd.take(np.arange(79, 158)) ``` 现在让我们基于训练样本中的点构造我们的分类器: ```py training.scatter('White Blood Cell Count', 'Glucose', colors='Color') plt.xlim(-2, 6) plt.ylim(-2, 6); ``` 我们得到以下分类区域和决策边界: 把测试数据放在这个图上,你可以立刻看到分类器对于几乎所有的点都正确,但也有一些错误。 例如,测试集的一些蓝点落在分类器的金色区域。 尽管存在一些错误,但分类器看起来在测试集上表现得相当好。 假设原始样本是从底层总体中随机抽取的,我们希望分类器在整个总体上具有相似的准确性,因为测试集是从原始样本中随机选取的。 ## 表的行 现在我们对最近邻分类有一个定性的了解,是时候实现我们的分类器了。 在本章之前,我们主要处理表格的单列。 但现在我们必须看看一个个体是否“接近”另一个个体。 个体数据包含在表格的行中。 那么让我们首先仔细看一下行。 这里是原始表格`ckd`,包含慢性肾病患者资料。 ```py ckd = Table.read_table('ckd.csv').relabeled('Blood Glucose Random', 'Glucose') ``` 对应第一个患者的数据在表中第 0 行,与 Python 的索引系统一致。 `Table`的`row`方法将行索引作为其参数来访问行。 ```py ckd.row(0) Row(Age=48, Blood Pressure=70, Specific Gravity=1.0049999999999999, Albumin=4, Sugar=0, Red Blood Cells='normal', Pus Cell='abnormal', Pus Cell clumps='present', Bacteria='notpresent', Glucose=117, Blood Urea=56, Serum Creatinine=3.7999999999999998, Sodium=111, Potassium=2.5, Hemoglobin=11.199999999999999, Packed Cell Volume=32, White Blood Cell Count=6700, Red Blood Cell Count=3.8999999999999999, Hypertension='yes', Diabetes Mellitus='no', Coronary Artery Disease='no', Appetite='poor', Pedal Edema='yes', Anemia='yes', Class=1) ``` 行拥有自己的数据类型:它们是行对象。 注意屏幕不仅显示行中的值,还显示相应列的标签。 行通常不是数组,因为它们的元素可以是不同的类型。 例如,上面那行的一些元素是字符串(如`'abnormal'`),有些是数字。 所以行不能被转换成数组。 但是,行与数组有一些特征。 您可以使用`item`来访问行中的特定元素。 例如,要访问患者 0 的白蛋白水平,我们可以查看上面那行的打印输出中的标签,发现它是第 3 项: ```py ckd.row(0).item(3) 4 ``` ### 将行转换为数组(可能的时候) 元素都是数字(或都是字符串)的行可以转换为数组。 将行转换为数组可以让我们访问算术运算和其他漂亮的 NumPy 函数,所以它通常很有用。 回想一下,在上一节中,我们试图根据血红蛋白和血糖两个属性将患者划分为“CKD”或“非 CKD”,这两个属性都是以标准单位测量的。 ```py ckd = Table().with_columns( 'Hemoglobin', standard_units(ckd.column('Hemoglobin')), 'Glucose', standard_units(ckd.column('Glucose')), 'Class', ckd.column('Class') ) color_table = Table().with_columns( 'Class', make_array(1, 0), 'Color', make_array('darkblue', 'gold') ) ckd = ckd.join('Class', color_table) ckd ``` | Class | Hemoglobin | Glucose | Color | | --- | --- | --- | --- | | 0 | 0.456884 | 0.133751 | gold | | 0 | 1.153 | -0.947597 | gold | | 0 | 0.770138 | -0.762223 | gold | | 0 | 0.596108 | -0.190654 | gold | | 0 | -0.239236 | -0.49961 | gold | | 0 | -0.0304002 | -0.159758 | gold | | 0 | 0.282854 | -0.00527964 | gold | | 0 | 0.108824 | -0.623193 | gold | | 0 | 0.0740178 | -0.515058 | gold | | 0 | 0.83975 | -0.422371 | gold | (省略了 148 行) 下面是两个属性的散点图,以及新患者 Alice 对应的红点。 她的血红蛋白值是 0(即平均值)和血糖为 1.1(即比平均值高 1.1 个 SD)。 ```py alice = make_array(0, 1.1) ckd.scatter('Hemoglobin', 'Glucose', colors='Color') plots.scatter(alice.item(0), alice.item(1), color='red', s=30); ``` 为了找到 Alice 点和其他点之间的距离,我们只需要属性的值: ```py ckd_attributes = ckd.select('Hemoglobin', 'Glucose') ckd_attributes ``` | Hemoglobin | Glucose | | --- | --- | | 0.456884 | 0.133751 | | 1.153 | -0.947597 | | 0.770138 | -0.762223 | | 0.596108 | -0.190654 | | -0.239236 | -0.49961 | | -0.0304002 | -0.159758 | | 0.282854 | -0.00527964 | | 0.108824 | -0.623193 | | 0.0740178 | -0.515058 | | 0.83975 | -0.422371 | (省略了 148 行) 每行由我们的训练样本中的一个点的坐标组成。 由于行现在只包含数值,因此可以将它们转换为数组。 为此,我们使用函数`np.array`,将任何类型的有序对象(如行)转换为数组。 (我们的老朋友`make_array`用于创建数组,而不是用于将其他类型的序列转换为数组。) ```py ckd_attributes.row(3) Row(Hemoglobin=0.59610766482326683, Glucose=-0.19065363034327712) np.array(ckd_attributes.row(3)) array([ 0.59610766, -0.19065363]) ``` 这非常方便,因为我们现在可以在每行的数据上使用数组操作了。 ### 只有两个属性时点的距离 我们需要做的主要计算是,找出 Alice 的点与其他点之间的距离。 为此,我们需要的第一件事就是计算任意一对点之间的距离。 我们如何实现呢? 在二维空间中,这非常简单。 如果我们在坐标`(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 - x1`和`y0 - y1`,我们想要求出斜边的长度。) 在下一节中,我们将看到,当存在两个以上的属性时,这个公式有个直接的扩展。 现在,让我们使用公式和数组操作来求出 Alice 和第 3 行病人的距离。 ```py patient3 = np.array(ckd_attributes.row(3)) alice, patient3 (array([ 0. , 1.1]), array([ 0.59610766, -0.19065363])) distance = np.sqrt(np.sum((alice - patient3)**2)) distance 1.4216649188818471 ``` 我们需要 Alice 和一堆点之间的距离,所以让我们写一个称为距离的函数来计算任意一对点之间的距离。 该函数将接受两个数组,每个数组包含一个点的`(x, y)`坐标。 (记住,那些实际上是患者的血红蛋白和血糖水平。) ```py def distance(point1, point2): """Returns the Euclidean distance between point1 and point2. Each argument is an array containing the coordinates of a point.""" return np.sqrt(np.sum((point1 - point2)**2)) distance(alice, patient3) 1.4216649188818471 ``` 我们已经开始建立我们的分类器:距离函数是第一个积木。 现在让我们来研究下一个片段。 ### 在整个行上使用`apply` 回想一下,如果要将函数应用于表的列的每个元素,一种方法是调用`table_name.apply(function_name, column_label)`。 当我们在列的每个元素上调用该函数时,它求值为由函数返回值组成的数组。所以数组的每个条目都基于表的相应行。 如果使用`apply`而不指定列标签,则整行将传递给该函数。 让我们在一个非常小的表格上,看看它的工作原理,表格包含训练样本中前五个患者的信息。 ```py t = ckd_attributes.take(np.arange(5)) t ``` | Hemoglobin | Glucose | | --- | --- | | 0.456884 | 0.133751 | | 1.153 | -0.947597 | | 0.770138 | -0.762223 | | 0.596108 | -0.190654 | | -0.239236 | -0.49961 | 举个例子,假设对于每个病人,我们都想知道他们最不寻常的属性是多么的不寻常。 具体而言,如果患者的血红蛋白水平远高于其血糖水平,我们想知道它离平均值有多远。 如果她的血糖水平远远高于她的血红蛋白水平,那么我们想知道它离平均值有多远。 这与获取两个量的绝对值的最大值是一样的。 为了为特定的行执行此操作,我们可以将行转换为数组并使用数组操作。 ```py def max_abs(row): return np.max(np.abs(np.array(row))) max_abs(t.row(4)) 0.49961028259186968 ``` 现在我们可以将`max_abs`应用于`t`表的每一行: ```py t.apply(max_abs) array([ 0.4568837 , 1.15300352, 0.77013762, 0.59610766, 0.49961028]) ``` 这种使用`apply`的方式帮助我们创建分类器的下一个积木。 ### Alice 的 K 最近邻 如果我们想使用 K 最近邻分类器来划分 Alice,我们必须确定她的 K 个最近邻。 这个过程中的步骤是什么? 假设`k = 5`。 然后这些步骤是: + 步骤 1:的是 Alice 与训练样本中每个点之间的距离。 + 步骤 2:按照距离的升序对数据表进行排序。 + 步骤 3:取得有序表的前 5 行。 步骤 2 和步骤 3 似乎很简单,只要我们有了距离。 那么我们来关注步骤 1。 这是爱丽丝: ```py alice array([ 0. , 1.1]) ``` 我们需要一个函数,它可以求出 Alice 和另一个点之间的距离,它的坐标包含在一行中。 `distance`函数返回任意两点之间的距离,他们的坐标位于数组中。 我们可以使用它来定义`distance_from_alice`,它将一行作为参数,并返回该行与 Alice 之间的距离。 ```py def distance_from_alice(row): """Returns distance between Alice and a row of the attributes table""" return distance(alice, np.array(row)) distance_from_alice(ckd_attributes.row(3)) 1.4216649188818471 ``` 现在我们可以调用`apply`,将`distance_from_alice`函数应用于`ckd_attributes`的每一行,第一步完成了。 ```py distances = ckd_attributes.apply(distance_from_alice) ckd_with_distances = ckd.with_column('Distance from Alice', distances) ckd_with_distances ``` | Class | Hemoglobin | Glucose | Color | Distance from Alice | | --- | --- | --- | --- | --- | | 0 | 0.456884 | 0.133751 | gold | 1.06882 | | 0 | 1.153 | -0.947597 | gold | 2.34991 | | 0 | 0.770138 | -0.762223 | gold | 2.01519 | | 0 | 0.596108 | -0.190654 | gold | 1.42166 | | 0 | -0.239236 | -0.49961 | gold | 1.6174 | | 0 | -0.0304002 | -0.159758 | gold | 1.26012 | | 0 | 0.282854 | -0.00527964 | gold | 1.1409 | | 0 | 0.108824 | -0.623193 | gold | 1.72663 | | 0 | 0.0740178 | -0.515058 | gold | 1.61675 | | 0 | 0.83975 | -0.422371 | gold | 1.73862 | (省略了 148 行) 对于步骤 2,让我们以距离的升序对表排序: ```py sorted_by_distance = ckd_with_distances.sort('Distance from Alice') sorted_by_distance ``` | Class | Hemoglobin | Glucose | Color | Distance from Alice | | --- | --- | --- | --- | --- | | 1 | 0.83975 | 1.2151 | darkblue | 0.847601 | | 1 | -0.970162 | 1.27689 | darkblue | 0.986156 | | 0 | -0.0304002 | 0.0874074 | gold | 1.01305 | | 0 | 0.14363 | 0.0874074 | gold | 1.02273 | | 1 | -0.413266 | 2.04928 | darkblue | 1.03534 | | 0 | 0.387272 | 0.118303 | gold | 1.05532 | | 0 | 0.456884 | 0.133751 | gold | 1.06882 | | 0 | 0.178436 | 0.0410639 | gold | 1.07386 | | 0 | 0.00440582 | 0.025616 | gold | 1.07439 | | 0 | -0.169624 | 0.025616 | gold | 1.08769 | (省略了 148 行) 步骤 3:前五行对应 Alice 的五个最近邻;你可以将五替换为任意正整数。 ```py alice_5_nearest_neighbors = sorted_by_distance.take(np.arange(5)) alice_5_nearest_neighbors ``` | Class | Hemoglobin | Glucose | Color | Distance from Alice | | --- | --- | --- | --- | --- | | 1 | 0.83975 | 1.2151 | darkblue | 0.847601 | | 1 | -0.970162 | 1.27689 | darkblue | 0.986156 | | 0 | -0.0304002 | 0.0874074 | gold | 1.01305 | | 0 | 0.14363 | 0.0874074 | gold | 1.02273 | | 1 | -0.413266 | 2.04928 | darkblue | 1.03534 | 爱丽丝五个最近邻中有三个是蓝点,两个是金点。 所以 5 邻近的分类器会把爱丽丝划分为蓝色:它可能预测爱丽丝有慢性肾病。 下面的图片放大了爱丽丝和她五个最近邻。 这两个金点就在红点正下方的圆圈内。 分类器说,爱丽丝更像她身边的三个蓝点。 我们正在实现我们的 K 最近邻分类器。 在接下来的两节中,我们将把它放在一起并评估其准确性。