User-based коллаборативная фильтрация

Урок реализует user-based коллаборативную фильтрацию целиком: находим похожих пользователей и предсказываем оценки взвешенным усреднением их мнений.

User-based CF предсказывает оценку пользователя для товара как взвешенное среднее оценок похожих на него пользователей, где вес — мера похожести.

Алгоритм по шагам

Чтобы предсказать, как целевой пользователь оценит товар, который он ещё не видел, мы:

  1. считаем похожесть целевого пользователя со всеми остальными;
  2. среди тех, кто оценил этот товар, берём оценки;
  3. усредняем их с весами, равными похожести.

Формула прогноза: сумма по соседям sim(u, v) * rating(v, i), делённая на сумму sim(u, v). Чем больше похож сосед — тем сильнее его голос.

Полная реализация

import math

ratings = {
    "Аня":   {"Матрица": 5, "Титаник": 2, "Аватар": 4, "Шрек": 1},
    "Борис": {"Матрица": 4, "Титаник": 1, "Аватар": 5},
    "Вера":  {"Матрица": 1, "Титаник": 5, "Шрек": 4},
    "Глеб":  {"Матрица": 5, "Аватар": 4, "Шрек": 1},
}

def cosine(a, b):
    common = set(a) & set(b)
    if not common:
        return 0.0
    num = sum(a[i] * b[i] for i 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

target = "Борис"
sims = {u: cosine(ratings[target], ratings[u]) for u in ratings if u != target}
print("Похожесть на Бориса:")
for u in sorted(sims, key=lambda x: -sims[x]):
    print(f"  {u}: {round(sims[u], 3)}")

# предсказываем оценки для невиденных Борисом фильмов
seen = set(ratings[target])
candidates = set()
for u in ratings:
    if u != target:
        candidates |= set(ratings[u])
candidates -= seen

preds = {}
for movie in candidates:
    num = den = 0.0
    for u in ratings:
        if u != target and movie in ratings[u]:
            num += sims[u] * ratings[u][movie]
            den += sims[u]
    if den > 0:
        preds[movie] = num / den

print("\nПрогноз для Бориса:")
for m in sorted(preds, key=lambda x: -preds[x]):
    print(f"  {m}: {round(preds[m], 2)}")

Вывод:

Похожесть на Бориса:
  Аня: 0.956
  Глеб: 0.952
  Вера: 0.214

Прогноз для Бориса:
  Шрек: 1.3

Борис ближе всего к Ане и Глебу, у которых «Шрек» получил низкие оценки, поэтому прогноз по «Шреку» низкий — мы его рекомендовать не станем.

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

В чистом виде user-based CF плохо масштабируется: для миллионов пользователей считать попарную похожесть со всеми невозможно. На практике сужают круг — берут только top-k самых похожих соседей и/или ограничивают кандидатов теми, кто пересекается с целевым хотя бы по нескольким товарам. Ещё одна тонкость: профили пользователей быстро меняются (сегодня посмотрел детектив, завтра комедию), поэтому матрицу похожести приходится часто пересчитывать — это и есть слабое место user-based по сравнению с item-based.

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

  • Не нормировать на сумму похожестей. Без деления на sum(sim) прогноз раздувается и теряет шкалу.
  • Включать соседей с нулевой или отрицательной похожестью. Их вклад только зашумляет прогноз; отсекайте по порогу.
  • Пересчитывать всё на каждый запрос. Матрицу похожести считают офлайн и кешируют.

Итоги

  • User-based CF предсказывает оценку как взвешенное среднее по похожим пользователям.
  • Вес соседа равен его похожести на целевого пользователя.
  • Нормировка на сумму похожестей возвращает прогноз в исходную шкалу.
  • Подход плохо масштабируется и чувствителен к смене вкусов — отсюда top-k и кеширование.
Проверьте себя
1. Как user-based CF предсказывает оценку товара?
AБерёт оценку самого активного пользователя
BУсредняет оценки похожих пользователей с весами, равными их похожести
CБерёт среднее по всему каталогу
DВыбирает случайную оценку
2. Почему user-based CF плохо масштабируется?
AОценки всегда отрицательны
BПрофили пользователей быстро меняются, а попарная похожесть по миллионам пользователей дорога
CКосинус нельзя посчитать на больших данных
DОна требует знать контент товаров