Эмбеддинги и 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.