Эти 8 советов помогут вам обнаружить ошибки перед обучением модели машинного обучения.

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

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

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

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

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

Образец набора данных

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

Одна строка представляет покупателя с его характеристиками и двоичной целевой переменной. customer_id - это индекс в DataFrame.

np.random.seed(42)
n = 1000
df = pd.DataFrame(
    {
        "customer_id": ["customer_%d" % i for i in range(n)],
        "product_a_ratio": np.random.random_sample(n),
        "std_price_product_a": np.random.normal(0, 1, n),
        "n_purchases_product_a": np.random.randint(0, 10, n),
    }
)
df.loc[100, "std_price_product_a"] = pd.NA
df["product_b_ratio"] = df["product_a_ratio"]
df["y"] = np.random.randint(0, 2, n)
df.set_index("customer_id", inplace=True)
df.head()

1. Отметьте цель

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

assert (
    len(set(df[df.y == 0].index).intersection(df[df.y == 1])) == 0
), "Positive customers have intersection with negative customers"

Я использую приведенный выше оператор assert в Jupyter Notebook, который прерывает выполнение, если в обучающем наборе есть ошибка. Поэтому, когда я создаю новый обучающий набор и использую команду Jupyter: «Перезапустить ядро ​​и запустить все ячейки», я могу быть уверен, что обучающий набор имеет требуемые свойства.

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



2. Проверить дубликаты

Как дубликаты попадают в обучающую выборку?

Много раз с объединениями в базах данных SQL!

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

Посмотрите мою статью о том, как избежать ошибок в SQL:



Как мы можем убедиться, что в нашем обучающем наборе нет дубликатов?

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

assert len(df[df.index.duplicated()]) == 0, "There are duplicates in trainset"

3. Проверьте недостающие значения.

По моему опыту, отсутствующие значения появляются по двум причинам:

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

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

Например, LightGBM по умолчанию поддерживает пропущенные значения, и мы можем настроить желаемое поведение.

LightGBM использует NA (NaN) для представления отсутствующих значений по умолчанию. Измените его на использование нуля, установив zero_as_missing = true. Когда zero_as_missing = false (по умолчанию), непоказанные значения в разреженных матрицах (и LightSVM) обрабатываются как нули. Когда zero_as_missing = true, NA и нули (включая непоказанные значения в разреженных матрицах (и LightSVM)) обрабатываются как отсутствующие.

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

for col in df.columns:
    assert df[df[col].isnull()].shape[0] == 0, "%s col has %d missing values" % (col, df[df[col].isnull()].shape[0])

В столбце std_price_product_a отсутствует одно значение. Удалим запись и запустим проверку еще раз.

df = df[df['std_price_product_a'].notnull()].copy()
for col in df.columns:
    assert df[df[col].isnull()].shape[0] == 0, "%s col has %d missing values" % (col, df[df[col].isnull()].shape[0])

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

Если для определенной функции ожидается отсутствующее значение, мы можем занести ее в белый список, чтобы проверка не прервала выполнение в следующий раз.

4. Проверьте масштабы функций.

Работая над проектированием функций, мы определяем определенные функции по шкале от 0 до 1 или по какой-либо другой шкале. Стоит проверить, не находится ли объект между желаемыми границами.

В этом примере мы проверяем только, находятся ли объекты на шкале от 0 до 1, но я бы посоветовал вам добавить больше проверок, подходящих для вашего набора данных.

features_on_0_1_scale = [
    'product_a_ratio',
    'product_b_ratio',
    'y',
]
for col in features_on_0_1_scale:
    assert df[col].min() >= 0 and df[col].max() <= 1, "%s is not on 0 - 1 scale" % col

5. Проверьте типы функций.

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

Мы можем установить типы функций в цикле for:

feature_types = {
    "product_a_ratio": "float64",
    "std_price_product_a": "float64",
    "n_purchases_product_a": "int64",
    "product_b_ratio": "float64",
    "y": "int64",
}
for feature, dtype in feature_types.items():
    df.loc[:, feature] = df[feature].astype(dtype)

