Лучшие практики для часто упускаемой из виду части процесса машинного обучения

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

❓ Что такое случайное семя?

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

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

1. Разделение данных на наборы для обучения / проверки / тестирования: случайные начальные числа гарантируют, что данные разделяются одинаково каждый раз при запуске кода.

2. Обучение модели: такие алгоритмы, как случайный лес и усиление градиента, недетерминированы (для заданного входа выходные данные не всегда одинаковы) и поэтому требуют случайного начального аргумента для воспроизводимых результатов.

Помимо воспроизводимости, случайные семена также важны для результатов тестирования. Если вы тестируете несколько версий алгоритма, важно, чтобы все версии использовали одни и те же данные и были как можно более похожими (за исключением параметров, которые вы тестируете).

Как обычно устанавливаются случайные семена

Несмотря на свою важность, случайные семена часто устанавливаются без особых усилий. Я виноват в этом. Обычно я использую дату того дня, над которым работаю (поэтому 1 марта 2020 года я бы использовал начальное число 20200301). Некоторые люди каждый раз используют одно и то же начальное число, а другие генерируют их случайным образом.

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

🚢 Титаник Данные

Теперь я продемонстрирую, какое влияние может иметь выбор случайного начального числа. Для этого я воспользуюсь хорошо известным набором данных Titanic (ссылка для скачивания находится ниже).



Следующий код и графики созданы в Python, но я нашел аналогичные результаты в R. Полный код, связанный с этим сообщением, можно найти в репозитории GitHub ниже:



Во-первых, давайте посмотрим на несколько строк этих данных:

import pandas as pd
train_all = pd.read_csv('train.csv') 
# Show selected columns 
train_all.drop(['PassengerId', 'Parch', 'Ticket', 'Embarked', 'Cabin'], axis = 1).head()

Данные Титаника уже разделены на обучающие и тестовые наборы. Классическая задача для этого набора данных - предсказать выживаемость пассажиров (кодируется в столбце Survived). В тестовых данных нет ярлыков для столбца Survived, поэтому я сделаю следующее:

1. Предоставление части обучающих данных для использования в качестве набора для проверки.

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

Разделение данных

Начнем с общего распределения столбца Survived.

In [19]: train_all.Survived.value_counts() / train_all.shape[0] 
Out[19]:
0    0.616162 
1    0.383838 
Name: Survived, dtype: float64

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

from sklearn.model_selection import train_test_split
# Create data frames for dependent and independent variables 
X = train_all.drop('Survived', axis = 1) 
y = train_all.Survived  
# Split 1 
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size = 0.2, random_state = 135153) 
In [41]: y_train.value_counts() / len(y_train) 
Out[41]:  
0    0.655899 
1    0.344101 
Name: Survived, dtype: float64  
In [42]: y_val.value_counts() / len(y_val) 
Out[42]:  
0    0.458101 
1    0.541899 
Name: Survived, dtype: float64

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

# Split 2 
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size = 0.2, random_state = 163035) 
In [44]: y_train.value_counts() / len(y_train) 
Out[44]:  
0    0.577247 
1    0.422753 
Name: Survived, dtype: float64  
In [45]: y_val.value_counts() / len(y_val) 
Out[45]:  
0    0.77095 
1    0.22905 Name: Survived, dtype: float64

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

Полное раскрытие, эти примеры - самые экстремальные, которые я обнаружил после перебора 200K случайных начальных чисел. Тем не менее, у этих результатов есть несколько проблем. Во-первых, в обоих случаях распределение выживаемости существенно различается между обучающей и проверочной наборами. Это, скорее всего, отрицательно скажется на обучении модели. Во-вторых, эти выходы сильно отличаются друг от друга. Если, как это делает большинство людей, вы произвольно устанавливаете случайное начальное число, полученные разбиения данных могут сильно различаться в зависимости от вашего выбора.

Я расскажу о лучших практиках в конце поста. Затем я хочу показать, как различались Survival распределения обучения и проверки для всех 200K случайных начальных чисел, которые я тестировал.

~ 23% разбиений данных привели к разнице в процентах выживаемости не менее 5% между обучающими и проверочными наборами. Более 1% разделений привели к разнице в процентах выживаемости не менее 10%. Наибольшая разница в процентах выживаемости составила ~ 20%. Вывод здесь заключается в том, что использование произвольного случайного начального числа может привести к большим различиям между распределениями обучающего и проверочного набора. Эти различия могут иметь непредвиденные последующие последствия в процессе моделирования.

📈 Обучение модели

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

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

Сначала я создам набор для обучения и проверки.

X = X[['Pclass', 'Sex', 'SibSp', 'Fare']]  # These will be my predictors 
# The “Sex” variable is a string and needs to be one-hot encoded 
X['gender_dummy'] = pd.get_dummies(X.Sex)['female'] 
X = X.drop(['Sex'], axis = 1)  
# Divide data into training and validation sets 
# I’ll discuss exactly why I divide the data this way in the next section 
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size = 0.2, random_state = 20200226, stratify = y)

Теперь я обучу пару моделей и оценим точность на наборе для проверки.

# Model 1 
from sklearn.ensemble import RandomForestClassifier 
from sklearn.metrics import accuracy_score  
# Create and fit model 
clf = RandomForestClassifier(n_estimators = 50, random_state = 11850) 
clf = clf.fit(X_train, y_train)  
preds = clf.predict(X_val)  # Get predictions  
In [74]: round(accuracy_score(y_true = y_val, y_pred = preds), 3) Out[74]: 0.765  
# Model 2
# Create and fit model 
clf = RandomForestClassifier(n_estimators = 50, random_state = 2298)
clf = clf.fit(X_train, y_train)
preds = clf.predict(X_val)  # Get predictions
In [78]: round(accuracy_score(y_true = y_val, y_pred = preds), 3)
Out[78]: 0.827

Я протестировал 25K случайных семян, чтобы найти эти результаты, но изменение точности на ›6% определенно заслуживает внимания! Опять же, эти 2 модели идентичны, за исключением случайного начального числа.

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

В то время как большинство моделей достигли точности ~ 80%, существует значительное количество моделей с оценкой от 79% до 82% и небольшое количество моделей, которые имеют оценку за пределами этого диапазона. В зависимости от конкретного варианта использования эти различия достаточно велики, чтобы иметь значение. Следовательно, при передаче результатов заинтересованным сторонам следует учитывать отклонения в производительности модели из-за случайного выбора начального числа.

Лучшие практики

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

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

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

Вот как выглядит стратифицированная выборка в коде.

# Overall distribution of “Survived” column 
In [19]: train_all.Survived.value_counts() / train_all.shape[0] 
Out[19]:  
0    0.616162 
1    0.383838 
Name: Survived, dtype: float64  
# Stratified sampling (see last argument) 
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size = 0.2, random_state = 20200226, stratify = y)   
In [10]: y_train.value_counts() / len(y_train) 
Out[10]:  
0    0.616573 
1    0.383427 
Name: Survived, dtype: float64  
In [11]: y_val.value_counts() / len(y_val) 
Out[11]:  
0    0.614525 
1    0.385475 
Name: Survived, dtype: float64

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

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

Однако, прежде чем сообщать показатели эффективности заинтересованным сторонам, окончательная модель должна быть обучена и оценена с 2–3 дополнительными начальными числами, чтобы понять возможные различия в результатах. Эта практика позволяет более точно передавать характеристики модели. Для критически важной модели, работающей в производственной среде, стоит подумать о запуске этой модели с несколькими начальными значениями и усреднением результата (хотя, вероятно, это тема для отдельного сообщения в блоге).

🏁 Заключение

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

Если вам понравился этот пост, ознакомьтесь с другими моими работами ниже!