@pcdesign

Как ускорить код с подсчетом похожести?

Вот код:
import difflib
arr = [
    {"_id": 1, "list_word_int": [189, 114, 188,
                                 90, 2, 68, 96, 0, 250, 168, 150, 126]},
    {"_id": 2, "list_word_int": [224, 26, 56,
                                 153, 139, 128, 126, 220, 190, 137], },
    {"_id": 3, "list_word_int": [188, 241, 225,
                                 134, 134, 30, 134, 187, 204, 227, 3]},
    {"_id": 4, "list_word_int": [
        224, 166, 159, 236, 82, 17, 82, 21, 227, 97], },
    {"_id": 5, "list_word_int": [98, 96, 38, 107, 142, 134, 13, 36, 23], }
]


for a in arr:
    for b in arr:
        if b["_id"] == a["_id"]:
            continue

        sm = difflib.SequenceMatcher(None, a["list_word_int"],
                                     b["list_word_int"])
        ratio = sm.ratio()
        print("id= ", a["_id"],
              "Сравниваемый id=", b["_id"],
              "Коэффициент похожести:", ratio)


Вот результат:

id=  1 Сравниваемый id= 2 Коэффициент похожести: 0.09090909090909091
id=  1 Сравниваемый id= 3 Коэффициент похожести: 0.08695652173913043
id=  1 Сравниваемый id= 4 Коэффициент похожести: 0.0
id=  1 Сравниваемый id= 5 Коэффициент похожести: 0.09523809523809523
id=  2 Сравниваемый id= 1 Коэффициент похожести: 0.09090909090909091
id=  2 Сравниваемый id= 3 Коэффициент похожести: 0.0
id=  2 Сравниваемый id= 4 Коэффициент похожести: 0.1
id=  2 Сравниваемый id= 5 Коэффициент похожести: 0.0
id=  3 Сравниваемый id= 1 Коэффициент похожести: 0.08695652173913043
id=  3 Сравниваемый id= 2 Коэффициент похожести: 0.0
id=  3 Сравниваемый id= 4 Коэффициент похожести: 0.09523809523809523
id=  3 Сравниваемый id= 5 Коэффициент похожести: 0.1
id=  4 Сравниваемый id= 1 Коэффициент похожести: 0.0
id=  4 Сравниваемый id= 2 Коэффициент похожести: 0.1
id=  4 Сравниваемый id= 3 Коэффициент похожести: 0.09523809523809523
id=  4 Сравниваемый id= 5 Коэффициент похожести: 0.0
id=  5 Сравниваемый id= 1 Коэффициент похожести: 0.09523809523809523
id=  5 Сравниваемый id= 2 Коэффициент похожести: 0.0
id=  5 Сравниваемый id= 3 Коэффициент похожести: 0.1
id=  5 Сравниваемый id= 4 Коэффициент похожести: 0.0

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

Все бы ничего, но у меня в массиве arr не 5 элементов как в примере, а 30 тыс.
Примерное время завершения работы при 30 тыс. элементах 7 дней.
Есть вариант это ускорить?
  • Вопрос задан
  • 258 просмотров
Решения вопроса 3
@deliro
1. Убери повторения (в моём примере это уже сделано), сравнивать id=5 с id=1 не надо, если ты уже сравнил id=1 с id=5. Они симметричны
2. Если кэш поможет (в чём я сомневаюсь) — можно его оставить. Если ты уверен, что не будет двух неуникальных list_word_int — выбрасывай кэш смело.
3. Это вроде можно распараллелить. Задействуй все ядра
4. Перепиши это на быстрый компилируемый язык вроде Golang или Cython

Однопоточный код
import difflib
from functools import lru_cache
from itertools import combinations

arr = [
    {"_id": 1, "list_word_int": (189, 114, 188, 90, 2, 68, 96, 0, 250, 168, 150, 126)},
    {"_id": 2, "list_word_int": (224, 26, 56, 153, 139, 128, 126, 220, 190, 137)},
    {"_id": 3, "list_word_int": (188, 241, 225, 134, 134, 30, 134, 187, 204, 227, 3)},
    {"_id": 4, "list_word_int": (224, 166, 159, 236, 82, 17, 82, 21, 227, 97)},
    {"_id": 5, "list_word_int": (98, 96, 38, 107, 142, 134, 13, 36, 23)},
]


@lru_cache(maxsize=2 ** 13)
def get_ratio(lst1, lst2):
    return difflib.SequenceMatcher(None, lst1, lst2).ratio()


