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

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 当作计算器使用:

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

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

注意

轮到你来:输入一些你自己的表达式。你可以使用星号(*)表示乘法,左斜线(/)表示除法,你可以用括号括起表达式。

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

>>> 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所示。

>>> import nltk
>>> nltk.download()
../images/nltk-downloader.png

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

一旦数据被下载到你的机器,你就可以使用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 解释器,我们已经准备好上手了。

1.3 搜索文本

除了阅读文本之外,还有很多方法可以用来研究文本内容。词语索引视角显示一个指定单词的每一次出现,连同一些上下文一起显示。下面我们输入text1 后面跟一个点,再输入函数名concordance,然后将"monstrous" 放在括号里,来查一下Moby Dick 《白鲸记》中的词monstrous

>>> 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《创世纪》找出某人活了多久。你也可以看看text4Inaugural Address Corpus《就职演说语料》,回到1789 年看看那时英语的例子,搜索如nation, terrorgod 这样的词,看看随着时间推移这些词的使用如何不同。我们也包括了text5NPS Chat Corpus《NPS 聊天语料库》:你可以在里面搜索一些网络词,如im urlol(注意这个语料库未经审查!)

在你花了一小会儿研究这些文本之后,我们希望你对语言的丰富性和多样性有一个新的认识。在下一章中,你将学习获取更广泛的文本,包括英语以外其他语言的文本。

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

>>> 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允许我们研究两个或两个以上的词共同的上下文,如monstrousvery我们必须用方括号和圆括号把这些词括起来,中间用逗号分割:

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

注意

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

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

>>> text4.dispersion_plot(["citizens", "democracy", "freedom", "duties", "America"])
>>>
../images/inaugural.png

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

注意

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

注意

你还可以使用https://books.google.com/ngrams 画出词汇随着时间的使用频率。

现在轻松一下,让我们尝试产生一些刚才看到的不同风格的随机文本。要做到这一点,我们需要输入文本的名字后面跟函数名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
>>>

Note

generate() 方法在 NLTK 3.0 中不可用,但会在后续版本中恢复。

1.4 词汇计数

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

首先,让我们算出文本从头到尾的长度,包括文本中出现的词和标点符号。我们使用函数len获取长度,请看在《创世纪》中使用的例子:

>>> len(text3)
44764
>>>

《创世纪》有44764 个词和标点符号或者叫“词符”。词符 表示一个我们想要整体对待的字符序列 —— 例如hairyhis:)当我们计数文本如to be or not to be 这个短语中词符的个数时,我们计数这些序列出现的次数。因此,我们的例句中出现了tobe 各两次,ornot 各一次。然而在例句中只有4 个不同的词。《创世纪》中有多少不同的词?要用Python 来回答这个问题,我们处理问题的方法将稍有改变。一个文本词汇表只是它用到的词符的集合,因为在集合中所有重复的元素都只算一个。Python 中我们可以使用命令:set(text3) 获得text3 的词汇表。当你这样做时,屏幕上的很多词会掠过。现在尝试以下操作:

>>> sorted(set(text3)) [1]
['!', "'", '(', ')', ',', ',)', '.', '.)', ':', ';', ';)', '?', '?)',
'A', 'Abel', 'Abelmizraim', 'Abidah', 'Abide', 'Abimael', 'Abimelech',
'Abr', 'Abrah', 'Abraham', 'Abram', 'Accad', 'Achbor', 'Adah', ...]
>>> len(set(text3)) [2]
2789
>>>

sorted() 包裹起Python 表达式set(text3) [1],我们得到一个词汇项的排序表,这个表以各种标点符号开始,然后是以A 开头的词汇。大写单词排在小写单词前面。我们通过求集合中元素的个数间接获得词汇表的大小,再次使用len来获得这个数值[2]尽管小说中有44,764 个词符,但只有2,789 个不同的单词或“词类型”。一个词类型是指一个词在一个文本中独一无二的出现形式或拼写 —— 也就是说,这个词在词汇表中是唯一的。我们计数的2,789 个元素中包括标点符号,所以我们把这些叫做唯一元素类型而不是词类型。

