18

假设我的数据库中有以下两个字符串:

(1) 'Levi Watkins Learning Center - Alabama State University'
(2) 'ETH Library'

我的软件从数据源接收自由文本输入,它应该将这些自由文本与数据库中的预定义字符串(上述字符串)相匹配。

例如,如果软件得到字符串,它应该识别出这与比它'Alabama University'更相似。(1)(2)

起初,我想使用著名的字符串度量,例如 Levenshtein-Damerau 或 Trigrams,但这会导致不需要的结果,如下所示:

http://fuzzy-string.com/Compare/Transform.aspx?r=Levi+Watkins+Learning+Center+-+Alabama+State+University&q=Alabama+University

http://fuzzy-string.com/Compare/Transform.aspx?r=ETH+Library&q=Alabama+University

Difference to (1): 37
Difference to (2): 14

(2)获胜是因为它比(1), 即使包含搜索字符串的(1)两个单词 (Alabama和) 也短得多。University

我也用 Trigrams 尝试过(使用 Javascript 库模糊集),但我在那里得到了类似的结果。

是否有一个字符串度量可以识别搜索字符串与 的相似性(1)

4

6 回答 6

6

您可以尝试使用 Word Mover's Distance https://github.com/mkusner/wmd代替。该算法的一个显着优势是它在计算文档中单词之间的差异时结合了隐含的含义。论文可以在这里找到

于 2016-03-24T05:21:26.243 回答
3

你应该改变你的方法:

levenshtein Distance 擅长以“字符”或“单词”为单位计算相似度。

从概念上讲,您将阿拉巴马州和大学(2 个单词)视为 2 个单位,并且您想要计算单词之间的距离,其中 levenshtein 距离应该意味着阿拉巴马州和大学之间有多少个单词,应该是 1。

但是,您正在尝试应用为单词中的字符实现的 levenshtein 算法。此实现仅适用于匹配单个单词 NOT 句子。

最好你应该在顶部和每次匹配中为“单词”实现自己的 levenshtein 算法(使用 BK-Tree),再次使用 levenshtein 匹配每个单词作为“字符”。

您对 (1) 的结果应该与该算法的距离 1 匹配,而对 (2) 不匹配。

于 2014-01-10T14:38:00.633 回答
3

我想答案不再需要了,但我喜欢这个问题,它让我思考如何结合 RegEx 和 Levenshtein 字符串度量的优点,但减少对距离的依赖。

到目前为止,我已经提出了一个解析器,它遵循这个前提和逻辑:

  • 它使用 Python3 和正则表达式模块(OP 没有提到任何语言/模块要求)
  • 任何needle被搜索的内容都将从其标点字符中删除
  • Everyhaystack也被剥夺了标点符号 - 所以N.A.S.ANASA- 就像needle它最初一样N.A.S.A.- 我知道这在相当多的情况下可能会出现问题,但鉴于前提我无法想出更好的解决方案。
  • 长度不超过 3 个字符的每个单词都needle将被删除(不需要 is、on、at、no 等)
  • 匹配不区分大小写
  • needle被拆分为wordgroup包含n项目的 s:n在 dict 中定义,0 < k <= l其中kdict 键是
  • a 中的每个单词wordgroup必须相互跟随,n它们之间的单词距离最大
  • 每个单词,根据它的n长度,可以有一个不同的允许错误阈值:可以指定e总共的错误,substitions,inserts 和eletions,同样用一个 dict 保存 key whered0 < k <= n
  • 前面提到的两个 dict 都包含键/lambda 对,这对于它们的最后/第一项进行计算很有用

在线演示在这里

contextual_fuzzy_matcher.py:

from collections import OrderedDict
import regex


class ContextualFuzzyMatcher(object):
    maximum_word_distance = 2
    word_distance = r"\s(?:[\w]+\s){{0,{}}}".format(maximum_word_distance)
    punctuation = regex.compile(r"[\u2000-\u206F\u2E00-\u2E7F\\'!\"#$%&\(\)\*\+,\-\.\/:;<=>\?@\[\]\^_`\{\|\}~]")
    groups = OrderedDict((
        (0, lambda l: l),
        (4, lambda l: 3),
        (8, lambda l: 6),
        (10, lambda l: l // 0.75),
    ))
    tolerances = OrderedDict((
        (0, {
            'e': lambda l: 0,
            's': lambda l: 0,
            'i': lambda l: 0,
            'd': lambda l: 0,
        }),
        (3, {
            'e': lambda l: 1,
            's': lambda l: 1,
            'i': lambda l: 1,
            'd': lambda l: 1,
        }),
        (6, {
            'e': lambda l: 2,
            's': lambda l: 1,
            'i': lambda l: 1,
            'd': lambda l: 1,
        }),
        (9, {
            'e': lambda l: 3,
            's': lambda l: 2,
            'i': lambda l: 2,
            'd': lambda l: 2,
        }),
        (12, {
            'e': lambda l: l // 4,
            's': lambda l: l // 6,
            'i': lambda l: l // 6,
            'd': lambda l: l // 6,
        }),
    ))

    def __init__(self, needle):
        self.sentence = needle
        self.words = self.sentence_to_words(sentence)
        self.words_len = len(self.words)
        self.group_size = self.get_group_size()
        self.word_groups = self.get_word_groups()
        self.regexp = self.get_regexp()

    def sentence_to_words(self, sentence):
        sentence = regex.sub(self.punctuation, "", sentence)
        sentence = regex.sub(" +", " ", sentence)
        return [word for word in sentence.split(' ') if len(word) > 2]

    def get_group_size(self):
        return list(value for key, value in self.groups.items() if self.words_len >= key)[-1](self.words_len)

    def get_word_groups(self):
        return [self.words[i:i + self.group_size] for i in range(self.words_len - self.group_size + 1)]

    def get_tolerance(self, word_len):
        return list(value for key, value in self.tolerances.items() if word_len >= key)[-1]

    def get_regexp(self):
        combinations = []
        for word_group in self.word_groups:
            distants = []
            for word in word_group:
                word_len = len(word)
                tolerance = self.get_tolerance(word_len)
                distants.append(r"({}){{e<={},s<={},i<={},d<={}}}".format(
                    word,
                    tolerance['e'](word_len),
                    tolerance['s'](word_len),
                    tolerance['i'](word_len),
                    tolerance['d'](word_len),
                ))
            combinations.append(
                self.word_distance.join(distants)
            )
        return regex.compile(
            r"|".join(combinations),
            regex.MULTILINE | regex.IGNORECASE
        )

    def findall(self, haystack):
        return self.regexp.findall(haystack)

主要.py:

test_sentences = [
    'Levi Watkins Learning Center - Alabama State University',
    'ETH Library'
]
test_texts = [
    "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Sapien eget mi proin sed libero enim sed. Nec tincidunt praesent semper feugiat nibh sed pulvinar. Habitasse platea dictumst quisque sagittis. Tortor condimentum lacinia quis vel eros donec ac odio. Platea dictumst vestibulum rhoncus est pellentesque elit ullamcorper dignissim. Ultricies tristique nulla aliquet enim tortor at. Mi proin sed libero enim sed faucibus. Fames ac turpis egestas integer eget aliquet nibh. Potenti nullam ac tortor vitae purus faucibus ornare suspendisse. Cras semper auctor neque vitae tempus quam pellentesque nec. Quam lacus suspendisse faucibus interdum posuere. Neque laoreet suspendisse interdum consectetur libero id faucibus nisl tincidunt. Viverra tellus in hac habitasse. Nibh nisl condimentum id venenatis a condimentum vitae. Tincidunt dui ut ornare lectus."
    "Mattis aliquam faucibus purus in massa tempor nec feugiat nisl. Amet consectetur adipiscing elit ut aliquam purus. Turpis massa tincidunt dui ut ornare. Suscipit tellus mauris a diam maecenas sed enim ut sem. Id consectetur purus ut faucibus pulvinar elementum. Est velit egestas dui id. Felis imperdiet proin fermentum leo. Faucibus nisl tincidunt eget nullam non nisi est sit. Elit pellentesque habitant morbi tristique. Nisi lacus sed viverra tellus. Morbi tristique senectus et netus et malesuada fames. Id diam vel quam elementum pulvinar. Id nibh tortor id aliquet lectus. Sem integer vitae justo eget magna. Quisque sagittis purus sit amet volutpat consequat. Auctor elit sed vulputate mi sit amet. Venenatis lectus magna fringilla urna porttitor rhoncus dolor purus. Adipiscing diam donec adipiscing tristique risus nec feugiat in fermentum. Bibendum est ultricies integer quis."
    "Interdum posuere lorem ipsum dolor sit. Convallis convallis tellus id interdum velit. Sollicitudin aliquam ultrices sagittis orci a scelerisque purus. Vel quam elementum pulvinar etiam. Adipiscing bibendum est ultricies integer quis. Tellus molestie nunc non blandit. Sit amet porttitor eget dolor morbi non arcu. Scelerisque purus semper eget duis at tellus. Diam maecenas sed enim ut sem viverra. Vulputate odio ut enim blandit volutpat maecenas. Faucibus purus in massa tempor nec. Bibendum ut tristique et egestas quis ipsum suspendisse. Ut aliquam purus sit amet luctus venenatis lectus magna. Ac placerat vestibulum lectus mauris ultrices eros in cursus turpis. Feugiat pretium nibh ipsum consequat nisl vel pretium. Elit pellentesque habitant morbi tristique senectus et.",
    "Found at ETH's own Library", # ' will be a problem - it adds one extra deletion
    "State University of Alabama has a learning center called Levi Watkins",
    "The ETH library is not to be confused with Alabama State university's Levi Watkins Learning center",
    "ETH Library",
    "Alabma State Unversity",
    "Levi Wtkins Learning"
]


for test_sentence in test_sentences:
    parser = ContextualFuzzyMatcher(test_sentence)
    for test_text in test_texts:
        for match in parser.findall(test_text):
            print(match)

返回:

('', '', '', '', '', '', '', '', '', '', '', '', ' Alabama', 'State', 'university')
(' Levi', 'Watkins', 'Learning', '', '', '', '', '', '', '', '', '', '', '', '')
('', '', '', '', '', '', '', '', '', '', '', '', 'Alabma', 'State', 'Unversity')
('Levi', 'Wtkins', 'Learning', '', '', '', '', '', '', '', '', '', '', '', '')
(' ETH', 'library')
('ETH', 'Library')

我完全意识到这离完美的解决方案还很遥远,而且我的示例很少而且没有真正的代表性——但也许通过调整配置并进行大量实际测试,它可能能够涵盖相当多的不会产生太多误报的情况。此外,由于它是基于类的,因此可以针对不同的来源进行不同的继承和配置——也许在科学文本中,最大字距 1 就足够了,在报纸文章中可能需要 3,等等。

于 2018-06-18T15:06:29.977 回答
2

首先,您的距离分数需要根据数据库条目和/或输入的长度进行调整。5 距离对 10 个字符的表达式比 5 距离对 100 个字符的表达式差得多。

但是您的方法的主要问题是普通的 Levenshtein 不是子字符串匹配算法。它将一个字符串的所有内容与另一个字符串的所有内容进行比较。案例 (1) 中的大距离是由于数据库表达式中有大量不在输入表达式中的单词。

为了解决这个问题,您最好使用可以匹配子字符串的算法,例如 Fuzzy Bitap 或 Smith–Waterman。

如果你必须使用 Levenshtein 或类似的,你可能想用它来比较单词和单词,然后根据匹配单词的数量和匹配的质量生成一些分数。

于 2018-06-18T12:14:31.217 回答
1

您可以尝试使用归一化的 levenshtein 距离:

李玉健,刘波,“规范化的 Levenshtein 距离度量,” IEEE Transactions on Pattern Analysis and Machine Intelligence,vol。29,没有。6,第 1091-1095 页,2007 年 6 月,doi:10.1109/TPAMI.2007.1078 http://www.computer.org/csdl/trans/tp/2007/06/i1091-abs.html

他们建议标准化 levenshtein 距离。通过这样做,在比较长 10 的序列时,较长两个权重的序列中一个字符的差异比相同的差异更大。

于 2013-11-25T07:15:41.650 回答
-1

关键字计数

您还没有真正定义为什么您认为选项一是“更接近”的匹配,至少在任何算法意义上都没有。似乎您的期望是基于选项一比选项二具有更多匹配关键字的概念,那么为什么不只根据每个字符串中的关键字数量进行匹配呢?

例如,使用 Ruby 2.0:

string1 = 'Levi Watkins Learning Center - Alabama State University'
string2 = 'ETH Library'
strings = [str1, str2]

keywords  = 'Alabama University'.split
keycount  = {}

# Count matching keywords in each string.
strings.each do |str|
  keyword_hits  = Hash.new(0)
  keywords.each { |word| keyword_hits[word] += str.scan(/#{word}/).count }
  keyword_count = keyword_hits.values.reduce :+
  keycount[str] =  keyword_count
end

# Sort by keyword count, and print results.
keycount.sort.reverse.map { |e| pp "#{e.last}: #{e.first}" }

这将打印:

“2:Levi Watkins 学习中心 - 阿拉巴马州立大学”
“0:ETH 图书馆”

这符合您对语料库的期望。您可能希望使用其他算法对结果进行额外的传递以优化结果或打破平局,但这至少应该让您指向正确的方向。

于 2013-11-23T14:08:28.477 回答