Содержание курса 02 / 27

02 / 27 · Постановка задачи обучения

Данные, признаки и утечки

От сырых столбцов к матрице «объекты–признаки»: типы признаков, one-hot и целевое кодирование категорий и почему целевое кодирование даёт утечку, если не вычислять его вне фолда, масштабирование и почему оно нужно расстояниям и градиентному спуску, пропущенные значения, конструирование признаков и два способа обмануть оценку качества: утечка целевой переменной, когда признак несёт ответ, и загрязнение выборки, когда шаг предобработки видел данные для оценки.

На этой странице 13

Вводная заметка считала каждый объект вектором x∈Rdx \in \mathbb R^d, а каждую обучающую строку — независимым наблюдением из того распределения, с которым встретится модель. Настоящие данные в исходном виде не удовлетворяют ни одному из этих предположений. В таблице кредитных заявок есть название района, дата, доход, который часть заявителей не указала, цель кредита свободным текстом и столбцы, заполненные через несколько месяцев после решения по заявке. Эта заметка превращает такие столбцы в числа, которыми может пользоваться модель, и показывает два способа, которыми информация о целевой переменной просачивается в признаки и выдаёт бесполезную модель за отличную.

Обозначения. В матрице «объекты–признаки» (object–feature matrix) X∈Rn×dX \in \mathbb R^{n \times d} одна строка на объект и один столбец на признак; xijx_{ij} — признак jj объекта ii, y=(y1,…,yn)y = (y_1, \dots, y_n) — значения целевой переменной. Категориальный признак принимает значения cc из конечного множества CC категорий; ncn_c — число обучающих строк в категории cc, yˉc\bar y_c — их среднее значение целевой переменной; yˉ\bar y — общее среднее.

Какие бывают признаки#

ТипПримерЧто нужно модели
Числовойдоход, площадь, число покупокчасто масштаб и преобразование против асимметрии
Бинарныйесть ли автомобильничего: 0 или 1
Категориальный (номинальный)район, товар, браузеркодирование; у категорий нет порядка
Порядковыйуровень образования, оценка 1–5порядок, но не обязательно равные шаги
Дата и времядата заявкичасти (час, день недели, месяц) и разности («дней с момента»)
Текст, множествацель кредита, тегивектор, например счётчики слов

Тип определяет, какая арифметика имеет смысл. Среднее двух районов — не район, а кодирование районов числами 1, 2, 3 сообщает линейной модели, что район 2 лежит посередине между 1 и 3. Текст превращается в вектор счётчиков, мешок слов (bag of words); курс NLP строит его в заметках о тексте как данных и о мешке слов, и получающаяся матрица — это матрица «объекты–признаки» с десятками тысяч разреженных столбцов.

Кодирование категорий#

One-hot кодирование (one-hot encoding) заменяет признак с CC категориями на CC бинарных столбцов, по одному на категорию, с единственной единицей в каждой строке. Тогда линейная модель выучивает по одному весу на категорию — ровно ту свободу, которая должна быть у номинального признака. Если в модели есть свободный член, CC столбцов в сумме всегда дают 1 и дублируют его, поэтому один столбец обычно отбрасывают. Без штрафа это нужно для единственности решения: CC столбцов и свободный член линейно зависимы, и бесконечно много векторов весов дают одни и те же прогнозы. Со штрафом ridge или lasso можно оставить все CC столбцов, потому что штраф сам выбирает одно решение; отбрасывание столбца тогда меняет подгонку, ведь остальные веса сжимаются к отброшенной опорной категории, а не к общему значению. One-hot хорошо работает до нескольких сотен категорий; для признака со 100 000 значений (пользователь, магазин, почтовый индекс) матрица становится огромной, а у большинства категорий лишь горстка строк, так что их веса оцениваются почти ни по чему.

Целевое кодирование (target encoding) заменяет каждую категорию одним числом — оценкой среднего значения целевой переменной в этой категории, E[Y∣категория=c]E[Y \mid \text{категория} = c] (Micci-Barreca, 2001). Для редкой категории сырое среднее yˉc\bar y_c шумное, поэтому его стягивают к общему среднему:

g(c)=ncyˉc+a yˉnc+a,g(c) = \frac{n_c \bar y_c + a\, \bar y}{n_c + a} ,

с весом априорного распределения a>0a > 0, который действует как aa дополнительных строк со значением, равным общему среднему. Для бинарной целевой переменной это апостериорное среднее в бета-биномиальной модели с априорным распределением Beta(ayˉ, a(1−yˉ))\mathrm{Beta}(a\bar y,\ a(1 - \bar y)), центрированным в yˉ\bar y. Для числовой целевой переменной та же формула — общая оценка со сжатием (shrinkage); её байесовское прочтение — нормальная модель: нормальное априорное распределение на среднее категории с центром в yˉ\bar y и нормальный шум, где aa — отношение дисперсии шума к априорной дисперсии. В обоих случаях категория с большим числом строк сохраняет своё среднее, редкая остаётся близко к априорному значению. Разобранный пример. Категория из 3 строк, 2 из них положительные, в данных с общей долей положительных 0.2, закодированная с a=10a = 10: g=(2+10⋅0.2)/(3+10)=4/13=0.308g = (2 + 10 \cdot 0.2)/(3 + 10) = 4/13 = 0.308 вместо сырых 2/32/3.

Почему наивное целевое кодирование даёт утечку#

Если yˉc\bar y_c вычислено по обучающим строкам, оно включает целевое значение самой строки. В крайнем случае категория, встретившаяся один раз, кодируется собственной меткой, и модель, читающая кодировку, читает ответ. Prokhorenkova et al. (2018), раздел 3.2, приводят чистый вариант: если каждая категория уникальна, а метка — честная монета, закодированные обучающие строки идеально разделяются одним порогом, тогда как каждая тестовая строка получает априорное значение, и модель выдаёт 0.5. Распределение закодированного признака при заданной метке различается для обучающих и тестовых строк — а именно на таком различии модель обучать нельзя. Исключение только самой строки не помогает: для одной категории, общей для всех строк, среднее с исключением одного объекта (leave-one-out) (∑jyj−yi)/(n−1)(\sum_j y_j - y_i)/(n - 1) принимает одно значение для положительных строк и другое для отрицательных, и снова их разделяет один порог (тот же раздел).

Исправление — кодировать каждую обучающую строку статистиками, вычисленными по другим строкам, как в кросс-валидации. Кодирование вне фолда (out-of-fold encoding) делит обучающую выборку на фолды и кодирует строки каждого фолда средними по остальным фолдам; тестовые строки получают средние по всей обучающей выборке. Восемь строк, две категории, два фолда, без сглаживания:

СтрокаКатегорияyyФолдНаивное gggg вне фолда
1A110.751.0
2A010.751.0
3A120.750.5
4A120.750.5
5B010.250.5
6B120.250.0
7B020.250.0
8B010.250.5

Строки 1 и 2 кодируются по строкам 3 и 4 (обе положительные), строки 5 и 8 — по строкам 6 и 7. Значения вне фолда шумнее: это честная цена того, что мы не смотрим на собственную метку строки, и причина, по которой на практике их сглаживают. CatBoost использует упорядоченный вариант: он перемешивает строки и кодирует каждую по строкам, стоящим перед ней, так что каждая строка закодирована по данным, которые она могла бы увидеть в потоке (Prokhorenkova et al., 2018, раздел 3.2).

На данных, где категория не несёт никакой информации (2000 строк, 500 категорий примерно по 4 строки, случайные метки), логистическая регрессия на наивной кодировке достигает ROC-AUC 0.774 по кросс-валидации; с кодированием вне фолда, обученным внутри каждого обучающего фолда, она получает 0.48 — заслуженный уровень случайного угадывания (раздел «Код»).

