Введение

Привет! Я Хавьер Галдеано. Я недавно получивший диплом инженера-информатика и интересуюсь применением технологий в реальных жизненных ситуациях. Поскольку мне очень интересен искусственный интеллект, я разработаю несколько простых проектов, чтобы начать свое путешествие, надеясь, что мы сможем поделиться точками зрения и узнать что-то новое!

Это также моя первая статья на Medium, так что… надеюсь, она вам понравится!

Обучение с подкреплением

В настоящее время рассматриваются два основных метода ИИ: контролируемое и неконтролируемое обучение. Тем не менее, обучение с подкреплением считается новой парадигмой ИИ, которую обычно называют типом метода обучения без учителя.

В задаче RL агент узнает, что делать в любой ситуации, чтобы максимизировать вознаграждение. Он учится главным образом путем определения того, какие действия приносят наибольшее вознаграждение, просто пробуя их во время тренировки. Поскольку это основная идея, давайте посмотрим на компоненты проблемы RL:

Элементы проблемы RL

  • Агент: это рассказчик действий. Я научу его предпринимать действия, максимизирующие вознаграждение.
  • Среда: это мир, с которым агент столкнется при выполнении действий. Это будут все исторические данные о ценах, которые мы рассматриваем для торговли.
  • Состояние: это текущее представление среды. Это фактическое состояние окружающей среды, то есть данные того дня, в котором мы с нетерпением ожидаем действий.
  • Действие: это решения, которые агент может принять в каждом отдельном состоянии. В моем случае я рассмотрел три основных варианта: продать, не действовать, купить.
  • Награда: выгода от совершения действия в определенном состоянии. Мы просто посчитаем разницу между дневными ценами.
  • Политика: это стратегия, которой будет следовать агент, чтобы максимизировать долгосрочное вознаграждение. Это может быть максимизация выгоды, минимизация потерь… Вариантов много. Я рассмотрю возможность получить максимальную выгоду.

Q-Learning

Понимание элементов нашей проблемы… Что можно считать решением проблемы RL? Хорошо…

«Решение задачи обучения с подкреплением означает, грубо говоря, поиск политики, которая принесет большую выгоду в долгосрочной перспективе. Политика π считается лучшей или равной политике π’, если ее ожидаемая доходность больше или равна доходности π’ для всех государств». [1]

Было реализовано несколько методов, в том числе Q-Learning, примененный в этой статье. Q-Learning представлял собой развитие алгоритма управления временной разницей вне политики, реализованного в 1989 году Крисом Уоткинсом.

«В этом случае изученная функция ценности действия Q напрямую аппроксимирует q*, оптимальную функцию ценности действия, независимую от проводимой политики» [1]

Какие элементы мы должны учитывать в задаче Q-Learning?

  • Значение Q: обычно обозначается как Q(s,a) и соответствует ожидаемому вознаграждению после выбора действия в определенном состоянии в соответствии со стратегией принятия решения. S известен как состояние, а A – это действие.
  • Q-функция: это функциональное представление Q-значений, обычно применяемое для решения сложных задач с непрерывными состояниями и действиями.
  • Q-Table: это структура данных, в которой хранятся значения Q для каждой пары состояния и действия. Q-Table используется для хранения и обновления Q-значений по мере завершения этапов обучения путем взаимодействия с окружающей средой.

Процедура обновления Q-таблицы осуществляется с помощью уравнения Беллмана:

Где:

  • Q(s,a): текущее значение Q.
  • α: известный как скорость обучения. Этот параметр определяет величину обновления Q-значений во время обучения. Его диапазон значений — [0,1].
  • γ: известный как коэффициент скидки. Этот параметр помогает агенту выбрать действие, обеспечивая значение важности между немедленными вознаграждениями и будущими. Чем выше значение, тем больше будет предпочтение немедленному вознаграждению.
  • R: ожидается награда.
  • Max(Q(s’,a’)): наибольшее значение Q, хранящееся в таблице Q для состояния, между всеми возможными действиями.

Теперь, когда я представил простой обзор такого рода проблем… Давайте начнем с реализации:

Часть 1. Окружающая среда

Как было сказано ранее, среда — это все данные, которые у меня будут для обучения агента, что в данном случае ограничено рынком, который мы используем. Я выбрал биткойн/евро. Давайте посмотрим, как это выглядит:

Но знайте, давайте подумаем о теории, упомянутой ранее... Если наш агент должен быть обучен для каждого отдельного состояния (которое будет предоставлено данными OHLCV), принимая во внимание уникальную информацию, которую каждый день получает... Будет ли когда-нибудь состояние повторяться в будущее?

Я искренне считаю, что пытаться обучить агента без преобразования данных — это пустая трата времени, так что давайте сделаем это! При определении среды, с которой столкнется агент, я буду учитывать Скорость изменений или ROC. Он определяется как изменение между двумя ценами за определенный период времени. Давайте проверим формулу:

Где «Цена закрытия X» — это фактическая цена, а «Цена закрытия X-N» — это цена за N дней до фактической. Теперь у нас может быть более интересное представление данных, но по-прежнему много разных состояний, поэтому я создам категории для этих преобразований:

Если разделить набор данных на эти категории, состояния станут более частыми, что сделает агент более способным извлекать закономерности, которые можно будет применить для принятия решений. Включенные периоды будут N = {1,7,14,30}.

Прежде всего, учитывая все данные и категории, штаты будут иметь следующую структуру:

Зная это, строк Q-таблицы будет столько, сколько уникальных состояний было обработано.

Часть 2. Обучение агента

Давайте перейдем непосредственно к этапу обучения. Сначала нам нужно определить нашего агента:

class AgenteQ:
    
    def __init__(self, states, num_states, num_actions, learning_rate, discount_factor):
        """
        Default constructor

        @param num_states: number of existent states in dataset
        @param num_actions: number of possible actions
        @param learning_rate: magnitude of updating q-values
        @param discount_factor: relative importance of long-term rewards
        """
        self.num_states = num_states
        self.num_actions = num_actions
        self.learning_rate  = learning_rate
        self.discount_factor = discount_factor
        self.q_table = np.zeros((num_states,num_actions))
        self.states = np.array(states)

Далее нам нужно определить метод, при котором агент будет выбирать лучшее действие:

    def choose_action(self,state,epsilon):
        """
        Function used to choose the optimal action through a Epsilon-Greedy policy

        @param state: state the agent is facing
        @param epsilon: exploration-exploitation ratio of actions
        """
        if np.random.uniform() < epsilon:
            return int(np.random.choice(self.num_actions))
        else:
            index = np.where((self.states == state).all(axis=1))
            if index[0].size == 0:
                return int(0)
            else:
                return int(np.argmax(self.q_table[index[0],:]))

Наконец, я определю метод, который поможет нам обновить фактические значения Q в таблице Q агента:

   def actualize_q_table(self,state,action,reward,next_state):
        """
        Function to update de Q-Values.
        
        Q(s,a)->Q(s,a)+α⋅(r+γ*max(a')*Q(s',a')-Q(s,a))
            Q(s,a) ===> valor actual para el par state-acción en la Q-Table.
            α ========> tasa de aprendizaje (cuan agresivo se produce el ajuste de valores en la Q-Table)
            r ========> reward obtenida al tomar la acción 'a' en el state 's'
            γ ========> factor de descuento (ponderación de importancia de rewards futuras-inmediatas)
            max(a')*Q(s',a') ===> máximo valor de la Q-Table para las posibles actiones en el siguiente state (estimación del valor futuro)

        @param state: state to update
        @param action: action choosen
        @param reward: reward for taking the action
        @param next_state: following state
        """

        index = np.where((self.states == state).all(axis=1))
        next_state = np.where((self.states == next_state).all(axis=1))

        # Actual Value -> Q(s,a)
        valor_q_actual = self.q_table[index[0],action]

        # Max. value for the next state -> max(a')*Q(s',a')
        valor_q_next = np.max(self.q_table[next_state[0]])

        # r+γ*max(a')*Q(s',a')
        valor_objective = reward + self.learning_rate * valor_q_next

        # Update the Q-Values of the Q-Table
        self.q_table[index[0],action] = valor_q_actual + self.learning_rate*(valor_objective-valor_q_actual)

Однако… Как мы можем инициализировать нашу Q-таблицу? Ну, по этой теме обсуждаются разные точки зрения. Хотя некоторые люди решают инициализировать Q-таблицу случайными значениями, есть возможность инициализировать ее 0 (и это то, что я выбираю). Итак, первый вид Q-Table будет выглядеть так:

Далее я определю метод обучения:

def train_agent(states, dataOHLCV, q_value_agent, initial_investment, num_episodes):
        """
        Function to train the agent
        @param states: states provided to the training phase
        @param dataOHLCV: OHLCV of the states provided
        @param q_value_agent: instance of the define agent
        @param initial_investment: initial portfolio value
        @num_episodes: number of episodes of the training phase
        """
        max_epsilon = 1.00
        min_epsilon = 0.00
        exploration_decay_rate = 0.01
        epsilon = max_epsilon

        portfolio_performance = []

        for iteration in range(num_episodes):
            if epsilon-exploration_decay_rate >= min_epsilon:
                epsilon = epsilon - exploration_decay_rate

            Owned = False
            Portfolio_Value = []
            Portfolio_Value.append(initial_investment)
            QuantityOwned = np.float64(0)

            for i in range(len(states)-1):
                # Índices
                next = states[i+1]
                state = i
    
                # reward
                reward = 0

                # action a realizar
                action = q_value_agent.choose_action(states[i],epsilon)

                if action == 0:
                    if Owned == False:
                        reward = (-1)*(((dataOHLCV[i+1,3]-dataOHLCV[i,3])/dataOHLCV[i,3]))*100
                        q_value_agent.update_q_table(states[i],action,reward,next)
                        Portfolio_Value.append(Portfolio_Value[i])
                    else:
                        reward = (-1)*(((dataOHLCV[i+1,3]-dataOHLCV[i,3])/dataOHLCV[i,3]))*100
                        q_value_agent.update_q_table(states[i],action,reward,next)
                        Owned = False
                        Portfolio_Value.append(QuantityOwned*dataOHLCV[i,3])
                        QuantityOwned = 0

                elif action == 1:
                    if Owned == False:
                        reward = ((dataOHLCV[i+1,3]-dataOHLCV[i,3])/dataOHLCV[i,3])*100
                        q_value_agent.update_q_table(states[i],action,reward,next)
                        Portfolio_Value.append(Portfolio_Value[i])
                    else:
                        reward = ((dataOHLCV[i+1,3]-dataOHLCV[i,3])/dataOHLCV[i,3])*100
                        q_value_agent.update_q_table(states[i],action,reward,next)
                        Portfolio_Value.append(QuantityOwned*dataOHLCV[i,3])

                elif action == 2:
                    if Owned == False:
                        reward = ((dataOHLCV[i+1,3]-dataOHLCV[i,3])/dataOHLCV[i,3])*100
                        q_value_agent.update_q_table(states[i],action,reward,next)
                        Owned = True
                        QuantityOwned = Portfolio_Value[i-1]/dataOHLCV[i,3]
                        Portfolio_Value.append(Portfolio_Value[i])
                    else:
                        reward = ((dataOHLCV[i+1,3]-dataOHLCV[i,3])/dataOHLCV[i,3])*100
                        q_value_agent.update_q_table(states[i],action,reward,next)
                        Portfolio_Value.append(QuantityOwned*dataOHLCV[i,3])                    
            portfolio_performance.append(Portfolio_Value[len(Portfolio_Value)-1])
            
        return Portfolio_Value,  portfolio_performance

Затем, после этапа обучения, Q-Table будет иметь следующий вид:

Напоследок проверим, какова отдача нашего агента во время обучения! Два разных графика: окончательный доход нашего агента и окончательный доход за эпоху во время обучения:

Часть 3. Тестирование агента

Это будет выпускной экзамен агента. Теперь он столкнется с неизвестными данными. Что я буду считать хорошим результатом? Что ж, в данном случае, учитывая, что это первая проблема RL, которую я пытаюсь решить, я буду удовлетворен только доходностью по сравнению со стратегией «Купи и держи».

Давайте быстро рассмотрим наш метод тестирования:

def act(states, dataOHLCV, q_value_agent, initial_investment):
    portfolio_performance = []
    epsilon = 0.001
    Owned=False
    
    Portfolio_Value = []
    Portfolio_Value.append(initial_investment)
    
    QuantityOwned = np.float64(0)

    for i in range(len(states)-1):
        action = q_value_agent.choose_action(states[i],epsilon)
        
        if action == 0:
            if Owned == False:
                Portfolio_Value.append(Portfolio_Value[i])
            else:
                Owned = False
                Portfolio_Value.append(QuantityOwned*dataOHLCV[i,3])
                QuantityOwned = 0

        elif action == 1:
            if Owned == False:
                Portfolio_Value.append(Portfolio_Value[i])
            else:
                Portfolio_Value.append(QuantityOwned*dataOHLCV[i,3])

        elif action == 2:
            if Owned == False:
                Owned = True
                QuantityOwned = Portfolio_Value[i-1]/dataOHLCV[i,3]
                Portfolio_Value.append(Portfolio_Value[i])
            else:
                Portfolio_Value.append(QuantityOwned*dataOHLCV[i,3])         

        portfolio_performance.append(Portfolio_Value[len(Portfolio_Value)-1])
    return Portfolio_Value

Давайте завершим этот проект! Вот стоимость портфеля на этапе тестирования:

Как видите, агент не потерял в цене на этапе тестирования, но итоговая стоимость все равно ниже, чем у стратегии Buy & Hold:

  • Окончательная стоимость: 10 538,16 евро.
  • Купить и удерживать возврат: 12 474,00 евро.

Несмотря на то, что Buy & Hold одерживает победу, очевидно, что есть куда совершенствоваться. Многообещающая информация, которую мы смогли извлечь, заключается в том, что максимальное значение, полученное в ходе тестирования, составило 11,994,00 евро, что ближе к стратегии «Купи и держи». Нам нужно подчеркнуть несколько вещей:

  • Я только что предоставил агенту данные ROC, и очевидно, что это даже не торговая стратегия. В будущем можно будет попробовать более сложные варианты.
  • Никакого сложного этапа предварительной обработки. Обычно корреляционный анализ необходимо проводить для улучшения качества данных, предоставляемых агенту.
  • Никакой сложной настройки гиперпараметров не производилось. Это сложная проблема сама по себе.

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

Спасибо за прочтение!

Ну, вот и все. Надеюсь, вам понравится мой первый пост. Я впервые пишу в блоге и не совсем уверен, поможет ли это читателю начать знакомство с RL.

Принимая это во внимание, каждое сомнение или совет, который вы хотите мне дать… буду очень признателен! Надеюсь, скоро увидимся в другом проекте!

Рекомендации

Вот несколько рекомендуемых лекций для полного понимания RL и Q-Learning, использованных в статье:

  • [1] Р. С. Саттон и А. Г. Барто, Обучение с подкреплением: введение (второе издание).
  • [2] Ю. Хилпиш, Python для финансов: освоение финансов, управляемых данными.
  • [3] Дж. П. Мюллер и Л. Массарон, Машинное обучение для чайников.