2.文本规范化

文本规范化

文本规范化定义为这样的一个过程,它包含一系列步骤,依次是转换、清洗以及将文本数据标准化成可供 NLP、分析系统和应用程序使用的格式。通常,文本切分本身也是文本规范化的一部分。除了文本切分以外,还有各种其他技术,包括文本清洗、大小写转换、词语矫正、停用词删除、词干提取和词型还原。文本规范化也常常称为文本清洗或转换

在开始之前,请使用以下代码段来加载基本的依存关系以及将使用的语料库:

import  nltk
import  re
import  string
from  pprint  import  pprint
 
corpus  =  [ "The brown fox wasn't that quick and he couldn't win the race" ,
           "Hey that's a great deal! I just bought a phone for $199" ,
           "@@You'll (learn) a **lot** in the book. Python is an amazing language!@@" ]

文本清洗

通常,要使用或分析的文本数据都包含大量无关和不必要的标识和字符,在进行其他操作 (如切分和其他规范操作) 之前,应该先删除它们。这包括从如 HTML 之类的数据源中提取有意义的文本,数据源中可能包含不必要的 HTML 标记,甚至是来自 XML 和 JSON feed 的数据。解析并清洗这些数据的方法很多,以删除不必要的标签。可以使用 nltk 的 clean_html 函数,甚至是 BeautifulSoup 库来解析 HTML 数据。还可以使用自己的逻辑,包括正则表达式、xpath 和 hxml 库来解析 XML 数据。从 JSON 获取数据较为容易,因为它具有明确的键值注释。

文本切分

通常,在删除数据多余字符和符号操作的前后,进行文本切分操作。文本切分和删除多余字符的顺序取决于你要解决的问题和你正在处理的数据。

以下代码段定义了文本切分函数:

def  tokenize_text(text):
     sentences  =  nltk.sent_tokenize(text)
     word_tokens  =  [nltk.word_tokenize(sentence)  for  sentence  in  sentences]
     return  word_tokens

这个函数的功能是接受文本数据,再从中提取句子,最后将每个句子划分成标识,这些标识可以是单词,特殊字符或标点符号。以下代码段说明了该函数的功能:

In [ 64 ]: token_list  =  [tokenize_text(text)
for  text  in  corpus]
 
In [ 65 ]: pprint(token_list)
[[[ 'The' ,
    'brown' ,
    'fox' ,
    'was' ,
    "n't" ,
    'that' ,
    'quick' ,
    'and' ,
    'he' ,
    'could' ,
    "n't" ,
    'win' ,
    'the' ,
    'race' ]],
  [[ 'Hey' 'that' "'s" , 'a ', ' great ', ' deal ', ' !'],
   [ 'I' 'just' 'bought' 'a' 'phone' 'for' '$' '199' ]],
  [[ '@' ,
    '@' ,
    'You' ,
    "'ll" ,
    '(' ,
    'learn' ,
    ')' ,
    'a' ,
    '**lot**' ,
    'in' ,
    'the' ,
    'book' ,
    '.' ],
   [ 'Python' 'is' 'an' 'amazing' 'language' '!' ],
   [ '@' '@' ]]]

现在,可以看出两个语料库中的文本是如何被切分的,也可以在更多的文本数据中尝试一下切分函数,看看能不能改进它!

删除特殊字符

文本规范化的一个重要任务是删除多余的字符,诸如特殊字符或标点符号。这个步骤通常在切分操作前后进行。这样做的主要原因是,当分析文本并提取基于 NLP 和机器学习的特征或信息时,标点符号或特殊字符往往没有多大的意义。将在切分前后删除这两类特殊的字符。

以下代码段显示了如何在切分之后删除特殊字符:

def  remove_characters_after_tokenization(tokens):
     pattern  =  re. compile ( '[{}]' . format (re.escape(string.punctuation)))
     filtered_tokens  =  filter ( None , [pattern.sub('', token)  for  token  in  tokens])
     return  filtered_tokens
In [ 72 ]: filtered_list_1  =  [
     filter (
         None ,[
                 remove_characters_after_tokenization(tokens)
                 for  tokens  in  sentence_tokens
         ]
     )
     for  sentence_tokens  in  token_list
]
 
In [ 73 ]:  print (filtered_list_1)
[< filter  object  at  0x7f0605b73fd0 >, < filter  object  at  0x7f0605b191d0 >, < filter  object  at  0x7f0605b194a8 >]

本质上,在这里使用的是 string.punctuation 属性,它由所有可能的特殊字符 / 符号组成,并从中创建一个正则表达式模式。使用它来匹配并删除符号和字符标识。使用正则表达式 sub 算法删除特殊字符之后,可以使用 filter 函数删除空标识。

def  remove_characters_before_tokenization(sentence, keep_apostrophes = False ):
     sentence  =  sentence.strip()
     if  keep_apostrophes:
         PATTERN  =  r '[?|$|&|*|%|@|(|)|~]'
         filtered_sentence  =  re.sub(PATTERN, r'', sentence)
     else :
         PATTERN  =  r '[^a-zA-Z0-9 ]'
         filtered_sentence  =  re.sub(PATTERN, r'', sentence)
     return  filtered_sentence
