Python自然语言处理

978-7-115-33368-1
作者: 【美】Steven Bird Ewan Klein Edward Loper
译者: 陈涛张旭崔杨刘海平
编辑: 陈冀康

图书目录:

详情

本书提供了非常易学的自然语言处理入门介绍,该领域涵盖从文本和电子邮件预测过滤,到自动总结和翻译等多种语言处理技术。你将学会编写 Python程序处理大量非结构化文本,并将理解用于分析书面通信内容和结构的主要算法。

图书摘要

版权信息

书名:Python自然语言处理

ISBN:978-7-115-33368-1

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

• 著    [美]Steven Bird Ewan Klein Edward Loper

  译     陈 涛 张 旭 崔 杨 刘海平

  责任编辑  陈冀康

• 人民邮电出版社出版发行  北京市丰台区成寿寺路11号

  邮编 100164  电子邮件 315@ptpress.com.cn

  网址 http://www.ptpress.com.cn

• 读者服务热线:(010)81055410

  反盗版热线:(010)81055315


Copyright© 2009 by O’Reilly Media,Inc.

Simplified Chinese Edition,jointly published by O’Reilly Media,Inc.and Posts & TelecomPress, 2013.Authorized translation of the English edition,2012 O’Reilly Media,Inc,the owner of all rights to publish and sell the same.

All rights reserved including the rights of reproduction in whole or in part in any form.

本书中文简体版由O’Reilly Media,Inc.授权人民邮电出版社出版。未经出版者书面许可,对本书的任何部分不得以任何方式复制或抄袭。

版权所有,侵权必究。


自然语言处理(Natural Language Processing,NLP)是计算机科学领域与人工智能领域中的一个重要方向。它研究能够实现人与计算机之间用自然语言进行有效通信的各种理论和方法,涉及所有用计算机对自然语言进行的操作。

本书是自然语言处理领域的一本实用入门指南,旨在帮助读者学习如何编写程序来分析书面语言。本书基于Python编程语言以及一个名为NLTK的自然语言工具包的开源库,但并不要求读者有Python编程的经验。全书共11章,按照难易程度顺序编排。第1章到第3章介绍了语言处理的基础,讲述如何使用小的Python程序分析感兴趣的文本信息。第4章讨论结构化程序设计,以巩固前面几章中介绍的编程要点。第5章到第7章介绍语言处理的基本原理,包括标注、分类和信息提取等。第8章到第10章介绍了句子解析、句法结构识别和句意表达方法。第11章介绍了如何有效管理语言数据。后记部分简要讨论了NLP领域的过去和未来。

本书的实践性很强,包括上百个实际可用的例子和分级练习。本书可供读者用于自学,也可以作为自然语言处理或计算语言学课程的教科书,还可以作为人工智能、文本挖掘、语料库语言学等课程的补充读物。


本书封面上的动物是露脊鲸,所有大型鲸鱼中最稀有的。可以通过它约占身体总长三分之一的巨大的头来识别。它生活在两个半球的大洋表面温带和凉爽的海洋中。露脊鲸的名字被认为得自捕鲸人,他们认为它“正是”要杀死并取油的鲸鱼。虽然自从20世纪30年代以来它就被保护,但露脊鲸仍然濒危物种。

大而笨重的露脊鲸通过其头部的老茧很容易区别于其他鲸鱼。它有一个广阔无背鳍的背部和从眼睛上面开始的长拱嘴。它的身体是黑色的,除了肚皮上的白色补丁。伤口和疤痕可能会呈现明亮的橙色,往往会被鲸虱子或cyamids感染。老茧——也在气孔附近出现,眼睛、下巴、上唇的上面——呈黑色或灰色。它有大的、形状如桨的鳍状肢和独特的V形吹气孔,由广泛分布在其头部的顶端的气孔发出,水柱可以上升到海洋表面16英尺以上。

露脊鲸以浮游生物包括像磷虾虾和桡足类为食。作为须鲸,它们有一串从每一侧上颌骨悬挂下来的225~250个边缘折叠板,那里应该是牙齿。呈黑色,长有7.2英尺。露脊鲸是海中食草动物,经常张着自己的嘴巴慢慢地游。水流进嘴里再通过须,猎物被困在舌头附近。

因为雌性要到10岁才能达到性成熟,在一年之久的妊娠后才能生下小鲸,所以露脊鲸数量增长缓慢。小露脊鲸一般跟随母亲一年。

露脊鲸遍布世界各地,但数量很少。一只露脊鲸通常单独或组成1~3只的小团体活动,但求偶时,它们可能形成30只的大队伍。与大多数须鲸一样,它们随季节性迁徙。在寒冷水域栖息觅食,然后迁移到温暖的水域繁殖和生产。虽然它们在哺乳季节可能游到远海,但露脊鲸会在沿海地区进行繁殖。有趣的是,许多雌性不会每年都回到这些沿海繁殖区,而是只在繁殖年来到该地区。其他年份它们去哪里了这仍是一个谜。

露脊鲸唯一的天敌是逆戟鲸和人类。当危机出现时,一组露脊鲸可能在一起围成一个圈,用尾巴指向外面,以阻止捕食。这种防御并非总是成功的,小鲸偶尔会与它们的母亲分开而被杀害。

露脊鲸是游泳最慢的鲸之一,尽管它们短期爆发可能达到10英里每小时。它们可以下潜到至少1000英尺,并潜水长达40分钟。即使经过多年的保护状态,露脊鲸仍然极度濒危。只在过去的15年中有证据表明它们在南半球的数量有所恢复。仍然不知道露脊鲸是否会在北半球存活。虽然目前没有捕杀,当前保护的问题包括船舶碰撞、捕鱼活动的冲突、栖息地的破坏、石油钻探和与其他鲸类可能的竞争冲突。露脊鲸没有牙齿,所以在某些情况下,耳骨和眼球晶体可以用来估计露脊鲸在死亡时的年龄。相信露脊鲸至少能活50年,但是有关它们寿命的数据很少。

封面图片来自多佛尔画报档案。


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最近完成了宾夕法尼亚大学自然语言处理的机器学习博士学位。爱德华是史蒂芬在2000年秋季计算语言学研究生课程的学生,也是教师助手和NLTK开发的成员。除了NLTK,他帮助开发了用于记录和测试Python软件的两个包:epydoc和doctest。


这是一本关于自然语言处理的书。所谓“自然语言”,是指人们日常交流使用的语言,如英语、印地语、葡萄牙语等。相对于编程语言和数学符号这样的人工语言,自然语言随着一代代的传递而不断演化,因而很难用明确的规则来确定。从广义上讲,“自然语言处理”(Natural Language Processing,NLP)包含所有用计算机对自然语言进行的操作,从最简单的通过计数词汇出现的频率来比较不同的写作风格,到最复杂的完全“理解”人所说的话,或至少达到能对人的话语作出有效反应的程度。

NLP的技术应用日益广泛。例如:手机和手持电脑对输入法联想提示和手写识别的支持;网络搜索引擎能搜索到非结构化文本中的信息;机器翻译能把中文文本翻译成西班牙文。通过提供更自然的人机界面和获取存储信息的高级手段,语言处理正在这个多语种的信息社会中扮演着更核心的角色。

这本书提供自然语言处理领域的入门指南。它可以用来自学,也可以作为自然语言处理或计算语言学课程的教科书,或是作为人工智能、文本挖掘、语料库语言学课程的补充读物。本书实用性强,包括上百个实例和分级练习。

本书基于Python编程语言及名为自然语言工具包(Natural Language Toolkit, NLTK)的开源库。NLTK包含大量的软件、数据和文档,所有这些都可以从http://www.nltk.org/上免费下载。NLTK的发行版本支持Windows、Macintosh和UNIX平台。强烈建议你下载Python和NLTk,与我们一起尝试书中的例子和练习。

从科学、经济、社会和文化因素来看,NLP十分重要。NLP正在迅速成长,其中很多理论和方法在大量新的语言技术中得到广泛应用。所以对很多人来说掌握NLP知识十分重要。在应用领域,包括从事人机交互、商业信息分析、Web软件开发的人;在学术界,包括从事人文计算学、语料库语言学到计算机科学和人工智能领域的人。(学术界的很多人把NLP称为“计算语言学”)

本书旨在帮助所有想要学习编写程序来分析书面语言的人,不管他们以前的编程经验如何。

初学编程?

本书的前几章适合没有编程经验的读者,只要你不怕应对新概念和学习新的计算机技能。书中的例子和数以百计的分级练习,你都可以亲自尝试一下。如果你需要关于Python的更一般的介绍,http://docs.python.org/给出了Python资源列表。

初学Python?

有经验的程序员可以很快掌握书中的Python代码,而把更多精力专注于自然语言处理。所有涉及的Python功能都经过精心解释和举例说明,你很快就会体会到Python在这些应用领域的妙用。书中的语言索引会帮你查找书中的相关论述。

已经精通Python?

你可以浏览一下Python的例子并且钻研从第1章开始就提到的语言分析材料。很快你就能在这个神奇的领域展现你的技能。

本书是一本介绍NLP的实用书籍。你将通过例子学习编写真正的程序,并通过实践验证自己想法的价值。如果你没有学过编程,本书将教你如何编程。与其他编程书籍不同的是,我们提供了丰富的NLP实例和练习。我们撰写本书时讲究探究原理,无论是严谨的语言学还是计算分析学,我们不回避所涉及的任何基础理论。我们曾经试图在理论与实践之间寻求折中,确定它们之间的联系与边界。最终我们认识到如果不能寓教于乐,几乎无法实现这个目标,所以我们竭尽所能写入了很多既有益又有趣的应用和例子,有的甚至有些异想天开。

