Как использовать многопоточность для улучшения производительности в Python

1

У меня есть список sentences который имеет около 500,000 sentences. А также список concepts которые имеют около 13,000,000 concepts. Для каждого предложения я хочу извлечь concepts из sentences в порядке предложений и записать их в вывод.

Например, моя программа на Python выглядит следующим образом.

import re

sentences = ['data mining is the process of discovering patterns in large data sets involving methods at the intersection of machine learning statistics and database systems', 
             'data mining is an interdisciplinary subfield of computer science and statistics with an overall goal to extract information from a data set and transform the information into a comprehensible structure for further use',
             'data mining is the analysis step of the knowledge discovery in databases process or kdd']

concepts = ['data mining', 'database systems', 'databases process', 
            'interdisciplinary subfield', 'information', 'knowledge discovery',
            'methods', 'machine learning', 'patterns', 'process']

output = []
counting = 0

re_concepts = [re.escape(t) for t in concepts]

find_all_concepts = re.compile('|'.join(re_concepts), flags=re.DOTALL).findall

for sentence in sentences:
    output.append(find_all_concepts(sentence))

print(output)

Выход есть; [['data mining', 'process', 'patterns', 'methods', 'machine learning', 'database systems'], ['data mining', 'interdisciplinary subfield', 'information', 'information'], ['data mining', 'knowledge discovery', 'databases process']]

Однако порядок вывода для меня не важен. то есть мой вывод также может выглядеть следующим образом (другими словами, списки внутри output могут быть перетасованы).

[['data mining', 'interdisciplinary subfield', 'information', 'information'], ['data mining', 'knowledge discovery', 'databases process'], ['data mining', 'process', 'patterns', 'methods', 'machine learning', 'database systems']]

[['data mining', 'knowledge discovery', 'databases process'], ['data mining', 'interdisciplinary subfield', 'information', 'information'], ['data mining', 'process', 'patterns', 'methods', 'machine learning', 'database systems']]

Однако из-за длины моих sentences и concepts эта программа все еще довольно медленная.

Можно ли еще улучшить производительность (с точки зрения времени), используя многопоточность в python?

  • 6
    Многопоточность в python великолепна, когда ваши потоки в основном ждут - сетевых ресурсов, веб-запросов, запросов к БД, дискового ввода-вывода и т. Д. В этом случае кажется, что вы привязаны к процессору, и в этом случае многопоточность не поможет вообще.
  • 1
    Есть много других вещей, которые я бы попробовал перед многопоточностью. Pypy, Cython, умный алгоритм и т. Д.
Показать ещё 10 комментариев
Теги:
multithreading

3 ответа

1
Лучший ответ

Этот ответ будет касаться улучшения производительности без использования параллелизма.


Как вы структурировали свой поиск, вы ищете 13 миллионов уникальных вещей в каждом предложении. Вы сказали, что для каждого предложения требуется 3-5 минут, а длина слов в concepts колеблется от одного до десяти.

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

Пример предложения, разбитого на 4 строки слов:

'data mining is the process of discovering patterns in large data sets involving methods at the intersection of machine learning statistics and database systems'
# becomes
[('data', 'mining', 'is', 'the'),
 ('mining', 'is', 'the', 'process'),
 ('is', 'the', 'process', 'of'),
 ('the', 'process', 'of', 'discovering'),
 ('process', 'of', 'discovering', 'patterns'),
 ('of', 'discovering', 'patterns', 'in'),
 ('discovering', 'patterns', 'in', 'large'),
 ('patterns', 'in', 'large', 'data'),
 ('in', 'large', 'data', 'sets'),
 ('large', 'data', 'sets', 'involving'),
 ('data', 'sets', 'involving', 'methods'),
 ('sets', 'involving', 'methods', 'at'),
 ('involving', 'methods', 'at', 'the'),
 ('methods', 'at', 'the', 'intersection'),
 ('at', 'the', 'intersection', 'of'),
 ('the', 'intersection', 'of', 'machine'),
 ('intersection', 'of', 'machine', 'learning'),
 ('of', 'machine', 'learning', 'statistics'),
 ('machine', 'learning', 'statistics', 'and'),
 ('learning', 'statistics', 'and', 'database'),
 ('statistics', 'and', 'database', 'systems')]

