diff --git a/SUMMARY.md b/SUMMARY.md index 8a7d01457d8606f36a1cfe84adb5ab34b536005f..202b66fb058170de63df53061342e8e39ac62b0e 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -205,14 +205,21 @@ + [CNN原理](docs/dl/CNN原理.md) + [RNN原理](docs/dl/RNN原理.md) + [LSTM原理](docs/dl/LSTM原理.md) -+ 自然语言处理 - + [第1章_入门介绍](docs/nlp/1.入门介绍.md) - + [第2章_分词](docs/nlp/2.分词.md) - + [第3章_命名实体识别](docs/nlp/3.命名实体识别.md) - + [第10章_篇章分析-内容概述](docs/nlp/3.1.篇章分析-内容概述.md) - + [第10章_篇章分析-内容标签](docs/nlp/3.2.篇章分析-内容标签.md) - + [第10章_篇章分析-情感分析](docs/nlp/3.3.篇章分析-情感分析.md) - + [第10章_篇章分析-自动摘要](docs/nlp/3.4.篇章分析-自动摘要.md) ++ [自然语言处理](docs/nlp/README.md) + + [前言](docs/nlp/0.md) + + [1 语言处理与 Python](docs/nlp/1.md) + + [2 获得文本语料和词汇资源](docs/nlp/2.md) + + [3 处理原始文本](docs/nlp/3.md) + + [4 编写结构化程序](docs/nlp/4.md) + + [5 分类和标注词汇](docs/nlp/5.md) + + [6 学习分类文本](docs/nlp/6.md) + + [7 从文本提取信息](docs/nlp/7.md) + + [8 分析句子结构](docs/nlp/8.md) + + [9 构建基于特征的语法](docs/nlp/9.md) + + [10 分析句子的意思](docs/nlp/10.md) + + [11 语言学数据管理](docs/nlp/11.md) + + [后记:语言的挑战](docs/nlp/12.md) + + [索引](docs/nlp/14.md) + TensorFlow 2.0 - 教程 + [安装指南](docs/TensorFlow2.x/安装指南.md) + [Kears 快速入门](docs/TensorFlow2.x/Keras快速入门.md) diff --git a/docs/nlp/0.md b/docs/nlp/0.md new file mode 100644 index 0000000000000000000000000000000000000000..20f7697bfb03532a6adf4ad9597a1514f3941ddc --- /dev/null +++ b/docs/nlp/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/1.md b/docs/nlp/1.md new file mode 100644 index 0000000000000000000000000000000000000000..1e816109e1e338b67316a8445f820a629ce7a071 --- /dev/null +++ b/docs/nlp/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/1.\345\205\245\351\227\250\344\273\213\347\273\215.md" "b/docs/nlp/1.\345\205\245\351\227\250\344\273\213\347\273\215.md" deleted file mode 100644 index 4468f5e623e2cdd1f8d7908187fa5d78fad82bde..0000000000000000000000000000000000000000 --- "a/docs/nlp/1.\345\205\245\351\227\250\344\273\213\347\273\215.md" +++ /dev/null @@ -1,102 +0,0 @@ -# 自然语言处理 - 1.入门介绍 - -* 语言是知识和思维的载体 -* 自然语言处理 (Natural Language Processing, NLP) 是计算机科学,人工智能,语言学关注计算机和人类(自然)语言之间的相互作用的领域。 - -## NLP相关的技术 - -| 中文 | 英文 | 描述 | -| --- | --- | --- | -| 分词 | Word Segmentation | 将连续的自然语言文本,切分成具有语义合理性和完整性的词汇序列 | -| 命名实体识别 | Named Entity Recognition | 识别自然语言文本中具有特定意义的实体(人、地、机构、时间、作品等) | -| 词性标注 | Part-Speech Tagging | 为自然语言文本中的每个词汇赋予一个词性(名词、动词、形容词等) | -| 依存句法分析 | Dependency Parsing | 自动分析句子中的句法成分(主语、谓语、宾语、定语、状语和补语等成分) | -| 词向量与语义相似度 | Word Embedding & Semantic Similarity | 依托全网海量数据和深度神经网络技术,实现了对词汇的向量化表示,并据此实现了词汇的语义相似度计算 | -| 文本语义相似度 | Text Semantic Similarity | 依托全网海量数据和深度神经网络技术,实现文本间的语义相似度计算的能力 | -| 篇章分析 | Document Analysis | 分析篇章级文本的内在结构,进而分析文本情感倾向,提取评论性观点,并生成反映文本关键信息的标签与摘要 | -| 机器翻译技术 | Machine Translating | 基于互联网大数据,融合深度神经网络、统计、规则多种翻译方法,帮助用户跨越语言鸿沟,与世界自由沟通 | - -## 场景案例 - -### 案例1(解决交叉歧义) - -**分词(Word Segmentation)** : 将连续的自然语言文本,切分成具有语义合理性和完整性的词汇序列 - -例句: 致毕业和尚未毕业的同学。 - -1. `致` `毕业` `和` `尚未` `毕业` `的` `同学` -2. `致` `毕业` `和尚` `未` `毕业` `的` `同学` - -其他案例: - -1. 校友 和 老师 给 尚未 毕业 同学 的 一 封 信 -2. 本科 未 毕业 可以 当 和尚 吗 - -### 案例2(从粒度整合未登录体词) - -**命名实体识别(Named Entity Recognition)**: 识别自然语言文本中具有特定意义的实体(人、地、机构、时间、作品等) - -例句: 天使爱美丽在线观看 - -* 分词: `天使` `爱` `美丽` `在线` `观看` -* 实体: 天使爱美丽 -> 电影 - -其他案例: - -1. 网页: 天使爱美丽 土豆 高清视频 -2. 网页: 在线直播 爱 美丽 的 天使 - -### 案例3(结构歧义问题) - -* **词性标注(Part-Speech Tagging)**: 为自然语言文本中的每个词汇赋予一个词性(名词、动词、形容词等) -* **依存句法分析(Dependency Parsing)**: 自动分析句子中的句法成分(主语、谓语、宾语、定语、状语和补语等成分) - -评论: 房间里还可以欣赏日出 - -* 房间里: 主语 -* 还可以: 情态动词 -* 欣赏: 动词 -* 日出: 宾语 - -歧义: - -1. 房间还可以 -2. 可以欣赏日出 - -### 案例4(词汇语言相似度) - -**词向量与语义相似度(Word Embedding & Semantic Similarity)**: 对词汇进行向量化表示,并据此实现词汇的语义相似度计算。 - -例如: 西瓜 与 (呆瓜/草莓),哪个更接近? - -* 向量化表示: 西瓜(0.1222, 0.22333, .. ) -* 相似度计算: 呆瓜(0.115) 草莓(0.325) -* 向量化表示: (-0.333, 0.1223 .. ) (0.333, 0.3333, .. ) - -### 案例5(文本语义相似度) - -**文本语义相似度(Text Semantic Similarity)**: 依托全网海量数据和深度神经网络技术,实现文本间的语义相似度计算的能力 - -例如: 车头如何防止车牌 与 (前牌照怎么装/如何办理北京牌照),哪个更接近? - -* 向量化表示: 车头如何防止车牌(0.1222, 0.22333, .. ) -* 相似度计算: 前牌照怎么装(0.762) 如何办理北京牌照(0.486) -* 向量化表示: (-0.333, 0.1223 .. ) (0.333, 0.3333, .. ) - -### 案例6(篇章分析) - -**篇章分析(Document Analysis)**: 分析篇章级文本的内在结构,进而分析文本情感倾向,提取评论性观点,并生成反映文本关键信息的标签与摘要 - -例如: - -![](img/1.自然语言处理入门介绍/篇章分析.jpg) - -### 案例7(机器翻译) - -**机器翻译技术(Machine Translating)**: 基于互联网大数据,融合深度神经网络、统计、规则多种翻译方法,帮助用户跨越语言鸿沟,与世界自由沟通 - -![](img/1.自然语言处理入门介绍/机器翻译.png) - ---- - -* 参考百度科普课程: diff --git a/docs/nlp/10.md b/docs/nlp/10.md new file mode 100644 index 0000000000000000000000000000000000000000..273cb83417f4ca3b02084bfc5cdc2063f6214eeb --- /dev/null +++ b/docs/nlp/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/11.md b/docs/nlp/11.md new file mode 100644 index 0000000000000000000000000000000000000000..99486b507932622d86858f68cda838f8c33a2a9f --- /dev/null +++ b/docs/nlp/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/12.md b/docs/nlp/12.md new file mode 100644 index 0000000000000000000000000000000000000000..7e1ee224bbf5f3b6fb80ea7cce364b759dec60e2 --- /dev/null +++ b/docs/nlp/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/14.md b/docs/nlp/14.md new file mode 100644 index 0000000000000000000000000000000000000000..89cee9905659f65db54cf6fd497c9a09b7d5ceca --- /dev/null +++ b/docs/nlp/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/2.md b/docs/nlp/2.md new file mode 100644 index 0000000000000000000000000000000000000000..75c198c1bda36fd95c31a78eb1930f3aedb920f3 --- /dev/null +++ b/docs/nlp/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/2.\345\210\206\350\257\215.md" "b/docs/nlp/2.\345\210\206\350\257\215.md" deleted file mode 100644 index 44420de3571be10703328ff2c8b3e609bfceebcd..0000000000000000000000000000000000000000 --- "a/docs/nlp/2.\345\210\206\350\257\215.md" +++ /dev/null @@ -1,694 +0,0 @@ -# 自然语言处理 - 2.分词 - -**分词(Word Segmentation)**: 将连续的自然语言文本,切分成具有语义合理性和完整性的词汇序列 - -例句: 致毕业和尚未毕业的同学。 - -1. `致` `毕业` `和` `尚未` `毕业` `的` `同学` -2. `致` `毕业` `和尚` `未` `毕业` `的` `同学` - -> 今天我们聊聊 jieba 结巴分词器(牛逼) - -* 第一个不靠吹嘘学校or公司,完全靠实力开源的一个项目 -* 知乎上网友评论是腾讯,而最近GitHub上看到的是百度邮箱 -* “结巴”中文分词: 做最好的 Python 中文分词组件(这个的确没吹牛!) -* GitHub上面 README.md 为中文版本(别看这个小事,很多中国公司开源项目都是英文) - -## 部署使用 - -> 安装 - -`pip install jieba` - -> 引用 - -`import jieba` - -> 特点 - -* 四种分词模式 - * 精确模式: 试图将句子最精确地切开,适合文本分析; - * 全模式: 把句子中所有的可以成词的词语都扫描出来, 速度非常快,但是不能解决歧义 - * 搜索引擎模式: 在精确模式的基础上,对长词再次切分,提高召回率,适合用于搜索引擎分词 - * paddle模式: 利用PaddlePaddle深度学习框架,训练序列标注(双向GRU)网络模型实现分词,同时支持词性标注 - * paddle模式使用需安装paddlepaddle-tiny: `pip install paddlepaddle-tiny==1.6.1` - * 目前paddle模式支持jieba v0.40及以上版本。 - * jieba v0.40以下版本,请升级 jieba: `pip install jieba --upgrade` -* 支持繁体分词 -* 支持自定义词典 -* MIT 授权协议 - -## 四种分词模式 - -> 1.精确模式 - -精确模式: 试图将句子最精确地切开,适合文本分析 - -```python -# encoding=utf-8 -import jieba - -seg_list = jieba.cut("他来到了网易杭研大厦") # 默认是精确模式 -print(", ".join(seg_list)) - -seg_list = jieba.cut("我来到北京清华大学", cut_all=False) -print("Default Mode: " + "/ ".join(seg_list)) # 精确模式 - -# 输出结果 -#【精确模式】: 我/ 来到/ 北京/ 清华大学 -``` - -> 2.全模式 - -全模式: 把句子中所有的可以成词的词语都扫描出来, 速度非常快,但是不能解决歧义 - -```python -# encoding=utf-8 -import jieba - -seg_list = jieba.cut("我来到北京清华大学", cut_all=True) -print("Full Mode: " + "/ ".join(seg_list)) # 全模式 - - -# 输出结果 -#【全模式】: 我/ 来到/ 北京/ 清华/ 清华大学/ 华大/ 大学 -``` - -> 3.搜索引擎模式 - -搜索引擎模式: 在精确模式的基础上,对长词再次切分,提高召回率,适合用于搜索引擎分词 - -```python -# encoding=utf-8 -import jieba - -seg_list = jieba.cut_for_search("小明硕士毕业于中国科学院计算所,后在日本京都大学深造") # 搜索引擎模式 -print(", ".join(seg_list)) - -# 输出结果 -#【搜索引擎模式】: 小明, 硕士, 毕业, 于, 中国, 科学, 学院, 科学院, 中国科学院, 计算, 计算所, 后, 在, 日本, 京都, 大学, 日本京都大学, 深造 -``` - -> 4.paddle 模式 - -paddle 模式: 利用PaddlePaddle深度学习框架,训练序列标注(双向GRU)网络模型实现分词,同时支持词性标注 - -* paddle模式使用需安装paddlepaddle-tiny: `pip install paddlepaddle-tiny==1.6.1` -* 目前paddle模式支持jieba v0.40及以上版本。 -* jieba v0.40以下版本,请升级 jieba: `pip install jieba --upgrade` - -```python -# encoding=utf-8 -import jieba - -jieba.enable_paddle() # 启动paddle模式。 0.40版之后开始支持,早期版本不支持 -strs=["我来到北京清华大学", "乒乓球拍卖完了", "中国科学技术大学"] -for str in strs: - seg_list = jieba.cut(str,use_paddle=True) # 使用paddle模式 - print("Paddle Mode: " + '/'.join(list(seg_list))) - -# 输出结果 -#【Paddle 模式】 我/来到/北京/清华大学 -#【Paddle 模式】 乒乓球/拍卖/完/了 -#【Paddle 模式】 中国/科学技术/大学 -``` - -## 添加自定义词典 - -> 延迟加载机制 - -* jieba 采用延迟加载,`import jieba` 和 `jieba.Tokenizer()` 不会立即触发词典的加载,一旦有必要才开始加载词典构建前缀字典。 -* 如果你想手工初始 jieba,也可以手动初始化。 - -```python -import jieba -jieba.initialize() # 手动初始化(可选) -``` - -在 0.28 之前的版本是不能指定主词典的路径的,有了延迟加载机制后,你可以改变主词典的路径: - -```python -jieba.set_dictionary('data/dict.txt.big') -``` - -案例 - -```python -# encoding=utf-8 -import sys -sys.path.append("../") -import jieba - -def cuttest(test_sent): - result = jieba.cut(test_sent) - print(" ".join(result)) - -def testcase(): - cuttest("这是一个伸手不见五指的黑夜。我叫孙悟空,我爱北京,我爱Python和C++。") - cuttest("我不喜欢日本和服。") - cuttest("雷猴回归人间。") - cuttest("工信处女干事每月经过下属科室都要亲口交代24口交换机等技术性器件的安装工作") - cuttest("我需要廉租房") - cuttest("永和服装饰品有限公司") - cuttest("我爱北京天安门") - cuttest("abc") - cuttest("隐马尔可夫") - cuttest("雷猴是个好网站") - -if __name__ == "__main__": - testcase() - """foobar.txt 格式 - 的 3188252 uj - 了 883634 ul - 是 796991 v - """ - jieba.set_dictionary("foobar.txt") - print("================================") - testcase() -``` - -> 切换其他词典 - -* 占用内存较小的词典文件: -* 支持繁体分词更好的词典文件: -* 下载你所需要的词典,然后覆盖 `jieba/dict.txt` 即可;或者用 `jieba.set_dictionary('data/dict.txt.big')` - -> 载入自定义词典 - -* 开发者可以指定自己自定义的词典,以便包含 jieba 词库里没有的词 - * 虽然 jieba 有新词识别能力,但是自行添加新词可以保证更高的正确率 -* 用法: `jieba.load_userdict(file_name)` # file_name 为文件类对象或自定义词典的路径 - * 词典格式和 dict.txt 一样,一个词占一行 - * 每一行分三部分: 词语、词频(可省略)、词性(可省略),`用空格隔开`,`顺序不可颠倒` - * file_name 若为路径或二进制方式打开的文件,则文件必须为 UTF-8 编码 - * 词频省略时使用自动计算的能保证分出该词的词频 - -```python -""" filename 内容为: -创新办 3 i -云计算 5 -凱特琳 nz -台中 -""" -# encoding=utf-8 -import jieba - -seg_list = jieba.cut("我来到北京清华大学", cut_all=False) -print("Default Mode: " + "/ ".join(seg_list)) # 精确模式 - -jieba.load_userdict(file_name) - -seg_list = jieba.cut("我来到北京清华大学", cut_all=False) -print("Default Mode: " + "/ ".join(seg_list)) # 精确模式 - -# 加载自定义词库前: 李小福 / 是 / 创新 / 办 / 主任 / 也 / 是 / 云 / 计算 / 方面 / 的 / 专家 / -# 加载自定义词库后: 李小福 / 是 / 创新办 / 主任 / 也 / 是 / 云计算 / 方面 / 的 / 专家 / -``` - -## 调整词典的词 - -* 使用 `add_word(word, freq=None, tag=None)` 和 `del_word(word)` 可在程序中动态修改词典。 -* 使用 `suggest_freq(segment, tune=True)` 可调节单个词语的词频,使其能(或不能)被分出来。 - -**注意: 自动计算的词频在使用 HMM 新词发现功能时可能无效** - -代码示例: - -```python -print('/'.join(jieba.cut('如果放到post中将出错。', HMM=False))) -# 如果/放到/post/中将/出错/。 - -jieba.suggest_freq(('中', '将'), True) -# 494 -print('/'.join(jieba.cut('如果放到post中将出错。', HMM=False))) -# 如果/放到/post/中/将/出错/。 - - -print('/'.join(jieba.cut('「台中」正确应该不会被切开', HMM=False))) -#「/台/中/」/正确/应该/不会/被/切开 - -jieba.suggest_freq('台中', True) -# 69 -print('/'.join(jieba.cut('「台中」正确应该不会被切开', HMM=False))) -#「/台中/」/正确/应该/不会/被/切开 -``` - -## 关键词提取 - -### 基于 TF-IDF 算法的关键词抽取 - -* 基于该框架的 TF-IDF 效果一般 - * 1.它使用的是他默认的 IDF 值的文件【不是针对我们的项目】 - * 2.我们先得有词,你才能计算 IDF的值。而框架是要先提供IDF值才能计算最终的 TF-IDF 值。 -* 如果你有计算IDF的值存成文件,再加载进来,计算TF-IDF值,才能得到适合这个类型数据的值! - -> TF `优化点: 分子/分母 都加1` - -TF: term frequency 短期频率, 用于衡量一个词在一个文件中的出现频率。因为每个文档的长度的差别可以很大,因而一个词在某个文档中出现的次数可能远远大于另一个文档,所以词频通常就是一个词出现的次数除以文档的总长度,相当于是做了一次归一化。 - -公式: `TF(t) = (词t在某个文档中出现的总次数) / (某个文档的词总数)` - -> IDF `优化点: 分子/分母 都加1` - -IDF: inverse document frequency 逆向文件频率,用于衡量一个词的重要性/区分度。计算词频TF的时候,所有的词语都被当做一样重要的,但是某些词,比如”is”, “of”, “that”很可能出现很多很多次,但是可能根本并不重要,因此我们需要减轻在多个文档中都频繁出现的词的权重。 - -公式: `IDF = log_e(总文档数/词t出现的文档数)` - -> TF-IDF(term frequency–inverse document frequency) - -公式: `TF-IDF = TF*IDF` - -> 注意 - -`非常短的文本很可能影响 tf-idf 值` - -> 行业用途 - -``` -TF-IDF(term frequency–inverse document frequency)是一种用于资讯检索与文本挖掘的常用加权技术。 -TF-IDF是一种统计方法,用以评估一字词对于一个文件集或一个语料库中的其中一份文件的重要程度。字词的重要性随着它在文件中出现的次数成正比增加,但同时会随着它在语料库中出现的频率成反比下降。 -TF-IDF加权的各种形式常被搜索引擎应用,作为文件与用户查询之间相关程度的度量或评级。 -除了TF-IDF以外,互联网上的搜索引擎还会使用基于连结分析的评级方法,以确定文件在搜寻结果中出现的顺序。 -``` - - -```python -import jieba.analyse - -# allowPOS('ns', 'n', 'vn', 'v') 地名、名词、动名词、动词 -tags = jieba.analyse.extract_tags(sentence, topK=20, withWeight=False, allowPOS=()) -""" -* sentence 为待提取的文本 -* topK 为返回几个 TF/IDF 权重最大的关键词,默认值为 20 -* withWeight 为是否一并返回关键词权重值,默认值为 False -* allowPOS 仅包括指定词性的词,默认值为空,即不筛选 -* jieba.analyse.TFIDF(idf_path=None) 新建 TFIDF 实例,idf_path 为 IDF 频率文件 - # idf是jieba通过语料库统计得到的 - # idf的值时通过语料库统计得到的,所以,实际使用时,可能需要依据使用环境,替换为使用对应的语料库统计得到的idf值。 -""" - -# 关键词提取所使用逆向文件频率(IDF)文本语料库可以切换成自定义语料库的路径 -jieba.analyse.set_idf_path(file_name) # file_name为自定义语料库的路径 - -# 关键词提取所使用停止词(Stop Words)文本语料库可以切换成自定义语料库的路径 -jieba.analyse.set_stop_words(file_name) # file_name为自定义语料库的路径 - - -# 测试案例 -# 下载地址: https://github.com/fxsjy/jieba/blob/master/extra_dict/stop_words.txt -""" -the -of -is -""" -jieba.analyse.set_stop_words("~/work/data/nlp/jieba/extra_dict/stop_words.txt") -# 下载地址: https://raw.githubusercontent.com/fxsjy/jieba/master/extra_dict/idf.txt.big -"""格式如下: -劳动防护 13.900677652 -勞動防護 13.900677652 -奥萨贝尔 13.900677652 -""" -jieba.analyse.set_idf_path("~/work/data/nlp/jieba/extra_dict/idf.txt.big") -content = "此外,公司拟对全资子公司吉林欧亚置业有限公司增资4.3亿元,增资后,吉林欧亚置业注册资本由7000万元增加到5亿元。吉林欧亚置业主要经营范围为房地产开发及百货零售等业务。目前在建吉林欧亚城市商业综合体项目。2013年,实现营业收入0万元,实现净利润-139.13万元。" -# content = open("~/work/data/nlp/jieba/extra_dict/test_content.txt", 'rb').read() -for word, weight in jieba.analyse.extract_tags(content, withWeight=True): - print('%s %s' % (word, weight)) - -"""输出结果: -吉林 1.0174270215234043 -欧亚 0.7300142700289363 -增资 0.5087135107617021 -实现 0.5087135107617021 -置业 0.4887134522112766 -万元 0.3392722481859574 -此外 0.25435675538085106 -全资 0.25435675538085106 -有限公司 0.25435675538085106 -4.3 0.25435675538085106 -注册资本 0.25435675538085106 -7000 0.25435675538085106 -增加 0.25435675538085106 -主要 0.25435675538085106 -房地产 0.25435675538085106 -业务 0.25435675538085106 -目前 0.25435675538085106 -城市 0.25435675538085106 -综合体 0.25435675538085106 -2013 0.25435675538085106 -""" -``` - -### 基于 TextRank 算法的关键词抽取 - -> 基本思想: - -1. 将待抽取关键词的文本进行分词 -2. 以固定窗口大小(默认为5,通过span属性调整),词之间的共现关系,构建图 -3. 计算图中节点的PageRank,注意是无向带权图 - -> 举例说明 - -```py -例如: sentence = "A B C A D B C B A" - -第一次 index = 0 -dict[(A, B)] = 2 -dict[(A, C)] = 1 -dict[(A, A)] = 1 -dict[(A, D)] = 2 - -第一次 index = 1 -dict[(B, A)] = 1 -dict[(B, B)] = 1 -dict[(B, C)] = 2 -dict[(B, D)] = 1 - -由于是 无向带权图 - -graph[start].append((start, end, weight)) -graph[end].append((end, start, weight)) - -假设: -A B => 20 -所有 B => 50 -A, C => 5 -所有 C => 15 -总共: 10个单词 -A 权重PR值: 1/10 = 0.1 -s_A = 20/50 * 0.1 + 5/15 * 0.1 -d阻尼系数,即按照超链接进行浏览的概率,一般取经验值为0.85 -1−d浏览者随机跳转到一个新网页的概率 -A 权重PR值: (1 - d) + d * s_A -``` - -> 代码案例 - -```python -# encoding=utf-8 -import jieba - -# 直接使用,接口相同,注意默认过滤词性。 -# allowPOS('ns', 'n', 'vn', 'v') 地名、名词、动名词、动词 -jieba.analyse.textrank(sentence, topK=20, withWeight=False, allowPOS=('ns', 'n', 'vn', 'v')) -# 新建自定义 TextRank 实例 -jieba.analyse.TextRank() - - -# 测试案例 -content = "此外,公司拟对全资子公司吉林欧亚置业有限公司增资4.3亿元,增资后,吉林欧亚置业注册资本由7000万元增加到5亿元。吉林欧亚置业主要经营范围为房地产开发及百货零售等业务。目前在建吉林欧亚城市商业综合体项目。2013年,实现营业收入0万元,实现净利润-139.13万元。" -# content = open("~/work/data/nlp/jieba/extra_dict/test_content.txt", 'rb').read() -for x, w in jieba.analyse.textrank(content, withWeight=True): - print('%s %s' % (x, w)) -""" -吉林 1.0 -欧亚 0.9966893354178172 -置业 0.6434360313092776 -实现 0.5898606692859626 -收入 0.43677859947991454 -增资 0.4099900531283276 -子公司 0.35678295947672795 -城市 0.34971383667403655 -商业 0.34817220716026936 -业务 0.3092230992619838 -在建 0.3077929164033088 -营业 0.3035777049319588 -全资 0.303540981053475 -综合体 0.29580869172394825 -注册资本 0.29000519464085045 -有限公司 0.2807830798576574 -零售 0.27883620861218145 -百货 0.2781657628445476 -开发 0.2693488779295851 -经营范围 0.2642762173558316 -""" -``` - - -## 词性标注 - -* `jieba.posseg.POSTokenizer(tokenizer=None)` 新建自定义分词器, tokenizer 参数可指定内部使用的 `jieba.Tokenizer` 分词器, `jieba.posseg.dt` 为默认词性标注分词器 -* 标注句子分词后每个词的词性,采用和 ictclas 兼容的标记法 -* 除了jieba默认分词模式,提供paddle模式下的词性标注功能。paddle模式采用延迟加载方式,通过 `enable_paddle()` 安装 `paddlepaddle-tiny`,并且import相关代码 - -paddle模式词性标注对应表如下: - -paddle模式词性和专名类别标签集合如下表,其中词性标签 24 个(小写字母),专名类别标签 4 个(大写字母) - -| 标签 | 含义 | 标签 | 含义 | 标签 | 含义 | 标签 | 含义 | -| ---- | -------- | ---- | -------- | ---- | -------- | ---- | -------- | -| n | 普通名词 | f | 方位名词 | s | 处所名词 | t | 时间 | -| nr | 人名 | ns | 地名 | nt | 机构名 | nw | 作品名 | -| nz | 其他专名 | v | 普通动词 | vd | 动副词 | vn | 名动词 | -| a | 形容词 | ad | 副形词 | an | 名形词 | d | 副词 | -| m | 数量词 | q | 量词 | r | 代词 | p | 介词 | -| c | 连词 | u | 助词 | xc | 其他虚词 | w | 标点符号 | -| PER | 人名 | LOC | 地名 | ORG | 机构名 | TIME | 时间 | - - -```python -# encoding=utf-8 -import jieba -import jieba.posseg as pseg - - -words = jieba.posseg.cut("我爱北京天安门") -for word, flag in words: - print('%s %s' % (word, flag)) - -""" -我 r -爱 v -北京 ns -天安门 ns -""" - -words = pseg.cut("我爱北京天安门") #jieba默认模式 -jieba.enable_paddle() #启动paddle模式。 0.40版之后开始支持,早期版本不支持 -words = pseg.cut("我爱北京天安门",use_paddle=True) #paddle模式 -for word, flag in words: - print('%s %s' % (word, flag)) - -""" -我 r -爱 v -北京 ns -天安门 ns -""" -``` - -## 并行分词 - -原理: 将目标文本按行分隔后,把各行文本分配到多个 Python 进程并行分词,然后归并结果,从而获得分词速度的可观提升 - -基于 python 自带的 multiprocessing 模块,目前暂不支持 Windows - -用法: - -* `jieba.enable_parallel(4)` # 开启并行分词模式,参数为并行进程数 -* `jieba.disable_parallel()` # 关闭并行分词模式 - -```python -import sys -import time -import jieba - -jieba.enable_parallel() - -url = sys.argv[1] -content = open(url, "rb").read() -t1 = time.time() -words = "/ ".join(jieba.cut(content)) - -t2 = time.time() -tm_cost = t2-t1 - -log_f = open("1.log","wb") -log_f.write(words.encode('utf-8')) - -print('speed %s bytes/second' % (len(content)/tm_cost)) -``` - -实验结果: 在 4 核 3.4GHz Linux 机器上,对金庸全集进行精确分词,获得了 1MB/s 的速度,是单进程版的 3.3 倍。 - -注意: 并行分词仅支持默认分词器 `jieba.dt` 和 `jieba.posseg.dt` - -## Tokenize: 返回词语在原文的起止位置 - -注意,输入参数只接受 unicode - -> 默认模式 - -```python -# encoding=utf-8 -import jieba - - -result = jieba.tokenize(u'永和服装饰品有限公司') -for tk in result: - print("word %s\t\t start: %d \t\t end:%d" % (tk[0],tk[1],tk[2])) - -""" -word 永和 start: 0 end:2 -word 服装 start: 2 end:4 -word 饰品 start: 4 end:6 -word 有限公司 start: 6 end:10 -""" -``` - - -```python -# encoding=utf-8 -import jieba - - -result = jieba.tokenize(u'永和服装饰品有限公司', mode='search') -for tk in result: - print("word %s\t\t start: %d \t\t end:%d" % (tk[0],tk[1],tk[2])) - -""" -word 永和 start: 0 end:2 -word 服装 start: 2 end:4 -word 饰品 start: 4 end:6 -word 有限 start: 6 end:8 -word 公司 start: 8 end:10 -word 有限公司 start: 6 end:10 -""" -``` - -## ChineseAnalyzer for Whoosh 搜索引擎 - -引用: `from jieba.analyse import ChineseAnalyzer` - -* `pip install whoosh` -* Whoosh是一个用来索引文本并能根据索引搜索的的包含类和方法的类库。它允许你开发一个针对自己内容的搜索引擎。 -* 例如,如果你想创建一个博客软件,你可以使用Whoosh添加一个允许用户搜索博客类目的搜索功能。 - -```python -# -*- coding: UTF-8 -*- -from __future__ import unicode_literals -import sys,os -sys.path.append("../") -from whoosh.index import create_in,open_dir -from whoosh.fields import * -from whoosh.qparser import QueryParser - -from jieba.analyse.analyzer import ChineseAnalyzer - -analyzer = ChineseAnalyzer() - -schema = Schema(title=TEXT(stored=True), path=ID(stored=True), content=TEXT(stored=True, analyzer=analyzer)) -if not os.path.exists("tmp"): - os.mkdir("tmp") - -ix = create_in("tmp", schema) # for create new index -#ix = open_dir("tmp") # for read only -writer = ix.writer() - -writer.add_document( - title="document1", - path="/a", - content="This is the first document we’ve added!" -) - -writer.add_document( - title="document2", - path="/b", - content="The second one 你 中文测试中文 is even more interesting! 吃水果" -) - -writer.add_document( - title="document3", - path="/c", - content="买水果然后来世博园。" -) - -writer.add_document( - title="document4", - path="/c", - content="工信处女干事每月经过下属科室都要亲口交代24口交换机等技术性器件的安装工作" -) - -writer.add_document( - title="document4", - path="/c", - content="咱俩交换一下吧。" -) - -writer.commit() -searcher = ix.searcher() -parser = QueryParser("content", schema=ix.schema) - -for keyword in ("水果世博园","你","first","中文","交换机","交换"): - print("result of ", keyword) - q = parser.parse(keyword) - results = searcher.search(q) - for hit in results: - print(hit.highlights("content")) - print("="*10) - -for t in analyzer("我的好朋友是李明;我爱北京天安门;IBM和Microsoft; I have a dream. this is intetesting and interested me a lot"): - print(t.text) - - -""" -result of 水果世博园 -买水果然后来世博园 -========== -result of 你 -second one 中文测试中文 is even more interesting -========== -result of first -first document we’ve added -========== -result of 中文 -second one 你 中文测试中文 is even more interesting -========== -result of 交换机 -干事每月经过下属科室都要亲口交代24口交换机等技术性器件的安装工作 -========== -result of 交换 -咱俩交换一下吧 -干事每月经过下属科室都要亲口交代24口交换机等技术性器件的安装工作 -========== -我 -好 -朋友 -是 -李明 -我 -爱 -北京 -天安 -天安门 -ibm -microsoft -dream -intetest -interest -me -lot -""" -``` - ---- - -测试过: - -* jieba: [https://github.com/fxsjy/jieba](https://github.com/fxsjy/jieba) -* Hanlp: [https://github.com/hankcs/HanLP](https://github.com/hankcs/HanLP) -* nltk: [https://yiyibooks.cn/yiyi/nltk_python/index.html](https://yiyibooks.cn/yiyi/nltk_python/index.html) - * nltk 已经兼容 stanford corenlp 分词模版 - * CoreNLP: [https://github.com/Lynten/stanford-corenlp](https://github.com/Lynten/stanford-corenlp) -* pyltp: [http://www.ltp-cloud.com/document](http://www.ltp-cloud.com/document) -* pkuseg: [https://github.com/lancopku/pkuseg-pytho](https://github.com/lancopku/pkuseg-python) - -呆测试: - -* NLPIR: [http://ictclas.nlpir.org](http://ictclas.nlpir.org) -* 新浪云: [http://www.sinacloud.com/doc/sae/python/segment.html](http://www.sinacloud.com/doc/sae/python/segment.html) -* 盘古分词: [http://pangusegment.codeplex.com/](http://pangusegment.codeplex.com/) -* 搜狗分词: [http://www.sogou.com/labs/webservice/](http://www.sogou.com/labs/webservice/) -* 庖丁解牛: [https://code.google.com/p/paoding/](https://code.google.com/p/paoding/) -* BosonNLP: [http://bosonnlp.com/dev/center](http://bosonnlp.com/dev/center) -* IKAnalyzer: [http://www.oschina.net/p/ikanalyzer](http://www.oschina.net/p/ikanalyzer) -* SCWS中文分词: [http://www.xunsearch.com/scws/docs.php](http://www.xunsearch.com/scws/docs.php) diff --git "a/docs/nlp/3.1.\347\257\207\347\253\240\345\210\206\346\236\220-\345\206\205\345\256\271\346\246\202\350\277\260.md" "b/docs/nlp/3.1.\347\257\207\347\253\240\345\210\206\346\236\220-\345\206\205\345\256\271\346\246\202\350\277\260.md" deleted file mode 100644 index 7113a4a3a28fbcec3040b6c1c2fa782ef345a503..0000000000000000000000000000000000000000 --- "a/docs/nlp/3.1.\347\257\207\347\253\240\345\210\206\346\236\220-\345\206\205\345\256\271\346\246\202\350\277\260.md" +++ /dev/null @@ -1,30 +0,0 @@ -# 篇章分析-内容概述 - -## 篇章分析变迁 - -1. 内容生态: 新浪 -> 百家号、今日头条(自媒体) -2. 用户成为信息的生产中心: web 1.0 -> 百度贴吧、新浪微博、团购网站(用户评论,富有个人情感和用户观点的信息) -3. 移动、无屏: 显示屏 -> 手机、Siri(展示的终端) - -## 篇章分析场景 - -篇章分析重要性: 让人们最平等`便捷`地`获取信息`,`找到所求`。 - -1. 个性化信息获取(搜索引擎的理解和推荐): 从搜索的角度来看,通过对内容的深入理解,我们能够精准地对内容进行分析,然后将内容推荐给需要的用户,达到不搜即得。 -2. 便捷咨询阅读(头条的热门推荐): 从资讯阅读的角度来看,我们通过对内容进行概括总结、形成摘要,就能搞让用户更快捷地浏览信息、获取知识。 -3. 信息直接满足: 更进一步说,对用户的问题,我们可以基于内容理解,直接给出答案,从而满足用户的需求。 - -`总之`: 通过篇章分析,我们能够进行内容理解,从而更好地服务用户。 - -## 篇章分析概述 - -`篇章是形式上互相衔接、语义上前后连贯的句子序列。` - -有以下3种: -* 1.文章: 新闻稿、博客、微博 -* 2.评论: O2O服务的用户评论、豆瓣的影评、微博上的动态 -* 3.对话: 话题上是相互衔接的、语义上也是连贯的一个对话序列 - -## 篇章分析任务 - -![](img/3.1.篇章分析-内容标签/篇章分析任务.jpg) diff --git "a/docs/nlp/3.2.\347\257\207\347\253\240\345\210\206\346\236\220-\345\206\205\345\256\271\346\240\207\347\255\276.md" "b/docs/nlp/3.2.\347\257\207\347\253\240\345\210\206\346\236\220-\345\206\205\345\256\271\346\240\207\347\255\276.md" deleted file mode 100644 index 3fbc6240ca7601879f371fe3527705bbc708989c..0000000000000000000000000000000000000000 --- "a/docs/nlp/3.2.\347\257\207\347\253\240\345\210\206\346\236\220-\345\206\205\345\256\271\346\240\207\347\255\276.md" +++ /dev/null @@ -1,82 +0,0 @@ -# 篇章分析-内容标签 - -`标签`: 这种种的`单词`和`词组`都是一种标签的形式 -1. 新闻稿,打出关于该报道的各种各样的标签,来表示其关键信息 -2. 论文中,我们也会表明一些文章的`领域分类`以及`关键词`等标签 -3. 微博用#代表一个话题,这是典型的社会化标签 - -## 标签用途 - -1. 关键信息展示 - * 用户可以大致了解文章的主要信息,从而决定要不要对信息进行进一步深入地浏览 - -2. 频道划分 - * 在很多的媒体网站,经常会有频道划分,使用了就是文章的分类标签 - -3. 话题聚合 - * 标签也可以用来做话题聚合(例如: #人民的名义# 集合所有关于这个话题的信息,让用户更深入的了解信息) - -## 应用: 个性化推荐 - -* 标签可以用来建立用户的画像 - -比如对对于用户搜索过的Query,还有他浏览过的文章,都可以通过标签的技术。提取出主要的兴趣点,从而也就建立了用户的画像 - -* 标签可以对内容进行建模 - -通过标签技术,我们能够提取文章中的关键信息标签。这样来看标签就作为了用户和内容的一个共同表示。 - -* 推荐的时候,我们通过对用户画像的标签和内容模型的标签进行匹配,就能够对用户进行一个精准的个性化推荐 - -## 百度内容标签 - -![](img/3.2.篇章分析-内容标签/百度内容标签.jpg) - -## 标签体系: 面向推荐的标签图谱 - -* 标签图谱刻画了用户的兴趣点,以及兴趣点之间的关联关系。 -* 节点表示了用户的兴趣点,而边表示了兴趣点之间的关联关系(边是带有权重的,表示关联强度)。 -* 包括3种节点: 主题标签-绿色,话题标签-紫色,实体标签-蓝色。 -* 有了关联关系,我们可以进行一定程度的探索和泛化。(例如: 无人驾驶和人工智能关联很强,如果有人看了无人驾驶,我们就给他推荐人工智能) - -![](img/3.2.篇章分析-内容标签/面向推荐的标签图谱.jpg) - -## 标签体系: 基于大数据分析的图谱构建 - -* 用户信息来源: 贴吧、微博 -* 标签的相关性分析: 通过关联规则,发现2个标签总同时出现,我们觉得这是高相关的。 - -![](img/3.2.篇章分析-内容标签/基于大数据分析的图谱构建.jpg) - -## 标签计算 - -> 主题分类 - -* 主题标签的计算,是一种很典型的文本分类问题: 传统的朴素贝叶斯、最大熵、SVM 等解决方案。 -* 当前我们主要采用的是: 基于神经网络的方法(可以看右侧的示意图) -* 整个网络分成3层次: - * 第一层 原始特征层: 抽取简单的原始特征,例如说文章出现的单词、词组 等等 - * 第二层 表示层: 通过一些 embedding的算法、CNN、LSTM的方法 - * 第三层 排序层: 计算文章与主题之间的相似度,具体会计算每个主题与文章的相似度,并将相似度作为最终的一个主题分类的结果。这种计算的好处能够天然的支持多标记,也就是一篇文章可以同时计算出多个主题标签。 - -![](img/3.2.篇章分析-内容标签/主题分类.jpg) - -> 通用标签 - -* 通用标签主要是计算内容中的实体和话题,我们综合了两种策略。 -* 第一种策略: 针对比较热门的高频标签 - * 这种标签我们主要通过一些预测的方法得到,预测的方法: 基于相似度计算得到的---这种方法并不要求标签一定在文章中出现 - * 例如: 美国大选这种标签,如果一篇文章出现了 `希拉里` `特朗普` `辩论` 等一些词,即使没有出现美国大选,我们通过语义相似度的方法也能把这个标签计算出来。 -* 第二种策略: 面向中低频的标签 - * 这种标签相关的信息,不是那么丰富,所以我们计算的时候更多依赖的是标签在文章中的信息 - * 比如: 这个标签在文章中出现的频率 或 出现的位置;如果出现在标题,那么它可能就会比较重要。 -* 通过融合这2种策略,形成我们通用标签的结果。 - -![](img/3.2.篇章分析-内容标签/通用标签.jpg) - -## 内容标签在Feed流中的应用 - -1. 标签可以用来话题聚合: 比如表示人工智能的标签全部都会集合到同一个话题下面。这样用户可以对人工智能这个话题进行非常充分的浏览。 -2. 话题频道划分: 比如我们在手机百度上面就可以看到,Feed流上面有多个栏目,用户可以点击 `体育` `时尚`等频道 - -![](img/3.2.篇章分析-内容标签/内容标签在Feed流中的应用.jpg) diff --git "a/docs/nlp/3.3.\347\257\207\347\253\240\345\210\206\346\236\220-\346\203\205\346\204\237\345\210\206\346\236\220.md" "b/docs/nlp/3.3.\347\257\207\347\253\240\345\210\206\346\236\220-\346\203\205\346\204\237\345\210\206\346\236\220.md" deleted file mode 100644 index 854b731b15d5a4e4893e899cb6de377c3dd92662..0000000000000000000000000000000000000000 --- "a/docs/nlp/3.3.\347\257\207\347\253\240\345\210\206\346\236\220-\346\203\205\346\204\237\345\210\206\346\236\220.md" +++ /dev/null @@ -1,45 +0,0 @@ -# 篇章分析-情感分类 - -## 用户评论剧增 - -`服务评论` `商品评论` `社交评论` - -## 情感分析应用 - -`消费决策` `舆情分析` - -> 情感分类 和 观点挖掘 - -* 对(文本的)观点、情感、情绪和评论进行分析计算 - -![](img/3.3.篇章分析-情感分类/情感分类和观点挖掘.jpg) - -> 情感分类 - -* 给定一个文本判断其情感的极性,包括积极、中性、消极。 -* LSTM 对文本进行语义表示,进而基于语义表示进行情感分类。 - -![](img/3.3.篇章分析-情感分类/情感分类.jpg) - -> 观点挖掘 - -* 观点聚类: 主要目标是对大量的评论数据进行聚类,将相同的观点抽取出来,并形成一个情感搭配词典(算法是: 搭配抽取、词法分析、聚类归一,从而获得一个情感搭配。我们就可以进行观点抽取) -* 观点抽取: 就是对输入的文本进行计算,将其中的情感标签抽取出来,这里的标签,都是来自于情感搭配词典的,也就是观点聚类获得的词典。 - * 观点抽取一种简单的做法是直接通过标签匹配的方式得到,比如: 服务不错这个情感搭配,恰好在文本中出现,我们就可以把它抽取出来。 - * 但是这种简单的抽取方法,其实上只能从字面上抽取情感搭配,而无法解决字面不一致的,但是意思一样的情感搭配抽取,因此我们还引入了语义相似度的方法。这种方法主要是通过神经网络进行计算的。它能解决这种字面不一致,语义一样的抽取问题。 - -![](img/3.3.篇章分析-情感分类/观点挖掘.jpg) - -> 观点摘要 - -综合了情感分类和观点挖掘的一些技术,而获得的一个整体的应用技术 - -![](img/3.3.篇章分析-情感分类/观点摘要.jpg) - -## 百度应用: 评论观点 - -![](img/3.3.篇章分析-情感分类/百度应用评论观点.jpg) - -## 百度应用: 推荐理由 - -![](img/3.3.篇章分析-情感分类/百度应用推荐理由.jpg) diff --git "a/docs/nlp/3.4.\347\257\207\347\253\240\345\210\206\346\236\220-\350\207\252\345\212\250\346\221\230\350\246\201.md" "b/docs/nlp/3.4.\347\257\207\347\253\240\345\210\206\346\236\220-\350\207\252\345\212\250\346\221\230\350\246\201.md" deleted file mode 100644 index bd31032f1aa922a251d0b0fa957c144edf8595ab..0000000000000000000000000000000000000000 --- "a/docs/nlp/3.4.\347\257\207\347\253\240\345\210\206\346\236\220-\350\207\252\345\212\250\346\221\230\350\246\201.md" +++ /dev/null @@ -1,62 +0,0 @@ -# 篇章分析-自动摘要 - -## 信息爆炸与移动化 - -![](img/3.4.篇章分析-自动摘要/信息爆炸与移动化.jpg) - -## 自动摘要应用 - -* 便捷信息浏览 - * 我们可以为每个新闻抽取摘要,用户可以通过摘要快速了解新闻概况。 - * 进而决定是否要进一步细致地浏览。 - * 而更进一步说: 摘要还可以直接进行信息满足。 -* 信息满足 - * 传统搜索得到一大批网页信息 - * 现在通过问答技术我们能够将网页中最核心的片段摘要提取出来。 - * 用户通过阅读片段,就可以直接得到满足,而不需要打开页面。 - -## 自动摘要 - -* 对海量内容进行提炼与总结 -* 以简洁、直观的摘要来概括用户所关注的主要内容 -* 方便用户快速了解与浏览海量内容 - -![](img//3.4.篇章分析-自动摘要/摘要系统.jpg) - -* 自动摘要分类 - -![](img/3.4.篇章分析-自动摘要/自动摘要分类.jpg) - -* 典型摘要计算流程 - -![](img/3.4.篇章分析-自动摘要/典型摘要计算流程.jpg) - -> 基于篇章信息的通用新闻摘要 - -![](img/3.4.篇章分析-自动摘要/基于篇章信息的通用新闻摘要.jpg) - -> 篇章主题摘要 - -![](img/3.4.篇章分析-自动摘要/篇章主题摘要.jpg) - -> 问答摘要 - -![](img/3.4.篇章分析-自动摘要/问答摘要.jpg) - -## 百度应用 - -> 文本和语言摘要 - -![](img/3.4.篇章分析-自动摘要/百度应用文本和语言摘要.jpg) - -> 问答摘要 - -![](img/3.4.篇章分析-自动摘要/百度应用问答摘要.jpg) - -> 搜索播报摘要和图像摘要 - -![](img/3.4.篇章分析-自动摘要/百度应用搜索播报摘要和图像摘要.jpg) - -## 总结 - -![](img/3.4.篇章分析-自动摘要/总结.jpg) diff --git a/docs/nlp/3.md b/docs/nlp/3.md new file mode 100644 index 0000000000000000000000000000000000000000..deb310ff81f3870dee215320dab2006eb2a22dd5 --- /dev/null +++ b/docs/nlp/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/3.\345\221\275\345\220\215\345\256\236\344\275\223\350\257\206\345\210\253.md" "b/docs/nlp/3.\345\221\275\345\220\215\345\256\236\344\275\223\350\257\206\345\210\253.md" deleted file mode 100644 index 6a1658f7622d6c523fd906195aba93f076f8087f..0000000000000000000000000000000000000000 --- "a/docs/nlp/3.\345\221\275\345\220\215\345\256\236\344\275\223\350\257\206\345\210\253.md" +++ /dev/null @@ -1,13 +0,0 @@ - - - -使用Bakeoff-3评测中所采用的的BIO标注集: - -B-PER、I-PER 代表人名首字、人名非首字, -B-LOC、I-LOC 代表地名首字、地名非首字, -B-ORG、I-ORG 代表组织机构名首字、组织机构名非首字, -O 代表该字不属于命名实体的一部分。 - - -输入输出的计算方式: -$$L_{out}=floor((L_{in}+2*padding-dilation*(kernerl\_size-1)-1)/stride+1)$$ diff --git a/docs/nlp/4.md b/docs/nlp/4.md new file mode 100644 index 0000000000000000000000000000000000000000..2009e9cc0ab2d738322ecbb3f317133110b37372 --- /dev/null +++ b/docs/nlp/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/5.md b/docs/nlp/5.md new file mode 100644 index 0000000000000000000000000000000000000000..81ef8bc08397ced0b7ba9a2708b8b13af1006d1e --- /dev/null +++ b/docs/nlp/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/6.md b/docs/nlp/6.md new file mode 100644 index 0000000000000000000000000000000000000000..84fe1a9c0ec16982e8d258cda6e8f4e6d90de9b0 --- /dev/null +++ b/docs/nlp/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/7.md b/docs/nlp/7.md new file mode 100644 index 0000000000000000000000000000000000000000..748347398864c9faa9e595a75ffe43d27313b5fb --- /dev/null +++ b/docs/nlp/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/8.md b/docs/nlp/8.md new file mode 100644 index 0000000000000000000000000000000000000000..9cb7a886f936adebf9176e30140b2fc99d3f691a --- /dev/null +++ b/docs/nlp/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/9.md b/docs/nlp/9.md new file mode 100644 index 0000000000000000000000000000000000000000..183c1e629160d508ebbc3495a59db47b7ff703a3 --- /dev/null +++ b/docs/nlp/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/Images/05c7618d4554f83cfa6105c528703794.jpg b/docs/nlp/Images/05c7618d4554f83cfa6105c528703794.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a66202b3b28a81f27271c0f54c2c9de9cef23b6a Binary files /dev/null and b/docs/nlp/Images/05c7618d4554f83cfa6105c528703794.jpg differ diff --git a/docs/nlp/Images/07e7d99633e4a107388f7202380cce55.jpg b/docs/nlp/Images/07e7d99633e4a107388f7202380cce55.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c9a369be517237d23d1eb51f33fac7a52d9bd1ce Binary files /dev/null and b/docs/nlp/Images/07e7d99633e4a107388f7202380cce55.jpg differ diff --git a/docs/nlp/Images/0ddbd56a410c886c77d1c72a84e27883.jpg b/docs/nlp/Images/0ddbd56a410c886c77d1c72a84e27883.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1f860d6e8e049370e906ed19d6b7f9b357638a31 Binary files /dev/null and b/docs/nlp/Images/0ddbd56a410c886c77d1c72a84e27883.jpg differ diff --git a/docs/nlp/Images/0de8ea07b37cc379da8aeb4a0c9fd5bf.jpg b/docs/nlp/Images/0de8ea07b37cc379da8aeb4a0c9fd5bf.jpg new file mode 100644 index 0000000000000000000000000000000000000000..51a5eb4dad0351931c97bb3b5f5a84390b6585a8 Binary files /dev/null and b/docs/nlp/Images/0de8ea07b37cc379da8aeb4a0c9fd5bf.jpg differ diff --git a/docs/nlp/Images/0e6e187e03f69a6548e7daa4f95e1548.jpg b/docs/nlp/Images/0e6e187e03f69a6548e7daa4f95e1548.jpg new file mode 100644 index 0000000000000000000000000000000000000000..881a4da78ebc56a4bbd4120cde22c451a644083d Binary files /dev/null and b/docs/nlp/Images/0e6e187e03f69a6548e7daa4f95e1548.jpg differ diff --git a/docs/nlp/Images/0e768e8c4378c2b0b3290aab46dc770e.jpg b/docs/nlp/Images/0e768e8c4378c2b0b3290aab46dc770e.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ae0c12174a28a1b2438b4f49d82794a7cc6148a6 Binary files /dev/null and b/docs/nlp/Images/0e768e8c4378c2b0b3290aab46dc770e.jpg differ diff --git a/docs/nlp/Images/0f4441cdaf35bfa4d58fc64142cf4736.jpg b/docs/nlp/Images/0f4441cdaf35bfa4d58fc64142cf4736.jpg new file mode 100644 index 0000000000000000000000000000000000000000..08f8f6a8025e6aca43d326c81e08153d8c3cb208 Binary files /dev/null and b/docs/nlp/Images/0f4441cdaf35bfa4d58fc64142cf4736.jpg differ diff --git a/docs/nlp/Images/102675fd70e434164536c75bf7f8f043.jpg b/docs/nlp/Images/102675fd70e434164536c75bf7f8f043.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9a81a389d4b2c37093b40f164b3a761a38c49ed5 Binary files /dev/null and b/docs/nlp/Images/102675fd70e434164536c75bf7f8f043.jpg differ diff --git a/docs/nlp/Images/1094084b61ac3f0e4416e92869c52ccd.jpg b/docs/nlp/Images/1094084b61ac3f0e4416e92869c52ccd.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cfd8f2a91d038c446f58275baab0e19b99e28723 Binary files /dev/null and b/docs/nlp/Images/1094084b61ac3f0e4416e92869c52ccd.jpg differ diff --git a/docs/nlp/Images/10a8a58e33a0a6b7fb71389ea2114566.jpg b/docs/nlp/Images/10a8a58e33a0a6b7fb71389ea2114566.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2219f36c644b88ae1cc17db4e717c08b5b9dd63a Binary files /dev/null and b/docs/nlp/Images/10a8a58e33a0a6b7fb71389ea2114566.jpg differ diff --git a/docs/nlp/Images/10a910dd6de117ab7a0ab352519f7297.jpg b/docs/nlp/Images/10a910dd6de117ab7a0ab352519f7297.jpg new file mode 100644 index 0000000000000000000000000000000000000000..72fe377c6eb315e5aaf490bb32f6ae3ee538e3b6 Binary files /dev/null and b/docs/nlp/Images/10a910dd6de117ab7a0ab352519f7297.jpg differ diff --git a/docs/nlp/Images/12573c3a9015654728fe798e170a3c50.jpg b/docs/nlp/Images/12573c3a9015654728fe798e170a3c50.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f8d0f58820b33eb6123831fa0d24d5ff5361b75f Binary files /dev/null and b/docs/nlp/Images/12573c3a9015654728fe798e170a3c50.jpg differ diff --git a/docs/nlp/Images/13361de430cd983e689417c547330bbc.jpg b/docs/nlp/Images/13361de430cd983e689417c547330bbc.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e0ece5f407ede690235a8a548fbda0a9083baef7 Binary files /dev/null and b/docs/nlp/Images/13361de430cd983e689417c547330bbc.jpg differ diff --git a/docs/nlp/Images/13f25b9eba42f74ad969a74cee78551e.jpg b/docs/nlp/Images/13f25b9eba42f74ad969a74cee78551e.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ae840901dba5ad53b65b816ff7e35759bfad1466 Binary files /dev/null and b/docs/nlp/Images/13f25b9eba42f74ad969a74cee78551e.jpg differ diff --git a/docs/nlp/Images/14a0a2692f06286091f0cca17de5c0f3.jpg b/docs/nlp/Images/14a0a2692f06286091f0cca17de5c0f3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..57dfbc143608e3c20c968bfb0c146a722abff72b Binary files /dev/null and b/docs/nlp/Images/14a0a2692f06286091f0cca17de5c0f3.jpg differ diff --git a/docs/nlp/Images/14a15b73cb826ac3464754d6db3e9e54.jpg b/docs/nlp/Images/14a15b73cb826ac3464754d6db3e9e54.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fe518f4f7596bf4b0ed652304f45a9007685051f Binary files /dev/null and b/docs/nlp/Images/14a15b73cb826ac3464754d6db3e9e54.jpg differ diff --git a/docs/nlp/Images/160f0b5ad0e4b8745b925d63377a69ba.jpg b/docs/nlp/Images/160f0b5ad0e4b8745b925d63377a69ba.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c9a369be517237d23d1eb51f33fac7a52d9bd1ce Binary files /dev/null and b/docs/nlp/Images/160f0b5ad0e4b8745b925d63377a69ba.jpg differ diff --git a/docs/nlp/Images/17af2b7a4de652abd5c2c71a94cc1c7b.jpg b/docs/nlp/Images/17af2b7a4de652abd5c2c71a94cc1c7b.jpg new file mode 100644 index 0000000000000000000000000000000000000000..92ce2f85bf238e83846cf53a68c23053f339b33c Binary files /dev/null and b/docs/nlp/Images/17af2b7a4de652abd5c2c71a94cc1c7b.jpg differ diff --git a/docs/nlp/Images/1ab45939cc9babb242ac45ed03a94f7a.jpg b/docs/nlp/Images/1ab45939cc9babb242ac45ed03a94f7a.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2f2ecd393a4e555ecad47010279f2f62e257e77c Binary files /dev/null and b/docs/nlp/Images/1ab45939cc9babb242ac45ed03a94f7a.jpg differ diff --git a/docs/nlp/Images/1b33abb14fc8fe7c704d005736ddb323.jpg b/docs/nlp/Images/1b33abb14fc8fe7c704d005736ddb323.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8bc37e7d25bd2c3e8e389ef70a17cd4b14590925 Binary files /dev/null and b/docs/nlp/Images/1b33abb14fc8fe7c704d005736ddb323.jpg differ diff --git a/docs/nlp/Images/1c54b3124863d24d17b2edec4f1d47e5.jpg b/docs/nlp/Images/1c54b3124863d24d17b2edec4f1d47e5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e6240b5d01435109957cc0150b1cd56d1730868f Binary files /dev/null and b/docs/nlp/Images/1c54b3124863d24d17b2edec4f1d47e5.jpg differ diff --git a/docs/nlp/Images/1cf5b2605018e587fa94db2ac671e930.jpg b/docs/nlp/Images/1cf5b2605018e587fa94db2ac671e930.jpg new file mode 100644 index 0000000000000000000000000000000000000000..bc4b19cc809752c7b829cd67d3d372da0078065b Binary files /dev/null and b/docs/nlp/Images/1cf5b2605018e587fa94db2ac671e930.jpg differ diff --git a/docs/nlp/Images/21f757d2c1451cb55bc923dcb4d8d1ab.jpg b/docs/nlp/Images/21f757d2c1451cb55bc923dcb4d8d1ab.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e74f104efd732ef15dfbf007fcb959f8ca26298b Binary files /dev/null and b/docs/nlp/Images/21f757d2c1451cb55bc923dcb4d8d1ab.jpg differ diff --git a/docs/nlp/Images/24589c2eb435b25724aed562d1d47617.jpg b/docs/nlp/Images/24589c2eb435b25724aed562d1d47617.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9b4c94ec8d3077612a4726467edfb9062266972a Binary files /dev/null and b/docs/nlp/Images/24589c2eb435b25724aed562d1d47617.jpg differ diff --git a/docs/nlp/Images/273b9ed1ea067e4503c00dbd193216e8.jpg b/docs/nlp/Images/273b9ed1ea067e4503c00dbd193216e8.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3fde8ca5a432d3ef0081966385f8c19d196a5c43 Binary files /dev/null and b/docs/nlp/Images/273b9ed1ea067e4503c00dbd193216e8.jpg differ diff --git a/docs/nlp/Images/27ffeabf8327e1810d6ac35642a72700.jpg b/docs/nlp/Images/27ffeabf8327e1810d6ac35642a72700.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8f8bae227d1b11844e3d721ca3f7b18e915a454e Binary files /dev/null and b/docs/nlp/Images/27ffeabf8327e1810d6ac35642a72700.jpg differ diff --git a/docs/nlp/Images/2ce816f11fd01927802253d100780b0a.jpg b/docs/nlp/Images/2ce816f11fd01927802253d100780b0a.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d74ef954f36d817b0300b0b87262448f1fb19fae Binary files /dev/null and b/docs/nlp/Images/2ce816f11fd01927802253d100780b0a.jpg differ diff --git a/docs/nlp/Images/2cfe7a0ac0b6007d979cf3aecda8a9f5.jpg b/docs/nlp/Images/2cfe7a0ac0b6007d979cf3aecda8a9f5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b8e541770516bd7177cec216a050431889583e4e Binary files /dev/null and b/docs/nlp/Images/2cfe7a0ac0b6007d979cf3aecda8a9f5.jpg differ diff --git a/docs/nlp/Images/334be383b5db7ffe3599cc03bc74bf9e.jpg b/docs/nlp/Images/334be383b5db7ffe3599cc03bc74bf9e.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ae840901dba5ad53b65b816ff7e35759bfad1466 Binary files /dev/null and b/docs/nlp/Images/334be383b5db7ffe3599cc03bc74bf9e.jpg differ diff --git a/docs/nlp/Images/346344c2e5a627acfdddf948fb69cb1d.jpg b/docs/nlp/Images/346344c2e5a627acfdddf948fb69cb1d.jpg new file mode 100644 index 0000000000000000000000000000000000000000..82ca075121646a0ebede2d2a529dbbf7ed85bb05 Binary files /dev/null and b/docs/nlp/Images/346344c2e5a627acfdddf948fb69cb1d.jpg differ diff --git a/docs/nlp/Images/37edef9faf625ac06477a0ab0118afca.jpg b/docs/nlp/Images/37edef9faf625ac06477a0ab0118afca.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2a74bc95a74322a9cb2bb6b8ecca02b443ac3f80 Binary files /dev/null and b/docs/nlp/Images/37edef9faf625ac06477a0ab0118afca.jpg differ diff --git a/docs/nlp/Images/3a93e0258a010fdda935b4ee067411a5.jpg b/docs/nlp/Images/3a93e0258a010fdda935b4ee067411a5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c9a369be517237d23d1eb51f33fac7a52d9bd1ce Binary files /dev/null and b/docs/nlp/Images/3a93e0258a010fdda935b4ee067411a5.jpg differ diff --git a/docs/nlp/Images/3edaf7564caaab7c8ddc83db1d5408a3.jpg b/docs/nlp/Images/3edaf7564caaab7c8ddc83db1d5408a3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..28f07f716a9c82088fb62ea2e950cc9e70dc7dd2 Binary files /dev/null and b/docs/nlp/Images/3edaf7564caaab7c8ddc83db1d5408a3.jpg differ diff --git a/docs/nlp/Images/4150e51ab7e511f8d4f72293054ceb22.jpg b/docs/nlp/Images/4150e51ab7e511f8d4f72293054ceb22.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c0b7cb51dbdd83fc74b59e19f3838089f5f507b3 Binary files /dev/null and b/docs/nlp/Images/4150e51ab7e511f8d4f72293054ceb22.jpg differ diff --git a/docs/nlp/Images/431fed60785d71efd9010589288ca55d.jpg b/docs/nlp/Images/431fed60785d71efd9010589288ca55d.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9ab98b73863e244a172953b7e8b6b359cf9f4bd2 Binary files /dev/null and b/docs/nlp/Images/431fed60785d71efd9010589288ca55d.jpg differ diff --git a/docs/nlp/Images/43a0f901bdd8f343b2885d2b2e95b996.jpg b/docs/nlp/Images/43a0f901bdd8f343b2885d2b2e95b996.jpg new file mode 100644 index 0000000000000000000000000000000000000000..caa9c0fb579a09280ba57f0f2587d909dc407606 Binary files /dev/null and b/docs/nlp/Images/43a0f901bdd8f343b2885d2b2e95b996.jpg differ diff --git a/docs/nlp/Images/484180fc6abc244116b30e57cb6c0cf5.jpg b/docs/nlp/Images/484180fc6abc244116b30e57cb6c0cf5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..28522fa54086463791bf5e7eca9f2a43bf77593d Binary files /dev/null and b/docs/nlp/Images/484180fc6abc244116b30e57cb6c0cf5.jpg differ diff --git a/docs/nlp/Images/496754d8cdb6262f8f72e1f066bab359.jpg b/docs/nlp/Images/496754d8cdb6262f8f72e1f066bab359.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ae840901dba5ad53b65b816ff7e35759bfad1466 Binary files /dev/null and b/docs/nlp/Images/496754d8cdb6262f8f72e1f066bab359.jpg differ diff --git a/docs/nlp/Images/499f8953a7d39c034e6840bdacd99d08.jpg b/docs/nlp/Images/499f8953a7d39c034e6840bdacd99d08.jpg new file mode 100644 index 0000000000000000000000000000000000000000..98959d2c32f05f1e870b2b45f1b168ad57fb2303 Binary files /dev/null and b/docs/nlp/Images/499f8953a7d39c034e6840bdacd99d08.jpg differ diff --git a/docs/nlp/Images/4a87f1dccc0e18aab5ec599d8d8358d6.jpg b/docs/nlp/Images/4a87f1dccc0e18aab5ec599d8d8358d6.jpg new file mode 100644 index 0000000000000000000000000000000000000000..deeda2036c001319d1aaab76ac7cdf7c64c2e26e Binary files /dev/null and b/docs/nlp/Images/4a87f1dccc0e18aab5ec599d8d8358d6.jpg differ diff --git a/docs/nlp/Images/4acbae4abe459cf45122fe134ff7672d.jpg b/docs/nlp/Images/4acbae4abe459cf45122fe134ff7672d.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f1a8be461430f9e1707ea29a4381213fb9da8456 Binary files /dev/null and b/docs/nlp/Images/4acbae4abe459cf45122fe134ff7672d.jpg differ diff --git a/docs/nlp/Images/4b32a28b1aab7148420347abc990ee67.jpg b/docs/nlp/Images/4b32a28b1aab7148420347abc990ee67.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2b9a661659b5b0a13ab5b29bdf48dcde6ee6b3b7 Binary files /dev/null and b/docs/nlp/Images/4b32a28b1aab7148420347abc990ee67.jpg differ diff --git a/docs/nlp/Images/4b5cae275c53c53ccc8f2f779acada3e.jpg b/docs/nlp/Images/4b5cae275c53c53ccc8f2f779acada3e.jpg new file mode 100644 index 0000000000000000000000000000000000000000..82ca075121646a0ebede2d2a529dbbf7ed85bb05 Binary files /dev/null and b/docs/nlp/Images/4b5cae275c53c53ccc8f2f779acada3e.jpg differ diff --git a/docs/nlp/Images/4cdc400cf76b0354304e01aeb894877b.jpg b/docs/nlp/Images/4cdc400cf76b0354304e01aeb894877b.jpg new file mode 100644 index 0000000000000000000000000000000000000000..efd0b698ad23ed69c27f6c81eed040306a53eb91 Binary files /dev/null and b/docs/nlp/Images/4cdc400cf76b0354304e01aeb894877b.jpg differ diff --git a/docs/nlp/Images/4cec5bd698a48f7e083696dd51ae9e7a.jpg b/docs/nlp/Images/4cec5bd698a48f7e083696dd51ae9e7a.jpg new file mode 100644 index 0000000000000000000000000000000000000000..08f8f6a8025e6aca43d326c81e08153d8c3cb208 Binary files /dev/null and b/docs/nlp/Images/4cec5bd698a48f7e083696dd51ae9e7a.jpg differ diff --git a/docs/nlp/Images/513df73dfd52feca2c96a86dcc261c8b.jpg b/docs/nlp/Images/513df73dfd52feca2c96a86dcc261c8b.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d486e1ec072aaa7bdbd92390b7f21a7cd79728e3 Binary files /dev/null and b/docs/nlp/Images/513df73dfd52feca2c96a86dcc261c8b.jpg differ diff --git a/docs/nlp/Images/532d5f3185ea7edaec68683d89a74182.jpg b/docs/nlp/Images/532d5f3185ea7edaec68683d89a74182.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6471a26f4f70679e62e12d2f14e9018f05807da6 Binary files /dev/null and b/docs/nlp/Images/532d5f3185ea7edaec68683d89a74182.jpg differ diff --git a/docs/nlp/Images/542fee25c56235c899312bed3d5ee9ba.jpg b/docs/nlp/Images/542fee25c56235c899312bed3d5ee9ba.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a93a102161a190f9ab259605204481e897abb1d5 Binary files /dev/null and b/docs/nlp/Images/542fee25c56235c899312bed3d5ee9ba.jpg differ diff --git a/docs/nlp/Images/55f4da85888c6e9974fea5360283035a.jpg b/docs/nlp/Images/55f4da85888c6e9974fea5360283035a.jpg new file mode 100644 index 0000000000000000000000000000000000000000..417c239c34962e6cf7021ec9d4400a88eaae3f6d Binary files /dev/null and b/docs/nlp/Images/55f4da85888c6e9974fea5360283035a.jpg differ diff --git a/docs/nlp/Images/56cee123595482cf3edaef089cb9a6a7.jpg b/docs/nlp/Images/56cee123595482cf3edaef089cb9a6a7.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ebf309ec863c857a3831d2350edc5cff8619ee1c Binary files /dev/null and b/docs/nlp/Images/56cee123595482cf3edaef089cb9a6a7.jpg differ diff --git a/docs/nlp/Images/5741a8d50a63172cbd2c825cda1b61d5.jpg b/docs/nlp/Images/5741a8d50a63172cbd2c825cda1b61d5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d6a76a18b1425d1dceddcc95eda42918b11a527c Binary files /dev/null and b/docs/nlp/Images/5741a8d50a63172cbd2c825cda1b61d5.jpg differ diff --git a/docs/nlp/Images/58a1097522dc6fbe24eddd96cfd6cbc9.jpg b/docs/nlp/Images/58a1097522dc6fbe24eddd96cfd6cbc9.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6da97acc998e617da70cc538010d14663202657d Binary files /dev/null and b/docs/nlp/Images/58a1097522dc6fbe24eddd96cfd6cbc9.jpg differ diff --git a/docs/nlp/Images/5ac0acc015eeb4b0a40c6c23b7f95395.jpg b/docs/nlp/Images/5ac0acc015eeb4b0a40c6c23b7f95395.jpg new file mode 100644 index 0000000000000000000000000000000000000000..633add1215ddc96c097bc01237b3b84041f0e410 Binary files /dev/null and b/docs/nlp/Images/5ac0acc015eeb4b0a40c6c23b7f95395.jpg differ diff --git a/docs/nlp/Images/5c0c6dc75f29f253edaecb566d608aa3.jpg b/docs/nlp/Images/5c0c6dc75f29f253edaecb566d608aa3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fea57bdbbbba7851663c4a6f65ac81c2e0d9f50d Binary files /dev/null and b/docs/nlp/Images/5c0c6dc75f29f253edaecb566d608aa3.jpg differ diff --git a/docs/nlp/Images/5c3caa46daa29a053a04713bab6e4f03.jpg b/docs/nlp/Images/5c3caa46daa29a053a04713bab6e4f03.jpg new file mode 100644 index 0000000000000000000000000000000000000000..34b85ab58854417347960fd497f1f0cd59c122ec Binary files /dev/null and b/docs/nlp/Images/5c3caa46daa29a053a04713bab6e4f03.jpg differ diff --git a/docs/nlp/Images/5e197b7d253f66454a97af2a93c30a8e.jpg b/docs/nlp/Images/5e197b7d253f66454a97af2a93c30a8e.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a99158a355bce7409cbab517b0ab8ecb2ba1398c Binary files /dev/null and b/docs/nlp/Images/5e197b7d253f66454a97af2a93c30a8e.jpg differ diff --git a/docs/nlp/Images/5eeb4cf55b6d18d4bcb098fc72ddc6d7.jpg b/docs/nlp/Images/5eeb4cf55b6d18d4bcb098fc72ddc6d7.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a1e07013854ec291680adfc44bdc863dab99a4eb Binary files /dev/null and b/docs/nlp/Images/5eeb4cf55b6d18d4bcb098fc72ddc6d7.jpg differ diff --git a/docs/nlp/Images/6372ba4f28e69f0b220c75a9b2f4decf.jpg b/docs/nlp/Images/6372ba4f28e69f0b220c75a9b2f4decf.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ae840901dba5ad53b65b816ff7e35759bfad1466 Binary files /dev/null and b/docs/nlp/Images/6372ba4f28e69f0b220c75a9b2f4decf.jpg differ diff --git a/docs/nlp/Images/63a8e4c47e813ba9630363f9b203a19a.jpg b/docs/nlp/Images/63a8e4c47e813ba9630363f9b203a19a.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9b4c94ec8d3077612a4726467edfb9062266972a Binary files /dev/null and b/docs/nlp/Images/63a8e4c47e813ba9630363f9b203a19a.jpg differ diff --git a/docs/nlp/Images/64864d38550248d5bd9b82eeb6f0583b.jpg b/docs/nlp/Images/64864d38550248d5bd9b82eeb6f0583b.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e97f2b3ae7473145254e23fde2ce1a7dc7623482 Binary files /dev/null and b/docs/nlp/Images/64864d38550248d5bd9b82eeb6f0583b.jpg differ diff --git a/docs/nlp/Images/66d94cb86ab90a95cfe745d9613c37b1.jpg b/docs/nlp/Images/66d94cb86ab90a95cfe745d9613c37b1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..688b31cad556aaf0f06f32007ddcbe707528a59b Binary files /dev/null and b/docs/nlp/Images/66d94cb86ab90a95cfe745d9613c37b1.jpg differ diff --git a/docs/nlp/Images/67abca0731a79d664847dee1390f2e13.jpg b/docs/nlp/Images/67abca0731a79d664847dee1390f2e13.jpg new file mode 100644 index 0000000000000000000000000000000000000000..94d60438fb89332fb82bc9017e1e7fb968de5906 Binary files /dev/null and b/docs/nlp/Images/67abca0731a79d664847dee1390f2e13.jpg differ diff --git a/docs/nlp/Images/6ac827d2d00b6ebf8bbc704f430af896.jpg b/docs/nlp/Images/6ac827d2d00b6ebf8bbc704f430af896.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c9a369be517237d23d1eb51f33fac7a52d9bd1ce Binary files /dev/null and b/docs/nlp/Images/6ac827d2d00b6ebf8bbc704f430af896.jpg differ diff --git a/docs/nlp/Images/6ddd472200240ea6c0cab35349a8403e.jpg b/docs/nlp/Images/6ddd472200240ea6c0cab35349a8403e.jpg new file mode 100644 index 0000000000000000000000000000000000000000..02448f3f9c4b9c82d64f2d6b04a2bb553d98c1b6 Binary files /dev/null and b/docs/nlp/Images/6ddd472200240ea6c0cab35349a8403e.jpg differ diff --git a/docs/nlp/Images/6efeadf518b11a6441906b93844c2b19.jpg b/docs/nlp/Images/6efeadf518b11a6441906b93844c2b19.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c9a369be517237d23d1eb51f33fac7a52d9bd1ce Binary files /dev/null and b/docs/nlp/Images/6efeadf518b11a6441906b93844c2b19.jpg differ diff --git a/docs/nlp/Images/723ad3b660335fc3b79e7bd2c947b195.jpg b/docs/nlp/Images/723ad3b660335fc3b79e7bd2c947b195.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ae840901dba5ad53b65b816ff7e35759bfad1466 Binary files /dev/null and b/docs/nlp/Images/723ad3b660335fc3b79e7bd2c947b195.jpg differ diff --git a/docs/nlp/Images/74248e04835acdba414fd407bb4f3241.jpg b/docs/nlp/Images/74248e04835acdba414fd407bb4f3241.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b11a071c4b235bc54d3260630092621b62bf0af0 Binary files /dev/null and b/docs/nlp/Images/74248e04835acdba414fd407bb4f3241.jpg differ diff --git a/docs/nlp/Images/77460905bcad52d84e324fc4821ed903.jpg b/docs/nlp/Images/77460905bcad52d84e324fc4821ed903.jpg new file mode 100644 index 0000000000000000000000000000000000000000..82ca075121646a0ebede2d2a529dbbf7ed85bb05 Binary files /dev/null and b/docs/nlp/Images/77460905bcad52d84e324fc4821ed903.jpg differ diff --git a/docs/nlp/Images/78213332718eae7fffd6314dae12484e.jpg b/docs/nlp/Images/78213332718eae7fffd6314dae12484e.jpg new file mode 100644 index 0000000000000000000000000000000000000000..dbb05a4e581eb0e2b37267bcccc097108b781d10 Binary files /dev/null and b/docs/nlp/Images/78213332718eae7fffd6314dae12484e.jpg differ diff --git a/docs/nlp/Images/78bc6ca8410394dcf6910484bc97e2b6.jpg b/docs/nlp/Images/78bc6ca8410394dcf6910484bc97e2b6.jpg new file mode 100644 index 0000000000000000000000000000000000000000..82ca075121646a0ebede2d2a529dbbf7ed85bb05 Binary files /dev/null and b/docs/nlp/Images/78bc6ca8410394dcf6910484bc97e2b6.jpg differ diff --git a/docs/nlp/Images/7a979f968bd33428b02cde62eaf2b615.jpg b/docs/nlp/Images/7a979f968bd33428b02cde62eaf2b615.jpg new file mode 100644 index 0000000000000000000000000000000000000000..82ca075121646a0ebede2d2a529dbbf7ed85bb05 Binary files /dev/null and b/docs/nlp/Images/7a979f968bd33428b02cde62eaf2b615.jpg differ diff --git a/docs/nlp/Images/7aee076ae156921aba96ac5d4f9ed419.jpg b/docs/nlp/Images/7aee076ae156921aba96ac5d4f9ed419.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c17ef03144bc407f7c53667bd51c74e92be5ebbf Binary files /dev/null and b/docs/nlp/Images/7aee076ae156921aba96ac5d4f9ed419.jpg differ diff --git a/docs/nlp/Images/7bbd845f6f0cf6246561d2859cbcecbf.jpg b/docs/nlp/Images/7bbd845f6f0cf6246561d2859cbcecbf.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5250158b6414a4edb67149947126c7b641e1f2b9 Binary files /dev/null and b/docs/nlp/Images/7bbd845f6f0cf6246561d2859cbcecbf.jpg differ diff --git a/docs/nlp/Images/7c20d0adbadb35031a28bfcd6dff9900.jpg b/docs/nlp/Images/7c20d0adbadb35031a28bfcd6dff9900.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ae840901dba5ad53b65b816ff7e35759bfad1466 Binary files /dev/null and b/docs/nlp/Images/7c20d0adbadb35031a28bfcd6dff9900.jpg differ diff --git a/docs/nlp/Images/7e6ea96aad77f3e523494b3972b5a989.jpg b/docs/nlp/Images/7e6ea96aad77f3e523494b3972b5a989.jpg new file mode 100644 index 0000000000000000000000000000000000000000..82ca075121646a0ebede2d2a529dbbf7ed85bb05 Binary files /dev/null and b/docs/nlp/Images/7e6ea96aad77f3e523494b3972b5a989.jpg differ diff --git a/docs/nlp/Images/7f27bfe5324e4d9573ddd210531a8126.jpg b/docs/nlp/Images/7f27bfe5324e4d9573ddd210531a8126.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cd6a3d09689789766413a99fd2a5ecc008660524 Binary files /dev/null and b/docs/nlp/Images/7f27bfe5324e4d9573ddd210531a8126.jpg differ diff --git a/docs/nlp/Images/7f97e7ac70a7c865fb1020795f6e7236.jpg b/docs/nlp/Images/7f97e7ac70a7c865fb1020795f6e7236.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9f6c27ac4a6a518670d533227f05598a4e6f7dba Binary files /dev/null and b/docs/nlp/Images/7f97e7ac70a7c865fb1020795f6e7236.jpg differ diff --git a/docs/nlp/Images/81e6b07f4541d7d5a7900508d11172bd.jpg b/docs/nlp/Images/81e6b07f4541d7d5a7900508d11172bd.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f15270f1a71fb5c5ad0c44d15394d4aa64af9c3a Binary files /dev/null and b/docs/nlp/Images/81e6b07f4541d7d5a7900508d11172bd.jpg differ diff --git a/docs/nlp/Images/82ddd91230422f3ba446f46ca73ff663.jpg b/docs/nlp/Images/82ddd91230422f3ba446f46ca73ff663.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4d79f22b09dd06f82e14fb20bf0d5dc9a4749f19 Binary files /dev/null and b/docs/nlp/Images/82ddd91230422f3ba446f46ca73ff663.jpg differ diff --git a/docs/nlp/Images/84419adac7940aeb3026bc0a8b3e5fb4.jpg b/docs/nlp/Images/84419adac7940aeb3026bc0a8b3e5fb4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..68eec32c51b3f7c5e17ebd4310709cf589881e91 Binary files /dev/null and b/docs/nlp/Images/84419adac7940aeb3026bc0a8b3e5fb4.jpg differ diff --git a/docs/nlp/Images/854532b0c5c8869f9012833955e75b20.jpg b/docs/nlp/Images/854532b0c5c8869f9012833955e75b20.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c9a369be517237d23d1eb51f33fac7a52d9bd1ce Binary files /dev/null and b/docs/nlp/Images/854532b0c5c8869f9012833955e75b20.jpg differ diff --git a/docs/nlp/Images/86cecc4bd139ddaf1a5daf9991f39945.jpg b/docs/nlp/Images/86cecc4bd139ddaf1a5daf9991f39945.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a0d52aee89759b758e4c70c444375710a4136f2a Binary files /dev/null and b/docs/nlp/Images/86cecc4bd139ddaf1a5daf9991f39945.jpg differ diff --git a/docs/nlp/Images/89747cee31bc672bfbbb6891a9099a25.jpg b/docs/nlp/Images/89747cee31bc672bfbbb6891a9099a25.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e5a152eb4856e7f3c6c51d2da7a31bb677813c80 Binary files /dev/null and b/docs/nlp/Images/89747cee31bc672bfbbb6891a9099a25.jpg differ diff --git a/docs/nlp/Images/8b4bb6b0ec5bb337fdb00c31efcc1645.jpg b/docs/nlp/Images/8b4bb6b0ec5bb337fdb00c31efcc1645.jpg new file mode 100644 index 0000000000000000000000000000000000000000..08f8f6a8025e6aca43d326c81e08153d8c3cb208 Binary files /dev/null and b/docs/nlp/Images/8b4bb6b0ec5bb337fdb00c31efcc1645.jpg differ diff --git a/docs/nlp/Images/8bfa073b3f3753285055c1e3ef689826.jpg b/docs/nlp/Images/8bfa073b3f3753285055c1e3ef689826.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ac057833e3a1979ff82d4cf62b4ad41c2c453c7e Binary files /dev/null and b/docs/nlp/Images/8bfa073b3f3753285055c1e3ef689826.jpg differ diff --git a/docs/nlp/Images/8c5ec1a0132f7c85fd96eda9d9929d15.jpg b/docs/nlp/Images/8c5ec1a0132f7c85fd96eda9d9929d15.jpg new file mode 100644 index 0000000000000000000000000000000000000000..df19f86e656940bc212866d93dde9330b0c8b3f4 Binary files /dev/null and b/docs/nlp/Images/8c5ec1a0132f7c85fd96eda9d9929d15.jpg differ diff --git a/docs/nlp/Images/8cb61a943f3d34f94596e77065410cd3.jpg b/docs/nlp/Images/8cb61a943f3d34f94596e77065410cd3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c7fb3967314825938b18a7b91dece1802bf301e3 Binary files /dev/null and b/docs/nlp/Images/8cb61a943f3d34f94596e77065410cd3.jpg differ diff --git a/docs/nlp/Images/910abba27da60798441e98902cce64ca.jpg b/docs/nlp/Images/910abba27da60798441e98902cce64ca.jpg new file mode 100644 index 0000000000000000000000000000000000000000..796a791b91569755a417da63157dc8a6e9fcc068 Binary files /dev/null and b/docs/nlp/Images/910abba27da60798441e98902cce64ca.jpg differ diff --git a/docs/nlp/Images/92cc2e7821d464cfbaaf651a360cd413.jpg b/docs/nlp/Images/92cc2e7821d464cfbaaf651a360cd413.jpg new file mode 100644 index 0000000000000000000000000000000000000000..08f8f6a8025e6aca43d326c81e08153d8c3cb208 Binary files /dev/null and b/docs/nlp/Images/92cc2e7821d464cfbaaf651a360cd413.jpg differ diff --git a/docs/nlp/Images/934b688727805b37f2404f7497c52027.jpg b/docs/nlp/Images/934b688727805b37f2404f7497c52027.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ae840901dba5ad53b65b816ff7e35759bfad1466 Binary files /dev/null and b/docs/nlp/Images/934b688727805b37f2404f7497c52027.jpg differ diff --git a/docs/nlp/Images/953f4a408c97594449de5ca84c294719.jpg b/docs/nlp/Images/953f4a408c97594449de5ca84c294719.jpg new file mode 100644 index 0000000000000000000000000000000000000000..83800c90b5019d9808287434e2a434eda4bf8a02 Binary files /dev/null and b/docs/nlp/Images/953f4a408c97594449de5ca84c294719.jpg differ diff --git a/docs/nlp/Images/96fd8d34602a08c09a19f5b2c5c19380.jpg b/docs/nlp/Images/96fd8d34602a08c09a19f5b2c5c19380.jpg new file mode 100644 index 0000000000000000000000000000000000000000..631db4804c09ec15b683d55de34795bb979364c6 Binary files /dev/null and b/docs/nlp/Images/96fd8d34602a08c09a19f5b2c5c19380.jpg differ diff --git a/docs/nlp/Images/99dbcae75bd96499dce3b3671032a106.jpg b/docs/nlp/Images/99dbcae75bd96499dce3b3671032a106.jpg new file mode 100644 index 0000000000000000000000000000000000000000..216d33849bbc17b5899eabbc7c1ae0cd460217b4 Binary files /dev/null and b/docs/nlp/Images/99dbcae75bd96499dce3b3671032a106.jpg differ diff --git a/docs/nlp/Images/9ea1d0111a40bd865c651712f276bc31.jpg b/docs/nlp/Images/9ea1d0111a40bd865c651712f276bc31.jpg new file mode 100644 index 0000000000000000000000000000000000000000..be80bdf419fd6b780c2184bab3355a9c54a017d5 Binary files /dev/null and b/docs/nlp/Images/9ea1d0111a40bd865c651712f276bc31.jpg differ diff --git a/docs/nlp/Images/a538227535079e1fa1e906af90af28eb.jpg b/docs/nlp/Images/a538227535079e1fa1e906af90af28eb.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0f2f641a1e52dfa92fbcb1b6b2a7d2a3824d73ef Binary files /dev/null and b/docs/nlp/Images/a538227535079e1fa1e906af90af28eb.jpg differ diff --git a/docs/nlp/Images/aa68e0e8f4d58caa31e5542dabe4ddc2.jpg b/docs/nlp/Images/aa68e0e8f4d58caa31e5542dabe4ddc2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c9a369be517237d23d1eb51f33fac7a52d9bd1ce Binary files /dev/null and b/docs/nlp/Images/aa68e0e8f4d58caa31e5542dabe4ddc2.jpg differ diff --git a/docs/nlp/Images/ab3d4c917ad3461f18759719a288afa5.jpg b/docs/nlp/Images/ab3d4c917ad3461f18759719a288afa5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..82ca075121646a0ebede2d2a529dbbf7ed85bb05 Binary files /dev/null and b/docs/nlp/Images/ab3d4c917ad3461f18759719a288afa5.jpg differ diff --git a/docs/nlp/Images/b1aad2b60635723f14976fb5cb9ca372.jpg b/docs/nlp/Images/b1aad2b60635723f14976fb5cb9ca372.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7fa4c4ac6feef7f0f5b22d6abf21a84a1bbc432b Binary files /dev/null and b/docs/nlp/Images/b1aad2b60635723f14976fb5cb9ca372.jpg differ diff --git a/docs/nlp/Images/b2af1426c6cd2403c8b938eb557a99d1.jpg b/docs/nlp/Images/b2af1426c6cd2403c8b938eb557a99d1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f915e51e5da0f11bac5179d33797257e31dd9356 Binary files /dev/null and b/docs/nlp/Images/b2af1426c6cd2403c8b938eb557a99d1.jpg differ diff --git a/docs/nlp/Images/b502c97e1f935240559d38b397805b32.jpg b/docs/nlp/Images/b502c97e1f935240559d38b397805b32.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0c4013aecb88607ed5c30a6530309f41c6eb8df4 Binary files /dev/null and b/docs/nlp/Images/b502c97e1f935240559d38b397805b32.jpg differ diff --git a/docs/nlp/Images/bc7e6aa002dd5ced295ba1de2ebbbf61.jpg b/docs/nlp/Images/bc7e6aa002dd5ced295ba1de2ebbbf61.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6cbd1b203a86d4ef3b16a2c29306d746f2992397 Binary files /dev/null and b/docs/nlp/Images/bc7e6aa002dd5ced295ba1de2ebbbf61.jpg differ diff --git a/docs/nlp/Images/bcf758e8278f3295df58c6eace05152c.jpg b/docs/nlp/Images/bcf758e8278f3295df58c6eace05152c.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9b4c94ec8d3077612a4726467edfb9062266972a Binary files /dev/null and b/docs/nlp/Images/bcf758e8278f3295df58c6eace05152c.jpg differ diff --git a/docs/nlp/Images/be33958d0b44c88caac0dcf4d4ec84c6.jpg b/docs/nlp/Images/be33958d0b44c88caac0dcf4d4ec84c6.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c9a369be517237d23d1eb51f33fac7a52d9bd1ce Binary files /dev/null and b/docs/nlp/Images/be33958d0b44c88caac0dcf4d4ec84c6.jpg differ diff --git a/docs/nlp/Images/bf2a88150e1927f2e52d745ab6cae7ce.jpg b/docs/nlp/Images/bf2a88150e1927f2e52d745ab6cae7ce.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b9fc4a2618a4586d017cf90bed99cb6e476a186e Binary files /dev/null and b/docs/nlp/Images/bf2a88150e1927f2e52d745ab6cae7ce.jpg differ diff --git a/docs/nlp/Images/bfd8f5fc51eb8848dfb986c7937ded23.jpg b/docs/nlp/Images/bfd8f5fc51eb8848dfb986c7937ded23.jpg new file mode 100644 index 0000000000000000000000000000000000000000..94dfc9c5dfb9a33baefe911aa63016383ea0bb35 Binary files /dev/null and b/docs/nlp/Images/bfd8f5fc51eb8848dfb986c7937ded23.jpg differ diff --git a/docs/nlp/Images/c28c02e5c661caa578acaea0fa9e9dd2.jpg b/docs/nlp/Images/c28c02e5c661caa578acaea0fa9e9dd2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b3d94755725f2536325f2b6a826cdc21cf967e43 Binary files /dev/null and b/docs/nlp/Images/c28c02e5c661caa578acaea0fa9e9dd2.jpg differ diff --git a/docs/nlp/Images/c370689374c63baf915c9e44c4b270d4.jpg b/docs/nlp/Images/c370689374c63baf915c9e44c4b270d4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8ae4ceb258d4ecfa2e6ac345144ac42e47f52530 Binary files /dev/null and b/docs/nlp/Images/c370689374c63baf915c9e44c4b270d4.jpg differ diff --git a/docs/nlp/Images/ca21bcde8ab16a341929b7fb9ccb0a0e.jpg b/docs/nlp/Images/ca21bcde8ab16a341929b7fb9ccb0a0e.jpg new file mode 100644 index 0000000000000000000000000000000000000000..08f8f6a8025e6aca43d326c81e08153d8c3cb208 Binary files /dev/null and b/docs/nlp/Images/ca21bcde8ab16a341929b7fb9ccb0a0e.jpg differ diff --git a/docs/nlp/Images/ced4e829d6a662a2be20187f9d7b71b5.jpg b/docs/nlp/Images/ced4e829d6a662a2be20187f9d7b71b5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..79cb579508daae2e72a9808ae3c510b1a95d09f8 Binary files /dev/null and b/docs/nlp/Images/ced4e829d6a662a2be20187f9d7b71b5.jpg differ diff --git a/docs/nlp/Images/cf5ffc116dbddb4a34c65925b0d558cb.jpg b/docs/nlp/Images/cf5ffc116dbddb4a34c65925b0d558cb.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0024342305469e0bc284cdee0252b6ce46e8a90d Binary files /dev/null and b/docs/nlp/Images/cf5ffc116dbddb4a34c65925b0d558cb.jpg differ diff --git a/docs/nlp/Images/d167c4075a237573a350e298a184d4fb.jpg b/docs/nlp/Images/d167c4075a237573a350e298a184d4fb.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f5d31a0c9ed6298df94fe75fe7b0d361cbb8d466 Binary files /dev/null and b/docs/nlp/Images/d167c4075a237573a350e298a184d4fb.jpg differ diff --git a/docs/nlp/Images/d2c5cc51cd61a6915f24d0c52eaab9c5.jpg b/docs/nlp/Images/d2c5cc51cd61a6915f24d0c52eaab9c5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b71871d17f30b3118ae99380794335735bfe2047 Binary files /dev/null and b/docs/nlp/Images/d2c5cc51cd61a6915f24d0c52eaab9c5.jpg differ diff --git a/docs/nlp/Images/d315c52900de61a078cd8391c1a1c604.jpg b/docs/nlp/Images/d315c52900de61a078cd8391c1a1c604.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4a5bb8b8a42f3e12f90e416032e3a19e7adc0b32 Binary files /dev/null and b/docs/nlp/Images/d315c52900de61a078cd8391c1a1c604.jpg differ diff --git a/docs/nlp/Images/d3541d4caaf5c92d8ab496ccd7ee9a2d.jpg b/docs/nlp/Images/d3541d4caaf5c92d8ab496ccd7ee9a2d.jpg new file mode 100644 index 0000000000000000000000000000000000000000..92151e7897783fe37f7730f4de003b7fb814c8af Binary files /dev/null and b/docs/nlp/Images/d3541d4caaf5c92d8ab496ccd7ee9a2d.jpg differ diff --git a/docs/nlp/Images/d41ca3c99b067df414f6ed0847a641ca.jpg b/docs/nlp/Images/d41ca3c99b067df414f6ed0847a641ca.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8bd63e2cf61a3bf2df2b5afb310532d219a8157d Binary files /dev/null and b/docs/nlp/Images/d41ca3c99b067df414f6ed0847a641ca.jpg differ diff --git a/docs/nlp/Images/d46d7efa5d57b31c68c4ddddd464d6c0.jpg b/docs/nlp/Images/d46d7efa5d57b31c68c4ddddd464d6c0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..11da8b6c5c3faad2932d8b1ad043e3cadd7e93bd Binary files /dev/null and b/docs/nlp/Images/d46d7efa5d57b31c68c4ddddd464d6c0.jpg differ diff --git a/docs/nlp/Images/d87676460c87d0516fb382b929c07302.jpg b/docs/nlp/Images/d87676460c87d0516fb382b929c07302.jpg new file mode 100644 index 0000000000000000000000000000000000000000..baab72af3de0742f471f5a908e2259dec45da44b Binary files /dev/null and b/docs/nlp/Images/d87676460c87d0516fb382b929c07302.jpg differ diff --git a/docs/nlp/Images/da1752497a2a17be12b2acb282918a7a.jpg b/docs/nlp/Images/da1752497a2a17be12b2acb282918a7a.jpg new file mode 100644 index 0000000000000000000000000000000000000000..228e2ceeea26cbc65ab525930985bd5a16c22d98 Binary files /dev/null and b/docs/nlp/Images/da1752497a2a17be12b2acb282918a7a.jpg differ diff --git a/docs/nlp/Images/da516572f97daebe1be746abd7bd2268.jpg b/docs/nlp/Images/da516572f97daebe1be746abd7bd2268.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1533c0bf2d96a85969a3454cb2b4cbc7ad552341 Binary files /dev/null and b/docs/nlp/Images/da516572f97daebe1be746abd7bd2268.jpg differ diff --git a/docs/nlp/Images/daf7c1761ffc1edbb39e6ca11863854f.jpg b/docs/nlp/Images/daf7c1761ffc1edbb39e6ca11863854f.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b242a70268174d09fe452e6b940b4f7e1cd193f5 Binary files /dev/null and b/docs/nlp/Images/daf7c1761ffc1edbb39e6ca11863854f.jpg differ diff --git a/docs/nlp/Images/de0715649664a49a5ab2e2b61ae2675a.jpg b/docs/nlp/Images/de0715649664a49a5ab2e2b61ae2675a.jpg new file mode 100644 index 0000000000000000000000000000000000000000..31f7cc55f07f906c0a21dba92730cba8e2344e27 Binary files /dev/null and b/docs/nlp/Images/de0715649664a49a5ab2e2b61ae2675a.jpg differ diff --git a/docs/nlp/Images/df98e5a4903ccc7253cc7cc6ebda4fea.jpg b/docs/nlp/Images/df98e5a4903ccc7253cc7cc6ebda4fea.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1a227e21553c32cce1de5bbbd9e80f4f223a384a Binary files /dev/null and b/docs/nlp/Images/df98e5a4903ccc7253cc7cc6ebda4fea.jpg differ diff --git a/docs/nlp/Images/e04c36ac970436161b45be660ea3a7d2.jpg b/docs/nlp/Images/e04c36ac970436161b45be660ea3a7d2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..dc3dd9c80d110f36d924b118ee2f419dbd5e63f6 Binary files /dev/null and b/docs/nlp/Images/e04c36ac970436161b45be660ea3a7d2.jpg differ diff --git a/docs/nlp/Images/e104303155c3fbb0742ea67a0560ab7f.jpg b/docs/nlp/Images/e104303155c3fbb0742ea67a0560ab7f.jpg new file mode 100644 index 0000000000000000000000000000000000000000..838f181740bf8d70a01b06de80ffa9745a1f77ea Binary files /dev/null and b/docs/nlp/Images/e104303155c3fbb0742ea67a0560ab7f.jpg differ diff --git a/docs/nlp/Images/e112e308d5c3454875146f40e4b48f3f.jpg b/docs/nlp/Images/e112e308d5c3454875146f40e4b48f3f.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e17ae0496f79a074e8dddc4f2c3c8b56ead72455 Binary files /dev/null and b/docs/nlp/Images/e112e308d5c3454875146f40e4b48f3f.jpg differ diff --git a/docs/nlp/Images/e24d03f1dec3f8a75e8042579446a47e.jpg b/docs/nlp/Images/e24d03f1dec3f8a75e8042579446a47e.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3e7aad47be36c23fcfc1b8a7deb5dcd4c69d8905 Binary files /dev/null and b/docs/nlp/Images/e24d03f1dec3f8a75e8042579446a47e.jpg differ diff --git a/docs/nlp/Images/e33fb540f11c5ea9a07441be8a407d43.jpg b/docs/nlp/Images/e33fb540f11c5ea9a07441be8a407d43.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7096848042a35e77f055d13c4c677d44601afe94 Binary files /dev/null and b/docs/nlp/Images/e33fb540f11c5ea9a07441be8a407d43.jpg differ diff --git a/docs/nlp/Images/e372da3dc801bc1211a47d2e82840b64.jpg b/docs/nlp/Images/e372da3dc801bc1211a47d2e82840b64.jpg new file mode 100644 index 0000000000000000000000000000000000000000..84738439acbca9e2fe7d3a079cad78722a690d7d Binary files /dev/null and b/docs/nlp/Images/e372da3dc801bc1211a47d2e82840b64.jpg differ diff --git a/docs/nlp/Images/e591e60c490795add5183c998132ebc0.jpg b/docs/nlp/Images/e591e60c490795add5183c998132ebc0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3db5c9a525e9b6521b8e2339dfd3a98ea4e2114e Binary files /dev/null and b/docs/nlp/Images/e591e60c490795add5183c998132ebc0.jpg differ diff --git a/docs/nlp/Images/e5fb07e997b9718f18dbf677e3d6634d.jpg b/docs/nlp/Images/e5fb07e997b9718f18dbf677e3d6634d.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c9a369be517237d23d1eb51f33fac7a52d9bd1ce Binary files /dev/null and b/docs/nlp/Images/e5fb07e997b9718f18dbf677e3d6634d.jpg differ diff --git a/docs/nlp/Images/e685801a8cec4515b47e1bda95deb59d.jpg b/docs/nlp/Images/e685801a8cec4515b47e1bda95deb59d.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0102b219a2754f8f01e596ecac63bbde6ba755b6 Binary files /dev/null and b/docs/nlp/Images/e685801a8cec4515b47e1bda95deb59d.jpg differ diff --git a/docs/nlp/Images/e941b64ed778967dd0170d25492e42df.jpg b/docs/nlp/Images/e941b64ed778967dd0170d25492e42df.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ae840901dba5ad53b65b816ff7e35759bfad1466 Binary files /dev/null and b/docs/nlp/Images/e941b64ed778967dd0170d25492e42df.jpg differ diff --git a/docs/nlp/Images/e9d9a0887996a6bac6c52bb0bfaf9fdf.jpg b/docs/nlp/Images/e9d9a0887996a6bac6c52bb0bfaf9fdf.jpg new file mode 100644 index 0000000000000000000000000000000000000000..dc5ea15e2805951cf836910d1a398112339d9cb1 Binary files /dev/null and b/docs/nlp/Images/e9d9a0887996a6bac6c52bb0bfaf9fdf.jpg differ diff --git a/docs/nlp/Images/ea84debad296c6385399fb2252fc93f1.jpg b/docs/nlp/Images/ea84debad296c6385399fb2252fc93f1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5e741069438fd2d7228af7be22b88e2dfb141565 Binary files /dev/null and b/docs/nlp/Images/ea84debad296c6385399fb2252fc93f1.jpg differ diff --git a/docs/nlp/Images/eb630c6034e9ed7274ef2e04b9694347.jpg b/docs/nlp/Images/eb630c6034e9ed7274ef2e04b9694347.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5254f509ac2cf011bc77429752ccffcf6aedd14f Binary files /dev/null and b/docs/nlp/Images/eb630c6034e9ed7274ef2e04b9694347.jpg differ diff --git a/docs/nlp/Images/edd1b5af9dcd26541183ce4d6a634e54.jpg b/docs/nlp/Images/edd1b5af9dcd26541183ce4d6a634e54.jpg new file mode 100644 index 0000000000000000000000000000000000000000..08571186058f988796a85c727bcd2c5225b04d71 Binary files /dev/null and b/docs/nlp/Images/edd1b5af9dcd26541183ce4d6a634e54.jpg differ diff --git a/docs/nlp/Images/eeff7ed83be48bf40aeeb3bf9db5550e.jpg b/docs/nlp/Images/eeff7ed83be48bf40aeeb3bf9db5550e.jpg new file mode 100644 index 0000000000000000000000000000000000000000..82ca075121646a0ebede2d2a529dbbf7ed85bb05 Binary files /dev/null and b/docs/nlp/Images/eeff7ed83be48bf40aeeb3bf9db5550e.jpg differ diff --git a/docs/nlp/Images/ef661b5a01845fe5440027afca461925.jpg b/docs/nlp/Images/ef661b5a01845fe5440027afca461925.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9cefd6ee20f110580b201f7e22ca51ed849b10ca Binary files /dev/null and b/docs/nlp/Images/ef661b5a01845fe5440027afca461925.jpg differ diff --git a/docs/nlp/Images/f093aaace735b4961dbf9fa7d5c8ca37.jpg b/docs/nlp/Images/f093aaace735b4961dbf9fa7d5c8ca37.jpg new file mode 100644 index 0000000000000000000000000000000000000000..307a06a220ba8045bb4e87eae2c709f7b456fcad Binary files /dev/null and b/docs/nlp/Images/f093aaace735b4961dbf9fa7d5c8ca37.jpg differ diff --git a/docs/nlp/Images/f202bb6a4c773430e3d1340de573d0e5.jpg b/docs/nlp/Images/f202bb6a4c773430e3d1340de573d0e5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ae840901dba5ad53b65b816ff7e35759bfad1466 Binary files /dev/null and b/docs/nlp/Images/f202bb6a4c773430e3d1340de573d0e5.jpg differ diff --git a/docs/nlp/Images/f3ad266a67457b4615141d6ba83e724e.jpg b/docs/nlp/Images/f3ad266a67457b4615141d6ba83e724e.jpg new file mode 100644 index 0000000000000000000000000000000000000000..08f8f6a8025e6aca43d326c81e08153d8c3cb208 Binary files /dev/null and b/docs/nlp/Images/f3ad266a67457b4615141d6ba83e724e.jpg differ diff --git a/docs/nlp/Images/f4891d12ae20c39b685951ad3cddf1aa.jpg b/docs/nlp/Images/f4891d12ae20c39b685951ad3cddf1aa.jpg new file mode 100644 index 0000000000000000000000000000000000000000..82ca075121646a0ebede2d2a529dbbf7ed85bb05 Binary files /dev/null and b/docs/nlp/Images/f4891d12ae20c39b685951ad3cddf1aa.jpg differ diff --git a/docs/nlp/Images/f9e1ba3246770e3ecb24f813f33f2075.jpg b/docs/nlp/Images/f9e1ba3246770e3ecb24f813f33f2075.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c9a369be517237d23d1eb51f33fac7a52d9bd1ce Binary files /dev/null and b/docs/nlp/Images/f9e1ba3246770e3ecb24f813f33f2075.jpg differ diff --git a/docs/nlp/Images/fb1a02fe3607a0deb452086296fd6f69.jpg b/docs/nlp/Images/fb1a02fe3607a0deb452086296fd6f69.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7ed3e176dfc08c8cb13c7800bf0288d55e390eef Binary files /dev/null and b/docs/nlp/Images/fb1a02fe3607a0deb452086296fd6f69.jpg differ diff --git a/docs/nlp/Images/ff868af58b8c1843c38287717b137f7c.jpg b/docs/nlp/Images/ff868af58b8c1843c38287717b137f7c.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e78edb2af3f1f1656515b691db7a59dba7d8c7b2 Binary files /dev/null and b/docs/nlp/Images/ff868af58b8c1843c38287717b137f7c.jpg differ diff --git a/docs/nlp/Images/ffa808c97c7034af1bc2806ed7224203.jpg b/docs/nlp/Images/ffa808c97c7034af1bc2806ed7224203.jpg new file mode 100644 index 0000000000000000000000000000000000000000..82ca075121646a0ebede2d2a529dbbf7ed85bb05 Binary files /dev/null and b/docs/nlp/Images/ffa808c97c7034af1bc2806ed7224203.jpg differ diff --git a/docs/nlp/Images/fff90c564d2625f739b442b23301906e.jpg b/docs/nlp/Images/fff90c564d2625f739b442b23301906e.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1a64c58a35b89631ad04c7a534ff47c310c9aefa Binary files /dev/null and b/docs/nlp/Images/fff90c564d2625f739b442b23301906e.jpg differ diff --git a/docs/nlp/README.md b/docs/nlp/README.md index b595d19264b92d1e9ef4404a81b9272da95869a3..d329b5a9d2df6d273e81faa58374f7c627daa9ca 100644 --- a/docs/nlp/README.md +++ b/docs/nlp/README.md @@ -1,109 +1,48 @@ -# 【入门须知】必须了解 +# Python 自然语言处理 第二版 -实体: 抽取 -关系: 图谱 -意图: 分类 +## – 使用自然语言工具包分析文本 -* **【入门须知】必须了解**: -* **【入门教程】强烈推荐: PyTorch 自然语言处理**: -* Python 自然语言处理 第二版: -* 推荐一个[liuhuanyong大佬](https://github.com/liuhuanyong)整理的nlp全面知识体系: +> 作者:**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。 -## nlp 学习书籍和工具: ++ [在线阅读](https://nltk.apachecn.org) ++ [在线阅读(Gitee)](https://apachecn.gitee.io/nlp-py-2e-zh/) -* 百度搜索: Python自然语言处理 -* 读书笔记: -* Python自然语言处理工具汇总: +本版本的 NLTK 已经针对 Python 3 和 NLTK 3 更新。本书的第一版由 O'Reilly 出版,可以在 [http://nltk.org/book_1ed/](http://nltk.org/book_1ed/) 访问到。(本书目前没有计划出第二版) -## nlp 全局介绍视频: (简单做了解就行) +本版本的初译基于[原书第一版的翻译](http://www.52nlp.cn/resources),参见第一版[译者的话](./15.html)。 -地址链接: http://bit.baidu.com/Course/detail/id/56.html - -1. 自然语言处理知识入门 -2. 百度机器翻译 -3. 篇章分析 -4. UNIT: 语言理解与交互技术 +本书的发行遵守 [Creative Commons Attribution Noncommercial No-Derivative-Works 3.0 US License](http://creativecommons.org/licenses/by-nc-nd/3.0/us/) 条款。 -## 中文 NLP +如果对材料有任何疑问请提交至 [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} 查看文档 +``` -1. [反向传递](/docs/dl/反向传递.md): https://www.cnblogs.com/charlotte77/p/5629865.html -2. [CNN原理](/docs/dl/CNN原理.md): http://www.cnblogs.com/charlotte77/p/7759802.html -3. [RNN原理](/docs/dl/RNN原理.md): https://blog.csdn.net/qq_39422642/article/details/78676567 -4. [LSTM原理](/docs/dl/LSTM原理.md): https://blog.csdn.net/weixin_42111770/article/details/80900575 +### PYPI -> [Word2Vec 原理](/docs/nlp/Word2Vec.md): +``` +pip install nlp-py-2e-zh +nlp-py-2e-zh +# 访问 http://localhost:{port} 查看文档 +``` -1. 负采样 +### NPM -介绍: - 自然语言处理领域中,判断两个单词是不是一对上下文词(context)与目标词(target),如果是一对,则是正样本,如果不是一对,则是负样本。 - 采样得到一个上下文词和一个目标词,生成一个正样本(positive example),生成一个负样本(negative example),则是用与正样本相同的上下文词,再在字典中随机选择一个单词,这就是负采样(negative sampling)。 - -案例: - 比如给定一句话“这是去上学的班车”,则对这句话进行正采样,得到上下文“上”和目标词“学”,则这两个字就是正样本。 - 负样本的采样需要选定同样的“上”,然后在训练的字典中任意取另一个字,如“我”、“梦”、“目”,这一对就构成负样本。 - 训练需要正样本和负样本同时存在。 - -优势: - 负采样的本质: 每次让一个训练样本只更新部分权重,其他权重全部固定;减少计算量;(一定程度上还可以增加随机性) - -## nlp 操作流程 - -[本项目](https://pytorch.apachecn.org/docs/1.0/#/char_rnn_classification_tutorial) 试图通过名字分类问题给大家描述一个基础的深度学习中自然语言处理模型,同时也向大家展示了Pytorch的基本玩法。 其实对于大部分基础的NLP工作,都是类似的套路: - -1. 收集数据 -2. 清洗数据 -3. 为数据建立字母表或词表(vocabulary或者叫look-up table) -4. 根据字母表或者词表把数据向量化 -5. 搭建神经网络,深度学习中一般以LSTM或者GRU为主,按照需求结合各种其他的工具,包括embedding,注意力机制,双向RNN等等常见算法。 -6. 输入数据,按需求得到输出,比如分类模型根据类别数来得到输出,生成模型根据指定的长度或者结束标志符来得到输出等等。 -7. 把输出的结果进行处理,得到最终想要的数据。常需要把向量化的结果根据字母表或者词表变回文本数据。 -8. 评估模型。 - -如果真的想要对自然语言处理或者序列模型有更加全面的了解,建议大家去网易云课堂看一看吴恩达深度学习微专业中的序列模型这一板块,可以说是讲的非常清楚了。 此外极力推荐两个blog: - -1. 讲述RNN循环神经网络在深度学习中的各种应用场景。http://karpathy.github.io/2015/05/21/rnn-effectiveness/ -2. 讲述LSTM的来龙去脉。http://colah.github.io/posts/2015-08-Understanding-LSTMs/ - -最后,本文参考整合了: - -* Pytorch中文文档: https://pytorch.apachecn.org -* Pytorch官方文档: http://pytorch.org/tutorials/intermediate/char_rnn_classification_tutorial.html -* Ngarneau小哥的博文: https://github.com/ngarneau/understanding-pytorch-batching-lstm -* 另外,本项目搭配Sung Kim的Pytorch Zero To All的第13讲rnn_classification会更加方便食用喔,视频可以在油管和b站中找到。 - -## nlp - 比赛链接 - -* https://competitions.codalab.org/competitions/12731 -* https://sites.ualberta.ca/%7Emiyoung2/COLIEE2018/ -* https://visualdialog.org/challenge/2018 -+ 人机对话 NLP - - http://jddc.jd.com -+ 司法数据文本的 NLP - - http://cail.cipsc.org.cn -+ “达观杯” 文本智能处理挑战赛 - - http://www.dcjingsai.com/common/cmpt/“达观杯”文本智能处理挑战赛_竞赛信息.html -+ 中文论文摘要数据 - - https://biendata.com/competition/smpetst2018 -+ 中文问答任务 - - https://biendata.com/competition/CCKS2018_4/ -+ 第二届讯飞杯中文机器阅读理解评测 - - http://www.hfl-tek.com/cmrc2018 -+ 2018机器阅读理解技术竞赛 这也是结束了的 NLP - - http://mrc2018.cipsc.org.cn -+ 句子文本相似度计算 - - https://www.kaggle.com/c/quora-question-pairs - - -* * * - -【比赛收集平台】: https://github.com/iphysresearch/DataSciComp +``` +npm install -g nlp-py-2e-zh +nlp-py-2e-zh +# 访问 http://localhost:{port} 查看文档 +``` diff --git a/docs/nlp/SUMMARY.md b/docs/nlp/SUMMARY.md new file mode 100644 index 0000000000000000000000000000000000000000..01b63cdd3515a6e28d1cbc72ea3884392055a8e1 --- /dev/null +++ b/docs/nlp/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) diff --git a/docs/nlp/Word2Vec.md b/docs/nlp/Word2Vec.md deleted file mode 100644 index 042acb92c34ec702136ec68ea03e055770e65ffc..0000000000000000000000000000000000000000 --- a/docs/nlp/Word2Vec.md +++ /dev/null @@ -1,48 +0,0 @@ -# Word2Vec 讲解 - -## 介绍 - -**需要复习** 手写 Word2Vec 源码: https://blog.csdn.net/u014595019/article/details/51943428 - -* 2013年,Google开源了一款用于词向量计算的工具—— `word2vec`,引起了工业界和学术界的关注。 -* `word2vec` 算法或模型的时候,其实指的是其背后用于计算 **word vector** 的 `CBoW` 模型和 `Skip-gram` 模型 -* 很多人以为 `word2vec` 指的是一个算法或模型,这也是一种谬误。 -* 因此通过 Word2Vec 技术 输出的词向量可以被用来做很多NLP相关的工作,比如聚类、找同义词、词性分析等等. - -> 适用场景 - -1. cbow适用于小规模,或者主题比较散的语料,毕竟他的向量产生只跟临近的字有关系,更远的语料并没有被采用。 -2. 而相反的skip-gram可以处理基于相同语义,义群的一大批语料。 - -## CBoW 模型(Continuous Bag-of-Words Model) - -* 连续词袋模型(CBOW)常用于NLP深度学习。 -* 这是一种模式,它试图根据目标词 `之前` 和 `之后` 几个单词的背景来预测单词(CBOW不是顺序)。 -* CBOW 模型: 能够根据输入周围n-1个词来预测出这个词本身. - * 也就是说,CBOW模型的输入是某个词A周围的n个单词的词向量之和,输出是词A本身的词向量. - -![CBoW 模型/img/NLP/Word2Vce/CBoW.png) - -## Skip-gram 模型 - -* skip-gram与CBOW相比,只有细微的不同。skip-gram的输入是当前词的词向量,而输出是周围词的词向量。 -* Skip-gram 模型: 能够根据词本身来预测周围有哪些词. - * 也就是说,Skip-gram模型的输入是词A本身,输出是词A周围的n个单词的词向量. - -![Skip-gram 模型/img/NLP/Word2Vce/Skip-gram.png) - - -明天看看这个案例: https://blog.csdn.net/lyb3b3b/article/details/72897952 - - -## 补充: NPLM - Ngram 模型 - -* n-gram 模型是一种近似策略,作了一个马尔可夫假设: 认为目标词的条件概率只与其之前的 n 个词有关 -* NPLM基于 n-gram, 相当于目标词只有上文。 - - -* * * - -参考资料: - -1. https://www.cnblogs.com/iloveai/p/word2vec.html diff --git "a/docs/nlp/img/1.\350\207\252\347\204\266\350\257\255\350\250\200\345\244\204\347\220\206\345\205\245\351\227\250\344\273\213\347\273\215/\346\234\272\345\231\250\347\277\273\350\257\221.png" "b/docs/nlp/img/1.\350\207\252\347\204\266\350\257\255\350\250\200\345\244\204\347\220\206\345\205\245\351\227\250\344\273\213\347\273\215/\346\234\272\345\231\250\347\277\273\350\257\221.png" deleted file mode 100644 index 63bac712181851f7df43a253e85bc936eaf3adce..0000000000000000000000000000000000000000 Binary files "a/docs/nlp/img/1.\350\207\252\347\204\266\350\257\255\350\250\200\345\244\204\347\220\206\345\205\245\351\227\250\344\273\213\347\273\215/\346\234\272\345\231\250\347\277\273\350\257\221.png" and /dev/null differ diff --git "a/docs/nlp/img/1.\350\207\252\347\204\266\350\257\255\350\250\200\345\244\204\347\220\206\345\205\245\351\227\250\344\273\213\347\273\215/\347\257\207\347\253\240\345\210\206\346\236\220.jpg" "b/docs/nlp/img/1.\350\207\252\347\204\266\350\257\255\350\250\200\345\244\204\347\220\206\345\205\245\351\227\250\344\273\213\347\273\215/\347\257\207\347\253\240\345\210\206\346\236\220.jpg" deleted file mode 100644 index f49293de88cf53f7c53adb3bbcb747cdba616a25..0000000000000000000000000000000000000000 Binary files "a/docs/nlp/img/1.\350\207\252\347\204\266\350\257\255\350\250\200\345\244\204\347\220\206\345\205\245\351\227\250\344\273\213\347\273\215/\347\257\207\347\253\240\345\210\206\346\236\220.jpg" and /dev/null differ diff --git "a/docs/nlp/img/3.1.\347\257\207\347\253\240\345\210\206\346\236\220-\345\206\205\345\256\271\346\240\207\347\255\276/\347\257\207\347\253\240\345\210\206\346\236\220\344\273\273\345\212\241.jpg" "b/docs/nlp/img/3.1.\347\257\207\347\253\240\345\210\206\346\236\220-\345\206\205\345\256\271\346\240\207\347\255\276/\347\257\207\347\253\240\345\210\206\346\236\220\344\273\273\345\212\241.jpg" deleted file mode 100644 index f49293de88cf53f7c53adb3bbcb747cdba616a25..0000000000000000000000000000000000000000 Binary files "a/docs/nlp/img/3.1.\347\257\207\347\253\240\345\210\206\346\236\220-\345\206\205\345\256\271\346\240\207\347\255\276/\347\257\207\347\253\240\345\210\206\346\236\220\344\273\273\345\212\241.jpg" and /dev/null differ diff --git "a/docs/nlp/img/3.2.\347\257\207\347\253\240\345\210\206\346\236\220-\345\206\205\345\256\271\346\240\207\347\255\276/\344\270\273\351\242\230\345\210\206\347\261\273.jpg" "b/docs/nlp/img/3.2.\347\257\207\347\253\240\345\210\206\346\236\220-\345\206\205\345\256\271\346\240\207\347\255\276/\344\270\273\351\242\230\345\210\206\347\261\273.jpg" deleted file mode 100644 index fa4919a74b1db9d11eff618a59c83f881fa0c5a0..0000000000000000000000000000000000000000 Binary files "a/docs/nlp/img/3.2.\347\257\207\347\253\240\345\210\206\346\236\220-\345\206\205\345\256\271\346\240\207\347\255\276/\344\270\273\351\242\230\345\210\206\347\261\273.jpg" and /dev/null differ diff --git "a/docs/nlp/img/3.2.\347\257\207\347\253\240\345\210\206\346\236\220-\345\206\205\345\256\271\346\240\207\347\255\276/\345\206\205\345\256\271\346\240\207\347\255\276\345\234\250Feed\346\265\201\344\270\255\347\232\204\345\272\224\347\224\250.jpg" "b/docs/nlp/img/3.2.\347\257\207\347\253\240\345\210\206\346\236\220-\345\206\205\345\256\271\346\240\207\347\255\276/\345\206\205\345\256\271\346\240\207\347\255\276\345\234\250Feed\346\265\201\344\270\255\347\232\204\345\272\224\347\224\250.jpg" deleted file mode 100644 index 4b561398828bc05b4cea254b32e86f25381c8346..0000000000000000000000000000000000000000 Binary files "a/docs/nlp/img/3.2.\347\257\207\347\253\240\345\210\206\346\236\220-\345\206\205\345\256\271\346\240\207\347\255\276/\345\206\205\345\256\271\346\240\207\347\255\276\345\234\250Feed\346\265\201\344\270\255\347\232\204\345\272\224\347\224\250.jpg" and /dev/null differ diff --git "a/docs/nlp/img/3.2.\347\257\207\347\253\240\345\210\206\346\236\220-\345\206\205\345\256\271\346\240\207\347\255\276/\345\237\272\344\272\216\345\244\247\346\225\260\346\215\256\345\210\206\346\236\220\347\232\204\345\233\276\350\260\261\346\236\204\345\273\272.jpg" "b/docs/nlp/img/3.2.\347\257\207\347\253\240\345\210\206\346\236\220-\345\206\205\345\256\271\346\240\207\347\255\276/\345\237\272\344\272\216\345\244\247\346\225\260\346\215\256\345\210\206\346\236\220\347\232\204\345\233\276\350\260\261\346\236\204\345\273\272.jpg" deleted file mode 100644 index 6bb0ab2bcb7f3e5ab47e449fa98e3713a252c174..0000000000000000000000000000000000000000 Binary files "a/docs/nlp/img/3.2.\347\257\207\347\253\240\345\210\206\346\236\220-\345\206\205\345\256\271\346\240\207\347\255\276/\345\237\272\344\272\216\345\244\247\346\225\260\346\215\256\345\210\206\346\236\220\347\232\204\345\233\276\350\260\261\346\236\204\345\273\272.jpg" and /dev/null differ diff --git "a/docs/nlp/img/3.2.\347\257\207\347\253\240\345\210\206\346\236\220-\345\206\205\345\256\271\346\240\207\347\255\276/\347\231\276\345\272\246\345\206\205\345\256\271\346\240\207\347\255\276.jpg" "b/docs/nlp/img/3.2.\347\257\207\347\253\240\345\210\206\346\236\220-\345\206\205\345\256\271\346\240\207\347\255\276/\347\231\276\345\272\246\345\206\205\345\256\271\346\240\207\347\255\276.jpg" deleted file mode 100644 index 3a0ccda5becd9f6a9ced78135ed6dfaece318ac1..0000000000000000000000000000000000000000 Binary files "a/docs/nlp/img/3.2.\347\257\207\347\253\240\345\210\206\346\236\220-\345\206\205\345\256\271\346\240\207\347\255\276/\347\231\276\345\272\246\345\206\205\345\256\271\346\240\207\347\255\276.jpg" and /dev/null differ diff --git "a/docs/nlp/img/3.2.\347\257\207\347\253\240\345\210\206\346\236\220-\345\206\205\345\256\271\346\240\207\347\255\276/\351\200\232\347\224\250\346\240\207\347\255\276.jpg" "b/docs/nlp/img/3.2.\347\257\207\347\253\240\345\210\206\346\236\220-\345\206\205\345\256\271\346\240\207\347\255\276/\351\200\232\347\224\250\346\240\207\347\255\276.jpg" deleted file mode 100644 index 74a3e8a24aa5d27adc91e05ec47b071562ea0493..0000000000000000000000000000000000000000 Binary files "a/docs/nlp/img/3.2.\347\257\207\347\253\240\345\210\206\346\236\220-\345\206\205\345\256\271\346\240\207\347\255\276/\351\200\232\347\224\250\346\240\207\347\255\276.jpg" and /dev/null differ diff --git "a/docs/nlp/img/3.2.\347\257\207\347\253\240\345\210\206\346\236\220-\345\206\205\345\256\271\346\240\207\347\255\276/\351\235\242\345\220\221\346\216\250\350\215\220\347\232\204\346\240\207\347\255\276\345\233\276\350\260\261.jpg" "b/docs/nlp/img/3.2.\347\257\207\347\253\240\345\210\206\346\236\220-\345\206\205\345\256\271\346\240\207\347\255\276/\351\235\242\345\220\221\346\216\250\350\215\220\347\232\204\346\240\207\347\255\276\345\233\276\350\260\261.jpg" deleted file mode 100644 index e96b72eb1b0ffbf49e4e034c2cf2a082d8bad4b1..0000000000000000000000000000000000000000 Binary files "a/docs/nlp/img/3.2.\347\257\207\347\253\240\345\210\206\346\236\220-\345\206\205\345\256\271\346\240\207\347\255\276/\351\235\242\345\220\221\346\216\250\350\215\220\347\232\204\346\240\207\347\255\276\345\233\276\350\260\261.jpg" and /dev/null differ diff --git "a/docs/nlp/img/3.3.\347\257\207\347\253\240\345\210\206\346\236\220-\346\203\205\346\204\237\345\210\206\347\261\273/\346\203\205\346\204\237\345\210\206\347\261\273.jpg" "b/docs/nlp/img/3.3.\347\257\207\347\253\240\345\210\206\346\236\220-\346\203\205\346\204\237\345\210\206\347\261\273/\346\203\205\346\204\237\345\210\206\347\261\273.jpg" deleted file mode 100644 index 8a5572a5790c5f5e3ac9e50566786ed82ba50da5..0000000000000000000000000000000000000000 Binary files "a/docs/nlp/img/3.3.\347\257\207\347\253\240\345\210\206\346\236\220-\346\203\205\346\204\237\345\210\206\347\261\273/\346\203\205\346\204\237\345\210\206\347\261\273.jpg" and /dev/null differ diff --git "a/docs/nlp/img/3.3.\347\257\207\347\253\240\345\210\206\346\236\220-\346\203\205\346\204\237\345\210\206\347\261\273/\346\203\205\346\204\237\345\210\206\347\261\273\345\222\214\350\247\202\347\202\271\346\214\226\346\216\230.jpg" "b/docs/nlp/img/3.3.\347\257\207\347\253\240\345\210\206\346\236\220-\346\203\205\346\204\237\345\210\206\347\261\273/\346\203\205\346\204\237\345\210\206\347\261\273\345\222\214\350\247\202\347\202\271\346\214\226\346\216\230.jpg" deleted file mode 100644 index 6813abfb2724ba86570107064ce8c344ac3c0008..0000000000000000000000000000000000000000 Binary files "a/docs/nlp/img/3.3.\347\257\207\347\253\240\345\210\206\346\236\220-\346\203\205\346\204\237\345\210\206\347\261\273/\346\203\205\346\204\237\345\210\206\347\261\273\345\222\214\350\247\202\347\202\271\346\214\226\346\216\230.jpg" and /dev/null differ diff --git "a/docs/nlp/img/3.3.\347\257\207\347\253\240\345\210\206\346\236\220-\346\203\205\346\204\237\345\210\206\347\261\273/\347\231\276\345\272\246\345\272\224\347\224\250\346\216\250\350\215\220\347\220\206\347\224\261.jpg" "b/docs/nlp/img/3.3.\347\257\207\347\253\240\345\210\206\346\236\220-\346\203\205\346\204\237\345\210\206\347\261\273/\347\231\276\345\272\246\345\272\224\347\224\250\346\216\250\350\215\220\347\220\206\347\224\261.jpg" deleted file mode 100644 index 0bcb9f6e95c72925c1ba2c3a0e633a6685557822..0000000000000000000000000000000000000000 Binary files "a/docs/nlp/img/3.3.\347\257\207\347\253\240\345\210\206\346\236\220-\346\203\205\346\204\237\345\210\206\347\261\273/\347\231\276\345\272\246\345\272\224\347\224\250\346\216\250\350\215\220\347\220\206\347\224\261.jpg" and /dev/null differ diff --git "a/docs/nlp/img/3.3.\347\257\207\347\253\240\345\210\206\346\236\220-\346\203\205\346\204\237\345\210\206\347\261\273/\347\231\276\345\272\246\345\272\224\347\224\250\350\257\204\350\256\272\350\247\202\347\202\271.jpg" "b/docs/nlp/img/3.3.\347\257\207\347\253\240\345\210\206\346\236\220-\346\203\205\346\204\237\345\210\206\347\261\273/\347\231\276\345\272\246\345\272\224\347\224\250\350\257\204\350\256\272\350\247\202\347\202\271.jpg" deleted file mode 100644 index 23272d5cb7ce7c37657c4156c488009c0ac2353f..0000000000000000000000000000000000000000 Binary files "a/docs/nlp/img/3.3.\347\257\207\347\253\240\345\210\206\346\236\220-\346\203\205\346\204\237\345\210\206\347\261\273/\347\231\276\345\272\246\345\272\224\347\224\250\350\257\204\350\256\272\350\247\202\347\202\271.jpg" and /dev/null differ diff --git "a/docs/nlp/img/3.3.\347\257\207\347\253\240\345\210\206\346\236\220-\346\203\205\346\204\237\345\210\206\347\261\273/\350\247\202\347\202\271\346\214\226\346\216\230.jpg" "b/docs/nlp/img/3.3.\347\257\207\347\253\240\345\210\206\346\236\220-\346\203\205\346\204\237\345\210\206\347\261\273/\350\247\202\347\202\271\346\214\226\346\216\230.jpg" deleted file mode 100644 index 4020096d0aed4e09f5c1a368444cc33b1a954e83..0000000000000000000000000000000000000000 Binary files "a/docs/nlp/img/3.3.\347\257\207\347\253\240\345\210\206\346\236\220-\346\203\205\346\204\237\345\210\206\347\261\273/\350\247\202\347\202\271\346\214\226\346\216\230.jpg" and /dev/null differ diff --git "a/docs/nlp/img/3.3.\347\257\207\347\253\240\345\210\206\346\236\220-\346\203\205\346\204\237\345\210\206\347\261\273/\350\247\202\347\202\271\346\221\230\350\246\201.jpg" "b/docs/nlp/img/3.3.\347\257\207\347\253\240\345\210\206\346\236\220-\346\203\205\346\204\237\345\210\206\347\261\273/\350\247\202\347\202\271\346\221\230\350\246\201.jpg" deleted file mode 100644 index b20b1a007a0fcd5232c166572b20529109ec03fb..0000000000000000000000000000000000000000 Binary files "a/docs/nlp/img/3.3.\347\257\207\347\253\240\345\210\206\346\236\220-\346\203\205\346\204\237\345\210\206\347\261\273/\350\247\202\347\202\271\346\221\230\350\246\201.jpg" and /dev/null differ diff --git "a/docs/nlp/img/3.4.\347\257\207\347\253\240\345\210\206\346\236\220-\350\207\252\345\212\250\346\221\230\350\246\201/\344\277\241\346\201\257\347\210\206\347\202\270\344\270\216\347\247\273\345\212\250\345\214\226.jpg" "b/docs/nlp/img/3.4.\347\257\207\347\253\240\345\210\206\346\236\220-\350\207\252\345\212\250\346\221\230\350\246\201/\344\277\241\346\201\257\347\210\206\347\202\270\344\270\216\347\247\273\345\212\250\345\214\226.jpg" deleted file mode 100644 index 9838a9900d029cb369e17b2d9023260475511944..0000000000000000000000000000000000000000 Binary files "a/docs/nlp/img/3.4.\347\257\207\347\253\240\345\210\206\346\236\220-\350\207\252\345\212\250\346\221\230\350\246\201/\344\277\241\346\201\257\347\210\206\347\202\270\344\270\216\347\247\273\345\212\250\345\214\226.jpg" and /dev/null differ diff --git "a/docs/nlp/img/3.4.\347\257\207\347\253\240\345\210\206\346\236\220-\350\207\252\345\212\250\346\221\230\350\246\201/\345\205\270\345\236\213\346\221\230\350\246\201\350\256\241\347\256\227\346\265\201\347\250\213.jpg" "b/docs/nlp/img/3.4.\347\257\207\347\253\240\345\210\206\346\236\220-\350\207\252\345\212\250\346\221\230\350\246\201/\345\205\270\345\236\213\346\221\230\350\246\201\350\256\241\347\256\227\346\265\201\347\250\213.jpg" deleted file mode 100644 index 5847416becbca6c1d74defa22da2790aa34e6ac4..0000000000000000000000000000000000000000 Binary files "a/docs/nlp/img/3.4.\347\257\207\347\253\240\345\210\206\346\236\220-\350\207\252\345\212\250\346\221\230\350\246\201/\345\205\270\345\236\213\346\221\230\350\246\201\350\256\241\347\256\227\346\265\201\347\250\213.jpg" and /dev/null differ diff --git "a/docs/nlp/img/3.4.\347\257\207\347\253\240\345\210\206\346\236\220-\350\207\252\345\212\250\346\221\230\350\246\201/\345\237\272\344\272\216\347\257\207\347\253\240\344\277\241\346\201\257\347\232\204\351\200\232\347\224\250\346\226\260\351\227\273\346\221\230\350\246\201.jpg" "b/docs/nlp/img/3.4.\347\257\207\347\253\240\345\210\206\346\236\220-\350\207\252\345\212\250\346\221\230\350\246\201/\345\237\272\344\272\216\347\257\207\347\253\240\344\277\241\346\201\257\347\232\204\351\200\232\347\224\250\346\226\260\351\227\273\346\221\230\350\246\201.jpg" deleted file mode 100644 index 4e77f7847537e1886b6ddcc5c75ad795fbc85bc6..0000000000000000000000000000000000000000 Binary files "a/docs/nlp/img/3.4.\347\257\207\347\253\240\345\210\206\346\236\220-\350\207\252\345\212\250\346\221\230\350\246\201/\345\237\272\344\272\216\347\257\207\347\253\240\344\277\241\346\201\257\347\232\204\351\200\232\347\224\250\346\226\260\351\227\273\346\221\230\350\246\201.jpg" and /dev/null differ diff --git "a/docs/nlp/img/3.4.\347\257\207\347\253\240\345\210\206\346\236\220-\350\207\252\345\212\250\346\221\230\350\246\201/\346\200\273\347\273\223.jpg" "b/docs/nlp/img/3.4.\347\257\207\347\253\240\345\210\206\346\236\220-\350\207\252\345\212\250\346\221\230\350\246\201/\346\200\273\347\273\223.jpg" deleted file mode 100644 index 496f3319145dfa70683f109aeaf5a364438dc9f5..0000000000000000000000000000000000000000 Binary files "a/docs/nlp/img/3.4.\347\257\207\347\253\240\345\210\206\346\236\220-\350\207\252\345\212\250\346\221\230\350\246\201/\346\200\273\347\273\223.jpg" and /dev/null differ diff --git "a/docs/nlp/img/3.4.\347\257\207\347\253\240\345\210\206\346\236\220-\350\207\252\345\212\250\346\221\230\350\246\201/\346\221\230\350\246\201\347\263\273\347\273\237.jpg" "b/docs/nlp/img/3.4.\347\257\207\347\253\240\345\210\206\346\236\220-\350\207\252\345\212\250\346\221\230\350\246\201/\346\221\230\350\246\201\347\263\273\347\273\237.jpg" deleted file mode 100644 index 097d8d67344d930ef10c58b11aee390bf7338441..0000000000000000000000000000000000000000 Binary files "a/docs/nlp/img/3.4.\347\257\207\347\253\240\345\210\206\346\236\220-\350\207\252\345\212\250\346\221\230\350\246\201/\346\221\230\350\246\201\347\263\273\347\273\237.jpg" and /dev/null differ diff --git "a/docs/nlp/img/3.4.\347\257\207\347\253\240\345\210\206\346\236\220-\350\207\252\345\212\250\346\221\230\350\246\201/\347\231\276\345\272\246\345\272\224\347\224\250\346\220\234\347\264\242\346\222\255\346\212\245\346\221\230\350\246\201\345\222\214\345\233\276\345\203\217\346\221\230\350\246\201.jpg" "b/docs/nlp/img/3.4.\347\257\207\347\253\240\345\210\206\346\236\220-\350\207\252\345\212\250\346\221\230\350\246\201/\347\231\276\345\272\246\345\272\224\347\224\250\346\220\234\347\264\242\346\222\255\346\212\245\346\221\230\350\246\201\345\222\214\345\233\276\345\203\217\346\221\230\350\246\201.jpg" deleted file mode 100644 index b5d5e87f1ff86b77cf1b7b41eb348b458c5cbee1..0000000000000000000000000000000000000000 Binary files "a/docs/nlp/img/3.4.\347\257\207\347\253\240\345\210\206\346\236\220-\350\207\252\345\212\250\346\221\230\350\246\201/\347\231\276\345\272\246\345\272\224\347\224\250\346\220\234\347\264\242\346\222\255\346\212\245\346\221\230\350\246\201\345\222\214\345\233\276\345\203\217\346\221\230\350\246\201.jpg" and /dev/null differ diff --git "a/docs/nlp/img/3.4.\347\257\207\347\253\240\345\210\206\346\236\220-\350\207\252\345\212\250\346\221\230\350\246\201/\347\231\276\345\272\246\345\272\224\347\224\250\346\226\207\346\234\254\345\222\214\350\257\255\350\250\200\346\221\230\350\246\201.jpg" "b/docs/nlp/img/3.4.\347\257\207\347\253\240\345\210\206\346\236\220-\350\207\252\345\212\250\346\221\230\350\246\201/\347\231\276\345\272\246\345\272\224\347\224\250\346\226\207\346\234\254\345\222\214\350\257\255\350\250\200\346\221\230\350\246\201.jpg" deleted file mode 100644 index b384cae549b37862d667971c042d7e25014dd50a..0000000000000000000000000000000000000000 Binary files "a/docs/nlp/img/3.4.\347\257\207\347\253\240\345\210\206\346\236\220-\350\207\252\345\212\250\346\221\230\350\246\201/\347\231\276\345\272\246\345\272\224\347\224\250\346\226\207\346\234\254\345\222\214\350\257\255\350\250\200\346\221\230\350\246\201.jpg" and /dev/null differ diff --git "a/docs/nlp/img/3.4.\347\257\207\347\253\240\345\210\206\346\236\220-\350\207\252\345\212\250\346\221\230\350\246\201/\347\231\276\345\272\246\345\272\224\347\224\250\351\227\256\347\255\224\346\221\230\350\246\201.jpg" "b/docs/nlp/img/3.4.\347\257\207\347\253\240\345\210\206\346\236\220-\350\207\252\345\212\250\346\221\230\350\246\201/\347\231\276\345\272\246\345\272\224\347\224\250\351\227\256\347\255\224\346\221\230\350\246\201.jpg" deleted file mode 100644 index 0223fe51b0ddc796155c344a179b1caed6884376..0000000000000000000000000000000000000000 Binary files "a/docs/nlp/img/3.4.\347\257\207\347\253\240\345\210\206\346\236\220-\350\207\252\345\212\250\346\221\230\350\246\201/\347\231\276\345\272\246\345\272\224\347\224\250\351\227\256\347\255\224\346\221\230\350\246\201.jpg" and /dev/null differ diff --git "a/docs/nlp/img/3.4.\347\257\207\347\253\240\345\210\206\346\236\220-\350\207\252\345\212\250\346\221\230\350\246\201/\347\257\207\347\253\240\344\270\273\351\242\230\346\221\230\350\246\201.jpg" "b/docs/nlp/img/3.4.\347\257\207\347\253\240\345\210\206\346\236\220-\350\207\252\345\212\250\346\221\230\350\246\201/\347\257\207\347\253\240\344\270\273\351\242\230\346\221\230\350\246\201.jpg" deleted file mode 100644 index 2d98364bc5cba15ed4d110ef248d52a56048dcd0..0000000000000000000000000000000000000000 Binary files "a/docs/nlp/img/3.4.\347\257\207\347\253\240\345\210\206\346\236\220-\350\207\252\345\212\250\346\221\230\350\246\201/\347\257\207\347\253\240\344\270\273\351\242\230\346\221\230\350\246\201.jpg" and /dev/null differ diff --git "a/docs/nlp/img/3.4.\347\257\207\347\253\240\345\210\206\346\236\220-\350\207\252\345\212\250\346\221\230\350\246\201/\350\207\252\345\212\250\346\221\230\350\246\201\345\210\206\347\261\273.jpg" "b/docs/nlp/img/3.4.\347\257\207\347\253\240\345\210\206\346\236\220-\350\207\252\345\212\250\346\221\230\350\246\201/\350\207\252\345\212\250\346\221\230\350\246\201\345\210\206\347\261\273.jpg" deleted file mode 100644 index 0267472daf55b669acf2cd9b0419a27b688f830a..0000000000000000000000000000000000000000 Binary files "a/docs/nlp/img/3.4.\347\257\207\347\253\240\345\210\206\346\236\220-\350\207\252\345\212\250\346\221\230\350\246\201/\350\207\252\345\212\250\346\221\230\350\246\201\345\210\206\347\261\273.jpg" and /dev/null differ diff --git "a/docs/nlp/img/3.4.\347\257\207\347\253\240\345\210\206\346\236\220-\350\207\252\345\212\250\346\221\230\350\246\201/\351\227\256\347\255\224\346\221\230\350\246\201.jpg" "b/docs/nlp/img/3.4.\347\257\207\347\253\240\345\210\206\346\236\220-\350\207\252\345\212\250\346\221\230\350\246\201/\351\227\256\347\255\224\346\221\230\350\246\201.jpg" deleted file mode 100644 index e7f7a8cd59a148f3fdaf5f98d141c82d7c71eb83..0000000000000000000000000000000000000000 Binary files "a/docs/nlp/img/3.4.\347\257\207\347\253\240\345\210\206\346\236\220-\350\207\252\345\212\250\346\221\230\350\246\201/\351\227\256\347\255\224\346\221\230\350\246\201.jpg" and /dev/null differ diff --git a/docs/nlp/img/F94581F64C21A1094A473397DFA42F9C.jpg b/docs/nlp/img/F94581F64C21A1094A473397DFA42F9C.jpg deleted file mode 100644 index 495ea2f5ed05acb02309569bf955f890dfb528fa..0000000000000000000000000000000000000000 Binary files a/docs/nlp/img/F94581F64C21A1094A473397DFA42F9C.jpg and /dev/null differ diff --git a/docs/nlp/img/Word2Vce/CBoW.png b/docs/nlp/img/Word2Vce/CBoW.png deleted file mode 100644 index d2a7c2080024363d27424759f8bd3b0e07603404..0000000000000000000000000000000000000000 Binary files a/docs/nlp/img/Word2Vce/CBoW.png and /dev/null differ diff --git a/docs/nlp/img/Word2Vce/Skip-gram.png b/docs/nlp/img/Word2Vce/Skip-gram.png deleted file mode 100644 index 1e39b9d15446079f1abcc75d2e9c6dbd86a97a87..0000000000000000000000000000000000000000 Binary files a/docs/nlp/img/Word2Vce/Skip-gram.png and /dev/null differ