Подтвердить что ты не робот

Как я могу оптимизировать этот код Python для генерации всех слов со словом-расстоянием 1?

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

def distance(word1, word2):
    difference = 0
    for i in range(len(word1)):
        if word1[i] != word2[i]:
            difference += 1
    return difference

def getchildren(word, wordlist):
    return [ w for w in wordlist if distance(word, w) == 1 ]

Примечания:

  • distance() вызывается более 5 миллионов раз, большинство из которых принадлежит getchildren, который должен получить все слова в списке слов, которые отличаются от word ровно на 1 букву.
  • wordlist предварительно фильтруется только для слов, содержащих такое же количество букв, как word, поэтому он гарантирует, что word1 и word2 имеют одинаковое количество символов.
  • Я новичок в Python (начал изучать его 3 дня назад), так что комментарии по соглашениям об именах или другие вещи стиля также были оценены.
  • для списка слов, 12dict список слов, используя файл "2 + 2lemma.txt"

Результаты:

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

Я тестировал два набора входов, которые я назову A и B

Оптимизация1: итерация по индексам слова1,2... из

for i in range(len(word1)):
        if word1[i] != word2[i]:
            difference += 1
    return difference

до итерации по буквенным парам с помощью zip(word1, word2)

for x,y in zip (word1, word2):
        if x != y:
            difference += 1
    return difference

Получено время выполнения от 11,92 до 9,18 для ввода A и от 79,30 до 74,59 для ввода B

Optimization2: Добавлен отдельный метод для разных вариантов в дополнение к дистанционному методу (который мне еще нужен в другом месте для эвристики A *)

def is_neighbors(word1,word2):
    different = False
    for c1,c2 in zip(word1,word2):
        if c1 != c2:
            if different:
                return False
            different = True
    return different

Получено время выполнения от 9,18 до 8,83 для ввода A и от 74,59 до 70,14 для входа B

Optimization3: Большим победителем здесь было использование izip вместо zip

Получено время выполнения от 8,83 до 5,02 для ввода A и от 70,14 до 41,69 для входа B

Возможно, мне лучше написать его на более низком уровне, но я доволен этим пока. Спасибо всем!

Изменить еще раз: Другие результаты Используя метод Mark, проверяющий случай, когда первая буква не соответствует, убрала его с 5.02 → 3.59 и 41.69 → 29.82

Основываясь на этом и включении izip вместо range, я закончил с этим:

def is_neighbors(word1,word2):
    if word1[0] != word2[0]:
        return word1[1:] == word2[1:]
    different = False
    for x,y in izip(word1[1:],word2[1:]):
        if x != y:
            if different:
                return False
            different = True
    return different

Что сжалось немного больше, приведя время от 3.59 до 3.38 и 29.82 → 27.88

Еще больше результатов!

Попытка Sumudu предположить, что я сгенерировал список всех строк, которые являются 1 буквой от слова, и затем проверяя, какие из них были в списке слов, вместо функции is_neighbor, в которой я оказался с этим:

def one_letter_off_strings(word):
    import string
    dif_list = []
    for i in xrange(len(word)):
        dif_list.extend((word[:i] + l + word[i+1:] for l in string.ascii_lowercase if l != word[i]))
    return dif_list

def getchildren(word, wordlist):
    oneoff = one_letter_off_strings(word)
    return ( w for w in oneoff if w in wordlist )

Который оказался медленнее (3.38 → 3.74 и 27.88 → 34.40), но он казался многообещающим. Сначала я подумал, что часть, которую мне нужно оптимизировать, была "one_letter_off_strings", но профилирование показало обратное и что медленная часть была фактически

( w for w in oneoff if w in wordlist )

Я думал, что если будет какая-то разница, если бы я переключил "oneoff" и "wordlist" и сделал сравнение другим способом, когда он ударил меня, что я искал пересечение двух списков. Я заменяю это на set-intersection на буквах:

return set(oneoff) & set(wordlist)

Bam! 3,74 → 0,23 и 34,40 → 2,25

Это действительно потрясающе, общая разница в скорости от моей первоначальной наивной реализации: 23,79 → 0,23 и 180,07 → 2,25, что примерно в 80-100 раз превышает первоначальную реализацию.

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

Великие дебаты:

