web-dev-qa-db-ja.com

効率的なレーベンシュタイン距離計算のための簡単なトライの実装-Java

更新

完了。以下は、最終的にすべてのテストに合格したコードです。繰り返しになりますが、これはMuriloVasconceloによるSteveHanovのアルゴリズムの修正バージョンをモデルにしています。助けてくれたすべてに感謝します!

/**
 * Computes the minimum Levenshtein Distance between the given Word (represented as an array of Characters) and the
 * words stored in theTrie. This algorithm is modeled after Steve Hanov's blog article "Fast and Easy Levenshtein
 * distance using a Trie" and Murilo Vasconcelo's revised version in C++.
 * 
 * http://stevehanov.ca/blog/index.php?id=114
 * http://murilo.wordpress.com/2011/02/01/fast-and-easy-levenshtein-distance-using-a-trie-in-c/
 * 
 * @param ArrayList<Character> Word - the characters of an input Word as an array representation
 * @return int - the minimum Levenshtein Distance
 */
private int computeMinimumLevenshteinDistance(ArrayList<Character> Word) {

    theTrie.minLevDist = Integer.MAX_VALUE;

    int iWordLength = Word.size();
    int[] currentRow = new int[iWordLength + 1];

    for (int i = 0; i <= iWordLength; i++) {
        currentRow[i] = i;
    }

    for (int i = 0; i < iWordLength; i++) {
        traverseTrie(theTrie.root, Word.get(i), Word, currentRow);
    }
    return theTrie.minLevDist;
}

/**
 * Recursive helper function. Traverses theTrie in search of the minimum Levenshtein Distance.
 * 
 * @param TrieNode node - the current TrieNode
 * @param char letter - the current character of the current Word we're working with
 * @param ArrayList<Character> Word - an array representation of the current Word
 * @param int[] previousRow - a row in the Levenshtein Distance matrix
 */
private void traverseTrie(TrieNode node, char letter, ArrayList<Character> Word, int[] previousRow) {

    int size = previousRow.length;
    int[] currentRow = new int[size];
    currentRow[0] = previousRow[0] + 1;

    int minimumElement = currentRow[0];
    int insertCost, deleteCost, replaceCost;

    for (int i = 1; i < size; i++) {

        insertCost = currentRow[i - 1] + 1;
        deleteCost = previousRow[i] + 1;

        if (Word.get(i - 1) == letter) {
            replaceCost = previousRow[i - 1];
        } else {
            replaceCost = previousRow[i - 1] + 1;
        }

        currentRow[i] = minimum(insertCost, deleteCost, replaceCost);

        if (currentRow[i] < minimumElement) {
            minimumElement = currentRow[i];
        }
    }

    if (currentRow[size - 1] < theTrie.minLevDist && node.isWord) {
        theTrie.minLevDist = currentRow[size - 1];
    }

    if (minimumElement < theTrie.minLevDist) {

        for (Character c : node.children.keySet()) {
            traverseTrie(node.children.get(c), c, Word, currentRow);
        }
    }
}

更新2

最後に、ほとんどのテストケースでこれを機能させることができました。私の実装は、実際には MuriloのC++バージョン から Steve Hanovのアルゴリズム への直接翻訳です。では、このアルゴリズムをどのようにリファクタリングしたり、最適化したりする必要がありますか?以下はコードです...

public int search(String Word) {

    theTrie.minLevDist = Integer.MAX_VALUE;

    int size = Word.length();
    int[] currentRow = new int[size + 1];

    for (int i = 0; i <= size; i++) {
        currentRow[i] = i;
    }
    for (int i = 0; i < size; i++) {
        char c = Word.charAt(i);
        if (theTrie.root.children.containsKey(c)) {
            searchRec(theTrie.root.children.get(c), c, Word, currentRow);
        }
    }
    return theTrie.minLevDist;
}
private void searchRec(TrieNode node, char letter, String Word, int[] previousRow) {

    int size = previousRow.length;
    int[] currentRow = new int[size];
    currentRow[0] = previousRow[0] + 1;

    int insertCost, deleteCost, replaceCost;

    for (int i = 1; i < size; i++) {

        insertCost = currentRow[i - 1] + 1;
        deleteCost = previousRow[i] + 1;

        if (Word.charAt(i - 1) == letter) {
            replaceCost = previousRow[i - 1];
        } else {
            replaceCost = previousRow[i - 1] + 1;
        }
        currentRow[i] = minimum(insertCost, deleteCost, replaceCost);
    }

    if (currentRow[size - 1] < theTrie.minLevDist && node.isWord) {
        theTrie.minLevDist = currentRow[size - 1];
    }

    if (minElement(currentRow) < theTrie.minLevDist) {

        for (Character c : node.children.keySet()) {
            searchRec(node.children.get(c), c, Word, currentRow);

        }
    }
}

この質問に貢献してくれたすべての人に感謝します。 Levenshtein Automataを動作させようとしましたが、実行できませんでした。

したがって、上記のコードに関するリファクタリングや最適化に関する提案を探しています。混乱があったら教えてください。いつものように、必要に応じて残りのソースコードを提供できます。


更新1

簡単なTrieデータ構造を実装し、スティーブハノフのpythonチュートリアルでレーベンシュタイン距離を計算するためのチュートリアルに従っています。実際、私はの計算に興味があります。 minimum Levenshtein与えられた単語とTrie内の単語の間の距離、したがって私は次のようにしています MuriloVasconcelosのSteveHanovのアルゴリズムのバージョン 。うまく機能していませんが、これが私のTrieクラスです。

public class Trie {

    public TrieNode root;
    public int minLevDist;

    public Trie() {
        this.root = new TrieNode(' ');
    }

    public void insert(String Word) {

        int length = Word.length();
        TrieNode current = this.root;

        if (length == 0) {
            current.isWord = true;
        }
        for (int index = 0; index < length; index++) {

            char letter = Word.charAt(index);
            TrieNode child = current.getChild(letter);

            if (child != null) {
                current = child;
            } else {
                current.children.put(letter, new TrieNode(letter));
                current = current.getChild(letter);
            }
            if (index == length - 1) {
                current.isWord = true;
            }
        }
    }
}

...およびTrieNodeクラス:

public class TrieNode {

    public final int ALPHABET = 26;

    public char letter;
    public boolean isWord;
    public Map<Character, TrieNode> children;

    public TrieNode(char letter) {
        this.isWord = false;
        this.letter = letter;
        children = new HashMap<Character, TrieNode>(ALPHABET);
    }

    public TrieNode getChild(char letter) {

        if (children != null) {
            if (children.containsKey(letter)) {
                return children.get(letter); 
            }
        }
        return null;
    }
}

今、私は検索を実装しようとしました Murilo Vasconcelos がそれを持っていますが、何かがおかしいので、これをデバッグするのに助けが必要です。これをリファクタリングする方法や、バグがどこにあるかを指摘する方法について提案してください。私が最初にリファクタリングしたいのは「minCost」グローバル変数ですが、それは最小のものです。とにかく、ここにコードがあります...

public void search(String Word) {

    int size = Word.length();
    int[] currentRow = new int[size + 1];

    for (int i = 0; i <= size; i++) {
        currentRow[i] = i;
    }
    for (int i = 0; i < size; i++) {
        char c = Word.charAt(i);
        if (theTrie.root.children.containsKey(c)) {
            searchRec(theTrie.root.children.get(c), c, Word, currentRow);
        }
    }
}

private void searchRec(TrieNode node, char letter, String Word, int[] previousRow) {

    int size = previousRow.length;
    int[] currentRow = new int[size];
    currentRow[0] = previousRow[0] + 1;

    int replace, insertCost, deleteCost;

    for (int i = 1; i < size; i++) {

        char c = Word.charAt(i - 1);

        insertCost = currentRow[i - 1] + 1;
        deleteCost = previousRow[i] + 1;
        replace = (c == letter) ? previousRow[i - 1] : (previousRow[i - 1] + 1);

        currentRow[i] = minimum(insertCost, deleteCost, replace);
    }

    if (currentRow[size - 1] < minCost && !node.isWord) {
        minCost = currentRow[size - 1];
    }
    Integer minElement = minElement(currentRow);
    if (minElement < minCost) {

        for (Map.Entry<Character, TrieNode> entry : node.children.entrySet()) {
            searchRec(node, entry.getKey(), Word, currentRow);
        }
    }
}

コメントが不足していることをお詫び申し上げます。だから私は何が間違っているのですか?

初期投稿

2つの文字列間の レーベンシュタイン距離 を計算する効率的な方法を理解することを期待して、記事を読んでいます トライを使用した高速で簡単なレーベンシュタイン距離 。これに関する私の主な目標は、大量の単語セットが与えられた場合、入力単語とこの単語セットの間の最小レーベンシュタイン距離を見つけられるようにすることです。

