web-dev-qa-db-ja.com

与えられた2つの文字列の距離の類似性の尺度を計算する方法は?

2つの文字列の類似度を計算する必要があります。それで、私は正確に何を意味するのでしょうか?例で説明しましょう:

  • 実際の単語:hospital
  • 誤った単語:haspita

ここでの目的は、実際のWordを取得するために、誤ったWordを変更する必要がある文字数を決定することです。この例では、2文字を変更する必要があります。それで、何パーセントでしょうか?私は常に本当の言葉の長さを取ります。したがって、2/8 = 25%になるため、これらの2つの与えられたストリングDSMは75%です。

パフォーマンスを重要な考慮事項として、これをどのように達成できますか?

56
MonsterMMORPG

探しているのはedit distanceまたは Levenshtein distance 。ウィキペディアの記事では、計算方法が説明されており、このアルゴリズムをC#で非常に簡単にコーディングできるように、下部に素敵な擬似コードがあります。

以下にリンクされている最初のサイトの実装を示します。

private static int  CalcLevenshteinDistance(string a, string b)
    {
    if (String.IsNullOrEmpty(a) && String.IsNullOrEmpty(b)) {
        return 0;
    }
    if (String.IsNullOrEmpty(a)) {
        return b.Length;
    }
    if (String.IsNullOrEmpty(b)) {
        return a.Length;
    }
    int  lengthA   = a.Length;
    int  lengthB   = b.Length;
    var  distances = new int[lengthA + 1, lengthB + 1];
    for (int i = 0;  i <= lengthA;  distances[i, 0] = i++);
    for (int j = 0;  j <= lengthB;  distances[0, j] = j++);

    for (int i = 1;  i <= lengthA;  i++)
        for (int j = 1;  j <= lengthB;  j++)
            {
            int  cost = b[j - 1] == a[i - 1] ? 0 : 1;
            distances[i, j] = Math.Min
                (
                Math.Min(distances[i - 1, j] + 1, distances[i, j - 1] + 1),
                distances[i - 1, j - 1] + cost
                );
            }
    return distances[lengthA, lengthB];
    }
46
dasblinkenlight

私は数週間前にこのまったく同じ問題に対処しました。誰かが今尋ねているので、コードを共有します。徹底的なテストでは、最大距離が指定されていない場合でも、WikipediaのC#の例よりもコードが約10倍高速です。最大距離を指定すると、このパフォーマンスゲインは30x〜100x +に増加します。パフォーマンスに関するいくつかの重要な点に注意してください。

  • 同じ単語を何度も比較する必要がある場合は、最初に単語を整数の配列に変換します。 Damerau-Levenshteinアルゴリズムには、多くの>、<、==比較、およびints比較がcharsよりもはるかに高速に含まれています。
  • 距離が指定された最大値を超えた場合に終了する短絡メカニズムが含まれています
  • 他の場所で見たすべての実装のように、大規模な行列ではなく、3つの配列の回転セットを使用します
  • 配列が短いWord幅を横切っていることを確認してください。