Процесс:

concepts = set(concepts)
sentence = sentence.split()
#one word
for meme in sentence:
    if meme in concepts:
        #keep it
#two words
for meme in zip(sentence,sentence[1:]):
    if ' '.join(meme) in concepts:
        #keep it
#three words
for meme in zip(sentence,sentence[1:],sentence[2:]):
    if ' '.join(meme) in concepts:
        #keep it

Адаптируя рецепт itertools (попарно), вы можете автоматизировать процесс создания строк из n слов из предложения:

from itertools import tee
def nwise(iterable, n=2):
    "s -> (s0,s1), (s1,s2), (s2, s3), ... for n=2"
    iterables = tee(iterable, n)
    # advance each iterable to the appropriate starting point
    for i, thing in enumerate(iterables[1:],1):
        for _ in range(i):
            next(thing, None)
    return zip(*iterables)

Тестирование каждого предложения выглядит следующим образом

sentence = sentence.strip().split()
for n in [1,2,3,4,5,6,7,8,9,10]:
    for meme in nwise(sentence,n):
        if ' '.join(meme) in concepts:
            #keep meme

Я сделал набор из 13e6 случайных строк по 20 символов в каждой, чтобы приблизить concepts.

import random, string
data =set(''.join(random.choice(string.printable) for _ in range(20)) for _ in range(13000000))

Тестирование строки из четырех или сорока символов на членство в data последовательно занимает около 60 наносекунд. Предложение из ста слов содержит 955 строк из одного-десяти слов, поэтому поиск по этому предложению должен занять ~ 60 мкс.

Первое предложение из вашего примера 'data mining is the process of discovering patterns in large data sets involving methods at the intersection of machine learning statistics and database systems' содержит 195 возможных концепций (строки из одного-десяти слов). Время для следующих двух функций примерно одинаково: около 140 микросекунд для f и 150 микросекунд для g:

def f(sentence, data=data, nwise=nwise):
    '''iterate over memes in sentence and see if they are in data'''
    sentence = sentence.strip().split()
    found = []
    for n in [1,2,3,4,5,6,7,8,9,10]:
        for meme in nwise(sentence,n):
            meme = ' '.join(meme)
            if meme in data:
                found.append(meme)
    return found

def g(sentence, data=data, nwise=nwise):
    'make a set of the memes in sentence then find its intersection with data'''
    sentence = sentence.strip().split()
    test_strings = set(' '.join(meme) for n in range(1,11) for meme in nwise(sentence,n))
    found = test_strings.intersection(data)
    return found

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

После тестирования данных вашего примера я обнаружил, что g не будет работать, если в предложении дважды появится понятие.


Так что здесь все вместе с понятиями, перечисленными в том порядке, в котором они находятся в каждом предложении. Новая версия f займет больше времени, но добавленное время должно быть относительно небольшим. Если это возможно, вы бы опубликовали комментарий, дающий мне знать, насколько он длиннее оригинала? (Мне любопытно).

from itertools import tee

sentences = ['data mining is the process of discovering patterns in large data sets involving methods at the intersection of machine learning statistics and database systems', 
             'data mining is an interdisciplinary subfield of computer science and statistics with an overall goal to extract information from a data set and transform the information into a comprehensible structure for further use',
             'data mining is the analysis step of the knowledge discovery in databases process or kdd']

concepts = ['data mining', 'database systems', 'databases process', 
            'interdisciplinary subfield', 'information', 'knowledge discovery',
            'methods', 'machine learning', 'patterns', 'process']

concepts = set(concepts)

def nwise(iterable, n=2):
    "s -> (s0,s1), (s1,s2), (s2, s3), ... for n=2"
    iterables = tee(iterable, n)
    # advance each iterable to the appropriate starting point
    for i, thing in enumerate(iterables[1:],1):
        for _ in range(i):
            next(thing, None)
    return zip(*iterables)

