Comment calculer la mesure de similitude de distance de 2 chaînes données?

J'ai besoin de calculer la similitude entre 2 chaînes. Qu'est-ce exactement que je veux dire? Permettez - moi d'expliquer avec un exemple:

  • Le vrai mot: hospital
  • mot erroné: haspita

Maintenant, mon but est de déterminer combien de caractères j'ai besoin de modifier le mot erroné pour obtenir le vrai mot. Dans cet exemple, je dois modifier 2 lettres. Alors, quel serait le pourcentage? Je prends toujours la longueur du vrai mot. Donc, il devient 2 / 8 = 25% donc ces 2 chaîne donnée DSM est 75%.

Comment puis-je y parvenir avec la performance comme considération clé?

49
demandé sur Andez 2012-02-26 18:05:46

7 réponses

Ce que vous cherchez est appelé modifier à distance ou Levenshtein. L'article wikipedia explique comment il est calculé, et a un joli morceau de pseudocode en bas pour vous aider à coder cet algorithme en C# très facilement.

Voici une implémentation du premier site lié ci-dessous:

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];
    }
42
répondu dasblinkenlight 2018-09-03 10:14:57

Je viens d'aborder exactement le même problème il y a quelques semaines. Puisque quelqu'un Demande Maintenant, je vais partager le code. Dans mes tests exhaustifs, mon code est environ 10 fois plus rapide que l'exemple C# sur Wikipedia même lorsqu'aucune distance maximale n'est fournie. Lorsqu'une distance maximale est fournie, ce gain de performance augmente à 30x-100x +. Notez quelques points clés pour la performance:

  • si vous devez comparer les mêmes mots encore et encore, convertissez d'abord les mots en tableaux d'entiers. Le L'algorithme de Damerau-Levenshtein inclut de nombreuses comparaisons >, ints comparent beaucoup plus rapidement que chars.
  • Il comprend un mécanisme de court-circuit pour arrêter si la distance dépasse un maximum fourni
  • utilisez un ensemble rotatif de trois tableaux plutôt qu'une matrice massive comme dans toutes les implémentations que j'ai vues ailleurs
  • Assurez-vous que vos tableaux découpent la largeur de mot la plus courte.

Code (cela fonctionne exactement de la même manière si vous remplacez int[] par String dans le déclarations de paramètres:

/// <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 est:

static void Swap<T>(ref T arg1,ref T arg2) {
    T temp = arg1;
    arg1 = arg2;
    arg2 = temp;
}
63
répondu Joshua Honig 2015-12-18 10:53:28

Il existe un grand nombre d'algorithmes de distance de similarité de chaîne qui peuvent être utilisés. Certains énumérés ici (mais pas exhaustivement énumérés sont):

Un la bibliothèque qui contient l'implémentation de tous ces éléments est appelée SimMetrics qui a à la fois java et c # implémentations.

33
répondu Anastasiosyal 2012-02-26 14:26:25

J'ai trouvé que Levenshtein et Jaro-Winkler sont excellents pour les petites différences entre les chaînes telles que:

  • fautes d'Orthographe; ou
  • au lieu de o dans un personnes nom.

Cependant, en comparant quelque chose comme les titres d'articles où des morceaux significatifs du texte seraient les mêmes mais avec du "bruit" sur les bords, Smith-Waterman-Gotoh a été fantastique:

Comparez ces 2 titres (qui sont le même mais formulé différemment de différentes sources):

Une endonucléase d'Escherichia coli qui introduit des scissions de chaînes polynucléotidiques uniques dans L'ADN irradié aux ultraviolets

Endonucléase III: une endonucléase d'Escherichia coli qui introduit des Scissions de chaînes polynucléotidiques uniques dans L'ADN irradié aux ultraviolets

Ce site fournit une comparaison d'algorithme des chaînes montre:

  • Levenshtein: 81
  • Smith-Waterman Gotoh 94
  • Jaro Winkler 78

Jaro Winkler et Levenshtein ne sont pas aussi compétents que Smith Waterman Gotoh pour détecter la similitude. Si nous comparons deux titres qui ne sont pas le même article, mais qui ont du texte correspondant:

Métabolisme des graisses dans les plantes supérieures. la fonction des acyl thioestérases dans le métabolisme des acyl-coenzymes A et protéine porteuse d'acyle-acyle s

Métabolisme des graisses dans les plantes supérieures. la détermination de protéine porteuse acyle-acyle et coenzyme acyle {[11] } A dans un mélange lipidique complexe

Jaro Winkler donne un faux positif, mais Smith Waterman Gotoh ne le fait pas:

  • Levenshtein: 54
  • Smith-Waterman Gotoh 49
  • Jaro Winkler 89

Commeanastasiosyal a souligné, SimMetrics a le code java pour ces algorithmes. J'ai eu du succès en utilisant le code java SmithWatermanGotoh de SimMetrics .

12
répondu joshweir 2017-05-23 12:10:11

Voici une approche alternative:

, C'est trop long pour un commentaire.

Une méthode typique pour trouver la similarité est la distance de Levenshtein, et il n'y a aucun doute une bibliothèque avec du code disponible.

Malheureusement, cela nécessite une comparaison avec chaque chaîne. Vous pourriez être en mesure d'écrire une version spécialisée du code pour court-circuiter le calcul si la distance est supérieure à un certain seuil, vous devrez toujours faire toutes les comparaisons.

Une autre idée est pour utiliser une variante de trigrammes ou n-grammes. Ce sont des séquences de n caractères (ou n mots ou n séquences génomiques ou n quoi que ce soit). Gardez un mappage des trigrammes aux chaînes et choisissez ceux qui ont le plus grand chevauchement. Un choix typique de n est "3", d'où le nom.

Par exemple, l'anglais aurait ces trigrammes:

  • Fra
  • ngl
  • gli
  • lis
  • ish

Et L'Angleterre avoir:

  • Fra
  • ngl
  • gla
  • lan
  • et

Eh bien, 2 sur 7 (ou 4 sur 10) correspondent. Si cela fonctionne pour vous, et vous pouvez indexer la table trigramme/chaîne et obtenir une recherche plus rapide.

Vous pouvez également combiner cela avec Levenshtein pour réduire l'ensemble de comparaison à ceux qui ont un nombre minimum de n-grammes en commun.

4
répondu Gordon Linoff 2014-12-06 23:31:05

Voici mon implémentation de Damerau Levenshtein Distance, qui renvoie non seulement le coefficient de similarité, mais renvoie également les emplacements d'erreur dans Word corrigé (cette fonctionnalité peut être utilisée dans les éditeurs de texte). Aussi mon implémentation prend en charge différents poids d'erreurs (substitution, suppression, insertion, transposition).

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
}

CE code fait partie de mon projet: Yandex-Linguistics.NET .

J'ai écrit certains tests et il me semble que la méthode est travailler.

Mais les commentaires et remarques sont les bienvenus.

4
répondu Ivan Kochurkin 2015-02-18 14:13:55

Voici un VB.net mise en œuvre:

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
répondu GHC 2016-04-13 07:30:07