RecSys · учебник
Тренажёр Виджеты Повторение О проекте Все главы ← Взаимодействия История →

Часть III · Ранжирование · глава 13 из 19

Мультизадачность, дебиасинг и дистилляция

Три сюжета, и каждый чинит свою поломку последнего слоя ранжирования: целей на самом деле больше одной, данные смещены позицией показа, а модель, которая лучше всех, не помещается в бюджет. Объединяет их то, что все три — про расхождение между тем, чему модель учится, и тем, для чего её применяют.

Что унести из главы
  • Общий ствол — это компромисс, у которого есть цена. Если задачам нужны представления под углом 90°, каждая получает 0.71 сигнала; при 180° обе не получают ничего.
  • MMoE стоит долю процента параметров. Гейты — 1.54% от модели; всё остальное — просто ёмкость экспертов.
  • Голову CVR нельзя применять там, где она не училась. Она учится на 5% данных и применяется ко всем показам — в 20 раз больше объектов. Отсюда вся конструкция ESMM.
  • Метка бинарна, предсказание учителя — нет. При десяти показах шум оценки клика составляет 138% от самой величины: метка почти ничего не говорит о конкретном объекте.

1. Мультизадачность

Ранжирование почти никогда не оптимизирует один сигнал. Классическая раскладка, на примере видеоплатформы:

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

Shared bottom и его беда

Простейшая конструкция: сначала общие слои, затем отдельные головы под каждую задачу. Плюсы очевидны — регуляризация, поскольку задачи «поддерживают» друг друга, перенос знания и экономия ресурсов.

Shared bottom вход общий ствол задача A задача B конфликт тянет ствол в разные стороны MMoE вход эксперт 1 эксперт 2 эксперт 3 задача A + свой гейт задача B + свой гейт у каждой задачи свои веса экспертов — несвязанные задачи выучивают свои подмножества
Разница не в числе параметров, а в том, кто решает, какая часть модели работает на какую задачу.
Сколько стоит конфликт

Простая модель, показывающая суть. Пусть задачам нужны представления, направления которых расходятся на угол \(\alpha\). Общий ствол способен выдать только одно направление, оптимум по сумме лежит на биссектрисе, и каждая задача получает \(\cos(\alpha/2)\) от своего сигнала:

Угол \(\alpha\)Доля сигнала каждой задачеПотеря против отдельной модели
1.00000.0%
30°0.96593.4%
60°0.866013.4%
90°0.707129.3%
120°0.500050.0%
180°0.0000100.0%

Числа воспроизводятся скриптом _tools/mtl_demo.py в этом репозитории.

Модель нарочно упрощённая, но она объясняет два наблюдения сразу. Первое: при небольшом расхождении общий ствол почти бесплатен — при 30° потеря 3.4%, и мультизадачность выглядит чистым выигрышем. Второе: деградация нелинейна, и на конфликтующих задачах обе проседают разом.

Отсюда же лечение «в лоб», которое действительно работает: просто увеличить ёмкость ствола, чтобы места хватило обоим направлениям. Дорого, но честно.

Mixture of Experts и MMoE

От MoE к MMoE

Исходная конструкция:

$$ y = \sum_{i=1}^{K} g_i(x)\, f_i(x) $$

где \(f_i\) — эксперты, а \(g\) — гейт, задающий веса. Ключевая идея — условные вычисления: на каждом объекте работает только часть экспертов.

MMoE заменяет общий ствол смесью экспертов, но главное в аббревиатуре — первая буква: multi-gate, у каждой задачи свой гейт. Если задачи не связаны, под каждую выучивается своё подмножество экспертов, и конфликт из предыдущего раздела просто не возникает.

Во что обходится идея

Вход 1024, представление 256, четыре задачи. Shared bottom — один ствол на 2.62e+05 параметров.

ЭкспертовПараметры экспертовГейтыВсегоК shared bottom
25.24e+058.19e+035.32e+05×2.0
41.05e+061.64e+041.06e+06×4.1
82.10e+063.28e+042.13e+06×8.1

