Warning: Undefined array key 0 in /var/www/tgoop/function.php on line 65

Warning: Trying to access array offset on value of type null in /var/www/tgoop/function.php on line 65
- Telegram Web
Telegram Web
В рекомендательных системах, как и в некоторых других областях ML, практически во всех данных присутствует важное поле — таймстемп. Почти всё, с чем мы работаем, — это события, и нам важно, когда они произошли. И на этом можно было бы не заострять внимание. Но есть нюансы.

Почему важны таймстемпы? Во-первых, конечно, самый правильный способ измерять качество моделей в офлайне — разделяя trainset и testset по времени (причём глобально, а не для каждого пользователя отдельно, как любят делать в научных статьях). Потому что в реальной жизни всё будет работать именно так: мы обучаем модель, потом выкатываем её, и потом уже она работает на пользователях. А вообще-то, чаще всего у нас модели постоянно инкрементально дообучаются, поэтому и тестировать хорошо бы в таком же инкрементальном/онлайн режиме, а не с отложенным тестсетом.

Во-вторых, иногда нам нужно в офлайне вычислить какие-то фичи. Конечно, лучше всего этого просто не делать, а всегда использовать фичи, залогированные в онлайне. И вообще, золотое правило — лучше всегда обучаться ровно на тех данных, которые используются при применении модели. Чем меньше от этого правила отходишь, тем меньше неожиданных проблем приходится разгребать.

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

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

Есть ещё и специальный вид вычисления — обучение и применение подмодели. (Например, фичи от трансформеров мы хотим использовать в верхней ранжирующей модели, а трансформеры постоянно дообучаются.) Вся логика тут сохраняется, при применении нужно брать последние известные данные (историю пользователю) и последнюю обученную подмодель (трансформеры). Это, кстати, показывает, что это примерно одно и то же — вычисление ретроспективных фичей и тестирование моделей в онлайн-режиме. Для тестирования сначала надо просто посчитать фичу — предсказание модели.

Отдельно скажу про избитый вопрос — клиентское или серверное время. Всегда используйте серверное. Клиентское время может быть в другом часовом поясе, а главное — просто битым (привет прослушиваниям в Яндекс Музыке из 2099 года). Его можно использовать как сопутствующую информацию (в том числе и для фичей), но не для основного механизма тестирования моделей.

Наконец, мало где можно встретить такое параноидальное внимание к деталям, но всё-таки уточню, что у пользовательских событий (взаимодействий с объектами) есть не один таймстемп и не два, а много:
T_req_start — пришёл пользовательский запрос на рекомендации;
T_req_end — мы построили рекомендации;
T_impression — пользователь увидел (начал смотреть/слушать) порекомендованный объект;
T_click, T_like, ... — пользователь совершил какое-то действие с объектом;
T_ingest — наша система узнала про всё это (что в свою очередь, конечно, зависит от конкретной части системы, которая узнала).

С точки зрения правильного тестирования модели, самые главные из этих таймстемпов — T_req_start и T_ingest. Но, к сожалению, не часто их разделяют, а вместо этого используют один таймстемп и иногда добавляют отставание. Так проще, а разница между ними обычно незначительная. Но думаю, что чем сложнее и мощнее становятся наши модели, тем больше проблем будет у неправильного учёта таймстемпов. С транформерами, например, это уже стало выстреливать намного чаще, чем раньше.
Представьте себе систему рекомендаций, в которой очень важен реалтайм, т.е. быстрое обновление. Например, рекомендации новостей или постов. В ней очень важно быстро обновлять внутренние параметры, особенно те, которые соответствуют самим новостям. Это могут быть как обычные счетчики, выражающие CTR документов, так и обучаемые эмбеддинги для каждой новости, в которых в том числе и этот CTR будет зашит. Будем считать, что у нас есть модель, которая обучается в реальном времени и обновляет эти эмбеддинги.

А теперь давайте подумаем, в какой момент времени относительно времени показа (T_impression) и времени разных взаимодействий (T_click, etc.), о которых говорили в прошлом посте, наша модель должна получить новый сэмпл для обучения (т.е. каким должен быть T_ingest в обучатор модели). Конечно, нам хочется, чтобы это происходило как можно раньше, чтобы мы могли быстро выявлять очень популярные новости. Но сделать это прямо в момент показа мы не можем, так как еще не знаем, произойдёт ли клик.

Самый простой вариант — подождать X времени (скажем, несколько минут) после показа и соответствующим образом решить, положительный ли этот сэмпл или нет. Тогда модель будет предсказывать, с какой вероятностью пользователь кликнет/полайкает/… документ за время X. Если X будет слишком большим, реакция системы будет медленнее. Если X будет слишком маленьким, то мы упустим часть положительных взаимодействий из-за того, что они не успели произойти за X. Эта часть может оказаться совсем не нулевой, больше, чем кажется на первый взгляд, особенно для более сложных таргетов (например, подписался ли пользователь на источник новости). У каждого таргета есть своё вероятностное распределение времени, через которое он случается после показа. И у этих распределений тяжелые хвосты.

Есть ли способы эффективнее?

Например, можно было бы завести несколько разных событий с разными X: кликнул ли за 30 секунд, за 2 минуты, за 5 минут и т.д. Но тогда мы увеличим объём данных для обучения в несколько раз, хотя информации для обучения мы будем выдавать не сильно больше, чем раньше. Это очень большая нагрузка на инфраструктуру и на обучение.

Можно было бы сразу же в момент показа использовать негативный сэмпл, а потом в случае, если какое-то действие произойдёт, сделать новый сэмпл, «уточняющий» предыдущий. (А если еще одно действие произойдёт — то еще одно уточнение, и так далее.) Так как уточнений будет на порядок меньше, чем показов, то нагрузка на инфраструктуру возрастёт не слишком сильно. Понятно, как обрабатывать такие уточнения для счетчиков. А вот как это делать для обучающихся эмбеддингов? Т.е. как «отменить» действие предыдущего негативного сэмпла? Возможно, можно просто сделать обратный шаг — шаг по градиенту (т.е. шаг градиентного подъёма вместо градиентного спуска). Но параметры модели на этот момент уже не те, которые были при прямом шаге, поэтому это не будет совсем точной его отменой. Не разнесёт ли модель от этого? Кроме того, как только новость появляется, она сразу получает много негативных сэмплов, а позитивные придут только с задержкой, из-за чего получается сильное смещение в негативную сторону.

Можно также комбинировать этот способ с изначальным простым — сначала чуть-чуть подождать (маленькое X), использовать первый сэмпл и уже только после этого начинать делать уточнения. Это снизит степень проблем, связанных с уточнениями. Но решит ли полностью? Да и сложновато получается.

Наверно, стоит отметить, что если подумать не про сторону объектов (новостей), а про сторону пользователей, то эта проблема уже кажется не такой специфической — обновлять рекомендации для пользователя в реальном времени хотят все. И если используется обучаемый для каждого пользователя эмбеддинг, то проблема та же самая. Но, например, если использовать трансформер по пользовательской истории, то в него можно запихнуть всё как есть, а он уже сам разберётся, достаточно ли времени прошло от каждого показа, чтобы считать его полноценно отрицательным событием.

Что думаете?
Неделю назад я выступал в Яндексе с презентаций про свой карьерный путь. Это такой немного странный жанр, вроде автобиографии, с примесью каких-то своих выводов и мыслей. Я даже сначала думал отказаться, вроде такое не должно быть интересно большому число людей. Но оказалось, что нет, народ пришёл послушать, и даже немало вопросов задали.

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

А в процессе подумал, что про многое из этого можно было бы написать и сюда в канал. Чуть ли не каждый слайд. Да и один знакомый высказал такую же мысль. Вот только я снова не уверен, что это будет многим интересно, особенно людям, не связанным с Яндексом. Ведь основная часть этого карьерного пути (больше 14 лет!) прошла именно там.

В общем, выскажите своё мнение:
❤️ — да, с интересом будете читать
🤨 — нет, не надо такого

P.S. Если наберётся 100 сердечек Порога нет, всё равно потом сам решу :)

P.P.S. ⭐️ тоже засчитываются как положительные голоса :D
Please open Telegram to view this post
VIEW IN TELEGRAM
Не знаю, все ли поняли, за что проголосовали. Это будет не один и даже не один десяток постов. Но постараюсь их чередовать с обычными постами. И истории будут в около-хронологическом порядке, поэтому ждать байки про Майкрософт придётся ещё долго 🙂

Работать я начал ещё до первого курса. После завершительных школьных экзаменов папа взял меня работать к себе в отдел в филиале РЖД. Моим первым рабочим языком (Паскаль не в счёт) стал VB.NET. К счастью, потом стали переписывать всё на C#. Занимался я тренажёром машиниста, но это не помешало в последствии друзьям подшучивать надо мной: «Миш, у меня сайт РЖД не работает, почини!» Задачи у меня были не очень сложные, поэтому вся энергия выливалась в перфекционизм: пытался заставить остальных писать код хорошо — по канонам ООП и без транслита в названиях.

В 2007 году я закончил второй курс на мехмате и пошёл на кафедру алгебры, в научное руководство к Лене Буниной. Тем летом её пригласили в Яндекс открывать Школу Анализа Данных, куда она меня и позвала учиться. Я как раз очень хотел научиться алгоритмам, поэтому с радостью пошёл. Причём так как Лена меня уже хорошо знала, то взяли меня без собеседований, по блату. Вместо собеседования, она просто покормила меня в столовой Яндекса (еще на Самокатной).

Учиться в ШАДе мне очень понравилось, было много всего интересного. Например, курс алгоритмов, который вёл Макс Бабенко. Мы были первым набором ШАДа, поэтому на нас как раз была отладка в продакшене. К примеру, лекции Ширяева только человека 3-4 с курса могли понять. И из-за этого после первого семестра нам устроили специальный туториал, где объясняли понятнее. А на том же курсе алгоритмов ещё пока не было той самой жесткой системы код-ревью.

Важно понимать, что это были те времена, когда самой крутой IT-компанией в мире, куда хотели попасть все вокруг, была компания Google. И я, конечно, тоже хотел. Да и Макс Бабенко тоже говорил, что если вдруг он решит всё-таки пойти работать в индустрию (для чего ему пришлось бы частично пожертвовать научной работой), то тоже скорее пойдёт в Google.

Во втором семестре у нас с ребятами из ШАДа завелась традиция — каждую вторую пятницу после занятий ходить вместе ужинать в какой-нибудь бар неподалёку. И Лена тоже с нами ходила. На одном из таких вечеров у нас завёлся примерно такой разговор:
— Ой, да ну этот Яндекс, вот вырастим, закончим ШАД и пойдём лучше в Google, — соглашались мы с товарищем, обсуждая вопрос, идти ли в Яндекс на стажировку (как делали многие однокурсники по ШАДу).
— Да? — спросила Лена, — А вот если, например, вы бы пошли не просто в Яндекс, а пошли бы туда работать с Максом?
— С Максом?.. — Задумались мы. А Макса мы считали не просто отличным лектором по алгоритмам, но и профессионалом, у которого хотелось поучиться на практике, — Ну, с Максом, пожалуй, можно даже в Яндекс.

И через пару месяцев мы все, включая Макса, оказались в Яндексе.

Без собеседований, по блату.

#lifestories
Летом прослушал выпуск подкаста с Максимом Страховым, а затем и подписался на его канал. Он рассказывает про то, как определяются уровни инженеров в FAANG-like компаниях.


Junior
Активно учится и растёт. Выполняет задачи под присмотром. Нужно рассказать, как делать. Пользы приносит меньше, чем на него тратят сил. По сути — инвестиция.

Middle
Независимо выполняет задачи. Может сам придумать, как делать, но нужно рассказать, что делать.

Senior
Достигает цели. Сам может придумать, что делать для достижения.

Staff
Разрабатывает стратегию и ставит цели.

Senior Staff
Запускает проекты с импактом крупного масштаба.

Principal
Трансформирует индустрию.


Конечно, это переупрощенное определение. Есть море нюансов. Очень многое зависит от компании, от конкретного отдела в компании и даже просто от везения (если говорить о повышениях и офферах). Что такое «импакт крупного масштаба»? Где четкая граница между задачами и целями? Почему в этой шкале только тип выполняемой работы, но нет её качества?

Тем не менее считаю это крайне полезным ориентиром. На нём можно основываться, дополнять и уточнять.

Если бы у меня был такой референс несколько лет назад, мне было бы чуть-чуть проще — и на калибровках, и направлять своих подчиненных, а может быть, и для собственного роста.
Попав в Яндекс, мы получили проект от Ильи Сегаловича. Илья умел очень классно делиться идеями и объяснять суть. Он нам рассказал, что на самом деле Гугл в своё время выиграл у всех предыдущих поисковиков за счёт хорошо сделанных сниппетов. А теперь для нас самое главное — сделать так, чтобы поисковые результаты не были сплошь одинаковыми. Надо бороться с полу-дублями.

Только сделать это у нас не удалось. Зато мне удалось получить свою первую психологическую травму на работе.

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

Когда нужно было разобраться в каком-то куске поискового кода, Макс сказал:
— Ну давай посмотрим, кто автор этого кода... Ага, некий Антон с ником pg@. Просто сходи и спроси у него, что здесь происходит.

Я сходил и спросил. Антон с ником pg@ ответил мне, чтобы я просто прочитал код.

Прочитать и понять код у меня не получилось. А так как работали мы на четверть ставки, то в следующий раз мы с Максом встретились примерно через неделю. Узнав, что прогресса особо нет, Макс сказал:
— Нет, ну так дело не пойдёт. Пойдём вместе сходим и спросим.

Сходили и спросили. На что Антон с ником pg@ просто накричал на нас обоих: какого чёрта какие-то стажёры его отвлекают и не могут даже за неделю самостоятельно прочитать код?!

С тех пор ни я, ни Макс уже больше никогда не хотели работать в Яндекс.Поиске.

#lifestories
После неудачи с первым проектом мы решили заняться чем-то ещё. И снова у Ильи Сегаловича возникла прекрасная идея: давайте сделаем специальный поиск по стихотворениям! Обычный поиск здесь не очень хорошо справляется, потому что стихи нужно искать в меньшей степени по смыслу, а в большей — по совпадению последовательности слов. И так зародился проект Стихолюб.

С этим проектом у нас пошло сильно лучше, потому что его можно было писать отдельно от существующей кодовой базы. А так как мы были группой исследовательских проектов, свой исследовательский прототип мы решили писать на том языке, который и я, и Макс знали лучше всего. На C#.

За полгода получилось сделать отличный поиск стихов, в котором были и расстояние Левенштейна, и суффиксные массивы, и алгоритм Ахо-Корасик, и фильтр Блума. И это даже неплохо работало.

И вот в конце апреля 2009 приходит к нам снова Сегалович и говорит:
— Слушайте, тут такое дело... 6 июня юбилей Пушкина. Очень хочется к этой дате что-то прикольное запустить. Нельзя ли теперь быстро ваш прототип превратить в продакшен?

Мы слегка удивились.

Я работал на четверть ставки.
Впереди меня ждала сессия.
C++ я знал очень плохо.

Учитывая это, быстро всё переписывать на православные технологии мы всё-таки отказались.

И тогда Сегалович решил договориться со всеми, чтобы проект выкатили в продакшен прямо в таком виде. На C#. На виндовых серверах.

От одного из виндовых админов мы получили новое прозвище. «Эти стихоблуды опять чего-то странное делают», — написал он на общую рассылку.

А руководитель качества поиска Денис усмехнулся:
— Вы что, с ума сошли? На C#? Как вы вообще собираетесь 5K RPS держать на своём дот-нете?

Когда провели нагрузочное тестирование, наш поиск выдержал и 10K, а дальше закончились патроны. 🤷‍♂️

Но на всякий случай решили написать отдельное правило на верхнем поиске, которое бы фильтровало трафик к нам. (Именно там и был фильтр Блума.) Правда, потом это правило просто забыли включить, и мы получили весь поисковый трафик.

Но перед самым запуском нас встретила ещё пара неожиданных проблем.

Во-первых, по соображениям безопасности, нельзя было где-либо показывать наружу, что поиск работает на винде. Поэтому Максу пришлось быстро патчить какую-то виндовую библиотеку, чтобы IIS притворялся nginx-ом в каких-то заголовках.

А во-вторых, surprise-surprise, оказывается, для такого колдунщика нужно не просто научиться находить самое подходящее стихотворение в базе, но и сделать классификатор, который бы говорил, показывать колдунщик или мы нашли мусор.

Я как раз прослушал новый курс в ШАДе под названием Machine Learning. Мы собрали ответы нашего поиска и разметили их на мусор. Получили целых 300 примеров! И дальше я несколько дней пытался обучить на этом SVM с разными фичами. И feature selection написал. Но почему-то всё равно получилось плохо.

В итоге наш продакт-менеджер Рост просто сам придумал decision tree, с которым мы и запустились.

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

А ещё, за пару дней до запуска к нам пришли Антон с ником pg@ и его руководитель и сказали:
— Ребят, вы, конечно, молодцы. Но мы не хотим потом поддерживать ещё один поиск, да ещё на C#.

И после запуска мы специально наняли в команду ещё одного разработчика Алексея — преподавателя C++ в ШАДе, — вместе с которым я всё-таки переписал это, заодно и подучив язык хоть немного.

А переписанный Стихолюб проработал ещё несколько лет. Пока его не отключили за ненадобностью.

#lifestories
Please open Telegram to view this post
VIEW IN TELEGRAM
Невероятно.

Разработчик Алексей рассказал в комментах, что Стихолюб до сих пор жив!
Forwarded from Alexey Zobnin
Удивительно, но тот стихолюб на C++ до сих пор работает в проде.
Я уже пару раз писал про счётчики и про то, как убирать из них смещение. Но так и не затронул некоторые важные технические моменты. А именно — что счётчики занимают место в профилях пользователей и объектов. (Профиль объекта — это вся накопленная информация про объект; он не обязательно должен быть физически одной структурой.) И это значит, что мы не можем хранить бесконечное число счётчиков. Например, если профиль пользователя занимает 1 Мб, то с этим уже довольно сложно работать в realtime-системе.

