1. 程式人生 > >【NLP】單詞糾錯——python小練習

【NLP】單詞糾錯——python小練習

起源

本文翻譯自大牛 Peter Norvig 的博文,作為本渣渣技術部落格的第一篇內容,熟悉一下這個部落格的操作哈~

意思就是大牛自己的兩個大牛朋友問大牛,為什麼谷歌的拼寫檢查功能這麼厲害,大牛很驚訝,為什麼這麼厲害的兩個工程師+數學家竟然不懂這種簡單的演算法原理嗎?看來此時只能本大牛寫一個簡單的解釋讓大家能夠從中獲得一些有益的啟發了。這只是一個玩具程式碼,正確率大概在80%~90%之間。速度大概在每秒鐘十個詞左右。程式碼很短。

下面就是程式碼啦~

import re
from collections import Counter

def words(text): return re.findall(r'\w+'
, text.lower()) WORDS = Counter(words(open('big.txt').read())) def P(word, N=sum(WORDS.values())): "Probability of `word`." return WORDS[word] / N def correction(word): "Most probable spelling correction for word." return max(candidates(word), key=P) def candidates(word): "Generate possible spelling corrections for word."
return (known([word]) or known(edits1(word)) or known(edits2(word)) or [word]) def known(words): "The subset of `words` that appear in the dictionary of WORDS." return set(w for w in words if w in WORDS) def edits1(word): "All edits that are one edit away from `word`." letters = 'abcdefghijklmnopqrstuvwxyz'
splits = [(word[:i], word[i:]) for i in range(len(word) + 1)] deletes = [L + R[1:] for L, R in splits if R] transposes = [L + R[1] + R[0] + R[2:] for L, R in splits if len(R)>1] replaces = [L + c + R[1:] for L, R in splits if R for c in letters] inserts = [L + c + R for L, R in splits for c in letters] return set(deletes + transposes + replaces + inserts) def edits2(word): "All edits that are two edits away from `word`." return (e2 for e1 in edits1(word) for e2 in edits1(e1))

函式“correction(word)” 會返回“word”最近似的正確拼寫內容:

>>> correction('speling')
'spelling'

>>> correction('korrectud')
'corrected'

一些概率知識

拼寫檢查器的目的是找到最近似錯誤輸入“w”的正確拼寫,但是對於一個錯誤拼寫,其正確的候選者有很多(例如:“lates”應該被糾正為“late”呢,還是“lattes”呢?)。因此我們可以採取概率的思路,在錯誤拼寫w出現的條件下,選擇所有可能的備選糾正單詞c中概率最大的。

argmaxccandidatesp(c|w)

由貝葉斯公式可得:

p(c|w)=P(c)×P(w|c)P(w)
由於P(w) 對於每個待選擇的c都是一樣大小的,因此我們就忽略這個因素,最終公式變形為: argmaxccandidatesP(c)×P(w|c)
這個公式中由四個主要的部分:
選擇機構:argmax
我們選擇備選單詞中概率最高的單詞作為輸出。
備選模型ccandidates
這一部分告訴我們考慮哪些單詞作為備選。
語言模型P(c)
單詞c出現在語料庫中的概率。例如,在一個英文語料庫中,有7%的單詞是“the”,那麼P(the)=0.07
錯誤模型 P(w|c)
當用戶想輸入C時,錯輸入成w的概率。例如,P(teh|the)應該遠大於P(theeexyz|the)

我們用條件概率 P(w|c) 和先驗概率P(c) 這兩個便於考慮和學習的因素替代了後延概率P(c|w) ,這樣問題更容易分析和解決。

python具體實現過程

1、選擇機構 :由python的max函式實現
2、備選模型 :通過一些簡單的操作(edits),生成一個set作為備選單詞庫。如:刪除一個字母(deletions),交換兩個字母的位置(transposes),把一個字母替換成另一個字母(replacement),增加一個字母(insertion)。通過這幾個常見的拼寫錯誤,可以擴展出一系列的備選單詞,形成一個set。

def edits1(word):
    "All edits that are one edit away from `word`."
    letters    = 'abcdefghijklmnopqrstuvwxyz'
    splits     = [(word[:i], word[i:])    for i in range(len(word) + 1)]
    deletes    = [L + R[1:]               for L, R in splits if R]
    transposes = [L + R[1] + R[0] + R[2:] for L, R in splits if len(R)>1]
    replaces   = [L + c + R[1:]           for L, R in splits if R for c in letters]
    inserts    = [L + c + R               for L, R in splits for c in letters]
    return set(deletes + transposes + replaces + inserts)

這是一個很大的set,因為對於一個長度為n的單詞,會生成n個deletions,n-1個transpositions,26n個replacements,16(n+1)個insertions,總共是(54n+25)個可能性。例如:

>>> len(edits1('somthing'))
442

然而我們可以定義一個識別這些生成的備選單詞正確性的模組,只匹配詞典中存在的詞。這個set將會變得很小,因為隨機生成單詞中,許多都是非法拼寫的,並非真正存在。

def known(words): return set(w for w in words if w in WORDS)

>>> known(edits1('somthing'))
{'something', 'soothing'}

同樣,我們考慮經過兩步驟的簡單操作(edits)後得到的糾錯備選模型(例如,寫錯了兩個字母,寫掉了兩個字母),經過兩次簡單操作的組和將會生成更多的備選單詞,但是也僅有很少一部分是正確拼寫的單詞,例如:

def edits2(word): return (e2 for e1 in edits1(word) for e2 in edits1(e1))

>>> len(set(edits2('something'))
90902

>>> known(edits2('something'))
{'seething', 'smoothing', 'something', 'soothing'}

>>> known(edits2('somthing'))
{'loathing', 'nothing', 'scathing', 'seething', 'smoothing', 'something', 'soothing', 'sorting'}

經過edits2(w)處理的單詞,與原始單詞w的edit distance(不知道怎麼翻譯,翻譯為編輯距離?) 為2。

3、語言模型 我們通過統計在語料庫中某個詞(word)出現的頻率來衡量一個詞的先驗概率P(word),這裡我們使用一個語料庫big.txt來構建我們的語言模型。這個語料庫含有100萬個單詞,裡面包含一本書和一些常見詞彙的列表。定義函式 word 來把語料文字打碎成一個一個單詞的形式,然後構建一個計數器counter,統計每個詞的出現頻率,概率P代表了每個詞出現的概率:

def words(text): return re.findall(r'\w+', text.lower())

WORDS = Counter(words(open('big.txt').read()))

def P(word, N=sum(WORDS.values())): return WORDS[word] / N

通過一些簡單的NLP操作,我們可以看到這裡有32192個單詞,所有單詞一共出現了1115504次,’the’是出現概率最大的,一共出現了79808次:

>>> len(WORDS)
32192

>>> sum(WORDS.values())
1115504

>>> WORDS.most_common(10)
[('the', 79808),
 ('of', 40024),
 ('and', 38311),
 ('to', 28765),
 ('in', 22020),
 ('a', 21124),
 ('that', 12512),
 ('he', 12401),
 ('was', 11410),
 ('it', 10681),
 ('his', 10034),
 ('is', 9773),
 ('with', 9739),
 ('as', 8064),
 ('i', 7679),
 ('had', 7383),
 ('for', 6938),
 ('at', 6789),
 ('by', 6735),
 ('on', 6639)]

>>> max(WORDS, key=P)
'the'

>>> P('the')
0.07154434228832886

>>> P('outrivaled')
8.9645577245801e-07

>>> P('unmentioned')
0.0

4、錯誤模型
作者大牛說寫這個玩具程式碼的時候,在飛機上(人家坐個飛機都這麼厲害。。。),沒有網際網路也沒有資料,因此這個錯誤模型不是學習得到的,是通過定義一些特殊的規則來衡量的。因此,定義了一個函式candidates(word),根據優先順序產生一個非空的候選單詞序列:

優先順序排序如下
1、原始單詞
2、與原始單詞的edit distance為1的單詞(即經過一次編輯產生的那些拼寫)
3、與原始單詞的edit distance為2的單詞(即經過兩次編輯產生的那些拼寫)
4、原始單詞,即使那些單詞是詞典中沒有的。

因此我們把條件概率模組替換成了這樣一種優先順序的排序。或許這其中還有很多不完善的地方,如根據什麼別的語料庫統計到,人們寫單詞寫錯的時候是寫掉一個字母比多加一個字母常見,交換兩個字母比寫錯一個字母常見等這些規則是我們在沒學習也沒資料的時候未知的,也是你在定義自己的拼寫糾錯器時,可以自己考慮的內容。因為我們現在只是一個玩具程式碼,所以我們採取了這樣一個簡單的優先順序排序模式來替代這個重要的部分。

def correction(word): return max(candidates(word), key=P)

def candidates(word): 
    return known([word]) or known(edits1(word)) or known(edits2(word)) or [word]

模型評價

評價資料集
作者用一個牛津大學的資料集測評了自己的玩具程式碼,當你完善了自己的糾錯模型之後,或許你也可以通過這個方式來測試一下你模型的準確率。測試的程式碼如下:

def unit_tests():
    assert correction('speling') == 'spelling'              # insert
    assert correction('korrectud') == 'corrected'           # replace 2
    assert correction('bycycle') == 'bicycle'               # replace
    assert correction('inconvient') == 'inconvenient'       # insert 2
    assert correction('arrainged') == 'arranged'            # delete
    assert correction('peotry') =='poetry'                  # transpose
    assert correction('peotryy') =='poetry'                 # transpose + delete
    assert correction('word') == 'word'                     # known
    assert correction('quintessential') == 'quintessential' # unknown
    assert words('This is a TEST.') == ['this', 'is', 'a', 'test']
    assert Counter(words('This is a test. 123; A TEST this is.')) == (
           Counter({'123': 1, 'a': 2, 'is': 2, 'test': 2, 'this': 2}))
    assert len(WORDS) == 32192
    assert sum(WORDS.values()) == 1115504
    assert WORDS.most_common(10) == [
     ('the', 79808),
     ('of', 40024),
     ('and', 38311),
     ('to', 28765),
     ('in', 22020),
     ('a', 21124),
     ('that', 12512),
     ('he', 12401),
     ('was', 11410),
     ('it', 10681)]
    assert WORDS['the'] == 79808
    assert P('quintessential') == 0
    assert 0.07 < P('the') < 0.08
    return 'unit_tests pass'

def spelltest(tests, verbose=False):
    "Run correction(wrong) on all (right, wrong) pairs; report results."
    import time
    start = time.clock()
    good, unknown = 0, 0
    n = len(tests)
    for right, wrong in tests:
        w = correction(wrong)
        good += (w == right)
        if w != right:
            unknown += (right not in WORDS)
            if verbose:
                print('correction({}) => {} ({}); expected {} ({})'
                      .format(wrong, w, WORDS[w], right, WORDS[right]))
    dt = time.clock() - start
    print('{:.0%} of {} correct ({:.0%} unknown) at {:.0f} words per second '
          .format(good / n, n, unknown / n, n / dt))

def Testset(lines):
    "Parse 'right: wrong1 wrong2' lines into [('right', 'wrong1'), ('right', 'wrong2')] pairs."
    return [(right, wrong)
            for (right, wrongs) in (line.split(':') for line in lines)
            for wrong in wrongs.split()]

print(unit_tests())
spelltest(Testset(open('spell-testset1.txt'))) # Development set
spelltest(Testset(open('spell-testset2.txt'))) # Final test set

測試結果如下:

unit_tests pass
75% of 270 correct at 41 words per second
68% of 400 correct at 35 words per second
None

在這裡作者又自嘲了一下說自己的測試結果正確率只有75%,沒達到之前說的80%-90%,可能是由於資料集太難了。

不過通過以上這些內容,我們也大概知道了最簡單單詞糾錯程式原理。希望可以幫到大家~
嗯~ o( ̄▽ ̄)o,我的第一篇部落格看著還挺像那麼一回事的哈~謝謝大家支援~

此外如果大家只是需要單詞糾錯的功能,而不是來學原理的話呢,個人推薦github上搜索python3的一個庫,叫做autocorrect