Хорошо, у меня и Неизвестного есть большие дебаты, которые вы можете прочитать в комментариях своего ответа. Он утверждает, что быстрее будет использовать оригинальный метод (используя is_neighbor вместо использования наборов), если он был перенесен на C. Я пробовал в течение 2 часов, чтобы получить модуль C, который я написал, чтобы строить и быть связанными без особого успеха после попытки выполните this и этот пример, и это похоже на процесс немного отличается в Windows? Я не знаю, но я отказался от этого. В любом случае, здесь полный код программы и текстовый файл взяты из 12dict word listиспользуя файл "2 + 2lemma.txt". Извините, если код немного грязный, это было просто что-то, что я взломал. Также я забыл вырезать запятые из списка слов, чтобы на самом деле ошибка, которую вы можете оставить для одного и того же сравнения или исправить, добавив запятую в список символов в чистых средах.

from itertools import izip
def unique(seq):  
    seen = {} 
    result = [] 
    for item in seq: 
        if item in seen:
            continue 
        seen[item] = 1 
        result.append(item) 
    return result
def cleanentries(li):
    pass
    return unique( [w.strip('[]') for w in li if w != "->"] )
def distance(word1, word2):
    difference = 0
    for x,y in izip (word1, word2):
        if x != y:
            difference += 1
    return difference
def is_neighbors(word1,word2):
    if word1[0] != word2[0]:
        return word1[1:] == word2[1:]
    different = False
    for x,y in izip(word1[1:],word2[1:]):
        if x != y:
            if different:
                return False
            different = True
    return different
def one_letter_off_strings(word):
    import string
    dif_list = []
    for i in xrange(len(word)):
        dif_list.extend((word[:i] + l + word[i+1:] for l in string.ascii_lowercase if l != word[i]))
    return dif_list

def getchildren(word, wordlist):
    oneoff = one_letter_off_strings(word)
    return set(oneoff) & set(wordlist)
def AStar(start, goal, wordlist):
    import Queue
    closedset = []
    openset = [start]
    pqueue = Queue.PriorityQueue(0)
    g_score = {start:0}         #Distance from start along optimal path.
    h_score = {start:distance(start, goal)}
    f_score = {start:h_score[start]}
    pqueue.put((f_score[start], start))
    parent_dict = {}
    while len(openset) > 0:
        x = pqueue.get(False)[1]
        if x == goal:
            return reconstruct_path(parent_dict,goal)
        openset.remove(x)
        closedset.append(x)
        sortedOpen = [(f_score[w], w, g_score[w], h_score[w]) for w in openset]
        sortedOpen.sort()
        for y in getchildren(x, wordlist):
            if y in closedset:
                continue
            temp_g_score = g_score[x] + 1
            temp_is_better = False
            appended = False
            if (not y in openset): 
                openset.append(y)
                appended = True
                h_score[y] = distance(y, goal)
                temp_is_better = True
            elif temp_g_score < g_score[y] :
                temp_is_better = True
            else :
                pass
            if temp_is_better:
                parent_dict[y] = x
                g_score[y] = temp_g_score
                f_score[y] = g_score[y] + h_score[y]
                if appended :
                    pqueue.put((f_score[y], y))
    return None


def reconstruct_path(parent_dict,node):
     if node in parent_dict.keys():
         p = reconstruct_path(parent_dict,parent_dict[node])
         p.append(node)
         return p
     else:
         return []        

wordfile = open("2+2lemma.txt")
wordlist = cleanentries(wordfile.read().split())
wordfile.close()
words = []
while True:
    userentry = raw_input("Hello, enter the 2 words to play with separated by a space:\n ")
    words = [w.lower() for w in userentry.split()]
    if(len(words) == 2 and len(words[0]) == len(words[1])):
        break
print "You selected %s and %s as your words" % (words[0], words[1])
wordlist = [ w for w in wordlist if len(words[0]) == len(w)]
answer = AStar(words[0], words[1], wordlist)
if answer != None:
    print "Minimum number of steps is %s" % (len(answer))
    reply = raw_input("Would you like the answer(y/n)? ")
    if(reply.lower() == "y"):
        answer.insert(0, words[0])
        print "\n".join(answer)
    else:
        print "Good luck!"
else:
    print "Sorry, there no answer to yours"
