diff --git a/SUMMARY.md b/SUMMARY.md index 3a3171fc845138f2bc6e5a1a58d75d45965ffba5..a8bb2cd6863bb81a6ee1341c03c90c992c338f07 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -323,3 +323,36 @@ + [五、强化学习](docs/ml-for-humans/5.md) + [六、最好的机器学习资源](docs/ml-for-humans/6.md) + [机器学习超级复习笔记](docs/super-machine-learning-revision-notes/README.md) ++ [Python 自然语言处理 第二版](docs/nlp-py-2e/README.md) + + [前言](docs/nlp-py-2e/0.md) + + [1 语言处理与 Python](docs/nlp-py-2e/1.md) + + [2 获得文本语料和词汇资源](docs/nlp-py-2e/2.md) + + [3 处理原始文本](docs/nlp-py-2e/3.md) + + [4 编写结构化程序](docs/nlp-py-2e/4.md) + + [5 分类和标注词汇](docs/nlp-py-2e/5.md) + + [6 学习分类文本](docs/nlp-py-2e/6.md) + + [7 从文本提取信息](docs/nlp-py-2e/7.md) + + [8 分析句子结构](docs/nlp-py-2e/8.md) + + [9 构建基于特征的语法](docs/nlp-py-2e/9.md) + + [10 分析句子的意思](docs/nlp-py-2e/10.md) + + [11 语言学数据管理](docs/nlp-py-2e/11.md) + + [后记:语言的挑战](docs/nlp-py-2e/12.md) + + [索引](docs/nlp-py-2e/14.md) ++ [计算与推断思维](docs/data8-textbook-zh/README.md) + + [一、数据科学](docs/data8-textbook-zh/1.md) + + [二、因果和实验](docs/data8-textbook-zh/2.md) + + [三、Python 编程](docs/data8-textbook-zh/3.md) + + [四、数据类型](docs/data8-textbook-zh/4.md) + + [五、表格](docs/data8-textbook-zh/5.md) + + [六、可视化](docs/data8-textbook-zh/6.md) + + [七、函数和表格](docs/data8-textbook-zh/7.md) + + [八、随机性](docs/data8-textbook-zh/8.md) + + [九、经验分布](docs/data8-textbook-zh/9.md) + + [十、假设检验](docs/data8-textbook-zh/10.md) + + [十一、估计](docs/data8-textbook-zh/11.md) + + [十二、为什么均值重要](docs/data8-textbook-zh/12.md) + + [十三、预测](docs/data8-textbook-zh/13.md) + + [十四、回归的推断](docs/data8-textbook-zh/14.md) + + [十五、分类](docs/data8-textbook-zh/15.md) + + [十六、比较两个样本](docs/data8-textbook-zh/16.md) + + [十七、更新预测](docs/data8-textbook-zh/17.md) diff --git a/docs/data8-textbook-zh/1.md b/docs/data8-textbook-zh/1.md new file mode 100644 index 0000000000000000000000000000000000000000..9e2d40aa2a3c523b7c0f54a9121f4f0292e192af --- /dev/null +++ b/docs/data8-textbook-zh/1.md @@ -0,0 +1,257 @@ +# 一、数据科学 + +> 原文:[Data Science](https://github.com/data-8/textbook/tree/gh-pages/chapters/01) + +> 译者:[飞龙](https://github.com/wizardforcel) + +> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) + +> 自豪地采用[谷歌翻译](https://translate.google.cn/) + +## 什么是数据科学 + +数据科学是通过探索,预测和推断,从大量不同的数据集中得出有用的结论。探索涉及识别信息中的规律。预测涉及使用我们所知道的信息,对我们希望知道的值作出知情的猜测。推断涉及量化我们的确定程度:我们发现的这些规律是否也出现在新的观察中?我们的预测有多准确?我们用于探索的主要工具是可视化和描述性统计,用于预测的是机器学习和优化,用于推理的是统计测试和模型。 + +统计学是数据科学的核心部分,因为统计学研究,如何用不完整的信息做出有力的结论。计算是一个重要组成部分,因为编程允许我们将分析技巧应用于大量不同的数据集,它们在真实应用中出现:不仅包括数字,还包括文本,图像,视频和传感器读数。数据科学就是所有这些东西,但是由于应用的原因,它不仅仅是其部分的总和。通过理解一个特定的领域,数据科学家学习提出有关他们的数据的适当的问题,并正确地解释我们的推理和计算工具提供的答案。 + +## 简介 + +数据是对我们周围世界的描述,通过观察来收集并存储在计算机上。计算机使我们能够从这些描述中推断出世界的特性。数据科学是使用计算从数据中得出结论的学科。有效的数据分析有三个核心方面:探索,预测和推理。本文对三者进行了一致的研究,同时介绍了统计思想和计算机科学的基本思想。我们专注于一套最小的核心技巧,应用于广泛的实际应用。数据科学的基础不仅需要理解统计和计算技巧,还需要认识到它们如何应用于真实场景。 + +对于我们希望研究的世界的任何方面,无论是地球气象,世界市场,政治民意调查还是人类思想,我们收集的数据通常都提供了这个主题的不完整描述。数据科学的核心挑战是使用这部分信息作出可靠的结论。 + +在这个努力中,我们将结合两个基本工具:计算和随机化。例如,我们可能想使用温度观测来了解气候变化的趋势。计算机允许我们使用所有可用的信息得出结论。我们不仅仅关注一个地区的平均气温,而是将整个温度的范围一起考虑,来构建更加细致的分析。随机性允许我们考虑许多不同方式,来完善不完整的信息。我们不会假设温度会以某种特定的方式变化,而是学习使用随机性来设想许多可能的情景,这些情景都与我们观察到的数据一致。 + +应用这种方法需要学习,如何为一台计算机编程,所以这个文本穿插了编程的完整介绍,并假设没有任何先验知识。 具有编程经验的读者会发现,我们涵盖了计算中的几个主题,这些主题并没有出现在典型的计算机科学课程中。 数据科学也需要对数量进行仔细的推理,但是本书并不假设超出基本代数的数学或统计背景。 在本文中你会发现很少的方程。 相反,技巧使用一种编程语言描述,对于读者和执行它们的计算机来说,是相同的。 + +### 计算工具 + +本文使用 Python 3 编程语言,以及数值和数据可视化的标准工具集,它们在商业应用,科学实验和开源项目中广泛使用。 Python 已经招募了许多专业人士,它们使用数据得出结论。通过学习 Python 语言,你将加入一个拥有百万人口的,软件开发人员和数据科学家社区。 + +入门。开始用 Python 编写程序的最简单和推荐的方法是,登录到本文的配套网站 。如果你拥有`@ berkeley.edu`电子邮件地址,则你已经可以完全访问该网站上托管的编程环境。如果没有,请填写[此表格](https://goo.gl/forms/saQpxdqzS2rKxjTc2)来申请访问。 + +你不能完全仅仅使用这个基于 Web 的编程环境。 Python 程序可以由任何计算机执行,无论其制造商或操作系统如何,只要安装了该语言的支持。如果你希望安装符合本文的 Python 版本及其附带库,我们推荐将 Anaconda 发行版与 Python 3 语言解释器,IPython 库和 Jupyter 笔记本环境打包在一起。 + +本文包括所有这些计算工具的完整介绍。你将学习编写程序,从数据生成图像,并使用在线发布的真实世界的数据集。 + +### 统计技巧 + +统计学科长期以来一直面临与数据科学相同的根本挑战:如何使用不完整的信息得出有关世界的有力结论。统计学最重要的贡献之一是,用于描述观察与结论之间关系的,一致而准确的词汇。本文继续保持同样的传统,重点是统计学中的一组核心推断问题:假设检验,置信度估计和未知量预测。 + +数据科学通过充分利用计算,数据可视化,机器学习,优化和信息访问来扩展统计领域。快速计算机和互联网的结合使得任何人都能够访问和分析大量的数据集:数百万篇新闻文章,完整的百科全书,任何领域的数据库以及大量的音乐,照片和视频库。 + +真实数据集的应用激发了我们在整个文本中描述的统计技巧。真实数据通常没有规律或匹配标准方程。如果把过多的注意力集中在简单的总结上,比如平均值,那么真实数据中有趣的变化就会丢失。计算机使一系列基于重采样的方法成为可能,它们适用于各种不同的推理问题,考虑了所有可用的信息,并且需要很少的假设或条件。虽然这些技巧经常留作统计学的研究生课程,但它们的灵活性和简单性非常适合数据科学应用。 + +## 为什么是数据科学 + +最重要的决策仅仅使用部分信息和不确定的结果做出。然而,许多决策的不确定性,可以通过获取大量公开的数据集和有效分析所需的计算工具,而大幅度降低。以数据为导向的决策已经改变了一大批行业,包括金融,广告,制造业和房地产。同时,大量的学科正在迅速发展,将大规模的数据分析纳入其理论和实践。 + +学习数据科学使个人能够将这些技巧用于工作,科学研究和个人决策。批判性思维一直是严格教育的标志,但在数据支持下,批判往往是最有效的。对世界任何方面的批判性分析,可能是商业或社会科学,涉及归纳推理;结论很少直接证明,仅仅由现有的证据支持。数据科学提供了手段,对任何一组观测结果进行精确,可靠和定量的论证。有了信息和计算机的前所未有的访问,如果没有有效的推理技巧,对世界上任何可以衡量的方面的批判性思考都是不完整的。 + +世界上有太多没有答案的问题和困难的挑战,所以不能把这个批判性的推理留给少数专家。所有受过教育的社会成员都可以建立推断数据的能力。这些工具,技巧和数据集都是随手可用的;本文的目的是使所有人都能访问它们。 + +## 绘制经典作品 + +在这个例子中,我们将探讨两个经典小说的统计:马克吐温(Mark Twain)的《哈克贝利·芬历险记》(The Adventures of Huckleberry Finn)和路易莎·梅·奥尔科特(Louisa May Alcott)的《小女人》(Little Women)。任何一本书的文本都可以通过电脑以极快的速度读取。 1923 年以前出版的书籍目前处于公有领域,这意味着每个人都有权以任何方式复制或使用文本。 [古登堡计划](http://www.gutenberg.org/)是一个在线出版公共领域书籍的网站。使用 Python,我们可以直接从网络上加载这些书籍的文本。 + +这个例子是为了说明本书的一些广泛的主题。如果还不理解程序的细节,别担心。相反,重点关注下面生成的图像。后面的部分将介绍下面使用的 Python 编程语言的大部分功能。 + +首先,我们将这两本书的内容读入章节列表中,称为`huck_finn_chapters`和`little_women_chapters`。在 Python 中,名称不能包含任何空格,所以我们经常使用下划线`_`来代表空格。在下面的行中,左侧提供了一个名称,右侧描述了一些计算的结果。统一资源定位符或 URL 是互联网上某些内容的地址;这里是一本书的文字。`#`符号是注释的起始,计算机会忽略它,但有助于人们阅读代码。 + +```py +# Read two books, fast! + +huck_finn_url = 'https://www.inferentialthinking.com/chapters/01/3/huck_finn.txt' +huck_finn_text = read_url(huck_finn_url) +huck_finn_chapters = huck_finn_text.split('CHAPTER ')[44:] + +little_women_url = 'https://www.inferentialthinking.com/chapters/01/3/little_women.txt' +little_women_text = read_url(little_women_url) +little_women_chapters = little_women_text.split('CHAPTER ')[1:] +``` + +虽然计算机不能理解书的文本,它可以向我们提供文本结构的一些视角。名称`huck_finn_chapters`现在已经绑定到书中章节的列表。我们可以将其放到一个表中,来观察每一章的开头。 + +```py +# Display the chapters of Huckleberry Finn in a table. + +Table().with_column('Chapters', huck_finn_chapters) +``` + +| Chapters | +| --- | +| I. YOU don't know about me without you have read a book ... | +| II. WE went tiptoeing along a path amongst the trees bac ... | +| III. WELL, I got a good going-over in the morning from o ... | +| IV. WELL, three or four months run along, and it was wel ... | +| V. I had shut the door to. Then I turned around and ther ... | +| VI. WELL, pretty soon the old man was up and around agai ... | +| VII. "GIT up! What you 'bout?" I opened my eyes and look ... | +| VIII. THE sun was up so high when I waked that I judged ... | +| IX. I wanted to go and look at a place right about the m ... | +| X. AFTER breakfast I wanted to talk about the dead man a ... | + +(已省略 33 行) + +每一章都以章节号开头,以罗马数字的形式,后面是本章的第一个句子。古登堡计划将每章的第一个单词变为大写。 + +### 文本特征 + +《哈克贝利·芬历险记》描述了哈克和吉姆沿着密西西比河的旅程。汤姆·索亚(Tom Sawyer)在行动进行的时候加入了他们的行列。在加载文本后,我们可以快速地看到这些字符在本书的任何一处被提及的次数。 + +```py +# Count how many times the names Jim, Tom, and Huck appear in each chapter. + +counts = Table().with_columns([ + 'Jim', np.char.count(huck_finn_chapters, 'Jim'), + 'Tom', np.char.count(huck_finn_chapters, 'Tom'), + 'Huck', np.char.count(huck_finn_chapters, 'Huck') + ]) + +# Plot the cumulative counts: +# how many times in Chapter 1, how many times in Chapters 1 and 2, and so on. + +cum_counts = counts.cumsum().with_column('Chapter', np.arange(1, 44, 1)) +cum_counts.plot(column_for_xticks=3) +plots.title('Cumulative Number of Times Each Name Appears', y=1.08); +``` + +![](img/1-1.png) + +在上图中,横轴显示章节号,纵轴显示每个字符在该章节被提及到的次数。 + +吉姆是核心人物,它的名字出现了很多次。请注意,第 30 章中汤姆出现并加入了哈克和吉姆,在此之前,汤姆在本书中几乎没有提及。他和吉姆的曲线在这个位置上迅速上升,因为涉及两者的行动都在变多。至于哈克,他的名字几乎没有出现,因为他是叙述者。 + +《小女人》是南北战争期间四个姐妹一起长大的故事。 在这本书中,章节号码拼写了出来,章节标题用大写字母表示。 + +```py +# The chapters of Little Women, in a table + +Table().with_column('Chapters', little_women_chapters) +``` + +| Chapters | +| --- | +| ONE PLAYING PILGRIMS "Christmas won't be Christmas witho ... | +| TWO A MERRY CHRISTMAS Jo was the first to wake in the gr ... | +| THREE THE LAURENCE BOY "Jo! Jo! Where are you?" cried Me ... | +| FOUR BURDENS "Oh, dear, how hard it does seem to take up ... | +| FIVE BEING NEIGHBORLY "What in the world are you going t ... | +| SIX BETH FINDS THE PALACE BEAUTIFUL The big house did pr ... | +| SEVEN AMY'S VALLEY OF HUMILIATION "That boy is a perfect ... | +| EIGHT JO MEETS APOLLYON "Girls, where are you going?" as ... | +| NINE MEG GOES TO VANITY FAIR "I do think it was the most ... | +| TEN THE P.C. AND P.O. As spring came on, a new set of am ... | + +(已省略 37 行) + +我们可以跟踪主要人物的提及,来了解本书的情节。 主角乔(Jo)和她的姐妹梅格(Meg),贝丝(Beth)和艾米(Amy)经常互动,直到第 27 章中她独自搬到纽约。 + +```py +# Counts of names in the chapters of Little Women + +counts = Table().with_columns([ + 'Amy', np.char.count(little_women_chapters, 'Amy'), + 'Beth', np.char.count(little_women_chapters, 'Beth'), + 'Jo', np.char.count(little_women_chapters, 'Jo'), + 'Meg', np.char.count(little_women_chapters, 'Meg'), + 'Laurie', np.char.count(little_women_chapters, 'Laurie'), + + ]) + +# Plot the cumulative counts. + +cum_counts = counts.cumsum().with_column('Chapter', np.arange(1, 48, 1)) +cum_counts.plot(column_for_xticks=5) +plots.title('Cumulative Number of Times Each Name Appears', y=1.08); +``` + +![](img/1-2.png) + +劳里(Laurie)是个年轻人,最后和其中一个女孩结婚。 看看你是否可以使用这个图来猜测是哪一个。 + +### 另一种文本特征 + +在某些情况下,数量之间的关系能让我们做出预测。 本文将探讨如何基于不完整的信息做出准确的预测,并研究结合多种不确定信息来源进行决策的方法。 + +作为从多个来源获取信息的可视化的例子,让我们首先使用计算机来获取一些信息,它们通常手工难以获取。在小说的语境中,“特征”(Character)这个词有第二个含义:一个印刷符号,如字母,数字或标点符号。 在这里,我们要求计算机来计算《哈克贝利·芬》和《小女人》的每章中的字符和句号数量。 + +```py +# In each chapter, count the number of all characters; +# call this the "length" of the chapter. +# Also count the number of periods. + +chars_periods_huck_finn = Table().with_columns([ + 'Huck Finn Chapter Length', [len(s) for s in huck_finn_chapters], + 'Number of Periods', np.char.count(huck_finn_chapters, '.') + ]) +chars_periods_little_women = Table().with_columns([ + 'Little Women Chapter Length', [len(s) for s in little_women_chapters], + 'Number of Periods', np.char.count(little_women_chapters, '.') + ]) +``` + + +这里是《哈克贝利·芬》的数据。 表格的每一行对应小说的一个章节,并显示章节中的字符和句号数量。 毫不奇怪,字符少的章节往往句号也少,一般来说 - 章节越短,句子越少,反之亦然。 然而,这种关系并不是完全可以预测的,因为句子的长度各不相同,还可能涉及其他标点符号,例如问号。 + +`chars_periods_huck_finn` + + +| 《哈克贝利·芬》章节长度 | 句号数量 | +| --- | --- | +| 7026 | 66 | +| 11982 | 117 | +| 8529 | 72 | +| 6799 | 84 | +| 8166 | 91 | +| 14550 | 125 | +| 13218 | 127 | +| 22208 | 249 | +| 8081 | 71 | +| 7036 | 70 | + +(已省略 33 行) + +这里是《小女人》的对应数据: + +`chars_periods_little_women` + +| 《小女人》章节长度 | 句号数量 | +| --- | --- | +| 21759 | 189 | +| 22148 | 188 | +| 20558 | 231 | +| 25526 | 195 | +| 23395 | 255 | +| 14622 | 140 | +| 14431 | 131 | +| 22476 | 214 | +| 33767 | 337 | +| 18508 | 185 | + +(已省略 37 行) + + +你可以看到,《小女人》的章节总的来说比《哈克贝利·芬》的章节要长。让我们来看看这两个简单的变量 - 每一章的长度和句子数量 - 能否告诉我们这两本书的更多内容。 我们实现它的一个方法是在同一个图上绘制两组数据。 + +在下面的图中,每本书的每一章都有一个点。 蓝色圆点对应于《哈克贝利·芬》,金色圆点对应于《小女人》。横轴表示句号数量,纵轴表示字符数。 + +```py +plots.figure(figsize=(6, 6)) +plots.scatter(chars_periods_huck_finn.column(1), + chars_periods_huck_finn.column(0), + color='darkblue') +plots.scatter(chars_periods_little_women.column(1), + chars_periods_little_women.column(0), + color='gold') +plots.xlabel('Number of periods in chapter') +plots.ylabel('Number of characters in chapter'); +``` + +![](img/1-3.png) + +这个绘图向我们展示,《小女人》的许多章节,而不是所有章节都比《哈克贝利·芬》的章节长,正如我们通过查看数字所看到的那样。 但它也向我们展示了更多东西。 请注意,蓝点粗略聚集在一条直线上,黄点也是如此。 此外看起来,两种颜色的点可能聚集在同一条直线上。 + +现在查看包含大约 100 个句号的所有章节。 绘图显示,这些章节大致包含约 10,000 个字符到约 15,000 个字符。每个句子大约有 100 到 150 个字符。 + +事实上,从这个绘图看来,这两本书的两个句号之间平均有 100 到 150 个字符,这是一个非常粗略的估计。 也许这两个伟大的 19 世纪小说正在表明我们现在非常熟悉的东西:Twitter 的 140 个字符的限制。 diff --git a/docs/data8-textbook-zh/10.md b/docs/data8-textbook-zh/10.md new file mode 100644 index 0000000000000000000000000000000000000000..8faf90fe8124658b588b146aae514be5391d0c51 --- /dev/null +++ b/docs/data8-textbook-zh/10.md @@ -0,0 +1,1140 @@ +# 十、假设检验 + +> 原文:[Testing Hypotheses](https://github.com/data-8/textbook/tree/gh-pages/chapters/10) + +> 译者:[飞龙](https://github.com/wizardforcel) + +> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) + +> 自豪地采用[谷歌翻译](https://translate.google.cn/) + +数据科学家们经常面对世界的是或不是的问题。你在这个课程中看到了一些这样的问题的例子: + ++ 巧克力对你有好处吗? ++ Broad Street 水泵的水是否会导致霍乱? ++ 加州的人口统计在过去的十年中有所改变吗? + +我们是否回答这些问题取决于我们的数据。加州的人口普查数据可以解决人口统计的问题,而答案几乎没有任何不确定性。我们知道 Broad Street 水泵的水源受到霍乱病人的污染,所以我们可以很好地猜测它是否会引起霍乱。 + +巧克力还是其他任何实验对你有好处,几乎肯定要由医学专家来决定,但是第一步是使用数据科学分析来自研究和随机实验的数据。 + +在本章中,我们将试图回答这样的问题,根据样本和经验分布的结论。我们将以北加利福尼亚州公民自由联盟(ACLU)2010 年进行的一项研究为例。 + +## 陪审团选拔 + +2010 年,ACLU 在加利福尼亚州阿拉米达县提交了一份陪审团选择的报告。报告得出的结论是,在阿拉米达县的陪审团小组成员中,某些族裔人数不足,并建议对专家组进行一些改革,来合理分配陪审员。在本节中,我们将自己分析数据,并检查出现的一些问题。 + +### 陪审团 + +陪审团是一群被选为准陪审员的人;终审的陪审团是从他们中挑选的。陪审团可以由几十人或几千人组成,具体情况取决于审判情况。根据法律,陪审团应该是审判所在社区的代表。加州“民事诉讼法(California's Code of Civil Procedure)”第 197 条规定:“All persons selected for jury service shall be selected at random, from a source or sources inclusive of a representative cross section of the population of the area served by the court.” + +最终的陪审团是通过故意纳入或排除,从陪审团中挑选出来的。法律允许潜在的陪审员出于医疗原因而被免责;双方的律师可以从名单上挑选一些潜在的陪审员进行所谓的“先制性反对(peremptory challenges)”。初审法官可以根据陪审团填写的问卷进行选择;等等。但最初的陪审团似乎是合格陪审员的总体的随机样本。 + +### 阿拉米达县的陪审团构成 + +ACLU 的研究重点是阿拉米达县陪审团的种族组成。 ACLU 编辑了 2009 年和 2010 年在阿拉米达县进行的 11 次重罪审判中陪审团的种族组成的数据。在这些陪审团中,报告出庭的陪审员的总人数是 1453 人。ACLU 收集了所有人口的统计数据,并将这些数据与该县所有合格陪审员的组成进行比较。 + +数据列在下面的表格中,称为`jury`。 对于每个种族来说,第一个值就是该种族所有合格的陪审员候选人的比例。 第二个值是出现在出现在陪审团选拔过程的人中,那个种族的人的比例。 + +```py +jury = Table().with_columns( + 'Ethnicity', make_array('Asian', 'Black', 'Latino', 'White', 'Other'), + 'Eligible', make_array(0.15, 0.18, 0.12, 0.54, 0.01), + 'Panels', make_array(0.26, 0.08, 0.08, 0.54, 0.04) +) + +jury +``` + + +| Ethnicity | Eligible | Panels | +| --- | --- | --- | +| Asian | 0.15 | 0.26 | +| Black | 0.18 | 0.08 | +| Latino | 0.12 | 0.08 | +| White | 0.54 | 0.54 | +| Other | 0.01 | 0.04 | + +研究中的一些种族代表性过多,一些代表性不足。 条形图有助于显示差异。 + +```py +jury.barh('Ethnicity') +``` + +![](img/10-1.png) + +### 两个分布的距离 + +可视化使我们能够快速了解,两个分布之间的相似性和差异。 为了更准确地说出这些差异,我们必须首先量化两个分布之间的差异。 这将使我们的分析能够基于更多东西,不仅仅是我们能够通过眼睛做出的评估。 + +为了测量两个分布之间的差异,我们将计算一个数量,称之为它们之间的总变异距离(total variation distance)。 + +为了计算总变异距离,我们首先考虑每个类别中两个比例之间的差异。 + +```py +# Augment the table with a column of differences between proportions + +jury_with_diffs = jury.with_column( + 'Difference', jury.column('Panels') - jury.column('Eligible') +) +jury_with_diffs +``` + + +| Ethnicity | Eligible | Panels | Difference | +| --- | --- | --- | --- | +| Asian | 0.15 | 0.26 | 0.11 | +| Black | 0.18 | 0.08 | -0.1 | +| Latino | 0.12 | 0.08 | -0.04 | +| White | 0.54 | 0.54 | 0 | +| Other | 0.01 | 0.04 | 0.03 | + +```py +jury_with_diffs.column('Abs. Difference').sum()/2 +0.14000000000000001 +``` + +这个数量 0.14 是合格陪审员总体中种族分布与陪审团分布情况之间的总变异距离(TVD)。 + +只要加上正的差异,我们就可以得到相同的结果。 但是,我们的方法包含所有绝对差异,不需要追踪哪些差异是正的而哪些不是。 + +### 计算 TVD 的函数 + +函数`total_variation_distance`返回两个数组中的分布的 TVD。 + +```py +def total_variation_distance(distribution_1, distribution_2): + return np.abs(distribution_1 - distribution_2).sum()/2 +``` + +函数`table_tvd `使用函数`total_variation_distance`来返回表的两列中的分布的 TVD。 + +```py +def table_tvd(table, label, other): + return total_variation_distance(table.column(label), table.column(other)) + +table_tvd(jury, 'Eligible', 'Panels') +0.14000000000000001 +``` + +### 陪审团是否是总体的代表? + +现在我们将转到合格的陪审员和陪审团的 TVD 的值。我们如何解释 0.14 的距离呢?要回答这个问题,请回想一下,陪审团应该是随机选择的。因此,将 0.14 的值与合格的陪审员和随机选择的陪审团的 TVD 进行比较,会有帮助。 + +为了这样做,我们将在模拟中使用我们的技能。研究共有 1453 名准陪审员。所以让我们从合格的陪审员的总体中随机抽取大小为 1453 的样本。 + +技术注解。准陪审员的随机样本将会不放回地选中。但是,如果样本的大小相对于总体的大小较小,那么无放回的取样类似于放回的取样;总体中的比例在几次抽取之间变化不大。阿拉米达县的合格陪审员的总体超过一百万,与此相比,约 1500 人的样本量相当小。因此,我们将带放回地抽样。 + +### 从合格的陪审员中随机抽样 + +到目前为止,我们已经使用`np.random.choice`从数组元素中随机抽样,并使用`sample `对表的行进行抽样。 但是现在我们必须从一个分布中抽样:一组种族以及它们的比例。 + +为此,我们使用函数`proportions_from_distribution`。 它有三个参数: + ++ 表名 ++ 包含比例的列的标签 ++ 样本大小 + +该函数执行带放回地随机抽样,并返回一个新的表,该表多出了一列`Random Sample`,是随机样本中所出现的比例。 + +所有陪审团的总大小是 1453,所以让我们把这个数字赋给给一个名成,然后调用: + +```py +proportions_from_distribution. + +panel_size = 1453 +panels_and_sample = proportions_from_distribution(jury, 'Eligible', panel_size) +panels_and_sample +``` + + +| Ethnicity | Eligible | Panels | Random Sample | +| --- | --- | --- | --- | +| Asian | 0.15 | 0.26 | 0.14797 | +| Black | 0.18 | 0.08 | 0.193393 | +| Latino | 0.12 | 0.08 | 0.116311 | +| White | 0.54 | 0.54 | 0.532691 | +| Other | 0.01 | 0.04 | 0.00963524 | + +从结果中可以清楚地看出,随机样本的分布与合格总体的分布非常接近,与陪审团的分布不同。 + +和之前一样,可视化会有帮助。 + +```py +panels_and_sample.barh('Ethnicity') +``` + +![](img/10-2.png) + +灰色条形与蓝色条形比金色条形更接近。 随机样本类似于合格的总体,而不是陪审团。 + +我们可以通过计算合格总体的分布与随机样本之间的 TVD,来量化这一观察结果。 + +```py +table_tvd(panels_and_sample, 'Eligible', 'Random Sample') +0.013392980041293877 +``` + +将其与陪审团的距离 0.14 进行比较,可以看到我们在条形图中看到的数值。 合格总体与陪审团之间的 TVD 为 0.14,但合格总体与随机样本之间的 TVD 小得多。 + +当然,随机样本和合格陪审员的分布之间的距离取决于样本。 再次抽样可能会给出不同的结果。 + +### 随机样本和总体之间有多少差异? + +随机样本与合格陪审员的分布之间的 TVD,是我们用来衡量两个分布之间距离的统计量。 通过重复抽样过程,我们可以看到不同随机样本的统计量是多少。 下面的代码根据抽样过程的大量重复,来计算统计量的经验分布。 + +```py +# Compute empirical distribution of TVDs + +panel_size = 1453 +repetitions = 5000 + +tvds = make_array() + +for i in np.arange(repetitions): + + new_sample = proportions_from_distribution(jury, 'Eligible', panel_size) + tvds = np.append(tvds, table_tvd(new_sample, 'Eligible', 'Random Sample')) + +results = Table().with_column('TVD', tvds) +results +``` + + +| TVD | +| --- | +| 0.0247075 | +| 0.0141569 | +| 0.0138403 | +| 0.0214384 | +| 0.012278 | +| 0.017309 | +| 0.0219752 | +| 0.0192017 | +| 0.02351 | +| 0.00818995 | + +(省略了 4990 行) + +上面每一行包含大小为 1453 的随机样本与合格的陪审员的 TVD。 + +这一列的直方图显示,从合格候选人中随机抽取 1453 名陪审员的结果是,偏离合格陪审员的种族分布的分布几乎不超过 0.05。 + +```py +results.hist(bins=np.arange(0, 0.2, 0.005)) +``` + +![](img/10-3.png) + +### 陪审团和随机样本比如何? + +然而,研究中的陪审团与合格总体并不十分相似。陪审团和总体之间的 TVD 是 0.14,这距离上面的直方图的尾部很远。这看起来不像是随机样本和合格总体之间的典型距离。 + +所以我们的分析支持 ACLU 的计算,即陪审团不是合格陪审员的分布的代表。然而,与大多数这样的分析一样,它并没有说明分布为什么不同,或者差异可能暗示了什么。 + +ACLU 报告讨论了这些差异的几个可能的原因。例如,一些少数群体在选民登记记录和机动车辆部门(选择陪审员的两个主要来源)的代表性不足。在进行研究时,该县没有一个有效的程序,用于跟踪那些被选中但未出庭的准陪审员。ACLU 列举了其他几个原因。不管出于何种原因,似乎很明显,陪审团的组成与我们对随机样本的预期不同,它来自`Eligible`列的分布。 + +### 数据上的问题 + +我们已经开发出一种强大的技术,来帮助决定一个分布是否像另一个分布的随机样本。但是数据科学不仅仅是技术。特别是数据科学总是需要仔细研究如何收集数据。 + +合格的陪审员。首先,重要的是要记住,不是每个人都有资格担任陪审团的职位。阿拉米达县高级法院在其网站上说:“如果你是18 岁的美国公民,和传召所在的县或区的居民,你可能会被要求担任职位。你必须能够理解英语,身体上和精神上都有能力担任,此外,你在过去 12 个月内不得担任任何类型的陪审员,也没有被判重罪。 + +人口普查没有保存所有这些类别的人口记录。因此 ACLU 必须以其他方式获得合格陪审员的人口统计资料。以下是他们对自己所遵循的过程的描述,以及它可能包含的一些缺陷。 + +“为了确定阿拉米达县具有陪审团资格的人口的统计数据,我们使用了一个声明,它为阿拉米达县人民起诉斯图亚特·亚历山大的审判而准备。在声明中,圣地亚哥州立大学的人口统计学家 Weeks 教授,根据 2000 年的人口普查数据估算了阿拉米达县的具有陪审团资格的人口,为了得出这个估计值,Weeks 教授考虑到了不符合陪审团担任条件的人数,因为他们不会说英文,不是公民,因此,他的估计应该是对阿拉米达县实际具有陪审团资格的人口的准确评估,而不仅仅是审查居住在阿拉米达的所有人口的种族和族裔的人口普查报告。应该指出的是,Weeks 教授所依据的人口普查数据现在已经有十年了,县的人口统计数据的某些类别,可能已经改变了两到三个百分点。” + +因此,分析中使用的合格陪审员的种族分布本身就是一个估计,可能有点过时。 + +陪审团。 此外,陪审团并不从整个合格总体中选出。 阿拉米达县高等法院说:“法院的目标是提供县人口的准确的横截面,陪审员的名字是从登记选民和/或车管局发出的驾驶执照中随机抽取的”。 + +所有这些都产生了复杂问题,就是如何准确估计阿拉米达县合格陪审员的种族构成。 + +目前还不清楚,1453 个陪审团成员如何划分为不同的种族类别(ACLU 报告称“律师......合作收集陪审团数据”)。 存在严重的社会,文化和政治因素,影响谁被归类或自我分类到每个种族类别。 我们也不知道陪审团中这些类别的定义,是否与 Weeks 教授所使用的定义相同,Weeks 教授又在它的估算过程中使用了人口普查类别。 因此被比较的两个分布的对应关系,也存在问题。 + +### 美国最高法院,1965年:斯温 VS 阿拉巴马州 + +在二十世纪六十年代初期,阿拉巴马州的塔拉迪加县,一个名叫罗伯特·斯温的黑人被指控强奸一名白人妇女,并被判处死刑。 +他援引所有陪审团是白人的其他因素,对他的判决提出上诉。当时,只有 21 岁或以上的男子被允许在塔拉迪加县的陪审团中任职。 在县里,合格的陪审员中有 26% 是黑人,但在 Swain 的审判中选出的 100 名陪审团中只有 8 名黑人男子。 审判陪审团没有选定黑人。 + +1965 年,美国最高法院驳回了斯温的上诉。 法院在其裁决中写道:“整体百分比差距很小,没有反映出包括或排除特定数量的黑人的尝试”。(... the overall percentage disparity has been small and reflects no studied attempt to include or exclude a specified number of Negroes.) + +让我们用我们开发的方法来检查,陪审团中的 100 名黑人中的 8 名与合格陪审员的分布之间的差异。 + +```py +swain_jury = Table().with_columns( + 'Ethnicity', make_array('Black', 'Other'), + 'Eligible', make_array(0.26, 0.74), + 'Panel', make_array(0.08, 0.92) +) + +swain_jury +``` + + +| Ethnicity | Eligible | Panel | +| --- | --- | --- | +| Black | 0.26 | 0.08 | +| Other | 0.74 | 0.92 | + +```py +table_tvd(swain_jury, 'Eligible', 'Panel') +0.18000000000000002 +``` + +两个分布之间的 TVD 是 0.18。 这与合格总体的分布和随机样本之间的 TVD 比较如何? + +为了回答这个问题,我们可以模拟从随机样本中计算的 TVD。 + +```py +# Compute empirical distribution of TVDs + +panel_size = 100 +repetitions = 5000 + +tvds = make_array() + +for i in np.arange(repetitions): + + new_sample = proportions_from_distribution(swain_jury, 'Eligible', panel_size) + tvds = np.append(tvds, table_tvd(new_sample, 'Eligible', 'Random Sample')) + +results = Table().with_column('TVD', tvds) +results.hist(bins = np.arange(0, 0.2, 0.01)) +``` + +![](img/10-4.png) + +随机样本的 TVD 小于我们所得的值 0.18,它是陪审团和合格陪审员的 TVD。 + +在这个分析中,数据并没有像我们以前的分析那样被问题盖住 - 涉及的人总数相对较少,而且最高法院案件的统计工作也很仔细。 + +因此,我们的分析有了明确的结论,那就是陪审团不是总体的代表。 最高法院的判决“整体百分比差距很小”是很难接受的。 + +## 检验的术语 + +在陪审团选择的例子的背景下,我们已经形成了一些假设统计检验的基本概念。使用统计检验作为决策的一种方法是许多领域的标准,并且存在标准的术语。以下是大多数统计检验中的步骤顺序,以及一些术语和示例。 + +### 第一步:假设 + +所有的统计检验都试图在世界的两种观点中进行选择。具体而言,选择是如何生成数据的两种观点之间的选择。这两种观点被称为假设。 + +原(零)假设。这就是说,数据在明确指定的假设条件下随机生成,这些假设使计算几率成为可能。 “零”一词强化了这样一个观点,即如果数据看起来与零假设的预测不同,那么这种差异只是偶然的。 + +在阿拉米达县陪审团选择的例子中,原假设是从合格的陪审员人群中,随机抽取这些陪审团。虽然审团的种族组成与合格的陪审员的总体不同,但除了机会变异以外,没有任何理由存在差异。 + +备选假设。这就是说,除了几率以外的某些原因使数据与原假设所预测的数据不同。非正式而言,备选假设认为观察到的差异是“真实的”。 + +在我们阿拉米达县陪审团选择的例子中,备选假设是,这些小组不是随机选出来的。除了几率以外的事情导致了,陪审团的种族组成和合格陪审员总体的种族组成之间存在差异。 + +### 第二步:检验统计量 + +为了在这两个假设之间作出决策,我们必须选择一个统计量作为我们决策的依据。 这被称为检验统计量。 + +在阿拉米达县陪审团的例子中,我们使用的检验统计量是,陪审团与合格陪审员的总体的种族分布之间的总变异距离。 + +计算检验统计量的观察值通常是统计检验中的第一个计算步骤。 在我们的例子中,陪审团与总体之间的总变异距离的观察值是 0.14。 + +### 第三步:检验统计量的概率分布,在原假设下 + +这个步骤把检验统计量的观察值放在一边,而是把重点放在,如果原假设为真,统计量的值是什么。 在原假设下,由于几率,样本可能出现不同的情况。 所以检验统计量可能会有所不同。 这个步骤包括在随机性的原假设下,计算出所有可能的检验统计量及其所有概率。 + +换句话说,在这个步骤中,我们假设原假设为真,并计算检验统计量的概率分布。 对于许多检验统计量来说,这在数学和计算上都是一项艰巨的任务。 因此,我们通过抽样过程的大量重复,通过统计量的经验分布来近似检验统计量的概率分布。 + +在我们的例子中,我们通过直方图可视化了这个分布。 + +### 第四步 检验的结论 + +原假设和备选假设之间的选择,取决于步骤 2 和 3 的结果之间的比较:检验统计量的观察值以及它的分布,就像由原假设预测的那样。 + +如果二者一致,则观察到的检验统计量与原假设的预测一致。 换句话说,这个检验并不偏向备选假设;数据更加支持原假设。 + +但如果两者不一致,就像我们阿拉米达县陪审团的例子那样,那么数据就不支持原假设。 这就是为什么我们得出结论,陪审团不是随机挑选的。 几率之外的东西影响了他们的构成。 + +如果数据不支持原假设,我们说检验拒绝了原假设。 + +## 孟德尔的豌豆花 + +格雷戈·孟德尔(1822-1884)是一位奥地利僧侣,被公认为现代遗传学领域的奠基人。 孟德尔对植物进行了仔细而大规模的实验,提出遗传学的基本规律。 + +他的许多实验都在各种豌豆上进行。 他提出了一系列每个品种的假设。 这些被称为模型。 然后他通过种植植物和收集数据来测试他的模型的有效性。 + +让我们分析这样的实验的数据,看看孟德尔的模型是否好。 + +在一个特定的品种中,每个植物具有紫色或白色的花。 每个植物的颜色不受其他植物颜色的影响。 孟德尔推测,植物应随机具有紫色或白色的花,比例为 3:1。 + +原假设。 对于每种植物,75% 的几率是紫色的花,25% 的几率是白色的花,无论其他植物的颜色如何。 + +也就是说,原假设是孟德尔的模型是好的。 任何观察到的模型偏差都是机会变异的结果。 + +当然,有一个相反的观点。 + +备选假设。 孟德尔的模型是无效的。 + +让我们看看孟德尔收集的数据更加支持这些假设中的哪一个。 + +`flowers`表包含了由模型预测的比例,以及孟德尔种植的植物数据。 + +```py +flowers = Table().with_columns( + 'Color', make_array('Purple', 'White'), + 'Model Proportion', make_array(0.75, 0.25), + 'Plants', make_array(705, 224) +) + +flowers +``` + + +| Color | Model Proportion | Plants | +| --- | --- | --- | +| Purple | 0.75 | 705 | +| White | 0.25 | 224 | + +共有 929 株植物。 为了观察颜色的分布是否接近模型预测的结果,我们可以找到观察到的比例和模型比例之间的总变异距离,就像我们之前那样。 但是只有两个类别(紫色和白色),我们有一个更简单的选择:我们可以查看紫色的花的比例。 白色的比例没有新的信息,因为它只是 1 减去紫色的比例。 + +```py +total_plants = flowers.column('Plants').sum() +total_plants +929 +observed_proportion = flowers.column('Plants').item(0)/total_plants +observed_proportion +0.7588805166846071 +``` + +检验统计量。 由于该模型预测 75% 的植物花为紫色,相关的统计量是 0.75 与观察到的花为紫色的植物的比例之间的差异。 + +```py +observed_statistic = abs(observed_proportion - 0.75) +observed_statistic +0.0088805166846070982 +``` + +这个值与原假设所说的应该的情况相比如何? 为了回答这个问题,我们需要使用模型来模拟植物的新样本并计算每个样本的统计量。 + +我们将首先创建数组`model_colors`,包含颜色,比例由模型给定。 然后我们可以使用`np.random.choice`从这个数组中,带放回地随机抽样 929 次。 根据孟德尔的模型,这就是植物的生成过程。 + +```py +model_colors = make_array('Purple', 'Purple', 'Purple', 'White') +new_sample = np.random.choice(model_colors, total_plants) +``` + +> 译者注:这里可以使用`np.random.choice`的`p`参数来简化编程。 + +> ```py +> new_sample = np.random.choice(['Purple', 'White'], total_plants, p=[0.75, 0.25]) +> ``` + + +为了与我们观察到的统计量进行比较,我们需要知道这个新样本中,花为紫色的植物的比例与 0.75 的差。 + +```py +proportion_purple = np.count_nonzero(new_sample == 'Purple')/total_plants +abs(proportion_purple - 0.75) +0.016953713670613602 +``` + +检验统计量的经验分布,在原假设为真的情况下。 毫不奇怪,我们得到的值与我们观察到的统计量之间的差约为 0.00888。 但是如果我们又取了一个样本,会有多大的不同呢? 你可以通过重新运行上面的两个单元格来回答这个问题,或者使用`for`循环来模拟统计量。 + +```py +repetitions = 5000 + +sampled_stats = make_array() + +for i in np.arange(repetitions): + new_sample = np.random.choice(model_colors, total_plants) + proportion_purple = np.count_nonzero(new_sample == 'Purple')/total_plants + sampled_stats = np.append(sampled_stats, abs(proportion_purple - 0.75)) + +results = Table().with_column('Distance from 0.75', sampled_stats) +results.hist() +``` + +![](img/10-5.png) + +检验的结论。 根据孟德尔的数据,统计量的观测值是 0.00888,刚好 0.01 以下。 这正好在这个分布的中心。 + +```py +results.hist() + +#Plot the observed statistic as a large red point on the horizontal axis +plots.scatter(observed_statistic, 0, color='red', s=30); +``` + +![](img/10-6.png) + +基于孟德尔数据的统计量,与我们基于孟德尔模型的模拟的分布是一致的。 因此,与备选假设相比,数据更加支持原假设 - 孟德尔的模型是好的。 + +## P 值和“一致”的含义 + +在阿拉米达县陪审团的例子中,我们观察到的检验统计量显然与原假设的预测差距很大。在豌豆花的例子中,观察到的统计量与原假设所预测的分布一致。所以在这两个例子中,选择哪个假设是明显的。 + +但是有时候这个决策还不是很明显。观察到的检验统计量是否与原假设预测的分布一致,是一个判断问题。我们建议你使用检验统计量的值以及原假设预测的分布图,来做出判断。这将使你的读者可以自己判断两者是否一致。 + +如果你不想做出自己的判断,你可以遵循一些惯例。这些惯例基于所谓的观察到的显着性水平,或简称 P 值。 P 值是一个几率,使用检验统计量的概率分布计算,可以用步骤 3 中的经验分布来近似。 + +求出 P 值的实用说明。现在,我们只是给出一个求出该值的机械的方法;意义和解释放到下一节中。方法:将观察到的检验统计量放在直方图的横轴上,求出从以该点起始的尾部比例。这就是 P 值,或者是基于经验分布的 P 值的相当好的近似值。 + +```py +empirical_P = np.count_nonzero(sampled_stats >= observed_statistic)/repetitions +empirical_P +0.5508 +``` + +观察到的统计量 0.00888 非常接近孟德尔模型下所有统计量的中位数。 你可以把它看作是我们之前评论的一个量化,即观察到的统计量正好在原假设的分布中心。 + +但是如果离得更远呢? 例如,如果观察到的统计量是 0.035 呢? 那么我们会得出什么结论呢? + +```py +np.count_nonzero(sampled_stats >= 0.035)/repetitions +0.0122 +``` + +这个比例就很小了。 如果 P 值较小,那就意味着它的尾部很小,所以观察到的统计量远离原假设的预测。 这意味着数据支持备选假设而不是支持原假设。 + +所以如果我们观察到的统计量是 0.035 而不是 0.00888,我们会选择备选假设。 + +那么多小算“小”呢? 这里有个约定。 + ++ 如果 P 值小于 5%,结果称为“统计学显着”。 + ++ 如果 P 值更小 - 小于 1%,结果被称为“高度统计学显着”。 + +在这两种情况下,检验的结论是数据支持备选假设。 + +### 约定的历史注解 + +上面定义的统计学显着性的确定,已经在所有应用领域的统计分析中成为标准。当一个约定被如此普遍遵循时,研究它是如何产生的就有趣了。 + +统计检验方法 - 基于随机样本数据在假设之间选择 - 由 Ronald Fisher 爵士在 20 世纪初开发。在 1925 年出版的《写给研究工作者的统计学方法》(Statistical Methods for Research Workers)一书中的下列陈述中,Ronald 爵士可能在不知情的情况下建立了统计学显著的约定。对于 5% 的水平,他写道:“判断一个偏差是否显著的时候,将它当做一个极限非常方便。 + +Ronald 爵士觉得“方便”的东西变成了截断,获得了普适常数的地位。无论罗纳德爵士如何选出了这个点,这个值是他在众多值中的个人选择:在 1926 年的一篇文章中,他写道:“如果二十分之一看起来还是不够高,如果我们愿意的话, 我们可以把线画在百分之二的地方,或者百分之一。个人来说,作者更倾向于把显著的较低标准设为 5%...” + +Fisher 知道“低”是一个判断问题,没有独特的定义。我们建议你遵循他的优秀例子。提供你的数据,作出判断,并解释你为什么这样做。 + +### GSI 的辩护 + +假设检验是最广泛使用的统计推断方法之一。我们已经看到,它的用途十分广泛,例如审团选择和豌豆花。在本节的最后一个例子中,我们将在另一个完全不同的语境中对假设进行测试。 + +伯克利统计班的 350 名学生被分为 12 个讨论小组,由研究生导师(GSI)带领。期中之后后,第三组的学生注意到,他们的成绩平均上低于班上的其他人。 + +在这种情况下,学生们往往会抱怨这一组的 GSI 。他们肯定觉得,GSI 的教学一定是有问题的。否则为什么他们组会比别人做得更差呢? + +GSI 通常有更多的统计学经验,他们的观点往往是不同的:如果你只是从全班随机抽取一部分学生,他们的平均分数就可能与学生不满意的分数相似。 + +GSI 的立场是一个明确的几率模型。我们来检验一下。 + +原假设:第三组的平均成绩类似于从班上随机抽取的相同数量的学生的平均成绩。 + +备选假设:不是,太低了。 + +`scores`包含整个班级的每个学生的小组编号和期中成绩。期中成绩是 0 到 25 的整数;0 的意思是学生没来考试。 + +```py +scores = Table.read_table('scores_by_section.csv') +scores +``` + +| Section | Midterm | +| --- | --- | +| 1 | 22 | +| 2 | 12 | +| 2 | 23 | +| 2 | 14 | +| 1 | 20 | +| 3 | 25 | +| 4 | 19 | +| 1 | 24 | +| 5 | 8 | +| 6 | 14 | + +(省略了 349 行) + +这是 12 个小组的平均成绩。 + +```py +scores.group('Section', np.mean).show() +``` + +| Section | Midterm mean | +| --- | --- | +| 1 | 15.5938 | +| 2 | 15.125 | +| 3 | 13.6667 | +| 4 | 14.7667 | +| 5 | 17.4545 | +| 6 | 15.0312 | +| 7 | 16.625 | +| 8 | 16.3103 | +| 9 | 14.5667 | +| 10 | 15.2353 | +| 11 | 15.8077 | +| 12 | 15.7333 | + +第三组平均成绩比其他组低一点。 这看起来像机会变异? + +我们知道如何找出答案。 我们首先从全班随机挑选一个“第三组”,看看它的平均得分是多少;然后再做一遍又一遍。 + +首先,我们需要第三组的学生人数: + +```py +scores.group('Section') +``` + +| Section | count | +| --- | --- | +| 1 | 32 | +| 2 | 32 | +| 3 | 27 | +| 4 | 30 | +| 5 | 33 | +| 6 | 32 | +| 7 | 24 | +| 8 | 29 | +| 9 | 30 | +| 10 | 34 | + +(省略了 2 行) + +现在我们的计划是,从班上随机挑选 27 名学生,并计算他们的平均分数。 + +所有学生的成绩都在一张表上,每个学生一行。 因此,我们将使用`sample`来随机选择行,使用`with_replacement = False`选项,以便我们无放回地抽样。 (稍后我们会看到,结果几乎与我们通过放回取样所得到的结果相同)。 + +```py +scores.sample(27, with_replacement=False).column('Midterm').mean() +13.703703703703704 +``` + +我们已经准备好,模拟随机的“第三组”的均值的经验分布。 + +```py +section_3_mean = 13.6667 + +repetitions = 10000 + +means = make_array() + +for i in np.arange(repetitions): + new_mean = scores.sample(27, with_replacement=False).column('Midterm').mean() + means = np.append(means, new_mean) + +emp_p_value = np.count_nonzero(means <= section_3_mean)/repetitions +print('Empirical P-value:', emp_p_value) +results = Table().with_column('Random Sample Mean', means) +results.hist() + +#Plot the observed statistic as a large red point on the horizontal axis +plots.scatter(section_3_mean, 0, color='red', s=30); +Empirical P-value: 0.0581 +``` + +![](img/10-7.png) + +从直方图来看,第三组的较低均值看起来有些不寻常,但 5% 截断值的惯例更加偏向 GSI 的假设。 有了这个截断值,我们说这个结果不是统计学显著的。 + +## 错误概率 + +在我们决定我们的数据更加支持哪个假设的过程中,最后一步涉及数据的原假设的一致性判断。 虽然绝大多数时候这一步都能产生正确的决策,但有时也会让我们误入歧途。 原因是机会变异。 例如,即使当原假设为真时,机会变异也可能导致样本看起来与原假设的预测完全不同。 + +在本节中,我们将研究假设的统计检验如何可能得出这样的结论,也就是实际上原假设为真时,数据支持备选假设。 + +由于我们根据 P 值做出决策,现在应该给出一个更正式的定义,而不是“在经验直方图的横坐标上放置观察到的统计量,并且求出大于它的尾部区域”的机械方法。 + +### P 值的定义 + +P 值是在原假设下,检验统计量等于在数据中观察到的值,或甚至在备选假设方向上更进一步的几率。 + +让我们先看看这个定义如何与前一节的计算结果一致。 + +### 回顾孟德尔的豌豆花 + +在这个例子中,我们评估孟德尔的豌豆物种的遗传模型是否良好。 首先回顾一下我们如何建立决策过程,然后在这个背景下考察 P 值的定义。 + +原假设。 孟德尔的模型是好的:植物的花是紫色或白色,类似于来自总体紫色,紫色,紫色,白色的带放回随机样本。 + +备选假设。 孟德尔的模型是错误的。 + +检验统计量。0.75 与花为紫色的植物的观察比例的距离: + +![](img/tex-10-1.gif) + +样本量较大(929),所以如果孟德尔的模型好,那么观察到的紫色花的比例应该接近 0.75。 如果孟德尔的模型是错误的,则观察到的紫色比例不应该接近0.75,从而使统计值量更大。 + +因此,在这种情况下,“备选假设的方向”意味着“更大”。 +检验统计量的观测值(四舍五入到小数点后五位)是 ![](img/tex-10-2.gif)。根据定义,P 值是从孟德尔的模型中抽取的样本,产生 0.00888 或更大的统计量的几率。 + +虽然我们还没有学会如何精确地计算这个几率,但我们可以通过模拟来逼近它,这就是我们在前一节中所做的。 以下是该部分的所有相关代码。 + +```py +# The model and the data +model_colors = make_array('Purple', 'Purple', 'Purple', 'White') +total_plants = 929 +observed_statistic = 0.0088805166846070982 +# Simulating the test statistic under the null hypothesis +repetitions = 5000 +sampled_stats = make_array() +for i in np.arange(repetitions): + new_sample = np.random.choice(model_colors, total_plants) + proportion_purple = np.count_nonzero(new_sample == 'Purple')/total_plants + sampled_stats = np.append(sampled_stats, abs(proportion_purple - 0.75)) + +# The P-value (an approximation based on the simulation) +empirical_P = np.count_nonzero(sampled_stats >= observed_statistic)/repetitions +# Displaying the results +results = Table().with_column('Distance from 0.75', sampled_stats) +print('Empirical P-value:', empirical_P) +results.hist() +plots.scatter(observed_statistic, 0, color='red', s=30); +Empirical P-value: 0.5436 +``` + +![](img/10-8.png) + +注意 P 值的计算根据孟德尔的模型,基于所有抽取样本的重复,并且每次都计算检验统计量: + +```py +empirical_P = np.count_nonzero(sampled_stats >= observed_statistic)/repetitions +empirical_P +0.5436 +``` + +这是统计量大于等于观测值 0.00888 的样本比例。 + +计算结果表明,如果孟德尔的假设是真实的,那么得到一个植物样本,它的检验统计量大于等于孟德尔的观测值,这个几率大概是 54%。 这是一个很大的几率(并且比“较小”的惯例上的 5% 截断值要大得多)。 因此,孟德尔的数据产生了一个统计量,基于他的模型是不足为奇的,这个数据支持他的模型而不是支持备选假设。 + +### 回顾 GSI 的辩护 + +在这个例子中,第三组由一个班级 12 个组中的 27 个学生组成,期中分数均值低于其他组。 我们试图在以下假设之间作出决策: + +原假设:第三组的平均分数类似于从班上随机挑选的 27 名学生的平均分数。 + +备选假设:不是,太低了。 + +检验统计量。 抽样分数的均值。 + +在这里,备选假设说了,观察到的平均值太低,并不从随机抽样中产生 - 第三组里面有些东西使得平均值较低。 + +所以在这里,“备选假设的方向”是指“较小”。 + +检验统计量的观测值是第三组的平均分 13.6667。因此,根据定义,P 值是 27 位随机选取的学生的平均分 13.6667 或更小的几率。 + +这是我们通过近似来模拟的几率。 这是上一节的代码。 + +```py +# The data +scores = Table.read_table('scores_by_section.csv') +sec_3_mean = 13.6667 +sec_3_size = 27 +# Simulating the test statistic under the null hypothesis +repetitions = 10000 +means = make_array() +for i in np.arange(repetitions): + new_mean = scores.sample(sec_3_size, with_replacement=False).column('Midterm').mean() + means = np.append(means, new_mean) + +# The P-value (an empirical approximation based on the simulation) +empirical_P = np.count_nonzero(means <= sec_3_mean)/repetitions +# Displaying the results +print('Empirical P-value:', empirical_P) +results = Table().with_column('Random Sample Mean', means) +results.hist() +plots.scatter(sec_3_mean, 0, color='red', s=30); +Empirical P-value: 0.0569 +``` + +![](img/10-9.png) + +经验 P 值的计算在下面的单元格中。 + +```py +empirical_P = np.count_nonzero(means <= sec_3_mean)/repetitions +empirical_P +0.0569 +``` + +这是随机样本的比例,其中样本均值小于等于第三组的均值 13.667。 + +模拟结果显示,随机抽样组的 27 名学生平均分数低于第三组的均值,几率为大约 6% ​​。如果按照传统的 5% 截断值作为“较小” P 值的定义,那么 6% 不小了,结果不是统计学显着的。换句话说,你没有足够的证据来拒绝原假设的随机性。 + +你可以尽管违背约定,选择不同的截断值。如果你这样做,请记住以下几点: + ++ 始终提供检验统计量的观察值和 P 值,以便读者可以自行决定 P 值是否小。 ++ 只有当传统的所得结果不符合你的喜好时,才需要违背约定。 ++ 即使你的检验结论为,第三组平均分数低于随机抽样的学生的平均分数,也没有为什么它较低的信息。 + +### 做出错误决策的概率 + +这种平均分数的分析产生了一个重要的观测,关于我们的检验做出错误结论的概率。 + +假设你决定使用 5% 的截断值作为 P 值。 也就是说,如果 P 值低于 5%,那么假设你会选择备选假设,否则保持原假设。 + +那么从样本均值的经验直方图可以看出,如果第三组的平均值是 12,那么你会说“太低了”。12 左侧的面积不足 5%。 + +```py +results.hist() +``` + +![](img/10-10.png) + +13 左边的面积也不到 5%。 左侧面积小于 5% 的所有样本均值以红色显示。 + +![](img/10-11.png) + +你可以看到,如果第三组的平均值接近 13,并且你使用 5% 的截断值作为 P 值,那么你应该说小组的均值不像随机样本的均值。 + +你也可以看到,随机样本的均值可能在 13 左右(尽管不太可能)。事实上,在我们的模拟中,5000 个随机样本中有几个的均值与 13 相差 0.01 以内。 + +```py +results.where('Random Sample Mean', are.between(12.99, 13.01)).num_rows +13 +``` + +你看到的是检验做出错误结论的可能性。 + +如果你使用了 10% 的截断值而不是 5%,那么这里的红色部分意味着,你可能得出结论,它太低了,不能从随机样本中产生,即使在你不知情的情况下,它们是来自随机样本。 + +![](img/10-12.png) + +### 做出错误决策的几率 + +假设你想测试一个硬币是否均匀。 那么假设是: + +原假设:硬币是均匀的。 也就是说,结果是来自正面和反面的随机样本。 + +备选假设:硬币不均匀。 + +假设你的数据基于 400 个硬币的投掷。 你会预计平等的硬币能够在 400 个次投掷中拥有 200 个正面,所以合理的检验统计量就是使用 ![](img/tex-10-3.gif)。 + +我们可以在均匀的原假设下模拟统计量。 + +```py +coin = make_array('Heads', 'Tails') +num_tosses = 400 + +repetitions = 10000 +heads = make_array() +for i in np.arange(repetitions): + tosses = np.random.choice(coin, 400) + heads = np.append(heads, np.count_nonzero(tosses == 'Heads')) + +sampled_stats = abs(heads - 200) +results = Table().with_column('|Number of Heads - 200|', sampled_stats) +results.hist(bins = np.arange(0, 45, 5)) +``` + +![](img/10-13.png) + +如果硬币是不均匀的,那么你预计硬币的数量就不是 200,或者换句话说,如果硬币是均匀的,那么你预计,检验统计量就会大一些。 + +因此,正如在孟德尔的豌豆花的例子中,P 值是统计量经验分布的右侧尾部的区域。 + +假设你决定使用 3.5% 的截断值作为 P 值。 那么即使硬币碰巧是均匀的,对于模拟中的 10000 个检验统计量的前 3.5%,你也会得出“不均匀”的结论。 + +换句话说,如果你用3.5% 的 P 值作为临界值,而硬币恰好是均匀的,那么大概有 3.5% 的概率你会认为硬币是不均匀的。 + +### P 值的截断值是错误概率 + +上面的例子是一个普遍事实的特例: + +如果对 P 值使用`p%`的截断值,并且原假设恰好是真的,那么大约有`p%`的概率,你的检验就会得出结论:备选假设是正确的。 + +因此,1% 的截断值比 5% 更保守 - 如果原假设恰好是真的,那么结论为“备选假设”的可能性就会降低。出于这个原因,医学治疗随机对照试验通常使用 1% 作为决定以下两个假设之间的临界值: + +原假设:实验没有效果;患者的实验组和对照组的结果之间的观察到的差异,是由于随机性造成的。 + +备选假设:实验有效果。 + +这个想法是,控制结论为实验有效,而实际上无效的几率。这减少了给予患者无效治疗的风险。 + +尽管如此,即使你将截断值设置为 1% 那样低,并且实验没有任何效果,但有大约 1% 的几率得出结论:实验是有效的。这由于机会变异。来自随机样本的数据很可能最终导致你误入歧途。 + +### 数据窥探 + +上面的讨论意味着,如果我们进行 500 个单独的随机对照实验,其中实验实际上没有效果,并且每个实验使用 1% 的截断值,那么通过机会变异,500 个实验中的约 5 个将得出结论:实验确实有效果。 + +我们可以希望,没有人会对一无所获的实验进行 500 次。但研究人员使用相同的数据测试多个假设并不罕见。例如,在一项关于药物作用的随机对照试验中,研究人员可能会测试该药物是否对各种不同疾病有影响。 + +现在假设药物对任何东西都没有影响。只是机会变异,一小部分的测试可能会得出结论,它确实有效果。所以,当你阅读一篇使用假设检验的研究,并得出实验有效的结论时,总是询问研究人员,在发现所报告的效果之前,究竟检验了多少种不同的效果。 + +如果研究人员在找到给出“高度统计学显著”的结论之前,进行了多个不同的检验,请谨慎使用结果。这项研究可能会受到数据窥探的影响,这实际上意味着将数据捏造成一个假象。 + +在这种情况下,验证报告结果的一种方法是,复制实验并单独检验该特定效果。如果它再次表现为显著,就验证了原来的结论。 + +### 技术注解:其他类型的错误 + +当然,还有另外一种错误:认为治疗什么也不做,事实上它做了一些事情。近似这个错误超出了本节的范围。要知道,如果你建立你的测试来减少两个错误之一,你几乎总是增加另一个。 + +### 技术注解:识别拒绝域 + +在上面的硬币投掷的例子中,我们基于 400 次投掷,使用 P 值的 3.5 倍的截断值来测试硬币的平等性。检验统计量是 ![](img/tex-10-4.gif)。我们在平等的原假设下模拟了这个统计量。 + +由于所有统计数据的前 3.5%,检验的结论是硬币是不平等的,在下面展示为红色。 + +![](img/10-14.png) + +从图中可以看出,在平等的原假设下,大约前 3.5% 的检验统计量的值大于 20。你也可以通过求出这些值的比例来确认: + +```py +results.where('|Number of Heads - 200|', are.above_or_equal_to(21)).num_rows/results.num_rows +0.0372 +``` + +也就是说,如果检验统计量是 21 或更高,那么以 3.5% 的截断点,你会得出结论:硬币是不公平的。 + +也就是说,如果检验统计量是 21 或更高,你将拒绝原假设。因此,“21 以上”的范围被称为该检验的拒绝域。它对应的正面数量是 221 及以上,或者是 179 及以下。 + +如果你没有在直方图上将其标记为红色,你将如何找到这些值?百分位数函数在这里派上用场。它需要你尝试查找的百分比水平以及包含数据的数组。统计量的“前 3.5%”对应于统计量的第 96.5 个百分点: + +```py +percentile(96.5, results.column(0)) +21.0 +``` + +注意。由于“重复”(即数据中的几个相同的值)和数据数组的任意长度,百分位数并不总是那么整齐。在本课程的后面,我们将给出一个涵盖所有情况的百分位数的精确定义。就目前而言,只要认为`percentile `函数返回一个答案,与你直觉上看做百分点的东西相近即可。 + +## 示例:漏风门 + +2015 年 1 月 18 日,印第安纳波利斯小马队(Indianapolis Colts)和新英格兰爱国者队(New England Patriots)进行了美式橄榄球大会(AFC)冠军赛,来确定哪支球队将晋级超级碗(Super Bowl)。比赛结束后,有人指责爱国者的橄榄球没有按照规定的要求膨胀,并且更软。这可能是一个优势,因为较软的球可能更容易被捕获。 + +几个星期以来,美国橄榄球界充满了指责,否认,理论和怀疑:在 20 世纪 70 年代水门事件的政治丑闻之后,新闻界标记了“漏风门”这个话题。国家橄榄球联盟(NFL)委托了独立分析小组。在这个例子中,我们将执行我们自己的数据分析。 + +压强通常以磅/平方英寸(psi)来衡量。 NFL 规则规定了比赛用球必须充气为 12.5psi 到 13.5psi 的压强。每个队都拥有 12 个球。球队有责任保持自己的球的压强,但比赛官方会检查球。在 AFC 比赛开始之前,所有爱国者的球都在 12.5psi 左右。小马队的大部分球在大约 13.0psi。但是,这些赛前数据没有被记录下来。 + +在第二节,小马队拦截了一个爱国者的球。在边线上,他们测量了球的压强,并确定它低于 12.5psi 的阈值。他们及时通知了官方。 + +中场休息时,所有的比赛用球都被收集起来检查。两名官方人员 Clete Blakeman 和 Dyrol Prioleau 测量了每个球的压强。这里是数据;压强的单位是磅/平方英寸。被小马队拦截的爱国者的球在这个时候没有被检查。大多数小马队的球也没有 - 官方只是耗完了时间,为了下半场的开始,不得不交出了这些球。 + +```py +football = Table.read_table('football.csv') +football = football.drop('Team') +football.show() +``` + + +| Ball | Blakeman | Prioleau | +| --- | --- | --- | +| Patriots 1 | 11.5 | 11.8 | +| Patriots 2 | 10.85 | 11.2 | +| Patriots 3 | 11.15 | 11.5 | +| Patriots 4 | 10.7 | 11 | +| Patriots 5 | 11.1 | 11.45 | +| Patriots 6 | 11.6 | 11.95 | +| Patriots 7 | 11.85 | 12.3 | +| Patriots 8 | 11.1 | 11.55 | +| Patriots 9 | 10.95 | 11.35 | +| Patriots 10 | 10.5 | 10.9 | +| Patriots 11 | 10.9 | 11.35 | +| Colts 1 | 12.7 | 12.35 | +| Colts 2 | 12.75 | 12.3 | +| Colts 3 | 12.5 | 12.95 | +| Colts 4 | 12.55 | 12.15 | + +对于被检查的 15 个球中的每一个,两名官员获得了不同的结果。 在同一物体上重复测量得到不同的结果并不少见,特别是当测量由不同的人进行时。 所以我们将每个球赋为这个球上进行的两次测量的平均值。 + +```py +football = football.with_column( + 'Combined', (football.column(1)+football.column(2))/2 + ) +football.show() +``` + + +| Ball | Blakeman | Prioleau | Combined | +| --- | --- | --- | --- | +| Patriots 1 | 11.5 | 11.8 | 11.65 | +| Patriots 2 | 10.85 | 11.2 | 11.025 | +| Patriots 3 | 11.15 | 11.5 | 11.325 | +| Patriots 4 | 10.7 | 11 | 10.85 | +| Patriots 5 | 11.1 | 11.45 | 11.275 | +| Patriots 6 | 11.6 | 11.95 | 11.775 | +| Patriots 7 | 11.85 | 12.3 | 12.075 | +| Patriots 8 | 11.1 | 11.55 | 11.325 | +| Patriots 9 | 10.95 | 11.35 | 11.15 | +| Patriots 10 | 10.5 | 10.9 | 10.7 | +| Patriots 11 | 10.9 | 11.35 | 11.125 | +| Colts 1 | 12.7 | 12.35 | 12.525 | +| Colts 2 | 12.75 | 12.3 | 12.525 | +| Colts 3 | 12.5 | 12.95 | 12.725 | +| Colts 4 | 12.55 | 12.15 | 12.35 | + +一眼望去,爱国者队的压强显然低于小马队。 由于一些放气在比赛过程中是正常的,独立分析师决定计算距离比赛开始的压强下降值。 回想一下,爱国者的球开始时是大约 12.5psi,小马队的球是大约 13.0psi。 因此爱国者球的压强下降值计算为 12.5 减中场时的压强,小马队的球的压强下降值为 13.0 减半场的压强。 + +我们来构建两张表,一张是爱国者的数据,一张是小马的。 每张表的最后一列是距离开始的压强下降值。 + +```py +patriots = football.where('Ball', are.containing('Patriots')) +patriots = patriots.with_column('Drop', 12.5-patriots.column('Combined')) +patriots.show() +``` + +| Ball | Blakeman | Prioleau | Combined | Drop | +| --- | --- | --- | --- | --- | +| Patriots 1 | 11.5 | 11.8 | 11.65 | 0.85 | +| Patriots 2 | 10.85 | 11.2 | 11.025 | 1.475 | +| Patriots 3 | 11.15 | 11.5 | 11.325 | 1.175 | +| Patriots 4 | 10.7 | 11 | 10.85 | 1.65 | +| Patriots 5 | 11.1 | 11.45 | 11.275 | 1.225 | +| Patriots 6 | 11.6 | 11.95 | 11.775 | 0.725 | +| Patriots 7 | 11.85 | 12.3 | 12.075 | 0.425 | +| Patriots 8 | 11.1 | 11.55 | 11.325 | 1.175 | +| Patriots 9 | 10.95 | 11.35 | 11.15 | 1.35 | +| Patriots 10 | 10.5 | 10.9 | 10.7 | 1.8 | +| Patriots 11 | 10.9 | 11.35 | 11.125 | 1.375 | + +```py +colts = football.where('Ball', are.containing('Colts')) +colts = colts.with_column('Drop', 13.0-colts.column('Combined')) +colts +``` + +| Ball | Blakeman | Prioleau | Combined | Drop | +| --- | --- | --- | --- | --- | +| Colts 1 | 12.7 | 12.35 | 12.525 | 0.475 | +| Colts 2 | 12.75 | 12.3 | 12.525 | 0.475 | +| Colts 3 | 12.5 | 12.95 | 12.725 | 0.275 | +| Colts 4 | 12.55 | 12.15 | 12.35 | 0.65 | + +看起来好像爱国者的漏气比小马队更大。 自然统计量是两个平均漏气之间的差异。 我们将处理它,但你可以自由地用其他自然统计量重复分析,例如整体平均漏气与爱国者之间的差异。 + +```py +patriots_mean = patriots.column('Drop').mean() +colts_mean = colts.column('Drop').mean() + +observed_statistic = patriots_mean - colts_mean +observed_statistic +0.73352272727272805 +``` + +这种正面的差异反映了这样的事实,即爱国者的球的平均压强下降值大于小马队。 + +难道这个差异是偶然的,还是爱国者的下降值太大? 这个问题非常类似于我们之前问过的问题,关于一个大班中的一个小组的成绩。就像我们在这个例子中所做的那样,我们将建立原假设。 + +原假设:爱国者的下降值就是 15 次下降值中的,大小为 11 的随机样本。 由于机会变异,均值比小马队高。 + +备选假设:爱国者的下降值太大,并不仅仅是机会变异的结果。 + +如果原假设是真的,那么爱国者的下降值就可以对比从 15 次下降值随机不带放回抽取的 11 个。 所以让我们创建一个,含有所有 15 个下降值,并从中随机抽取。 + +```py +drops = Table().with_column( + 'Drop', np.append(patriots.column('Drop'), colts.column('Drop')) +) +drops.show() +``` + +| Drop | +| --- | +| 0.85 | +| 1.475 | +| 1.175 | +| 1.65 | +| 1.225 | +| 0.725 | +| 0.425 | +| 1.175 | +| 1.35 | +| 1.8 | +| 1.375 | +| 0.475 | +| 0.475 | +| 0.275 | +| 0.65 | + +```py +drops.sample(with_replacement=False).show() +``` + +| Drop | +| --- | +| 1.225 | +| 1.175 | +| 1.175 | +| 0.475 | +| 1.375 | +| 0.425 | +| 0.85 | +| 0.65 | +| 1.35 | +| 1.65 | +| 0.725 | +| 0.475 | +| 1.475 | +| 1.8 | +| 0.275 | + +注意`sample`的使用没有带样本大小。 这是因为`sample`使用的默认样本大小是表格的行数;如果你不指定样本大小,则会返回与原始表格大小相同的样本。 这对于我们的目的非常理想,因为当你不放回抽样时(通过指定`with_replacement = False`),并且次数与行数相同,最终会对所有行进行随机洗牌。 运行几次该单元格来查看输出如何变化。 + +我们现在可以使用打乱表的前 11 行作为原假设下的爱国者的下降值的模拟。 剩下的四行形成了对应的小马队的下降值的模拟。 我们可以使用这两个模拟数组来模拟我们在原假设下的检验统计量。 + +```py +shuffled = drops.sample(with_replacement=False) + +new_patriots = shuffled.take(np.arange(11)) +new_patriots_mean = new_patriots.column(0).mean() + +new_colts = shuffled.take(np.arange(11, drops.num_rows)) +new_colts_mean = new_colts.column(0).mean() + +simulated_stat = new_patriots_mean - new_colts_mean +simulated_stat +-0.70681818181818212 +``` + +运行几次该单元格来查看检验统计量的变化情况。 请记住,模拟是在原假设下,即爱国者的下降值类似于随机抽样的 15 个下降值。 + +现在是我们熟悉的步骤了。 我们将在院假设下重复模拟检验统计量。 模拟结束时,数组的`simulated_statistics`将包含所有模拟的检验统计量。 + +```py +simulated_statistics = make_array() +repetitions = 10000 + +for i in np.arange(repetitions): + shuffled = drops.sample(with_replacement=False) + new_patriots_mean = shuffled.take(np.arange(11)).column(0).mean() + new_colts_mean = shuffled.take(np.arange(11, drops.num_rows)).column(0).mean() + new_statistic = new_patriots_mean - new_colts_mean + simulated_statistics = np.append(simulated_statistics, new_statistic) +``` + +现在对于经验 P 值,这是一个几率(在原假设下计算),所得的检验统计量等于观察到统计量,或者更加偏向备选假设方向。 为了弄清楚如何计算它,重要的是要回忆另一个假设: + +备选假设:爱国者的下降值太大,并不仅仅是机会变异的结果。 + +“备选假设的方向”是爱国者的下降值很大,对应我们的检验统计量,“爱国者的均值减去小马队的均值”较大。 所以 P 值是几率(在原假设下计算),所得检验统计量大于等于我们 0.73352272727272805。 + +```py +empirical_P = np.count_nonzero(simulated_statistics >= observed_statistic)/repetitions +empirical_P +0.0027 +``` + +这是一个非常小的 P 值。 为了观察它,下面是原假设下检验统计量的经验分布,其中观察到的统计量标在横轴上。 + +```py +print('Observed Statistic:', observed_statistic) +print('Empirical P:', empirical_P) +results = Table().with_column('Simulated Statistic', simulated_statistics) +results.hist() +plots.scatter(observed_statistic, 0, color='red', s=30); +Observed Statistic: 0.733522727273 +Empirical P: 0.0027 +``` + +![](img/10-15.png) + +请注意,分布大部分集中在 0 左右。在原假设下,爱国者的下降值是所有 15 下降值的随机样本,因此小马对也是如此。 所以这两组下降值的平均值应该大致相等,因此它们的差值应该在 0 左右。 + +但是检验统计量的观察值离分布的中心还有很远的距离。 使用什么是“小”的任何合理的截断值,经验 P 值都是小的。 所以我们最终拒绝原假设的随机性,并得出结论,爱国者的下降值太大,并不单独反映机会变异。 + +独立的调查小组以数种不同的方式分析数据,并考虑到物理定律。最后的报告说: + +> “爱国者比赛用球的平均压降超过了小马队的球的平均压降 0.45psi 至 1.02psi,这取决于所使用的测量仪的各种可能的假设,并假设爱国者的球的初始压强为 12.5psi,小马队的球是 13.0psi。” + +> - 2015 年 1 月 18 日,由 NFL 委托对 AFC 冠军赛的调查报告 + +我们的分析显示,平均压降约为 0.73psi,接近“0.45 至 1.02psi”的中心,因此与官方分析一致。 + +请记住,我们对假设的检验并没有确定差异不是偶然的原因。 建立因果关系通常比进行假设检验更为复杂。 + +但足球世界里最重要的问题是因果关系:问题是爱国者足球的压强过大是否是故意的。 如果你对调查人员的答案感到好奇,这里是[完整的报告](https://nfllabor.files.wordpress.com/2015/05/investigative-and-expert-reports-re-footballs-used-during-afc-championsh.pdf)。 + diff --git a/docs/data8-textbook-zh/11.md b/docs/data8-textbook-zh/11.md new file mode 100644 index 0000000000000000000000000000000000000000..e2d841111a19c20e7341a332b2201abdfc07db4b --- /dev/null +++ b/docs/data8-textbook-zh/11.md @@ -0,0 +1,1045 @@ +# 十一、估计 + +> 原文:[Estimation](https://github.com/data-8/textbook/tree/gh-pages/chapters/11) + +> 译者:[飞龙](https://github.com/wizardforcel) + +> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) + +> 自豪地采用[谷歌翻译](https://translate.google.cn/) + + +在前一章中,我们开始开发推断思维的方法。特别是,我们学会了如何使用数据,在世界的两个假设之间做决策。但是我们通常只想知道,某件事情有多大。 + +例如,在前面的章节中,我们调查了敌人可能拥有的战机数量。在选举年,我们可能想知道有多少选民赞成特定候选人。为了评估目前的经济状况,我们可能会对美国家庭年收入的中位数感兴趣。 + +在本章中,我们将开发一种估计未知参数的方法。请记住,参数是总体相关的数值。 + +要弄清参数的值,我们需要数据。如果我们有整个人口的相关数据,我们可以简单地计算参数。 + +但是,如果人口非常庞大(例如,如果它由美国的所有家庭组成),那么收集整个人口的数据可能过于昂贵和耗时。在这种情况下,数据科学家依赖从人口中随机抽样。 + +这导致了一个推断问题:如何根据随机样本中的数据,对未知参数做出正确的结论?我们将用推断思维来回答这个问题。 + +基于随机样本的统计量可能是总体中未知参数的合理估计。例如,你可能希望使用家庭样本的年收入中位数,来估计美国所有家庭的年收入中位数。 + +但任何统计量的值都取决于样本,样本基于随机抽取。所以每次数据科学家得到了一个基于随机样本的估计,他们都面临一个问题: + +“如果样本是不同的,这个估计有多大的不同呢?” + +在本章中,你将学习一种回答这个问题的方法。答案将为你提供工具来估算数值参数,并量化估算中的误差量。 + +我们将以百分位数开始。最有名的百分位数是中位数,通常用于收入数据的摘要。在我们即将开发的估计方法中,其他百分位数也是非常重要的。所以我们一开始要仔细定义百分位数。 + +## 百分位数 + +数值数据可以按照升序或降序排序。因此,数值数据集的值具有等级顺序。百分位数是特定等级的值。 + +例如,如果你的考试成绩在第 95 个百分位,一个常见的解释是只有 5% 的成绩高于你的成绩。中位数是第 50 个百分位;通常假定数据集中 50% 的值高于中值。 + +但是,给予百分位一个精确定义,适用于所有等级和所有列表,需要一些谨慎。为了明白为什么,考虑一个极端的例子,一个班级的所有学生在考试中得分为 75 分。那么 75 是中位数的自然候选,但是 50% 的分数高于 75 并不是真的。另外,75 同样是第 95 个或第 25 个百分位数,或任何其他百分位数的自然候选。在定义百分位数时,必须将重复 - 也就是相同的数据值 - 考虑在内。 + +当相关的索引不明确时,你还必须小心列表到底有多长。例如,10 个值的集合的第 87 个百分位数是多少?有序集合的第 8 个值,还是第 9 个,还是其中的某个位置? + +### 数值的例子 + +在给出所有百分位数的一般定义之前,我们将把数值集合的第80个百分点定义为集合中的(一定条件的)最小值,它至少与所有值的 80% 一样大。 + +例如,考虑非洲,南极洲,亚洲,北美洲和南美洲五大洲的大小,四舍五入到最接近的百万平方英里。 + +```py +sizes = make_array(12, 17, 6, 9, 7) +``` + +第 80 个百分位数是(一定条件的)最小值,至少和 80% 的值一样大,也就是五个元素的五分之四。等于 12: + +```py +np.sort(sizes) +array([ 6, 7, 9, 12, 17]) +``` + +第 80 个百分位数是列表中的一个值,也就是 12。你可以看到,80% 的值小于等于它,并且它是列表中满足这个条件的最小值。 + +与之类似,第 70 个百分位数是该集合中(一定条件的)最小值,至少与 70% 的元素一样大。 现在 5 个元素中的 70% 是“3.5 个元素”,所以第 70 个百分位数是列表中的第 4 个元素。 它是 12,与这些数据的第 80 百分位数相同。 + +### `percentile`函数 + +`percentile`函数接受两个参数:一个 0 到 100 之间的等级,和一个数组。它返回数组相应的百分位数。 + +```py +percentile(70, sizes) +12 +``` + +### 一般定义 + +令`p`为 0 到 100 之间的数字。集合的第`p`个百分位数是集合中的(一定条件)的最小值,它至少与`p%`的所有值一样大。 + +通过这个定义,可以计算任何值的集合的任何 0 到 100 之间的百分位数,并且它始终是集合的一个元素。 + +实际上,假设集合中有`n`个元素。 要找到第`p`个百分位数: + ++ 对集合升序排序。 ++ 计算`n`的`p%`:`(p/100) * n`。叫做`k`。 ++ 如果`k`是一个整数,则取有序集合的第`k`个元素。 ++ 如果`k`不是一个整数,则将其四舍五入到下一个整数,并采用有序集合的那个元素。 + +### 示例 + +`scores_and_sections`表包含 359 名学生,每个学生一行。 列是学生的讨论分组和期中分数。 + +```py +scores_and_sections = Table.read_table('scores_by_section.csv') +scores_and_sections +``` + + +| Section | Midterm | +| --- | --- | +| 1 | 22 | +| 2 | 12 | +| 2 | 23 | +| 2 | 14 | +| 1 | 20 | +| 3 | 25 | +| 4 | 19 | +| 1 | 24 | +| 5 | 8 | +| 6 | 14 | + +(省略了 349 列) + +```py +scores_and_sections.select('Midterm').hist(bins=np.arange(-0.5, 25.6, 1)) +``` + +![](img/11-1.png) + +分数的第 85 个百分位数是多少? 为了使用`percentile`函数,创建包含期中分数的数组`scores`,并找到第 85 个百分位数: + +```py +scores = scores_and_sections.column(1) +percentile(85, scores) +22 +``` + +根据`percentile`函数,第 85 个百分点数是 22。为了检查这是否符合我们的新定义,我们直接应用定义。 + +首先,把分数升序排列: + +```py +sorted_scores = np.sort(scores_and_sections.column(1)) +``` +数组中有 359 个分数。所以下面,计算 359 的 85%,它是 305.15。 + +```py +0.85 * 359 +305.15 +``` + +这不是一个整数。 根据我们的定义,中位数是`sorted_scores`的第 306 个元素,按 Python 的索引约定,它是数组的第 305 项。 + +```py +# The 306th element of the sorted array + +sorted_scores.item(305) +22 +``` + +它和我们通过使用`percentile`得到的答案一样。以后,我们会仅仅使用`percentile`。 + +### 四分位数 + +数值集合的第一个四分位数是第 25 个百分分数。 这个术语(quartile)来自第一个季度(quarter)。 第二个四分位数是中位数,第三个四分位数是第 75 个百分位数。 + +对于我们的分数数据,这些值是: + +```py +percentile(25, scores) +11 +percentile(50, scores) +16 +percentile(75, scores) +20 +``` + +分数的分布有时归纳为“中等 50%”区间,在第一和第三个四分位数之间。 + +## 自举法 + +一个数据科学家正在使用随机样本中的数据来估计未知参数。她使用样本来计算用作估计值的统计量。 + +一旦她计算出了统计量的观察值,她就可以把它作为她的估计值,然后顺其自然。 但她是一名数据科学家。 她知道她的随机样本只是众多可能的随机样本之一,因此她的估计只是众多合理估算之一。 + +这些估计的变化有多大? 为了回答这个问题,似乎她需要从总体中抽取另一个样本,并根据新样本计算一个新的估计值。 但是她没有资源来回到总体中,再抽取一个样本。 + +这个数据科学家看起来好像卡住了。 + +幸运的是,一个叫做自举法的好主意可以帮助她。 由于从总体中生成新样本是不可行的,自举法通过称为重采样的方法生成新的随机样本:新样本从原始样本中随机抽取。 + +在本节中,我们将看到自举法的工作方式和原因。 在本章的其余部分,我们将使用自举法进行推理。 + +### 旧金山市的雇员薪资 + +[SF OpenData](https://data.sfgov.org/) 是一个网站,旧金山市和县在上面公开提供他们的一些数据。 其中一个数据集包含城市雇员的薪资数据。 其中包括市营医院的医疗专业人员,警察,消防员,运输工人,民选官员以及市内所有其他雇员。 + +2015 日历年的薪资数据见表`sf2015`。 + +```py +sf2015 = Table.read_table('san_francisco_2015.csv') +sf2015 +``` + +| Year Type | Year | Organization Group Code | Organization Group | Department Code | Department | Union Code | Union | Job Family Code | Job Family | Job Code | Job | Employee Identifier | Salaries | Overtime | Other Salaries | Total Salary | Retirement | Health/Dental | Other Benefits | Total Benefits | Total Compensation | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| Calendar | 2015 | 2 | Public Works, Transportation & Commerce | WTR | PUC Water Department | 21 | Prof & Tech Engineers - Miscellaneous, Local 21 | 2400 | Lab, Pharmacy & Med Techs | 2481 | Water Qualitytech I/II | 21538 | 82146 | 0 | 0 | 82146 | 16942.2 | 12340.9 | 6337.73 | 35620.8 | 117767 | +| Calendar | 2015 | 2 | Public Works, Transportation & Commerce | DPW | General Services Agency - Public Works | 12 | Carpet, Linoleum and Soft Tile Workers, Local 12 | 7300 | Journeyman Trade | 7393 | Soft Floor Coverer | 5459 | 32165.8 | 973.19 | 848.96 | 33987.9 | 0 | 4587.51 | 2634.42 | 7221.93 | 41209.8 | +| Calendar | 2015 | 4 | Community Health | DPH | Public Health | 790 | SEIU - Miscellaneous, Local 1021 | 1600 | Payroll, Billing & Accounting | 1636 | Health Care Billing Clerk 2 | 41541 | 71311 | 5757.98 | 0 | 77069 | 14697.6 | 12424.5 | 6370.06 | 33492.2 | 110561 | +| Calendar | 2015 | 4 | Community Health | DPH | Public Health | 351 | Municipal Executive Association - Miscellaneous | 0900 | Management | 2620 | Food Service Mgr Administrator | 26718 | 28430.2 | 0 | 763.07 | 29193.3 | 0 | 4223.14 | 5208.51 | 9431.65 | 38625 | +| Calendar | 2015 | 2 | Public Works, Transportation & Commerce | MTA | Municipal Transportation Agency | 790 | SEIU - Miscellaneous, Local 1021 | 8200 | Protection & Apprehension | 8201 | School Crossing Guard | 45810 | 7948.75 | 0 | 0 | 7948.75 | 0 | 2873.17 | 616.24 | 3489.41 | 11438.2 | +| Calendar | 2015 | 1 | Public Protection | POL | Police | 911 | Police Officers' Association | Q000 | Police Services | Q002 | Police Officer | 32906 | 2235 | 0 | 0 | 2235 | 490.36 | 286.72 | 176.57 | 953.65 | 3188.65 | +| Calendar | 2015 | 4 | Community Health | DPH | Public Health | 791 | SEIU - Staff and Per Diem Nurses, Local 1021 | 2300 | Nursing | 2328 | Nurse Practitioner | 7506 | 187247 | 0 | 11704.1 | 198951 | 37683.7 | 12424.5 | 11221.7 | 61329.9 | 260281 | +| Calendar | 2015 | 2 | Public Works, Transportation & Commerce | MTA | Municipal Transportation Agency | 253 | Transport Workers - Transit Operators, Local 250-A | 9100 | Street Transit | 9163 | Transit Operator | 36773 | 66988.5 | 3512.88 | 2770.39 | 73271.8 | 19127.2 | 13203 | 5455.1 | 37785.3 | 111057 | +| Calendar | 2015 | 6 | General Administration & Finance | CAT | City Attorney | 311 | Municipal Attorneys' Association | 8100 | Legal & Court | 8177 | Attorney (Civil/Criminal) | 12963 | 135190 | 0 | 1562.5 | 136752 | 27501.8 | 12424.5 | 10103 | 50029.3 | 186781 | +| Calendar | 2015 | 3 | Human Welfare & Neighborhood Development | DSS | Human Services | 535 | SEIU - Human Services, Local 1021 | 9700 | Community Development | 9703 | Emp & Training Spec 2 | 35179 | 70474.8 | 147.28 | 1647.24 | 72269.3 | 14650.3 | 10696.9 | 5993.11 | 31340.3 | 103610 | + +(省略了 42979 行) + +共有 42,979 名员工,每个人一行。 有许多列包含市政部门隶属关系的信息,以及员工薪酬方案不同部分的详细信息。 这是对应市长 Ed Lee 的一行。 + +```py +sf2015.where('Job', are.equal_to('Mayor')) +``` + +| Year Type | Year | Organization Group Code | Organization Group | Department Code | Department | Union Code | Union | Job Family Code | Job Family | Job Code | Job | Employee Identifier | Salaries | Overtime | Other Salaries | Total Salary | Retirement | Health/Dental | Other Benefits | Total Benefits | Total Compensation | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| Calendar | 2015 | 6 | General Administration & Finance | MYR | Mayor | 556 | Elected Officials | 1100 | Administrative & Mgmt (Unrep) | 1190 | Mayor | 22433 | 288964 | 0 | 0 | 288964 | 58117 | 12424.5 | 20293 | 90834.5 | 379798 | + +我们要研究最后一栏,总薪酬。 这是员工的工资加上市政府对退休和福利计划的贡献。 + +日历年的财务方案有时难以理解,因为它们取决于雇用日期,员工是否在城市内部换工作等等。 例如,`Total Compensation`列中的最低值看起来有点奇怪。 + +```py +sf2015.sort('Total Compensation') +``` + +| Year Type | Year | Organization Group Code | Organization Group | Department Code | Department | Union Code | Union | Job Family Code | Job Family | Job Code | Job | Employee Identifier | Salaries | Overtime | Other Salaries | Total Salary | Retirement | Health/Dental | Other Benefits | Total Benefits | Total Compensation | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| Calendar | 2015 | 1 | Public Protection | FIR | Fire Department | 798 | Firefighters - Miscellaneous, Local 798 | H000 | Fire Services | H002 | Firefighter | 43833 | 0 | 0 | 0 | 0 | 0 | 0 | -423.76 | -423.76 | -423.76 | +| Calendar | 2015 | 4 | Community Health | DPH | Public Health | 790 | SEIU - Miscellaneous, Local 1021 | 9900 | Public Service Aide | 9924 | PS Aide Health Services | 27871 | -292.4 | 0 | 0 | -292.4 | 0 | -95.58 | -22.63 | -118.21 | -410.61 | +| Calendar | 2015 | 1 | Public Protection | JUV | Juvenile Probation | 790 | SEIU - Miscellaneous, Local 1021 | 8300 | Correction & Detention | 8320 | Counselor, Juvenile Hall | 10517 | 0 | 0 | 0 | 0 | 0 | 0 | -159.12 | -159.12 | -159.12 | +| Calendar | 2015 | 6 | General Administration & Finance | CPC | City Planning | 21 | Prof & Tech Engineers - Miscellaneous, Local 21 | 1000 | Information Systems | 1053 | IS Business Analyst-Senior | 18961 | 0 | 0 | 0 | 0 | 0 | 0 | -26.53 | -26.53 | -26.53 | +| Calendar | 2015 | 6 | General Administration & Finance | CPC | City Planning | 21 | Prof & Tech Engineers - Miscellaneous, Local 21 | 5200 | Professional Engineering | 5277 | Planner 1 | 19387 | 0 | 0 | 0 | 0 | 0 | 0 | -9.51 | -9.51 | -9.51 | +| Calendar | 2015 | 2 | Public Works, Transportation & Commerce | PUC | PUC Public Utilities Commission | 21 | Prof & Tech Engineers - Miscellaneous, Local 21 | 1000 | Information Systems | 1044 | IS Engineer-Principal | 28988 | 0 | 0 | 0 | 0 | 0 | 0 | -3.1 | -3.1 | -3.1 | +| Calendar | 2015 | 1 | Public Protection | JUV | Juvenile Probation | 39 | Stationary Engineers, Local 39 | 7300 | Journeyman Trade | 7335 | Senior Stationary Engineer | 19125 | 0 | 0 | 0 | 0 | 0 | 0 | -0.01 | -0.01 | -0.01 | +| Calendar | 2015 | 1 | Public Protection | ECD | Department of Emergency Management | 351 | Municipal Executive Association - Miscellaneous | 0900 | Management | 0922 | Manager I | 30025 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | +| Calendar | 2015 | 7 | General City Responsibilities | UNA | General Fund Unallocated | 790 | SEIU - Miscellaneous, Local 1021 | 3200 | Recreation | 3280 | Assistant Recreation Director | 49784 | 0 | 0 | 0 | 0 | 0 | 0 | 1.27 | 1.27 | 1.27 | +| Calendar | 2015 | 4 | Community Health | DPH | Public Health | 250 | SEIU - Health Workers, Local 1021 | 2600 | Dietary & Food | 2654 | Cook | 26768 | 0 | 0 | 2.21 | 2.21 | 0 | 0 | 0.17 | 0.17 | 2.38 | + +(省略了 42979 行) + +为了便于比较,我们将专注于那些工作时间相当于至少半年的人。 最低工资约为每小时 10 美元,52 周每周 20 小时,工资约为 1 万美元。 + +```py +sf2015 = sf2015.where('Salaries', are.above(10000)) +sf2015.num_rows +36569 +``` + +### 总体和参数 + +让这张超过 36500 行的表格成为我们的总体。 这是总薪资的直方图。 + +```py +sf_bins = np.arange(0, 700000, 25000) +sf2015.select('Total Compensation').hist(bins=sf_bins) +``` + +![](img/11-2.png) + +虽然大部分值都低于 300,000 美元,但有一些还是比较高的。 例如,首席投资官的总薪资不多是 65 万美元。 这就是为什么横轴延伸到了 700,000 美元。 + +```py +sf2015.sort('Total Compensation', descending=True).show(2) +``` + +| Year Type | Year | Organization Group Code | Organization Group | Department Code | Department | Union Code | Union | Job Family Code | Job Family | Job Code | Job | Employee Identifier | Salaries | Overtime | Other Salaries | Total Salary | Retirement | Health/Dental | Other Benefits | Total Benefits | Total Compensation | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| Calendar | 2015 | 6 | General Administration & Finance | RET | Retirement System | 351 | Municipal Executive Association - Miscellaneous | 1100 | Administrative & Mgmt (Unrep) | 1119 | Chief Investment Officer | 46881 | 507832 | 0 | 0 | 507832 | 105053 | 12424.5 | 23566.2 | 141044 | 648875 | +| Calendar | 2015 | 6 | General Administration & Finance | ADM | General Services Agency - City Admin | 164 | Physicians and Dentists - Miscellaneous | 2500 | Med Therapy & Auxiliary | 2598 | Asst Med Examiner | 1016 | 279311 | 3829.36 | 114434 | 397574 | 56211.6 | 12424.5 | 14299.1 | 82935.2 | 480509 | + +(省略了 36567 行) + +现在让参数为总薪资的中位数。 + +既然我们有能力从总体中得到所有数据,我们可以简单计算参数: + +```py +pop_median = percentile(50, sf2015.column('Total Compensation')) +pop_median +110305.78999999999 +``` + +所有员工的薪酬总额的中位数刚刚超过 110,300 美元。 + +从实际的角度来看,我们没有理由抽取样本来估计这个参数,因为我们只是知道它的值。 但在本节中,我们假装不知道这个值,看看我们如何根据随机样本来估计它。 + +在后面的章节中,我们将回到现实,在参数未知的情况下工作。 就目前而言,我们是无所不知的。 + +### 随机样本和估计 + +让我们无放回地随机抽取 500 名员工的样本,并将所选员工的总薪酬的中位数作为我们的参数估计量。 + +```py +our_sample = sf2015.sample(500, with_replacement=False) +our_sample.select('Total Compensation').hist(bins=sf_bins) +``` + +![](img/11-3.png) + +```py +est_median = percentile(50, our_sample.column('Total Compensation')) +est_median +113598.99000000001 +``` + +样本量很大。 根据平均定律,样本的分布与总体的分布相似,因此样本中位数与总体中位数相差不大(尽管当然并不完全相同)。 + +所以现在我们有了参数的估计。 但是,如果样本是不同的,估计的值也会不同。 我们希望能够量化估计的值在不同样本间的差异。 这个变化的测量将有助于我们衡量我们可以将参数估计得多么准确。 + +为了查看样本有多么不同,我们可以从总体中抽取另一个样本,但这样做就作弊了。 我们正试图模仿现实生活,我们不能掌握所有的人口数据。 + +用某种方式,我们必须得到另一个随机样本,而不从总体中抽样。 + +### 自举法:从样本中重采样 + +我们所做的是,从样本中随机抽样。 我们知道了,大型随机样本可能类似于用于抽取的总体。 这一观察使得数据科学家可以通过自举来提升自己:抽样过程可以通过从样本中抽样来复制。 + +以下是自举法的步骤,用于生成类似总体的另一个随机样本: + ++ 将原始样本看做总体。 ++ 从样本中随机抽取样本,与原始样本大小相同。 + +二次样本的大小与原始样本相同很重要。 原因是估计量的变化取决于样本的大小。 由于我们的原始样本由 500 名员工组成,我们的样本中位数基于 500 个值。 为了看看样本变化多少,我们必须将其与 500 个其他样本的中位数进行比较。 + +如果我们从大小为 500 的样本中,无放回地随机抽取了 500 次,我们只会得到相同的样本。 通过带放回抽取,我们就可以让新样本与原始样本不同,因为有些员工可能会被抽到一次以上,其他人则完全不会。 + +为什么这是一个好主意? 按照平均定律,原始样本的分布可能与总体相似,所有“二次样本”的分布可能与原始样本相似。 因此,所有二次样本的分布也可能与总体相似。 + +![](img/11-4.png) + +### 二次样本的中位数 + +回想一下,使用`sample`方法而没有指定样本大小时,默认情况下样本大小等于用于抽取样本的表的行数。 这是完美的自举! 这是从原始样本中抽取的一个新样本,以及相应的样本中位数。 + +```py +resample_1 = our_sample.sample() +resample_1.select('Total Compensation').hist(bins=sf_bins) +``` + +![](img/11-5.png) + +```py +resampled_median_1 = percentile(50, resample_1.column('Total Compensation')) +resampled_median_1 +110001.16 +``` + +通过重采样,我们有了总体中位数的另一个估计。 通过一次又一次的重采样,我们得到许多这样的估计,因此有了估计的经验分布。 + +```py +resample_2 = our_sample.sample() +resampled_median_2 = percentile(50, resample_2.column('Total Compensation')) +resampled_median_2 +110261.39999999999 +``` + +### 自举样本中位数的经验分布 + +让我们定义一个函数`bootstrap_median`,该函数接受我们的原始样本,包含变量的列的标签,以及我们想要的自举样本的数量,并返回二次样本的相应中值的数组。 + +每次我们重采样并找到中位数,我们重复自举过程。 所以自举样本的数量将被称为重复数量。 + +```py +def bootstrap_median(original_sample, label, replications): + """Returns an array of bootstrapped sample medians: + original_sample: table containing the original sample + label: label of column containing the variable + replications: number of bootstrap samples + """ + just_one_column = original_sample.select(label) + medians = make_array() + for i in np.arange(replications): + bootstrap_sample = just_one_column.sample() + resampled_median = percentile(50, bootstrap_sample.column(0)) + medians = np.append(medians, resampled_median) + + return medians +``` + +我们现在将自举过程重复 5000 次。 数组`bstrap_medians`包含所有 5,000 个自举样本的中位数。 注意代码的运行时间比我们以前的代码要长。 因为要做很多重采样! + +```py +bstrap_medians = bootstrap_median(our_sample, 'Total Compensation', 5000) +``` + +这是 5000 个中位数的直方图。 红点是总体的参数:它是整个总体的中位数,我们碰巧知道但没有在自举过程中使用。 + +```py +resampled_medians = Table().with_column('Bootstrap Sample Median', bstrap_medians) + +#median_bins=np.arange(100000, 130000, 2500) +#resampled_medians.hist(bins = median_bins) +resampled_medians.hist() + +plots.scatter(pop_median, 0, color='red', s=30); +``` + +![](img/11-6.png) + +重要的是要记住,红点是固定的:110,305.79 美元,总体的中位数。 经验直方图是随机抽取的结果,将相对于红点随机定位。 + +请记住,所有这些计算的重点是估计人口中位数,它是红点。我们的估计是所有随机生成的样本中位数,它们的直方图你在上面看到了。 我们希望这些估计量包含参数 - 如果没有,它们就脱线了。 + +### 估计量是否捕获了参数 + +红点正好落在二次样本的中位数的经验直方图中间,而不是尾部的几率有多少? 要回答这个问题,我们必须定义“中间”。 让我们将它看做“红点落在二次样本的中位数的中间 95%”。 + +以下是二次采样中位数的“中间 95%”的两端: + +```py +left = percentile(2.5, bstrap_medians) +left +107652.71000000001 +right = percentile(97.5, bstrap_medians) +right +119256.73 +``` + +总体中位数 110,305 美元在这两个数中间。下面的直方图展示了区间和总体中位数。 + +```py +#median_bins=np.arange(100000, 130000, 2500) +#resampled_medians.hist(bins = median_bins) +resampled_medians.hist() + +plots.plot(make_array(left, right), make_array(0, 0), color='yellow', lw=3, zorder=1) +plots.scatter(pop_median, 0, color='red', s=30, zorder=2); +``` + +![](img/11-7.png) + +我们例子中,估计量的“中间 95%”的区间捕获了参数。 但是,这是一个偶然吗? + +要查看区间包含参数的频率,我们必须一遍又一遍地运行整个过程。具体而言,我们将重复以下过程 100 次: + ++ 从总体中抽取一个大小为 500 的原始样本。 ++ 执行 5000 次重复的自举过程,并生成二次样本的中位数的“中间 95%”的区间。 ++ 我们最后得到了 100 个区间,并计算其中有多少个包含总体中位数。 + +剧透警告:自举的统计理论表明,这个数字应该在 95 左右。它可能高于或低于 95,但不会离得太远。 + +```py +# THE BIG SIMULATION: This one takes several minutes. + +# Generate 100 intervals, in the table intervals + +left_ends = make_array() +right_ends = make_array() + +total_comps = sf2015.select('Total Compensation') + +for i in np.arange(100): + first_sample = total_comps.sample(500, with_replacement=False) + medians = bootstrap_median(first_sample, 'Total Compensation', 5000) + left_ends = np.append(left_ends, percentile(2.5, medians)) + right_ends = np.append(right_ends, percentile(97.5, medians)) + +intervals = Table().with_columns( + 'Left', left_ends, + 'Right', right_ends +) +``` + +对于 100 个重复中的每个,我们得到了一个中位数估计量的区间。 + +```py +intervals +``` + +| Left | Right | +| --- | --- | +| 100547 | 115112 | +| 98788.4 | 112129 | +| 107981 | 121218 | +| 100965 | 114796 | +| 102596 | 112056 | +| 105386 | 113909 | +| 105225 | 116918 | +| 102844 | 116712 | +| 106584 | 118054 | +| 108451 | 118421 | + +(省略了 90 行) + +良好的区间是那些包含我们试图估计的参数的区间。 通常参数是未知的,但在本节中,我们碰巧知道参数是什么。 + +```py +pop_median +110305.78999999999 +``` + +100 个区间中有多少个包含总体中位数? 这是左端低于且右端高于总体中位数的区间数量。 + +```py +intervals.where('Left', are.below(pop_median)).where('Right', are.above(pop_median)).num_rows +95 +``` + +构建所有区间需要花费几分钟时间,但如果你有耐心,请再试一次。最有可能的是,100 个区间中有大约 95 个将是良好的:它们将包含参数。 + +因为它们有较大的重叠,所以很难在横轴上显示所有的区间 - 毕竟,它们都试图估计相同的参数。下图通过竖直堆叠,在相同轴域上展示的每个区间。纵轴简单地是重复的序号,区间从中生成。 + +红线是参数所在的位置。良好的区间覆盖了参数;通常有大约 95 个。 + +如果一个区间不能覆盖这个参数,就是个糟糕的事情。在这个地方,你可以看到红线周围的“亮光”。他们中只有很少 - 通常是大约 5 个 - 但是他们确实存在。 + +任何基于抽样的方法都有可能脱线。基于随机抽样的方法的优点是,我们可以量化它们可能脱线的频率。 + +![](img/11-8.png) + +为了总结模拟所示的内容,假设你通过以下过程来估计总体中位数: + +从总体中随机抽取一个大样本。 +自举你的随机样本,并从新的随机样本中获取估计量。 +重复上述步骤数千次,并获得数千个估计量。 +挑选所有估计量的“中间 95%”的区间。 +这给了你一个估计量的区间。现在,如果重复整个过程 100 次,会得到 100 个区间,那么 100 个区间中的大约 95 个将包含总体的参数。 + +换句话说,95% 的时间内,这个估计过程捕获了参数。 + +你可以用一个不同的值代替 95%,只要它不是 100。假设你用 80% 代替了 95%,并保持样本大小为 500。那么你的估计量的区间将比我们这里的模拟要短,因为“中间 80%”是比“中间 95%”更小的范围。只有大约 80% 的区间将包含参数。 + +## 置信区间 + +我们已经开发了一种方法,通过使用随机抽样和自举来估计参数。我们的方法产生一个估计区间,来解释随机样本的机会变异。通过提供一个估计区间而不是一个估计量,我们给自己一些回旋的余地。 + +在前面的例子中,我们看到我们的估计过程在 95% 的时间内产生了一个良好的区间,一个“良好”的区间就是包含这个参数的区间。对于这个过程的结果很好,我们说我们有 95% 的置信度(信心)。我们的估计区间称为参数的 95% 置信区间,95% 称为区间的置信度。 + +前一个例子中的情况有点不寻常。因为我们碰巧知道参数的值,所以我们能够检查一个区间是好还是不好,这反过来又帮助我们看到,我们的估计过程每 100 次中有 95 次捕获了参数。 + +但通常情况下,数据科学家不知道参数的值。这就是他们首先想要估计的原因。在这种情况下,他们通过使用一些方法,类似我们开发的方法,获得未知参数的估计区间。由于统计理论,和我们所看到的演示,数据科学家可以确信,他们产生区间的过程,会以已知百分比的几率,产生一个良好的区间。 + +### 总体中位数的置信区间:自举百分位数方法 + +现在我们使用自举法来估计未知总体的中位数。 数据来自大型医院系统中的新生儿样本; 我们将把它看作是一个简单的随机样本,虽然抽样分多个阶段完成。 Deborah Nolan 和 Terry Speed 的 Stat Labs 拥有一个大数据集的详细信息,这个样本是从中抽取的。 + +`baby`表中包含以下母婴偶对的数量:婴儿的出生体重(盎司),孕期天数,母亲的年龄,母亲身高(英寸),孕期体重(磅),母亲是否在孕期吸烟。 + +```py +baby = Table.read_table('baby.csv') +baby +``` + + +| Birth Weight | Gestational Days | Maternal Age | Maternal Height | Maternal Pregnancy Weight | Maternal Smoker | +| --- | --- | --- | --- | --- | --- | +| 120 | 284 | 27 | 62 | 100 | False | +| 113 | 282 | 33 | 64 | 135 | False | +| 128 | 279 | 28 | 64 | 115 | True | +| 108 | 282 | 23 | 67 | 125 | True | +| 136 | 286 | 25 | 62 | 93 | False | +| 138 | 244 | 33 | 62 | 178 | False | +| 132 | 245 | 23 | 65 | 140 | False | +| 120 | 289 | 25 | 62 | 125 | False | +| 143 | 299 | 30 | 66 | 136 | True | +| 140 | 351 | 27 | 68 | 120 | False | + +(省略了 1164 行) + +出生体重是新生儿健康的一个重要因素 - 较小的婴儿比较大的婴儿在初期需要更多的医疗护理。 因此,在婴儿出生前估计出生体重是有帮助的。 一种方法是检查出生体重和怀孕天数之间的关系。 + +这种关系的一个简单的衡量标准是出生体重与怀孕天数的比值。`ratios`表包含`baby`的前两列,以及一列`ratios`。 这一列的第一个条目按以下方式计算: + +![](img/tex-11-1.gif) + +```py +ratios = baby.select('Birth Weight', 'Gestational Days').with_column( + 'Ratio BW/GD', baby.column('Birth Weight')/baby.column('Gestational Days') +) +ratios +``` + + +| Birth Weight | Gestational Days | Ratio BW/GD | +| --- | --- | --- | --- | +| 120 | 284 | 0.422535 | +| 113 | 282 | 0.400709 | +| 128 | 279 | 0.458781 | +| 108 | 282 | 0.382979 | +| 136 | 286 | 0.475524 | +| 138 | 244 | 0.565574 | +| 132 | 245 | 0.538776 | +| 120 | 289 | 0.415225 | +| 143 | 299 | 0.478261 | +| 140 | 351 | 0.39886 | + +(省略了 1164 行) + +```py +ratios.select('Ratio BW/GD').hist() +``` + +![](img/11-9.png) + +一眼望去,直方图看起来相当对称,密度在 4opd 到 4.5opd 的区间内是最大的。 但仔细一看,就可以看出一些比例相当大。 比率的最大值刚好超过 0.78opd,几乎是通常值的两倍。 + +```py +ratios.sort('Ratio BW/GD', descending=True).take(0) +``` + + +| Birth Weight | Gestational Days | Ratio BW/GD | +| --- | --- | --- | +| 116 | 148 | 0.783784 | + +中位数提供了通常比例的感觉,因为它不受非常大或非常小的比例的影响。 样本(比值)的中位数约为 0.429opd。 + +```py +np.median(ratios.column(2)) +0.42907801418439717 +``` + +但是总体中位数是多少? 我们不知道,所以我们会估计它。 + +我们的方法将与前一节完全相同。 我们将自举样本 5000 次,结果是 5000 个中位数的估计量。 我们 95% 的置信区间将是我们所有估计量的“中间 95%”。 + +回忆前一节定义的`bootstrap_median`函数。 我们将调用这个函数,并构造总体(比值)中位数的 95% 置信区间。请记住,`ratios`表包含来自我们的原始样本的相关数据。 + +```py +def bootstrap_median(original_sample, label, replications): + + """Returns an array of bootstrapped sample medians: + original_sample: table containing the original sample + label: label of column containing the variable + replications: number of bootstrap samples + """ + + just_one_column = original_sample.select(label) + medians = make_array() + for i in np.arange(replications): + bootstrap_sample = just_one_column.sample() + resampled_median = percentile(50, bootstrap_sample.column(0)) + medians = np.append(medians, resampled_median) + + return medians +# Generate the medians from 5000 bootstrap samples +bstrap_medians = bootstrap_median(ratios, 'Ratio BW/GD', 5000) +# Get the endpoints of the 95% confidence interval +left = percentile(2.5, bstrap_medians) +right = percentile(97.5, bstrap_medians) + +make_array(left, right) +array([ 0.42545455, 0.43262411]) +``` + +95% 置信区间是 0.425opd 到 0.433opd。 我们估计的总体(出生重量与怀孕天数的比值)中位数,在 0.425opd 到 0.433opd 的范围内。 + +基于原始样本的估计量 0.429 恰好在区间两端的中间,尽管这通常不是真的。 + +为了使我们的结果可视化,让我们画出我们自举的中位数的经验直方图,并将置信区间置于横轴上。 + +```py +resampled_medians = Table().with_column( + 'Bootstrap Sample Median', bstrap_medians +) +resampled_medians.hist(bins=15) +plots.plot(make_array(left, right), make_array(0, 0), color='yellow', lw=8); +``` + +![](img/11-10.png) + +这个直方图和区间就像我们在前一节中绘制的直方图和区间,只有一个很大的区别 - 没有显示参数的红点。 我们不知道这个点应该在哪里,或者它是否在区间中。 + +我们只是有一个估计区间。 这是估计量的 95% 置信区间,因为生成它的过程在 95% 的时间中产生了良好的区间。 那肯定是在随机猜测! + +请记住,这个区间是一个大约 95% 的置信区间。 计算中涉及到很多近似值。 近似值并不差,但并不准确。 + +### 总体均值的置信区间:自举百分位数方法 + +我们为中位数所做的事情也可以用于均值。 假设我们想估计总体中的母亲的平均年龄。 自然估计量是样本中的母亲的平均年龄。 这是他们的年龄分布,他们的平均年龄大约是 27.2 岁。 + +```py +baby.select('Maternal Age').hist() +``` + +![](img/11-11.png) + +```py +np.mean(baby.column('Maternal Age')) +27.228279386712096 +``` + +母亲的平均年龄是多少? 我们不知道这个参数的值。 + +我们用自举法来估计未知参数。 为此,我们将编辑`bootstrap_median`的代码,而不是定义函数`bootstrap_mean`。 代码是相同的,除了统计量是代替中位数的均值,并且收集在一个名为`means`而不是`medians`的数组中。 + +```py +def bootstrap_mean(original_sample, label, replications): + + """Returns an array of bootstrapped sample means: + original_sample: table containing the original sample + label: label of column containing the variable + replications: number of bootstrap samples + """ + + just_one_column = original_sample.select(label) + means = make_array() + for i in np.arange(replications): + bootstrap_sample = just_one_column.sample() + resampled_mean = np.mean(bootstrap_sample.column(0)) + means = np.append(means, resampled_mean) + + return means +# Generate the means from 5000 bootstrap samples +bstrap_means = bootstrap_mean(baby, 'Maternal Age', 5000) + +# Get the endpoints of the 95% confidence interval +left = percentile(2.5, bstrap_means) +right = percentile(97.5, bstrap_means) + +make_array(left, right) +array([ 26.89778535, 27.55962521]) +``` + +95% 置信区间是约 26.9 岁到约 27.6 岁。 也就是说,我们估计的母亲的平均年龄在 26.9 岁到 27.6 岁之间。 + +注意两端距原始样本均值 27.2 岁的距离。 样本量非常大 - 1174 个母亲 - 所以样本均值变化不大。 我们将在下一章进一步探讨这个观察。 + +下面显示了 5000 个自举均值的经验直方图,以及总体均值的 95% 置信区间。 + +```py +resampled_means = Table().with_column( + 'Bootstrap Sample Mean', bstrap_means +) +resampled_means.hist(bins=15) +plots.plot(make_array(left, right), make_array(0, 0), color='yellow', lw=8); +``` + +![](img/11-12.png) + +原始样本的均值(27.23 岁)同样接近区间中心。 这并不奇怪,因为每个自举样本都是从相同的原始样本中抽取的。 自举样本的均值大约对称分布原始样本(从其中抽取)的均值的两侧。 + +还要注意,即使所采样的年龄的直方图完全不是对称的,二次样本的均值的经验直方图也是大致对称的钟形: + +```py +baby.select('Maternal Age').hist() +``` + +![](img/11-13.png) + +这是概率统计的中心极限定理的结果。 在后面的章节中,我们将看到这个定理是什么。 + +### 80% 置信区间 + +你可以使用自举法来构建任意水平的置信区间。 例如,要为总体中的平均年龄构建 80% 置信区间,可以选取二次样本的均值的“中间 80%”。 所以你会希望为两个尾巴的每一个分配 10%,因此端点是二次样本的均值的第 10 和第 90 个百分位数。 + +```py +left_80 = percentile(10, bstrap_means) +right_80 = percentile(90, bstrap_means) +make_array(left_80, right_80) +array([ 27.01192504, 27.439523 ]) +resampled_means.hist(bins=15) +plots.plot(make_array(left_80, right_80), make_array(0, 0), color='yellow', lw=8); +``` + +![](img/11-14.png) + +这个 80% 置信区间比 95% 置信区间要短得多。 它只是约定 27.0 岁到约 27.4 岁。 虽然这是估计量的较窄区间,你知道这个过程在 80% 的时间内产生良好的区间。 + +之前过程产生了较宽的区间,但是我们对产生它的过程拥有更高的置信度。 + +为了以较高的置信度获得较窄的置信区间,你必须从较大的样本开始。 我们将在下一章看到为什么。 + +### 总体比例的置信区间:自举百分位数方法 + +在样本中,39% 的母亲在怀孕期间吸烟。 + +```py +baby.where('Maternal Smoker', are.equal_to(True)).num_rows/baby.num_rows +0.3909710391822828 +``` + +以下对观察很实用,这个比例也可以通过数组操作来计算: + +```py +smoking = baby.column('Maternal Smoker') +np.count_nonzero(smoking)/len(smoking) +0.3909710391822828 +``` + +> 译者注: + +> `np.count_nonzero(arr)`等价于`np.sum(arr != 0)`。 + +总体中有百分之多少的母亲在怀孕期间吸烟? 这是一个未知的参数,我们可以通过自举置信区间来估计。 这个过程中的步骤与我们用来估计总体均值和中位数的步骤相似。 + +我们将首先定义一个函数`bootstrap_proportion`,返回一个自举样本的比例的数组。 我们再一次通过编辑`bootstrap_median`的定义来实现它。 计算中唯一的变化是用二次样本的吸烟者比例代替中位数。 该代码假定数据列由布尔值组成。 其他的改变只是数组的名字,来帮助我们阅读和理解我们的代码。 + +```py +def bootstrap_proportion(original_sample, label, replications): + + """Returns an array of bootstrapped sample proportions: + original_sample: table containing the original sample + label: label of column containing the Boolean variable + replications: number of bootstrap samples + """ + + just_one_column = original_sample.select(label) + proportions = make_array() + for i in np.arange(replications): + bootstrap_sample = just_one_column.sample() + resample_array = bootstrap_sample.column(0) + resampled_proportion = np.count_nonzero(resample_array)/len(resample_array) + proportions = np.append(proportions, resampled_proportion) + + return proportions +``` + +让我们使用`bootstrap_proportion`来构建总体的母亲吸烟者百分比的 95% 置信区间。 该代码类似于均值和中位数的相应代码。 + +```py +# Generate the proportions from 5000 bootstrap samples +bstrap_props = bootstrap_proportion(baby, 'Maternal Smoker', 5000) + +# Get the endpoints of the 95% confidence interval +left = percentile(2.5, bstrap_props) +right = percentile(97.5, bstrap_props) + +make_array(left, right) +array([ 0.36286201, 0.41908007]) +``` + +置信区间是 36% 到 42%。原始样本的百分比 39% 非常接近于区间的中心。你可以在下面看到: + +```py +resampled_proportions = Table().with_column( + 'Bootstrap Sample Proportion', bstrap_props +) +resampled_proportions.hist(bins=15) +plots.plot(make_array(left, right), make_array(0, 0), color='yellow', lw=8); +``` + +![](img/11-15.png) + +### 自举法的注意事项 + +自举法是一个优雅而强大的方法。在使用之前,记住一些要点非常重要。 + +以大型随机样本开始。如果你不这样做,该方法可能无法正常工作。它的成功基于大型随机样本(因此也从样本中重采样)。平均定律说,如果随机样本很大,这很可能是真的。 + +为了近似统计量的概率分布,最好多次复制重采样过程。数千次重复将产生样本中位数分布的正确近似,特别是如果总体分布存在峰值并且不是非常不对称的话。在我们的例子中,我们使用了 5000 次重复,但一般会推荐 10000 次。 + +自举百分位数方法适用于基于大型随机样本,估计总体中位数或均值。但是,它也有其局限性,所有的估计方法也是如此。例如,在以下情况下,它预期没有效果。 + ++ 目标是估计总体中的最小值或最大值,或非常低或非常高的百分位数,或受总体中稀有元素影响较大的参数。 ++ 统计量的概率分布不是近似钟形的。 ++ 原始样本非常小,比如 10 或 15。 + +## 使用置信区间 + +```py +def bootstrap_median(original_sample, label, replications): + + """Returns an array of bootstrapped sample medians: + original_sample: table containing the original sample + label: label of column containing the variable + replications: number of bootstrap samples + """ + + just_one_column = original_sample.select(label) + medians = make_array() + for i in np.arange(replications): + bootstrap_sample = just_one_column.sample() + resampled_median = percentile(50, bootstrap_sample.column(0)) + medians = np.append(medians, resampled_median) + + return medians +def bootstrap_mean(original_sample, label, replications): + + """Returns an array of bootstrapped sample means: + original_sample: table containing the original sample + label: label of column containing the variable + replications: number of bootstrap samples + """ + + just_one_column = original_sample.select(label) + means = make_array() + for i in np.arange(replications): + bootstrap_sample = just_one_column.sample() + resampled_mean = np.mean(bootstrap_sample.column(0)) + means = np.append(means, resampled_mean) + + return means +def bootstrap_proportion(original_sample, label, replications): + + """Returns an array of bootstrapped sample proportions: + original_sample: table containing the original sample + label: label of column containing the Boolean variable + replications: number of bootstrap samples + """ + + just_one_column = original_sample.select(label) + proportions = make_array() + for i in np.arange(replications): + bootstrap_sample = just_one_column.sample() + resample_array = bootstrap_sample.column(0) + resampled_proportion = np.count_nonzero(resample_array)/len(resample_array) + proportions = np.append(proportions, resampled_proportion) + + return proportions +``` + +置信区间只有一个目的 - 根据随机样本中的数据估计未知参数。在最后一节中,我们说区间`(36%, 42%)`是总体中吸烟者百分比的约 95% 的置信区间。正式的表述方式为,据我们估计,总体中的吸烟者比例在 36% 到 42% 之间,我们的估计过程在 95% 的时间内是正确的。 + +克制住将置信区间用于其他目的的冲动,这很重要。例如,回想一下,我们计算了区间`(26.9 yr, 27.6 yr)`,作为母亲平均年龄的约 95% 的置信区间。区间的一个令人惊讶的常见误用是得出这样的结论,约 95% 的女性在 26.9 岁至 27.6 岁之间。你不需要怎么了解置信区间,来查看这是不是正确的 - 你不会预计 95% 的母亲的年龄在这个较小的范围内。实际上,抽样年龄的直方图显示出相当多的变化。 + +```py +baby = Table.read_table('baby.csv') +baby.select('Maternal Age').hist() +``` + +![](img/11-16.png) + +抽样年龄的一小部分在`(26.9, 27.6)`的区间内,你可能会预计总体中的百分比很小。 区间只是估计一个数字:总体中所有年龄的平均值。 + +但是,除了仅仅告诉我们这个参数有多大之外,用置信区间来估计一个参数确实有重要的用处。 + +### 使用置信区间来检验假设 + +我们总体(年龄)均值的 95% 置信区间是 26.9 岁到 27.6 岁。假设有人想要测试以下假设: + +原假设。人口的平均年龄是 30 岁。 + +备选假设。人口的平均年龄不是 30 岁。 + +那么,如果你使用 5% 的截断值作为 P 值,则会拒绝原假设。这是因为总体平均值 30 不在 95% 置信区间内。在 5% 的显着性水平上,30 对于人口平均值并不合理。 + +置信区间的使用是置信区间和检验之间二元性结果:如果你正在测试总体平均值是否是特定值 x,并且你使用的 5% 截断值作为 P 值,那么如果 x 不在平均值的 95% 置信区间内,你将拒绝原零假设。 + +这可以由统计理论来确定。在实践中,它只是归结为,检查原假设中指定的值是否在置信区间内。 + +如果你使用 1% 的截断值作为 P 值,你必须检查,原假设中指定的值是否在总体均值的 99% 置信区间内。 + +粗略地说,如果样本量很大,这些陈述也适用于总体比例。 + +虽然我们现在有一种方法,使用置信区间来检验一种特定假设,但是你可能想知道,测试总体(年龄)的均值是否等于 30 的意义。实际上,这个意义并不清楚。但是在某些情况下,对这种假设的检验既自然又有用。 + +我们将在数据的背景下来研究它,这些数据是霍奇金病治疗的随机对照试验中收集的信息的子集。霍奇金病是一种通常影响年轻人的癌症。这种疾病是可以治愈的,但治疗可能非常艰难。该试验的目的是找出治疗癌症的剂量,并且将对患者的不利影响最小化。 + +这张表格包含治疗对 22 名患者肺部的影响的数据。这些列是: + ++ 身高(厘米) ++ 覆盖物辐射的测量(颈部,胸部,手臂下) ++ 化疗的测量 ++ 基线下,即在治疗开始时的肺健康得分;较高的分数对应于更健康的肺 ++ 治疗后 15 个月,相同的肺的健康得分 + +```py +hodgkins = Table.read_table('hodgkins.csv') +hodgkins +``` + +| height | rad | chemo | base | month15 | +| --- | --- | --- | --- | --- | +| 164 | 679 | 180 | 160.57 | 87.77 | +| 168 | 311 | 180 | 98.24 | 67.62 | +| 173 | 388 | 239 | 129.04 | 133.33 | +| 157 | 370 | 168 | 85.41 | 81.28 | +| 160 | 468 | 151 | 67.94 | 79.26 | +| 170 | 341 | 96 | 150.51 | 80.97 | +| 163 | 453 | 134 | 129.88 | 69.24 | +| 175 | 529 | 264 | 87.45 | 56.48 | +| 185 | 392 | 240 | 149.84 | 106.99 | +| 178 | 479 | 216 | 92.24 | 73.43 | + +(省略了 12 行) + +我们将比较基准和 15 个月的得分。 由于每行对应一个病人,我们说基线得分的样本和 15 个月得分的样本是成对的 - 它们不是每组 22 个值的两组,而是 22 对值,每个病人一个。 + +一眼望去,你可以看到,15 个月的得分往往低于基线得分 - 抽样患者的肺似乎在治疗后 15 个月更差。 这个由`drop`列主要是正值来证实,它是基线得分减去 15 个月的得分。 + +```py +hodgkins = hodgkins.with_column( + 'drop', hodgkins.column('base') - hodgkins.column('month15') +) +hodgkins +``` + +![](img/11-17.png) + +| height | rad | chemo | base | month15 | drop | +| --- | --- | --- | --- | --- | --- | +| 164 | 679 | 180 | 160.57 | 87.77 | 72.8 | +| 168 | 311 | 180 | 98.24 | 67.62 | 30.62 | +| 173 | 388 | 239 | 129.04 | 133.33 | -4.29 | +| 157 | 370 | 168 | 85.41 | 81.28 | 4.13 | +| 160 | 468 | 151 | 67.94 | 79.26 | -11.32 | +| 170 | 341 | 96 | 150.51 | 80.97 | 69.54 | +| 163 | 453 | 134 | 129.88 | 69.24 | 60.64 | +| 175 | 529 | 264 | 87.45 | 56.48 | 30.97 | +| 185 | 392 | 240 | 149.84 | 106.99 | 42.85 | +| 178 | 479 | 216 | 92.24 | 73.43 | 18.81 | + +(省略了 12 行) + +```py +hodgkins.select('drop').hist(bins=np.arange(-20, 81, 20)) +``` + +```py +np.mean(hodgkins.column('drop')) +28.615909090909096 +``` + +但是,这可能是机会变异的结果吗? 似乎并不如此,但数据来自随机样本。 难道在整个人群中,平均下降值只有 0 吗? + +为了回答这个问题,我们可以设定两个假设: + +原假设:总体(下降值)均值为 0。 + +备选假设:总体(下降值)均值不为 0。 + +为了使用 1% 的截断值作为 P 值来验证这个假设,让我们为总体(下降值)均值构建近似 99% 置信区间。 + +```py +bstrap_means = bootstrap_mean(hodgkins, 'drop', 10000) + +left = percentile(0.5, bstrap_means) +right = percentile(99.5, bstrap_means) + +make_array(left, right) +array([ 17.25045455, 40.60136364]) +resampled_means = Table().with_column( + 'Bootstrap Sample Mean', bstrap_means +) +resampled_means.hist() +plots.plot(make_array(left, right), make_array(0, 0), color='yellow', lw=8); +``` + +![](img/11-18.png) + +总体均值的 99% 置信区间是约 17 到约 40。区间不包含 0。因此,我们拒绝原假设。 + +但是请注意,我们所做的不仅仅是简单得出结论:总体均值不是 0,我们估计了均值的幅度是多大。这比仅仅说“不是 0”更有用。 + +对于准确性的注解:我们的置信区间相当宽泛,主要有两个原因: + ++ 置信水平很高(99%)。 ++ 与我们之前的例子相比,样本量相对较小。 + +在下一章中,我们将研究样本大小如何影响准确性。我们还将研究,样本均值的经验分布为何经常出现钟形,尽管底层数据的分布根本不是钟形的。 + +### 尾注 + +一个领域的术语通常来自该领域的主要研究人员。首先提出自举技术的 [Brad Efron](https://en.wikipedia.org/wiki/Bradley_Efron) 用了一个[美国血统的术语](https://en.wikipedia.org/wiki/Bootstrapping)。中国统计学家不甘示弱,[提出了自己的方法](http://econpapers.repec.org/article/eeestapro/v_3a37_3ay_3a1998_3ai_3a4_3ap_3a321-329.htm)。 diff --git a/docs/data8-textbook-zh/12.md b/docs/data8-textbook-zh/12.md new file mode 100644 index 0000000000000000000000000000000000000000..7743aa4e11fdfbecca907dc28ca060af153bbf93 --- /dev/null +++ b/docs/data8-textbook-zh/12.md @@ -0,0 +1,1272 @@ +# 十二、为什么均值重要 + +> 原文:[Why the Mean Matters](https://github.com/data-8/textbook/tree/gh-pages/chapters/12) + +> 译者:[飞龙](https://github.com/wizardforcel) + +> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) + +> 自豪地采用[谷歌翻译](https://translate.google.cn/) + + +在这个课程中,我们已经研究了几个不同的统计量,包括总编译距离,最大值,中位数和平均值。在关于随机性的明确假设下,我们绘制了所有这些统计量的经验分布。有些统计量,比如最大和总变异距离,分布明显偏向一个方向。但是,无论研究对象如何,样本均值的经验分布几乎总是接近钟形。 + +如果随机样本的性质是真的,不管总体如何,它都能成为一个有力的推理工具,因为我们通常不清楚总体中的数据。大型随机样本的均值分布属于这类性质。这就是随机抽样方法广泛用于数据科学的原因。 + +在本章中,我们将研究均值,以及我们可以说的一些东西,仅仅使用最基本的底层总体的假设。我们要解决的问题包括: + ++ 均值正好测量了什么? ++ 大部分数据与平均值有多接近? ++ 样本量如何与样本的均值相关? ++ 为什么随机样本的经验分布出现钟形? ++ 我们如何有效地使用抽样方法进行推理? + +## 均值的性质 + +在这个课程中,我们可以互换地使用“average”和“mean”两个单词(译者注,在中文中都译为“均值”),后面也一样。 在你高中甚至更早的时候,你熟悉均值的定义。 + +定义:数值集合的均值是集合中所有元素的总和,除以集合中元素的数量。 + +`np.average`和`np.mean`方法返回数组的均值。 + +```py +not_symmetric = make_array(2, 3, 3, 9) +np.average(not_symmetric) +4.25 +np.mean(not_symmetric) +4.25 +``` + +### 基本性质 + +上面的定义和例子指出了均值的一些性质。 + ++ 它不一定是集合中的一个元素。 ++ 即使集合的所有元素都是整数,也不一定是整数。 ++ 它在集合的最小值和最大值之间。 ++ 它不一定在两个极值的正中间;集合中一半的元素并不总是大于均值。 ++ 如果集合含有一个变量的值,以指定单位测量,则均值也具有相同的单位。 + +我们现在将研究一些其他性质,它有助于理解均值,并与其他统计量相关。 + +### 均值是个“平滑器” + +你可以将均值视为“均衡”或“平滑”操作。 例如,将上面的`not_symmetric`中的条目设想为四个不同人的口袋中的美元。 为此,你先把所有的钱都放进一个大袋子,然后平均分配给四个人。 最开始,他们在口袋中装了不同数量的钱(2 美元,3 美元,3 美元和9 美元),但现在每个人都有平均数量 4.25 美元。 + +### 均值的性质 + +如果一个集合只包含 1 和 0,那么集合的总和就是集合中 1 的数量,集合的均值就是 1 的比例。 + +```py +zero_one = make_array(1, 1, 1, 0) +sum(zero_one) +3 +np.mean(zero_one) +0.75 +``` + +捏可以将 1 替换为布尔值`True`,0 替换为`False`。 + +```py +np.mean(make_array(True, True, True, False)) +0.75 +``` + +因为比例是均值的一个特例,随机样本均值的结果也适用于随机样本比例。 + +### 均值和直方图 + +集合`{2, 3, 3, 9}`的平均值是 4.25,这不是数据的“正中间的点”。 那么这是什么意思? + +为了了解它,请注意,平均值可以用不同的方式计算。 + +![](img/tex-12-1.gif) + +最后一个表达式就是一个普遍事实的例子:当我们计算平均值时,集合中的每个不同的值都由它在集合中出现的时间比例加权。 + +这有一个重要的结果。 集合的平均值仅取决于不同的值及其比例,而不取决于集合中元素的数量。 换句话说,集合的平均值仅取决于集合中值的分布。 + +因此,如果两个集合具有相同的分布,则它们具有相同的均值。 + +例如,这里是另一个集合,它的分布与`not_symmetric`相同,因此均值也相同。 + +```py +not_symmetric +array([2, 3, 3, 9]) +same_distribution = make_array(2, 2, 3, 3, 3, 3, 9, 9) +np.mean(same_distribution) +4.25 +``` + +均值是分布直方图的物理属性。这里是`not_symmetric`的分布直方图,或者等价的`same_distribution`的分布直方图。 + +![](img/12-1.png) + +想象一下,直方图是由纸板组成的图形,它附着在一条线上,线沿着横轴延伸。并且,将这些条形想象为附加在值 2, 3 和 9 上的权重。假设你尝试在线上的某个点平衡这个图形。如果该点接近 2,图形就向右倾斜。如果该点接近 9,则图形就向左倾斜。之间的某个地方是这个数字取得平衡的点。这个点是 4.25,就是均值。 + +均值是直方图的重心或平衡点。 + +为了理解这是为什么,了解一些物理会有帮助。重心的计算与我们计算平均值的方法完全相同,通过将不同值按它们比例加权。 + +因为均值是一个平衡点,有时在直方图的底部显示为一个支点或三角形。 + +![](img/12-2.png) + +### 均值和中位数 + +如果一个学生的考试成绩低于平均水平,这是否意味着该学生在该考试中处于后一半? + +对于学生来说,回答是“不一定”。 原因与直方图的平衡点即均值,和数据的“中间点”即中位数之间的关系有关。 + +通过这个关系很容易看到一个简单的例子。 这里是数组`symmetric`的集合`{2, 3, 3, 4}`的直方图。 分布对称于 3。均值和中位数都等于 3。 + +```py +symmetric = make_array(2, 3, 3, 4) +``` + +![](img/12-3.png) + +```py +np.mean(symmetric) +3.0 +percentile(50, symmetric) +3 +``` + +一般来说,对于对称分布,均值和中位数是相等的。 + +如果分布不对称呢? 我们来比较`symmetric`和`not_symmetric`。 + +![](img/12-4.png) + +蓝色直方图表示原始的`symmetric`分布。 `not_symmetric `的金色从左端起始,和蓝色一样,但是最右边的条形到了数值 9。棕色部分是两个直方图重叠的位置。 + +蓝色分布的中位数和均值都等于 3。金色分布的中值也等于 3,尽管右半部分与左边的分布不同。 + +但金色分布的平均值不是 3:金色直方图在 3 时不平衡。平衡点已经向右移动到 4.25。 + +在金色分布中,4 个条目中有 3 个(75%)低于平均水平。 因此,低于平均分的学生可以放心。 他或她可能是班上的大多数人。 + +一般来说,如果直方图的一边有尾巴(整数属于是“偏斜的”),那么平均值就会从中间拉到尾巴的方向。 + +### 示例 + +`sf2015`表包含 2015 年旧金山城市员工的薪水和福利数据。与以前一样,我们将我们的分析仅限于那些等价于至少就业半年的人。 + +```py +sf2015 = Table.read_table('san_francisco_2015.csv').where('Salaries', are.above(10000)) +``` + +我们前面看到了,最高薪资高于 60 万美元,但绝大多数雇员的薪资低于 30 万美元。 + +```py +sf2015.select('Total Compensation').hist(bins = np.arange(10000, 700000, 25000)) +``` + +![](img/12-5.png) + +这个直方图向右偏斜;它的右侧有个尾巴。 + +平均值拉向了尾巴的方向。 所以我们预计平均薪酬会比中位数大,事实确实如此。 + +```py +compensation = sf2015.column('Total Compensation') +percentile(50, compensation) +110305.78999999999 +np.mean(compensation) +114725.98411824222 +``` + +大量总体的收入分布往往是右偏的。 当总体的大部分收入中到低,但很小一部分收入很高时,直方图的右侧有条细长的尾巴。 + +平均收入受这条尾巴的影响:尾巴向右延伸得越远,平均值就越大。 但中位数不受分布极值的影响。 这就是经济学家经常用收入分布的中位数来代替平均值的原因。 + +## 可变性 + +平均值告诉我们直方图平衡的位置。 但是在我们所看到的几乎所有的直方图中,值都位于均值的两边。 他们距离均值有多远? 为了回答这个问题,我们将开发一个关于均值的可变性度量。 + +我们首先描述如何计算度量值。 然后我们就会明白,为什么这是很好的计算方法。 + +### 距离均值的偏差的大致大小 + +为了简单起见,我们将在简单数组`any_numbers `的上下文中开始计算,它由四个值组成。 你将会看到,我们的方法非常易于扩展到任何其他数组。 + +```py +any_numbers = make_array(1, 2, 2, 10) +``` + +我们的目标是,大致衡量这些数值离他们的平均水平有多远。 为了实现它,我们首先需要均值: + +```py +# Step 1. The average. + +mean = np.mean(any_numbers) +mean +3.75 +``` + +接下来,我们来看看每个数值离均值有多远。 这些被称为到均值的偏差。 “到均值的偏差”只是每个值减去平均值。 `calculation_steps`表显示了结果。 + +```py +# Step 2. The deviations from average. + +deviations = any_numbers - mean +calculation_steps = Table().with_columns( + 'Value', any_numbers, + 'Deviation from Average', deviations + ) +calculation_steps +``` + + +| Value | Deviation from Average | +| --- | --- | +| 1 | -2.75 | +| 2 | -1.75 | +| 2 | -1.75 | +| 10 | 6.25 | + +一些偏差是负的;它们对应于低于均值的值。 正的偏差对应于高于平均值的值。 + +要计算偏差有多大,计算偏差的平均值是很自然的。 但是当所有的偏差加在一起的时候,会发生一些有趣的事: + +```py +sum(deviations) +0.0 +``` + +正的偏差正好和负的偏差抵消。 无论列表的直方图是什么样子,所有的数字列表都是如此:到均值的偏差总和为零。 + +由于偏差的总和为零,偏差的均值也将为零: + +```py +np.mean(deviations) +0.0 +``` + +因此,偏差的均值不是偏差大小的有用度量。 我们真正想知道的是偏差有多大,不管它们是正的还是负的。 所以我们需要一种方法来消除偏差的符号。 + +有两种历史悠久的丢掉符号的方式:绝对值和平方。 事实证明,采用平方会构建一个度量,带有非常强大的性质,其中一些我们将在这个课程中学习。 + +所以让我们计算所有偏差的平方,来消除符号。 那么我们将计算平方的均值: + +```py +# Step 3. The squared deviations from average + +squared_deviations = deviations ** 2 +calculation_steps = calculation_steps.with_column( + 'Squared Deviations from Average', squared_deviations + ) +calculation_steps +``` + + +| Value | Deviation from Average | Squared Deviations from Average | +| --- | --- | --- | +| 1 | -2.75 | 7.5625 | +| 2 | -1.75 | 3.0625 | +| 2 | -1.75 | 3.0625 | +| 10 | 6.25 | 39.0625 | + +```py +# Step 4. Variance = the mean squared deviation from average + +variance = np.mean(squared_deviations) +variance +13.1875 +``` + +方差:上面计算的偏差平方的均值称为方差。 + +虽然方差确实给了我们延展度的概念,但它和原始变量不是一个量纲,因为它的单位是原始变量的平方。 这使得解释非常困难。 + +所以我们通过计算方差的算术平方根的来返回原来的量纲: + +```py +# Step 5. +# Standard Deviation: root mean squared deviation from average +# Steps of calculation: 5 4 3 2 1 + +sd = variance ** 0.5 +sd +3.6314597615834874 +``` + +### 标准差 + +我们刚计算出来的数量叫做列表的标准差,简写为 SD。 它大致衡量列表中的数字与其平均水平的差距。 + +定义:列表的 SD 定义为方差(偏差平方的均值)的算术平方根。这很拗口。 但是从左到右阅读,你需要执行一系列的步骤的计算。 + +计算:上述五个步骤会产生 SD。 你还可以使用函数`np.std`来计算数组中值的标准差: + +```py +np.std(any_numbers) +3.6314597615834874 +``` + +> 译者注:写在一起就是`np.mean((arr - arr.mean()) ** 2) ** 0.5`。 + +### 使用 SD + +要看看我们可以从SD中学到什么,让我们转向一个比`any_numbers`更有趣的数据集。 `nba13`表包含了 2013 年 NBA 的球员数据。对于每个球员来说,表格中记录了球员通常的位置,他的身高(英寸),体重(磅)和年龄。 + +```py +nba13 = Table.read_table('nba2013.csv') +nba13 +``` + + +| Name | Position | Height | Weight | Age in 2013 | +| --- | --- | --- | --- | --- | +| DeQuan Jones | Guard | 80 | 221 | 23 | +| Darius Miller | Guard | 80 | 235 | 23 | +| Trevor Ariza | Guard | 80 | 210 | 28 | +| James Jones | Guard | 80 | 215 | 32 | +| Wesley Johnson | Guard | 79 | 215 | 26 | +| Klay Thompson | Guard | 79 | 205 | 23 | +| Thabo Sefolosha | Guard | 79 | 215 | 29 | +| Chase Budinger | Guard | 79 | 218 | 25 | +| Kevin Martin | Guard | 79 | 185 | 30 | +| Evan Fournier | Guard | 79 | 206 | 20 | + +(省略了 495 行) + +这里是球员身高的直方图。 + +```py +nba13.select('Height').hist(bins=np.arange(68, 88, 1)) +``` + +![](img/12-6.png) + +NBA 球员身材高大并不奇怪! 他们的平均身高只有 79 英寸(6'7"),比美国男子的平均身高高出 10 英寸。 + +```py +mean_height = np.mean(nba13.column('Height')) +mean_height +79.065346534653472 +``` + +球员的身高距离平均有多远? 这通过身高的 SD 来测量,大约是 3.45 英寸。 + +```py +sd_height = np.std(nba13.column('Height')) +sd_height +3.4505971830275546 +``` + +俄克拉荷马雷霆的高个中锋哈希姆·塔比特(Hasheem Thabeet)是最高的球员,身高 87 英寸。 + +```py +nba13.sort('Height', descending=True).show(3) +``` + +| Name | Position | Height | Weight | Age in 2013 | +| --- | --- | --- | --- | --- | +| Hasheem Thabeet | Center | 87 | 263 | 26 | +| Roy Hibbert | Center | 86 | 278 | 26 | +| Tyson Chandler | Center | 85 | 235 | 30 | + +(省略了 502 行) + +Thabeet 比平均身高高了大约 8 英寸。 + +```py +87 - mean_height +7.9346534653465284 +``` + +这个就是距离均值的偏差,大约是 2.3 乘标准差。 + +```py +(87 - mean_height)/sd_height +2.2995015194397923 +``` + +换句话说,最高球员的身高比均值高了 2.3 个 SD。 + +以赛亚·托马斯(Isaiah Thomas)身高 69 英寸,是 2013 年 NBA 最矮的球员之一。他的身高比均值低了 2.9 个 SD。 + +```py +nba13.sort('Height').show(3) +``` + + +| Name | Position | Height | Weight | Age in 2013 | +| --- | --- | --- | --- | --- | +| Isaiah Thomas | Guard | 69 | 185 | 24 | +| Nate Robinson | Guard | 69 | 180 | 29 | +| John Lucas III | Guard | 71 | 157 | 30 | + +(省略了 502 行) + +```py +(69 - mean_height)/sd_height +-2.9169868288775844 +``` + +我们观察到,最高和最矮的球员都距离平均身高只有几个标准差。 这是例子,说明了为什么 SD 是延展度的有效度量。无论直方图的形状如何,平均值和 SD 一起告诉你很多东西,关于直方图在数轴上的位置。 + +### 使用 SD 度量延展度的最主要原因 + +非正式声明:在所有的数值数据集中,大部分条目都在“均值上下几个标准差”的范围内。 + +现在,先克制住自己,不要了解“散”,“少”等模糊词的确切含义。 我们将在本节的后面进行详细说明。 我们仅仅在更多示例的背景下研究这个陈述。 + +我们已经看到,所有 NBA 球员的身高都在“均值上下几个标准差”的范围内。 + +那年龄呢? 这里是分布的直方图,以及年龄的平均值和标准差。 + +```py +nba13.select('Age in 2013').hist(bins=np.arange(15, 45, 1)) +``` + +![](img/12-7.png) + +```py +ages = nba13.column('Age in 2013') +mean_age = np.mean(ages) +sd_age = np.std(ages) +mean_age, sd_age +(26.19009900990099, 4.3212004417203067) +``` + +平均年龄只有 26 岁,标准差大约是 4.3 岁。 + +年龄与均值相差多远? 就像我们对身高所做的那样,让我们看看两个年龄的极端值。 + +Juwan Howard 是年龄最大的球员 40 岁。 + +```py +nba13.sort('Age in 2013', descending=True).show(3) +``` + + +| Name | Position | Height | Weight | Age in 2013 | +| --- | --- | --- | --- | --- | +| Juwan Howard | Forward | 81 | 250 | 40 | +| Marcus Camby | Center | 83 | 235 | 39 | +| Derek Fisher | Guard | 73 | 210 | 39 | + +(省略了 502 行) + +Howard 的年龄比均值高了 3.2 个标准差。 + +```py +(40 - mean_age)/sd_age +3.1958482778922357 +``` + +年龄最小的是 15 岁的 Jarvis Varnado,他当年在迈阿密热火队(Miami Heat)夺得了 NBA 总冠军。 他的年龄比均值低了 2.6 个标准差。 + +```py +nba13.sort('Age in 2013').show(3) +``` + + +| Name | Position | Height | Weight | Age in 2013 | +| --- | --- | --- | --- | --- | +| Jarvis Varnado | Forward | 81 | 230 | 15 | +| Giannis Antetokounmpo | Forward | 81 | 205 | 18 | +| Sergey Karasev | Guard | 79 | 197 | 19 | + +(省略了 502 行) + +```py +(15 - mean_age)/sd_age +-2.5895811038670811 +``` + +对于高度和年龄,我们观察到的东西非常普遍。 对于所有列表,大部分条目都不超过平均值 2 或 3 个标准差。 + +### 切比雪夫边界 + +俄罗斯数学家切比雪夫(Pafnuty Chebychev,1821-1894)证明了这个结论,使我们的粗略陈述更加精确。 + +对于所有列表和所有数字`z`,“均值上下`z`个标准差”范围内的条目比例至少为 ![](img/tex-12-2.gif)。 + +值得注意的是,结果给出了一个界限,而不是一个确切的数值或近似值。 + +是什么让结果变得强大,对于所有列表来说都是这样呢 - 所有的分布,无论多么不规则? + +具体来说,对于每个列表: + +在“均值上下两个标准差”范围内的比例至少是`1 - 1/4 = 0.75` + +在“均值上下三个标准差”范围内的比例至少为`1 - 1/9 ≈ 0.89` + +在“均值上下 4.5 个标准差”范围内的比例至少为`1 - 1/4.5^2 ≈ 0.95` + +如上所述,切比雪夫的结果给出了一个下界,而不是一个确切的答案或近似值。例如,“均值上下两个标准差”范围内的条目百分比可能比 75% 大得多。但它不会更小。 + +### 标准单位 + +在上面的计算中,`z`的数量是标准单位,高于平均水平的标准差的数量。 + +标准单位的某些值是负值,对应于低于均值的原始值。 标准单位的其他是正值。 但是无论列表的分布如何,切比雪夫边界意味着标准单位一般在`(-5, 5)`范围内。 + +要将一个值转换为标准单位,首先要求出距离平均值有多远,然后将该偏差与标准差比较。 + +![](img/tex-12-3.gif) + +我们将会看到,标准单位经常用于数据分析。 所以定义一个函数,将数值的数组转换为标准单位是很有用的。 + +```py +def standard_units(numbers_array): + "Convert any array of numbers to standard units." + return (numbers_array - np.mean(numbers_array))/np.std(numbers_array) +``` + +### 示例 + +我们在前面的章节中看到,`united`表包含了`Delay`列,包括 2015 年夏天联合航空数千航班的起飞延误时间,以分钟为单位。我们将创建一个名为`Delay (Standard Units)`的新列, 通过将函数`standard_units`应用于`Delay`列。 这使我们可以看到所有延误时间(分钟)以及标准单位的相应值。 + +```py +united = Table.read_table('united_summer2015.csv') +united = united.with_column( + 'Delay (Standard Units)', standard_units(united.column('Delay')) +) +united +``` + + +| Date | Flight Number | Destination | Delay | Delay (Standard Units) | +| --- | --- | --- | --- | --- | +| 6/1/15 | 73 | HNL | 257 | 6.08766 | +| 6/1/15 | 217 | EWR | 28 | 0.287279 | +| 6/1/15 | 237 | STL | -3 | -0.497924 | +| 6/1/15 | 250 | SAN | 0 | -0.421937 | +| 6/1/15 | 267 | PHL | 64 | 1.19913 | +| 6/1/15 | 273 | SEA | -6 | -0.573912 | +| 6/1/15 | 278 | SEA | -8 | -0.62457 | +| 6/1/15 | 292 | EWR | 12 | -0.117987 | +| 6/1/15 | 300 | HNL | 20 | 0.0846461 | +| 6/1/15 | 317 | IND | -10 | -0.675228 | + +(省略了 13815 行) + +我们看到的标准单位与我们根据切比雪夫边界的预期一致。 大部分都是相当小的值;只有一个大于 6。 + +但是,当我们将延误时间从高到低排序时,会发生一些惊人的事情。 我们看到的标准单位是非常高的! + +```py +united.sort('Delay', descending=True) +``` + +| Date | Flight Number | Destination | Delay | Delay (Standard Units) | +| --- | --- | --- | --- | --- | +| 6/21/15 | 1964 | SEA | 580 | 14.269 | +| 6/22/15 | 300 | HNL | 537 | 13.1798 | +| 6/21/15 | 1149 | IAD | 508 | 12.4453 | +| 6/20/15 | 353 | ORD | 505 | 12.3693 | +| 8/23/15 | 1589 | ORD | 458 | 11.1788 | +| 7/23/15 | 1960 | LAX | 438 | 10.6722 | +| 6/23/15 | 1606 | ORD | 430 | 10.4696 | +| 6/4/15 | 1743 | LAX | 408 | 9.91236 | +| 6/17/15 | 1122 | HNL | 405 | 9.83637 | +| 7/27/15 | 572 | ORD | 385 | 9.32979 | + +(省略了 13815 行) + +这表明,数据有可能高于均值很多个标准差(对于延误了 10 个小时的航班)。 延误的最高值超过 14 个标准单位。 + +然而,这些极端值的比例很小,切比雪夫边界仍然是真的。 例如,让我们计算在“均值上下三个标准差”范围内的延误百分比。 这与标准单位在`(-3, 3)`范围内的时间百分比相同。 这大约是 98%,计算在下面,和切比雪夫边界“至少 89%”一致。 + +```py +within_3_sd = united.where('Delay (Standard Units)', are.between(-3, 3)) +within_3_sd.num_rows/united.num_rows +0.9790235081374322 +``` + +延误时间的直方图如下所示,横轴以标准单位表示。 从上表中可以看出,右边的尾巴一直延伸到`z = 14.27 `个标准单位(580 分钟)。 在`z = -3`到`z = 3`范围外的直方图面积大约是 2%,加起来非常小,在直方图中几乎不可见。 + +```py +united.hist('Delay (Standard Units)', bins=np.arange(-5, 15.5, 0.5)) +plots.xticks(np.arange(-6, 17, 3)); +``` + +![](img/12-8.png) + +## 标准差和正态曲线 + +我们知道均值是直方图的平衡点。 标准差与平均值不同,通常不容易通过查看直方图来识别。 + +然而,有一种分布形状,它的标准差与平均值几乎一样清晰可辨。 这是钟形分布。 本节将查看该形状,因为它经常出现在概率直方图中,也出现在一些数据的直方图中。 + +### 数据的大致钟形的直方图 + +让我们看看母亲的身高分布,它们在我们熟悉的 1174 对母亲和新生儿的样本中。母亲的平均身高为 64 英寸,SD 为 2.5 英寸。 与篮球运动员的身高不同,母亲身高关于钟形曲线中的平均值对称分布。 + +```py +baby = Table.read_table('baby.csv') +heights = baby.column('Maternal Height') +mean_height = np.round(np.mean(heights), 1) +mean_height +64.0 +sd_height = np.round(np.std(heights), 1) +sd_height +2.5 +baby.hist('Maternal Height', bins=np.arange(55.5, 72.5, 1), unit='inch') +positions = np.arange(-3, 3.1, 1)*sd_height + mean_height +plots.xticks(positions); +``` + +![](img/12-9.png) + +上面单元格中的最后两行代码更改了横轴的标签。 现在,对于`z=0, ±1, ±2, ±3`,标签对应于“标签上下`z`个标准差”。 由于分布的形状,“中心”具有明确的含义,在 64 处清晰可见。 + +### 如何定位钟形曲线上的 SD + +要看 SD 如何与曲线相关,请从曲线顶部开始,向右看。 请注意,曲线有一个地方,从看起来像“倒扣的杯子”,变为“朝右的杯子”。 在形式上,曲线有一个拐点。 这个点高于均值一个 SD。 这是`z = 1`的点,即“均值加一个标准差”,为 66.5 英寸。 + +在均值的左边也对称,拐点在`z = -1`处,也就是“均值减一个标准差”,为 61.5 英寸。 + +一般来说,对于钟形分布,SD 是均值和任一侧的拐点之间的距离。 + +### 标准正态曲线 + +除了轴上的标签,我们所看到的所有钟形直方图,看起来基本相同。 的确,通过适当地重新标记坐标轴,从所有这些曲线中,实际上只能绘制一条曲线。 + +为了绘制这条基本曲线,我们将使用标准单位,我们可以将每个列表转换成它。所得到的曲线因此被称为标准正态曲线。 + +标准正态曲线的方程令人印象深刻。 但是现在,最好把它看作是变量直方图的平滑轮廓,变量以标准单位测量并具有钟形分布。 + +![](img/tex-12-4.gif) + +![](img/12-10.png) + +与往常一样,当你检查新的直方图时,首先查看横轴。在标准正态曲线的横轴上,这些值是标准单位。 + +这里是曲线的一些属性。有些是通过观察显而易见的,有些则需要大量的数学才能建立起来。 + +曲线下面的总面积是1.所以你可以把它看作是绘制为密度标度的直方图。 + +曲线是对称的。所以如果一个变量具有这个分布,它的平均值和中位数都是 0。 + +曲线的拐点在 -1 和 +1 处。 + +如果一个变量具有这种分布,那么它的 SD 是 1。正态曲线是 SD 清晰可辨的极少数分布之一。 + +由于我们将曲线视为平滑的直方图,因此我们希望用曲线下方的面积来表示数据总量的比例。 + +平滑曲线下的面积通常是通过微积分来计算的,使用一种称为积分的方法。然而,一个数学的事实是,标准的正态曲线不能通过任何微积分方式来积分。 + +因此,曲线下方的面积必须近似。这就是几乎所有的统计教科书,都带有曲线下方的面积的原因。这也是所有统计系统,包括 Python 模块在内,都包含提供这些面积的优秀近似的方法的原因。 + +```py +from scipy import stats +``` + +### 标准正态的累积分布函数(CDF) + +用于求出正态曲线下的面积的基本函数是`stats.norm.cdf`。 它接受一个数值参数,并返回曲线下,该数值的左侧的所有面积。 它在形式上被称为标准正态曲线的“累积分布函数”。 在口语里缩写为 CDF。 + +让我们使用这个函数来求出标准正态曲线下,`z=1`左侧的面积。 + +![](img/12-11.png) + +阴影区域的数值可以通过调用`stats.norm.cdf`来求出。 + +```py +stats.norm.cdf(1) +0.84134474606854293 +``` + +这大概是 84%。 现在我们可以使用曲线的对称性,以及曲线下面的总面积为 1 事实,来求出其他面积。 + +`z = 1`右侧的面积大概是`100% - 84% = 16%`。 + +![](img/12-12.png) + +```py +1 - stats.norm.cdf(1) +0.15865525393145707 +``` + +`z = -1`和`z = 1`之间的面积可以用几种不同的方式来计算。 它是下面的曲线下方的金色区域。 + +![](img/12-13.png) + +例如,我们可以将面积计算为“`100% -`两个相等的尾巴”,结果大致是`100% - 2X16% = 68%`。 + +或者我们可以注意到,`z = 1`和`z = -1`之间的区域等于`z = 1`左边的所有区域,减去`z = -1`左边的所有区域。 + +```py +stats.norm.cdf(1) - stats.norm.cdf(-1) +0.68268949213708585 +``` + +通过类似的计算,我们看到`-2`和`2`之间的区域大约是 95%。 + +![](img/12-14.png) + +```py +stats.norm.cdf(2) - stats.norm.cdf(-2) +0.95449973610364158 +``` + +换句话说,如果一个直方图大致是钟形,那么在“均值上下两个标准差”范围内的数据比例大约是 95%。 + +这比切比雪夫的下界 75% 还要多。 切比雪夫边界较弱,因为它必须适用于所有的分布。 如果我们知道一个分布是正态的,那么我们就有很好的比例近似,而不仅仅是边界。 + +下表比较了我们对所有分布和正态分布的了解。 请注意,当`z = 1`时,切比雪夫的边界是正确的,但没有启发性。 + + +| Percent in Range | All Distributions: Bound | Normal Distribution: Approximation | +| --- | --- | --- | +| 均值上下一个标准差 | 至少 0% | 约 68% | +| 均值上下两个标准差 | 至少 75% | 约 95% | +| 均值上下三个标准差 | 至少 88.888...% | 约 99.73% | + +## 中心极限定律 + +我们在本课程中看到的很少数据直方图是钟形的。 当我们遇到一个钟形的分布时,它几乎总是一个基于随机样本的统计量的经验直方图。 + +下面的例子显示了两个非常不同的情况,其中在这样的直方图中出现了近似的钟形。 + +### 轮盘赌的净收益 + +在前面的章节中,如果我们在轮盘的不同轮次上重复下相同的赌注,那么我们所花费的总金额的粗略形状就会成为钟形。 + +```py +wheel +``` + +| Pocket | Color | +| --- | --- | +| 0 | green | +| 00 | green | +| 1 | red | +| 2 | black | +| 3 | red | +| 4 | black | +| 5 | red | +| 6 | black | +| 7 | red | +| 8 | black | + +(省略了 28 行) + +回想一下,红色的下注返回相等的钱,1 比 1。我们定义的函数`red_winnings`返回对红色下注一美元的净收益。具体来说,该函数将颜色作为参数,如果颜色为红色,则返回 1。 对于所有其他颜色,它返回 -1。 + +```py +def red_winnings(color): + if color == 'red': + return 1 + else: + return -1 +``` + +`red`表展示了红色情况下,每个口袋的奖金。 + +```py +red = wheel.with_column( + 'Winnings: Red', wheel.apply(red_winnings, 'Color') + ) +red +``` + +| Pocket | Color | Winnings: Red | +| --- | --- | --- | +| 0 | green | -1 | +| 00 | green | -1 | +| 1 | red | 1 | +| 2 | black | -1 | +| 3 | red | 1 | +| 4 | black | -1 | +| 5 | red | 1 | +| 6 | black | -1 | +| 7 | red | 1 | +| 8 | black | -1 | + +(省略了 28 行) + +你在赌注上的净收益`Winnings: Red`的随机抽样。 有 1/18 的几率赚一美元,20/38 的几率损失一美元。 这个概率分布显示在下面的直方图中。 + +```py +red.select('Winnings: Red').hist(bins=np.arange(-1.5, 1.6, 1)) +``` + +![](img/12-15.png) + +现在假设你多次对红色下注。 你的净收益将是来自上述分布的,多个带放回随机抽样的总和。 + +这将需要一些数学,来列出净收益的所有可能值,以及所有的记录。 我们不会那样做;相反,我们将通过模拟来逼近概率分布,就像我们在这个过程中一直做的那样。 + +下面的代码模拟你的净收益,如果你在轮盘赌的 400 个不同的轮次中,对红色下注一美元。 + +```py +num_bets = 400 +repetitions = 10000 + +net_gain_red = make_array() + +for i in np.arange(repetitions): + spins = red.sample(num_bets) + new_net_gain_red = spins.column('Winnings: Red').sum() + net_gain_red = np.append(net_gain_red, new_net_gain_red) + + +results = Table().with_column( + 'Net Gain on Red', net_gain_red + ) +results.hist(bins=np.arange(-80, 50, 6)) +``` + +![](img/12-16.png) + +这是一个大致钟形的直方图,即使我们正在绘制的分布并不是钟形。 + +中心。分布集中在`-$20`附近。 要知道为什么,请注意,你的奖金在 18/38 左右的下注中为 1 美元,剩下的 20/38 则为负一美元。 所以每个一美元赌注的平均奖金大概是 -5.26 美分: + +```py +average_per_bet = 1*(18/38) + (-1)*(20/38) +average_per_bet +-0.05263157894736842 +``` + +因此,在 400 次下注中,你预计净收益大约是 21 美元。 + +```py +400 * average_per_bet +-21.052631578947366 +``` + +为了确认,我们可以计算 10,000 次模拟净收益的平均值: + +```py +np.mean(results.column(0)) +-20.8992 +``` + +延展。让你的眼睛沿着曲线从中心开始,注意到拐点在 0 附近。在钟形曲线上,SD 是中心到拐点的距离。 中心大概是 -20 美元,这意味着分布的标准差大约是 20 美元。 + +在下一节中,我们将看到 20 美元是怎么来的。 现在,让我们通过简单计算 10,000 个模拟净收益的 SD 来证实我们的观察: + +```py +np.std(results.column(0)) +20.043159415621083 +``` + +总结。 400 次下注的净收益是每个单独赌注的 400 个奖金的总和。 这个总和的概率分布近似正态,我们可以近似它的均值和标准差。 + +### 平均航班延误 + +`united`表包含 2015 年夏季旧金山机场出发的 13,825 个联合航空国内航班的出发延误数据。正如我们以前所见,延误的分布的右侧有着很长的尾巴。 + +```py +united = Table.read_table('united_summer2015.csv') +united.select('Delay').hist(bins=np.arange(-20, 300, 10)) +``` + +![](img/12-17.png) + +平均延误约为 16.6 分钟,SD 约为 39.5 分钟。 注意 SD 与平均值相比有多大。 但是右侧的较大偏差会产生影响,尽管它们在数据中占很小的比例。 + +```py +mean_delay = np.mean(united.column('Delay')) +sd_delay = np.std(united.column('Delay')) + +mean_delay, sd_delay +(16.658155515370705, 39.480199851609314) +``` + +现在假设我们随机抽取了 400 个延误。 如果你愿意,你可以无放回抽样,但是结果与放回抽样非常相似。 如果你从 13,825 个中无放回地抽取几百个,那么每当你抽出一个值时,几乎不会改变总体。 + +在样本中,平均延误会是多少? 我们预计在 16 或 17 左右,因为这是总体的均值。 但可能会有些偏差。 让我们看看我们通过抽样得到了什么。 我们将处理`delay `表,仅包含延迟的列。 + +```py +delay = united.select('Delay') +np.mean(delay.sample(400).column('Delay')) +16.68 +``` + +样本均值根据样本的出现方式而变化,因此我们将重复模拟抽样过程,并绘制样本均值的经验直方图。 这是样本均值的概率直方图的近似值。 + +```py +sample_size = 400 +repetitions = 10000 + +means = make_array() + +for i in np.arange(repetitions): + sample = delay.sample(sample_size) + new_mean = np.mean(sample.column('Delay')) + means = np.append(means, new_mean) + +results = Table().with_column( + 'Sample Mean', means +) +results.hist(bins=np.arange(10, 25, 0.5)) +``` + +![](img/12-18.png) + +即使我们从非常偏斜的分布抽样,我们再次看到了大致的钟形。 正如我们所期望的那样,这个钟形的中心在 16 到 17 之间。 + +### 中心极限定律 + +钟形出现在这样的环境中的原因,是一个概率理论的显着结果,称为中心极限定律。 + +中心极限定理表明,无论用于抽取样本的总体分布如何,带放回抽取的大型随机样本的总和或均值的概率分布大致是正态的。 + +我们在研究切比雪夫边界时指出,不管总体分布如何,结果都可以应用于随机样本,这非常强大,因为在数据科学中,我们很少知道总体的分布。 + +如果我们有一个大型随机样本,那么中心极限定理就能够在总体知识很少的情况下进行推理。 这就是它是统计推断领域的核心的原因。 + +### 紫色的花的分布 + +回忆孟德尔的豌豆植物的花朵颜色的概率模型。 该模型表明,植物的花朵颜色类似于来自`{紫色,紫色,紫色,白色}`的带放回随机抽样。 + +在植物的大型样本中,紫色的花约有多少比例? 我们预计答案约为 0.75,模型中紫色的比例。 而且,由于比例是均值,中心极限定理表明,紫色的样本比例的分布大致是正态的。 + +我们可以通过模拟来确认。 我们来模拟 200 株植物样本中紫色的花的比例。 + +```py +colors = make_array('Purple', 'Purple', 'Purple', 'White') + +model = Table().with_column('Color', colors) + +model +``` + + +| Color | +| --- | +| Purple | +| Purple | +| Purple | +| White | + +```py +props = make_array() + +num_plants = 200 +repetitions = 10000 + +for i in np.arange(repetitions): + sample = model.sample(num_plants) + new_prop = np.count_nonzero(sample.column('Color') == 'Purple')/num_plants + props = np.append(props, new_prop) + +results = Table().with_column('Sample Proportion: 200', props) +results.hist(bins=np.arange(0.65, 0.85, 0.01)) +``` + +![](img/12-19.png) + +正如你所期望的那样,中央极限定理预测了,正态曲线再次集中于 0.75 左右。 + +如果我们增加样本量,这个分布如何变化? 让我们再次运行代码,样本量为 800 ,并将模拟结果收集在同一个表中,我们在里面收集了样本量为 200 的模拟结果。我们使重复次数与之前相同,以便两列具有相同的长度。 + +```py +props2 = make_array() + +num_plants = 800 + +for i in np.arange(repetitions): + sample = model.sample(num_plants) + new_prop = np.count_nonzero(sample.column('Color') == 'Purple')/num_plants + props2 = np.append(props2, new_prop) + +results = results.with_column('Sample Proportion: 800', props2) +results.hist(bins=np.arange(0.65, 0.85, 0.01)) +``` + +![](img/12-20.png) + +两个分布都大致是正态,但一个比另一个更窄。 样本量为 800 的比例,比样本量为 200 的比例更紧密地聚集在 0.75 左右。增加样本量可以减少样本比例的可变性。 + +这应该不会令人惊讶。 我们多次产生了这样的直觉,更大的样本量通常会降低统计量的可变性。 然而,在样本均值的案例中,我们可以量化样本量和可变性之间的关系。 + +样本量究竟是如何影响样本均值或比例的可变性呢? 这是我们将在下一节中讨论的问题。 + +## 样本均值的可变性 + +根据中心极限定理,大型随机样本的均值的概率分布是大致正态的。 钟形曲线以总体平均值为中心。 一些样本均值较高,有些则较低,但距离总体均值的偏差在两边大致对称,正如我们已经看到的那样。 形式上,概率论表明样本均值是总体均值的无偏估计。 + +在我们的模拟中,我们也注意到较大样本的均值,相对较小样本的平均值更倾向于紧密聚集于总体均值附近。 在本节中,我们将量化样本均值的可变性,并建立可变性和样本量之间的关系。 + +我们从航班延误表开始。 平均延误时间约为 16.7 分钟,延误分布右倾。 + +```py +united = Table.read_table('united_summer2015.csv') +delay = united.select('Delay') +pop_mean = np.mean(delay.column('Delay')) +pop_mean +16.658155515370705 +``` + +![](img/12-21.png) + +现在我们来随机抽样,来查看样本均值的概率分布。 像往常一样,我们将使用模拟来得到这种分布的经验近似。 + +我们将定义一个函数`simulate_sample_mean`来实现,因为我们将在稍后改变样本量。 参数是表的名称,包含变量的列标签,样本量和模拟次数。 + +```py +"""Empirical distribution of random sample means""" + +def simulate_sample_mean(table, label, sample_size, repetitions): + + means = make_array() + + for i in range(repetitions): + new_sample = table.sample(sample_size) + new_sample_mean = np.mean(new_sample.column(label)) + means = np.append(means, new_sample_mean) + + sample_means = Table().with_column('Sample Means', means) + + # Display empirical histogram and print all relevant quantities + sample_means.hist(bins=20) + plots.xlabel('Sample Means') + plots.title('Sample Size ' + str(sample_size)) + print("Sample size: ", sample_size) + print("Population mean:", np.mean(table.column(label))) + print("Average of sample means: ", np.mean(means)) + print("Population SD:", np.std(table.column(label))) + print("SD of sample means:", np.std(means)) +``` + +让我们模拟 100 个延误的随机样本的均值,然后是 400 个,最后是 625 个延误的均值。 我们将对这些过程中的每一个执行 10,000 次重复。 `xlim`和`ylim`在所有图表中设置一致的坐标轴,以便比较。 你可以忽略每个单元格中的这两行代码。 + +```py +simulate_sample_mean(delay, 'Delay', 100, 10000) +plots.xlim(5, 35) +plots.ylim(0, 0.25); +Sample size: 100 +Population mean: 16.6581555154 +Average of sample means: 16.662059 +Population SD: 39.4801998516 +SD of sample means: 3.90507237968 +``` + +![](img/12-22.png) + +```py +simulate_sample_mean(delay, 'Delay', 400, 10000) +plots.xlim(5, 35) +plots.ylim(0, 0.25); +Sample size: 400 +Population mean: 16.6581555154 +Average of sample means: 16.67117625 +Population SD: 39.4801998516 +SD of sample means: 1.98326299651 +``` + +![](img/12-23.png) + +```py +simulate_sample_mean(delay, 'Delay', 625, 10000) +plots.xlim(5, 35) +plots.ylim(0, 0.25); +Sample size: 625 +Population mean: 16.6581555154 +Average of sample means: 16.68523712 +Population SD: 39.4801998516 +SD of sample means: 1.60089096006 +``` + +![](img/12-24.png) + +你可以在实践中看到中心极限定律 - 样本均值的直方图是大致正态的,即使延误本身的直方图与正态分布相差甚远。 + +你还可以看到,样本均值的三个直方图中的每一个中心都非常接近总体均值。 在每种情况下,“样本均值的均值”非常接近 16.66 分钟,是总体均值。 每个直方图上方的打印输出都提供了这两个值。 像预期一样,样本均值是对总体均值的无偏估计。 + +### 所有样本均值的 SD + +随着样本量的增加,你还可以看到直方图变窄,因此更高。 我们之前已经看到,但现在我们将更加关注延展度的度量。 + +所有延误总体的标准差约为 40 分钟。 + +```py +pop_sd = np.std(delay.column('Delay')) +pop_sd +39.480199851609314 +``` + +看看上面的样本均值的直方图中的标准差。在这三个里面,延误总体的标准差约为 40 分钟,因为所有的样本都来自同一个总体。 + +现在来看,样本量为 100 时,所有 10,000 个样本均值的标准差。标准差是总体标准差的十分之一。当样本量为 400 时,所有样本均值的标准差约为总体标准差的二十分之一。当样本量为 625 时,样本均值的标准差为总体标准差的二十五分之一。 + + +将样本均值的经验分布的标准差与“总体标准差除以样本量的平方根”的数量进行比较,似乎是一个好主意。 + +这里是数值。对于第一列中的每个样本量,抽取 10,000 个该大小的随机样本,并计算 10,000 个样本均值。第二列包含那些 10,000 个样本均值的标准差。第三列包含计算结果“总体标准差除以样本量的平方根”。 + +该单元格需要一段时间来运行,因为这是大型模拟。但是你很快就会看到它值得等待。 + + +```py +repetitions = 10000 +sample_sizes = np.arange(25, 626, 25) + +sd_means = make_array() + +for n in sample_sizes: + means = make_array() + for i in np.arange(repetitions): + means = np.append(means, np.mean(delay.sample(n).column('Delay'))) + sd_means = np.append(sd_means, np.std(means)) + +sd_comparison = Table().with_columns( + 'Sample Size n', sample_sizes, + 'SD of 10,000 Sample Means', sd_means, + 'pop_sd/sqrt(n)', pop_sd/np.sqrt(sample_sizes) +) +sd_comparison +``` + + +| Sample Size n | SD of 10,000 Sample Means | pop_sd/sqrt(n) | +| --- | --- | --- | +| 25 | 7.95017 | 7.89604 | +| 50 | 5.53425 | 5.58334 | +| 75 | 4.54429 | 4.55878 | +| 100 | 3.96157 | 3.94802 | +| 125 | 3.51095 | 3.53122 | +| 150 | 3.23949 | 3.22354 | +| 175 | 3.00694 | 2.98442 | +| 200 | 2.74606 | 2.79167 | +| 225 | 2.63865 | 2.63201 | +| 250 | 2.51853 | 2.49695 | + +(省略了 15 行) + +第二列和第三列的值非常接近。 如果我们用横轴上的样本量绘制每个列,那么这两个图基本上是不可区分的。 + +```py +sd_comparison.plot('Sample Size n') +``` + +![](img/12-25.png) + +那里确实有两条曲线。但他们彼此如此接近,看起来好像只有一个。 + +我们看到了一个普遍结果的实例。 请记住,上面的图表基于每个样本量的 10,000 个重复。 但是每个样本量有超过 10,000 个样本。 样本均值的概率分布基于大小固定的所有可能样本的均值。 + +固定样本大小。如果样本是从总体中带放回随机抽取的: + +![](img/tex-12-5.gif) + +这是所有可能样本均值的标准差。 它大致衡量了样本均值与总体均值的差距。 + +### 用于样本均值的中心极限定律 + +如果从总体中带放回地抽取大型随机样本,那么不管总体分布情况如何,样本均值的概率分布大致是正态的,以总体均值为中心,标准等于总体标准差除以样本量的平方根。 + +### 样本均值的准确性 + +所有可能的样本均值的标准差表示样本均值的变化程度。因此,它被视为样本均值作为总体均值的估计的准确度的一个度量。标准差越小,估计越准确。 + +公式表明: + ++ 总体大小不影响样本均值的准确性。公式中的任何地方都没有出现总体大小。 ++ 总体标准差是一个常数;从总体中抽取的每个样本都是一样的。样本量可以变化。由于样本量出现在分母中,样本均值的可变性随着样本量的增加而降低,因此准确度增加。 + +### 平方根法则 + + +从标准差比较表中可以看出,25 次航班延误的随机样本的均值的标准差约为 8 分钟。 如果你将样本量乘以 4,你将得到大小为 100 的样本。所有这些样本的均值的标准差约为 4 分钟。 这比 8 分钟还小,但并不是 4 倍,只有 2 倍。 这是因为分母中的样本量上面有一个平方根。 样本量增加了 4 倍,但标准差下降了`2 = sqrt(4)`倍。 换句话说,准确度上升了`2 = sqrt(4)`倍。 + +一般来说,当你将样本量乘以一个因数时,样本均值的准确度将会上升该因数的平方根。 + +所以为了提高 10 倍的准确度,你必须将样本量乘以 100 倍。精度并不便宜! + +## 选取样本量 + +候选人 A 在大选中竞选。一个投票机构想要估计投票给她的选民的比例。假设他们打算随机抽取选民,但实际上他们的抽样方法会更复杂。他们如何决定样本应该多大,才能达到理想的准确度? + +在作出一些假设之后,我们现在可以回答这个问题: + ++ 选民人数非常多,所以我们可以假定随机样本带放回地抽取。 ++ 投票机构将通过为候选人 A 的选民百分比,构建一个约 95% 置信区间来做出估计。 ++ 准确度的理想水平是间隔宽度不应超过 1%。这非常准确!例如,置信区间`(33.2%, 34%)`可以,但`(33.2%, 35%)`不行。 ++ 我们将以候选人 A 的选民比例为例。回想一下,比例是一个平均值,其中总体中的值只有 0(你不计算的个体类型)或 1(你计算的个体类型)。 + +### 置信区间的宽度 + +如果我们有一个随机样本,我们可以使用自举法为候选人 A 的选民百分比构建一个置信区间。但是我们还没有样本 - 我们试图找出样本有多大,为了让我们的置信区间如我们所希望的那样狭窄。 + +在这样的情况下,了解理论预测的结果会有帮助。 + +中心极限定律表明,样本比例大致是正态分布的,以总体中 1 的比例为中心,标准差等于总体中 0 和 1 的标准差除以样本量的平方根。 + +所以即使我们不能把自己的目标作为自举比例的第 2.5 和第 97.5 个百分点,那么置信区间仍然是正态分布的“中间 95%”。 + +有没有另外一种方法来求出间隔有多大?是的,因为我们知道对于正态分布变量,“中心上下两个标准差”的间隔包含 95% 的数据。 + +置信区间将延伸到样本比例的两个标准差,位于中心的任一侧。因此,间隔的宽度将是样本比例的 4 个标准差。 + +我们愿意容忍`1% = 0.01`的宽度。因此,使用上一节中开发的公式: + +![](img/tex-12-6.gif) + +所以: + +![](img/tex-12-7.gif) + +### 01 集合的标准差 + +如果我们知道总体的标准差,我们就完成了。 我们可以计算样本量的平方根,然后取平方得到样本量。 但我们不知道总体的标准差。 总体中,候选人 A 的每个选民为 1,其余选民为 0,我们不知道每种选民的比例是多少。 这就是我们正在估计的。 + +那么我们卡住了吗? 不,因为我们可以限制人口的标准差。 这里是两个这样的分布的直方图,一个是相等比例的 1 和 0 ,另一个是 90% 的 1 和 10% 的 0。 哪一个标准差更大? + +![](img/12-26.png) + +请记住,总体中的可能值只有 0 和 1。 + +蓝色直方图(50% 的 1 和 50% 的 0)比金色延展度更大。 它的均值是 0.5。 距离均值的偏差,一半等于 0.5,另一半等于 -0.5,所以标准差是 0.5。 + +在金色直方图中,所有的区域都挤压在 1 左右,从而延展度更小。 90% 的偏差很小,为 0.1。 其他的 10% 是 -0.9 ,较大,但总体上的延展度比蓝色直方图小。 + +如果我们改变 1 的比例或者让 0 的比例大于 1 的比例,那么同样的观察也成立。 我们通过计算不同比例,只包含 0 和 1 的 10 个元素的总体的标准差来检查它。 函数`np.ones`对此很有用。 它接受一个正整数作为它的参数,并返回一个由多个 1 组成的数组。 + +```py +sd = make_array() +for i in np.arange(1, 10, 1): + # Create an array of i 1's and (10-i) 0's + population = np.append(np.ones(i), 1-np.ones(10-i)) + sd = np.append(sd, np.std(population)) + +zero_one_sds = Table().with_columns( + "Population Proportion of 1's", np.arange(0.1, 1, 0.1), + "Population SD", sd +) + +zero_one_sds +``` + + +| Population Proportion of 1's | Population SD | +| --- | --- | +| 0.1 | 0.3 | +| 0.2 | 0.4 | +| 0.3 | 0.458258 | +| 0.4 | 0.489898 | +| 0.5 | 0.5 | +| 0.6 | 0.489898 | +| 0.7 | 0.458258 | +| 0.8 | 0.4 | +| 0.9 | 0.3 | + +毫不奇怪,10% 的 1 和 90% 的 0 的总体标准差,与 90% 的 1 和 10% 的 0 的总体标准差相同。 那是因为你把直方图的一个条和两一个条互换,延展度没有变化。 + +更重要的是,出于我们的目的,标准差随着 1 的比例增加而增加,直到 1 的比例为 0.5;然后开始对称下降。 + +```py +zero_one_sds.scatter("Population Proportion of 1's") +``` + +![](img/12-27.png) + +总结:01 总体的标准差最大为 0.5。 当 50% 的总体为 1 而另外 50% 为 0 时,这就是标准差的值。 + +### 样本量 + +我们知道了 ![](img/tex-12-7.gif),并且 01 总体的标准差最大为 0.5,无论总体中 1 的比例。 所以这样是安全的: + +![](img/tex-12-8.gif) + +所以样本量应该至少是`200 ^ 2 = 40,000`。 这是一个巨大的样本! 但是,如果你想以较高的置信度确保高精度,不管总体是什么样子,那就是你所需要的。 diff --git a/docs/data8-textbook-zh/13.md b/docs/data8-textbook-zh/13.md new file mode 100644 index 0000000000000000000000000000000000000000..ef52bae1baef3cffe92fe08b4d4fa96e109d5f1f --- /dev/null +++ b/docs/data8-textbook-zh/13.md @@ -0,0 +1,1478 @@ +# 十三、预测 + +> 原文:[Prediction](https://github.com/data-8/textbook/tree/gh-pages/chapters/13) + +> 译者:[飞龙](https://github.com/wizardforcel) + +> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) + +> 自豪地采用[谷歌翻译](https://translate.google.cn/) + +数据科学的一个重要方面,是发现数据可以告诉我们什么未来的事情。气候和污染的数据说了几十年内温度的什么事情?根据一个人的互联网个人信息,哪些网站可能会让他感兴趣?病人的病史如何用来判断他或她对治疗的反应? + +为了回答这样的问题,数据科学家已经开发出了预测的方法。在本章中,我们将研究一种最常用的方法,基于一个变量的值来预测另一个变量。 + +方法的基础由弗朗西斯·高尔顿爵士(Sir Francis Galton)奠定。我们在 7.1 节看到,高尔顿研究了身体特征是如何从一代传到下一代的。他最著名的工作之一,是根据父母的高度预测子女的身高。我们已经研究了高尔顿为此收集的数据集。`heights `表包含了 934 个成年子女的双亲身高和子女身高(全部以英寸为单位)。 + +```py +# Galton's data on heights of parents and their adult children +galton = Table.read_table('galton.csv') +heights = Table().with_columns( + 'MidParent', galton.column('midparentHeight'), + 'Child', galton.column('childHeight') + ) +heights +``` + + +| MidParent | Child | +| --- | --- | +| 75.43 | 73.2 | +| 75.43 | 69.2 | +| 75.43 | 69 | +| 75.43 | 69 | +| 73.66 | 73.5 | +| 73.66 | 72.5 | +| 73.66 | 65.5 | +| 73.66 | 65.5 | +| 72.06 | 71 | +| 72.06 | 68 | + +(省略了 924 行) + +```py +heights.scatter('MidParent') +``` + +![](img/13-1.png) + +收集数据的主要原因是能够预测成年子女的身高,他们的父母与数据集中相似。 在注意到两个变量之间的正相关之后,我们在第 7.1 节中做了这些预测。 + +我们的方法是,基于新人的双亲身高周围的所有点来做预测。 为此,我们编写了一个名为`predict_child`的函数,该函数以双亲身高作为参数,并返回双亲身高在半英寸之内的,所有子女的平均身高。 + +```py +def predict_child(mpht): + """Return a prediction of the height of a child + whose parents have a midparent height of mpht. + + The prediction is the average height of the children + whose midparent height is in the range mpht plus or minus 0.5 inches. + """ + + close_points = heights.where('MidParent', are.between(mpht-0.5, mpht + 0.5)) + return close_points.column('Child').mean() +``` + +我们将函数应用于`Midparent`列,可视化我们的结果。 + +```py +# Apply predict_child to all the midparent heights + +heights_with_predictions = heights.with_column( + 'Prediction', heights.apply(predict_child, 'MidParent') + ) +# Draw the original scatter plot along with the predicted values + +heights_with_predictions.scatter('MidParent') +``` + +![](img/13-2.png) + +给定双亲身高的预测值,大致位于给定身高处的垂直条形的中心。这种预测方法称为回归。 本章后面我们会看到这个术语的来源。 我们也会看到,我们是否可以避免将“接近”任意定义为“在半英寸之内”。 但是首先我们要开发一个可用于很多环境的方法,来决定一个变量作为另一个变量的预测值有多好。 + +## 相关性 + +在本节中,我们将开发一种度量,度量散点图紧密聚集在一条直线上的程度。 形式上,这被称为测量线性关联。 + +`hybrid`表包含了 1997 年到 2013 年在美国销售的混合动力车的数据。数据来自佛罗里达大学 [Larry Winner 教授](http://www.stat.ufl.edu/~winner/)的在线数据档案。这些列为: + ++ `vehicle`:车的型号 ++ `year`:出厂年份 ++ `msrp`: 2013 年制造商的建议零售价(美元) ++ `acceleration`: 加速度(千米每小时每秒) ++ `mpg`: 燃油效率(英里每加仑) ++ `class`: 型号的类别 + +(省略了 143 行) + +下图是`msrp`与`acceleration`的散点图。 这意味着`msrp`绘制在纵轴上并且`acceleration`在横轴上。 + +```py +hybrid.scatter('acceleration', 'msrp') +``` + +![](img/13-3.png) + +注意正相关。 散点图倾斜向上,表明加速度较大的车辆通常成本更高;相反,价格更高的汽车通常具有更大的加速。 + +`msrp`与`mpg`的散点图表明了负相关。 `mpg`较高的混合动力车往往成本较低。 这似乎令人惊讶,直到你明白了,加速更快的汽车往往燃油效率更低,行驶里程更低。 之前的散点图显示,这些也是价格更高的车型。 + +```py +hybrid.scatter('mpg', 'msrp') +``` + +![](img/13-4.png) + +除了负相关,价格与效率的散点图显示了两个变量之间的非线性关系。 这些点似乎围绕在一条曲线周围,而不是一条直线。 + +但是,如果我们只将数据限制在 SUV 类别中,价格和效率之间仍然负相关的,但是这种关系似乎更为线性。 SUV 价格与加速度之间的关系也呈线性趋势,但是斜率是正的。 + +```py +suv = hybrid.where('class', 'SUV') +suv.scatter('mpg', 'msrp') +``` + +![](img/13-5.png) + +```py +suv.scatter('acceleration', 'msrp') +``` + +![](img/13-6.png) + +你会注意到,即使不关注变量被测量的单位,我们也可以从散点图的大体方向和形状中得到有用的信息。 + +事实上,我们可以将所有的变量绘制成标准单位,并且绘图看起来是一样的。 这给了我们一个方法,来比较两个散点图中的线性程度。 + +回想一下,在前面的章节中,我们定义了`standard_units`函数来将数值数组转换为标准单位。 + +```py +def standard_units(any_numbers): + "Convert any array of numbers to standard units." + return (any_numbers - np.mean(any_numbers))/np.std(any_numbers) +``` + +我们可以使用这个函数重新绘制 SUV 的两个散点图,所有变量都以标准单位测量。 + +```py +Table().with_columns( + 'mpg (standard units)', standard_units(suv.column('mpg')), + 'msrp (standard units)', standard_units(suv.column('msrp')) +).scatter(0, 1) +plots.xlim(-3, 3) +plots.ylim(-3, 3); +``` + +![](img/13-7.png) + +```py +Table().with_columns( + 'acceleration (standard units)', standard_units(suv.column('acceleration')), + 'msrp (standard units)', standard_units(suv.column('msrp')) +).scatter(0, 1) +plots.xlim(-3, 3) +plots.ylim(-3, 3); +``` + +![](img/13-8.png) + +我们在这些数字中看到的关联与我们之前看到的一样。 另外,由于现在两张散点图的刻度完全相同,我们可以看到,第二张图中的线性关系比第一张图中的线性关系更加模糊。 + +我们现在将定义一个度量,使用标准单位来量化我们看到的这种关联。 + +### 相关系数 + +相关系数测量两个变量之间线性关系的强度。 在图形上,它测量散点图聚集在一条直线上的程度。 + +相关系数这个术语不容易表述,所以它通常缩写为相关性并用`r`表示。 + +以下是一些关于`r`的数学事实,我们将通过模拟观察。 + ++ 相关系数`r`是介于`-1`和`1`之间的数字。 ++ `r`度量了散点图围绕一条直线聚集的程度。 ++ 如果散点图是完美的向上倾斜的直线,`r = 1`,如果散点图是完美的向下倾斜的直线,`r = -1`。 + +函数`r_scatter`接受`r`值作为参数,模拟相关性非常接近`r`的散点图。 由于模拟中的随机性,相关性不会完全等于`r`。 + +调用`r_scatter`几次,以`r`的不同值作为参数,并查看散点图如何变化。 + +当`r = 1`时,散点图是完全线性的,向上倾斜。 当`r = -1`时,散点图是完全线性的,向下倾斜。 当`r = 0`时,散点图是围绕水平轴的不定形云,并且变量据说是不相关的。 + +```py +r_scatter(0.9) +``` + +![](img/13-9.png) + +```py +r_scatter(0.25) +``` + +![](img/13-10.png) + +```py +r_scatter(0) +``` + +![](img/13-11.png) + +```py +r_scatter(-0.55) +``` + +![](img/13-12.png) + +### 计算`r` + +目前为止,`r`的公式还不清楚。 它拥有超出本课程范围的数学基础。 然而,你将会看到,这个计算很简单,可以帮助我们理解`r`的几个属性。 + +`r`的公式: + +`r`是两个变量的乘积的均值,这两个变量都以标准单位来衡量。 + +以下是计算中的步骤。 我们将把这些步骤应用于`x`和`y`值的简单表格。 + +```py +x = np.arange(1, 7, 1) +y = make_array(2, 3, 1, 5, 2, 7) +t = Table().with_columns( + 'x', x, + 'y', y + ) +t +``` + +| x | y | +| --- | --- | +| 1 | 2 | +| 2 | 3 | +| 3 | 1 | +| 4 | 5 | +| 5 | 2 | +| 6 | 7 | + +根据散点图,我们预计`r`将是正值,但不等于 1。 + +```py +t.scatter(0, 1, s=30, color='red') +``` + +![](img/13-13.png) + +第一步:将每个变量转换为标准单位。 + +```py +t_su = t.with_columns( + 'x (standard units)', standard_units(x), + 'y (standard units)', standard_units(y) + ) +t_su +``` + +| x | y | x (standard units) | y (standard units) | +| --- | --- | --- | --- | +| 1 | 2 | -1.46385 | -0.648886 | +| 2 | 3 | -0.87831 | -0.162221 | +| 3 | 1 | -0.29277 | -1.13555 | +| 4 | 5 | 0.29277 | 0.811107 | +| 5 | 2 | 0.87831 | -0.648886 | +| 6 | 7 | 1.46385 | 1.78444 | + +第二步:将每一对标准单位相乘 + +```py +t_product = t_su.with_column('product of standard units', t_su.column(2) * t_su.column(3)) +t_product +``` + +| x | y | x (standard units) | y (standard units) | product of standard units | +| --- | --- | --- | --- | +| 1 | 2 | -1.46385 | -0.648886 | 0.949871 | +| 2 | 3 | -0.87831 | -0.162221 | 0.142481 | +| 3 | 1 | -0.29277 | -1.13555 | 0.332455 | +| 4 | 5 | 0.29277 | 0.811107 | 0.237468 | +| 5 | 2 | 0.87831 | -0.648886 | -0.569923 | +| 6 | 7 | 1.46385 | 1.78444 | 2.61215 | + +第三步:`r`是第二步计算的乘积的均值。 + +```py +# r is the average of the products of standard units + +r = np.mean(t_product.column(4)) +r +0.61741639718977093 +``` + +正如我们的预期,`r`是个不等于的正值。 + +### `r`的性质 + +计算结果表明: + +`r`是一个纯数字。 它没有单位。 这是因为`r`基于标准单位。 +`r`不受任何轴上单位的影响。 这也是因为`r`基于标准单位。 +`r`不受轴的交换的影响。 在代数上,这是因为标准单位的乘积不依赖于哪个变量被称为`x `和`y`。 在几何上,轴的切换关于`y = x`直线翻转了散点图,但不会改变群聚度和关联的符号。 + +```py +t.scatter('y', 'x', s=30, color='red') +``` + +![](img/13-14.png) + +### `correlation `函数 + + +我们将要重复计算相关性,所以定义一个函数会有帮助,这个函数通过执行上述所有步骤来计算它。 让我们定义一个函数`correlation `,它接受一个表格,和两列的标签。该函数返回`r`,它是标准单位下这些列的值的乘积的平均值。 + +```py +def correlation(t, x, y): + return np.mean(standard_units(t.column(x))*standard_units(t.column(y))) +``` + +让我们在`t`的`x`和`y`列上调用函数。 该函数返回`x`和`y`之间的相关性的相同答案,就像直接应用`r`的公式一样。 + +```py +correlation(t, 'x', 'y') +0.61741639718977093 +``` + +我们注意到,变量被指定的顺序并不重要。 + +```py +correlation(t, 'y', 'x') +0.61741639718977093 +``` + +在`suv`表的列上调用`correlation`,可以使我们看到价格和效率之间的相关性,以及价格和加速度之间的相关性。 + +```py +correlation(suv, 'mpg', 'msrp') +-0.6667143635709919 +correlation(suv, 'acceleration', 'msrp') +0.48699799279959155 +``` + +这些数值证实了我们的观察: + +价格和效率之间存在负相关关系,而价格和加速度之间存在正相关关系。 +价格和加速度之间的线性关系(相关性约为 0.5),比价格和效率之间的线性关系稍弱(相关性约为 -0.67)。 +相关性是一个简单而强大的概念,但有时会被误用。 在使用`r`之前,重要的是要知道相关性能做和不能做什么。 + +### 相关不是因果 + +相关只衡量关联,并不意味着因果。 尽管学区内的孩子的体重与数学能力之间的相关性可能是正的,但这并不意味着做数学会使孩子更重,或者说增加体重会提高孩子的数学能力。 年龄是一个使人混淆的变量:平均来说,较大的孩子比较小的孩子更重,数学能力更好。 + +### 相关性度量线性关联 + +相关性只测量一种关联 - 线性关联。 具有较强非线性关联的变量可能具有非常低的相关性。 这里有一个变量的例子,它具有完美的二次关联`y = x ^ 2`,但是相关性等于 0。 + +```py +new_x = np.arange(-4, 4.1, 0.5) +nonlinear = Table().with_columns( + 'x', new_x, + 'y', new_x**2 + ) +nonlinear.scatter('x', 'y', s=30, color='r') +``` + +![](img/13-15.png) + +```py +correlation(nonlinear, 'x', 'y') +0.0 +``` + +### 相关性受到离群点影响 + +离群点可能对相关性有很大的影响。 下面是一个例子,其中通过增加一个离群点,`r`等于 1 的散点图变成`r`等于 0 的图。 + +```py +line = Table().with_columns( + 'x', make_array(1, 2, 3, 4), + 'y', make_array(1, 2, 3, 4) + ) +line.scatter('x', 'y', s=30, color='r') +``` + +![](img/13-16.png) + +```py +correlation(line, 'x', 'y') +1.0 +outlier = Table().with_columns( + 'x', make_array(1, 2, 3, 4, 5), + 'y', make_array(1, 2, 3, 4, 0) + ) +outlier.scatter('x', 'y', s=30, color='r') +``` + +![](img/13-17.png) + +```py +correlation(outlier, 'x', 'y') +0.0 +``` + +### 生态相关性应谨慎解读 + +基于汇总数据的相关性可能会产生误导。 作为一个例子,这里是 2014 年 SAT 批判性阅读和数学成绩的数据。50 个州和华盛顿特区各有一个点。`Participation Rate`列包含参加考试的高中学生的百分比。 接下来的三列显示了每个州的测试每个部分的平均得分,最后一列是测试总得分的平均值。 + +```py +sat2014 = Table.read_table('sat2014.csv').sort('State') +sat2014 +``` + + +| State | Participation Rate | Critical Reading | Math | Writing | Combined | +| --- | --- | --- | --- | --- | --- | +| Alabama | 6.7 | 547 | 538 | 532 | 1617 | +| Alaska | 54.2 | 507 | 503 | 475 | 1485 | +| Arizona | 36.4 | 522 | 525 | 500 | 1547 | +| Arkansas | 4.2 | 573 | 571 | 554 | 1698 | +| California | 60.3 | 498 | 510 | 496 | 1504 | +| Colorado | 14.3 | 582 | 586 | 567 | 1735 | +| Connecticut | 88.4 | 507 | 510 | 508 | 1525 | +| Delaware | 100 | 456 | 459 | 444 | 1359 | +| District of Columbia | 100 | 440 | 438 | 431 | 1309 | +| Florida | 72.2 | 491 | 485 | 472 | 1448 | + +(省略了 41 行) + +数学得分与批判性阅读得分的散点图紧密聚集在一条直线上; 相关性接近 0.985。 + +```py +sat2014.scatter('Critical Reading', 'Math') +``` + +![](img/13-18.png) + +```py +correlation(sat2014, 'Critical Reading', 'Math') +0.98475584110674341 +``` + +这是个非常高的相关性。但重要的是要注意,这并不能反映学生的数学和批判性阅读得分之间的关系强度。 + +数据由每个州的平均分数组成。但是各州不参加考试 - 而是学生。表中的数据通过将每个州的所有学生聚集为(这个州里面的两个变量的均值处的)单个点而创建。但并不是所有州的学生都会在这个位置,因为学生的表现各不相同。如果你为每个学生绘制一个点,而不是每个州一个点,那么在上图中的每个点周围都会有一圈云状的点。整体画面会更模糊。学生的数学和批判性阅读得分之间的相关性,将低于基于州均值计算的数值。 + +基于聚合和均值的相关性被称为生态相关性,并且经常用于报告。正如我们刚刚所看到的,他们必须谨慎解读。 + +### 严重还是开玩笑? + +2012 年,在著名的《新英格兰医学杂志》(New England Journal of Medicine)上发表的一篇论文,研究了一组国家巧克力消费与的诺贝尔奖之间的关系。《科学美国人》(Scientific American)严肃地做出回应,而其他人更加轻松。 欢迎你自行决定!下面的图表应该让你有兴趣去看看。 + +![](img/13-19.png) + +## 回归直线 + +相关系数`r`并不只是测量散点图中的点聚集在一条直线上的程度。 它也有助于确定点聚集的直线。 在这一节中,我们将追溯高尔顿和皮尔逊发现这条直线的路线。 + +高尔顿的父母及其成年子女身高的数据显示出线性关系。 当我们基于双亲身高的子女身高的预测大致沿着直线时,就证实了线性。 + +```py +galton = Table.read_table('galton.csv') + +heights = Table().with_columns( + 'MidParent', galton.column('midparentHeight'), + 'Child', galton.column('childHeight') + ) +def predict_child(mpht): + """Return a prediction of the height of a child + whose parents have a midparent height of mpht. + + The prediction is the average height of the children + whose midparent height is in the range mpht plus or minus 0.5 inches. + """ + + close_points = heights.where('MidParent', are.between(mpht-0.5, mpht + 0.5)) + return close_points.column('Child').mean() +heights_with_predictions = heights.with_column( + 'Prediction', heights.apply(predict_child, 'MidParent') + ) +heights_with_predictions.scatter('MidParent') +``` + +![](img/13-20.png) + +### 标准单位下的度量 + +让我们看看,我们是否能找到一个方法来确定这条线。 首先,注意到线性关联不依赖于度量单位 - 我们也可以用标准单位来衡量这两个变量。 + +```py +def standard_units(xyz): + "Convert any array of numbers to standard units." + return (xyz - np.mean(xyz))/np.std(xyz) +heights_SU = Table().with_columns( + 'MidParent SU', standard_units(heights.column('MidParent')), + 'Child SU', standard_units(heights.column('Child')) +) +heights_SU +``` + + +| MidParent SU | Child SU | +| --- | --- | +| 3.45465 | 1.80416 | +| 3.45465 | 0.686005 | +| 3.45465 | 0.630097 | +| 3.45465 | 0.630097 | +| 2.47209 | 1.88802 | +| 2.47209 | 1.60848 | +| 2.47209 | -0.348285 | +| 2.47209 | -0.348285 | +| 1.58389 | 1.18917 | +| 1.58389 | 0.350559 | + +(省略了 924 行) + +在这个刻度上,我们可以像以前一样精确地计算我们的预测。 但是首先我们必须弄清楚,如何将“接近”的点的旧定义转换为新的刻度上的一个值。 我们曾经说过,如果双亲高度在 0.5 英寸之内,它们就是“接近”的。 由于标准单位以标准差为单位测量距离,所以我们必须计算出,0.5 英寸是多少个双亲身高的标准差。 + +双亲身高的标准差约为 1.8 英寸。 所以 0.5 英寸约为 0.28 个标准差。 + +```py +sd_midparent = np.std(heights.column(0)) +sd_midparent +1.8014050969207571 +0.5/sd_midparent +0.27756111096536701 +``` + +现在我们准备修改我们的预测函数,来预测标准单位。 所有改变的是,我们正在使用标准单位的值的表格,并定义如上所述的“接近”。 + +```py +def predict_child_su(mpht_su): + """Return a prediction of the height (in standard units) of a child + whose parents have a midparent height of mpht_su in standard units. + """ + close = 0.5/sd_midparent + close_points = heights_SU.where('MidParent SU', are.between(mpht_su-close, mpht_su + close)) + return close_points.column('Child SU').mean() +heights_with_su_predictions = heights_SU.with_column( + 'Prediction SU', heights_SU.apply(predict_child_su, 'MidParent SU') + ) +heights_with_su_predictions.scatter('MidParent SU') +``` + +![](img/13-21.png) + +这个绘图看起来就像在原始刻度上绘图。 只改变了轴上的数字。 这证实了我们可以通过在标准单位下工作,来理解预测过程。 + +### 确定标准单位下的直线 + +高尔顿的散点图形状是个橄榄球 - 就是说,像橄榄球一样大致椭圆形。不是所有的散点图都是橄榄形的,甚至那些线性关联的也不都是。但在这一节中,我们假装我们是高尔顿,只能处理橄榄形的散点图。在下一节中,我们将把我们的分析推广到其他形状的绘图。 + +这里是一个橄榄形散点图,两个变量以标准单位测量。 45 度线显示为红色。 + +![](img/13-22.png) + +但是 45 度线不是经过垂直条形的中心的线。你可以看到在下图中,1.5 个标准单位的垂直线显示为黑色。蓝线附近的散点图上的点的高度都大致在 -2 到 3 的范围内。红线太高,无法命中中心。 + +![](img/13-23.png) + +所以 45 度线不是“均值图”。该线是下面显示的绿线。 + +![](img/13-24.png) + +两条线都经过原点`(0,0)`。绿线穿过垂直条形的中心(至少大概),比红色的 45 度线平坦。 + +45 度线的斜率为 1。所以绿色的“均值图”直线的斜率是正值但小于 1。 + +这可能是什么值呢?你猜对了 - 这是`r`。 + +### 标准单位下的回归直线 + +绿色的“均值图”线被称为回归直线,我们将很快解释原因。 但首先,让我们模拟一些`r`值不同的橄榄形散点图,看看直线是如何变化的。 在每种情况中,绘制红色 45 度线作比较。 + +执行模拟的函数为`regression_line`,并以`r`为参数。 + +```py +regression_line(0.95) +``` + +![](img/13-25.png) + +```py +regression_line(0.6) +``` + +![](img/13-26.png) + +当`r`接近于 1 时,散点图,45 度线和回归线都非常接近。 但是对于`r`较低值来说,回归线显然更平坦。 + +### 回归效应 + +就预测而言,这意味着,对于双亲身高为 1.5 个标准单位的家长来说,我们对女子身高的预测要稍低于 1.5 个标准单位。如果双亲高度是 2 个标准单位,我们对子女身高的预测,会比 2 个标准单位少一些。 + +换句话说,我们预测,子女会比父母更接近均值。 + +弗朗西斯·高尔顿爵士就不高兴了。他一直希望,特别高的父母会有特别高的子女。然而,数据是清楚的,高尔顿意识到,高个子父母通常拥有并不是特别高的子女。高尔顿沮丧地将这种现象称为“回归平庸”。 + +高尔顿还注意到,特别矮的父母通常拥有相对于他们这一代高一些的子女。一般来说,一个变量的平均值远远低于另一个变量的平均值。这被称为回归效应。 + +### 回归直线的方程 + +在回归中,我们使用一个变量(我们称`x`)的值来预测另一个变量的值(我们称之为`y`)。 当变量`x`和`y`以标准单位测量时,基于`x`预测`y`的回归线斜率为`r`并通过原点。 因此,回归线的方程可写为: + +![](img/tex-13-1.gif) + +在数据的原始单位下,就变成了: + +![](img/tex-13-2.gif) + +原始单位的回归线的斜率和截距可以从上图中导出。 + +![](img/tex-13-3.gif) + +![](img/tex-13-4.gif) + +下面的三个函数计算相关性,斜率和截距。 它们都有三个参数:表的名称,包含`x`的列的标签以及包含`y`的列的标签。 + +```py +def correlation(t, label_x, label_y): + return np.mean(standard_units(t.column(label_x))*standard_units(t.column(label_y))) + +def slope(t, label_x, label_y): + r = correlation(t, label_x, label_y) + return r*np.std(t.column(label_y))/np.std(t.column(label_x)) + +def intercept(t, label_x, label_y): + return np.mean(t.column(label_y)) - slope(t, label_x, label_y)*np.mean(t.column(label_x)) +``` + +### 回归直线和高尔顿的数据 + +双亲身高和子女身高之间的相关性是 0.32: + +```py +galton_r = correlation(heights, 'MidParent', 'Child') +galton_r +0.32094989606395924 +``` + +我们也可以找到回归直线的方程,来基于双亲身高预测子女身高: + +```py +galton_slope = slope(heights, 'MidParent', 'Child') +galton_intercept = intercept(heights, 'MidParent', 'Child') +galton_slope, galton_intercept +(0.63736089696947895, 22.636240549589751) +``` + +回归直线的方程是: + +![](img/tex-13-5.gif) + +这也成为回归方程。回归方程的主要用途是根据`x`预测`y`。 + +例如,对于 70.48 英寸的双亲身高,回归直线预测,子女身高为 67.56 英寸。 + +```py +galton_slope*70.48 + galton_intercept +67.557436567998622 +``` + +我们最初的预测,通过计算双亲身高接近 70.48 的所有子女的平均身高来完成,这个预测非常接近:67.63 英寸,而回归线的预测是 67.55 英寸。 + +```py +heights_with_predictions.where('MidParent', are.equal_to(70.48)).show(3) +``` + +| MidParent | Child | Prediction | +| --- | --- | --- | +| 70.48 | 74 | 67.6342 | +| 70.48 | 70 | 67.6342 | +| 70.48 | 68 | 67.6342 | + +(省略了 5 行) + +这里是高尔顿的表格的所有行,我们的原始预测,以及子女身高的回归预测。 + +```py +heights_with_predictions = heights_with_predictions.with_column( + 'Regression Prediction', galton_slope*heights.column('MidParent') + galton_intercept +) +heights_with_predictions +``` + + +| MidParent | Child | Prediction | Regression Prediction | +| --- | --- | --- | --- | +| 75.43 | 73.2 | 70.1 | 70.7124 | +| 75.43 | 69.2 | 70.1 | 70.7124 | +| 75.43 | 69 | 70.1 | 70.7124 | +| 75.43 | 69 | 70.1 | 70.7124 | +| 73.66 | 73.5 | 70.4158 | 69.5842 | +| 73.66 | 72.5 | 70.4158 | 69.5842 | +| 73.66 | 65.5 | 70.4158 | 69.5842 | +| 73.66 | 65.5 | 70.4158 | 69.5842 | +| 72.06 | 71 | 68.5025 | 68.5645 | +| 72.06 | 68 | 68.5025 | 68.5645 | + +(省略了 924 行) + +```py +heights_with_predictions.scatter('MidParent') +``` + +![](img/13-27.png) + +灰色圆点显示回归预测,全部在回归线上。 注意这条线与均值的金色图非常接近。 对于这些数据,回归线很好地逼近垂直条形的中心。 + +### 拟合值 + +所有的预测值都在直线上,被称为“拟合值”。 函数`fit`使用表名和`x`和`y`的标签,并返回一个拟合值数组,散点图中每个点一个。 + +```py +def fit(table, x, y): + """Return the height of the regression line at each x value.""" + a = slope(table, x, y) + b = intercept(table, x, y) + return a * table.column(x) + b +``` + +下图比上图更轻易看到直线: + +```py +heights.with_column('Fitted', fit(heights, 'MidParent', 'Child')).scatter('MidParent') +``` + +![](img/13-28.png) + +另一个绘制直线的方式是在表方法`scatter`中,使用选项`fit_line=True`。 + +```py +heights.scatter('MidParent', fit_line=True) +``` + +![](img/13-29.png) + +### 斜率的测量单位 + +斜率是一个比值,值得花点时间来研究它的测量单位。 我们的例子来自熟悉的医院系统中产妇的数据集。 孕期体重与高度的散点图看起来像是一个橄榄球,已经在一场比赛中使用了很多次,但足够接近橄榄球,我们可以让我们的拟合直线穿过它来证明。 在后面的章节中,我们将看到如何使这种证明更正式。 + +```py +baby = Table.read_table('baby.csv') +baby.scatter('Maternal Height', 'Maternal Pregnancy Weight', fit_line=True) +``` + +![](img/13-30.png) + +```py +slope(baby, 'Maternal Height', 'Maternal Pregnancy Weight') +3.5728462592750558 +``` + +回归线的斜率是 3.57 磅每英寸。 这意味着,对于身高相差 1 英寸的两名女性来说,我们对孕期体重的预测相差 3.57 磅。 对于身高相差 2 英寸的女性,我们预测的孕期体重相差`2 * 3.57 ~= 7.14`磅。 + +请注意,散点图中的连续垂直条形相距 1 英寸,因为高度已经舍入到最近的英寸。 另一种考虑斜率的方法是取两个相连的条形(相隔 1 英寸),相当于两组身高相差 1 英寸的女性。 3.57 磅每英寸的斜率意味着,较高组的平均孕期体重比较矮组多大约 3.57 磅。 + +### 示例 + +假设我们的目标是使用回归,基于巴塞特猎犬的体重来估计它的身高,所用的样本与回归模型看起来一致。 假设观察到的相关性`r`为 0.5,并且这两个变量的汇总统计量如下表所示: + + +| | average | SD | +| --- | --- | --- | +| height | 14 inches | 2 inches | +| weight | 50 pounds | 5 pounds | + +为了计算回归线的方程,我们需要斜率和截距。 + +![](img/tex-13-6.gif) + +![](img/tex-13-7.gif) + +回归线的方程允许我们,根据给定重量(磅)计算估计高度(英寸): + +![](img/tex-13-8.gif) + +线的斜率衡量随着重量的单位增长的估计高度的增长。 斜率是正值,重要的是要注意,这并不表示我们认为,如果体重增加巴塞特猎狗就会变得更高。 斜率反映了两组狗的平均身高的差异,这两组狗的体重相差 1 磅。 具体来说,考虑一组重量为`w`磅,以及另一组重量为`w + 1`磅的狗。 我们估计,第二组的均值高出 0.2 英寸。 对于样本中的所有`w`值都是如此。 + +一般来说,回归线的斜率可以解释为随着`x`单位增长的`y`平均增长。 请注意,如果斜率为负值,那么对于`x`的每单位增长,`y`的平均值会减少。 + +### 尾注 + +即使我们没有建立回归方程的数学基础,我们可以看到,当散点图是橄榄形的时候,它会给出相当好的预测。 这是一个令人惊讶的数学事实,无论散点图的形状如何,同一个方程给出所有直线中的“最好”的预测。 这是下一节的主题。 + +## 最小二乘法 + +我们已经回溯了高尔顿和皮尔森用于开发回归线方程的步骤,它穿过橄榄形的散点图。但不是所有的散点图都是橄榄形的,甚至不是线性的。每个散点图都有一个“最优”直线吗?如果是这样,我们仍然可以使用上一节中开发的斜率和截距公式,还是需要新的公式? + +为了解决这些问题,我们需要一个“最优”的合理定义。回想一下,这条线的目的是预测或估计`y`的值,在给定`x`值的情况下。估计通常不是完美的。每个值都由于误差而偏离真正的值。“最优”直线的合理标准是,它在所有直线中总体误差尽可能最小。 + +在本节中,我们将精确确定这个标准,看看我们能否确定标准下的最优直线。 + +我们的第一个例子是小说《小女人》数据集,每章都有一行。目标是根据句子数来估计字符数(即字母,空格标点符号等等)。回想一下,我们在本课程的第一堂课中试图实现它。 + +```py +little_women = Table.read_table('little_women.csv') +little_women = little_women.move_to_start('Periods') +little_women.show(3) +``` + + +| Periods | Characters | +| --- | --- | +| 189 | 21759 | +| 188 | 22148 | +| 231 | 20558 | + +(省略了 44 行) + +```py +little_women.scatter('Periods', 'Characters') +``` + +![](img/13-31.png) + +为了探索数据,我们将需要使用上一节定义的函数`correlation`,`slope`,`intercept`和`fit `。 + +```py +correlation(little_women, 'Periods', 'Characters') +0.92295768958548163 +``` + +散点图明显接近线性,相关性大于 0.92。 + +### 估计中的误差 + +下图显示了我们在上一节中开发的散点图和直线。 我们还不知道这是否是所有直线中最优的。 我们首先必须准确表达“最优”的意思。 + +```py +lw_with_predictions = little_women.with_column('Linear Prediction', fit(little_women, 'Periods', 'Characters')) +lw_with_predictions.scatter('Periods') +``` + +![](img/13-32.png) + +对应于散点图上的每个点,预测的误差是计算为实际值减去预测值。 它是点与直线之间的垂直距离,如果点在线之下,则为负值。 + +```py +actual = lw_with_predictions.column('Characters') +predicted = lw_with_predictions.column('Linear Prediction') +errors = actual - predicted +lw_with_predictions.with_column('Error', errors) +``` + + +| Periods | Characters | Linear Prediction | Error | +| --- | --- | --- | --- | +| 189 | 21759 | 21183.6 | 575.403 | +| 188 | 22148 | 21096.6 | 1051.38 | +| 231 | 20558 | 24836.7 | -4278.67 | +| 195 | 25526 | 21705.5 | 3820.54 | +| 255 | 23395 | 26924.1 | -3529.13 | +| 140 | 14622 | 16921.7 | -2299.68 | +| 131 | 14431 | 16138.9 | -1707.88 | +| 214 | 22476 | 23358 | -882.043 | +| 337 | 33767 | 34056.3 | -289.317 | +| 185 | 18508 | 20835.7 | -2327.69 | + +(省略了 37 行) + +我们可以使用`slope `和`intercept`来计算拟合直线的斜率和截距。 下图显示了该直线(浅蓝色)。 对应于四个点的误差以红色显示。 这四个点没什么特别的。 他们只是为了展示的清晰而被选中。 函数`lw_errors`以斜率和截距(按照该顺序)作为参数,并绘制该图形。 + +```py +lw_reg_slope = slope(little_women, 'Periods', 'Characters') +lw_reg_intercept = intercept(little_women, 'Periods', 'Characters') +print('Slope of Regression Line: ', np.round(lw_reg_slope), 'characters per period') +print('Intercept of Regression Line:', np.round(lw_reg_intercept), 'characters') +lw_errors(lw_reg_slope, lw_reg_intercept) +Slope of Regression Line: 87.0 characters per period +Intercept of Regression Line: 4745.0 characters +``` + +![](img/13-33.png) + +如果我们用不同的线来创建我们的估计,误差将会不同。 下面的图表显示了如果我们使用另一条线进行估算,误差会有多大。 第二张图显示了通过使用完全愚蠢的线获得了较大误差。 + +```py +lw_errors(50, 10000) +``` + +![](img/13-34.png) + +```py +lw_errors(-100, 50000) +``` + +![](img/13-35.png) + +### 均方根误差(RMSE) + +我们现在需要的是误差大小的一个总体衡量。 你会认识到创建它的方法 - 这正是我们开发标准差的方式。 + +如果你使用任意直线来计算你的估计值,那么你的一些误差可能是正的,而其他的则是负的。 为了避免误差大小在测量时抵消,我们将采用误差平方的均值而不是误差的均值。 + +估计的均方误差大概是误差的平方有多大,但正如我们前面提到的,它的单位很难解释。 取平方根产生均方根误差(RMSE),与预测变量的单位相同,因此更容易理解。 + +### 使 RMSE 最小 + +到目前为止,我们的观察可以总结如下。 + ++ 要根据`x`估算`y`,可以使用任何你想要的直线。 ++ 每个直线都有估计的均方根误差。 ++ “更好”的直线有更小的误差。 + +有没有“最好”的直线? 也就是说,是否有一条线可以使所有行中的均方根误差最小? + +为了回答这个问题,我们首先定义一个函数`lw_rmse`,通过《小女人》的散点图来计算任意直线的均方根误差。 函数将斜率和截距(按此顺序)作为参数。 + +```py +def lw_rmse(slope, intercept): + lw_errors(slope, intercept) + x = little_women.column('Periods') + y = little_women.column('Characters') + fitted = slope * x + intercept + mse = np.mean((y - fitted) ** 2) + print("Root mean squared error:", mse ** 0.5) +lw_rmse(50, 10000) +Root mean squared error: 4322.16783177 +``` + +![](img/13-36.png) + +```py +lw_rmse(-100, 50000) +Root mean squared error: 16710.1198374 +``` + +![](img/13-37.png) + +正如预期的那样,不好的直线 RMSE 很大。 但是如果我们选择接近于回归线的斜率和截距,则 RMSE 要小得多。 + +```py +lw_rmse(90, 4000) +Root mean squared error: 2715.53910638 +``` + +![](img/13-38.png) + +这是对应于回归线的均方根误差。 通过显着的数学事实,没有其他线路能击败这一条。 + +回归线是所有直线之间的唯一直线,使估计的均方误差最小。 + +```py +lw_rmse(lw_reg_slope, lw_reg_intercept) +Root mean squared error: 2701.69078531 +``` + +![](img/13-39.png) + +这个声明的证明需要超出本课程范围的抽象数学。 另一方面,我们有一个强大的工具 -- Python,它可以轻松执行大量的数值计算。 所以我们可以使用 Python 来确认回归线最小化的均方误差。 + +### 数值优化 + +首先注意,使均方根误差最小的直线,也是使平方误差最小的直线。 平方根对最小值没有任何影响。 所以我们会为自己节省一个计算步骤,并将平均方差 MSE 减到最小。 + +我们试图根据《小女人》的句子数(`x`)来预测字符数量(`y`)。 如果我们使用 ![](img/tex-13-9.gif) 直线,它将有一个 MSE,它取决于斜率`a`和截距`b`。 函数`lw_mse`以斜率和截距为参数,并返回相应的 MSE。 + +```py +def lw_mse(any_slope, any_intercept): + x = little_women.column('Periods') + y = little_women.column('Characters') + fitted = any_slope*x + any_intercept + return np.mean((y - fitted) ** 2) +``` + +让我们确认一下,`lw_mse`得到回归线的 RMSE 的正确答案。 请记住,`lw_mse`返回均方误差,所以我们必须取平方根来得到 RMSE。 + +```py +lw_mse(lw_reg_slope, lw_reg_intercept)**0.5 +2701.690785311856 +``` + +![](img/13-40.png) + +它和我们之前使用`lw_rmse `得到的值相同。 + +```py +lw_rmse(lw_reg_slope, lw_reg_intercept) +Root mean squared error: 2701.69078531 +``` + +你可以确认对于其他的斜率和截距,`lw_mse`也返回正确的值。 例如,这里是我们之前尝试的,非常不好的直线的 RMSE。 + +```py +lw_mse(-100, 50000)**0.5 +16710.119837353752 +``` + +这里是这条直线的 RMSE,它接近回归线。 + +```py +lw_mse(90, 4000)**0.5 +2715.5391063834586 +``` + +如果我们尝试不同的值,我们可以通过反复试验找到一个误差较低的斜率和截距,但这需要一段时间。 幸运的是,有一个 Python 函数为我们做了所有的试错。 + +`minimize`函数可用于寻找函数的参数,函数在这里返回其最小值。 Python 使用类似的试错法,遵循使输出值递减的变化量。 + +`minimize`的参数是一个函数,它本身接受数值参数并返回一个数值。 例如,函数`lw_mse`以数值斜率和截距作为参数,并返回相应的 MSE。 + +调用`minimize(lw_mse)`返回一个数组,由斜率和截距组成,它们使 MSE 最小。 这些最小值是通过智能试错得出的极好的近似值,而不是基于公式的精确值。 + +```py +best = minimize(lw_mse) +best +array([ 86.97784117, 4744.78484535]) +``` + +这些值与我们之前使用`slope`和`intercept`函数计算的值相同。 由于最小化的不精确性,我们看到较小的偏差,但是这些值本质上是相同的。 + +```py +print("slope from formula: ", lw_reg_slope) +print("slope from minimize: ", best.item(0)) +print("intercept from formula: ", lw_reg_intercept) +print("intercept from minimize: ", best.item(1)) +slope from formula: 86.9778412583 +slope from minimize: 86.97784116615884 +intercept from formula: 4744.78479657 +intercept from minimize: 4744.784845352655 +``` + +### 最小二乘直线 + +因此我们发现,不仅回归线具有最小的均方误差,而且均方误差的最小化也给出了回归线。 回归线是最小化均方误差的唯一直线。 + +这就是回归线有时被称为“最小二乘直线”的原因。 + +## 最小二乘回归 + +在前面的章节中,我们开发了回归直线的斜率和截距方程,它穿过一个橄榄形的散点图。 事实证明,无论散点图的形状如何,最小二乘直线的斜率和截距都与我们开发的公式相同。 + +我们在《小女人》的例子中看到了它,但是让我们以散点图显然不是橄榄形的例子来证实它。 对于这些数据,我们再次受惠于佛罗里达大学 Larry Winner 教授的丰富数据档案。 《国际运动科学杂志》(International Journal of Exercise Science)2013 年的一项研究,研究了大学生铅球运动员,并考察了力量与铅球距离的关系。 总体由 28 名女大学生运动员组成。 运动员在赛季前的“1RM power clean”中举起的最大值(公斤)是衡量力量的指标。 距离(米)是运动员个人最佳成绩。 + +```py +shotput = Table.read_table('shotput.csv') +shotput +``` + +| Weight Lifted | Shot Put Distance | +| --- | --- | +| 37.5 | 6.4 | +| 51.5 | 10.2 | +| 61.3 | 12.4 | +| 61.3 | 13 | +| 63.6 | 13.2 | +| 66.1 | 13 | +| 70 | 12.7 | +| 92.7 | 13.9 | +| 90.5 | 15.5 | +| 90.5 | 15.8 | + +(省略了 18 行) + +```py +shotput.scatter('Weight Lifted') +``` + +![](img/13-41.png) + +这不是橄榄形的散点图。 事实上,它似乎有一点非线性成分。 但是,如果我们坚持用一条直线来做出预测,那么所有直线之中仍然有一条最好的直线。 + +我们为回归线的斜率和截距建立公式,它来源于橄榄形的散点图,并给出了下列值: + +```py +slope(shotput, 'Weight Lifted', 'Shot Put Distance') +0.098343821597819972 +intercept(shotput, 'Weight Lifted', 'Shot Put Distance') +5.9596290983739522 +``` + +即使散点图不是橄榄形,使用这些公式还有意义吗? 我们可以通过求出使 MSE 最小的斜率和截距来回答这个问题。 + +我们将定义函数`shotput_linear_mse`,以斜体和截距作为参数并返回相应的 MSE。 然后将`minimize`应用于`shotput_linear_mse`将返回最优斜率和截距。 + +```py +def shotput_linear_mse(any_slope, any_intercept): + x = shotput.column('Weight Lifted') + y = shotput.column('Shot Put Distance') + fitted = any_slope*x + any_intercept + return np.mean((y - fitted) ** 2) +minimize(shotput_linear_mse) +array([ 0.09834382, 5.95962911]) +``` + +这些值与我们使用我们的公式得到的值相同。 总结: + +无论散点图的形状如何,都有一条独特的线,可以使估计的均方误差最小。 它被称为回归线,其斜率和截距由下式给出: + +![](img/tex-13-10.gif) + +> 译者注:也就是`cov(x, y)/var(x)`。 + +![](img/tex-13-11.gif) + +```py +fitted = fit(shotput, 'Weight Lifted', 'Shot Put Distance') +shotput.with_column('Best Straight Line', fitted).scatter('Weight Lifted') +``` + +![](img/13-42.png) + +### 非线性回归 + +上面的图表强化了我们之前的观察,即散点图有点弯曲。 因此,最好拟合曲线而不是直线。 研究假设举起的重量与铅球距离之间是二次关系。 所以让我们使用二次函数来预测,看看我们能否找到最好的曲线。 + +我们必须找到所有二次函数中最好的二次函数,而不是所有直线中最好的直线。 最小二乘法允许我们这样做。 + +这种最小化的数学是复杂的,不容易仅仅通过检查散点图来发现。 但是数值最小化和线性预测一样简单! 再次通过使用最小化我们可以得到最好的二次预测。 让我们看看这是如何工作的。 + +回想一下,二次函数的形式: + +``` +f(x) = ax^2 + bx + c +``` + +`a`、`b`和`c`是常数。 + +为了基于举起的重量找到最好的二次函数来预测距离,使用最小二乘法,我们首先编写一个函数,以三个常量为自变量的,用上面的二次函数计算拟合值,然后返回均方误差。 + +该函数被称为`shotput_quadratic_mse`。 请注意,定义与`lw_mse`的定义类似,不同的是拟合值基于二次函数而不是线性。 + +```py +def shotput_quadratic_mse(a, b, c): + x = shotput.column('Weight Lifted') + y = shotput.column('Shot Put Distance') + fitted = a*(x**2) + b*x + c + return np.mean((y - fitted) ** 2) +``` + +我们现在可以像之前那样使用`minimize`,并找到使 MSE 最小的常数。 + +```py +best = minimize(shotput_quadratic_mse) +best +array([ -1.04004838e-03, 2.82708045e-01, -1.53182115e+00]) +``` + +我们预测,一个举起`x`公斤的运动员的铅球距离大概是`-0.00104x^2 + 0.2827x - 1.5318`米。 例如,如果运动员可以举起 100 公斤,预测的距离是 16.33 米。 在散点图上,在 100 公斤左右的垂直条形的中心附近。 + +```py +(-0.00104)*(100**2) + 0.2827*100 - 1.5318 +16.3382 +``` + +以下是所有`Weight Lifted`的预测。 你可以看到他们穿过散点图的中心,大致上接近。 + +```py +x = shotput.column(0) +shotput_fit = best.item(0)*(x**2) + best.item(1)*x + best.item(2) +shotput.with_column('Best Quadratic Curve', shotput_fit).scatter(0) +``` + +![](img/13-43.png) + +## 视觉诊断 + +假设数据科学家已经决定使用线性回归,基于预测变量估计响应变量的值。 为了了解这种估计方法的效果如何,数据科学家必须知道估计值距离实际值多远。 这些差异被称为残差。 + +![](img/tex-13-12.gif) + +残差就是剩下的东西 - 估计之后的剩余。 + +残差是回归线和点的垂直距离。 散点图中的每个点都有残差。 残差是`y`的观测值与`y`的拟合值之间的差值,所以对于点`(x, y)`: + +![](img/tex-13-13.gif) + +`residual`函数计算残差。 该计算假设我们已经定义的所有相关函数:`standard_units`,`correlation`,`slope`,`intercept`和`fit`。 + +```py +def residual(table, x, y): + return table.column(y) - fit(table, x, y) +``` + +继续使用高尔顿的数据的例子,基于双亲身高(预测变量)来估计成年子女身高(响应变量),让我们计算出拟合值和残差。 + +```py +heights = heights.with_columns( + 'Fitted Value', fit(heights, 'MidParent', 'Child'), + 'Residual', residual(heights, 'MidParent', 'Child') + ) +heights +``` + + +| MidParent | Child | Fitted Value | Residual | +| --- | --- | --- | --- | +| 75.43 | 73.2 | 70.7124 | 2.48763 | +| 75.43 | 69.2 | 70.7124 | -1.51237 | +| 75.43 | 69 | 70.7124 | -1.71237 | +| 75.43 | 69 | 70.7124 | -1.71237 | +| 73.66 | 73.5 | 69.5842 | 3.91576 | +| 73.66 | 72.5 | 69.5842 | 2.91576 | +| 73.66 | 65.5 | 69.5842 | -4.08424 | +| 73.66 | 65.5 | 69.5842 | -4.08424 | +| 72.06 | 71 | 68.5645 | 2.43553 | +| 72.06 | 68 | 68.5645 | -0.564467 | + +(省略了 924 行) + +如果要处理的变量太多,以可视化开始总是很有帮助的。 函数`scatter_fit`绘制数据的散点图,以及回归线。 + +```py +def scatter_fit(table, x, y): + table.scatter(x, y, s=15) + plots.plot(table.column(x), fit(table, x, y), lw=4, color='gold') + plots.xlabel(x) + plots.ylabel(y) +scatter_fit(heights, 'MidParent', 'Child') +``` + +![](img/13-44.png) + +通过绘制残差和预测变量来绘制残差图。函数`residual_plot`就是这样做的。 + +```py +def residual_plot(table, x, y): + x_array = table.column(x) + t = Table().with_columns( + x, x_array, + 'residuals', residual(table, x, y) + ) + t.scatter(x, 'residuals', color='r') + xlims = make_array(min(x_array), max(x_array)) + plots.plot(xlims, make_array(0, 0), color='darkblue', lw=4) + plots.title('Residual Plot') +residual_plot(heights, 'MidParent', 'Child') +``` + +![](img/13-45.png) + +双亲身高在横轴上,就像原始散点图中一样。 但是现在纵轴显示了残差。 请注意,该图看上去以`y=0`的横线为中心(以深蓝色显示)。 还要注意,绘图没有显示上升或下降的趋势。 我们稍后会观察到所有的回归都是如此。 + +### 回归诊断 + +残差图有助于我们直观评估线性回归分析的质量。 这种评估被称为诊断。 函数`regression_diagnostic_plots`绘制原始散点图以及残差图,以便于比较。 + +```py +def regression_diagnostic_plots(table, x, y): + scatter_fit(table, x, y) + residual_plot(table, x, y) +regression_diagnostic_plots(heights, 'MidParent', 'Child') +``` + +![](img/13-46.png) + +![](img/13-47.png) + +这个残差图表明,线性回归是合理的估计方法。 注意残差关于`y=0`的横线上下对称分布,相当于原始散点图大致上下对称。 还要注意,绘图的垂直延伸,在子女身高最常见的值上相当均匀。 换句话说,除了一些离群点之外,绘图并不是一些地方窄。另一些地方宽。 + +换句话说,在预测变量的观察范围内,回归的准确性似乎是相同的。 + +良好回归的残差图不显示任何规律。 在预测变量的范围内,残差在`y=0`的直线处上下相同。 + +### 检测非线性 + +绘制数据的散点图,通常表明了两个变量之间的关系是否是非线性的。 然而,通常情况下,残差图中比原始散点图中更容易发现非线性。 这通常是因为这两个图的规模:残差图允许我们放大错误,从而更容易找出规律。 + +![](img/13-48.png) + +我们的数据是海牛的年龄和长度的数据集,这是一种海洋哺乳动物(维基共享资源图)。 数据在一个名为`dugong`的表中。 年龄以年为单位,长度以米为单位。 因为海牛通常不跟踪他们的生日,年龄是根据他们的牙齿状况等变量来估计的。 + +```py +dugong = Table.read_table('http://www.statsci.org/data/oz/dugongs.txt') +dugong = dugong.move_to_start('Length') +dugong +``` + +| Length | Age | +| --- | --- | +| 1.8 | 1 | +| 1.85 | 1.5 | +| 1.87 | 1.5 | +| 1.77 | 1.5 | +| 2.02 | 2.5 | +| 2.27 | 4 | +| 2.15 | 5 | +| 2.26 | 5 | +| 2.35 | 7 | +| 2.47 | 8 | + +(省略了 17 行) + +如果我们可以衡量海牛的长度,对于它的年龄我们可以说什么呢? 让我们来看看我们的数据说了什么。 这是一个长度(预测变量)和年龄(响应变量)的回归。 这两个变量之间的相关性相当大,为 0.83。 + +```py +correlation(dugong, 'Length', 'Age') +0.82964745549057139 +``` + +尽管相关性仍然很高,绘图显示出曲线规律,在残差图中更加明显。 + +```py +regression_diagnostic_plots(dugong, 'Length', 'Age') +``` + +![](img/13-49.png) + +![](img/13-50.png) + +虽然你可以发现原始散点图中的非线性,但在残差图中更明显。 + +在长度的较低一端,残差几乎都是正的;然后他们几乎都是负的;然后在较高一端,残差再次为正。 换句话说,回归估计值过高,然后过低,然后过高。 这意味着使用曲线而不是直线来估计年龄会更好。 + +当残差图显示了规律时,变量之间可能存在非线性关系。 + +### 检测异方差 + +异方差这个词,那些准备拼写游戏的人肯定会感兴趣。 对于数据科学家来说,其兴趣在于它的意义,即“不均匀延伸”。 + +回想一下`hybrid `表,包含美国混合动力汽车的数据。这是燃油效率对加速度的回归。这个关联是负面的:加速度高的汽车往往效率较低。 + +```py +regression_diagnostic_plots(hybrid, 'acceleration', 'mpg') +``` + +![](img/13-51.png) + +![](img/13-52.png) + +注意残差图在加速度的较低一端变得发散。 换句话说,对于较低的加速度,误差的大小的变化比较高值更大。 残差图中比原始的散点图中更容易注意到不均匀的变化。 + +如果残差图显示`y=0`的横线处的不均匀变化,则在预测变量的范围内,回归的估计不是同等准确的。 + +## 数值诊断 + +除了可视化之外,我们还可以使用残差的数值属性来评估回归的质量。 我们不会在数学上证明这些属性。 相反,我们将通过计算来观察它们,看看它们告诉我们回归的什么东西。 + +下面列出的所有事实都适用于散点图的所有形状,无论它们是否是线性的。 + +### 残差图不展示形状 + +对于每一个线性回归,无论是好还是坏,残差图都不展示任何趋势。 总的来说,它是平坦的。 换句话说,残差和预测变量是不相关的。 + +你可以在上面所有的残差图中看到它。 我们还可以计算每种情况下,预测变量和残差之间的相关性。 + +```py +correlation(heights, 'MidParent', 'Residual') +-2.7196898076470642e-16 +``` + +这看起来不是零,但它是个很小的数字,除了由于计算的舍入误差之外,它就是零。 在这里也一样,取小数点后 10 位。 减号是因为上面的舍入。 + +```py +round(correlation(heights, 'MidParent', 'Residual'), 10) +-0.0 +dugong = dugong.with_columns( + 'Fitted Value', fit(dugong, 'Length', 'Age'), + 'Residual', residual(dugong, 'Length', 'Age') +) +round(correlation(dugong, 'Length', 'Residual'), 10) +0.0 +``` + +### 残差的均值 + +不管散点图的形状如何,剩余的均值都是 0。 + +这类似于这样一个事实,如果你选取任何数值列表并计算距离均值的偏差的列表,则偏差的均值为 0。 + +在上面的所有残差图中,你看到`y=0`的横线穿过图的中心。 这是这个事实的可视化。 + +作为一个数值示例,这里是高尔顿数据集中,基于双亲高度的子女高度的回归的残差均值。 + +```py +round(np.mean(heights.column('Residual')), 10) +0.0 +``` + +海牛长度和年龄的回归的残差均值也是一样。 残差均值为 0,除了舍入误差。 + +```py +round(np.mean(dugong.column('Residual')), 10) +0.0 +``` + +### 残差的标准差 + +无论散点图的形状如何,残差的标准差是响应变量的标准差的一个比例。 比例是 ![](img/tex-13-14.gif)。 + +![](img/tex-13-15.gif) + +我们将很快看到,它如何衡量回归估计的准确性。 但首先,让我们通过例子来确认。 + +在子女身高和双亲身高的案例中,残差的标准差约为 3.39 英寸。 + +```py +np.std(heights.column('Residual')) +3.3880799163953426 +``` + +这和响应变量的标准差乘`sqrt(1 - r^2)`相同。 + +```py +r = correlation(heights, 'MidParent', 'Child') +np.sqrt(1 - r**2) * np.std(heights.column('Child')) +3.3880799163953421 +``` + +混合动力汽车的加速和里程的回归也是如此。 相关性`r`是负数(约 -0.5),但`r^2`是正数,所以`sqrt(1 - r^2)`是一个分数。 + +```py +r = correlation(hybrid, 'acceleration', 'mpg') +r +-0.5060703843771186 +hybrid = hybrid.with_columns( + 'fitted mpg', fit(hybrid, 'acceleration', 'mpg'), + 'residual', residual(hybrid, 'acceleration', 'mpg') +) +np.std(hybrid.column('residual')), np.sqrt(1 - r**2)*np.std(hybrid.column('mpg')) +(9.4327368334302903, 9.4327368334302903) +``` + +现在让我们看看,残差的标准差是如何衡量回归的好坏。请记住,残差的均值为 0。因此,残差的标准差越小,则残差越接近于 0。换句话说,如果残差的标准差小,那么回归中的总体误差就小。 + +极端情况是`r = 1`或`r = -1`。在这两种情况下,`sqrt(1 - r^2) = 0`。因此,残差的均值为 0,标准差为 0,因此残差都等于 0。回归线确实是完美的估计。我们在本章的前面看到,如果`r = ± 1`,散点图是一条完美的直线,与回归线相同,所以回归估计中确实没有错误。 + +但通常`r`不是极端的。如果`r`既不是`±1`也不是 0,那么`sqrt(1 - r^2)`是一个适当的分数,并且回归估计的误差大小,整体上大致在 0 和`y`的标准差之间。 + +最糟糕的情况是`r = 0`。那么`sqrt(1 - r^2)` = 1,残差的标准差等于`y`的标准差。这与观察结果一致,如果`r = 0`那么回归线就是`y`的均值上的一条横线。在这种情况下,回归的均方根误差是距离`y`的平均值的偏差的均方根,这是`y`的标准差。实际上,如果`r = 0`,那么这两个变量之间就没有线性关联,所以使用线性回归没有任何好处。 + +### 另一种解释`r`的方式 + +我们可以重写上面的结果,不管散点图的形状如何: + +![](img/tex-13-16.gif) + +互补的结果是,无论散点图的形状如何,拟合值的标准差是观察值`y`的标准差的一个比例。比例是`|r|`。 + +![](img/tex-13-17.gif) + +要查看比例在哪里出现,请注意拟合值全部位于回归线上,而`y`的观测值是散点图中所有点的高度,并且更加可变。 + +```py +scatter_fit(heights, 'MidParent', 'Child') +``` + +![](img/13-53.png) + +拟合值的范围在 64 到 71 之间,而所有子女的身高则变化很大,大约在 55 到 80 之间。 + +为了在数值上验证结果,我们只需要计算双方的一致性。 + +```py +correlation(heights, 'MidParent', 'Child') +0.32094989606395924 +``` + +这里是出生体重的拟合值的标准差与观察值的标准差的比值: + +```py +np.std(heights.column('Fitted Value'))/np.std(heights.column('Child')) +0.32094989606395957 +``` + +这个比例等于`r`,证实了我们的结果。 + +绝对值出现在哪里? 首先要注意的是,标准差不能是负数,标准差的比值也不行。 那么当`r`是负数时会发生什么呢? 燃油效率和加速度的例子将向我们展示。 + +```py +correlation(hybrid, 'acceleration', 'mpg') +-0.5060703843771186 +np.std(hybrid.column('fitted mpg'))/np.std(hybrid.column('mpg')) +0.5060703843771186 +``` + +两个标准差的比值就是`|r|`。 + +解释这个结果的更标准的方法是,回想一下: + +![](img/tex-13-18.gif) + +因此,对结果的两边取平方: + +![](img/tex-13-19.gif) diff --git a/docs/data8-textbook-zh/14.md b/docs/data8-textbook-zh/14.md new file mode 100644 index 0000000000000000000000000000000000000000..6f1add1609451aa535aae24149ddd188119d33ba --- /dev/null +++ b/docs/data8-textbook-zh/14.md @@ -0,0 +1,394 @@ +# 十四、回归的推断 + +> 原文:[Inference for Regression](https://github.com/data-8/textbook/tree/gh-pages/chapters/14) + +> 译者:[飞龙](https://github.com/wizardforcel) + +> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) + +> 自豪地采用[谷歌翻译](https://translate.google.cn/) + +到目前为止,我们对变量之间关系的分析纯粹是描述性的。我们知道如何找到穿过散点图的最佳直线来绘制。在所有直线中它的估计的均方误差最小,从这个角度来看,这条线是最好的。 + +但是,如果我们的数据是更大总体的样本呢?如果我们在样本中发现了两个变量之间的线性关系,那么对于总体也是如此嘛?它会是完全一样的线性关系吗?我们可以预测一个不在我们样本中的新的个体的响应变量吗? + +如果我们认为,散点图反映了被绘制的两个变量之间的基本关系,但是并没有完全规定这种关系,那么就会出现这样的推理和预测问题。例如,出生体重与孕期的散点图,显示了我们样本中两个变量之间的精确关系;但是我们可能想知道,对于抽样总体中的所有新生儿或实际中的一般新生儿,这样的关系是否是真实的,或者说几乎是正确的。 + +一如既往,推断思维起始于仔细检查数据的假设。一组假设被称为模型。大致线性的散点图中的一组随机性的假设称为回归模型。 + +## 回归模型 + +简而言之,这样的模型认为,两个变量之间的底层关系是完全线性的;这条直线是我们想要识别的信号。但是,我们无法清楚地看到这条线。我们看到的是分散在这条线上的点。在每一点上,信号都被随机噪声污染。因此,我们的推断目标是将信号从噪声中分离出来。 + +更详细地说,回归模型规定了,散点图中的点是随机生成的,如下所示。 + ++ `x`和`y`之间的关系是完全线性的。我们看不到这个“真实直线”,但它是存在的。 ++ 散点图通过将线上的点垂直移动,或上或下来创建,如下所示: ++ 对于每个`x`,找到真实直线上的相应点(即信号),然后生成噪声或误差。 ++ 误差从误差总体中带放回随机抽取,总体是均值为 0 的正态分布。 ++ 创建一个点,横坐标为`x`,纵坐标为“`x`处的真实高度加上误差”。 ++ 最后,从散点图中删除真正的线,只显示创建的点。 + +基于这个散点图,我们应该如何估计真实直线? 我们可以使其穿过散点图的最佳直线是回归线。 所以回归线是真实直线的自然估计。 + +下面的模拟显示了回归直线与真实直线的距离。 第一个面板显示如何从真实直线生成散点图。 第二个显示我们看到的散点图。 第三个显示穿过散点图的回归线。 第四个显示回归线和真实直线。 + +为了运行模拟,请使用三个参数调用`draw_and_compare`函数:真实直线的斜率,真实直线的截距以及样本量。 + +运行模拟几次,用不同的斜率和截距,以及不同的样本量。 因为所有的点都是根据模型生成的,所以如果样本量适中,你会看到回归线是真实直线的一个良好估计。 + +```py +# The true line, +# the points created, +# and our estimate of the true line. +# Arguments: true slope, true intercept, number of points + +draw_and_compare(4, -5, 10) +``` + +![](img/14-1.png) + +实际上,我们当然不会看到真实直线。 模拟结果表明,如果回归模型看起来合理,并且如果我们拥有大型样本,那么回归线就是真实直线的一个良好近似。 + +## 真实斜率的推断 + +我们的模拟表明,如果回归模型成立,并且样本量很大,则回归线很可能接近真实直线。 这使我们能够估计真实直线的斜率。 + +我们将使用我们熟悉的母亲和她们的新生儿的样本,来开发估计真实直线的斜率的方法。 首先,我们来看看我们是否相信,回归模型是一系列适当假设,用于描述出生体重和孕期之间的关系。 + +```py +correlation(baby, 'Gestational Days', 'Birth Weight') +0.40754279338885108 +``` + +总的来说,散点图看起来相当均匀地分布在这条线上,尽管一些点分布在主云形的周围。 相关系数为 0.4,回归线斜率为正。 + +这是否反映真实直线斜率为正的事实? 为了回答这个问题,让我们看看我们能否估计真实斜率。 我们当然有了一个估计:我们的回归线斜率。 这大约是 0.47 盎司每天。 + +```py +slope(baby, 'Gestational Days', 'Birth Weight') +0.46655687694921522 +``` + +但是如果散点图出现的方式不同,回归线会有所不同,可能会有不同的斜率。 我们如何计算,斜率可能有多么不同? + +我们需要点的另一个样本,以便我们可以绘制回归线穿过新的散点图,并找出其斜率。 但另一个样本从哪里得到呢? + +你猜对了 - 我们将自举我们的原始样本。 这会给我们自举的散点图,通过它我们可以绘制回归线。 + +### 自举散点图 + +我们可以通过对原始样本带放回地随机抽样,来模拟新样本,它的次数与原始样本量相同。 这些新样本中的每一个都会给我们一个散点图。 我们将这个称为自举散点图,简而言之,我们将调用整个过程来自举散点图。 + +这里是来自样本的原始散点图,以及自举重采样过程的四个复制品。 请注意,重采样散点图通常比原始图稀疏一点。 这是因为一些原始的点没有在样本中被选中。 + +![](img/14-3.png) + +### 估计真实斜率 + +我们可以多次自举散点图,并绘制穿过每个自举图的回归线。 每条线都有一个斜率。 我们可以简单收集所有的斜率并绘制经验直方图。 回想一下,在默认情况下,`sample`方法带放回地随机抽取,次数与表中的行数相同。 也就是说,`sample`默认生成一个自举样本。 + +```py +slopes = make_array() +for i in np.arange(5000): + bootstrap_sample = baby.sample() + bootstrap_slope = slope(bootstrap_sample, 'Gestational Days', 'Birth Weight') + slopes = np.append(slopes, bootstrap_slope) +Table().with_column('Bootstrap Slopes', slopes).hist(bins=20) +``` + +![](img/14-4.png) + +然后,我们可以使用`percentile `方法,为真实直线的斜率构建约 95% 置信区间。 置信区间从 5000 个自举斜率的第 2.5 百分位数,延伸到第 97.5 百分位数。 + +```py +left = percentile(2.5, slopes) +right = percentile(97.5, slopes) +left, right +(0.38209399211893086, 0.56014757838023777) +``` + +### 用于自举斜率的函数 + +让我们收集我们估计斜率的方法的所有步骤,并定义函数`bootstrap_slope`来执行它们。 它的参数是表的名称,预测变量和响应变量的标签,以及自举复制品的所需数量。 在每个复制品中,该函数自举原始散点图并计算所得回归线的斜率。 然后绘制所有生成的斜率的直方图,并打印由斜率的“中间 95%”组成的区间。 + +```py +def bootstrap_slope(table, x, y, repetitions): + + # For each repetition: + # Bootstrap the scatter, get the slope of the regression line, + # augment the list of generated slopes + slopes = make_array() + for i in np.arange(repetitions): + bootstrap_sample = table.sample() + bootstrap_slope = slope(bootstrap_sample, x, y) + slopes = np.append(slopes, bootstrap_slope) + + # Find the endpoints of the 95% confidence interval for the true slope + left = percentile(2.5, slopes) + right = percentile(97.5, slopes) + + # Slope of the regression line from the original sample + observed_slope = slope(table, x, y) + + # Display results + Table().with_column('Bootstrap Slopes', slopes).hist(bins=20) + plots.plot(make_array(left, right), make_array(0, 0), color='yellow', lw=8); + print('Slope of regression line:', observed_slope) + print('Approximate 95%-confidence interval for the true slope:') + print(left, right) +``` + +当响应变量为出生体重,预测变量为孕期时,我们调用`bootstrap_slope`来找到真实斜率的置信区间,我们得到了一个区间,非常接近我们之前获得的东西:大约 0.38 到 0.56 盎司每天。 + +```py +bootstrap_slope(baby, 'Gestational Days', 'Birth Weight', 5000) +Slope of regression line: 0.466556876949 +Approximate 95%-confidence interval for the true slope: +0.378663152966 0.555005146304 +``` + +![](img/14-5.png) + +现在我们有一个函数,可以自动完成估计在回归模型中展示斜率的过程,我们也可以在其他变量上使用它。 + +例如,我们来看看出生体重与母亲身高的关系。 更高的女性往往有更重的婴儿吗? + +回归模型似乎是合理的,基于散点图,但相关性不高。 这只有大约 0.2。 + +```py +scatter_fit(baby, 'Maternal Height', 'Birth Weight') +``` + +![](img/14-6.png) + +```py +correlation(baby, 'Maternal Height', 'Birth Weight') +0.20370417718968034 +``` + +像之前一样,我们使用`bootstrap_slope `来估计回归模型中真实直线的斜率。 + +```py +bootstrap_slope(baby, 'Maternal Height', 'Birth Weight', 5000) +Slope of regression line: 1.47801935193 +Approximate 95%-confidence interval for the true slope: +1.0403083964 1.91576886223 +``` + +![](img/14-7.png) + +真实斜率的 95% 置信区间,从约 1 延伸到约 1.9 盎司每英寸。 + +### 真实斜率可能为 0 嘛? + +假设我们相信我们的数据遵循回归模型,并且我们拟合回归线来估计真实直线。 如果回归线不完全是平的,几乎总是如此,我们将观察到散点图中的一些线性关联。 + +但是,如果这种观察是假的呢? 换句话说,如果真实直线是平的 - 也就是说,这两个变量之间没有线性关系 - 我们观察到的联系,只是由于从样本中产生点的随机性。 + +这是一个模拟,说明了为什么会出现这个问题。 我们将再次调用`draw_and_compare`函数,这次要求真实斜率为 0。我们的目标是,观察我们的回归线是否显示不为 0 的斜率。 + +请记住函数`draw_and_compare`的参数是真实直线的斜率和截距,以及要生成的点的数量。 + +```py +draw_and_compare(0, 10, 25) +``` + +![](img/14-8.png) + +运行模拟几次,每次保持真实直线的斜率为 0 。你会注意到,虽然真实直线的斜率为 0,但回归线的斜率通常不为 0。回归线有时会向上倾斜,有时会向下倾斜,每次都给我们错误的印象,即这两个变量是相关的。 + +为了确定我们所看到的斜率是否真实,我们想测试以下假设: + +原假设。真实直线的斜率是 0。 + +备选假设。真实直线的斜率不是 0。 + +我们很有条件来实现它。由于我们可以为真实斜率构建一个 95% 的置信区间,我们所要做的就是看区间是否包含 0。 + +如果没有,那么我们可以拒绝原假设(P 值为 5% 的截断值)。 + +如果真实斜率的置信区间确实包含 0,那么我们没有足够的证据来拒绝原假设。也许我们看到的斜率是假的。 + +我们在一个例子中使用这个方法。假设我们试图根据母亲的年龄来估计新生儿的出生体重。根据样本,根据母亲年龄估计出生体重的回归线的斜率为正,约为 0.08 盎司每年。 + +```py +slope(baby, 'Maternal Age', 'Birth Weight') +0.085007669415825132 +``` + +虽然斜率为正,但是很小。 回归线非常接近平的,这就产生了一个问题,真实直线是否是平的。 + +```py +scatter_fit(baby, 'Maternal Age', 'Birth Weight') +``` + +![](img/14-9.png) + +我们可以使用`bootstrap_slope`来估计真实直线的斜率。 计算表明,真实斜率的约 95% 的自举置信区间左端为负,右端为正 - 换句话说,区间包含 0。 + +```py +bootstrap_slope(baby, 'Maternal Age', 'Birth Weight', 5000) +Slope of regression line: 0.0850076694158 +Approximate 95%-confidence interval for the true slope: +-0.104335243815 0.272791852339 +``` + +![](img/14-10.png) + +因为区间包含 0,所以我们不能拒绝原假设,母亲年龄与新生儿出生体重之间的真实线性关系的斜率为 0。基于此分析,使用母亲年龄作为预测变量,基于回归模型预测出生体重是不明智的。 + +## 预测区间 + +回归的主要用途之一是对新个体进行预测,这个个体不是我们原始样本的一部分,但是与样本个体相似。在模型的语言中,我们想要估计新值`x`的`y`。 + +我们的估计是真实直线在`x`处的高度。当然,我们不知道真实直线。我们使用我们的样本点的回归线来代替。 + +给定值`x`的拟合值,是基于`x`值的`y`的回归估计。换句话说,给定值`x`的拟合值就是回归线在`x`处的高度。 + +假设我们试图根据孕期天数来预测新生儿的出生体重。我们在前面的章节中看到,这些数据非常适合回归模型,真实直线的斜率的 95% 置信区间不包含 0。因此,我们的预测似乎是合理的。 + +下图显示了预测位于回归线上的位置。红线是`x = 300`。 + +![](img/14-11.png) + +红线与回归线的相交点的高度是孕期天数 300 的拟合值。 + +函数`fitted_value`计算这个高度。像函数的相关性,斜率和截距一样,它的参数是表的名称和`x`和`y`的列标签。但是它也需要第四个参数,即`x`的值,在这个值上进行估算。 + +```py +def fitted_value(table, x, y, given_x): + a = slope(table, x, y) + b = intercept(table, x, y) + return a * given_x + b +``` + +孕期天数 300 的拟合值约为 129.2 盎司。 换句话说,对于孕期为 300 天的孕妇,我们估计的新生儿体重约为 129.2 盎司。 + +```py +fit_300 = fitted_value(baby, 'Gestational Days', 'Birth Weight', 300) +fit_300 +129.2129241703143 +``` + +### 预测的可变性 + +我们已经开发了一种方法,使用我们样本中的数据,根据孕期天数预测新生儿的体重。 但作为数据科学家,我们知道样本可能有所不同。 如果样本不同,回归线也会不一样,我们的预测也是。 为了看看我们的预测有多好,我们必须了解预测的可变性。 + +为此,我们必须生成新的样本。 我们可以像上一节那样,通过自举散点图来实现。 然后,我们为每个散点图的复制品拟合回归线,并根据每一行进行预测。 下图显示了 10 条这样的线,以及孕期天数 300 对应的出生体重预测。 + +![](img/14-12.png) + +```py +lines +``` + +| slope | intercept | prediction at x=300 | +| --- | --- | --- | +| 0.503931 | -21.6998 | 129.479 | +| 0.53227 | -29.5647 | 130.116 | +| 0.518771 | -25.363 | 130.268 | +| 0.430556 | -1.06812 | 128.099 | +| 0.470229 | -11.7611 | 129.308 | +| 0.48713 | -16.5314 | 129.608 | +| 0.51241 | -23.2954 | 130.428 | +| 0.52473 | -27.2053 | 130.214 | +| 0.409943 | 5.22652 | 128.21 | +| 0.468065 | -11.6967 | 128.723 | + + +每一行的预测都不相同。 下表显示了 10 条线的斜率、截距以及预测。 + +### 自举预测区间 + +如果我们增加重采样过程的重复次数,我们可以生成预测的经验直方图。这将允许我们创建预测区间,使用为斜率创建自举置信区间时的相同的百分比方法。 + +让我们定义一个名为`bootstrap_prediction`的函数来实现。该函数有五个参数: + ++ 表的名称 ++ 预测变量和响应变量的列标签 ++ 用于预测的`x`的值 ++ 所需的自举重复次数 + +在每次重复中,函数将自举原始散点图,并基于`x`的指定值查找`y`的预测值。具体来说,它调用我们在本节前面定义的函数`fitted_value`,来寻找指定`x`处的拟合值。 + +最后,绘制所有预测值的经验直方图,并打印由预测值的“中间 95%”组成的区间。它还打印基于穿过原始散点图的回归线的预测值。 + +```py +# Bootstrap prediction of variable y at new_x +# Data contained in table; prediction by regression of y based on x +# repetitions = number of bootstrap replications of the original scatter plot + +def bootstrap_prediction(table, x, y, new_x, repetitions): + + # For each repetition: + # Bootstrap the scatter; + # get the regression prediction at new_x; + # augment the predictions list + predictions = make_array() + for i in np.arange(repetitions): + bootstrap_sample = table.sample() + bootstrap_prediction = fitted_value(bootstrap_sample, x, y, new_x) + predictions = np.append(predictions, bootstrap_prediction) + + # Find the ends of the approximate 95% prediction interval + left = percentile(2.5, predictions) + right = percentile(97.5, predictions) + + # Prediction based on original sample + original = fitted_value(table, x, y, new_x) + + # Display results + Table().with_column('Prediction', predictions).hist(bins=20) + plots.xlabel('predictions at x='+str(new_x)) + plots.plot(make_array(left, right), make_array(0, 0), color='yellow', lw=8); + print('Height of regression line at x='+str(new_x)+':', original) + print('Approximate 95%-confidence interval:') + print(left, right) +bootstrap_prediction(baby, 'Gestational Days', 'Birth Weight', 300, 5000) +Height of regression line at x=300: 129.21292417 +Approximate 95%-confidence interval: +127.300774171 131.361729528 +``` + +![](img/14-13.png) + +上图显示了基于 5000 次重复的自举过程,孕期天数 300 的预测出生体重的自举经验直方图。经验分布大致是正泰的。 + +我们已经通过预测的“中间 95%”,即预测的第 2.5 百分位数到第 97.5 百分位数的区间,构建了分数的约 95% 的预测区间。 区间范围从大约 127 到大约 131。基于原始样本的预测是大约 129,接近区间的中心。 + +### 改变预测变量的值的效果 + +下图显示了孕期天数 285 的 5,000 次自举预测的直方图。 基于原始样本的预测是约 122 盎司,区间范围从约 121 盎司到约 123 盎司。 + +```py +bootstrap_prediction(baby, 'Gestational Days', 'Birth Weight', 285, 5000) +Height of regression line at x=285: 122.214571016 +Approximate 95%-confidence interval: +121.177089926 123.291373304 +``` + +![](img/14-14.png) + +请注意,这个区间比孕妇天数 300 的预测区间更窄。 让我们来调查其原因。 + +孕妇天数均值约为 279 天: + +```py +np.mean(baby.column('Gestational Days')) +279.10136286201021 +``` + +所以 285 比 300 更接近分布的中心。 通常,基于自举样本的回归线,在预测变量的分布中心附近彼此更接近。 因此,所有的预测值也更接近。 这解释了预测区间的宽度更窄。 + +你可以在下面的图中看到这一点,它显示了 10 个自举复制品中每一个的`x = 285`和`x = 300`的预测值。 通常情况下,直线在`x = 300`处比`x = 285`处相距更远,因此`x = 300`的预测更加可变。 + +![](img/14-15.png) + +### 注意事项 + +我们在本章中进行的所有预测和测试,都假设回归模型是成立的。 具体来说,这些方法假设,散点图中的点由直线上的点产生,然后通过添加随机正态噪声将它们推离直线。 + +如果散点图看起来不像那样,那么模型可能不适用于数据。 如果模型不成立,那么假设模型为真的计算是无效的。 + +因此,在开始基于模型进行预测,或者对模型参数进行假设检验之前,我们首先要确定回归模型是否适用于我们的数据。 一个简单的方法就是,按照我们在本节所做的操作,即绘制两个变量的散点图,看看它看起来是否大致线性,并均匀分布在一条线上。 我们还应该使用残差图,执行我们在前一节中开发的诊断。 diff --git a/docs/data8-textbook-zh/15.md b/docs/data8-textbook-zh/15.md new file mode 100644 index 0000000000000000000000000000000000000000..3954acec27012481d28044623f47bc368106cd6f --- /dev/null +++ b/docs/data8-textbook-zh/15.md @@ -0,0 +1,1272 @@ +# 十五、分类 + +> 原文:[Classification](https://github.com/data-8/textbook/tree/gh-pages/chapters/15) + +> 译者:[飞龙](https://github.com/wizardforcel) + +> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) + +> 自豪地采用[谷歌翻译](https://translate.google.cn/) + + +[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') +``` + +![](img/15-1.png) + +假设爱丽丝是不在数据集中的新患者。 如果我告诉你爱丽丝的血红蛋白水平和血糖水平,你可以预测她是否有 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) +``` + +![](img/15-2.png) + +因此,我们的最近邻分类器是这样工作的: + ++ 找到训练集中离新点最近的点。 ++ 如果最近的点是“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) +``` + +![](img/15-3.png) + +这里有数百个未分类的新点,都是红色的。 + +![](img/15-4.png) + +每个红点在训练集中都有一个最近邻(与之前的蓝点和金点相同)。对于一些红点,你可以很容易地判断最近邻是蓝色还是金色。对于其他点来说,通过眼睛来做出决定更为棘手。那些是靠近决策边界的点。 + +但是计算机可以很容易地确定每个点的最近邻。那么让我们将我们的最近邻分类器应用于每个红点: + +对于每个红点,它必须找到训练集中最近的点;它必须将红点的颜色改变为最近邻的颜色。 + +结果图显示哪些点将划分为“CKD”(全部为蓝色),或者“非 CKD”(全部为金色)。 + +![](img/15-5.png) + +决策边界是分类器从将红点转换为蓝色变成金色的地方。 + +## KNN + +然而,两个类别的分类并不总是那么清晰。例如,假设我们不用血红蛋白水平而是看白细胞计数。看看会发生什么: + +```py +ckd.scatter('White Blood Cell Count', 'Glucose', colors='Color') +``` + +![](img/15-6.png) + +如你所见,非 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') +``` + +![](img/15-7.png) + +之前,我们说我们预计得到一些分类错误,因为在左下方有一些蓝色和金色的点。 + +但是训练集中的点,也就是已经在散点图上的点呢?我们会把它们误分类吗? + +答案是否。请记住,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); +``` + +![](img/15-8.png) + +我们得到以下分类区域和决策边界: + +![](img/15-9.png) + +把测试数据放在这个图上,你可以立刻看到分类器对于几乎所有的点都正确,但也有一些错误。 例如,测试集的一些蓝点落在分类器的金色区域。 + +![](img/15-10.png) + +尽管存在一些错误,但分类器看起来在测试集上表现得相当好。 假设原始样本是从底层总体中随机抽取的,我们希望分类器在整个总体上具有相似的准确性,因为测试集是从原始样本中随机选取的。 + +## 表的行 + +现在我们对最近邻分类有一个定性的了解,是时候实现我们的分类器了。 + +在本章之前,我们主要处理表格的单列。 但现在我们必须看看一个个体是否“接近”另一个个体。 个体数据包含在表格的行中。 + +那么让我们首先仔细看一下行。 + +这里是原始表格`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); +``` + +![](img/15-11.png) + +为了找到 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)`处有另一个点,则它们之间的距离是: + +![](img/tex-15-1.gif) + +(这是从哪里来的?它来自勾股定理:我们有一个直角三角形,边长为`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 邻近的分类器会把爱丽丝划分为蓝色:它可能预测爱丽丝有慢性肾病。 + +下面的图片放大了爱丽丝和她五个最近邻。 这两个金点就在红点正下方的圆圈内。 分类器说,爱丽丝更像她身边的三个蓝点。 + +![](img/15-12.png) + +我们正在实现我们的 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') +``` + +![](img/15-13.png) + +非常有趣! 这两个测量值看起来对于预测钞票是否伪造有帮助。 然而,在这个例子中,你现在可以看到蓝色的簇和金色的簇之间有一些重叠。 这表示基于这两个数字,很难判断钞票是否合法。 不过,你可以使用 K 最近邻分类器来预测钞票的合法性。 + +花点时间想一想:假设我们使用`k = 11`(是假如)。 图中的哪些部分会得到正确的结果,哪些部分会产生错误? 决定边界是什么样子? + +数据中显示的规律可能非常乱。 例如,如果使用与图像不同的一对测量值,我们可以得到以下结果: + +```py +banknotes.scatter('WaveletSkew', 'Entropy', colors='Color') +``` + +![](img/15-14.png) + +似乎存在规律,但它是非常复杂。 尽管如此, 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')); +``` + +![](img/15-15.png) + +真棒!只用 2 个属性,两个簇之间有一些重叠(这意味着对于重叠中的一些点,分类器必然犯一些错误)。但是当我们使用这三个属性时,两个簇几乎没有重叠。换句话说,使用这 3 个属性的分类器比仅使用 2 个属性的分类器更精确。 + +这是分类中的普遍现象。每个属性都可能会给你提供新的信息,所以更多的属性有时可以帮助你建立一个更好的分类器。当然开销是,现在我们必须收集更多的信息来衡量每个属性的值,但是如果这个开销显着提高了我们的分类器的精度,那么它可能非常值得。 + +综上所述:你现在知道如何使用 K 最近邻分类,预测是与否的问题的答案,基于一些属性值,假设你有一个带有样本的训练集,其中正确的预测已知。总的路线图是这样的: + +找出一些属性,你认为可能帮助你预测问题的答案。 +收集一组训练样本,其中你知道属性值以及正确预测。 +为了预测未来,测量属性的值,然后使用 K 最近邻分类来预测问题的答案。 + +### 多维距离 + +我们知道如何在二维空间中计算距离。 如果我们在坐标`(x0, y0)`处有一个点,而在`(x1, y1)`处有另一个点,则它们之间的距离是: + +![](img/tex-15-2.gif) + +在三维空间中,点是`(x0, y0, z0)`和`(x1, y1, z1)`,它们之间的距离公式为: + +![](img/tex-15-3.gif) + +在 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') +``` + +![](img/15-16.png) + +蓝点(第一类)几乎完全与金点分离。 这表明了,为什么两种第一类葡萄酒之间的距离小于两个不同类别葡萄酒之间的距离。 我们使用不同的一对属性,也可以看到类似的现象: + +```py +wine_with_colors.scatter('Alcalinity of Ash', 'Ash', colors='Color') +``` + +![](img/15-17.png) + +但是对于不同的偶对,图像更加模糊。 + +```py +wine_with_colors.scatter('Magnesium', 'Total Phenols', colors='Color') +``` + +![](img/15-18.png) + +让我们来看看,是否可以基于所有的属性来实现一个分类器。 之后,我们会看到它有多准确。 + +### 实现计划 + +现在是时候编写一些代码来实现分类器了。 输入是我们要分类的一个点。 分类器的原理是,找到训练集中的 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 +``` + +是的! 分类器弄对了。 + +但是我们还不知道它对于所有其它葡萄酒如何,而且无论如何我们都知道,测试已经属于训练集的葡萄酒可能过于乐观了。 在本章的最后部分,我们将葡萄酒分为训练集和测试集,然后测量分类器在测试集上的准确性。 + +## 分类器的准确性 + +为了看看我们的分类器做得如何,我们可以将 50% 的数据放入训练集,另外 50% 放入测试集。基本上,我们保留一些数据以便以后使用,所以我们可以用它来测量分类器的准确性。我们始终将这个称为测试集。有时候,人们会把你留下用于测试的数据叫做保留集,他们会把这个估计准确率的策略称为保留方法。 + +请注意,这种方法需要严格的纪律。在开始使用机器学习方法之前,你必须先取出一些数据,然后放在一边用于测试。你必须避免使用测试集来开发你的分类器:你不应该用它来帮助训练你的分类器或者调整它的设置,或者用头脑风暴的方式来改进你的分类器。相反,在最后你已经完成分类器之后,当你想要它的准确率的无偏估计时,你应该仅仅使用它使用一次。 + +### 测量我们的葡萄酒分类器的准确率 + +好吧,让我们应用保留方法来评估 K 最近邻分类器识别葡萄酒的有效性。数据集有 178 个葡萄酒,所以我们将随机排列数据集,并将其中的 89 个放在训练集中,其余 89 个放在测试集中。 + +```py +shuffled_wine = wine.sample(with_replacement=False) +training_set = shuffled_wine.take(np.arange(89)) +test_set = shuffled_wine.take(np.arange(89, 178)) +``` + +我们将使用训练集中的 89 个葡萄酒来训练分类器,并评估其在测试集上的表现。 为了让我们更轻松,我们将编写一个函数,在测试集中每个葡萄酒上评估分类器: + +```py +def count_zero(array): + """Counts the number of 0's in an array""" + return len(array) - np.count_nonzero(array) + +def count_equal(array1, array2): + """Takes two numerical arrays of equal length + and counts the indices where the two are equal""" + return count_zero(array1 - array2) + +def evaluate_accuracy(training, test, k): + test_attributes = test.drop('Class') + def classify_testrow(row): + return classify(training, row, k) + c = test_attributes.apply(classify_testrow) + return count_equal(c, test.column('Class')) / test.num_rows +``` + +现在到了答案揭晓的时候了,我们来看看我们做得如何。 我们将任意使用`k = 5`。 + +```py +evaluate_accuracy(training_set, test_set, 5) +0.9213483146067416 +``` + +对于一个简单的分类器来说,这个准确率完全不差。 + +### 乳腺癌诊断 + +现在我想展示乳腺癌诊断的例子。我受到布列塔尼·温格(Brittany Wenger)的启发,他在 2012 年赢得了谷歌科学竞赛,还是一位 17 岁的高中生。这是布列塔尼: + + + +布列塔尼的科学竞赛项目是构建一个诊断乳腺癌的分类算法。由于她构建了一个精度接近 99% 的算法,她获得了大奖。 + +让我们看看我们能做得如何,使用我们在这个课程中学到的思路。 + +所以,让我告诉你一些数据集的信息。基本上,如果一个女性的乳房存在肿块,医生可能想要进行活检,看看它是否是癌症。有几个不同的过程用于实现它。布列塔尼专注于细针抽吸(FNA),因为它比替代方案的侵袭性小。医生得到一块样本,放在显微镜下,拍摄一张照片,一个训练有素的实验室技术人员分析图像,来确定是否是癌症。我们得到一张图片,像下面这样: + + + + + +不幸的是,区分良性和恶性可能是棘手的。因此,研究人员已经研究了机器学习的用法,来帮助完成这项任务。我们的想法是,我们要求实验室技术人员分析图像并计算各种属性:诸如细胞的通常大小,细胞大小之间有多少变化等等。然后,我们将尝试使用这些信息来预测(分类)样本是否是恶性的。我们有一套来自女性的过去样本的训练集,其中正确的诊断已知,我们希望我们的机器学习算法可以使用它们来学习如何预测未来样本的诊断。 + +我们最后得到了以下数据集。对于`Class`列,1 表示恶性(癌症);0 意味着良性(不是癌症)。 + +```py +patients = Table.read_table('breast-cancer.csv').drop('ID') +patients +``` + + +| Clump Thickness | Uniformity of Cell Size | Uniformity of Cell Shape | Marginal Adhesion | Single Epithelial Cell Size | Bare Nuclei | Bland Chromatin | Normal Nucleoli | Mitoses | Class | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 5 | 1 | 1 | 1 | 2 | 1 | 3 | 1 | 1 | 0 | +| 5 | 4 | 4 | 5 | 7 | 10 | 3 | 2 | 1 | 0 | +| 3 | 1 | 1 | 1 | 2 | 2 | 3 | 1 | 1 | 0 | +| 6 | 8 | 8 | 1 | 3 | 4 | 3 | 7 | 1 | 0 | +| 4 | 1 | 1 | 3 | 2 | 1 | 3 | 1 | 1 | 0 | +| 8 | 10 | 10 | 8 | 7 | 10 | 9 | 7 | 1 | 1 | +| 1 | 1 | 1 | 1 | 2 | 10 | 3 | 1 | 1 | 0 | +| 2 | 1 | 2 | 1 | 2 | 1 | 3 | 1 | 1 | 0 | +| 2 | 1 | 1 | 1 | 2 | 1 | 1 | 1 | 5 | 0 | +| 4 | 2 | 1 | 1 | 2 | 1 | 2 | 1 | 1 | 0 | + +(省略了 673 行) + +所以我们有 9 个不同的属性。 我不知道如何制作它们全部的 9 维散点图,所以我要挑选两个并绘制它们: + +```py +color_table = Table().with_columns( + 'Class', make_array(1, 0), + 'Color', make_array('darkblue', 'gold') +) +patients_with_colors = patients.join('Class', color_table) +patients_with_colors.scatter('Bland Chromatin', 'Single Epithelial Cell Size', colors='Color') +``` + +![](img/15-19.png) + +这个绘图完全是误导性的,因为有一堆点的`x`坐标和`y`坐标都有相同的值。 为了更容易看到所有的数据点,我将为`x`和`y`值添加一点点随机抖动。 这是看起来的样子: + +![](img/15-20.png) + +例如,你可以看到有大量的染色质为 2 和上皮细胞大小为 2 的样本;所有都不是癌症。 + +请记住,抖动仅用于可视化目的,为了更容易感知数据。 我们现在已经准备好使用这些数据了,我们将使用原始数据(没有抖动)。 + +首先,我们将创建一个训练集和一个测试集。 数据集有 683 名患者,因此我们将随机排列数据集,并将其中的 342 个放在训练集中,其余的 341 个放在测试集中。 + +```py +shuffled_patients = patients.sample(683, with_replacement=False) +training_set = shuffled_patients.take(np.arange(342)) +test_set = shuffled_patients.take(np.arange(342, 683)) +``` + +让我们选取 5 个最近邻,并观察我们的分类器如何。 + +```py +evaluate_accuracy(training_set, test_set, 5) +0.967741935483871 +``` + +准确性超过 96%。不错!这样一个简单的技术再一次相当不错。 + +作为脚注,你可能已经注意到布列塔尼·温格做得更好了。 她使用了什么技术? 一个关键的创新是,她将置信评分纳入了结果:她的算法有一种方法来确定何时无法做出有把握的预测,对于那些患者,甚至不尝试预测他们的诊断。 她的算法对于做出预测的病人是 99% 准确的,所以这个扩展看起来有点帮助。 + +## 多元回归 + +现在我们已经探索了使用多个属性来预测类别变量的方法,让我们返回来预测定量变量。 预测数值量被称为回归,多个属性进行回归的常用方法称为多元线性回归。 + +### 房价 + +下面的房价和属性数据集在爱荷华州埃姆斯市收集了数年。 数据集的描述在线显示。 我们将仅仅关注列的一个子集。 我们将尝试从其它列中预测价格列。 + +```py +all_sales = Table.read_table('house.csv') +sales = all_sales.where('Bldg Type', '1Fam').where('Sale Condition', 'Normal').select( + 'SalePrice', '1st Flr SF', '2nd Flr SF', + 'Total Bsmt SF', 'Garage Area', + 'Wood Deck SF', 'Open Porch SF', 'Lot Area', + 'Year Built', 'Yr Sold') +sales.sort('SalePrice') +``` + + +| SalePrice | 1st Flr SF | 2nd Flr SF | Total Bsmt SF | Garage Area | Wood Deck SF | Open Porch SF | Lot Area | Year Built | Yr Sold | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 35000 | 498 | 0 | 498 | 216 | 0 | 0 | 8088 | 1922 | 2006 | +| 39300 | 334 | 0 | 0 | 0 | 0 | 0 | 5000 | 1946 | 2007 | +| 40000 | 649 | 668 | 649 | 250 | 0 | 54 | 8500 | 1920 | 2008 | +| 45000 | 612 | 0 | 0 | 308 | 0 | 0 | 5925 | 1940 | 2009 | +| 52000 | 729 | 0 | 270 | 0 | 0 | 0 | 4130 | 1935 | 2008 | +| 52500 | 693 | 0 | 693 | 0 | 0 | 20 | 4118 | 1941 | 2006 | +| 55000 | 723 | 363 | 723 | 400 | 0 | 24 | 11340 | 1920 | 2008 | +| 55000 | 796 | 0 | 796 | 0 | 0 | 0 | 3636 | 1922 | 2008 | +| 57625 | 810 | 0 | 0 | 280 | 119 | 24 | 21780 | 1910 | 2009 | +| 58500 | 864 | 0 | 864 | 200 | 0 | 0 | 8212 | 1914 | 2010 | + +(省略了 1992 行) + +销售价格的直方图显示出大量的变化,分布显然不是正态。 右边的长尾包含几个价格非常高的房屋。 左边的短尾不包含任何售价低于 35,000 美元的房屋。 + +```py +sales.hist('SalePrice', bins=32, unit='$') +``` + +![](img/15-21.png) + +### 相关性 + +没有单个属性足以预测销售价格。 例如,第一层面积(平方英尺)与销售价格相关,但仅解释其一些变化。 + +```py +sales.scatter('1st Flr SF', 'SalePrice') + +correlation(sales, 'SalePrice', '1st Flr SF') +0.64246625410302249 +``` + +![](img/15-22.png) + +事实上,没有任何单个属性与销售价格的相关性大于 0.7(销售价格本身除外)。 + +```py +for label in sales.labels: + print('Correlation of', label, 'and SalePrice:\t', correlation(sales, label, 'SalePrice')) +Correlation of SalePrice and SalePrice: 1.0 +Correlation of 1st Flr SF and SalePrice: 0.642466254103 +Correlation of 2nd Flr SF and SalePrice: 0.35752189428 +Correlation of Total Bsmt SF and SalePrice: 0.652978626757 +Correlation of Garage Area and SalePrice: 0.638594485252 +Correlation of Wood Deck SF and SalePrice: 0.352698666195 +Correlation of Open Porch SF and SalePrice: 0.336909417026 +Correlation of Lot Area and SalePrice: 0.290823455116 +Correlation of Year Built and SalePrice: 0.565164753714 +Correlation of Yr Sold and SalePrice: 0.0259485790807 +``` + +但是,组合属性可以提供更高的相关性。 特别是,如果我们总结一楼和二楼的面积,那么结果的相关性就比任何单独的属性都要高。 + +```py +both_floors = sales.column(1) + sales.column(2) +correlation(sales.with_column('Both Floors', both_floors), 'SalePrice', 'Both Floors') +0.7821920556134877 +``` + +这种高度相关性表明,我们应该尝试使用多个属性来预测销售价格。 在具有多个观测属性和要预测的单个数值(这里是销售价格)的数据集中,多重线性回归可能是有效的技术。 + +## 多元线性回归 + +在多元线性回归中,通过将每个属性值乘以不同的斜率,从数值输入属性预测数值输出,然后对结果求和。 在这个例子中,第一层的斜率将代表房屋第一层面积的美元每平方英尺,它应该用于我们的预测。 + +在开始预测之前,我们将数据随机分成一个相同大小的训练和测试集。 + +```py +train, test = sales.split(1001) +print(train.num_rows, 'training and', test.num_rows, 'test instances.') +1001 training and 1001 test instances. +``` + +多元回归中的斜率是一个数组,例子中每个属性拥有一个斜率值。 预测销售价格包括,将每个属性乘以斜率并将结果相加。 + +```py +def predict(slopes, row): + return sum(slopes * np.array(row)) + +example_row = test.drop('SalePrice').row(0) +print('Predicting sale price for:', example_row) +example_slopes = np.random.normal(10, 1, len(example_row)) +print('Using slopes:', example_slopes) +print('Result:', predict(example_slopes, example_row)) +Predicting sale price for: Row(1st Flr SF=1092, 2nd Flr SF=1020, Total Bsmt SF=952.0, Garage Area=576.0, Wood Deck SF=280, Open Porch SF=0, Lot Area=11075, Year Built=1969, Yr Sold=2008) +Using slopes: [ 9.99777721 9.019661 11.13178317 9.40645585 11.07998556 + 11.03830075 10.26908341 10.42534332 11.00103437] +Result: 195583.275784 +``` + +结果是估计的销售价格,可以将其与实际销售价格进行比较,以评估斜率是否提供准确的预测。 由于上面的`example_slopes`是随机选取的,我们不应该期望它们提供准确的预测。 + +```py +print('Actual sale price:', test.column('SalePrice').item(0)) +print('Predicted sale price using random slopes:', predict(example_slopes, example_row)) +Actual sale price: 206900 +Predicted sale price using random slopes: 195583.275784 +``` + +### 最小二乘回归 + +执行多元回归的下一步是定义最小二乘目标。 我们对训练集中的每一行执行预测,然后根据实际价格计算预测的均方根误差(RMSE)。 + +```py +train_prices = train.column(0) +train_attributes = train.drop(0) + +def rmse(slopes, attributes, prices): + errors = [] + for i in np.arange(len(prices)): + predicted = predict(slopes, attributes.row(i)) + actual = prices.item(i) + errors.append((predicted - actual) ** 2) + return np.mean(errors) ** 0.5 + +def rmse_train(slopes): + return rmse(slopes, train_attributes, train_prices) + +print('RMSE of all training examples using random slopes:', rmse_train(example_slopes)) +RMSE of all training examples using random slopes: 69653.9880638 +``` + +最后,我们使用`minimize `函数来找到使 RMSE 最低的斜率。 由于我们想要最小化的函数`rmse_train`需要一个数组而不是一个数字,所以我们必须向`minimize`函数传递`array = True`参数。 当使用这个参数时,`minimize`也需要斜率的初始猜测,以便知道输入数组的维数。 最后,为了加速优化,我们使用`smooth = True`属性,指出`rmse_train`是一个平滑函数。 计算最佳斜率可能需要几分钟的时间。 + +```py +best_slopes = minimize(rmse_train, start=example_slopes, smooth=True, array=True) +print('The best slopes for the training set:') +Table(train_attributes.labels).with_row(list(best_slopes)).show() +print('RMSE of all training examples using the best slopes:', rmse_train(best_slopes)) +The best slopes for the training set: +``` + + +| 1st Flr SF | 2nd Flr SF | Total Bsmt SF | Garage Area | Wood Deck SF | Open Porch SF | Lot Area | Year Built | Yr Sold | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 73.7779 | 72.3057 | 51.8885 | 46.5581 | 39.3267 | 11.996 | 0.451265 | 538.243 | -534.634 | + +```py +RMSE of all training examples using the best slopes: 31146.4442711 +``` + +### 解释多元线性回归 + +让我们来解释这些结果。 最佳斜率为我们提供了一个方法,从其房屋属性估算价格。 一楼的面积约为 75 美元每平方英尺(第一个斜率),而二楼的面积约为 70 元每平方英尺(第二个斜率)。 最后的负值描述了市场:最近几年的价格平均较低。 + +大约 3 万美元的 RMSE 意味着,我们基于所有属性的销售价格的最佳线性预测,在训练集上平均差了大约 3 万美元。 当预测测试集的价格时,我们发现了类似的误差,这表明我们的预测方法可推广到来自同一总体的其他样本。 + +```py +test_prices = test.column(0) +test_attributes = test.drop(0) + +def rmse_test(slopes): + return rmse(slopes, test_attributes, test_prices) + +rmse_linear = rmse_test(best_slopes) +print('Test set RMSE for multiple linear regression:', rmse_linear) +Test set RMSE for multiple linear regression: 31105.4799398 +``` + +如果预测是完美的,那么预测值和实际值的散点图将是一条斜率为 1 的直线。我们可以看到大多数点落在该线附近,但预测中存在一些误差。 + +```py +def fit(row): + return sum(best_slopes * np.array(row)) + +test.with_column('Fitted', test.drop(0).apply(fit)).scatter('Fitted', 0) +plots.plot([0, 5e5], [0, 5e5]); +``` + +![](img/15-23.png) + +多元回归的残差图通常将误差(残差)与预测变量的实际值进行比较。 我们在下面的残差图中看到,我们系统性低估了昂贵房屋的值,由图右侧的许多正的残差值所示。 + +```py +test.with_column('Residual', test_prices-test.drop(0).apply(fit)).scatter(0, 'Residual') +plots.plot([0, 7e5], [0, 0]); +``` + +![](img/15-24.png) + +就像简单的线性回归一样,解释预测结果至少和预测一样重要。 很多解释多元回归的课程不包含在这个课本中。 完成这门课之后的下一步自然是深入研究线性建模和回归。 + +## 最近邻回归 + +另一种预测房屋销售价格的方法是使用类似房屋的价格。 这个最近邻的方法与我们的分类器非常相似。 为了加速计算,我们将只使用与原始分析中销售价格相关性最高的属性。 + +```py +train_nn = train.select(0, 1, 2, 3, 4, 8) +test_nn = test.select(0, 1, 2, 3, 4, 8) +train_nn.show(3) +``` + + +| SalePrice | 1st Flr SF | 2nd Flr SF | Total Bsmt SF | Garage Area | Year Built | +| --- | --- | --- | --- | --- | --- | +| 240000 | 1710 | 0 | 1710 | 550 | 2004 | +| 229000 | 1302 | 735 | 672 | 472 | 1996 | +| 136500 | 864 | 0 | 864 | 336 | 1978 | + +(省略了 998 行) + +最近邻的计算与最近邻分类器相同。 在这种情况下,我们将从距离计算中排除`'SalePrice'`而不是`'Class'`列。 第一个测试行的五个最近邻如下所示。 + +```py +def distance(pt1, pt2): + """The distance between two points, represented as arrays.""" + return np.sqrt(sum((pt1 - pt2) ** 2)) + +def row_distance(row1, row2): + """The distance between two rows of a table.""" + return distance(np.array(row1), np.array(row2)) + +def distances(training, example, output): + """Compute the distance from example for each row in training.""" + dists = [] + attributes = training.drop(output) + for row in attributes.rows: + dists.append(row_distance(row, example)) + return training.with_column('Distance', dists) + +def closest(training, example, k, output): + """Return a table of the k closest neighbors to example.""" + return distances(training, example, output).sort('Distance').take(np.arange(k)) + +example_nn_row = test_nn.drop(0).row(0) +closest(train_nn, example_nn_row, 5, 'SalePrice') +``` + + +| SalePrice | 1st Flr SF | 2nd Flr SF | Total Bsmt SF | Garage Area | Year Built | Distance | +| --- | --- | --- | --- | --- | --- | +| 150000 | 1299 | 0 | 967 | 494 | 1954 | 51.9711 | +| 144000 | 1344 | 0 | 1024 | 484 | 1958 | 60.8358 | +| 183500 | 1299 | 0 | 1001 | 486 | 1979 | 68.6003 | +| 140000 | 1283 | 0 | 931 | 506 | 1962 | 76.5049 | +| 173000 | 1287 | 0 | 957 | 541 | 1977 | 77.2464 | + +预测价格的一个简单方法是计算最近邻的价格均值。 + +```py +def predict_nn(example): + """Return the majority class among the k nearest neighbors.""" + return np.average(closest(train_nn, example, 5, 'SalePrice').column('SalePrice')) + +predict_nn(example_nn_row) +158100.0 +``` + +最后,我们可以使用一个测试样本,检查我们的预测是否接近真实销售价格。 看起来很合理! + +```py +print('Actual sale price:', test_nn.column('SalePrice').item(0)) +print('Predicted sale price using nearest neighbors:', predict_nn(example_nn_row)) +Actual sale price: 146000 +Predicted sale price using nearest neighbors: 158100.0 +``` + +### 尾注 + +为了为整个测试集评估这个方法的性能,我们将`predict_nn`应用于每个测试示例,然后计算预测的均方根误差。 预测的计算可能需要几分钟的时间。 + +```py +nn_test_predictions = test_nn.drop('SalePrice').apply(predict_nn) +rmse_nn = np.mean((test_prices - nn_test_predictions) ** 2) ** 0.5 + +print('Test set RMSE for multiple linear regression: ', rmse_linear) +print('Test set RMSE for nearest neighbor regression:', rmse_nn) +Test set RMSE for multiple linear regression: 30232.0744208 +Test set RMSE for nearest neighbor regression: 31210.6572877 +``` + +对于这些数据,这两种技术的误差非常相似! 对于不同的数据集,一种技术可能会胜过另一种。 通过计算两种技术在同一数据上的均方根误差,我们可以公平比较这些方法。值得注意的是:表现的差异可能不完全由于技术;这可能由于随机变化,由于首先对训练和测试集进行抽样。 + +最后,我们可以为这些预测画出一个残差图。 我们仍然低估了最昂贵房屋的价格,但偏差似乎并不像系统性的。 然而,较低价格的残差非常接近零,这表明较低价格的预测准确性非常高。 + +```py +test.with_column('Residual', test_prices-nn_test_predictions).scatter(0, 'Residual') +plots.plot([0, 7e5], [0, 0]); +``` + +![](img/15-25.png) diff --git a/docs/data8-textbook-zh/16.md b/docs/data8-textbook-zh/16.md new file mode 100644 index 0000000000000000000000000000000000000000..58e582f9286ba4f8b38b433d2081966d0091e946 --- /dev/null +++ b/docs/data8-textbook-zh/16.md @@ -0,0 +1,896 @@ +# 十六、比较两个样本 + +> 原文:[Comparing Two Samples](https://github.com/data-8/textbook/tree/gh-pages/chapters/16) + +> 译者:[飞龙](https://github.com/wizardforcel) + +> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) + +> 自豪地采用[谷歌翻译](https://translate.google.cn/) + + +最近邻分类方法的动机是这样的,个体可能像最近的邻居。 从另一个角度来看,我们可以说一个类别的个体不像另一个类别中的个体。 机器学习为我们提供了一种有力的方法来发现这种相似性的缺乏,并将其用于分类。 它揭示了一种模式,通过一次检查一两个属性,我们不一定能发现它。 + +但是,我们可以从属性中学到很多东西。 为了了解它,我们将比较两个类中的属性分布。 + +让我们来看看 Brittany Wenger 的乳腺癌数据,看看是否只用一个属性,就有希望生成一个合理的分类器。 和以前一样,我们将在随机选择的训练集上进行探索,然后在剩余的保留集上测试我们的分类器。 + +```py +patients = Table.read_table('breast-cancer.csv').drop('ID') +shuffled_patients = patients.sample(with_replacement=False) +training_set = shuffled_patients.take(np.arange(341)) +test_set = shuffled_patients.take(np.arange(341, 683)) +training_set +``` + +| Clump Thickness | Uniformity of Cell Size | Uniformity of Cell Shape | Marginal Adhesion | Single Epithelial Cell Size | Bare Nuclei | Bland Chromatin | Normal Nucleoli | Mitoses | Class | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 5 | 1 | 1 | 1 | 2 | 1 | 2 | 1 | 1 | 0 | +| 5 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | +| 4 | 1 | 1 | 1 | 2 | 1 | 1 | 1 | 1 | 0 | +| 5 | 1 | 2 | 1 | 2 | 1 | 3 | 1 | 1 | 0 | +| 4 | 10 | 8 | 5 | 4 | 1 | 10 | 1 | 1 | 1 | +| 7 | 2 | 4 | 1 | 3 | 4 | 3 | 3 | 1 | 1 | +| 9 | 4 | 5 | 10 | 6 | 10 | 4 | 8 | 1 | 1 | +| 3 | 1 | 1 | 1 | 2 | 2 | 3 | 1 | 1 | 0 | +| 3 | 2 | 1 | 1 | 2 | 1 | 2 | 2 | 1 | 0 | +| 6 | 3 | 3 | 5 | 3 | 10 | 3 | 5 | 3 | 0 | + +(省略了 331 行) + +让我们看看第二个属性`Uniformity of Cell Size`,能告诉我们患者分类的什么事情。 + +```py +training_cellsize = training_set.select('Class', 'Uniformity of Cell Size').relabel(1, 'Uniformity') +training_cellsize +``` + + +| Class | Uniformity | +| --- | --- | +| 0 | 1 | +| 0 | 1 | +| 0 | 1 | +| 0 | 1 | +| 1 | 10 | +| 1 | 2 | +| 1 | 4 | +| 0 | 1 | +| 0 | 2 | +| 0 | 3 | + +(省略了 331 行) + +`Class`和`Uniformity`列显示为数字,但他们真的都是类别值。 这些类别是“癌症”(1)和“非癌症”(0)。 `Uniformity`为 1-10,但是这些标签是由人确定的,他们也可能有十个标签,如“非常一致”,“不一致”等等。 (一致性的 2 不一定是 1 的两倍。)所以我们比较两个类别分布,每个分类一个。 + +对于每个类别和每个一致评分,我们都需要训练集的患者数量。`pivot`方法将为我们计数。 + +```py +training_counts = training_cellsize.pivot('Class', 'Uniformity') +training_counts +``` + +| Uniformity | 0 | 1 | +| --- | --- | --- | +| 1 | 181 | 3 | +| 2 | 21 | 2 | +| 3 | 16 | 15 | +| 4 | 4 | 18 | +| 5 | 0 | 17 | +| 6 | 0 | 8 | +| 7 | 0 | 8 | +| 8 | 1 | 13 | +| 9 | 1 | 4 | +| 10 | 0 | 29 | + + +我们现在有了一些东西,类似于每个类别的一致评分的分布。 而这两者看起来相当不同。 但是,我们要小心 - 这两个类别的患者总数是 341(训练集的大小),超过一半的人在类别 0 里面。 + +```py +np.sum(training_counts.column('0')) +224 +``` + +所以为了比较两个分布,我们应该把计数转换成比例然后可视化。 + + +```py +def proportions(array): + return array/np.sum(array) +training_dists = training_counts.select(0).with_columns( + '0', proportions(training_counts.column('0')), + '1', proportions(training_counts.column('1')) +) +training_dists.barh('Uniformity') +``` + +![](img/16-1.png) + +这两个分布看起来不一样! 事实上,它们看起来相当不同,我们应该能够基于对这种差异的直截了当的观察来构建一个非常合理的分类器。 一个简单的分类规则是:“如果一致性大于 3,类别就是 1,也就是说这个单元格就有癌症的,否则类别就是 0。 + +这么粗糙的东西有什么好处吗? 让我们试试看。 对于测试集中的任何个体,我们所要做的就是,查看一致评分是否大于 3。例如,对于前 4 名患者,我们将得到一组四个布尔值: + +```py +test_set.take(np.arange(4)).column('Uniformity of Cell Size') > 3 +array([ True, False, False, False], dtype=bool) +``` + +请记住,`True`等于`1`,如果一致性大于 3,那么这是我们要划分的分类。因此,为了测量粗分类器的准确性,我们所要做的就是,求得测试集患者的比例, 其中分类与患者已知的分类相同。 我们将使用上一节中写的`count_equal`函数。 + +```py +classification = test_set.column('Uniformity of Cell Size') > 3 + +count_equal(classification, test_set.column('Class'))/test_set.num_rows +0.935672514619883 +``` + +这相当准确,即使我们只使用单个属性单行代码的分类器! + +这是否意味着上一章中最近邻的方法是不必要的? 不,因为那些更准确,并且对于癌症诊断,任何患者都想要尽可能精确的方法。 但是看到简单的方法并不坏,这是令人欣慰的。 + +## 两个类别分布 + +为了查看两个数值变量如何相关,可以使用相关系数来衡量线性关联。 但是,我们应该如何确定两个分类变量是否相关? 例如,我们如何决定一个属性是否与个体的类别有关? 这是一个很重要的问题,因为如果不相关的话,你可以把它从你的分类器中删除。 + + +在乳腺癌数据中,我们来看看有丝分裂活动是否与这个类别有关。 我们已经标记了“癌症”和“非癌症”的类别,以便以后参考。 + +```py +classes = Table().with_columns( + 'Class', make_array(0, 1), + 'Class Label', make_array('Not Cancer', 'Cancer') +) +patients = Table.read_table('breast-cancer.csv').drop('ID').join('Class', classes) +patients = patients.drop('Class').relabel('Class Label', 'Class') +mitoses = patients.select('Class', 'Mitoses') +mitoses +``` + + +| Class | Mitoses | +| --- | --- | +| Not Cancer | 1 | +| Not Cancer | 1 | +| Not Cancer | 1 | +| Not Cancer | 1 | +| Not Cancer | 1 | +| Not Cancer | 1 | +| Not Cancer | 1 | +| Not Cancer | 5 | +| Not Cancer | 1 | +| Not Cancer | 1 | + +(省略了 673 行) + +我们可以使用`pivot`和`proportions`(在前面的章节中定义)来显示两类中`Mitoses`的分布。 + +```py +counts = mitoses.pivot('Class', 'Mitoses') +counts +``` + +| Mitoses | Cancer | Not Cancer | +| --- | --- | --- | +| 1 | 132 | 431 | +| 2 | 27 | 8 | +| 3 | 31 | 2 | +| 4 | 12 | 0 | +| 5 | 5 | 1 | +| 6 | 3 | 0 | +| 7 | 8 | 1 | +| 8 | 7 | 1 | +| 10 | 14 | 0 | + +```py +dists = counts.select(0).with_columns( + 'Cancer', proportions(counts.column(1)), + 'Not Cancer', proportions(counts.column(2)) + +) +dists.barh(0) +``` + +![](img/16-2.png) + +与“非癌症”类别的分布相比,“癌症”类别的`Mitoses`都集中于最低评分。 + +所以看起来类别和有丝分裂活动是相关的。 但是,这可能只是由于偶然嘛? + +为了了解偶然来自哪里,请记住,数据就像是来自更大总体的随机样本 - 总体包含我们可能要分类的新个体。 可能在总体中,类别和有丝分裂是相互独立的,只是由于偶然与样本相关。 + +### 假设 + +我们试着通过对以下假设进行测试来回答这个问题。 + +原假设。 在总体中,类别和有丝分裂评分是相互独立的;换句话说,这两个类别的有丝分裂的分布是一样的。 由于偶然性,样本分布是不同的。 + +备选假说。 在总体中,类别和有丝分裂评分是相关的。 + +为了了解如何测试它,我们再看一下数据。 + +```py +mitoses +``` + + +| Class | Mitoses | +| --- | --- | +| Not Cancer | 1 | +| Not Cancer | 1 | +| Not Cancer | 1 | +| Not Cancer | 1 | +| Not Cancer | 1 | +| Not Cancer | 1 | +| Not Cancer | 1 | +| Not Cancer | 5 | +| Not Cancer | 1 | +| Not Cancer | 1 | + +(省略了 673 行) + +### 随机排列 + +如果类别和有丝分裂评分是不相关的,那么`Mitoses`值出现的顺序并不重要,因为它们与类别的值无关,所有的重新排列应该是等可能的。 这与我们在分析足球`Deflategate`数据时采用的方法相同。 + +所以让我们将所有的`Mitoses`值整理到一个名为`shuffled_mitoses`的数组中。 你可以看到下面的第一项,但它包含 683 个项目,因为它是整个`Mitoses`列的排列(即重新排列)。 + +```py +shuffled_mitoses = mitoses.select('Mitoses').sample(with_replacement=False).column(0) +shuffled_mitoses.item(0) +1 +``` + +让我们扩展`mitoses`表,添加一列乱序的值。 + +```py +mitoses = mitoses.with_column('Shuffled Mitoses', shuffled_mitoses) +mitoses +``` + + +| Class | Mitoses | Shuffled Mitoses | +| --- | --- | --- | +| Not Cancer | 1 | 1 | +| Not Cancer | 1 | 1 | +| Not Cancer | 1 | 1 | +| Not Cancer | 1 | 1 | +| Not Cancer | 1 | 7 | +| Not Cancer | 1 | 1 | +| Not Cancer | 1 | 1 | +| Not Cancer | 5 | 3 | +| Not Cancer | 1 | 1 | +| Not Cancer | 1 | 2 | + +(省略了 673 行) + +让我们看看乱序数据的有丝分裂的分布,使用与原始数据相同的过程。 + +```py +shuffled = mitoses.select('Class', 'Shuffled Mitoses') + +shuffled_counts = shuffled.pivot('Class', 'Shuffled Mitoses') + +shuffled_counts +``` + + +| Shuffled Mitoses | Cancer | Not Cancer | +| --- | --- | --- | +| 1 | 199 | 364 | +| 2 | 12 | 23 | +| 3 | 12 | 21 | +| 4 | 5 | 7 | +| 5 | 2 | 4 | +| 6 | 0 | 3 | +| 7 | 3 | 6 | +| 8 | 3 | 5 | +| 10 | 3 | 11 | + +这两个类中的乱序数据的分布可以展示为条形图,就像原始数据一样。 + +```py +shuffled_dists = shuffled_counts.select(0).with_columns( + 'Cancer', proportions(shuffled_counts.column(1)), + 'Not Cancer', proportions(shuffled_counts.column(2)) +) +shuffled_dists.barh(0) +``` + +![](img/16-3.png) + +这与原始条形图看起来有点不同,为方便起见,再次展示如下。 + +```py +dists.barh(0) +``` + +![](img/16-4.png) + +### 检验统计量:总变异距离 + +我们需要一个测试统计量来衡量蓝色和金色分布之间的差异。 回想一下,总变异距离可以用来量化两个类别分布的差异。 + +```py +def tvd(dist1, dist2): + return 0.5*(np.sum(np.abs(dist1 - dist2))) +``` + +在原始样本中,两个类别的有丝分裂的分布的 TVD 约为 0.4: + +```py +observed_tvd = tvd(dists.column(1), dists.column(2)) +observed_tvd +0.41841946549059517 +``` + +但是在乱序的样本中,它比较小: + +```py +tvd(shuffled_dists.column(1), shuffled_dists.column(2)) +0.022173847487655045 +``` + +随机排列的有丝分裂评分和原始评分似乎表现不一样。 但是如果我们再次运行,随机打乱可能会有所不同。 让我们重新打乱并重新计算总变异距离。 + +```py +shuffled_mitoses = mitoses.select('Mitoses').sample(with_replacement=False).column(0) + +shuffled = mitoses.select('Class').with_column('Shuffled Mitoses', shuffled_mitoses) + +shuffled_counts = shuffled.pivot('Class', 'Shuffled Mitoses') + +tvd(proportions(shuffled_counts.column(1)), proportions(shuffled_counts.column(2))) +0.039937426966715643 +``` + +总变异距离仍然比我们从原始数据得到的 0.42 小很多。 为了看看它变化了多少,我们不得不重复多次随机打乱过程,在它现在已经变得很熟悉了。 + +### 原假设下 TVD 的经验分布 + +如果原假设是真的,则有丝分裂评分的所有排列都是等可能的。 有很多可能的排列;让我们做 5000 次,看看我们的检验统计量的变化。 代码与上面的代码完全一样,只是现在我们将收集所有 5000 个距离并绘制经验直方图。 + +```py +repetitions = 5000 +tvds = make_array() +for i in np.arange(repetitions): + shuffled_mitoses = mitoses.select('Mitoses').sample(with_replacement=False).column(0) + shuffled = mitoses.select('Class').with_column('Shuffled Mitoses', shuffled_mitoses) + shuffled_counts = shuffled.pivot('Class', 'Shuffled Mitoses') + new_tvd = tvd(proportions(shuffled_counts.column(1)), proportions(shuffled_counts.column(2))) + tvds = np.append(tvds, new_tvd) + +Table().with_column('TVD', tvds).hist(bins=20) +plots.title('Empirical Distribution Under the Null') +print('Observed TVD:', observed_tvd) +Observed TVD: 0.418419465491 +``` + +![](img/16-5.png) + +观察到的总变异距离 0.42 根本不接近于假设零假设为真所产生的分布。 数据支持备选假设:有丝分裂评分与类别有关。 + +### 两个类别分布的相等性的排列检验 + + +我们上面所做的检验被称为原假设的排列检验,即两个样本是从相同的底层分布中抽取的。 + +为了定义一个执行检验的函数,我们可以复制前一个单元格的代码,并更改表和列的名称。函数`permutation_test_tvd`接受数据表的名称,包含类别变量的列标签,它的分布要检验,包含二元类别变量的列标签,以及要运行的随机排列的数量。 + +在我们上面的例子中,我们没有计算 P 值,因为观测值远离原假设下统计量的分布。但是,一般来说,我们应该计算 P 值,因为在其他例子中统计量可能不是那么极端。 P 值是“假设原假设为真,所得距离大于等于观测距离”的几率,因为备选假设比原假设预测了更大的距离。 + +```py +def permutation_test_tvd(table, variable, classes, repetitions): + + """Test whether a categorical variable is independent of classes: + table: name of table containing the sample + variable: label of column containing categorical variable whose distribution is of interest + classes: label of column containing binary class data + repetitions: number of random permutations""" + + # Find the tvd between the distributions of variable in the two classes + counts = table.select(classes, variable).pivot(classes, variable) + observed_tvd = tvd(proportions(counts.column(1)), proportions(counts.column(2))) + + # Assuming the null is true, randomly permute the variable and collect all the new tvd's + tvds = make_array() + for i in np.arange(repetitions): + shuffled_var = table.select(variable).sample(with_replacement=False).column(0) + shuffled = table.select(classes).with_column('Shuffled Variable', shuffled_var) + shuffled_counts = shuffled.pivot(classes, 'Shuffled Variable') + new_tvd =tvd(proportions(shuffled_counts.column(1)), proportions(shuffled_counts.column(2))) + tvds = np.append(tvds, new_tvd) + + # Find the empirical P-value: + emp_p = np.count_nonzero(tvds >= observed_tvd)/repetitions + + # Draw the empirical histogram of the tvd's generated under the null, + # and compare with the value observed in the original sample + Table().with_column('TVD', tvds).hist(bins=20) + plots.title('Empirical Distribution Under the Null') + print('Observed TVD:', observed_tvd) + print('Empirical P-value:', emp_p) +permutation_test_tvd(patients, 'Clump Thickness', 'Class', 5000) +Observed TVD: 0.638310905047 +Empirical P-value: 0.0 +``` + +![](img/16-6.png) + +同样,观测距离 0.64 离原假设预测的分布很远。 经验 P 值为 0,所以准确的 P 值将接近于零。 因此,如果类别和有丝分裂评分是不相关的,那么观测的数据是极不可能的。 + +所以得出的结论是,有丝分裂评分与类别有关,不仅在样本中,而且在总体中。 + +我们使用排列检验来帮助我们确定,类别属性的分布是否与类别相关。 一般来说,排列检验可以这样使用来确定,两个类别分布是否从相同的基本分布随机抽样。 + +## A/B 测试 + +我们使用随机排列来查看,两个样本是否从相同的基本分类分布抽取。 如果样本是数值的,则可以使用相同的方法;检验统计量的选择通常比较简单。 在我们使用`Deflategate`数据的例子中,我们使用了不同的方法来测试爱国者队和小马队用球是否来自相同的基本分布。 + +在现代数据分析中,决定两个数值样本是否来自相同的基本分布称为 A/B 测试。 名称是指两个样本 A 和 B 的标签。 + +### 吸烟者和不吸烟者 + +我们对随机抽样的母亲及其新生儿进行了许多不同的分析,但是我们还没有查看母亲是否吸烟的数据。 研究的目的之一,是看母亲吸烟是否与出生体重有关。 + +```py +baby = Table.read_table('baby.csv') +baby +``` + + +| Birth Weight | Gestational Days | Maternal Age | Maternal Height | Maternal Pregnancy Weight | Maternal Smoker | +| --- | --- | --- | --- | --- | --- | +| 120 | 284 | 27 | 62 | 100 | False | +| 113 | 282 | 33 | 64 | 135 | False | +| 128 | 279 | 28 | 64 | 115 | True | +| 108 | 282 | 23 | 67 | 125 | True | +| 136 | 286 | 25 | 62 | 93 | False | +| 138 | 244 | 33 | 62 | 178 | False | +| 132 | 245 | 23 | 65 | 140 | False | +| 120 | 289 | 25 | 62 | 125 | False | +| 143 | 299 | 30 | 66 | 136 | True | +| 140 | 351 | 27 | 68 | 120 | False | + +(省略了 1164 行) + +我们首先选择`Birth Weight`和`Maternal Smoker`。 样本中有 715 名非吸烟者,459 名吸烟者。 + +```py +weight_smoke = baby.select('Birth Weight', 'Maternal Smoker') +weight_smoke.group('Maternal Smoker') +``` + + +| Maternal Smoker | count | +| --- | --- | +| False | 715 | +| True | 459 | + +下面的第一个直方图显示了样本中非吸烟者的婴儿出生体重的分布。 第二个显示了吸烟者的婴儿出生体重。 + +```py +nonsmokers = baby.where('Maternal Smoker', are.equal_to(False)) +nonsmokers.hist('Birth Weight', bins=np.arange(40, 181, 5), unit='ounce') +``` + +![](img/16-7.png) + +```py +smokers = baby.where('Maternal Smoker', are.equal_to(True)) +smokers.hist('Birth Weight', bins=np.arange(40, 181, 5), unit='ounce') +``` + +![](img/16-8.png) + +两种分布都大致是钟形,中心在 120 盎司附近。 当然,这些分布并不相同,这就产生了这样一个问题,即差异是否仅仅反映了机会变异,还是反映了总体分布的差异。 + +这个问题可以通过假设检验来回答。 + +原假设:在总体中,不吸烟的母亲的婴儿出生体重的分布和吸烟的母亲相同。 样本中的差异是偶然的。 + +备选假设:两种分布在总体中是不同的。 + +检验统计量:出生体重是一个定量变量,所以用均值的绝对差作为检验统计量是合理的。 + +检验统计量的观测值约为 9.27 盎司。 + +```py +means_table = weight_smoke.group('Maternal Smoker', np.mean) +means_table +``` + +| Maternal Smoker | Birth Weight mean | +| --- | --- | +| False | 123.085 | +| True | 113.819 | + +```py +nonsmokers_mean = means_table.column(1).item(0) +smokers_mean = means_table.column(1).item(1) +nonsmokers_mean - smokers_mean +9.266142572024918 +``` + +## 排列检验 + +为了看看原假设下是否有可能出现这种差异,我们将使用排列检验,就像我们在前一节中所做的那样。 我们必须为检验统计量改变代码。 为此,我们将像上面那样计算平均值的差,然后取绝对值。 + +请记住,在原假设下,出生体重的所有排列与`Maternal Smoker `列等可能出现。 所以,就像以前一样,每次重复都是打乱正在比较的变量。 + +```py +def permutation_test_means(table, variable, classes, repetitions): + + """Test whether two numerical samples + come from the same underlying distribution, + using the absolute difference between the means. + table: name of table containing the sample + variable: label of column containing the numerical variable + classes: label of column containing names of the two samples + repetitions: number of random permutations""" + + t = table.select(variable, classes) + + # Find the observed test statistic + means_table = t.group(classes, np.mean) + obs_stat = abs(means_table.column(1).item(0) - means_table.column(1).item(1)) + + # Assuming the null is true, randomly permute the variable + # and collect all the generated test statistics + stats = make_array() + for i in np.arange(repetitions): + shuffled_var = t.select(variable).sample(with_replacement=False).column(0) + shuffled = t.select(classes).with_column('Shuffled Variable', shuffled_var) + m_tbl = shuffled.group(classes, np.mean) + new_stat = abs(m_tbl.column(1).item(0) - m_tbl.column(1).item(1)) + stats = np.append(stats, new_stat) + + # Find the empirical P-value: + emp_p = np.count_nonzero(stats >= obs_stat)/repetitions + + # Draw the empirical histogram of the tvd's generated under the null, + # and compare with the value observed in the original sample + Table().with_column('Test Statistic', stats).hist(bins=20) + plots.title('Empirical Distribution Under the Null') + print('Observed statistic:', obs_stat) + print('Empirical P-value:', emp_p) +permutation_test_means(baby, 'Birth Weight', 'Maternal Smoker', 5000) +Observed statistic: 9.266142572024918 +Empirical P-value: 0.0 +``` + +![](img/16-9.png) + +原始样本中的观测差异约为 9.27 盎司,与此分布不一致:经验 P 值为 0,这意味着确切的 P 值确实非常小。 因此,测试的结论是,在总体中,不吸烟者和吸烟者的婴儿出生体重的分布是不同的。 + + +## 差值的自举置信区间 + +我们的 A/B 测试得出结论,这两个分布是不同的,但有点不尽人意。他们有多么不同?哪一个均值更大?这些自然是测试无法回答的问题。 + +回想一下,我们之前已经讨论过这个问题了:不仅仅是问“两个分布是否不同”的是与否的问题,我们可以通过不作任何假设,并简单地估计均值之间的差异,来学到更多。 + +观测差异(不吸烟者减去吸烟者)约为 9.27 盎司;这个正面迹象表明,不吸烟的母亲通常有更大的婴儿。但由于随机性,样本可能会有所不同。为了了解有多么不同,我们必须生成更多的样本;为了生成更多的样本,我们将使用`bootstrap`,就像我们以前做过的那样。自举过程不会假设这两个分布是否相同。它只是复制原始随机样本并计算统计量的新值。 + +函数`bootstrap_ci_means`返回总体中两组均值之间差异的自举置信区间。在我们的例子中,置信区间将估计总体中吸烟和不吸烟的母亲的婴儿的平均出生体重之间的差异。 + ++ 表名称,它包含原始样本中的数据 ++ 列标签,它包含数值变量 ++ 列标签,它包含两个样本的名称 ++ 自举的重复次数 + +该函数使用自举百分比方法,返回两个均值之间的差异的约 95% 置信区间。 + +```py +def bootstrap_ci_means(table, variable, classes, repetitions): + + """Bootstrap approximate 95% confidence interval + for the difference between the means of the two classes + in the population""" + + t = table.select(variable, classes) + + mean_diffs = make_array() + for i in np.arange(repetitions): + bootstrap_sample = t.sample() + m_tbl = bootstrap_sample.group(classes, np.mean) + new_stat = m_tbl.column(1).item(0) - m_tbl.column(1).item(1) + mean_diffs = np.append(mean_diffs, new_stat) + + left = percentile(2.5, mean_diffs) + right = percentile(97.5, mean_diffs) + + # Find the observed test statistic + means_table = t.group(classes, np.mean) + obs_stat = means_table.column(1).item(0) - means_table.column(1).item(1) + + Table().with_column('Difference Between Means', mean_diffs).hist(bins=20) + plots.plot(make_array(left, right), make_array(0, 0), color='yellow', lw=8) + print('Observed difference between means:', obs_stat) + print('Approximate 95% CI for the difference between means:') + print(left, 'to', right) +bootstrap_ci_means(baby, 'Birth Weight', 'Maternal Smoker', 5000) +Observed difference between means: 9.266142572024918 +Approximate 95% CI for the difference between means: +7.23940878698 to 11.3907887554 + +``` + +![](img/16-10.png) + +不吸烟的母亲的婴儿比吸烟的母亲的婴儿平均重 7.2 盎司到 11.4 盎司。 这比“两个分布不同”更有用。 由于置信区间不包含 0,它也告诉我们这两个分布是不同的。 所以置信区间估计了我们的均值之间的差异,也让我们决定两个基本分布是否相同。 + +不吸烟的母亲比吸烟的母亲平均年龄稍大。 + +```py +bootstrap_ci_means(baby, 'Maternal Age', 'Maternal Smoker', 5000) +Observed difference between means: 0.8076725017901509 +Approximate 95% CI for the difference between means: +0.154278698588 to 1.4701157656 +``` + +![](img/16-11.png) + +但毫不奇怪,证据并没有指出,他们的平均身高与不吸烟的母亲不同。 零在均值之间差异的置信区间中。 + +```py +bootstrap_ci_means(baby, 'Maternal Height', 'Maternal Smoker', 5000) +Observed difference between means: 0.09058914941267915 +Approximate 95% CI for the difference between means: +-0.390841928035 to 0.204388297872 +``` + +![](img/16-12.png) + +总之: + +如果你想知道两个基本分布是否相同,则可以使用带有适当检验统计量的排列检验。 当分布是类别时,我们使用总变异距离,而分布是数值时,我们使用均值之间的绝对差。 + +为了比较两个数值分布,将假设检验替换为估计,通常更富有信息。 只需估计一个差异,比如两组均值之间的差异。 这可以通过构建自举置信区间来完成。 如果零不在这个区间内,你可以得出这样的结论:这两个分布是不同的,你也可以估计均值有多么不同。 + +## 因果 + +我们用于比较两个样本的方法在随机对照实验的分析中具有强大的用途。由于在这些实验中,实验组和对照组被随机分配,因此如果实验完全没有效果,结果中的任何差异,可以与仅仅由于分配中的随机性而发生的情况进行比较。如果观察到的差异比我们预测的,纯粹由于偶然的差异更为显着,我们就会有因果关系的证据。由于个体无偏分配到实验组和对照组,两组结果中的差异可归因于实验。 + +随机对照实验分析的关键是,了解偶然因素如何出现。这有助于我们设定明确的原假设和备选假设。一旦完成,我们可以简单地使用前一节的方法来完成分析。 + +让我们看看如何在一个例子中实现。 + +### 治疗慢性背痛:RCT + +成年人的背痛可能非常顽固,难以治疗。常见的方法从皮质类固醇到针灸。随机对照试验(RCT)检验了使用肉毒毒素 A 来治疗的效果。肉毒杆菌毒素是一种神经毒蛋白,会导致肉毒中毒的疾病;维基百科说肉毒杆菌是“已知最致命的毒素”。有七种类型的肉毒杆菌毒素。肉毒杆菌毒素 A 是可导致人类疾病的类型之一,但也被用于治疗涉及肌肉的各种疾病。福斯特(Foster),克拉普(Clapp)和贾巴里(Jabbari)在 2001 年分析的随机对照试验(RCT)将其用作一种治疗背痛的方法。 + +将 31 名背痛患者随机分为实验组和对照组,实验组 15 例,对照组 16 例。对照组给予生理盐水,试验是双盲的,医生和病人都不知道他们在哪个组。 + +研究开始 8 周后,实验组 15 名中的 9 名和对照组 16 名中的 2 名缓解了疼痛(由研究人员精确定义)。这些数据在`bta`表中,似乎表明实验有明显的益处。 + +```py +bta = Table.read_table('bta.csv') +bta +``` + +| Group | Result | +| --- | --- | +| Control | 1 | +| Control | 1 | +| Control | 0 | +| Control | 0 | +| Control | 0 | +| Control | 0 | +| Control | 0 | +| Control | 0 | +| Control | 0 | +| Control | 0 | + +(省略了 21 行) + +```py +bta.group('Group', np.mean) +``` + +| Group | Result mean | +| --- | --- | +| Control | 0.125 | +| Treatment | 0.6 | + +在实验组中,60% 的患者缓解了疼痛,而对照组只有 12.5%。没有一个患者有任何副作用。 + +因此,这表示是 A 型肉毒毒素比盐水更好。但结论还没确定。病人随机分配到两组,所以也许差异可能由于偶然? + +为了理解这意味着什么,我们必须考虑这样的可能性,即在研究中的 31 个人中,有些人可能比其他人恢复得更好,即使没有任何治疗的帮助。如果他们中的大部分不正常地分配到实验组,只是偶然呢?即使实验组仅仅给予对照组的生理盐水,实验组的结果可能会好于对照组。 + +为了解释这种可能性,我们首先仔细建立机会模型。 + +### 潜在的结果 + +在患者随机分为两组之前,我们的大脑本能地想象出每个患者的两种可能的结果:患者分配到实验组的结果,以及分配给对照组的结果。这被称为患者的两个潜在的结果。 + +因此有 31 个潜在的实验结果和 31 个潜在的对照结果。问题关于 31 个结果的这两组的分布。他们是一样的,还是不一样? + +我们还不能回答这个问题,因为我们没有看到每个组中的所有 31 个值。我们只能看到随机选择的 16 个潜在的对照结果,以及其余 15 个患者的实验结果。 + +这是一个展示设定的好方法。每个病人都有一张双面票: + +![](img/16-13.png) + +随机化之后,我们可以看到随机选择的一组票的右半部分,以及剩余分组的左半部分。 + +`observed_outcomes`表收集每个患者潜在结果的信息,每张“票”的未观察的一半留空。 (这只是考虑`bta`表的另一种方式,它载有的信息相同。) + +```py +observed_outcomes = Table.read_table("observed_outcomes.csv") +observed_outcomes.show() +``` + +| Group | Outcome if assigned treatment | Outcome if assigned control | +| --- | --- | --- | +| Control | Unknown | 1 | +| Control | Unknown | 1 | +| Control | Unknown | 0 | +| Control | Unknown | 0 | +| Control | Unknown | 0 | +| Control | Unknown | 0 | +| Control | Unknown | 0 | +| Control | Unknown | 0 | +| Control | Unknown | 0 | +| Control | Unknown | 0 | +| Control | Unknown | 0 | +| Control | Unknown | 0 | +| Control | Unknown | 0 | +| Control | Unknown | 0 | +| Control | Unknown | 0 | +| Control | Unknown | 0 | +| Treatment | 1 | Unknown | +| Treatment | 1 | Unknown | +| Treatment | 1 | Unknown | +| Treatment | 1 | Unknown | +| Treatment | 1 | Unknown | +| Treatment | 1 | Unknown | +| Treatment | 1 | Unknown | +| Treatment | 1 | Unknown | +| Treatment | 1 | Unknown | +| Treatment | 0 | Unknown | +| Treatment | 0 | Unknown | +| Treatment | 0 | Unknown | +| Treatment | 0 | Unknown | +| Treatment | 0 | Unknown | +| Treatment | 0 | Unknown | + +### 假设检验 + +问题是实验是否有用。根据观察得出的结果,问题在于第 2 列(包括未知数)的 31 个“实验”值的分布是否与第 3 列 31 个“对照”值的分布不同(同样包括未知数)。 + +原假设:所有 31 个潜在“实验”结果的分布与所有 31 个潜在“对照”结果的分布相同。实验与对照没有任何不同。两个样本的差异只是偶然而已。 + +备选假设:31 个潜在“实验”结果的分布与 31 个对照结果的分布不同。治疗做了一些不同于控制。 + +为了检验这些假设,请注意,如果原假设是真实的,那么 31 个观察结果的所有分布,对于标记为“对照”的 16 个结果和另外标记为“实验”的 15 个结果将具有相等的可能性。所以我们可以简单地对这些值进行排列,看看这两个组的分布是多么不同。更简单地说,由于数据是数值的,我们可以看到两个均值有多么不同。 + +这正是我们在上一节中为 A/B 测试所做的。样本 A 现在是对照组,样本 B 是实验组。我们的检验统计量是两组平均值的绝对差。 + +让我们为均值之间的差异运行我们的排列检验。只有 31 个观测值,所以我们可以运行大量的排列,而不必等待太久的结果。 + +```py +def permutation_test_means(table, variable, classes, repetitions): + + """Test whether two numerical samples + come from the same underlying distribution, + using the absolute difference between the means. + table: name of table containing the sample + variable: label of column containing the numerical variable + classes: label of column containing names of the two samples + repetitions: number of random permutations""" + + t = table.select(variable, classes) + + # Find the observed test statistic + means_table = t.group(classes, np.mean) + obs_stat = abs(means_table.column(1).item(0) - means_table.column(1).item(1)) + + # Assuming the null is true, randomly permute the variable + # and collect all the generated test statistics + stats = make_array() + for i in np.arange(repetitions): + shuffled_var = t.select(variable).sample(with_replacement=False).column(0) + shuffled = t.select(classes).with_column('Shuffled Variable', shuffled_var) + m_tbl = shuffled.group(classes, np.mean) + new_stat = abs(m_tbl.column(1).item(0) - m_tbl.column(1).item(1)) + stats = np.append(stats, new_stat) + + # Find the empirical P-value: + emp_p = np.count_nonzero(stats >= obs_stat)/repetitions + + # Draw the empirical histogram of the tvd's generated under the null, + # and compare with the value observed in the original sample + Table().with_column('Test Statistic', stats).hist() + plots.title('Empirical Distribution Under the Null') + print('Observed statistic:', obs_stat) + print('Empirical P-value:', emp_p) +permutation_test_means(bta, 'Result', 'Group', 20000) +Observed statistic: 0.475 +Empirical P-value: 0.00965 +``` + +![](img/16-14.png) + +经验 P 值非常小(研究报告 P 值为 0.009,这与我们的计算一致),因此证据倾向于备选假设:潜在实验和控制分布是不同的。 + +这是实验导致差异的证据,因为随机化确保了没有影响结论的混淆变量。 + +如果实验没有被随机分配,我们的测试仍然会指出我们 31 位患者的实验和背痛结果之间的关联。但要小心:没有随机化,这种关联并不意味着,实验会导致背痛结果的改变。例如,如果患者自己选择是否进行实验,则可能疼痛更严重的患者更可能选择实验,并且甚至在没有药物治疗的情况下,更可能减轻疼痛。预先存在的疼痛将成为分析中的混淆因素。 + +### 效果的置信区间 + +正如我们在上一节中指出的那样,只是简单地得出结论,说治疗是有用的,还不够。我们还想知道它做了什么。 + +因此,不要对两个基本分布的假设进行是与否的测试,而是仅仅估计它们之间的差异。具体来说,我们查看所有 31 个对照结果的平均值减去所有 31 个实验结果的平均值。这是未知的参数,因为我们只有 16 个对照值和 15 个实验值。 + +在我们的样本中,平均值的差异是 -47.5%。对照组平均为 12.5%,而治疗组平均为 60%。差异的负面信号表明实验组效果更好。 + +```py +group_means = bta.group('Group', np.mean) +group_means +``` + +| Group | Result mean | +| --- | --- | +| Control | 0.125 | +| Treatment | 0.6 | + +```py +group_means.column(1).item(0) - group_means.column(1).item(1) +-0.475 +``` + +但这只是一个样本的结果;样本可能会有所不同。 因此,我们将使用`bootstrap`复制样本,并重新计算差异。 这正是我们在前一节所做的。 + +```py +def bootstrap_ci_means(table, variable, classes, repetitions): + + """Bootstrap approximate 95% confidence interval + for the difference between the means of the two classes + in the population""" + + t = table.select(variable, classes) + + mean_diffs = make_array() + for i in np.arange(repetitions): + bootstrap_sample = t.sample() + m_tbl = bootstrap_sample.group(classes, np.mean) + new_stat = m_tbl.column(1).item(0) - m_tbl.column(1).item(1) + mean_diffs = np.append(mean_diffs, new_stat) + + left = percentile(2.5, mean_diffs) + right = percentile(97.5, mean_diffs) + + # Find the observed test statistic + means_table = t.group(classes, np.mean) + obs_stat = means_table.column(1).item(0) - means_table.column(1).item(1) + + Table().with_column('Difference Between Means', mean_diffs).hist(bins=20) + plots.plot(make_array(left, right), make_array(0, 0), color='yellow', lw=8) + print('Observed difference between means:', obs_stat) + print('Approximate 95% CI for the difference between means:') + print(left, 'to', right) +bootstrap_ci_means(bta, 'Result', 'Group', 20000) +Observed difference between means: -0.475 +Approximate 95% CI for the difference between means: +-0.759090909091 to -0.162393162393 +``` + +![](img/16-15.png) + +基本分布的均值之间的差异的约 95% 置信区间,范围是约 -80% 到 -20%。换句话说,实验组好转了 20% 到 80% 左右。 + +注意这个变化很大的估计。那是因为每个组的样本量只有 15 个左右。虽然这些作用于这些数值而没有进一步的假设,但结果并不十分精确。 + +### 元分析 + +虽然 RCT 确实真名了肉毒杆菌毒素 A 实验帮助了患者,但对 31 名患者进行的研究不足以确定治疗的有效性。这不仅仅是因为样本量小。我们在这一部分的结果对于研究中的 31 位患者是有效的,但我们对所有可能患者的总体真正感兴趣。如果 31 名患者是来自较大总体的随机样本,那么我们的置信区间对该总体是有效的。但他们不是随机样本。 + +2011 年,一组研究人员对实验的研究进行了元分析。也就是说,他们确定了所有被痛治疗的可用研究,并总结了整理后的结果。 + +有几项研究,但没有多少可以纳入科学合理的方式:“由于非随机性,不完整或未发表的数据,我们排除了 19 项研究的证据。只剩下三个随机对照试验,其中之一是我们在本节研究的。元分析给予它所有研究的最高评价(LBP 代表背痛):“我们确定了三项研究,它们调查了 BoNT 治疗 LBP 的优点,但只有一项的偏差风险低,并且使用非特异性 LBP(N = 31)来评价患者”。 + +元分析得出的结论是:“有一些低质量的证据表明,BoNT 注射剂能改善疼痛,功能,或者两者都比注射生理盐水更好,而且质量很低的证据表明,它比针灸或类固醇注射更好。进一步的研究很可能会对效果评估和我们的信心产生重要影响,未来的试验应该对患者总体,实验方案和比较组进行标准化,争取更多的参与者,并包括长期结果,成本效益分析和临床相关性的发现”。 + +为了确定医疗有好处,需要很多精心的工作。了解如何分析随机对照试验是这项工作的重要组成部分。现在你们知道了如何实现,你们有条件帮助医疗和其他行业建立因果关系。 diff --git a/docs/data8-textbook-zh/17.md b/docs/data8-textbook-zh/17.md new file mode 100644 index 0000000000000000000000000000000000000000..fa645c80d5c443cbba11af9de73270da6b37d044 --- /dev/null +++ b/docs/data8-textbook-zh/17.md @@ -0,0 +1,300 @@ +# 十七、更新预测 + +> 原文:[Updating Predictions](https://github.com/data-8/textbook/tree/gh-pages/chapters/17) + +> 译者:[飞龙](https://github.com/wizardforcel) + +> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) + +> 自豪地采用[谷歌翻译](https://translate.google.cn/) + +我们知道如何使用训练数据将一个点划分为两类之一。 我们的分类只是对类别的预测,基于最接近我们的新点的,训练点中最常见的类别。 + +假设我们最终发现了我们的新点的真实类别。 然后我们会知道我们的分类是否正确。 另外,我们将会有一个新点,可以加入到我们的训练集中,因为我们知道它的类别。 这就更新了我们的训练集。 所以,我们自然希望,根据新的训练集更新我们的分类器。 + +本章将介绍一些简单的情况,其中新的数据会使我们更新我们的预测。 虽然本章中的例子在计算方面较简单,但是更新方法可以推广到复杂的设定,是机器学习最强大的工具之一。 + +## “更可能”的二分类器 + +让我们尝试使用数据,将一个点划分为两个类别之一,选择我们认为更可能的类别。 为此,我们不仅需要数据,而且还要清楚地描述几率是什么样。 + +我们将从一个简单的人造情况开始,开发主要的技术,然后跳到更有趣的例子。 + +假设有个大学班级,其组成如下: + ++ 60% 的学生为二年级,其余的 40% 是三年级 ++ 50% 二年级学生已经声明了他们的专业 ++ 80% 三年级学生已经声明了他们的专业 + +现在假设我从班上随机挑选一个学生。 你能否用“更可能”的标准,将学生划分为二年级或三年级? + +你可以,因为这个学生是随机挑选的,所以你知道这个学生是二年级的几率是 60%。 这比三年级的 40% 的可能性更大,所以你会把学生划分为二年级。 + +专业的信息是无关紧要的,因为我们已经知道班上二,三年的比例。 + +我们有了非常简单的分类器! 但是现在假设我给了你一些被挑选的学生的更多信息: + +这个学生已经声明了专业。 + +这个知识会改变你的分类吗? + +## 基于新信息更新预测 + +现在我们知道学生已经宣布了专业,重要的是要看看年级和专业声明的关系。 二年级的学生比三年级多,这仍然正确。 但是,三年级的学生,比二年级的学生,声明专业的比例更高,这也是事实。 我们的分类器必须考虑到这两个观察。 + +为了使这个可视化,我们将使用`students`表,它包含 100 个学生,每个学生一行,学生的年级和专业比例和数据中相同。 + +```py +students.show(3) +``` + + +| Year | Major | +| --- | --- | +| Second | Undeclared | +| Second | Undeclared | +| Second | Undeclared | + +(省略了 97 行) + +为了检查比例是否正确,我们使用`pivot`,按照这两个变量对每个学生进行交叉分类。 + +```py +students.pivot('Major', 'Year') +``` + +| Year | Declared | Undeclared | +| --- | --- | --- | +| Second | 30 | 30 | +| Third | 32 | 8 | + +![](img/17-1.png) + +总人数为 100 人,其中二年级 60 人,三年级 40 人。 二年级中,每个专业类别有 50%。 三年级的 40 人中,20% 是未声明的,80% 已声明。 因此,这 100 人的比例和我们问题中的班级相同,我们可以假定,我们的学生是从 100 名学生中随机抽取的。 + +我们必须选择学生最可能进入的那一行。当我们对这个学生一无所知时,他或她可能在四个单元格中的任何一个,因此更可能在第一行(二年级),因为那里包含更多的学生。 + +但是现在我们知道这个学生已经声明了专业,所以可能结果的空间已经减少了:现在学生只能在两个已声明的单元格中的一个。 + +这些单元格共有 62 名学生,其中 32 名是三年级。 这是一半以上,即使不是太多。 + +所以,考虑到学生专业的新信息,我们必须更新我们的预测,现在将学生划分为三年级。 + +我们的分类的正确几率是多少? 对于所有声明了专业的 32 个三年级,我们是正确的,对于那 30 个二年级,我们是错误的。 因此,我们的正确几率大约是 0.516。 + +换句话说,我们正确几率是声明专业的学生中三年级的比例。 + +```py +32/(30+32) +0.5161290322580645 +``` + +## 树形图 + +我们刚刚计算的比例基于 100 名学生。 但是班级没有理由没有 200 名学生,只要单元格中的所有比例都是正确的。 那么我们的计算就变成了`64 /(60 + 64)`,就是 0.516。 + +所以计算只取决于不同类别的比例,而不是计数。 为了便于比较,比例可以用树形图可视化,直接显示在数据透视表下方。 + +```py +students.pivot('Major', 'Year') +``` + +| Year | Declared | Undeclared | +| --- | --- | --- | +| Second | 30 | 30 | +| Third | 32 | 8 | + +像数据透视表一样,该图将学生分成四个不同的组,称为“分支”。请注意,“三年级已声明”分支中的学生比例为`0.4 x 0.8 = 0.32`,对应于数据透视表中“三年级已声明”单元格中的 32 名学生。 “二年级已声明”分支中包含学生的`0.6 x 0.5 = 0.3`,对应于数据透视表中“二年级已声明”单元格中的 30 个。 + +我们知道,被挑选的学生属于“已声明”分支。也就是说,学生在两个顶层分支之一。这两个分支现在形成了我们的简化概率空间,所有几率的计算必须相对于这个简化空间的总概率。 + +所以,考虑到学生已声明专业,他们是三年级的几率可以直接从树中计算出来。答案是相对于两个“已声明”分类的总比例,“三年级已声明”分类的比例。 + +也就是说,答案是和以前一样,已声明的学生中三年级的比例。 + +```py +(0.4 * 0.8)/(0.6 * 0.5 + 0.4 * 0.8) +0.5161290322580645 +``` + +### 贝叶斯法则 + +我们刚刚使用的方法来源于托马斯·贝叶斯牧师(1701-1761)。他的方法解决了所谓的“逆向概率”问题:假设有了新的数据,如何更新之前发现的几率?虽然贝叶斯生活在三个世纪之前,但他的方法现在在机器学习中广泛使用。 + +我们将在学生总体的背景下讲述这个规则。首先,一些术语: + +先验概率。在我们知道所选学生的专业声明状态之前,学生是二年级的几率是 60%,学生是三年级的几率是 40%。这是两个类别的先验概率。 + +可能性。这是专业状态在给出学生类别情况下的几率;因此可以从树形图中读出。例如,假设学生是二年级,已声明的可能性是 0.5。 + +后验概率。这些是考虑专业声明状态的信息后,二年级的概率。我们计算了其中的一个: + +假设学生已经声明,学生是三年级的后验概率表示为 ![](img/tex-17-1.gif),计算如下。 + +![](img/tex-17-2.gif) + +另一个后验概率是: + +![](img/tex-17-3.gif) + +```py +(0.6 * 0.5)/(0.6 * 0.5 + 0.4 * 0.8) +0.4838709677419354 +``` + +这大概是 0.484,还不到一半,与我们三年的分类一致。 + +请注意,两个后验概率的分母相同:新信息,也就是学生已声明的几率。 + +正因为如此,贝叶斯方法有时被归纳为比例陈述: + +![](img/tex-17-4.gif) + +公式非常便于高效地描述计算。 但是在我们的学生示例这样的情况中,不用公式来思考更简单。 我们仅仅使用树形图。 + +## 做出决策 + +贝叶斯规则的一个主要用途,是基于不完整的信息做出决策,并在新的信息到来时纳入它们。本节指出了在决策时保持你的假设的重要性。 + +许多疾病的医学检测都会返回阳性或阴性结果。阳性结果意味着,根据检测患者有疾病。阴性结果意味着,检测的结论是患者没有这种疾病。 + +医学检测经过精心设计,非常准确。但是很少有检测是 100% 准确的。几乎所有检测都有两种错误: + +假阳性是,检测结果为阳性,但患者没有该疾病的错误。 + +假阴性是,检测结果为阴性,但患者确实有这种疾病的错误。 + +这些错误可能会影响人们的决策。假阳性可能引起焦虑和不必要的治疗(在某些情况下,这是昂贵的或危险的)。如果由于其阴性检测结果,患者未接受治疗,则假阴性可能具有更严重的后果。 + +### 罕见疾病的检测 + +假设总体很大,疾病只占总体的一小部分。 下面的属性图总结了这种疾病的信息,以及它的医学检测。 + +![](img/17-2.png) + +总的来说,只有千分之四的总体有这种疾病。 检测相当准确:假阳性几率非常小,为 5/1000,但是假阴性更大(尽管还是很小),为 1/100。 + +个体可能知道也可能不知道他们是否患有这种疾病;通常情况下,人们会进行检测来确认他们是否拥有。 + +所以假设随机从总体中挑选一个人并进行检测。 如果检测结果是阳性的,你会如何分类:患病还是没有患病? + +我们可以通过应用贝叶斯规则,和使用我们的“更可能”的分类器来回答这个问题。 鉴于该人已经检测出阳性,他或她患病的几率是相对于`Test Positive`分支中的总比例,顶层分支的比例。 + +```py +(0.004 * 0.99)/(0.004 * 0.99 + 0.996*0.005 ) +0.44295302013422816 +``` + +鉴于这个人已经检测出阳性,他或她有这种疾病的几率是大约 44%。 所以我们将它们分类为:没有疾病。 + +这是一个奇怪的结论。 我们有一个相当准确的检测,一个人检测出阳性,我们的分类是...他们没有这种疾病? 这似乎没有任何意义。 + +面对一个令人不安的答案,首先要做的是检查计算。 上面的算法是正确的。 我们来看看是否可以用不同的方式得到相同的答案。 + +函数`population`群体返回 100,000 名患者的结果表格,它的列展示了实际情况和检测结果。 检测与树中描述的相同。 但是有这种疾病的比例是这个函数的参数。 + +我们将 0.004 用作参数来调用`population`,然后调用`pivot `,对这十万人中的每一个人进行交叉分类。 + +```py +population(0.004).pivot('Test Result', 'True Condition') +``` + + +| True Condition | Negative | Positive | +| --- | --- | --- | +| Disease | 4 | 396 | +| No Disease | 99102 | 498 | + +表的单元格计数正确。 例如,根据总体的描述,一千人中有四人患有这种疾病。 表格中有十万人,所以 400 人应该有这种病。 这就是表格所显示的:`4 + 396 = 400`。在这 400 认中,99% 获得了阳性检测结果:`0.99 x 400 = 396`。 + +```py +396/(396 + 498) +0.4429530201342282 +``` + +这就是我们通过使用贝叶斯规则得到的答案。`Positives`列中的计数显示为什么它小于 1/2。 在阳性的人中,更多的人没有疾病而不是有疾病。 + +原因是,很大一部分人没有这种疾病。检测出假阳性的一小部分人比真阳性要多。 这在树形图中更容易可视化: + +![](img/17-2.png) + +真阳性的比例是总体一小部分(0.004)的很大一部分(0.99)。 +假阳性的比例是总体很大一部分(0.996)的一小部分(0.005)。 +这两个比例是可比的;第二个大一点。 + +所以,鉴于随机选择的人检测为阳性,我们将他们划分为,更有可能没有疾病,是正确的。 + +### 主观先验 + +正确并不总令人满意。将阳性患者划分为不患有该疾病似乎仍然有些错误,对于这样的精确检测来说。由于计算是正确的,我们来看看我们的概率计算的基础:随机性假设。 + +我们的假设是,一个随机选择的人进行了检测,并得到了阳性结果。但是这在现实中并没有发生。因为他们认为他们可能有疾病,或者因为他们的医生认为他们可能有疾病,人们去接受检测。被检测的人不是随机选择的总体的成员。 + +这就是为什么,我们对被检测者的直觉与我们得到的答案不太相符。我们正在想象一个病人接受检测的现实情况,因为有一些理由让他们这样做,而计算基于随机选择的人进行检测。 + +所以让我们在更现实的假设下重做我们的计算,即病人正在接受检测,因为医生认为病人有发病的机会。 + +这里需要注意的是,“医生认为有机会”是指医生的意见,而不是总体中的比例。这被称为主观概率。在病人是否患有这种疾病的情况下,这也是主观的先验概率。 + +一些研究人员坚持认为,所有的概率必须是相对的频率,但主观概率导出都是。候选人赢得下一次选举的几率,大地震在下一个十年将会袭击湾区的几率,某个国家赢得下一届足球世界杯的几率:这些都不是基于相对频率或长期的频率。每个都包含主观因素。涉及它们的所有计算也都有主观因素。 + +假设医生的主观意见是,患者有 5% 的几率患病。那么树形图中的先验概率将会改变: + +![](img/17-3.png) + +鉴于病人检测为阳性,他或她有这种疾病的几率是由贝叶斯规则给出。 + +```py +(0.05 * 0.99)/(0.05 * 0.99 + 0.95 * 0.005) +0.9124423963133641 +``` + +改变先验的效果是惊人的。 即使病人患病的医生的先验概率(5%)很低,一旦患者检测出阳性,患病的后验概率高达 91% 以上。 + +如果患者检测出阳性,医生认为患者患病是合理的。 + +### 确认结果 + +虽然医生的意见是主观的,但我们可以产生一个人造总体,5% 的人患有这种疾病,并且使用相同的检测来进行检测。 然后,我们可以计算不同类别的人数,看看这些计数是否与我们使用贝叶斯规则得到的答案一致。 + + +我们可以使用`population(0.05)`和`pivot`构建相应的总体,并看看四个单元格中的计数。 + +```py +population(0.05).pivot('Test Result', 'True Condition') +``` + + +| True Condition | Negative | Positive | +| --- | --- | --- | +| Disease | 50 | 4950 | +| No Disease | 94525 | 475 | + + +在这个人工创建的 10 万人的总体中,有 5000 人(5%)患有这种疾病,其中 99% 的人检测为阳性,导致 4950 人为真阳性。 将其与 475 个假阳性相比:在阳性中,拥有疾病的比例与我们通过贝叶斯规则得到的结果相同。 + +```py +4950/(4950 + 475) +0.9124423963133641 +``` + +因为我们可以一个具有合适比例的总体,我们也可以使用模拟来确认我们的答案是否合理。 `pop_05`表包含 10 万人的总体,使用医生的先验患病概率 5%,以及检测的错误率来生成。 我们从总体中抽取一个规模为 10,000 的简单随机样本,并提取`positive`表,仅包含样本中阳性检测结果的个体。 + +```py +pop_05 = population(0.05) + +sample = pop_05.sample(10000, with_replacement=False) + +positive = sample.where('Test Result', are.equal_to('Positive')) +``` + +在这些阳性结果中,真实比例是多少? 那是拥有这种疾病的阳性的比例: + +```py +positive.where('True Condition', are.equal_to('Disease')).num_rows/positive.num_rows +0.9131205673758865 +``` + +运行这两个单元格几次,你会发现,阳性中真阳性的比例位于我们通过贝叶斯规则计算的值 0.912 周围。 + +你也可以以不同参数调用`population`函数,来改变先验患病概率,并查看后验概率如何受到影响。 diff --git a/docs/data8-textbook-zh/2.md b/docs/data8-textbook-zh/2.md new file mode 100644 index 0000000000000000000000000000000000000000..e1eb5ff863b1c2169a43cfe1944209a94041d573 --- /dev/null +++ b/docs/data8-textbook-zh/2.md @@ -0,0 +1,167 @@ +# 二、因果和实验 + +> 原文:[Causality and Experiments](https://github.com/data-8/textbook/tree/gh-pages/chapters/02) + +> 译者:[飞龙](https://github.com/wizardforcel) + +> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) + +> 自豪地采用[谷歌翻译](https://translate.google.cn/) + +“这些问题已经,而且可能永远留在自然界难以捉摸的秘密之中,它们属于人类智力根本难以接近的一类问题。 - 1849 年 9 月,伦敦时报,霍乱如何传染和传播 + +死刑有威慑作用吗?巧克力对你有好处吗?什么导致乳腺癌? + +所有这些问题试图为结果找到一个原因。仔细检查数据可以帮助揭示这些问题。在本节中,你将学习建立因果关系所涉及的一些基本概念。 + +观察是良好科学的关键。观察研究是一项研究,科学家根据他们所观察到的,但却无法产生的数据作出结论。在数据科学中,许多这样的研究涉及对一组个体的观察,称为实验的利害关系(factor of interest),以及对每个个体的测量结果。 + +将个体视为人是最容易的。在研究巧克力是否对健康有好处时,个体确实是人,实验是吃巧克力,结果可能是血压的测量。但观察研究中的个体不一定是人。在研究死刑是否具有威慑作用时,个体可以为联盟的 50 个州。允许死刑的州的法律是实验,结果可能是州的谋杀率。 + +根本问题是实验是否对结果有影响。实验和结果之间的任何关系被称为关联。如果实验导致结果发生,那么这个关联是因果关系。因果关系是本节开头提出的所有三个问题的核心。例如,问题之一是巧克力是否直接导致健康状况的改善,而不是巧克力与健康之间是否存在关联。 + +因果关系的建立往往分两个阶段进行。首先,观察一个关联。接下来,更仔细的分析决定了因果关系。 + +## John Snow 和 Broad 街水泵 + +### 观察和可视化:John Snow 和 Broad 街水泵 + +精确观察导致建立因果关系的例子之一,最早可以追溯到 150 多年前。为了将你的思维带回正确的时间,试着想象一下 19 世纪 50 年代的伦敦。这是世界上最富裕的城市,但其中许多人却极度贫困。那时,查尔斯·狄更斯(Charles Dickens)在名气鼎盛时,正在写作关于他们的困境的文章。这个城市的贫困地区疾病盛行,霍乱是最可怕的。那个时候还不知道细菌会导致疾病,主流理论是“瘴气”是主要的罪魁祸首。 瘴气表现为恶臭,被认为是由腐烂物质引起的无形的有毒颗粒。伦敦的部分地区气味非常糟糕,特别是在炎热的天气里。为了保护自己免受感染,那些有能力的人把甜的东西放在鼻子上。 + +几年来,一个名叫约翰·斯诺(John Snow)的医生一直在跟踪着时不时袭击英国的巨大霍乱。疾病突然到来,几乎立即致命:人们在一两天内死亡,数百人在一个星期内死亡,单批总死亡人数可能达到数万人。斯诺对瘴气理论持怀疑态度。他注意到,当整个家庭被霍乱摧毁时,邻居有时完全没有受到影响。当他们呼吸和邻居一样的空气和瘴气时,不好的气味和霍乱的发生之间没有什么紧密的联系。 + +斯诺还注意到,这种疾病的发作几乎总是牵涉呕吐和腹泻。因此,他认为这种感染是由人们吃或喝的东西来进行的,而不是他们所呼吸的空气。他主要怀疑被污染的水。 + +1854 年 8 月底,霍乱在过度拥挤的伦敦苏豪区爆发。随着死亡人数的增加,斯诺用一种在疾病传播研究中成为标准的方法,勤奋地将它们记录下来:他画了一张地图。在该地区的街道地图上,他记录了每次死亡的地点。 + +这是斯诺的原始地图。每个黑色条形代表一次死亡。黑色圆圈标记了水泵的位置。地图上显示了一个惊人的启示 - 死亡大致集中在 Broad 街水泵周围。 + +![](img/2-1.jpg) + +斯诺仔细研究了他的地图,并调查了明显的异常。他们都设计 Broad 街水泵。例如: + ++ 死亡发生在离 Rupert 街水泵更近的房子,而不是 Broad 街。尽管 Rupert 街水泵直线上更近,但由于街道布局不方便,是死路一条。那些房子里的居民使用了 Broad 街水泵。 ++ 泵东边的两个街区没有死亡。那是 Lion Brewery 的位置,那里的工人喝了他们酿造的东西。如果他们想喝水,啤酒厂有自己的井。 ++ Broad 街水泵几个街区之外的房子里,发生了少量死亡。那些孩子在上学路上从 Broad 街水泵饮水。泵的水清凉爽口。 + +最后一个支持斯诺的理论的证据是,在距离 Soho 区很远的 Hampstead 地区的两个孤立的死亡事件。斯诺对这些人感到困惑,直到他得知死者是住在 Broad 街的 Susannah Eley 夫人和她的侄女。Eley 夫人每天都将 Broad 街水泵的水带到 Hampstead 给她。她喜欢水的味道。 + +后来发现了一个粪坑,距离 Broad 街水泵几英尺远,渗入了井里面。因此,来自霍乱受害者房子的污水污染了水泵的水。 + +斯诺用他的地图来说服当地政府,拆除 Broad 街水泵的手柄。虽然霍乱疫情已经在减少,但是停止使用这种水泵有可能阻止了许多人死于未来的疾病。 + +Broad 街水泵的手柄的拆除已成为一个传奇。在亚特兰大的疾病控制中心(CDC),当科学家寻找流行病问题的简单答案时,他们有时会互相问:“这个水泵的手柄在哪里?” + +斯诺的地图是数据可视化的最早和最强大的用法之一。现在各种疾病地图是跟踪流行病的标准工具。 + +### 因果关系 + +虽然地图给了斯诺强有力的证据,说明了供水的清洁是控制霍乱的关键,但是,为了使“污染的水导致疾病的传播”这个科学论证有说服力,还有很长一段路要走。为了使案例更有说服力,他必须使用比较法。 + +科学家使用比较来确定实验与结果之间的关联。他们比较了一组接受实验的个体(实验组)的结果,和一组没有接受实验的个体的结果(对照组)。例如,现在的研究人员可能会比较死刑国家和没有死刑的国家的平均谋杀率。 + +如果结果不同,那就是表明关联的证据。但是为了确定因果关系,需要更加小心。 + +## 斯诺的“大实验” + +斯诺为自己在 Soho 中学到的东西感到鼓舞,他对霍乱的死亡情况做了更彻底的分析。一段时间中,他一直在收集伦敦一个地区的数据,这里由两家水厂服务。Lambeth 水厂从污水排入泰晤士河的地方的上游抽水。它的水比较干净。但 Southwark and Vauxhall (S&V) 公司在污水排放的下游抽水,因此其供水受到污染。 + +下图显示了两家公司所服务的地区。斯诺专注于两个服务地区重叠的地方。 + +![](img/2-2.jpg) + +斯诺注意到,S&V 供应的人和 Lambeth 供应的人之间没有系统的差别。 “每家公司都供应富人和穷人,大房子和小房子,接受不同公司的供水的人的状况或职业并没有差别......接受两家公司供水的人或者房子都没什么区别,它们周围的物理状况也没什么区别...” + +唯一的区别是供水方面,“一组供水含有伦敦的污水,其中有一些可能来自霍乱病人,另一组则不含。” + +斯诺相信他能够得出一个清楚的结论,斯诺在下表中总结了他的数据。 + + +| Supply Area | Number of houses | cholera deaths | deaths per 10,000 houses | +| --- | --- | --- | --- | +| S&V | 40,046 | 1,263 | 315 | +| Lambeth | 26,107 | 98 | 37 | +| Rest of London | 256,423 | 1,422 | 59 | + +数字在指责 S&V。 S&V 供应的房屋的霍乱死亡率几乎是 Lambeth 供应的房屋的十倍。 + +## 建立因果 + +用本节前面提出的语言,可以将 S&V 房屋中的人作为实验组,Lambeth 房屋中的人作为对照组。斯诺的分析中的一个关键因素是,除了实验组以外,两组相互比较。 + +为了确定供水是否引起霍乱,斯诺必须比较两个彼此相似的群体,它们只有一方面不同:供水。只有这样,他才能够将其结果的差异归因于供水。如果这两个群体在其他方面有所不同,那么就很难把供水视为疾病的来源。例如,如果实验组由工厂工人组成,而对照组不是,那么两组之间的结果之间的差异可能是由于供水,工厂工作或两者兼有,或使两组彼此不同的其它因素。最后的图景会更加模糊。 + +斯诺的才智在于,确定可以使他的比较清晰的两组。他开始着手建立水污染和霍乱感染之间的因果关系,并且在很大程度上他成功了,尽管瘴气学说忽视甚至嘲笑他。当然,斯诺并不了解人类感染霍乱的详细机制。这个发现是在 1883 年,当时德国科学家罗伯特·科赫(Robert Koch)分离出霍乱弧菌,这种霍乱弧菌是进入人体小肠并引起霍乱的细菌。 + +事实上,霍乱弧菌在 1854 年由意大利的菲利波·帕齐尼(Filippo Pacini)发现,就在斯诺在伦敦分析他的数据的时候。由于意大利瘴气学说的统治,帕齐尼的发现并不为人所知。但到了十九世纪末,瘴气学说正在消失。随后的历史证明了帕齐尼和约翰·斯诺。斯诺的方法导致了流行病学领域的发展,它是疾病传播的研究。 + +### 混淆 + +现在让我们回到更现代化的时代,带着我们一路上学到的重要经验: + +在一项观察研究中,如果实验组和对照组在实验以外的方面有所不同,则很难对因果关系作出结论。 + +两组之间的根本区别(除了实验)被称为混淆因素,因为当你试图得出结论时,它可能会混淆你(也就是搞砸你)。 + +示例:咖啡和肺癌。二十世纪六十年代的研究表明,喝咖啡的人患肺癌的比率高于不喝咖啡的人。因此,有些人认为咖啡是肺癌的一个原因。但咖啡不会导致肺癌。分析包含一个混淆因素 - 吸烟。在那些日子里,喝咖啡的人也可能是吸烟者,吸烟确实会导致肺癌。喝咖啡与肺癌有关,但不会导致疾病。 + +混淆因素在观察研究中很常见。良好的研究需要非常小心,以减少混淆。 + +## 随机化 + +避免混淆的一个很好的方法是,将个体随机分配到实验和对照组,然后将实验给予分配到实验组的人。随机化使两组除了实验之外都相似。 + +如果你能够将个体随机分为实验组和对照组,你正在进行一项随机对照试验(RCT)。有时候,人们在实验中的反应会受到他们知道他们在哪个群体的影响。所以你可能希望进行盲法实验,其中个体不知道他们是在实验组还是对照组。为了使它有效,你必须把安慰剂给控制组,这是一种和实验看起来完全一样的东西,但实际上没有效果。 + +随机对照实验早已成为医学领域的黄金标准,例如确定新药是否有效。在经济学等其他领域也越来越普遍。 + +示例:墨西哥的福利补贴。在 20 世纪 90 年代的墨西哥村庄,贫困家庭的孩子往往没有入学。其中一个原因是年龄较大的孩子可以上班,从而帮助家庭。墨西哥财政部长 Santiago Levy 着手调查福利项目是否可以用来提升入学率和改善健康状况。他在一组村庄进行了一项随机对照试验,随机选择其中的一些来接受一个名为 PROGRESA 的新福利项目。如果他们的孩子定期上学,并且家庭使用了预防性医疗保险,那么这个项目会把钱捐给贫困家庭。如果孩子上中学而不是小学,会给他们更多钱,来补偿孩子的工资损失,女孩上学比男孩给的更多。其余的村庄没有得到这个实验,并形成了对照组。由于随机化,没有销魂因素,可以确定 PROGRESA 增加了入学率。对于男孩,入学率从对照组的 73% 上升到 PROGRESA 组的 77%。对于女孩来说,增长幅度更大,从对照组的 67% 增加到 PROGRESA 组的近75%。由于这个实验的成功,墨西哥政府以 OPORTUNIDADES 这个新名称支持这个项目,作为对一个健康和受过良好教育的人口的投资。 + +在某些情况下,即使目标是调查因果关系,也不可能进行随机对照实验。例如,假设你想研究怀孕期间饮酒的影响,你随机将一些孕妇分配到你的“酒精”组。如果你给他们喝一杯,你不应该期待她们会合作。在这种情况下,你几乎总是在进行观察研究,而不是实验。要警惕混淆因素。 + +## 尾注 + +根据我们开发的术语,约翰·斯诺进行了一项观察研究,而不是一个随机的实验。但是他把自己的研究称为“大实验”,因为他写道:“至少三十万人......被分成两组,他们无法选择,在大多数情况下,他们并不知情......” + +斯诺的这种研究有时被称为“自然实验”。然而,真正的随机化并不仅仅意味着,实验和对照组“在他们无法选择的情况下”进行选择。 + +随机化的方法可以像掷硬币一样简单。它也可能更复杂一点。但是随机化的每一种方法都是由一系列精心定义的步骤组成的,这些步骤允许几率以数学方式指定。这有两个重要的结果。 + ++ 它使我们能够以数学方式,计算随机化产生实验和对照组的可能性。 + ++ 它使我们能够对实验组和对照组之间的差异作出精确的数学表述。这反过来帮助我们对实验是否有效作出正确的结论。 + +在本课程中,你将学习如何进行和分析你自己的随机实验。这将涉及比本节更多的细节。目前,只需关注主要思想:尝试建立因果关系,如果可能,进行随机对照实验。如果你正在进行一项观察研究,你可能能够建立联系而不是因果关系。在根据观察研究得出因果关系的结论之前,要非常小心混淆因素。 + +### 术语 + ++ observational study:观察研究 ++ treatment:实验 ++ outcome:结果 ++ association:关联/联系 ++ causal association:因果联系 ++ causality:因果(关系) ++ comparison:比较 ++ treatment group:实验组 ++ control group:对照组 ++ epidemiology:流行病学/传染病学 ++ confounding:混淆 ++ randomization:随机化 ++ randomized controlled experiment:随机对照实验 ++ randomized controlled trial (RCT):随机对照实验 ++ blind:盲法 ++ placebo:安慰剂 + +### 有趣的事实 + ++ 约翰·斯诺有时被称为流行病学之父,但他是专业的麻醉师。 他的病人之一是维多利亚女王,她是分娩时麻醉剂的早期接受者。 + ++ 弗洛伦斯·南丁格尔,现代护理实践的创始人,因其在克里米亚战争中的工作而闻名,是一位顽固瘴气主义者。 她没有时间研究传染病和细菌的理论,也没有时间讲述她的话。 她说:“与这个学说相关的荒谬是无穷无尽的。一言以蔽之,从一般意义上说,没有任何科学研究可以接受的证据表明,存在传染病这样的事情。” + ++ 后来的随机对照试验表明,PROGRESA 坚持的条件 - 孩子上学,预防性医疗保险 - 对于提升入学率没有必要。 只是提高福利金就足够了。 + +### 扩展阅读 + ++ [The Strange Case of the Broad Street Pump: John Snow and the Mystery of Cholera](http://www.ucpress.edu/book.php?isbn=9780520250499) 由 Sandra Hempel 所著,加利福尼亚大学出版社出版,读起来像是侦探小说。 这是本节中约翰·斯诺和他的工作的主要来源之一。 一些警告:这本书的一些内容令人反胃。 + ++ [Poor Economics](http://www.pooreconomics.com/) 由 MIT 的 Abhijit V. Banerjee 和 Esther Duflo 所著的畅销书,是对抗全球贫困的方式的易理解的真实记录。 它包含了很多 RCT 的例子,包括本节中的 PROGRESA 示例。 diff --git a/docs/data8-textbook-zh/3.md b/docs/data8-textbook-zh/3.md new file mode 100644 index 0000000000000000000000000000000000000000..9ed40e41a91eb7c8ee25ec89f61aee997a762142 --- /dev/null +++ b/docs/data8-textbook-zh/3.md @@ -0,0 +1,443 @@ +# 三、Python 编程 + +> 原文:[Programming in Python](https://github.com/data-8/textbook/tree/gh-pages/chapters/03) + +> 译者:[飞龙](https://github.com/wizardforcel) + +> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) + +> 自豪地采用[谷歌翻译](https://translate.google.cn/) + +编程可以极大地提高我们收集和分析世界信息的能力,而这些信息又可以通过上一节所述的谨慎推理来发现。 在数据科学中,编写程序的目的是,指示计算机执行分析步骤。 电脑无法自行研究世界。 人们必须准确描述计算机应该执行什么步骤来收集和分析数据,这些步骤是通过程序来表达的。 + +## 表达式 + +编程语言比人类语言简单得多。 尽管如此,在任何语言中,还是有一些语法规则需要学习,这里就是我们开始的地方。 在本文中,我们将使用 Python 编程语言。 学习语法规则是必不可少的,最基本的程序中使用的规则也是更复杂程序的核心。 + +程序由表达式组成,向计算机描述了如何组合数据片段。 例如,乘法表达式由两个数字表达式之间的`*`符号组成。表达式,例如`3*4`,由计算机求值。在这种情况下,(IPython 中的)每个单元格中的最后一个表达式的值(求值结果)将显示在单元格下方,这里是 12。 + +```py +3 * 4 +12 +``` + +编程语言的语法规则是僵化的。 在 Python 中,`*`符号不能连续出现两次。 计算机不会试图解释一个与规定的表达式结构不同的表达式。 相反,它会显示`SyntaxError`错误。 语言的语法是其语法规则的集合,`SyntaxError`表示表达式结构不匹配任何语法规则。 + +```py +3 * * 4 + File "", line 1 + 3 * * 4 + ^ +SyntaxError: invalid syntax +``` + +表达式的小改动可以完全改变它的含义。 下面,`*`之间的空格已被删除。 因为`**`出现在两个数字表达式之间,所以表达式是一个格式良好的指数表达式(第一个数字的第二个数字次方,`3*3*3*3`)。 符号`*`和`**`称为运算符,它们组合的值称为操作数。 + +```py +3 ** 4 +81 +``` + +常用操作符。 数据科学通常涉及数值的组合,而编程语言中的一组操作符,是为了使得表达式可以用于表示任何类型的算术。 在Python中,以下操作符是必不可少的。 + + + +| 表达式类型 | 运算符 | 示例 | 值 | +| --- | --- | --- | --- | +| 加法 | `+` | `2 + 3` | 5 | +| 减法 | `-` | `2 - 3` | -1 | +| 乘法 | `*` | `2 * 3` | 6 | +| 除法 | `/` | `7 / 3` | 2.66667 | +| 取余 | `%` | `7 % 3` | 1 | +| 指数 | `**` | `2 ** 0.5` | 1.41421 | + +Python 表达式遵循熟悉的优先级规则,与代数中相同:乘法和除法在加法和减法之前计算。 圆括号可以用来在较大的表达式中,将较小的表达式组合在一起。 + +```py +1 + 2 * 3 * 4 * 5 / 6 ** 3 + 7 + 8 - 9 + 10 +17.555555555555557 +1 + 2 * (3 * 4 * 5 / 6) ** 3 + 7 + 8 - 9 + 10 +2017.0 +``` + +### 示例 + + +这里是一个图表,来自 20 世纪 80 年代初期的“华盛顿邮报”(The Washington Post),试图比较几十年来医生的收入与其他专业人员的收入。 我们是否真的需要在每个条形上看到两个头(一个带有听诊器)? 耶鲁大学教授爱德华·图夫特(Edward Tufte)是世界上量化信息可视化的专家之一,他为这种不必要的修饰创造了“垃圾图表”(chartjunk)一词。 这张图也是 Tufte 痛恨的“数据与油墨比例过低”的一个例子。 + +![](img/3-1.png) + +华盛顿邮报图片 + +最重要的是,图的横轴不是按比例绘制的。 这对条形图的形状有显着的影响。 当按规模绘制并把装饰修剪掉时,图表显示的趋势非常不同于原来明显的线性增长。 下面的优雅图表由统计系统 R 的创始人之一 Ross Ihaka 提供。 + +![](img/3-2.png) + +Ross Ihaka 的图片版本 + +在 1939 年到 1963 年间,医生的收入从 3,262 美元增加到 25,050 美元。 所以在这个时期,每年的平均收入增加了大约 900 美元。 + +```py +(25050 - 3262)/(1963 - 1939) +907.8333333333334 +``` + +在 Ross Ihaka 的图表中可以看到,在这个时期,医生的收入大致呈线性上升,并且保持在一个相对稳定的水平。 正如我们刚刚计算的那样,这个比率大约是 900 美元。 + +但是从 1963 年到 1976 年,这个比例是三倍多: + +```py +(62799 - 25050)/(1976 - 1963) +2903.769230769231 +``` + +这就是 1963 年之后,这个图形急剧上升的原因。 + +本章介绍了许多类型的表达式。 学习编程需要结合学到所有的东西,调查计算机的行为。 如果你连续除两次会发生什么? 你并不需要总是问专家(或互联网);许多这些细节可以通过自己尝试发现。 + +## 数值 + +### 整数值 + +计算机为执行数值计算而设计,但是关于处理数字有一些重要的细节,每个处理定量数据的程序员都应该知道它。 Python(和大多数其他编程语言)区分两种不同类型的数字: + ++ 整数在 Python 语言中称为`int`值。 它们只能表示没有小数部分的整数(负数,零或正数) ++ 实数在 Python 语言中被称为`float `值(或浮点值)。 他们可以表示全部或部分数字,但有一些限制。 + +数值的类型在展示方式上是明显的:`int`值没有小数点,`float`值总是有一个小数点。 + +```py +# Some int values +2 +2 +1 + 3 +4 +-1234567890000000000 +-1234567890000000000 +# Some float values +1.2 +1.2 +1.5 + 2 +3.5 +3 / 1 +3.0 +-12345678900000000000.0 +-1.23456789e+19 +``` + +当一个`float `值和一个`int`值,通过算术运算符组合在一起时,结果总是一个`float`值。 在大多数情况下,两个整数的组合形成另一个整数,但任何数字(`int`或`float`)除以另一个将是一个`float`值。 非常大或非常小的`float`值可以使用科学记数法表示。 + +### 浮点值 + +浮点值非常灵活,但他们有限制。 + +`float`可以表示非常大和非常小的数字。存在限制,但你很少遇到他们。 +浮点数只能表示任何数字的 15 或 16 位有效数字;剩下的精度就会丢失。 这个有限的精度对于绝大多数应用来说已经足够了。 +将浮点值与算术运算结合后,最后的几位数字可能不正确。 第一次遇到时,微小的舍入错误往往令人困惑。 + +第一个限制可以通过两种方式来观察。 如果一个计算的结果是一个非常大的数字,那么它被表示为无限大。 如果结果是非常小的数字,则表示为零。 + +```py +2e306 * 10 +2e+307 +2e306 * 100 +inf +2e-322 / 10 +2e-323 +2e-322 / 100 +0.0 +``` + +第二个限制可以通过涉及超过 15 位有效数字的表达式来观察。 在进行任何算术运算之前,这些额外的数字被丢弃。 + +```py +0.6666666666666666 - 0.6666666666666666123456789 +0.0 +``` + +当两个表达式应该相等时,可以观察到第三个限制。 例如,表达式`2 ** 0.5`计算 2 的平方根,但是该值的平方不会完全恢复成 2。 + +```py +2 ** 0.5 +1.4142135623730951 +(2 ** 0.5) * (2 ** 0.5) +2.0000000000000004 +(2 ** 0.5) * (2 ** 0.5) - 2 +4.440892098500626e-16 +``` + +上面的最终结果是`0.0000000000000004440892098500626`,这个数字非常接近零。 这个算术表达式的正确答案是 0,但是最后的有效数字中的一个小错误,在科学记数法中显得非常不同。 这种行为几乎出现在所有的编程语言中,因为它是在计算机上进行算术运算的标准方式的结果。 + +尽管`float `并不总是精确的,但它们当然是可靠的,并且在所有不同种类的计算机和编程语言中,以相同的方式工作。 + +## 名称 + +名称通过赋值语句在 Python 中得到一个值。 在赋值中,名称后面是`=`,再后面是任何表达式。 `=`右边的表达式的值被赋给名称。 一旦名称有了赋给它的值,在将来的表达式中,值会替换为这个名称。 + +```py +a = 10 +b = 20 +a + b +30 +``` + +之前赋值的名称可以在`=`右边的表达式中使用。 + +```py +quarter = 1/4 +half = 2 * quarter +half +0.5 +``` + +但是,仅仅是表达式的当前值赋给了名称。 如果该值稍后改变,则由该值定义的名称将不会自动更改。 + +```py +quarter = 4 +half +0.5 +``` + +名称必须以字母开头,但可以包含字母和数字。 名称不能包含空格;相反,通常使用下划线字符`_`来替换每个空格。名称只在你编写的时候是有用的;程序员可以选择易于理解的名称。 通常,比起`a`和`b`,你可以创造更有意义的名字。 例如,为了描述加利福尼亚州伯克利 5 美元商品的销售税,以下名称阐明了各种相关数量的含义。 + +```py +purchase_price = 5 +state_tax_rate = 0.075 +county_tax_rate = 0.02 +city_tax_rate = 0 +sales_tax_rate = state_tax_rate + county_tax_rate + city_tax_rate +sales_tax = purchase_price * sales_tax_rate +sales_tax +0.475 +``` + +## 示例:增长率 + +相同数量在不同时间取得的两次测量值之间的关系通常表示为增长率。 例如,美国联邦政府在 2002 年[雇用](http://www.bls.gov/opub/mlr/2013/article/industry-employment-and-output-projections-to-2022-1.htm)了 276.6 万人,在 2012 年雇用了 281.4 万人。为了计算增长率,我们必须首先决定将哪个值作为初始值。 对于随着时间变化的数值,较早的值是一个自然的选择。 然后,我们将变动值和初始值之间的差除以初始值。 + +```py +initial = 2766000 +changed = 2814000 +(changed - initial) / initial +0.01735357917570499 +``` + +通常从两个测量值的比例中减去 1,这产生相同的值。 + +```py +(changed/initial) - 1 +0.017353579175704903 +``` + +这个值是 10 年间的增长率。 增长率的一个实用属性是,即使值以不同的单位表示,它们也不会改变。 所以,例如,我们可以以千人为单位,在 2002 年和 2012 年之间表达同样的关系。 + +```py +initial = 2766 +changed = 2814 +(changed/initial) - 1 +0.017353579175704903 +``` + +10 年以来,美国联邦政府的雇员人数仅增长了 1.74%。 那个时候,美国联邦政府的总支出从 2.37 万亿美元增加到 2012 年的 3.38 万亿美元。 + +```py +initial = 2.37 +changed = 3.38 +(changed/initial) - 1 +0.4261603375527425 +``` + +联邦预算增长 42.6% 远高于联邦雇员增长 1.74%。 实际上,联邦雇员的数量增长速度远远低于美国人口。美国人口同期增长 9.21%,从 2002 年的 2.8760 亿人增加到 2012 年的 3.41 亿。 + +```py +initial = 287.6 +changed = 314.1 +(changed/initial) - 1 +0.09214186369958277 +``` + +增长率可能是负值,表示某种值的下降。 例如,美国的制造业就业岗位从 2002 年 的 1530 万减少到 2012 年的 1190 万,增长率为 -22.2%。 + +```py +initial = 15.3 +changed = 11.9 +(changed/initial) - 1 +-0.2222222222222222 +``` + +年增长率是一年之内的某个数量的增长率。 年增长率为 0.035,累计十年,十年增长率为 0.41(即 41%)。 + +```py +1.035 * 1.035 * 1.035 * 1.035 * 1.035 * 1.035 * 1.035 * 1.035 * 1.035 * 1.035 - 1 +0.410598760621121 +``` + +相同的计算可以使用名称和指数表达。 + +```py +annual_growth_rate = 0.035 +ten_year_growth_rate = (1 + annual_growth_rate) ** 10 - 1 +ten_year_growth_rate +0.410598760621121 +``` + +同样,十年的增长率可以用来计算等价的年增长率。 下面,`t`是两次测量值之间经过的年数。 下面计算过去 10 年联邦支出的年增长率。 + +```py +initial = 2.37 +changed = 3.38 +t = 10 +(changed/initial) ** (1/t) - 1 +0.03613617208346853 +``` + +十年来的总增长率相当于每年增长 3.6%。 + +总之,增长率`g`用来描述`initial `(初始值)和经过一段时间`t`之后的`changed`(变化值)的相对大小。 为了计算`changed`,使用指数来重复应用增长率`g` `t`次。 + +```py +initial * (1 + g) ** t +``` + +为了计算`g`,计算总增长率的`1/t`次方并减一。 + +```py +(changed/initial) ** (1/t) - 1 +``` + +## 调用表达式 + +调用表达式调用函数,这些函数是具名操作。 函数名称首先出现,然后是括号中的表达式。 + +```py +abs(-12) +12 +round(5 - 1.3) +4 +max(2, 2 + 3, 4) +5 +``` + +在这最后一个例子中,`max`函数在三个参数:`2`, `5`和`4`上调用。圆括号内每个表达式的值被传递给函数,函数返回整个调用表达式的最终值。 `max`函数可以接受任意数量的参数并返回最大值。 + +一些函数默认是可用的,比如`abs`和`round`,但是大部分内置于 Python 语言的函数都存储在一个称为模块的函数集合中。 导入语句用于访问模块,如`math `或`operator`。 + +```py +import math +import operator +math.sqrt(operator.add(4, 5)) +3.0 +``` + +可以使用`+`和`**`运算符来表达等价的表达式。 + +```py +(4 + 5) ** 0.5 +3.0 +``` + +运算符和调用表达式可以在表达式中一起使用。 两个值之间的百分比差异用于比较一些值,它们明显既不是`initial `也不是`changed`。 例如,2014 年,佛罗里达农场生产了 27.2 亿个蛋,而爱荷华州农场生产了 162.5 亿个鸡蛋 [1]。 百分比差值是数值之差的绝对值的 100 倍,再除以它们的平均值。 在这种情况下,差值大于平均值,所以百分比差异大于 100。 + +> [1] + +```py +florida = 2.72 +iowa = 16.25 +100*abs(florida-iowa)/((florida+iowa)/2) +142.6462836056932 +``` + +学习不同函数的行为,是学习编程语言的重要组成部分。 Jupyter 笔记本可以帮助你记住不同函数的名称和效果。 编辑代码单元格时,在输入名称的开头之后按 Tab 键,来显示补全该名称的方式列表。 例如,在`math`后按 Tab 键,来查看`math`模块中所有可用函数。 打字将缩小选项列表的范围。 为了了解函数的更多信息,请在它的名称之后放置一个`?`。 例如,输入`math.log`将显示`math`模块中`log`函数的描述。 + +```py +math.log? +log(x[, base]) + +Return the logarithm of x to the given base. +If the base not specified, returns the natural logarithm (base e) of x. +``` + +示例调用中的方括号表示参数是可选的。 也就是说,可以用一个或两个参数来调用`log`。 + +```py +math.log(16, 2) +4.0 +math.log(16)/math.log(2) +4.0 +``` + +[Python 的内建函数](https://docs.python.org/3/library/functions.html)列表非常长,包含了许多在数据科学应用中不需要的函数。 [`math`模块中的数学函数](https://docs.python.org/3/library/math.html)列表同样很长。 本文将在上下文中介绍最重要的函数,而不是期望读者记住或理解这些列表。 + +### 示例 + +1869 年,一位名叫查尔斯·约瑟夫·米纳德(Charles Joseph Minard)的法国土木工程师,创造了一个图表,仍被认为是有史以来最伟大的图表之一。 它显示了拿破仑军队从莫斯科撤退期间的损失。 1812 年,拿破仑开始征服俄罗斯,他的军队中有超过 35 万人。 他们确实到达了莫斯科,但是沿路一直受到损失的困扰。 俄国军队不断撤退到俄罗斯深处,故意焚烧田野,并在撤退时摧毁村庄。 这使法国军队在俄罗斯冬季来临之时,没有食物或避难所。法国军队在莫斯科没有取得决定性的胜利就撤退了。 之后天气变冷,死了更多的人。 回来的人还不到一万。 + +![](img/3-3.png) + +Minard 的地图 + +这个图表绘制在东欧地图上。 它始于左端的波兰-俄罗斯边界。 浅棕色的条形表示拿破仑的军队正在向莫斯科进军,黑色的条形代表军队的撤退。 在图表的每个点上,军队的宽度与军队中士兵的数量成正比。在图表的底部,Minard 包括了回程的温度。 + +注意当军队撤退时,黑色条形变窄。 渡过贝尔齐纳河是个特别的灾难,你能在图表上看到吗? + +由于其简单和有力,这个图标是出色的。 Minard 展示了六个变量: + ++ 士兵的数量 ++ 行军的方向 ++ 位置的经纬度 ++ 回程的温度 ++ 十一月和十二月的具体日期的位置 + +Tufte 说 Minard 的图是“可能是有史以来最好的统计图表”。 + +这里是 Minard 数据的一个子集,取自 Leland Wilkinson 的 The Grammar of Graphics。 + +![](img/3-4.png) + +Minard 的子集 + +每一行表示特定位置的军队状态。 列以度为单位展示经度和纬度,位置的名称,军队是前进还是撤退,以及估计的人数。 + +在这个表格中,连续两个地点之间的人数的最大变化是在莫斯科撤退的时候,也是最大的百分比变化。 + +```py +moscou = 100000 +wixma = 55000 +wixma - moscou +-45000 +(wixma - moscou)/moscou +-0.45 +``` + +在莫斯科的战斗中,人数下降了 45%。 换句话说,进入莫斯科的拿破仑的军队中,有几乎一半的人没有继续前进。 + +正如你在图表中看到的,Moiodexno 非常接近军队出发位置 Kowno。 在前进期间进入 Smolensk 的人中,只有不到 10% 的人在返回的途中到达了 Moiodexno。 + +```py +smolensk_A = 145000 +moiodexno = 12000 +(moiodexno - smolensk_A)/smolensk_A +-0.9172413793103448 +``` + +是的,只要使用没有名称的数字就可以做这些计算。 但是这些名称使得阅读代码和解释结果变得更容易。 + +值得注意的是,更大的绝对变化并不总是对应更大的百分比变化。 + +在前进期间,从 Smolensk 到 Dorogobouge 的绝对损失是 5000 人,而撤退期间,从 Smolensk 到 Orscha 的相应损失是 4000 人。 + +然而,Smolensk 和 Orscha 之间的百分比变化要大得多,因为,在撤退期间,Smolensk 的人员总数要小得多。 + +```py +dorogobouge = 140000 +smolensk_R = 24000 +orscha = 20000 +abs(dorogobouge - smolensk_A) +5000 +abs(dorogobouge - smolensk_A)/smolensk_A +0.034482758620689655 +abs(orscha - smolensk_R) +4000 +abs(orscha - smolensk_R)/smolensk_R +0.16666666666666666 +``` diff --git a/docs/data8-textbook-zh/4.md b/docs/data8-textbook-zh/4.md new file mode 100644 index 0000000000000000000000000000000000000000..edad78f31c41b1ae79cab4c62f01609db1beefa0 --- /dev/null +++ b/docs/data8-textbook-zh/4.md @@ -0,0 +1,369 @@ +# 四、数据类型 + +> 原文:[Data Types](https://github.com/data-8/textbook/tree/gh-pages/chapters/04) + +> 译者:[飞龙](https://github.com/wizardforcel) + +> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) + +> 自豪地采用[谷歌翻译](https://translate.google.cn/) + +每个值都有一个类型,内建的`type`函数返回任何表达式的结果的类型: + +```py +type(3) +int +type(3/1) +float +``` + +表达式的`type`是其最终值的类型。 所以,`type`函数永远不会表明,表达式的类型是一个名称,因为名称总是求值为它们被赋予的值。 + +```py +x = 3 +type(x) # The type of x is an int, not a name +int +``` + +我们已经遇到的另一种类型是内置函数。 Python 表明这个类型是一个`builtin_function_or_method`;函数和方法之间的区别在这个阶段并不重要。 + +```py +type(abs) +builtin_function_or_method +``` + +这一章会探索其他实用的数据类型。 + +## 字符串 + +世界上大部分的数据都是文本,计算机中表示的文本被称为字符串。 字符串可以代表一个单词,一个句子,甚至是图书馆中每本书的内容。 由于文本可以包含数字(如`5`)或布尔值(`True`),字符串也可以描述这些东西。 + +表达式的含义取决于其结构和正在组合的值的类型。 因此,例如,将两个字符串加在一起会产生另一个字符串。 这个表达式仍然是一个加法表达式,但是它组合了一个不同类型的值。 + +```py +"data" + "science" +'datascience' +``` + + +加法完全是字面的;它将这两个字符串组合在一起而不考虑其内容。 它不增加空间,因为这些是不同的词;它取决于程序员(你)来指定。 + +```py +"data" + " " + "science" +'data science' +``` + +单引号和双引号都可以用来创建字符串:`'hi'`和`"hi"`是相同的表达式。 双引号通常是首选,因为它们允许在字符串中包含单引号。 + +```py +"This won't work with a single-quoted string!" +"This won't work with a single-quoted string!" +``` + +为什么不能? 试试看。 + +`str`函数返回任何值的字符串表示形式。 使用此函数,可以构建具有嵌入值的字符串。 + +```py +"That's " + str(1 + 1) + ' ' + str(True) +"That's 2 True" +``` + +## 字符串方法 + +可以使用字符串方法,从现有的字符串中构造相关的字符串,这些方法是操作字符串的函数。 这些方法通过在字符串后面放置一个点,然后调用该函数来调用。 + +例如,以下方法生成一个字符串的大写版本。 + +```py +"loud".upper() +'LOUD' +``` + +也许最重要的方法是`replace`,它替换字符串中的所有子字符串的实例。 `replace`方法有两个参数,即被替换的文本和替代值。 + +```py +'hitchhiker'.replace('hi', 'ma') +'matchmaker' +``` + +字符串方法也可以使用变量名称进行调用,只要这些名称绑定到字符串。 因此,例如,通过首先创建`"ingrain" `然后进行第二次替换,以下两个步骤的过程从`"train"`生成`"degrade"`一词。 + +```py +s = "train" +t = s.replace('t', 'ing') +u = t.replace('in', 'de') +u +'degrade' +``` + + +注意`t = s.replace('t', 'ing')`的一行,不改变字符串`s`,它仍然是`"train"`。 方法调用`s.replace('t', 'ing')`只有一个值,即字符串`"ingrain"`。 + +```py +s +'train' +``` + + +这是我们第一次看到方法,但是方法并不是字符串仅有的。 我们将很快看到,其他类型的对象可以拥有它们。 + +## 比较 + +布尔值通常来自比较运算符。 Python 包含了各种比较值的运算符。 例如,`3 > 1 + 1`。 + +```py +3 > 1 + 1 +True +``` + +值`True`表明这个比较是有效的;Python 已经证实了`3`和`1 + 1`之间关系的这个简单的事实。下面列出了一整套通用的比较运算符。 + + +| 比较 | 运算符 | True 示例 | False 示例 | +| --- | --- | --- | --- | +| 小于 | `<` | `2 < 3` | `2 < 2` | +| 大于 | `>` | `3 > 2` | `3 > 3` | +| 小于等于 | `<=` | `2 <= 2` | `3 <= 2` | +| 大于等于 | `>=` | `3 >= 3` | `2 >= 3` | +| 等于 | `==` | `3 == 3` | `3 == 2` | +| 不等于 | `!=` | `3 != 2` | `2 != 2` | + +一个表达式可以包含多个比较,并且为了使整个表达式为真,它们都必须有效。 例如,我们可以用下面的表达式表示`1 + 1`在`1`和`3`之间。 + +```py +1 < 1 + 1 < 3 +True +``` + +两个数字的平均值总是在较小的数字和较大的数字之间。 我们用下面的数字`x`和`y`来表示这种关系。 你可以尝试不同的`x`和`y`值来确认这种关系。 + +```py +x = 12 +y = 5 +min(x, y) <= (x+y)/2 <= max(x, y) +True +``` + +字符串也可以比较,他们的顺序是字典序。 较短的字符串小于以较短的字符串开头的较长的字符串。 + +```py +"Dog" > "Catastrophe" > "Cat" +True +``` + +## 序列 + +值可以分组到集合中,这允许程序员组织这些值,并使用单个名称引用它们中的所有值。 通过将值分组在一起,我们可以编写代码,一次执行许多数据计算。 + + +在几个值上调用`make_array`函数,将它们放到一个数组中,这是一种顺序集合。 下面,我们将四个不同的温度收集到一个名为`temps`的数组中。 这些分别是 1850 年,1900 年,1950 年和 2000 年的几十年间,地球上所有陆地的估计日平均绝对高温(摄氏度),表示为 1951 年至 1980 年间平均绝对高温的偏差,为 14.48 度。 + +集合允许我们使用单个名称,将多个值传递给一个函数。 例如,`sum`函数计算集合中所有值的和,`len`函数计算其长度。 (这是我们放入的值的数量。)一起使用它们,我们可以计算一个集合的平均值。 + +```py +sum(highs)/len(highs) +14.434000000000001 +``` + +日高温和低温的完整图表在下面。 + +### 日高温均值 + +![](img/4-1.png) + +### 日低温均值 + +![](img/4-2.png) + +## 数组 + + +Python 中有很多种类的集合,我们在这门课中主要使用数组。 我们已经看到,`make_array`函数可以用来创建数值的数组。 + +数组也可以包含字符串或其他类型的值,但是单个数组只能包含单一类型的数据。 (无论如何,把不相似的数据组合在一起,通常都没有意义)。例如: + +```py +english_parts_of_speech = make_array("noun", "pronoun", "verb", "adverb", "adjective", "conjunction", "preposition", "interjection") +english_parts_of_speech +array(['noun', 'pronoun', 'verb', 'adverb', 'adjective', 'conjunction', + 'preposition', 'interjection'], + dtype=' 译者注: + +> ```py +> import numpy as np +> make_array = lambda *args: np.asarray(args) +> ``` + +返回到温度数据,我们创建 1850 年,1900 年,1950 年和 2000 年的几十年间,[日平均高温](http://berkeleyearth.lbl.gov/auto/Regional/TMAX/Text/global-land-TMAX-Trend.txt)的数组。 + +```py +baseline_high = 14.48 +highs = make_array(baseline_high - 0.880, + baseline_high - 0.093, + baseline_high + 0.105, + baseline_high + 0.684) +highs +array([ 13.6 , 14.387, 14.585, 15.164]) +``` + +数组可以用在算术表达式中来计算其内容。 当数组与单个数组合时,该数与数组的每个元素组合。 因此,我们可以通过编写熟悉的转换公式,将所有这些温度转换成华氏温度。 + +```py +(9/5) * highs + 32 +array([ 56.48 , 57.8966, 58.253 , 59.2952]) +``` + +![](img/4-3.png) + +数组也有方法,这些方法是操作数组值的函数。 数值集合的均值是其总和除以长度。 以下示例中的每对括号都是调用表达式的一部分;它调用一个无参函数来对数组`highs`进行计算。 + +```py +highs.size +4 +highs.sum() +57.736000000000004 +highs.mean() +14.434000000000001 +``` + +### 数组上的函数 + +`numpy`包,在程序中缩写为`np`,为 Python 程序员提供了创建和操作数组的,方便而强大的函数。 + +```py +import numpy as np +``` + +例如,`diff`函数计算数组中每两个相邻元素之间的差。 差数组的第一个元素是原数组的第二个元素减去第一个元素。 + +```py +np.diff(highs) +array([ 0.787, 0.198, 0.579]) +``` + +[完整的 Numpy 参考](http://docs.scipy.org/doc/numpy/reference/)详细列出了这些功能,但一个小的子集通常用于数据处理应用。 它们分组到了`np`中不同的包中。 学习这些词汇是学习 Python 语言的重要组成部分,因此在你处理示例和问题时,请经常回顾这个列表。 + +但是,你不需要记住这些,只需要将它用作参考。 + +每个这些函数接受数组作为参数,并返回单个值。 + + +| 函数 | 描述 | +| --- | --- | +| `np.prod` | 将所有元素相乘 | +| `np.sum` | 将所有元素相加 | +| `np.all` | 测试是否所有元素是真值 (非零数值是真值) | +| `np.any` | 测试是否任意元素是真值(非零数值是真值) | +| `np.count_nonzero` | 计算非零元素的数量 | + +每个这些函数接受字符串数组作为参数,并返回数组。 + + +| 函数 | 描述 | +| --- | --- | +| `np.char.lower` | 将每个元素变成小写 | +| `np.char.upper` | 将每个元素变成大写 | +| `np.char.strip` | 移除每个元素开头或末尾的空格 | +| `np.char.isalpha` | 每个元素是否只含有字母(没有数字或者符号) | +| `np.char.isnumeric` | 每个元素是否只含有数字(没有字母) | + +每个这些函数接受字符串数组和一个搜索字符串。 + + +| 函数 | 描述 | +| --- | --- | +| np.char.count | 在数组的元素中,计算搜索字符串的出现次数 | +| np.char.find | 在每个元素中,搜索字符串的首次出现位置 | +| np.char.rfind | 在每个元素中,搜索字符串的最后一次出现位置 | +| np.char.startswith | 每个字符串是否以搜索字符串起始 | + +## 范围 + +范围是一个数组,按照递增或递减的顺序排列,每个元素按照一定的间隔分开。 范围在很多情况下非常有用,所以值得了解它们。 + +范围使用`np.arange`函数来定义,该函数接受一个,两个或三个参数:起始值,终止值和“步长”。 + +如果将一个参数传递给`np.arange`,那么它将成为终止值,其中`start = 0`,`step = 1`。 两个参数提供了起始值和终止值,`step = 1`。 三个参数明确地提供了起始值,终止值和步长。 + +范围始终包含其`start`值,但不包括其`end`值。 它按照`step`计数,并在到达`end`之前停止。 + +```py +np.arange(end): An array starting with 0 of increasing consecutive integers, stopping before end. +np.arange(5) +array([0, 1, 2, 3, 4]) +``` + +要注意,数值从`0`起始,并仅仅增加到`4`,并不是`5`。 + +```py +np.arange(start, end): An array of consecutive increasing integers from start, stopping before end. +np.arange(3, 9) +array([3, 4, 5, 6, 7, 8]) +np.arange(start, end, step): A range with a difference of step between each pair of consecutive values, starting from start and stopping before end. +np.arange(3, 30, 5) +array([ 3, 8, 13, 18, 23, 28]) +``` + +这个数组从`3`起始,增加了步长`5`变成`8`,然后增加步长`5`变成`13`,以此类推。 + +当你指定步长时,起始值、终止值和步长可正可负,可以是整数也可以是分数。 + +```py +np.arange(1.5, -2, -0.5) +array([ 1.5, 1. , 0.5, 0. , -0.5, -1. , -1.5]) +``` + +### 示例:莱布尼茨的 π 公式 + +伟大的德国数学家和哲学家戈特弗里德·威廉·莱布尼茨(Gottfried Wilhelm Leibniz,1646 ~ 1716年)发现了一个简单分数的无穷和。 公式是: + +![](img/tex-4-1.gif) + +虽然需要一些数学来确定它,但我们可以用数组来说服我们自己,公式是有效的。 让我们计算莱布尼茨的无穷和的前 5000 个项,看它是否接近 π。 + +我们将计算这个有限的总和,首先加上所有的正项,然后减去所有负项的和 [1]: + +![](img/tex-4-2.gif) + +> [1] 令人惊讶的是,当我们将无限多个分数相加时,顺序可能很重要。但是我们对 π 的近似只使用了大量的数量有限的分数,所以可以按照任何方便的顺序,将这些项相加。 + +和中的正项的分母是`1, 5, 9`,以此类推。数组`by_four_to_20`包含`17`之前的这些数。 + +```py +by_four_to_20 = np.arange(1, 20, 4) +by_four_to_20 +array([ 1, 5, 9, 13, 17]) +``` + +为了获得 π 的准确近似,我们使用更长的数组`positive_term_denominators`。 + +```py +positive_term_denominators = np.arange(1, 10000, 4) +positive_term_denominators +array([ 1, 5, 9, ..., 9989, 9993, 9997]) +``` + +我们实际打算加起来的正项,就是一除以这些分母。 + +```py +positive_terms = 1 / positive_term_denominators +``` + +负向的分母是`3, 7, 11`,以此类推。这个数组就是`positive_term_denominators`加二。 + +```py +negative_terms = 1 / (positive_term_denominators + 2) +``` + +整体的和是: + +```py +4 * ( sum(positive_terms) - sum(negative_terms) ) +3.1413926535917955 +``` + +这非常接近于`π = 3.14159...`。莱布尼茨公式看起来不错。 diff --git a/docs/data8-textbook-zh/5.md b/docs/data8-textbook-zh/5.md new file mode 100644 index 0000000000000000000000000000000000000000..5be50eba9c9b2d229d4bce30bb4846d5abe1f0ea --- /dev/null +++ b/docs/data8-textbook-zh/5.md @@ -0,0 +1,1341 @@ +# 五、表格 + +> 原文:[Tables](https://github.com/data-8/textbook/tree/gh-pages/chapters/05) + +> 译者:[飞龙](https://github.com/wizardforcel) + +> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) + +> 自豪地采用[谷歌翻译](https://translate.google.cn/) + + +表格是表示数据集的基本对象类型。 表格可以用两种方式查看: + ++ 具名列的序列,每列都描述数据集中所有条目的一个方面,或者 ++ 行的序列,每行都包含数据集中单个条目的所有信息。 + +为了使用表格,导入所有称为`datascience`的模块,这是为这篇文章创建的模块。 + +```py +from datascience import * +``` + +空表格可以使用`Table`创建。空表格是实用的,因为他可以扩展来包含新行和新列。 + +```py +Table() +``` + +表格上的`with_columns`方法使用带有附加标签的列,构造一个新表。 表格的每一列都是一个数组。 为了将一个新列添加到表中,请使用标签和数组调用`with_columns`。 (`with_column`方法具有相同的效果。) + +下面,我们用一个没有列的空表开始每个例子。 + +```py +Table().with_columns('Number of petals', make_array(8, 34, 5)) +``` + +| Number of petals | +| --- | +| 8 | +| 34 | +| 5 | + +为了添加两个(或更多)新列,请为每列提供一个数组和标签。 所有列必须具有相同的长度,否则会发生错误。 + +```py +Table().with_columns( + 'Number of petals', make_array(8, 34, 5), + 'Name', make_array('lotus', 'sunflower', 'rose') +) +``` + + +| Number of petals | Name | +| --- | --- | +| 8 | lotus | +| 34 | sunflower | +| 5 | rose | + +我们可以给这个表格一个名词,之后使用另外一列扩展表格。 + +```py +flowers = Table().with_columns( + 'Number of petals', make_array(8, 34, 5), + 'Name', make_array('lotus', 'sunflower', 'rose') +) + +flowers.with_columns( + 'Color', make_array('pink', 'yellow', 'red') +) +``` + + +| Number of petals | Name | Color | +| --- | --- | --- | +| 8 | lotus | pink | +| 34 | sunflower | yellow | +| 5 | rose | red | + +`with_columns`方法每次调用时,都会创建一个新表,所以原始表不受影响。 例如,表`an_example`仍然只有它创建时的两列。 + +``` +flowers +``` + +| Number of petals | Name | +| --- | --- | +| 8 | lotus | +| 34 | sunflower | +| 5 | rose | + +通过这种方式创建表涉及大量的输入。 如果数据已经输入到某个地方,通常可以使用 Python 将其读入表格中,而不是逐个单元格地输入。 + +通常,表格从包含逗号分隔值的文件创建。这些文件被称为 CSV 文件。 + +下面,我们使用`Table`的`read_table`方法,来读取一个 CSV 文件,它包含了一些数据,Minard 在他的拿破仑的俄罗斯战役的图片中使用。 数据放在名为`minard`的表中。 + +``` +minard = Table.read_table('minard.csv') +minard +``` + +| Longitude | Latitude | City | Direction | Survivors | +| --- | --- | --- | --- | --- | +| 32 | 54.8 | Smolensk | Advance | 145000 | +| 33.2 | 54.9 | Dorogobouge | Advance | 140000 | +| 34.4 | 55.5 | Chjat | Advance | 127100 | +| 37.6 | 55.8 | Moscou | Advance | 100000 | +| 34.3 | 55.2 | Wixma | Retreat | 55000 | +| 32 | 54.6 | Smolensk | Retreat | 24000 | +| 30.4 | 54.4 | Orscha | Retreat | 20000 | +| 26.8 | 54.3 | Moiodexno | Retreat | 12000 | + +我们将使用这个小的表格来演示一些有用的表格方法。 然后,我们将使用这些相同的方法,并在更大的数据表上开发其他方法。 + +### 表格的大小 + +`num_columns `方法提供了表中的列数量,`num_rows`是行数量。 + +```py +minard.num_columns +5 +minard.num_rows +8 +``` + +### 列标签 + +`labels `方法可以用来列出所有列的标签。 对于`minard`,并不是特别有用,但是对于那些非常大的表格,并不是所有的列都在屏幕上可见。 + +```py +minard.labels +('Longitude', 'Latitude', 'City', 'Direction', 'Survivors') +``` + +我们使用`relabeled `修改列标签。这会创建新的表格,并保留`minard`不变。 + +``` +minard.relabeled('City', 'City Name') +``` + +| Longitude | Latitude | City Name | Direction | Survivors | +| --- | --- | --- | --- | --- | +| 32 | 54.8 | Smolensk | Advance | 145000 | +| 33.2 | 54.9 | Dorogobouge | Advance | 140000 | +| 34.4 | 55.5 | Chjat | Advance | 127100 | +| 37.6 | 55.8 | Moscou | Advance | 100000 | +| 34.3 | 55.2 | Wixma | Retreat | 55000 | +| 32 | 54.6 | Smolensk | Retreat | 24000 | +| 30.4 | 54.4 | Orscha | Retreat | 20000 | +| 26.8 | 54.3 | Moiodexno | Retreat | 12000 | + +但是,这个方法并不修改原始表。 + +``` +minard +``` + +| Longitude | Latitude | City | Direction | Survivors | +| --- | --- | --- | --- | --- | +| 32 | 54.8 | Smolensk | Advance | 145000 | +| 33.2 | 54.9 | Dorogobouge | Advance | 140000 | +| 34.4 | 55.5 | Chjat | Advance | 127100 | +| 37.6 | 55.8 | Moscou | Advance | 100000 | +| 34.3 | 55.2 | Wixma | Retreat | 55000 | +| 32 | 54.6 | Smolensk | Retreat | 24000 | +| 30.4 | 54.4 | Orscha | Retreat | 20000 | +| 26.8 | 54.3 | Moiodexno | Retreat | 12000 | + +常见的模式时将原始名称`minard`赋给新的表,以便`minard `未来的所有使用,都会引用修改标签的表格。 + +```py +minard = minard.relabeled('City', 'City Name') +minard +``` + +| Longitude | Latitude | City Name | Direction | Survivors | +| --- | --- | --- | --- | --- | +| 32 | 54.8 | Smolensk | Advance | 145000 | +| 33.2 | 54.9 | Dorogobouge | Advance | 140000 | +| 34.4 | 55.5 | Chjat | Advance | 127100 | +| 37.6 | 55.8 | Moscou | Advance | 100000 | +| 34.3 | 55.2 | Wixma | Retreat | 55000 | +| 32 | 54.6 | Smolensk | Retreat | 24000 | +| 30.4 | 54.4 | Orscha | Retreat | 20000 | +| 26.8 | 54.3 | Moiodexno | Retreat | 12000 | + +### 访问列中的数据 + +我们可以使用列标签来访问列中的数据数组。 + +```py +minard.column('Survivors') +array([145000, 140000, 127100, 100000, 55000, 24000, 20000, 12000]) +``` + +五列的下标分别为`0, 1, 2, 3, 4`。`Survivors`列也可以使用列下标来访问。 + +```py +minard.column(4) +array([145000, 140000, 127100, 100000, 55000, 24000, 20000, 12000]) +``` + +数组中的八个条目下标为`0, 1, 2, ..., 7`。列中的条目可以使用`item`访问,就像任何数组那样。 + +```py +minard.column(4).item(0) +145000 +minard.column(4).item(5) +24000 +``` + +### 处理列中的数据 + +因为列是数组,所以我们可以使用数组操作来探索新的信息。 例如,我们可以创建一个新列,其中包含 Smolensk 之后每个城市的所有幸存者的百分比。 + +```py +initial = minard.column('Survivors').item(0) +minard = minard.with_columns( + 'Percent Surviving', minard.column('Survivors')/initial +) +minard +``` + + +| Longitude | Latitude | City Name | Direction | Survivors | Percent Surviving | +| --- | --- | --- | --- | --- | --- | +| 32 | 54.8 | Smolensk | Advance | 145000 | 100.00% | +| 33.2 | 54.9 | Dorogobouge | Advance | 140000 | 96.55% | +| 34.4 | 55.5 | Chjat | Advance | 127100 | 87.66% | +| 37.6 | 55.8 | Moscou | Advance | 100000 | 68.97% | +| 34.3 | 55.2 | Wixma | Retreat | 55000 | 37.93% | +| 32 | 54.6 | Smolensk | Retreat | 24000 | 16.55% | +| 30.4 | 54.4 | Orscha | Retreat | 20000 | 13.79% | +| 26.8 | 54.3 | Moiodexno | Retreat | 12000 | 8.28% | + +要使新列中的比例显示为百分比,我们可以使用选项`PercentFormatter`调用`set_format`方法。 `set_format`方法接受`Formatter `对象,存在日期(`DateFormatter`),货币(`CurrencyFormatter`),数字和百分比。 + +```py +minard.set_format('Percent Surviving', PercentFormatter) +``` + +| Longitude | Latitude | City Name | Direction | Survivors | Percent Surviving | +| --- | --- | --- | --- | --- | --- | +| 32 | 54.8 | Smolensk | Advance | 145000 | 100.00% | +| 33.2 | 54.9 | Dorogobouge | Advance | 140000 | 96.55% | +| 34.4 | 55.5 | Chjat | Advance | 127100 | 87.66% | +| 37.6 | 55.8 | Moscou | Advance | 100000 | 68.97% | +| 34.3 | 55.2 | Wixma | Retreat | 55000 | 37.93% | +| 32 | 54.6 | Smolensk | Retreat | 24000 | 16.55% | +| 30.4 | 54.4 | Orscha | Retreat | 20000 | 13.79% | +| 26.8 | 54.3 | Moiodexno | Retreat | 12000 | 8.28% | + +### 选择列的集合 + +`select `方法创建一个新表,仅仅包含指定的列。 + +```py +minard.select('Longitude', 'Latitude') +``` + + +| Longitude | Latitude | +| --- | --- | +| 32 | 54.8 | +| 33.2 | 54.9 | +| 34.4 | 55.5 | +| 37.6 | 55.8 | +| 34.3 | 55.2 | +| 32 | 54.6 | +| 30.4 | 54.4 | +| 26.8 | 54.3 | + +使用列索引而不是标签,也可以执行相同选择。 + +```py +minard.select(0, 1) +``` + + +| Longitude | Latitude | +| --- | --- | +| 32 | 54.8 | +| 33.2 | 54.9 | +| 34.4 | 55.5 | +| 37.6 | 55.8 | +| 34.3 | 55.2 | +| 32 | 54.6 | +| 30.4 | 54.4 | +| 26.8 | 54.3 | + +`select `的结果是个新表,即使当你选择一列时也是这样。 + +``` +minard.select('Survivors') +``` + +| Survivors | +| --- | +| 145000 | +| 140000 | +| 127100 | +| 100000 | +| 55000 | +| 24000 | +| 20000 | +| 12000 | + +要注意结果是个表格,不像`column`的结果,它是个数组。 + +```py +minard.column('Survivors') +array([145000, 140000, 127100, 100000, 55000, 24000, 20000, 12000]) +``` + +另一种创建新表,包含列集合的方式,是`drop`你不想要的列。 + +```py +minard.drop('Longitude', 'Latitude', 'Direction') +``` + + +| City Name | Survivors | Percent Surviving | +| --- | --- | --- | +| Smolensk | 145000 | 100.00% | +| Dorogobouge | 140000 | 96.55% | +| Chjat | 127100 | 87.66% | +| Moscou | 100000 | 68.97% | +| Wixma | 55000 | 37.93% | +| Smolensk | 24000 | 16.55% | +| Orscha | 20000 | 13.79% | +| Moiodexno | 12000 | 8.28% | + +`select `和`drop`都不修改原始表格。 相反,他们创建了共享相同数据的新小型表格。 保留的原始表格是实用的! 你可以生成多个不同的表格,只考虑某些列,而不用担心会互相影响。 + +``` +minard +``` + +| Longitude | Latitude | City Name | Direction | Survivors | Percent Surviving | +| --- | --- | --- | --- | --- | --- | +| 32 | 54.8 | Smolensk | Advance | 145000 | 100.00% | +| 33.2 | 54.9 | Dorogobouge | Advance | 140000 | 96.55% | +| 34.4 | 55.5 | Chjat | Advance | 127100 | 87.66% | +| 37.6 | 55.8 | Moscou | Advance | 100000 | 68.97% | +| 34.3 | 55.2 | Wixma | Retreat | 55000 | 37.93% | +| 32 | 54.6 | Smolensk | Retreat | 24000 | 16.55% | +| 30.4 | 54.4 | Orscha | Retreat | 20000 | 13.79% | +| 26.8 | 54.3 | Moiodexno | Retreat | 12000 | 8.28% | + +我们用过的所有方法都可以用在任何表格上。 + +## 对行排序 + +CNN 在 2016 年 3 月报道说:“NBA 是全球薪水最高的职业体育联盟。”`nba_salaries`包含了 2015~2016 年间所有 NBA 球员的薪水。 + +每行表示一个球员。列为: + + +| 列标签 | 描述 | +| --- | --- | +| `PLAYER` | 球员名称 | +| `POSITION `| 球员在队里的位置 | +| `TEAM` | 队的明确 | +| `'15-'16 SALARY` | 2015~2016 年的球员薪水,单位是百万美元。 | + +位置代码是 PG(控球后卫),SG(得分后卫),PF(大前锋),SF(小前锋)和 C(中锋)。 但接下来的内容并不涉及篮球运动的细节。 + +第一行显示,亚特兰大老鹰队(Atlanta Hawks)的大前锋保罗·米尔萨普(Paul Millsap)在 2015~2016 年间的薪水接近 1870 万美元。 + +```py +# This table can be found online: https://www.statcrunch.com/app/index.php?dataid=1843341 +nba_salaries = Table.read_table('nba_salaries.csv') +nba_salaries +``` + +| `PLAYER` | `POSITION` | `TEAM` | `'15-'16 SALARY` | +| --- | --- | --- | --- | +| Paul Millsap | PF | Atlanta Hawks | 18.6717 | +| Al Horford | C | Atlanta Hawks | 12 | +| Tiago Splitter | C | Atlanta Hawks | 9.75625 | +| Jeff Teague | PG | Atlanta Hawks | 8 | +| Kyle Korver | SG | Atlanta Hawks | 5.74648 | +| Thabo Sefolosha | SF | Atlanta Hawks | 4 | +| Mike Scott | PF | Atlanta Hawks | 3.33333 | +| Kent Bazemore | SF | Atlanta Hawks | 2 | +| Dennis Schroder | PG | Atlanta Hawks | 1.7634 | +| Tim Hardaway Jr. | SG | Atlanta Hawks | 1.30452 | + +(省略了 407 行) + + +该表包含 417 行,每个球员一行。 只显示了 10 行。`show`方法允许我们指定行数,缺省值(没有指定)是表的所有行。 + +```py +nba_salaries.show(3) +``` + + +| `PLAYER` | `POSITION` | `TEAM` | `'15-'16 SALARY` | +| --- | --- | --- | --- | +| Paul Millsap | PF | Atlanta Hawks | 18.6717 | +| Al Horford | C | Atlanta Hawks | 12 | +| Tiago Splitter | C | Atlanta Hawks | 9.75625 | + +(省略了 414 行) + +通过浏览大约 20 行左右,你会看到行按字母顺序排列。 也可以使用`sort`方法,按球员姓名的字母顺序列出相同的行。 `sort`的参数是列标签或索引。 + +```py +nba_salaries.sort('PLAYER').show(5) +``` + +| `PLAYER` | `POSITION` | `TEAM` | `'15-'16 SALARY` | +| --- | --- | --- | --- | +| Aaron Brooks | PG | Chicago Bulls | 2.25 | +| Aaron Gordon | PF | Orlando Magic | 4.17168 | +| Aaron Harrison | SG | Charlotte Hornets | 0.525093 | +| Adreian Payne | PF | Minnesota Timberwolves | 1.93884 | +| Al Horford | C | Atlanta Hawks | 12 | + +(省略了 412 行) + + +440/5000 +为了检查球员的薪水,如果数据是按薪水排序的话,会更有帮助。 + +为了实现它,我们首先简化薪水列的标签(只是为了方便),然后用新的标签`SALARY`进行排序。 + +这会按照薪水的升序排列表中的所有行,最低的薪水在最前面。 输出是一个新表,列与原始表格相同,但行是重新排列的。 + +```py +nba = nba_salaries.relabeled("'15-'16 SALARY", 'SALARY') +nba.sort('SALARY') +``` + +| `PLAYER` | `POSITION` | `TEAM` | `'15-'16 SALARY` | +| --- | --- | --- | --- | +| Thanasis Antetokounmpo | SF | New York Knicks | 0.030888 | +| Jordan McRae | SG | Phoenix Suns | 0.049709 | +| Cory Jefferson | PF | Phoenix Suns | 0.049709 | +| Elliot Williams | SG | Memphis Grizzlies | 0.055722 | +| Orlando Johnson | SG | Phoenix Suns | 0.055722 | +| Phil Pressey | PG | Phoenix Suns | 0.055722 | +| Keith Appling | PG | Orlando Magic | 0.061776 | +| Sean Kilpatrick | SG | Denver Nuggets | 0.099418 | +| Erick Green | PG | Utah Jazz | 0.099418 | +| Jeff Ayres | PF | Los Angeles Clippers | 0.111444 | + +(省略了 407 行) + +这些数字有些难以比较,因为这些球员中的一些,在赛季中改变了球队,并从不止一支球队获得了薪水。 只有最后一支球队的薪水出现在表中。 控球后卫菲尔·普莱西(Phil Pressey)在年内从费城搬到了凤凰城,可能会再次转到金州勇士队(Golden State Warriors)。 + +CNN 的报道是薪酬水平的另一端 - 那些在世界上薪水最高的球员。 + +为了按照薪水的降序对表格的行排序,我们必须以`descending=True`调用`sort`函数。 + +```py +nba.sort('SALARY', descending=True) +``` + +| `PLAYER` | `POSITION` | `TEAM` | `'15-'16 SALARY` | +| --- | --- | --- | --- | +| Kobe Bryant | SF | Los Angeles Lakers | 25 | +| Joe Johnson | SF | Brooklyn Nets | 24.8949 | +| LeBron James | SF | Cleveland Cavaliers | 22.9705 | +| Carmelo Anthony | SF | New York Knicks | 22.875 | +| Dwight Howard | C | Houston Rockets | 22.3594 | +| Chris Bosh | PF | Miami Heat | 22.1927 | +| Chris Paul | PG | Los Angeles Clippers | 21.4687 | +| Kevin Durant | SF | Oklahoma City Thunder | 20.1586 | +| Derrick Rose | PG | Chicago Bulls | 20.0931 | +| Dwyane Wade | SG | Miami Heat | 20 | + +(省略了 407 行) + +科比(Kobe Bryant)在湖人队(Lakers)的最后一个赛季是薪水最高的,2500 万美元。 请注意,MVP 斯蒂芬·库里(Stephen Curry)并没有出现在前 10 名之列。他排在后面,我们将在后面看到。 + +### 具名参数 + +这个调用表达式的`descending = True`部分称为具名参数。 调用一个函数或方法时,每个参数都有一个位置和一个名字。 从函数或方法的帮助文本中都可以看出它们。 + +```py +help(nba.sort) +Help on method sort in module datascience.tables: + +sort(column_or_label, descending=False, distinct=False) method of datascience.tables.Table instance + Return a Table of rows sorted according to the values in a column. + + Args: + ``column_or_label``: the column whose values are used for sorting. + + ``descending``: if True, sorting will be in descending, rather than + ascending order. + + ``distinct``: if True, repeated values in ``column_or_label`` will + be omitted. + + Returns: + An instance of ``Table`` containing rows sorted based on the values + in ``column_or_label``. + + >>> marbles = Table().with_columns( + ... "Color", make_array("Red", "Green", "Blue", "Red", "Green", "Green"), + ... "Shape", make_array("Round", "Rectangular", "Rectangular", "Round", "Rectangular", "Round"), + ... "Amount", make_array(4, 6, 12, 7, 9, 2), + ... "Price", make_array(1.30, 1.30, 2.00, 1.75, 1.40, 1.00)) + >>> marbles + Color | Shape | Amount | Price + Red | Round | 4 | 1.3 + Green | Rectangular | 6 | 1.3 + Blue | Rectangular | 12 | 2 + Red | Round | 7 | 1.75 + Green | Rectangular | 9 | 1.4 + Green | Round | 2 | 1 + >>> marbles.sort("Amount") + Color | Shape | Amount | Price + Green | Round | 2 | 1 + Red | Round | 4 | 1.3 + Green | Rectangular | 6 | 1.3 + Red | Round | 7 | 1.75 + Green | Rectangular | 9 | 1.4 + Blue | Rectangular | 12 | 2 + >>> marbles.sort("Amount", descending = True) + Color | Shape | Amount | Price + Blue | Rectangular | 12 | 2 + Green | Rectangular | 9 | 1.4 + Red | Round | 7 | 1.75 + Green | Rectangular | 6 | 1.3 + Red | Round | 4 | 1.3 + Green | Round | 2 | 1 + >>> marbles.sort(3) # the Price column + Color | Shape | Amount | Price + Green | Round | 2 | 1 + Red | Round | 4 | 1.3 + Green | Rectangular | 6 | 1.3 + Green | Rectangular | 9 | 1.4 + Red | Round | 7 | 1.75 + Blue | Rectangular | 12 | 2 + >>> marbles.sort(3, distinct = True) + Color | Shape | Amount | Price + Green | Round | 2 | 1 + Red | Round | 4 | 1.3 + Green | Rectangular | 9 | 1.4 + Red | Round | 7 | 1.75 + Blue | Rectangular | 12 | 2 +``` + +在`help`文本的最上面,出现了`sort`方法的签名。 + +```py +sort(column_or_label, descending=False, distinct=False) +``` + +这描述了`sort`的三个参数的位置,名称和默认值。 调用此方法时,可以使用位置参数或具名参数,因此以下三个调用完全相同。 + +```py +sort('SALARY', True) +sort('SALARY', descending=True) +sort(column_or_label='SALARY', descending=True) +``` + +当一个参数只是`True`或`False`时,包含参数名称是实用的约定,以便更明显地说明参数值的含义。 + +## 行的选取 + +通常,我们只想提取那些行,它们对应具有特定特征的条目。 例如,我们可能只需要对应勇士的行,或者获得超过一千万美元的球员。 或者我们可能只想要薪水前五名的人。 + +### 指定行 + + +`Table`的方法就是干这个的 - 它需要一组指定的行。 它的参数是行索引或索引数组,它创建一个只包含这些行的新表。 + +例如,如果我们只想要`nba`的第一行,我们可以这样使用`take`。 + +```py +nba +``` + +| `PLAYER` | `POSITION` | `TEAM` | `'15-'16 SALARY` | +| --- | --- | --- | --- | +| Paul Millsap | PF | Atlanta Hawks | 18.6717 | +| Al Horford | C | Atlanta Hawks | 12 | +| Tiago Splitter | C | Atlanta Hawks | 9.75625 | +| Jeff Teague | PG | Atlanta Hawks | 8 | +| Kyle Korver | SG | Atlanta Hawks | 5.74648 | +| Thabo Sefolosha | SF | Atlanta Hawks | 4 | +| Mike Scott | PF | Atlanta Hawks | 3.33333 | +| Kent Bazemore | SF | Atlanta Hawks | 2 | +| Dennis Schroder | PG | Atlanta Hawks | 1.7634 | +| Tim Hardaway Jr. | SG | Atlanta Hawks | 1.30452 | + +(省略了 407 行) + +```py +nba.take(0) +``` + +| `PLAYER` | `POSITION` | `TEAM` | `'15-'16 SALARY` | +| --- | --- | --- | --- | +| Paul Millsap | PF | Atlanta Hawks | 18.6717 | + + +这是一个新表,只拥有我们指定的单个行。 + +通过指定一系列索引作为参数,我们还可以获得第四,第五和第六行。 + +```py +nba.take(np.arange(3, 6)) +``` + + +| `PLAYER` | `POSITION` | `TEAM` | `'15-'16 SALARY` | +| --- | --- | --- | --- | +| Jeff Teague | PG | Atlanta Hawks | 8 | +| Kyle Korver | SG | Atlanta Hawks | 5.74648 | +| Thabo Sefolosha | SF | Atlanta Hawks | 4 | + +如果我们想要前五个最高薪球员的表格,我们可以先按薪水排序,然后取前五行: + +```py +nba.sort('SALARY', descending=True).take(np.arange(5)) +``` + +| `PLAYER` | `POSITION` | `TEAM` | `'15-'16 SALARY` | +| --- | --- | --- | --- | +| Kobe Bryant | SF | Los Angeles Lakers | 25 | +| Joe Johnson | SF | Brooklyn Nets | 24.8949 | +| LeBron James | SF | Cleveland Cavaliers | 22.9705 | +| Carmelo Anthony | SF | New York Knicks | 22.875 | +| Dwight Howard | C | Houston Rockets | 22.3594 | + +### 对应指定特征的行 + + +更常见的情况是,我们打算访问一组行中的数据,它们具有某种特征,但是我们并不知道其索引。 例如,我们可能想要所有薪水大于一千万美元的球员的数据,但我们不希望花费时间,对已排序的表中的行进行计数。 + +`where`方法可以做到。 它的输出是一个表格,列与原始表格相同,但只有特征出现的行。 + +`where`的第一个参数是列标签,列中包含信息,有关某行是否具有我们想要的特征。 如果特征是“薪水超过一千万美元”,那么列就是`SALARY`。 + +`where`的第二个参数是用于指定特征的方式。 一些例子会使指定的一般方式更容易理解。 + +在第一个例子中,我们提取了所有薪水超过一千万美元的人的数据。 + +```py +nba.where('SALARY', are.above(10)) +``` + +| `PLAYER` | `POSITION` | `TEAM` | `'15-'16 SALARY` | +| --- | --- | --- | --- | +| Paul Millsap | PF | Atlanta Hawks | 18.6717 | +| Al Horford | C | Atlanta Hawks | 12 | +| Joe Johnson | SF | Brooklyn Nets | 24.8949 | +| Thaddeus Young | PF | Brooklyn Nets | 11.236 | +| Al Jefferson | C | Charlotte Hornets | 13.5 | +| Nicolas Batum | SG | Charlotte Hornets | 13.1253 | +| Kemba Walker | PG | Charlotte Hornets | 12 | +| Derrick Rose | PG | Chicago Bulls | 20.0931 | +| Jimmy Butler | SG | Chicago Bulls | 16.4075 | +| Joakim Noah | C | Chicago Bulls | 13.4 | + +(省略了 59 行) + +`are.above(10)`的参数的确保了,每个选择的行的`SALARY`大于 10。 + +新的表格有 69 行,相当于 69 个球员的薪水是一千万美元。 按顺序排列这些行使数据更易于分析。 多伦多猛龙队(Toronto Raptors)的德玛尔·德罗赞(DeMar DeRozan)是这个分组(薪水超过一千万美元)中“最穷”的一个。 + +```py +nba.where('SALARY', are.above(10)).sort('SALARY') +``` + +| `PLAYER` | `POSITION` | `TEAM` | `'15-'16 SALARY` | +| --- | --- | --- | --- | +| DeMar DeRozan | SG | Toronto Raptors | 10.05 | +| Gerald Wallace | SF | Philadelphia 76ers | 10.1059 | +| Luol Deng | SF | Miami Heat | 10.1516 | +| Monta Ellis | SG | Indiana Pacers | 10.3 | +| Wilson Chandler | SF | Denver Nuggets | 10.4494 | +| Brendan Haywood | C | Cleveland Cavaliers | 10.5225 | +| Jrue Holiday | PG | New Orleans Pelicans | 10.5955 | +| Tyreke Evans | SG | New Orleans Pelicans | 10.7346 | +| Marcin Gortat | C | Washington Wizards | 11.2174 | +| Thaddeus Young | PF | Brooklyn Nets | 11.236 | + +(省略了 59 行) + + +斯蒂芬·库里(Stephen Curry)挣了多少? 对于答案,我们必须访问`PLAYER`的值等于`Stephen Curry`的行。 这是一个只包含一行的表格: + +```py +nba.where('PLAYER', are.equal_to('Stephen Curry')) +``` + +| `PLAYER` | `POSITION` | `TEAM` | `'15-'16 SALARY` | +| --- | --- | --- | --- | +| Stephen Curry | PG | Golden State Warriors | 11.3708 | + +库里只有不到 1140 万美元。 这是很多钱,但还不到勒布朗·詹姆斯(LeBron James)薪水的一半。 你可以在本节前面的“前 5 名”表中找到薪水,或者你可以在上面的代码中找到它,将`'Stephen Curry`换成`'LeBron James'`。 + +代码中再次使用了`are`,但这次是谓词`equal_to`而不是上面那个。 因此,例如,你可以得到包含所有的勇士的表格: + +```py +nba.where('TEAM', are.equal_to('Golden State Warriors')).show() +``` + +| `PLAYER` | `POSITION` | `TEAM` | `'15-'16 SALARY` | +| --- | --- | --- | --- | +| Klay Thompson | SG | Golden State Warriors | 15.501 | +| Draymond Green | PF | Golden State Warriors | 14.2609 | +| Andrew Bogut | C | Golden State Warriors | 13.8 | +| Andre Iguodala | SF | Golden State Warriors | 11.7105 | +| Stephen Curry | PG | Golden State Warriors | 11.3708 | +| Jason Thompson | PF | Golden State Warriors | 7.00847 | +| Shaun Livingston | PG | Golden State Warriors | 5.54373 | +| Harrison Barnes | SF | Golden State Warriors | 3.8734 | +| Marreese Speights | C | Golden State Warriors | 3.815 | +| Leandro Barbosa | SG | Golden State Warriors | 2.5 | +| Festus Ezeli | C | Golden State Warriors | 2.00875 | +| Brandon Rush | SF | Golden State Warriors | 1.27096 | +| Kevon Looney | SF | Golden State Warriors | 1.13196 | +| Anderson Varejao | PF | Golden State Warriors | 0.289755 | + +这部分表格已经按薪水排序,因为原始的表格按薪水排序,列出了同一个球队中球员。 行尾的`.show()`确保显示所有行,而不仅仅是前 10 行。 + +请求某列等于某值的行非常普遍,因此`are.equal_to`调用是可选的。 相反,仅仅使用列名和值来调用`where`方法,以达到相同的效果。 + +``` +nba.where('TEAM', 'Denver Nuggets') +# equivalent to nba.where('TEAM', are.equal_to('Denver Nuggets')) +``` + +| `PLAYER` | `POSITION` | `TEAM` | `'15-'16 SALARY` | +| --- | --- | --- | --- | +| Danilo Gallinari | SF | Denver Nuggets | 14 | +| Kenneth Faried | PF | Denver Nuggets | 11.236 | +| Wilson Chandler | SF | Denver Nuggets | 10.4494 | +| JJ Hickson | C | Denver Nuggets | 5.6135 | +| Jameer Nelson | PG | Denver Nuggets | 4.345 | +| Will Barton | SF | Denver Nuggets | 3.53333 | +| Emmanuel Mudiay | PG | Denver Nuggets | 3.10224 | +| Darrell Arthur | PF | Denver Nuggets | 2.814 | +| Jusuf Nurkic | C | Denver Nuggets | 1.842 | +| Joffrey Lauvergne | C | Denver Nuggets | 1.70972 | + +(省略了 4 行) + +### 多个属性 + +通过重复使用`where`,你可以访问具有多个指定特征的行。 例如,这是一种方法,提取薪水超过一千五百万美元的所有控球后卫。 + +```py +nba.where('POSITION', 'PG').where('SALARY', are.above(15)) +``` + +| `PLAYER` | `POSITION` | `TEAM` | `'15-'16 SALARY` | +| --- | --- | --- | --- | +| Derrick Rose | PG | Chicago Bulls | 20.0931 | +| Kyrie Irving | PG | Cleveland Cavaliers | 16.4075 | +| Chris Paul | PG | Los Angeles Clippers | 21.4687 | +| Russell Westbrook | PG | Oklahoma City Thunder | 16.7442 | +| John Wall | PG | Washington Wizards | 15.852 | + +### 一般形式 + +现在你已经意识到,通过选择具有给定特征的行,来创建新表的一般方法,是使用`where`和`are`,以及适当的条件: + +```py +original_table_name.where(column_label_string, are.condition) + +nba.where('SALARY', are.between(10, 10.3)) +``` + +| `PLAYER` | `POSITION` | `TEAM` | `'15-'16 SALARY` | +| --- | --- | --- | --- | +| Luol Deng | SF | Miami Heat | 10.1516 | +| Gerald Wallace | SF | Philadelphia 76ers | 10.1059 | +| Danny Green | SG | San Antonio Spurs | 10 | +| DeMar DeRozan | SG | Toronto Raptors | 10.05 | + +请注意,上面的表格包括赚一千万美元的 Danny Green,而不包括一千三百万美元的 Monta Ellis。 与 Python 中的其他地方一样,范围包括左端但不包括右端。 + +如果我们指定一个任何行都不满足的条件,我们得到一个带有列标签但没有行的表。 + +```py +nba.where('PLAYER', are.equal_to('Barack Obama')) +``` + +| PLAYER | POSITION | TEAM | SALARY | +| --- | --- | --- | --- | +| | | | + +### 更多条件 + +这里有一些谓词,你可能会觉得有用。 请注意,`x`和`y`是数字,`STRING`是一个字符串,`Z`是数字或字符串;你必须指定这些,取决于你想要的特征。 + +| 谓词 | 描述 | +| --- | --- | +| `are.equal_to(Z)` | 等于`Z` | +| `are.above(x)` | 大于`x` | +| `are.above_or_equal_to(x)` | 大于等于`x` | +| `are.below(x)` | 小于`x` | +| `are.below_or_equal_to(x)` | 小于等于`x` | +| `are.between(x, y)` | 大于等于`x`,小于`y` | +| `are.strictly_between(x, y)` | 大于`x`,小于`y` | +| `are.between_or_equal_to(x, y)` | 大于等于`x`,小于等于`y` | +| `are.containing(S)` | 包含字符串`S` | + +你也可以指定任何这些条件的否定,通过在条件前面使用`.not_`。 + + +| 谓词 | 描述 | +| --- | --- | +| `are.not_equal_to(Z)` | 不等于`Z` | +| `are.not_above(x)` | 不大于`x` | + +以及其他。通常的逻辑规则是适用的,例如,“不大于`x`”等价于“小于等于`x`”。 + +我们以一系列示例结束这一节。 + +`are.containing`的使用有助于少打一些字。 例如,你可以指定`Warriors`而不是`Golden State Warriors`: + +```py +nba.where('TEAM', are.containing('Warriors')).show() +``` + +| `PLAYER` | `POSITION` | `TEAM` | `'15-'16 SALARY` | +| --- | --- | --- | --- | +| Klay Thompson | SG | Golden State Warriors | 15.501 | +| Draymond Green | PF | Golden State Warriors | 14.2609 | +| Andrew Bogut | C | Golden State Warriors | 13.8 | +| Andre Iguodala | SF | Golden State Warriors | 11.7105 | +| Stephen Curry | PG | Golden State Warriors | 11.3708 | +| Jason Thompson | PF | Golden State Warriors | 7.00847 | +| Shaun Livingston | PG | Golden State Warriors | 5.54373 | +| Harrison Barnes | SF | Golden State Warriors | 3.8734 | +| Marreese Speights | C | Golden State Warriors | 3.815 | +| Leandro Barbosa | SG | Golden State Warriors | 2.5 | +| Festus Ezeli | C | Golden State Warriors | 2.00875 | +| Brandon Rush | SF | Golden State Warriors | 1.27096 | +| Kevon Looney | SF | Golden State Warriors | 1.13196 | +| Anderson Varejao | PF | Golden State Warriors | 0.289755 | + +你可以提取所有后卫的数据,包括控球后卫和得分后卫。 + +```py +nba.where('POSITION', are.containing('G')) +``` + +| `PLAYER` | `POSITION` | `TEAM` | `'15-'16 SALARY` | +| --- | --- | --- | --- | +| Jeff Teague | PG | Atlanta Hawks | 8 | +| Kyle Korver | SG | Atlanta Hawks | 5.74648 | +| Dennis Schroder | PG | Atlanta Hawks | 1.7634 | +| Tim Hardaway Jr. | SG | Atlanta Hawks | 1.30452 | +| Jason Richardson | SG | Atlanta Hawks | 0.947276 | +| Lamar Patterson | SG | Atlanta Hawks | 0.525093 | +| Terran Petteway | SG | Atlanta Hawks | 0.525093 | +| Avery Bradley | PG | Boston Celtics | 7.73034 | +| Isaiah Thomas | PG | Boston Celtics | 6.91287 | +| Marcus Smart | PG | Boston Celtics | 3.43104 | + +(省略了 171 行) + +你可以获取所有不是克利夫兰骑士队的球员,并且薪水不低于两千万美元: + +```py +other_than_Cavs = nba.where('TEAM', are.not_equal_to('Cleveland Cavaliers')) +other_than_Cavs.where('SALARY', are.not_below(20)) +``` + +| `PLAYER` | `POSITION` | `TEAM` | `'15-'16 SALARY` | +| --- | --- | --- | --- | +| Joe Johnson | SF | Brooklyn Nets | 24.8949 | +| Derrick Rose | PG | Chicago Bulls | 20.0931 | +| Dwight Howard | C | Houston Rockets | 22.3594 | +| Chris Paul | PG | Los Angeles Clippers | 21.4687 | +| Kobe Bryant | SF | Los Angeles Lakers | 25 | +| Chris Bosh | PF | Miami Heat | 22.1927 | +| Dwyane Wade | SG | Miami Heat | 20 | +| Carmelo Anthony | SF | New York Knicks | 22.875 | +| Kevin Durant | SF | Oklahoma City Thunder | 20.1586 | + +有很多方式可以创建相同的表格。这里是另一种,并且显然你可以想出来更多。 + +```py +other_than_Cavs.where('SALARY', are.above_or_equal_to(20)) +``` + +| `PLAYER` | `POSITION` | `TEAM` | `'15-'16 SALARY` | +| --- | --- | --- | --- | +| Joe Johnson | SF | Brooklyn Nets | 24.8949 | +| Derrick Rose | PG | Chicago Bulls | 20.0931 | +| Dwight Howard | C | Houston Rockets | 22.3594 | +| Chris Paul | PG | Los Angeles Clippers | 21.4687 | +| Kobe Bryant | SF | Los Angeles Lakers | 25 | +| Chris Bosh | PF | Miami Heat | 22.1927 | +| Dwyane Wade | SG | Miami Heat | 20 | +| Carmelo Anthony | SF | New York Knicks | 22.875 | +| Kevin Durant | SF | Oklahoma City Thunder | 20.1586 | + +你可以看到,`where`的使用提供了很大的灵活性,来访问你感兴趣的特征。 不要犹豫,尝试它吧! + +## 示例:人口趋势 + +### 美国人口的趋势 + +现在我们做好了处理大量的数据表的准备。 下面的文件包含“美国居民人口的年度估计,按年龄和性别分列”。 请注意,`read_table`可以直接从 URL 读取数据。 + +```py +# As of Jan 2017, this census file is online here: +data = 'http://www2.census.gov/programs-surveys/popest/datasets/2010-2015/national/asrh/nc-est2015-agesex-res.csv' + +# A local copy can be accessed here in case census.gov moves the file: +# data = 'nc-est2015-agesex-res.csv' + +full_census_table = Table.read_table(data) +full_census_table +``` + +| SEX | AGE | CENSUS2010POP | ESTIMATESBASE2010 | POPESTIMATE2010 | POPESTIMATE2011 | POPESTIMATE2012 | POPESTIMATE2013 | POPESTIMATE2014 | POPESTIMATE2015 | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 0 | 0 | 3944153 | 3944160 | 3951330 | 3963087 | 3926540 | 3931141 | 3949775 | 3978038 | +| 0 | 1 | 3978070 | 3978090 | 3957888 | 3966551 | 3977939 | 3942872 | 3949776 | 3968564 | +| 0 | 2 | 4096929 | 4096939 | 4090862 | 3971565 | 3980095 | 3992720 | 3959664 | 3966583 | +| 0 | 3 | 4119040 | 4119051 | 4111920 | 4102470 | 3983157 | 3992734 | 4007079 | 3974061 | +| 0 | 4 | 4063170 | 4063186 | 4077551 | 4122294 | 4112849 | 3994449 | 4005716 | 4020035 | +| 0 | 5 | 4056858 | 4056872 | 4064653 | 4087709 | 4132242 | 4123626 | 4006900 | 4018158 | +| 0 | 6 | 4066381 | 4066412 | 4073013 | 4074993 | 4097605 | 4142916 | 4135930 | 4019207 | +| 0 | 7 | 4030579 | 4030594 | 4043046 | 4083225 | 4084913 | 4108349 | 4155326 | 4148360 | +| 0 | 8 | 4046486 | 4046497 | 4025604 | 4053203 | 4093177 | 4095711 | 4120903 | 4167887 | +| 0 | 9 | 4148353 | 4148369 | 4125415 | 4035710 | 4063152 | 4104072 | 4108349 | 4133564 | + +(已省略 296 行) + +只显示了表格的前 10 行。稍后我们将看到如何显示整个表格;但是,这通常不适用于大型表格。 + +表格的描述一起出现。`SEX`列包含数字代码:`0`代表总体,`1代`表男性,`2`代表女性。 `AGE`列包含完整年份为单位的年龄,但特殊值`999`是人口的总和。 其余的列包含美国人口的估计。 + +通常,公共表格将包含更多的信息,不仅仅是特定调查或分析所需的信息。 在这种情况下,我们假设我们只对 2010 年到 2014 年的人口变化感兴趣。让我们选择相关的列。 + +```py +partial_census_table = full_census_table.select('SEX', 'AGE', 'POPESTIMATE2010', 'POPESTIMATE2014') +partial_census_table +``` + +| SEX | AGE | POPESTIMATE2010 | POPESTIMATE2014 | +| --- | --- | --- | --- | +| 0 | 0 | 3951330 | 3949775 | +| 0 | 1 | 3957888 | 3949776 | +| 0 | 2 | 4090862 | 3959664 | +| 0 | 3 | 4111920 | 4007079 | +| 0 | 4 | 4077551 | 4005716 | +| 0 | 5 | 4064653 | 4006900 | +| 0 | 6 | 4073013 | 4135930 | +| 0 | 7 | 4043046 | 4155326 | +| 0 | 8 | 4025604 | 4120903 | +| 0 | 9 | 4125415 | 4108349 | + +(已省略 296 行) + +我们也可以简化所选列的标签。 + +```py +us_pop = partial_census_table.relabeled('POPESTIMATE2010', '2010').relabeled('POPESTIMATE2014', '2014') +us_pop +``` + +| SEX | AGE | 2010 | 2014 | +| --- | --- | --- | --- | +| 0 | 0 | 3951330 | 3949775 | +| 0 | 1 | 3957888 | 3949776 | +| 0 | 2 | 4090862 | 3959664 | +| 0 | 3 | 4111920 | 4007079 | +| 0 | 4 | 4077551 | 4005716 | +| 0 | 5 | 4064653 | 4006900 | +| 0 | 6 | 4073013 | 4135930 | +| 0 | 7 | 4043046 | 4155326 | +| 0 | 8 | 4025604 | 4120903 | +| 0 | 9 | 4125415 | 4108349 | + +(已省略 296 行) + +我们现在有了一个易于使用的表格。 表中的每一列都是一个等长的数组,因此列可以使用算术进行组合。 这是 2010 年至 2014 年的人口变化。 + +```py +us_pop.column('2014') - us_pop.column('2010') +array([ -1555, -8112, -131198, ..., 6443, 12950, 4693244]) +``` + +让我们使用包含这些变化的列来扩展`us_pop`,一列是绝对数值,另一列是相对于 2010 年数值的百分比。 + +```py +change = us_pop.column('2014') - us_pop.column('2010') +census = us_pop.with_columns( + 'Change', change, + 'Percent Change', change/us_pop.column('2010') +) +census.set_format('Percent Change', PercentFormatter) +``` + +| SEX | AGE | 2010 | 2014 | Change | Percent Change | +| --- | --- | --- | --- | --- | --- | +| 0 | 0 | 3951330 | 3949775 | -1555 | -0.04% | +| 0 | 1 | 3957888 | 3949776 | -8112 | -0.20% | +| 0 | 2 | 4090862 | 3959664 | -131198 | -3.21% | +| 0 | 3 | 4111920 | 4007079 | -104841 | -2.55% | +| 0 | 4 | 4077551 | 4005716 | -71835 | -1.76% | +| 0 | 5 | 4064653 | 4006900 | -57753 | -1.42% | +| 0 | 6 | 4073013 | 4135930 | 62917 | 1.54% | +| 0 | 7 | 4043046 | 4155326 | 112280 | 2.78% | +| 0 | 8 | 4025604 | 4120903 | 95299 | 2.37% | +| 0 | 9 | 4125415 | 4108349 | -17066 | -0.41% | + +(已省略 296 行) + +将数据排序。让我们按照人口绝对变化的降序排序表格。 + +```py +census.sort('Change', descending=True) +``` + +| SEX | AGE | 2010 | 2014 | Change | Percent Change | +| --- | --- | --- | --- | --- | --- | +| 0 | 999 | 309346863 | 318907401 | 9560538 | 3.09% | +| 1 | 999 | 152088043 | 156955337 | 4867294 | 3.20% | +| 2 | 999 | 157258820 | 161952064 | 4693244 | 2.98% | +| 0 | 67 | 2693707 | 3485241 | 791534 | 29.38% | +| 0 | 64 | 2706055 | 3487559 | 781504 | 28.88% | +| 0 | 66 | 2621335 | 3347060 | 725725 | 27.69% | +| 0 | 65 | 2678525 | 3382824 | 704299 | 26.29% | +| 0 | 71 | 1953607 | 2519705 | 566098 | 28.98% | +| 0 | 34 | 3822189 | 4364748 | 542559 | 14.19% | +| 0 | 23 | 4217228 | 4702156 | 484928 | 11.50% | + +毫不奇怪,排序后表格的第一行对应整个人口:所有年龄和性别的分组。 从 2010 年到 2014 年,美国人口增加了约 950 万人,仅为 3%。 + +接下来的两行分别对应所有的男性和所有的女性。 以绝对数量和百分比来衡量,男性人口的增长高于女性人口。 百分比变化都在 3% 左右。 + +现在看看接下来的几行。 百分比变化从总人口的 3% 左右,上升到 60 年代末和 70 年代初的近 30%。 这个惊人的变化称为美国的老龄化。 + +到目前为止,2014 年 64~67 岁年龄段的绝对变化最大。什么可以解释这一大幅增长的原因? 我们可以通过考察相关分组的出生年份,来探索这个问题。 + +那些 2010 年在 64~67 岁年龄段的人,在 1943 年到 1946 年间出生。珍珠港的袭击是在 1941 年底,美军在 1942 年发动了一场大规模战争,结束于 1945 年。 + +2014 年 64 岁到 67 岁的人生于 1947 年到 1950 年,是美国二战后的生育高峰。 + +战后的生育高峰,是我们观察到的巨大变化的主要原因。 + +## 示例:性别趋势 + +### 性别比例的趋势 + +我们现在拥有了足够的编码技能,足以检查美国人群中的特征和趋势。在这个例子中,我们将查看不同年龄的男性和女性的分布情况。我们将继续使用上一节中的`us_pop`表。 + +```py +us_pop +``` + +| SEX | AGE | 2010 | 2014 | +| --- | --- | --- | --- | +| 0 | 0 | 3951330 | 3949775 | +| 0 | 1 | 3957888 | 3949776 | +| 0 | 2 | 4090862 | 3959664 | +| 0 | 3 | 4111920 | 4007079 | +| 0 | 4 | 4077551 | 4005716 | +| 0 | 5 | 4064653 | 4006900 | +| 0 | 6 | 4073013 | 4135930 | +| 0 | 7 | 4043046 | 4155326 | +| 0 | 8 | 4025604 | 4120903 | +| 0 | 9 | 4125415 | 4108349 | + +(已省略 296 行) + +我们从之前对这个数据集的检查得知,表格的描述一起出现。 它提醒了表中包含的内容。 + +每一行表示一个年龄。`SEX`列包含数字代码:`0`代表总数,`1`代表男性,`2`代表女性。 年龄栏包含以完整年份为单位的年龄,但特殊值`999`代表整个人口,不论年龄是什么。其余的列包含美国人口的估计。 + +### 理解`AGE=100` + +作为一个初步的例子,我们来解释一下表中最后的年龄的数据,其中年龄是`100`岁。下面的代码提取了男性和女性(性别代码`0`)的组合分组,年龄最大的行。 + +```py +us_pop.where('SEX', are.equal_to(0)).where('AGE', are.between(97, 101)) +``` + +| SEX | AGE | 2010 | 2014 | +| --- | --- | --- | --- | +| 0 | 97 | 68893 | 83089 | +| 0 | 98 | 47037 | 59726 | +| 0 | 99 | 32178 | 41468 | +| 0 | 100 | 54410 | 71626 | + +不足为奇的是,年龄越大,人数越少,例如 99 岁的人数少于 98 岁。 + +然而,令人吃惊的是,100 岁的数值比 99 岁的数值要大得多。仔细查看文件,这是因为人口普查局使用 100 作为 100 或岁以上的每个人的代码。 + +年龄为 100 岁的人不仅仅代表 100 岁的人,还包括年龄在 100 岁以上的人。这就是为什么那一行的数字大于 99 岁的人的数字。 + +### 男性和女性的整体比例 + +我们现在开始考察 2014 年的性别比例。首先,我们一起来看看所有的年龄。 请记住,这意味着查看`AGE`编码为`999`的行。`all_ages`表包含此信息。其中有三行:一个是两种性别总体,一个是男性(`SEX`代码为`1`),一个是女性(`SEX`代码为`2`)。 + +```py +us_pop_2014 = us_pop.drop('2010') +all_ages = us_pop_2014.where('AGE', are.equal_to(999)) +all_ages +``` + +| SEX | AGE | 2014 | +| --- | --- | --- | +| 0 | 999 | 318907401 | +| 1 | 999 | 156955337 | +| 2 | 999 | 161952064 | + +`all_ages`的第 0 行包含两年中每年的美国总人口。2014 年,美国人口刚刚少于 3.19 亿。 + +第 1 行包含男性的计数,女性是第 2 行。 比较这两行可以看到,在 2014 年,美国的女性比男性多。 + +第 1 行和第 2 行的人口数加起来为第 0 行的总人口数。 + +为了与其他数量进行比较,我们需要将这些数字转换为总人口中的百分比。 让我们访问 2014 年的总数并命名。 然后,我们将显示带有比例列的人口表格。 与我们先前观察到的,女性人数多于男性的情况一致,2014 年约有 50.8% 的人口是女性,两年的每年中,约有 49.2% 的人口是男性。 + +```py +pop_2014 = all_ages.column('2014').item(0) +all_ages.with_column( + 'Proportion', all_ages.column('2014')/pop_2014 +).set_format('Proportion', PercentFormatter) +``` + +| SEX | AGE | 2014 | Proportion | +| --- | --- | --- | --- | +| 0 | 999 | 318907401 | 100.00% | +| 1 | 999 | 156955337 | 49.22% | +| 2 | 999 | 161952064 | 50.78% | + +### 新生儿中男孩和女孩的比例 + +但是,当我们查看婴儿时,情况正好相反。 让我们将婴儿定义为还没有完整一年的人,对应年龄为 0 的行。这里是他们的人口数量。 你可以看到男婴比女婴多。 + +```py +infants = us_pop_2014.where('AGE', are.equal_to(0)) +infants +``` + +| SEX | AGE | 2014 | +| --- | --- | --- | +| 0 | 0 | 3949775 | +| 1 | 0 | 2020326 | +| 2 | 0 | 1929449 | + +像以前一样,我们可以将这些数字转换成婴儿总数中的百分比。 所得表格显示,2014 年,美国超过 51% 的婴儿是男性。 + +```py +infants_2014 = infants.column('2014').item(0) +infants.with_column( + 'Proportion', infants.column('2014')/infants_2014 +).set_format('Proportion', PercentFormatter) +``` + +| SEX | AGE | 2014 | Proportion | +| --- | --- | --- | --- | +| 0 | 0 | 3949775 | 100.00% | +| 1 | 0 | 2020326 | 51.15% | +| 2 | 0 | 1929449 | 48.85% | + +事实上,长期以来,新生儿中男孩的比例略高于 1/2。 这个原因还没有得到彻底的理解,[科学家们还在努力](http://www.npr.org/sections/health-shots/2015/03/30/396384911/why-are-more-baby-boys-born-than-girls)。 + +### 每个年龄的男女比例 + +我们已经看到,虽然男婴比女婴多,但女性总数比男性多。 所以很显然,性别之间的分隔在不同年龄之间必须有所不同。 + +为了研究这个变化,我们将女性和男性的数据分开,并消除所有年龄的组合,年龄编码为 999 的行。 + +`females`和`male`表格分别包含两个性别的数据。 + +```py +females_all_rows = us_pop_2014.where('SEX', are.equal_to(2)) +females = females_all_rows.where('AGE', are.not_equal_to(999)) +females +``` + +| SEX | AGE | 2014 | +| --- | --- | --- | +| 2 | 0 | 1929449 | +| 2 | 1 | 1931375 | +| 2 | 2 | 1935991 | +| 2 | 3 | 1957483 | +| 2 | 4 | 1961199 | +| 2 | 5 | 1962561 | +| 2 | 6 | 2024870 | +| 2 | 7 | 2032494 | +| 2 | 8 | 2015285 | +| 2 | 9 | 2010659 | + +(省略了 91 行) + +```py +males_all_rows = us_pop_2014.where('SEX', are.equal_to(1)) +males = males_all_rows.where('AGE', are.not_equal_to(999)) +males +``` + +| SEX | AGE | 2014 | +| --- | --- | --- | +| 1 | 0 | 2020326 | +| 1 | 1 | 2018401 | +| 1 | 2 | 2023673 | +| 1 | 3 | 2049596 | +| 1 | 4 | 2044517 | +| 1 | 5 | 2044339 | +| 1 | 6 | 2111060 | +| 1 | 7 | 2122832 | +| 1 | 8 | 2105618 | +| 1 | 9 | 2097690 | + +(省略了 91 行) + +现在的计划是,比较两年中每一年的,每个年龄的女性人数和男性人数。 数组和表格的方法为我们提供了直接的方式。 这两个表格中,每个年龄都有一行。 + +```py +males.column('AGE') +array([ 0, 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, 56, 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, 84, 85, 86, 87, 88, 89, 90, + 91, 92, 93, 94, 95, 96, 97, 98, 99, 100]) +females.column('AGE') +array([ 0, 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, 56, 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, 84, 85, 86, 87, 88, 89, 90, + 91, 92, 93, 94, 95, 96, 97, 98, 99, 100]) +``` + +对于任何特定年龄,我们都可以通过将女性人数除以男性人数,获的女性:男性性别比例。 为了一步完成它,我们可以使用列来提取女性数量的数组,和相应的男性数量的数组,然后简单地将一个数组除以另一个数组。 逐元素相除将为所有年份创建性别比例的数组。 + +```py +ratios = Table().with_columns( + 'AGE', females.column('AGE'), + '2014 F:M RATIO', females.column('2014')/males.column('2014') +) +ratios +``` + + +| AGE | 2014 F:M RATIO | +| --- | --- | +| 0 | 0.955019 | +| 1 | 0.956884 | +| 2 | 0.956672 | +| 3 | 0.955058 | +| 4 | 0.959248 | +| 5 | 0.959998 | +| 6 | 0.959172 | +| 7 | 0.957445 | +| 8 | 0.957099 | +| 9 | 0.958511 | + +(省略了 91 行) + +从输出中可以看出,九岁以下儿童的比例都在 0.96 左右。 当男女比例小于 1 时,女性比男性少。 因此,我们所看到的是,在 0~9 岁的年龄段中,女孩比男孩少。此外,在每个年龄中,每 100 个男孩大约对应 96 个女孩。 + +那么人口中女性的整体比例为什么高于男性呢? + +当我们检查年龄的另一端时,会发现一些非同寻常的事情。 以下是 75 岁以上的男女比例。 + +```py +ratios.where('AGE', are.above(75)).show() +``` + + +| AGE | 2014 F:M RATIO | +| --- | --- | +| 76 | 1.23487 | +| 77 | 1.25797 | +| 78 | 1.28244 | +| 79 | 1.31627 | +| 80 | 1.34138 | +| 81 | 1.37967 | +| 82 | 1.41932 | +| 83 | 1.46552 | +| 84 | 1.52048 | +| 85 | 1.5756 | +| 86 | 1.65096 | +| 87 | 1.72172 | +| 88 | 1.81223 | +| 89 | 1.91837 | +| 90 | 2.01263 | +| 91 | 2.09488 | +| 92 | 2.2299 | +| 93 | 2.33359 | +| 94 | 2.52285 | +| 95 | 2.67253 | +| 96 | 2.87998 | +| 97 | 3.09104 | +| 98 | 3.41826 | +| 99 | 3.63278 | +| 100 | 4.25966 | + +不仅所有这些比例大于 1,在所有这些年龄段中,女性比男性多,其中许多比例大于 1。 + ++ 在 89 岁和 90 岁中,比例接近 2,这意味着 2014 年这些年龄的女性约为男性的两倍。 ++ 在 98 岁和 99 岁中,女性约为男性的 3.5 至 4 倍。 + +如果你想知道有多少高龄的人,你可以使用 Python 来发现: + +```py +males.where('AGE', are.between(98, 100)) +``` + +| SEX | AGE | 2014 | +| --- | --- | --- | +| 1 | 98 | 13518 | +| 1 | 99 | 8951 | + +``` +females.where('AGE', are.between(98, 100)) +``` + +| SEX | AGE | 2014 | +| --- | --- | --- | +| 2 | 98 | 46208 | +| 2 | 99 | 32517 | + +下图展示了年龄相关的性别比率。 蓝色曲线显示 2014 年的比例与年龄。 + +从 0 岁到 60 岁,这个比例差不多是 1(表示男性和女性差不多相等),但从 65 岁开始,比例开始急剧上升(女性多于男性)。 + +美国女性人数多于男性,部分原因是老年妇女的显着的性别不平衡。 + +```py +ratios.plot('AGE') +``` + +![](img/5-1.png) diff --git a/docs/data8-textbook-zh/6.md b/docs/data8-textbook-zh/6.md new file mode 100644 index 0000000000000000000000000000000000000000..09889a6664351397f195cb9de952d093278ac354 --- /dev/null +++ b/docs/data8-textbook-zh/6.md @@ -0,0 +1,887 @@ +# 六、可视化 + +> 原文:[Visualization](https://github.com/data-8/textbook/tree/gh-pages/chapters/06) + +> 译者:[飞龙](https://github.com/wizardforcel) + +> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) + +> 自豪地采用[谷歌翻译](https://translate.google.cn/) + +表格是一种组织和可视化数据的强大方式。然而,无论数据如何组织,数字的大型表格可能难以解释。 有时解释图片比数字容易得多。 + +在本章中,我们将开发一些数据分析的基本图形方法。 我们的数据源是[互联网电影数据库](http://www.imdb.com/)(IMDB),这是一个在线数据库,包含电影,电视节目,和视频游戏等信息。[Box Office Mojo](http://www.boxofficemojo.com/) 网站提供了许多 IMDB 数据摘要,我们已经采用了其中一些。 我们也使用了 [The Numbers](http://www.the-numbers.com/) 的数据摘要,这个网站的口号是“数据和电影业务的相遇之处”。 + +## 散点图和线形图 + + +`actors`表包含好莱坞的男性和女性演员的数据。 其中的列是: + +| 列 | 内容 | +| --- | --- | +| Actor | 演员名称 | +| Total Gross | 演员所有电影的国内票房总收入(百万美元) | +| Number of Movies | 演员所演的电影数量 | +| Average per Movie | 总收入除以电影数量 | +| #1 Movie | 演员所演的票房最高的电影 | +| Gross | 演员的 #1 电影的国内票房总收入(百万美元) | + +在总票房的计算中,数据的制表人没有包括一些电影,其中演员是客串角色或陈述角色,没有太多的登场时间。 + +这个表格有 50 行,对应着 50 个最顶级的演员。 这个表已经按照`Total Gross`排序了,所以很容易看出,`Harrison Ford`是最棒的演员。 总的来说,他的电影的国内票房收入比其他演员的电影多。 + +```py +actors = Table.read_table('actors.csv') +actors +``` + +| Actor | Total Gross | Number of Movies | Average per Movie | #1 Movie | Gross | +| --- | --- | --- | --- | --- | --- | +| Harrison Ford | 4871.7 | 41 | 118.8 | Star Wars: The Force Awakens | 936.7 | +| Samuel L. Jackson | 4772.8 | 69 | 69.2 | The Avengers | 623.4 | +| Morgan Freeman | 4468.3 | 61 | 73.3 | The Dark Knight | 534.9 | +| Tom Hanks | 4340.8 | 44 | 98.7 | Toy Story 3 | 415 | +| Robert Downey, Jr. | 3947.3 | 53 | 74.5 | The Avengers | 623.4 | +| Eddie Murphy | 3810.4 | 38 | 100.3 | Shrek 2 | 441.2 | +| Tom Cruise | 3587.2 | 36 | 99.6 | War of the Worlds | 234.3 | +| Johnny Depp | 3368.6 | 45 | 74.9 | Dead Man's Chest | 423.3 | +| Michael Caine | 3351.5 | 58 | 57.8 | The Dark Knight | 534.9 | +| Scarlett Johansson | 3341.2 | 37 | 90.3 | The Avengers | 623.4 | + +(已省略 40 行) + +术语。变量是我们称之为“特征”的东西的正式名称,比如`'number of movies'`。 术语“变量”强调了,对于不同的个体,这个特征可以有不同的值 - 演员所演电影的数量因人而异。 + +拥有数值的变量(如`'number of movies'`或`'average gross receipts per movie'`)的变量称为定量或数值变量。 + +### 散点图 + +散点图展示两个数值变量之间的关系。 在前面的章节中,我们看到了一个散点图的例子,我们看了两个经典小说的时间段和角色数量。 + +`Table`的`scatter`方法绘制一个散点图,由表格的每一行组成。它的第一个参数是要在横轴上绘制的列标签,第二个参数是纵轴上的列标签。 + +```py +actors.scatter('Number of Movies', 'Total Gross') +``` + +![](img/6-1.png) + +散点图包含 50 个点,表中的每个演员为一个。 一般来说,你可以看到它向上倾斜。 一个演员的电影越多,所有这些电影的总收入就越多。 + +在形式上,我们说图表显示了变量之间的关联,并且关联是正的:一个变量的高值往往与另一个变量的高值相关联,而低值也是一样,通常情况下。 + +当然有一些变化。 一些演员有很多电影,但总收入中等。 其他人电影数量中等,但收入很高。正相关只是一个大体趋势的叙述。 + +在课程后面,我们将学习如何量化关联。目前,我们只是定性地思考。 + +现在我们已经探索了电影的数量与总收入的关系,让我们把注意力转向它与每部电影的平均收入的关系。 + +```py +actors.scatter('Number of Movies', 'Average per Movie') +``` + +![](img/6-2.png) + +这是一个截然不同的情况,并表现出负相关。 一般来说,演员的电影数量越多,每部电影的平均收入就越少。 + +另外,有一个点是非常高的,在绘图的左边。 它对应于一个电影数量很少,每部电影平均值很高的演员。 这个点是异常的。 它位于数据的一般范围之外。 事实上,这与绘图中的其他所有点相差甚远。 + +我们将通过查看绘图的左右两端的点,来进一步检查负相关。 + +对于右端,我们通过查看没有异常值的部分来放大图的主体。 + +```py +no_outlier = actors.where('Number of Movies', are.above(10)) +no_outlier.scatter('Number of Movies', 'Average per Movie') +``` + +![](img/6-3.png) + +负相关仍然清晰可见。 让我们找出一些演员,对应位于绘图右侧的点,这里电影数量较多: + +```py +actors.where('Number of Movies', are.above(60)) +``` + +| Actor | Total Gross | Number of Movies | Average per Movie | #1 Movie | Gross | +| --- | --- | --- | --- | --- | --- | +| Samuel L. Jackson | 4772.8 | 69 | 69.2 | The Avengers | 623.4 | +| Morgan Freeman | 4468.3 | 61 | 73.3 | The Dark Knight | 534.9 | +| Robert DeNiro | 3081.3 | 79 | 39 | Meet the Fockers | 279.3 | +| Liam Neeson | 2942.7 | 63 | 46.7 | The Phantom Menace | 474.5 | + +伟大的演员罗伯特·德尼罗(Robert DeNiro)拥有最高的电影数量和最低的每部电影的平均收入。 其他优秀的演员在不远处的点,但德尼罗在极远处。 + +为了理解负相关,请注意,演员所演的电影越多,在风格,流派和票房方片,这些电影变化就越大。 例如,一个演员可能会出现在一些高收入的动作电影或喜剧中(如 Meet Fockers),也可能是优秀但不会吸引大量人群的小众电影。 因此,演员的每部电影的平均收入值可能相对较低。 + +为了从不同的角度来看待这个观点,现在让我们来看看这个异常点。 + +```py +actors.where('Number of Movies', are.below(10)) +``` + +| Actor | Total Gross | Number of Movies | Average per Movie | #1 Movie | Gross | +| --- | --- | --- | --- | --- | --- | +| Anthony Daniels | 3162.9 | 7 | 451.8 | Star Wars: The Force Awakens | 936.7 | + +作为一名演员,安东尼·丹尼尔斯(Anthony Daniels)可能没有罗伯特·德尼罗(Robert DeNiro)的身材。 但是他的 7 部电影的平均收入却高达每部电影近 4.52 亿美元。 + +这些电影是什么? 你可能知道《星球大战:C-3PO》中的 Droid C-3PO,那是金属机甲里面的安东尼·丹尼尔斯。 他扮演 C-3PO。 + +![](img/6-4.png) + +丹尼尔斯先生的全部电影(除了客串)都是由高收入的“星球大战”系列电影组成的。 这就解释了他的高平均收入和低电影数量。 + +类型和制作预算等变量,会影响电影数量与每部电影的平均收入之间的关联。 这个例子提醒人们,研究两个变量之间的关联,往往也涉及到了解其他相关的变量。 + +## 线形图 + + +线形图是最常见的可视化图形之一,通常用于研究时序型的趋势和模式。 + + +`movies_by_year`表包含了 1980 年到 2015 年间,美国电影公司制作的电影的数据。这些列是: + +| 列 | 内容 | +| --- | --- | +| Year | 年份 | +| Total Gross | 所有发行电影的国内总票房收入(以百万美元为单位) | +| Number of Movies | 发行的电影数量 | +| #1 Movie | 收入最高的电影 | + +```py +movies_by_year = Table.read_table('movies_by_year.csv') +movies_by_year +``` + + +| Year | Total Gross | Number of Movies | #1 Movie | +| --- | --- | --- | --- | +| 2015 | 11128.5 | 702 | Star Wars: The Force Awakens | +| 2014 | 10360.8 | 702 | American Sniper | +| 2013 | 10923.6 | 688 | Catching Fire | +| 2012 | 10837.4 | 667 | The Avengers | +| 2011 | 10174.3 | 602 | Harry Potter / Deathly Hallows (P2) | +| 2010 | 10565.6 | 536 | Toy Story 3 | +| 2009 | 10595.5 | 521 | Avatar | +| 2008 | 9630.7 | 608 | The Dark Knight | +| 2007 | 9663.8 | 631 | Spider-Man 3 | +| 2006 | 9209.5 | 608 | Dead Man's Chest | + +(省略了 26 行) + +`Table`的`plot`方法产生线形图。 它的两个参数与散点图相同:首先是横轴上的列,然后是纵轴上的列。 这是 1980 年到 2015 年间每年发行的电影数量的线形图。 + +```py +movies_by_year.plot('Year', 'Number of Movies') +``` + +![](img/6-5.png) + +虽然每年的数字都有明显的变化,但图形急剧上升,然后呈现平缓的上升趋势。 20 世纪 80 年代早期的剧增,部分是由于在上世纪 70 年代,电影制作人推动电影业的几年后,电影制片厂重新回到电影制作的前沿。 + +我们的重点将放在最近几年。 根据电影的主题,对应 2000 年到 2015 年的行,分配给名称`century_21`。 + +```py +century_21 = movies_by_year.where('Year', are.above(1999)) +century_21.plot('Year', 'Number of Movies') +``` + +![](img/6-6.png) + +2008 年的全球金融危机有明显的效果 - 2009 年发行的电影数量急剧下降。 + +但是,美元数量并没有太大的变化。 + +```py +century_21.plot('Year', 'Total Gross') +``` + +![](img/6-7.png) + +尽管发生了金融危机,电影发行的数量也少得多,但 2009 年的国内总收入仍高于 2008 年。 + +造成这种矛盾的一个原因是,人们在经济衰退时往往会去看电影。 “经济低迷时期,美国人涌向电影”,“纽约时报”于 2009 年 2 月说。文章引用南加州大学的马丁·卡普兰(Martin Kaplan)的话说:“人们想要忘记自己的烦恼,想和别人在一起。” 当节假日和昂贵的款待难以负担,电影提供了受欢迎的娱乐和宽慰。 + +2009 年的高票房收入的另一个原因是,电影《阿凡达》及其 3D 版本。 阿凡达不仅是 2009 年的第一部电影,它也是有史以来第二高的总票房电影,我们将在后面看到。 + +```py +century_21.where('Year', are.equal_to(2009)) +``` + +| Year | Total Gross | Number of Movies | #1 Movie | +| --- | --- | --- | --- | +| 2009 | 10595.5 | 521 | Avatar | + +## 类别分许 + +### 可视化类别分布 + +许多数据不以数字的形式出现。 数据可以是音乐片段,或地图上的地方。 他们也可以是类别,你可以在里面放置个体。 以下是一些类别变量的例子。 + ++ 个体是冰淇淋纸盒,变量就是纸盒里的味道。 ++ 个体是职业篮球运动员,变量是球员的队伍。 ++ 个体是年,而变量是今年最高票房电影的流派。 ++ 个体是调查对象,变量是他们从“完全不满意”,“有点满意”和“非常满意”中选择的回答。 + +`icecream `表包含 30 盒冰激凌的数据。 + +```py +icecream = Table().with_columns( + 'Flavor', make_array('Chocolate', 'Strawberry', 'Vanilla'), + 'Number of Cartons', make_array(16, 5, 9) +) +icecream +``` + + +| Flavor | Number of Cartons | +| --- | --- | +| Chocolate | 16 | +| Strawberry | 5 | +| Vanilla | 9 | + +分类变量“口味”的值是巧克力,草莓和香草。 表格显示了每种口味的纸盒数量。 我们称之为分布表。 分布显示了所有变量的值,以及每个变量的频率。 + +### 条形图 + +条形图是可视化类别分布的熟悉方式。 它为每个类别显示一个条形。 条形的间隔相等,宽度相同。 每个条形的长度与相应类别的频率成正比。 + +我们使用横条绘制条形图,因为这样更容易标注条形图。 所以`Table`的方法称为`barh`。 它有两个参数:第一个是类别的列标签,第二个是频率的列标签。 + +```py +icecream.barh('Flavor', 'Number of Cartons') +``` + +![](img/6-8.png) + +如果表格只包含一列类别和一列频率(如冰淇淋),则方法调用甚至更简单。 你可以指定包含类别的列,`barh`将使用另一列中的值作为频率。 + +```py +icecream.barh('Flavor') +``` + +![](img/6-9.png) + +### 类别分布的特征 + +除了纯粹的视觉差异之外,条形图和我们在前面章节中看到的两个图表之间还有一个重要的区别。 它们是散点图和线图,两者都显示两个数值变量 - 两个轴上的变量都是数值型的。 相比之下,条形图的一个轴上是类别,在另一个轴上具有数值型频率。 + +这对图表有影响。首先,每个条形的宽度和相邻条形之间的间隔完全取决于生成图的人,或者用于生成该图的程序。 Python 为我们做了这些选择。 如果你要手动绘制条形图,则可以做出完全不同的选择,并且仍然会是完全正确的条形图,前提是你使用相同宽度绘制了所有条形,并使所有间隔保持相同。 + +最重要的是,条形可以以任何顺序绘制。 “巧克力”,“香草”和“草莓”这些类别没有普遍的等级顺序,不像数字`5, 7`和`10`。 + +这意味着我们可以绘制一个易于解释的条形图,方法是按降序重新排列条形图。 为了实现它,我们首先按照`Number of Cartons`的降序,重新排列`icecream `的行,然后绘制条形图。 + +```py +icecream.sort('Number of Cartons', descending=True).barh('Flavor') +``` + +![](img/6-10.png) + +这个条形图包含的信息和以前的完全一样,但是它更容易阅读。 虽然在只读三个条形的情况下,这并不是一个巨大的收益,但是当分类数量很大时,这可能是相当重要的。 + +## 组合分类数据 + +为了构造`icecream`表,有人不得不查看 30 个冰淇淋盒子,并计算每种口味的数量。 但是,如果我们的数据还没有包含频率,我们必须在绘制条形图之前计算频率。 这是一个例子,其中它是必要的。 + +`top`表由美国历史上最畅销的电影组成。 第一列包含电影的标题;《星球大战:原力觉醒》(Star Wars: The Force Awakens)排名第一,美国票房总额超过 9 亿美元。 第二列包含制作电影的工作室的名称。 第三列包含国内票房收入(美元),第四列包含按 2016 年价格计算的,票面总收入。 第五列包含电影的发行年份。 + +列表中有 200 部电影。 根据未调整的总收入,这是前十名。 + +```py +top = Table.read_table('top_movies.csv') +top +``` + +| Title | Studio | Gross | Gross (Adjusted) | Year | +| --- | --- | --- | --- | --- | +| Star Wars: The Force Awakens | Buena Vista (Disney) | 906723418 | 906723400 | 2015 | +| Avatar | Fox | 760507625 | 846120800 | 2009 | +| Titanic | Paramount | 658672302 | 1178627900 | 1997 | +| Jurassic World | Universal | 652270625 | 687728000 | 2015 | +| Marvel's The Avengers | Buena Vista (Disney) | 623357910 | 668866600 | 2012 | +| The Dark Knight | Warner Bros. | 534858444 | 647761600 | 2008 | +| Star Wars: Episode I - The Phantom Menace | Fox | 474544677 | 785715000 | 1999 | +| Star Wars | Fox | 460998007 | 1549640500 | 1977 | +| Avengers: Age of Ultron | Buena Vista (Disney) | 459005868 | 465684200 | 2015 | +| The Dark Knight Rises | Warner Bros. | 448139099 | 500961700 | 2012 | + +(省略了 190 行) + +迪斯尼的子公司布埃纳维斯塔(Buena Vista)就像福克斯(Fox)和华纳兄弟(Warner Brothers)一样,经常出现在前十名中 如果我们从 200 行中看,哪个工作室最常出现? + +为了解决这个问题,首先要注意的是,我们需要的只是一个拥有电影和工作室的表格;其他信息是不必要的。 + +```py +movies_and_studios = top.select('Title', 'Studio') +``` + +`Table`的`group`方法组允许我们,通过将每个工作室当做一个类别,并将每一行分配给一个类别,来计算每个工作室出现在表中的频率。 `group`方法将包含类别的列标签作为其参数,并返回每个类别中行数量的表格。 数量列始终称为`count`,但如果你希望的话,则可以使用`relabeled`更改该列。 + +```py +movies_and_studios.group('Studio') +``` + +| Studio | count | +| --- | --- | +| AVCO | 1 | +| Buena Vista (Disney) | 29 | +| Columbia | 10 | +| Disney | 11 | +| Dreamworks | 3 | +| Fox | 26 | +| IFC | 1 | +| Lionsgate | 3 | +| MGM | 7 | +| MPC | 1 | + + +(省略了 14 行) + +因此,`group`创建一个分布表,显示电影在类别(工作室)之间如何分布。 + +现在我们可以使用这个表格,以及我们上面获得的图形技能来绘制条形图,显示前 200 个最高收入的电影中,哪个工作室是最常见的。 + +```py +studio_distribution = movies_and_studios.group('Studio') +studio_distribution.sort('count', descending=True).barh('Studio') +``` + +![](img/6-11.png) + +华纳兄弟(Warner Brothers)和布埃纳维斯塔(Buena Vista)是前 200 电影中最常见的工作室。 华纳兄弟制作了哈利波特电影,布埃纳维斯塔制作了星球大战。 + +由于总收入以未经调整的美元来衡量,所以最近几年的顶级电影比过去几十年更频繁,这并不令人惊讶。 以绝对数量来看,现在的电影票价比以前更高,因此总收入也更高。 这是通过条形图证明的,这些条形图显示了 200 部电影的发行年份。 + +```py +movies_and_years = top.select('Title', 'Year') +movies_and_years.group('Year').sort('count', descending=True).barh('Year') +``` + +![](img/6-12.png) + +所有最长的条形都对应 2000 年以后的年份。这与我们的观察一致,即最近几年应该是最频繁的。 + +### 面向数值变量 + +这张图有一些未解决的地方。 虽然它确实回答了这个问题,200 部最受欢迎的电影中,最常见的发行年份,但并没有按时间顺序列出所有年份。 它将年作为一个分类变量。 + +但是,年份是固定的时序单位,确实拥有顺序。 他们也有相对于彼此的固定的数值距离。 让我们看看当我们试图考虑它的时候会发生什么。 + +默认情况下,`barh`将类别(年)从最低到最高排序。 所以我们将运行这个代码,但不按`count`进行排序。 + +```py +movies_and_years.group('Year').barh('Year') +``` + +![](img/6-13.png) + +现在年份是升序了。 但是这个条形图还是有点问题。 1921 年和 1937 年的条形与 1937 年和 1939 年的条形相距甚远。条形图并没有显示出,200 部电影中没有一部是在 1922 年到 1936 年间发布的。基于这种可视化,这种不一致和遗漏,使早期年份的分布难以理解。 + +条形图用做类别变量的可视化。 当变量是数值,并且我们创建可视化时,必须考虑其值之间的数值关系。 这是下一节的主题。 + +## 数值分布 + +### 可视化数值分布 + +数据科学家研究的许多变量是定量的或数值的。它们的值是你可以做算术的数字。我们所看到的例子包括一本书的章节数量,电影的收入以及美国人的年龄。 + +类别变量的值可以按照数字编码,但是这不会使变量成为定量的。在我们研究的,按年龄组分类的人口普查数据的例子中,分类变量`SEX`中,`'Male'`的数字代码为`1`,`'Female'`的数字代码为`2`,以及分组`1`和`2`的合计为`0`。 `1`和`2`是数字,在这种情况下,从`2`中减`1`或取`0,1`和`2`的平均值,或对这三个值执行其他算术是没有意义的。 `SEX`是一个类别变量,即使这些值已经赋予一个数字代码。 + +对于我们的主要示例,我们将返回到我们在可视化分类数据时,所研究的数据集。这是一个表格,它由美国历史上最畅销的电影中的数据组成。为了方便起见,这里再次描述表格。 + +第一列包含电影的标题。第二列包含制作电影的工作室的名称。第三个包含国内票房总值(美元),第四个包含按 2016 年价格计算的票面收入总额。第五个包含电影的发行年份。 + +列表中有 200 部电影。根据`Gross`列中未调整的总收入,这是前十名。 + +```py +top = Table.read_table('top_movies.csv') +# Make the numbers in the Gross and Gross (Adjusted) columns look nicer: +top.set_format([2, 3], NumberFormatter) +``` + +| Title | Studio | Gross | Gross (Adjusted) | Year | +| --- | --- | --- | --- | --- | +| Star Wars: The Force Awakens | Buena Vista (Disney) | 906,723,418 | 906,723,400 | 2015 | +| Avatar | Fox | 760,507,625 | 846,120,800 | 2009 | +| Titanic | Paramount | 658,672,302 | 1,178,627,900 | 1997 | +| Jurassic World | Universal | 652,270,625 | 687,728,000 | 2015 | +| Marvel's The Avengers | Buena Vista (Disney) | 623,357,910 | 668,866,600 | 2012 | +| The Dark Knight | Warner Bros. | 534,858,444 | 647,761,600 | 2008 | +| Star Wars: Episode I - The Phantom Menace | Fox | 474,544,677 | 785,715,000 | 1999 | +| Star Wars | Fox | 460,998,007 | 1,549,640,500 | 1977 | +| Avengers: Age of Ultron | Buena Vista (Disney) | 459,005,868 | 465,684,200 | 2015 | +| The Dark Knight Rises | Warner Bros. | 448,139,099 | 500,961,700 | 2012 | + +(省略了 190 行) + +### 可视化调整后收入的分布 + +在本节中,我们将绘制`Gross (Adjusted)`列中数值变量的分布图。 为了简单起见,我们创建一个包含我们所需信息的小表。 而且由于三位数字比九位数字更容易处理,我们以百万美元衡量调整后的总收入。 注意如何使用舍入仅保留两位小数。 + +```py +millions = top.select(0).with_column('Adjusted Gross', + np.round(top.column(3)/1e6, 2)) +millions +``` + +| Title | Adjusted Gross | +| --- | --- | +| Star Wars: The Force Awakens | 906.72 | +| Avatar | 846.12 | +| Titanic | 1178.63 | +| Jurassic World | 687.73 | +| Marvel's The Avengers | 668.87 | +| The Dark Knight | 647.76 | +| Star Wars: Episode I - The Phantom Menace | 785.72 | +| Star Wars | 1549.64 | +| Avengers: Age of Ultron | 465.68 | +| The Dark Knight Rises | 500.96 | + +### 直方图 + +数值数据集的直方图看起来非常像条形图,虽然它有一些重要的差异,我们将在本节中讨论。 首先,我们只画出调整后收入的直方图。 + +hist方法生成列中值的直方图。 可选的单位参数用于两个轴上的标签。 直方图显示调整后的总额分布,以百万美元为单位。 + +```py +millions.hist('Adjusted Gross', unit="Million Dollars") +``` + +![](img/6-14.png) + +### 横轴 + +这些金额已被分组为连续的间隔,称为桶。尽管在这个数据集中,没有电影正好在两个桶之间的边缘上,但是`hist`必须考虑数值可能在边缘的情况。所以`hist`有一个端点约定:`bin`包含左端点的数据,但不包含右端点的数据。 + +我们使用符号`[a, b)`表示从`a`开始并在`b`结束但不包括`b`的桶。 + +有时,必须在第一个或最后一个箱中进行调整,以确保包含变量的最小值和最大值。在前面研究的人口普查数据中,你看到了一个这样的调整的例子,其中“100”岁的年龄实际上意味着“100 岁以上”。 + +我们可以看到,有 10 个桶(有些桶很低,难以看到),而且它们的宽度都是一样的。我们也可以看到,没有一部电影的收入不到三亿美元,那是因为我们只考虑有史以来最畅销的电影。 + +准确看到桶的末端在哪里,有点困难。例如,精确地确定值 500 位于横轴上的位置并不容易。所以很难判断一个条形的结束位置和下一个条形的开始位置。 + +可选参数`bins`可以与`hist`一起使用来指定桶的端点。它必须由一系列数字组成,这些数字以第一个桶的左端开始,以最后一个桶的右端结束。我们首先将桶中的数字设置为`300,400,500`等等,以`2000`结尾。 + +```py +millions.hist('Adjusted Gross', bins=np.arange(300,2001,100), unit="Million Dollars") +``` + +![](img/6-15.png) + +这个图的横轴比较容易阅读。 标签`200,400,600`等以对应的值为中心。 最高的条形是对应三亿到四亿美元之间的电影。 + +少数电影投入了 8 亿美元甚至更多。 这导致这个数字“向右倾斜”,或者更不正式地说,“右侧长尾”。 大量人口的收入或租金等变量的分布也经常具有这种形式。 + +### 桶的数量 + +可以使用`bin`方法从一个表格中计算出桶中的值的数量,该方法接受列标签或索引,以及可选的序列或桶的数量。 结果是直方图的表格形式。 第一列列出了桶的左端点(但请参阅下面关于最终值的注释)。 第二列包含`Adjusted Gross`列中所有值在相应桶中的数量。 也就是说,它计数所有`Adjusted Gross`的所有值,它们大于或等于`bin`中的值,但小于下一个`bin`中的值。 + +```py +bin_counts = millions.bin('Adjusted Gross', bins=np.arange(300,2001,100)) +bin_counts.show() +``` + +| bin | Adjusted Gross count | +| --- | --- | +| 300 | 81 | +| 400 | 52 | +| 500 | 28 | +| 600 | 16 | +| 700 | 7 | +| 800 | 5 | +| 900 | 3 | +| 1000 | 1 | +| 1100 | 3 | +| 1200 | 2 | +| 1300 | 0 | +| 1400 | 0 | +| 1500 | 1 | +| 1600 | 0 | +| 1700 | 1 | +| 1800 | 0 | +| 1900 | 0 | +| 2000 | 0 | + +注意最后一行的`bin`值 2000。 这不是任何条形的左端点 - 这是最后一个条形的右端点。 按照端点约定,那里的数据不包括在内。 因此,相应的计数记录为 0,并且即使已经有超过二十亿美元的电影也被记录为 0。 当`bin`或`hist`使用`bin`参数调用时,图只考虑在指定`bin`中的值。 + +一旦数值已经分入桶中,所得数量可以用来使用`bin_column`命名参数来生成直方图,以指定哪个列包含桶的下界。 + +```py +bin_counts.hist('Adjusted Gross count', bin_column='bin', unit='Million Dollars') +``` + +![](img/6-16.png) + +### 纵轴:密度刻度 + +一旦我们已经照顾到细节,如桶的末端,直方图的横轴易于阅读。 纵轴的特征需要更多关注。 我们会一一讲解。 + +我们先来看看如何计算垂直轴上的数字。 如果计算看起来有些奇怪,请耐心等待 - 本节的其余部分将解释原因。 + +计算。每个条形的高度是桶中的元素的百分比,除以桶的宽度。 + +> 译者注:存在很多种直方图,比如频数直方图、频率质量直方图和频率密度直方图。它们的纵轴数值不相同,但是图形形状是一样的。这里是最后一种,频率密度直方图。 + +```py +counts = bin_counts.relabeled('Adjusted Gross count', 'Count') +percents = counts.with_column( + 'Percent', (counts.column('Count')/200)*100 + ) +heights = percents.with_column( + 'Height', percents.column('Percent')/100 + ) +heights +``` + +| bin | Count | Percent | Height | +| --- | --- | --- | --- | +| 300 | 81 | 40.5 | 0.405 | +| 400 | 52 | 26 | 0.26 | +| 500 | 28 | 14 | 0.14 | +| 600 | 16 | 8 | 0.08 | +| 700 | 7 | 3.5 | 0.035 | +| 800 | 5 | 2.5 | 0.025 | +| 900 | 3 | 1.5 | 0.015 | +| 1000 | 1 | 0.5 | 0.005 | +| 1100 | 3 | 1.5 | 0.015 | +| 1200 | 2 | 1 | 0.01 | + +(省略了 8 行) + +在上面直方图的纵轴上查看数字,检查列高度是否正确。 + +如果我们只查看表格的第一行,计算就会变得清晰。 + +请记住,数据集中有 200 部电影。这个`[300,400)`的桶包含 81 部电影。这是所有电影的 40.5%:![](img/tex-6-1.gif)。 + +`[300, 400)`桶的宽度是`400-300 = 100`。所以 ![](img/tex-6-2.gif)。 + +用于计算高度的代码使用了总共​​有 200 个电影,以及每个箱的宽度是 100 的事实。 + +单位。条形的高度是 40.5% 除以 1 亿美元,因此高度是 0.405% 每百万美元。 + +这种绘制直方图的方法创建了一个垂直轴,它是在密度刻度上的。条形的高度不是桶中条目的百分比;它是桶中的条目除以桶的宽度。这就是为什么高度衡量拥挤度或密度。 + +让我们看看为什么这很重要。 + +### 不等的桶 + +直方图相比条形图的一个优点是,直方图可以包含不等宽度的桶。 以下将`Millions `中的值分为三个不均匀的类别。 + +```py +uneven = make_array(300, 400, 600, 1500) +millions.hist('Adjusted Gross', bins=uneven, unit="Million Dollars") +``` + +![](img/6-17.png) + +这里是三个桶中的数量。 + +```py +millions.bin('Adjusted Gross', bins=uneven) +``` + + +| bin | Adjusted Gross count | +| --- | --- | +| 300 | 81 | +| 400 | 80 | +| 600 | 37 | +| 1500 | 0 | + +虽然范围`[300,400)`和`[400,600)`具有几乎相同的计数,但前者的高度是后者的两倍,因为它只有一半的宽度。 `[300,400)`中的值的密度是`[400,600)`中的密度的两倍。 + +直方图帮助我们可视化数轴上数据最集中的地方,特别是当桶不均匀的时候。 + +### 仅仅绘制数量的问题 + + +可以使用`hist`方法的`normed=False`选项直接在图表中显示数量。 生成的图表与直方图具有相同的形状,但这些桶的宽度均相等,尽管纵轴上的数字不同。 + +```py +millions.hist('Adjusted Gross', bins=np.arange(300,2001,100), normed=False) +``` + +![](img/6-18.png) + +虽然数量刻度可能比密度刻度更自然,但当桶宽度不同时,图表高度的有误导性。 下面看起来(由于计数)高收入电影相当普遍,事实上我们已经看到它们相对较少。 + +```py +millions.hist('Adjusted Gross', bins=uneven, normed=False) +``` + +![](img/6-19.png) + +即使使用的方法被称为`hist`,上面的图不是一个直方图。 误导性地夸大了至少 6 亿美元的电影比例。 每个桶的高度只是按照桶中的电影数量绘制,而不考虑桶宽度的差异。 + +如果最后两个桶组合起来,情况就变得更加荒谬了。 + +```py +very_uneven = make_array(300, 400, 1500) +millions.hist('Adjusted Gross', bins=very_uneven, normed=False) +``` + +![](img/6-20.png) + +在这个基于数量的图像中,电影分布完全失去了形状。 + +### 直方图:通用原则和计算 + +上图显示,眼睛将面积视为“较大”的东西,而不是高度。当桶的宽度不同时,这种观察变得尤为重要。 + +这就是直方图具有两个定义属性的原因: + ++ 桶按比例绘制并且是连续的(尽管有些可能是空的),因为横轴上的值是数值型的。 ++ 每个条形的面积与桶中的条目数成比例。 + +属性(2)是绘制直方图的关键,通常实现如下: + +``` +条形的面积 = 桶中条目的百分比 +``` + +高度的计算仅仅使用了一个事实,条形是长方形的。 + +``` +条形的面积 = 条形的高度 * 桶的宽度 +``` + +因此, + +``` +条形的高度 = 条形的面积 / 桶的宽度 + = 桶中条目的百分比 / 桶的宽度 +``` + +高度的单位是“百分比每横轴单位”。 + +当使用这种方法绘制时,直方图被称为在密度刻度上绘制。 在这个刻度上: + ++ 每个条形的面积等于相应桶中的数据值的百分比。 ++ 直方图中所有条形的总面积为 100%。 从比例的角度来讲,我们说直方图中所有条形的面积“总计为 1”。 + +### 平顶和细节水平 + +即使密度刻度使用面积正确表示了百分比,但是通过将值分组到桶中,丢失了一些细节。 + +再看一下下图中的`[300,400)`的桶。 值为“0.405% 每百万美元”的桶的平顶,隐藏了电影在这个桶分布有些不均匀的事实。 + +```py +millions.hist('Adjusted Gross', bins=uneven, unit="Million Dollars") +``` + +![](img/6-21.png) + +为了看到它,让我们将`[300, 400)`划分为更窄的 10 个桶。每个桶的宽度都是一千万美元。 + +```py +some_tiny_bins = make_array(300, 310, 320, 330, 340, 350, 360, 370, 380, 390, 400, 600, 1500) +millions.hist('Adjusted Gross', bins=some_tiny_bins, unit='Million Dollars') +``` + +![](img/6-22.png) + +### 直方图 Q&A + +让我们再画一遍直方图,这次只有四个桶,检查我们对概念的理解。 + +```py +uneven_again = make_array(300, 350, 400, 450, 1500) +millions.hist('Adjusted Gross', bins=uneven_again, unit='Million Dollars') + +millions.bin('Adjusted Gross', bins=uneven_again) +``` + +![](img/6-23.png) + +| bin | Adjusted Gross count | +| --- | --- | +| 300 | 32 | +| 350 | 49 | +| 400 | 25 | +| 450 | 92 | +| 1500 | 0 | + +再次查看直方图,并将`[400,450)`的桶与`[450,1500)`桶进行比较。 + +问:哪个桶里面有更多的电影? + +答:`[450,1500)`的桶。它有 92 部电影,而`[400,450)`桶中有 25 部电影。 + +问:那么为什么`[450,1500)`的桶比`[400,450)`桶短得多呢? + +答:因为高度代表桶里每单位空间的密度,而不是桶里的电影数量。 `[450,1500)`的桶中的电影确实比`[400,450)`的桶多,但它也是一个大桶。 所以它不那么拥挤。 其中的电影密度要低得多。 + +### 条形图和直方图的区别 + ++ 条形图为每个类别展示一个数量。 它们通常用于显示类别变量的分布。 直方图显示定量变量的分布。 ++ 条形图中的所有条形都具有相同的宽度,相邻的条形之间有相等的间距。 直方图的条形可以具有不同的宽度,并且是连续的。 ++ 条形图中条形的长度(或高度,如果垂直绘制)与每个类别的值成正比。 直方图中条形的高度是密度的度量;直方图中的条形的面积与桶中的条目数量成正比。 + +## 重叠的图表 + +在这一章中,我们学习了如何通过绘制图表来显示数据。 这种可视化的常见用法是比较两个数据集。 在本节中,我们将看到如何叠加绘图,即将它们绘制在单个图形中,拥有同一对坐标轴 + +为了使重叠有意义,重叠的图必须表示相同的变量并以相同的单位进行测量。 + +为了绘制重叠图,可以用相同的方法调用`scatter`,`plot`和`barh`方法。 对于`scatter`和`plot`,一列必须作为所有叠加图的公共横轴。 对于`barh`,一列必须作为一组类别的公共轴。 一般的调用看起来像这样: + +```py +name_of_table.method(column_label_of_common_axis, array_of_labels_of_variables_to_plot) +``` + +更常见的是,我们首先仅仅选取图表所需的列。之后通过指定共同轴上的变量来调用方法。 + +```py +name_of_table.method(column_label_of_common_axis) +``` + +### 散点图 + +高尔顿(Franics Galton,1822 ~ 1911 年)是一位英国博学家,他是分析数值变量之间关系的先驱。 他对有争议的优生学领域特别感兴趣,实际上,他创造了这个术语 - 这涉及到如何将物理特征从一代传到下一代。 + +高尔顿精心收集了大量的数据,其中一些我们将在本课程中分析。 这是高尔顿的,有关父母及其子女身高的数据的子集。 具体来说,数据由 179 名男性组成,他们在家庭中第一个出生。数据是他们自己的高度和父母的高度。所有的高度都是以英寸来测量的。 + +```py +heights = Table.read_table('galton_subset.csv') +heights +``` + +| father | mother | son | +| --- | --- | --- | +| 78.5 | 67 | 73.2 | +| 75.5 | 66.5 | 73.5 | +| 75 | 64 | 71 | +| 75 | 64 | 70.5 | +| 75 | 58.5 | 72 | +| 74 | 68 | 76.5 | +| 74 | 62 | 74 | +| 73 | 67 | 71 | +| 73 | 67 | 68 | +| 73 | 66.5 | 71 | + +(省略了 169 行) + +`scatter`方法使我们能够可视化,儿子的身高如何与父母的身高有关。 在图中,儿子的身高将形成公共的横轴。 + +```py +heights.scatter('son') +``` + +![](img/6-24.png) + +注意我们仅仅指定了公共的横轴上的变量(儿子的身高)。 Python 绘制了两个散点图:这个变量和另外两个之间的关系,每个关系一个。 + +金色和蓝色的散点图向上倾斜,并显示出儿子的高度和父母的高度之间的正相关。 蓝色(父亲)的绘图一般比金色高,因为父亲一般比母亲高。 + +### 线形图 + +我们的下一个例子涉及更近的儿童数据。 我们将返回到人口普查数据表`us_pop`,再次在下面创建用于参考。 由此,我们将提取 0 至 18 岁年龄段的所有儿童的数量。 + +```py +# Read the full Census table +census_url = 'http://www2.census.gov/programs-surveys/popest/datasets/2010-2015/national/asrh/nc-est2015-agesex-res.csv' +full_census_table = Table.read_table(census_url) + +# Select columns from the full table and relabel some of them +partial_census_table = full_census_table.select(['SEX', 'AGE', 'POPESTIMATE2010', 'POPESTIMATE2014']) +us_pop = partial_census_table.relabeled('POPESTIMATE2010', '2010').relabeled('POPESTIMATE2014', '2014') + +# Access the rows corresponding to all children, ages 0-18 +children = us_pop.where('SEX', are.equal_to(0)).where('AGE', are.below(19)).drop('SEX') +children.show() +``` + +| AGE | 2010 | 2014 | +| --- | --- | --- | +| 0 | 3951330 | 3949775 | +| 1 | 3957888 | 3949776 | +| 2 | 4090862 | 3959664 | +| 3 | 4111920 | 4007079 | +| 4 | 4077551 | 4005716 | +| 5 | 4064653 | 4006900 | +| 6 | 4073013 | 4135930 | +| 7 | 4043046 | 4155326 | +| 8 | 4025604 | 4120903 | +| 9 | 4125415 | 4108349 | +| 10 | 4187062 | 4116942 | +| 11 | 4115511 | 4087402 | +| 12 | 4113279 | 4070682 | +| 13 | 4119666 | 4171030 | +| 14 | 4145614 | 4233839 | +| 15 | 4231002 | 4164796 | +| 16 | 4313252 | 4168559 | +| 17 | 4376367 | 4186513 | +| 18 | 4491005 | 4227920 | + +现在我们可以绘制两个叠加的线形图,显示 2010 年和 2014 年的不同年龄的儿童人数。方法调用类似于前面例子中的`scatter`调用。 + +```py +children.plot('AGE') +``` + +![](img/6-25.png) + +在这个刻度上,重要的是要记住我们只有`0,1,2`岁等等的数据。 两个图形的点相互“交织”。 + +这些图表在一些地方相互交叉:例如,2010 年的 4 岁人数比 2014 年多,2014 年的 14 岁人数比 2010 年多。 + +当然,2014 年的 14 岁儿童大部分都是 2010 年的 10 岁儿童。为了看到这一点,请查看 14 岁的金色图表和 10 岁的蓝色图表。事实上,你会注意到,整个金色图表(2014 年)看起来像蓝色图表(2010 年)向右滑了 4 年。 由于 2010 年至 2014 年间进入该国的儿童的净效应,这个下滑幅度还是有所上升, 幸运的是,在这些年代,没有太多的生命损失。 + +### 条形图 + +对于本节的最后一个例子,我们看看加利福尼亚州以及整个美国的成人和儿童的种族分布情况。 + +凯撒家庭基金会根据人口普查数据,编制了美国人口种族分布情况。基金会的网站提供了 2014 年整个美国人口以及当年 18 岁以下的美国儿童的数据汇总。 + +这里是一个表格,采用了美国和加利福尼亚州的数据。 这些列代表美国和加利福尼亚州的每个人,美国和加州的儿童。 表格的主体包含不同类别的比例。 每一列显示了,该列对应的人群的种族分布。 所以在每一列中,条目总计为 1。 + +```py +usa_ca = Table.read_table('usa_ca_2014.csv') +usa_ca +``` + + +| Ethnicity | USA All | CA All | USA Children | CA Children | +| --- | --- | --- | --- | --- | +| Black | 0.12 | 0.05 | 0.14 | 0.05 | +| Hispanic | 0.18 | 0.38 | 0.24 | 0.5 | +| White | 0.62 | 0.39 | 0.52 | 0.29 | +| Other | 0.08 | 0.18 | 0.1 | 0.16 | + +我们自然想要比较这些分布。 直接比较列是有意义的,因为所有条目都是比例,因此在相同刻度上。 + +`barh`方法允许我们通过在相同轴域上绘制多个条形图,将比较可视化。这个调用类似于`scatter`和`plot`:我们必须指定类别的公共轴。 + +> 译者注:轴域(Axes)是横轴和纵轴围城的区域。 + +```py +usa_ca.barh('Ethnicity') +``` + +![](img/6-26.png) + +虽然绘制叠加的条形图非常简单,但是我们可以在这个图表上找到太多的信息,以便能够理清种群之间的相似性和差异性。 似乎很清楚的是,美国所有人和美国儿童的种族分布比任何其他列都更相似,但是一次比较一对要容易得多。 + +首先比较美国和加利福尼亚的整个人口。 + +```py +usa_ca.select('Ethnicity', 'USA All', 'CA All').barh('Ethnicity') +``` + +![](img/6-27.png) + +这两个分布是完全不同的。 加利福尼亚州的拉美裔和其他类别比例较高,黑人和白人比例相应较低。 这种差异主要是由于,加利福尼亚州的地理位置和移民模式,无论是历史上还是近几十年来。 例如,加利福尼亚的“其他”类别包括相当一部分亚洲人和太平洋岛民。 + +从图中可以看出,2014 年加州近 40% 的人口是拉美裔。 与该州儿童人口的比较表明,未来几年拉美裔人口的比例可能会更高。 在加州儿童中,50% 属于拉美裔。 + +```py +usa_ca.select('Ethnicity', 'CA All', 'CA Children').barh('Ethnicity') +``` + +![](img/6-28.png) + +更复杂的数据集自然会产生各种有趣的可视化效果,包括不同种类的重叠图形。 为了分析这些数据,获得更多的数据操作技能的有帮助的,这样我们就可以将数据转化为一种形式,使我们能够使用本节中的方法。 在下一章中,我们将介绍其中的一些技巧。 diff --git a/docs/data8-textbook-zh/7.md b/docs/data8-textbook-zh/7.md new file mode 100644 index 0000000000000000000000000000000000000000..02e0d2c5225ce2daa9a0d0ecbe61a4a8557ce920 --- /dev/null +++ b/docs/data8-textbook-zh/7.md @@ -0,0 +1,1452 @@ +# 七、函数和表格 + +> 原文:[Functions and Tables](https://github.com/data-8/textbook/tree/gh-pages/chapters/07) + +> 译者:[飞龙](https://github.com/wizardforcel) + +> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) + +> 自豪地采用[谷歌翻译](https://translate.google.cn/) + +通过使用 Python 中已有的函数,我们正在建立一个使用的技术清单,用于识别数据集中的规律和主题。 现在我们将探索Python编程语言的核心功能:函数定义。 + +我们在本书中已经广泛使用了函数,但从未定义过我们自己的函数。定义一个函数的目的是,给一个计算过程命名,它可能会使用多次。计算中有许多需要重复计算的情况。 例如,我们常常希望对表的列中的每个值执行相同的操作。 + +## 定义函数 + +`double`函数的定义仅仅使一个数值加倍。 + +```py +# Our first function definition + +def double(x): + """ Double x """ + return 2*x +``` + +我们通过编写`def`来开始定义任何函数。 下面是这个小函数的其他部分(语法)的细分: + +![](img/7-1.jpg) + +当我们运行上面的单元格时,没有使特定的数字加倍,并且`double`主体中的代码还没有求值。因此,我们的函数类似于一个菜谱。 每次我们遵循菜谱中的指导,我们都需要以食材开始。 每次我们想用我们的函数来使一个数字加倍时,我们需要指定一个数字。 + +我们可以用和调用其他函数完全相同的方式,来调用`double`。 每次我们这样做的时候,主体中的代码都会执行,参数的值赋给了名称`x`。 + +```py +double(17) +34 +double(-0.6/4) +-0.3 +``` + +以上两个表达式都是调用表达式。 在第二个里面,计算了表达式`-0.6 / 4`的值,然后将其作为参数`x`传递给`double`函数。 每个调用表达式最终都会执行`double`的主体,但使用不同的`x`值。 + +`double`的主体只有一行: + +```py +return 2*x +``` + +执行这个`return`语句会完成`double`函数体的执行,并计算调用表达式的值。 + +`double`的参数可以是任何表达式,只要它的值是一个数字。 例如,它可以是一个名称。 `double`函数不知道或不在意如何计算或存储参数。 它唯一的工作是,使用传递给它的参数的值来执行它自己的主体。 + +```py +any_name = 42 +double(any_name) +84 +``` + +参数也可以是任何可以加倍的值。例如,可以将整个数值数组作为参数传递给`double`,结果将是另一个数组。 + +```py +double(make_array(3, 4, 5)) +array([ 6, 8, 10]) +``` + +但是,函数内部定义的名称(包括像double的x这样的参数)只存在一小会儿。 它们只在函数被调用的时候被定义,并且只能在函数体内被访问。 我们不能在`double`之外引用`x`。 技术术语是`x`具有局部作用域。 + +因此,即使我们在上面的单元格中调用了`double`,名称`x`也不能在函数体外识别。 + +```py +x +--------------------------------------------------------------------------- +NameError Traceback (most recent call last) + in () +----> 1 x + +NameError: name 'x' is not defined +``` + +文档字符串。 虽然`double`比较容易理解,但是很多函数执行复杂的任务,并且没有解释就很难使用。 (你自己也可能已经发现了!)因此,一个组成良好的函数有一个唤起它的行为的名字,以及文档。 在 Python 中,这被称为文档字符串 - 描述了它的行为和对其参数的预期。 文档字符串也可以展示函数的示例调用,其中调用前面是`>>>`。 + +文档字符串可以是任何字符串,只要它是函数体中的第一个东西。 文档字符串通常在开始和结束处使用三个引号来定义,这允许字符串跨越多行。 第一行通常是函数的完整但简短的描述,而下面的行则为将来的用户提供了进一步的指导。 + +下面是一个名为`percent`的函数定义,它带有两个参数。定义包括一个文档字符串。 + +```py +# A function with more than one argument + +def percent(x, total): + """Convert x to a percentage of total. + + More precisely, this function divides x by total, + multiplies the result by 100, and rounds the result + to two decimal places. + + >>> percent(4, 16) + 25.0 + >>> percent(1, 6) + 16.67 + """ + return round((x/total)*100, 2) +percent(33, 200) +16.5 +``` + +将上面定义的函数`percent`与下面定义的函数`percents`进行对比。 后者以数组为参数,将数组中的所有数字转换为数组中所有值的百分数。 百分数都四舍五入到两位,这次使用`round`来代替`np.round`,因为参数是一个数组而不是一个数字。 + +```py +def percents(counts): + """Convert the values in array_x to percents out of the total of array_x.""" + total = counts.sum() + return np.round((counts/total)*100, 2) +``` + +函数`percents`返回一个百分数的数组,除了四舍五入之外,它总计是 100。 + +```py +some_array = make_array(7, 10, 4) +percents(some_array) +array([ 33.33, 47.62, 19.05]) +``` + +理解 Python 执行函数的步骤是有帮助的。 为了方便起见,我们在下面的同一个单元格中放入了函数定义和对这个函数的调用。 + +```py +def biggest_difference(array_x): + """Find the biggest difference in absolute value between two adjacent elements of array_x.""" + diffs = np.diff(array_x) + absolute_diffs = abs(diffs) + return max(absolute_diffs) + +some_numbers = make_array(2, 4, 5, 6, 4, -1, 1) +big_diff = biggest_difference(some_numbers) +print("The biggest difference is", big_diff) +The biggest difference is 5 +``` + +这就是当我们运行单元格时,所发生的事情。 + +![](img/7-2.jpg) + +## 多个参数 + +可以有多种方式来推广一个表达式或代码块,因此一个函数可以有多个参数,每个参数决定结果的不同方面。 例如,我们以前定义的百分比`percents`,每次都四舍五入到两位。 以下两个参的数定义允许不同调用四舍五入到不同的位数。 + +```py +def percents(counts, decimal_places): + """Convert the values in array_x to percents out of the total of array_x.""" + total = counts.sum() + return np.round((counts/total)*100, decimal_places) + +parts = make_array(2, 1, 4) +print("Rounded to 1 decimal place: ", percents(parts, 1)) +print("Rounded to 2 decimal places:", percents(parts, 2)) +print("Rounded to 3 decimal places:", percents(parts, 3)) +Rounded to 1 decimal place: [ 28.6 14.3 57.1] +Rounded to 2 decimal places: [ 28.57 14.29 57.14] +Rounded to 3 decimal places: [ 28.571 14.286 57.143] +``` + +这个新定义的灵活性来源于一个小的代价:每次调用该函数时,都必须指定小数位数。默认参数值允许使用可变数量的参数调用函数;在调用表达式中未指定的任何参数都被赋予其默认值,这在`def`语句的第一行中进行了说明。 例如,在`percents`的最终定义中,可选参数`decimal_places`赋为默认值`2`。 + +```py +def percents(counts, decimal_places=2): + """Convert the values in array_x to percents out of the total of array_x.""" + total = counts.sum() + return np.round((counts/total)*100, decimal_places) + +parts = make_array(2, 1, 4) +print("Rounded to 1 decimal place:", percents(parts, 1)) +print("Rounded to the default number of decimal places:", percents(parts)) +Rounded to 1 decimal place: [ 28.6 14.3 57.1] +Rounded to the default number of decimal places: [ 28.57 14.29 57.14] +``` + +## 注:方法 + +函数通过将参数表达式放入函数名称后面的括号来调用。 任何独立定义的函数都是这样调用的。 你也看到了方法的例子,这些方法就像函数一样,但是用点符号来调用,比如`some_table.sort(some_label)`。 你定义的函数将始终首先使用函数名称,并传入所有参数来调用。 + +## 在列上应用函数 + +我们已经看到很多例子,通过将函数应用于现有列或其他数组,来创建新的表格的列。 所有这些函数都以数组作为参数。 但是我们经常打算,通过一个函数转换列中的条目,它不将数组作为它的函数。 例如,它可能只需要一个数字作为它的参数,就像下面定义的函数`cut_off_at_100`。 + +```py +def cut_off_at_100(x): + """The smaller of x and 100""" + return min(x, 100) +cut_off_at_100(17) +17 +cut_off_at_100(117) +100 +cut_off_at_100(100) +100 +``` + +如果参数小于或等于 100,函数`cut_off_at_100`只返回它的参数。但是如果参数大于 100,则返回 100。 + +在我们之前使用人口普查数据的例子中,我们看到变量`AGE`的值为 100,表示“100 岁以上”。 以这种方式将年龄限制在 100 岁,正是`cut_off_at_100`所做的。 + +为了一次性对很多年龄使用这个函数,我们必须能够引用函数本身,而不用实际调用它。 类似地,我们可能会向厨师展示一个蛋糕的菜谱,并要求她用它来烤 6 个蛋糕。 在这种情况下,我们不会使用这个配方自己烘烤蛋糕, 我们的角色只是把菜谱给厨师。 同样,我们可以要求一个表格,在列中的 6 个不同的数字上调用`cut_off_at_100`。 + +首先,我们创建了一个表,一列是人,一列是它们的年龄。 例如,`C`是 52 岁。 + +```py +ages = Table().with_columns( + 'Person', make_array('A', 'B', 'C', 'D', 'E', 'F'), + 'Age', make_array(17, 117, 52, 100, 6, 101) +) +ages +``` + +| Person | Age | +| --- | --- | +| A | 17 | +| B | 117 | +| C | 52 | +| D | 100 | +| E | 6 | +| F | 101 | + +### 应用 + +要在 100 岁截断年龄,我们将使用一个新的`Table`方法。 `apply`方法在列的每个元素上调用一个函数,形成一个返回值的新数组。 为了指出要调用的函数,只需将其命名(不带引号或括号)。 输入值的列的名称必须是字符串,仍然出现在引号内。 + +```py +ages.apply(cut_off_at_100, 'Age') +array([ 17, 100, 52, 100, 6, 100]) +``` + +我们在这里所做的是,将`cut_off_at_100`函数应用于`age`表的`Age`列中的每个值。 输出是函数的相应返回值的数组。 例如,17 还是 17,117 变成了 100,52 还是 52,等等。 + +此数组的长度与`age`表中原始`Age`列的长度相同,可用作名为`Cut Off Age`的新列中的值,并与现有的`Person`和`Age`列共存。 + + +```py +ages.with_column( + 'Cut Off Age', ages.apply(cut_off_at_100, 'Age') +) +``` + +| Person | Age | Cut Off Age | +| --- | --- | --- | +| A | 17 | 17 | +| B | 117 | 100 | +| C | 52 | 52 | +| D | 100 | 100 | +| E | 6 | 6 | +| F | 101 | 100 | + +### 作为值的函数 + +我们已经看到,Python 有很多种值。 例如,`6`是一个数值,`"cake"`是一个文本值,`Table()`是一个空表,`age`是一个表值(因为我们在上面定义)的名称。 + +在 Python 中,每个函数(包括`cut_off_at_100`)也是一个值。 这有助于再次考虑菜谱。 蛋糕的菜谱是一个真实的东西,不同于蛋糕或配料,你可以给它一个名字,像“阿尼的蛋糕菜谱”。 当我们用`def`语句定义`cut_off_at_100`时,我们实际上做了两件事情:我们创建了一个函数来截断数字 100,我们给它命名为`cut_off_at_100`。 + +我们可以引用任何函数,通过写下它的名字,而没有实际调用它必需的括号或参数。当我们在上面调用`apply`时,我们做了这个。 当我们自己写下一个函数的名字,作为单元格中的最后一行时,Python 会生成一个函数的文本表示,就像打印一个数字或一个字符串值一样。 + +```py +cut_off_at_100 + +``` + +请注意,我们没有使用引号(它只是一段文本)或`cut_off_at_100()`(它是一个函数调用,而且是无效的)。我们只是写下`cut_off_at_100`来引用这个函数。 + +就像我们可以为其他值定义新名称一样,我们可以为函数定义新名称。 例如,假设我们想把我们的函数称为`cut_off`,而不是`cut_off_at_100`。 我们可以这样写: + +```py +cut_off = cut_off_at_100 +``` +现在`cut_off`就是函数名称了。它是`cut_off_at_100`的相同函数。所以打印出的值应该相同。 + +```py +cut_off + +``` + +让我们看看另一个`apply`的应用。 + +### 示例:预测 + +数据科学经常用来预测未来。 如果我们试图预测特定个体的结果 - 例如,她将如何回应处理方式,或者他是否会购买产品,那么将预测基于其他类似个体的结果是很自然的。 + +查尔斯·达尔文(Charles Darwin)的堂兄弗朗西斯·高尔顿(Sir Francis Galton)是使用这个思想来基于数值数据进行预测的先驱。 他研究了物理特征是如何传递下来的。 + +下面的数据是父母和他们的成年子女的身高测量值,由高尔顿仔细收集。 每行对应一个成年子女。 变量是家庭的数字代码,父母的身高(以英寸为单位),“双亲身高”,这是父母双方身高的加权平均值 [1],家庭中子女的数量 ,以及子女的出生次序(第几个),性别和身高。 + +> [1] 高尔顿在计算男性和女性的平均身高之前,将女性身高乘上 1.08。对于这个的讨论,请查看 [Chance](http://chance.amstat.org/2013/09/1-pagano/),这是一个由美国统计协会出版的杂志。 + +```py +# Galton's data on heights of parents and their adult children +galton = Table.read_table('galton.csv') +galton +``` + + +| family | father | mother | midparentHeight | children | childNum | gender | childHeight | +| --- | --- | --- | --- | --- | --- | --- | --- | +| 1 | 78.5 | 67 | 75.43 | 4 | 1 | male | 73.2 | +| 1 | 78.5 | 67 | 75.43 | 4 | 2 | female | 69.2 | +| 1 | 78.5 | 67 | 75.43 | 4 | 3 | female | 69 | +| 1 | 78.5 | 67 | 75.43 | 4 | 4 | female | 69 | +| 2 | 75.5 | 66.5 | 73.66 | 4 | 1 | male | 73.5 | +| 2 | 75.5 | 66.5 | 73.66 | 4 | 2 | male | 72.5 | +| 2 | 75.5 | 66.5 | 73.66 | 4 | 3 | female | 65.5 | +| 2 | 75.5 | 66.5 | 73.66 | 4 | 4 | female | 65.5 | +| 3 | 75 | 64 | 72.06 | 2 | 1 | male | 71 | +| 3 | 75 | 64 | 72.06 | 2 | 2 | female | 68 | + +(省略了 924 行) + +收集数据的主要原因是,能够预测父母所生的子女的成年身高,其中父母和数据集中的类似。让我们尝试这样做,用双亲的身高作为我们预测的基础变量。 因此双亲的身高是我们的预测性变量。 + +表格`heights`包含双亲和子女的身高。 两个变量的散点图显示了正相关,正如我们对这些变量的预期。 + +```py +heights = galton.select(3, 7).relabeled(0, 'MidParent').relabeled(1, 'Child') +heights +``` + +| MidParent | Child | +| --- | --- | +| 75.43 | 73.2 | +| 75.43 | 69.2 | +| 75.43 | 69 | +| 75.43 | 69 | +| 73.66 | 73.5 | +| 73.66 | 72.5 | +| 73.66 | 65.5 | +| 73.66 | 65.5 | +| 72.06 | 71 | +| 72.06 | 68 | + +(省略了 924 行) + +```py +heights.scatter(0) +``` + +![](img/7-3.png) + +现在假设高尔顿遇到了新的一对夫妇,与他的数据集类似,并且想知道他们的子女有多高。考虑到双亲身高是 68 英寸,他预测子女身高的一个好方法是什么? + +一个合理的方法是基于约 68 英寸的双亲身高对应的所有点,来做预测。预测值等于从这些点计算的子女身高的均值。 + +假设我们是高尔顿,并执行这个计划。现在我们只是对“68 英寸左右”的含义做一个合理的定义,并用它来处理。在课程的后面,我们将研究这种选择的后果。 + +我们的“接近”的意思是“在半英寸之内”。下图显示了 67.5 英寸和 68.5 英寸之间的双亲身高对应的所有点。这些都是红色直线之间的点。每一个点都对应一个子女;我们对新夫妇的子女身高的预测是所有子女的平均身高。这由金色的点表示。 + +忽略代码,仅仅专注于理解到达金色的点的心理过程。 + +```py +heights.scatter('MidParent') +_ = plots.plot([67.5, 67.5], [50, 85], color='red', lw=2) +_ = plots.plot([68.5, 68.5], [50, 85], color='red', lw=2) +_ = plots.scatter(68, 66.24, color='gold', s=40) +``` + +![](img/7-4.png) + +为了准确计算出金色的点的位置,我们首先需要确定直线之间的所有点。 这些点对应于`MidParent`在 67.5 英寸和 68.5 英寸之间的行。 + +```py +close_to_68 = heights.where('MidParent', are.between(67.5, 68.5)) +close_to_68 +``` + + +| MidParent | Child | +| --- | --- | +| 68.44 | 62 | +| 67.94 | 71.2 | +| 67.94 | 67 | +| 68.33 | 62.5 | +| 68.23 | 73 | +| 68.23 | 72 | +| 68.23 | 69 | +| 67.98 | 73 | +| 67.98 | 71 | +| 67.98 | 71 | + +(省略了 121 行) + +双亲身高为 68 英寸的子女的预测身高,是这些行中子女的平均身高。 这是 66.24 英寸。 + +```py +close_to_68.column('Child').mean() +66.24045801526718 +``` + + +我们现在有了一种方法,给定任何数据集中的双亲身高,就可以预测子女的身高。我们可以定义一个函数`predict_child`来实现它。 除了名称的选择之外,函数的主体由上面两个单元格中的代码组成。 + +```py +def predict_child(mpht): + """Predict the height of a child whose parents have a midparent height of mpht. + + The prediction is the average height of the children whose midparent height is + in the range mpht plus or minus 0.5. + """ + + close_points = heights.where('MidParent', are.between(mpht-0.5, mpht + 0.5)) + return close_points.column('Child').mean() +``` + +给定 68 英寸的双亲身高,函数`predict_child`返回与之前相同的预测(66.24 英寸)。 定义函数的好处在于,我们可以很容易地改变预测变量的值,并得到一个新的预测结果。 + +```py +predict_child(68) +66.24045801526718 +predict_child(74) +70.415789473684214 +``` + +这些预测有多好? 我们可以了解它,通过将预测值与我们已有的数据进行比较。 为此,我们首先将函数`predict_child`应用于`Midparent`列,并将结果收入称为`Prediction`的新列中。 + +```py +# Apply predict_child to all the midparent heights + +heights_with_predictions = heights.with_column( + 'Prediction', heights.apply(predict_child, 'MidParent') +) +heights_with_predictions +``` + +| MidParent | Child | Prediction | +| --- | --- | --- | +| 75.43 | 73.2 | 70.1 | +| 75.43 | 69.2 | 70.1 | +| 75.43 | 69 | 70.1 | +| 75.43 | 69 | 70.1 | +| 73.66 | 73.5 | 70.4158 | +| 73.66 | 72.5 | 70.4158 | +| 73.66 | 65.5 | 70.4158 | +| 73.66 | 65.5 | 70.4158 | +| 72.06 | 71 | 68.5025 | +| 72.06 | 68 | 68.5025 | + +(省略了 924 行) + +为了查看预测值相对于观察数据的位置,可以使用`MidParent`作为公共水平轴绘制重叠的散点图。 + +```py +heights_with_predictions.scatter('MidParent') +``` + +![](img/7-5.png) + +金色的点的图形称为均值图,因为每个金色的点都是两条直线的中心,就像之前绘制的那样。每个都按照给定的双亲高度,做出了子女高度的预测。例如,散点图显示,对于 72 英寸的双亲高度,子女的预测高度将在 68 英寸和 69 英寸之间,事实上,`predict_child(72)`返回 68.5。 + +高尔顿的计算和可视化与我们非常相似,除了他没有 Python。他通过散点图绘制了均值图,并注意到它大致沿着直线。这条直线现在被称为回归线,是最常见的预测方法之一。高尔顿的朋友,数学家卡尔·皮尔森(Karl Pearson)用这些分析来形式化关联的概念。 + +这个例子,就像约翰·斯诺(John Snow)对霍乱死亡的分析一样,说明了现代数据科学的一些基本概念的根源可追溯到一个多世纪之前。高尔顿的方法,比如我们在这里使用的方法,是最近邻预测方法的雏形,现在在不同的环境中有着有效的应用。机器学习的现代领域包括这些方法的自动化,来基于庞大且快速发展的数据集进行预测。 + +## 按照单变量分类 + +数据科学家经常需要根据共有的特征,将个体分成不同的组,然后确定组的一些特征。 例如,在使用高尔顿高度数据的例子中,我们看到根据父母的平均高度对家庭进行分类,然后找出每个小组中子女的平均身高,较为实用。 + +这部分关于将个体分类到非数值类别。我们从回顾`gourp`的基本用法开始。 + +### 计算每个分类的数量 + +具有单个参数的`group `方法计算列中每个值的数量。 结果中,分组列(用于分组的列)中的每个唯一值是一行。 + +这是一个关于冰淇淋圆通的小型数据表。 `group `方法可以用来列出不同的口味,并提供每种口味的计数。 + +```py +cones = Table().with_columns( + 'Flavor', make_array('strawberry', 'chocolate', 'chocolate', 'strawberry', 'chocolate'), + 'Price', make_array(3.55, 4.75, 6.55, 5.25, 5.25) +) +cones +``` + + +| Flavor | Price | +| --- | --- | +| strawberry | 3.55 | +| chocolate | 4.75 | +| chocolate | 6.55 | +| strawberry | 5.25 | +| chocolate | 5.25 | + +```py +cones.group('Flavor') +``` + +| Flavor | count | +| --- | --- | +| chocolate | 3 | +| strawberry | 2 | + +有两个不同的类别,巧克力和草莓。 `group`的调用会在每个类别中创建一个计数表。 该列默认称为`count`,并包含每个类别中的行数。 + +注意,这一切都可以从`Flavor`列中找到。`Price `列尚未使用。 + +但是如果我们想要每种不同风味的圆筒的总价格呢? 这是`group`的第二个参数的作用。 + +### 发现每个类别的特征 + + +`group`的可选的第二个参数是一个函数,用于聚合所有这些行的其他列中的值。 例如,`sum`将累计与每个类别匹配的所有行中的价格。 这个结果中,分组列中每个唯一值是一行,但与原始表列数相同。 + +为了找到每种口味的总价格,我们再次调用`group`,用`Flavor`作为第一个参数。 但这一次有第二个参数:函数名称`sum`。 + +```py +cones.group('Flavor', sum) +``` + +| Flavor | Price sum | +| --- | --- | +| chocolate | 16.55 | +| strawberry | 8.8 | + +为了创建这个新表格,`group`已经计算了对应于每种不同口味的,所有行中的`Price`条目的总和。 三个`chocolate`行的价格共计`$16.55`(你可以假设价格是以美元计量的)。 两个`strawberry`行的价格共计`8.80`。 + +新创建的“总和”列的标签是`Price sum`,它通过使用被求和列的标签,并且附加单词`sum`创建。 + +由于`group`计算除了类别之外的所有列的`sum`,因此不需要指定必须对价格求和。 + +为了更详细地了解`group`在做什么,请注意,你可以自己计算总价格,不仅可以通过心算,还可以使用代码。 例如,要查找所有巧克力圆筒的总价格,你可以开始创建一个仅包含巧克力圆筒的新表,然后访问价格列: + +```py +cones.where('Flavor', are.equal_to('chocolate')).column('Price') +array([ 4.75, 6.55, 5.25]) +sum(cones.where('Flavor', are.equal_to('chocolate')).column('Price')) +16.550000000000001 +``` + +这就是`group `对`Flavor`中每个不同的值所做的事情。 + +```py +# For each distinct value in `Flavor, access all the rows +# and create an array of `Price` + +cones_choc = cones.where('Flavor', are.equal_to('chocolate')).column('Price') +cones_strawb = cones.where('Flavor', are.equal_to('strawberry')).column('Price') + +# Display the arrays in a table + +grouped_cones = Table().with_columns( + 'Flavor', make_array('chocolate', 'strawberry'), + 'Array of All the Prices', make_array(cones_choc, cones_strawb) +) + +# Append a column with the sum of the `Price` values in each array + +price_totals = grouped_cones.with_column( + 'Sum of the Array', make_array(sum(cones_choc), sum(cones_strawb)) +) +price_totals +``` + +| Flavor | Array of All the Prices | Sum of the Array | +| --- | --- | --- | +| chocolate | [ 4.75 6.55 5.25] | 16.55 | +| strawberry | [ 3.55 5.25] | 8.8 | + +你可以用任何其他可以用于数组的函数来替换`sum`。 例如,你可以使用`max`来查找每个类别中的最大价格: + +```py +cones.group('Flavor', max) +``` + +| Flavor | Price max | +| --- | --- | +| chocolate | 6.55 | +| strawberry | 5.25 | + +同样,`group`在每个`Flavor`分类中创建价格数组,但现在它寻找每个数组的`max `。 + +```py +price_maxes = grouped_cones.with_column( + 'Max of the Array', make_array(max(cones_choc), max(cones_strawb)) +) +price_maxes +``` + + + +| Flavor | Array of All the Prices | Max of the Array | +| --- | --- | --- | +| chocolate | [ 4.75 6.55 5.25] | 6.55 | +| strawberry | [ 3.55 5.25] | 5.25 | + +实际上,只有一个参数的原始调用,与使用`len`作为函数并清理表格的效果相同。 + +```py +lengths = grouped_cones.with_column( + 'Length of the Array', make_array(len(cones_choc), len(cones_strawb)) +) +lengths +``` + + +| Flavor | Array of All the Prices | Length of the Array | +| --- | --- | --- | +| chocolate | [ 4.75 6.55 5.25] | 3 | +| strawberry | [ 3.55 5.25] | 2 | + +### 示例:NBA 薪水 + +`nba`表包含了 2015~2016 年 NBA 球员的数据。 我们早些时候审查了这些数据。 回想一下,薪水以百万美元计算。 + +```py +nba1 = Table.read_table('nba_salaries.csv') +nba = nba1.relabeled("'15-'16 SALARY", 'SALARY') +nba +``` + + +| PLAYER | POSITION | TEAM | SALARY | +| --- | --- | --- | --- | +| Paul Millsap | PF | Atlanta Hawks | 18.6717 | +| Al Horford | C | Atlanta Hawks | 12 | +| Tiago Splitter | C | Atlanta Hawks | 9.75625 | +| Jeff Teague | PG | Atlanta Hawks | 8 | +| Kyle Korver | SG | Atlanta Hawks | 5.74648 | +| Thabo Sefolosha | SF | Atlanta Hawks | 4 | +| Mike Scott | PF | Atlanta Hawks | 3.33333 | +| Kent Bazemore | SF | Atlanta Hawks | 2 | +| Dennis Schroder | PG | Atlanta Hawks | 1.7634 | +| Tim Hardaway Jr. | SG | Atlanta Hawks | 1.30452 | + +(省略了 407 行) + +(1)每支球队为球员的工资支付了多少钱? + +唯一涉及的列是`TEAM`和`SALARY`。 我们必须按`TEAM`对这些行进行分组,然后对这些分类的工资进行求和。 + +```py +teams_and_money = nba.select('TEAM', 'SALARY') +teams_and_money.group('TEAM', sum) +``` + + +| TEAM | SALARY sum | +| --- | --- | +| Atlanta Hawks | 69.5731 | +| Boston Celtics | 50.2855 | +| Brooklyn Nets | 57.307 | +| Charlotte Hornets | 84.1024 | +| Chicago Bulls | 78.8209 | +| Cleveland Cavaliers | 102.312 | +| Dallas Mavericks | 65.7626 | +| Denver Nuggets | 62.4294 | +| Detroit Pistons | 42.2118 | +| Golden State Warriors | 94.0851 | + +(省略了 20 行) + +(2)五个位置的每个中有多少个 NBA 球员呢? + +我们必须按`POSITION`分类并计数。 这可以通过一个参数来完成: + +```py +nba.group('POSITION') +``` + +| POSITION | count | +| --- | --- | +| C | 69 | +| PF | 85 | +| PG | 85 | +| SF | 82 | +| SG | 96 | + +(3)五个位置的每个中,球员平均薪水是多少? + +这一次,我们必须按`POSITION`分组,并计算薪水的均值。 为了清楚起见,我们将用一张表格来描述位置和薪水。 + +```py +positions_and_money = nba.select('POSITION', 'SALARY') +positions_and_money.group('POSITION', np.mean) +``` + + +| POSITION | SALARY mean | +| --- | --- | +| C | 6.08291 | +| PF | 4.95134 | +| PG | 5.16549 | +| SF | 5.53267 | +| SG | 3.9882 | + +中锋是最高薪的职位,均值超过 600 万美元。 + +如果我们开始没有选择这两列,那么`group`不会尝试对`nba`中的类别列计算“平均”。 (“亚特兰大老鹰”和“波士顿凯尔特人队”这两个字符串是不可能平均)。它只对数值列做算术,其余的都是空白的。 + +```py +nba.group('POSITION', np.mean) +``` + + +| POSITION | PLAYER mean | TEAM mean | SALARY mean | +| --- | --- | --- | --- | +| C | | | 6.08291 | +| PF | | | 4.95134 | +| PG | | | 5.16549 | +| SF | | | 5.53267 | +| SG | | | 3.9882 | + +## 交叉分类 + +### 通过多个变量的交叉分类 + +当个体具有多个特征时,有很多不同的对他们分类的方法。 例如,如果我们有大学生的人口数据,对于每个人我们都有专业和大学的年数,那么这些学生就可以按照专业,按年份,或者是专业和年份的组合来分类。 + +`group`方法也允许我们根据多个变量划分个体。 这被称为交叉分类。 + +## 两个变量:计算每个类别偶对的数量 + +`more_cones`表记录了六个冰淇淋圆筒的味道,颜色和价格。 + +```py +more_cones = Table().with_columns( + 'Flavor', make_array('strawberry', 'chocolate', 'chocolate', 'strawberry', 'chocolate', 'bubblegum'), + 'Color', make_array('pink', 'light brown', 'dark brown', 'pink', 'dark brown', 'pink'), + 'Price', make_array(3.55, 4.75, 5.25, 5.25, 5.25, 4.75) +) + +more_cones +``` + + +| Flavor | Color | Price | +| --- | --- | --- | +| strawberry | pink | 3.55 | +| chocolate | light brown | 4.75 | +| chocolate | dark brown | 5.25 | +| strawberry | pink | 5.25 | +| chocolate | dark brown | 5.25 | +| bubblegum | pink | 4.75 | + +我们知道如何使用`group`,来计算每种口味的冰激凌圆筒的数量。 + +```py +more_cones.group('Flavor') +``` + + +| Flavor | count | +| --- | --- | +| bubblegum | 1 | +| chocolate | 3 | +| strawberry | 2 | + +但是现在每个圆筒也有一个颜色。 为了将圆筒按风味和颜色进行分类,我们将把标签列表作为参数传递给`group`。 在分组列中出现的每个唯一值的组合,在生成的表格中都占一行。 和以前一样,一个参数(这里是一个列表,但是也可以是一个数组)提供了行数。 + +虽然有六个圆筒,但只有四种风味和颜色的唯一组合。 两个圆筒是深褐色的巧克力,还有两个粉红色的草莓。 + +```py +more_cones.group(['Flavor', 'Color']) +``` + + +| Flavor | Color | count | +| --- | --- | --- | +| bubblegum | pink | 1 | +| chocolate | dark brown | 2 | +| chocolate | light brown | 1 | +| strawberry | pink | 2 | + +## 两个变量:查找每个类别偶对的特征 + + +第二个参数聚合所有其他列,它们不在分组列的列表中。 + +```py +more_cones.group(['Flavor', 'Color'], sum) +``` + + +| Flavor | Color | Price sum | +| --- | --- | --- | +| bubblegum | pink | 4.75 | +| chocolate | dark brown | 10.5 | +| chocolate | light brown | 4.75 | +| strawberry | pink | 8.8 | + + +三个或更多的变量。 你可以使用`group`,按三个或更多类别变量对行分类。 只要将它们全部包含列表中,它是第一个参数。 但是由多个变量交叉分类可能会变得复杂,因为不同类别组合的数量可能相当大。 + +### 数据透视表:重新排列`group`的输出 + + +交叉分类的许多使用只涉及两个类别变量,如上例中的`Flavor`和`Color`。 在这些情况下,可以在不同类型的表中显示分类结果,称为数据透视表(pivot table)。 数据透视表,也被称为列联表(contingency table),可以更容易地处理根据两个变量进行分类的数据。 + +回想一下,使用`group `来计算每个风味和颜色的类别偶对的圆筒数量: + +```py +more_cones.group(['Flavor', 'Color']) +``` + +| Flavor | Color | count | +| --- | --- | --- | +| bubblegum | pink | 1 | +| chocolate | dark brown | 2 | +| chocolate | light brown | 1 | +| strawberry | pink | 2 | + +使用`Table`的`pivot`方法可以以不同方式展示相同数据。暂时忽略这些代码,然后查看所得表。 + +```py +more_cones.pivot('Flavor', 'Color') +``` + + +| Color | bubblegum | chocolate | strawberry | +| --- | --- | --- | --- | +| dark brown | 0 | 2 | 0 | +| light brown | 0 | 1 | 0 | +| pink | 1 | 0 | 2 | + + +请注意,此表格显示了所有九种可能的风味和颜色偶对,包括我们的数据中不存在的偶对,比如“深棕色泡泡糖”。 还要注意,每个偶对中的计数都出现在表格的正文中:要找到浅棕色巧克力圆筒的数量,用眼睛沿着浅棕色的行看,直到它碰到巧克力一列。 + +`group`方法接受两个标签的列表,因为它是灵活的:可能需要一个或三个或更多。 另一方面,数据透视图总是需要两个列标签,一个确定列,一个确定行。 + +`pivot`方法与`group`方法密切相关:`group`将拥有相同值的组合的行分组在一起。它与`group`不同,因为它将所得值组织在一个网格中。 `pivot`的第一个参数是列标签,包含的值将用于在结果中形成新的列。第二个参数是用于行的列标签。结果提供了原始表的所有行的计数,它们拥有相同的行和列值组合。 + +像`group`一样,`pivot`可以和其他参数一同使用,来发现每个类别组合的特征。名为`values`的第三个可选参数表示一列值,它们替换网格的每个单元格中的计数。所有这些值将不会显示,但是;第四个参数`collect`表示如何将它们全部汇总到一个聚合值中,来显示在单元格中。 + +用例子来澄清这一点。这里是一个透视表,用于寻找每个单元格中的圆筒的总价格。 + +```py +more_cones.pivot('Flavor', 'Color', values='Price', collect=sum) +``` + + +| Color | bubblegum | chocolate | strawberry | +| --- | --- | --- | --- | +| dark brown | 0 | 10.5 | 0 | +| light brown | 0 | 4.75 | 0 | +| pink | 4.75 | 0 | 8.8 | + +这里`group `做了同一件事。 + +```py +more_cones.group(['Flavor', 'Color'], sum) +``` + + +| Flavor | Color | Price sum | +| --- | --- | --- | +| bubblegum | pink | 4.75 | +| chocolate | dark brown | 10.5 | +| chocolate | light brown | 4.75 | +| strawberry | pink | 8.8 | + +尽管两个表中的数字都相同,但由`pivot`生成的表格更易于阅读,因而更易于分析。 透视表的优点是它将分组的值放到相邻的列中,以便它们可以进行组合和比较。 + +### 示例:加州成人的教育和收入 + +加州的开放数据门户是丰富的加州生活的信息来源。 这是 2008 至 2014 年间加利福尼亚州教育程度和个人收入的数据集。数据来源于美国人口普查的当前人口调查。 + +对于每年,表格都记录了加州的`Population Count`(人口数量),按照年龄,性别,教育程度和个人收入,构成不同的组合。 我们将只研究 2014 年的数据。 + +```py +full_table = Table.read_table('educ_inc.csv') +ca_2014 = full_table.where('Year', are.equal_to('1/1/14 0:00')).where('Age', are.not_equal_to('00 to 17')) +ca_2014 +``` + + +| Year | Age | Gender | Educational Attainment | Personal Income | Population Count | +| --- | --- | --- | --- | --- | --- | +| 1/1/14 0:00 | 18 to 64 | Female | No high school diploma | H: 75,000 and over | 2058 | +| 1/1/14 0:00 | 65 to 80+ | Male | No high school diploma | H: 75,000 and over | 2153 | +| 1/1/14 0:00 | 65 to 80+ | Female | No high school diploma | G: 50,000 to 74,999 | 4666 | +| 1/1/14 0:00 | 65 to 80+ | Female | High school or equivalent | H: 75,000 and over | 7122 | +| 1/1/14 0:00 | 65 to 80+ | Female | No high school diploma | F: 35,000 to 49,999 | 7261 | +| 1/1/14 0:00 | 65 to 80+ | Male | No high school diploma | G: 50,000 to 74,999 | 8569 | +| 1/1/14 0:00 | 18 to 64 | Female | No high school diploma | G: 50,000 to 74,999 | 14635 | +| 1/1/14 0:00 | 65 to 80+ | Male | No high school diploma | F: 35,000 to 49,999 | 15212 | +| 1/1/14 0:00 | 65 to 80+ | Male | College, less than 4-yr degree | B: 5,000 to 9,999 | 15423 | +| 1/1/14 0:00 | 65 to 80+ | Female | Bachelor's degree or higher | A: 0 to 4,999 | 15459 | + +(省略了 117 行) + +表中的每一行对应一组年龄,性别,教育程度和收入。 总共有 127 个这样的组合! + +作为第一步,从一个或两个变量开始是个好主意。 我们只关注一对:教育程度和个人收入。 + +```py +educ_inc = ca_2014.select('Educational Attainment', 'Personal Income', 'Population Count') +educ_inc +``` + + +| Educational Attainment | Personal Income | Population Count | +| --- | --- | --- | +| No high school diploma | H: 75,000 and over | 2058 | +| No high school diploma | H: 75,000 and over | 2153 | +| No high school diploma | G: 50,000 to 74,999 | 4666 | +| High school or equivalent | H: 75,000 and over | 7122 | +| No high school diploma | F: 35,000 to 49,999 | 7261 | +| No high school diploma | G: 50,000 to 74,999 | 8569 | +| No high school diploma | G: 50,000 to 74,999 | 14635 | +| No high school diploma | F: 35,000 to 49,999 | 15212 | +| College, less than 4-yr degree | B: 5,000 to 9,999 | 15423 | +| Bachelor's degree or higher | A: 0 to 4,999 | 15459 | + +(省略了 117 行) + +我们先看看教育程度。 这个变量的分类已经由不同的收入水平细分了。 因此,我们将按照教育程度分组,并将每个分类中的人口数量相加。 + +```py +education = educ_inc.select('Educational Attainment', 'Population Count') +educ_totals = education.group('Educational Attainment', sum) +educ_totals +``` + + +| Educational Attainment | Population Count sum | +| --- | --- | +| Bachelor's degree or higher | 8525698 | +| College, less than 4-yr degree | 7775497 | +| High school or equivalent | 6294141 | +| No high school diploma | 4258277 | + +教育程度只有四类。 计数太大了,查看百分比更有帮助。 为此,我们将使用前面章节中定义的函数`percents`。 它将数值数组转换为输入数组总量的百分比数组。 + +```py +def percents(array_x): + return np.round( (array_x/sum(array_x))*100, 2) +``` + +我们现在有加州成人的教育程度分布。 超过 30% 的人拥有学士或更高学位,而几乎 16% 没有高中文凭。 + +```py +educ_distribution = educ_totals.with_column( + 'Population Percent', percents(educ_totals.column(1)) +) +educ_distribution +``` + + +| Educational Attainment | Population Count sum | Population Percent | +| --- | --- | --- | +| Bachelor's degree or higher | 8525698 | 31.75 | +| College, less than 4-yr degree | 7775497 | 28.96 | +| High school or equivalent | 6294141 | 23.44 | +| No high school diploma | 4258277 | 15.86 | + +通过使用`pivot`,我们可以得到一张加州成人的透视表(计数表),按照`Educational Attainment`和`Personal Income`交叉分类。 + +```py +totals = educ_inc.pivot('Educational Attainment', 'Personal Income', values='Population Count', collect=sum) +totals +``` + + +| Personal Income | Bachelor's degree or higher | College, less than 4-yr degree | High school or equivalent | No high school diploma | +| --- | --- | --- | --- | --- | +| A: 0 to 4,999 | 575491 | 985011 | 1161873 | 1204529 | +| B: 5,000 to 9,999 | 326020 | 810641 | 626499 | 597039 | +| C: 10,000 to 14,999 | 452449 | 798596 | 692661 | 664607 | +| D: 15,000 to 24,999 | 773684 | 1345257 | 1252377 | 875498 | +| E: 25,000 to 34,999 | 693884 | 1091642 | 929218 | 464564 | +| F: 35,000 to 49,999 | 1122791 | 1112421 | 782804 | 260579 | +| G: 50,000 to 74,999 | 1594681 | 883826 | 525517 | 132516 | +| H: 75,000 and over | 2986698 | 748103 | 323192 | 58945 | + +在这里你可以看到`pivot`相比其他方法的威力。 计数的每一列都是个人收入在特定教育程度中的分布。 将计数转换为百分数可以让我们比较四个分布。 + +```py +distributions = totals.select(0).with_columns( + "Bachelor's degree or higher", percents(totals.column(1)), + 'College, less than 4-yr degree', percents(totals.column(2)), + 'High school or equivalent', percents(totals.column(3)), + 'No high school diploma', percents(totals.column(4)) + ) + +distributions +``` + + +| Personal Income | Bachelor's degree or higher | College, less than 4-yr degree | High school or equivalent | No high school diploma | +| --- | --- | --- | --- | --- | +| A: 0 to 4,999 | 6.75 | 12.67 | 18.46 | 28.29 | +| B: 5,000 to 9,999 | 3.82 | 10.43 | 9.95 | 14.02 | +| C: 10,000 to 14,999 | 5.31 | 10.27 | 11 | 15.61 | +| D: 15,000 to 24,999 | 9.07 | 17.3 | 19.9 | 20.56 | +| E: 25,000 to 34,999 | 8.14 | 14.04 | 14.76 | 10.91 | +| F: 35,000 to 49,999 | 13.17 | 14.31 | 12.44 | 6.12 | +| G: 50,000 to 74,999 | 18.7 | 11.37 | 8.35 | 3.11 | +| H: 75,000 and over | 35.03 | 9.62 | 5.13 | 1.38 | + +一眼就能看出,超过 35% 的学士或以上学位的收入达到 $75,000 美元以上,而其他教育分类中,少于 10% 的人达到了这一水平。 + +下面的条形图比较了没有高中文凭的加州成年人的个人收入分布情况,和完成学士或更高学位的人的收入分布情况。 分布的差异是惊人的。 教育程度与个人收入有明显的正相关关系。 + +```py +distributions.select(0, 1, 4).barh(0) +``` + +![](img/7-6.png) + +## 按列连接表 + +通常,同一个人的数据在多个表格中维护。 例如,大学的一个办公室可能有每个学生完成学位的时间的数据,而另一个办公室则有学生学费和经济援助的数据。 + +为了了解学生的经历,将两个数据集放在一起可能会有帮助。 如果数据是在两个表中,每个学生都有一行,那么我们希望将这些列放在一起,确保行是匹配的,以便将每个学生的信息保持在一行上。 + +让我们在一个简单的示例的背景下实现它,然后在更大的数据集上使用这个方法。 + +圆筒表是我们以前遇到的。 现在假设每种口味的冰激凌的评分都在独立的表格中。 + +```py +cones = Table().with_columns( + 'Flavor', make_array('strawberry', 'vanilla', 'chocolate', 'strawberry', 'chocolate'), + 'Price', make_array(3.55, 4.75, 6.55, 5.25, 5.75) +) +cones +``` + + +| Flavor | Price | +| --- | --- | +| strawberry | 3.55 | +| vanilla | 4.75 | +| chocolate | 6.55 | +| strawberry | 5.25 | +| chocolate | 5.75 | + +```py +ratings = Table().with_columns( + 'Kind', make_array('strawberry', 'chocolate', 'vanilla'), + 'Stars', make_array(2.5, 3.5, 4) +) +ratings +``` + + +| Kind | Stars | +| --- | --- | +| strawberry | 2.5 | +| chocolate | 3.5 | +| vanilla | 4 | + +每个表都有一个包含冰淇淋风味的列:`cones`有`Flavor`列,`ratings `有`Kind`列。 这些列中的条目可以用来连接两个表。 + +`join`方法创建一个新的表,其中`cones`表中的每个圆筒都增加了评分信息。 对于`cones`中的每个圆筒,`join`会找到`ratings `中的行,它的`Kind `匹配圆筒的`Flavor`。 我们必须告诉`join`使用这些列进行匹配。 + +```py +rated = cones.join('Flavor', ratings, 'Kind') +rated +``` + + +| Flavor | Price | Stars | +| --- | --- | --- | +| chocolate | 6.55 | 3.5 | +| chocolate | 5.75 | 3.5 | +| strawberry | 3.55 | 2.5 | +| strawberry | 5.25 | 2.5 | +| vanilla | 4.75 | 4 | + +现在每个圆筒不仅拥有价格,而且还有风味评分。 + +一般来说,使用来自另一个表(比如`table2`)的信息来扩充一个表(如`table1`)的`join `调用如下所示: + +```py +table1.join(table1_column_for_joining, table2, table2_column_for_joining) + +``` + +新的`rated`表使我们能够计算出价格与星星的比值,你可以把它看作是非正式的价值衡量标准。 低的值更好 - 它们意味着你为评分的每个星星花费更少。 + +```py +rated.with_column('$/Star', rated.column('Price') / rated.column('Stars')).sort(3) +``` + + +| Flavor | Price | Stars | $/Star | +| --- | --- | --- | --- | +| vanilla | 4.75 | 4 | 1.1875 | +| strawberry | 3.55 | 2.5 | 1.42 | +| chocolate | 5.75 | 3.5 | 1.64286 | +| chocolate | 6.55 | 3.5 | 1.87143 | +| strawberry | 5.25 | 2.5 | 2.1 | + +虽然草莓在这三种口味中评分最低,但是这个标准下草莓更加便宜,因为每颗星星的花费并不高。 + +警告。顺序很重要。由于`join`中的第二个表用于扩充第一个表,所以重要的是,第一个表中的每一行在第二个表中只有一个匹配的行。如果第一个表中的某一行在第二个表中没有匹配项,则信息可能丢失。如果第一个表中的某一行在第二个表中有多个匹配项,那么`join`将只选择一个,这也是一种信息丢失。 + +我们可以在下面的例子中看到它,它试图通过相同的两列连接相同的两个表格,但是以另一种顺序。这种连接是没有意义的:它试图用价格来扩展每种风味的评分,但是根据`cones`表,每种风味都有一个以上的圆筒(和价格)。结果是两个圆筒消失了。`join`方法仅仅在`cones`寻找对应`chocolate`的第一行,而忽略其他行。 + +```py +ratings.join('Kind', cones, 'Flavor') +``` + +| Kind | Stars | Price | +| --- | --- | --- | +| chocolate | 3.5 | 6.55 | +| strawberry | 2.5 | 3.55 | +| vanilla | 4 | 4.75 | + +假设有个冰淇淋的评分表,我们已经求出了每种风味的平均评分。 + +```py +reviews = Table().with_columns( + 'Flavor', make_array('vanilla', 'chocolate', 'vanilla', 'chocolate'), + 'Stars', make_array(5, 3, 5, 4) +) +reviews +``` + + +| Flavor | Stars | +| --- | --- | +| vanilla | 5 | +| chocolate | 3 | +| vanilla | 5 | +| chocolate | 4 | + +```py +average_review = reviews.group('Flavor', np.average) +average_review +``` + + +| Flavor | Stars average | +| --- | --- | +| chocolate | 3.5 | +| vanilla | 5 | + +我们可以连接`cones `和`average_review`,通过提供用于连接的列标签。 + + +```py +cones.join('Flavor', average_review, 'Flavor') +``` + +| Flavor | Price | Stars average | +| --- | --- | --- | +| chocolate | 6.55 | 3.5 | +| chocolate | 5.75 | 3.5 | +| vanilla | 4.75 | 5 | + +注意草莓圆筒是如何消失的。 没有草莓圆筒的评价,所以没有草莓的行可以连接的东西。 这可能是一个问题,也可能不是 - 这取决于我们试图使用连接表执行的分析。 + +## 湾区共享单车 + +在本章结尾,我们通过使用我们学过的所有方法,来检验新的大型数据集。 我们还将介绍一个强大的可视化工具`map_table`。 + +湾区自行车共享服务公司在其系统中发布了一个数据集,描述了 2014 年 9 月到 2015 年 8 月期间的每个自行车的租赁。 总共有 354152 次出租。 表的列是: + ++ 租赁 ID ++ 租赁的时间,以秒为单位 ++ 开始日期 ++ 起点站的名称和起始终端的代码 ++ 终点站的名称和终止终端的代码 ++ 自行车的序列号 ++ 订阅者类型和邮政编码 + +```py +trips = Table.read_table('trip.csv') +trips +``` + + +| Trip ID | Duration | Start Date | Start Station | Start Terminal | End Date | End Station | End Terminal | Bike # | Subscriber Type | Zip Code | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 913460 | 765 | 8/31/2015 23:26 | Harry Bridges Plaza (Ferry Building) | 50 | 8/31/2015 23:39 | San Francisco Caltrain (Townsend at 4th) | 70 | 288 | Subscriber | 2139 | +| 913459 | 1036 | 8/31/2015 23:11 | San Antonio Shopping Center | 31 | 8/31/2015 23:28 | Mountain View City Hall | 27 | 35 | Subscriber | 95032 | +| 913455 | 307 | 8/31/2015 23:13 | Post at Kearny | 47 | 8/31/2015 23:18 | 2nd at South Park | 64 | 468 | Subscriber | 94107 | +| 913454 | 409 | 8/31/2015 23:10 | San Jose City Hall | 10 | 8/31/2015 23:17 | San Salvador at 1st | 8 | 68 | Subscriber | 95113 | +| 913453 | 789 | 8/31/2015 23:09 | Embarcadero at Folsom | 51 | 8/31/2015 23:22 | Embarcadero at Sansome | 60 | 487 | Customer | 9069 | +| 913452 | 293 | 8/31/2015 23:07 | Yerba Buena Center of the Arts (3rd @ Howard) | 68 | 8/31/2015 23:12 | San Francisco Caltrain (Townsend at 4th) | 70 | 538 | Subscriber | 94118 | +| 913451 | 896 | 8/31/2015 23:07 | Embarcadero at Folsom | 51 | 8/31/2015 23:22 | Embarcadero at Sansome | 60 | 363 | Customer | 92562 | +| 913450 | 255 | 8/31/2015 22:16 | Embarcadero at Sansome | 60 | 8/31/2015 22:20 | Steuart at Market | 74 | 470 | Subscriber | 94111 | +| 913449 | 126 | 8/31/2015 22:12 | Beale at Market | 56 | 8/31/2015 22:15 | Temporary Transbay Terminal (Howard at Beale) | 55 | 439 | Subscriber | 94130 | +| 913448 | 932 | 8/31/2015 21:57 | Post at Kearny | 47 | 8/31/2015 22:12 | South Van Ness at Market | 66 | 472 | Subscriber | 94702 | + +(省略了 354142 行) + +我们只专注于免费行程,这是持续不到 1800 秒(半小时)的行程。 长途行程需要付费。 + +下面的直方图显示,大部分行程需要大约 10 分钟(600 秒)左右。 很少有人花了近 30 分钟(1800 秒),可能是因为人们试图在截止时间之前退还自行车,以免付费。 + +```py +commute = trips.where('Duration', are.below(1800)) +commute.hist('Duration', unit='Second') +``` + +![](img/7-7.png) + +我们可以通过指定更多的桶来获得更多的细节。 但整体形状并没有太大变化。 + +```py +commute.hist('Duration', bins=60, unit='Second') +``` + +![](img/7-8.png) + +### 使用`group `和`pivot`探索数据 + +我们可以使用`group `来识别最常用的起点站。 + +```py +starts = commute.group('Start Station').sort('count', descending=True) +starts +``` + + +| Start Station | count | +| --- | --- | +| San Francisco Caltrain (Townsend at 4th) | 25858 | +| San Francisco Caltrain 2 (330 Townsend) | 21523 | +| Harry Bridges Plaza (Ferry Building) | 15543 | +| Temporary Transbay Terminal (Howard at Beale) | 14298 | +| 2nd at Townsend | 13674 | +| Townsend at 7th | 13579 | +| Steuart at Market | 13215 | +| Embarcadero at Sansome | 12842 | +| Market at 10th | 11523 | +| Market at Sansome | 11023 | + +(省略了 60 行) + +大多数行程起始于 Townsend 的 Caltrain 站,和旧金山的四号车站。 人们乘坐火车进入城市,然后使用共享单车到达下一个目的地。 + +`group`方法也可以用于按照起点站和终点站,对租赁进行分类。 + +```py +commute.group(['Start Station', 'End Station']) +``` + + +| Start Station | End Station | count | +| --- | --- | --- | +| 2nd at Folsom | 2nd at Folsom | 54 | +| 2nd at Folsom | 2nd at South Park | 295 | +| 2nd at Folsom | 2nd at Townsend | 437 | +| 2nd at Folsom | 5th at Howard | 113 | +| 2nd at Folsom | Beale at Market | 127 | +| 2nd at Folsom | Broadway St at Battery St | 67 | +| 2nd at Folsom | Civic Center BART (7th at Market) | 47 | +| 2nd at Folsom | Clay at Battery | 240 | +| 2nd at Folsom | Commercial at Montgomery | 128 | +| 2nd at Folsom | Davis at Jackson | 28 | + +(省略了 1619 行) + +共有五十四次行程开始和结束于在 Folsom 二号车站,。 有很多人(437 人)往返于 Folsom 二号和 Townsend 二号车站之间。 + +`pivot`方法执行相同的分类,但将结果显示在一个透视表中,该表显示了起点和终点站的所有可能组合,即使其中一些不对应任何行程。 请记住,`pivot`函数的第一个参数指定了数据透视表的列标签;第二个参数指定行标签。 + +在 Beale at Market 附近有一个火车站以及一个湾区快速公交(BART)站,解释了从那里开始和结束的大量行程。 + +```py +commute.pivot('Start Station', 'End Station') +``` + + +| End Station | 2nd at Folsom | 2nd at South Park | 2nd at Townsend | 5th at Howard | Adobe on Almaden | Arena Green / SAP Center | Beale at Market | Broadway St at Battery St | California Ave Caltrain Station | Castro Street and El Camino Real | Civic Center BART (7th at Market) | Clay at Battery | Commercial at Montgomery | Cowper at University | Davis at Jackson | Embarcadero at Bryant | Embarcadero at Folsom | Embarcadero at Sansome | Embarcadero at Vallejo | Evelyn Park and Ride | Franklin at Maple | Golden Gate at Polk | Grant Avenue at Columbus Avenue | Harry Bridges Plaza (Ferry Building) | Howard at 2nd | Japantown | MLK Library | Market at 10th | Market at 4th | Market at Sansome | Mechanics Plaza (Market at Battery) | Mezes Park | Mountain View Caltrain Station | Mountain View City Hall | Palo Alto Caltrain Station | Park at Olive | Paseo de San Antonio | Post at Kearny | Powell Street BART | Powell at Post (Union Square) | Redwood City Caltrain Station | Redwood City Medical Center | Redwood City Public Library | Rengstorff Avenue / California Street | Ryland Park | SJSU - San Salvador at 9th | SJSU 4th at San Carlos | San Antonio Caltrain Station | San Antonio Shopping Center | San Francisco Caltrain (Townsend at 4th) | San Francisco Caltrain 2 (330 Townsend) | San Francisco City Hall | San Jose City Hall | San Jose Civic Center | San Jose Diridon Caltrain Station | San Mateo County Center | San Pedro Square | San Salvador at 1st | Santa Clara County Civic Center | Santa Clara at Almaden | South Van Ness at Market | Spear at Folsom | St James Park | Stanford in Redwood City | Steuart at Market | Temporary Transbay Terminal (Howard at Beale) | Townsend at 7th | University and Emerson | Washington at Kearny | Yerba Buena Center of the Arts (3rd @ Howard) | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 2nd at Folsom | 54 | 190 | 554 | 107 | 0 | 0 | 40 | 21 | 0 | 0 | 44 | 78 | 54 | 0 | 9 | 77 | 32 | 41 | 14 | 0 | 0 | 11 | 30 | 416 | 53 | 0 | 0 | 169 | 114 | 302 | 33 | 0 | 0 | 0 | 0 | 0 | 0 | 60 | 121 | 88 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 694 | 445 | 21 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 38 | 57 | 0 | 0 | 39 | 237 | 342 | 0 | 17 | 31 | +| 2nd at South Park | 295 | 164 | 71 | 180 | 0 | 0 | 208 | 85 | 0 | 0 | 112 | 87 | 160 | 0 | 37 | 56 | 178 | 83 | 116 | 0 | 0 | 57 | 73 | 574 | 500 | 0 | 0 | 139 | 199 | 1633 | 119 | 0 | 0 | 0 | 0 | 0 | 0 | 299 | 84 | 113 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 559 | 480 | 48 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 66 | 152 | 0 | 0 | 374 | 429 | 143 | 0 | 63 | 209 | +| 2nd at Townsend | 437 | 151 | 185 | 92 | 0 | 0 | 608 | 350 | 0 | 0 | 80 | 329 | 168 | 0 | 386 | 361 | 658 | 506 | 254 | 0 | 0 | 27 | 315 | 2607 | 295 | 0 | 0 | 110 | 225 | 845 | 177 | 0 | 0 | 0 | 0 | 0 | 0 | 120 | 100 | 141 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 905 | 299 | 14 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 72 | 508 | 0 | 0 | 2349 | 784 | 417 | 0 | 57 | 166 | +| 5th at Howard | 113 | 177 | 148 | 83 | 0 | 0 | 59 | 130 | 0 | 0 | 203 | 76 | 129 | 0 | 30 | 57 | 49 | 166 | 54 | 0 | 0 | 85 | 78 | 371 | 478 | 0 | 0 | 303 | 158 | 168 | 90 | 0 | 0 | 0 | 0 | 0 | 0 | 93 | 183 | 169 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 690 | 1859 | 48 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 116 | 102 | 0 | 0 | 182 | 750 | 200 | 0 | 43 | 267 | +| Adobe on Almaden | 0 | 0 | 0 | 0 | 11 | 4 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 17 | 7 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 25 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 7 | 7 | 16 | 0 | 0 | 0 | 0 | 0 | 19 | 23 | 265 | 0 | 20 | 4 | 5 | 10 | 0 | 0 | 14 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | +| Arena Green / SAP Center | 0 | 0 | 0 | 0 | 7 | 64 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 16 | 5 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 21 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 24 | 3 | 7 | 0 | 0 | 0 | 0 | 0 | 6 | 20 | 7 | 0 | 56 | 12 | 38 | 259 | 0 | 0 | 13 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | +| Beale at Market | 127 | 79 | 183 | 59 | 0 | 0 | 59 | 661 | 0 | 0 | 201 | 75 | 101 | 0 | 247 | 178 | 38 | 590 | 165 | 0 | 0 | 54 | 435 | 57 | 72 | 0 | 0 | 286 | 236 | 163 | 26 | 0 | 0 | 0 | 0 | 0 | 0 | 49 | 227 | 179 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 640 | 269 | 25 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 243 | 128 | 0 | 0 | 16 | 167 | 35 | 0 | 64 | 45 | +| Broadway St at Battery St | 67 | 89 | 279 | 119 | 0 | 0 | 1022 | 110 | 0 | 0 | 62 | 283 | 226 | 0 | 191 | 198 | 79 | 231 | 35 | 0 | 0 | 5 | 70 | 168 | 49 | 0 | 0 | 32 | 97 | 341 | 214 | 0 | 0 | 0 | 0 | 0 | 0 | 169 | 71 | 218 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 685 | 438 | 7 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 18 | 106 | 0 | 0 | 344 | 748 | 50 | 0 | 79 | 47 | +| California Ave Caltrain Station | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 38 | 1 | 0 | 0 | 0 | 29 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 192 | 40 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 6 | 0 | 0 | 0 | 17 | 10 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 57 | 0 | 0 | +| Castro Street and El Camino Real | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 30 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 14 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 931 | 34 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 7 | 0 | 0 | 0 | 4 | 12 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | + +(省略了 60 行) + +我们也可以使用`pivot`来寻找起点和终点站之间的最短骑行时间。 在这里,`pivot`已经接受了可选参数`Duration`,以及`min`函数,它对每个单元格中的值执行。 + +```py +commute.pivot('Start Station', 'End Station', 'Duration', min) +``` + + +| End Station | 2nd at Folsom | 2nd at South Park | 2nd at Townsend | 5th at Howard | Adobe on Almaden | Arena Green / SAP Center | Beale at Market | Broadway St at Battery St | California Ave Caltrain Station | Castro Street and El Camino Real | Civic Center BART (7th at Market) | Clay at Battery | Commercial at Montgomery | Cowper at University | Davis at Jackson | Embarcadero at Bryant | Embarcadero at Folsom | Embarcadero at Sansome | Embarcadero at Vallejo | Evelyn Park and Ride | Franklin at Maple | Golden Gate at Polk | Grant Avenue at Columbus Avenue | Harry Bridges Plaza (Ferry Building) | Howard at 2nd | Japantown | MLK Library | Market at 10th | Market at 4th | Market at Sansome | Mechanics Plaza (Market at Battery) | Mezes Park | Mountain View Caltrain Station | Mountain View City Hall | Palo Alto Caltrain Station | Park at Olive | Paseo de San Antonio | Post at Kearny | Powell Street BART | Powell at Post (Union Square) | Redwood City Caltrain Station | Redwood City Medical Center | Redwood City Public Library | Rengstorff Avenue / California Street | Ryland Park | SJSU - San Salvador at 9th | SJSU 4th at San Carlos | San Antonio Caltrain Station | San Antonio Shopping Center | San Francisco Caltrain (Townsend at 4th) | San Francisco Caltrain 2 (330 Townsend) | San Francisco City Hall | San Jose City Hall | San Jose Civic Center | San Jose Diridon Caltrain Station | San Mateo County Center | San Pedro Square | San Salvador at 1st | Santa Clara County Civic Center | Santa Clara at Almaden | South Van Ness at Market | Spear at Folsom | St James Park | Stanford in Redwood City | Steuart at Market | Temporary Transbay Terminal (Howard at Beale) | Townsend at 7th | University and Emerson | Washington at Kearny | Yerba Buena Center of the Arts (3rd @ Howard) | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 2nd at Folsom | 61 | 97 | 164 | 268 | 0 | 0 | 271 | 407 | 0 | 0 | 483 | 329 | 306 | 0 | 494 | 239 | 262 | 687 | 599 | 0 | 0 | 639 | 416 | 282 | 80 | 0 | 0 | 506 | 237 | 167 | 250 | 0 | 0 | 0 | 0 | 0 | 0 | 208 | 264 | 290 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 300 | 303 | 584 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 590 | 208 | 0 | 0 | 318 | 149 | 448 | 0 | 429 | 165 | +| 2nd at South Park | 61 | 60 | 77 | 86 | 0 | 0 | 78 | 345 | 0 | 0 | 290 | 188 | 171 | 0 | 357 | 104 | 81 | 490 | 341 | 0 | 0 | 369 | 278 | 122 | 60 | 0 | 0 | 416 | 142 | 61 | 68 | 0 | 0 | 0 | 0 | 0 | 0 | 60 | 237 | 106 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 63 | 66 | 458 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 399 | 63 | 0 | 0 | 79 | 61 | 78 | 0 | 270 | 96 | +| 2nd at Townsend | 137 | 67 | 60 | 423 | 0 | 0 | 311 | 469 | 0 | 0 | 546 | 520 | 474 | 0 | 436 | 145 | 232 | 509 | 494 | 0 | 0 | 773 | 549 | 325 | 221 | 0 | 0 | 667 | 367 | 265 | 395 | 0 | 0 | 0 | 0 | 0 | 0 | 319 | 455 | 398 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 125 | 133 | 742 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 777 | 241 | 0 | 0 | 291 | 249 | 259 | 0 | 610 | 284 | +| 5th at Howard | 215 | 300 | 384 | 68 | 0 | 0 | 357 | 530 | 0 | 0 | 179 | 412 | 364 | 0 | 543 | 419 | 359 | 695 | 609 | 0 | 0 | 235 | 474 | 453 | 145 | 0 | 0 | 269 | 161 | 250 | 306 | 0 | 0 | 0 | 0 | 0 | 0 | 234 | 89 | 202 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 256 | 221 | 347 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 375 | 402 | 0 | 0 | 455 | 265 | 357 | 0 | 553 | 109 | +| Adobe on Almaden | 0 | 0 | 0 | 0 | 84 | 275 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 701 | 387 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 229 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 441 | 452 | 318 | 0 | 0 | 0 | 0 | 0 | 309 | 146 | 182 | 0 | 207 | 358 | 876 | 101 | 0 | 0 | 369 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | +| Arena Green / SAP Center | 0 | 0 | 0 | 0 | 305 | 62 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 526 | 546 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 403 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 288 | 875 | 685 | 0 | 0 | 0 | 0 | 0 | 440 | 420 | 153 | 0 | 166 | 624 | 759 | 116 | 0 | 0 | 301 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | +| Beale at Market | 219 | 343 | 417 | 387 | 0 | 0 | 60 | 155 | 0 | 0 | 343 | 122 | 153 | 0 | 115 | 216 | 170 | 303 | 198 | 0 | 0 | 437 | 235 | 149 | 204 | 0 | 0 | 535 | 203 | 88 | 72 | 0 | 0 | 0 | 0 | 0 | 0 | 191 | 316 | 191 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 499 | 395 | 526 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 575 | 173 | 0 | 0 | 87 | 94 | 619 | 0 | 222 | 264 | +| Broadway St at Battery St | 351 | 424 | 499 | 555 | 0 | 0 | 195 | 62 | 0 | 0 | 520 | 90 | 129 | 0 | 70 | 340 | 284 | 128 | 101 | 0 | 0 | 961 | 148 | 168 | 357 | 0 | 0 | 652 | 351 | 218 | 221 | 0 | 0 | 0 | 0 | 0 | 0 | 255 | 376 | 316 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 611 | 599 | 799 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 738 | 336 | 0 | 0 | 169 | 291 | 885 | 0 | 134 | 411 | +| California Ave Caltrain Station | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 82 | 1645 | 0 | 0 | 0 | 628 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1771 | 0 | 484 | 131 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1077 | 0 | 0 | 0 | 870 | 911 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 531 | 0 | 0 | +| Castro Street and El Camino Real | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 74 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 499 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 201 | 108 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 654 | 0 | 0 | 0 | 953 | 696 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | + +(省略了 60 行) + +有人从 Folsom 的二号车站骑到 Beale at Market,距离大约五个街区,非常快(271 秒,约 4.5 分钟)。 2nd Avenue 和 Almaden 的 Adobe 之间没有自行车行程,因为后者在不同的城市。 + +### 绘制地图 + +`stations`表包含每个自行车站的地理信息,包括纬度,经度和“地标”,它是该站所在城市的名称。 + +```py +stations = Table.read_table('station.csv') +stations +``` + +| station_id | name | lat | long | dockcount | landmark | installation | +| --- | --- | --- | --- | --- | --- | --- | +| 2 | San Jose Diridon Caltrain Station | 37.3297 | -121.902 | 27 | San Jose | 8/6/2013 | +| 3 | San Jose Civic Center | 37.3307 | -121.889 | 15 | San Jose | 8/5/2013 | +| 4 | Santa Clara at Almaden | 37.334 | -121.895 | 11 | San Jose | 8/6/2013 | +| 5 | Adobe on Almaden | 37.3314 | -121.893 | 19 | San Jose | 8/5/2013 | +| 6 | San Pedro Square | 37.3367 | -121.894 | 15 | San Jose | 8/7/2013 | +| 7 | Paseo de San Antonio | 37.3338 | -121.887 | 15 | San Jose | 8/7/2013 | +| 8 | San Salvador at 1st | 37.3302 | -121.886 | 15 | San Jose | 8/5/2013 | +| 9 | Japantown | 37.3487 | -121.895 | 15 | San Jose | 8/5/2013 | +| 10 | San Jose City Hall | 37.3374 | -121.887 | 15 | San Jose | 8/6/2013 | +| 11 | MLK Library | 37.3359 | -121.886 | 19 | San Jose | 8/6/2013 | + +(省略了 60 行) + +我们可以使用`Marker.map_table`来绘制一个车站所在位置的地图。 该函数在一个表格上进行操作,该表格的列依次是纬度,经度以及每个点的可选标识符。 + +```py +Marker.map_table(stations.select('lat', 'long', 'name')) +``` + +[地图 1](src/7-1.html) + +地图使用 OpenStreetMap 创建的,OpenStreetMap 是一个开放的在线地图系统,你可以像使用 Google 地图或任何其他在线地图一样使用。 放大到旧金山,看看车站如何分布。 点击一个标记,看看它是哪个站。 + +你也可以用彩色圆圈表示地图上的点。 这是旧金山自行车站的地图。 + +```py +sf = stations.where('landmark', are.equal_to('San Francisco')) +sf_map_data = sf.select('lat', 'long', 'name') +Circle.map_table(sf_map_data, color='green', radius=200) +``` + +[地图 2](src/7-2.html) + +### 更多信息的地图:`join`的应用 + +自行车站位于湾区五个不同的城市。 为了区分每个城市,通过使用不同的颜色,我们首先使用`group`来标识所有城市,并为每个城市分配一个颜色。 + +```py +cities = stations.group('landmark').relabeled('landmark', 'city') +cities +``` + + +| city | count | +| --- | --- | +| Mountain View | 7 | +| Palo Alto | 5 | +| Redwood City | 7 | +| San Francisco | 35 | +| San Jose | 16 | + +```py +colors = cities.with_column('color', make_array('blue', 'red', 'green', 'orange', 'purple')) +colors +``` + +| city | count | color | +| --- | --- | --- | +| Mountain View | 7 | blue | +| Palo Alto | 5 | red | +| Redwood City | 7 | green | +| San Francisco | 35 | orange | +| San Jose | 16 | purple | + +现在我们可以按照`landmark`连接`stations`和`colors`,之后选取绘制地图所需的列。 + +```py +joined = stations.join('landmark', colors, 'city') +colored = joined.select('lat', 'long', 'name', 'color') +Marker.map_table(colored) + +``` + +[地图 3](src/7-3.html) + +现在五个不同城市由五种不同颜色标记。 + +要查看大部分自行车租赁的来源,让我们确定起点站: + +```py +starts = commute.group('Start Station').sort('count', descending=True) +starts +``` + +| Start Station | count | +| --- | --- | --- | +| San Francisco Caltrain (Townsend at 4th) | 25858 | +| San Francisco Caltrain 2 (330 Townsend) | 21523 | +| Harry Bridges Plaza (Ferry Building) | 15543 | +| Temporary Transbay Terminal (Howard at Beale) | 14298 | +| 2nd at Townsend | 13674 | +| Townsend at 7th | 13579 | +| Steuart at Market | 13215 | +| Embarcadero at Sansome | 12842 | +| Market at 10th | 11523 | +| Market at Sansome | 11023 | + +(省略了 60 行) + +我们可以包含映射这些车站所需的地理数据,首先连接`starts `的`stations`: + +```py +station_starts = stations.join('name', starts, 'Start Station') +station_starts +``` + + +| name | station_id | lat | long | dockcount | landmark | installation | count | +| --- | --- | --- | --- | --- | --- | --- | --- | +| 2nd at Folsom | 62 | 37.7853 | -122.396 | 19 | San Francisco | 8/22/2013 | 7841 | +| 2nd at South Park | 64 | 37.7823 | -122.393 | 15 | San Francisco | 8/22/2013 | 9274 | +| 2nd at Townsend | 61 | 37.7805 | -122.39 | 27 | San Francisco | 8/22/2013 | 13674 | +| 5th at Howard | 57 | 37.7818 | -122.405 | 15 | San Francisco | 8/21/2013 | 7394 | +| Adobe on Almaden | 5 | 37.3314 | -121.893 | 19 | San Jose | 8/5/2013 | 522 | +| Arena Green / SAP Center | 14 | 37.3327 | -121.9 | 19 | San Jose | 8/5/2013 | 590 | +| Beale at Market | 56 | 37.7923 | -122.397 | 19 | San Francisco | 8/20/2013 | 8135 | +| Broadway St at Battery St | 82 | 37.7985 | -122.401 | 15 | San Francisco | 1/22/2014 | 7460 | +| California Ave Caltrain Station | 36 | 37.4291 | -122.143 | 15 | Palo Alto | 8/14/2013 | 300 | +| Castro Street and El Camino Real | 32 | 37.386 | -122.084 | 11 | Mountain View | 12/31/2013 | 1137 | + +(省略了 58 行) + +现在我们只提取绘制地图所需的数据,为每个站添加一个颜色和一个面积。 面积是起始于每个站点的租用次数的 1000 倍,其中选择了常数 1000,以便在地图上以适当的比例绘制圆圈。 + +```py +starts_map_data = station_starts.select('lat', 'long', 'name').with_columns( + 'color', 'blue', + 'area', station_starts.column('count') * 1000 +) +starts_map_data.show(3) +Circle.map_table(starts_map_data) +``` + + +| lat | long | name | color | area | +| --- | --- | --- | --- | --- | +| 37.7853 | -122.396 | 2nd at Folsom | blue | 7841000 | +| 37.7823 | -122.393 | 2nd at South Park | blue | 9274000 | +| 37.7805 | -122.39 | 2nd at Townsend | blue | 13674000 | + +(省略了 65 行) + +[地图 4](src/7-4.html) + +旧金山的一大块表明,这个城市的东部是湾区自行车租赁的重点区域。 diff --git a/docs/data8-textbook-zh/8.md b/docs/data8-textbook-zh/8.md new file mode 100644 index 0000000000000000000000000000000000000000..129f332c6f46ce8e4ec6bcbb91d07377bc67ae79 --- /dev/null +++ b/docs/data8-textbook-zh/8.md @@ -0,0 +1,1001 @@ +# 八、随机性 + +> 原文:[Randomness](https://github.com/data-8/textbook/tree/gh-pages/chapters/08) + +> 译者:[飞龙](https://github.com/wizardforcel) + +> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) + +> 自豪地采用[谷歌翻译](https://translate.google.cn/) + +在前面的章节中,我们开发了深入描述数据所需的技能。 数据科学家也必须能够理解随机性。 例如,他们必须能够随机将个体分配到实验组和对照组,然后试图说明,观察到的两组结果之间的差异是否仅仅是由于随机分配,或真正由于实验所致。 + +在这一章中,我们开始分析随机性。 首先,我们将使用 Python 进行随机选择。 在`numpy`中有一个叫做`random`的子模块,它包含许多涉及随机选择的函数。 其中一个函数称为`choice`。 它从一个数组中随机选取一个项目,选择任何项目都是等可能的。 函数调用是`np.random.choice(array_name)`,其中`array_name`是要从中进行选择的数组的名称。 + +因此,下面的代码以 50% 的几率求值为`treatment`,50% 的机率为`control`。 + +```py +two_groups = make_array('treatment', 'control') +np.random.choice(two_groups) +'treatment' +``` + +上面的代码和我们迄今运行的所有其他代码之间的巨大差异在于,上面的代码并不总是返回相同的值。 它可以返回`treatment`或`control`,我们不会提前知道会选择哪一个。 我们可以通过提供第二个参数来重复这个过程,它是重复这个过程的次数。 + +```py +np.random.choice(two_groups, 10) +array(['treatment', 'control', 'treatment', 'control', 'control', + 'treatment', 'treatment', 'control', 'control', 'control'], + dtype=' 1 + 1 +True +``` + +`True`表示比较是有效的;Python 已经证实了`3`和`1 + 1`的关系的这个简单事实。 下面列出了一整套通用的比较运算符。 + + +| 比较 | 运算符 | True 示例 | False 示例 | +| --- | --- | --- | --- | +| 小于 | `<` | `2 < 3` | `2 < 2` | +| 大于 | `>` | `3 > 2` | `3 > 3` | +| 小于等于 | `<=` | `2 <= 2` | `3 <= 2` | +| 大于等于 | `>=` | `3 >= 3` | `2 >= 3` | +| 等于 | `==` | `3 == 3` | `3 == 2` | +| 不等于 | `!=` | `3 != 2` | `2 != 2` | + +注意比较中的两个等号`==`用于确定相等性。 这是必要的,因为 Python 已经使用`=`来表示名称的赋值,我们之前看到过。 它不能将相同的符号用于不同的目的。 因此,如果你想检查`5`是否等于`10/2`,那么你必须小心:`5 = 10/`2返回一个错误信息,因为 Python 假设你正试图将表达式`10/2`的值赋给一个名称,它是数字`5`。相反,你必须使用`5 == 10/2`,其计算结果为`True`。 + +```py +5 = 10/2 + File "", line 1 + 5 = 10/2 + ^ +SyntaxError: can't assign to literal +5 == 10/2 +True +``` + +一个表达式可以包含多个比较,并且它们都必须满足,为了整个表达式为真。 例如,我们可以用下面的表达式表示`1 + 1`在`1`和`3`之间。 + +```py +1 < 1 + 1 < 3 +True +``` + +两个数字的平均值总是在较小的数字和较大的数字之间。 我们用下面的数字`x`和`y`来表示这种关系。 你可以尝试不同的`x`和`y`值来确认这种关系。 + +```py +x = 12 +y = 5 +min(x, y) <= (x+y)/2 <= max(x, y) +True +``` + +### 字符串比较 + +字符串也可以比较,他们的顺序是字典序。 较短的字符串小于以较短的字符串开头的较长的字符串。 + +```py +'Dog' > 'Catastrophe' > 'Cat' +``` + +我们回到随机选择。 回想一下由两个元素组成的数组`two_groups`,`treatment`和`control`。 为了看一个随机分配的个体是否去了实验组,你可以使用比较: + +```py +np.random.choice(two_groups) == 'treatment' +False +``` + +和以前一样,随机选择并不总是一样的,所以比较的结果也不总是一样的。 这取决于是选择`treatment`还是`control`。 对于任何涉及随机选择的单元格,多次运行单元格来获得结果的变化是一个好主意。 + +## 比较数组和值 + +回想一下,我们可以对数组中的很多数字执行算术运算。 例如,`make_array(0, 5, 2)*2`等同于`make_array(0, 10, 4)`。 以类似的方式,如果我们比较一个数组和一个值,则数组的每个元素都与该值进行比较,并将比较结果求值为布尔值数组。 + +```py +tosses = make_array('Tails', 'Heads', 'Tails', 'Heads', 'Heads') +tosses == 'Heads' +array([False, True, False, True, True], dtype=bool) +``` + +`numpy`方法`count_nonzero`计算数组的非零(即`True`)元素的数量。 + +```py +np.count_nonzero(tosses == 'Heads') +3 +``` + +## 条件语句 + +在许多情况下,行动和结果取决于所满足的一组特定条件。例如,随机对照试验的个体如果被分配给实验组,则接受实验。赌徒如果赢了赌注就赚钱。 + +在本节中,我们将学习如何使用代码来描述这种情况。条件语句是一个多行语句,它允许 Python 根据表达式的真值选择不同的选项。虽然条件语句可以出现在任何地方,但它们通常出现在函数体内,以便根据参数值执行可变的行为。 + +条件语句总是以`if`开头,这是一行,后面跟着一个缩进的主体。只有当`if`后面的表达式(称为`if`表达式)求值为真时,才会执行主体。如果`if`表达式的计算结果为`False`,则跳过`if`的主体。 + +让我们开始定义一个返回数字符号的函数。 + +```py +def sign(x): + + if x > 0: + return 'Positive' +sign(3) +'Positive' +``` + +如果输入是正数,则此函数返回正确的符号。 但是,如果输入不是正数,那么`if`表达式的计算结果为`false`,所以`return`语句被跳过,函数调用没有值(为`None`)。 + +```py +sign(-3) +``` + +所以,让我们改进我们的函数来返回负数,如果输入是负数。 我们可以通过添加一个`elif`子句来实现,其中`elif`是 Python 的`else, if`的缩写。 + +```py +def sign(x): + + if x > 0: + return 'Positive' + + elif x < 0: + return 'Negative' +``` + +现在当输入为`-3`时,`sign`返回正确答案。 + +```py +sign(-3) +'Negative' +``` + +那么如果输入是`0`呢?为了处理这个情况,我们可以添加`elif`子句: + +```py +def sign(x): + + if x > 0: + return 'Positive' + + elif x < 0: + return 'Negative' + + elif x == 0: + return 'Neither positive nor negative' +sign(0) +'Neither positive nor negative' +``` + +与之等价,我们可以用`else`子句替换最后的`elif`子句,只有前面的所有比较都是`false`,才会执行它的正文。 也就是说,输入值等于`0`的时候。 + +```py +def sign(x): + + if x > 0: + return 'Positive' + + elif x < 0: + return 'Negative' + + else: + return 'Neither positive nor negative' +sign(0) +'Neither positive nor negative' +``` + +### 一般形式 + +条件语句也可以有多个具有多个主体的子句,只有其中一个主体可以被执行。 多子句的条件语句的一般格式如下所示。 + +```py +if : + +elif : + +elif : + +... +else: + +``` + +总是只有一个`if`子句,但是可以有任意数量的`elif`子句。 Python 将依次求解头部的`if`和`elif`表达式,直到找到一个真值,然后执行相应的主体。 `else`子句是可选的。 当提供`else`头部时,只有在前面的子句的头部表达式都不为真时才执行`else`头部。 `else`子句必须总是在最后(或根本没有)。 + +### 示例:"另一个" + +现在我们将使用条件语句来定义一个看似相当虚假和对立的函数,但是在本章后面的章节中会变得方便。 它需要一个数组,包含两个元素(例如,`red`和`blue`),以及另一个用于比较的元素。 如果该元素为`red`,则该函数返回`blue`。 如果元素是(例如)`blue`,则函数返回`red`。 这就是为什么我们要将函数称为`other_one`。 + +```py +def other_one(x, a_b): + + """Compare x with the two elements of a_b; + if it is equal to one of them, return the other one; + if it is not equal to either of them, return an error message. + """ + if x == a_b.item(0): + return a_b.item(1) + + elif x == a_b.item(1): + return a_b.item(0) + + else: + return 'The input is not valid.' +colors = make_array('red', 'blue') +other_one('red', colors) +'blue' +other_one('blue', colors) +'red' +other_one('potato', colors) +'The input is not valid.' +``` + +## 迭代 + +编程中经常出现这样的情况,特别是在处理随机性时,我们希望多次重复一个过程。 例如,要检查`np.random.choice`是否实际上是随机选取的,我们可能需要多次运行下面的单元格,以查看`Heads`是否以大约 50% 的几率出现。 + +```py +np.random.choice(make_array('Heads', 'Tails')) +'Heads' +``` + +我们可能希望重新运行代码,带有稍微不同的输入或其他稍微不同的行为。 我们可以多次复制粘贴代码,但是这很枯燥,容易出现拼写错误,如果我们想要这样做一千次或一百万次,忘记它吧。 + +更自动化的解决方案是使用`for`语句遍历序列的内容。 这被称为迭代。 `for`语句以单词`for`开头,后面跟着一个名字,我们要把这个序列中的每个项目赋给它,后面跟着单词`in`,最后以一个表达式结束,它求值为一个序列。 对于序列中的每个项目,`for`语句的缩进主体执行一次。 + +```py +for i in np.arange(3): + print(i) +0 +1 +2 +``` + +想象一下,没有`for`语句的情况下,完全实现`for`语句功能的代码,这样很有帮助。 (这被称为循环展开。)`for`语句简单地复制了内部的代码,但是在每次迭代之前,它从给定的序列中将我们选择的名称赋为一个新的值。 例如,以下是上面循环的展开版本: + +```py +i = np.arange(3).item(0) +print(i) +i = np.arange(3).item(1) +print(i) +i = np.arange(3).item(2) +print(i) +0 +1 +2 +``` + +> 译者注:实际的实现方式不是这样,但是效果一样。这里不做深究。 + +请注意,我的名字是任意的,就像我们用`=`赋值的名字一样。 + +在这里我们用一个更为现实的方式使用`for`语句:我们从数组中打印`5`个随机选项。 + +```py +coin = make_array('Heads', 'Tails') + +for i in np.arange(5): + print(np.random.choice(make_array('Heads', 'Tails'))) +Heads +Heads +Tails +Heads +Heads +``` + +在这种情况下,我们只执行了几次完全相同的(随机)操作,所以我们`for`语句中的代码实际上并不涉及到`i`。 + +### 扩展数组 + +虽然上面的`for`语句确实模拟了五次硬币投掷的结果,但结果只是简单地打印出来,并不是我们可以用来计算的形式。 因此,`for`语句的典型用法是创建一个结果数组,每次都扩展它。 + +`numpy`中的`append`方法可以帮助我们实现它。 调用`np.append(array_name,value)`将求出一个新的数组,它是由`value`扩展的`array_name`。在使用`append`时请记住,数组的所有条目必须具有相同的类型。 + +```py +pets = make_array('Cat', 'Dog') +np.append(pets, 'Another Pet') +array(['Cat', 'Dog', 'Another Pet'], + dtype='>> is_goat('Goat 1') + True + >>> is_goat('Goat 2') + True + >>> is_goat('Car') + False + """ + if door_name == "Goat 1": + return True + elif door_name == "Goat 2": + return True + else: + return False + + +def monty_hall(): + + """ Play the Monty Hall game once + and return an array of three strings: + + original choice, what Monty throws out, what remains + """ + + original = np.random.choice(doors) + + if is_goat(original): + return make_array(original, other_one(original, goats), 'Car') + + else: + throw_out = np.random.choice(goats) + return make_array(original, throw_out, other_one(throw_out, goats)) +``` + +让我们玩几次这个游戏。这里是一个结果。你应该运行几次单元格来观察结果如何变化。 + +```py +monty_hall() +array(['Car', 'Goat 2', 'Goat 1'], + dtype=' 原文:[Empirical Distributions](https://github.com/data-8/textbook/tree/gh-pages/chapters/09) + +> 译者:[飞龙](https://github.com/wizardforcel) + +> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) + +> 自豪地采用[谷歌翻译](https://translate.google.cn/) + +大部分数据科学都涉及来自大型随机样本的数据。 在本节中,我们将研究这些样本的一些属性。 + +我们将从一个简单的实验开始:多次掷骰子并跟踪出现的点数。 `die `表包含骰子面上的点数。 所有的数字只出现一次,因为我们假设骰子是平等的。 + +```py +die = Table().with_column('Face', np.arange(1, 7, 1)) +die +``` + + +| Face | +| --- | +| 1 | +| 2 | +| 3 | +| 4 | +| 5 | +| 6 | + +### 概率分布 + +下面的直方图帮助我们可视化,每个面出现概率为 1/6 事实。 我们说直方图显示了所有可能的面的概率分布。 由于所有的条形表示相同的百分比几率,所以这个分布成为整数 1 到 6 上的均匀分布。 + +```py +die_bins = np.arange(0.5, 6.6, 1) +die.hist(bins = die_bins) +``` + +![](img/9-1.png) + +递增值由相同的固定量分隔,例如骰子面上的值(递增值由 1 分隔),这样的变量被称为离散值。上面的直方图被称为离散直方图。它的桶由数组`die_bins`指定,并确保每个条形的中心是对应的整数值。 + +重要的是要记住,骰子不能显示 1.3 个点或 5.2 个点 - 总是显示整数个点。但是我们的可视化将每个值的概率扩展到条形区域。虽然在本课程的这个阶段这看起来有些随意,但是稍后当我们在离散直方图上叠加平滑曲线时,这将变得很重要。 + +在继续之前,让我们确保轴域上的数字是有意义的。每个面的概率是 1/6,四舍五入到小数点后两位的概率是 16.67%。每个桶的宽度是 1 个单位。所以每个条形的高度是每单位 16.67%。这与图形的水平和垂直比例一致。 + +### 经验分布 + +上面的分布由每个面的理论概率组成。 这不基于数据。 不投掷任何骰子,它就可以被研究和理解。 + +另一方面,经验分布是观测数据的分布。 他们可以通过经验直方图可视化。 + +让我们通过模拟一个骰子的投掷来获得一些数据。 这可以通过 1 到 6 的整数的带放回随机抽样来完成。为了使用 Python 来实现,我们将使用`Table`的`sample`方法,它带放回地随机抽取表中的行。它的参数是样本量,它返回一个由选定的行组成的表。 `with_replacement=False`的可选参数指定了应该抽取样本而不放回,但不适用于投掷骰子。 + +这是一个十次骰子投掷的结果。 + +```py +die.sample(10) +``` + + +| Face | +| --- | +| 5 | +| 3 | +| 3 | +| 4 | +| 2 | +| 2 | +| 4 | +| 1 | +| 6 | +| 6 | + +我们可以使用相同的方法来模拟尽可能多的投掷,然后绘制结果的经验直方图。 因为我们要反复这样做,所以我们定义了一个函数`empirical_hist_die`,它以样本量为参数;该函数根据其参数多次投掷骰子,然后绘制直方图。 + +```py +def empirical_hist_die(n): + die.sample(n).hist(bins = die_bins) +``` + +### 经验直方图 + + +这是十次投掷的经验直方图。 它看起来不像上面的概率直方图。 运行该单元格几次,看看它如何变化。 + +```py +empirical_hist_die(10) +``` + +![](img/9-2.png) + +当样本量增加时,经验直方图开始看起来更像是理论概率的直方图。 + +```py +empirical_hist_die(100) +``` + +![](img/9-3.png) + +```py +empirical_hist_die(1000) +``` + +![](img/9-4.png) + +当我们增加模拟中的投掷次数时,每个条形的面积接近 16.67%,这是概率直方图中每个条形的面积。 + +我们在实例中观察到了一般规则: + +### 平均定律 + +如果偶然的实验在相同的条件下独立重复,那么从长远来看,事件发生的频率越来越接近事件的理论概率。 + +例如,从长远来看,四点的比例越来越接近 1/6。 + +这里“独立地且在相同的条件下”意味着,无论所有其他重复的结果如何,每个重复都以相同的方式执行。 + +## 从总体中取样 + +当随机样本来自较大总体时,平均定律也成立。 + +作为一个例子,我们将研究航班延误时间的总体。 `united `表包含 2015 年夏天从旧金山出发的美联航国内航班的数据。数据由[美国运输部运输统计局](http://www.transtats.bts.gov/Fields.asp?Table_ID=293)公布。 + +这里有 13,825 行,每行对应一个航班。 列是航班日期,航班号,目的地机场代码和以分钟为单位的出发延误时间。有些延误时间是负的;那些航班提前离开。 + +```py +united = Table.read_table('united_summer2015.csv') +united +``` + + +| Date | Flight Number | Destination | Delay | +| --- | --- | --- | --- | +| 6/1/15 | 73 | HNL | 257 | +| 6/1/15 | 217 | EWR | 28 | +| 6/1/15 | 237 | STL | -3 | +| 6/1/15 | 250 | SAN | 0 | +| 6/1/15 | 267 | PHL | 64 | +| 6/1/15 | 273 | SEA | -6 | +| 6/1/15 | 278 | SEA | -8 | +| 6/1/15 | 292 | EWR | 12 | +| 6/1/15 | 300 | HNL | 20 | +| 6/1/15 | 317 | IND | -10 | + +(省略了 13815 行) + +一个航班提前 16 分钟起飞,另一个航班延误 580 分钟。 其他延迟时间几乎都在 -10 分钟到 200 分钟之间,如下面的直方图所示。 + +```py +united.column('Delay').min() +-16 + +united.column('Delay').max() +580 + +delay_bins = np.append(np.arange(-20, 301, 10), 600) +united.select('Delay').hist(bins = delay_bins, unit = 'minute') +``` + +![](img/9-5.png) + +就本节而言,仅仅关注部分数据就足够了,我们忽略延迟超过 200 分钟的 0.8% 的航班。 这个限制只是为了视觉便利。 该表仍然保留所有的数据。 + +```py +united.where('Delay', are.above(200)).num_rows/united.num_rows +0.008390596745027125 + +delay_bins = np.arange(-20, 201, 10) +united.select('Delay').hist(bins = delay_bins, unit = 'minute') +``` + +![](img/9-6.png) + +`[0,10)`的条形高度不到每分钟 3%,这意味着只有不到 30% 的航班延误了 0 到 10 分钟。 这是通过行的计数来确认的: + +```py +united.where('Delay', are.between(0, 10)).num_rows/united.num_rows +0.2935985533453888 +``` + +### 样本的经验分布 + +现在让我们将这 13,825 个航班看做一个总体,并从中带放回地抽取随机样本。 将我们的分析代码打包成一个函数是有帮助的。 函数`empirical_hist_delay`以样本量为参数,绘制结果的经验直方图。 + +```py +def empirical_hist_delay(n): + united.sample(n).select('Delay').hist(bins = delay_bins, unit = 'minute') +``` + +正如我们用骰子所看到的,随着样本量的增加,样本的经验直方图更接近于总体的直方图。 将这些直方图与上面的总体直方图进行比较。 + +```py +empirical_hist_delay(10) +``` + +![](img/9-7.png) + +```py +empirical_hist_delay(100) +``` + +![](img/9-8.png) + +最一致的可见差异在总体中罕见的值之中。 在我们的示例中,这些值位于分布的最右边。 但随着样本量的增加,这些值以大致正确的比例,开始出现在样本中。 + +```py +empirical_hist_delay(1000) +``` + +![](img/9-9.png) + +### 样本的经验直方图的总结 + +我们在本节中观察到的东西,可以总结如下: + +对于大型随机样本,样本的经验直方图类似于总体的直方图,概率很高。 + +这证明了,在统计推断中使用大型随机样本是合理的。 这个想法是,由于大型随机样本可能类似于从中抽取的总体,从样本中计算出的数量可能接近于总体中相应的数量。 + +## 轮盘赌 + +上面的分布让我们对整个随机样本有了印象。但有时候我们只是对基于样本计算的一个或两个量感兴趣。 + +例如,假设样本包含一系列投注的输赢。那么我们可能只是对赢得的总金额感兴趣,而不是输赢的整个序列。 + +使用我们的几率长期行为的新知识,让我们探索赌博游戏。我们将模拟轮盘赌,它在拉斯维加斯和蒙特卡洛等赌场中受欢迎。 + +在内华达,轮盘赌的主要随机器是一个带有 38 个口袋的轮子。其中两个口袋是绿色的,十八个黑色,十八个红色。轮子在主轴上,轮子上有一个小球。当轮子旋转时,球体跳起来,最后落在其中一个口袋里。这就是获胜的口袋。 + +`wheel`表代表内华达轮盘赌的口袋。 + +```py +wheel +``` + +| Pocket | Color | +| --- | --- | +| 0 | green | +| 00 | green | +| 1 | red | +| 2 | black | +| 3 | red | +| 4 | black | +| 5 | red | +| 6 | black | +| 7 | red | +| 8 | black | + +(省略了 28 行) + +你可以对轮盘赌桌上展示的几个预先指定的口袋下注。 如果你对“红色”下注,如果球落在红色的口袋里,你就赢了。 + +红色的下注返回相等的钱。 也就是说,它支付一比一。为了理解这是什么意思,假设你在“红色”下注一美元。 第一件事情发生之前,即使在车轮旋转之前,你必须交出你的一美元。 如果球落在绿色或黑色的口袋里,你就失去它了。 如果球落在红色的口袋里,你会把你的钱拿回来(让你不输不赢),再加上另外一美元的奖金。 + +函数`red_winnings`以一个颜色作为参数,如果颜色是红色,则返回`1`。 对于所有其他颜色,它返回`-1`。 我们将`red_winnings`应用于`wheel`的`Color`列,来获得新的表`bets`,如果你对红色下注一美元,它显示每个口袋的净收益。 + +```py +def red_winnings(color): + if color == 'red': + return 1 + else: + return -1 +bets = wheel.with_column( + 'Winnings: Red', wheel.apply(red_winnings, 'Color') + ) +bets +``` + + +| Pocket | Color | Winnings: Red | +| --- | --- | --- | +| 0 | green | -1 | +| 00 | green | -1 | +| 1 | red | 1 | +| 2 | black | -1 | +| 3 | red | 1 | +| 4 | black | -1 | +| 5 | red | 1 | +| 6 | black | -1 | +| 7 | red | 1 | +| 8 | black | -1 | + +(省略了 28 行) + +假设我们决定对红色下注一美元,会发生什么呢? + +这里是一轮的模拟。 + +```py +one_spin = bets.sample(1) +one_spin +``` + + +| Pocket | Color | Winnings: Red | +| --- | --- | --- | +| 14 | red | 1 | + +这轮的颜色是`Color`列中的值。 无论你的赌注如何,结果可能是红色,绿色或黑色。 要看看这些事件发生的频率,我们可以模拟许多这样的单独轮次,并绘制出我们所看到的颜色的条形图。 (我们可以称之为经验条形图。) + +为了实现它,我们可以使用`for`循环。 我们在这里选择了重复 5000 次,但是当你运行这个单元格时,你可以改变它。 + +```py +num_simulations = 5000 + +colors = make_array() +winnings_on_red = make_array() + +for i in np.arange(num_simulations): + spin = bets.sample(1) + new_color = spin.column("Color").item(0) + colors = np.append(colors, new_color) + new_winnings = spin.column('Winnings: Red') + winnings_on_red = np.append(winnings_on_red, new_winnings) + +Table().with_column('Color', colors)\ + .group('Color')\ + .barh('Color') +``` + +![](img/9-10.png) + +38 个口袋里有 18 个是红色的,每个口袋都是等可能的。 因此,在 5000 次模拟中,我们预计大致(但可能不是完全)看到`18/38*5000`或者 2,368 次红色。模拟证明了这一点。 + +在模拟中,我们也记录了你的奖金。 这些经验直方图显示了,你对红色下注的不同结果的(近似)几率。 + +```py +Table().with_column('Winnings: Red', winnings_on_red)\ + .hist(bins = np.arange(-1.55, 1.65, .1)) +``` + +![](img/9-11.png) + +每个模拟的唯一可能的结果是,你赢了一美元或输了一美元,这反映在直方图中。 我们也可以看到,你赢的次数要比输的次数少一点。 你喜欢这个赌博策略吗? + +### 多次游戏 + +大多数轮盘赌玩家玩好几轮。 假设你在 200 次独立轮次反复下注一美元。 你总共会赚多少钱? + +这里是一套 200 轮的模拟。 `spins`表包括所有 200 个赌注的结果。 你的净收益是`Winnings: Red`列中所有 +1 和 -1 的和。 + +```py +spins = bets.sample(200) +spins.column('Winnings: Red').sum() +-26 +``` + +运行几次单元格。 有时你的净收益是正的,但更多的时候它似乎是负的。 + +为了更清楚地看到发生了什么,让我们多次模拟 200 轮,就像我们模拟一轮那样。 对于每次模拟,我们将记录来自 200 轮的总奖金。 然后我们将制作 5000 个不同的模拟总奖金的直方图。 + +```py +num_spins = 200 + +net_gain = make_array() + +for i in np.arange(num_simulations): + spins = bets.sample(num_spins) + new_net_gain = spins.column('Winnings: Red').sum() + net_gain = np.append(net_gain, new_net_gain) + +Table().with_column('Net Gain on Red', net_gain).hist() +``` + +![](img/9-12.png) + +注意横轴上 0 的位置。 这就是你不赚不赔的地方。 通过使用这个赌博策略,你喜欢这个赚钱几率吗? + +如果对红色下注不吸引人,也许值得尝试不同的赌注。 “分割”(Split)是轮盘赌桌上两个相邻号码的下注,例如 0 和 00。分割的回报是 17 比 1。 + +`split_winnings`函数将口袋作为参数,如果口袋是 0 或 00,则返回 17。对于所有其他口袋,返回 -1。 + +表格`more_bets`是投注表格的一个版本,扩展的一列是对 0/00 分割下注的情况下,每个口袋的奖金。 + +```py +def split_winnings(pocket): + if pocket == '0': + return 17 + elif pocket == '00': + return 17 + else: + return -1 +more_bets = wheel.with_columns( + 'Winnings: Red', wheel.apply(red_winnings, 'Color'), + 'Winnings: Split', wheel.apply(split_winnings, 'Pocket') + ) +more_bets +``` + +| Pocket | Color | Winnings: Red | Winnings: Split | +| --- | --- | --- | --- | +| 0 | green | -1 | 17 | +| 00 | green | -1 | 17 | +| 1 | red | 1 | -1 | +| 2 | black | -1 | -1 | +| 3 | red | 1 | -1 | +| 4 | black | -1 | -1 | +| 5 | red | 1 | -1 | +| 6 | black | -1 | -1 | +| 7 | red | 1 | -1 | +| 8 | black | -1 | -1 | + +(省略了 28 行) + +下面的代码模拟了两个投注的结果 - 红色和 0/00 分割 - 在 200 轮中。 代码与以前的模拟相同,除了添加了 Split。 (注意:`num_simulations`和`num_spins`之前分别定义为 5,000 和 200,所以我们不需要再次定义它们。) + +```py +net_gain_red = make_array() +net_gain_split = make_array() + +for i in np.arange(num_simulations): + spins = more_bets.sample(num_spins) + new_net_gain_red = spins.column('Winnings: Red').sum() + net_gain_red = np.append(net_gain_red, new_net_gain_red) + new_net_gain_split = spins.column('Winnings: Split').sum() + net_gain_split = np.append(net_gain_split, new_net_gain_split) + +Table().with_columns( + 'Net Gain on Red', net_gain_red, + 'Net Gain on Split', net_gain_split + ).hist(bins=np.arange(-200, 200, 20)) +``` + +![](img/9-13.png) + +横轴上 0 的位置表明,无论你选择哪种赌注,你都更有可能赔钱而不是赚钱。在两个直方图中,不到 50% 的区域在 0 的右侧。 + +然而,分割的赌注赚钱几率更大,赚取超过 50 美元的机会也是如此。 金色直方图有很多区域在五十美元的右侧,而蓝色直方图几乎没有。 那么你应该对分割下注吗? + +这取决于你愿意承担多少风险,因为直方图还表明,如果你对分割下注,你比对红色下注更容易损失超过 50 美元。 + +轮盘赌桌上,所有赌注的单位美元的预期净损失相同(除了线注,这是更糟的)。 但一些赌注的回报比其他赌注更为可变。 你可以选择这些赌注,只要你准备好可能会大输一场。 + +## 统计量的经验分布 + +平均定律意味着,大型随机样本的经验分布类似于总体的分布,概率相当高。 + +在两个直方图中可以看到相似之处:大型随机样本的经验直方图很可能类似于总体的直方图。 + +提醒一下,这里是所有美联航航班延误的直方图,以及这些航班的大小为 1000 的随机样本的经验直方图。 + +```py +united = Table.read_table('united_summer2015.csv') +delay_bins = np.arange(-20, 201, 10) +united.select('Delay').hist(bins = delay_bins, unit = 'minute') +plots.title('Population'); +``` + +![](img/9-14.png) + +```py +sample_1000 = united.sample(1000) +sample_1000.select('Delay').hist(bins = delay_bins, unit = 'minute') +plots.title('Sample of Size 1000'); +``` + +![](img/9-15.png) + +两个直方图明显相似,虽然他们并不等价。 + +### 参数 + +我们经常对总体相关的数量感兴趣。 + +在选民的总体中,有多少人会投票给候选人 A 呢? +在 Facebook 用户的总体中,用户最多拥有的 Facebook 好友数是多少? +在美联航航班的总体中,起飞延误时间的中位数是多少? + +与总体相关的数量被称为参数。 对于美联航航班的总体,我们知道参数“延误时间的中位数”的值: + +```py +np.median(united.column('Delay')) +2.0 +``` + +NumPy 函数`median`返回数组的中值(中位数)。 在所有的航班中,延误时间的中位数为 2 分钟。 也就是说,总体中约有 50% 的航班延误了 2 分钟以内: + +```py +united.where('Delay', are.below_or_equal_to(2)).num_rows/united.num_rows +0.5018444846292948 +``` + +一半的航班在预定起飞时间的 2 分钟之内起飞。 这是非常短暂的延误! + +注意。 由于“重复”,百分比并不完全是 50,也就是说,延误了 2 分钟的航班有 480 个。数据集中的重复很常见,我们不会在这个课程中担心它。 + +```py +united.where('Delay', are.equal_to(2)).num_rows +480 +``` + +### 统计 + +在很多情况下,我们会感兴趣的是找出未知参数的值。 为此,我们将依赖来自总体的大型随机样本的数据。 + +统计量(注意是单数!)是使用样本中数据计算的任何数字。 因此,样本中位数是一个统计量。 + +请记住,`sample_1000`包含来自`united`的 1000 个航班的随机样本。 样本中位数的观测值是: + +```py +np.median(sample_1000.column('Delay')) +2.0 +``` + +我们的样本 - 一千个航班 - 给了我们统计量的观测值。 这提出了一个重要的推论问题: + +统计量的数值可能会有所不同。 使用基于随机样本的任何统计量时,首先考虑的事情是,样本可能不同,因此统计量也可能不同。 + +```py +np.median(united.sample(1000).column('Delay')) +3.0 +``` + +运行单元格几次来查看答案的变化。 通常它等于 2,与总体参数值相同。 但有时候不一样。 + +统计量有多么不同? 回答这个问题的一种方法是多次运行单元格,并记下这些值。 这些值的直方图将告诉我们统计量的分布。 + +我们将使用`for`循环来“多次运行单元格”。 在此之前,让我们注意模拟中的主要步骤。 + +### 模拟统计量 + +我们将使用以下步骤来模拟样本中位数。 你可以用任何其他样本量来替换 1000 的样本量,并将样本中位数替换为其他统计量。 + +第一步:生成一个统计量。 抽取大小为 1000 的随机样本,并计算样本的中位数。 注意中位数的值。 + +第二步:生成更多的统计值。 重复步骤 1 多次,每次重新抽样。 + +第三步:结果可视化。 在第二步结束时,你将会记录许多样本中位数,每个中位数来自不同的样本。 你可以在表格中显示所有的中位数。 你也可以使用直方图来显示它们 - 这是统计量的经验直方图。 + +我们现在执行这个计划。 正如在所有的模拟中,我们首先创建一个空数组,我们在其中收集我们的结果。 + ++ 上面的第一步是`for`循环的主体。 ++ 第二步,重复第一步“无数次”,由循环完成。 我们“无数次”是5000次,但是你可以改变这个。 ++ 第三步是显示表格,并在后面的单元格中调用`hist`。 + +该单元格需要大量的时间来运行。 那是因为它正在执行抽取大小为 1000 的样本,并计算其中位数的过程,重复 5000 次。 这是很多抽样和重复! + +```py +medians = make_array() + +for i in np.arange(5000): + new_median = np.median(united.sample(1000).column('Delay')) + medians = np.append(medians, new_median) + +Table().with_column('Sample Median', medians) +``` + + +| Sample Median | +| --- | +| 3 | +| 2 | +| 2 | +| 3 | +| 2 | +| 2 | +| 2 | +| 3 | +| 1 | +| 3 | + +(省略了 4990 行) + +```py +Table().with_column('Sample Median', medians).hist(bins=np.arange(0.5, 5, 1)) +``` + +![](img/9-16.png) + +你可以看到样本中位数很可能接近 2,这是总体中位数的值。 由于 1000 次航班延误的样本可能与延误总体相似,因此这些样本的延误中位数应接近总体的延误中位数,也就不足为奇了。 + +这是一个例子,统计量如何较好估计参数。 + +### 模拟的威力 + +如果我们能够生成所有可能的大小为 1000 的随机样本,我们就可以知道所有可能的统计量(样本中位数),以及所有这些值的概率。我们可以在统计量的概率直方图中可视化所有值和概率。 + +但在许多情况下(包括这个),所有可能的样本数量足以超过计算机的容量,概率的纯粹数学计算可能有些困难。 + +这是经验直方图的作用。 + +我们知道,如果样本量很大,并且如果重复抽样过程无数次,那么根据平均定律,统计量的经验直方图可能类似于统计量的概率直方图。 + +这意味着反复模拟随机过程是一种近似概率分布的方法,不需要在数学上计算概率,或者生成所有可能的随机样本。因此,计算机模拟成为数据科学中的一个强大工具。他们可以帮助数据科学家理解随机数量的特性,这些数据会以其他方式进行分析。 + +这就是这种的模拟的经典例子。 + +### 估计敌军飞机的数量 + +在第二次世界大战中,为盟军工作的数据分析师负责估算德国战机的数量。 这些数据包括盟军观察到的德国飞机的序列号。 这些序列号为数据分析师提供了答案。 + +为了估算战机总数,数据分析人员必须对序列号做出一些假设。 这里有两个这样的假设,大大简化,使我们的计算更容易。 + ++ 战机有`N`架,编号为 `1,2, ..., N`。 + ++ 观察到的飞机从`N`架飞机中均匀、随机带放回地抽取。 + +目标是估计数字`N`。 这是未知的参数。 + +假设你观察一些飞机并记下他们的序列号。 你如何使用这些数据来猜测`N`的值? 用于估计的自然和简单的统计量,就是观察到的最大的序列号。 + +让我们看看这个统计量如何用于估计。 但首先是另一个简化:现在一些历史学家估计,德国的飞机制造业生产了近 10 万架不同类型的战机,但在这里我们只能想象一种。 这使得假设 1 更易于证明。 + +假设实际上有`N = 300 `个这样的飞机,而且你观察到其中的 30 架。 我们可以构造一个名为`serialno`的表,其中包含序列号`1`到`N`。 然后,我们可以带放回取样 30 次(见假设 2),来获得我们的序列号样本。 我们的统计量是这 30 个数字中的最大值。 这就是我们用来估计参数`N`的东西。 + +```py +N = 300 +serialno = Table().with_column('serial Number', np.arange(1, N+1)) +serialno +``` + + +| serial number | +| --- | +| 1 | +| 2 | +| 3 | +| 4 | +| 5 | +| 6 | +| 7 | +| 8 | +| 9 | +| 10 | + +(省略了 290 行) + +```py +serialno.sample(30).column(0).max() +291 +``` + +与所有涉及随机抽样的代码一样,运行该单元几次;来查看变化。你会发现,即使只有 300 个观测值,最大的序列号通常在 250-300 范围内。 + +原则上,最大的序列号可以像 1 那样小,如果你不幸看到了 30 次 1 号机。如果你至少观察到一次 300 号机,它可能会增大到 300。但通常情况下,它似乎处于非常高的 200 以上。看起来,如果你使用最大的观测序列号作为你对总数的估计,你不会有太大的错误。 + +### 模拟统计 + +让我们模拟统计,看看我们能否证实它。模拟的步骤是: + +第一步。从 1 到 300 带放回地随机抽样 30 次,并注意观察到的最大数量。这是统计量。 + +第二步。重复步骤一 750 次,每次重新取样。你可以用任何其他的大数值代替 750。 + +第三步。创建一个表格来显示统计量的 750 个观察值,并使用这些值绘制统计量的经验直方图。 + +```py +sample_size = 30 +repetitions = 750 +maxes = make_array() + +for i in np.arange(repetitions): + sampled_numbers = serialno.sample(sample_size) + maxes = np.append(maxes, sampled_numbers.column(0).max()) + +Table().with_column('Max Serial Number', maxes) +``` + + +| Max Serial Number | +| --- | +| 280 | +| 253 | +| 294 | +| 299 | +| 298 | +| 237 | +| 296 | +| 297 | +| 293 | +| 295 | + +(省略了 740 行) + +```py +every_ten = np.arange(1, N+100, 10) +Table().with_column('Max Serial Number', maxes).hist(bins = every_ten) +``` + +![](img/9-17.png) + +这是 750 个估计值的直方图,每个估计值是统计量“观察到的最大序列号”的观测值。 + +正如你所看到的,尽管在理论上它们可能会小得多,但估计都在 300 附近。直方图表明,作为飞机总数的估计,最大的序列号可能低了大约 10 到 25 个。但是,飞机的真实数量低了 50 个是不太可能的。 + +### 良好的近似 + +我们前面提到过,如果生成所有可能的样本,并计算每个样本的统计量,那么你将准确了解统计量可能有多么不同。事实上,你将会完整地列举统计量的所有可能值及其所有概率。 + +换句话说,你将得到统计量的概率分布和概率直方图。 + +统计量的概率分布也称为统计量的抽样分布,因为它基于所有可能的样本。 + +但是,我们上面已经提到,可能的样本总数往往非常大。例如,如果有 300 架飞机,你可以看到的,30 个序列号的可能序列总数为: + +```py +300**30 +205891132094649000000000000000000000000000000000000000000000000000000000000 +``` + +这是很多样本。 幸运的是,我们不必生成所有这些。 我们知道统计量的经验直方图,基于许多但不是全部可能的样本,是概率直方图的很好的近似。 因此统计量的经验分布让我们很好地了解到,统计量可能有多么不同。 + +确实,统计量的概率分布包含比经验分布更准确的统计量信息。 但是,正如在这个例子中一样,通常经验分布所提供的近似值,足以让数据科学家了解统计量可以变化多少。 如果你有一台计算机,经验分布更容易计算。 因此,当数据科学家试图理解统计的性质时,通常使用经验分布而不是精确的概率分布。 + +### 参数的不同估计 + +这里举一个例子来说明这一点。 到目前为止,我们已经使用了最大的观测序号作为飞机总数的估计。 但还有其他可能的估计,我们现在将考虑其中之一。 + +这个估计的基本思想是观察到的序列号的平均值可能在1到`N`之间。 因此,如果`A`是平均值,那么: + +![](img/tex-9-1.gif) + +因此,可以使用一个新的统计量化来估计飞机总数:取观测到的平均序列号并加倍。 + +与使用最大的观测数据相比,这种估计方法如何? 计算新统计量的概率分布并不容易。 但是和以前一样,我们可以模拟它来近似得到概率。 我们来看看基于重复抽样的统计量的经验分布。 为了便于比较,重复次数选择为 750,与之前的模拟相同。 + +```py +maxes = make_array() +twice_ave = make_array() + +for i in np.arange(repetitions): + sampled_numbers = serialno.sample(sample_size) + + new_max = sampled_numbers.column(0).max() + maxes = np.append(maxes, new_max) + + new_twice_ave = 2*np.mean(sampled_numbers.column(0)) + twice_ave = np.append(twice_ave, new_twice_ave) + + +results = Table().with_columns( + 'Repetition', np.arange(1, repetitions+1), + 'Max', maxes, + '2*Average', twice_ave +) + +results +``` + + +| Repetition | Max | 2*Average | +| --- | --- | --- | +| 1 | 296 | 312.067 | +| 2 | 283 | 290.133 | +| 3 | 290 | 250.667 | +| 4 | 296 | 306.8 | +| 5 | 298 | 335.533 | +| 6 | 281 | 240 | +| 7 | 300 | 317.267 | +| 8 | 295 | 322.067 | +| 9 | 296 | 317.6 | +| 10 | 299 | 308.733 | + +(省略了 740 行) + +请注意,与所观察到的最大数字不同,新的估计值(“平均值的两倍”)可能会高估飞机的数量。 当观察到的序列号的平均值接近于`N`而不是`1`时,就会发生这种情况。 + +下面的直方图显示了两个估计的经验分布。 + +```py +results.drop(0).hist(bins = every_ten) +``` + +![](img/9-18.png) + +你可以看到,原有方法几乎总是低估; 形式上,我们说它是有偏差的。 但它的变异性很小,很可能接近真正的飞机总数。 + +新方法高估了它,和低估的频率一样,因此从长远来看,平均而言大致没有偏差。 然而,它比旧的估计更可变,因此容易出现较大的绝对误差。 + +这是一个偏差 - 变异性权衡的例子,在竞争性估计中并不罕见。 你决定使用哪种估计取决于对你最重要的误差种类。 就敌机而言,低估总数可能会造成严重的后果,在这种情况下,你可能会选择使用更加可变的方法,它一半几率都是高估的。 另一方面,如果高估导致了防范不存在的飞机的不必要的高成本,那么你可能会对低估的方法感到满意。 + +### 技术注解 + +事实上,“两倍均值”不是无偏的。平均而言,它正好高估了 1。例如,如果`N`等于 3,来自`1,2,3`的抽取结果的均值是`2`,`2 x 2 = 4`,它比`N`多了 1。“两倍均值”减 1 是`N`的无偏估计量。 diff --git a/docs/data8-textbook-zh/README.md b/docs/data8-textbook-zh/README.md new file mode 100644 index 0000000000000000000000000000000000000000..51aa05da9e343d385b88b63a994948bdad62d912 --- /dev/null +++ b/docs/data8-textbook-zh/README.md @@ -0,0 +1,54 @@ +# UCB Data8 计算与推断思维 + +> 原书:[data-8/textbook](https://github.com/data-8/textbook/) +> +> 译者:[飞龙](https://github.com/wizardforcel) +> +> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) +> +> 欢迎任何人参与和完善:一个人可以走的很快,但是一群人却可以走的更远。 + ++ [ApacheCN 机器学习交流群 629470233](http://shang.qq.com/wpa/qunwpa?idkey=30e5f1123a79867570f665aa3a483ca404b1c3f77737bc01ec520ed5f078ddef) ++ [ApacheCN 学习资源](http://www.apachecn.org/) ++ [UCB Prob140 课本:面向数据科学的概率论](https://github.com/apachecn/prob140-textbook-zh) ++ [UCB DS100 课本:数据科学的原理与技巧](https://github.com/apachecn/ds100-textbook-zh) + + + ++ [在线阅读](https://data8.apachecn.org) ++ [在线阅读(Gitee)](https://apachecn.gitee.io/data8-textbook-zh/) ++ [PDF格式](https://www.gitbook.com/download/pdf/book/wizardforcel/data8-textbook) ++ [EPUB格式](https://www.gitbook.com/download/epub/book/wizardforcel/data8-textbook) ++ [MOBI格式](https://www.gitbook.com/download/mobi/book/wizardforcel/data8-textbook) ++ [代码仓库](https://github.com/apachecn/data8-textbook-zh) + + +## 下载 + +### Docker + +``` +docker pull apachecn0/data8-textbook-zh +docker run -tid -p :80 apachecn0/data8-textbook-zh +# 访问 http://localhost:{port} 查看文档 +``` + +### PYPI + +``` +pip install data8-textbook-zh +data8-textbook-zh +# 访问 http://localhost:{port} 查看文档 +``` + +### NPM + +``` +npm install -g data8-textbook-zh +data8-textbook-zh +# 访问 http://localhost:{port} 查看文档 +``` + +## 赞助我 + +![](img/qr_alipay.png) diff --git a/docs/data8-textbook-zh/SUMMARY.md b/docs/data8-textbook-zh/SUMMARY.md new file mode 100644 index 0000000000000000000000000000000000000000..875bf4e88491e1c237811401aedbbb43d9ecef85 --- /dev/null +++ b/docs/data8-textbook-zh/SUMMARY.md @@ -0,0 +1,18 @@ ++ [计算与推断思维](README.md) ++ [一、数据科学](1.md) ++ [二、因果和实验](2.md) ++ [三、Python 编程](3.md) ++ [四、数据类型](4.md) ++ [五、表格](5.md) ++ [六、可视化](6.md) ++ [七、函数和表格](7.md) ++ [八、随机性](8.md) ++ [九、经验分布](9.md) ++ [十、假设检验](10.md) ++ [十一、估计](11.md) ++ [十二、为什么均值重要](12.md) ++ [十三、预测](13.md) ++ [十四、回归的推断](14.md) ++ [十五、分类](15.md) ++ [十六、比较两个样本](16.md) ++ [十七、更新预测](17.md) \ No newline at end of file diff --git a/docs/data8-textbook-zh/img/1-1.png b/docs/data8-textbook-zh/img/1-1.png new file mode 100644 index 0000000000000000000000000000000000000000..188eb9e65f7753a728444ab9e6cb391c9cc4cbb2 Binary files /dev/null and b/docs/data8-textbook-zh/img/1-1.png differ diff --git a/docs/data8-textbook-zh/img/1-2.png b/docs/data8-textbook-zh/img/1-2.png new file mode 100644 index 0000000000000000000000000000000000000000..79cb930f04e8ec44cc63a612c04dc404fbc4c414 Binary files /dev/null and b/docs/data8-textbook-zh/img/1-2.png differ diff --git a/docs/data8-textbook-zh/img/1-3.png b/docs/data8-textbook-zh/img/1-3.png new file mode 100644 index 0000000000000000000000000000000000000000..296768b5c918e18e0e76c05c66aefd050679f62a Binary files /dev/null and b/docs/data8-textbook-zh/img/1-3.png differ diff --git a/docs/data8-textbook-zh/img/10-1.png b/docs/data8-textbook-zh/img/10-1.png new file mode 100644 index 0000000000000000000000000000000000000000..9a27e44e5e5aabbf8824d77389f9cbfca196728b Binary files /dev/null and b/docs/data8-textbook-zh/img/10-1.png differ diff --git a/docs/data8-textbook-zh/img/10-10.png b/docs/data8-textbook-zh/img/10-10.png new file mode 100644 index 0000000000000000000000000000000000000000..3608897b4ad2a3a8c8efa491faf0546fe68afecb Binary files /dev/null and b/docs/data8-textbook-zh/img/10-10.png differ diff --git a/docs/data8-textbook-zh/img/10-11.png b/docs/data8-textbook-zh/img/10-11.png new file mode 100644 index 0000000000000000000000000000000000000000..0f2435c06b1b4b958d05e471eb9558c635fd9fbd Binary files /dev/null and b/docs/data8-textbook-zh/img/10-11.png differ diff --git a/docs/data8-textbook-zh/img/10-12.png b/docs/data8-textbook-zh/img/10-12.png new file mode 100644 index 0000000000000000000000000000000000000000..cae9ab3866cb605bdf3d1085f6795e57ed6f77d7 Binary files /dev/null and b/docs/data8-textbook-zh/img/10-12.png differ diff --git a/docs/data8-textbook-zh/img/10-13.png b/docs/data8-textbook-zh/img/10-13.png new file mode 100644 index 0000000000000000000000000000000000000000..6989739c08b93ae1a73334add74a1067b61bd1c9 Binary files /dev/null and b/docs/data8-textbook-zh/img/10-13.png differ diff --git a/docs/data8-textbook-zh/img/10-14.png b/docs/data8-textbook-zh/img/10-14.png new file mode 100644 index 0000000000000000000000000000000000000000..db9e4972fc871e7b15a2523d0ab61577812e6b3e Binary files /dev/null and b/docs/data8-textbook-zh/img/10-14.png differ diff --git a/docs/data8-textbook-zh/img/10-15.png b/docs/data8-textbook-zh/img/10-15.png new file mode 100644 index 0000000000000000000000000000000000000000..0a09fc7d7dc6e21a445ec224a94897d760519624 Binary files /dev/null and b/docs/data8-textbook-zh/img/10-15.png differ diff --git a/docs/data8-textbook-zh/img/10-2.png b/docs/data8-textbook-zh/img/10-2.png new file mode 100644 index 0000000000000000000000000000000000000000..0312c7496a9318faa7c22cdc52412164b66ccde0 Binary files /dev/null and b/docs/data8-textbook-zh/img/10-2.png differ diff --git a/docs/data8-textbook-zh/img/10-3.png b/docs/data8-textbook-zh/img/10-3.png new file mode 100644 index 0000000000000000000000000000000000000000..f3e6d42417ad624f15c214b9f4626a9c287bf51f Binary files /dev/null and b/docs/data8-textbook-zh/img/10-3.png differ diff --git a/docs/data8-textbook-zh/img/10-4.png b/docs/data8-textbook-zh/img/10-4.png new file mode 100644 index 0000000000000000000000000000000000000000..8d460a3fec34203cec3ac8e98f79d12c6c7b89dc Binary files /dev/null and b/docs/data8-textbook-zh/img/10-4.png differ diff --git a/docs/data8-textbook-zh/img/10-5.png b/docs/data8-textbook-zh/img/10-5.png new file mode 100644 index 0000000000000000000000000000000000000000..7d9efa01ac0a54e4dabdb9e5c583920861f515f6 Binary files /dev/null and b/docs/data8-textbook-zh/img/10-5.png differ diff --git a/docs/data8-textbook-zh/img/10-6.png b/docs/data8-textbook-zh/img/10-6.png new file mode 100644 index 0000000000000000000000000000000000000000..6ca8550cfea3d2d2a552ebb0ae6d674bc30532ea Binary files /dev/null and b/docs/data8-textbook-zh/img/10-6.png differ diff --git a/docs/data8-textbook-zh/img/10-7.png b/docs/data8-textbook-zh/img/10-7.png new file mode 100644 index 0000000000000000000000000000000000000000..2adfe5ad5928e73ae91864defdd8a6affd9a31a0 Binary files /dev/null and b/docs/data8-textbook-zh/img/10-7.png differ diff --git a/docs/data8-textbook-zh/img/10-8.png b/docs/data8-textbook-zh/img/10-8.png new file mode 100644 index 0000000000000000000000000000000000000000..3faf1bc4778dee4fc0638c994a76b1da4dd1d85c Binary files /dev/null and b/docs/data8-textbook-zh/img/10-8.png differ diff --git a/docs/data8-textbook-zh/img/10-9.png b/docs/data8-textbook-zh/img/10-9.png new file mode 100644 index 0000000000000000000000000000000000000000..a2549e21a7eeade812e7b06798c2cebeb65092ef Binary files /dev/null and b/docs/data8-textbook-zh/img/10-9.png differ diff --git a/docs/data8-textbook-zh/img/11-1.png b/docs/data8-textbook-zh/img/11-1.png new file mode 100644 index 0000000000000000000000000000000000000000..9fef7ffee706ff5a444e0a22164c965c8b5f6047 Binary files /dev/null and b/docs/data8-textbook-zh/img/11-1.png differ diff --git a/docs/data8-textbook-zh/img/11-10.png b/docs/data8-textbook-zh/img/11-10.png new file mode 100644 index 0000000000000000000000000000000000000000..2454b9c3e98e9f1dbf22a311b315021ddc95c5a3 Binary files /dev/null and b/docs/data8-textbook-zh/img/11-10.png differ diff --git a/docs/data8-textbook-zh/img/11-11.png b/docs/data8-textbook-zh/img/11-11.png new file mode 100644 index 0000000000000000000000000000000000000000..374a9be45bd24c55e060622840f93145f411985b Binary files /dev/null and b/docs/data8-textbook-zh/img/11-11.png differ diff --git a/docs/data8-textbook-zh/img/11-12.png b/docs/data8-textbook-zh/img/11-12.png new file mode 100644 index 0000000000000000000000000000000000000000..fae8202611f943cc4c93738834f2a38279954ae1 Binary files /dev/null and b/docs/data8-textbook-zh/img/11-12.png differ diff --git a/docs/data8-textbook-zh/img/11-13.png b/docs/data8-textbook-zh/img/11-13.png new file mode 100644 index 0000000000000000000000000000000000000000..374a9be45bd24c55e060622840f93145f411985b Binary files /dev/null and b/docs/data8-textbook-zh/img/11-13.png differ diff --git a/docs/data8-textbook-zh/img/11-14.png b/docs/data8-textbook-zh/img/11-14.png new file mode 100644 index 0000000000000000000000000000000000000000..2ed6cf1aea99b4ba180d1192c136e8206f652dc5 Binary files /dev/null and b/docs/data8-textbook-zh/img/11-14.png differ diff --git a/docs/data8-textbook-zh/img/11-15.png b/docs/data8-textbook-zh/img/11-15.png new file mode 100644 index 0000000000000000000000000000000000000000..598e60a38cedd009d03ba2797434f17104844448 Binary files /dev/null and b/docs/data8-textbook-zh/img/11-15.png differ diff --git a/docs/data8-textbook-zh/img/11-16.png b/docs/data8-textbook-zh/img/11-16.png new file mode 100644 index 0000000000000000000000000000000000000000..374a9be45bd24c55e060622840f93145f411985b Binary files /dev/null and b/docs/data8-textbook-zh/img/11-16.png differ diff --git a/docs/data8-textbook-zh/img/11-17.png b/docs/data8-textbook-zh/img/11-17.png new file mode 100644 index 0000000000000000000000000000000000000000..ab315b82bb2fa3ba85389c21af97efdb5ebbe5a2 Binary files /dev/null and b/docs/data8-textbook-zh/img/11-17.png differ diff --git a/docs/data8-textbook-zh/img/11-18.png b/docs/data8-textbook-zh/img/11-18.png new file mode 100644 index 0000000000000000000000000000000000000000..0ae51192daa44c810db1d45a7b21835bd5aa835a Binary files /dev/null and b/docs/data8-textbook-zh/img/11-18.png differ diff --git a/docs/data8-textbook-zh/img/11-2.png b/docs/data8-textbook-zh/img/11-2.png new file mode 100644 index 0000000000000000000000000000000000000000..68c1230132fe34adc1577c6d4d998121b6067e90 Binary files /dev/null and b/docs/data8-textbook-zh/img/11-2.png differ diff --git a/docs/data8-textbook-zh/img/11-3.png b/docs/data8-textbook-zh/img/11-3.png new file mode 100644 index 0000000000000000000000000000000000000000..db30c0e98f9873935dca2ba43354d7b19a163caa Binary files /dev/null and b/docs/data8-textbook-zh/img/11-3.png differ diff --git a/docs/data8-textbook-zh/img/11-4.png b/docs/data8-textbook-zh/img/11-4.png new file mode 100644 index 0000000000000000000000000000000000000000..01c3002a6f00fbc2f4883df4f28275017e7b25f1 Binary files /dev/null and b/docs/data8-textbook-zh/img/11-4.png differ diff --git a/docs/data8-textbook-zh/img/11-5.png b/docs/data8-textbook-zh/img/11-5.png new file mode 100644 index 0000000000000000000000000000000000000000..389c6d34f0f7285efef0219a2eeba8545c7b15b9 Binary files /dev/null and b/docs/data8-textbook-zh/img/11-5.png differ diff --git a/docs/data8-textbook-zh/img/11-6.png b/docs/data8-textbook-zh/img/11-6.png new file mode 100644 index 0000000000000000000000000000000000000000..e01c0579dd0815d7213ec33ac6e3aa9d61e5006c Binary files /dev/null and b/docs/data8-textbook-zh/img/11-6.png differ diff --git a/docs/data8-textbook-zh/img/11-7.png b/docs/data8-textbook-zh/img/11-7.png new file mode 100644 index 0000000000000000000000000000000000000000..ca3001828b6af0d33f434d695e09e64536834c2e Binary files /dev/null and b/docs/data8-textbook-zh/img/11-7.png differ diff --git a/docs/data8-textbook-zh/img/11-8.png b/docs/data8-textbook-zh/img/11-8.png new file mode 100644 index 0000000000000000000000000000000000000000..b7e4558e0792ca5e22ac5c33d525e486bd90e004 Binary files /dev/null and b/docs/data8-textbook-zh/img/11-8.png differ diff --git a/docs/data8-textbook-zh/img/11-9.png b/docs/data8-textbook-zh/img/11-9.png new file mode 100644 index 0000000000000000000000000000000000000000..560b985725b658f6b11983d19e2eb0d6a2c0280f Binary files /dev/null and b/docs/data8-textbook-zh/img/11-9.png differ diff --git a/docs/data8-textbook-zh/img/12-1.png b/docs/data8-textbook-zh/img/12-1.png new file mode 100644 index 0000000000000000000000000000000000000000..3fa92438b147fcde56420ec87d29c6327618b66c Binary files /dev/null and b/docs/data8-textbook-zh/img/12-1.png differ diff --git a/docs/data8-textbook-zh/img/12-10.png b/docs/data8-textbook-zh/img/12-10.png new file mode 100644 index 0000000000000000000000000000000000000000..8fed5f474af7894e459781a702a426ac623375ec Binary files /dev/null and b/docs/data8-textbook-zh/img/12-10.png differ diff --git a/docs/data8-textbook-zh/img/12-11.png b/docs/data8-textbook-zh/img/12-11.png new file mode 100644 index 0000000000000000000000000000000000000000..d717405f97b8b6fb89132b0ce507ce4bc00f49ca Binary files /dev/null and b/docs/data8-textbook-zh/img/12-11.png differ diff --git a/docs/data8-textbook-zh/img/12-12.png b/docs/data8-textbook-zh/img/12-12.png new file mode 100644 index 0000000000000000000000000000000000000000..20d7f2124038df17d7b349ca2d9e748634083856 Binary files /dev/null and b/docs/data8-textbook-zh/img/12-12.png differ diff --git a/docs/data8-textbook-zh/img/12-13.png b/docs/data8-textbook-zh/img/12-13.png new file mode 100644 index 0000000000000000000000000000000000000000..902db8b38b6f72f245d19883298ae51b2f857974 Binary files /dev/null and b/docs/data8-textbook-zh/img/12-13.png differ diff --git a/docs/data8-textbook-zh/img/12-14.png b/docs/data8-textbook-zh/img/12-14.png new file mode 100644 index 0000000000000000000000000000000000000000..11229f237ad13d2c47903100a30fe9cbe608d92a Binary files /dev/null and b/docs/data8-textbook-zh/img/12-14.png differ diff --git a/docs/data8-textbook-zh/img/12-15.png b/docs/data8-textbook-zh/img/12-15.png new file mode 100644 index 0000000000000000000000000000000000000000..0c5e50cabe058fd1f5c13366ef536f20e41ebbf0 Binary files /dev/null and b/docs/data8-textbook-zh/img/12-15.png differ diff --git a/docs/data8-textbook-zh/img/12-16.png b/docs/data8-textbook-zh/img/12-16.png new file mode 100644 index 0000000000000000000000000000000000000000..cfc9c6d08bc48cb0b8040fcbc91d586e60039bfb Binary files /dev/null and b/docs/data8-textbook-zh/img/12-16.png differ diff --git a/docs/data8-textbook-zh/img/12-17.png b/docs/data8-textbook-zh/img/12-17.png new file mode 100644 index 0000000000000000000000000000000000000000..01dd3ec330f4928dc2d9d2f6d12999e32b34626f Binary files /dev/null and b/docs/data8-textbook-zh/img/12-17.png differ diff --git a/docs/data8-textbook-zh/img/12-18.png b/docs/data8-textbook-zh/img/12-18.png new file mode 100644 index 0000000000000000000000000000000000000000..d54f37e890be0636dddda81b30fa29393a063f5d Binary files /dev/null and b/docs/data8-textbook-zh/img/12-18.png differ diff --git a/docs/data8-textbook-zh/img/12-19.png b/docs/data8-textbook-zh/img/12-19.png new file mode 100644 index 0000000000000000000000000000000000000000..473cf97924389f3d31579c9831b5395cd696a8c5 Binary files /dev/null and b/docs/data8-textbook-zh/img/12-19.png differ diff --git a/docs/data8-textbook-zh/img/12-2.png b/docs/data8-textbook-zh/img/12-2.png new file mode 100644 index 0000000000000000000000000000000000000000..88c8b309fe319f023799939bc2d8ab0bd858e061 Binary files /dev/null and b/docs/data8-textbook-zh/img/12-2.png differ diff --git a/docs/data8-textbook-zh/img/12-20.png b/docs/data8-textbook-zh/img/12-20.png new file mode 100644 index 0000000000000000000000000000000000000000..2ea0b8c95f83badb4bb0490ef8cd326775be3fff Binary files /dev/null and b/docs/data8-textbook-zh/img/12-20.png differ diff --git a/docs/data8-textbook-zh/img/12-21.png b/docs/data8-textbook-zh/img/12-21.png new file mode 100644 index 0000000000000000000000000000000000000000..4cd22acd3f5b34484995397fd6e62be5fccbcf88 Binary files /dev/null and b/docs/data8-textbook-zh/img/12-21.png differ diff --git a/docs/data8-textbook-zh/img/12-22.png b/docs/data8-textbook-zh/img/12-22.png new file mode 100644 index 0000000000000000000000000000000000000000..41921becdd5bbb7ab0ee3ef9fd14cb72d9a16ee7 Binary files /dev/null and b/docs/data8-textbook-zh/img/12-22.png differ diff --git a/docs/data8-textbook-zh/img/12-23.png b/docs/data8-textbook-zh/img/12-23.png new file mode 100644 index 0000000000000000000000000000000000000000..dcd17f2dd38e17620e22702b576a1ea74b40e30f Binary files /dev/null and b/docs/data8-textbook-zh/img/12-23.png differ diff --git a/docs/data8-textbook-zh/img/12-24.png b/docs/data8-textbook-zh/img/12-24.png new file mode 100644 index 0000000000000000000000000000000000000000..f734497efa2569971e8670785da2460c56a2b9ae Binary files /dev/null and b/docs/data8-textbook-zh/img/12-24.png differ diff --git a/docs/data8-textbook-zh/img/12-25.png b/docs/data8-textbook-zh/img/12-25.png new file mode 100644 index 0000000000000000000000000000000000000000..dc5d77db73da13f51d42d907d89650ffeee70873 Binary files /dev/null and b/docs/data8-textbook-zh/img/12-25.png differ diff --git a/docs/data8-textbook-zh/img/12-26.png b/docs/data8-textbook-zh/img/12-26.png new file mode 100644 index 0000000000000000000000000000000000000000..41df94f560df30b07b75836947bc5bed6f95d93d Binary files /dev/null and b/docs/data8-textbook-zh/img/12-26.png differ diff --git a/docs/data8-textbook-zh/img/12-27.png b/docs/data8-textbook-zh/img/12-27.png new file mode 100644 index 0000000000000000000000000000000000000000..bb5dd0b36e4b370fe85474e63b8246d3aa06efaf Binary files /dev/null and b/docs/data8-textbook-zh/img/12-27.png differ diff --git a/docs/data8-textbook-zh/img/12-3.png b/docs/data8-textbook-zh/img/12-3.png new file mode 100644 index 0000000000000000000000000000000000000000..71d7dd1920042a1b026dcafe48f02c0aa1d45b34 Binary files /dev/null and b/docs/data8-textbook-zh/img/12-3.png differ diff --git a/docs/data8-textbook-zh/img/12-4.png b/docs/data8-textbook-zh/img/12-4.png new file mode 100644 index 0000000000000000000000000000000000000000..a08acb6d9c92b507d3284f77cd35d854ccd7ea3e Binary files /dev/null and b/docs/data8-textbook-zh/img/12-4.png differ diff --git a/docs/data8-textbook-zh/img/12-5.png b/docs/data8-textbook-zh/img/12-5.png new file mode 100644 index 0000000000000000000000000000000000000000..d8086e66cd47f98f25eb01581b71722640c7ed56 Binary files /dev/null and b/docs/data8-textbook-zh/img/12-5.png differ diff --git a/docs/data8-textbook-zh/img/12-6.png b/docs/data8-textbook-zh/img/12-6.png new file mode 100644 index 0000000000000000000000000000000000000000..a8a3fae1db43e15e556619ebcc330803f3b40eb4 Binary files /dev/null and b/docs/data8-textbook-zh/img/12-6.png differ diff --git a/docs/data8-textbook-zh/img/12-7.png b/docs/data8-textbook-zh/img/12-7.png new file mode 100644 index 0000000000000000000000000000000000000000..0d206f38a2f8d46ad5ef1a2c21eaf8c49ebaedcb Binary files /dev/null and b/docs/data8-textbook-zh/img/12-7.png differ diff --git a/docs/data8-textbook-zh/img/12-8.png b/docs/data8-textbook-zh/img/12-8.png new file mode 100644 index 0000000000000000000000000000000000000000..d1e8553f0c9d31c0bc591d6dc82b0442fb0a4d1d Binary files /dev/null and b/docs/data8-textbook-zh/img/12-8.png differ diff --git a/docs/data8-textbook-zh/img/12-9.png b/docs/data8-textbook-zh/img/12-9.png new file mode 100644 index 0000000000000000000000000000000000000000..7b212a76486801eccb1faaf3183a38ffb4eca768 Binary files /dev/null and b/docs/data8-textbook-zh/img/12-9.png differ diff --git a/docs/data8-textbook-zh/img/13-1.png b/docs/data8-textbook-zh/img/13-1.png new file mode 100644 index 0000000000000000000000000000000000000000..f28a3dc256cf6eb84c2bea0bef50490da4f60ece Binary files /dev/null and b/docs/data8-textbook-zh/img/13-1.png differ diff --git a/docs/data8-textbook-zh/img/13-10.png b/docs/data8-textbook-zh/img/13-10.png new file mode 100644 index 0000000000000000000000000000000000000000..bc411ebdd9eac3c93e0618e5731003c100682754 Binary files /dev/null and b/docs/data8-textbook-zh/img/13-10.png differ diff --git a/docs/data8-textbook-zh/img/13-11.png b/docs/data8-textbook-zh/img/13-11.png new file mode 100644 index 0000000000000000000000000000000000000000..cbf772e2c07d7b95723fb0d4666cac2d3e304d68 Binary files /dev/null and b/docs/data8-textbook-zh/img/13-11.png differ diff --git a/docs/data8-textbook-zh/img/13-12.png b/docs/data8-textbook-zh/img/13-12.png new file mode 100644 index 0000000000000000000000000000000000000000..c270143cec0a6f84e94c83d14cdfc97e1e8bfe95 Binary files /dev/null and b/docs/data8-textbook-zh/img/13-12.png differ diff --git a/docs/data8-textbook-zh/img/13-13.png b/docs/data8-textbook-zh/img/13-13.png new file mode 100644 index 0000000000000000000000000000000000000000..99ff25b3928fd6da820cd9bffbfa322bbf04bbb4 Binary files /dev/null and b/docs/data8-textbook-zh/img/13-13.png differ diff --git a/docs/data8-textbook-zh/img/13-14.png b/docs/data8-textbook-zh/img/13-14.png new file mode 100644 index 0000000000000000000000000000000000000000..4df5ebbb0b2d8975deebeff89e51732e14abc163 Binary files /dev/null and b/docs/data8-textbook-zh/img/13-14.png differ diff --git a/docs/data8-textbook-zh/img/13-15.png b/docs/data8-textbook-zh/img/13-15.png new file mode 100644 index 0000000000000000000000000000000000000000..60b34369631f66dc7cac6de0d1c02e948ccaeb5b Binary files /dev/null and b/docs/data8-textbook-zh/img/13-15.png differ diff --git a/docs/data8-textbook-zh/img/13-16.png b/docs/data8-textbook-zh/img/13-16.png new file mode 100644 index 0000000000000000000000000000000000000000..bd6c856e6ee986055408ca5edfa43aa976100e2b Binary files /dev/null and b/docs/data8-textbook-zh/img/13-16.png differ diff --git a/docs/data8-textbook-zh/img/13-17.png b/docs/data8-textbook-zh/img/13-17.png new file mode 100644 index 0000000000000000000000000000000000000000..e9f23e966beb27b7d66e0d11381f7ce265b80e84 Binary files /dev/null and b/docs/data8-textbook-zh/img/13-17.png differ diff --git a/docs/data8-textbook-zh/img/13-18.png b/docs/data8-textbook-zh/img/13-18.png new file mode 100644 index 0000000000000000000000000000000000000000..bf9bde7ada88590bde35b80ae4ec650d0eb217e8 Binary files /dev/null and b/docs/data8-textbook-zh/img/13-18.png differ diff --git a/docs/data8-textbook-zh/img/13-19.png b/docs/data8-textbook-zh/img/13-19.png new file mode 100644 index 0000000000000000000000000000000000000000..bf9a1c99d9a9bdf01162dcd4486e66b538c30349 Binary files /dev/null and b/docs/data8-textbook-zh/img/13-19.png differ diff --git a/docs/data8-textbook-zh/img/13-2.png b/docs/data8-textbook-zh/img/13-2.png new file mode 100644 index 0000000000000000000000000000000000000000..b4b7e47080e7c35522fa9e9e752d0bc6c55e4dd9 Binary files /dev/null and b/docs/data8-textbook-zh/img/13-2.png differ diff --git a/docs/data8-textbook-zh/img/13-20.png b/docs/data8-textbook-zh/img/13-20.png new file mode 100644 index 0000000000000000000000000000000000000000..b4b7e47080e7c35522fa9e9e752d0bc6c55e4dd9 Binary files /dev/null and b/docs/data8-textbook-zh/img/13-20.png differ diff --git a/docs/data8-textbook-zh/img/13-21.png b/docs/data8-textbook-zh/img/13-21.png new file mode 100644 index 0000000000000000000000000000000000000000..6f201491817771d31d0977f309c50ffc4e1efefa Binary files /dev/null and b/docs/data8-textbook-zh/img/13-21.png differ diff --git a/docs/data8-textbook-zh/img/13-22.png b/docs/data8-textbook-zh/img/13-22.png new file mode 100644 index 0000000000000000000000000000000000000000..f0c3d7df1befaee7ebf0d7945ae79faf5386bbba Binary files /dev/null and b/docs/data8-textbook-zh/img/13-22.png differ diff --git a/docs/data8-textbook-zh/img/13-23.png b/docs/data8-textbook-zh/img/13-23.png new file mode 100644 index 0000000000000000000000000000000000000000..098cb1f637e0093b8dc0251a39f4519dbdd9f4a4 Binary files /dev/null and b/docs/data8-textbook-zh/img/13-23.png differ diff --git a/docs/data8-textbook-zh/img/13-24.png b/docs/data8-textbook-zh/img/13-24.png new file mode 100644 index 0000000000000000000000000000000000000000..1d777ae2b035ba57d1abc36c4565a7a8111a9c51 Binary files /dev/null and b/docs/data8-textbook-zh/img/13-24.png differ diff --git a/docs/data8-textbook-zh/img/13-25.png b/docs/data8-textbook-zh/img/13-25.png new file mode 100644 index 0000000000000000000000000000000000000000..55b5bf067dfeaf3b85f232c1840322c24e13476e Binary files /dev/null and b/docs/data8-textbook-zh/img/13-25.png differ diff --git a/docs/data8-textbook-zh/img/13-26.png b/docs/data8-textbook-zh/img/13-26.png new file mode 100644 index 0000000000000000000000000000000000000000..866f37b7d942e589f0a715203db6d9d26521b0d3 Binary files /dev/null and b/docs/data8-textbook-zh/img/13-26.png differ diff --git a/docs/data8-textbook-zh/img/13-27.png b/docs/data8-textbook-zh/img/13-27.png new file mode 100644 index 0000000000000000000000000000000000000000..e6273af0f7eccc2c58b90243826b6ca8fa35be45 Binary files /dev/null and b/docs/data8-textbook-zh/img/13-27.png differ diff --git a/docs/data8-textbook-zh/img/13-28.png b/docs/data8-textbook-zh/img/13-28.png new file mode 100644 index 0000000000000000000000000000000000000000..56a47751c01e69eb19add5894ff1af49951e4daa Binary files /dev/null and b/docs/data8-textbook-zh/img/13-28.png differ diff --git a/docs/data8-textbook-zh/img/13-29.png b/docs/data8-textbook-zh/img/13-29.png new file mode 100644 index 0000000000000000000000000000000000000000..f893b436da08facd2ea5b1064fcb7a5ec4f3bcad Binary files /dev/null and b/docs/data8-textbook-zh/img/13-29.png differ diff --git a/docs/data8-textbook-zh/img/13-3.png b/docs/data8-textbook-zh/img/13-3.png new file mode 100644 index 0000000000000000000000000000000000000000..174e95593a332dd49f6116e523911c4f9dd4c49a Binary files /dev/null and b/docs/data8-textbook-zh/img/13-3.png differ diff --git a/docs/data8-textbook-zh/img/13-30.png b/docs/data8-textbook-zh/img/13-30.png new file mode 100644 index 0000000000000000000000000000000000000000..23d8b7bcbe27c92100db36b9850421fc21e285ad Binary files /dev/null and b/docs/data8-textbook-zh/img/13-30.png differ diff --git a/docs/data8-textbook-zh/img/13-31.png b/docs/data8-textbook-zh/img/13-31.png new file mode 100644 index 0000000000000000000000000000000000000000..1da558c7b1e1d0f53e23ee9042924dd429f69a6b Binary files /dev/null and b/docs/data8-textbook-zh/img/13-31.png differ diff --git a/docs/data8-textbook-zh/img/13-32.png b/docs/data8-textbook-zh/img/13-32.png new file mode 100644 index 0000000000000000000000000000000000000000..0c48c8f78a1e3b313d5c6118475fb2780f675d14 Binary files /dev/null and b/docs/data8-textbook-zh/img/13-32.png differ diff --git a/docs/data8-textbook-zh/img/13-33.png b/docs/data8-textbook-zh/img/13-33.png new file mode 100644 index 0000000000000000000000000000000000000000..83dd8974ca42f739eec4f7a07b0fea0c9a41f9bf Binary files /dev/null and b/docs/data8-textbook-zh/img/13-33.png differ diff --git a/docs/data8-textbook-zh/img/13-34.png b/docs/data8-textbook-zh/img/13-34.png new file mode 100644 index 0000000000000000000000000000000000000000..fc6cb9188ac6ff1f096791c89336c3e34ae5cb5d Binary files /dev/null and b/docs/data8-textbook-zh/img/13-34.png differ diff --git a/docs/data8-textbook-zh/img/13-35.png b/docs/data8-textbook-zh/img/13-35.png new file mode 100644 index 0000000000000000000000000000000000000000..8fa6630d057d0511510a1087c85006ec22090f9f Binary files /dev/null and b/docs/data8-textbook-zh/img/13-35.png differ diff --git a/docs/data8-textbook-zh/img/13-36.png b/docs/data8-textbook-zh/img/13-36.png new file mode 100644 index 0000000000000000000000000000000000000000..fc6cb9188ac6ff1f096791c89336c3e34ae5cb5d Binary files /dev/null and b/docs/data8-textbook-zh/img/13-36.png differ diff --git a/docs/data8-textbook-zh/img/13-37.png b/docs/data8-textbook-zh/img/13-37.png new file mode 100644 index 0000000000000000000000000000000000000000..8fa6630d057d0511510a1087c85006ec22090f9f Binary files /dev/null and b/docs/data8-textbook-zh/img/13-37.png differ diff --git a/docs/data8-textbook-zh/img/13-38.png b/docs/data8-textbook-zh/img/13-38.png new file mode 100644 index 0000000000000000000000000000000000000000..57e62d349ec7a4c858969213b662fdb3d94ecfb3 Binary files /dev/null and b/docs/data8-textbook-zh/img/13-38.png differ diff --git a/docs/data8-textbook-zh/img/13-39.png b/docs/data8-textbook-zh/img/13-39.png new file mode 100644 index 0000000000000000000000000000000000000000..83dd8974ca42f739eec4f7a07b0fea0c9a41f9bf Binary files /dev/null and b/docs/data8-textbook-zh/img/13-39.png differ diff --git a/docs/data8-textbook-zh/img/13-4.png b/docs/data8-textbook-zh/img/13-4.png new file mode 100644 index 0000000000000000000000000000000000000000..3227908d74c25788791b604a230ac5e31657dd98 Binary files /dev/null and b/docs/data8-textbook-zh/img/13-4.png differ diff --git a/docs/data8-textbook-zh/img/13-40.png b/docs/data8-textbook-zh/img/13-40.png new file mode 100644 index 0000000000000000000000000000000000000000..83dd8974ca42f739eec4f7a07b0fea0c9a41f9bf Binary files /dev/null and b/docs/data8-textbook-zh/img/13-40.png differ diff --git a/docs/data8-textbook-zh/img/13-41.png b/docs/data8-textbook-zh/img/13-41.png new file mode 100644 index 0000000000000000000000000000000000000000..b324237ce41ef0f9cc1e992032df61256ac9a12b Binary files /dev/null and b/docs/data8-textbook-zh/img/13-41.png differ diff --git a/docs/data8-textbook-zh/img/13-42.png b/docs/data8-textbook-zh/img/13-42.png new file mode 100644 index 0000000000000000000000000000000000000000..9af34ac8197c3ac267041bbf13b1d926bc193643 Binary files /dev/null and b/docs/data8-textbook-zh/img/13-42.png differ diff --git a/docs/data8-textbook-zh/img/13-43.png b/docs/data8-textbook-zh/img/13-43.png new file mode 100644 index 0000000000000000000000000000000000000000..64d9c41529f53cb7df2f61db6a69504ee5d7e66b Binary files /dev/null and b/docs/data8-textbook-zh/img/13-43.png differ diff --git a/docs/data8-textbook-zh/img/13-44.png b/docs/data8-textbook-zh/img/13-44.png new file mode 100644 index 0000000000000000000000000000000000000000..74c8706fc626d3136980c4f616f6be4b8fe288e2 Binary files /dev/null and b/docs/data8-textbook-zh/img/13-44.png differ diff --git a/docs/data8-textbook-zh/img/13-45.png b/docs/data8-textbook-zh/img/13-45.png new file mode 100644 index 0000000000000000000000000000000000000000..4f3ef94af73d461a7075549d1f14598325b63687 Binary files /dev/null and b/docs/data8-textbook-zh/img/13-45.png differ diff --git a/docs/data8-textbook-zh/img/13-46.png b/docs/data8-textbook-zh/img/13-46.png new file mode 100644 index 0000000000000000000000000000000000000000..74c8706fc626d3136980c4f616f6be4b8fe288e2 Binary files /dev/null and b/docs/data8-textbook-zh/img/13-46.png differ diff --git a/docs/data8-textbook-zh/img/13-47.png b/docs/data8-textbook-zh/img/13-47.png new file mode 100644 index 0000000000000000000000000000000000000000..4f3ef94af73d461a7075549d1f14598325b63687 Binary files /dev/null and b/docs/data8-textbook-zh/img/13-47.png differ diff --git a/docs/data8-textbook-zh/img/13-48.jpg b/docs/data8-textbook-zh/img/13-48.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7ed374907d195a0ea0b5ef91d43e37acdf3c2b1c Binary files /dev/null and b/docs/data8-textbook-zh/img/13-48.jpg differ diff --git a/docs/data8-textbook-zh/img/13-49.png b/docs/data8-textbook-zh/img/13-49.png new file mode 100644 index 0000000000000000000000000000000000000000..0c917023896415267c01d352e9f8a9f84cbdd3c0 Binary files /dev/null and b/docs/data8-textbook-zh/img/13-49.png differ diff --git a/docs/data8-textbook-zh/img/13-5.png b/docs/data8-textbook-zh/img/13-5.png new file mode 100644 index 0000000000000000000000000000000000000000..221ea2dc66dfa70944cb5dd203a50195937ac2e1 Binary files /dev/null and b/docs/data8-textbook-zh/img/13-5.png differ diff --git a/docs/data8-textbook-zh/img/13-50.png b/docs/data8-textbook-zh/img/13-50.png new file mode 100644 index 0000000000000000000000000000000000000000..95014a636265d436da3fe069e240367e65bb2363 Binary files /dev/null and b/docs/data8-textbook-zh/img/13-50.png differ diff --git a/docs/data8-textbook-zh/img/13-51.png b/docs/data8-textbook-zh/img/13-51.png new file mode 100644 index 0000000000000000000000000000000000000000..94569578d4e69cdfab958f3fd904f3a528e9a6cd Binary files /dev/null and b/docs/data8-textbook-zh/img/13-51.png differ diff --git a/docs/data8-textbook-zh/img/13-52.png b/docs/data8-textbook-zh/img/13-52.png new file mode 100644 index 0000000000000000000000000000000000000000..b142cbafa872d5076ed0be007aa984440fa7ca70 Binary files /dev/null and b/docs/data8-textbook-zh/img/13-52.png differ diff --git a/docs/data8-textbook-zh/img/13-53.png b/docs/data8-textbook-zh/img/13-53.png new file mode 100644 index 0000000000000000000000000000000000000000..74c8706fc626d3136980c4f616f6be4b8fe288e2 Binary files /dev/null and b/docs/data8-textbook-zh/img/13-53.png differ diff --git a/docs/data8-textbook-zh/img/13-6.png b/docs/data8-textbook-zh/img/13-6.png new file mode 100644 index 0000000000000000000000000000000000000000..f5f898b88d299f73672f3363da9adc8592d8f200 Binary files /dev/null and b/docs/data8-textbook-zh/img/13-6.png differ diff --git a/docs/data8-textbook-zh/img/13-7.png b/docs/data8-textbook-zh/img/13-7.png new file mode 100644 index 0000000000000000000000000000000000000000..f5219f92242d4d7ca8426c19f6f319cca6baf6e9 Binary files /dev/null and b/docs/data8-textbook-zh/img/13-7.png differ diff --git a/docs/data8-textbook-zh/img/13-8.png b/docs/data8-textbook-zh/img/13-8.png new file mode 100644 index 0000000000000000000000000000000000000000..610b78ddd82e02b7e0c3b47bcb0ad80168f9c997 Binary files /dev/null and b/docs/data8-textbook-zh/img/13-8.png differ diff --git a/docs/data8-textbook-zh/img/13-9.png b/docs/data8-textbook-zh/img/13-9.png new file mode 100644 index 0000000000000000000000000000000000000000..eeb5bc3e4d787a691bc03fb3a17538e49092395a Binary files /dev/null and b/docs/data8-textbook-zh/img/13-9.png differ diff --git a/docs/data8-textbook-zh/img/14-1.png b/docs/data8-textbook-zh/img/14-1.png new file mode 100644 index 0000000000000000000000000000000000000000..786e8ce52c0a4b545c16e71889c5c869925de013 Binary files /dev/null and b/docs/data8-textbook-zh/img/14-1.png differ diff --git a/docs/data8-textbook-zh/img/14-10.png b/docs/data8-textbook-zh/img/14-10.png new file mode 100644 index 0000000000000000000000000000000000000000..62de6ec3a5f4eb82eba2570b64defa693bca2b77 Binary files /dev/null and b/docs/data8-textbook-zh/img/14-10.png differ diff --git a/docs/data8-textbook-zh/img/14-11.png b/docs/data8-textbook-zh/img/14-11.png new file mode 100644 index 0000000000000000000000000000000000000000..848494ae05f674471a4319e275af0808c2ea8f93 Binary files /dev/null and b/docs/data8-textbook-zh/img/14-11.png differ diff --git a/docs/data8-textbook-zh/img/14-12.png b/docs/data8-textbook-zh/img/14-12.png new file mode 100644 index 0000000000000000000000000000000000000000..87097f474cd0ad563c9fd2e4a5708550c19cc4e9 Binary files /dev/null and b/docs/data8-textbook-zh/img/14-12.png differ diff --git a/docs/data8-textbook-zh/img/14-13.png b/docs/data8-textbook-zh/img/14-13.png new file mode 100644 index 0000000000000000000000000000000000000000..3854362dd9a0df89dcdf73fcfad567cd94e1aae6 Binary files /dev/null and b/docs/data8-textbook-zh/img/14-13.png differ diff --git a/docs/data8-textbook-zh/img/14-14.png b/docs/data8-textbook-zh/img/14-14.png new file mode 100644 index 0000000000000000000000000000000000000000..8ac7625869855d1458127032b68f7ceb7ad47369 Binary files /dev/null and b/docs/data8-textbook-zh/img/14-14.png differ diff --git a/docs/data8-textbook-zh/img/14-15.png b/docs/data8-textbook-zh/img/14-15.png new file mode 100644 index 0000000000000000000000000000000000000000..7c70d8a160a2a8d3e47aa08f51c119c5024da831 Binary files /dev/null and b/docs/data8-textbook-zh/img/14-15.png differ diff --git a/docs/data8-textbook-zh/img/14-2.png b/docs/data8-textbook-zh/img/14-2.png new file mode 100644 index 0000000000000000000000000000000000000000..094bfa9a5e39cf08345bffa26e728945dc492850 Binary files /dev/null and b/docs/data8-textbook-zh/img/14-2.png differ diff --git a/docs/data8-textbook-zh/img/14-3.png b/docs/data8-textbook-zh/img/14-3.png new file mode 100644 index 0000000000000000000000000000000000000000..9a3b2b1d1c9158a65e2b23eafbf28ccba3ad7e5b Binary files /dev/null and b/docs/data8-textbook-zh/img/14-3.png differ diff --git a/docs/data8-textbook-zh/img/14-4.png b/docs/data8-textbook-zh/img/14-4.png new file mode 100644 index 0000000000000000000000000000000000000000..4053fd114cc1e927f2426e3dea800e07677e7732 Binary files /dev/null and b/docs/data8-textbook-zh/img/14-4.png differ diff --git a/docs/data8-textbook-zh/img/14-5.png b/docs/data8-textbook-zh/img/14-5.png new file mode 100644 index 0000000000000000000000000000000000000000..901f350bce90bf1af78d26343ba657fec8005087 Binary files /dev/null and b/docs/data8-textbook-zh/img/14-5.png differ diff --git a/docs/data8-textbook-zh/img/14-6.png b/docs/data8-textbook-zh/img/14-6.png new file mode 100644 index 0000000000000000000000000000000000000000..8b0a165ad3d5714d6996e6a00ce02f10086ddffe Binary files /dev/null and b/docs/data8-textbook-zh/img/14-6.png differ diff --git a/docs/data8-textbook-zh/img/14-7.png b/docs/data8-textbook-zh/img/14-7.png new file mode 100644 index 0000000000000000000000000000000000000000..09b7d292362f138feb39ce0e3c3b6454df2e55d1 Binary files /dev/null and b/docs/data8-textbook-zh/img/14-7.png differ diff --git a/docs/data8-textbook-zh/img/14-8.png b/docs/data8-textbook-zh/img/14-8.png new file mode 100644 index 0000000000000000000000000000000000000000..a40ec298e78ece54b0a869f7874f4dd3dbd727fb Binary files /dev/null and b/docs/data8-textbook-zh/img/14-8.png differ diff --git a/docs/data8-textbook-zh/img/14-9.png b/docs/data8-textbook-zh/img/14-9.png new file mode 100644 index 0000000000000000000000000000000000000000..6c54d33bb99a54088f5fce23b37188be77735d9c Binary files /dev/null and b/docs/data8-textbook-zh/img/14-9.png differ diff --git a/docs/data8-textbook-zh/img/15-1.png b/docs/data8-textbook-zh/img/15-1.png new file mode 100644 index 0000000000000000000000000000000000000000..0411d11f1acc9998ceec9fa8acf67d8f10a9e0be Binary files /dev/null and b/docs/data8-textbook-zh/img/15-1.png differ diff --git a/docs/data8-textbook-zh/img/15-10.png b/docs/data8-textbook-zh/img/15-10.png new file mode 100644 index 0000000000000000000000000000000000000000..51bfeab395ffd11785046941ca4810f6e88a059c Binary files /dev/null and b/docs/data8-textbook-zh/img/15-10.png differ diff --git a/docs/data8-textbook-zh/img/15-11.png b/docs/data8-textbook-zh/img/15-11.png new file mode 100644 index 0000000000000000000000000000000000000000..abc3e28a85f38d0b046932e8e475b8bc9145172e Binary files /dev/null and b/docs/data8-textbook-zh/img/15-11.png differ diff --git a/docs/data8-textbook-zh/img/15-12.png b/docs/data8-textbook-zh/img/15-12.png new file mode 100644 index 0000000000000000000000000000000000000000..e4db1bb5e7de30d40327af4ee6eb2a0d294fe2e7 Binary files /dev/null and b/docs/data8-textbook-zh/img/15-12.png differ diff --git a/docs/data8-textbook-zh/img/15-13.png b/docs/data8-textbook-zh/img/15-13.png new file mode 100644 index 0000000000000000000000000000000000000000..bf3f7ba6ae8b55c8b5988f26c9be2b4e611cbef0 Binary files /dev/null and b/docs/data8-textbook-zh/img/15-13.png differ diff --git a/docs/data8-textbook-zh/img/15-14.png b/docs/data8-textbook-zh/img/15-14.png new file mode 100644 index 0000000000000000000000000000000000000000..9a944a593b1b1d22478f951bf328dd69f2585af8 Binary files /dev/null and b/docs/data8-textbook-zh/img/15-14.png differ diff --git a/docs/data8-textbook-zh/img/15-15.png b/docs/data8-textbook-zh/img/15-15.png new file mode 100644 index 0000000000000000000000000000000000000000..bd0317de5d116c2638bf6b319c7a7074ed051568 Binary files /dev/null and b/docs/data8-textbook-zh/img/15-15.png differ diff --git a/docs/data8-textbook-zh/img/15-16.png b/docs/data8-textbook-zh/img/15-16.png new file mode 100644 index 0000000000000000000000000000000000000000..ef675822797fb3d7354a9d88cefbbc456c9d205c Binary files /dev/null and b/docs/data8-textbook-zh/img/15-16.png differ diff --git a/docs/data8-textbook-zh/img/15-17.png b/docs/data8-textbook-zh/img/15-17.png new file mode 100644 index 0000000000000000000000000000000000000000..fa92080862944c08ccab5d0bb4915d751f8bd850 Binary files /dev/null and b/docs/data8-textbook-zh/img/15-17.png differ diff --git a/docs/data8-textbook-zh/img/15-18.png b/docs/data8-textbook-zh/img/15-18.png new file mode 100644 index 0000000000000000000000000000000000000000..894e515f50663a1026fd6884ae368ce42ac3e36c Binary files /dev/null and b/docs/data8-textbook-zh/img/15-18.png differ diff --git a/docs/data8-textbook-zh/img/15-19.png b/docs/data8-textbook-zh/img/15-19.png new file mode 100644 index 0000000000000000000000000000000000000000..279562889f3f3b3856d856784712be014df9f416 Binary files /dev/null and b/docs/data8-textbook-zh/img/15-19.png differ diff --git a/docs/data8-textbook-zh/img/15-2.png b/docs/data8-textbook-zh/img/15-2.png new file mode 100644 index 0000000000000000000000000000000000000000..fba1463c28371c3718ae3a81f392b08892f8ca70 Binary files /dev/null and b/docs/data8-textbook-zh/img/15-2.png differ diff --git a/docs/data8-textbook-zh/img/15-20.png b/docs/data8-textbook-zh/img/15-20.png new file mode 100644 index 0000000000000000000000000000000000000000..5240ab0afe0b4b9c7b72b08dbac3212901327719 Binary files /dev/null and b/docs/data8-textbook-zh/img/15-20.png differ diff --git a/docs/data8-textbook-zh/img/15-21.png b/docs/data8-textbook-zh/img/15-21.png new file mode 100644 index 0000000000000000000000000000000000000000..c014af7319a319e180bf4e8e9c2aeb8b01dbc8b7 Binary files /dev/null and b/docs/data8-textbook-zh/img/15-21.png differ diff --git a/docs/data8-textbook-zh/img/15-22.png b/docs/data8-textbook-zh/img/15-22.png new file mode 100644 index 0000000000000000000000000000000000000000..3749b5859f54ff415bd0db976a7382c2f34b4d40 Binary files /dev/null and b/docs/data8-textbook-zh/img/15-22.png differ diff --git a/docs/data8-textbook-zh/img/15-23.png b/docs/data8-textbook-zh/img/15-23.png new file mode 100644 index 0000000000000000000000000000000000000000..9e9ea7a7b3965acb52cd12a290d23a19e726a3d8 Binary files /dev/null and b/docs/data8-textbook-zh/img/15-23.png differ diff --git a/docs/data8-textbook-zh/img/15-24.png b/docs/data8-textbook-zh/img/15-24.png new file mode 100644 index 0000000000000000000000000000000000000000..f3ee271af0bf33cff5c1b22ca1684f2098365c79 Binary files /dev/null and b/docs/data8-textbook-zh/img/15-24.png differ diff --git a/docs/data8-textbook-zh/img/15-25.png b/docs/data8-textbook-zh/img/15-25.png new file mode 100644 index 0000000000000000000000000000000000000000..c40fd667967cad06d21592a3110ac52dab5ef168 Binary files /dev/null and b/docs/data8-textbook-zh/img/15-25.png differ diff --git a/docs/data8-textbook-zh/img/15-3.png b/docs/data8-textbook-zh/img/15-3.png new file mode 100644 index 0000000000000000000000000000000000000000..8de0e82eb2ebdd5e3daa67efad5ac57cafba4f3f Binary files /dev/null and b/docs/data8-textbook-zh/img/15-3.png differ diff --git a/docs/data8-textbook-zh/img/15-4.png b/docs/data8-textbook-zh/img/15-4.png new file mode 100644 index 0000000000000000000000000000000000000000..be09bb450c2d7848f58b92bea89cb13d97e5ba24 Binary files /dev/null and b/docs/data8-textbook-zh/img/15-4.png differ diff --git a/docs/data8-textbook-zh/img/15-5.png b/docs/data8-textbook-zh/img/15-5.png new file mode 100644 index 0000000000000000000000000000000000000000..8267789783912e86684f061381a513a4b96baf25 Binary files /dev/null and b/docs/data8-textbook-zh/img/15-5.png differ diff --git a/docs/data8-textbook-zh/img/15-6.png b/docs/data8-textbook-zh/img/15-6.png new file mode 100644 index 0000000000000000000000000000000000000000..5fc6b7e0d0a8a6b349e74ee7ffcde82b57fedc96 Binary files /dev/null and b/docs/data8-textbook-zh/img/15-6.png differ diff --git a/docs/data8-textbook-zh/img/15-7.png b/docs/data8-textbook-zh/img/15-7.png new file mode 100644 index 0000000000000000000000000000000000000000..e381cd8cb79d7d25301132b56a41b9e24fa0bcd8 Binary files /dev/null and b/docs/data8-textbook-zh/img/15-7.png differ diff --git a/docs/data8-textbook-zh/img/15-8.png b/docs/data8-textbook-zh/img/15-8.png new file mode 100644 index 0000000000000000000000000000000000000000..fd2fcce1574f0437c1ebb53744506c5a6e016fcf Binary files /dev/null and b/docs/data8-textbook-zh/img/15-8.png differ diff --git a/docs/data8-textbook-zh/img/15-9.png b/docs/data8-textbook-zh/img/15-9.png new file mode 100644 index 0000000000000000000000000000000000000000..15180c8e5adef93408b81ec3fde6e6c791c95a80 Binary files /dev/null and b/docs/data8-textbook-zh/img/15-9.png differ diff --git a/docs/data8-textbook-zh/img/16-1.png b/docs/data8-textbook-zh/img/16-1.png new file mode 100644 index 0000000000000000000000000000000000000000..ddc328ab0a4de5a1f0360c3ed20d2a915ea05a21 Binary files /dev/null and b/docs/data8-textbook-zh/img/16-1.png differ diff --git a/docs/data8-textbook-zh/img/16-10.png b/docs/data8-textbook-zh/img/16-10.png new file mode 100644 index 0000000000000000000000000000000000000000..17f6d7ed252ec6be35086b69efa3820abf2aaf26 Binary files /dev/null and b/docs/data8-textbook-zh/img/16-10.png differ diff --git a/docs/data8-textbook-zh/img/16-11.png b/docs/data8-textbook-zh/img/16-11.png new file mode 100644 index 0000000000000000000000000000000000000000..b8c13cae2e57ac0870ea25f403bea7535d8c7039 Binary files /dev/null and b/docs/data8-textbook-zh/img/16-11.png differ diff --git a/docs/data8-textbook-zh/img/16-12.png b/docs/data8-textbook-zh/img/16-12.png new file mode 100644 index 0000000000000000000000000000000000000000..960a0d17f307af81c8f2e8417d1054e1c2748846 Binary files /dev/null and b/docs/data8-textbook-zh/img/16-12.png differ diff --git a/docs/data8-textbook-zh/img/16-13.png b/docs/data8-textbook-zh/img/16-13.png new file mode 100644 index 0000000000000000000000000000000000000000..1f58a3f9107c5f837fc1e929476448fba8c4f139 Binary files /dev/null and b/docs/data8-textbook-zh/img/16-13.png differ diff --git a/docs/data8-textbook-zh/img/16-14.png b/docs/data8-textbook-zh/img/16-14.png new file mode 100644 index 0000000000000000000000000000000000000000..b22e166f225716d697ed12ed7f858917b5604e1c Binary files /dev/null and b/docs/data8-textbook-zh/img/16-14.png differ diff --git a/docs/data8-textbook-zh/img/16-15.png b/docs/data8-textbook-zh/img/16-15.png new file mode 100644 index 0000000000000000000000000000000000000000..31030d1ceb81b43c3d312f8a224fcb5e0918e9fa Binary files /dev/null and b/docs/data8-textbook-zh/img/16-15.png differ diff --git a/docs/data8-textbook-zh/img/16-2.png b/docs/data8-textbook-zh/img/16-2.png new file mode 100644 index 0000000000000000000000000000000000000000..4c30f85453844dc95dc1e39467f183b1dc6bc3e5 Binary files /dev/null and b/docs/data8-textbook-zh/img/16-2.png differ diff --git a/docs/data8-textbook-zh/img/16-3.png b/docs/data8-textbook-zh/img/16-3.png new file mode 100644 index 0000000000000000000000000000000000000000..11133b4b21675ef9563692cb510a238534ba669f Binary files /dev/null and b/docs/data8-textbook-zh/img/16-3.png differ diff --git a/docs/data8-textbook-zh/img/16-4.png b/docs/data8-textbook-zh/img/16-4.png new file mode 100644 index 0000000000000000000000000000000000000000..7a2e1313a93a94b233ba407cba3a605df88bfa3d Binary files /dev/null and b/docs/data8-textbook-zh/img/16-4.png differ diff --git a/docs/data8-textbook-zh/img/16-5.png b/docs/data8-textbook-zh/img/16-5.png new file mode 100644 index 0000000000000000000000000000000000000000..9dbe9d00922878b504c7f4db55f2de3bee29b346 Binary files /dev/null and b/docs/data8-textbook-zh/img/16-5.png differ diff --git a/docs/data8-textbook-zh/img/16-6.png b/docs/data8-textbook-zh/img/16-6.png new file mode 100644 index 0000000000000000000000000000000000000000..d2488cdc2e8a21725d58764f2be0315a696a3375 Binary files /dev/null and b/docs/data8-textbook-zh/img/16-6.png differ diff --git a/docs/data8-textbook-zh/img/16-7.png b/docs/data8-textbook-zh/img/16-7.png new file mode 100644 index 0000000000000000000000000000000000000000..e32dce25666b8cb7c447c49fdc38a2c4ddaa58ff Binary files /dev/null and b/docs/data8-textbook-zh/img/16-7.png differ diff --git a/docs/data8-textbook-zh/img/16-8.png b/docs/data8-textbook-zh/img/16-8.png new file mode 100644 index 0000000000000000000000000000000000000000..5052713ff08c1e7f32d150275d3ccc8bf817ef15 Binary files /dev/null and b/docs/data8-textbook-zh/img/16-8.png differ diff --git a/docs/data8-textbook-zh/img/16-9.png b/docs/data8-textbook-zh/img/16-9.png new file mode 100644 index 0000000000000000000000000000000000000000..2007e6928ec9fbe47f9d5e87fe8322db739b4620 Binary files /dev/null and b/docs/data8-textbook-zh/img/16-9.png differ diff --git a/docs/data8-textbook-zh/img/17-1.png b/docs/data8-textbook-zh/img/17-1.png new file mode 100644 index 0000000000000000000000000000000000000000..90cf9b2284003b517e63b0bd590ba06b891615e7 Binary files /dev/null and b/docs/data8-textbook-zh/img/17-1.png differ diff --git a/docs/data8-textbook-zh/img/17-2.png b/docs/data8-textbook-zh/img/17-2.png new file mode 100644 index 0000000000000000000000000000000000000000..0299b6dfdbf9857d82502543068008c4bafa6a7a Binary files /dev/null and b/docs/data8-textbook-zh/img/17-2.png differ diff --git a/docs/data8-textbook-zh/img/17-3.png b/docs/data8-textbook-zh/img/17-3.png new file mode 100644 index 0000000000000000000000000000000000000000..fa71965107f94d7c27e5f4ed2b1549b6793632f8 Binary files /dev/null and b/docs/data8-textbook-zh/img/17-3.png differ diff --git a/docs/data8-textbook-zh/img/2-1.jpg b/docs/data8-textbook-zh/img/2-1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..370e7b6fd89af29486d60dcae15f2e2d1109f469 Binary files /dev/null and b/docs/data8-textbook-zh/img/2-1.jpg differ diff --git a/docs/data8-textbook-zh/img/2-2.jpg b/docs/data8-textbook-zh/img/2-2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..44a8dd67621badf469717de98f68df775eda4066 Binary files /dev/null and b/docs/data8-textbook-zh/img/2-2.jpg differ diff --git a/docs/data8-textbook-zh/img/3-1.png b/docs/data8-textbook-zh/img/3-1.png new file mode 100644 index 0000000000000000000000000000000000000000..c6909e9a4ebb7780c451e1e6a189c8b455a04bdd Binary files /dev/null and b/docs/data8-textbook-zh/img/3-1.png differ diff --git a/docs/data8-textbook-zh/img/3-2.png b/docs/data8-textbook-zh/img/3-2.png new file mode 100644 index 0000000000000000000000000000000000000000..0fab72750560e17537c1cd82b892224c8cdd92fb Binary files /dev/null and b/docs/data8-textbook-zh/img/3-2.png differ diff --git a/docs/data8-textbook-zh/img/3-3.png b/docs/data8-textbook-zh/img/3-3.png new file mode 100644 index 0000000000000000000000000000000000000000..37d53117bd2712576dbad0e9496180260cd3e3a2 Binary files /dev/null and b/docs/data8-textbook-zh/img/3-3.png differ diff --git a/docs/data8-textbook-zh/img/3-4.png b/docs/data8-textbook-zh/img/3-4.png new file mode 100644 index 0000000000000000000000000000000000000000..3419de26a769f1890bfbcd9adfe839531c7b3c63 Binary files /dev/null and b/docs/data8-textbook-zh/img/3-4.png differ diff --git a/docs/data8-textbook-zh/img/4-1.png b/docs/data8-textbook-zh/img/4-1.png new file mode 100644 index 0000000000000000000000000000000000000000..d875b4b3011417a81cbadb34814ecd6a1efe41df Binary files /dev/null and b/docs/data8-textbook-zh/img/4-1.png differ diff --git a/docs/data8-textbook-zh/img/4-2.png b/docs/data8-textbook-zh/img/4-2.png new file mode 100644 index 0000000000000000000000000000000000000000..44692fee6cb7044bcd5b4218cfc9328b7640a611 Binary files /dev/null and b/docs/data8-textbook-zh/img/4-2.png differ diff --git a/docs/data8-textbook-zh/img/4-3.png b/docs/data8-textbook-zh/img/4-3.png new file mode 100644 index 0000000000000000000000000000000000000000..01e88ef335ae5d666169f8270065698cb15d6e72 Binary files /dev/null and b/docs/data8-textbook-zh/img/4-3.png differ diff --git a/docs/data8-textbook-zh/img/5-1.png b/docs/data8-textbook-zh/img/5-1.png new file mode 100644 index 0000000000000000000000000000000000000000..8ade1387668397c97e9d6b619d395c6bc11733e1 Binary files /dev/null and b/docs/data8-textbook-zh/img/5-1.png differ diff --git a/docs/data8-textbook-zh/img/6-1.png b/docs/data8-textbook-zh/img/6-1.png new file mode 100644 index 0000000000000000000000000000000000000000..893f2ff68a3aa152a5fa1e3132f9860b09a01feb Binary files /dev/null and b/docs/data8-textbook-zh/img/6-1.png differ diff --git a/docs/data8-textbook-zh/img/6-10.png b/docs/data8-textbook-zh/img/6-10.png new file mode 100644 index 0000000000000000000000000000000000000000..ed7b21e6ddfe29a2fc72daceb4dd31e38ac9dcfc Binary files /dev/null and b/docs/data8-textbook-zh/img/6-10.png differ diff --git a/docs/data8-textbook-zh/img/6-11.png b/docs/data8-textbook-zh/img/6-11.png new file mode 100644 index 0000000000000000000000000000000000000000..b58f97829d15464ebb3e93ac260cac4cef453959 Binary files /dev/null and b/docs/data8-textbook-zh/img/6-11.png differ diff --git a/docs/data8-textbook-zh/img/6-12.png b/docs/data8-textbook-zh/img/6-12.png new file mode 100644 index 0000000000000000000000000000000000000000..e4b18865ed11fee320ecad8c50fcb3292f421bc3 Binary files /dev/null and b/docs/data8-textbook-zh/img/6-12.png differ diff --git a/docs/data8-textbook-zh/img/6-13.png b/docs/data8-textbook-zh/img/6-13.png new file mode 100644 index 0000000000000000000000000000000000000000..7a1a147c1a8a6ad76cdeeed8582864c29cd8a6aa Binary files /dev/null and b/docs/data8-textbook-zh/img/6-13.png differ diff --git a/docs/data8-textbook-zh/img/6-14.png b/docs/data8-textbook-zh/img/6-14.png new file mode 100644 index 0000000000000000000000000000000000000000..3871e8ac5137538ac10a0719f2b805e001d6ddd7 Binary files /dev/null and b/docs/data8-textbook-zh/img/6-14.png differ diff --git a/docs/data8-textbook-zh/img/6-15.png b/docs/data8-textbook-zh/img/6-15.png new file mode 100644 index 0000000000000000000000000000000000000000..556b99d05125032ea190cf65e922a72a027cdd15 Binary files /dev/null and b/docs/data8-textbook-zh/img/6-15.png differ diff --git a/docs/data8-textbook-zh/img/6-16.png b/docs/data8-textbook-zh/img/6-16.png new file mode 100644 index 0000000000000000000000000000000000000000..6746284735a3250299d4f0a13e74aa0fb73a48d4 Binary files /dev/null and b/docs/data8-textbook-zh/img/6-16.png differ diff --git a/docs/data8-textbook-zh/img/6-17.png b/docs/data8-textbook-zh/img/6-17.png new file mode 100644 index 0000000000000000000000000000000000000000..d58b2f991ff28c00d531c96022a1e9c2b2f86f48 Binary files /dev/null and b/docs/data8-textbook-zh/img/6-17.png differ diff --git a/docs/data8-textbook-zh/img/6-18.png b/docs/data8-textbook-zh/img/6-18.png new file mode 100644 index 0000000000000000000000000000000000000000..b89e927a76515d5cb93460b8f4ab6ead619cc7e5 Binary files /dev/null and b/docs/data8-textbook-zh/img/6-18.png differ diff --git a/docs/data8-textbook-zh/img/6-19.png b/docs/data8-textbook-zh/img/6-19.png new file mode 100644 index 0000000000000000000000000000000000000000..d973726a4b1533443862f2d8f88618de2dab7470 Binary files /dev/null and b/docs/data8-textbook-zh/img/6-19.png differ diff --git a/docs/data8-textbook-zh/img/6-2.png b/docs/data8-textbook-zh/img/6-2.png new file mode 100644 index 0000000000000000000000000000000000000000..38ff2068bb85587570c2d9d82b30af228077546f Binary files /dev/null and b/docs/data8-textbook-zh/img/6-2.png differ diff --git a/docs/data8-textbook-zh/img/6-20.png b/docs/data8-textbook-zh/img/6-20.png new file mode 100644 index 0000000000000000000000000000000000000000..62912248ffaa10c35948a8123cc356b78edfa895 Binary files /dev/null and b/docs/data8-textbook-zh/img/6-20.png differ diff --git a/docs/data8-textbook-zh/img/6-21.png b/docs/data8-textbook-zh/img/6-21.png new file mode 100644 index 0000000000000000000000000000000000000000..d58b2f991ff28c00d531c96022a1e9c2b2f86f48 Binary files /dev/null and b/docs/data8-textbook-zh/img/6-21.png differ diff --git a/docs/data8-textbook-zh/img/6-22.png b/docs/data8-textbook-zh/img/6-22.png new file mode 100644 index 0000000000000000000000000000000000000000..1afce7f49f5645f8d58cecde75f58d0af7c94d1a Binary files /dev/null and b/docs/data8-textbook-zh/img/6-22.png differ diff --git a/docs/data8-textbook-zh/img/6-23.png b/docs/data8-textbook-zh/img/6-23.png new file mode 100644 index 0000000000000000000000000000000000000000..5b0c244b4bd7b7e9fe03d4e935ac7960ea85f563 Binary files /dev/null and b/docs/data8-textbook-zh/img/6-23.png differ diff --git a/docs/data8-textbook-zh/img/6-24.png b/docs/data8-textbook-zh/img/6-24.png new file mode 100644 index 0000000000000000000000000000000000000000..fe7de6a26e3afb969e1031296c8cfc2bfe8a14bc Binary files /dev/null and b/docs/data8-textbook-zh/img/6-24.png differ diff --git a/docs/data8-textbook-zh/img/6-25.png b/docs/data8-textbook-zh/img/6-25.png new file mode 100644 index 0000000000000000000000000000000000000000..b59cb2e3cb31197608e27612b3275635d52e2a31 Binary files /dev/null and b/docs/data8-textbook-zh/img/6-25.png differ diff --git a/docs/data8-textbook-zh/img/6-26.png b/docs/data8-textbook-zh/img/6-26.png new file mode 100644 index 0000000000000000000000000000000000000000..fae3b62e414913722044adcc32c1192d3f8c90f6 Binary files /dev/null and b/docs/data8-textbook-zh/img/6-26.png differ diff --git a/docs/data8-textbook-zh/img/6-27.png b/docs/data8-textbook-zh/img/6-27.png new file mode 100644 index 0000000000000000000000000000000000000000..78c944cb19ae0322b32a22cc67c85467d443aac7 Binary files /dev/null and b/docs/data8-textbook-zh/img/6-27.png differ diff --git a/docs/data8-textbook-zh/img/6-28.png b/docs/data8-textbook-zh/img/6-28.png new file mode 100644 index 0000000000000000000000000000000000000000..877bc61ef254df7471d70451219271f66618569c Binary files /dev/null and b/docs/data8-textbook-zh/img/6-28.png differ diff --git a/docs/data8-textbook-zh/img/6-3.png b/docs/data8-textbook-zh/img/6-3.png new file mode 100644 index 0000000000000000000000000000000000000000..444bcae8dd97aeb5927e472591f9e5ac90e68d09 Binary files /dev/null and b/docs/data8-textbook-zh/img/6-3.png differ diff --git a/docs/data8-textbook-zh/img/6-4.png b/docs/data8-textbook-zh/img/6-4.png new file mode 100644 index 0000000000000000000000000000000000000000..076176c064e4b0d53e52816f6c321e1546703439 Binary files /dev/null and b/docs/data8-textbook-zh/img/6-4.png differ diff --git a/docs/data8-textbook-zh/img/6-5.png b/docs/data8-textbook-zh/img/6-5.png new file mode 100644 index 0000000000000000000000000000000000000000..21da96b913a9fe1f0a2b59fe90ea7b2fc7b489fb Binary files /dev/null and b/docs/data8-textbook-zh/img/6-5.png differ diff --git a/docs/data8-textbook-zh/img/6-6.png b/docs/data8-textbook-zh/img/6-6.png new file mode 100644 index 0000000000000000000000000000000000000000..0304e3d88d76bc9659eba77dfbda9ac918efbe29 Binary files /dev/null and b/docs/data8-textbook-zh/img/6-6.png differ diff --git a/docs/data8-textbook-zh/img/6-7.png b/docs/data8-textbook-zh/img/6-7.png new file mode 100644 index 0000000000000000000000000000000000000000..7c4956cc6b9317cd958458780596c502642fcc05 Binary files /dev/null and b/docs/data8-textbook-zh/img/6-7.png differ diff --git a/docs/data8-textbook-zh/img/6-8.png b/docs/data8-textbook-zh/img/6-8.png new file mode 100644 index 0000000000000000000000000000000000000000..37db8a8bb4122816f55f21d3fade864c7450af3e Binary files /dev/null and b/docs/data8-textbook-zh/img/6-8.png differ diff --git a/docs/data8-textbook-zh/img/6-9.png b/docs/data8-textbook-zh/img/6-9.png new file mode 100644 index 0000000000000000000000000000000000000000..37db8a8bb4122816f55f21d3fade864c7450af3e Binary files /dev/null and b/docs/data8-textbook-zh/img/6-9.png differ diff --git a/docs/data8-textbook-zh/img/7-1.jpg b/docs/data8-textbook-zh/img/7-1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9b62a89588121d0d8b885c09cbf0a5a15f08ad09 Binary files /dev/null and b/docs/data8-textbook-zh/img/7-1.jpg differ diff --git a/docs/data8-textbook-zh/img/7-2.jpg b/docs/data8-textbook-zh/img/7-2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..56d6b2545e2112209557021045503fce05001574 Binary files /dev/null and b/docs/data8-textbook-zh/img/7-2.jpg differ diff --git a/docs/data8-textbook-zh/img/7-3.png b/docs/data8-textbook-zh/img/7-3.png new file mode 100644 index 0000000000000000000000000000000000000000..404f2f561876ca7e0fdfbf0bb35115dc2da2fa0f Binary files /dev/null and b/docs/data8-textbook-zh/img/7-3.png differ diff --git a/docs/data8-textbook-zh/img/7-4.png b/docs/data8-textbook-zh/img/7-4.png new file mode 100644 index 0000000000000000000000000000000000000000..2c09f4c39f94a5e1bb0661961fcb0082efa93eb7 Binary files /dev/null and b/docs/data8-textbook-zh/img/7-4.png differ diff --git a/docs/data8-textbook-zh/img/7-5.png b/docs/data8-textbook-zh/img/7-5.png new file mode 100644 index 0000000000000000000000000000000000000000..1107206c0d6932eb95e438c1baa8944d5332e6c6 Binary files /dev/null and b/docs/data8-textbook-zh/img/7-5.png differ diff --git a/docs/data8-textbook-zh/img/7-6.png b/docs/data8-textbook-zh/img/7-6.png new file mode 100644 index 0000000000000000000000000000000000000000..2f9685df5c92d0e243ab2cf69aa4c885b98be019 Binary files /dev/null and b/docs/data8-textbook-zh/img/7-6.png differ diff --git a/docs/data8-textbook-zh/img/7-7.png b/docs/data8-textbook-zh/img/7-7.png new file mode 100644 index 0000000000000000000000000000000000000000..23acaac667406f533659864d484bc2bfbf917121 Binary files /dev/null and b/docs/data8-textbook-zh/img/7-7.png differ diff --git a/docs/data8-textbook-zh/img/7-8.png b/docs/data8-textbook-zh/img/7-8.png new file mode 100644 index 0000000000000000000000000000000000000000..110adcc1f84d9c12f29e91b4a0a52fef2de44139 Binary files /dev/null and b/docs/data8-textbook-zh/img/7-8.png differ diff --git a/docs/data8-textbook-zh/img/8-1.png b/docs/data8-textbook-zh/img/8-1.png new file mode 100644 index 0000000000000000000000000000000000000000..faa298bd83aa667e937467b9e02ead9cb51b57b8 Binary files /dev/null and b/docs/data8-textbook-zh/img/8-1.png differ diff --git a/docs/data8-textbook-zh/img/8-2.png b/docs/data8-textbook-zh/img/8-2.png new file mode 100644 index 0000000000000000000000000000000000000000..eaf2eced9aa82527947cbfa4f507b4fb5a21156a Binary files /dev/null and b/docs/data8-textbook-zh/img/8-2.png differ diff --git a/docs/data8-textbook-zh/img/8-3.png b/docs/data8-textbook-zh/img/8-3.png new file mode 100644 index 0000000000000000000000000000000000000000..cdde9df36f73fb1a8be2744457bb1a5ce8195907 Binary files /dev/null and b/docs/data8-textbook-zh/img/8-3.png differ diff --git a/docs/data8-textbook-zh/img/8-4.png b/docs/data8-textbook-zh/img/8-4.png new file mode 100644 index 0000000000000000000000000000000000000000..7ee3f15478e67b82d3e883b020675564dbd54156 Binary files /dev/null and b/docs/data8-textbook-zh/img/8-4.png differ diff --git a/docs/data8-textbook-zh/img/9-1.png b/docs/data8-textbook-zh/img/9-1.png new file mode 100644 index 0000000000000000000000000000000000000000..b803d28a9e37c450c45019d96ea7307dc5654b0d Binary files /dev/null and b/docs/data8-textbook-zh/img/9-1.png differ diff --git a/docs/data8-textbook-zh/img/9-10.png b/docs/data8-textbook-zh/img/9-10.png new file mode 100644 index 0000000000000000000000000000000000000000..8d681165edf69bc4e6e5d1550fe40342a37adfdf Binary files /dev/null and b/docs/data8-textbook-zh/img/9-10.png differ diff --git a/docs/data8-textbook-zh/img/9-11.png b/docs/data8-textbook-zh/img/9-11.png new file mode 100644 index 0000000000000000000000000000000000000000..e5fc2589373914df23bef9854caab84ff8ec7667 Binary files /dev/null and b/docs/data8-textbook-zh/img/9-11.png differ diff --git a/docs/data8-textbook-zh/img/9-12.png b/docs/data8-textbook-zh/img/9-12.png new file mode 100644 index 0000000000000000000000000000000000000000..42344f9ca57c1fb6e9e572ecc4103f8a6b56aa71 Binary files /dev/null and b/docs/data8-textbook-zh/img/9-12.png differ diff --git a/docs/data8-textbook-zh/img/9-13.png b/docs/data8-textbook-zh/img/9-13.png new file mode 100644 index 0000000000000000000000000000000000000000..d57de745d9fd516bc81200302f95983aff2ec29f Binary files /dev/null and b/docs/data8-textbook-zh/img/9-13.png differ diff --git a/docs/data8-textbook-zh/img/9-14.png b/docs/data8-textbook-zh/img/9-14.png new file mode 100644 index 0000000000000000000000000000000000000000..d079848f66311c5fa5dd42402b234a22f7e9f918 Binary files /dev/null and b/docs/data8-textbook-zh/img/9-14.png differ diff --git a/docs/data8-textbook-zh/img/9-15.png b/docs/data8-textbook-zh/img/9-15.png new file mode 100644 index 0000000000000000000000000000000000000000..9b7da802e6ca46bf2466736673aa6571942bf813 Binary files /dev/null and b/docs/data8-textbook-zh/img/9-15.png differ diff --git a/docs/data8-textbook-zh/img/9-16.png b/docs/data8-textbook-zh/img/9-16.png new file mode 100644 index 0000000000000000000000000000000000000000..ce63448ff6eb4602d95f38002bcf2e8aee974238 Binary files /dev/null and b/docs/data8-textbook-zh/img/9-16.png differ diff --git a/docs/data8-textbook-zh/img/9-17.png b/docs/data8-textbook-zh/img/9-17.png new file mode 100644 index 0000000000000000000000000000000000000000..5f640a1d3a0d2599c13299e3e1537c359ca23230 Binary files /dev/null and b/docs/data8-textbook-zh/img/9-17.png differ diff --git a/docs/data8-textbook-zh/img/9-18.png b/docs/data8-textbook-zh/img/9-18.png new file mode 100644 index 0000000000000000000000000000000000000000..f2bf94c153752794e1ec292bd7e26003cbe2527a Binary files /dev/null and b/docs/data8-textbook-zh/img/9-18.png differ diff --git a/docs/data8-textbook-zh/img/9-2.png b/docs/data8-textbook-zh/img/9-2.png new file mode 100644 index 0000000000000000000000000000000000000000..e57e58697805f78d63598b11682cd3b0da9f0dca Binary files /dev/null and b/docs/data8-textbook-zh/img/9-2.png differ diff --git a/docs/data8-textbook-zh/img/9-3.png b/docs/data8-textbook-zh/img/9-3.png new file mode 100644 index 0000000000000000000000000000000000000000..651329cce97369d7f1f2d45da495b4fb2bd04525 Binary files /dev/null and b/docs/data8-textbook-zh/img/9-3.png differ diff --git a/docs/data8-textbook-zh/img/9-4.png b/docs/data8-textbook-zh/img/9-4.png new file mode 100644 index 0000000000000000000000000000000000000000..fc12f585f26091c4f08b808e0fb357f7891d8aa7 Binary files /dev/null and b/docs/data8-textbook-zh/img/9-4.png differ diff --git a/docs/data8-textbook-zh/img/9-5.png b/docs/data8-textbook-zh/img/9-5.png new file mode 100644 index 0000000000000000000000000000000000000000..15b472fc73618859a9be296450aa712bd9d73c72 Binary files /dev/null and b/docs/data8-textbook-zh/img/9-5.png differ diff --git a/docs/data8-textbook-zh/img/9-6.png b/docs/data8-textbook-zh/img/9-6.png new file mode 100644 index 0000000000000000000000000000000000000000..6ae87b723506a7dcedb20fdf74780eb257d774b4 Binary files /dev/null and b/docs/data8-textbook-zh/img/9-6.png differ diff --git a/docs/data8-textbook-zh/img/9-7.png b/docs/data8-textbook-zh/img/9-7.png new file mode 100644 index 0000000000000000000000000000000000000000..3f0faf43ac9f4e90865832797be8daee0882c8a9 Binary files /dev/null and b/docs/data8-textbook-zh/img/9-7.png differ diff --git a/docs/data8-textbook-zh/img/9-8.png b/docs/data8-textbook-zh/img/9-8.png new file mode 100644 index 0000000000000000000000000000000000000000..5727cf79d7b76e51d08a53faf3ab4fbfe1d1d6b5 Binary files /dev/null and b/docs/data8-textbook-zh/img/9-8.png differ diff --git a/docs/data8-textbook-zh/img/9-9.png b/docs/data8-textbook-zh/img/9-9.png new file mode 100644 index 0000000000000000000000000000000000000000..280e66bc77bc645b75311bd6a6c65f0e21617acc Binary files /dev/null and b/docs/data8-textbook-zh/img/9-9.png differ diff --git a/docs/data8-textbook-zh/img/qr_alipay.png b/docs/data8-textbook-zh/img/qr_alipay.png new file mode 100644 index 0000000000000000000000000000000000000000..154eb4c805cb8ad1f6b7a037feeead0d344fa5b4 Binary files /dev/null and b/docs/data8-textbook-zh/img/qr_alipay.png differ diff --git a/docs/data8-textbook-zh/img/tex-10-1.gif b/docs/data8-textbook-zh/img/tex-10-1.gif new file mode 100644 index 0000000000000000000000000000000000000000..e5db37aefdb79f68a58289136fea82e4955adfc7 Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-10-1.gif differ diff --git a/docs/data8-textbook-zh/img/tex-10-2.gif b/docs/data8-textbook-zh/img/tex-10-2.gif new file mode 100644 index 0000000000000000000000000000000000000000..296d96c38e1895596dd948aafed04adecbe04f38 Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-10-2.gif differ diff --git a/docs/data8-textbook-zh/img/tex-10-3.gif b/docs/data8-textbook-zh/img/tex-10-3.gif new file mode 100644 index 0000000000000000000000000000000000000000..d1176b24c071ef41b325569fef8cc44d7eeb28b3 Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-10-3.gif differ diff --git a/docs/data8-textbook-zh/img/tex-10-4.gif b/docs/data8-textbook-zh/img/tex-10-4.gif new file mode 100644 index 0000000000000000000000000000000000000000..4e3c08aba2c5a41e5f820ff26133b7a57d601827 Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-10-4.gif differ diff --git a/docs/data8-textbook-zh/img/tex-11-1.gif b/docs/data8-textbook-zh/img/tex-11-1.gif new file mode 100644 index 0000000000000000000000000000000000000000..023df360654c8f274f578f443587efd40e12a043 Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-11-1.gif differ diff --git a/docs/data8-textbook-zh/img/tex-12-1.gif b/docs/data8-textbook-zh/img/tex-12-1.gif new file mode 100644 index 0000000000000000000000000000000000000000..009cbac1a04006f5450aeb0a86497285d63da45e Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-12-1.gif differ diff --git a/docs/data8-textbook-zh/img/tex-12-2.gif b/docs/data8-textbook-zh/img/tex-12-2.gif new file mode 100644 index 0000000000000000000000000000000000000000..c57ec0750e1f672870e00dfaa78645de3f445050 Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-12-2.gif differ diff --git a/docs/data8-textbook-zh/img/tex-12-3.gif b/docs/data8-textbook-zh/img/tex-12-3.gif new file mode 100644 index 0000000000000000000000000000000000000000..2e3f294c7612daec0e31f4e05003e2f3342deaad Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-12-3.gif differ diff --git a/docs/data8-textbook-zh/img/tex-12-4.gif b/docs/data8-textbook-zh/img/tex-12-4.gif new file mode 100644 index 0000000000000000000000000000000000000000..64d25bc91718c2d869a1ef7e424c41d2da2ba1c2 Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-12-4.gif differ diff --git a/docs/data8-textbook-zh/img/tex-12-5.gif b/docs/data8-textbook-zh/img/tex-12-5.gif new file mode 100644 index 0000000000000000000000000000000000000000..aa3eb521f4d6c48c60c9f4fae0789f136291f7f5 Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-12-5.gif differ diff --git a/docs/data8-textbook-zh/img/tex-12-6.gif b/docs/data8-textbook-zh/img/tex-12-6.gif new file mode 100644 index 0000000000000000000000000000000000000000..b9112fb36e3db9d7130653390a001401e63b4b39 Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-12-6.gif differ diff --git a/docs/data8-textbook-zh/img/tex-12-7.gif b/docs/data8-textbook-zh/img/tex-12-7.gif new file mode 100644 index 0000000000000000000000000000000000000000..763611715d203aacc7af9c03ae0990e21da37c2e Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-12-7.gif differ diff --git a/docs/data8-textbook-zh/img/tex-12-8.gif b/docs/data8-textbook-zh/img/tex-12-8.gif new file mode 100644 index 0000000000000000000000000000000000000000..feedf124238a465118f95dafb37e7f6db214a54d Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-12-8.gif differ diff --git a/docs/data8-textbook-zh/img/tex-13-1.gif b/docs/data8-textbook-zh/img/tex-13-1.gif new file mode 100644 index 0000000000000000000000000000000000000000..262df1cd80393977419f0a5f31d5f0706ab2da24 Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-13-1.gif differ diff --git a/docs/data8-textbook-zh/img/tex-13-10.gif b/docs/data8-textbook-zh/img/tex-13-10.gif new file mode 100644 index 0000000000000000000000000000000000000000..e227b30c97d2792a4bbfc828dc66f30de9d2356a Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-13-10.gif differ diff --git a/docs/data8-textbook-zh/img/tex-13-11.gif b/docs/data8-textbook-zh/img/tex-13-11.gif new file mode 100644 index 0000000000000000000000000000000000000000..3de64536dda84c2958a0cfaa215df00c31d910ec Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-13-11.gif differ diff --git a/docs/data8-textbook-zh/img/tex-13-12.gif b/docs/data8-textbook-zh/img/tex-13-12.gif new file mode 100644 index 0000000000000000000000000000000000000000..37defc48df6d701e6a94f0013dc0554372d62b8c Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-13-12.gif differ diff --git a/docs/data8-textbook-zh/img/tex-13-13.gif b/docs/data8-textbook-zh/img/tex-13-13.gif new file mode 100644 index 0000000000000000000000000000000000000000..e3ba1978bf5bdbab4bee4e9040e33357da5c02e5 Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-13-13.gif differ diff --git a/docs/data8-textbook-zh/img/tex-13-14.gif b/docs/data8-textbook-zh/img/tex-13-14.gif new file mode 100644 index 0000000000000000000000000000000000000000..3f4ebd753f74bc020a6f73f867c49c9a31d5d414 Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-13-14.gif differ diff --git a/docs/data8-textbook-zh/img/tex-13-15.gif b/docs/data8-textbook-zh/img/tex-13-15.gif new file mode 100644 index 0000000000000000000000000000000000000000..fd7fab5466dd25e77cef8e17ec253d834e8912b7 Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-13-15.gif differ diff --git a/docs/data8-textbook-zh/img/tex-13-16.gif b/docs/data8-textbook-zh/img/tex-13-16.gif new file mode 100644 index 0000000000000000000000000000000000000000..fd7fab5466dd25e77cef8e17ec253d834e8912b7 Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-13-16.gif differ diff --git a/docs/data8-textbook-zh/img/tex-13-17.gif b/docs/data8-textbook-zh/img/tex-13-17.gif new file mode 100644 index 0000000000000000000000000000000000000000..ab0443082dd8090600e648619b382648ea6b428c Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-13-17.gif differ diff --git a/docs/data8-textbook-zh/img/tex-13-18.gif b/docs/data8-textbook-zh/img/tex-13-18.gif new file mode 100644 index 0000000000000000000000000000000000000000..42b104fe19f039575f13fe823f7b82bc6b9490ef Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-13-18.gif differ diff --git a/docs/data8-textbook-zh/img/tex-13-19.gif b/docs/data8-textbook-zh/img/tex-13-19.gif new file mode 100644 index 0000000000000000000000000000000000000000..ab495d492a4c11212dd107f747cfa14e277fb39b Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-13-19.gif differ diff --git a/docs/data8-textbook-zh/img/tex-13-2.gif b/docs/data8-textbook-zh/img/tex-13-2.gif new file mode 100644 index 0000000000000000000000000000000000000000..dda28a225b0e58dc201729581f4511ad2b1d9f6e Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-13-2.gif differ diff --git a/docs/data8-textbook-zh/img/tex-13-3.gif b/docs/data8-textbook-zh/img/tex-13-3.gif new file mode 100644 index 0000000000000000000000000000000000000000..e227b30c97d2792a4bbfc828dc66f30de9d2356a Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-13-3.gif differ diff --git a/docs/data8-textbook-zh/img/tex-13-4.gif b/docs/data8-textbook-zh/img/tex-13-4.gif new file mode 100644 index 0000000000000000000000000000000000000000..3de64536dda84c2958a0cfaa215df00c31d910ec Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-13-4.gif differ diff --git a/docs/data8-textbook-zh/img/tex-13-5.gif b/docs/data8-textbook-zh/img/tex-13-5.gif new file mode 100644 index 0000000000000000000000000000000000000000..e694f9f46083a132f5ca89ebec4573b204a4bc4a Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-13-5.gif differ diff --git a/docs/data8-textbook-zh/img/tex-13-6.gif b/docs/data8-textbook-zh/img/tex-13-6.gif new file mode 100644 index 0000000000000000000000000000000000000000..6e9dd678c1fb2bcf56de18f986c775e277c3b1e8 Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-13-6.gif differ diff --git a/docs/data8-textbook-zh/img/tex-13-7.gif b/docs/data8-textbook-zh/img/tex-13-7.gif new file mode 100644 index 0000000000000000000000000000000000000000..1233686949ab0b5d5c96818fa735fb5eff527ee5 Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-13-7.gif differ diff --git a/docs/data8-textbook-zh/img/tex-13-8.gif b/docs/data8-textbook-zh/img/tex-13-8.gif new file mode 100644 index 0000000000000000000000000000000000000000..16d0361f40fa61ea5dce5ec52d88dc3b45f60ece Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-13-8.gif differ diff --git a/docs/data8-textbook-zh/img/tex-13-9.gif b/docs/data8-textbook-zh/img/tex-13-9.gif new file mode 100644 index 0000000000000000000000000000000000000000..d983ff821d09ef290e287d9e36c76441ce801b10 Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-13-9.gif differ diff --git a/docs/data8-textbook-zh/img/tex-15-1.gif b/docs/data8-textbook-zh/img/tex-15-1.gif new file mode 100644 index 0000000000000000000000000000000000000000..53b620bb44a779a6325eaed2763108b3643df96f Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-15-1.gif differ diff --git a/docs/data8-textbook-zh/img/tex-15-2.gif b/docs/data8-textbook-zh/img/tex-15-2.gif new file mode 100644 index 0000000000000000000000000000000000000000..53b620bb44a779a6325eaed2763108b3643df96f Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-15-2.gif differ diff --git a/docs/data8-textbook-zh/img/tex-15-3.gif b/docs/data8-textbook-zh/img/tex-15-3.gif new file mode 100644 index 0000000000000000000000000000000000000000..2bfb9e89e912f729cd325cc3a60da73b22274cdd Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-15-3.gif differ diff --git a/docs/data8-textbook-zh/img/tex-17-1.gif b/docs/data8-textbook-zh/img/tex-17-1.gif new file mode 100644 index 0000000000000000000000000000000000000000..bbcb94c9bb4965d70d067f50047b6bf36b5bf64d Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-17-1.gif differ diff --git a/docs/data8-textbook-zh/img/tex-17-2.gif b/docs/data8-textbook-zh/img/tex-17-2.gif new file mode 100644 index 0000000000000000000000000000000000000000..97a4b3835c1b90cd22d6b6ed6ea01520778319ea Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-17-2.gif differ diff --git a/docs/data8-textbook-zh/img/tex-17-3.gif b/docs/data8-textbook-zh/img/tex-17-3.gif new file mode 100644 index 0000000000000000000000000000000000000000..8e15a44f77e01320e8833cea79a060311e8cbe77 Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-17-3.gif differ diff --git a/docs/data8-textbook-zh/img/tex-17-4.gif b/docs/data8-textbook-zh/img/tex-17-4.gif new file mode 100644 index 0000000000000000000000000000000000000000..081b28c7e55e20b72f830c2b02e0a011f2428796 Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-17-4.gif differ diff --git a/docs/data8-textbook-zh/img/tex-4-1.gif b/docs/data8-textbook-zh/img/tex-4-1.gif new file mode 100644 index 0000000000000000000000000000000000000000..b1dd5777150501df9bb9fca767d7d3f9acd3888f Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-4-1.gif differ diff --git a/docs/data8-textbook-zh/img/tex-4-2.gif b/docs/data8-textbook-zh/img/tex-4-2.gif new file mode 100644 index 0000000000000000000000000000000000000000..f44292eb61b79b7b00a2db9b6320b2f4d6822d8f Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-4-2.gif differ diff --git a/docs/data8-textbook-zh/img/tex-6-1.gif b/docs/data8-textbook-zh/img/tex-6-1.gif new file mode 100644 index 0000000000000000000000000000000000000000..a32cfedbffae411efec653d3d2623368bda99b74 Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-6-1.gif differ diff --git a/docs/data8-textbook-zh/img/tex-6-2.gif b/docs/data8-textbook-zh/img/tex-6-2.gif new file mode 100644 index 0000000000000000000000000000000000000000..29fa634c898d982289fa1dcf9e1a5fc857615067 Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-6-2.gif differ diff --git a/docs/data8-textbook-zh/img/tex-8-1.gif b/docs/data8-textbook-zh/img/tex-8-1.gif new file mode 100644 index 0000000000000000000000000000000000000000..caa8a4000c037002a26f4e44af78f982761e09bf Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-8-1.gif differ diff --git a/docs/data8-textbook-zh/img/tex-8-10.gif b/docs/data8-textbook-zh/img/tex-8-10.gif new file mode 100644 index 0000000000000000000000000000000000000000..d454104d6e0c9f76bc855aad778b32ccaab8c0df Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-8-10.gif differ diff --git a/docs/data8-textbook-zh/img/tex-8-11.gif b/docs/data8-textbook-zh/img/tex-8-11.gif new file mode 100644 index 0000000000000000000000000000000000000000..fb1bcb8d40947a2a2fc37ad50dfad0d700fcd814 Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-8-11.gif differ diff --git a/docs/data8-textbook-zh/img/tex-8-12.gif b/docs/data8-textbook-zh/img/tex-8-12.gif new file mode 100644 index 0000000000000000000000000000000000000000..a98db8087632b27607ee60a7831ca63eb06b80fe Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-8-12.gif differ diff --git a/docs/data8-textbook-zh/img/tex-8-13.gif b/docs/data8-textbook-zh/img/tex-8-13.gif new file mode 100644 index 0000000000000000000000000000000000000000..9c16fe1e221f7189d41d88cc33e3a48762398292 Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-8-13.gif differ diff --git a/docs/data8-textbook-zh/img/tex-8-14.gif b/docs/data8-textbook-zh/img/tex-8-14.gif new file mode 100644 index 0000000000000000000000000000000000000000..7612f303aafd79b5d7c56c35cf78178cd06474fd Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-8-14.gif differ diff --git a/docs/data8-textbook-zh/img/tex-8-15.gif b/docs/data8-textbook-zh/img/tex-8-15.gif new file mode 100644 index 0000000000000000000000000000000000000000..12b7a998d124ccb4e5cec19f5193d494863dd381 Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-8-15.gif differ diff --git a/docs/data8-textbook-zh/img/tex-8-16.gif b/docs/data8-textbook-zh/img/tex-8-16.gif new file mode 100644 index 0000000000000000000000000000000000000000..1ec6b90be5bda6ef5cde8697cfb9479f19cd956c Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-8-16.gif differ diff --git a/docs/data8-textbook-zh/img/tex-8-2.gif b/docs/data8-textbook-zh/img/tex-8-2.gif new file mode 100644 index 0000000000000000000000000000000000000000..b0c77f21b157b86fbd5c84f3b249798c6effa53d Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-8-2.gif differ diff --git a/docs/data8-textbook-zh/img/tex-8-3.gif b/docs/data8-textbook-zh/img/tex-8-3.gif new file mode 100644 index 0000000000000000000000000000000000000000..93d2cfcf1efe7ee7c2c05fea8ec297a015878790 Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-8-3.gif differ diff --git a/docs/data8-textbook-zh/img/tex-8-4.gif b/docs/data8-textbook-zh/img/tex-8-4.gif new file mode 100644 index 0000000000000000000000000000000000000000..d95a8dc694f55fa878a9d7501ffeef4a5d102d0f Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-8-4.gif differ diff --git a/docs/data8-textbook-zh/img/tex-8-5.gif b/docs/data8-textbook-zh/img/tex-8-5.gif new file mode 100644 index 0000000000000000000000000000000000000000..9b96575a030dfd1f0d02464683adb5bf7e174732 Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-8-5.gif differ diff --git a/docs/data8-textbook-zh/img/tex-8-6.gif b/docs/data8-textbook-zh/img/tex-8-6.gif new file mode 100644 index 0000000000000000000000000000000000000000..9afcdda70fbaffbf74ec1588a0847e0da648b381 Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-8-6.gif differ diff --git a/docs/data8-textbook-zh/img/tex-8-7.gif b/docs/data8-textbook-zh/img/tex-8-7.gif new file mode 100644 index 0000000000000000000000000000000000000000..2ce7b2f4f13358c09cafaa8905b4c92ff6f03035 Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-8-7.gif differ diff --git a/docs/data8-textbook-zh/img/tex-8-8.gif b/docs/data8-textbook-zh/img/tex-8-8.gif new file mode 100644 index 0000000000000000000000000000000000000000..1bf7bfe2da6c85404185db236d0f94de8d65b049 Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-8-8.gif differ diff --git a/docs/data8-textbook-zh/img/tex-8-9.gif b/docs/data8-textbook-zh/img/tex-8-9.gif new file mode 100644 index 0000000000000000000000000000000000000000..44926e12e604b963e2d980762537fc648391f243 Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-8-9.gif differ diff --git a/docs/data8-textbook-zh/img/tex-9-1.gif b/docs/data8-textbook-zh/img/tex-9-1.gif new file mode 100644 index 0000000000000000000000000000000000000000..688e4f8938b87471bd881f27b1fa270a56952e24 Binary files /dev/null and b/docs/data8-textbook-zh/img/tex-9-1.gif differ diff --git a/docs/nlp-py-2e/0.md b/docs/nlp-py-2e/0.md new file mode 100644 index 0000000000000000000000000000000000000000..20f7697bfb03532a6adf4ad9597a1514f3941ddc --- /dev/null +++ b/docs/nlp-py-2e/0.md @@ -0,0 +1,196 @@ +# 前言 + +这是一本关于自然语言处理的书。所谓“自然语言”,是指人们日常交流使用的语言,如英语,印地语,葡萄牙语等。相对于编程语言和数学符号这样的人工语言,自然语言随着一代人传给另一代人而不断演化,因而很难用明确的规则来刻画。从广义上讲,“自然语言处理”(Natural Language Processing 简称 NLP)包含所有用计算机对自然语言进行的操作。举个极端的例子,它可以是简单的通过计数词出现的频率来比较不同的写作风格。另外一个极端的例子,NLP 包括完全“理解”人所说的话,至少要能达到对人的话语作出有效反应的程度。 + +基于 NLP 的技术应用日益广泛。例如:手机和手持电脑支持输入法联想提示和手写识别;网络搜索引擎能搜到非结构化文本中的信息;机器翻译能把中文文本翻译成西班牙文;文本分析让我们能够检测推文和博客中的情感。通过提供更自然的人机界面和更复杂的存储信息获取手段,语言处理正在这个多语种的信息社会中扮演更核心的角色。 + +这本书提供自然语言处理领域非常方便的入门指南。它可以用来自学,也可以作为自然语言处理或计算语言学课程的教科书,或是人工智能、文本挖掘、语料库语言学课程的补充读物。本书的实践性很强,包括几百个实际可用的例子和分级练习。 + +本书基于 Python 编程语言及其上的一个名为*自然语言工具包*(Natural Language Toolkit ,简称 NLTK)的开源库。NLTK 包含大量的软件、数据和文档,所有这些都可以从`http://nltk.org/`免费下载。NLTK 的发行版本支持 Windows、Macintosh 和 Unix 平台。我们强烈建议你下载 Python 和 NLTk,与我们一起尝试书中的例子和练习。 + +## 读者 + +NLP 是科学、经济、社会和文化的一个重要因素。NLP 正在迅速成长,它的很多理论和方法在大量新的语言技术中得到应用。所以对很多行业的人来说掌握 NLP 知识十分重要。在应用领域包括从事人机交互、商业信息分析、web 软件开发的人。在学术界包括从人文计算学、语料库语言学到计算机科学和人工智能领域的人。(学术界的很多人把 NLP 叫称为“计算语言学”。) + +本书旨在帮助所有想要学习如何编写程序分析书面语言的人,不管他们以前的编程经验如何: + +```py +>>> for line in open("file.txt"): +... for word in line.split(): +... if word.endswith('ing'): +... print(word) +``` + +这段程序演示了 Python 的一些主要特征。首先,使用空格*缩进*代码,从而使`if`后面的代码都在前面一行`for`语句的范围之内;这保证了检查单词是否以`ing`结尾的测试对所有单词都进行。第二,Python 是*面向对象*语言;每一个变量都是包含特定属性和方法的对象。例如,变量`line`的值不仅仅是一个字符串序列。它是一个`string`对象,包含一个用来把字符串分割成词的`split()`方法(或叫操作)。我们在对象名称后面写上一个点再写上方法名称就可以调用对象的一个方法,例如`line.split()`。第三,方法的*参数*写在括号内。例如,上面的例子中的`word.endswith('ing')`具有一个参数`'ing'`表示我们需要找的是`ing`结尾的词而不是别的结尾的词。最后也是最重要的,Python 的可读性如此之强以至于可以相当容易的猜出程序的功能,即使你以前从未写过一行代码。 + +我们选择 Python 是因为它的学习曲线比较平缓,文法和语义都很清晰,具有良好的处理字符串的功能。作为解释性语言,Python 便于交互式编程。作为面向对象语言,Python 允许数据和方法被方便的封装和重用。作为动态语言,Python 允许属性等到程序运行时才被添加到对象,允许变量自动类型转换,提高开发效率。Python 自带强大的标准库,包括图形编程、数值处理和网络连接等组件。 + +Python 在世界各地的工业、科研、教育领域应用广泛。它因为提高了软件的生产效率、质量和可维护性而备受称赞。`http://python.org/about/success/`中列举了许多成功使用 Python 的故事。 + +NLTK 定义了一个使用 Python 进行 NLP 编程的基础工具。它提供重新表示自然语言处理相关数据的基本类,词性标注、文法分析、文本分类等任务的标准接口以及这些任务的标准实现,可以组合起来解决复杂的问题。 + +NLTK 自带大量文档。作为本书的补充,`http://nltk.org/`网站提供的 API 文档涵盖工具包中每一个模块、类和函数,详细说明了各种参数,还给出了用法示例。 + +## Python 3 和 NLTK 3 + +本书的这个版本已更新并支持 Python 3 和 NLTK 3。Python 3 包括一些重大的变化: + +* `print`语句现在是函数,因此需要括号; +* 许多函数现在返回迭代器而不是列表 (以节省内存使用); +* 整数除法返回一个浮点数 +* 所有文本现在都是 Unicode 编码 +* 字符串的格式化使用`format`方法 + +这些变化的更多细节请参见`https://docs.python.org/dev/whatsnew/3.0.html`.。有一个`2to3.py`工具可以将 Python 2 代码转换为 Python 3;关详细信息请参阅`https://docs.python.org/2/library/2to3.html`。 + +NLTK 同样很多地方都有更改: + +* 许多类型使用`fromstring()`方法从字符串初始化 +* 许多函数现在返回迭代器而不是列表 +* `ContextFreeGrammar`现在叫做`CFG`,`WeightedGrammar`现在叫做`PCFG` +* `batch_tokenize()`现在叫做`tokenize_sents()`;对应的标记器, 解析器和分类器都有变化 +* 有些实现已删除以支持外部包,或因为不能充分维护 + +更详细的变更列表请参见`https://github.com/nltk/nltk/wiki/Porting-your-code-to-NLTK-3.0`。 + +## 软件安装需求 + +为了充分利用好本书,你应该安装一些免费的软件包。`http://nltk.org/`上有这些软件包当前的下载链接和安装说明。 + +| Python: | 本书中例子假定你正在使用 Python 3.2 或更高版本。(注,NLTK 3.0 也适用于 Python 2.6 和 2.7)。 | +| --- | --- | +| NLTK: | 这本书中的代码示例使用 NLTK 3.0 版。NLTK 的后续版本将会向后兼容 NLTK 3.0。 | +| NLTK-Data: | 包含本书中分析和处理的语言语料库。 | +| NumPy: | (推荐)这是一个科学计算库,支持多维数组和线性代数,在某些计算概率、标记、聚类和分类任务中用到。 | +| Matplotlib: | (推荐)这是一个用于数据可视化的 2D 绘图库,本书在产生线图和条形图的程序例子中用到。 | +| 斯坦福大学 NLP 工具: | (推荐)NLTK 包括斯坦福大学 NLP 工具的接口,可用于大型语言处理(见`http://nlp.stanford.edu/software/`)。 | +| NetworkX: | (可选)这是一个用于存储和操作由节点和边组成的网络结构的函数库。可视化语义网络还需要安装 *Graphviz* 库。 | +| Prover9: | (可选)这是一个使用一阶等式逻辑定理的自动证明器,用于支持语言处理中的推理。 | + +## 自然语言工具包(NLTK) + +NLTK 创建于 2001 年,最初是宾州大学计算机与信息科学系计算语言学课程的一部分。从那以后,在数十名贡献者的帮助下不断发展壮大。如今,它已被几十所大学的课程所采纳,并作为许多研究项目的基础。[VIII.1](http://www.nltk.org/book/ch00.html#tab-modules) 列出了 NLTK 的一些最重要的模块。 + +表 VIII.1: + +语言处理任务与相应 NLTK 模块以及功能描述 + +| 语言处理任务 | NLTK 模块 | 功能 | +| --- | --- | --- | +| 访问语料库 | `corpus` | 语料库与词典的标准化接口 | +| 字符串处理 | `tokenize, stem` | 分词,分句,提取主干 | +| 搭配的发现 | `collocations` | t-检验,卡方,点互信息 PMI | +| 词性标注 | `tag` | N 元组, backoff, Brill, HMM, TnT | +| 机器学习 | `classify, cluster, tbl` | 决策树,最大熵,贝叶斯,EM,k-means | +| 分块 | `chunk` | 正则表达式,N 元组,命名实体 | +| 解析 | `parse, ccg` | 图表,基于特征,一致性,概率,依赖 | +| 语义解释 | `sem, inference` | λ演算,一阶逻辑,模型检验 | +| 指标评测 | `metrics` | 精度,召回率,协议系数 | +| 概率和估计 | `probability` | 频率分布,平滑概率分布 | +| 应用 | `app, chat` | 图形化的语料库检索工具,分析器,WordNet 查看器,聊天机器人 | +| 语言学领域的工作 | `toolbox` | 处理 SIL 工具箱格式的数据 | + +NLTK 设计中的四个主要目标: + +| 简单: | 提供一个直观的框架和大量构建模块,使用户获取 NLP 知识而不必陷入像标注语言数据那样繁琐的事务中 | +| --- | --- | +| 一致: | 提供一个具有一致的接口和数据结构的,并且方法名称容易被猜到的统一的框架 | +| 可扩展: | 提供一种结构,新的软件模块包括同一个任务中的不同的实现和相互冲突的方法都可以方便添加进来 | +| 模块化: | 提供的组件可以独立使用而无需理解工具包的其他部分 | + +对比上述目标,我们有意回避了工具包三个非需求行的但可能有用的特征。首先,虽然工具包提供了广泛的工具,但它不是面面俱全的;它是一个工具包而不是一个系统,它将会随着 NLP 领域一起演化。第二,虽然这个工具包的效率足以支持实际的任务,但它运行时的性能还没有高度优化;这种优化往往涉及更复杂的算法或使用 C 或 C++ 等较低一级的编程语言来实现。这将影响工具包的可读性且更难以安装。第三,我们试图避开巧妙的编程技巧,因为我们相信清楚直白的实现比巧妙却可读性差的方法好。 + +## 教师请看 + +自然语言处理一般是在高年级本科生或研究生层次开设的为期一个学期的课程。很多教师都发现,在如此短的时间里涵盖理论和实践两个方面是十分困难的。有些课程注重理论而排挤实践练习,剥夺了学生编写程序自动处理语言带来的挑战和兴奋感。另一些课程仅仅教授语言学编程而不包含任何重要的 NLP 内容。最初开发 NLTK 就是为了解决这个问题,使在一个学期里同时教授大量理论和实践成为可能,无论学生事先有没有编程经验。 + +算法和数据结构在所有 NLP 教学大纲中都十分重要。它们本身可能非常枯燥,而 NLTK 提供的交互式图形用户界面能一步一步看到算法过程,使它们变得鲜活。大多数 NLTK 组件都有一个无需用户输入任何数据就能执行有趣的任务的示范性例子。学习本书的一个有效的方法就是交互式重现书中的例子,把它们输入到 Python 会话控制台,观察它们做了些什么,修改它们去探索试验或理论问题。 + +本书包含了数百个练习,可作为学生作业的基础。最简单的练习涉及用指定的方式修改已有的程序片段来回答一个具体的问题。另一个极端,NLTK 为研究生水平的研究项目提供了一个灵活的框架,包括所有的基本数据结构和算法的标准实现,几十个广泛使用的数据集(语料库)的接口,以及一个灵活可扩展的体系结构。NLTK 网站上还有其他资源支持教学中使用 NLTK。 + +我们相信本书是唯一为学生提供在学习编程的环境中学习 NLP 的综合性框架。各个章节和练习通过 NLTK 紧密耦合,并将各章材料分割开,为学生(即使是那些以前没有编程经验的学生)提供一个实用的 NLP 的入门指南。学完这些材料后,学生将准备好尝试一本更加深层次的教科书,例如《语音和语言处理》,作者是 Jurafsky 和 Martin(Prentice Hall 出版社,2008 年)。 + +本书介绍编程概念的顺序与众不同,以一个重要的数据类型字符串列表开始,然后介绍重要的控制结构如推导和条件式等。这些概念允许我们在一开始就做一些有用的语言处理。有了这样做的冲动,我们回过头来系统的介绍一些基础概念,如字符串,循环,文件等。这样的方法同更传统的方法达到了同样的效果而不必要求读者自己已经对编程感兴趣。 + +[IX.1](http://www.nltk.org/book/ch00.html#tab-course-plans) 列出了两个课程计划表。第一个适用于文科,第二个适用于理工科。其他的课程计划应该涵盖前 5 章,然后把剩余的时间投入单独的领域,例如:文本分类(第 6、7 章)、文法(第 8、9 章)、语义(第 10 章)或者语言数据管理(第 11 章)。 + +表 IX.1: + +课程计划建议;每一章近似的课时数 + +| 章节 | 文科 | 理工科 | +| --- | --- | --- | +| 第 1 章 语言处理与 Python | 2-4 | 2 | +| 第 2 章 获得文本语料和词汇资源 | 2-4 | 2 | +| 第 3 章 处理原始文本 | 2-4 | 2 | +| 第 4 章 编写结构化程序 | 2-4 | 1-2 | +| 第 5 章 分类和标注单词 | 2-4 | 2-4 | +| 第 6 章 学习本文分类 | 0-2 | 2-4 | +| 第 7 章 从文本提取信息 | 2 | 2-4 | +| 第 8 章 分析句子结构 | 2-4 | 2-4 | +| 第 9 章 构建基于特征的文法 | 2-4 | 1-4 | +| 第 10 章 分析句子的含义 | 1-2 | 1-4 | +| 第 11 章 语言学数据管理 | 1-2 | 1-4 | +| 总计 | 18-36 | 18-36 | + +## 本书使用的约定 + +本书使用以下印刷约定: + +**粗体** -- 表示新的术语。 + +*斜体* -- 用在段落中表示语言学例子、文本的名称和 URL,文件名和后缀名也用斜体。 + +`等宽字体` -- 用来表示程序清单,用在段落中表示变量、函数名、语句或关键字等程序元素;也用来表示程序名。 + +`等宽粗体` -- 表示应该由用户输入的命令或其他文本。 + +`等宽斜体` -- 表示应由用户提供的值或上下文决定的值来代替文本中的值;也在程序代码例子中表示元变量。 + +注 + +此图标表示提示、建议或一般性注意事项。 + +警告! + +此图标表示警告或重要提醒。 + +## 使用例子代码 + +本书是为了帮你完成你的工作的。。一般情况下,你都可以在你的程序或文档中使用本书中的代码。不需要得到我们获得允许,除非你要大量的复制代码。例如,编写的程序用到书中几段代码不需要许可。销售和分发 O'Reilly 书籍中包含的例子的 CD-ROM 需要获得许可。援引本书和书中的例子来回答问题不需要许可。大量的将本书中的例子纳入你的产品文档将需要获得许可。 + +我们希望但不是一定要求被参考文献引用。一个引用通常包括标题,作者,出版者和 ISBN。例如:Python 自然语言处理,Steven Bird,Ewan Klein 和 Edward Loper。O'Reilly Media, 978-0-596-51649-9\. 如果你觉得你使用本书的例子代码超出了上面列举的一般用途或许可,随时通过 *permissions@oreilly.com* 联系我们。 + +## 致谢 + +作者感激为本书早期手稿提供反馈意见的人,他们是:Doug Arnold, Michaela Atterer, Greg Aumann, Kenneth Beesley, Steven Bethard, Ondrej Bojar, Chris Cieri, Robin Cooper, Grev Corbett, James Curran, Dan Garrette, Jean Mark Gawron, Doug Hellmann, Nitin Indurkhya, Mark Liberman, Peter Ljunglöf, Stefan Müller, Robin Munn, Joel Nothman, Adam Przepiorkowski, Brandon Rhodes, Stuart Robinson, Jussi Salmela, Kyle Schlansker, Rob Speer 和 Richard Sproat。感谢许许多多的学生和同事,他们关于课堂材料的意见演化成本书的这些章节,其中包括巴西,印度和美国的 NLP 与语言学暑期学校的参加者。没有`nltk-dev`开发社区的成员们的努力这本书也不会存在,他们为建设和壮大 NLTK 无私奉献他们的时间和专业知识,他们的名字都记录在 NLTK 网站上。 + +非常感谢美国国家科学基金会、语言数据联盟、Edward Clarence Dyason 奖学金、宾州大学、爱丁堡大学和墨尔本大学对我们在本书相关的工作上的支持。 + +感谢 Julie Steele、Abby Fox、Loranah Dimant 以及其他 O'Reilly 团队成员,他们组织大量 NLP 和 Python 社区成员全面审阅我们的手稿,很高兴的为满足我们的需要定制 O'Reilly 的生成工具。感谢他们一丝不苟的审稿工作。 + +在准备 Python 3 修订版的过程中,感谢 Michael Korobov 领导将 NLTK 移植到 Python 3,以及 Antoine Trux 对第一版细致的反馈意见。 + +最后,我们对我们的合作伙伴欠了巨额的感情债,他们是 Mimo 和 Jee。我们希望我们的孩子 —— Andrew, Alison、Kirsten、Leonie 和 Maaike —— 能从这些页面中感受到我们对语言和计算的热情。 + +## 关于作者 + +**Steven Bird** 是在墨尔本大学计算机科学与软件工程系副教授和美国宾夕法尼亚大学的语言数据联盟的高级研究助理。他于 1990 年在爱丁堡大学完成计算音韵学博士学位,由 Ewan Klein 指导。后来到喀麦隆开展夏季语言学研究所主持下的 Grassfields 班图语语言实地调查。最近,他作为语言数据联盟副主任花了几年时间领导研发队伍,创建已标注文本的大型数据库的模型和工具。在墨尔本大学,他建立了一个语言技术研究组,并在各级本科计算机科学课程任教。2009 年,史蒂芬成为计算语言学学会主席。 + +**Ewan Klein**是英国爱丁堡大学信息学院语言技术教授。于 1978 年在剑桥大学完成形式语义学博士学位。在苏塞克斯和纽卡斯尔大学工作多年后,参加了在爱丁堡的教学岗位。他于 1993 年参与了爱丁堡语言科技集团的建立,并一直与之密切联系。从 2000 到 2002,他离开大学作为圣克拉拉的埃迪法公司的总部在爱丁堡的自然语言的研究小组的研发经理,负责处理口语对话。Ewan 是计算语言学协会欧洲分会(European Chapter of the Association for Computational Linguistics)前任主席,并且是人类语言技术(ELSNET)欧洲卓越网络的创始成员和协调员。 + +**Edward Loper** 最近完成了宾夕法尼亚大学自然语言处理的机器学习博士学位。Edward 是 Steven 在 2000 年秋季计算语言学研究生课程的学生,也是教师助手和 NLTK 开发的成员。除了 NLTK,他帮助开发了用于文档化和测试 Python 软件的两个包:`epydoc`和`doctest`。 + +## 版税 + +出售这本书的版税将被用来支持自然语言工具包的发展。 + +![Images/authors.png](Images/d87676460c87d0516fb382b929c07302.jpg) + +图 XIV.1:Edward Loper, Ewan Klein 和 Steven Bird, 斯坦福大学, 2007 年 7 月 + +## 关于本文档... + +针对 NLTK 3.0 进行更新。本章来自于《Python 自然语言处理》,[Steven Bird](http://estive.net/), [Ewan Klein](http://homepages.inf.ed.ac.uk/ewan/) 和 [Edward Loper](http://ed.loper.org/),Copyright © 2014 作者所有。本章依据 [*Creative Commons Attribution-Noncommercial-No Derivative Works 3\.0 United States License*](http://creativecommons.org/licenses/by-nc-nd/3.0/us/) 条款,与[*自然语言工具包*](http://nltk.org/) 3.0 版一起发行。 + +本文档构建于 2015 年 7 月 1 日 星期三 12:30:05 AEST \ No newline at end of file diff --git a/docs/nlp-py-2e/1.md b/docs/nlp-py-2e/1.md new file mode 100644 index 0000000000000000000000000000000000000000..1e816109e1e338b67316a8445f820a629ce7a071 --- /dev/null +++ b/docs/nlp-py-2e/1.md @@ -0,0 +1,969 @@ +# 1 语言处理与 Python + +上百万字的文本,是容易拿到手的。假设我们会写一些简单的程序,那我们可以用它来做些什么?在本章中,我们将解决以下几个问题: + +1. 将简单的程序与大量的文本结合起来,我们能实现什么? +2. 我们如何能自动提取概括文本风格和内容的关键词和短语? +3. Python 编程语言为上述工作提供了哪些工具和技术? +4. 自然语言处理中有哪些有趣的挑战? + +本章分为完全不同风格的两部分。在“语言计算”部分,我们将选取一些语言相关的编程任务而不去解释它们是如何实现的。在“近观 Python”部分,我们将系统地回顾关键的编程概念。两种风格将按章节标题区分,而后面几章将混合两种风格而不作明显的区分。我们希望这种风格的介绍能使你对接下来将要碰到的内容有一个真实的体味,与此同时,涵盖语言学与计算机科学的基本概念。如果你对这两个方面已经有了基本的了解,可以跳到第 5 节 ; 我们将在后续的章节中重复所有要点,如果错过了什么,你可以很容易地在`http://nltk.org/`上查询在线参考材料。如果这些材料对你而言是全新的,那么本章将引发比解答本身更多的问题,这些问题将在本书的其余部分讨论。 + +## 1 语言计算:文本和单词 + +我们都对文本非常熟悉,因为我们每天都读到和写到。在这里,把文本视为我们写的程序的原始数据,这些程序以很多有趣的方式处理和分析文本。但在我们能写这些程序之前,我们必须得从 Python 解释器开始。 + +## 1.1 Python 入门 + +Python 对用户友好的一个方式是你可以交互式地直接打字给解释器 —— 将要运行你的 Python 代码的程序。你可以通过一个简单的叫做交互式开发环境(Interactive DeveLopment Environment,简称 IDLE)的图形接口来访问 Python 解释器。在 Mac 上,你可以在“应用程序 → MacPython”中找到;在 Windows 中,你可以在“程序 → Python”中找到。在 Unix 下,你可以在 shell 输入`idle`来运行 Python(如果没有安装,尝试输入`python`)。解释器将会输出关于你的 Python 的版本简介,请检查你运行的是否是 Python 3.2 更高的版本(这里是 3.4.2): + +```py +Python 3.4.2 (default, Oct 15 2014, 22:01:37) +[GCC 4.2.1 Compatible Apple LLVM 5.1 (clang-503.0.40)] on darwin +Type "help", "copyright", "credits" or "license" for more information. +>>> +``` + +注 + +如果你无法运行 Python 解释器可能是因为没有正确安装 Python。请访问`http://python.org/`查阅详细操作说明。NLTK 3.0 在 Python 2.6 和 2.7 上同样可以工作。如果你使用的是这些较旧的版本,注意`/`运算符会向下舍入小数(所以`1/3`会得到`0`)。为了得到预期的除法行为,你需要输入:`from __future__ import division` + +`>>>`提示符表示 Python 解释器正在等待输入。复制这本书的例子时,自己不要键入`>>>`。现在,让我们开始把 Python 当作计算器使用: + +```py +>>> 1 + 5 * 2 - 3 +8 +>>> +``` + +一旦解释器计算并显示出答案,提示符就会出现。这表示 Python 解释器在等待另一个指令。 + +注意 + +**轮到你来**:输入一些你自己的表达式。你可以使用星号(`*`)表示乘法,左斜线(`/`)表示除法,你可以用括号括起表达式。 + +前面的例子演示了如何交互式的使用 Python 解释器,试验 Python 语言中各种表达式,看看它们做些什么。现在让我们尝试一个无意义的表达式,看看解释器如何处理: + +```py +>>> 1 + + File "", line 1 + 1 + + ^ +SyntaxError: invalid syntax +>>> +``` + +产生了一个语法错误。在 Python 中,指令以加号结尾是没有意义的。Python 解释器会指出发生错误的行(“标准输入”``的第 1 行)。 + +现在我们学会使用 Python 解释器了,已经准备好可以开始处理语言数据了。 + +## 1.2 NLTK 入门 + +在进一步深入之前,应先安装 NLTK 3.0,可以从`http://nltk.org/`免费下载。按照说明下载适合你的操作系统的版本。 + +安装完 NLTK 之后,像前面那样启动 Python 解释器,在 Python 提示符后面输入下面两个命令来安装本书所需的数据,然后选择`book`集合,如 1.1 所示。 + +```py +>>> import nltk +>>> nltk.download() +``` + +![Images/nltk-downloader.png](Images/7f27bfe5324e4d9573ddd210531a8126.jpg) + +图 1.1:下载 NLTK 图书集:使用`nltk.download()`浏览可用的软件包.下载器上`Collections`选项卡显示软件包如何被打包分组,选择`book`标记所在行,可以获取本书的例子和练习所需的全部数据。这些数据包括约 30 个压缩文件,需要 100MB 硬盘空间。完整的数据集(即下载器中的`all`)在本书写作期间大约是这个大小的 10 倍,还在不断扩充。 + +一旦数据被下载到你的机器,你就可以使用 Python 解释器加载其中一些。第一步是在 Python 提示符后输入一个特殊的命令,告诉解释器去加载一些我们要用的文本:`from nltk.book import *` 。这条语句是说“从 NLTK 的`book`模块加载所有的东西”。这个`book`模块包含你阅读本章所需的所有数据。。在输出欢迎信息之后,将会加载几本书的文本(这将需要几秒钟)。下面连同你将看到的输出一起再次列出这条命令。注意拼写和标点符号的正确性,记住不要输入`>>>`。 + +```py +>>> from nltk.book import * +*** Introductory Examples for the NLTK Book *** +Loading text1, ..., text9 and sent1, ..., sent9 +Type the name of the text or sentence to view it. +Type: 'texts()' or 'sents()' to list the materials. +text1: Moby Dick by Herman Melville 1851 +text2: Sense and Sensibility by Jane Austen 1811 +text3: The Book of Genesis +text4: Inaugural Address Corpus +text5: Chat Corpus +text6: Monty Python and the Holy Grail +text7: Wall Street Journal +text8: Personals Corpus +text9: The Man Who Was Thursday by G . K . Chesterton 1908 +>>> +``` + +任何时候我们想要找到这些文本,只需要在 Python 提示符后输入它们的名字: + +```py +>>> text1 + +>>> text2 + +>>> +``` + +现在我们可以和这些数据一起来使用 Python 解释器,我们已经准备好上手了。 + +## 1.3 搜索文本 + +除了阅读文本之外,还有很多方法可以用来研究文本内容。词语索引视角显示一个指定单词的每一次出现,连同一些上下文一起显示。下面我们输入`text1`后面跟一个点,再输入函数名`concordance`,然后将`"monstrous"`放在括号里,来查一下 *Moby Dick*《白鲸记》中的词`monstrous`: + +```py +>>> text1.concordance("monstrous") +Displaying 11 of 11 matches: +ong the former , one was of a most monstrous size . ... This came towards us , +ON OF THE PSALMS . " Touching that monstrous bulk of the whale or ork we have r +ll over with a heathenish array of monstrous clubs and spears . Some were thick +d as you gazed , and wondered what monstrous cannibal and savage could ever hav +that has survived the flood ; most monstrous and most mountainous ! That Himmal +they might scout at Moby Dick as a monstrous fable , or still worse and more de +th of Radney .'" CHAPTER 55 Of the monstrous Pictures of Whales . I shall ere l +ing Scenes . In connexion with the monstrous pictures of whales , I am strongly +ere to enter upon those still more monstrous stories of them which are to be fo +ght have been rummaged out of this monstrous cabinet there is no telling . But +of Whale - Bones ; for Whales of a monstrous size are oftentimes cast up dead u +>>> +``` + +在一段特定的文本上第一次使用`concordance`会花费一点时间来构建索引,因此接下来的搜索会很快。 + +注意 + +**轮到你来**:尝试搜索其他词;为了方便重复输入,你也许会用到上箭头,`Ctrl+↑`或者`Alt+p`获取之前输入的命令,然后修改要搜索的词。你也可以在我们包含的其他文本上搜索。例如, 使用`text2.concordance("affection")`,搜索《理智与情感》(*Sense and Sensibility*)中的`affection`。使用`text3.concordance("lived")`搜索《创世纪》(Genesis)找出某人活了多久。你也可以看看`text4`,《就职演说语料》(*Inaugural Address Corpus*),回到 1789 年看看那时英语的例子,搜索如`nation`, `terror`,`god`这样的词,看看随着时间推移这些词的使用如何不同。我们也包括了`text5`,《NPS 聊天语料库》(NPS Chat Corpus):你可以在里面搜索一些网络词,如`im ur`,`lol`。(注意这个语料库未经审查!) + +在你花了一小会儿研究这些文本之后,我们希望你对语言的丰富性和多样性有一个新的认识。在下一章中,你将学习获取更广泛的文本,包括英语以外其他语言的文本。 + +词语索引使我们看到词的上下文。例如,我们看到`monstrous`出现的上下文, `the ___ pictures`和`a ___ size`。还有哪些词出现在相似的上下文中?我们可以通过在被查询的文本名后添加函数名`similar`,然后在括号中插入相关的词来查找到: + +```py +>>> text1.similar("monstrous") +mean part maddens doleful gamesome subtly uncommon careful untoward +exasperate loving passing mouldy christian few true mystifying +imperial modifies contemptible +>>> text2.similar("monstrous") +very heartily so exceedingly remarkably as vast a great amazingly +extremely good sweet +>>> +``` + +观察我们从不同的文本中得到的不同结果。Austen 使用这些词与 Melville 完全不同;在她那里,`monstrous`是正面的意思,有时它的功能像词`very`一样作强调成分。 + +函数`common_contexts`允许我们研究两个或两个以上的词共同的上下文,如`monstrous`和`very`。我们必须用方括号和圆括号把这些词括起来,中间用逗号分割: + +```py +>>> text2.common_contexts(["monstrous", "very"]) +a_pretty is_pretty am_glad be_glad a_lucky +>>> +``` + +注意 + +**轮到你来**:挑选另一对词,使用`similar()`和`common_contexts()`函数比较它们在两个不同文本中的用法。 + +自动检测出现在文本中的特定的词,并显示同样上下文中出现的一些词,这只是一个方面。我们也可以判断词在文本中的*位置*:从文本开头算起在它前面有多少词。这个位置信息可以用离散图表示。每一个竖线代表一个单词,每一行代表整个文本。在 1.2 中,我们看到在过去 220 年中的一些显著的词语用法模式(在一个由就职演说语料首尾相连的人为组合的文本中)。可以用下面的方法画出这幅图。你也许会想尝试更多的词(如,`liberty`,`constitution`)和不同的文本。你能在看到这幅图之前预测一个词的分布吗?跟以前一样,请保证引号、逗号、中括号及小括号的使用完全正确。 + +```py +>>> text4.dispersion_plot(["citizens", "democracy", "freedom", "duties", "America"]) +>>> +``` + +![Images/inaugural.png](Images/10a8a58e33a0a6b7fb71389ea2114566.jpg) + +图 1.2:美国总统就职演说词汇分布图:可以用来研究随时间推移语言使用上的变化。 + +注意 + +**重要事项**:为了画出这本书中用到的图形,你需要安装 Python 的 NumPy 和 Matplotlib 包。请参阅`http://nltk.org/`上的安装说明。 + +注意 + +你还可以使用`https://books.google.com/ngrams`画出词汇随着时间的使用频率。 + +现在轻松一下,让我们尝试产生一些刚才看到的不同风格的随机文本。要做到这一点,我们需要输入文本的名字后面跟函数名`generate`。(需要带括号,但括号里没有也什么。) + +```py +>>> text3.generate() +In the beginning of his brother is a hairy man , whose top may reach +unto heaven ; and ye shall sow the land of Egypt there was no bread in +all that he was taken out of the month , upon the earth . So shall thy +wages be ? And they made their father ; and Isaac was old , and kissed +him : and Laban with his cattle in the midst of the hands of Esau thy +first born , and Phichol the chief butler unto his son Isaac , she +>>> +``` + +Note + +`generate()`方法在 NLTK 3.0 中不可用,但会在后续版本中恢复。 + +## 1.4 词汇计数 + +关于前面例子中出现的文本,最明显的事实是它们所使用的词汇不同。在本节中,我们将看到如何使用计算机以各种有用的方式计数词汇。像以前一样,你将会马上开始用 Python 解释器进行试验,即使你可能还没有系统的研究过 Python。通过修改这些例子测试一下你是否理解它们,尝试一下本章结尾处的练习。 + +首先,让我们算出文本从头到尾的长度,包括文本中出现的词和标点符号。我们使用函数`len`获取长度,请看在《创世纪》中使用的例子: + +```py +>>> len(text3) +44764 +>>> +``` + +《创世纪》有 44764 个词和标点符号或者叫“词符”。词符 表示一个我们想要整体对待的字符序列 —— 例如`hairy`,`his`或`:)`。当我们计数文本如`to be or not to be`这个短语中词符的个数时,我们计数这些序列出现的次数。因此,我们的例句中出现了`to`和`be`各两次,`or`和`not`各一次。然而在例句中只有 4 个不同的词。《创世纪》中有多少不同的词?要用 Python 来回答这个问题,我们处理问题的方法将稍有改变。一个文本词汇表只是它用到的词符的*集合*,因为在集合中所有重复的元素都只算一个。Python 中我们可以使用命令:`set(text3)`获得`text3`的词汇表。当你这样做时,屏幕上的很多词会掠过。现在尝试以下操作: + +```py +>>> sorted(set(text3)) ❶ +['!', "'", '(', ')', ',', ',)', '.', '.)', ':', ';', ';)', '?', '?)', +'A', 'Abel', 'Abelmizraim', 'Abidah', 'Abide', 'Abimael', 'Abimelech', +'Abr', 'Abrah', 'Abraham', 'Abram', 'Accad', 'Achbor', 'Adah', ...] +>>> len(set(text3)) ❷ +2789 +>>> +``` + +用`sorted()`包裹起 Python 表达式`set(text3)`❶,我们得到一个词汇项的排序表,这个表以各种标点符号开始,然后是以`A`开头的词汇。大写单词排在小写单词前面。我们通过求集合中元素的个数间接获得词汇表的大小,再次使用`len`来获得这个数值❷。尽管小说中有 44,764 个词符,但只有 2,789 个不同的单词或“词类型”。一个词类型是指一个词在一个文本中独一无二的出现形式或拼写 —— 也就是说,这个词在词汇表中是唯一的。我们计数的 2,789 个元素中包括标点符号,所以我们把这些叫做唯一元素类型而不是词类型。 + +现在,让我们对文本词汇丰富度进行测量。下一个例子向我们展示,不同的单词数目只是单词总数的 6%,或者每个单词平均被使用了 16 次(记住,如果你使用的是 Python 2,请在开始输入`from __future__ import division`)。 + +```py +>>> len(set(text3)) / len(text3) +0.06230453042623537 +>>> +``` + +接下来,让我们专注于特定的词。我们可以计数一个词在文本中出现的次数,计算一个特定的词在文本中占据的百分比: + +```py +>>> text3.count("smote") +5 +>>> 100 * text4.count('a') / len(text4) +1.4643016433938312 +>>> +``` + +注 + +**轮到你来**:`text5`中`lol`出现了多少次?它占文本全部词数的百分比是多少? + +你也许想要对几个文本重复这些计算,但重新输入公式是乏味的。你可以自己命名一个任务,如`lexical_diversity`或`percentage`,然后用一个代码块关联它。现在,你只需输入一个很短的名字就可以代替一行或多行 Python 代码,而且你想用多少次就用多少次。执行一个任务的代码段叫做一个函数,我们使用关键字`def`给函数定义一个简短的名字。下面的例子演示如何定义两个新的函数,`lexical_diversity()`和`percentage()`: + +```py +>>> def lexical_diversity(text): ❶ +... return len(set(text)) / len(text) ❷ +... +>>> def percentage(count, total): ❸ +... return 100 * count / total +... +``` + +小心! + +当遇到第一行末尾的冒号后,Python 解释器提示符由`>>>`变为`...` 。`...`提示符表示 Python 期望在后面是一个缩进代码块 。缩进是输入四个空格还是敲击`Tab`键,这由你决定。要结束一个缩进代码段,只需输入一个空行。 + +`lexical_diversity()`❶的定义中,我们指定了一个`text`参数。这个参数是我们想要计算词汇多样性的实际文本的一个“占位符”,并在用到这个函数的时候出现在将要运行的代码块中❷。类似地,`percentage()`定义了两个参数,`count`和`total`❸。 + +只要 Python 知道了`lexical_diversity()`和`percentage()`是指定代码段的名字,我们就可以继续使用这些函数: + +```py +>>> lexical_diversity(text3) +0.06230453042623537 +>>> lexical_diversity(text5) +0.13477005109975562 +>>> percentage(4, 5) +80.0 +>>> percentage(text4.count('a'), len(text4)) +1.4643016433938312 +>>> +``` + +扼要重述一下,我们使用或调用一个如`lexical_diversity()`这样的函数,只要输入它的名字后面跟一个左括号,再输入文本名字,然后是右括号。这些括号经常出现,它们的作用是分割任务名—— 如`lexical_diversity()`,与任务将要处理的数据 ——如`text3`。调用函数时放在参数位置的数据值叫做函数的实参。 + +在本章中你已经遇到了几个函数,如`len()`, `set()`和`sorted()`。通常我们会在函数名后面加一对空括号,像`len()`中的那样,这只是为了表明这是一个函数而不是其他的 Python 表达式。函数是编程中的一个重要概念,我们在一开始提到它们,是为了让新同学体会编程的强大和富有创造力。如果你现在觉得有点混乱,请不要担心。 + +稍后我们将看到如何使用函数列表显示数据,像表 1.1 显示的那样。表的每一行将包含不同数据的相同的计算,我们用函数来做这种重复性的工作。 + +表 1.1: + +*Brown 语料库*中各种文体的词汇多样性 + +```py +>>> sent1 = ['Call', 'me', 'Ishmael', '.'] +>>> +``` + +在提示符后面,我们输入自己命名的`sent1`,后跟一个等号,然后是一些引用的词汇,中间用逗号分割并用括号包围。这个方括号内的东西在 Python 中叫做列表:它就是我们存储文本的方式。我们可以通过输入它的名字来查阅它❶。我们可以查询它的长度❷。我们甚至可以对它调用我们自己的函数`lexical_diversity()`❸。 + +```py +>>> sent1 ❶ +['Call', 'me', 'Ishmael', '.'] +>>> len(sent1) ❷ +4 +>>> lexical_diversity(sent1) ❸ +1.0 +>>> +``` + +还定义了其它几个列表,分别对应每个文本开始的句子,`sent2` … `sent9`。在这里我们检查其中的两个;你可以自己在 Python 解释器中尝试其余的(如果你得到一个错误说`sent2`没有定义,你需要先输入`from nltk.book import *`)。 + +```py +>>> sent2 +['The', 'family', 'of', 'Dashwood', 'had', 'long', +'been', 'settled', 'in', 'Sussex', '.'] +>>> sent3 +['In', 'the', 'beginning', 'God', 'created', 'the', +'heaven', 'and', 'the', 'earth', '.'] +>>> +``` + +注意 + +**轮到你来**:通过输入名字、等号和一个单词列表, 组建几个你自己的句子,如`ex1 = ['Monty', 'Python', 'and', 'the', 'Holy', 'Grail']`。重复一些我们先前在第 1 节看到的其他 Python 操作,如:`sorted(ex1)`, `len(set(ex1))`, `ex1.count('the')`。 + +令人惊喜的是,我们可以对列表使用 Python 加法运算。两个列表相加❶创造出一个新的列表,包括第一个列表的全部,后面跟着第二个列表的全部。 + +```py +>>> ['Monty', 'Python'] + ['and', 'the', 'Holy', 'Grail'] ❶ +['Monty', 'Python', 'and', 'the', 'Holy', 'Grail'] +>>> +``` + +注意 + +这种加法的特殊用法叫做连接;它将多个列表组合为一个列表。我们可以把句子连接起来组成一个文本。 + +不必逐字的输入列表,可以使用简短的名字来引用预先定义好的列表。 + +```py +>>> sent4 + sent1 +['Fellow', '-', 'Citizens', 'of', 'the', 'Senate', 'and', 'of', 'the', +'House', 'of', 'Representatives', ':', 'Call', 'me', 'Ishmael', '.'] +>>> +``` + +如果我们想要向链表中增加一个元素该如何?这种操作叫做追加。当我们对一个列表使用`append()`时,列表自身会随着操作而更新。 + +```py +>>> sent1.append("Some") +>>> sent1 +['Call', 'me', 'Ishmael', '.', 'Some'] +>>> +``` + +## 2.2 索引列表 + +正如我们已经看到的,Python 中的一个文本是一个单词的列表,用括号和引号的组合来表示。就像处理一页普通的文本,我们可以使用`len(text1)`计算`text1`的词数,使用`text1.count('heaven')`计算一个文本中出现的特定的词,如`'heaven'`。 + +稍微花些耐心,我们可以挑选出打印出来的文本中的第 1 个、第 173 个或第 14278 个词。类似的,我们也可以通过它在列表中出现的次序找出一个 Python 列表的元素。表示这个位置的数字叫做这个元素的索引。在文本名称后面的方括号里写下索引,Python 就会表示出文本中这个索引处如`173`的元素: + +```py +>>> text4[173] +'awaken' +>>> +``` + +我们也可以反过来做;找出一个词第一次出现的索引: + +```py +>>> text4.index('awaken') +173 +>>> +``` + +索引是一种常见的用来获取文本中词汇的方式,或者更一般的,访问列表中的元素的方式。Python 也允许我们获取子列表,从大文本中任意抽取语言片段,术语叫做切片。 + +```py +>>> text5[16715:16735] +['U86', 'thats', 'why', 'something', 'like', 'gamefly', 'is', 'so', 'good', +'because', 'you', 'can', 'actually', 'play', 'a', 'full', 'game', 'without', +'buying', 'it'] +>>> text6[1600:1625] +['We', "'", 're', 'an', 'anarcho', '-', 'syndicalist', 'commune', '.', 'We', +'take', 'it', 'in', 'turns', 'to', 'act', 'as', 'a', 'sort', 'of', 'executive', +'officer', 'for', 'the', 'week'] +>>> +``` + +索引有一些微妙,我们将在一个构造的句子的帮助下探讨这些: + +```py +>>> sent = ['word1', 'word2', 'word3', 'word4', 'word5', +... 'word6', 'word7', 'word8', 'word9', 'word10'] +>>> sent[0] +'word1' +>>> sent[9] +'word10' +>>> +``` + +请注意,索引从零开始:`sent`第 0 个元素写作`sent[0]`,是第一个单词`'word1'`,而`sent`的第 9 个元素是`'word10'`。原因很简单:Python 从计算机内存中的列表获取内容的时候,它已经位于第一个元素;我们要告诉它向前多少个元素。因此,向前 0 个元素使它留在第一个元素上。 + +注意 + +这种从零算起的做法刚开始接触会有些混乱,但这是现代编程语言普遍使用的。如果你已经掌握了 19XY 是 20 世纪中的一年这样的计数世纪的系统,或者如果你生活在一个建筑物楼层编号从 1 开始的国家,你很开就会掌握它的窍门,步行`n-1`级楼梯到第`n`层。 + +现在,如果我们不小心使用的索引过大就会得到一个错误: + +```py +>>> sent[10] +Traceback (most recent call last): + File "", line 1, in ? +IndexError: list index out of range +>>> +``` + +这次不是一个语法错误,因为程序片段在语法上是正确的。相反,它是一个运行时错误,它会产生一个`回溯`消息显示错误的上下文、错误的名称:`IndexError`以及简要的解释说明。 + +让我们再次使用构造的句子仔细看看切片。这里我们发现切片`5:8`包含`sent`中索引为 5,6 和 7 的元素: + +```py +>>> sent[5:8] +['word6', 'word7', 'word8'] +>>> sent[5] +'word6' +>>> sent[6] +'word7' +>>> sent[7] +'word8' +>>> +``` + +按照惯例,`m:n`表示元素`m…n-1`。正如下一个例子显示的那样,如果切片从列表第一个元素开始,我们可以省略第一个数字❶, 如果切片到列表最后一个元素处结尾,我们可以省略第二个数字❷: + +```py +>>> sent[:3] ❶ +['word1', 'word2', 'word3'] +>>> text2[141525:] ❷ +['among', 'the', 'merits', 'and', 'the', 'happiness', 'of', 'Elinor', 'and', 'Marianne', +',', 'let', 'it', 'not', 'be', 'ranked', 'as', 'the', 'least', 'considerable', ',', +'that', 'though', 'sisters', ',', 'and', 'living', 'almost', 'within', 'sight', 'of', +'each', 'other', ',', 'they', 'could', 'live', 'without', 'disagreement', 'between', +'themselves', ',', 'or', 'producing', 'coolness', 'between', 'their', 'husbands', '.', +'THE', 'END'] +>>> +``` + +我们可以通过赋值给它的索引值来修改列表中的元素。在接下来的例子中,我们把`sent[0]`放在等号左侧❶。我们也可以用新内容替换掉一整个片段❷。最后一个尝试报错的原因是这个链表只有四个元素而要获取其后面的元素就产生了错误❸。 + +```py +>>> sent[0] = 'First' ❶ +>>> sent[9] = 'Last' +>>> len(sent) +10 +>>> sent[1:9] = ['Second', 'Third'] ❷ +>>> sent +['First', 'Second', 'Third', 'Last'] +>>> sent[9] ❸ +Traceback (most recent call last): + File "", line 1, in ? +IndexError: list index out of range +>>> +``` + +注意 + +**轮到你来**:花几分钟定义你自己的句子,使用前文中的方法修改个别词和词组(切片)。尝试本章结尾关于列表的练习,检验你是否理解。 + +## 2.3 变量 + +从第 1 节一开始,你已经访问过名为`text1`, `text2`等的文本。像这样只输入简短的名字来引用一本 250,000 字的书节省了很多打字时间。一般情况下,我们可以对任何我们关心的计算命名。我们在前面的小节中已经这样做了,例如定义一个变量变量`sent1`,如下所示: + +```py +>>> sent1 = ['Call', 'me', 'Ishmael', '.'] +>>> +``` + +这样的语句形式是:`变量 = 表达式`。Python 将计算右边的表达式把结果保存在变量中。这个过程叫做赋值。它并不产生任何输出,你必须在新的一行输入变量的名字来检查它的内容。等号可能会有些误解,因为信息是从右边流到左边的。你把它想象成一个左箭头可能会有帮助。变量的名字可以是任何你喜欢的名字,如`my_sent`, `sentence`, `xyzzy`。变量必须以字母开头,可以包含数字和下划线。下面是变量和赋值的一些例子: + +```py +>>> my_sent = ['Bravely', 'bold', 'Sir', 'Robin', ',', 'rode', +... 'forth', 'from', 'Camelot', '.'] +>>> noun_phrase = my_sent[1:4] +>>> noun_phrase +['bold', 'Sir', 'Robin'] +>>> wOrDs = sorted(noun_phrase) +>>> wOrDs +['Robin', 'Sir', 'bold'] +>>> +``` + +请记住,排序表中大写字母出现在小写字母之前。 + +注意 + +请注意,在前面的例子中,我们将`my_sent`的定义分成两行。Python 表达式可以被分割成多行,只要它出现在任何一种括号内。Python 使用`...`提示符表示期望更多的输入。在这些连续的行中有多少缩进都没有关系,只是加入缩进通常会便于阅读。 + +最好是选择有意义的变量名,它能提醒你代码的含义,也帮助别人读懂你的 Python 代码。Python 并不会理解这些名称的意义;它只是盲目的服从你的指令,如果你输入一些令人困惑的代码,例如`one = 'two'`或`two = 3`,它也不会反对。唯一的限制是变量名不能是 Python 的保留字,如`def`, `if`, `not`, 和`import`。如果你使用了保留字,Python 会产生一个语法错误: + +```py +>>> not = 'Camelot' +File "", line 1 + not = 'Camelot' + ^ +SyntaxError: invalid syntax +>>> +``` + +我们将经常使用变量来保存计算的中间步骤,尤其是当这样做使代码更容易读懂时。因此,`len(set(text1))`也可以写作: + +```py +>>> vocab = set(text1) +>>> vocab_size = len(vocab) +>>> vocab_size +19317 +>>> +``` + +小心! + +为 Python 变量选择名称(标识符)时请注意。首先,应该以字母开始,后面跟数字(`0`到`9`)或字母。因此,`abc23`是好的,但是`23abc`会导致一个语法错误。名称是大小写敏感的,这意味着`myVar`和`myvar`是不同的变量。变量名不能包含空格,但可以用下划线把单词分开,例如`my_var`。注意不要插入连字符来代替下划线:`my-var`不对,因为 Python 会把`-`解释为减号。 + +## 2.4 字符串 + +我们用来访问列表元素的一些方法也可以用在单独的词或字符串上。例如可以把一个字符串指定给一个变量❶,索引一个字符串❷,切片一个字符串❸: + +```py +>>> name = 'Monty' ❶ +>>> name[0] ❷ +'M' +>>> name[:4] ❸ +'Mont' +>>> +``` + +我们还可以对字符串执行乘法和加法: + +```py +>>> name * 2 +'MontyMonty' +>>> name + '!' +'Monty!' +>>> +``` + +我们可以把列表中的单词连接起来组成单个字符串,或者把字符串分割成一个列表,如下面所示: + +```py +>>> ' '.join(['Monty', 'Python']) +'Monty Python' +>>> 'Monty Python'.split() +['Monty', 'Python'] +>>> +``` + +我们将在第 3 章回到字符串的主题。目前,我们已经有了两个重要的基石——列表和字符串——已经准备好可以重新做一些语言分析了。 + +## 3 计算语言:简单的统计 + +让我们重新开始探索用我们的计算资源处理大量文本的方法。我们在第 1 节已经开始讨论了,在那里我们看到如何搜索词及其上下文,如何汇编一个文本中的词汇,如何产生一种文体的随机文本等。 + +在本节中,我们重新拾起是什么让一个文本不同与其他文本这样的问题,并使用程序自动寻找特征词汇和文字表达。正如在第 1 节中那样,你可以通过复制它们到 Python 解释器中来尝试 Python 语言的新特征,你将在下一节中系统的了解这些功能。 + +在这之前,你可能会想通过预测下面的代码的输出来检查你对上一节的理解。你可以使用解释器来检查你是否正确。如果你不确定如何做这个任务,你最好在继续之前复习一下上一节的内容。 + +```py +>>> saying = ['After', 'all', 'is', 'said', 'and', 'done', +... 'more', 'is', 'said', 'than', 'done'] +>>> tokens = set(saying) +>>> tokens = sorted(tokens) +>>> tokens[-2:] +what output do you expect here? +>>> +``` + +## 3.1 频率分布 + +我们如何能自动识别文本中最能体现文本的主题和风格的词汇?试想一下,要找到一本书中使用最频繁的 50 个词你会怎么做?一种方法是为每个词项设置一个计数器,如图 3.1 显示的那样。计数器可能需要几千行,这将是一个极其繁琐的过程——如此繁琐以至于我们宁愿把任务交给机器来做。 + +![Images/tally.png](Images/de0715649664a49a5ab2e2b61ae2675a.jpg) + +图 3.1:计数一个文本中出现的词(频率分布) + +图 3.1 中的表被称为频率分布,它告诉我们在文本中的每一个词项的频率。(一般情况下,它能计数任何观察得到的事件。)这是一个“分布”因为它告诉我们文本中单词词符的总数是如何分布在词项中的。因为我们经常需要在语言处理中使用频率分布,NLTK 中内置了它们。让我们使用`FreqDist`寻找《白鲸记》中最常见的 50 个词: + +```py +>>> fdist1 = FreqDist(text1) ❶ +>>> print(fdist1) ❷ + +>>> fdist1.most_common(50) ❸ +[(',', 18713), ('the', 13721), ('.', 6862), ('of', 6536), ('and', 6024), +('a', 4569), ('to', 4542), (';', 4072), ('in', 3916), ('that', 2982), +("'", 2684), ('-', 2552), ('his', 2459), ('it', 2209), ('I', 2124), +('s', 1739), ('is', 1695), ('he', 1661), ('with', 1659), ('was', 1632), +('as', 1620), ('"', 1478), ('all', 1462), ('for', 1414), ('this', 1280), +('!', 1269), ('at', 1231), ('by', 1137), ('but', 1113), ('not', 1103), +('--', 1070), ('him', 1058), ('from', 1052), ('be', 1030), ('on', 1005), +('so', 918), ('whale', 906), ('one', 889), ('you', 841), ('had', 767), +('have', 760), ('there', 715), ('But', 705), ('or', 697), ('were', 680), +('now', 646), ('which', 640), ('?', 637), ('me', 627), ('like', 624)] +>>> fdist1['whale'] +906 +>>> +``` + +第一次调用`FreqDist`时,传递文本的名称作为参数❶。我们可以看到已经被计算出来的《白鲸记》中的总的词数(`outcomes`)—— 260,819❷。表达式`most_common(50)`给出文本中 50 个出现频率最高的单词类型❸。 + +注意 + +**轮到你来**:使用`text2`尝试前面的频率分布的例子。注意正确使用括号和大写字母。如果你得到一个错误消息`NameError: name 'FreqDist' is not defined`,你需要在一开始输入`from nltk.book import *` + +上一个例子中是否有什么词有助于我们把握这个文本的主题或风格呢?只有一个词,`whale`,稍微有些信息量!它出现了超过 900 次。其余的词没有告诉我们关于文本的信息;它们只是“管道”英语。这些词在文本中占多少比例?我们可以产生一个这些词汇的累积频率图,使用`fdist1.plot(50, cumulative=True)`来生成 3.2 中的图。这 50 个词占了书的将近一半! + +![Images/fdist-moby.png](Images/513df73dfd52feca2c96a86dcc261c8b.jpg) + +图 3.2:《白鲸记》中 50 个最常用词的累积频率图:这些词占了所有词符的将近一半。 + +如果高频词对我们没有帮助,那些只出现了一次的词(所谓的 hapaxes)又如何呢?输入`fdist1.hapaxes()`来查看它们。这个列表包含`lexicographer`, `cetological`, `contraband`, `expostulations`以及其他 9,000 多个。看来低频词太多了,没看到上下文我们很可能有一半的 hapaxes 猜不出它们的意义!既然高频词和低频词都没有帮助,我们需要尝试其他的办法。 + +## 3.2 细粒度的选择词 + +接下来,让我们看看文本中的*长*词,也许它们有更多的特征和信息量。为此我们采用集合论的一些符号。我们想要找出文本词汇表长度中超过 15 个字符的词。我们定义这个性质为`P`,则`P(w)`为真当且仅当词`w`的长度大余 15 个字符。现在我们可以用`(1a)` 中的数学集合符号表示我们感兴趣的词汇。它的含义是:此集合中所有`w`都满足`w`是集合`V`(词汇表)的一个元素且`w`有性质`P`。 + +```py +>>> V = set(text1) +>>> long_words = [w for w in V if len(w) > 15] +>>> sorted(long_words) +['CIRCUMNAVIGATION', 'Physiognomically', 'apprehensiveness', 'cannibalistically', +'characteristically', 'circumnavigating', 'circumnavigation', 'circumnavigations', +'comprehensiveness', 'hermaphroditical', 'indiscriminately', 'indispensableness', +'irresistibleness', 'physiognomically', 'preternaturalness', 'responsibilities', +'simultaneousness', 'subterraneousness', 'supernaturalness', 'superstitiousness', +'uncomfortableness', 'uncompromisedness', 'undiscriminating', 'uninterpenetratingly'] +>>> +``` + +对于词汇表`V`中的每一个词`w`,我们检查`len(w)`是否大于 15;所有其他词汇将被忽略。我们将在后面更仔细的讨论这里的语法。 + +注意 + +**轮到你来**:在 Python 解释器中尝试上面的表达式,改变文本和长度条件做一些实验。如果改变变量名,你的结果会产生什么变化吗,如使用`[word for word in vocab if ...]`? + +让我们回到寻找文本特征词汇的任务上来。请注意,`text4`中的长词反映国家主题 — `constitutionally`, `transcontinental` — 而`text5`中的长词反映的不是真正的内容`boooooooooooglyyyyyy`和`yuuuuuuuuuuuummmmmmmmmmmm`。我们是否已经成功的自动提取出文本的特征词汇呢?好的,这些很长的词通常是唯一词,也许找出*频繁出现的*长词会更好。这样看起来更有前途,因为这样忽略了短高频词(如`the`)和长低频词(如`antiphilosophists`)。以下是聊天语料库中所有长度超过 7 个字符,且出现次数超过 7 次的词: + +```py +>>> fdist5 = FreqDist(text5) +>>> sorted(w for w in set(text5) if len(w) > 7 and fdist5[w] > 7) +['#14-19teens', '#talkcity_adults', '((((((((((', '........', 'Question', +'actually', 'anything', 'computer', 'cute.-ass', 'everyone', 'football', +'innocent', 'listening', 'remember', 'seriously', 'something', 'together', +'tomorrow', 'watching'] +>>> +``` + +注意我们是如何使用两个条件:`len(w) > 7`保证词长都超过七个字母,`fdist5[w] > 7`保证这些词出现超过 7 次。最后,我们已成功地自动识别出与文本内容相关的高频词。这很小的一步却是一个重要的里程碑:一小块代码,处理数以万计的词,产生一些有信息量的输出。 + +## 3.3 词语搭配和双连词 + +一个搭配是异乎寻常地经常在一起出现的词序列。`red wine`是一个搭配,而`the wine`不是。搭配的一个特点是其中的词不能被类似的词置换。例如:`maroon wine`(粟色酒)听起来就很奇怪。 + +要获取搭配,我们先从提取文本词汇中的词对,也就是双连词开始。使用函数`bigrams()`很容易实现: + +```py +>>> list(bigrams(['more', 'is', 'said', 'than', 'done'])) +[('more', 'is'), ('is', 'said'), ('said', 'than'), ('than', 'done')] +>>> +``` + +注意 + +如果上面省掉`list()`,只输入`bigrams(['more', ...])`,你将看到``的输出形式。这是 Python 的方式表示它已经准备好要计算一个序列,在这里是双连词。现在,你只需要知道告诉 Python 使用`list()`将它转换成一个列表。 + +在这里我们看到词对`than-done`是一个双连词,在 Python 中写成`('than', 'done')`。现在,搭配基本上就是频繁的双连词,除非我们更加注重包含不常见词的的情况。特别的,我们希望找到比我们基于单个词的频率预期得到的更频繁出现的双连词。`collocations()`函数为我们做这些。我们将在以后看到它是如何工作。 + +```py +>>> text4.collocations() +United States; fellow citizens; four years; years ago; Federal +Government; General Government; American people; Vice President; Old +World; Almighty God; Fellow citizens; Chief Magistrate; Chief Justice; +God bless; every citizen; Indian tribes; public debt; one another; +foreign nations; political parties +>>> text8.collocations() +would like; medium build; social drinker; quiet nights; non smoker; +long term; age open; Would like; easy going; financially secure; fun +times; similar interests; Age open; weekends away; poss rship; well +presented; never married; single mum; permanent relationship; slim +build +>>> +``` + +文本中出现的搭配很能体现文本的风格。为了找到`red wine`这个搭配,我们将需要处理更大的文本。 + +## 3.4 计数其他东西 + +计数词汇是有用的,我们也可以计数其他东西。例如,我们可以查看文本中词长的分布,通过创造一长串数字的列表的`FreqDist`,其中每个数字是文本中对应词的长度: + +```py +>>> [len(w) for w in text1] ❶ +[1, 4, 4, 2, 6, 8, 4, 1, 9, 1, 1, 8, 2, 1, 4, 11, 5, 2, 1, 7, 6, 1, 3, 4, 5, 2, ...] +>>> fdist = FreqDist(len(w) for w in text1) ❷ +>>> print(fdist) ❸ + +>>> fdist +FreqDist({3: 50223, 1: 47933, 4: 42345, 2: 38513, 5: 26597, 6: 17111, 7: 14399, + 8: 9966, 9: 6428, 10: 3528, ...}) +>>> +``` + +我们以导出`text1`中每个词的长度的列表开始❶,然后`FreqDist`计数列表中每个数字出现的次数❷。结果❸是一个包含 25 万左右个元素的分布,每一个元素是一个数字,对应文本中一个词标识符。但是只有 20 个不同的元素,从 1 到 20,因为只有 20 个不同的词长。也就是说,有由 1 个字符,2 个字符,...,20 个字符组成的词,而没有由 21 个或更多字符组成的词。有人可能会问不同长度的词的频率是多少?(例如,文本中有多少长度为 4 的词?长度为 5 的词是否比长度为 4 的词多?等等)。下面我们回答这个问题: + +```py +>>> fdist.most_common() +[(3, 50223), (1, 47933), (4, 42345), (2, 38513), (5, 26597), (6, 17111), (7, 14399), +(8, 9966), (9, 6428), (10, 3528), (11, 1873), (12, 1053), (13, 567), (14, 177), +(15, 70), (16, 22), (17, 12), (18, 1), (20, 1)] +>>> fdist.max() +3 +>>> fdist[3] +50223 +>>> fdist.freq(3) +0.19255882431878046 +>>> +``` + +由此我们看到,最频繁的词长度是 3,长度为 3 的词有 50,000 多个(约占书中全部词汇的 20%)。虽然我们不会在这里追究它,关于词长的进一步分析可能帮助我们了解作者、文体或语言之间的差异。 + +3.1 总结了 NLTK 频率分布类中定义的函数。 + +表 3.1: + +NLTK 频率分布类中定义的函数 + +```py +>>> sent7 +['Pierre', 'Vinken', ',', '61', 'years', 'old', ',', 'will', 'join', 'the', +'board', 'as', 'a', 'nonexecutive', 'director', 'Nov.', '29', '.'] +>>> [w for w in sent7 if len(w) < 4] +[',', '61', 'old', ',', 'the', 'as', 'a', '29', '.'] +>>> [w for w in sent7 if len(w) <= 4] +[',', '61', 'old', ',', 'will', 'join', 'the', 'as', 'a', 'Nov.', '29', '.'] +>>> [w for w in sent7 if len(w) == 4] +['will', 'join', 'Nov.'] +>>> [w for w in sent7 if len(w) != 4] +['Pierre', 'Vinken', ',', '61', 'years', 'old', ',', 'the', 'board', +'as', 'a', 'nonexecutive', 'director', '29', '.'] +>>> +``` + +所有这些例子都有一个共同的模式:`[w for w in text if *condition* ]`,其中`*condition*`是 Python 中的一个“测试”,得到真或者假。在前面的代码例子所示的情况中,条件始终是数值比较。然而,我们也可以使用表 4.2 中列出的函数测试词汇的各种属性。 + +表 4.2: + +一些词比较运算符 + +```py +>>> sorted(w for w in set(text1) if w.endswith('ableness')) +['comfortableness', 'honourableness', 'immutableness', 'indispensableness', ...] +>>> sorted(term for term in set(text4) if 'gnt' in term) +['Sovereignty', 'sovereignties', 'sovereignty'] +>>> sorted(item for item in set(text6) if item.istitle()) +['A', 'Aaaaaaaaah', 'Aaaaaaaah', 'Aaaaaah', 'Aaaah', 'Aaaaugh', 'Aaagh', ...] +>>> sorted(item for item in set(sent7) if item.isdigit()) +['29', '61'] +>>> +``` + +我们还可以创建更复杂的条件。如果`c`是一个条件,那么`not c`也是一个条件。如果我们有两个条件`c[1]`和`c[2]`,那么我们可以使用合取和析取将它们合并形成一个新的条件:`c[1] and c[2]`, `c[1] or c[2]`。 + +注意 + +**轮到你来**:运行下面的例子,尝试解释每一条指令中所发生的事情。然后,试着自己组合一些条件。 + +```py +>>> sorted(w for w in set(text7) if '-' in w and 'index' in w) +>>> sorted(wd for wd in set(text3) if wd.istitle() and len(wd) > 10) +>>> sorted(w for w in set(sent7) if not w.islower()) +>>> sorted(t for t in set(text2) if 'cie' in t or 'cei' in t) +``` + +## 4.2 对每个元素进行操作 + +在第 3 节中,我们看到计数词汇以外的其他项目的一些例子。让我们仔细看看我们所使用的符号: + +```py +>>> [len(w) for w in text1] +[1, 4, 4, 2, 6, 8, 4, 1, 9, 1, 1, 8, 2, 1, 4, 11, 5, 2, 1, 7, 6, 1, 3, 4, 5, 2, ...] +>>> [w.upper() for w in text1] +['[', 'MOBY', 'DICK', 'BY', 'HERMAN', 'MELVILLE', '1851', ']', 'ETYMOLOGY', '.', ...] +>>> +``` + +这些表达式形式为`[f(w) for ...]`或`[w.f() for ...]`,其中`f`是一个函数,用来计算词长,或把字母转换为大写。现阶段你还不需要理解两种表示方法:`f(w)`和`w.f()`。而只需学习对列表上的所有元素执行相同的操作的这种 Python 习惯用法。在前面的例子中,遍历`text1`中的每一个词,一个接一个的赋值给变量`w`并在变量上执行指定的操作。 + +注意 + +上面描述的表示法被称为“列表推导”。这是我们的第一个 Python 习惯用法的例子,一中固定的表示法,我们习惯使用的方法,省去了每次分析的烦恼。掌握这些习惯用法是成为一流 Python 程序员的一个重要组成部分。 + +让我们回到计数词汇的问题,这里使用相同的习惯用法: + +```py +>>> len(text1) +260819 +>>> len(set(text1)) +19317 +>>> len(set(word.lower() for word in text1)) +17231 +>>> +``` + +由于我们不重复计算像`This`和`this`这样仅仅大小写不同的词,就已经从词汇表计数中抹去了 2,000 个!还可以更进一步,通过过滤掉所有非字母元素,从词汇表中消除数字和标点符号: + +```py +>>> len(set(word.lower() for word in text1 if word.isalpha())) +16948 +>>> +``` + +这个例子稍微有些复杂:将所有纯字母组成的词小写。也许只计数小写的词会更简单一些,但这却是一个错误的答案(为什么?)。 + +如果你对列表推导不那么充满信心,请不要担心,因为在下面的章节中你会看到更多的例子及解释。 + +## 4.3 嵌套代码块 + +大多数编程语言允许我们在条件表达式或者`if`语句条件满足时执行代码块。我们在`[w for w in sent7 if len(w) > 4]`这样的代码中已经看到条件测试的例子。在下面的程序中,我们创建一个叫`word`的变量包含字符串值`'cat'`。`if`语句中检查`len(word) > 5`是否为真。它确实为真,所以`if`语句的代码块被调用,`print`语句被执行,向用户显示一条消息。别忘了要缩进,在`print`语句前输入四个空格。 + +```py +>>> word = 'cat' +>>> if len(word) < 5: +... print('word length is less than 5') +... ❶ +word length is less than 5 +>>> +``` + +使用 Python 解释器时,我们必须添加一个额外的空白行❶,这样它才能检测到嵌套块结束。 + +注意 + +如果你正在使用 Python 2.6 或 2.7,为了识别上面的`print`函数,需要包括以下行: + +```py +>>> from __future__ import print_function +``` + +如果我们改变测试条件为`len(word) >= 5`来检查`word`的长度是否大于等于`5`,那么测试将不再为真。此时,`if`语句后面的代码段将不会被执行,没有消息显示给用户: + +```py +>>> if len(word) >= 5: +... print('word length is greater than or equal to 5') +... +>>> +``` + +`if`语句被称为一种控制结构,因为它控制缩进块中的代码将是否运行。另一种控制结构是`for`循环。尝试下面的代码,请记住包含冒号和四个空格: + +```py +>>> for word in ['Call', 'me', 'Ishmael', '.']: +... print(word) +... +Call +me +Ishmael +. +>>> +``` + +这叫做循环,因为 Python 以循环的方式执行里面的代码。它以`word = 'Call'`赋值开始,使用变量`word`命名列表的第一个元素。然后,显示`word`的值给用户。接下来它回到`for`语句,执行`word = 'me'`赋值,然后显示这个新值给用户,以此类推。它以这种方式不断运行,直到列表中所有项都被处理完。 + +## 4.4 条件循环 + +现在,我们可以将`if`语句和`for`语句结合。循环链表中每一项,只输出结尾字母是`l`的词。我们将为变量挑选另一个名字以表明 Python 并不在意变量名的意义。 + +```py +>>> sent1 = ['Call', 'me', 'Ishmael', '.'] +>>> for xyzzy in sent1: +... if xyzzy.endswith('l'): +... print(xyzzy) +... +Call +Ishmael +>>> +``` + +你会发现在`if`和`for`语句所在行末尾——缩进开始之前——有一个冒号。事实上,所有的 Python 控制结构都以冒号结尾。冒号表示当前语句与后面的缩进块有关联。 + +我们也可以指定当`if`语句的条件不满足时采取的行动。在这里,我们看到`elif`(`else if`)语句和`else`语句。请注意,这些在缩进代码前也有冒号。 + +```py +>>> for token in sent1: +... if token.islower(): +... print(token, 'is a lowercase word') +... elif token.istitle(): +... print(token, 'is a titlecase word') +... else: +... print(token, 'is punctuation') +... +Call is a titlecase word +me is a lowercase word +Ishmael is a titlecase word +. is punctuation +>>> +``` + +正如你看到的,即便只有这么一点儿 Python 知识,你就已经可以开始构建多行的 Python 程序。分块开发程序,在整合它们之前测试每一块代码是否达到你的预期是很重要的。这也是 Python 交互式解释器的价值所在,也是为什么你必须适应它的原因。 + +最后,让我们把一直在探索的习惯用法组合起来。首先,我们创建一个包含`cie`或`cei`的词的列表,然后循环输出其中的每一项。请注意`print`语句中给出的额外信息:`end=' '`。它告诉 Python 在每个单词后面打印一个空格(而不是默认的换行)。 + +```py +>>> tricky = sorted(w for w in set(text2) if 'cie' in w or 'cei' in w) +>>> for word in tricky: +... print(word, end=' ') +ancient ceiling conceit conceited conceive conscience +conscientious conscientiously deceitful deceive ... +>>> +``` + +## 5 自动理解自然语言 + +我们一直在各种文本和 Python 编程语言的帮助自下而上的探索语言。然而,我们也对通过构建有用的语言技术,开拓我们的语言和计算知识感兴趣。现在,我们将借此机会从代码的细节中退出来,描绘一下自然语言处理的全景图。 + +纯粹应用层面,我们大家都需要帮助才能找到隐含在网络上的文本中的浩瀚的信息。搜索引擎在网络的发展和普及中发挥了关键作用,但也有一些缺点。它需要技能、知识和一点运气才能找到这样一些问题的答案:我用有限的预算能参观费城和匹兹堡的哪些景点?专家们怎么评论数码单反相机?过去的一周里可信的评论员都对钢材市场做了哪些预测?让计算机来自动回答这些问题,涉及包括信息提取、推理与总结在内的广泛的语言处理任务,将需要在一个更大规模更稳健的层面实施,这超出了我们当前的能力。 + +哲学层面,构建智能机器是人工智能长久以来的挑战,语言理解是智能行为的重要组成部分。这一目标多年来一直被看作是太困难了。然而,随着 NLP 技术日趋成熟,分析非结构化文本的方法越来越健壮,应用越来越广泛,对自然语言理解的期望变成一个合理的目标再次浮现。 + +在本节中,我们将描述一些语言理解技术,给你一种有趣的挑战正在等着你的感觉。 + +## 5.1 词意消歧 + +在词意消歧中,我们要算出特定上下文中的词被赋予的是哪个意思。思考存在歧义的词`serve`和`dish`: + +```py +>>> sorted(set(w.lower() for w in text1)) +>>> sorted(w.lower() for w in set(text1)) +``` + +* ◑ 下面两个测试的差异是什么:`w.isupper()`和`not w.islower()`? + + * ◑ 写一个切片表达式提取`text2`中最后两个词。 + + * ◑ 找出聊天语料库(`text5`)中所有四个字母的词。使用频率分布函数(`FreqDist`),以频率从高到低显示这些词。 + + * ◑ 复习第 4 节中条件循环的讨论。使用`for`和`if`语句组合循环遍历《巨蟒和圣杯》(`text6`)的电影剧本中的词,`print`所有的大写词,每行输出一个。 + + * ◑ 写表达式找出`text6`中所有符合下列条件的词。结果应该是单词列表的形式:`['word1', 'word2', ...]`。 + + 1. 以`ize`结尾 + 2. 包含字母`z` + 3. 包含字母序列`pt` + 4. 除了首字母外是全部小写字母的词(即`titlecase`) + + * ◑ 定义`sent`为一个单词列表:`['she', 'sells', 'sea', 'shells', 'by', 'the', 'sea', 'shore']`。编写代码执行以下任务: + + 1. 输出所有`sh`开头的单词 + 2. 输出所有长度超过 4 个字符的词 + + * ◑ 下面的 Python 代码是做什么的?`sum(len(w) for w in text1)`你可以用它来算出一个文本的平均字长吗? + + * ◑ 定义一个名为`vocab_size(text)`的函数,以文本作为唯一的参数,返回文本的词汇量。 + + * ◑ 定义一个函数`percent(word, text)`,计算一个给定的词在文本中出现的频率,结果以百分比表示。 + + * ◑ 我们一直在使用集合存储词汇表。试试下面的 Python 表达式:`set(sent3) > set(text1)`。实验在`set()`中使用不同的参数。它是做什么用的?你能想到一个实际的应用吗? + +## 关于本文档... + +针对 NLTK 3.0 进行更新。本章来自于《Python 自然语言处理》,[Steven Bird](http://estive.net/), [Ewan Klein](http://homepages.inf.ed.ac.uk/ewan/) 和 [Edward Loper](http://ed.loper.org/),Copyright © 2014 作者所有。本章依据 [*Creative Commons Attribution-Noncommercial-No Derivative Works 3\.0 United States License*](http://creativecommons.org/licenses/by-nc-nd/3.0/us/) 条款,与[*自然语言工具包*](http://nltk.org/) 3.0 版一起发行。 + +本文档构建于星期三 2015 年 7 月 1 日 12:30:05 AEST + + + + + diff --git a/docs/nlp-py-2e/10.md b/docs/nlp-py-2e/10.md new file mode 100644 index 0000000000000000000000000000000000000000..273cb83417f4ca3b02084bfc5cdc2063f6214eeb --- /dev/null +++ b/docs/nlp-py-2e/10.md @@ -0,0 +1,999 @@ +# 10 分析句子的意思 + +我们已经看到利用计算机的能力来处理大规模文本是多么有用。现在我们已经有了分析器和基于特征的语法,我们能否做一些类似分析句子的意思这样有用的事情?本章的目的是要回答下列问题: + +1. 我们如何能表示自然语言的意思,使计算机能够处理这些表示? +2. 我们怎样才能将意思表示与无限的句子集合关联? +3. 我们怎样才能使用程序来连接句子的意思表示到知识的存储? + +一路上,我们将学习一些逻辑语义领域的形式化技术,看看如何用它们来查询存储了世间真知的数据库。 + +## 1 自然语言理解 + +## 1.1 查询数据库 + +假设有一个程序,让我们输入一个自然语言问题,返回给我们正确的答案: + +```py +>>> nltk.data.show_cfg('grammars/book_grammars/sql0.fcfg') +% start S +S[SEM=(?np + WHERE + ?vp)] -> NP[SEM=?np] VP[SEM=?vp] +VP[SEM=(?v + ?pp)] -> IV[SEM=?v] PP[SEM=?pp] +VP[SEM=(?v + ?ap)] -> IV[SEM=?v] AP[SEM=?ap] +NP[SEM=(?det + ?n)] -> Det[SEM=?det] N[SEM=?n] +PP[SEM=(?p + ?np)] -> P[SEM=?p] NP[SEM=?np] +AP[SEM=?pp] -> A[SEM=?a] PP[SEM=?pp] +NP[SEM='Country="greece"'] -> 'Greece' +NP[SEM='Country="china"'] -> 'China' +Det[SEM='SELECT'] -> 'Which' | 'What' +N[SEM='City FROM city_table'] -> 'cities' +IV[SEM=''] -> 'are' +A[SEM=''] -> 'located' +P[SEM=''] -> 'in' +``` + +这使我们能够分析 SQL 查询: + +```py +>>> from nltk import load_parser +>>> cp = load_parser('grammars/book_grammars/sql0.fcfg') +>>> query = 'What cities are located in China' +>>> trees = list(cp.parse(query.split())) +>>> answer = trees[0].label()['SEM'] +>>> answer = [s for s in answer if s] +>>> q = ' '.join(answer) +>>> print(q) +SELECT City FROM city_table WHERE Country="china" +``` + +注意 + +**轮到你来**:设置跟踪为最大,运行分析器,即`cp = load_parser('grammars/book_grammars/sql0.fcfg', trace=3)`,研究当边被完整的加入到图表中时,如何建立`sem`的值。 + +最后,我们在数据库`city.db`上执行查询,检索出一些结果: + +```py +>>> from nltk.sem import chat80 +>>> rows = chat80.sql_query('corpora/city_database/city.db', q) +>>> for r in rows: print(r[0], end=" ") ❶ +canton chungking dairen harbin kowloon mukden peking shanghai sian tientsin +``` + +由于每行`r`是一个单元素的元组,我们输出元组的成员,而不是元组本身❶。 + +总结一下,我们已经定义了一个任务:计算机对自然语言查询做出反应,返回有用的数据。我们通过将英语的一个小的子集翻译成 SQL 来实现这个任务们可以说,我们的 NLTK 代码已经“理解”SQL,只要 Python 能够对数据库执行 SQL 查询,通过扩展,它也“理解”如`What cities are located in China`这样的查询。这相当于自然语言理解的例子能够从荷兰语翻译成英语。假设你是一个英语为母语的人,已经开始学习荷兰语。你的老师问你是否理解`(3)`的意思: + +```py +>>> nltk.boolean_ops() +negation - +conjunction & +disjunction | +implication -> +equivalence <-> +``` + +从命题符号和布尔运算符,我们可以建立命题逻辑的规范公式(或简称公式)的无限集合。首先,每个命题字母是一个公式。然后,如果φ是一个公式,那么`-`φ也是一个公式。如果φ和ψ是公式,那么`(`φ `&` ψ`)` `(`φ `|` ψ`)` `(`φ `->` ψ`)` `(`φ `<->` ψ`)`也是公式。 + +2.1 指定了包含这些运算符的公式为真的条件。和以前一样,我们使用φ和ψ作为句子中的变量,`iff`作为`if and only if`(当且仅当)的缩写。 + +表 2.1: + +命题逻辑的布尔运算符的真值条件。 + +```py +>>> read_expr = nltk.sem.Expression.fromstring +>>> read_expr('-(P & Q)') + +>>> read_expr('P & Q') + +>>> read_expr('P | (R -> Q)') + Q))> +>>> read_expr('P <-> -- P') + --P)> +``` + +从计算的角度来看,逻辑给了我们进行推理的一个重要工具。假设你表达`Freedonia is not to the north of Sylvania`,而你给出理由`Sylvania is to the north of Freedonia`。在这种情况下,你已经给出了一个论证。句子`Sylvania is to the north of Freedonia`是论证的假设,而`Freedonia is not to the north of Sylvania`是结论。从假设一步一步推到结论,被称为推理。通俗地说,就是我们以在结论前面写`therefore`这样的格式写一个论证。 + +```py +>>> lp = nltk.sem.Expression.fromstring +>>> SnF = read_expr('SnF') +>>> NotFnS = read_expr('-FnS') +>>> R = read_expr('SnF -> -FnS') +>>> prover = nltk.Prover9() +>>> prover.prove(NotFnS, [SnF, R]) +True +``` + +这里有另一种方式可以看到结论如何得出。`SnF -> -FnS`在语义上等价于`-SnF | -FnS`,其中`|`是对应于`or`的二元运算符。在一般情况下,`φ|ψ`在条件`s`中为真,要么`φ`在`s`中为真,要么`ψ`在`s`中为真。现在,假设`SnF`和`-SnF | -FnS`都在`s`中为真。如果`SnF`为真,那么`-SnF`不可能也为真;经典逻辑的一个基本假设是:一个句子在一种情况下不能同时为真和为假。因此,`-FnS`必须为真。 + +回想一下,我们解释相对于一个模型的一种逻辑语言的句子,它们是这个世界的一个非常简化的版本。一个命题逻辑的模型需要为每个可能的公式分配值`True`或`False`。我们一步步的来做这个:首先,为每个命题符号分配一个值,然后确定布尔运算符的含义(即 2.1)和运用它们到这些公式的组件的值,来计算复杂的公式的值。`估值`是从逻辑的基本符号映射到它们的值。下面是一个例子: + +```py +>>> val = nltk.Valuation([('P', True), ('Q', True), ('R', False)]) +``` + +我们使用一个配对的链表初始化一个`估值`,每个配对由一个语义符号和一个语义值组成。所产生的对象基本上只是一个字典,映射逻辑符号(作为字符串处理)为适当的值。 + +```py +>>> val['P'] +True +``` + +正如我们稍后将看到的,我们的模型需要稍微更加复杂些,以便处理将在下一节中讨论的更复杂的逻辑形式;暂时的,在下面的声明中先忽略参数`dom`和`g`。 + +```py +>>> dom = set() +>>> g = nltk.Assignment(dom) +``` + +现在,让我们用`val`初始化模型`m`: + +```py +>>> m = nltk.Model(dom, val) +``` + +每一个模型都有一个`evaluate()`方法,可以确定逻辑表达式,如命题逻辑的公式,的语义值;当然,这些值取决于最初我们分配给命题符号如`P`,`Q`和`R`的真值。 + +```py +>>> print(m.evaluate('(P & Q)', g)) +True +>>> print(m.evaluate('-(P & Q)', g)) +False +>>> print(m.evaluate('(P & R)', g)) +False +>>> print(m.evaluate('(P | R)', g)) +True +``` + +注意 + +**轮到你来**:做实验为不同的命题逻辑公式估值。模型是否给出你所期望的值? + +到目前为止,我们已经将我们的英文句子翻译成命题逻辑。因为我们只限于用字母如`P`和`Q`表示原子句子,不能深入其内部结构。实际上,我们说将原子句子分成主语、宾语和谓词并没有语义上的好处。然而,这似乎是错误的:如果我们想形式化如`(9)`这样的论证,就必须要能“看到里面”基本的句子。因此,我们将超越命题逻辑到一个更有表现力的东西,也就是一阶逻辑。这正是我们下一节要讲的。 + +## 3 一阶逻辑 + +本章的剩余部分,我们将通过翻译自然语言表达式为一阶逻辑来表示它们的意思。并不是所有的自然语言语义都可以用一阶逻辑表示。但它是计算语义的一个不错的选择,因为它具有足够的表现力来表达语义的很多方面,而且另一方面,有出色的现成系统可用于开展一阶逻辑自动推理。 + +下一步我们将描述如何构造一阶逻辑公式,然后是这样的公式如何用来评估模型。 + +## 3.1 句法 + +一阶逻辑保留所有命题逻辑的布尔运算符。但它增加了一些重要的新机制。首先,命题被分析成谓词和参数,这将我们与自然语言的结构的距离拉近了一步。一阶逻辑的标准构造规则承认以下术语:独立变量和独立常量、带不同数量的参数的谓词。例如,`Angus walks`可以被形式化为`walk(angus)`,`Angus sees Bertie`可以被形式化为`see(angus, bertie)`。我们称`walk`为一元谓词,`see`为二元谓词。作为谓词使用的符号不具有内在的含义,虽然很难记住这一点。回到我们前面的一个例子,`(13a)`和`(13b)`之间没有逻辑区别。 + +```py +>>> read_expr = nltk.sem.Expression.fromstring +>>> expr = read_expr('walk(angus)', type_check=True) +>>> expr.argument + +>>> expr.argument.type +e +>>> expr.function + +>>> expr.function.type + +``` + +为什么我们在这个例子的结尾看到``呢?虽然类型检查器会尝试推断出尽可能多的类型,在这种情况下,它并没有能够推断出`walk`的类型,所以其结果的类型是未知的。虽然我们期望`walk`的类型是``,迄今为止类型检查器知道的,在这个上下文中可能是一些其他类型,如``或`e, t>`。要帮助类型检查器,我们需要指定一个信号,作为一个字典来实施,明确的与非逻辑常量类型关联: + +```py +>>> sig = {'walk': ''} +>>> expr = read_expr('walk(angus)', signature=sig) +>>> expr.function.type +e +``` + +一种二元谓词具有类型`>`。虽然这是先组合类型`e`的一个参数成一个一元谓词的类型,我们可以用二元谓词的两个参数直接组合来表示二元谓词。例如,在`Angus sees Cyril`的翻译中谓词`see`会与它的参数结合得到结果`see(angus, cyril)`。 + +在一阶逻辑中,谓词的参数也可以是独立变量,如`x, y, z`。在 NLTK 中,我们采用的惯例:`e`类型的变量都是小写。独立变量类似于人称代词,如`he`,`she`和`it`,其中我们为了弄清楚它们的含义需要知道它们使用的上下文。 + +解释`(14)`中的代名词的方法之一是指向上下文中相关的个体。 + +```py +((exists x. dog(x)) -> bark(x)) + +``` + +## 3.2 一阶定理证明 + +回顾一下我们较早前在`(10)`中提出的`to the north of`上的限制: + +```py +all x. all y.(north_of(x, y) -> -north_of(y, x)) + +``` + +令人高兴的是,定理证明器证明我们的论证是有效的。相反,它得出结论:不能从我们的假设推到出`north_of(f, s)`: + +```py +>>> FnS = read_expr('north_of(f, s)') +>>> prover.prove(FnS, [SnF, R]) +False +``` + +## 3.3 一阶逻辑语言总结 + +我们将借此机会重新表述前面的命题逻辑的语法规则,并添加量词的形式化规则;所有这些一起组成一阶逻辑的句法。此外,我们会明确相关表达式的类型。我们将采取约定:``,一种由`n`个类型为`e`的参数组成产生一个类型为`t`的表达式的谓词的类型。在这种情况下,我们说`n`是谓词的元数。 + + > 1. 如果`P`是``类型的谓词,并且`α[1],..., α[n]`是`e`类型的项,则`P(α[1], ..., α[n])`的类型为`t`。 +> 2. 如果`α`和`β`均为`e`类型,则(`α = β`)和(`α != β`)均为`t`类型。 +> 3. 如果`φ`是·类型,则`-φ`也是。 +> 4. 如果`φ`和`ψ`为`t`类型,那么`(φ & ψ), (φ | ψ), (φ -> ψ), (φ <-> ψ)`也是如此。 +> 5. 如果`φ`是`t`类型,而`x`是`e`类型变量,则存在`x.φ`,而所有`x.φ`都是`t`类型。 + +3.1 总结了`logic`模块的新的逻辑常量,以及`Expression`模块的两个方法。 + +表 3.1: + +一阶逻辑所需的新的逻辑关系和运算符总结,以及`Expression`类的两个有用的方法。 + +```py +>>> dom = {'b', 'o', 'c'} +``` + +我们使用工具函数`Valuation.fromstring()`将`symbol => value`形式的字符串序列转换成一个`Valuation`对象。 + +```py +>>> v = """ +... bertie => b +... olive => o +... cyril => c +... boy => {b} +... girl => {o} +... dog => {c} +... walk => {o, c} +... see => {(b, o), (c, b), (o, c)} +... """ +>>> val = nltk.Valuation.fromstring(v) +>>> print(val) +{'bertie': 'b', + 'boy': {('b',)}, + 'cyril': 'c', + 'dog': {('c',)}, + 'girl': {('o',)}, + 'olive': 'o', + 'see': {('o', 'c'), ('c', 'b'), ('b', 'o')}, + 'walk': {('c',), ('o',)}} +``` + +根据这一估值,`see`的值是一个元组的集合,包含 Bertie 看到 Olive、Cyril 看到 Bertie 和 Olive 看到 Cyril。 + +注意 + +**轮到你来**:模仿 1.2 绘制一个图,描述域`m`和相应的每个一元谓词的集合。 + +你可能已经注意到,我们的一元谓词(即`boy`,`girl`,`dog`)也是以单个元组的集合而不是个体的集合出现的。这使我们能够方便的统一处理任何元数的关系。一个形式为`P(τ[1], ... τ[n])`的谓词,其中`P`是`n`元的,为真的条件是对应于`(τ[1], ..., τ[n])`的值的元组属于`P`的值的元组的集合。 + +```py +>>> ('o', 'c') in val['see'] +True +>>> ('b',) in val['boy'] +True +``` + +## 3.5 独立变量和赋值 + +在我们的模型,上下文的使用对应的是为变量赋值。这是一个从独立变量到域中实体的映射。赋值使用构造函数`Assignment`,它也以论述的模型的域为参数。我们无需实际输入任何绑定,但如果我们要这样做,它们是以`(变量,值)`的形式来绑定,类似于我们前面看到的估值。 + +```py +>>> g = nltk.Assignment(dom, [('x', 'o'), ('y', 'c')]) +>>> g +{'y': 'c', 'x': 'o'} +``` + +此外,还可以使用`print()`查看赋值,使用与逻辑教科书中经常出现的符号类似的符号: + +```py +>>> print(g) +g[c/y][o/x] +``` + +现在让我们看看如何为一阶逻辑的原子公式估值。首先,我们创建了一个模型,然后调用`evaluate()`方法来计算真值。 + +```py +>>> m = nltk.Model(dom, val) +>>> m.evaluate('see(olive, y)', g) +True +``` + +这里发生了什么?我们正在为一个公式估值,类似于我们前面的例子`see(olive, cyril)`。然而,当解释函数遇到变量`y`时,不是检查`val`中的值,它在变量赋值`g`中查询这个变量的值: + +```py +>>> g['y'] +'c' +``` + +由于我们已经知道`o`和`c`在`see`关系中表示的含义,所以`True`值是我们所期望的。在这种情况下,我们可以说赋值`g`满足公式`see(olive, y)`。相比之下,下面的公式相对`g`的评估结果为`False`(检查为什么会是你看到的这样)。 + +```py +>>> m.evaluate('see(y, x)', g) +False +``` + +在我们的方法中(虽然不是标准的一阶逻辑),变量赋值是部分的。例如,`g`中除了`x`和`y`没有其它变量。方法`purge()`清除一个赋值中所有的绑定。 + +```py +>>> g.purge() +>>> g +{} +``` + +如果我们现在尝试为公式,如`see(olive, y)`,相对于`g`估值,就像试图解释一个包含一个`him`的句子,我们不知道`him`指什么。在这种情况下,估值函数未能提供一个真值。 + +```py +>>> m.evaluate('see(olive, y)', g) +'Undefined' +``` + +由于我们的模型已经包含了解释布尔运算的规则,任意复杂的公式都可以组合和评估。 + +```py +>>> m.evaluate('see(bertie, olive) & boy(bertie) & -walk(bertie)', g) +True +``` + +确定模型中公式的真假的一般过程称为模型检查。 + +## 3.6 量化 + +现代逻辑的关键特征之一就是变量满足的概念可以用来解释量化的公式。让我们用`(24)`作为一个例子。 + +```py +>>> m.evaluate('exists x.(girl(x) & walk(x))', g) +True +``` + +在这里`evaluate()``True`,因为`dom`中有某些`u`通过绑定`x`到`u`的赋值满足(`(25)` )。事实上,`o`是这样一个`u`: + +```py +>>> m.evaluate('girl(x) & walk(x)', g.add('x', 'o')) +True +``` + +NLTK 中提供了一个有用的工具是`satisfiers()`方法。它返回满足开放公式的所有个体的集合。该方法的参数是一个已分析的公式、一个变量和一个赋值。下面是几个例子: + +```py +>>> fmla1 = read_expr('girl(x) | boy(x)') +>>> m.satisfiers(fmla1, 'x', g) +{'b', 'o'} +>>> fmla2 = read_expr('girl(x) -> walk(x)') +>>> m.satisfiers(fmla2, 'x', g) +{'c', 'b', 'o'} +>>> fmla3 = read_expr('walk(x) -> girl(x)') +>>> m.satisfiers(fmla3, 'x', g) +{'b', 'o'} +``` + +想一想为什么`fmla2`和`fmla3`是那样的值,这是非常有用。`->`的真值条件的意思是`fmla2`等价于`-girl(x) | walk(x)`,要么不是女孩要么没有步行的个体满足条件。因为`b`(Bertie)和`c`(Cyril)都不是女孩,根据模型`m`,它们都满足整个公式。当然`o`也满足公式,因为`o`两项都满足。现在,因为话题的域的每一个成员都满足`fmla2`,相应的全称量化公式也为真。 + +```py +>>> m.evaluate('all x.(girl(x) -> walk(x))', g) +True +``` + +换句话说,一个全称量化公式`∀x.φ`关于`g`为真,只有对每一个`u`,`φ`关于`g[u/x]`为真。 + +注意 + +**轮到你来**:先用笔和纸,然后用`m.evaluate()`,尝试弄清楚`all x.(girl(x) & walk(x))`和`exists x.(boy(x) -> walk(x))`的真值。确保你能理解为什么它们得到这些值。 + +## 3.7 量词范围歧义 + +当我们给一个句子的形式化表示*两*个量词时,会发生什么? + +```py +>>> v2 = """ +... bruce => b +... elspeth => e +... julia => j +... matthew => m +... person => {b, e, j, m} +... admire => {(j, b), (b, b), (m, e), (e, m)} +... """ +>>> val2 = nltk.Valuation.fromstring(v2) +``` + +`admire`关系可以使用`(28)`所示的映射图进行可视化。 + +```py +>>> dom2 = val2.domain +>>> m2 = nltk.Model(dom2, val2) +>>> g2 = nltk.Assignment(dom2) +>>> fmla4 = read_expr('(person(x) -> exists y.(person(y) & admire(x, y)))') +>>> m2.satisfiers(fmla4, 'x', g2) +{'e', 'b', 'm', 'j'} +``` + +这表明`fmla4`包含域中每一个个体。相反,思考下面的公式`fmla5`;没有满足`y`的值。 + +```py +>>> fmla5 = read_expr('(person(y) & all x.(person(x) -> admire(x, y)))') +>>> m2.satisfiers(fmla5, 'y', g2) +set() +``` + +也就是说,没有大家都钦佩的人。看看另一个开放的公式`fmla6`,我们可以验证有一个人,即 Bruce,它被 Julia 和 Bruce 都钦佩。 + +```py +>>> fmla6 = read_expr('(person(y) & all x.((x = bruce | x = julia) -> admire(x, y)))') +>>> m2.satisfiers(fmla6, 'y', g2) +{'b'} +``` + +注意 + +**轮到你来**:基于`m2`设计一个新的模型,使`(27a)`在你的模型中为假;同样的,设计一个新的模型使`(27b)`为真。 + +## 3.8 模型的建立 + +我们一直假设我们已经有了一个模型,并要检查模型中的一个句子的真值。相比之下,模型的建立是给定一些句子的集合,尝试创造一种新的模型。如果成功,那么我们知道集合是一致的,因为我们有模型的存在作为证据。 + +我们通过创建`Mace()`的一个实例并调用它的`build_model()`方法来调用 Mace4 模式产生器,与调用 Prover9 定理证明器类似的方法。一种选择是将我们的候选的句子集合作为假设,保留目标为未指定。下面的交互显示了`[a, c1]`和`[a, c2]`都是一致的列表,因为`Mace`成功的为它们都建立了一个模型,而`[c1, c2]`不一致。 + +```py +>>> a3 = read_expr('exists x.(man(x) & walks(x))') +>>> c1 = read_expr('mortal(socrates)') +>>> c2 = read_expr('-mortal(socrates)') +>>> mb = nltk.Mace(5) +>>> print(mb.build_model(None, [a3, c1])) +True +>>> print(mb.build_model(None, [a3, c2])) +True +>>> print(mb.build_model(None, [c1, c2])) +False +``` + +我们也可以使用模型建立器作为定理证明器的辅助。假设我们正试图证明`S ⊢ g`,即`g`是假设`S = [s1, s2, ..., sn]`的逻辑派生。我们可以同样的输入提供给 Mace4,模型建立器将尝试找出一个反例,就是要表明`g`*不*遵循从`S`。因此,给定此输入,Mace4 将尝试为假设`S`连同`g`的否定找到一个模型,即列表`S' = [s1, s2, ..., sn, -g]`。如果`g`从`S`不能证明出来,那么 Mace4 会返回一个反例,比 Prover9 更快的得出结论:无法找到所需的证明。相反,如果`g`从`S`是可以证明出来,Mace4 可能要花很长时间不能成功地找到一个反例模型,最终会放弃。 + +让我们思考一个具体的方案。我们的假设是列表`[There is a woman that every man loves, Adam is a man, Eve is a woman]`。我们的结论是`Adam loves Eve`。Mace4 能找到使假设为真而结论为假的模型吗?在下面的代码中,我们使用`MaceCommand()`检查已建立的模型。 + +```py +>>> a4 = read_expr('exists y. (woman(y) & all x. (man(x) -> love(x,y)))') +>>> a5 = read_expr('man(adam)') +>>> a6 = read_expr('woman(eve)') +>>> g = read_expr('love(adam,eve)') +>>> mc = nltk.MaceCommand(g, assumptions=[a4, a5, a6]) +>>> mc.build_model() +True +``` + +因此答案是肯定的:Mace4 发现了一个反例模型,其中`Adam`爱某个女人而不是`Eve`。但是,让我们细看 Mace4 的模型,转换成我们用来估值的格式。 + +```py +>>> print(mc.valuation) +{'C1': 'b', + 'adam': 'a', + 'eve': 'a', + 'love': {('a', 'b')}, + 'man': {('a',)}, + 'woman': {('a',), ('b',)}} +``` + +这个估值的一般形式应是你熟悉的:它包含了一些单独的常量和谓词,每一个都有适当类型的值。可能令人费解的是`C1`。它是一个“Skolem 常量”,模型生成器作为存在量词的表示引入的。也就是说,模型生成器遇到`a4`里面的`exists y`,它知道,域中有某个个体`b`满足`a4`中的开放公式。然而,它不知道`b`是否也是它的输入中的某个地方的一个独立常量的标志,所以它为`b`凭空创造了一个新名字,即`C1`。现在,由于我们的假设中没有关于独立常量`adam`和`eve`的信息,模型生成器认为没有任何理由将它们当做表示不同的实体,于是它们都得到映射到`a`。此外,我们并没有指定`man`和`woman`表示不相交的集合,因此,模型生成器让它们相互重叠。这个演示非常明显的隐含了我们用来解释我们的情境的知识,而模型生成器对此一无所知。因此,让我们添加一个新的假设,使`man`和`woman`不相交。模型生成器仍然产生一个反例模型,但这次更符合我们直觉的有关情况: + +```py +>>> a7 = read_expr('all x. (man(x) -> -woman(x))') +>>> g = read_expr('love(adam,eve)') +>>> mc = nltk.MaceCommand(g, assumptions=[a4, a5, a6, a7]) +>>> mc.build_model() +True +>>> print(mc.valuation) +{'C1': 'c', + 'adam': 'a', + 'eve': 'b', + 'love': {('a', 'c')}, + 'man': {('a',)}, + 'woman': {('c',), ('b',)}} +``` + +经再三考虑,我们可以看到我们的假设中没有说 Eve 是论域中唯一的女性,所以反例模型其实是可以接受的。如果想排除这种可能性,我们将不得不添加进一步的假设,如`exists y. all x. (woman(x) -> (x = y))`以确保模型中只有一个女性。 + +## 4 英语句子的语义 + +## 4.1 基于特征的语法中的合成语义学 + +在本章开头,我们简要说明了一种在句法分析的基础上建立语义表示的方法,使用在 9 开发的语法框架。这一次,不是构建一个 SQL 查询,我们将建立一个逻辑形式。我们设计这样的语法的指导思想之一是组合原则。(也称为 Frege 原则;下面给出的公式参见(Gleitman & Liberman, 1995)。) + +**组合原则**:整体的含义是部分的含义与它们的句法结合方式的函数。 + +我们将假设一个复杂的表达式的语义相关部分由句法分析理论给出。在本章中,我们将认为表达式已经用上下文无关语法分析过。然而,这不是组合原则的内容。 + +我们现在的目标是以一种可以与分析过程平滑对接的方式整合语义表达的构建。`(29)`说明了我们想建立的这种分析的第一个近似。 + +```py +VP[SEM=?v] -> IV[SEM=?v] +NP[SEM=] -> 'Cyril' +IV[SEM=<\x.bark(x)>] -> 'barks' + +``` + +## 4.2 λ演算 + +在 3 中,我们指出数学集合符号对于制定我们想从文档中选择的词的属性`P`很有用。我们用`(31)`说明这个,它是“所有`w`的集合,其中`w`是`V`(词汇表)的元素且`w`有属性`P`”的表示。 + +```py +>>> read_expr = nltk.sem.Expression.fromstring +>>> expr = read_expr(r'\x.(walk(x) & chew_gum(x))') +>>> expr + +>>> expr.free() +set() +>>> print(read_expr(r'\x.(walk(x) & chew_gum(y))')) +\x.(walk(x) & chew_gum(y)) +``` + +我们对绑定表达式中的变量的结果有一个特殊的名字:λ 抽象。当你第一次遇到 λ 抽象时,很难对它们的意思得到一个直观的感觉。`(33b)`的一对英语表示是“是一个`x`,其中`x`步行且`x`嚼口香糖”或“具有步行和嚼口香糖的属性。”通常认为λ-抽象可以很好的表示动词短语(或无主语从句),尤其是当它作为参数出现在它自己的右侧时。如`(34a)`和它的翻译`(34b)`中的演示。 + +```py +(walk(x) & chew_gum(x))[gerald/x] + +``` + +虽然我们迄今只考虑了λ-抽象的主体是一个某种类型`t`的开放公式,这不是必要的限制;主体可以是任何符合语法的表达式。下面是一个有两个λ的例子。 + +```py +>>> print(read_expr(r'\x.\y.(dog(x) & own(y, x))(cyril)').simplify()) +\y.(dog(cyril) & own(y,cyril)) +>>> print(read_expr(r'\x y.(dog(x) & own(y, x))(cyril, angus)').simplify()) ❶ +(dog(cyril) & own(angus,cyril)) +``` + +我们所有的λ-抽象到目前为止只涉及熟悉的一阶变量:`x`、`y`等——类型`e`的变量。但假设我们要处理一个抽象,例如`\x.walk(x)`作为另一个λ-抽象的参数?我们不妨试试这个: + +```py +\y.y(angus)(\x.walk(x)) + +``` + +当β-约减在一个应用`f(a)`中实施时,我们检查是否有自由变量在`a`同时也作为`f`的子术语中绑定的变量出现。假设在上面讨论的例子中,`x`是`a`中的自由变量,`f`包括子术语`exists x.P(x)`。在这种情况下,我们产生一个`exists x.P(x)`的字母变体,也就是说,`exists z1.P(z1)`,然后再进行约减。这种重新标记由`logic`中的β-约减代码自动进行,可以在下面的例子中看到的结果。 + +```py +>>> expr3 = read_expr('\P.(exists x.P(x))(\y.see(y, x))') +>>> print(expr3) +(\P.exists x.P(x))(\y.see(y,x)) +>>> print(expr3.simplify()) +exists z1.see(z1,x) +``` + +注意 + +当你在下面的章节运行这些例子时,你可能会发现返回的逻辑表达式的变量名不同;例如你可能在前面的公式的`z1`的位置看到`z14`。这种标签的变化是无害的——事实上,它仅仅是一个字母变体的例子。 + +在此附注之后,让我们回到英语句子的逻辑形式建立的任务。 + +## 4.3 量化的 NP + +在本节开始,我们简要介绍了如何为`Cyril barks`构建语义表示。你会以为这太容易了——肯定还有更多关于建立组合语义的。例如,量词?没错,这是一个至关重要的问题。例如,我们要给出`(42a)`的逻辑形式`(42b)`。如何才能实现呢? + +```py +>>> read_expr = nltk.sem.Expression.fromstring +>>> tvp = read_expr(r'\X x.X(\y.chase(x,y))') +>>> np = read_expr(r'(\P.exists x.(dog(x) & P(x)))') +>>> vp = nltk.sem.ApplicationExpression(tvp, np) +>>> print(vp) +(\X x.X(\y.chase(x,y)))(\P.exists x.(dog(x) & P(x))) +>>> print(vp.simplify()) +\x.exists z2.(dog(z2) & chase(x,z2)) +``` + +为了建立一个句子的语义表示,我们也需要组合主语`NP`的语义。如果后者是一个量化的表达式,例如`every girl`,一切都与我们前面讲过的`a dog barks`一样的处理方式;主语转换为函数表达式,这被用于`VP`的语义表示。然而,我们现在似乎已经用适当的名称为自己创造了另一个问题。到目前为止,这些已经作为单独的常量进行了语义的处理,这些不能作为像`(47)`那样的表达式的函数应用。因此,我们需要为它们提出不同的语义表示。我们在这种情况下所做的是重新解释适当的名称,使它们也成为如量化的`NP`那样的函数表达式。这里是`Angus`的 λ 表达式。 + +```py +>>> from nltk import load_parser +>>> parser = load_parser('grammars/book_grammars/simple-sem.fcfg', trace=0) +>>> sentence = 'Angus gives a bone to every dog' +>>> tokens = sentence.split() +>>> for tree in parser.parse(tokens): +... print(tree.label()['SEM']) +all z2.(dog(z2) -> exists z1.(bone(z1) & give(angus,z1,z2))) +``` + +NLTK 提供一些实用工具使获得和检查的语义解释更容易。函数`interpret_sents()`用于批量解释输入句子的列表。它建立一个字典`d`,其中对每个输入的句子`sent`,`d[sent]`是包含`sent`的分析树和语义表示的`(synrep, semrep)`对的列表。该值是一个列表,因为`sent`可能有句法歧义;在下面的例子中,列表中的每个句子只有一个分析树。 + +```py +>>> sents = ['Irene walks', 'Cyril bites an ankle'] +>>> grammar_file = 'grammars/book_grammars/simple-sem.fcfg' +>>> for results in nltk.interpret_sents(sents, grammar_file): +... for (synrep, semrep) in results: +... print(synrep) +(S[SEM=] + (NP[-LOC, NUM='sg', SEM=<\P.P(irene)>] + (PropN[-LOC, NUM='sg', SEM=<\P.P(irene)>] Irene)) + (VP[NUM='sg', SEM=<\x.walk(x)>] + (IV[NUM='sg', SEM=<\x.walk(x)>, TNS='pres'] walks))) +(S[SEM=] + (NP[-LOC, NUM='sg', SEM=<\P.P(cyril)>] + (PropN[-LOC, NUM='sg', SEM=<\P.P(cyril)>] Cyril)) + (VP[NUM='sg', SEM=<\x.exists z3.(ankle(z3) & bite(x,z3))>] + (TV[NUM='sg', SEM=<\X x.X(\y.bite(x,y))>, TNS='pres'] bites) + (NP[NUM='sg', SEM=<\Q.exists x.(ankle(x) & Q(x))>] + (Det[NUM='sg', SEM=<\P Q.exists x.(P(x) & Q(x))>] an) + (Nom[NUM='sg', SEM=<\x.ankle(x)>] + (N[NUM='sg', SEM=<\x.ankle(x)>] ankle))))) +``` + +现在我们已经看到了英文句子如何转换成逻辑形式,前面我们看到了在模型中如何检查逻辑形式的真假。把这两个映射放在一起,我们可以检查一个给定的模型中的英语句子的真值。让我们看看前面定义的模型`m`。工具`evaluate_sents()`类似于`interpret_sents()`,除了我们需要传递一个模型和一个变量赋值作为参数。输出是三元组`(synrep, semrep, value)`,其中`synrep`、`semrep`和以前一样,`value`是真值。为简单起见,下面的例子只处理一个简单的句子。 + +```py +>>> v = """ +... bertie => b +... olive => o +... cyril => c +... boy => {b} +... girl => {o} +... dog => {c} +... walk => {o, c} +... see => {(b, o), (c, b), (o, c)} +... """ +>>> val = nltk.Valuation.fromstring(v) +>>> g = nltk.Assignment(val.domain) +>>> m = nltk.Model(val.domain, val) +>>> sent = 'Cyril sees every boy' +>>> grammar_file = 'grammars/book_grammars/simple-sem.fcfg' +>>> results = nltk.evaluate_sents([sent], grammar_file, m, g)[0] +>>> for (syntree, semrep, value) in results: +... print(semrep) +... print(value) +all z4.(boy(z4) -> see(cyril,z4)) +True +``` + +## 4.5 再述量词歧义 + +上述方法的一个重要的限制是它们没有处理范围歧义。我们的翻译方法是句法驱动的,认为语义表示与句法分析紧密耦合,语义中量词的范围也因此反映句法分析树中相应的`NP`的相对范围。因此,像`(26)`这样的句子,在这里重复,总是会被翻译为`(53a)`而不是`(53b)`。 + +```py +\P.exists y.(dog(y) & P(y))(\z2.chase(z1,z2)) + +``` + +最后,我们调用`s_retrieve()`检查读法。 + +```py +>>> cs_semrep.s_retrieve(trace=True) +Permutation 1 + (\P.all x.(girl(x) -> P(x)))(\z2.chase(z2,z4)) + (\P.exists x.(dog(x) & P(x)))(\z4.all x.(girl(x) -> chase(x,z4))) +Permutation 2 + (\P.exists x.(dog(x) & P(x)))(\z4.chase(z2,z4)) + (\P.all x.(girl(x) -> P(x)))(\z2.exists x.(dog(x) & chase(z2,x))) +``` + +```py +>>> for reading in cs_semrep.readings: +... print(reading) +exists x.(dog(x) & all z3.(girl(z3) -> chase(z3,x))) +all x.(girl(x) -> exists z4.(dog(z4) & chase(x,z4))) +``` + +## 5 段落语义层 + +段落是句子的序列。很多时候,段落中的一个句子的解释依赖它前面的句子。一个明显的例子来自照应代词,如`he`、`she`和`it`。给定一个段落如`Angus used to have a dog. But he recently disappeared.`,你可能会解释`he`指的是 Angus 的狗。然而,在`Angus used to have a dog. He took him for walks in New Town.`中,你更可能解释`he`指的是`Angus`自己。 + +## 5.1 段落表示理论 + +一阶逻辑中的量化的标准方法仅限于单个句子。然而,似乎是有量词的范围可以扩大到两个或两个以上的句子的例子。。我们之前看到过一个,下面是第二个例子,与它的翻译一起。 + +```py +([x, y], [angus(x), dog(y), own(x,y)]) + +``` + +我们可以使用`draw()`方法❶可视化结果,如 5.2 所示。 + +```py +>>> drs1.draw() ❶ +``` + +![Images/drs_screenshot0.png](Images/66d94cb86ab90a95cfe745d9613c37b1.jpg) + +图 5.2:DRS 截图 + +我们讨论 5.1 中 DRS 的真值条件时,假设最上面的段落指称被解释为存在量词,而条件也进行了解释,虽然它们是联合的。事实上,每一个 DRS 都可以转化为一阶逻辑公式,`fol()`方法实现这种转换。 + +```py +>>> print(drs1.fol()) +exists x y.(angus(x) & dog(y) & own(x,y)) +``` + +作为一阶逻辑表达式功能补充,DRT 表达式有 DRS 连接运算符,用`+`符号表示。两个 DRS 的连接是一个单独的 DRS 包含合并的段落指称和来自两个论证的条件。DRS 连接自动进行 α 转换绑定变量避免名称冲突。 + +```py +>>> drs2 = read_dexpr('([x], [walk(x)]) + ([y], [run(y)])') +>>> print(drs2) +(([x],[walk(x)]) + ([y],[run(y)])) +>>> print(drs2.simplify()) +([x,y],[walk(x), run(y)]) +``` + +虽然迄今为止见到的所有条件都是原子的,一个 DRS 可以内嵌入另一个 DRS,这是全称量词被处理的方式。在`drs3`中,没有顶层的段落指称,唯一的条件是由两个子 DRS 组成,通过蕴含连接。再次,我们可以使用`fol()`来获得真值条件的句柄。 + +```py +>>> drs3 = read_dexpr('([], [(([x], [dog(x)]) -> ([y],[ankle(y), bite(x, y)]))])') +>>> print(drs3.fol()) +all x.(dog(x) -> exists y.(ankle(y) & bite(x,y))) +``` + +我们较早前指出 DRT 旨在通过链接照应代词和现有的段落指称来解释照应代词。DRT 设置约束条件使段落指称可以像先行词那样“可访问”,但并不打算解释一个特殊的先行词如何被从候选集合中选出的。模块`nltk.sem.drt_resolve_anaphora`采用了类此的保守策略:如果 DRS 包含`PRO(x)`形式的条件,方法`resolve_anaphora()`将其替换为`x = [...]`形式的条件,其中`[...]`是一个可能的先行词列表。 + +```py +>>> drs4 = read_dexpr('([x, y], [angus(x), dog(y), own(x, y)])') +>>> drs5 = read_dexpr('([u, z], [PRO(u), irene(z), bite(u, z)])') +>>> drs6 = drs4 + drs5 +>>> print(drs6.simplify()) +([u,x,y,z],[angus(x), dog(y), own(x,y), PRO(u), irene(z), bite(u,z)]) +>>> print(drs6.simplify().resolve_anaphora()) +([u,x,y,z],[angus(x), dog(y), own(x,y), (u = [x,y,z]), irene(z), bite(u,z)]) +``` + +由于指代消解算法已分离到它自己的模块,这有利于在替代程序中交换,使对正确的先行词的猜测更加智能。 + +我们对 DRS 的处理与处理λ-抽象的现有机制是完全兼容的,因此可以直接基于 DRT 而不是一阶逻辑建立组合语义表示。这种技术在下面的不确定性规则(是语法`drt.fcfg`的一部分)中说明。为便于比较,我们已经从`simple-sem.fcfg`增加了不确定性的平行规则。 + +```py +Det[num=sg,SEM=<\P Q.(([x],[]) + P(x) + Q(x))>] -> 'a' +Det[num=sg,SEM=<\P Q. exists x.(P(x) & Q(x))>] -> 'a' + +``` + +## 5.2 段落处理 + +我们解释一句话时会使用丰富的上下文知识,一部分取决于前面的内容,一部分取决于我们的背景假设。DRT 提供了一个句子的含义如何集成到前面段落表示中的理论,但是在前面的讨论中明显缺少这两个部分。首先,一直没有尝试纳入任何一种推理;第二,我们只处理了个别句子。这些遗漏由模块`nltk.inference.discourse`纠正。 + +段落是一个句子的序列`s[1], ..., s[n]`,段落线是读法的序列`s[1]-r[i], ... s[n]-r[j]`,每个序列对应段落中的一个句子。该模块按增量处理句子,当有歧义时保持追踪所有可能的线。为简单起见,下面的例子中忽略了范围歧义。 + +```py +>>> dt = nltk.DiscourseTester(['A student dances', 'Every student is a person']) +>>> dt.readings() + +s0 readings: + +s0-r0: exists x.(student(x) & dance(x)) + +s1 readings: + +s1-r0: all x.(student(x) -> person(x)) +``` + +一个新句子添加到当前的段落时,设置参数`consistchk=True`会通过每条线,即每个可接受的读法的序列的检查模块来检查一致性。在这种情况下,用户可以选择收回有问题的句子。 + +```py +>>> dt.add_sentence('No person dances', consistchk=True) +Inconsistent discourse: d0 ['s0-r0', 's1-r0', 's2-r0']: + s0-r0: exists x.(student(x) & dance(x)) + s1-r0: all x.(student(x) -> person(x)) + s2-r0: -exists x.(person(x) & dance(x)) +``` + +```py +>>> dt.retract_sentence('No person dances', verbose=True) +Current sentences are +s0: A student dances +s1: Every student is a person +``` + +以类似的方式,我们使用`informchk=True`检查新的句子φ是否对当前的段落有信息量。定理证明器将段落线中现有的句子当做假设,尝试证明φ;如果没有发现这样的证据,那么它是有信息量的。 + +```py +>>> dt.add_sentence('A person dances', informchk=True) +Sentence 'A person dances' under reading 'exists x.(person(x) & dance(x))': +Not informative relative to thread 'd0' +``` + +也可以传递另一套假设作为背景知识,并使用这些筛选出不一致的读法;详情请参阅`http://nltk.org/howto`上的段落 HOWTO。 + +`discourse`模块可适应语义歧义,筛选出不可接受的读法。下面的例子调用 Glue 语义和 DRT。由于 Glue 语义模块被配置为使用的覆盖面广的 Malt 依存关系分析器,输入(`Every dog chases a boy. He runs.`)需要分词和标注。 + +```py +>>> from nltk.tag import RegexpTagger +>>> tagger = RegexpTagger( +... [('^(chases|runs)$', 'VB'), +... ('^(a)$', 'ex_quant'), +... ('^(every)$', 'univ_quant'), +... ('^(dog|boy)$', 'NN'), +... ('^(He)$', 'PRP') +... ]) +>>> rc = nltk.DrtGlueReadingCommand(depparser=nltk.MaltParser(tagger=tagger)) +>>> dt = nltk.DiscourseTester(['Every dog chases a boy', 'He runs'], rc) +>>> dt.readings() + +s0 readings: + +s0-r0: ([],[(([x],[dog(x)]) -> ([z3],[boy(z3), chases(x,z3)]))]) +s0-r1: ([z4],[boy(z4), (([x],[dog(x)]) -> ([],[chases(x,z4)]))]) + +s1 readings: + +s1-r0: ([x],[PRO(x), runs(x)]) +``` + +段落的第一句有两种可能的读法,取决于量词的作用域。第二句的唯一的读法通过条件`PRO(x)`表示代词`He`。现在让我们看看段落线的结果: + +```py +>>> dt.readings(show_thread_readings=True) +d0: ['s0-r0', 's1-r0'] : INVALID: AnaphoraResolutionException +d1: ['s0-r1', 's1-r0'] : ([z6,z10],[boy(z6), (([x],[dog(x)]) -> +([],[chases(x,z6)])), (z10 = z6), runs(z10)]) +``` + +当我们检查段落线`d0`和`d1`时,我们看到读法`s0-r0`,其中`every dog`超出了`a boy`的范围,被认为是不可接受的,因为第二句的代词不能得到解释。相比之下,段落线`d1`中的代词(重写为`z24`)*通过*等式`(z24 = z20)`绑定。 + +不可接受的读法可以通过传递参数`filter=True`过滤掉。 + +```py +>>> dt.readings(show_thread_readings=True, filter=True) +d1: ['s0-r1', 's1-r0'] : ([z12,z15],[boy(z12), (([x],[dog(x)]) -> +([],[chases(x,z12)])), (z17 = z12), runs(z15)]) +``` + +虽然这一小段是极其有限的,它应该能让你对于我们在超越单个句子后产生的语义处理问题,以及部署用来解决它们的技术有所了解。 + +## 6 小结 + +* 一阶逻辑是一种适合在计算环境中表示自然语言的含义的语言,因为它很灵活,足以表示自然语言含义的很多有用的方面,具有使用一阶逻辑推理的高效的定理证明器。(同样的,自然语言语义中也有各种各样的现象,需要更强大的逻辑机制。) +* 在将自然语言句子翻译成一阶逻辑的同时,我们可以通过检查一阶公式模型表述这些句子的真值条件。 +* 为了构建成分组合的意思表示,我们为一阶逻辑补充了λ-演算。 +* λ-演算中的β-约简在语义上与函数传递参数对应。句法上,它包括将被函数表达式中的λ绑定的变量替换为函数应用中表达式提供的参数。 +* 构建模型的一个关键部分在于建立估值,为非逻辑常量分配解释。这些被解释为`n`元谓词或独立常量。 +* 一个开放表达式是一个包含一个或多个自由变量的表达式。开放表达式只在它的自由变量被赋值时被解释。 +* 量词的解释是对于具有变量`x`的公式`φ[x]`,构建个体的集合,赋值`g`分配它们作为`x`的值使`φ[x]`为真。然后量词对这个集合加以约束。 +* 一个封闭的表达式是一个没有自由变量的表达式;也就是,变量都被绑定。一个封闭的表达式是真是假取决于所有变量赋值。 +* 如果两个公式只是由绑定操作符(即λ或量词)绑定的变量的标签不同,那么它们是α-等价。重新标记公式中的绑定变量的结果被称为α-转换。 +* 给定有两个嵌套量词`Q[1]`和`Q[2]`的公式,最外层的量词`Q[1]`有较广的范围(或范围超出`Q[2]`)。英语句子往往由于它们包含的量词的范围而产生歧义。 +* 在基于特征的语法中英语句子可以通过将`sem`作为特征与语义表达关联。一个复杂的表达式的`sem`值通常包括成分表达式的`sem`值的函数应用。 + +## 7 深入阅读 + +关于本章的进一步材料以及如何安装 Prover9 定理证明器和 Mace4 模型生成器的内容请查阅`http://nltk.org/`。这两个推论工具一般信息见(McCune, 2008)。 + +用 NLTK 进行语义分析的更多例子,请参阅`http://nltk.org/howto`上的语义和逻辑 HOWTO。请注意,范围歧义还有其他两种解决方法,即(Blackburn & Bos, 2005)描述的 Hole 语义和(Dalrymple, 1999)描述的 Glue 语义。 + +自然语言语义中还有很多现象没有在本章中涉及到,主要有: + +1. 事件、时态和体; +2. 语义角色; +3. 广义量词,如`most`; +4. 内涵结构,例如像`may`和`believe`这样的动词。 + +`(1)`和`(2)`可以使用一阶逻辑处理,`(3)`和`(4)`需要不同的逻辑。下面的读物中很多都讲述了这些问题。 + +建立自然语言前端数据库方面的结果和技术的综合概述可以在(Androutsopoulos, Ritchie, & Thanisch, 1995)中找到。 + +任何一本现代逻辑的入门书都将提出命题和一阶逻辑。强烈推荐(Hodges, 1977),书中有很多有关自然语言的有趣且有洞察力的文字和插图。 + +要说范围广泛,参阅两卷本的关于逻辑教科书(Gamut, 1991)和(Gamut, 1991),也包含了有关自然语言的形式语义的当代材料,例如 Montague 文法和内涵逻辑。(Kamp & Reyle, 1993)提供段落表示理论的权威报告,包括涵盖大量且有趣的自然语言片段,包括时态、体和形态。另一个对许多自然语言结构的语义的全面研究是(Carpenter, 1997)。 + +有许多作品介绍语言学理论框架内的逻辑语义。(Chierchia & McConnell-Ginet, 1990)与句法相对无关,而(Heim & Kratzer, 1998)和(Larson & Segal, 1995)都更明确的倾向于将语义真值条件整合到乔姆斯基框架中。 + +(Blackburn & Bos, 2005)是致力于计算语义的第一本教科书,为该领域提供了极好的介绍。它扩展了许多本章涵盖的主题,包括量词范围歧义的未指定、一阶逻辑推理以及段落处理。 + +要获得更先进的当代语义方法的概述,包括处理时态和广义量词,尝试查阅(Lappin, 1996)或(Benthem & Meulen, 1997)。 + +## 8 练习 + +1. ☼ 将下列句子翻译成命题逻辑,并用`Expression.fromstring()`验证结果。提供显示你的翻译中命题变量如何对应英语表达的一个要点。 + + 1. If Angus sings, it is not the case that Bertie sulks. + 2. Cyril runs and barks. + 3. It will snow if it doesn't rain. + 4. It's not the case that Irene will be happy if Olive or Tofu comes. + 5. Pat didn't cough or sneeze. + 6. If you don't come if I call, I won't come if you call. +2. ☼ 翻译下面的句子为一阶逻辑的谓词参数公式。 + + 1. Angus likes Cyril and Irene hates Cyril. + 2. Tofu is taller than Bertie. + 3. Bruce loves himself and Pat does too. + 4. Cyril saw Bertie, but Angus didn't. + 5. Cyril is a fourlegged friend. + 6. Tofu and Olive are near each other. +3. ☼ 翻译下列句子为成一阶逻辑的量化公式。 + + 1. Angus likes someone and someone likes Julia. + 2. Angus loves a dog who loves him. + 3. Nobody smiles at Pat. + 4. Somebody coughs and sneezes. + 5. Nobody coughed or sneezed. + 6. Bruce loves somebody other than Bruce. + 7. Nobody other than Matthew loves somebody Pat. + 8. Cyril likes everyone except for Irene. + 9. Exactly one person is asleep. +4. ☼ 翻译下列动词短语,使用λ-抽象和一阶逻辑的量化公式。 + + 1. feed Cyril and give a capuccino to Angus + 2. be given 'War and Peace' by Pat + 3. be loved by everyone + 4. be loved or detested by everyone + 5. be loved by everyone and detested by no-one +5. ☼ 思考下面的语句: + + ```py + >>> read_expr = nltk.sem.Expression.fromstring + >>> e2 = read_expr('pat') + >>> e3 = nltk.sem.ApplicationExpression(e1, e2) + >>> print(e3.simplify()) + exists y.love(pat, y) + ``` + + 显然这里缺少了什么东西,即`e1`值的声明。为了`ApplicationExpression(e1, e2)`被β-转换为`exists y.love(pat, y)`,`e1`必须是一个以`pat`为参数的λ-抽象。你的任务是构建这样的一个抽象,将它绑定到`e1`,使上面的语句都是满足(上到字母方差)。此外,提供一个`e3.simplify()`的非正式的英文翻译。 + + 现在根据`e3.simplify()`的进一步情况(如下所示)继续做同样的任务。 + + ```py + >>> print(e3.simplify()) + exists y.(love(pat,y) | love(y,pat)) + ``` + + ```py + >>> print(e3.simplify()) + exists y.(love(pat,y) | love(y,pat)) + ``` + + ```py + >>> print(e3.simplify()) + walk(fido) + ``` + +6. ☼ 如前面的练习中那样,找到一个λ-抽象`e1`,产生与下面显示的等效的结果。 + + ```py + >>> e2 = read_expr('chase') + >>> e3 = nltk.sem.ApplicationExpression(e1, e2) + >>> print(e3.simplify()) + \x.all y.(dog(y) -> chase(x,pat)) + ``` + + ```py + >>> e2 = read_expr('chase') + >>> e3 = nltk.sem.ApplicationExpression(e1, e2) + >>> print(e3.simplify()) + \x.exists y.(dog(y) & chase(pat,x)) + ``` + + ```py + >>> e2 = read_expr('give') + >>> e3 = nltk.sem.ApplicationExpression(e1, e2) + >>> print(e3.simplify()) + \x0 x1.exists y.(present(y) & give(x1,y,x0)) + ``` + +7. ☼ 如前面的练习中那样,找到一个λ-抽象`e1`,产生与下面显示的等效的结果。 + + ```py + >>> e2 = read_expr('bark') + >>> e3 = nltk.sem.ApplicationExpression(e1, e2) + >>> print(e3.simplify()) + exists y.(dog(x) & bark(x)) + ``` + + ```py + >>> e2 = read_expr('bark') + >>> e3 = nltk.sem.ApplicationExpression(e1, e2) + >>> print(e3.simplify()) + bark(fido) + ``` + + ```py + >>> e2 = read_expr('\\P. all x. (dog(x) -> P(x))') + >>> e3 = nltk.sem.ApplicationExpression(e1, e2) + >>> print(e3.simplify()) + all x.(dog(x) -> bark(x)) + ``` + +8. ◑ 开发一种方法,翻译英语句子为带有二元广义量词的公式。在此方法中,给定广义量词`Q`,量化公式的形式为`Q(A, B)`,其中`A`和`B`是``类型的表达式。那么,例如`all(A, B)`为真当且仅当`A`表示的是`B`所表示的一个子集。 + +9. ◑ 扩展前面练习中的方法,使量词如`most`和`exactly three`的真值条件可以在模型中计算。 + +10. ◑ 修改`sem.evaluate`代码,使它能提供一个有用的错误消息,如果一个表达式不在模型的估值函数的域中。 + +11. ★ 从儿童读物中选择三个或四个连续的句子。一个例子是`nltk.corpus.gutenberg`中的故事集:`bryant-stories.txt`,`burgess-busterbrown.txt`和`edgeworth-parents.txt`。开发一个语法,能将你的句子翻译成一阶逻辑,建立一个模型,使它能检查这些翻译为真或为假。 + +12. ★ 实施前面的练习,但使用 DRT 作为意思表示。 + +13. (Warren & Pereira, 1982)为出发点,开发一种技术,转换一个自然语言查询为一种可以更加有效的在模型中评估的形式。例如,给定一个`(P(x) & Q(x))`形式的查询,将它转换为`(Q(x) & P(x))`,如果`Q`的范围比`P`小。 + +## 关于本文档... + +针对 NLTK 3.0 作出更新。本章来自于《Python 自然语言处理》,[Steven Bird](http://estive.net/), [Ewan Klein](http://homepages.inf.ed.ac.uk/ewan/) 和 [Edward Loper](http://ed.loper.org/),Copyright © 2014 作者所有。本章依据 [*Creative Commons Attribution-Noncommercial-No Derivative Works 3\.0 United States License*](http://creativecommons.org/licenses/by-nc-nd/3.0/us/) 条款,与[*自然语言工具包*](http://nltk.org/) 3.0 版一起发行。 + +本文档构建于星期三 2015 年 7 月 1 日 12:30:05 AEST \ No newline at end of file diff --git a/docs/nlp-py-2e/11.md b/docs/nlp-py-2e/11.md new file mode 100644 index 0000000000000000000000000000000000000000..99486b507932622d86858f68cda838f8c33a2a9f --- /dev/null +++ b/docs/nlp-py-2e/11.md @@ -0,0 +1,794 @@ +# 11 语言学数据管理 + +已标注的语言数据的结构化集合在 NLP 的大部分领域都是至关重要的,但是,我们使用它们仍然面临着许多障碍。本章的目的是要回答下列问题: + +1. 我们如何设计一种新的语言资源,并确保它的覆盖面、平衡以及支持广泛用途的文档? +2. 现有数据对某些分析工具格式不兼容,我们如何才能将其转换成合适的格式? +3. 有什么好的方法来记录我们已经创建的资源的存在,让其他人可以很容易地找到它? + +一路上,我们将研究当前语料库的设计、创建一个语料库的典型工作流程,及语料库的生命周期。与在其他章节中一样,会有语言数据管理实际实验的很多例子,包括在语言学现场教学课程、实验室的工作和网络爬取中收集的数据。 + +## 1 语料库结构:一个案例研究 + +TIMIT 语料库是第一个广泛发布的已标注语音数据库,它有一个特别清晰的组织结构。TIMIT 由一个包括克萨斯仪器公司和麻省理工学院的财团开发,它也由此得名。它被设计用来为声学-语音知识的获取提供数据,并支持自动语音识别系统的开发和评估。 + +## 1.1 TIMIT 的结构 + +与布朗语料库显示文章风格和来源的平衡选集一样,TIMIT 包括方言、说话者和材料的平衡选集。对 8 个方言区中的每一种方言,具有一定年龄范围和教育背景的 50 个男性和女性的说话者每人读 10 个精心挑选的句子。设计中有两句话是所有说话者都读的,带来方言的变化: + +```py +>>> phonetic = nltk.corpus.timit.phones('dr1-fvmh0/sa1') +>>> phonetic +['h#', 'sh', 'iy', 'hv', 'ae', 'dcl', 'y', 'ix', 'dcl', 'd', 'aa', 'kcl', +'s', 'ux', 'tcl', 'en', 'gcl', 'g', 'r', 'iy', 's', 'iy', 'w', 'aa', +'sh', 'epi', 'w', 'aa', 'dx', 'ax', 'q', 'ao', 'l', 'y', 'ih', 'ax', 'h#'] +>>> nltk.corpus.timit.word_times('dr1-fvmh0/sa1') +[('she', 7812, 10610), ('had', 10610, 14496), ('your', 14496, 15791), +('dark', 15791, 20720), ('suit', 20720, 25647), ('in', 25647, 26906), +('greasy', 26906, 32668), ('wash', 32668, 37890), ('water', 38531, 42417), +('all', 43091, 46052), ('year', 46052, 50522)] +``` + +除了这种文本数据,TIMIT 还包括一个词典,提供每一个词的可与一个特定的话语比较的规范的发音: + +```py +>>> timitdict = nltk.corpus.timit.transcription_dict() +>>> timitdict['greasy'] + timitdict['wash'] + timitdict['water'] +['g', 'r', 'iy1', 's', 'iy', 'w', 'ao1', 'sh', 'w', 'ao1', 't', 'axr'] +>>> phonetic[17:30] +['g', 'r', 'iy', 's', 'iy', 'w', 'aa', 'sh', 'epi', 'w', 'aa', 'dx', 'ax'] +``` + +这给了我们一点印象:语音处理系统在处理或识别这种特殊的方言(新英格兰)的语音中必须做什么。最后,TIMIT 包括说话人的人口学统计,允许细粒度的研究声音、社会和性别特征。 + +```py +>>> nltk.corpus.timit.spkrinfo('dr1-fvmh0') +SpeakerInfo(id='VMH0', sex='F', dr='1', use='TRN', recdate='03/11/86', +birthdate='01/08/60', ht='5\'05"', race='WHT', edu='BS', +comments='BEST NEW ENGLAND ACCENT SO FAR') +``` + +## 1.2 主要设计特点 + +TIMIT 演示了语料库设计中的几个主要特点。首先,语料库包含语音和字形两个标注层。一般情况下,文字或语音语料库可能在多个不同的语言学层次标注,包括形态、句法和段落层次。此外,即使在给定的层次仍然有不同的标注策略,甚至标注者之间也会有分歧,因此我们要表示多个版本。TIMIT 的第二个特点是:它在多个维度的变化与方言地区和二元音覆盖范围之间取得平衡。人口学统计的加入带来了许多更独立的变量,这可能有助于解释数据中的变化,便于以后出于在建立语料库时没有想到的目的使用语料库,例如社会语言学。第三个特点是:将原始语言学事件作为录音来捕捉和作为标注来捕捉之间有明显的区分。两者一致表示文本语料库正确,原始文本通常有被认为是不可改变的作品的外部来源。那个作品的任何包含人的判断的转换——即使如分词一样简单——也是后来的修订版,因此以尽可能接近原始的形式保留源材料是十分重要的。 + +![Images/timit-structure.png](Images/953f4a408c97594449de5ca84c294719.jpg) + +图 1.2:发布的 TIMIT 语料库的结构:CD-ROM 包含文档、顶层的训练和测试目录;训练和测试目录都有 8 子目录,每个方言区一个;这些目录又包含更多子目录,每个说话者一个;列出的目录是女性说话者`aks0`的目录的内容,显示 10 个`wav`文件配以一个录音文本文件、一个录音文本词对齐文件和一个音标文件。 + +TIMIT 的第四个特点是语料库的层次结构。每个句子 4 个文件,500 个说话者每人 10 个句子,有 20,000 个文件。这些被组织成一个树状结构,示意图如 1.2 所示。在顶层分成训练集和测试集,用于开发和评估统计模型。 + +最后,请注意虽然 TIMIT 是语音语料库,它的录音文本和相关数据只是文本,可以使用程序处理了,就像任何其他的文本语料库那样。因此,许多在这本书中所描述的计算方法都适用。此外,注意 TIMIT 语料库包含的所有数据类型分为词汇和文字两个基本类别,我们将在下面讨论。说话者人口学统计数据只不过是词汇数据类型的另一个实例。 + +当我们考虑到文字和记录结构是计算机科学中关注数据管理的两个子领域首要内容,即全文检索领域和数据库领域,这最后的观察就不太令人惊讶了。语言数据管理的一个显着特点是往往将这两种数据类型放在一起,可以利用这两个领域的成果和技术。 + +## 1.3 基本数据类型 + +![Images/datatypes.png](Images/13361de430cd983e689417c547330bbc.jpg) + +图 1.3:基本语言数据类型——词汇和文本:它们的多样性中,词汇具有记录结构,而已标注文本具有时间组织。 + +不考虑它的复杂性,TIMIT 语料库只包含两种基本数据类型,词典和文本。正如我们在 2 中所看到的,大多数词典资源都可以使用记录结构表示,即一个关键字加一个或多个字段,如 1.3 所示。词典资源可能是一个传统字典或比较词表,如下所示。它也可以是一个短语词典,其中的关键字是一个短语而不是一个词。词典还包括记录结构化的数据,我们可以通过对应主题的非关键字字段来查找条目。我们也可以构造特殊的表格(称为范例)来进行对比和说明系统性的变化,1.3 显示了三个动词。TIMIT 的说话者表也是一种词典资源。 + +在最抽象的层面上,文本是一个真实的或虚构的讲话事件的表示,该事件的时间过程也在文本本身存在。一个文本可以是一个小单位,如一个词或句子,也可以是一个完整的叙述或对话。它可能会有标注如词性标记、形态分析、话语结构等。正如我们在 IOB 标注`(7)`中所看到的可以使用单个词的标记表示更高层次的成分。因此,1.3 所示的文本的抽象就足够了。 + +不考虑单独的语料库的复杂性和特质,最基本的,它们是带有记录结构化数据的文本集合。语料库的内容往往偏重于这些类型中的一种或多种。例如:布朗语料库包含 500 个文本文件,但我们仍然可以使用表将这些文件与 15 种不同风格关联。在事情的另一面,WordNet 包含 117659 个同义词集记录,也包含许多例子句子(小文本)来说明词的用法。TIMIT 处在中间,含有大量的独立的文本和词汇类型的材料。 + +## 2 语料库生命周期 + +语料库并不是从天而降的,需要精心的准备和许多人长时期的输入。原始数据需要进行收集、清理、记录并以系统化的结构存储。标注可分为各种层次,一些需要语言的形态或句法的专门知识。要在这个阶段成功取决于建立一个高效的工作流程,包括适当的工具和格式转换器。质量控制程序可以将寻找标注中的不一致落实到位,确保尽最大可能在标注者之间达成一致。由于任务的规模和复杂性,大型语料库可能需要几年的准备,包括几十或上百人多年的努力。在本节中,我们简要地回顾语料库生命周期的各个阶段。 + +## 2.1 语料库创建的三种方案 + +语料库的一种类型是设计在创作者的探索过程中逐步展现。这是典型的传统“领域语言学”模式,即来自会话的材料在它被收集的时候就被分析,明天的想法往往基于今天的分析中产生的问题。。在随后几年的研究中产生的语料不断被使用,并可能用作不确定的档案资源。计算机化明显有利于这种类型的工作,以广受欢迎的程序 Shoebox 为例,它作为 Toolbox 重新发布,现在已有超过二十年的历史(见 4)。其他的软件工具,甚至是简单的文字处理器和电子表格,通常也可用于采集数据。在下一节,我们将着眼于如何从这些来源提取数据。 + +另一种语料库创建方案是典型的实验研究,其中一些精心设计的材料被从一定范围的人类受试者中收集,然后进行分析来评估一个假设或开发一种技术。此类数据库在实验室或公司内被共享和重用已很常见,经常被更广泛的发布。这种类型的语料库是“共同任务”的科研管理方法的基础,这在过去的二十年已成为政府资助的语言技术研究项目。在前面的章节中,我们已经遇到很多这样的语料库;我们将看到如何编写 Python 程序实践这些语料库发布前必要的一些任务。 + +最后,还有努力为一个特定的语言收集“参考语料”,如*美国国家语料库*(ANC)和*英国国家语料库*(BNC)。这里的目标已经成为产生各种形式、风格和语言的使用的一个全面的记录。除了规模庞大的挑战,还严重依赖自动标注工具和后期编辑共同修复错误。然而,我们可以编写程序来查找和修复错误,还可以分析语料库是否平衡。 + +## 2.2 质量控制 + +自动和手动的数据准备的好的工具是必不可少的。然而,一个高质量的语料库的建立很大程度取决于文档、培训和工作流程等平凡的东西。标注指南确定任务并记录标记约定。它们可能会定期更新以覆盖不同的情况,同时制定实现更一致的标注的新规则。在此过程中标注者需要接受训练,包括指南中没有的情况的解决方法。需要建立工作流程,尽可能与支持软件一起,跟踪哪些文件已被初始化、标注、验证、手动检查等等。可能有多层标注,由不同的专家提供。不确定或不一致的情况可能需要裁决。 + +大的标注任务需要多个标注者,由此产生一致性的问题。一组标注者如何能一致的处理呢?我们可以通过将一部分独立的原始材料由两个人分别标注,很容易地测量标注的一致性。这可以揭示指南中或标注任务的不同功能的不足。在对质量要求较高的情况下,整个语料库可以标注两次,由专家裁决不一致的地方。 + +报告标注者之间对语料库达成的一致性被认为是最佳实践(如通过两次标注 10% 的语料库)。这个分数作为一个有用的在此语料库上训练的所有自动化系统的期望性能的上限。 + +小心! + +应谨慎解释标注者之间一致性得分,因为标注任务的难度差异巨大。例如,90% 的一致性得分对于词性标注是可怕的得分,但对语义角色标注是可以预期的得分。 + +Kappa 系数`K`测量两个人判断类别和修正预期的期望一致性的一致性。例如,假设要标注一个项目,四种编码选项可能性相同。这种情况下,两个人随机编码预计有 25% 可能达成一致。因此,25% 一致性将表示为`k = 0`,相应的较好水平的一致性将依比例决定。对于一个 50% 的一致性,我们将得到`k = 0.333`,因为 50 是从 25 到 100 之间距离的三分之一。还有许多其他一致性测量方法;详情请参阅`help(nltk.metrics.agreement)`。 + +![Images/windowdiff.png](Images/58a1097522dc6fbe24eddd96cfd6cbc9.jpg) + +图 2.1:一个序列的三种分割:小矩形代表字、词、句,总之,任何可能被分为语言单位的序列;`S[1]`和`S[2]`是接近一致的,两者都与`S[3]`显著不同。 + +我们还可以测量语言输入的两个独立分割的一致性,例如分词、句子分割、命名实体识别。在 2.1 中,我们看到三种可能的由标注者(或程序)产生的项目序列的分割。虽然没有一个完全一致,`S[1]`和`S[2]`是接近一致的,我们想要一个合适的测量。Windowdiff 是评估两个分割一致性的一个简单的算法,通过在数据上移动一个滑动窗口计算近似差错的部分得分。如果我们将词符预处理成 0 和 1 的序列,当词符后面跟着边界符号时记录下来,我们就可以用字符串表示分割,应用 windowdiff 打分器。 + +```py +>>> s1 = "00000010000000001000000" +>>> s2 = "00000001000000010000000" +>>> s3 = "00010000000000000001000" +>>> nltk.windowdiff(s1, s1, 3) +0.0 +>>> nltk.windowdiff(s1, s2, 3) +0.190... +>>> nltk.windowdiff(s2, s3, 3) +0.571... +``` + +上面的例子中,窗口大小为 3。Windowdiff 计算在一对字符串上滑动这个窗口。在每个位置它计算两个字符串在这个窗口内的边界的总数,然后计算差异。最后累加这些差异。我们可以增加或缩小窗口的大小来控制测量的敏感度。 + +## 2.3 维护与演变 + +随着大型语料库的发布,研究人员立足于均衡的从为完全不同的目的而创建的语料库中派生出的子集进行调查的可能性越来越大。例如,Switchboard 数据库,最初是为识别说话人的研究而收集的,已被用作语音识别、单词发音、口吃、句法、语调和段落结构研究的基础。重用语言语料库的动机包括希望节省时间和精力,希望在别人可以复制的材料上工作,有时希望研究语言行为的更加自然的形式。为这样的研究选择子集的过程本身可视为一个不平凡的贡献。 + +除了选择语料库的适当的子集,这个新的工作可能包括重新格式化文本文件(如转换为 XML),重命名文件,重新为文本分词,选择数据的一个子集来充实等等。多个研究小组可以独立的做这项工作,如 2.2 所示。在以后的日子,应该有人想要组合不同的版本的源数据,这项任务可能会非常繁重。 + +![Images/evolution.png](Images/e33fb540f11c5ea9a07441be8a407d43.jpg) + +图 2.2:语料库随着时间的推移而演变:语料库发布后,研究小组将独立的使用它,选择和丰富不同的部分;然后研究努力整合单独的标注,面临校准注释的艰巨的挑战。 + +由于缺乏有关派生的版本如何创建的,哪个版本才是最新的等记录,使用派生的语料库的任务变得更加困难。 + +这种混乱情况的改进方法是集中维护语料库,专家委员会定期修订和扩充它,考虑第三方的意见,不时发布的新版本。出版字典和国家语料库可能以这种方式集中维护。然而,对于大多数的语料库,这种模式是完全不切实际的。 + +原始语料库的出版的一个中间过程是要有一个能识别其中任何一部分的规范。每个句子、树、或词条都有一个全局的唯一标识符,每个词符、节点或字段(分别)都有一个相对偏移。标注,包括分割,可以使用规范的标识符(一个被称为对峙注释的方法)引用源材料。这样,新的标注可以与源材料独立分布,同一来源的多个独立标注可以对比和更新而不影响源材料。 + +如果语料库出版提供了多个版本,版本号或日期可以是识别规范的一部分。整个语料的版本标识符之间的对应表,将使任何对峙的注释更容易被更新。 + +小心! + +有时一个更新的语料包含对一直在外部标注的基本材料的修正。词符可能会被分拆或合并,成分可能已被重新排列。新老标识符之间可能不会一一对应。使对峙标注打破新版本的这些组件比默默允许其标识符指向不正确的位置要好。 + +## 3 数据采集 + +## 3.1 从网上获取数据 + +网络是语言分析的一个丰富的数据源。我们已经讨论了访问单个文件,如 RSS 订阅、搜索引擎的结果(见 3.1)的方法。然而,在某些情况下,我们要获得大量的 Web 文本。 + +最简单的方法是获得出版的网页文本的文集。Web 语料库 ACL 特别兴趣组(SIGWAC)在`http://www.sigwac.org.uk/`维护一个资源列表。使用定义好的 Web 语料库的优点是它们有文档、稳定并允许重复性实验。 + +如果所需的内容在一个特定的网站,有许多实用程序能捕获网站的所有可访问内容,如 *GNU Wget*,`http://www.gnu.org/software/wget/`。为了最大的灵活性和可控制,可以使用网络爬虫如 *Heritrix*,`http://crawler.archive.org/`(Croft, Metzler, & Strohman, 2009)。例如:如果我们要编译双语文本集合,对应两种语言的文档对,爬虫需要检测站点的结构以提取文件之间的对应关系,它需要按照捕获的对应方式组织下载的页面。写你自己的网页爬虫可能使很有诱惑力的,但也有很多陷阱需要克服,如检测 MIME 类型、转换相对地址为绝对 URL、避免被困在循环链接结构、处理网络延迟、避免使站点超载或被禁止访问该网站等。 + +## 3.2 从字处理器文件获取数据 + +文字处理软件通常用来在具有有限的可计算基础设施的项目中手工编制文本和词汇。这些项目往往提供数据录入模板,通过字处理软件并不能保证数据结构正确。例如,每个文本可能需要有一个标题和日期。同样,每个词条可能有一些必须的字段。随着数据规模和复杂性的增长,用于维持其一致性的时间的比重也增大。 + +我们怎样才能提取这些文件的内容,使我们能够在外部程序中操作?此外,我们如何才能验证这些文件的内容,以帮助作者创造结构良好的数据,在原始的创作过程中最大限度提高数据的质量? + +考虑一个字典,其中的每个条目都有一个词性字段,从一个 20 个可能值的集合选取,在发音字段显示,以 11 号黑体字呈现。传统的文字处理器没有能够验证所有的词性字段已正确输入和显示的搜索函数或宏。这个任务需要彻底的手动检查。如果字处理器允许保存文档为一种非专有的格式,如 text、HTML 或 XML,有时我们可以写程序自动做这个检查。 + +思考下面的一个词条的片段:`sleep [sli:p] v.i.condition of body and mind...`。我们可以在 MSWord 中输入这些词,然后“另存为网页”,然后检查生成的 HTML 文件: + +```py +

sleep + + [sli:p] + + v.i. + + a condition of body and mind ... +

+ +``` + +这个简单的程序只是冰山一角。我们可以开发复杂的工具来检查字处理器文件的一致性,并报告错误,使字典的维护者可以*使用原来的文字处理器*纠正的原始文件。 + +只要我们知道数据的正确格式,就可以编写其他程序将数据转换成不同格式。3.1 中的程序使用`nltk.clean_html()`剥离 HTML 标记,提取词和它们的发音,以“逗号分隔值”(CSV)格式生成输出。 + +```py +from bs4 import BeautifulSoup + +def lexical_data(html_file, encoding="utf-8"): + SEP = '_ENTRY' + html = open(html_file, encoding=encoding).read() + html = re.sub(r' 2: + yield entry.split(' ', 3) +``` + +with gzip.open(fn+".gz","wb") as f_out: + +f_out.write(bytes(s, 'UTF-8')) + +注意 + +更多 HTML 复杂的处理可以使用`http://www.crummy.com/software/BeautifulSoup/`上的 *BeautifulSoup* 的包。 + +## 3.3 从电子表格和数据库中获取数据 + +电子表格通常用于获取词表或范式。例如,一个比较词表可以用电子表格创建,用一排表示每个同源组,每种语言一列(见`nltk.corpus.swadesh`和`www.rosettaproject.org`)。大多数电子表格软件可以将数据导出为 CSV 格式。正如我们将在下面看到的,使用`csv`模块 Python 程序可以很容易的访问它们。 + +有时词典存储在一个完全成熟的关系数据库。经过适当的标准化,这些数据库可以确保数据的有效性。例如,我们可以要求所有词性都来自指定的词汇,通过声明词性字段为*枚举类型*或用一个外键引用一个单独的词性表。然而,关系模型需要提前定义好的数据(模式)结构,这与高度探索性的构造语言数据的主导方法相违背。被认为是强制性的和独特的字段往往需要是可选的、可重复。只有当数据类型提前全都知道时关系数据库才是适用的,如果不是,或者几乎所有的属性都是可选的或重复的,关系的做法就行不通了。 + +然而,当我们的目标只是简单的从数据库中提取内容时,完全可以将表格(或 SQL 查询结果)转换成 CSV 格式,并加载到我们的程序中。我们的程序可能会执行不太容易用 SQL 表示的语言学目的的查询,如`select all words that appear in example sentences for which no dictionary entry is provided`。对于这个任务,我们需要从记录中提取足够的信息,使它连同词条和例句能被唯一的识别。让我们假设现在这个信息是在一个 CSV 文件`dict.csv`中: + +```py +"sleep","sli:p","v.i","a condition of body and mind ..." +"walk","wo:k","v.intr","progress by lifting and setting down each foot ..." +"wake","weik","intrans","cease to sleep" + +``` + +然后,这些信息将可以指导正在进行的工作来丰富词汇和更新关系数据库的内容。 + +## 3.4 转换数据格式 + +已标注语言数据很少以最方便的格式保存,往往需要进行各种格式转换。字符编码之间的转换已经讨论过(见 3.3)。在这里,我们专注于数据结构。 + +最简单的情况,输入和输出格式是同构的。例如,我们可能要将词汇数据从 Toolbox 格式转换为 XML,可以直接一次一个的转换词条`(4)`。数据结构反映在所需的程序的结构中:一个`for`循环,每次循环处理一个词条。 + +另一种常见的情况,输出是输入的摘要形式,如一个倒置的文件索引。有必要在内存中建立索引结构(见 4.8),然后把它以所需的格式写入一个文件。下面的例子构造一个索引,映射字典定义的词汇到相应的每个词条❶的语意❷,已经对定义文本分词❸,并丢弃短词❹。一旦该索引建成,我们打开一个文件,然后遍历索引项,以所需的格式输出行❺。 + +```py +>>> idx = nltk.Index((defn_word, lexeme) ❶ +... for (lexeme, defn) in pairs ❷ +... for defn_word in nltk.word_tokenize(defn) ❸ +... if len(defn_word) > 3) ❹ +>>> with open("dict.idx", "w") as idx_file: +... for word in sorted(idx): +... idx_words = ', '.join(idx[word]) +... idx_line = "{}: {}".format(word, idx_words) ❺ +... print(idx_line, file=idx_file) +``` + +由此产生的文件`dict.idx`包含下面的行。(如果有更大的字典,我们希望找到每个索引条目中列出的多个语意)。 + +```py +body: sleep +cease: wake +condition: sleep +down: walk +each: walk +foot: walk +lifting: walk +mind: sleep +progress: walk +setting: walk +sleep: wake + +``` + +## 3.5 决定要包含的标注层 + +发布的语料库中所包含的信息的丰富性差别很大。语料库最低限度通常会包含至少一个声音或字形符号的序列。事情的另一面,一个语料库可以包含大量的信息,如句法结构、形态、韵律、每个句子的语义、加上段落关系或对话行为的标注。标注的这些额外的层可能正是有人执行一个特定的数据分析任务所需要的。例如,如果我们可以搜索特定的句法结构,找到一个给定的语言模式就更容易;如果每个词都标注了意义,为语言模式归类就更容易。这里提供一些常用的标注层: + +* 分词:文本的书写形式不能明确地识别它的词符。分词和规范化的版本作为常规的正式版本的补充可能是一个非常方便的资源。 +* 断句:正如我们在 3 中看到的,断句比它看上去的似乎更加困难。因此,一些语料库使用明确的标注来断句。 +* 分段:段和其他结构元素(标题,章节等)可能会明确注明。 +* 词性:文档中的每个单词的词类。 +* 句法结构:一个树状结构,显示一个句子的组成结构。 +* 浅层语义:命名实体和共指标注,语义角色标签。 +* 对话与段落:对话行为标记,修辞结构 + +不幸的是,现有的语料库之间在如何表示标注上并没有多少一致性。然而,两个大类的标注表示应加以区别。内联标注通过插入带有标注信息的特殊符号或控制序列修改原始文档。例如,为文档标注词性时,字符串`"fly"`可能被替换为字符串`"fly/NN"`来表示词`fly`在文中是名词。相比之下,对峙标注不修改原始文档,而是创建一个新的文档,通过使用指针引用原始文档来增加标注信息。例如,这个新的文档可能包含字符串`">token id=8 pos='NN'/>"`,表示 8 号词符是一个名词。(我们希望可以确保的分词本身不会变化,因为它会导致默默损坏这种引用。) + +## 3.6 标准和工具 + +一个用途广泛的语料库需要支持广泛的格式。然而,NLP 研究的前沿需要各种新定义的没有得到广泛支持的标注。一般情况下,并没有广泛使用的适当的创作、发布和使用语言数据的工具。大多数项目都必须制定它们自己的一套工具,供内部使用,这对缺乏必要的资源的其他人没有任何帮助。此外,我们还没有一个可以胜任的普遍接受的标准来表示语料库的结构和内容。没有这样的标准,就不可能有通用的工具——同时,没有可用的工具,适当的标准也不太可能被开发、使用和接受。 + +针对这种情况的一个反应就是开拓未来开发一种通用的能充分表现捕获多种标注类型(见 8 的例子)的格式。NLP 的挑战是编写程序处理这种格式的泛化。例如,如果编程任务涉及树数据,文件格式允许任意有向图,那么必须验证输入数据检查树的属性如根、连通性、无环。如果输入文件包含其他层的标注,该程序将需要知道数据加载时如何忽略它们,将树数据保存到文件时不能否定或抹杀这些层。 + +另一种反应一直是写一个一次性的脚本来操纵语料格式;这样的脚本将许多 NLP 研究人员的文件夹弄得乱七八糟。在语料格式解析工作应该只进行一次(每编程语言)的前提下,NLTK 中的语料库阅读器是更系统的方法。 + +![Images/three-layer-arch.png](Images/102675fd70e434164536c75bf7f8f043.jpg) + +图 3.2:通用格式对比通用接口 + +不是集中在一种共同的格式,我们认为更有希望开发一种共同的接口(参见`nltk.corpus`)。思考 NLP 中的一个重要的语料类型 treebanks 的情况。将短语结构树存储在一个文件中的方法很多。我们可以使用嵌套的括号、或嵌套的 XML 元素、或每行带有一个`(child-id,parent-id)`对的依赖符号、或一个 XML 版本的依赖符号等。然而,每种情况中的逻辑结构几乎是相同的。很容易设计一种共同的接口,使应用程序员编写代码使用如`children()`、`leaves()`、`depth()`等方法来访问树数据。注意这种做法来自计算机科学中已经接受的做法,即即抽象数据类型、面向对象设计、三层结构`(3.2)`。其中的最后一个——来自关系数据库领域——允许终端用户应用程序使用通用的模型(“关系模型”)和通用的语言(SQL)抽象出文件存储的特质,并允许新的文件系统技术的出现,而不会干扰到终端用户的应用。以同样的方式,一个通用的语料库接口将应用程序从数据格式隔离。 + +在此背景下,创建和发布一个新的语料库时,尽可能使用现有广泛使用的格式是权宜之计。如果这样不可能,语料库可以带有一些软件——如`nltk.corpus`模块——支持现有的接口方法。 + +## 3.7 处理濒危语言时特别注意事项 + +语言对科学和艺术的重要性体现在文化宝库包含在语言中。世界上大约 7000 种人类语言中的每一个都是丰富的,在它独特的方面,在它口述的历史和创造的传说,在它的文法结构和它的变化的词汇和它们含义中的细微差别。受威胁残余文化中的词能够区分具有科学家未知的治疗用途的植物亚种。当人们互相接触,每个人都为之前的语言提供一个独特的窗口,语言随着时间的推移而变化。世界许多地方,小的语言变化从一个镇都另一个镇,累加起来在一个半小时的车程的空间中成为一种完全不同的语言。对于其惊人的复杂性和多样性,人类语言犹如丰富多彩的挂毯随着时间和空间而伸展。 + +然而,世界上大多数语言面临灭绝。对此,许多语言学家都在努力工作,记录语言,构建这个世界语言遗产的重要方面的丰富记录。在 NLP 的领域能为这方面的努力提供什么帮助吗?开发标注器、分析器、命名实体识别等不是最优先的,通常没有足够的数据来开发这样的工具。相反,最经常提出的是需要更好的工具来收集和维护数据,特别是文本和词汇。 + +从表面看,开始收集濒危语言的文本应该是一件简单的事情。即使我们忽略了棘手的问题,如谁拥有文本,文本中包含的文化知识有关敏感性,转录仍然有很多明显的实际问题。大多数语言缺乏标准的书写形式。当一种语言没有文学传统时,拼写和标点符号的约定也没有得到很好的建立。因此,通常的做法是与文本收集一道创建一个词典,当在文本中出现新词时不断更新词典。可以使用文字处理器(用于文本)和电子表格(用于词典)来做这项工作。更妙的是,SIL 的自由语言软件 Toolbox 和 Fieldworks 对文本和词汇的创建集成提供了很好的支持。 + +当濒危语言的说话者学会自己输入文本时,一个共同的障碍就是对正确的拼写的极度关注。有一个词典大大有助于这一进程,但我们需要让查找的方法不要假设有人能确定任意一个词的引文形式。这个问题对具有复杂形态的包括前缀的语言可能是很急迫的。这种情况下,使用语义范畴标注词项,并允许通过语义范畴或注释查找是十分有益的。 + +允许通过相似的发音查找词项也是很有益的。下面是如何做到这一点的一个简单的演示。第一步是确定易混淆的字母序列,映射复杂的版本到更简单的版本。我们还可以注意到,辅音群中字母的相对顺序是拼写错误的一个来源,所以我们将辅音字母顺序规范化。 + +```py +>>> mappings = [('ph', 'f'), ('ght', 't'), ('^kn', 'n'), ('qu', 'kw'), +... ('[aeiou]+', 'a'), (r'(.)\1', r'\1')] +>>> def signature(word): +... for patt, repl in mappings: +... word = re.sub(patt, repl, word) +... pieces = re.findall('[^aeiou]+', word) +... return ''.join(char for piece in pieces for char in sorted(piece))[:8] +>>> signature('illefent') +'lfnt' +>>> signature('ebsekwieous') +'bskws' +>>> signature('nuculerr') +'nclr' +``` + +下一步,我们对词典中的所有词汇创建从特征到词汇的映射。我们可以用这为一个给定的输入词找到候选的修正(但我们必须先计算这个词的特征)。 + +```py +>>> signatures = nltk.Index((signature(w), w) for w in nltk.corpus.words.words()) +>>> signatures[signature('nuculerr')] +['anicular', 'inocular', 'nucellar', 'nuclear', 'unicolor', 'uniocular', 'unocular'] +``` + +最后,我们应该按照与原词相似程度对结果排序。通过函数`rank()`完成。唯一剩下的函数提供给用户一个简单的接口: + +```py +>>> def rank(word, wordlist): +... ranked = sorted((nltk.edit_distance(word, w), w) for w in wordlist) +... return [word for (_, word) in ranked] +>>> def fuzzy_spell(word): +... sig = signature(word) +... if sig in signatures: +... return rank(word, signatures[sig]) +... else: +... return [] +>>> fuzzy_spell('illefent') +['olefiant', 'elephant', 'oliphant', 'elephanta'] +>>> fuzzy_spell('ebsekwieous') +['obsequious'] +>>> fuzzy_spell('nucular') +['anicular', 'inocular', 'nucellar', 'nuclear', 'unocular', 'uniocular', 'unicolor'] +``` + +这仅仅是一个演示,其中一个简单的程序就可以方便的访问语言书写系统可能不规范或语言的使用者可能拼写的不是很好的上下文中的词汇数据。其他简单的 NLP 在这个领域的应用包括:建立索引以方便对数据的访问,从文本中拾取词汇表,构建词典时定位词语用法的例子,在知之甚少的数据中检测普遍或特殊模式,并在创建的数据上使用各种语言的软件工具执行专门的验证。我们将在 5 返回到其中的最后一个。 + +## 4 使用 XML + +可扩展标记语言(XML)为设计特定领域的标记语言提供了一个框架。它有时被用于表示已标注的文本和词汇资源。不同于 HTML 的标签是预定义的,XML 允许我们组建自己的标签。不同于数据库,XML 允许我们创建的数据而不必事先指定其结构,它允许我们有可选的、可重复的元素。在本节中,我们简要回顾一下 XML 的一些与表示语言数据有关的特征,并说明如何使用 Python 程序访问 XML 文件中存储的数据。 + +## 4.1 语言结构中使用 XML + +由于其灵活性和可扩展性,XML 是表示语言结构的自然选择。下面是一个简单的词汇条目的例子。 + +```py + + whale + noun + any of the larger cetacean mammals having a streamlined + body and breathing through a blowhole on the head + + +``` + +## 4.2 XML 的作用 + +我们可以用 XML 来表示许多种语言信息。然而,灵活性是要付出代价的。每次我们增加复杂性,如允许一个元素是可选的或重复的,我们对所有访问这些数据的程序都要做出更多的工作。我们也使它更难以检查数据的有效性,或使用一种 XML 查询语言来查询数据。 + +因此,使用 XML 来表示语言结构并不能神奇地解决数据建模问题。我们仍然需要解决如何结构化数据,然后用一个模式定义结构,并编写程序读取和写入格式,以及把它转换为其他格式。同样,我们仍然需要遵循一些有关数据规范化的标准原则。这是明智的,可以避免相同信息的重复复制,所以当只有一个副本变化时,不会导致数据不一致。例如,交叉引用表示为`headword>/xref>`将重复存储一些其他词条的核心词,如果在其他位置的字符串的副本被修改,链接就会被打断。信息类型之间存在的依赖关系需要建模,使我们不能创建没有根的元素。例如,如果 sense 的定义不能作为词条独立存在,那么`sense`就要嵌套在`entry`元素中。多对多关系需要从层次结构中抽象出来。例如,如果一个单词可以有很多对应的`senses`,一个`sense`可以有几个对应的单词,而单词和`senses`都必须作为`(word, sense)`对的列表分别枚举。这种复杂的结构甚至可以分割成三个独立的 XML 文件。 + +正如我们看到的,虽然 XML 提供了一个格式方便和用途广泛的工具,但它不是能解决一切问题的灵丹妙药。 + +## 4.3 `ElementTree`接口 + +Python 的`ElementTree`模块提供了一种方便的方式访问存储在 XML 文件中的数据。`ElementTree`是 Python 标准库(自从 Python 2.5)的一部分,也作为 NLTK 的一部分提供,以防你在使用 Python 2.4。 + +我们将使用 XML 格式的莎士比亚戏剧集来说明`ElementTree`的使用方法。让我们加载 XML 文件并检查原始数据,首先在文件的顶部❶,在那里我们看到一些 XML 头和一个名为`play.dtd`的模式,接着是根元素`PLAY`。我们从 Act 1❷再次获得数据。(输出中省略了一些空白行。) + +```py +>>> merchant_file = nltk.data.find('corpora/shakespeare/merchant.xml') +>>> raw = open(merchant_file).read() +>>> print(raw[:163]) ❶ + + + + +The Merchant of Venice +>>> print(raw[1789:2006]) ❷ +ACT I +SCENE I. Venice. A street. +Enter ANTONIO, SALARINO, and SALANIO + +ANTONIO +In sooth, I know not why I am so sad: +``` + +我们刚刚访问了作为一个字符串的 XML 数据。正如我们看到的,在 Act 1 开始处的字符串包含 XML 标记`TITLE`、`SCENE`、`STAGEDIR`等。 + +下一步是作为结构化的 XML 数据使用`ElementTree`处理文件的内容。我们正在处理一个文件(一个多行字符串),并建立一棵树,所以方法的名称是`parse`❶并不奇怪。变量`merchant`包含一个 XML 元素`PLAY`❷。此元素有内部结构;我们可以使用一个索引来得到它的第一个孩子,一个`TITLE`元素❸。我们还可以看到该元素的文本内容:戏剧的标题❹。要得到所有的子元素的列表,我们使用`getchildren()`方法❺。 + +```py +>>> from xml.etree.ElementTree import ElementTree +>>> merchant = ElementTree().parse(merchant_file) ❶ +>>> merchant + # [_element-play] +>>> merchant[0] + # [_element-title] +>>> merchant[0].text +'The Merchant of Venice' # [_element-text] +>>> merchant.getchildren() ❺ +[, , +, , +, , +, , +] +``` + +这部戏剧由标题、角色、一个场景的描述、字幕和五幕组成。每一幕都有一个标题和一些场景,每个场景由台词组成,台词由行组成,有四个层次嵌套的结构。让我们深入到第四幕: + +```py +>>> merchant[-2][0].text +'ACT IV' +>>> merchant[-2][1] + +>>> merchant[-2][1][0].text +'SCENE I. Venice. A court of justice.' +>>> merchant[-2][1][54] + +>>> merchant[-2][1][54][0] + +>>> merchant[-2][1][54][0].text +'PORTIA' +>>> merchant[-2][1][54][1] + +>>> merchant[-2][1][54][1].text +"The quality of mercy is not strain'd," +``` + +注意 + +**轮到你来**:对语料库中包含的其他莎士比亚戏剧,如《罗密欧与朱丽叶》或《麦克白》,重复上述的一些方法;方法列表请参阅`nltk.corpus.shakespeare.fileids()`。 + +虽然我们可以通过这种方式访问整个树,使用特定名称查找子元素会更加方便。回想一下顶层的元素有几种类型。我们可以使用`merchant.findall('ACT')`遍历我们感兴趣的类型(如幕)。下面是一个做这种特定标记在每一个级别的嵌套搜索的例子: + +```py +>>> for i, act in enumerate(merchant.findall('ACT')): +... for j, scene in enumerate(act.findall('SCENE')): +... for k, speech in enumerate(scene.findall('SPEECH')): +... for line in speech.findall('LINE'): +... if 'music' in str(line.text): +... print("Act %d Scene %d Speech %d: %s" % (i+1, j+1, k+1, line.text)) +Act 3 Scene 2 Speech 9: Let music sound while he doth make his choice; +Act 3 Scene 2 Speech 9: Fading in music: that the comparison +Act 3 Scene 2 Speech 9: And what is music then? Then music is +Act 5 Scene 1 Speech 23: And bring your music forth into the air. +Act 5 Scene 1 Speech 23: Here will we sit and let the sounds of music +Act 5 Scene 1 Speech 23: And draw her home with music. +Act 5 Scene 1 Speech 24: I am never merry when I hear sweet music. +Act 5 Scene 1 Speech 25: Or any air of music touch their ears, +Act 5 Scene 1 Speech 25: By the sweet power of music: therefore the poet +Act 5 Scene 1 Speech 25: But music for the time doth change his nature. +Act 5 Scene 1 Speech 25: The man that hath no music in himself, +Act 5 Scene 1 Speech 25: Let no such man be trusted. Mark the music. +Act 5 Scene 1 Speech 29: It is your music, madam, of the house. +Act 5 Scene 1 Speech 32: No better a musician than the wren. +``` + +不是沿着层次结构向下遍历每一级,我们可以寻找特定的嵌入的元素。例如,让我们来看看演员的顺序。我们可以使用频率分布看看谁最能说: + +```py +>>> from collections import Counter +>>> speaker_seq = [s.text for s in merchant.findall('ACT/SCENE/SPEECH/SPEAKER')] +>>> speaker_freq = Counter(speaker_seq) +>>> top5 = speaker_freq.most_common(5) +>>> top5 +[('PORTIA', 117), ('SHYLOCK', 79), ('BASSANIO', 73), +('GRATIANO', 48), ('LORENZO', 47)] +``` + +我们也可以查看对话中谁跟着谁的模式。由于有 23 个演员,我们需要首先使用 3 中描述的方法将“词汇”减少到可处理的大小。 + +```py +>>> from collections import defaultdict +>>> abbreviate = defaultdict(lambda: 'OTH') +>>> for speaker, _ in top5: +... abbreviate[speaker] = speaker[:4] +... +>>> speaker_seq2 = [abbreviate[speaker] for speaker in speaker_seq] +>>> cfd = nltk.ConditionalFreqDist(nltk.bigrams(speaker_seq2)) +>>> cfd.tabulate() + ANTO BASS GRAT OTH PORT SHYL +ANTO 0 11 4 11 9 12 +BASS 10 0 11 10 26 16 +GRAT 6 8 0 19 9 5 + OTH 8 16 18 153 52 25 +PORT 7 23 13 53 0 21 +SHYL 15 15 2 26 21 0 +``` + +忽略 153 的条目,因为是前五位角色(标记为`OTH`)之间相互对话,最大的值表示 Othello 和 Portia 的相互对话最多。 + +## 4.4 使用`ElementTree`访问 Toolbox 数据 + +在 4 中,我们看到了一个访问 Toolbox 数据的简单的接口,Toolbox 数据是语言学家用来管理数据的一种流行和行之有效的格式。这一节中,我们将讨论以 Toolbox 软件所不支持的方式操纵 Toolbox 数据的各种技术。我们讨论的方法也可以应用到其他记录结构化数据,不必管实际的文件格式。 + +我们可以用`toolbox.xml()`方法来访问 Toolbox 文件,将它加载到一个`elementtree`对象中。此文件包含一个巴布亚新几内亚罗托卡特语的词典。 + +```py +>>> from nltk.corpus import toolbox +>>> lexicon = toolbox.xml('rotokas.dic') +``` + +有两种方法可以访问`lexicon`对象的内容:通过索引和通过路径。索引使用熟悉的语法;`lexicon[3]`返回 3 号条目(实际上是从 0 算起的第 4 个条目);`lexicon[3][0]`返回它的第一个字段: + +```py +>>> lexicon[3][0] + +>>> lexicon[3][0].tag +'lx' +>>> lexicon[3][0].text +'kaa' +``` + +第二种方式访问 lexicon 对象的内容是使用路径。`lexicon`是一系列`record`对象,其中每个都包含一系列字段对象,如`lx`和`ps`。使用路径`record/lx`,我们可以很方便地解决所有的语意。这里,我们使用`findall()`函数来搜索路径`record/lx`的所有匹配,并且访问该元素的文本内容,将其规范化为小写。 + +```py +>>> [lexeme.text.lower() for lexeme in lexicon.findall('record/lx')] +['kaa', 'kaa', 'kaa', 'kaakaaro', 'kaakaaviko', 'kaakaavo', 'kaakaoko', +'kaakasi', 'kaakau', 'kaakauko', 'kaakito', 'kaakuupato', ..., 'kuvuto'] +``` + +让我们查看 XML 格式的 Toolbox 数据。`ElementTree`的`write()`方法需要一个文件对象。我们通常使用 Python 内置的`open()`函数创建。为了屏幕上显示输出,我们可以使用一个特殊的预定义的文件对象称为`stdout`❶ (标准输出),在 Python 的`sys`模块中定义的。 + +```py +>>> import sys +>>> from nltk.util import elementtree_indent +>>> from xml.etree.ElementTree import ElementTree +>>> elementtree_indent(lexicon) +>>> tree = ElementTree(lexicon[3]) +>>> tree.write(sys.stdout, encoding='unicode') ❶ + + kaa + N + MASC + isi + cooking banana + banana bilong kukim + itoo + FLORA +
12/Aug/2005
+ Taeavi iria kaa isi kovopaueva kaparapasia. + Taeavi i bin planim gaden banana bilong kukim tasol long paia. + Taeavi planted banana in order to cook it. +
+``` + +## 4.5 格式化条目 + +我们可以使用在前一节看到的同样的想法生成 HTML 表格而不是纯文本。这对于将 Toolbox 词汇发布到网络上非常有用。它产生 HTML 元素``,``(表格的行)和`
`(表格数据)。 + +```py +>>> html = "\n" +>>> for entry in lexicon[70:80]: +... lx = entry.findtext('lx') +... ps = entry.findtext('ps') +... ge = entry.findtext('ge') +... html += " \n" % (lx, ps, ge) +>>> html += "
%s%s%s
" +>>> print(html) + + + + + + + + + + + +
kakae???small
kakaeCLASSchild
kakaeviraADVsmall-like
kakapikoa???small
kakapikotoNnewborn baby
kakapuVplace in sling for purpose of carrying
kakapuaNsling for lifting
kakaraNarm band
KakarapaiaNvillage name
kakarauNfrog
+``` + +## 5 使用 Toolbox 数据 + +鉴于 Toolbox 在语言学家中十分流行,我们将讨论一些使用 Toolbox 数据的进一步的方法。很多在前面的章节讲过的方法,如计数、建立频率分布、为同现制表,这些都可以应用到 Toolbox 条目的内容上。例如,我们可以为每个条目计算字段的平均个数: + +```py +>>> from nltk.corpus import toolbox +>>> lexicon = toolbox.xml('rotokas.dic') +>>> sum(len(entry) for entry in lexicon) / len(lexicon) +13.635... +``` + +在本节中我们将讨论记录语言学的背景下出现的都不被 Toolbox 软件支持的两个任务。 + +## 5.1 为每个条目添加一个字段 + +添加一个自动从现有字段派生出的新的字段往往是方便的。这些字段经常使搜索和分析更加便捷。例如,在 5.1 中我们定义了一个函数`cv()`,将辅音和元音的字符串映射到相应的 CV 序列,即`kakapua`将映射到`CVCVCVV`。这种映射有四个步骤。首先,将字符串转换为小写,然后将所有非字母字符`[^a-z]`用下划线代替。下一步,将所有元音替换为`V`。最后,所有不是`V`或下划线的必定是一个辅音,所以我们将它替换为`C`。现在,我们可以扫描词汇,在每个`lx`字段后面添加一个新的`cv`字段。5.1 显示了它对一个特定条目上做的内容;注意输出的最后一行表示新的`cv`字段。 + +```py +from xml.etree.ElementTree import SubElement + +def cv(s): + s = s.lower() + s = re.sub(r'[^a-z]', r'_', s) + s = re.sub(r'[aeiou]', r'V', s) + s = re.sub(r'[^V_]', r'C', s) + return (s) + +def add_cv_field(entry): + for field in entry: + if field.tag == 'lx': + cv_field = SubElement(entry, 'cv') + cv_field.text = cv(field.text) +``` + +注意 + +如果一个 Toolbox 文件正在不断更新,`code-add-cv-field`中的程序将需要多次运行。可以修改`add_cv_field()`来修改现有条目的内容。使用这样的程序为数据分析创建一个附加的文件比替换手工维护的源文件要安全。 + +## 5.2 验证 Toolbox 词汇 + +Toolbox 格式的许多词汇不符合任何特定的模式。有些条目可能包括额外的字段,或以一种新的方式排序现有字段。手动检查成千上万的词汇条目是不可行的。我们可以在`Counter`的帮助下很容易地找出频率异常的字段序列: + +```py +>>> from collections import Counter +>>> field_sequences = Counter(':'.join(field.tag for field in entry) for entry in lexicon) +>>> field_sequences.most_common() +[('lx:ps:pt:ge:tkp:dt:ex:xp:xe', 41), ('lx:rt:ps:pt:ge:tkp:dt:ex:xp:xe', 37), +('lx:rt:ps:pt:ge:tkp:dt:ex:xp:xe:ex:xp:xe', 27), ('lx:ps:pt:ge:tkp:nt:dt:ex:xp:xe', 20), ...] +``` + +检查完高频字段序列后,我们可以设计一个词汇条目的上下文无关语法。在 5.2 中的语法使用我们在 8 看到的 CFG 格式。这样的语法模型隐含 Toolbox 条目的嵌套结构,建立一个树状结构,树的叶子是单独的字段名。最后,我们遍历条目并报告它们与语法的一致性,如 5.2 所示。那些被语法接受的在前面加一个`'+'`❶,那些被语法拒绝的在前面加一个`'-'`❷。在开发这样一个文法的过程中,它可以帮助过滤掉一些标签❸。 + +```py +grammar = nltk.CFG.fromstring(''' + S -> Head PS Glosses Comment Date Sem_Field Examples + Head -> Lexeme Root + Lexeme -> "lx" + Root -> "rt" | + PS -> "ps" + Glosses -> Gloss Glosses | + Gloss -> "ge" | "tkp" | "eng" + Date -> "dt" + Sem_Field -> "sf" + Examples -> Example Ex_Pidgin Ex_English Examples | + Example -> "ex" + Ex_Pidgin -> "xp" + Ex_English -> "xe" + Comment -> "cmt" | "nt" | + ''') + +def validate_lexicon(grammar, lexicon, ignored_tags): + rd_parser = nltk.RecursiveDescentParser(grammar) + for entry in lexicon: + marker_list = [field.tag for field in entry if field.tag not in ignored_tags] + if list(rd_parser.parse(marker_list)): + print("+", ':'.join(marker_list)) ❶ + else: + print("-", ':'.join(marker_list)) ❷ +``` + +另一种方法是用一个词块分析器`(7)`,因为它能识别局部结构并报告已确定的局部结构,会更加有效。在 5.3 中我们为词汇条目建立一个词块语法,然后解析每个条目。这个程序的输出的一个示例如 5.4 所示。 + +```py +grammar = r""" + lexfunc: {(*)*} + example: {*} + sense: {***} + record: {+
} + """ +``` + +![Images/iu-mien.png](Images/17af2b7a4de652abd5c2c71a94cc1c7b.jpg) + +图 5.4:一个词条的 XML 表示,对 Toolbox 记录的词块分析的结果 + +## 6 使用 OLAC 元数据描述语言资源 + +NLP 社区的成员的一个共同需要是发现具有很高精度和召回率的语言资源。数字图书馆社区目前已开发的解决方案包括元数据聚集。 + +## 6.1 什么是元数据? + +元数据最简单的定义是“关于数据的结构化数据”。元数据是对象或资源的描述信息,无论是物理的还是电子的。而术语“元数据”本身是相对较新的,只要收集的信息被组织起来,元数据下面隐含的意义却一直在被使用。图书馆目录是一种行之有效的元数据类型;它们已经作为资源管理和发现工具有几十年了。元数据可以由“手工”产生也可以使用软件自动生成。 + +都柏林核心元数据倡议于 1995 年开始开发在网络上进行资源发现的约定。都柏林核心元数据元素表示一个广泛的、跨学科一致的元素核心集合,这些元素核心集合有可能对资源发现有广泛作用。都柏林核心由 15 个元数据元素组成,其中每个元素都是可选的和可重复的,它们是:标题,创建者,主题,描述,发布者,参与者,日期,类型,格式,标识符,来源,语言,关系,覆盖范围和版权。此元数据集可以用来描述数字或传统的格式中存放的资源。 + +开放档案倡议(OAI)提供了一个跨越数字化的学术资料库的共同框架,不考虑资源的类型,包括文档,资料,软件,录音,实物制品,数码代替品等等。每个库由一个网络访问服务器提供归档项目的公共访问。每个项目都有一个唯一的标识符,并与都柏林核心元数据记录(也可以是其他格式的记录)关联。OAI 为元数据搜索服务定义了一个协议来“收获”资源库的内容。 + +## 6.2 OLAC:开放语言档案社区 + +开放语言档案社区(OLAC)是正在创建的一个世界性语言资源的虚拟图书馆的机构和个人的国际伙伴关系:(i)制订目前最好的关于语言资源的数字归档实施的共识,(ii)开发存储和访问这些资源的互操作信息库和服务的网络。OLAC 在网上的主页是`http://www.language-archives.org/`。 + +OLAC 元数据是描述语言资源的标准。通过限制某些元数据元素的值为使用受控词表中的术语,确保跨库描述的统一性。OLAC 元数据可用于描述物理和数字格式的数据和工具。OLAC 元数据扩展了都柏林核心元数据集(一个描述所有类型的资源被广泛接受的标准)。对这个核心集,OLAC 添加了语言资源的基本属性,如主题语言和语言类型。下面是一个完整的 OLAC 记录的例子: + +```py + + + A grammar of Kayardild. With comparative notes on Tangkic. + Evans, Nicholas D. + Kayardild grammar + Kayardild + English + Kayardild Grammar (ISBN 3110127954) + Berlin - Mouton de Gruyter + Nicholas Evans + hardcover, 837 pages + related to ISBN 0646119966 + Australia + + Text + + +``` + +## 6.3 传播语言资源 + +语言数据财团存放 NLTK 数据存储库,一个开发的归档,社区成员可以上传语料库和保存好的模型。这些资源可以使用 NLTK 的下载工具方便地访问。 + +## 7 小结 + +* 大多数语料库中基本数据类型是已标注的文本和词汇。文本有时间结构,而词汇有记录结构。 +* 语料库的生命周期,包括数据收集、标注、质量控制以及发布。发布后生命周期仍然继续,因为语料库会在研究过程中被修改和丰富。 +* 语料库开发包括捕捉语言使用的代表性的样本与使用任何一个来源或文体都有足够的材料之间的平衡;增加变量的维度通常由于资源的限制而不可行。 +* XML 提供了一种有用的语言数据的存储和交换格式,但解决普遍存在的数据建模问题没有捷径。 +* Toolbox 格式被广泛使用在语言记录项目中;我们可以编写程序来支持 Toolbox 文件的维护,将它们转换成 XML。 +* 开放语言档案社区(OLAC)提供了一个用于记录和发现语言资源的基础设施。 + +## 8 深入阅读 + +本章的附加材料发布在`http://nltk.org/`,包括网络上免费提供的资源的链接。 + +语言学语料库的首要来源是*语言数据联盟*和*欧洲语言资源局*,两者都有广泛的在线目录。本书中提到的主要语料库的细节也有介绍:美国国家语料库(Reppen, Ide, & Suderman, 2005)、英国国家语料库(BNC, 1999),Thesaurus Linguae Graecae(TLG, 1999)、儿童语言数据交换系统(CHILDES)(MacWhinney, 1995)和 TIMIT(S., Lamel, & William, 1986)。 + +计算语言学协会定期组织研讨会发布论文集,它的两个特别兴趣组:SIGWAC 和 SIGANN;前者推动使用网络作为语料,发起去除 HTML 标记的 CLEANEVAL 任务;后者鼓励对语言注解的互操作性的努力。 + +(Buseman, Buseman, & Early, 1996)提供 Toolbox 数据格式的全部细节,最新的发布可以从`http://www.sil.org/computing/toolbox/`免费下载。构建一个 Toolbox 词典的过程指南参见`http://www.sil.org/computing/ddp/`。我们在 Toolbox 上努力的更多的例子记录在(Tamanji, Hirotani, & Hall, 1999)和(Robinson, Aumann, & Bird, 2007)。(Bird & Simons, 2003)调查了语言数据管理的几十个其他工具。也请参阅关于文化遗产数据的语言技术的 LaTeCH 研讨会的论文集。 + +有很多优秀的 XML 资源(如`http://zvon.org/`)和编写 Python 程序处理 XML 的资源。许多编辑器都有 XML 模式。XML 格式的词汇信息包括 OLIF`http://www.olif.net/`和 LIFT`http://code.google.com/p/lift-standard/`。 + +对于语言标注软件的调查,见`http://www.ldc.upenn.edu/annotation/`的*语言标注页*。对峙注解最初的提出是(Thompson & McKelvie, 1997)。语言标注的一个抽象的数据模型称为“标注图”在(Bird & Liberman, 2001)提出。语言描述的一个通用本体(GOLD)记录在`http://www.linguistics-ontology.org/`中。 + +有关规划和建设语料库的指导,请参阅(Meyer, 2002)和(Farghaly, 2003) 。关于标注者之间一致性得分的方法的更多细节,见(Artstein & Poesio, 2008)和(Pevzner & Hearst, 2002)。 + +Rotokas 数据由 Stuart Robinson 提供,勉方言数据由 Greg Aumann 提供。 + +有关开放语言档案社区的更多信息,请访问`http://www.language-archives.org/`,或参见(Simons & Bird, 2003)。 + +## 9 练习 + +1. ◑ 在 5.1 中新字段出现在条目底部。修改这个程序使它就在`lx`字段后面插入新的子元素。(提示:使用`Element('cv')`创建新的`cv`字段,分配给它一个文本值,然后使用父元素的`insert()`方法。) + +2. ◑ 编写一个函数,从一个词汇条目删除指定的字段。(我们可以在把数据给别人之前用它做些清洁,如删除包含无关或不确定的内容的字段。) + +3. ◑ 写一个程序,扫描一个 HTML 字典文件,找出具有非法词性字段的条目,并报告每个条目的*核心词*。 + +4. ◑ 写一个程序,找出所有出现少于 10 次的词性(`ps`字段)。或许有打字错误? + +5. ◑ We saw a method for discovering cases of whole-word reduplication. Write a function to find words that may contain partial reduplication. Use the `re.search()` method, and the following regular expression: `(..+)\1` + +6. ◑ 我们看到一个增加`cv`字段的方法。一件有趣的问题是当有人修改的`lx`字段的内容时,保持这个字段始终最新。为这个程序写一个版本,添加`cv`字段,取代所有现有的`cv`字段。 + +7. ◑ 写一个函数,添加一个新的字段`syl`,计数一个词中的音节数。 + +8. ◑ 写一个函数,显示一个词位的完整条目。当词位拼写错误时,它应该显示拼写最相似的词位的条目。 + +9. ◑ 写一个函数,从一个词典中找出最频繁的连续字段对(如`ps`后面往往是`pt`)。(这可以帮助我们发现一些词条的结构。) + +10. ◑ 使用办公软件创建一个电子表格,每行包含一个词条,包括一个中心词,词性和注释。以 CSV 格式保存电子表格。写 Python 代码来读取 CSV 文件并以 Toolbox 格式输出,使用`lx`表示中心词,`ps`表示词性,`gl`表示注释。 + +11. ◑ 在`nltk.Index`帮助下,索引莎士比亚的戏剧中的词。产生的数据结构允许按单个词查找,如`music`,返回演出、场景和台词的引用的列表,`[(3, 2, 9), (5, 1, 23), ...]`的形式,其中`(3, 2, 9)`表示第 3 场演出场景 2 台词 9。 + +12. ◑ 构建一个条件频率分布记录《威尼斯商人》中每段台词的词长,以角色名字为条件,如`cfd['PORTIA'][12]`会给我们 Portia 的 12 个词的台词的数目。 + +13. ★ 获取 CSV 格式的比较词表,写一个程序,输出相互之间至少有三个编辑距离的同源词。 + +14. ★ 建立一个出现在例句的词位的索引。假设对于一个给定条目的词位是`w`。然后为这个条目添加一个单独的交叉引用字段`xrf`,引用其它有例句包含`w`的条目的中心词。对所有条目做这个,结果保存为 Toolbox 格式文件。 + +15. ◑ 写一个递归函数将任意树转换为对应的 XML,其中非终结符不能表示成 XML 元素,叶子表示文本内容,如: + + ```py + >S> + >NP type="SBJ"> + >NP> + >NNP>Pierre>/NNP> + >NNP>Vinken>/NNP> + >/NP> + >COMMA>,>/COMMA> + + ``` \ No newline at end of file diff --git a/docs/nlp-py-2e/12.md b/docs/nlp-py-2e/12.md new file mode 100644 index 0000000000000000000000000000000000000000..7e1ee224bbf5f3b6fb80ea7cce364b759dec60e2 --- /dev/null +++ b/docs/nlp-py-2e/12.md @@ -0,0 +1,78 @@ +# 后记:语言的挑战 + +自然语言抛出一些有趣的计算性挑战。我们已经在前面的章节探讨过许多这样的挑战,包括分词、标注、分类、信息提取和建立句法和语义表示。你现在应该已经准备好操作大型数据集,来创建语言现象的强健模型,并将它们扩展到实际语言技术的组件中。我们希望自然语言工具包(NLTK)对于开放令人振奋的实用自然语言处理的的努力到比以前更广泛的受众已经起作用。 + +尽管已经取得前面的所有成果,语言呈现给我们的远远不是计算上的临时挑战。考虑下面的句子,它们证实语言的丰富性: + +``` +(1) + +a. Overhead the day drives level and grey, hiding the sun by a flight of grey spears. (William Faulkner, *As I Lay Dying*, 1935) + +b. When using the toaster please ensure that the exhaust fan is turned on. (sign in dormitory kitchen) + +c. Amiodarone weakly inhibited CYP2C9, CYP2D6, and CYP3A4-mediated activities with Ki values of 45.1-271.6 μM (Medline, PMID: 10718780) + +d. Iraqi Head Seeks Arms (spoof news headline) + +e. The earnest prayer of a righteous man has great power and wonderful results. (James 5:16b) + +f. Twas brillig, and the slithy toves did gyre and gimble in the wabe (Lewis Carroll, *Jabberwocky*, 1872) + +g. There are two ways to do this, AFAIK :smile: (internet discussion archive) +``` + +语言丰富性的其他证据是以语言为工作中心的学科的浩瀚阵容。一些明显的学科包括翻译、文学批评、哲学、人类学和心理学。许多不太明显的学科研究语言的使用,包括法律、诠释学、辩论术、电话学、教育学、考古学、密码分析学及言语病理学。它们分别应用不同的方法来收集观察资料、发展理论和测试假设。它们都有助于加深我们对语言和表现在语言中的智能的理解。 + +鉴于语言的复杂性和从不同的角度研究它的广泛的价值,很显然这里我们仅仅已经触及了表面。此外,在 NLP 本身,有许多我们没有提到的重要方法和应用。 + +在我们的后记中,我们将以更宽广的视角看待 NLP,包括它的基础和你可能想要探索的进一步的方向。一些主题还没有得到 NLTK 很好的支持,你可能想通过为工具包贡献新的软件和数据来修正这些问题,。 + +## 语言处理与符号处理 + +以计算方式处理自然语言的真正观念脱胎于一个研究项目,可以追溯到 1900 年代早期,使用逻辑重建数学推理,最清楚地表明是在 Frege、Russell、Wittgenstein、Tarski、Lambek 和 Carnap 的工作中。这项工作导致语言作为可以自动处理的形式化系统的概念。三个后来的发展奠定了自然语言处理的基础。第一个是形式语言理论。它定义一个语言为被一类自动机接受的字符串的集合,如上下文无关语言和下推自动机,并提供计算句法的支柱。 + +第二个发展是符号逻辑。它提供一个捕捉选定的自然语言的表达的逻辑证明的有关方面的形式化方法。符号逻辑中的形式化演算提供一种语言的句法和推理规则,并可能在一套理论模型中对规则进行解释;例子是命题逻辑和一阶逻辑。给定这样的演算和一个明确的句法和语义,通过将自然语言的表达翻译成形式化演算的表达式,联系语义与自然语言的表达成为可能。例如,如果我们翻译`John saw Mary`为公式`saw(j,m)`,我们(或明或暗地)将英语动词`saw`解释为一个二元关系,而`John`和`Mary`表示个体元素。更多的一般性的表达式如`All birds fly`需要量词,在这个例子中是`∀`,意思是对所有的:`∀x (bird(x) → fly(x))`。逻辑的使用提供了技术性的机制处理推理,而推理是语言理解的重要组成部分。 + +另一个密切相关的发展是组合原理,即一个复杂表达式的意思由它的各个部分的意思和它们的组合模式组成`(10)`。这一原理提供了句法和语义之间的有用的对应,即一个复杂的表达式的含义可以递归的计算。考虑句子`It is not true that p`,其中`p`是一个命题。我们可以表示这个句子的意思为`not(p)`。同样,我们可以表示`John saw Mary`的意思为`saw(j, m)`。现在,我们可以使用上述信息递归地计算`It is not true that John saw Mary`的表示,得到`not(saw(j,m))`。 + +刚刚简要介绍的方法都有一个前提,自然语言计算关键依赖于操纵符号表示的规则。NLP 发展的一个特定时期,特别是 1980 年代,这个前提为语言学家和 NLP 从业人员提供了一个共同的起点,导致一种被称为基于归一(基于特征)语法的形式化语法家族(参见 9),也导致了在 Prolog 编程语言上实现 NLP 应用。虽然基于语法的自然语言处理仍然是一个研究的重要领域,由于多种因素在过去的 15-20 年它已经有些黯然失色。一个显著的影响因素来自于自动语音识别。虽然早期的语音处理采用一个模拟一类基于规则的音韵处理的模型,典型的如《Sound Pattern of English》(Chomsky & Halle, 1968),结果远远不能够解决实时的识别实际的讲话这样困难的问题。相比之下,包含从大量语音数据中学习的模式的系统明显更准确、高效和稳健的。此外,言语社区发现建立对常见的测试数据的性能的定量测量的共享资源对建立更好的系统的过程有巨大帮助。最终,大部分的 NLP 社区拥抱面向数据密集型的语言处理,配合机器学习技术和评价为主导的方法的越来越多地使用。 + +## 当代哲学划分 + +在上一节中描述的自然语言处理的两种方法的对比与在西方哲学的启蒙时期出现的关于理性主义与经验主义和现实主义与理想主义的早期形而上学的辩论有关。这些辩论出现在反对一切知识的来源被认为是神的启示的地方的正统思想的背景下。在十七和十八世纪期间,哲学家认为人类理性或感官经验优先了启示。笛卡尔和莱布尼兹以及其他人采取了理性的立场,声称所有的真理来源于人类思想,从出生起在我们的脑海中就植入的“天赋观念”的存在。例如,他们认为欧几里德几何原理是使用人的理性制定的,而不是超自然的启示或感官体验的结果。相比之下,洛克和其他人采取了经验主义的观点,认为我们的知识的主要来源是我们的感官经验,人类理性在翻译这些经验上起次要作用。这一立场经常引用的证据是伽利略的发现——基于对行星运动的仔细观察——太阳系是以太阳为中心,而不是地球为中心。在语言学的背景下,本次辩论导致以下问题:人类语言经验与我们先天的“语言能力”各自多大程度上作为我们的语言知识的基础?在 NLP 中这个问题表现为在计算模型构建中语料库数据与语言学反省之间的优先级。 + +还有一个问题,在现实主义和理想主义之间的辩论中被奉若神明的是理论结构的形而上学的地位。康德主张现象与我们可以体验的表现以及不能直接被认识的“事情本身”之间的相互区别。语言现实主义者会认为如名词短语这样的理论建构是一个现实世界的实体,是人类看法和理由的独立存在,它实际*导致*观测到的语言现象。另一方面,语言理想主义者会说名词短语以及如语义表示这样更抽象的结构本质上无法观察到,只是担任有用的虚构的角色。语言学家写理论的方式往往与现实主义的立场相违背,而 NLP 从业人员占据中立地位,不然就倾向于理想主义立场。因此,在 NLP 中,如果一个理论的抽象导致一个有用的结果往往就足够了;不管这个结果是否揭示了任何人类语言处理。 + +这些问题今天仍然存在,表现为符号与统计方法、深层与浅层处理、二元与梯度分类以及科学与工程目标之间的区别。然而,这样的反差现在已经非常细微,辩论不再像从前那样是两极化。事实上,大多数的讨论——大部分的进展——都包含一个“平衡协调”。例如,一种中间立场是假设人类天生被赋予基于类比和记忆的学习方法(弱理性主义),并使用这些方法确定他们的感官语言经验(经验主义)的有意义的模式。 + +整本书中,我们已经看到了这种方法的很多例子。每次语料统计指导上下文无关语法产生式的选择,统计方法就会给出符号模型,即“语法工程”。每次使用基于规则的方法创建的一个语料被用来作为特征来源训练统计语言模型时,符号方法都会给出统计模型,即“语法推理”。圆圈是封闭的。 + +## NLTK 路线图 + +自然语言工具包是在不断发展的,随着人们贡献代码而不断扩大。NLP 和语言学的一些领域(还)没有得到 NLTK 很好的支持,特别欢迎在这些领域的贡献。有关这本书的出版之后的开发新闻,请查阅`http://nltk.org/`。 + +| 音韵学和形态学: | 研究声音模式和文字结构的计算方法,通常用一个有限状态机工具包。如不规则词形变化和非拼接形态这样的现象使用我们一直在学习的字符串处理方法很难解决。该技术面临的挑战不仅仅是连接 NLTK 到一个高性能的有限状态机工具包,而且要避免词典数据的重复以及链接形态分析器和语法分析器所需形态学特征。 | +| --- | --- | +| 高性能模块: | 一些 NLP 任务的计算量太大,使纯 Python 实现不可行。然而,在某些情况下,耗时只出现在训练模型期间,不是在标注输入期间使用它们。NLTK 中的包系统提供了一个方便的方式来发布训练好的模型,即使那些使用不能随意发布的语料库训练的模型。替代方法是开发高性能的机器学习工具的 Python 接口,或通过使用类似与 MapReduce 的并行编程技术扩展 Python 的能力。 | +| 词汇语义学: | 这是一个充满活力的领域,目前的研究大多围绕词典、本体、多词表达式等的继承模型,大都在现在的 NLTK 的范围之外。一个保守的目标是从丰富的外部存储获得词汇信息,以支持词义消歧、解析和语义解释等任务。 | +| 自然语言生成: | 从含义的内在表示生产连贯的文本是 NLP 的重要组成部分;用于 NLP 的基于归一的方法已经在 NLTK 中开发,在这一领域做出更大的贡献还有限制。 | +| 语言实地调查: | 语言学家面临的一个重大挑战是记录数以千计的濒危语言,这项工作产生大量异构且快速变化的数据。更多的实地调查的数据格式,包括行间的文本格式和词汇交换格式,在 NLTK 中得到支持,帮助语言学家维护和分析这些数据,解放他们,使他们能在数据提炼中花费尽可能多的时间。 | +| 其他语言: | 对英语以外的语言的 NLP 改进支持包括两方面的工作:获准发布更多 NLTK 中的收集的语料库;写特定语言的 HOWTO 文件发布到`http://nltk.org/howto`,说明 NLTK 中的使用,讨论语言相关的 NLP 问题,包括字符编码、分词、形态。一个特定语言专长的 NLP 研究人员可以安排翻译这本书,并在 NLTK 的网站上保存一个副本;这将不仅仅是翻译讨论的内容,而要使用目标语言的数据提供等效的可行的例子,一项不平凡的事业。 | +| NLTK-Contrib: | 许多 NLTK 中的核心组件都由 NLP 社区成员贡献,它们最初被安置在 NLTK 中的“Contrib”包,`nltk_contrib`。对添加到这个包中的软件的唯一要求是它必须用 Python 编写,与 NLP 有关,并给予与 NLTK 中其他软件一样的开源许可。不完善的软件也是值得欢迎的,随着时间的推移可能会被 NLP 社区的其他成员改进。 | +| 教材: | 从 NLTK 开发的最初起,教材一直伴随着软件逐渐扩大填补这本书,也加上大量的网上材料。我们希望弄清楚提供这些材料包括:幻灯片、习题集、解答集、我们所覆盖的主题更详细的理解的教员的名字,并通知作者,我们可以为他们在`http://nltk.org/`上做链接。具有特殊价值的材料,帮助 NLP 成为计算机科学和语言学系的本科主流课程,或者使 NLP 在二级本科课程中可以获得,在那里对语言、文学、计算机科学以及信息技术课程中的计算内容有明显的限制。 | +| 只是一个工具包: | 在序言中已经指出,NLTK 是一个工具包,而不是一个系统。在 NLTK、Python、其他 Python 库、外部 NLP 的工具和格式的接口集成中会有很多问题需要解决。 | + +## Envoi... + +语言学家有时会被问到他们说多少种语言,不得不解释说这一领域实际上关注语言间共享的抽象结构的研究,一种比学说尽可能多的语言更深刻更难以捉摸的研究。同样的,计算机科学家有时会被问到他们懂多少种编程语言,不得不解释说计算机科学实际上关注能在任何编程语言中实施的数据结构和算法的研究,一种比争取学习尽可能多的编程语言更深刻更难以捉摸。 + +这本书涵盖了自然语言处理领域的许多主题。大多数的例子都使用 Python 和英语。不过,如果读者得出的结论是 NLP 是有关如何编写 Python 程序操纵英文文本,或者更广泛的,关于如何编写程序(以任何一种编程语言)处理(任何一种自然语言)文本的,这将是不幸的。我们选择 Python 和英语是权宜之计,仅此而已。即使我们关注编程本身也只是一种解决问题的手段:作为一种了解表示和操纵语言标注文本的集合的数据结构和算法的方式,作为一种方法来建立新的语言技术,更好地服务于信息社会的需求,并最终作为对人类语言极度丰富性的更深的理解的方法。 + +*但是目前为止,快乐编程吧!* + +## 关于本文档... + +针对 NLTK 3.0 作出更新。本章来自于《Python 自然语言处理》,[Steven Bird](http://estive.net/), [Ewan Klein](http://homepages.inf.ed.ac.uk/ewan/) 和 [Edward Loper](http://ed.loper.org/),Copyright © 2014 作者所有。本章依据 [*Creative Commons Attribution-Noncommercial-No Derivative Works 3\.0 United States License*](http://creativecommons.org/licenses/by-nc-nd/3.0/us/) 条款,与[*自然语言工具包*](http://nltk.org/) 3.0 版一起发行。 + +本文档构建于星期三 2015 年 7 月 1 日 12:30:05 AEST \ No newline at end of file diff --git a/docs/nlp-py-2e/14.md b/docs/nlp-py-2e/14.md new file mode 100644 index 0000000000000000000000000000000000000000..89cee9905659f65db54cf6fd497c9a09b7d5ceca --- /dev/null +++ b/docs/nlp-py-2e/14.md @@ -0,0 +1,458 @@ +# 索引 + +* _ + + * [(phrasal) projections *(3.2)*](ch09.html#_phrasal__projections_index_term) +* A + + * [accuracy *(3.2)*](ch06.html#accuracy_index_term) + * [address *(4.5)*](ch10.html#address_index_term) + * [adjectives *(2.6)*](ch05.html#adjectives_index_term) + * [adverbs *(2.6)*](ch05.html#adverbs_index_term) + * [agreement *(1.1)*](ch09.html#agreement_index_term) + * [alphabetic variants *(4.2)*](ch10.html#alphabetic_variants_index_term) + * [anaphora resolution *(5.2)*](ch01.html#anaphora_resolution_index_term) + * [anaphoric antecedent *(5.1)*](ch10.html#anaphoric_antecedent_index_term) + * [antecedent *(5.2)*](ch01.html#antecedent_index_term) + * [antonymy *(5.3)*](ch02.html#antonymy_index_term) + * [appending *(2.1)*](ch01.html#appending_index_term) + * [appropriate *(5)*](ch09.html#appropriate_index_term) + * [argument *(2)*](ch10.html#argument_index_term) + * [arity *(3.3)*](ch10.html#arity_index_term) + * [articles *(2.6)*](ch05.html#articles_index_term) + * [assignment *(3.5)*](ch10.html#assignment_index_term) + * [associative array *(3)*](ch05.html#associative_array_index_term) + * [assumption *(2)*](ch10.html#assumption_index_term) + * [atomic *(1.3)*](ch09.html#atomic_index_term) + * [attribute value matrix *(1.3)*](ch09.html#attribute_value_matrix_index_term) + * [auxiliaries *(3.3)*](ch09.html#auxiliaries_index_term) + * [auxiliary *(1.3)*](ch09.html#auxiliary_index_term) +* B + + * [backoff *(4.3)*](ch05.html#backoff_index_term) + * [backtracks *(4.1)*](ch08.html#backtracks_index_term) + * [base case *(4.7.1)*](ch04.html#base_case_index_term) + * [basic types *(3.1)*](ch10.html#basic_types_index_term) + * [bigrams *(3.3)*](ch01.html#bigrams_index_term) + * [binary predicate *(3.1)*](ch10.html#binary_predicate_index_term) + * [bind *(3.1)*](ch10.html#bind_index_term) + * [binding operators *(4.5)*](ch10.html#binding_operators_index_term) + * [binning *(5.3)*](ch06.html#binning_index_term) + * [BIO Format *(8)*](ch07.html#bio_format_index_term) + * [Bold *(X)*](ch00.html#bold_index_term) + * [boolean *(1.3)*](ch09.html#boolean_index_term) + * [boolean operators *(2)*](ch10.html#boolean_operators_index_term) + * [bottom-up *(4.7.3)*](ch04.html#bottom_up_index_term) + * [bottom-up parsing *(4.1)*](ch08.html#bottom_up_parsing_index_term) + * [bound *(3.1)*](ch10.html#bound_index_term) + * [bound *(3.1)*](ch10.html#bound_index_term_2) + * [breakpoints *(4.6.4)*](ch04.html#breakpoints_index_term) +* C + + * [call-by-value *(4.4.2)*](ch04.html#call_by_value_index_term) + * [call *(1.4)*](ch01.html#call_index_term) + * [call structure *(4.7.3)*](ch04.html#call_structure_index_term) + * [Catalan numbers *(6.2)*](ch08.html#catalan_numbers_index_term) + * [characteristic function *(3.4)*](ch10.html#characteristic_function_index_term) + * [chart *(4.4)*](ch08.html#chart_index_term) + * [chart parsing *(4.4)*](ch08.html#chart_parsing_index_term) + * [child *(4.2)*](ch07.html#child_index_term) + * [chink *(2.5)*](ch07.html#chink_index_term) + * [chink *(8)*](ch07.html#chink_index_term_2) + * [chunk grammar *(2.1)*](ch07.html#chunk_grammar_index_term) + * [chunk *(2)*](ch07.html#chunk_index_term) + * [chunking *(2)*](ch07.html#chunking_index_term) + * [class label *(1)*](ch06.html#class_label_index_term) + * [Classification *(1)*](ch06.html#classification_index_term) + * [closed class *(7.4)*](ch05.html#closed_class_index_term) + * [closed *(3.1)*](ch10.html#closed_index_term) + * [closures *(3.4.2)*](ch03.html#closures_index_term) + * [code point *(3.3.1)*](ch03.html#code_point_index_term) + * [coindex *(2)*](ch09.html#coindex_index_term) + * [collocation *(3.3)*](ch01.html#collocation_index_term) + * [comparative wordlist *(4.3)*](ch02.html#comparative_wordlist_index_term) + * [complements *(5.1)*](ch08.html#complements_index_term) + * [complete *(9)*](ch08.html#complete_index_term) + * [complex *(1.3)*](ch09.html#complex_index_term) + * [complex types *(3.1)*](ch10.html#complex_types_index_term) + * [components *(5.5)*](ch01.html#components_index_term) + * [concatenation *(3.2.1)*](ch03.html#concatenation_index_term) + * [conclusion *(2)*](ch10.html#conclusion_index_term) + * [conditional expression *(4.3)*](ch01.html#conditional_expression_index_term) + * [conditional frequency distribution *(2)*](ch02.html#conditional_frequency_distribution_index_term) + * [conditional *(6.3)*](ch06.html#conditional_index_term) + * [confusion matrix *(3.4)*](ch06.html#confusion_matrix_index_term) + * [consecutive classification *(1.6)*](ch06.html#consecutive_classification_index_term) + * [consistent *(1.2)*](ch10.html#consistent_index_term) + * [constituent *(2.1)*](ch08.html#constituent_index_term) + * [constituent structure *(2.1)*](ch08.html#constituent_structure_index_term) + * [control *(4)*](ch01.html#control_index_term) + * [control structure *(4.3)*](ch01.html#control_structure_index_term) + * [Cooper storage *(4.5)*](ch10.html#cooper_storage_index_term) + * [coordinate structure *(2.1)*](ch08.html#coordinate_structure_index_term) + * [copy *(4.1.1)*](ch04.html#copy_index_term) + * [coreferential *(3.1)*](ch10.html#coreferential_index_term) + * [corpora *(0)*](ch02.html#corpora_index_term) + * [Corpus Linguistics *(7)*](ch02.html#corpus_linguistics_index_term) + * [cross-validation *(3.5)*](ch06.html#cross_validation_index_term) +* D + + * [data intensive *(I)*](ch12.html#data_intensive_index_term) + * [debugger *(4.6.4)*](ch04.html#debugger_index_term) + * [decision nodes *(4)*](ch06.html#decision_nodes_index_term) + * [decision stump *(4)*](ch06.html#decision_stump_index_term) + * [decision tree *(4)*](ch06.html#decision_tree_index_term) + * [decoding *(3.3.1)*](ch03.html#decoding_index_term) + * [defensive programming *(4.4.4)*](ch04.html#defensive_programming_index_term) + * [dependents *(5)*](ch08.html#dependents_index_term) + * [determiners *(2.6)*](ch05.html#determiners_index_term) + * [dev-test *(1.2)*](ch06.html#dev_test_index_term) + * [development set *(1.2)*](ch06.html#development_set_index_term) + * [dialogue acts *(2.2)*](ch06.html#dialogue_acts_index_term) + * [dictionary *(3)*](ch05.html#dictionary_index_term) + * [dictionary *(3.2)*](ch05.html#dictionary_index_term_2) + * [directed acyclic graphs *(2)*](ch09.html#directed_acyclic_graphs_index_term) + * [discourse *(5)*](ch10.html#discourse_index_term) + * [discourse referents *(5.1)*](ch10.html#discourse_referents_index_term) + * [discourse representation structure *(5.1)*](ch10.html#discourse_representation_structure_index_term) + * [dispersion plot *(1.3)*](ch01.html#dispersion_plot_index_term) + * [divide-and-conquer *(4.7)*](ch04.html#divide_and_conquer_index_term) + * [docstring *(4.4)*](ch04.html#docstring_index_term) + * [doctest block *(4.4.6)*](ch04.html#doctest_block_index_term) + * [domain *(3.4)*](ch10.html#domain_index_term) + * [DRS conditions *(5.1)*](ch10.html#drs_conditions_index_term) + * [duck typing *(4.3)*](ch07.html#duck_typing_index_term) + * [dynamic programming *(4.4)*](ch08.html#dynamic_programming_index_term) +* E + + * [empiricism *(II)*](ch12.html#empiricism_index_term) + * [encode *(1.1)*](ch06.html#encode_index_term) + * [encoding *(3.3.1)*](ch03.html#encoding_index_term) + * [entails *(5.3)*](ch02.html#entails_index_term) + * [equivalent *(2)*](ch09.html#equivalent_index_term) + * [error analysis *(1.2)*](ch06.html#error_analysis_index_term) + * [evaluation set *(3.1)*](ch06.html#evaluation_set_index_term) + * [existential quantifier *(3.1)*](ch10.html#existential_quantifier_index_term) + * [Expected Likelihood Estimation *(5.2)*](ch06.html#expected_likelihood_estimation_index_term) + * [export *(3.9.2)*](ch03.html#export_index_term) +* F + + * [F-Measure *(3.3)*](ch06.html#f_measure_index_term) + * [F-Score *(3.3)*](ch06.html#f_score_index_term) + * [f-structure *(5)*](ch09.html#f_structure_index_term) + * [False negatives *(3.3)*](ch06.html#false_negatives_index_term) + * [False positives *(3.3)*](ch06.html#false_positives_index_term) + * [feature extractor *(1.1)*](ch06.html#feature_extractor_index_term) + * [feature *(1.2)*](ch09.html#feature_index_term) + * [feature path *(2)*](ch09.html#feature_path_index_term) + * [feature set *(1.1)*](ch06.html#feature_set_index_term) + * [feature structures *(1)*](ch09.html#feature_structures_index_term) + * [features *(1.1)*](ch06.html#features_index_term) + * [fields *(4.2.2)*](ch04.html#fields_index_term) + * [filler *(3.4)*](ch09.html#filler_index_term) + * [folds *(3.5)*](ch06.html#folds_index_term) + * [formal language theory *(I)*](ch12.html#formal_language_theory_index_term) + * [format string *(3.9.2)*](ch03.html#format_string_index_term) + * [free *(3.1)*](ch10.html#free_index_term) + * [frequency distribution *(3.1)*](ch01.html#frequency_distribution_index_term) + * [function *(3.2)*](ch02.html#function_index_term) +* G + + * [gaps *(3.4)*](ch09.html#gaps_index_term) + * [gazetteer *(5)*](ch07.html#gazetteer_index_term) + * [generalized quantifiers *(8)*](ch10.html#generalized_quantifiers_index_term) + * [generative grammar *(8)*](ch08.html#generative_grammar_index_term) + * [generative *(6.3)*](ch06.html#generative_index_term) + * [generator expression *(4.2.3)*](ch04.html#generator_expression_index_term) + * [gerund *(7.1)*](ch05.html#gerund_index_term) + * [Glue semantics *(7)*](ch10.html#glue_semantics_index_term) + * [glyphs *(3.3.1)*](ch03.html#glyphs_index_term) + * [gold standard *(4.4)*](ch05.html#gold_standard_index_term) + * [graphs *(4.8.2)*](ch04.html#graphs_index_term) + * [greedy sequence classification *(1.6)*](ch06.html#greedy_sequence_classification_index_term) +* H + + * [hapaxes *(3.1)*](ch01.html#hapaxes_index_term) + * [hash array *(3)*](ch05.html#hash_array_index_term) + * [head features *(6)*](ch09.html#head_features_index_term) + * [head *(5)*](ch08.html#head_index_term) + * [headword *(4)*](ch02.html#headword_index_term) + * [Heldout Estimation *(5.2)*](ch06.html#heldout_estimation_index_term) + * [Hidden Markov Models *(1.7)*](ch06.html#hidden_markov_models_index_term) + * [Hole semantics *(7)*](ch10.html#hole_semantics_index_term) + * [holonyms *(5.3)*](ch02.html#holonyms_index_term) + * [homonyms *(4)*](ch02.html#homonyms_index_term) + * [hyponyms *(5.2)*](ch02.html#hyponyms_index_term) +* I + + * [idealism *(II)*](ch12.html#idealism_index_term) + * [identifiers *(2.3)*](ch01.html#identifiers_index_term) + * [immediate constituents *(2.1)*](ch08.html#immediate_constituents_index_term) + * [immutable *(4.2.2)*](ch04.html#immutable_index_term) + * [inconsistent *(1.2)*](ch10.html#inconsistent_index_term) + * [indented code block *(1.4)*](ch01.html#indented_code_block_index_term) + * [independence assumption *(5.1)*](ch06.html#independence_assumption_index_term) + * [index *(2.2)*](ch01.html#index_index_term) + * [inference *(2)*](ch10.html#inference_index_term) + * [Information Extraction *(1)*](ch07.html#information_extraction_index_term) + * [information gain *(4.1)*](ch06.html#information_gain_index_term) + * [Inline annotation *(3.5)*](ch11.html#inline_annotation_index_term) + * [interpreter *(1.1)*](ch01.html#interpreter_index_term) + * [IOB tags *(2.6)*](ch07.html#iob_tags_index_term) + * [iterative optimization *(6)*](ch06.html#iterative_optimization_index_term) +* J + + * [joint classifier *(1.6)*](ch06.html#joint_classifier_index_term) + * [joint-feature *(6.1)*](ch06.html#joint_feature_index_term) +* K + + * [Kappa *(2.2)*](ch11.html#kappa_index_term) + * [key *(3.2)*](ch05.html#key_index_term) + * [keyword arguments *(4.5.4)*](ch04.html#keyword_arguments_index_term) + * [Kleene closures *(3.4.2)*](ch03.html#kleene_closures_index_term) +* L + + * [lambda expressions *(4.5.1)*](ch04.html#lambda_expressions_index_term) + * [latent semantic analysis *(4.8.4)*](ch04.html#latent_semantic_analysis_index_term) + * [leaf nodes *(4)*](ch06.html#leaf_nodes_index_term) + * [left-corner *(4.3)*](ch08.html#left_corner_index_term) + * [left-corner parser *(4.3)*](ch08.html#left_corner_parser_index_term) + * [left-recursive *(3.3)*](ch08.html#left_recursive_index_term) + * [lemma *(4)*](ch02.html#lemma_index_term) + * [letter trie *(4.7.1)*](ch04.html#letter_trie_index_term) + * [lexical acquisition *(9)*](ch08.html#lexical_acquisition_index_term) + * [lexical categories *(0)*](ch05.html#lexical_categories_index_term) + * [lexical entry *(4)*](ch02.html#lexical_entry_index_term) + * [lexical relations *(5.3)*](ch02.html#lexical_relations_index_term) + * [lexicon *(10)*](ch06.html#lexicon_index_term) + * [LGB rule *(4.4.3)*](ch04.html#lgb_rule_index_term) + * [library *(3.3)*](ch02.html#library_index_term) + * [licensed *(3.4)*](ch09.html#licensed_index_term) + * [likelihood ratios *(1.1)*](ch06.html#likelihood_ratios_index_term) + * [Linear-Chain Conditional Random Field Models *(1.7)*](ch06.html#linear_chain_conditional_random_field_models_index_term) + * [list *(2.1)*](ch01.html#list_index_term) + * [local variables *(3.2)*](ch02.html#local_variables_index_term) + * [logical constants *(3.1)*](ch10.html#logical_constants_index_term) + * [logical form *(2)*](ch10.html#logical_form_index_term) +* M + + * [machine translation *(5.3)*](ch01.html#machine_translation_index_term) + * [mapping *(3)*](ch05.html#mapping_index_term) + * [maximal projection *(3.2)*](ch09.html#maximal_projection_index_term) + * [Maximum Entropy *(6)*](ch06.html#maximum_entropy_index_term) + * [Maximum Entropy Markov Models *(1.7)*](ch06.html#maximum_entropy_markov_models_index_term) + * [Maximum Entropy principle *(6.2)*](ch06.html#maximum_entropy_principle_index_term) + * [meronyms *(5.3)*](ch02.html#meronyms_index_term) + * [methods *(3.2)*](ch02.html#methods_index_term) + * [modals *(2.6)*](ch05.html#modals_index_term) + * [model checking *(3.5)*](ch10.html#model_checking_index_term) + * [model *(1.2)*](ch10.html#model_index_term) + * [models *(7)*](ch06.html#models_index_term) + * [module *(3.3)*](ch02.html#module_index_term) + * [morpho-syntactic *(7.5)*](ch05.html#morpho_syntactic_index_term) + * [morphological analysis *(7.5)*](ch05.html#morphological_analysis_index_term) + * [multiword expression *(3.11)*](ch03.html#multiword_expression_index_term) + * [mutable *(4.2.2)*](ch04.html#mutable_index_term) +* N + + * [n-gram tagger *(5.3)*](ch05.html#n_gram_tagger_index_term) + * [naive Bayes assumption *(5.1)*](ch06.html#naive_bayes_assumption_index_term) + * [naive Bayes *(5)*](ch06.html#naive_bayes_index_term) + * [named entity detection *(1.1)*](ch07.html#named_entity_detection_index_term) + * [named entity recognition *(5)*](ch07.html#named_entity_recognition_index_term) + * [newlines *(3.1.5)*](ch03.html#newlines_index_term) + * [NLTK Data Repository *(6.3)*](ch11.html#nltk_data_repository_index_term) + * [non-logical constants *(3.1)*](ch10.html#non_logical_constants_index_term) + * [non-standard words *(3.6.2)*](ch03.html#non_standard_words_index_term) + * [normalized *(3.6)*](ch03.html#normalized_index_term) + * [noun phrase chunking *(2.1)*](ch07.html#noun_phrase_chunking_index_term) + * [noun phrase *(II)*](ch12.html#noun_phrase_index_term) + * [NP-chunking *(2.1)*](ch07.html#np_chunking_index_term) +* O + + * [objective function *(3.8.2)*](ch03.html#objective_function_index_term) + * [open class *(7.4)*](ch05.html#open_class_index_term) + * [open formula *(3.1)*](ch10.html#open_formula_index_term) + * [out-of-vocabulary *(5.5)*](ch05.html#out_of_vocabulary_index_term) + * [overfit *(4.1)*](ch06.html#overfit_index_term) + * [overfitting *(1.2)*](ch06.html#overfitting_index_term) +* P + + * [package *(3.3)*](ch02.html#package_index_term) + * [parameter *(1.4)*](ch01.html#parameter_index_term) + * [parameters *(5.5)*](ch06.html#parameters_index_term) + * [parent *(4.2)*](ch07.html#parent_index_term) + * [parser *(4)*](ch08.html#parser_index_term) + * [part-of-speech tagging *(0)*](ch05.html#part_of_speech_tagging_index_term) + * [partial information *(2.1)*](ch09.html#partial_information_index_term) + * [parts of speech *(0)*](ch05.html#parts_of_speech_index_term) + * [personal pronouns *(2.6)*](ch05.html#personal_pronouns_index_term) + * [phonology *(I)*](ch12.html#phonology_index_term) + * [phrasal level *(3.2)*](ch09.html#phrasal_level_index_term) + * [POS-tagger *(1)*](ch05.html#pos_tagger_index_term) + * [POS-tagging *(0)*](ch05.html#pos_tagging_index_term) + * [pre-sort *(4.7)*](ch04.html#pre_sort_index_term) + * [Precision *(3.3)*](ch06.html#precision_index_term) + * [precision/recall trade-off *(5.3)*](ch05.html#precision_recall_trade_off_index_term) + * [predicates *(3.1)*](ch10.html#predicates_index_term) + * [prepositional phrase attachment ambiguity *(3.1)*](ch08.html#prepositional_phrase_attachment_ambiguity_index_term) + * [prepositional phrase *(2.1)*](ch08.html#prepositional_phrase_index_term) + * [present participle *(7.1)*](ch05.html#present_participle_index_term) + * [principle of compositionality *(I)*](ch12.html#principle_of_compositionality_index_term) + * [prior probability *(5)*](ch06.html#prior_probability_index_term) + * [probabilistic context free grammar *(6.3)*](ch08.html#probabilistic_context_free_grammar_index_term) + * [productions *(1.1)*](ch08.html#productions_index_term) + * [projective *(5)*](ch08.html#projective_index_term) + * [proof goal *(3.2)*](ch10.html#proof_goal_index_term) + * [Propositional logic *(2)*](ch10.html#propositional_logic_index_term) + * [propositional symbols *(2)*](ch10.html#propositional_symbols_index_term) + * [prune *(4.1)*](ch06.html#prune_index_term) +* Q + + * [question answering *(5.3)*](ch01.html#question_answering_index_term) +* R + + * [rationalism *(II)*](ch12.html#rationalism_index_term) + * [raw string *(3.4.2)*](ch03.html#raw_string_index_term) + * [realism *(II)*](ch12.html#realism_index_term) + * [Recall *(3.3)*](ch06.html#recall_index_term) + * [recognizing *(4.4)*](ch08.html#recognizing_index_term) + * [record *(4.2.2)*](ch04.html#record_index_term) + * [recursion *(4.7.1)*](ch04.html#recursion_index_term) + * [recursive *(3.3)*](ch08.html#recursive_index_term) + * [reduce *(4.2)*](ch08.html#reduce_index_term) + * [reentrancy *(2)*](ch09.html#reentrancy_index_term) + * [refactor *(4.4.5)*](ch04.html#refactor_index_term) + * [regression testing *(4.6.5)*](ch04.html#regression_testing_index_term) + * [relation detection *(1.1)*](ch07.html#relation_detection_index_term) + * [relational operators *(4.1)*](ch01.html#relational_operators_index_term) + * [replacement field *(3.9.2)*](ch03.html#replacement_field_index_term) + * [return value *(3.2)*](ch02.html#return_value_index_term) + * [root element *(4.3)*](ch11.html#root_element_index_term) + * [root node *(4)*](ch06.html#root_node_index_term) + * [runtime error *(2.2)*](ch01.html#runtime_error_index_term) +* S + + * [S-Retrieval *(4.5)*](ch10.html#s_retrieval_index_term) + * [satisfies *(3.5)*](ch10.html#satisfies_index_term) + * [scope *(3.7)*](ch10.html#scope_index_term) + * [segmentation *(3.8)*](ch03.html#segmentation_index_term) + * [semantic role labeling *(5.2)*](ch01.html#semantic_role_labeling_index_term) + * [sequence classifier *(1.6)*](ch06.html#sequence_classifier_index_term) + * [sequence *(3.2.6)*](ch03.html#sequence_index_term) + * [shift *(4.2)*](ch08.html#shift_index_term) + * [shift-reduce parser *(4.2)*](ch08.html#shift_reduce_parser_index_term) + * [siblings *(4.2)*](ch07.html#siblings_index_term) + * [signature *(3.1)*](ch10.html#signature_index_term) + * [slash categories *(3.4)*](ch09.html#slash_categories_index_term) + * [slicing *(2.2)*](ch01.html#slicing_index_term) + * [smoothing *(5.2)*](ch06.html#smoothing_index_term) + * [stack trace *(4.6.4)*](ch04.html#stack_trace_index_term) + * [standoff annotation *(2.3)*](ch11.html#standoff_annotation_index_term) + * [standoff annotation *(3.5)*](ch11.html#standoff_annotation_index_term_2) + * [start-symbol *(3.1)*](ch08.html#start_symbol_index_term) + * [stopwords *(4.1)*](ch02.html#stopwords_index_term) + * [string formatting *(3.9.2)*](ch03.html#string_formatting_index_term) + * [string *(3.2)*](ch03.html#string_index_term) + * [strings *(2.4)*](ch01.html#strings_index_term) + * [structurally ambiguous *(3.1)*](ch08.html#structurally_ambiguous_index_term) + * [structure sharing *(2)*](ch09.html#structure_sharing_index_term) + * [structured data *(1)*](ch07.html#structured_data_index_term) + * [stylistics *(1.3)*](ch02.html#stylistics_index_term) + * [subcategorized *(5.1)*](ch08.html#subcategorized_index_term) + * [subsumption *(2.1)*](ch09.html#subsumption_index_term) + * [subtype *(5)*](ch09.html#subtype_index_term) + * [supervised *(1)*](ch06.html#supervised_index_term) + * [Swadesh wordlists *(4.3)*](ch02.html#swadesh_wordlists_index_term) + * [symbolic logic *(I)*](ch12.html#symbolic_logic_index_term) + * [synonyms *(5.1)*](ch02.html#synonyms_index_term) + * [synset *(5.1)*](ch02.html#synset_index_term) + * [syntax error *(1.1)*](ch01.html#syntax_error_index_term) +* T + + * [T9 *(3.4.2)*](ch03.html#t9_index_term) + * [tag *(2)*](ch09.html#tag_index_term) + * [tag patterns *(2.2)*](ch07.html#tag_patterns_index_term) + * [tagged *(2.2)*](ch05.html#tagged_index_term) + * [tagging *(0)*](ch05.html#tagging_index_term) + * [tagset *(0)*](ch05.html#tagset_index_term) + * [terms *(3.1)*](ch10.html#terms_index_term) + * [test set *(1.1)*](ch06.html#test_set_index_term) + * [test set *(3.1)*](ch06.html#test_set_index_term_2) + * [text alignment *(5.4)*](ch01.html#text_alignment_index_term) + * [textonyms *(3.4.2)*](ch03.html#textonyms_index_term) + * [token *(1.4)*](ch01.html#token_index_term) + * [tokenization *(3.1.1)*](ch03.html#tokenization_index_term) + * [top-down *(4.7.3)*](ch04.html#top_down_index_term) + * [top-down parsing *(4.1)*](ch08.html#top_down_parsing_index_term) + * [total likelihood *(6)*](ch06.html#total_likelihood_index_term) + * [train *(5.1)*](ch05.html#train_index_term) + * [training *(5.1)*](ch05.html#training_index_term) + * [training set *(1.1)*](ch06.html#training_set_index_term) + * [training set *(1.2)*](ch06.html#training_set_index_term_2) + * [transitive verbs *(5.1)*](ch08.html#transitive_verbs_index_term) + * [tree *(4.2)*](ch07.html#tree_index_term) + * [True negatives *(3.3)*](ch06.html#true_negatives_index_term) + * [True positives *(3.3)*](ch06.html#true_positives_index_term) + * [truth-conditions *(2)*](ch10.html#truth_conditions_index_term) + * [tuple *(4.2)*](ch04.html#tuple_index_term) + * [Turing Test *(5.5)*](ch01.html#turing_test_index_term) + * [Type I errors *(3.3)*](ch06.html#type_i_errors_index_term) + * [Type II errors *(3.3)*](ch06.html#type_ii_errors_index_term) + * [type-raising *(4.3)*](ch10.html#type_raising_index_term) + * [Typed feature structures *(5)*](ch09.html#typed_feature_structures_index_term) + * [types *(3.1)*](ch10.html#types_index_term) +* U + + * [unary predicate *(3.1)*](ch10.html#unary_predicate_index_term) + * [unbounded dependency construction *(3.4)*](ch09.html#unbounded_dependency_construction_index_term) + * [underspecified *(1.2)*](ch09.html#underspecified_index_term) + * [unification *(2.1)*](ch09.html#unification_index_term) + * [unique beginners *(5.2)*](ch02.html#unique_beginners_index_term) + * [universal quantifier *(3.1)*](ch10.html#universal_quantifier_index_term) + * [unseen *(7)*](ch06.html#unseen_index_term) + * [unstructured data *(1)*](ch07.html#unstructured_data_index_term) +* V + + * [valencies *(5.1)*](ch08.html#valencies_index_term) + * [valid *(2)*](ch10.html#valid_index_term) + * [validity *(4.1)*](ch11.html#validity_index_term) + * [valuation function *(3.4)*](ch10.html#valuation_function_index_term) + * [value *(3.2)*](ch05.html#value_index_term) + * [variable *(2.3)*](ch01.html#variable_index_term) + * [verb phrase *(2.1)*](ch08.html#verb_phrase_index_term) +* W + + * [weights *(5.5)*](ch06.html#weights_index_term) + * [well formed formulas *(2)*](ch10.html#well_formed_formulas_index_term) + * [well formed *(4.1)*](ch11.html#well_formed_index_term) + * [well-formed substring table *(4.4)*](ch08.html#well_formed_substring_table_index_term) + * [wildcard *(3.4.1)*](ch03.html#wildcard_index_term) + * [word classes *(0)*](ch05.html#word_classes_index_term) + * [word sense disambiguation *(5.1)*](ch01.html#word_sense_disambiguation_index_term) + * [word type *(1.4)*](ch01.html#word_type_index_term) + * [WordNet *(5)*](ch02.html#wordnet_index_term) +* X + + * [XML attribute *(4.1)*](ch11.html#xml_attribute_index_term) + * [XML element *(4.1)*](ch11.html#xml_element_index_term) +* Z + + * [zero projection *(3.2)*](ch09.html#zero_projection_index_term) +* Α + + * [α-conversion *(4.2)*](ch10.html#α_conversion_index_term) + * [α equivalents *(4.2)*](ch10.html#α_equivalents_index_term) +* Β + + * [β-reduction *(4.2)*](ch10.html#β_reduction_index_term) +* Λ + + * [λ abstraction *(4.2)*](ch10.html#λ_abstraction_index_term) + * [λ operator *(4.2)*](ch10.html#λ_operator_index_term) + +## 关于本文档... + +针对 NLTK 3.0 作出更新。本章来自于《Python 自然语言处理》,[Steven Bird](http://estive.net/), [Ewan Klein](http://homepages.inf.ed.ac.uk/ewan/) 和 [Edward Loper](http://ed.loper.org/),Copyright © 2014 作者所有。本章依据 [*Creative Commons Attribution-Noncommercial-No Derivative Works 3\.0 United States License*](http://creativecommons.org/licenses/by-nc-nd/3.0/us/) 条款,与[*自然语言工具包*](http://nltk.org/) 3.0 版一起发行。 + +本文档构建于星期三 2015 年 7 月 1 日 12:30:05 AEST \ No newline at end of file diff --git a/docs/nlp-py-2e/2.md b/docs/nlp-py-2e/2.md new file mode 100644 index 0000000000000000000000000000000000000000..75c198c1bda36fd95c31a78eb1930f3aedb920f3 --- /dev/null +++ b/docs/nlp-py-2e/2.md @@ -0,0 +1,1241 @@ +# 2 获得文本语料和词汇资源 + +在自然语言处理的实际项目中,通常要使用大量的语言数据或者语料库。本章的目的是要回答下列问题: + +1. 什么是有用的文本语料和词汇资源,我们如何使用 Python 获取它们? +2. 哪些 Python 结构最适合这项工作? +3. 编写 Python 代码时我们如何避免重复的工作? + +本章继续通过语言处理任务的例子展示编程概念。在系统的探索每一个 Python 结构之前请耐心等待。如果你看到一个例子中含有一些不熟悉的东西,请不要担心。只需去尝试它,看看它做些什么——如果你很勇敢——通过使用不同的文本或词替换代码的某些部分来进行修改。这样,你会将任务与编程习惯用法关联起来,并在后续的学习中了解怎么会这样和为什么是这样。 + +## 1 获取文本语料库 + +正如刚才提到的,一个文本语料库是一大段文本。许多语料库的设计都要考虑一个或多个文体间谨慎的平衡。我们曾在第 1 章研究过一些小的文本集合,例如美国总统就职演说。这种特殊的语料库实际上包含了几十个单独的文本——每个人一个演讲——但为了处理方便,我们把它们头尾连接起来当做一个文本对待。第 1 章中也使用变量预先定义好了一些文本,我们通过输入`from nltk.book import *`来访问它们。然而,因为我们希望能够处理其他文本,本节中将探讨各种文本语料库。我们将看到如何选择单个文本,以及如何处理它们。 + +## 1.1 古腾堡语料库 + +NLTK 包含古腾堡项目(Project Gutenberg)电子文本档案的经过挑选的一小部分文本,该项目大约有 25,000 本免费电子图书,放在`http://www.gutenberg.org/`上。我们先要用 Python 解释器加载 NLTK 包,然后尝试`nltk.corpus.gutenberg.fileids()`,下面是这个语料库中的文件标识符: + +```py +>>> import nltk +>>> nltk.corpus.gutenberg.fileids() +['austen-emma.txt', 'austen-persuasion.txt', 'austen-sense.txt', 'bible-kjv.txt', +'blake-poems.txt', 'bryant-stories.txt', 'burgess-busterbrown.txt', +'carroll-alice.txt', 'chesterton-ball.txt', 'chesterton-brown.txt', +'chesterton-thursday.txt', 'edgeworth-parents.txt', 'melville-moby_dick.txt', +'milton-paradise.txt', 'shakespeare-caesar.txt', 'shakespeare-hamlet.txt', +'shakespeare-macbeth.txt', 'whitman-leaves.txt'] +``` + +让我们挑选这些文本的第一个——简·奥斯丁的《爱玛》——并给它一个简短的名称`emma`,然后找出它包含多少个词: + +```py +>>> emma = nltk.corpus.gutenberg.words('austen-emma.txt') +>>> len(emma) +192427 +``` + +注意 + +在第 1 章中,我们演示了如何使用`text1.concordance()`命令对`text1`这样的文本进行索引。然而,这是假设你正在使用由`from nltk.book import *`导入的 9 个文本之一。现在你开始研究`nltk.corpus`中的数据,像前面的例子一样,你必须采用以下语句对来处理索引和第 1 章中的其它任务: + +```py +>>> emma = nltk.Text(nltk.corpus.gutenberg.words('austen-emma.txt')) +>>> emma.concordance("surprize") +``` + +在我们定义`emma`, 时,我们调用了 NLTK 中的`corpus`包中的`gutenberg`对象的`words()`函数。但因为总是要输入这么长的名字很繁琐,Python 提供了另一个版本的`import`语句,示例如下: + +```py +>>> from nltk.corpus import gutenberg +>>> gutenberg.fileids() +['austen-emma.txt', 'austen-persuasion.txt', 'austen-sense.txt', ...] +>>> emma = gutenberg.words('austen-emma.txt') +``` + +让我们写一个简短的程序,通过循环遍历前面列出的`gutenberg`文件标识符列表相应的`fileid`,然后计算统计每个文本。为了使输出看起来紧凑,我们将使用`round()`舍入每个数字到最近似的整数。 + +```py +>>> for fileid in gutenberg.fileids(): +... num_chars = len(gutenberg.raw(fileid)) ❶ +... num_words = len(gutenberg.words(fileid)) +... num_sents = len(gutenberg.sents(fileid)) +... num_vocab = len(set(w.lower() for w in gutenberg.words(fileid))) +... print(round(num_chars/num_words), round(num_words/num_sents), round(num_words/num_vocab), fileid) +... +5 25 26 austen-emma.txt +5 26 17 austen-persuasion.txt +5 28 22 austen-sense.txt +4 34 79 bible-kjv.txt +5 19 5 blake-poems.txt +4 19 14 bryant-stories.txt +4 18 12 burgess-busterbrown.txt +4 20 13 carroll-alice.txt +5 20 12 chesterton-ball.txt +5 23 11 chesterton-brown.txt +5 18 11 chesterton-thursday.txt +4 21 25 edgeworth-parents.txt +5 26 15 melville-moby_dick.txt +5 52 11 milton-paradise.txt +4 12 9 shakespeare-caesar.txt +4 12 8 shakespeare-hamlet.txt +4 12 7 shakespeare-macbeth.txt +5 36 12 whitman-leaves.txt +``` + +这个程序显示每个文本的三个统计量:平均词长、平均句子长度和本文中每个词出现的平均次数(我们的词汇多样性得分)。请看,平均词长似乎是英语的一个一般属性,因为它的值总是`4`。(事实上,平均词长是`3`而不是`4`,因为`num_chars`变量计数了空白字符。)相比之下,平均句子长度和词汇多样性看上去是作者个人的特点。 + +前面的例子也表明我们怎样才能获取“原始”文本❶而不用把它分割成词符。`raw()`函数给我们没有进行过任何语言学处理的文件的内容。因此,例如`len(gutenberg.raw('blake-poems.txt'))`告诉我们文本中出现的*字符*个数,包括词之间的空格。`sents()`函数把文本划分成句子,其中每一个句子是一个单词列表: + +```py +>>> macbeth_sentences = gutenberg.sents('shakespeare-macbeth.txt') +>>> macbeth_sentences +[['[', 'The', 'Tragedie', 'of', 'Macbeth', 'by', 'William', 'Shakespeare', +'1603', ']'], ['Actus', 'Primus', '.'], ...] +>>> macbeth_sentences[1116] +['Double', ',', 'double', ',', 'toile', 'and', 'trouble', ';', +'Fire', 'burne', ',', 'and', 'Cauldron', 'bubble'] +>>> longest_len = max(len(s) for s in macbeth_sentences) +>>> [s for s in macbeth_sentences if len(s) == longest_len] +[['Doubtfull', 'it', 'stood', ',', 'As', 'two', 'spent', 'Swimmers', ',', 'that', +'doe', 'cling', 'together', ',', 'And', 'choake', 'their', 'Art', ':', 'The', +'mercilesse', 'Macdonwald', ...]] +``` + +注意 + +除了`words()`, `raw()`和`sents()`之外,大多数 NLTK 语料库阅读器还包括多种访问方法。一些语料库提供更加丰富的语言学内容,例如:词性标注,对话标记,语法树等;在后面的章节中,我们将看到这些。 + +## 1.2 网络和聊天文本 + +虽然古腾堡项目包含成千上万的书籍,它代表既定的文学。考虑较不正式的语言也是很重要的。NLTK 的网络文本小集合的内容包括 Firefox 交流论坛,在纽约无意听到的对话,《加勒比海盗》的电影剧本,个人广告和葡萄酒的评论: + +```py +>>> from nltk.corpus import webtext +>>> for fileid in webtext.fileids(): +... print(fileid, webtext.raw(fileid)[:65], '...') +... +firefox.txt Cookie Manager: "Don't allow sites that set removed cookies to se... +grail.txt SCENE 1: [wind] [clop clop clop] KING ARTHUR: Whoa there! [clop... +overheard.txt White guy: So, do you have any plans for this evening? Asian girl... +pirates.txt PIRATES OF THE CARRIBEAN: DEAD MAN'S CHEST, by Ted Elliott & Terr... +singles.txt 25 SEXY MALE, seeks attrac older single lady, for discreet encoun... +wine.txt Lovely delicate, fragrant Rhone wine. Polished leather and strawb... +``` + +还有一个即时消息聊天会话语料库,最初由美国海军研究生院为研究自动检测互联网幼童虐待癖而收集的。语料库包含超过 10,000 张帖子,以`UserNNN`形式的通用名替换掉用户名,手工编辑消除任何其他身份信息,制作而成。语料库被分成 15 个文件,每个文件包含几百个按特定日期和特定年龄的聊天室(青少年、20 岁、30 岁、40 岁、再加上一个通用的成年人聊天室)收集的帖子。文件名中包含日期、聊天室和帖子数量,例如`10-19-20s_706posts.xml`包含 2006 年 10 月 19 日从 20 多岁聊天室收集的 706 个帖子。 + +```py +>>> from nltk.corpus import nps_chat +>>> chatroom = nps_chat.posts('10-19-20s_706posts.xml') +>>> chatroom[123] +['i', 'do', "n't", 'want', 'hot', 'pics', 'of', 'a', 'female', ',', +'I', 'can', 'look', 'in', 'a', 'mirror', '.'] +``` + +## 1.3 布朗语料库 + +布朗语料库是第一个百万词级的英语电子语料库的,由布朗大学于 1961 年创建。这个语料库包含 500 个不同来源的文本,按照文体分类,如:*新闻*、*社论*等。表 1.1 给出了各个文体的例子(完整列表,请参阅`http://icame.uib.no/brown/bcm-los.html`)。 + +表 1.1: + +布朗语料库每一部分的示例文档 + +```py +>>> from nltk.corpus import brown +>>> brown.categories() +['adventure', 'belles_lettres', 'editorial', 'fiction', 'government', 'hobbies', +'humor', 'learned', 'lore', 'mystery', 'news', 'religion', 'reviews', 'romance', +'science_fiction'] +>>> brown.words(categories='news') +['The', 'Fulton', 'County', 'Grand', 'Jury', 'said', ...] +>>> brown.words(fileids=['cg22']) +['Does', 'our', 'society', 'have', 'a', 'runaway', ',', ...] +>>> brown.sents(categories=['news', 'editorial', 'reviews']) +[['The', 'Fulton', 'County'...], ['The', 'jury', 'further'...], ...] +``` + +布朗语料库是一个研究文体之间的系统性差异——一种叫做文体学的语言学研究——很方便的资源。让我们来比较不同文体中的情态动词的用法。第一步是产生特定文体的计数。记住做下面的实验之前要`import nltk`: + +```py +>>> from nltk.corpus import brown +>>> news_text = brown.words(categories='news') +>>> fdist = nltk.FreqDist(w.lower() for w in news_text) +>>> modals = ['can', 'could', 'may', 'might', 'must', 'will'] +>>> for m in modals: +... print(m + ':', fdist[m], end=' ') +... +can: 94 could: 87 may: 93 might: 38 must: 53 will: 389 +``` + +注意 + +我们需要包包含`end=' '`以让`print`函数将其输出放在单独的一行。 + +注意 + +**轮到你来**:选择布朗语料库的不同部分,修改前面的例子,计数包含`wh`的词,如:`what`, `when`, `where`, `who`和`why`。 + +下面,我们来统计每一个感兴趣的文体。我们使用 NLTK 提供的带条件的频率分布函数。在第 2 节中会系统的把下面的代码一行行拆开来讲解。现在,你可以忽略细节,只看输出。 + +```py +>>> cfd = nltk.ConditionalFreqDist( +... (genre, word) +... for genre in brown.categories() +... for word in brown.words(categories=genre)) +>>> genres = ['news', 'religion', 'hobbies', 'science_fiction', 'romance', 'humor'] +>>> modals = ['can', 'could', 'may', 'might', 'must', 'will'] +>>> cfd.tabulate(conditions=genres, samples=modals) + can could may might must will + news 93 86 66 38 50 389 + religion 82 59 78 12 54 71 + hobbies 268 58 131 22 83 264 +science_fiction 16 49 4 12 8 16 + romance 74 193 11 51 45 43 + humor 16 30 8 8 9 13 +``` + +请看,新闻文体中最常见的情态动词是`will`,而言情文体中最常见的情态动词是`could`。你能预言这些吗?这种可以区分文体的词计数方法将在第六章中再次谈及。 + +## 1.4 路透社语料库 + +路透社语料库包含 10,788 个新闻文档,共计 130 万字。这些文档分成 90 个主题,按照“训练”和“测试”分为两组;因此,`fileid`为`'test/14826'`的文档属于测试组。这样分割是为了训练和测试算法的,这种算法自动检测文档的主题,我们将在第六章中看到。 + +```py +>>> from nltk.corpus import reuters +>>> reuters.fileids() +['test/14826', 'test/14828', 'test/14829', 'test/14832', ...] +>>> reuters.categories() +['acq', 'alum', 'barley', 'bop', 'carcass', 'castor-oil', 'cocoa', +'coconut', 'coconut-oil', 'coffee', 'copper', 'copra-cake', 'corn', +'cotton', 'cotton-oil', 'cpi', 'cpu', 'crude', 'dfl', 'dlr', ...] +``` + +与布朗语料库不同,路透社语料库的类别是有互相重叠的,只是因为新闻报道往往涉及多个主题。我们可以查找由一个或多个文档涵盖的主题,也可以查找包含在一个或多个类别中的文档。为方便起见,语料库方法既接受单个的`fileid`也接受`fileids`列表作为参数。 + +```py +>>> reuters.categories('training/9865') +['barley', 'corn', 'grain', 'wheat'] +>>> reuters.categories(['training/9865', 'training/9880']) +['barley', 'corn', 'grain', 'money-fx', 'wheat'] +>>> reuters.fileids('barley') +['test/15618', 'test/15649', 'test/15676', 'test/15728', 'test/15871', ...] +>>> reuters.fileids(['barley', 'corn']) +['test/14832', 'test/14858', 'test/15033', 'test/15043', 'test/15106', +'test/15287', 'test/15341', 'test/15618', 'test/15648', 'test/15649', ...] +``` + +类似的,我们可以以文档或类别为单位查找我们想要的词或句子。这些文本中最开始的几个词是标题,按照惯例以大写字母存储。 + +```py +>>> reuters.words('training/9865')[:14] +['FRENCH', 'FREE', 'MARKET', 'CEREAL', 'EXPORT', 'BIDS', +'DETAILED', 'French', 'operators', 'have', 'requested', 'licences', 'to', 'export'] +>>> reuters.words(['training/9865', 'training/9880']) +['FRENCH', 'FREE', 'MARKET', 'CEREAL', 'EXPORT', ...] +>>> reuters.words(categories='barley') +['FRENCH', 'FREE', 'MARKET', 'CEREAL', 'EXPORT', ...] +>>> reuters.words(categories=['barley', 'corn']) +['THAI', 'TRADE', 'DEFICIT', 'WIDENS', 'IN', 'FIRST', ...] +``` + +## 1.5 就职演说语料库 + +在第 1 章,我们看到了就职演说语料库,但是把它当作一个单独的文本对待。图 [inaugural](./ch01.html#fig-inaugural) 中使用的“词偏移”就像是一个坐标轴;它是语料库中词的索引数,从第一个演讲的第一个词开始算起。然而,语料库实际上是 55 个文本的集合,每个文本都是一个总统的演说。这个集合的一个有趣特性是它的时间维度: + +```py +>>> from nltk.corpus import inaugural +>>> inaugural.fileids() +['1789-Washington.txt', '1793-Washington.txt', '1797-Adams.txt', ...] +>>> [fileid[:4] for fileid in inaugural.fileids()] +['1789', '1793', '1797', '1801', '1805', '1809', '1813', '1817', '1821', ...] +``` + +请注意,每个文本的年代都出现在它的文件名中。要从文件名中获得年代,我们使用`fileid[:4]`提取前四个字符。 + +让我们来看看词汇`America`和`citizen`随时间推移的使用情况。下面的代码使用`w.lower()`❶将就职演说语料库中的词汇转换成小写,然后用`startswith()`❶检查它们是否以“目标”词汇`america`或`citizen`开始。因此,它会计算如`American's`和`Citizens`等词。我们将在第 2 节学习条件频率分布,现在只考虑输出,如图 1.1 所示。 + +```py +>>> cfd = nltk.ConditionalFreqDist( +... (target, fileid[:4]) +... for fileid in inaugural.fileids() +... for w in inaugural.words(fileid) +... for target in ['america', 'citizen'] +... if w.lower().startswith(target)) ❶ +>>> cfd.plot() +``` + +![Images/4cdc400cf76b0354304e01aeb894877b.jpg](Images/4cdc400cf76b0354304e01aeb894877b.jpg) + +图 1.1:条件频率分布图:计数就职演说语料库中所有以`america`或`citizen`开始的词;每个演讲单独计数;这样就能观察出随时间变化用法上的演变趋势;计数没有与文档长度进行归一化处理。 + +## 1.6 标注文本语料库 + +许多文本语料库都包含语言学标注,有词性标注、命名实体、句法结构、语义角色等。NLTK 中提供了很方便的方式来访问这些语料库中的几个,还有一个包含语料库和语料样本的数据包,用于教学和科研的话可以免费下载。表 1.2 列出了其中一些语料库。有关下载信息请参阅`http://nltk.org/data`。关于如何访问 NLTK 语料库的其它例子,请在`http://nltk.org/howto`查阅语料库的 HOWTO。 + +表 1.2: + +NLTK 中的一些语料库和语料库样本:关于下载和使用它们,请参阅 NLTK 网站的信息。 + +```py +>>> nltk.corpus.cess_esp.words() +['El', 'grupo', 'estatal', 'Electricit\xe9_de_France', ...] +>>> nltk.corpus.floresta.words() +['Um', 'revivalismo', 'refrescante', 'O', '7_e_Meio', ...] +>>> nltk.corpus.indian.words('hindi.pos') +['पूर्ण', 'प्रतिबंध', 'हटाओ', ':', 'इराक', 'संयुक्त', ...] +>>> nltk.corpus.udhr.fileids() +['Abkhaz-Cyrillic+Abkh', 'Abkhaz-UTF8', 'Achehnese-Latin1', 'Achuar-Shiwiar-Latin1', +'Adja-UTF8', 'Afaan_Oromo_Oromiffa-Latin1', 'Afrikaans-Latin1', 'Aguaruna-Latin1', +'Akuapem_Twi-UTF8', 'Albanian_Shqip-Latin1', 'Amahuaca', 'Amahuaca-Latin1', ...] +>>> nltk.corpus.udhr.words('Javanese-Latin1')[11:] +['Saben', 'umat', 'manungsa', 'lair', 'kanthi', 'hak', ...] +``` + +这些语料库的最后,`udhr`,是超过 300 种语言的世界人权宣言。这个语料库的`fileids`包括有关文件所使用的字符编码,如`UTF8`或者`Latin1`。让我们用条件频率分布来研究`udhr`语料库中不同语言版本中的字长差异。图 1.2 中所示的输出(自己运行程序可以看到一个彩色图)。注意,`True`和`False`是 Python 内置的布尔值。 + +```py +>>> from nltk.corpus import udhr +>>> languages = ['Chickasaw', 'English', 'German_Deutsch', +... 'Greenlandic_Inuktikut', 'Hungarian_Magyar', 'Ibibio_Efik'] +>>> cfd = nltk.ConditionalFreqDist( +... (lang, len(word)) +... for lang in languages +... for word in udhr.words(lang + '-Latin1')) +>>> cfd.plot(cumulative=True) +``` + +![Images/da1752497a2a17be12b2acb282918a7a.jpg](Images/da1752497a2a17be12b2acb282918a7a.jpg) + +图 1.2:累积字长分布:世界人权宣言的 6 个翻译版本;此图显示,5 个或 5 个以下字母组成的词在 Ibibio 语言的文本中占约 80%,在德语文本中占 60%,在 Inuktitut 文本中占 25%。 + +注意 + +**轮到你来**:在`udhr.fileids()`中选择一种感兴趣的语言,定义一个变量`raw_text = udhr.raw('Language-Latin1')`。使用`nltk.FreqDist(raw_text).plot()`画出此文本的字母频率分布图。 + +不幸的是,许多语言没有大量的语料库。通常是政府或工业对发展语言资源的支持不够,个人的努力是零碎的,难以发现或重用。有些语言没有既定的书写系统,或濒临灭绝。(见第 7 节有关如何寻找语言资源的建议。) + +## 1.8 文本语料库的结构 + +到目前为止,我们已经看到了大量的语料库结构;1.3 总结了它们。最简单的一种没有任何结构,仅仅是一个文本集合。通常,文本会按照其可能对应的文体、来源、作者、语言等分类。有时,这些类别会重叠,尤其是在按主题分类的情况下,因为一个文本可能与多个主题相关。偶尔的,文本集有一个时间结构,新闻集合是最常见的例子。 + +![Images/7f97e7ac70a7c865fb1020795f6e7236.jpg](Images/7f97e7ac70a7c865fb1020795f6e7236.jpg) + +图 1.3:文本语料库的常见结构:最简单的一种语料库是一些孤立的没有什么特别的组织的文本集合;一些语料库按如文体(布朗语料库)等分类组织结构;一些分类会重叠,如主题类别(路透社语料库);另外一些语料库可以表示随时间变化语言用法的改变(就职演说语料库)。 + +表 1.3: + +NLTK 中定义的基本语料库函数:使用`help(nltk.corpus.reader)`可以找到更多的文档,也可以阅读`http://nltk.org/howto`上的在线语料库的 HOWTO。 + +```py +>>> raw = gutenberg.raw("burgess-busterbrown.txt") +>>> raw[1:20] +'The Adventures of B' +>>> words = gutenberg.words("burgess-busterbrown.txt") +>>> words[1:20] +['The', 'Adventures', 'of', 'Buster', 'Bear', 'by', 'Thornton', 'W', '.', +'Burgess', '1920', ']', 'I', 'BUSTER', 'BEAR', 'GOES', 'FISHING', 'Buster', +'Bear'] +>>> sents = gutenberg.sents("burgess-busterbrown.txt") +>>> sents[1:20] +[['I'], ['BUSTER', 'BEAR', 'GOES', 'FISHING'], ['Buster', 'Bear', 'yawned', 'as', +'he', 'lay', 'on', 'his', 'comfortable', 'bed', 'of', 'leaves', 'and', 'watched', +'the', 'first', 'early', 'morning', 'sunbeams', 'creeping', 'through', ...], ...] +``` + +## 1.9 加载你自己的语料库 + +如果你有自己收集的文本文件,并且想使用前面讨论的方法访问它们,你可以很容易地在 NLTK 中的`PlaintextCorpusReader`帮助下加载它们。检查你的文件在文件系统中的位置;在下面的例子中,我们假定你的文件在`/usr/share/dict`目录下。不管是什么位置,将变量`corpus_root`❶的值设置为这个目录。`PlaintextCorpusReader`初始化函数❷的第二个参数可以是一个如`['a.txt', 'test/b.txt']`这样的`fileids`列表,或者一个匹配所有`fileids`的模式,如`'[abc]/.*\.txt'`(关于正则表达式的信息见 3.4 节)。 + +```py +>>> from nltk.corpus import PlaintextCorpusReader +>>> corpus_root = '/usr/share/dict' ❶ +>>> wordlists = PlaintextCorpusReader(corpus_root, '.*') ❷ +>>> wordlists.fileids() +['README', 'connectives', 'propernames', 'web2', 'web2a', 'words'] +>>> wordlists.words('connectives') +['the', 'of', 'and', 'to', 'a', 'in', 'that', 'is', ...] +``` + +举另一个例子,假设你在本地硬盘上有自己的宾州树库(第 3 版)的拷贝,放在`C:\corpora`。我们可以使用`BracketParseCorpusReader`访问这些语料。我们指定`corpus_root`为存放语料库中解析过的《华尔街日报》部分❶的位置,并指定`file_pattern`与它的子文件夹中包含的文件匹配❷(用前斜杠)。 + +```py +>>> from nltk.corpus import BracketParseCorpusReader +>>> corpus_root = r"C:\corpora\penntreebank\parsed\mrg\wsj" ❶ +>>> file_pattern = r".*/wsj_.*\.mrg" ❷ +>>> ptb = BracketParseCorpusReader(corpus_root, file_pattern) +>>> ptb.fileids() +['00/wsj_0001.mrg', '00/wsj_0002.mrg', '00/wsj_0003.mrg', '00/wsj_0004.mrg', ...] +>>> len(ptb.sents()) +49208 +>>> ptb.sents(fileids='20/wsj_2013.mrg')[19] +['The', '55-year-old', 'Mr.', 'Noriega', 'is', "n't", 'as', 'smooth', 'as', 'the', +'shah', 'of', 'Iran', ',', 'as', 'well-born', 'as', 'Nicaragua', "'s", 'Anastasio', +'Somoza', ',', 'as', 'imperial', 'as', 'Ferdinand', 'Marcos', 'of', 'the', 'Philippines', +'or', 'as', 'bloody', 'as', 'Haiti', "'s", 'Baby', Doc', 'Duvalier', '.'] +``` + +## 2 条件频率分布 + +我们在第 3 节介绍了频率分布。我们看到给定某个词汇或其他元素的列表`mylist`,`FreqDist(mylist)`会计算列表中每个元素项目出现的次数。在这里,我们将推广这一想法。 + +当语料文本被分为几类,如文体、主题、作者等时,我们可以计算每个类别独立的频率分布。这将允许我们研究类别之间的系统性差异。在上一节中,我们是用 NLTK 的`ConditionalFreqDist`数据类型实现的。条件频率分布是频率分布的集合,每个频率分布有一个不同的“条件”。这个条件通常是文本的类别。2.1 描绘了一个带两个条件的条件频率分布的片段,一个是新闻文本,一个是言情文本。 + +![Images/b1aad2b60635723f14976fb5cb9ca372.jpg](Images/b1aad2b60635723f14976fb5cb9ca372.jpg) + +图 2.1:计数文本集合中单词出现次数(条件频率分布) + +## 2.1 条件和事件 + +频率分布计算观察到的事件,如文本中出现的词汇。条件频率分布需要给每个事件关联一个条件。所以不是处理一个单词词序列❶,我们必须处理的是一个配对序列❷: + +```py +>>> text = ['The', 'Fulton', 'County', 'Grand', 'Jury', 'said', ...] ❶ +>>> pairs = [('news', 'The'), ('news', 'Fulton'), ('news', 'County'), ...] ❷ +``` + +每个配对的形式是:`(条件, 事件)`。如果我们按文体处理整个布朗语料库,将有 15 个条件(每个文体一个条件)和 1,161,192 个事件(每一个词一个事件)。 + +## 2.2 按文体计数词汇 + +在 1 中,我们看到一个条件频率分布,其中条件为布朗语料库的每一节,并对每节计数词汇。`FreqDist()`以一个简单的列表作为输入,`ConditionalFreqDist()`以一个配对列表作为输入。 + +```py +>>> from nltk.corpus import brown +>>> cfd = nltk.ConditionalFreqDist( +... (genre, word) +... for genre in brown.categories() +... for word in brown.words(categories=genre)) +``` + +让我们拆开来看,只看两个文体,新闻和言情。对于每个文体❷,我们遍历文体中的每个词❸,以产生文体与词的配对❶ : + +```py +>>> genre_word = [(genre, word) ❶ +... for genre in ['news', 'romance'] ❷ +... for word in brown.words(categories=genre)] ❸ +>>> len(genre_word) +170576 +``` + +因此,在下面的代码中我们可以看到,列表`genre_word`的前几个配对将是`('news', word)`❶的形式,而最后几个配对将是`('romance', word)`❷的形式。 + +```py +>>> genre_word[:4] +[('news', 'The'), ('news', 'Fulton'), ('news', 'County'), ('news', 'Grand')] # [_start-genre] +>>> genre_word[-4:] +[('romance', 'afraid'), ('romance', 'not'), ('romance', "''"), ('romance', '.')] # [_end-genre] +``` + +现在,我们可以使用此配对列表创建一个`ConditionalFreqDist`,并将它保存在一个变量`cfd`中。像往常一样,我们可以输入变量的名称来检查它❶,并确认它有两个条件❷: + +```py +>>> cfd = nltk.ConditionalFreqDist(genre_word) +>>> cfd ❶ + +>>> cfd.conditions() +['news', 'romance'] # [_conditions-cfd] +``` + +让我们访问这两个条件,它们每一个都只是一个频率分布: + +```py +>>> print(cfd['news']) + +>>> print(cfd['romance']) + +>>> cfd['romance'].most_common(20) +[(',', 3899), ('.', 3736), ('the', 2758), ('and', 1776), ('to', 1502), +('a', 1335), ('of', 1186), ('``', 1045), ("''", 1044), ('was', 993), +('I', 951), ('in', 875), ('he', 702), ('had', 692), ('?', 690), +('her', 651), ('that', 583), ('it', 573), ('his', 559), ('she', 496)] +>>> cfd['romance']['could'] +193 +``` + +## 2.3 绘制分布图和分布表 + +除了组合两个或两个以上的频率分布和更容易初始化之外,`ConditionalFreqDist`还为制表和绘图提供了一些有用的方法。 + +1.1 是基于下面的代码产生的一个条件频率分布绘制的。条件是词`america`或`citizen`❷,被绘图的计数是在特定演讲中出现的词的次数。它利用了每个演讲的文件名——例如`1865-Lincoln.txt`——的前 4 个字符包含年代的事实❶。这段代码为文件`1865-Lincoln.txt`中每个小写形式以`america`开头的词——如`Americans`——产生一个配对`('america', '1865')`。 + +```py +>>> from nltk.corpus import inaugural +>>> cfd = nltk.ConditionalFreqDist( +... (target, fileid[:4]) ❶ +... for fileid in inaugural.fileids() +... for w in inaugural.words(fileid) +... for target in ['america', 'citizen'] ❷ +... if w.lower().startswith(target)) +``` + +图 1.2 也是基于下面的代码产生的一个条件频率分布绘制的。这次的条件是语言的名称,图中的计数来源于词长❶。它利用了每一种语言的文件名是语言名称后面跟`'-Latin1'`(字符编码)的事实。 + +```py +>>> from nltk.corpus import udhr +>>> languages = ['Chickasaw', 'English', 'German_Deutsch', +... 'Greenlandic_Inuktikut', 'Hungarian_Magyar', 'Ibibio_Efik'] +>>> cfd = nltk.ConditionalFreqDist( +... (lang, len(word)) ❶ +... for lang in languages +... for word in udhr.words(lang + '-Latin1')) +``` + +在`plot()`和`tabulate()`方法中,我们可以使用`conditions=`来选择指定哪些条件显示。如果我们忽略它,所有条件都会显示。同样,我们可以使用`samples=parameter`来限制要显示的样本。这使得载入大量数据到一个条件频率分布,然后通过选定条件和样品,绘图或制表的探索成为可能。这也使我们能全面控制条件和样本的显示顺序。例如:我们可以为两种语言和长度少于 10 个字符的词汇绘制累计频率数据表,如下所示。我们解释一下上排最后一个单元格中数值的含义是英文文本中 9 个或少于 9 个字符长的词有 1,638 个。 + +```py +>>> cfd.tabulate(conditions=['English', 'German_Deutsch'], +... samples=range(10), cumulative=True) + 0 1 2 3 4 5 6 7 8 9 + English 0 185 525 883 997 1166 1283 1440 1558 1638 +German_Deutsch 0 171 263 614 717 894 1013 1110 1213 1275 +``` + +注意 + +**轮到你来**:处理布朗语料库的新闻和言情文体,找出一周中最有新闻价值并且是最浪漫的日子。定义一个变量`days`,包含星期的列表,如`['Monday', ...]`。然后使用`cfd.tabulate(samples=days)`为这些词的计数制表。接下来用`plot`替代`tabulate`尝试同样的事情。你可以在额外的参数`samples=['Monday', ...]`的帮助下控制星期输出的顺序。 + +你可能已经注意到:我们已经在使用的条件频率分布看上去像列表推导,但是不带方括号。通常,我们使用列表推导作为一个函数的参数,如`set([w.lower() for w in t])`,忽略掉方括号而只写`set(w.lower() for w in t)`是允许的。(更多的讲解请参见 4.2 节“生成器表达式”的讨论。) + +## 2.4 使用双连词生成随机文本 + +我们可以使用条件频率分布创建一个双连词表(词对)。(我们在 3 中介绍过。)`bigrams()`函数接受一个单词列表,并建立一个连续的词对列表。记住,为了能看到结果而不是神秘的“生成器对象”,我们需要使用`list()`函数: + +```py +>>> sent = ['In', 'the', 'beginning', 'God', 'created', 'the', 'heaven', +... 'and', 'the', 'earth', '.'] +>>> list(nltk.bigrams(sent)) +[('In', 'the'), ('the', 'beginning'), ('beginning', 'God'), ('God', 'created'), +('created', 'the'), ('the', 'heaven'), ('heaven', 'and'), ('and', 'the'), +('the', 'earth'), ('earth', '.')] +``` + +在 2.2 中,我们把每个词作为一个条件,对每个词我们有效的创建它的后续词的频率分布。函数`generate_model()`包含一个简单的循环来生成文本。当我们调用这个函数时,我们选择一个词(如`'living'`)作为我们的初始内容,然后进入循环,我们输入变量`word`的当前值,重新设置`word`为上下文中最可能的词符(使用`max()`);下一次进入循环,我们使用那个词作为新的初始内容。正如你通过检查输出可以看到的,这种简单的文本生成方法往往会在循环中卡住;另一种方法是从可用的词汇中随机选择下一个词。 + +```py +def generate_model(cfdist, word, num=15): + for i in range(num): + print(word, end=' ') + word = cfdist[word].max() + +text = nltk.corpus.genesis.words('english-kjv.txt') +bigrams = nltk.bigrams(text) +cfd = nltk.ConditionalFreqDist(bigrams) ❶ +``` + +条件频率分布是一个对许多 NLP 任务都有用的数据结构。2.1 总结了它们常用的方法。 + +表 2.1: + +NLTK 中的条件频率分布:定义、访问和可视化一个计数的条件频率分布的常用方法和习惯用法。 + +```py +print('Monty Python') + +``` + +你也可以输入`from monty import *`,它将做同样的事情。 + +从现在起,你可以选择使用交互式解释器或文本编辑器来创建你的程序。使用解释器测试你的想法往往比较方便,修改一行代码直到达到你期望的效果。测试好之后,你就可以将代码粘贴到文本编辑器(去除所有`>>>`和`...`提示符),继续扩展它。给文件一个小而准确的名字,使用所有的小写字母,用下划线分割词汇,使用`.py`文件名后缀,例如`monty_python.py`。 + +注意 + +**要点**:我们的内联代码的例子包含`>>>`和`...`提示符,好像我们正在直接与解释器交互。随着程序变得更加复杂,你应该在编辑器中输入它们,没有提示符,如前面所示的那样在编辑器中运行它们。当我们在这本书中提供更长的程序时,我们将不使用提示符以提醒你在文件中输入它而不是使用解释器。你可以看到 2.2 已经这样了。请注意,这个例子还包括两行代码带有 Python 提示符;它是任务的互动部分,在这里你观察一些数据,并调用一个函数。请记住,像 2.2 这样的所有示例代码都可以从`http://nltk.org/`下载。 + +## 3.2 函数 + +假设你正在分析一些文本,这些文本包含同一个词的不同形式,你的一部分程序需要将给定的单数名词变成复数形式。假设需要在两个地方做这样的事,一个是处理一些文本,另一个是处理用户的输入。 + +比起重复相同的代码好几次,把这些事情放在一个函数中会更有效和可靠。一个函数是命名的代码块,执行一些明确的任务,就像我们在 1 中所看到的那样。一个函数通常被定义来使用一些称为参数的变量接受一些输入,并且它可能会产生一些结果,也称为返回值。我们使用关键字`def`加函数名以及所有输入参数来定义一个函数,接下来是函数的主体。这里是我们在 1 看到的函数(对于 Python 2,请包含`import`语句,这样可以使除法像我们期望的那样运算): + +```py +>>> from __future__ import division +>>> def lexical_diversity(text): +... return len(text) / len(set(text)) +``` + +我们使用关键字`return`表示函数作为输出而产生的值。在这个例子中,函数所有的工作都在`return`语句中完成。下面是一个等价的定义,使用多行代码做同样的事。我们将把参数名称从`text`变为`my_text_data`,注意这只是一个任意的选择: + +```py +>>> def lexical_diversity(my_text_data): +... word_count = len(my_text_data) +... vocab_size = len(set(my_text_data)) +... diversity_score = vocab_size / word_count +... return diversity_score +``` + +请注意,我们已经在函数体内部创造了一些新的变量。这些是局部变量,不能在函数体外访问。现在我们已经定义一个名为`lexical_diversity`的函数。但只定义它不会产生任何输出!函数在被“调用”之前不会做任何事情: + +```py +>>> from nltk.corpus import genesis +>>> kjv = genesis.words('english-kjv.txt') +>>> lexical_diversity(kjv) +0.06230453042623537 +``` + +让我们回到前面的场景,实际定义一个简单的函数来处理英文的复数词。3.1 中的函数`plural()`接受单数名词,产生一个复数形式,虽然它并不总是正确的。(我们将在 4.4 中以更长的篇幅讨论这个函数。) + +```py +def plural(word): + if word.endswith('y'): + return word[:-1] + 'ies' + elif word[-1] in 'sx' or word[-2:] in ['sh', 'ch']: + return word + 'es' + elif word.endswith('an'): + return word[:-2] + 'en' + else: + return word + 's' +``` + +`endswith()`函数总是与一个字符串对象一起使用(如 3.1 中的`word`)。要调用此函数,我们使用对象的名字,一个点,然后跟函数的名称。这些函数通常被称为方法。 + +## 3.3 模块 + +随着时间的推移,你将会发现你创建了大量小而有用的文字处理函数,结果你不停的把它们从老程序复制到新程序中。哪个文件中包含的才是你要使用的函数的最新版本?如果你能把你的劳动成果收集在一个单独的地方,而且访问以前定义的函数不必复制,生活将会更加轻松。 + +要做到这一点,请将你的函数保存到一个文件`text_proc.py`。现在,你可以简单的通过从文件导入它来访问你的函数: + +```py +>>> from text_proc import plural +>>> plural('wish') +wishes +>>> plural('fan') +fen +``` + +显然,我们的复数函数明显存在错误,因为`fan`的复数是`fans`。不必再重新输入这个函数的新版本,我们可以简单的编辑现有的。因此,在任何时候我们的复数函数只有一个版本,不会再有使用哪个版本的困扰。 + +在一个文件中的变量和函数定义的集合被称为一个 Python 模块。相关模块的集合称为一个包。处理布朗语料库的 NLTK 代码是一个模块,处理各种不同的语料库的代码的集合是一个包。NLTK 的本身是包的集合,有时被称为一个库。 + +小心! + +如果你正在创建一个包含一些你自己的 Python 代码的文件,一定*不*要将文件命名为`nltk.py`:这可能会在导入时占据“真正的”NLTK 包。当 Python 导入模块时,它先查找当前目录(文件夹)。 + +## 4 词汇资源 + +词典或者词典资源是一个词和/或短语以及一些相关信息的集合,例如:词性和词意定义等相关信息。词典资源附属于文本,通常在文本的帮助下创建和丰富。例如:如果我们定义了一个文本`my_text`,然后`vocab = sorted(set(my_text))`建立`my_text`的词汇,同时`word_freq = FreqDist(my_text)`计数文本中每个词的频率。`vocab`和`word_freq`都是简单的词汇资源。同样,如我们在 1 中看到的,词汇索引为我们提供了有关词语用法的信息,可能在编写词典时有用。4.1 中描述了词汇相关的标准术语。一个词项包括词目(也叫词条)以及其他附加信息,例如词性和词意定义。两个不同的词拼写相同被称为同音异义词。 + +![Images/1b33abb14fc8fe7c704d005736ddb323.jpg](Images/1b33abb14fc8fe7c704d005736ddb323.jpg) + +图 4.1:词典术语:两个拼写相同的词条(同音异义词)的词汇项,包括词性和注释信息。 + +最简单的词典是除了一个词汇列表外什么也没有。复杂的词典资源包括在词汇项内和跨词汇项的复杂的结构。在本节,我们来看看 NLTK 中的一些词典资源。 + +## 4.1 词汇列表语料库 + +NLTK 包括一些仅仅包含词汇列表的语料库。词汇语料库是 Unix 中的`/usr/share/dict/words`文件,被一些拼写检查程序使用。我们可以用它来寻找文本语料中不寻常的或拼写错误的词汇,如 4.2 所示。 + +```py +def unusual_words(text): + text_vocab = set(w.lower() for w in text if w.isalpha()) + english_vocab = set(w.lower() for w in nltk.corpus.words.words()) + unusual = text_vocab - english_vocab + return sorted(unusual) + +>>> unusual_words(nltk.corpus.gutenberg.words('austen-sense.txt')) +['abbeyland', 'abhorred', 'abilities', 'abounded', 'abridgement', 'abused', 'abuses', +'accents', 'accepting', 'accommodations', 'accompanied', 'accounted', 'accounts', +'accustomary', 'aches', 'acknowledging', 'acknowledgment', 'acknowledgments', ...] +>>> unusual_words(nltk.corpus.nps_chat.words()) +['aaaaaaaaaaaaaaaaa', 'aaahhhh', 'abortions', 'abou', 'abourted', 'abs', 'ack', +'acros', 'actualy', 'adams', 'adds', 'adduser', 'adjusts', 'adoted', 'adreniline', +'ads', 'adults', 'afe', 'affairs', 'affari', 'affects', 'afk', 'agaibn', 'ages', ...] +``` + +还有一个停用词语料库,就是那些高频词汇,如`the`,`to`和`also`,我们有时在进一步的处理之前想要将它们从文档中过滤。停用词通常几乎没有什么词汇内容,而它们的出现会使区分文本变困难。 + +```py +>>> from nltk.corpus import stopwords +>>> stopwords.words('english') +['i', 'me', 'my', 'myself', 'we', 'our', 'ours', 'ourselves', 'you', 'your', 'yours', +'yourself', 'yourselves', 'he', 'him', 'his', 'himself', 'she', 'her', 'hers', +'herself', 'it', 'its', 'itself', 'they', 'them', 'their', 'theirs', 'themselves', +'what', 'which', 'who', 'whom', 'this', 'that', 'these', 'those', 'am', 'is', 'are', +'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had', 'having', 'do', 'does', +'did', 'doing', 'a', 'an', 'the', 'and', 'but', 'if', 'or', 'because', 'as', 'until', +'while', 'of', 'at', 'by', 'for', 'with', 'about', 'against', 'between', 'into', +'through', 'during', 'before', 'after', 'above', 'below', 'to', 'from', 'up', 'down', +'in', 'out', 'on', 'off', 'over', 'under', 'again', 'further', 'then', 'once', 'here', +'there', 'when', 'where', 'why', 'how', 'all', 'any', 'both', 'each', 'few', 'more', +'most', 'other', 'some', 'such', 'no', 'nor', 'not', 'only', 'own', 'same', 'so', +'than', 'too', 'very', 's', 't', 'can', 'will', 'just', 'don', 'should', 'now'] +``` + +让我们定义一个函数来计算文本中*没有*在停用词列表中的词的比例: + +```py +>>> def content_fraction(text): +... stopwords = nltk.corpus.stopwords.words('english') +... content = [w for w in text if w.lower() not in stopwords] +... return len(content) / len(text) +... +>>> content_fraction(nltk.corpus.reuters.words()) +0.7364374824583169 +``` + +因此,在停用词的帮助下,我们筛选掉文本中四分之一的词。请注意,我们在这里结合了两种不同类型的语料库,使用词典资源来过滤文本语料的内容。 + +![Images/b2af1426c6cd2403c8b938eb557a99d1.jpg](Images/b2af1426c6cd2403c8b938eb557a99d1.jpg) + +图 4.3:一个字母拼词谜题::在由随机选择的字母组成的网格中,选择里面的字母组成词;这个谜题叫做“目标”。 + +一个词汇列表对解决如图 4.3 中这样的词的谜题很有用。我们的程序遍历每一个词,对于每一个词检查是否符合条件。检查必须出现的字母❷和长度限制❶是很容易的(这里我们只查找 6 个或 6 个以上字母的词)。只使用指定的字母组合作为候选方案,尤其是一些指定的字母出现了两次(这里如字母`v`)这样的检查是很棘手的。`FreqDist`比较法❸允许我们检查每个*字母*在候选词中的频率是否小于或等于相应的字母在拼词谜题中的频率。 + +```py +>>> puzzle_letters = nltk.FreqDist('egivrvonl') +>>> obligatory = 'r' +>>> wordlist = nltk.corpus.words.words() +>>> [w for w in wordlist if len(w) >= 6 ❶ +... and obligatory in w ❷ +... and nltk.FreqDist(w) <= puzzle_letters] ❸ +['glover', 'gorlin', 'govern', 'grovel', 'ignore', 'involver', 'lienor', +'linger', 'longer', 'lovering', 'noiler', 'overling', 'region', 'renvoi', +'revolving', 'ringle', 'roving', 'violer', 'virole'] +``` + +另一个词汇列表是名字语料库,包括 8000 个按性别分类的名字。男性和女性的名字存储在单独的文件中。让我们找出同时出现在两个文件中的名字,即性别暧昧的名字: + +```py +>>> names = nltk.corpus.names +>>> names.fileids() +['female.txt', 'male.txt'] +>>> male_names = names.words('male.txt') +>>> female_names = names.words('female.txt') +>>> [w for w in male_names if w in female_names] +['Abbey', 'Abbie', 'Abby', 'Addie', 'Adrian', 'Adrien', 'Ajay', 'Alex', 'Alexis', +'Alfie', 'Ali', 'Alix', 'Allie', 'Allyn', 'Andie', 'Andrea', 'Andy', 'Angel', +'Angie', 'Ariel', 'Ashley', 'Aubrey', 'Augustine', 'Austin', 'Averil', ...] +``` + +正如大家都知道的,以字母`a`结尾的名字几乎都是女性。我们可以在 4.4 中看到这一点以及一些其它的模式,该图是由下面的代码产生的。请记住`name[-1]`是`name`的最后一个字母。 + +```py +>>> cfd = nltk.ConditionalFreqDist( +... (fileid, name[-1]) +... for fileid in names.fileids() +... for name in names.words(fileid)) +>>> cfd.plot() +``` + +![Images/5e197b7d253f66454a97af2a93c30a8e.jpg](Images/5e197b7d253f66454a97af2a93c30a8e.jpg) + +图 4.4:条件频率分布:此图显示男性和女性名字的结尾字母;大多数以`a, e`或`i`结尾的名字是女性;以`h`和`l`结尾的男性和女性同样多;以`k, o, r, s`和`t`结尾的更可能是男性。 + +## 4.2 发音的词典 + +一个稍微丰富的词典资源是一个表格(或电子表格),在每一行中含有一个词加一些性质。NLTK 中包括美国英语的 CMU 发音词典,它是为语音合成器使用而设计的。 + +```py +>>> entries = nltk.corpus.cmudict.entries() +>>> len(entries) +133737 +>>> for entry in entries[42371:42379]: +... print(entry) +... +('fir', ['F', 'ER1']) +('fire', ['F', 'AY1', 'ER0']) +('fire', ['F', 'AY1', 'R']) +('firearm', ['F', 'AY1', 'ER0', 'AA2', 'R', 'M']) +('firearm', ['F', 'AY1', 'R', 'AA2', 'R', 'M']) +('firearms', ['F', 'AY1', 'ER0', 'AA2', 'R', 'M', 'Z']) +('firearms', ['F', 'AY1', 'R', 'AA2', 'R', 'M', 'Z']) +('fireball', ['F', 'AY1', 'ER0', 'B', 'AO2', 'L']) +``` + +对每一个词,这个词典资源提供语音的代码——不同的声音不同的标签——叫做`phones`。请看`fire`有两个发音(美国英语中):单音节`F AY1 R`和双音节`F AY1 ER0`。CMU 发音词典中的符号是从 *Arpabet* 来的,更多的细节请参考`http://en.wikipedia.org/wiki/Arpabet`。 + +每个条目由两部分组成,我们可以用一个复杂的`for`语句来一个一个的处理这些。我们没有写`for entry in entries:`,而是用*两个*变量名`word, pron`替换`entry`❶。现在,每次通过循环时,`word`被分配条目的第一部分,`pron`被分配条目的第二部分: + +```py +>>> for word, pron in entries: ❶ +... if len(pron) == 3: ❷ +... ph1, ph2, ph3 = pron ❸ +... if ph1 == 'P' and ph3 == 'T': +... print(word, ph2, end=' ') +... +pait EY1 pat AE1 pate EY1 patt AE1 peart ER1 peat IY1 peet IY1 peete IY1 pert ER1 +pet EH1 pete IY1 pett EH1 piet IY1 piette IY1 pit IH1 pitt IH1 pot AA1 pote OW1 +pott AA1 pout AW1 puett UW1 purt ER1 put UH1 putt AH1 +``` + +上面的程序扫描词典中那些发音包含三个音素的条目❷。如果条件为真,就将`pron`的内容分配给三个新的变量:`ph1`, `ph2`和`ph3`。请注意实现这个功能的语句的形式并不多见❸。 + +这里是同样的`for`语句的另一个例子,这次使用内部的列表推导。这段程序找到所有发音结尾与`nicks`相似的词汇。你可以使用此方法来找到押韵的词。 + +```py +>>> syllable = ['N', 'IH0', 'K', 'S'] +>>> [word for word, pron in entries if pron[-4:] == syllable] +["atlantic's", 'audiotronics', 'avionics', 'beatniks', 'calisthenics', 'centronics', +'chamonix', 'chetniks', "clinic's", 'clinics', 'conics', 'conics', 'cryogenics', +'cynics', 'diasonics', "dominic's", 'ebonics', 'electronics', "electronics'", ...] +``` + +请注意,有几种方法来拼读一个读音:`nics`, `niks`, `nix`甚至`ntic's`加一个无声的`t`,如词`atlantic's`。让我们来看看其他一些发音与书写之间的不匹配。你可以总结一下下面的例子的功能,并解释它们是如何实现的? + +```py +>>> [w for w, pron in entries if pron[-1] == 'M' and w[-1] == 'n'] +['autumn', 'column', 'condemn', 'damn', 'goddamn', 'hymn', 'solemn'] +>>> sorted(set(w[:2] for w, pron in entries if pron[0] == 'N' and w[0] != 'n')) +['gn', 'kn', 'mn', 'pn'] +``` + +音素包含数字表示主重音`(1)`,次重音`(2)`和无重音`(0)`。作为我们最后的一个例子,我们定义一个函数来提取重音数字,然后扫描我们的词典,找到具有特定重音模式的词汇。 + +```py +>>> def stress(pron): +... return [char for phone in pron for char in phone if char.isdigit()] +>>> [w for w, pron in entries if stress(pron) == ['0', '1', '0', '2', '0']] +['abbreviated', 'abbreviated', 'abbreviating', 'accelerated', 'accelerating', +'accelerator', 'accelerators', 'accentuated', 'accentuating', 'accommodated', +'accommodating', 'accommodative', 'accumulated', 'accumulating', 'accumulative', ...] +>>> [w for w, pron in entries if stress(pron) == ['0', '2', '0', '1', '0']] +['abbreviation', 'abbreviations', 'abomination', 'abortifacient', 'abortifacients', +'academicians', 'accommodation', 'accommodations', 'accreditation', 'accreditations', +'accumulation', 'accumulations', 'acetylcholine', 'acetylcholine', 'adjudication', ...] +``` + +注意 + +这段程序的精妙之处在于:我们的用户自定义函数`stress()`调用一个内含条件的列表推导。还有一个双层嵌套`for`循环。这里有些复杂,等你有了更多的使用列表推导的经验后,你可能会想回过来重新阅读。 + +我们可以使用条件频率分布来帮助我们找到词汇的最小受限集合。在这里,我们找到所有 p 开头的三音素词❷,并按照它们的第一个和最后一个音素来分组❶。 + +```py +>>> p3 = [(pron[0]+'-'+pron[2], word) ❶ +... for (word, pron) in entries +... if pron[0] == 'P' and len(pron) == 3] ❷ +>>> cfd = nltk.ConditionalFreqDist(p3) +>>> for template in sorted(cfd.conditions()): +... if len(cfd[template]) > 10: +... words = sorted(cfd[template]) +... wordstring = ' '.join(words) +... print(template, wordstring[:70] + "...") +... +P-CH patch pautsch peach perch petsch petsche piche piech pietsch pitch pit... +P-K pac pack paek paik pak pake paque peak peake pech peck peek perc perk ... +P-L pahl pail paille pal pale pall paul paule paull peal peale pearl pearl... +P-N paign pain paine pan pane pawn payne peine pen penh penn pin pine pinn... +P-P paap paape pap pape papp paup peep pep pip pipe pipp poop pop pope pop... +P-R paar pair par pare parr pear peer pier poor poore por pore porr pour... +P-S pace pass pasts peace pearse pease perce pers perse pesce piece piss p... +P-T pait pat pate patt peart peat peet peete pert pet pete pett piet piett... +P-UW1 peru peugh pew plew plue prew pru prue prugh pshew pugh... +``` + +我们可以通过查找特定词汇来访问词典,而不必遍历整个词典。我们将使用 Python 的词典数据结构,在 3 节我们将系统的学习它。通过指定词典的名字后面跟一个包含在方括号里的关键字(例如词`'fire'`)来查词典❶。 + +```py +>>> prondict = nltk.corpus.cmudict.dict() +>>> prondict['fire'] ❶ +[['F', 'AY1', 'ER0'], ['F', 'AY1', 'R']] +>>> prondict['blog'] ❷ +Traceback (most recent call last): + File "", line 1, in +KeyError: 'blog' +>>> prondict['blog'] = [['B', 'L', 'AA1', 'G']] ❸ +>>> prondict['blog'] +[['B', 'L', 'AA1', 'G']] +``` + +如果我们试图查找一个不存在的关键字❷,就会得到一个`KeyError`。这与我们使用一个过大的整数索引一个列表时产生一个`IndexError`是类似的。词`blog`在发音词典中没有,所以我们对我们自己版本的词典稍作调整,为这个关键字分配一个值❸(这对 NLTK 语料库是没有影响的;下一次我们访问它,`blog`依然是空的)。 + +我们可以用任何词典资源来处理文本,如过滤掉具有某些词典属性的词(如名词),或者映射文本中每一个词。例如,下面的文本到发音函数在发音词典中查找文本中每个词: + +```py +>>> text = ['natural', 'language', 'processing'] +>>> [ph for w in text for ph in prondict[w][0]] +['N', 'AE1', 'CH', 'ER0', 'AH0', 'L', 'L', 'AE1', 'NG', 'G', 'W', 'AH0', 'JH', +'P', 'R', 'AA1', 'S', 'EH0', 'S', 'IH0', 'NG'] +``` + +## 4.3 比较词表 + +表格词典的另一个例子是比较词表。NLTK 中包含了所谓的斯瓦迪士核心词列表,几种语言中约 200 个常用词的列表。语言标识符使用 ISO639 双字母码。 + +```py +>>> from nltk.corpus import swadesh +>>> swadesh.fileids() +['be', 'bg', 'bs', 'ca', 'cs', 'cu', 'de', 'en', 'es', 'fr', 'hr', 'it', 'la', 'mk', +'nl', 'pl', 'pt', 'ro', 'ru', 'sk', 'sl', 'sr', 'sw', 'uk'] +>>> swadesh.words('en') +['I', 'you (singular), thou', 'he', 'we', 'you (plural)', 'they', 'this', 'that', +'here', 'there', 'who', 'what', 'where', 'when', 'how', 'not', 'all', 'many', 'some', +'few', 'other', 'one', 'two', 'three', 'four', 'five', 'big', 'long', 'wide', ...] +``` + +我们可以通过在`entries()`方法中指定一个语言列表来访问多语言中的同源词。更进一步,我们可以把它转换成一个简单的词典(我们将在 3 学到`dict()`函数)。 + +```py +>>> fr2en = swadesh.entries(['fr', 'en']) +>>> fr2en +[('je', 'I'), ('tu, vous', 'you (singular), thou'), ('il', 'he'), ...] +>>> translate = dict(fr2en) +>>> translate['chien'] +'dog' +>>> translate['jeter'] +'throw' +``` + +通过添加其他源语言,我们可以让我们这个简单的翻译器更为有用。让我们使用`dict()`函数把德语-英语和西班牙语-英语对相互转换成一个词典,然后用这些添加的映射*更新*我们原来的`translate`词典: + +```py +>>> de2en = swadesh.entries(['de', 'en']) # German-English +>>> es2en = swadesh.entries(['es', 'en']) # Spanish-English +>>> translate.update(dict(de2en)) +>>> translate.update(dict(es2en)) +>>> translate['Hund'] +'dog' +>>> translate['perro'] +'dog' +``` + +我们可以比较日尔曼语族和拉丁语族的不同: + +```py +>>> languages = ['en', 'de', 'nl', 'es', 'fr', 'pt', 'la'] +>>> for i in [139, 140, 141, 142]: +... print(swadesh.entries(languages)[i]) +... +('say', 'sagen', 'zeggen', 'decir', 'dire', 'dizer', 'dicere') +('sing', 'singen', 'zingen', 'cantar', 'chanter', 'cantar', 'canere') +('play', 'spielen', 'spelen', 'jugar', 'jouer', 'jogar, brincar', 'ludere') +('float', 'schweben', 'zweven', 'flotar', 'flotter', 'flutuar, boiar', 'fluctuare') +``` + +## 4.4 词汇工具:Shoebox 和 Toolbox + +可能最流行的语言学家用来管理数据的工具是 *Toolbox*,以前叫做 *Shoebox*,因为它用满满的档案卡片占据了语言学家的旧鞋盒。Toolbox 可以免费从`http://www.sil.org/computing/toolbox/`下载。 + +一个 Toolbox 文件由一个大量条目的集合组成,其中每个条目由一个或多个字段组成。大多数字段都是可选的或重复的,这意味着这个词汇资源不能作为一个表格或电子表格来处理。 + +下面是一个罗托卡特语的词典。我们只看第一个条目,词`kaa`的意思是`"to gag"`: + +```py +>>> from nltk.corpus import toolbox +>>> toolbox.entries('rotokas.dic') +[('kaa', [('ps', 'V'), ('pt', 'A'), ('ge', 'gag'), ('tkp', 'nek i pas'), +('dcsv', 'true'), ('vx', '1'), ('sc', '???'), ('dt', '29/Oct/2005'), +('ex', 'Apoka ira kaaroi aioa-ia reoreopaoro.'), +('xp', 'Kaikai i pas long nek bilong Apoka bikos em i kaikai na toktok.'), +('xe', 'Apoka is gagging from food while talking.')]), ...] +``` + +条目包括一系列的属性-值对,如`('ps', 'V')`表示词性是`'V'`(动词),`('ge', 'gag')`表示英文注释是'`'gag'`。最后的 3 个配对包含一个罗托卡特语例句和它的巴布亚皮钦语及英语翻译。 + +Toolbox 文件松散的结构使我们在现阶段很难更好的利用它。XML 提供了一种强有力的方式来处理这种语料库,我们将在 11 回到这个的主题。 + +注意 + +罗托卡特语是巴布亚新几内亚的布干维尔岛上使用的一种语言。这个词典资源由 Stuart Robinson 贡献给 NLTK。罗托卡特语以仅有 12 个音素(彼此对立的声音)而闻名。详情请参考:`http://en.wikipedia.org/wiki/Rotokas_language` + +## 5 WordNet + +WordNet 是面向语义的英语词典,类似与传统辞典,但具有更丰富的结构。NLTK 中包括英语 WordNet,共有 155,287 个词和 117,659 个同义词集合。我们将以寻找同义词和它们在 WordNet 中如何访问开始。 + +## 5.1 意义与同义词 + +考虑`(1a)`中的句子。如果我们用`automobile`替换掉`(1a)`中的词`motorcar`,变成`(1b)`,句子的意思几乎保持不变: + +```py +>>> from nltk.corpus import wordnet as wn +>>> wn.synsets('motorcar') +[Synset('car.n.01')] +``` + +因此,`motorcar`只有一个可能的含义,它被定义为`car.n.01`,`car`的第一个名词意义。`car.n.01`被称为 Synset 或“同义词集”,意义相同的词(或“词条”)的集合: + +```py +>>> wn.synset('car.n.01').lemma_names() +['car', 'auto', 'automobile', 'machine', 'motorcar'] +``` + +同义词集中的每个词可以有多种含义,例如:`car`也可能是火车车厢、一个货车或电梯厢。但我们只对这个同义词集中所有词来说最常用的一个意义感兴趣。同义词集也有一些一般的定义和例句: + +```py +>>> wn.synset('car.n.01').definition() +'a motor vehicle with four wheels; usually propelled by an internal combustion engine' +>>> wn.synset('car.n.01').examples() +['he needs a car to get to work'] +``` + +虽然定义帮助人们了解一个同义词集的本意,同义词集中的词往往对我们的程序更有用。为了消除歧义,我们将这些词标记为`car.n.01.automobile`,`car.n.01.motorcar`等。这种同义词集和词的配对叫做词条。我们可以得到指定同义词集的所有词条❶,查找特定的词条❷,得到一个词条对应的同义词集❸,也可以得到一个词条的“名字”❹: + +```py +>>> wn.synset('car.n.01').lemmas() ❶ +[Lemma('car.n.01.car'), Lemma('car.n.01.auto'), Lemma('car.n.01.automobile'), +Lemma('car.n.01.machine'), Lemma('car.n.01.motorcar')] +>>> wn.lemma('car.n.01.automobile') ❷ +Lemma('car.n.01.automobile') +>>> wn.lemma('car.n.01.automobile').synset() ❸ +Synset('car.n.01') +>>> wn.lemma('car.n.01.automobile').name() ❹ +'automobile' +``` + +与词`motorcar`意义明确且只有一个同义词集不同,词`car`是含糊的,有五个同义词集: + +```py +>>> wn.synsets('car') +[Synset('car.n.01'), Synset('car.n.02'), Synset('car.n.03'), Synset('car.n.04'), +Synset('cable_car.n.01')] +>>> for synset in wn.synsets('car'): +... print(synset.lemma_names()) +... +['car', 'auto', 'automobile', 'machine', 'motorcar'] +['car', 'railcar', 'railway_car', 'railroad_car'] +['car', 'gondola'] +['car', 'elevator_car'] +['cable_car', 'car'] +``` + +为方便起见,我们可以用下面的方式访问所有包含词`car`的词条。 + +```py +>>> wn.lemmas('car') +[Lemma('car.n.01.car'), Lemma('car.n.02.car'), Lemma('car.n.03.car'), +Lemma('car.n.04.car'), Lemma('cable_car.n.01.car')] +``` + +注意 + +**轮到你来**:写下词`dish`的你能想到的所有意思。现在,在 WordNet 的帮助下使用前面所示的相同的操作探索这个词。 + +## 5.2 WordNet 的层次结构 + +WordNet 的同义词集对应于抽象的概念,它们并不总是有对应的英语词汇。这些概念在层次结构中相互联系在一起。一些概念也很一般,如*实体*、*状态*、*事件*;这些被称为唯一前缀或者根同义词集。其他的,如`gas guzzer`和`hatch-back`等就比较具体的多。5.1 展示了一个概念层次的一小部分。 + +![Images/74248e04835acdba414fd407bb4f3241.jpg](Images/74248e04835acdba414fd407bb4f3241.jpg) + +图 5.1:WordNet 概念层次片段:每个节点对应一个同义词集;边表示上位词/下位词关系,即上级概念与从属概念的关系。 + +WordNet 使在概念之间漫游变的容易。例如:一个如`motorcar`这样的概念,我们可以看到它的更加具体(直接)的概念——下位词。 + +```py +>>> motorcar = wn.synset('car.n.01') +>>> types_of_motorcar = motorcar.hyponyms() +>>> types_of_motorcar[0] +Synset('ambulance.n.01') +>>> sorted(lemma.name() for synset in types_of_motorcar for lemma in synset.lemmas()) +['Model_T', 'S.U.V.', 'SUV', 'Stanley_Steamer', 'ambulance', 'beach_waggon', +'beach_wagon', 'bus', 'cab', 'compact', 'compact_car', 'convertible', +'coupe', 'cruiser', 'electric', 'electric_automobile', 'electric_car', +'estate_car', 'gas_guzzler', 'hack', 'hardtop', 'hatchback', 'heap', +'horseless_carriage', 'hot-rod', 'hot_rod', 'jalopy', 'jeep', 'landrover', +'limo', 'limousine', 'loaner', 'minicar', 'minivan', 'pace_car', 'patrol_car', +'phaeton', 'police_car', 'police_cruiser', 'prowl_car', 'race_car', 'racer', +'racing_car', 'roadster', 'runabout', 'saloon', 'secondhand_car', 'sedan', +'sport_car', 'sport_utility', 'sport_utility_vehicle', 'sports_car', 'squad_car', +'station_waggon', 'station_wagon', 'stock_car', 'subcompact', 'subcompact_car', +'taxi', 'taxicab', 'tourer', 'touring_car', 'two-seater', 'used-car', 'waggon', +'wagon'] +``` + +我们也可以通过访问上位词来浏览层次结构。有些词有多条路径,因为它们可以归类在一个以上的分类中。`car.n.01`与`entity.n.01`间有两条路径,因为`wheeled_vehicle.n.01`可以同时被归类为车辆和容器。 + +```py +>>> motorcar.hypernyms() +[Synset('motor_vehicle.n.01')] +>>> paths = motorcar.hypernym_paths() +>>> len(paths) +2 +>>> [synset.name() for synset in paths[0]] +['entity.n.01', 'physical_entity.n.01', 'object.n.01', 'whole.n.02', 'artifact.n.01', +'instrumentality.n.03', 'container.n.01', 'wheeled_vehicle.n.01', +'self-propelled_vehicle.n.01', 'motor_vehicle.n.01', 'car.n.01'] +>>> [synset.name() for synset in paths[1]] +['entity.n.01', 'physical_entity.n.01', 'object.n.01', 'whole.n.02', 'artifact.n.01', +'instrumentality.n.03', 'conveyance.n.03', 'vehicle.n.01', 'wheeled_vehicle.n.01', +'self-propelled_vehicle.n.01', 'motor_vehicle.n.01', 'car.n.01'] +``` + +我们可以用如下方式得到一个最一般的上位(或根上位)同义词集: + +```py +>>> motorcar.root_hypernyms() +[Synset('entity.n.01')] +``` + +注意 + +**轮到你来**:尝试 NLTK 中便捷的图形化 WordNet 浏览器:`nltk.app.wordnet()`。沿着上位词与下位词之间的链接,探索 WordNet 的层次结构。 + +## 5.3 更多的词汇关系 + +上位词和下位词被称为词汇关系,因为它们是同义集之间的关系。这个关系定位上下为“是一个”层次。WordNet 网络另一个重要的漫游方式是从元素到它们的部件(部分)或到它们被包含其中的东西(整体)。例如,一棵树的部分是它的树干,树冠等;这些都是`part_meronyms()`。一棵树的*实质*是包括心材和边材组成的,即`substance_meronyms()`。树木的集合形成了一个森林,即`member_holonyms()`: + +```py +>>> wn.synset('tree.n.01').part_meronyms() +[Synset('burl.n.02'), Synset('crown.n.07'), Synset('limb.n.02'), +Synset('stump.n.01'), Synset('trunk.n.01')] +>>> wn.synset('tree.n.01').substance_meronyms() +[Synset('heartwood.n.01'), Synset('sapwood.n.01')] +>>> wn.synset('tree.n.01').member_holonyms() +[Synset('forest.n.01')] +``` + +来看看可以获得多么复杂的东西,考虑具有几个密切相关意思的词`mint`。我们可以看到`mint.n.04`是`mint.n.02`的一部分,是组成`mint.n.05`的材质。 + +```py +>>> for synset in wn.synsets('mint', wn.NOUN): +... print(synset.name() + ':', synset.definition()) +... +batch.n.02: (often followed by `of') a large number or amount or extent +mint.n.02: any north temperate plant of the genus Mentha with aromatic leaves and + small mauve flowers +mint.n.03: any member of the mint family of plants +mint.n.04: the leaves of a mint plant used fresh or candied +mint.n.05: a candy that is flavored with a mint oil +mint.n.06: a plant where money is coined by authority of the government +>>> wn.synset('mint.n.04').part_holonyms() +[Synset('mint.n.02')] +>>> wn.synset('mint.n.04').substance_holonyms() +[Synset('mint.n.05')] +``` + +动词之间也有关系。例如,走路的动作包括抬脚的动作,所以走路蕴涵着抬脚。一些动词有多个蕴涵: + +```py +>>> wn.synset('walk.v.01').entailments() +[Synset('step.v.01')] +>>> wn.synset('eat.v.01').entailments() +[Synset('chew.v.01'), Synset('swallow.v.01')] +>>> wn.synset('tease.v.03').entailments() +[Synset('arouse.v.07'), Synset('disappoint.v.01')] +``` + +词条之间的一些词汇关系,如反义词: + +```py +>>> wn.lemma('supply.n.02.supply').antonyms() +[Lemma('demand.n.02.demand')] +>>> wn.lemma('rush.v.01.rush').antonyms() +[Lemma('linger.v.04.linger')] +>>> wn.lemma('horizontal.a.01.horizontal').antonyms() +[Lemma('inclined.a.02.inclined'), Lemma('vertical.a.01.vertical')] +>>> wn.lemma('staccato.r.01.staccato').antonyms() +[Lemma('legato.r.01.legato')] +``` + +你可以使用`dir()`查看词汇关系和同义词集上定义的其它方法,例如`dir(wn.synset('harmony.n.02'))`。 + +## 5.4 语义相似度 + +我们已经看到同义词集之间构成复杂的词汇关系网络。给定一个同义词集,我们可以遍历 WordNet 网络来查找相关含义的同义词集。知道哪些词是语义相关的,对索引文本集合非常有用,当搜索一个一般性的用语例如车辆时,就可以匹配包含具体用语例如豪华轿车的文档。 + +回想一下每个同义词集都有一个或多个上位词路径连接到一个根上位词,如`entity.n.01`。连接到同一个根的两个同义词集可能有一些共同的上位词(见图 5.1)。如果两个同义词集共用一个非常具体的上位词——在上位词层次结构中处于较低层的上位词——它们一定有密切的联系。 + +```py +>>> right = wn.synset('right_whale.n.01') +>>> orca = wn.synset('orca.n.01') +>>> minke = wn.synset('minke_whale.n.01') +>>> tortoise = wn.synset('tortoise.n.01') +>>> novel = wn.synset('novel.n.01') +>>> right.lowest_common_hypernyms(minke) +[Synset('baleen_whale.n.01')] +>>> right.lowest_common_hypernyms(orca) +[Synset('whale.n.02')] +>>> right.lowest_common_hypernyms(tortoise) +[Synset('vertebrate.n.01')] +>>> right.lowest_common_hypernyms(novel) +[Synset('entity.n.01')] +``` + +当然,我们知道,鲸鱼是非常具体的(须鲸更是如此),脊椎动物是更一般的,而实体完全是抽象的一般的。我们可以通过查找每个同义词集深度量化这个一般性的概念: + +```py +>>> wn.synset('baleen_whale.n.01').min_depth() +14 +>>> wn.synset('whale.n.02').min_depth() +13 +>>> wn.synset('vertebrate.n.01').min_depth() +8 +>>> wn.synset('entity.n.01').min_depth() +0 +``` + +WordNet 同义词集的集合上定义的相似度能够包括上面的概念。例如,`path_similarity`是基于上位词层次结构中相互连接的概念之间的最短路径在`0`-`1`范围的打分(两者之间没有路径就返回`-1`)。同义词集与自身比较将返回`1`。考虑以下的相似度:`minke, orca, tortoise, novel`。数字本身的意义并不大,当我们从海洋生物的语义空间转移到非生物时它是减少的。 + +```py +>>> right.path_similarity(minke) +0.25 +>>> right.path_similarity(orca) +0.16666666666666666 +>>> right.path_similarity(tortoise) +0.07692307692307693 +>>> right.path_similarity(novel) +0.043478260869565216 +``` + +注意 + +还有一些其它的相似性度量方法;你可以输入`help(wn)`获得更多信息。NLTK 还包括 VerbNet,一个连接到 WordNet 的动词的层次结构的词典。可以从`nltk.corpus.verbnet`访问。 + +## 6 小结 + +* 文本语料库是一个大型结构化文本的集合。NLTK 包含了许多语料库,如布朗语料库`nltk.corpus.brown`。 +* 有些文本语料库是分类的,例如通过文体或者主题分类;有时候语料库的分类会相互重叠。 +* 条件频率分布是一个频率分布的集合,每个分布都有一个不同的条件。它们可以用于通过给定内容或者文体对词的频率计数。 +* 行数较多的 Python 程序应该使用文本编辑器来输入,保存为`.py`后缀的文件,并使用`import`语句来访问。 +* Python 函数允许你将一段特定的代码块与一个名字联系起来,然后重用这些代码想用多少次就用多少次。 +* 一些被称为“方法”的函数与一个对象联系在起来,我们使用对象名称跟一个点然后跟方法名称来调用它,就像:`x.funct(y)`或者`word.isalpha()`。 +* 要想找到一些关于某个变量`v`的信息,可以在 Pyhon 交互式解释器中输入`help(v)`来阅读这一类对象的帮助条目。 +* WordNet 是一个面向语义的英语词典,由同义词的集合——或称为同义词集——组成,并且组织成一个网络。 +* 默认情况下有些函数是不能使用的,必须使用 Python 的`import`语句来访问。 + +## 7 深入阅读 + +本章的附加材料发布在`http://nltk.org/`,包括网络上免费提供的资源的链接。语料库方法总结请参阅`http://nltk.org/howto`上的语料库 HOWTO,在线 API 文档中也有更广泛的资料。 + +公开发行的语料库的重要来源是语言数据联盟(LDC)和欧洲语言资源局(ELRA)。提供几十种语言的数以百计的已标注文本和语音语料库。非商业许可证允许这些数据用于教学和科研目的。其中一些语料库也提供商业许可(但需要较高的费用)。 + +用于创建标注的文本语料库的好工具叫做 Brat,可从`http://brat.nlplab.org/`访问。 + +这些语料库和许多其他语言资源使用 OLAC 元数据格式存档,可以通过`http://www.language-archives.org/`上的 OLAC 主页搜索到。Corpora List 是一个讨论语料库内容的邮件列表,你可以通过搜索列表档案来找到资源或发布资源到列表中。*Ethnologue* 是最完整的世界上的语言的清单,`http://www.ethnologue.com/`。7000 种语言中只有几十中有大量适合 NLP 使用的数字资源。 + +本章触及语料库语言学领域。在这一领域的其他有用的书籍包括(Biber, Conrad, & Reppen, 1998), (McEnery, 2006), (Meyer, 2002), (Sampson & McCarthy, 2005), (Scott & Tribble, 2006)。在语言学中海量数据分析的深入阅读材料有:(Baayen, 2008), (Gries, 2009), (Woods, Fletcher, & Hughes, 1986)。 + +WordNet 原始描述是(Fellbaum, 1998)。虽然 WordNet 最初是为心理语言学研究开发的,它目前在自然语言处理和信息检索领域被广泛使用。WordNets 正在开发许多其他语言的版本,在`http://www.globalwordnet.org/`中有记录。学习 WordNet 相似性度量可以阅读(Budanitsky & Hirst, 2006)。 + +本章触及的其它主题是语音和词汇语义学,读者可以参考(Jurafsky & Martin, 2008)的第 7 和第 20 章。 + +## 8 练习 + +1. ☼ 创建一个变量`phrase`包含一个词的列表。实验本章描述的操作,包括加法、乘法、索引、切片和排序。 +2. ☼ 使用语料库模块处理`austen-persuasion.txt`。这本书中有多少词符?多少词型? +3. ☼ 使用布朗语料库阅读器`nltk.corpus.brown.words()`或网络文本语料库阅读器`nltk.corpus.webtext.words()`来访问两个不同文体的一些样例文本。 +4. ☼ 使用`state_union`语料库阅读器,访问《国情咨文报告》的文本。计数每个文档中出现的`men`、`women`和`people`。随时间的推移这些词的用法有什么变化? +5. ☼ 考查一些名词的整体部分关系。请记住,有 3 种整体部分关系,所以你需要使用:`member_meronyms()`, `part_meronyms()`, `substance_meronyms()`, `member_holonyms()`, `part_holonyms()`和`substance_holonyms()`。 +6. ☼ 在比较词表的讨论中,我们创建了一个对象叫做`translate`,通过它你可以使用德语和意大利语词汇查找对应的英语词汇。这种方法可能会出现什么问题?你能提出一个办法来避免这个问题吗? +7. ☼ 根据 Strunk 和 White 的《Elements of Style》,词`however`在句子开头使用是`in whatever way`或`to whatever extent`的意思,而没有`nevertheless`的意思。他们给出了正确用法的例子:`However you advise him, he will probably do as he thinks best.`(`http://www.bartleby.com/141/strunk3.html`)。使用词汇索引工具在我们一直在思考的各种文本中研究这个词的实际用法。也可以看 *LanguageLog* 发布在`http://itre.cis.upenn.edu/~myl/languagelog/archives/001913.html`上的《Fossilized prejudices about however》。 +8. ◑ 在名字语料库上定义一个条件频率分布,显示哪个*首*字母在男性名字中比在女性名字中更常用(参见 4.4)。 +9. ◑ 挑选两个文本,研究它们之间在词汇、词汇丰富性、文体等方面的差异。你能找出几个在这两个文本中词意相当不同的词吗,例如在《白鲸记》与《理智与情感》中的`monstrous`? +10. ◑ 阅读 BBC 新闻文章:[《UK's Vicky Pollards 'left behind'》](http://news.bbc.co.uk/1/hi/education/6173441.stm)。文章给出了有关青少年语言的以下统计:“使用最多的 20 个词,包括`yeah`, `no`, `but`和`like`,占所有词的大约三分之一”。对于大量文本源来说,所有词标识符的三分之一有多少词类型?你从这个统计中得出什么结论?更多相关信息请阅读`http://itre.cis.upenn.edu/~myl/languagelog/archives/003993.html`上的 *LanguageLog*。 +11. ◑ 调查模式分布表,寻找其他模式。试着用你自己对不同文体的印象理解来解释它们。你能找到其他封闭的词汇归类,展现不同文体的显著差异吗? +12. ◑ CMU 发音词典包含某些词的多个发音。它包含多少种不同的词?具有多个可能的发音的词在这个词典中的比例是多少? +13. ◑ 没有下位词的名词同义词集所占的百分比是多少?你可以使用`wn.all_synsets('n')`得到所有名词同义词集。 +14. ◑ 定义函数`supergloss(s)`,使用一个同义词集`s`作为它的参数,返回一个字符串,包含`s`的定义和`s`所有的上位词与下位词的定义的连接字符串。 +15. ◑ 写一个程序,找出所有在布朗语料库中出现至少 3 次的词。 +16. ◑ 写一个程序,生成一个词汇多样性得分表(例如词符/词型的比例),如我们在 1.1 所看到的。包括布朗语料库文体的全集(`nltk.corpus.brown.categories()`)。哪个文体词汇多样性最低(每个类型的标识符数最多)?这是你所期望的吗? +17. ◑ 写一个函数,找出一个文本中最常出现的 50 个词,停用词除外。 +18. ◑ 写一个程序,输出一个文本中 50 个最常见的双连词(相邻词对),忽略包含停用词的双连词。 +19. ◑ 写一个程序,按文体创建一个词频表,以 1 节给出的词频表为范例。选择你自己的词汇,并尝试找出那些在一个文体中很突出或很缺乏的词汇。讨论你的发现。 +20. ◑ 写一个函数`word_freq()`,用一个词和布朗语料库中的一个部分的名字作为参数,计算这部分语料中词的频率。 +21. ◑ 写一个程序,估算一个文本中的音节数,利用 CMU 发音词典。 +22. ◑ 定义一个函数`hedge(text)`,处理一个文本和产生一个新的版本在每三个词之间插入一个词`'like'`。 +23. ★ **齐夫定律**:`f(w)`是一个自由文本中的词`w`的频率。假设一个文本中的所有词都按照它们的频率排名,频率最高的在最前面。齐夫定律指出一个词类型的频率与它的排名成反比(即`f × r = k`,`k`是某个常数)。例如:最常见的第 50 个词类型出现的频率应该是最常见的第 150 个词型出现频率的 3 倍。 + 1. 写一个函数来处理一个大文本,使用`pylab.plot`画出相对于词的排名的词的频率。你认可齐夫定律吗?(提示:使用对数刻度会有帮助。)所绘的线的极端情况是怎样的? + 2. 随机生成文本,如使用`random.choice("abcdefg ")`,注意要包括空格字符。你需要事先`import random`。使用字符串连接操作将字符累积成一个很长的字符串。然后为这个字符串分词,产生前面的齐夫图,比较这两个图。此时你怎么看齐夫定律? +24. ★ 修改例 2.2 的文本生成程序,进一步完成下列任务: + 1. 在一个列表`words`中存储`n`个最相似的词,使用`random.choice()`从列表中随机选取一个词。(你将需要事先`import random`。) + 2. 选择特定的文体,如布朗语料库中的一部分或者《创世纪》翻译或者古腾堡语料库中的文本或者一个网络文本。在此语料上训练一个模型,产生随机文本。你可能要实验不同的起始字。文本的可理解性如何?讨论这种方法产生随机文本的长处和短处。 + 3. 现在使用两种不同文体训练你的系统,使用混合文体文本做实验。讨论你的观察结果。 +25. ★ 定义一个函数`find_language()`,用一个字符串作为其参数,返回包含这个字符串作为词汇的语言的列表。使用《世界人权宣言》`udhr`的语料,将你的搜索限制在 Latin-1 编码的文件中。 +26. ★ 名词上位词层次的分枝因素是什么?也就是说,对于每一个具有下位词——上位词层次中的子女——的名词同义词集,它们平均有几个下位词?你可以使用`wn.all_synsets('n')`获得所有名词同义词集。 +27. ★ 一个词的多义性是它所有含义的个数。利用 WordNet,使用`len(wn.synsets('dog', 'n'))`我们可以判断名词`dog`有 7 种含义。计算 WordNet 中名词、动词、形容词和副词的平均多义性。 +28. ★ 使用预定义的相似性度量之一给下面的每个词对的相似性打分。按相似性减少的顺序排名。你的排名与这里给出的顺序有多接近?(Miller & Charles, 1998)实验得出的顺序:`car-automobile, gem-jewel, journey-voyage, boy-lad, coast-shore, asylum-madhouse, magician-wizard, midday-noon, furnace-stove, food-fruit, bird-cock, bird-crane, tool-implement, brother-monk, lad-brother, crane-implement, journey-car, monk-oracle, cemetery-woodland, food-rooster, coast-hill, forest-graveyard, shore-woodland, monk-slave, coast-forest, lad-wizard, chord-smile, glass-magician, rooster-voyage, noon-string`。 + +## 关于本文档... + +针对 NLTK 3.0 作出更新。本章来自于《Python 自然语言处理》,[Steven Bird](http://estive.net/), [Ewan Klein](http://homepages.inf.ed.ac.uk/ewan/) 和 [Edward Loper](http://ed.loper.org/),Copyright © 2014 作者所有。本章依据 [*Creative Commons Attribution-Noncommercial-No Derivative Works 3\.0 United States License*](http://creativecommons.org/licenses/by-nc-nd/3.0/us/) 条款,与[*自然语言工具包*](http://nltk.org/) 3.0 版一起发行。 + +本文档构建于星期三 2015 年 7 月 1 日 12:30:05 AEST \ No newline at end of file diff --git a/docs/nlp-py-2e/3.md b/docs/nlp-py-2e/3.md new file mode 100644 index 0000000000000000000000000000000000000000..deb310ff81f3870dee215320dab2006eb2a22dd5 --- /dev/null +++ b/docs/nlp-py-2e/3.md @@ -0,0 +1,1758 @@ +# 3 处理原始文本 + +文本的最重要来源无疑是网络。探索现成的文本集合,如我们在前面章节中看到的语料库,是很方便的。然而,在你心中可能有你自己的文本来源,需要学习如何访问它们。 + +本章的目的是要回答下列问题: + +1. 我们怎样才能编写程序访问本地和网络上的文件,从而获得无限的语言材料? +2. 我们如何把文档分割成单独的词和标点符号,这样我们就可以开始像前面章节中在文本语料上做的那样的分析? +3. 我们怎样编程程序产生格式化的输出,并把结果保存在一个文件中? + +为了解决这些问题,我们将讲述 NLP 中的关键概念,包括分词和词干提取。在此过程中,你会巩固你的 Python 知识并且了解关于字符串、文件和正则表达式知识。既然这些网络上的文本都是 HTML 格式的,我们也将看到如何去除 HTML 标记。 + +注意 + +**重点**:从本章开始往后我们的例子程序将假设你以下面的导入语句开始你的交互式会话或程序: + +```py +>>> from __future__ import division # Python 2 users only +>>> import nltk, re, pprint +>>> from nltk import word_tokenize +``` + +## 3.1 从网络和硬盘访问文本 + +### 电子书 + +NLTK 语料库集合中有古腾堡项目的一小部分样例文本。然而,你可能对分析古腾堡项目的其它文本感兴趣。你可以在`http://www.gutenberg.org/catalog/`上浏览 25,000 本免费在线书籍的目录,获得 ASCII 码文本文件的 URL。虽然 90% 的古腾堡项目的文本是英语的,它还包括超过 50 种语言的材料,包括加泰罗尼亚语、中文、荷兰语、芬兰语、法语、德语、意大利语、葡萄牙语和西班牙语(每种语言都有超过 100 个文本)。 + +编号 2554 的文本是《罪与罚》的英文翻译,我们可以如下方式访问它。 + +```py +>>> from urllib import request +>>> url = "http://www.gutenberg.org/files/2554/2554.txt" +>>> response = request.urlopen(url) +>>> raw = response.read().decode('utf8') +>>> type(raw) + +>>> len(raw) +1176893 +>>> raw[:75] +'The Project Gutenberg EBook of Crime and Punishment, by Fyodor Dostoevsky\r\n' +``` + +注意 + +`read()`过程将需要几秒钟来下载这本大书。如果你使用的互联网代理 Python 不能正确检测出来,你可能需要在使用`urlopen`之前用下面的方法手动指定代理: + +```py +>>> proxies = {'http': 'http://www.someproxy.com:3128'} +>>> request.ProxyHandler(proxies) +``` + +变量`raw`包含一个有 1,176,893 个字符的字符串。(我们使用`type(raw)`可以看到它是一个字符串。)这是这本书原始的内容,包括很多我们不感兴趣的细节,如空格、换行符和空行。请注意,文件中行尾的`\r`和`\n`,这是 Python 用来显示特殊的回车和换行字符的方式(这个文件一定是在 Windows 机器上创建的)。对于语言处理,我们要将字符串分解为词和标点符号,正如我们在 1 中所看到的。这一步被称为分词,它产生我们所熟悉的结构,一个词汇和标点符号的列表。 + +```py +>>> tokens = word_tokenize(raw) +>>> type(tokens) + +>>> len(tokens) +254354 +>>> tokens[:10] +['The', 'Project', 'Gutenberg', 'EBook', 'of', 'Crime', 'and', 'Punishment', ',', 'by'] +``` + +请注意,分词需要 NLTK,但所有前面的打开一个 URL 以及读入一个字符串的任务都不需要。如果我们现在采取进一步的步骤从这个列表创建一个 NLTK 文本,我们可以进行我们在 1 中看到的所有的其他语言的处理,也包括常规的列表操作例如切片: + +```py +>>> text = nltk.Text(tokens) +>>> type(text) + +>>> text[1024:1062] +['CHAPTER', 'I', 'On', 'an', 'exceptionally', 'hot', 'evening', 'early', 'in', + 'July', 'a', 'young', 'man', 'came', 'out', 'of', 'the', 'garret', 'in', + 'which', 'he', 'lodged', 'in', 'S.', 'Place', 'and', 'walked', 'slowly', + ',', 'as', 'though', 'in', 'hesitation', ',', 'towards', 'K.', 'bridge', '.'] +>>> text.collocations() +Katerina Ivanovna; Pyotr Petrovitch; Pulcheria Alexandrovna; Avdotya +Romanovna; Rodion Romanovitch; Marfa Petrovna; Sofya Semyonovna; old +woman; Project Gutenberg-tm; Porfiry Petrovitch; Amalia Ivanovna; +great deal; Nikodim Fomitch; young man; Ilya Petrovitch; n't know; +Project Gutenberg; Dmitri Prokofitch; Andrey Semyonovitch; Hay Market +``` + +请注意,`Project Gutenberg`以一个搭配出现。这是因为从古腾堡项目下载的每个文本都包含一个首部,里面有文本的名称、作者、扫描和校对文本的人的名字、许可证等信息。有时这些信息出现在文件末尾页脚处。我们不能可靠地检测出文本内容的开始和结束,因此在从`原始`文本中挑出正确内容且没有其它内容之前,我们需要手工检查文件以发现标记内容开始和结尾的独特的字符串: + +```py +>>> raw.find("PART I") +5338 +>>> raw.rfind("End of Project Gutenberg's Crime") +1157743 +>>> raw = raw[5338:1157743] ❶ +>>> raw.find("PART I") +0 +``` + +方法`find()`和`rfind()`(反向查找)帮助我们得到字符串切片需要用到的正确的索引值❶。我们用这个切片重新给`raw`赋值,所以现在它以`PART I`开始一直到(但不包括)标记内容结尾的句子。 + +这是我们第一次接触到网络的实际内容:在网络上找到的文本可能含有不必要的内容,并没有一个自动的方法来去除它。但只需要少量的额外工作,我们就可以提取出我们需要的材料。 + +### 处理 HTML + +网络上的文本大部分是 HTML 文件的形式。你可以使用网络浏览器将网页作为文本保存为本地文件,然后按照下面关于文件的小节描述的那样来访问它。不过,如果你要经常这样做,最简单的办法是直接让 Python 来做这份工作。第一步是像以前一样使用`urlopen`。为了好玩,我们将挑选一个被称为《Blondes to die out in 200 years》的 BBC 新闻故事,一个都市传奇被 BBC 作为确立的科学事实流传下来: + +```py +>>> url = "http://news.bbc.co.uk/2/hi/health/2284783.stm" +>>> html = request.urlopen(url).read().decode('utf8') +>>> html[:60] +'>> from bs4 import BeautifulSoup +>>> raw = BeautifulSoup(html).get_text() +>>> tokens = word_tokenize(raw) +>>> tokens +['BBC', 'NEWS', '|', 'Health', '|', 'Blondes', "'to", 'die', 'out', ...] +``` + +它仍然含有不需要的内容,包括网站导航及有关报道等。通过一些尝试和出错你可以找到内容索引的开始和结尾,并选择你感兴趣的词符,按照前面讲的那样初始化一个文本。 + +```py +>>> tokens = tokens[110:390] +>>> text = nltk.Text(tokens) +>>> text.concordance('gene') +Displaying 5 of 5 matches: +hey say too few people now carry the gene for blondes to last beyond the next +blonde hair is caused by a recessive gene . In order for a child to have blond +have blonde hair , it must have the gene on both sides of the family in the g +ere is a disadvantage of having that gene or by chance . They do n't disappear +des would disappear is if having the gene was a disadvantage and I do not thin +``` + +### 处理搜索引擎的结果 + +网络可以被看作未经标注的巨大的语料库。网络搜索引擎提供了一个有效的手段,搜索大量文本作为有关的语言学的例子。搜索引擎的主要优势是规模:因为你正在寻找这样庞大的一个文件集,会更容易找到你感兴趣语言模式。而且,你可以使用非常具体的模式,仅仅在较小的范围匹配一两个例子,但在网络上可能匹配成千上万的例子。网络搜索引擎的第二个优势是非常容易使用。因此,它是一个非常方便的工具,可以快速检查一个理论是否合理。 + +表 3.1: + +搭配的谷歌命中次数:`absolutely`或`definitely`后面跟着`adore`, `love`, `like`或 `prefer`的搭配的命中次数。(Liberman, in *LanguageLog*, 2005)。 + +```py +>>> import feedparser +>>> llog = feedparser.parse("http://languagelog.ldc.upenn.edu/nll/?feed=atom") +>>> llog['feed']['title'] +'Language Log' +>>> len(llog.entries) +15 +>>> post = llog.entries[2] +>>> post.title +"He's My BF" +>>> content = post.content[0].value +>>> content[:70] +'

Today I was chatting with three of our visiting graduate students f' +>>> raw = BeautifulSoup(content).get_text() +>>> word_tokenize(raw) +['Today', 'I', 'was', 'chatting', 'with', 'three', 'of', 'our', 'visiting', +'graduate', 'students', 'from', 'the', 'PRC', '.', 'Thinking', 'that', 'I', +'was', 'being', 'au', 'courant', ',', 'I', 'mentioned', 'the', 'expression', +'DUI4XIANG4', '\u5c0d\u8c61', '("', 'boy', '/', 'girl', 'friend', '"', ...] +``` + +伴随着一些更深入的工作,我们可以编写程序创建一个博客帖子的小语料库,并以此作为我们 NLP 的工作基础。 + +### 读取本地文件 + +为了读取本地文件,我们需要使用 Python 内置的`open()`函数,然后是`read()`方法。假设你有一个文件`document.txt`,你可以像这样加载它的内容: + +```py +>>> f = open('document.txt') +>>> raw = f.read() +``` + +注意 + +**轮到你来**:使用文本编辑器创建一个名为`document.txt`的文件,然后输入几行文字,保存为纯文本。如果你使用 IDLE,在`File`菜单中选择`New Window`命令,在新窗口中输入所需的文本,然后在 IDLE 提供的弹出式对话框中的文件夹内保存文件为`document.txt`。然后在 Python 解释器中使用`f = open('document.txt')`打开这个文件,并使用`print(f.read())`检查其内容。 + +当你尝试这样做时可能会出各种各样的错误。如果解释器无法找到你的文件,你会看到类似这样的错误: + +```py +>>> f = open('document.txt') +Traceback (most recent call last): +File "", line 1, in -toplevel- +f = open('document.txt') +IOError: [Errno 2] No such file or directory: 'document.txt' +``` + +要检查你正试图打开的文件是否在正确的目录中,使用 IDLE `File`菜单上的`Open`命令;另一种方法是在 Python 中检查当前目录: + +```py +>>> import os +>>> os.listdir('.') +``` + +另一个你在访问一个文本文件时可能遇到的问题是换行的约定,这个约定因操作系统不同而不同。内置的`open()`函数的第二个参数用于控制如何打开文件:`open('document.txt', 'rU')` —— `'r'`意味着以只读方式打开文件(默认),`'U'`表示“通用”,它让我们忽略不同的换行约定。 + +假设你已经打开了该文件,有几种方法可以阅读此文件。`read()`方法创建了一个包含整个文件内容的字符串: + +```py +>>> f.read() +'Time flies like an arrow.\nFruit flies like a banana.\n' +``` + +回想一`'\n'`字符是换行符;这相当于按键盘上的`Enter`开始一个新行。 + +我们也可以使用一个`for`循环一次读文件中的一行: + +```py +>>> f = open('document.txt', 'rU') +>>> for line in f: +... print(line.strip()) +Time flies like an arrow. +Fruit flies like a banana. +``` + +在这里,我们使用`strip()`方法删除输入行结尾的换行符。 + +NLTK 中的语料库文件也可以使用这些方法来访问。我们只需使用`nltk.data.find()`来获取语料库项目的文件名。然后就可以使用我们刚才讲的方式打开和阅读它: + +```py +>>> path = nltk.data.find('corpora/gutenberg/melville-moby_dick.txt') +>>> raw = open(path, 'rU').read() +``` + +### 从 PDF、MS Word 及其他二进制格式中提取文本 + +ASCII 码文本和 HTML 文本是人可读的格式。文字常常以二进制格式出现,如 PDF 和 MSWord,只能使用专门的软件打开。第三方函数库如`pypdf`和`pywin32`提供了对这些格式的访问。从多列文档中提取文本是特别具有挑战性的。一次性转换几个文件,会比较简单些,用一个合适的应用程序打开文件,以文本格式保存到本地驱动器,然后以如下所述的方式访问它。如果该文档已经在网络上,你可以在 Google 的搜索框输入它的 URL。搜索结果通常包括这个文档的 HTML 版本的链接,你可以将它保存为文本。 + +### 捕获用户输入 + +有时我们想捕捉用户与我们的程序交互时输入的文本。调用 Python 函数`input()`提示用户输入一行数据。保存用户输入到一个变量后,我们可以像其他字符串那样操纵它。 + +```py +>>> s = input("Enter some text: ") +Enter some text: On an exceptionally hot evening early in July +>>> print("You typed", len(word_tokenize(s)), "words.") +You typed 8 words. +``` + +### NLP 的流程 + +3.1 总结了我们在本节涵盖的内容,包括我们在第一章中所看到的建立一个词汇表的过程。(其中一个步骤,规范化,将在 3.6 讨论。) + +![Images/pipeline1.png](Images/8c5ec1a0132f7c85fd96eda9d9929d15.jpg) + +图 3.1:处理流程:打开一个 URL,读里面 HTML 格式的内容,去除标记,并选择字符的切片;然后分词,是否转换为`nltk.Text`对象是可选择的;我们也可以将所有词汇小写并提取词汇表。 + +在这条流程后面还有很多操作。要正确理解它,这样有助于明确其中提到的每个变量的类型。使用`type(x)`我们可以找出任一 Python 对象`x`的类型,如`type(1)`是``因为`1`是一个整数。 + +当我们载入一个 URL 或文件的内容时,或者当我们去掉 HTML 标记时,我们正在处理字符串,也就是 Python 的``数据类型。(在 3.2 节,我们将学习更多有关字符串的内容): + +```py +>>> raw = open('document.txt').read() +>>> type(raw) + +``` + +当我们将一个字符串分词,会产生一个(词的)列表,这是 Python 的``类型。规范化和排序列表产生其它列表: + +```py +>>> tokens = word_tokenize(raw) +>>> type(tokens) + +>>> words = [w.lower() for w in tokens] +>>> type(words) + +>>> vocab = sorted(set(words)) +>>> type(vocab) + +``` + +一个对象的类型决定了它可以执行哪些操作。比如我们可以追加一个链表,但不能追加一个字符串: + +```py +>>> vocab.append('blog') +>>> raw.append('blog') +Traceback (most recent call last): + File "", line 1, in +AttributeError: 'str' object has no attribute 'append' +``` + +同样的,我们可以连接字符串与字符串,列表与列表,但我们不能连接字符串与列表: + +```py +>>> query = 'Who knows?' +>>> beatles = ['john', 'paul', 'george', 'ringo'] +>>> query + beatles +Traceback (most recent call last): + File "", line 1, in +TypeError: cannot concatenate 'str' and 'list' objects +``` + +## 3.2 字符串:最底层的文本处理 + +现在是时候研究一个之前我们一直故意避开的基本数据类型了。在前面的章节中,我们侧重于将文本作为一个词列表。我们并没有细致的探讨词汇以及它们是如何在编程语言中被处理的。通过使用 NLTK 中的语料库接口,我们可以忽略这些文本所在的文件。一个词的内容,一个文件的内容在编程语言中是由一个叫做字符串的基本数据类型来表示的。在本节中,我们将详细探讨字符串,并展示字符串与词汇、文本和文件之间的联系。 + +### 字符串的基本操作 + +可以使用单引号❶或双引号❷来指定字符串,如下面的例子代码所示。如果一个字符串中包含一个单引号,我们必须在单引号前加反斜杠❸让 Python 知道这是字符串中的单引号,或者也可以将这个字符串放入双引号中❷。否则,字符串内的单引号❹将被解释为字符串结束标志,Python 解释器会报告一个语法错误: + +```py +>>> monty = 'Monty Python' ❶ +>>> monty +'Monty Python' +>>> circus = "Monty Python's Flying Circus" ❷ +>>> circus +"Monty Python's Flying Circus" +>>> circus = 'Monty Python\'s Flying Circus' ❸ +>>> circus +"Monty Python's Flying Circus" +>>> circus = 'Monty Python's Flying Circus' ❹ + File "", line 1 + circus = 'Monty Python's Flying Circus' + ^ +SyntaxError: invalid syntax +``` + +有时字符串跨好几行。Python 提供了多种方式表示它们。在下面的例子中,一个包含两个字符串的序列被连接为一个字符串。我们需要使用反斜杠❶或者括号❷,这样解释器就知道第一行的表达式不完整。 + +```py +>>> couplet = "Shall I compare thee to a Summer's day?"\ +... "Thou are more lovely and more temperate:" ❶ +>>> print(couplet) +Shall I compare thee to a Summer's day?Thou are more lovely and more temperate: +>>> couplet = ("Rough winds do shake the darling buds of May," +... "And Summer's lease hath all too short a date:") ❷ +>>> print(couplet) +Rough winds do shake the darling buds of May,And Summer's lease hath all too short a date: +``` + +不幸的是,这些方法并没有展现给我们十四行诗的两行之间的换行。为此,我们可以使用如下所示的三重引号的字符串: + +```py +>>> couplet = """Shall I compare thee to a Summer's day? +... Thou are more lovely and more temperate:""" +>>> print(couplet) +Shall I compare thee to a Summer's day? +Thou are more lovely and more temperate: +>>> couplet = '''Rough winds do shake the darling buds of May, +... And Summer's lease hath all too short a date:''' +>>> print(couplet) +Rough winds do shake the darling buds of May, +And Summer's lease hath all too short a date: +``` + +现在我们可以定义字符串,也可以在上面尝试一些简单的操作。首先,让我们来看看`+`操作,被称为连接❶。此操作产生一个新字符串,它是两个原始字符串首尾相连粘贴在一起而成。请注意,连接不会做一些比较聪明的事,例如在词汇之间插入空格。我们甚至可以对字符串用乘法❷: + +```py +>>> 'very' + 'very' + 'very' ❶ +'veryveryvery' +>>> 'very' * 3 ❷ +'veryveryvery' +``` + +注意 + +**轮到你来**:试运行下面的代码,然后尝试使用你对字符串`+`和`*`操作的理解,弄清楚它是如何运作的。要小心区分字符串`' '`,这是一个空格符,和字符串`''`,这是一个空字符串。 + +```py +>>> a = [1, 2, 3, 4, 5, 6, 7, 6, 5, 4, 3, 2, 1] +>>> b = [' ' * 2 * (7 - i) + 'very' * i for i in a] +>>> for line in b: +... print(line) +``` + +我们已经看到加法和乘法运算不仅仅适用于数字也适用于字符串。但是,请注意,我们不能对字符串用减法或除法: + +```py +>>> 'very' - 'y' +Traceback (most recent call last): + File "", line 1, in +TypeError: unsupported operand type(s) for -: 'str' and 'str' +>>> 'very' / 2 +Traceback (most recent call last): + File "", line 1, in +TypeError: unsupported operand type(s) for /: 'str' and 'int' +``` + +这些错误消息是 Python 的另一个例子,告诉我们的数据类型混乱。第一种情况告诉我们减法操作(即`-`) 不能适用于`str`(字符串)对象类型,而第二种情况告诉我们除法的两个操作数不能分别为`str`和`int`。 + +### 输出字符串 + +到目前为止,当我们想看看变量的内容或想看到计算的结果,我们就把变量的名称输入到解释器。我们还可以使用`print`语句来看一个变量的内容: + +```py +>>> print(monty) +Monty Python +``` + +请注意这次是没有引号的。当我们通过输入变量的名字到解释器中来检查它时,解释器输出 Python 中的变量的值。因为它是一个字符串,结果被引用。然而,当我们告诉解释器`print`这个变量时,我们没有看到引号字符,因为字符串的内容里面没有引号。 + +`print`语句可以多种方式将多个元素显示在一行,就像这样: + +```py +>>> grail = 'Holy Grail' +>>> print(monty + grail) +Monty PythonHoly Grail +>>> print(monty, grail) +Monty Python Holy Grail +>>> print(monty, "and the", grail) +Monty Python and the Holy Grail +``` + +### 访问单个字符 + +正如我们在 2 看到的列表,字符串也是被索引的,从零开始。当我们索引一个字符串时,我们得到它的一个字符(或字母)。一个单独的字符并没有什么特别,它只是一个长度为`1`的字符串。 + +```py +>>> monty[0] +'M' +>>> monty[3] +'t' +>>> monty[5] +' ' +``` + +与列表一样,如果我们尝试访问一个超出字符串范围的索引时,会得到了一个错误: + +```py +>>> monty[20] +Traceback (most recent call last): + File "", line 1, in ? +IndexError: string index out of range +``` + +也与列表一样,我们可以使用字符串的负数索引,其中`-1`是最后一个字符的索引❶。正数和负数的索引给我们两种方式指示一个字符串中的任何位置。在这种情况下,当一个字符串长度为 12 时,索引`5`和`-7`都指示相同的字符(一个空格)。(请注意,`5 = len(monty) - 7`。) + +```py +>>> monty[-1] ❶ +'n' +>>> monty[5] +' ' +>>> monty[-7] +' ' +``` + +我们可以写一个`for`循环,遍历字符串中的字符。`print`函数包含可选的`end=' '`参数,这是为了告诉 Python 不要在行尾输出换行符。 + +```py +>>> sent = 'colorless green ideas sleep furiously' +>>> for char in sent: +... print(char, end=' ') +... +c o l o r l e s s g r e e n i d e a s s l e e p f u r i o u s l y +``` + +我们也可以计数单个字符。通过将所有字符小写来忽略大小写的区分,并过滤掉非字母字符。 + +```py +>>> from nltk.corpus import gutenberg +>>> raw = gutenberg.raw('melville-moby_dick.txt') +>>> fdist = nltk.FreqDist(ch.lower() for ch in raw if ch.isalpha()) +>>> fdist.most_common(5) +[('e', 117092), ('t', 87996), ('a', 77916), ('o', 69326), ('n', 65617)] +>>> [char for (char, count) in fdist.most_common()] +['e', 't', 'a', 'o', 'n', 'i', 's', 'h', 'r', 'l', 'd', 'u', 'm', 'c', 'w', +'f', 'g', 'p', 'b', 'y', 'v', 'k', 'q', 'j', 'x', 'z'] +``` + +```py +>>> monty[6:10] +'Pyth' +``` + +在这里,我们看到的字符是`'P'`, `'y'`, `'t'`和`'h'`,它们分别对应于`monty[6]` ... `monty[9]`而不包括`monty[10]`。这是因为切片开始于第一个索引,但结束于最后一个索引的前一个。 + +我们也可以使用负数索引切片——也是同样的规则,从第一个索引开始到最后一个索引的前一个结束;在这里是在空格字符前结束。 + +```py +>>> monty[-12:-7] +'Monty' +``` + +与列表切片一样,如果我们省略了第一个值,子字符串将从字符串的开头开始。如果我们省略了第二个值,则子字符串直到字符串的结尾结束: + +```py +>>> monty[:5] +'Monty' +>>> monty[6:] +'Python' +``` + +我们使用`in`操作符测试一个字符串是否包含一个特定的子字符串,如下所示: + +```py +>>> phrase = 'And now for something completely different' +>>> if 'thing' in phrase: +... print('found "thing"') +found "thing" +``` + +我们也可以使用`find()`找到一个子字符串在字符串内的位置: + +```py +>>> monty.find('Python') +6 +``` + +注意 + +**轮到你来**:造一句话,将它分配给一个变量, 例如,`sent = 'my sentence...'`。写切片表达式抽取个别词。(这显然不是一种方便的方式来处理文本中的词!) + +### 更多的字符串操作 + +Python 对处理字符串的支持很全面。3.2.所示是一个总结,其中包括一些我们还没有看到的操作。关于字符串的更多信息,可在 Python 提示符下输入`help(str)`。 + +表 3.2: + +有用的字符串方法:4.2 中字符串测试之外的字符串上的操作;所有的方法都产生一个新的字符串或列表 + +```py +>>> query = 'Who knows?' +>>> beatles = ['John', 'Paul', 'George', 'Ringo'] +>>> query[2] +'o' +>>> beatles[2] +'George' +>>> query[:2] +'Wh' +>>> beatles[:2] +['John', 'Paul'] +>>> query + " I don't" +"Who knows? I don't" +>>> beatles + 'Brian' +Traceback (most recent call last): + File "", line 1, in +TypeError: can only concatenate list (not "str") to list +>>> beatles + ['Brian'] +['John', 'Paul', 'George', 'Ringo', 'Brian'] +``` + +当我们在一个 Python 程序中打开并读入一个文件,我们得到一个对应整个文件内容的字符串。如果我们使用一个`for`循环来处理这个字符串元素,所有我们可以挑选出的只是单个的字符——我们不选择粒度。相比之下,列表中的元素可以很大也可以很小,只要我们喜欢:例如,它们可能是段落、句子、短语、单词、字符。所以,列表的优势是我们可以灵活的决定它包含的元素,相应的后续的处理也变得灵活。因此,我们在一段 NLP 代码中可能做的第一件事情就是将一个字符串分词放入一个字符串列表中`(3.7)`。相反,当我们要将结果写入到一个文件或终端,我们通常会将它们格式化为一个字符串`(3.9)`。 + +列表与字符串没有完全相同的功能。列表具有增强的能力使你可以改变其中的元素: + +```py +>>> beatles[0] = "John Lennon" +>>> del beatles[-1] +>>> beatles +['John Lennon', 'Paul', 'George'] +``` + +另一方面,如果我们尝试在一个*字符串*上这么做——将`query`的第 0 个字符修改为`'F'`——我们得到: + +```py +>>> query[0] = 'F' +Traceback (most recent call last): + File "", line 1, in ? +TypeError: object does not support item assignment +``` + +这是因为字符串是不可变的:一旦你创建了一个字符串,就不能改变它。然而,列表是可变的,其内容可以随时修改。作为一个结论,列表支持修改原始值的操作,而不是产生一个新的值。 + +注意 + +**轮到你来**:通过尝试本章结尾的一些练习,巩固你的字符串知识。 + +## 3.3 使用 Unicode 进行文字处理 + +我们的程序经常需要处理不同的语言和不同的字符集。“纯文本”的概念是虚构的。如果你住在讲英语国家,你可能在使用 ASCII 码而没有意识到这一点。如果你住在欧洲,你可能使用一种扩展拉丁字符集,包含丹麦语和挪威语中的`ø`,匈牙利语中的`ő`,西班牙和布列塔尼语中的`ñ`,捷克语和斯洛伐克语中的`ň`。在本节中,我们将概述如何使用 Unicode 处理使用非 ASCII 字符集的文本。 + +### 什么是 Unicode? + +Unicode 支持超过一百万种字符。每个字符分配一个编号,称为编码点。在 Python 中,编码点写作`\u*XXXX*`的形式,其中`*XXXX*`是四位十六进制形式数。 + +在一个程序中,我们可以像普通字符串那样操纵 Unicode 字符串。然而,当 Unicode 字符被存储在文件或在终端上显示,它们必须被编码为字节流。一些编码(如 ASCII 和 Latin-2)中每个编码点使用单字节,所以它们可以只支持 Unicode 的一个小的子集,足够单个语言使用了。其它的编码(如 UTF-8)使用多个字节,可以表示全部的 Unicode 字符。 + +文件中的文本都是有特定编码的,所以我们需要一些机制来将文本翻译成 Unicode——翻译成 Unicode 叫做解码。相对的,要将 Unicode 写入一个文件或终端,我们首先需要将 Unicode 转化为合适的编码——这种将 Unicode 转化为其它编码的过程叫做编码,如 3.3 所示。 + +![Images/unicode.png](Images/4a87f1dccc0e18aab5ec599d8d8358d6.jpg) + +图 3.3:Unicode 解码和编码 + +从 Unicode 的角度来看,字符是可以实现一个或多个字形的抽象的实体。只有字形可以出现在屏幕上或被打印在纸上。一个字体是一个字符到字形映射。 + +### 从文件中提取已编码文本 + +假设我们有一个小的文本文件,我们知道它是如何编码的。例如,`polish-lat2.txt`顾名思义是波兰语的文本片段(来源波兰语 Wikipedia;可以在`http://pl.wikipedia.org/wiki/Biblioteka_Pruska`中看到)。此文件是 Latin-2 编码的,也称为 ISO-8859-2。`nltk.data.find()`函数为我们定位文件。 + +```py +>>> path = nltk.data.find('corpora/unicode_samples/polish-lat2.txt') +``` + +Python 的`open()`函数可以读取编码的数据为 Unicode 字符串,并写出 Unicode 字符串的编码形式。它采用一个参数来指定正在读取或写入的文件的编码。因此,让我们使用编码`'latin2'`打开我们波兰语文件,并检查该文件的内容: + +```py +>>> f = open(path, encoding='latin2') +>>> for line in f: +... line = line.strip() +... print(line) +Pruska Biblioteka Państwowa. Jej dawne zbiory znane pod nazwą +"Berlinka" to skarb kultury i sztuki niemieckiej. Przewiezione przez +Niemców pod koniec II wojny światowej na Dolny Śląsk, zostały +odnalezione po 1945 r. na terytorium Polski. Trafiły do Biblioteki +Jagiellońskiej w Krakowie, obejmują ponad 500 tys. zabytkowych +archiwaliów, m.in. manuskrypty Goethego, Mozarta, Beethovena, Bacha. +``` + +如果这不能在你的终端正确显示,或者我们想要看到字符的底层数值(或“代码点”),那么我们可以将所有的非 ASCII 字符转换成它们两位数`\x*XX*`和四位数`\u*XXXX*`表示法: + +```py +>>> f = open(path, encoding='latin2') +>>> for line in f: +... line = line.strip() +... print(line.encode('unicode_escape')) +b'Pruska Biblioteka Pa\\u0144stwowa. Jej dawne zbiory znane pod nazw\\u0105' +b'"Berlinka" to skarb kultury i sztuki niemieckiej. Przewiezione przez' +b'Niemc\\xf3w pod koniec II wojny \\u015bwiatowej na Dolny \\u015al\\u0105sk, zosta\\u0142y' +b'odnalezione po 1945 r. na terytorium Polski. Trafi\\u0142y do Biblioteki' +b'Jagiello\\u0144skiej w Krakowie, obejmuj\\u0105 ponad 500 tys. zabytkowych' +b'archiwali\\xf3w, m.in. manuskrypty Goethego, Mozarta, Beethovena, Bacha.' +``` + +上面输出的第一行有一个以`\u`转义字符串开始的 Unicode 转义字符串,即`\u0144`。相关的 Unicode 字符在屏幕上将显示为字形`ń`。在前面例子中的第三行中,我们看到`\xf3`,对应字形为ó,在 128-255 的范围内。 + +在 Python 3 中,源代码默认使用 UTF-8 编码,如果你使用的 IDLE 或另一个支持 Unicode 的程序编辑器,你可以在字符串中包含 Unicode 字符。可以使用`\u*XXXX*`转义序列包含任意的 Unicode 字符。我们使用`ord()`找到一个字符的整数序数。例如: + +```py +>>> ord('ń') +324 +``` + +324 的 4 位十六进制数字的形式是 0144(输入`hex(324)`可以发现这点),我们可以定义一个具有适当转义序列的字符串。 + +```py +>>> nacute = '\u0144' +>>> nacute +'ń' +``` + +注意 + +决定屏幕上显示的字形的因素很多。如果你确定你的编码正确但你的 Python 代码仍然未能显示出你预期的字形,你应该检查你的系统上是否安装了所需的字体。可能需要配置你的区域设置来渲染 UTF-8 编码的字符,然后使用`print(nacute.encode('utf8'))`才能在你的终端看到ń显示。 + +我们还可以看到这个字符在一个文本文件内是如何表示为字节序列的: + +```py +>>> nacute.encode('utf8') +b'\xc5\x84' +``` + +`unicodedata`模块使我们可以检查 Unicode 字符的属性。在下面的例子中,我们选择超出 ASCII 范围的波兰语文本的第三行中的所有字符,输出它们的 UTF-8 转义值,然后是使用标准 Unicode 约定的它们的编码点整数(即以`U+`为前缀的十六进制数字),随后是它们的 Unicode 名称。 + +```py +>>> import unicodedata +>>> lines = open(path, encoding='latin2').readlines() +>>> line = lines[2] +>>> print(line.encode('unicode_escape')) +b'Niemc\\xf3w pod koniec II wojny \\u015bwiatowej na Dolny \\u015al\\u0105sk, zosta\\u0142y\\n' +>>> for c in line: ❶ +... if ord(c) > 127: +... print('{} U+{:04x} {}'.format(c.encode('utf8'), ord(c), unicodedata.name(c))) +b'\xc3\xb3' U+00f3 LATIN SMALL LETTER O WITH ACUTE +b'\xc5\x9b' U+015b LATIN SMALL LETTER S WITH ACUTE +b'\xc5\x9a' U+015a LATIN CAPITAL LETTER S WITH ACUTE +b'\xc4\x85' U+0105 LATIN SMALL LETTER A WITH OGONEK +b'\xc5\x82' U+0142 LATIN SMALL LETTER L WITH STROKE +``` + +如果你使用`c`替换掉❶中的`c.encode('utf8')`,如果你的系统支持 UTF-8,你应该看到类似下面的输出: + +ó U+00f3 LATIN SMALL LETTER O WITH ACUTEś U+015b LATIN SMALL LETTER S WITH ACUTEŚ U+015a LATIN CAPITAL LETTER S WITH ACUTEą U+0105 LATIN SMALL LETTER A WITH OGONEKł U+0142 LATIN SMALL LETTER L WITH STROKE + +另外,根据你的系统的具体情况,你可能需要用`'latin2'`替换示例中的编码`'utf8'`。 + +下一个例子展示 Python 字符串函数和`re`模块是如何能够与 Unicode 字符一起工作的。(我们会在下面一节中仔细看看`re`模块。`\w`匹配一个“单词字符”,参见 3.4)。 + +```py +>>> line.find('zosta\u0142y') +54 +>>> line = line.lower() +>>> line +'niemców pod koniec ii wojny światowej na dolny śląsk, zostały\n' +>>> line.encode('unicode_escape') +b'niemc\\xf3w pod koniec ii wojny \\u015bwiatowej na dolny \\u015bl\\u0105sk, zosta\\u0142y\\n' +>>> import re +>>> m = re.search('\u015b\w*', line) +>>> m.group() +'\u015bwiatowej' +``` + +NLTK 分词器允许 Unicode 字符串作为输入,并输出相应地 Unicode 字符串。 + +```py +>>> word_tokenize(line) +['niemców', 'pod', 'koniec', 'ii', 'wojny', 'światowej', 'na', 'dolny', 'śląsk', ',', 'zostały'] +``` + +### 在 Python 中使用本地编码 + +如果你习惯了使用特定的本地编码字符,你可能希望能够在一个 Python 文件中使用你的字符串输入及编辑的标准方法。为了做到这一点,你需要在你的文件的第一行或第二行中包含字符串:`'# -*- coding: >coding> -*-'`。请注意``必须是像`'latin-1'`, `'big5'`或`'utf-8'`这样的字符串 (见 3.4)。 + +![Images/polish-utf8.png](Images/5eeb4cf55b6d18d4bcb098fc72ddc6d7.jpg) + +图 3.4:Unicode 与 IDLE:IDLE 编辑器中 UTF-8 编码的字符串字面值;这需要在 IDLE 属性中设置了相应的字体;这里我们选择 Courier CE。 + +上面的例子还说明了正规表达式是如何可以使用编码的字符串的。 + +## 3.4 使用正则表达式检测词组搭配 + +许多语言处理任务都涉及模式匹配。例如:我们可以使用`endswith('ed')`找到以`ed`结尾的词。在 4.2 中我们看到过各种这样的“词测试”。正则表达式给我们一个更加强大和灵活的方法描述我们感兴趣的字符模式。 + +注意 + +介绍正则表达式的其他出版物有很多,它们围绕正则表达式的语法组织,应用于搜索文本文件。我们不再赘述这些,只专注于在语言处理的不同阶段如何使用正则表达式。像往常一样,我们将采用基于问题的方式,只在解决实际问题需要时才介绍新特性。在我们的讨论中,我们将使用箭头来表示正则表达式,就像这样:`patt`。 + +在 Python 中使用正则表达式,需要使用`import re`导入`re`库。我们还需要一个用于搜索的词汇列表;我们再次使用词汇语料库`(4)`。我们将对它进行预处理消除某些名称。 + +```py +>>> import re +>>> wordlist = [w for w in nltk.corpus.words.words('en') if w.islower()] +``` + +### 使用基本的元字符 + +让我们使用正则表达式`'ed$'`,我们将使用函数`re.search(p, s)`检查字符串`s`中是否有模式`p`。我们需要指定感兴趣的字符,然后使用美元符号,它是正则表达式中有特殊用途的符号,用来匹配单词的末尾: + +```py +>>> [w for w in wordlist if re.search('ed$', w)] +['abaissed', 'abandoned', 'abased', 'abashed', 'abatised', 'abed', 'aborted', ...] +``` + +`.`通配符匹配任何单个字符。假设我们有一个 8 个字母组成的词的字谜室,`j`是其第三个字母,`t`是其第六个字母。空白单元格中的每个地方,我们用一个句点: + +```py +>>> [w for w in wordlist if re.search('^..j..t..$', w)] +['abjectly', 'adjuster', 'dejected', 'dejectly', 'injector', 'majestic', ...] +``` + +注意 + +**轮到你来**:驼字符`^`匹配字符串的开始,就像如果我们不用这两个符号而使用`..j..t..`搜索,刚才例子中我们会得到什么样的结果? + +最后,`?`符号表示前面的字符是可选的。因此我们可以使用`sum(1 for w in text if re.search('^e-?mail', w))`计数一个文本中这个词(任一拼写形式)出现的总次数。 + +### 范围与闭包 + +![Images/T9.png](Images/cf5ffc116dbddb4a34c65925b0d558cb.jpg) + +图 3.5:T9:9 个键上的文本 + +T9 系统用于在手机上输入文本(见 3.5))。两个或两个以上以相同击键顺序输入的词汇,叫做 textonyms。例如,`hole`和`golf`都是通过序列 4653 输入。还有哪些其它词汇由相同的序列产生?这里我们使用正则表达式`'^[ghi][mno][jlk][def]$'`。 + +```py +>>> [w for w in wordlist if re.search('^[ghi][mno][jlk][def]$', w)] +['gold', 'golf', 'hold', 'hole'] +``` + +表达式的第一部分`^[ghi]`匹配以`g, h, i`开始的词。表达式的下一部分,`[mno]`限制了第二个字符是`m, n, o`。第三部分和第四部分同样被限制。只有 4 个单词满足这些限制。注意,方括号内的字符的顺序是没有关系的,所以我们可以写成`^[hig][nom][ljk][fed]$` + +注意 + +**轮到你来**:来看一些“手指绕口令”,只用一部分数字键盘搜索词汇。例如`^[ghijklmno]+`的`+`表示什么意思? + +让我们进一步探索`+`符号。请注意,它可以适用于单个字母或括号内的字母集: + +```py +>>> chat_words = sorted(set(w for w in nltk.corpus.nps_chat.words())) +>>> [w for w in chat_words if re.search('^m+i+n+e+$', w)] +['miiiiiiiiiiiiinnnnnnnnnnneeeeeeeeee', 'miiiiiinnnnnnnnnneeeeeeee', 'mine', +'mmmmmmmmiiiiiiiiinnnnnnnnneeeeeeee'] +>>> [w for w in chat_words if re.search('^[ha]+$', w)] +['a', 'aaaaaaaaaaaaaaaaa', 'aaahhhh', 'ah', 'ahah', 'ahahah', 'ahh', +'ahhahahaha', 'ahhh', 'ahhhh', 'ahhhhhh', 'ahhhhhhhhhhhhhh', 'h', 'ha', 'haaa', +'hah', 'haha', 'hahaaa', 'hahah', 'hahaha', 'hahahaa', 'hahahah', 'hahahaha', ...] +``` + +很显然,`+`简单地表示“前面的项目的一个或多个实例”,它可以是单独的字母如`m`,可以是一个集合如`[fed]`或者一个范围如`[d-f]`。现在让我们用`*`替换`+`,它表示“前面的项目的零个或多个实例”。正则表达式`^m*i*n*e*`匹配`me`, `min`和`mmmmm`。请注意`+`和`*`符号有时被称为的 Kleene 闭包,或者干脆闭包。 + +运算符`^`当它出现在方括号内的第一个字符位置时有另外的功能。例如,`[^aeiouAEIOU]`匹配除元音字母之外的所有字母。我们可以搜索 NPS 聊天语料库中完全由非元音字母组成的词汇,使用`^[^aeiouAEIOU]+`。请注意其中包含非字母字符。 + +下面是另外一些正则表达式的例子,用来寻找匹配特定模式的词符,这些例子演示如何使用一些新的符号:`\`, `{}`, `()`和`|`。 + +```py +>>> wsj = sorted(set(nltk.corpus.treebank.words())) +>>> [w for w in wsj if re.search('^[0-9]+\.[0-9]+$', w)] +['0.0085', '0.05', '0.1', '0.16', '0.2', '0.25', '0.28', '0.3', '0.4', '0.5', +'0.50', '0.54', '0.56', '0.60', '0.7', '0.82', '0.84', '0.9', '0.95', '0.99', +'1.01', '1.1', '1.125', '1.14', '1.1650', '1.17', '1.18', '1.19', '1.2', ...] +>>> [w for w in wsj if re.search('^[A-Z]+\$$', w)] +['C$', 'US$'] +>>> [w for w in wsj if re.search('^[0-9]{4}$', w)] +['1614', '1637', '1787', '1901', '1903', '1917', '1925', '1929', '1933', ...] +>>> [w for w in wsj if re.search('^[0-9]+-[a-z]{3,5}$', w)] +['10-day', '10-lap', '10-year', '100-share', '12-point', '12-year', ...] +>>> [w for w in wsj if re.search('^[a-z]{5,}-[a-z]{2,3}-[a-z]{,6}$', w)] +['black-and-white', 'bread-and-butter', 'father-in-law', 'machine-gun-toting', +'savings-and-loan'] +>>> [w for w in wsj if re.search('(ed|ing)$', w)] +['62%-owned', 'Absorbed', 'According', 'Adopting', 'Advanced', 'Advancing', ...] +``` + +注意 + +**轮到你来**:研究前面的例子,在你继续阅读之前尝试弄清楚`\`, `{}`, `()`和`|`这些符号的功能。 + +你可能已经知道反斜杠表示其后面的字母不再有特殊的含义而是按照字面的表示匹配词中特定的字符。因此,虽然`.`很特别,但是`\.`只匹配一个句号。大括号表达式,如`{3,5}`, 表示前面的项目重复指定次数。管道字符表示从其左边的内容和右边的内容中选择一个。圆括号表示一个操作符的范围,它们可以与管道(或叫析取)符号一起使用,如`w(i|e|ai|oo)t`,匹配`wit`, `wet`, `wait`和`woot`。你可以省略这个例子里的最后一个表达式中的括号,使用`ed|ing`。 + +我们已经看到的元字符总结在 3.3 中: + +表 3.3: + +正则表达式基本元字符,其中包括通配符,范围和闭包 + +```py +>>> word = 'supercalifragilisticexpialidocious' +>>> re.findall(r'[aeiou]', word) +['u', 'e', 'a', 'i', 'a', 'i', 'i', 'i', 'e', 'i', 'a', 'i', 'o', 'i', 'o', 'u'] +>>> len(re.findall(r'[aeiou]', word)) +16 +``` + +让我们来看看一些文本中的两个或两个以上的元音序列,并确定它们的相对频率: + +```py +>>> wsj = sorted(set(nltk.corpus.treebank.words())) +>>> fd = nltk.FreqDist(vs for word in wsj +... for vs in re.findall(r'[aeiou]{2,}', word)) +>>> fd.most_common(12) +[('io', 549), ('ea', 476), ('ie', 331), ('ou', 329), ('ai', 261), ('ia', 253), +('ee', 217), ('oo', 174), ('ua', 109), ('au', 106), ('ue', 105), ('ui', 95)] +``` + +注意 + +**轮到你来**:在 W3C 日期时间格式中,日期像这样表示:2009-12-31。将以下 Python 代码中的`?`替换为正则表达式, 以便将字符串`'2009-12-31'`转换为整数列表`[2009, 12, 31]`: + +`[int(n) for n in re.findall(?, '2009-12-31')]` + +### 在单词片段上做更多事情 + +一旦我们会使用`re.findall()`从单词中提取素材,就可以在这些片段上做一些有趣的事情,例如将它们粘贴在一起或用它们绘图。 + +英文文本是高度冗余的,忽略掉词内部的元音仍然可以很容易的阅读,有些时候这很明显。例如,`declaration`变成`dclrtn`,`inalienable`变成`inlnble`,保留所有词首或词尾的元音序列。在我们的下一个例子中,正则表达式匹配词首元音序列,词尾元音序列和所有的辅音;其它的被忽略。这三个析取从左到右处理,如果词匹配三个部分中的一个,正则表达式后面的部分将被忽略。我们使用`re.findall()`提取所有匹配的词中的字符,然后使`''.join()`将它们连接在一起(更多连接操作参见 3.9)。 + +```py +>>> regexp = r'^[AEIOUaeiou]+|[AEIOUaeiou]+$|[^AEIOUaeiou]' +>>> def compress(word): +... pieces = re.findall(regexp, word) +... return ''.join(pieces) +... +>>> english_udhr = nltk.corpus.udhr.words('English-Latin1') +>>> print(nltk.tokenwrap(compress(w) for w in english_udhr[:75])) +Unvrsl Dclrtn of Hmn Rghts Prmble Whrs rcgntn of the inhrnt dgnty and +of the eql and inlnble rghts of all mmbrs of the hmn fmly is the fndtn +of frdm , jstce and pce in the wrld , Whrs dsrgrd and cntmpt fr hmn +rghts hve rsltd in brbrs acts whch hve outrgd the cnscnce of mnknd , +and the advnt of a wrld in whch hmn bngs shll enjy frdm of spch and +``` + +接下来,让我们将正则表达式与条件频率分布结合起来。在这里,我们将从罗托卡特语词汇中提取所有辅音-元音序列,如`ka`和`si`。因为每部分都是成对的,它可以被用来初始化一个条件频率分布。然后我们为每对的频率画出表格: + +```py +>>> rotokas_words = nltk.corpus.toolbox.words('rotokas.dic') +>>> cvs = [cv for w in rotokas_words for cv in re.findall(r'[ptksvr][aeiou]', w)] +>>> cfd = nltk.ConditionalFreqDist(cvs) +>>> cfd.tabulate() + a e i o u +k 418 148 94 420 173 +p 83 31 105 34 51 +r 187 63 84 89 79 +s 0 0 100 2 1 +t 47 8 0 148 37 +v 93 27 105 48 49 +``` + +考查`s`行和`t`行,我们看到它们是部分的“互补分布”,这个证据表明它们不是这种语言中的独特音素。从而我们可以令人信服的从罗托卡特语字母表中去除`s`,简单加入一个发音规则:当字母`t`跟在`i`后面时发`s`的音。(注意单独的条目`su`即`kasuari`,`cassowary`是从英语中借来的)。 + +如果我们想要检查表格中数字背后的词汇,有一个索引允许我们迅速找到包含一个给定的辅音-元音对的单词的列表将会有帮助,例如,`cv_index['su']`应该给我们所有含有`su`的词汇。下面是我们如何能做到这一点: + +```py +>>> cv_word_pairs = [(cv, w) for w in rotokas_words +... for cv in re.findall(r'[ptksvr][aeiou]', w)] +>>> cv_index = nltk.Index(cv_word_pairs) +>>> cv_index['su'] +['kasuari'] +>>> cv_index['po'] +['kaapo', 'kaapopato', 'kaipori', 'kaiporipie', 'kaiporivira', 'kapo', 'kapoa', +'kapokao', 'kapokapo', 'kapokapo', 'kapokapoa', 'kapokapoa', 'kapokapora', ...] +``` + +这段代码依次处理每个词`w`,对每一个词找出匹配正则表达式`[ptksvr][aeiou]`的所有子字符串。对于词`kasuari`,它找到`ka`, `su`和`ri`。因此,`cv_word_pairs`将包含`('ka', 'kasuari')`, `('su', 'kasuari')`和`('ri', 'kasuari')`。更进一步使用`nltk.Index()`转换成有用的索引。 + +### 查找词干 + +在使用网络搜索引擎时,我们通常不介意(甚至没有注意到)文档中的词汇与我们的搜索条件的后缀形式是否相同。查询`laptops`会找到含有`laptop`的文档,反之亦然。事实上,`laptop`与`laptops`只是词典中的同一个词(或词条)的两种形式。对于一些语言处理任务,我们想忽略词语结尾,只是处理词干。 + +抽出一个词的词干的方法有很多种。这里的是一种简单直观的方法,直接去掉任何看起来像一个后缀的字符: + +```py +>>> def stem(word): +... for suffix in ['ing', 'ly', 'ed', 'ious', 'ies', 'ive', 'es', 's', 'ment']: +... if word.endswith(suffix): +... return word[:-len(suffix)] +... return word +``` + +虽然我们最终将使用 NLTK 中内置的词干提取器,看看我们如何能够使用正则表达式处理这个任务是有趣的。我们的第一步是建立一个所有后缀的连接。我们需要把它放在括号内以限制这个析取的范围。 + +```py +>>> re.findall(r'^.*(ing|ly|ed|ious|ies|ive|es|s|ment)$', 'processing') +['ing'] +``` + +在这里,尽管正则表达式匹配整个单词,`re.findall()`只是给我们后缀。这是因为括号有第二个功能:选择要提取的子字符串。如果我们要使用括号来指定析取的范围,但不想选择要输出的字符串,必须添加`?:`,它是正则表达式许多神秘奥妙的地方之一。下面是改进后的版本。 + +```py +>>> re.findall(r'^.*(?:ing|ly|ed|ious|ies|ive|es|s|ment)$', 'processing') +['processing'] +``` + +然而,实际上,我们会想将词分成词干和后缀。所以,我们应该用括号括起正则表达式的这两个部分: + +```py +>>> re.findall(r'^(.*)(ing|ly|ed|ious|ies|ive|es|s|ment)$', 'processing') +[('process', 'ing')] +``` + +这看起来很有用途,但仍然有一个问题。让我们来看看另外的词,`processes`: + +```py +>>> re.findall(r'^(.*)(ing|ly|ed|ious|ies|ive|es|s|ment)$', 'processes') +[('processe', 's')] +``` + +正则表达式错误地找到了后缀`-s`,而不是后缀`-es`。这表明另一个微妙之处:星号操作符是“贪婪的”,所以表达式的`.*`部分试图尽可能多的匹配输入的字符串。如果我们使用“非贪婪”版本的`*`操作符,写成`*?`,我们就得到我们想要的: + +```py +>>> re.findall(r'^(.*?)(ing|ly|ed|ious|ies|ive|es|s|ment)$', 'processes') +[('process', 'es')] +``` + +我们甚至可以通过使第二个括号中的内容变成可选,来得到空后缀: + +```py +>>> re.findall(r'^(.*?)(ing|ly|ed|ious|ies|ive|es|s|ment)?$', 'language') +[('language', '')] +``` + +这种方法仍然有许多问题,(你能发现它们吗?)但我们仍将继续定义一个函数来获取词干,并将它应用到整个文本: + +```py +>>> def stem(word): +... regexp = r'^(.*?)(ing|ly|ed|ious|ies|ive|es|s|ment)?$' +... stem, suffix = re.findall(regexp, word)[0] +... return stem +... +>>> raw = """DENNIS: Listen, strange women lying in ponds distributing swords +... is no basis for a system of government. Supreme executive power derives from +... a mandate from the masses, not from some farcical aquatic ceremony.""" +>>> tokens = word_tokenize(raw) +>>> [stem(t) for t in tokens] +['DENNIS', ':', 'Listen', ',', 'strange', 'women', 'ly', 'in', 'pond', 'distribut', +'sword', 'i', 'no', 'basi', 'for', 'a', 'system', 'of', 'govern', '.', 'Supreme', +'execut', 'power', 'deriv', 'from', 'a', 'mandate', 'from', 'the', 'mass', ',', +'not', 'from', 'some', 'farcical', 'aquatic', 'ceremony', '.'] +``` + +请注意我们的正则表达式不但将`ponds`的`s`删除,也将`is`和`basis`的删除。它产生一些非词如`distribut`和`deriv`,但这些在一些应用中是可接受的词干。 + +### 搜索已分词文本 + +你可以使用一种特殊的正则表达式搜索一个文本中多个词(这里的文本是一个词符列表)。例如,`">a> >man>"`找出文本中所有`a man`的实例。尖括号用于标记词符的边界,尖括号之间的所有空白都被忽略(这只对 NLTK 中的`findall()`方法处理文本有效)。在下面的例子中,我们使用`<.*>`❶,它将匹配所有单个词符,将它括在括号里,于是只匹配词(例如`monied`)而不匹配短语(例如,`a monied man`)会生成。第二个例子找出以词`bro`结尾的三个词组成的短语❷。最后一个例子找出以字母`l`开始的三个或更多词组成的序列❸。 + +```py +>>> from nltk.corpus import gutenberg, nps_chat +>>> moby = nltk.Text(gutenberg.words('melville-moby_dick.txt')) +>>> moby.findall(r" (<.*>) ") ❶ +monied; nervous; dangerous; white; white; white; pious; queer; good; +mature; white; Cape; great; wise; wise; butterless; white; fiendish; +pale; furious; better; certain; complete; dismasted; younger; brave; +brave; brave; brave +>>> chat = nltk.Text(nps_chat.words()) +>>> chat.findall(r"<.*> <.*> ") ❷ +you rule bro; telling you bro; u twizted bro +>>> chat.findall(r"{3,}") ❸ +lol lol lol; lmao lol lol; lol lol lol; la la la la la; la la la; la +la la; lovely lol lol love; lol lol lol.; la la la; la la la +``` + +注意 + +**轮到你来**:巩固你对正则表达式模式与替换的理解,使用`nltk.re_show(p, s)`,它能标注字符串`s`中所有匹配模式`p`的地方,以及`nltk.app.nemo()`,它能提供一个探索正则表达式的图形界面。更多的练习,可以尝试本章尾的正则表达式的一些练习。 + +当我们研究的语言现象与特定词语相关时建立搜索模式是很容易的。在某些情况下,一个小小的创意可能会花很大功夫。例如,在大型文本语料库中搜索`x and other ys`形式的表达式能让我们发现上位词(见 5): + +```py +>>> from nltk.corpus import brown +>>> hobbies_learned = nltk.Text(brown.words(categories=['hobbies', 'learned'])) +>>> hobbies_learned.findall(r"<\w*> <\w*s>") +speed and other activities; water and other liquids; tomb and other +landmarks; Statues and other monuments; pearls and other jewels; +charts and other items; roads and other features; figures and other +objects; military and other areas; demands and other factors; +abstracts and other compilations; iron and other metals +``` + +只要有足够多的文本,这种做法会给我们一整套有用的分类标准信息,而不需要任何手工劳动。然而,我们的搜索结果中通常会包含误报,即我们想要排除的情况。例如,结果`demands and other factors`暗示`demand`是类型`factor`的一个实例,但是这句话实际上是关于要求增加工资的。尽管如此,我们仍可以通过手工纠正这些搜索的结果来构建自己的英语概念的本体。 + +注意 + +这种自动和人工处理相结合的方式是最常见的建造新的语料库的方式。我们将在 11 继续讲述这些。 + +搜索语料也会有遗漏的问题,即漏掉了我们想要包含的情况。仅仅因为我们找不到任何一个搜索模式的实例,就断定一些语言现象在一个语料库中不存在,是很冒险的。也许我们只是没有足够仔细的思考合适的模式。 + +注意 + +**轮到你来**:查找模式`as x as y`的实例以发现实体及其属性信息。 + +## 3.6 规范化文本 + +在前面的程序例子中,我们在处理文本词汇前经常要将文本转换为小写,即`set(w.lower() for w in text)`。通过使用`lower()`我们将文本规范化为小写,这样一来`The`与`the`的区别被忽略。我们常常想比这走得更远,去掉所有的词缀以及提取词干的任务等。更进一步的步骤是确保结果形式是字典中确定的词,即叫做词形归并的任务。我们依次讨论这些。首先,我们需要定义我们将在本节中使用的数据: + +```py +>>> raw = """DENNIS: Listen, strange women lying in ponds distributing swords +... is no basis for a system of government. Supreme executive power derives from +... a mandate from the masses, not from some farcical aquatic ceremony.""" +>>> tokens = word_tokenize(raw) +``` + +### 词干提取器 + +NLTK 中包括几个现成的词干提取器,如果你需要一个词干提取器,你应该优先使用它们中的一个,而不是使用正则表达式制作自己的词干提取器,因为 NLTK 中的词干提取器能处理的不规则的情况很广泛。`Porter`和`Lancaster`词干提取器按照它们自己的规则剥离词缀。请看`Porter`词干提取器正确处理了词`lying`(将它映射为`lie`),而`Lancaster`词干提取器并没有处理好。 + +```py +>>> porter = nltk.PorterStemmer() +>>> lancaster = nltk.LancasterStemmer() +>>> [porter.stem(t) for t in tokens] +['DENNI', ':', 'Listen', ',', 'strang', 'women', 'lie', 'in', 'pond', +'distribut', 'sword', 'is', 'no', 'basi', 'for', 'a', 'system', 'of', 'govern', +'.', 'Suprem', 'execut', 'power', 'deriv', 'from', 'a', 'mandat', 'from', +'the', 'mass', ',', 'not', 'from', 'some', 'farcic', 'aquat', 'ceremoni', '.'] +>>> [lancaster.stem(t) for t in tokens] +['den', ':', 'list', ',', 'strange', 'wom', 'lying', 'in', 'pond', 'distribut', +'sword', 'is', 'no', 'bas', 'for', 'a', 'system', 'of', 'govern', '.', 'suprem', +'execut', 'pow', 'der', 'from', 'a', 'mand', 'from', 'the', 'mass', ',', 'not', +'from', 'som', 'farc', 'aqu', 'ceremony', '.'] +``` + +词干提取过程没有明确定义,我们通常选择心目中最适合我们的应用的词干提取器。如果你要索引一些文本和使搜索支持不同词汇形式的话,`Porter`词干提取器是一个很好的选择(3.6 所示,它采用*面向对象*编程技术,这超出了本书的范围,字符串格式化技术将在 3.9 讲述,`enumerate()`函数将在 4.2 解释)。 + +```py +class IndexedText(object): + + def __init__(self, stemmer, text): + self._text = text + self._stemmer = stemmer + self._index = nltk.Index((self._stem(word), i) + for (i, word) in enumerate(text)) + + def concordance(self, word, width=40): + key = self._stem(word) + wc = int(width/4) # words of context + for i in self._index[key]: + lcontext = ' '.join(self._text[i-wc:i]) + rcontext = ' '.join(self._text[i:i+wc]) + ldisplay = '{:>{width}}'.format(lcontext[-width:], width=width) + rdisplay = '{:{width}}'.format(rcontext[:width], width=width) + print(ldisplay, rdisplay) + + def _stem(self, word): + return self._stemmer.stem(word).lower() +``` + +### 词形归并 + +WordNet 词形归并器只在产生的词在它的词典中时才删除词缀。这个额外的检查过程使词形归并器比刚才提到的词干提取器要慢。请注意,它并没有处理`lying`,但它将`women`转换为`woman`。 + +```py +>>> wnl = nltk.WordNetLemmatizer() +>>> [wnl.lemmatize(t) for t in tokens] +['DENNIS', ':', 'Listen', ',', 'strange', 'woman', 'lying', 'in', 'pond', +'distributing', 'sword', 'is', 'no', 'basis', 'for', 'a', 'system', 'of', +'government', '.', 'Supreme', 'executive', 'power', 'derives', 'from', 'a', +'mandate', 'from', 'the', 'mass', ',', 'not', 'from', 'some', 'farcical', +'aquatic', 'ceremony', '.'] +``` + +如果你想编译一些文本的词汇,或者想要一个有效词条(或中心词)列表,WordNet 词形归并器是一个不错的选择。 + +注意 + +另一个规范化任务涉及识别非标准词,包括数字、缩写、日期以及映射任何此类词符到一个特殊的词汇。例如,每一个十进制数可以被映射到一个单独的标识符`0.0`,每首字母缩写可以映射为`AAA`。这使词汇量变小,提高了许多语言建模任务的准确性。 + +## 3.7 用正则表达式为文本分词 + +分词是将字符串切割成可识别的构成一块语言数据的语言单元。虽然这是一项基础任务,我们能够一直拖延到现在为止才讲,是因为许多语料库已经分过词了,也因为 NLTK 中包括一些分词器。现在你已经熟悉了正则表达式,你可以学习如何使用它们来为文本分词,并对此过程中有更多的掌控权。 + +### 分词的简单方法 + +文本分词的一种非常简单的方法是在空格符处分割文本。考虑以下摘自《爱丽丝梦游仙境》中的文本: + +```py +>>> raw = """'When I'M a Duchess,' she said to herself, (not in a very hopeful tone +... though), 'I won't have any pepper in my kitchen AT ALL. Soup does very +... well without--Maybe it's always pepper that makes people hot-tempered,'...""" +``` + +我们可以使用`raw.split()`在空格符处分割原始文本。使用正则表达式能做同样的事情,匹配字符串中的所有空格符❶是不够的,因为这将导致分词结果包含`\n`换行符;我们需要匹配任何数量的空格符、制表符或换行符❷: + +```py +>>> re.split(r' ', raw) ❶ +["'When", "I'M", 'a', "Duchess,'", 'she', 'said', 'to', 'herself,', '(not', 'in', +'a', 'very', 'hopeful', 'tone\nthough),', "'I", "won't", 'have', 'any', 'pepper', +'in', 'my', 'kitchen', 'AT', 'ALL.', 'Soup', 'does', 'very\nwell', 'without--Maybe', +"it's", 'always', 'pepper', 'that', 'makes', 'people', "hot-tempered,'..."] +>>> re.split(r'[ \t\n]+', raw) ❷ +["'When", "I'M", 'a', "Duchess,'", 'she', 'said', 'to', 'herself,', '(not', 'in', +'a', 'very', 'hopeful', 'tone', 'though),', "'I", "won't", 'have', 'any', 'pepper', +'in', 'my', 'kitchen', 'AT', 'ALL.', 'Soup', 'does', 'very', 'well', 'without--Maybe', +"it's", 'always', 'pepper', 'that', 'makes', 'people', "hot-tempered,'..."] +``` + +正则表达式`[ \t\n]+`匹配一个或多个空格、制表符(`\t`)或换行符(`\n`)。其他空白字符,如回车和换页符,实际上应该也包含。于是,我们将使用一个`re`库内置的缩写`\s`,它表示匹配所有空白字符。前面的例子中第二条语句可以改写为`re.split(r'\s+', raw)`。 + +注意 + +**要点**:记住在正则表达式前加字母`r`(表示“原始的”),它告诉 Python 解释器按照字面表示对待字符串,而不去处理正则表达式中包含的反斜杠字符。 + +在空格符处分割文本给我们如`'(not'`和`'herself,'`这样的词符。另一种方法是使用 Python 提供给我们的字符类`\w`匹配词中的字符,相当于`[a-zA-Z0-9_]`。它还定义了这个类的补集`\W`,即所有字母、数字和下划线以外的字符。我们可以在一个简单的正则表达式中用`\W`来分割所有单词字符*以外*的输入: + +```py +>>> re.split(r'\W+', raw) +['', 'When', 'I', 'M', 'a', 'Duchess', 'she', 'said', 'to', 'herself', 'not', 'in', +'a', 'very', 'hopeful', 'tone', 'though', 'I', 'won', 't', 'have', 'any', 'pepper', +'in', 'my', 'kitchen', 'AT', 'ALL', 'Soup', 'does', 'very', 'well', 'without', +'Maybe', 'it', 's', 'always', 'pepper', 'that', 'makes', 'people', 'hot', 'tempered', +''] +``` + +可以看到,在开始和结尾都给了我们一个空字符串(要了解原因请尝试`'xx'.split('x')`)。通过`re.findall(r'\w+', raw)`使用模式匹配词汇而不是空白符号,我们得到相同的标识符,但没有空字符串。现在,我们正在匹配词汇,我们处在扩展正则表达式覆盖更广泛的情况的位置。正则表达式`\w+|\S\w*`将首先尝试匹配词中字符的所有序列。如果没有找到匹配的,它会尝试匹配后面跟着词中字符的任何*非*空白字符(`\S`是`\s`的补)。这意味着标点会与跟在后面的字母(如`'s`)在一起,但两个或两个以上的标点字符序列会被分割。 + +```py +>>> re.findall(r'\w+|\S\w*', raw) +["'When", 'I', "'M", 'a', 'Duchess', ',', "'", 'she', 'said', 'to', 'herself', ',', +'(not', 'in', 'a', 'very', 'hopeful', 'tone', 'though', ')', ',', "'I", 'won', "'t", +'have', 'any', 'pepper', 'in', 'my', 'kitchen', 'AT', 'ALL', '.', 'Soup', 'does', +'very', 'well', 'without', '-', '-Maybe', 'it', "'s", 'always', 'pepper', 'that', +'makes', 'people', 'hot', '-tempered', ',', "'", '.', '.', '.'] +``` + +让我们扩展前面表达式中的`\w+`,允许连字符和撇号:`\w+([-']\w+)*`。这个表达式表示`\w+`后面跟零个或更多`[-']\w+`的实例;它会匹配`hot-tempered`和`it's`。(我们需要在这个表达式中包含`?:`,原因前面已经讨论过。)我们还将添加一个模式来匹配引号字符,让它们与它们包括的文字分开。 + +```py +>>> print(re.findall(r"\w+(?:[-']\w+)*|'|[-.(]+|\S\w*", raw)) +["'", 'When', "I'M", 'a', 'Duchess', ',', "'", 'she', 'said', 'to', 'herself', ',', +'(', 'not', 'in', 'a', 'very', 'hopeful', 'tone', 'though', ')', ',', "'", 'I', +"won't", 'have', 'any', 'pepper', 'in', 'my', 'kitchen', 'AT', 'ALL', '.', 'Soup', +'does', 'very', 'well', 'without', '--', 'Maybe', "it's", 'always', 'pepper', +'that', 'makes', 'people', 'hot-tempered', ',', "'", '...'] +``` + +上面的表达式也包括`[-.(]+`,这会使双连字符、省略号和左括号被单独分词。 + +3.4 列出了我们已经在本节中看到的正则表达式字符类符号,以及一些其他有用的符号。 + +表 3.4: + +正则表达式符号 + +```py +>>> text = 'That U.S.A. poster-print costs $12.40...' +>>> pattern = r'''(?x) # set flag to allow verbose regexps +... ([A-Z]\.)+ # abbreviations, e.g. U.S.A. +... | \w+(-\w+)* # words with optional internal hyphens +... | \$?\d+(\.\d+)?%? # currency and percentages, e.g. $12.40, 82% +... | \.\.\. # ellipsis +... | [][.,;"'?():-_`] # these are separate tokens; includes ], [ +... ''' +>>> nltk.regexp_tokenize(text, pattern) +['That', 'U.S.A.', 'poster-print', 'costs', '$12.40', '...'] +``` + +使用 verbose 标志时,不可以再使用`' '`来匹配一个空格字符;使用`\s`代替。`regexp_tokenize()`函数有一个可选的`gaps`参数。设置为`True`时,正则表达式指定标识符间的距离,就像使用`re.split()`一样。 + +注意 + +我们可以使用`set(tokens).difference(wordlist)`通过比较分词结果与一个词表,然后报告任何没有在词表出现的标识符,来评估一个分词器。你可能想先将所有标记变成小写。 + +### 分词的进一步问题 + +分词是一个比你可能预期的要更为艰巨的任务。没有单一的解决方案能在所有领域都行之有效,我们必须根据应用领域的需要决定那些是词符。 + +在开发分词器时,访问已经手工分词的原始文本是有益的,这可以让你的分词器的输出结果与高品质(或称“黄金标准”)的词符进行比较。NLTK 语料库集合包括宾州树库的数据样本,包括《华尔街日报》原始文本(`nltk.corpus.treebank_raw.raw()`)和分好词的版本(`nltk.corpus.treebank.words()`)。 + +分词的最后一个问题是缩写的存在,如`didn't`。如果我们想分析一个句子的意思,将这种形式规范化为两个独立的形式:`did`和`n't`(或`not`)可能更加有用。我们可以通过查表来做这项工作。 + +## 3.8 分割 + +本节将讨论更高级的概念,你在第一次阅读本章时可能更愿意跳过本节。 + +分词是一个更普遍的分割问题的一个实例。在本节中,我们将看到这个问题的另外两个实例,它们使用与到目前为止我们已经在本章看到的完全不同的技术。 + +### 断句 + +在词级水平处理文本通常假定能够将文本划分成单个句子。正如我们已经看到,一些语料库已经提供句子级别的访问。在下面的例子中,我们计算布朗语料库中每个句子的平均词数: + +```py +>>> len(nltk.corpus.brown.words()) / len(nltk.corpus.brown.sents()) +20.250994070456922 +``` + +在其他情况下,文本可能只是作为一个字符流。在将文本分词之前,我们需要将它分割成句子。NLTK 通过包含`Punkt`句子分割器(Kiss & Strunk, 2006)使得这个功能便于使用。这里是使用它为一篇小说文本断句的例子。(请注意,如果在你读到这篇文章时分割器内部数据已经更新过,你会看到不同的输出): + +```py +>>> text = nltk.corpus.gutenberg.raw('chesterton-thursday.txt') +>>> sents = nltk.sent_tokenize(text) +>>> pprint.pprint(sents[79:89]) +['"Nonsense!"', + 'said Gregory, who was very rational when anyone else\nattempted paradox.', + '"Why do all the clerks and navvies in the\n' + 'railway trains look so sad and tired, so very sad and tired?', + 'I will\ntell you.', + 'It is because they know that the train is going right.', + 'It\n' + 'is because they know that whatever place they have taken a ticket\n' + 'for that place they will reach.', + 'It is because after they have\n' + 'passed Sloane Square they know that the next station must be\n' + 'Victoria, and nothing but Victoria.', + 'Oh, their wild rapture!', + 'oh,\n' + 'their eyes like stars and their souls again in Eden, if the next\n' + 'station were unaccountably Baker Street!"', + '"It is you who are unpoetical," replied the poet Syme.'] +``` + +请注意,这个例子其实是一个单独的句子,报道 Lucian Gregory 先生的演讲。然而,引用的演讲包含几个句子,这些已经被分割成几个单独的字符串。这对于大多数应用程序是合理的行为。 + +断句是困难的,因为句号会被用来标记缩写而另一些句号同时标记缩写和句子结束,就像发生在缩写如`U.S.A.`上的那样。 + +断句的另一种方法见 2 节。 + +### 分词 + +对于一些书写系统,由于没有词的可视边界表示这一事实,文本分词变得更加困难。例如,在中文中,三个字符的字符串:爱国人`(ai4 "love" [verb], guo3 "country", ren2 "person")` 可以被分词为“爱国/人”,`country-loving person`,或者“爱/国人”,`love country-person`。 + +类似的问题在口语语言处理中也会出现,听者必须将连续的语音流分割成单个的词汇。当我们事先不认识这些词时,这个问题就演变成一个特别具有挑战性的版本。语言学习者会面对这个问题,例如小孩听父母说话。考虑下面的人为构造的例子,单词的边界已被去除: + +```py +>>> text = "doyouseethekittyseethedoggydoyoulikethekittylikethedoggy" +>>> seg1 = "0000000000000001000000000010000000000000000100000000000" +>>> seg2 = "0100100100100001001001000010100100010010000100010010000" +``` + +观察由 0 和 1 组成的分词表示字符串。它们比源文本短一个字符,因为长度为`n`文本可以在`n-1`个地方被分割。3.7 中的`segment()`函数演示了我们可以从这个表示回到初始分词的文本。 + +```py +def segment(text, segs): + words = [] + last = 0 + for i in range(len(segs)): + if segs[i] == '1': + words.append(text[last:i+1]) + last = i+1 + words.append(text[last:]) + return words +``` + +现在分词的任务变成了一个搜索问题:找到将文本字符串正确分割成词汇的字位串。我们假定学习者接收词,并将它们存储在一个内部词典中。给定一个合适的词典,是能够由词典中的词的序列来重构源文本的。根据(Brent, 1995),我们可以定义一个目标函数,一个打分函数,我们将基于词典的大小和从词典中重构源文本所需的信息量尽力优化它的值。我们在 3.8 中说明了这些。 + +![Images/brent.png](Images/ced4e829d6a662a2be20187f9d7b71b5.jpg) + +图 3.8:计算目标函数:给定一个假设的源文本的分词(左),推导出一个词典和推导表,它能让源文本重构,然后合计每个词项(包括边界标志)与推导表的字符数,作为分词质量的得分;得分值越小表明分词越好。 + +实现这个目标函数是很简单的,如例子 3.9 所示。 + +```py +def evaluate(text, segs): + words = segment(text, segs) + text_size = len(words) + lexicon_size = sum(len(word) + 1 for word in set(words)) + return text_size + lexicon_size +``` + +最后一步是寻找最小化目标函数值的 0 和 1 的模式,如 3.10 所示。请注意,最好的分词包括像`thekitty`这样的“词”,因为数据中没有足够的证据进一步分割这个词。 + +```py +from random import randint + +def flip(segs, pos): + return segs[:pos] + str(1-int(segs[pos])) + segs[pos+1:] + +def flip_n(segs, n): + for i in range(n): + segs = flip(segs, randint(0, len(segs)-1)) + return segs + +def anneal(text, segs, iterations, cooling_rate): + temperature = float(len(segs)) + while temperature > 0.5: + best_segs, best = segs, evaluate(text, segs) + for i in range(iterations): + guess = flip_n(segs, round(temperature)) + score = evaluate(text, guess) + if score < best: + best, best_segs = score, guess + score, segs = best, best_segs + temperature = temperature / cooling_rate + print(evaluate(text, segs), segment(text, segs)) + print() + return segs +``` + +有了足够的数据,就可能以一个合理的准确度自动将文本分割成词汇。这种方法可用于为那些词的边界没有任何视觉表示的书写系统分词。 + +## 3.9 格式化:从列表到字符串 + +我们经常会写程序来汇报一个单独的数据项例如一个语料库中满足一些复杂的标准的特定的元素,或者一个单独的总数统计例如一个词计数器或一个标注器的性能。更多的时候,我们写程序来产生一个结构化的结果;例如:一个数字或语言形式的表格,或原始数据的格式变换。当要表示的结果是语言时,文字输出通常是最自然的选择。然而当结果是数值时,可能最好是图形输出。在本节中,你将会学到呈现程序输出的各种方式。 + +### 从列表到字符串 + +我们用于文本处理的最简单的一种结构化对象是词列表。当我们希望把这些输出到显示器或文件时,必须把这些词列表转换成字符串。在 Python 做这些,我们使用`join()`方法,并指定字符串作为使用的“胶水”。 + +```py +>>> silly = ['We', 'called', 'him', 'Tortoise', 'because', 'he', 'taught', 'us', '.'] +>>> ' '.join(silly) +'We called him Tortoise because he taught us .' +>>> ';'.join(silly) +'We;called;him;Tortoise;because;he;taught;us;.' +>>> ''.join(silly) +'WecalledhimTortoisebecausehetaughtus.' +``` + +所以`' '.join(silly)`的意思是:取出`silly`中的所有项目,将它们连接成一个大的字符串,使用`' '`作为项目之间的间隔符。即`join()`是一个你想要用来作为胶水的字符串的一个方法。(许多人感到`join()`的这种表示方法是违反直觉的。)`join()`方法只适用于一个字符串的列表——我们一直把它叫做一个文本——在 Python 中享有某些特权的一个复杂类型。 + +### 字符串与格式 + +我们已经看到了有两种方式显示一个对象的内容: + +```py +>>> word = 'cat' +>>> sentence = """hello +... world""" +>>> print(word) +cat +>>> print(sentence) +hello +world +>>> word +'cat' +>>> sentence +'hello\nworld' +``` + +`print`命令让 Python 努力以人最可读的形式输出的一个对象的内容。第二种方法——叫做变量提示——向我们显示可用于重新创建该对象的字符串。重要的是要记住这些都仅仅是字符串,为了你用户的方便而显示的。它们并不会给我们实际对象的内部表示的任何线索。 + +还有许多其他有用的方法来将一个对象作为字符串显示。这可能是为了人阅读的方便,或是因为我们希望导出我们的数据到一个特定的能被外部程序使用的文件格式。 + +格式化输出通常包含变量和预先指定的字符串的一个组合,例如给定一个频率分布`fdist`,我们可以这样做: + +```py +>>> fdist = nltk.FreqDist(['dog', 'cat', 'dog', 'cat', 'dog', 'snake', 'dog', 'cat']) +>>> for word in sorted(fdist): +... print(word, '->', fdist[word], end='; ') +cat -> 3; dog -> 4; snake -> 1; +``` + +输出包含变量和常量交替出现的表达式是难以阅读和维护的。一个更好的解决办法是使用字符串格式化表达式。 + +```py +>>> for word in sorted(fdist): +... print('{}->{};'.format(word, fdist[word]), end=' ') +cat->3; dog->4; snake->1; +``` + +要了解这里发生了什么事情,让我们在字符串格式化表达式上面测试一下。(现在,这将是你探索新语法的常用方法。) + +```py +>>> '{}->{};'.format ('cat', 3) +'cat->3;' +``` + +花括号`'{}'`标记一个替换字段的出现:它作为传递给`str.format()`方法的对象的字符串值的占位符。我们可以将`'{}'`嵌入到一个字符串的内部,然后以适当的参数调用`format()`来让字符串替换它们。包含替换字段的字符串叫做格式字符串。 + +让我们更深入的解开这段代码,以便更仔细的观察它的行为: + +```py +>>> '{}->'.format('cat') +'cat->' +>>> '{}'.format(3) +'3' +>>> 'I want a {} right now'.format('coffee') +'I want a coffee right now' +``` + +我们可以有任意个数目的占位符,但`str.format`方法必须以数目完全相同的参数来调用。 + +```py +>>> '{} wants a {} {}'.format ('Lee', 'sandwich', 'for lunch') +'Lee wants a sandwich for lunch' +>>> '{} wants a {} {}'.format ('sandwich', 'for lunch') +Traceback (most recent call last): +... + '{} wants a {} {}'.format ('sandwich', 'for lunch') +IndexError: tuple index out of range +``` + +从左向右取用给`format()`的参数,任何多余的参数都会被简单地忽略。 + + + +Unexpected indentation. + +```py +>>> '{} wants a {}'.format ('Lee', 'sandwich', 'for lunch') +'Lee wants a sandwich' +``` + +格式字符串中的替换字段可以以一个数值开始,它表示`format()`的位置参数。`'from {} to {}'`这样的语句等同于`'from {0} to {1}'`,但是我们使用数字来得到非默认的顺序: + +```py +>>> 'from {1} to {0}'.format('A', 'B') +'from B to A' +``` + +我们还可以间接提供值给占位符。下面是使用`for`循环的一个例子: + +```py +>>> template = 'Lee wants a {} right now' +>>> menu = ['sandwich', 'spam fritter', 'pancake'] +>>> for snack in menu: +... print(template.format(snack)) +... +Lee wants a sandwich right now +Lee wants a spam fritter right now +Lee wants a pancake right now +``` + +### 对齐 + +到目前为止,我们的格式化字符串可以在页面(或屏幕)上输出任意的宽度。我们可以通过插入一个冒号`':'`跟随一个整数来添加空白以获得指定宽带的输出。所以`{:6}`表示我们想让字符串对齐到宽度 6。数字默认表示右对齐❶,单我们可以在宽度指示符前面加上`'>'`对齐选项来让数字左对齐❷。 + +```py +>>> '{:6}'.format(41) ❶ +' 41' +>>> '{:<6}' .format(41) ❷ +'41 ' +``` + +字符串默认是左对齐,但可以通过`'>'`对齐选项右对齐。 + + + +Unexpected indentation. + +```py +>>> '{:6}'.format('dog') ❶ +'dog ' +>>> '{:>6}'.format('dog') ❷ + ' dog' +``` + +其它控制字符可以用于指定浮点数的符号和精度;例如`{:.4f}`表示浮点数的小数点后面应该显示 4 个数字。 + +```py +>>> import math +>>> '{:.4f}'.format(math.pi) +'3.1416' +``` + +字符串格式化很聪明,能够知道如果你包含一个`'%'`在你的格式化字符串中,那么你想表示这个值为百分数;不需要乘以 100。 + +```py +>>> count, total = 3205, 9375 +>>> "accuracy for {} words: {:.4%}".format(total, count / total) +'accuracy for 9375 words: 34.1867%' +``` + +格式化字符串的一个重要用途是用于数据制表。回想一下,在 1 中,我们看到从条件频率分布中制表的数据。让我们自己来制表,行使对标题和列宽的完全控制,如 3.11 所示。注意语言处理工作与结果制表之间是明确分离的。 + +```py +def tabulate(cfdist, words, categories): + print('{:16}'.format('Category'), end=' ') # column headings + for word in words: + print('{:>6}'.format(word), end=' ') + print() + for category in categories: + print('{:16}'.format(category), end=' ') # row heading + for word in words: # for each word + print('{:6}'.format(cfdist[category][word]), end=' ') # print table cell + print() # end the row + +>>> from nltk.corpus import brown +>>> cfd = nltk.ConditionalFreqDist( +... (genre, word) +... for genre in brown.categories() +... for word in brown.words(categories=genre)) +>>> genres = ['news', 'religion', 'hobbies', 'science_fiction', 'romance', 'humor'] +>>> modals = ['can', 'could', 'may', 'might', 'must', 'will'] +>>> tabulate(cfd, modals, genres) +Category can could may might must will +news 93 86 66 38 50 389 +religion 82 59 78 12 54 71 +hobbies 268 58 131 22 83 264 +science_fiction 16 49 4 12 8 16 +romance 74 193 11 51 45 43 +humor 16 30 8 8 9 13 +``` + +回想一下 3.6 中的列表, 我们使用格式字符串`'{:{width}}'`并绑定一个值给`format()`中的`width`参数。这我们使用变量知道字段的宽度。 + +```py +>>> '{:{width}}' % ("Monty Python", width=15) +'Monty Python ' +``` + +我们可以使用`width = max(len(w) for w in words)`自动定制列的宽度,使其足够容纳所有的词。 + +### 将结果写入文件 + +我们已经看到了如何读取文本文件`(3.1)`。将输出写入文件往往也很有用。下面的代码打开可写文件`output.txt`,将程序的输出保存到文件。 + +```py +>>> output_file = open('output.txt', 'w') +>>> words = set(nltk.corpus.genesis.words('english-kjv.txt')) +>>> for word in sorted(words): +... print(word, file=output_file) +``` + +当我们将非文本数据写入文件时,我们必须先将它转换为字符串。正如我们前面所看到的,可以使用格式化字符串来做这一转换。让我们把总词数写入我们的文件: + +```py +>>> len(words) +2789 +>>> str(len(words)) +'2789' +>>> print(str(len(words)), file=output_file) +``` + +小心! + +你应该避免包含空格字符的文件名例如`output file.txt`,和除了大小写外完全相同的文件名,例如`Output.txt`和`output.TXT`。 + +### 文本换行 + +当程序的输出是文档式的而不是像表格时,通常会有必要包装一下以便可以方便地显示它。考虑下面的输出,它的行尾溢出了,且使用了一个复杂的`print`语句: + +```py +>>> saying = ['After', 'all', 'is', 'said', 'and', 'done', ',', +... 'more', 'is', 'said', 'than', 'done', '.'] +>>> for word in saying: +... print(word, '(' + str(len(word)) + '),', end=' ') +After (5), all (3), is (2), said (4), and (3), done (4), , (1), more (4), is (2), said (4), than (4), done (4), . (1), +``` + +我们可以在 Python 的`textwrap`模块的帮助下采取换行。为了最大程度的清晰,我们将每一个步骤分在一行: + +```py +>>> from textwrap import fill +>>> format = '%s (%d),' +>>> pieces = [format % (word, len(word)) for word in saying] +>>> output = ' '.join(pieces) +>>> wrapped = fill(output) +>>> print(wrapped) +After (5), all (3), is (2), said (4), and (3), done (4), , (1), more +(4), is (2), said (4), than (4), done (4), . (1), +``` + +请注意,在`more`与其下面的数字之间有一个换行符。如果我们希望避免这种情况,可以重新定义格式化字符串,使它不包含空格(例如`'%s_(%d),'`,然后不输出`wrapped`的值,我们可以输出`wrapped.replace('_', ' ')`。 + +## 3.10 小结 + +* 在本书中,我们将文本作为一个单词列表。“原始文本”是一个潜在的长字符串,其中包含文字和用于设置格式的空白字符,也是我们通常存储和可视化文本的方式。 +* 在 Python 中指定一个字符串使用单引号或双引号:`'Monty Python'`,`"Monty Python"`。 +* 字符串中的字符是使用索引来访问的,索引从零计数:`'Monty Python'[0]`给出的值是`M`。字符串的长度使用`len()`得到。 +* 子字符串使用切片符号来访问: `'Monty Python'[1:5]`给出的值是`onty`。如果省略起始索引,子字符串从字符串的开始处开始;如果省略结尾索引,切片会一直到字符串的结尾处结束。 +* 字符串可以被分割成列表:`'Monty Python'.split()`给出`['Monty', 'Python']`。列表可以连接成字符串:`'/'.join(['Monty', 'Python'])`给出`'Monty/Python'`。 +* 我们可以使用`text = open('input.txt').read()`从一个文件`input.txt`读取文本。可以使用`text = request.urlopen(url).read().decode('utf8')`从一个`url`读取文本。我们可以使用`for line in open(f)`遍历一个文本文件的每一行。 +* 我们可以通过打开一个用于写入的文件`output_file = open('output.txt', 'w')`来向文件写入文本,然后添加内容到文件中`print("Monty Python", file=output_file)`。 +* 在网上找到的文本可能包含不需要的内容(如页眉、页脚和标记),在我们做任何语言处理之前需要去除它们。 +* 分词是将文本分割成基本单位或词符,例如词和标点符号等。基于空格符的分词对于许多应用程序都是不够的,因为它会捆绑标点符号和词。NLTK 提供了一个现成的分词器`nltk.word_tokenize()`。 +* 词形归并是一个过程,将一个词的各种形式(如`appeared`,`appears`)映射到这个词标准的或引用的形式,也称为词位或词元(如`appear`)。 +* 正则表达式是用来指定模式的一种强大而灵活的方法。一旦我们导入`re`模块,我们就可以使用`re.findall()`来找到一个字符串中匹配一个模式的所有子字符串。 +* 如果一个正则表达式字符串包含一个反斜杠,你应该使用带有一个`r`前缀的原始字符串:`r'regexp'`,来告诉 Python 不要预处理这个字符串。 +* 当某些字符前使用了反斜杠时,例如`\n`, 处理时会有特殊的含义(换行符);然而,当反斜杠用于正则表达式通配符和操作符之前时,如`\.`, `\|`, `\ +* 一个字符串格式化表达式`template % arg_tuple`包含一个格式字符串`template`,它由如`%-6s`和`%0.2d`这样的转换标识符符组成。 + +## 7 深入阅读 + +本章的附加材料发布在`http://nltk.org/`,包括网络上免费提供的资源的链接。记得咨询`http://docs.python.org/`上的的参考材料。(例如:此文档涵盖“通用换行符支持”,解释了各种操作系统如何规定不同的换行符。) + +更多的使用 NLTK 处理词汇的例子请参阅`http://nltk.org/howto`上的分词、词干提取以及语料库 HOWTO 文档。(Jurafsky & Martin, 2008)的第 2、3 章包含正则表达式和形态学的更高级的材料。Python 文本处理更广泛的讨论请参阅(Mertz, 2003)。规范非标准词的信息请参阅(Sproat et al, 2001) + +关于正则表达式的参考材料很多,无论是理论的还是实践的。在 Python 中使用正则表达式的一个入门教程,请参阅 Kuchling 的《Regular Expression HOWTO》,`http://www.amk.ca/python/howto/regex/`。关于使用正则表达式的全面而详细的手册,请参阅(Friedl, 2002),其中涵盖包括 Python 在内大多数主要编程语言的语法。其他材料还包括(Jurafsky & Martin, 2008)的第 2.1 节,(Mertz, 2003)的第 3 章。 + +网上有许多关于 Unicode 的资源。以下是与处理 Unicode 的 Python 的工具有关的有益的讨论: + ++ `Ned Batchelder, Pragmatic Unicode, http://nedbatchelder.com/text/unipain.html` ++ `Unicode HOWTO, Python Documentation, http://docs.python.org/3/howto/unicode.html` ++ `David Beazley, Mastering Python 3 I/O, http://pyvideo.org/video/289/pycon-2010--mastering-python-3-i-o` ++ `Joel Spolsky, The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!), http://www.joelonsoftware.com/articles/Unicode.html` + +SIGHAN,ACL 中文语言处理特别兴趣小组`http://sighan.org/`,重点关注中文文本分词的问题。我们分割英文文本的方法依据(Brent, 1995);这项工作属于语言获取领域(Niyogi, 2006)。 + +搭配是多词表达式的一种特殊情况。一个多词表达式是一个小短语,仅从它的词汇不能预测它的意义和其他属性,例如词性(Baldwin & Kim, 2010)。 + +模拟退火是一种启发式算法,找寻在一个大型的离散的搜索空间上的一个函数的最佳值的最好近似,基于对金属冶炼中的退火的模拟。该技术在许多人工智能文本中都有描述。 + +(Hearst, 1992)描述了使用如 x and other ys 的搜索模式发现文本中下位词的方法。 + +## 3.12 练习 + +1. ☼ 定义一个字符串`s = 'colorless'`。写一个 Python 语句将其变为`colourless`,只使用切片和连接操作。 + +2. ☼ 我们可以使用切片符号删除词汇形态上的结尾。例如,`'dogs'[:-1]`删除了`dogs`的最后一个字符,留下`dog`。使用切片符号删除下面这些词的词缀(我们插入了一个连字符指示词缀的边界,请在你的字符串中省略掉连字符): `dish-es`, `run-ning`, `nation-ality`, `un-do`, `pre-heat`。 + +3. ☼ 我们看到如何通过索引超出一个字符串的末尾产生一个`IndexError`。构造一个向左走的太远走到字符串的前面的索引,这有可能吗? + +4. ☼ 我们可以为分片指定一个“步长”。下面的表达式间隔一个字符返回一个片内字符:`monty[6:11:2]`。也可以反向进行:`monty[10:5:-2]`。自己尝试一下,然后实验不同的步长。 + +5. ☼ 如果你让解释器处理`monty[::-1]`会发生什么?解释为什么这是一个合理的结果。 + +6. ☼ 说明以下的正则表达式匹配的字符串类。 + + 1. `[a-zA-Z]+` + 2. `[A-Z][a-z]*` + 3. `p[aeiou]{,2}t` + 4. `\d+(\.\d+)?` + 5. `([^aeiou][aeiou][^aeiou])*` + 6. `\w+|[^\w\s]+` + + 使用`nltk.re_show()`测试你的答案。 + +7. ☼ 写正则表达式匹配下面字符串类: + + 1. 一个单独的限定符(假设只有`a, an, the`为限定符)。 + 2. 整数加法和乘法的算术表达式,如`2*3+8`。 +8. ☼ 写一个工具函数以 URL 为参数,返回删除所有的 HTML 标记的 URL 的内容。使用`from urllib import request`和`request.urlopen('http://nltk.org/').read().decode('utf8')`来访问 URL 的内容。 + +9. ☼ 将一些文字保存到文件`corpus.txt`。定义一个函数`load(f)`以要读取的文件名为其唯一参数,返回包含文件中文本的字符串。 + + 1. 使用`nltk.regexp_tokenize()`创建一个分词器分割这个文本中的各种标点符号。使用一个多行的正则表达式,使用 verbose 标志`(?x)`带有行内注释。 + 2. 使用`nltk.regexp_tokenize()`创建一个分词器分割以下几种表达式:货币金额;日期;个人和组织的名称。 +10. ☼ 将下面的循环改写为列表推导: + + ```py + >>> sent = ['The', 'dog', 'gave', 'John', 'the', 'newspaper'] + >>> result = [] + >>> for word in sent: + ... word_len = (word, len(word)) + ... result.append(word_len) + >>> result + [('The', 3), ('dog', 3), ('gave', 4), ('John', 4), ('the', 3), ('newspaper', 9)] + ``` + +11. ☼ 定义一个字符串`raw`包含你自己选择的句子。现在,以空格以外的其它字符例如`'s'`分割`raw`。 + +12. ☼ 写一个`for`循环输出一个字符串的字符,每行一个。 + +13. ☼ 在字符串上调用不带参数的`split`与以`' '`作为参数的区别是什么,即`sent.split()`与`sent.split(' ')`相比?当被分割的字符串包含制表符、连续的空格或一个制表符与空格的序列会发生什么?(在 IDLE 中你将需要使用`'\t'`来输入制表符。) + +14. ☼ 创建一个变量`words`,包含一个词列表。实验`words.sort()`和`sorted(words)`。它们有什么区别? + +15. ☼ 通过在 Python 提示符输入以下表达式,探索字符串和整数的区别:`"3" * 7`和`3 * 7`。尝试使用`int("3")`和`str(3)`进行字符串和整数之间的转换。 + +16. ☼ 使用文本编辑器创建一个文件`prog.py`,包含单独的一行`monty = 'Monty Python'`。接下来,打开一个新的 Python 会话,并在提示符下输入表达式`monty`。你会从解释器得到一个错误。现在,请尝试以下代码(注意你要丢弃文件名中的`.py`): + + ```py + >>> from prog import monty + >>> monty + ``` + + 这一次,Python 应该返回一个值。你也可以尝试`import prog`,在这种情况下,Python 应该能够处理提示符处的表达式`prog.monty`。 + +17. ☼ 格式化字符串`%6s`与`%-6s`用来显示长度大于 6 个字符的字符串时,会发生什么? + +18. ◑ 阅读语料库中的一些文字,为它们分词,输出其中出现的所有`wh-`类型词的列表。(英语中的`wh-`类型词被用在疑问句,关系从句和感叹句:`who, which, what`等。)按顺序输出它们。在这个列表中有因为有大小写或标点符号的存在而重复的词吗? + +19. ◑ 创建一个文件,包含词汇和(任意指定)频率,其中每行包含一个词,一个空格和一个正整数,如`fuzzy 53`。使用`open(filename).readlines()`将文件读入一个 Python 列表。接下来,使用`split()`将每一行分成两个字段,并使用`int()`将其中的数字转换为一个整数。结果应该是一个列表形式:`[['fuzzy', 53], ...]`。 + +20. ◑ 编写代码来访问喜爱的网页,并从中提取一些文字。例如,访问一个天气网站,提取你所在的城市今天的最高温度预报。 + +21. ◑ 写一个函数`unknown()`,以一个 URL 为参数,返回一个那个网页出现的未知词列表。为了做到这一点,请提取所有由小写字母组成的子字符串(使用`re.findall()`),并去除所有在 Words 语料库(`nltk.corpus.words`)中出现的项目。尝试手动分类这些词,并讨论你的发现。 + +22. ◑ 使用上面建议的正则表达式处理网址`http://news.bbc.co.uk/`,检查处理结果。你会看到那里仍然有相当数量的非文本数据,特别是 JavaScript 命令。你可能还会发现句子分割没有被妥善保留。定义更深入的正则表达式,改善此网页文本的提取。 + +23. ◑ 你能写一个正则表达式以这样的方式来分词吗,将词`don't`分为`do`和`n't`?解释为什么这个正则表达式无法正常工作:`n't|\w+`。 + +24. ◑ 尝试编写代码将文本转换成`hAck3r`,使用正则表达式和替换,其中`e` → `3`, `i` → `1`, `o` → `0`, `l` → `|`, `s` → `5`, `.`→ `5w33t!`, `ate` → `8`。在转换之前将文本规范化为小写。自己添加更多的替换。现在尝试将`s`映射到两个不同的值:词开头的`s`映射为` + +25. ◑ *Pig Latin* 是英语文本的一个简单的变换。文本中每个词的按如下方式变换:将出现在词首的所有辅音(或辅音群)移到词尾,然后添加`ay`,例如`string → ingstray`, `idle → idleay`。`http://en.wikipedia.org/wiki/Pig_Latin` + + 1. 写一个函数转换一个词为 Pig Latin。 + 2. 写代码转换文本而不是单个的词。 + 3. 进一步扩展它,保留大写字母,将`qu`保持在一起(例如这样`quiet`会变成`ietquay`),并检测`y`是作为一个辅音(如`yellow`)还是一个元音(如`style`)。 +26. ◑ 下载一种包含元音和谐的语言(如匈牙利语)的一些文本,提取词汇的元音序列,并创建一个元音二元语法表。 + +27. ◑ Python 的`random`模块包括函数`choice()`,它从一个序列中随机选择一个项目,例如`choice("aehh ")`会产生四种可能的字符中的一个,字母`h`的几率是其它字母的两倍。写一个表达式产生器,从字符串`"aehh "`产生 500 个随机选择的字母的序列,并将这个表达式写入函数`''.join()`调用中,将它们连接成一个长字符串。你得到的结果应该看起来像失去控制的喷嚏或狂笑:`he haha ee heheeh eha`。使用`split()`和`join()`再次规范化这个字符串中的空格。 + +28. ◑ 考虑下面的摘自 MedLine 语料库的句子中的数字表达式:`The corresponding free cortisol fractions in these sera were 4.53 +/- 0.15% and 8.16 +/- 0.23%, respectively.`。我们应该说数字表达式`4.53 +/- 0.15%`是三个词吗?或者我们应该说它是一个单独的复合词?或者我们应该说它实际上是*九*个词,因为它读作`four point five three,plus or minus fifteen percent`?或者我们应该说这不是一个“真正的”词,因为它不会出现在任何词典中?讨论这些不同的可能性。你能想出产生这些答案中至少两个以上可能性的应用领域吗? + +29. ◑ 可读性测量用于为一个文本的阅读难度打分,给语言学习者挑选适当难度的文本。在一个给定的文本中,让我们定义`μ[w]`为每个词的平均字母数,`μ[s]`为每个句子的平均词数。文本自动可读性指数(ARI)被定义为:`4.71 * μ[w] + 0.5 μ[s] - 21.43`。计算布朗语料库各部分的 ARI 得分,包括`f`(lore)和`j`(learned)部分。利用`nltk.corpus.brown.words()`产生一个词汇序列,`nltk.corpus.brown.sents()`产生一个句子的序列的事实。 + +30. ◑ 使用`Porter`词干提取器规范化一些已标注的文本,对每个词调用提取词干器。用`Lancaster`词干提取器做同样的事情,看看你是否能观察到一些差别。 + +31. ◑ 定义变量`saying`包含列表`['After', 'all', 'is', 'said', 'and', 'done', ',', 'more', 'is', 'said', 'than', 'done', '.']`。使用`for`循环处理这个列表,并将结果存储在一个新的链表`lengths`中。提示:使用`lengths = []`,从分配一个空列表给`lengths`开始。然后每次循环中用`append()`添加另一个长度值到列表中。现在使用列表推导做同样的事情。 + +32. ◑ 定义一个变量`silly`包含字符串:`'newly formed bland ideas are inexpressible in an infuriating way'`。(这碰巧是合法的解释,讲英语西班牙语双语者可以适用于乔姆斯基著名的无意义短语,`colorless green ideas sleep furiously`,来自维基百科)。编写代码执行以下任务: + + 1. 分割`silly`为一个字符串列表,每一个词一个字符串,使用 Python 的`split()`操作,并保存到叫做`bland`的变量中。 + 2. 提取`silly`中每个词的第二个字母,将它们连接成一个字符串,得到`'eoldrnnnna'`。 + 3. 使用`join()`将`bland`中的词组合回一个单独的字符串。确保结果字符串中的词以空格隔开。 + 4. 按字母顺序输出`silly`中的词,每行一个。 +33. ◑ `index()`函数可用于查找序列中的项目。例如,`'inexpressible'.index('e')`告诉我们字母`e`的第一个位置的索引值。 + + 1. 当你查找一个子字符串会发生什么,如`'inexpressible'.index('re')`? + 2. 定义一个变量`words`,包含一个词列表。现在使用`words.index()`来查找一个单独的词的位置。 + 3. 定义上一个练习中的变量`silly`。使用`index()`函数结合列表切片,建立一个包括`silly`中`in`之前(但不包括)的所有的词的列表`phrase`。 +34. ◑ 编写代码,将国家的形容词转换为它们对应的名词形式,如将`Canadian`和`Australian`转换为`Canada`和`Australia`(见`http://en.wikipedia.org/wiki/List_of_adjectival_forms_of_place_names`)。 + +35. ◑ 阅读 LanguageLog 中关于短语的`as best as p can`和`as best p can`形式的帖子,其中`p`是一个代名词。在一个语料库和 3.5 中描述的搜索已标注的文本的`findall()`方法的帮助下,调查这一现象。`http://itre.cis.upenn.edu/~myl/languagelog/archives/002733.html` + +36. ◑ 研究《创世记》的 lolcat 版本,使用`nltk.corpus.genesis.words('lolcat.txt')`可以访问,和`http://www.lolcatbible.com/index.php?title=How_to_speak_lolcat`上将文本转换为 lolspeak 的规则。定义正则表达式将英文词转换成相应的 lolspeak 词。 + +37. ◑ 使用`help(re.sub)`和参照本章的深入阅读,阅读有关`re.sub()`函数来使用正则表达式进行字符串替换。使用`re.sub`编写代码从一个 HTML 文件中删除 HTML 标记,规范化空格。 + +38. ★ 分词的一个有趣的挑战是已经被分割的跨行的词。例如如果`long-term`被分割,我们就得到字符串`long-\nterm`。 + + 1. 写一个正则表达式,识别连字符连结的跨行处的词汇。这个表达式将需要包含`\n`字符。 + 2. 使用`re.sub()`从这些词中删除`\n`字符。 + 3. 你如何确定一旦换行符被删除后不应该保留连字符的词汇,如`'encyclo-\npedia'`? +39. ★ 阅读维基百科 *Soundex* 条目。用 Python 实现这个算法。 + +40. ★ 获取两个或多个文体的原始文本,计算它们各自的在前面关于阅读难度的练习中描述的阅读难度得分。例如,比较 ABC 农村新闻和 ABC 科学新闻(`nltk.corpus.abc`)。使用`Punkt`处理句子分割。 + +41. ★ 将下面的嵌套循环重写为嵌套列表推导: + + ```py + >>> words = ['attribution', 'confabulation', 'elocution', + ... 'sequoia', 'tenacious', 'unidirectional'] + >>> vsequences = set() + >>> for word in words: + ... vowels = [] + ... for char in word: + ... if char in 'aeiou': + ... vowels.append(char) + ... vsequences.add(''.join(vowels)) + >>> sorted(vsequences) + ['aiuio', 'eaiou', 'eouio', 'euoia', 'oauaio', 'uiieioa'] + ``` + +42. ★ 使用 WordNet 为一个文本集合创建语义索引。扩展例 3.6 中的一致性搜索程序,使用它的第一个同义词集偏移索引每个词,例如`wn.synsets('dog')[0].offset`(或者使用上位词层次中的一些祖先的偏移,这是可选的)。 + +43. ★ 在多语言语料库如世界人权宣言语料库(`nltk.corpus.udhr`),和 NLTK 的频率分布和关系排序的功能(`nltk.FreqDist`, `nltk.spearman_correlation`)的帮助下,开发一个系统,猜测未知文本。为简单起见,使用一个单一的字符编码和少几种语言。 + +44. ★ 写一个程序处理文本,发现一个词以一种新的意义被使用的情况。对于每一个词计算这个词所有同义词集与这个词的上下文的所有同义词集之间的 WordNet 相似性。(请注意,这是一个粗略的办法;要做的很好是困难的,开放性研究问题。) + +45. ★ 阅读关于规范化非标准词的文章(Sproat et al, 2001),实现一个类似的文字规范系统。 + +## 关于本文档... + +针对 NLTK 3.0 作出更新。本章来自于《Python 自然语言处理》,[Steven Bird](http://estive.net/), [Ewan Klein](http://homepages.inf.ed.ac.uk/ewan/) 和 [Edward Loper](http://ed.loper.org/),Copyright © 2014 作者所有。本章依据 [*Creative Commons Attribution-Noncommercial-No Derivative Works 3\.0 United States License*](http://creativecommons.org/licenses/by-nc-nd/3.0/us/) 条款,与[*自然语言工具包*](http://nltk.org/) 3.0 版一起发行。 + +本文档构建于星期三 2015 年 7 月 1 日 12:30:05 AEST \ No newline at end of file diff --git a/docs/nlp-py-2e/4.md b/docs/nlp-py-2e/4.md new file mode 100644 index 0000000000000000000000000000000000000000..2009e9cc0ab2d738322ecbb3f317133110b37372 --- /dev/null +++ b/docs/nlp-py-2e/4.md @@ -0,0 +1,1477 @@ +# 4 编写结构化程序 + +现在,你对 Python 编程语言处理自然语言的能力已经有了体会。不过,如果你是 Python 或者编程新手,你可能仍然要努力对付 Python,而尚未感觉到你在完全控制它。在这一章中,我们将解决以下问题: + +1. 怎么能写出结构良好、可读的程序,你和其他人将能够很容易的重新使用它? +2. 基本结构块,如循环、函数以及赋值,是如何执行的? +3. Python 编程的陷阱有哪些,你怎么能避免它们吗? + +一路上,你将巩固基本编程结构的知识,了解更多关于以一种自然和简洁的方式使用 Python 语言特征的内容,并学习一些有用的自然语言数据可视化技术。如前所述,本章包含许多例子和练习(和以前一样,一些练习会引入新材料)。编程新手的读者应仔细做完它们,并在需要时查询其他编程介绍;有经验的程序员可以快速浏览本章。 + +在这本书的其他章节中,为了讲述 NLP 的需要,我们已经组织了一些编程的概念。在这里,我们回到一个更传统的方法,材料更紧密的与编程语言的结构联系在一起。这里不会完整的讲述编程语言,我们只关注对 NLP 最重要的语言结构和习惯用法。 + +## 4.1 回到基础 + +### 赋值 + +赋值似乎是最基本的编程概念,不值得单独讨论。不过,也有一些令人吃惊的微妙之处。思考下面的代码片段: + +```py +>>> foo = 'Monty' +>>> bar = foo ❶ +>>> foo = 'Python' ❷ +>>> bar +'Monty' +``` + +这个结果与预期的完全一样。当我们在上面的代码中写`bar = foo`时❶,`foo`的值(字符串`'Monty'`)被赋值给`bar`。也就是说,`bar`是`foo`的一个副本,所以当我们在第❷行用一个新的字符串`'Python'`覆盖`foo`时,`bar`的值不会受到影响。 + +然而,赋值语句并不总是以这种方式复制副本。赋值总是一个表达式的值的复制,但值并不总是你可能希望的那样。特别是结构化对象的“值”,例如一个列表,实际上是一个对象的引用。在下面的例子中,❶将`foo`的引用分配给新的变量`bar`。现在,当我们在❷行修改`foo`内的东西,我们可以看到`bar`的内容也已改变。 + +```py +>>> foo = ['Monty', 'Python'] +>>> bar = foo ❶ +>>> foo[1] = 'Bodkin' ❷ +>>> bar +['Monty', 'Bodkin'] +``` + +![Images/array-memory.png](Images/64864d38550248d5bd9b82eeb6f0583b.jpg) + +图 4.1:列表赋值与计算机内存:两个列表对象`foo`和`bar`引用计算机内存中的相同的位置;更新`foo`将同样修改`bar`,反之亦然。 + +`bar = foo`❶行并不会复制变量的内容,只有它的“引用对象”。要了解这里发生了什么事,我们需要知道列表是如何存储在计算机内存的。在 4.1 中,我们看到一个列表`foo`是对存储在位置 3133 处的一个对象的引用(它自身是一个指针序列,其中的指针指向其它保存字符串的位置)。当我们赋值`bar = foo`时,仅仅是 3133 位置处的引用被复制。这种行为延伸到语言的其他方面,如参数传递`(4.4)`。 + +让我们做更多的实验,通过创建一个持有空列表的变量`empty`,然后在下一行使用它三次。 + +```py +>>> empty = [] +>>> nested = [empty, empty, empty] +>>> nested +[[], [], []] +>>> nested[1].append('Python') +>>> nested +[['Python'], ['Python'], ['Python']] +``` + +请看,改变列表中嵌套列表内的一个项目,它们全改变了。这是因为三个元素中的每一个实际上都只是一个内存中的同一列表的引用。 + +注意 + +**轮到你来**:用乘法创建一个列表的列表:`nested = [[]] * 3`。现在修改列表中的一个元素,观察所有的元素都改变了。使用 Python 的`id()`函数找出任一对象的数字标识符, 并验证`id(nested[0])`,`id(nested[1])`与`id(nested[2])`是一样的。 + +现在请注意,当我们分配一个新值给列表中的一个元素时,它并不会传送给其他元素: + +```py +>>> nested = [[]] * 3 +>>> nested[1].append('Python') +>>> nested[1] = ['Monty'] +>>> nested +[['Python'], ['Monty'], ['Python']] +``` + +我们一开始用含有 3 个引用的列表,每个引用指向一个空列表对象。然后,我们通过给它追加`'Python'`修改这个对象,结果变成包含 3 个到一个列表对象`['Python']`的引用的列表。下一步,我们使用到一个新对象`['Monty']`的引用来*覆盖*三个元素中的一个。这最后一步修改嵌套列表内的 3 个对象引用中的 1 个。然而,`['Python']`对象并没有改变,仍然是在我们的嵌套列表的列表中的两个位置被引用。关键是要明白通过一个对象引用修改一个对象与通过覆盖一个对象引用之间的区别。 + +注意 + +**重要**:要从列表`foo`复制项目到一个新的列表`bar`,你可以写`bar = foo[:]`。这会复制列表中的对象引用。若要复制结构而不复制任何对象引用,请使用`copy.deepcopy()`。 + +### 等式 + +Python 提供两种方法来检查一对项目是否相同。`is`操作符测试对象的 ID。我们可以用它来验证我们早先的对对象的观察。首先,我们创建一个列表,其中包含同一对象的多个副本,证明它们不仅对于`==`完全相同,而且它们是同一个对象: + +```py +>>> size = 5 +>>> python = ['Python'] +>>> snake_nest = [python] * size +>>> snake_nest[0] == snake_nest[1] == snake_nest[2] == snake_nest[3] == snake_nest[4] +True +>>> snake_nest[0] is snake_nest[1] is snake_nest[2] is snake_nest[3] is snake_nest[4] +True +``` + +现在,让我们将一个新的 python 放入嵌套中。我们可以很容易地表明这些对象不完全相同: + +```py +>>> import random +>>> position = random.choice(range(size)) +>>> snake_nest[position] = ['Python'] +>>> snake_nest +[['Python'], ['Python'], ['Python'], ['Python'], ['Python']] +>>> snake_nest[0] == snake_nest[1] == snake_nest[2] == snake_nest[3] == snake_nest[4] +True +>>> snake_nest[0] is snake_nest[1] is snake_nest[2] is snake_nest[3] is snake_nest[4] +False +``` + +你可以再做几对测试,发现哪个位置包含闯入者,函数`id()`使检测更加容易: + +```py +>>> [id(snake) for snake in snake_nest] +[4557855488, 4557854763, 4557855488, 4557855488, 4557855488] +``` + +这表明列表中的第二个项目有一个独特的标识符。如果你尝试自己运行这段代码,请期望看到结果列表中的不同数字,以及闯入者可能在不同的位置。 + +有两种等式可能看上去有些奇怪。然而,这真的只是类型与标识符式的区别,与自然语言相似,这里在一种编程语言中呈现出来。 + +### 条件 + +在`if`语句的条件部分,一个非空字符串或列表被求值为真,而一个空字符串或列表的被求值为假。 + +```py +>>> mixed = ['cat', '', ['dog'], []] +>>> for element in mixed: +... if element: +... print(element) +... +cat +['dog'] +``` + +也就是说,我们*不必*在条件中写`if len(element) > 0:`。 + +使用`if...elif`而不是在一行中使用两个`if`语句有什么区别?嗯,考虑以下情况: + +```py +>>> animals = ['cat', 'dog'] +>>> if 'cat' in animals: +... print(1) +... elif 'dog' in animals: +... print(2) +... +1 +``` + +因为表达式中`if`子句条件满足,Python 就不会求值`elif`子句,所以我们永远不会得到输出`2`。相反,如果我们用一个`if`替换`elif`,那么我们将会输出`1`和`2`。所以`elif`子句比单独的`if`子句潜在地给我们更多信息;当它被判定为真时,告诉我们不仅条件满足而且前面的`if`子句条件*不*满足。 + +`all()`函数和`any()`函数可以应用到一个列表(或其他序列),来检查是否全部或任一项目满足某个条件: + +```py +>>> sent = ['No', 'good', 'fish', 'goes', 'anywhere', 'without', 'a', 'porpoise', '.'] +>>> all(len(w) > 4 for w in sent) +False +>>> any(len(w) > 4 for w in sent) +True +``` + +## 4.2 序列 + +到目前为止,我们已经看到了两种序列对象:字符串和列表。另一种序列被称为元组。元组由逗号操作符❶构造,而且通常使用括号括起来。实际上,我们已经在前面的章节中看到过它们,它们有时也被称为“配对”,因为总是有两名成员。然而,元组可以有任何数目的成员。与列表和字符串一样,元组可以被索引❷和切片❸,并有长度❹。 + +```py +>>> t = 'walk', 'fem', 3 ❶ +>>> t +('walk', 'fem', 3) +>>> t[0] ❷ +'walk' +>>> t[1:] ❸ +('fem', 3) +>>> len(t) ❹ +3 +``` + +小心! + +元组使用逗号操作符来构造。括号是一个 Python 语法的一般功能,设计用于分组。定义一个包含单个元素`'snark'`的元组是通过添加一个尾随的逗号,像这样:`'snark',`。空元组是一个特殊的情况下,使用空括号`()`定义。 + +让我们直接比较字符串、列表和元组,在各个类型上做索引、切片和长度操作: + +```py +>>> raw = 'I turned off the spectroroute' +>>> text = ['I', 'turned', 'off', 'the', 'spectroroute'] +>>> pair = (6, 'turned') +>>> raw[2], text[3], pair[1] +('t', 'the', 'turned') +>>> raw[-3:], text[-3:], pair[-3:] +('ute', ['off', 'the', 'spectroroute'], (6, 'turned')) +>>> len(raw), len(text), len(pair) +(29, 5, 2) +``` + +请注意在此代码示例中,我们在一行代码中计算多个值,中间用逗号分隔。这些用逗号分隔的表达式其实就是元组——如果没有歧义,Python 允许我们忽略元组周围的括号。当我们输出一个元组时,括号始终显示。通过以这种方式使用元组,我们隐式的将这些项目聚集在一起。 + +### 序列类型上的操作 + +我们可以用多种有用的方式遍历一个序列`s`中的项目,如 4.1 所示。 + +表 4.1: + +遍历序列的各种方式 + +```py +>>> raw = 'Red lorry, yellow lorry, red lorry, yellow lorry.' +>>> text = word_tokenize(raw) +>>> fdist = nltk.FreqDist(text) +>>> sorted(fdist) +[',', '.', 'Red', 'lorry', 'red', 'yellow'] +>>> for key in fdist: +... print(key + ':', fdist[key], end='; ') +... +lorry: 4; red: 1; .: 1; ,: 3; Red: 1; yellow: 2 +``` + +在接下来的例子中,我们使用元组重新安排我们的列表中的内容。(可以省略括号,因为逗号比赋值的优先级更高。) + +```py +>>> words = ['I', 'turned', 'off', 'the', 'spectroroute'] +>>> words[2], words[3], words[4] = words[3], words[4], words[2] +>>> words +['I', 'turned', 'the', 'spectroroute', 'off'] +``` + +这是一种地道和可读的移动列表内的项目的方式。它相当于下面的传统方式不使用元组做上述任务(注意这种方法需要一个临时变量`tmp`)。 + +```py +>>> tmp = words[2] +>>> words[2] = words[3] +>>> words[3] = words[4] +>>> words[4] = tmp +``` + +正如我们已经看到的,Python 有序列处理函数,如`sorted()`和`reversed()`,它们重新排列序列中的项目。也有修改序列结构的函数,可以很方便的处理语言。因此,`zip()`接收两个或两个以上的序列中的项目,将它们“压缩”打包成单个的配对列表。给定一个序列`s`,`enumerate(s)`返回一个包含索引和索引处项目的配对。 + +```py +>>> words = ['I', 'turned', 'off', 'the', 'spectroroute'] +>>> tags = ['noun', 'verb', 'prep', 'det', 'noun'] +>>> zip(words, tags) + +>>> list(zip(words, tags)) +[('I', 'noun'), ('turned', 'verb'), ('off', 'prep'), +('the', 'det'), ('spectroroute', 'noun')] +>>> list(enumerate(words)) +[(0, 'I'), (1, 'turned'), (2, 'off'), (3, 'the'), (4, 'spectroroute')] +``` + +注意 + +只在需要的时候进行计算(或者叫做“惰性计算”特性),这是 Python 3 和 NLTK 3 的一个普遍特点。当你期望看到一个序列时,如果你看到的却是类似``这样的结果, 你可以强制求值这个对象,只要把它放在一个期望序列的上下文中,比如`list(x)`或`for item in x`。 + +对于一些 NLP 任务,有必要将一个序列分割成两个或两个以上的部分。例如,我们可能需要用 90% 的数据来“训练”一个系统,剩余 10% 进行测试。要做到这一点,我们指定想要分割数据的位置❶,然后在这个位置分割序列❷。 + +```py +>>> text = nltk.corpus.nps_chat.words() +>>> cut = int(0.9 * len(text)) ❶ +>>> training_data, test_data = text[:cut], text[cut:] ❷ +>>> text == training_data + test_data ❸ +True +>>> len(training_data) / len(test_data) ❹ +9.0 +``` + +我们可以验证在此过程中的原始数据没有丢失,也没有重复❸。我们也可以验证两块大小的比例是我们预期的❹。 + +### 合并不同类型的序列 + +让我们综合关于这三种类型的序列的知识,一起使用列表推导处理一个字符串中的词,按它们的长度排序。 + +```py +>>> words = 'I turned off the spectroroute'.split() ❶ +>>> wordlens = [(len(word), word) for word in words] ❷ +>>> wordlens.sort() ❸ +>>> ' '.join(w for (_, w) in wordlens) ❹ +'I off the turned spectroroute' +``` + +上述代码段中每一行都包含一个显著的特征。一个简单的字符串实际上是一个其上定义了方法如`split()`❶的对象。我们使用列表推导建立一个元组的列表❷,其中每个元组由一个数字(词长)和这个词组成,例如`(3, 'the')`。我们使用`sort()`方法❸就地排序列表。最后,丢弃长度信息,并将这些词连接回一个字符串❹。(下划线❹只是一个普通的 Python 变量,我们约定可以用下划线表示我们不会使用其值的变量。) + +我们开始谈论这些序列类型的共性,但上面的代码说明了这些序列类型的重要的区别。首先,字符串出现在开头和结尾:这是很典型的,我们的程序先读一些文本,最后产生输出给我们看。列表和元组在中间,但使用的目的不同。一个链表是一个典型的具有相同类型的对象的序列,它的长度是任意的。我们经常使用列表保存词序列。相反,一个元组通常是不同类型的对象的集合,长度固定。我们经常使用一个元组来保存一个纪录,与一些实体相关的不同字段的集合。使用列表与使用元组之间的区别需要一些时间来习惯,所以这里是另一个例子: + +```py +>>> lexicon = [ +... ('the', 'det', ['Di:', 'D@']), +... ('off', 'prep', ['Qf', 'O:f']) +... ] +``` + +在这里,用一个列表表示词典,因为它是一个单一类型的对象的集合——词汇条目——没有预定的长度。个别条目被表示为一个元组,因为它是一个有不同的解释的对象的集合,例如正确的拼写形式、词性、发音(以 SAMPA 计算机可读的拼音字母表示,`http://www.phon.ucl.ac.uk/home/sampa/`)。请注意,这些发音都是用列表存储的。(为什么呢?) + +注意 + +决定何时使用元组还是列表的一个好办法是看一个项目的内容是否取决与它的位置。例如,一个已标注的词标识符由两个具有不同解释的字符串组成,我们选择解释第一项为词标识符,第二项为标注。因此,我们使用这样的元组:`('grail', 'noun')`;一个形式为`('noun', 'grail')`的元组将是无意义的,因为这将是一个词`noun`被标注为`grail`。相反,一个文本中的元素都是词符, 位置并不重要。因此, 我们使用这样的列表:`['venetian', 'blind']`;一个形式为`['blind', 'venetian']`的列表也同样有效。词的语言学意义可能会有所不同,但作为词符的列表项的解释是不变的。 + +列表和元组之间的使用上的区别已经讲过了。然而,还有一个更加基本的区别:在 Python 中,列表是可变的,而元组是不可变的。换句话说,列表可以被修改,而元组不能。这里是一些在列表上的操作,就地修改一个列表。 + +```py +>>> lexicon.sort() +>>> lexicon[1] = ('turned', 'VBD', ['t3:nd', 't3`nd']) +>>> del lexicon[0] +``` + +注意 + +**轮到你来**:使用`lexicon = tuple(lexicon)`将`lexicon`转换为一个元组,然后尝试上述操作,确认它们都不能运用在元组上。 + +### 生成器表达式 + +我们一直在大量使用列表推导,因为用它处理文本结构紧凑和可读性好。下面是一个例子,分词和规范化一个文本: + +```py +>>> text = '''"When I use a word," Humpty Dumpty said in rather a scornful tone, +... "it means just what I choose it to mean - neither more nor less."''' +>>> [w.lower() for w in word_tokenize(text)] +['``', 'when', 'i', 'use', 'a', 'word', ',', "''", 'humpty', 'dumpty', 'said', ...] +``` + +假设我们现在想要进一步处理这些词。我们可以将上面的表达式插入到一些其他函数的调用中❶,Python 允许我们省略方括号❷。 + +```py +>>> max([w.lower() for w in word_tokenize(text)]) ❶ +'word' +>>> max(w.lower() for w in word_tokenize(text)) ❷ +'word' +``` + +第二行使用了生成器表达式。这不仅仅是标记方便:在许多语言处理的案例中,生成器表达式会更高效。在❶中,列表对象的存储空间必须在`max()`的值被计算之前分配。如果文本非常大的,这将会很慢。在❷中,数据流向调用它的函数。由于调用的函数只是简单的要找最大值——按字典顺序排在最后的词——它可以处理数据流,而无需存储迄今为止的最大值以外的任何值。 + +## 4.3 风格的问题 + +编程是作为一门科学的艺术。无可争议的程序设计的“圣经”,Donald Knuth 的 2500 页的多卷作品,叫做《计算机程序设计艺术》。已经有许多书籍是关于文学化编程的,它们认为人类,不只是电脑,必须阅读和理解程序。在这里,我们挑选了一些编程风格的问题,它们对你的代码的可读性,包括代码布局、程序与声明的风格、使用循环变量都有重要的影响。 + +### Python 代码风格 + +编写程序时,你会做许多微妙的选择:名称、间距、注释等等。当你在看别人编写的代码时,风格上的不必要的差异使其难以理解。因此,Python 语言的设计者发表了 Python 代码风格指南,http`http://www.python.org/dev/peps/pep-0008/`。风格指南中提出的基本价值是一致性,目的是最大限度地提高代码的可读性。我们在这里简要回顾一下它的一些主要建议,并请读者阅读完整的指南,里面有对实例的详细的讨论。 + +代码布局中每个缩进级别应使用 4 个空格。你应该确保当你在一个文件中写 Python 代码时,避免使用`tab`缩进,因为它可能由于不同的文本编辑器的不同解释而产生混乱。每行应少于 80 个字符长;如果必要的话,你可以在圆括号、方括号或花括号内换行,因为 Python 能够探测到该行与下一行是连续的。如果你需要在圆括号、方括号或大括号中换行,通常可以添加额外的括号,也可以在行尾需要换行的地方添加一个反斜杠: + +```py +>>> if (len(syllables) > 4 and len(syllables[2]) == 3 and +... syllables[2][2] in [aeiou] and syllables[2][3] == syllables[1][3]): +... process(syllables) +>>> if len(syllables) > 4 and len(syllables[2]) == 3 and \ +... syllables[2][2] in [aeiou] and syllables[2][3] == syllables[1][3]: +... process(syllables) +``` + +注意 + +键入空格来代替制表符很快就会成为一件苦差事。许多程序编辑器内置对 Python 的支持,能自动缩进代码,突出任何语法错误(包括缩进错误)。关于 Python 编辑器列表,请见`http://wiki.python.org/moin/PythonEditors`。 + +### 过程风格与声明风格 + +我们刚才已经看到可以不同的方式执行相同的任务,其中蕴含着对执行效率的影响。另一个影响程序开发的因素是*编程风格*。思考下面的计算布朗语料库中词的平均长度的程序: + +```py +>>> tokens = nltk.corpus.brown.words(categories='news') +>>> count = 0 +>>> total = 0 +>>> for token in tokens: +... count += 1 +... total += len(token) +>>> total / count +4.401545438271973 +``` + +在这段程序中,我们使用变量`count`跟踪遇到的词符的数量,`total`储存所有词的长度的总和。这是一个低级别的风格,与机器代码,即计算机的 CPU 所执行的基本操作,相差不远。两个变量就像 CPU 的两个寄存器,积累许多中间环节产生的值,和直到最才有意义的值。我们说,这段程序是以*过程*风格编写,一步一步口授机器操作。现在,考虑下面的程序,计算同样的事情: + +```py +>>> total = sum(len(t) for t in tokens) +>>> print(total / len(tokens)) +4.401... +``` + +第一行使用生成器表达式累加标示符的长度,第二行像前面一样计算平均值。每行代码执行一个完整的、有意义的工作,可以高级别的属性,如:“`total`是标识符长度的总和”,的方式来理解。实施细节留给 Python 解释器。第二段程序使用内置函数,在一个更抽象的层面构成程序;生成的代码是可读性更好。让我们看一个极端的例子: + +```py +>>> word_list = [] +>>> i = 0 +>>> while i < len(tokens): +... j = 0 +... while j < len(word_list) and word_list[j] <= tokens[i]: +... j += 1 +... if j == 0 or tokens[i] != word_list[j-1]: +... word_list.insert(j, tokens[i]) +... i += 1 +... +``` + +等效的声明版本使用熟悉的内置函数,可以立即知道代码的目的: + +```py +>>> word_list = sorted(set(tokens)) +``` + +另一种情况,对于每行输出一个计数值,一个循环计数器似乎是必要的。然而,我们可以使用`enumerate()`处理序列`s`,为`s`中每个项目产生一个`(i, s[i])`形式的元组,以`(0, s[0])`开始。下面我们枚举频率分布的值,生成嵌套的`(rank, (word, count))`元组。按照产生排序项列表时的需要,输出`rank+1`使计数从`1`开始。 + +```py +>>> fd = nltk.FreqDist(nltk.corpus.brown.words()) +>>> cumulative = 0.0 +>>> most_common_words = [word for (word, count) in fd.most_common()] +>>> for rank, word in enumerate(most_common_words): +... cumulative += fd.freq(word) +... print("%3d %6.2f%% %s" % (rank + 1, cumulative * 100, word)) +... if cumulative > 0.25: +... break +... + 1 5.40% the + 2 10.42% , + 3 14.67% . + 4 17.78% of + 5 20.19% and + 6 22.40% to + 7 24.29% a + 8 25.97% in +``` + +到目前为止,使用循环变量存储最大值或最小值,有时很诱人。让我们用这种方法找出文本中最长的词。 + +```py +>>> text = nltk.corpus.gutenberg.words('milton-paradise.txt') +>>> longest = '' +>>> for word in text: +... if len(word) > len(longest): +... longest = word +>>> longest +'unextinguishable' +``` + +然而,一个更加清楚的解决方案是使用两个列表推导,它们的形式现在应该很熟悉: + +```py +>>> maxlen = max(len(word) for word in text) +>>> [word for word in text if len(word) == maxlen] +['unextinguishable', 'transubstantiate', 'inextinguishable', 'incomprehensible'] +``` + +请注意,我们的第一个解决方案找到第一个长度最长的词,而第二种方案找到*所有*最长的词(通常是我们想要的)。虽然有两个解决方案之间的理论效率的差异,主要的开销是到内存中读取数据;一旦数据准备好,第二阶段处理数据可以瞬间高效完成。我们还需要平衡我们对程序的效率与程序员的效率的关注。一种快速但神秘的解决方案将是更难理解和维护的。 + +### 计数器的一些合理用途 + +在有些情况下,我们仍然要在列表推导中使用循环变量。例如:我们需要使用一个循环变量中提取列表中连续重叠的 N 元组: + +```py +>>> sent = ['The', 'dog', 'gave', 'John', 'the', 'newspaper'] +>>> n = 3 +>>> [sent[i:i+n] for i in range(len(sent)-n+1)] +[['The', 'dog', 'gave'], + ['dog', 'gave', 'John'], + ['gave', 'John', 'the'], + ['John', 'the', 'newspaper']] +``` + +确保循环变量范围的正确相当棘手的。因为这是 NLP 中的常见操作,NLTK 提供了支持函数`bigrams(text)`、`trigrams(text)`和一个更通用的`ngrams(text, n)`。 + +下面是我们如何使用循环变量构建多维结构的一个例子。例如,建立一个`m`行`n`列的数组,其中每个元素是一个集合,我们可以使用一个嵌套的列表推导: + +```py +>>> m, n = 3, 7 +>>> array = [[set() for i in range(n)] for j in range(m)] +>>> array[2][5].add('Alice') +>>> pprint.pprint(array) +[[set(), set(), set(), set(), set(), set(), set()], + [set(), set(), set(), set(), set(), set(), set()], + [set(), set(), set(), set(), set(), {'Alice'}, set()]] +``` + +请看循环变量`i`和`j`在产生对象过程中没有用到,它们只是需要一个语法正确的`for`语句。这种用法的另一个例子,请看表达式`['very' for i in range(3)]`产生一个包含三个`'very'`实例的列表,没有整数。 + +请注意,由于我们前面所讨论的有关对象复制的原因,使用乘法做这项工作是不正确的。 + +```py +>>> array = [[set()] * n] * m +>>> array[2][5].add(7) +>>> pprint.pprint(array) +[[{7}, {7}, {7}, {7}, {7}, {7}, {7}], + [{7}, {7}, {7}, {7}, {7}, {7}, {7}], + [{7}, {7}, {7}, {7}, {7}, {7}, {7}]] +``` + +迭代是一个重要的编程概念。采取其他语言中的习惯用法是很诱人的。然而, Python 提供一些优雅和高度可读的替代品,正如我们已经看到。 + +## 4.4 函数:结构化编程的基础 + +函数提供了程序代码打包和重用的有效途径,已经在 3 中解释过。例如,假设我们发现我们经常要从 HTML 文件读取文本。这包括以下几个步骤,打开文件,将它读入,规范化空白符号,剥离 HTML 标记。我们可以将这些步骤收集到一个函数中,并给它一个名字,如`get_text()`,如 4.2 所示。 + +```py +import re +def get_text(file): + """Read text from a file, normalizing whitespace and stripping HTML markup.""" + text = open(file).read() + text = re.sub(r'<.*?>', ' ', text) + text = re.sub('\s+', ' ', text) + return text +``` + +现在,任何时候我们想从一个 HTML 文件得到干净的文字,都可以用文件的名字作为唯一的参数调用`get_text()`。它会返回一个字符串,我们可以将它指定给一个变量,例如:`contents = get_text("test.html")`。每次我们要使用这一系列的步骤,只需要调用这个函数。 + +使用函数可以为我们的程序节约空间。更重要的是,我们为函数选择名称可以提高程序*可读性*。在上面的例子中,只要我们的程序需要从文件读取干净的文本,我们不必弄乱这四行代码的程序,只需要调用`get_text()`。这种命名方式有助于提供一些“语义解释”——它可以帮助我们的程序的读者理解程序的“意思”。 + +请注意,上面的函数定义包含一个字符串。函数定义内的第一个字符串被称为文档字符串。它不仅为阅读代码的人记录函数的功能,从文件加载这段代码的程序员也能够访问: + +```py +| >>> help(get_text) +| Help on function get_text in module __main__: +| +| get(text) +| Read text from a file, normalizing whitespace and stripping HTML markup. + +``` + +我们首先定义函数的两个参数,`msg`和`num`❶。然后调用函数,并传递给它两个参数,`monty`和`3`❷;这些参数填补了参数提供的“占位符”,为函数体中出现的`msg`和`num`提供值。 + +我们看到在下面的例子中不需要有任何参数: + +```py +>>> def monty(): +... return "Monty Python" +>>> monty() +'Monty Python' +``` + +函数通常会通过`return`语句将其结果返回给调用它的程序,正如我们刚才看到的。对于调用程序,它看起来就像函数调用已被函数结果替代,例如: + +```py +>>> repeat(monty(), 3) +'Monty Python Monty Python Monty Python' +>>> repeat('Monty Python', 3) +'Monty Python Monty Python Monty Python' +``` + +一个 Python 函数并不是一定需要有一个`return`语句。有些函数做它们的工作的同时会附带输出结果、修改文件或者更新参数的内容。(这种函数在其他一些编程语言中被称为“过程”)。 + +考虑以下三个排序函数。第三个是危险的,因为程序员可能没有意识到它已经修改了给它的输入。一般情况下,函数应该修改参数的内容(`my_sort1()`)或返回一个值(`my_sort2()`),而不是两个都做(`my_sort3()`)。 + +```py +>>> def my_sort1(mylist): # good: modifies its argument, no return value +... mylist.sort() +>>> def my_sort2(mylist): # good: doesn't touch its argument, returns value +... return sorted(mylist) +>>> def my_sort3(mylist): # bad: modifies its argument and also returns it +... mylist.sort() +... return mylist +``` + +### 参数传递 + +早在 4.1 节中,你就已经看到了赋值操作,而一个结构化对象的值是该对象的引用。函数也是一样的。Python 按它的值来解释函数的参数(这被称为按值调用)。在下面的代码中,`set_up()`有两个参数,都在函数内部被修改。我们一开始将一个空字符串分配给`w`,将一个空列表分配给`p`。调用该函数后,`w`没有变,而`p`改变了: + +```py +>>> def set_up(word, properties): +... word = 'lolcat' +... properties.append('noun') +... properties = 5 +... +>>> w = '' +>>> p = [] +>>> set_up(w, p) +>>> w +'' +>>> p +['noun'] +``` + +请注意,`w`没有被函数改变。当我们调用`set_up(w, p)`时,`w`(空字符串)的值被分配到一个新的变量`word`。在函数内部`word`值被修改。然而,这种变化并没有传播给`w`。这个参数传递过程与下面的赋值序列是一样的: + +```py +>>> w = '' +>>> word = w +>>> word = 'lolcat' +>>> w +'' +``` + +让我们来看看列表`p`上发生了什么。当我们调用`set_up(w, p)`,`p`的值(一个空列表的引用)被分配到一个新的本地变量`properties`,所以现在这两个变量引用相同的内存位置。函数修改`properties`,而这种变化也反映在`p`值上,正如我们所看到的。函数也分配给`properties`一个新的值(数字`5`);这并不能修改该内存位置上的内容,而是创建了一个新的局部变量。这种行为就好像是我们做了下列赋值序列: + +```py +>>> p = [] +>>> properties = p +>>> properties.append('noun') +>>> properties = 5 +>>> p +['noun'] +``` + +因此,要理解 Python 按值传递参数,只要了解它是如何赋值的就足够了。记住,你可以使用`id()`函数和`is`操作符来检查每个语句执行之后你对对象标识符的理解。 + +### 变量的作用域 + +函数定义为变量创建了一个新的局部的作用域。当你在函数体内部分配一个新的变量时,这个名字只在该函数内部被定义。函数体外或者在其它函数体内,这个名字是不可见的。这一行为意味着你可以选择变量名而不必担心它与你的其他函数定义中使用的名称冲突。 + +当你在一个函数体内部使用一个现有的名字时,Python 解释器先尝试按照函数本地的名字来解释。如果没有发现,解释器检查它是否是一个模块内的全局名称。最后,如果没有成功,解释器会检查是否是 Python 内置的名字。这就是所谓的名称解析的 LGB 规则:本地(local),全局(global),然后内置(built-in)。 + +小心! + +一个函数可以使用`global`声明创建一个新的全局变量。然而,这种做法应尽可能避免。在函数内部定义全局变量会导致上下文依赖性而限制函数的可移植性(或重用性)。一般来说,你应该使用参数作为函数的输入,返回值作为函数的输出。 + +### 参数类型检查 + +我们写程序时,Python 不会强迫我们声明变量的类型,这允许我们定义参数类型灵活的函数。例如,我们可能希望一个标注只是一个词序列,而不管这个序列被表示为一个列表、元组(或是迭代器,一种新的序列类型,超出了当前的讨论范围)。 + +然而,我们常常想写一些能被他人利用的程序,并希望以一种防守的风格,当函数没有被正确调用时提供有益的警告。下面的`tag()`函数的作者假设其参数将始终是一个字符串。 + +```py +>>> def tag(word): +... if word in ['a', 'the', 'all']: +... return 'det' +... else: +... return 'noun' +... +>>> tag('the') +'det' +>>> tag('knight') +'noun' +>>> tag(["'Tis", 'but', 'a', 'scratch']) ❶ +'noun' +``` + +该函数对参数`'the'`和`'knight'`返回合理的值,传递给它一个列表❶,看看会发生什么——它没有抱怨,虽然它返回的结果显然是不正确的。此函数的作者可以采取一些额外的步骤来确保`tag()`函数的参数`word`是一个字符串。一种直白的做法是使用`if not type(word) is str`检查参数的类型,如果`word`不是一个字符串,简单地返回 Python 特殊的空值`None`。这是一个略微的改善,因为该函数在检查参数类型,并试图对错误的输入返回一个“特殊的”诊断结果。然而,它也是危险的,因为调用程序可能不会检测`None`是故意设定的“特殊”值,这种诊断的返回值可能被传播到程序的其他部分产生不可预测的后果。如果这个词是一个 Unicode 字符串这种方法也会失败。因为它的类型是`unicode`而不是`str`。这里有一个更好的解决方案,使用`assert`语句和 Python 的`basestring`的类型一起,它是`unicode`和`str`的共同类型。 + +```py +>>> def tag(word): +... assert isinstance(word, basestring), "argument to tag() must be a string" +... if word in ['a', 'the', 'all']: +... return 'det' +... else: +... return 'noun' +``` + +如果`assert`语句失败,它会产生一个不可忽视的错误而停止程序执行。此外,该错误信息是容易理解的。程序中添加断言能帮助你找到逻辑错误,是一种防御性编程。一个更根本的方法是在本节后面描述的使用文档字符串为每个函数记录参数的文档。 + +### 功能分解 + +结构良好的程序通常都广泛使用函数。当一个程序代码块增长到超过 10-20 行,如果将代码分成一个或多个函数,每一个有明确的目的,这将对可读性有很大的帮助。这类似于好文章被划分成段,每段话表示一个主要思想。 + +函数提供了一种重要的抽象。它们让我们将多个动作组合成一个单一的复杂的行动,并给它关联一个名称。(比较我们组合动作“去”和“带回”为一个单一的更复杂的动作“取回”。)当我们使用函数时,主程序可以在一个更高的抽象水平编写,使其结构更透明,例如 + +```py +>>> data = load_corpus() +>>> results = analyze(data) +>>> present(results) +``` + +适当使用函数使程序更具可读性和可维护性。另外,重新实现一个函数已成为可能——使用更高效的代码替换函数体——不需要关心程序的其余部分。 + +思考 4.3 中`freq_words`函数。它更新一个作为参数传递进来的频率分布的内容,并输出前`n`个最频繁的词的列表。 + +```py +from urllib import request +from bs4 import BeautifulSoup + +def freq_words(url, freqdist, n): + html = request.urlopen(url).read().decode('utf8') + raw = BeautifulSoup(html).get_text() + for word in word_tokenize(raw): + freqdist[word.lower()] += 1 + result = [] + for word, count in freqdist.most_common(n): + result = result + [word] + print(result) +``` + +这个函数有几个问题。该函数有两个副作用:它修改了第二个参数的内容,并输出它已计算的结果的经过选择的子集。如果我们在函数内部初始化`FreqDist()`对象(在它被处理的同一个地方),并且去掉选择集而将结果显示给调用程序的话,函数会更容易理解和更容易在其他地方重用。考虑到它的任务是找出频繁的一个词,它应该只应该返回一个列表,而不是整个频率分布。在 4.4 中,我们重构此函数,并通过去掉`freqdist`参数简化其接口。 + +```py +from urllib import request +from bs4 import BeautifulSoup + +def freq_words(url, n): + html = request.urlopen(url).read().decode('utf8') + text = BeautifulSoup(html).get_text() + freqdist = nltk.FreqDist(word.lower() for word in word_tokenize(text)) + return [word for (word, _) in fd.most_common(n)] +``` + +`freq_words`函数的可读性和可用性得到改进。 + +注意 + +我们将`_`用作变量名。这是对任何其他变量没有什么不同,除了它向读者发出信号,我们没有使用它保存的信息。 + +### 编写函数的文档 + +如果我们已经将工作分解成函数分解的很好了,那么应该很容易使用通俗易懂的语言描述每个函数的目的,并且在函数的定义顶部的文档字符串中提供这些描述。这个说明不应该解释函数是如何实现的;实际上,应该能够不改变这个说明,使用不同的方法,重新实现这个函数。 + +对于最简单的函数,一个单行的文档字符串通常就足够了(见 4.2)。你应该提供一个在一行中包含一个完整的句子的三重引号引起来的字符串。对于不寻常的函数,你还是应该在第一行提供一个一句话总结,因为很多的文档字符串处理工具会索引这个字符串。它后面应该有一个空行,然后是更详细的功能说明(见`http://www.python.org/dev/peps/pep-0257/`的文档字符串约定的更多信息)。 + +文档字符串可以包括一个 doctest 块,说明使用的函数和预期的输出。这些都可以使用 Python 的`docutils`模块自动测试。文档字符串应当记录函数的每个参数的类型和返回类型。至少可以用纯文本来做这些。然而,请注意,NLTK 使用 Sphinx 标记语言来记录参数。这种格式可以自动转换成富结构化的 API 文档(见`http://nltk.org/`),并包含某些“字段”的特殊处理,例如`param`,允许清楚地记录函数的输入和输出。4.5 演示了一个完整的文档字符串。 + +```py +def accuracy(reference, test): + """ + Calculate the fraction of test items that equal the corresponding reference items. + + Given a list of reference values and a corresponding list of test values, + return the fraction of corresponding values that are equal. + In particular, return the fraction of indexes + {0>> accuracy(['ADJ', 'N', 'V', 'N'], ['N', 'N', 'V', 'ADJ']) + 0.5 + + :param reference: An ordered list of reference values + :type reference: list + :param test: A list of values to compare against the corresponding + reference values + :type test: list + :return: the accuracy score + :rtype: float + :raises ValueError: If reference and length do not have the same length + """ + + if len(reference) != len(test): + raise ValueError("Lists must have the same length.") + num_correct = 0 + for x, y in zip(reference, test): + if x == y: + num_correct += 1 + return float(num_correct) / len(reference) +``` + +## 4.5 更多关于函数 + +本节将讨论更高级的特性,你在第一次阅读本章时可能更愿意跳过此节。 + +### 作为参数的函数 + +到目前为止,我们传递给函数的参数一直都是简单的对象,如字符串或列表等结构化对象。Python 也允许我们传递一个函数作为另一个函数的参数。现在,我们可以抽象出操作,对相同数据进行不同操作。正如下面的例子表示的,我们可以传递内置函数`len()`或用户定义的函数`last_letter()`作为另一个函数的参数: + +```py +>>> sent = ['Take', 'care', 'of', 'the', 'sense', ',', 'and', 'the', +... 'sounds', 'will', 'take', 'care', 'of', 'themselves', '.'] +>>> def extract_property(prop): +... return [prop(word) for word in sent] +... +>>> extract_property(len) +[4, 4, 2, 3, 5, 1, 3, 3, 6, 4, 4, 4, 2, 10, 1] +>>> def last_letter(word): +... return word[-1] +>>> extract_property(last_letter) +['e', 'e', 'f', 'e', 'e', ',', 'd', 'e', 's', 'l', 'e', 'e', 'f', 's', '.'] +``` + +对象`len`和`last_letter`可以像列表和字典那样被传递。请注意,只有在我们调用该函数时,才在函数名后使用括号;当我们只是将函数作为一个对象,括号被省略。 + +Python 提供了更多的方式来定义函数作为其他函数的参数,即所谓的 lambda 表达式。试想在很多地方没有必要使用上述的`last_letter()`函数,因此没有必要给它一个名字。我们可以等价地写以下内容: + +```py +>>> extract_property(lambda w: w[-1]) +['e', 'e', 'f', 'e', 'e', ',', 'd', 'e', 's', 'l', 'e', 'e', 'f', 's', '.'] +``` + +我们的下一个例子演示传递一个函数给`sorted()`函数。当我们用唯一的参数(需要排序的链表)调用后者,它使用内置的比较函数`cmp()`。然而,我们可以提供自己的排序函数,例如按长度递减排序。 + +```py +>>> sorted(sent) +[',', '.', 'Take', 'and', 'care', 'care', 'of', 'of', 'sense', 'sounds', +'take', 'the', 'the', 'themselves', 'will'] +>>> sorted(sent, cmp) +[',', '.', 'Take', 'and', 'care', 'care', 'of', 'of', 'sense', 'sounds', +'take', 'the', 'the', 'themselves', 'will'] +>>> sorted(sent, lambda x, y: cmp(len(y), len(x))) +['themselves', 'sounds', 'sense', 'Take', 'care', 'will', 'take', 'care', +'the', 'and', 'the', 'of', 'of', ',', '.'] +``` + +### 累计函数 + +这些函数以初始化一些存储开始,迭代和处理输入的数据,最后返回一些最终的对象(一个大的结构或汇总的结果)。做到这一点的一个标准的方式是初始化一个空链表,累计材料,然后返回这个链表,如 4.6 中所示函数`search1()`。 + +```py +def search1(substring, words): + result = [] + for word in words: + if substring in word: + result.append(word) + return result + +def search2(substring, words): + for word in words: + if substring in word: + yield word +``` + +函数`search2()`是一个生成器。第一次调用此函数,它运行到`yield`语句然后停下来。调用程序获得第一个词,完成任何必要的处理。一旦调用程序对另一个词做好准备,函数会从停下来的地方继续执行,直到再次遇到`yield`语句。这种方法通常更有效,因为函数只产生调用程序需要的数据,并不需要分配额外的内存来存储输出(参见前面关于生成器表达式的讨论)。 + +下面是一个更复杂的生成器的例子,产生一个词列表的所有排列。为了强制`permutations()`函数产生所有它的输出,我们将它包装在`list()`调用中❶。 + +```py +>>> def permutations(seq): +... if len(seq) <= 1: +... yield seq +... else: +... for perm in permutations(seq[1:]): +... for i in range(len(perm)+1): +... yield perm[:i] + seq[0:1] + perm[i:] +... +>>> list(permutations(['police', 'fish', 'buffalo'])) ❶ +[['police', 'fish', 'buffalo'], ['fish', 'police', 'buffalo'], + ['fish', 'buffalo', 'police'], ['police', 'buffalo', 'fish'], + ['buffalo', 'police', 'fish'], ['buffalo', 'fish', 'police']] +``` + +注意 + +`permutations`函数使用了一种技术叫递归,将在下面 4.7 讨论。产生一组词的排列对于创建测试一个语法的数据十分有用(8)。 + +### 高阶函数 + +Python 提供一些具有函数式编程语言如 Haskell 标准特征的高阶函数。我们将在这里演示它们,与使用列表推导的相对应的表达一起。 + +让我们从定义一个函数`is_content_word()`开始,它检查一个词是否来自一个开放的实词类。我们使用此函数作为`filter()`的第一个参数,它对作为它的第二个参数的序列中的每个项目运用该函数,只保留该函数返回`True`的项目。 + +```py +>>> def is_content_word(word): +... return word.lower() not in ['a', 'of', 'the', 'and', 'will', ',', '.'] +>>> sent = ['Take', 'care', 'of', 'the', 'sense', ',', 'and', 'the', +... 'sounds', 'will', 'take', 'care', 'of', 'themselves', '.'] +>>> list(filter(is_content_word, sent)) +['Take', 'care', 'sense', 'sounds', 'take', 'care', 'themselves'] +>>> [w for w in sent if is_content_word(w)] +['Take', 'care', 'sense', 'sounds', 'take', 'care', 'themselves'] +``` + +另一个高阶函数是`map()`,将一个函数运用到一个序列中的每一项。它是我们在 4.5 看到的函数`extract_property()`的一个通用版本。这里是一个简单的方法找出布朗语料库新闻部分中的句子的平均长度,后面跟着的是使用列表推导计算的等效版本: + +```py +>>> lengths = list(map(len, nltk.corpus.brown.sents(categories='news'))) +>>> sum(lengths) / len(lengths) +21.75081116158339 +>>> lengths = [len(sent) for sent in nltk.corpus.brown.sents(categories='news')] +>>> sum(lengths) / len(lengths) +21.75081116158339 +``` + +在上面的例子中,我们指定了一个用户定义的函数`is_content_word()`和一个内置函数`len()`。我们还可以提供一个 lambda 表达式。这里是两个等效的例子,计数每个词中的元音的数量。 + +```py +>>> list(map(lambda w: len(filter(lambda c: c.lower() in "aeiou", w)), sent)) +[2, 2, 1, 1, 2, 0, 1, 1, 2, 1, 2, 2, 1, 3, 0] +>>> [len(c for c in w if c.lower() in "aeiou") for w in sent] +[2, 2, 1, 1, 2, 0, 1, 1, 2, 1, 2, 2, 1, 3, 0] +``` + +列表推导为基础的解决方案通常比基于高阶函数的解决方案可读性更好,我们在整个这本书的青睐于使用前者。 + +### 命名的参数 + +当有很多参数时,很容易混淆正确的顺序。我们可以通过名字引用参数,甚至可以给它们分配默认值以供调用程序没有提供该参数时使用。现在参数可以按任意顺序指定,也可以省略。 + +```py +>>> def repeat(msg='', num=1): +... return msg * num +>>> repeat(num=3) +'' +>>> repeat(msg='Alice') +'Alice' +>>> repeat(num=5, msg='Alice') +'AliceAliceAliceAliceAlice' +``` + +这些被称为关键字参数。如果我们混合使用这两种参数,就必须确保未命名的参数在命名的参数前面。必须是这样,因为未命名参数是根据位置来定义的。我们可以定义一个函数,接受任意数量的未命名和命名参数,并通过一个就地的参数列表`*args`和一个就地的关键字参数字典`**kwargs`来访问它们。(字典将在 3 中讲述。) + +```py +>>> def generic(*args, **kwargs): +... print(args) +... print(kwargs) +... +>>> generic(1, "African swallow", monty="python") +(1, 'African swallow') +{'monty': 'python'} +``` + +当`*args`作为函数参数时,它实际上对应函数所有的未命名参数。下面是另一个这方面的 Python 语法的演示,处理可变数目的参数的函数`zip()`。我们将使用变量名`*song`来表示名字`*args`并没有什么特别的。 + +```py +>>> song = [['four', 'calling', 'birds'], +... ['three', 'French', 'hens'], +... ['two', 'turtle', 'doves']] +>>> list(zip(song[0], song[1], song[2])) +[('four', 'three', 'two'), ('calling', 'French', 'turtle'), ('birds', 'hens', 'doves')] +>>> list(zip(*song)) +[('four', 'three', 'two'), ('calling', 'French', 'turtle'), ('birds', 'hens', 'doves')] +``` + +应该从这个例子中明白输入`*song`仅仅是一个方便的记号,相当于输入了`song[0], song[1], song[2]`。 + +下面是另一个在函数的定义中使用关键字参数的例子,有三种等效的方法来调用这个函数: + +```py +>>> def freq_words(file, min=1, num=10): +... text = open(file).read() +... tokens = word_tokenize(text) +... freqdist = nltk.FreqDist(t for t in tokens if len(t) >= min) +... return freqdist.most_common(num) +>>> fw = freq_words('ch01.rst', 4, 10) +>>> fw = freq_words('ch01.rst', min=4, num=10) +>>> fw = freq_words('ch01.rst', num=10, min=4) +``` + +命名参数的另一个作用是它们允许选择性使用参数。因此,我们可以在我们高兴使用默认值的地方省略任何参数:`freq_words('ch01.rst', min=4)`, `freq_words('ch01.rst', 4)`。可选参数的另一个常见用途是作为标志使用。这里是同一个的函数的修订版本,如果设置了`verbose`标志将会报告其进展情况: + +```py +>>> def freq_words(file, min=1, num=10, verbose=False): +... freqdist = FreqDist() +... if verbose: print("Opening", file) +... text = open(file).read() +... if verbose: print("Read in %d characters" % len(file)) +... for word in word_tokenize(text): +... if len(word) >= min: +... freqdist[word] += 1 +... if verbose and freqdist.N() % 100 == 0: print(".", sep="") +... if verbose: print +... return freqdist.most_common(num) +``` + +小心! + +注意不要使用可变对象作为参数的默认值。这个函数的一系列调用将使用同一个对象,有时会出现离奇的结果,就像我们稍后会在关于调试的讨论中看到的那样。 + +小心! + +如果你的程序将使用大量的文件,它是一个好主意来关闭任何一旦不再需要的已经打开的文件。如果你使用`with`语句,Python 会自动关闭打开的文件: + +```py +>>> with open("lexicon.txt") as f: +... data = f.read() +... # process the data +``` + +## 4.6 程序开发 + +编程是一种技能,需要获得几年的各种编程语言和任务的经验。关键的高层次能力是*算法设计*及其在*结构化编程*中的实现。关键的低层次的能力包括熟悉语言的语法结构,以及排除故障的程序(不能表现预期的行为的程序)的各种诊断方法的知识。 + +本节描述一个程序模块的内部结构,以及如何组织一个多模块的程序。然后描述程序开发过程中出现的各种错误,你可以做些什么来解决这些问题,更好的是,从一开始就避免它们。 + +### Python 模块的结构 + +程序模块的目的是把逻辑上相关的定义和函数结合在一起,以方便重用和更高层次的抽象。Python 模块只是一些单独的`.py`文件。例如,如果你在处理一种特定的语料格式,读取和写入这种格式的函数可以放在一起。这两种格式所使用的常量,如字段分隔符或一个`EXTN = ".inf"`文件扩展名,可以共享。如果要更新格式,你就会知道只有一个文件需要改变。类似地,一个模块可以包含用于创建和操纵一种特定的数据结构如语法树的代码,或执行特定的处理任务如绘制语料统计图表的代码。 + +当你开始编写 Python 模块,有一些例子来模拟是有益的。你可以使用变量`__file__`定位你的系统中任一 NLTK 模块的代码,例如: + +```py +>>> nltk.metrics.distance.__file__ +'/usr/lib/python2.5/site-packages/nltk/metrics/distance.pyc' +``` + +这将返回模块已编译`.pyc`文件的位置,在你的机器上你可能看到的位置不同。你需要打开的文件是对应的`.py`源文件,它和`.pyc`文件放在同一目录下。另外,你可以在网站上查看该模块的最新版本`http://code.google.com/p/nltk/source/browse/trunk/nltk/nltk/metrics/distance.py`。 + +与其他 NLTK 的模块一样,`distance.py`以一组注释行开始,包括一行模块标题和作者信息。(由于代码会被发布,也包括代码可用的 URL、版权声明和许可信息。)接下来是模块级的文档字符串,三重引号的多行字符串,其中包括当有人输入`help(nltk.metrics.distance)`将被输出的关于模块的信息。 + +```py +# Natural Language Toolkit: Distance Metrics +# +# Copyright (C) 2001-2013 NLTK Project +# Author: Edward Loper +# Steven Bird +# Tom Lippincott +# URL: +# For license information, see LICENSE.TXT +# + +""" +Distance Metrics. + +Compute the distance between two items (usually strings). +As metrics, they must satisfy the following three requirements: + +1\. d(a, a) = 0 +2\. d(a, b) >= 0 +3\. d(a, c) <= d(a, b) + d(b, c) +""" + +``` + +### 多模块程序 + +一些程序汇集多种任务,例如从语料库加载数据、对数据进行一些分析、然后将其可视化。我们可能已经有了稳定的模块来加载数据和实现数据可视化。我们的工作可能会涉及到那些分析任务的编码,只是从现有的模块调用一些函数。4.7 描述了这种情景。 + +![Images/multi-module.png](Images/e685801a8cec4515b47e1bda95deb59d.jpg) + +图 4.7:一个多模块程序的结构:主程序`my_program.py`从其他两个模块导入函数;独特的分析任务在主程序本地进行,而一般的载入和可视化任务被分离开以便可以重用和抽象。 + +通过将我们的工作分成几个模块和使用`import`语句访问别处定义的函数,我们可以保持各个模块简单,易于维护。这种做法也将导致越来越多的模块的集合,使我们有可能建立复杂的涉及模块间层次结构的系统。设计这样的系统是一个复杂的软件工程任务,这超出了本书的范围。 + +### 错误源头 + +掌握编程技术取决于当程序不按预期运作时各种解决问题的技能的总结。一些琐碎的东西,如放错位置的符号,可能导致程序的行为异常。我们把这些叫做“bugs”,因为它们与它们所导致的损害相比较小。它们不知不觉的潜入我们的代码,只有在很久以后,我们在一些新的数据上运行程序时才会发现它们的存在。有时,一个错误的修复仅仅是暴露出另一个,于是我们得到了鲜明的印象,bug 在移动。我们唯一的安慰是 bugs 是自发的而不是程序员的错误。 + +繁琐浮躁不谈,调试代码是很难的,因为有那么多的方式出现故障。我们对输入数据、算法甚至编程语言的理解可能是错误的。让我们分别来看看每种情况的例子。 + +首先,输入的数据可能包含一些意想不到的字符。例如,WordNet 的同义词集名称的形式是`tree.n.01`,由句号分割成 3 个部分。最初 NLTK 的 WordNet 模块使用`split('.')`分解这些名称。然而,当有人试图寻找词`PhD`时,这种方法就不能用了,它的同义集名称是`ph.d..n.01`,包含 4 个逗号而不是预期的 2 个。解决的办法是使用`rsplit('.', 2)`利用最右边的句号最多分割两次,留下字符串`ph.d.`不变。虽然在模块发布之前已经测试过,但就在几个星期前有人检测到这个问题(见`http://code.google.com/p/nltk/issues/detail?id=297`)。 + +第二,提供的函数可能不会像预期的那样运作。例如,在测试 NLTK 中的 WordNet 接口时,一名作者注意到没有同义词集定义了反义词,而底层数据库提供了大量的反义词的信息。这看着像是 WordNet 接口中的一个错误,结果却是对 WordNet 本身的误解:反义词在词条中定义,而不是在义词集中。唯一的“bug”是对接口的一个误解(参见`http://code.google.com/p/nltk/issues/detail?id=98`)。 + +第三,我们对 Python 语义的理解可能出错。很容易做出关于两个操作符的相对范围的错误的假设。例如,`"%s.%s.%02d" % "ph.d.", "n", 1`产生一个运行时错误`TypeError: not enough arguments for format string`。这是因为百分号操作符优先级高于逗号运算符。解决办法是添加括号强制限定所需的范围。作为另一个例子,假设我们定义一个函数来收集一个文本中给定长度的所有词符。该函数有文本和词长作为参数,还有一个额外的参数,允许指定结果的初始值作为参数: + +```py +>>> def find_words(text, wordlength, result=[]): +... for word in text: +... if len(word) == wordlength: +... result.append(word) +... return result +>>> find_words(['omg', 'teh', 'lolcat', 'sitted', 'on', 'teh', 'mat'], 3) ❶ +['omg', 'teh', 'teh', 'mat'] +>>> find_words(['omg', 'teh', 'lolcat', 'sitted', 'on', 'teh', 'mat'], 2, ['ur']) ❷ +['ur', 'on'] +>>> find_words(['omg', 'teh', 'lolcat', 'sitted', 'on', 'teh', 'mat'], 3) ❸ +['omg', 'teh', 'teh', 'mat', 'omg', 'teh', 'teh', 'mat'] +``` + +我们第一次调用`find_words()`❶,我们得到所有预期的三个字母的词。第二次,我们为`result`指定一个初始值,一个单元素列表`['ur']`,如预期,结果中有这个词连同我们的文本中的其他双字母的词。现在,我们再次使用❶中相同的参数调用`find_words()`❸,但我们得到了不同的结果!我们每次不使用第三个参数调用`find_words()`,结果都只会延长前次调用的结果,而不是以在函数定义中指定的空链表`result`开始。程序的行为并不如预期,因为我们错误地认为在函数被调用时会创建默认值。然而,它只创建了一次,在 Python 解释器加载这个函数时。这一个列表对象会被使用,只要没有给函数提供明确的值。 + +### 调试技术 + +由于大多数代码错误是因为程序员的不正确的假设,你检测 bug 要做的第一件事是检查你的假设。通过给程序添加`print`语句定位问题,显示重要的变量的值,并显示程序的进展程度。 + +如果程序产生一个“异常”——运行时错误——解释器会输出一个堆栈跟踪,精确定位错误发生时程序执行的位置。如果程序取决于输入数据,尽量将它减少到能产生错误的最小尺寸。 + +一旦你已经将问题定位在一个特定的函数或一行代码,你需要弄清楚是什么出了错误。使用交互式命令行重现错误发生时的情况往往是有益的。定义一些变量,然后复制粘贴可能出错的代码行到会话中,看看会发生什么。检查你对代码的理解,通过阅读一些文档和测试与你正在试图做的事情相同的其他代码示例。尝试将你的代码解释给别人听,也许他们会看出出错的地方。 + +Python 提供了一个调试器,它允许你监视程序的执行,指定程序暂停运行的行号(即断点),逐步调试代码段和检查变量的值。你可以如下方式在你的代码中调用调试器: + +```py +>>> import pdb +>>> import mymodule +>>> pdb.run('mymodule.myfunction()') +``` + +它会给出一个提示`(Pdb)`,你可以在那里输入指令给调试器。输入`help`来查看命令的完整列表。输入`step`(或只输入`s`)将执行当前行然后停止。如果当前行调用一个函数,它将进入这个函数并停止在第一行。输入`next`(或只输入`n`)是类似的,但它会在当前函数中的下一行停止执行。`break`(或`b`)命令可用于创建或列出断点。输入`continue`(或`c`)会继续执行直到遇到下一个断点。输入任何变量的名称可以检查它的值。 + +我们可以使用 Python 调试器来查找`find_words()`函数的问题。请记住问题是在第二次调用函数时产生的。我们一开始将不使用调试器而调用该函数,使用可能的最小输入。第二次我们使用调试器调用它。 + +```py +>>> import pdb +>>> find_words(['cat'], 3) # [_first-run] +['cat'] +>>> pdb.run("find_words(['dog'], 3)") # [_second-run] +> (1)() +(Pdb) step +--Call-- +> (1)find_words() +(Pdb) args +text = ['dog'] +wordlength = 3 +result = ['cat'] + +``` + +### 防御性编程 + +为了避免一些调试的痛苦,养成防御性的编程习惯是有益的。不要写 20 行程序然后测试它,而是自下而上的打造一些明确可以运作的小的程序片。每次你将这些程序片组合成更大的单位都要仔细的看它是否能如预期的运作。考虑在你的代码中添加`assert`语句,指定变量的属性,例如`assert(isinstance(text, list))`。如果`text`的值在你的代码被用在一些较大的环境中时变为了一个字符串,将产生一个`AssertionError`,于是你会立即得到问题的通知。 + +一旦你觉得你发现了错误,作为一个假设查看你的解决方案。在重新运行该程序之前尝试预测你修正错误的影响。如果 bug 不能被修正,不要陷入盲目修改代码希望它会奇迹般地重新开始运作的陷阱。相反,每一次修改都要尝试阐明错误是什么和为什么这样修改会解决这个问题的假设。如果这个问题没有解决就撤消这次修改。 + +当你开发你的程序时,扩展其功能,并修复所有 bug,维护一套测试用例是有益的。这被称为回归测试,因为它是用来检测代码“回归”的地方——修改代码后会带来一个意想不到的副作用是以前能运作的程序不运作了的地方。Python 以`doctest`模块的形式提供了一个简单的回归测试框架。这个模块搜索一个代码或文档文件查找类似与交互式 Python 会话这样的文本块,这种形式你已经在这本书中看到了很多次。它执行找到的 Python 命令,测试其输出是否与原始文件中所提供的输出匹配。每当有不匹配时,它会报告预期值和实际值。有关详情,请查询在`http://docs.python.org/library/doctest.html`上的`doctest`文档。除了回归测试它的值,`doctest`模块有助于确保你的软件文档与你的代码保持同步。 + +也许最重要的防御性编程策略是要清楚的表述你的代码,选择有意义的变量和函数名,并通过将代码分解成拥有良好文档的接口的函数和模块尽可能的简化代码。 + +## 4.7 算法设计 + +本节将讨论更高级的概念,你在第一次阅读本章时可能更愿意跳过本节。 + +解决算法问题的一个重要部分是为手头的问题选择或改造一个合适的算法。有时会有几种选择,能否选择最好的一个取决于对每个选择随数据增长如何执行的知识。关于这个话题的书很多,我们只介绍一些关键概念和精心描述在自然语言处理中最普遍的做法。 + +最有名的策略被称为分而治之。我们解决一个大小为`n`的问题通过将其分成两个大小为`n/2`的问题,解决这些问题,组合它们的结果成为原问题的结果。例如,假设我们有一堆卡片,每张卡片上写了一个词。我们可以排序这一堆卡片,通过将它分成两半分别给另外两个人来排序(他们又可以做同样的事情)。然后,得到两个排好序的卡片堆,将它们并成一个单一的排序堆就是一项容易的任务了。参见 4.8 这个过程的说明。 + +![Images/mergesort.png](Images/1094084b61ac3f0e4416e92869c52ccd.jpg) + +图 4.8:通过分而治之排序:对一个数组排序,我们将其分成两半并对每一半进行排序(递归);将每个排好序的一半合并成一个完整的链表(再次递归);这个算法被称为“归并排序“。 + +另一个例子是在词典中查找一个词的过程。我们打开在书的中部附近的一个地方,比较我们的词与当前页面上的词。如果它在词典中的词前面,我们就在词典的前一半重复上面的过程;如果它在后面,我们就使用词典的后一半。这种搜索方法被称为二分查找,因为它的每一步都将问题分裂成一半。 + +算法设计的另一种方法,我们解决问题通过将它转化为一个我们已经知道如何解决的问题的一个实例。例如,为了检测列表中的重复项,我们可以预排序这个列表,然后通过一次扫描检查是否有相邻的两个元素是相同的。 + +### 递归 + +上面的关于排序和搜索的例子有一个引人注目的特征:解决一个大小为`n`的问题,可以将其分成两半,然后处理一个或多个大小为`n/2`的问题。实现这种方法的一种常见方式是使用递归。我们定义一个函数`f`,从而简化了问题,并调用自身来解决一个或多个同样问题的更简单的实例。然后组合它们的结果成为原问题的解答。 + +例如,假设我们有`n`个词,要计算出它们结合在一起有多少不同的方式能组成一个词序列。如果我们只有一个词(`n=1`),只是一种方式组成一个序列。如果我们有 2 个词,就有 2 种方式将它们组成一个序列。3 个词有 6 种可能性。一般的,`n`个词有`n × n-1 × … × 2 × 1`种方式(即`n`的阶乘)。我们可以将这些编写成如下代码: + +```py +>>> def factorial1(n): +... result = 1 +... for i in range(n): +... result *= (i+1) +... return result +``` + +但是,也可以使用一种递归算法来解决这个问题,该算法基于以下观察。假设我们有办法为`n-1`不同的词构建所有的排列。然后对于每个这样的排列,有`n`个地方我们可以插入一个新词:开始、结束或任意两个词之间的`n-2`个空隙。因此,我们简单的将`n-1`个词的解决方案数乘以`n`的值。我们还需要基础案例,也就是说,如果我们有一个词,只有一个顺序。我们可以将这些编写成如下代码: + +```py +>>> def factorial2(n): +... if n == 1: +... return 1 +... else: +... return n * factorial2(n-1) +``` + +这两种算法解决同样的问题。一个使用迭代,而另一个使用递归。我们可以用递归处理深层嵌套的对象,例如 WordNet 的上位词层次。让我们计数给定同义词集`s`为根的上位词层次的大小。我们会找到`s`的每个下位词的大小,然后将它们加到一起(我们也将加 1 表示同义词集本身)。下面的函数`size1()`做这项工作;注意函数体中包括`size1()`的递归调用: + +```py +>>> def size1(s): +... return 1 + sum(size1(child) for child in s.hyponyms()) +``` + +我们也可以设计一种这个问题的迭代解决方案处理层的层次结构。第一层是同义词集本身❶,然后是同义词集所有的下位词,之后是所有下位词的下位词。每次循环通过查找上一层的所有下位词计算下一层❸。它也保存了到目前为止遇到的同义词集的总数❷。 + +```py +>>> def size2(s): +... layer = [s] ❶ +... total = 0 +... while layer: +... total += len(layer) ❷ +... layer = [h for c in layer for h in c.hyponyms()] ❸ +... return total +``` + +迭代解决方案不仅代码更长而且更难理解。它迫使我们程序式的思考问题,并跟踪`layer`和`total`随时间变化发生了什么。让我们满意的是两种解决方案均给出了相同的结果。我们将使用`import`语句的一个新的形式,允许我们缩写名称`wordnet`为`wn`: + +```py +>>> from nltk.corpus import wordnet as wn +>>> dog = wn.synset('dog.n.01') +>>> size1(dog) +190 +>>> size2(dog) +190 +``` + +作为递归的最后一个例子,让我们用它来构建一个深嵌套的对象。一个字母查找树是一种可以用来索引词汇的数据结构,一次一个字母。(这个名字来自于单词`retrieval`)。例如,如果`trie`包含一个字母的查找树,那么`trie['c']`是一个较小的查找树,包含所有以`c`开头的词。4.9 演示了使用 Python 字典`(3)`构建查找树的递归过程。若要插入词`chien`(`dog`的法语),我们将`c`分类,递归的掺入`hien`到`trie['c']`子查找树中。递归继续直到词中没有剩余的字母,于是我们存储的了预期值(本例中是词`dog`)。 + +```py +def insert(trie, key, value): + if key: + first, rest = key[0], key[1:] + if first not in trie: + trie[first] = {} + insert(trie[first], rest, value) + else: + trie['value'] = value +``` + +小心! + +尽管递归编程结构简单,但它是有代价的。每次函数调用时,一些状态信息需要推入堆栈,这样一旦函数执行完成可以从离开的地方继续执行。出于这个原因,迭代的解决方案往往比递归解决方案的更高效。 + +### 权衡空间与时间 + +我们有时可以显著的加快程序的执行,通过建设一个辅助的数据结构,例如索引。4.10 实现一个简单的电影评论语料库的全文检索系统。通过索引文档集合,它提供更快的查找。 + +```py +def raw(file): + contents = open(file).read() + contents = re.sub(r'<.*?>', ' ', contents) + contents = re.sub('\s+', ' ', contents) + return contents + +def snippet(doc, term): + text = ' '*30 + raw(doc) + ' '*30 + pos = text.index(term) + return text[pos-30:pos+30] + +print("Building Index...") +files = nltk.corpus.movie_reviews.abspaths() +idx = nltk.Index((w, f) for f in files for w in raw(f).split()) + +query = '' +while query != "quit": + query = input("query> ") # use raw_input() in Python 2 + if query in idx: + for doc in idx[query]: + print(snippet(doc, query)) + else: + print("Not found") +``` + +一个更微妙的空间与时间折中的例子涉及使用整数标识符替换一个语料库的词符。我们为语料库创建一个词汇表,每个词都被存储一次的列表,然后转化这个列表以便我们能通过查找任意词来找到它的标识符。每个文档都进行预处理,使一个词列表变成一个整数列表。现在所有的语言模型都可以使用整数。见 4.11 中的内容,如何为一个已标注的语料库做这个的例子的列表。 + +```py +def preprocess(tagged_corpus): + words = set() + tags = set() + for sent in tagged_corpus: + for word, tag in sent: + words.add(word) + tags.add(tag) + wm = dict((w, i) for (i, w) in enumerate(words)) + tm = dict((t, i) for (i, t) in enumerate(tags)) + return [[(wm[w], tm[t]) for (w, t) in sent] for sent in tagged_corpus] +``` + +空间时间权衡的另一个例子是维护一个词汇表。如果你需要处理一段输入文本检查所有的词是否在现有的词汇表中,词汇表应存储为一个集合,而不是一个列表。集合中的元素会自动索引,所以测试一个大的集合的成员将远远快于测试相应的列表的成员。 + +我们可以使用`timeit`模块测试这种说法。`Timer`类有两个参数:一个是多次执行的语句,一个是只在开始执行一次的设置代码。我们将分别使用一个整数的列表❶和一个整数的集合❷模拟 10 万个项目的词汇表。测试语句将产生一个随机项,它有 50% 的机会在词汇表中❸。 + +```py +>>> from timeit import Timer +>>> vocab_size = 100000 +>>> setup_list = "import random; vocab = range(%d)" % vocab_size ❶ +>>> setup_set = "import random; vocab = set(range(%d))" % vocab_size ❷ +>>> statement = "random.randint(0, %d) in vocab" % (vocab_size * 2) ❸ +>>> print(Timer(statement, setup_list).timeit(1000)) +2.78092288971 +>>> print(Timer(statement, setup_set).timeit(1000)) +0.0037260055542 +``` + +执行 1000 次链表成员资格测试总共需要 2.8 秒,而在集合上的等效试验仅需 0.0037 秒,也就是说快了三个数量级! + +### 动态规划 + +动态规划是一种自然语言处理中被广泛使用的算法设计的一般方法。“programming”一词的用法与你可能想到的感觉不同,是规划或调度的意思。动态规划用于解决包含多个重叠的子问题的问题。不是反复计算这些子问题,而是简单的将它们的计算结果存储在一个查找表中。在本节的余下部分,我们将介绍动态规划,在一个相当不同的背景下来句法分析。 + +Pingala 是大约生活在公元前 5 世纪的印度作家,作品有被称为《Chandas Shastra》的梵文韵律专著。Virahanka 大约在公元 6 世纪延续了这项工作,研究短音节和长音节组合产生一个长度为`n`的旋律的组合数。短音节,标记为`S`,占一个长度单位,而长音节,标记为`L`,占 2 个长度单位。例如,Pingala 发现,有 5 种方式构造一个长度为 4 的旋律:`V[4] = {LL, SSL, SLS, LSS, SSSS}`。请看,我们可以将`V[4]`分成两个子集,以`L`开始的子集和以`S`开始的子集,如`(1)`所示。 + +```py +V4 = + LL, LSS + i.e. L prefixed to each item of V2 = {L, SS} + SSL, SLS, SSSS + i.e. S prefixed to each item of V3 = {SL, LS, SSS} + +``` + +有了这个观察结果,我们可以写一个小的递归函数称为`virahanka1()`来计算这些旋律,如 4.12 所示。请注意,要计算`V[4]`,我们先要计算`V[3]`和`V[2]`。但要计算`V[3]`,我们先要计算`V[2]`和`V[1]`。在`(2)`中描述了这种调用结构。 + +```py +from numpy import arange +from matplotlib import pyplot + +colors = 'rgbcmyk' # red, green, blue, cyan, magenta, yellow, black + +def bar_chart(categories, words, counts): + "Plot a bar chart showing counts for each word by category" + ind = arange(len(words)) + width = 1 / (len(categories) + 1) + bar_groups = [] + for c in range(len(categories)): + bars = pyplot.bar(ind+c*width, counts[categories[c]], width, + color=colors[c % len(colors)]) + bar_groups.append(bars) + pyplot.xticks(ind+width, words) + pyplot.legend([b[0] for b in bar_groups], categories, loc='upper left') + pyplot.ylabel('Frequency') + pyplot.title('Frequency of Six Modal Verbs by Genre') + pyplot.show() +``` + +![Images/modal_genre.png](Images/2ce816f11fd01927802253d100780b0a.jpg) + +图 4.14:条形图显示布朗语料库中不同部分的情态动词频率:这个可视化图形由 4.13 中的程序产生。 + +从该柱状图可以立即看出`may`和`must`有几乎相同的相对频率。`could`和`might`也一样。 + +也可以动态的产生这些数据的可视化图形。例如,一个使用表单输入的网页可以允许访问者指定搜索参数,提交表单,看到一个动态生成的可视化图形。要做到这一点,我们必须为`matplotlib`指定`Agg`后台,它是一个产生栅格(像素)图像的库❶。下一步,我们像以前一样使用相同的 Matplotlib 方法,但不是用`pyplot.show()`显示结果在图形终端,而是使用`pyplot.savefig()`把它保存到一个文件❷。我们指定文件名,然后输出一些 HTML 标记指示网页浏览器来加载该文件。 + +```py +>>> from matplotlib import use, pyplot +>>> use('Agg') ❶ +>>> pyplot.savefig('modals.png') ❷ +>>> print('Content-Type: text/html') +>>> print() +>>> print('') +>>> print('') +>>> print('') +``` + +### NetworkX + +NetworkX 包定义和操作被称为图的由节点和边组成的结构。它可以从`https://networkx.lanl.gov/`得到。NetworkX 可以和 Matplotlib 结合使用可视化如 WordNet 的网络结构(语义网络,我们在 5 介绍过)。4.15 中的程序初始化一个空的图❸,然后遍历 WordNet 上位词层次为图添加边❶。请注意,遍历是递归的❷,使用在 4.7 讨论的编程技术。结果显示在 4.16。 + +```py +import networkx as nx +import matplotlib +from nltk.corpus import wordnet as wn + +def traverse(graph, start, node): + graph.depth[node.name] = node.shortest_path_distance(start) + for child in node.hyponyms(): + graph.add_edge(node.name, child.name) ❶ + traverse(graph, start, child) ❷ + +def hyponym_graph(start): + G = nx.Graph() ❸ + G.depth = {} + traverse(G, start, start) + return G + +def graph_draw(graph): + nx.draw_graphviz(graph, + node_size = [16 * graph.degree(n) for n in graph], + node_color = [graph.depth[n] for n in graph], + with_labels = False) + matplotlib.pyplot.show() +``` + +![Images/dog-graph.png](Images/8cb61a943f3d34f94596e77065410cd3.jpg) + +图 4.16:使用 NetworkX 和 Matplotlib 可视化数据:WordNet 的上位词层次的部分显示,开始于`dog.n.01`(中间最黑的节点);节点的大小对应节点的孩子的数目,颜色对应节点到`dog.n.01`的距离;此可视化图形由 4.15 中的程序产生。 + +### csv + +语言分析工作往往涉及数据统计表,包括有关词项的信息、试验研究的参与者名单或从语料库提取的语言特征。这里有一个 CSV 格式的简单的词典片段: + +sleep, sli:p, v.i, a condition of body and mind ...walk, wo:k, v.intr, progress by lifting and setting down each foot ...wake, weik, intrans, cease to sleep + +我们可以使用 Python 的 CSV 库读写这种格式存储的文件。例如,我们可以打开一个叫做`lexicon.csv`的 CSV 文件❶,并遍历它的行❷: + +```py +>>> import csv +>>> input_file = open("lexicon.csv", "rb") ❶ +>>> for row in csv.reader(input_file): ❷ +... print(row) +['sleep', 'sli:p', 'v.i', 'a condition of body and mind ...'] +['walk', 'wo:k', 'v.intr', 'progress by lifting and setting down each foot ...'] +['wake', 'weik', 'intrans', 'cease to sleep'] +``` + +每一行是一个字符串列表。如果字段包含有数值数据,它们将作为字符串出现,所以都必须使用`int()`或`float()`转换。 + +### NumPy + +NumPy 包对 Python 中的数值处理提供了大量的支持。NumPy 有一个多维数组对象,它可以很容易初始化和访问: + +```py +>>> from numpy import array +>>> cube = array([ [[0,0,0], [1,1,1], [2,2,2]], +... [[3,3,3], [4,4,4], [5,5,5]], +... [[6,6,6], [7,7,7], [8,8,8]] ]) +>>> cube[1,1,1] +4 +>>> cube[2].transpose() +array([[6, 7, 8], + [6, 7, 8], + [6, 7, 8]]) +>>> cube[2,1:] +array([[7, 7, 7], + [8, 8, 8]]) +``` + +NumPy 包括线性代数函数。在这里我们进行矩阵的奇异值分解,潜在语义分析中使用的操作,它能帮助识别一个文档集合中的隐含概念。 + +```py +>>> from numpy import linalg +>>> a=array([[4,0], [3,-5]]) +>>> u,s,vt = linalg.svd(a) +>>> u +array([[-0.4472136 , -0.89442719], + [-0.89442719, 0.4472136 ]]) +>>> s +array([ 6.32455532, 3.16227766]) +>>> vt +array([[-0.70710678, 0.70710678], + [-0.70710678, -0.70710678]]) +``` + +NLTK 中的聚类包`nltk.cluster`中广泛使用 NumPy 数组,支持包括 k-means 聚类、高斯 EM 聚类、组平均凝聚聚类以及聚类分析图。有关详细信息,请输入`help(nltk.cluster)`。 + +### 其他 Python 库 + +还有许多其他的 Python 库,你可以使用`http://pypi.python.org/`处的 Python 包索引找到它们。许多库提供了外部软件接口,例如关系数据库(如`mysql-python`)和大数据集合(如`PyLucene`)。许多其他库能访问各种文件格式,如 PDF、MSWord 和 XML(`pypdf`, `pywin32`, `xml.etree`)、RSS 源(如`feedparser`)以及电子邮箱(如`imaplib`, `email`)。 + +## 6 小结 + +* Python 赋值和参数传递使用对象引用,例如如果`a`是一个列表,我们分配`b = a`,然后任何`a`上的操作都将修改`b`,反之亦然。 +* `is`操作测试是否两个对象是相同的内部对象,而`==`测试是否两个对象是相等的。两者的区别和词符与词类型的区别相似。 +* 字符串、列表和元组是不同类型的序列对象,支持常见的操作如:索引、切片、`len()`、`sorted()`和使用`in`的成员测试。 +* 声明式的编程风格通常会产生更简洁更可读的代码;手动递增循环变量通常是不必要的;枚举一个序列,使用`enumerate()`。 +* 函数是一个重要的编程抽象,需要理解的关键概念有:参数传递、变量的作用域和文档字符串。 +* 函数作为一个命名空间:函数内部定义的名称在该函数外不可见,除非这些名称被宣布为是全局的。 +* 模块允许将材料与本地的文件逻辑的关联起来。一个模块作为一个命名空间:在一个模块中定义的名称——如变量和函数——在其他模块中不可见,除非这些名称被导入。 +* 动态规划是一种在 NLP 中广泛使用的算法设计技术,它存储以前的计算结果,以避免不必要的重复计算。 + +## 4.10 深入阅读 + +本章已经触及编程中的许多主题,一些是 Python 特有的,一些是相当普遍的。我们只是触及了表面,你可能想要阅读更多有关这些主题的内容,可以从`http://nltk.org/`处的本章深入阅读材料开始。 + +Python 网站提供大量文档。理解内置的函数和标准类型是很重要的,在`http://docs.python.org/library/functions.html`和`http://docs.python.org/library/stdtypes.html`处有描述。我们已经学习了生成器以及它们对提高效率的重要性;关于迭代器的信息,一个密切相关的话题请看`http://docs.python.org/library/itertools.html`。查询你喜欢的 Python 书籍中这些主题的更多信息。使用 Python 进行多媒体处理包括声音文件的一个优秀的资源是(Guzdial, 2005)。 + +使用在线 Python 文档时,要注意你安装的版本可能与你正在阅读的文档的版本不同。你可以使用`import sys; sys.version`松地查询你有的是什么版本。特定版本的文档在`http://www.python.org/doc/versions/`处。 + +算法设计是计算机科学中一个丰富的领域。一些很好的出发点是(Harel, 2004), (Levitin, 2004), (Knuth, 2006)。(Hunt & Thomas, 2000)和(McConnell, 2004)为软件开发实践提供了有益的指导。 + +## 4.11 练习 + +1. ☼ 使用 Python 的帮助功能,找出更多关于序列对象的内容。在解释器中输入`help(str)`,`help(list)`和`help(tuple)`。这会给你一个每种类型支持的函数的完整列表。一些函数名字有些特殊,两侧有下划线;正如帮助文档显示的那样,每个这样的函数对应于一些较为熟悉的东西。例如`x.__getitem__(y)`仅仅是以长篇大论的方式使用`x[y]`。 + +2. ☼ 确定三个同时在元组和链表上都可以执行的操作。确定三个不能在元组上执行的列表操作。命名一段使用列表替代元组会产生一个 Python 错误的代码。 + +3. ☼ 找出如何创建一个由单个项目组成的元组。至少有两种方法可以做到这一点。 + +4. ☼ 创建一个列表`words = ['is', 'NLP', 'fun', '?']`。使用一系列赋值语句(如`words[1] = words[2]`)和临时变量`tmp`将这个列表转换为列表`['NLP', 'is', 'fun', '!']`。现在,使用元组赋值做相同的转换。 + +5. ☼ 通过输入`help(cmp)`阅读关于内置的比较函数`cmp`的内容。它与比较运算符在行为上有何不同? + +6. ☼ 创建一个 N 元组的滑动窗口的方法在下面两种极端情况下是否正确:`n = 1`和`n = len(sent)`? + +7. ☼ 我们指出当空字符串和空链表出现在`if`从句的条件部分时,它们的判断结果是`False`。在这种情况下,它们被说成出现在一个布尔上下文中。实验各种不同的布尔上下文中的非布尔表达式,看它们是否被判断为`True`或`False`。 + +8. ☼ 使用不等号比较字符串,如`'Monty' > 'Python'`。当你做`'Z' > 'a'`时会发生什么?尝试具有共同前缀的字符串对,如`'Monty' > 'Montague'`。阅读有关“字典排序”的内容以便了解这里发生了什么事。尝试比较结构化对象,如`('Monty', 1) > ('Monty', 2)`。这与预期一样吗? + +9. ☼ 写代码删除字符串开头和结尾处的空白,并规范化词之间的空格为一个单独的空格字符。 + + 1. 使用`split()`和`join()`做这个任务 + 2. 使用正则表达式替换做这个任务 +10. ☼ 写一个程序按长度对词排序。定义一个辅助函数`cmp_len`,它在词长上使用`cmp`比较函数。 + +11. ◑ 创建一个词列表并将其存储在变量`sent1`。现在赋值`sent2 = sent1`。修改`sent1`中的一个项目,验证`sent2`改变了。 + + 1. 现在尝试同样的练习,但使用`sent2 = sent1[:]`赋值。再次修改`sent1`看看`sent2`会发生什么。解释。 + 2. 现在定义`text1`为一个字符串列表的列表(例如表示由多个句子组成的文本)。现在赋值`text2 = text1[:]`,分配一个新值给其中一个词,例如`text1[1][1] = 'Monty'`。检查这对`text2`做了什么。解释。 + 3. 导入 Python 的`deepcopy()`函数(即`from copy import deepcopy`),查询其文档,使用它生成任一对象的新副本。 +12. ◑ 使用列表乘法初始化`n x m`的空字符串列表的咧表,例如`word_table = [[''] * n] * m`。当你设置其中一个值时会发生什么事,例如`word_table[1][2] = "hello"`?解释为什么会出现这种情况。现在写一个表达式,使用`range()`构造一个列表,表明它没有这个问题。 + +13. ◑ 写代码初始化一个称为`word_vowels`的二维数组的集合,处理一个词列表,添加每个词到`word_vowels[l][v]`,其中`l`是词的长度,`v`是它包含的元音的数量。 + +14. ◑ 写一个函数`novel10(text)`输出所有在一个文本最后 10% 出现而之前没有遇到过的词。 + +15. ◑ 写一个程序将一个句子表示成一个单独的字符串,分割和计数其中的词。让它输出每一个词和词的频率,每行一个,按字母顺序排列。 + +16. ◑ 阅读有关 Gematria 的内容,它是一种方法,分配一个数字给词汇,具有相同数字的词之间映射以发现文本隐藏的含义(`http://en.wikipedia.org/wiki/Gematria`, `http://essenes.net/gemcal.htm`)。 + + 1. 写一个函数`gematria()`,根据`letter_vals`中的字母值,累加一个词的字母的数值: + + ```py + >>> letter_vals = {'a':1, 'b':2, 'c':3, 'd':4, 'e':5, 'f':80, 'g':3, 'h':8, + ... 'i':10, 'j':10, 'k':20, 'l':30, 'm':40, 'n':50, 'o':70, 'p':80, 'q':100, + ... 'r':200, 's':300, 't':400, 'u':6, 'v':6, 'w':800, 'x':60, 'y':10, 'z':7} + ``` + + 2. 处理一个语料库(`nltk.corpus.state_union`)对每个文档计数它有多少词的字母数值为 666。 + + 3. 写一个函数`decode()`来处理文本,随机替换词汇为它们的 Gematria 等值的词,以发现文本的“隐藏的含义”。 + +17. ◑ 写一个函数`shorten(text, n)`处理文本,省略文本中前`n`个最频繁出现的词。它的可读性会如何? + +18. ◑ 写代码输出词汇的索引,允许别人根据其含义查找词汇(或它们的发言;词汇条目中包含的任何属性)。 + +19. ◑ 写一个列表推导排序 WordNet 中与给定同义词集接近的同义词集的列表。例如,给定同义词集`minke_whale.n.01`, `orca.n.01`, `novel.n.01`和`tortoise.n.01`,按照它们与`right_whale.n.01`的`shortest_path_distance()`对它们进行排序。 + +20. ◑ 写一个函数处理一个词列表(含重复项),返回一个按照频率递减排序的词列表(没有重复项)。例如如果输入列表中包含词`table`的 10 个实例,`chair`的 9 个实例,那么在输出列表中`table`会出现在`chair`前面。 + +21. ◑ 写一个函数以一个文本和一个词汇表作为它的参数,返回在文本中出现但不在词汇表中的一组词。两个参数都可以表示为字符串列表。你能使用`set.difference()`在一行代码中做这些吗? + +22. ◑ 从 Python 标准库的`operator`模块导入`itemgetter()`函数(即`from operator import itemgetter`)。创建一个包含几个词的列表`words`。现在尝试调用:`sorted(words, key=itemgetter(1))`和`sorted(words, key=itemgetter(-1))`。解释`itemgetter()`正在做什么。 + +23. ◑ 写一个递归函数`lookup(trie, key)`在查找树中查找一个关键字,返回找到的值。扩展这个函数,当一个词由其前缀唯一确定时返回这个词(例如`vanguard`是以`vang-`开头的唯一的词,所以`lookup(trie, 'vang')`应该返回与`lookup(trie, 'vanguard')`相同的内容)。 + +24. ◑ 阅读关于“关键字联动”的内容((Scott & Tribble, 2006)的第 5 章)。从 NLTK 的莎士比亚语料库中提取关键字,使用 NetworkX 包,画出关键字联动网络。 + +25. ◑ 阅读有关字符串编辑距离和 Levenshtein 算法的内容。尝试`nltk.edit_distance()`提供的实现。这用的是动态规划的何种方式?它使用的是自下而上还是自上而下的方法?(另见`http://norvig.com/spell-correct.html`) + +26. ◑卡塔兰数出现在组合数学的许多应用中,包括解析树的计数`(6)`。该级数可以定义如下:`C[0] = 1`,`C[n+1] = Σ0..n (C[i]C[n-i])`。 + + 1. 编写一个递归函数计算第`n`个卡塔兰数`C[n]`。 + 2. 现在写另一个函数使用动态规划做这个计算。 + 3. 使用`timeit`模块比较当`n`增加时这些函数的性能。 +27. ★ 重现有关著作权鉴定的(Zhao & Zobel, 2007)中的一些结果。 + +28. ★ 研究性别特异词汇选择,看你是否可以重现一些`http://www.clintoneast.com/articles/words.php`的结果 + +29. ★ 写一个递归函数漂亮的按字母顺序排列输出一个查找树,例如: + + ```py + chair: 'flesh' + ---t: 'cat' + --ic: 'stylish' + ---en: 'dog' + + ``` + + + + + + + + + diff --git a/docs/nlp-py-2e/5.md b/docs/nlp-py-2e/5.md new file mode 100644 index 0000000000000000000000000000000000000000..81ef8bc08397ced0b7ba9a2708b8b13af1006d1e --- /dev/null +++ b/docs/nlp-py-2e/5.md @@ -0,0 +1,1218 @@ +# 5 分类和标注词汇 + +早在小学你就学过名词、动词、形容词和副词之间的差异。这些“词类”不是闲置的文法家的发明,而是对许多语言处理任务都有用的分类。正如我们将看到的,这些分类源于对文本中词的分布的简单的分析。本章的目的是要回答下列问题: + +1. 什么是词汇分类,在自然语言处理中它们是如何使用? +2. 一个好的存储词汇和它们的分类的 Python 数据结构是什么? +3. 我们如何自动标注文本中词汇的词类? + +一路上,我们将介绍 NLP 的一些基本技术,包括序列标注、N 元模型、回退和评估。这些技术在许多方面都很有用,标注为我们提供了一个表示它们的简单的上下文。我们还将看到,在典型的 NLP 处理流程中,标注为何是位于分词之后的第二个步骤。 + +将单词按它们的词性分类并进行相应地标注的过程,称为词语性质标注、词性标注或简称标注。词性也称为词类或词汇类别。用于特定任务的标记的集合被称为一个标记集。我们在本章的重点是运用标记和自动标注文本。 + +## 1 使用词性标注器 + +一个词语性质标注器或者词性标注器处理一个单词序列,为每个词附加一个词性标记(不要忘记`import nltk`): + +```py +>>> text = word_tokenize("And now for something completely different") +>>> nltk.pos_tag(text) +[('And', 'CC'), ('now', 'RB'), ('for', 'IN'), ('something', 'NN'), +('completely', 'RB'), ('different', 'JJ')] +``` + +在这里我们看到`and`是`CC`,并列连词;`now`和`completely`是`RB`,副词;`for`是`IN`,介词;`something`是`NN`,名词;`different`是`JJ`,形容词。 + +注意 + +NLTK 为每个标记提供了文档,可以使用该标记来查询,如`nltk.help.upenn_tagset('RB')`,或者一个正则表达,如`nltk.help.upenn_tagset('NN.*')`。一些语料库有标记集文档的 README 文件,见`nltk.corpus.???.readme()`,用语料库的名称替换。 + +让我们来看看另一个例子,这次包括一些同形同音异义词: + +```py +>>> text = word_tokenize("They refuse to permit us to obtain the refuse permit") +>>> nltk.pos_tag(text) +[('They', 'PRP'), ('refuse', 'VBP'), ('to', 'TO'), ('permit', 'VB'), ('us', 'PRP'), +('to', 'TO'), ('obtain', 'VB'), ('the', 'DT'), ('refuse', 'NN'), ('permit', 'NN')] +``` + +请注意`refuse`和`permit`都以现在时动词(`VBP`)和名词(`NN`)形式出现。例如`refUSE`是一个动词,意为“拒绝”,而`REFuse`是一个名词,意为“垃圾”(即它们不是同音词)。因此,我们需要知道正在使用哪一个词以便能正确读出文本。(出于这个原因,文本转语音系统通常进行词性标注。) + +注意 + +**轮到你来**:很多单词,如`ski`和`race`,可以用作名词或动词而发音没有区别。你能想出其他的吗?提示:想想一个常见的东西,尝试把词`to`放到它前面,看它是否也是一个动词;或者想想一个动作,尝试把`the`放在它前面,看它是否也是一个名词。现在用这个词的两种用途造句,在这句话上运行词性标注器。 + +词汇类别如“名词”和词性标记如`NN`,看上去似乎有其用途,但在细节上将使许多读者感到晦涩。你可能想知道要引进这种额外的信息的理由是什么。很多这些类别源于对文本中单词分布的粗略分析。考虑下面的分析,涉及`woman`(名词),`bought`(动词),`over`(介词)和`the`(限定词)。`text.similar()`方法接收一个单词`w`,找出所有上下文`w[1]w w[2]`,然后找出所有出现在相同上下文中的词`w'`,即`w[1]w'w[2]`。 + +```py +>>> text = nltk.Text(word.lower() for word in nltk.corpus.brown.words()) +>>> text.similar('woman') +Building word-context index... +man day time year car moment world family house boy child country job +state girl place war way case question +>>> text.similar('bought') +made done put said found had seen given left heard been brought got +set was called felt in that told +>>> text.similar('over') +in on to of and for with from at by that into as up out down through +about all is +>>> text.similar('the') +a his this their its her an that our any all one these my in your no +some other and +``` + +可以观察到,搜索`woman`找到名词;搜索`bought`找到的大部分是动词;搜索`over`一般会找到介词;搜索`the`找到几个限定词。一个标注器能够正确识别一个句子的上下文中的这些词的标记,例如`The woman bought over $150,000 worth of clothes`。 + +一个标注器还可以为我们对未知词的认识建模,例如我们可以根据词根`scrobble`猜测`scrobbling`可能是一个动词,并有可能发生在`he was scrobbling`这样的上下文中。 + +## 2 已经标注的语料库 + +## 2.1 表示已经标注的词符 + +按照 NLTK 的约定,一个已标注的词符使用一个由词符和标记组成的元组来表示。我们可以使用函数`str2tuple()`从表示一个已标注的词符的标准字符串创建一个这样的特殊元组: + +```py +>>> tagged_token = nltk.tag.str2tuple('fly/NN') +>>> tagged_token +('fly', 'NN') +>>> tagged_token[0] +'fly' +>>> tagged_token[1] +'NN' +``` + +我们可以直接从一个字符串构造一个已标注的词符的列表。第一步是对字符串分词以便能访问单独的`单词/标记`字符串,然后将每一个转换成一个元组(使用`str2tuple()`)。 + +```py +>>> sent = ''' +... The/AT grand/JJ jury/NN commented/VBD on/IN a/AT number/NN of/IN +... other/AP topics/NNS ,/, AMONG/IN them/PPO the/AT Atlanta/NP and/CC +... Fulton/NP-tl County/NN-tl purchasing/VBG departments/NNS which/WDT it/PPS +... said/VBD ``/`` ARE/BER well/QL operated/VBN and/CC follow/VB generally/RB +... accepted/VBN practices/NNS which/WDT inure/VB to/IN the/AT best/JJT +... interest/NN of/IN both/ABX governments/NNS ''/'' ./. +... ''' +>>> [nltk.tag.str2tuple(t) for t in sent.split()] +[('The', 'AT'), ('grand', 'JJ'), ('jury', 'NN'), ('commented', 'VBD'), +('on', 'IN'), ('a', 'AT'), ('number', 'NN'), ... ('.', '.')] +``` + +## 2.2 读取已标注的语料库 + +NLTK 中包括的若干语料库已标注了词性。下面是一个你用文本编辑器打开一个布朗语料库的文件就能看到的例子: + + > The/at Fulton/np-tl County/nn-tl Grand/jj-tl Jury/nn-tl said/vbd Friday/nr an/at investigation/nn of/in Atlanta's/np$ recent/jj primary/nn election/nn produced/vbd `/` no/at evidence/nn ''/'' that/cs any/dti irregularities/nns took/vbd place/nn ./. + +其他语料库使用各种格式存储词性标记。NLTK 的语料库阅读器提供了一个统一的接口,使你不必理会这些不同的文件格式。与刚才提取并显示的上面的文件不同,布朗语料库的语料库阅读器按如下所示的方式表示数据。注意,词性标记已转换为大写的,自从布朗语料库发布以来这已成为标准的做法。 + +```py +>>> nltk.corpus.brown.tagged_words() +[('The', 'AT'), ('Fulton', 'NP-TL'), ...] +>>> nltk.corpus.brown.tagged_words(tagset='universal') +[('The', 'DET'), ('Fulton', 'NOUN'), ...] +``` + +只要语料库包含已标注的文本,NLTK 的语料库接口都将有一个`tagged_words()`方法。下面是一些例子,再次使用布朗语料库所示的输出格式: + +```py +>>> print(nltk.corpus.nps_chat.tagged_words()) +[('now', 'RB'), ('im', 'PRP'), ('left', 'VBD'), ...] +>>> nltk.corpus.conll2000.tagged_words() +[('Confidence', 'NN'), ('in', 'IN'), ('the', 'DT'), ...] +>>> nltk.corpus.treebank.tagged_words() +[('Pierre', 'NNP'), ('Vinken', 'NNP'), (',', ','), ...] +``` + +并非所有的语料库都采用同一组标记;看前面提到的标记集的帮助函数和`readme()`方法中的文档。最初,我们想避免这些标记集的复杂化,所以我们使用一个内置的到“通用标记集“的映射: + +```py +>>> nltk.corpus.brown.tagged_words(tagset='universal') +[('The', 'DET'), ('Fulton', 'NOUN'), ...] +>>> nltk.corpus.treebank.tagged_words(tagset='universal') +[('Pierre', 'NOUN'), ('Vinken', 'NOUN'), (',', '.'), ...] +``` + +NLTK 中还有其他几种语言的已标注语料库,包括中文,印地语,葡萄牙语,西班牙语,荷兰语和加泰罗尼亚语。这些通常含有非 ASCII 文本,当输出较大的结构如列表时,Python 总是以十六进制显示这些。 + +```py +>>> nltk.corpus.sinica_treebank.tagged_words() +[('ä', 'Neu'), ('åæ', 'Nad'), ('åç', 'Nba'), ...] +>>> nltk.corpus.indian.tagged_words() +[('মহিষের', 'NN'), ('সন্তান', 'NN'), (':', 'SYM'), ...] +>>> nltk.corpus.mac_morpho.tagged_words() +[('Jersei', 'N'), ('atinge', 'V'), ('m\xe9dia', 'N'), ...] +>>> nltk.corpus.conll2002.tagged_words() +[('Sao', 'NC'), ('Paulo', 'VMI'), ('(', 'Fpa'), ...] +>>> nltk.corpus.cess_cat.tagged_words() +[('El', 'da0ms0'), ('Tribunal_Suprem', 'np0000o'), ...] +``` + +如果你的环境设置正确,有适合的编辑器和字体,你应该能够以人可读的方式显示单个字符串。例如,2.1 显示的使用`nltk.corpus.indian`访问的数据。 + +![Images/tag-indian.png](Images/1c54b3124863d24d17b2edec4f1d47e5.jpg) + +图 2.1:四种印度语言的词性标注数据:孟加拉语、印地语、马拉地语和泰卢固语 + +如果语料库也被分割成句子,将有一个`tagged_sents()`方法将已标注的词划分成句子,而不是将它们表示成一个大列表。在我们开始开发自动标注器时,这将是有益的,因为它们在句子列表上被训练和测试,而不是词。 + +## 2.3 通用词性标记集 + +已标注的语料库使用许多不同的标记集约定来标注词汇。为了帮助我们开始,我们将看一看一个简化的标记集(2.1 中所示)。 + +表 2.1: + +通用词性标记集 + +```py +>>> from nltk.corpus import brown +>>> brown_news_tagged = brown.tagged_words(categories='news', tagset='universal') +>>> tag_fd = nltk.FreqDist(tag for (word, tag) in brown_news_tagged) +>>> tag_fd.most_common() +[('NOUN', 30640), ('VERB', 14399), ('ADP', 12355), ('.', 11928), ('DET', 11389), + ('ADJ', 6706), ('ADV', 3349), ('CONJ', 2717), ('PRON', 2535), ('PRT', 2264), + ('NUM', 2166), ('X', 106)] +``` + +注意 + +**轮到你来**:使用`tag_fd.plot(cumulative=True)`为上面显示的频率分布绘图。标注为上述列表中的前五个标记的词的百分比是多少? + +我们可以使用这些标记做强大的搜索,结合一个图形化的词性索引工具`nltk.app.concordance()`。用它来寻找任一单词和词性标记的组合,如`N N N N`, `hit/VD`, `hit/VN`或者`the ADJ man`。 + +## 2.4 名词 + +名词一般指的是人、地点、事情或概念,例如: `woman, Scotland, book, intelligence`。名词可能出现在限定词和形容词之后,可以是动词的主语或宾语,如 2.2 所示。 + +表 2.2: + +一些名词的句法模式 + +```py +>>> word_tag_pairs = nltk.bigrams(brown_news_tagged) +>>> noun_preceders = [a[1] for (a, b) in word_tag_pairs if b[1] == 'NOUN'] +>>> fdist = nltk.FreqDist(noun_preceders) +>>> [tag for (tag, _) in fdist.most_common()] +['NOUN', 'DET', 'ADJ', 'ADP', '.', 'VERB', 'CONJ', 'NUM', 'ADV', 'PRT', 'PRON', 'X'] +``` + +这证实了我们的断言,名词出现在限定词和形容词之后,包括数字形容词(数词,标注为`NUM`)。 + +## 2.5 动词 + +动词是用来描述事件和行动的词,例如 2.3 中的`fall, eat`。在一个句子中,动词通常表示涉及一个或多个名词短语所指示物的关系。 + +表 2.3: + +一些动词的句法模式 + +```py +>>> wsj = nltk.corpus.treebank.tagged_words(tagset='universal') +>>> word_tag_fd = nltk.FreqDist(wsj) +>>> [wt[0] for (wt, _) in word_tag_fd.most_common() if wt[1] == 'VERB'] +['is', 'said', 'are', 'was', 'be', 'has', 'have', 'will', 'says', 'would', + 'were', 'had', 'been', 'could', "'s", 'can', 'do', 'say', 'make', 'may', + 'did', 'rose', 'made', 'does', 'expected', 'buy', 'take', 'get', 'might', + 'sell', 'added', 'sold', 'help', 'including', 'should', 'reported', ...] +``` + +请注意,频率分布中计算的项目是词-标记对。由于词汇和标记是成对的,我们可以把词作作为条件,标记作为事件,使用条件-事件对的链表初始化一个条件频率分布。这让我们看到了一个给定的词的标记的频率顺序列表: + +```py +>>> cfd1 = nltk.ConditionalFreqDist(wsj) +>>> cfd1['yield'].most_common() +[('VERB', 28), ('NOUN', 20)] +>>> cfd1['cut'].most_common() +[('VERB', 25), ('NOUN', 3)] +``` + +我们可以颠倒配对的顺序,这样标记作为条件,词汇作为事件。现在我们可以看到对于一个给定的标记可能的词。我们将用《华尔街日报 》的标记集而不是通用的标记集来这样做: + +```py +>>> wsj = nltk.corpus.treebank.tagged_words() +>>> cfd2 = nltk.ConditionalFreqDist((tag, word) for (word, tag) in wsj) +>>> list(cfd2['VBN']) +['been', 'expected', 'made', 'compared', 'based', 'priced', 'used', 'sold', +'named', 'designed', 'held', 'fined', 'taken', 'paid', 'traded', 'said', ...] +``` + +要弄清`VBD`(过去式)和`VBN`(过去分词)之间的区别,让我们找到可以同是`VBD`和`VBN`的词汇,看看一些它们周围的文字: + +```py +>>> [w for w in cfd1.conditions() if 'VBD' in cfd1[w] and 'VBN' in cfd1[w]] +['Asked', 'accelerated', 'accepted', 'accused', 'acquired', 'added', 'adopted', ...] +>>> idx1 = wsj.index(('kicked', 'VBD')) +>>> wsj[idx1-4:idx1+1] +[('While', 'IN'), ('program', 'NN'), ('trades', 'NNS'), ('swiftly', 'RB'), + ('kicked', 'VBD')] +>>> idx2 = wsj.index(('kicked', 'VBN')) +>>> wsj[idx2-4:idx2+1] +[('head', 'NN'), ('of', 'IN'), ('state', 'NN'), ('has', 'VBZ'), ('kicked', 'VBN')] +``` + +在这种情况下,我们可以看到过去分词`kicked`前面是助动词`have`的形式。这是普遍真实的吗? + +注意 + +**轮到你来**:通过`list(cfd2['VN'])`指定一个过去分词的列表,尝试收集所有直接在列表中项目前面的词-标记对。 + +## 2.6 形容词和副词 + +另外两个重要的词类是形容词和副词。形容词修饰名词,可以作为修饰语(如`the large pizza`中的`large`),或者谓语(如`the pizza is large`)。英语形容词可以有内部结构(如`the falling stocks`中的`fall+ing`)。副词修饰动词,指定动词描述的事件的时间、方式、地点或方向(如`the stocks fell quickly`中的`quickly`)。副词也可以修饰的形容词(如`Mary's teacher was really nice`中的`really`)。 + +英语中还有几个封闭的词类,如介词,冠词(也常称为限定词)(如`the, a`),情态动词(如`should, may`)和人称代词(如`she, they`)。每个词典和语法对这些词的分类都不同。 + +注意 + +**轮到你来**:如果你对这些词性中的一些不确定,使用`nltk.app.concordance()`学习它们,或在 YouTube 看《Schoolhouse Rock!》语法视频,或者查询本章结束的进一步阅读一节。 + +## 2.7 未简化的标记 + +让我们找出每个名词类型中最频繁的名词。2.2 中的程序找出所有以`NN`开始的标记,并为每个标记提供了几个示例单词。你会看到有许多`NN`的变种;此外,大多数的标记都有后缀修饰符:`-NC`表示引用,`-HL`表示标题中的词,`-TL`表示标题(布朗标记的特征)。 + +```py +def findtags(tag_prefix, tagged_text): + cfd = nltk.ConditionalFreqDist((tag, word) for (word, tag) in tagged_text + if tag.startswith(tag_prefix)) + return dict((tag, cfd[tag].most_common(5)) for tag in cfd.conditions()) + +>>> tagdict = findtags('NN', nltk.corpus.brown.tagged_words(categories='news')) +>>> for tag in sorted(tagdict): +... print(tag, tagdict[tag]) +... +NN [('year', 137), ('time', 97), ('state', 88), ('week', 85), ('man', 72)] +NN$ [("year's", 13), ("world's", 8), ("state's", 7), ("nation's", 6), ("company's", 6)] +NN$-HL [("Golf's", 1), ("Navy's", 1)] +NN$-TL [("President's", 11), ("Army's", 3), ("Gallery's", 3), ("University's", 3), ("League's", 3)] +NN-HL [('sp.', 2), ('problem', 2), ('Question', 2), ('business', 2), ('Salary', 2)] +NN-NC [('eva', 1), ('aya', 1), ('ova', 1)] +NN-TL [('President', 88), ('House', 68), ('State', 59), ('University', 42), ('City', 41)] +NN-TL-HL [('Fort', 2), ('Dr.', 1), ('Oak', 1), ('Street', 1), ('Basin', 1)] +NNS [('years', 101), ('members', 69), ('people', 52), ('sales', 51), ('men', 46)] +NNS$ [("children's", 7), ("women's", 5), ("janitors'", 3), ("men's", 3), ("taxpayers'", 2)] +NNS$-HL [("Dealers'", 1), ("Idols'", 1)] +NNS$-TL [("Women's", 4), ("States'", 3), ("Giants'", 2), ("Bros.'", 1), ("Writers'", 1)] +NNS-HL [('comments', 1), ('Offenses', 1), ('Sacrifices', 1), ('funds', 1), ('Results', 1)] +NNS-TL [('States', 38), ('Nations', 11), ('Masters', 10), ('Rules', 9), ('Communists', 9)] +NNS-TL-HL [('Nations', 1)] +``` + +当我们开始在本章后续部分创建词性标注器时,我们将使用未简化的标记。 + +## 2.8 探索已标注的语料库 + +让我们简要地回过来探索语料库,我们在前面的章节中看到过,这次我们探索词性标记。 + +假设我们正在研究词`often`,想看看它是如何在文本中使用的。我们可以试着看看跟在`often`后面的词汇 + +```py +>>> brown_learned_text = brown.words(categories='learned') +>>> sorted(set(b for (a, b) in nltk.bigrams(brown_learned_text) if a == 'often')) +[',', '.', 'accomplished', 'analytically', 'appear', 'apt', 'associated', 'assuming', +'became', 'become', 'been', 'began', 'call', 'called', 'carefully', 'chose', ...] +``` + +然而,使用`tagged_words()`方法查看跟随词的词性标记可能更有指导性: + +```py +>>> brown_lrnd_tagged = brown.tagged_words(categories='learned', tagset='universal') +>>> tags = [b[1] for (a, b) in nltk.bigrams(brown_lrnd_tagged) if a[0] == 'often'] +>>> fd = nltk.FreqDist(tags) +>>> fd.tabulate() + PRT ADV ADP . VERB ADJ + 2 8 7 4 37 6 +``` + +请注意`often`后面最高频率的词性是动词。名词从来没有在这个位置出现(在这个特别的语料中)。 + +接下来,让我们看一些较大范围的上下文,找出涉及特定标记和词序列的词(在这种情况下,`">Verb> to >Verb>"`)。在`code-three-word-phrase`中,我们考虑句子中的每个三词窗口❶,检查它们是否符合我们的标准❷。如果标记匹配,我们输出对应的词❸。 + +```py +from nltk.corpus import brown +def process(sentence): + for (w1,t1), (w2,t2), (w3,t3) in nltk.trigrams(sentence): ❶ + if (t1.startswith('V') and t2 == 'TO' and t3.startswith('V')): ❷ + print(w1, w2, w3) ❸ + +>>> for tagged_sent in brown.tagged_sents(): +... process(tagged_sent) +... +combined to achieve +continue to place +serve to protect +wanted to wait +allowed to place +expected to become +... +``` + +最后,让我们看看与它们的标记关系高度模糊不清的词。了解为什么要标注这样的词是因为它们各自的上下文可以帮助我们弄清楚标记之间的区别。 + +```py +>>> brown_news_tagged = brown.tagged_words(categories='news', tagset='universal') +>>> data = nltk.ConditionalFreqDist((word.lower(), tag) +... for (word, tag) in brown_news_tagged) +>>> for word in sorted(data.conditions()): +... if len(data[word]) > 3: +... tags = [tag for (tag, _) in data[word].most_common()] +... print(word, ' '.join(tags)) +... +best ADJ ADV NP V +better ADJ ADV V DET +close ADV ADJ V N +cut V N VN VD +even ADV DET ADJ V +grant NP N V - +hit V VD VN N +lay ADJ V NP VD +left VD ADJ N VN +like CNJ V ADJ P - +near P ADV ADJ DET +open ADJ V N ADV +past N ADJ DET P +present ADJ ADV V N +read V VN VD NP +right ADJ N DET ADV +second NUM ADV DET N +set VN V VD N - +that CNJ V WH DET +``` + +注意 + +**轮到你来**:打开词性索引工具`nltk.app.concordance()`并加载完整的布朗语料库(简化标记集)。现在挑选一些上面代码例子末尾处列出的词,看看词的标记如何与词的上下文相关。例如搜索`near`会看到所有混合在一起的形式,搜索`near/ADJ`会看到它作为形容词使用,`near N`会看到只是名词跟在后面的情况,等等。更多的例子,请修改附带的代码,以便它列出的词具有三个不同的标签。 + +## 3 使用 Python 字典映射单词到其属性 + +正如我们已经看到,`(word, tag)`形式的一个已标注词是词和词性标记的关联。一旦我们开始做词性标注,我们将会创建分配一个标记给一个词的程序,标记是在给定上下文中最可能的标记。我们可以认为这个过程是从词到标记的映射。在 Python 中最自然的方式存储映射是使用所谓的字典数据类型(在其他的编程语言又称为关联数组或哈希数组)。在本节中,我们来看看字典,看它如何能表示包括词性在内的各种不同的语言信息。 + +## 3.1 索引列表 VS 字典 + +我们已经看到,文本在 Python 中被视为一个词列表。链表的一个重要的属性是我们可以通过给出其索引来“看”特定项目,例如`text1[100]`。请注意我们如何指定一个数字,然后取回一个词。我们可以把链表看作一种简单的表格,如 3.1 所示。 + +![Images/maps01.png](Images/e9d9a0887996a6bac6c52bb0bfaf9fdf.jpg) + +图 3.1:列表查找:一个整数索引帮助我们访问 Python 列表的内容。 + +对比这种情况与频率分布`(3)`,在那里我们指定一个词然后取回一个数字,如`fdist['monstrous']`,它告诉我们一个给定的词在文本中出现的次数。用词查询对任何使用过字典的人都很熟悉。3.2 展示一些更多的例子。 + +![Images/maps02.png](Images/484180fc6abc244116b30e57cb6c0cf5.jpg) + +图 3.2:字典查询:我们使用一个关键字,如某人的名字、一个域名或一个英文单词,访问一个字典的条目;字典的其他名字有映射、哈希表、哈希和关联数组。 + +在电话簿中,我们用名字查找一个条目得到一个数字。当我们在浏览器中输入一个域名,计算机查找它得到一个 IP 地址。一个词频表允许我们查一个词找出它在一个文本集合中的频率。在所有这些情况中,我们都是从名称映射到数字,而不是其他如列表那样的方式。总之,我们希望能够在任意类型的信息之间映射。3.1 列出了各种语言学对象以及它们的映射。 + +表 3.1: + +语言学对象从键到值的映射 + +```py +>>> pos = {} +>>> pos +{} +>>> pos['colorless'] = 'ADJ' ❶ +>>> pos +{'colorless': 'ADJ'} +>>> pos['ideas'] = 'N' +>>> pos['sleep'] = 'V' +>>> pos['furiously'] = 'ADV' +>>> pos ❷ +{'furiously': 'ADV', 'ideas': 'N', 'colorless': 'ADJ', 'sleep': 'V'} +``` + +所以,例如,❶说的是`colorless`的词性是形容词,或者更具体地说:在字典`pos`中,键`'colorless'`被分配了值`'ADJ'`。当我们检查`pos`的值时❷,我们看到一个键-值对的集合。一旦我们以这样的方式填充了字典,就可以使用键来检索值: + +```py +>>> pos['ideas'] +'N' +>>> pos['colorless'] +'ADJ' +``` + +当然,我们可能会无意中使用一个尚未分配值的键。 + +```py +>>> pos['green'] +Traceback (most recent call last): + File "", line 1, in ? +KeyError: 'green' +``` + +这就提出了一个重要的问题。与列表和字符串不同,我们可以用`len()`算出哪些整数是合法索引,我们如何算出一个字典的合法键?如果字典不是太大,我们可以简单地通过查看变量`pos`检查它的内容。正如在前面(❷行)所看到,这为我们提供了键-值对。请注意它们的顺序与最初放入它们的顺序不同;这是因为字典不是序列而是映射(参见 3.2),键没有固定地排序。 + +换种方式,要找到键,我们可以将字典转换成一个列表❶——要么在期望列表的上下文中使用字典,如作为`sorted()`的参数❷,要么在`for`循环中❸。 + +```py +>>> list(pos) ❶ +['ideas', 'furiously', 'colorless', 'sleep'] +>>> sorted(pos) ❷ +['colorless', 'furiously', 'ideas', 'sleep'] +>>> [w for w in pos if w.endswith('s')] ❸ +['colorless', 'ideas'] +``` + +注意 + +当你输入`list(pos)`时,你看到的可能会与这里显示的顺序不同。如果你想看到有序的键,只需要对它们进行排序。 + +与使用一个`for`循环遍历字典中的所有键一样,我们可以使用`for`循环输出列表: + +```py +>>> for word in sorted(pos): +... print(word + ":", pos[word]) +... +colorless: ADJ +furiously: ADV +sleep: V +ideas: N +``` + +最后,字典的方法`keys()`、`values()`和`items()`允许我们以单独的列表访问键、值以及键-值对。我们甚至可以排序元组❶,按它们的第一个元素排序(如果第一个元素相同,就使用它们的第二个元素)。 + +```py +>>> list(pos.keys()) +['colorless', 'furiously', 'sleep', 'ideas'] +>>> list(pos.values()) +['ADJ', 'ADV', 'V', 'N'] +>>> list(pos.items()) +[('colorless', 'ADJ'), ('furiously', 'ADV'), ('sleep', 'V'), ('ideas', 'N')] +>>> for key, val in sorted(pos.items()): ❶ +... print(key + ":", val) +... +colorless: ADJ +furiously: ADV +ideas: N +sleep: V +``` + +我们要确保当我们在字典中查找某词时,一个键只得到一个值。现在假设我们试图用字典来存储可同时作为动词和名词的词`sleep`: + +```py +>>> pos['sleep'] = 'V' +>>> pos['sleep'] +'V' +>>> pos['sleep'] = 'N' +>>> pos['sleep'] +'N' +``` + +最初,`pos['sleep']`给的值是`'V'`。但是,它立即被一个新值`'N'`覆盖。换句话说,字典中只能有`'sleep'`的一个条目。然而,有一个方法可以在该项目中存储多个值:我们使用一个列表值,例如`pos['sleep'] = ['N', 'V']`。事实上,这就是我们在 4 中看到的 CMU 发音字典,它为一个词存储多个发音。 + +## 3.3 定义字典 + +我们可以使用键-值对格式创建字典。有两种方式做这个,我们通常会使用第一个: + +```py +>>> pos = {'colorless': 'ADJ', 'ideas': 'N', 'sleep': 'V', 'furiously': 'ADV'} +>>> pos = dict(colorless='ADJ', ideas='N', sleep='V', furiously='ADV') +``` + +请注意,字典的键必须是不可改变的类型,如字符串和元组。如果我们尝试使用可变键定义字典会得到一个`TypeError`: + +```py +>>> pos = {['ideas', 'blogs', 'adventures']: 'N'} +Traceback (most recent call last): + File "", line 1, in +TypeError: list objects are unhashable +``` + +## 3.4 默认字典 + +如果我们试图访问一个不在字典中的键,会得到一个错误。然而,如果一个字典能为这个新键自动创建一个条目并给它一个默认值,如 0 或者一个空链表,将是有用的。由于这个原因,可以使用一种特殊的称为`defaultdict`的字典。为了使用它,我们必须提供一个参数,用来创建默认值,如`int`, `float`, `str`, `list`, `dict`, `tuple`。 + +```py +>>> from collections import defaultdict +>>> frequency = defaultdict(int) +>>> frequency['colorless'] = 4 +>>> frequency['ideas'] +0 +>>> pos = defaultdict(list) +>>> pos['sleep'] = ['NOUN', 'VERB'] +>>> pos['ideas'] +[] +``` + +注意 + +这些默认值实际上是将其他对象转换为指定类型的函数(例如`int("2")`, `list("2")`)。当它们不带参数被调用时——`int()`, `list()`——它们分别返回`0`和`[]` 。 + +前面的例子中指定字典项的默认值为一个特定的数据类型的默认值。然而,也可以指定任何我们喜欢的默认值,只要提供可以无参数的被调用产生所需值的函数的名子。让我们回到我们的词性的例子,创建一个任一条目的默认值是`'N'`的字典❶。当我们访问一个不存在的条目时❷,它会自动添加到字典❸。 + +```py +>>> pos = defaultdict(lambda: 'NOUN') ❶ +>>> pos['colorless'] = 'ADJ' +>>> pos['blog'] ❷ +'NOUN' +>>> list(pos.items()) +[('blog', 'NOUN'), ('colorless', 'ADJ')] # [_automatically-added] +``` + +注意 + +上面的例子使用一个 lambda 表达式,在 4.4 介绍过。这个 lambda 表达式没有指定参数,所以我们用不带参数的括号调用它。因此,下面的`f`和`g`的定义是等价的: + +```py +>>> f = lambda: 'NOUN' +>>> f() +'NOUN' +>>> def g(): +... return 'NOUN' +>>> g() +'NOUN' +``` + +让我们来看看默认字典如何被应用在较大规模的语言处理任务中。许多语言处理任务——包括标注——费很大力气来正确处理文本中只出现过一次的词。如果有一个固定的词汇和没有新词会出现的保证,它们会有更好的表现。在一个默认字典的帮助下,我们可以预处理一个文本,替换低频词汇为一个特殊的“超出词汇表”词符`UNK`。(你能不看下面的想出如何做吗?) + +我们需要创建一个默认字典,映射每个词为它们的替换词。最频繁的`n`个词将被映射到它们自己。其他的被映射到`UNK`。 + +```py +>>> alice = nltk.corpus.gutenberg.words('carroll-alice.txt') +>>> vocab = nltk.FreqDist(alice) +>>> v1000 = [word for (word, _) in vocab.most_common(1000)] +>>> mapping = defaultdict(lambda: 'UNK') +>>> for v in v1000: +... mapping[v] = v +... +>>> alice2 = [mapping[v] for v in alice] +>>> alice2[:100] +['UNK', 'Alice', "'", 's', 'UNK', 'in', 'UNK', 'by', 'UNK', 'UNK', 'UNK', +'UNK', 'CHAPTER', 'I', '.', 'UNK', 'the', 'Rabbit', '-', 'UNK', 'Alice', +'was', 'beginning', 'to', 'get', 'very', 'tired', 'of', 'sitting', 'by', +'her', 'sister', 'on', 'the', 'UNK', ',', 'and', 'of', 'having', 'nothing', +'to', 'do', ':', 'once', 'or', 'twice', 'she', 'had', 'UNK', 'into', 'the', +'book', 'her', 'sister', 'was', 'UNK', ',', 'but', 'it', 'had', 'no', +'pictures', 'or', 'UNK', 'in', 'it', ',', "'", 'and', 'what', 'is', 'the', +'use', 'of', 'a', 'book', ",'", 'thought', 'Alice', "'", 'without', +'pictures', 'or', 'conversation', "?'" ...] +>>> len(set(alice2)) +1001 +``` + +## 3.5 递增地更新字典 + +我们可以使用字典计数出现的次数,模拟 [fig-tally](./ch01.html#fig-tally) 所示的计数词汇的方法。首先初始化一个空的`defaultdict`,然后处理文本中每个词性标记。如果标记以前没有见过,就默认计数为零。每次我们遇到一个标记,就使用`+=`运算符递增它的计数。 + +```py +>>> from collections import defaultdict +>>> counts = defaultdict(int) +>>> from nltk.corpus import brown +>>> for (word, tag) in brown.tagged_words(categories='news', tagset='universal'): +... counts[tag] += 1 +... +>>> counts['NOUN'] +30640 +>>> sorted(counts) +['ADJ', 'PRT', 'ADV', 'X', 'CONJ', 'PRON', 'VERB', '.', 'NUM', 'NOUN', 'ADP', 'DET'] + +>>> from operator import itemgetter +>>> sorted(counts.items(), key=itemgetter(1), reverse=True) +[('NOUN', 30640), ('VERB', 14399), ('ADP', 12355), ('.', 11928), ...] +>>> [t for t, c in sorted(counts.items(), key=itemgetter(1), reverse=True)] +['NOUN', 'VERB', 'ADP', '.', 'DET', 'ADJ', 'ADV', 'CONJ', 'PRON', 'PRT', 'NUM', 'X'] +``` + +3.3 中的列表演示了一个重要的按值排序一个字典的习惯用法,来按频率递减顺序显示词汇。`sorted()`的第一个参数是要排序的项目,它是由一个词性标记和一个频率组成的元组的列表。第二个参数使用函数`itemgetter()`指定排序的键。在一般情况下,`itemgetter(n)`返回一个函数,这个函数可以在一些其他序列对象上被调用获得这个序列的第`n`个元素,例如: + +```py +>>> pair = ('NP', 8336) +>>> pair[1] +8336 +>>> itemgetter(1)(pair) +8336 +``` + +`sorted()`的最后一个参数指定项目是否应被按相反的顺序返回,即频率值递减。 + +在 3.3 的开头还有第二个有用的习惯用法,那里我们初始化一个`defaultdict`,然后使用`for`循环来更新其值。下面是一个示意版本: + +```py +>>> my_dictionary = defaultdict(_function to create default value_) +>>> for item in sequence: +... # my_dictionary[item_key] is updated with information about item +``` + +下面是这种模式的另一个示例,我们按它们最后两个字母索引词汇: + +```py +>>> last_letters = defaultdict(list) +>>> words = nltk.corpus.words.words('en') +>>> for word in words: +... key = word[-2:] +... last_letters[key].append(word) +... +>>> last_letters['ly'] +['abactinally', 'abandonedly', 'abasedly', 'abashedly', 'abashlessly', 'abbreviately', +'abdominally', 'abhorrently', 'abidingly', 'abiogenetically', 'abiologically', ...] +>>> last_letters['zy'] +['blazy', 'bleezy', 'blowzy', 'boozy', 'breezy', 'bronzy', 'buzzy', 'Chazy', ...] +``` + +下面的例子使用相同的模式创建一个颠倒顺序的词字典。(你可能会试验第 3 行来弄清楚为什么这个程序能运行。) + +```py +>>> anagrams = defaultdict(list) +>>> for word in words: +... key = ''.join(sorted(word)) +... anagrams[key].append(word) +... +>>> anagrams['aeilnrt'] +['entrail', 'latrine', 'ratline', 'reliant', 'retinal', 'trenail'] +``` + +由于积累这样的词是如此常用的任务,NLTK 提供一个创建`defaultdict(list)`更方便的方式,形式为`nltk.Index()`。 + +```py +>>> anagrams = nltk.Index((''.join(sorted(w)), w) for w in words) +>>> anagrams['aeilnrt'] +['entrail', 'latrine', 'ratline', 'reliant', 'retinal', 'trenail'] +``` + +注意 + +`nltk.Index`是一个支持额外初始化的`defaultdict(list)`。类似地,`nltk.FreqDist`本质上是一个额外支持初始化的`defaultdict(int)`(附带排序和绘图方法)。 + +## 3.6 复杂的键和值 + +我们可以使用具有复杂的键和值的默认字典。让我们研究一个词可能的标记的范围,给定词本身和它前一个词的标记。我们将看到这些信息如何被一个词性标注器使用。 + +```py +>>> pos = defaultdict(lambda: defaultdict(int)) +>>> brown_news_tagged = brown.tagged_words(categories='news', tagset='universal') +>>> for ((w1, t1), (w2, t2)) in nltk.bigrams(brown_news_tagged): ❶ +... pos[(t1, w2)][t2] += 1 ❷ +... +>>> pos[('DET', 'right')] ❸ +defaultdict(, {'ADJ': 11, 'NOUN': 5}) +``` + +这个例子使用一个字典,它的条目的默认值也是一个字典(其默认值是`int()`,即 0)。请注意我们如何遍历已标注语料库的双连词,每次遍历处理一个词-标记对❶。每次通过循环时,我们更新字典`pos`中的条目`(t1, w2)`,一个标记和它*后面*的词❷。当我们在`pos`中查找一个项目时,我们必须指定一个复合键❸,然后得到一个字典对象。一个词性标注器可以使用这些信息来决定词`right`,前面是一个限定词时,应标注为`ADJ`。 + +## 3.7 反转字典 + +字典支持高效查找,只要你想获得任意键的值。如果`d`是一个字典,`k`是一个键,输入`d[k]`,就立即获得值。给定一个值查找对应的键要慢一些和麻烦一些: + +```py +>>> counts = defaultdict(int) +>>> for word in nltk.corpus.gutenberg.words('milton-paradise.txt'): +... counts[word] += 1 +... +>>> [key for (key, value) in counts.items() if value == 32] +['brought', 'Him', 'virtue', 'Against', 'There', 'thine', 'King', 'mortal', +'every', 'been'] +``` + +如果我们希望经常做这样的一种“反向查找”,建立一个映射值到键的字典是有用的。在没有两个键具有相同的值情况,这是一个容易的事。只要得到字典中的所有键-值对,并创建一个新的值-键对字典。下一个例子演示了用键-值对初始化字典`pos`的另一种方式。 + +```py +>>> pos = {'colorless': 'ADJ', 'ideas': 'N', 'sleep': 'V', 'furiously': 'ADV'} +>>> pos2 = dict((value, key) for (key, value) in pos.items()) +>>> pos2['N'] +'ideas' +``` + +首先让我们将我们的词性字典做的更实用些,使用字典的`update()`方法加入再一些词到`pos`中,创建多个键具有相同的值的情况。这样一来,刚才看到的反向查找技术就将不再起作用(为什么不?)作为替代,我们不得不使用`append()`积累词和每个词性,如下所示: + +```py +>>> pos.update({'cats': 'N', 'scratch': 'V', 'peacefully': 'ADV', 'old': 'ADJ'}) +>>> pos2 = defaultdict(list) +>>> for key, value in pos.items(): +... pos2[value].append(key) +... +>>> pos2['ADV'] +['peacefully', 'furiously'] +``` + +现在,我们已经反转字典`pos`,可以查任意词性找到所有具有此词性的词。可以使用 NLTK 中的索引支持更容易的做同样的事,如下所示: + +```py +>>> pos2 = nltk.Index((value, key) for (key, value) in pos.items()) +>>> pos2['ADV'] +['peacefully', 'furiously'] +``` + +3.2 给出 Python 字典方法的总结。 + +表 3.2: + +Python 字典方法:常用的方法与字典相关习惯用法的总结。 + +```py +>>> from nltk.corpus import brown +>>> brown_tagged_sents = brown.tagged_sents(categories='news') +>>> brown_sents = brown.sents(categories='news') +``` + +## 4.1 默认标注器 + +最简单的标注器是为每个词符分配同样的标记。这似乎是一个相当平庸的一步,但它建立了标注器性能的一个重要的底线。为了得到最好的效果,我们用最有可能的标记标注每个词。让我们找出哪个标记是最有可能的(现在使用未简化标记集): + +```py +>>> tags = [tag for (word, tag) in brown.tagged_words(categories='news')] +>>> nltk.FreqDist(tags).max() +'NN' +``` + +现在我们可以创建一个将所有词都标注成`NN`的标注器。 + +```py +>>> raw = 'I do not like green eggs and ham, I do not like them Sam I am!' +>>> tokens = word_tokenize(raw) +>>> default_tagger = nltk.DefaultTagger('NN') +>>> default_tagger.tag(tokens) +[('I', 'NN'), ('do', 'NN'), ('not', 'NN'), ('like', 'NN'), ('green', 'NN'), +('eggs', 'NN'), ('and', 'NN'), ('ham', 'NN'), (',', 'NN'), ('I', 'NN'), +('do', 'NN'), ('not', 'NN'), ('like', 'NN'), ('them', 'NN'), ('Sam', 'NN'), +('I', 'NN'), ('am', 'NN'), ('!', 'NN')] +``` + +不出所料,这种方法的表现相当不好。在一个典型的语料库中,它只标注正确了八分之一的标识符,正如我们在这里看到的: + +```py +>>> default_tagger.evaluate(brown_tagged_sents) +0.13089484257215028 +``` + +默认的标注器给每一个单独的词分配标记,即使是之前从未遇到过的词。碰巧的是,一旦我们处理了几千词的英文文本之后,大多数新词都将是名词。正如我们将看到的,这意味着,默认标注器可以帮助我们提高语言处理系统的稳定性。我们将很快回来讲述这个。 + +## 4.2 正则表达式标注器 + +正则表达式标注器基于匹配模式分配标记给词符。例如,我们可能会猜测任一以`ed`结尾的词都是动词过去分词,任一以`'s`结尾的词都是名词所有格。可以用一个正则表达式的列表表示这些: + +```py +>>> patterns = [ +... (r'.*ing$', 'VBG'), # gerunds +... (r'.*ed$', 'VBD'), # simple past +... (r'.*es$', 'VBZ'), # 3rd singular present +... (r'.*ould$', 'MD'), # modals +... (r'.*\'s$', 'NN$'), # possessive nouns +... (r'.*s$', 'NNS'), # plural nouns +... (r'^-?[0-9]+(.[0-9]+)?$', 'CD'), # cardinal numbers +... (r'.*', 'NN') # nouns (default) +... ] +``` + +请注意,这些是顺序处理的,第一个匹配上的会被使用。现在我们可以建立一个标注器,并用它来标记一个句子。做完这一步会有约五分之一是正确的。 + +```py +>>> regexp_tagger = nltk.RegexpTagger(patterns) +>>> regexp_tagger.tag(brown_sents[3]) +[('``', 'NN'), ('Only', 'NN'), ('a', 'NN'), ('relative', 'NN'), ('handful', 'NN'), +('of', 'NN'), ('such', 'NN'), ('reports', 'NNS'), ('was', 'NNS'), ('received', 'VBD'), +("''", 'NN'), (',', 'NN'), ('the', 'NN'), ('jury', 'NN'), ('said', 'NN'), (',', 'NN'), +('``', 'NN'), ('considering', 'VBG'), ('the', 'NN'), ('widespread', 'NN'), ...] +>>> regexp_tagger.evaluate(brown_tagged_sents) +0.20326391789486245 +``` + +最终的正则表达式`.*`是一个全面捕捉的,标注所有词为名词。这与默认标注器是等效的(只是效率低得多)。除了作为正则表达式标注器的一部分重新指定这个,有没有办法结合这个标注器和默认标注器呢?我们将很快看到如何做到这一点。 + +注意 + +**轮到你来**:看看你能不能想出一些模式,提高上面所示的正则表达式标注器的性能。(请注意 1 描述部分自动化这类工作的方法。) + +## 4.3 查询标注器 + +很多高频词没有`NN`标记。让我们找出 100 个最频繁的词,存储它们最有可能的标记。然后我们可以使用这个信息作为“查找标注器”(NLTK `UnigramTagger`)的模型: + +```py +>>> fd = nltk.FreqDist(brown.words(categories='news')) +>>> cfd = nltk.ConditionalFreqDist(brown.tagged_words(categories='news')) +>>> most_freq_words = fd.most_common(100) +>>> likely_tags = dict((word, cfd[word].max()) for (word, _) in most_freq_words) +>>> baseline_tagger = nltk.UnigramTagger(model=likely_tags) +>>> baseline_tagger.evaluate(brown_tagged_sents) +0.45578495136941344 +``` + +现在应该并不奇怪,仅仅知道 100 个最频繁的词的标记就使我们能正确标注很大一部分词符(近一半,事实上)。让我们来看看它在一些未标注的输入文本上做的如何: + +```py +>>> sent = brown.sents(categories='news')[3] +>>> baseline_tagger.tag(sent) +[('``', '``'), ('Only', None), ('a', 'AT'), ('relative', None), +('handful', None), ('of', 'IN'), ('such', None), ('reports', None), +('was', 'BEDZ'), ('received', None), ("''", "''"), (',', ','), +('the', 'AT'), ('jury', None), ('said', 'VBD'), (',', ','), +('``', '``'), ('considering', None), ('the', 'AT'), ('widespread', None), +('interest', None), ('in', 'IN'), ('the', 'AT'), ('election', None), +(',', ','), ('the', 'AT'), ('number', None), ('of', 'IN'), +('voters', None), ('and', 'CC'), ('the', 'AT'), ('size', None), +('of', 'IN'), ('this', 'DT'), ('city', None), ("''", "''"), ('.', '.')] +``` + +许多词都被分配了一个`None`标签,因为它们不在 100 个最频繁的词之中。在这些情况下,我们想分配默认标记`NN`。换句话说,我们要先使用查找表,如果它不能指定一个标记就使用默认标注器,这个过程叫做回退`(5)`。我们可以做到这个,通过指定一个标注器作为另一个标注器的参数,如下所示。现在查找标注器将只存储名词以外的词的词-标记对,只要它不能给一个词分配标记,它将会调用默认标注器。 + +```py +>>> baseline_tagger = nltk.UnigramTagger(model=likely_tags, +... backoff=nltk.DefaultTagger('NN')) +``` + +让我们把所有这些放在一起,写一个程序来创建和评估具有一定范围的查找标注器 ,4.1。 + +```py +def performance(cfd, wordlist): + lt = dict((word, cfd[word].max()) for word in wordlist) + baseline_tagger = nltk.UnigramTagger(model=lt, backoff=nltk.DefaultTagger('NN')) + return baseline_tagger.evaluate(brown.tagged_sents(categories='news')) + +def display(): + import pylab + word_freqs = nltk.FreqDist(brown.words(categories='news')).most_common() + words_by_freq = [w for (w, _) in word_freqs] + cfd = nltk.ConditionalFreqDist(brown.tagged_words(categories='news')) + sizes = 2 ** pylab.arange(15) + perfs = [performance(cfd, words_by_freq[:size]) for size in sizes] + pylab.plot(sizes, perfs, '-bo') + pylab.title('Lookup Tagger Performance with Varying Model Size') + pylab.xlabel('Model Size') + pylab.ylabel('Performance') + pylab.show() +``` + +![Images/tag-lookup.png](Images/f093aaace735b4961dbf9fa7d5c8ca37.jpg) + +图 4.2:查找标注器 + +可以观察到,随着模型规模的增长,最初的性能增加迅速,最终达到一个稳定水平,这时模型的规模大量增加性能的提高很小。(这个例子使用`pylab`绘图软件包,在 4.8 讨论过)。 + +## 4.4 评估 + +在前面的例子中,你会注意到对准确性得分的强调。事实上,评估这些工具的表现是 NLP 的一个中心主题。回想 [fig-sds](./ch01.html#fig-sds) 中的处理流程;一个模块输出中的任何错误都在下游模块大大的放大。 + +我们对比人类专家分配的标记来评估一个标注器的表现。由于我们通常很难获得专业和公正的人的判断,所以使用黄金标准测试数据来代替。这是一个已经手动标注并作为自动系统评估标准而被接受的语料库。当标注器对给定词猜测的标记与黄金标准标记相同,标注器被视为是正确的。 + +当然,设计和实施原始的黄金标准标注的也是人。更深入的分析可能会显示黄金标准中的错误,或者可能最终会导致一个修正的标记集和更复杂的指导方针。然而,黄金标准就目前有关的自动标注器的评估而言被定义成“正确的”。 + +注意 + +开发一个已标注语料库是一个重大的任务。除了数据,它会产生复杂的工具、文档和实践,为确保高品质的标注。标记集和其他编码方案不可避免地依赖于一些理论主张,不是所有的理论主张都被共享,然而,语料库的创作者往往竭尽全力使他们的工作尽可能理论中立,以最大限度地提高其工作的有效性。我们将在 11 讨论创建一个语料库的挑战。 + +## 5 N 元标注 + +## 5.1 一元标注 + +一元标注器基于一个简单的统计算法:对每个标识符分配这个独特的标识符最有可能的标记。例如,它将分配标记`JJ`给词`frequent`的所有出现,因为`frequent`用作一个形容词(例如`a frequent word`)比用作一个动词(例如`I frequent this cafe`)更常见。一个一元标注器的行为就像一个查找标注器`(4)`,除了有一个更方便的建立它的技术,称为训练。在下面的代码例子中,我们训练一个一元标注器,用它来标注一个句子,然后评估: + +```py +>>> from nltk.corpus import brown +>>> brown_tagged_sents = brown.tagged_sents(categories='news') +>>> brown_sents = brown.sents(categories='news') +>>> unigram_tagger = nltk.UnigramTagger(brown_tagged_sents) +>>> unigram_tagger.tag(brown_sents[2007]) +[('Various', 'JJ'), ('of', 'IN'), ('the', 'AT'), ('apartments', 'NNS'), +('are', 'BER'), ('of', 'IN'), ('the', 'AT'), ('terrace', 'NN'), ('type', 'NN'), +(',', ','), ('being', 'BEG'), ('on', 'IN'), ('the', 'AT'), ('ground', 'NN'), +('floor', 'NN'), ('so', 'QL'), ('that', 'CS'), ('entrance', 'NN'), ('is', 'BEZ'), +('direct', 'JJ'), ('.', '.')] +>>> unigram_tagger.evaluate(brown_tagged_sents) +0.9349006503968017 +``` + +我们训练一个`UnigramTagger`,通过在我们初始化标注器时指定已标注的句子数据作为参数。训练过程中涉及检查每个词的标记,将所有词的最可能的标记存储在一个字典里面,这个字典存储在标注器内部。 + +## 5.2 分离训练和测试数据 + +现在,我们正在一些数据上训练一个标注器,必须小心不要在相同的数据上测试,如我们在前面的例子中的那样。一个只是记忆它的训练数据,而不试图建立一个一般的模型的标注器会得到一个完美的得分,但在标注新的文本时将是无用的。相反,我们应该分割数据,90% 为测试数据,其余 10% 为测试数据: + +```py +>>> size = int(len(brown_tagged_sents) * 0.9) +>>> size +4160 +>>> train_sents = brown_tagged_sents[:size] +>>> test_sents = brown_tagged_sents[size:] +>>> unigram_tagger = nltk.UnigramTagger(train_sents) +>>> unigram_tagger.evaluate(test_sents) +0.811721... +``` + +虽然得分更糟糕了,但是现在我们对这种标注器的用处有了更好的了解,如它在之前没有遇见的文本上的表现。 + +## 5.3 一般的 N 元标注 + +在基于一元处理一个语言处理任务时,我们使用上下文中的一个项目。标注的时候,我们只考虑当前的词符,与更大的上下文隔离。给定一个模型,我们能做的最好的是为每个词标注其*先验的*最可能的标记。这意味着我们将使用相同的标记标注一个词,如`wind`,不论它出现的上下文是`the wind`还是`to wind`。 + +一个 N 元标注器是一个一元标注器的一般化,它的上下文是当前词和它前面`n - 1`个标识符的词性标记,如图 5.1 所示。要选择的标记是圆圈里的`t[n]`,灰色阴影的是上下文。在 5.1 所示的 N 元标注器的例子中,我们让`n = 3`;也就是说,我们考虑当前词的前两个词的标记。一个 N 元标注器挑选在给定的上下文中最有可能的标记。 + +![Images/tag-context.png](Images/12573c3a9015654728fe798e170a3c50.jpg) + +图 5.1:标注器上下文 + +注意 + +1-gram 标注器是一元标注器另一个名称:即用于标注一个词符的上下文的只是词符本身。2-gram 标注器也称为*二元标注器*,3-gram 标注器也称为*三元标注器*。 + +`NgramTagger`类使用一个已标注的训练语料库来确定对每个上下文哪个词性标记最有可能。这里我们看 N 元标注器的一个特殊情况,二元标注器。首先,我们训练它,然后用它来标注未标注的句子: + +```py +>>> bigram_tagger = nltk.BigramTagger(train_sents) +>>> bigram_tagger.tag(brown_sents[2007]) +[('Various', 'JJ'), ('of', 'IN'), ('the', 'AT'), ('apartments', 'NNS'), +('are', 'BER'), ('of', 'IN'), ('the', 'AT'), ('terrace', 'NN'), +('type', 'NN'), (',', ','), ('being', 'BEG'), ('on', 'IN'), ('the', 'AT'), +('ground', 'NN'), ('floor', 'NN'), ('so', 'CS'), ('that', 'CS'), +('entrance', 'NN'), ('is', 'BEZ'), ('direct', 'JJ'), ('.', '.')] +>>> unseen_sent = brown_sents[4203] +>>> bigram_tagger.tag(unseen_sent) +[('The', 'AT'), ('population', 'NN'), ('of', 'IN'), ('the', 'AT'), ('Congo', 'NP'), +('is', 'BEZ'), ('13.5', None), ('million', None), (',', None), ('divided', None), +('into', None), ('at', None), ('least', None), ('seven', None), ('major', None), +('``', None), ('culture', None), ('clusters', None), ("''", None), ('and', None), +('innumerable', None), ('tribes', None), ('speaking', None), ('400', None), +('separate', None), ('dialects', None), ('.', None)] +``` + +请注意,二元标注器能够标注训练中它看到过的句子中的所有词,但对一个没见过的句子表现很差。只要遇到一个新词(如 13.5),就无法给它分配标记。它不能标注下面的词(如`million`),即使是在训练过程中看到过的,只是因为在训练过程中从来没有见过它前面有一个`None`标记的词。因此,标注器标注句子的其余部分也失败了。它的整体准确度得分非常低: + +```py +>>> bigram_tagger.evaluate(test_sents) +0.102063... +``` + +当`n`越大,上下文的特异性就会增加,我们要标注的数据中包含训练数据中不存在的上下文的几率也增大。这被称为*数据稀疏*问题,在 NLP 中是相当普遍的。因此,我们的研究结果的精度和覆盖范围之间需要有一个权衡(这与信息检索中的精度/召回权衡有关)。 + +小心! + +N 元标注器不应考虑跨越句子边界的上下文。因此,NLTK 的标注器被设计用于句子列表,其中一个句子是一个词列表。在一个句子的开始,`t[n - 1]`和前面的标记被设置为`None`。 + +## 5.4 组合标注器 + +解决精度和覆盖范围之间的权衡的一个办法是尽可能的使用更精确的算法,但却在很多时候落后于具有更广覆盖范围的算法。例如,我们可以按如下方式组合二元标注器、一元注器和一个默认标注器,如下: + +1. 尝试使用二元标注器标注标识符。 +2. 如果二元标注器无法找到一个标记,尝试一元标注器。 +3. 如果一元标注器也无法找到一个标记,使用默认标注器。 + +大多数 NLTK 标注器允许指定一个回退标注器。回退标注器自身可能也有一个回退标注器: + +```py +>>> t0 = nltk.DefaultTagger('NN') +>>> t1 = nltk.UnigramTagger(train_sents, backoff=t0) +>>> t2 = nltk.BigramTagger(train_sents, backoff=t1) +>>> t2.evaluate(test_sents) +0.844513... +``` + +注意 + +**轮到你来**:通过定义一个名为`t3`的`TrigramTagger`,扩展前面的例子,它是`t2`的回退标注器。 + +请注意,我们在标注器初始化时指定回退标注器,从而使训练能利用回退标注器。于是,在一个特定的上下文中,如果二元标注器将分配与它的一元回退标注器一样的标记,那么二元标注器丢弃训练的实例。这样保持尽可能小的二元标注器模型。我们可以进一步指定一个标注器需要看到一个上下文的多个实例才能保留它,例如`nltk.BigramTagger(sents, cutoff=2, backoff=t1)`将会丢弃那些只看到一次或两次的上下文。 + +## 5.5 标注生词 + +我们标注生词的方法仍然是回退到一个正则表达式标注器或一个默认标注器。这些都无法利用上下文。因此,如果我们的标注器遇到词`blog`,训练过程中没有看到过,它会分配相同的标记,不论这个词出现的上下文是`the blog`还是`to blog`。我们怎样才能更好地处理这些生词,或词汇表以外的项目? + +一个有用的基于上下文标注生词的方法是限制一个标注器的词汇表为最频繁的`n`个词,使用 3 中的方法替代每个其他的词为一个特殊的词 UNK。训练时,一个一元标注器可能会学到 UNK 通常是一个名词。然而,N 元标注器会检测它的一些其他标记中的上下文。例如,如果前面的词是`to`(标注为`TO`),那么 UNK 可能会被标注为一个动词。 + +## 5.6 存储标注器 + +在大语料库上训练一个标注器可能需要大量的时间。没有必要在每次我们需要的时候训练一个标注器,很容易将一个训练好的标注器保存到一个文件以后重复使用。让我们保存我们的标注器`t2`到文件`t2.pkl`。 + +```py +>>> from pickle import dump +>>> output = open('t2.pkl', 'wb') +>>> dump(t2, output, -1) +>>> output.close() +``` + +现在,我们可以在一个单独的 Python 进程中,我们可以载入保存的标注器。 + +```py +>>> from pickle import load +>>> input = open('t2.pkl', 'rb') +>>> tagger = load(input) +>>> input.close() +``` + +现在让我们检查它是否可以用来标注。 + +```py +>>> text = """The board's action shows what free enterprise +... is up against in our complex maze of regulatory laws .""" +>>> tokens = text.split() +>>> tagger.tag(tokens) +[('The', 'AT'), ("board's", 'NN$'), ('action', 'NN'), ('shows', 'NNS'), +('what', 'WDT'), ('free', 'JJ'), ('enterprise', 'NN'), ('is', 'BEZ'), +('up', 'RP'), ('against', 'IN'), ('in', 'IN'), ('our', 'PP$'), ('complex', 'JJ'), +('maze', 'NN'), ('of', 'IN'), ('regulatory', 'NN'), ('laws', 'NNS'), ('.', '.')] +``` + +## 5.7 准确性的极限 + +一个 N 元标注器准确性的上限是什么?考虑一个三元标注器的情况。它遇到多少词性歧义的情况?我们可以根据经验决定这个问题的答案: + +```py +>>> cfd = nltk.ConditionalFreqDist( +... ((x[1], y[1], z[0]), z[1]) +... for sent in brown_tagged_sents +... for x, y, z in nltk.trigrams(sent)) +>>> ambiguous_contexts = [c for c in cfd.conditions() if len(cfd[c]) > 1] +>>> sum(cfd[c].N() for c in ambiguous_contexts) / cfd.N() +0.049297702068029296 +``` + +因此,`1/20`的三元是有歧义的示例。给定当前单词及其前两个标记,根据训练数据,在 5% 的情况中,有一个以上的标记可能合理地分配给当前词。假设我们总是挑选在这种含糊不清的上下文中最有可能的标记,可以得出三元标注器准确性的一个下界。 + +调查标注器准确性的另一种方法是研究它的错误。有些标记可能会比别的更难分配,可能需要专门对这些数据进行预处理或后处理。一个方便的方式查看标注错误是混淆矩阵。它用图表表示期望的标记(黄金标准)与实际由标注器产生的标记: + +```py +>>> test_tags = [tag for sent in brown.sents(categories='editorial') +... for (word, tag) in t2.tag(sent)] +>>> gold_tags = [tag for (word, tag) in brown.tagged_words(categories='editorial')] +>>> print(nltk.ConfusionMatrix(gold_tags, test_tags)) +``` + +基于这样的分析,我们可能会决定修改标记集。或许标记之间很难做出的区分可以被丢弃,因为它在一些较大的处理任务的上下文中并不重要。 + +分析标注器准确性界限的另一种方式来自人类标注者之间并非 100% 的意见一致。 + +一般情况下,标注过程会损坏区别:例如当所有的人称代词被标注为`PRP`时,词的特性通常会失去。与此同时,标注过程引入了新的区别从而去除了含糊之处:例如`deal`标注为`VB`或`NN`。这种消除某些区别并引入新的区别的特点是标注的一个重要的特征,有利于分类和预测。当我们引入一个标记集的更细的划分时,在 N 元标注器决定什么样的标记分配给一个特定的词时,可以获得关于左侧上下文的更详细的信息。然而,标注器同时也将需要做更多的工作来划分当前的词符,只是因为有更多可供选择的标记。相反,使用较少的区别(如简化的标记集),标注器有关上下文的信息会减少,为当前词符分类的选择范围也较小。 + +我们已经看到,训练数据中的歧义导致标注器准确性的上限。有时更多的上下文能解决这些歧义。然而,在其他情况下,如(Church, Young, & Bloothooft, 1996)中指出的,只有参考语法或现实世界的知识,才能解决歧义。尽管有这些缺陷,词性标注在用统计方法进行自然语言处理的兴起过程中起到了核心作用。1990 年代初,统计标注器令人惊讶的精度是一个惊人的示范,可以不用更深的语言学知识解决一小部分语言理解问题,即词性消歧。这个想法能再推进吗?第 7 中,我们将看到,它可以。 + +## 6 基于转换的标注 + +N 元标注器的一个潜在的问题是它们的 N 元表(或语言模型)的大小。如果使用各种语言技术的标注器部署在移动计算设备上,在模型大小和标注器准确性之间取得平衡是很重要的。使用回退标注器的 N 元标注器可能存储三元和二元表,这是很大的稀疏阵列,可能有数亿条条目。 + +第二个问题是关于上下文。N 元标注器从前面的上下文中获得的唯一的信息是标记,虽然词本身可能是一个有用的信息源。N 元模型使用上下文中的词的其他特征为条件是不切实际的。在本节中,我们考察 Brill 标注,一种归纳标注方法,它的性能很好,使用的模型只有 N 元标注器的很小一部分。 + +Brill 标注是一种*基于转换的学习*,以它的发明者命名。一般的想法很简单:猜每个词的标记,然后返回和修复错误。在这种方式中,Brill 标注器陆续将一个不良标注的文本转换成一个更好的。与 N 元标注一样,这是有*监督的学习*方法,因为我们需要已标注的训练数据来评估标注器的猜测是否是一个错误。然而,不像 N 元标注,它不计数观察结果,只编制一个转换修正规则列表。 + +Brill 标注的的过程通常是与绘画类比来解释的。假设我们要画一棵树,包括大树枝、树枝、小枝、叶子和一个统一的天蓝色背景的所有细节。不是先画树然后尝试在空白处画蓝色,而是简单的将整个画布画成蓝色,然后通过在蓝色背景上上色“修正”树的部分。以同样的方式,我们可能会画一个统一的褐色的树干再回过头来用更精细的刷子画进一步的细节。Brill 标注使用了同样的想法:以大笔画开始,然后修复细节,一点点的细致的改变。让我们看看下面的例子: + +```py +>>> nltk.tag.brill.demo() +Training Brill tagger on 80 sentences... +Finding initial useful rules... + Found 6555 useful rules. + + B | + S F r O | Score = Fixed - Broken + c i o t | R Fixed = num tags changed incorrect -> correct + o x k h | u Broken = num tags changed correct -> incorrect + r e e e | l Other = num tags changed incorrect -> incorrect + e d n r | e +------------------+------------------------------------------------------- + 12 13 1 4 | NN -> VB if the tag of the preceding word is 'TO' + 8 9 1 23 | NN -> VBD if the tag of the following word is 'DT' + 8 8 0 9 | NN -> VBD if the tag of the preceding word is 'NNS' + 6 9 3 16 | NN -> NNP if the tag of words i-2...i-1 is '-NONE-' + 5 8 3 6 | NN -> NNP if the tag of the following word is 'NNP' + 5 6 1 0 | NN -> NNP if the text of words i-2...i-1 is 'like' + 5 5 0 3 | NN -> VBN if the text of the following word is '*-1' + ... +>>> print(open("errors.out").read()) + left context | word/test->gold | right context +--------------------------+------------------------+-------------------------- + | Then/NN->RB | ,/, in/IN the/DT guests/N +, in/IN the/DT guests/NNS | '/VBD->POS | honor/NN ,/, the/DT speed +'/POS honor/NN ,/, the/DT | speedway/JJ->NN | hauled/VBD out/RP four/CD +NN ,/, the/DT speedway/NN | hauled/NN->VBD | out/RP four/CD drivers/NN +DT speedway/NN hauled/VBD | out/NNP->RP | four/CD drivers/NNS ,/, c +dway/NN hauled/VBD out/RP | four/NNP->CD | drivers/NNS ,/, crews/NNS +hauled/VBD out/RP four/CD | drivers/NNP->NNS | ,/, crews/NNS and/CC even +P four/CD drivers/NNS ,/, | crews/NN->NNS | and/CC even/RB the/DT off +NNS and/CC even/RB the/DT | official/NNP->JJ | Indianapolis/NNP 500/CD a + | After/VBD->IN | the/DT race/NN ,/, Fortun +ter/IN the/DT race/NN ,/, | Fortune/IN->NNP | 500/CD executives/NNS dro +s/NNS drooled/VBD like/IN | schoolboys/NNP->NNS | over/IN the/DT cars/NNS a +olboys/NNS over/IN the/DT | cars/NN->NNS | and/CC drivers/NNS ./. +``` + +## 7 如何确定一个词的分类 + +我们已经详细研究了词类,现在转向一个更基本的问题:我们如何首先决定一个词属于哪一类?在一般情况下,语言学家使用形态学、句法和语义线索确定一个词的类别。 + +## 7.1 形态学线索 + +一个词的内部结构可能为这个词分类提供有用的线索。举例来说:`-ness`是一个后缀,与形容词结合产生一个名词,如`happy → happiness`, `ill → illness`。如果我们遇到的一个以`-ness`结尾的词,很可能是一个名词。同样的,`-ment`是与一些动词结合产生一个名词的后缀,如`govern → government`和`establish → establishment`。 + +英语动词也可以是形态复杂的。例如,一个动词的现在分词以`-ing`结尾,表示正在进行的还没有结束的行动(如`falling, eating`)。`-ing`后缀也出现在从动词派生的名词中,如`the falling of the leaves`(这被称为动名词)。 + +## 7.2 句法线索 + +另一个信息来源是一个词可能出现的典型的上下文语境。例如,假设我们已经确定了名词类。那么我们可以说,英语形容词的句法标准是它可以立即出现在一个名词前,或紧跟在词`be`或`very`后。根据这些测试,`near`应该被归类为形容词: + +```py +Statement User117 Dude..., I wanted some of that +ynQuestion User120 m I missing something? +Bye User117 I'm gonna go fix food, I'll be back later. +System User122 JOIN +System User2 slaps User122 around a bit with a large trout. +Statement User121 18/m pm me if u tryin to chat + +``` + +## 10 练习 + +1. ☼ 网上搜索“spoof newspaper headlines”,找到这种宝贝:`British Left Waffles on Falkland Islands`和`Juvenile Court to Try Shooting Defendant`。手工标注这些头条,看看词性标记的知识是否可以消除歧义。 +2. ☼ 和别人一起,轮流挑选一个既可以是名词也可以是动词的词(如`contest`);让对方预测哪一个可能是布朗语料库中频率最高的;检查对方的预测,为几个回合打分。 +3. ☼ 分词和标注下面的句子:`They wind back the clock, while we chase after the wind`。涉及哪些不同的发音和词类? +4. ☼ 回顾 3.1 中的映射。讨论你能想到的映射的其他的例子。它们从什么类型的信息映射到什么类型的信息? +5. ☼ 在交互模式下使用 Python 解释器,实验本章中字典的例子。创建一个字典`d`,添加一些条目。如果你尝试访问一个不存在的条目会发生什么,如`d['xyz']`? +6. ☼ 尝试从字典`d`删除一个元素,使用语法`del d['abc']`。检查被删除的项目。 +7. ☼ 创建两个字典,`d1`和`d2`,为每个添加一些条目。现在发出命令`d1.update(d2)`。这做了什么?它可能是有什么用? +8. ☼ 创建一个字典`e`,表示你选择的一些词的一个单独的词汇条目。定义键如`headword`、`part-of-speech`、`sense`和`example`,分配给它们适当的值。 +9. ☼ 自己验证`go`和`went`在分布上的限制,也就是说,它们不能自由地在 7 中的`(3d)`演示的那种上下文中互换。 +10. ☼ 训练一个一元标注器,在一些新的文本上运行。观察有些词没有分配到标记。为什么没有? +11. ☼ 了解词缀标注器(输入`help(nltk.AffixTagger)`)。训练一个词缀标注器,在一些新的文本上运行。设置不同的词缀长度和最小词长做实验。讨论你的发现。 +12. ☼ 训练一个没有回退标注器的二元标注器,在一些训练数据上运行。下一步,在一些新的数据运行它。标注器的准确性会发生什么?为什么呢? +13. ☼ 我们可以使用字典指定由一个格式化字符串替换的值。阅读关于格式化字符串的 Python 库文档`http://docs.python.org/lib/typesseq-strings.html`,使用这种方法以两种不同的格式显示今天的日期。 +14. ◑ 使用`sorted()`和`set()`获得布朗语料库使用的标记的排序的列表,删除重复。 +15. ◑ 写程序处理布朗语料库,找到以下问题的答案: + 1. 哪些名词常以它们复数形式而不是它们的单数形式出现?(只考虑常规的复数形式,`-s`后缀形式的)。 + 2. 哪个词的不同标记数目最多。它们是什么,它们代表什么? + 3. 按频率递减的顺序列出标记。前 20 个最频繁的标记代表什么? + 4. 名词后面最常见的是哪些标记?这些标记代表什么? +16. ◑ 探索有关查找标注器的以下问题: + 1. 回退标注器被省略时,模型大小变化,标注器的准确性会发生什么? + 2. 思考 4.2 的曲线;为查找标注器推荐一个平衡内存和准确性的好的规模。你能想出在什么情况下应该尽量减少内存使用,什么情况下性能最大化而不必考虑内存使用? +17. ◑ 查找标注器的准确性上限是什么,假设其表的大小没有限制?(提示:写一个程序算出被分配了最有可能的标记的词的词符的平均百分比。) +18. ◑ 生成已标注数据的一些统计数据,回答下列问题: + 1. 总是被分配相同词性的词类的比例是多少? + 2. 多少词是有歧义的,从某种意义上说,它们至少和两个标记一起出现? + 3. 布朗语料库中这些有歧义的词的*词符*的百分比是多少? +19. ◑ `evaluate()`方法算出一个文本上运行的标注器的精度。例如,如果提供的已标注文本是`[('the', 'DT'), ('dog', 'NN')]`,标注器产生的输出是`[('the', 'NN'), ('dog', 'NN')]`,那么得分为`0.5`。让我们尝试找出评价方法是如何工作的: + 1. 一个标注器`t`将一个词汇列表作为输入,产生一个已标注词列表作为输出。然而,`t.evaluate()`只以一个正确标注的文本作为唯一的参数。执行标注之前必须对输入做些什么? + 2. 一旦标注器创建了新标注的文本,`evaluate()`方法可能如何比较它与原来标注的文本,计算准确性得分? + 3. 现在,检查源代码来看看这个方法是如何实现的。检查`nltk.tag.api.__file__`找到源代码的位置,使用编辑器打开这个文件(一定要使用文件`api.py`,而不是编译过的二进制文件`api.pyc`)。 +20. ◑ 编写代码,搜索布朗语料库,根据标记查找特定的词和短语,回答下列问题: + 1. 产生一个标注为`MD`的不同的词的按字母顺序排序的列表。 + 2. 识别可能是复数名词或第三人称单数动词的词(如 `deals, flies`)。 + 3. 识别三个词的介词短语形式`IN + DET + NN`(如`in the lab`)。 + 4. 男性与女性代词的比例是多少? +21. ◑ 在 3.1 中我们看到动词`adore, love, like, prefer`及前面的限定符`absolutely`和`definitely`的频率计数的表格。探讨这四个动词前出现的所有限定符。 +22. ◑ 我们定义可以用来做生词的回退标注器的`regexp_tagger`。这个标注器只检查基数词。通过特定的前缀或后缀字符串进行测试,它应该能够猜测其他标记。例如,我们可以标注所有`-s`结尾的词为复数名词。定义一个正则表达式标注器(使用`RegexpTagger()`),测试至少 5 个单词拼写的其他模式。(使用内联文档解释规则。) +23. ◑ 考虑上一练习中开发的正则表达式标注器。使用它的`accuracy()`方法评估标注器,尝试想办法提高其性能。讨论你的发现。客观的评估如何帮助开发过程? +24. ◑ 数据稀疏问题有多严重?调查 N 元标注器当`n`从 1 增加到 6 时的准确性。为准确性得分制表。估计这些标注器需要的训练数据,假设词汇量大小为`10^5`而标记集的大小为`10^2`。 +25. ◑ 获取另一种语言的一些已标注数据,在其上测试和评估各种标注器。如果这种语言是形态复杂的,或者有词类的任何字形线索(如),可以考虑为它开发一个正则表达式标注器(排在一元标注器之后,默认标注器之前)。对比同样的运行在英文数据上的标注器,你的标注器的准确性如何?讨论你在运用这些方法到这种语言时遇到的问题。 +26. ◑ 4.1 绘制曲线显示查找标注器的性能随模型的大小增加的变化。绘制当训练数据量变化时一元标注器的性能曲线。 +27. ◑ 检查 5 中定义的二元标注器`t2`的混淆矩阵,确定简化的一套或多套标记。定义字典做映射,在简化的数据上评估标注器。 +28. ◑ 使用简化的标记集测试标注器(或制作一个你自己的,通过丢弃每个标记名中除第一个字母外所有的字母)。这种标注器需要做的区分更少,但由它获得的信息也更少。讨论你的发现。 +29. ◑ 回顾一个二元标注器训练过程中遇到生词,标注句子的其余部分为`None`的例子。一个二元标注器可能只处理了句子的一部分就失败了,即使句子中没有包含生词(即使句子在训练过程中使用过)。在什么情况下会出现这种情况呢?你可以写一个程序,找到一些这方面的例子吗? +30. ◑ 预处理布朗新闻数据,替换低频词为 UNK,但留下标记不变。在这些数据上训练和评估一个二元标注器。这样有多少帮助?一元标注器和默认标注器的贡献是什么? +31. ◑ 修改 4.1 中的程序,通过将`pylab.plot()`替换为`pylab.semilogx()`,在 *x* 轴上使用对数刻度。关于结果图形的形状,你注意到了什么?梯度告诉你什么呢? +32. ◑ 使用`help(nltk.tag.brill.demo)`阅读 Brill 标注器演示函数的文档。通过设置不同的参数值试验这个标注器。是否有任何训练时间(语料库大小)和性能之间的权衡? +33. ◑ 写代码构建一个集合的字典的字典。用它来存储一套可以跟在具有给定词性标记的给定词后面的词性标记,例如`word[i] → tag[i] → tag[i + 1]`。 +34. ★ 布朗语料库中有 264 个不同的词有 3 种可能的标签。 + 1. 打印一个表格,一列中是整数`1..10`,另一列是语料库中有`1..10`个不同标记的不同词的数目。 + 2. 对有不同的标记数量最多的词,输出语料库中包含这个词的句子,每个可能的标记一个。 +35. ★ 写一个程序,按照词`must`后面的词的标记为它的上下文分类。这样可以区分`must`的“必须”和“应该”两种词意上的用法吗? +36. ★ 创建一个正则表达式标注器和各种一元以及 N 元标注器,包括回退,在布朗语料库上训练它们。 + 1. 创建这些标注器的 3 种不同组合。测试每个组合标注器的准确性。哪种组合效果最好? + 2. 尝试改变训练语料的规模。它是如何影响你的结果的? +37. ★ 我们标注生词的方法一直要考虑这个词的字母(使用`RegexpTagger()`),或完全忽略这个词,将它标注为一个名词(使用`nltk.DefaultTagger()`)。这些方法对于有新词却不是名词的文本不会很好。思考句子`I like to blog on Kim's blog`。如果`blog`是一个新词,那么查看前面的标记(`TO`和`NP`)即我们需要一个对前面的标记敏感的默认标注器。 + 1. 创建一种新的一元标注器,查看前一个词的标记,而忽略当前词。(做到这一点的最好办法是修改`UnigramTagger()`的源代码,需要 Python 中的面向对象编程的知识。 + 2. 将这个标注器加入到回退标注器序列(包括普通的三元和二元标注器),放在常用默认标注器的前面。 + 3. 评价这个新的一元标注器的贡献。 +38. ★ 思考 5 中的代码,它确定一个三元标注器的准确性上限。回顾 Abney 的关于精确标注的不可能性的讨论(Church, Young, & Bloothooft, 1996)。解释为什么正确标注这些例子需要获取词和标记以外的其他种类的信息。你如何估计这个问题的规模? +39. ★ 使用`nltk.probability`中的一些估计技术,例如 *Lidstone* 或 *Laplace* 估计,开发一种统计标注器,它在训练中没有遇到而测试中遇到的上下文中表现优于 N 元回退标注器。 +40. ★ 检查 Brill 标注器创建的诊断文件`rules.out`和`errors.out`。通过访问源代码(`http://www.nltk.org/code`)获得演示代码,创建你自己版本的 Brill 标注器。并根据你从检查`rules.out`了解到的,删除一些规则模板。增加一些新的规则模板,这些模板使用那些可能有助于纠正你在`errors.out`看到的错误的上下文。 +41. ★ 开发一个 N 元回退标注器,允许在标注器初始化时指定逆 N 元组,如`["the", "the"]`。一个逆 N 元组被分配一个数字 0,被用来防止这个 N 元回退(如避免估计`P(the | the)`而只做`P(the)`)。 +42. ★ 使用布朗语料库开发标注器时,调查三种不同的方式来定义训练和测试数据之间的分割:类别(`category`)、来源(`fileid`)和句子。比较它们的相对性能,并讨论哪种方法最合理。(你可能要使用`n`交叉验证,在 3 中讨论的,以提高评估的准确性。) +43. ★ 开发你自己的`NgramTagger`,从 NLTK 中的类继承,封装本章中所述的已标注的训练和测试数据的词汇表缩减方法。确保一元和默认回退标注器有机会获得全部词汇。 + +## 关于本文档... + +针对 NLTK 3.0 作出更新。本章来自于《Python 自然语言处理》,[Steven Bird](http://estive.net/), [Ewan Klein](http://homepages.inf.ed.ac.uk/ewan/) 和 [Edward Loper](http://ed.loper.org/),Copyright © 2014 作者所有。本章依据 [*Creative Commons Attribution-Noncommercial-No Derivative Works 3\.0 United States License*](http://creativecommons.org/licenses/by-nc-nd/3.0/us/) 条款,与[*自然语言工具包*](http://nltk.org/) 3.0 版一起发行。 + +本文档构建于星期三 2015 年 7 月 1 日 12:30:05 AEST \ No newline at end of file diff --git a/docs/nlp-py-2e/6.md b/docs/nlp-py-2e/6.md new file mode 100644 index 0000000000000000000000000000000000000000..84fe1a9c0ec16982e8d258cda6e8f4e6d90de9b0 --- /dev/null +++ b/docs/nlp-py-2e/6.md @@ -0,0 +1,802 @@ +# 6 学习分类文本 + +模式识别是自然语言处理的一个核心部分。以`-ed`结尾的词往往是过去时态动词`(5)`。频繁使用`will`是新闻文本的暗示`(3)`。这些可观察到的模式——词的结构和词频——恰好与特定方面的含义关联,如时态和主题。但我们怎么知道从哪里开始寻找,形式的哪一方面关联含义的哪一方面? + +本章的目的是要回答下列问题: + +1. 我们怎样才能识别语言数据中能明显用于对其分类的特征? +2. 我们怎样才能构建语言模型,用于自动执行语言处理任务? +3. 从这些模型中我们可以学到哪些关于语言的知识? + +一路上,我们将研究一些重要的机器学习技术,包括决策树、朴素贝叶斯分类器和最大熵分类。我们会掩盖这些技术的数学和统计的基础,集中关注如何以及何时使用它们(更多的技术背景知识见进一步阅读一节)。在看这些方法之前,我们首先需要知道这个主题的范围十分广泛。 + +## 1 有监督分类 + +分类是为给定的输入选择正确的类标签的任务。在基本的分类任务中,每个输入被认为是与所有其它输入隔离的,并且标签集是预先定义的。这里是分类任务的一些例子: + +* 判断一封电子邮件是否是垃圾邮件。 +* 从一个固定的主题领域列表中,如“体育”、“技术”和“政治”,决定新闻报道的主题是什么。 +* 决定词`bank`给定的出现是用来指河的坡岸、一个金融机构、向一边倾斜的动作还是在金融机构里的存储行为。 + +基本的分类任务有许多有趣的变种。例如,在多类分类中,每个实例可以分配多个标签;在开放性分类中,标签集是事先没有定义的;在序列分类中,一个输入列表作为一个整体分类。 + +一个分类称为有监督的,如果它的建立基于训练语料的每个输入包含正确标签。有监督分类使用的框架图如 1.1 所示。 + +![Images/supervised-classification.png](Images/fb1a02fe3607a0deb452086296fd6f69.jpg) + +图 1.1:有监督分类。(a)在训练过程中,特征提取器用来将每一个输入值转换为特征集。这些特征集捕捉每个输入中应被用于对其分类的基本信息,我们将在下一节中讨论它。特征集与标签的配对被送入机器学习算法,生成模型。(b)在预测过程中,相同的特征提取器被用来将未见过的输入转换为特征集。之后,这些特征集被送入模型产生预测标签。 + +在本节的其余部分,我们将着眼于分类器如何能够解决各种各样的任务。我们讨论的目的不是要范围全面,而是给出在文本分类器的帮助下执行的任务的一个代表性的例子。 + +## 1.1 性别鉴定 + +在 4 中,我们看到,男性和女性的名字有一些鲜明的特点。以`a, e, i`结尾的很可能是女性,而以`k, o, r, s`和`t`结尾的很可能是男性。让我们建立一个分类器更精确地模拟这些差异。 + +创建一个分类器的第一步是决定输入的什么样的特征是相关的,以及如何为那些特征编码。在这个例子中,我们一开始只是寻找一个给定的名称的最后一个字母。以下特征提取器函数建立一个字典,包含有关给定名称的相关信息: + +```py +>>> def gender_features(word): +... return {'last_letter': word[-1]} +>>> gender_features('Shrek') +{'last_letter': 'k'} +``` + +这个函数返回的字典被称为特征集,映射特征名称到它们的值。特征名称是区分大小写的字符串,通常提供一个简短的人可读的特征描述,例如本例中的`'last_letter'`。特征值是简单类型的值,如布尔、数字和字符串。 + +注意 + +大多数分类方法要求特征使用简单的类型进行编码,如布尔类型、数字和字符串。但要注意仅仅因为一个特征是简单类型,并不一定意味着该特征的值易于表达或计算。的确,它可以用非常复杂的和有信息量的值作为特征,如第 2 个有监督分类器的输出。 + +现在,我们已经定义了一个特征提取器,我们需要准备一个例子和对应类标签的列表。 + +```py +>>> from nltk.corpus import names +>>> labeled_names = ([(name, 'male') for name in names.words('male.txt')] + +... [(name, 'female') for name in names.words('female.txt')]) +>>> import random +>>> random.shuffle(labeled_names) +``` + +接下来,我们使用特征提取器处理`names`数据,并划分特征集的结果链表为一个训练集和一个测试集。训练集用于训练一个新的“朴素贝叶斯”分类器。 + +```py +>>> featuresets = [(gender_features(n), gender) for (n, gender) in labeled_names] +>>> train_set, test_set = featuresets[500:], featuresets[:500] +>>> classifier = nltk.NaiveBayesClassifier.train(train_set) +``` + +在本章的后面,我们将学习更多关于朴素贝叶斯分类器的内容。现在,让我们只是在上面测试一些没有出现在训练数据中的名字: + +```py +>>> classifier.classify(gender_features('Neo')) +'male' +>>> classifier.classify(gender_features('Trinity')) +'female' +``` + +请看《黑客帝国》中这些角色的名字被正确分类。尽管这部科幻电影的背景是在 2199 年,但它仍然符合我们有关名字和性别的预期。我们可以在大数据量的未见过的数据上系统地评估这个分类器: + +```py +>>> print(nltk.classify.accuracy(classifier, test_set)) +0.77 +``` + +最后,我们可以检查分类器,确定哪些特征对于区分名字的性别是最有效的: + +```py +>>> classifier.show_most_informative_features(5) +Most Informative Features + last_letter = 'a' female : male = 33.2 : 1.0 + last_letter = 'k' male : female = 32.6 : 1.0 + last_letter = 'p' male : female = 19.7 : 1.0 + last_letter = 'v' male : female = 18.6 : 1.0 + last_letter = 'f' male : female = 17.3 : 1.0 +``` + +此列表显示训练集中以`a`结尾的名字中女性是男性的 38 倍,而以`k`结尾名字中男性是女性的 31 倍。这些比率称为似然比,可以用于比较不同特征-结果关系。 + +注意 + +**轮到你来**:修改`gender_features()`函数,为分类器提供名称的长度、它的第一个字母以及任何其他看起来可能有用的特征。用这些新特征重新训练分类器,并测试其准确性。 + +在处理大型语料库时,构建一个包含每一个实例的特征的单独的列表会使用大量的内存。在这些情况下,使用函数`nltk.classify.apply_features`,返回一个行为像一个列表而不会在内存存储所有特征集的对象: + +```py +>>> from nltk.classify import apply_features +>>> train_set = apply_features(gender_features, labeled_names[500:]) +>>> test_set = apply_features(gender_features, labeled_names[:500]) +``` + +## 1.2 选择正确的特征 + +选择相关的特征,并决定如何为一个学习方法编码它们,这对学习方法提取一个好的模型可以产生巨大的影响。建立一个分类器的很多有趣的工作之一是找出哪些特征可能是相关的,以及我们如何能够表示它们。虽然使用相当简单而明显的特征集往往可以得到像样的性能,但是使用精心构建的基于对当前任务的透彻理解的特征,通常会显著提高收益。 + +典型地,特征提取通过反复试验和错误的过程建立的,由哪些信息是与问题相关的直觉指引的。它通常以“厨房水槽”的方法开始,包括你能想到的所有特征,然后检查哪些特征是实际有用的。我们在 1.2 中对名字性别特征采取这种做法。 + +```py +def gender_features2(name): + features = {} + features["first_letter"] = name[0].lower() + features["last_letter"] = name[-1].lower() + for letter in 'abcdefghijklmnopqrstuvwxyz': + features["count({})".format(letter)] = name.lower().count(letter) + features["has({})".format(letter)] = (letter in name.lower()) + return features +``` + +然而,你要用于一个给定的学习算法的特征的数目是有限的——如果你提供太多的特征,那么该算法将高度依赖你的训练数据的特性,而一般化到新的例子的效果不会很好。这个问题被称为过拟合,当运作在小训练集上时尤其会有问题。例如,如果我们使用 1.2 中所示的特征提取器训练朴素贝叶斯分类器,将会过拟合这个相对较小的训练集,造成这个系统的精度比只考虑每个名字最后一个字母的分类器的精度低约 1%: + +```py +>>> featuresets = [(gender_features2(n), gender) for (n, gender) in labeled_names] +>>> train_set, test_set = featuresets[500:], featuresets[:500] +>>> classifier = nltk.NaiveBayesClassifier.train(train_set) +>>> print(nltk.classify.accuracy(classifier, test_set)) +0.768 +``` + +一旦初始特征集被选定,完善特征集的一个非常有成效的方法是错误分析。首先,我们选择一个开发集,包含用于创建模型的语料数据。然后将这种开发集分为训练集和开发测试集。 + +```py +>>> train_names = labeled_names[1500:] +>>> devtest_names = labeled_names[500:1500] +>>> test_names = labeled_names[:500] +``` + +训练集用于训练模型,开发测试集用于进行错误分析。测试集用于系统的最终评估。由于下面讨论的原因,我们将一个单独的开发测试集用于错误分析而不是使用测试集是很重要的。在 1.3 中显示了将语料数据划分成不同的子集。 + +![Images/corpus-org.png](Images/86cecc4bd139ddaf1a5daf9991f39945.jpg) + +图 1.3:用于训练有监督分类器的语料数据组织图。语料数据分为两类:开发集和测试集。开发集通常被进一步分为训练集和开发测试集。 + +已经将语料分为适当的数据集,我们使用训练集训练一个模型❶,然后在开发测试集上运行❷。 + +```py +>>> train_set = [(gender_features(n), gender) for (n, gender) in train_names] +>>> devtest_set = [(gender_features(n), gender) for (n, gender) in devtest_names] +>>> test_set = [(gender_features(n), gender) for (n, gender) in test_names] +>>> classifier = nltk.NaiveBayesClassifier.train(train_set) ❶ +>>> print(nltk.classify.accuracy(classifier, devtest_set)) ❷ +0.75 +``` + +使用开发测试集,我们可以生成一个分类器预测名字性别时的错误列表: + +```py +>>> errors = [] +>>> for (name, tag) in devtest_names: +... guess = classifier.classify(gender_features(name)) +... if guess != tag: +... errors.append( (tag, guess, name) ) +``` + +然后,可以检查个别错误案例,在那里该模型预测了错误的标签,尝试确定什么额外信息将使其能够作出正确的决定(或者现有的哪部分信息导致其做出错误的决定)。然后可以相应的调整特征集。我们已经建立的名字分类器在开发测试语料上产生约 100 个错误: + +```py +>>> for (tag, guess, name) in sorted(errors): +... print('correct={:<8} guess={:<8s} name={:<30}'.format(tag, guess, name)) +correct=female guess=male name=Abigail + ... +correct=female guess=male name=Cindelyn + ... +correct=female guess=male name=Katheryn +correct=female guess=male name=Kathryn + ... +correct=male guess=female name=Aldrich + ... +correct=male guess=female name=Mitch + ... +correct=male guess=female name=Rich + ... +``` + +浏览这个错误列表,它明确指出一些多个字母的后缀可以指示名字性别。例如,`yn`结尾的名字显示以女性为主,尽管事实上,`n`结尾的名字往往是男性;以`ch`结尾的名字通常是男性,尽管以`h`结尾的名字倾向于是女性。因此,调整我们的特征提取器包括两个字母后缀的特征: + +```py +>>> def gender_features(word): +... return {'suffix1': word[-1:], +... 'suffix2': word[-2:]} +``` + +使用新的特征提取器重建分类器,我们看到测试数据集上的性能提高了近 3 个百分点(从 76.5% 到 78.2%): + +```py +>>> train_set = [(gender_features(n), gender) for (n, gender) in train_names] +>>> devtest_set = [(gender_features(n), gender) for (n, gender) in devtest_names] +>>> classifier = nltk.NaiveBayesClassifier.train(train_set) +>>> print(nltk.classify.accuracy(classifier, devtest_set)) +0.782 +``` + +这个错误分析过程可以不断重复,检查存在于由新改进的分类器产生的错误中的模式。每一次错误分析过程被重复,我们应该选择一个不同的开发测试/训练分割,以确保该分类器不会开始反映开发测试集的特质。 + +但是,一旦我们已经使用了开发测试集帮助我们开发模型,关于这个模型在新数据会表现多好,我们将不能再相信它会给我们一个准确地结果。因此,保持测试集分离、未使用过,直到我们的模型开发完毕是很重要的。在这一点上,我们可以使用测试集评估模型在新的输入值上执行的有多好。 + +## 1.3 文档分类 + +在 1 中,我们看到了语料库的几个例子,那里文档已经按类别标记。使用这些语料库,我们可以建立分类器,自动给新文档添加适当的类别标签。首先,我们构造一个标记了相应类别的文档清单。对于这个例子,我们选择电影评论语料库,将每个评论归类为正面或负面。 + +```py +>>> from nltk.corpus import movie_reviews +>>> documents = [(list(movie_reviews.words(fileid)), category) +... for category in movie_reviews.categories() +... for fileid in movie_reviews.fileids(category)] +>>> random.shuffle(documents) +``` + +接下来,我们为文档定义一个特征提取器,这样分类器就会知道哪些方面的数据应注意`(1.4)`。对于文档主题识别,我们可以为每个词定义一个特性表示该文档是否包含这个词。为了限制分类器需要处理的特征的数目,我们一开始构建一个整个语料库中前 2000 个最频繁词的列表❶。然后,定义一个特征提取器❷,简单地检查这些词是否在一个给定的文档中。 + +```py +all_words = nltk.FreqDist(w.lower() for w in movie_reviews.words()) +word_features = list(all_words)[:2000] ❶ + +def document_features(document): ❷ + document_words = set(document) ❸ + features = {} + for word in word_features: + features['contains({})'.format(word)] = (word in document_words) + return features +``` + +注意 + +在❸中我们计算文档的所有词的集合,而不仅仅检查是否`word in document`,因为检查一个词是否在一个集合中出现比检查它是否在一个列表中出现要快的多`(4.7)`。 + +现在,我们已经定义了我们的特征提取器,可以用它来训练一个分类器,为新的电影评论加标签`(1.5)`。为了检查产生的分类器可靠性如何,我们在测试集上计算其准确性❶。再一次的,我们可以使用`show_most_informative_features()`来找出哪些特征是分类器发现最有信息量的❷。 + +```py +featuresets = [(document_features(d), c) for (d,c) in documents] +train_set, test_set = featuresets[100:], featuresets[:100] +classifier = nltk.NaiveBayesClassifier.train(train_set) +``` + +显然在这个语料库中,提到 Seagal 的评论中负面的是正面的大约 8 倍,而提到 Damon 的评论中正面的是负面的大约 6 倍。 + +## 1.4 词性标注 + +第 5 中,我们建立了一个正则表达式标注器,通过查找词内部的组成,为词选择词性标记。然而,这个正则表达式标注器是手工制作的。作为替代,我们可以训练一个分类器来算出哪个后缀最有信息量。首先,让我们找出最常见的后缀: + +```py +>>> from nltk.corpus import brown +>>> suffix_fdist = nltk.FreqDist() +>>> for word in brown.words(): +... word = word.lower() +... suffix_fdist[word[-1:]] += 1 +... suffix_fdist[word[-2:]] += 1 +... suffix_fdist[word[-3:]] += 1 +``` + +```py +>>> common_suffixes = [suffix for (suffix, count) in suffix_fdist.most_common(100)] +>>> print(common_suffixes) +['e', ',', '.', 's', 'd', 't', 'he', 'n', 'a', 'of', 'the', + 'y', 'r', 'to', 'in', 'f', 'o', 'ed', 'nd', 'is', 'on', 'l', + 'g', 'and', 'ng', 'er', 'as', 'ing', 'h', 'at', 'es', 'or', + 're', 'it', '``', 'an', "''", 'm', ';', 'i', 'ly', 'ion', ...] +``` + +接下来,我们将定义一个特征提取器函数,检查给定的单词的这些后缀: + +```py +>>> def pos_features(word): +... features = {} +... for suffix in common_suffixes: +... features['endswith({})'.format(suffix)] = word.lower().endswith(suffix) +... return features +``` + +特征提取函数的行为就像有色眼镜一样,强调我们的数据中的某些属性(颜色),并使其无法看到其他属性。分类器在决定如何标记输入时,将完全依赖它们强调的属性。在这种情况下,分类器将只基于一个给定的词拥有(如果有)哪个常见后缀的信息来做决定。 + +现在,我们已经定义了我们的特征提取器,可以用它来训练一个新的“决策树”的分类器(将在 4 讨论): + +```py +>>> tagged_words = brown.tagged_words(categories='news') +>>> featuresets = [(pos_features(n), g) for (n,g) in tagged_words] +``` + +```py +>>> size = int(len(featuresets) * 0.1) +>>> train_set, test_set = featuresets[size:], featuresets[:size] +``` + +```py +>>> classifier = nltk.DecisionTreeClassifier.train(train_set) +>>> nltk.classify.accuracy(classifier, test_set) +0.62705121829935351 +``` + +```py +>>> classifier.classify(pos_features('cats')) +'NNS' +``` + +决策树模型的一个很好的性质是它们往往很容易解释——我们甚至可以指示 NLTK 将它们以伪代码形式输出: + +```py +>>> print(classifier.pseudocode(depth=4)) +if endswith(,) == True: return ',' +if endswith(,) == False: + if endswith(the) == True: return 'AT' + if endswith(the) == False: + if endswith(s) == True: + if endswith(is) == True: return 'BEZ' + if endswith(is) == False: return 'VBZ' + if endswith(s) == False: + if endswith(.) == True: return '.' + if endswith(.) == False: return 'NN' +``` + +在这里,我们可以看到分类器一开始检查一个词是否以逗号结尾——如果是,它会得到一个特别的标记`","`。接下来,分类器检查词是否以`"the"`尾,这种情况它几乎肯定是一个限定词。这个“后缀”被决策树早早使用是因为词`"the"`太常见。分类器继续检查词是否以`"s"`结尾。如果是,那么它极有可能得到动词标记`VBZ`(除非它是这个词`"is"`,它有特殊标记`BEZ`),如果不是,那么它往往是名词(除非它是标点符号“.”)。实际的分类器包含这里显示的`if-then`语句下面进一步的嵌套,参数`depth=4`只显示决策树的顶端部分。 + +## 1.5 探索上下文语境 + +通过增加特征提取函数,我们可以修改这个词性标注器来利用各种词内部的其他特征,例如词长、它所包含的音节数或者它的前缀。然而,只要特征提取器仅仅看着目标词,我们就没法添加依赖词出现的*上下文语境*特征。然而上下文语境特征往往提供关于正确标记的强大线索——例如,标注词`"fly"`,如果知道它前面的词是`a`将使我们能够确定它是一个名词,而不是一个动词。 + +为了采取基于词的上下文的特征,我们必须修改以前为我们的特征提取器定义的模式。不是只传递已标注的词,我们将传递整个(未标注的)句子,以及目标词的索引。1.6 演示了这种方法,使用依赖上下文的特征提取器定义一个词性标记分类器。 + +```py +def pos_features(sentence, i): ❶ + features = {"suffix(1)": sentence[i][-1:], + "suffix(2)": sentence[i][-2:], + "suffix(3)": sentence[i][-3:]} + if i == 0: + features["prev-word"] = "" + else: + features["prev-word"] = sentence[i-1] + return features +``` + +很显然,利用上下文特征提高了我们的词性标注器的准确性。例如,分类器学到一个词如果紧跟在词`large`或`gubernatorial`后面,极可能是名词。然而,它无法学到,一个词如果它跟在形容词后面可能是名词,这样更一般的,因为它没有获得前面这个词的词性标记。在一般情况下,简单的分类器总是将每一个输入与所有其他输入独立对待。在许多情况下,这非常有道理。例如,关于名字是否倾向于男性或女性的决定可以通过具体分析来做出。然而,有很多情况,如词性标注,我们感兴趣的是解决彼此密切相关的分类问题。 + +## 1.6 序列分类 + +为了捕捉相关的分类任务之间的依赖关系,我们可以使用联合分类器模型,收集有关输入,选择适当的标签。在词性标注的例子中,各种不同的序列分类器模型可以被用来为一个给定的句子中的所有的词共同选择词性标签。 + +一种序列分类器策略,称为连续分类或贪婪序列分类,是为第一个输入找到最有可能的类标签,然后使用这个问题的答案帮助找到下一个输入的最佳的标签。这个过程可以不断重复直到所有的输入都被贴上标签。这是 5 中的二元标注器采用的方法,它一开始为句子的第一个词选择词性标记,然后为每个随后的词选择标记,基于词本身和前面词的预测的标记。 + +在 1.7 中演示了这一策略。首先,我们必须扩展我们的特征提取函数使其具有参数`history`,它提供一个我们到目前为止已经为句子预测的标记的列表❶。`history`中的每个标记对应`sentence`中的一个词。但是请注意,`history`将只包含我们已经归类的词的标记,也就是目标词左侧的词。因此,虽然是有可能查看目标词右边的词的某些特征,但查看那些词的标记是不可能的(因为我们还未产生它们)。 + +已经定义了特征提取器,我们可以继续建立我们的序列分类器❷。在训练中,我们使用已标注的标记为征提取器提供适当的历史信息,但标注新的句子时,我们基于标注器本身的输出产生历史信息。 + +```py + def pos_features(sentence, i, history): ❶ + features = {"suffix(1)": sentence[i][-1:], + "suffix(2)": sentence[i][-2:], + "suffix(3)": sentence[i][-3:]} + if i == 0: + features["prev-word"] = "" + features["prev-tag"] = "" + else: + features["prev-word"] = sentence[i-1] + features["prev-tag"] = history[i-1] + return features + +class ConsecutivePosTagger(nltk.TaggerI): ❷ + + def __init__(self, train_sents): + train_set = [] + for tagged_sent in train_sents: + untagged_sent = nltk.tag.untag(tagged_sent) + history = [] + for i, (word, tag) in enumerate(tagged_sent): + featureset = pos_features(untagged_sent, i, history) + train_set.append( (featureset, tag) ) + history.append(tag) + self.classifier = nltk.NaiveBayesClassifier.train(train_set) + + def tag(self, sentence): + history = [] + for i, word in enumerate(sentence): + featureset = pos_features(sentence, i, history) + tag = self.classifier.classify(featureset) + history.append(tag) + return zip(sentence, history) +``` + +## 1.7 其他序列分类方法 + +这种方法的一个缺点是我们的决定一旦做出无法更改。例如,如果我们决定将一个词标注为名词,但后来发现的证据表明应该是一个动词,就没有办法回去修复我们的错误。这个问题的一个解决方案是采取转型策略。转型联合分类的工作原理是为输入的标签创建一个初始值,然后反复提炼那个值,尝试修复相关输入之间的不一致。Brill 标注器,`(1)`中描述的,是这种策略的一个很好的例子。 + +另一种方案是为词性标记所有可能的序列打分,选择总得分最高的序列。隐马尔可夫模型就采取这种方法。隐马尔可夫模型类似于连续分类器,它不光看输入也看已预测标记的历史。然而,不是简单地找出一个给定的词的单个最好的标签,而是为标记产生一个概率分布。然后将这些概率结合起来计算标记序列的概率得分,最高概率的标记序列会被选中。不幸的是,可能的标签序列的数量相当大。给定 30 个标签的标记集,有大约 600 万亿(`30^10`)种方式来标记一个 10 个词的句子。为了避免单独考虑所有这些可能的序列,隐马尔可夫模型要求特征提取器只看最近的标记(或最近的`n`个标记,其中`n`是相当小的)。由于这种限制,它可以使用动态规划`(4.7)`,有效地找出最有可能的标记序列。特别是,对每个连续的词索引`i`,每个可能的当前及以前的标记都被计算得分。这种同样基础的方法被两个更先进的模型所采用,它们被称为最大熵马尔可夫模型和线性链条件随机场模型;但为标记序列打分用的是不同的算法。 + +## 2 有监督分类的更多例子 + +## 2.1 句子分割 + +句子分割可以看作是一个标点符号的分类任务:每当我们遇到一个可能会结束一个句子的符号,如句号或问号,我们必须决定它是否终止了当前句子。 + +第一步是获得一些已被分割成句子的数据,将它转换成一种适合提取特征的形式: + +```py +>>> sents = nltk.corpus.treebank_raw.sents() +>>> tokens = [] +>>> boundaries = set() +>>> offset = 0 +>>> for sent in sents: +... tokens.extend(sent) +... offset += len(sent) +... boundaries.add(offset-1) +``` + +在这里,`tokens`是单独句子标识符的合并列表,`boundaries`是一个包含所有句子边界词符索引的集合。下一步,我们需要指定用于决定标点是否表示句子边界的数据特征: + +```py +>>> def punct_features(tokens, i): +... return {'next-word-capitalized': tokens[i+1][0].isupper(), +... 'prev-word': tokens[i-1].lower(), +... 'punct': tokens[i], +... 'prev-word-is-one-char': len(tokens[i-1]) == 1} +``` + +基于这一特征提取器,我们可以通过选择所有的标点符号创建一个加标签的特征集的列表,然后标注它们是否是边界标识符: + +```py +>>> featuresets = [(punct_features(tokens, i), (i in boundaries)) +... for i in range(1, len(tokens)-1) +... if tokens[i] in '.?!'] +``` + +使用这些特征集,我们可以训练和评估一个标点符号分类器: + +```py +>>> size = int(len(featuresets) * 0.1) +>>> train_set, test_set = featuresets[size:], featuresets[:size] +>>> classifier = nltk.NaiveBayesClassifier.train(train_set) +>>> nltk.classify.accuracy(classifier, test_set) +0.936026936026936 +``` + +使用这种分类器进行断句,我们只需检查每个标点符号,看它是否是作为一个边界标识符;在边界标识符处分割词列表。在 2.1 中的清单显示了如何可以做到这一点。 + +```py +def segment_sentences(words): + start = 0 + sents = [] + for i, word in enumerate(words): + if word in '.?!' and classifier.classify(punct_features(words, i)) == True: + sents.append(words[start:i+1]) + start = i+1 + if start < len(words): + sents.append(words[start:]) + return sents +``` + +## 2.2 识别对话行为类型 + +处理对话时,将对话看作说话者执行的*行为*是很有用的。对于表述行为的陈述句这种解释是最直白的,例如`I forgive you`或`I bet you can't climb that hill`。但是问候、问题、回答、断言和说明都可以被认为是基于语言的行为类型。识别对话中言语下的对话行为是理解谈话的重要的第一步。 + +NPS 聊天语料库,在 1 中的展示过,包括超过 10,000 个来自即时消息会话的帖子。这些帖子都已经被贴上 15 种对话行为类型中的一种标签,例如“陈述”,“情感”,“是否问题”和“接续”。因此,我们可以利用这些数据建立一个分类器,识别新的即时消息帖子的对话行为类型。第一步是提取基本的消息数据。我们将调用`xml_posts()`来得到一个数据结构,表示每个帖子的 XML 注释: + +```py +>>> posts = nltk.corpus.nps_chat.xml_posts()[:10000] +``` + +下一步,我们将定义一个简单的特征提取器,检查帖子包含什么词: + +```py +>>> def dialogue_act_features(post): +... features = {} +... for word in nltk.word_tokenize(post): +... features['contains({})'.format(word.lower())] = True +... return features +``` + +最后,我们通过为每个帖子提取特征(使用`post.get('class')`获得一个帖子的对话行为类型)构造训练和测试数据,并创建一个新的分类器: + +```py +>>> featuresets = [(dialogue_act_features(post.text), post.get('class')) +... for post in posts] +>>> size = int(len(featuresets) * 0.1) +>>> train_set, test_set = featuresets[size:], featuresets[:size] +>>> classifier = nltk.NaiveBayesClassifier.train(train_set) +>>> print(nltk.classify.accuracy(classifier, test_set)) +0.67 +``` + +## 2.3 识别文字蕴含 + +识别文字蕴含(RTE)是判断文本`T`的一个给定片段是否蕴含着另一个叫做“假设”的文本(已经在 5 讨论过)。迄今为止,已经有 4 个 RTE 挑战赛,在那里共享的开发和测试数据会提供给参赛队伍。这里是挑战赛 3 开发数据集中的文本/假设对的两个例子。标签`True`表示蕴含成立,`False`表示蕴含不成立。 + +```py +Challenge 3, Pair 34 (True) + +T: Parviz Davudi was representing Iran at a meeting of the Shanghai Co-operation Organisation (SCO), the fledgling association that binds Russia, China and four former Soviet republics of central Asia together to fight terrorism. + +H: China is a member of SCO. + +Challenge 3, Pair 81 (False) + +T: According to NC Articles of Organization, the members of LLC company are H. Nelson Beavers, III, H. Chester Beavers and Jennie Beavers Stewart. + +``` + +应当强调,文字和假设之间的关系并不一定是逻辑蕴涵,而是一个人是否会得出结论:文本提供了合理的证据证明假设是真实的。 + +我们可以把 RTE 当作一个分类任务,尝试为每一对预测`True/False`标签。虽然这项任务的成功做法似乎看上去涉及语法分析、语义和现实世界的知识的组合,RTE 的许多早期的尝试使用粗浅的分析基于文字和假设之间的在词级别的相似性取得了相当不错的结果。在理想情况下,我们希望如果有一个蕴涵那么假设所表示的所有信息也应该在文本中表示。相反,如果假设中有的资料文本中没有,那么就没有蕴涵。 + +在我们的 RTE 特征探测器`(2.2)`中,我们让词(即词类型)作为信息的代理,我们的特征计数词重叠的程度和假设中有而文本中没有的词的程度(由`hyp_extra()`方法获取)。不是所有的词都是同样重要的——命名实体,如人、组织和地方的名称,可能会更为重要,这促使我们分别为`words`和`nes`(命名实体)提取不同的信息。此外,一些高频虚词作为“停用词”被过滤掉。 + +```py +def rte_features(rtepair): + extractor = nltk.RTEFeatureExtractor(rtepair) + features = {} + features['word_overlap'] = len(extractor.overlap('word')) + features['word_hyp_extra'] = len(extractor.hyp_extra('word')) + features['ne_overlap'] = len(extractor.overlap('ne')) + features['ne_hyp_extra'] = len(extractor.hyp_extra('ne')) + return features +``` + +为了说明这些特征的内容,我们检查前面显示的文本/假设对 34 的一些属性: + +```py +>>> rtepair = nltk.corpus.rte.pairs(['rte3_dev.xml'])[33] +>>> extractor = nltk.RTEFeatureExtractor(rtepair) +>>> print(extractor.text_words) +{'Russia', 'Organisation', 'Shanghai', 'Asia', 'four', 'at', +'operation', 'SCO', ...} +>>> print(extractor.hyp_words) +{'member', 'SCO', 'China'} +>>> print(extractor.overlap('word')) +set() +>>> print(extractor.overlap('ne')) +{'SCO', 'China'} +>>> print(extractor.hyp_extra('word')) +{'member'} +``` + +这些特征表明假设中所有重要的词都包含在文本中,因此有一些证据支持标记这个为 *True*。 + +`nltk.classify.rte_classify`模块使用这些方法在合并的 RTE 测试数据上取得了刚刚超过 58% 的准确率。这个数字并不是很令人印象深刻的,还需要大量的努力,更多的语言学处理,才能达到更好的结果。 + +## 2.4 扩展到大型数据集 + +Python 提供了一个良好的环境进行基本的文本处理和特征提取。然而,它处理机器学习方法需要的密集数值计算不能够如 C 语言那样的低级语言那么快。因此,如果你尝试在大型数据集使用纯 Python 的机器学习实现(如`nltk.NaiveBayesClassifier`),你可能会发现学习算法会花费大量的时间和内存。 + +如果你打算用大量训练数据或大量特征来训练分类器,我们建议你探索 NLTK 与外部机器学习包的接口。只要这些软件包已安装,NLTK 可以透明地调用它们(通过系统调用)来训练分类模型,明显比纯 Python 的分类实现快。请看 NLTK 网站上推荐的 NLTK 支持的机器学习包列表。 + +## 3 评估 + +为了决定一个分类模型是否准确地捕捉了模式,我们必须评估该模型。评估的结果对于决定模型是多么值得信赖以及我们如何使用它是非常重要。评估也可以是一个有效的工具,用于指导我们在未来改进模型。 + +## 3.1 测试集 + +大多数评估技术为模型计算一个得分,通过比较它在测试集(或评估集)中为输入生成的标签与那些输入的正确标签。该测试集通常与训练集具有相同的格式。然而,测试集与训练语料不同是非常重要的:如果我们简单地重复使用训练集作为测试集,那么一个只记住了它的输入而没有学会如何推广到新的例子的模型会得到误导人的高分。 + +建立测试集时,往往是一个可用于测试的和可用于训练的数据量之间的权衡。对于有少量平衡的标签和一个多样化的测试集的分类任务,只要 100 个评估实例就可以进行有意义的评估。但是,如果一个分类任务有大量的标签或包括非常罕见的标签,那么选择的测试集的大小就要保证出现次数最少的标签至少出现 50 次。此外,如果测试集包含许多密切相关的实例——例如来自一个单独文档中的实例——那么测试集的大小应增加,以确保这种多样性的缺乏不会扭曲评估结果。当有大量已标注数据可用时,只使用整体数据的 10% 进行评估常常会在安全方面犯错。 + +选择测试集时另一个需要考虑的是测试集中实例与开发集中的实例的相似程度。这两个数据集越相似,我们对将评估结果推广到其他数据集的信心就越小。例如,考虑词性标注任务。在一种极端情况,我们可以通过从一个反映单一的文体(如新闻)的数据源随机分配句子,创建训练集和测试集: + +```py +>>> import random +>>> from nltk.corpus import brown +>>> tagged_sents = list(brown.tagged_sents(categories='news')) +>>> random.shuffle(tagged_sents) +>>> size = int(len(tagged_sents) * 0.1) +>>> train_set, test_set = tagged_sents[size:], tagged_sents[:size] +``` + +在这种情况下,我们的测试集和训练集将是*非常*相似的。训练集和测试集均取自同一文体,所以我们不能相信评估结果可以推广到其他文体。更糟糕的是,因为调用`random.shuffle()`,测试集中包含来自训练使用过的相同的文档的句子。如果文档中有相容的模式(也就是说,如果一个给定的词与特定词性标记一起出现特别频繁),那么这种差异将体现在开发集和测试集。一个稍好的做法是确保训练集和测试集来自不同的文件: + +```py +>>> file_ids = brown.fileids(categories='news') +>>> size = int(len(file_ids) * 0.1) +>>> train_set = brown.tagged_sents(file_ids[size:]) +>>> test_set = brown.tagged_sents(file_ids[:size]) +``` + +如果我们要执行更令人信服的评估,可以从与训练集中文档联系更少的文档中获取测试集: + +```py +>>> train_set = brown.tagged_sents(categories='news') +>>> test_set = brown.tagged_sents(categories='fiction') +``` + +如果我们在此测试集上建立了一个性能很好的分类器,那么我们完全可以相信它有能力很好的泛化到用于训练它的数据以外的数据。 + +## 3.2 准确度 + +用于评估一个分类最简单的度量是准确度,测量测试集上分类器正确标注的输入的比例。例如,一个名字性别分类器,在包含 80 个名字的测试集上预测正确的名字有 60 个,它有`60/80 = 75%`的准确度。`nltk.classify.accuracy()`函数会在给定的测试集上计算分类器模型的准确度: + +```py +>>> classifier = nltk.NaiveBayesClassifier.train(train_set) +>>> print('Accuracy: {:4.2f}'.format(nltk.classify.accuracy(classifier, test_set))) +0.75 +``` + +解释一个分类器的准确性得分,考虑测试集中单个类标签的频率是很重要的。例如,考虑一个决定词`bank`每次出现的正确的词意的分类器。如果我们在金融新闻文本上评估分类器,那么我们可能会发现,`金融机构`的意思 20 个里面出现了 19 次。在这种情况下,95% 的准确度也难以给人留下深刻印象,因为我们可以实现一个模型,它总是返回`金融机构`的意义。然而,如果我们在一个更加平衡的语料库上评估分类器,那里的最频繁的词意只占 40%,那么 95% 的准确度得分将是一个更加积极的结果。(在 2 测量标注一致性程度时也会有类似的问题。) + +## 3.3 精确度和召回率 + +另一个准确度分数可能会产生误导的实例是在“搜索”任务中,如信息检索,我们试图找出与特定任务有关的文档。由于不相关的文档的数量远远多于相关文档的数量,一个将每一个文档都标记为无关的模型的准确度分数将非常接近 100%。 + +![Images/precision-recall.png](Images/e591e60c490795add5183c998132ebc0.jpg) + +图 3.1:真与假的阳性和阴性 + +因此,对搜索任务使用不同的测量集是很常见的,基于 3.1 所示的四个类别的每一个中的项目的数量: + +* 真阳性是相关项目中我们正确识别为相关的。 + +* I 型错误:是不相关项目中我们错误识别为相关的。 +* II 型错误:是相关项目中我们错误识别为不相关的。 + +给定这四个数字,我们可以定义以下指标: + +* F 度量值(或 F-Score),组合精确度和召回率为一个单独的得分,被定义为精确度和召回率的调和平均数`(2 × Precision × Recall) / (Precision + Recall)`。 + +## 3.4 混淆矩阵 + +当处理有 3 个或更多的标签的分类任务时,基于模型错误类型细分模型的错误是有信息量的。一个混淆矩阵是一个表,其中每个`cells[i,j]`表示正确的标签`i`被预测为标签`j`的次数。因此,对角线项目(即`cells[i, i]`)表示正确预测的标签,非对角线项目表示错误。在下面的例子中,我们为 4 中开发的一元标注器生成一个混淆矩阵: + +```py +>>> def tag_list(tagged_sents): +... return [tag for sent in tagged_sents for (word, tag) in sent] +>>> def apply_tagger(tagger, corpus): +... return [tagger.tag(nltk.tag.untag(sent)) for sent in corpus] +>>> gold = tag_list(brown.tagged_sents(categories='editorial')) +>>> test = tag_list(apply_tagger(t2, brown.tagged_sents(categories='editorial'))) +>>> cm = nltk.ConfusionMatrix(gold, test) +>>> print(cm.pretty_format(sort_by_count=True, show_percents=True, truncate=9)) + | N | + | N I A J N V N | + | N N T J . S , B P | +----+----------------------------------------------------------------+ + NN | <11.8%> 0.0% . 0.2% . 0.0% . 0.3% 0.0% | + IN | 0.0% <9.0%> . . . 0.0% . . . | + AT | . . <8.6%> . . . . . . | + JJ | 1.7% . . <3.9%> . . . 0.0% 0.0% | + . | . . . . <4.8%> . . . . | +NNS | 1.5% . . . . <3.2%> . . 0.0% | + , | . . . . . . <4.4%> . . | + VB | 0.9% . . 0.0% . . . <2.4%> . | + NP | 1.0% . . 0.0% . . . . <1.8%>| +----+----------------------------------------------------------------+ +(row = reference; col = test) + +``` + +这个混淆矩阵显示常见的错误,包括`NN`替换为了`JJ`(1.6% 的词),`NN`替换为了`NNS`(1.5% 的词)。注意点(`.`)表示值为 0 的单元格,对角线项目——对应正确的分类——用尖括号标记。 + +## 3.5 交叉验证 + +为了评估我们的模型,我们必须为测试集保留一部分已标注的数据。正如我们已经提到,如果测试集是太小了,我们的评价可能不准确。然而,测试集设置较大通常意味着训练集设置较小,如果已标注数据的数量有限,这样设置对性能会产生重大影响。 + +这个问题的解决方案之一是在不同的测试集上执行多个评估,然后组合这些评估的得分,这种技术被称为交叉验证。特别是,我们将原始语料细分为 N 个子集称为折叠。对于每一个这些的折叠,我们使用*除*这个折叠中的数据外其他所有数据训练模型,然后在这个折叠上测试模型。即使个别的折叠可能是太小了而不能在其上给出准确的评价分数,综合评估得分是基于大量的数据,因此是相当可靠的。 + +第二,同样重要的,采用交叉验证的优势是,它可以让我们研究不同的训练集上性能变化有多大。如果我们从所有 N 个训练集得到非常相似的分数,然后我们可以相当有信心,得分是准确的。另一方面,如果 N 个训练集上分数很大不同,那么,我们应该对评估得分的准确性持怀疑态度。 + +## 4 决策树 + +接下来的三节中,我们将仔细看看可用于自动生成分类模型的三种机器学习方法:决策树、朴素贝叶斯分类器和最大熵分类器。正如我们所看到的,可以把这些学习方法看作黑盒子,直接训练模式,使用它们进行预测而不需要理解它们是如何工作的。但是,仔细看看这些学习方法如何基于一个训练集上的数据选择模型,会学到很多。了解这些方法可以帮助指导我们选择相应的特征,尤其是我们关于那些特征如何编码的决定。理解生成的模型可以让我们更好的提取信息,哪些特征对有信息量,那些特征之间如何相互关联。 + +决策树是一个简单的为输入值选择标签的流程图。这个流程图由检查特征值的决策节点和分配标签的叶节点组成。为输入值选择标签,我们以流程图的初始决策节点开始,称为其根节点。此节点包含一个条件,检查输入值的特征之一,基于该特征的值选择一个分支。沿着这个描述我们输入值的分支,我们到达了一个新的决策节点,有一个关于输入值的特征的新的条件。我们继续沿着每个节点的条件选择的分支,直到到达叶节点,它为输入值提供了一个标签。4.1 显示名字性别任务的决策树模型的例子。 + +![Images/decision-tree.png](Images/1ab45939cc9babb242ac45ed03a94f7a.jpg) + +图 4.1:名字性别任务的决策树模型。请注意树图按照习惯画出“颠倒的”,根在上面,叶子在下面。 + +一旦我们有了一个决策树,就可以直接用它来分配标签给新的输入值。不那么直接的是我们如何能够建立一个模拟给定的训练集的决策树。但在此之前,我们看一下建立决策树的学习算法,思考一个简单的任务:为语料库选择最好的“决策树桩”。决策树桩是只有一个节点的决策树,基于一个特征决定如何为输入分类。每个可能的特征值一个叶子,为特征有那个值的输入指定类标签。要建立决策树桩,我们首先必须决定哪些特征应该使用。最简单的方法是为每个可能的特征建立一个决策树桩,看哪一个在训练数据上得到最高的准确度,也有其他的替代方案,我们将在下面讨论。一旦我们选择了一个特征,就可以通过分配一个标签给每个叶子,基于在训练集中所选的例子的最频繁的标签,建立决策树桩(即选择特征具有那个值的例子)。 + +给出了选择决策树桩的算法,生长出较大的决策树的算法就很简单了。首先,我们选择分类任务的整体最佳的决策树桩。然后,我们在训练集上检查每个叶子的准确度。没有达到足够的准确度的叶片被新的决策树桩替换,新决策树桩是在根据到叶子的路径选择的训练语料的子集上训练的。例如,我们可以使 4.1 中的决策树生长,通过替换最左边的叶子为新的决策树桩,这个新的决策树桩是在名字不以`k`开始或以一个元音或`l`结尾的训练集的子集上训练的。 + +## 4.1 熵和信息增益 + +正如之前提到的,有几种方法来为决策树桩确定最有信息量的特征。一种流行的替代方法,被称为信息增益,当我们用给定的特征分割输入值时,衡量它们变得更有序的程度。要衡量原始输入值集合如何无序,我们计算它们的标签的墒,如果输入值的标签非常不同,墒就高;如果输入值的标签都相同,墒就低。特别地,熵被定义为每个标签的概率乘以那个标签的对数概率的总和: + +```py +import math +def entropy(labels): + freqdist = nltk.FreqDist(labels) + probs = [freqdist.freq(l) for l in freqdist] + return -sum(p * math.log(p,2) for p in probs) +``` + +一旦我们已经计算了原始输入值的标签集的墒,就可以判断应用了决策树桩之后标签会变得多么有序。为了这样做,我们计算每个决策树桩的叶子的熵,利用这些叶子熵值的平均值(加权每片叶子的样本数量)。信息增益等于原来的熵减去这个新的减少的熵。信息增益越高,将输入值分为相关组的决策树桩就越好,于是我们可以通过选择具有最高信息增益的决策树桩来建立决策树。 + +决策树的另一个考虑因素是效率。前面描述的选择决策树桩的简单算法必须为每一个可能的特征构建候选决策树桩,并且这个过程必须在构造决策树的每个节点上不断重复。已经开发了一些算法通过存储和重用先前评估的例子的信息减少训练时间。 + +决策树有一些有用的性质。首先,它们简单明了,容易理解。决策树顶部附近尤其如此,这通常使学习算法可以找到非常有用的特征。决策树特别适合有很多层次的分类区别的情况。例如,决策树可以非常有效地捕捉进化树。 + +然而,决策树也有一些缺点。一个问题是,由于决策树的每个分支会划分训练数据,在训练树的低节点,可用的训练数据量可能会变得非常小。因此,这些较低的决策节点可能 + +过拟合训练集,学习模式反映训练集的特质而不是问题底层显著的语言学模式。对这个问题的一个解决方案是当训练数据量变得太小时停止分裂节点。另一种方案是长出一个完整的决策树,但随后剪去在开发测试集上不能提高性能的决策节点。 + +决策树的第二个问题是,它们强迫特征按照一个特定的顺序进行检查,即使特征可能是相对独立的。例如,按主题分类文档(如体育、汽车或谋杀之谜)时,特征如`hasword(football)`极可能表示一个特定标签,无论其他的特征值是什么。由于决策树顶部附近的空间有限,大部分这些特征将需要在树中的许多不同的分支中重复。因为越往树的下方,分支的数量成指数倍增长,重复量可能非常大。 + +一个相关的问题是决策树不善于利用对正确的标签具有较弱预测能力的特征。由于这些特征的影响相对较小,它们往往出现在决策树非常低的地方。决策树学习的时间远远不够用到这些特征,也不能留下足够的训练数据来可靠地确定它们应该有什么样的影响。如果我们能够在整个训练集中看看这些特征的影响,那么我们也许能够做出一些关于它们是如何影响标签的选择的结论。 + +决策树需要按一个特定的顺序检查特征的事实,限制了它们的利用相对独立的特征的能力。我们下面将讨论的朴素贝叶斯分类方法克服了这一限制,允许所有特征“并行”的起作用。 + +## 5 朴素贝叶斯分类器 + +在朴素贝叶斯分类器中,每个特征都得到发言权,来确定哪个标签应该被分配到一个给定的输入值。为一个输入值选择标签,朴素贝叶斯分类器以计算每个标签的先验概率开始,它由在训练集上检查每个标签的频率来确定。之后,每个特征的贡献与它的先验概率组合,得到每个标签的似然估计。似然估计最高的标签会分配给输入值。5.1 说明了这一过程。 + +![Images/naive-bayes-triangle.png](Images/fff90c564d2625f739b442b23301906e.jpg) + +图 5.1:使用朴素贝叶斯分类器为文档选择主题的程序的抽象图解。在训练语料中,大多数文档是有关汽车的,所以分类器从接近“汽车”的标签的点上开始。但它会考虑每个特征的影响。在这个例子中,输入文档中包含的词`dark`,它是谋杀之谜的一个不太强的指标,也包含词`football`,它是体育文档的一个有力指标。每个特征都作出了贡献之后,分类器检查哪个标签最接近,并将该标签分配给输入。 + +个别特征对整体决策作出自己的贡献,通过“投票反对”那些不经常出现的特征的标签。特别是,每个标签的似然得分由于与输入值具有此特征的标签的概率相乘而减小。例如,如果词`run`在 12% 的体育文档中出现,在 10% 的谋杀之谜的文档中出现,在 2% 的汽车文档中出现,那么体育标签的似然得分将被乘以 0.12,谋杀之谜标签将被乘以 0.1,汽车标签将被乘以 0.02。整体效果是略高于体育标签的得分的谋杀之谜标签的得分会减少,而汽车标签相对于其他两个标签会显著减少。这个过程如 5.2 和 5.3 所示。 + +![Images/naive_bayes_bargraph.png](Images/b502c97e1f935240559d38b397805b32.jpg) + +图 5.2:计算朴素贝叶斯的标签似然得分。朴素贝叶斯以计算每个标签的先验概率开始,基于每个标签出现在训练数据中的频率。然后每个特征都用于估计每个标签的似然估计,通过用输入值中有那个特征的标签的概率乘以它。似然得分结果可以认为是从具有给定的标签和特征集的训练集中随机选取的值的概率的估计,假设所有特征概率是独立的。 + +## 5.1 底层的概率模型 + +理解朴素贝叶斯分类器的另一种方式是它为输入选择最有可能的标签,基于下面的假设:每个输入值是通过首先为那个输入值选择一个类标签,然后产生每个特征的方式产生的,每个特征与其他特征完全独立。当然,这种假设是不现实的,特征往往高度依赖彼此。我们将在本节结尾回过来讨论这个假设的一些后果。这简化的假设,称为朴素贝叶斯假设(或独立性假设),使得它更容易组合不同特征的贡献,因为我们不必担心它们相互影响。 + +![Images/naive_bayes_graph.png](Images/3edaf7564caaab7c8ddc83db1d5408a3.jpg) + +图 5.3:一个贝叶斯网络图演示朴素贝叶斯分类器假定的生成过程。要生成一个标记的输入,模型先为输入选择标签,然后基于该标签生成每个输入的特征。对给定标签,每个特征被认为是完全独立于所有其他特征的。 + +基于这个假设,我们可以计算表达式`P(label|features)`,给定一个特别的特征集一个输入具有特定标签的概率。要为一个新的输入选择标签,我们可以简单地选择使`P(l|features)`最大的标签 l。 + +一开始,我们注意到`P(label|features)`等于具有特定标签*和*特定特征集的输入的概率除以具有特定特征集的输入的概率: + +```py +>>> from nltk.corpus import senseval +>>> instances = senseval.instances('hard.pos') +>>> size = int(len(instances) * 0.1) +>>> train_set, test_set = instances[size:], instances[:size] +``` + +使用这个数据集,建立一个分类器,预测一个给定的实例的正确的词意标签。关于使用 Senseval 2 语料库返回的实例对象的信息请参阅`http://nltk.org/howto`上的语料库 HOWTO。 + +* ☼ 使用本章讨论过的电影评论文档分类器,产生对分类器最有信息量的 30 个特征的列表。你能解释为什么这些特定特征具有信息量吗?你能在它们中找到什么惊人的发现吗? + + * ☼ 选择一个本章所描述的分类任务,如名字性别检测、文档分类、词性标注或对话行为分类。使用相同的训练和测试数据,相同的特征提取器,建立该任务的三个分类器::决策树、朴素贝叶斯分类器和最大熵分类器。比较你所选任务上这三个分类器的准确性。你如何看待如果你使用了不同的特征提取器,你的结果可能会不同? + + * ☼ 同义词`strong`和`powerful`的模式不同(尝试将它们与`chip`和`sales`结合)。哪些特征与这种区别有关?建立一个分类器,预测每个词何时该被使用。 + + * ◑ 对话行为分类器为每个帖子分配标签,不考虑帖子的上下文背景。然而,对话行为是高度依赖上下文的,一些对话行序列可能比别的更相近。例如,“是否问题”对话行为更容易被一个`yanswer`回答而不是以一个`问候`来回答。利用这一事实,建立一个连续的分类器,为对话行为加标签。一定要考虑哪些特征可能是有用的。参见 1.7 词性标记的连续分类器的代码,获得一些想法。 + + * ◑ 词特征在处理文本分类中是非常有用的,因为在一个文档中出现的词对于其语义内容是什么具有强烈的指示作用。然而,很多词很少出现,一些在文档中的最有信息量的词可能永远不会出现在我们的训练数据中。一种解决方法是使用一个词典,它描述了词之间的不同。使用 WordNet 词典,加强本章介绍的电影评论文档分类器,使用概括一个文档中出现的词的特征,使之更容易匹配在训练数据中发现的词。 + + * ★ PP 附件语料库是描述介词短语附着决策的语料库。语料库中的每个实例被编码为`PPAttachment`对象: + + ```py + >>> from nltk.corpus import ppattach + >>> ppattach.attachments('training') + [PPAttachment(sent='0', verb='join', noun1='board', + prep='as', noun2='director', attachment='V'), + PPAttachment(sent='1', verb='is', noun1='chairman', + prep='of', noun2='N.V.', attachment='N'), + ...] + >>> inst = ppattach.attachments('training')[1] + >>> (inst.noun1, inst.prep, inst.noun2) + ('chairman', 'of', 'N.V.') + ``` + + 选择`inst.attachment`为`N`的唯一实例: + + ```py + >>> nattach = [inst for inst in ppattach.attachments('training') + ... if inst.attachment == 'N'] + ``` + + 使用此子语料库,建立一个分类器,尝试预测哪些介词是用来连接一对给定的名词。例如,给定的名词对`team`和`researchers`,分类器应该预测出介词`of`。更多的使用 PP 附件语料库的信息,参阅`http://nltk.org/howto`上的语料库 HOWTO。 + + * ★ 假设你想自动生成一个场景的散文描述,每个实体已经有了一个唯一描述此实体的词,例如`the jar`,只是想决定在有关的各项目中是否使用`in`或`on`,例如`the book is in the cupboard`对比`the book is on the shelf`。通过查找语料数据探讨这个问题;编写需要的程序。 + +``` +(13) + +a. in the car *versus* on the train + +b. in town *versus* on campus + +c. in the picture *versus* on the screen + +d. in Macbeth *versus* on Letterman +``` + +## 关于本文档... + +针对 NLTK 3.0 作出更新。本章来自于《Python 自然语言处理》,[Steven Bird](http://estive.net/), [Ewan Klein](http://homepages.inf.ed.ac.uk/ewan/) 和 [Edward Loper](http://ed.loper.org/),Copyright © 2014 作者所有。本章依据 [*Creative Commons Attribution-Noncommercial-No Derivative Works 3\.0 United States License*](http://creativecommons.org/licenses/by-nc-nd/3.0/us/) 条款,与[*自然语言工具包*](http://nltk.org/) 3.0 版一起发行。 + +本文档构建于星期三 2015 年 7 月 1 日 12:30:05 AEST + + + + + diff --git a/docs/nlp-py-2e/7.md b/docs/nlp-py-2e/7.md new file mode 100644 index 0000000000000000000000000000000000000000..748347398864c9faa9e595a75ffe43d27313b5fb --- /dev/null +++ b/docs/nlp-py-2e/7.md @@ -0,0 +1,763 @@ +# 7 从文本提取信息 + +对于任何给定的问题,很可能已经有人把答案写在某个地方了。以电子形式提供的自然语言文本的数量真的惊人,并且与日俱增。然而,自然语言的复杂性使访问这些文本中的信息非常困难。NLP 目前的技术水平仍然有很长的路要走才能够从不受限制的文本对意义建立通用的表示。如果我们不是集中我们的精力在问题或“实体关系”的有限集合,例如:“不同的设施位于何处”或“谁被什么公司雇用”上,我们就能取得重大进展。本章的目的是要回答下列问题: + +1. 我们如何能构建一个系统,从非结构化文本中提取结构化数据如表格? +2. 有哪些稳健的方法识别一个文本中描述的实体和关系? +3. 哪些语料库适合这项工作,我们如何使用它们来训练和评估我们的模型? + +一路上,我们将应用前面两章中的技术来解决分块和命名实体识别。 + +## 1 信息提取 + +信息有很多种形状和大小。一个重要的形式是结构化数据:实体和关系的可预测的规范的结构。例如,我们可能对公司和地点之间的关系感兴趣。给定一个公司,我们希望能够确定它做业务的位置;反过来,给定位置,我们会想发现哪些公司在该位置做业务。如果我们的数据是表格形式,如 1.1 中的例子,那么回答这些问题就很简单了。 + +表 1.1: + +位置数据 + +```py +>>> locs = [('Omnicom', 'IN', 'New York'), +... ('DDB Needham', 'IN', 'New York'), +... ('Kaplan Thaler Group', 'IN', 'New York'), +... ('BBDO South', 'IN', 'Atlanta'), +... ('Georgia-Pacific', 'IN', 'Atlanta')] +>>> query = [e1 for (e1, rel, e2) in locs if e2=='Atlanta'] +>>> print(query) +['BBDO South', 'Georgia-Pacific'] +``` + +表 1.2: + +在亚特兰大运营的公司 + +```py +>>> def ie_preprocess(document): +... sentences = nltk.sent_tokenize(document) ❶ +... sentences = [nltk.word_tokenize(sent) for sent in sentences] ❷ +... sentences = [nltk.pos_tag(sent) for sent in sentences] ❸ +``` + +注意 + +请记住我们的例子程序假设你以`import nltk, re, pprint`开始交互式会话或程序。 + +接下来,命名实体识别中,我们分割和标注可能组成一个有趣关系的实体。通常情况下,这些将被定义为名词短语,例如`the knights who say "ni"`或者适当的名称如`Monty Python`。在一些任务中,同时考虑不明确的名词或名词块也是有用的,如`every student`或`cats`,这些不必要一定与确定的`NP`和适当名称一样的方式指示实体。 + +最后,在提取关系时,我们搜索对文本中出现在附近的实体对之间的特殊模式,并使用这些模式建立元组记录实体之间的关系。 + +## 2 词块划分 + +我们将用于实体识别的基本技术是词块划分,它分割和标注多词符的序列,如 2.1 所示。小框显示词级分词和词性标注,大框显示高级别的词块划分。每个这种较大的框叫做一个词块。就像分词忽略空白符,词块划分通常选择词符的一个子集。同样像分词一样,词块划分器生成的片段在源文本中不能重叠。 + +![Images/chunk-segmentation.png](Images/0e768e8c4378c2b0b3290aab46dc770e.jpg) + +图 2.1:词符和词块级别的分割与标注 + +在本节中,我们将在较深的层面探讨词块划分,以词块的定义和表示开始。我们将看到正则表达式和 N 元的方法来词块划分,使用 CoNLL-2000 词块划分语料库开发和评估词块划分器。我们将在`(5)`和 6 回到命名实体识别和关系抽取的任务。 + +## 2.1 名词短语词块划分 + +我们将首先思考名词短语词块划分或`NP`词块划分任务,在那里我们寻找单独名词短语对应的词块。例如,这里是一些《华尔街日报》文本,其中的`NP`词块用方括号标记: + +```py +>>> sentence = [("the", "DT"), ("little", "JJ"), ("yellow", "JJ"), ❶ +... ("dog", "NN"), ("barked", "VBD"), ("at", "IN"), ("the", "DT"), ("cat", "NN")] + +>>> grammar = "NP: {

?*}" ❷ + +>>> cp = nltk.RegexpParser(grammar) ❸ +>>> result = cp.parse(sentence) ❹ +>>> print(result) ❺ +(S + (NP the/DT little/JJ yellow/JJ dog/NN) + barked/VBD + at/IN + (NP the/DT cat/NN)) +>>> result.draw() ❻ +``` + +![tree_images/ch07-tree-1.png](Images/da516572f97daebe1be746abd7bd2268.jpg) + +## 2.2 标记模式 + +组成一个词块语法的规则使用标记模式来描述已标注的词的序列。一个标记模式是一个词性标记序列,用尖括号分隔,如`
?>JJ>*>NN>`。标记模式类似于正则表达式模式`(3.4)`。现在,思考下面的来自《华尔街日报》的名词短语: + +```py +another/DT sharp/JJ dive/NN +trade/NN figures/NNS +any/DT new/JJ policy/NN measures/NNS +earlier/JJR stages/NNS +Panamanian/JJ dictator/NN Manuel/NNP Noriega/NNP + +``` + +## 2.3 用正则表达式进行词块划分 + +要找到一个给定的句子的词块结构,`RegexpParser`词块划分器以一个没有词符被划分的平面结构开始。词块划分规则轮流应用,依次更新词块结构。一旦所有的规则都被调用,返回生成的词块结构。 + +2.3 显示了一个由 2 个规则组成的简单的词块语法。第一条规则匹配一个可选的限定词或所有格代名词,零个或多个形容词,然后跟一个名词。第二条规则匹配一个或多个专有名词。我们还定义了一个进行词块划分的例句❶,并在此输入上运行这个词块划分器❷。 + +```py +grammar = r""" + NP: {?*} # chunk determiner/possessive, adjectives and noun + {+} # chunk sequences of proper nouns +""" +cp = nltk.RegexpParser(grammar) +sentence = [("Rapunzel", "NNP"), ("let", "VBD"), ("down", "RP"), ❶ + ("her", "PP$"), ("long", "JJ"), ("golden", "JJ"), ("hair", "NN")] +``` + +注意 + +` + +如果标记模式匹配位置重叠,最左边的匹配优先。例如,如果我们应用一个匹配两个连续的名词文本的规则到一个包含三个连续的名词的文本,则只有前两个名词将被划分: + +```py +>>> nouns = [("money", "NN"), ("market", "NN"), ("fund", "NN")] +>>> grammar = "NP: {} # Chunk two consecutive nouns" +>>> cp = nltk.RegexpParser(grammar) +>>> print(cp.parse(nouns)) +(S (NP money/NN market/NN) fund/NN) +``` + +一旦我们创建了`money market`词块,我们就已经消除了允许`fund`被包含在一个词块中的上下文。这个问题可以避免,使用一种更加宽容的块规则,如`NP: {>NN>+}`。 + +注意 + +我们已经为每个块规则添加了一个注释。这些是可选的;当它们的存在时,词块划分器将它作为其跟踪输出的一部分输出这些注释。 + +## 2.4 探索文本语料库 + +在 2 中,我们看到了我们如何在已标注的语料库中提取匹配的特定的词性标记序列的短语。我们可以使用词块划分器更容易的做同样的工作,如下: + +```py +>>> cp = nltk.RegexpParser('CHUNK: { }') +>>> brown = nltk.corpus.brown +>>> for sent in brown.tagged_sents(): +... tree = cp.parse(sent) +... for subtree in tree.subtrees(): +... if subtree.label() == 'CHUNK': print(subtree) +... +(CHUNK combined/VBN to/TO achieve/VB) +(CHUNK continue/VB to/TO place/VB) +(CHUNK serve/VB to/TO protect/VB) +(CHUNK wanted/VBD to/TO wait/VB) +(CHUNK allowed/VBN to/TO place/VB) +(CHUNK expected/VBN to/TO become/VB) +... +(CHUNK seems/VBZ to/TO overtake/VB) +(CHUNK want/VB to/TO buy/VB) +``` + +注意 + +**轮到你来**:将上面的例子封装在函数`find_chunks()`内,以一个如`"CHUNK: {>V.*> >TO> >V.*>}"`的词块字符串作为参数。使用它在语料库中搜索其他几种模式,例如连续四个或多个名词,例如`"NOUNS: {>N.*>{4,}}"` + +## 2.5 词缝加塞 + +有时定义我们想从一个词块中排除什么比较容易。我们可以定义词缝为一个不包含在词块中的一个词符序列。在下面的例子中,`barked/VBD at/IN`是一个词缝: + +```py +[ the/DT little/JJ yellow/JJ dog/NN ] barked/VBD at/IN [ the/DT cat/NN ] + +``` + +## 2.6 词块的表示:标记与树 + +作为标注和分析之间的中间状态`(8)`,词块结构可以使用标记或树来表示。最广泛的文件表示使用 IOB 标记。在这个方案中,每个词符被三个特殊的词块标记之一标注,`I`(内部),`O`(外部)或`B`(开始)。一个词符被标注为`B`,如果它标志着一个词块的开始。块内的词符子序列被标注为`I`。所有其他的词符被标注为`O`。`B`和`I`标记后面跟着词块类型,如`B-NP`, `I-NP`。当然,没有必要指定出现在词块外的词符类型,所以这些都只标注为`O`。这个方案的例子如 2.5 所示。 + +![Images/chunk-tagrep.png](Images/542fee25c56235c899312bed3d5ee9ba.jpg) + +图 2.5:词块结构的标记表示形式 + +IOB 标记已成为文件中表示词块结构的标准方式,我们也将使用这种格式。下面是 2.5 中的信息如何出现在一个文件中的: + +```py +We PRP B-NP +saw VBD O +the DT B-NP +yellow JJ I-NP +dog NN I-NP + +``` + +注意 + +NLTK 使用树作为词块的内部表示,并提供这些树与 IOB 格式互换的方法。 + +## 3 开发和评估词块划分器 + +现在你对分块的作用有了一些了解,但我们并没有解释如何评估词块划分器。和往常一样,这需要一个合适的已标注语料库。我们一开始寻找将 IOB 格式转换成 NLTK 树的机制,然后是使用已化分词块的语料库如何在一个更大的规模上做这个。我们将看到如何为一个词块划分器相对一个语料库的准确性打分,再看看一些数据驱动方式搜索`NP`词块。我们整个的重点在于扩展一个词块划分器的覆盖范围。 + +## 3.1 读取 IOB 格式与 CoNLL2000 语料库 + +使用`corpus`模块,我们可以加载已经标注并使用 IOB 符号划分词块的《华尔街日报》文本。这个语料库提供的词块类型有`NP`,`VP`和`PP`。正如我们已经看到的,每个句子使用多行表示,如下所示: + +```py +he PRP B-NP +accepted VBD B-VP +the DT B-NP +position NN I-NP +... + +``` + +![tree_images/ch07-tree-2.png](Images/d167c4075a237573a350e298a184d4fb.jpg) + +我们可以使用 NLTK 的`corpus`模块访问较大量的已经划分词块的文本。CoNLL2000 语料库包含 27 万词的《华尔街日报文本》,分为“训练”和“测试”两部分,标注有词性标记和 IOB 格式词块标记。我们可以使用`nltk.corpus.conll2000`访问这些数据。下面是一个读取语料库的“训练”部分的第 100 个句子的例子: + +```py +>>> from nltk.corpus import conll2000 +>>> print(conll2000.chunked_sents('train.txt')[99]) +(S + (PP Over/IN) + (NP a/DT cup/NN) + (PP of/IN) + (NP coffee/NN) + ,/, + (NP Mr./NNP Stone/NNP) + (VP told/VBD) + (NP his/PRP$ story/NN) + ./.) +``` + +正如你看到的,CoNLL2000 语料库包含三种词块类型:`NP`词块,我们已经看到了;`VP`词块如`has already delivered`;`PP`块如`because of`。因为现在我们唯一感兴趣的是`NP`词块,我们可以使用`chunk_types`参数选择它们: + +```py +>>> print(conll2000.chunked_sents('train.txt', chunk_types=['NP'])[99]) +(S + Over/IN + (NP a/DT cup/NN) + of/IN + (NP coffee/NN) + ,/, + (NP Mr./NNP Stone/NNP) + told/VBD + (NP his/PRP$ story/NN) + ./.) +``` + +## 3.2 简单的评估和基准 + +现在,我们可以访问一个已划分词块语料,可以评估词块划分器。我们开始为没有什么意义的词块解析器`cp`建立一个基准,它不划分任何词块: + +```py +>>> from nltk.corpus import conll2000 +>>> cp = nltk.RegexpParser("") +>>> test_sents = conll2000.chunked_sents('test.txt', chunk_types=['NP']) +>>> print(cp.evaluate(test_sents)) +ChunkParse score: + IOB Accuracy: 43.4% + Precision: 0.0% + Recall: 0.0% + F-Measure: 0.0% +``` + +IOB 标记准确性表明超过三分之一的词被标注为`O`,即没有在`NP`词块中。然而,由于我们的标注器没有找到*任何*词块,其精度、召回率和 F 度量均为零。现在让我们尝试一个初级的正则表达式词块划分器,查找以名词短语标记的特征字母开头的标记(如`CD`, `DT`和`JJ`)。 + +```py +>>> grammar = r"NP: {<[CDJNP].*>+}" +>>> cp = nltk.RegexpParser(grammar) +>>> print(cp.evaluate(test_sents)) +ChunkParse score: + IOB Accuracy: 87.7% + Precision: 70.6% + Recall: 67.8% + F-Measure: 69.2% +``` + +正如你看到的,这种方法达到相当好的结果。但是,我们可以采用更多数据驱动的方法改善它,在这里我们使用训练语料找到对每个词性标记最有可能的块标记(`I`, `O`或`B`)。换句话说,我们可以使用*一元标注器*`(4)`建立一个词块划分器。但不是尝试确定每个词的正确的词性标记,而是根据每个词的词性标记,尝试确定正确的词块标记。 + +在 3.1 中,我们定义了`UnigramChunker`类,使用一元标注器给句子加词块标记。这个类的大部分代码只是用来在 NLTK 的`ChunkParserI`接口使用的词块树表示和嵌入式标注器使用的 IOB 表示之间镜像转换。类定义了两个方法:一个构造函数❶,当我们建立一个新的`UnigramChunker`时调用;以及`parse`方法❸,用来给新句子划分词块。 + +```py +class UnigramChunker(nltk.ChunkParserI): + def __init__(self, train_sents): ❶ + train_data = [[(t,c) for w,t,c in nltk.chunk.tree2conlltags(sent)] + for sent in train_sents] + self.tagger = nltk.UnigramTagger(train_data) ❷ + + def parse(self, sentence): ❸ + pos_tags = [pos for (word,pos) in sentence] + tagged_pos_tags = self.tagger.tag(pos_tags) + chunktags = [chunktag for (pos, chunktag) in tagged_pos_tags] + conlltags = [(word, pos, chunktag) for ((word,pos),chunktag) + in zip(sentence, chunktags)] + return nltk.chunk.conlltags2tree(conlltags) +``` + +构造函数❶需要训练句子的一个列表,这将是词块树的形式。它首先将训练数据转换成适合训练标注器的形式,使用`tree2conlltags`映射每个词块树到一个`word,tag,chunk`三元组的列表。然后使用转换好的训练数据训练一个一元标注器,并存储在`self.tagger`供以后使用。 + +`parse`方法❸接收一个已标注的句子作为其输入,以从那句话提取词性标记开始。它然后使用在构造函数中训练过的标注器`self.tagger`,为词性标记标注 IOB 词块标记。接下来,它提取词块标记,与原句组合,产生`conlltags`。最后,它使用`conlltags2tree`将结果转换成一个词块树。 + +现在我们有了`UnigramChunker`,可以使用 CoNLL2000 语料库训练它,并测试其表现: + +```py +>>> test_sents = conll2000.chunked_sents('test.txt', chunk_types=['NP']) +>>> train_sents = conll2000.chunked_sents('train.txt', chunk_types=['NP']) +>>> unigram_chunker = UnigramChunker(train_sents) +>>> print(unigram_chunker.evaluate(test_sents)) +ChunkParse score: + IOB Accuracy: 92.9% + Precision: 79.9% + Recall: 86.8% + F-Measure: 83.2% +``` + +这个分块器相当不错,达到整体 F 度量 83% 的得分。让我们来看一看通过使用一元标注器分配一个标记给每个语料库中出现的词性标记,它学到了什么: + +```py +>>> postags = sorted(set(pos for sent in train_sents +... for (word,pos) in sent.leaves())) +>>> print(unigram_chunker.tagger.tag(postags)) +[('#', 'B-NP'), ('$', 'B-NP'), ("''", 'O'), ('(', 'O'), (')', 'O'), + (',', 'O'), ('.', 'O'), (':', 'O'), ('CC', 'O'), ('CD', 'I-NP'), + ('DT', 'B-NP'), ('EX', 'B-NP'), ('FW', 'I-NP'), ('IN', 'O'), + ('JJ', 'I-NP'), ('JJR', 'B-NP'), ('JJS', 'I-NP'), ('MD', 'O'), + ('NN', 'I-NP'), ('NNP', 'I-NP'), ('NNPS', 'I-NP'), ('NNS', 'I-NP'), + ('PDT', 'B-NP'), ('POS', 'B-NP'), ('PRP', 'B-NP'), ('PRP$', 'B-NP'), + ('RB', 'O'), ('RBR', 'O'), ('RBS', 'B-NP'), ('RP', 'O'), ('SYM', 'O'), + ('TO', 'O'), ('UH', 'O'), ('VB', 'O'), ('VBD', 'O'), ('VBG', 'O'), + ('VBN', 'O'), ('VBP', 'O'), ('VBZ', 'O'), ('WDT', 'B-NP'), + ('WP', 'B-NP'), ('WP$', 'B-NP'), ('WRB', 'O'), ('``', 'O')] +``` + +它已经发现大多数标点符号出现在`NP`词块外,除了两种货币符号`#`和`它也发现限定词(`DT`)和所有格(`PRP + +建立了一个一元分块器,很容易建立一个二元分块器:我们只需要改变类的名称为`BigramChunker`,修改 3.1 行❷构造一个`BigramTagger`而不是`UnigramTagger`。由此产生的词块划分器的性能略高于一元词块划分器: + +```py +>>> bigram_chunker = BigramChunker(train_sents) +>>> print(bigram_chunker.evaluate(test_sents)) +ChunkParse score: + IOB Accuracy: 93.3% + Precision: 82.3% + Recall: 86.8% + F-Measure: 84.5% +``` + +## 3.3 训练基于分类器的词块划分器 + +无论是基于正则表达式的词块划分器还是 N 元词块划分器,决定创建什么词块完全基于词性标记。然而,有时词性标记不足以确定一个句子应如何划分词块。例如,考虑下面的两个语句: + +```py +class ConsecutiveNPChunkTagger(nltk.TaggerI): ❶ + + def __init__(self, train_sents): + train_set = [] + for tagged_sent in train_sents: + untagged_sent = nltk.tag.untag(tagged_sent) + history = [] + for i, (word, tag) in enumerate(tagged_sent): + featureset = npchunk_features(untagged_sent, i, history) ❷ + train_set.append( (featureset, tag) ) + history.append(tag) + self.classifier = nltk.MaxentClassifier.train( ❸ + train_set, algorithm='megam', trace=0) + + def tag(self, sentence): + history = [] + for i, word in enumerate(sentence): + featureset = npchunk_features(sentence, i, history) + tag = self.classifier.classify(featureset) + history.append(tag) + return zip(sentence, history) + +class ConsecutiveNPChunker(nltk.ChunkParserI): ❹ + def __init__(self, train_sents): + tagged_sents = [[((w,t),c) for (w,t,c) in + nltk.chunk.tree2conlltags(sent)] + for sent in train_sents] + self.tagger = ConsecutiveNPChunkTagger(tagged_sents) + + def parse(self, sentence): + tagged_sents = self.tagger.tag(sentence) + conlltags = [(w,t,c) for ((w,t),c) in tagged_sents] + return nltk.chunk.conlltags2tree(conlltags) +``` + +留下来唯一需要填写的是特征提取器。首先,我们定义一个简单的特征提取器,它只是提供了当前词符的词性标记。使用此特征提取器,我们的基于分类器的词块划分器的表现与一元词块划分器非常类似: + +```py +>>> def npchunk_features(sentence, i, history): +... word, pos = sentence[i] +... return {"pos": pos} +>>> chunker = ConsecutiveNPChunker(train_sents) +>>> print(chunker.evaluate(test_sents)) +ChunkParse score: + IOB Accuracy: 92.9% + Precision: 79.9% + Recall: 86.7% + F-Measure: 83.2% +``` + +我们还可以添加一个特征表示前面词的词性标记。添加此特征允许词块划分器模拟相邻标记之间的相互作用,由此产生的词块划分器与二元词块划分器非常接近。 + +```py +>>> def npchunk_features(sentence, i, history): +... word, pos = sentence[i] +... if i == 0: +... prevword, prevpos = "", "" +... else: +... prevword, prevpos = sentence[i-1] +... return {"pos": pos, "prevpos": prevpos} +>>> chunker = ConsecutiveNPChunker(train_sents) +>>> print(chunker.evaluate(test_sents)) +ChunkParse score: + IOB Accuracy: 93.6% + Precision: 81.9% + Recall: 87.2% + F-Measure: 84.5% +``` + +下一步,我们将尝试为当前词增加特征,因为我们假设这个词的内容应该对词块划有用。我们发现这个特征确实提高了词块划分器的表现,大约 1.5 个百分点(相应的错误率减少大约 10%)。 + +```py +>>> def npchunk_features(sentence, i, history): +... word, pos = sentence[i] +... if i == 0: +... prevword, prevpos = "", "" +... else: +... prevword, prevpos = sentence[i-1] +... return {"pos": pos, "word": word, "prevpos": prevpos} +>>> chunker = ConsecutiveNPChunker(train_sents) +>>> print(chunker.evaluate(test_sents)) +ChunkParse score: + IOB Accuracy: 94.5% + Precision: 84.2% + Recall: 89.4% + F-Measure: 86.7% +``` + +最后,我们尝试用多种附加特征扩展特征提取器,例如预取特征❶、配对特征❷和复杂的语境特征❸。这最后一个特征,称为`tags-since-dt`,创建一个字符串,描述自最近的限定词以来遇到的所有词性标记,或如果没有限定词则在索引`i`之前自语句开始以来遇到的所有词性标记。 + +```py +>>> def npchunk_features(sentence, i, history): +... word, pos = sentence[i] +... if i == 0: +... prevword, prevpos = "", "" +... else: +... prevword, prevpos = sentence[i-1] +... if i == len(sentence)-1: +... nextword, nextpos = "", "" +... else: +... nextword, nextpos = sentence[i+1] +... return {"pos": pos, +... "word": word, +... "prevpos": prevpos, +... "nextpos": nextpos, ❶ +... "prevpos+pos": "%s+%s" % (prevpos, pos), ❷ +... "pos+nextpos": "%s+%s" % (pos, nextpos), +... "tags-since-dt": tags_since_dt(sentence, i)} ❸ +``` + +```py +>>> def tags_since_dt(sentence, i): +... tags = set() +... for word, pos in sentence[:i]: +... if pos == 'DT': +... tags = set() +... else: +... tags.add(pos) +... return '+'.join(sorted(tags)) +``` + +```py +>>> chunker = ConsecutiveNPChunker(train_sents) +>>> print(chunker.evaluate(test_sents)) +ChunkParse score: + IOB Accuracy: 96.0% + Precision: 88.6% + Recall: 91.0% + F-Measure: 89.8% +``` + +注意 + +**轮到你来**:尝试为特征提取器函数`npchunk_features`增加不同的特征,看看是否可以进一步改善`NP`词块划分器的表现。 + +## 4 语言结构中的递归 + +## 4.1 用级联词块划分器构建嵌套结构 + +到目前为止,我们的词块结构一直是相对平的。已标注词符组成的树在如`NP`这样的词块节点下任意组合。然而,只需创建一个包含递归规则的多级的词块语法,就可以建立任意深度的词块结构。4.1 是名词短语、介词短语、动词短语和句子的模式。这是一个四级词块语法器,可以用来创建深度最多为 4 的结构。 + +```py +grammar = r""" + NP: {+} # Chunk sequences of DT, JJ, NN + PP: {} # Chunk prepositions followed by NP + VP: {+$} # Chunk verbs and their arguments + CLAUSE: {} # Chunk NP, VP + """ +cp = nltk.RegexpParser(grammar) +sentence = [("Mary", "NN"), ("saw", "VBD"), ("the", "DT"), ("cat", "NN"), + ("sit", "VB"), ("on", "IN"), ("the", "DT"), ("mat", "NN")] +``` + +不幸的是,这一结果丢掉了`saw`为首的`VP`。它还有其他缺陷。当我们将此词块划分器应用到一个有更深嵌套的句子时,让我们看看会发生什么。请注意,它无法识别❶开始的`VP`词块。 + +```py +>>> sentence = [("John", "NNP"), ("thinks", "VBZ"), ("Mary", "NN"), +... ("saw", "VBD"), ("the", "DT"), ("cat", "NN"), ("sit", "VB"), +... ("on", "IN"), ("the", "DT"), ("mat", "NN")] +>>> print(cp.parse(sentence)) +(S + (NP John/NNP) + thinks/VBZ + (NP Mary/NN) + saw/VBD # [_saw-vbd] + (CLAUSE + (NP the/DT cat/NN) + (VP sit/VB (PP on/IN (NP the/DT mat/NN))))) +``` + +这些问题的解决方案是让词块划分器在它的模式中循环:尝试完所有模式之后,重复此过程。我们添加一个可选的第二个参数`loop`指定这套模式应该循环的次数: + +```py +>>> cp = nltk.RegexpParser(grammar, loop=2) +>>> print(cp.parse(sentence)) +(S + (NP John/NNP) + thinks/VBZ + (CLAUSE + (NP Mary/NN) + (VP + saw/VBD + (CLAUSE + (NP the/DT cat/NN) + (VP sit/VB (PP on/IN (NP the/DT mat/NN))))))) +``` + +注意 + +这个级联过程使我们能创建深层结构。然而,创建和调试级联过程是困难的,关键点是它能更有效地做全面的分析(见第 8 章)。另外,级联过程只能产生固定深度的树(不超过级联级数),完整的句法分析这是不够的。 + +## 4.2 Trees + +树是一组连接的加标签节点,从一个特殊的根节点沿一条唯一的路径到达每个节点。下面是一棵树的例子(注意它们标准的画法是颠倒的): + +```py +(S + (NP Alice) + (VP + (V chased) + (NP + (Det the) + (N rabbit)))) +``` + +虽然我们将只集中关注语法树,树可以用来编码任何同构的超越语言形式序列的层次结构(如形态结构、篇章结构)。一般情况下,叶子和节点值不一定要是字符串。 + +在 NLTK 中,我们通过给一个节点添加标签和一系列的孩子创建一棵树: + +```py +>>> tree1 = nltk.Tree('NP', ['Alice']) +>>> print(tree1) +(NP Alice) +>>> tree2 = nltk.Tree('NP', ['the', 'rabbit']) +>>> print(tree2) +(NP the rabbit) +``` + +我们可以将这些不断合并成更大的树,如下所示: + +```py +>>> tree3 = nltk.Tree('VP', ['chased', tree2]) +>>> tree4 = nltk.Tree('S', [tree1, tree3]) +>>> print(tree4) +(S (NP Alice) (VP chased (NP the rabbit))) +``` + +下面是树对象的一些的方法: + +```py +>>> print(tree4[1]) +(VP chased (NP the rabbit)) +>>> tree4[1].label() +'VP' +>>> tree4.leaves() +['Alice', 'chased', 'the', 'rabbit'] +>>> tree4[1][1][1] +'rabbit' +``` + +复杂的树用括号表示难以阅读。在这些情况下,`draw`方法是非常有用的。它会打开一个新窗口,包含树的一个图形表示。树显示窗口可以放大和缩小,子树可以折叠和展开,并将图形表示输出为一个 postscript 文件(包含在一个文档中)。 + +```py +>>> tree3.draw() +``` + +![Images/parse_draw.png](Images/96fd8d34602a08c09a19f5b2c5c19380.jpg) + +## 4.3 树遍历 + +使用递归函数来遍历树是标准的做法。4.2 中的内容进行了演示。 + +```py +def traverse(t): + try: + t.label() + except AttributeError: + print(t, end=" ") + else: + # Now we know that t.node is defined + print('(', t.label(), end=" ") + for child in t: + traverse(child) + print(')', end=" ") + + >>> t = nltk.Tree('(S (NP Alice) (VP chased (NP the rabbit)))') + >>> traverse(t) + ( S ( NP Alice ) ( VP chased ( NP the rabbit ) ) ) +``` + +注意 + +我们已经使用了一种叫做动态类型的技术,检测`t`是一棵树(如定义了`t.label()`)。 + +## 5 命名实体识别 + +在本章开头,我们简要介绍了命名实体(NE)。命名实体是确切的名词短语,指示特定类型的个体,如组织、人、日期等。5.1 列出了一些较常用的 NE 类型。这些应该是不言自明的,除了`FACILITY`:建筑和土木工程领域的人造产品;以及`GPE`:地缘政治实体,如城市、州/省、国家。 + +表 5.1: + +常用命名实体类型 + +```py +Eddy N B-PER +Bonte N I-PER +is V O +woordvoerder N O +van Prep O +diezelfde Pron O +Hogeschool N B-ORG +. Punc O + +``` + +```py +>>> print(nltk.ne_chunk(sent)) +(S + The/DT + (GPE U.S./NNP) + is/VBZ + one/CD + ... + according/VBG + to/TO + (PERSON Brooke/NNP T./NNP Mossman/NNP) + ...) +``` + +## 6 关系抽取 + +一旦文本中的命名实体已被识别,我们就可以提取它们之间存在的关系。如前所述,我们通常会寻找指定类型的命名实体之间的关系。进行这一任务的方法之一是首先寻找所有`X, α, Y`)形式的三元组,其中`X`和`Y`是指定类型的命名实体,`α`表示`X`和`Y`之间关系的字符串。然后我们可以使用正则表达式从`α`的实体中抽出我们正在查找的关系。下面的例子搜索包含词`in`的字符串。特殊的正则表达式`(?!\b.+ing\b)`是一个否定预测先行断言,允许我们忽略如`success in supervising the transition of`中的字符串,其中`in`后面跟一个动名词。 + +```py +>>> IN = re.compile(r'.*\bin\b(?!\b.+ing)') +>>> for doc in nltk.corpus.ieer.parsed_docs('NYT_19980315'): +... for rel in nltk.sem.extract_rels('ORG', 'LOC', doc, +... corpus='ieer', pattern = IN): +... print(nltk.sem.rtuple(rel)) +[ORG: 'WHYY'] 'in' [LOC: 'Philadelphia'] +[ORG: 'McGlashan & Sarrail'] 'firm in' [LOC: 'San Mateo'] +[ORG: 'Freedom Forum'] 'in' [LOC: 'Arlington'] +[ORG: 'Brookings Institution'] ', the research group in' [LOC: 'Washington'] +[ORG: 'Idealab'] ', a self-described business incubator based in' [LOC: 'Los Angeles'] +[ORG: 'Open Text'] ', based in' [LOC: 'Waterloo'] +[ORG: 'WGBH'] 'in' [LOC: 'Boston'] +[ORG: 'Bastille Opera'] 'in' [LOC: 'Paris'] +[ORG: 'Omnicom'] 'in' [LOC: 'New York'] +[ORG: 'DDB Needham'] 'in' [LOC: 'New York'] +[ORG: 'Kaplan Thaler Group'] 'in' [LOC: 'New York'] +[ORG: 'BBDO South'] 'in' [LOC: 'Atlanta'] +[ORG: 'Georgia-Pacific'] 'in' [LOC: 'Atlanta'] +``` + +搜索关键字`in`执行的相当不错,虽然它的检索结果也会误报,例如`[ORG: House Transportation Committee] , secured the most money in the [LOC: New York]`;一种简单的基于字符串的方法排除这样的填充字符串似乎不太可能。 + +如前文所示,`conll2002`命名实体语料库的荷兰语部分不只包含命名实体标注,也包含词性标注。这允许我们设计对这些标记敏感的模式,如下面的例子所示。`clause()`方法以分条形式输出关系,其中二元关系符号作为参数`relsym`的值被指定❶。 + +```py +>>> from nltk.corpus import conll2002 +>>> vnv = """ +... ( +... is/V| # 3rd sing present and +... was/V| # past forms of the verb zijn ('be') +... werd/V| # and also present +... wordt/V # past of worden ('become) +... ) +... .* # followed by anything +... van/Prep # followed by van ('of') +... """ +>>> VAN = re.compile(vnv, re.VERBOSE) +>>> for doc in conll2002.chunked_sents('ned.train'): +... for r in nltk.sem.extract_rels('PER', 'ORG', doc, +... corpus='conll2002', pattern=VAN): +... print(nltk.sem.clause(r, relsym="VAN")) ❶ +VAN("cornet_d'elzius", 'buitenlandse_handel') +VAN('johan_rottiers', 'kardinaal_van_roey_instituut') +VAN('annie_lennox', 'eurythmics') +``` + +注意 + +**轮到你来**:替换最后一行❶为`print(rtuple(rel, lcon=True, rcon=True))`。这将显示实际的词表示两个 NE 之间关系以及它们左右的默认 10 个词的窗口的上下文。在一本荷兰语词典的帮助下,你也许能够找出为什么结果`VAN('annie_lennox', 'eurythmics')`是个误报。 + +## 7 小结 + +* 信息提取系统搜索大量非结构化文本,寻找特定类型的实体和关系,并用它们来填充有组织的数据库。这些数据库就可以用来寻找特定问题的答案。 +* 信息提取系统的典型结构以断句开始,然后是分词和词性标注。接下来在产生的数据中搜索特定类型的实体。最后,信息提取系统着眼于文本中提到的相互临近的实体,并试图确定这些实体之间是否有指定的关系。 +* 实体识别通常采用词块划分器,它分割多词符序列,并用适当的实体类型给它们加标签。常见的实体类型包括组织、人员、地点、日期、时间、货币、`GPE`(地缘政治实体)。 +* 用基于规则的系统可以构建词块划分器,例如 NLTK 中提供的`RegexpParser`类;或使用机器学习技术,如本章介绍的`ConsecutiveNPChunker`。在这两种情况中,词性标记往往是搜索词块时的一个非常重要的特征。 +* 虽然词块划分器专门用来建立相对平坦的数据结构,其中没有任何两个词块允许重叠,但它们可以被串联在一起,建立嵌套结构。 +* 关系抽取可以使用基于规则的系统,它通常查找文本中的连结实体和相关的词的特定模式;或使用机器学习系统,通常尝试从训练语料自动学习这种模式。 + +## 8 深入阅读 + +本章的附加材料发布在`http://nltk.org/`,包括网络上免费提供的资源的链接。关于使用 NLTK 词块划分的更多的例子,请看在`http://nltk.org/howto`上的词块划分 HOWTO。 + +分块的普及很大一部分是由于 Abney 的开创性的工作,如(Church, Young, & Bloothooft, 1996)。`http://www.vinartus.net/spa/97a.pdf`中描述了 Abney 的 Cass 词块划分器器。 + +根据 Ross 和 Tukey 在 1975 年的论文(Church, Young, & Bloothooft, 1996),单词词缝最初的意思是一个停用词序列。 + +IOB 格式(有时也称为 BIO 格式)由(Ramshaw & Marcus, 1995)开发用来`NP`划分词块,并被由《Conference on Natural Language Learning》在 1999 年用于`NP`加括号共享任务。CoNLL 2000 采用相同的格式标注了华尔街日报的文本作为一个`NP`词块划分共享任务的一部分。 + +(Jurafsky & Martin, 2008)的 13.5 节包含有关词块划分的一个讨论。第 22 章讲述信息提取,包括命名实体识别。有关生物学和医学中的文本挖掘的信息,请参阅(Ananiadou & McNaught, 2006)。 + +## 9 练习 + +1. ☼ IOB 格式分类标注标识符为`I`、`O`和`B`。三个标签为什么是必要的?如果我们只使用`I`和`O`标记会造成什么问题? +2. ☼ 写一个标记模式匹配包含复数中心名词在内的名词短语,如`"many/JJ researchers/NNS", "two/CD weeks/NNS", "both/DT new/JJ positions/NNS"`。通过泛化处理单数名词短语的标记模式,尝试做这个。 +3. ☼ 选择 CoNLL 语料库中三种词块类型之一。研究 CoNLL 语料库,并尝试观察组成这种类型词块的词性标记序列的任何模式。使用正则表达式词块划分器`nltk.RegexpParser`开发一个简单的词块划分器。讨论任何难以可靠划分词块的标记序列。 +4. ☼ *词块*的早期定义是出现在词缝之间的内容。开发一个词块划分器以将完整的句子作为一个单独的词块开始,然后其余的工作完全加塞词缝完成。在你自己的应用程序的帮助下,确定哪些标记(或标记序列)最有可能组成词缝。相对于完全基于词块规则的词块划分器,比较这种方法的表现和易用性。 +5. ◑ 写一个标记模式,涵盖包含动名词在内的名词短语,如`"the/DT receiving/VBG end/NN", "assistant/NN managing/VBG editor/NN"`。将这些模式加入到语法,每行一个。用自己设计的一些已标注的句子,测试你的工作。 +6. ◑ 写一个或多个标记模式处理有连接词的名词短语,如`"July/NNP and/CC August/NNP", "all/DT your/PRP$ managers/NNS and/CC supervisors/NNS", "company/NN courts/NNS and/CC adjudicators/NNS"`。 +7. ◑ 用任何你之前已经开发的词块划分器执行下列评估任务。(请注意,大多数词块划分语料库包含一些内部的不一致,以至于任何合理的基于规则的方法都将产生错误。) + 1. 在来自词块划分语料库的 100 个句子上评估你的词块划分器,报告精度、召回率和 F-量度。 + 2. 使用`chunkscore.missed()`和`chunkscore.incorrect()`方法识别你的词块划分器的错误。讨论。 + 3. 与本章的评估部分讨论的基准词块划分器比较你的词块划分器的表现。 +8. ◑ 使用基于正则表达式的词块语法`RegexpChunk`,为 CoNLL 语料库中词块类型中的一个开发一个词块划分器。使用词块、词缝、合并或拆分规则的任意组合。 +9. ◑ 有时一个词的标注不正确,例如`"12/CD or/CC so/RB cases/VBZ"`中的中心名词。不用要求手工校正标注器的输出,好的词块划分器使用标注器的错误输出也能运作。查找使用不正确的标记正确为名词短语划分词块的其他例子。 +10. ◑ 二元词块划分器的准确性得分约为 90%。研究它的错误,并试图找出它为什么不能获得 100% 的准确率。实验三元词块划分。你能够再提高准确性吗? +11. ★ 在 IOB 词块标注上应用 N 元和 Brill 标注方法。不是给词分配词性标记,在这里我们给词性标记分配 IOB 标记。例如如果标记`DT`(限定符)经常出现在一个词块的开头,它会被标注为`B`(开始)。相对于本章中讲到的正则表达式词块划分方法,评估这些词块划分方法的表现。 +12. ★ 在 5 中我们看到,通过查找有歧义的 N 元组可以得到标注准确性的上限,即在训练数据中有多种可能的方式标注的 N 元组。应用同样的方法来确定一个 N 元词块划分器的上限。 +13. ★ 挑选 CoNLL 语料库中三种词块类型之一。编写函数为你选择的类型做以下任务: + 1. 列出与此词块类型的每个实例一起出现的所有标记序列。 + 2. 计数每个标记序列的频率,并产生一个按频率减少的顺序排列的列表;每行要包含一个整数(频率)和一个标记序列。 + 3. 检查高频标记序列。使用这些作为开发一个更好的词块划分器的基础。 +14. ★ 在评估一节中提到的基准词块划分器往往会产生比它应该产生的块更大的词块。例如,短语`[every/DT time/NN] [she/PRP] sees/VBZ [a/DT newspaper/NN]`包含两个连续的词块,我们的基准词块划分器不正确地将前两个结合: `[every/DT time/NN she/PRP]`。写一个程序,找出这些通常出现在一个词块的开头的词块内部的标记有哪些,然后设计一个或多个规则分裂这些词块。将这些与现有的基准词块划分器组合,重新评估它,看看你是否已经发现了一个改进的基准。 +15. ★ 开发一个`NP`词块划分器,转换 POS 标注文本为元组的一个列表,其中每个元组由一个后面跟一个名词短语和介词的动词组成,如`the little cat sat on the mat`变成`('sat', 'on', 'NP')`... +16. ★ 宾州树库样例包含一部分已标注的《华尔街日报》文本,已经按名词短语划分词块。其格式使用方括号,我们已经在本章遇到它了几次。该语料可以使用`for sent in nltk.corpus.treebank_chunk.chunked_sents(fileid)`来访问。这些都是平坦的树,正如我们使用`nltk.corpus.conll2000.chunked_sents()`得到的一样。 + 1. 函数`nltk.tree.pprint()`和`nltk.chunk.tree2conllstr()`可以用来从一棵树创建树库和 IOB 字符串。编写函数`chunk2brackets()`和`chunk2iob()`,以一个单独的词块树为它们唯一的参数,返回所需的多行字符串表示。 + 2. 写命令行转换工具`bracket2iob.py`和`iob2bracket.py`,(分别)读取树库或 CoNLL 格式的一个文件,将它转换为其他格式。(从 NLTK 语料库获得一些原始的树库或 CoNLL 数据,保存到一个文件,然后使用`for line in open(filename)`从 Python 访问它。) +17. ★ 一个 N 元词块划分器可以使用除当前词性标记和`n-1`个前面的词块的标记以外其他信息。调查其他的上下文模型,如`n-1`个前面的词性标记,或一个写前面词块标记连同前面和后面的词性标记的组合。 +18. ★ 思考一个 N 元标注器使用临近的标记的方式。现在观察一个词块划分器可能如何重新使用这个序列信息。例如:这两个任务将使用名词往往跟在形容词后面(英文中)的信息。这会出现相同的信息被保存在两个地方的情况。随着规则集规模增长,这会成为一个问题吗?如果是,推测可能会解决这个问题的任何方式。 + +## 关于本文档... + +针对 NLTK 3.0 作出更新。本章来自于《Python 自然语言处理》,[Steven Bird](http://estive.net/), [Ewan Klein](http://homepages.inf.ed.ac.uk/ewan/) 和 [Edward Loper](http://ed.loper.org/),Copyright © 2014 作者所有。本章依据 [*Creative Commons Attribution-Noncommercial-No Derivative Works 3\.0 United States License*](http://creativecommons.org/licenses/by-nc-nd/3.0/us/) 条款,与[*自然语言工具包*](http://nltk.org/) 3.0 版一起发行。 + +本文档构建于星期三 2015 年 7 月 1 日 12:30:05 AEST \ No newline at end of file diff --git a/docs/nlp-py-2e/8.md b/docs/nlp-py-2e/8.md new file mode 100644 index 0000000000000000000000000000000000000000..9cb7a886f936adebf9176e30140b2fc99d3f691a --- /dev/null +++ b/docs/nlp-py-2e/8.md @@ -0,0 +1,571 @@ +# 8 分析句子结构 + +前面的章节重点关注词:如何识别它们,分析它们的结构,分配给他们词汇类别,以及获得它们的含义。我们还看到了如何识别词序列或 N 元组中的模式。然而,这些方法只触碰到支配句子的复杂约束的表面。我们需要一种方法处理自然语言中显著的歧义。我们还需要能够应对这样一个事实,句子有无限的可能,而我们只能写有限的程序来分析其结构和发现它们的含义。 + +本章的目的是要回答下列问题: + +1. 我们如何使用形式化语法来描述无限的句子集合的结构? +2. 我们如何使用句法树来表示句子结构? +3. 语法分析器如何分析一个句子并自动构建句法树? + +一路上,我们将覆盖英语句法的基础,并看到句子含义有系统化的一面,只要我们确定了句子结构,将更容易捕捉。 + +## 1 一些语法困境 + +## 1.1 语言数据和无限可能性 + +前面的章节中已经为你讲述了如何处理和分析的文本语料库,我们一直强调处理大量的每天都在增加的电子语言数据是 NLP 的挑战。让我们更加细致的思考这些数据,做一个思想上的实验,我们有一个巨大的语料库,包括在过去 50 年中英文表达或写成的一切。我们称这个语料库为“现代英语”合理吗?有许多为什么我们的回答可能是否定的的原因。回想一下,在 3 中,我们让你搜索网络查找`the of`模式的实例。虽然很容易在网上找到包含这个词序列的例子,例如`New man at the of IMG`(见`http://www.telegraph.co.uk/sport/2387900/New-man-at-the-of-IMG.html`),说英语的人会说大多数这样的例子是错误的,因此它们根本不是英语。 + +因此,我们可以说,“现代英语”并不等同于我们想象中的语料库中的非常大的词序列的集合。说英语的人可以判断这些序列,并将拒绝其中一些不合语法的。 + +同样,组成一个新的句子,并让说话者认为它是非常好的英语是很容易的。例如,句子有一个有趣的属性,它们可以嵌入更大的句子中。考虑下面的句子: + +```py +>>> groucho_grammar = nltk.CFG.fromstring(""" +... S -> NP VP +... PP -> P NP +... NP -> Det N | Det N PP | 'I' +... VP -> V NP | VP PP +... Det -> 'an' | 'my' +... N -> 'elephant' | 'pajamas' +... V -> 'shot' +... P -> 'in' +... """) +``` + +这个文法允许以两种方式分析句子,取决于介词短语`in my pajamas`是描述大象还是枪击事件。 + +```py +>>> sent = ['I', 'shot', 'an', 'elephant', 'in', 'my', 'pajamas'] +>>> parser = nltk.ChartParser(groucho_grammar) +>>> for tree in parser.parse(sent): +... print(tree) +... +(S + (NP I) + (VP + (VP (V shot) (NP (Det an) (N elephant))) + (PP (P in) (NP (Det my) (N pajamas))))) +(S + (NP I) + (VP + (V shot) + (NP (Det an) (N elephant) (PP (P in) (NP (Det my) (N pajamas)))))) +``` + +程序产生两个括号括起的结构,我们可以用树来表示它们,如`(3b)`所示: + +```py +grammar1 = nltk.CFG.fromstring(""" + S -> NP VP + VP -> V NP | V NP PP + PP -> P NP + V -> "saw" | "ate" | "walked" + NP -> "John" | "Mary" | "Bob" | Det N | Det N PP + Det -> "a" | "an" | "the" | "my" + N -> "man" | "dog" | "cat" | "telescope" | "park" + P -> "in" | "on" | "by" | "with" + """) +``` + +在 3.1 中的语法包含涉及各种句法类型的产生式,如在 3.1 中所列出的。 + +表 3.1: + +句法类型 + +```py +>>> grammar1 = nltk.data.load('file:mygrammar.cfg') +>>> sent = "Mary saw Bob".split() +>>> rd_parser = nltk.RecursiveDescentParser(grammar1) +>>> for tree in rd_parser.parse(sent): +... print(tree) +``` + +确保你的文件名后缀为`.cfg`,并且字符串`'file:mygrammar.cfg'`中间没有空格符。如果命令`print(tree)`没有产生任何输出,这可能是因为你的句子`sent`并不符合你的语法。在这种情况下,可以将分析器的跟踪设置打开:`rd_parser = nltk.RecursiveDescentParser(grammar1, trace=2)`。你还可以查看当前使用的语法中的产生式,使用命令`for p in grammar1.productions(): print(p)`。 + +当你编写 CFG 在 NLTK 中分析时,你不能将语法类型与词汇项目一起写在同一个产生式的右侧。因此,产生式`PP -> 'of' NP`是不允许的。另外,你不得在产生式右侧仿制多个词的词汇项。因此,不能写成`NP -> 'New York'`,而要写成类似`NP -> 'New_York'`这样的。 + +## 3.3 句法结构中的递归 + +一个语法被认为是递归的,如果语法类型出现在产生式左侧也出现在右侧,如 3.3 所示。产生式`Nom -> Adj Nom`(其中`Nom`是名词性的类别)包含`Nom`类型的直接递归,而`S`上的间接递归来自于两个产生式的组合`S -> NP VP`和`VP -> V S`。 + +```py +grammar2 = nltk.CFG.fromstring(""" + S -> NP VP + NP -> Det Nom | PropN + Nom -> Adj Nom | N + VP -> V Adj | V NP | V S | V NP PP + PP -> P NP + PropN -> 'Buster' | 'Chatterer' | 'Joe' + Det -> 'the' | 'a' + N -> 'bear' | 'squirrel' | 'tree' | 'fish' | 'log' + Adj -> 'angry' | 'frightened' | 'little' | 'tall' + V -> 'chased' | 'saw' | 'said' | 'thought' | 'was' | 'put' + P -> 'on' + """) +``` + +要看递归如何从这个语法产生,思考下面的树。`(10a)`包括嵌套的名词短语,而`(10b)`包含嵌套的句子。 + +```py +>>> rd_parser = nltk.RecursiveDescentParser(grammar1) +>>> sent = 'Mary saw a dog'.split() +>>> for tree in rd_parser.parse(sent): +... print(tree) +(S (NP Mary) (VP (V saw) (NP (Det a) (N dog)))) +``` + +注意 + +`RecursiveDescentParser()`接受一个可选的参数`trace`。如果`trace`大于零,则分析器将报告它解析一个文本的步骤。 + +递归下降分析有三个主要的缺点。首先,左递归产生式,如`NP -> NP PP`会进入死循环。第二,分析器浪费了很多时间处理不符合输入句子的词和结构。第三,回溯过程中可能会丢弃分析过的成分,它们将需要在之后再次重建。例如,从`VP -> V NP`上回溯将放弃为`NP`创建的子树。如果分析器之后处理`VP -> V NP PP`,那么`NP`子树必须重新创建。 + +递归下降分析是一种自上而下分析。自上而下分析器在检查输入之前先使用文法*预测*输入将是什么!然而,由于输入对分析器一直是可用的,从一开始就考虑输入的句子会是更明智的做法。这种方法被称为自下而上分析,在下一节中我们将看到一个例子。 + +## 4.2 移进-归约分析 + +一种简单的自下而上分析器是移进-归约分析器。与所有自下而上的分析器一样,移进-归约分析器尝试找到对应文法生产式*右侧*的词和短语的序列,用左侧的替换它们,直到整个句子归约为一个`S`。 + +移位-规约分析器反复将下一个输入词推到堆栈`(4.1)`;这是移位操作。如果堆栈上的前`n`项,匹配一些产生式的右侧的`n`个项目,那么就把它们弹出栈,并把产生式左边的项目压入栈。这种替换前`n`项为一项的操作就是规约操作。此操作只适用于堆栈的顶部;规约栈中的项目必须在后面的项目被压入栈之前做。当所有的输入都使用过,堆栈中只剩余一个项目,也就是一颗分析树作为它的根的`S`节点时,分析器完成。移位-规约分析器通过上述过程建立一颗分析树。每次弹出堆栈`n`个项目,它就将它们组合成部分的分析树,然后将这压回推栈。我们可以使用图形化示范`nltk.app.srparser()`看到移位-规约分析算法步骤。执行此分析器的六个阶段,如 4.2 所示。 + +![Images/srparser1-6.png](Images/56cee123595482cf3edaef089cb9a6a7.jpg) + +图 4.2:移进-归约分析器的六个阶段:分析器一开始把输入的第一个词转移到堆栈;一旦堆栈顶端的项目与一个文法产生式的右侧匹配,就可以将它们用那个产生式的左侧替换;当所有输入都被使用过且堆栈中只有剩余一个项目`S`时,分析成功。 + +NLTK 中提供`ShiftReduceParser()`,移进-归约分析器的一个简单的实现。这个分析器不执行任何回溯,所以它不能保证一定能找到一个文本的解析,即使确实存在一个这样的解析。此外,它最多只会找到一个解析,即使有多个解析存在。我们可以提供一个可选的`trace`参数,控制分析器报告它分析一个文本的步骤的繁琐程度。 + +```py +>>> sr_parser = nltk.ShiftReduceParser(grammar1) +>>> sent = 'Mary saw a dog'.split() +>>> for tree in sr_parser.parse(sent): +... print(tree) + (S (NP Mary) (VP (V saw) (NP (Det a) (N dog)))) +``` + +注意 + +**轮到你来**:以跟踪模式运行上述分析器,看看序列的移进和规约操作,使用`sr_parse = nltk.ShiftReduceParser(grammar1, trace=2)`。 + +移进-规约分析器可能会到达一个死胡同,而不能找到任何解析,即使输入的句子是符合语法的。这种情况发生时,没有剩余的输入,而堆栈包含不能被规约到一个`S`的项目。问题出现的原因是较早前做出的选择不能被分析器撤销(虽然图形演示中用户可以撤消它们的选择)。分析器可以做两种选择:(a)当有多种规约可能时选择哪个规约(b)当移进和规约都可以时选择哪个动作。 + +移进-规约分析器可以改进执行策略来解决这些冲突。例如,它可以通过只有在不能规约时才移进,解决移进-规约冲突;它可以通过优先执行规约操作,解决规约-规约冲突;它可以从堆栈移除更多的项目。(一个通用的移进-规约分析器,是一个“超前 LR 分析器”,普遍使用在编程语言编译器中。) + +移进-规约分析器相比递归下降分析器的好处是,它们只建立与输入中的词对应的结构。此外,每个结构它们只建立一次,例如`NP(Det(the), N(man))`只建立和压入栈一次,不管以后`VP -> V NP PP`规约或者`NP -> NP PP`规约会不会用到。 + +## 4.3 左角落分析器 + +递归下降分析器的问题之一是当它遇到一个左递归产生式时,会进入无限循环。这是因为它盲目应用文法产生式而不考虑实际输入的句子。左角落分析器是我们已经看到的自下而上与自上而下方法的混合体。 + +语法`grammar1`允许我们对`John saw Mary`生成下面的分析: + +```py +>>> text = ['I', 'shot', 'an', 'elephant', 'in', 'my', 'pajamas'] +>>> groucho_grammar.productions(rhs=text[1]) +[V -> 'shot'] +``` + +对于我们的 WFST,我们用 Python 中的列表的咧表创建一个`(n-1) × (n-1)`的矩阵,在 4.4 中的函数`init_wfst()`中用每个标识符的词汇类型初始化它。我们还定义一个实用的函数`display()`来为我们精美的输出 WFST。正如预期的那样,`V`在`(1, 2)`单元中。 + +```py +def init_wfst(tokens, grammar): + numtokens = len(tokens) + wfst = [[None for i in range(numtokens+1)] for j in range(numtokens+1)] + for i in range(numtokens): + productions = grammar.productions(rhs=tokens[i]) + wfst[i][i+1] = productions[0].lhs() + return wfst + +def complete_wfst(wfst, tokens, grammar, trace=False): + index = dict((p.rhs(), p.lhs()) for p in grammar.productions()) + numtokens = len(tokens) + for span in range(2, numtokens+1): + for start in range(numtokens+1-span): + end = start + span + for mid in range(start+1, end): + nt1, nt2 = wfst[start][mid], wfst[mid][end] + if nt1 and nt2 and (nt1,nt2) in index: + wfst[start][end] = index[(nt1,nt2)] + if trace: + print("[%s] %3s [%s] %3s [%s] ==> [%s] %3s [%s]" % \ + (start, nt1, mid, nt2, end, start, index[(nt1,nt2)], end)) + return wfst + +def display(wfst, tokens): + print('\nWFST ' + ' '.join(("%-4d" % i) for i in range(1, len(wfst)))) + for i in range(len(wfst)-1): + print("%d " % i, end=" ") + for j in range(1, len(wfst)): + print("%-4s" % (wfst[i][j] or '.'), end=" ") + print() +>>> tokens = "I shot an elephant in my pajamas".split() +>>> wfst0 = init_wfst(tokens, groucho_grammar) +>>> display(wfst0, tokens) +WFST 1 2 3 4 5 6 7 +0 NP . . . . . . +1 . V . . . . . +2 . . Det . . . . +3 . . . N . . . +4 . . . . P . . +5 . . . . . Det . +6 . . . . . . N +>>> wfst1 = complete_wfst(wfst0, tokens, groucho_grammar) +>>> display(wfst1, tokens) +WFST 1 2 3 4 5 6 7 +0 NP . . S . . S +1 . V . VP . . VP +2 . . Det NP . . . +3 . . . N . . . +4 . . . . P . PP +5 . . . . . Det NP +6 . . . . . . N +``` + +回到我们的表格表示,假设对于词`an`我们有`Det`在`(2, 3)`单元,对以词`elephant`有`N`在`(3, 4)`单元,对于`an elephant`我们应该在`(2, 4)`放入什么?我们需要找到一个形如`A -> Det N`的产生式。查询了文法,我们知道我们可以输入`(0, 2)`单元的`NP`。 + +更一般的,我们可以在`(i, j)`输入`A`,如果有一个产生式`A -> B C`,并且我们在`(i, k)`中找到非终结符`B`,在`(k, j)`中找到非终结符`C`。4.4 中的程序使用此规则完成 WFST。通过调用函数`complete_wfst()`时设置`trace`为`True`,我们看到了显示 WFST 正在被创建的跟踪输出: + +```py +>>> wfst1 = complete_wfst(wfst0, tokens, groucho_grammar, trace=True) +[2] Det [3] N [4] ==> [2] NP [4] +[5] Det [6] N [7] ==> [5] NP [7] +[1] V [2] NP [4] ==> [1] VP [4] +[4] P [5] NP [7] ==> [4] PP [7] +[0] NP [1] VP [4] ==> [0] S [4] +[1] VP [4] PP [7] ==> [1] VP [7] +[0] NP [1] VP [7] ==> [0] S [7] +``` + +例如,由于我们在`wfst[2][3]`找到`Det`,在`wfst[3][4]`找到`N`,我们可以添加`NP`到`wfst[2][4]`。 + +注意 + +为了帮助我们更简便地通过产生式的右侧检索产生式,我们为语法创建一个索引。这是空间-时间权衡的一个例子:我们对语法做反向查找,每次我们想要通过右侧查找产生式时不必遍历整个产生式列表。 + +![Images/chart_positions2.png](Images/eb630c6034e9ed7274ef2e04b9694347.jpg) + +图 4.5:图数据结构:图中额外的边表示非终结符。 + +我们得出结论,只要我们已经在`(0, 7)`单元构建了一个`S`节点,表明我们已经找到了一个涵盖了整个输入的句子,我们就为整个输入字符串找到了一个解析。最后的 WFST 状态如 4.5 所示。 + +请注意,在这里我们没有使用任何内置的分析函数。我们已经从零开始实现了一个完整的初级图表分析器! + +WFST 有几个缺点。首先,正如你可以看到的,WFST 本身不是一个分析树,所以该技术严格地说是认识到一个句子被一个语法承认,而不是分析它。其次,它要求每个非词汇语法生产式是二元的。虽然可以将任意的 CFG 转换为这种形式,我们宁愿使用这种方法时没有这样的规定。第三,作为一个自下而上的语法,它潜在的存在浪费,它会在不符合文法的地方提出成分。 + +最后,WFST 并不能表示句子中的结构歧义(如两个动词短语的读取)。`(1, 7)`单元中的`VP`实际上被输入了两次,一次是读取`V NP`,一次是读取`VP PP` 。这是不同的假设,第二个会覆盖第一个(虽然如此,这并不重要,因为左侧是相同的。)图表分析器使用稍微更丰富的数据结构和一些有趣的算法来解决这些问题(详细情况参见本章末尾的进一步阅读一节)。 + +注意 + +**轮到你来**:尝试交互式图表分析器应用程序`nltk.app.chartparser()`。 + +## 5 依存关系和依存文法 + +短语结构文法是关于词和词序列如何结合起来形成句子成分的。一个独特的和互补的方式,依存语法,集中关注的是词与其他词之间的关系。依存关系是一个中心词与它的依赖之间的二元对称关系。一个句子的中心词通常是动词,所有其他词要么依赖于中心词,要么依赖路径与它联通。 + +一个句子的中心词通常是动词,所有其他词要么依赖于中心词,要么依赖路径与它联通。5.1 显示一个依存关系图,箭头从中心词指出它们的依赖。 + +![Images/depgraph0.png](Images/ff868af58b8c1843c38287717b137f7c.jpg) + +图 5.1:依存结构:箭头从中心词指向它们的依赖;标签表示依赖的语法功能如:主语、宾语或修饰语。 + +5.1 中的弧加了依赖与它的中心词之间的语法功能标签。例如,`I`是`shot`(这是整个句子的中心词)的`SBJ`(主语),`in`是一个`NMOD`(`elephant`的名词修饰语)。与短语结构语法相比,依存语法可以作为一种依存关系直接用来表示语法功能。 + +下面是 NLTK 为依存语法编码的一种方式——注意它只能捕捉依存关系信息,不能指定依存关系类型: + +```py +>>> groucho_dep_grammar = nltk.DependencyGrammar.fromstring(""" +... 'shot' -> 'I' | 'elephant' | 'in' +... 'elephant' -> 'an' | 'in' +... 'in' -> 'pajamas' +... 'pajamas' -> 'my' +... """) +>>> print(groucho_dep_grammar) +Dependency grammar with 7 productions + 'shot' -> 'I' + 'shot' -> 'elephant' + 'shot' -> 'in' + 'elephant' -> 'an' + 'elephant' -> 'in' + 'in' -> 'pajamas' + 'pajamas' -> 'my' +``` + +依存关系图是一个投影,当所有的词都按线性顺序书写,边可以在词上绘制而不会交叉。这等于是说一个词及其所有后代依赖(依赖及其依赖的依赖,等等)在句子中形成一个连续的词序列。5.1 是一个投影,我们可以使用投影依存关系分析器分析很多英语句子。下面的例子演示`groucho_dep_grammar`如何提供了一种替代的方法来捕捉附着歧义,我们之前在研究短语结构语法中遇到的。 + +```py +>>> pdp = nltk.ProjectiveDependencyParser(groucho_dep_grammar) +>>> sent = 'I shot an elephant in my pajamas'.split() +>>> trees = pdp.parse(sent) +>>> for tree in trees: +... print(tree) +(shot I (elephant an (in (pajamas my)))) +(shot I (elephant an) (in (pajamas my))) +``` + +这些括号括起来的依存关系结构也可以显示为树,依赖被作为它们的中心词的孩子。 + +```py +VP -> TV NP +TV -> 'chased' | 'saw' + +``` + +## 5.2 扩大规模 + +到目前为止,我们只考虑了“玩具语法”,演示分析的关键环节的少量的语法。但有一个明显的问题就是这种做法是否可以扩大到覆盖自然语言的大型语料库。手工构建这样的一套产生式有多么困难?一般情况下,答案是:*非常困难*。即使我们允许自己使用各种形式化的工具,它们可以提供语法产生式更简洁的表示,保持对覆盖一种语言的主要成分所需要的众多产生式之间的复杂的相互作用的控制,仍然是极其困难的。换句话说,很难将语法模块化,每部分语法可以独立开发。反过来这意味着,在一个语言学家团队中分配编写语法的任务是很困难的。另一个困难是当语法扩展到包括更加广泛的成分时,适用于任何一个句子的分析的数量也相应增加。换句话说,歧义随着覆盖而增加。 + +尽管存在这些问题,一些大的合作项目在为几种语言开发基于规则的语法上已取得了积极的和令人印象深刻的结果。例如,词汇功能语法(LFG)项目、中心词驱动短语结构文法(HPSG)LinGO 矩阵框架和词汇化树邻接语法 XTAG 项目。 + +## 6 语法开发 + +分析器根据短语结构语法在句子上建立树。现在,我们上面给出的所有例子只涉及玩具语法包含少数的产生式。如果我们尝试扩大这种方法的规模来处理现实的语言语料库会发生什么?在本节中,我们将看到如何访问树库,并看看开发广泛覆盖的语法的挑战。 + +## 6.1 树库和语法 + +`corpus`模块定义了`treebank`语料的阅读器,其中包含了宾州树库语料的 10% 的样本。 + +```py +>>> from nltk.corpus import treebank +>>> t = treebank.parsed_sents('wsj_0001.mrg')[0] +>>> print(t) +(S + (NP-SBJ + (NP (NNP Pierre) (NNP Vinken)) + (, ,) + (ADJP (NP (CD 61) (NNS years)) (JJ old)) + (, ,)) + (VP + (MD will) + (VP + (VB join) + (NP (DT the) (NN board)) + (PP-CLR + (IN as) + (NP (DT a) (JJ nonexecutive) (NN director))) + (NP-TMP (NNP Nov.) (CD 29)))) + (. .)) +``` + +我们可以利用这些数据来帮助开发一个语法。例如,6.1 中的程序使用一个简单的过滤器找出带句子补语的动词。假设我们已经有一个形如`VP -> Vs S`的产生式,这个信息使我们能够识别那些包括在`Vs`的扩张中的特别的动词。 + +```py +def filter(tree): + child_nodes = [child.label() for child in tree + if isinstance(child, nltk.Tree)] + return (tree.label() == 'VP') and ('S' in child_nodes) +``` + +PP 附着语料库`nltk.corpus.ppattach`是另一个有关特别动词配价的信息源。在这里,我们演示挖掘这个语料库的技术。它找出具有固定的介词和名词的介词短语对,其中介词短语附着到`VP`还是`NP`,由选择的动词决定。 + +```py +>>> from collections import defaultdict +>>> entries = nltk.corpus.ppattach.attachments('training') +>>> table = defaultdict(lambda: defaultdict(set)) +>>> for entry in entries: +... key = entry.noun1 + '-' + entry.prep + '-' + entry.noun2 +... table[key][entry.attachment].add(entry.verb) +... +>>> for key in sorted(table): +... if len(table[key]) > 1: +... print(key, 'N:', sorted(table[key]['N']), 'V:', sorted(table[key]['V'])) +``` + +这个程序的输出行中我们发现`offer-from-group N: ['rejected'] V: ['received']`,这表示`received`期望一个单独的`PP`附着到`VP`而`rejected`不是的。和以前一样,我们可以使用此信息来帮助构建语法。 + +NLTK 语料库收集了来自 PE08 跨框架跨领域分析器评估共享任务的数据。一个更大的文法集合已准备好用于比较不同的分析器,它可以通过下载`large_grammars`包获得(如`python -m nltk.downloader large_grammars`)。 + +NLTK 语料库也收集了*中央研究院树库语料*,包括 10,000 句已分析的句子,来自*现代汉语中央研究院平衡语料库*。让我们加载并显示这个语料库中的一棵树。 + +```py +>>> nltk.corpus.sinica_treebank.parsed_sents()[3450].draw() +``` + +![Images/sinica-tree.png](Images/10a910dd6de117ab7a0ab352519f7297.jpg) + +## 6.2 有害的歧义 + +不幸的是,随着文法覆盖范围的增加和输入句子长度的增长,分析树的数量也迅速增长。事实上,它以天文数字的速度增长。 + +让我们在一个简单的例子帮助下来探讨这个问题。词`fish`既是名词又是动词。我们可以造这样的句子`fish fish fish`,意思是`fish like to fish for other fish`。(用`police`尝试一下,如果你喜欢更有意思的东西。)下面是`fish`句子的玩具文法。 + +```py +>>> grammar = nltk.CFG.fromstring(""" +... S -> NP V NP +... NP -> NP Sbar +... Sbar -> NP V +... NP -> 'fish' +... V -> 'fish' +... """) +``` + +现在,我们可以尝试分析一个较长的句子,`fish fish fish fish fish`,其中一个意思是:`fish that other fish fish are in the habit of fishing fish themselves`。我们使用 NLTK 的图表分析器,它在本章前面介绍过。这句话有两种读法。 + +```py +>>> tokens = ["fish"] * 5 +>>> cp = nltk.ChartParser(grammar) +>>> for tree in cp.parse(tokens): +... print(tree) +(S (NP fish) (V fish) (NP (NP fish) (Sbar (NP fish) (V fish)))) +(S (NP (NP fish) (Sbar (NP fish) (V fish))) (V fish) (NP fish)) +``` + +随着句子长度增加到`(3, 5, 7, ...)`,我们得到的分析树的数量是:`1; 2; 5; 14; 42; 132; 429; 1,430; 4,862; 16,796; 58,786; 208,012; …`(这是卡塔兰数,我们在 4 的练习中见过)。最后一个是句子长度为 23 的分析树的数目,这是宾州树库 WSJ 部分的句子的平均长度。对于一个长度为 50 的句子有超过`10^12`的解析,这只是 Piglet 句子长度的一半`(1)`,这些句子小孩可以毫不费力的处理。没有实际的自然语言处理系统可以为一个句子构建数以百万计的树,并根据上下文选择一个合适的。很显然,人也不会这样做! + +请注意,这个问题不是只在我们选择的例子中存在。(Church & Patil, 1982)指出`PP`附着句法歧义在像`(18)`这样的句子中也是按卡塔兰数的比例增长。 + +```py +def give(t): + return t.label() == 'VP' and len(t) > 2 and t[1].label() == 'NP'\ + and (t[2].label() == 'PP-DTV' or t[2].label() == 'NP')\ + and ('give' in t[0].leaves() or 'gave' in t[0].leaves()) +def sent(t): + return ' '.join(token for token in t.leaves() if token[0] not in '*-0') +def print_node(t, width): + output = "%s %s: %s / %s: %s" %\ + (sent(t[0]), t[1].label(), sent(t[1]), t[2].label(), sent(t[2])) + if len(output) > width: + output = output[:width] + "..." + print(output) +``` + +我们可以观察到一种强烈的倾向就是最短的补语最先出现。然而,这并没有解释类似`give NP: federal judges / NP: a raise`的形式,其中有生性起了重要作用。事实上,根据(Bresnan & Hay, 2006)的调查,存在大量的影响因素。这些偏好可以用加权语法来表示。 + +概率上下文无关语法(或 *PCFG*)是一种上下文无关语法,它的每一个产生式关联一个概率。它会产生与相应的上下文无关语法相同的文本解析,并给每个解析分配一个概率。PCFG 产生的一个解析的概率仅仅是它用到的产生式的概率的乘积。 + +最简单的方法定义一个 PCFG 是从一个加权产生式序列组成的特殊格式的字符串加载它,其中权值出现在括号里,如 6.4 所示。 + +```py +grammar = nltk.PCFG.fromstring(""" + S -> NP VP [1.0] + VP -> TV NP [0.4] + VP -> IV [0.3] + VP -> DatV NP NP [0.3] + TV -> 'saw' [1.0] + IV -> 'ate' [1.0] + DatV -> 'gave' [1.0] + NP -> 'telescopes' [0.8] + NP -> 'Jack' [0.2] + """) +``` + +有时可以很方便的将多个产生式组合成一行,如`VP -> TV NP [0.4] | IV [0.3] | DatV NP NP [0.3]`。为了确保由文法生成的树能形成概率分布,PCFG 语法强加了约束,产生式所有给定的左侧的概率之和必须为 1。6.4 中的语法符合这个约束:对`S`只有一个产生式,它的概率是 1.0;对于`VP`,`0.4+0.3+0.3=1.0`;对于`NP`,`0.8+0.2=1.0`。`parse()`返回的分析树包含概率: + +```py +>>> viterbi_parser = nltk.ViterbiParser(grammar) +>>> for tree in viterbi_parser.parse(['Jack', 'saw', 'telescopes']): +... print(tree) +(S (NP Jack) (VP (TV saw) (NP telescopes))) (p=0.064) +``` + +现在,分析树被分配了概率,一个给定的句子可能有数量庞大的可能的解析就不再是问题。分析器将负责寻找最有可能的解析。 + +## 7 小结 + +* 句子都有内部组织结构,可以用一棵树表示。组成结构的显著特点是:递归、中心词、补语和修饰语。 +* 语法是一个潜在的无限的句子集合的一个紧凑的特性;我们说,一棵树是符合语法规则的或语法树授权一棵树。 +* 语法是用于描述一个给定的短语是否可以被分配一个特定的成分或依赖结构的一种形式化模型。 +* 给定一组句法类别,上下文无关文法使用一组生产式表示某类型`A`的短语如何能够被分析成较小的序列`α[1] ... α[n]`。 +* 依存语法使用产生式指定给定的中心词的依赖是什么。 +* 一个句子有一个以上的句法分析就产生句法歧义(如介词短语附着歧义)。 +* 分析器是一个过程,为符合语法规则的句子寻找一个或多个相应的树。 +* 一个简单的自上而下分析器是递归下降分析器,在语法产生式的帮助下递归扩展开始符号(通常是`S`),尝试匹配输入的句子。这个分析器并不能处理左递归产生式(如`NP -> NP PP`)。它盲目扩充类别而不检查它们是否与输入字符串兼容的方式效率低下,而且会重复扩充同样的非终结符然后丢弃结果。 +* 一个简单的自下而上的分析器是移位-规约分析器,它把输入移到一个堆栈中,并尝试匹配堆栈顶部的项目和语法产生式右边的部分。这个分析器不能保证为输入找到一个有效的解析,即使它确实存在,它建立子结构而不检查它是否与全部语法一致。 + +## 8 深入阅读 + +本章的附加材料发布在`http://nltk.org/`,包括网络上免费提供的资源的链接。关于使用 NLTK 分析的更多的例子,请看在`http://nltk.org/howto`上的分析 HOWTO。 + +有许多关于句法的入门书籍。(O'Grady et al, 2004)是一个语言学概论,而(Radford, 1988)以容易接受的方式介绍转换语法,推荐其中的无限制依赖结构的转换文法。在形式语言学中最广泛使用的术语是生成语法,虽然它与生成并没有关系(Chomsky, 1965)。X-bar 句法来自于(Jacobs & Rosenbaum, 1970),并在(Jackendoff, 1977)得到更深的拓展(我们使用素数替代了 Chomsky 印刷上要求更高的单杠)。 + +(Burton-Roberts, 1997)是一本面向实践的关于如何分析英语成分的教科书,包含广泛的例子和练习。(Huddleston & Pullum, 2002)提供了一份最新的英语句法现象的综合分析。 + +(Jurafsky & Martin, 2008)的第 12 章讲述英语的形式文法;13.1-3 节讲述简单的分析算法和歧义处理技术;第 14 章讲述统计分析;第 16 章讲述乔姆斯基层次和自然语言的形式复杂性。(Levin, 1993)根据它们的句法属性,将英语动词划分成更细的类。 + +有几个正在进行的建立大规模的基于规则的语法的项目,如 LFG Pargram 项目`http://www2.parc.com/istl/groups/nltt/pargram/`,HPSG LinGO 矩阵框架`http://www.delph-in.net/matrix/`以及 XTAG 项目`http://www.cis.upenn.edu/~xtag/`。 + +## 9 练习 + +1. ☼ 你能想出符合语法的却可能之前从来没有被说出的句子吗?(与伙伴轮流进行。)这告诉你关于人类语言的什么? + +2. ☼ 回想一下 Strunk 和 White 的禁止在句子开头使用`however`表示`although`的意思。在网上搜索句子开头使用的`however`。这个成分使用的有多广泛? + +3. ☼ 思考句子`Kim arrived or Dana left and everyone cheered`。用括号的形式表示`and`和`or`的相对范围。产生这两种解释对应的树结构。 + +4. ☼ `Tree`类实现了各种其他有用的方法。请看`Tree`帮助文档查阅更多细节(如导入`Tree`类,然后输入`help(Tree)`)。 + +5. ☼ 在本练习中,你将手动构造一些分析树。 + + 1. 编写代码产生两棵树,对应短语`old men and women`的不同读法 + 2. 将本章中表示的任一一颗树编码为加标签的括号括起的形式,使用`nltk.Tree()`检查它是否符合语法。使用`draw()`显示树。 + 3. 如`(a)`中那样,为`The woman saw a man last Thursday`画一棵树。 +6. ☼ 写一个递归函数,遍历一颗树,返回树的深度,一颗只有一个节点的树的深度应为 0。(提示:子树的深度是其子女的最大深度加 1。) + +7. ☼ 分析 A.A. Milne 关于 Piglet 的句子,为它包含的所有句子画下划线,然后用`S`替换这些(如第一句话变为`S when:lx S`)。为这种“压缩”的句子画一个树形结构。用于建立这样一个长句的主要的句法结构是什么? + +8. ☼ 在递归下降分析器的演示中,通过选择`Edit`菜单上的`Edit Text`改变实验句子。 + +9. ☼ `grammar1`中的语法能被用来描述长度超过 20 词的句子吗? + +10. ☼ 使用图形化图表分析器接口,尝试不同的规则调用策略做实验。拿出你自己的可以使用图形界面手动执行的策略。描述步骤,并报告它的任何效率的提高(例如用结果图表示大小)。这些改进取决于语法结构吗?你觉得一个更聪明的规则调用策略能显著提升性能吗? + +11. ☼ 对于一个你已经见过的或一个你自己设计的 CFG,用笔和纸手动跟踪递归下降分析器和移位-规约分析器的执行。 + +12. ☼ 我们已经看到图表分析器增加边而从来不从图表中删除的边。为什么? + +13. ☼ 思考词序列:`Buffalo buffalo Buffalo buffalo buffalo buffalo Buffalo buffalo`。如`http://en.wikipedia.org/wiki/Buffalo_buffalo_Buffalo_buffalo_buffalo_buffalo_Buffalo_buffalo`解释的,这是一个语法正确的句子。思考此维基百科页面上表示的树形图,写一个合适的语法。正常情况下是小写,模拟听到这句话时听者会遇到的问题。你能为这句话找到其他的解析吗?当句子变长时分析树的数量如何增长?(这些句子的更多的例子可以在`http://en.wikipedia.org/wiki/List_of_homophonous_phrases`找到。) + +14. ◑ 你可以通过选择`Edit`菜单上的`Edit Grammar`修改递归下降分析器演示程序。改变第二次扩充产生式,即`NP -> Det N PP`为`NP -> NP PP`。使用`Step`按钮,尝试建立一个分析树。发生了什么? + +15. ◑ 扩展`grammar2`中的语法,将产生式中的介词扩展为不及物的,及物的和需要`PP`补语的。基于这些产生式,使用前面练习中的方法为句子`Lee ran away home`画一棵树。 + +16. ◑ 挑选一些常用动词,完成以下任务: + + 1. 写一个程序在 PP 附着语料库`nltk.corpus.ppattach`找到那些动词。找出任何这样的情况,相同的动词有两种不同的附着,其中第一个是名词,或者第二个是名词,或者介词保持不变(正如我们在 2 句法歧义中讨论过的)。 + 2. 制定 CFG 语法产生式涵盖其中一些情况。 +17. ◑ 写一个程序,比较自上而下的图表分析器与递归下降分析器的效率`(4)`。使用相同的语法和输入的句子。使用`timeit`模块比较它们的性能(见 4.7,如何做到这一点的一个例子)。 + +18. 比较自上而下、自下而上和左角落分析器的性能,使用相同的语法和 3 个符合语法的测试句子。使用`timeit`记录每个分析器在同一个句子上花费的时间。写一个函数,在这三句话上运行这三个分析器,输出`3×3`格的时间,以及行和列的总计。讨论你的发现。 + +19. ◑ 阅读“garden path”的句子。一个分析器的计算工作与人类处理这些句子的困难性有什么关系?`http://en.wikipedia.org/wiki/Garden_path_sentence` + +20. ◑ 若要在一个窗口比较多个树,我们可以使用`draw_trees()`方法。定义一些树,尝试一下: + + ```py + >>> from nltk.draw.tree import draw_trees + >>> draw_trees(tree1, tree2, tree3) + ``` + +21. ◑ 使用树中的位置,列出宾州树库前 100 个句子的主语;为了使结果更容易查看,限制从高度最高为 2 的子树提取主语。 + +22. ◑ 查看 PP 附着语料库,尝试提出一些影响`PP`附着的因素。 + +23. ◑ 在本节中,我们说过简单的用术语 N 元组不能描述所有语言学规律。思考下面的句子,尤其是短语`in his turn`的位置。这是基于 N 元组的方法的一个问题吗? + + > `What was more, the in his turn somewhat youngish Nikolay Parfenovich also turned out to be the only person in the entire world to acquire a sincere liking to our "discriminated-against" public procurator.` (Dostoevsky: The Brothers Karamazov) +24. ◑ 编写一个递归函数产生嵌套的括号括起的形式的一棵树,显示去掉叶节点之后的子树的非终结符。于是上面的关于 Pierre Vinken 的例子会产生:`[[[NNP NNP]NP , [ADJP [CD NNS]NP JJ]ADJP ,]NP-SBJ MD [VB [DT NN]NP [IN [DT JJ NN]NP]PP-CLR [NNP CD]NP-TMP]VP .]S`。连续的类别应用空格分隔。 + +25. ◑ 从古登堡工程下载一些电子图书。写一个程序扫描这些文本中任何极长的句子。你能找到的最长的句子是什么?这么长的句子的句法结构是什么? + +26. ◑ 修改函数`init_wfst()`和`complete_wfst()`,使 WFST 中每个单元的内容是一组非终端符而不是一个单独的非终结符。 + +27. ◑ 思考 4.4 中的算法。你能解释为什么分析上下文无关语法是与`n^3`成正比的,其中`n`是输入句子的长度。 + +28. ◑ 处理宾州树库语料库样本`nltk.corpus.treebank`中的每棵树,在`Tree.productions()`的帮助下提取产生式。丢弃只出现一次的产生式。具有相同的左侧和类似的右侧的产生式可以被折叠,产生一个等价的却更紧凑的规则集。编写代码输出一个紧凑的语法。 + +29. ★ 英语中定义句子`S`的主语的一种常见的方法是作为`S`的*孩子*和`VP`的*兄弟*的名词短语。写一个函数,以一句话的树为参数,返回句子主语对应的子树。如果传递给这个函数的树的根节点不是`S`或它缺少一个主语,应该怎么做? + +30. ★ 写一个函数,以一个语法(如 3.1 定义的语法)为参数,返回由这个语法随机产生的一个句子。(使用`grammar.start()`找出语法的开始符号;`grammar.productions(lhs)`得到具有指定左侧的语法的产生式的列表;`production.rhs()`得到一个产生式的右侧。) + +31. ★ 使用回溯实现移位-规约分析器的一个版本,使它找出一个句子所有可能的解析,它可以被称为“递归上升分析器”。咨询维基百科关于回溯的条目`http://en.wikipedia.org/wiki/Backtracking` + +32. ★ 正如我们在 7 中所看到的,可以将词块表示成它们的词块标签。当我们为包含`gave`的句子做这个的时候,我们发现如以下模式: + + ```py + gave NP + gave up NP in NP + gave NP up + gave NP NP + gave NP to NP + + ``` \ No newline at end of file diff --git a/docs/nlp-py-2e/9.md b/docs/nlp-py-2e/9.md new file mode 100644 index 0000000000000000000000000000000000000000..183c1e629160d508ebbc3495a59db47b7ff703a3 --- /dev/null +++ b/docs/nlp-py-2e/9.md @@ -0,0 +1,726 @@ +# 9 构建基于特征的语法 + +自然语言具有范围广泛的语法结构,用 8 中所描述的简单的方法很难处理的如此广泛的语法结构。为了获得更大的灵活性,我们改变我们对待语法类别如`S`、`NP`和`V`的方式。我们将这些原子标签分解为类似字典的结构,其特征可以为一个范围的值。 + +本章的目的是要回答下列问题: + +1. 我们怎样用特征扩展上下文无关语法框架,以获得更细粒度的对语法类别和产生式的控制? +2. 特征结构的主要形式化属性是什么,我们如何使用它们来计算? +3. 我们现在用基于特征的语法能捕捉到什么语言模式和语法结构? + +一路上,我们将介绍更多的英语句法主题,包括约定、子类别和无限制依赖成分等现象。 + +## 1 语法特征 + +在第六章中,我们描述了如何建立基于检测文本特征的分类器。那些特征可能非常简单,如提取一个单词的最后一个字母,或者更复杂一点儿,如分类器自己预测的词性标签。在本章中,我们将探讨特征在建立基于规则的语法中的作用。对比特征提取,记录已经自动检测到的特征,我们现在要*声明*词和短语的特征。我们以一个很简单的例子开始,使用字典存储特征和它们的值。 + +```py +>>> kim = {'CAT': 'NP', 'ORTH': 'Kim', 'REF': 'k'} +>>> chase = {'CAT': 'V', 'ORTH': 'chased', 'REL': 'chase'} +``` + +对象`kim`和`chase`有几个共同的特征,`CAT`(语法类别)和`ORTH`(正字法,即拼写)。此外,每一个还有更面向语义的特征:`kim['REF']`意在给出`kim`的指示物,而`chase['REL']`给出`chase`表示的关系。在基于规则的语法上下文中,这样的特征和特征值对被称为特征结构,我们将很快看到它们的替代符号。 + +特征结构包含各种有关语法实体的信息。这些信息不需要详尽无遗,我们可能要进一步增加属性。例如,对于一个动词,根据动词的参数知道它扮演的“语义角色”往往很有用。对于`chase`,主语扮演“施事”的角色,而宾语扮演“受事”角色。让我们添加这些信息,使用`'sbj'`和`'obj'`作为占位符,它会被填充,当动词和它的语法参数结合时: + +```py +>>> chase['AGT'] = 'sbj' +>>> chase['PAT'] = 'obj' +``` + +如果我们现在处理句子`Kim chased Lee`,我们要“绑定”动词的施事角色和主语,受事角色和宾语。我们可以通过链接到相关的`NP`的`REF`特征做到这个。在下面的例子中,我们做一个简单的假设:在动词直接左侧和右侧的`NP`分别是主语和宾语。我们还在例子结尾为`Lee`添加了一个特征结构。 + +```py +>>> sent = "Kim chased Lee" +>>> tokens = sent.split() +>>> lee = {'CAT': 'NP', 'ORTH': 'Lee', 'REF': 'l'} +>>> def lex2fs(word): +... for fs in [kim, lee, chase]: +... if fs['ORTH'] == word: +... return fs +>>> subj, verb, obj = lex2fs(tokens[0]), lex2fs(tokens[1]), lex2fs(tokens[2]) +>>> verb['AGT'] = subj['REF'] +>>> verb['PAT'] = obj['REF'] +>>> for k in ['ORTH', 'REL', 'AGT', 'PAT']: +... print("%-5s => %s" % (k, verb[k])) +ORTH => chased +REL => chase +AGT => k +PAT => l +``` + +同样的方法可以适用不同的动词,例如`surprise`,虽然在这种情况下,主语将扮演“源事”(`SRC`)的角色,宾语扮演“体验者”(`EXP`)的角色: + +```py +>>> surprise = {'CAT': 'V', 'ORTH': 'surprised', 'REL': 'surprise', +... 'SRC': 'sbj', 'EXP': 'obj'} +``` + +特征结构是非常强大的,但我们操纵它们的方式是极其*特别的*。我们本章接下来的任务是,显示上下文无关语法和分析如何能扩展到合适的特征结构,使我们可以一种更通用的和有原则的方式建立像这样的分析。我们将通过查看句法协议的现象作为开始;我们将展示如何使用特征典雅的表示协议约束,并在一个简单的语法中说明它们的用法。 + +由于特征结构是表示任何形式的信息的通用的数据结构,我们将从更形式化的视点简要地看着它们,并演示 NLTK 提供的特征结构的支持。在本章的最后一部分,我们将表明,特征的额外表现力开辟了一个用于描述语言结构的复杂性的广泛的可能性。 + +## 1.1 句法协议 + +下面的例子展示词序列对,其中第一个是符合语法的而第二个不是。(我们在词序列的开头用星号表示它是不符合语法的。) + +```py +S -> NP VP +NP -> Det N +VP -> V + +Det -> 'this' +N -> 'dog' +V -> 'runs' + +``` + +## 1.2 使用属性和约束 + +我们说过非正式的语言类别具有*属性*;例如,名词具有复数的属性。让我们把这个弄的更明确: + +```py +N[NUM=pl] + +``` + +注意一个句法类别可以有多个特征,例如`V[TENSE=pres, NUM=pl]`。在一般情况下,我们喜欢多少特征就可以添加多少。 + +关于 1.1 的最后的细节是语句`%start S`。这个“指令”告诉分析器以`S`作为文法的开始符号。 + +一般情况下,即使我们正在尝试开发很小的语法,把产生式放在一个文件中我们可以编辑、测试和修改是很方便的。我们将 1.1 以 NLTK 的数据格式保存为文件`'feat0.fcfg'`。你可以使用`nltk.data.load()`制作你自己的副本进行进一步的实验。 + +1.2 说明了基于特征的语法图表解析的操作。为输入分词之后,我们导入`load_parser`函数❶,以语法文件名为输入,返回一个图表分析器`cp`❷。调用分析器的`parse()`方法将迭代生成的分析树;如果文法无法分析输入,`trees`将为空,并将会包含一个或多个分析树,取决于输入是否有句法歧义。 + +```py +>>> tokens = 'Kim likes children'.split() +>>> from nltk import load_parser ❶ +>>> cp = load_parser('grammars/book_grammars/feat0.fcfg', trace=2) ❷ +>>> for tree in cp.parse(tokens): +... print(tree) +... +|.Kim .like.chil.| +Leaf Init Rule: +|[----] . .| [0:1] 'Kim' +|. [----] .| [1:2] 'likes' +|. . [----]| [2:3] 'children' +Feature Bottom Up Predict Combine Rule: +|[----] . .| [0:1] PropN[NUM='sg'] -> 'Kim' * +Feature Bottom Up Predict Combine Rule: +|[----] . .| [0:1] NP[NUM='sg'] -> PropN[NUM='sg'] * +Feature Bottom Up Predict Combine Rule: +|[----> . .| [0:1] S[] -> NP[NUM=?n] * VP[NUM=?n] {?n: 'sg'} +Feature Bottom Up Predict Combine Rule: +|. [----] .| [1:2] TV[NUM='sg', TENSE='pres'] -> 'likes' * +Feature Bottom Up Predict Combine Rule: +|. [----> .| [1:2] VP[NUM=?n, TENSE=?t] -> TV[NUM=?n, TENSE=?t] * NP[] {?n: 'sg', ?t: 'pres'} +Feature Bottom Up Predict Combine Rule: +|. . [----]| [2:3] N[NUM='pl'] -> 'children' * +Feature Bottom Up Predict Combine Rule: +|. . [----]| [2:3] NP[NUM='pl'] -> N[NUM='pl'] * +Feature Bottom Up Predict Combine Rule: +|. . [---->| [2:3] S[] -> NP[NUM=?n] * VP[NUM=?n] {?n: 'pl'} +Feature Single Edge Fundamental Rule: +|. [---------]| [1:3] VP[NUM='sg', TENSE='pres'] -> TV[NUM='sg', TENSE='pres'] NP[] * +Feature Single Edge Fundamental Rule: +|[==============]| [0:3] S[] -> NP[NUM='sg'] VP[NUM='sg'] * +(S[] + (NP[NUM='sg'] (PropN[NUM='sg'] Kim)) + (VP[NUM='sg', TENSE='pres'] + (TV[NUM='sg', TENSE='pres'] likes) + (NP[NUM='pl'] (N[NUM='pl'] children)))) +``` + +分析过程中的细节对于当前的目标并不重要。然而,有一个实施上的问题与我们前面的讨论语法的大小有关。分析包含特征限制的产生式的一种可行的方法是编译出问题中特征的所有可接受的值,是我们最终得到一个大的完全指定的`(6)`中那样的 CFG。相比之下,前面例子中显示的分析器过程直接与给定语法的未指定的产生式一起运作。特征值从词汇条目“向上流动”,变量值于是通过如`{?n: 'sg', ?t: 'pres'}`这样的绑定(即字典)与那些值关联起来。当分析器装配有关它正在建立的树的节点的信息时,这些变量绑定被用来实例化这些节点中的值;从而通过查找绑定中`?n`和`?t`的值,未指定的`VP[NUM=?n, TENSE=?t] -> TV[NUM=?n, TENSE=?t] NP[]`实例化为`VP[NUM='sg', TENSE='pres'] -> TV[NUM='sg', TENSE='pres'] NP[]`。 + +最后,我们可以检查生成的分析树(在这种情况下,只有一个)。 + +```py +>>> for tree in trees: print(tree) +(S[] + (NP[NUM='sg'] (PropN[NUM='sg'] Kim)) + (VP[NUM='sg', TENSE='pres'] + (TV[NUM='sg', TENSE='pres'] likes) + (NP[NUM='pl'] (N[NUM='pl'] children)))) +``` + +## 1.3 术语 + +到目前为止,我们只看到像`sg`和`pl`这样的特征值。这些简单的值通常被称为原子——也就是,它们不能被分解成更小的部分。原子值的一种特殊情况是布尔值,也就是说,值仅仅指定一个属性是真还是假。例如,我们可能要用布尔特征`AUX`区分助动词,如`can`,`may`,`will`和`do`。例如,产生式`V[TENSE=pres, AUX=+] -> 'can'`意味着`can`接受`TENSE`的值为`pres`,并且`AUX`的值为`+`或`true`。有一个广泛采用的约定用缩写表示布尔特征`f`;不用`AUX=+`或`AUX=-`,我们分别用`+AUX`和`-AUX`。这些都是缩写,然而,分析器就像`+`和`-`是其他原子值一样解释它们。`(15)`显示了一些有代表性的产生式: + +```py +V[TENSE=pres, +AUX] -> 'can' +V[TENSE=pres, +AUX] -> 'may' + +V[TENSE=pres, -AUX] -> 'walks' +V[TENSE=pres, -AUX] -> 'likes' + +``` + +在传递中,我们应该指出有显示 AVM 的替代方法;1.3 显示了一个例子。虽然特征结构呈现的`(16)`中的风格不太悦目,我们将坚持用这种格式,因为它对应我们将会从 NLTK 得到的输出。 + +关于表示,我们也注意到特征结构,像字典,对特征的*顺序*没有指定特别的意义。所以`(16)`等同于: + +```py +[AGR = [NUM = pl ]] +[ [PER = 3 ]] +[ [GND = fem ]] +[ ] +[POS = N ] + +``` + +## 2 处理特征结构 + +在本节中,我们将展示如何在 NLTK 中构建和操作特征结构。我们还将讨论统一的基本操作,这使我们能够结合两个不同的特征结构中的信息。 + +NLTK 中的特征结构使用构造函数`FeatStruct()`声明。原子特征值可以是字符串或整数。 + +```py +>>> fs1 = nltk.FeatStruct(TENSE='past', NUM='sg') +>>> print(fs1) +[ NUM = 'sg' ] +[ TENSE = 'past' ] +``` + +一个特征结构实际上只是一种字典,所以我们可以平常的方式通过索引访问它的值。我们可以用我们熟悉的方式*赋*值给特征: + +```py +>>> fs1 = nltk.FeatStruct(PER=3, NUM='pl', GND='fem') +>>> print(fs1['GND']) +fem +>>> fs1['CASE'] = 'acc' +``` + +我们还可以为特征结构定义更复杂的值,如前面所讨论的。 + +```py +>>> fs2 = nltk.FeatStruct(POS='N', AGR=fs1) +>>> print(fs2) +[ [ CASE = 'acc' ] ] +[ AGR = [ GND = 'fem' ] ] +[ [ NUM = 'pl' ] ] +[ [ PER = 3 ] ] +[ ] +[ POS = 'N' ] +>>> print(fs2['AGR']) +[ CASE = 'acc' ] +[ GND = 'fem' ] +[ NUM = 'pl' ] +[ PER = 3 ] +>>> print(fs2['AGR']['PER']) +3 +``` + +指定特征结构的另一种方法是使用包含`feature=value`格式的特征-值对的方括号括起的字符串,其中值本身可能是特征结构: + +```py +>>> print(nltk.FeatStruct("[POS='N', AGR=[PER=3, NUM='pl', GND='fem']]")) +[ [ GND = 'fem' ] ] +[ AGR = [ NUM = 'pl' ] ] +[ [ PER = 3 ] ] +[ ] +[ POS = 'N' ] +``` + +特征结构本身并不依赖于语言对象;它们是表示知识的通用目的的结构。例如,我们可以将一个人的信息用特征结构编码: + +```py +>>> print(nltk.FeatStruct(NAME='Lee', TELNO='01 27 86 42 96', AGE=33)) +[ AGE = 33 ] +[ NAME = 'Lee' ] +[ TELNO = '01 27 86 42 96' ] +``` + +在接下来的几页中,我们会使用这样的例子来探讨特征结构的标准操作。这将使我们暂时从自然语言处理转移,因为在我们回来谈论语法之前需要打下基础。坚持! + +将特征结构作为图来查看往往是有用的;更具体的,作为有向无环图(DAG)。`(19)`等同于上面的 AVM。 + +```py +>>> print(nltk.FeatStruct("""[NAME='Lee', ADDRESS=(1)[NUMBER=74, STREET='rue Pascal'], +... SPOUSE=[NAME='Kim', ADDRESS->(1)]]""")) +[ ADDRESS = (1) [ NUMBER = 74 ] ] +[ [ STREET = 'rue Pascal' ] ] +[ ] +[ NAME = 'Lee' ] +[ ] +[ SPOUSE = [ ADDRESS -> (1) ] ] +[ [ NAME = 'Kim' ] ] +``` + +括号内的整数有时也被称为标记或同指标志。整数的选择并不重要。可以有任意数目的标记在一个单独的特征结构中。 + +```py +>>> print(nltk.FeatStruct("[A='a', B=(1)[C='c'], D->(1), E->(1)]")) +[ A = 'a' ] +[ ] +[ B = (1) [ C = 'c' ] ] +[ ] +[ D -> (1) ] +[ E -> (1) ] +``` + +## 2.1 包含和统一 + +认为特征结构提供一些对象的部分信息是很正常的,在这个意义上,我们可以根据它们通用的程度给特征结构排序。例如,`(23a)`比`(23b)`具有更少特征,`(23b)`比`(23c)`具有更少特征。 + +```py +[NUMBER = 74] + +``` + +统一被正式定义为一个(部分)二元操作:`FS[0] ⊔ FS[1]`。统一是对称的,所以`FS[0] ⊔ FS[1] = FS[1] ⊔ FS[0]`。在 Python 中也是如此: + +```py +>>> print(fs2.unify(fs1)) +[ CITY = 'Paris' ] +[ NUMBER = 74 ] +[ STREET = 'rue Pascal' ] +``` + +如果我们统一两个具有包含关系的特征结构,那么统一的结果是两个中更具体的那个: + +```py +>>> fs0 = nltk.FeatStruct(A='a') +>>> fs1 = nltk.FeatStruct(A='b') +>>> fs2 = fs0.unify(fs1) +>>> print(fs2) +None +``` + +现在,如果我们看一下统一如何与结构共享相互作用,事情就变得很有趣。首先,让我们在 Python 中定义`(21)`: + +```py +>>> fs0 = nltk.FeatStruct("""[NAME=Lee, +... ADDRESS=[NUMBER=74, +... STREET='rue Pascal'], +... SPOUSE= [NAME=Kim, +... ADDRESS=[NUMBER=74, +... STREET='rue Pascal']]]""") +>>> print(fs0) +[ ADDRESS = [ NUMBER = 74 ] ] +[ [ STREET = 'rue Pascal' ] ] +[ ] +[ NAME = 'Lee' ] +[ ] +[ [ ADDRESS = [ NUMBER = 74 ] ] ] +[ SPOUSE = [ [ STREET = 'rue Pascal' ] ] ] +[ [ ] ] +[ [ NAME = 'Kim' ] ] +``` + +我们为`Kim`的地址指定一个`CITY`作为参数会发生什么?请注意,`fs1`需要包括从特征结构的根到`CITY`的整个路径。 + +```py +>>> fs1 = nltk.FeatStruct("[SPOUSE = [ADDRESS = [CITY = Paris]]]") +>>> print(fs1.unify(fs0)) +[ ADDRESS = [ NUMBER = 74 ] ] +[ [ STREET = 'rue Pascal' ] ] +[ ] +[ NAME = 'Lee' ] +[ ] +[ [ [ CITY = 'Paris' ] ] ] +[ [ ADDRESS = [ NUMBER = 74 ] ] ] +[ SPOUSE = [ [ STREET = 'rue Pascal' ] ] ] +[ [ ] ] +[ [ NAME = 'Kim' ] ] +``` + +通过对比,如果`fs1`与`fs2`的结构共享版本统一,结果是非常不同的(如图`(22)`所示): + +```py +>>> fs2 = nltk.FeatStruct("""[NAME=Lee, ADDRESS=(1)[NUMBER=74, STREET='rue Pascal'], +... SPOUSE=[NAME=Kim, ADDRESS->(1)]]""") +>>> print(fs1.unify(fs2)) +[ [ CITY = 'Paris' ] ] +[ ADDRESS = (1) [ NUMBER = 74 ] ] +[ [ STREET = 'rue Pascal' ] ] +[ ] +[ NAME = 'Lee' ] +[ ] +[ SPOUSE = [ ADDRESS -> (1) ] ] +[ [ NAME = 'Kim' ] ] +``` + +不是仅仅更新`Kim`的`Lee`的地址的“副本”,我们现在同时更新他们两个的地址。更一般的,如果统一包含指定一些路径`π`的值,那么统一同时更新等价于π的任何路径的值。 + +正如我们已经看到的,结构共享也可以使用变量表示,如`?x`。 + +```py +>>> fs1 = nltk.FeatStruct("[ADDRESS1=[NUMBER=74, STREET='rue Pascal']]") +>>> fs2 = nltk.FeatStruct("[ADDRESS1=?x, ADDRESS2=?x]") +>>> print(fs2) +[ ADDRESS1 = ?x ] +[ ADDRESS2 = ?x ] +>>> print(fs2.unify(fs1)) +[ ADDRESS1 = (1) [ NUMBER = 74 ] ] +[ [ STREET = 'rue Pascal' ] ] +[ ] +[ ADDRESS2 -> (1) ] +``` + +## 3 扩展基于特征的语法 + +在本节中,我们回到基于特征的语法,探索各种语言问题,并展示将特征纳入语法的好处。 + +## 3.1 子类别 + +第 8 中,我们增强了类别标签表示不同类别的动词,分别用标签`IV`和`TV`表示不及物动词和及物动词。这使我们能编写如下的产生式: + +```py +VP -> IV +VP -> TV NP + +``` + +## 3.2 核心词回顾 + +我们注意到,在上一节中,通过从主类别标签分解出子类别信息,我们可以表达有关动词属性的更多概括。类似的另一个属性如下:`V`类的表达式是`VP`类的短语的核心。同样,`N`是`NP`的核心词,`A`(即形容词)是`AP`的核心词,`P`(即介词)是`PP`的核心词。并非所有的短语都有核心词——例如,一般认为连词短语(如`the book and the bell`)缺乏核心词——然而,我们希望我们的语法形式能表达它所持有的父母/核心子女关系。现在,`V`和`VP`只是原子符号,我们需要找到一种方法用特征将它们关联起来(就像我们以前关联`IV`和`TV`那样)。 + +X-bar 句法通过抽象出短语级别的概念,解决了这个问题。它通常认为有三个这样的级别。如果`N`表示词汇级别,那么`N`'表示更高一层级别,对应较传统的级别`Nom`,`N`''表示短语级别,对应类别`NP`。`(34a)`演示了这种表示结构,而`(34b)`是更传统的对应。 + +```py +S -> N[BAR=2] V[BAR=2] +N[BAR=2] -> Det N[BAR=1] +N[BAR=1] -> N[BAR=1] P[BAR=2] +N[BAR=1] -> N[BAR=0] P[BAR=2] +N[BAR=1] -> N[BAR=0]XS + +``` + +## 3.3 助动词与倒装 + +倒装从句——其中的主语和动词顺序互换——出现在英语疑问句,也出现在“否定”副词之后: + +```py +S[+INV] -> V[+AUX] NP VP + +``` + +## 3.4 无限制依赖成分 + +考虑下面的对比: + +```py +>>> nltk.data.show_cfg('grammars/book_grammars/feat1.fcfg') +% start S +# ################### +# Grammar Productions +# ################### +S[-INV] -> NP VP +S[-INV]/?x -> NP VP/?x +S[-INV] -> NP S/NP +S[-INV] -> Adv[+NEG] S[+INV] +S[+INV] -> V[+AUX] NP VP +S[+INV]/?x -> V[+AUX] NP VP/?x +SBar -> Comp S[-INV] +SBar/?x -> Comp S[-INV]/?x +VP -> V[SUBCAT=intrans, -AUX] +VP -> V[SUBCAT=trans, -AUX] NP +VP/?x -> V[SUBCAT=trans, -AUX] NP/?x +VP -> V[SUBCAT=clause, -AUX] SBar +VP/?x -> V[SUBCAT=clause, -AUX] SBar/?x +VP -> V[+AUX] VP +VP/?x -> V[+AUX] VP/?x +# ################### +# Lexical Productions +# ################### +V[SUBCAT=intrans, -AUX] -> 'walk' | 'sing' +V[SUBCAT=trans, -AUX] -> 'see' | 'like' +V[SUBCAT=clause, -AUX] -> 'say' | 'claim' +V[+AUX] -> 'do' | 'can' +NP[-WH] -> 'you' | 'cats' +NP[+WH] -> 'who' +Adv[+NEG] -> 'rarely' | 'never' +NP/NP -> +Comp -> 'that' +``` + +3.1 中的语法包含一个“缺口引进”产生式,即`S[-INV] -> NP S/NP`。为了正确的预填充斜线特征,我们需要为扩展`S`,`VP`和`NP`的产生式中箭头两侧的斜线添加变量值。例如,`VP/?x -> V SBar/?x`是`VP -> V SBar`的斜线版本,也就是说,可以为一个成分的父母`VP`指定斜线值,只要也为孩子`SBar`指定同样的值。最后,`NP/NP ->`允许`NP`上的斜线信息为空字符串。使用 3.1 中的语法,我们可以分析序列`who do you claim that you like`。 + +```py +>>> tokens = 'who do you claim that you like'.split() +>>> from nltk import load_parser +>>> cp = load_parser('grammars/book_grammars/feat1.fcfg') +>>> for tree in cp.parse(tokens): +... print(tree) +(S[-INV] + (NP[+WH] who) + (S[+INV]/NP[] + (V[+AUX] do) + (NP[-WH] you) + (VP[]/NP[] + (V[-AUX, SUBCAT='clause'] claim) + (SBar[]/NP[] + (Comp[] that) + (S[-INV]/NP[] + (NP[-WH] you) + (VP[]/NP[] (V[-AUX, SUBCAT='trans'] like) (NP[]/NP[] ))))))) +``` + +这棵树的一个更易读的版本如`(52)`所示。 + +```py +>>> tokens = 'you claim that you like cats'.split() +>>> for tree in cp.parse(tokens): +... print(tree) +(S[-INV] + (NP[-WH] you) + (VP[] + (V[-AUX, SUBCAT='clause'] claim) + (SBar[] + (Comp[] that) + (S[-INV] + (NP[-WH] you) + (VP[] (V[-AUX, SUBCAT='trans'] like) (NP[-WH] cats)))))) +``` + +此外,它还允许没有`wh`结构的倒装句: + +```py +>>> tokens = 'rarely do you sing'.split() +>>> for tree in cp.parse(tokens): +... print(tree) +(S[-INV] + (Adv[+NEG] rarely) + (S[+INV] + (V[+AUX] do) + (NP[-WH] you) + (VP[] (V[-AUX, SUBCAT='intrans'] sing)))) +``` + +## 3.5 德语中的格和性别 + +与英语相比,德语的协议具有相对丰富的形态。例如,在德语中定冠词根据格、性别和数量变化,如 3.1 所示。 + +表 3.1: + +德语定冠词的形态范式 + +```py +>>> nltk.data.show_cfg('grammars/book_grammars/german.fcfg') +% start S + # Grammar Productions + S -> NP[CASE=nom, AGR=?a] VP[AGR=?a] + NP[CASE=?c, AGR=?a] -> PRO[CASE=?c, AGR=?a] + NP[CASE=?c, AGR=?a] -> Det[CASE=?c, AGR=?a] N[CASE=?c, AGR=?a] + VP[AGR=?a] -> IV[AGR=?a] + VP[AGR=?a] -> TV[OBJCASE=?c, AGR=?a] NP[CASE=?c] + # Lexical Productions + # Singular determiners + # masc + Det[CASE=nom, AGR=[GND=masc,PER=3,NUM=sg]] -> 'der' + Det[CASE=dat, AGR=[GND=masc,PER=3,NUM=sg]] -> 'dem' + Det[CASE=acc, AGR=[GND=masc,PER=3,NUM=sg]] -> 'den' + # fem + Det[CASE=nom, AGR=[GND=fem,PER=3,NUM=sg]] -> 'die' + Det[CASE=dat, AGR=[GND=fem,PER=3,NUM=sg]] -> 'der' + Det[CASE=acc, AGR=[GND=fem,PER=3,NUM=sg]] -> 'die' + # Plural determiners + Det[CASE=nom, AGR=[PER=3,NUM=pl]] -> 'die' + Det[CASE=dat, AGR=[PER=3,NUM=pl]] -> 'den' + Det[CASE=acc, AGR=[PER=3,NUM=pl]] -> 'die' + # Nouns + N[AGR=[GND=masc,PER=3,NUM=sg]] -> 'Hund' + N[CASE=nom, AGR=[GND=masc,PER=3,NUM=pl]] -> 'Hunde' + N[CASE=dat, AGR=[GND=masc,PER=3,NUM=pl]] -> 'Hunden' + N[CASE=acc, AGR=[GND=masc,PER=3,NUM=pl]] -> 'Hunde' + N[AGR=[GND=fem,PER=3,NUM=sg]] -> 'Katze' + N[AGR=[GND=fem,PER=3,NUM=pl]] -> 'Katzen' + # Pronouns + PRO[CASE=nom, AGR=[PER=1,NUM=sg]] -> 'ich' + PRO[CASE=acc, AGR=[PER=1,NUM=sg]] -> 'mich' + PRO[CASE=dat, AGR=[PER=1,NUM=sg]] -> 'mir' + PRO[CASE=nom, AGR=[PER=2,NUM=sg]] -> 'du' + PRO[CASE=nom, AGR=[PER=3,NUM=sg]] -> 'er' | 'sie' | 'es' + PRO[CASE=nom, AGR=[PER=1,NUM=pl]] -> 'wir' + PRO[CASE=acc, AGR=[PER=1,NUM=pl]] -> 'uns' + PRO[CASE=dat, AGR=[PER=1,NUM=pl]] -> 'uns' + PRO[CASE=nom, AGR=[PER=2,NUM=pl]] -> 'ihr' + PRO[CASE=nom, AGR=[PER=3,NUM=pl]] -> 'sie' + # Verbs + IV[AGR=[NUM=sg,PER=1]] -> 'komme' + IV[AGR=[NUM=sg,PER=2]] -> 'kommst' + IV[AGR=[NUM=sg,PER=3]] -> 'kommt' + IV[AGR=[NUM=pl, PER=1]] -> 'kommen' + IV[AGR=[NUM=pl, PER=2]] -> 'kommt' + IV[AGR=[NUM=pl, PER=3]] -> 'kommen' + TV[OBJCASE=acc, AGR=[NUM=sg,PER=1]] -> 'sehe' | 'mag' + TV[OBJCASE=acc, AGR=[NUM=sg,PER=2]] -> 'siehst' | 'magst' + TV[OBJCASE=acc, AGR=[NUM=sg,PER=3]] -> 'sieht' | 'mag' + TV[OBJCASE=dat, AGR=[NUM=sg,PER=1]] -> 'folge' | 'helfe' + TV[OBJCASE=dat, AGR=[NUM=sg,PER=2]] -> 'folgst' | 'hilfst' + TV[OBJCASE=dat, AGR=[NUM=sg,PER=3]] -> 'folgt' | 'hilft' + TV[OBJCASE=acc, AGR=[NUM=pl,PER=1]] -> 'sehen' | 'moegen' + TV[OBJCASE=acc, AGR=[NUM=pl,PER=2]] -> 'sieht' | 'moegt' + TV[OBJCASE=acc, AGR=[NUM=pl,PER=3]] -> 'sehen' | 'moegen' + TV[OBJCASE=dat, AGR=[NUM=pl,PER=1]] -> 'folgen' | 'helfen' + TV[OBJCASE=dat, AGR=[NUM=pl,PER=2]] -> 'folgt' | 'helft' + TV[OBJCASE=dat, AGR=[NUM=pl,PER=3]] -> 'folgen' | 'helfen' +``` + +正如你可以看到的,特征`objcase`被用来指定动词支配它的对象的格。下一个例子演示了包含支配与格的动词的句子的分析树。 + +```py +>>> tokens = 'ich folge den Katzen'.split() +>>> cp = load_parser('grammars/book_grammars/german.fcfg') +>>> for tree in cp.parse(tokens): +... print(tree) +(S[] + (NP[AGR=[NUM='sg', PER=1], CASE='nom'] + (PRO[AGR=[NUM='sg', PER=1], CASE='nom'] ich)) + (VP[AGR=[NUM='sg', PER=1]] + (TV[AGR=[NUM='sg', PER=1], OBJCASE='dat'] folge) + (NP[AGR=[GND='fem', NUM='pl', PER=3], CASE='dat'] + (Det[AGR=[NUM='pl', PER=3], CASE='dat'] den) + (N[AGR=[GND='fem', NUM='pl', PER=3]] Katzen)))) +``` + +在开发语法时,排除不符合语法的词序列往往与分析符合语法的词序列一样具有挑战性。为了能知道在哪里和为什么序列分析失败,设置`load_parser()`方法的`trace`参数可能是至关重要的。思考下面的分析故障: + +```py +>>> tokens = 'ich folge den Katze'.split() +>>> cp = load_parser('grammars/book_grammars/german.fcfg', trace=2) +>>> for tree in cp.parse(tokens): +... print(tree) +|.ich.fol.den.Kat.| +Leaf Init Rule: +|[---] . . .| [0:1] 'ich' +|. [---] . .| [1:2] 'folge' +|. . [---] .| [2:3] 'den' +|. . . [---]| [3:4] 'Katze' +Feature Bottom Up Predict Combine Rule: +|[---] . . .| [0:1] PRO[AGR=[NUM='sg', PER=1], CASE='nom'] + -> 'ich' * +Feature Bottom Up Predict Combine Rule: +|[---] . . .| [0:1] NP[AGR=[NUM='sg', PER=1], CASE='nom'] -> PRO[AGR=[NUM='sg', PER=1], CASE='nom'] * +Feature Bottom Up Predict Combine Rule: +|[---> . . .| [0:1] S[] -> NP[AGR=?a, CASE='nom'] * VP[AGR=?a] {?a: [NUM='sg', PER=1]} +Feature Bottom Up Predict Combine Rule: +|. [---] . .| [1:2] TV[AGR=[NUM='sg', PER=1], OBJCASE='dat'] -> 'folge' * +Feature Bottom Up Predict Combine Rule: +|. [---> . .| [1:2] VP[AGR=?a] -> TV[AGR=?a, OBJCASE=?c] * NP[CASE=?c] {?a: [NUM='sg', PER=1], ?c: 'dat'} +Feature Bottom Up Predict Combine Rule: +|. . [---] .| [2:3] Det[AGR=[GND='masc', NUM='sg', PER=3], CASE='acc'] -> 'den' * +|. . [---] .| [2:3] Det[AGR=[NUM='pl', PER=3], CASE='dat'] -> 'den' * +Feature Bottom Up Predict Combine Rule: +|. . [---> .| [2:3] NP[AGR=?a, CASE=?c] -> Det[AGR=?a, CASE=?c] * N[AGR=?a, CASE=?c] {?a: [NUM='pl', PER=3], ?c: 'dat'} +Feature Bottom Up Predict Combine Rule: +|. . [---> .| [2:3] NP[AGR=?a, CASE=?c] -> Det[AGR=?a, CASE=?c] * N[AGR=?a, CASE=?c] {?a: [GND='masc', NUM='sg', PER=3], ?c: 'acc'} +Feature Bottom Up Predict Combine Rule: +|. . . [---]| [3:4] N[AGR=[GND='fem', NUM='sg', PER=3]] -> 'Katze' * +``` + +跟踪中的最后两个`Scanner`行显示`den`被识别为两个可能的类别:`Det[AGR=[GND='masc', NUM='sg', PER=3], CASE='acc']`和`Det[AGR=[NUM='pl', PER=3], CASE='dat']`。我们从 3.2 中的语法知道`Katze`的类别是`N[AGR=[GND=fem, NUM=sg, PER=3]]`。因而,产生式`NP[CASE=?c, AGR=?a] -> Det[CASE=?c, AGR=?a] N[CASE=?c, AGR=?a]`中没有变量`?a`的绑定,这将满足这些限制,因为`Katze`的`AGR`值将不与`den`的任何一个`AGR`值统一,也就是`[GND='masc', NUM='sg', PER=3]`或`[NUM='pl', PER=3]`。 + +## 4 小结 + +* 上下文无关语法的传统分类是原子符号。特征结构的一个重要的作用是捕捉精细的区分,否则将需要数量翻倍的原子类别。 +* 通过使用特征值上的变量,我们可以表达语法产生式中的限制,允许不同的特征规格的实现可以相互依赖。 +* 通常情况下,我们在词汇层面指定固定的特征值,限制短语中的特征值与它们的孩子中的对应值统一。 +* 特征值可以是原子的或复杂的。原子值的一个特定类别是布尔值,按照惯例用[+/- `f`]表示。 +* 两个特征可以共享一个值(原子的或复杂的)。具有共享值的结构被称为重入。共享的值被表示为 AVM 中的数字索引(或标记)。 +* 一个特征结构中的路径是一个特征的元组,对应从图的根开始的弧的序列上的标签。 +* 两条路径是等价的,如果它们共享一个值。 +* 包含的特征结构是偏序的。`FS[0]`包含`FS[1]`,当包含在`FS[0]`中的所有信息也出现在`FS[1]`中。 +* 两种结构`FS[0]`和`FS[1]`的统一,如果成功,就是包含`FS[0]`和`FS[1]`的合并信息的特征结构`FS[2]`。 +* 如果统一在 FS 中指定一条路径`π`,那么它也指定等效与`π`的每个路径`π'`。 +* 我们可以使用特征结构建立对大量广泛语言学现象的简洁的分析,包括动词子类别,倒装结构,无限制依赖结构和格支配。 + +## 5 深入阅读 + +本章进一步的材料请参考`http://nltk.org/`,包括特征结构、特征语法和语法测试套件。 + +X-bar 句法:(Jacobs & Rosenbaum, 1970), (Jackendoff, 1977)(我们使用素数替代了 Chomsky 印刷上要求更高的单杠)。 + +协议现象的一个很好的介绍,请参阅(Corbett, 2006)。 + +理论语言学中最初使用特征的目的是捕捉语音的音素特性。例如,音`/b/`可能会被分解成结构`[+labial, +voice]`。一个重要的动机是捕捉分割的类别之间的一般性;例如`/n/`在任一`+labial`辅音前面被读作`/m/`。在乔姆斯基语法中,对一些现象,如协议,使用原子特征是很标准的,原子特征也用来捕捉跨句法类别的概括,通过类比与音韵。句法理论中使用特征的一个激进的扩展是广义短语结构语法(GPSG; (Gazdar, Klein, & and, 1985)),特别是在使用带有复杂值的特征。 + +从计算语言学的角度来看,(Dahl & Saint-Dizier, 1985)提出语言的功能方面可以被属性-值结构的统一捕获,一个类似的方法由(Grosz & Stickel, 1983)在 PATR-II 形式体系中精心设计完成。词汇功能语法(LFG; (Bresnan, 1982))的早期工作介绍了 F 结构的概念,它的主要目的是表示语法关系和与成分结构短语关联的谓词参数结构。(Shieber, 1986)提供了研究基于特征语法方面的一个极好的介绍。 + +当研究人员试图为反面例子建模时,特征结构的代数方法的一个概念上的困难出现了。另一种观点,由(Kasper & Rounds, 1986)和(Johnson, 1988)开创,认为语法涉及结构功能的描述而不是结构本身。这些描述使用逻辑操作如合取相结合,而否定仅仅是特征描述上的普通的逻辑运算。这种面向描述的观点对 LFG 从一开始就是不可或缺的(参见(Huang & Chen, 1989)),也被中心词驱动短语结构语法的较高版本采用(HPSG; (Sag & Wasow, 1999))。`http://www.cl.uni-bremen.de/HPSG-Bib/`上有 HPSG 文献的全面的参考书目。 + +本章介绍的特征结构无法捕捉语言信息中重要的限制。例如,有没有办法表达`NUM`的值只允许是`sg`和`pl`,而指定`[NUM=masc]`是反常的。同样地,我们不能说`AGR`的复合值必须包含特征`PER`,`NUM`和`gnd`的指定,但不能包含如`[SUBCAT=trans]`这样的指定。指定类型的特征结构被开发出来弥补这方面的不足。开始,我们规定总是键入特征值。对于原子值,值就是类型。例如,我们可以说`NUM`的值是类型`num`。此外,`num`是`NUM`最一般类型的值。由于类型按层次结构组织,通过指定`NUM`的值为`num`的子类型,即要么是`sg`要么是`pl`,我们可以更富含信息。 + +In the case of complex values, we say that feature structures are themselves typed. So for example the value of `AGR` will be a feature structure of type `AGR`. We also stipulate that all and only `PER`, `NUM` and `GND` are appropriate features for a structure of type `AGR`. 一个早期的关于指定类型的特征结构的很好的总结是(Emele & Zajac, 1990)。一个形式化基础的更全面的检查可以在(Carpenter, 1992)中找到,(Copestake, 2002)重点关注为面向 HPSG 的方法实现指定类型的特征结构。 + +有很多著作是关于德语的基于特征语法框架上的分析的。(Nerbonne, Netter, & Pollard, 1994)是这个主题的 HPSG 著作的一个好的起点,而(Muller, 2002)给出 HPSG 中的德语句法非常广泛和详细的分析。 + +(Jurafsky & Martin, 2008)的第 15 章讨论了特征结构、统一的算法和将统一整合到分析算法中。 + +## 6 练习 + +1. ☼ 需要什么样的限制才能正确分析词序列,如`I am happy`和`she is happy`而不是`you is happy`或`they am happy`?实现英语中动词`be`的现在时态范例的两个解决方案,首先以语法`(6)`作为起点,然后以语法 `(18)`为起点。 + +2. ☼ 开发 1.1 中语法的变体,使用特征`count`来区分下面显示的句子: + + ```py + fs1 = nltk.FeatStruct("[A = ?x, B= [C = ?x]]") + fs2 = nltk.FeatStruct("[B = [D = d]]") + fs3 = nltk.FeatStruct("[B = [C = d]]") + fs4 = nltk.FeatStruct("[A = (1)[B = b], C->(1)]") + fs5 = nltk.FeatStruct("[A = (1)[D = ?x], C = [E -> (1), F = ?x] ]") + fs6 = nltk.FeatStruct("[A = [D = d]]") + fs7 = nltk.FeatStruct("[A = [D = d], C = [F = [D = d]]]") + fs8 = nltk.FeatStruct("[A = (1)[D = ?x, G = ?x], C = [B = ?x, E -> (1)] ]") + fs9 = nltk.FeatStruct("[A = [B = b], C = [E = [G = e]]]") + fs10 = nltk.FeatStruct("[A = (1)[B = b], C -> (1)]") + ``` + + 在纸上计算下面的统一的结果是什么。(提示:你可能会发现绘制图结构很有用。) + + 1. `fs1` and `fs2` + 2. `fs1` and `fs3` + 3. `fs4` and `fs5` + 4. `fs5` and `fs6` + 5. `fs5` and `fs7` + 6. `fs8` and `fs9` + 7. `fs8` and `fs10` + + 用 Python 检查你的答案。 + +3. ◑ 列出两个包含`[A=?x, B=?x]`的特征结构。 + +4. ◑ 忽略结构共享,给出一个统一两个特征结构的非正式算法。 + +5. ◑ 扩展 3.2 中的德语语法,使它能处理所谓的动词第二顺位结构,如下所示: + + `Heute sieht der Hund die Katze. (58)` + +6. ◑ 同义动词的句法属性看上去略有不同(Levin, 1993)。思考下面的动词`loaded`、`filled`和`dumped`的语法模式。你能写语法产生式处理这些数据吗? + + ```py + + (59) + + a. The farmer *loaded* the cart with sand + + b. The farmer *loaded* sand into the cart + + c. The farmer *filled* the cart with sand + + d. *The farmer *filled* sand into the cart + + e. *The farmer *dumped* the cart with sand + + f. The farmer *dumped* sand into the cart + ``` + +7. ★ 形态范例很少是完全正规的,矩阵中的每个单元的意义有不同的实现。例如,词位`walk`的现在时态词性变化只有两种不同形式:第三人称单数的`walks`和所有其他人称和数量的组合的`walk`。一个成功的分析不应该额外要求 6 个可能的形态组合中有 5 个有相同的实现。设计和实施一个方法处理这个问题。 + +8. ★ 所谓的核心特征在父节点和核心孩子节点之间共享。例如,`TENSE`是核心特征,在一个`VP`和它的核心孩子`V`之间共享。更多细节见(Gazdar, Klein, & and, 1985)。我们看到的结构中大部分是核心结构——除了`SUBCAT`和`SLASH`。由于核心特征的共享是可以预见的,它不需要在语法产生式中明确表示。开发一种方法自动计算核心结构的这种规则行为的比重。 + +9. ★ 扩展 NLTK 中特征结构的处理,允许统一值为列表的特征,使用这个来实现一个 HPSG 风格的子类别分析,核心类别的`SUBCAT`是它的补语的类别和它直接父母的`SUBCAT`值的连结。 + +10. ★ 扩展 NLTK 的特征结构处理,允许带未指定类别的产生式,例如`S[-INV] --> ?x S/?x`。 + +11. ★ 扩展 NLTK 的特征结构处理,允许指定类型的特征结构。 + +12. ★ 挑选一些(Huddleston & Pullum, 2002)中描述的文法结构,建立一个基于特征的语法计算它们的比例。 + +## 关于本文档... + +针对 NLTK 3.0 进行更新。本章来自于《Python 自然语言处理》,[Steven Bird](http://estive.net/), [Ewan Klein](http://homepages.inf.ed.ac.uk/ewan/) 和 [Edward Loper](http://ed.loper.org/),Copyright © 2014 作者所有。本章依据 [*Creative Commons Attribution-Noncommercial-No Derivative Works 3\.0 United States License*](http://creativecommons.org/licenses/by-nc-nd/3.0/us/) 条款,与[*自然语言工具包*](http://nltk.org/) 3.0 版一起发行。 + +本文档构建于星期三 2015 年 7 月 1 日 12:30:05 AEST \ No newline at end of file diff --git a/docs/nlp-py-2e/Images/05c7618d4554f83cfa6105c528703794.jpg b/docs/nlp-py-2e/Images/05c7618d4554f83cfa6105c528703794.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a66202b3b28a81f27271c0f54c2c9de9cef23b6a Binary files /dev/null and b/docs/nlp-py-2e/Images/05c7618d4554f83cfa6105c528703794.jpg differ diff --git a/docs/nlp-py-2e/Images/07e7d99633e4a107388f7202380cce55.jpg b/docs/nlp-py-2e/Images/07e7d99633e4a107388f7202380cce55.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c9a369be517237d23d1eb51f33fac7a52d9bd1ce Binary files /dev/null and b/docs/nlp-py-2e/Images/07e7d99633e4a107388f7202380cce55.jpg differ diff --git a/docs/nlp-py-2e/Images/0ddbd56a410c886c77d1c72a84e27883.jpg b/docs/nlp-py-2e/Images/0ddbd56a410c886c77d1c72a84e27883.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1f860d6e8e049370e906ed19d6b7f9b357638a31 Binary files /dev/null and b/docs/nlp-py-2e/Images/0ddbd56a410c886c77d1c72a84e27883.jpg differ diff --git a/docs/nlp-py-2e/Images/0de8ea07b37cc379da8aeb4a0c9fd5bf.jpg b/docs/nlp-py-2e/Images/0de8ea07b37cc379da8aeb4a0c9fd5bf.jpg new file mode 100644 index 0000000000000000000000000000000000000000..51a5eb4dad0351931c97bb3b5f5a84390b6585a8 Binary files /dev/null and b/docs/nlp-py-2e/Images/0de8ea07b37cc379da8aeb4a0c9fd5bf.jpg differ diff --git a/docs/nlp-py-2e/Images/0e6e187e03f69a6548e7daa4f95e1548.jpg b/docs/nlp-py-2e/Images/0e6e187e03f69a6548e7daa4f95e1548.jpg new file mode 100644 index 0000000000000000000000000000000000000000..881a4da78ebc56a4bbd4120cde22c451a644083d Binary files /dev/null and b/docs/nlp-py-2e/Images/0e6e187e03f69a6548e7daa4f95e1548.jpg differ diff --git a/docs/nlp-py-2e/Images/0e768e8c4378c2b0b3290aab46dc770e.jpg b/docs/nlp-py-2e/Images/0e768e8c4378c2b0b3290aab46dc770e.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ae0c12174a28a1b2438b4f49d82794a7cc6148a6 Binary files /dev/null and b/docs/nlp-py-2e/Images/0e768e8c4378c2b0b3290aab46dc770e.jpg differ diff --git a/docs/nlp-py-2e/Images/0f4441cdaf35bfa4d58fc64142cf4736.jpg b/docs/nlp-py-2e/Images/0f4441cdaf35bfa4d58fc64142cf4736.jpg new file mode 100644 index 0000000000000000000000000000000000000000..08f8f6a8025e6aca43d326c81e08153d8c3cb208 Binary files /dev/null and b/docs/nlp-py-2e/Images/0f4441cdaf35bfa4d58fc64142cf4736.jpg differ diff --git a/docs/nlp-py-2e/Images/102675fd70e434164536c75bf7f8f043.jpg b/docs/nlp-py-2e/Images/102675fd70e434164536c75bf7f8f043.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9a81a389d4b2c37093b40f164b3a761a38c49ed5 Binary files /dev/null and b/docs/nlp-py-2e/Images/102675fd70e434164536c75bf7f8f043.jpg differ diff --git a/docs/nlp-py-2e/Images/1094084b61ac3f0e4416e92869c52ccd.jpg b/docs/nlp-py-2e/Images/1094084b61ac3f0e4416e92869c52ccd.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cfd8f2a91d038c446f58275baab0e19b99e28723 Binary files /dev/null and b/docs/nlp-py-2e/Images/1094084b61ac3f0e4416e92869c52ccd.jpg differ diff --git a/docs/nlp-py-2e/Images/10a8a58e33a0a6b7fb71389ea2114566.jpg b/docs/nlp-py-2e/Images/10a8a58e33a0a6b7fb71389ea2114566.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2219f36c644b88ae1cc17db4e717c08b5b9dd63a Binary files /dev/null and b/docs/nlp-py-2e/Images/10a8a58e33a0a6b7fb71389ea2114566.jpg differ diff --git a/docs/nlp-py-2e/Images/10a910dd6de117ab7a0ab352519f7297.jpg b/docs/nlp-py-2e/Images/10a910dd6de117ab7a0ab352519f7297.jpg new file mode 100644 index 0000000000000000000000000000000000000000..72fe377c6eb315e5aaf490bb32f6ae3ee538e3b6 Binary files /dev/null and b/docs/nlp-py-2e/Images/10a910dd6de117ab7a0ab352519f7297.jpg differ diff --git a/docs/nlp-py-2e/Images/12573c3a9015654728fe798e170a3c50.jpg b/docs/nlp-py-2e/Images/12573c3a9015654728fe798e170a3c50.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f8d0f58820b33eb6123831fa0d24d5ff5361b75f Binary files /dev/null and b/docs/nlp-py-2e/Images/12573c3a9015654728fe798e170a3c50.jpg differ diff --git a/docs/nlp-py-2e/Images/13361de430cd983e689417c547330bbc.jpg b/docs/nlp-py-2e/Images/13361de430cd983e689417c547330bbc.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e0ece5f407ede690235a8a548fbda0a9083baef7 Binary files /dev/null and b/docs/nlp-py-2e/Images/13361de430cd983e689417c547330bbc.jpg differ diff --git a/docs/nlp-py-2e/Images/13f25b9eba42f74ad969a74cee78551e.jpg b/docs/nlp-py-2e/Images/13f25b9eba42f74ad969a74cee78551e.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ae840901dba5ad53b65b816ff7e35759bfad1466 Binary files /dev/null and b/docs/nlp-py-2e/Images/13f25b9eba42f74ad969a74cee78551e.jpg differ diff --git a/docs/nlp-py-2e/Images/14a0a2692f06286091f0cca17de5c0f3.jpg b/docs/nlp-py-2e/Images/14a0a2692f06286091f0cca17de5c0f3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..57dfbc143608e3c20c968bfb0c146a722abff72b Binary files /dev/null and b/docs/nlp-py-2e/Images/14a0a2692f06286091f0cca17de5c0f3.jpg differ diff --git a/docs/nlp-py-2e/Images/14a15b73cb826ac3464754d6db3e9e54.jpg b/docs/nlp-py-2e/Images/14a15b73cb826ac3464754d6db3e9e54.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fe518f4f7596bf4b0ed652304f45a9007685051f Binary files /dev/null and b/docs/nlp-py-2e/Images/14a15b73cb826ac3464754d6db3e9e54.jpg differ diff --git a/docs/nlp-py-2e/Images/160f0b5ad0e4b8745b925d63377a69ba.jpg b/docs/nlp-py-2e/Images/160f0b5ad0e4b8745b925d63377a69ba.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c9a369be517237d23d1eb51f33fac7a52d9bd1ce Binary files /dev/null and b/docs/nlp-py-2e/Images/160f0b5ad0e4b8745b925d63377a69ba.jpg differ diff --git a/docs/nlp-py-2e/Images/17af2b7a4de652abd5c2c71a94cc1c7b.jpg b/docs/nlp-py-2e/Images/17af2b7a4de652abd5c2c71a94cc1c7b.jpg new file mode 100644 index 0000000000000000000000000000000000000000..92ce2f85bf238e83846cf53a68c23053f339b33c Binary files /dev/null and b/docs/nlp-py-2e/Images/17af2b7a4de652abd5c2c71a94cc1c7b.jpg differ diff --git a/docs/nlp-py-2e/Images/1ab45939cc9babb242ac45ed03a94f7a.jpg b/docs/nlp-py-2e/Images/1ab45939cc9babb242ac45ed03a94f7a.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2f2ecd393a4e555ecad47010279f2f62e257e77c Binary files /dev/null and b/docs/nlp-py-2e/Images/1ab45939cc9babb242ac45ed03a94f7a.jpg differ diff --git a/docs/nlp-py-2e/Images/1b33abb14fc8fe7c704d005736ddb323.jpg b/docs/nlp-py-2e/Images/1b33abb14fc8fe7c704d005736ddb323.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8bc37e7d25bd2c3e8e389ef70a17cd4b14590925 Binary files /dev/null and b/docs/nlp-py-2e/Images/1b33abb14fc8fe7c704d005736ddb323.jpg differ diff --git a/docs/nlp-py-2e/Images/1c54b3124863d24d17b2edec4f1d47e5.jpg b/docs/nlp-py-2e/Images/1c54b3124863d24d17b2edec4f1d47e5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e6240b5d01435109957cc0150b1cd56d1730868f Binary files /dev/null and b/docs/nlp-py-2e/Images/1c54b3124863d24d17b2edec4f1d47e5.jpg differ diff --git a/docs/nlp-py-2e/Images/1cf5b2605018e587fa94db2ac671e930.jpg b/docs/nlp-py-2e/Images/1cf5b2605018e587fa94db2ac671e930.jpg new file mode 100644 index 0000000000000000000000000000000000000000..bc4b19cc809752c7b829cd67d3d372da0078065b Binary files /dev/null and b/docs/nlp-py-2e/Images/1cf5b2605018e587fa94db2ac671e930.jpg differ diff --git a/docs/nlp-py-2e/Images/21f757d2c1451cb55bc923dcb4d8d1ab.jpg b/docs/nlp-py-2e/Images/21f757d2c1451cb55bc923dcb4d8d1ab.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e74f104efd732ef15dfbf007fcb959f8ca26298b Binary files /dev/null and b/docs/nlp-py-2e/Images/21f757d2c1451cb55bc923dcb4d8d1ab.jpg differ diff --git a/docs/nlp-py-2e/Images/24589c2eb435b25724aed562d1d47617.jpg b/docs/nlp-py-2e/Images/24589c2eb435b25724aed562d1d47617.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9b4c94ec8d3077612a4726467edfb9062266972a Binary files /dev/null and b/docs/nlp-py-2e/Images/24589c2eb435b25724aed562d1d47617.jpg differ diff --git a/docs/nlp-py-2e/Images/273b9ed1ea067e4503c00dbd193216e8.jpg b/docs/nlp-py-2e/Images/273b9ed1ea067e4503c00dbd193216e8.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3fde8ca5a432d3ef0081966385f8c19d196a5c43 Binary files /dev/null and b/docs/nlp-py-2e/Images/273b9ed1ea067e4503c00dbd193216e8.jpg differ diff --git a/docs/nlp-py-2e/Images/27ffeabf8327e1810d6ac35642a72700.jpg b/docs/nlp-py-2e/Images/27ffeabf8327e1810d6ac35642a72700.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8f8bae227d1b11844e3d721ca3f7b18e915a454e Binary files /dev/null and b/docs/nlp-py-2e/Images/27ffeabf8327e1810d6ac35642a72700.jpg differ diff --git a/docs/nlp-py-2e/Images/2ce816f11fd01927802253d100780b0a.jpg b/docs/nlp-py-2e/Images/2ce816f11fd01927802253d100780b0a.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d74ef954f36d817b0300b0b87262448f1fb19fae Binary files /dev/null and b/docs/nlp-py-2e/Images/2ce816f11fd01927802253d100780b0a.jpg differ diff --git a/docs/nlp-py-2e/Images/2cfe7a0ac0b6007d979cf3aecda8a9f5.jpg b/docs/nlp-py-2e/Images/2cfe7a0ac0b6007d979cf3aecda8a9f5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b8e541770516bd7177cec216a050431889583e4e Binary files /dev/null and b/docs/nlp-py-2e/Images/2cfe7a0ac0b6007d979cf3aecda8a9f5.jpg differ diff --git a/docs/nlp-py-2e/Images/334be383b5db7ffe3599cc03bc74bf9e.jpg b/docs/nlp-py-2e/Images/334be383b5db7ffe3599cc03bc74bf9e.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ae840901dba5ad53b65b816ff7e35759bfad1466 Binary files /dev/null and b/docs/nlp-py-2e/Images/334be383b5db7ffe3599cc03bc74bf9e.jpg differ diff --git a/docs/nlp-py-2e/Images/346344c2e5a627acfdddf948fb69cb1d.jpg b/docs/nlp-py-2e/Images/346344c2e5a627acfdddf948fb69cb1d.jpg new file mode 100644 index 0000000000000000000000000000000000000000..82ca075121646a0ebede2d2a529dbbf7ed85bb05 Binary files /dev/null and b/docs/nlp-py-2e/Images/346344c2e5a627acfdddf948fb69cb1d.jpg differ diff --git a/docs/nlp-py-2e/Images/37edef9faf625ac06477a0ab0118afca.jpg b/docs/nlp-py-2e/Images/37edef9faf625ac06477a0ab0118afca.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2a74bc95a74322a9cb2bb6b8ecca02b443ac3f80 Binary files /dev/null and b/docs/nlp-py-2e/Images/37edef9faf625ac06477a0ab0118afca.jpg differ diff --git a/docs/nlp-py-2e/Images/3a93e0258a010fdda935b4ee067411a5.jpg b/docs/nlp-py-2e/Images/3a93e0258a010fdda935b4ee067411a5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c9a369be517237d23d1eb51f33fac7a52d9bd1ce Binary files /dev/null and b/docs/nlp-py-2e/Images/3a93e0258a010fdda935b4ee067411a5.jpg differ diff --git a/docs/nlp-py-2e/Images/3edaf7564caaab7c8ddc83db1d5408a3.jpg b/docs/nlp-py-2e/Images/3edaf7564caaab7c8ddc83db1d5408a3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..28f07f716a9c82088fb62ea2e950cc9e70dc7dd2 Binary files /dev/null and b/docs/nlp-py-2e/Images/3edaf7564caaab7c8ddc83db1d5408a3.jpg differ diff --git a/docs/nlp-py-2e/Images/4150e51ab7e511f8d4f72293054ceb22.jpg b/docs/nlp-py-2e/Images/4150e51ab7e511f8d4f72293054ceb22.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c0b7cb51dbdd83fc74b59e19f3838089f5f507b3 Binary files /dev/null and b/docs/nlp-py-2e/Images/4150e51ab7e511f8d4f72293054ceb22.jpg differ diff --git a/docs/nlp-py-2e/Images/431fed60785d71efd9010589288ca55d.jpg b/docs/nlp-py-2e/Images/431fed60785d71efd9010589288ca55d.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9ab98b73863e244a172953b7e8b6b359cf9f4bd2 Binary files /dev/null and b/docs/nlp-py-2e/Images/431fed60785d71efd9010589288ca55d.jpg differ diff --git a/docs/nlp-py-2e/Images/43a0f901bdd8f343b2885d2b2e95b996.jpg b/docs/nlp-py-2e/Images/43a0f901bdd8f343b2885d2b2e95b996.jpg new file mode 100644 index 0000000000000000000000000000000000000000..caa9c0fb579a09280ba57f0f2587d909dc407606 Binary files /dev/null and b/docs/nlp-py-2e/Images/43a0f901bdd8f343b2885d2b2e95b996.jpg differ diff --git a/docs/nlp-py-2e/Images/484180fc6abc244116b30e57cb6c0cf5.jpg b/docs/nlp-py-2e/Images/484180fc6abc244116b30e57cb6c0cf5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..28522fa54086463791bf5e7eca9f2a43bf77593d Binary files /dev/null and b/docs/nlp-py-2e/Images/484180fc6abc244116b30e57cb6c0cf5.jpg differ diff --git a/docs/nlp-py-2e/Images/496754d8cdb6262f8f72e1f066bab359.jpg b/docs/nlp-py-2e/Images/496754d8cdb6262f8f72e1f066bab359.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ae840901dba5ad53b65b816ff7e35759bfad1466 Binary files /dev/null and b/docs/nlp-py-2e/Images/496754d8cdb6262f8f72e1f066bab359.jpg differ diff --git a/docs/nlp-py-2e/Images/499f8953a7d39c034e6840bdacd99d08.jpg b/docs/nlp-py-2e/Images/499f8953a7d39c034e6840bdacd99d08.jpg new file mode 100644 index 0000000000000000000000000000000000000000..98959d2c32f05f1e870b2b45f1b168ad57fb2303 Binary files /dev/null and b/docs/nlp-py-2e/Images/499f8953a7d39c034e6840bdacd99d08.jpg differ diff --git a/docs/nlp-py-2e/Images/4a87f1dccc0e18aab5ec599d8d8358d6.jpg b/docs/nlp-py-2e/Images/4a87f1dccc0e18aab5ec599d8d8358d6.jpg new file mode 100644 index 0000000000000000000000000000000000000000..deeda2036c001319d1aaab76ac7cdf7c64c2e26e Binary files /dev/null and b/docs/nlp-py-2e/Images/4a87f1dccc0e18aab5ec599d8d8358d6.jpg differ diff --git a/docs/nlp-py-2e/Images/4acbae4abe459cf45122fe134ff7672d.jpg b/docs/nlp-py-2e/Images/4acbae4abe459cf45122fe134ff7672d.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f1a8be461430f9e1707ea29a4381213fb9da8456 Binary files /dev/null and b/docs/nlp-py-2e/Images/4acbae4abe459cf45122fe134ff7672d.jpg differ diff --git a/docs/nlp-py-2e/Images/4b32a28b1aab7148420347abc990ee67.jpg b/docs/nlp-py-2e/Images/4b32a28b1aab7148420347abc990ee67.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2b9a661659b5b0a13ab5b29bdf48dcde6ee6b3b7 Binary files /dev/null and b/docs/nlp-py-2e/Images/4b32a28b1aab7148420347abc990ee67.jpg differ diff --git a/docs/nlp-py-2e/Images/4b5cae275c53c53ccc8f2f779acada3e.jpg b/docs/nlp-py-2e/Images/4b5cae275c53c53ccc8f2f779acada3e.jpg new file mode 100644 index 0000000000000000000000000000000000000000..82ca075121646a0ebede2d2a529dbbf7ed85bb05 Binary files /dev/null and b/docs/nlp-py-2e/Images/4b5cae275c53c53ccc8f2f779acada3e.jpg differ diff --git a/docs/nlp-py-2e/Images/4cdc400cf76b0354304e01aeb894877b.jpg b/docs/nlp-py-2e/Images/4cdc400cf76b0354304e01aeb894877b.jpg new file mode 100644 index 0000000000000000000000000000000000000000..efd0b698ad23ed69c27f6c81eed040306a53eb91 Binary files /dev/null and b/docs/nlp-py-2e/Images/4cdc400cf76b0354304e01aeb894877b.jpg differ diff --git a/docs/nlp-py-2e/Images/4cec5bd698a48f7e083696dd51ae9e7a.jpg b/docs/nlp-py-2e/Images/4cec5bd698a48f7e083696dd51ae9e7a.jpg new file mode 100644 index 0000000000000000000000000000000000000000..08f8f6a8025e6aca43d326c81e08153d8c3cb208 Binary files /dev/null and b/docs/nlp-py-2e/Images/4cec5bd698a48f7e083696dd51ae9e7a.jpg differ diff --git a/docs/nlp-py-2e/Images/513df73dfd52feca2c96a86dcc261c8b.jpg b/docs/nlp-py-2e/Images/513df73dfd52feca2c96a86dcc261c8b.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d486e1ec072aaa7bdbd92390b7f21a7cd79728e3 Binary files /dev/null and b/docs/nlp-py-2e/Images/513df73dfd52feca2c96a86dcc261c8b.jpg differ diff --git a/docs/nlp-py-2e/Images/532d5f3185ea7edaec68683d89a74182.jpg b/docs/nlp-py-2e/Images/532d5f3185ea7edaec68683d89a74182.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6471a26f4f70679e62e12d2f14e9018f05807da6 Binary files /dev/null and b/docs/nlp-py-2e/Images/532d5f3185ea7edaec68683d89a74182.jpg differ diff --git a/docs/nlp-py-2e/Images/542fee25c56235c899312bed3d5ee9ba.jpg b/docs/nlp-py-2e/Images/542fee25c56235c899312bed3d5ee9ba.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a93a102161a190f9ab259605204481e897abb1d5 Binary files /dev/null and b/docs/nlp-py-2e/Images/542fee25c56235c899312bed3d5ee9ba.jpg differ diff --git a/docs/nlp-py-2e/Images/55f4da85888c6e9974fea5360283035a.jpg b/docs/nlp-py-2e/Images/55f4da85888c6e9974fea5360283035a.jpg new file mode 100644 index 0000000000000000000000000000000000000000..417c239c34962e6cf7021ec9d4400a88eaae3f6d Binary files /dev/null and b/docs/nlp-py-2e/Images/55f4da85888c6e9974fea5360283035a.jpg differ diff --git a/docs/nlp-py-2e/Images/56cee123595482cf3edaef089cb9a6a7.jpg b/docs/nlp-py-2e/Images/56cee123595482cf3edaef089cb9a6a7.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ebf309ec863c857a3831d2350edc5cff8619ee1c Binary files /dev/null and b/docs/nlp-py-2e/Images/56cee123595482cf3edaef089cb9a6a7.jpg differ diff --git a/docs/nlp-py-2e/Images/5741a8d50a63172cbd2c825cda1b61d5.jpg b/docs/nlp-py-2e/Images/5741a8d50a63172cbd2c825cda1b61d5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d6a76a18b1425d1dceddcc95eda42918b11a527c Binary files /dev/null and b/docs/nlp-py-2e/Images/5741a8d50a63172cbd2c825cda1b61d5.jpg differ diff --git a/docs/nlp-py-2e/Images/58a1097522dc6fbe24eddd96cfd6cbc9.jpg b/docs/nlp-py-2e/Images/58a1097522dc6fbe24eddd96cfd6cbc9.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6da97acc998e617da70cc538010d14663202657d Binary files /dev/null and b/docs/nlp-py-2e/Images/58a1097522dc6fbe24eddd96cfd6cbc9.jpg differ diff --git a/docs/nlp-py-2e/Images/5ac0acc015eeb4b0a40c6c23b7f95395.jpg b/docs/nlp-py-2e/Images/5ac0acc015eeb4b0a40c6c23b7f95395.jpg new file mode 100644 index 0000000000000000000000000000000000000000..633add1215ddc96c097bc01237b3b84041f0e410 Binary files /dev/null and b/docs/nlp-py-2e/Images/5ac0acc015eeb4b0a40c6c23b7f95395.jpg differ diff --git a/docs/nlp-py-2e/Images/5c0c6dc75f29f253edaecb566d608aa3.jpg b/docs/nlp-py-2e/Images/5c0c6dc75f29f253edaecb566d608aa3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fea57bdbbbba7851663c4a6f65ac81c2e0d9f50d Binary files /dev/null and b/docs/nlp-py-2e/Images/5c0c6dc75f29f253edaecb566d608aa3.jpg differ diff --git a/docs/nlp-py-2e/Images/5c3caa46daa29a053a04713bab6e4f03.jpg b/docs/nlp-py-2e/Images/5c3caa46daa29a053a04713bab6e4f03.jpg new file mode 100644 index 0000000000000000000000000000000000000000..34b85ab58854417347960fd497f1f0cd59c122ec Binary files /dev/null and b/docs/nlp-py-2e/Images/5c3caa46daa29a053a04713bab6e4f03.jpg differ diff --git a/docs/nlp-py-2e/Images/5e197b7d253f66454a97af2a93c30a8e.jpg b/docs/nlp-py-2e/Images/5e197b7d253f66454a97af2a93c30a8e.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a99158a355bce7409cbab517b0ab8ecb2ba1398c Binary files /dev/null and b/docs/nlp-py-2e/Images/5e197b7d253f66454a97af2a93c30a8e.jpg differ diff --git a/docs/nlp-py-2e/Images/5eeb4cf55b6d18d4bcb098fc72ddc6d7.jpg b/docs/nlp-py-2e/Images/5eeb4cf55b6d18d4bcb098fc72ddc6d7.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a1e07013854ec291680adfc44bdc863dab99a4eb Binary files /dev/null and b/docs/nlp-py-2e/Images/5eeb4cf55b6d18d4bcb098fc72ddc6d7.jpg differ diff --git a/docs/nlp-py-2e/Images/6372ba4f28e69f0b220c75a9b2f4decf.jpg b/docs/nlp-py-2e/Images/6372ba4f28e69f0b220c75a9b2f4decf.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ae840901dba5ad53b65b816ff7e35759bfad1466 Binary files /dev/null and b/docs/nlp-py-2e/Images/6372ba4f28e69f0b220c75a9b2f4decf.jpg differ diff --git a/docs/nlp-py-2e/Images/63a8e4c47e813ba9630363f9b203a19a.jpg b/docs/nlp-py-2e/Images/63a8e4c47e813ba9630363f9b203a19a.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9b4c94ec8d3077612a4726467edfb9062266972a Binary files /dev/null and b/docs/nlp-py-2e/Images/63a8e4c47e813ba9630363f9b203a19a.jpg differ diff --git a/docs/nlp-py-2e/Images/64864d38550248d5bd9b82eeb6f0583b.jpg b/docs/nlp-py-2e/Images/64864d38550248d5bd9b82eeb6f0583b.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e97f2b3ae7473145254e23fde2ce1a7dc7623482 Binary files /dev/null and b/docs/nlp-py-2e/Images/64864d38550248d5bd9b82eeb6f0583b.jpg differ diff --git a/docs/nlp-py-2e/Images/66d94cb86ab90a95cfe745d9613c37b1.jpg b/docs/nlp-py-2e/Images/66d94cb86ab90a95cfe745d9613c37b1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..688b31cad556aaf0f06f32007ddcbe707528a59b Binary files /dev/null and b/docs/nlp-py-2e/Images/66d94cb86ab90a95cfe745d9613c37b1.jpg differ diff --git a/docs/nlp-py-2e/Images/67abca0731a79d664847dee1390f2e13.jpg b/docs/nlp-py-2e/Images/67abca0731a79d664847dee1390f2e13.jpg new file mode 100644 index 0000000000000000000000000000000000000000..94d60438fb89332fb82bc9017e1e7fb968de5906 Binary files /dev/null and b/docs/nlp-py-2e/Images/67abca0731a79d664847dee1390f2e13.jpg differ diff --git a/docs/nlp-py-2e/Images/6ac827d2d00b6ebf8bbc704f430af896.jpg b/docs/nlp-py-2e/Images/6ac827d2d00b6ebf8bbc704f430af896.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c9a369be517237d23d1eb51f33fac7a52d9bd1ce Binary files /dev/null and b/docs/nlp-py-2e/Images/6ac827d2d00b6ebf8bbc704f430af896.jpg differ diff --git a/docs/nlp-py-2e/Images/6ddd472200240ea6c0cab35349a8403e.jpg b/docs/nlp-py-2e/Images/6ddd472200240ea6c0cab35349a8403e.jpg new file mode 100644 index 0000000000000000000000000000000000000000..02448f3f9c4b9c82d64f2d6b04a2bb553d98c1b6 Binary files /dev/null and b/docs/nlp-py-2e/Images/6ddd472200240ea6c0cab35349a8403e.jpg differ diff --git a/docs/nlp-py-2e/Images/6efeadf518b11a6441906b93844c2b19.jpg b/docs/nlp-py-2e/Images/6efeadf518b11a6441906b93844c2b19.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c9a369be517237d23d1eb51f33fac7a52d9bd1ce Binary files /dev/null and b/docs/nlp-py-2e/Images/6efeadf518b11a6441906b93844c2b19.jpg differ diff --git a/docs/nlp-py-2e/Images/723ad3b660335fc3b79e7bd2c947b195.jpg b/docs/nlp-py-2e/Images/723ad3b660335fc3b79e7bd2c947b195.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ae840901dba5ad53b65b816ff7e35759bfad1466 Binary files /dev/null and b/docs/nlp-py-2e/Images/723ad3b660335fc3b79e7bd2c947b195.jpg differ diff --git a/docs/nlp-py-2e/Images/74248e04835acdba414fd407bb4f3241.jpg b/docs/nlp-py-2e/Images/74248e04835acdba414fd407bb4f3241.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b11a071c4b235bc54d3260630092621b62bf0af0 Binary files /dev/null and b/docs/nlp-py-2e/Images/74248e04835acdba414fd407bb4f3241.jpg differ diff --git a/docs/nlp-py-2e/Images/77460905bcad52d84e324fc4821ed903.jpg b/docs/nlp-py-2e/Images/77460905bcad52d84e324fc4821ed903.jpg new file mode 100644 index 0000000000000000000000000000000000000000..82ca075121646a0ebede2d2a529dbbf7ed85bb05 Binary files /dev/null and b/docs/nlp-py-2e/Images/77460905bcad52d84e324fc4821ed903.jpg differ diff --git a/docs/nlp-py-2e/Images/78213332718eae7fffd6314dae12484e.jpg b/docs/nlp-py-2e/Images/78213332718eae7fffd6314dae12484e.jpg new file mode 100644 index 0000000000000000000000000000000000000000..dbb05a4e581eb0e2b37267bcccc097108b781d10 Binary files /dev/null and b/docs/nlp-py-2e/Images/78213332718eae7fffd6314dae12484e.jpg differ diff --git a/docs/nlp-py-2e/Images/78bc6ca8410394dcf6910484bc97e2b6.jpg b/docs/nlp-py-2e/Images/78bc6ca8410394dcf6910484bc97e2b6.jpg new file mode 100644 index 0000000000000000000000000000000000000000..82ca075121646a0ebede2d2a529dbbf7ed85bb05 Binary files /dev/null and b/docs/nlp-py-2e/Images/78bc6ca8410394dcf6910484bc97e2b6.jpg differ diff --git a/docs/nlp-py-2e/Images/7a979f968bd33428b02cde62eaf2b615.jpg b/docs/nlp-py-2e/Images/7a979f968bd33428b02cde62eaf2b615.jpg new file mode 100644 index 0000000000000000000000000000000000000000..82ca075121646a0ebede2d2a529dbbf7ed85bb05 Binary files /dev/null and b/docs/nlp-py-2e/Images/7a979f968bd33428b02cde62eaf2b615.jpg differ diff --git a/docs/nlp-py-2e/Images/7aee076ae156921aba96ac5d4f9ed419.jpg b/docs/nlp-py-2e/Images/7aee076ae156921aba96ac5d4f9ed419.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c17ef03144bc407f7c53667bd51c74e92be5ebbf Binary files /dev/null and b/docs/nlp-py-2e/Images/7aee076ae156921aba96ac5d4f9ed419.jpg differ diff --git a/docs/nlp-py-2e/Images/7bbd845f6f0cf6246561d2859cbcecbf.jpg b/docs/nlp-py-2e/Images/7bbd845f6f0cf6246561d2859cbcecbf.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5250158b6414a4edb67149947126c7b641e1f2b9 Binary files /dev/null and b/docs/nlp-py-2e/Images/7bbd845f6f0cf6246561d2859cbcecbf.jpg differ diff --git a/docs/nlp-py-2e/Images/7c20d0adbadb35031a28bfcd6dff9900.jpg b/docs/nlp-py-2e/Images/7c20d0adbadb35031a28bfcd6dff9900.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ae840901dba5ad53b65b816ff7e35759bfad1466 Binary files /dev/null and b/docs/nlp-py-2e/Images/7c20d0adbadb35031a28bfcd6dff9900.jpg differ diff --git a/docs/nlp-py-2e/Images/7e6ea96aad77f3e523494b3972b5a989.jpg b/docs/nlp-py-2e/Images/7e6ea96aad77f3e523494b3972b5a989.jpg new file mode 100644 index 0000000000000000000000000000000000000000..82ca075121646a0ebede2d2a529dbbf7ed85bb05 Binary files /dev/null and b/docs/nlp-py-2e/Images/7e6ea96aad77f3e523494b3972b5a989.jpg differ diff --git a/docs/nlp-py-2e/Images/7f27bfe5324e4d9573ddd210531a8126.jpg b/docs/nlp-py-2e/Images/7f27bfe5324e4d9573ddd210531a8126.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cd6a3d09689789766413a99fd2a5ecc008660524 Binary files /dev/null and b/docs/nlp-py-2e/Images/7f27bfe5324e4d9573ddd210531a8126.jpg differ diff --git a/docs/nlp-py-2e/Images/7f97e7ac70a7c865fb1020795f6e7236.jpg b/docs/nlp-py-2e/Images/7f97e7ac70a7c865fb1020795f6e7236.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9f6c27ac4a6a518670d533227f05598a4e6f7dba Binary files /dev/null and b/docs/nlp-py-2e/Images/7f97e7ac70a7c865fb1020795f6e7236.jpg differ diff --git a/docs/nlp-py-2e/Images/81e6b07f4541d7d5a7900508d11172bd.jpg b/docs/nlp-py-2e/Images/81e6b07f4541d7d5a7900508d11172bd.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f15270f1a71fb5c5ad0c44d15394d4aa64af9c3a Binary files /dev/null and b/docs/nlp-py-2e/Images/81e6b07f4541d7d5a7900508d11172bd.jpg differ diff --git a/docs/nlp-py-2e/Images/82ddd91230422f3ba446f46ca73ff663.jpg b/docs/nlp-py-2e/Images/82ddd91230422f3ba446f46ca73ff663.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4d79f22b09dd06f82e14fb20bf0d5dc9a4749f19 Binary files /dev/null and b/docs/nlp-py-2e/Images/82ddd91230422f3ba446f46ca73ff663.jpg differ diff --git a/docs/nlp-py-2e/Images/84419adac7940aeb3026bc0a8b3e5fb4.jpg b/docs/nlp-py-2e/Images/84419adac7940aeb3026bc0a8b3e5fb4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..68eec32c51b3f7c5e17ebd4310709cf589881e91 Binary files /dev/null and b/docs/nlp-py-2e/Images/84419adac7940aeb3026bc0a8b3e5fb4.jpg differ diff --git a/docs/nlp-py-2e/Images/854532b0c5c8869f9012833955e75b20.jpg b/docs/nlp-py-2e/Images/854532b0c5c8869f9012833955e75b20.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c9a369be517237d23d1eb51f33fac7a52d9bd1ce Binary files /dev/null and b/docs/nlp-py-2e/Images/854532b0c5c8869f9012833955e75b20.jpg differ diff --git a/docs/nlp-py-2e/Images/86cecc4bd139ddaf1a5daf9991f39945.jpg b/docs/nlp-py-2e/Images/86cecc4bd139ddaf1a5daf9991f39945.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a0d52aee89759b758e4c70c444375710a4136f2a Binary files /dev/null and b/docs/nlp-py-2e/Images/86cecc4bd139ddaf1a5daf9991f39945.jpg differ diff --git a/docs/nlp-py-2e/Images/89747cee31bc672bfbbb6891a9099a25.jpg b/docs/nlp-py-2e/Images/89747cee31bc672bfbbb6891a9099a25.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e5a152eb4856e7f3c6c51d2da7a31bb677813c80 Binary files /dev/null and b/docs/nlp-py-2e/Images/89747cee31bc672bfbbb6891a9099a25.jpg differ diff --git a/docs/nlp-py-2e/Images/8b4bb6b0ec5bb337fdb00c31efcc1645.jpg b/docs/nlp-py-2e/Images/8b4bb6b0ec5bb337fdb00c31efcc1645.jpg new file mode 100644 index 0000000000000000000000000000000000000000..08f8f6a8025e6aca43d326c81e08153d8c3cb208 Binary files /dev/null and b/docs/nlp-py-2e/Images/8b4bb6b0ec5bb337fdb00c31efcc1645.jpg differ diff --git a/docs/nlp-py-2e/Images/8bfa073b3f3753285055c1e3ef689826.jpg b/docs/nlp-py-2e/Images/8bfa073b3f3753285055c1e3ef689826.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ac057833e3a1979ff82d4cf62b4ad41c2c453c7e Binary files /dev/null and b/docs/nlp-py-2e/Images/8bfa073b3f3753285055c1e3ef689826.jpg differ diff --git a/docs/nlp-py-2e/Images/8c5ec1a0132f7c85fd96eda9d9929d15.jpg b/docs/nlp-py-2e/Images/8c5ec1a0132f7c85fd96eda9d9929d15.jpg new file mode 100644 index 0000000000000000000000000000000000000000..df19f86e656940bc212866d93dde9330b0c8b3f4 Binary files /dev/null and b/docs/nlp-py-2e/Images/8c5ec1a0132f7c85fd96eda9d9929d15.jpg differ diff --git a/docs/nlp-py-2e/Images/8cb61a943f3d34f94596e77065410cd3.jpg b/docs/nlp-py-2e/Images/8cb61a943f3d34f94596e77065410cd3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c7fb3967314825938b18a7b91dece1802bf301e3 Binary files /dev/null and b/docs/nlp-py-2e/Images/8cb61a943f3d34f94596e77065410cd3.jpg differ diff --git a/docs/nlp-py-2e/Images/910abba27da60798441e98902cce64ca.jpg b/docs/nlp-py-2e/Images/910abba27da60798441e98902cce64ca.jpg new file mode 100644 index 0000000000000000000000000000000000000000..796a791b91569755a417da63157dc8a6e9fcc068 Binary files /dev/null and b/docs/nlp-py-2e/Images/910abba27da60798441e98902cce64ca.jpg differ diff --git a/docs/nlp-py-2e/Images/92cc2e7821d464cfbaaf651a360cd413.jpg b/docs/nlp-py-2e/Images/92cc2e7821d464cfbaaf651a360cd413.jpg new file mode 100644 index 0000000000000000000000000000000000000000..08f8f6a8025e6aca43d326c81e08153d8c3cb208 Binary files /dev/null and b/docs/nlp-py-2e/Images/92cc2e7821d464cfbaaf651a360cd413.jpg differ diff --git a/docs/nlp-py-2e/Images/934b688727805b37f2404f7497c52027.jpg b/docs/nlp-py-2e/Images/934b688727805b37f2404f7497c52027.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ae840901dba5ad53b65b816ff7e35759bfad1466 Binary files /dev/null and b/docs/nlp-py-2e/Images/934b688727805b37f2404f7497c52027.jpg differ diff --git a/docs/nlp-py-2e/Images/953f4a408c97594449de5ca84c294719.jpg b/docs/nlp-py-2e/Images/953f4a408c97594449de5ca84c294719.jpg new file mode 100644 index 0000000000000000000000000000000000000000..83800c90b5019d9808287434e2a434eda4bf8a02 Binary files /dev/null and b/docs/nlp-py-2e/Images/953f4a408c97594449de5ca84c294719.jpg differ diff --git a/docs/nlp-py-2e/Images/96fd8d34602a08c09a19f5b2c5c19380.jpg b/docs/nlp-py-2e/Images/96fd8d34602a08c09a19f5b2c5c19380.jpg new file mode 100644 index 0000000000000000000000000000000000000000..631db4804c09ec15b683d55de34795bb979364c6 Binary files /dev/null and b/docs/nlp-py-2e/Images/96fd8d34602a08c09a19f5b2c5c19380.jpg differ diff --git a/docs/nlp-py-2e/Images/99dbcae75bd96499dce3b3671032a106.jpg b/docs/nlp-py-2e/Images/99dbcae75bd96499dce3b3671032a106.jpg new file mode 100644 index 0000000000000000000000000000000000000000..216d33849bbc17b5899eabbc7c1ae0cd460217b4 Binary files /dev/null and b/docs/nlp-py-2e/Images/99dbcae75bd96499dce3b3671032a106.jpg differ diff --git a/docs/nlp-py-2e/Images/9ea1d0111a40bd865c651712f276bc31.jpg b/docs/nlp-py-2e/Images/9ea1d0111a40bd865c651712f276bc31.jpg new file mode 100644 index 0000000000000000000000000000000000000000..be80bdf419fd6b780c2184bab3355a9c54a017d5 Binary files /dev/null and b/docs/nlp-py-2e/Images/9ea1d0111a40bd865c651712f276bc31.jpg differ diff --git a/docs/nlp-py-2e/Images/a538227535079e1fa1e906af90af28eb.jpg b/docs/nlp-py-2e/Images/a538227535079e1fa1e906af90af28eb.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0f2f641a1e52dfa92fbcb1b6b2a7d2a3824d73ef Binary files /dev/null and b/docs/nlp-py-2e/Images/a538227535079e1fa1e906af90af28eb.jpg differ diff --git a/docs/nlp-py-2e/Images/aa68e0e8f4d58caa31e5542dabe4ddc2.jpg b/docs/nlp-py-2e/Images/aa68e0e8f4d58caa31e5542dabe4ddc2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c9a369be517237d23d1eb51f33fac7a52d9bd1ce Binary files /dev/null and b/docs/nlp-py-2e/Images/aa68e0e8f4d58caa31e5542dabe4ddc2.jpg differ diff --git a/docs/nlp-py-2e/Images/ab3d4c917ad3461f18759719a288afa5.jpg b/docs/nlp-py-2e/Images/ab3d4c917ad3461f18759719a288afa5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..82ca075121646a0ebede2d2a529dbbf7ed85bb05 Binary files /dev/null and b/docs/nlp-py-2e/Images/ab3d4c917ad3461f18759719a288afa5.jpg differ diff --git a/docs/nlp-py-2e/Images/b1aad2b60635723f14976fb5cb9ca372.jpg b/docs/nlp-py-2e/Images/b1aad2b60635723f14976fb5cb9ca372.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7fa4c4ac6feef7f0f5b22d6abf21a84a1bbc432b Binary files /dev/null and b/docs/nlp-py-2e/Images/b1aad2b60635723f14976fb5cb9ca372.jpg differ diff --git a/docs/nlp-py-2e/Images/b2af1426c6cd2403c8b938eb557a99d1.jpg b/docs/nlp-py-2e/Images/b2af1426c6cd2403c8b938eb557a99d1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f915e51e5da0f11bac5179d33797257e31dd9356 Binary files /dev/null and b/docs/nlp-py-2e/Images/b2af1426c6cd2403c8b938eb557a99d1.jpg differ diff --git a/docs/nlp-py-2e/Images/b502c97e1f935240559d38b397805b32.jpg b/docs/nlp-py-2e/Images/b502c97e1f935240559d38b397805b32.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0c4013aecb88607ed5c30a6530309f41c6eb8df4 Binary files /dev/null and b/docs/nlp-py-2e/Images/b502c97e1f935240559d38b397805b32.jpg differ diff --git a/docs/nlp-py-2e/Images/bc7e6aa002dd5ced295ba1de2ebbbf61.jpg b/docs/nlp-py-2e/Images/bc7e6aa002dd5ced295ba1de2ebbbf61.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6cbd1b203a86d4ef3b16a2c29306d746f2992397 Binary files /dev/null and b/docs/nlp-py-2e/Images/bc7e6aa002dd5ced295ba1de2ebbbf61.jpg differ diff --git a/docs/nlp-py-2e/Images/bcf758e8278f3295df58c6eace05152c.jpg b/docs/nlp-py-2e/Images/bcf758e8278f3295df58c6eace05152c.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9b4c94ec8d3077612a4726467edfb9062266972a Binary files /dev/null and b/docs/nlp-py-2e/Images/bcf758e8278f3295df58c6eace05152c.jpg differ diff --git a/docs/nlp-py-2e/Images/be33958d0b44c88caac0dcf4d4ec84c6.jpg b/docs/nlp-py-2e/Images/be33958d0b44c88caac0dcf4d4ec84c6.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c9a369be517237d23d1eb51f33fac7a52d9bd1ce Binary files /dev/null and b/docs/nlp-py-2e/Images/be33958d0b44c88caac0dcf4d4ec84c6.jpg differ diff --git a/docs/nlp-py-2e/Images/bf2a88150e1927f2e52d745ab6cae7ce.jpg b/docs/nlp-py-2e/Images/bf2a88150e1927f2e52d745ab6cae7ce.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b9fc4a2618a4586d017cf90bed99cb6e476a186e Binary files /dev/null and b/docs/nlp-py-2e/Images/bf2a88150e1927f2e52d745ab6cae7ce.jpg differ diff --git a/docs/nlp-py-2e/Images/bfd8f5fc51eb8848dfb986c7937ded23.jpg b/docs/nlp-py-2e/Images/bfd8f5fc51eb8848dfb986c7937ded23.jpg new file mode 100644 index 0000000000000000000000000000000000000000..94dfc9c5dfb9a33baefe911aa63016383ea0bb35 Binary files /dev/null and b/docs/nlp-py-2e/Images/bfd8f5fc51eb8848dfb986c7937ded23.jpg differ diff --git a/docs/nlp-py-2e/Images/c28c02e5c661caa578acaea0fa9e9dd2.jpg b/docs/nlp-py-2e/Images/c28c02e5c661caa578acaea0fa9e9dd2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b3d94755725f2536325f2b6a826cdc21cf967e43 Binary files /dev/null and b/docs/nlp-py-2e/Images/c28c02e5c661caa578acaea0fa9e9dd2.jpg differ diff --git a/docs/nlp-py-2e/Images/c370689374c63baf915c9e44c4b270d4.jpg b/docs/nlp-py-2e/Images/c370689374c63baf915c9e44c4b270d4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8ae4ceb258d4ecfa2e6ac345144ac42e47f52530 Binary files /dev/null and b/docs/nlp-py-2e/Images/c370689374c63baf915c9e44c4b270d4.jpg differ diff --git a/docs/nlp-py-2e/Images/ca21bcde8ab16a341929b7fb9ccb0a0e.jpg b/docs/nlp-py-2e/Images/ca21bcde8ab16a341929b7fb9ccb0a0e.jpg new file mode 100644 index 0000000000000000000000000000000000000000..08f8f6a8025e6aca43d326c81e08153d8c3cb208 Binary files /dev/null and b/docs/nlp-py-2e/Images/ca21bcde8ab16a341929b7fb9ccb0a0e.jpg differ diff --git a/docs/nlp-py-2e/Images/ced4e829d6a662a2be20187f9d7b71b5.jpg b/docs/nlp-py-2e/Images/ced4e829d6a662a2be20187f9d7b71b5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..79cb579508daae2e72a9808ae3c510b1a95d09f8 Binary files /dev/null and b/docs/nlp-py-2e/Images/ced4e829d6a662a2be20187f9d7b71b5.jpg differ diff --git a/docs/nlp-py-2e/Images/cf5ffc116dbddb4a34c65925b0d558cb.jpg b/docs/nlp-py-2e/Images/cf5ffc116dbddb4a34c65925b0d558cb.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0024342305469e0bc284cdee0252b6ce46e8a90d Binary files /dev/null and b/docs/nlp-py-2e/Images/cf5ffc116dbddb4a34c65925b0d558cb.jpg differ diff --git a/docs/nlp-py-2e/Images/d167c4075a237573a350e298a184d4fb.jpg b/docs/nlp-py-2e/Images/d167c4075a237573a350e298a184d4fb.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f5d31a0c9ed6298df94fe75fe7b0d361cbb8d466 Binary files /dev/null and b/docs/nlp-py-2e/Images/d167c4075a237573a350e298a184d4fb.jpg differ diff --git a/docs/nlp-py-2e/Images/d2c5cc51cd61a6915f24d0c52eaab9c5.jpg b/docs/nlp-py-2e/Images/d2c5cc51cd61a6915f24d0c52eaab9c5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b71871d17f30b3118ae99380794335735bfe2047 Binary files /dev/null and b/docs/nlp-py-2e/Images/d2c5cc51cd61a6915f24d0c52eaab9c5.jpg differ diff --git a/docs/nlp-py-2e/Images/d315c52900de61a078cd8391c1a1c604.jpg b/docs/nlp-py-2e/Images/d315c52900de61a078cd8391c1a1c604.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4a5bb8b8a42f3e12f90e416032e3a19e7adc0b32 Binary files /dev/null and b/docs/nlp-py-2e/Images/d315c52900de61a078cd8391c1a1c604.jpg differ diff --git a/docs/nlp-py-2e/Images/d3541d4caaf5c92d8ab496ccd7ee9a2d.jpg b/docs/nlp-py-2e/Images/d3541d4caaf5c92d8ab496ccd7ee9a2d.jpg new file mode 100644 index 0000000000000000000000000000000000000000..92151e7897783fe37f7730f4de003b7fb814c8af Binary files /dev/null and b/docs/nlp-py-2e/Images/d3541d4caaf5c92d8ab496ccd7ee9a2d.jpg differ diff --git a/docs/nlp-py-2e/Images/d41ca3c99b067df414f6ed0847a641ca.jpg b/docs/nlp-py-2e/Images/d41ca3c99b067df414f6ed0847a641ca.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8bd63e2cf61a3bf2df2b5afb310532d219a8157d Binary files /dev/null and b/docs/nlp-py-2e/Images/d41ca3c99b067df414f6ed0847a641ca.jpg differ diff --git a/docs/nlp-py-2e/Images/d46d7efa5d57b31c68c4ddddd464d6c0.jpg b/docs/nlp-py-2e/Images/d46d7efa5d57b31c68c4ddddd464d6c0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..11da8b6c5c3faad2932d8b1ad043e3cadd7e93bd Binary files /dev/null and b/docs/nlp-py-2e/Images/d46d7efa5d57b31c68c4ddddd464d6c0.jpg differ diff --git a/docs/nlp-py-2e/Images/d87676460c87d0516fb382b929c07302.jpg b/docs/nlp-py-2e/Images/d87676460c87d0516fb382b929c07302.jpg new file mode 100644 index 0000000000000000000000000000000000000000..baab72af3de0742f471f5a908e2259dec45da44b Binary files /dev/null and b/docs/nlp-py-2e/Images/d87676460c87d0516fb382b929c07302.jpg differ diff --git a/docs/nlp-py-2e/Images/da1752497a2a17be12b2acb282918a7a.jpg b/docs/nlp-py-2e/Images/da1752497a2a17be12b2acb282918a7a.jpg new file mode 100644 index 0000000000000000000000000000000000000000..228e2ceeea26cbc65ab525930985bd5a16c22d98 Binary files /dev/null and b/docs/nlp-py-2e/Images/da1752497a2a17be12b2acb282918a7a.jpg differ diff --git a/docs/nlp-py-2e/Images/da516572f97daebe1be746abd7bd2268.jpg b/docs/nlp-py-2e/Images/da516572f97daebe1be746abd7bd2268.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1533c0bf2d96a85969a3454cb2b4cbc7ad552341 Binary files /dev/null and b/docs/nlp-py-2e/Images/da516572f97daebe1be746abd7bd2268.jpg differ diff --git a/docs/nlp-py-2e/Images/daf7c1761ffc1edbb39e6ca11863854f.jpg b/docs/nlp-py-2e/Images/daf7c1761ffc1edbb39e6ca11863854f.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b242a70268174d09fe452e6b940b4f7e1cd193f5 Binary files /dev/null and b/docs/nlp-py-2e/Images/daf7c1761ffc1edbb39e6ca11863854f.jpg differ diff --git a/docs/nlp-py-2e/Images/de0715649664a49a5ab2e2b61ae2675a.jpg b/docs/nlp-py-2e/Images/de0715649664a49a5ab2e2b61ae2675a.jpg new file mode 100644 index 0000000000000000000000000000000000000000..31f7cc55f07f906c0a21dba92730cba8e2344e27 Binary files /dev/null and b/docs/nlp-py-2e/Images/de0715649664a49a5ab2e2b61ae2675a.jpg differ diff --git a/docs/nlp-py-2e/Images/df98e5a4903ccc7253cc7cc6ebda4fea.jpg b/docs/nlp-py-2e/Images/df98e5a4903ccc7253cc7cc6ebda4fea.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1a227e21553c32cce1de5bbbd9e80f4f223a384a Binary files /dev/null and b/docs/nlp-py-2e/Images/df98e5a4903ccc7253cc7cc6ebda4fea.jpg differ diff --git a/docs/nlp-py-2e/Images/e04c36ac970436161b45be660ea3a7d2.jpg b/docs/nlp-py-2e/Images/e04c36ac970436161b45be660ea3a7d2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..dc3dd9c80d110f36d924b118ee2f419dbd5e63f6 Binary files /dev/null and b/docs/nlp-py-2e/Images/e04c36ac970436161b45be660ea3a7d2.jpg differ diff --git a/docs/nlp-py-2e/Images/e104303155c3fbb0742ea67a0560ab7f.jpg b/docs/nlp-py-2e/Images/e104303155c3fbb0742ea67a0560ab7f.jpg new file mode 100644 index 0000000000000000000000000000000000000000..838f181740bf8d70a01b06de80ffa9745a1f77ea Binary files /dev/null and b/docs/nlp-py-2e/Images/e104303155c3fbb0742ea67a0560ab7f.jpg differ diff --git a/docs/nlp-py-2e/Images/e112e308d5c3454875146f40e4b48f3f.jpg b/docs/nlp-py-2e/Images/e112e308d5c3454875146f40e4b48f3f.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e17ae0496f79a074e8dddc4f2c3c8b56ead72455 Binary files /dev/null and b/docs/nlp-py-2e/Images/e112e308d5c3454875146f40e4b48f3f.jpg differ diff --git a/docs/nlp-py-2e/Images/e24d03f1dec3f8a75e8042579446a47e.jpg b/docs/nlp-py-2e/Images/e24d03f1dec3f8a75e8042579446a47e.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3e7aad47be36c23fcfc1b8a7deb5dcd4c69d8905 Binary files /dev/null and b/docs/nlp-py-2e/Images/e24d03f1dec3f8a75e8042579446a47e.jpg differ diff --git a/docs/nlp-py-2e/Images/e33fb540f11c5ea9a07441be8a407d43.jpg b/docs/nlp-py-2e/Images/e33fb540f11c5ea9a07441be8a407d43.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7096848042a35e77f055d13c4c677d44601afe94 Binary files /dev/null and b/docs/nlp-py-2e/Images/e33fb540f11c5ea9a07441be8a407d43.jpg differ diff --git a/docs/nlp-py-2e/Images/e372da3dc801bc1211a47d2e82840b64.jpg b/docs/nlp-py-2e/Images/e372da3dc801bc1211a47d2e82840b64.jpg new file mode 100644 index 0000000000000000000000000000000000000000..84738439acbca9e2fe7d3a079cad78722a690d7d Binary files /dev/null and b/docs/nlp-py-2e/Images/e372da3dc801bc1211a47d2e82840b64.jpg differ diff --git a/docs/nlp-py-2e/Images/e591e60c490795add5183c998132ebc0.jpg b/docs/nlp-py-2e/Images/e591e60c490795add5183c998132ebc0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3db5c9a525e9b6521b8e2339dfd3a98ea4e2114e Binary files /dev/null and b/docs/nlp-py-2e/Images/e591e60c490795add5183c998132ebc0.jpg differ diff --git a/docs/nlp-py-2e/Images/e5fb07e997b9718f18dbf677e3d6634d.jpg b/docs/nlp-py-2e/Images/e5fb07e997b9718f18dbf677e3d6634d.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c9a369be517237d23d1eb51f33fac7a52d9bd1ce Binary files /dev/null and b/docs/nlp-py-2e/Images/e5fb07e997b9718f18dbf677e3d6634d.jpg differ diff --git a/docs/nlp-py-2e/Images/e685801a8cec4515b47e1bda95deb59d.jpg b/docs/nlp-py-2e/Images/e685801a8cec4515b47e1bda95deb59d.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0102b219a2754f8f01e596ecac63bbde6ba755b6 Binary files /dev/null and b/docs/nlp-py-2e/Images/e685801a8cec4515b47e1bda95deb59d.jpg differ diff --git a/docs/nlp-py-2e/Images/e941b64ed778967dd0170d25492e42df.jpg b/docs/nlp-py-2e/Images/e941b64ed778967dd0170d25492e42df.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ae840901dba5ad53b65b816ff7e35759bfad1466 Binary files /dev/null and b/docs/nlp-py-2e/Images/e941b64ed778967dd0170d25492e42df.jpg differ diff --git a/docs/nlp-py-2e/Images/e9d9a0887996a6bac6c52bb0bfaf9fdf.jpg b/docs/nlp-py-2e/Images/e9d9a0887996a6bac6c52bb0bfaf9fdf.jpg new file mode 100644 index 0000000000000000000000000000000000000000..dc5ea15e2805951cf836910d1a398112339d9cb1 Binary files /dev/null and b/docs/nlp-py-2e/Images/e9d9a0887996a6bac6c52bb0bfaf9fdf.jpg differ diff --git a/docs/nlp-py-2e/Images/ea84debad296c6385399fb2252fc93f1.jpg b/docs/nlp-py-2e/Images/ea84debad296c6385399fb2252fc93f1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5e741069438fd2d7228af7be22b88e2dfb141565 Binary files /dev/null and b/docs/nlp-py-2e/Images/ea84debad296c6385399fb2252fc93f1.jpg differ diff --git a/docs/nlp-py-2e/Images/eb630c6034e9ed7274ef2e04b9694347.jpg b/docs/nlp-py-2e/Images/eb630c6034e9ed7274ef2e04b9694347.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5254f509ac2cf011bc77429752ccffcf6aedd14f Binary files /dev/null and b/docs/nlp-py-2e/Images/eb630c6034e9ed7274ef2e04b9694347.jpg differ diff --git a/docs/nlp-py-2e/Images/edd1b5af9dcd26541183ce4d6a634e54.jpg b/docs/nlp-py-2e/Images/edd1b5af9dcd26541183ce4d6a634e54.jpg new file mode 100644 index 0000000000000000000000000000000000000000..08571186058f988796a85c727bcd2c5225b04d71 Binary files /dev/null and b/docs/nlp-py-2e/Images/edd1b5af9dcd26541183ce4d6a634e54.jpg differ diff --git a/docs/nlp-py-2e/Images/eeff7ed83be48bf40aeeb3bf9db5550e.jpg b/docs/nlp-py-2e/Images/eeff7ed83be48bf40aeeb3bf9db5550e.jpg new file mode 100644 index 0000000000000000000000000000000000000000..82ca075121646a0ebede2d2a529dbbf7ed85bb05 Binary files /dev/null and b/docs/nlp-py-2e/Images/eeff7ed83be48bf40aeeb3bf9db5550e.jpg differ diff --git a/docs/nlp-py-2e/Images/ef661b5a01845fe5440027afca461925.jpg b/docs/nlp-py-2e/Images/ef661b5a01845fe5440027afca461925.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9cefd6ee20f110580b201f7e22ca51ed849b10ca Binary files /dev/null and b/docs/nlp-py-2e/Images/ef661b5a01845fe5440027afca461925.jpg differ diff --git a/docs/nlp-py-2e/Images/f093aaace735b4961dbf9fa7d5c8ca37.jpg b/docs/nlp-py-2e/Images/f093aaace735b4961dbf9fa7d5c8ca37.jpg new file mode 100644 index 0000000000000000000000000000000000000000..307a06a220ba8045bb4e87eae2c709f7b456fcad Binary files /dev/null and b/docs/nlp-py-2e/Images/f093aaace735b4961dbf9fa7d5c8ca37.jpg differ diff --git a/docs/nlp-py-2e/Images/f202bb6a4c773430e3d1340de573d0e5.jpg b/docs/nlp-py-2e/Images/f202bb6a4c773430e3d1340de573d0e5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ae840901dba5ad53b65b816ff7e35759bfad1466 Binary files /dev/null and b/docs/nlp-py-2e/Images/f202bb6a4c773430e3d1340de573d0e5.jpg differ diff --git a/docs/nlp-py-2e/Images/f3ad266a67457b4615141d6ba83e724e.jpg b/docs/nlp-py-2e/Images/f3ad266a67457b4615141d6ba83e724e.jpg new file mode 100644 index 0000000000000000000000000000000000000000..08f8f6a8025e6aca43d326c81e08153d8c3cb208 Binary files /dev/null and b/docs/nlp-py-2e/Images/f3ad266a67457b4615141d6ba83e724e.jpg differ diff --git a/docs/nlp-py-2e/Images/f4891d12ae20c39b685951ad3cddf1aa.jpg b/docs/nlp-py-2e/Images/f4891d12ae20c39b685951ad3cddf1aa.jpg new file mode 100644 index 0000000000000000000000000000000000000000..82ca075121646a0ebede2d2a529dbbf7ed85bb05 Binary files /dev/null and b/docs/nlp-py-2e/Images/f4891d12ae20c39b685951ad3cddf1aa.jpg differ diff --git a/docs/nlp-py-2e/Images/f9e1ba3246770e3ecb24f813f33f2075.jpg b/docs/nlp-py-2e/Images/f9e1ba3246770e3ecb24f813f33f2075.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c9a369be517237d23d1eb51f33fac7a52d9bd1ce Binary files /dev/null and b/docs/nlp-py-2e/Images/f9e1ba3246770e3ecb24f813f33f2075.jpg differ diff --git a/docs/nlp-py-2e/Images/fb1a02fe3607a0deb452086296fd6f69.jpg b/docs/nlp-py-2e/Images/fb1a02fe3607a0deb452086296fd6f69.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7ed3e176dfc08c8cb13c7800bf0288d55e390eef Binary files /dev/null and b/docs/nlp-py-2e/Images/fb1a02fe3607a0deb452086296fd6f69.jpg differ diff --git a/docs/nlp-py-2e/Images/ff868af58b8c1843c38287717b137f7c.jpg b/docs/nlp-py-2e/Images/ff868af58b8c1843c38287717b137f7c.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e78edb2af3f1f1656515b691db7a59dba7d8c7b2 Binary files /dev/null and b/docs/nlp-py-2e/Images/ff868af58b8c1843c38287717b137f7c.jpg differ diff --git a/docs/nlp-py-2e/Images/ffa808c97c7034af1bc2806ed7224203.jpg b/docs/nlp-py-2e/Images/ffa808c97c7034af1bc2806ed7224203.jpg new file mode 100644 index 0000000000000000000000000000000000000000..82ca075121646a0ebede2d2a529dbbf7ed85bb05 Binary files /dev/null and b/docs/nlp-py-2e/Images/ffa808c97c7034af1bc2806ed7224203.jpg differ diff --git a/docs/nlp-py-2e/Images/fff90c564d2625f739b442b23301906e.jpg b/docs/nlp-py-2e/Images/fff90c564d2625f739b442b23301906e.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1a64c58a35b89631ad04c7a534ff47c310c9aefa Binary files /dev/null and b/docs/nlp-py-2e/Images/fff90c564d2625f739b442b23301906e.jpg differ diff --git a/docs/nlp-py-2e/README.md b/docs/nlp-py-2e/README.md new file mode 100644 index 0000000000000000000000000000000000000000..d329b5a9d2df6d273e81faa58374f7c627daa9ca --- /dev/null +++ b/docs/nlp-py-2e/README.md @@ -0,0 +1,48 @@ +# Python 自然语言处理 第二版 + +## – 使用自然语言工具包分析文本 + +> 作者:**Steven Bird, Ewan Klein 和 Edward Loper** +> +> 译者:[Python 文档协作翻译小组](http://usyiyi.cn/translate/nltk_python/index.html) +> +> 最后更新:2017.3.4 +> +> [Django 文档协作翻译小组](http://python.usyiyi.cn/django/index.html)人手紧缺,有兴趣的朋友可以加入我们,完全公益性质。交流群:467338606。 + ++ [在线阅读](https://nltk.apachecn.org) ++ [在线阅读(Gitee)](https://apachecn.gitee.io/nlp-py-2e-zh/) + +本版本的 NLTK 已经针对 Python 3 和 NLTK 3 更新。本书的第一版由 O'Reilly 出版,可以在 [http://nltk.org/book_1ed/](http://nltk.org/book_1ed/) 访问到。(本书目前没有计划出第二版) + +本版本的初译基于[原书第一版的翻译](http://www.52nlp.cn/resources),参见第一版[译者的话](./15.html)。 + +本书的发行遵守 [Creative Commons Attribution Noncommercial No-Derivative-Works 3.0 US License](http://creativecommons.org/licenses/by-nc-nd/3.0/us/) 条款。 + +如果对材料有任何疑问请提交至 [nltk-users](http://groups.google.com/group/nltk-users) 邮件列表。请在 [issue tracker](https://github.com/nltk/nltk_book/issues) 上报告错误。 + +## 下载 + +### Docker + +``` +docker pull apachecn0/nlp-py-2e-zh +docker run -tid -p :80 apachecn0/nlp-py-2e-zh +# 访问 http://localhost:{port} 查看文档 +``` + +### PYPI + +``` +pip install nlp-py-2e-zh +nlp-py-2e-zh +# 访问 http://localhost:{port} 查看文档 +``` + +### NPM + +``` +npm install -g nlp-py-2e-zh +nlp-py-2e-zh +# 访问 http://localhost:{port} 查看文档 +``` diff --git a/docs/nlp-py-2e/SUMMARY.md b/docs/nlp-py-2e/SUMMARY.md new file mode 100644 index 0000000000000000000000000000000000000000..01b63cdd3515a6e28d1cbc72ea3884392055a8e1 --- /dev/null +++ b/docs/nlp-py-2e/SUMMARY.md @@ -0,0 +1,15 @@ ++ [Python 自然语言处理 第二版](README.md) ++ [前言](0.md) ++ [1 语言处理与 Python](1.md) ++ [2 获得文本语料和词汇资源](2.md) ++ [3 处理原始文本](3.md) ++ [4 编写结构化程序](4.md) ++ [5 分类和标注词汇](5.md) ++ [6 学习分类文本](6.md) ++ [7 从文本提取信息](7.md) ++ [8 分析句子结构](8.md) ++ [9 构建基于特征的语法](9.md) ++ [10 分析句子的意思](10.md) ++ [11 语言学数据管理](11.md) ++ [后记:语言的挑战](12.md) ++ [索引](14.md)