def f(sentence, concepts=concepts, nwise=nwise):
    '''iterate over memes in sentence and see if they are in concepts'''
    indices = set()
    #print(sentence)
    words = sentence.strip().split()
    for n in [1,2,3,4,5,6,7,8,9,10]:
        for meme in nwise(words,n):
            meme = ' '.join(meme)
            if meme in concepts:
                start = sentence.find(meme)
                end = len(meme)+start
                while (start,end) in indices:
                    #print(f'{meme} already found at character:{start} - looking for another one...') 
                    start = sentence.find(meme, end)
                    end = len(meme)+start
                indices.add((start, end))
    return [sentence[start:end] for (start,end) in sorted(indices)]


###########
results = []
for sentence in sentences:
    results.append(f(sentence))
    #print(f'{sentence}\n\t{results[-1]})')


In [20]: results
Out[20]: 
[['data mining', 'process', 'patterns', 'methods', 'machine learning', 'database systems'],
 ['data mining', 'interdisciplinary subfield', 'information', 'information'],
 ['data mining', 'knowledge discovery', 'databases process', 'process']]
  • 0
    Вау, это впечатляет. Итак, если я вас правильно понял, data - это список concepts и мне нужно добавить found результаты в output ? Я запишу это на своем реальном наборе данных и сообщу вам результаты. очень взволнован, чтобы бежать.
  • 0
    Привет, Я до сих пор не запускать программу , как я не совсем уверен , что подразумевается под data , nwise , found . Можете ли вы опубликовать полный код? С нетерпением жду Вашего ответа.
Показать ещё 18 комментариев
2

Приведет ли многопоточность к реальному увеличению производительности, зависит не только от реализации в Python и объема данных, но также от аппаратного обеспечения, выполняющего программу. В некоторых случаях, когда аппаратное обеспечение не дает никаких преимуществ, многопоточность может привести к замедлению работы из-за увеличения накладных расходов.

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

Оставайтесь ближе к вашему примеру структуры, реализации и именования:

import re
import queue
import threading

sentences = ['data mining is the process of discovering patterns in large data sets involving methods at the intersection of machine learning statistics and database systems',
             'data mining is an interdisciplinary subfield of computer science and statistics with an overall goal to extract information from a data set and transform the information into a comprehensible structure for further use',
             'data mining is the analysis step of the knowledge discovery in databases process or kdd']

concepts = ['data mining', 'database systems', 'databases process',
            'interdisciplinary subfield', 'information', 'knowledge discovery',
            'methods', 'machine learning', 'patterns', 'process']

re_concepts = [re.escape(t) for t in concepts]

find_all_concepts = re.compile('|'.join(re_concepts), flags=re.DOTALL).findall


def do_find_all_concepts(q_in, l_out):
    while True:
        sentence = q_in.get()
        l_out.append(find_all_concepts(sentence))
        q_in.task_done()


# Queue with default maxsize of 0, infinite queue size
sentences_q = queue.Queue()
output = []

# any reasonable number of workers
num_threads = 2
for i in range(num_threads):
    worker = threading.Thread(target=do_find_all_concepts, args=(sentences_q, output))
    # once there nothing but daemon threads left, Python exits the program
    worker.daemon = True
    worker.start()

# put all the input on the queue
for s in sentences:
    sentences_q.put(s)

# wait for the entire queue to be processed
sentences_q.join()
print(output)

Пользователь @wwii спросил о нескольких потоках, не влияющих на производительность для проблем с процессором. Вместо использования нескольких потоков, обращающихся к одной и той же выходной переменной, вы также можете использовать несколько процессов, получить доступ к общей очереди вывода, например:

import re
import queue
import multiprocessing

sentences = [
    'data mining is the process of discovering patterns in large data sets involving methods at the intersection of machine learning statistics and database systems',
    'data mining is an interdisciplinary subfield of computer science and statistics with an overall goal to extract information from a data set and transform the information into a comprehensible structure for further use',
    'data mining is the analysis step of the knowledge discovery in databases process or kdd']

