Diversity и Fairness
Фильтр-пузыри, diversity-aware ранжирование, exploration vs exploitation.
Diversity и Fairness — как не превратить рекомендации в ловушку
Загрузка интерактивного виджета...
Ты слушаешь один трек — и Spotify начинает крутить похожие. Через неделю весь Discover Weekly — один жанр. Ты покупаешь корм для кота — и Amazon месяц показывает только кошачьи товары. Это фильтр-пузырь (filter bubble): рекомендательная система, оптимизированная на релевантность, сужает мир пользователя до узкой ниши.
Фильтр-пузырь — не просто дискомфорт. Это прямой ущерб бизнесу: пользователь скучает, перестаёт кликать, уходит. YouTube внутри называет это «rabbit hole problem» — зритель попадает в спираль всё более однотипного контента, а потом выгорает. Борьба с этим — задача diversity (разнообразия) и fairness (честности) рекомендаций.
В этой ноде: почему одна релевантность губит систему → MMR и DPP как инструменты разнообразия → fairness и popularity bias → exploration-стратегии. Это знание отличает junior-инженера («я обучил модель, NDCG отличный») от middle/senior («я построил систему, которая не деградирует со временем»).
Большая картина: почему релевантность — не всё
Представь: модель предсказывает CTR и выдаёт топ-10 по убыванию вероятности клика. Что происходит?
Шаг 1. Popularity bias. Популярные айтемы накопили больше кликов → модель уверена в их CTR → они попадают в топ чаще → собирают ещё больше кликов. Это замкнутый цикл (feedback loop). 80% показов достаётся 1% каталога. Длинный хвост (long tail) — тысячи нишевых товаров — никогда не показывается.
Шаг 2. Однообразие. Даже если модель идеально знает вкусы пользователя, 10 идентичных кроссовок в выдаче — ужасный UX. Пользователь хочет набор, а не 10 копий лучшего айтема. В ресторане ты не заказываешь 5 тарелок одного блюда — ты хочешь меню.
Шаг 3. Стагнация. Система рекомендует → пользователь кликает → система учится на кликах → рекомендует ещё уже. Exploration (показ нового) падает до нуля, модель перестаёт узнавать новое о пользователе. Его вкусы меняются, а модель застряла в прошлом.

Вывод: оптимизировать только relevance — это оптимизировать краткосрочный CTR ценой долгосрочного engagement. Diversity, fairness и exploration — механизмы, которые удерживают систему здоровой.
MMR — Maximal Marginal Relevance
MMR — самый простой и самый популярный способ добавить diversity. Идея: выбираем следующий айтем в список так, чтобы он был релевантен пользователю и одновременно отличался от уже выбранных.
Аналогия: ты составляешь плейлист для вечеринки. Каждый следующий трек должен быть хорошим (релевантность) и непохожим на предыдущие (diversity). Если поставил три рок-хита подряд — следующим лучше добавить электронику, даже если четвёртый рок-трек чуть популярнее.
rel(i) — релевантность айтема i для пользователя, sim(i,j) — сходство с уже выбранным j ∈ S, λ — баланс: 1 = только релевантность, 0 = только разнообразие
Алгоритм — жадный: 1. Начинаем с пустого S. Первый айтем — самый релевантный 2. На каждом шаге из оставшихся кандидатов выбираем i с максимальным MMR(i) 3. Добавляем i в S, повторяем пока |S| = K Сложность: O(K·N), где N — число кандидатов. На практике кандидатов 100-1000 (после стадии retrieval), так что это быстро.
Lambda (λ) — ключевой гиперпараметр. На практике λ ≈ 0.5–0.7. Подбирается через A/B-тест по бизнес-метрикам (session length, retention), а не по offline NDCG. Маленький λ — слишком случайные рекомендации, большой λ — нет эффекта.
import numpy as np
from typing import List, Tuple
def mmr_rerank(
relevance: np.ndarray, # (N,) — скоры релевантности
embeddings: np.ndarray, # (N, dim) — эмбеддинги айтемов
lam: float = 0.6,
k: int = 10,
) -> List[int]:
"""MMR re-ranking: жадно выбираем K айтемов."""
# Нормализуем эмбеддинги для косинусного сходства
norms = np.linalg.norm(embeddings, axis=1, keepdims=True) + 1e-8
emb_norm = embeddings / norms
selected: List[int] = []
remaining = set(range(len(relevance)))
for _ in range(min(k, len(relevance))):
best_score, best_idx = -np.inf, -1
for idx in remaining:
if selected:
sims = emb_norm[idx] @ emb_norm[selected].T
max_sim = float(np.max(sims))
else:
max_sim = 0.0
score = lam * relevance[idx] - (1 - lam) * max_sim
if score > best_score:
best_score, best_idx = score, idx
selected.append(best_idx)
remaining.discard(best_idx)
return selected
# Пример: 5 айтемов, первые три — кроссовки (похожие эмбеддинги)
rel = np.array([0.95, 0.90, 0.85, 0.70, 0.65])
embs = np.array([
[1.0, 0.1], [0.9, 0.15], [0.95, 0.12], # кроссовки
[0.1, 0.9], [0.2, 0.85], # куртки
])
print(mmr_rerank(rel, embs, lam=1.0, k=3)) # [0,1,2] — три кроссовки
print(mmr_rerank(rel, embs, lam=0.5, k=3)) # [0,3,1] — разнообразие!DPP — Determinantal Point Processes
MMR — жадный: каждый шаг оптимален локально, но не глобально. DPP (Determinantal Point Process) решает задачу diversity на уровне всего набора: вероятность выбрать подмножество S пропорциональна определителю подматрицы ядра L.
Аналогия: представь, что ты расставляешь магниты на столе. Одноимённые полюса отталкиваются — магниты естественно распределяются равномерно по поверхности. DPP работает так же: «похожие» айтемы «отталкиваются», и модель автоматически выбирает разнообразный набор.
L — ядро (kernel matrix), q_i — качество (релевантность) айтема i, φ_i — нормализованный вектор признаков. det(L_S) → 0, если айтемы в S похожи (строки/столбцы пропорциональны)
Почему DPP лучше MMR? MMR жадный — выбирает локально оптимально. DPP оценивает всё подмножество целиком — комбинация из 10 айтемов может быть лучше, даже если каждый отдельный уступает. DPP имеет вероятностную интерпретацию (можно семплировать разные наборы) и естественно моделирует repulsion — определитель падает, когда строки коррелируют.
Почему DPP используют реже? Точный inference O(N³), жадная аппроксимация O(N·K²) vs O(N·K) у MMR. Сложнее в реализации, а разница с хорошо настроенным MMR часто невелика. Вывод: MMR — рабочая лошадка для 90% случаев. DPP — когда diversity критична (новостные ленты, поиск) и бюджет на latency позволяет.
Fairness — честность рекомендаций
Diversity — про UX: «покажи пользователю разное». Fairness — про этику и бизнес: «не дискриминируй ни пользователей, ни поставщиков контента». Рекомендательная система распределяет внимание (attention), а внимание = деньги.
Exposure bias (перекос показов). Маркетплейс: 100 продавцов, но 90% показов достаются 5 крупнейшим. Остальные 95 продавцов не получают трафика → уходят → ассортимент сужается → пользователь получает меньше выбора → платформа деградирует. Это не просто «нечестно» — это разрушает экосистему.
Provider fairness (честность к поставщикам). Каждый продавец/автор заслуживает fair share показов, пропорциональных качеству. Метрики: exposure parity (равномерность показов между группами), amortized fairness (на одном запросе можно быть «нечестным», но за N запросов показы выравниваются), envy-freeness (ни один поставщик не предпочёл бы оказаться на месте другого).
Consumer fairness (честность к пользователям). Модель не должна давать худшие рекомендации определённым группам: новичкам (мало истории), пользователям из «небогатых» регионов, определённым демографическим группам. Gender/race bias в рекомендациях вакансий — реальная проблема с юридическими последствиями.
import numpy as np
def exposure_fairness(
recommendations: list[list[int]], # список рекомендаций для N юзеров
item_groups: dict[int, str], # item_id → группа поставщика
) -> dict[str, float]:
"""Считаем долю показов для каждой группы поставщиков."""
from collections import Counter
total = Counter()
for recs in recommendations:
for item in recs:
total[item_groups.get(item, "unknown")] += 1
n_total = sum(total.values())
return {group: count / n_total for group, count in total.items()}
# Пример: 3 группы поставщиков, 1000 запросов по 10 айтемов
# Идеально: ~33% на каждую. Реальность: крупный поставщик забирает 70%
# exposure = {"big_seller": 0.72, "medium": 0.21, "small": 0.07} ← проблемаPopularity bias и long tail
Popularity bias — конкретный и самый частый вид unfairness в рекомендациях. Распределение взаимодействий следует степенному закону (power law): 1% айтемов собирают 80% кликов. Остальные 99% — это long tail, нишевые товары, которые в совокупности могут быть ценнее головы распределения.
Почему модели усиливают bias: (1) больше данных → лучше предсказание — модель уверена в CTR популярных, но не уверена в нишевых; (2) feedback loop — популярные показываются чаще → больше кликов → кажутся ещё лучше; (3) sparse signal — для айтема с 10 взаимодействиями нельзя выучить хороший эмбеддинг.
Calibrated recommendations (Steck, 2018) — элегантный подход: распределение категорий в рекомендациях должно соответствовать реальным вкусам пользователя. Если 60% истории пользователя — рок и 40% — джаз, то и в рекомендациях должно быть примерно 60/40, а не 100% рок.
KL-дивергенция между p (распределение жанров в истории юзера) и q (распределение жанров в рекомендациях). Минимизируем CKL → калиброванные рекомендации
- IPS (Inverse Propensity Scoring) — вес нишевых айтемов при обучении: weight = 1/P(shown). Редкий айтем → высокий вес
- Causal debiasing — popularity как confounding variable, вычитаем её эффект
- Popularity-aware regularization — штраф за рекомендацию и без того популярных
- Post-processing — не более N айтемов из одной категории в выдаче
Exploration — как выбраться из пузыря
Все предыдущие методы (MMR, DPP, calibration) работают на этапе re-ranking — перетасовывают уже сгенерированных кандидатов. Exploration идёт дальше: целенаправленно показывает айтемы, в которых модель не уверена, чтобы собрать новый сигнал.
Это классическая дилемма exploration vs exploitation: exploitation — рекомендуй то, что точно зайдёт (по текущей модели), exploration — покажи что-то новое, чтобы узнать вкусы лучше. Как в ресторане: можно всегда заказывать любимую пиццу (exploitation), а можно попробовать суши (exploration). Без exploration ты никогда не узнаешь, что суши тебе нравятся больше.
ε-greedy — простейшая стратегия: с вероятностью (1−ε) рекомендуем топ по модели, с вероятностью ε показываем случайный айтем. Типичное ε = 0.05–0.1. Плюс: тривиально реализовать. Минус: случайный explore неэффективен — лучше исследовать там, где uncertainty максимальна.
Thompson Sampling — умнее: для каждого айтема моделируем распределение CTR (не точечную оценку). На каждом запросе семплируем из распределений и показываем айтем с наибольшим семплом. Айтем с высоким средним CTR часто выигрывает (exploit), а с широким распределением (мало данных) — иногда тоже (explore). Чем больше данных — тем уже распределение → exploration снижается автоматически.
import numpy as np
class ThompsonSamplingRecommender:
"""Thompson Sampling для выбора из K кандидатов."""
def __init__(self, n_items: int):
# Beta(alpha, beta) prior для CTR каждого айтема
self.alpha = np.ones(n_items) # число кликов + 1
self.beta = np.ones(n_items) # число не-кликов + 1
def recommend(self, candidates: list[int], k: int = 5) -> list[int]:
# Семплируем CTR из апостериорного Beta-распределения
sampled_ctr = np.random.beta(
self.alpha[candidates], self.beta[candidates]
)
# Показываем топ-K по семплированному CTR
top_k = np.argsort(sampled_ctr)[-k:][::-1]
return [candidates[i] for i in top_k]
def update(self, item: int, clicked: bool):
if clicked:
self.alpha[item] += 1 # больше кликов → распределение сдвигается вправо
else:
self.beta[item] += 1 # больше показов без кликов → сдвигается влево
# Новый айтем: alpha=1, beta=1 → широкое распределение → часто explore
# Популярный: alpha=500, beta=1500 → узкое около 0.25 → стабильный exploitContextual bandits — ещё умнее: учитывают контекст (фичи пользователя, время суток, устройство). Модель не просто оценивает средний CTR айтема, а предсказывает CTR для данного пользователя в данный момент. LinUCB и NeuralUCB — популярные алгоритмы. В контексте diversity: bandit-подход позволяет exploration подстраивать под конкретного юзера — новичку больше explore, опытному — меньше.
Метрики diversity: как измерить разнообразие
- ILS (Intra-List Similarity) — среднее попарное сходство айтемов в списке. ILS = 0 → все разные, ILS = 1 → все одинаковые. Target: < 0.5
- Coverage — какой % каталога рекомендуется хоть кому-то. 5% = модель игнорирует 95% айтемов
- Novelty — средняя «неожиданность»: novelty(i) = −log₂(popularity(i)). Популярные дают низкую novelty
- Calibration — KL-дивергенция между распределением категорий в истории и в рекомендациях. 0 = идеально
- Serendipity — рекомендация, которая и неожиданна, и понравилась. Измеряется только онлайн
На практике diversity-метрики трекают рядом с качеством (NDCG, Hit Rate). Типичный A/B: «MMR с λ=0.5 vs baseline — NDCG −2%, но session length +5% и coverage +15%». Бизнес решает: стоит ли потеря точности выигрыша в engagement.
🎯 На собеседовании
Junior
Middle
Senior
Собираем всё вместе
Relevance — необходимое, но недостаточное условие хорошей рекомендательной системы. Без diversity пользователь попадает в filter bubble и теряет интерес. Без fairness платформа теряет поставщиков и деградирует. Без exploration модель перестаёт учиться.
Инструментарий: MMR — рабочая лошадка для re-ranking (λ-баланс между relevance и diversity). DPP — когда нужна глобальная оптимизация набора. Calibrated recommendations — чтобы рекомендации отражали реальные вкусы. Thompson Sampling / contextual bandits — для умного exploration.
Если запомнить одну вещь: хорошая рекомендательная система оптимизирует долгосрочный engagement, а не краткосрочный CTR. Diversity и fairness — не «nice to have», а часть бизнес-метрики. Дальше: Multi-Stage Ranking покажет, где именно в pipeline встраивается diversity, а A/B-тестирование — как измерять бизнес-эффект от diversity.
Материалы
Ключевая статья: жанры в рекомендациях должны совпадать с жанрами в истории юзера. Netflix.
Формализация fairness для поставщиков через exposure allocation. Amortized fairness.
Практические методы борьбы с popularity bias на этапе re-ranking.
Обзор bandit-подходов для exploration в рекомендациях: ε-greedy, Thompson Sampling, LinUCB.
Глава про рекомендательные системы с кодом на PyTorch. Разделы про diversity и evaluation.