【问题标题】:Recursive python matching algorithm based on subsets working too slowly基于子集的递归python匹配算法工作太慢
【发布时间】:2020-05-26 09:44:23
【问题描述】:

我正在构建一个网络应用程序,以根据标签表示的兴趣将考虑间隔年的高中学生与已参加间隔年的学生进行匹配。原型在covidgapyears.com。我从来没有写过匹配/推荐算法,所以尽管人们提出了诸如协同过滤和关联规则挖掘之类的建议,或者适应稳定的婚姻问题,但我认为这些都行不通,因为它是一个小数据集(几百个用户现在,很快几千)。所以我用常识编写了自己的算法。

它本质上是接收学生感兴趣的标签列表,然后搜索与那些已经度过间隔年并在网站上注册的人完全匹配的这些标签(他们也注册时选择的标签)。如下所示,精确匹配是用户指定的标签全部包含在某个配置文件中(即,是一个子集)。如果它无法找到与用户输入的所有标签的完全匹配,它将检查标签列表本身的所有 n-1 长度子集,以查看是否有任何选择性较低的查询匹配。它递归地执行此操作,直到找到至少 3 个匹配项。虽然它适用于小标签选择(最多 5-7),但对于较大的标签选择(7-13)会变慢,需要几秒钟才能返回结果。 When 11-13 tags are selected, hits a Heroku error due to worker timeout.

我做了一些测试,将变量放入算法中以计算计算量,似乎当它深入递归堆栈时,它每次检查几百个子集(查看该子集是否有精确匹配,如果有,则将其添加到结果列表中以输出),并且当您添加一个标签时,计算总数加倍(对于越来越多的标签,它进行了 54、150、270、500、1000、1900、3400 次操作) .确实,每个深度都有几百个子集。但是我写的exactMatches是O(1)(没有迭代),除了像IF这样的其他O(1)操作之外,子集循环内的FOR最多会经历大约10次。这与每次数千次计算的测量结果一致。

这并不让我感到惊讶,因为选择和迭代所有子集似乎不会变得更难,但我的问题是为什么它这么慢,尽管只进行了几千次计算。我知道我的计算机在 GHz 下运行,并且我希望 Web 服务器是相似的,所以几千次计算肯定会接近瞬时?我错过了什么,我该如何改进这个算法?我应该研究其他任何方法吗?

# takes in a list of length n and returns a list of all combos of subsets of depth n
def arbSubsets(seq, n):
    return list(itertools.combinations(seq, len(seq)-n))

# takes in a tagsList and check Gapper.objects.all to see if any gapper has all those tags
def exactMatches(tagsList):
    tagsSet = set(tagsList)
    exactMatches = []
    for gapper in Gapper.objects.all():
        gapperSet = set(gapper.tags.names())
        if tagsSet.issubset(gapperSet):
            exactMatches.append(gapper)
    return exactMatches

# takes in tagsList that has been cleaned to remove any tags that NO gappers have and then checks gapper objects to find optimal match
def matchGapper(tagsList, depth, results):
    # handles the case where we're only given tags contained by no gappers
    if depth == len(tagsList):
        return []
    # counter variable is to measure complexity for debugging
    counter += 1
    # we don't want too many results or it stops feeling tailored
    upper_limit_results = 3
    # now we must check subsets for match
    subsets = arbSubsets(tagsList, depth)
    for subset in subsets:
        counter += 1
        matches = exactMatches(subset)
        if matches:
            for match in matches:
                counter += 1
                # new need to check because we might be adding depth 2 to results from depth 1
                #  which we didn't do before, to make sure we have at least 3 results
                if match not in results:
                    # don't want to show too many or it doesn't feel tailored anymore
                    counter += 1
                    if len(results) > upper_limit_results: break
                    results.append(match)
    # always give at least 3 results
    if len(results) > 2:
        return results
    else:
        # check one level deeper (less specific) into tags if not enough gappers that match to get more results
        counter += 1
        return matchGapper(tagsList, depth + 1, results)

# this is the list of matches we then return to the user 
matches = matchGapper(tagsList, 0, [])

