Как улучшить производительность скрипта, работающего с большим объемом данных?

Мой скрипт машинного обучения производит много данных (миллионы BTrees содержится в одном корне 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, поэтому может оказаться слишком большим в вашем случае использования, но если вы выполняете все записи в одной транзакции, чем это может быть быстрее (за счет необходимости напиши все ведро на обновление).

Матф

Другие вопросы по тегам