Глубокое обучение
Нетти — мой личный предсказатель победителя игры НБА
Использование нейронных сетей для прогнозирования победителя любой игры НБА
Еще в 2018 году у меня была ясная цель: создать модель машинного обучения, способную с выгодой предсказывать победителей игр НБА с целью ее продажи.
Прошло 5 лет с тех пор, как я начал этот проект, и я никогда не рассказывал, как я это сделал. Я намеревался заработать на этом деньги, и, рассказав всем об этом процессе, я подумал, что кто-то украдет их у меня.
Глупый я.
Сегодня я решил рассказать, что такое Netty и как он на самом деле работает.
Короче говоря, Netty — это нейронная сеть, способная предсказать победителя игры НБА с относительно высокой точностью (более 70 %). Если вам интересно, откуда взялось это имя, то краткая форма слова «сеть» — «сеть», и Нетти просто звучала более мило и человечно.
Зачем мне раскрывать такую важную информацию? Во-первых, потому что я больше не хочу его продавать или зарабатывать на этом деньги. Во-вторых, потому что я вижу две выгоды в обмене информацией:
- Как доказательство того, на что я способен. Я потратил много времени на создание этого проекта самостоятельно. Я провел много исследований, но потерпел неудачу и провел много часов, борясь с данными. Это не то же самое, что прочитать пост и получить знания: я действительно вышел и создал все это.
- Чтобы вдохновлять других. С тех пор, как я начал использовать Medium, особенно благодаря таким публикациям, как Towards Data Science[1], я многому научился у других коллег-программистов. Я хотел принести пользу сообществу и начал писать посты, чтобы вдохновлять, учить и помогать всем, кто заинтересован в чтении моих историй.
Сегодняшний ничем не отличается, и я уверен, что это будет один из самых полезных постов, которые я когда-либо публиковал.
Вот содержание, которое я пройду:
- Предыстория и цель.
- Этап исследования.
- Техническая реализация — сбор данных, обработка, EDA, создание моделей…
- Выводы и выводы
Я надеюсь, что эта статья вдохновит людей на создание прогностических моделей, если они в этом заинтересованы, независимо от их опыта или фактического знания предмета.
Предыстория и цель
Как уже говорилось, я хотел построить его, чтобы разбогатеть. В то время мне было всего 19 лет, и, почти не имея опыта работы с искусственным интеллектом, я верил, что это может довольно быстро стать пассивным бизнесом.
Я все еще думаю, что у этого есть потенциал заработать много денег, но теперь моя точка зрения гораздо более реалистична, и я знаю, что потребуется много удачи, маркетинга и времени, чтобы действительно достичь этого, превратив это в бизнес.
В любом случае.
Эта вера дала мне мотивацию, необходимую для того, чтобы стать фанатом искусственного интеллекта, и помогла мне на протяжении +2-летнего пути, который я собирался пройти. Мне нужно было узнать, как работают алгоритмы, мне пришлось освоить очистку и преобразование данных, весь процесс машинного обучения. трубопровод…
Так что в каком-то смысле я был девственником: моим единственным опытом работы с искусственным интеллектом или машинным/глубоким обучением был единственный перцептрон, который я создал самостоятельно, используя чистый код Python (без продвинутых библиотек и фреймворков), который был в состоянии предсказать, будет ли входное число было положительным или отрицательным.
Это далеко не фантастика и довольно глупо, но это помогло мне понять основы работы нейронных сетей на самом базовом уровне.
Этап исследования
Я знал цель, но ничего не знал о различных типах алгоритмов, которые мог бы использовать. Однако я знал, что хочу создать нейронную сеть. Типичные алгоритмы машинного обучения были хороши, но мне хотелось использовать что-то более продвинутое, и нейронные сети выглядели убедительно.
Но даже в этом относительно небольшом диапазоне возможностей варианты были безграничны. Существует так много типов слоев, сетей и гиперпараметров.
Я бы попробовал базовую глубокую нейронную сеть, в которой у нас есть разные слои, и нейроны каждого слоя связаны со всеми нейронами следующего слоя, в котором информация всегда распространяется вперед от входа к выходу. Но это все еще казалось слишком простым, и мне нужно было что-то более уникальное, чтобы иметь возможность зарабатывать деньги.
Именно тогда я понял, что должны быть цифровые доказательства того, что другие люди пытаются достичь тех же результатов, способом, похожим или связанным с тем, чего я хотел.
Поэтому я начал поиск в Интернете и нашел несколько статей и статей. Назвать несколько:
- Использование рынка ставок на спорт с помощью машинного обучения[2]
- Оптимальные стратегии ставок на спорт на практике: экспериментальный обзор[3]
- Учимся прогнозировать результаты футбола на основе реляционных данных с помощью деревьев с градиентным усилением [4]
Если вы просмотрите их, вы увидите, что я не просто искал статьи по прогнозам НБА: я читал статьи, применимые и к другим видам спорта, а некоторые даже относились к области трейдинга и попыток обыграть рынок. Целью было понять, что именно пытались сделать эксперты, и оценить, какие из этих техник я мог бы использовать и комбинировать, чтобы создать свою собственную.
Я потратил бесчисленные часы в поисках ценной информации, которая могла бы помочь мне в разработке Netty. И это был постоянный и повторяющийся процесс. Затем я пробовал то, что мне нравилось больше всего, и возвращался к прочтению других статей, чтобы опробовать другие подходы.
Но чтение не принесло мне успеха. Итак, я начал кодировать.
Техническая реализация
Я мог бы часами рассказывать о том, что я пробовал и что не сработало, но я хочу, чтобы этот пост был привлекательным и интересным, поэтому я перейду сразу к тому, что сработало, а что использовалось в финальной версии Netty.
Сбор данных
Я использовал свои навыки парсинга, чтобы создать паука с помощью scrapy[5], который смог собрать все необходимые мне данные.
ВНИМАНИЕ: Всегда проверяйте, что вы сканируете веб-сайты с соответствующей лицензией или убедитесь, что у вас есть на это право.
Я сосредоточился на последних 12 сезонах: начиная с 2006 года. И данные, которые я получил, были:
- Данные игрока: в основном статистика игрока на уровне игры (PTS, AST, REB…). Не только традиционная статистика, но и продвинутая, четыре фактора, разное…
- Данные о команде: практически то же самое, что и данные об игроках, но агрегированы на уровне команды. Однако здесь я сосредоточился только на традиционной статистике.
- Данные игры. Для каждой игры мне нужно было определить победителя игры, кто был местным жителем, а кто посетителем…
Это была «легкая» часть — простая, то есть не требующая особых размышлений, мне просто нужно было построить скребок и позволить ему делать свою работу.
Я сохранил все данные в базе данных DuckDB[6]. Не стесняйтесь проверить пост, который я написал об этой удивительной DMBS:
Когда все данные были сохранены, пришло время их использовать.
Предварительная обработка данных
Данные, которые я скачал, были совершенно сырыми: я просто скачал табличные данные в том виде, в котором они появились на сайте.
Чтобы использовать эти данные, мне нужно было сначала их очистить. К счастью, этот процесс был простым — полученные данные были довольно качественными. Я просто удалил некоторые столбцы со слишком большим количеством пропущенных значений, вменил некоторые другие… Я просто сделал то, что имело больше смысла.
После очистки наступил этап разработки функций. Я хотел создать новые функции, которые могли бы помочь модели принимать более обоснованные решения. Например:
- % побед команд.
- Победная или проигрышная серия команд.
- Различная домашняя статистика для команды, играющей дома.
- Различная выездная статистика по командам, играющим на выезде.
- Была ли игра B2B-игрой для команд или нет.
# Some examples df['h_Win%'] = df.groupby('team').shift(1).groupby('team')['h_Win'].rolling(window=82, min_periods=1).mean() df['a_Win%'] = df.groupby('team').shift(1).groupby('team')['a_Win'].rolling(window=82, min_periods=1).mean()
На этом этапе я просто создавал переменные. Я собирался отфильтровать некоторые из них на будущих этапах, но сейчас было просто время творить.
После создания множества новых столбцов, которые, как я думал, могут оказаться полезными, пришло время масштабировать данные, и я выполнил простую стандартизацию, удалив среднее значение признака для каждого значения и затем разделив его на стандартное отклонение признака.
# Feature standardization df['feature'] = (df['feature'] - df['feature'].mean()) / df['feature'].std()
Потом у нас возникла проблема: объединив статистику игрока и команды, я получил более 600 функций. Мне нужно было выполнить своего рода уменьшение размерности, чтобы сохранить только те, которые были наиболее важными и не избыточными.
В то время я мало что знал, поэтому решил использовать Важность функции (вероятно, я воспользуюсь им снова). Если вы не знали, «Важность функции относится к методам, которые присваивают оценку входным функциям в зависимости от того, насколько они полезны для прогнозирования целевой переменной» [7].
Существует множество алгоритмов, с помощью которых мы можем попытаться получить эти наиболее важные функции, и я решил использовать случайные леса и scikit-learn:
from sklearn.ensemble import RandomForestRegressor X = #... matrix containing all features and games y = #... vector with final outcome of games features = #... List of feature names, same order as X rf = RandomForestRegressor() rf.fit(X, y) importances = rf.feature_importances_ for i, importance in enumerate(importances): print(f'Feature: {features[i]}, has an importance of {importance}')
Это показало важность каждой функции; Мне нужно было только отсортировать их и сохранить самые важные.
Моделирование
Настало время построить модель. Но некоторые решения пришлось принять заранее:
- Сколько или какие функции я собирался использовать на уровне игрока?
- Сколько или какие функции я собирался использовать на уровне команды?
- Поможет ли использование букмекерских коэффициентов как обратной вероятности победы команды?
Возникло несколько вопросов, и был только один способ ответить на них: протестировать их. В итоге входами сети оказались:
- Около 30 статистических данных на уровне игроков (наиболее важных) из 8 игроков, у которых было наибольшее среднее игровое время с начала сезона.
- На уровне команды мы использовали функции перед игрой (те, которые мы разработали, такие как Win%, полоса…).
Коэффициенты были отброшены, поскольку они оказались довольно точными (очевидно, что у букмекерских контор есть надежные алгоритмы, и их точность трудно превзойти), и в итоге модель полагалась почти исключительно на коэффициенты.
Когда дело доходит до модели, одним из возможных решений может быть простое сглаживание матрицы игроков и добавление статистики на уровне команды. Однако это привело бы к входному слою размером примерно 30x8x2 + 15x2 = 510 входных объектов.
Выполнимо, но все исследования, которые я прочитал, научили меня лучшему подходу: использованию сверточной сети (ну, скорее, сверточного слоя). Несмотря на то, что мы здесь не играем с изображениями, данные на уровне игрока представляли собой трехмерную матрицу, которая легко вписывалась в сверточную сеть:
У нас есть более 30 функций в одном измерении, 8 наиболее часто используемых игроков во втором измерении, а поскольку у нас играют две команды, это создает третье измерение.
Затем эти входные данные обрабатываются сверточным слоем, и результат выравнивается.
Это решило вопрос с вводом данных игроком, но как насчет предигровой статистики команд? На этот раз я упростил его и сохранил линейным: сначала данные хозяев поля, а затем те же характеристики для команды гостей.
Эти данные были добавлены в качестве входных данных к сглаженному сверточному выводу:
Теперь все последующие слои являются простыми плотными слоями. Конкретно модель имела еще два скрытых слоя (32 и 8 нейронов соответственно) и один выходной слой с одним единственным нейроном: вероятность победы хозяев поля.
И это был конечный результат с точки зрения архитектуры. Когда дело доходит до гиперпараметров, отличных от количества нейронов или слоев, не стесняйтесь проверить их в приведенном ниже коде.
Опять же, в то время я был новичком, и некоторые решения могли быть неоптимальными, но в то время они работали.
from tensorflow.keras.models import Model, Sequential from tensorflow.keras.layers import Input, Dense, Dropout, Conv2D, Flatten, concatenate from tensorflow.keras.regularizers import l2 from tensorflow.keras.optimizers import Adam def get_model(player_shape, team_shape): # Create Convolutional Network conv_model = Sequential(name="Conv") conv_model.add(Conv2D(filters=32, kernel_size=(1, 8), input_shape=player_shape, data_format="channels_last", activation="tanh", name="Convolutional")) # Flatten the results conv_model.add(Flatten(name="Flatten")) # Wrap this first part of the network X = Input(shape=player_shape, name="PlayerInput") conv_encoded = conv_model(X) # Create dense network with conv results and team data as inputs dense_input = Input(shape=team_shape) x = concatenate([conv_encoded, dense_input]) x = Dense(32, activation="tanh", kernel_regularizer = l2(0.02876), name="Dense64")(x) x = Dropout(0.2)(x) x = Dense(8, activation="tanh", kernel_regularizer = l2(0.02875), name="Dense16")(x) x = Dropout(0.08)(x) # Output neuron Y = Dense(1, activation='sigmoid', name="Output")(x) opt = Adam(lr=0.001) nba_model = Model(inputs=[X, dense_input], outputs=Y) nba_model.compile( optimizer=opt, loss='binary_crossentropy', metrics = ['accuracy'] ) print(nba_model.summary()) return nba_model model = get_model( playerMatrices['2021-22']['x'][0].shape, teamMatrices['2021-22']['x'][0].shape )
На этом первая версия Нетти была готова. Мы получаем много входных данных, а результат прост: вероятность победы хозяев поля.
Ниже вы можете увидеть точность и потерю модели с течением времени, пока я обучал ее более 1500 эпох:
Сезоны с 2008–09 по 2016–17 годы я использовал для тренировок, а последующие до 2020–21 – для тестов. Окончательная точность для всех сезонов составила 72,19%, а для невидимых сезонов – 71,76%.
Это действительно хорошие цифры, особенно после прочтения всех исследований по этой теме.
Однако было ли этого достаточно, чтобы заработать мне деньги?
Результаты и заключение
Прежде всего: я не рекламирую ставки на спорт. Я считаю, что это опасно и для большинства вообще не выгодно.
Зарабатывать деньги с помощью спортивных прогнозов непросто. Netty удалось заработать — так что это было прибыльно — но сумма денег, которую она смогла заработать, не стоила предполагаемого риска и волатильности.
Тем не менее, я все еще думаю, что мне удалось сохранить его на относительно низком уровне. Существует множество потенциальных улучшений, некоторые из которых я уже опробовал в более продвинутой версии, например:
- Использование пользовательской функции потерь для корреляции наших результатов с коэффициентами букмекерской конторы. Логика этой идеи заключается в том, что если прогнозы нашей модели аналогичны прогнозам букмекерских контор, мы не заработаем никаких денег (из-за их маржи).
- Использование пользовательской функции потерь, максимизирующей прибыль. На самом деле нас не волнует точность — наша цель — увеличить прибыль. Создав правильную функцию потерь, мы могли бы сосредоточиться на этом.
- Использование более сложной архитектуры. Я использовал сверточный слой, но все остальное было просто глубоким прямым распространением. Ничего особенного.
- А почему бы нам не попробовать стратегию DeepQL? Это всего лишь идея, которая, возможно, даже нереализуема, но, на мой взгляд, имеет смысл хотя бы попробовать ее. В конце концов, Нетти похожа на агента, пытающегося решить, делать ставку или нет и какую сумму, учитывая обстоятельства того конкретного момента.
Идеи и предложения могут быть безграничны. Слишком много подходов, чтобы попробовать…
Так что вперед и попробуйте сами!
Я не делюсь всем кодом, потому что он очень, очень длинный. Вместо этого я поместил несколько фрагментов наиболее важных частей, чтобы дать представление о том, что я делаю.
Тем не менее, не стесняйтесь обращаться, если у вас есть какие-либо вопросы или предложения.
Thanks for reading the post! I really hope you enjoyed it and found it insightful. Follow me and subscribe to my mail list for more content like this one, it helps a lot! @polmarin
Ресурсы
[1] На пути к науке о данных — средний
[2] Губачек, Ондржей и Шир, Густав и Железный, Филип. (2019). Использование рынка ставок на спорт с использованием машинного обучения. Международный журнал прогнозирования. 35. 10.1016/j.ijforecast.2019.01.001.
[3] Ухрин, Матей и Шир, Густав и Губачек, Ондржей и Железный, Филип. (2021). Оптимальные стратегии ставок на спорт на практике: экспериментальный обзор.
[4] Губачек О., Шурек Г. и Железный Ф. Обучение прогнозированию результатов футбола на основе реляционных данных с деревьями с градиентным усилением. Mach Learn 108, 29–47 (2019). https://doi.org/10.1007/s10994-018-5704-6
[5] Скрапи | Быстрая и мощная платформа для парсинга и веб-сканирования
[6] Внутрипроцессная система управления базами данных SQL OLAP — DuckDB
[7] Как рассчитать важность функции с помощью Python — MachineLearningMastery