【问题标题】:Optimize matching elements from two large data sets using Levenshtein distance (comparing each element to each other element)使用 Levenshtein 距离优化来自两个大型数据集的匹配元素(将每个元素与其他元素进行比较)
【发布时间】:2019-06-27 22:12:22
【问题描述】:

我目前正在解决一个问题,即从另一个名为“ListB”的列表中找到最匹配的数据,即“ListA”。每当我发现“ListA”的元素与“ListB”中的任何元素匹配时,该元素的置信度和准确度为 70% 或更高,我将列表 B 中的匹配字符串和列表 A 中的字符串添加到我进一步的元组中保存在数据库中。

Levenshtien 算法给了我一个数字,我将它与我的阈值 70 进行比较,如果返回的值等于或大于 70% 的阈值,我将其附加到“ListA”的原始字符串元素中。

如果“ListA”和“ListB”中的记录在数千个值范围内,并且如果我将记录增加到一百万个,则我为此过程编写的代码可以正常工作,大约需要一个小时来计算每个值的距离列表 A 的元素。

我需要针对庞大的数据集优化流程。请告知我需要在哪里进行改进。

到目前为止,我的流程代码如下所示

  public static PerformFuzzyMatch()
  {
    // Fetch the ListA & List B from SQL tables
     var ListACount = await FuzzyMatchRepo.FetchListACount();
     var ListB = await FuzzyMatchRepo.FetchListBAsync();

    //Split the ListA data to smaller chunks and loop through those chunks 
     var splitGroupSize = 1000;
     var sourceDataBatchesCount = ListACount / splitGroupSize;

     // Loop through the smaller chunks of List A
     for (int b = 0; b < sourceDataBatchesCount; b++)
     {
       var currentBatchMatchedWords = new List<Tuple<string, string, double>>();
       int skipRowCount = b * splitGroupSize;
       int takeRowCount = splitGroupSize;

       // Get chunks of data from ListA according to the skipRowCount and takeRowCount
   var currentSourceDataBatch = FuzzyMatchRepository.FetchSourceDataBatch(skipRowCount, takeRowCount);

 //Loop through the ListB and parallely calculate the distance between chunks of List A and List B data
   for (int i = 0; i < ListB.Count; i++)
   {
     Parallel.For(
      0,
      currentSourceDataBatch.Count,
      new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount * 10 },
      cntr =>
      {
         try
         {
            //call the Levenshtien Algorithm to calculate the distance between each element of ListB and the smaller chunk of List A.
              int leven = LevenshteinDistance(currentSourceDataBatch[cntr], ListB[i]);
              int length = Math.Max(currentSourceDataBatch[cntr].Length, ListB[i].Length);
              double similarity = double similarity = 1.0 - (double)leven / length;
              if ((similarity * 100) >= 70)
              {
     currentBatchMatchedWords.Add(Tuple.Create(currentSourceDataBatch[cntr], ListB[i], similarity));
            }
          cntr++;
         }
        catch (Exception ex)
        {
         exceptions.Enqueue(ex);
        }
      });
     }
   }
  }

它调用的算法是计算距离

public static int LevenshteinDistance(this string input, string comparedTo, bool caseSensitive = false)
    {
        if (string.IsNullOrWhiteSpace(input) || string.IsNullOrWhiteSpace(comparedTo))
        {
            return -1;
        }

        if (!caseSensitive)
        {
            input = Common.Hashing.InvariantUpperCaseStringExtensions.ToUpperInvariant(input);
            comparedTo = Common.Hashing.InvariantUpperCaseStringExtensions.ToUpperInvariant(comparedTo);
        }

        int inputLen = input.Length;
        int comparedToLen = comparedTo.Length;

        int[,] matrix = new int[inputLen, comparedToLen];

        //initialize           
        for (var i = 0; i < inputLen; i++)
        {
            matrix[i, 0] = i;
        }
        for (var i = 0; i < comparedToLen; i++)
        {
            matrix[0, i] = i;
        }

        //analyze
        for (var i = 1; i < inputLen; i++)
        {
            ushort si = input[i - 1];
            for (var j = 1; j < comparedToLen; j++)
            {
                ushort tj = comparedTo[j - 1];
                int cost = (si == tj) ? 0 : 1;

                int above = matrix[i - 1, j];
                int left = matrix[i, j - 1];
                int diag = matrix[i - 1, j - 1];
                int cell = FindMinimumOptimized(above + 1, left + 1, diag + cost);

                //transposition
                if (i > 1 && j > 1)
                {
                    int trans = matrix[i - 2, j - 2] + 1;
                    if (input[i - 2] != comparedTo[j - 1])
                    {
                        trans++;
                    }
                    if (input[i - 1] != comparedTo[j - 2])
                    {
                        trans++;
                    }
                    if (cell > trans)
                    {
                        cell = trans;
                    }
                }
                matrix[i, j] = cell;
            }
        }
        return matrix[inputLen - 1, comparedToLen - 1];
    }

寻找最小值优化的实现

public static int FindMinimumOptimized(int a, int b, int c)
    {
        return Math.Min(a, Math.Min(b, c));
    }

【问题讨论】:

  • 你永远不应该使用 MaxDegreeOfParallelism = Environment.ProcessorCount * 10。要么你是 CPU 密集型的,你会增加线程开销,要么你是 I/O 密集型的,你应该在你的代码。
  • new List>();也许你需要一堂课
  • 我的意思是要求每个内核运行 10 个线程不会让速度更快。我的意思是多线程不是免费的。看起来您正在使用带有线程的线程不安全集合。
  • List 不是线程安全的。你不能像现在这样Add
  • @Shahid 您需要使用来自System.Threading.Concurrent 命名空间的线程安全集合之一。最接近List&lt;T&gt; 的可能是ConcurrentBag&lt;T&gt;

标签: c# performance optimization


【解决方案1】:

这是一个固有地具有二次成本O(N^2) 的计算。随着项目数量的增加,这总是会非常糟糕地扩展。

您可以将其并行化,但这只是执行时间的常数因子增益。

你能找到其他一些基于平等的标准来匹配吗?在这种情况下,您可以使用基于哈希的算法来非常快速地创建候选检查。例如,假设您正在匹配文本文章,并且您希望几乎所有 Levenstein 匹配都出现在同一日历日撰写的文章中,那么您可以先按日期匹配(使用 O(N) 复杂度),然后才对所有文章进行二次比较那天的物品。您还可以比较前一天和第二天的项目,以留出一些余地。

如果你不能这样做,你必须接受二次缩放。

一个好的代码模式是这样的:

var pairs = (from a in listA
             from b in listB //cross product
             select new { a, b });

var matches = 
 pairs
 .AsParallel()
 .Where(pair => IsLevenshteinMatch(pair))
 .ToList();

您可以丢弃为此编写的所有复杂代码。在使用并发性或并行性时,考虑一下最好的设计通常是值得的。通常,对于常见问题有非常紧凑的解决方案。

【讨论】:

  • 感谢 @usr 的富有洞察力的回答。不幸的是,我没有任何其他标准可以匹配。我尝试从类似于 ListA 的 ListB 中找到最匹配的单词。我跟进了您的建议并使用了投影而不是嵌套的,但性能提升没有任何改善。
  • @Shahid 不,这具有相同的性能。鉴于您必须进行二次匹配,您无法做得更好。并行化技术的选择与性能无关,因为 99.999% 的时间都花在了 Levenshtein。
  • 我曾经遇到过同样的问题。我匹配了长文本文章。通过首先要求任何两篇文章有多个共同的稀有词,我能够消除许多 Levenshtein 比较。这可以使用基于平等的技术。我输掉了几场真正的 Levenshein 比赛,但损失的金额非常小。 @Shahid 实际上,我想我将文本分成句子,并要求每对文章共享一个相当长的句子。我不太记得了。你可以变得聪明。
  • 我在优化 Levenshtein 算法时偶然发现了这一点 github.com/felixfang/LevenshteinAutomaton 将看看它。让我们希望它提供了一条出路。谢谢你的帮助。真的很感激!
  • @Shahid 看来您需要为每个不同的输入创建一个自动机。那仍然是二次的。也许它仍然更快。我很想知道你的结果。
猜你喜欢
  • 2013-06-06
  • 1970-01-01
  • 1970-01-01
  • 2017-09-29
  • 2021-02-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多