reply = raw_input("Press enter to exit")

Я оставил метод is_neighbors, хотя он не использовался. Это метод, который предлагается портировать на C. Чтобы использовать его, просто замените getchildren следующим:

def getchildren(word, wordlist):
    return ( w for w in wordlist if is_neighbors(word, w))

Что касается того, чтобы заставить его работать как модуль C, я так далеко не понял, но это то, что я придумал:

#include "Python.h"

static PyObject *
py_is_neighbor(PyObject *self, Pyobject *args)
{
    int length;
    const char *word1, *word2;
    if (!PyArg_ParseTuple(args, "ss", &word1, &word2, &length))
        return NULL;

    int i;
    int different = 0;
    for (i =0; i < length; i++)
    {
        if (*(word1 + i) != *(word2 + i))
        {
            if (different)
            {
                return Py_BuildValue("i", different);
            }
            different = 1;
        }
    }
    return Py_BuildValue("i", different);
}

PyMethodDef methods[] = {
    {"isneighbor", py_is_neighbor, METH_VARARGS, "Returns whether words are neighbors"},
    {NULL, NULL, 0, NULL}
};

PyMODINIT_FUNC
initIsNeighbor(void)
{
    Py_InitModule("isneighbor", methods);
}

Я профилировал это, используя:

python -m cProfile "Wordgame.py"

И записанное время было общим временем вызова метода AStar. Быстрый набор входных данных - "стихотворные поэты", а длинный набор входных данных - "стихи поэтов". Временные интервалы, очевидно, будут различаться между разными машинами, поэтому, если кто-то в конечном итоге попытается это дать сравнение результатов программы как есть, а также с модулем C.

4b9b3361

Ответ 1

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

Я предлагаю это, потому что, если ваши слова имеют разумную длину (~ 10 букв), тогда вы будете искать только 250 потенциальных слов, что, вероятно, быстрее, если ваш список слов больше нескольких сотен слов.

Ответ 2

Ваша функция distance вычисляет общее расстояние, когда вы действительно заботитесь только о расстоянии = 1. В большинстве случаев вы будете знать это > 1 в нескольких символах, чтобы вы могли вернуться раньше и сэкономить много времени.

Кроме того, может быть лучший алгоритм, но я не могу думать об этом.

Изменить: Другая идея.

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

def DifferentByOne(word1, word2):
    if word1[0] != word2[0]:
        return word1[1:] == word2[1:]
    same = True
    for i in range(1, len(word1)):
        if word1[i] != word2[i]:
            if same:
                same = False
            else:
                return False
    return not same

Изменить 2: Я удалил чек, чтобы увидеть, являются ли строки одинаковой длины, поскольку вы говорите, что это избыточно. Выполняя тесты Ryan на мой собственный код и на функцию is_neighbors предоставленную MizardX, я получаю следующее:

  • Исходное расстояние(): 3,7 секунды
  • My DifferentByOne(): 1,1 секунды
  • MizardX is_neighbors(): 3,7 секунды

Отредактируйте 3: (Вероятно, вы попадаете на территорию вики сообщества здесь, но...)

Попытка окончательного определения is_neighbors() с помощью izip вместо zip: 2.9 секунд.

Здесь моя последняя версия, которая еще раз в 1,1 секунды:

def DifferentByOne(word1, word2):
    if word1[0] != word2[0]:
        return word1[1:] == word2[1:]
    different = False
    for i in range(1, len(word1)):
        if word1[i] != word2[i]:
            if different:
                return False
            different = True
    return different

Ответ 3

from itertools import izip

def is_neighbors(word1,word2):
    different = False
    for c1,c2 in izip(word1,word2):
        if c1 != c2:
            if different:
                return False
            different = True
    return different

Или, может быть, вставить код izip:

def is_neighbors(word1,word2):
    different = False
    next1 = iter(word1).next
    next2 = iter(word2).next
    try:
        while 1:
            if next1() != next2():
                if different:
                    return False
                different = True
    except StopIteration:
        pass
    return different

И перезаписан getchildren:

def iterchildren(word, wordlist):
    return ( w for w in wordlist if is_neighbors(word, w) )
  • izip(a,b) возвращает итератор по парам значений из a и b.
  • zip(a,b) возвращает список пар из a и b.

Ответ 4

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