现在,让我们对文本词汇丰富度进行测量。下一个例子向我们展示,不同的单词数目只是单词总数的6%,或者每个单词平均被使用了16 次(记住,如果你使用的是Python 2,请在开始输入from __future__ import division)。

>>> len(set(text3)) / len(text3)
0.06230453042623537
>>>

接下来,让我们专注于特定的词。我们可以计数一个词在文本中出现的次数,计算一个特定的词在文本中占据的百分比:

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

轮到你来: text5lol 出现了多少次?它占文本全部词数的百分比是多少?

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

>>> def lexical_diversity(text): [1]
...     return len(set(text)) / len(text) [2]
...
>>> def percentage(count, total): [3]
...     return 100 * count / total
...

小心!

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

lexical_diversity() [1]的定义中,我们指定了一个text 参数这个参数是我们想要计算词汇多样性的实际文本的一个“占位符”,并在用到这个函数的时候出现在将要运行的代码块中 [2]类似地,percentage() 定义了两个参数,counttotal [3]

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

>>> 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 语料库中各种文体的词汇多样性

体裁词符类型词汇多样性
技能和爱好82345119350.145
幽默2169550170.231
小说:科学1447032330.223
新闻:报告文学100554143940.143
小说:浪漫7002284520.121
宗教3939963730.162

2 近观Python:将文本当做单词列表

你已经见过Python 编程语言的一些重要元素。让我们花几分钟系统的复习一下。

2.1 列表

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

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

在提示符后面,我们输入自己命名的sent1,后跟一个等号,然后是一些引用的词汇,中间用逗号分割并用括号包围。这个方括号内的东西在Python 中叫做列表:它就是我们存储文本的方式。我们可以通过输入它的名字来查阅它[1]我们可以查询它的长度[2]我们甚至可以对它调用我们自己的函数lexical_diversity()[3]

>>> sent1 [1]
['Call', 'me', 'Ishmael', '.']
>>> len(sent1) [2]
4
>>> lexical_diversity(sent1) [3]
1.0
>>>

还定义了其它几个列表,分别对应每个文本开始的句子,sent2sent9在这里我们检查其中的两个;你可以自己在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 节看到的其他Python 操作,如:sorted(ex1), len(set(ex1)), ex1.count('the')

令人惊喜的是,我们可以对列表使用Python 加法运算。两个列表相加[1]创造出一个新的列表,包括第一个列表的全部,后面跟着第二个列表的全部。

>>> ['Monty', 'Python'] + ['and', 'the', 'Holy', 'Grail'] [1]
['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']
>>>

2.2 索引列表

正如我们已经看到的,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'
>>>

请注意,索引从零开始:sent 第0 个元素写作sent[0],是第一个单词'word1',而sent 的第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 包含sent 中索引为5,6 和7的元素:

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

按照惯例,m:n 表示元素mn-1正如下一个例子显示的那样,如果切片从列表第一个元素开始,我们可以省略第一个数字[1], 如果切片到列表最后一个元素处结尾,我们可以省略第二个数字 [2]