请注意本书并不是一本工具书。本书讲述的Python和NLP是精心挑选的,并通过教程的形式展现的。关于参考材料,请查阅http://python.org/http://www.nltk.org/,那里有大量可搜索的资源。

本书也不是高深的计算机科学文章。书中的内容属于入门级和中级,目标读者是那些想要学习如何使用Python和自然语言分析包来分析文本的人。若想学习NLTK中更高级的算法,你可以查阅http://www.nltk.org/中的Python代码库,或查询在本书中引用的其他文献。

通过钻研本书,你将学到如下内容:

根据读者知识背景和学习NLP的动机不同,从本书中获得的技能和知识也将不同,详情见表P-1。

表P-1 目标和背景不同的读者,阅读本书可获得的技能和知识

目标

艺术与人文背景

科学与工程背景

语言分析

操控大型语料库,设计语言模型,验证由经验得出的假设

使用数据建模、数据挖掘和知识发掘的技术来分析自然语言

语言技术

应用NLP技术构建高效的系统来处理语言学任务

在高效的语言处理软件中使用语言学算法和数据结构

本书前几章按照概念的难易程度编排。先是实用地介绍语言处理,讲述如何使用小的Python程序分析感兴趣的文本信息(第1~3章)。接着是结构化程序设计章节(第4章),用来巩固散布在前面几章中学习的编程要点。之后,加快速度,我们用一系列章节讲述语言处理的基本原理:标注、分类和信息提取(第5~7章)。接下来的3章探索了句子解析、句法结构识别和句意表达方法构建(第8~10章)。最后一章重点讲述了如何有效管理语言数据(第11章)。本书结尾处的后记简要讨论了NLP领域的过去和未来。

每一章中我们都在两种不同的叙述风格间切换。一种风格是以自然语言为主线。我们分析语言,探索语言学概念,在讨论中使用编程的例子。我们经常会使用尚未系统介绍的Python结构,这样你可以在钻研这些程序如何运作的细节之前了解它们的用处。就像学习一门外语的惯用表达一样,你能够买到好吃的糕点而不必先学会复杂的提问句型。另一种风格是以程序设计语言为主线。我们分析程序、探索算法,而不以语言学例子为主。

每章结尾都有一系列分级练习,用于巩固所学的知识。练习按照如下的标准分级:○初级练习,对范例代码稍加修改等简单的练习;◑中级练习,深入探索所学知识的某个方面,需要仔细地分析和设计;●高级练习,开放式的任务,挑战你对所学知识的理解并要求你独立思考解决的方案(新学编程的读者可以跳过这些)。

每一章都有深入阅读环节和放置在http://www.nltk.org/网站上的“额外内容”部分,用来介绍更深入的相关材料及一些网络资源。所有实例代码都可从网上下载。

Python是一种简单但功能强大的编程语言,其自带的函数非常适合处理语言数据。Python可以从http://www.python.org/免费下载,并能够在各种平台上安装运行。

下面的4行Python程序就可以操作file.txt文件,输出所有后缀是“ing”的词。

   >>> 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://www.python.org/about/success/列举了许多成功使用Python的例子。

NLTK定义了使用Python进行NLP编程的基础工具。它提供了与自然语言处理相关的数据表示基本类,词性标注、文法分析、文本分类等任务的标准接口及这些任务的标准实现,可以组合起来解决复杂的问题。

NLTK自带大量文档。作为本书的补充,http://www.nltk.org/网站提供的API文档涵盖了工具包中每一个模块、类和函数,详细说明了各种参数,以及用法示例。该网站还为广大用户、开发人员和导师提供了很多包含大量的例子和测试用例的HOWTO。

为了充分利用好本书,你应该安装一些免费的软件包。http://www.nltk.org/上有这些软件包当前的下载链接和安装说明。

Python

本书的例子都假定你正在使用Python 2.4或2.5版本。一旦NLTK依赖的库支持Python 3.0,我们将把NLTK移植到Python 3.0。

NLTK

本书的代码示例使用NLTK 2.0版本。NLTK的后续版本是兼容的。

NLTK-Data

包含本书中所分析和处理的语言语料库。

NumPy(推荐)

这是一个科学计算库,支持多维数组和线性代数,在某些概率计算、标记、聚类和分类任务中会用到。

Matplotlib(推荐)

这是一个用于数据可视化的2D绘图库,在产生线图和条形图的程序例子中会用到。

NetworkX(可选)

这是一个用于存储和操作由节点和边组成的网络结构的函数库。实现可视化语义网络还需要安装Graphviz库。

Prover9(可选)

这是一个使用一阶等式逻辑的定理自动证明器,用于支持语言处理中的推理。

NLTK创建于2001年,最初是宾州大学计算机与信息科学系计算语言学课程的一部分。从那以后,在数十名贡献者的帮助下不断发展壮大。如今,它已被几十所大学的课程所采纳,并作为许多研究项目的基础。表P -2列出了NLTK的一些最重要的模块。

表P-2 语言处理任务与相应NLTK模块及功能描述

语言处理任务

NLTK模块

功能描述

获取语料库

nltk.corpus

语料库和词典的标准化接口

字符串处理

nltk.tokenize, nltk.stem

分词、句子分解、提取主干

搭配探究

nltk.collocations

t-检验、卡方、点互信息

词性标识符

nltk.tag

n-gram、backoff、Brill、HMM、TnT

分类

nltk.classify, nltk.cluster

决策树、最大熵、朴素贝叶斯、EM、k-means

分块

nltk.chunk

正则表达式、n-gram、命名实体

解析

nltk.parse

图表、基于特征、一致性、概率性、依赖项

语义解释

nltk.sem, nltk.inference

λ演算、一阶逻辑、模型检验

指标评测

nltk.metrics

精度、召回率、协议系数

概率与估计

nltk.probability

频率分布、平滑概率分布

应用

nltk.app, nltk.chat

图形化的关键词排序、分析器、WordNet查看器、聊天机器人

语言学领域的工作

nltk.toolbox

处理SIL工具箱格式的数据

NLTK设计中的4个主要目标如下。

简易性

提供直观的框架和大量模块,使用户获取NLP知识而不必陷入像标注语言数据那样繁琐的事务中。

一致性

提供具有一致的接口和数据结构并且方法名称容易被猜到的统一框架。

可扩展性

提供一种结构使得新的软件模块可以方便添加进来,模块包括同一任务中不同的或相互冲突的实现方式。

模块化

提供可以独立使用而与工具包的其他部分无关的组件。

对比上述目标,我们回避了工具包的潜在实用性。首先,虽然工具包提供了广泛的工具,但它不是面面俱全的。第一,它是一个工具包而不是一个系统,它将会随着NLP领域一起发展。第二,虽然这个工具包的效率足以支持实际的任务,但它运行时的性能还没有高度优化。这种优化往往涉及更复杂的算法或使用C或C++等较低一级的编程语言来实现。这将使得工具包的可读性变差且更难以安装。第三,我们试图避开巧妙的编程技巧,因为我们相信清楚直白的实现比巧妙却可读性差的方法好。

自然语言处理一般是在高年级本科生或研究生阶段开设的为期一个学期的课程。很多教师都发现,在如此短的时间里涵盖理论和实践两个方面是十分困难的。有些课程注重理论而排除掉实践练习,这剥夺了学生编写程序自动处理语言带来的挑战和兴奋感。另一些课程仅仅教授语言学编程而不包含任何重要的NLP内容。最初开发NLTK就是为了解决这个问题,无论学生之前是否具有编程经验,都能使教师在一个学期里同时教授大量理论和实践成为可能。

在所有NLP教学大纲中算法和数据结构部分都十分重要。它们本身可能非常枯燥,而NLTK提供的交互式图形用户界面能让读者一步一步看到算法过程,使它们变得鲜活。大多数NLTK组件都有一个无需用户输入任何数据就能执行有趣任务的示范性例子。学习本书的一种有效方法就是通过交互式重现书中的范例,把它们输入到Python会话控制台,观察它们的功能,尝试修改它们去探索经验性问题或者理论性问题。

本书包含了数百个练习,可作为学生作业。最简单的练习包括用指定的方式修改已有的程序片段来回答具体的问题。另一方面,NLTK为研究生水平的研究项目提供了一个灵活的框架,包括所有的基本数据结构和算法的标准实现,几十个广泛使用的数据集(语料库)的接口,以及一个灵活可扩展的体系结构。NLTK网站上还有其他资源可以支持NLTK教学。

我们相信本书是唯一能为学生提供在学习编程的环境中学习NLP的综合性教程。各个章节和练习通过与NLTK紧密结合,并将各章材料有序分割开,为学生(即使是那些以前没有编程经验的学生)提供一个实用的NLP的入门指南。学完这些材料后,学生能准备好尝试一本更加深层次的教科书,例如:Speech and Language Processing(《语音和语言处理》),作者是Jurafsky和Martin(Prentice Hall出版社,2008年)。

本书介绍编程概念的顺序与众不同。以一个重要的数据类型:字符串列表(链表)开始,然后介绍重要的控制结构,如推导和条件式等。这些常用知识允许我们在一开始就做一些有用的语言处理。当有了这样动机,我们再回过头来系统地介绍一些基础概念,如字符串、循环、文件等。这种方法同更传统的方法相比,达到了同样的效果而不必要求读者对编程感兴趣。

表P-3列出了两个课程计划表。第一个适用于艺术人文专业背景的读者,第二个适用于科学与工程背景的读者。其他的课程计划应该涵盖前5章,然后把剩余的时间投入单独的领域,例如:文本分类(第6、7章)、文法(第8、9章)、语义(第10章)或者语言数据管理(第11章)。

表P-3 课程计划建议(每一章近似的课时数)

艺术人文专业

理工科

