У меня есть список 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?
Этот ответ будет касаться улучшения производительности без использования параллелизма.
Как вы структурировали свой поиск, вы ищете 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']]
data
- это список concepts
и мне нужно добавить found
результаты в output
? Я запишу это на своем реальном наборе данных и сообщу вам результаты. очень взволнован, чтобы бежать.
data
, nwise
, found
. Можете ли вы опубликовать полный код? С нетерпением жду Вашего ответа.
Приведет ли многопоточность к реальному увеличению производительности, зависит не только от реализации в 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())
Еще больше накладных расходов, но с использованием ресурсов ЦП, доступных и на других ядрах.
num_threads = 2
на то, что вы считаете оптимальным. Вы можете протестировать немного с меньшим набором (но больше, чем указано в примере) и использовать некоторые функции синхронизации, чтобы решить, какое количество потоков является оптимальным для вашей системы. Чем больше, тем лучше, потому что ваше оборудование и операционная система могут делать только столько параллельно.
Вот два решения, использующие 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, я продолжал взрывать свой компьютер, потому что каждому процессу требовалась своя копия набора, а на моем компьютере просто не было оперативной памяти.
Я собираюсь опубликовать еще один ответ, касающийся метода тестирования понятий в предложениях.