Всем снова привет! В прошлом году я опубликовал несколько руководств по финансовому прогнозированию с использованием нейронных сетей, и я думаю, что некоторые из результатов были по крайней мере интересными и достойными применения в реальных торговых приложениях. Если вы их читали, то, должно быть, заметили, что когда вы пытаетесь подогнать какую-то модель машинного обучения к «случайным» данным в надежде найти скрытые закономерности, вы, как правило, сильно перестраиваетесь под набор поездов. Мы использовали различные методы регуляризации и дополнительные данные для решения этой проблемы, но это требует очень много времени и немного напоминает слепой поиск.
Сегодня я хочу представить немного другой подход к настройке тех же алгоритмов. Рассмотрение их с вероятностной точки зрения позволяет нам изучить регуляризацию на основе данных как таковых, оценить достоверность наших прогнозов, использовать гораздо меньше данных для обучения и ввести дополнительные вероятностные зависимости в наши модели. Я не буду так сильно углубляться в технические или математические детали байесовских моделей или вариационного вывода, я дам некоторый обзор, но также сконцентрируюсь больше на применении. Как всегда, вы можете проверить код здесь.
Я также рекомендую проверить свои предыдущие уроки по финансовому прогнозированию с помощью нейронных сетей:
- Простое прогнозирование временных рядов (и сделанные ошибки)
- Корректное прогнозирование одномерных временных рядов + бэктестинг
- Многомерное прогнозирование временных рядов
- Прогнозирование волатильности и нестандартные убытки
- Многозадачное и мультимодальное обучение
- Оптимизация гиперпараметров
- Улучшение классических стратегий с помощью нейронных сетей
- Вероятностное программирование и пиропрогнозы
- Бэктестинг в пандах
Для более глубокого понимания вероятностного программирования, байесовского моделирования и их приложений я рекомендую вам проверить следующие ресурсы:
- Распознавание образов и машинное обучение
- Байесовские методы для хакеров
- Документация библиотек ниже
и проверить следующие библиотеки 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!