Python, огромная проблема производительности итерации - PullRequest
9 голосов
/ 21 декабря 2009

Я делаю итерацию по 3 словам, каждое длиной около 5 миллионов символов, и я хочу найти последовательности из 20 символов, которые идентифицируют каждое слово. То есть я хочу найти все последовательности длиной 20 в одном слове, которое является уникальным для этого слова. Моя проблема в том, что написанный мною код выполняется очень долго. Я даже не выполнил ни одного слова, запустив свою программу за ночь.

Приведенная ниже функция берет список, содержащий словари, в которых каждый словарь содержит каждое возможное слово из 20 и его местоположение из одного из 5 миллионов длинных слов.

Если у кого-то есть идея, как оптимизировать это, я был бы очень благодарен, я понятия не имею, как продолжить ...

вот пример моего кода:

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 ]

10 голосов
/ 21 декабря 2009
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 ) имеет значение O (n) (n - длина каждого слова), и вы должны использовать set(slices(..)) (с операциями над множествами, такими как разность), чтобы получить кусочки уникальный для всех слов (пример ниже). Вы также можете написать функцию для возврата набора, если вы не хотите отслеживать местоположения. Использование памяти будет высоким (хотя по-прежнему O (n), просто большой фактор), возможно, будет уменьшено (хотя и ненамного, если длина всего 20) с помощью специального класса «lazy slice» , который хранит базу последовательность (строка) плюс начало и конец (или начало и длина).

Печать уникальных ломтиков:

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)

В тестовом сценарии, более близком к вашим условиям, с использованием случайно сгенерированных слов длиной 5 м и длиной фрагмента 20, использование памяти было настолько высоким, что мой тестовый сценарий быстро превысил предел основной памяти в 1 ГБ и начал перебивать виртуальную память. В этот момент Python потратил очень мало времени на процессор, и я его убил. Сокращение либо длины фрагмента, либо длины слова (так как я использовал совершенно случайные слова, которые уменьшают дубликаты и увеличивает использование памяти), чтобы уместиться в основную память, и это заняло меньше минуты. Эта ситуация плюс 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]
0 голосов
/ 29 декабря 2009

Давайте попробуем улучшить Отличный ответ Роджера Пейта .

Во-первых, давайте сохраним наборы вместо словарей - они так или иначе управляют уникальностью.

Во-вторых, поскольку мы, скорее всего, исчерпаем память быстрее, чем время процессора (и терпение), мы можем пожертвовать эффективностью процессора ради эффективности памяти. Так что, возможно, попробуйте только 20-е, начиная с одной конкретной буквы. Для ДНК это снижает требования на 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 для ДНК) или каждые 3 (64 прохода) и т. Д.

0 голосов
/ 23 декабря 2009

Позвольте мне построить Ответ Роджера Пейта . Если проблема с памятью, я бы предложил вместо использования строк в качестве ключей к словарю использовать хеш-значение строки. Это позволило бы сэкономить на хранении дополнительной копии строк в качестве ключей (в худшем случае, в 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

(Если вы действительно хотите проявить фантазию, вы можете использовать скользящий хеш , хотя вам нужно будет изменить функцию.)

Теперь мы можем объединить все хэши:

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)

(Я пытался расширить его, чтобы сделать все три. Это возможно, но осложнения отвлекли бы от алгоритма.)

Имейте в виду, я не проверял это. Также, возможно, вы можете многое сделать для упрощения кода, но алгоритм имеет смысл. Сложная часть - это выбор хеша. Слишком много столкновений, и вы ничего не получите. Слишком мало, и вы столкнетесь с проблемами памяти. Если вы имеете дело только с базовыми кодами ДНК, вы можете хешировать 20-символьную строку в 40-битное число и при этом не иметь коллизий. Таким образом, кусочки занимают почти четверть памяти. В ответе Роджера Пейта это сэкономит примерно 250 МБ памяти.

Код все еще O (N ^ 2), но константа должна быть намного ниже.

0 голосов
/ 22 декабря 2009

Вы говорите, что у вас "слово" длиной 5 миллионов символов, но мне трудно поверить, что это слово в обычном смысле.

Если вы можете предоставить больше информации о ваших входных данных, может быть доступно конкретное решение.

Например, английский текст (или любой другой письменный язык) может быть достаточно повторяющимся, чтобы можно было использовать trie . В худшем случае, однако, он исчерпал бы память, создавая все 256 ^ 20 ключей. Знание ваших вкладов имеет все значение.


редактировать

Я взглянул на некоторые данные генома, чтобы увидеть, как сложилась эта идея, используя жестко закодированное отображение [acgt] -> [0123] и 4 дочерних элемента на узел trie.

  1. аденовирус 2 : 35 937 п.н. -> 35 899 отдельных последовательностей по 20 оснований с использованием 469 339 три-узлов

  2. энтеробактерии фага лямбда : 48 502 п.н. -> 40 921 различных последовательностей по 20 оснований с использованием 529 384 узлов три.

Я не получил никаких коллизий ни внутри, ни между двумя наборами данных, хотя, возможно, в ваших данных больше избыточности и / или дублирования. Вы должны попробовать это, чтобы увидеть.

Если вы получили полезное количество коллизий, вы можете попробовать объединить три входа, построить один три, записать происхождение каждого листа и обрезать коллизии из этого три по ходу движения.

Если вы не можете найти способ обрезки клавиш, попробуйте использовать более компактное представление. Например, вам нужно только , чтобы хранить [acgt] / [0123], 2 бита, что может сэкономить ваше пространство за счет немного более сложного кода.

Я не думаю, что вы можете просто перебить это - вам нужно найти какой-то способ уменьшить масштаб проблемы, и это зависит от вашего знания предметной области.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...