2020-12-29 18:37:05
23
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)
|
||||
|
||||
196
docs/nlp/0.md
Normal file
@@ -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`。
|
||||
|
||||
## 版税
|
||||
|
||||
出售这本书的版税将被用来支持自然语言工具包的发展。
|
||||
|
||||

|
||||
|
||||
图 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
|
||||
969
docs/nlp/1.md
Normal file
@@ -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 "<stdin>", line 1
|
||||
1 +
|
||||
^
|
||||
SyntaxError: invalid syntax
|
||||
>>>
|
||||
```
|
||||
|
||||
产生了一个语法错误。在 Python 中,指令以加号结尾是没有意义的。Python 解释器会指出发生错误的行(“标准输入”`<stdin>`的第 1 行)。
|
||||
|
||||
现在我们学会使用 Python 解释器了,已经准备好可以开始处理语言数据了。
|
||||
|
||||
## 1.2 NLTK 入门
|
||||
|
||||
在进一步深入之前,应先安装 NLTK 3.0,可以从`http://nltk.org/`免费下载。按照说明下载适合你的操作系统的版本。
|
||||
|
||||
安装完 NLTK 之后,像前面那样启动 Python 解释器,在 Python 提示符后面输入下面两个命令来安装本书所需的数据,然后选择`book`集合,如 1.1 所示。
|
||||
|
||||
```py
|
||||
>>> import nltk
|
||||
>>> nltk.download()
|
||||
```
|
||||
|
||||

|
||||
|
||||
图 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
|
||||
<Text: Moby Dick by Herman Melville 1851>
|
||||
>>> text2
|
||||
<Text: Sense and Sensibility by Jane Austen 1811>
|
||||
>>>
|
||||
```
|
||||
|
||||
现在我们可以和这些数据一起来使用 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"])
|
||||
>>>
|
||||
```
|
||||
|
||||

|
||||
|
||||
图 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 "<stdin>", 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 "<stdin>", 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 "<stdin>", 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 显示的那样。计数器可能需要几千行,这将是一个极其繁琐的过程——如此繁琐以至于我们宁愿把任务交给机器来做。
|
||||
|
||||

|
||||
|
||||
图 3.1:计数一个文本中出现的词(频率分布)
|
||||
|
||||
图 3.1 中的表被称为频率分布,它告诉我们在文本中的每一个词项的频率。(一般情况下,它能计数任何观察得到的事件。)这是一个“分布”因为它告诉我们文本中单词词符的总数是如何分布在词项中的。因为我们经常需要在语言处理中使用频率分布,NLTK 中内置了它们。让我们使用`FreqDist`寻找《白鲸记》中最常见的 50 个词:
|
||||
|
||||
```py
|
||||
>>> fdist1 = FreqDist(text1) ❶
|
||||
>>> print(fdist1) ❷
|
||||
<FreqDist with 19317 samples and 260819 outcomes>
|
||||
>>> 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 个词占了书的将近一半!
|
||||
|
||||

|
||||
|
||||
图 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', ...])`,你将看到`<generator object bigrams at 0x10fb8b3a8>`的输出形式。这是 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) ❸
|
||||
<FreqDist with 19 samples and 260819 outcomes>
|
||||
>>> 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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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)**: 分析篇章级文本的内在结构,进而分析文本情感倾向,提取评论性观点,并生成反映文本关键信息的标签与摘要
|
||||
|
||||
例如:
|
||||
|
||||

