Метрики точности и ранжирования
Урок вводит ключевые метрики качества рекомендаций — precision@k, recall@k, MAP и NDCG — и реализует их на чистом Python.
Метрика @k оценивает не весь прогноз, а только top-k выданных рекомендаций — потому что пользователь видит лишь верхушку списка.
Почему оцениваем именно топ
Рекомендатель выдаёт упорядоченный список, но человек смотрит первые несколько позиций. Поэтому метрики смотрят на top-k: насколько хороши первые k рекомендаций. Нам понадобится relevant — множество объектов, которые пользователю реально понравились (из отложенной тестовой части), и recommended — упорядоченный список модели.
Precision@k и Recall@k
Precision@k — какая доля из k показанных оказалась релевантной («не показали ли мы мусор»). Recall@k — какую долю всех релевантных мы поймали в top-k («не упустили ли мы нужное»). Это та же пара точность/полнота, что в учебнике «Машинное обучение», но обрезанная по k.
def precision_at_k(recommended, relevant, k):
rec_k = recommended[:k]
hits = sum(1 for x in rec_k if x in relevant)
return hits / k
def recall_at_k(recommended, relevant, k):
rec_k = recommended[:k]
hits = sum(1 for x in rec_k if x in relevant)
return hits / len(relevant) if relevant else 0.0
recommended = ["A", "B", "C", "D", "E"]
relevant = {"B", "D", "F"}
print("P@3 =", round(precision_at_k(recommended, relevant, 3), 3))
print("R@5 =", round(recall_at_k(recommended, relevant, 5), 3))Вывод:
P@3 = 0.333 R@5 = 0.667
MAP: учитываем позицию попаданий
Precision@k не различает, попал релевантный объект на первое место или на пятое. Average Precision (AP) награждает за то, что релевантные стоят выше, а MAP усредняет AP по всем пользователям.
def average_precision(recommended, relevant, k):
score, hits = 0.0, 0
for i, item in enumerate(recommended[:k], start=1):
if item in relevant:
hits += 1
score += hits / i
return score / min(len(relevant), k) if relevant else 0.0
users = [
(["A", "B", "C", "D", "E"], {"B", "D", "F"}),
(["X", "Y", "Z"], {"X", "Z"}),
]
maps = [average_precision(r, rel, 5) for r, rel in users]
print("AP user1 =", round(maps[0], 3))
print("AP user2 =", round(maps[1], 3))
print("MAP =", round(sum(maps) / len(maps), 3))Вывод:
AP user1 = 0.333 AP user2 = 0.833 MAP = 0.583
NDCG: градуированная релевантность
Иногда релевантность не бинарна: один фильм «понравился», другой «обожаю». NDCG учитывает градуированную полезность и логарифмически штрафует за низкую позицию, а затем нормирует на идеальный порядок (значение 1 = идеально отсортировано).
import math
def dcg(rels):
return sum(rel / math.log2(i + 2) for i, rel in enumerate(rels))
def ndcg(rels):
ideal = sorted(rels, reverse=True)
idcg = dcg(ideal)
return dcg(rels) / idcg if idcg else 0.0
rels = [3, 2, 0, 1, 2] # релевантность выданного списка по позициям
print("DCG =", round(dcg(rels), 3))
print("NDCG =", round(ndcg(rels), 3))Вывод:
DCG = 5.466 NDCG = 0.96
Как работает под капотом
Все эти метрики считают на отложенной выборке: часть взаимодействий прячут, обучают модель на остальном, а затем проверяют, попали ли спрятанные объекты в top-k. Для неявных данных берут ранжирующие метрики (precision/recall/MAP/NDCG), а не RMSE оценок. Усреднение всегда идёт по пользователям, а не по всем строкам, иначе активные пользователи перетянут результат на себя.
Частые ошибки
- Считать метрику по всему списку. Пользователь видит топ; оценивайте @k.
- Применять RMSE к неявным данным. Там нет «правильной оценки» — нужны ранжирующие метрики.
- Усреднять по взаимодействиям, а не по пользователям. Иначе несколько сверхактивных пользователей исказят картину.
Итоги
- Метрики @k оценивают только видимую пользователю верхушку списка.
- Precision@k ловит мусор, Recall@k ловит упущенное.
- MAP и NDCG награждают за то, что релевантное стоит выше.
- Метрики считают на отложенной выборке и усредняют по пользователям.