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

Хотя большинство моделей машинного обучения хорошо задокументированы, выбор гиперпараметров все еще может быть весьма запутанным. Выбор оптимальных настроек обычно требует перебора различных параметров, чтобы увидеть, как они влияют на точность модели, вызывая процесс Поиск в сетке. К счастью, Scikit-learn разработала GridSearchCV инструмент для автоматизации этого процесса. GridSearchCV также сохраняет оценки точности комбинаций гиперпараметров по мере перебора параметров, что позволяет удобно исследовать влияние этих параметров на модель. Давайте посмотрим, как это работает.

Этот рабочий процесс был создан в Python 3.10, и соответствующий блокнот Jupyter можно найти здесь.

1. Импорт и обработка данных

В этом проекте используется общедоступный набор данных Уровень адаптации учащихся в онлайн-образовании на kaggle, опубликованный как Прогнозирование уровня адаптации учащихся в онлайн-образовании с использованием подходов машинного обучения на 12-й Международной конференции 2021 по компьютерным коммуникациям и сетям. Технологии (ICCCNT) материалы конференции (DOI: 10.1109/ICCCNT51525.2021.9579741). Здесь авторы собрали массив демографических, экономических и других исходных данных для студентов, обучающихся онлайн, а также индивидуальные оценки их адаптации к онлайн-обучению. Затем авторы использовали эти данные для прогнозирования уровня адаптивности новых студентов, создав модель классификатора машинного обучения. Они сравнили точность ряда моделей, включая K-Nearest Neighbors, Decision Tree, Random Forest, Support Vector Machine, Artificial Neural Network и Naive Bayes, что дало нам хорошие ориентиры при разработке и сравнении нашего собственного диапазона моделей классификаторов.

Начнем с загрузки и импорта данных в виде кадра данных pandas.

#download and import data
import wget
import os.path
import pandas as pd
file_path = 'data/students_adaptability_level_online_education.csv'
if not os.path.isfile(file_path):
    url = 'https://www.kaggle.com/datasets/mdmahmudulhasansuzan/students-adaptability-level-in-online-education?select=students_adaptability_level_online_education.csv'
    wget.download(url, out = file_path)
    
df = pd.read_csv(file_path)
df.head()

Похоже, что все наши переменные являются объектами. Давайте подтвердим перед обработкой данных.

#check data structure
df.dtypes

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

#check distribution of all data values and list values in dict
values_dict = {}
for column in df:
    values_dict[column] = df[column].value_counts().sort_index().index.to_list()
#inspect values
values_dict

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

#reorder lists
order = [0,5,1,2,3,4]
values_dict[‘Age’] = [values_dict[‘Age’][i] for i in order]
order = [1,0,2]
values_dict[‘Education Level’] = [values_dict[‘Education Level’][i] for i in order]
values_dict[‘Financial Condition’] = [values_dict[‘Financial Condition’][i] for i in order]
order = [1,0]
values_dict[‘Institution Type’] = [values_dict[‘Institution Type’][i] for i in order]
values_dict[‘Load-shedding’] = [values_dict[‘Load-shedding’][i] for i in order]
order = [1,2,0]
values_dict[‘Adaptivity Level’] = [values_dict[‘Adaptivity Level’][i] for i in order]
#build integer-coded dataframe
coded_dict = {}
for column in df:
    coded_dict[column] = []
    for i in range(0,df.shape[0]):
        coded_dict[column].append(values_dict[column].index(df[column][i]))
coded_df = pd.DataFrame(coded_dict)
coded_df.head()

2. Исследование данных

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

#plot correlation matrix
import seaborn as sns
import matplotlib.pyplot as plt
cmap = sns.color_palette(“vlag”, as_cmap=True).reversed()
sns.heatmap(coded_df.corr(method=’spearman’), cmap=cmap, vmin=-1, vmax=1)
plt.show()

