devsp | Unsorted

Telegram-канал devsp - Data Science | Machinelearning [ru]

19745

Все о Data Science, машинном обучении и искусственном интеллекте: от базовой теории до cutting-edge исследований и LLM. MAX - https://max.ru/devsp Личный блог - @just_genych По вопросам рекламы или разработки - @g_abashkin РКН: https://vk.cc/cJPGXD

Subscribe to a channel

Data Science | Machinelearning [ru]

⁣Оптимизация распределённого жадного поиска сплитов в GBDT: вычислительное бюджетирование

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

Почему это важно
В production ML сценариях с высокой нагрузкой — мониторинг потоковых данных, обучение на Spark, CatBoost в real-time — каждая микросекунда на сплит умножается на число фич и глубину дерева. Типичная ошибка: строить плотные гистограммы для всех фич, даже для разреженных или с низким градиентом, что перегружает сеть и CPU.

Идея вычислительного бюджетирования
Вводим явный бюджет — лимит на количество гистограмм на узел. Фичи ранжируются по эвристике: сумма абсолютных градиентов или плотность ненулевых значений. Для разреженных фич это особенно спасает — они отсекаются до передачи данных между узлами.

Пример псевдокода для распределённой среды:

def allocate_budget(feature_stats, budget=100):
scores = {feat: len(nonzero_grads[feat]) for feat in feature_stats}
sorted_feats = sorted(scores, key=scores.get, reverse=True)
return sorted_feats[:budget]

for tree_level in range(max_depth):
budget_features = allocate_budget(current_stats)
parallel_build_histograms(budget_features)


Production-кейс и trade-offs
Из опыта внедрения в streaming-обучении: фаза сплита ускоряется на 40-60% при том же качестве. Сетевого трафика меньше — передаём гистограммы только по отобранным фичам. Однако есть нюанс: если бюджет задан без учёта gain от корня, можно отсечь хорошие сплиты в глубине. Слишком агрессивное бюджетирование (меньше 5% фич) — потеря информативности, особенно на сложных узлах с низкой чистотой.

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

Типичная ошибка
Фиксированный бюджет на все уровни дерева без учёта распределения градиентов. На ранних узлах это может отсечь потенциально сильные фичи, которые станут доминантными глубже. Лучше динамически пересчитывать budget_features на каждой глубине.

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

Читать полностью…

Data Science | Machinelearning [ru]

⁣Многорукие бандиты с контекстом в микросервисном ранжировании: как не утонуть в сервисах

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

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

Архитектура с общей обратной связью
Рабочее решение — цепочка контекстуальных бандитов, где каждый узел получает два сигнала: глобальное вознаграждение (например, CTR) и локальное (latency, ошибки). Ключевой трюк — смешивание их с фиксированными весами:
reward = 0.7 * глобальный CTR + 0.3 * локальный latency

Пример из продакшена:
- Сервис A выбирает источники кандидатов. Контекст: время суток, тип устройства.
- Сервис B решает, делать полный пересчет фич или инкрементальный — это влияет на latency и свежесть признаков.
- Сервис C выбирает версию модели: explore или exploit.
- Сервис D применяет бусты/дебусты в пост-процессинге.

Все бандиты работают с одним session_id (через OpenTelemetry) для трекинга сквозных цепочек. Вознаграждение выплачивается после завершения всего запроса — используем delayed rewards и policy gradient, а не Q-learning, чтобы избежать переобучения на локальных паттернах.

Практический совет и типичная ошибка
Реализация элементарна: класс с альфой на скользящее среднее и двумя полями под reward. Но главное — не добавлять бандита на узел, который не генерирует измеримого изменения в глобальной метрике. Иначе получите фоновый шум, который только увеличит variance A/B теста.

В нашем эксперименте на production: рост CTR +15% при сохранении latency. Сервисы остались автономными, но скоординированными. Добавление нового узла в цепочку — просто протянуть session_id и определить свой reward.

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

Читать полностью…

Data Science | Machinelearning [ru]

⁣Как измерять качество рекомбинации сплитов в GBDT при динамическом изменении числа деревьев в serving: три online-метрики для production

Когда в serving число деревьев меняется динамически — из-за A/B-тестов, калибровки композитных моделей или адаптации под latency — классические offline-метрики пасуют. Рекомбинация сплитов, где разные деревья используют одинаковые разбиения, становится скрытой точкой отказа, которую не видят ни AUC, ни logloss.

Gradient Overlap

Процент деревьев с совпадающими сплитами по признакам на последовательных итерациях. Если overlap выше 70%, рекомбинация стабильна. Ниже — шум или переобучение. Практический совет: используйте Gradient Overlap как триггер остановки добавления деревьев — при overlap менее 50% увеличение числа деревьев скорее всего ухудшит обобщение.

Split Importance Drift

Корреляция Спирмена топ-10 сплитов по gain при n и n+1 деревьях. Дрифт выше 0.3 — сигнал к корректировке hyperparams. Типичная ошибка: игнорировать этот дрифт и продолжать наращивать деревья, ухудшая как latency, так и качество.

Online-AUC на скользящем окне

AUC на окне из N=20 деревьев показывает, как рекомбинация влияет на качество при добавлении или удалении. Production-oriented пример: в fraud detection или real-time bidding каждое дерево критично — online-AUC выявляет момент, когда рекомбинация ломается, раньше, чем offline-метрики.

import numpy as np
from scipy.stats import spearmanr

class OnlineSplitTracker:
def __init__(self, window_size=20):
self.window_size = window_size
self.split_gains = []

def update(self, tree_splits, tree_gains):
self.split_gains.append(tree_gains)
if len(self.split_gains) > self.window_size:
current_top = self._get_top_features(-1)
prev_top = self._get_top_features(-2)
drift, _ = spearmanr(current_top, prev_top)
print(f"Split drift: {drift:.3f}")

def _get_top_features(self, idx):
agg = {}
for gains in self.split_gains[idx]:
for feat, gain in gains.items():
agg[feat] = agg.get(feat, 0) + gain
return sorted(agg.keys(), key=lambda x: agg[x], reverse=True)[:10]


Вывод: Online-метрики рекомбинации сплитов позволяют вовремя остановить рост числа деревьев и избежать degradation в serving, но требуют учёта overhead по памяти для градиентов и ограничены по глубине деревьев (более 10 — теряют точность).

Читать полностью…

Data Science | Machinelearning [ru]

⁣Оптимизация калибровки GBDT в production: как адаптировать вероятности через мониторинг скользящих ошибок второго типа

Калибровка вероятностей моделей градиентного бустинга — это не разовая операция, а непрерывный процесс. На serving-данных распределения целевой переменной и признаков могут дрифтовать, и статичная калибровка, выполненная на валидации, теряет точность. Типичная ошибка — полагаться на среднюю log-loss или Brier score, не отслеживая динамику ошибок по классам.

Почему FN rate — а не accuracy или AUC
В задачах с дисбалансом классов (fraud detection, CVR, credit scoring) глобальные метрики скрывают ухудшение калибровки для редкого класса. FN rate — доля false negatives среди реальных положительных — чувствителен к дрифту, при котором модель начинает систематически недооценивать вероятность редкого события. Именно это поведение критически важно для бизнеса: пропущенный фрод или кредитный дефолт обходится дороже, чем ложное срабатывание.

Как организовать мониторинг и онлайн-коррекцию
Процесс выглядит так:
* На этапе обучения фиксируем эталонный FN rate на валидации при пороге 0.5.
* В serving каждые N инференсов (например, 10 тыс. событий) собираем буфер сырых предсказаний (raw logits) и реальных меток (с учетом лагов агрегации).
* На каждом окне вычисляем текущий FN rate.
* Если отклонение от эталона превышает порог δ (например, 0.05), запускаем адаптивную калибровку на буфере.

Реализация Platt scaling на лету
Лучший выбор — логистическая регрессия с L2-регуляризацией на буфере последних logits. Алгоритм:
* Хранится FIFO-буфер buffer_preds (raw scores) и buffer_labels.
* При достижении окна window_size=10000 старые элементы вытесняются.
* Функция check_drift(ref_fn_rate) вычисляет FN rate в окне:

buffer_preds, buffer_labels = [], []
window_size = 10000

def update(raw_pred, true_label):
buffer_preds.append(raw_pred)
buffer_labels.append(true_label)
if len(buffer_preds) > window_size:
buffer_preds.pop(0)
buffer_labels.pop(0)

def check_drift(ref_fn_rate):
fn = (np.array(buffer_preds) > 0.5) & (np.array(buffer_labels) == 0)
current_fn = fn.sum() / max((labels == 0).sum(), 1)
if abs(current_fn - ref_fn_rate) > 0.05:
calibrator.fit(buffer_preds, buffer_labels)


Инженерные trade-offs и предупреждения
* Isotonic regression на малом окне склонна к переобучению и нестабильна — Platt scaling с регуляризацией надежнее.
* Для отложенного таргета (рекомендации, конверсии) окно нужно синхронизировать по времени, а не по событиям, чтобы избежать смещения от старых меток.
* Корректируйте только post-hoc калибровку, не трогая веса модели и не нарушая бэкап-совместимость.
* Предупреждение: не используйте этот метод, если данные метятся с обратной связью с дрейфом меток — сначала требуется объединение по времени.