In [ 94 ]: filtered_list_2  =  [remove_characters_before_tokenization(sentence)
    ....:                      for  sentence  in  corpus]
 
In [ 95 ]:  print (filtered_list_2)
[ 'The brown fox wasnt that quick and he couldnt win the race' 'Hey thats a great deal I just bought a phone for 199' 'Youll learn a lot in the book Python is an amazing language' ]
In [ 96 ]: cleaned_corpus  =  [remove_characters_before_tokenization(sentence, keep_apostrophes = True )
    ....:                    for  sentence  in  corpus]
 
In [ 97 ]:  print (cleaned_corpus)
[ "The brown fox wasn't that quick and he couldn't win the race" "Hey that's a great deal! I just bought a phone for 199" "You'll learn a lot in the book. Python is an amazing language!" ]

上面的输出显示了在切分前删除特殊字符的两种不同的方式,一种是删除所有特殊字符,另一种是保留撇号 ( ' ) 和句号,这两种方法均使用正则表达式。至此,应该已经意识到,正如前面所述,正则表达式是非常强大的工具。通常,在删除这些字符后,就可以对干净的文本使用切分或进行其他规范化操作了。有时候,想要保留句子中的撇号做作为跟踪文本的方式,并在需要的时候扩展它们。

扩展缩写词

缩写词(contraction)是词或音节的缩短形式。它们即在书面形式中存在,也在口语中存在。现在单词的缩短版本可以通过删除特定的字母和音节获得。在英语的缩写形式中,缩写词通常是从单词中删除一些元音来创建的。举例来说 “is not” 所写成 "isn't", "will not" 所写成 "won't",应该注意到了,缩写词撇号用来表示缩写,而一些元音和其他字母责备删除了。通常,在正式无邪时会避免使用缩写词,但在非正式情况下,它们被官方使用。

英语中存在各种形式的缩写词,这些形式的缩写词与助动词的类型相关,不同的助动词给出了常规缩写词、否定缩写词和其他特殊的口语缩写词(其中一些可能并不涉及助动词),

缩写词确实为 NLP 和文本分析制造了一个难题,首先因为在该单词中有一个特殊的撇好字符。此外,有两个甚至更多的单词由缩写词表示,当尝试执行词语切分或者词语标准化时,这就会发生一连串复杂的问题。因此,在处理文本时,需要一些确切的步骤来处理缩写词。理想情况下,可以对缩写词和对应的扩展词语进行适当的反映,然后使用映射关系扩展文本中的所有所写词。下面创建了一个缩写词机器扩展形式的词汇表:

contractions.py  折叠源码
# -*- coding: utf-8 -*-
"""
Created on Mon Aug 01 01:11:02 2016
@author: DIP
"""
 
CONTRACTION_MAP  =  {
"ain't" "is not" ,
"aren't" "are not" ,
"can't" "cannot" ,
"can't've" "cannot have" ,
"'cause" "because" ,
"could've" "could have" ,
"couldn't" "could not" ,
"couldn't've" "could not have" ,
"didn't" "did not" ,
"doesn't" "does not" ,
"don't" "do not" ,
"hadn't" "had not" ,
"hadn't've" "had not have" ,
"hasn't" "has not" ,
"haven't" "have not" ,
"he'd" "he would" ,
"he'd've" "he would have" ,
"he'll" "he will" ,
"he'll've" "he he will have" ,
"he's" "he is" ,
"how'd" "how did" ,
"how'd'y" "how do you" ,
"how'll" "how will" ,
"how's" "how is" ,
"I'd" "I would" ,
"I'd've" "I would have" ,
"I'll" "I will" ,
"I'll've" "I will have" ,
"I'm" "I am" ,
"I've" "I have" ,
"i'd" "i would" ,
"i'd've" "i would have" ,
"i'll" "i will" ,
"i'll've" "i will have" ,
"i'm" "i am" ,
"i've" "i have" ,
"isn't" "is not" ,
"it'd" "it would" ,
"it'd've" "it would have" ,
"it'll" "it will" ,
"it'll've" "it will have" ,
"it's" "it is" ,
"let's" "let us" ,
"ma'am" "madam" ,
"mayn't" "may not" ,
"might've" "might have" ,
"mightn't" "might not" ,
"mightn't've" "might not have" ,
"must've" "must have" ,
"mustn't" "must not" ,
"mustn't've" "must not have" ,
"needn't" "need not" ,
"needn't've" "need not have" ,
"o'clock" "of the clock" ,
"oughtn't" "ought not" ,
"oughtn't've" "ought not have" ,
"shan't" "shall not" ,
"sha'n't" "shall not" ,
"shan't've" "shall not have" ,
"she'd" "she would" ,
"she'd've" "she would have" ,
"she'll" "she will" ,
"she'll've" "she will have" ,
"she's" "she is" ,
"should've" "should have" ,
"shouldn't" "should not" ,
"shouldn't've" "should not have" ,
"so've" "so have" ,
"so's" "so as" ,
"that'd" "that would" ,
"that'd've" "that would have" ,
"that's" "that is" ,
"there'd" "there would" ,
"there'd've" "there would have" ,
"there's" "there is" ,
"they'd" "they would" ,
"they'd've" "they would have" ,
"they'll" "they will" ,
"they'll've" "they will have" ,
"they're" "they are" ,
"they've" "they have" ,
"to've" "to have" ,
"wasn't" "was not" ,
"we'd" "we would" ,
"we'd've" "we would have" ,
"we'll" "we will" ,
"we'll've" "we will have" ,
"we're" "we are" ,
"we've" "we have" ,
"weren't" "were not" ,
"what'll" "what will" ,
"what'll've" "what will have" ,
"what're" "what are" ,
"what's" "what is" ,
"what've" "what have" ,
"when's" "when is" ,
"when've" "when have" ,
"where'd" "where did" ,
"where's" "where is" ,
"where've" "where have" ,
"who'll" "who will" ,
"who'll've" "who will have" ,
"who's" "who is" ,
"who've" "who have" ,
"why's" "why is" ,
"why've" "why have" ,
"will've" "will have" ,
"won't" "will not" ,
"won't've" "will not have" ,
"would've" "would have" ,
"wouldn't" "would not" ,
"wouldn't've" "would not have" ,
"y'all" "you all" ,
"y'all'd" "you all would" ,
"y'all'd've" "you all would have" ,
"y'all're" "you all are" ,
"y'all've" "you all have" ,
"you'd" "you would" ,
"you'd've" "you would have" ,
"you'll" "you will" ,
"you'll've" "you will have" ,
"you're" "you are" ,
"you've" "you have"
}

部分缩写词汇表显示在以下代码段中:

CONTRACTION_MAP  =  {
    "isn't" "is not" ,
    "aren't" "are not" ,
    "con't" "cannot" ,
    "can't've" "cannot have" ,
    .
    .
    .
    "you'll've" "your will have" ,
    "you're" "you are" ,
    "you've" "you have"
}

请记住,一些缩写词可以对应多种形式。举例来说,缩写词 “you‘ll” 可以表示 "you wil" 或者 "you shall"。为了简化,每个缩写词选取了一个扩展形式。下一步,想要扩展缩写词,需要使用以下代码段: 

def  expand_contractions(sentence, contraction_mapping):
     contractions_pattern  =  re. compile ( '({})' . format ( '|' .join(contraction_mapping.keys())),
                                       flags = re.IGNORECASE|re.DOTALL)
     def  expand_match(contraction):
         match  =  contraction.group( 0 )
         first_char  =  match[ 0 ]
         expanded_contraction  =  contraction_mapping.get(match)\
                                 if  contraction_mapping.get(match)\
                                 else  contraction_mapping.get(match.lower())                      
         expanded_contraction  =  first_char + expanded_contraction[ 1 :]
         return  expanded_contraction
     expanded_sentence  =  contractions_pattern.sub(expand_match, sentence)
     return  expanded_sentence

上面代码段中的主函数 expand_contractions 使用了 expand_match 函数来查找与正则表达式模式相匹配的每个所写单词,这些正则表达式模式有 CONTRACTION_MAP 库中的所写单词构成。匹配所写后,用相应的扩展版本替换它,并保留费所写形式的单词。

应用该函数,如下所示:

In [ 147 ]: expanded_corpus  =  [expand_contractions(sentence, CONTRACTION_MAP)
    .....:                      for  sentence  in  cleaned_corpus]
 
In [ 148 ]:  print (expanded_corpus)
[ 'The brown fox was not that quick and he could not win the race' 'Hey that is a great deal! I just bought a phone for 199' 'You will learn a lot in the book. Python is an amazing language!' ]

从结果可以看出,正如所预期的那样,每个所写单词都被正确的扩展了。可以构建一个更好的缩写词扩展器!

大小写转换

通常,会希望修改单词或句子的大小写,以使诸如特殊单词或标识匹配的工作更加容易。通常有两种类型的大小写转换操作。即小写转换和大写转换,通过这两种操作可以将文本正文完全转换为小写或带哦。当然也有其他大小写形式,如句式大小写(sejtejce case) 或单词首字母大小写 (proper case)。小写字体是一种格式,其中所有文本的字母都是小写字母,大写字体格式则全部都是大写字母。

以下代码段说明了以上概念:

In [ 150 ]:  print (corpus[ 0 ].lower())
the brown fox wasn 't that quick and he couldn' t win the race
 
In [ 151 ]:  print (corpus[ 0 ].upper())
THE BROWN FOX WASN 'T THAT QUICK AND HE COULDN' T WIN THE RACE

删除停用词

停用词(stopword,有时也拼写成 stop word)是指没有或只有极小意义的词语。通常在处理过程中将它们从文本中删除,已保留具有较大意义及语境的词语。如果你基于单个表示聚合语料库,然后查询词语评论,就会发现停用词的出现频率是最高的。类似 "a" "the" "me" 和 “and so on” 这样的单词就是停用词。目前还没有普遍或已穷尽的停用词列表。每个领域或语言可能都有一些列独有的提用此。以下代码展示了一种过滤和删除英语停用词的方法:

def  remove_stopwords(tokens):
     stopword_list  =  nltk.corpus.stopwords.words( 'english' )
     filtered_tokens  =  [token  for  token  in  tokens  if  token  not  in  stopword_list]
     return  filtered_tokens

在前面的函数中,使用了 nltk,他有一个英文的停用词列表。使用它来过滤掉所有与停用词相对应的标识。使用 tokenize_text 函数来分隔在前面获取到的 expanded_corpus,然后使用前面的函数删除停用词:

In [ 153 ]: expanded_corpus_tokens  =  [tokenize_text(text)
    .....:                            for  text  in  expanded_corpus]
 
In [ 154 ]: filtered_list_3  =   [[remove_stopwords(tokens)
    .....:                          for  tokens  in  sentence_tokens]
    .....:                          for  sentence_tokens  in  expanded_corpus_tokens]
 
In [ 155 ]:  print (filtered_list_3)
[[[ 'The' 'brown' 'fox' 'quick' 'could' 'win' 'race' ]], [[ 'Hey' 'great' 'deal' '!' ], [ 'I' 'bought' 'phone' '199' ]], [[ 'You' 'learn' 'lot' 'book' '.' ], [ 'Python' 'amazing' 'language' '!' ]]]

与之前的输出相比,本次输出的标识明显减少了,通过比较,可以发现被删除的标识都是停用词。想要查看 nltk 库中所有的英文停用词汇列表,请打印 nltk.corpus.stopwords.words('english'),其他语言包括:

arabic       danish  english  french  greek      indonesian  kazakh  norwegian   README    russian  swedish
azerbaijani  dutch   finnish  german  hungarian  italian     nepali  portuguese  romanian  spanish  turkish

请记住一个重要的事情,就是在上述情况下(在第一句话中), 删除了诸如 “no” 和 “not” 这样的否定词,通常,这类词语应当保留,以便于在注入情绪分析等应用中句子语意不会失真,因此在这些应用中需要确保此类词语不会被删除。

词语校正

文本规范化面临的主要挑战之一是文本中存在不正确的单词。这里不正确的定义包括拼写错误的单词以及某些字母过多重复的单词。举例来说,“finally” 一次可能会被错误的写成 “fianlly”,或者被想要表达强烈情绪的人写成 “finalllllyyyyyy”。目前主要的目标是将这些单词标准化为正确行使,使不至于失去文本中的重要信息。

校正重复字符

在这里,将介绍一种语法和语义组合使用的拼写校正方法。首先,从校正这些单词的语法开始,然后转向语义。

算法的第一步是,使用正则表达式来识别单词的重复字符,然后使用置换来逐个删除重复字符。考虑前面的例子中 “finalllyyy” 一词,可以使用模式 r'(\w*)(\w)\2(\w*)' 来识别单词中的两个不同字符之间的重复字符。通过利用正则表达式匹配组(组1、2 和 3)并使用模式 r'\1\2\3',能够使用替换方式消除一个重复字符,然后迭代此过程,知道消除所有重复字符。

以下代码单说明上述过程:

In [ 160 ]: old_word  =  'finalllyyy'
 
In [ 161 ]: repeat_pattern  =  re. compile (r '(\w*)(\w)\2(\w*)' )
 
In [ 162 ]: match_substitution  =  r '\1\2\3'
 
In [ 163 ]: step  =  1
 
 
In [ 164 ]:  while  True :
        new_word  =  repeat_pattern.sub(match_substitution, old_word)
        if  new_word ! =  old_word:
                print ( 'Step: {} Word: {}' . format (step, new_word))
                step  + =  1
                old_word  =  new_word
                continue
        else :
                print ( "Final word:" , new_word)
                break
    .....:
Step:  1  Word: finalllyy
Step:  2  Word: finallly
Step:  3  Word:  finally
Step:  4  Word: finaly
Final word: finaly

上面的代码段显示了重复字符如何逐步被删除,直到得到最终的单词 “finaly”,然而,在语义上,这个词不是正确的,正确的词是 "finally",即在步骤 3 中获得的单词。现在将使用 WordNet 语料库来检查每个步骤得到的单词,一旦获得有效单词就立刻终止循环。这就引入了算法所需的语义校正,如下代码段所示:

from  nltk.corpus  import  wordnet
old_word  =  'finalllyyy'
repeat_pattern  =  re. compile (r '(\w*)(\w)\2(\w*)' )
match_substitution  =  r '\1\2\3'
step  =  1
In [ 184 ]:  while  True :
        if  wordnet.synsets(old_word):
                print ( "Fonal correct word:" , old_word)
                break
        new_word  =  repeat_pattern.sub(match_substitution, old_word)
        if  new_word ! =  old_word:
                print ( 'Step: {} Word: {}' . format (step, new_word))
                step  + =  1
                old_word  =  new_word
                continue
        else :
                print ( "Final word:" , new_word)
                break
    .....:
Step:  1  Word: finalllyy
Step:  2  Word: finallly
Step:  3  Word:  finally
Fonal correct word:  finally

从上面的代码段中可以看出,在第 3 步骤后代码终止了,获得了正确的、符合语法的词义的单词。

可以通过将高逻辑编写到函数中来构建一个更好的代码段,以便使其在校正词语时变得更为通用,如下面的代码段所示:

def  remove_repeated_characters(tokens):
     repeat_pattern  =  re. compile (r '(\w*)(\w)\2(\w*)' )
     match_substitution  =  r '\1\2\3'
     def  replace(old_word):
         if  wordnet.synsets(old_word):
             return  old_word
         new_word  =  repeat_pattern.sub(match_substitution, old_word)
         return  replace(new_word)  if  new_word ! =  old_word  else  new_word
     correct_tokens  =  [replace(word)  for  word  in  tokens]
     return  correct_tokens

如前所述,该代码段使用内部函数 repliace() 来实现算法,然后在内部函数 remove_repeated_characters() 中对句子中的每个标识重复调用它。

可以在下面的代码段中看到上述代码的实际运算情况,以下代码段包含了一个实际的例句:

In [ 194 ]: sample_sentence  =  'My schooool is realllllyyy amaaazingggg'
 
In [ 195 ]: sample_sentence_tokens  =  tokenize_text(sample_sentence)[ 0 ]
 
In [ 196 ]: remove_repeated_characters(sample_sentence_tokens)
Out[ 196 ]: [ 'My' 'school' 'is' 'really' 'amazing' ]

从上面的输出可以看出,函数执行过程符合预期,它替换了每个标识中的重复字符,然后按照要求给出了正确的标识。

校正拼写错误

面临的另外一个问题是由人为错误导致的拼写错误,甚至是由于自动更正文本等能导致的机器拼写错误。有多种处理拼写错误的方法,其最终目标都是活的拼写正确的文本标识。下面介绍最为著名的算法之一,它由谷歌研究主管 Peter Norvig 开发。可以在 http://norvig.com/spell-correct.html 上找到完整详细的算法说明。

下面主要的目标是,给出一个单词,找到这个单词最有可能的正确形式。遵循的方法是生成一系列类似输入词的候选词,并从该集合中选择最有可能的单词作为正确的单词。使用标准英文单词语料库,根据语料库中单词的频率,从距离输入单词最近的最后一组候选词中识别出正确的单词。这个距离(即一个单词与输入单词的测量距离)也称为编辑距离(edit distance)。使用的输入语料库包含 Gutenberg 语料库数据、维基词典和英国国家语料库中的最常用单词列表。可以使用 https://mirror.shileizcc.com/wiki_Resources/python/text_analysis/big.txt 下载它:

$ wget https: //mirror .shileizcc.com /wiki_Resources/python/text_analysis/big .txt

可以使用以下代码段来生成英文中最常出现的单词及其计数:

def  tokens(text):
     """
     Get all words from the corpus
     """
     return  re.findall( '[a-z]+' , text.lower())
In [ 11 ]: WORDS  =  tokens( open ( 'big.txt' ).read())
 
In [ 12 ]: WORD_COUNTS  =  collections.Counter(WORDS)
 
In [ 13 ]:  print (WORD_COUNTS.most_common( 10 ))
[( 'the' 80030 ), ( 'of' 40025 ), ( 'and' 38313 ), ( 'to' 28766 ), ( 'in' 22050 ), ( 'a' 21155 ), ( 'that' 12512 ), ( 'he' 12401 ), ( 'was' 11410 ), ( 'it' 10681 )]

拥有了自己的词汇之后,就可以定义三个函数,计算出与输入单词的编辑距离为0、1 和 2 的单词组。这些编辑距离由插入、删除、添加和调换位置等操作产生。

以下代码段定义了实现该功能的函数:

def  edits0(word):
     """
     Return all strings that are zero edits away
     from the input word (i.e., the word itself).
     """
     return  {word}
 
def  edits1(word):
     """
     Return all strings that are one edit away
     from the input word.
     """
     alphabet  =  'abcdefghijklmnopqrstuvwxyz'
     def  splits(word):
         """
         Return a list of all possible (first, rest) pairs
         that the input word is made of.
         """
         return  [(word[:i], word[i:])
                 for  in  range ( len (word) + 1 )]      
     pairs       =  splits(word)
     deletes     =  [a + b[ 1 :]            for  (a, b)  in  pairs  if  b]
     transposes  =  [a + b[ 1 ] + b[ 0 ] + b[ 2 :]  for  (a, b)  in  pairs  if  len (b) >  1 ]
     replaces    =  [a + c + b[ 1 :]          for  (a, b)  in  pairs  for  in  alphabet  if  b]
     inserts     =  [a + c + b              for  (a, b)  in  pairs  for  in  alphabet]
     return  set (deletes  +  transposes  +  replaces  +  inserts)
 
def  edits2(word):
     """Return all strings that are two edits away
     from the input word.
     """
     return  {e2  for  e1  in  edits1(word)  for  e2  in  edits1(e1)}

还可以定义一个 known() 函数,该函数根据单词是否存在于词汇词典 WORD_COUNTS 中,从 edit 函数得出的时候选词组中返回一个单词子集。使我们可以从后算词组中获得一个有效单词列表:

def  known(words):
     """
     Return the subset of words that are actually
     in our WORD_COUNTS dictionary.
     """
     return  {w  for  in  words  if  in  WORD_COUNTS}

从下面的代码段中可以看出,这些函数完成了输入单词的拼写纠校正。基于与输入单词之间的编辑距离,它给出了最有可能的候选词:

In [ 23 ]: edits0(word)
Out[ 23 ]: { 'fianlly' }
 
In [ 24 ]: known(edits0(word))
Out[ 24 ]:  set ()
 
In [ 25 ]: edits1(word)
Out[ 25 ]:
{ 'afianlly' ,
  'aianlly' ,
  'bfianlly' ,
  'bianlly' ,
  'cfianlly' ,
  'cianlly' ,
...
 
 
In [ 26 ]: known(edits1(word))
Out[ 26 ]: { 'finally' }
 
In [ 27 ]: edits2(word)
Out[ 27 ]:
{ 'aaanlly' ,
  'aafianlly' ,
  'aaianlly' ,
  'aainlly' ,
  'aanlly' ,
  'abanlly' ,
...
 
 
In [ 28 ]: known(edits2(word))
Out[ 28 ]: { 'faintly' 'finally' 'finely' 'frankly' }

上面的输出显示了一组能够替换错误输入词的候选词。通过赋予编辑距离更小的单词更高的优先级,可以从前面的列表中选出候选词,如下列代码段所示:

In [ 30 ]: candidates  =  (known(edits0(word))  or
    ....: known(edits1(word))  or
    ....: known(edits2(word))  or
    ....: [word])
 
In [ 31 ]: candidates
Out[ 31 ]: { 'finally' }

假如在前面的候选词中两个单词的编辑距离相同,则可以通过使用 max (candidates, key=WORD_CONUTS.get) 函数从词汇字典 WORD_COUNTS 中选取出现频率最高的词来作为有效词。现在,使用上述逻辑定义拼写校正函数:

def  correct(word):
     """
     Get the best correct spelling for the input word
     """
     # Priority is for edit distance 0, then 1, then 2
     # else defaults to the input word itself.
     candidates  =  (known(edits0(word))  or
                   known(edits1(word))  or
                   known(edits2(word))  or
                   [word])
     return  max (candidates, key = WORD_COUNTS.get)

可以对拼写错误的单词使用上述函数来直接校正它们,如下面的代码段所示:

In [ 33 ]: correct( 'fianlly' )
Out[ 33 ]:  'finally'
 
In [ 34 ]: correct( 'FIANLLY' )
Out[ 34 ]:  'FIANLLY'

可以看出这个函数对大小写比较敏感,它无法校正非小写的单词,因此编写了下列函数,以使其能够同时校正大写和小写的单词。该函数的逻辑时存储单词的原始大小写格式,然后将所有字母转换为小写字母,更正拼写错误,最后使用 case_of 函数将其重新转换回初始的大小写格式:

def  correct_match(match):
       """
       Spell-correct word in match,
       and preserve proper upper/lower/title case.
       """
       word  =  match.group()
       def  case_of(text):
          """
          Return the case-function appropriate
          for text: upper, lower, title, or just str.:
          """
          return  ( str .upper  if  text.isupper()  else
                 str .lower  if  text.islower()  else
                 str .title  if  text.istitle()  else
                 str )
       return  case_of(word)(correct(word.lower()))
 
 
def  correct_text_generic(text):
     """
     Correct all the words within a text,
     returning the corrected text.
     """
     return  re.sub( '[a-zA-Z]+' , correct_match, text)

现在,上述函数既可以用来校正大写单词,也可以用来校正小写单词,如下面的代码段所示:

In [ 49 ]: correct_text_generic( 'fianlly' )
Out[ 49 ]:  'finally'
 
In [ 50 ]: correct_text_generic( 'FIANLLY' )
Out[ 50 ]:  'FINALLY'

当然,这种方法并不总是准确,如果单词没有出现在词汇字段中,就有可能无法被校正。使用更多的词汇表数据以涵盖更多的词语可以解决这个问题。在 pattern 库中也有类似的、开箱机用的算法,如下面的代码段所示:

安装 Pattern

$ pip  install  pattern

如果出现如下错误:

error  折叠源码
$ pip3  install  pattern
Collecting pattern
   Downloading https: //files .pythonhosted.org /packages/1e/07/b0e61b6c818ed4b6145fe01d1c341223aa6cfbc3928538ad1f2b890924a3/Pattern-3 .6.0. tar .gz (22.2MB)
     100% |████████████████████████████████| 22.3MB 1.5MB /s
Collecting future (from pattern)
   Downloading https: //files .pythonhosted.org /packages/00/2b/8d082ddfed935f3608cc61140df6dcbf0edea1bc3ab52fb6c29ae3e81e85/future-0 .16.0. tar .gz (824kB)
     100% |████████████████████████████████| 829kB 21.4MB /s
Collecting backports.csv (from pattern)
   Downloading https: //files .pythonhosted.org /packages/71/f7/5db9136de67021a6dce4eefbe50d46aa043e59ebb11c83d4ecfeb47b686e/backports .csv-1.0.6-py2.py3-none-any.whl
Collecting mysqlclient (from pattern)
   Downloading https: //files .pythonhosted.org /packages/ec/fd/83329b9d3e14f7344d1cb31f128e6dbba70c5975c9e57896815dbb1988ad/mysqlclient-1 .3.13. tar .gz (90kB)
     100% |████████████████████████████████| 92kB 25.0MB /s
     Complete output from  command  python setup.py egg_info:
     /bin/sh : 1: mysql_config: not found
     Traceback (most recent call last):
       File  "<string>" , line 1,  in  <module>
       File  "/tmp/pip-install-il6bltj_/mysqlclient/setup.py" , line 18,  in  <module>
         metadata, options = get_config()
       File  "/tmp/pip-install-il6bltj_/mysqlclient/setup_posix.py" , line 53,  in  get_config
         libs = mysql_config( "libs_r" )
       File  "/tmp/pip-install-il6bltj_/mysqlclient/setup_posix.py" , line 28,  in  mysql_config
         raise EnvironmentError( "%s not found"  % (mysql_config.path,))
     OSError: mysql_config not found
 
     ----------------------------------------
Command  "python setup.py egg_info"  failed with error code 1  in  /tmp/pip-install-il6bltj_/mysqlclient/

请执行解决:

$ apt-get  install  libmysqlclient-dev python3-dev
In [ 52 ]:  from  pattern.en  import  suggest
 
In [ 53 ]:  print (suggest( 'fianlly' ))
[( 'finally' 1.0 )]
 
In [ 54 ]:  print (suggest( 'flaot' ))
[( 'flat' 0.85 ), ( 'float' 0.15 )]

除此之外,Python 还提供了几个强大的库,包括 PyEnchant 库,它基于 enchant,以及 aspell-python 库,它是目前很流行的 GNU Aspell 的一个 Python 封装。

词干提取

想要理解词干提取的过程需要先理解词干(stem)的含义。它是任何自然语言中最小的独立单元。词素由词干和词缀(affixe)组成。词缀是指前缀、后缀等词语单元,它们附加到次赶上以改变其含义或创建一个新单词。词干也经常称为单词的基本形式,可以通过在次赶上添加词缀来创建新词,这个过程称为词性变化。相反的过程是从单词的变形形式中获得单词的基本形式,这成为词干提取。

以 “JUMP” 一词为例,可以对其添加词缀形成新的单词,如 “JUMPS” “JUMPED” 和 “JUMPING”。在这种情况下,基本单词 “JUMP” 是词干。如果对着三种变形形式中的任一种进行词干提取都将得到基本形式。如图:

上图显示了词干在所有变化中是如何存在的,它构建了一个基础,每个词形变化都是在其上添加词缀构成的。词干提取帮助将词语标准化到其基础词干而不用考虑其词形变化,这对于许多应用程序大有裨益,如文本变化分类或聚类以及信息检索。搜索引擎广泛使用这些技术来提高更好。更准确的结果,而无需高绿单词的形式。

对于词干提取器,nltk 包有几种实现算法。这些词干提取器包含在 stem 模块中,该模块继承了 nltk.stem.api 模块中的 StemmerI 接口。甚至可以使用这个类(严格来说,它是一个接口)作为你的基类来创建自己的词干提取器。目前,最受环境的词干提取器之一就是波特词干提取器,它基于其发明人马丁 波特(Martin Porter) 博士所开发的算法。其原始算法拥有 5 个不同的阶段,用于减少变形的提取词干,其中每个阶段都有自己的一套规则。此外,还有一个 Porter2 词干提取算法,它是波特博士在原始算法基础上提出的改进算法。以下代码段展示了波特词干提取器:

In [ 55 ]:  from  nltk.stem  import  PorterStemmer
 
In [ 56 ]: ps  =  PorterStemmer()
 
In [ 57 ]:  print (ps.stem( 'jumping' ), ps.stem( 'jumps' ), ps.stem( 'jumped' ))
jump jump jump
  
In [ 58 ]:  print (ps.stem( 'lying' ))
lie
 
In [ 59 ]:  print (ps.stem( 'strange' ))
strang

兰卡斯特提取器(Lancaster stemmer)基于兰卡斯特词干算法,通常也称为佩斯/哈斯科词干提取器(Paice/Husk stemmer),由克里斯 D 佩斯(chris D. Paice) 提出。该词干提取器是一个迭代提取器,具体超过 120 条规则来具体说明如何删除或替换词缀已获得词干。以下代码段展示了兰卡斯特词干提取器的用法:

In [ 60 ]:  from  nltk.stem  import  LancasterStemmer
 
In [ 61 ]: ls  =  LancasterStemmer()
 
In [ 62 ]:  print (ls.stem( 'jumping' ), ls.stem( 'jumps' ), ls.stem( 'jumped' ))
jump jump jump
 
 
In [ 63 ]:  print (ls.stem( 'lying' ))
lying
 
In [ 64 ]:  print (ls.stem( 'strange' ))
strange

可以看出,这个词干提取器的行为与波特词干提取的行为是不同的。

还有一些其他的词干辞去器,包括 RegexpStemmer,它使支持英语外,还支持其他 13 中不同的语言。

以下代码段显示了如何使用 RegexpStemmer 执行词干提取。RegexpStemmer 使用正则表达式来识别词语中的形态学词缀,并且删除与之匹配的任何部分:

In [ 68 ]:  from  nltk.stem  import  RegexpStemmer
 
In [ 69 ]: rs  =  RegexpStemmer( 'ing$|s$|ed$' min = 4 )
 
In [ 70 ]:  print (rs.stem( 'jumping' ), rs.stem( 'jumps' ), rs.stem( 'jumped' ))
jump jump jump
 
In [ 71 ]:  print (rs.stem( 'lying' ))
ly
 
In [ 72 ]:  print (rs.stem( 'strange' ))
strange

可以看出上面的词干提取结果与之前的词干提取结果之间的差异,上面的词干提取结果完全取决于自定义的提取规则(该规则基于正则表达式)。以下代码段展示了如何使用 SnowballStemmer 来给予其他语言的词干提取:

In [ 79 ]:  from  nltk.stem  import  SnowballStemmer
 
In [ 80 ]: ss  =  SnowballStemmer( "german" )
 
In [ 81 ]: ss.stem( 'autobahnen' )
Out[ 81 ]:  'autobahn'
 
In [ 82 ]: ss.stem( 'springen' )
Out[ 82 ]:  'spring'

波特词干提取器目前最常用的词干提取器,但是在实际执行词干提取时,还是应该根据具体问题来选词干提取器,并经过反复试验以验证提取器效果。如果需要的话,也可以根据自定义的规则来创建自己的提取器。

词形还原

词性还原(lemmatization)的过程与词干提取非常想次,去除词缀已获得单词的基本形式。但在这种情况下,这种基本形式称为跟词(root word),而不是词干。它们的不同之处在于,词干不一定是标准的、正确的单词。也就是说,他可能不存在与词典中。根词也称为词元(lemma),始终存在于词典中。

词形还原的过程比词干提取慢很多,因为它涉及一个附加步骤,当且仅当词元存在于词典中时,才通过去除词缀形成根形式或词元。nltk 包有一个强大的词形还原模块,它使用 WordNet、单词的语法和语义(如词性和语境)来获得词根或词元。词性包括三个实体,名词、动词和形容词,最常见与自然语言。

以下代码段显示了如何对每类词语执行词形还原:

In [ 83 ]:  from  nltk.stem  import  WordNetLemmatizer
 
In [ 84 ]: wnl  =  WordNetLemmatizer()
 
In [ 85 ]:  print (wnl.lemmatize( 'cars' 'n' ))
car
 
In [ 86 ]:  print (wnl.lemmatize( 'men' 'n' ))
men
In [ 87 ]:  print (wnl.lemmatize( 'runing' 'v' ))
run
 
In [ 88 ]:  print (wnl.lemmatize( 'ate' 'v' ))
eat
In [ 89 ]:  print (wnl.lemmatize( 'saddest' 'a' ))
sad
 
In [ 90 ]:  print (wnl.lemmatize( 'fancier' 'a' ))
fancy

上述代码段展示了每个单词是如何使用词形还原回其基本格式的。词形还原有助于进行词语标准化。上述代码利用了 WordNetLemmatizer 类,它使用 WordNetCorpusReader 类的 morphy() 函数。该函数使用单词及其词形,通过对比 WordNet 语料库,并采用递归技术删除词缀知道在词汇网络中找到匹配项,最终获得输入词的基本形式或词元。如果没有找到词配项,则将返回输入词(输入词不做任何变化)。

在这里,词性非常重要,因为如果词性是错误的,那么词形还原就是失效,如下面的代码所示:

In [ 91 ]:  print (wnl.lemmatize( 'ate' 'n' ))
ate
 
In [ 92 ]:  print (wnl.lemmatize( 'fancier' 'v' ))
fancier

猜你喜欢

转载自www.cnblogs.com/dalton/p/11353911.html
今日推荐