"расстояние" называется более 5 миллионов раз

Почему это? Возможно, лучший способ оптимизации - попытаться сократить количество вызовов до distance, а не бритья миллисекунд времени distance's. Невозможно рассказать, не видя полного script, но оптимизация конкретной функции вообще не нужна.

Если это невозможно, возможно, вы могли бы написать его как модуль C?

Ответ 5

Как часто вызывается функция расстояния с теми же аргументами? Простой в реализации оптимизации будет использовать memoization.

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

Короткое замыкание оценки даст вам только прибыль, если слова, которые вы используете, очень длинны, так как используемый вами алгоритм расстояния от помех в основном равен O (n), где n - длина слова.

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

Результаты тайм-аута

Ваше решение

d = """\
def distance(word1, word2):
    difference = 0
    for i in range(len(word1)):
        if word1[i] != word2[i]:
            difference += 1
    return difference
"""
t1 = timeit.Timer('distance("hello", "belko")', d)
print t1.timeit() # prints 6.502113536776391

Один вкладыш

d = """\
from itertools import izip
def hamdist(s1, s2):
    return sum(ch1 != ch2 for ch1, ch2 in izip(s1,s2))
"""
t2 = timeit.Timer('hamdist("hello", "belko")', d)
print t2.timeit() # prints 10.985101179

Оценка ярлыков

d = """\
def distance_is_one(word1, word2):
    diff = 0
    for i in xrange(len(word1)):
        if word1[i] != word2[i]:
            diff += 1
        if diff > 1:
            return False
    return diff == 1
"""
t3 = timeit.Timer('hamdist("hello", "belko")', d)
print t2.timeit() # prints 6.63337

Ответ 6

Ну, вы можете начать с разрыва цикла, если разница составляет 2 или более.

Также вы можете изменить

for i in range(len(word1)):

to

for i in xrange(len(word1)):

Поскольку xrange генерирует последовательности по требованию, а не генерирует сразу весь диапазон чисел.

Вы также можете попробовать сравнить длины слов, которые будут быстрее. Также обратите внимание, что ваш код не работает, если word1 больше слова2

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

Изменить 2

Попытка объяснить мой анализ алгоритма Сумуду по сравнению с проверкой различий char на char.

Когда у вас есть слово длины L, количество слов "по-одному", которое вы создадите, будет 25L. Из реализаций наборов на современных компьютерах известно, что скорость поиска составляет приблизительно log (n) base 2, где n - количество элементов для поиска.

Увидев, что большинство из 5 миллионов слов, которые вы тестируете, не в наборе, большую часть времени вы будете перемещаться по всему набору, что означает, что он действительно становится журналом (25L), а не только log (25L)/2. (и это предполагает наилучший сценарий для множеств, которые сравнивают строку по строке, эквивалентно сравнению char на char)

Теперь мы рассмотрим временную сложность определения "по-одному". Если мы предположим, что вам нужно проверить все слово, то количество операций на слово станет L. Мы знаем, что большинство слов отличаются на 2 очень быстро. И зная, что большинство префиксов занимают небольшую часть слова, мы можем логически предположить, что большую часть времени вы будете разбивать на L/2, или половину слова (и это консервативная оценка).

Итак, теперь мы построим временные сложности двух поисков, L/2 и log (25L), и, имея в виду, что это даже если строка соответствует той же скорости, что и char, соответствующая (высоко в пользу наборов). У вас есть журнал уравнений (25 * L) > L/2, который можно упростить до log (25) > L/2 - log (L). Как видно из графика, нужно быстрее использовать алгоритм соответствия char, пока вы не достигнете очень больших чисел L.

alt text

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

Я был первым человеком в этом вопросе, предлагающим разбить разницу в 2 или более. Дело в том, что идея Mark о нарезке строк (если word1 [0]!= Word2 [0]: return word1 [1:] == word2 [1:]) просто помещает то, что мы делаем в C. Как вы think word1 [1:] == word2 [1:] рассчитывается? То же самое, что мы делаем.

