15.md 19.6 KB
Newer Older
W
ch15.  
wizardforcel 已提交
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
# 十五、分类

[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 行)

W
ch15.  
wizardforcel 已提交
56
一些变量是类别(像“异常”这样的词),还有一些是定量的。 定量变量都有不同的规模。 我们将要通过眼睛比较和估计距离,所以我们只选择一些变量并在标准单位下工作。 之后我们就不用担心每个变量的规模。
W
ch15.  
wizardforcel 已提交
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83

```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 行)

W
ch15.  
wizardforcel 已提交
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164
我们来看两列,(病人的血液中)血红蛋白水平和血糖水平(一天中的随机时间;没有专门为血液测试禁食)。

我们将绘制一个散点图来显示两个变量之间的关系。 蓝点是 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`个患者,因此使用他们的诊断来预测爱丽丝的诊断似乎是合理的。

W
ch15.  
wizardforcel 已提交
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239
## 训练和测试

我们最近的邻居分类器有多好?要回答这个问题,我们需要知道我们的分类有多正确。如果患者患有慢性肾脏疾病,那么我们的分类器有多可能将其选出来呢?

如果病人在我们的训练集中,我们可以立即找到。我们已经知道病人位于什么类别,所以我们可以比较我们的预测和病人的真实类别。

但是分类器的重点在于对未在训练集中的新患者进行预测。我们不知道这些病人位于什么类别,但我们可以根据分类器做出预测。如何知道预测是否正确?

一种方法是等待患者之后的医学检查,然后检查我们的预测是否与检查结果一致。用这种方法,当我们可以说我们的预测有多准确的时候,它就不再能用于帮助病人了。

相反,我们将在一些真实类别已知的病人上尝试我们的分类器。然后,我们将计算分类器正确的时间比例。这个比例将作为我们分类器准确预测的所有新患者的比例的估计值。这就是所谓的测试。

## 过于乐观的“测试”

训练集提供了一组非常吸引人的患者,我们在它们上测试我们的分类器,因为我们可以知道训练集中每个患者的分类。

但是,我们要小心,如果我们走这条道路,前面就会有隐患。一个例子会告诉我们为什么。

假设我们使用 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);
```

我们得到以下分类区域和决策边界:

把测试数据放在这个图上,你可以立刻看到分类器对于几乎所有的点都正确,但也有一些错误。 例如,测试集的一些蓝点落在分类器的金色区域。

尽管存在一些错误,但分类器看起来在测试集上表现得相当好。 假设原始样本是从底层总体中随机抽取的,我们希望分类器在整个总体上具有相似的准确性,因为测试集是从原始样本中随机选取的。