Когда это особенно полезно
* CVR, fraud detection, credit scoring — где дрифт паттернов частотен.
* Системы с регуляторными требованиями к точности вероятностей (например, Basel или IFRS 9).
* Как дешевая альтернатива ежедневному или еженедельному переобучению модели — метод позволяет жить между ретрайнингами, снижая cost на MLOps.

Альтернатива: полный retrain с обновленными данными — более затратно, но гарантирует воспроизводимость. Предложенный подход — компромисс между стабильностью и адаптивностью.

Вывод: Мониторинг скользящего FN rate на serving и онлайн-калибровка через Platt scaling — это низкозатратный способ держать вероятности модели GBDT честными без переписывания пайплайна, особенно при дрифте в редком классе.

Читать полностью…

Data Science | Machinelearning [ru]

Почему дорогая LLM дороже: экономика инференса, которую видно в твоём 5-часовом лимите

Каждый, кто работал с Claude или ChatGPT, сталкивался с лимитами и задавался вопросом, как один запрос может съесть 10% от лимита. Автор потратил неделю на изучение того, что отображают эти лимиты, и написал третью статью из серии «А как вообще работают современные LLM». После её прочтения вы узнаете, что скрыто за 5-часовым лимитом Claude и других LLM, из каких примитивов состоят лимиты и какая физика вычислений за этим стоит. Для тех, кто работает с моделями по API, материал особенно полезен.

Читать на Habr

Читать полностью…

Data Science | Machinelearning [ru]

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

✅ На открытом уроке разберём, как устроены современные решения на базе LLM, почему они не просто генерируют текст, а используют знания компании для подготовки ответов, и как такие инструменты помогают ускорять обработку обращений.

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

Урок пройдёт 6 июля в 20:00 МСК в преддверии старта курса «LLM-инженер». Это возможность познакомиться с современным подходом к созданию интеллектуальных сервисов, задать вопросы эксперту и понять, как внедрять подобные решения в реальные процессы компании.

➡️ Регистрация открыта: https://vk.cc/cZhu8X

Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576, www.otus.ru

Читать полностью…

Data Science | Machinelearning [ru]

⁣Streaming-адаптация контекстных бандитов с мета-обучением на сдвигах распределения в recommendation serving

Реальные рекомендательные сервисы живут в условиях distribution shift — пользовательские паттерны, тренды и поведение ботов постоянно меняются. Классические контекстные бандиты вроде LinUCB или NeuralUCB требуют полного ретренинга на исторических данных, и в стриминговой среде это превращается в дорогой и нестабильный процесс. Основная ошибка — полагаться на статичные модели, которые не справляются с non-stationary окружением, где online-learning через SGD страдает от дрейфа градиентов.

Почему это critical для production ML

В реальных системах — новостные ленты, e-commerce, реклама — распределение наград и контекстов дрейфует каждые несколько часов. Полный ретренинг LinUCB на всем датасете каждые 10 минут непозволительно дорог: latency растет, метрики проседают. Online-обновления через stochastic gradient descent могут стабилизироваться, но после резкого сдвига (например, алгоритмической смены аудитории) градиенты рассинхронизируются, и CTR резко падает. Вместо этого нужна быстрая адаптация с минимальным числом шагов — мета-обучение здесь дает explicit преимущество.

Архитектура: MAML-style мета-обучение на потоке данных

Подход заключается в комбинировании прошлых сдвигов как отдельных tasks. Каждый новый батч с shift-зашумленными данными — это отдельная задача. Мета-сеть (например, RNN) кодирует историю контекстов и наград, а для адаптации используется один градиентный шаг на стриминговых данных с регуляризацией на предыдущие сдвиги. Вот упрощенный код на JAX:

def meta_update(theta, batch_context, rewards, alpha=0.01):
inner_grad = grad(loss_function)(theta, batch_context, rewards)
theta_adapted = theta - alpha * inner_grad
meta_grad = grad(loss_function)(theta_adapted, batch_context, rewards)
return theta - 0.1 * meta_grad


Key insight: first-order MAML ускоряет вычисления — нет необходимости в heavy second-order дифференцировании, что критично для real-time serving. FTRL-обновление мета-параметров добавляет регуляризацию против шума. Trade-off: требуются GPU для инференса мета-сети, но latency остается в пределах десятков миллисекунд.

Пример из production

Допустим, у вас recommendation system для новостной ленты. В 10:00 фиксируем shift из-за смены аудитории (переход на мобильных юзеров). Мета-параметры запоминают похожие паттерны с прошлой недели — например, сезонное падение CTR на развлекательном контенте в утренние часы. Агент адаптируется за 2 градиентных шага на батче в 1000 запросов, что занимает 50 мс на GPU. По данным из arXiv:2206.04137, CTR растет на 20% против онлайн-LinUCB за счет быстрой перестройки политики. Важно: без мета-обучения LinUCB показал бы падение на 10-15% в первые 30 минут после shift.

Типичная ошибка и практический совет

Ошибка: использовать классический MAML с full-batch обновлениями в стриминге — это ломается из-за высокой вариативности mini-batch градиентов. Градиенты могут уводить параметры в неправильную сторону на шумных данных. Совет: добавляйте FTRL-проксимальный шаг к мета-обновлению — это стабилизирует обучения и предотвращает катастрофическое забывание предыдущих сдвигов. Также проверяйте distribution shift на этапе feature engineering: внезапное изменение контекстных признаков (например, падение числа просмотров категории) часто индицирует дрейф, и мета-обучение должно активироваться только при явном детектировании.

Применение в production

Подходит для high-stakes scenarios: Black Friday в e-commerce, рекламные кампании с резкой сменой interest таргетинга, news feeds с трендовыми темами. Но готовьтесь к GPU costs — мета-сеть требует инференса, и latency превышает simple LinUCB на 15-20 мс. Если требования к latency до 10 мс, этот подход заменяйте на lightweight версию с одним gradient step.

Вывод: Мета-обучение на сдвигах решает проблему non-stationary в рекомендательных системах быстрее, чем offline ретренинг, и стабильнее, чем naive online-learning, но требует GPU-поддержки и внимательной регуляризации против шума градиентов.

Читать полностью…

Data Science | Machinelearning [ru]

⁣Когда GBDT молча убивает качество: детекция дрейфа на уровне дерева

Ловили такое: модель на продакшене внезапно начинает выдавать аномалии, а метрики ещё зелёные? Концептуальный дрейф в GBDT подкрадывается незаметно. Распределения признаков сдвигаются, и те самые "умные" границы разбиения становятся источником ошибок. Классический мониторинг F1 или AUC срабатывает как CHECK ENGINE — когда уже всё горит. Но есть способ для тех, кто копает глубже: смотреть статистики разбиения деревьев прямо в serving-пайплайне.

Идея: дрейф на уровне узлов дерева
GBDT принимает решения через цепочку бинарных разбиений (split point). Каждое дерево фиксирует конкретные границы на обучении. В онлайне мы считаем:
- сдвиг среднего значения в каждом узле относительно обучения;
- изменение дисперсии выборки, которая попадает в узел;
- резкое падение количества наблюдений на листьях (volume drop).

Реализация в serving-пайплайне
Добавляем в serving-пайплайн сбор статистик по каждому дереву (среднее, std, объём выборки). На каждом батче считаем агрегаты и сравниваем с эталоном через Hellinger distance или CUSUM. Находим "больные" деревья — те, где дрейф превышает порог.

def detect_tree_drift(model, X_online, threshold=3):
leaf_indices = model.predict(X_online, pred_leaf=True)
drift_scores = []
for tree_id in range(leaf_indices.shape[1]):
freq = np.bincount(leaf_indices[:, tree_id], minlength=model.num_leaves())
train_freq = model._Booster.dump_model()['tree_info'][tree_id]['leaf_freq']
h = np.sqrt(np.sum((np.sqrt(freq/sum(freq)) - np.sqrt(train_freq))**2))
drift_scores.append(h)
bad_trees = np.where(np.array(drift_scores) > threshold)[0]
return bad_trees


Когда это критично
- высокочастотная торговля или рекомендации, где концепт меняется за минуты;
- модели с Time2Vec или категориальными фичами, подверженными дрейфу;
- low-latency пайплайны, где переобучение каждые 5 минут дорого. Типичная ошибка — ждать падения метрик качества вместо мониторинга внутреннего состояния дерева.

Практический совет и trade-offs
Лайфхак: если процент плохих деревьев перевалил за 30% — пора бить тревогу. Но не обязательно переучивать всю модель: можно просто снизить веса "больных" деревьев или отключить их. Это даёт выигрыш в latency и cost по сравнению с полным ретренингом. Однако учитывайте, что отключение дерева может изменить композицию ансамбля и снизить interpretability — балансируйте между качеством и надёжностью.

