Оптимизируя производительность моего приложения, я столкнулся с огромным узким местом производительности в нескольких строках (Python) кода.

У меня есть N токенов. каждому токену присваивается значение. Некоторые токены противоречат друг другу (например, токены 8 и 12 не могут «жить вместе»). Моя работа - найти k-лучшие токен-группы. Значение группы токенов - это просто сумма значений токенов в ней.

Наивный алгоритм (который я реализовал ...):

  1. найти все 2 ^ N перестановок токенов в группе токенов
  2. Устранить токен-группы, в которых есть противоречия
  3. Рассчитать стоимость всех оставшихся токен-групп
  4. Сортировать токен-группы по значению
  5. Выберите топ K токен-групп

Числа реального мира - мне нужны 10 лучших групп токенов из группы из 20 токенов (для которых я рассчитал 1 000 000 перестановок (!)), Суженных до 3500 непротиворечивых групп токенов. Это заняло 5 секунд на моем ноутбуке ...

Я уверен, что могу как-то оптимизировать шаги 1 + 2, генерируя только непротиворечивые группы токенов.

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

Мой фактический код:

all_possibilities = sum((list(itertools.combinations(token_list, i)) for i in xrange(len(token_list)+1)), [])
all_possibilities = [list(option) for option in all_possibilities if self._no_contradiction(option)] 
all_possibilities = [(option, self._probability(option)) for option in all_possibilities]
all_possibilities.sort(key = lambda result: -result[1]) # sort by descending probability

Пожалуйста помоги?

Таль .

3
Tal Weiss 28 Июн 2010 в 17:37

4 ответа

Лучший ответ

Действительно простой способ получить все непротиворечивые токен-группы:

#!/usr/bin/env python

token_list = ['a', 'b', 'c']

contradictions = {
    'a': set(['b']),
    'b': set(['a']),
    'c': set()
}

result = []

while token_list:
    token = token_list.pop()
    new = [set([token])]
    for r in result:
        if token not in contradictions or not r & contradictions[token]:
            new.append(r | set([token]))
    result.extend(new)

print result
2
Florian Diesch 28 Июн 2010 в 15:56

Следующее решение генерирует все максимальные непротиворечивые подмножества, используя тот факт, что нет смысла пропускать элемент из решения, если он не противоречит другому элементу в решении.

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

def solve(tokens, contradictions):
   if not tokens:
      yield set()
   else:
      tokens = set(tokens)
      t = tokens.pop()
      for solution in solve(tokens - contradictions[t], contradictions):
         yield solution | set([t])
      if contradictions[t] & tokens:
         for solution in solve(tokens, contradictions):
            if contradictions[t] & solution:
               yield solution

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

0
jchl 28 Июн 2010 в 15:56

Вот возможный «эвристически оптимизированный» подход и небольшой пример:

import itertools

# tokens in decreasing order of value (must all be > 0)
toks = 12, 11, 8, 7, 6, 2, 1

# contradictions (dict highestvaltok -> set of incompatible ones)
cont = {12: set([11, 8, 7, 2]),
    11: set([8, 7, 6]),
         7: set([2]),
     2: set([1]),
       }

rec_calls = 0

def bestgroup(toks, contdict, arein=(), contset=()):
  """Recursively compute the highest-valued non-contradictory subset of toks."""
  global rec_calls
  toks = list(toks)
  while toks:
    # find the top token compatible w/the ones in `arein`
    toptok = toks.pop(0)
    if toptok in contset:
      continue
    # try to extend with and without this toptok
    without_top = bestgroup(toks, contdict, arein, contset)
    contset = set(contset).union(c for c in contdict.get(toptok, ()))
    newarein = arein + (toptok,)
    with_top = bestgroup(toks, contdict, newarein, contset)
    rec_calls += 1
    if sum(with_top) > sum(without_top):
      return with_top
    else:
      return without_top
  return arein

def noncongroups(toks, contdict):
  """Count possible, non-contradictory subsets of toks."""
  tot = 0
  for l in range(1, len(toks) + 1):
    for c in itertools.combinations(toks, l):
      if any(cont[k].intersection(c) for k in c if k in contdict): continue
      tot += 1
  return tot


print bestgroup(toks, cont)
print 'calls: %d (vs %d of %d)' % (rec_calls, noncongroups(toks, cont), 2**len(toks))

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

Если это приводит к приемлемому ускорению ваших тестов «реальных вариантов использования», то дальнейшая оптимизация может привести к альфа-отсечке (так что вы можете остановить рекурсию по путям, которые, как вы знаете, являются непродуктивными - это мотивация для убывания порядка в токенах; -) и устранение рекурсии (используя вместо этого явный стек внутри функции). Но я хотел сохранить эту первую версию простой, чтобы ее можно было легко понять и проверить (кроме того, дальнейшая оптимизация, которую я имею в виду, поможет лишь незначительно, я подозреваю - скажем, в лучшем случае, вдвое уменьшив типичную среду выполнения, если даже так много).

2
Alex Martelli 28 Июн 2010 в 15:09

Простой подход на этапах 1 + 2 может выглядеть следующим образом: сначала определите список токенов и словарь противоречий (каждый ключ является токеном, а каждое значение - набором токенов). Затем для каждого токена выполните два действия:

  • добавьте его в result, если он еще не противоречит, и увеличьте набор conflicting токенами, которые противоречат текущему добавленному токену
  • не добавляйте его в result (не обращайте на него внимания) и переходите к следующему токену.

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

token_list = ['a', 'b', 'c']

contradictions = {
    'a': set(['b']),
    'b': set(['a']),
    'c': set()
}

class Generator(object):
    def __init__(self, token_list, contradictions):
        self.list = token_list
        self.contradictions = contradictions
        self.max_start = len(self.list) - 1

    def add_no(self, start, result, conflicting):
        if start < self.max_start:
            for g in self.gen(start + 1, result, conflicting):
                yield g
        else:
            yield result[:]

    def add_yes(self, token, start, result, conflicting):
        result.append(token)
        new_conflicting = conflicting | self.contradictions[token]
        for g in self.add_no(start, result, new_conflicting):
            yield g
        result.pop()

    def gen(self, start, result, conflicting):
        token = self.list[start]
        if token not in conflicting:
            for g in self.add_yes(token, start, result, conflicting):
                yield g
        for g in self.add_no(start, result, conflicting):
            yield g

    def go(self):
        return self.gen(0, [], set())

Пример использования:

g = Generator(token_list, contradictions)
for x in g.go():
    print x

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

3
DzinX 28 Июн 2010 в 14:35