Поэтому обычно настраивают лимит по количеству счётчиков каждого типа. Когда достигаем этого лимита, то выбрасываем самые «ненужные» счётчики — либо самые старые (давно не обновлявшиеся), либо с наименьшим значением (а при экспоненциальном затухании значение само учитывает и время обновления).

Но бывают случаи, когда такой стратегии недостаточно. Например, когда пользователи листают ленту рекомендаций и мы хотим запомнить все показы, чтобы не рекомендовать их ещё раз. У наиболее активных пользователей могут набраться десятки тысяч показов. Счётчики тут — не самое эффективное средство.

Если хочется просто отфильтровать объекты, то можно использовать широко известную вероятностную структуру — фильтр Блума. Иногда он будет отфильтровывать лишнее, но редко, и нас это обычно устраивает. А чтобы он не рос и не «засорялся» бесконечно с историей пользователя (удалять-то из него нельзя), можно сделать очередь фильтров: когда в последнем фильтре становится слишком много элементов, заводим новый фильтр и новые элементы добавляем уже в него, а когда фильтров в очереди становится много — удаляем самый старый.

Кстати, в нашей платформе в Яндексе мы сделали более эффективную по месту реализацию фильтра — quotient filter.

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

А можно ли избавиться от первого недостатка? Можно ли сделать структуру, которая будет хранить небинарные значения, как у счётчиков, но делать это приближенно (нам же это для фичей в основном нужно) и за счёт этого — более компактно?

Можно! Это называется count-min sketch, и это простое обобщение фильтра Блума (counting Bloom filter) с той же самой идеей использовать несколько хеш-функций. И, кстати, с экспоненциальным затуханием прекрасно совмещается.

К сожалению, у меня нет практического опыта с этим, чтобы сказать — эффективнее ли для фичей использовать count-min sketch или обычное обрезание счётчиков.
Школа Анализа Данных славится в первую очередь тем, что готовит специалистов по машинному обучению. Но когда я в неё пошёл в 2007, я и слов-то таких не знал. Я шёл туда, чтобы изучать computer science, просто потому что было очень интересно. (Хотя ещё за год до этого я и про словосочетание computer science ухмылялся: что это ещё за наука такая — о компьютерах?)

И вот только к концу моего обучения в ШАД пригласили Константина Воронцова читать лекции по машинному обучению. У нашего потока оставался всего один семестр, поэтому Воронцову пришлось для нас ужать курс вдвое. Но мне этого хватило, чтобы понять, что это именно то, чем я и хотел бы заниматься.

Семинары по ML тогда вёл Александр Дьяконов и делал это, надо сказать, не очень хорошо. Многим не нравилось. И когда я закончил ШАД, внезапно оказалось, что Дьяконов уходит писать докторскую и больше вести семинары не будет. Надо искать нового семинариста. И Воронцов вместе с Леной Буниной подумали-подумали и почему-то решили, что никого лучше свежего выпускника ШАД, прошедшего только укороченный курс и ещё даже не окончившего мехмат, с неуспешным опытом применения ML в рабочем проекте, но зато с горящими глазами — им на эту роль не найти!

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

Меня хватило на два года. После этого я понял, что быть преподавателем — это не совсем моё, удовлетворения от процесса я получаю сильно меньше, чем трачу сил на подготовку к семинарам и проверку домашек. Но была и польза: меня стали узнавать почти все студенты ШАДа, что помогло мне получить неплохую репутацию в Яндексе (у вас тоже так бывает, что с вами здороваются незнакомые вам люди?) и, в частности, лучше нанимать к себе в команду.

Но только я перестал быть семинаристом, как Лена нашла мне новую роль в ШАДе: я стал вести научный семинар по машинному обучению и информационному поиску, где мы со студентами разбирали статьи. В результате чего у меня появилось несколько студентов в научном руководстве. И один из них (не будем показывать пальцем; привет, Рома!) даже весьма успешно защитил магистерскую.

Года через два мне и эта роль надоела.

И я стал вести спецкурс по алгоритмам на мехмате «от ШАДа», уже в роли лектора. Оказывается, лектором (по теме с уже имеющейся понятной и стабильной программой) быть намного проще, чем семинаристом.

А еще через год я попал в учёный совет ШАДа и пробыл там ещё много лет.

А находясь в совете, я даже слегка зацепил движуху по открытию ФКН ВШЭ.

А в 2015 я выступил на конференции ШАД в Берлине, где получил от коллег прозвище «Доктор Майкл Ройзнер».

