怎樣寫一個拼寫檢查器 Peter Norvig 翻譯: Eric You XU

怎樣寫一個拼寫檢查器

Peter Norvig
翻譯: Eric You XU


上個星期, 我的兩個朋友 Dean 和 Bill 分別告訴我說他們對 Google 的快速高質量的拼寫檢查工具感到驚奇. 比如說在搜索的時候鍵入 [speling], 在不到 0.1 秒的時間內, Google 會返回: 你要找的是不是 [spelling]. (Yahoo! 和微軟也有類似的功能). 讓我感到有點奇怪的是我原想 Dean 和 Bill 這兩個很牛的工程師和數學家應該對於使用統計語言模型構建拼寫檢查器有職業的敏感. 但是他們似乎沒有這個想法. 我後來想了想, 他們的確沒什麼理由很熟悉統計語言模型. 不是他們的知識有問題, 而是我預想的本來就是不對的.

我覺得, 如果對這方面的工作做個解釋, 他們和其他人肯定會受益. 然而像Google 的那樣工業強度的拼寫檢查器的全部細節只會讓人感到迷惑而不是受到啓迪. 前幾天我乘飛機回家的時候, 順便寫了幾十行程序, 作爲一個玩具性質的拼寫檢查器. 這個拼寫檢查器大約1秒能處理10多個單詞, 並且達到 80% -90% 的準確率. 下面就是我的代碼, 用Python 2.5 寫成, 一共21 行, 是一個功能完備的拼寫檢查器.

import re, collections

def words(text): return re.findall('[a-z]+', text.lower())

def train(features):
    model
= collections.defaultdict(lambda: 1)
   
for f in features:
        model
[f] += 1
   
return model

NWORDS
= train(words(file('big.txt').read()))

alphabet
= 'abcdefghijklmnopqrstuvwxyz'

def edits1(word):
    n
= len(word)
   
return set([word[0:i]+word[i+1:] for i in range(n)] +                     # deletion
               
[word[0:i]+word[i+1]+word[i]+word[i+2:] for i in range(n-1)] + # transposition
               
[word[0:i]+c+word[i+1:] for i in range(n) for c in alphabet] + # alteration
               
[word[0:i]+c+word[i:] for i in range(n+1) for c in alphabet])  # insertion

def known_edits2(word):
   
return set(e2 for e1 in edits1(word) for e2 in edits1(e1) if e2 in NWORDS)

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

def correct(word):
    candidates
= known([word]) or known(edits1(word)) or known_edits2(word) or [word]
   
return max(candidates, key=lambda w: NWORDS[w])

這段代碼定義了一個函數叫 correct, 它以一個單詞作爲輸入參數, 返回最可能的拼寫建議結果. 比如說:

>>> correct('speling')
'spelling'
>>> correct('korrecter')
'corrector'

 

拼寫檢查器的原理, 一些簡單的概率知識

我簡單的介紹一下它的工作原理. 給定一個單詞, 我們的任務是選擇和它最相似的拼寫正確的單詞. (如果這個單詞本身拼寫就是正確的, 那麼最相近的就是它自己啦). 當然, 不可能絕對的找到相近的單詞, 比如說給定 lates 這個單詞, 它應該別更正爲 late 呢還是 latest 呢? 這些困難指示我們, 需要使用概率論, 而不是基於規則的判斷. 我們說, 給定一個詞 w, 在所有正確的拼寫詞中, 我們想要找一個正確的詞 c, 使得對於 w 的條件概率最大, 也就是說:

argmaxc P(c|w)

按照 貝葉斯理論 上面的式子等價於:

argmaxc P(w|c) P(c) / P(w)

因爲用戶可以輸錯任何詞, 因此對於任何 c 來講, 出現 w 的概率 P(w) 都是一樣的, 從而我們在上式中忽略它, 寫成:

argmaxc P(w|c) P(c)

這個式子有三個部分, 從右到左, 分別是:

1. P(c), 文章中出現一個正確拼寫詞 c 的概率, 也就是說, 在英語文章中, c 出現的概率有多大呢? 因爲這個概率完全由英語這種語言決定, 我們稱之爲做語言模型. 好比說, 英語中出現 the 的概率  P('the') 就相對高, 而出現  P('zxzxzxzyy') 的概率接近0(假設後者也是一個詞的話).

2. P(w|c), 在用戶想鍵入 c 的情況下敲成 w 的概率. 因爲這個是代表用戶會以多大的概率把 c 敲錯成 w, 因此這個被稱爲誤差模型.

3. argmaxc, 用來枚舉所有可能的 c 並且選取概率最大的, 因爲我們有理由相信, 一個(正確的)單詞出現的頻率高, 用戶又容易把它敲成另一個錯誤的單詞, 那麼, 那個敲錯的單詞應該被更正爲這個正確的.

有人肯定要問, 你笨啊, 爲什麼把最簡單的一個 P(c|w) 變成兩項複雜的式子來計算? 答案是本質上 P(c|w) 就是和這兩項同時相關的, 因此拆成兩項反而容易處理. 舉個例子, 比如一個單詞 thew 拼錯了. 看上去 thaw 應該是正確的, 因爲就是把 a 打成 e 了. 然而, 也有可能用戶想要的是 the, 因爲 the 是英語中常見的一個詞, 並且很有可能打字時候手不小心從 e 滑到 w 了. 因此, 在這種情況下, 我們想要計算  P(c|w), 就必須同時考慮 c 出現的概率和從 c 到 w 的概率. 把一項拆成兩項反而讓這個問題更加容易更加清晰.

現在, 讓我們看看程序究竟是怎麼一回事. 首先是計算 P(c), 我們可以讀入一個巨大的文本文件, big.txt, 這個裏面大約有幾百萬個詞(相當於是語料庫了). 這個文件是由Gutenberg 計劃 中可以獲取的一些書, WiktionaryBritish National Corpus 語料庫構成. (當時在飛機上我只有福爾摩斯全集, 我後來又加入了一些, 直到效果不再顯著提高爲止).

然後, 我們利用一個叫 words 的函數把語料中的單詞全部抽取出來, 轉成小寫, 並且去除單詞中間的特殊符號. 這樣, 單詞就會成爲字母序列, don't 就變成 don 和 t 了.1 接着我們訓練一個概率模型, 別被這個術語嚇倒, 實際上就是數一數每個單詞出現幾次. 在 train 函數中, 我們就做這個事情.

def words(text): return re.findall('[a-z]+', text.lower()) 

def train(features):
    model
= collections.defaultdict(lambda: 1)
   
for f in features:
        model
[f] += 1
   
return model

NWORDS
= train(words(file('big.txt').read()))

實際上, NWORDS[w] 存儲了單詞 w 在語料中出現了多少次. 不過一個問題是要是遇到我們從來沒有過見過的新詞怎麼辦. 假如說一個詞拼寫完全正確, 但是語料庫中沒有包含這個詞, 從而這個詞也永遠不會出現在訓練集中. 於是, 我們就要返回出現這個詞的概率是0. 這個情況不太妙, 因爲概率爲0這個代表了這個事件絕對不可能發生, 而在我們的概率模型中, 我們期望用一個很小的概率來代表這種情況. 實際上處理這個問題有很多成型的標準方法, 我們選取一個最簡單的方法: 從來沒有過見過的新詞一律假設出現過一次. 這個過程一般成爲”平滑化”, 因爲我們把概率分佈爲0的設置爲一個小的概率值. 在語言實現上, 我們可以使用Python collention 包中的 defaultdict 類, 這個類和 python 標準的 dict (其他語言中可能稱之爲 hash 表) 一樣, 唯一的不同就是可以給任意的鍵設置一個默認值, 在我們的例子中, 我們使用一個匿名的 lambda:1 函數, 設置默認值爲 1.


然後的問題是: 給定一個單詞 w, 怎麼能夠枚舉所有可能的正確的拼寫呢? 實際上前人已經研究得很充分了, 這個就是一個編輯距離的概念. 這兩個詞之間的編輯距離
定義爲使用了幾次插入(在詞中插入一個單字母), 刪除(刪除一個單字母), 交換(交換相鄰兩個字母), 替換(把一個字母換成另一個)的操作從一個詞變到另一個詞.
下面這個函數可以返回所有與單詞 w 編輯距離爲 1 的集合.

def edits1(word):
    n
= len(word)
   
return set([word[0:i]+word[i+1:] for i in range(n)] +                     # deletion
               
[word[0:i]+word[i+1]+word[i]+word[i+2:] for i in range(n-1)] + # transposition
               
[word[0:i]+c+word[i+1:] for i in range(n) for c in alphabet] + # alteration
               
[word[0:i]+c+word[i:] for i in range(n+1) for c in alphabet])  # insertion

顯然, 這個集合很大. 對於一個長度爲 n 的單詞, 可能有n種刪除, n-1中對換, 26n 種 (譯註: 實際上是 25n 種)替換 和 26(n+1) 種插入 (譯註: 實際上比這個小, 因爲在一個字母前後再插入這個字母構成的詞是等價的). 這樣的話, 一共就是 54n + 25 中情況 (當中還有一點重複). 比如說, 和 something 這個單詞的編輯距離爲1 的詞按照這個算來是 511 個, 而實際上是 494 個.

一般講拼寫檢查的文獻宣稱大約80-95%的拼寫錯誤都是介於編譯距離 1 以內. 然而下面我們看到, 當我對於一個有270個拼寫錯誤的語料做實驗的時候, 我發現只有76%的拼寫錯誤是屬於編輯距離爲1的集合. 或許是我選取的例子比典型的例子難處理一點吧. 不管怎樣, 我覺得這個結果不夠好, 因此我開始考慮編輯距離爲 2 的那些單詞了. 這個事情很簡單, 遞歸的來看, 就是把 edit1 函數再作用在 edit1 函數的返回集合的每一個元素上就行了. 因此, 我們定義函數 edit2:

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

這個語句寫起來很簡單, 實際上背後是很龐大的計算量: 與 something 編輯距離爲2的單詞居然達到了 114,324 個. 不過編輯距離放寬到2以後, 我們基本上就能覆蓋所有的情況了, 在270個樣例中, 只有3個的編輯距離大於2. 當然我們可以做一些小小的優化: 在這些編輯距離小於2的詞中間, 只把那些正確的詞作爲候選詞. 我們仍然考慮所有的可能性, 但是不需要構建一個很大的集合, 因此, 我們構建一個函數叫做 known_edits2, 這個函數只返回那些正確的並且與 w 編輯距離小於2 的詞的集合:

def known_edits2(word):
   
return set(e2 for e1 in edits1(word) for e2 in edits1(e1) if e2 in NWORDS)

現在, 在剛纔的 something 例子中, known_edits2('something') 只能返回 3 個單詞: 'smoothing', 'something' 和 'soothing', 而實際上所有編輯距離爲 1 或者  2 的詞一共有 114,324 個. 這個優化大約把速度提高了 10%.

最後剩下的就是誤差模型部分 P(w|c) 了. 這個也是當時難住我的部分. 當時我在飛機上, 沒有網絡, 也就沒有數據用來構建一個拼寫錯誤模型. 不過我有一些常識性的知識: 把一個元音拼成另一個的概率要大於輔音 (因爲人常常把 hello 打成 hallo 這樣); 把單詞的第一個字母拼錯的概率會相對小, 等等. 但是我並沒有具體的數字去支撐這些證據. 因此, 我選擇了一個簡單的方法: 編輯距離爲1的正確單詞比編輯距離爲2的優先級高, 而編輯距離爲0的正確單詞優先級比編輯距離爲1的高. 因此, 用代碼寫出來就是:

(譯註: 此處作者使用了Python語言的一個巧妙性質: 短路表達式. 在下面的代碼中, 如果known(set)非空, candidate 就會選取這個集合, 而不繼續計算後面的; 因此, 通過Python語言的短路表達式, 作者很簡單的實現了優先級)

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

def correct(word):
    candidates
= known([word]) or known(edits1(word)) or known_edits2(word) or [word]
   
return max(candidates, key=lambda w: NWORDS[w])

correct 函數從一個候選集合中選取最大概率的. 實際上, 就是選取有最大 P(c) 值的那個. 所有的 P(c) 值都存儲在 NWORDS 結構中.

 

效果

現在我們看看算法效果怎麼樣. 在飛機上我嘗試了好幾個例子, 效果還行. 飛機着陸後, 我從牛津文本檔案庫 (Oxford Text Archive)下載了 Roger Mitton 的 Birkbeck 拼寫錯誤語料庫. 從這個庫中, 我取出了兩個集合, 作爲我要做拼寫檢查的目標. 第一個集合用來作爲在開發中作爲參考, 第二個作爲最後的結果測試. 也就是說, 我程序完成之前不參考它, 而把程序在其上的測試結果作爲最後的效果. 用兩個集合一個訓練一個對照是一種良好的實踐, 至少這樣可以避免我通過對特定數據集合進行特殊調整從而自欺欺人. 這裏我給出了一個測試的例子和一個運行測試的例子. 實際的完整測試例子和程序可以參見 spell.py.

tests1 = { 'access': 'acess', 'accessing': 'accesing', 'accommodation':
   
'accomodation acommodation acomodation', 'account': 'acount', ...}

tests2
= {'forbidden': 'forbiden', 'decisions': 'deciscions descisions',
   
'supposedly': 'supposidly', 'embellishing': 'embelishing', ...}

def spelltest(tests, bias=None, verbose=False):
   
import time
    n
, bad, unknown, start = 0, 0, 0, time.clock()
   
if bias:
       
for target in tests: NWORDS[target] += bias
   
for target,wrongs in tests.items():
       
for wrong in wrongs.split():
            n
+= 1
            w
= correct(wrong)
           
if w!=target:
                bad
+= 1
                unknown
+= (target not in NWORDS)
               
if verbose:
                   
print '%r => %r (%d); expected %r (%d)' % (
                        wrong
, w, NWORDS[w], target, NWORDS[target])
   
return dict(bad=bad, n=n, bias=bias, pct=int(100. - 100.*bad/n),
                unknown
=unknown, secs=int(time.clock()-start) )

print spelltest(tests1)
print spelltest(tests2) ## only do this after everything is debugged

這個程序給出了下面的輸出:

{'bad': 68, 'bias': None, 'unknown': 15, 'secs': 16, 'pct': 74, 'n': 270}
{'bad': 130, 'bias': None, 'unknown': 43, 'secs': 26, 'pct': 67, 'n': 400}

在270個測試樣本上 270 , 我大約能在13秒內得到 74% 的正確率 (每秒17個正確詞), 在測試集上, 我得到 67% 正確率 (每秒 15 個).

 

更新: 在這篇文章的原來版本中, 我把結果錯誤的報告高了. 原因是程序中一個小bug. 雖然這個 bug 很不起眼, 但我實際上應該能夠避免. 我爲對閱讀我老版本的這篇文章的讀者造成感到抱歉. 在 spelltest 源程序的第四行, 我忽略了if bias:  並且把 bias 默認值賦值爲0. 我原來想: 如果 bias 是0 , NWORDS[target] += bias 這個語句就不起作用. 而實際上, 雖然這個語句沒有改變 NWORDS[target] 的值, 這個卻讓 (target in NWORDS) 爲真. 這樣的話, spelltest 就會把訓練集合中那些不認識的正確拼寫的單詞都當成認識來處理了, 程序就會"作弊". 我很喜歡 defaultdict 的簡潔, 所以在程序中使用了它, 如果使用 dicts 就不會有這個問題了.2

結論: 我達到了簡潔, 快速開發和運行速度這三個目標, 不過準確率不算太好.

 

將來工作

怎樣才能做到更好結果呢? 讓我們回過頭來看看概率模型中的三個因素:  (1) P(c); (2) P(w|c); and (3) argmaxc. 我們通過程序給出錯誤答案的那些例子入手, 看看這三個因素外, 我們還忽略了什麼.

  1. P(c), 語言模型. 在語言模型中, 有兩種問題會造成最後的錯誤識別. 其中最嚴重的一個因素就是 未知單詞. 在訓練集合中, 一共有15個未知單詞, 它們大約佔了5%; 在測試集合中, 有43個未知詞, 它們佔了11%. 當把 spelltest 的調用參數 verbose 設置爲 True 的時候: 我們可以看到下面的輸出:
    correct('economtric') => 'economic' (121); expected 'econometric' (1)
    correct
    ('embaras') => 'embargo' (8); expected 'embarrass' (1)
    correct
    ('colate') => 'coat' (173); expected 'collate' (1)
    correct
    ('orentated') => 'orentated' (1); expected 'orientated' (1)
    correct
    ('unequivocaly') => 'unequivocal' (2); expected 'unequivocally' (1)
    correct
    ('generataed') => 'generate' (2); expected 'generated' (1)
    correct
    ('guidlines') => 'guideline' (2); expected 'guidelines' (1)

    在這個結果中, 我們可以使用看到 correct 函數作用在那些拼錯的單詞上的結果. (其中 NWORDS 中單詞出現次數在括號中),  然後是我們期望的輸出以及出現的次數. 這個結果告訴我們, 如果程序根本就不知道 'econometric' 是一個單詞, 它也就不可能去把 'economtric' 糾正成 'econometric'. 這個問題可以通過往訓練集合中加入更多語料來解決, 不過也有可能引入更多錯誤. 同時注意到最後四行, 實際上我們的訓練集中有正確的單詞, 只是形式略有不同. 因此, 我們可以改進一下程序, 比如在動詞後面加 '-ed' 或者在名詞後面加 '-s' 也是合法的.

    第二個可能導致錯誤的因素是概率: 兩個詞都出現在我們的字典裏面了, 但是恰恰我們選的概率大的那個不是用戶想要的. 不過我要說的是這個問題其實不是最嚴重的, 也不是獨立發生的, 其他原因可能更加嚴重. 

    我們可以模擬一下看看如果我們提高語言模型, 最後結果能好多少. 比如說, 我們在訓練集上小"作弊"一下. 我們在 spelltest 函數中有一個參數叫做 bias, 實際上就是代表把正確的拼寫詞多添加幾次, 以便提高語言模型中相應的概率. 比如說, 在語料中, 假設正確的詞出現的頻率多了1次, 或者10次, 或者更多. 如果我們增加 bias 這個參數的值, 可以看到訓練集和測試集上的準確率都顯著提高了.

    Bias 訓練集. 測試集
    0 74% 67%
    1 74% 70%
    10 76% 73%
    100 82% 77%
    1000 89% 80%


    在兩個集合上我們都能做到大約 80-90%. 這個顯示出如果我們有一個好的語言模型, 我們或能達到準確率這個目標. 不過, 這個顯得過於樂觀了, 因爲構建一個更大的語言模型會引入新的詞, 從而可能還會引入一些錯誤結果, 儘管這個地方我們沒觀察到這個現象.

    處理未知詞還有另外一種辦法, 比如說, 假如遇到這個詞: "electroencephalographicallz", 比較好的糾正的方法是把最後的 "z" 變成 "y", 因爲 '-cally' 是英文中很常見的一個後綴. 雖然 "electroencephalographically" 這個詞也不在我們的字典中, 我們也能通過基於音節或者前綴後綴等性質給出拼寫建議. 當然, 這種簡單前後綴判斷的方法比基於構詞法的要簡單的多.

  2. P(w|c) 是誤差模型. 到目前爲止, 我們都是用的一個很簡陋的模型: 距離越短, 概率越大. 這個也造成了一些問題, 比如下面的例子中, correct 函數返回了編輯距離爲 1 的詞作爲答案, 而正確答案恰恰編輯距離是 2: 
    correct('reciet') => 'recite' (5); expected 'receipt' (14)
    correct
    ('adres') => 'acres' (37); expected 'address' (77)
    correct
    ('rember') => 'member' (51); expected 'remember' (162)
    correct
    ('juse') => 'just' (768); expected 'juice' (6)
    correct
    ('accesing') => 'acceding' (2); expected 'assessing' (1)

    舉個例子, 程序認爲在 'adres' 中把 'd' 變成 'c' 從而得到 'acres' 的優先級比把 d 寫成 dd 以及 s 寫成 ss 的優先級高, 從而作出了錯誤的判斷. 還有些時候程序在兩個編輯距離一樣的候選詞中選擇了錯誤的一個, 比如:

    correct('thay') => 'that' (12513); expected 'they' (4939)
    correct
    ('cleark') => 'clear' (234); expected 'clerk' (26)
    correct
    ('wer') => 'her' (5285); expected 'were' (4290)
    correct
    ('bonas') => 'bones' (263); expected 'bonus' (3)
    correct
    ('plesent') => 'present' (330); expected 'pleasant' (97)

    這個例子給我們一個同樣的教訓: 在 'thay' 中, 把 'a' 變成 'e' 的概率比把 'y' 拼成 't' 大. 爲了正確的選擇 'they', 我們至少要在先驗概率上乘以 2.5, 才能使得最後 they 的機率超過 that, 從而選擇 they. 

    顯然, 我們可以用一個更好的模型來衡量拼錯單詞的概率. 比如說, 把一個字母順手打成兩個, 或者把一個元音打成另一個的情況都應該比其他打字錯誤更加容易發生. 當然, 更好的辦法還是從數據入手: 比如說, 找一個拼寫錯誤語料, 然後統計插入; 刪除; 交換和變換在給定周圍字母情況下的概率. 爲了採集到這些概率, 可能我們需要非常大的數據集. 比如說, 如果我們帶着觀察左右兩個字母作爲上下文, 看看一個字母替換成另一個的概率, 就一共有 266 種情況, 也就是大約超過 3 億個情況. 然後每種情況需要平均幾個證據作爲支撐, 因此我們知道10億個字母的訓練集. 如果爲了保證更好的質量, 可能至少100億個才差不多.

    需要注意的是, 語言模型和誤差模型之間是有聯繫的. 我們的程序中假設了編輯距離爲 1 的優先於編輯距離爲 2 的. 這種誤差模型或多或少也使得語言模型的優點難以發揮. 我們之所以沒有往語言模型中加入很多不常用的單詞, 是因爲我們擔心添加這些單詞後, 他們恰好和我們要更正的詞編輯距離是1, 從而那些出現頻率更高但是編輯距離爲 2 的單詞就不可能被選中了. 如果有一個更加好的誤差模型, 或許我們就能夠放心大膽的添加更多的不常用單詞了. 下面就是一個因爲添加不常用單詞影響結果的例子:

    correct('wonted') => 'wonted' (2); expected 'wanted' (214)
    correct
    ('planed') => 'planed' (2); expected 'planned' (16)
    correct
    ('forth') => 'forth' (83); expected 'fourth' (79)
    correct
    ('et') => 'et' (20); expected 'set' (325)
  3. 枚舉所有可能的概率並且選擇最大的: argmaxc. 我們的程序枚舉了直到編輯距離爲2的所有單詞. 在測試集合中, 270個單詞中, 只有3個編輯距離大於2, 但是在測試集合中, 400箇中卻有23個. 他們是:
    purple perpul
    curtains courtens
    minutes muinets

    successful sucssuful
    hierarchy heiarky
    profession preffeson
    weighted wagted
    inefficient ineffiect
    availability avaiblity
    thermawear thermawhere
    nature natior
    dissension desention
    unnecessarily unessasarily
    disappointing dissapoiting
    acquaintances aquantences
    thoughts thorts
    criticism citisum
    immediately imidatly
    necessary necasery
    necessary nessasary
    necessary nessisary
    unnecessary unessessay
    night nite
    minutes muiuets
    assessing accesing
    necessitates nessisitates

    我們可以考慮有限的允許一些編輯距離爲3的情況. 比如說, 我們可以只允許在元音旁邊插入一個元音, 或者把元音替換, 或者把 c 寫成 s 等等. 這些基本上就覆蓋了上面所有的情況了.

  4. 第四種, 也是最好的一種改進方法是改進 correct  函數的接口, 讓他可以分析上下文給出決斷. 因爲很多情況下, 僅僅根據單詞本身做決斷很難, 這個單詞本身就在字典中, 但是在上下文中, 應該被更正爲另一個單詞. 比如說: 
    correct('where') => 'where' (123); expected 'were' (452)
    correct
    ('latter') => 'latter' (11); expected 'later' (116)
    correct
    ('advice') => 'advice' (64); expected 'advise' (20)

    如果單看 'where' 這個單詞本身, 我們無從知曉說什麼情況下該把 correct('where') 返回 'were' , 又在什麼情況下返回 'where'. 但是如果我們給 correct 函數的是:'They where going', 這時候 "where" 就應該被更正爲 "were".

    上下文可以幫助程序從多個候選答案中選出最好的, 比如說: 

    correct('hown') => 'how' (1316); expected 'shown' (114)
    correct
    ('ther') => 'the' (81031); expected 'their' (3956)
    correct
    ('quies') => 'quiet' (119); expected 'queries' (1)
    correct
    ('natior') => 'nation' (170); expected 'nature' (171)
    correct
    ('thear') => 'their' (3956); expected 'there' (4973)
    correct
    ('carrers') => 'carriers' (7); expected 'careers' (2)

    爲什麼 'thear' 要被更正爲 'there' 而不是 'their' 呢?  只看單詞本身, 這個問題不好回答, 不過一旦放句子 'There's no there thear' 中, 答案就立即清楚明瞭了.

    要構建一個同時能處理多個詞(詞以及上下文)的系統, 我們需要大量的數據. 所幸的是 Google 已經公開發布了最長 5個單詞的所有序列數據庫, 這個是從上千億個詞的語料數據中收集得到的. 我相信一個能達到 90% 準確率的拼寫檢查器已經需要考慮上下文以做決定了. 不過, 這個, 咱們改天討論 :)

     

  5. 我們可以通過優化訓練數據和測試數據來提高準確率. 我們抓取了大約100萬個單詞並且假設這些詞都是拼寫正確的. 但是這個事情並不這麼完美, 這些數據集也可能有錯. 我們可以嘗試這找出這些錯並且修正他們. 這個地方, 修正測試集合並不困難. 我留意到至少有三種情況下, 測試集合說我們的程序給出了錯誤的答案, 而我卻認爲我們程序的答案比測試集給的答案要好, 比如說: (實際上測試集給的三個答案的拼寫都不正確)
    correct('aranging') => 'arranging' (20); expected 'arrangeing' (1)
    correct
    ('sumarys') => 'summary' (17); expected 'summarys' (1)
    correct
    ('aurgument') => 'argument' (33); expected 'auguments' (1)

    我們還可以決定英語的變種, 以便訓練我們的程序, 比如說下面的三個錯誤是因爲美式英語和英式英語拼發不一樣造成的, (我們的訓練集兩者都有):

    correct('humor') => 'humor' (17); expected 'humour' (5)
    correct
    ('oranisation') => 'organisation' (8); expected 'organization' (43)
    correct
    ('oranised') => 'organised' (11); expected 'organized' (70)
  6. 最後的一個改進是讓程序運行得更加快一點. 比如說, 我們用編譯語言來寫, 而不是用解釋語言. 我們可以使用查找表, 而不用Python提供的通用的 dict 對象, 我們可以緩存計算結果, 從而避免重複計算, 等等. 一個小建議是: 在做任何速度優化之前, 先弄清楚到底程序的時間花在什麼地方了. 

延伸閱讀

訂正

我原始的程序一共 20行, 不過 Ivan Peev 指出說我的 string.lowercase, 在某些 locale 和某些版本的 Python中會包含 a-z 以外的更多字母, 因此我加了一個字母表, 我也可以使用 string.ascii_lowercase.

感謝 Jay Liang 指出一共 54n+25 個編輯距離爲 1的詞, 而不是 55n+25 個.

感謝Dmitriy Ryaboy 指出 NWORDS[target] += bias bug.

其他編程語言實現

我發表這個文章後, 很多人用其他語言實現了. 我的目的是算法而不是 Python. 對於那些比較不同語言的人, 這些其他語言實現可能很有意思:

其他自然語言翻譯




譯註:

1. 這個地方顯然作者是爲了簡化程序, 實際上don't 一般都按照 dont 來處理.

2. 如果程序把訓練集合中正確的目標詞存放到 NWORDS 中, 就等價於提前知道答案了, 如果這個錯誤的詞編輯距離爲 2 之內沒有其他正確詞, 只有一個這個答案, 程序肯定會選取這個答案. 這個就是所謂的"作弊".

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章