Item-based collaborative filtering

Фильмы похожи, если их оценивали одни и те же люди (косинус по столбцам матрицы взаимодействий). Никаких жанров — только поведение.

Загружаем данные

score — внутренний ранжировочный балл (сумма похожестей соседей, взвешенная твоими оценками). Это не рейтинг 1–5, не вероятность и не «качество» фильма; сравнивать его можно только внутри одного пользователя и одной модели.

Жанры на карточках показаны только для интерпретации в интерфейсе — модель их не использует (item-CF смотрит лишь на поведение).

Матрица сходства: топ-15 популярных фильмов

Ярче клетка — сильнее «совместная» похожесть (часто оцениваются вместе). Диагональ приглушена (сходство фильма с самим собой = 1). Наведи на клетку — покажет точное значение.

Загружаем данные

Осторожно с интуицией. У популярных фильмов сходство часто завышено: их смотрит широкая аудитория, поэтому у них много общих зрителей — но это не всегда настоящая вкусовая близость, а popularity bias. Именно поэтому нужны shrinkage (мы уже применяем, λ=20), centering и осторожная интерпретация cosine.

Теория простым языком

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

Чем это отличается от content-based

Content-based смотрел внутрь фильма (жанры, теги). Здесь мы про содержание фильма не знаем ничего — смотрим только на то, кто что оценил. Этот подход так и называется:

Коллаборативная фильтрация — рекомендации, построенные на «коллективном» поведении пользователей. Слово «коллаборативная» = «совместная»: мы используем оценки многих людей, чтобы помочь одному.

Бывает двух видов: смотреть на похожих пользователей («такие же люди, как ты, любят…») или на похожие айтемы («к этому фильму обычно идёт вот этот»). Мы реализовали второй — item-based, и точно не потому, что айтемов «меньше»: у нас как раз наоборот, 610 пользователей и 9724 фильма.

Выбор здесь не универсальный, а из сравнения двух вещей. Первая — что быстрее устаревает: на каталоге фильмов похожесть двух картин почти не двигается от одного нового просмотра, а профиль человека двигается сразу; в новостной ленте всё наоборот — там айтемы живут часы. Вторая — что больше: матрица похожестей квадратична по своей стороне, и при 10⁸ пользователей user-user просто не соберётся, а при 10⁴ айтемов item-item соберётся легко. Предрассчитать можно обе; вопрос в том, как часто пересчитывать и влезает ли результат. На MovieLens обе стороны маленькие, так что item-based здесь — выбор учебный, а не вынужденный.

▸Как CF вырос из корреляции
БылоContent-based смотрел только на признаки айтема.
ПроблемаПризнаки бывают бедными, а похожесть по содержанию ≠ похожесть по вкусу.
ИдеяПохожесть выводим из поведения: сначала «похожие пользователи → усреднить их оценки», потом «похожие айтемы» — на каталоге, который меняется медленнее людей, такая матрица дольше остаётся свежей.
Стало лучшеКоллективная мудрость: персонализация без метаданных, ловит неочевидные связи между айтемами.
Осталось слабымРазреженность, холодный старт, popularity bias, шумные similarity на редких совместных оценках.
ДальшеMF / ALS — учить скрытые факторы вместо ручной similarity.

Когда два фильма считаются похожими

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

Косинусное сходство — число, показывающее, насколько «в одну сторону» смотрят два списка чисел. Здесь списки — это оценки фильмов разными людьми. Высокий косинус = фильмы оценивали одни и те же люди и похоже.
Пример. «Историю игрушек» и «Парк Юрского периода» жанрово роднит мало. Но их любят одни и те же зрители 90-х → по поведению они «похожи», и CF это улавливает, хотя content-based — нет.

Как получаются рекомендации

1. Берём фильмы, которые ты хорошо оценил (rating ≥ 4) — только лайки; низкие оценки в профиль не идут, чтобы нелюбимое не тянуло рекомендации вверх.
2. Для каждого находим самые похожие на него (по столбцам матрицы).
3. Складываем эти «голоса»: фильм, похожий сразу на несколько твоих любимых, получает высокий балл. Верхние по баллу и показываем (убрав уже виденные).

Если у пользователя нет оценок ≥ 4, откатываемся на всю его историю (иначе рекомендовать было бы не от чего). Первично здесь смысл: низкая оценка — не слабый лайк. Выигрыш в точности при этом есть, но маленький, и интервал его не отделяет от нуля:

Загружаем данные
kNN (k ближайших соседей) — способ рекомендовать через «соседей»: для айтема берут k самых похожих на него и опираются на них. «Сосед» здесь — это похожий фильм, а не человек рядом.
Псевдокод
# офлайн: считаем похожести между всеми парами фильмов один раз
нормируем каждый столбец-фильм матрицы оценок
sim[i][j] = cosine(столбец i, столбец j)

recommend(пользователь u, K):
    score = {}
    для каждого фильма j, который u оценил ≥ 4 (лайки):
        для каждого соседа i из топ-похожих на j:
            score[i] += sim[i][j] * r[u][j]
    # если лайков нет — берём всю историю u (fallback)
    убрать из score уже виденные u фильмы
    вернуть K фильмов с наибольшим score
▸Формула: похожесть и итоговый балл

Похожесть фильмов ii и jj — косинус их столбцов оценок r⋅ir_{\cdot i} и r⋅jr_{\cdot j}:

sim(i,j)=∑urui ruj∑urui2 ∑uruj2\mathrm{sim}(i,j) = \frac{\sum_{u} r_{ui}\, r_{uj}}{\sqrt{\sum_u r_{ui}^2}\,\sqrt{\sum_u r_{uj}^2}}

Балл фильма ii для пользователя uu — сумма похожестей на то, что uu лайкнул (оценка ≥ 4), взвешенная этими оценками:

score(u,i)=∑j : ruj ≥ 4sim(i,j) ruj\mathrm{score}(u,i) = \sum_{j\,:\, r_{uj}\,\ge\,4} \mathrm{sim}(i,j)\, r_{uj}

Так фильмы с низкой оценкой не дают вклада вовсе. Альтернативы: взять все оценки как вес (тогда 2★ даёт малый положительный вклад — методологически спорно) или центрировать ruj−rˉur_{uj}-\bar r_u (ниже среднего → минус). Мы берём positive-only из смысловых соображений; замер обоих вариантов — выше.

На практике сумму берут не по всем jj, а только по k ближайшим соседям фильма ii — отсюда «kNN». Сколько именно соседей оставлено у нас, написано в контракте обучения внизу страницы.

Сколько стоит усадка похожестей

Усадка sim′=nijnij+λ sim\mathrm{sim}' = \frac{n_{ij}}{n_{ij}+\lambda}\,\mathrm{sim} тянет к нулю похожести, посчитанные по двум-трём общим зрителям. Звучит разумно — но «звучит разумно» не значит «работает»:

Загружаем данные

Главные сложности

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

Хочется сказать: «зато item-CF обыгрывает popularity, вот он, выигрыш персонализации». Точечно так и есть — но стоит посчитать парный интервал, и уверенность исчезает:

Загружаем данные

Нижняя граница чуть ниже нуля, то есть на MovieLens в этом протоколе преимущество персонализации над «просто популярным» не доказано. Это не приговор методу: 363 оцениваемых пользователя — очень мало, и интервал такой ширины ожидаем. Но разница в третьем знаке между двумя моделями на такой выборке — ещё не результат, и подавать её как результат нельзя.

Сильные стороны

  • Реальная персонализация: учитывает твою историю.
  • Не нужны описания айтемов — достаточно поведения (но его надо корректно логировать: логи грязные, смещённые, без impressions — см. B1).
  • Похожести можно предрассчитать offline; если каталог меняется медленнее вкусов, пересчитывать их приходится реже, чем user-user.

Слабые стороны

  • Холодный старт: бесполезна для новых пользователей и айтемов.
  • Разреженность данных бьёт по качеству.
  • Память и вычисления растут с числом айтемов (похожести между всеми парами).

⚠️ Что может пойти не так

  • Разреженность: у двух фильмов может быть всего пара общих зрителей — косинус по такой выборке случаен. Помогает усадка (shrinkage): чем меньше общих оценок n_ij, тем сильнее похожесть тянут к нулю множителем n_ij/(n_ij+λ). Здесь это уже применено, λ = 20 — замер обоих вариантов чуть выше.
  • Косинус по сырым оценкам смещён: щедрый зритель, который всем ставит 5, раздувает похожести. Часто оценки сначала центрируют (вычитают среднее пользователя).
  • Холодный старт: новый фильм не с чем сравнить, новому пользователю — нечего складывать.
  • Популярные фильмы лезут в соседи ко всему подряд (их многие смотрели), поэтому без поправок item-CF тоже подвержен popularity bias.

🧠 Проверь себя: Каталог меняется медленно, а люди заходят каждый день и всё время что-то смотрят. Что это говорит о выборе между item-based и user-based?

Как подобраны ручки — и почему не по тесту

Загружаем данные

Конфигурация обучения

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

Загружаем данные

Что дальше

До финальной сборки не хватает ещё 11 модулей по этому пути.

Порядок здесь — рекомендация из карты курса, ничего не блокируется. Отметка «прочитано» хранится только в этом браузере.

Источники