Рекурсивный алгоритм сопоставления python, основанный на слишком медленной работе подмножеств - PullRequest
0 голосов
/ 26 мая 2020
• 1000 Прототип находится по адресу covidgapyears.com . Я никогда не писал алгоритм сопоставления / рекомендации, поэтому, хотя люди предлагали такие вещи, как совместная фильтрация и поиск правил ассоциации или адаптация проблемы стабильного брака, я не думаю, что что-либо из этого будет работать, потому что это небольшой набор данных (несколько сотен пользователей щас скоро несколько тысяч). Поэтому я написал свой собственный алгоритм, руководствуясь здравым смыслом.

По сути, он берет список тегов, которые интересуют студента, а затем ищет точное совпадение этих тегов с кем-то, кто взял годовой перерыв и зарегистрировался на сайте ( кто также выбрал теги при регистрации). Как показано ниже, точное совпадение - это когда теги, указанные пользователем, ВСЕ содержатся в каком-то профиле (т. Е. Являются подмножеством). Если он не может найти точное совпадение со ВСЕМИ введенными пользователем тегами, , он проверит все подмножества длины n-1 в самом списке тегов, чтобы увидеть, есть ли совпадения для каких-либо менее избирательных запросов . Он делает это рекурсивно, пока не будет найдено по крайней мере 3 совпадения. Хотя он отлично работает для выбора небольших тегов (до 5-7), он становится медленным для выбора более крупных тегов (7-13), возвращая результат за несколько секунд. Когда выбрано 11-13 тегов, выдает ошибку Heroku из-за тайм-аута рабочего.

Я провел несколько тестов, поместив переменные внутри алгоритма для подсчета вычислений, и кажется, что когда он немного углубляется в рекурсивный стек, он каждый раз проверяет несколько сотен подмножеств (чтобы увидеть, есть ли точное соответствие для это подмножество, и если есть, добавьте его в список результатов для вывода), и общее количество вычислений удваивается, когда вы добавляете еще один тег (было выполнено 54, 150, 270, 500, 1000, 1900, 3400 операций для большего количества и больше тегов). Это правда, что на каждой глубине есть несколько сотен подмножеств. Но точное соответствие - это O (1), как я его написал (без итераций), и помимо других операций O (1), таких как IF, FOR внутри подмножества l oop будет, самое большее, проходить через 10 раз. Это согласуется с измеренным результатом нескольких тысяч вычислений каждый раз.

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

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

Ответы [ 2 ]

0 голосов
/ 26 мая 2020

Хорошо, после долгой возни с таймерами я понял это. При сопоставлении задействовано несколько функций: excMatches, matchGapper и arbSubset. Когда я помещаю счетчик в глобальную переменную и измеряю операции (измеряемые как строки выполняемого моего кода , он получал около 2-10К для больших входных данных (около 10 тегов) ).

Это правда, что arbSubset, возвращающий список подмножеств, поначалу кажется вероятным узким местом. Но если вы присмотритесь, мы: 1) обрабатываем небольшое количество тегов (порядка 10-50) и, что более важно, 2) мы вызываем arbSubset только при рекурсии matchGapper, что происходит не более 10 раз, поскольку tagsList может быть только около 10 (порядка 10-50, как указано выше). И когда я проверил время, необходимое для создания arbSubset, оно было порядка 2e-5. Таким образом, общее время, затрачиваемое на генерацию подмножеств произвольного размера, составляет всего 2e-4. Другими словами, это не источник 5-30 секунд ожидания в веб-приложении.

И так, помимо этого, зная, что arbSubset вызывается только порядка 10 раз, и при этом работает быстро, и зная, что в моем примере выполняется не более 10К вычислений. code начинает становиться ясно, что я должен использовать какую-то нестандартную функцию, я не знаю - например, set () или .issubset () или что-то в этом роде - которая требует нетривиального количество времени для вычисления и выполняется много раз. Добавив несколько счетчиков еще в некоторых местах, становится ясно, что на точное соответствие () приходится около 95-99% всех выполняемых вычислений (как и следовало ожидать, если нам нужно проверять все комбинации подмножеств различных размеров на предмет точных совпадений).

Итак, проблема на данном этапе сводится к тому, что точное соответствие занимает около 0,02 с (эмпирически), как реализовано, и вызывается несколько тысяч раз. И поэтому мы можем либо попытаться ускорить его на пару порядков (это уже довольно оптимально), либо воспользоваться другим подходом, который не предполагает поиск совпадений с использованием подмножеств. Мой друг предложил создать dict со всеми комбинациями тегов (например, ключи 2 ^ len (tagsList)) и установить их равными спискам зарегистрированных профилей с этой точной комбинацией. Таким образом, запросы - это просто обход (огромного) словаря, который может быть выполнен быстро. Любые другие предложения приветствуются.

0 голосов
/ 26 мая 2020

Не похоже, что вы выполняете несколько сотен вычислительных шагов. Фактически у вас есть несколько сотен вариантов для каждой глубины, поэтому вам не следует складывать, а умножать количество шагов на каждой глубине, чтобы оценить сложность вашего решения.

Кроме того, это утверждение: This or adapting the stable marriage problem, I don't think any of those will work because it's a small dataset также явно неверно. Хотя эти алгоритмы могут оказаться излишними для некоторых очень простых случаев, они все еще действительны и будут работать для них.

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