【问题标题】:Levenshtein distance giving strange valuesLevenshtein 距离给出奇怪的值
【发布时间】:2021-06-18 16:06:35
【问题描述】:

这是一个字符串T

'男式衬衫团队 brienne 有趣的讽刺衬衫 特色 图形 T 恤 杯子 婴儿装 非常真实 激情 辉煌 设计 详细插图 强烈欣赏 东西 创意 br 商店 数千个设计在不同衬衫中发现 婴儿装 杯子 有趣的流行文化 抽象 诙谐的许多设计 照亮一天 好日子 几乎每个人else 符合 ul li 质量 短袖 圆领衬衫 100 棉 柔软耐用 舒适 手感合身 标准尺寸 怀疑 l xl 可用 li li 可持续发展标签公司 构想信念 纺织行业开始负责任地行事使用国家艺术直接服装设备印刷的纯棉 li li 服装裂纹剥离水洗 li li 图形 T 恤设计专业印刷的独特设计看起来很棒让某人微笑有趣可爱复古表现力艺术品 li ul'

我已经突出显示了上面字符串的一部分,因为上面是字符串的预处理版本,因此可能难以阅读。

我得到以下值:

fuzz.partial_ratio('short sleeve', T)50

fuzz.partial_ratio('long sleeve', T)73

fuzz.partial_ratio('dsfsdf sleeve', T)62

fuzz.partial_ratio('sleeve', T)50

我对此感到非常困惑。第一个和第四个值不应该是 100 吗?当然我错过了一些东西,但我无法弄清楚。

编辑:这是我在卸载 python-Levenshtein 库后运行的另一个示例:

'第一次成功的方式妻子告诉 v 2 长袖衬衫 id 1084 第一次成功方式妻子告诉 v 2 长袖衬衫设计印花质量 100 长袖棉衬衫运动灰色 90 棉 10 涤标准长袖衬衫时尚合身紧身款式请检查尺码表列出的附加图片随时联系我们第一次尺码问题满意度 100 件保证衬衫通常发货工作日中午订购下一个工作日中午订购长袖衬衫 100 棉标准衬衫时尚合身组合运输多件商品'

fuzz.partial_ratio('long sleeve', T) 给出 27

fuzz.partial_ratio('short sleeve', T) 给出 33

fuzz.partial_ratio('sleeveless', T) 给出 40

fuzz.partial_ratio('dsfasd sleeve', T) 给出 23

不幸的是,这个问题似乎不是 python-Levenshtein 库独有的。

【问题讨论】:

  • 您确定您正确使用了 levenshtein 吗?它应该用于相似长度的字符串。如果您在整堆文本中寻找“相似”字符串,我会使用“窗口”进行搜索,并一次将其移动一个字符。此外,如果您将该段落与fuzzy 中的任何字符串进行比较,这不是搜索操作,这是这些字符串彼此之间的“接近程度”操作。
  • 我不认为你缺少任何东西。图书馆里似乎有一个微妙的错误。试图弄清楚到底发生了什么。
  • 是的,他是对的,对齐块后应该是 100。我将使用代码库进行调试并检查它,看看哪里出了问题,或者函数中是否存在假设。

标签: python levenshtein-distance fuzzywuzzy


【解决方案1】:

fuzzywuzzy 库中某处存在一个非常奇怪且微妙的错误。

如果我们运行以下命令

from fuzzywuzzy import fuzz

fuzz.partial_ratio('funny', 'aa aaaaa aaaa aaaaaaa funny aaaaaaa aaaaaaaa aaaaaaa aaaa aaaa aaayaaaa auaa aaaa aaaaaaaa aaaaaaaaa aaaaaa aaaaaaaa aaaaa aaaa aa aaaaaaaaaaa aaaaaa aaaffaaaaaaa aaaaa aaayaaaa auaa funny aaaa aaaaaa')

它返回0

如果我们从这个字符串的开头删除一个字母:

fuzz.partial_ratio('funny', 'a aaaaa aaaa aaaaaaa funny aaaaaaa aaaaaaaa aaaaaaa aaaa aaaa aaayaaaa auaa aaaa aaaaaaaa aaaaaaaaa aaaaaa aaaaaaaa aaaaa aaaa aa aaaaaaaaaaa aaaaaa aaaffaaaaaaa aaaaa aaayaaaa auaa funny aaaa aaaaaa')

它返回100

(对冗长而可怕的字符串感到抱歉。我已尝试将其简化为尽可能简单的字符串,但我似乎看不到驱动此错误的逻辑)

Github 上好像有similar bug reports

安装 python-Levenshtein 似乎可以解决我上面的示例(如果未安装 python-Levenshtein,fuzzywuzzy 将恢复为 difflib),但不会更改您的原始示例。

安装了python-Levenshtein,我可以将您的示例简化为:

fuzz.partial_ratio('sleeve', 's l e e v sleeve e ')

返回50

从较长的字符串中删除第一个字母:

fuzz.partial_ratio('sleeve', 'l e e v sleeve e ')

返回100

这提供了一些关于可能发生的事情的提示,但我怀疑这需要深入了解python-Levenshtein 才能弄清楚。

我的建议?提交错误报告。然后找到另一个库来比较字符串。 RapidFuzz 可能是一个合适的选择。

更新:

我认为该错误可能与使用python-Levenshtein库中的opcodes有关:

from Levenshtein import opcodes

opcodes('sleeve', 's l e e v sleeve e ')

返回:

[('equal', 0, 1, 0, 1),
 ('insert', 1, 1, 1, 2),
 ('equal', 1, 2, 2, 3),
 ('insert', 2, 2, 3, 4),
 ('equal', 2, 3, 4, 5),
 ('insert', 3, 3, 5, 6),
 ('equal', 3, 4, 6, 7),
 ('insert', 4, 4, 7, 8),
 ('equal', 4, 5, 8, 9),
 ('insert', 5, 5, 9, 12),
 ('equal', 5, 6, 12, 13),
 ('insert', 6, 6, 13, 19)]

当在fuzzywuzzy 中使用时,这显然不是预期的结果,即使这些是一组最少的编辑操作。在fuzzywuzzy 中,优先级应该放在连续块上,而 Levenshtein 距离的正式定义并没有优先考虑连续块和非连续块(至少在我看来不是)。请注意,difflib.SequenceMatcher.get_opcodes() 会给出不同的结果。

我怀疑需要一些非常仔细的想法来修复这个错误并让它正确。

【讨论】:

  • 该错误必须与 python-Levenshtein 序列匹配器有关,因为我只能在安装包的情况下复制它。
  • 我也安装了 python levenshtein。并感谢您的回答!我使用 python levenshtein 中的函数测试了其中一些边缘案例,并且在那里也得到了一些时髦的结果。我醒来后会编辑问题
  • 是的,它归结为假设最小编辑操作将优先考虑连续块,但事实并非如此。只要较短单词的字母按顺序出现在较长的字符串中(甚至在其他单词中间隔开),那么使用fuzzywuzzy 就会遇到问题。不妨试试 RapidFuzz 作为替代方案。
  • 我在大多数功能中也遇到过这种或其他形式的错误。 process.extract_one 还对某些字符串做了一些有趣的事情,它似乎不符合 Levenshtein 距离的真实定义。就像@the23Effect 状态一样,只有安装了 python-Levenshtein。
  • 现实情况是,在将短字符串与更大的文本部分进行比较时,Levenshtein 距离是一个糟糕的度量标准。具有显式间隙惩罚的算法更合适,例如 Smith-Waterman。我不确定fuzzywuzzy 库的解决方案是什么。您不想破坏向后兼容性,但使用 Levenshtein 距离会引入太多有问题的边缘情况。
【解决方案2】:

算法背后的一般思想是在较长的字符串中找到最佳匹配的子字符串。但是,在 FuzzyWuzzy 中执行此操作的方式存在一些问题。在下面对算法的描述中,s1 指的是较短的字符串,s2 是较长的字符串,s2_substr 是 s2 的子字符串。 他们通过以下步骤实现该算法:

  1. 他们使用最长公共子序列算法在s2中找到s1的最长公共子串
  2. 他们使用这些公共子序列的起始索引从s2 中提取长度为s1_len 的子字符串。此子字符串s2_substr 放在s2 的末尾时,可以比s1_len 短。
  3. 他们遍历这些子字符串 s2_substr,并使用标准化的 InDel-Distance(如 Levenshtein 距离,但没有替换)将它们与 s1 进行比较

我知道这个实现的以下缺点

  1. 使用 python-Levenshtein 时,FuzzyWuzzy 使用它来查找最长公共子序列并计算相似度。但是,已知 python-Levenshtein 用于查找最长公共子序列的实现已损坏(请参阅here),我不知道对此有简单的解决方法。有人提出了一个修复方案,但是它只修复了这个案例并在不同的案例中引入了问题。 (这是您描述的原始问题)
  2. 当不使用 python-Levenshtein 时,使用 difflib 计算最长公共子序列使用 difflib 计算。但是,正如here FuzzyWuzzy 所述,不会禁用自动垃圾启发式算法,这会在字符串长度差异很大时导致错误结果。我刚刚创建了一个 PR 来解决这个问题:https://github.com/seatgeek/fuzzywuzzy/pull/303,但存储库并没有真正得到积极维护,而且 SeatGeek 看起来很好,有很多缺点,因为它对于他们的用例来说已经足够好了。 (这是您稍后添加的 difflib 的问题)
  3. 相似度本身就有缺陷。它假设最佳匹配子串s2_substr 总是从最长公共子序列之一的起点开始。尽管在许多情况下都是如此,但情况并非总是如此。 (您没有遇到这个问题,我还没有在 FuzzyWuzzy 或 RapidFuzz 中看到关于此问题的错误报告。结果仅在一些非常具体的边缘情况下存在很大差异,大多数用户可能不会经常遇到)

哪种算法更适合很大程度上取决于您的需求。第一个简单的解决方案是用我的库RapidFuzz 替换 FuzzyWuzzy。这解决了我描述的 LCS 算法的问题。但是,它使用与 FuzzyWuzzy 相同的算法来计算相似度,因此也存在第三个问题。我正在寻找更好的算法(有关更多详细信息,请查看following question)。 正如 Andrew Guy 所指出的,Smith-Waterman 距离也可能是一种替代方法。不过和fuzz.partial_ratio还是有很大区别的:

  1. 它使用统一的 Levenshtein 距离(插入/删除/替换的权重均为 1),而 fuzz.partial_ratio 使用 InDel 距离。如果这对您很重要,则可以通过在实现时将 Substitutions 的权重设为 2 来调整它以使用 InDel 距离。
  2. fuzz.partial_ratio 总是采用长度为s1_len 的子字符串,而 Smith Waterman 算法搜索最佳对齐的子字符串,而不关心它的长度。这还不错,您应该意识到这一点。一个缺点是,由于子字符串的长度未知,因此更难对结果进行归一化(使其相似度得分在 0 到 100 之间)。这不是一个真正的问题,因为您可以只搜索最低距离而不是最高相似度。

我没有在 RapidFuzz 中使用 Smith-Waterman 算法来计算 fuzz.partial_ratio 的原因是我希望它直接替代 FuzzyWuzzy 中的实现。不过,我也计划在未来添加 Smith-Waterman 算法。

【讨论】:

  • 很好的答案,很明显你比我更了解问题的复杂性。需要注意的是,我对 Smith-Waterman 算法的建议来自于我在生物序列分析方面的背景,这与 NLP 有点不同。重要的一点是它不会将单词边界与任何其他字符区别对待,因此在 NLP 设置中使用时可能会产生奇怪的结果。