Часть 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.120 | 0.083 | +0.97 | +0.60 |
| кулинария | 0.293 | 0.083 | +0.73 | +0.35 |
| техно | 0.685 | 0.083 | +0.73 | −0.34 |
| музыка | 0.835 | 0.083 | +0.83 | −0.56 |
Числа воспроизводятся скриптом _tools/history_demo.py в этом репозитории.
Смотрите на последнюю строку. Внимание находит единственное музыкальное событие и даёт ему вес в 10 раз выше равномерного. Скор меняет знак: +0.83 против −0.56.
Усреднение этот сигнал не «недооценило» — оно его уничтожило. Одно событие из двенадцати в среднем неотличимо от шума, и вся информация о том, что человек вообще слушает музыку, пропала на этапе пулинга.
Обратите внимание и на строку «спорт»: там разница между вниманием и средним минимальна. Когда история однородна, усреднение работает нормально — и именно поэтому проблему долго не замечали.
- Переключайте кандидата — веса по истории пересчитываются, потому что внимание считается относительно него. Серый пунктир (простое среднее) при этом не двигается вообще: он один и тот же для всех кандидатов.
- Выберите кандидата «музыка» — тот самый случай из таблицы выше.
- Ползунок τ показывает вторую половину сюжета: внимание вырождается на обоих концах шкалы.
Что сказать на собесе: «Target attention отличается от пулинга тем, что веса зависят от кандидата. Поэтому редкое, но решающее событие истории не растворяется в среднем — на этом и построены DIN, BST и TransAct».
2. DIN: внимание, зависящее от кандидата
Пусть \(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, отвечающий за реальное время. Устройство стоит помнить как образец инженерного решения:
- 100 последних действий пользователя;
- обучаемый эмбеддинг типа действия, конкатенируемый к вектору айтема — сохранение того, что это было, а не только с чем;
- эмбеддинг айтема берётся из отдельной графовой модели;
- двухслойный трансформер;
- вектор кандидата конкатенируется к векторам событий — раннее связывание;
- вектор пользователя — конкатенация последних \(K\) векторов и max pooling.
Обратите внимание на предпоследний пункт: раннее связывание даёт выразительность, и за неё придётся платить. Счёт придёт в разделе 6.
4. Температура внимания
Веса считаются софтмаксом от скалярных произведений, делённых на температуру. Посмотрим на энтропию весов — она измеряет, насколько внимание «размазано». Максимум при 12 событиях: \(\ln 12 = 2.48\).
| \(\tau\) | Энтропия весов | Максимальный вес |
|---|---|---|
| 2.00 | 2.43 | 0.173 |
| 1.00 | 2.20 | 0.317 |
| 0.35 | 0.66 | 0.835 |
| 0.10 | 0.01 | 0.999 |
| 0.05 | 0.00 | 1.000 |
Числа воспроизводятся скриптом _tools/history_demo.py.
При \(\tau = 2\) энтропия 2.43 почти равна максимуму 2.48 — внимание превратилось в обычное усреднение, ради ухода от которого всё и затевалось. При \(\tau = 0.05\) энтропия ноль и вес одного события равен единице — внимание превратилось в жёсткий выбор одного элемента истории, а вся остальная история выброшена.
Это та же температура и по той же причине, что в двухбашенной модели: скалярные произведения нормированных векторов зажаты в \([-1; 1]\), и без деления софтмакс над ними почти равномерен.
5. SASRec, BERT4Rec и главный урок
Отдельная линия — трансформеры не для ранжирования, а для кандидатогенерации. Перенос из обработки языка прямой: слова = айтемы, предложения = пользователи, задача — предсказание следующего айтема, архитектура — трансформер-декодер с каузальной маской. Это SASRec.
Полезная оптика, которая помогает удержать конструкцию в голове: языковая модель — это двухбашенная модель с обучаемыми эмбеддингами слов, где левая башня обрабатывает контекст, а правая сводится к таблице эмбеддингов.
Формально задача та же, что у двухбашенных кандген-моделей: пары (пользователь, позитив), где пользователь — последовательность прошлых взаимодействий. Но практики использованы более старые, чем мы разбирали:
- бинарная кросс-энтропия вместо софтмакса;
- сэмплируется всего один негатив, и тот равномерно.
Это и стало источником многолетней методологической путаницы в литературе.
BERT4Rec применяет двунаправленное внимание вместо каузального. Чтобы учиться на ту же задачу, пришлось бы отказаться от teacher forcing, а это очень медленно, — поэтому используется cloze task: маскируем случайные токены и предсказываем их. Заодно там учились на полный софтмакс без сэмплирования негативов. Результаты вышли лучше, чем у SASRec.
Оказалось, что SASRec с правильным лоссом гораздо лучше ванильного — и обгоняет BERT4Rec. То есть исходное преимущество BERT4Rec объяснялось не двунаправленным вниманием, а тем, что он учился на полный софтмакс, тогда как SASRec — на бинарную кросс-энтропию с одним равномерным негативом.
Годы сравнений архитектур сравнивали функции потерь.
Каталог 106. Назовём «трудными» 100 айтемов, ближайших к позитиву, — те, различать которые модель и должна учиться. Вероятность, что случайный равномерный негатив окажется трудным: 0.0001.
| Негативов на позитив | Вероятность увидеть хотя бы один трудный |
|---|---|
| 1 | 0.0001 |
| 100 | 0.0100 |
| 1000 | 0.0952 |
| 10000 | 0.6321 |
| 100000 | 1.0000 |
Числа воспроизводятся скриптом _tools/history_demo.py.
Чтобы увидеть трудный негатив хотя бы в половине случаев, нужно 6931 сэмпл вместо одного. Полный софтмакс включает все 106.
Вот почему смена лосса дала больше, чем смена архитектуры: с одним равномерным негативом модель почти никогда не видит того, что должна научиться различать. Никакая архитектура этого не исправит.
Урок, который стоит унести далеко за пределы этой главы: в рекомендательных моделях задача обучения и функция потерь часто решают больше, чем архитектура. Сравнивая две работы, первым делом смотрите, одинаковы ли у них лосс и схема негативов.
6. Раннее связывание против позднего
Теперь про то, как всё это выживает под нагрузкой. Продовая линия развития хорошо показывает, что архитектуру диктует не качество, а бюджет.
История 100 событий, размерность 256, два слоя, 500 кандидатов на запрос.
| Связывание | Прогонов энкодера | Операций | При 50 GFLOPS |
|---|---|---|---|
| Позднее | 1 | 6.27e+07 | 1.25 мс |
| Раннее | 500 | 3.13e+10 | 627 мс |
Числа воспроизводятся скриптом _tools/history_demo.py.
При бюджете ранжирования в 20 мс позднее связывание помещается с запасом, раннее промахивается в 31 раз. Разница ровно в числе кандидатов — потому что при раннем связывании вектор пользователя зависит от кандидата и переиспользовать его нельзя.
Отсюда конструкция: двухбашенная архитектура, где трансформер по истории и контексту считается один раз, айтем кодируется отдельной башней, а скор — скалярное произведение. Тот же компромисс «выразительность против стоимости», что в кандидатогенерации, только теперь он приходит на этап ранжирования.
Два аргумента, и второй чисто арифметический.
Информационный: показ выбран предыдущей моделью, а клик — самим человеком. Показы говорят о системе, клики — о пользователе.
Инженерный: при CTR 5% показов в 20 раз больше, а внимание квадратично по длине:
| Кликов за период | Показов за тот же период | Внимание дороже в |
|---|---|---|
| 100 | 2 000 | 400 раз |
| 500 | 10 000 | 400 раз |
| 2000 | 40 000 | 400 раз |
Числа воспроизводятся скриптом _tools/history_demo.py.
Переход с кликов на показы стоит 400-кратного роста стоимости внимания — при том, что информации в них меньше. Решение принимается легко.
Как измерить время на документе. Напрямую его никто не сообщает. Стандартный приём — мерить по возврату в выдачу: если следующее событие пользователя произошло через \(t\) секунд, примерно столько он и провёл на документе. Отсутствие возврата трактуется отдельно. Так строится таргет «глубокий клик» — пребывание дольше \(N\) секунд.
Нейросеть как признак для бустинга. Важный паттерн внедрения: предсказание трансформера подаётся признаком на вход ранжирующему CatBoost. Сеть не заменяет бустинг, а становится для него сильным признаком — это дешевле и безопаснее, чем менять весь стек сразу.
7. Длина истории: два контура
Хочется анализировать глубокую историю, но добавить её в модель, работающую на запросе, нельзя.
| Длина истории | Операций | Дороже, чем при 100 событиях |
|---|---|---|
| 100 | 6.27e+07 | 1.0 |
| 500 | 5.18e+08 | 8.3 |
| 2000 | 5.14e+09 | 82.1 |
| 8000 | 6.97e+10 | 1112.7 |
Числа воспроизводятся скриптом _tools/history_demo.py.
При 8000 событиях квадратичный член — уже 94% стоимости. Обратите внимание на характер роста: до нескольких сотен событий доминируют проекции и стоимость почти линейна, а дальше внимание съедает всё.
Есть два выхода, и на практике их комбинируют.
- Event picking — брать не последние \(n\) событий, а выбирать среди глубокой истории по более хитрому принципу.
- Офлайн-модель — считать вектор по глубокой истории заранее.
Плюсы: эмбеддинги пользователей пересчитываются раз в сутки; разрабатывать и внедрять заметно проще; за те же ресурсы можно поставить более тяжёлую модель. Именно так доводят длину истории до двух тысяч событий и увеличивают модель в несколько раз.
Минусы ровно два, и оба принципиальны: не учитывается свежая история и не учитывается контекст. Пользователь, который десять минут назад начал искать что-то новое, для офлайн-вектора не изменился.
Поэтому офлайн-модель не заменяет контур реального времени, а дополняет его.
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 раз, а информации меньше.
Два контура
Офлайн — глубина без свежести и контекста; на запросе — свежесть без глубины. Складываем.
Первоисточники
- G. Zhou et al. Deep Interest Network for Click-Through Rate Prediction, KDD 2018.
- Q. Chen et al. Behavior Sequence Transformer for E-commerce Recommendation in Alibaba, DLP-KDD 2019.
- W.-C. Kang, J. McAuley. Self-Attentive Sequential Recommendation, ICDM 2018 — SASRec.
- F. Sun et al. BERT4Rec: Sequential Recommendation with Bidirectional Encoder Representations from Transformer, CIKM 2019.
- A. Petrov, C. Macdonald. Turning Dross Into Gold Loss: is BERT4Rec really better than SASRec?, RecSys 2023 — развязка из раздела 5.
- X. Xia et al. TransAct: Transformer-based Realtime User Action Model for Recommendation at Pinterest, KDD 2023.
- P. Covington, J. Adams, E. Sargin. Deep Neural Networks for YouTube Recommendations, RecSys 2016 — точка отсчёта с усреднением.
- Числа главы:
_tools/history_demo.pyв этом репозитории.