if __name__ == "__main__":
    for a, b in combinations(arr, 2):
        ratio = get_ratio(a["list_word_int"], b["list_word_int"])
        print(
            "id= ",
            a["_id"],
            "Сравниваемый id=",
            b["_id"],
            "Коэффициент похожести:",
            ratio,
        )

    print(get_ratio.cache_info())
Параллельное выполнение, генерация комбинаций во всех процессах
import difflib
import multiprocessing as mp
import os
from itertools import combinations, cycle

arr = [
    {"_id": 1, "list_word_int": [189, 114, 188, 90, 2, 68, 96, 0, 250, 168, 150, 126]},
    {"_id": 2, "list_word_int": [224, 26, 56, 153, 139, 128, 126, 220, 190, 137]},
    {"_id": 3, "list_word_int": [188, 241, 225, 134, 134, 30, 134, 187, 204, 227, 3]},
    {"_id": 4, "list_word_int": [224, 166, 159, 236, 82, 17, 82, 21, 227, 97]},
    {"_id": 5, "list_word_int": [98, 96, 38, 107, 142, 134, 13, 36, 23]},
]


def target(id_, count):
    pid = os.getpid()

    for i, (a, b) in zip(cycle(range(count)), combinations(arr, 2)):
        if i != id_:
            continue
        ratio = difflib.SequenceMatcher(
            None, a["list_word_int"], b["list_word_int"]
        ).ratio()
        print(f"PID: {pid} id={a['_id']} & {b['_id']} ratio={ratio}")


if __name__ == "__main__":
    processes = []

    for x in range(mp.cpu_count()):
        p = mp.Process(target=target, args=(x, mp.cpu_count()))
        p.start()
        processes.append(p)

    for p in processes:
        p.join()
Один процесс генерирует комбинации в очередь, остальные обрабатывают
import difflib
import multiprocessing as mp
import os
from itertools import combinations

arr = [
    {"_id": 1, "list_word_int": [189, 114, 188, 90, 2, 68, 96, 0, 250, 168, 150, 126]},
    {"_id": 2, "list_word_int": [224, 26, 56, 153, 139, 128, 126, 220, 190, 137]},
    {"_id": 3, "list_word_int": [188, 241, 225, 134, 134, 30, 134, 187, 204, 227, 3]},
    {"_id": 4, "list_word_int": [224, 166, 159, 236, 82, 17, 82, 21, 227, 97]},
    {"_id": 5, "list_word_int": [98, 96, 38, 107, 142, 134, 13, 36, 23]},
]


def queue_creator(q, w_count):
    pid = os.getpid()
    print("Created queue generator PID", pid)

    for a, b in combinations(arr, 2):
        q.put((a, b))
    for _ in range(w_count):
        q.put(("stop", None))


def worker(q):
    pid = os.getpid()
    print("Created worker PID", pid)

    while True:
        a, b = q.get()
        if a == "stop":
            break

        ratio = difflib.SequenceMatcher(
            None, a["list_word_int"], b["list_word_int"]
        ).ratio()
        print(f"PID:{pid} {a['_id']} & {b['_id']} ratio={ratio}")


if __name__ == "__main__":
    queue = mp.Queue()
    # 1 воркер на генерацию комбинаций, остальные на обработку
    workers_count = (mp.cpu_count() - 1) or 1
    q_process = mp.Process(target=queue_creator, args=(queue, workers_count))
    q_process.start()
    processes = [q_process]

    for x in range(workers_count):
        p = mp.Process(target=worker, args=(queue,))
        p.start()
        processes.append(p)

    for process in processes:
        process.join()

Ответ написан
LaRN
@LaRN
Senior Developer
Можно сэкономить время если не сравнивать 2 с 1, т.к. ранее уже сравнивался 1 с 2.
Это и других повторов касается, так в вашем примере 20 сравнений, а по факту хватит 4+3+2+1=10 сравнений, т.е. ускорение в 2 раза.
Ответ написан
Комментировать
sM0kfyz
@sM0kfyz
Frontend dev.
Вы сравниваете любые два элемента дважды, как вариант можете удалять элемент из массива после того как уже сравнили его со всеми. То есть после выполнения вложенного цикла. Тогда можно ускорить в два раза. Других способов ускорить нет, только если разбираться как работает библиотека.
Ответ написан
Пригласить эксперта
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы