Python,巨大的迭代性能问题

时间:2009-12-21 18:16:07

标签: python iteration bioinformatics

我正在通过3个单词进行迭代,每个单词大约有500万个字符,我希望找到标识每个单词的20个字符的序列。也就是说,我想在一个单词中找到长度为20的所有序列,这个序列对于该单词是唯一的。我的问题是我写的代码需要很长时间才能运行。我甚至没有完成一个单词来运行我的程序过夜。

下面的函数采用一个包含字典的列表,其中每个字典包含20个可能的单词,以及500万个单词之一的位置。

如果有人知道如何优化这一点我会非常感激,我不知道如何继续......

这是我的代码示例:

def findUnique(list):
    # Takes a list with dictionaries and compairs each element in the dictionaries
    # with the others and puts all unique element in new dictionaries and finally
    # puts the new dictionaries in a list.
    # The result is a list with (in this case) 3 dictionaries containing all unique
    # sequences and their locations from each string.
    dicList=[]
    listlength=len(list)
    s=0
    valuelist=[]
    for i in list:
        j=i.values()
        valuelist.append(j)
    while s<listlength:
        currdic=list[s]
        dic={}
        for key in currdic:
            currval=currdic[key]
            test=True
            n=0
            while n<listlength:
                if n!=s:
                    if currval in valuelist[n]: #this is where it takes to much time
                        n=listlength
                        test=False
                    else:
                        n+=1
                else:
                    n+=1
            if test:
                dic[key]=currval
        dicList.append(dic)
        s+=1
    return dicList

4 个答案:

答案 0 :(得分:10)

def slices(seq, length, prefer_last=False):
  unique = {}
  if prefer_last: # this doesn't have to be a parameter, just choose one
    for start in xrange(len(seq) - length + 1):
      unique[seq[start:start+length]] = start
  else: # prefer first
    for start in xrange(len(seq) - length, -1, -1):
      unique[seq[start:start+length]] = start
  return unique

# or find all locations for each slice:
import collections
def slices(seq, length):
  unique = collections.defaultdict(list)
  for start in xrange(len(seq) - length + 1):
    unique[seq[start:start+length]].append(start)
  return unique

这个函数(当前在我的iter_util module中)是O(n)(n是每个单词的长度),你可以使用set(slices(..))(使用诸如差异之类的集合操作)来获得唯一的切片所有单词(例如下面的例子)。如果您不想跟踪位置,也可以编写函数来返回一个集合。内存使用率会很高(尽管仍然是O(n),只是一个很大的因素),可能会缓解(尽管长度不超过20),使用特殊的"lazy slice" class来存储基本序列(字符串)加上开始和停止(或开始和长度)。

打印独特切片:

a = set(slices("aab", 2)) # {"aa", "ab"}
b = set(slices("abb", 2)) # {"ab", "bb"}
c = set(slices("abc", 2)) # {"ab", "bc"}
all = [a, b, c]
import operator
a_unique = reduce(operator.sub, (x for x in all if x is not a), a)
print a_unique # {"aa"}

包括地点:

a = slices("aab", 2)
b = slices("abb", 2)
c = slices("abc", 2)
all = [a, b, c]
import operator
a_unique = reduce(operator.sub, (set(x) for x in all if x is not a), set(a))
# a_unique is only the keys so far
a_unique = dict((k, a[k]) for k in a_unique)
# now it's a dict of slice -> location(s)
print a_unique # {"aa": 0} or {"aa": [0]}
               # (depending on which slices function used)

在更接近您条件的测试脚本中,使用随机生成的5m字符和切片长度为20的单词,内存使用率非常高,以至于我的测试脚本很快达到了我的1G主内存限制并开始抖动虚拟内存。那时,Python花了很少的时间在CPU上,我杀了它。减少切片长度或字长(因为我使用完全随机的单词减少重复并增加内存使用)以适应主内存并且它运行不到一分钟。这种情况加上原始代码中的O(n ** 2)将永远存在,这也是算法时间和空间复杂性都很重要的原因。

import operator
import random
import string

def slices(seq, length):
  unique = {}
  for start in xrange(len(seq) - length, -1, -1):
    unique[seq[start:start+length]] = start
  return unique

def sample_with_repeat(population, length, choice=random.choice):
  return "".join(choice(population) for _ in xrange(length))

word_length = 5*1000*1000
words = [sample_with_repeat(string.lowercase, word_length) for _ in xrange(3)]
slice_length = 20
words_slices_sets = [set(slices(x, slice_length)) for x in words]
unique_words_slices = [reduce(operator.sub,
                              (x for x in words_slices_sets if x is not n),
                              n)
                       for n in words_slices_sets]
print [len(x) for x in unique_words_slices]

答案 1 :(得分:0)

你说你有500万个字符的“字”,但我发现很难相信这是一般意义上的一个字。

如果您可以提供有关输入数据的更多信息,则可能会提供特定的解决方案。

例如,英文文本(或任何其他书面语言)可能足够重复,trie可用。然而,在最坏的情况下,构建所有256 ^ 20密钥的内存将耗尽。了解您的输入会产生重大影响。


修改

我看了一些基因组数据,看看这个想法如何叠加,使用硬编码[acgt] - &gt;映射和每个节点4个孩子。

  1. 腺病毒2 :35,937bp - >使用469,339个trie节点的35,899个不同的20碱基序列

  2. 肠杆菌噬菌体λ:48,502bp - >使用529,384个trie节点的40,921个不同的20碱基序列。

  3. 我没有在两个数据集之内或之间发生任何冲突,尽管数据中可能存在更多冗余和/或重叠。你必须尝试看看。

    如果确实获得了有用数量的碰撞,您可以尝试将三个输入组合在一起,构建一个trie,记录每个叶子的原点并在出发时修剪trie中的碰撞。

    如果找不到修剪键的方法,可以尝试使用更紧凑的表示法。例如,您只需要需要 2位来存储[acgt] / [0123],这可能会以稍微复杂的代码为代价来节省空间。

    我认为你不能蛮力这个 - 你需要找到一些方法来减少问题的规模,这取决于你的领域知识。

答案 2 :(得分:0)

让我建立Roger Pate's answer。如果内存是个问题,我建议您不要使用字符串作为字典的键,而是可以使用字符串的散列值。这样可以节省将字符串的额外副本存储为密钥的成本(最坏的情况是,存储单个“单词”的20倍)。

import collections
def hashed_slices(seq, length, hasher=None):
  unique = collections.defaultdict(list)
  for start in xrange(len(seq) - length + 1):
    unique[hasher(seq[start:start+length])].append(start)
  return unique

(如果你真的想要花哨,可以使用rolling hash,但你需要更改功能。)

现在,我们可以结合所有哈希值:

unique = []  # Unique words in first string

# create a dictionary of hash values -> word index -> start position
hashed_starts = [hashed_slices(word, 20, hashing_fcn) for word in words]
all_hashed = collections.defaultdict(dict)
for i, hashed in enumerate(hashed_starts) :
   for h, starts in hashed.iteritems() :
     # We only care about the first word
     if h in hashed_starts[0] :
       all_hashed[h][i]=starts

# Now check all hashes
for starts_by_word in all_hashed.itervalues() :
  if len(starts_by_word) == 1 :
    # if there's only one word for the hash, it's obviously valid
    unique.extend(words[0][i:i+20] for i in starts_by_word.values())
  else :
    # we might have a hash collision
    candidates = {}
    for word_idx, starts in starts_by_word.iteritems() :
      candidates[word_idx] = set(words[word_idx][j:j+20] for j in starts)
    # Now go that we have the candidate slices, find the unique ones
    valid = candidates[0]
    for word_idx, candidate_set in candidates.iteritems() :
      if word_idx != 0 :
        valid -= candidate_set
    unique.extend(valid)

(我尝试将其扩展为完成所有三项。这是可能的,但并发症会影响算法。)

警告,我没有测试过这个。此外,您可以做很多事情来简化代码,但算法很有意义。困难的部分是选择哈希。太多的碰撞,你将无法获得任何东西。太少,你会遇到内存问题。如果您只处理DNA基本代码,则可以将20个字符的字符串散列为40位数字,但仍然没有冲突。因此切片将占据内存的近四分之一。这将在Roger Pate的答案中节省大约250 MB的内存。

代码仍然是O(N ^ 2),但常数应该低得多。

答案 3 :(得分:0)

让我们尝试改进Roger Pate's excellent answer

首先,让我们保留集而不是字典 - 无论如何它们都会管理唯一性。

其次,由于我们可能会耗尽内存,而不是耗尽CPU时间(以及耐心),因此为了提高内存效率,我们可能会牺牲CPU效率。所以也许只从一个特定的字母开始尝试20年代。对于DNA,这将需求降低了75%。

seqlen = 20
maxlength = max([len(word) for word in words])
for startletter in letters:
    for letterid in range(maxlength):
        for wordid,word in words:
            if (letterid < len(word)):
                letter = word[letterid]
                if letter is startletter:
                    seq = word[letterid:letterid+seqlen]
                    if seq in seqtrie and not wordid in seqtrie[seq]:
                        seqtrie[seq].append(wordid)

或者,如果那仍然是太多的记忆,我们可以通过每个可能的起始对(16个通过而不是4个用于DNA),或者每3个(64个通过)等。