Content-based (TF-IDF)
Каждый фильм описывается числовым вектором из жанров; похожесть — косинус между векторами. Мнения других пользователей не нужны.
Похожие на Toy Story (1995):
Теория простым языком
Content-based рекомендует «похожее на похожее»: если тебе зашёл фильм, найдём другие фильмы, похожие на него по описанию — у нас это жанры. Оценки других людей здесь не нужны: смотрим на карточку самого фильма.
Главная мысль
Чтобы находить «похожие» фильмы, надо сначала научиться измерять похожесть. А чтобы её измерить — описать каждый фильм числами. Разберём это по шагам, вводя слова по дороге.
Важное отличие от коллаборативной фильтрации: здесь нам не нужны взаимодействия других людей с этим фильмом. Нужны только его собственные признаки. Поэтому новый фильм, который ещё никто не оценил, мы всё равно можем рекомендовать — жанры у него есть с первого дня.
Оговорка, которая важнее, чем кажется: «собственные признаки» — это признаки, не зависящие от поведения. В MovieLens рядом с жанрами лежат ещё и теги, и их очень хочется добавить: они точные и редкие. Но теги ставят зрители — это поведенческие данные, у них есть время появления, и у по-настоящему нового фильма их нет. Ниже — что случилось, когда мы их всё-таки добавили.
▸Почему появился content-based подход
Шаг 1. Превращаем фильм в числа
Компьютер не понимает слова «комедия», ему нужны числа. Поэтому каждому фильму сопоставляют список чисел — по одному на каждое возможное слово. Если слово есть у фильма — число больше нуля, если нет — ноль.
Но не все слова одинаково полезны. Жанр «драма» есть у тысяч фильмов и почти ничего не говорит, а «Film-Noir» — редкий и потому информативный. Чтобы редкие слова весили больше, используют приём:
Шаг 2. Меряем похожесть
Теперь у каждого фильма есть вектор. Похожесть двух фильмов — это насколько их векторы «смотрят в одну сторону».
Шаг 3. Рекомендации лично тебе
«Похожие на один фильм» — это полдела. Чтобы советовать персонально, строят твой портрет из того, что тебе уже понравилось (оценки от 4 и выше) — низкие оценки в профиль не идут, иначе мы бы учили модель на том, что тебе как раз не зашло.
# офлайн: один раз превращаем фильмы в векторы
для каждого фильма i:
v[i] = tf-idf(жанры фильма i)
recommend(пользователь u, K):
liked = фильмы, которым u поставил оценку ≥ 4
profile = среднее из нормированных v[i] по liked
для каждого фильма i, не виденного u:
score[i] = cosine(profile, v[i])
вернуть K фильмов с наибольшим score▸Формулы: TF-IDF, косинус, профиль
Классическая запись TF-IDF — та, что встречается в учебниках:
где — как часто слово встречается у фильма, — у скольких фильмов оно есть вообще, — всего фильмов. Редкое слово (маленький ) → большой → больший вес.
Но векторы на этой странице посчитаны не по ней. Мы берём TfidfVectorizer из TF-IDF-реализации scikit-learn в конфигурации по умолчанию, а у неё формула другая — со сглаживанием и добавленной единицей:
Что здесь делает , понятно и важно: слово, которое есть у всех фильмов, по первой формуле получает вес ровно 0 и исчезает из векторов совсем, а по второй остаётся с весом, равным одной только . Затем вектор нормируется по , поэтому косинус считается обычным скалярным произведением.
А вот smooth_idf содержательно не делает у нас ничего, и это стоит сказать прямо. Формально он считает так, будто к коллекции добавлен ещё один документ, содержащий каждое слово разом. Объяснять его «защитой от деления на ноль» соблазнительно и неверно: у любого слова обученного словаря по построению, а незнакомое слово обученный векторизатор просто выбрасывает — делить тоже не на что. Весь его эффект здесь — чуть более сжатый диапазон весов. Мы его не трогали: это значение по умолчанию, и знать про него нужно ровно затем, чтобы повторить наши числа, — по учебничной формуле они не сойдутся. Точная конфигурация — в контракте обучения внизу страницы.
Косинусное сходство двух векторов:
Деление на длины и делает меру независимой от длины описания. При неотрицательных весах числитель ≥ 0, поэтому .
Профиль пользователя — среднее нормированных векторов понравившихся фильмов :
Теги: как выглядит утечка, когда её измеряешь
Главное в этой таблице — что честный вариант существует. У общего для всех вектора айтема единого «как есть на тот момент» и правда нет: протокол прячет у каждого пользователя его последнее событие, и одна общая отсечка тут не строится. Но признаки не обязаны быть общими — для каждого оцениваемого события можно собрать свой срез по его времени, и мы ровно это и посчитали. Такой замер дороже в сотни раз (по сборке векторов на событие) — а прироста не даёт почти никакого. Зато он показывает, чем на самом деле был прирост в нижней строке.
Отсюда и решение в проде: жанры. Не потому, что «с тегами нельзя», а потому что с честными тегами не за что платить. Если такая фича нужна всерьёз, правильный ход — сменить протокол на единый временной срез: тогда у всей выборки одна граница времени, и признаки снова можно собрать один раз.
Побочный урок про сам метод виден там же, в строке про словарь: столько «содержания» у нас и есть, и именно на нём модель показывает свои метрики. Когда в статьях content-based выигрывает, за ним обычно стоят тексты описаний и эмбеддинги, а не список жанров.
Сильная сторона: холодный старт айтема
Content-based с этим справляется: даже у только что вышедшего фильма есть жанры, а значит — вектор. Его можно рекомендовать сразу, не дожидаясь ни одной оценки. Это его главный козырь — и заодно ещё один довод против тегов: у фильма, который никто не смотрел, тегов нет по определению, так что модель на тегах этот козырь теряет ровно там, где он нужен.
Сильные стороны
- Решает холодный старт айтема: новому фильму хватает его описания.
- Объяснимость: «похоже по жанрам» — понятная причина рекомендации.
- Не зависит от других пользователей — работает даже с одним человеком.
Слабые стороны
- Однообразные списки: внутри выдачи фильмы похожи друг на друга — diversity@10 у content-based самая низкая из всех моделей курса (см. карточку метрик выше).
- Качество ограничено качеством описаний: по одним жанрам легко промахнуться.
- Слабая точность персональных топ-списков (это видно по метрикам на этой странице).
⚠️ Что может пойти не так
- Качество = качество признаков: если у фильмов только жанры (как часто бывает), векторы грубые и похожесть смазана.
- Путать «однообразно» с «популярно». У нашего content-based diversity@10 действительно самая низкая, а вот novelty@10 — самая ВЫСОКАЯ из всех моделей: он уходит в хвост каталога (и покрытие у него вчетверо выше, чем у item-CF). Это разные величины, и одна не выводится из другой.
- «Пузырь фильтров» — гипотеза про ПЕТЛЮ: показали похожее → человек кликнул → модель ещё сузилась. Одной офлайн-выдачей она не проверяется: нужны динамика показов и реакция людей во времени. Как это вообще выглядит на симуляции — в модуле про логи и разметку.
- Профиль усредняет всё подряд: у человека с очень разными вкусами «средний» вектор оказывается в пустоте между кластерами и плохо описывает любой из них.
- Низкие оценки нельзя кидать в профиль наравне с высокими — иначе нелюбимое тянет рекомендации к себе.
🧠 Проверь себя: Почему новый фильм content-based может рекомендовать сразу, а коллаборативная фильтрация — нет?
Конфигурация обучения
Всё, что нужно, чтобы повторить числа на этой странице. Значения читаются из самих обученных моделей, а не набраны в вёрстке, — разойтись с кодом они не могут.
Что дальше
Опирается на этот модуль — здесь он нужен как предпосылка
Можно читать параллельно — тот же блок, порядок между ними не важен
До финальной сборки не хватает ещё 11 модулей по этому пути.
Порядок здесь — рекомендация из карты курса, ничего не блокируется. Отметка «прочитано» хранится только в этом браузере.
Источники
- Feature extraction: Tf–idf term weightingscikit-learn · документациявариант нормировки TF-IDF, который воспроизведён в нашей реализации
- Item-Based Collaborative Filtering Recommendation AlgorithmsB. Sarwar, G. Karypis, J. Konstan, J. Riedl · 2001 · статьякосинусная похожесть айтемов — общая механика с item-CF, отличается только источник признаков