|
||||
|
||||
### 案例7(机器翻译)
|
||||
|
||||
**机器翻译技术(Machine Translating)**: 基于互联网大数据,融合深度神经网络、统计、规则多种翻译方法,帮助用户跨越语言鸿沟,与世界自由沟通
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
* 参考百度科普课程: <http://bit.baidu.com/product>
|
||||
999
docs/nlp/10.md
Normal file
@@ -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)')
|
||||
<NegatedExpression -(P & Q)>
|
||||
>>> read_expr('P & Q')
|
||||
<AndExpression (P & Q)>
|
||||
>>> read_expr('P | (R -> Q)')
|
||||
<OrExpression (P | (R -> Q))>
|
||||
>>> read_expr('P <-> -- P')
|
||||
<IffExpression (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
|
||||
<ConstantExpression angus>
|
||||
>>> expr.argument.type
|
||||
e
|
||||
>>> expr.function
|
||||
<ConstantExpression walk>
|
||||
>>> expr.function.type
|
||||
<e,?>
|
||||
```
|
||||
|
||||
为什么我们在这个例子的结尾看到`<e,?>`呢?虽然类型检查器会尝试推断出尽可能多的类型,在这种情况下,它并没有能够推断出`walk`的类型,所以其结果的类型是未知的。虽然我们期望`walk`的类型是`<e, t>`,迄今为止类型检查器知道的,在这个上下文中可能是一些其他类型,如`<e, e>`或`<e, >e, t>`。要帮助类型检查器,我们需要指定一个信号,作为一个字典来实施,明确的与非逻辑常量类型关联:
|
||||
|
||||
```py
|
||||
>>> sig = {'walk': '<e, t>'}
|
||||
>>> expr = read_expr('walk(angus)', signature=sig)
|
||||
>>> expr.function.type
|
||||
e
|
||||
```
|
||||
|
||||
一种二元谓词具有类型`<e, <e, t>>`。虽然这是先组合类型`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 一阶逻辑语言总结
|
||||
|
||||
我们将借此机会重新表述前面的命题逻辑的语法规则,并添加量词的形式化规则;所有这些一起组成一阶逻辑的句法。此外,我们会明确相关表达式的类型。我们将采取约定:`<e^n, t>`,一种由`n`个类型为`e`的参数组成产生一个类型为`t`的表达式的谓词的类型。在这种情况下,我们说`n`是谓词的元数。
|
||||
|
||||
> 1. 如果`P`是`<e[n], t>`类型的谓词,并且`α[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>] -> '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
|
||||
<LambdaExpression \x.(walk(x) & chew_gum(x))>
|
||||
>>> 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=<walk(irene)>]
|
||||
(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=<exists z3.(ankle(z3) & bite(cyril,z3))>]
|
||||
(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() ❶
|
||||
```
|
||||
|
||||

|
||||
|
||||
图 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`是`<e, t>`类型的表达式。那么,例如`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
|
||||
794
docs/nlp/11.md
Normal file
@@ -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 的第二个特点是:它在多个维度的变化与方言地区和二元音覆盖范围之间取得平衡。人口学统计的加入带来了许多更独立的变量,这可能有助于解释数据中的变化,便于以后出于在建立语料库时没有想到的目的使用语料库,例如社会语言学。第三个特点是:将原始语言学事件作为录音来捕捉和作为标注来捕捉之间有明显的区分。两者一致表示文本语料库正确,原始文本通常有被认为是不可改变的作品的外部来源。那个作品的任何包含人的判断的转换——即使如分词一样简单——也是后来的修订版,因此以尽可能接近原始的形式保留源材料是十分重要的。
|
||||
|
||||

|
||||
|
||||
图 1.2:发布的 TIMIT 语料库的结构:CD-ROM 包含文档、顶层的训练和测试目录;训练和测试目录都有 8 子目录,每个方言区一个;这些目录又包含更多子目录,每个说话者一个;列出的目录是女性说话者`aks0`的目录的内容,显示 10 个`wav`文件配以一个录音文本文件、一个录音文本词对齐文件和一个音标文件。
|
||||
|
||||
TIMIT 的第四个特点是语料库的层次结构。每个句子 4 个文件,500 个说话者每人 10 个句子,有 20,000 个文件。这些被组织成一个树状结构,示意图如 1.2 所示。在顶层分成训练集和测试集,用于开发和评估统计模型。
|
||||
|
||||
最后,请注意虽然 TIMIT 是语音语料库,它的录音文本和相关数据只是文本,可以使用程序处理了,就像任何其他的文本语料库那样。因此,许多在这本书中所描述的计算方法都适用。此外,注意 TIMIT 语料库包含的所有数据类型分为词汇和文字两个基本类别,我们将在下面讨论。说话者人口学统计数据只不过是词汇数据类型的另一个实例。
|
||||
|
||||
当我们考虑到文字和记录结构是计算机科学中关注数据管理的两个子领域首要内容,即全文检索领域和数据库领域,这最后的观察就不太令人惊讶了。语言数据管理的一个显着特点是往往将这两种数据类型放在一起,可以利用这两个领域的成果和技术。
|
||||
|
||||
## 1.3 基本数据类型
|
||||
|
||||

|
||||
|
||||
图 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)`。
|
||||
|
||||

|
||||
|
||||
图 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 所示。在以后的日子,应该有人想要组合不同的版本的源数据,这项任务可能会非常繁重。
|
||||
|
||||

|
||||
|
||||
图 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
|
||||
<p class=MsoNormal>sleep
|
||||
<span style='mso-spacerun:yes'> </span>
|
||||
[<span class=SpellE>sli:p</span>]
|
||||
<span style='mso-spacerun:yes'> </span>
|
||||
<b><span style='font-size:11.0pt'>v.i.</span></b>
|
||||
<span style='mso-spacerun:yes'> </span>
|
||||
<i>a condition of body and mind ...<o:p></o:p></i>
|
||||
</p>
|
||||
|
||||
```
|
||||
|
||||
这个简单的程序只是冰山一角。我们可以开发复杂的工具来检查字处理器文件的一致性,并报告错误,使字典的维护者可以*使用原来的文字处理器*纠正的原始文件。
|
||||
|
||||
只要我们知道数据的正确格式,就可以编写其他程序将数据转换成不同格式。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'<p', SEP + '<p', html)
|
||||
text = BeautifulSoup(html).get_text()
|
||||
text = ' '.join(text.split())
|
||||
for entry in text.split(SEP):
|
||||
if entry.count(' ') > 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 中的语料库阅读器是更系统的方法。
|
||||
|
||||