【问题讨论】:

  • intertools.combinations 的时间复杂度是 O(n!),n 是数组的长度。这是我猜的主要问题
  • 不要认为这是问题所在。在 Ivaylo 的回答中查看我对 itertools 的评论。
  • 要进行准确计数,counter 必须是全局的。在matchGapper 之外定义counter 并在该函数的顶部包含global counter 行。然后在程序终止后打印counter。如果匹配不严格,您可以考虑的一种方法是“禁忌搜索”。

标签: python algorithm time-complexity match subset


【解决方案1】:

您似乎没有执行几百个计算步骤。事实上,每个深度都有数百个选项,因此您不应该添加,而是乘以每个深度的步数来估计解决方案的复杂性。

此外,这句话:This or adapting the stable marriage problem, I don't think any of those will work because it's a small dataset 显然也不正确。尽管这些算法在一些非常简单的情况下可能有点过头了,但它们仍然有效并且适用于这些情况。

【讨论】:

  • 确实每个深度都有几百个子集。但是我写的exactMatches是O(1)(没有迭代),除了像IF这样的其他O(1)操作之外,子集循环内的FOR最多会经历大约10次。然后,随后的几千次计算的理论估计与我在调试时在 matchGappers 中的每一步之后输入的计算计数器变量一致。我认为我在这方面是对的,至少。所以它并没有真正回答我的问题。
  • 另外,wrt 现有的算法,是的,我不知道如何把它变成一个稳定的婚姻问题,所以我不知道如何适应它,但如果可以的话,它会起作用。但是,其他建议是统计 (ML) 方法,它们会对小型数据集产生不良结果,因此不起作用。
  • @TanishqKumar 你能分享一下你是如何实现计数器变量的吗?经验证据表明不可能是几千步
  • 添加为计数器变量。也许我得到几千作为大标签输入的计数器的事实表明瓶颈在 itertools.combinations 或 .issubset() 中,我没有测量内部计算的函数?
  • itertools.combinations 绝对不是一个简单的运算,而是一个指数运算。 .issubset 也是(预期的)线性而不是常数。
【解决方案2】:

好的,所以在摆弄了很多计时器之后,我想通了。匹配时有几个函数在起作用:exactMatches、matchGapper 和 arbSubset。当我将计数器放入一个全局变量并测量操作时(以 我的代码 正在执行的行来衡量,它对于大型输入来说大约为 2-10K(大约 10标签))。

确实,返回子集列表的 arbSubset 乍一看似乎是一个合理的瓶颈。但如果你仔细观察,我们 1) 处理少量标签(10-50 的顺序),更重要的是,2) 我们只在递归 matchGapper 时调用 arbSubset,这最多只发生大约 10 次,因为 tagsList只能是 10 左右(10-50 的顺序,如上)。当我检查生成 arbSubsets 所需的时间时,它是 2e-5 的顺序。因此,生成任意大小的子集的总时间仅为 2e-4。换句话说,不是网络应用程序中 5-30 秒等待时间的来源。

除此之外,知道 arbSubset 只被调用了 10 次,而且速度很快,并且知道在我的代码中最多只进行了大约 10K 次计算 em> 开始变得很清楚,我必须使用一些开箱即用的功能,我不知道——比如 set() 或 .issubset() 或类似的东西——这需要大量的计算时间,并执行多次。在更多的地方添加一些计数器,很明显,exactMatch() 占发生的所有计算的 95-99% 左右(如果我们必须检查不同大小的子集的所有组合是否为 exactMatches,这是可以预期的)。

因此,在这一点上,问题归结为这样一个事实:exactMatch 在实现时大约需要 0.02 秒(根据经验),并且被调用了数千次。因此,我们可以尝试使其速度提高几个数量级(它已经非常理想),或者采用另一种不涉及使用子集查找匹配的方法。我的一个朋友建议使用所有标签组合(因此 2^len(tagsList) 键)创建一个字典,并将它们设置为具有该确切组合的已注册配置文件列表。这样,查询只是遍历一个(巨大的)字典,可以快速完成。欢迎任何其他建议。

【讨论】:

    猜你喜欢
    • 2021-12-01
    • 2011-07-05
    • 2015-11-04
    • 2015-11-03
    • 1970-01-01
    • 2019-04-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多