关闭

使用Python+NLTK实现英文单词词频统计

标签: pythonnltk词频统计词形还原
881人阅读 评论(0) 收藏 举报
分类:

使用Python+NLTK实现英文单词词频统计

应用场景

本人近来想要提高英语水平,决定从直接看英文书籍开始做起,在选择英文书的时候,我需要了解这本书的词汇量以及词频,这样在遇到生词的时候,我可以有针对性的对那些出现频率高的单词着重记忆。

在真正开始做的时候,发现统计词频并不像想象中那样简单。需要考虑到词形变化,动词、名词、形容词、副词、代词等等,这些词都会在有若干种不同的形式,来表达不同的意思。例如move和moved,这显然应该归为一个词而不应该分为两个词来统计,于是,我们需要对各种形式的单词进行词形还原(lemmatization)。

Fork Me

本项目已在github上开源,大家可以随意fork、下载,欢迎大家指出程序中的错误和不足,共同学习!
https://github.com/Sailingboat1988/wordcounter

参考运行环境

  • Python 2.7.12
  • NLTK 3.2.2
  • Ubuntu 16.04 LTS

流程步骤图

操作流程如下图:

Created with Raphaël 2.1.0开始读取文件过滤特殊符号以及还原常见缩写单词分词词形还原统计词频结果写入文件结束

其中词形还原需要用到NLTK,同时也是本文着重描述的地方。

详细步骤

以下为详细步骤。每一步骤中所涉及到的代码仅作为举例说明,如需完整代码,请参见文章开始或结尾处的github地址。

读取文件

在Python中,可以使用with open (file) as f: 来进行文件操作,例如:

with open (file) as f:
    for line in f:
        do something...

过滤特殊符号以及还原常见缩写单词

因为我们要统计的是词频,因此像双引号、单引号、句点、逗号等等这些符号都要过滤掉,只留单词。这些符号被去掉之后采用空格符号来占位。

通过使用正则表达式,可以达到此目的:

pat_letter = re.compile(r'[^a-zA-Z \']+')
new_text = pat_letter.sub(' ', text).strip().lower()

这里并没有过滤掉单引号,因为单引号在英文中同时也是缩写符号,例如 He's, I'm, I'd 等等。

接下来要对这种缩写形式进行转换,情形比较多,但都可以使用正则表达式来一一处理:

import re
# to find the 's following the pronouns. re.I is refers to ignore case
pat_is = re.compile("(it|he|she|that|this|there|here)(\'s)", re.I)
# to find the 's following the letters
pat_s = re.compile("(?<=[a-zA-Z])\'s")
# to find the ' following the words ending by s
pat_s2 = re.compile("(?<=s)\'s?")
# to find the abbreviation of not
pat_not = re.compile("(?<=[a-zA-Z])n\'t")
# to find the abbreviation of would
pat_would = re.compile("(?<=[a-zA-Z])\'d")
# to find the abbreviation of will
pat_will = re.compile("(?<=[a-zA-Z])\'ll")
# to find the abbreviation of am
pat_am = re.compile("(?<=[I|i])\'m")
# to find the abbreviation of are
pat_are = re.compile("(?<=[a-zA-Z])\'re")
# to find the abbreviation of have
pat_ve = re.compile("(?<=[a-zA-Z])\'ve")

new_text = pat_is.sub(r"\1 is", new_text)
new_text = pat_s.sub("", new_text)
new_text = pat_s2.sub("", new_text)
new_text = pat_not.sub(" not", new_text)
new_text = pat_would.sub(" would", new_text)
new_text = pat_will.sub(" will", new_text)
new_text = pat_am.sub(" am", new_text)
new_text = pat_are.sub(" are", new_text)
new_text = pat_ve.sub(" have", new_text)
new_text = new_text.replace('\'', ' ')

分词

由于是英文,所以分词很容易,使用str.split()方法就可以实现分词,split()方法默认采用“空白字符”(WhiteSpace String)作为分隔符,参见python文档关于split()方法的描述:

split(…)
S.split([sep [,maxsplit]]) -> list of strings
Return a list of the words in the string S, using sep as the delimiter string. If maxsplit is given, at most maxsplit splits are done. If sep is not specified or is None, any whitespace string is a separator and empty strings are removed from the result.

词形还原

词形还原所涉及的具体情况还挺多的,例如,动词有一般式、过去式、过去分词、现在分词、单数第三人称一般式,名词有一般形式、可数名词复数形式,形容词和副词有一般式、比较级、最高级。通过自己的写规则的方式显然不太现实,那么有没有现成的工具可以完成这些词形的还原工作呢?答案是有的,这就是NLTK了。

NLTK

NLTK (Natural Language Toolkit) 是一个基于Python平台用于自然语言处理的工具集,包含许多很强大的功能,而我们现在要用的是其众多功能之一的词形还原(lemmatization)。
NLTK 的安装过程请参见官网上的步骤。
官网链接

单词的TAG

在nltk中,用来描述一个单词的词性(含词形、时态等概念)的就是tag,请看下面这个例子:

import nltk
from nltk.tokenize import word_tokenize
tag = nltk.pos_tag(word_tokenize("John's big idea isn't all that bad."))

word_tokenize返回的值是一个list类型,它的元素类型是str,如下:

['John', "'s", 'big', 'idea', 'is', "n't", 'all', 'that', 'bad', '.']

细心的朋友可能会看出来,这不就相当于分词吗?没错,这就是分词,但是它分完的结果并不能为我们所用,因为包含了像 's . 这一类符号,这不是我们统计词频想要的,所以我们要自己分词,以方便我们过滤掉这些特殊符号或者还原这些特定的缩写。
上面的list结果传入nltk.pos_tag方法,可以获得每一个单词的词性,如下:

[('John', 'NNP'), ("'s", 'POS'), ('big', 'JJ'), ('idea', 'NN'), ('is', 'VBZ'), ("n't", 'RB'), ('all', 'PDT'), ('that', 'DT'), ('bad', 'JJ'), ('.', '.')]

这是一个list,它的元素类型是tuple,('John', 'NNP')表示”John”的词性是NNP(Proper noun, singular),即专有名词单数。这个NNP就称为一个TAG,在下面做词形还原的时候会用到。
查看更多tag解释请参见 Alphabetical list of part-of-speech tags used in the Penn Treebank Project

词形还原

词形还原需要用到nltk.stem.wordnet.WordNetLemmatizer,请参考如下代码:

from nltk.stem.wordnet import WordNetLemmatizer
lmtzr = WordNetLemmatizer()
new_word = lmtzr.lemmatize('taken', 'v')

new_word 的值是u'take',该方法是将动词过去分词taken还原成一般式,第二个参数’v’是一个描述字符,表示单词是一个动词,这种用于词形还原的“描述词性的字符”称之为pos(与上面的tag有关系但不是一回事),其他pos可以参见help(nltk.corpus.wordnet),如下:

| Data and other attributes defined here:
| ADJ = u’a’
| ADJ_SAT = u’s’
| ADV = u’r’
| MORPHOLOGICAL_SUBSTITUTIONS = {u’a’: [(u’er’, u”), (u’est’, u”), (u’…
| NOUN = u’n’
| VERB = u’v’

这个ADJ_SAT是个什么东西,我也没闹明白,知道的同学还请告知一下。
这些pos和上面的tag有相似的地方,我也不明白为什么在nltk中要保持这两套描述体系,在它们之间我们可以发现对应关系,进而可以写出一个方法,通过tag来获得pos,如下:

def get_wordnet_pos(treebank_tag):
    if treebank_tag.startswith('J'):
        return nltk.corpus.wordnet.ADJ
    elif treebank_tag.startswith('V'):
        return nltk.corpus.wordnet.VERB
    elif treebank_tag.startswith('N'):
        return nltk.corpus.wordnet.NOUN
    elif treebank_tag.startswith('R'):
        return nltk.corpus.wordnet.ADV
    else:
        return ''

必须说明一点,这个方法并不是我的原创,而是来自stackoverflow上一位大神的回答,参见wordnet-lemmatization-and-pos-tagging-in-python

接下来我们就可以利用lmtzr.lemmatize(word, pos)以及刚刚获得的pos来对单词进行还原了

统计词频

统计词频主要是依靠collections.Counter(word_list).most_common()方法来自动统计并排序,如下:

import collections
collections.Counter("How tragic that most people had to get ill before they understood what a gift it was to be alive".split())

result:

[('to', 2), ('a', 1), ('be', 1), ('what', 1), ('gift', 1), ('most', 1), ('ill', 1), ('people', 1), ('had', 1), ('it', 1), ('alive', 1), ('How', 1), ('that', 1), ('tragic', 1), ('they', 1), ('understood', 1), ('get', 1), ('was', 1), ('before', 1)]

结果写入文件

这一步骤很简单,就不做过多说明了,请看代码:

def write_to_file(words, file='results.txt'):
    f = open(file, 'w')
    for item in words:
        for field in item:
            f.write(str(field)+',')
        f.write('\n')

完整代码已放到Github上开源,请移步 https://github.com/Sailingboat1988/wordcounter

转载请注明出处!

1
1

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:1033次
    • 积分:29
    • 等级:
    • 排名:千里之外
    • 原创:2篇
    • 转载:0篇
    • 译文:0篇
    • 评论:0条
    文章分类
    文章存档