基于子集工作过慢的递归python匹配算法

基于子集工作过慢的递归python匹配算法,python,algorithm,time-complexity,match,subset,Python,Algorithm,Time Complexity,Match,Subset,我正在构建一个web应用程序,根据标签所表示的兴趣,将考虑空档年的高中生与参加空档年的学生进行匹配。一个原型正在研制中。我从来没有写过匹配/推荐算法,所以尽管人们提出了协作过滤和关联规则挖掘之类的建议,或是适应稳定婚姻问题,但我认为这些都不管用,因为这是一个小数据集,现在只有几百个用户,很快就会有几千个用户。所以我用常识写了我自己的alg 它本质上是接收一个学生感兴趣的标签列表,然后搜索那些标签与某个人的精确匹配,该人在空档年进行了注册,并且在注册时也选择了标签。下面给出的exactMatch是

我正在构建一个web应用程序,根据标签所表示的兴趣,将考虑空档年的高中生与参加空档年的学生进行匹配。一个原型正在研制中。我从来没有写过匹配/推荐算法,所以尽管人们提出了协作过滤和关联规则挖掘之类的建议,或是适应稳定婚姻问题,但我认为这些都不管用,因为这是一个小数据集,现在只有几百个用户,很快就会有几千个用户。所以我用常识写了我自己的alg

它本质上是接收一个学生感兴趣的标签列表,然后搜索那些标签与某个人的精确匹配,该人在空档年进行了注册,并且在注册时也选择了标签。下面给出的exactMatch是指用户指定的标记都包含在某个概要文件中,即是一个子集。如果无法找到与用户输入的所有标记完全匹配的标记,它将检查标记列表本身的所有n-1长度子集,以查看是否有任何选择性较差的查询具有匹配项。它递归地执行此操作,直到找到至少3个匹配项。虽然它可以很好地用于小标记选择(最多5-7个),但对于较大的标记选择(7-13个),它会变慢,需要几秒钟才能返回结果。当选择11-13个标记时,由于工作超时,会出现Heroku错误


我做了一些测试,在算法中加入变量来计算,似乎当它深入递归堆栈时,它每次都会检查几百个子集,看看是否有与该子集的精确匹配,如果有,将其添加到结果列表以输出,当你再添加一个标签时,计算的总次数会翻倍。对于越来越多的标签,它进行了5415027050010001900,3400次运算。每个深度上确实有几百个子集。但是exactMatches是O1,正如我所写的,没有迭代,除了其他O1操作,比如IF,子集循环中的FOR最多会经历10次。这与每次数千次计算的测量结果一致

这并没有让我感到惊讶,因为选择和迭代所有子集似乎变得不难,但我的问题是,尽管只做了几千次计算,但为什么它如此缓慢。我知道我的电脑是GHz运行的,我希望网络服务器也是类似的,所以几千次计算肯定是瞬间完成的吧?我遗漏了什么?如何改进该算法?我还应该研究其他方法吗

# 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, [])

看起来你没有做几百个计算步骤。事实上,对于每个深度,您有几百个选项,因此您不应该添加,而应该乘以每个深度的步数来估计解决方案的复杂性


另外,这句话:这或适应稳定的婚姻问题,我认为这些都不管用,因为这是一个小数据集,显然也是不正确的。虽然这些算法对于一些非常简单的情况来说可能有些过分,但它们仍然有效,并且会对它们起作用。

好吧,在对计时器进行了大量的修改之后,我已经找到了它。匹配时有几个功能:exactMatches、matchGapper和arbSubset。当我将计数器放入一个全局变量中,并以代码执行行的形式测量操作时,对于大约10个标记的大输入,它大约有2-10K

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

因此,撇开这一点不谈,我知道arbSubset只被调用了10次,而且调用速度很快,而且知道我的代码中最多只进行了10K的计算,我开始清楚地意识到我必须使用一些开箱即用的函数,我不知道像set或.issubset之类的东西需要花费大量的时间来计算,并且执行了很多次。在更多的地方添加一些计数器,很明显,exactMatch占所有计算的95-99%,如果我们必须检查各种大小的子集的所有组合以获得exactMatch,这是意料之中的

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

intertools.combinations的时间复杂性已开启!,n是数组的长度。这是主要问题,我想这不是问题所在。请参阅我在Ivaylo回答中对itertools的评论。要进行准确计数,计数器必须是全局的。在matchGapper之外定义计数器,并在该函数顶部包含行、全局计数器。然后在程序终止后打印计数器。如果匹配不是严格的,你可以考虑的一种方法是禁忌搜索。每个深度都有几百个子集是正确的。但是exactMatches是O1,正如我所写的,没有迭代,除了其他O1操作,比如IF,子集循环中的FOR最多会经历10次。接下来的几千次计算的理论估计与调试时在matchGappers中的每一步之后输入的计算计数器变量一致。至少在这方面我是对的。所以它并没有真正回答我的问题。另外,wrt现有的算法,是的,我不知道如何把它变成一个稳定的婚姻问题,所以我不知道如何适应它,但如果我可以,它会工作。然而,其他的建议是统计ML方法,它会给小数据集带来不好的结果,因此不起作用。@TanishqKumar你能分享一下你是如何实现计数器变量的吗?经验证据表明,它不可能是几千步加上作为反变量。也许事实上,我得到了几千个作为大标记输入的计数器,这表明瓶颈在itertools.combines或.issubset中,我没有测量过的函数?itertools.combines的内部计算绝对不是简单的,而是指数运算。issubset也应为线性而不是常数。