Масштабирование#

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

  • Расстояния. Два заявителя с доходами 50 000 и 52 000 и возрастом 25 и 60 лет находятся на евклидовом расстоянии 20002+352=2000.3\sqrt{2000^2 + 35^2} = 2000.3: разница в возрасте почти ничего не добавляет. В наборе данных wine, входящем в scikit-learn, стандартное отклонение содержания пролина равно 314, а нефлавоноидных фенолов — 0.12; классификатор по 5 ближайшим соседям на сырых столбцах имеет долю правильных ответов (accuracy) по кросс-валидации 0.663, а на стандартизованных — 0.961. Заметка о методе ближайших соседей к этому вернётся.
  • Градиентный спуск. Для метода наименьших квадратов с двумя независимыми центрированными признаками со стандартными отклонениями 1 и 100 поверхность потерь — эллипс, оси которого различаются в отношение масштабов, а число обусловленности X⊤XX^\top X около 10410^4; после стандартизации оно равно 1.02. Число шагов, которое нужно градиентному спуску, растёт с числом обусловленности, как показывает заметка о градиентном спуске.
  • Штрафы. Регуляризация штрафует все коэффициенты одинаково, поэтому признак в крупном масштабе (с малым коэффициентом) штрафуется слабее, чем та же информация в мелком масштабе.

Стандартизация вычитает среднее каждого столбца и делит на его стандартное отклонение; min–max-масштабирование переводит каждый столбец в [0,1][0, 1]; робастный вариант использует медиану и межквартильный размах, чтобы масштаб не задавали несколько экстремальных значений. Деревья делят выборку по порогам одного признака за раз, и на них не влияет никакое монотонное преобразование признака. Масштабирование не всегда улучшает результат: когда все признаки уже измеряются в одних единицах, как интенсивности пикселей, стандартизация раздувает шум почти постоянных столбцов. Каким бы ни был метод, его средние и разбросы — статистики данных, и вычислять их нужно только по обучающим строкам; раздел о загрязнении выборки уточняет это.

Пропущенные значения#

Значение может отсутствовать по важным причинам. Rubin (1976) различает значения, пропущенные полностью случайно (missing completely at random; сбой датчика, ни с чем не связанный), пропущенные случайно (missing at random) при заданных наблюдаемых признаках и пропущенные неслучайно (missing not at random), когда пропуск зависит от самого пропущенного значения (люди с высоким доходом отказываются его указывать). В последнем случае сам факт пропуска информативен.

Большинству моделей нужно число в каждой ячейке. Стандартный рецепт заполняет пропуск статистикой обучающего столбца (медианой для асимметричных признаков, самой частой категорией для категориальных) и добавляет бинарный индикатор пропуска (missing indicator). Для линейной модели w0+w1x1+w2[x1 пропущен]+…w_0 + w_1 x_1 + w_2 [x_1 \text{ пропущен}] + \dots вес индикатора сдвигает прогноз для строк с пропуском, так что модель фактически сама выучивает лучшее значение для заполнения, какую бы константу ни поставили в ячейку. Библиотеки градиентного бустинга обрабатывают пропуски сами: отправляют их в ту ветвь разбиения, которая сильнее уменьшает потери.

Конструирование признаков#

Модели находят закономерности в тех признаках, которые им дали; признаки определяют, какие закономерности найти легко. Типичные конструкции на табличных данных:

  • Отношения и разности: долг, делённый на доход, цена квадратного метра, число дней между регистрацией и первой покупкой.
  • Части дат: час суток, день недели, месяц, флаг праздника; и у спроса, и у мошенничества сильные недельные циклы.
  • Преобразования асимметричных сумм: log⁡(1+x)\log(1 + x) для доходов и сумм покупок, чтобы линейная модель видела относительные, а не абсолютные различия.
  • Агрегаты по истории: число покупок за последние 30 дней, средний размер корзины, доля возвращённых заказов. Каждый из них нужно вычислять на момент прогноза, только по событиям до него.
  • Взаимодействия: произведение двух признаков или пересечение двух категорий — для моделей, которые не могут построить их сами. Деревья и нейронные сети строят взаимодействия самостоятельно, и это одна из причин, по которым им нужно меньше ручного конструирования.

Утечка целевой переменной#

Утечка целевой переменной (target leakage) — это признак, несущий информацию о целевой переменной, которая не будет доступна в момент прогноза (Kaufman et al., 2012). Типичные формы:

  • Вычислено после события. Предсказывать дефолт по кредиту по «числу звонков из отдела взыскания» или повторную госпитализацию по коду диагноза, внесённому при этой повторной госпитализации. Признак есть в исторической таблице потому, что исход уже произошёл.
  • Замаскированная копия целевой переменной. «Общая выплаченная сумма» при предсказании того, будет ли кредит выплачен; «заполнена дата закрытия счёта» при предсказании оттока.
  • Агрегаты, включающие будущее. «Средние траты клиента», вычисленные по всей истории, включая месяцы после даты прогноза.
  • Артефакты сбора данных. Строки отсортированы по исходу, так что идентификатор строки предсказывает метку, или положительные примеры собраны из другого источника в другом формате файлов.

Симптом — модель, которая намного лучше, чем кто-либо ожидал, причём большую часть работы делают один-два признака. Лекарство — отметка времени для каждого признака и правило, что признак может использовать только информацию до момента прогноза. Kapoor and Narayanan (2023) нашли утечки в опубликованных работах из 17 научных областей, затрагивающие 294 статьи; в их повторном анализе прогнозирования гражданских войн сложные модели потеряли преимущество перед логистической регрессией, как только утечку устранили.

Загрязнение выборки#

Второму виду утечки плохой признак не нужен. Загрязнение выборки (train–test contamination) происходит, когда любой обучаемый на данных шаг конвейера, а не только модель, видит строки, используемые для оценки:

  • масштабирование или заполнение пропусков, средние для которых вычислены по всем строкам;
  • словарь, взвешивание TF-IDF или модель векторных представлений слов, обученные на всех текстах, — для текстов это обсуждается в заметке об оценке качества в NLP;
  • целевое кодирование, вычисленное по всем строкам;
  • отбор признаков (feature selection) по всем строкам: оставить признаки, сильнее всего коррелирующие с меткой, а затем оценить на них модель кросс-валидацией;
  • дубликаты или почти дубликаты одного объекта в обучающей и оценочной выборках.

Насколько вреден шаг, зависит от того, сколько он узнаёт о целевой переменной. Масштабирование узнаёт только средние и разбросы; на данных wine стандартизация всех строк до кросс-валидации даёт ту же долю правильных ответов, что и стандартизация внутри каждого обучающего фолда, — 0.961 в обоих случаях. Отбор признаков узнаёт, какие признаки согласуются с метками всех строк, включая оценочные, и эффект получается драматическим. Классическая демонстрация использует данные вообще без сигнала (Hastie, Tibshirani and Friedman, 2009, раздел 7.10.2; Ambroise and McLachlan, 2002):

Рисунок

Отбор признаков вне кросс-валидации находит сигнал в чистом шуме

Доля правильных ответов по кросс-валидации на шуме при двух способах отбораКрасная кривая поднимается почти до 1 и возвращается к 0.5 при k = 1000; синяя лежит около 0.5 при всех k.0.40.50.60.70.80.91.01101001000число отобранных признаков k (из 1000)отбор по всем даннымотбор внутри каждого фолдаистинная доля правильных ответов 0.5Доля правильных ответов по кросс-валидации на шуме при двух способах отбораКрасная кривая поднимается почти до 1 и возвращается к 0.5 при k = 1000; синяя лежит около 0.5 при всех k.0.40.50.60.70.80.91.01101001000число отобранных признаков k (из 1000)отбор по всем даннымотбор внутри каждого фолдаистинная доля правильных ответов 0.5
60 наборов по 50 объектов с 1000 случайными признаками, не связанными с меткой. Из признаков берутся k с наибольшей разницей средних по классам, классификатор — ближайший центроид, доля правильных ответов оценивается 5-кратной кросс-валидацией, среднее по наборам. Если признаки выбраны один раз по всем 50 объектам (красная кривая), оценка растёт с 0.68 при k = 1 до 0.99 при k = 200 и падает до 0.50, когда отбора уже нет. Если отбор повторяется внутри каждого обучающего фолда (синяя), оценка остаётся между 0.47 и 0.50: честные 0.5.

При 1000 шумовых признаков и 50 объектах некоторые признаки случайно согласуются с метками, и отбор по всем строкам выбирает именно те случайные совпадения, которые отложенные фолды затем «подтвердят». Утечка растёт с числом отобранных признаков, вплоть до оценённой доли правильных ответов 0.99 при 200 признаках, и исчезает, когда оставлены все 1000, потому что тогда ничего не отбиралось. Повторение отбора внутри каждого обучающего фолда устраняет её полностью: оценка остаётся на истинных 0.5. Раздел «Код» повторяет эксперимент с 10 000 шумовых признаков, 20 лучшими из них и логистической регрессией: 0.87 при отборе по всем строкам, 0.49 при отборе внутри фолдов.

Практическое правило — оформить каждый зависящий от данных шаг как часть модели, конвейер (pipeline), который обучается на обучающей части каждого разбиения и только применяется к оценочной части. В scikit-learn это make_pipeline(StandardScaler(), SelectKBest(...), LogisticRegression()), целиком переданный функции кросс-валидации. То же правило касается и человека, настраивающего модель: выбирать признаки, глядя на результаты на тестовой выборке, — это отбор признаков на тестовой выборке.

Что изменилось#

Сырая таблицаПодготовленные признаки
Категориистроки или произвольные кодыone-hot столбцы или целевое кодирование, вычисленное вне фолда
Масштабыединицы измерениястандартизованы там, где используются расстояния, градиенты или штрафы
Пропускипустые ячейкизаполнены по статистикам обучающей выборки, плюс индикатор
Времястолбец с датойчасти, разности, агрегаты по истории на момент прогноза
Оценка качествалюбое разбиение итоговой таблицыкаждый обучаемый шаг внутри обучающего фолда; признаки ограничены информацией, доступной в момент прогноза

Ограничения#

  • Конструирование признаков зависит от задачи. Никакой рецепт не скажет, какие отношения или агрегаты важны; решают знание предметной области и валидация, и каждый опробованный признак — ещё один шанс переобучиться под валидационную выборку.
  • Утечку нельзя обнаружить по одной таблице. Заполнен ли столбец до или после исхода — факт о процессе, породившем данные, а не о числах.
  • Кодирование вне фолда стоит дисперсии. Оно шумнее наивного и требует сглаживания; при очень редких категориях one-hot с регуляризацией или хеширование может оказаться лучше.
  • Предположения о пропусках непроверяемы. Неслучайные пропуски нельзя отличить от случайных по одним наблюдаемым значениям.

Что нужно запомнить#

  • Тип признака определяет допустимую арифметику; категориям нужно кодирование, датам — части и разности, тексту — вектор.
  • One-hot даёт каждой категории свой вес; целевое кодирование заменяет категорию её сглаженным средним значением целевой переменной ncyˉc+ayˉnc+a\frac{n_c \bar y_c + a \bar y}{n_c + a} — апостериорным средним для бинарной целевой переменной и оценкой со сжатием в общем случае.
  • Наивное целевое кодирование даёт утечку собственной метки строки, и leave-one-out тоже; вычисляйте его вне фолда или в случайном порядке.
  • Масштабируйте признаки для моделей на расстояниях, для градиентного спуска и для штрафов; деревьям это безразлично.
  • Заполняйте пропуски по статистикам обучающей выборки и добавляйте индикатор; сам пропуск может быть информативен.
  • Утечка целевой переменной — признак, недоступный в момент прогноза. Загрязнение выборки — обучаемый шаг, видевший оценочные строки. Оба почти всегда делают оценки оптимистичными; отбор признаков по всем строкам может превратить чистый шум в 99% правильных ответов.
  • Помещайте каждый обучаемый шаг в конвейер, который заново обучается внутри каждого обучающего фолда.

Код#

Сглаженное целевое кодирование и кодирование вне фолда, отбор признаков внутри и вне кросс-валидации, масштабирование. Напечатанные числа получены с NumPy 2.1.3 и scikit-learn 1.9.1. Передать разбиватель в cv у TargetEncoder можно только в scikit-learn 1.9 и новее; в версиях от 1.3 до 1.8 TargetEncoder(cv=5, shuffle=True, random_state=0) делает такое же кодирование вне фолда со стратифицированными внутренними фолдами (его AUC в scikit-learn 1.9.1 равен 0.493):

import numpy as np
from sklearn.datasets import load_wine
from sklearn.feature_selection import SelectKBest, f_classif
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import KFold, StratifiedKFold, cross_val_score
from sklearn.neighbors import KNeighborsClassifier
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler, TargetEncoder

rng = np.random.default_rng(0)
cv = StratifiedKFold(5, shuffle=True, random_state=0)

# smoothed target encoding of a category with 3 objects, 2 positive, global rate 0.2, prior weight 10
print(round((2 + 10 * 0.2) / (3 + 10), 3))                                    # 0.308

# pure noise: 100 objects, 10,000 features, labels independent of everything; keep the 20 "best" features
X = rng.normal(size=(100, 10_000))
y = rng.permutation(np.repeat([0, 1], 50))
X20 = SelectKBest(f_classif, k=20).fit_transform(X, y)                       # selected on all 100 rows: leak
wrong = cross_val_score(LogisticRegression(), X20, y, cv=cv).mean()
right = cross_val_score(make_pipeline(SelectKBest(f_classif, k=20), LogisticRegression()), X, y, cv=cv).mean()
print(round(wrong, 3), round(right, 3))                                       # 0.87 0.49

# target encoding of a noise category with 500 values (about 4 rows each), 2,000 rows, random labels
cat = rng.integers(0, 500, size=(2000, 1))
yb = rng.integers(0, 2, size=2000)
counts, sums = np.bincount(cat[:, 0], minlength=500), np.bincount(cat[:, 0], weights=yb, minlength=500)
naive = (sums / np.maximum(counts, 1))[cat]                                   # mean label of the category, own row included
auc_naive = cross_val_score(LogisticRegression(), naive, yb, cv=cv, scoring='roc_auc').mean()
oof = make_pipeline(TargetEncoder(cv=KFold(5, shuffle=True, random_state=0)), LogisticRegression())   # разбиватель в cv: scikit-learn >= 1.9
auc_oof = cross_val_score(oof, cat, yb, cv=cv, scoring='roc_auc').mean()      # out-of-fold encoding inside each fold
print(round(auc_naive, 3), round(auc_oof, 3))                                 # 0.774 0.48

# scaling: kNN on the wine data, raw, standardized on all rows, standardized inside each training fold
Xw, yw = load_wine(return_X_y=True)
print(Xw.std(axis=0).max().round(2), Xw.std(axis=0).min().round(2))         # 314.02 0.12 (proline, nonflavanoid phenols)
raw = cross_val_score(KNeighborsClassifier(5), Xw, yw, cv=cv).mean()
outside = cross_val_score(KNeighborsClassifier(5), StandardScaler().fit_transform(Xw), yw, cv=cv).mean()
inside = cross_val_score(make_pipeline(StandardScaler(), KNeighborsClassifier(5)), Xw, yw, cv=cv).mean()
print(round(raw, 3), round(outside, 3), round(inside, 3))                     # 0.663 0.961 0.961

# the condition number of X^T X for two independent features on scales 1 and 100, raw and standardized
Z = rng.normal(size=(10_000, 2)) * [1, 100]
Zs = StandardScaler().fit_transform(Z)
print(f'{np.linalg.cond(Z.T @ Z):.2g}', f'{np.linalg.cond(Zs.T @ Zs):.3g}')   # 1e+04 1.02

Дальше#

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

Литература#