# 十五、分类 [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`个患者,因此使用他们的诊断来预测爱丽丝的诊断似乎是合理的。