|
||||
|
||||
图 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
|
||||
<entry>
|
||||
<headword>whale</headword>
|
||||
<pos>noun</pos>
|
||||
<gloss>any of the larger cetacean mammals having a streamlined
|
||||
body and breathing through a blowhole on the head</gloss>
|
||||
</entry>
|
||||
|
||||
```
|
||||
|
||||
## 4.2 XML 的作用
|
||||
|
||||
我们可以用 XML 来表示许多种语言信息。然而,灵活性是要付出代价的。每次我们增加复杂性,如允许一个元素是可选的或重复的,我们对所有访问这些数据的程序都要做出更多的工作。我们也使它更难以检查数据的有效性,或使用一种 XML 查询语言来查询数据。
|
||||
|
||||
因此,使用 XML 来表示语言结构并不能神奇地解决数据建模问题。我们仍然需要解决如何结构化数据,然后用一个模式定义结构,并编写程序读取和写入格式,以及把它转换为其他格式。同样,我们仍然需要遵循一些有关数据规范化的标准原则。这是明智的,可以避免相同信息的重复复制,所以当只有一个副本变化时,不会导致数据不一致。例如,交叉引用表示为`<xref>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]) ❶
|
||||
<?xml version="1.0"?>
|
||||
<?xml-stylesheet type="text/css" href="shakes.css"?>
|
||||
<!-- <!DOCTYPE PLAY SYSTEM "play.dtd"> -->
|
||||
<PLAY>
|
||||
<TITLE>The Merchant of Venice</TITLE>
|
||||
>>> print(raw[1789:2006]) ❷
|
||||
<TITLE>ACT I</TITLE>
|
||||
<SCENE><TITLE>SCENE I. Venice. A street.</TITLE>
|
||||
<STAGEDIR>Enter ANTONIO, SALARINO, and SALANIO</STAGEDIR>
|
||||
<SPEECH>
|
||||
<SPEAKER>ANTONIO</SPEAKER>
|
||||
<LINE>In sooth, I know not why I am so sad:</LINE>
|
||||
```
|
||||
|
||||
我们刚刚访问了作为一个字符串的 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' at 0x10ac43d18> # [_element-play]
|
||||
>>> merchant[0]
|
||||
<Element 'TITLE' at 0x10ac43c28> # [_element-title]
|
||||
>>> merchant[0].text
|
||||
'The Merchant of Venice' # [_element-text]
|
||||
>>> merchant.getchildren() ❺
|
||||
[<Element 'TITLE' at 0x10ac43c28>, <Element 'PERSONAE' at 0x10ac43bd8>,
|
||||
<Element 'SCNDESCR' at 0x10b067f98>, <Element 'PLAYSUBT' at 0x10af37048>,
|
||||
<Element 'ACT' at 0x10af37098>, <Element 'ACT' at 0x10b936368>,
|
||||
<Element 'ACT' at 0x10b934b88>, <Element 'ACT' at 0x10cfd8188>,
|
||||
<Element 'ACT' at 0x10cfadb38>]
|
||||
```
|
||||
|
||||
这部戏剧由标题、角色、一个场景的描述、字幕和五幕组成。每一幕都有一个标题和一些场景,每个场景由台词组成,台词由行组成,有四个层次嵌套的结构。让我们深入到第四幕:
|
||||
|
||||
```py
|
||||
>>> merchant[-2][0].text
|
||||
'ACT IV'
|
||||
>>> merchant[-2][1]
|
||||
<Element 'SCENE' at 0x10cfd8228>
|
||||
>>> merchant[-2][1][0].text
|
||||
'SCENE I. Venice. A court of justice.'
|
||||
>>> merchant[-2][1][54]
|
||||
<Element 'SPEECH' at 0x10cfb02c8>
|
||||
>>> merchant[-2][1][54][0]
|
||||
<Element 'SPEAKER' at 0x10cfb0318>
|
||||
>>> merchant[-2][1][54][0].text
|
||||
'PORTIA'
|
||||
>>> merchant[-2][1][54][1]
|
||||
<Element 'LINE' at 0x10cfb0368>
|
||||
>>> 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]
|
||||
<Element 'lx' at 0x10b2f6958>
|
||||
>>> 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') ❶
|
||||
<record>
|
||||
<lx>kaa</lx>
|
||||
<ps>N</ps>
|
||||
<pt>MASC</pt>
|
||||
<cl>isi</cl>
|
||||
<ge>cooking banana</ge>
|
||||
<tkp>banana bilong kukim</tkp>
|
||||
<pt>itoo</pt>
|
||||
<sf>FLORA</sf>
|
||||
<dt>12/Aug/2005</dt>
|
||||
<ex>Taeavi iria kaa isi kovopaueva kaparapasia.</ex>
|
||||
<xp>Taeavi i bin planim gaden banana bilong kukim tasol long paia.</xp>
|
||||
<xe>Taeavi planted banana in order to cook it.</xe>
|
||||
</record>
|
||||
```
|
||||
|
||||
## 4.5 格式化条目
|
||||
|
||||
我们可以使用在前一节看到的同样的想法生成 HTML 表格而不是纯文本。这对于将 Toolbox 词汇发布到网络上非常有用。它产生 HTML 元素`<table>`,`<tr>`(表格的行)和`<td>`(表格数据)。
|
||||
|
||||
```py
|
||||
>>> html = "<table>\n"
|
||||
>>> for entry in lexicon[70:80]:
|
||||
... lx = entry.findtext('lx')
|
||||
... ps = entry.findtext('ps')
|
||||
... ge = entry.findtext('ge')
|
||||
... html += " <tr><td>%s</td><td>%s</td><td>%s</td></tr>\n" % (lx, ps, ge)
|
||||
>>> html += "</table>"
|
||||
>>> print(html)
|
||||
<table>
|
||||
<tr><td>kakae</td><td>???</td><td>small</td></tr>
|
||||
<tr><td>kakae</td><td>CLASS</td><td>child</td></tr>
|
||||
<tr><td>kakaevira</td><td>ADV</td><td>small-like</td></tr>
|
||||
<tr><td>kakapikoa</td><td>???</td><td>small</td></tr>
|
||||
<tr><td>kakapikoto</td><td>N</td><td>newborn baby</td></tr>
|
||||
<tr><td>kakapu</td><td>V</td><td>place in sling for purpose of carrying</td></tr>
|
||||
<tr><td>kakapua</td><td>N</td><td>sling for lifting</td></tr>
|
||||
<tr><td>kakara</td><td>N</td><td>arm band</td></tr>
|
||||
<tr><td>Kakarapaia</td><td>N</td><td>village name</td></tr>
|
||||
<tr><td>kakarau</td><td>N</td><td>frog</td></tr>
|
||||
</table>
|
||||
```
|
||||
|
||||
## 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: {<lf>(<lv><ln|le>*)*}
|
||||
example: {<rf|xv><xn|xe>*}
|
||||
sense: {<sn><ps><pn|gv|dv|gn|gp|dn|rn|ge|de|re>*<example>*<lexfunc>*}
|
||||
record: {<lx><hm><sense>+<dt>}
|
||||
"""
|
||||
```
|
||||
|
||||

|
||||
|
||||
图 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
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<olac:olac xmlns:olac="http://www.language-archives.org/OLAC/1.1/"
|
||||
xmlns="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:dcterms="http://purl.org/dc/terms/"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.language-archives.org/OLAC/1.1/
|
||||
http://www.language-archives.org/OLAC/1.1/olac.xsd">
|
||||
<title>A grammar of Kayardild. With comparative notes on Tangkic.</title>
|
||||
<creator>Evans, Nicholas D.</creator>
|
||||
<subject>Kayardild grammar</subject>
|
||||
<subject xsi:type="olac:language" olac:code="gyd">Kayardild</subject>
|
||||
<language xsi:type="olac:language" olac:code="en">English</language>
|
||||
<description>Kayardild Grammar (ISBN 3110127954)</description>
|
||||
<publisher>Berlin - Mouton de Gruyter</publisher>
|
||||
<contributor xsi:type="olac:role" olac:code="author">Nicholas Evans</contributor>
|
||||
<format>hardcover, 837 pages</format>
|
||||
<relation>related to ISBN 0646119966</relation>
|
||||
<coverage>Australia</coverage>
|
||||
<type xsi:type="olac:linguistic-type" olac:code="language_description"/>
|
||||
<type xsi:type="dcterms:DCMIType">Text</type>
|
||||
</olac:olac>
|
||||
|
||||
```
|
||||
|
||||
## 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>
|
||||
|
||||
```
|
||||
78
docs/nlp/12.md
Normal file
@@ -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
|
||||
458
docs/nlp/14.md
Normal file
@@ -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
|
||||
1241
docs/nlp/2.md
Normal file
694
docs/nlp/2.分词.md
@@ -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()
|
||||
```
|
||||
|
||||
> 切换其他词典
|
||||
|
||||
* 占用内存较小的词典文件: <https://github.com/fxsjy/jieba/raw/master/extra_dict/dict.txt.small>
|
||||
* 支持繁体分词更好的词典文件: <https://github.com/fxsjy/jieba/raw/master/extra_dict/dict.txt.big>
|
||||
* 下载你所需要的词典,然后覆盖 `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 水果世博园
|
||||
买<b class="match term0">水果</b>然后来<b class="match term1">世博园</b>
|
||||
==========
|
||||
result of 你
|
||||
second one <b class="match term0">你</b> 中文测试中文 is even more interesting
|
||||
==========
|
||||
result of first
|
||||
<b class="match term0">first</b> document we’ve added
|
||||
==========
|
||||
result of 中文
|
||||
second one 你 <b class="match term0">中文</b>测试<b class="match term0">中文</b> is even more interesting
|
||||
==========
|
||||
result of 交换机
|
||||
干事每月经过下属科室都要亲口交代24口<b class="match term0">交换机</b>等技术性器件的安装工作
|
||||
==========
|
||||
result of 交换
|
||||
咱俩<b class="match term0">交换</b>一下吧
|
||||
干事每月经过下属科室都要亲口交代24口<b class="match term0">交换</b>机等技术性器件的安装工作
|
||||
==========
|
||||
我
|
||||
好
|
||||
朋友
|
||||
是
|
||||
李明
|
||||
我
|
||||
爱
|
||||
北京
|
||||
天安
|
||||
天安门
|
||||
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)
|
||||
@@ -1,30 +0,0 @@
|
||||
# 篇章分析-内容概述
|
||||
|
||||
## 篇章分析变迁
|
||||
|
||||
1. 内容生态: 新浪 -> 百家号、今日头条(自媒体)
|
||||
2. 用户成为信息的生产中心: web 1.0 -> 百度贴吧、新浪微博、团购网站(用户评论,富有个人情感和用户观点的信息)
|
||||
3. 移动、无屏: 显示屏 -> 手机、Siri(展示的终端)
|
||||
|
||||
## 篇章分析场景
|
||||
|
||||
篇章分析重要性: 让人们最平等`便捷`地`获取信息`,`找到所求`。
|
||||
|
||||
1. 个性化信息获取(搜索引擎的理解和推荐): 从搜索的角度来看,通过对内容的深入理解,我们能够精准地对内容进行分析,然后将内容推荐给需要的用户,达到不搜即得。
|
||||
2. 便捷咨询阅读(头条的热门推荐): 从资讯阅读的角度来看,我们通过对内容进行概括总结、形成摘要,就能搞让用户更快捷地浏览信息、获取知识。
|
||||
3. 信息直接满足: 更进一步说,对用户的问题,我们可以基于内容理解,直接给出答案,从而满足用户的需求。
|
||||
|
||||
`总之`: 通过篇章分析,我们能够进行内容理解,从而更好地服务用户。
|
||||
|
||||
## 篇章分析概述
|
||||
|
||||
`篇章是形式上互相衔接、语义上前后连贯的句子序列。`
|
||||
|
||||
有以下3种:
|
||||
* 1.文章: 新闻稿、博客、微博
|
||||
* 2.评论: O2O服务的用户评论、豆瓣的影评、微博上的动态
|
||||
* 3.对话: 话题上是相互衔接的、语义上也是连贯的一个对话序列
|
||||
|
||||
## 篇章分析任务
|
||||
|
||||

|
||||
@@ -1,82 +0,0 @@
|
||||
# 篇章分析-内容标签
|
||||
|
||||
`标签`: 这种种的`单词`和`词组`都是一种标签的形式
|
||||
1. 新闻稿,打出关于该报道的各种各样的标签,来表示其关键信息
|
||||
2. 论文中,我们也会表明一些文章的`领域分类`以及`关键词`等标签
|
||||
3. 微博用#代表一个话题,这是典型的社会化标签
|
||||
|
||||
## 标签用途
|
||||
|
||||
1. 关键信息展示
|
||||
* 用户可以大致了解文章的主要信息,从而决定要不要对信息进行进一步深入地浏览
|
||||
|
||||
2. 频道划分
|
||||
* 在很多的媒体网站,经常会有频道划分,使用了就是文章的分类标签
|
||||
|
||||
3. 话题聚合
|
||||
* 标签也可以用来做话题聚合(例如: #人民的名义# 集合所有关于这个话题的信息,让用户更深入的了解信息)
|
||||
|
||||
## 应用: 个性化推荐
|
||||
|
||||
* 标签可以用来建立用户的画像
|
||||
|
||||
比如对对于用户搜索过的Query,还有他浏览过的文章,都可以通过标签的技术。提取出主要的兴趣点,从而也就建立了用户的画像
|
||||
|
||||
* 标签可以对内容进行建模
|
||||
|
||||
通过标签技术,我们能够提取文章中的关键信息标签。这样来看标签就作为了用户和内容的一个共同表示。
|
||||
|
||||
* 推荐的时候,我们通过对用户画像的标签和内容模型的标签进行匹配,就能够对用户进行一个精准的个性化推荐
|
||||
|
||||
## 百度内容标签
|
||||
|
||||

|
||||
|
||||
## 标签体系: 面向推荐的标签图谱
|
||||
|
||||
* 标签图谱刻画了用户的兴趣点,以及兴趣点之间的关联关系。
|
||||
* 节点表示了用户的兴趣点,而边表示了兴趣点之间的关联关系(边是带有权重的,表示关联强度)。
|
||||
* 包括3种节点: 主题标签-绿色,话题标签-紫色,实体标签-蓝色。
|
||||
* 有了关联关系,我们可以进行一定程度的探索和泛化。(例如: 无人驾驶和人工智能关联很强,如果有人看了无人驾驶,我们就给他推荐人工智能)
|
||||
|
||||

|
||||
|
||||
## 标签体系: 基于大数据分析的图谱构建
|
||||
|
||||
* 用户信息来源: 贴吧、微博
|
||||
* 标签的相关性分析: 通过关联规则,发现2个标签总同时出现,我们觉得这是高相关的。
|
||||
|
||||

|
||||
|
||||
## 标签计算
|
||||
|
||||
> 主题分类
|
||||
|
||||
* 主题标签的计算,是一种很典型的文本分类问题: 传统的朴素贝叶斯、最大熵、SVM 等解决方案。
|
||||
* 当前我们主要采用的是: 基于神经网络的方法(可以看右侧的示意图)
|
||||
* 整个网络分成3层次:
|
||||
* 第一层 原始特征层: 抽取简单的原始特征,例如说文章出现的单词、词组 等等
|
||||
* 第二层 表示层: 通过一些 embedding的算法、CNN、LSTM的方法
|
||||
* 第三层 排序层: 计算文章与主题之间的相似度,具体会计算每个主题与文章的相似度,并将相似度作为最终的一个主题分类的结果。这种计算的好处能够天然的支持多标记,也就是一篇文章可以同时计算出多个主题标签。
|
||||
|
||||

|
||||
|
||||
> 通用标签
|
||||
|
||||
* 通用标签主要是计算内容中的实体和话题,我们综合了两种策略。
|
||||
* 第一种策略: 针对比较热门的高频标签
|
||||
* 这种标签我们主要通过一些预测的方法得到,预测的方法: 基于相似度计算得到的---这种方法并不要求标签一定在文章中出现
|
||||
* 例如: 美国大选这种标签,如果一篇文章出现了 `希拉里` `特朗普` `辩论` 等一些词,即使没有出现美国大选,我们通过语义相似度的方法也能把这个标签计算出来。
|
||||
* 第二种策略: 面向中低频的标签
|
||||
* 这种标签相关的信息,不是那么丰富,所以我们计算的时候更多依赖的是标签在文章中的信息
|
||||
* 比如: 这个标签在文章中出现的频率 或 出现的位置;如果出现在标题,那么它可能就会比较重要。
|
||||
* 通过融合这2种策略,形成我们通用标签的结果。
|
||||
|
||||

|
||||
|
||||
## 内容标签在Feed流中的应用
|
||||
|
||||
1. 标签可以用来话题聚合: 比如表示人工智能的标签全部都会集合到同一个话题下面。这样用户可以对人工智能这个话题进行非常充分的浏览。
|
||||
2. 话题频道划分: 比如我们在手机百度上面就可以看到,Feed流上面有多个栏目,用户可以点击 `体育` `时尚`等频道
|
||||
|
||||

|
||||
@@ -1,45 +0,0 @@
|
||||
# 篇章分析-情感分类
|
||||
|
||||
## 用户评论剧增
|
||||
|
||||
`服务评论` `商品评论` `社交评论`
|
||||
|
||||
## 情感分析应用
|
||||
|
||||
`消费决策` `舆情分析`
|
||||
|
||||
> 情感分类 和 观点挖掘
|
||||
|
||||
* 对(文本的)观点、情感、情绪和评论进行分析计算
|
||||
|
||||

|
||||
|
||||
> 情感分类
|
||||
|
||||
* 给定一个文本判断其情感的极性,包括积极、中性、消极。
|
||||
* LSTM 对文本进行语义表示,进而基于语义表示进行情感分类。
|
||||
|
||||

|
||||
|
||||
> 观点挖掘
|
||||
|
||||
* 观点聚类: 主要目标是对大量的评论数据进行聚类,将相同的观点抽取出来,并形成一个情感搭配词典(算法是: 搭配抽取、词法分析、聚类归一,从而获得一个情感搭配。我们就可以进行观点抽取)
|
||||
* 观点抽取: 就是对输入的文本进行计算,将其中的情感标签抽取出来,这里的标签,都是来自于情感搭配词典的,也就是观点聚类获得的词典。
|
||||
* 观点抽取一种简单的做法是直接通过标签匹配的方式得到,比如: 服务不错这个情感搭配,恰好在文本中出现,我们就可以把它抽取出来。
|
||||
* 但是这种简单的抽取方法,其实上只能从字面上抽取情感搭配,而无法解决字面不一致的,但是意思一样的情感搭配抽取,因此我们还引入了语义相似度的方法。这种方法主要是通过神经网络进行计算的。它能解决这种字面不一致,语义一样的抽取问题。
|
||||
|
||||

|
||||
|
||||
> 观点摘要
|
||||
|
||||
综合了情感分类和观点挖掘的一些技术,而获得的一个整体的应用技术
|
||||
|
||||

|
||||
|
||||
## 百度应用: 评论观点
|
||||
|
||||

|
||||
|
||||
## 百度应用: 推荐理由
|
||||
|
||||

|
||||
@@ -1,62 +0,0 @@
|
||||
# 篇章分析-自动摘要
|
||||
|
||||
## 信息爆炸与移动化
|
||||
|
||||

|
||||
|
||||
## 自动摘要应用
|
||||
|
||||
* 便捷信息浏览
|
||||
* 我们可以为每个新闻抽取摘要,用户可以通过摘要快速了解新闻概况。
|
||||
* 进而决定是否要进一步细致地浏览。
|
||||
* 而更进一步说: 摘要还可以直接进行信息满足。
|
||||
* 信息满足
|
||||
* 传统搜索得到一大批网页信息
|
||||
* 现在通过问答技术我们能够将网页中最核心的片段摘要提取出来。
|
||||
* 用户通过阅读片段,就可以直接得到满足,而不需要打开页面。
|
||||
|
||||
## 自动摘要
|
||||
|
||||
* 对海量内容进行提炼与总结
|
||||
* 以简洁、直观的摘要来概括用户所关注的主要内容
|
||||
* 方便用户快速了解与浏览海量内容
|
||||
|
||||

|
||||
|
||||
* 自动摘要分类
|
||||
|
||||

|
||||
|
||||
* 典型摘要计算流程
|
||||
|
||||

|
||||
|
||||
> 基于篇章信息的通用新闻摘要
|
||||
|
||||

|
||||
|
||||
> 篇章主题摘要
|
||||
|
||||

|
||||
|
||||
> 问答摘要
|
||||
|
||||

|
||||
|
||||
## 百度应用
|
||||
|
||||
> 文本和语言摘要
|
||||
|
||||

|
||||
|
||||
> 问答摘要
|
||||
|
||||

|
||||
|
||||
> 搜索播报摘要和图像摘要
|
||||
|
||||

|
||||
|
||||
## 总结
|
||||
|
||||

|
||||
1758
docs/nlp/3.md
Normal file
@@ -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)$$
|
||||
1477
docs/nlp/4.md
Normal file
1218
docs/nlp/5.md
Normal file
802
docs/nlp/6.md
Normal file
@@ -0,0 +1,802 @@
|
||||
# 6 学习分类文本
|
||||
|
||||
模式识别是自然语言处理的一个核心部分。以`-ed`结尾的词往往是过去时态动词`(5)`。频繁使用`will`是新闻文本的暗示`(3)`。这些可观察到的模式——词的结构和词频——恰好与特定方面的含义关联,如时态和主题。但我们怎么知道从哪里开始寻找,形式的哪一方面关联含义的哪一方面?
|
||||
|
||||
本章的目的是要回答下列问题:
|
||||
|
||||
1. 我们怎样才能识别语言数据中能明显用于对其分类的特征?
|
||||
2. 我们怎样才能构建语言模型,用于自动执行语言处理任务?
|
||||
3. 从这些模型中我们可以学到哪些关于语言的知识?
|
||||
|
||||
一路上,我们将研究一些重要的机器学习技术,包括决策树、朴素贝叶斯分类器和最大熵分类。我们会掩盖这些技术的数学和统计的基础,集中关注如何以及何时使用它们(更多的技术背景知识见进一步阅读一节)。在看这些方法之前,我们首先需要知道这个主题的范围十分广泛。
|
||||
|
||||
## 1 有监督分类
|
||||
|
||||
分类是为给定的输入选择正确的类标签的任务。在基本的分类任务中,每个输入被认为是与所有其它输入隔离的,并且标签集是预先定义的。这里是分类任务的一些例子:
|
||||
|
||||
* 判断一封电子邮件是否是垃圾邮件。
|
||||
* 从一个固定的主题领域列表中,如“体育”、“技术”和“政治”,决定新闻报道的主题是什么。
|
||||
* 决定词`bank`给定的出现是用来指河的坡岸、一个金融机构、向一边倾斜的动作还是在金融机构里的存储行为。
|
||||
|
||||
基本的分类任务有许多有趣的变种。例如,在多类分类中,每个实例可以分配多个标签;在开放性分类中,标签集是事先没有定义的;在序列分类中,一个输入列表作为一个整体分类。
|
||||
|
||||
一个分类称为有监督的,如果它的建立基于训练语料的每个输入包含正确标签。有监督分类使用的框架图如 1.1 所示。
|
||||
|
||||

|
||||
|
||||
图 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 中显示了将语料数据划分成不同的子集。
|
||||
|
||||

|
||||
|
||||
图 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"] = "<START>"
|
||||
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"] = "<START>"
|
||||
features["prev-tag"] = "<START>"
|
||||
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%。
|
||||
|
||||

|
||||
|
||||
图 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 显示名字性别任务的决策树模型的例子。
|
||||
|
||||

|
||||
|
||||
图 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 说明了这一过程。
|
||||
|
||||

|
||||
|
||||
图 5.1:使用朴素贝叶斯分类器为文档选择主题的程序的抽象图解。在训练语料中,大多数文档是有关汽车的,所以分类器从接近“汽车”的标签的点上开始。但它会考虑每个特征的影响。在这个例子中,输入文档中包含的词`dark`,它是谋杀之谜的一个不太强的指标,也包含词`football`,它是体育文档的一个有力指标。每个特征都作出了贡献之后,分类器检查哪个标签最接近,并将该标签分配给输入。
|
||||
|
||||
个别特征对整体决策作出自己的贡献,通过“投票反对”那些不经常出现的特征的标签。特别是,每个标签的似然得分由于与输入值具有此特征的标签的概率相乘而减小。例如,如果词`run`在 12% 的体育文档中出现,在 10% 的谋杀之谜的文档中出现,在 2% 的汽车文档中出现,那么体育标签的似然得分将被乘以 0.12,谋杀之谜标签将被乘以 0.1,汽车标签将被乘以 0.02。整体效果是略高于体育标签的得分的谋杀之谜标签的得分会减少,而汽车标签相对于其他两个标签会显著减少。这个过程如 5.2 和 5.3 所示。
|
||||
|
||||

|
||||
|
||||
图 5.2:计算朴素贝叶斯的标签似然得分。朴素贝叶斯以计算每个标签的先验概率开始,基于每个标签出现在训练数据中的频率。然后每个特征都用于估计每个标签的似然估计,通过用输入值中有那个特征的标签的概率乘以它。似然得分结果可以认为是从具有给定的标签和特征集的训练集中随机选取的值的概率的估计,假设所有特征概率是独立的。
|
||||
|
||||
## 5.1 底层的概率模型
|
||||
|
||||
理解朴素贝叶斯分类器的另一种方式是它为输入选择最有可能的标签,基于下面的假设:每个输入值是通过首先为那个输入值选择一个类标签,然后产生每个特征的方式产生的,每个特征与其他特征完全独立。当然,这种假设是不现实的,特征往往高度依赖彼此。我们将在本节结尾回过来讨论这个假设的一些后果。这简化的假设,称为朴素贝叶斯假设(或独立性假设),使得它更容易组合不同特征的贡献,因为我们不必担心它们相互影响。
|
||||
|
||||

|
||||
|
||||
图 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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
763
docs/nlp/7.md
Normal file
@@ -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 所示。小框显示词级分词和词性标注,大框显示高级别的词块划分。每个这种较大的框叫做一个词块。就像分词忽略空白符,词块划分通常选择词符的一个子集。同样像分词一样,词块划分器生成的片段在源文本中不能重叠。
|
||||
|
||||

|
||||
|
||||
图 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: {<DT>?<JJ>*<NN>}" ❷
|
||||
|
||||
>>> 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() ❻
|
||||
```
|
||||
|
||||

|
||||
|
||||
## 2.2 标记模式
|
||||
|
||||
组成一个词块语法的规则使用标记模式来描述已标注的词的序列。一个标记模式是一个词性标记序列,用尖括号分隔,如`<DT>?>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: {<DT|PP\$>?<JJ>*<NN>} # chunk determiner/possessive, adjectives and noun
|
||||
{<NNP>+} # 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: {<NN><NN>} # 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: {<V.*> <TO> <V.*>}')
|
||||
>>> 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 所示。
|
||||
|
||||

|
||||
|
||||
图 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
|
||||
...
|
||||
|
||||
```
|
||||
|
||||

|
||||
|
||||
我们可以使用 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 = "<START>", "<START>"
|
||||
... 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 = "<START>", "<START>"
|
||||
... 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 = "<START>", "<START>"
|
||||
... else:
|
||||
... prevword, prevpos = sentence[i-1]
|
||||
... if i == len(sentence)-1:
|
||||
... nextword, nextpos = "<END>", "<END>"
|
||||
... 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: {<DT|JJ|NN.*>+} # Chunk sequences of DT, JJ, NN
|
||||
PP: {<IN><NP>} # Chunk prepositions followed by NP
|
||||
VP: {<VB.*><NP|PP|CLAUSE>+$} # Chunk verbs and their arguments
|
||||
CLAUSE: {<NP><VP>} # 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()
|
||||
```
|
||||
|
||||

|
||||
|
||||
## 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
|
||||
571
docs/nlp/8.md
Normal file
@@ -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 所示。
|
||||
|
||||

|
||||
|
||||
图 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]`。
|
||||
|
||||
注意
|
||||
|
||||
为了帮助我们更简便地通过产生式的右侧检索产生式,我们为语法创建一个索引。这是空间-时间权衡的一个例子:我们对语法做反向查找,每次我们想要通过右侧查找产生式时不必遍历整个产生式列表。
|
||||
|
||||

|
||||
|
||||
图 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 显示一个依存关系图,箭头从中心词指出它们的依赖。
|
||||
|
||||

|
||||
|
||||
图 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()
|
||||
```
|
||||
|
||||

|
||||
|
||||
## 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
|
||||
|
||||
```
|
||||
726
docs/nlp/9.md
Normal file
@@ -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
|
||||
BIN
docs/nlp/Images/05c7618d4554f83cfa6105c528703794.jpg
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
docs/nlp/Images/07e7d99633e4a107388f7202380cce55.jpg
Normal file
|
After Width: | Height: | Size: 964 B |
BIN
docs/nlp/Images/0ddbd56a410c886c77d1c72a84e27883.jpg
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
docs/nlp/Images/0de8ea07b37cc379da8aeb4a0c9fd5bf.jpg
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
docs/nlp/Images/0e6e187e03f69a6548e7daa4f95e1548.jpg
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
docs/nlp/Images/0e768e8c4378c2b0b3290aab46dc770e.jpg
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
docs/nlp/Images/0f4441cdaf35bfa4d58fc64142cf4736.jpg
Normal file
|
After Width: | Height: | Size: 952 B |
BIN
docs/nlp/Images/102675fd70e434164536c75bf7f8f043.jpg
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
docs/nlp/Images/1094084b61ac3f0e4416e92869c52ccd.jpg
Normal file
|
After Width: | Height: | Size: 300 KiB |
BIN
docs/nlp/Images/10a8a58e33a0a6b7fb71389ea2114566.jpg
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
docs/nlp/Images/10a910dd6de117ab7a0ab352519f7297.jpg
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
docs/nlp/Images/12573c3a9015654728fe798e170a3c50.jpg
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
docs/nlp/Images/13361de430cd983e689417c547330bbc.jpg
Normal file
|
After Width: | Height: | Size: 244 KiB |
BIN
docs/nlp/Images/13f25b9eba42f74ad969a74cee78551e.jpg
Normal file
|
After Width: | Height: | Size: 968 B |
BIN
docs/nlp/Images/14a0a2692f06286091f0cca17de5c0f3.jpg
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
docs/nlp/Images/14a15b73cb826ac3464754d6db3e9e54.jpg
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
docs/nlp/Images/160f0b5ad0e4b8745b925d63377a69ba.jpg
Normal file
|
After Width: | Height: | Size: 964 B |
BIN
docs/nlp/Images/17af2b7a4de652abd5c2c71a94cc1c7b.jpg
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
docs/nlp/Images/1ab45939cc9babb242ac45ed03a94f7a.jpg
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
docs/nlp/Images/1b33abb14fc8fe7c704d005736ddb323.jpg
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
docs/nlp/Images/1c54b3124863d24d17b2edec4f1d47e5.jpg
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
docs/nlp/Images/1cf5b2605018e587fa94db2ac671e930.jpg
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
docs/nlp/Images/21f757d2c1451cb55bc923dcb4d8d1ab.jpg
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
docs/nlp/Images/24589c2eb435b25724aed562d1d47617.jpg
Normal file
|
After Width: | Height: | Size: 964 B |
BIN
docs/nlp/Images/273b9ed1ea067e4503c00dbd193216e8.jpg
Normal file
|
After Width: | Height: | Size: 337 KiB |
BIN
docs/nlp/Images/27ffeabf8327e1810d6ac35642a72700.jpg
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
docs/nlp/Images/2ce816f11fd01927802253d100780b0a.jpg
Normal file
|
After Width: | Height: | Size: 123 KiB |
BIN
docs/nlp/Images/2cfe7a0ac0b6007d979cf3aecda8a9f5.jpg
Normal file
|
After Width: | Height: | Size: 292 KiB |
BIN
docs/nlp/Images/334be383b5db7ffe3599cc03bc74bf9e.jpg
Normal file
|
After Width: | Height: | Size: 968 B |
BIN
docs/nlp/Images/346344c2e5a627acfdddf948fb69cb1d.jpg
Normal file
|
After Width: | Height: | Size: 937 B |
BIN
docs/nlp/Images/37edef9faf625ac06477a0ab0118afca.jpg
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
docs/nlp/Images/3a93e0258a010fdda935b4ee067411a5.jpg
Normal file
|
After Width: | Height: | Size: 964 B |
BIN
docs/nlp/Images/3edaf7564caaab7c8ddc83db1d5408a3.jpg
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
docs/nlp/Images/4150e51ab7e511f8d4f72293054ceb22.jpg
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
docs/nlp/Images/431fed60785d71efd9010589288ca55d.jpg
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
docs/nlp/Images/43a0f901bdd8f343b2885d2b2e95b996.jpg
Normal file
|
After Width: | Height: | Size: 207 KiB |
BIN
docs/nlp/Images/484180fc6abc244116b30e57cb6c0cf5.jpg
Normal file
|
After Width: | Height: | Size: 253 KiB |
BIN
docs/nlp/Images/496754d8cdb6262f8f72e1f066bab359.jpg
Normal file
|
After Width: | Height: | Size: 968 B |
BIN
docs/nlp/Images/499f8953a7d39c034e6840bdacd99d08.jpg
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
docs/nlp/Images/4a87f1dccc0e18aab5ec599d8d8358d6.jpg
Normal file
|
After Width: | Height: | Size: 416 KiB |
BIN
docs/nlp/Images/4acbae4abe459cf45122fe134ff7672d.jpg
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
docs/nlp/Images/4b32a28b1aab7148420347abc990ee67.jpg
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
docs/nlp/Images/4b5cae275c53c53ccc8f2f779acada3e.jpg
Normal file
|
After Width: | Height: | Size: 937 B |
BIN
docs/nlp/Images/4cdc400cf76b0354304e01aeb894877b.jpg
Normal file
|
After Width: | Height: | Size: 434 KiB |
BIN
docs/nlp/Images/4cec5bd698a48f7e083696dd51ae9e7a.jpg
Normal file
|
After Width: | Height: | Size: 952 B |
BIN
docs/nlp/Images/513df73dfd52feca2c96a86dcc261c8b.jpg
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
docs/nlp/Images/532d5f3185ea7edaec68683d89a74182.jpg
Normal file
|
After Width: | Height: | Size: 242 KiB |
BIN
docs/nlp/Images/542fee25c56235c899312bed3d5ee9ba.jpg
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
docs/nlp/Images/55f4da85888c6e9974fea5360283035a.jpg
Normal file
|
After Width: | Height: | Size: 190 KiB |
BIN
docs/nlp/Images/56cee123595482cf3edaef089cb9a6a7.jpg
Normal file
|
After Width: | Height: | Size: 280 KiB |
BIN
docs/nlp/Images/5741a8d50a63172cbd2c825cda1b61d5.jpg
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
docs/nlp/Images/58a1097522dc6fbe24eddd96cfd6cbc9.jpg
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
docs/nlp/Images/5ac0acc015eeb4b0a40c6c23b7f95395.jpg
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
docs/nlp/Images/5c0c6dc75f29f253edaecb566d608aa3.jpg
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
docs/nlp/Images/5c3caa46daa29a053a04713bab6e4f03.jpg
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
docs/nlp/Images/5e197b7d253f66454a97af2a93c30a8e.jpg
Normal file
|
After Width: | Height: | Size: 206 KiB |
BIN
docs/nlp/Images/5eeb4cf55b6d18d4bcb098fc72ddc6d7.jpg
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
docs/nlp/Images/6372ba4f28e69f0b220c75a9b2f4decf.jpg
Normal file
|
After Width: | Height: | Size: 968 B |
BIN
docs/nlp/Images/63a8e4c47e813ba9630363f9b203a19a.jpg
Normal file
|
After Width: | Height: | Size: 964 B |
BIN
docs/nlp/Images/64864d38550248d5bd9b82eeb6f0583b.jpg
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
docs/nlp/Images/66d94cb86ab90a95cfe745d9613c37b1.jpg
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
docs/nlp/Images/67abca0731a79d664847dee1390f2e13.jpg
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
docs/nlp/Images/6ac827d2d00b6ebf8bbc704f430af896.jpg
Normal file
|
After Width: | Height: | Size: 964 B |
BIN
docs/nlp/Images/6ddd472200240ea6c0cab35349a8403e.jpg
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
docs/nlp/Images/6efeadf518b11a6441906b93844c2b19.jpg
Normal file
|
After Width: | Height: | Size: 964 B |
BIN
docs/nlp/Images/723ad3b660335fc3b79e7bd2c947b195.jpg
Normal file
|
After Width: | Height: | Size: 968 B |
BIN
docs/nlp/Images/74248e04835acdba414fd407bb4f3241.jpg
Normal file
|
After Width: | Height: | Size: 182 KiB |
BIN
docs/nlp/Images/77460905bcad52d84e324fc4821ed903.jpg
Normal file
|
After Width: | Height: | Size: 937 B |
BIN
docs/nlp/Images/78213332718eae7fffd6314dae12484e.jpg
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
docs/nlp/Images/78bc6ca8410394dcf6910484bc97e2b6.jpg
Normal file
|
After Width: | Height: | Size: 937 B |
BIN
docs/nlp/Images/7a979f968bd33428b02cde62eaf2b615.jpg
Normal file
|
After Width: | Height: | Size: 937 B |
BIN
docs/nlp/Images/7aee076ae156921aba96ac5d4f9ed419.jpg
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
docs/nlp/Images/7bbd845f6f0cf6246561d2859cbcecbf.jpg
Normal file
|
After Width: | Height: | Size: 962 B |
BIN
docs/nlp/Images/7c20d0adbadb35031a28bfcd6dff9900.jpg
Normal file
|
After Width: | Height: | Size: 968 B |
BIN
docs/nlp/Images/7e6ea96aad77f3e523494b3972b5a989.jpg
Normal file
|
After Width: | Height: | Size: 937 B |
BIN
docs/nlp/Images/7f27bfe5324e4d9573ddd210531a8126.jpg
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
docs/nlp/Images/7f97e7ac70a7c865fb1020795f6e7236.jpg
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
docs/nlp/Images/81e6b07f4541d7d5a7900508d11172bd.jpg
Normal file
|
After Width: | Height: | Size: 80 KiB |