Я читал ваше объяснение несколько раз, но я не совсем последовал за ним, не могли бы вы объяснить его немного более глубоким? Также я не очень хорошо знаком с C, и я работаю на языках высокого уровня в течение последних нескольких лет (ближе всего учился С++ в средней школе 6 лет назад

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

Подробнее

Вот более подробное объяснение Davy8

def getchildren(word, wordlist):
    oneoff = one_letter_off_strings(word)
    return set(oneoff) & set(wordlist)

Ваша функция one_letter_off_strings создаст набор из строк 25L (где L - количество букв).

Создание набора из списка слов создаст набор строк D (где D - длина словаря). Создав перекресток, вы ДОЛЖНЫ перебирать каждую oneoff и посмотреть, существует ли она в списке слов.

Сложность времени для этой операции подробно описана выше. Эта операция менее эффективна, чем сравнение слова с каждым словом в списке слов. Метод Sumudu - это оптимизация в C, а не алгоритм.

Подробнее Объяснение 2

Всего всего 4500 слов (потому что список слов предварительно фильтруется для 5 буквенных слов, прежде чем он будет передан алгоритму), пересекаясь с 125 однобуквенными словами. Казалось, что вы говорите, что пересечение - log (меньше) или в журнале других слов (125, 2). Сравните это с тем, чтобы снова предположить, что вы сказали, где сравнение слов разбивается на буквы L/2, я обойду это до 2, хотя для 5-буквенного слова это скорее всего будет 3. Это сравнение выполняется 4500 раз, поэтому 9000. log (125,2) составляет около 6,9, а log (4500,2) - около 12. Lemme знает, неверно ли я интерпретировал ваши номера.

Чтобы создать пересечение 125 однобуквенных слов со словарем 4500, вам необходимо сделать 125 * 4500 сравнений. Это не журнал (125,2). Это в лучшем случае 125 * log (4500, 2), предполагая, что словарь предварительно задан. Макетов нет. Вы также выполняете строку с строкой вместо char путем сравнения char здесь.

Ответ 7

Для такой простой функции, которая имеет такую ​​большую производительность, я бы, вероятно, создал библиотеку C и назову ее с помощью ctypes, Один из основателей reddit утверждает, что он сделал сайт 2x столь же быстрым, используя эту технику.

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

Ответ 8

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

def getchildren(word, wordlist):
    return [ w for w in wordlist if distance(word, w) == 1 ]

к

def getchildren(word, wordlist):
    return ( w for w in wordlist if distance(word, w) == 1 )

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

Кроме того, следуя по неизвестному ответу, это может быть более "питонический" способ записи distance():

def distance(word1, word2):
    difference = 0
    for x,y in zip (word1, word2):
        if x == y:
            difference += 1
    return difference

Но это запутывает то, что предназначено, когда len (word1)!= len (word2), в случае zip он будет возвращать столько символов, сколько кратчайшее. (Что может оказаться оптимизацией...)

Ответ 9

Попробуйте следующее:

def distance(word1, word2):
  return sum([not c1 == c2 for c1, c2 in zip(word1,word2)])

Кроме того, есть ли у вас ссылка на вашу игру? Мне нравится быть уничтоженным в словесных играх

Ответ 10

Первое, что мне нужно:

from operator import ne

def distance(word1, word2):
    return sum(map(ne, word1, word2))

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

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

Ответ 11

для этого фрагмента:

for x,y in zip (word1, word2):
    if x != y:
        difference += 1
return difference

я бы использовал этот:

return sum(1 for i in xrange(len(word1)) if word1[i] == word2[i])

тот же шаблон будет следовать по всему предоставленному коду...

Ответ 12

Все остальные сосредоточились только на явном расчете расстояния, не делая ничего о построении кандидатов на дистанцию-1. Вы можете улучшить, используя хорошо известную структуру данных, называемую Trie, чтобы объединить неявный расчет расстояния с задачей генерации всех соседних слов distance-1. Trie - связанный список, где каждый node обозначает букву, а поле "next" - это dict с 26 элементами, указывающими на следующий node.

Здесь псевдокод: пройдите Итеративно для вашего слова; при каждом node добавьте к результатам все расстояния-0 и расстояние-1 соседей; держите счетчик расстояния и уменьшите его. Вам не нужна рекурсия, просто функция поиска, которая принимает дополнительный аргумент distance_so_far integer.

Небольшой компромисс дополнительной скорости для увеличения пространства O (N) может быть получен путем создания отдельных Tries для слов длины - 3, длина-4, длина-5 и т.д.