>>> sent[:3] [1]
['word1', 'word2', 'word3']
>>> text2[141525:] [2]
['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] 放在等号左侧[1]我们也可以用新内容替换掉一整个片段[2]最后一个尝试报错的原因是这个链表只有四个元素而要获取其后面的元素就产生了错误[3]

>>> sent[0] = 'First' [1]
>>> sent[9] = 'Last'
>>> len(sent)
10
>>> sent[1:9] = ['Second', 'Third'] [2]
>>> sent
['First', 'Second', 'Third', 'Last']
>>> sent[9] [3]
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
IndexError: list index out of range
>>>

注意

轮到你来:花几分钟定义你自己的句子,使用前文中的方法修改个别词和词组(切片)。尝试本章结尾关于列表的练习,检验你是否理解。

2.3 变量

从第1节一开始,你已经访问过名为text1, text2 等的文本。像这样只输入简短的名字来引用一本250,000 字的书节省了很多打字时间。一般情况下,我们可以对任何我们关心的计算命名。我们在前面的小节中已经这样做了,例如定义一个变量变量 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 变量选择名称(标识符)时请注意。首先,应该以字母开始,后面跟数字(09)或字母。因此,abc23 是好的,但是23abc 会导致一个语法错误。名称是大小写敏感的,这意味着myVarmyvar 是不同的变量。变量名不能包含空格,但可以用下划线把单词分开,例如my_var注意不要插入连字符来代替下划线:my-var 不对,因为Python 会把"-"解释为减号。

2.4 字符串

我们用来访问列表元素的一些方法也可以用在单独的词或字符串上。例如可以把一个字符串指定给一个变量[1],索引一个字符串[2],切片一个字符串[3]

>>> name = 'Monty' [1]
>>> name[0] [2]
'M'
>>> name[:4] [3]
'Mont'
>>>

我们还可以对字符串执行乘法和加法:

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

我们可以把列表中的单词连接起来组成单个字符串,或者把字符串分割成一个列表,如下面所示:

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

我们将在第3章回到字符串的主题。目前,我们已经有了两个重要的基石——列表和字符串——已经准备好可以重新做一些语言分析了。

3 计算语言:简单的统计

让我们重新开始探索用我们的计算资源处理大量文本的方法。我们在第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?
>>>

3.1 频率分布

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

../images/tally.png

图 3.1:计数一个文本中出现的词(频率分布)

3.1 中的表被称为频率分布,它告诉我们在文本中的每一个词项的频率。(一般情况下,它能计数任何观察得到的事件。)这是一个“分布”因为它告诉我们文本中单词词符的总数是如何分布在词项中的。因为我们经常需要在语言处理中使用频率分布,NLTK 中内置了它们。让我们使用FreqDist 寻找《白鲸记》中最常见的50 个词:

>>> fdist1 = FreqDist(text1) [1]
>>> print(fdist1) [2]
<FreqDist with 19317 samples and 260819 outcomes>
>>> fdist1.most_common(50) [3]
[(',', 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时,传递文本的名称作为参数[1]我们可以看到已经被计算出来的《白鲸记》中的总的词数(“outcomes”)—— 260,819[2]表达式most_common(50) 给出文本中50 个出现频率最高的单词类型[3]

注意

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

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

../images/fdist-moby.png

图 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 V(词汇表)的一个元素且w有性质P

(1)

a.{w | wV & P(w)}

b.[w for w in V if p(w)]

对应的Python 表达式为(1b)(请注意,它产生一个列表,而不是集合,这意味着可能会有相同的元素。)观察这两个表达式它们是多么相似。让我们进行下一步,编写可执行的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 保证词长都超过七个字母,fdist5[w] > 7 保证这些词出现超过7 次。最后,我们已成功地自动识别出与文本内容相关的高频词。这很小的一步却是一个重要的里程碑:一小块代码,处理数以万计的词,产生一些有信息量的输出。

3.3 词语搭配和双连词

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

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

>>> 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() 函数为我们做这些。我们将在以后看到它是如何工作。

>>> 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,其中每个数字是文本中对应词的长度:

>>> [len(w) for w in text1] [1]
[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)  [2]
>>> print(fdist)  [3]
<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 中每个词的长度的列表开始[1],然后FreqDist 计数列表中每个数字出现的次数[2]结果[3] 是一个包含25 万左右个元素的分布,每一个元素是一个数字,对应文本中一个词标识符。但是只有20 个不同的元素,从1 到20,因为只有20 个不同的词长。也就是说,有由1 个字符,2 个字符,...,20 个字符组成的词,而没有由21 个或更多字符组成的词。有人可能会问不同长度的词的频率是多少?(例如,文本中有多少长度为4 的词?长度为5 的词是否比长度为4 的词多?等等)。下面我们回答这个问题:

>>> 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 频率分布类中定义的函数

示例描述
fdist = FreqDist(samples)创建包含给定样本的频率分布
fdist[sample] += 1增加样本的数目
fdist['monstrous']计数给定样本出现的次数
fdist.freq('monstrous')给定样本的频率
fdist.N()样本总数
fdist.most_common(n)最常见的n 个样本和它们的频率
for sample in fdist:遍历样本
fdist.max()数值最大的样本
fdist.tabulate()绘制频率分布表
fdist.plot()绘制频率分布图
fdist.plot(cumulative=True)绘制累积频率分布图
fdist1 |= fdist2使用fdist2 更新fdist1 中的数目
fdist1 < fdist2测试样本在fdist1 中出现的频率是否小于fdist2

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

4 回到Python:决策与控制

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

4.1 条件

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

表 4.1

数值比较运算符

运算符关系
<小于
<=小于或等于
==等于(注意是两个“=”号而不是一个)
!=不等于
>大于
>=大于或等于

我们可以使用这些从新闻文本句子中选出不同的词。下面是一些例子——注意行与行之间只是运算符不同。它们都使用sent7text7华尔街日报)的第一句话。像以前一样,如果你得到一个错误说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 中的一个“测试”,得到真或者假。在前面的代码例子所示的情况中,条件始终是数值比较。然而,我们也可以使用表4.2 中列出的函数测试词汇的各种属性。

表 4.2:

一些词比较运算符

函数含义
s.startswith(t)测试s是否以t 开头
s.endswith(t)测试s是否以t 结尾
t in s测试t是否是s的子串
s.islower()测试s中所有字符是否包含大小有区别的字符,且都是小写
s.isupper()测试s中所有字符是否包含大小有区别的字符,且都是大写
s.isalpha()测试s是否非空,且s中的所有字符是字母
s.isalnum()测试s是否非空,且s中的所有字符是字母或数字
s.isdigit()测试s是否非空,且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 也是一个条件。如果我们有两个条件c1c2,那么我们可以使用合取和析取将它们合并形成一个新的条件:c1 and c2, c1 or c2

注意

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

>>> 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节中,我们看到计数词汇以外的其他项目的一些例子。让我们仔细看看我们所使用的符号:

>>> [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 程序员的一个重要组成部分。

让我们回到计数词汇的问题,这里使用相同的习惯用法:

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

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

>>> 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语句前输入四个空格。

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

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

注意

如果你正在使用Python 2.6 或2.7,为了识别上面的print函数,需要包括以下行︰

>>> from __future__ import print_function

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

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

if语句被称为一种控制结构,因为它控制缩进块中的代码将是否运行。另一种控制结构是for循环。尝试下面的代码,请记住包含冒号和四个空格:

>>> 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并不在意变量名的意义。

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

你会发现在iffor语句所在行末尾——缩进开始之前——有一个冒号。事实上,所有的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交互式解释器的价值所在,也是为什么你必须适应它的原因。

最后,让我们把一直在探索的习惯用法组合起来。首先,我们创建一个包含ciecei的词的列表,然后循环输出其中的每一项。请注意print语句中给出的额外信息︰end=' '它告诉Python在每个单词后面打印一个空格(而不是默认的换行)。

>>> 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 词意消歧

词意消歧中,我们要算出特定上下文中的词被赋予的是哪个意思。思考存在歧义的词servedish

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

5.2 指代消解

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

(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。处理这个问题的计算技术包括指代消解 ——确定代词或名词短语指的是什么—— 和语义角色标注 —— 确定名词短语如何与动词相关联(如施事,受事,工具等)。

5.3 自动生成语言

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

(5)

a.文本: ...The thieves stole the paintings. They were subsequently sold. ...

b.人: Who or what was sold?

c.机器: The paintings.

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

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

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

5.4 机器翻译

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

今天,特定语言之间实用的翻译系统已经存在,有些已经集成到搜索引擎中了。但是,这些系统有一些严重的缺点,通过把一个句子在一对语言之间来回转换直至平衡就完全暴露出来了,例如:

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行之后,句子变得没有意义(但请注意以逗号指示的各种分句,以及从jumpleap的变化)。翻译系统不能识别一个词是某个名字的一部分,并且弄错了语法结构。

注意

轮到你来: 使用http://translationparty.com/自己尝试这个例子

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

5.5 人机对话系统

在人工智能的历史,主要的智能测试是一个语言学测试,叫做图灵测试:一个响应用户文本输入的对话系统能否表现的自然到我们无法区分它是人工生成的响应?相比之下,今天的商业对话系统能力是非常有限的,但在较小的给定领域仍然有些作用,就像我们在这里看到的:

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 ...时,这些简单的规则总是生成放映时间。这就足够系统提供有益的服务了。

../images/dialogue.png

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

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

注意

轮到你来: 作为一个原始的对话系统的例子,尝试与NLTK 的chatbot 谈话。要看到chatbots,请运行nltk.chat.chatbots()(记住首先要import nltk。)

5.6 文本的含义

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

(7)

a.文本:David Golinkin is the editor or author of eighteen books, and over 150 responsa, articles, sermons and books

b.假设:Golinkin has written eighteen books

为了确定假说是否得到文本的支持,该系统需要以下背景知识:(i) 如果有人是一本书的作者,那么他/她写了这本书;(ii) 如果有人是一本书的编辑,那么他/她没有(完全)写这本书;(iii) 如果有人是18 本书的编辑或作者,则无法断定他/她是18 本书的作者。

5.7 NLP 的局限性

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

6 小结

7 深入阅读

本章介绍了一些编程、自然语言处理和语言学的新概念,都混在一起。其中不少将会在下面的章节得到巩固。你可能也想咨询与本章相关的在线材料(在http://nltk.org/),包括额外的背景资料的链接以及在线NLP 系统的链接。你可能还喜欢在维基百科中阅读一些语言学和自然语言处理相关的概念(如:搭配,图灵测试,词型-词符的区别等)。

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

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

关于第5节讲述的主体更多细节, 以及NLP 更一般的相关信息,你可能想阅读以下的优秀图书中的一本:

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

一些介绍语言学的优秀的教科书:[Finegan2007]_, (O'Grady et al, 2004), (OSU, 2007)你可能想查阅LanguageLog,一个流行的语言学博客,不定期发布一些本书中描述的技术应用。

8 练习

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

  2. ☼ 26 个字母可以组成26 的10 次方或者26 ** 10 个10 字母长的字符串。这相当于141167095653376100 个字母长度的字符串可能有多少个?

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

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

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

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

  7. ☼ 查找text5中的搭配。

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

  9. ☼ 复习第2节关于列表和字符串的内容。

    1. 定义一个字符串并将它分配给一个变量,如:my_string = 'My String'String'(在字符串中放一些更有趣的东西)。用两种方法输出这个变量的内容,一种是通过简单地输入变量的名称,然后按回车;另一种是通过使用print语句。
    2. 尝试使用my_string + my_string或者用它乘以一个数将字符串添加到它自身,例如my_string * 3请注意,连接在一起的字符串之间没有空格。怎样能解决这个问题?
  10. ☼ 使用的语法my_sent = ["My", "sent"]定义一个变量my_sent为一个单词列表(用你自己的词或喜欢的话)。

    1. 使用' '.join(my_sent)将其转换成一个字符串。
    2. 使用split()将字符串分割回开始的列表形式。
  11. ☼ 定义几个包含词链表的变量,例如phrase1, phrase2等。将它们连接在一起组成不同的组合(使用加法运算符),最终形成完整的句子。len(phrase1 + phrase2)len(phrase1) + len(phrase2)之间的关系是什么?

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

    1. "Monty Python"[6:12]
    2. ["Monty", "Python"][1]
  13. ☼ 我们已经看到如何用单词列表表示一个句子,其中每个词是一个字符序列。sent1[2][2]代表什么意思?为什么?请用其他的索引值做实验。

  14. ☼ 在变量sent3中保存的是text3的第一句话。thesent3中的索引是1,因为sent3[1] 给我们的是'the'sent3中“the”的其它出现的索引值是多少?

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

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

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

  18. ◑ 使用列表加法setsorted操作,计算句子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)中所有四个字母的词。使用频率分布函数(FreqDist),以频率从高到低显示这些词。

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

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

    1. ize 结尾
    2. 包含字母z
    3. 包含字母序列pt
    4. 除了首字母外是全部小写字母的词(即titlecase
  25. ◑ 定义sent为一个单词列表:['she', 'sells', 'sea', 'shells', 'by', 'the', 'sea', 'shore']编写代码执行以下任务:

    1. 输出所有sh开头的单词
    2. 输出所有长度超过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()中使用不同的参数。它是做什么用的?你能想到一个实际的应用吗?

关于本文档...

针对NLTK 3.0 进行更新。本章来自于Natural Language Processing with PythonSteven Bird, Ewan KleinEdward Loper,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

Docutils System Messages

System Message: ERROR/3 (ch01.rst2, line 1889); backlink

Unknown target name: "finegan2007".