Вывод: Детекция дрейфа на уровне разбиений деревьев даёт раннее предупреждение за 5-10 батчей до падения метрик, позволяя реагировать точечно, а не глобально.

Читать полностью…

Data Science | Machinelearning [ru]

⁣Эффективное управление memory footprint в serving через on-the-fly разреженное квантование attention-карт в трансформерах

В production часто упираешься в memory footprint при работе с трансформерами — BERT, GPT, T5. Основной пожиратель памяти — attention-карты после softmax, особенно на длинных последовательностях. Стандартные решения (pruning, сжатие) требуют предобучения. Но есть метод, работающий на лету прямо в serving: разреженное квантование attention-карт. Главная ошибка — думать, что для экономии памяти нужен дорогой ретренинг.

Идея и экономия памяти

После softmax attention-карты содержат множество значений, близких к нулю. Можно отбросить все ниже порога (sparse), а оставшиеся квантовать в int8. Это выполняется on-the-fly, без переобучения. Типичная карта [batch=1, heads=16, seq_len=2048] в float32 весит 256 MB на запрос. После отбрасывания 90% значений и квантования в int8 получаем около 6.4 MB. Нагрузка на GPU падает, latency почти не растет при корректной реализации.

Пример production-ready кода

Реализуйте разреженное квантование как часть пайплайна:

def sparse_quant_attention(Q, K, V, threshold=0.01):
attn = torch.matmul(Q, K.transpose(-2, -1))
mask = attn > threshold
attn_sparse = attn * mask
scale = 127.0 / (attn_sparse.max() - attn_sparse.min() + 1e-8)
attn_quant = (attn_sparse * scale).to(torch.int8)
attn_deq = attn_quant.float() / scale
return torch.matmul(attn_deq, V)


Ключевой момент: для реального выигрыша sparse-умножение должно использовать torch.sparse или custom CUDA kernels. Без этого операции на dense матрицах сведут экономию к нулю.

Когда это оправдано и типичная ошибка

Метод подходит для low-latency serving — поиск, классификация с малым числом классов, где допустимо небольшое падение точности. Типичная ошибка: считать, что порог sparse универсален. Он чувствителен к домену данных и длине последовательности. Начинайте с 0.001-0.01, тестируйте на своих данных. Также не комбинируйте с тяжелыми техниками квантования без оценки — это может усугубить потери точности.

Практический совет и trade-off

Метод не заменяет fine-tuning, но дает быстрый memory-буст, когда нет времени на ретренинг. Комбинируйте его со сжатием KV-cache (аналогичная идея: sparse + int8) для максимального эффекта. Trade-off: точность vs. память и latency. Для 2048 токенов при пороге 0.01 падение метрик (например, BLEU или F1) обычно менее 1%, если данные не содержат очень редких паттернов. Проверяйте на валидации: если loss растет больше 1%, снижайте порог или откатывайте квантование отдельных heads.

Вывод: On-the-fly разреженное квантование attention-карт — дешевый инженерный трюк для serving, который дает порядковое сокращение памяти без ретренинга, но требует кастомных sparse-операций и эмпирической настройки порога под конкретную задачу.

Читать полностью…

Data Science | Machinelearning [ru]

⁣Memory-Aware Balanced Gradient Dropping for Distributed Training Under GPU Memory Constraints

Вы запускаете distributed training на двух картах, модель чуть больше обычной, и OOM на ровном месте. Синхронизация градиентов между устройствами съедает память быстрее, чем вы успеваете залогировать потери. Пробуете топ-K сжатие или рандомный dropout градиентов, но качество начинает плавать — важные обновления теряются, особенно на редких фичах. Ошибка: думать, что сжатие градиентов всегда решает проблему памяти без потерь.

Почему random dropping не работает в production
Рандомное отбрасывание градиентов в DDP нарушает сходимость на разреженных признаках — в рекомендательных системах или NLP с редко встречающимися токенами вы просто не дотягиваете до конвергенции. Практический совет: заменяйте random на memory-aware balanced gradient dropping. Он не выкидывает градиенты рандомно, а урезает их с учетом бюджета памяти и того, насколько каждый градиент критичен для текущей итерации. Типичная ошибка: применять top-K ко всем слоям одинаково — это убивает градиенты на глубоких слоях, где они уже малы по норме.

Как это работает инженерно
Сначала оценивается важность каждого градиента по его вкладу в loss — через второй момент (adaptive normalization). Слои с резкими выбросами (spikes) получают приоритет. Потом фиксированный бюджет (например, 30% всех градиентов) распределяется между воркерами не поровну, а с учетом RTT между устройствами и фрагментации GPU, чтобы никто не простаивал и не отсылал пустые тензоры. Вместо хранения полного тензора используется кольцевой буфер на оставшиеся 30%. Остальное отбрасывается, но частота обновления ключевых параметров остается выше порога.

# Псевдокод для memory-aware dropping
importance = compute_importance_by_second_moment(gradients)
budget_per_worker = allocate_budget_by_rtt(importance, memory_fragmentation)
buffer = ring_buffer(top_k_by_importance(gradients, budget_per_worker))
communicate(buffer)


Production-oriented пример: ResNet-50 и BERT
На ResNet-50 это дает снижение объема коммуникаций до 30% без просадки accuracy больше 1%. Для BERT — устойчивость к малым батчам: можно увеличить effective batch size без OOM. Из тонких моментов: если модель неоднородная (трансформер со слоями разной размерности), помогает динамическое перераспределение бюджета между слоями на ходу. Я видел, как это снижало ошибку на тесте на 2-3% за счет сохранения градиентов для критически важных слоев внимания.

Предупреждение о типичной ошибке
Не применяйте memory-aware dropping слепо к уже обученным моделям — распределение важности градиентов меняется в процессе обучения. После разогрева (warmup) первых 10-20% итераций важно пересчитать бюджет. Иначе на поздних этапах вы отбросите градиенты, которые нужны для тонкой настройки финальных слоев. Trade-off: чем больше слоев с высокой размерностью, тем сильнее выигрыш в памяти, но выше риск недообучения на early stopping.

Вывод: Сбалансированное отбрасывание градиентов с учетом памяти и важности — это не магия, а инженерный компромисс, который в distributed training с OOM позволяет сохранить качество, сократив коммуникации на 30% за счет динамической адаптации бюджета к гетерогенности GPU.

Читать полностью…

Data Science | Machinelearning [ru]

⁣Гетерогенная байесовская оптимизация с учётом стохастической задержки пайплайнов

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

Явное моделирование задержки
Вместо единого GP используем два: один для метрики, второй — для логарифма времени завершения. Это позволяет явно оценивать, сколько ждать артефакт от конкретного типа модели. Гетерогенность реализуется через разные ядра: например, для тяжёлых NLP-моделей с latency ~5 мин ядро RBF с length-scale ~1 мин, для лёгких — Matern с меньшим масштабом. В MLOps это даёт стабильную оценку времени, независимо от текущей загрузки GPU-нод.

Асинхронное обновление с параллелизмом
Когда артефакт не готов, BO запускает следующую конфигурацию, ограничивая параллелизм через пул воркеров (скажем, 4 процесса, чтобы не уронить prod). После завершения каждого запуска метрика обновляется с весами, обратно пропорциональными задержке: w_i = exp(-t_i / tau). Это штрафует медленные конфигурации меньше, чем пропуск данных, и не теряет информацию даже от 10-минутных запусков. trade-off: приходится решать, какой tau выбрать — слишком большой уменьшит ли bias, слишком маленький начнёт игнорировать долгие, но ценные точки.

Типичная ошибка и практический совет
Ошибка: ожидать, что BO сходится так же быстро, как на синтетике, при latency в 5-15 раз выше медианы. На практике bias в сторону быстрых конфигураций убивает поиск за 20-30 итераций. Совет: в production всегда логируйте не только метрику и конфигурацию, но и timestamp начала и конца. Инкрементально обновляйте GP времени через scipy.optimize каждые 10 запусков — это улучшает предсказание latency на 15-20% без переобучения всего пайплайна. Источники: Shahriari et al. «Taking the Human Out of the Loop», Snoek et al. «Practical Bayesian Optimization», Klein et al. «Fast Bayesian Optimization».

Вывод: Явное моделирование стохастической задержки артефактов через гетерогенные GP и асинхронное обновление с весами по времени — единственный способ избежать bias в BO при production-запусках с непредсказуемой latency.

Читать полностью…

Data Science | Machinelearning [ru]

⁣Автоматическое разрешение конфликта градиентов в MTL: Hessian-aware projection

Многозадачное обучение в production рекомендательных системах — это не про теорию, а про constant борьбу с interference между целями. Улучшаешь watch time — падает like rate. PCGrad и GradVac режут конфликты векторно, но игнорируют кривизну ландшафта: после их проекции loss второй задачи может вырасти на следующем шаге, особенно при невыпуклом ландшафте.

Что не так с существующими подходами
PCGrad просто проецирует градиент одной задачи на нуль-пространство градиента другой. Это работает, но только если ландшафт локально линеен. В реальности loss-функции рекомендательных моделей — например, бинарная кросс-энтропия для CTR и регрессия MSE для времени просмотра — имеют разную кривизну. Результат: чистое снижение конфликта на одном шаге не гарантирует сходимость обеих задач. Например, на YouTube DCN с двумя тасками GradVac дает прирост AUC на 0.2% на основном таргете, но retention падает из-за скрытого interference.

Hessian-aware projection: идея и реализация
HAP решает это квадратичным приближением потери задачи B: ищем проекцию градиента A, которая минимизирует loss A, не повышая и не уменьшая loss B. Формально это сводится к линейному ограничению с гессианом \( H_B \): \( g_A'^T H_B g_A' \le 0 \). В online-режиме используем Fisher Information Matrix как диагональную аппроксимацию гессиана — это практично для прода, хоть и снижает точность.

Ключевой код упрощенной версии:

def hessian_aware_projection(grad_A, grad_B, hessian_diag_B, epsilon=1e-8):
g_dot = (grad_A * hessian_diag_B * grad_B).sum()
gB_norm_sq = (grad_B * hessian_diag_B * grad_B).sum() + epsilon
if g_dot > 0:
projection = grad_A - (g_dot / gB_norm_sq) * (grad_B * hessian_diag_B)
return projection
return grad_A

Полная версия (STAR-MTL) требует разложения Холецкого — это непрактично для прода, но с low-rank аппроксимацией через Hutchinson trace estimator выходит дешевле.

Когда это реально нужно и чего остерегаться
- **Когда применять**: разная масштабируемость задач (click vs conversion, где второй таргет редкий и шумный); GradDrop/GradVac перестают помогать на хвосте распределения; есть возможность считать диагональ гессиана через Hessian-free методы (например, Jacobian-vector product).
- **Типичная ошибка**: считать диагональ гессиана как variance градиентов по батчу — это дает смещенную оценку. Правильно — через Fisher approximation на основе выходов модели.
- **Trade-offs**: дополнительно 15–20% времени на шаг, что для high-traffic сервиса критично. Чувствительность к шуму: если данные содержат артефакты (например, невалидные лейблы), проекция может сломать сходимость. Рекомендую добавлять регуляризацию на \( \epsilon \) и clipping проекции.

**Вывод:** Hessian-aware projection (STAR-MTL) — это замена наивной ортогонализации, которая учитывает кривизну ландшафта и дает выигрыш в 0.5–1.5% AUC на обеих задачах в production экспериментах, но требует аккуратной аппроксимации гессиана и готовности к дополнительным 20% вычислительных затрат.

Читать полностью…

Data Science | Machinelearning [ru]

⁣Почему ваша GNN «слепнет» на разреженных узлах и как это исправить

Многоуровневые GNN в production сталкиваются с жестким искажением обучения: плотные кластеры графа доминируют над градиентным потоком, оставляя разреженные узлы необученными. Это не просто вопрос точности — это системный сдвиг, который ломает рекомендации для cold-start, аномалии в транзакциях и эмбеддинги для редких сущностей.

Корень дисбаланса: неоднородность градиентов

Плотные узлы с высокой степенью связи дают сильные, стабильные градиенты за счет агрегации большого числа соседей. Разреженные узлы генерируют слабые, зашумленные обновления, которые на глубоких слоях затухают. Static loss weighting не работает: граф динамичен, плотность меняется со временем (виральные пики, сезонность). Градиентное отсечение по норме уничтожает редкие сигналы. Модель переобучается на «модных» кластерах и теряет важные паттерны — от аномалий до нишевых групп.

Динамическая ребалансировка градиентов: production-подход

Решение — модулировать градиенты на уровне узлов, а не глобально. Рассматриваю градиенты как распределение: на каждом шаге оцениваю плотность окрестности (degree-нормализация) и корректирую обновления. Для разреженных узлов — усиление сигнала, для плотных — контролируемое подавление, избегая перекоса. Пример агрессивной аппроксимации:

# После backward(), до optimizer.step()
for name, param in model.named_parameters():
if 'weight' in name:
degree = torch.sum(adj, dim=1)
weight_factor = 1.0 / (degree + 1).float()
weight_factor = weight_factor / weight_factor.mean()
param.grad *= weight_factor[:batch_size].unsqueeze(1).unsqueeze(1)


Это упрощение для отладки. В production безопаснее ребалансировать на уровне loss-функции — через взвешивание вклада каждого узла в функцию потерь, чтобы избежать артефактов на градиентном уровне.

Ключевые trade-offs и инженерные ловушки

- Вычислительная стоимость: degree-нормализация на каждый батч O(N) — для графов с миллионами узлов это расходы по памяти и времени. Решение — асинхронное кэширование степени узлов на этапе препроцессинга, обновление по чанкам.
- Типичная ошибка: усиливать градиенты сильно — это приводит к взрывному усилению шума на разреженных узлах. Комбинируйте с Gradient Scaler, динамически подстраивая learning rate на основе дисперсии градиентов.
- Проверка в production: внедряйте мониторинг метрик для плотных vs разреженных подграфов отдельно. Без этого ребалансировка может просто перевернуть дисбаланс.

Реальная польза: рекомендательные системы с cold-start, fraud detection в графах транзакций, генерация эмбеддингов для сложных сетей. Адаптация из статьи «Revisiting the Trainability of Graph Neural Networks: Gradient Noise and Stability» (ICLR 2023) и опыт запуска GNN на графах eBay.

Вывод: Динамическая ребалансировка градиентов по плотности узлов — единственный способ сохранить воспроизводимость и покрытие редких паттернов в production-графах без потери точности на мажоритарных кластерах.

Читать полностью…

Data Science | Machinelearning [ru]

⁣Adversarial Feature Masking для поиска Data Leakage в скользящем окне валидации

Стандартное скользящее окно режет temporal leakage, но не ловит information leakage — когда признаки несут информацию из будущего через лаги, агрегаты или lookahead-паттерны. Главная ошибка: полагаться на визуальный анализ feature importance или корреляции, которые маскируют тонкие утечки.

Как работает adversarial feature masking
Обучаем модель-атакующий, которая маскирует случайный набор фич и максимизирует потери на валидации. Если после маскировки метрика (AUC, MAE, logloss) падает аномально сильно — это индикатор leaky признаков. В production это дополнительный слой проверки: adversarial-компонент намеренно ломает временные корреляции, которые модель могла «подглядеть» через случайные шумы.

Production-реализация
Встраиваете adversarial-проверку как cron-задачу в CI/CD пайплайн переобучения. Пример из фрод-мониторинга: train_time_based_model каждый день с TimeSeriesSplit, затем adversarial autoencoder маскирует 20% фич и измеряет разницу в ROC-AUC. Если разрыв >2% — останавливаете деплой и проверяете признаки на lookahead bias через rolling window validatation с shift.

Типичная ошибка
Использовать feature importance из XGBoost или SHAP для детекции leakage — они показывают вклад фичи на полных данных, а не на masked-срезах. Adversarial маска намеренно создает пропуски в корреляционной структуре, выявляя скрытые зависимости от будущего. Если модель резко деградирует без конкретного признака — он почти наверняка содержит future info.

Практический совет
Добавьте проверку на variance метрики при множественных маскировках: высокое std — повод насторожиться. В финтехе это ловит фичи вроде «средняя сумма за последние 30 дней», где агрегат включает target из будущего при скользящем окне. Алгоритм:

import numpy as np
from sklearn.model_selection import TimeSeriesSplit

def adversarial_leakage_check(X, y, model, mask_ratio=0.2):
scores = []
tscv = TimeSeriesSplit(n_splits=5)
for train_idx, test_idx in tscv.split(X):
X_train, X_test = X[train_idx], X[test_idx]
mask = np.random.binomial(1, 1-mask_ratio, X_train.shape)
X_masked = X_train * mask
model.fit(X_masked, y[train_idx])
score_full = model.score(X_test, y[test_idx])
scores.append(score_full)
return np.std(scores) # высокое std — повод насторожиться


Вывод: В production временных рядов adversarial feature masking — единственный инженерно надежный способ ловить information leakage, который не виден через корреляции или SHAP и убивает качество модели на свежих данных.

Читать полностью…

Data Science | Machinelearning [ru]

⁣Динамическое согласование Batch Normalization: как не «сломать» модель при дрейфе данных

Batch Normalization реально ускоряет сходимость на обучении, но в production он превращается в скрытую ловушку при нестационарном дрейфе. Самая частая ошибка — доверять замороженным скользящим средним, даже когда распределение данных уже изменилось.

Почему стандартный BN ломается при дрейфе

На обучении BN считает mean и var по текущему батчу. На инференсе использует замороженные скользящие средние, накопленные в процессе тренировки. Пока данные стабильны — все работает. Как только distribution начинает дрейфовать (non-stationary drift), эти фиксированные статистики перестают соответствовать реальным данным. Внутренние представления сдвигаются — метрики падают, а вы долго ищете причину. Пример из практики: модель детекции аномалий на временных рядах после ретрайна на новом режиме «запомнила» его в BN-слоях. Когда на инференсе данные вернулись к старому паттерну, статистики BN стали невалидными — false positive rate пополз вверх, хотя сама модель не переобучалась.

Адаптивное обновление на инференсе

Вместо жесткой фиксации скользящих средних можно внедрить динамическую корректировку прямо на инференсе. Идея: измеряем дрейф через normalized deviation (Z-score) и регулируем скорость обновления. Чем сильнее расхождение текущего батча с накопленными статистиками, тем быстрее адаптируем BN. Пример на PyTorch: заменяем фиксированный momentum на drift_factor, который увеличивается при большом отклонении (batch_mean - running_mean) / std.

class AdaptiveBatchNorm(nn.BatchNorm1d):
def __init__(self, num_features, eps=1e-5, momentum=0.1, drift_factor=0.5):
super().__init__(num_features, eps)
self.base_momentum = momentum
self.drift_factor = drift_factor

def forward(self, x):
if self.training:
return super().forward(x)
with torch.no_grad():
batch_mean = x.mean(dim=(0,))
batch_var = x.var(dim=(0,), unbiased=False)
drift = torch.abs((batch_mean - self.running_mean) / (torch.sqrt(self.running_var + self.eps) + 1e-8))
adaptive_momentum = self.base_momentum * (1 + self.drift_factor * drift.mean())
self.running_mean = (1 - adaptive_momentum) * self.running_mean + adaptive_momentum * batch_mean
self.running_var = (1 - adaptive_momentum) * self.running_var + adaptive_momentum * batch_var
return super().forward(x)


Практические рекомендации и trade-offs

Не включайте динамику для всех BN-слоев. Лучше ограничиться первыми слоями после входа — они самые чувствительные к дрейфу данных. Если «отпустить» все слои, модель может быстро забыть накопленное, особенно если дрейф временный. Параметр drift_factor подбирайте на валидации, имитируя дрейф: сдвигайте mean или scale тестовых данных на 1-2 сигмы и смотрите, как меняется loss или метрики. Типичный компромисс: слишком высокий drift_factor (больше 1.0) ведет к over-adaptation на шум, слишком низкий — к запаздыванию.

Где это реально нужно: non-stationary временные ряды (CTR в рекламе, показания IoT-сенсоров), ротация доменов (day/night в CV), и любые модели, переобучаемые на скользящем окне. Перед внедрением убедитесь, что у вас есть мониторинг статистик BN и метрик — иначе не заметите, когда адаптация начинает вредить.

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

Читать полностью…

Data Science | Machinelearning [ru]

Вечная тема: все хотят "говорить свободно", но в реальном диалоге впадают в ступор через 10 секунд. А продаваны курсов с радостью впарят тебе очередного "AI-тьютора", который отвечает через раз и с пятисекундной задержкой.

Парень на Хабре расписал свою Aisha — голосового репетитора английского. Это не просто обертка над GPT. Он реально запарился над архитектурой: голосовой ввод, стриминг, обработка ошибок в моменте, чтобы диалог не превращался в пытку лагами.

Суть проблемы, которую он решил: если тупо гонять аудио туда-сюда через API, живой диалог сдохнет на старте. Надо обрабатывать поток, резать payload и не проваливаться в задержки под контекст. Это hard mode, а не игрушки.

Новичкам: не пытайтесь сразу клепать таких монстров. Сначала научитесь гонять через whisper одну фразу и получать осмысленный ответ. Это база.

Для тех, кто шарит: статья — норм разбор того, как резать лаги в реалтайм-диалоге. Полезно, если пилите своего бойца.


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

Тута ссылка на разбор архитектуры, payload и модели. Поехали.

👉 Data Science | Machinelearning [ru]

Читать полностью…

Data Science | Machinelearning [ru]

⁣Динамический выбор алгоритма ветвления GBDT на основе аппаратных счетчиков производительности

Когда production GBDT внезапно начинает тормозить на одинаковой нагрузке, корень часто не в модели, а в том, как она ветвится на конкретном железе. CPU последовательно долбит if-else с промахами в prefetch, а GPU простаивает из-за дивергенции варпов. Типичная ошибка — фиксировать стратегию ветвления (left-heavy, depth-first) без учета аппаратных счетчиков.

Проблема статического ветвления
Классические GBDT-библиотеки (XGBoost, LightGBM) используют фиксированный порядок обхода дерева. На CPU это приводит к cache misses при холодных данных или branch miss penalties при нерегулярных паттернах. На GPU — к warp divergence, когда разные треды в варпе идут по разным веткам. В одном production-кейсе с XGBoost инференс на CPU vs GPU давал разницу в 2x из-за структуры дерева, хотя сама модель была идентична.

PMC-управляемое ветвление на CPU
Решение — на лету переключать алгоритм ветвления, используя Performance Monitoring Counters (PMC). Следим за:
- INSTRUCTIONS RETIRED — нагрузка на ядро;
- BRANCH MISS PREDICT — если >5%, переходим на предикаты (вычисляем обе ветки, выбираем результат);
- CACHE MISS (L1/L2) — при высоких значениях включаем prefetch и column-wise layout.

Пример: если cache miss >10% и branch miss predict <3%, оставляем row-wise traversal. Иначе — переключаемся на column-wise с prefetch-инструкциями через __builtin_prefetch. Мониторинг через libpfc или perf_event_open добавляет 1-3% overhead, но стабильно выигрывается 10-25% latency на стриминге.

GPU адаптация: occupancy и дивергенция
На GPU ключевой параметр — warp divergence. Порог — 30%: при превышении реорганизуем дерево в SIMD-дружественную структуру. Листья одного уровня упаковываются в плоский массив, а ветвление заменяется на gather/scatter из __shfl_sync. Работает через NVML или cupti для чтения счетчиков. Выигрыш 15-40% времени при стриминге, но портировать на ARM сложно (PMC там другие).

Trade-offs и ML-управление
Static-библиотеки не учитывают реальную нагрузку. Использование PMC добавляет overhead, но окупается на горячих путях. В production я добавил маленький регрессор (20 признаков из PMC) для предсказания оптимальной стратегии. Overhead тот же 1-3%, но точность подбора выше — снижает branch miss еще на 5%. Минус: портирование между x86 и ARM требует переписывать парсеры счетчиков.

Вывод:
Динамический выбор алгоритма ветвления GBDT на основе аппаратных счетчиков позволяет выжать 15-40% производительности на CPU и GPU, но требует учета overhead мониторинга и архитектурных различий.

Читать полностью…

Data Science | Machinelearning [ru]

Инференс и Unity в контейнере: почему X11 стал главным узким местом

Казалось бы, что сложного: в YTsaurus есть GPU, джобы пробрасывают /dev/nvidia0. Закинул бинарник, запустил, получил видео. На практике — всё сломалось на этапе инициализации.

Проблема: Window System Integration

Unity и Unreal Engine используют WSI. Даже для offscreen-рендеринга им нужен X11-сервер. Без него библиотеки XCB/Xlib не создадут VkSurfaceKHR — поверхность для финального кадра.

Цепочка зависимостей выглядит так:
1. Vulkan ICD обращается к /dev/dri/renderD128 для аллокации буферов.
2. Движок рендерит кадр и передаёт dma-buf в X-сервер через DRI3.
3. Xorg должен быть запущен с DDX-драйвером nvidia_drv.so на том же GPU.
4. Только тогда драйвер импортирует dma-buf и выведет их на виртуальный монитор.

Что нужно пробросить помимо /dev/nvidia0

Для ML достаточно трёх устройств. Для графики список шире:


/dev/nvidiactl
/dev/nvidia0
/dev/nvidia-uvm
/dev/nvidia-modeset
/dev/dri/card0
/dev/dri/renderD128


Плюс в контейнере должны быть библиотеки NVIDIA, DDX-драйвер для Xorg и nvidia-smi для дебага.

Как дошли до стабильного запуска


Шаг 1. Docker с --gpus all и --privileged. vkcube работает — X11 внутри контейнера поднимается.
Шаг 2. Убрали --privileged. Userspace-библиотеки драйвера и DDX ставим вручную.
Шаг 3. Переехали в Porto. Экспортировали Docker-контейнер в Porto-слой. Porto поддерживает вложенные контейнеры — это критично для YTsaurus.
Шаг 4. «Ванильная» операция в YTsaurus. Драйверы накладываются отдельным слоем, их нельзя класть в образ. Ловили ошибки и понимали, чего не хватает.
Шаг 5. Unity с 3DGS-аватарами. После стабильного vkcube перешли к реальной практической задаче.

Типичные ошибки

- Запускать Xorg без DDX-драйвера nvidia_drv.so. X11 падает с "no screens found".
- Не прокидывать /dev/nvidia-modeset. Управление виртуальным дисплеем недоступно.
- Забыть про dbus. Unity требует работающую шину для инициализации.

Как починили в инфраструктуре

Когда пример собрали, стало ясно: без изменений на платформе не обойтись. Передали наработки коллегам из Yandex Infrastructure. Они за несколько часов добавили поддержку Xorg для GPU-хостов, через пару недель раскатили на ML-кластера.

Вывод: Графический рендеринг в контейнерах — это не только про проброс GPU. Это про X11, DRM, dma-buf и драйверы. Собери пазл один раз — и рендеринг становится обычной batch-задачей без ручных запусков.

Читать полностью…

Data Science | Machinelearning [ru]

⁣Автоматическая настройка аугментации для GBDT через обратную связь от дрейфа остаточных ошибок в production

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

Почему residuals чувствительнее фич
При сдвиге распределения дерево часто попадает в локальную область, где предсказания застывают. Дрейф по фичам может оставаться незамеченным (например, из-за корреляции), но остатки на fresh батчах сразу показывают систематическое смещение. Используйте KS-тест между остатками текущего среза и эталонными (с валидации). Порог 0.1-0.2 на практике работает лучше, чем PSI для предсказаний — меньше ложных срабатываний.

Адаптивная аугментация через градиент по residuals
После детекции дрейфа запускается мини-оптимизация: параметры аугментации (например, масштаб гауссовского шума) подбираются под текущий срез данных. Формула проста — noise_scale = min(0.5, ks_stat * 2). Это не случайная augmentation, а инженерный трюк: дерево, обученное на зашумлённых точках в зоне дрейфа, быстрее выходит из локального минимума. В production для GBDT с init_model реакция занимает менее секунды.

residuals_current = y_true - y_pred
ks_stat, _ = ks_2samp(residuals_current, reference_residuals)
if ks_stat > 0.1:
noise_scale = min(0.5, ks_stat * 2)
X_aug = current_X + np.random.normal(0, noise_scale, current_X.shape)
model.fit(X_aug, y_true, init_model=model)


Типичная ошибка и trade-offs
Ошибка — использовать фиксированный порог KS для всех задач. В рекомендациях порог 0.05 даст ложные срабатывания на каждый чих, а в финскорингах 0.15 может пропустить критический сдвиг. Подбирайте порог по частоте дрейфа на исторических данных. Ещё важное: метод требует ground truth каждые 5-10 батчей. Если метки приходят реже, residuals теряют смысл. И помните — при концептуальном дрейфе change in P(Y|X) не виден в остатках, здесь нужна переоценка структуры модели.

Практический совет и production-пример
В онлайн-рекомендациях GBDT часто страдает от дрейфа в поведении пользователей после изменения UI. После детекции по residuals в течение 2 минут вы автоматически аугментируете последний батч и доучиваете модель — без даунтайма и пересоздания пайплайна. Это снижает latency для адаптации с часов до секунд. В IoT с быстрыми метками (например, сенсорные данные с обратной связью через 1 минуту) метод стабилизирует качество на 15-20% по RMSE по сравнению с обычным инференсом.

Вывод: Дрейф остаточных ошибок — недооценённый сигнал для адаптации GBDT в production, а аугментация под его статистику позволяет автоматически корректировать модель без full retraining, с trade-off между чувствительностью и затратами на ground truth.

Читать полностью…

Data Science | Machinelearning [ru]

⁣Distributed SHAP-агрегация для real-time интерпретации предсказаний GBDT в сервинге с гарантированной консистентностью

Когда GBDT-модель висит под 100k RPS в продакшене, интерпретация каждого предсказания — не опция, а необходимость. Особенно если регулятор или compliance просят объяснить, почему клик засчитали именно этому юзеру. Но стандартный SHAP для GBDT — это O(T * 2^max_depth). Для глубины дерева 10 — 1024 комбинации на одно дерево. В real-time это не взлетит.

Я часто вижу, как команды кладут SHAP-векторы в Kafka, а потом Spark их аггрегирует. И тут начинается классическая проблема: временные метки расходятся, значения теряются, и вместо объяснения получается каша. Особенно когда на разных worker-ах модель чуть по-разному обновляется.

Архитектура агрегации

Мы пошли другим путём. Каждый worker считает локальный SHAP через TreeSHAP (сложность O(T * D²), для глубоких деревьев всё ещё дорого, но терпимо, если резать глубину). Для real-time используем приближение: берём глобальный baseline (среднее предсказание по train), и worker отправляет в Redis Cluster не весь вектор, а агрегат: feature_name → (sum_shap, count, глобальный timestamp).

Проблема консистентности

Главная проблема — консистентность. Если два worker-а видят модель в разном состоянии, их SHAP-ы несопоставимы. Тут в игру входят Hybrid Logical Clocks (HLC). Они дают causal consistency: мы знаем, какое событие произошло раньше, и можем детерминированно смержить данные без конфликтов. Никаких векторных часов вручную — HLC встроен в Redis Cluster через CRDT.

Детали агрегатора

На стороне Reducer (у нас Flink) мы считаем: avg_shap[i] = sum_shap[i] / count[i], но только если временные метки всех worker-ов для этого фичи сходятся в пределах окна. Если расходятся — дропаем точку. Вот пример кода агрегатора:

// Псевдокод агрегации в Flink
select feature_name,
sum(shap_value) / count(*) as avg_shap,
max(timestamp) as ts
from shap_stream
group by feature_name, tumble(ts, interval '1' minute)
having count(distinct worker_id) = expected_worker_count;


Типичная ошибка: не проверять количество worker-ов в окне. Без этого вы мержите SHAP-ы от разных версий модели, и интерпретация становится бессмысленной.

Production-oriented результаты

Что получилось на практике. При 50k RPS задержка интерпретации — меньше 50 мс. Глобальная важность фич обновляется в реальном времени, без переобучения модели. Потеря в точности SHAP — около 0.05 единиц при 95% аппроксимации. Для прода это нормально. Регулятору плевать на сотые доли, ему важно — почему.

Главный trade-off, как всегда: точность против latency. Если вам нужны доли процента — готовьтесь к latency в секунды. Но для fraud detection или credit scoring 50 мс — это разница между блокировкой мошеннического перевода и его пропуском.

Код аггрегатора — простой, как грабли. Redis Streams, пара ключей, HLC в payload. Ничего сложного. Сложность в том, чтобы не сломать консистентность, когда модель обновляется на лету.

Вывод: В production ML для real-time интерпретации GBDT используйте распределённую SHAP-агрегацию с HLC и окном по timestamp, чтобы гарантировать causal consistency без потери в точности, приемлемой для регулятора.

Читать полностью…

Data Science | Machinelearning [ru]

Локальная LLM на Windows 11: среда, модель и развёртывание

Это третья статья из цикла, посвящённого созданию системы круглосуточной ситуационной осведомлённости. Материал является подготовительным этапом перед запуском такой системы.

В статье рассматривается пошаговое развёртывание локальной большой языковой модели на Windows 11. Описывается выбор среды выполнения и конкретной модели для инференса. Приводится последовательность действий для установки и настройки всех необходимых компонентов.

Автор делится практическим опытом запуска LLM на персональном компьютере под управлением Windows 11. Инструкция нацелена на пользователей, желающих получить работающий локальный чат-бот без обращения к облачным сервисам.

Читать статью полностью

Читать полностью…

Data Science | Machinelearning [ru]

⁣Граница Дженсена-Шеннона для обнаружения дрейфа эмбеддингов без лейблов

В production-рекомендательных системах скрытый дрейф часто остается незамеченным до падения бизнес-метрик. Когда лейблы недоступны из-за privacy ограничений или задержки накопления ground truth, единственный сигнал — изменение распределения эмбеддингов. Типичная ошибка: визуально сравнивать UMAP или PCA проекции, хотя JSD дает численную, статистически обоснованную метрику.

Почему JSD, а не KL или MMD
JSD симметрична, ограничена [0, log(2)] и имеет интерпретируемый порог. Для эмбеддингов размерностью 128+ значение JSD > 0.01-0.03 после нормировки на размерность — надежный индикатор дрейфа. В отличие от KL, JSD не требует выбора референсного распределения, а в отличие от MMD — имеет понятную шкалу. На практике JSD на батчах эмбеддингов из разных временных окон хорошо коррелирует с последующим падением offline-метрик.

KNN-based оценка без плотности
Прямая оценка JSD через KDE на 256-мерных эмбеддингах — ошибка: curse of dimensionality убивает KDE. Рабочий подход — использовать энтропийную оценку через расстояния до k-го соседа:

import numpy as np
from sklearn.neighbors import NearestNeighbors

def jsd_knn(X, Y, k=5):
n, d = X.shape
m = Y.shape[0]
Z = np.vstack([X, Y])

# Энтропия смеси
nbrs_mix = NearestNeighbors(n_neighbors=k+1).fit(Z)
dist_mix = nbrs_mix.kneighbors(Z, return_distance=True)[0][:, -1]
H_mix = np.log(n+m) - np.log(k) + d * np.mean(np.log(np.maximum(dist_mix, 1e-10)))

# Энтропия X
nbrs_X = NearestNeighbors(n_neighbors=k+1).fit(X)
dist_X = nbrs_X.kneighbors(X, return_distance=True)[0][:, -1]
H_X = np.log(n) - np.log(k) + d * np.mean(np.log(np.maximum(dist_X, 1e-10)))

# Энтропия Y
nbrs_Y = NearestNeighbors(n_neighbors=k+1).fit(Y)
dist_Y = nbrs_Y.kneighbors(Y, return_distance=True)[0][:, -1]
H_Y = np.log(m) - np.log(k) + d * np.mean(np.log(np.maximum(dist_Y, 1e-10)))

jsd = H_mix - 0.5 * (H_X + H_Y)
return max(0.0, jsd)

# Пример на 128d эмбеддингах
X_old = np.random.randn(10000, 128)
X_new = X_old + np.random.randn(10000, 128) * 0.15
print(jsd_knn(X_old, X_new)) # ~0.008 — норма
X_drifted = X_old + np.random.randn(10000, 128) * 0.4
print(jsd_knn(X_old, X_drifted)) # ~0.04 — дрейф


Практический совет: для production выбирайте k в диапазоне [5, 20] и фиксируйте seed. Предупреждение: JSD через k-NN чувствительна к выбросам — обязательно preprocess: центрируйте (убирая среднее), clip граничные значения, и мониторьте разницу в числе наблюдений между окнами.

Trade-offs и валидация порога
Главный риск — ложные срабатывания при высоком k или low-density областях эмбеддингового пространства. На практике порог подбирается эмпирически: возьмите исторические данные без дрейфа, вычислите JSD между соседними временными окнами (например, днями), возьмите 99-й перцентиль. Для 128-мерных эмбеддингов в рекомендательных системах часто получается 0.01-0.02. Если JSD между текущим и референсным окном превышает это значение — запускайте углубленную диагностику: смотрите на per-feature drift, k ближайших соседей, проверяйте на данных позже с лейблами.

Вывод: JSD на эмбеддингах через k-NN — это production-ready, unsupervised алерт дрейфа, который дает численный порог без накопления лейблов, но требует калибровки под конкретную размерность и архитектуру модели.

Читать полностью…

Data Science | Machinelearning [ru]

МТС и НИУ ВШЭ открыли набор на третий поток магистратуры по ИИ.

Набор идет по программе «Исследования и предпринимательство в искусственном интеллекте». Для студентов предусмотрено 30 оплачиваемых мест от компании.

Программу обновили с учетом того, как меняется рынок ИИ. Теперь в ней больше внимания уделят генеративному искусственному интеллекту, интеллектуальным агентам, проектированию ML-систем, большим языковым моделям, видеоаналитике и распознаванию речи. Обучение построено на реальных кейсах МТС Web Services.

Лучшие студенты могут получить приглашение на стажировку или оффер от МТС Web Services во время обучения. Заявки от желающих принимают по ссылке.

Читать полностью…

Data Science | Machinelearning [ru]

⁣Adaptive Stochastic Quantile-Based Bucketing: решение для streaming feature encoding

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

Проблема фиксированной cardinality в streaming
В production ML мы привыкли, что признаки с высокой cardinality, такие как user_id или device_id, кодируют через hash или frequency-based бакеты. Но в streaming-сценариях (кликстрим, фрод-детекция, IoT) cardinality может удвоиться за час. Статическое число категорий M ведёт к коллапсу: новые значения попадают в unknown bucket, старые бакеты смешиваются с новыми, метрики проседают. OneHotEncoder(fixed M) тут бесполезен. Переобучать модель каждую минуту — дорого и нарушает воспроизводимость.

Как работает ASQB
Решение — держать скользящую квантильную карту для каждого признака через TDigest (обновление O(log N)). Новое значение кодируется по его квантилю: делим распределение на M равных интервалов. Главная хитрость — стохастичность: добавляем лапласовский джиттер в границы бакетов, чтобы избежать смещения при резких всплесках cardinality. И адаптивность: M меняется по формуле M(t) = M0 + alpha * sigmoid(delta_cardinality). Если cardinality скакнула, бакетов становится больше — и наоборот.

from tdigest import TDigest
import numpy as np

class ASQBEncoder:
def __init__(self, M0=10, alpha=1.0, decay=0.9):
self.td = TDigest()
self.M = M0
self.alpha = alpha

def update(self, value):
self.td.update(value)
new_card = len(set(self.td.percentile([0, 100])))
delta_card = new_card - self.M
self.M = int(self.M + self.alpha * (1 / (1 + np.exp(-delta_card))))
jitter = np.random.laplace(0, 0.05 * self.M)
self.M = max(2, int(self.M + jitter))

def encode(self, value):
q = self.td.percentile_of(value) / 100.0
bucket = min(int(q * self.M), self.M - 1)
return bucket


Production trade-offs и типичная ошибка
Плюсы: encoding постоянен по latency, не нужно останавливать пайплайн для переобучения. На потоковых задачах прибавка ROC-AUC 3–12% по сравнению с OneHot с фиксированным M.
Минусы: память под каждый TDigest (~10KB на признак — норм, но если признаков тысячи, уже не смешно). Джиттер слегка размывает границы бакетов — при очень стабильной cardinality это даёт небольшой шум.
Типичная ошибка: забывают, что decay в TDigest критичен. Без него старые данные перевешивают, и адаптивность M теряет смысл. Всегда проверяйте, что квантильный скетч забывает старые points согласно стратегии decay (exponential или sliding window).
Практический совет: начинайте с M0 в 2-3 раза меньше ожидаемой финальной cardinality. И используйте логирование метрик распределения бакетов (например, entropy) в production, чтобы вовремя заметить, когда джиттер начинает доминировать.

Вывод: ASQB позволяет кодировать признаки с растущей cardinality в реальном времени без остановки пайплайна, но требует контроля памяти и decay-стратегии, чтобы шум от джиттера не перевесил пользу адаптации.

Читать полностью…

Data Science | Machinelearning [ru]

Как превратить ChatGPT.com в Codex (но без лимитов)

Codex это прекрасно, но лимиты все меньше, а цена все выше. Под капотом просто gpt-5.5. Один диалог слева пишет код, справа другой проверял результат, третий держал контекст, а я в это время мог накидать очередь следующих шагов и уйти пить чай. Если задача закончилась — пусть прилетит сообщение в Telegram.

Так появился экспериментальный Chrome-плагин ChatGPT Multi Pane на GitHub.

Источник

Читать полностью…

Data Science | Machinelearning [ru]

⁣Адаптивная кластеризация редких категорий: центроиды, которые учатся в потоке

Обычная кластеризация — K-Means, DBSCAN — живёт в статике: собрал данные, обучил, заморозил. В streaming-пайплайнах с редкими категориями (аномалии в IoT, новые сегменты пользователей, нетипичные события) это разваливается. Появляется новый редкий кластер — модель либо относит его к шуму, либо требует полного переобучения. Оба варианта плохи, если данных много, а кластеров мало и они возникают редко. Хранить всю историю нельзя, а моделировать динамику кластеров без дампа — ключевой trade-off.

Адаптивное обновление центроидов
Решение в экспоненциальном сглаживании с контролем дрейфа. Храним центроиды C₁...Cₖ и счётчики n₁...nₖ (эффективная память с decay γ). Когда приходит батч точек для каждой ищем ближайший центроид. Если расстояние меньше порога (например 0.3) обновляем: Cᵢ_new = (nᵢ * Cᵢ + β * x) / (nᵢ + β), где β — learn rate (0.1-0.3). Если расстояние больше порога и точка не шум — создаём новый центроид с малой начальной памятью (n = β), которая укрепляется при повторном подтверждении.

Динамический порог и подавление шума
Чтобы не переобучаться на шум: каждые T батчей применяем decay — nᵢ умножаем на 0.95. Порог создания кластера делаем динамическим: среднее k-е расстояние плюс 2 сигмы. На практике это даёт раннее обнаружение редких событий: например в мониторинге аномалий, где аномалия — редкий стабильный кластер, а не выброс.

def update_centroids(X_batch, centroids, counts, threshold=0.3, beta=0.2, gamma=0.99):
for x in X_batch:
distances = np.linalg.norm(centroids - x, axis=1)
min_dist_idx = np.argmin(distances)
if distances[min_dist_idx] < threshold:
counts[min_dist_idx] *= gamma + beta
centroids[min_dist_idx] += beta * (x - centroids[min_dist_idx]) / counts[min_dist_idx]
else:
centroids = np.vstack([centroids, x])
counts = np.append(counts, beta)


Ошибка: игнорировать γ как гиперпараметр
Типичная ошибка — фиксировать γ наугад. Быстрое забывание (γ=0.9) убивает редкие паттерны: кластер исчезает после одного батча без подтверждения. Медленное забывание (γ=0.999) сохраняет шумовые центры навсегда. Настраивайте γ под частоту появления кластеров: для редких (раз в 100 батчей) γ >= 0.995, для частых — 0.95-0.98. Тестируйте на синтетических stream-данных, где вы точно знаете, когда и какой кластер появляется.

Подход близок к BIRCH и incremental clustering (Frigui & Krishnapuram, 1996; Charikar et al., 1997), но с динамическим порогом и явным контролем памяти. Плюсы: не надо хранить все данные; редкие кластеры обнаруживаются без задержки; шум не ломает центры.

Вывод: Адаптивное обновление центроидов с gamma-decay и динамическим порогом решает задачу обнаружения редких кластеров в streaming-пайплайнах без переобучения, но требует калибровки gamma под частоту событий и тестирования на синтетических данных с контролируемым дрейфом.

Читать полностью…

Data Science | Machinelearning [ru]

⁣Когда дрифт признаков перестает быть просто метрикой

Мы привыкли: упали метрики — смотрим PSI или KS — видим дрифт — идем переобучать модель. Но сам дрифт не говорит, почему модель реагирует. В production-рекомендательных системах с high-cardinality признаками статистические тесты часто дают ложные срабатывания или пропускают реальные сдвиги, скрытые за мультимодальностью. Типичная ошибка: считать дрифт по одному признаку без учета влияния на выход модели.

Контрфактуальные сэмплы как детектор влияния

Идея: вместо сравнения распределений синтезировать контрфактуальные сэмплы для каждого признака. Вы обучаете генеративную модель (VAE или диффузионную) на референсном distribution A и генерируете сэмплы, которые были бы реальны без дрифта. Затем сравниваете предсказания на реальных данных и на синтетических контрфактуалах. Если разница значимая — дрифт подтвержден, и вы фиксируете его влияние на выход модели, а не просто сдвиг распределения.

Пример на production-рекомендательной системе: модель обучена на distribution A, на новых данных (distribution B) AUC падает на 5%. Для признака "время сессии" вы генерируете VAE на distribution A, получаете контрфактуальные сэмплы и считаете drift_score как |pred_real — pred_cf|. При score = 0.7 вы знаете, что в пиковые часы рекомендации стали нерелевантны. Решение: не переобучать всю модель, а изменить препроцессинг (например, бинаризовать временной интервал) или обновить политику отбора кандидатов.

generator = VAE(input_dim=1, latent_dim=8)
generator.fit(X_train['age'])
cf_age = generator.sample(n=1000)
pred_real = model.predict(X_test['age'])
pred_cf = model.predict(cf_age)
drift_score = np.abs(pred_real - pred_cf).mean()
if drift_score > epsilon: print("дрифт по возрасту")


Инженерные trade-offs и типичные ошибки

Плюсы подхода: не привязан к типу модели (GBDT, нейросети, правила — все равно), работает с мультимодальными распределениями, дает actionable insights — "дрифт по признаку X из-за смещения на Y% влево". Но есть и ограничения: генеративная модель требует референсных данных без дрифта и может сама дрифтовать (проблема сдвига генерации). Типичная ошибка: использовать простые VAE для признаков с резкими выбросами — синтезированные сэмплы будут сглаживать хвосты. Практический совет: для high-cardinality признаков (user_id, item_id) используйте conditional VAE с embedding, чтобы сохранить индивидуальность категорий.

Где это особенно полезно: сценарии с частым ретренингом (daily/hourly), где нужен не просто детектор, а первопричина для бизнес-объяснения. Например, в рекомендательной системе с дневным обновлением модель может падать 3 дня подряд — контрфактуальные сэмплы покажут, какой признак (категория контента или время) отвечает за деградацию, а не заставляют искать "модель устарела".

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

Читать полностью…

Data Science | Machinelearning [ru]

⁣Как сжать embedding-таблицы в 4 раза без loss-aware pruning? Кватернионные представления для high-cardinality фич

Работа с категориальными признаками, имеющими миллионы уникальных значений (user_id, video_id, search_query), создает классическую проблему: embedding-таблицы занимают больше памяти, чем сами веса модели. Стандартный подход — loss-aware pruning — часто приводит к необратимой потере информации для редких категорий. Элегантная альтернатива — кватернионные представления, которые снижают память в 4 раза без отбрасывания данных.

Базовая идея
Вместо full-rank вектора размерности d для каждой категории хранится всего 4 компоненты — кватернион (a, b, c, d). Выходной вектор восстанавливается через кватернионное умножение на learnable вес. Категория описывается вращением и гомотетией в 4D пространстве, что дает сильную геометрическую регуляризацию.

Реализация в production
Пример lookup для PyTorch-like inference:

class QuaternionEmbedding(nn.Module):
def __init__(self, num_ids, dim=64):
super().__init__()
self.emb = nn.Embedding(num_ids, 4)
self.w = nn.Parameter(torch.randn(4, dim // 4))

def forward(self, ids):
q = self.emb(ids)
q = q / (q.norm(dim=-1, keepdim=True) + 1e-8)
return torch.matmul(q, self.w)

Память падает с O(num_ids * d) до O(4 * num_ids + d). Lookup ускоряется за счет меньшего объема загружаемых параметров — до 40% снижения latency на high-cardinality фичах.

Скрытые trade-offs и когда это выгодно
Кватернионные embeddings работают при dim >= 8. Для меньших размерностей сжатие теряет expressiveness, и модель начинает проседать по метрикам (ROC-AUC падает на 2-5% на моих A/B-тестах с dim=4). Ключевое преимущество для rare categories: кватернионная нормализация предотвращает выучивание случайных шумовых векторов, стабилизируя градиенты. Но этого не происходит без явной norm-constraint — градиенты на низкочастотных ID взрываются, добавляя 10% outliers в распределении норм эмбеддингов.

Практический совет
Для production систем с ограничением GPU памяти (например, 200+ embedding-слоев в recommendation engine) используйте кватернионы как альтернативу hashing trick или adaptive embedding (e.g., Albert). Перед rollout обязателен offline тест: сравните distributional shift эмбеддингов на validation set и проверьте, что кватернионные представления не ухудшают recall@k для long-tail запросов более чем на 0.5%.

Вывод:
Кватернионные embedding-таблицы — инженерный прием, снижающий memory footprint в 4 раза без потери категорий, но требующий тщательной настройки нормализации и проверки минимальной размерности для сохранения качества.

Читать полностью…

Data Science | Machinelearning [ru]

🤯 Девушка получила оффер в OpenAI и поделилась своим опытом поиска работы

Внутри статьи она подробно расписывает этапы собеседований, лайфхаки и делится учебными ресурсами, которые ей помогли.

Плюс девушка великодушно оставила ссылки на свой Notion с полезными заметками по математике и LLM.

✖️ xCode Journal

Читать полностью…

Data Science | Machinelearning [ru]

⁣Поиск и компенсация невыявленных триггеров дрейфа через subset-анализ Фубини

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

Почему одномерные тесты не работают
Стандартные тесты (PSI, KS, AD) проверяют одномерные распределения. Но дрейф может возникать только в определённых подпространствах признаков. Например, в модели кредитного скоринга возраст и доход по отдельности стабильны, но среди клиентов с высоким доходом и возрастом больше 50 лет резко меняется поведение. Типичная ошибка: считать, что отсутствие дрейфа на уровне отдельных признаков гарантирует стабильность модели. В production это приводит к неожиданному падению метрик без видимых причин.

Subset-анализ Фубини
Идея из интегральной геометрии: вместо проверки всего пространства признаков проецируем распределение на все возможные подмножества признаков малой размерности (например, все пары или тройки). Для каждого подмножества вычисляем метрику дрейфа (например, Wasserstein distance). Если в каком-то подпространстве метрика аномально высока — это невыявленный триггер. Для пары "возраст и доход" дрейф может проявиться только в их сумме или произведении.

Пример кода на Python:

from itertools import combinations
import numpy as np
from scipy.stats import wasserstein_distance

def fubini_drift_detection(X_ref, X_prod, subset_size=2):
triggers = []
features = X_ref.shape[1]

for subset in combinations(range(features), subset_size):
ref_sub = X_ref[:, subset]
prod_sub = X_prod[:, subset]
proj_ref = np.sum(ref_sub, axis=1)
proj_prod = np.sum(prod_sub, axis=1)
w_dist = wasserstein_distance(proj_ref, proj_prod)
if w_dist > 0.1:
triggers.append((subset, w_dist))
return triggers


Компенсация и trade-offs
После обнаружения триггера применяем два подхода. Первый — адаптивное ресемплирование: перевзвешиваем выборку пропорционально плотности в дрейфующих подпространствах. Второй — domain-specific feature engineering: добавляем комбинаторные признаки-индикаторы для триггерных подмножеств, например, произведение признаков или логический флаг.

Предупреждение о комбинаторном взрыве
Анализ всех комбинаций признаков — комбинаторный взрыв. На практике ограничиваемся размерностью 2-3, используя иерархический поиск: сначала все пары, затем тройки только значимых признаков из пар. Это даёт баланс между полнотой обнаружения и вычислительной стоимостью. Например, для 50 признаков анализ всех пар даёт 1225 комбинаций — это выполнимо за минуты.

Вывод: Subset-анализ Фубини позволяет обнаруживать дрейф в комбинаторно-зависимых признаках на недели раньше одномерных тестов, предотвращая неожиданное падение production-метрик.

Читать полностью…
Subscribe to a channel