本案例首先以布朗语料库为例,介绍如何利用NLTK获取内置语料库并进行简单统计以及创建本地语料库,接着以WordNet为例,介绍了NLTK中语言知识库的使用方法。

1.语料库的获取和使用

1.1 语料库的获取

首先我们载入nltk库并调用__version__方法查看一下版本。

In [1]:
import nltk
In [2]:
nltk.__version__
Out[2]:
'3.5'

调用函数nltk.download()会打开NLTK Downloader交互器,上面可以浏览并下载可用的软件包,包括文本集、语料库和模型等。但由于下载速度较慢且下载时容易中断,建议大家从官网nltk.org/nltk_data/ 直接下载数据,放在指定目录下即可。我们首先下载并加载布朗语料库,它创建于20世纪60年代初,收录了15种不同类别,总计500个文本,每个文本超过2000词,整个语料库约1014300词。

In [3]:
# 加载布朗语料库
from nltk.corpus import brown

调用fileids()方法可以查看语料库中所有文件的文件名,一共有500个文件,每个文件代表一个文本。

In [4]:
# 查看这个语料库中的文件名
brown.fileids()[:10]
Out[4]:
['ca01',
 'ca02',
 'ca03',
 'ca04',
 'ca05',
 'ca06',
 'ca07',
 'ca08',
 'ca09',
 'ca10']
In [262]:
len(brown.fileids())
Out[262]:
500

调用categories()方法可以查看语料库中所有的文本类别,一共有15个类别,分别为:

类别 中文解释
adventure 冒险
belles_lettres 纯文学
editorial 社论
fiction 小说
government 政府
hobbies 爱好
humor 幽默文学
learned 博览
lore 传说
mystery 推理小说
news 新闻
religion 宗教
reviews 评论
romance 言情
science_fiction 科幻小说
In [263]:
# 查看所有类别
brown.categories()
Out[263]:
['adventure',
 'belles_lettres',
 'editorial',
 'fiction',
 'government',
 'hobbies',
 'humor',
 'learned',
 'lore',
 'mystery',
 'news',
 'religion',
 'reviews',
 'romance',
 'science_fiction']

调用raw()方法可以访问原始语料库文本,通过设定参数categories='romance'获取言情类的文本,返回的文本中包括分词、词性标注和命名实体识别的结果。

In [271]:
brown.raw(categories='romance')[:1000]
Out[271]:
'\n\n\tThey/ppss neither/cc liked/vbd nor/cc disliked/vbd the/at Old/jj-tl Man/nn-tl ./.\nTo/in them/ppo he/pps could/md have/hv been/ben the/at broken/vbn bell/nn in/in the/at church/nn tower/nn which/wdt rang/vbd before/in and/cc after/in Mass/nn-tl ,/, and/cc at/in noon/nn ,/, and/cc at/in six/cd each/dt evening/nn --/-- its/pp$ tone/nn ,/, repetitive/jj ,/, monotonous/jj ,/, never/rb breaking/vbg the/at boredom/nn of/in the/at streets/nns ./.\nThe/at Old/jj-tl Man/nn-tl was/bedz unimportant/jj ./.\n\n\n\tYet/rb if/cs he/pps were/bed not/* there/rb ,/, they/ppss would/md have/hv missed/vbn him/ppo ,/, as/cs they/ppss would/md have/hv missed/vbn the/at sounds/nns of/in bees/nns buzzing/vbg against/in the/at screen/nn door/nn in/in early/jj June/np ;/. ;/.\nor/cc the/at smell/nn of/in thick/jj tomato/nn paste/nn --/-- the/at ripe/jj smell/nn that/wps was/bedz both/abx sweet/jj and/cc sour/jj --/-- rising/vbg up/rp from/in aluminum/nn trays/nns wrapped/vbn in/in fly-dotted/jj cheesecloth/nn ./.\nO'

通过调用words()sents()paras()方法可以分别以词、句子和段落的形式访问语料库文本,返回结果是一个列表,其中的元素分别为单个词、单个句子和一个段落。

