Как рассчитать меру подобия расстояния для заданных 2 строк? - PullRequest
56 голосов
/ 26 февраля 2012

Мне нужно вычислить сходство между 2 строками.Так что именно я имею в виду?Позвольте мне объяснить на примере:

  • Настоящее слово: hospital
  • Ошибочное слово: haspita

Теперь моя цель - определитьсколько символов мне нужно изменить ошибочное слово, чтобы получить реальное слово.В этом примере мне нужно изменить 2 буквы.Так какой будет процент?Я всегда беру длину настоящего слова.Таким образом, оно становится равным 2/8 = 25%, поэтому эти два заданных строковых значения DSM составляют 75%.

Как я могу добиться этого, когда производительность является ключевым фактором?

Ответы [ 7 ]

66 голосов
/ 26 февраля 2012

Я только что обратился к той же самой проблеме несколько недель назад. Так как кто-то спрашивает сейчас, я поделюсь кодом. В моих исчерпывающих тестах мой код примерно в 10 раз быстрее, чем пример C # в Википедии, даже если не указано максимальное расстояние. Когда предоставляется максимальное расстояние, это увеличение производительности увеличивается до 30x - 100x +. Обратите внимание на пару ключевых моментов для производительности:

  • Если вам нужно сравнивать одни и те же слова снова и снова, сначала преобразуйте слова в массивы целых чисел. Алгоритм Дамерау-Левенштейна включает в себя множество сравнений>, <, == и <code>ints сравнение намного быстрее, чем chars.
  • Включает механизм короткого замыкания для выхода, если расстояние превышает заданный максимум
  • Используйте вращающийся набор из трех массивов, а не массивную матрицу, как во всех реализациях, которые я видел в других местах
  • Убедитесь, что ваши массивы срезаются по более короткой ширине слова.

Код (он работает точно так же, если вы замените 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;
}
46 голосов
/ 26 февраля 2012

То, что вы ищете, называется редактировать расстояние или расстояние Левенштейна . В статье в Википедии объясняется, как она рассчитывается, и внизу есть хороший фрагмент псевдокода, который поможет вам очень легко кодировать этот алгоритм в 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];
    }
35 голосов
/ 26 февраля 2012

Существует большое количество алгоритмов расстояния сходства строк, которые можно использовать. Некоторые из перечисленных здесь (но не исчерпывающе перечислены):

Библиотека, которая содержит реализацию всего этого, называется SimMetrics которая имеет реализации java и c #.

18 голосов
/ 07 июля 2016

Я обнаружил, что Левенштейн и Джаро Винклер отлично подходят для небольших различий между строками, таких как:

  • орфографические ошибки; или
  • ö вместо o в имени человека.

Однако при сравнении чего-то вроде заголовков статей, где значимые куски текста были бы одинаковыми, но с «шумом» по краям, Smith-Waterman-Gotoh был фантастическим:

Сравните эти 2 заголовка (которые одинаковы, но отличаются от разных источников):

эндонуклеаза из Escherichia coli, которая вводит отдельные полинуклеотидные расщепления цепи в ультрафиолетовой облученной ДНК

Эндонуклеаза III: Эндонуклеаза из Escherichia coli, которая вводит отдельные полинуклеотидные расщепления цепи в ультрафиолетовой облученной ДНК

Этот сайт, который предоставляет алгоритм сравнения строк показывает:

  • Левенштейн: 81
  • Смит-Водяной Гото 94
  • Яро Винклер 78

Яро Винклер и Левенштейн не так компетентны, как Смит Уотерман Гото, в обнаружении сходства. Если мы сравним два заголовка, которые не относятся к одной и той же статье, но имеют соответствующий текст:

Метаболизм жира у высших растений. Функция ацилтиоэстераз в метаболизме ацил-коферментов A и ацилацил-белка-носителя s

Метаболизм жира у высших растений. Определение ацилацил-белка-носителя и ацил-коэнзима A в сложной липидной смеси

Джаро Винклер дает ложный положительный результат, но Смит Водяной Гото не делает:

  • Левенштейн: 54
  • Смит-Водянин Гото 49
  • Яро Винклер 89

Как указывало Anastasiosyal , SimMetrics имеет код Java для этих алгоритмов. Мне удалось использовать Java-код SmithWatermanGotoh от SimMetrics .

6 голосов
/ 18 февраля 2015

Вот моя реализация Damerau Levenshtein Distance, которая возвращает не только коэффициент подобия, но также возвращает места ошибок в исправленном слове (эта функция может использоваться в текстовых редакторах). Также моя реализация поддерживает разные веса ошибок (подстановка, удаление, вставка, транспонирование).

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
}

Этот код является частью моего проекта: Яндекс.Лингвистика.NET .

Я написал несколько тестов и мне кажется, что метод работает.

Но комментарии и замечания приветствуются.

4 голосов
/ 07 декабря 2014

Вот альтернативный подход:

Это слишком долго для комментария.

Типичным методом нахождения сходства является расстояние Левенштейна, и нет никаких сомнений в том, что есть библиотека с доступным кодом.

К сожалению, это требует сравнения с каждой строкой.Возможно, вы сможете написать специализированную версию кода для короткого замыкания в вычислениях, если расстояние больше некоторого порога, вам все равно придется выполнять все сравнения.

Другая идея заключается в использовании некоторого варианта.триграмм или н-граммов.Это последовательности из n символов (или n слов, или n геномных последовательностей, или n независимо).Сохраняйте сопоставление триграмм со строками и выбирайте те, которые имеют наибольшее перекрытие.Типичным выбором n является «3», отсюда и название.

Например, английский может иметь следующие триграммы:

  • Eng
  • ngl
  • gli
  • lis
  • ish

И Англия будет иметь:

  • Eng
  • ngl
  • gla
  • lan
  • и

Ну, 2 из 7 (или 4 из 10) совпадают.Если это работает для вас, и вы можете проиндексировать таблицу триграмм / строк и получить более быстрый поиск.

Вы также можете объединить это с Левенштейном, чтобы сократить набор сравнений до тех, которые имеют некоторое минимальное число n-граммы общего.

0 голосов
/ 13 апреля 2016

Вот реализация 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
...