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

Часть IV · Последовательности и выдача · глава 14 из 19

Трансформеры над историей

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

Что унести из главы
  • Усреднение растворяет редкое решающее событие. Единственный трек в истории из двенадцати событий получает вес 0.835 вместо 0.083, и скор меняется с −0.56 на +0.83.
  • Раннее связывание стоит ровно столько, сколько кандидатов. При 500 кандидатах энкодер прогоняется 500 раз: 627 мс вместо 1.25, то есть в 31 раз мимо бюджета.
  • Внимание квадратично по длине. История в 8000 событий дороже сотни в 1113 раз — отсюда разделение на офлайн- и реальный контуры.
  • Главный методологический урок: преимущество BERT4Rec над SASRec объяснялось не двунаправленным вниманием, а функцией потерь. Сравнивая работы, первым делом сверяйте лосс и схему негативов.

1. Точка отсчёта: усреднение

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

Что при этом теряется

Возьмём историю из 12 событий: 8 про спорт, 2 про кулинарию, 1 про технику и ровно один музыкальный трек. И четырёх кандидатов, по одному на каждую тему.

КандидатМакс. вес историиРавномерный весСкор с вниманиемСкор со средним
спорт0.1200.083+0.97+0.60
кулинария0.2930.083+0.73+0.35
техно0.6850.083+0.73−0.34
музыка0.8350.083+0.83−0.56

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

Смотрите на последнюю строку. Внимание находит единственное музыкальное событие и даёт ему вес в 10 раз выше равномерного. Скор меняет знак: +0.83 против −0.56.

Усреднение этот сигнал не «недооценило» — оно его уничтожило. Одно событие из двенадцати в среднем неотличимо от шума, и вся информация о том, что человек вообще слушает музыку, пропала на этапе пулинга.

Обратите внимание и на строку «спорт»: там разница между вниманием и средним минимальна. Когда история однородна, усреднение работает нормально — и именно поэтому проблему долго не замечали.

Что здесь надо увидеть
  1. Переключайте кандидата — веса по истории пересчитываются, потому что внимание считается относительно него. Серый пунктир (простое среднее) при этом не двигается вообще: он один и тот же для всех кандидатов.
  2. Выберите кандидата «музыка» — тот самый случай из таблицы выше.
  3. Ползунок τ показывает вторую половину сюжета: внимание вырождается на обоих концах шкалы.

Что сказать на собесе: «Target attention отличается от пулинга тем, что веса зависят от кандидата. Поэтому редкое, но решающее событие истории не растворяется в среднем — на этом и построены DIN, BST и TransAct».

2. DIN: внимание, зависящее от кандидата

Target-aware attention

Пусть \(e_1, \dots, e_H\) — эмбеддинги исторических событий, \(v_A\) — эмбеддинг кандидата. Тогда вектор пользователя:

$$ v_U(A) = \sum_{j=1}^{H} w_j\, e_j, \qquad w_j = g(e_j, v_A) $$

Формально это тот же пулинг событий — но веса перестают быть равномерными и зависят от того, кого мы оцениваем. Отсюда и запись \(v_U(A)\): у пользователя больше нет одного вектора, у него вектор на каждого кандидата.

Деталь, которую любят спрашивать

Авторы DIN не используют softmax-нормализацию attention-скоров. Это выглядит как небрежность, а на самом деле содержательное решение.

Сумма \(\sum_j w_j\) интерпретируется как интенсивность интереса пользователя к кандидату. При softmax-нормализации сумма всегда равна единице — и эта информация теряется: пользователь с десятью релевантными событиями в истории и пользователь с одним получат одинаковую по «массе» свёртку.

Нормализация здесь не бесплатна: она выравнивает то, что как раз хотелось бы различать.

Постановка из статьи как образец: предсказываем вероятность клика на рекламу товара; история — клики пользователя, вектор события — конкатенация обучаемых эмбеддингов товара, магазина и категории; 16 групп признаков с эмбеддингом размерности 12; две недели на обучение, один день на тест, 2 млрд примеров. За DIN последовало целое семейство — Deep Interest Evolution, Adaptive Interest, Multi-Interest Network и так далее.

3. BST и TransAct: порядок и реальное время

Следующая поломка очевидна: пулинг не учитывает порядок событий. «Купил телефон, потом чехол» и «купил чехол, потом телефон» для него одно и то же.

Behavior Sequence Transformer: добавляем позиционные эмбеддинги к векторам событий и ставим трансформер энкодером. Скрытое представление таргет-айтема используется как вектор пользователя.

TransAct — подмодуль ранжирующей модели Pinterest, отвечающий за реальное время. Устройство стоит помнить как образец инженерного решения:

Обратите внимание на предпоследний пункт: раннее связывание даёт выразительность, и за неё придётся платить. Счёт придёт в разделе 6.

4. Температура внимания

Внимание вырождается на обоих концах шкалы

Веса считаются софтмаксом от скалярных произведений, делённых на температуру. Посмотрим на энтропию весов — она измеряет, насколько внимание «размазано». Максимум при 12 событиях: \(\ln 12 = 2.48\).

\(\tau\)Энтропия весовМаксимальный вес
2.002.430.173
1.002.200.317
0.350.660.835
0.100.010.999
0.050.001.000

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

При \(\tau = 2\) энтропия 2.43 почти равна максимуму 2.48 — внимание превратилось в обычное усреднение, ради ухода от которого всё и затевалось. При \(\tau = 0.05\) энтропия ноль и вес одного события равен единице — внимание превратилось в жёсткий выбор одного элемента истории, а вся остальная история выброшена.

Это та же температура и по той же причине, что в двухбашенной модели: скалярные произведения нормированных векторов зажаты в \([-1; 1]\), и без деления софтмакс над ними почти равномерен.

5. SASRec, BERT4Rec и главный урок

Отдельная линия — трансформеры не для ранжирования, а для кандидатогенерации. Перенос из обработки языка прямой: слова = айтемы, предложения = пользователи, задача — предсказание следующего айтема, архитектура — трансформер-декодер с каузальной маской. Это SASRec.

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

Что в постановке SASRec устарело

Формально задача та же, что у двухбашенных кандген-моделей: пары (пользователь, позитив), где пользователь — последовательность прошлых взаимодействий. Но практики использованы более старые, чем мы разбирали:

  • бинарная кросс-энтропия вместо софтмакса;
  • сэмплируется всего один негатив, и тот равномерно.

Это и стало источником многолетней методологической путаницы в литературе.

BERT4Rec применяет двунаправленное внимание вместо каузального. Чтобы учиться на ту же задачу, пришлось бы отказаться от teacher forcing, а это очень медленно, — поэтому используется cloze task: маскируем случайные токены и предсказываем их. Заодно там учились на полный софтмакс без сэмплирования негативов. Результаты вышли лучше, чем у SASRec.

Развязка, ради которой стоит помнить эту историю

Оказалось, что SASRec с правильным лоссом гораздо лучше ванильного — и обгоняет BERT4Rec. То есть исходное преимущество BERT4Rec объяснялось не двунаправленным вниманием, а тем, что он учился на полный софтмакс, тогда как SASRec — на бинарную кросс-энтропию с одним равномерным негативом.

Годы сравнений архитектур сравнивали функции потерь.

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

Каталог 106. Назовём «трудными» 100 айтемов, ближайших к позитиву, — те, различать которые модель и должна учиться. Вероятность, что случайный равномерный негатив окажется трудным: 0.0001.

Негативов на позитивВероятность увидеть хотя бы один трудный
10.0001
1000.0100
10000.0952
100000.6321
1000001.0000

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

Чтобы увидеть трудный негатив хотя бы в половине случаев, нужно 6931 сэмпл вместо одного. Полный софтмакс включает все 106.

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

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

6. Раннее связывание против позднего

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

Счёт за раннее связывание