Числа воспроизводятся скриптом _tools/mtl_demo.py.

Обратите внимание на пропорцию: при четырёх экспертах гейты — это 1.54% параметров модели. То есть собственно идея MMoE стоит полтора процента, а платим мы за экспертов, то есть просто за ёмкость.

Отсюда правильная постановка вопроса при сравнении: MMoE надо сравнивать не с узким shared bottom, а с расширенным до той же ёмкости. Если прирост остаётся — работает механизм разделения задач; если исчезает — работала просто ёмкость.

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

2. ESMM: когда голова применяется не там, где училась

Отдельный и очень характерный сюжет. Пусть мы хотим ранжировать выдачу по покупкам. Есть две очевидные попытки, и обе неудачны — а разбор почему и есть содержание раздела.

Устройство воронки

Возьмём типичные цифры: 108 показов, CTR 5%, конверсия из клика в покупку 3%.

  • кликов: 5.0e+06, покупок: 1.5e+05;
  • \(P(\text{покупка} \mid \text{показ}) = 0.15\%\);
  • дисбаланс классов при обучении напрямую: 1 к 666.

Числа воспроизводятся скриптом _tools/mtl_demo.py.

Две неудачные попытки

Попытка 1: учить \(P(\text{покупка} \mid \text{показ})\) напрямую. Не выходит по двум причинам. Разреженность — позитивов крайне мало, 1 к 666. И сложность сигнала: о покупке мы знаем слишком мало, чтобы её предсказывать, и вся неизвестная нам часть превращается в шум.

Попытка 2: моделировать \(P(\text{покупка} \mid \text{клик})\). Позитивов относительно больше — 3% против 0.15%, и обучение идёт заметно лучше. Но ранжировать по такой модели нельзя, и вот почему.

Голова CVR учится на 5.0e+06 кликах — это 5% данных, а применяется ко всем 108 показам, то есть к в 20 раз большему числу объектов. Мы ранжируем показы так, будто все они уже кликнуты. Это несоответствие срезов обучения и применения — и его стоит уметь называть именно так.

Чем это оборачивается в выдаче
АйтемpCTRpCVRpCTR · pCVR
A0.0100.4000.00400
B0.0800.0900.00720
C0.0400.1500.00600

Порядок по pCVR: A > C > B. Порядок по произведению: B > C > A — полностью обратный.

Наверх по pCVR встаёт A с ожидаемой покупкой 0.00400 вместо B с 0.00720 — потеря 44% на верхней позиции.

Числа воспроизводятся скриптом _tools/mtl_demo.py.

Айтем A — это нишевый товар: кликают редко, но кто кликнул, тот покупает. По условной вероятности он чемпион, по безусловной — аутсайдер.

Решение: моделируем всю цепочку
$$ \text{показ} \;\to\; \text{клик} \;\to\; \text{покупка} $$ $$ P(\text{покупка} \mid \text{показ}) = P(\text{клик}\mid\text{показ}) \cdot P(\text{покупка}\mid\text{клик}) $$

Две головы, CTR и CVR, и произведение как итоговый скор. Но заметьте: вторая голова всё ещё применяется не на том срезе, на котором учится. Декомпозиция сама по себе проблему не решает.

Поэтому ключевой приём ESMM в другом: вторую голову учат на лосс по \(P(\text{покупка}\mid\text{показ})\), то есть на всём пространстве показов, а не только на кликах. Градиент до головы CVR доходит через произведение, и она обучается на том же срезе, на котором потом работает.

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

3. Позиционный дебиасинг

На ранних позициях выдачи CTR выше — по двум разным причинам, и первое, что надо сделать, это их различить.

Смешать их — значит либо «вычесть» вместе со смещением настоящую релевантность, либо не вычесть ничего.

Как смещение переворачивает порядок

Пусть вероятность просмотра позиции убывает как \(1/\text{pos}^{0.7}\):

ПозицияВероятность просмотраВес IPW = \(1/e\)
11.00001.00
20.61561.62
50.32413.09
100.19955.01
200.12288.14

Теперь два айтема. У X истинная релевантность 0.12, но показали его на позиции 5. У Y релевантность 0.10, показали на позиции 1.

  • Наблюдаемый CTR: X — 0.0389, Y — 0.1000.
  • Наивная модель уверенно ставит выше Y, хотя на самом деле лучше X.
  • После деления на вероятность просмотра: X — 0.1200, Y — 0.1000. Порядок восстановлен.

Числа воспроизводятся скриптом _tools/mtl_demo.py.

Отсюда же два следствия, о которых спрашивают. False negatives: часть некликнутых айтемов на самом деле хороши, их просто не увидели. И петля обратной связи: наверху то, что нравится системе, из-за позиционного смещения оно чаще получает положительный фидбек и подтверждает само себя.

СпособКак работаетЦена
Учить только на первой позицииберём \(P(\text{клик}\mid\text{показ}, pos=1)\)при слейте в 20 позиций остаётся 5% данных
Позиция как признакдобавляем позицию в модель, на инференсе подставляем \(pos = 1\)просто, но модель может «спрятать» релевантность внутрь позиционного признака
Inverse propensity weightingперевзвешиваем сэмплы обратно вероятности просмотранужны истинные вероятности, а для этого — случайный трафик; и разброс весов в 8.1 раза раздувает дисперсию
Bias-башняотдельная маленькая башня, моделирующая смещениетребует аккуратной настройки, но это продовый стандарт
Shallow bias tower: почему это красиво

Устройство простое, и в нём три существенные детали.

  1. На входе башни — вся контекстная информация, полезная для моделирования смещения. Не только позиция: например, устройство сильно влияет на позиционный эффект, потому что на телефоне видно меньше позиций, чем на десктопе.
  2. При обучении выходы складываются: \(\text{logit} = f_{\text{main}}(u,c,i) + f_{\text{bias}}(pos, \text{device}, \dots)\). И обязательно дропаут выхода башни — иначе она объяснит позицией слишком многое и перетянет на себя сигнал релевантности, а основная модель недоучится.
  3. На инференсе башню просто выкидывают — остаётся чистый скор релевантности.

Идея хороша тем, что смещение моделируется явно и отделяемо. Мы не пытаемся вычесть его постфактум из готового скора, а даём модели отдельный канал, куда его удобно списать, — и потом отрезаем этот канал.

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

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

Проверять удобно виджетом калибровки в тренажёре; аргументация — в главе 10.

4. Дистилляция знаний

Мотивация чисто инженерная: размен качества на задержку. Модель больше — качество выше, но у ранкера жёсткий потолок в десятки миллисекунд на сотни кандидатов. Дистилляция разрывает эту связь: большая модель-учитель обучается офлайн, а маленький ученик учится воспроизводить её выход и работает в рантайме.

Почему предсказания учителя полезнее меток

Момент, который на собеседовании звучит парадоксально: у нас же есть настоящие метки, зачем учиться на чужие предсказания? Причин две, и обе стоит уметь развернуть.

Причина 1: метка бинарна, предсказание — нет

Метка говорит «клика не было». Учитель говорит «вероятность клика была 0.40» или «0.02» — и это совершенно разные ситуации, которые метка не различает вовсе.

Такой сигнал называют dark knowledge: учитель передаёт не ответ, а форму распределения — в том числе информацию о том, насколько задача была трудной.

Причина 2: учитель — сглаженная версия данных

Пусть истинная вероятность клика 0.05. Насколько точно её оценивает сама метка, если объект показали \(n\) раз?

Показов у объектаСтандартная ошибкаОтносительный шум
10.2179435.9%
100.0689137.8%
1000.021843.6%
10000.006913.8%
100000.00224.4%

Числа воспроизводятся скриптом _tools/mtl_demo.py.

При десяти показах шум оценки — 138% от самой величины. Метка почти ничего не говорит о конкретном объекте, только о его классе. А мы уже знаем, что у большинства каталога показов именно столько.

Учитель усреднил этот шум по всей обучающей выборке и отдаёт ученику сглаженную цель. А шум — главный источник переобучения на хвосте, как в главе 8.

Почему у ученика две головы

При прямой дистилляции ученик учится сразу на две цели — настоящие метки и предсказания учителя — с весом между ними. Это простейший вариант, и в онлайн-ранжировании он приносит специфическую проблему.

Auxiliary distillation

Приём: у ученика делают две головы. Одна учится на целевые метки, другая — на предсказания учителя. Для ранжирования используется голова с целевыми метками.

Зачем так сложно? Потому что у учителя есть свои смещения, и мы не хотим, чтобы они влияли на ранжирование. Учитель обучался на логах той же системы и унаследовал её позиционное смещение, её перекос к популярному, её петлю обратной связи — всё, о чём был предыдущий раздел. Дистиллируя его напрямую, мы дистиллируем и это.

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

Второй мотив — калибровка. Предсказания учителя её ломают, а голова, обученная на настоящих метках, сохраняет. Для рекламы, где скор умножается на ставку, это решающий аргумент.

Четыре способа сломать дистилляцию в онлайне
  1. Учитель дрейфует. Его переучивают, и цель ученика меняется скачком. Ученик, обученный на старого учителя, начинает противоречить новому — а мониторинг этого не покажет, потому что обе модели по отдельности выглядят нормально.
  2. Учителя надо чем-то считать. Он не обязан работать в рантайме, но обязан пройти по всему обучающему потоку. Дистилляция не бесплатна — она переносит стоимость из инференса в обучение.
  3. Ученик наследует потолок учителя. Если учитель ошибается систематически, ученик воспроизведёт ошибку и никогда не превзойдёт его на этом подмножестве — хотя на настоящих метках мог бы.
  4. Замкнутый круг версий. Учитель обучен на логах, собранных предыдущим учеником. Через несколько итераций система дистиллирует саму себя — та же петля обратной связи, только на уровне моделей.

Вопросы с собеседований

Зачем ранжирующей модели несколько задач и в чём беда shared bottom?

Затем, что сигналов всегда несколько: вовлечённость (клики, время), удовлетворённость (лайки, оценки), долгосрочные метрики. Отдельная модель на каждый — в разы больше ресурсов и сложнее поддержка.

Shared bottom даёт регуляризацию, перенос знания и экономию. Беда возникает на конфликтующих задачах: градиенты голов тянут общее представление в разные стороны. В простой геометрической модели, где направления задач расходятся на угол α, каждая получает cos(α/2): при 90° это 0.71, потеря 29%, при 180° — ноль.

Лечение «в лоб» — расширить ствол, чтобы места хватило обоим направлениям. Аккуратнее — MMoE.

Что такое MMoE и чем он отличается от MoE?

MoE: \(y = \sum_i g_i(x) f_i(x)\), где \(f_i\) — эксперты, \(g\) — гейт; ключевая идея — условные вычисления, на объекте работает часть экспертов.

MMoE — multi-gate: у каждой задачи свой гейт, то есть свои веса экспертов. Если задачи не связаны, под каждую выучивается своё подмножество экспертов, и конфликт не возникает.

Важное замечание для честного сравнения: гейты — это порядка полутора процентов параметров, всё остальное просто ёмкость экспертов. Поэтому MMoE надо сравнивать не с узким shared bottom, а с расширенным до той же ёмкости — иначе непонятно, что дало прирост.

Что такое ESMM и какую проблему он решает?

Проблема — несоответствие срезов обучения и применения. Учить P(покупка | показ) напрямую мешает разреженность: при CTR 5% и CVR 3% дисбаланс 1 к 666, плюс сигнал слишком сложный. Учить P(покупка | клик) проще, но такая голова обучается на 5% данных (только клики) и применяется ко всем показам — в 20 раз большему числу объектов.

Почему это ломает ранжирование: pCVR — вероятность при условии клика. Нишевый товар с pCTR 0.01 и pCVR 0.40 по условной вероятности чемпион, а по ожидаемой покупке 0.004 против 0.0072 у популярного — потеря 44% на верхней позиции.

ESMM: моделируем цепочку показ → клик → покупка, P(покупка|показ) = P(клик|показ)·P(покупка|клик), две головы. И ключевой приём — вторую голову учат на лосс по P(покупка|показ), то есть на всём пространстве показов; градиент доходит до неё через произведение.

Почему CTR выше на первых позициях и что с этим делать?

По двум разным причинам, и их надо различать: пользователи чаще кликают наверх, потому что видят раньше и больше доверяют (это смещение), и наверху действительно стоят более релевантные айтемы (это работа модели). Смешать их — значит вычесть вместе со смещением настоящую релевантность.

Конкретно смещение переворачивает порядок: айтем с релевантностью 0.12 на позиции 5 даёт наблюдаемый CTR 0.039, а айтем с релевантностью 0.10 на позиции 1 — 0.100. Наивная модель уверенно ставит выше худший.

Четыре лечения: учить только на первой позиции (при слейте 20 остаётся 5% данных), позиция как признак с подстановкой pos=1 на инференсе (модель может спрятать релевантность в позиционный признак), IPW (нужен случайный трафик, а разброс весов в 8 раз раздувает дисперсию) и bias-башня — продовый стандарт.

Как устроена bias-башня?

Отдельная маленькая башня, на вход которой идёт вся контекстная информация, полезная для моделирования смещения, — не только позиция, но и, например, устройство: на телефоне видно меньше позиций, чем на десктопе.

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

Красота идеи в том, что смещение моделируется явно и отделяемо: мы не вычитаем его постфактум, а даём отдельный канал, куда его удобно списать, и потом отрезаем этот канал.

Зачем учиться на предсказания учителя, если есть настоящие метки?

Две причины. Первая: метка бинарна, предсказание — нет. «Клика не было» одинаково выглядит для объекта с вероятностью 0.40 и с вероятностью 0.02, а учитель эти случаи различает. Это dark knowledge — передаётся не ответ, а форма распределения.

Вторая: учитель — сглаженная версия данных. При истинной вероятности клика 0.05 и десяти показах стандартная ошибка оценки 0.069, то есть 138% от самой величины: метка почти ничего не говорит о конкретном объекте. Учитель усреднил шум по всей выборке, и ученик получает менее шумную цель — а шум и есть главный источник переобучения на хвосте.

Мотивация всего приёма — размен качества на задержку: учитель считается офлайн, ученик влезает в бюджет ранкера.

Почему у ученика делают две головы?

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

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

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

Где дистилляция ломается в проде?

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

Шпаргалка одним экраном

Shared bottom

Регуляризация и экономия, но при расхождении задач на 90° каждая получает 0.71 сигнала.

MMoE

Свой гейт у каждой задачи. Гейты — 1.54% параметров; сравнивать надо при равной ёмкости.

Воронка

CTR 5%, CVR 3% → 0.15% покупок, дисбаланс 1 к 666.

ESMM

P(buy|imp)=P(clk|imp)·P(buy|clk), и CVR-голову учить на всём пространстве показов.

Две причины

Наверху выше CTR и из-за смещения, и из-за релевантности. Смешать — потерять сигнал.

Bias-башня

Контекст на входе, сумма логитов, дропаут выхода, на инференсе выкинуть.

Dark knowledge

При 10 показах шум метки 138%. Учитель отдаёт сглаженную цель, метка — только класс.

Две головы

Ранжируем головой на метках; дистилляционная — вспомогательная. Смещения остаются в чужой голове.

Первоисточники