1. 程式人生 > >文字糾錯,BK樹

文字糾錯,BK樹

從編輯距離、BK樹到文字糾錯

搜尋引擎裡有一個很重要的話題,就是文字糾錯,主要有兩種做法,一是從詞典糾錯,一是分析使用者搜尋日誌,今天我們探討使用基於詞典的方式糾錯,核心思想就是基於編輯距離,使用BK樹。下面我們來逐一探討:

編輯距離

1965年,俄國科學家Vladimir
Levenshtein給字串相似度做出了一個明確的定義叫做Levenshtein距離,我們通常叫它“編輯距離”。

字串A到B的編輯距離是指,只用插入、刪除和替換三種操作,最少需要多少步可以把A變成B。例如,從FAME到GATE需要兩步(兩次替換),從GAME到ACM則需要三步(刪除G和E再新增C)。Levenshtein給出了編輯距離的一般求法,就是大家都非常熟悉的經典動態規劃問題。

 class LevenshteinDistanceFunction {

        private final boolean isCaseSensitive;

        public LevenshteinDistanceFunction(boolean isCaseSensitive) {
            this.isCaseSensitive = isCaseSensitive;
        }

        public int distance(CharSequence left, CharSequence right) {
            int leftLength = left.length(), rightLength = right.length();

            // special cases.
            if (leftLength == 0)
                return rightLength;
            if (rightLength == 0)
                return leftLength;

            // Use the iterative matrix method.
            int[] currentRow = new int[rightLength + 1];
            int[] nextRow    = new int[rightLength + 1];

            // Fill first row with all edit counts.
            for (int i = 0; i <= rightLength; i++)
                currentRow[i] = i;

            for (int i = 1; i <= leftLength; i++) {
                nextRow[0] = i;

                for(int j = 1; j <= rightLength; j++) {
                    int subDistance = currentRow[j - 1]; // Distance without insertions or deletions.
                    if (!charEquals(left.charAt(i - 1), right.charAt(j - 1), isCaseSensitive))
                            subDistance++; // Add one edit if letters are different.
                    nextRow[j] = Math.min(Math.min(nextRow[j - 1], currentRow[j]) + 1, subDistance);
                }

                // Swap rows, use last row for next row.
                int[] t = currentRow;
                currentRow = nextRow;
                nextRow = t;
            }

            return currentRow[rightLength];
        }

    }

BK樹

編輯距離的經典應用就是用於拼寫檢錯,如果使用者輸入的詞語不在詞典中,自動從詞典中找出編輯距離小於某個數n的單詞,讓使用者選擇正確的那一個,n通常取到2或者3。

