Эмбеддинги и two-tower архитектура

Урок объясняет, как нейросети учат эмбеддинги пользователей и товаров и как two-tower архитектура генерирует кандидатов в больших системах.

Two-tower модель — это две нейросети («башни»), которые независимо превращают пользователя и товар в эмбеддинги одного пространства так, что близость эмбеддингов означает релевантность.

От факторов к эмбеддингам

Матричное разложение уже учило по вектору на пользователя и товар. Нейросетевые рекомендации обобщают эту идею: вместо одного вектора на сущность, в эмбеддинг можно «вшить» сколько угодно признаков. Башня пользователя принимает его историю, демографию, контекст и выдаёт вектор. Башня товара принимает признаки товара (категория, текст, картинка) и выдаёт вектор того же размера. Скор релевантности = близость (обычно скалярное произведение или косинус) этих двух эмбеддингов.

  пользователь ----> [ башня U ] ----> u_emb \
                                                 ----> близость = релевантность
  товар --------> [ башня I ] ----> i_emb /

Почему именно две башни

Главная хитрость в том, что башни независимы. Эмбеддинги всех товаров можно посчитать заранее, один раз, и сложить в индекс. Когда приходит запрос, мы считаем только эмбеддинг пользователя, а затем ищем ближайшие к нему товарные эмбеддинги. Это превращает рекомендацию миллионов кандидатов в задачу поиска ближайших соседей (ANN) — той самой, что решают векторные БД. Без раздельных башен пришлось бы прогонять через сеть каждую пару пользователь-товар, что нереально для большого каталога.

Близость в коде

Сами эмбеддинги учит нейросеть (для этого нужны torch/tensorflow — такой код нельзя запускать в браузере). Но саму операцию поиска ближайших по уже готовым векторам легко показать на stdlib.

import math

def cosine(a, b):
    num = sum(x * y for x, y in zip(a, b))
    na = math.sqrt(sum(x * x for x in a))
    nb = math.sqrt(sum(y * y for y in b))
    return num / (na * nb) if na and nb else 0.0

user_emb = [0.9, 0.2, 0.1]
item_embs = {
    "боевик_1": [0.8, 0.3, 0.1],
    "драма_1":  [0.1, 0.9, 0.2],
    "боевик_2": [0.85, 0.1, 0.2],
    "комедия_1":[0.2, 0.2, 0.9],
}
scores = {name: cosine(user_emb, e) for name, e in item_embs.items()}
print("Кандидаты по близости эмбеддингов:")
for name in sorted(scores, key=lambda x: -scores[x]):
    print(f"  {name}: {round(scores[name], 3)}")

Вывод:

Кандидаты по близости эмбеддингов:
  боевик_1: 0.99
  боевик_2: 0.988
  комедия_1: 0.354
  драма_1: 0.337

Как работает под капотом

Two-tower обучают на парах «пользователь — товар, с которым он взаимодействовал» как на положительных примерах, добавляя случайные товары как отрицательные (negative sampling — привет неявному фидбэку). Функция потерь притягивает эмбеддинги положительных пар и отталкивает отрицательные. В продакшене башня товаров считается офлайн, эмбеддинги кладутся в ANN-индекс (HNSW, IVF), а онлайн остаётся быстрый запрос «дай 500 ближайших к этому пользователю» — это этап генерации кандидатов.

Частые ошибки

  • Считать скор для каждой пары онлайн. Теряется весь смысл двух башен; считайте товарные эмбеддинги заранее и ищите ANN.
  • Забыть negative sampling. Без отрицательных примеров модель не научится отличать релевантное от случайного.
  • Смешивать пространства башен. Эмбеддинги пользователя и товара должны жить в одном пространстве, иначе близость бессмысленна.

Итоги

  • Two-tower кодирует пользователя и товар в эмбеддинги одного пространства.
  • Близость эмбеддингов = релевантность; башни независимы.
  • Товарные эмбеддинги считают офлайн и ищут ANN — это генерация кандидатов.
  • Обучение идёт на положительных парах с negative sampling.
Проверьте себя
1. В чём ключевое преимущество раздельных «башен» в two-tower модели?
AОни делают модель красивее
BЭмбеддинги товаров можно посчитать заранее и искать ближайших (ANN), не прогоняя каждую пару через сеть
CОни убирают необходимость в обучении
DОни работают только без эмбеддингов
2. Зачем two-tower обучают с negative sampling?
AЧтобы ускорить инференс
BЧтобы дать модели отрицательные примеры и научить отличать релевантное от случайного
CЧтобы удалить положительные пары
DЭто не нужно при наличии эмбеддингов