Почему это полезно? Что происходит с целочисленным типом данных, когда мы добавляем пропущенное значение?

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

Давайте посмотрим на пример ниже:

df.n_purchases_product_a.values[:10]
# output
array([0, 8, 0, 2, 7, 2, 3, 7, 0, 5])
# add NA to first row
df.loc[0, "n_purchases_product_a"] = pd.NA
df.n_purchases_product_a[:10].values
# output
array([0.0, 8.0, 0.0, 2.0, 7.0, 2.0, 3.0, 7.0, 0.0, 5.0], dtype=object)

6. Уникальные особенности

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

Стоит проверить, входит ли определенный элемент в модель более одного раза. Это кажется тривиальным, но вы можете по ошибке дублировать функции при кодировании и перезапуске Jupyter Notebook в X-й раз.

features = [
    "product_a_ratio",
    "std_price_product_a",
    "n_purchases_product_a",
    "product_b_ratio",
]
assert len(set(features)) == len(features), "Features names are not unique"

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

set(df.columns) - set(features)
# Output
{'y'}

7. Проверьте корреляции.

Проверка корреляции между функциями (и целью) важна при моделировании.

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

Сильно коррелированные функции также проблематичны для моделей, у которых нет проблем с мультиколлинеарностью, таких как Random Forest или Boosting. Например. модель делит важность функции между коррелированной функцией A и функцией B, что вводит в заблуждение.

Давайте построим корреляционную матрицу и попытаемся выявить сильно коррелированные признаки:

import matplotlib.pyplot as plt
import seaborn as sns
corr = df[features].corr()
fig, ax = plt.subplots(figsize=(14, 14))
ax = sns.heatmap(
    corr,
    vmin=-1,
    vmax=1,
    center=0,
    cmap=sns.diverging_palette(20, 220, n=200),
    square=True,
)
ax.set_xticklabels(ax.get_xticklabels(), rotation=45, horizontalalignment="right");

В корреляционной матрице выше мы видим, что product_a_ratio и product_b_ratio сильно коррелированы.

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

В нашем примере для функций коэффициент корреляции Пирсона (PC) равен 1.0, поэтому мы можем безопасно удалить одну из них. Но если бы ПК был 0.9, мы могли бы снизить общую точность модели, удалив такую ​​функцию.

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

features = [
    "product_a_ratio",
    "std_price_product_a",
    "n_purchases_product_a",
    #    "product_b_ratio", # feature has pearson correlation 1.0 with product_a_ratio
]

8. Делайте заметки.

После того, как вы обучили модель и проверили метрики, следующим шагом будет проверка работоспособности с несколькими образцами, которые модель классифицировала с уверенностью (клиенты, классифицированные с вероятностью 0 или 1).

Я обычно просматриваю:

  • топ-5 положительных прогнозов, которые отмечены как положительные в обучающей выборке,
  • топ-5 отрицательных прогнозов, которые отмечены как положительные в обучающей выборке,
  • 5 лучших положительных прогнозов, отмеченных как отрицательные в обучающей выборке.

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

Чтобы помочь вам вспомнить, что вы уже написали отзыв о клиенте, добавьте столбец примечаний в свой DataFrame и напишите краткое примечание к каждому образцу, который вы просматриваете:

df.loc['customer_0', "notes"] = "Positive in training set, but should be negative"
df.loc['customer_1', "notes"] = "good prediction as positive"
df

Заключение

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

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

Шаблон резко снижает количество лишних проверок работоспособности. Меньше моментов типа «Что я опять облажался?».

Вы можете скачать шаблон Блокнот Jupyter здесь.

Прежде чем ты уйдешь

- Labeling and Data Engineering for Conversational AI and Analytics
- Data Science for Business Leaders [Course]
- Intro to Machine Learning with PyTorch [Course]
- Become a Growth Product Manager [Course]
- Deep Learning (Adaptive Computation and ML series) [Ebook]
- Free skill tests for Data Scientists & Machine Learning Engineers

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

Следуйте за мной в Twitter, где я регулярно чирикаю о Data Science и машинном обучении.