FuzzyWuzzy
由于您提到将 FuzzyWuzzy 用作距离度量,因此我将专注于实现fuzz.ratio
FuzzyWuzzy 使用的算法的有效方法。FuzzyWuzzy 提供以下两种实现fuzz.ratio
:
- difflib,完全用 Python 实现
- python-Levenshtein,它使用权重为 2 的加权 Levenshtein 距离进行替换(替换是删除 + 插入)。Python-Levenshtein 是用 C 实现的,比纯 Python 实现快得多。
在 python-Levenshtein 中的实现
的实现python-Levenshtein
使用以下实现:
- 删除两个字符串的公共前缀和后缀,因为它们对最终结果没有任何影响。这可以在线性时间内完成,因此匹配相似的字符串非常快。
- 修剪后的字符串之间的 Levenshtein 距离是通过二次运行时和线性内存使用来实现的。
快速模糊
我是RapidFuzz库的作者,它以更高效的方式实现了 FuzzyWuzzy 使用的算法。RapidFuzz 使用以下接口fuzz.ratio
:
def ratio(s1, s2, processor = None, score_cutoff = 0)
附加score_cutoff
参数可用于提供分数阈值,作为 0 到 100 之间的浮点数。对于 ratio < score_cutoff,返回 0。在某些情况下,实现可以使用它来使用更多更优化的实现。下面我将描述 RapidFuzz 根据输入参数使用的优化。以下max distance
是指在不低于分数阈值的情况下可能的最大距离。
最大距离 == 0
可以使用直接比较来计算相似度,因为不允许字符串之间存在差异。该算法的时间复杂度为O(N)
。
最大距离 == 1 和 len(s1) == len(s2)
也可以使用直接比较来计算相似度,因为替换会导致编辑距离高于最大距离。该算法的时间复杂度为O(N)
。
删除常用前缀
两个比较字符串的共同前缀/后缀不会影响 Levenshtein 距离,因此在计算相似度之前删除了词缀。此步骤针对以下任何算法执行。
最大距离 <= 4
使用了mbleven算法。该算法检查阈值下所有可能的编辑操作max distance
。原始算法的描述可以在这里找到。我更改了此算法以支持 2 的权重进行替换。作为与正常 Levenshtein 距离的区别,该算法甚至可以在此处使用高达 4 的阈值,因为较高的替换权重会减少可能的编辑操作的数量。该算法的时间复杂度为O(N)
。
len(shorter string) <= 64 删除常用词缀后
使用 BitPA1 算法,并行计算 Levenshtein 距离。该算法在此处进行了描述,并在此实现中扩展了对 UTF32 的支持。该算法的时间复杂度为O(N)
。
长度 > 64 的字符串
Levenshtein 距离是使用 Wagner-Fischer 和 Ukkonens 优化计算的。该算法的时间复杂度为O(N * M)
。这可能会在未来被 BitPal 的分块实现所取代。
处理器的改进
FuzzyWuzzy 提供了多个类似process.extractOne
的处理器,用于计算查询和多项选择之间的相似度。在 C++ 中实现这一点也允许两个更重要的优化:
当使用同样用 C++ 实现的记分器时,我们可以直接调用记分器的 C++ 实现,而不必在 Python 和 C++ 之间来回切换,这提供了巨大的加速
我们可以根据使用的记分器对查询进行预处理。例如,当fuzz.ratio
用作记分器时,它只需将查询存储到 BitPal 使用的 64 位块中一次,这在计算 Levenshtein 距离时节省了大约 50% 的运行时间
到目前为止,只有extractOne
andextract_iter
是在 Python 中实现的,而extract
您将使用的仍然是在 Python 中实现并使用extract_iter
. 所以它已经可以使用 2. 优化,但仍然需要在 Python 和 C++ 之间进行很多切换,这不是最佳的(这可能也会在 v1.0.0 中添加)。
基准
我在开发过程中为个人评分者执行了基准测试extractOne
,显示了 RapidFuzz 和 FuzzyWuzzy 之间的性能差异。请记住,您的案例(所有字符串长度为 20)的性能可能不太好,因为使用的数据集中的许多字符串都非常小。
可重复科学数据的来源:
运行图形基准测试的硬件(规范):
- CPU:i7-8550U单核
- 内存:8 GB
- 操作系统:Fedora 32
基准得分手
可以在此处找到此基准测试的代码
基准提取一
对于此基准测试,代码process.extractOne
稍作更改以删除score_cutoff
参数。这样做是因为每当找到更好的匹配时 inextractOne
就会score_cutoff
增加(一旦找到完美匹配就退出)。process.extract
将来,对没有这种行为的基准进行基准测试会更有意义(基准是使用 执行的process.extractOne
,因为process.extract
尚未在 C++ 中完全实现)。基准代码可以在这里找到
这表明,在可能的情况下,不应直接使用计分器,而是通过处理器使用,这样可以执行更多优化。
选择
作为替代方案,您可以使用 C++ 实现。库 RapidFuzz在此处可用于 C++ 。C++中的实现也相对简单
// function to load words into vector
std::vector<std::string> choices = load("words.txt");
std::string query = choices[0];
std::vector<double> results;
results.reserve(choices.size());
rapidfuzz::fuzz::CachedRatio<decltype(query)> scorer(query);
for (const auto& choice : choices)
{
results.push_back(scorer.ratio(choice));
}
或并行使用 open mp
// function to load words into vector
std::vector<std::string> choices = load("words.txt");
std::string query = choices[0];
std::vector<double> results;
results.reserve(choices.size());
rapidfuzz::fuzz::CachedRatio<decltype(query)> scorer(query);
#pragma omp parallel for
for (const auto& choice : choices)
{
results.push_back(scorer.ratio(choice));
}
在我的机器上(参见上面的基准测试),这在并行版本中评估了 4300 万字/秒和 1.23 亿字/秒。这大约是 Python 实现的 1.5 倍(由于 Python 和 C++ 类型之间的转换)。然而,C++ 版本的主要优点是您可以相对自由地以任何您想要的方式组合算法,而在 Python 版本中,您被迫使用process
C++ 中实现的函数来获得良好的性能。