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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM