Как известно, для ML-задач вроде рекомендаций очень популярной техникой являются счётчики в качестве фичей. Например, сколько раз пользователь кликал на документы с того же хоста. Или просто CTR документа (хотя это уже не один счётчик, а выражение от двух — счётчика кликов и счётчика показов). Часто используют и композитные ключи агрегации: например, количество событий такого-то типа в таком-то регионе из такой-то категории для такого-то сегмента пользователей.
Часто счётчики — это первое, что используют для рекомендаций. (Ну, иногда начинают с какой-нибудь матричной факторизации.) Возникли они как попытка скормить в традиционные модели категориальные фичи (user ID, document ID, topic, ...). Но и сейчас, в эпоху нейросетей, которые сами хорошо умеют использовать категориальные фичи (моделируя их эмбеддингами), счётчики всё ещё продолжают быть очень полезными, даже необходимыми. Просто потому, что они дешевле в обработке: во-первых, их можно посчитать за очень длинный период (тогда как обучать нейросеть за тот же период может быть слишком затратно), во-вторых, их легко обновлять в реальном времени (а обновление нейросетей в реальном времени — довольно сложная вещь, в лучшем случае их обновляют в near real-time, т.е. с какой-то существенной задержкой от реального времени).
Счётчики полезно считать за разные периоды времени — час, день, неделя, месяц и т.д. Ведь если пользователь, например, раньше никогда не интересовался какой-то темой, а в последний день заинтересовался, то это довольно полезный сигнал. А для документов соотношение счётчиков за разные периоды выражает тренды.
Но вот что меня удивило: оказывается, далеко не все используют простую модификацию счётчиков — экспоненциальное затухание. Счётчики с экспоненциальным затуханием считают не количество каких-то событий, а сумму весов, экспоненциально затухающих по времени.
Обновление обычных счётчиков за конечный период не слишком-то простое. В офлайн-обработке иногда просто заново вычисляют значение за новые период. В realtime же нужно вычесть количество старых событий, выпадающих из окна расчёта, и добавить новые. Чтобы вычитать, нужно хранить все эти старые события, ну или хотя бы их агрегаты за более мелкие периоды (например, за каждые 5 минут или полчаса). Экспоненциальные счётчики же обновлять очень просто:
NewCounterValue = OldCounterValue * 2^(-(CurrentTime - LastUpdateTime) / HalfLife) + EventValue
Вместо конечного окна агрегации используется HalfLife — период полураспада, т.е. время, за которое счётчик уменьшается вдвое, если ничего нового не происходит. Для обновления не надо ничего дополнительного хранить, кроме времени последнего обновления. Математическим языком это называется индуктивной функцией — нужно знать только добавленный элемент и старое значение функции. При этом выразительность экспоненциальных счётчиков (т.е. полезность для модели) обычно не меньше обычных, а иногда даже больше.В следующем посте обсудим ещё одно улучшение счётчиков.