Привет, чемпион!

Если ты читаешь этот пост, значит, тебе стало интересно, не допускаешь ли этих ошибок ты?! Почти уверен, что ты допускал эти ошибки хотя бы раз в жизни. Мы не застрахованы от совершения ошибок, такова наша человеческая натура — ошибаться для нас естественно. Однако, я постараюсь уберечь тебя от тех ошибок, которые совершал сам или замечал у других.

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

Ошибка #1 — AUC и вероятности


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

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

model = best_model()  # Инициализируем модель
model.fit(train, y) # Обучаем модель на тренировочных данных
sample['target'] = model.predict(test)[0;:] # Делаем прогноз и записываем ответ
sample.to_csv('submission.csv') # Сохраняем ответ

Где тут ошибка? В целом, смертельного тут ничего нет. Сам код вполне адекватный. Роковой ошибкой здесь является использование функции predict вместо predict_proba. Вон эта функция, предательски смотрит на тебя с 3-й строчки кода.

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

Ошибка #2 — Аккаунты и дополнительные попытки


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

Суть проста. Ты регистрируешь второй профиль, с которого тестируешь дополнительные гипотезы в надежде увеличить точность решения. Тебе это даже помогает. Однако система на Kaggle достаточно успешно отслеживает такие лазейки хоть и не сразу оповещается тебя об этом. Незнание, что Kaggle запросто снимает с лидерборда после объявления финальных результатов, губит медали начинающих каглеров =). Если уж так делаешь, то подходи к этому вопросу с головой!

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

Ошибка #3 — Диверсификация решений


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

Ситуация была следующая. Как известно, на многих соревновательных платформах типа Kaggle необходимо выбрать несколько (обычно два) финальных сабмита. По сабмитам тебя будет судить система на приватных данных. В последний день соревнования у меня был следующий выбор:

  • Модель № 1 со скором 91%
  • Почти та же модель № 1, но со скором 90 %
  • Совсем другая модель № 2 со скором 89%

Какие две модели ты бы выбрал? Логично ли выбрать две лучших модели? Однако, с точки зрения минимизации рисков — оптимально диверсифицировать подходы и выбрать 1 и 3 модели. Суть в том, что, если первая модель будет неудачной, то и вторая модель той же природы также окажется плохой, а вот 3-я модель, в силу иной природы, может стать выигрышной. Именно так и произошло у меня. Я не рискнул выбрать второй моделью — робастную языковую модель в соревновании JigSaw 2022 на Kaggle. Хотя эта модель могла поднять меня на 400 мест вверх.

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

Запомни: диверсифицируй риски и не доверяй полностью скору на публичном борде!

Ошибка #4 — Остановка или «Ой, а ведь мы же могли выиграть!»


Именно из-за этой ошибки люди часто говорят эту фразу! Эта ошибка выкована из сплава лени и надежды на случайный фарт. Именно по этой ошибке я суммарно за последний год потерял около 300 тысяч призовых. Хотя вполне себе их заслужил.

Первый мой случай был в хакатоне от Россельхозбанка по классификации временных рядов. Всё свелось к тому, что за 10 минут до конца чемпионата, нам удалось выбить скор на топ-4. Призовые, как известно, начинаются только с 3-го места. Тогда я не верил в силу шафла и магии стабильных решений. Как итог, мы не стали прикреплять решение для сабмита на 4-е место. Ну вот не дураки ли!

Как ты думаешь, что произошло на следующее утром с лидербордом?! Проснувшись утром, мы увидели себя на втором месте общего рейтинга. Наше решение оказалось более точным на приватной выборке. Однако, далее, код решения проверялся на воспроизводимость. В нашем же случае код решения прикреплён не был, был отправлен только csv-файл с ответами. Сколько мы не просили потом организаторов прикрепить само решение, после дедлайна нам уже этого сделать не дали. Слава богу, предыдущей версии решения хватило на топ-5. Получили фирменный мерч, уже неплохо.

Скажу, что, получив такой опыт, впредь я всегда довожу решение до конца. Так у меня появились случаи, когда я поднимал скор за считаные часы до конца соревнований из непризовых мест — в топ-3. Полезная привычка!

Запомни: бейся до последнего. Почти всегда происходят shake-up на лидерборде. Обуздай его! А ещё ведь очень часто снимают участников с лидерборда. Непризовое место на паблике сегодня не означает непризовое место на привате завтра! Верь в своё решение! В противном случае ты хотя бы получишь опыт.

Ошибка #5 — Невоспроизводимость результата против новых гипотез.


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

Почему? В чём смысл? Это хоть на что-то влияет? Да! Смотри картинку ниже!

