Как стать автором
Обновить

Формируем тренировочный сэмпл данных при distribution shift

Время на прочтение 6 мин
Количество просмотров 1.5K
Автор оригинала: Max Halford
Дисклеймер: статья является переведенным продуктом автора Max’a Halforda. Перевод не чистый, а адаптивный. Такой, чтобы было понимание на любом рубеже знаний.



«Мои друзья и я недавно прошли квалификацию в финал Data Science Game 2017. Первой частью соревнований был Kaggle с датасетом от компании Deezer(1). Проблема состояла в решении задачи бинарной классификации: нужно было предсказать, собирается ли пользователь перейти к прослушиванию предложенной ему композиции.

Как и другие команды мы извлекли релевантные признаки и обучили XGBoost(2) классификатор. Однако, мы сделали особенную вещь — подвыборку обучающего набора данных, такую, что он (обучающий набор) стал более репрезентативен для тестового набора.»


Одним из базовых требований к процессу обучения для успешной работы машинной модели является одинаковая природа распределений в тренировочном и тестовом наборах данных. Как грубый пример: модель обучена на пользователях 20-тилетнего возраста, а в тестовой выборке пользователи 60+ лет.

Здесь интуитивно естественно, что с возрастом, на котором модель не обучалась, она и не справится. Конечно, такой пример сугубо синтетический, но в реальности для весомых различий достаточно обучить модель на 20+ и попробовать заставить работать на 30+. Результат будет аналогичен.

Так происходит, потому что модели изучают распределения(3) данных. Если распределения признака в обучающем и тестовом наборе совпадают, модель скажет вам «спасибо».

Вставка переводчика: когда я села за перевод у меня возник вопрос: зачем обучение подгонять под тест, ведь, по сути, тест отражает невидимые данные, которые будут поступать модели на вход в продакшене. Потом я проспалась, перечитала, и всё прошло. Фишка в том, что под влиянием факторов ретроспектива может стать нерелевантной для настоящего времени. Об этом далее (пример чуток переадаптирован).

Смещение в распределениях по одной фиче может происходить по разным причинам. Наиболее интуитивный пример можно позаимствовать у Facebook.

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

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

Таким образом, искажение (distribution shift) возникает, когда распределение ретроспективных данных становится неактуальными для предсказания новых.

В датасете компании Deezer несоответствие распределений было в признаке, измеряющем количество прослушанных песен до момента решения задачи предсказания. И в общучающем, и в тестовом наборах данных данная фича имела экспоненциальное(4) распределение. Однако в тестовом наборе данных оно было более выражено, поэтому среднее в тренировочном сете было ниже, чем в тестовом. После ресемплинга тренировочного распределения удалось повысить метрику ROC-AUC(5) и подняться по рейтинга примерно на 20 пунктов.

Ниже приведен пример разницы распределений:

import numpy as np
import plotly.figure_factory as ff

train = np.random.exponential(2, size=100000)
test = np.random.exponential(1, size=10000)

distplot = ff.create_distplot([train, test], ['Train', 'Test'], bin_size=0.5)
distplot.update_layout(title_text='Распределения Test, Train')

""

Идея нивелирования distribution shift состоит в том, чтобы переформировать обучающий сэмпл так, чтобы он отражал тестовое распределение.

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

Сделать так, чтобы объекты, чаще встречающиеся в тестовом наборе данных также часто встречались и в обучающем! Но как определить, какие объекты нужны более, а какие менее часто?

Весами!

Шаги действий будут примерно такие:

  • поделить числовую прямую распределения на равные интервалы (или корзины (bins)
  • посчитать количество объектов в каждой корзине (bin size)
  • для каждого наблюдения в корзине рассчитать его вес равный 1/(bin size)
  • создать подвыборку k с распределением, учитывающим веса (объекты с более высоким весом станут появляться в подвыборке чаще)

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

SAMPLE_SIZE = 50000
N_BINS = 300

# Задаем частоту корзин, иначе говоря получаем перцентили распределения тестового массива.
# Каждое значение полученное значение будет границей интервала корзины
step = 100 / N_BINS

test_percentiles = [
    np.percentile(test, q, axis=0)
    for q in np.arange(start=step, stop=100, step=step)
]

# Присваиваем каждой коризне набор объектов. 
# Функция возвращает индекс корзины, в которое входит значение
train_bins = np.digitize(train, test_percentiles)

# Считаем количество объектов в каждой корзине или сколько индекс i из входного массива,
# где 0 будет соответствовать количеству вхождений индекса нуль, 1 количеству вхождений индекса 1 и так до i индекса
train_bin_counts = np.bincount(train_bins)

# Считаем веса каждого объекта, единица делится на количество вхождений объектов в корзине
weights = 1 / np.array([train_bin_counts[x] for x in train_bins])

# Нормируем веса так, чтобы их сумма равнялась единице
weights_norm = weights / np.sum(weights)

np.random.seed(0)

sample = np.random.choice(train, size=SAMPLE_SIZE, p=weights_norm, replace=False)

distplot_with_sample = ff.create_distplot([train, test, sample], ['Train', 'Test', 'New train'], bin_size=0.5)
distplot_with_sample.update_layout(title_text='Распределения Test, Train, New train')

""

Новое распределение (зеленое) теперь стало лучше соответствовать распределению тестовой выборки (оранжевое). Аналогичные действия были использованы нами на соревновании — исходный датасет содержал 3 миллиона строк, размер новой выборки мы сгенерировали из 1.3 миллиона объектов. Данных стало меньше, но репрезентативность распределения улучшила качество обучения.

Несколько примечаний из личного опыта автора:

  • Количество корзин не играет большой роли, но чем меньше корзин, тем быстрее учится алгоритм (попробуйте изменить в примере количество корзин (N_BINS) на 3, 30 и вы увидите, что разница действительно невелика)
  • Что касается размера, то чем меньше разница между обучающим и тестовым распределениями, тем меньших размеров обучающей новой выборки достаточно, чтобы “повторить” тестовое распределение, по крайней мере если брать объекты без возвращения, избегая формирования дубликатов.
    (В нашем примере мы перераспределили частотность, за счет чего корзины более “набитые” объектами стали содержать больше значений, относительно менее “набитых” корзин. Это подняло распределение тк концентрация значений сменилась. На мой взгляд достаточность обучающего набора должна исходить из размера тестового сета, но это только примечание переводчика)

Алгоритм переформирования есть на гитхабе автора статьи (папка xam). В будущем автор планирует разбирать новые темы и делиться ими в блоге.

Надеюсь перевод и примечания были полезны и понятны. Жду вашей обратной связи в конструктивном формате. Спасибо за уделенное статье время.

Сноски:

1. Deezer — французский интернет-сервис потоковой передачи музыки. Типа Spotify, Я.Музыки и ну вы поняли

2. XGBoost — алгоритм экстремального градиентного бустинга. Мне крайне понравилась его обзывка как “градиентный бустинг на стероидах”. Идея бустинга заключается в обучении нескольких однородных слабых учеников, каждый из которых формирует оценки на основе ретроспективного опыта обучения предыдущего, обращая внимание на те классы, где прошлый алгоритм больше всего споткнулся. Идея градиента заключается, простым словом, в минимизации ошибки обучения. XGBoost, как алгоритм, является более вычислительно выгодной конфигурацией градиентного бустинга (Gradient Boosting)

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

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

5. ROC-AUC (Area Under receiver operating characteristic Curve) — площадь под кривой “характеристики обработки приемника” — дословное название, поскольку метрика притопала из теории обработки сигналов. ROC-кривая очень крута — она демонстрирует соотношение True Positive и False Positive ответов модели по мере смены порога вероятности для отнесения к какому-либо классу, образуя “дугу”. Благодаря тому, что видно соотношение TP и FP можно подбирать оптимальный порог вероятностей в зависимости от ошибок 1го и 2го рода.

В случае рассмотрения именно точности модели, не внимания вероятностный порог ответов, используется метрика ROC-AUC, принимающая значения в диапазоне [0,1]. Для константной модели при балансе классов ROC-AUC будет приблизительно равна 0,5, следовательно модели ниже — не проходят sanity check (проверку на вменяемость). Чем ближе площадь под ROC кривой к единице тем лучше, но для индексации полезности результатов в целом, AUC-ROC обученной модели релевантно сравнивать с AUC-ROC константной модели.
Теги:
Хабы:
+3
Комментарии 0
Комментарии Комментировать

Публикации

Истории

Работа

Python разработчик
136 вакансий
Data Scientist
66 вакансий

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн