8 ошибок, из-за которых ты проиграешь в соревновательном Data Science
Привет, чемпион!
Если ты читаешь этот пост, значит, тебе стало интересно, не допускаешь ли этих ошибок ты?! Почти уверен, что ты допускал эти ошибки хотя бы раз в жизни. Мы не застрахованы от совершения ошибок, такова наша человеческая натура — ошибаться для нас естественно. Однако, я постараюсь уберечь тебя от тех ошибок, которые совершал сам или замечал у других.
Так вышло, что за время участия в чемпионатах по соревновательному анализу данных я достаточно часто бывал в призовых местах. Однако, бывали случаи, когда я лишался призовых по глупости или неосторожности. Рассказываю по порядку.
Ошибка #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, но это другая история.
▍ Заключение
Как видишь, ошибки бывают разные. Существует миллион способов ошибиться и проиграть чемпионат или вообще не выбраться в топ. Тем не менее не бойся делать попытки и ошибаться. Важно совершать новые ошибки и не допускать старых! Тогда ты будешь идти вперёд как бронебойный поезд или как сын маминой подруги!
Надеюсь, тебе были полезны примеры из моего опыта. А если тебе интересно узнать ещё больше про соревновательный анализ данных и про трюки из этого направления, то подписывайся на мой канал в телеграм. Буду рад поделиться с тобой новыми трюками.