這個問題的難點在於,怎樣才能快速在字典裡找出最相近的單詞?可以像 使用貝葉斯做英文拼寫檢查(c#) 裡是那樣,通過單詞自動修改一個單詞,檢查是否在詞典裡,這樣有暴力破解的嫌疑,是否有更優雅的方案呢?

1973年,Burkhard和Keller提出的BK樹有效地解決了這個問題。BK樹的核心思想是:

令d(x,y)表示字串x到y的Levenshtein距離,那麼顯然:
d(x,y) = 0 當且僅當 x=y (Levenshtein距離為0 <==> 字串相等)
d(x,y) = d(y,x) (從x變到y的最少步數就是從y變到x的最少步數)
d(x,y) + d(y,z) >= d(x,z) (從x變到z所需的步數不會超過x先變成y再變成z的步數)

最後這一個性質叫做三角形不等式。就好像一個三角形一樣,兩邊之和必然大於第三邊。

BK建樹

首先我們隨便找一個單詞作為根(比如GAME)。以後插入一個單詞時首先計算單詞與根的Levenshtein距離:如果這個距離值是該節點處頭一次出現,建立一個新的兒子節點;否則沿著對應的邊遞迴下去。例如,我們插入單詞FAME,它與GAME的距離為1,於是新建一個兒子,連一條標號為1的邊;下一次插入GAIN,算得它與GAME的距離為2,於是放在編號為2的邊下。再下次我們插入GATE,它與GAME距離為1,於是沿著那條編號為1的邊下去,遞迴地插入到FAME所在子樹;GATE與FAME的距離為2,於是把GATE放在FAME節點下,邊的編號為2。

enter description here

BK查詢

如果我們需要返回與錯誤單詞距離不超過n的單詞,這個錯誤單詞與樹根所對應的單詞距離為d,那麼接下來我們只需要遞迴地考慮編號在d-n到d+n範圍內的邊所連線的子樹。由於n通常很小,因此每次與某個節點進行比較時都可以排除很多子樹

可以通過下圖(來自 超酷演算法(1):BK樹 (及個人理解))理解:

enter description here

BK 實現

知道了原理實現就簡單了,這裡從github找一段程式碼

建樹:

public boolean add(T t) {
        if (t == null)
            throw new NullPointerException();

        if (rootNode == null) {
            rootNode = new Node<>(t);
            length = 1;
            modCount++; // Modified tree by adding root.
            return true;
        }

        Node<T> parentNode = rootNode;
        Integer distance;
        while ((distance = distanceFunction.distance(parentNode.item, t)) != 0
                || !t.equals(parentNode.item)) {
            Node<T> childNode = parentNode.children.get(distance);
            if (childNode == null) {
                parentNode.children.put(distance, new Node<>(t));
                length++;
                modCount++; // Modified tree by adding a child.
                return true;
            }
            parentNode = childNode;
        }

        return false;
    }

查詢:

 public List<SearchResult<T>> search(T t, int radius) {
        if (t == null)
            return Collections.emptyList();
        ArrayList<SearchResult<T>> searchResults = new ArrayList<>();
        ArrayDeque<Node<T>> nextNodes = new ArrayDeque<>();
        if (rootNode != null)
            nextNodes.add(rootNode);

        while(!nextNodes.isEmpty()) {
            Node<T> nextNode = nextNodes.poll();
            int distance = distanceFunction.distance(nextNode.item, t);
            if (distance <= radius)
                searchResults.add(new SearchResult<>(distance, nextNode.item));
            int lowBound = Math.max(0, distance - radius), highBound = distance + radius;
            for (Integer i = lowBound; i <= highBound; i++) {
                if (nextNode.children.containsKey(i))
                    nextNodes.add(nextNode.children.get(i));
            }
        }

        searchResults.trimToSize();
        Collections.sort(searchResults);
        return Collections.unmodifiableList(searchResults);
    }

使用BK樹做文字糾錯

準備詞典,18萬的影視名稱:
enter description here

測試程式碼:

  static void outputSearchResult( List<SearchResult<CharSequence>> results){
        for(SearchResult<CharSequence> item : results){
            System.out.println(item.item);
        }
    }

    static void test(BKTree<CharSequence> tree,String word){
        System.out.println(word+"的最相近結果:");
        outputSearchResult(tree.search(word,Math.max(1,word.length()/4)));
    }

    public static void main(String[] args) {

        BKTree<CharSequence> tree = new BKTree(DistanceFunctions.levenshteinDistance());
        List<String> testStrings = FileUtil.readLine("./src/main/resources/act/name.txt");
        System.out.println("詞典條數:"+testStrings.size());
        long startTime = System.currentTimeMillis();
        for(String testStr: testStrings){
            tree.add(testStr.replace(".",""));
        }
        System.out.println("建樹耗時:"+(System.currentTimeMillis()-startTime)+"ms");
        startTime = System.currentTimeMillis();
        String[] testWords = new String[]{
                "湄公河凶案",
                "葫蘆絲兄弟",
                "少林足球"
        };

        for (String testWord: testWords){
            test(tree,testWord);
        }
        System.out.println("測試耗時:"+(System.currentTimeMillis()-startTime)+"ms");
    }

結果:

詞典條數:18513
建樹耗時:421ms
湄公河凶案的最相近結果:
湄公河大案
葫蘆絲兄弟的最相近結果:
葫蘆兄弟
少林足球的最相近結果:
少林足球
笑林足球
測試耗時:20ms

參考:
http://blog.csdn.net/tradymeky/article/details/40581547
https://github.com/sk-scd91/BKTree
https://www.cnblogs.com/data2value/p/5707973.html