第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。例如:“Natural Language Bocessing with Rthow,Steven Bird,Ewan Klein和Edward Loper。版权所有2009 Steven Bird, Ewan Klein和Edward Loper, 978-0-596-51649-9。”如果你觉得你使用本书的例子代码超出了上面列举的一般用途或许可,请通过permissions@oreilly.com随时联系我们。

当你看到任何你喜爱的技术书的封面上印有Safari®在线丛书的图标时,这意味着这本书可以在O'Reilly网络Safari书架上找到。

Safari提供比电子书更好的解决方案。它是一个虚拟图书馆,你可以轻松搜索数以千计的顶尖技术书籍,可剪切和粘贴例子代码,并下载一些章节,在你需要最准确最新的信息时快速找到答案。欢迎免费试用http://my.safaribooksonline.com

关于本书的意见和咨询请写信给出版商。

O'Reilly Media公司

1005 Gravenstein Highway North

Sebastopol, CA 95472

中国:

北京市西城区西直门南大街2号成铭大厦C座807(100035)

奥莱利技术咨询(北京)有限公司

我们为本书的勘误表、例子等信息制作了一个网页。你可以访问这个页面:

http://www.oreilly.com/catalog/9780596516499

作者通过NLTK网站提供了各章的其他材料:

http://www.nltk.org/

要发表评论或询问有关这本书的技术问题,发送电子邮件至:

bookquestions@oreilly.com

欲了解更多有关我们的书籍、会议、资源中心和O'Reilly网络的信息,请参阅我们的网站:

http://www.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开发社区的成员的努力就不会产生这本书,他们为建设和壮大NLTK无私奉献了他们的时间和专业知识,他们的名字都记录在NLTK网站上。

非常感谢美国国家科学基金会、语言数据联盟、Edward Clarence Dyason奖学金、宾州大学、爱丁堡大学和墨尔本大学对本书相关工作的支持。

感谢Julie Steele、Abby Fox、Loranah Dimant及其他O’Reilly团队成员。他们组织大量NLP和Python社区成员全面审阅我们的手稿,还主动为满足我们的需要而定制O'Reilly的生成工具。感谢他们一丝不苟的审稿工作。

最后,深深地感谢我们的合伙人,他们是Kay、Mimo和Jee。感谢在我们写作本书的几年里他们付出的关心、耐心和支持。我们希望我们的孩子——Andrew、Alison、Kirsten、Leonie和Maaike——能从这些页面中感觉到我们对语言和计算的热情。

这本书的版税将用来支持自然语言工具包的发展。

图P-1. Edward Loper、Ewan Klein和Steven Bird,斯坦福大学,2007年7月


我们能够很容易地得到数百万数量级的文本。假设我们会写一些简单的程序,那可以用它来做些什么?本章将解决以下几个问题。

(1)通过将技术性较简单的程序与大规模文本结合起来,我们能实现什么?

(2)如何自动地提取出关键字和词组,用来总结文本的风格和内容?

(3)Python编程语言为上述工作提供了哪些工具和技术?

(4)自然语言处理中有哪些有趣的挑战呢?

本章分为风格完全不同的两部分。在1.1节,我们将进行一些与语言相关的编程练习而不去解释它们是如何实现的。在1.2节,我们将系统地回顾关键的编程概念。我们使用章节标题来区分这两种风格,而后面几章则不像前面一样,是将两种风格混合在一起,不作明显的区分。我们希望这种介绍风格能使你对将要出现的内容有一个真实的体会,与此同时,介绍中还涵盖了语言学与计算机科学的基本概念。如果你对这两个方面已经有了基本的了解,可以直接从1.5节开始学习。我们将在后续的章节中重复所有要点,如果错过了什么,你可以很容易地在http://www.nltk.org/上查询在线参考材料。如果这些材料对你而言是全新的,那么本章所提出的问题比它还要多,这些问题将在本书的其余部分中进行讨论。

我们都对文本非常熟悉,因为我们每天都在进行阅读和写作。在本书中,把文本视为编写程序的原始数据,并通过很多有趣的编程方式来处理和分析文本。但在能写这些程序之前,必须得从了解Python解释器开始。

Python入门

Python与用户友好交互的方式之一包括你可以在交互式解释器直接输入代码——解释器将运行你的Python代码的程序。你可以通过一个叫做交互式开发环境(Interactive Development Environment,IDLE)的简单图形接口来访问Python解释器。在Mac上,你可以在“Applications→MacPython”中找到;在Windows中,你可以在“程序→Python”中找到。在UNIX下,你可以在shell输入“idle”来运行Python(如果没有安装,尝试输入python)。解释器将会输入关于你的Python的版本简介,请检查你是否运行在Python 2.4或2.5(这里是2.5.1)。

Python 2.5.1 (r251:54863, Apr 15 2008, 22:57:26)
[GCC 4.0.1 (Apple Inc. build 5465)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>

如果你无法运行Python解释器,可能是由于没有正确安装Python。请访问http://python.org/查阅详细操作说明。

提示符>>>表示Python解释器正在等待输入。复制这本书的例子时,自己不要键入>>>。现在,把Python当作计算器使用。

>>> 1 + 5 * 2 - 3
8
>>>

一旦解释器完成计算并显示出答案,提示符就会重新出现。这表示Python解释器在等待另一个指令。

 轮到你来

输入几个自己的表达式。可以使用星号(*)表示乘法,左斜线表示除法,可以用括号括起表达式。请注意:除法并不总是像你期望的那样。当输入1/3时,是整数除法(小数会被四舍五入),当输入1.0/3.0时,是“浮点数”(或十进制)除法。要想获得通常平时期望的除法(在Python3.0中的标准),需要输入:from __future__import division。

前面的例子展示了如何使用Python交互式解释器,体验Python语言中各种表达式,看看它们能做些什么。现在让我们尝试一个无意义的表达式,看看解释器将如何处理。

>>> 1 + 
 File "<stdin>", line 1
   1 +
    ^
 SyntaxError: invalid syntax
 >>>

结果产生了一个语法错误。在Python中,指令以加号结尾是没有意义的。Python解释器会指出发生错误的行(<stdin>的第1行,<stdin>表示“标准输入”)。

现在我们学会使用Python解释器了,已经准备好开始处理语言数据了。

NLTK入门

首先应该安装NLTK,可以从http://www.nltk.org/上免费下载。按照说明下载适合你的操作系统的版本。

一旦安装完成,便可像前面那样启动Python解释器。在Python提示符后面输入下面两行命令来安装本书所需的数据,然后选择book,如图1-1所示。

>>> import nltk
>>> nltk.download()

图1-1 下载NLTK图书合集:使用nltk.download()浏览可用的软件包。下载器上的Collections选项卡显示软件包如何被打包分组,然后选择book标记所在行,可以获取本书所有例子和练习需要的全部数据。这些数据需要100MB硬盘空间,包括约30个压缩文件。完整的数据集(即下载器中的所有)大约是这个大小的5倍(在本书写作期间),并且还在不断扩充

一旦数据被下载到你的机器,你就可以使用Python解释器加载其中的一些了。第一步是在Python提示符后输入一个特殊的命令,告诉解释器去加载一些我们要用的文本:from nltk.book import * 。这条语句是说“从NLTK的book模块中加载所有的条目”。book模块包含你阅读本章时所需的所有数据。在输出欢迎信息之后,将会加载一些书的文本(这将需要几秒钟)。下面就是你需要输入的命令及输出的结果,注意拼写和标点符号的正确性,记住不要输入>>>。

>>> 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提示符后输入它们的名字即可。

>>> text1
<Text: Moby Dick by Herman Melville 1851>
>>> text2
<Text: Sense and Sensibility by Jane Austen 1811>
>>>

现在我们可以使用Python解释器和这些数据,准备开始了。

搜索文本

除了简单地阅读文本之外,还有很多方法可以用来查看文本内容。词语索引视图可以显示指定单词的出现情况,同时还可以显示一些上下文。下面我们输入text1并在后面跟一个点,再输入函数名concordance,然后将monstrous放在括号里,用来查找《白鲸记》中的词monstrous

>>> text1.concordance("monstrous")
Building index...
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 Himm
they might scout at Moby Dick as a monstrous fable , or still worse and more de
h 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
>>>

 轮到你来

尝试搜索其他词。为了方便重复输入,你也许会用到上箭头、Ctrl-↑或者Alt-p以获取之前输入的命令,然后修改要搜索的词。你也可以尝试搜索已经列入的其他文本。例如:使用text2.concordance("affection")搜索《理智与情感》中的词affection;使用text3.concordance("lived")搜索《创世纪》找出某人活了多久;你也可以看看text4,《就职演说语料》,回到1789年去看看那时英语使用的例子,并且搜索如nation, terror, god这样的词,看看随着时间的推移这些词的使用有何不同;同样还有text5,《NPS聊天语料库》,你可以在里面搜索一些网络用语,如im, ur, lol。(注意这个语料库是未经审查的!)

通过一段时间对这些文本的研究,我们希望你能对语言的丰富性和多样性有一个新的认识。在下一章中,你将学习如何获取更广泛的文本,包括英语以外其他语言的文本。

关键词索引让我们可以看到上下文中的词。例如:我们看到monstrous出现在文章中,如the___picturesthe____ size。还有哪些词出现在相似的上下文中?我们可以通过在被查询的文本名后添加函数名similar,然后在括号中插入相关词的方法来查找到。

>>> text1.similar("monstrous")
Building word-context index...
subtly impalpable pitiable curious imperial perilous trustworthy
abundant untoward singular lamentable few maddens horrible loving lazy
mystifying christian exasperate puzzled
>>> text2.similar("monstrous")
Building word-context index...
very exceedingly so heartily a great good amazingly as sweet
remarkably extremely vast
>>>

可以发现从不同的文本中能够得到不同的结果。Austen(奥斯丁,英国女小说家)在词汇的使用上与Melville完全不同。对于她来说,monstrous是正面的意思,有时它的功能像词very一样用作强调成分。

我们可以使用函数common_contexts研究共用两个或两个以上词汇的上下文,如monstrousvery。使用方括号和圆括号将这些词括起来,中间用逗号分割。

>>> text2.common_contexts(["monstrous", "very"])
be_glad am_glad a_pretty is_pretty a_lucky
>>>>

 轮到你来:

挑选另一对词,使用similar()和common_contexts()函数比较它们在两个不同文本中的用法。

自动检测出现在文本中的特定的词,并显示同一上下文中出现的其他词。我们也可以判断词在文本中的位置:从文本开头算起有多少词出现。这个位置信息可以用离散图表示。每一列代表一个单词,每一行代表整个文本。在图1-2中,我们看到在过去220年中的一些显著的词语用法模式(在一个由就职演说语料首尾相连组合的人工文本中)。可以利用下面的方法画出离散图。你也许会想尝试更多的单词(如:libertyconstitution)和不同的文本。你能在看到这幅图之前预测一个单词的分布吗?如前所述,保证引号、逗号、中括号及小括号的使用完全正确。

>>> text4.dispersion_plot(["citizens", "democracy", "freedom", "duties", "America"])
>>>

图1-2 美国总统就职演说词汇分布图:可以用来研究随时间推移语言使用上的变化

 重要事项:

为了画出本书中用到的图形,你需要安装Python的NumPy和Matplotlib程序包。请参阅http://www.nltk.org/上的安装说明。

现在轻松一下,尝试以上述不同风格产生一些随机文本。要做到这一点,我们需要输入后面跟着函数名generate的文本名称。(需要带括号,但括号里什么也没有。)

>>> 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
>>>

请注意,第一次运行此命令时,由于要搜集单词序列的统计信息,因而执行速度比较慢。每次运行后,输出的文本都会不同。现在尝试产生就职演说风格或互联网聊天室风格的随机文本。虽然文本是随机的,但它重复使用了源文本中常见的单词和短语,从而使我们能感觉到它的风格和内容。

在generate产生输出时,标点符号与前面的单词分开。虽然这不是正确的英文格式,但我们之所以这么做是为了确保文字和标点符号是彼此独立的。更多关于这方面的内容将在第3章学习。

计数词汇

在前面例子中出现的文本中,最明显的地方在于它们所使用的词汇不同。在本节中,我们将看到如何使用计算机并以各种有用的方式来计数词汇。像以前一样,你可以马上开始使用Python解释器进行试验,即使你可能还没有系统地研究过Python。修改这些例子并测试一下你对它们的理解程度,尝试一下本章结尾的练习题。

首先,让以文本中出现的单词和标点符号为单位算出文本从头到尾的长度。使用函数len获取长度,参考《创世纪》中使用的例子。

>>> len(text3)
44764
>>>

《创世纪》有44764个单词和标点符号,也被称作“标识符”。标识符是表示一组字符序列——如:hairy、his或者:)——的术语。当计算文本中标识符的个数时,如to be or not to be这句话,我们计算的是这些序列出现的次数。因此,例句中出现了tobe各两次,ornot各一次。然而在例句中只有4个不同的单词。《创世纪》中有多少不同的单词?如果要用Python来回答这个问题,就不得不稍微改变一下提出的问题。因为一个文本词汇表只是它用到的标识符的集合,在集合中所有重复的元素都只算一个。在Python中可以使用命令:set(text3)来获得text3的词汇表。这样做之后,屏幕上的很多词就会被掠过。现在尝试以下操作。

