Модуль 7. Честное сравнение¶
Чему вы научитесь в этом модуле
- Объяснять, зачем нужны именно три выборки (а не две), и что именно идёт не так, когда их только две.
- Количественно измерять на симуляции, насколько тестовая метрика портится от многократного подглядывания.
- Обнаруживать утечку данных по характерному симптому — подозрительно хорошей метрике.
- Правильно разбивать временные ряды так, чтобы результат оценки соответствовал реальности.
- Вести журнал экспериментальных прогонов, по которому даже через полгода можно восстановить, откуда взялось конкретное число.
Время: примерно одна неделя. Пререквизиты: модуль 6.
Ноутбук: открыть в Colab · notebooks/07-honest-comparison.ipynb
Зачем нужен этот модуль¶
Это самый короткий модуль части II — и самый ценный. Все предыдущие модули учили получать числа (обучать модели, вычислять метрики). Этот модуль определяет, значат ли эти числа хоть что-нибудь.
Модуль 1 перечислил шесть способов получить улучшение, которого на самом деле не существует. Три из них — подглядывание в тестовую выборку, утечка данных и остановка по результату — коренятся именно в процедуре сравнения моделей. Здесь они и лечатся.
Три выборки, а не две¶
Обучающая выборка. На ней подбираются веса модели.
Валидационная выборка. На ней принимаются все промежуточные решения: какую модель выбрать, какую глубину установить, какое значение \(\lambda\) использовать, когда остановить обучение.
Тестовая выборка. На ней измеряется итоговый результат. Открывается строго один раз.
Двух выборок недостаточно, и причина этого неочевидна. Когда вы выбираете лучшую из двадцати моделей по результатам на валидационной выборке, вы одновременно отбираете и настоящее качество, и случайную удачу на этой конкретной выборке. Модель-победитель победила отчасти благодаря случайности, и поэтому её результат на валидации систематически завышен по сравнению с тем, что она покажет на действительно новых данных.
Это ровно то же самое множественное сравнение, которое описано в модуле 1 (шестой пункт списка ошибок). Из двадцати моделей одна неизбежно окажется «удачливее» других просто в силу теории вероятностей.
Тестовая выборка существует именно для того, чтобы измерить качество победителя на данных, которые не участвовали в процессе его отбора. Если тестовая выборка уже использовалась для принятия решений — она перестаёт быть тестовой и превращается во вторую валидационную.
Тестовая выборка открывается ровно один раз
Если вы посмотрели на тестовую метрику, изменили модель, а затем посмотрели на тест снова — тестовая выборка испорчена. Не на уровне кода, а на уровне вас самих: вы стали каналом, через который информация из тестовой выборки просочилась в решения о модели.
После двадцати таких итераций тестовая метрика завышена точно так же, как валидационная. Это не теоретическое утверждение — это измеряется количественно, и в ноутбуке это третий экспериментальный сюжет.
Кросс-валидация¶
Одно случайное разбиение данных даёт одно число, а из модуля 1 мы знаем, чего стоит одно число — практически ничего.
K-fold кросс-валидация: данные разбиваются на \(K\) равных частей (фолдов). Модель обучается на \(K-1\) частях и проверяется на оставшейся одной; затем процедура повторяется \(K\) раз, каждый раз с другой частью в роли тестовой. В результате получается \(K\) измерений качества: по ним можно вычислить среднее, оценить разброс и построить доверительный интервал с помощью бутстрэпа из модуля 1.
Вот что здесь важно и о чём часто забывают: разброс результатов между фолдами обычно оказывается больше, чем разница между сравниваемыми моделями. Модель А со средней метрикой 0.85 против модели Б со средней 0.84, если при этом разброс между фолдами составляет 0.03, — это не победа А, а отсутствие достаточных данных для какого-либо вывода. Правило перекрытия доверительных интервалов из модуля 1 применимо и здесь.
Выбор числа фолдов \(K\): обычно пять или десять. Больше — вычислительно дороже и почти без дополнительной пользы. Меньше — обучающая выборка на каждом фолде заметно уменьшается, что снижает качество обучения.
Если гиперпараметры модели подбираются с помощью кросс-валидации и качество финальной модели тоже измеряется с помощью неё же, вы фактически вернулись к ситуации с двумя выборками. Честный вариант — вложенная (nested) кросс-валидация: внешний цикл измеряет итоговое качество, внутренний — подбирает гиперпараметры. Это дорого вычислительно, поэтому на практике чаще просто сохраняют отдельную тестовую выборку.
Утечка данных¶
Утечка — это ситуация, когда в обучающие данные попадает информация, которая в момент реального предсказания была бы недоступна.
Симптом утечки всегда один и тот же: метрика качества внезапно стала значительно лучше ожидаемого. Приятная неожиданность в машинном обучении практически всегда означает ошибку, и первым делом нужно искать именно её, а не радоваться.
Каталог типичных случаев утечки, каждый из которых встречается на практике регулярно:
Предобработка до разбиения данных. Нормализация, отбор признаков, заполнение пропусков, кодирование категориальных переменных — всё это вычислено по полному набору данных до разбиения на обучающую и тестовую выборки. Статистики тестовой выборки (среднее, стандартное отклонение) оказались «вмонтированы» в обучающие данные через параметры масштабирования.
Дубликаты. Один и тот же объект оказался и в обучающей, и в тестовой выборке. На пользовательских данных это обычное дело: один и тот же человек мог оставить несколько записей.
Признак из будущего. «Дата закрытия сделки» используется как признак в задаче предсказания того, закроется ли сделка. Универсальная проверка: был бы этот признак известен в тот момент, когда предсказание реально потребовалось бы?
Признак-следствие. Формально это не «информация из будущего», но признак порождён самой целевой переменной. Пример: число обращений в службу поддержки как признак для предсказания оттока. Клиенты обращаются в поддержку именно потому, что уже принимают решение об уходе.
Групповая утечка. Записи одного и того же пациента, одного магазина или одной пользовательской сессии случайно распределились между обучающей и тестовой выборками. Модель выучивает идентичность группы, а не содержательную закономерность. Лечится группировкой при разбиении: все записи одной группы попадают целиком в одну выборку.
Временные ряды¶
Это отдельный важный случай, в котором стандартное случайное разбиение данных принципиально некорректно.
Если случайно перемешать данные во времени, модель будет обучаться на будущих данных и проверяться на прошлых. Полученная метрика будет выглядеть превосходно — и не будет означать ровным счётом ничего.
Правильный подход — разбиение строго по времени: обучение происходит на прошлых данных, проверка — на будущих, и «окно» постепенно сдвигается вперёд. Обучились на данных января–марта, проверили на апреле. Затем обучились на данных января–апреля, проверили на мае. И так далее.
Цена ошибки поддаётся измерению. В ноутбуке на временном ряде, где зависимость прошлого с будущим медленно дрейфует со временем — а именно так ведут себя спрос, цены и пользовательское поведение, — случайное разбиение показывает ошибку в два с половиной раза меньше настоящей.
Тот же самый принцип распространяется на любую задачу, где данные поступают последовательно во времени, — а это подавляющее большинство задач, которые действительно внедряются в продуктовую среду.
Журнал экспериментальных прогонов¶
Последний сюжет модуля, и он про то, что будет через полгода.
Число в отчёте живёт значительно дольше, чем память о том, каким образом оно было получено. Через полгода на вопрос «почему здесь написано 0.87» невозможно ответить, если процедура получения этого числа нигде не записана.
Минимальный набор информации, который необходимо фиксировать по каждому прогону:
- Что за модель и все её параметры (гиперпараметры),
- Зерно генератора случайных чисел (или все зёрна, если их несколько),
- Какие данные использовались — версия набора данных, а не просто имя файла,
- Git-коммит кода, на котором проводился эксперимент,
- Версии ключевых библиотек,
- Значения метрик на валидационной и на тестовой выборке (если тест открывался),
- Дата и время прогона.
Почему в списке указаны версии библиотек
Ноутбук к модулю 5 этого курса вычислял площадь под кривой с помощью функции np.trapz. На машине автора стоял NumPy версии 1.26, и всё работало. В NumPy 2.0 эту функцию убрали из публичного API, и первый же запуск в системе непрерывной интеграции упал с ошибкой AttributeError.
Код не менялся, данные не менялись, зерно генератора то же самое. Прогон сломала одна строчка в чужом списке изменений. Без записи версий библиотек расследование такой проблемы начинается с чистого листа.
Исправление, кстати, оказалось лучше оригинала: площадь теперь вычисляется двумя строками напрямую, и вопрос о названии конкретной функции больше не возникает ни в какой версии NumPy.
Конкретный формат журнала вторичен. Простой CSV-файл работает прекрасно; для больших проектов существуют специализированные инструменты — в частности, mlango, где прогон открывается одной командой и записывается вместе с коммитом и артефактами. Но CSV-файл, который вы действительно ведёте, всегда лучше специализированного инструмента, который вы установили и забыли.
Критерий качества журнала ровно один: можно ли по записи воспроизвести указанное число. Если нет — журнал не выполняет своей функции.
Практическая часть¶
Часть 1. Работа с ноутбуком¶
Откройте notebooks/07-honest-comparison.ipynb.
Что содержится внутри:
- Выбор лучшей из двадцати моделей по валидации: количественное измерение того, насколько завышен результат победителя.
- K-fold кросс-валидация вручную. Сравнение разброса между фолдами с разницей между моделями.
- Подглядывание в тестовую выборку: шестьдесят итераций «улучшений» — измеряем, как деградирует реальное качество при неизменно растущей тестовой метрике.
- Утечка через нормализацию до разбиения: количественная оценка того, сколько процентных пунктов качества она «дарит».
- Групповая утечка на данных с повторяющимися пользователями.
- Временной ряд: случайное разбиение против разбиения по времени — на одних и тех же данных.
- Журнал прогонов в двадцать строк кода.
Часть 2. Аудит собственной задачи¶
Возьмите любую свою задачу, на которой уже обучена модель.
- Пройдитесь по каталогу утечек и по каждому пункту напишите «нет, потому что…». Именно письменно: на словах всегда всё выглядит «в порядке».
- Подсчитайте, сколько раз вы заглядывали в тестовую метрику в процессе работы. Честно.
- Пересчитайте качество модели с помощью кросс-валидации и постройте доверительный интервал.
- Сравните полученный результат с числом, которое вы раньше считали итоговым.
Четвёртый пункт иногда бывает неприятным. Именно в этом и заключается практическая польза модуля.
Задание¶
- Реализуйте K-fold с группировкой: объекты, принадлежащие одной группе, гарантированно не расходятся по разным фолдам.
- Проведите симуляцию подглядывания: сгенерируйте пятьдесят случайных моделей, выберите лучшую по тестовой метрике. Постройте разрыв между «тестовой» оценкой победителя и его честным качеством.
- Возьмите набор данных, содержащий временные метки. Измерьте качество модели при случайном разбиении и при разбиении по времени. Разница между этими двумя числами и есть цена методологической ошибки.
- Внесите утечку данных намеренно: добавьте признак, слегка коррелированный с целевой переменной через информацию из будущего. Обнаружьте его с помощью пермутационной важности из модуля 6.
- Заведите журнал прогонов и проведите через него все эксперименты этого модуля. Через неделю попробуйте воспроизвести любое число, опираясь только на записи в журнале.
Проверка усвоения¶
- Зачем нужны именно три выборки? Что конкретно завышается, когда их только две?
- Почему тестовая выборка открывается ровно один раз?
- Разброс между фолдами оказался больше, чем разница между моделями. Какой из этого следует вывод?
- Каков единственный надёжный внешний симптом утечки данных?
- Какой вопрос нужно задать про каждый признак, чтобы обнаружить «признак из будущего»?
- Почему случайное разбиение временного ряда даёт бессмысленную метрику?
- Что такое групповая утечка и каким способом она устраняется?
- Какая информация должна быть в записи прогона, чтобы по ней можно было воспроизвести результат?
Что дальше¶
Часть II завершена. Теперь у нас есть модели, метрики и процедура, которая делает сравнение моделей честным. Далее — часть III: нейронные сети, начиная с обратного распространения ошибки, написанного с нуля на NumPy.
Всё, что появится в части III, будет сравниваться с градиентным бустингом из модуля 6 по правилам этого модуля. Иначе это будет не результат, а впечатление.
Приятная неожиданность в машинном обучении почти всегда означает утечку данных. Ищите её, а не радуйтесь.