2016-01-04 4 views
1

Мой сценарий машинного обучения дает много данных (миллионы BTree s содержится в одном корне BTree) и сохраняет его в FileStorage ZODB, главным образом потому, что все это не поместится в ОЗУ. Скрипт также часто изменяет ранее добавленные данные.Как повысить производительность скрипта, работающего на большом количестве данных?

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

Я попытался установить cache_size на различные значения между 1000 и 50000. Честно говоря, различия в скорости были незначительными.

Я думал о переключении на RelStorage, но, к сожалению, в the docs они упоминают только, как настроить фреймворки, такие как Zope или Plone. Я использую только ZODB.

Интересно, будет ли RelStorage быстрее в моем случае.

Вот как я в настоящее время установки ZODB подключение:

import ZODB 
connection = ZODB.connection('zodb.fs', ...) 
dbroot = connection.root() 

Это для меня ясно, что ZODB в настоящее время является узким местом моего сценария. Я ищу совет, как я мог решить эту проблему.

Я выбрал ZODB beacuse Я думал, что база данных 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 
    

    (функции с зеркалом названия просто поменять местами столбцы (действия).Это делается beacuse Connect 4 доски, которые являются вертикальными отражениями друг друга эквивалентны.)


После 550000 игр len(dbroot.actions_values) является 6018450.


Согласно iotop операции ввода-вывод принимает 90% времени.

+1

Ваш * размер данных * является узким местом. Здесь только ZODB может сделать это. Я не уверен, что RelStorage поможет здесь; вам нужно создать адаптер (выберите один из них, соответствующий вашей выбранной базе данных из пакета '' адаптеры '' (https://github.com/zodb/relstorage/tree/master/relostorage/adapters)) и передайте его ['RelStorage()' instance] (https://github.com/zodb/relstorage/blob/master/relstorage/storage.py#L78); используйте этот экземпляр вместо объекта FileStorage. –

+0

@MartijnPieters Что может здесь помочь? Это первый раз, когда мне приходится иметь дело с этим большим количеством данных. Есть ли какая-нибудь другая душа, которая подойдет лучше? Я немного ошеломлен перспективой ждать неделю, пока мой алгоритм не закончится. :) – Luke

+1

Микко вы там накрыли. –

ответ

2

Использование любой (другой) базы данных, вероятно, не поможет, поскольку они подвержены тем же ограничениям ввода-вывода IO и ограничениям памяти, что и ZODB. Если вам удастся выгрузить вычисления самому механизму базы данных (PostgreSQL + с помощью SQL-скриптов), это может помочь, поскольку механизм базы данных будет иметь больше информации, чтобы сделать разумные варианты выполнения кода, но нет ничего волшебного здесь, и то же самое можно сделать скорее всего, с ZODB с легкостью.

Некоторые идеи, что можно сделать:

  • индексироваться данных вместо загрузки полных объектов (равные SQL «полное сканирование таблицы»). Храните интеллектуальные препроцессы копии данных: индексы, суммы, частичные.

  • сделать объекты сами по себе мелкие (классы Python имеют __slots__ трюк)

  • используются транзакции разумным способом. Не пытайтесь обрабатывать все данные в одном большом фрагменте.

  • Параллельная обработка - использовать все ядра процессора вместо однотридовой подхода

  • Не используйте BTrees - может быть, есть что-то более эффективным для случая использования

Имея некоторые примеры кода ваш сценарий, фактические размеры RAM и Data.fs и т. д. помогут здесь дать дополнительные идеи.

+1

Я обновил свой вопрос с помощью образцов кода и данных. – Luke

+1

Что бы я сделал, это посмотреть, привязаны ли вы к процессору или привязаны к IO. Например. наблюдайте 'htop' или' top' во время выполнения скрипта. Если вы связаны с ЦП, я бы разделил основной цикл, так что он использует несколько потоков или процессов для перебора данных. Если вы связаны с IO (я сомневаюсь), вы можете сделать осколок отдельного ZODB на несколько физических дисков (компьютеров). Я понял (все) игры не зависят друг от друга? –

+1

@ miko-ohtaama Вы сомневаетесь, но это правда. Я был связан с ЦП, когда размер проблемы был меньше. Тогда «Активность системы» в KDE показала 25%, что, я думаю, означает max. на одном ядре). Теперь он показывает около 15% или меньше. – Luke

1

Для того, чтобы быть понятным здесь, какой класс BTree вы используете? OOBTree?

два аспекта об этих btrees:

1) Каждый BTree состоит из ряда Ковши. Перед каждым разделом каждый ковш будет удерживать определенное количество элементов. Я не могу вспомнить, сколько предметов они хранят в настоящее время, но однажды я попытался настроить C-код для них и перекомпилировать, чтобы удерживать большее число, поскольку выбранное значение было выбрано почти два десятилетия назад.

2) Иногда возможно построить очень не сбалансированные Btrees. например если вы добавите значения в отсортированном порядке (например, временную метку, которая только когда-либо возрастает), вы получите дерево, которое заканчивается тем, что O (n) выполняет поиск. Несколько лет назад был написан сценарий, написанный людьми в Jarn, который мог бы перебалансировать BTrees в Zope's Catalog, который может быть адаптирован для вас.

3) Вместо использования OOBTree вы можете вместо этого использовать OOBucket. В конечном итоге это будет всего лишь один маринад в ZODB, поэтому может оказаться слишком большим в вашем случае использования, но если вы делаете все записи в одной транзакции, чем это может быть быстрее (за счет необходимости повторного использования, напишите весь Bucket при обновлении).

-Matt

Смежные вопросы