python代码需要很长时间才能完成

时间:2016-03-07 12:20:53

标签: python performance multiprocessing processing-efficiency coding-efficiency

让我在前面,我编写了有趣的代码,这是我在业余时间过去几天一直在努力的代码挑战。挑战在于我给出了一堆由空格(文档)分隔的单词,然后是列表中的一些搜索项。我必须在文档中找到那些searchTerms最接近的位置。基本上,找到包含所有searchTerms并输出该子集的文档的最小子集。到目前为止,我的功能似乎正在我的系统上工作。但是,当我上传时,我被告知我的算法执行时间太长。我的思考过程是在文档中找到searchTerm的每个实例,然后对它运行itertools.product()。然后我测试每一个,以根据索引值确定哪一个是最短的。以下是我到目前为止的情况:

def answer(document, searchTerms):
    from itertools import product

    #build a list of the input document
    document = document.split()

    index = []
    #find all indexes for the searchTerms and build a list of lists 
    for w in searchTerms:
        index.append([i for i,x in enumerate(document) if x == w])

    #build iterator of all possible combinations of indexes for each of the searchTerms
    combinations = product(*index)

    #recover memory
    del index

    #build tuple of minimum distance between all search terms
    shortest  = min(((max(x) - min(x),x) for x in combinations),key=lambda x: x[0])

    return (' '.join(document[min(shortest[1]):max(shortest[1])+1]))

我尝试使用多处理来加速我的代码部分,但还没有完全正确的语法。例如:

from multiprocessing import Pool
p = Pool(processes=2)
shortest = p.map(min_max,combinations)

def min_max(combinations):
    return min(((max(x) - min(x),x) for x in combinations))

结果:

Traceback (most recent call last):
File "./searchTerms2.py", line 65, in <module>
print (answer(document,searchTerms))
File "./searchTerms2.py", line 45, in answer
shortest = p.map(min_max,combinations)
File "/usr/lib/python2.7/multiprocessing/pool.py", line 251, in map
return self.map_async(func, iterable, chunksize).get()
File "/usr/lib/python2.7/multiprocessing/pool.py", line 567, in get
raise self._value
TypeError: 'int' object is not iterable

任何指针都将非常感激。有没有更好的方法来解决这个问题?是否有哪些方面可以提高效率?

- EDIT-- 对问题的进一步解释:

document = 'this is a song that never ends it goes on and on my friend some people started singing it not knowing what it was and they will continue singing it forever just because this is the song'

searchTerms = ['this', 'goes','on']

应该导致:

'this is a song that never ends it goes on'

这适用于我当前的算法,但如果给出更大的文档和searchTerms,则不够快。我希望这更清楚......

我一直在为我的代码计时,看起来我最大的表现来自:

shortest  = min(((max(x) - min(x),x) for x in combinations),key=lambda x: x[0])

当我增加'document'中的单词数量并在'searchTerms'中添加额外的searchTerms时,我看到该行上出现了很大的影响。其他一切与我所知道的差别很小......

2 个答案:

答案 0 :(得分:0)

代码的主要减速来自于您在不同单词之间寻找所有索引组合的事实。显然,大多数这些组合在最短的时间内甚至不具备远程资格。这是一种应该运行得更快的算法:

  1. 查找所有搜索字词的索引,以搜索字词(正如您所做)分隔
  2. 使用最少匹配的字词作为基础。
  3. 对于基本术语的每次出现,找到最接近的每个术语(请注意,这比计算每个组合的距离要快得多)。最近邻居的最大传播距离是候选人的长度。
  4. 找到最短的候选人并返回。
  5. 此版本使用字典理解和MyPlugin参数key。但是,它不使用min模块之外的任何内容。下面的代码在Python 2.7和3.5下运行:

    __builtins__

    如果def answer(document, searchTerms): #build a list of the input document document = document.split() # construct list of indices of occurrences for each term indices = {w: [i for i,x in enumerate(document) if x == w] for w in searchTerms} # find the least frequent term and isolate it leastFrequent = min(indices.keys(), key=lambda x: len(indices[x])) loopIndex = indices[leastFrequent] del indices[leastFrequent] # for each element of leastFrequent, compute the nearest distance to each other item candidates = [None] * len(loopIndex) for index, element in enumerate(loopIndex): neighbors = [None] * len(indices) # find the distance to the nearest neighbor in each other list for ind, term in enumerate(indices): neighbors[ind] = min(indices[term], key=lambda x, e=element: abs(x - e)) # the run length is the maximum of the maximum and element minus the minimum of the minimum and element start = min(min(neighbors), element) end = max(max(neighbors), element) + 1 length = end - start candidates[index] = length, start, end # get the shortest candidate segment winner = min(candidates, key=lambda x: x[0]) return ' '.join(document[winner[1]:winner[2]]) 个搜索字词各自(几何)平均s次出现,则此算法将在大约k时间内运行。 O(k * s * k) = O(s * k^2)的因素来自k上的循环以及其中element的调用。系数min来自k上的循环。通过将最不频繁的元素作为基础,我们将显着减少其中一个term项。特别是对于其中一个术语只出现一次的情况,它保证在每个可能的组合中,因此外部循环只运行一次。

    为了进行比较,您的实现使用k,它生成itertools.product嵌套循环,每个循环运行s次迭代。这使得大约k运行时。

答案 1 :(得分:0)

我一直在考虑这个问题一天,我发现它非常有趣。它有助于将“文档”视为一条线,将每个“字”视为一条线。然后,任何解决方案都是一个窗口/范围,覆盖该行的一部分,左侧(开始)和右侧(结束)。

尝试解决方案: Mad Physicist的解决方案不起作用的原因是它从这一行上的一个点开始,并将这个点和每个其他点之间的距离视为正交,当它们实际包含大量重叠时。它仅选择每个匹配搜索词的最近点,这限制了搜索的解空间,因此错过了一些解决方案。找到一个例子并不难,例如:

document = 'a x x d x x x a j x x'
searchTerms = 'a d j'.split()

d开头,然后选择最接近的a,当进一步a将产生更短的整体解决方案时。

暴力解决方案 您在问题中的解决方案使用product生成可能的解决方案并检查每个解决方案。对于像你发布的例子这样的小问题,这很好,实际上非常快,但随着doc的长度增加,特别是搜索术语的数量,product的组合数量迅速增长。

新解决方案: 我们能做的第一件事就是要意识到任何不包括最小和最大索引之间所有点的组合都是无效的。这消除了很多组合,使得你实际上只选择(开始结束)点的组合,无论搜索词的数量是多少。

虽然可能有一些奇特的数学公式来生成这些组合,但我采取了不同的方法......

如果您将每个搜索词的索引分别视为从最低索引到最高索引的迷你窗口,很明显解决方案的 end 索引的下限是最大值开始所有这些范围的索引。这是因为 end 索引较低的任何窗口都不会包含此搜索词。 start 索引的下限只是最低匹配索引。

此(开始结束)必须是一个解决方案,因此我们将其用作初始猜测,然后按照以下步骤操作:

它有助于创建所有匹配索引的平面列表并在此列表上工作,因为所有其他索引都无关紧要。在这种情况下start = 0.

start 索引高级到下一个匹配索引(平面列表中的start++)。这会将最左边的匹配弹出窗口。获取不小于 start 的下一个匹配的索引。如果此索引已经在该范围内,那么我们已经减少了冗余匹配并获得了另一个解决方案。如果此索引位于右侧范围之外,请移动 end 以展开范围以再次包含此匹配项。如果没有更多可用的匹配,那么我们已经没有解决方案了。

重复此过程,直到没有更多解决方案,跟踪哪个解决方案产生最短 range = end - start。这是最终解决方案。

<强>测试

为了在测试中获得更多变化并验证我的解决方案是否生成与原始解决方案相同的解决方案,我随机抓取k中的document个搜索字词:

import random
terms = random.sample(document.split(), random.randint(3,5))
print(terms)
s1 = answer_product(document, terms)
s2 = answer_window(document, terms)

assert s1 == s2

然后尝试做一个我用过的简单基准:

import timeit
N = 1000
for j in xrange(2,8):
    terms = random.sample(document.split(), j)
    print(N,j)
    print('window: %s s'%timeit.timeit(lambda: answer_window(document*4, terms), number=N))
    print('product: %s s'%timeit.timeit(lambda: answer_product(document*4, terms), number=N))

在我的计算机上,对于N=1000,k=2的小案例,它们在t~=0.03s周围非常快。但是,当k增长到k=7时,answer_product增长到t>20s所需的时间answer_window仍为t~=0.03s。请注意,由于我没有用于测试的实际“文档”,我只是将示例乘以4以增加搜索量。

╔═════╦═══╦═══════════════════╦════════════════════╦═══════╗ ║ N ║ k ║ answer_window (s) ║ answer_product (s) ║ p/w ║ ╠═════╬═══╬═══════════════════╬════════════════════╬═══════╣ ║ 1e3 ║ 2 ║ 0.0231 ║ 0.0347 ║ 1.5 ║ ║ 1e3 ║ 3 ║ 0.0227 ║ 0.058 ║ 2.55 ║ ║ 1e3 ║ 4 ║ 0.025 ║ 0.242 ║ 9.68 ║ ║ 1e3 ║ 5 ║ 0.0326 ║ 3.044 ║ 93.4 ║ ║ 1e3 ║ 6 ║ 0.035 ║ 11.55 ║ 330 ║ ║ 1e3 ║ 7 ║ 0.0299 ║ 23.82 ║ 797 ║ ║ 1e5 ║ 2 ║ 2.2 ║ 2.524 ║ 1.15 ║ ║ 1e5 ║ 3 ║ 2.195 ║ 2.743 ║ 1.25 ║ ║ 1e5 ║ 4 ║ 3.272 ║ 46.51 ║ 14.2 ║ ║ 1e5 ║ 5 ║ 3.74 ║ 67.71 ║ 18.1 ║ ║ 1e5 ║ 6 ║ 3.52 ║ 1137 ║ 323 ║ ║ 1e5 ║ 7 ║ 3.98 ║ 4519 ║ 1135 ║ ╚═════╩═══╩═══════════════════╩════════════════════╩═══════╝

<强>代码:

def answer_window(doc, terms):
    doc = doc.split()
    # create a grouping of indices by match and a flat array of all match
    # indices
    index = {w:[] for w in terms}
    indices = []
    j = 0
    for (i, w) in enumerate(doc):
        if w in index:
            # save real doc indices in flat array and use indices into that
            # array to simplify stepping.  both are automatically ordered
            indices.append(i)
            index[w].append(j)
            j += 1
    # find the maximum leftmost match index.  this is the lower bound on the
    # right side of the solution window
    highest_min = max(v[0] for v in index.values())

    # start with lowest minimum index (first) and highest minimum index (which
    # is the lower bound on the right side). this must be a solution.
    # then look for a shorter one by stepping the left side, replacing lost
    # matches from the right (expanding when necessary) until the left cannot
    # be advanced anymore.  this will cover all possible solution windows and the
    # one with the shortest length is saved
    start, end = 0, highest_min
    sol = start, end
    dsol = indices[sol[1]]-indices[sol[0]]
    while True:
        # pop leftmost match
        pop = doc[indices[start]]
        start += 1
        # need to make sure we still have the match we popped in the range
        for j in index[pop]:
            if j >= start:
                # another copy to the right!
                if j > end:
                    # must expand end to include the replacement
                    end = j
                    if indices[end]-indices[start] < dsol:
                        # new window is shorter than sol
                        sol = start, end
                        dsol = indices[sol[1]]-indices[sol[0]]
                elif indices[end]-indices[start] < dsol:
                    # the replacement is already inside the range, and moving
                    # the left side made the window smaller than sol
                    sol = start,end
                    dsol = indices[sol[1]]-indices[sol[0]]
                break # done with this pop
            else:
                # this match is left of our window
                pass
        else:
            # found no replacement, can't shrink left side anymore so we are
            # out of solutions
            break
    return (' '.join(doc[indices[sol[0]]:indices[sol[1]]+1]))