Часть 3 из 3

Привет! Это третья часть серии, в которой я создаю комплексное приложение, которое в первую очередь предназначено для прогнозирования исхода игры между двумя выбранными командами в пуле из 30 профессиональных команд НБА. Я делаю эту серию в основном для того, чтобы показать ключевые аспекты того, что помогло мне создать этот проект, так как я многому научился в процессе, и я считаю, что знаниями всегда нужно делиться.

Цель

Основная цель этой статьи — рассказать об основных концепциях, которые я использовал в процессе получения рабочей модели, и о том, как вы можете применить их в своих собственных проектах. Здесь мы попытаемся обучить модель машинного обучения, которая может превзойти производительность модели прогнозирования, NBA Oracle [1]. Этот проект работает с невидимыми данными, и я черпал из него вдохновение. Автор говорит, что их проект даже превзошел мнения экспертов, и результаты представлены в таблице ниже:

Настройка нашей эталонной метрики

Первоначально мы хотим увидеть, как базовая модель классификации работает с нашими готовыми к обучению данными, и мы собираемся назвать ее базовой моделью. Это будет наша первая ссылка, и мы постараемся улучшить ее. Наши данные r за сезоны 2021 и 2022 годов содержат 1200 образцов игр на момент написания. Итак, сначала мы загрузим наши данные с помощью панд и удалим существующие столбцы, чтобы упорядочить их. Например, дата игры и сезон. Мы также выделяем столбец с результатом игры в целевой вектор и определяем его как y. Столбец features, связанный с матрицей, будет определен как x.

С этого момента мы будем активно использовать библиотеку scikit-learn, которая является самой популярной библиотекой для машинного обучения в Python. Имея в руках этот инструмент, мы теперь можем обучать данные из тестовых данных отдельно и обучать нашу базовую модель, логистическую регрессию. Нам нужно только настроить параметр max_iter на большое число, чтобы он мог сойтись на наших данных. Мы можем визуализировать наши результаты с помощью хорошего метода ConfusionMatrixDisplay от sklearn; это покажет нашу матрицу путаницы, как показано ниже. Вы должны подключить свою модель, тестовые функции и тестовую цель.

С методом разделения обучения и тестирования от scikit-learn мы получаем хорошую точность 72,6% на тестовом наборе! Перекрестная проверка — это хороший способ проверить предсказания моделей, поскольку он разделяет данные на пакеты и несколько раз обучает тесты, где каждая часть данных используется в качестве обучения, а также в конечном итоге в качестве теста. Вы можете получить больше информации на странице sklearn, если хотите узнать подробности.

Используя оценки перекрестной проверки с пятью партиями, мы получаем превосходную точность 74,3%! Для меня это урок того, почему качественные данные имеют значение и почему стоит потратить время на то, чтобы добраться до них, поскольку мы уже превзошли эталон, который должны были превзойти, и теперь можно идти домой — шучу. Поскольку теперь у нас есть новый референс, давайте посмотрим, что мы можем сделать, чтобы улучшить его!

Разработка новых моделей

Мы собираемся попробовать четыре модели через Scikit-learn. Первым будет логистическая регрессия; это хорошо для анализа в первую очередь из-за его простоты и эффективности. Затем мы используем машины опорных векторов (SVM), которые хороши для данных большого размера из-за их ядра и сотен функций нашего набора данных.

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

Изначально данные для обучения моделей будут такими же, как мы использовали для базовой модели. Чтобы поддерживать чистоту кода и возможность повторного использования, мы создаем класс Python, ModelParams, для хранения диапазона параметров для каждой модели. Например, мы настроили его так, чтобы он имел набор штрафных параметров для логистической регрессии, а именно l1, l2 и elasticnet (сочетание l1 и l2).

Этот класс также будет иметь атрибуты масштабатора данных, масштабатор Standard для линейных моделей и масштабировщик MinMax для ансамблей. Ансамблевые модели работают без необходимости масштабирования данных благодаря тому, как деревья решений выбирают функции, оказывающие наибольшее влияние на целевую переменную. Итак, при вызове этого класса мы создаем экземпляр объекта конвейера с нужной нам моделью — с масштабатором или без него — и со всем набором параметров, связанных с этой моделью. У него также будет метод, который позволит нам визуализировать модель, масштабатор и диапазоны параметров после создания экземпляра.

Мы создаем второй класс, ModelDevelopment, который может использовать конвейер, который мы создали ранее, а также функции X и целевые данные y, которые должны быть созданы первыми. Два метода обеспечивают обучающую функциональность класса: один используется для обучения модели всем наборам параметров настройки с именем grid_search, а другой используется для обучения случайным параметрам в переданной группе. Мы назвали это random_search. В нем также есть методы, которые мы можем использовать для анализа метрик и кривой AUC ROC метода после обучения.

Метод grid_search использует GridSearchCV из scikit-learn. Он отвечает за обучение нашей модели n*folders раз (грубая сила). n — это объединенный результат при использовании всех функций пройденного нами набора, а папки — это разделение перекрестной проверки. У него также есть возможность выбрать метрику подсчета очков, которая входит в поезд. Метод возвращает лучшую модель, лучшие параметры и лучший результат для этой лучшей модели. Он также имеет атрибуты всех партитур, если мы хотим получить к нему доступ. Мы будем использовать этот метод, чтобы найти оптимальные параметры линейных моделей, поскольку они имеют меньше параметров для объединения и быстрее обучаются по сравнению с более сложными моделями.

Метод random_search использует RandomizedSearchCV из scikit-learn. Он делает все, что делает grid_search, за исключением того, что он будет обучать нашу модель z*folders раз. z является подмножеством случайных параметров настройки набора n. Мы будем обучать наши модели ансамбля, так как мы можем установить количество шагов обучения. Недостатком многих параметров является то, что их приходится настраивать, а сложность модели увеличивает время обучения по сравнению с линейными моделями.

Что касается проверочного анализа, в классе есть метод model_metric для просмотра оценок, связанных с accuracy, precision, recall, f1 и auc. У него также есть метод, который строит кривую auc roc, и он называется roc_curve. Третий метод используется для построения матрицы путаницы модели и называется plot_confusion_matrix. После использования обучающих функций эти методы будут принимать оптимальные параметры как атрибуты класса выбранной модели, поэтому им не нужны передаваемые аргументы.

Теперь все подключаем и смотрим какие параметры получаем для каждой модели. При использовании обучения и теста x и y результаты составляют около 72% — 76% для модели классификатора с повышением градиента.

Как видим, они близки к базовой модели, которая составила 72,6%. Это означает, что мы сделали все возможное, чтобы настроить модели, но это только немного повышает нашу точность. Что мы можем сделать, чтобы попытаться улучшить это? Ответ классический: давайте поиграем с нашими данными!

Улучшение качества данных с помощью выбора признаков

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

Мы выполним выбор функций тремя способами: во-первых, путем исчерпывающего выбора функций с использованием файла SequentialFeatureSelector из sklearn. Затем с помощью аналитического выбора с использованием SHAP, библиотеки Python, которая очень популярна для визуализации важности функций и их влияния на модель. Наконец, мы будем использовать метод анализа основных компонентов (PCA), чтобы выбрать лучшие функции для модели.

Первый метод обучает модель с одним подмножеством функций за раз. Он проходит через каждую функцию, и мы можем выбрать, будет ли она двигаться от одной функции ко всем функциям (направление = вперед)… или будет ли она начинаться со всех функций и заканчиваться на одной функции (направление = назад). Отсюда исчерпывающая концепция. Мы должны передать модель, направление, количество функций и показатель метрики в качестве аргументов методу SequentialFeatureSelector. Ниже мы видим фрагмент кода, адаптированный для этой задачи:

from sklearn.feature_selection import SequentialFeatureSelector
from time import time
import os

def select_nBest_features(model, n_features):
    """ Info:
     Choose a model and a number of features to select the best features among this number, saving the results in a csv file,
     plots the top n_best_features and returns a dataframe with the feature importance.
     As the method dont return the importances, just the most important features, we need to get to that after the selection. 
      -----------------------------------------------------------------------------------------------------------------
       Input:
        model: The model to be used.
        n_features: The number of best features to select
        -----------------------------------------------------------------------------------------------------------------
            Output:
                Best features for the chosen model.
                
            """
    #verifies if the model is saved already to proceed with the selection
    if not os.path.isfile(f'best_features/{model[-1].__class__.__name__}_{n_features}.csv'): # model[-1] is the model itself in the pipeline
        start = time()
        sfs = SequentialFeatureSelector(model[-1], n_features_to_select=n_features, direction='backward', scoring='accuracy', cv=5, n_jobs=-1)
        sfs.fit(X, y)
        end = time()
        print(f"Features selected by forward sequential selection: {X.columns[sfs.get_support()]}") #feature names for the best features
        print(f'Minutes elapsed: {round((end - start)/60)}') #time in minutes
        #saving the best features to csv
        pd.Series(X.columns[sfs.get_support()]).to_csv(f'best_features/{model[-1]}_{n_features}.csv', index=False)
    else:
        print(f'Best {n_features} features for {model[-1].__class__.__name__} already saved.')

    #loading the best features for plotting
    return pd.read_csv(f'best_features/{model[-1].__class__.__name__}_{n_features}.csv', header=None) 

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

Примечание: нам нужно передать масштабированные данные для SHAP, потому что мы не можем передать конвейерную модель. Мы можем использовать только модель, содержащуюся в последнем шаге предыдущего пайпа (скейлер + модель). Например, если мы хотим увидеть 10 наиболее важных функций модели случайного леса, мы можем сделать что-то вроде этого:

# Create SHAP explainer
explainer = shap.TreeExplainer(RF[-1], X_scaled, seed=42)
shap_values = explainer.shap_values(X_scaled)

plt.figure(figsize=(10, 10))
shap.summary_plot(shap_values, X_scaled, max_display=10, plot_type='bar')
plt.tight_layout()
plt.show()

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

Последний метод — использование PCA для уменьшения размерности. Таким образом, мы хотим, чтобы PCA преобразовывал n объектов данных в m основных компонентов, где m<n. Компоненты представляют собой ортогональные векторы, которые аппроксимируют данные методом наименьших квадратов, выделяя наиболее важные аспекты и подчеркивая их. Для этого мы должны сначала передать наши данные, масштабированные, вычитая среднее значение и получая точки данных вокруг единичной дисперсии. Чтобы понять интуицию PCA, я рекомендую эту ссылку.

Мы получим представление о том, какая часть наших 269 функций объясняется основными компонентами, изучив график ниже:

Хороший. Мы видим, что первые 100 основных компонентов могут объяснить около 95% вариаций наших данных. Что мы можем сделать сейчас, так это преобразовать наши функции данных в 100 основных компонентов и посмотреть, сможем ли мы повысить точность и другие показатели, предоставляемые методом sklearn, используя classification_report.

from sklearn.decomposition import PCA

def pca_metrics(model, X_train=X_train_scaled, X_test=X_test_scaled, y_train=y_train, y_test=y_test, n_components=0.95):
    """ Info:
     This function takes a model and returns the metrics reports of the model with the PCA.
      -----------------------------------------------------------------------------------------------------------------
       Input:
        model: The model to be used.
        X_train: The training set.
        X_test: The testing set.
        y_train: The training target.
        y_test: The testing target.
        n_components: The number of components to be used in the PCA.
        -----------------------------------------------------------------------------------------------------------------
        Output:
         classification_report: The classification report of the model with the PCA.
        """
    pca = PCA(n_components=n_components)
    X_train_pca = pca.fit_transform(X_train)
    X_test_pca = pca.transform(X_test)
    model.fit(X_train_pca, y_train)
    y_pred = model.predict(X_test_pca)
    #get the number of components
    n_components = pca.n_components_

    return classification_report(y_test, y_pred), n_components
# using 100 principal components to train and test our models
models = [lr, svm, rf, gbc]

for model in models:
    print('{}\n'.format(model[1]), pca_metrics(model, n_components=100)[0])
    print('Principal components used: {}\n'.format(pca_metrics(model, n_components=100)[1]))

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

plt.figure(figsize=(10, 20))

loadings = pd.DataFrame(
    data=pca.components_.T * np.sqrt(pca.explained_variance_), #type: ignore
    columns=[f'PC{i}' for i in range(1, len(X_train.columns) + 1)], #type: ignore
    index=X_train.columns #type: ignore
)
loadings['PC1'] = abs(loadings['PC1'])
#picking the 100 best featues accordnly to PCA 1
pca_best_features = loadings['PC1'].sort_values(ascending=False).head(100)

pca_best_features[::-1].plot(kind='barh', color='#087E8B')
plt.title('PC1 loadings', size=20)
plt.ylabel('Features', size=15)
plt.xlabel('Absolute Correlation', size=15)

#drawing a line at 0.5
plt.axvline(x=0.5, color='red', linestyle='--')

pca_best_features.head(10)

Итак, давайте подытожим, что мы сделали:

  • Обучение и тестирование моделей на данных с определенным набором функций
  • Выбраны лучшие гиперпараметры
  • Выбраны лучшие функции данных с моделями с их гиперпараметрами из предыдущих

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

Полученные результаты

Выполняя этот процесс, мы обнаруживаем, что лучшей моделью был случайный лес с точностью около 82,5% и AUC 81,8%. Он был обучен на данных с 50 лучшими функциями, выбранными с помощью метода выбора выхлопных функций, поэтому мы собираемся использовать его. Вот некоторые характеристики модели:

{'randomforestclassifier__n_estimators': 1000,
 'randomforestclassifier__min_weight_fraction_leaf': 0.0,
 'randomforestclassifier__min_samples_split': 10,
 'randomforestclassifier__min_samples_leaf': 2,
 'randomforestclassifier__min_impurity_decrease': 0.0,
 'randomforestclassifier__max_leaf_nodes': 31,
 'randomforestclassifier__max_features': 'sqrt',
 'randomforestclassifier__max_depth': 7,
 'randomforestclassifier__criterion': 'entropy'}

Заключительные мысли

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

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

Want to Connect?

If you want, please contact me on my LinkedIn for any feedback or questions.
I will be happy to answer.

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

[1] М. Беклер, Х. Ван, М. Папамайкл. «Оракул НБА», Питтсбург, 2013 г.