← к модулям

Как устроен эксперимент

Любые цифры метрик в этом приложении получены по одному и тому же протоколу. Чтобы им верить (и понимать их пределы), важно знать, как мы делим данные, что считаем «правильным ответом» и где этот протокол хитрит.

Почему нужен честный протокол оценки
БылоМожно было просто перемешать оценки и посчитать метрику.
ПроблемаRandom split легко даёт утечку и слишком оптимистичную оценку.
ИдеяИмитировать будущее хотя бы внутри пользователя: учимся на его прошлых взаимодействиях, проверяем на последнем.
Стало лучшеОценка ближе к настоящей задаче — «что рекомендовать дальше».
Осталось слабымLeave-last-out не равен полноценному global temporal split (события разных пользователей пересекаются во времени); offline ≠ online; один test-айтем упрощает реальность.
ДальшеTemporal split — темпоральная валидация; impression-aware негативы — данные и логи; и A/B (в следующих модулях).

Данные

Датасет MovieLens-small: 610 пользователей, 9724 фильма, ~100 000 оценок по шкале 0.5–5.

Explicit feedback (явный отклик)пользователь прямо выставил оценку (звёзды). Это не то же самое, что просто «посмотрел» — у нас есть и факт взаимодействия, и его сила.

Что считается «правильным ответом»

Чтобы проверить модель, надо знать, что для пользователя было бы хорошей рекомендацией. Мы берём за «правильный ответ» фильм, который он реально оценил высоко.

Порог релевантностиминимальная оценка, при которой фильм считается «понравившимся». По умолчанию ≥ 4 из 5. Если последний фильм пользователя оценён ниже — это не «релевантный» айтем, и такой пользователь в подсчёте точности не участвует. Порог можно менять на странице метрик и смотреть, как плывут числа.

Поэтому в оценке участвуют не все 610 человек, а только те (~360), чей спрятанный фильм понравился. Это честнее, чем награждать модель за угадывание фильма, который человек сам оценил на 1–2 звезды.

Как делим на train и test

Leave-last-outу каждого пользователя прячем его последнее по времени взаимодействие — оно уходит в test, всё остальное остаётся в train. Модель учится на прошлом и пытается предсказать «следующий» фильм. Это имитирует реальную задачу: предсказать будущее, а не подсмотреть его.

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

⚠️ Где наш протокол хитрит (и это нормально для учебного offline)

Утечка времени между пользователями. Мы прячем последний фильм укаждого человека по его личной хронологии. Но спрятанный у Ани фильм мог попасть в train к Боре, который посмотрел его позже Ани. Глобально по времени это не совсем честно. Более строгий вариант — единый временной срез по всем пользователям (global temporal split).

Нет отдельной валидации. Гиперпараметры моделей (число факторов, регуляризация) заданы руками и ни под что не подбираются — поэтому утечки от тюнинга нет. Но в «настоящем» эксперименте нужен отдельный validation-набор для подбора, отдельный от test.

Как считаем и усредняем

Для каждого оцениваемого пользователя строим топ-K рекомендаций, проверяем, попал ли туда спрятанный фильм, считаем метрики и усредняем по пользователям. K по умолчанию = 10.

⚠️ Одна метрика — разные реализации

«Recall@10» или «NDCG@10» в разных библиотеках и статьях считаютсяпо-разному: как обрабатывают ничьи в ранжировании, как усредняют, что считают релевантным, как нормируют. Поэтому цифры из двух источников нельзя сравнивать вслепую — всегда нужно знать точный протокол. Сравнивать осмысленно можно только модели внутри одного протокола — как здесь.

Неожиданный вывод про popularity

Когда «релевантно» = «понравилось» (порог ≥ 4), простой popularityстановится на удивление сильным: популярные фильмы непропорционально часто получают высокие оценки, поэтому «угадать понравившийся» ему легко. На этой задаче он даже обходит matrix factorization по NDCG.

Пример. Это и есть popularity bias в оценке: метрика, завязанная на популярное, льстит наивному baseline. Поэтому на одну accuracy-метрику нельзя смотреть в вакууме — смотри ещё coverage, novelty и diversity на странице метрик.