私の簡単な実装では、入力単語ごとに、入力単語と単語のセットの間のレーベンシュタイン距離を計算し、最小値を返します。それは動作しますが、効率的ではありません...

JavaでのTrieの実装を探していましたが、一見良いと思われる2つのソースを見つけました。

ただし、これらの実装は、私がやろうとしていることには複雑すぎるようです。それらがどのように機能し、Trieデータ構造が一般的にどのように機能するかを理解するためにそれらを読んでいると、私はさらに混乱するようになりました。

では、Javaで単純なTrieデータ構造をどのように実装するのでしょうか。私の直感は、各TrieNodeはそれが表す文字列と、必ずしもすべての文字ではなく、アルファベットの文字への参照も格納する必要があることを教えてくれます。私の直感は正しいですか?

それが実装されたら、次のタスクはレーベンシュタイン距離を計算することです。上記の記事のPythonコード例を読みましたが、Pythonは話せません。また、Javaの実装では、ヒットするとヒープメモリが不足します。再帰検索では、Trieデータ構造を使用してLevenshtein距離をどのように計算しますか? このソースコード をモデルにした簡単な実装がありますが、Trieを使用していません...それは非効率的な。

あなたのコメントや提案に加えて、いくつかのコードを見るのは本当に素晴らしいことです。結局のところ、これは私にとっての学習プロセスです...私はTrieを実装したことがありません...したがって、この経験から学ぶことがたくさんあります。

ありがとう。

p.s.必要に応じて、ソースコードを提供できます。また、 Nick Johnsonのブログ で提案されているようにBK-Treeを読み通して使用してみましたが、思ったほど効率的ではありません...または実装が間違っている可能性があります。

38
Hristo

「トライを使用した高速で簡単なレーベンシュタイン距離」の記事で説明されているアルゴリズムをC++で実装しましたが、非常に高速です。必要に応じて(PythonよりもC++をよく理解している)、コードをどこかに貼り付けることができます。

編集:私は blog に投稿しました。

9

レーベンシュタイン距離の効率を改善する必要はないと私が言えることから、距離計算を何度も実行する必要がない構造に文字列を格納する必要があります。つまり、検索スペースを削除します。

レーベンシュタイン距離は距離であるため、三角不等式を利用する任意の距離空間インデックスを使用できます。BKツリーについて説明しましたが、他にもあります。ヴァンテージポイントツリー、固定クエリツリー、二等分ツリー、空間近似ツリー。説明は次のとおりです。

Burkhard-Keller Tree

ノードは次のようにツリーに挿入されます。ルートノードの場合は、スペースから任意の要素を選択します。各Edgeの値がピボットからその要素までの距離になるように、一意のEdgeラベル付きの子を追加します。再帰的に適用し、エッジがすでに存在する場合は子をピボットとして選択します。

修正済み-クエリツリー

BKTと同様ですが、次の点が異なります。要素はリーフに格納されます。各葉には複数の要素があります。ツリーの各レベルで同じピボットが使用されます。

二等分線

各ノードには、カバー半径(中心要素とそのサブツリー要素のいずれかとの間の最大距離)を持つ2つのピボット要素が含まれています。最初のピボットに最も近い要素と2番目のピボットに最も近い要素を2つのセットにフィルタリングし、これらのセットから2つのサブツリーを再帰的に構築します。

空間近似ツリー

最初はすべての要素がバッグに入っています。ピボットとなる任意の要素を選択します。ピボットの範囲内で最近傍のコレクションを作成します。残りの各要素を、作成したばかりのコレクションからそれに最も近い要素のバッグに入れます。このコレクションの各要素からサブツリーを再帰的に形成します。

Vantage Point Tree

セットからピボットを任意に選択します。このピボットと残りのセットの各要素との間の距離の中央値を計算します。セットの要素を左と右の再帰サブツリーにフィルターし、中央値以下の距離を持つ要素が左を形成し、それより大きい要素が右を形成するようにします。

10
Robert

これは JavaのLevenshtein Automata (編集: github に移動)の例です。これらもおそらく役に立ちます:

http://svn.Apache.org/repos/asf/lucene/dev/trunk/lucene/src/Java/org/Apache/lucene/util/automaton/http:/ /svn.Apache.org/repos/asf/lucene/dev/trunk/lucene/src/test/org/Apache/lucene/util/automaton/

編集:上記のリンクはgithubに移動したようです:

https://github.com/Apache/lucene-solr/tree/master/lucene/core/src/Java/org/Apache/lucene/util/automatonhttps:// github.com/Apache/lucene-solr/tree/master/lucene/core/src/test/org/Apache/lucene/util/automaton

実験的なLuceneコードは dk.brics.automaton パッケージに基づいているようです。

使用法は以下のようなものです。

LevenshteinAutomata builder = new LevenshteinAutomata(s);
Automaton automata = builder.toAutomaton(n);
boolean result1 = BasicOperations.run(automata, "foo");
boolean result2 = BasicOperations.run(automata, "bar");
3
Taylor Leese

多くの点で、スティーブハノフのアルゴリズム(質問にリンクされている最初の記事 トライを使用した高速で簡単なレーベンシュタイン距離 )、Muriloとあなたが作成したアルゴリズムのポート(OP)、およびTrieまたは同様の構造を含むすべての関連アルゴリズムは、Levenshtein Automaton(ここで数回言及されています)のように機能します。

Given:
       dict is a dictionary represented as a DFA (ex. trie or dawg)
       dictState is a state in dict
       dictStartState is the start state in dict
       dictAcceptState is a dictState arrived at after following the transitions defined by a Word in dict
       editDistance is an edit distance
       laWord is a Word
       la is a Levenshtein Automaton defined for laWord and editDistance
       laState is a state in la
       laStartState is the start state in la
       laAcceptState is a laState arrived at after following the transitions defined by a Word that is within editDistance of laWord
       charSequence is a sequence of chars
       traversalDataStack is a stack of (dictState, laState, charSequence) tuples

Define dictState as dictStartState
Define laState as laStartState
Push (dictState, laState, "") on to traversalDataStack
While traversalDataStack is not empty
    Define currentTraversalDataTuple as the the product of a pop of traversalDataStack
    Define currentDictState as the dictState in currentTraversalDataTuple
    Define currentLAState as the laState in currentTraversalDataTuple
    Define currentCharSequence as the charSequence in currentTraversalDataTuple
    For each char in alphabet
        Check if currentDictState has outgoing transition labeled by char
        Check if currentLAState has outgoing transition labeled by char
        If both currentDictState and currentLAState have outgoing transitions labeled by char
            Define newDictState as the state arrived at after following the outgoing transition of dictState labeled by char
            Define newLAState as the state arrived at after following the outgoing transition of laState labeled by char
            Define newCharSequence as concatenation of currentCharSequence and char
            Push (newDictState, newLAState, newCharSequence) on to currentTraversalDataTuple
            If newDictState is a dictAcceptState, and if newLAState is a laAcceptState
                Add newCharSequence to resultSet
            endIf
        endIf
    endFor
endWhile

スティーブハノフのアルゴリズムとその前述の派生物は、明らかに、正式なレーベンシュタインオートマトンの代わりにレーベンシュタイン距離計算行列を使用します。 かなり高速ですが、正式なLevenshtein Automatonはパラメトリック状態(オートマトンの具体的な状態を表す抽象的な状態)生成され、トラバーサルに使用され、編集距離に関連するランタイム計算をすべてバイパスします。したがって、前述のアルゴリズムよりもさらに高速に実行する必要があります。

あなた(または他の誰か)が正式なレーベンシュタインオートマトンソリューションに興味がある場合は、 LevenshteinAutomaton をご覧ください。これは、前述のパラメトリック状態ベースのアルゴリズム、および純粋な具象状態トラバーサルベースのアルゴリズム(上記で概説)と動的プログラミングベースのアルゴリズム(編集距離と近傍決定の両方)を実装しています。それは本当にあなたによって維持されています:)。

2
Kevin

誰かがこの問題のさらに別の治療法を探している場合に備えて、ここにはそのままにしておきます。

http://code.google.com/p/oracleofwoodyallen/wiki/ApproximateStringMatching

1
spieden

私はあなたの最新のアップデート3を見ていましたが、アルゴリズムは私にはうまくいかないようです。

以下のテストケースがあることを確認しましょう。

    Trie dict = new Trie();
    dict.insert("arb");
    dict.insert("area");

    ArrayList<Character> Word = new ArrayList<Character>();
    Word.add('a');
    Word.add('r');
    Word.add('c');

この場合、"arc"とdictの間の最小編集距離は1である必要があります。これは、"arc""arb"の間の編集距離ですが、アルゴリズムは代わりに2を返します。

私は以下のコードピースを通過しました:

        if (Word.get(i - 1) == letter) {
            replaceCost = previousRow[i - 1];
        } else {
            replaceCost = previousRow[i - 1] + 1;
        }

少なくとも最初のループでは、文字はWordの文字の1つですが、代わりにトライのノードを比較する必要があるため、Wordの最初の文字と1行が重複しています。各DPマトリックスには、重複として最初の行があります。私はあなたがソリューションに置いたのとまったく同じコードを実行しました。

1
zdlgrj

私がそれを正しく見ているように、あなたはトライのすべての枝をループしたいと思います。再帰関数を使用することはそれほど難しくありません。同じ種類の関数を使用して、k最近傍アルゴリズムでもトライを使用しています。 Javaはわかりませんが、次のような擬似コードがあります。

function walk (testitem trie)
   make an empty array results
   function compare (testitem children distance)
     if testitem = None
        place the distance and children into results
     else compare(testitem from second position, 
                  the sub-children of the first child in children,
                  if the first item of testitem is equal to that 
                  of the node of the first child of children 
                  add one to the distance (! non-destructive)
                  else just the distance)
        when there are any children left
             compare (testitem, the children without the first item,
                      distance)
    compare(testitem, children of root-node in trie, distance set to 0)
    return the results

それが役に立てば幸い。

1
Folgert

関数walkは、テスト項目(たとえば、索引付け可能な文字列、または文字の配列)とトライを取ります。トライは、2つのスロットを持つオブジェクトにすることができます。 1つはトライのノードを指定し、もう1つはそのノードの子を指定します。子供たちも試してみます。 pythonでは、次のようになります。

class Trie(object):
    def __init__(self, node=None, children=[]):
        self.node = node
        self.children = children

またはLISPで...

(defstruct trie (node nil) (children nil))

これで、トライは次のようになります。

(trie #node None
      #children ((trie #node f
                       #children ((trie #node o
                                        #children ((trie #node o
                                                         #children None)))
                                  (trie #node u
                                        #children ((trie #node n
                                                         #children None)))))))

これで、内部関数(個別に作成することもできます)は、testitem、ツリーのルートノードの子(ノード値はNoneなど)を取得し、初期距離を0に設定します。

次に、ツリーの両方のブランチを、左から右に再帰的にトラバースします。

1
Folgert

私の直感によると、各TrieNodeは、それが表す文字列と、必ずしもすべての文字ではなく、アルファベットの文字への参照を格納する必要があります。私の直感は正しいですか?

いいえ、トライは文字列を表すのではなく、一連の文字列(およびそのすべての接頭辞)を表します。トライノードは、入力文字を別のトライノードにマップします。したがって、文字の配列とそれに対応するTrieNode参照の配列のようなものを保持する必要があります。 (特定の使用効率によっては、正確な表現ではない場合があります。)

1
Darius Bacon

さて、 これが私がやった方法です ずっと前に。辞書はトライとして格納しました。これは、ツリーの形に制限された有限状態マシンです。その制限をしないことでそれを強化することができます。たとえば、一般的なサフィックスは単に共有サブツリーにすることができます。 「国」、「国」、「国有化」、「国有化」のようなものをキャプチャするためにループを持つこともできます...

トライはできるだけシンプルにしてください。文字列を詰め込まないでください。

与えられた2つの弦の間の距離を見つけるためにこれを行わないことを忘れないでください。これを使用して、特定の1つの文字列に最も近い辞書内の文字列を検索します。所要時間は、許容できるレーベンシュタイン距離によって異なります。距離がゼロの場合、これは単にO(n)ここでnは単語の長さです。任意の距離の場合は、O(N)ここでNは辞書の単語数。

0
Mike Dunlavey

私が間違っている場合は訂正してください。ただし、update3には不要な余分なループがあり、プログラムが大幅に遅くなると思います。

for (int i = 0; i < iWordLength; i++) {
    traverseTrie(theTrie.root, Word.get(i), Word, currentRow);
}

TraverseTrie内では、すでにWord全体をループしているため、traverseTrieを1回だけ呼び出す必要があります。コードは次のようにする必要があります。

traverseTrie(theTrie.root, ' ', Word, currentRow);
0
user4980248