字符串操作
切分
将文本(段落)切分成语句
text = " Welcome readers. I hope you find it interesting. Please do reply. "
print(nltk.tokenize.sent_tokenize(text))
[' Welcome readers.', 'I hope you find it interesting.', 'Please do reply.']
这样,一段给定的文本就被分割成了独立的句子。我们还可以进一步对这些独立的句子进行处理。
要切分大批量的句子,我们可以加载PunktSentenceTokenizer 并使用其tokenize()函数来进行切分。下面的代码展示了该过程:
tokenizer = nltk.data.load('tokenizers/punkt/english.pickle')
text = " Hello everyone. Hope all are fine and doing well. Hope you find the book interesting. "
print(tokenizer.tokenize(text))
[' Hello everyone.', 'Hope all are fine and doing well.', 'Hope you find the book interesting.']
当然,也可以加载法语的pickle,以实现对法语段落的切分。
french_tokenizer=nltk.data.load('tokenizers/punkt/french.pickle')
将句子切分为单词 - - nltk.word_tokenize('sentence')
r = input("Please write a text")
# 输入 Please write a textToday is a pleasant day
print(len(nltk.word_tokenize(r))) # 返回 8
分词器的继承树如图
import nltk
from nltk.tokenize import RegexpTokenizer
token = RegexpTokenizer("\w+")
token.tokenize("Don't hesitate to ask questions.")
# ['Don', 't', 'hesitate', 'to', 'ask', 'questions']
from nltk.tokenize import regexp_tokenize
sent="Don't hesitate to ask questions"
print(regexp_tokenize(sent, pattern='\w+|\$[\d\.]+|\S+'))
# ['Don', "'t", 'hesitate', 'to', 'ask', 'questions']
标准化
-- 为了实现对自然语言文本的处理,我们需要对其执行标准化,主要涉及消除标点符号、将整个文本转换为大写或小写、数字转换成单词、扩展缩略词、文本的规范化等操作。
1)消除标点符号
text=[" It is a pleasant evening.","Guests, who came from US arrived at the venue","Food was tasty."]
from nltk.tokenize import word_tokenize
tokenized_docs=[word_tokenize(doc) for doc in text]
print(tokenized_docs)
# [['It', 'is', 'a', 'pleasant', 'evening', '.'],
# ['Guests', ',', 'who', 'came', 'from', 'US', 'arrived', 'at', 'the', 'venue'], ['Food', 'was', 'tasty', '.']]
以上代码得到了切分后的文本。以下代码将从切分后的文本中删除标点符号:
import re
import string
text=[" It is a pleasant evening.","Guests, who came from US arrived at the venue","Food was tasty."]
from nltk.tokenize import word_tokenize
tokenized_docs=[word_tokenize(doc) for doc in text]
# string.punctuation 表示所有标点字符
# re.escape( ) -- 在模式中转义所有非字母数字字符
# re.compile( ) -- 返回一个re模式,集体例子见下
x=re.compile('[%s]' % re.escape(string.punctuation))
tokenized_docs_no_punctuation = []
for review in tokenized_docs:
new_review = []
for token in review:
# re.sub用于替换字符串中的匹配项
new_token = x.sub(u'', token) # 把所有的标点符号替换成了空格
if not new_token == u'': # 如果new_token不是空格,则加入new_review中,达到了去标点的效果
new_review.append(new_token)
tokenized_docs_no_punctuation.append(new_review)
print(tokenized_docs_no_punctuation)
[['It', 'is', 'a', 'pleasant', 'evening'],
['Guests', 'who', 'came', 'from', 'US', 'arrived', 'at', 'the', 'venue'],
['Food', 'was', 'tasty']]
re.escape(pattern) 可以对字符串中所有可能被解释为正则运算符的字符进行转义的应用函数
re.escape('www.python.org')
# 'www\\.python\\.org'
re.findall(re.escape('w.py'),"jw.pyji w.py.f")
# ['w.py', 'w.py']
re.compile() -- 该函数根据包含的正则表达式的字符串创建模式对象。可以实现更有效率的匹配。在直接使用字符串表示的正则表达式进行search,match和findall操作时,python会将字符串转换为正则表达式对象。而使用compile完成一次转换之后,在每次使用模式的时候就不用重复转换。当然,使用re.compile()函数进行转换后,re.search(pattern, string)的调用方式就转换为 pattern.search(string)的调用方式。
>>> import re
>>> some_text = 'a,b,,,,c d'
>>> reObj = re.compile('[, ]+')
>>> reObj.split(some_text)
['a', 'b', 'c', 'd']
# 不使用re.compile
>>> import re
>>> some_text = 'a,b,,,,c d'
>>> re.split('[, ]+',some_text)
['a', 'b', 'c', 'd']
re.sub() -- 用于替换字符串中的匹配项。下面一个例子将字符串中的空格 ' ' 替换成 '-' :
import re
text = "JGood is a handsome boy, he is cool, clever, and so on..."
re.sub(r'\s+', '-', text)
# 'JGood-is-a-handsome-boy,-he-is-cool,-clever,-and-so-on...'
2)文本大小写转换
通过str.lower()和str.upper()函数
3) 处理停用词
停止词是指在执行信息检索任务或其他自然语言任务时需要被过滤掉的词,因为这些词对理解句子的整体意思没有多大的意义。许多搜索引擎通过去除停止词来工作,以便缩小搜索范围。消除停止词在NLP 中被认为是至关重要的标准化任务之一 。
NLTK 库为多种语言提供了一系列的停止词,为了可以从nltk_data/corpora/stopwords 中访问停止词列表,我们需要解压datafile 文件:
import nltk
from nltk.corpus import stopwords
stops=set(stopwords.words('english')) # 得到一个长度为179个词的 set
words=["Don't", 'hesitate','to','ask','questions']
[word for word in words if word not in stops]
# ["Don't", 'hesitate', 'ask', 'questions']
stopwords.fileids()
# 返回nltk支持的停用词的语言
# ['arabic', 'azerbaijani', 'danish', 'dutch', 'english', 'finnish', 'french', 'german',
# 'greek', 'hungarian', 'indonesian', 'italian', 'kazakh', 'nepali', 'norwegian',
# 'portuguese', 'romanian', 'russian', 'spanish', 'swedish', 'turkish']
替换和校正标识符
1)使用正则表达式替换单词
import re
replacement_patterns = [(r'won\'t', 'will not'), (r'can\'t', 'cannot'),
(r'i\'m', 'i am'), (r'ain\'t', 'is not'), (r'(\w+)\'ll', '\g<1> will'),
(r'(\w+)n\'t', '\g<1> not'), (r'(\w+)\'ve', '\g<1> have'), (r'(\w+)\'s', '\g<1> is'),
(r'(\w+)\'re', '\g<1> are'), (r'(\w+)\'d', '\g<1> would')]
# 通过使用文本替换,用缩略词的扩展形式来替换缩略词。例如,doesn’t 可以被替换为does not
class RegexpReplacer(object):
def __init__(self, patterns=replacement_patterns):
self.patterns = [(re.compile(regex), repl) for (regex, repl) in patterns]
def replace(self, text):
s = text
for (pattern, repl) in self.patterns:
(s, count) = re.subn(pattern, repl, s) # 类似于re.sub(),返回多了个替换次数
print(s, count) # 如下,一共输出10次,对应replacement_patterns元素个数
mid = RegexpReplacer()
sent="Don't hesitate to ask questions"
mid.replace(sent)
# Don't hesitate to ask questions 0
# Don't hesitate to ask questions 0
# Don't hesitate to ask questions 0
# Don't hesitate to ask questions 0
# Don't hesitate to ask questions 0
# Do not hesitate to ask questions 1
# Do not hesitate to ask questions 0
# Do not hesitate to ask questions 0
# Do not hesitate to ask questions 0
# Do not hesitate to ask questions 0
以上定义了替换模式,模式第一项表示需要被匹配的模式,第二项是其对应的替换模式。RegexpReplacer 类被定义用来执行编译模式对的任务,并且它提供了一个叫作replace()的方法,该方法的功能是用另一种模式来执行模式的替换。
2)在执行切分前先进行替换操作
import re
replacement_patterns = [(r'won\'t', 'will not'), (r'can\'t', 'cannot'),
(r'i\'m', 'i am'), (r'ain\'t', 'is not'), (r'(\w+)\'ll', '\g<1> will'),
(r'(\w+)n\'t', '\g<1> not'), (r'(\w+)\'ve', '\g<1> have'), (r'(\w+)\'s', '\g<1> is'),
(r'(\w+)\'re', '\g<1> are'), (r'(\w+)\'d', '\g<1> would')]
# 通过使用文本替换,用缩略词的扩展形式来替换缩略词。例如,doesn’t 可以被替换为does not
class RegexpReplacer(object):
def __init__(self, patterns=replacement_patterns):
self.patterns = [(re.compile(regex), repl) for (regex, repl) in patterns]
def replace(self, text):
s = text
for (pattern, repl) in self.patterns:
(s, count) = re.subn(pattern, repl, s) # 类似于re.sub(),返回多了个替换次数
return s
import nltk
from nltk.tokenize import word_tokenize
replacer = RegexpReplacer()
word_tokenize(replacer.replace("Don't hesitate to ask questions"))
# ['Do', "not", 'hesitate', 'to', 'ask', 'questions']
3)处理重复字符
先简单看一下re.sub()方法的参数(组)
上述解释(\w*)(\w)(\w*) 表示匹配组里的3个元素 分别对应 \1 \2 \3,
class RepeatReplacer(object):
def __init__(self):
self.repeat_regexp = re.compile(r'(\w*)(\w)\2(\w*)')
self.repl = r'\1\2\3'
def replace(self, word):
repl_word = self.repeat_regexp.sub(self.repl, word)
if repl_word != word:
return self.replace(repl_word) # 递归
else:
return repl_word
replacer=RepeatReplacer()
replacer.replace('lottt')
# lot
使用RepeatReplacer 的问题是它会将happy 转换为hapy,这样是不妥的。为了避免这个问题,我们可以嵌入wordnet 与其一起使用。
import re
from nltk.corpus import wordnet
class RepeatReplacer(object):
def __init__(self):
self.repeat_regexp = re.compile(r'(\w*)(\w)\2(\w*)')
self.repl = r'\1\2\3'
def replace(self, word):
if wordnet.synsets(word):
return word
repl_word = self.repeat_regexp.sub(self.repl, word)
if repl_word != word:
return self.replace(repl_word) # 递归
else:
return repl_word
# wordnet.synsets() - - 获得一个词的所有sense,包括词语的各种变形的sense:
print(wordnet.synsets('published'))
# [Synset('print.v.01'),
# Synset('publish.v.02'),
# Synset('publish.v.03'),
# Synset('published.a.01'),
# Synset('promulgated.s.01')]
replacer=RepeatReplacer()
replacer.replace('happy')
# 'happy'
4)用单词的同义词替换
需要添加同义词字典
class WordReplacer(object):
def __init__(self, word_map):
self.word_map = word_map
def replace(self, word):
return self.word_map.get(word, word)
replacer=WordReplacer({'congrats':'congratulations'})
print(replacer.replace('congrats'))
# 'congratulations'
replacer.replace('maths')
# 'maths'
字典的get()方法,如果不存在则返回单词本身。
使用编辑距离算法执行相似性度量
两个字符串之间的编辑距离或Levenshtein 编辑距离算法用于计算为了使两个字符串相等所插入、替换或删除的字符数量。
让我们看一看使用NLTK 中的nltk.metrics 包来计算编辑距离的代码:
import nltk
from nltk.metrics import *
print(edit_distance("relate","relation")) # 3
print(edit_distance("suggestion","calculation")) # 7
这里,当我们计算relate 和relation 之间的编辑距离时,需要执行三个操作(一个替换操作和两个插入操作)。当计算suggestion 和calculation 之间的编辑距离时,需要执行七个操作(六个替换操作和一个插入操作)。
使用Jaccard 系数执行相似性度量
有关Jaccard 相似度的代码如下:
def jacc_similarity(query, document):
first=set(query).intersection(set(document))
second=set(query).union(set(document))
return len(first)/len(second)
NLTK 中Jaccard 相似性系数的实现:
import nltk
from nltk.metrics import *
X=set([10,20,30,40])
Y=set([20,30,60])
print(jaccard_distance(X,Y)) # 0.6
2.6 词形还原 lemmatization
词形还原操作会利用上下文语境和词性来确定相关单词的变化形式,并运用不同的标准化规则,根据词性来获取相关的词根(也叫lemma)。
# 词形还原 lemmatization
print(nltk.stem.WordNetLemmatizer().lemmatize("ate")) # eat
WordNetLemmatizer 使用了wordnet,它会针对某个单词去搜索wordnet 这个语义字典。另外,它还用到了变形分析,以便直切词根并搜索到特殊的词形(即这个单词的相关变化)。因此在我们的例子中,通过ate 这个变量是有可能会得到eat 这个单词的,而这是词干提取操作无法做到的事情。
2.7 停用词移除 Stop word removal
思路就是想要简单地移除语料库中的在所有文档中都会出现的单词。通常情况下,冠词和代词都会被列为停用词。这些单词在一些NPL 任务(如说关于信息的检索和分类的任务)中是毫无意义的,这意味着这些单词通常不会产生很大的歧义。恰恰相反的是,在某些NPL 应用中,停用词被移除之后所产生的影响实际上是非常小的。在大多数时候,给定语言的停用词列表都是一份通过人工制定的、跨语料库的、针对最常见单词的停用词列表。虽然大多数语言的停用词列表都可以在相关网站上被找到,但也有一些停用词
列表是基于给定语料库来自动生成的。有一种非常简单的方式就是基于相关单词在文档中出现的频率(即该单词在文档中出现的次数)来构建一个停用词列表,出现在这些语料库中的单词都会被当作停用词。经过这样的充分研究,我们就会得到针对某些特定语料库的最佳停用词列表。NLTK 库中就内置了涵盖22 种语言的停用词列表。
# 停用词移除 Stop word removal
stop_list = nltk.corpus.stopwords.words('english')
text = 'This is just a text'
clean_list_word = [_ for _ in text.split() if _ not in stop_list]
print(clean_list_word) # 返回 ['This', 'text']
也可以使用 停用词文件,基于查表法来去除 停用词
其实NLTK 内部所采用的仍然是一个非常类似的方法(类似的停用词文件),查表法实现停用词的去除。这里建议使用NLTK 的停用词列表,因为这是一个更为标准化的列表,相比其他所有的实现都更为健全。而且,我们可以通过向该库的停用词构造器传递一个语言名称参数,来实现针对其他语言的类似方法。
2.8 罕见词移除
这是一个非常直观的操作,因为该操作针对的单词都有很强的唯一性,如说名称、品牌、产品名称、某些噪音性字符(例如html 代码的左缩进)等。这些词汇也都需要根据不同的NLP 任务来进行清除。
总而言之,我们绝对不希望看到所有噪音性质的分词出现。为此,我们通常会为单词设置一个标准长度,那些太短或太长的单词将会被移除。
2.9 拼写纠错
虽然并不是所有的NLP 应用都会用到拼写检查器(spellchecker),但的确有些用例是需要执行基本的拼写检查的。我们可以通过纯字典查找的方式来创建一个非常基本的拼写检查器。业界也有专门为此类应用开发的一些增强型的字符串算法,用于一些模糊的字符串匹配。其中最常用的是edit-distance 算法。NLTK 也为我们提供了多种内置了edit-distance算法的度量模块。
拼写检查需要使用编辑距离。比如across被拼成了acress,和它相近的有actress,caress等,一般编辑距离都要小于2。我们可以用pyenchant这个库来给出修改建议,并根据编辑距离进行筛选。
下面两个词的距离是3。
from nltk.metrics import edit_distance
print(edit_distance('rain', 'shine'))
# 返回 3
Q:在完成停用词移除之后,我们还可以执行其他NLP 操作吗?
A:不能执行其他NLP操作了,这是不可能的。所有典型的NLP 应用,如词性标注、断句处理等,都需要根据上下文语境来为既定文本生成相关的标签。一旦我们移除了停用词,其上下文环境也就不存在了。
Chapter 3 词性标注 Pos Tagging
• 何谓词性标注,以及其在NLP 中的重要性。
• 如何使用NLTK 中形形色色的词性标注。
• 如何用NLTK 创建自定义的词性标注。
3.1 何为词性标注
首先,我们要学习一些现成可用的POS 标注器,及其相配的token 集。在此,会看到一些以元组形式存在的、独立单词的POS。然后,再将焦点转移到其中一些标注其的内部工作原理上。最后,还将讨论如何从头开始创建一个自定义的标注器。
在讨论POS 时,总少不了会用到Penn Treebank这个最常用到的POS 标记库。Penn Treebank 原本是一个NLP 项目的名称,该项目主要用于对相关语料进行标注,标注内容包括词性标注以及句法分析。其语料来源是1989 年的华尔街日报,包含了2499 篇文章。这里指代该项目所标注的结果。
s = 'I was watching TV.'
s_list = nltk.pos_tag(nltk.word_tokenize(s))
print(s_list)
# [('I', 'PRP'), ('was', 'VBD'), ('watching', 'VBG'), ('TV', 'NN'), ('.', '.')]
上面返回的是一组(词形,词性标签)形式的元组,这就是一个NLTK 库内置的POS 标注器。
# Brown 语料库中各POS 标签的分布频率
tags = [tag for (word, tag) in brown.tagged_words(categories='news')]
freq = nltk.FreqDist(tags)
freq_dict = {}
for ele in freq:
freq_dict[ele] = freq[ele]
print(freq_dict)
{'AT': 8893, 'NP-TL': 741, 'NN-TL': 2486, 'JJ-TL': 689, 'VBD': 2524, 'NR': 495, 'NN': 13162, 'IN': 10616, 'NP$': 279, 'JJ': 4392,...
FreqDist::plot(n):该方法接受一个数字n,会绘制出现次数最多的前n项,在本例中即绘制高频词汇。
freq.plot(20)
FreqDist::tabulate(n):该方法接受一个数字n作为参数,会以表格的方式打印出现次数最多的前n项。
freq.tabulate(20)
FreqDist::most_common(n):该方法接受一个数字n作为参数,返回出现次数最多的前n项列表。
print(freq.most_common(15))
[('NN', 13162), ('IN', 10616), ('AT', 8893), ('NP', 6866), (',', 5133), ('NNS', 5066), ('.', 4452), ('JJ', 4392), ('CC', 2664), ('VBD', 2524), ('NN-TL', 2486), ('VB', 2440), ('VBN', 2269), ('RB', 2166), ('CD', 2020)]
总结
FreqDist类
- 继承自dict
- 构造函数接受一个列表
- 键为传入构造函数的列表中的不同项,值为项出现的次数
FreqDist成员方法:
- plot(n),绘制出现次数最多的前n项
- tabulate(n),该方法接受一个数字n作为参数,会以表格的方式打印出现次数最多的前n项
- most_common(n),该方法接受一个数字n作为参数,返回出现次数最多的前n项列表
- hapaxes(),返回一个低频项列表
- max(),该方法会返回出现次数最多的项。