А ещё я всё это время ходил вольнослушателем на разные курсы ШАДа. Оказалось, что после моего выпуска появилось даже ещё больше интересных курсов, чем было вначале. Причем настолько интересных, что можно даже отвлечься от работы и пойти в соседнее здание послушать лекцию раз в неделю.

Одним словом, трудно переоценить, сколько полезных знаний и знакомств мне дал ШАД. Я не знаю, как бы сложилась моя карьера (да и личная жизнь, чего уж там 😁), если бы не он. Наверняка не так интересно.

💛🔴

#lifestories
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
Обычно у рекомендательных сервисов есть главная метрика, которую они пытаются растить, north star. Насколько я могу судить (и когда-то я уже писал об этом), в большинстве случаев это одна из четырех:
1) Time spent (сколько времени пользователи проводят на сервисе)
2) Транзакции (количество или суммарная стоимость, GMV)
3) Подписки
4) DAU (или похожие метрики user retention)

Конечно же, это исходит от бизнес-модели сервиса.

У меня есть мнение (или лучше сказать — гипотеза), что среди этих метрик самая близкая к «чистому качеству» рекомендаций (user satisfaction, «счастью пользователей» и т.п.) — это именно DAU.

Например, давайте представим, что наша рекомендательная система стала настолько продвинутой, что может прямо обучаться на эти метрики. Что будет, если мы ей выдадим каждую из этих метрик как таргет? Ну, или просто поставим команде рекомендаций соответствующую цель.

Не очень сложно представить, как можно накрутить time spent. GMV — наверно, тоже (хотя тут слово «накрутить» не обязательно означает что-то плохое, деньги же тоже нужно зарабатывать, просто это может быть не сонаправленно с user satisfaction). Подписки — легко, если на сервисе есть контент, доступный только подписчикам (а если нет, то и оптимизировать эту метрику будет на порядок сложнее, чем остальные).

Для DAU тоже есть известный простой способ накрутки — присылать пуши (не говоря уже про дистрибуцию). Но это всё-таки немного про другой сценарий. А вот может ли система (или команда), которая управляет только тем, какой контент она рекомендует, накрутить DAU (т.е. заставить пользователей больше возвращаться в последующие дни), но понизить при этом user satisfaction? Я простых способов не знаю.

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

Расскажите, знаете ли вы способы накрутки DAU и что вообще думаете про метрики верхнего уровня для рекомендаций?
Из необычных, но прикольных примеров рекомендательных систем в повседневной жизни: динамические обои на лок-скрине iPhone.

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

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

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

А если поставить такие обои, то айфон будет сам выбирать лучшие (по его мнению) фотографии и вырезать из них удачный кроп. И у него вполне неплохо получается. Я частенько, видя что-то новое, пытаюсь узнать — а когда же это я такое снимал. Не зря съездил в отпуск, оказывается!

Если кто хочет тоже себе такое настроить: Settings -> Wallpaper -> Add New Wallpaper -> Photo Shuffle -> выбрать интересующие категории фото (например, я выбрал и природу, и города, и свою семью). Для Android такое тоже наверняка есть, да?
Месяц назад мне скинули ссылку на выступление от Яндекс Маркета на Highload++ (аж 2023, но, видимо, видео не так давно выложили) про их персональные рекомендации. Ничего особенного в нём нет, но зато есть кусок и про платформу DJ, и про алгоритм Mixigen — результаты работы нашей команды.
Кстати, если кто знает ещё публичные рассказы про DJ — дайте знать.

Про Миксиджен хочется рассказать подробнее, потому что это штука хорошая, а настоящую статью про него уже всё равно вряд ли кто-нибудь напишет. А жаль!
Про DJ когда-нибудь, может быть, тоже ещё подробнее напишу.

В индустриальных рекомендательных системах есть стадии генерации кандидатов и ранжирования, и первая обычно устроена примерно так: взять 100 кандидатов из генератора A, 200 кандидатов из генератора B и т.д. Эти числа — количество кандидатов из каждого источника — чаще всего заданы конфигом и подбираются, например, путём онлайн-экспериментов. Ну и если добавляется новый источник, ему нужно выделить какую-то квоту, уменьшив квоты остальных.

Но как-то раз, когда наша команда представила очередной новый генератор кандидатов, одна из продуктовых команд нас спросила: а есть ли какой-то способ более оптимально и автоматически подбирать эти параметры? Мы тогда такого способа не знали. Но один из разработчиков в нашей команде об этом подумал-подумал и в итоге придумал несложный алгоритм, который мы и назвали Миксидженом (по созвучию с MixCG — mixing candidate generator). Точнее, мы потом назвали его Mixigen 1.0, потому что еще через какое-то время я придумал его усовершенствование — Mixigen 2.0 🙂 Но про него уже будет в следующий раз.

Чтобы подбирать эти параметры, нужно задать метрику, которую мы хотим оптимизировать. Для генерации кандидатов стандартная метрика — полнота. Если есть какие-то положительные действия (например, покупки), можно посмотреть, какая доля из них попадает в список кандидатов к запросам от этого пользователя до покупки. В том же посте о генерации кандидатов я писал, чем эта метрика плоха.

И вот тот разработчик придумал, что если суммарно нам нужно выдать N кандидатов, мы знаем (залогировали или ретроспективно восстановили) списки из N кандидатов из каждого источника к каждому запросу и знаем, какие из них — те положительные, полноту которых мы хотим повысить, то задача просто сводится к задаче о максимальном покрытии. Эта задача NP-полная, но у неё есть очень простой жадный алгоритм с гарантией аппроксимации 1 - 1/e. А на практике оказалось, что он выдаёт полноту около 99% от идеальной.

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

Дополнительный позитивный эффект от такого алгоритма (возможно, даже важнее, чем повышение качества) в том, что теперь можно вообще не думать про этим параметры и — главное — удалять ненужные генераторы кандидатов. Если кто-то придумает новый генератор, то можно его сразу добавить (теоретически даже без онлайн-эксперимента) в список источников, а Mixigen сам решит, полезный ли он или его можно выкинуть.

В следующем посте опишу недостатки этой первой версии алгоритма и вторую версию, которой я до сих пор немного горжусь.
Please open Telegram to view this post
VIEW IN TELEGRAM
Мне тут сказали, что мои посты большие и сложные. Стоит ли их нарезать на более мелкие и публиковать в разные дни? Как вам будет удобнее читать?
Anonymous Poll
20%
Да, более мелкие порции в разные дни легче усваивать
65%
Нет, текущий формат лучше
15%
Наоборот, к следующему посту предыдущий забывается, поэтому серии постов лучше постить сразу целиком
А теперь о разных особенностях Mixigen 2.0.

🔹 Наивная имплементация описанной идеи будет тормозить, потому что инференс модели вызывается отдельно на каждую позицию каждого источника (кроме начальных позиций, их можно отскорить вместе). Но это легко исправить, если предположить, что близкие позиции имеют близкий скор. Тогда можно набирать из очередного источника не по одному кандидату, а сразу небольшими пачками (скажем, по 10). В таком варианте у нас это работало в пределах 10мс на запрос. Кроме того, вероятно, можно ещё сэкономить, вызывая инференс батчами.
🔹 Я не проверял на практике, но, судя по графикам из предыдущего поста, предсказания должны хорошо выражаться какой-то параметрической функцией от позиции — например, суммой сигмоид. Если обучить нейронную модель в таком виде, то инференс можно будет запускать вообще один раз — чтобы получить параметры этих функций для разных источников. А пересчитывать для разных позиций будет уже почти бесплатно.
🔹 Модель обучается на том же, что используется в продакшене. Если какие-то позиции источников никогда не используются, то и модель про них ничего не узнает. Поэтому стоит добавить немного эксплорейшена: после основного цикла алгоритма добавить ещё несколько очередных позиций из случайных источников.
🔹 Легко поддержать параметры, позволяющие ограничить снизу и сверху количество кандидатов из каждого источника. Это может быть полезно, в частности при добавлении нового источника. Но опять-таки, если посмотреть на графики, то обычно это не обязательно, новые источники и так предсказываются не очень плохо.
🔹 Как следствие из предыдущих двух пунктов, а также из-за обновлений модели ранжирования — модель Миксиджена важно регулярно дообучать.
🔹 Как и с многими дополнительными компонентами, здесь возникает нюанс при экспериментировании с разными ранкерами. Можно это делать, не меняя модель Миксиджена. Но т.к. она обучалась для продакшен-ранжирования, то и результат у продакшена будет чуть-чуть лучше. Обычно это почти ничего принципиально не меняет. Но можно и использовать разные модели под разные ранкеры. Более того, чтобы обучить новую модель Миксиджена под новый ранкер, даже необязательно запускать их в онлайн — ведь учимся мы на выходах ранкера, а не на реакциях пользователей.
🔹 Тоже не проверял на практике, но есть идея, что с помощью Миксиджена можно даже динамически контролировать суммарное число кандидатов. Если мы видим в какой-то момент, что вероятность быть порекомендованным опускается ниже порога, то можно уже на этом остановиться, выдать меньше кандидатов и сэкономить ресурсы следующих стадий.

Нельзя не сравнить Миксиджен с использованием дополнительной, легкой стадии ранжирования, потому что цель у них одна и та же.

🔸 Сразу скажу, что у меня не было полноценного опыта их сравнения в одном и том же проекте.
🔸 Очевидно, что легкое ранжирование сильно лучше по качеству, потому что использует информацию про объекты. Миксиджен использует только источники и позиции.
🔸 Но это требует и больших затрат как для инференса, так и для логирования. Чтобы хорошо обучить легкое ранжирование, нужно логировать фичи от хоть каких-то не порекомендованных кандидатов. В Миксиджене же это необязательно.
🔸 Миксиджен принципиально более масштабируем. Ведь ему всё равно, сколько суммарно кандидатов на входе. Ему только важно, сколько источников и сколько кандидатов нужно отдать на выходе.
🔸 Кстати, пока я писал эту серию постов, я осознал, что эту масштабируемость можно довести до предела. И по сути, именно это сделали ByteDance в своей последней статье про real-time индекс: merge статических списков объектов из 16К кластеров — частный случай Миксиджена.
🔸 Главное — эти подходы можно совмещать. На вход в легкое ранжирование тоже обычно идут кандидаты из разных источников, и вполне не бессмысленно этот этап так же оптимизировать.
После наших первых проектов разной успешности мы начали искать новые вызовы. Вдохновившись прекрасным взаимодействием с главными разработчиками Яндекса, Макс решил попробовать что-нибудь на стороне — и устроился в JetBrains. Но совсем уж бросить нас он не мог, поэтому в Яндексе тоже остался на минимальной ставке.

В качестве нового челленджа руководитель департамента разработки предложил нам поработать над новой системой распределенных вычислений. Следующий год прошёл для меня, как в тумане. Помню только, что я писал какой-то экспериментальный код для проверки эффективности чтения данных в поколоночном формате. И что каждую пятницу по вечерам мы собирались в кабинете у этого руководителя разработки и что-то обсуждали. Глобальной сути происходящего я не улавливал. Да и больше был занят преподаванием ML, чем рабочим проектом.

Через год стало чуть-чуть понятнее, что именно мы хотим сделать. В Яндексе на тот момент уже была своя система MapReduce, но к ней было много нареканий. Ну и... не исправлять же их! Лучше напишем новую!
Если серьёзно, то на этот раз, думаю, на то и правда были разумные причины — иногда систему действительно лучше переписать с нуля.

А ещё мы решили, что если нанять ещё одного разработчика, то дело веселей пойдёт (ведь именно так решают все проблемы в корпорациях, да?). И взяли в команду моего однокурсника. Пошло действительно чуть веселее. Макс вскоре ушёл из JetBrains (видимо, тоже разочаровавшись в нём) и стал больше времени уделять Яндексу.

Одним из основных референсов для такой системы у нас был BigTable от Гугла. Поэтому, когда нужно было как-то назвать папку с кодом, я назвал ее YandexTable. А через некоторое время к нам присоединился и главный разработчик предыдущего MapReduce и сказал, что это отличное название, только нужно сократить до YT и читать «Ыть». Возможно, название — это мой самый большой вклад в этот проект. Кода-то моего там уже не осталось, скорее всего.

Еще через какое-то время я сделал перерыв, поехав на стажировку в Америку (об этом — в следующий раз). Вернувшись, я понял, что третий год заниматься распределенной системой, главную цель которой я до сих пор не осознаю, мне больше не хочется. Но, понимая демотивацию от такого long-term проекта без ощутимых результатов, Макс сказал, что должно стать намного лучше и понятнее, когда мы наконец запустим первую операцию map на больших данных. И у нас появилась краткосрочная цель под названием «map к новому году».

Map к новому году мы не запустили. Запустили чуть позже. Да и всю бета-версию YT запустили через полгода. К тому моменту я уже начал собеседоваться в другие компании и был готов уходить (об этом — тоже в следующих сериях). Макс уже прекрасно понимал, что со мной ловить нечего и лучше меня просто отпустить.

Хотя это и не было проектом моей мечты, всё-таки опыт был незаменимый. Работая с Максом рука об руку, я научился писать асинхронный код, разрабатывать сложные компоненты и избегать костылей. Спасибо тебе, Макс!

Через несколько лет YT выиграл тендер в Яндексе и вытеснил другие MapReduce-системы (которых всего было от 3 до 5, по разным подсчётам). А два года назад вышел в open source как YTsaurus. На картинке снизу носохвост — он был любимой мягкой игрушкой Макса и символом нашей команды. Видимо, он и стал логотипом YTsaurus.

#lifestories
2025/03/09 16:22:13
Back to Top
HTML Embed Code: