Всем снова привет! В прошлом году я опубликовал несколько руководств по финансовому прогнозированию с использованием нейронных сетей, и я думаю, что некоторые из результатов были по крайней мере интересными и достойными применения в реальных торговых приложениях. Если вы их читали, то, должно быть, заметили, что когда вы пытаетесь подогнать какую-то модель машинного обучения к «случайным» данным в надежде найти скрытые закономерности, вы, как правило, сильно перестраиваетесь под набор поездов. Мы использовали различные методы регуляризации и дополнительные данные для решения этой проблемы, но это требует очень много времени и немного напоминает слепой поиск.

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

Я также рекомендую проверить свои предыдущие уроки по финансовому прогнозированию с помощью нейронных сетей:

  1. Простое прогнозирование временных рядов (и сделанные ошибки)
  2. Корректное прогнозирование одномерных временных рядов + бэктестинг
  3. Многомерное прогнозирование временных рядов
  4. Прогнозирование волатильности и нестандартные убытки
  5. Многозадачное и мультимодальное обучение
  6. Оптимизация гиперпараметров
  7. Улучшение классических стратегий с помощью нейронных сетей
  8. Вероятностное программирование и пиропрогнозы
  9. Бэктестинг в пандах

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

и проверить следующие библиотеки Python:

Вероятностное программирование

Что это за вероятностная вещь и почему мы называем ее программированием? Прежде всего, давайте вспомним, что такое наши «нормальные» нейронные сети и что мы от них получаем. У нас есть параметры (веса), которые представлены в виде матриц, а выходными данными обычно являются некоторые скалярные значения или векторы (например, в случае классификации). После обучения этой модели, скажем, с помощью SGD, у нас есть фиксированные матрицы, и предполагается, что сеть будет выводить один и тот же вектор на одной и той же входной выборке. И это совершенно правильно! Но что, если бы мы рассматривали все эти параметры и выходы как распределения, зависящие друг от друга? Каждый вес в вашей нейронной сети будет выборкой из некоторого распределения, так же, как выход - выборкой из всей сети, которая зависит от выборок из параметров. Что это нам дает?

Начнем с основ. Если рассматривать нашу сеть как набор зависимых друг от друга распределений, это, прежде всего, совместное распределение вероятностей p (y, z | x), где у нас есть выход y и некоторое «внутреннее», скрытые переменные модели z в зависимости от ввода x (все так же, как с обычными нейронными сетями). Нам интересно найти такие распределения нейронной сети, поэтому мы можем выбрать y ~ p (y | x) и получить наш результат в виде распределения (где ожидаемое значение выборок из этого распределения обычно является выходом, и стандартное отклонение - оценка неопределенности - чем больше хвосты - тем меньше мы уверены в наших результатах).

Эта настройка более или менее ясна, мы просто должны помнить, что теперь все параметры, входы и выходы в нашей модели являются распределениями, и во время обучения нам нужно подбирать параметры этих распределений, чтобы получить лучшую точность в реальной задаче. Мы также должны упомянуть, что форму распределения параметров мы устанавливаем сами (например, говоря, что все начальные веса w ~ Normal (0, 1) и после этого мы узнаем правильное среднее и дисперсия). Начальные распределения называются апостериорными, а распределения с подходящими параметрами после просмотра данных обучения - апостериорными. Последние мы используем для выборки и получения результата.

Как работает примерка модели? Общая схема называется вариационным выводом. Не вдаваясь в подробности, мы можем предположить, что мы хотим найти такую ​​модель, которая максимизировала бы логарифмическое правдоподобие p_w (z | x), где w - параметры модели (параметры распределения), z - наши скрытые переменные (выходы скрытых нейронов, которые выбираются из распределений с параметрами w), а x - выборки входных данных . Это наша модель. В Pyro мы вводим такую ​​сущность в качестве руководства для этой модели, которая будет состоять просто из некоторых распределений для всех скрытых переменных q_ф (z), где ф называются вариационными параметрами. Это распределение должно аппроксимировать «реальное» распределение параметров модели, которое наилучшим образом соответствует данным.

Целью обучения является минимизация ожидаемого значения [log (p_w (z | x)) - log (q_ф (z))] по отношению к входным данным и выборкам из руководства. Мы не будем здесь обсуждать детали учебного процесса, потому что он стоит нескольких университетских курсов и пока займемся оптимизацией черного ящика.

Ах да, зачем программирование? Потому что обычно такие вероятностные модели (как нейронные сети) описываются ориентированными графами от одной переменной к другой, и таким образом мы показываем зависимости переменных напрямую:

И изначально такие вероятностные языки программирования использовались для определения таких моделей и создания на них выводов.

Почему вероятностное программирование?

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

Почему не вероятностное программирование?

У меня еще нет большого опыта в байесовском моделировании, но то, что я узнал при использовании Pyro и PyMC3, означает, что процесс обучения действительно долгий, и трудно определить правильные априорные распределения. Более того, работа с образцами из распределения в производстве приводит к недоразумениям и двусмысленностям.

Подготовка данных

Я удалил данные о дневных ценах на Ethereum отсюда. Они включают в себя типичные кортежи OHLCV (открытое, высокое и низкое закрытие) и, кроме того, количество ежедневных твитов об Ethereum. Мы будем использовать семидневные изменения цены, объема и количества твитов в%, чтобы предсказать процентное изменение на следующий день.

На графике выше вы можете увидеть образец данных: синий - для изменения цены, желтый - для изменения количества твитов, а зеленый - для изменения объема. Между этими значениями существует некоторая положительная корреляция (0,1–0,2), поэтому мы ожидаем использовать некоторые шаблоны в данных для обучения нашей модели.

Байесовская линейная регрессия

Сначала я хотел проверить, как простая линейная регрессия выполнит нашу задачу (и я хотел скопировать результаты из Pyro tutorial). Мы определяем нашу модель в PyTorch следующим образом (более подробные объяснения см. В официальном руководстве):

class RegressionModel(nn.Module):
    def __init__(self, p):
        super(RegressionModel, self).__init__()
        self.linear = nn.Linear(p, 1)
def forward(self, x):
        # x * w + b
        return self.linear(x)

Но это простая детерминированная модель, с которой мы работали, но это способ определения вероятностной модели в Pyro:

def model(data):
    # Create unit normal priors over the parameters
    mu = Variable(torch.zeros(1, p)).type_as(data)
    sigma = Variable(torch.ones(1, p)).type_as(data)
    bias_mu = Variable(torch.zeros(1)).type_as(data)
    bias_sigma = Variable(torch.ones(1)).type_as(data)
    w_prior, b_prior = Normal(mu, sigma), Normal(bias_mu, bias_sigma)
    priors = {'linear.weight': w_prior, 'linear.bias': b_prior}
    lifted_module = pyro.random_module("module", regression_model, priors)
    lifted_reg_model = lifted_module()
with pyro.iarange("map", N, subsample=data):
        x_data = data[:, :-1]
        y_data = data[:, -1]
        # run the regressor forward conditioned on inputs
        prediction_mean = lifted_reg_model(x_data).squeeze()
        pyro.sample("obs",
                    Normal(prediction_mean, Variable(torch.ones(data.size(0))).type_as(data)),
                    obs=y_data.squeeze())

В приведенном выше коде вы можете видеть, что мы установили общие распределения модели линейной регрессии для параметров W и b,, и оба они равны ~ Normal (0, 1 ). Мы называем их априорными, создаем случайную функцию Pyro (в нашем случае RegressionModel в PyTorch), добавляем к ней априорные значения ({'linear.weight': w_prior, 'linear.bias': b_prior}) и выборка из этой модели p (y | x) на основе входных данных x.

А руководство к модели будет выглядеть так:

def guide(data):
    w_mu = Variable(torch.randn(1, p).type_as(data.data), requires_grad=True)
    w_log_sig = Variable(0.1 * torch.ones(1, p).type_as(data.data), requires_grad=True)
    b_mu = Variable(torch.randn(1).type_as(data.data), requires_grad=True)
    b_log_sig = Variable(0.1 * torch.ones(1).type_as(data.data), requires_grad=True)
    mw_param = pyro.param("guide_mean_weight", w_mu)
    sw_param = softplus(pyro.param("guide_log_sigma_weight", w_log_sig))
    mb_param = pyro.param("guide_mean_bias", b_mu)
    sb_param = softplus(pyro.param("guide_log_sigma_bias", b_log_sig))
    w_dist = Normal(mw_param, sw_param)
    b_dist = Normal(mb_param, sb_param)
    dists = {'linear.weight': w_dist, 'linear.bias': b_dist}
    lifted_module = pyro.random_module("module", regression_model, dists)
    return lifted_module()

Здесь мы определяем вариационное распределение для распределений, которые хотим «обучить». Как видите, мы определяем одинаковые распределения форм для W и b, но делаем их более близкими к реальности (насколько мы можем предположить). В этом примере я сделал их немного более узкими (~ Normal (0, 0,1)).

После обучения модели таким образом:

for j in range(3000):
    epoch_loss = 0.0
    perm = torch.randperm(N)
    # shuffle data
    data = data[perm]
    # get indices of each batch
    all_batches = get_batch_indices(N, 64)
    for ix, batch_start in enumerate(all_batches[:-1]):
        batch_end = all_batches[ix + 1]
        batch_data = data[batch_start: batch_end]
        epoch_loss += svi.step(batch_data)

После подгонки мы хотим взять образец y из модели. Мы делаем это 100 раз и проверяем средние значения и стандартные отклонения для каждого прогноза временного шага (и чем выше будет стандартное отклонение, тем менее уверенными мы можем быть в этом прогнозе).

preds = []
for i in range(100):
    sampled_reg_model = guide(X_test)
    pred = sampled_reg_model(X_test).data.numpy().flatten()
    preds.append(pred)

Как мы помним, с финансовыми прогнозами классические метрики, такие как MSE, MAE или MAPE, могут немного сбивать с толку - относительно небольшая частота ошибок не означает, что ваша модель работает хорошо, всегда важно проверять производительность визуально на данных вне выборки. , и вот что мы делаем:

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

Обычная нейронная сеть

После этой очень простой модели мы хотим попробовать нечто гораздо более интересное, например нейронную сеть. Сначала давайте изучим простой MLP с одним скрытым слоем, содержащим 25 нейронов с линейной активацией:

def get_model(input_size):
    main_input = Input(shape=(input_size, ), name='main_input')
    x = Dense(25, activation='linear')(main_input)
    output = Dense(1, activation = "linear", name = "out")(x)
    final_model = Model(inputs=[main_input], outputs=[output])
    final_model.compile(optimizer='adam',  loss='mse')
    return final_model

И натренируйте его за 100 эпох:

model = get_model(len(X_train[0]))
history = model.fit(X_train, Y_train, 
              epochs = 100, 
              batch_size = 64, 
              verbose=1, 
              validation_data=(X_test, Y_test),
              callbacks=[reduce_lr, checkpointer],
              shuffle=True)

И получите следующие результаты:

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

Байесовская нейронная сеть

Теперь я хочу определить ту же нейронную сеть, которую мы обучили в Keras, но в PyTorch:

class Net(torch.nn.Module):
    def __init__(self, n_feature, n_hidden):
        super(Net, self).__init__()
        self.hidden = torch.nn.Linear(n_feature, n_hidden)   # hidden layer
        self.predict = torch.nn.Linear(n_hidden, 1)   # output layer
def forward(self, x):
        x = self.hidden(x)
        x = self.predict(x)
        return x

По сравнению с байесовской регрессионной моделью у нас теперь есть два набора параметров (от входа к скрытому слою и от скрытого слоя к выходу), поэтому мы немного изменили распределение и априорные значения для нашей модели:

priors = {'hidden.weight': w_prior, 
              'hidden.bias': b_prior,
              'predict.weight': w_prior2,
              'predict.bias': b_prior2}

и гид:

dists = {'hidden.weight': w_dist, 
              'hidden.bias': b_dist,
              'predict.weight': w_dist2,
              'predict.bias': b_dist2}

Не забудьте задать разные имена для всех дистрибутивов в моделях, потому что не должно быть двусмысленности и повторов! Подробности смотрите в исходном коде. Проверим окончательные результаты после подбора модели и выборки:

Выглядит намного лучше, чем любой из предыдущих результатов!

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

for name in pyro.get_param_store().get_all_param_names():
    print name, pyro.param(name).data.numpy()

И вот как я делаю это с моделью Keras:

import tensorflow as tf
sess = tf.Session()
with sess.as_default():
    tf.global_variables_initializer().run()
dense_weights, out_weights = None, None
with sess.as_default():
    for layer in model.layers:
        if len(layer.weights) > 0:
            weights = layer.get_weights()
            if 'dense' in layer.name:
                dense_weights = layer.weights[0].eval()
            if 'out' in layer.name:
                out_weights = layer.weights[0].eval()

Например, для модели Keras веса последнего слоя имеют среднее значение и стандартное отклонение -0,0025901748, 0,30395043, а в модели Pyro они равны 0,0005974418, 0,0005974418. Намного меньше, и это хорошо! Это то, что делают многие регуляризации, такие как L2 или Dropout - доведите наши параметры до нуля, и мы сможем добиться этого с помощью вариационного вывода! Для весов скрытых слоев ситуация еще интереснее. Давайте изобразим некоторые весовые векторы в виде графиков, синий - это веса Кераса, а оранжевый - веса для Pyro:

Что действительно интересно, так это тот факт, что меньше не только среднее значение и стандартное отклонение весов, более того, веса стали разреженными, поэтому в основном мы изучили разреженное представление (своего рода L1) для первого набора весов и некоторую регуляризацию L2 для во-вторых, что потрясающе! Не забудьте запустить код!

Заключение

Мы использовали новый подход к обучению нейронных сетей. Вместо последовательного обновления статических весов мы обновляли распределение весов, и, таким образом, мы могли добиться интересных и многообещающих результатов. Я хочу подчеркнуть, что байесовские методы дали нам возможность регуляризовать нейронные сети без добавления регуляризатора вручную, понять неопределенность моделей и, возможно, использовать меньше данных для лучших результатов. Будьте на связи! :)

P.S.
Следите за мной также в Facebook, чтобы увидеть статьи AI, которые слишком короткие для Medium, Instagram для личных вещей и Linkedin!