Глубокое обучение

Нетти — мой личный предсказатель победителя игры НБА

Использование нейронных сетей для прогнозирования победителя любой игры НБА

Еще в 2018 году у меня была ясная цель: создать модель машинного обучения, способную с выгодой предсказывать победителей игр НБА с целью ее продажи.

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

Глупый я.

Сегодня я решил рассказать, что такое Netty и как он на самом деле работает.

Короче говоря, Netty — это нейронная сеть, способная предсказать победителя игры НБА с относительно высокой точностью (более 70 %). Если вам интересно, откуда взялось это имя, то краткая форма слова «сеть» — «сеть», и Нетти просто звучала более мило и человечно.

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

  1. Как доказательство того, на что я способен. Я потратил много времени на создание этого проекта самостоятельно. Я провел много исследований, но потерпел неудачу и провел много часов, борясь с данными. Это не то же самое, что прочитать пост и получить знания: я действительно вышел и создал все это.
  2. Чтобы вдохновлять других. С тех пор, как я начал использовать Medium, особенно благодаря таким публикациям, как Towards Data Science[1], я многому научился у других коллег-программистов. Я хотел принести пользу сообществу и начал писать посты, чтобы вдохновлять, учить и помогать всем, кто заинтересован в чтении моих историй.
    Сегодняшний ничем не отличается, и я уверен, что это будет один из самых полезных постов, которые я когда-либо публиковал.

Вот содержание, которое я пройду:

  • Предыстория и цель.
  • Этап исследования.
  • Техническая реализация — сбор данных, обработка, EDA, создание моделей…
  • Выводы и выводы

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

Предыстория и цель

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

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

В любом случае.

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

Так что в каком-то смысле я был девственником: моим единственным опытом работы с искусственным интеллектом или машинным/глубоким обучением был единственный перцептрон, который я создал самостоятельно, используя чистый код Python (без продвинутых библиотек и фреймворков), который был в состоянии предсказать, будет ли входное число было положительным или отрицательным.

Это далеко не фантастика и довольно глупо, но это помогло мне понять основы работы нейронных сетей на самом базовом уровне.

Этап исследования

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

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

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

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

Поэтому я начал поиск в Интернете и нашел несколько статей и статей. Назвать несколько:

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

Я потратил бесчисленные часы в поисках ценной информации, которая могла бы помочь мне в разработке 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