Реализация алгоритма TD-Gammon

Я пытаюсь реализовать алгоритм из статьи TD-Gammon Джеральда. Тезауро. Ядро алгоритма обучения описано в следующем абзаце:

введите описание изображения здесь

Я решил иметь один скрытый слой (если этого было достаточно, чтобы играть в нарды мирового класса в начале 1990-х, то и мне этого достаточно). Я почти уверен, что все, кроме функции train(), корректно (их легче тестировать), но я понятия не имею, правильно ли я реализовал этот окончательный алгоритм.

import numpy as np

class TD_network:
    """
    Neural network with a single hidden layer and a Temporal Displacement training algorithm
    taken from G. Tesauro's 1995 TD-Gammon article.
    """
    def __init__(self, num_input, num_hidden, num_output, hnorm, dhnorm, onorm, donorm):
        self.w21 = 2*np.random.rand(num_hidden, num_input) - 1
        self.w32 = 2*np.random.rand(num_output, num_hidden) - 1
        self.b2 = 2*np.random.rand(num_hidden) - 1
        self.b3 = 2*np.random.rand(num_output) - 1
        self.hnorm = hnorm
        self.dhnorm = dhnorm
        self.onorm = onorm
        self.donorm = donorm

    def value(self, input):
        """Evaluates the NN output"""
        assert(input.shape == self.w21[1,:].shape)
        h = self.w21.dot(input) + self.b2
        hn = self.hnorm(h)
        o = self.w32.dot(hn) + self.b3
        return(self.onorm(o))

    def gradient(self, input):
        """
        Calculates the gradient of the NN at the given input. Outputs a list of dictionaries
        where each dict corresponds to the gradient of an output node, and each element in
        a given dict gives the gradient for a subset of the weights. 
        """ 
        assert(input.shape == self.w21[1,:].shape)
        J = []
        h = self.w21.dot(input) + self.b2
        hn = self.hnorm(h)
        o = self.w32.dot(hn) + self.b3

        for i in range(len(self.b3)):
            db3 = np.zeros(self.b3.shape)
            db3[i] = self.donorm(o[i])

            dw32 = np.zeros(self.w32.shape)
            dw32[i, :] = self.donorm(o[i])*hn

            db2 = np.multiply(self.dhnorm(h), self.w32[i,:])*self.donorm(o[i])
            dw21 = np.transpose(np.outer(input, db2))

            J.append(dict(db3 = db3, dw32 = dw32, db2 = db2, dw21 = dw21))
        return(J)

    def train(self, input_states, end_result, a = 0.1, l = 0.7):
        """
        Trains the network using a single series of input states representing a game from beginning
        to end, and a final (supervised / desired) output for the end state
        """
        outputs = [self(input_state) for input_state in input_states]
        outputs.append(end_result)
        for t in range(len(input_states)):
            delta = dict(
                db3 = np.zeros(self.b3.shape),
                dw32 = np.zeros(self.w32.shape),
                db2 = np.zeros(self.b2.shape),
                dw21 = np.zeros(self.w21.shape))
            grad = self.gradient(input_states[t])
            for i in range(len(self.b3)):
                for key in delta.keys():
                    td_sum = sum([l**(t-k)*grad[i][key] for k in range(t + 1)])
                    delta[key] += a*(outputs[t + 1][i] - outputs[t][i])*td_sum
            self.w21 += delta["dw21"]
            self.w32 += delta["dw32"]
            self.b2 += delta["db2"]
            self.b3 += delta["db3"]

Я использую это так: я играю всю игру (точнее, нейронная сеть играет против самой себя), а затем отправляю состояния этой игры от начала до конца в train() вместе с окончательным результатом. Затем он берет этот игровой журнал и применяет приведенную выше формулу для изменения весов, используя первое состояние игры, затем первое и второе состояния игры и так далее до последнего момента, когда он использует весь список состояний игры. Затем я повторяю это много раз и надеюсь, что сеть узнает.

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

Однако я понятия не имею, правильно ли это, поскольку до сих пор я не смог заставить его играть в крестики-нолики на каком-либо разумном уровне. Тому может быть много причин. Возможно, я не даю достаточно скрытых узлов (я использовал от 10 до 12). Может быть, для тренировки нужно больше игр (я использовал 200 000). Возможно, было бы лучше с разными функциями нормализации (я пробовал сигмовидную и ReLU, дырявую и не дырявую, в разных вариациях). Возможно, параметры обучения настроены неправильно. Возможно, крестики-нолики и их детерминированный геймплей означают, что они «запираются» на определенных путях в дереве игры. Или, может быть, реализация обучения просто неверна. Вот почему я здесь.

Я неправильно понял алгоритм Тезауро?


comment
Привет, Артур, у меня нет ответа, но я вспомнил несколько слов Рича Саттона, чтобы представить сложность проблемы в контексте: основная причина неудачи заключается в том, что обратное распространение довольно сложно эффективно использовать, вдвойне сложно в онлайн-приложение, такое как обучение с подкреплением. Это правда, что Тезауро использовал этот подход в своем поразительно успешном приложении для игры в нарды, но обратите внимание, что во время работы с TDgammon Тезауро уже был экспертом в применении сетей обратного распространения к нардам. [...]   -  person Pablo EM    schedule 26.11.2019
comment
[...] Он уже создал лучшего в мире компьютерного игрока в нарды, используя обратные сети. Он уже изучил все приемы, настройки и настройки параметров, чтобы сети обратного распространения хорошо обучались. Если у вас нет столь же обширного опыта, вы, вероятно, будете очень разочарованы использованием сети обратного распространения в обучении с подкреплением. incompleteideas.net/RL-FAQ.html#backpropagation . Этот комментарий был написан несколько лет назад, и я не уверен, насколько он актуален сегодня. Но я думаю, что люди склонны недооценивать сложность сочетания RL + Backprop.   -  person Pablo EM    schedule 26.11.2019
comment
@PabloEM Я начинаю тебе верить. Не думал, что это так сложно. Хотя я не знаю, что проще. Может генетический алгоритм? Ну что ж.   -  person Arthur    schedule 27.11.2019


Ответы (1)


Я не могу сказать, что я полностью понимаю вашу реализацию, но мне бросается в глаза эта строка:

                    td_sum = sum([l**(t-k)*grad[i][key] for k in range(t + 1)])

Сравнивая с формулой, на которую вы ссылаетесь:

Я вижу как минимум два отличия:

  • Ваша реализация суммирует более t+1 элементов по сравнению с t элементами в формуле
  • Градиент должен быть проиндексирован тем же k, что и в l**(t-k), но в вашей реализации он проиндексирован i и key без какой-либо ссылки на k.

Возможно, если вы исправите эти несоответствия, ваше решение будет вести себя более ожидаемо.

person Seb    schedule 27.11.2019
comment
Полностью с вами согласен. Мне придется переместить вычисление grad так, чтобы вместо этого мы получили self.gradient(input_states[k]). И я думаю, что поставил t+1, потому что сумма в статье достигает k = t. Так что теперь вместо этого у меня есть td_sum = sum([l**(t-k - 1)*self.gradient(input_states[k])[i][key] for k in range(t)]), хотя это тоже не работает. Но я до сих пор не знаю, то ли это неправильная реализация, то ли я просто использую неправильные настройки. - person Arthur; 27.11.2019
comment
Я решил дать это награду, потому что, хотя моя проблема не полностью решена, ответ был полезен. Кроме того, награда была опубликована не для того, чтобы получить решение (хотя это и было конечной целью), а для того, чтобы привлечь больше внимания. Это внимание принесло этот ответ, так что вот куда пойдет награда. - person Arthur; 02.12.2019
comment
Большое спасибо, ценю это. Хотел бы я быть более полезным! Достигли ли вы какого-либо прогресса за это время? - person Seb; 02.12.2019
comment
Нет, я не видел. В основном потому, что у меня не было много времени, чтобы работать над этим. Но, прочитав комментарии Пабло выше, я более или менее решил, что откажусь от этого подхода и предпочитаю что-то более простое, например генетический алгоритм. Просто добавьте в игру кучу нейронных сетей и оставьте те, которые работают хорошо. Кажется, что есть меньше тонкой настройки, которая может пойти не так. - person Arthur; 02.12.2019