Точность достаточно сильно варьируется в зависимости от выбранного random seed

А теперь представь, что ты решил проверить новую фичу на предмет того, помогает ли она увеличить точность модели или нет. Ты добавляешь новую фичу к имеющимся. Вдруг видишь, что точность модели увеличивается. Вопрос: точность увеличилась из-за новой фичи или из-за случайности? — Ответ: Неизвестно =). Хочешь однозначно проверять гипотезы — фиксируй случайность, иначе твоя модель раз от раза будет показывать тебе разные результаты. Например, в коде ниже фиксируются все сиды при работе с pytorch и numpy:

def seed_everything(seed = 1234):
     random.seed(seed)
     os.environ['PYTHONHASHSEED'] = str(seed)
     np.random.seed(seed)     torch.manual_seed(seed)
     torch.cuda.manual_seed(seed)
     torch.backends.cudnn.deterministic = True

seed_everything()

# Обрати внимание, ML модели и методы тоже имеют свой random_state/seed !

Да, иногда полностью случайность зафиксировать нельзя. Окей, тогда просто усредни результат по сидам и будет тебе счастье!

Запомни: Фиксировать сиды надо хотя бы потому, что если ты попадёшь в топ лидеров, тебе важно будет раз от раза уметь воспроизводить свой результат! Особенно актуально, если соревнование требует не просто csv-файла с ответом, а ещё и код решения с воспроизводимым решением («code competition»).

Ошибка #6 — Считаем признаки снова и снова!


Эта ошибка уже не такая смертельная, но всё ещё неприятная. Если весь твой ML пайплайн отрабатывает не больше 10 минут, то не стоит беспокоиться. А вот если генерация признаков и обучение модели занимает больше часа? А если больше 3 часов? Как часто ты сможешь запускать такую модель, чтобы тестировать гипотезы? Всё усложняется ещё тем, что, если произойдёт сбой и твой код упадёт, то, скорее всего, придётся считать всё заново. Достаточно ли у тебя времени на такие случаи? — Думаю, что нет. Видел примеры, как новички часами тратят время зря, вместо того, чтобы сохранять промежуточные результаты и заниматься вещами поинтереснее.

Вот тебе простой пример кода, отражающий эту best practice.

generate_features = True  # Флаг, который ты легко можно переключать

if generate_features: # При первом проходе готовим данные
     df = make_features(df) # Подготавливаем признаки для трейна
     df.to_csv('train_with_features.csv', index=False) # Сохраняем
     sub = make_features(sub) # Подготавливаем признаки для сабмита
     sub.to_csv('test_with_features.csv', index=False) # Сохраняем

else: # Если признаки готовы, то теперь экономим наше бесценное время
     df = pd.read_csv('train_with_features.csv')
     sub = pd.read_csv('test_with_features.csv')

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

Теперь тебе не придётся пересчитывать каждый раз новые признаки. Можешь сразу переходить к обучению модели. По моему опыту удаётся экономить так десятки часов времени при работе с крупными объёмами данных. Логику этого кода можно всячески улучшать, но главное — помни, что экономия времени даст тебе возможность быть быстрее твоих оппонентов. Ничто не мешает тебе дампить не только результаты подготовки признаков, но и промежуточные результаты обучения моделей. Привет любителям Google Colab, у которых постоянно тухнет сессия =).

Запомни: не считай дважды то, что можно посчитать один раз (Конфуция).

Ошибка #7 — Валидация как бермудский треугольник для твоих данных


Казалось бы, при обучении ML моделей важную роль играет объём данных, но я постоянно вижу, как новички после онлайн-курсов по Data Science допускают эту глупую ошибку и теряют кусок данных на ровном месте. И ладно если речь идёт про учебный проект. А если так делать на чемпионатах или на работе?! — К успеху это не приведёт. Посмотрим на код с этой проблемой.

from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression

# X, y это данные для обучения
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, random_state=42)

model = LogisticRegression(random_state = 42)

model.fit(X_train, y_train)
print('Скор при валидации'.format(model.score(X_test, y_test))

sample['target'] = model.predict(sub)[0;:] # sub это данные для прогноза
sample.to_csv('submission.csv') # Сохраняем результат

Что тут не так? На первый взгляд, всё верно! Тут есть разбиение на тренировочную и тестовую выборки — хорошо!!! Модель даже оценивается на тестовой выборке — отлично! Более того, везде даже зафиксированы random_state'ы — здорово!!!

А вот не смущает ли тебя тот факт, что в этом коде модель не увидела 33% обучающих данных? Ты заметил, куда эти данные делись? Данные, отложенные для валидации, в итоге не были приглашены на танец fit'а. Эти данные просто-напросто остались грустить в сторонке. Вот как стоит поправить этот код, чтобы модель всё же увидела все данные при обучении.

# X, y это данные для обучения
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, random_state=42)

model = LogisticRegression(random_state = 42)

model.fit(X_train, y_train)
print('Скор при валидации'.format(model.score(X_test, y_test))

model.fit(X, y)  # переобучаем модель на всех данных!!!!

sample['target'] = model.predict(sub) # sub это данные для прогноза
sample.to_csv('submission.csv') # Сохраняем результат

Запомни: обучай модель на всех данных, которые у тебя есть. Частой практикой является усреднение по нескольким фолдам.

Ошибка #8 — Чё там по типам в Pandas?


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

Пример с ошибкой

И снова тут не возникает сомнений в коде. Вроде бы Pandas всё корректно подгрузил в память. Однако, нам так кажется только потому, что мы не знаем, как выглядят данные на самом деле. А вот как они выглядят в реальности:


Если явно указать тип столбца, то вдруг появляются нули в значениях. По ходу работы с табличными данными ты рано или поздно заметишь, что id-ники и категориальные переменные — это не редко строковые типы (str), которые начинаются с 0. Поэтому при конвертации в численный формат (int), этот ноль теряется. Надо ли говорить, к каким проблемам это может привести =)? Как минимум эта таблица будет плохо merge'ться с другими таблицами. Такого рода ошибки настолько популярные, что на моей памяти есть примеры победителей хакатона, кто выиграл несмотря на косяк с импортом. Видимо тогда на это не обратили внимание остальные участники тоже, и это уравняло шансы.

Запомни: Всегда задумывайся о том, какие типы данных ты импортируешь. Не доверяй это дело полностью Pandas'у. Есть еще много других тонкостей, которые полезно знать про Pandas, но это уже совсем другая история.

▍ Заключение


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

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

Комментарии (10)


  1. sergyalosovetsky
    05.04.2022 15:41
    +1

    >А вот не смущает ли тебя тот факт, что в этом коде модель не увидела 33% обучающих данных? Ты заметил, куда эти данные делись? Данные, отложенные для валидации, в итоге не были приглашены на танец fit'а

    по-моему, там было сделано все правильно

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

    иначе невозможно детектировать проблему переобучения модели


    1. Aleron75 Автор
      05.04.2022 16:40
      +3

      Привет, Сергей!

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


      1. vilka14
        06.04.2022 12:19

        В cs231 есть другое мнение на этот счет (п.6 Summary):

        Take note of the hyperparameters that gave the best results. There is a question of whether you should use the full training set with the best hyperparameters, since the optimal hyperparameters might change if you were to fold the validation data into your training set (since the size of the data would be larger). In practice it is cleaner to not use the validation data in the final classifier and consider it to be burned on estimating the hyperparameters. 

        https://cs231n.github.io/classification/

        Справедливости ради, этот совет относится к KNN. Я скорее к тому, что решение может быть не столь однозначным.


        1. Soukhinov
          06.04.2022 15:03
          +2

          Так вам надо победить, или следовать best practices из учебника?

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


    1. sunnybear
      05.04.2022 17:50

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


  1. lavifae
    06.04.2022 09:47
    +1

    Спасибо за хорошо структурированную статью! Мое уважение ошибке #8, не сталкивалась с подобными подставами со стороны pandas, а теперь и впредь не столкнусь.

    Ошибки 6 и 7 очень жизненные даже в рабочих задачах.

    + Всплывает и антипод ошибки 7 - экономия на данных в val/test, которая позднее приводит к недостоверным оценкам гипотез.


    1. Aleron75 Автор
      06.04.2022 11:26

      Благодарю!


  1. Seer777
    06.04.2022 15:37

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


  1. Ananiev_Genrih
    07.04.2022 09:30

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

    Так веб разработчики в 90-х говорили про Internet Explorer: "Это не баг, это фича, просто надо их всех помнить..."

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

    Wes McKinney:10 Things I Hate About pandas

    Утиные истории со стрелами на паркете


  1. yorgo
    07.04.2022 21:51

    Спасибо за статью, но позволю себе замечание по Ошибке #5:

    фиксировать seed'ы и random_state'ы надо

    А кто может дать гарантию, что соотношение метрика(модель_А) > метрика(модель_Б) на одном сиде сохранится при другом?

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

    Чтобы не было сюрпризов со стабильностью модели можно проводить т.н. nested cross-validation, когда сравниваются не точечные значения метрик на наборах гиперпараметров, а их агрегаты (например, доверительные интервалы) по дополнительным переразбиениям внутри каждого цикла обычной кросс-валидации.

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