Контентные рекомендации

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

Контентные рекомендации описывают каждый товар вектором признаков (жанры, теги, слова описания) и советуют объекты, похожие по содержанию на то, что пользователю уже нравилось.

Идея

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

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

TF-IDF: вес слов в описании

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

import math
from collections import Counter

docs = {
    "Матрица": "фантастика боевик киберпанк",
    "Бегущий": "фантастика киберпанк нуар",
    "Титаник": "драма романтика катастрофа",
    "Аватар":  "фантастика боевик приключения",
}
tokenized = {n: t.split() for n, t in docs.items()}
N = len(docs)
df = Counter()
for toks in tokenized.values():
    for t in set(toks):
        df[t] += 1
idf = {t: math.log(N / df[t]) for t in df}

def tfidf_vec(toks):
    tf = Counter(toks)
    return {t: (tf[t] / len(toks)) * idf[t] for t in tf}

def cosine(a, b):
    common = set(a) & set(b)
    num = sum(a[t] * b[t] for t in common)
    na = math.sqrt(sum(v*v for v in a.values()))
    nb = math.sqrt(sum(v*v for v in b.values()))
    return num / (na * nb) if na and nb else 0.0

vecs = {n: tfidf_vec(t) for n, t in tokenized.items()}
q = "Матрица"
sims = {n: cosine(vecs[q], vecs[n]) for n in vecs if n != q}
print(f"Похожие на '{q}' по контенту:")
for n in sorted(sims, key=lambda x: -sims[x]):
    print(f"  {n}: {round(sims[n], 3)}")

Вывод:

Похожие на 'Матрица' по контенту:
  Бегущий: 0.35
  Аватар: 0.35
  Титаник: 0.0

«Матрица» сблизилась с «Бегущим» (общий «киберпанк») и «Аватаром» (общие «фантастика», «боевик»), а с «Титаником» сходство нулевое — другой набор тегов.

Профиль пользователя и эмбеддинги

Профиль пользователя — это усреднённый (часто взвешенный по оценкам) вектор признаков понравившихся товаров. Рекомендация = товары, ближайшие к этому профилю по косинусу. Вместо TF-IDF современные системы берут плотные эмбеддинги (например, из языковых моделей для описаний или из CNN для изображений). Поиск ближайших по эмбеддингам — это прямой мост к векторным базам данных и ANN-поиску, о которых речь в учебнике «RAG и векторные БД».

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

Конвейер контентной системы: извлечь признаки товаров → построить векторы (TF-IDF или эмбеддинги) → собрать профиль пользователя как агрегат его истории → найти ближайшие к профилю товары. Для больших каталогов точный перебор заменяют приближённым поиском соседей (ANN), храня векторы в векторной БД.

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

  • Не взвешивать слова. Без IDF частые общие слова забивают сигнал; используйте TF-IDF или эмбеддинги.
  • Переспециализация. Контент рекомендует слишком похожее; добавляйте разнообразие и смешивайте с CF.
  • Сравнивать вектора без нормировки. Берите косинус, а не сырое скалярное произведение, иначе длинные описания «перевесят».

Итоги

  • Контентные рекомендации опираются на признаки товаров и профиль пользователя.
  • TF-IDF взвешивает слова по их информативности; косинус меряет близость.
  • Подход решает холодный старт по товарам, но склонен к переспециализации.
  • Эмбеддинги и ANN-поиск связывают контент-рекомендации с векторными БД.
Проверьте себя
1. Чем контентные рекомендации принципиально отличаются от коллаборативных?
AОни используют признаки самих товаров, а не поведение других пользователей
BОни всегда точнее
CОни не нуждаются в товарах
DОни работают только с числами
2. Зачем в TF-IDF нужна часть IDF?
AЧтобы ускорить токенизацию
BЧтобы понизить вес частых неинформативных слов и повысить вес редких характерных
CЧтобы перевести текст в числа случайно
DЧтобы удалить все слова