История 100 событий, размерность 256, два слоя, 500 кандидатов на запрос.

СвязываниеПрогонов энкодераОперацийПри 50 GFLOPS
Позднее16.27e+071.25 мс
Раннее5003.13e+10627 мс

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

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

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

Почему в истории клики, а не показы

Два аргумента, и второй чисто арифметический.

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

Инженерный: при CTR 5% показов в 20 раз больше, а внимание квадратично по длине:

Кликов за периодПоказов за тот же периодВнимание дороже в
1002 000400 раз
50010 000400 раз
200040 000400 раз

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

Переход с кликов на показы стоит 400-кратного роста стоимости внимания — при том, что информации в них меньше. Решение принимается легко.

Два практических приёма из той же линии

Как измерить время на документе. Напрямую его никто не сообщает. Стандартный приём — мерить по возврату в выдачу: если следующее событие пользователя произошло через \(t\) секунд, примерно столько он и провёл на документе. Отсутствие возврата трактуется отдельно. Так строится таргет «глубокий клик» — пребывание дольше \(N\) секунд.

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

7. Длина истории: два контура

Хочется анализировать глубокую историю, но добавить её в модель, работающую на запросе, нельзя.

Квадратичный член берёт своё
Длина историиОперацийДороже, чем при 100 событиях
1006.27e+071.0
5005.18e+088.3
20005.14e+0982.1
80006.97e+101112.7

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

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

Есть два выхода, и на практике их комбинируют.

офлайн-трансформер глубокая история, до 2000 пересчёт раз в сутки вектор трансформер на запросе свежая история контекст CLS-токен скалярное произв. башня айтема скор офлайн: дёшево, тяжёлые модели, длинная история — но без свежих событий и без контекста на запросе: свежесть и контекст — но короткая история и жёсткий бюджет
Контуры не конкурируют: вектор глубокой истории подаётся на вход быстрому трансформеру, который добавляет свежие события и контекст.
Чем платит и что даёт офлайн-контур

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

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

Поэтому офлайн-модель не заменяет контур реального времени, а дополняет его.

8. Куда это идёт

Общий вектор развития читается одной строкой: сумма эмбеддингов → target-aware attention → полноценный трансформер → единая модель над признаками пользователя, контекста, айтема и историей сразу.

НаправлениеЧто делаютОграничение
Единый трансформеристория и признаки в одной модели, разные параметры на признаковые токены и общие на последовательность, каузальная маска, SEP между последовательностями, пирамидальные слоиочень дорог вычислительно
Смешивание вместо вниманияистория как ключи и значения в кросс-внимании, а запросы — признаки; вместо внимания дешёвое перемешивание токеновлучше масштабируется, линия молодая
Кросс-доменные моделиистория из одного сервиса переиспользуется в другомнужна общая инфраструктура идентификаторов
Единая модель поиска и рекомендацийединый каталог, объединённая история, различается только контекст

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

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

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

Чем target attention отличается от усреднения истории?

Тем, что веса зависят от кандидата: \(v_U(A) = \sum_j w_j e_j\), где \(w_j = g(e_j, v_A)\). У пользователя больше нет одного вектора — у него вектор на каждого кандидата.

Зачем: усреднение растворяет редкое, но решающее событие. В истории из 12 событий единственный музыкальный трек получает при усреднении вес 0.083, а внимание даёт ему 0.835 — в десять раз больше. Скор меняет знак с −0.56 на +0.83.

Когда история однородна, разница минимальна — именно поэтому проблему долго не замечали.

Почему в DIN не нормализуют веса внимания софтмаксом?

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

То есть нормализация выравнивает ровно то, что хотелось бы различать.

Что делает температура внимания?

Задаёт, насколько внимание сосредоточено. Веса — софтмакс от скалярных произведений, делённых на τ, и на обоих концах шкалы конструкция вырождается.