>>> 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来获得这个数值。尽管书中有44764个标识符,但只有2789个不同的词汇或“词类型”。词类型是指一个词在一个文本中独一无二的出现或拼写形式。也就是说,这个单词在词汇表中是唯一的。计数的2789个项目中包括标点符号,所以把它们称作唯一项目类型而不是词类型。

现在,开始对文本词汇丰富度进行测量。下面的例子展示的结果含义为每个词平均被使用了16次(应该确保Python使用的是浮点除法)。

>>> from __future__ import division
>>> len(text3) / len(set(text3))
16.05 0197203298673
>>>

接下来,专注于特定的词。计数一个单词在文本中出现的次数,再计算一个特定词在文本中占据的百分比。

>>> text3.count("smote")
5
>>> 100 * text4.count('a') / len(text4)
1.46 43016433938312
>>>

 轮到你来:

text5中lol出现了多少次?它占文本全部词数的百分比是多少?

也许你想要对几个文本重复进行这些计算,但重新输入公式是很乏味的。方法是可以自己命名一个任务,如“lexical_diversity”或“percentage”,然后用一个代码块关联它。这样,你只需输入一个很短的名字就可以代替一行或多行Python代码,而且想用多少次就用多少次。执行一个任务的代码段叫做函数。使用关键字def给函数定义一个简短的名字。下面的例子演示的是如何定义两个新的函数,lexical_diversity()和percentage()。

>>> def lexical_diversity(text): ①
...    return len(text) / len(set(text)) ②
...
>>> def percentage(count, total): ③
...    return 100 * count / total
...

 注意!

当遇到第一行末尾的冒号时,Python解释器提示符由>>>变为...。...提示符表示Python期望的是在后面出现一个缩进代码块。缩进由你决定只需输入4个空格或是敲击Tab键。要结束一个缩进代码段,只需输入一个空行。

在lexical_diversity()的定义中,指定了一个text参数。这个参数是计算文本词汇多样性时的一个“占位符”,并在使用函数时,重现在运行的代码段中。类似地,percentage()定义了两个参数:count和total。

只要Python知道了lexical_diversity()和percentage()是指定代码段的名字,我们就可以继续使用这些函数了。

>>> lexical_diversity(text3)
16.05 0197203298673
>>> lexical_diversity(text5)
7.42 00461589185629
>>> percentage(4, 5)
80.0
>>> percentage(text4.count('a'), len(text4))
1.46 43016433938312
>>>

简要重述一下,使用或者说是调用一个如lexical_diversity()这样的函数时,只要输入它的名字并在后面跟一个左括号,再输入文本名字,然后是右括号即可。这些括号经常出现,它们的作用是将任务名——如:lexical_diversity()——与任务将要处理的数据——如:text3分割开。调用函数时放在参数位置的数据值叫做函数的实参

在本章中,你已经遇到了一些函数,如:len(),set()和sorted()。通常会在函数名后面加一对空括号,例如len(),这只是为了表明这是一个函数而不是其他的Python表达式。函数是编程中的一个重要概念,我们只是在一开始提到了它们,为了是让新手体会到编程的强大和它的创造力。如果你现在觉得有点混乱,请不要担心。

稍后将学习如何使用函数列表显示数据,见表1-1。表中每一行包含了不同数据相同的计算,将使用函数进行这种重复性的工作。

表1-1 Brown语料库中各种文体的词汇多样性

体  裁

标 识 符

类  型

词汇多样性

技能和爱好

82345

11935

6.9

幽默

21695

5017

4.3

小说:科学

14470

3233

4.5

新闻:报告文学

100554

14394

7.0

小说:浪漫

70022

8452

8.3

宗教

39399

6373

6.2

大家已经学习过Python编程语言的一些重要元素。下面进行简单的系统复习。

链表

文本是什么?一方面,它是一页纸上的符号序列,就像这页纸一样。另一方面,它是章节的序列,每一章由小节序列组成,这些小节由段落序列组成,以此类推。然而,对于我们而言,认为文本不外乎是单词和标点符号的序列。下面是如何展示Python中《白鲸记》的开篇句。

>>> sent1 = ['Call', 'me', 'Ishmael', '.']
>>>

在提示符后面,输入自己命名的sent1,后跟一个等号,然后是一些引用的词汇,中间以逗号分割并用括号包围。方括号里的内容在Python中叫做链表,是存储文本的方式。可以通过输入名字来查阅文本。同样可以查询文本的长度,甚至可以在自己的函数lexical_diversity()中使用

>>> sent1①
['Call', 'me', 'Ishmael', '.']
>>> len(sent1) ②
4
>>> lexical_diversity(sent1) ③
1.0
>>>

定义一些链表,将每个文本开始的句子定义为sent2…sent9。下面只检查其中的两个。你可以在Python解释器中查看其余的(如果得到的是一个错误表达:sent2没有定义,你需要先输入from nltk.book import *)。

>>> 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.1节中的其他Python操作,如:sorted(ex1),len(set(ex1)),ex1.count('the')。

令人惊喜的是,可以对链表使用Python加法运算。两个链表相加能够创造出一个新的链表,包括第一个链表的全部,并附着第二个链表的全部。

>>> ['Monty', 'Python'] + ['and', 'the', 'Holy', 'Grail'] ①
['Monty', 'Python', 'and', 'the', 'Holy', 'Grail']

这种加法操作的特殊用途叫做连接;它将多个链表组合为一个链表。可以通过把句子连接起来组成一个文本。

不必逐字地输入链表,可以使用简短的名字来引用预先定义好的链表。

>> sent4 + sent1
['Fellow', '-', 'Citizens', 'of', 'the', 'Senate', 'and', 'of', 'the',
'House', 'of', 'Representatives', ':', 'Call', 'me', 'Ishmael', '.']
>>>

如果想要在链表中增加一个单独的元素该如何做?这种操作叫做追加。当对一个链表使用append()时,链表自身会随着操作而更新。

>>> sent1.append("Some")
>>> sent1
['Call', 'me', 'Ishmael', '.', 'Some']
>>>

索引列表

正如已经看到的,Python中的文本是一个词汇的链表,用括号和引号来表示。就像处理一页普通的文本,我们可以使用len(text1)来计算text1的全部词数,使用text1.count('heaven')来计算一个文本中特定词出现的次数,如heaven

稍微耐心些,我们可以挑选出一篇文本中的第1个、第173个甚至第14278个词。类似的,也可以通过在链表中出现的次序找出Python链表的元素。表示这个位置的数字叫做这个元素的索引。在文本名称后面的方括号里写下索引,Python就会显示出文本中这个索引处——例如文本中第173个词。

>>> text4[173]
'awaken'
>>>

也可以反过来做;找出一个词第一次出现时的索引。

>>> text4.index('awaken')
173
>>>

索引是一种常见的用来获取文本中词汇的方式,或者,更通俗地讲,任何列表中的元素。通过Python也可以获取子链表,从大文本中任意抽取语言片段,术语叫做切片

>>> 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']
>>>

索引还有一些微妙之处,我们将在下面的句子中体会这些。

>>> sent = ['word1', 'word2', 'word3', 'word4', 'word5',
...     'word6', 'word7', 'word8', 'word9', 'word10']
>>> sent[0]
'word1'
>>> sent[9]
'word10'
>>>

需要注意的是,索引从零开始:第0个元素写作sent[0],其实是第1个词“word1”;而句子的第9个元素是“word10”。原因很简单:Python从计算机内存中的链表获取内容的时候,需要告诉它向前多少个元素。因此,向前0个元素使它留在第一个元素上。

这种从零算起的做法刚开始接触会有些混乱,但这是现代编程语言普遍使用的。19XY是20世纪中的一年,如果你已经掌握了这样的计数世纪的系统,或者如果你生活在一个建筑物楼层编号从1开始的国家,你很快就会掌握它的窍门,步行n-1级楼梯能够到达第n层。

现在,如果我们不小心使用的索引量过大就会产生错误。

>>> sent[10] 
Traceback (most recent call last):
 File "<stdin>", line 1, in ?
IndexError: list index out of range
>>>

这不是语法错误,因为程序片段在语法上是正确的。相反,它是一个运行时错误,它会产生一个回溯消息显示错误的上下文,并标注错误的名称:IndexError,以及简要的解释说明。

再次使用构造的句子仔细看看切片,这里我们发现切片5:8包含索引567的句子元素。

>>> sent[5:8]
['word6', 'word7', 'word8']
>>> sent[5]
'word6'
>>> sent[6]
'word7'
>>> sent[7]
'word8'
>>>

按照惯例,m:n表示元素mn-1。正如下一个例子所示,如果切片从链表第一个元素开始,可以省略第一个数字;如果切片到链表最后一个元素处结尾,则可以省略第二个数字

>>> 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]放在等号左侧。也可以用新内容替换掉整个片段。最后一个报错的原因是这个链表只有4个元素而要获取4后面的元素,所以产生了错误

>>> 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
>>>

 轮到你来:

定义你的句子,使用前文中的方法修改个别词和词组(切片)。尝试本章结尾关于链表的练习,检验你是否真正理解。

变量

从1.1节开始,已经查看过名为text1,text2等的文本。像这样通过只输入简短的名字来就能引用一本250000字的书的做法节省了很多打字时间。一般情况下,可以对任意的计算命名。在前面的小节中已经这样做了,如下所示,定义一个变量sent1。

>>> sent1 = ['Call', 'me', 'Ishmael', '.']
>>>

语句形式是:变量 = 表达式。Python通过计算右边的表达式把结果保存在变量中。这个过程被称为赋值。它并不产生任何输出,但只能在新的一行输入变量的名字才能够检查它的内容。等号可能会有些误解,因为信息是从右边流到左边的。你把它想象成一个左箭头可能会有帮助。变量的名字可以是任何你喜欢的名字,如:my_sent、sentence、xyzzy等。变量必须以字母开头,可以包含数字和下划线。下面是变量和赋值的一些例子。

>>> 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会产生语法错误。

>>> not = 'Camelot'
File "<stdin>", line 1
  not = 'Camelot'
     ^
SyntaxError: invalid syntax
>>>

我们经常使用变量来保存计算的中间步骤,尤其是在这样做能够使代码更容易被读懂时。因此,len(set(text1))也可以写作如下形式。

>>> vocab = set(text1)
>>> vocab_size = len(vocab)
>>> vocab_size
19317
>>>

 注意!

为Python变量选择名称(或标识符)时请注意。首先,应该以字母开始,后面跟数字(0到9)或字母。因此,abc23是正确的,而23abc会导致语法错误。名称是明确区分大小写的。这意味着myVar和myvar是不同的变量。变量名不能包含空格,但可以用下划线把单词分开,如my_var。注意不要插入连字符来代替下划线:my-var不对,因为Python会把-解释为减号。

字符串

一些用来访问链表元素的方法也可以用在单独的词或字符串上。例如可以把一个字符串指定给一个变量,索引一个字符串,划分一个字符串

>>> name = 'Monty' ①
>>> name[0] ②
'M'
>>> name[:4] ③
'Mont'
>>>

还可以对字符串执行乘法和加法。

>>> name * 2
'MontyMonty'
>>> name + '!'
'Monty!'
>>>

可以把词用链表连接起来组成单个字符串,或者把字符串分割成一个链表,如下面所示。

>>> ' '.join(['Monty', 'Python'])
'Monty Python'
>>> 'Monty Python'.split()
['Monty', 'Python']
>>>

我们将在第3章继续介绍字符串的内容。目前为止,已经学习了两个重要的基石——链表和字符串——准备好重新进行语言分析了。

让我们重新开始探索利用计算资源处理大量文本的方法。在1.1节已经讨论了如何搜索文章中的词,如何汇编一个文本中的词汇,如何以相同的方式产生随机文本等。

在本节中,我们重新考虑怎样使一个文本显得与众不同的问题,并使用程序来自动寻找特征词汇和文本的表达方式。正如在1.1节中那样,可以通过把它们到复制Python解释器中来尝试Python语言的新特征,并将在下一节中系统地了解这些功能。

在这之前,你可能会想通过预测下面代码的输出来检验你对上一节的理解。你可以使用解释器来检查你是否正确。如果你不确定如何完成这个任务,你最好在继续学习之前复习一下上一节的内容。

>>> 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?
>>>

频率分布

如何能自动识别文本中最能体现文本主题和风格的词汇?试想一下,要找到一本书中使用最频繁的50个词你会怎么做?一种方法是为每个词项设置一个计数器,如图1-3所示。计数器可能需要几千行代码,这将是一个极其繁琐的过程——如此繁琐以至于我们宁愿把任务交给机器来做。

图1-3 计数一个文本中出现的单词(频率分布)

图1-3中的表被称为频率分布,显示的是每一个词项在文本中出现的频率。(一般情况下,它能计数任何观察的到的事件。)这是一个“分布”,因为它告诉我们文本中词标识符的总数是如何分布在词项中的。因为我们经常需要在语言处理中使用频率分布,NLTK为它们提供内置支持。利用FreqDist寻找《白鲸记》中最常见的50个词。尝试做出下面的例子,然后阅读接下来的解释。

>>> fdist1 = FreqDist(text1) ①
>>> fdist1 ②
<FreqDist with 260819 outcomes>
>>> vocabulary1 = fdist1.keys() ③
>>> vocabulary1[:50] ④
[',', 'the', '.', 'of', 'and', 'a', 'to', ';', 'in', 'that', "'", '-','his', 'it', 'I', 's', 'is', 'he', 'with', 'was', 'as', '"', 'all', 'for','this', '!', 'at', 'by', 'but', 'not', '--', 'him', 'from', 'be', 'on','so', 'whale', 'one', 'you', 'had', 'have', 'there', 'But', 'or', 'were','now', 'which', '?', 'me', 'like']
>>> fdist1['whale']
906
>>>

第一次调用FreqDist时,传递文本的名称作为参数。计算得到的《白鲸记》中单词的总数(“结果”)——高达260819。表达式keys()为我们提供了文本中所有不同类型的链表,可以通过切片看看这个链表的前50项

 轮到你来:

使用text2尝试前面频率分布的例子。注意正确使用括号和大写字母。如果得到的是错误消息:NameError:name 'FreqDist' is not defined,则需要在一开始输入nltk.book import*。

上一个例子中是否有什么词有助于我们把握这个文本的主题或风格呢?只有一个词,whale,这是仅有的信息量!它出现了超过900次。其余的词没有告诉我们关于文本的信息,它们只是“管道”英语。这些词在文本中占多少比例?我们可以产生关于这些词汇的累积频率图,使用fdist1.plot(50, cumulative=True) 产生图1-4。这50个词占了这本书的将近一半!

图1-4 《白鲸记》中50个最常用词的累积频率图,这些词占了所有标识符的将近一半

如果高频词对我们没有帮助,那么只出现了一次的词(所谓的hapaxes)又如何呢?输入fdist1.hapaxes()查看结果。此链表包括lexicographercetologicalcontraband,expostulations等9000多个词。看来低频词太多了,没看到上下文我们很可能无法猜到点hapaxes的含义!既然高频词和低频词都没有帮助,那就需要尝试其他的办法。

细粒度的选择词

接下来,让我们看看文本中的长词,也许它们有更多的特征和信息量。为此可以采用集合论的一些符号。想要找出文本词汇表中长度超过15个字符的词,把它称为特性P,则当且仅当词w的长度大余15个字符时P(w)为真。现在可以用(1a)中的数学集合符号表示我们感兴趣的词汇。它的含义是:此集合中所有w都满足:w是集合V(词汇表)的一个元素且w有特性P。

(1)  a. {w | w V∈& P(w)}
   b. [w for w in V if p(w)]

(1b)给出了对应的Python表达式。(请注意,它产生的是一个链表而不是集合,这意味着可能会有相同的元素。)观察这两个表达式的相似度。并编写可执行的Python代码。

>>> 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中的长词是非正规表达方式,例如boooooooooooglyyyyyyyuuuuuuuuuuuummmmmmmmmmmm。我们是否已经成功地自动提取出文本的特征词汇了呢?那么,这些很长的词通常是hapaxes(即唯一的),也许对寻找长词出现的频率会更好。这样看起来更有效,因为这样忽略了短高频词(如the)和长低频词(如antiphilosophists)。以下是聊天语料库中所有长度超过7个字符并且出现次数超过7次的词。

>>> 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确保词长都超过7个字母,fdist5[w]> 7确保这些词出现次数超过7次。最后,我们成功地自动识别出与文本内容相关的高频词。这很小的一步却是一个重要的里程碑:一小块代码,处理数以万计的词,产生一些有信息量的输出。

词语搭配和双连词

搭配是不经常在一起出现的词序列。因此,red wine是一个搭配而the wine不是。搭配的特点是其中的词不能被类似的词置换。例如:maroon wine(粟色酒)听起来就很奇怪。

要获取搭配,首先从提取文本词汇中的词对也就是双连词开始。使用函数bigrams()很容易实现这点。

>>> bigrams(['more', 'is', 'said', 'than', 'done'])
[('more', 'is'), ('is', 'said'), ('said', 'than'), ('than', 'done')]
>>>

在这里我们看到词对than-done是一个双连词,在Python中写成('than','done')。现在,除了着重考虑包含生僻词的情况,搭配基本上是频繁的双连词。特别是在已知单个词汇频率的基础上,想要找到出现频率比预期频率更频繁的双连词。collocations()函数为我们做这些(将在以后学习它是如何实现的)。

>>> text4.collocations()
Building collocations list
United States; fellow citizens; years ago; Federal Government; General
Government; American people; Vice President; Almighty God; Fellow
citizens; Chief Magistrate; Chief Justice; God bless; Indian tribes;
public debt; foreign nations; political parties; State governments;
National Government; United Nations; public money
>>> text8.collocations()
Building collocations list
medium build; social drinker; quiet nights; long term; age open;
financially secure; fun times; similar interests; Age open; poss
rship; single mum; permanent relationship; slim build; seeks lady;
Late 30s; Photo pls; Vibrant personality; European background; ASIAN
LADY; country drives
>>>

文本中出现的搭配能非常明确地体现文本的类型。为了找到red wine这个搭配,需要处理更大的文本。

计算其他东西

计算词汇是有用的,也可以计算其他东西。例如,可以查看文本中词长的分布,通过创造一长串数字链表的FreqDist,其中每个数字表示文本中对应词的长度。

>>> [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]) ②
>>> fdist③
<FreqDist with 260819 outcomes>
>>> fdist.keys()
[3, 1, 4, 2, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 20]
>>>

从text1中每个词的长度的链表开始,然后计数FreqDist计数链表中每个数字出现的次数。结果是一个包含25万左右个元素的分布,每一个元素是一个数字,对应文本中一个词标识符。但是只有20个不同的元素被计数,从1到20,因为只有20个不同的词长。也就是说,有由1个字符,2个字符,……,20个字符组成的词,而没有由21个或更多字符组成的词。有人可能会问不同长度的词的频率是多少?(例如,文本中有多少个长度为4的词?长度为5的词是否比长度为4的词多?等等)。通过下面的例子回答这个问题。

>>> fdist.items()
[(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.19 255882431878046
>>>

由此可见,最频繁词的长度是3,长度为3的词有50000多个(约占书中全部词汇的20%)。虽然我们不会在这里研究它,但关于词长的进一步分析可能帮助我们了解作者、文体和语言之间的差异。表1-2总结了NLTK频率分布类中定义的函数。

表1-2 NLTK频率分布类中定义的函数

例  子

描  述

fdist = FreqDist(samples)

创建包含给定样本的频率分布

fdist.inc(sample)

增加样本

fdist['monstrous']

计数给定样本出现的次数

fdist.freq('monstrous')

给定样本的频率

fdist.N()

样本总数

fdist.keys()

以频率递减顺序排序的样本链表

for sample in fdist:

以频率递减的顺序遍历样本

fdist.max()

数值最大的样本

fdist.tabulate()

绘制频率分布表

fdist.plot()

绘制频率分布图

fdist.plot(cumulative=True)

绘制累积频率分布图

fdist1 < fdist2

测试样本在fdist1中出现的频率是否小于fdist2

关于频率分布的讨论中引入了一些重要的Python概念,我们将在1.4节系统地学习。

 轮到你来:

运行下面的例子,尝试解释每一条指令中所发生的事情。然后,试着自己组合一些条件。

到目前为止,小程序有了一些有趣的特征:处理语言的能力和通过自动化节省人力的潜力。程序设计的一个关键特征是让机器能按照我们的意愿决策,在遇到特定条件时执行特定命令,或者对文本数据从头到尾不断循环直到条件满足。这一特征被称为控制,这是本节的重点。

条件

>> 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])

Python广泛支持多种运算符,如:<和> =,可以测试值之间的关系。全部的关系运算符见表1-3。

表1-3 数值比较运算符

运 算 符

关  系

<

小于

<=

小于等于

==

等于(注意是两个“=”号而不是一个)

!=

不等于

>

大于

>=

大于等于

可以使用这些从新闻文本句子中选出不同的词。下面是一些例子——注意行与行之间只是运算符不同。它们都使用sent7,第一句话来自text7(华尔街日报)。像以前一样,如果得到错误结果,sent7没有定义,需要首先输入:from nltk.book import *。

>>> 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“测试”,得到真(true)或者假(false)。在前面的代码例子中,条件始终是数值比较。然而,也可以使用表1-4中列出的函数测试词汇的各种属性。

表1-4 词汇比较运算符

函  数

含  义

s.startswith(t)

测试s是否以t开头

s.endswith(t)

测试s是否以t结尾

t in s

测试s是否包含t

s.islower()

测试s中所有字符是否都是小写字母

s.isupper()

测试s中所有字符是否都是大写字母

s.isalpha()

测试s中所有字符是否都是字母

s.isalnum()

测试s中所有字符是否都是字母或数字

s.isdigit()

测试s中所有字符是否都是数字

s.istitle()

测试s是否首字母大写(s中所有的词都首字母大写)

下面是一些从文本中选择词汇运算符的例子:以-ableness结尾的词,包含gnt的词,首字母大写的词,完全由数字组成的词。

>>> 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也是一个条件。如果有两个条件c1和c2,那么还可以使用合取和析取将它们合并形成一个新的条件:c1 and c2,c1 or c2。

对每个元素进行操作

在1.3节中,列举了计数词汇以外的其他项目的一些例子。让我们仔细看看之前所使用的符号。

>>> [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习惯用法(idiom),即对链表上的所有元素执行相同的操作。在前面的例子中,遍历text1中的每一个词,依次地赋值给变量w并在变量上执行指定的操作。

上述的表示法被称为“链表推导”,这是第一个Python习惯用法的例子,是一种固定的表示法,我们习惯使用该方法,这样省去了每次分析的烦恼。掌握这些习惯用法是成为一流Python程序员的一个重要组成部分。

回到计数词汇的问题上,这里使用相同的习惯用法。

>>> len(text1)
260819
>>> len(set(text1))
19317
>>> len(set([word.lower() for word in text1]))
17231
>>>

由于不重复计算像Thisthis这样仅仅大小写不同的词,就这样从词汇表计数中抹去了2000个!更进一步,还可以通过过滤掉所有非字母元素,从词汇表中消除数字和标点符号。

>>> len(set([word.lower() for word in text1 if word.isalpha()]))
16948
>>>

这个例子稍微有些复杂:将所有纯字母组成的词小写。也许只计数小写的词会更简单一些,但这却是一个错误的答案(为什么?)。

如果你对链表推导不是很有信心,请不要担心,因为在下面的章节中你会学习到更多的例子及解释。

嵌套代码块

大多数编程语言允许我们在条件表达式或者说if语句条件满足时执行代码块。例如[w for w in sent7 if len(w) < 4]这样的条件测试的例子。在下面的程序中,我们创建了一个叫word的变量包含字符串值“cat”。在if语句中检查len(word)< 5是否为真。cat的长度确实小于5,所以if语句下的代码块被调用,print语句被执行,向用户显示一条消息。别忘了要缩进,在print语句前输入4个空格。

>>> word = 'cat'
>>> if len(word) < 5:
...   print 'word length is less than 5'
...  ①
word length is less than 5
>>>

使用Python解释器时,必须添加一个额外的空白行,这样它才能检测到嵌套块结束。

如果改变测试条件为len(word) >= 5,检查词的长度是否大于或等于5,那么测试将不再为真。此时,if语句后面的代码段将不会被执行,没有消息显示给用户。

>>> if len(word) >= 5:
...  print 'word length is greater than or equal to 5'
...
>>>

if语句被看作是控制结构,因为它控制缩进块中的代码是否运行。另一个控制结构是for循环。尝试下面的代码,请记住输入冒号和4个空格。

>>> for word in ['Call', 'me', 'Ishmael', '.']:
...   print word
...
Call
me
Ishmael
.
>>>

这叫做循环,因为Python以循环的方式执行里面的代码。它从word='Call'赋值开始,有效地使用变量word命名链表的第一个元素。然后,显示word的值给用户。接下来回到for语句,执行word = 'me'赋值,然后把这个新值显示给用户,以此类推。它以这种方式不断运行,直到链表中所有项都被处理完。

条件循环

现在,可以将if语句和for语句结合。循环链表中的每一项,只输出结尾字母是l的词。我们将为变量挑选另一个名字以表明Python并不在意变量名的意义。

>>> sent1 = ['Call', 'me', 'Ishmael', '.']
>>> for xyzzy in sent1:
...   if xyzzy.endswith('l'):
...      print xyzzy
...
Call
Ishmael
>>>

你会发现在if和for语句所在行末尾——缩进开始之前——有一个冒号。事实上,所有的Python控制结构都以冒号结尾。冒号表示当前语句与后面的缩进块有关联。

也可以指定当if语句的条件不满足时采取的行动。在这里,我们看到elif(else if)语句和else语句。请注意,这些语句在缩进代码前也有冒号。

>>> 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语句结尾处的逗号,以便使结果在同一行输出。

>>> tricky = sorted([w for w in set(text2) if 'cie' in w or 'cei' in w])
>>> for word in tricky:
...   print word,
ancient ceiling conceit conceited conceive conscience
conscientious conscientiously deceitful deceive ...
>>>

我们一直在各种文本和Python编程语言的帮助下自下而上地探索语言。然而,我们也对通过构建有用的语言技术,开拓语言和计算知识面的兴趣。现在,将借此机会从代码的细节中退出来,以描绘自然语言处理的全景图。

在纯应用层面上,我们都需要帮助才能在网络上的文本中找到有用的信息。搜索引擎在网络的发展和普及中发挥了关键作用,但也有一些缺点。它需要技能、知识和一点运气才能找到这样一些问题的答案:“我用有限的预算能参观费城和匹兹堡的哪些景点?”,“专家们怎么评论数码单反相机?”,“过去的一周里可信的评论员都对钢材市场做了哪些预测?”。让计算机来自动回答这些问题,涉及包括信息提取、推理与总结在内的广泛的语言处理任务,而且将需要在一个更大规模、更稳健的层面上实施,这超出了我们当前的能力。

在哲学层面上,构建智能机器是人工智能长久以来的挑战,语言理解是智能行为的重要组成部分。这一目标多年来一直被看作是太困难了。然而,随着NLP技术日趋成熟,用稳定的方法来分析非结构化文本越来越广泛,对自然语言理解的期望变成一个合理的目标再次浮现。

在本节中,我们将介绍一些语言理解技术。

词意消歧

词意消歧中,要分析出特定上下文中的词被赋予的是哪个意思。思考存在歧义的词serve和dish。

(2)  a. serve: help with food or drink; hold an office; put ball into play
   b. dish: plate; course of a meal; communications device

在包含短语he served the dish的句子中,你可以知道serve和dish用的都是它们与食物相关的含义。仅仅3个词,讨论的话题不太可能从体育转向陶器。这也许会迫使你眼前产生一幅怪异的画面:一个职业网球手正把他的郁闷发泄到放在网球场边上的陶瓷茶具上。换句话说,自动消除歧义需要使用上下文,利用相邻词汇的相近含义。另一个有关上下文影响的例子是词by,它有几种含义,例如:the book by Chesterton(施事——Chesterton是书的作者);the cup by the stove(位置——炉子在杯子旁边);submit by Friday(时间——星期五前提交)。观察(3)中斜体字的含义有助于解释by的含义。

(3)  a. The lost children were found by the searchers (施事)
   b. The lost children were found by the mountain (位置)
   c. The lost children were found by the afternoon (时间)

指代消解

更深刻的语言理解是解决“谁对谁做了什么”,即检测动词的主语和宾语。虽然你在小学已经学会了这些,但它比你想象的更难。在句子the thieves stole the paintings中,很容易分辨出谁做了偷窃的行为。考虑(4)中句子的3种可能,尝试确定是什么被出售、被抓和被发现(其中一种情况是有歧义的)。

(4)  a. The thieves stole the paintings. They were subsequently sold.
   b. The thieves stole the paintings. They were subsequently caught.
   c. The thieves stole the paintings. They were subsequently found.

要回答这个问题涉及到寻找代词they的先行词thieves或者paintings。处理这个问题的计算技术包括指代消解(anaphora resolution)——确定代词或名词短语指的是什么——和语义角色标注(semantic role labeling)——确定名词短语如何与动词相关联(如代理、受事、工具等)。

自动生成语言

如果能够自动地解决语言理解等问题,我们将能够继续进行那些包含自动生成语言的任务,如自动问答机器翻译。在自动问答中,一台机器应该能够回答用户关于特定文本集的问题。

(5)  a. Text: ...The thieves stole the paintings. They were subsequently sold. ...
   b. Human: Who or what was sold?
   c. Machine: The paintings.

机器的回答表明,它已经正确地计算出they是指paintings,而不是thieves。在机器翻译中,机器应该能够把文本翻译成另一种语言文字,并准确传达原文的意思。在把例子文本译成法文过程中,我们不得不在第二句中选择代词的性别:ils(男性),如果thieves被出售,elles(女性),如果paintings被出售。正确的翻译实际上取决于对代词的正确理解。

(6)  a. The thieves stole the paintings. They were subsequently found.
   b. Les voleurs ont voléles peintures. Ils ontététrouvés plus tard. 
     (the thieves)
   c. Les voleurs ont voléles peintures. Elles ontététrouvées plus tard.
     (the paintings)

所有这些例子中,弄清楚词的含义、动作的主语及代词的先行词是确定句子含义的步骤,也是希望语言理解系统能够做到的事情。

机器翻译

长久以来,机器翻译(MT)都是语言理解的圣杯,人们希望能找到从根本上提供高品质且符合语言习惯的任意两种语言之间的翻译。其历史可以追溯到冷战初期,当时自动翻译带来大量的政府赞助,它也是NLP本身的起源。

今天,特定语言之间实用的翻译系统已经形成,而且有些已经集成到搜索引擎中了。但是,这些系统有一些严重的缺点。我们可以在NLTK的“babelizer”的帮助下探索它们(当你使用from nltk.book import * 导入本章的材料时,它已经自动装载了)。这个程序把提交的英文句子翻译成指定语言,然后把结果重新翻译回英文。这样重复12次后结束或者得到曾经产生过的翻译时(表示一个循环)结束。

>>> babelize_shell()
NLTK Babelizer: type 'help' for a list of commands.
Babel> how long before the next flight to Alice Springs?
Babel> german
Babel> run
0> how long before the next flight to Alice Springs?
1> wie lang vor dem folgenden Flug zu Alice Springs?
2> how long before the following flight to Alice jump?
3> wie lang vor dem folgenden Flug zu Alice springen Sie?
4> how long before the following flight to Alice do you jump?
5> wie lang, bevor der folgende Flug zu Alice tun, Sie springen?
6> how long, before the following flight to Alice does, do you jump?
7> wie lang bevor der folgende Flug zu Alice tut, tun Sie springen?
8> how long before the following flight to Alice does, do you jump?
9> wie lang, bevor der folgende Flug zu Alice tut, tun Sie springen?
10> how long, before the following flight does to Alice, do do you jump?
11> wie lang bevor der folgende Flug zu Alice tut, Sie tun Sprung?
12> how long before the following flight does leap to Alice, does you?

观察可以发现,该系统正确地把Alice Springs从英文翻译成了德文(第1行),但在翻译回英文时却以Alice jump结束(第2行)。介词before一开始被翻译成德文对应的介词vor,但后来变成了连词bevor(第5行)。第5行以后的句子变得没有意义(但请注意以逗号指示的各种分句,以及从jump到leap的变化)。翻译系统无法识别当一个词是某个名字的一部分时,并且会弄错语法结构。语法问题在下面的例子中更加明显。是约翰发现了猪,还是猪找到了约翰?

>>> babelize_shell()
Babel> The pig that John found looked happy
Babel> german
Babel> run
0> The pig that John found looked happy
1> Das Schwein, das John fand, schaute gl?cklich
2> The pig, which found John, looked happy

机器翻译是困难的,一方面原因是给定的单词可能有几种不同的解释(取决于它的意思),另一方面原因是必须改变词序才能与目标语言的语法结构保持一致。如今,遇到的难题是,从新闻和政府网站发布的两种或两种以上的语言文档中可以收集到大量的相似文本。如果给出一个德文和英文双语的文档或者一个双语词典,就可以自动配对组成句子,这个过程叫做文本对齐。一旦有一百万个或更多的句子对,就可以检测出相应的单词和短语,并建立能用来翻译新文本的模型。

人机对话系统

在人工智能的历史中,主要的智能测试是一种语言学测试,叫做图灵测试:一个响应用户文本输入的对话系统能否表现得如此自然以致我们无法区分它是人工生成的响应?相比之下,今天的商业对话系统能力是非常有限的,但在有限的给定领域仍然能够发挥作用,如下面的下例子。

S: How may I help you?

U: When is Saving Private Ryan playing?

S: For what theater?

U: The Paramount theater.

S: Saving Private Ryan is not playing at the Paramount theater, but

it’s playing at the Madison theater at 3:00, 5:30, 8:00, and 10:30.

你不能要求这个系统提供驾驶指示或附近餐馆的细节,除非所需的信息已经被保存并且合适的问题答案已经被纳入语言处理系统。

观察可看出这个系统能够了解用户的目标:用户询问电影上映的时间,系统正确地判断出用户是想要看电影。这一推断看起来如此明显,以至于你可能都没有注意到它,一个自然语言系统需要被赋予这种自然的交互能力。没有它,当问到:“你知道拯救大兵瑞恩什么时候上映?”时,系统可能只会回答一个冷冷的、毫无用处的“是的”。然而,商业对话系统的开发者使用上下文语境假设和业务逻辑确保在用户以不同方式表达需求或提供信息时对特定应用都能有效处理。因此,如果你输入When is ...或者I want to know when ...或者Can you tell me when ...时,这些简单的规则总是对应着放映时间,这足以使系统能够提供有益的服务了。

对话系统展示了一般的NLP流程。图1-5所示为一个简单的对话系统架构。沿图的顶部从左向右是一些语言理解组件的“管道”。这些图是从语音输入经过文法分析到某种意义的重现。图的中间,从右向左是这些组件将概念转换为语音的逆向流程。这些组件构成了系统的动态方面。在图的底部是一些有代表性的静态信息:处理组件在其上运作的语言相关数据的仓库。

 轮到你来:

作为一个原始的对话系统的例子,尝试与NLTK的chatbot谈话。使用之前请运行nltk.chat.chatbots()。(记住要先输入nltk。)

图1-5 简单的语音对话系统的流程架构:分析语音输入(左上),识别单词,文法分析和在上下文中解释,应用相关的具体操作(右上);响应规划,实现文法结构,然后是适当的词形变化,最后到语音输出;处理的每个过程都蕴含不同类型的语言学知识

文本的含义

近年来,一个叫做文本含义识别(Recognizing Textual Entailment,RTE)的公开的“共享任务”使语言理解所面临的挑战成为关注焦点。基本情形很简单:试想你想找到证据来支持一个假设:Sandra Goudie被Max Purnell击败了。而有一段简短的文字似乎是有关的,例如:Sandra Goudie在2002年国会选举首次当选,通过击败工党候选人Max Purnell将现任绿党下院议员Jeanette Fitzsimons推到第三位,以微弱优势赢得了Coromandel席位。文本是否为假说提供了足够的证据呢?在这种特殊情况下,答案是“否”。你可以很容易得出这样的结论,但使用自动方法做出正确决策是困难的。RTE挑战为竞赛者开发系统提供数据,但这些数据对“蛮力”机器学习技术(将在第6章介绍)来说是不够的。因此,语言学分析是至关重要的。在前面的例子中,很重要的一点是让系统知道Sandra Goudie是假设中被击败的人,而不是文本中击败别人的人。思考下面的文本-假设对,这是任务困难性的另一个例证。

(7)  a. Text: David Golinkin is the editor or author of 18 books, and over 150
     responsa, articles, sermons and books
   b. Hypothesis: Golinkin has written 18 books

为了确定假说是否能得到文本的支持,该系统需要以下背景知识:

(1)如果有人是一本书的作者,那么他/她写了这本书;

(2)如果有人是一本书的编辑,那么他/她(完全)没有写这本书;

(3)如果有人是18本书的编辑或作者,则无法断定他/她是18本书的作者。

NLP的局限性

尽管在很多如RTE这样的任务研究中取得了进展,但在现实世界的应用中已经部署的语言理解系统仍不能进行常识推理或以一般的可靠的方式描述这个世界的知识。在等待这些困难的人工智能问题得到解决的同时,接受一些在推理和知识能力上存在严重限制的自然语言系统是有必要的。因此,从一开始,自然语言处理研究的重要目标一直是使用浅显但强大的技术代替无边无际的知识和推理能力,促进构建“语言理解”技术的艰巨任务不断取得进展。事实上,这是本书的目标之一,我们希望你能掌握这些知识和技能,构建有效的自然语言处理系统,并为构建智能机器这一长期的理想做出贡献。

本章综合介绍了有关编程、自然语言处理和语言学的新概念。其中的一些将会在下面的章节继续出现。然而,你可能也想咨询与本章相关的在线材料(在http://www.nltk.org/),包括额外的背景资料的链接及在线NLP系统的链接。你可能还喜欢在维基百科中阅读一些语言学和自然语言处理相关的概念(如搭配、图灵测试、类型-标识符的区别等)。

你应该自己去熟悉http://docs.python.org/上的Python文档,那里给出了许多教程和全面的参考材料。http://wiki.python.org/moin/BeginnersGuide上有《Python初学者指南》。关于Python的各种问题在http://www.python.org/doc/faq/general/的FAQ中都有回答。

随着对NLTK研究的深入,你可能想订阅有关新版工具包的邮件列表。有了NLTK用户邮件列表,用户在学习如何使用Python和NLTK做语言分析工作时可以相互帮助。在http://www.nltk.org/中有这些列表的详情。

如果需要第1.5节所讲述的话题及NLP的相关信息,你可以阅读以下的优秀图书。

计算语言学协会(The Association for Computational Linguistics,ACL)是代表NLP领域的国际组织。ACL网站上有许多有用的资源,包括:有关国际和地区的会议及研讨会的信息;到数以百计有用资源的ACL Wiki链接;包含过去50年以来大多数NLP研究文献的ACL选集,里面的论文全部建立索引且可免费下载。

一些介绍语言学的优秀的教科书:(Finegan,2007),(O’Grady et al., 2004),(OSU,2007)。LanguageLog,是一个流行的语言学博客,其上会不定期发布一些本书中描述的技术应用。

1.○尝试使用Python解释器作为一个计算器,输入表达式,如12/(4+1)。

2.○26个字母可以组成26的10次方或者26**10个10字母长的字符串。也就是141167095653376L(结尾处的L只是表示这是Python长数字格式)。100个字母长度的字符串可能有多少个?

3.○Python乘法运算可应用于链表。当你输入['Monty', 'Python'] * 20或者3 * sent1会发生什么?

4.○复习1.1节关于语言计算的内容。在text2中有多少个词?有多少个不同的词?

5.○比较表格1-1中幽默和言情小说的词汇多样性得分,哪一个文体中词汇更丰富?

6.○制作《理智与情感》中4个主角:Elinor、Marianne、Edward和Willoughby的分布图。在这部小说中关于男性和女性所扮演的不同角色,你能观察到什么?你能找出一对夫妻吗?

7.○查找text5中的搭配。

8.○思考下面的Python表达式:len(set(text4))。说明这个表达式的用途,并且描述在执行此计算中涉及的两个步骤。

9.○复习1.2节关于链表和字符串的内容。

  a.定义一个字符串,并且将它分配给一个变量,如:my_string = 'My String'(在字符串中放一些更有趣的东西)。用两种方法输出这个变量的内容,一种是通过简单地输入变量的名称,然后按回车;另一种是通过使用print语句。

  b.尝试使用my_string+ my_string或者用它乘以一个数将字符串添加到它自身,例如:my_string* 3。请注意,连接在一起的字符串之间没有空格。怎样才能解决这个问题?

10.○使用语法my_sent = ["My", "sent"],定义一个词链表变量my_sent(用自己的词或喜欢的话)。

  a.使用' '.join(my_sent)将其转换成一个字符串。

  b.使用split()在你指定的地方将字符串分割回链表。

11.○定义几个包含词链表的变量,例如:phrase1、phrase2等。将它们连接在一起组成不同的组合(使用加法运算符),最终形成完整的句子。len(phrase1 + phrase2)与len(phrase1) + len(phrase2) 之间的关系是什么?

12.○考虑下面两个具有相同值的表达式。哪一个在NLP中更常用?为什么?

a."Monty Python"[6:12]
b.["Monty", "Python"][1]

13.○我们已经学习了如何用词链表表示一个句子,其中每个词是一个字符序列。sent1[2][2]代表什么意思?为什么?并尝试其他的索引值。

14.○在变量sent3中保存的是text3的第一句话。在sent3中the的索引值是1,因为sent3[1]的值是“the”。sent3中“the”的其他两种出现的索引值是多少?

15.○复习1.4节讨论的条件语句。在聊天语料库(text5)中查找所有以字母b开头的词。按字母顺序显示出来。

16.○在Python解释器提示符下输入表达式range(10)。再尝试range(10, 20), range(10, 20, 2)和range(20, 10, -2)。在后续章节中我们将看到这个内置函数的多种用途。

17.◑使用text9.index()查找词sunset的索引值。你需要将这个词作为一个参数插入到圆括号之间。在尝试和出错的过程中,在完整的句子中找到包含这个词的切片。

18.◑使用链表加法、set和sorted操作,计算句子sent1...sent8的词汇表。

19.◑下面两行之间的差异是什么?哪一个的值比较大?其他文本也是同样情况吗?

>>> sorted(set([w.lower() for w in text1]))
>>> sorted([w.lower() for w in set(text1)]

20.◑w.isupper()和not w.islower()这两个测试之间的差异是什么?

21.◑编写一个切片表达式提取text2中的最后两个词。

22.◑找出聊天语料库(text5)中所有4个字母的词。使用频率分布函数(FreqDist),以频率从高到低显示这些词。

23.◑复习1.4节中的条件循环。使用for和if语句组合循环遍历电影剧本《巨蟒和圣杯》(text6)中的词,输出所有的大写词,每行输出一个。

24.◑编写表达式并找出text6中所有符合下列条件的词。结果应该以词链表形式表示:['word1', 'word2', ...]。

  a.以ize结尾。

  b.包含字母z。

  c.包含字母序列pt。

  d.除了首字母外是全部小写字母的词(即titlecase)。

25.◑定义sent为词链表['she', 'sells', 'sea', 'shells', 'by', 'the', 'sea', 'shore']。编写代码执行以下任务。

  a.输出所有sh开头的单词。

  b.输出所有长度超过4个字符的词。

26.◑下面的Python代码是做什么的?sum([len(w) for w in text1]),你可以用它来算出一个文本的平均字长吗?

27.◑定义一个名为vocab_size(text)的函数,以文本作为唯一的参数,返回文本的词汇量。

28.◑定义一个函数percent(word, text),计算一个给定的词在文本中出现的频率,结果以百分比表示。

29.◑我们一直在使用集合存储词汇表。试试下面的Python表达式:set(sent3) < set(text1)。尝试在set()中使用不同的参数。它是做什么用的?你能想到一个实际的应用吗?


相关图书

Python极客项目编程(第2版)
Python极客项目编程(第2版)
动手学自然语言处理
动手学自然语言处理
Python财务应用编程
Python财务应用编程
Web应用安全
Web应用安全
深度学习的数学——使用Python语言
深度学习的数学——使用Python语言
Python量子计算实践:基于Qiskit和IBM Quantum Experience平台
Python量子计算实践:基于Qiskit和IBM Quantum Experience平台

相关文章

相关课程