Как улучшить производительность скрипта, работающего с большим объемом данных?
Мой скрипт машинного обучения производит много данных (миллионы BTree
s содержится в одном корне BTree
) и сохранить его в ZODB FileStorage
Главным образом потому, что все это не помещается в ОЗУ. Скрипт также часто изменяет ранее добавленные данные.
Когда я увеличил сложность проблемы и, следовательно, нужно было хранить больше данных, я заметил проблемы с производительностью: теперь сценарий вычисляет данные в среднем в два-десять раз медленнее (единственное, что изменилось, - это объем данных, которые нужно сохранить. и позже извлекается для изменения).
Я пробовал настройку cache_size
к различным значениям между 1000 и 50000. Если честно, различия в скорости были незначительны.
Я думал о переходе на RelStorage
но, к сожалению, в документации они упоминают только о том, как настроить фреймворки, такие как Zope или Plone. Я использую только ZODB.
Интересно, если RelStorage
будет быстрее в моем случае.
Вот как я сейчас настраиваю соединение ZODB:
import ZODB
connection = ZODB.connection('zodb.fs', ...)
dbroot = connection.root()
Для меня ясно, что ZODB в настоящее время является узким местом моего сценария. Я ищу совет о том, как я мог решить эту проблему.
Я выбрал ZODB, потому что думал, что база данных NoSQL лучше подойдет для моего случая, и мне понравилась идея интерфейса, похожего на Python. dict
,
Код и структуры данных:
корневые структуры данных:
if not hasattr(dbroot, 'actions_values'): dbroot.actions_values = BTree() if not hasattr(dbroot, 'games_played'): dbroot.games_played = 0
actions_values
концептуально построен следующим образом:actions_values = { # BTree str(state): { # BTree # contiains actions (coulmn to pick to be exact, as I'm working on agent playing Connect 4) # and their values(only actions previously taken by the angent are present here), e.g.: 1: 0.4356 5: 0.3456 }, # other states }
state
простой 2D массив, представляющий игровую доску Возможные значения его полей:1
,2
или жеNone
:board = [ [ None ] * cols for _ in xrange(rows) ]
(в моем случае
rows = 6
а такжеcols = 7
)основной цикл:
should_play = 10000000 transactions_freq = 10000 packing_freq = 50000 player = ReinforcementPlayer(dbroot.actions_values, config) while dbroot.games_played < should_play: # max_epsilon at start and then linearly drops to min_epsilon: epsilon = max_epsilon - (max_epsilon - min_epsilon) * dbroot.games_played / (should_play - 1) dbroot.games_played += 1 sys.stdout.write('\rPlaying game %d of %d' % (dbroot.games_played, should_play)) sys.stdout.flush() board_state = player.play_game(epsilon) if(dbroot.games_played % transactions_freq == 0): print('Commiting...') transaction.commit() if(dbroot.games_played % packing_freq == 0): print('Packing DB...') connection.db().pack()
(
pack
Он также занимает много времени, но это не главная проблема; Я мог бы упаковать базу данных после завершения программы)Код работает на
dbroot
(внутриReinforcementPlayer
):def get_actions_with_values(self, player_id, state): if player_id == 1: lookup_state = state else: lookup_state = state.switch_players() lookup_state_str = str(lookup_state) if lookup_state_str in self.actions_values: return self.actions_values[lookup_state_str] mirror_lookup_state_str = str(lookup_state.mirror()) if mirror_lookup_state_str in self.actions_values: return self.mirror_actions(self.actions_values[mirror_lookup_state_str]) return None def get_value_of_action(self, player_id, state, action, default=0): actions = self.get_actions_with_values(player_id, state) if actions is None: return default return actions.get(action, default) def set_value_of_action(self, player_id, state, action, value): if player_id == 1: lookup_state = state else: lookup_state = state.switch_players() lookup_state_str = str(lookup_state) if lookup_state_str in self.actions_values: self.actions_values[lookup_state_str][action] = value return mirror_lookup_state_str = str(lookup_state.mirror()) if mirror_lookup_state_str in self.actions_values: self.actions_values[mirror_lookup_state_str][self.mirror_action(action)] = value return self.actions_values[lookup_state_str] = BTree() self.actions_values[lookup_state_str][action] = value
(Функции с зеркалом в названии просто меняют столбцы (действия). Это делается, потому что соедините 4 платы, вертикальные отражения друг от друга эквивалентны.)
После 550000 игр len(dbroot.actions_values)
6018450.
В соответствии с iotop
Операции ввода-вывода занимают 90% времени.
2 ответа
Использование любой (другой) базы данных, вероятно, не поможет, так как они подвержены тем же дисковым операциям ввода-вывода и ограничениям памяти, что и ZODB. Если вам удастся переложить вычисления на саму движок базы данных (PostgreSQL + с использованием сценариев SQL), это может помочь, так как движок базы данных будет иметь больше информации для интеллектуального выбора способов выполнения кода, но здесь нет ничего волшебного, и такие же вещи могут Скорее всего, сделать это с ZODB довольно легко.
Несколько идей, что можно сделать:
Иметь индексы данных вместо загрузки полных объектов (равнозначно SQL "полное сканирование таблицы"). Ведите интеллектуальную предварительную обработку копий данных: индексы, суммы, частичные.
Сделайте сами объекты меньше (классы Python имеют
__slots__
трюк)Используйте транзакции разумно. Не пытайтесь обрабатывать все данные в один большой кусок.
Параллельная обработка - используйте все ядра ЦП вместо однопоточного подхода
Не используйте BTrees - может быть, есть что-то более эффективное для вашего случая использования
Наличие некоторых примеров кода вашего скрипта, фактических размеров RAM и Data.fs и т. Д. Поможет здесь дать дальнейшие идеи.
Просто чтобы прояснить, какой класс BTree вы на самом деле используете? OOBTree?
Два аспекта об этих деревьях:
1) Каждый BTree состоит из нескольких блоков. Каждое ведро будет содержать определенное количество предметов перед разделением. Я не могу вспомнить, сколько предметов они хранят в настоящее время, но однажды я попытался настроить C-код для них и перекомпилировать, чтобы держать большее число, поскольку выбранное значение было выбрано почти два десятилетия назад.
2) Иногда можно построить очень несбалансированные деревья. например, если вы добавляете значения в отсортированном порядке (например, отметка времени, которая только увеличивается), в итоге вы получите дерево, которое в конечном итоге будет O(n) для поиска. Был сценарий, написанный людьми из Jarn несколько лет назад, который мог бы сбалансировать BTrees в Каталоге Zope, который может быть адаптирован для вас.
3) Вместо использования OOBTree вы можете использовать OOBucket. В конечном итоге это будет всего лишь один пик в ZODB, поэтому может оказаться слишком большим в вашем случае использования, но если вы выполняете все записи в одной транзакции, чем это может быть быстрее (за счет необходимости напиши все ведро на обновление).
Матф