Учитывая, что для получения образования требуется время (а большинство из нас прогрессирует с одинаковой скоростью), неудивительно, что возраст и уровень образования довольно сильно коррелируют друг с другом. Однако нет сильной индивидуальной корреляции с Уровнем адаптивности, что указывает на необходимость многомерного исследования. Примечательно, что существует слабая положительная корреляция между переменной ответа и длительностью занятий, финансовым состоянием и местоположением, что, по-видимому, указывает на то, что хорошо обеспеченные ресурсами учащиеся, живущие в городе и получающие больше общения с учителями, адаптируются лучше всего. Существует также слабая отрицательная корреляция с типом учебного заведения, что позволяет предположить, что негосударственные школы обеспечивают более высокую адаптивность, чем государственные школы.

Поскольку ни одна из переменных сильно не коррелирует с Уровнем адаптивности, давайте упорядочим (уменьшим размерность) данные и посмотрим, есть ли какие-либо кластеры в общей структуре данных. Во-первых, мы стандартизируем данные. Затем будем ординировать с Principal Component Analysis, который проецирует полные данные по измерениям наибольшей изменчивости (подробнее о ординации здесь).

#split X and Y, and standardize X
import numpy as np
import sklearn
from sklearn import preprocessing
Y = coded_df[‘Adaptivity Level’].to_numpy()
X = coded_df.loc[:, coded_df.columns != ‘Adaptivity Level’].to_numpy()
xform = preprocessing.StandardScaler()
X_z = xform.fit(X).transform(X)
#define pca plot fuction
def plot_pca(ordi,lab,y):
    '''
    Generates biplot of 1st and 2nd axes from an ordination model
    '''
    plt.figure()
    plt.scatter(ordi[y==0, 0], ordi[y==0, 1], color='red', label='Low')
    plt.scatter(ordi[y==1, 0], ordi[y==1, 1], color='blue', label='Moderate')
    plt.scatter(ordi[y==2, 0], ordi[y==2, 1], color='green', label='High')
    plt.xlabel(lab[0])
    plt.ylabel(lab[1])
    plt.legend()
    plt.show()
#train and plot PCA
from sklearn.decomposition import PCA
pca = PCA()
X_pca = pca.fit_transform(X_z)
labels = [f"PCA1 ({pca.explained_variance_ratio_[0]*100:.1f}%)",
          f"PCA2 ({pca.explained_variance_ratio_[1]*100:.1f}%)"]
plot_pca(X_pca,labels,Y)

Несмотря на то, что наблюдается небольшая видимая тенденция Низкая адаптивность к верхнему левому углу и Высокая адаптивность к нижнему правому краю, эта тенденция слабая и сильно смешанная. На первый и второй основные компоненты приходится только 39,0% общей изменчивости данных, поэтому мы не можем добиться значимого разделения данных только по двум измерениям простой ординации. Существуют дополнительные методы ординации, которые применяют более структурированную проекцию данных в попытке максимизировать внутренние структуры данных. t-Distributed Stochastic Neighbor Embedding — один из таких методов, который пытается максимизировать естественные кластеры данных.

#train and plot tSNE
from sklearn.manifold import TSNE
tsne = TSNE(n_components=2, learning_rate=300, perplexity=30, early_exaggeration=12, init=’random’, random_state=2019)
X_tsne = tsne.fit_transform(X_z)
labels = [“tSNE1”,”tSNE2"]
plot_pca(X_tsne,labels,Y)

Явно существует ряд похожих студенческих кластеров, но они не разделяются по уровню адаптивности. Давайте посмотрим, что мы можем сделать с некоторыми распространенными алгоритмами классификатора ML. Здесь мы будем использовать Logistic Regression, K-Nearest Neighbors, Decision Tree, Random Forest, Support Vector Machine и Artificial Neural Network. В оригинальной публикации авторы не использовали Logistic Regression, который часто не работает так же хорошо для n>2 классов, хотя и допускает мультиклассирование. Однако они построили модель Naive Bayes, которая предполагает, что ни одна из независимых переменных не влияет друг на друга. Тем не менее, мы можем разумно предположить взаимосвязь между некоторыми переменными, такими как вероятность того, что более образованные учащиеся будут старше, и насколько обеспеченные в финансовом отношении учащиеся с большей вероятностью смогут позволить себе более высокую скорость интернета и более дорогие устройства. Следовательно, это неверное предположение для этого набора данных, поэтому мы не будем включать модель Naive Bayes.

3. Оптимизация модели с помощью GridSearchCV

Все алгоритмы машинного обучения имеют ряд гиперпараметров, которые влияют на то, как они строят модель. К ним относятся regularization parameters, scaling values, solver algorithms, tree depth и number of neighbors, а также многие другие. Оптимальная настройка для любого из этих гиперпараметров редко бывает очевидной, поэтому необходимо повторять и измерять точность модели в диапазоне этих настроек. GridSearchCV автоматизирует этот процесс, запуская репликацию cross-validations всех комбинаций этих гиперпараметров, а затем выбирая набор гиперпараметров с наивысшей точностью модели.

Мы начнем со случайного разделения данных на тренировочный (80%) и тестовый (20%) наборы. GridSearchCV также разделит входные обучающие данные на тестовый поезд и, таким образом, сообщенную точность в выборочной точности обучения. Чтобы сравнить и выбрать лучшую общую модель, нам также необходимо измерить точность вне выборки с помощью тестового набора. Мы также отфильтруем предупреждающие сообщения, чтобы ограничить распечатки только сообщениями о ходе выполнения.

#split train and test data sets
from sklearn.model_selection import train_test_split
X_train, X_test, Y_train, Y_test = train_test_split(X_z, Y, test_size=0.2, random_state=2)
#ignore all warnings
from warnings import simplefilter
simplefilter(action='ignore')

Теперь мы можем приступить к оптимизации нашей первой модели. Начнем с Logistic Regression. Всегда полезно просмотреть описания гиперпараметров в руководстве пользователя перед построением новой модели. Для нашего набора данных мы оптимизируем гиперпараметры C, penalty и solver, установив для multi_class значение multinomial. Это делается с помощью словаря parameters. Затем мы создаем объект GridSearchCV, используя parameters объект модели Logisitic Regression и определенное количество cross-validations (здесь мы используем 10). Наконец, мы оптимизируем нашу модель, подгоняя наши обучающие данные.

#train logistic regression
from sklearn.model_selection import GridSearchCV
from sklearn.linear_model import LogisticRegression
parameters = {'C': np.logspace(-2, 0, 20),
              'penalty': ['none', 'l2', 'l1', 'elasticnet'],
              'solver': ['newton-cg', 'lbfgs', 'sag', 'saga'],
              'multi_class': ['multinomial']}
lr = LogisticRegression()
grid_search = GridSearchCV(lr, parameters, cv=10, verbose=0)
logreg_cv = grid_search.fit(X_train, Y_train)

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

#test logistic regression
print(“Tuned hpyerparameters (best parameters):”, logreg_cv.best_params_)
print(“Train accuracy:”, logreg_cv.best_score_)
print(“Test accuracy:”, logreg_cv.best_estimator_.score(X_test, Y_test))

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

#plot logreg scores
logreg_cv_df = pd.DataFrame(logreg_cv.cv_results_[‘params’])
logreg_cv_df[‘score’] = logreg_cv.cv_results_[‘mean_test_score’]
sns.lineplot(data=logreg_cv_df, x=’C’, y=’score’, hue=’penalty’)
plt.show()

Наиболее точная модель Logistic Regression была достигнута с параметрами l1 и l2 pentalty и regularization около 0,4. Тем не менее, при максимальной точности 70% это значительно уступает лучшей модели из оригинальной публикации (точность 89,63% достигается с моделью Random Forest).

Давайте посмотрим, сможем ли мы получить лучший результат, используя K-Nearest Neighbors.

#train k-nearest neighbors
from sklearn.neighbors import KNeighborsClassifier
parameters = {'n_neighbors': list(range(1, 20)),
              'weights': ['uniform', 'distance'],
              'algorithm': ['auto', 'ball_tree', 'kd_tree', 'brute'],
              'p': [1,2]}
knn = KNeighborsClassifier()
grid_search = GridSearchCV(knn, parameters, cv=10, verbose=0)
knn_cv = grid_search.fit(X_train, Y_train)
#test k-nearest neighbors
print("Tuned hpyerparameters (best parameters):", knn_cv.best_params_)
print("Train accuracy:", knn_cv.best_score_)
print("Test accuracy:", knn_cv.best_estimator_.score(X_test, Y_test))

#plot knn scores
knn_cv_df = pd.DataFrame(knn_cv.cv_results_[‘params’])
knn_cv_df[‘score’] = knn_cv.cv_results_[‘mean_test_score’]
sns.lineplot(data=knn_cv_df, x=’n_neighbors’, y=’score’, hue=’weights’, style=’algorithm’)
plt.show()

Метод соседства weighting оказался наиболее влиятельным гиперпараметром на точность модели K-Nearest Neighbors. Тип вычислений algorithm также играл большую роль при более низких значениях n_neighbor, но его влияние уменьшалось выше примерно 10 соседей. Эта модель достигла вневыборочной точности 88,80%, что приближается к точности 89,63%, которой первоначальные авторы достигли с моделью Random Forest.

Далее, давайте создадим файл Decision Tree.

#train decision tree
from sklearn.tree import DecisionTreeClassifier
parameters = {'criterion': ['gini', 'entropy', 'log_loss'],
              'splitter': ['best', 'random'],
              'max_depth': [2*n for n in range(1,10)],
              'max_features': ['auto', 'sqrt', 'log2'],
              'min_samples_leaf': [1, 2, 4],
              'min_samples_split': [2, 5, 10]}
tree = DecisionTreeClassifier()
grid_search = GridSearchCV(tree, parameters, cv=10, verbose=0)
tree_cv = grid_search.fit(X_train, Y_train)
#test decision tree
print("Tuned hpyerparameters (best parameters):", tree_cv.best_params_)
print("Train accuracy:", tree_cv.best_score_)
print("Test accuracy:", tree_cv.best_estimator_.score(X_test, Y_test))

#plot decision tree scores
tree_cv_df = pd.DataFrame(tree_cv.cv_results_[‘params’])
tree_cv_df[‘score’] = tree_cv.cv_results_[‘mean_test_score’]
sns.lineplot(data=tree_cv_df, x=’max_depth’, y=’score’, hue=’min_samples_leaf’, style=’min_samples_split’)
plt.show()

Модель Decision Tree показала лучшие результаты с минимальными значениями min_samples_leaf и min_samples_split и высоким деревом max_depth. Это указывает на то, что модель должна делать много небольших делений, чтобы классифицировать выборки, что согласуется с трудностью создания кластеров путем ординации. Модель по-прежнему достигла высокой точности 88,38%, хотя это глубокое, мелкозернистое переменное разделение не очень эффективно.

Теперь перейдем к Random Forest, который является Decision Tree «метаоценкой». Это также дало первоначальным авторам их лучший результат.

#train random forest
from sklearn.ensemble import RandomForestClassifier
parameters = {'criterion': ['gini', 'entropy', 'log_loss'],
              'max_depth': [2*n for n in range(1,10)],
              'max_features': ['auto', 'sqrt', 'log2'],
              'min_samples_leaf': [1, 2, 4],
              'min_samples_split': [2, 5, 10]}
forest = RandomForestClassifier()
grid_search = GridSearchCV(forest, parameters, cv=10, verbose=0)
forest_cv = grid_search.fit(X_train, Y_train)
#test random forest
print("Tuned hpyerparameters (best parameters):", forest_cv.best_params_)
print("Train accuracy:", forest_cv.best_score_)
print("Test accuracy:", forest_cv.best_estimator_.score(X_test, Y_test))

#plot random forest scores
forest_cv_df = pd.DataFrame(forest_cv.cv_results_[‘params’])
forest_cv_df[‘score’] = forest_cv.cv_results_[‘mean_test_score’]
sns.lineplot(data=forest_cv_df, x=’max_depth’, y=’score’, hue=’min_samples_leaf’, style=’min_samples_split’)
plt.show()

Наша модель Random Forest, основанная на консенсусе голосов от 100 независимых деревьев решений, лишь незначительно улучшила точность исходной модели Decision Tree. Он по-прежнему полагался на минимальные значения min_samples_leaf и min_samples_split, хотя и уменьшил max_depth до 14. Удивительно, что наш Random Forest оказался хуже по сравнению с исходной публикацией, хотя авторы не описали, какие гиперпараметры они выбрали, и мы не настроили все возможных параметров здесь.

Мы немного по-другому оптимизируем нашу модель Support Vector Machine. SVM находят функцию гиперплоскости, чтобы максимизировать разделение между классами, добавляя к данным более высокую размерность в своего рода обратной ординации. Это вычислительно затратная модель по сравнению с другими, и моя машина (32 ГБ ОЗУ, 3600 МГц / 8 ЦП) остановилась при попытке запустить ее через GridSearchCV сразу. Используемый здесь подход с вложенным циклом for представляет собой ручную итерацию GridSearchCV, которая выполняет только часть общего поиска по сетке за раз. Это все еще заняло у моей машины 4 дня, поэтому я рекомендую включить некоторое количество сообщений о прогрессе с настройкой verbose, чтобы сохранить ваше здравомыслие.

#train support vector machine
from sklearn.svm import SVC
svm_params = []
svm_scores = np.empty(0)
for i in np.logspace(-3, 3, 10):
    for j in range(2,5):
        for k in np.logspace(-3, 1, 10):
            parameters = {'C': [i],
                          'kernel': ['linear', 'poly', 'rbf', 'sigmoid'],
                          'degree': [j],
                          'gamma': [k]}
            svm = SVC()
            grid_search = GridSearchCV(svm, parameters, cv=10, verbose=1)
            svm_cv = grid_search.fit(X_train, Y_train)
            
            svm_params = svm_params + svm_cv.cv_results_['params']
            svm_scores = np.append(svm_scores, svm_cv.cv_results_['mean_test_score'])
#test support vector machine
print("Tuned hpyerparameters (best parameters):", svm_params[np.argmax(svm_scores)])
print("Train accuracy:", np.max(svm_scores))
svm_cv = SVC(C=svm_params[np.argmax(svm_scores)]['C'],
             degree=svm_params[np.argmax(svm_scores)]['degree'],
             gamma=svm_params[np.argmax(svm_scores)]['gamma'],
             kernel=svm_params[np.argmax(svm_scores)]['kernel'])
svm_cv.fit(X_train, Y_train)
print("Test accuracy:", svm_cv.score(X_test, Y_test))

#plot svm scores
svm_cv_df = pd.DataFrame(svm_params)
svm_cv_df['score'] = svm_scores
from mpl_toolkits.mplot3d import Axes3D
fig = plt.figure()
ax = fig.gca(projection='3d')
surf = ax.plot_trisurf(svm_cv_df[(svm_cv_df['C']==0.001) & (svm_cv_df['kernel']=='poly')]['degree'],
                       svm_cv_df[(svm_cv_df['C']==0.001) & (svm_cv_df['kernel']=='poly')]['gamma'],
                       svm_cv_df[(svm_cv_df['C']==0.001) & (svm_cv_df['kernel']=='poly')]['score'],
                       cmap=plt.cm.viridis, linewidth=0.2)
ax.view_init(18,300)
ax.set_xlabel('degree')
ax.set_ylabel('gamma')
ax.set_zlabel('score')
plt.show()

Support Vector Machine обеспечил лучшую классификацию с точностью 89,21%. Хотя это все еще немного хуже модели в оригинальной публикации. Наиболее важными гиперпараметрами были полиномиальные degree и gamma масштабирование.

Наконец, давайте посмотрим на модель Artificial Neural Net.

#train artificial neural network
from sklearn.neural_network import MLPClassifier
parameters = {'activation': ['identity', 'logistic', 'tanh', 'relu'],
              'solver': ['lbfgs', 'sgd', 'adam'],
              'alpha': np.logspace(-5, 0, 10),
              'learning_rate': ['adaptive'],
              'max_iter': [1000]}
ann = MLPClassifier()
grid_search = GridSearchCV(ann, parameters, cv=10, verbose=0)
ann_cv = grid_search.fit(X_train, Y_train)
#test artificial neural network
print("Tuned hpyerparameters (best parameters):", ann_cv.best_params_)
print("Train accuracy:", ann_cv.best_score_)
print("Test accuracy:", ann_cv.best_estimator_.score(X_test, Y_test))

#plot ann scores
ann_cv_df = pd.DataFrame(ann_cv.cv_results_['params'])
ann_cv_df['score'] = ann_cv.cv_results_['mean_test_score']
sns.lineplot(data=ann_cv_df, x='alpha', y='score', hue='solver', style='activation')
plt.legend(loc='lower right')
plt.show()

Artificial Neural Network имел более высокую точность внутри выборки, чем Support Vector Machine, но такую ​​же точность вне выборки - 89,21%. Гиперпараметр solver оказал наибольшее влияние на точность модели, при этом lbfgs показали лучшие результаты, особенно с функциями tanh и relu activation. Это соответствует документации ANN, которая рекомендует lbfgs для более быстрой сходимости на небольших наборах данных.

GridSearchCV также смог оптимизировать ИНС намного быстрее, чем SVM, за 1 час против 4 дней, что сделало ИНС более эффективным вариантом реализации для этого набора данных.

4. Окончательное сравнение моделей

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

#compare models
print("Logistic Regression: {}\nKNN: {}\nDecision Tree: {}\nRandom Forest: {}\nSVM: {}\nANN: {}".format(
    logreg_cv.best_estimator_.score(X_test, Y_test),
    knn_cv.best_estimator_.score(X_test, Y_test),
    tree_cv.best_estimator_.score(X_test, Y_test),
    forest_cv.best_estimator_.score(X_test, Y_test),
    svm_cv.score(X_test, Y_test),
    ann_cv.best_estimator_.score(X_test, Y_test)))

#plot confusion matrices using model predictions
from sklearn.metrics import confusion_matrix
logreg_cm = confusion_matrix(Y_test, logreg_cv.predict(X_test))
knn_cm = confusion_matrix(Y_test, knn_cv.predict(X_test))
tree_cm = confusion_matrix(Y_test, tree_cv.predict(X_test))
forest_cm = confusion_matrix(Y_test, forest_cv.predict(X_test))
svm_cm = confusion_matrix(Y_test, svm_cv.predict(X_test))
ann_cm = confusion_matrix(Y_test, ann_cv.predict(X_test))
vmax = max(np.amax(logreg_cm), np.amax(knn_cm), np.amax(tree_cm), np.amax(forest_cm), np.amax(svm_cm), np.amax(ann_cm))
fig, axs = plt.subplots(2, 3, constrained_layout=True)
sns.heatmap(logreg_cm, annot=True, cbar=False, ax=axs[0,0], cmap='mako', fmt='g', vmin=0, vmax=vmax)
sns.heatmap(knn_cm, annot=True, cbar=False, ax=axs[1,0], cmap='mako', fmt='g', vmin=0, vmax=vmax)
sns.heatmap(tree_cm, annot=True, cbar=False, ax=axs[0,1], cmap='mako', fmt='g', vmin=0, vmax=vmax)
sns.heatmap(forest_cm, annot=True, cbar=False, ax=axs[1,1], cmap='mako', fmt='g', vmin=0, vmax=vmax)
sns.heatmap(svm_cm, annot=True, cbar=False, ax=axs[0,2], cmap='mako', fmt='g', vmin=0, vmax=vmax)
sns.heatmap(ann_cm, annot=True, cbar=False, ax=axs[1,2], cmap='mako', fmt='g', vmin=0, vmax=vmax)
axs[0,0].set_title('Logistic Regression')
axs[1,0].set_title('K-Nearest Neighbors')
axs[0,1].set_title('Decision Tree')
axs[1,1].set_title('Random Forest')
axs[0,2].set_title('Support Vector Machine')
axs[1,2].set_title('Artificial Neural Network')
axs[0,0].xaxis.set_ticklabels(['Low', 'Mod', 'High'])
axs[1,0].xaxis.set_ticklabels(['Low', 'Mod', 'High'])
axs[0,1].xaxis.set_ticklabels(['Low', 'Mod', 'High'])
axs[1,1].xaxis.set_ticklabels(['Low', 'Mod', 'High'])
axs[0,2].xaxis.set_ticklabels(['Low', 'Mod', 'High'])
axs[1,2].xaxis.set_ticklabels(['Low', 'Mod', 'High'])
axs[0,0].yaxis.set_ticklabels(['Low', 'Mod', 'High'])
axs[1,0].yaxis.set_ticklabels(['Low', 'Mod', 'High'])
axs[0,1].yaxis.set_ticklabels(['Low', 'Mod', 'High'])
axs[1,1].yaxis.set_ticklabels(['Low', 'Mod', 'High'])
axs[0,2].yaxis.set_ticklabels(['Low', 'Mod', 'High'])
axs[1,2].yaxis.set_ticklabels(['Low', 'Mod', 'High'])
fig.supxlabel('Predicted Adaptability')
fig.supylabel('True Adaptability')
plt.show()

Здесь мы видим, что плохой результат Logistic Regression является результатом склонности к бинарным предсказаниям, классифицируя адаптируемость всех, кроме одного учащегося, как низкую или умеренную несмотря на то, что параметр mutli_class оптимизирован до multinomial. Все остальные модели были почти идентичными, отличаясь только количеством недоклассифицированных умеренно адаптируемых учащихся.

Наконец, давайте сравним, как точность внутри и вне выборки меняется для каждой модели.

#compare model accuracies
model_acc = {'Model': ['LogReg', 'KNN', 'Tree', 'Forest', 'SVM', 'ANN',
                       'LogReg', 'KNN', 'Tree', 'Forest', 'SVM', 'ANN'],
             'Accuracy': [logreg_cv.best_score_,
                          knn_cv.best_score_,
                          tree_cv.best_score_,
                          forest_cv.best_score_,
                          np.max(svm_scores),
                          ann_cv.best_score_,
                          logreg_cv.best_estimator_.score(X_test, Y_test),
                          knn_cv.best_estimator_.score(X_test, Y_test),
                          tree_cv.best_estimator_.score(X_test, Y_test),
                          forest_cv.best_estimator_.score(X_test, Y_test),
                          svm_cv.score(X_test, Y_test),
                          ann_cv.best_estimator_.score(X_test, Y_test)],
             'Data': ['GridSearch', 'GridSearch', 'GridSearch', 'GridSearch', 'GridSearch', 'GridSearch',
                      'Test', 'Test', 'Test', 'Test', 'Test', 'Test']}
model_df = pd.DataFrame(model_acc)
sns.catplot(kind='bar', y='Accuracy', x='Model', hue='Data', data=model_df)
plt.ylim(0.65, None)
plt.xlabel('Model', fontsize=13)
plt.ylabel('Accuracy', fontsize=13)
plt.show()

Точность внутри выборки была выше, чем точность вне выборки для каждой отдельной модели, что свидетельствует о важности обучения и проверки моделей с использованием разных данных. Помимо Logistic Regression, точность разных моделей была очень сопоставимой. Наш окончательный выбор модели сводился к выбору эффективности обучения между моделями Support Vector Machine и Artificial Nueral Network, хотя каждая из K-Nearest Neighbors, Decision Tree или Random Forest была значительно быстрее, чем ИНС, и лишь незначительно менее точна в этом наборе данных. В зависимости от ваших вычислительных ресурсов, более высокая эффективность обучения может разумно повлиять на окончательный выбор модели в пользу любой из этих трех других.

5. Выводы

Как мы видим, GridSearchCV — незаменимый инструмент для выбора модели машинного обучения, позволяющий быстро и удобно сравнивать гиперпараметры нескольких моделей. Однако для более ресурсоемких моделей, таких как Support Vector Machines, иногда необходимо вручную выполнить итерацию GridSearchCV, чтобы она завершилась. Это легко сделать с помощью for-loops.

Прежде чем остановиться на модели и развернуть ее, всегда рекомендуется провести перекрестную проверку точности вашей модели путем повторной выборки данных обучения и тестирования. Это можно сделать с помощью инструмента cross_val_score Scikit-learn, о котором я расскажу в следующем посте.