如何测量两个字符串的相似度,除了计算距离之外?(注:此处“距离”指字符串间的编辑距离)

3
我正在创建一个程序,检查单词是否为简化词(txt, msg等),如果是,则找到正确的拼写,如txt=text,msg=message。我在C#中使用NHunspell Suggest方法建议所有可能的结果。
问题是,如果输入“txt”,结果是text、tat、tot等。我不知道如何选择正确的单词。我使用了Levenshtein Distance(C# - Compare String Similarity),但结果仍然是1。
输入:txt 结果:text = 1,ext = 1,tit = 1
你能帮我获取简化词的含义或正确的拼写吗? 例如:msg

5
似乎改进它的一种方法是检查每个结果是否至少包含输入中的所有字符。 - Corak
我同意Corak的观点 - 对于你提供的例子,正确的选项是3个结果中唯一包含所有字母且按照正确顺序出现在输入中的那个,即"text"、"ext"和"tit"。 - Sam
你似乎正在尝试编写一个拼写检查器,它还可以提供建议。这不是一项简单的任务。你是在遇到编程问题,还是在想出算法方面有困难? - CodeCaster
我正在寻找一种算法来找到正确的单词,以及如何编写这个算法的程序。 - MMakati
5个回答

5

我已经使用您提供的样本数据测试了您的输入,只有 text 的距离为25,而其他的距离都为33。这里是我的代码:

string input = "TXT";
string[] words = new[]{"text","tat","tot"};
var levenshtein = new Levenshtein();  
const int maxDistance = 30;

var distanceGroups = words
        .Select(w => new
        {
            Word = w,
            Distance = levenshtein.iLD(w.ToUpperInvariant(), input)
        })
        .Where(x => x.Distance <= maxDistance)
        .GroupBy(x => x.Distance)
        .OrderBy(g => g.Key)
        .ToList();
foreach (var topCandidate in distanceGroups.First())
    Console.WriteLine("Word:{0} Distance:{1}", topCandidate.Word, topCandidate.Distance);

以下是Levenshtein类:

public class Levenshtein
{
    ///*****************************
    /// Compute Levenshtein distance 
    /// Memory efficient version
    ///*****************************
    public int iLD(String sRow, String sCol)
    {
        int RowLen = sRow.Length;  // length of sRow
        int ColLen = sCol.Length;  // length of sCol
        int RowIdx;                // iterates through sRow
        int ColIdx;                // iterates through sCol
        char Row_i;                // ith character of sRow
        char Col_j;                // jth character of sCol
        int cost;                   // cost

        /// Test string length
        if (Math.Max(sRow.Length, sCol.Length) > Math.Pow(2, 31))
            throw (new Exception("\nMaximum string length in Levenshtein.iLD is " + Math.Pow(2, 31) + ".\nYours is " + Math.Max(sRow.Length, sCol.Length) + "."));

        // Step 1

        if (RowLen == 0)
        {
            return ColLen;
        }

        if (ColLen == 0)
        {
            return RowLen;
        }

        /// Create the two vectors
        int[] v0 = new int[RowLen + 1];
        int[] v1 = new int[RowLen + 1];
        int[] vTmp;



        /// Step 2
        /// Initialize the first vector
        for (RowIdx = 1; RowIdx <= RowLen; RowIdx++)
        {
            v0[RowIdx] = RowIdx;
        }

        // Step 3

        /// Fore each column
        for (ColIdx = 1; ColIdx <= ColLen; ColIdx++)
        {
            /// Set the 0'th element to the column number
            v1[0] = ColIdx;

            Col_j = sCol[ColIdx - 1];


            // Step 4

            /// Fore each row
            for (RowIdx = 1; RowIdx <= RowLen; RowIdx++)
            {
                Row_i = sRow[RowIdx - 1];


                // Step 5

                if (Row_i == Col_j)
                {
                    cost = 0;
                }
                else
                {
                    cost = 1;
                }

                // Step 6

                /// Find minimum
                int m_min = v0[RowIdx] + 1;
                int b = v1[RowIdx - 1] + 1;
                int c = v0[RowIdx - 1] + cost;

                if (b < m_min)
                {
                    m_min = b;
                }
                if (c < m_min)
                {
                    m_min = c;
                }

                v1[RowIdx] = m_min;
            }

            /// Swap the vectors
            vTmp = v0;
            v0 = v1;
            v1 = vTmp;

        }

        // Step 7

        /// Value between 0 - 100
        /// 0==perfect match 100==totaly different
        /// 
        /// The vectors where swaped one last time at the end of the last loop,
        /// that is why the result is now in v0 rather than in v1
        //System.Console.WriteLine("iDist=" + v0[RowLen]);
        int max = System.Math.Max(RowLen, ColLen);
        return ((100 * v0[RowLen]) / max);
    }


    ///*****************************
    /// Compute the min
    ///*****************************

    private int Minimum(int a, int b, int c)
    {
        int mi = a;

        if (b < mi)
        {
            mi = b;
        }
        if (c < mi)
        {
            mi = c;
        }

        return mi;
    }

    ///*****************************
    /// Compute Levenshtein distance         
    ///*****************************
    public int LD(String sNew, String sOld)
    {
        int[,] matrix;              // matrix
        int sNewLen = sNew.Length;  // length of sNew
        int sOldLen = sOld.Length;  // length of sOld
        int sNewIdx; // iterates through sNew
        int sOldIdx; // iterates through sOld
        char sNew_i; // ith character of sNew
        char sOld_j; // jth character of sOld
        int cost; // cost

        /// Test string length
        if (Math.Max(sNew.Length, sOld.Length) > Math.Pow(2, 31))
            throw (new Exception("\nMaximum string length in Levenshtein.LD is " + Math.Pow(2, 31) + ".\nYours is " + Math.Max(sNew.Length, sOld.Length) + "."));

        // Step 1

        if (sNewLen == 0)
        {
            return sOldLen;
        }

        if (sOldLen == 0)
        {
            return sNewLen;
        }

        matrix = new int[sNewLen + 1, sOldLen + 1];

        // Step 2

        for (sNewIdx = 0; sNewIdx <= sNewLen; sNewIdx++)
        {
            matrix[sNewIdx, 0] = sNewIdx;
        }

        for (sOldIdx = 0; sOldIdx <= sOldLen; sOldIdx++)
        {
            matrix[0, sOldIdx] = sOldIdx;
        }

        // Step 3

        for (sNewIdx = 1; sNewIdx <= sNewLen; sNewIdx++)
        {
            sNew_i = sNew[sNewIdx - 1];

            // Step 4

            for (sOldIdx = 1; sOldIdx <= sOldLen; sOldIdx++)
            {
                sOld_j = sOld[sOldIdx - 1];

                // Step 5

                if (sNew_i == sOld_j)
                {
                    cost = 0;
                }
                else
                {
                    cost = 1;
                }

                // Step 6

                matrix[sNewIdx, sOldIdx] = Minimum(matrix[sNewIdx - 1, sOldIdx] + 1, matrix[sNewIdx, sOldIdx - 1] + 1, matrix[sNewIdx - 1, sOldIdx - 1] + cost);

            }
        }

        // Step 7

        /// Value between 0 - 100
        /// 0==perfect match 100==totaly different
        //System.Console.WriteLine("Dist=" + matrix[sNewLen, sOldLen]);
        int max = System.Math.Max(sNewLen, sOldLen);
        return (100 * matrix[sNewLen, sOldLen]) / max;
    }
}

+1,实现得非常优雅。告诉我,我从SQL中实现了SOUNDEX算法。这两者有什么区别?当然我可以读懂代码,但我想从你那里得到一个高层次的概述,因为显然你理解这个算法,可能也理解我实现的算法。例如,SOUNDEX例程是否存在您所知道的一些漏洞? - Mike Perrenoud
谢谢,但输入的单词必须是大写的吗?如果我改为小写,错误消息会是“序列不包含任何元素”。 - MMakati
@MMakati:那么,input.ToUpperInvariant()在开头有什么问题吗?我必须承认,我从我的代码中复制了这个,那里的大小写不重要。我不知道对你是否重要。 @Michael:实际上,这个实现来自这里。所以我不是一个Levenshtein算法专家。 - Tim Schmelter
我使用了 toUpper(),它有效了。我在想大小写是否会影响结果,但无论如何这个答案都很有帮助且可行。谢谢 @TimSchmelter。 - MMakati
@MMakati:当然,大小写会影响结果,这就是为什么我用 ToUpper 来抵消这种影响的原因。 - Tim Schmelter

0
你真的需要实现在 SQL 中存在的 SOUNDEX 程序。我已经在以下代码中完成了它:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Soundex
{
    class Program
    {
        static char[] ignoreChars = new char[] { 'a', 'e', 'h', 'i', 'o', 'u', 'w', 'y' };
        static Dictionary<char, int> charVals = new Dictionary<char, int>()
        {
            {'b',1},
            {'f',1},
            {'p',1},
            {'v',1},
            {'c',2},
            {'g',2},
            {'j',2},
            {'k',2},
            {'q',2},
            {'s',2},
            {'x',2},
            {'z',2},
            {'d',3},
            {'t',3},
            {'l',4},
            {'m',5},
            {'n',5},
            {'r',6}
        };

        static void Main(string[] args)
        {
            Console.WriteLine(Soundex("txt"));
            Console.WriteLine(Soundex("text"));
            Console.WriteLine(Soundex("ext"));
            Console.WriteLine(Soundex("tit"));
            Console.WriteLine(Soundex("Cammmppppbbbeeelll"));
        }

        static string Soundex(string s)
        {
            s = s.ToLower();
            StringBuilder sb = new StringBuilder();
            sb.Append(s.First());
            foreach (var c in s.Substring(1))
            {
                if (ignoreChars.Contains(c)) { continue; }

                // if the previous character yields the same integer then skip it
                if ((int)char.GetNumericValue(sb[sb.Length - 1]) == charVals[c]) { continue; }

                sb.Append(charVals[c]);
            }
            return string.Join("", sb.ToString().Take(4)).PadRight(4, '0');
        }
    }
}

看,使用这段代码,你给出的示例中唯一匹配的是text。运行控制台应用程序,你会看到输出结果(即txt将匹配text)。

0

这不是一个完整的解决方案,只是一个希望有所帮助的建议...

我认为人们不太可能使用与正确单词一样长的简化方式,因此您至少可以过滤掉所有长度≤输入长度的结果。


0

我认为类似 Word 的程序用于纠正拼写错误的方法之一是使用 NLP(自然语言处理)技术,以获取在拼写错误上下文中使用的名词/形容词的顺序,然后将其与已知的句子结构进行比较,他们可以估计 70% 的可能性是名词造成了拼写错误,然后利用此信息来过滤更正的拼写。

SharpNLP 看起来是一个不错的库,但我还没有机会去尝试。顺便说一下,我们在大学里将我们的算法应用于公共领域书籍,以建立已知句子结构的库。

请查看我在 SO 上发现的 sams simMetrics 库这里下载, 这里是文档),可以提供除 Levenshtein 距离之外的许多其他算法选项.


我必须尝试一下,我正在制作的程序确实是为了我的论文(关于Twitter上的情感)。我正在尝试检查推文中是否有快捷词。谢谢。 - MMakati
抱歉如果我的帖子有点混乱,但我认为这可以被视为一个拼写纠正系统。只是一个想法,您是否有办法解析这些维基页面以建立更全面/专业的字典以满足您的需求:http://en.wikipedia.org/wiki/List_of_acronyms:_A - Dead.Rabit

-1

扩展我的评论,你可以使用正则表达式搜索一个与输入“扩展”相关的结果。类似这样:

private int stringSimilarity(string input, string result)
{
    string regexPattern = ""
    foreach (char c in input)
        regexPattern += input + ".*"

    Match match = Regex.Match(result, regexPattern,
    RegexOptions.IgnoreCase);

    if (match.Success)
        return 1;
    else
        return 0;
}

忽略1和0 - 我不知道相似度评估是如何工作的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接