Python中一些自然语言工具的使用的入门教程
NLTK是使用Python教学以及实践计算语言学的极好工具。此外,计算语言学与人工智能、语言/专门语言识别、翻译以及语法检查等领域关系密切。
NLTK包括什么
NLTK会被自然地看作是具有栈结构的一系列层,这些层构建于彼此基础之上。那些熟悉人工语言(比如Python)的文法和解析的读者来说,理解自然语言模型中类似的——但更深奥的——层不会有太大困难。
术语表
全集(Corpora):相关文本的集合。例如,莎士比亚的作品可能被统称为一个文集(corpus);而若干个作者的作品称为全集。
直方图(Histogram):数据集中不同单词、字母或其他条目的出现频率的统计分布。
结构(Syntagmatic):对语段的研究;也就是全集中字母、单词或短语连续出现的统计关系。
上下文无关语法(Context-freegrammar):由四类形式语法构成的NoamChomsky层级中的第二类。参阅参考资料以获得详尽描述。
尽管NLTK附带了很多已经预处理(通常是手工地)到不同程度的全集,但是概念上每一层都是依赖于相邻的更低层次的处理。首先是断词;然后是为单词加上标签;然后将成组的单词解析为语法元素,比如名词短语或句子(取决于几种技术中的某一种,每种技术都有其优缺点);最后对最终语句或其他语法单元进行分类。通过这些步骤,NLTK让您可以生成关于不同元素出现情况的统计,并画出描述处理过程本身或统计合计结果的图表。
在本文中,您将看到关于低层能力的一些相对完整的示例,而对大部分高层次能力将只是进行简单抽象的描述。现在让我们来详细分析文本处理的首要步骤。
断词(Tokenization)
您可以使用NLTK完成的很多工作,尤其是低层的工作,与使用Python的基本数据结构来完成相比,并没有太大的区别。不过,NLTK提供了一组由更高的层所依赖和使用的系统化的接口,而不只是简单地提供实用的类来处理加过标志或加过标签的文本。
具体讲,nltk.tokenizer.Token类被广泛地用于存储文本的有注解的片断;这些注解可以标记很多不同的特性,包括词类(parts-of-speech)、子标志(subtoken)结构、一个标志(token)在更大文本中的偏移位置、语形词干(morphologicalstems)、文法语句成分,等等。实际上,一个Token是一种特别的字典——并且以字典形式访问——所以它可以容纳任何您希望的键。在NLTK中使用了一些专门的键,不同的键由不同的子程序包所使用。
让我们来简要地分析一下如何创建一个标志并将其拆分为子标志:
清单1.初识nltk.tokenizer.Token类
>>>fromnltk.tokenizerimport* >>>t=Token(TEXT='Thisismyfirsttestsentence') >>>WSTokenizer().tokenize(t,addlocs=True)#breakonwhitespace >>>printt['TEXT'] Thisismyfirsttestsentence >>>printt['SUBTOKENS'] [<This>@[0:4c],<is>@[5:7c],<my>@[8:10c],<first>@[11:16c], <test>@[17:21c],<sentence>@[22:30c]] >>>t['foo']='bar' >>>t <TEXT='Thisismyfirsttestsentence',foo='bar', SUBTOKENS=[<This>@[0:4c],<is>@[5:7c],<my>@[8:10c],<first>@[11:16c], <test>@[17:21c],<sentence>@[22:30c]]> >>>printt['SUBTOKENS'][0] <This>@[0:4c] >>>printtype(t['SUBTOKENS'][0]) <class'nltk.token.SafeToken'>
概率(Probability)
对于语言全集,您可能要做的一件相当简单的事情是分析其中各种事件(events)的频率分布,并基于这些已知频率分布做出概率预测。NLTK支持多种基于自然频率分布数据进行概率预测的方法。我将不会在这里介绍那些方法(参阅参考资料中列出的概率教程),只要说明您肯定会期望的那些与您已经知道的那些(不止是显而易见的缩放比例/正规化)之间有着一些模糊的关系就够了。
基本来讲,NLTK支持两种类型的频率分布:直方图和条件频率分布(conditionalfrequency)。nltk.probability.FreqDist类用于创建直方图;例如,可以这样创建一个单词直方图:
清单2.使用nltk.probability.FreqDist创建基本的直方图
>>>fromnltk.probabilityimport* >>>article=Token(TEXT=open('cp-b17.txt').read()) >>>WSTokenizer().tokenize(article) >>>freq=FreqDist() >>>forwordinarticle['SUBTOKENS']: ...freq.inc(word['TEXT']) >>>freq.B() 1194 >>>freq.count('Python') 12
概率教程讨论了关于更复杂特性的直方图的创建,比如“以元音结尾的词后面的词的长度”。nltk.draw.plot.Plot类可用于直方图的可视化显示。当然,您也可以这样分析高层次语法特性或者甚至是与NLTK无关的数据集的频率分布。
条件频率分布可能比普通的直方图更有趣。条件频率分布是一种二维直方图——它按每个初始条件或者“上下文”为您显示一个直方图。例如,教程提出了一个对应每个首字母的单词长度分布问题。我们就以这样分析:
清单3.条件频率分布:对应每个首字母的单词长度
>>>cf=ConditionalFreqDist() >>>forwordinarticle['SUBTOKENS']: ...cf[word['TEXT'][0]].inc(len(word['TEXT'])) ... >>>init_letters=cf.conditions() >>>init_letters.sort() >>>forcininit_letters[44:50]: ...print"Init%s:"%c, ...forlengthinrange(1,6): ...print"len%d/%.2f,"%(length,cf[c].freq(n)), ...print ... Inita:len1/0.03,len2/0.03,len3/0.03,len4/0.03,len5/0.03, Initb:len1/0.12,len2/0.12,len3/0.12,len4/0.12,len5/0.12, Initc:len1/0.06,len2/0.06,len3/0.06,len4/0.06,len5/0.06, Initd:len1/0.06,len2/0.06,len3/0.06,len4/0.06,len5/0.06, Inite:len1/0.18,len2/0.18,len3/0.18,len4/0.18,len5/0.18, Initf:len1/0.25,len2/0.25,len3/0.25,len4/0.25,len5/0.25,
条件频率分布在语言方面的一个极好应用是分析全集中的语段分布——例如,给出一个特定的词,接下来最可能出现哪个词。当然,语法会带来一些限制;不过,对句法选项的选择的研究属于语义学、语用论和术语范畴。
词干提取(Stemming)
nltk.stemmer.porter.PorterStemmer类是一个用于从英文单词中获得符合语法的(前缀)词干的极其便利的工具。这一能力尤其让我心动,因为我以前曾经用Python创建了一个公用的、全文本索引的搜索工具/库(见Developingafull-textindexerinPython中的描述,它已经用于相当多的其他项目中)。
尽管对大量文档进行关于一组确切词的搜索的能力是非常实用的(gnosis.indexer所做的工作),但是,对很多搜索用图而言,稍微有一些模糊将会有所帮助。也许,您不能特别确定您正在寻找的电子邮件是否使用了单词“complicated”、“complications”、“complicating”或者“complicates”,但您却记得那是大概涉及的内容(可能与其他一些词共同来完成一次有价值的搜索)。
NLTK中包括一个用于单词词干提取的极好算法,并且让您可以按您的喜好定制词干提取算法:
清单4.为语形根(morphologicalroots)提取单词词干
>>>fromnltk.stemmer.porterimportPorterStemmer >>>PorterStemmer().stem_word('complications') 'complic'
实际上,您可以怎样利用gnosis.indexer及其衍生工具或者完全不同的索引工具中的词干提取功能,取决于您的使用情景。幸运的是,gnosis.indexer有一个易于进行专门定制的开放接口。您是否需要一个完全由词干构成的索引?或者您是否在索引中同时包括完整的单词和词干?您是否需要将结果中的词干匹配从确切匹配中分离出来?在未来版本的gnosis.indexer中我将引入一些种类词干的提取能力,不过,最终用户可能仍然希望进行不同的定制。
无论如何,一般来说添加词干提取是非常简单的:首先,通过特别指定gnosis.indexer.TextSplitter来从一个文档中获得词干;然后,当然执行搜索时,(可选地)在使用搜索条件进行索引查找之前提取其词干,可能是通过定制您的MyIndexer.find()方法来实现。
在使用PorterStemmer时我发现nltk.tokenizer.WSTokenizer类确实如教程所警告的那样不好用。它可以胜任概念上的角色,但是对于实际的文本而言,您可以更好地识别出什么是一个“单词”。幸运的是,gnosis.indexer.TextSplitter是一个健壮的断词工具。例如:
清单5.基于拙劣的NLTK断词工具进行词干提取
>>>fromnltk.tokenizerimport* >>>article=Token(TEXT=open('cp-b17.txt').read()) >>>WSTokenizer().tokenize(article) >>>fromnltk.probabilityimport* >>>fromnltk.stemmer.porterimport* >>>stemmer=PorterStemmer() >>>stems=FreqDist() >>>forwordinarticle['SUBTOKENS']: ...stemmer.stem(word) ...stems.inc(word['STEM'].lower()) ... >>>word_stems=stems.samples() >>>word_stems.sort() >>>word_stems[20:40] ['"generator-bas','"implement','"lazili','"magic"','"partial', '"pluggable"','"primitives"','"repres','"secur','"semi-coroutines."', '"state','"understand','"weightless','"whatev','#','#-----', '#----------','#-------------','#---------------','#b17:']
查看一些词干,集合中的词干看起来并不是都可用于索引。很多根本不是实际的单词,还有其他一些是用破折号连接起来的组合词,单词中还被加入了一些不相干的标点符号。让我们使用更好的断词工具来进行尝试:
清单6.使用断词工具中灵巧的启发式方法来进行词干提取
>>>article=TS().text_splitter(open('cp-b17.txt').read()) >>>stems=FreqDist() >>>forwordinarticle: ...stems.inc(stemmer.stem_word(word.lower())) ... >>>word_stems=stems.samples() >>>word_stems.sort() >>>word_stems[60:80] ['bool','both','boundari','brain','bring','built','but','byte', 'call','can','cannot','capabl','capit','carri','case','cast', 'certain','certainli','chang','charm']
在这里,您可以看到有一些单词有多个可能的扩展,而且所有单词看起来都像是单词或者词素。断词方法对随机文本集合来说至关重要;公平地讲,NLTK捆绑的全集已经通过WSTokenizer()打包为易用且准确的断词工具。要获得健壮的实际可用的索引器,需要使用健壮的断词工具。
添加标签(tagging)、分块(chunking)和解析(parsing)
NLTK的最大部分由复杂程度各不相同的各种解析器构成。在很大程度上,本篇介绍将不会解释它们的细节,不过,我愿意大概介绍一下它们要达成什么目的。
不要忘记标志是特殊的字典这一背景——具体说是那些可以包含一个TAG键以指明单词的语法角色的标志。NLTK全集文档通常有部分专门语言已经预先添加了标签,不过,您当然可以将您自己的标签添加到没有加标签的文档。
分块有些类似于“粗略解析”。也就是说,分块工作的进行,或者基于语法成分的已有标志,或者基于您手工添加的或者使用正则表达式和程序逻辑半自动生成的标志。不过,确切地说,这不是真正的解析(没有同样的生成规则)。例如:
清单7.分块解析/添加标签:单词和更大的单位
>>>fromnltk.parser.chunkimportChunkedTaggedTokenizer >>>chunked="[the/DTlittle/JJcat/NN]sat/VBDon/IN[the/DTmat/NN]" >>>sentence=Token(TEXT=chunked) >>>tokenizer=ChunkedTaggedTokenizer(chunk_node='NP') >>>tokenizer.tokenize(sentence) >>>sentence['SUBTOKENS'][0] (NP:<the/DT><little/JJ><cat/NN>) >>>sentence['SUBTOKENS'][0]['NODE'] 'NP' >>>sentence['SUBTOKENS'][0]['CHILDREN'][0] <the/DT> >>>sentence['SUBTOKENS'][0]['CHILDREN'][0]['TAG'] 'DT' >>>chunk_structure=TreeToken(NODE='S',CHILDREN=sentence['SUBTOKENS']) (S: (NP:<the/DT><little/JJ><cat/NN>) <sat/VBD> <on/IN> (NP:<the/DT><mat/NN>))
所提及的分块工作可以由nltk.tokenizer.RegexpChunkParser类使用伪正则表达式来描述构成语法元素的一系列标签来完成。这里是概率教程中的一个例子:
清单8.使用标签上的正则表达式进行分块
>>>rule1=ChunkRule('<DT>?<JJ.*>*<NN.*>', ...'Chunkoptionaldet,zeroormoreadj,andanoun') >>>chunkparser=RegexpChunkParser([rule1],chunk_node='NP',top_node='S') >>>chunkparser.parse(sentence) >>>printsent['TREE'] (S:(NP:<the/DT><little/JJ><cat/NN>) <sat/VBD><on/IN> (NP:<the/DT><mat/NN>))
真正的解析将引领我们进入很多理论领域。例如,top-down解析器可以确保找到每一个可能的产品,但可能会非常慢,因为要频繁地(指数级)进行回溯。Shift-reduce效率更高,但是可能会错过一些产品。不论在哪种情况下,语法规则的声明都类似于解析人工语言的语法声明。本专栏曾经介绍了其中的一些:SimpleParse、mx.TextTools、Spark和gnosis.xml.validity(参阅参考资料)。
甚至,除了top-down和shift-reduce解析器以外,NLTK还提供了“chart解析器”,它可以创建部分假定,这样一个给定的序列就可以继而完成一个规则。这种方法可以是既有效又完全的。举一个生动的(玩具级的)例子:
清单9.为上下文无关语法定义基本的产品
>>>fromnltk.parser.chartimport* >>>grammar=CFG.parse(''' ...S->NPVP ...VP->VNP|VPPP ...V->"saw"|"ate" ...NP->"John"|"Mary"|"Bob"|DetN|NPPP ...Det->"a"|"an"|"the"|"my" ...N->"dog"|"cat"|"cookie" ...PP->PNP ...P->"on"|"by"|"with" ...''') >>>sentence=Token(TEXT='Johnsawacatwithmycookie') >>>WSTokenizer().tokenize(sentence) >>>parser=ChartParser(grammar,BU_STRATEGY,LEAF='TEXT') >>>parser.parse_n(sentence) >>>fortreeinsentence['TREES']:printtree (S: (NP:<John>) (VP: (VP:(V:<saw>)(NP:(Det:<a>)(N:<cat>))) (PP:(P:<with>)(NP:(Det:<my>)(N:<cookie>))))) (S: (NP:<John>) (VP: (V:<saw>) (NP: (NP:(Det:<a>)(N:<cat>)) (PP:(P:<with>)(NP:(Det:<my>)(N:<cookie>))))))
probabilisticcontext-freegrammar(或者说是PCFG)是一种上下文无关语法,它将其每一个产品关联到一个概率。同样,用于概率解析的解析器也捆绑到了NLTK中。
您在等待什么?
NLTK还有其他本篇简短介绍中不能涵盖的重要功能。例如,NLTK有一个完整的框架,用于通过类似于“naiveBayesian”和“maximumentropy”等模型的统计技术进行文本分类。即使还有篇幅,现在我也还不能解释其本质。不过,我认为,即使是NLTK较低的层,也可以成为一个既可用于教学应用程序也可用于实际应用程序的实用框架。