查看原文
其他

Python 自然语言处理入门

2017-05-01 Python开发者

(点击上方蓝字,快速关注我们)


编译:伯乐在线专栏作者 - Ree Ray

如有好文章投稿,请点击 → 这里了解详情


本文从概念和实际操作量方面,从零开始,介绍在Python中进行自然语言处理。


(作者案:本文是我最初发表在《ACM Crossroads》Volume 13,Issue 4 上的完整修订版。之所以修订是因为 Natural Language Toolkit(NLTK)改动较大。修订版代码兼容至最新版 NLTK(2013 年 9 月更新至 2.0.4 版)。尽管本文的代码一律经过测试,仍有可能出现一两个问题。如果你发现了问题,请向作者反映。如果你非用 0.7 版不可的话,请参考 这里。


1 缘起


本文试着向读者们介绍自然语言处理(Natural Language Processing)这一领域,通常简称为 NLP。然而,不同于一般只是描述 NLP 重要概念的文章,本文还借助 Python 来形象地说明。对于不熟悉 Python 的读者们,本文也提供了部分参考资料教你如何进行 Python 编程。


2 相关介绍


2.1 自然语言处理


自然语言处理广纳了众多技术,对自然或人类语言进行自动生成,处理与分析。虽然大部分 NLP 技术继承自语言学和人工智能,但同样受到诸如机器学习,计算统计学和认知科学这些相对新兴的学科影响。


在展示 NLP 技术的例子前,有必要介绍些非常基础的术语。请注意:为了让文章通俗易懂,这些定义在语言上就不一定考究。


  • 词例(Token):对输入文本做任何实际处理前,都需要将其分割成诸如词、标点符号、数字或纯字母数字(alphanumerics)等语言单元(linguistic units)。这些单元被称为词例。

  • 句子:由有序的词例序列组成。

  • 词例还原(Tokenization):将句子还原成所组成的词例。以分割型语言(segmented languages)英语为例,空格的存在使词例还原变得相对容易同时也索然无味。然而,对于汉语和阿拉伯语,因为没有清晰的边界,这项工作就稍显困难。另外,在某些非分割型语言(non-segmented languages)中,几乎所有的字符(characters)都能以单字(one-character)存在,但同样也可以组合在一起形成多字(multi-characterwords)形式。

  • 语料库:通常是由丰富句子组成的海量文本。

  • 词性标签(Part-of-speech (POS) Tag):任一单词都能被归入到至少一类词汇集(set of lexical)或词性条目(part-of-speech categories)中,例如:名词、动词、形容词和冠词等。词性标签用符号来代表一种词汇条目——NN(名词)、VB(动词)、JJ(形容词)和 AT(冠词)。Brown Corpus 是最悠久,也是最常用的标注集之一。详情且听下回分解。

  • 剖析树(Parse Tree):利用形式语法(formal grammar)的定义,可以用树状图来表示给定句子的句法(syntactic)结构。


认识了基本的术语,下面让我们了解 NLP 常见的任务:


  • 词性标注(POS Tagging):给定一个句子和组词性标签,常见的语言处理就是对句子中的每个词进行标注。举个例子,The ball is red,词性标注后将变成 The/AT ball/NN is/VB red/JJ。最先进的词性标注器[9]准确率高达 96%。文本的词性标注对于更复杂的 NLP 问题,例如我们后面会讨论到的句法分析(parsing)和机器翻译(machine translation)非常必要。

  • 计算形态学(Computational Morphology):大量建立在“语素”(morphemes/stems)基础上的词组成了自然语言,语素虽然是最小的语言单元,却富含意义。计算形态学所关心的是用计算机发掘和分析词的内部结构。

  • 句法分析(Parsing):在语法分析的问题中,句法分析器(parser)将给定句子构造成剖析树。为了分析语法,某些分析器假定一系列语法规则存在,但目前的解析器已经足够机智地借助复杂的统计模型[1]直接推断分析树。多数分析器能够在监督式设置(supervised setting)下操作并且句子已经被词性标注过了。统计句法分析是自然语言处理中非常活跃的研究领域。

  • 机器翻译(Machine Translation(MT)):机器翻译的目的是让计算机在没有人工干预的情况下,将给定某种语言的文本流畅地翻译成另一种语言文本。这是自然语言处理中最艰巨的任务之一,这些年来已经用许多不同的方式解决。几乎所有的机器翻译方法都依赖了词性标注和句法分析作为预处理。


2.2 Python


Python 是一种动态类型(dynamically-typed),面向对象的解释式(interpreted)编程语言。虽然它的主要优势在于允许编程人员快速开发项目,但是大量的标准库使它依然能适应大规模产品级工程项目。Python 的学习曲线非常陡峭并且有许多优秀的在线学习资源[11]。


2.3 自然语言工具集(Natural Language Toolkit)


尽管 Python 绝大部分的功能能够解决简单的 NLP 任务,但不足以处理标准的自然语言处理任务。这就是 NLTK (自然语言处理工具集)诞生的原因。NLTK 集成了模块和语料,以开源许可发布,允许学生对自然语言处理研究学习和生产研究。使用 NLTK 最大的优势是集成化(entirely self-contained),不仅提供了方便的函数和封装用于建立常见自然语言处理任务块,而且提供原始和预处理的标准语料库版本应用在自然语言处理的文献和课程中。


3 使用 NLTK


NLTK 官网提供了很棒的说明文件和教程进行学习指导[13]。单纯复述那些作者们的文字对于他们和本文都不公平。因此我会通过处理四个难度系数依次上升的 NLP 任务来介绍 NLTK。这些任务都来自于 NLTK 教程中没有给出答案的练习或者变化过。所以每个任务的解决办法和分析都是本文原创的。


3.1 NLTK 语料库


正如前文所说,NLTK 囊括数个在 NLP 研究圈里广泛使用的实用语料库。在本节中,我们来看看三个下文会用到的语料库:


  • 布朗语料库(Brown Corpus):Brown Corpus of Standard American English 被认为是第一个可以在计算语言学处理[6]中使用的通用英语语料库。它包含了一百万字 1961 年出版的美语文本。它代表了通用英语的样本,采样自小说,新闻和宗教文本。随后,在大量的人工标注后,诞生了词性标注过的版本。

  • 古登堡语料库(Gutenberg Corpus):古登堡语料库从最大的在线免费电子书[5]平台 古登堡计划(Gutenberg Project) 中选择了 14 个文本,整个语料库包含了一百七十万字。

  • Stopwords Corpus:除了常规的文本文字,另一类诸如介词,补语,限定词等含有重要的语法功能,自身却没有什么含义的词被称为停用词(stop words)。NLTK 所收集的停用词语料库(Stopwords Corpus)包含了 来自 11 种不同语言(包括英语)的 2400 个停用词。


3.2 NLTK 命名约定


在开始利用 NLTK 处理我们的任务以前,我们先来熟悉一下它的命名约定(naming conventions)。最顶层的包(package)是 nltk,我们通过使用完全限定(fully qualified)的加点名称例如:nltk.corpus and nltk.utilities 来引用它的内置模块。任何模块都能利用 Python 的标准结构 from . . . import . . . 来导入顶层的命名空间。


3.3 任务 1 : 探索语料库


上文提到,NLTK 含有多个 NLP 语料库。我们把这个任务制定为探索其中某个语料库。


任务:用 NLTK 的 corpus 模块读取包含在古登堡语料库的 austen-persuasion.txt,回答以下问题:


  • 这个语料库一共有多少字?

  • 这个语料库有多少个唯一单词(unique words)?

  • 前 10 个频率最高的词出现了几次?


利用 corpus 模块可以探索内置的语料库,而且 NLTK 还提供了包含多个好用的类和函数在概率模块中,可以用来计算任务中的概率分布。其中一个是 FreqDist,它可以跟踪分布中的采样频率(sample frequencies)。清单1 演示了如何使用这两个模块来处理第一个任务。


清单 1: NLTK 内置语料库的探索.


# 导入 gutenberg 集

>>> from nltk.corpus import gutenberg

# 都有些什么语料在这个集合里?

>>> print gutenberg.fileids()

['austen-emma.txt', 'austen-persuasion.txt', 'austen-sense.txt', 'bible-kjv.txt', 'blake-poems.txt', 'bryant-stories.txt', 'burgess-busterbrown.txt', 'carroll-alice.txt', 'chesterton-ball.txt', 'chesterton-brown.txt', 'chesterton-thursday.txt', 'edgeworth-parents.txt', 'melville-moby_dick.txt', 'milton-paradise.txt', 'shakespeare-caesar.txt', 'shakespeare-hamlet.txt', 'shakespeare-macbeth.txt', 'whitman-leaves.txt']

 

# 导入 FreqDist 类

>>> from nltk import FreqDist

# 频率分布实例化

>>> fd = FreqDist()

# 统计文本中的词例

>>> for word in gutenberg.words('austen-persuasion.txt'):

... fd.inc(word)

...

>>> print fd.N() # total number of samples

98171

>>> print fd.B() # number of bins or unique samples

6132

# 得到前 10 个按频率排序后的词

>>> for word in fd.keys()[:10]:

... print word, fd[word]

, 6750

the 3120

to 2775

. 2741

and 2739

of 2564

a 1529

in 1346

was 1330

; 1290


解答:简奥斯丁的小说 Persuasion 总共包含 98171 字和 6141 个唯一单词。此外,最常见的词例是逗号,接着是单词the。事实上,这个任务最后一部分是最有趣的经验观察之一,完美说明了单词的出现现象。如果你对海量的语料库进行统计,将每个单词的出现次数和单词出现的频率由高到低记录在表中,我们可以直观地发现列表中词频和词序的关系。事实上,齐普夫(Zipf)证明了这个关系可以表达为数学表达式,例如:对于任意给定单词,$fr$ = $k$, $f$ 是词频,$r$ 是词的排列,或者是在排序后列表中的词序,而 $k$ 则是一个常数。所以,举个例子,第五高频的词应该比第十高频的词的出现次数要多两倍。在 NLP 文献中,以上的关系通常被称为“齐普夫定律(Zipf’s Law)”。


即使由齐普夫定律描述的数学关系不一定完全准确,但它依然对于人类语言中单词分布的刻画很有用——词序小的词很常出现,而稍微词序大一点的则较为少出现,词序非常大的词则几乎没有怎么出现。任务 1 最后一部分使用 NLTK 非常容易通过图形进行可视化,如 清单 1a 所示。相关的 log-log 关系,如图 1,可以很清晰地发现我们语料库中对应的扩展关系。


清单 1a: 使用 NLTK 对齐普夫定律进行作图


>>> from nltk.corpus import gutenberg

>>> from nltk import FreqDist

# 作图需要 matplotlib(可以从 NLTK 下载页获得)

>>> import matplotlib

>>> import matplotlib.pyplot as plt

# 统计 Gutenberg 中每个词例数量

>>> fd = FreqDist()

>>> for text in gutenberg.fileids():

... for word in gutenberg.words(text):

... fd.inc(word)

# 初始化两个空列表来存放词序和词频

>>> ranks = []

>>> freqs = []

# 生成每个词例的(词序,词频)点并且将其添加到相应列表中,

# 注意循环中的 fd 会自动排序

>>> for rank, word in enumerate(fd):

... ranks.append(rank+1)

... freqs.append(fd[word])

...

# 在 log-log 图中展示词序和词频的关系

>>> plt.loglog(ranks, freqs)

>>> plt.xlabel(’frequency(f)’, fontsize=14, fontweight=’bold’)

>>> plt.ylabel(’rank(r)’, fontsize=14, fontweight=’bold’)

>>> plt.grid(True)

>>> plt.show()



图 1: 齐普夫定律在古登堡语料库中适用吗?


3.4 任务 2:预测单词


现在我们已经探索过语料库了,让我们定义一个任务,能够用上之前探索的结果。


任务:训练和创建一个单词预测器,例如:给定一个训练过语料库,写一个能够预测给定单词的一下个单词的程序。使用这个预测器随机生成一个 20 个词的句子。


要创建单词预测器,我们首先要在训练过的语料库中计算两个词的顺序分布,例如,我们需要累加给定单词接下来这个单词的出现次数。一旦我们计算出了分布,我们就可以通过输入一个单词,得到它在语料库中所有可能出现的下一个单词列表,并且可以从列表中随机输出一个单词。为了随机生成一个 20 个单词的句子,我只需要给定一个初始单词,利用预测器来预测下一个单词,然后重复操作指导直到句子满 20 个词。清单 2 描述了怎么利用 NLTK 提供的模块来简单实现。我们利用简奥斯丁的 Persuasion 作为训练语料库。


清单 2:利用 NLTK 预测单词


>>> from nltk.corpus import gutenberg

>>> from nltk import ConditionalFreqDist

>>> from random import choice

# 分布实例化

>>> cfd = ConditionalFreqDist()

# 对于每个实例,统计给定词的下一个词数量

>>> prev_word = None

>>> for word in gutenberg.words(’austen-persuasion.txt’):

... cfd[prev_word].inc(word)

... prev_word = word

# 给定“therefore”作为给定词作为预测器的初始词

>>> word = ’therefore’

>>> i = 1

# 找到给定词的所有下一个可能的词,并随机选择一个

>>> while i < 20:

... print word,

... lwords = cfd[word].samples()

... follower = choice(lwords)

... word = follower

... i += 1

...

therefore it known of women ought. Leave me so well

placed in five altogether well placed themselves delighted


解答:输出的 20 个单词的句子当然不合语法。但就词的角度两两来看,是合语法的,因为用以估计条件分布概率(conditional frequency distribution)的训练语料库是合乎语法的,而我们正是使用了这个条件分布概率。注意在本任务中,我们使用前一个词作为预测器的上下文提示。显然也可以使用前两个,甚至前三个词。


3.5 Task 3: 探索词性标签


NLTK 结合了一系列优秀的模块允许我们训练和构建相对复杂的词性标注器。然而,对于这次的任务,我们只限于对 NTLK 内置的已经标注过的语料库进行简单分析。


任务:对内置的布朗语料库分词(Tokenize)并创建一个或多个适合的数据结构能让我们回答以下问题:


  • 最高频的词性标签是什么?

  • 哪个词被不同的词性标签标注最多

  • 男性代词对于女性代词的出现频率比率如何?

  • 多少个词是有歧义(ambiguous)的?也就是至少有两个词性标签?


对于这个任务,一定要注意 NTLK 内置了两个版本的布朗语料库:第一个我们已经在前两个任务中使用了,是原始的版本,第二个是被标注过的版本,亦即是每个句子的每个词例都被正确地词性标注过了。这一版的每个句子保存在元素为二元元组的列表中,形如: (token,tag)。例如标注过的语料库中的一个句子 the ball is green,在 NLTK 会被表示为 [(’the’,’at’), (’ball’,’nn’), (’is’,’vbz’), (’green’,’jj’)]。


前面已经解释过了,布朗语料库包含 15 个不同的部分,用单词 “a” 到 “r” 来表示。每个部分代表不同的文本类型,这样分是很有必要的,但这不在本文讨论范围内。有了这个信息,我们必须要构建数据结构来分析这个标注过的语料库。思考我们需要解决的问题,我们要运用文本中的词例来发现词性标准的频率分布和词性标签的条件频率分布。注意 NLTK 同时也允许我们直接从顶层命名空间导入 FreqDist 和 ConditionalFreqDist 类。清单 3 演示了怎样在 NLTK 使用。


清单 3: 借助 NLTK 分析标注过的语料库


>>> from nltk.corpus import brown

>>> from nltk import FreqDist, ConditionalFreqDist

>>> fd = FreqDist()

>>> cfd = ConditionalFreqDist()

# 对于语料库中每个标注过的句子,以(词例,词性标签)对形式表示并统计词性标签和词例的词性标签

>>> for sentence in brown.tagged_sents():

... for (token, tag) in sentence:

... fd.inc(tag)

... cfd[token].inc(tag)

>>> fd.max() # 频率最高的词性标签是 ...

’NN’

>>> wordbins = [] # Initialize a list to hold (numtags,word) tuple

# 添加每个(词例的唯一词性标签,词例)元组到列表中

>>> for token in cfd.conditions():

... wordbins.append((cfd[token].B(), token))

...

# 按唯一词性标签数从高到低对元组进行排序

>>> wordbins.sort(reverse=True)

>>> print wordbins[0] # 标签最多的词例是 ...

(12, ’that’)

>>> male = [’he’,’his’,’him’,’himself’] # 男性代词

>>> female = [’she’,’hers’,’her’,’herself’] # 女性代词

>>> n_male, n_female = 0, 0 # 初始化计数器

# 男性代词总计:

>>> for m in male:

... n_male += cfd[m].N()

...

# 女性代词总计:

>>> for f in female:

... n_female += cfd[f].N()

...

>>> print float(n_male)/n_female # 计算比率

3.257

>>> n_ambiguous = 0

>>> for (ntags, token) in wordbins:

... if ntags > 1:

... n_ambiguous += 1

...

>>> n_ambiguous # 词性标签归属多于一个的词例数

8729


解答:在布朗语料库中最高频的词性标签理所当然是名词(NN)。含有最多词性标签的是词是 that。语料库中的男性代词使用率差不多是女性代词的三倍。最后,语料库中有 8700 多的词仍有歧义——这个数字可以看出词性标注任务的困难程度。


3.6 任务 4: 单词联想(Word Association)


自由单词联想是神经语言学(Psycholinguistics)常见的任务,尤其是在词汇检索(lexical retrieval)的语境下——对于人类受试者(human subjects)而言,在单词联想上,更倾向于选择有高度联想性词,而非完全无关的词。这说明单词联想的处理是相当直接的——受试者在听到一个特殊的词时要马上从心里泛起另一个词。


任务:利用大规模词性标注过的语料库来实现自由单词联想。忽略功能词(function words),假设联想词都是名词。


对于这个任务而言,需要用到“词共现”(word co-occurrences)这一概念,例如:统计彼此间最接近的单词出现次数,然后藉此估算出联想度。对于句子中的每个词例,我们将其观察规定范围内接下来所有的词并且利用条件频率分布统计它们在该语境的出现率。清单 4 演示了我们怎么用 Python 和 NLTK 对规定在 5 个单词的范围内的词性标注过的布朗语料库进行处理。


Listing 4: 利用 NLTK 实现单词联想


>>> from nltk.corpus import brown, stopwords

>>> from nltk import ConditionalFreqDist

>>> cfd = ConditionalFreqDist()

# 得到英文停用词表

>>> stopwords_list = stopwords.words(’english’)

# 定义一个函数,如果属于名词类则返回true

>>> def is_noun(tag):

... return tag.lower() in [’nn’,’nns’,’nn$’,’nn-tl’,’nn+bez’,

’nn+hvz’, ’nns$’,’np’,’np$’,’np+bez’,’nps’,

’nps$’,’nr’,’np-tl’,’nrs’,’nr$’]

...

# 统计前 5 个单词的出现次数

>>> for sentence in brown.tagged_sents():

... for (index, tagtuple) in enumerate(sentence):

... (token, tag) = tagtuple

... token = token.lower()

... if token not in stopwords_list and is_noun(tag):

... window = sentence[index+1:index+5]

... for (window_token, window_tag) in window:

... window_token = window_token.lower()

... if window_token not in stopwords_list and

is_noun(window_tag):

... cfd[token].inc(window_token)

# 好了。我们完成了!让我们开始进行联想!

>>> print cfd[’left’].max()

right

>>> print cfd[’life’].max()

death

>>> print cfd[’man’].max()

woman

>>> print cfd[’woman’].max()

world

>>> print cfd[’boy’].max()

girl

>>> print cfd[’girl’].max()

trouble

>>> print cfd[’male’].max()

female

>>> print cfd[’ball’].max()

player

>>> print cfd[’doctor’].max()

bills

>>> print cfd[’road’].max()

block


解答:我们构建的“单词联想器(word associator)”效果似乎出乎意料得好,尤其是在这么小的工作量下(事实上,在大众心理学的的语境下,我们的联想器似乎具备人类的特性,尽管是消极和悲观的)。我们的任务结果明确指出了通用语料库语言学的有效性。作为进一步的练习,联想器借助句法分析过的语料库和基于信息理论的联想测试能够很容易进行成熟的拓展[3]。


4 讨论


虽然本文使用了 Python 和 NLTK 作为基础 NLP 的介绍,但请注意,在 NLTK 之外还有另外的 NLP 框架活跃于学术界和工业界。其中比较流行的是 GATE(General Architecture for Text Engineering),由 University of Sheffield[4] 的 NLP 研究小组开发。使用 Java 编写的视窗环境,基础架构描述了语言处理部件彼此间的联系。GATE 提供免费下载,主要应用于文本挖掘(text mining)和信息抽取(information extraction)。


每种编程语言和框架都各有优劣。就本文而言,我们选择 Python 是因为与其他语言相比,它的优势在于:(a)可读性高(b)面相对象的范例(object-oriented paradigm)易于入门(c)易于扩展(d)强大的解码支持(e)强大的标准库。而且特别稳健,高效,这些都已经在复杂和大规模的 NLP 项目中得到了验证,例如最新的机器翻译解码器[2]。


5 结论


自然语言处理是非常热门的研究领域因此每年吸引了非常多研究生。它集合了多个学科诸如语言学,心理学,计算科学和数学的优势来研究人类语言。另外选择 NLP 作为研究生生涯更重要的原因是大量有意思的难题都没有固定的解决办法。举个例子,机器翻译初始问题(original problem)的存在推动了该领域的发展,即使经过二十年诱人而又活跃的研究以后,这个难题依旧尚待解决。还有另外几个前沿的 NLP 问题目前已经有大量的研究工作,其中一些列举如下:


  • 基于句法的机器翻译:从过去的数十年到现在,绝大部分的机器翻译都聚焦在使用统计方法通过大量语料库来学习词和短语的翻译。然而,越来越多的研究者开始在研究中加入句法[10]。

  • 多文本摘要:目前大量工作都是利用计算机从相近的文档集合[8]中自动生成高度相关的摘要。这个任务被视为比单文本的摘要困难,因为多文本中冗余信息更多。

  • 计算句法分析:虽然使用概率模型自动生成给定文本的句法结构由来已久,但进步空间还很大。最大的挑战是准确的分析,当英语拿来和中文[7]、阿拉伯语比较的时候,语言特性差异很大。


Python 和 NLTK 使每个编程人员不需要花费大量时间在获取资源上,直接可以接触 NLP 任务。文本意在给任何对学习 NLP 感兴趣的人提供解决这些简单的任务例子和参考。


6 作者简介


Nitin Madnani 是 Educational Testing Service(ETS)研究科学家。他此前是 University of Maryland, College Park 计算机科学系的博士生和 Institute for Advanced Computer Studies 助理研究员。他的研究方向是统计自然语言处理,尤其是机器翻译和文本摘要。不论任务大小,他始终坚信:Python大法好。


参考文献


[1]: Dan Bikel. 2004. On the Parameter Space of Generative Lexicalized Statistical Parsing Models. Ph.D. Thesis. http://www.cis.upenn.edu/~dbikel/papers/thesis.pdf


[2]: David Chiang. 2005. A hierarchical phrase-based model for statistical machine translation. Proceedings of ACL.


[3]: Kenneth W. Church and Patrick Hanks. 1990. Word association norms, mutual information, and lexicography. Computational Linguistics. 16(1).


[4:] H. Cunningham, D. Maynard, K. Bontcheva. and V. Tablan. 2002. GATE: A Framework and Graphical Development Environment for Robust NLP Tools and Applications. Proceedings of the 40th Anniversary Meeting of the Association for Computational Linguistics. 15


[5]: Michael Hart and Gregory Newby. Project Gutenberg. Proceedings of the 40th Anniversary Meeting of the Association for Computational Linguistics. http://www.gutenberg.org/wiki/Main_Page


[6]: H. Kucera and W. N. Francis. 1967. Computational Analysis of PresentDay American English. Brown University Press, Providence, RI.


[7]: Roger Levy and Christoper D. Manning. 2003. Is it harder to parse Chinese, or the Chinese Treebank ? Proceedings of ACL.


[8]: Dragomir R. Radev and Kathy McKeown. 1999. Generating natural language summaries from multiple on-line sources. Computational Linguistics. 24:469-500.


[9]: Adwait Ratnaparkhi 1996. A Maximum Entropy Part-Of-Speech Tagger. Proceedings of Empirical Methods on Natural Language Processing.


[10]: Dekai Wu and David Chiang. 2007. Syntax and Structure in Statistical Translation. Workshop at HLT-NAACL.


[11]: The Official Python Tutorial. http://docs.python.org/tut/tut.html


[12]: Natural Language Toolkit. http://nltk.sourceforge.net


[13]: NLTK Tutorial. http://nltk.sourceforge.net/index.php/Book


看完本文有收获?请转发分享给更多人

关注「大数据与机器学习文摘」,成为Top 1%

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存