In [272]:
# 分隔为词,根据类别选择文本
print(brown.words(categories='romance')[0])
print("-"*60)

# 分隔为句子,根据类别选择文本
print(brown.sents(categories='romance')[0])
print("-"*60)

# 分隔为段落,根据文件名选择文本
print(brown.paras(categories='romance')[0])
They
------------------------------------------------------------
['They', 'neither', 'liked', 'nor', 'disliked', 'the', 'Old', 'Man', '.']
------------------------------------------------------------
[['They', 'neither', 'liked', 'nor', 'disliked', 'the', 'Old', 'Man', '.'], ['To', 'them', 'he', 'could', 'have', 'been', 'the', 'broken', 'bell', 'in', 'the', 'church', 'tower', 'which', 'rang', 'before', 'and', 'after', 'Mass', ',', 'and', 'at', 'noon', ',', 'and', 'at', 'six', 'each', 'evening', '--', 'its', 'tone', ',', 'repetitive', ',', 'monotonous', ',', 'never', 'breaking', 'the', 'boredom', 'of', 'the', 'streets', '.'], ['The', 'Old', 'Man', 'was', 'unimportant', '.']]

除了根据文本类别选择文本,还可以调用参数fileids通过文件名选择文本,我们设定fileids='ca01'获取文本ca01。

In [91]:
# 分隔为段落,根据文件名选择文本
print(' '.join(brown.paras(fileids='ca01')[0][0]))
The Fulton County Grand Jury said Friday an investigation of Atlanta's recent primary election produced `` no evidence '' that any irregularities took place .

1.2 语料库简单统计

在获取语料库并了解了如何访问其中的文本之后,我们利用NLTK对语料库中的文本进行一些简单统计。首先我们调用FreqDist类,对言情类文本建立有序字典并按词频从高到低排序

In [273]:
# 建立有序字典,按词频从高到底排序
fdist = nltk.FreqDist(brown.words(categories='romance'))
fdist
Out[273]:
FreqDist({',': 3899, '.': 3736, 'the': 2758, 'and': 1776, 'to': 1502, 'a': 1335, 'of': 1186, '``': 1045, "''": 1044, 'was': 993, ...})

查看词频最高的10个词,发现标点和停用词占了大多数,我们要想办法去掉标点和停用词后再进行统计。

In [57]:
# 查看词频最高的10个词
sorted(fdist.items(), key=lambda items:items[1], reverse=True)[:10]
Out[57]:
[(',', 3899),
 ('.', 3736),
 ('the', 2758),
 ('and', 1776),
 ('to', 1502),
 ('a', 1335),
 ('of', 1186),
 ('``', 1045),
 ("''", 1044),
 ('was', 993)]

首先载入停用词语料库,接着加载英语停用词表,然后加载标记生成器RegexpTokenizer,将字母或者数字作为标记,删除其它内容,以此来删除标点,最后定义一个函数,输入一个文本,先去掉停用词,再去掉标点,返回处理后的结果。

In [121]:
# 加载停用词语料库
from nltk.corpus import stopwords

# 加载英语停用词表
english_stopwords = stopwords.words('english')

# 加载标记生成器
from nltk.tokenize import RegexpTokenizer

# 字母数字字符作为标记,删除其它所有内容
tokenizer = RegexpTokenizer(r'\w+')

# 定义函数去除停用词和标点
def remove_stopwords_punc(text):
    
    result = [w for w in text if w.lower() not in english_stopwords]
    result = [tokenizer.tokenize(w)[0] for w in result if tokenizer.tokenize(w)!=[]]
    return result
In [124]:
# 去除停用词和标点
new_text = remove_stopwords_punc(brown.words(categories='romance'))

再次建立有序字典,查看词频最高的10个词,可以看到停用词和标点都被去掉了。

In [125]:
# 再次建立有序字典,按词频从高到底排序
fdist1 = nltk.FreqDist(new_text)

# 查看词频最高的10个词
sorted(fdist1.items(), key=lambda items:items[1], reverse=True)[:10]
Out[125]:
[('said', 330),
 ('would', 244),
 ('could', 194),
 ('I', 190),
 ('like', 185),
 ('one', 175),
 ('back', 126),
 ('thought', 105),
 ('didn', 101),
 ('little', 100)]

调用FreqDist类的plot()方法可以绘制词频数量的累积折线图,可以看到前10名的词频总量接近1800个。

In [127]:
# 绘图
%matplotlib inline
fdist1.plot(10, cumulative=True)
Out[127]:
<matplotlib.axes._subplots.AxesSubplot at 0x21a13e5b080>

NLTK中的ConditionalFreqDist类可以计算词的条件频率分布,当语料库文本被分为几类(文体、主题、作者)时,可以计算每个类别独立的频率分布,这样就可以研究类别之间的系统性差异。下面我们将文本分隔为词并利用不同文本类别进行归类,组成有序词典。

In [66]:
# 带条件的词频率分布
cfd = nltk.ConditionalFreqDist(
            (genre, word)
            for genre in brown.categories()
            for word in brown.words(categories=genre))

指定一些类别,这里指定新闻news、爱好hobbies和幽默humor三个类别,并指定一些情态动词,包括can、could、may、must和will,调用tabulate()方法输出不同指定类别下情态动词的词频数量,建立关系表。从表中可以看到新闻类别中,情态动词will的词频最高;爱好类别中,情态动词can和will词频较高;幽默类别中,指定的情态动词词频都不高。

In [68]:
# 指定一部分类别
cats = ['news', 'hobbies', 'humor']

# 指定一些情态动词
modals = ['can', 'could', 'may', 'must', 'will']

# 输出指定类别和情态动词的数量关系表
cfd.tabulate(conditions=cats, samples=modals)
          can could   may  must  will 
   news    93    86    66    50   389 
hobbies   268    58   131    83   264 
  humor    16    30     8     9    13 

1.3 加载本地语料库

除了加载内置的语料库外,NLTK也支持加载本地语料库。以1998年人名日报语料库为例,首先将语料库的txt文件放到指定目录下,并指定文件过滤的方法,调用PlaintextCorpusReader类并输入语料库的文件路径和文件过滤方法,就可以创建一个本地语料库了。

In [278]:
from nltk.corpus import PlaintextCorpusReader
# 语料库本地目录
coupus_root = ".\dict"
# 加载文件过滤
file_pattern = '.*'
wodslist = PlaintextCorpusReader(coupus_root, file_pattern)

依然可以使用上述介绍的方法访问本地语料库中的文本。

In [285]:
print(wodslist.raw('词性标注@人民日报199801.txt')[:1000])
print("-"*60)
print(wodslist.words('词性标注@人民日报199801.txt')[:100])
19980101-01-001-001/m  迈向/v  充满/v  希望/n  的/u  新/a  世纪/n  ——/w  一九九八年/t  新年/t  讲话/n  (/w  附/v  图片/n  1/m  张/q  )/w  
19980101-01-001-002/m  中共中央/nt  总书记/n  、/w  国家/n  主席/n  江/nr  泽民/nr  
19980101-01-001-003/m  (/w  一九九七年/t  十二月/t  三十一日/t  )/w  
19980101-01-001-004/m  12月/t  31日/t  ,/w  中共中央/nt  总书记/n  、/w  国家/n  主席/n  江/nr  泽民/nr  发表/v  1998年/t  新年/t  讲话/n  《/w  迈向/v  充满/v  希望/n  的/u  新/a  世纪/n  》/w  。/w  (/w  新华社/nt  记者/n  兰/nr  红光/nr  摄/Vg  )/w  
19980101-01-001-005/m  同胞/n  们/k  、/w  朋友/n  们/k  、/w  女士/n  们/k  、/w  先生/n  们/k  :/w  
19980101-01-001-006/m  在/p  1998年/t  来临/v  之际/f  ,/w  我/r  十分/m  高兴/a  地/u  通过/p  [中央/n  人民/n  广播/vn  电台/n]nt  、/w  [中国/ns  国际/n  广播/vn  电台/n]nt  和/c  [中央/n  电视台/n]nt  ,/w  向/p  全国/n  各族/r  人民/n  ,/w  向/p  [香港/ns  特别/a  行政区/n]ns  同胞/n  、/w  澳门/ns  和/c  台湾/ns  同胞/n  、/w  海外/s  侨胞/n  ,/w  向/p  世界/n  各国/r  的/u  朋友/n  们/k  ,/w  致以/v  诚挚/a  的/u  问候/vn  和/c  良好/a  的/u  祝愿/vn  !/w  
19980101-01-001-007/m  1997年/t  ,/w  是/v  中国/ns  发展/vn  历史/n  上/f  非常/d  重要
------------------------------------------------------------
['19980101', '-', '01', '-', '001', '-', '001', '/', 'm', '迈向', '/', 'v', '充满', '/', 'v', '希望', '/', 'n', '的', '/', 'u', '新', '/', 'a', '世纪', '/', 'n', '——/', 'w', '一九九八年', '/', 't', '新年', '/', 't', '讲话', '/', 'n', '(/', 'w', '附', '/', 'v', '图片', '/', 'n', '1', '/', 'm', '张', '/', 'q', ')/', 'w', '19980101', '-', '01', '-', '001', '-', '002', '/', 'm', '中共中央', '/', 'nt', '总书记', '/', 'n', '、/', 'w', '国家', '/', 'n', '主席', '/', 'n', '江', '/', 'nr', '泽民', '/', 'nr', '19980101', '-', '01', '-', '001', '-', '003', '/', 'm', '(/', 'w', '一九九七年', '/', 't', '十二月', '/', 't']

注:NLTK内置的其它语料库及语料文本的下载信息请参阅nltk.org/nltk_data/ ,如何访问NLTK语料库的其它例子,请查阅nltk.org/howto

2.语言知识库的获取和使用

2.1 查看WordNet中的同义词集

NLTK中主要包含的语言知识库有WordNet和VerbNet,我们以WordNet为例,来看看在NLTK中如何使用语言知识库。WordNet是面向语义的英语词典,相较于传统字典结构更丰富,共包含155287个单词和117659个同义词。我们首先在corpus子模块中加载wordnet。

In [1]:
from nltk.corpus import wordnet

WordNet根据词条的意义将它们分组,每一个具有相同意义的词条组称为一个synset(同义词集),我们可以调用synsets()方法查看一个词的同义词集有哪些,比如“tiger”(老虎)这个词,可以看到它有两个同义词集,其中'tiger.n.01'表示tiger的第一个名词意义。

In [188]:
# 输出老虎(tiger)的同义词集
wordnet.synsets('tiger')
Out[188]:
[Synset('tiger.n.01'), Synset('tiger.n.02')]

每个同义词集都包含词集的定义和例句,通过调用definition()examples()方法可以分别查看它们,我们来查看一下'tiger.n.01'的定义和例句以及'tiger.n.02'的定义。

In [189]:
# 查看同义词集的定义和例句
print("定义:",wordnet.synset('tiger.n.01').definition())
print("例句:",wordnet.synset('tiger.n.01').examples()[0])
print("-"*60)
print("定义:",wordnet.synset('tiger.n.02').definition())
定义: a fierce or audacious person
例句: he's a tiger on the tennis court
------------------------------------------------------------
定义: large feline of forests in most of Asia having a tawny coat with black stripes; endangered

可以看到'tiger.n.01'定义的中文意思是“凶狠或大胆的人”,例句的中文意思是“他是网球场上的猛虎”。'tiger.n.01'定义的中文意思是“亚洲大部分地区的大型森林猫科动物,其茶色外表带有黑色条纹;濒临灭绝”。调用lemma()方法和lemma_names()方法可以查看同义词集中包含的词条,'tiger.n.02'一共包含两个词条,其中'tiger.n.02.Panthera_tigris'表示的是孟加拉虎,老虎的亚种。

In [190]:
# 查看同义词集中包含的词条
print("词条:",wordnet.synset('tiger.n.02').lemmas())

print("词条名:",wordnet.synset('tiger.n.02').lemma_names())
词条: [Lemma('tiger.n.02.tiger'), Lemma('tiger.n.02.Panthera_tigris')]
词条名: ['tiger', 'Panthera_tigris']

WordNet的同义词集相当于抽象的概念,这些概念在层次结构中相互联系在一起。在层次结构中,每个节点对应一个同义词集,边表示上位词/下位词关系,即上级概念与从属概念的关系。调用hyponyms()方法可以查看同义词集的下位同义词集。下面的结果可以看到'tiger.n.02'的下位同义词集是'bengal_tiger.n.01'(孟加拉虎),'tiger_cub.n.01'(小老虎)和'tigress.n.01'(雌老虎)。

In [191]:
# 查看下位同义词集
wordnet.synset('tiger.n.02').hyponyms()
Out[191]:
[Synset('bengal_tiger.n.01'), Synset('tiger_cub.n.01'), Synset('tigress.n.01')]

调用hypernyms()方法可以查看上位同义词集,'tiger.n.02'的上位同义词集为'big_cat.n.01'(大型猫科动物)。层次结构中最顶层的是11个抽象概念,称为基本类别始点,调用root_hypernyms()方法可以查看根上位同义词集,'tiger.n.02'的根上位同义词集是'entity.n.01'(实体)。

In [192]:
# 查看上位同义词集
print(wordnet.synset('tiger.n.02').hypernyms())

# 查看根上位同义词集
print(wordnet.synset('tiger.n.02').root_hypernyms())
[Synset('big_cat.n.01')]
[Synset('entity.n.01')]

调用hypernym_paths()方法可以查看同义词集的所有上位同义词路径,比如'tiger.n.02'的上位同义词路径为'big_cat.n.01'(大型猫科动物)→'feline.n.01'(猫科动物)→'carnivore.n.01'(食肉动物)→'placental.n.01'(胎生)→'mammal.n.01'(哺乳动物)→'vertebrate.n.01'(脊椎动物)→'chordate.n.01'(脊索动物)→'animal.n.01'(动物)→'organism.n.01'(有机体)→'living_thing.n.01'(生物)→'whole.n.02'(全体)→'object.n.01'(物体)→'physical_entity.n.01'(物理实体)→'entity.n.01'(实体)。

In [193]:
# 查看上位同义词集路径
wordnet.synset('tiger.n.02').hypernym_paths()
Out[193]:
[[Synset('entity.n.01'),
  Synset('physical_entity.n.01'),
  Synset('object.n.01'),
  Synset('whole.n.02'),
  Synset('living_thing.n.01'),
  Synset('organism.n.01'),
  Synset('animal.n.01'),
  Synset('chordate.n.01'),
  Synset('vertebrate.n.01'),
  Synset('mammal.n.01'),
  Synset('placental.n.01'),
  Synset('carnivore.n.01'),
  Synset('feline.n.01'),
  Synset('big_cat.n.01'),
  Synset('tiger.n.02')]]

2.2 词汇间关系

WordNet中另一个重要定义方式是从条目到它们的部分或到包含它们的东西,例如下面的例子,首先查看'human'(人)的同义词集,选择同义词集'homo.n.02',调用part_meronyms()方法可以查看这个同义词集中包含的部分,由返回的结果可以看到人由手臂、头发、面部、脚、头等部分组成。

In [235]:
wordnet.synsets('human')
Out[235]:
[Synset('homo.n.02'),
 Synset('human.a.01'),
 Synset('human.a.02'),
 Synset('human.a.03')]
In [236]:
# 查看整体中的部分
wordnet.synset('homo.n.02').part_meronyms()
Out[236]:
[Synset('arm.n.01'),
 Synset('body_hair.n.01'),
 Synset('face.n.01'),
 Synset('foot.n.01'),
 Synset('hand.n.01'),
 Synset('human_body.n.01'),
 Synset('human_head.n.01'),
 Synset('loin.n.02'),
 Synset('mane.n.02')]

调用substance_meronyms()方法还可以查看同义词集的实质组成,例如同义词集'water.n.01'(水)的实质是'hydrogen.n.01'(氢气)和'oxygen.n.01'(氧气)。

In [237]:
# 查看整体的实质
wordnet.synset('water.n.01').substance_meronyms()
Out[237]:
[Synset('hydrogen.n.01'), Synset('oxygen.n.01')]

不光名词之间存在联系,动词之间也会存在一些关系,比如蕴含关系。下面的例子中,调用entailments()方法查看'breathe.v.01'(呼吸)蕴含着'exhale.v.01'(呼出)和'inhale.v.02'(吸入)两个动词同义词集。

In [239]:
# 动词间的蕴含关系
wordnet.synset('breathe.v.01').entailments()
Out[239]:
[Synset('exhale.v.01'), Synset('inhale.v.02')]

词条之间还存在着反义词关系,比如调用antonyms()方法查看词条'laugh.v.01.laugh'(笑)的反义词词条为'cry.v.02.cry'(哭)。

In [246]:
# 反义词
wordnet.lemma('laugh.v.01.laugh').antonyms()
Out[246]:
[Lemma('cry.v.02.cry')]

每个同义词集都有一个或多个上位同义词集路径连接到一个根上位同义词集,连接到同一个根的两个同义词集可能有一些共同的上位同义词集,而且这两个同义词集一定有密切的联系。例如我们定义了下面5个同义词集,分别为'right_whale.n.01'(露脊鲸)、'orca.n.01'(虎鲸)、'minke_whale.n.01'(小须鲸)、'tortoise.n.01'( 龟)和'novel.n.01'(小说)。

In [2]:
# 共同上位词
right = wordnet.synset('right_whale.n.01')
orca = wordnet.synset('orca.n.01')
minke = wordnet.synset('minke_whale.n.01')
tortoise = wordnet.synset('tortoise.n.01')
novel = wordnet.synset('novel.n.01')

调用lowest_common_hypernyms()方法可以查看两个同义词集的最低共同上位同义词集,可以看到露脊鲸和小须鲸都属于baleen_whale(长须鲸),露脊鲸和虎鲸都属于whale(鲸鱼),露脊鲸和龟都属于vertebrate(脊椎动物),露脊鲸和小说都属于entity(实体)。

In [249]:
print(right.lowest_common_hypernyms(minke))
print(right.lowest_common_hypernyms(orca))
print(right.lowest_common_hypernyms(tortoise))
print(right.lowest_common_hypernyms(novel))
[Synset('baleen_whale.n.01')]
[Synset('whale.n.02')]
[Synset('vertebrate.n.01')]
[Synset('entity.n.01')]

越靠近上位的同义词集更具有一般性,我们可以调用min_depth()方法来量化这个一般性,比如baleen_whale(长须鲸)的深度为14,whale(鲸鱼)的深度为13,vertebrate(脊椎动物)的深度为8,entity(实体)的深度为0。

In [250]:
# 查看同义词集的深度
print(wordnet.synset('baleen_whale.n.01').min_depth())
print(wordnet.synset('whale.n.02').min_depth())
print(wordnet.synset('vertebrate.n.01').min_depth())
print(wordnet.synset('entity.n.01').min_depth())
14
13
8
0

调用path_similarity()方法还可以基于上位同义词集层次结构中相互关联的最短路径下,计算一个范围在0~1之间的相似度(两者之间没有路径返回-1,自身比较返回1),从下面的结果可以看到,从小须鲸、虎鲸、脊椎动物到小说,即从海洋生物到非生物时,相似度是逐渐减少的。

In [5]:
# 计算语义相似度
print(right.path_similarity(minke))
print(right.path_similarity(orca))
print(right.path_similarity(tortoise))
print(right.path_similarity(novel))
0.25
0.16666666666666666
0.07692307692307693
0.043478260869565216

3.总结

在本案例中,我们首先学习了如何利用NLTK获取内置语料库并进行简单统计以及创建本地语料库,接着我们以WordNet为例,介绍了NLTK中语言知识库的使用方法。案例中列举的例子较少,只是用于演示,其实语料库和语言知识库中蕴藏着很多有意思的信息,大家可以查阅官方文档进行进一步的尝试。