При τ = 2 энтропия весов 2.43 при максимуме ln(12) = 2.48 — внимание превращается в обычное усреднение, ради ухода от которого всё и затевалось. При τ = 0.05 энтропия ноль, вес одного события равен единице — жёсткий выбор одного элемента, остальная история выброшена.

Причина та же, что в двухбашенной модели: скалярные произведения нормированных векторов зажаты в [−1; 1], и без деления софтмакс над ними почти равномерен.

Расскажите про SASRec, BERT4Rec и чем закончилась их история.

SASRec — перенос из NLP: айтемы как слова, пользователи как предложения, задача next item prediction, трансформер-декодер с каузальной маской. BERT4Rec — двунаправленное внимание и cloze task вместо каузального предсказания; заодно там учились на полный софтмакс. BERT4Rec показывал результаты лучше.

Развязка: SASRec с правильным лоссом обгоняет BERT4Rec. Исходное преимущество объяснялось не двунаправленным вниманием, а функцией потерь — в ванильном SASRec это была бинарная кросс-энтропия с одним равномерным негативом.

Насколько это плохо: при каталоге в миллион вероятность, что случайный негатив окажется одним из ста ближайших к позитиву, равна 0.0001. Чтобы увидеть трудный негатив хотя бы в половине случаев, нужно почти семь тысяч сэмплов. Модель почти никогда не видит того, что должна научиться различать, и архитектура тут ни при чём.

Урок: сравнивая две работы, первым делом сверяйте лосс и схему негативов.

Почему в рекламе ушли от раннего связывания?

Из-за нагрузки. При раннем связывании вектор пользователя зависит от кандидата, поэтому трансформер по истории надо прогонять для каждого кандидата отдельно.

Счёт: история 100 событий, d = 256, два слоя, 500 кандидатов. Один прогон — 6.3e+07 операций, то есть 1.25 мс при 50 GFLOPS. Пятьсот прогонов — 627 мс, при бюджете ранжирования 20 мс это промах в 31 раз.

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

Почему в историю берут клики, а не показы?

Два аргумента. Информационный: показ выбран предыдущей моделью, а клик — самим человеком; показы говорят о системе, а не о пользователе. Инженерный: при CTR 5% показов в двадцать раз больше, а внимание квадратично по длине, значит стоимость растёт в 400 раз.

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

Как работать с очень длинной историей?

Прямо её в модель на запросе не поставить: внимание квадратично, и история в 8000 событий дороже сотни в 1113 раз, причём квадратичный член там уже 94% стоимости.

Два подхода. Event picking — выбирать среди глубокой истории по содержательному принципу, а не брать последние n. И офлайн-контур — считать вектор глубокой истории заранее, раз в сутки.

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

Как измеряют время пребывания на документе?

Напрямую его никто не сообщает. Мерят по возврату в выдачу: если следующее событие пользователя произошло через t секунд, примерно столько он и провёл на документе; отсутствие возврата трактуется отдельно. На этом строится таргет «глубокий клик» — пребывание дольше N секунд.

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

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

Усреднение

Вектор не зависит от кандидата. Единственный трек из 12 событий: вес 0.083 против 0.835.

DIN

\(v_U(A)=\sum_j w_j e_j\), \(w_j=g(e_j,v_A)\). Без softmax — сумма весов это интенсивность интереса.

BST, TransAct

Позиционные эмбеддинги и трансформер; тип действия отдельным эмбеддингом; раннее связывание.

Температура

τ=2 → энтропия 2.43 при максимуме 2.48, снова усреднение. τ=0.05 → жёсткий выбор.

Главный урок

BERT4Rec выигрывал лоссом, не вниманием. Один равномерный негатив трудный с вероятностью 0.0001.

Связывание

Раннее: 500 прогонов, 627 мс. Позднее: один прогон, 1.25 мс. Бюджет 20 мс.

Клики, не показы

Показов в 20 раз больше, внимание дороже в 400 раз, а информации меньше.

Два контура

Офлайн — глубина без свежести и контекста; на запросе — свежесть без глубины. Складываем.

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