草庐IT

c# - 尝试优化模糊匹配

coder 2024-05-21 原文

我有 2,500,000 个产品名称,我想尝试将它们组合在一起,即找到名称相似的产品。例如,我可以拥有三种产品:

  • 亨氏焗 bean 400g;
  • Hz Bkd bean 400g;
  • 亨氏 bean 400 克。

  • 它们实际上是相同的产品,可以合并在一起。

    我的计划是使用 Jaro–Winkler distance 的实现查找匹配项。该过程如下:
  • 列出内存中所有产品名称的大列表;
  • 选择列表中的第一个产品;
  • 将其与列表中紧随其后的每个产品进行比较并计算“Jaro 分数”;
  • 任何匹配度高(比如 0.95f 或更高)的产品都会被报告;
  • 转到下一个产品。

  • 所以这有一些优化,因为它只以一种方式匹配每个产品,节省了一半的处理时间。

    我对此进行了编码并进行了测试。它工作得很好,找到了几十个匹配项进行调查。

    将 1 个产品与 2,500,000 个其他产品进行比较并计算“Jaro Score”大约需要 20 秒。假设我的计算是正确的,这意味着完成处理需要一年中的大部分时间。

    显然这是不切实际的。

    我让同事检查了代码,他们设法将 Jaro Score 计算部分的速度提高了 20%。他们使进程成为多线程的,这让它更快了一点。我们还删除了一些存储的信息,将其缩减为仅包含产品名称和唯一标识符的列表;这似乎对处理时间没有任何影响。

    有了这些改进,我们仍然认为这需要几个月的时间来处理,我们需要几个小时(或最多几天)。

    我不想讨论太多细节,因为我认为这并不完全相关,但我将产品详细信息加载到列表中:
    private class Product
    {
        public int MemberId;
        public string MemberName;
        public int ProductId;
        public string ProductCode;
        public string ProductName;
    }
    private class ProductList : List<Product> { }
    private readonly ProductList _pl = new ProductList();
    

    然后我使用以下内容来处理每个产品:
    {Outer loop...
    var match = _pl[matchCount];
    
    for (int count = 1; count < _pl.Count; count++)
    {
        var search = _pl[count];
        //Don't match products with themselves (redundant in a one-tailed match)
        if (search.MemberId == match.MemberId && search.ProductId == match.ProductId)
            continue;
        float jaro = Jaro.GetJaro(search.ProductName, match.ProductName);
    
        //We only log matches that pass the criteria
        if (jaro > target)
        {
            //Load the details into the grid
            var row = new string[7];
            row[0] = search.MemberName;
            row[1] = search.ProductCode;
            row[2] = search.ProductName;
            row[3] = match.MemberName;
            row[4] = match.ProductCode;
            row[5] = match.ProductName;
            row[6] = (jaro*100).ToString("#,##0.0000");
            JaroGrid.Rows.Add(row);
        }
    }
    

    我认为出于这个问题的目的,我们可以假设 Jaro.GetJaro 方法是一个“黑匣子”,即它如何工作并不重要,因为这部分代码已经尽可能地优化,我可以不想想怎么改进。

    关于模糊匹配此产品列表的更好方法的任何想法?

    我想知道是否有一种“聪明”的方法来预处理列表以在匹配过程开始时获得最多的匹配项。例如,如果比较所有产品需要 3 个月的时间,而比较“可能”的产品只需 3 天,那么我们可以接受这一点。

    好的,有两个常见的事情来了。首先,是的,我确实利用了单尾匹配过程。真正的代码是:
    for (int count = matchCount + 1; count < _pl.Count; count++)
    

    我很遗憾发布了修改后的版本;我试图简化这个(坏主意)。

    其次,很多人都想看 Jaro 代码,所以这里是(它很长而且它最初不是我的 - 我什至可能在这里某处找到了它?)。顺便说一句,我喜欢在出现不匹配的情况后立即退出的想法。我现在就开始看!
    using System;
    using System.Text;
    
    namespace EPICFuzzyMatching
    {
        public static class Jaro
        {
            private static string CleanString(string clean)
            {
                clean = clean.ToUpper();
                return clean;
            }
    
            //Gets the similarity of the two strings using Jaro distance
            //param string1 the first input string
            //param string2 the second input string
            //return a value between 0-1 of the similarity
            public static float GetJaro(String string1, String string2)
            {
                //Clean the strings, we do some tricks here to help matching
                string1 = CleanString(string1);
                string2 = CleanString(string2);
    
                //Get half the length of the string rounded up - (this is the distance used for acceptable transpositions)
                int halflen = ((Math.Min(string1.Length, string2.Length)) / 2) + ((Math.Min(string1.Length, string2.Length)) % 2);
    
                //Get common characters
                String common1 = GetCommonCharacters(string1, string2, halflen);
                String common2 = GetCommonCharacters(string2, string1, halflen);
    
                //Check for zero in common
                if (common1.Length == 0 || common2.Length == 0)
                    return 0.0f;
    
                //Check for same length common strings returning 0.0f is not the same
                if (common1.Length != common2.Length)
                    return 0.0f;
    
                //Get the number of transpositions
                int transpositions = 0;
                int n = common1.Length;
                for (int i = 0; i < n; i++)
                {
                    if (common1[i] != common2[i])
                        transpositions++;
                }
                transpositions /= 2;
    
                //Calculate jaro metric
                return (common1.Length / ((float)string1.Length) + common2.Length / ((float)string2.Length) + (common1.Length - transpositions) / ((float)common1.Length)) / 3.0f;
            }
    
            //Returns a string buffer of characters from string1 within string2 if they are of a given
            //distance seperation from the position in string1.
            //param string1
            //param string2
            //param distanceSep
            //return a string buffer of characters from string1 within string2 if they are of a given
            //distance seperation from the position in string1
            private static String GetCommonCharacters(String string1, String string2, int distanceSep)
            {
                //Create a return buffer of characters
                var returnCommons = new StringBuilder(string1.Length);
    
                //Create a copy of string2 for processing
                var copy = new StringBuilder(string2);
    
                //Iterate over string1
                int n = string1.Length;
                int m = string2.Length;
                for (int i = 0; i < n; i++)
                {
                    char ch = string1[i];
    
                    //Set boolean for quick loop exit if found
                    bool foundIt = false;
    
                    //Compare char with range of characters to either side
                    for (int j = Math.Max(0, i - distanceSep); !foundIt && j < Math.Min(i + distanceSep, m); j++)
                    {
                        //Check if found
                        if (copy[j] == ch)
                        {
                            foundIt = true;
                            //Append character found
                            returnCommons.Append(ch);
                            //Alter copied string2 for processing
                            copy[j] = (char)0;
                        }
                    }
                }
                return returnCommons.ToString();
            }
        }
    }
    

    看到这个问题仍然有一些看法,我想我会快速更新发生的事情:
  • 我真的希望我最初发布了我正在使用的实际代码,因为人们仍然告诉我迭代一半(显然没有阅读第一段左右);
  • 我采纳了这里提出的一些建议,以及 SO 之外的其他人提出的一些建议,并将运行时间缩短到 70 小时左右;
  • 主要的改进是对数据进行预处理,以仅考虑具有相当高销售额的商品。不是很大,但它使工作量大大减少;
  • 我的笔记本电脑出现过热问题,所以我在周末用冰箱里的笔记本电脑完成了大部分工作。在这样做的过程中,我了解到冰箱不是笔记本电脑的好环境(太潮湿),大约一周后我的笔记本电脑就坏​​了;
  • 最终的结果是我实现了我打算做的事情,也许没有我希望的那么全面,但总的来说,我认为它是成功的;
  • 为什么我没有接受答案?好吧,下面的答案实际上都没有完全解决我最初的问题,尽管它们大多有帮助(我第一次发布这个问题几年后出现的一些答案肯定没有帮助),但我觉得选择一个作为“答案”是不公平的”。
  • 最佳答案

    恕我直言,您绝对应该发布 GetJaro() 代码,因为它是您程序中需要时间的部分。

    它涉及字符串操作并被执行数百万次。如果 StackOverflow 用户看到更多的改进,这只会减少一小部分计算时间,那么这将比删除列表处理的微秒带来更多的改进。

    tl; dr:优化需要时间的代码,而不是围绕它的循环。

    编辑:我必须把它放在答案中。不要使用浮点数,而是使用整数类型。由于不需要 FPU,因此速度要快得多。
    此外,我确实建议对输入进行标准化,如 ToUpper() 或使所有项目在外观上“相似”的东西。

    关于c# - 尝试优化模糊匹配,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/18916396/

    有关c# - 尝试优化模糊匹配的更多相关文章

    1. ruby - ECONNRESET (Whois::ConnectionError) - 尝试在 Ruby 中查询 Whois 时出错 - 2

      我正在用Ruby编写一个简单的程序来检查域列表是否被占用。基本上它循环遍历列表,并使用以下函数进行检查。require'rubygems'require'whois'defcheck_domain(domain)c=Whois::Client.newc.query("google.com").available?end程序不断出错(即使我在google.com中进行硬编码),并打印以下消息。鉴于该程序非常简单,我已经没有什么想法了-有什么建议吗?/Library/Ruby/Gems/1.8/gems/whois-2.0.2/lib/whois/server/adapters/base.

    2. ruby 正则表达式 - 如何替换字符串中匹配项的第 n 个实例 - 2

      在我的应用程序中,我需要能够找到所有数字子字符串,然后扫描每个子字符串,找到第一个匹配范围(例如5到15之间)的子字符串,并将该实例替换为另一个字符串“X”。我的测试字符串s="1foo100bar10gee1"我的初始模式是1个或多个数字的任何字符串,例如,re=Regexp.new(/\d+/)matches=s.scan(re)给出["1","100","10","1"]如果我想用“X”替换第N个匹配项,并且只替换第N个匹配项,我该怎么做?例如,如果我想替换第三个匹配项“10”(匹配项[2]),我不能只说s[matches[2]]="X"因为它做了两次替换“1fooX0barXg

    3. ruby - 匹配未转义的平衡定界符对 - 2

      如何匹配未被反斜杠转义的平衡定界符对(其本身未被反斜杠转义)(无需考虑嵌套)?例如对于反引号,我试过了,但是转义的反引号没有像转义那样工作。regex=/(?!$1:"how\\"#expected"how\\`are"上面的正则表达式不考虑由反斜杠转义并位于反引号前面的反斜杠,但我愿意考虑。StackOverflow如何做到这一点?这样做的目的并不复杂。我有文档文本,其中包括内联代码的反引号,就像StackOverflow一样,我想在HTML文件中显示它,内联代码用一些spanMaterial装饰。不会有嵌套,但转义反引号或转义反斜杠可能出现在任何地方。

    4. ruby-on-rails - 每次我尝试部署时,我都会得到 - (gcloud.preview.app.deploy) 错误响应 : [4] DEADLINE_EXCEEDED - 2

      我是Google云的新手,我正在尝试对其进行首次部署。我的第一个部署是RubyonRails项目。我基本上是在关注thisguideinthegoogleclouddocumentation.唯一的区别是我使用的是我自己的项目,而不是他们提供的“helloworld”项目。这是我的app.yaml文件runtime:customvm:trueentrypoint:bundleexecrackup-p8080-Eproductionconfig.ruresources:cpu:0.5memory_gb:1.3disk_size_gb:10当我转到我的项目目录并运行gcloudprevie

    5. ruby - 匹配大写字母并用后续字母填充,直到一定的字符串长度 - 2

      我有一个驼峰式字符串,例如:JustAString。我想按照以下规则形成长度为4的字符串:抓取所有大写字母;如果超过4个大写字母,只保留前4个;如果少于4个大写字母,则将最后大写字母后的字母大写并添加字母,直到长度变为4。以下是可能发生的3种情况:ThisIsMyString将产生TIMS(大写字母);ThisIsOneVeryLongString将产生TIOV(前4个大写字母);MyString将生成MSTR(大写字母+tr大写)。我设法用这个片段解决了前两种情况:str.scan(/[A-Z]/).first(4).join但是,我不太确定如何最好地修改上面的代码片段以处理最后一种

    6. c# - 如何在 ruby​​ 中调用 C# dll? - 2

      如何在ruby​​中调用C#dll? 最佳答案 我能想到几种可能性:为您的DLL编写(或找人编写)一个COM包装器,如果它还没有,则使用Ruby的WIN32OLE库来调用它;看看RubyCLR,其中一位作者是JohnLam,他继续在Microsoft从事IronRuby方面的工作。(估计不会再维护了,可能不支持.Net2.0以上的版本);正如其他地方已经提到的,看看使用IronRuby,如果这是您的技术选择。有一个主题是here.请注意,最后一篇文章实际上来自JohnLam(看起来像是2009年3月),他似乎很自在地断言RubyCL

    7. C# 到 Ruby sha1 base64 编码 - 2

      我正在尝试在Ruby中复制Convert.ToBase64String()行为。这是我的C#代码:varsha1=newSHA1CryptoServiceProvider();varpasswordBytes=Encoding.UTF8.GetBytes("password");varpasswordHash=sha1.ComputeHash(passwordBytes);returnConvert.ToBase64String(passwordHash);//returns"W6ph5Mm5Pz8GgiULbPgzG37mj9g="当我在Ruby中尝试同样的事情时,我得到了相同sha

    8. ruby-on-rails - Rails 3,嵌套资源,没有路由匹配 [PUT] - 2

      我真的为这个而疯狂。我一直在搜索答案并尝试我找到的所有内容,包括相关问题和stackoverflow上的答案,但仍然无法正常工作。我正在使用嵌套资源,但无法使表单正常工作。我总是遇到错误,例如没有路线匹配[PUT]"/galleries/1/photos"表格在这里:/galleries/1/photos/1/edit路线.rbresources:galleriesdoresources:photosendresources:galleriesresources:photos照片Controller.rbdefnew@gallery=Gallery.find(params[:galle

    9. 基于C#实现简易绘图工具【100010177】 - 2

      C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

    10. ruby - rbenv 安装 ruby​​ 校验和不匹配 osx - 2

      我已经在mountainlion上成功安装了rbenv和ruby​​build。运行rbenvinstall1.9.3-p392结束于:校验和不匹配:ruby-1.9.3-p392.tar.gz(文件已损坏)预期f689a7b61379f83cbbed3c7077d83859,得到1cfc2ff433dbe80f8ff1a9dba2fd5636它正在下载的文件看起来没问题,如果我使用curl手动下载文件,我会得到同样不正确的校验和。有没有人遇到过这个?他们是如何解决的? 最佳答案 tl:博士;使用浏览器从http://ftp.rub

    随机推荐