Javaでレーベンシュタイン距離を計算する方法?

Javaでレーベンシュタイン距離を計算する方法は?

1. 前書き

この記事では、編集距離とも呼ばれるレーベンシュタイン距離について説明します。 ここで説明するアルゴリズムは、1965年にロシアの科学者ウラジミールレーベンシュタインによって考案されました。

このアルゴリズムの反復的かつ再帰的なJava実装を提供します。

2. レーベンシュタイン距離とは何ですか?

レーベンシュタイン距離は、2つのStrings.間の非類似度の尺度です。数学的には、2つのStringsxyが与えられると、距離はxyに変換します。

通常、次の3種類の編集が許可されます。

  1. 文字の挿入c

  2. 文字の削除c

  3. 文字cc ‘への置換

例:If x = ‘shot' and y = ‘spot', the edit distance between the two is 1 because ‘shot' can be converted to ‘spot' by substituting ‘h‘ to ‘p‘.

問題の特定のサブクラスでは、各タイプの編集に関連するコストが異なる場合があります。

たとえば、キーボードの近くにいるキャラクターとの置換のコストが低く、それ以外の場合はコストが高くなります。 簡単にするために、この記事ではすべてのコストが等しいと見なします。

編集距離のアプリケーションの一部は次のとおりです。

  1. スペルチェッカー-テキストのスペルミスを検出し、辞書で最も近い正しいスペルを見つけます

  2. 盗用検出(参照–IEEE Paper

  3. DNA分析– 2つの配列間の類似性を見つける

  4. 音声認識(参照–Microsoft Research

3. アルゴリズムの定式化

それぞれ長さmnの2つのStrings xyを取り上げましょう。 各Stringx[1:m]およびy[1:n].と表すことができます

変換の最後に、両方のStringsが同じ長さになり、各位置に一致する文字が含まれることがわかっています。 したがって、各String,の最初の文字を検討すると、次の3つのオプションがあります。

  1. 代用:

    1. x[1]y[1]に置き換えるコスト(D1)を決定します。 両方の文字が同じ場合、このステップのコストはゼロになります。 そうでない場合、コストは1つになります

    2. ステップ1.1の後、両方のStringsが同じ文字で始まることがわかります。 したがって、総コストは、ステップ1.1のコストと残りのString x[2:m]y[2:n]に変換するコストの合計になります。

  2. 挿入:

    1. yの最初の文字と一致するように、xに文字を挿入します。このステップのコストは1になります。

    2. 2.1以降、yから1文字を処理しました。 したがって、総コストは、ステップ2.1(つまり、1)のコストと、完全なx[1:m]を残りのy (y[2:n])に変換するコストの合計になります。

  3. 削除:

    1. xから最初の文字を削除します。このステップのコストは1になります

    2. 3.1以降、xから1文字を処理しましたが、完全なyはまだ処理されていません。 総コストは、3.1(つまり、1)のコストと、残りのxを完全なyに変換するコストの合計になります。

ソリューションの次の部分は、これらの3つから選択するオプションを見つけることです。 最終的にどのオプションが最小コストにつながるかはわからないため、すべてのオプションを試して最適なものを選択する必要があります。

4. 素朴な再帰的な実装

セクション#3の各オプションの2番目のステップは、ほとんど同じ編集距離の問題ですが、元のStrings.のサブ文字列にあることがわかります。これは、各反復後に同じ問題が発生するが、%が小さくなることを意味します。 (t1)s

この観察は、再帰アルゴリズムを定式化するための鍵です。 繰り返し関係は次のように定義できます。

D(x [1:m]、y [1:n]) =分\ {

D(x [2:m]、y [2:n])+ x [1]をy [1]に置き換えるコスト、

D(x [1:m]、y [2:n])+ 1、

D(x [2:m]、y [1:n])+ 1

}

また、再帰的アルゴリズムの基本ケースを定義する必要があります。この場合、Stringsの一方または両方が空になったときです。

  1. 両方のStringsが空の場合、それらの間の距離はゼロです。

  2. Stringsの1つが空の場合、一方を他方に変換するには多数の挿入/削除が必要になるため、それらの間の編集距離は他のString,の長さになります。

    • 例:1つのString“dog”で、他のString“”(空)の場合、“dog”にするために空のStringに3つの挿入が必要です)s、または空にするために“dog”で3つの削除が必要です。 したがって、それらの間の編集距離は3です

このアルゴリズムの単純な再帰的実装:

public class EditDistanceRecursive {

   static int calculate(String x, String y) {
        if (x.isEmpty()) {
            return y.length();
        }

        if (y.isEmpty()) {
            return x.length();
        }

        int substitution = calculate(x.substring(1), y.substring(1))
         + costOfSubstitution(x.charAt(0), y.charAt(0));
        int insertion = calculate(x, y.substring(1)) + 1;
        int deletion = calculate(x.substring(1), y) + 1;

        return min(substitution, insertion, deletion);
    }

    public static int costOfSubstitution(char a, char b) {
        return a == b ? 0 : 1;
    }

    public static int min(int... numbers) {
        return Arrays.stream(numbers)
          .min().orElse(Integer.MAX_VALUE);
    }
}

This algorithm has the exponential complexity.各ステップで、3つの再帰呼び出しに分岐し、O(3^n)の複雑さを構築します。

次のセクションでは、これを改善する方法を説明します。

5. 動的計画法アプローチ

再帰呼び出しを分析すると、サブ問題の引数は元のStrings.のサフィックスであることがわかります。これは、m*nの一意の再帰呼び出し(mnのみが存在できることを意味します。 )sは、xおよびy)のサフィックスの数です。 したがって、最適解の複雑さは2次、O(m*n)である必要があります。

いくつかの副次的な問題を見てみましょう(セクション#4で定義された繰り返しの関係による):

  1. D(x[1:m], y[1:n])のサブ問題はD(x[2:m], y[2:n]), D(x[1:m], y[2:n])D(x[2:m], y[1:n])です

  2. D(x[1:m], y[2:n])のサブ問題はD(x[2:m], y[3:n]), D(x[1:m], y[3:n])D(x[2:m], y[2:n])です

  3. D(x[2:m], y[1:n])のサブ問題はD(x[3:m], y[2:n]), D(x[2:m], y[2:n])D(x[3:m], y[1:n])です

3つのケースすべてで、サブ問題の1つはD(x[2:m], y[2:n]).です。単純な実装のようにこれを3回計算する代わりに、これを1回計算して、必要に応じて結果を再利用できます。

この問題には多くの重複する副問題がありますが、副問題の解決策がわかれば、元の問題の答えを簡単に見つけることができます。 したがって、動的計画法ソリューションの定式化に必要な両方のプロパティ、つまりOverlapping Sub-ProblemsOptimal Substructureがあります。

memoizationを導入することで、単純な実装を最適化できます。つまり、サブ問題の結果を配列に格納し、キャッシュされた結果を再利用できます。

あるいは、テーブルベースのアプローチを使用して、これを繰り返し実装することもできます。

static int calculate(String x, String y) {
    int[][] dp = new int[x.length() + 1][y.length() + 1];

    for (int i = 0; i <= x.length(); i++) {
        for (int j = 0; j <= y.length(); j++) {
            if (i == 0) {
                dp[i][j] = j;
            }
            else if (j == 0) {
                dp[i][j] = i;
            }
            else {
                dp[i][j] = min(dp[i - 1][j - 1]
                 + costOfSubstitution(x.charAt(i - 1), y.charAt(j - 1)),
                  dp[i - 1][j] + 1,
                  dp[i][j - 1] + 1);
            }
        }
    }

    return dp[x.length()][y.length()];
}

このアルゴリズムは、再帰的な実装よりも大幅に優れたパフォーマンスを発揮します。 ただし、これにはかなりのメモリ消費が伴います。

これは、現在のセルの値を見つけるためにテーブル内の3つの隣接するセルの値のみが必要であることを観察することにより、さらに最適化できます。

6. 結論

この記事では、レーベンシュタイン距離とは何か、再帰的および動的プログラミングベースのアプローチを使用して距離を計算する方法について説明しました。

レーベンシュタイン距離は文字列の類似性の尺度の1つにすぎず、他のメトリックのいくつかはCosine Similarity(トークンベースのアプローチを使用し、文字列をベクトルと見なします)、Dice Coefficientなどです。

いつものように、例の完全な実装はover on GitHubで見つけることができます。