Привет, Хабр! С вами Никита Зелинский, Chief Data Scientist МТС, директор по машинному обучению и исследованию данных MWS. В прошлый раз рассказывал, куда расти Data Scientist и какие навыки для этого нужны, а сегодня будет адаптация моего доклада с конференции True Tech Day. Полную видеоверсию можно посмотреть в комьюнити True Tech в VK.
Обсудим, как трансформеры меняют индустрию рекомендательных систем и почему это уже не просто хайп, а устойчивый стандарт, с которым работают в реальных продуктах. Покажу путь от базовых подходов к state-of-the-art-архитектурам, а еще объясню, как с помощью open-source-библиотеки RecTools от МТС можно сравнивать, конфигурировать и оптимизировать рекомендательные алгоритмы на практике.
Будет полезно тем, кто хочет разобраться, как устроены трансформерные рекомендательные модели. Приступим!

Постановка next-item-prediction-задачи рекомендаций
Начнем с базового: что такое рекомендация? Рекомендательными сервисами пользуются все, но у задачи много формулировок. Самая простая — next item prediction.
Возьмем, например, KION. У нас есть несколько пользователей, и каждый из них использовал элементы каталога — то есть смотрел фильмы. Важно понимать, что постановка задачи, когда рассматривается только один тип взаимодействия, использовалась довольно долго, примерно до 2022 года. В этом варианте учитывается только факт действия — например, просмотр. Никакие дизлайки, рейтинги и другие формы взаимодействия в расчет не берутся, хотя в классическом соревновании от Netflix речь шла именно о предсказании рейтинга.

При других подходах — например, матричном разложении — можно было задавать разные веса и учитывать больше типов сигналов. Но в трансформерных остается один action — «посмотрел фильм». Наша задача — по истории просмотров предсказать, какой фильм пользователь захочет увидеть следующим. При обучении модели мы, конечно, знаем правильный ответ — таргет, к которому нужно прийти.
Фильмы берем из каталога, в нашем случае их 14 (размер каталога). Картинки и названия мы заменяем на ID, который есть у каждого фильма: это экономит место и упрощает представление данных.

При этом очевидно, что зрители смотрят фильмы по-разному, у всех разные паттерны потребления. Чтобы привести последовательности к одной длине по разным пользователям и вписать их в табличку, мы применяем padding — заполняем недостающие позиции. Например, если мы рассматриваем 100 последних событий, а у пользователя всего 98, в начало последовательности добавляются нули. Формально это не нули, а специальные токены, но выполняют они ту же функцию — выравнивают длину.
Если перейти от картинки к математике, ничего кардинально не меняется: табличка остается простой. Последовательность обозначаем как x: каждый пользователь представлен последовательностью items, с которыми он взаимодействовал. Наша задача — предсказать следующий item. Единственное, что мы делаем, — нумеруем эти позиции.

Упрощенная схема обучения SASRec
В 2018 году появилась SasRec (Self-Attentive Sequential Recommendation) — первая трансформерная модель в рекомендациях. Давайте посмотрим, как она решала задачу: это важно для дальнейшего понимания.
Нам нужно несколько вспомогательных элементов. Первый — матрица эмбеддингов. У нас есть item, но его векторное представление заранее неизвестно. Поэтому на старте мы инициализируем его случайными числами — присваиваем каждому фильму набор значений заданной длины. Например, 256. Можно больше, меньше — зависит от того, как это влияет на качество модели. Параметры подбираются на практике.

Во время обучения модель будет обращаться к этой таблице, вытаскивать нужные векторы и по мере обучения обновлять их через backprop. Получается такая lookup-таблица — словарь, в который можно подсматривать.
С items все более-менее понятно, но есть еще и позиционные эмбеддинги. Они пришли из NLP — рекомендательные системы это не изобретали. По принципу они похожи, но здесь мы кодируем не сам item, а его позицию в последовательности. Независимо от того, какой фильм стоит на последнем месте, embedding для этой позиции будет один и тот же. То есть embedding первой (если считать справа налево) — всегда одинаковый, вне зависимости от самого фильма или пользователя.
Во время обучения эта таблица тоже обновляется, но принцип сохраняется: модель смотрит не на содержимое item, а на его положение в списке. Это ключевое.

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

Дальше, при обучении, когда мы дойдем до подсчета loss, все будет меняться. Мы считаем loss на батче примеров, берем антиградиент и по нему обновляем веса — это стандартный backpropagation. Он проходит по всей сети справа налево (и вообще, в этой задаче многое идет справа налево), меняя значения векторов. На каждой итерации обучения они становятся все более точными — лучше описывают соответствующие item.
Конечно, на каком-то этапе обновления начинают замедляться и перестают давать прирост — это уже вопрос масштабируемости. Точно так же с позиционными, тоже сначала случайными, а потом, когда обучаемся, пересчитали назад.

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

Следующий шаг — складываем item-вектор и позиционный. После этого применяем dropout — на каждой итерации случайно зануляем часть весов. Это повышает устойчивость модели. Представьте себе игру, где нужно вытаскивать палочки из башни. Если она не развалилась, даже когда убрали половину, значит, конструкция надежная. В нейросетях работает тот же принцип.
Теперь — к attention-блоку. Мы обучаем три матрицы: query, key и value. Все они изначально тоже инициализируются случайными значениями, а потом обновляются по градиенту. В процессе — несколько матричных перемножений, наложение каузальной маски (чтобы модель не заглядывала вперед по последовательности) и применение softmax — функции, связанной с экспонентой, которая превращает скоры в веса внимания, с последующим умножением на матрицу V. Весь этот блок и есть тот самый attention — основа архитектуры трансформеров. По сути, весь слой строится на этом, с добавлением skip connections и полносвязных слоев. В LLM таких блоков очень много — часто десятки.
В результате этих операций мы получаем вектор, описывающий юзера на каждом шаге последовательности, так называемый user embedding. Его значение зависит от позиции: если пользователь посмотрел четыре фильма из пяти, embedding будет одним, если пять — уже другим.
На этом этапе у нас есть все, что нужно: эмбеддинги пользователей и items. Мы знаем, какой фильм был следующим в последовательности, и в курсе, какие он не смотрел. В оригинальной реализации SASRec для обучения берется один негатив — случайный фильм, с которым юзер не взаимодействовал. Дальше — просто: умножаем вектор пользователя на item и проверяем, насколько хорошо предсказание совпадает с реальностью. То есть близко ли значение к «посмотрел» (1) или «не посмотрел» (0).
Блок attention в модели SASRec — это упрощенный вариант, в реальности используется multi-head attention, то есть несколько параллельных каналов. Но чтобы не перегружать, оставлен один.

Дальше все просто: берем правильный item (тот, который пользователь действительно посмотрел) и один случайный негативный (тот, с чем он не взаимодействовал). Из той же таблицы эмбеддингов достаем их векторы. Считаем функцию, которая должна различать эти два случая: перемножаем вектор пользователя с embedding положительного item и с embedding отрицательного.
Задача обучения — подобрать веса так, чтобы скалярное произведение user-вектора на вектор положительного item стремилось к единице (то есть «посмотрел»), а на вектор негативного — к нулю («не посмотрел»).
Идея понятная, но в SASRec решили, что этого недостаточно. Поэтому добавили механизм shifted sequence — к нему скоро перейдем. В трансформерном блоке для простоты убраны layer norm и residual connections, иначе схема получалась бы слишком громоздкой.
Shifted Sequence и BERT4Rec

Shifted Sequence — это способ сформировать обучающую выборку. Представим, что у пользователя есть последовательность взаимодействий с item: item1, item2, item3 и так далее. На каждом шаге модель должна предсказать следующий элемент. Например, в момент item1 правильным ответом будет item2, в item2 — item3 и так далее. Получается, по текущему item и всем предыдущим нужно угадать следующий. Если в последовательности есть item13, а затем item8, то, увидев item13 и все, что было до него, модель должна предсказать item8. Это и есть правильный ответ.
Неправильный — любой другой item. В оригинальном датасете для обучения выбирается один такой негативный пример. Методика того, как именно выбрать этот неправильный item, называется negative sampling — к ней еще вернемся.

Заменяем пользователя на его embedding, подставляем embeddings для положительного и отрицательного item’ов. Дальше считаем так называемые logit — это просто скалярное произведение векторов (и да, это не тот logit который функция).
Embedding item умножается на embedding пользователя — это logit положительного ответа. Затем embedding другого, неправильного, item умножается на тот же embedding юзера — и получаем logit негативного ответа.
В базовом варианте это просто произведение векторов, но можно использовать и более сложную функцию. В некоторых работах вместо умножения применяют небольшую нейросеть — двух- или трехслойную, она учится вычислять степень соответствия между user и item. Все это повторяется для следующей позиции в последовательности — просто сдвигаемся на один шаг и продолжаем.

Дальше считаем binary cross entropy — по сути это небольшой loss, который задается так:
self.loss = tf.reduce_sum(
- tf.log(tf.sigmoid(self.pos_logits) + 1e-24) * istarget -
tf.log(1 - tf.sigmoid(self.neg_logits) + 1e-24) * istarget
) / tf.reduce_sum(istarget),
Такой расчет повторяется для всех сдвинутых элементов в последовательности, потому что у нас их много — на каждый шаг свой правильный ответ.

Вот такой нехитрый loss. Как только он посчитан, с помощью backpropagation вычисляется градиент, и по цепному правилу производной сложной функции мы обновляем все веса обратно — шаг за шагом, по всей сети.
Так происходит обучение SASRec — и это уже 2018 год. Часто все изображают схематично. Помните, на диаграмме был кружочек LLM? Уровень абстракции такой же — за кругом может скрываться разная внутренняя логика.

В SASRec обычно рисуют так: внизу embeddings items и позиционные кодировки, дальше — N блоков трансформера. Здесь уже добавлены skip connections (или residual-соединения), чтобы сигнал не затухал. Мы берем входной, пропускаем через блок и прибавляем результат к самому входу. Потом повторяем то же самое на следующем уровне.
Появляется layer normalization и полносвязный слой. На выходе — предсказание других items. Да, этот синий квадратик с layer norm, его можно перетаскивать в разные места, где лучше сработает Post Layer Norm, Pre Layer Norm.
Часто модель изображают в виде так называемой двухбашенной архитектуры, как на схеме справа: одна башня отвечает за пользователя, другая — за item. Хотя на самом деле трансформер может заглядывать в embeddings items, все равно такую схему используют для наглядности.

Сначала мы говорили, что у нас есть только один тип взаимодействия — «посмотрел фильм», а здесь уже появляется множество разных действий. Мы можем рассматривать не просто итерацию с item, а действия пользователя в сервисе: просмотр, дизлайк, оценка, пропуск, частичный просмотр и так далее. И item тогда — это не просто элемент каталога, а действие, привязанное к конкретному элементу. В этом случае модель будет предсказывать уже не только следующий фильм, а скор (число, которое, при должной калибровке можно было бы интерпретировать как"вероятность") для разных типов действий.

После SASRec вышел BERT4Rec. Здесь три ключевых отличия:
Другая loss-функция. Именно благодаря ей модель показывает лучшие результаты, как выяснилось позже.
Тип маски. В SASRec использовалась каузальная: модель не могла смотреть в будущее. А BERT4Rec — bidirectional, он видит как влево, так и вправо, то есть заглядывает в будущее.
Подход к обучению. Модель не пытается предсказывать следующий токен. Вместо этого из последовательности действий пользователя случайным образом маскируются некоторые элементы — как будто мы «играем в прятки». Задача модели — восстановить замаскированные по оставшемуся контексту, поэтому тут и разрешено «подглядывать» вперед.
При этом архитектурно модели схожи — они обе трансформерные.

В 2024 году фокус сместился. LLM действительно были на хайпе, но в сравнении с рекомендательными системами их «каталоги» выглядят небольшими. Если в мультиязычных моделях работают со словарями размером порядка сотен тысяч токенов, то в рекомендательных сервисах приходится иметь дело с каталогами по сотням миллионов объектов. Задача объективно сложнее: дискретное пространство огромное, да и плотность данных выше.
Кроме того, в NLP данные ограничены текстовыми корпусами, а вот в рекомендациях объемы взаимодействий с пользователями несопоставимо больше. Один только крупный стриминговый сервис генерирует десятки миллиардов событий в день — и модели приходится обновлять онлайн, чтобы не терять актуальность.
Когда вышла статья HSTU (на слайде выше выделено жирным), началась новая эпоха скейлинга. Стало понятно, что нужно подбирать архитектуры, которые сохраняют рост качества даже при увеличении глубины — вплоть до десятков трансформерных слоев (40+).
Индустрия пробовала все подряд. Экспериментировали с негативным сэмплированием: не брать примеры случайно, а учитывать их популярность или вероятность неправильной классификации; подключать вспомогательные модели для подбора «правильных» негативов. Пытались заменить скалярное произведение как функцию связи user–item на косинусное расстояние или обучаемый MLP. Пробовали разные схемы токенизации, формулировки задач, варианты функций потерь.
Эти шаги были локальными инженерными улучшениями, которые постепенно давали рост качества.

Авторы PinnerFormer в 2022-м пересмотрели саму постановку задачи.
В SASRec, как мы обсуждали, модель предсказывает следующий элемент пошагово — один за другим. Но обучение только на один элемент оказалось слишком узким. В PinnerFormer предложили предсказывать целое будущее окно взаимодействий. Например, фиксируем период (год истории) и ограничиваем до 400 элементов в последовательности, а затем пытаемся предсказать множество items, с которыми пользователь будет взаимодействовать в следующем месяце.
Таким образом модель учится захватывать долгосрочные интересы пользователя, а не только «что он кликнет прямо сейчас». Для бизнеса это особенно важно: понимание долгосрочных предпочтений, работа на удержание, сегментация интересов во времени.
Важно, что в 2022-м в индустрии еще практически никто не решал задачу негативного предсказания — того самого «красненького» на слайдах. Системы старались угадать, что понравится, но не умели явно моделировать то, что пользователь с большой вероятностью проигнорирует.

Новая постановка задачи
В 2024 году появились генеративные постановки. Одной из ключевых статей стала Actions Speak Louder, в которой была представлена модель HSTU. Идея — action interleaving: в последовательность включаются не только объекты каталога (items), но и действия пользователя (actions). Получается чередование: item–action, item–action, item–action. Каждый объект из каталога сопровождается тем, что сделал с ним пользователь: просмотр, лайк, покупка и так далее.

Этот подход неожиданно открыл путь к решению сразу нескольких задач. Например, при генерации кандидатов (того, что можно потенциально показать пользователю) нам нужно предсказывать именно items. А на этапе ранжирования, то есть выбора из кандидатов наиболее подходящих, нужно понимать actions. Теперь у нас есть токены для обоих случаев — и для items (синие), и для actions (красные).
Это дает нам понимание, чего ждать, когда мы подскажем ему фильм: мы получим лайк или просмотр, за который нам заплатит правообладатель? Это напрямую влияет на бизнес-метрики?

В статье HSTU противопоставляется классическому пайплайну с извлечением фичей и их инжинирингом. А теперь представьте: речь шла о десятках тысяч признаков. Сколько раз пользователь кликал, досмотрел ли до конца, какие категории предпочитает? Все это считалось отдельно и потом подавалось в сетку — и это было очень сложно прокручивать. Отдельно разрабатывались методы, чтобы учитывать все эти десятки тысяч фичей: как они взаимодействуют и влияют на ответ. Коллеги из HSTU предложили убрать все численные фичи. Если у нас есть достаточно длинная последовательность действий пользователя (а трансформер начинает работать, например, от пяти событий), то сетка сама выучит все эти фичи.
Архитектура HSTU велючает в себя как базовый механизм внимания — стандартный блок self-attention с матрицами Q, K, V, с добавлением relative attention (учет позиции и времени действия) и матрицей U — простой обучаемой gating-функцией: если сигнал положительный — проходит дальше, отрицательный — блокируется.
Отказ от ручных фичей позволил масштабироваться. На графиках статьи:
красное — классические feature-based подходы;
синее — HSTU с loss-функцией.
Чем больше ресурсов (PetaFLOPs/день) тратим, тем ниже loss и тем выше Hit Rate (доля угаданных items в топ-100).
Это и есть эффект scaling law: рост вычислительных ресурсов приводит к росту качества модели, а не к «потолку», как было раньше.

Модель FuXi

Начну с интересного факта. FuXi — это не цвет «фуксия», а имя первого мифического императора Китая. Чувствуете размах? У нас аналогично модель могла бы называться, например, «Рюрик».
Итак, FuXi — китайская модель, которая отличается новым подходом к представлению времени и позиции в последовательности.
Раньше в трансформерах временная и позиционная информация кодировались вместе (через разные варианты position encoding). В FuXi предложили учить отдельные каналы:
temporal — учитывает время суток и конкретные временные метки (например, днем пользователь смотрит одно, вечером — другое);
timeless — кодирует интервалы между событиями, что особенно важно, если пользователь активно взаимодействует неделю, а потом возвращается через год;
semantic — отвечает за схожесть items между собой. Его можно обучать отдельно: берем embeddings items, добавляем фичи, прогоняем через вспомогательную сеть и уже готовое представление подаем в трансформер.
Все эти каналы затем интегрируются в loss-функцию модели.
Ключевой результат: в FuXi удалось масштабироваться и обучить сеть глубиной до 32 слоев трансформера — полноценную тяжелую архитектуру. В экспериментальных условиях такое обучение занимало месяцы вычислений.

В продакшне FuXi работает в стриминговом режиме: поступает новый блок пользовательских данных — модель дообучается, затем приходит следующий — и процесс повторяется. Такой подход позволяет системе постоянно оставаться актуальной.
Несмотря на большую глубину (32 слоя трансформера), каждый уровень продолжал вносить вклад в качество. Улучшались как метрики hit rate (доля угаданных объектов в выдаче), так и метрики качества ранжирования, например NDCG, которая измеряет, насколько правильно система расставляет items по приоритету.
Библиотека для построения и валидации рекомендательных систем
Теперь расскажу о библиотеке RecTools от МТС — у нее почти 400 звезд на GitHub, и я очень советую попробовать ее для построения рекомендательных систем. В ней есть не только трансформеры, но и классические алгоритмы: матричные разложения, двухбашенные архитектуры. И мы добавили туда tot поддержку трансформеров.

Чтобы не зацикливаться на одной модели, например, на SASRec или FuXi, в RecTools можно комбинировать подходы. На слайде — примеры датасетов, на которых это доступно для экспериментов в туториалах. В KION средняя длина последовательности — больше 5,5 событий, и трансформеры уже отлично работают. А вот в MovieLens на 10 миллионов — там и длина последовательностей больше, и прирост от трансформеров еще заметнее.

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

Берем датасет, выбираем модель, например SASRec, которую мы подробно разбирали выше, и дальше просто вызываем fit, predict или recommend. Готово!
Важно отметить, что имплементации одной и той же модели могут немного отличаться — кто-то меняет порядок операций, кто-то по-другому строит архитектуру. Наша версия SASRec в RecTools — полностью open-source — сейчас, пожалуй, одна из лучших на открытых датасетах с точки зрения производительности и качества по метрикам.


Beyond accuracy метрики
В рекомендациях важно не только точно угадать один или несколько элементов, но и сделать подборку разнообразной, чтобы она не состояла из очевидных вариантов. За это отвечают так называемые Beyond accuracy метрики. Такие метрики помогают:
избегать ситуации, когда пользователю предлагают одно и то же;
повышать diversity (разнообразие выдачи);
учитывать novelty и serendipity (насколько результат неожиданно интересен);
усиливать персонализацию, делая рекомендации более «живыми».
Мы добавили валидацию по этим метрикам — к ней скоро перейдем. А пока остановимся на признаках.

В той базовой схеме, где трансформер обучается только на последовательности items, можно легко добавить дополнительные фичи. Это делается буквально в несколько строчек. Если у вас в каталоге есть другие признаки, например фильмы это или музыка, категория товара и так далее, их тоже можно передать модели. В результате код, который раньше занимал около 10 строк, станет чуть длиннее — примерно на 6 строк. Зато трансформер будет учитывать признаки items.

Это пример de-biased-валидации — подхода, при котором мы оцениваем не только точность угадывания, но и разнообразие выдачи. Нас интересует не просто, попал ли алгоритм в цель, но и насколько его ответ был нетривиальным. Речь о случаях, когда система предлагает непопулярный item — такой, о котором пользователь раньше не слышал — и он все равно оказывается релевантным. То есть рекомендация получилась не только точной, но и неожиданной, что особенно ценно в реальных продуктах.

RecTools стоит попробовать, потому что здесь все готово «из коробки» — валидация встроена и работает сразу. Это особенно важно, когда вы перебираете 20–30 моделей с разными параметрами. Плюс есть поддержка callbacks: на каждой эпохе можно автоматически считать метрики. Мы реализовали часть логики поверх PyTorch Lightning, так что все удобно пробрасывается и масштабируется.

Все, что отмечено красным на слайде ниже, указывает на Torch Native. Дальше идет Lighting, Data Scientistы понимают, в чем ее удобство: поддержка callbacks, удобная передача loss-функций, автоматизация многих рутинных процессов. Еще один значок, тот, с которого начинается почти каждый курс по Data Science, — это Pandas. У нас нет версии под TensorFlow или Keras — только Torch и Lightning.

А вот как выглядит история с конфигурированием. Здесь явно задаются все ключевые параметры: сколько трансформерных блоков использовать, голов внимания — multi-head или одна, размерность эмбеддингов, минимальное количество взаимодействий для пользователя и другие настройки. Все это задается прямо в коде и добавляет всего в семь строк. Полная конфигурация модели становится прозрачной и гибкой.

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

В движении:

В движении:
В завершение — краткое саммари. Мы поддерживаем debias-валидацию и у нас есть возможность конфигурировать трансформерные модели начиная с SASRec. Большинство часто используемых модификаций — изменения в loss-функциях, сэмплировании негативов, механизмах связывания, количестве слоев трансформера — уже реализованы в нашей библиотеке.
Это значит, что вы можете комбинировать элементы как конструктор и воспроизводить практически любую статью, опубликованную до 2024 года. Модели вроде FuXi, LiGR и HSTU — действительно масштабные, тяжелые, обучаются долго и пока не включены в RecTools. Но все, что было до них, легко запускается буквально в пять строк.
На этом сегодня у меня все. Если есть вопросы, задавайте в комментариях, постараюсь ответить!