ГлавнаяБлог3 ошибки в алгоритмах рекомендаций и как их исправить
Алгоритмы

3 ошибки в алгоритмах рекомендаций и как их исправить

Разбираем 3 реальные ошибки в алгоритмах рекомендаций на Python. Узнайте, почему Jaccard вредит ранжированию и как заменить его на recall. Примените паттерны в своём проекте.

Al
Редакция Algolitalgolit.ru
8 мин чтения13 июня 2026 г.

Почему ваш рекомендательный алгоритм работает плохо?

Вы написали красивый матчинг на косинусной близости и Jaccard, но в продакшене результаты выглядят случайными? Знакомая ситуация. Я прошёл этот путь на платформе OTONAMI, которая подбирает независимых артистов для музыкальных кураторов. После серии провалов родились три простых, но критических решения. Они превратили демо-алгоритм в рабочий инструмент. Разберём каждое на примерах кода на Python.

Структура задачи: как устроен скоринг

Матчинг трека и куратора считается по взвешенной сумме трёх суб-скорров:

score = genre_score * w_genre + mood_score * w_mood + audio_score * w_audio

Каждый суб-скорр лежит в диапазоне [0, 1]. Жанр — самый сильный предиктор, настроение — вторичный сигнал, аудио — скорее тай-брейкер. Разумные веса: 0.55 / 0.25 / 0.20. Но главное — не веса, а то, как считаются суб-скорры.

Решение 1 — Жанр считаем через recall, а не Jaccard

Интуиция подсказывает использовать Jaccard similarity: пересечение множеств жанров трека и куратора, делённое на объединение. Симметрично, математически красиво. И абсолютно неправильно для этой задачи.

Представьте куратора, который принимает 10 жанров — широкий профиль. Трек попадает ровно в один из них. Это идеальное совпадение! Но Jaccard даст 1 / 10 = 0.1, потому что объединение огромно. Чем шире куратор, тем сильнее Jaccard его наказывает. Полная противоположность нужному поведению.

Вопрос не симметричен. Он звучит так: попадает ли трек в жанровую нишу куратора? Поэтому жанровый скорр — это recall по жанрам трека: какая доля жанров трека покрыта куратором.

def genre_score(track_genres: list[str], curator_genres: list[str], open_to_all: bool) -> float:
    if open_to_all:
        return 1.0
    track_set = set(normalize(track_genres))
    if not track_set:
        return 0.5  # см. Решение 3
    curator_set = set(normalize(curator_genres))
    covered = len(track_set & curator_set)
    return covered / len(track_set)

Широкий куратор больше не наказывается за широту. Асимметрия реального вопроса встроена в метрику.

Решение 2 — Темп исключён из аудио-вектора намеренно

Аудио-скорр — это косинусная близость по вектору признаков: energy, danceability, acousticness, instrumentalness, valence. Шестое измерение, которое напрашивается — tempo. Я его исключил сознательно.

Автоматическое определение BPM ненадёжно и разрушительно для метрики расстояния: оно систематически ошибается в два раза (half-time/double-time). Медленная баллада на 60 BPM часто читается как 120 BPM. Когда это удвоенное значение попадает в вектор и вы вычисляете расстояние, оно не просто добавляет шум — оно пробивает дыру в скоре. Два трека, которые должны быть рядом, вдруг оказываются далеки по одной оси.

В ранней версии это вызывало баг: около 22% матчей схлопывались в плоский, бессмысленный скор. Почти всегда виноват был темп. Удаление темпа из скоринга сразу убрало целый класс ложных отрицаний.

def cosine_similarity(a: dict, b: dict, dimensions: list[str]) -> float:
    dot = 0.0
    mag_a = 0.0
    mag_b = 0.0
    for dim in dimensions:
        x = a.get(dim, 0)
        y = b.get(dim, 0)
        dot += x * y
        mag_a += x * x
        mag_b += y * y
    if mag_a == 0 or mag_b == 0:
        return 0.0
    return dot / (mag_a ** 0.5 * mag_b ** 0.5)

# Используем только проверенные измерения
AUDIO_DIMS = ['energy', 'danceability', 'acousticness', 'instrumentalness', 'valence']

Темп может показываться в UI для человека («похожая энергия и темп»), но никогда не должен влиять на скор. Держите объяснение и скоринг разделёнными.

Решение 3 — Пропущенные данные нейтральны, а не наказание

Независимая музыка полна пробелов. У многих треков нет надёжного аудиоанализа. Многие кураторы не заполнили теги настроения. Наивное решение — считать пропуск за ноль — тихо хоронит каждого нового или малоизученного артиста на дне рейтинга.

Поэтому, когда с какой-либо стороны нет аудио или настроений, этот фактор возвращает нейтральное значение 0.5. Оно не помогает и не вредит.

def audio_score(track_audio: dict | None, curator_audio: dict | None) -> float:
    if not track_audio or not curator_audio:
        return 0.5  # отсутствие сигнала — не evidence плохого совпадения
    return cosine_similarity(track_audio, curator_audio, AUDIO_DIMS)

Отсутствие доказательств — не доказательство плохого соответствия. Эта одна строка защищает самых новых артистов, которых и должна находить платформа для открытий.

Практический вывод: что делать прямо сейчас

Ни одно из этих решений не является хитрым алгоритмом. Это маленькие, скучные страховки: recall вместо Jaccard, одно удалённое измерение, один нейтральный fallback. Но каждое родилось из наблюдения за реальными ранжированиями, которые шли не так. Вместе они составляют основную разницу между матчером, который работает, и тем, который просто выполняется.

Ваше задание на сегодня:

  • Перепишите свой жанровый скорр с Jaccard на recall. Посмотрите, как изменится ранжирование для широких категорий.
  • Проверьте, не используете ли вы темп или другую шумную фичу в аудио-векторе. Удалите её и замерьте метрики.
  • Найдите места, где вы обнуляете пропущенные данные. Замените на нейтральное значение (0.5). Протестируйте на новых объектах.

Полный код библиотеки с типизацией доступен в репозитории music-matching-patterns. Применяйте эти паттерны в своих рекомендательных системах и не повторяйте чужих ошибок.

#рекомендательные алгоритмы#Jaccard#косинусная близость#Python#обработка данных
Al
Редакция Algolit

Пишем про алгоритмы, подготовку к собеседованиям и карьеру в IT — так, чтобы было понятно и полезно.

Хочешь закрепить знания на практике?

Решай задачи на Algolit — интерактивная платформа для обучения

Начать бесплатно →
3 ошибки в алгоритмах рекомендаций и как их исправить | Algolit