一個簡單的拼寫檢查器


記得以前就看過這篇文章:How to write a spelling corrector,文章將貝葉斯原理運用於拼寫檢查,二十幾行簡單的Python的代碼就實現了一個拼寫檢查器。

原作者python代碼:

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):
   splits     = [(word[:i], word[i:]) for i in range(len(word) + 1)]
   deletes    = [a + b[1:] for a, b in splits if b]
   transposes = [a + b[1] + b[0] + b[2:] for a, b in splits if len(b)>1]
   replaces   = [a + c + b[1:] for a, b in splits for c in alphabet if b]
   inserts    = [a + c + b     for a, b in splits for c in alphabet]
   return set(deletes + transposes + replaces + inserts)

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=NWORDS.get)

讀完整篇文章,感嘆數學的美妙之外,也很喜歡類似這樣的文章,將一個問題的原理講清楚,再配上一些代碼實例做說明,從小見大,從淺入深,對人很有啟發。

這里簡單的介紹下基於貝葉斯原理的拼寫檢查原理,再給出一個java版和C#版的實現。

拼寫檢查器的原理:給定一個單詞,選擇和它最相似的拼寫正確的單詞,需要使用概率論,而不是基於規則的判斷。給定一個詞 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 並且選取概率最大的, 因為我們有理由相信, 一個(正確的)單詞出現的頻率高, 用戶又容易把它敲成另一個錯誤的單詞, 那么, 那個敲錯的單詞應該被更正為這個正確的.

最終計算的就是argmaxc P(w|c) P(c),在原文中,以“編輯距離”的大小來確定求最大概率的優先級: 編輯距離為1的正確單詞比編輯距離為2的優先級高, 而編輯距離為0的正確單詞優先級比編輯距離為1的高。

java版實現:

public class SpellCorrect {
    private final HashMap<String, Integer> nWords = new HashMap<String, Integer>();

    //讀取語料庫,存儲在nWords中
    public SpellCorrect(String file) throws IOException {
        BufferedReader in = new BufferedReader(new FileReader(file));
        Pattern p = Pattern.compile("\\w+");
        for(String temp = ""; temp != null; temp = in.readLine()){
            Matcher m = p.matcher(temp.toLowerCase());
            while(m.find()) 
                nWords.put((temp = m.group()), nWords.containsKey(temp) ? nWords.get(temp) + 1 : 1);
        }
        in.close();
    }
    
    //和word編輯距離為1的單詞
    private final HashSet<String> edits(String word){
        HashSet<String> result = new HashSet<>();
        for(int i=0; i < word.length(); ++i)   //刪除
            result.add(word.substring(0, i) + word.substring(i+1));
        for(int i=0; i < word.length()-1; ++i) //交換
            result.add(word.substring(0, i) + word.substring(i+1, i+2) + word.substring(i, i+1) + word.substring(i+2));
        for(int i=0; i < word.length(); ++i)   //替換
            for(char c='a'; c <= 'z'; ++c) result.add(word.substring(0, i) + String.valueOf(c) + word.substring(i+1));
        for(int i=0; i <= word.length(); ++i)  //插入
            for(char c='a'; c <= 'z'; ++c) result.add(word.substring(0, i) + String.valueOf(c) + word.substring(i));
        return result;
    }

    public final String correct(String word) {
        if(nWords.containsKey(word)) //如果在語料庫中,直接返回
            return word;
        HashSet<String> set = edits(word);
        HashMap<Integer, String> candidates = new HashMap<Integer, String>();
        for(String s : set) //在語料庫中找編輯距離為1且出現次數最多的單詞為正確單詞
            if(nWords.containsKey(s)) 
                candidates.put(nWords.get(s),s);
        if(candidates.size() > 0) 
            return candidates.get(Collections.max(candidates.keySet()));
        for(String s : set) //在語料庫中找編輯距離為2且出現次數最多的單詞為正確單詞
            for(String w : edits(s)) 
                if(nWords.containsKey(w)) 
                    candidates.put(nWords.get(w),w);
        return candidates.size() > 0 ? candidates.get(Collections.max(candidates.keySet())) : word;
    }

    public static void main(String args[]) throws IOException {
        SpellCorrect spellCorrect = new SpellCorrect("big.txt");
        System.out.println(spellCorrect.correct("spel"));
        System.out.println(spellCorrect.correct("speling"));
    }
}

C#版實現:

namespace SpellCorrect
{
    class Program
    {
        const string Alphabet = "abcdefghijklmnopqrstuvwxyz";

        static IEnumerable<string> Edits1(string w)
        {
            // Deletion
            return (from i in Enumerable.Range(0, w.Length)
                    select w.Substring(0, i) + w.Substring(i + 1))
                // Transposition
             .Union(from i in Enumerable.Range(0, w.Length - 1)
                    select w.Substring(0, i) + w.Substring(i + 1, 1) +
                           w.Substring(i, 1) + w.Substring(i + 2))
                // Alteration
             .Union(from i in Enumerable.Range(0, w.Length)
                    from c in Alphabet
                    select w.Substring(0, i) + c + w.Substring(i + 1))
                // Insertion
             .Union(from i in Enumerable.Range(0, w.Length + 1)
                    from c in Alphabet
                    select w.Substring(0, i) + c + w.Substring(i));
        }

        static string Correct(string word, Dictionary<string,int> nWords)
        {
            Func<IEnumerable<string>, IEnumerable<string>> nullIfEmpty = c => c.Any() ? c : null;

            var candidates =
                nullIfEmpty(new[] { word }.Where(nWords.ContainsKey))
                ?? nullIfEmpty(Edits1(word).Where(nWords.ContainsKey))
                ?? nullIfEmpty((from e1 in Edits1(word)
                                from e2 in Edits1(e1)
                                where nWords.ContainsKey(e2)
                                select e2).Distinct());

            return candidates == null
                ? word
                : (from cand in candidates
                   orderby (nWords.ContainsKey(cand) ? nWords[cand] : 1) descending
                   select cand).First();
        }

        static void Main(string[] args)
        {
            var nWords = (from Match m in Regex.Matches(File.ReadAllText("big.txt").ToLower(), "[a-z]+")
                          group m.Value by m.Value)
                     .ToDictionary(gr => gr.Key, gr => gr.Count());

            Console.WriteLine(Correct("spel",nWords));
            Console.WriteLine(Correct("speling",nWords));

            Console.ReadKey();
        }
    }
}

兩個版本的實現都參考了網上其他人的代碼,略有修改。

語料庫下載:big.txt

 

 

參考:

http://norvig.com/spell-correct.html

http://blog.youxu.info/spell-correct.html

 


免責聲明!

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



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