コード(パラメーター宣言でint[]Stringに置き換えてもまったく同じように機能します。

/// <summary>
/// Computes the Damerau-Levenshtein Distance between two strings, represented as arrays of
/// integers, where each integer represents the code point of a character in the source string.
/// Includes an optional threshhold which can be used to indicate the maximum allowable distance.
/// </summary>
/// <param name="source">An array of the code points of the first string</param>
/// <param name="target">An array of the code points of the second string</param>
/// <param name="threshold">Maximum allowable distance</param>
/// <returns>Int.MaxValue if threshhold exceeded; otherwise the Damerau-Leveshteim distance between the strings</returns>
public static int DamerauLevenshteinDistance(int[] source, int[] target, int threshold) {

    int length1 = source.Length;
    int length2 = target.Length;

    // Return trivial case - difference in string lengths exceeds threshhold
    if (Math.Abs(length1 - length2) > threshold) { return int.MaxValue; }

    // Ensure arrays [i] / length1 use shorter length 
    if (length1 > length2) {
        Swap(ref target, ref source);
        Swap(ref length1, ref length2);
    }

    int maxi = length1;
    int maxj = length2;

    int[] dCurrent = new int[maxi + 1];
    int[] dMinus1 = new int[maxi + 1];
    int[] dMinus2 = new int[maxi + 1];
    int[] dSwap;

    for (int i = 0; i <= maxi; i++) { dCurrent[i] = i; }

    int jm1 = 0, im1 = 0, im2 = -1;

    for (int j = 1; j <= maxj; j++) {

        // Rotate
        dSwap = dMinus2;
        dMinus2 = dMinus1;
        dMinus1 = dCurrent;
        dCurrent = dSwap;

        // Initialize
        int minDistance = int.MaxValue;
        dCurrent[0] = j;
        im1 = 0;
        im2 = -1;

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

            int cost = source[im1] == target[jm1] ? 0 : 1;

            int del = dCurrent[im1] + 1;
            int ins = dMinus1[i] + 1;
            int sub = dMinus1[im1] + cost;

            //Fastest execution for min value of 3 integers
            int min = (del > ins) ? (ins > sub ? sub : ins) : (del > sub ? sub : del);

            if (i > 1 && j > 1 && source[im2] == target[jm1] && source[im1] == target[j - 2])
                min = Math.Min(min, dMinus2[im2] + cost);

            dCurrent[i] = min;
            if (min < minDistance) { minDistance = min; }
            im1++;
            im2++;
        }
        jm1++;
        if (minDistance > threshold) { return int.MaxValue; }
    }

    int result = dCurrent[maxi];
    return (result > threshold) ? int.MaxValue : result;
}

Swapは次のとおりです。

static void Swap<T>(ref T arg1,ref T arg2) {
    T temp = arg1;
    arg1 = arg2;
    arg2 = temp;
}
66
Joshua Honig

使用できる文字列類似距離アルゴリズムは多数あります。ここにリストされているものがあります(ただし、完全にリストされているわけではありません)。

これらすべてへの実装を含むライブラリは、 SimMetrics と呼ばれ、Javaとc#実装の両方があります。

35
Anastasiosyal

LevenshteinJaro Winkler は、次のような文字列間の小さな違いに最適です。

  • スペルミス;または
  • ö人物名にoの代わりに。

しかし、テキストのかなりの部分は同じであるが、エッジの周りに「ノイズ」がある記事タイトルのようなものを比較するとき、 Smith-Waterman-Gotoh は素晴らしいです:

これらの2つのタイトルを比較します(これらは同じですが、異なるソースから異なる言い方をしています)。

紫外線照射されたDNAに単一のポリヌクレオチド鎖切断を導入する大腸菌由来のエンドヌクレアーゼ

エンドヌクレアーゼIII:紫外線照射DNAに単一ポリヌクレオチド鎖切断を導入する大腸菌由来のエンドヌクレアーゼ

アルゴリズム比較を提供するこのサイト 文字列の:

  • レーベンシュタイン:81
  • スミス・ウォーターマン・ゴトー94
  • ヤロ・ウィンクラー78

Jaro WinklerとLevenshteinは、類似性の検出においてSmith Waterman Gotohほど有能ではありません。同じ記事ではないが、一致するテキストがある2つのタイトルを比較する場合:

高等植物の脂肪代謝アシル補酵素の代謝におけるアシルチオエステラーゼの機能Aおよびアシル-アシルキャリアタンパク質s

高等植物の脂肪代謝アシル-アシルキャリアタンパク質の決定およびアシル補酵素複雑な脂質混合物中のA

Jaro Winklerは偽陽性を示しますが、Smith Waterman Gotohはしません。

  • レーベンシュタイン:54
  • スミス・ウォーターマン・ゴトー49
  • ジャロ・ウィンクラー89

Anastasiosyal が指摘したように、 SimMetrics にはこれらのアルゴリズムのJavaコードがあります。 SmithWatermanGotoh Java SimMetricsのコード

20
joshweir

Damerau Levenshtein Distanceの実装を次に示します。これは、類似度係数だけでなく、修正されたWordのエラー位置も返します(この機能はテキストエディターで使用できます)。また、私の実装では、さまざまな重みのエラー(置換、削除、挿入、転置)をサポートしています。

public static List<Mistake> OptimalStringAlignmentDistance(
  string Word, string correctedWord,
  bool transposition = true,
  int substitutionCost = 1,
  int insertionCost = 1,
  int deletionCost = 1,
  int transpositionCost = 1)
{
    int w_length = Word.Length;
    int cw_length = correctedWord.Length;
    var d = new KeyValuePair<int, CharMistakeType>[w_length + 1, cw_length + 1];
    var result = new List<Mistake>(Math.Max(w_length, cw_length));

    if (w_length == 0)
    {
        for (int i = 0; i < cw_length; i++)
            result.Add(new Mistake(i, CharMistakeType.Insertion));
        return result;
    }

    for (int i = 0; i <= w_length; i++)
        d[i, 0] = new KeyValuePair<int, CharMistakeType>(i, CharMistakeType.None);

    for (int j = 0; j <= cw_length; j++)
        d[0, j] = new KeyValuePair<int, CharMistakeType>(j, CharMistakeType.None);

    for (int i = 1; i <= w_length; i++)
    {
        for (int j = 1; j <= cw_length; j++)
        {
            bool equal = correctedWord[j - 1] == Word[i - 1];
            int delCost = d[i - 1, j].Key + deletionCost;
            int insCost = d[i, j - 1].Key + insertionCost;
            int subCost = d[i - 1, j - 1].Key;
            if (!equal)
                subCost += substitutionCost;
            int transCost = int.MaxValue;
            if (transposition && i > 1 && j > 1 && Word[i - 1] == correctedWord[j - 2] && Word[i - 2] == correctedWord[j - 1])
            {
                transCost = d[i - 2, j - 2].Key;
                if (!equal)
                    transCost += transpositionCost;
            }

            int min = delCost;
            CharMistakeType mistakeType = CharMistakeType.Deletion;
            if (insCost < min)
            {
                min = insCost;
                mistakeType = CharMistakeType.Insertion;
            }
            if (subCost < min)
            {
                min = subCost;
                mistakeType = equal ? CharMistakeType.None : CharMistakeType.Substitution;
            }
            if (transCost < min)
            {
                min = transCost;
                mistakeType = CharMistakeType.Transposition;
            }

            d[i, j] = new KeyValuePair<int, CharMistakeType>(min, mistakeType);
        }
    }

    int w_ind = w_length;
    int cw_ind = cw_length;
    while (w_ind >= 0 && cw_ind >= 0)
    {
        switch (d[w_ind, cw_ind].Value)
        {
            case CharMistakeType.None:
                w_ind--;
                cw_ind--;
                break;
            case CharMistakeType.Substitution:
                result.Add(new Mistake(cw_ind - 1, CharMistakeType.Substitution));
                w_ind--;
                cw_ind--;
                break;
            case CharMistakeType.Deletion:
                result.Add(new Mistake(cw_ind, CharMistakeType.Deletion));
                w_ind--;
                break;
            case CharMistakeType.Insertion:
                result.Add(new Mistake(cw_ind - 1, CharMistakeType.Insertion));
                cw_ind--;
                break;
            case CharMistakeType.Transposition:
                result.Add(new Mistake(cw_ind - 2, CharMistakeType.Transposition));
                w_ind -= 2;
                cw_ind -= 2;
                break;
        }
    }
    if (d[w_length, cw_length].Key > result.Count)
    {
        int delMistakesCount = d[w_length, cw_length].Key - result.Count;
        for (int i = 0; i < delMistakesCount; i++)
            result.Add(new Mistake(0, CharMistakeType.Deletion));
    }

    result.Reverse();

    return result;
}

public struct Mistake
{
    public int Position;
    public CharMistakeType Type;

    public Mistake(int position, CharMistakeType type)
    {
        Position = position;
        Type = type;
    }

    public override string ToString()
    {
        return Position + ", " + Type;
    }
}

public enum CharMistakeType
{
    None,
    Substitution,
    Insertion,
    Deletion,
    Transposition
}

このコードは私のプロジェクトの一部です: Yandex-Linguistics.NET

tests をいくつか書いたのですが、メソッドが機能しているように思えます。

しかし、コメントや発言は大歓迎です。

6
Ivan Kochurkin

別のアプローチを次に示します。

これはコメントするには長すぎます。

類似性を見つけるための典型的な方法はレーベンシュタイン距離であり、利用可能なコードを備えたライブラリは間違いありません。

残念ながら、これにはすべての文字列との比較が必要です。距離が特定のしきい値よりも大きい場合、計算を短絡させるためにコードの特殊バージョンを作成できる場合がありますが、すべての比較を行う必要があります。

別のアイデアは、トライグラムまたはnグラムのバリアントを使用することです。これらは、n個の文字のシーケンス(またはn個の単語、n個のゲノムシーケンス、またはn個)です。文字列へのトライグラムのマッピングを保持し、最大のオーバーラップを持つものを選択します。 nの典型的な選択は「3」であるため、名前です。

たとえば、英語には次のトライグラムがあります。

  • Eng
  • ngl
  • gli
  • lis
  • hは

そして、イングランドには次のものがあります。

  • Eng
  • ngl
  • gla
  • そして

まあ、7のうち2つ(または10のうち4)が一致します。これで問題が解決し、trigram/stringテーブルにインデックスを付けて検索を高速化できます。

また、これをレーベンシュタインと組み合わせて、最小数の共通のn-gramがある比較セットを減らすことができます。

4
Gordon Linoff

VB.netの実装は次のとおりです。

Public Shared Function LevenshteinDistance(ByVal v1 As String, ByVal v2 As String) As Integer
    Dim cost(v1.Length, v2.Length) As Integer
    If v1.Length = 0 Then
        Return v2.Length                'if string 1 is empty, the number of edits will be the insertion of all characters in string 2
    ElseIf v2.Length = 0 Then
        Return v1.Length                'if string 2 is empty, the number of edits will be the insertion of all characters in string 1
    Else
        'setup the base costs for inserting the correct characters
        For v1Count As Integer = 0 To v1.Length
            cost(v1Count, 0) = v1Count
        Next v1Count
        For v2Count As Integer = 0 To v2.Length
            cost(0, v2Count) = v2Count
        Next v2Count
        'now work out the cheapest route to having the correct characters
        For v1Count As Integer = 1 To v1.Length
            For v2Count As Integer = 1 To v2.Length
                'the first min term is the cost of editing the character in place (which will be the cost-to-date or the cost-to-date + 1 (depending on whether a change is required)
                'the second min term is the cost of inserting the correct character into string 1 (cost-to-date + 1), 
                'the third min term is the cost of inserting the correct character into string 2 (cost-to-date + 1) and 
                cost(v1Count, v2Count) = Math.Min(
                    cost(v1Count - 1, v2Count - 1) + If(v1.Chars(v1Count - 1) = v2.Chars(v2Count - 1), 0, 1),
                    Math.Min(
                        cost(v1Count - 1, v2Count) + 1,
                        cost(v1Count, v2Count - 1) + 1
                    )
                )
            Next v2Count
        Next v1Count

        'the final result is the cheapest cost to get the two strings to match, which is the bottom right cell in the matrix
        'in the event of strings being equal, this will be the result of zipping diagonally down the matrix (which will be square as the strings are the same length)
        Return cost(v1.Length, v2.Length)
    End If
End Function
0
GHC