concepts = ['data mining', 'database systems', 'databases process',
            'interdisciplinary subfield', 'information', 'knowledge discovery',
            'methods', 'machine learning', 'patterns', 'process']

re_concepts = [re.escape(t) for t in concepts]

find_all_concepts = re.compile('|'.join(re_concepts), flags=re.DOTALL).findall


def do_find_all_concepts(q_in, q_out):
    try:
        while True:
            sentence = q_in.get(False)
            q_out.put(find_all_concepts(sentence))
    except queue.Empty:
        pass


if __name__ == '__main__':
    # default maxsize of 0, infinite queue size
    sentences_q = multiprocessing.Queue()
    output_q = multiprocessing.Queue()

    # any reasonable number of workers
    num_processes = 2
    pool = multiprocessing.Pool(num_processes, do_find_all_concepts, (sentences_q, output_q))

    # put all the input on the queue
    for s in sentences:
        sentences_q.put(s)

    # wait for the entire queue to be processed
    pool.close()
    pool.join()
    while not output_q.empty():
        print(output_q.get())

Еще больше накладных расходов, но с использованием ресурсов ЦП, доступных и на других ядрах.

  • 0
    Большое спасибо. Можем ли мы улучшить число потоков до 10, если предположить, что у меня увеличилось аппаратное обеспечение?
  • 1
    Да, просто измените num_threads = 2 на то, что вы считаете оптимальным. Вы можете протестировать немного с меньшим набором (но больше, чем указано в примере) и использовать некоторые функции синхронизации, чтобы решить, какое количество потоков является оптимальным для вашей системы. Чем больше, тем лучше, потому что ваше оборудование и операционная система могут делать только столько параллельно.
Показать ещё 3 комментария
1

Вот два решения, использующие concurrent.futures.ProcessPoolExecutor, который будет распределять задачи по разным процессам. Ваша задача связана с процессором, а не с вводом/выводом, поэтому потоки, вероятно, не помогут.

import re
import concurrent.futures

# using the lists in your example

re_concepts = [re.escape(t) for t in concepts]
all_concepts = re.compile('|'.join(re_concepts), flags=re.DOTALL)

def f(sequence, regex=all_concepts):
    result = regex.findall(sequence)
    return result

if __name__ == '__main__':

    out1 = []
    with concurrent.futures.ProcessPoolExecutor() as executor:
        futures = [executor.submit(f, s) for s in sentences]
        for future in concurrent.futures.as_completed(futures):
            try:
                result = future.result()
            except Exception as e:
                print(e)
            else:
                #print(result)
                out1.append(result)   

    out2 = []
    with concurrent.futures.ProcessPoolExecutor() as executor:
        for result in executor.map(f, sentences):
            #print(result)
            out2.append(result)

Executor.map() имеет параметр chunksize: в документах говорится, что отправка кусков, содержащих более одного элемента итерируемого, может быть полезной. Функция должна быть реорганизована для учета этого. Я протестировал это с помощью функции, которая просто возвращала бы то, что было отправлено, но независимо от размера фрагмента, который я указал, тестовая функция возвращала только отдельные элементы. Figure пойди разберись?

def h(sequence):
    return sequence

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

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

data =set(''.join(random.choice(string.printable) for _ in range(20)) for _ in range(13000000))

Выборка для потока io.BytesIO занимает около 7,5 секунд, а удаление из потока io.BytesIO - 9 секунд. Если используется многопроцессорное решение, может быть выгодно один раз протолкнуть объект концепта (в любой форме) на жесткий диск, а затем каждый процесс отрывать от жесткого диска вместо того, чтобы проталкивать/отрывать на каждой стороне IPC каждый раз новый процесс. создан, безусловно, стоит попробовать - YMMV. На моем жестком диске маринованный набор - 380 МБ.

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

Я собираюсь опубликовать еще один ответ, касающийся метода тестирования понятий в предложениях.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню