RecSys · учебник
Тренажёр Виджеты Повторение О проекте Все главы ← Фильтры Обзор

Дополнение · X-алгоритм · страница 8 из 11

Разметка и видимость: можно ли показать этот пост

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

Коротко

  • Три возможных ответа: показать, показать за заглушкой, не показывать. Не два — промежуточный вариант существует и важен.
  • Первый запрет обрывает проверку. Остальные правила уже не вычисляются, а в вердикт записывается имя решившего.
  • Для рекомендаций действует дополнительный набор правил, и он может только запрещать. Один и тот же пост подписчику показывается, а незнакомому — нет.
  • Репутация аккаунта считается тремя разными способами: по реакции других на его посты, по структуре графа подписок и по последовательности его собственных действий.
  • Детектор ботов — тот же трансформер над последовательностью действий, что и рекомендательная модель. Одна архитектура, противоположные задачи.

1. Три ответа вместо двух

pub enum VfAction {
    Allow,
    Drop(FilteredReason),
    Interstitial(FilteredReason),
}

Фрагмент из mod.rs · код X, Apache 2.0, коммит 28e414f

Промежуточный вариант — заглушка, которую пользователь может нажать и посмотреть содержимое, — это не мелочь интерфейса, а важное проектное решение.

Зачем нужен третий вариант

Представьте систему только с двумя ответами. Тогда для каждого спорного случая — откровенное изображение, сцена насилия, шокирующее фото — приходится выбирать между «показать всем» и «не показать никому». Оба варианта плохи: первый бьёт по тем, кто такого не хочет, второй по тем, кто осознанно на это подписан.

Заглушка разрывает эту дилемму, перенося решение на пользователя. Система говорит «здесь то, что может быть вам неприятно», а дальше человек решает сам.

Инженерное следствие важнее этического: при трёх ответах цена ошибки классификатора резко падает. Модель, определяющая откровенный контент с точностью 90%, в двухвариантной системе на каждой десятой ошибке либо показывает лишнее, либо скрывает нормальное. С заглушкой ошибка первого рода стоит одного лишнего нажатия. Это позволяет ставить порог агрессивнее и ловить больше.

2. Как вычисляется вердикт

fn evaluate_rules(rules: &[Box<dyn Rule>], context: &RuleContext<'_>) -> Verdict {
    let mut worst = VfAction::Allow;
    let mut decided_by = None;
    for rule in rules {
        match rule.evaluate(context) {
            VfAction::Drop(reason) => {
                return Verdict { action: VfAction::Drop(reason), decided_by: Some(rule.name()) };
            }
            VfAction::Interstitial(reason) => {
                if matches!(worst, VfAction::Allow) {
                    worst = VfAction::Interstitial(reason);
                    decided_by = Some(rule.name());
                }
            }
            VfAction::Allow => {}
        }
    }
    ...
}

Фрагмент из mod.rs · код X, Apache 2.0, коммит 28e414f

Три свойства, каждое со смыслом.

Почему важно, кто именно решил

В вердикт записывается decided_by — имя правила. Казалось бы, зачем: результат один и тот же.

Затем, что без этого поля система становится неотлаживаемой. Вопрос «почему мой пост не показывают» без него имеет ответ «сработало какое-то из двадцати девяти правил». С ним — «сработало вот это конкретное».

И именно это поле делает возможным отчёт пользователю, о котором ниже. Прозрачность здесь не приделана сбоку, а следует из того, что вердикт с самого начала сохраняет причину.

3. Два набора правил: почему подписчику показывают больше

Правила разбиты на политики по уровню строгости:

pub struct Policies {
    filter_all: Vec<Box<dyn Rule>>,
    timeline_home: Vec<Box<dyn Rule>>,
    timeline_home_recommendations: Vec<Box<dyn Rule>>,
}

Фрагмент из registry.rs · код X, Apache 2.0, коммит 28e414f

Базовый набор — 29 правил, применяемых ко всему в ленте. Второй набор — дополнительный, только для рекомендаций, то есть постов от тех, на кого зритель не подписан.

Посмотрите на состав второго набора — в нём систематически встречается слово «high recall»:

ПравилоЧто означает
SPAM_HIGH_RECALL_DROPСпам, пойманный детектором с высокой полнотой
NSFW_HIGH_RECALL_DROPВзрослый контент, пойманный с высокой полнотой
NSFW_HIGH_PRECISION_DROPТо же, но с высокой точностью
DO_NOT_AMPLIFY_DROPЯвная пометка «не усиливать»
MALICIOUS_URL_DROPВредоносная ссылка
COMPROMISED_USER_DROPАккаунт, признанный взломанным
Высокая полнота против высокой точности — и как из этого сделали продукт

Мы разбирали этот компромисс в главе «AUC и калибровка» и щупали в виджете AUC: двигая порог, нельзя одновременно увеличить и точность, и полноту. Детектор с высокой полнотой ловит почти весь спам, но заодно много обычных постов. Детектор с высокой точностью ошибается редко, но и пропускает много.

Классическая дилемма: какой порог выбрать? Здесь на неё дан неожиданный ответ — оба, но для разных ситуаций.

  • Пост от аккаунта, на который вы подписаны: применяется только точный детектор. Ложное срабатывание здесь дорого — вы сами выбрали читать этого человека, и скрыть его пост из-за подозрения было бы вмешательством в ваш выбор.
  • Пост от незнакомого аккаунта: применяется ещё и полный детектор. Ложное срабатывание почти бесплатно — вы этот пост не ждали и не заметите его отсутствия. А вот пропустить спам, который система сама вам подсунула, — заметно и неприятно.

Асимметрия цены ошибки прямо превращена в асимметрию порога. Красивый приём: вместо выбора одной точки на кривой берутся две и применяются там, где каждая уместна.

Обратите внимание и на второе следствие: дополнительный набор может только запрещать. Он не способен разрешить то, что запретил базовый. Строгость только растёт по мере удаления от круга подписок.

4. Откуда берутся лейблы

Правила читают лейблы. Лейблы производит вторая дорожка системы — та, что работает непрерывно и к вашему запросу отношения не имеет.

Что понимают про содержимое grox классификаторы текста и медиа: спам, взрослое, насилие media-model-proxy модели изображений и видео, сверка с известным материалом clip эмбеддинги картинок и текста — вход для классификаторов выше Что понимают про аккаунт — три независимых взгляда agatha как реагируют другие: жалобы на лайк, спам-жалобы на лайк окно 30 дней user-cred-v2 структура графа: PageRank по подпискам и взаимодействиям масса → логарифм → скор bdsm поведение самого аккаунта: трансформер над последовательностью его действий botmaker · scarecrow движок правил: на событие, если условия, поставить лейбл Хранилище лейблов Читается на дорожке запроса правилами видимости. Это единственная точка стыка двух дорожек: всё выше происходит непрерывно и заранее, а не в тот момент, когда вы открываете ленту.
Классификаторы содержимого и три взгляда на репутацию аккаунта. Правила видимости читают только готовые лейблы.

Репутация по реакции других

Первый способ оценить аккаунт — посмотреть, как на его посты реагируют. В коде видны названия метрик: ReportsPerFav, SpamReportsPerFav, окно наблюдения — 30 дней.

Ключевое здесь — отношение, а не абсолютное число. Крупный аккаунт получает больше жалоб просто потому, что его больше видят. Считать жалобы в штуках означало бы наказывать за размер. Отношение жалоб к лайкам от размера не зависит.

Это в точности та мысль, которую мы разбирали в сглаженном CTR: сырое отношение на малых числах шумит — у аккаунта с двумя лайками и одной жалобой отношение 0.5, — и его сглаживают априором. Тот же приём, тот же мотив.

Репутация по графу

Второй способ вообще не смотрит на содержимое. Он запускает PageRank по графу подписок и взаимодействий, получая для каждого аккаунта «массу», а из неё — скор:

private val ScoreSlope = 7.07
private val ScoreIntercept = 165.2

val score = if (mass <= 0) 0.0 else ScoreIntercept + ScoreSlope * scala.math.log(mass)

Фрагмент из UserCredV2.scala · код X, Apache 2.0, коммит 28e414f

$$ \text{score} \;=\; 165.2 \;+\; 7.07 \cdot \ln(\text{mass}). $$
Зачем логарифм

Масса PageRank распределена по степенному закону — тому самому, который разбирается в главе «Длинный хвост и степенной закон» и который можно покрутить в виджете длинного хвоста. Между самым крупным аккаунтом и средним разница на много порядков.

Использовать такую величину как признак напрямую нельзя: любая линейная модель будет видеть только верхушку, а различия внутри хвоста для неё сольются в ноль. Логарифм превращает мультипликативную шкалу в аддитивную — разница «в десять раз» становится постоянным сдвигом независимо от того, где по шкале мы находимся.

Коэффициенты 7.07 и 165.2 подобраны так, чтобы результат лёг в удобный диапазон. Заметьте, что содержательного смысла в конкретных числах нет — это калибровка шкалы, а не открытие о природе графа.

Ценность этого сигнала в том, что он дорог в подделке. Написать спамный текст ничего не стоит, а вот попасть в центр графа подписок нельзя без того, чтобы на тебя реально подписались живые люди. Накрутка подписчиков ботами тут почти не помогает: PageRank учитывает вес источника, а масса самих ботов близка к нулю.

Репутация по собственному поведению

Третий способ — самый интересный для нас, потому что архитектурно он повторяет рекомендательную модель. Из описания в репозитории:

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

Остановимся здесь, потому что параллель поразительная.

Рекомендательная модель берёт последовательность действий пользователя и предсказывает, что он сделает дальше. Детектор ботов берёт ту же самую последовательность действий и предсказывает, человек ли это вообще.

Один и тот же вход, одна и та же архитектура, и даже одна и та же интуиция: последовательность действий содержит больше информации, чем любые агрегаты над ней. Мы обосновывали это в главе «Трансформеры над историей» применительно к рекомендациям — «лайкнул 50 постов про космос за час» и «лайкал по посту в неделю год» дают одинаковые счётчики, но совершенно разные последовательности.

Для детекции ботов это работает ещё сильнее. Бот выдаёт себя не тем, что он делает — каждое отдельное действие выглядит нормально, — а ритмом и порядком: равномерные интервалы, отсутствие пауз на сон, одинаковые цепочки. Ровно та информация, которую агрегаты уничтожают, а трансформер над последовательностью видит.

Практический вывод для собеседования: если вы умеете строить рекомендательный трансформер над историей, вы умеете строить и детектор аномального поведения. Это одна и та же задача с разной целевой переменной.

Движок правил

Лейблы ставят не модели напрямую, а движок правил: botmaker — язык, компилятор и исполнитель, scarecrow — сервис, который применяет правила к событиям по мере их поступления. Правило читается как «на такое событие, если выполнены такие условия, поставить такой лейбл».

Зачем отдельный язык, если можно писать на обычном? Затем же, зачем правила видимости вынесены из модели: чтобы изменение можно было раскатить без выкатки кода, чтобы оно было читаемо не только программистами, и чтобы его можно было проверить и оспорить.

Часть правил в репозитории отсутствует — прямо сказано, что ради снижения риска обхода. Это честная граница открытости, и её стоит держать в голове: код правил видимости открыт полностью, а вот часть логики, порождающей лейблы, — нет.

5. Отчёт пользователю как часть системы

Замыкает конструкцию инструмент, показывающий человеку, какие лейблы стоят на его аккаунте и постах. Ежедневные джобы собирают применённые лейблы, серверная часть агрегирует их за период.

Почему это не косметика

Вернёмся к ограничению, которое мы отметили на странице обзора: открытый код показывает, какие решения приняты, но не показывает, что модель выучила на данных. Правило можно прочитать. Веса классификатора спама — нельзя, их и нет в репозитории.

Отчёт закрывает именно эту дыру, но с другой стороны. Вы не видите, как работает классификатор, — зато видите его выход применительно к вам. А дальше по коду правил можете проследить, что именно этот лейбл делает с видимостью ваших постов.

Получается связка: код объясняет механику, отчёт даёт наблюдаемый результат. По отдельности каждая половина почти бесполезна, вместе — проверяема. И заметьте, что технически всё это держится на одном поле decided_by в вердикте: без сохранения причины никакого отчёта не построить.

Частые ошибки и подводные камни

На чём спотыкаются
  • Считать, что решений два. Третий вариант — заглушка — принципиален: он позволяет ставить порог классификатора агрессивнее, потому что цена ложного срабатывания падает до одного нажатия.
  • Думать, что правила одинаковы для всех постов. Для рекомендаций действует дополнительный набор, включающий детекторы с высокой полнотой. Один и тот же пост подписчику покажут, а незнакомому — нет.
  • Считать жалобы в штуках. Крупный аккаунт получает больше жалоб просто потому, что его больше видят. Считают отношение к лайкам, и его сглаживают на малых числах.
  • Подавать PageRank-массу в модель как есть. Она распределена по степенному закону; без логарифма признак различает только верхушку.
  • Не сохранять причину вердикта. Без имени сработавшего правила система неотлаживаема, а отчёт пользователю невозможен.
  • Путать открытость правил и открытость моделей. Правила видимости открыты полностью; часть логики, порождающей лейблы, намеренно не опубликована.

Вопросы с собеседований

Зачем в системе модерации три возможных ответа, а не два?

Промежуточный вариант — показать за заглушкой — переносит решение на пользователя и резко снижает цену ошибки классификатора. В двухвариантной системе ложное срабатывание означает либо скрытый нормальный пост, либо показанный неприемлемый; с заглушкой оно стоит одного лишнего нажатия.

Практическое следствие: порог детектора можно ставить агрессивнее и ловить больше, не платя за это раздражением пользователей. То есть третий ответ — это не про интерфейс, а про рабочую точку на кривой точность-полнота.

Как использовать сразу два детектора одного и того же — с высокой точностью и с высокой полнотой?

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

Вместо выбора одной точки на кривой берутся две и применяются там, где каждая уместна. Важная деталь: дополнительный набор правил может только запрещать — строгость растёт по мере удаления от круга подписок, но никогда не падает.

Как оценить репутацию аккаунта и почему нужно несколько способов?

В разобранной системе их три, и они опираются на принципиально разные данные. По реакции других: отношение жалоб к лайкам за 30 дней — именно отношение, иначе крупные аккаунты наказывались бы за размер. По структуре графа: PageRank по подпискам и взаимодействиям, потом логарифм массы. По собственному поведению: трансформер над последовательностью действий аккаунта.

Несколько нужно потому, что у каждого своя уязвимость. Реакция других подделывается скоординированными жалобами. Граф подделывается накруткой подписчиков, хотя и плохо — PageRank учитывает вес источника, а масса ботов близка к нулю. Поведение подделывается имитацией человеческого ритма, что дорого в масштабе. Обмануть все три одновременно значительно труднее, чем любой один.

Чем детектор ботов похож на рекомендательную модель?

Архитектурно — это одно и то же: трансформер над последовательностью действий пользователя. Разница только в целевой переменной: рекомендательная модель предсказывает, что человек сделает дальше, детектор — человек ли это вообще.

Общая интуиция тоже одна: последовательность содержит информацию, которую агрегаты уничтожают. Для рекомендаций это порядок и темп интересов, для детекции — ритм: равномерные интервалы, отсутствие пауз на сон, повторяющиеся цепочки действий. Каждое отдельное действие бота выглядит нормально, выдаёт его именно последовательность.

Почему решение о видимости не встроено в ранжирующую модель?

Четыре причины, и все видны в коде. Разная цена ошибки — понижение скора и запрет показа несопоставимы по последствиям. Разная скорость изменений — правила меняются по требованиям закона за часы, модель переобучается по расписанию. Проверяемость — правило можно прочитать и оспорить, понижение скора нельзя. Гарантия — понижение скора не гарантирует, что пост не покажут: если конкурентов мало, он всё равно окажется в выдаче.

Последний пункт часто упускают, а он решающий. Жёсткое требование «не показывать» реализуется только жёстким фильтром; мягкое занижение такого обещания не даёт.

Шпаргалка одним экраном

Три ответа

Показать · за заглушкой · не показывать. Третий снижает цену ошибки классификатора.

Порядок вычисления

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

Два набора

29 базовых правил плюс дополнительные для рекомендаций. Второй может только запрещать.

High recall против high precision

Полный детектор — только для незнакомых аккаунтов, точный — для всех.

Репутация: реакция

Отношение жалоб к лайкам за 30 дней, сглаженное. Не абсолютные счётчики.

Репутация: граф

PageRank → масса → \(165.2 + 7.07\ln(\text{mass})\). Логарифм из-за степенного закона.

Репутация: поведение

Трансформер над последовательностью действий. Та же архитектура, что в рекомендациях.

Движок правил

Отдельный язык: раскатка без выкатки кода, читаемость, проверяемость.

Прозрачность

Код объясняет механику, отчёт даёт наблюдаемый результат. Держится на поле «кто решил».

Первоисточники