Data Science | Machinelearning [ru]
Все о Data Science, машинном обучении и искусственном интеллекте: от базовой теории до cutting-edge исследований и LLM. Личный блог автора - @just_genych По вопросам рекламы или разработки - @g_abashkin РКН: https://vk.cc/cJPGXD
Show more📈 Analytical overview of Telegram channel Data Science | Machinelearning [ru]
Channel Data Science | Machinelearning [ru] (@devsp) in the Russian language segment is an active participant. Currently, the community unites 19 771 subscribers, ranking 6 510 in the Technologies & Applications category and 33 305 in the Russia region.
📊 Audience metrics and dynamics
Since its creation on невідомо, the project has demonstrated rapid growth, gathering an audience of 19 771 subscribers.
According to the latest data from 30 August, 2026, the channel demonstrates stable activity. Although there has been a change in the number of participants by -152 over the last 30 days and by -11 over the last 24 hours, overall reach remains high.
- Verification status: Not verified
- Engagement rate (ER): The average audience engagement rate is 7.98%. Within the first 24 hours after publication, content typically collects 4.93% reactions from the total number of subscribers.
- Post reach: On average, each post receives 1 578 views. Within the first day, a publication typically gains 974 views.
- Reactions and interaction: The audience actively supports content: the average number of reactions per post is 6.
- Thematic interests: Content is focused on key topics such as llm, nvidia, контекст, openai, архитектура.
📝 Description and content policy
The author describes the resource as a platform for expressing subjective opinions:
“Все о Data Science, машинном обучении и искусственном интеллекте: от базовой теории до cutting-edge исследований и LLM.
Личный блог автора - @just_genych
По вопросам рекламы или разработки - @g_abashkin
РКН: https://vk.cc/cJPGXD”
Thanks to the high frequency of updates (latest data received on 31 August, 2026), the channel maintains relevance and a high level of publication reach. Analytics show that the audience actively interacts with content, making it an important point of influence in the Technologies & Applications category.
class OnlineAttributor:
def __init__(self, model, latency_budget_ms=5):
self.model = model
self.budget = latency_budget_ms / 1000
async def get_attribution(self, features):
shap_values = await asyncio.to_thread(
self._tree_shap, features, timeout=self.budget
)
if shap_values is None:
shap_values = await asyncio.to_thread(
self._global_importance, features
)
return shap_values
Также полезно:
* batching — группируешь запросы по 10-50 штук и считаешь SHAP векторно. Latency per item падает в разы.
* precomputed SHAP для стримовых запросов — делаешь offline-атрибуцию раз в 10 минут и кешируешь. Если фичи не дрифтят резко, этого хватает.
Типичные ошибки и trade-offs
Линейные модели вроде LIME плохо работают на нелинейностях бустинга. Для CatBoost или LightGBM лучше использовать встроенный TreeSHAP через predict с pred_contrib=True — он дешевле и точнее.
Другая распространённая ошибка — не учитывать распределение latency. При high-throughput среднее может быть 1 мс, но 95-й перцентиль — 50 мс из-за сложных объектов. Нужно закладывать таймаут на 99-й перцентиль и падать на global importance.
По моему опыту, комбинация Fast TreeSHAP с pruning, adaptive timeout и fallback на global importance даёт типичное время меньше 2 мс на объект при 100 деревьях. Без этого интерпретация на production становится узким горлышком.
Вывод: Для online-атрибуции в градиентном бустинге при high-throughput инференсе используйте Fast TreeSHAP с адаптивным таймаутом и кэшированием, а не полный SHAP на каждый запрос — это даёт баланс между точностью и latency.⚡️ С промокодом TSFIRST билеты сейчас всего по 1 000 ₽ (Скоро цена вырастет, так что успевай, пока такая халява. Мы предупредили!)Забирай билет по суперцене на сайте! 🔺🔻🔸🔹🔶🔷
class UnionFind:
def __init__(self):
self.parent = {}
def find(self, x):
if self.parent.setdefault(x, x) != x:
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
def union(self, x, y):
px, py = self.find(x), self.find(y)
self.parent[px] = py
Этот код — основа. В real-time пайплайне граф должен динамически обновляться: новые алиасы выявляются через HLL sketch или LSH, а сам граф живет в Redis или RocksDB. Если latency меньше 10ms — предвычисляй и загружай в память при старте стрима.
Production сценарий: Kafka Streams и перестройка графа
На практике это выглядит так: на этапе Kafka Streams каждый новый признак прогоняется через lookup-таблицу связности, где Union-Find возвращает каноническое имя. Раз в час граф перестраивается по свежим логам — это позволяет учитывать новые синонимы без остановки потока. Типичный выигрыш — cardinality фичей падает на 30-50%, что напрямую снижает размерность модели и latency инференса.
Trade-offs и предупреждение
Подход особенно окупается в мультитенантных системах, где датасеты от разных команд с разным неймингом, в A/B тестах, где колонки переименовывают на лету, или когда feature engineering делают руками без CI/CD. Но здесь есть ключевой trade-off: скорость против точности. Если гнаться за каждым синонимом — граф разрастается, latency растет. Если реже — часть алиасов остается, и модель снова видит шум. Ошибка: пытаться найти все синонимы сразу. Лучше начинать с Union-Find, потом делать incremental clustering, постепенно расширяя на HLL-sketches для редких паттернов.
Вывод:
Graph-based дедупликация с Union-Find и инкрементальным обновлением в Redis — это инженерный баланс между гибкостью и latency, который решает проблему feature aliasing без тонн мусорного кода в real-time пайплайнах.import torch
def add_feature_wise_noise(grad, features, lambda_noise=0.01):
var = features.var(dim=0, unbiased=True)
noise = torch.randn_like(grad) * (lambda_noise * var.sqrt())
return grad + noise
for x_batch, y_batch in dataloader:
pred = model(x_batch)
loss = criterion(pred, y_batch)
loss.backward()
for param in model.parameters():
if param.grad is not None:
param.grad = add_feature_wise_noise(param.grad, x_batch)
optimizer.step()
optimizer.zero_grad()
Практические советы и предупреждения
- λ — ключевой параметр. Слишком маленький — эффекта ноль. Слишком большой — модель перестанет сходиться. Я обычно начинаю с 10⁻³ и подбираю по валидации на исторических дрейфах. Это типичная ошибка — не настраивать λ под конкретные данные.
- FGNI не отменяет мониторинг дрейфа, но заметно повышает робастность в промежутках между детекциями. Он не заменяет отслеживание метрик, а дополняет его, давая дополнительный запас надёжности.
- Метод лучше всего заходит на tabular data и MLP. На RNN или трансформерах придётся модифицировать — шуметь, например, по hidden state. Прямое применение к градиентам параметров на этих архитектурах может быть нестабильным.
Вывод: Feature-wise gradient noise injection — простой и дешёвый по вычислениям способ сделать online-пайплайн устойчивее к дрейфу за счёт адаптивного регуляризатора прямо в градиентном спуске.def process_batch(batch_requests):
all_unique_ids = {}
for req in batch_requests:
for item_id in req['item_ids']:
all_unique_ids[item_id] = True
unique_ids_list = list(all_unique_ids.keys())
embeddings_map = compute_embeddings(unique_ids_list)
results = []
for req in batch_requests:
req_embeddings = [embeddings_map[i] for i in req['item_ids']]
results.append(compute_scores(req['user_vec'], req_embeddings))
return results
Производственный пример
Допустим, у вас сервис для скоринга объявлений в real-time: 100 запросов в батче, каждый с 50 item'ами, но уникальных — всего 200. Без кэширования — 5000 вызовов модели эмбеддингов, с batch-level — 200. Сокращение в 25 раз. Для критичного по latency пайплайна это разница между SLA violation и стабильной работой.
Практический совет и trade-offs
Добавьте LRU-кэш второго уровня для hot items с TTL в 1 минуту. Batch-level — первый фильтр, отсекающий дубликаты внутри батча, глобальный кэш ловит переиспользование между батчами. Но не забывайте: требуется синхронизация внутри пайплайна — нужно собрать все ID до вычислений. Это может стать узким местом для batch size > 1000 или при асинхронной обработке. Типичная ошибка: путать batch-level с глобальным TTL-кэшем и терять дубликаты внутри одного батча.
Вывод:
Batch-level caching — самый простой способ снять duplicate-нагрузку на генерацию эмбеддингов в real-time, сокращая latency в разы без значительных overhead по памяти или инфраструктуре.class QuantLayer(nn.Module):
def __init__(self, num_features, bits=4):
super().__init__()
self.scales = nn.Parameter(torch.ones(num_features))
self.zero_points = nn.Parameter(torch.zeros(num_features))
self.max_val = 2**(bits-1) - 1
def forward(self, x):
x_scaled = x / self.scales
x_quant = torch.clamp(torch.round(x_scaled), -self.max_val, self.max_val)
return x_quant * self.scales
Практический совет: используй learnable параметры, инициализированные из предварительной калибровки на репрезентативном датасете. Это ускоряет сходимость и снижает риск переобучения.
Production-метрики и trade-offs
На BERT-подобных моделях FWQAT даёт прирост F1 на 2-3% относительно обычного QAT. ResNet-50 теряет всего 0.5% против FP32, тогда как стандартный QAT режет 2-3%. Типичная ошибка — тонкий fine-tuning без репрезентативной выборки. Для стабильности нужно минимум 1% обучающих данных с сохранением оригинального распределения. На старых GPU без аппаратного INT4 (например, P40) эмуляция дорогая, но гибрид Int8+FP16 через кастомные ops улучшает блочное квантование.
Инженерные нюансы при деплое
В production FWQAT легко интегрируется: кастомные ops для TensorRT или прямой вызов learnable параметров из рантайма. Предупреждение: после fine-tuning обязательно перекалибруй scale и zero-point на валидационном датасете — иначе дрейф данных сломает метрики. Latency растёт на 5-10% из-за per-feature операций, но это окупается при памяти менее 4 ГБ на батч.
Вывод: Feature-wise QAT сохраняет метрики на production-моделях с INT4 за счёт раздельного квантования каждого признака, но требует репрезентативного fine-tuning и обязательной перекалибровки в production-пайплайне.position_id модификацию в эмбеддинги. Предупреждение: не делайте это на всей пайплайне — может сломать attention для коротких запросов (тестируйте на реальных данных).
4. Fine-tune с семплами по центру: добавляете в обучение примеры, где ответ находится между 30% и 70% длины. Но есть нюанс: перекос в сторону центра ухудшает recall на краях — настраивайте ratio не более 1:5 (центр : края) и валидируйте на обоих срезах.
Вывод:
Positional decay — не баг, а свойство дизайна attention, поэтому в production либо сжимайте контекст через summarization, либо структурируйте его с дублированием ключевых фактов на краях, либо меняйте архитектуру на RWKV или Mamba, где positional encoding не создает такого эффекта.df['avg_target_by_city'] = df.groupby('city')['target'].transform('mean')
Если делаешь это на всем датасете до разбивки на train/val, модель на валидации видит среднее, посчитанное с учетом future-меток. Метрики взлетают, в проде — провал. Решение: агрегаты строго в рамках train-фолда через target_encoding в Pipeline или GroupKFold. И никаких transform до split.
2. Time-aware валидация: иллюзия порядка
Временные ряды без строгого разбиения по времени — утечка из-за shuffle. Модель на валидации подсматривает данные из будущего. Простое правило: никакого train_test_split с random_state. Используй TimeSeriesSplit или PurgedGroupTimeSeriesSplit. И проверяй lag-фичи — они любят заглядывать вперед. Типичная ошибка: добавление rolling-агрегатов на всем датасете, а не в рамках временного окна.
3. Фичи-строки, которые знают ответ
Бывает, поле user_flag появляется только после события (таргета). Или transaction_id коррелирует с таргетом: новые транзакции — выше риск дефолта. Удали ID-поля, проверь корреляцию с таргетом. Значение >0.95 — явный leakage. Еще один production-пример: в NLP пайплайне, когда токен документа использется как фича, но он присваивается после разметки таргета. Это ломает валидацию в LabelPropagation на stream-данных.
Как детектировать скрытые утечки
* Lasso-регрессия: если модель оставляет 1-2 фичи с огромными весами — red flag.
* Permutation importance: аномально высокое падение метрики при перестановке одной фичи.
* Lookahead bias audit: проверь, что признаки вычисляются на момент t-1, а не t. Используй reverse-engineering на отложенной выборке по времени.
Вывод: Label leakage убивает ML-продукт — лучше потратить час на аудит пайплайна с time-aware валидацией и permutation tests, чем два месяца на восстановление репутации.def spherical_gradient_clip(grad, max_norm=1.0, eps=1e-8):
norm = grad.norm()
if norm > max_norm:
return grad * (max_norm / norm)
elif norm < eps:
return torch.randn_like(grad) * eps
return grad
Инженерные trade-offs в production ML
Комбинируйте Spherical Gradient с layer normalization и gradient checkpointing при длине последовательности >500 шагов (финансы, IoT, логи). Внимание: нормировка градиента увеличивает latency на ~5-10%, но стабильность сходимости окупается на реальных данных. Типичная ошибка — применять Spherical Gradient к Transformer без нормировки весов, что ломает attention scores при большой разрядности.
Практический совет по валидации
Для online-обучения на стрим-данных сравните variance градиентов до и после применения на синтетике с length=500. В production на Transfomer для временных рядов Spherical Gradient показывает снижение variance на 40-60% и ускорение сходимости loss в 1.5 раза по сравнению с gradient clipping.
Вывод:
Нормируйте градиент в сферическом пространстве, а не просто отсекайте — это единственный способ сохранить стабильность online-обучения на длинных последовательностях без потери чувствительности к редким событиям.click_time = t мы хотим оценить:
P(conversion | click, x)
Но на дату сборки датасета T мы знаем только одно из двух:
* конверсия уже произошла до T
* конверсии пока не видно
Второй случай не равен converted = 0. Это censored observation: объект наблюдался недостаточно долго.
Типичная ошибка:
converted = 1, если conversion_time - click_time <= 7d
converted = 0, иначе
Так свежие клики получают искусственно заниженный CVR, модель учится на ложных negative, offline-метрики зависят от «зрелости» среза, а production-калибровка плывет: модель предсказывает full-window CVR, а мониторинг видит partial-window CVR.
Baseline: обучаться только на mature data
Если целевой горизонт - конверсия за 7 дней, а данные доступны до 2025-01-31, то в train стоит брать клики не позже 2025-01-24.
Плюсы:
* честные лейблы
* простая валидация
* легко дебажить пайплайн и leakage
Минусы:
* теряем свежие данные
* хуже адаптация к сезонности и изменению трафика
* при длинном conversion lag train сильно устаревает
Практический совет: явно храните в feature store или training dataset поля event_time, label_observed_until, horizon и label_age. Без них невозможно воспроизвести разметку и понять, почему CVR изменился после очередного retraining.
Более сильный подход: моделировать задержку
Можно разложить задачу на вероятность конверсии и распределение delay:
P(y = 1, delay <= H | x)
Например:
* CVR-модель оценивает вероятность самой конверсии
* delay-модель оценивает P(delay <= age | y=1, x)
Это ближе к survival analysis: есть событие, time-to-event и censored observations. Такой подход особенно полезен, если задержка зависит от категории товара, канала, географии, цены, устройства или ретаргетинга.
Альтернатива - дискретный hazard:
P(conversion at day k | no conversion before day k, x)
Тогда клик, наблюдавшийся только 2 дня, все еще полезен для обучения первых двух шагов, а не выбрасывается целиком. Trade-off: модель и inference становятся сложнее, зато меньше потерь данных и честнее работа с длинным хвостом конверсий.
Оценка: test тоже должен быть mature
Если horizon = 7d, то holdout должен содержать только объекты, для которых прошло минимум 7 дней после клика. Иначе вы измеряете не качество модели, а незрелость лейблов.
Хорошая схема:
train: clicks [D0, D1] validation: clicks [D2, D3] label cutoff: >= D3 + horizonВ production дополнительно смотрите метрики по delay buckets: *
0-1h
* 1-24h
* 1-3d
* 3-7d
* 7d+
Так видно, где ломается система: в быстрых конверсиях, длинном хвосте, data freshness, attribution или из-за цензурированных лейблов. Для A/B-тестов это особенно важно: ранний readout может завысить эффект модели, которая хорошо ловит быстрые конверсии, но проигрывает на полном окне.
Вывод:
Delayed feedback в CVR - это не косметика разметки, а инженерное ограничение ML-системы: без mature labels, явного label cutoff и корректной валидации модель оптимизирует артефакты логирования вместо реальной конверсии.Главный анти-паттерн: писать новые документы новым encoder’ом в старый индекс со старыми embedding’ами.Такой индекс становится смешанным: часть векторов живёт в одном пространстве, часть - в другом. ANN формально работает, но nearest neighbors уже не имеют корректной семантики. Версионируем embedding space как production contract Версионировать нужно не просто
model_name, а полный контракт:
embedding_version = encoder + tokenizer + pooling + normalization + dim + metric
Если поменялось что-то из этого - это новая версия пространства.
Практический совет: храните embedding_version рядом с документом, запросом, индексом и retrieval-логами. Иначе при деградации recall или CTR вы не поймёте, какой encoder реально участвовал в выдаче.
Поднимаем новый индекс и включаем dual-write
Старый путь:
docs_v1 -> embeddings_v1 -> ann_index_v1Новый путь:
docs_v2 -> embeddings_v2 -> ann_index_v2Даже если документы те же, embedding’и должны быть пересчитаны новым encoder’ом. Для ANN это новый corpus. Важно: параметры индекса тоже стоит перетюнить. Например, для HNSW старые
M, efConstruction, efSearch могут быть не оптимальны для нового распределения.
На время миграции новые и обновлённые документы пишем в обе версии:
on_document_upsert(doc):
emb_v1 = encoder_v1(doc)
emb_v2 = encoder_v2(doc)
index_v1.upsert(doc.id, emb_v1)
index_v2.upsert(doc.id, emb_v2)
Это дороже по compute и ingestion latency, зато старый retrieval продолжает работать, а новый индекс догоняет актуальное состояние. Если v1 скоро выключается, dual-write можно держать только до cutover плюс короткое rollback window.
Backfill, shadow-read и критерии готовности
Для v2 нужно пересчитать embedding’и всего корпуса и залить их в новый индекс. Здесь важны не ноутбучные метрики, а инженерная надёжность:
- идемпотентность задач;
- контроль lag’а;
- дедупликация upsert’ов;
- checkpoint’ы;
- отдельные лимиты на encoder и ANN ingestion;
- сверка количества документов между индексами;
- контроль доли документов без v2 embedding.
Миграция не готова, пока новый индекс не покрывает production corpus с приемлемым lag.
Перед переключением включаем shadow-read:
query -> encoder_v1 -> index_v1 -> results_v1
-> encoder_v2 -> index_v2 -> results_v2
Пользователю показываем только v1, но сравниваем:
- recall@k на размеченных данных;
- overlap@k между v1 и v2;
- NDCG/MRR, если есть клики или асессоры;
- latency p95/p99;
- tail failures;
- распределение score’ов;
- downstream-метрики в ранжировании, рекомендациях или RAG.
Предупреждение: высокий overlap@k не гарантирует улучшения продукта. Новый retrieval может менять diversity, freshness, coverage и нагрузку на следующий ranker. Cutover лучше делать через feature flag, с мониторингом качества, latency, error rate и быстрым rollback на ann_index_v1.
Вывод:
Обновление encoder’а - это миграция embedding contract и ANN-инфраструктуры, а не простая замена модели в inference path.z_val, если немного увеличить вес train-примера z_train.
I(z_train, z_val) ≈ - ∇L_val^T H^-1 ∇L_train
где H - Hessian по параметрам модели.
Если influence большой и положительный, train-пример, вероятно, увеличивает validation loss и вредит качеству.
Плюсы:
- аккуратная теоретическая постановка;
- можно связывать конкретные train-примеры с конкретными ошибками модели.
Минусы:
- дорогой H^-1;
- плохо масштабируется на большие нейросети;
- чувствителен к non-convexity, batchnorm/dropout, чекпоинтам и приближению Hessian.
В production обычно используют приближения: LiSSA, conjugate gradients, low-rank approximation или считают influence только для последнего слоя / head модели.
2. TracIn
Более инженерный вариант: train-пример полезен для val-примера, если их градиенты по ходу обучения направлены похоже. Вреден - если направлены противоположно.
TracIn(z_train, z_val) = Σ_c η_c · ∇L_train(θ_c) · ∇L_val(θ_c)
где θ_c - чекпоинты, η_c - learning rate.
Сильно отрицательный score означает: train-пример тянет модель против направления, полезного для validation.
Мини-скетч для последнего слоя:
for ckpt in checkpoints:
model.load_state_dict(load(ckpt))
g_val = mean_grad(model.head, val_loader)
for i, batch in enumerate(train_subset):
g_train = grad(model.head, batch)
scores[i] += lr[ckpt] * dot(g_train, g_val)
harmful = argsort(scores)[:K]
Практический совет: считайте score не по всему validation, а по важным production-срезам: новые пользователи, редкие классы, проблемные регионы, свежий drift, сегменты с высокой бизнес-ценой или высоким SLA.
3. Data pruning перед fine-tune
Рабочий пайплайн:
1. Зафиксировать production-like validation set без leakage.
2. Обучить baseline / fine-tune и сохранить несколько чекпоинтов.
3. Посчитать influence или TracIn для train→val.
4. Проверить top harmful samples:
- label noise;
- outdated distribution;
- конфликтующие дубликаты;
- corrupted inputs;
- неправильная task/schema version.
5. Удалить, downweight или отправить на relabeling.
6. Повторить fine-tune и проверить не только общий metric, но и regression по сегментам.
Production-пример: перед дообучением рекомендательной модели на свежих логах можно найти старые взаимодействия с изменившейся таксономией товаров, конфликтующие labels после миграции схемы или ботовый трафик, который ухудшает ranking loss на свежем holdout.
4. Предупреждение
Не стоит слепо удалять все «вредные» примеры. Иногда они ухудшают текущий validation, но нужны для long-tail robustness, fairness или устойчивости к редким сценариям.
Безопаснее начинать с top-K, делать human-in-the-loop аудит, сравнивать варианты remove / downweight / relabel и смотреть trade-off между quality, latency пересчета, стоимостью разметки, воспроизводимостью и надежностью мониторинга.
Вывод:
Influence Functions и TracIn полезны не как магическая чистка данных, а как инженерный способ сделать fine-tuning менее токсичным к шуму, устаревшим данным и конфликтующей разметке.Предсказываем churn на датуOffline все красиво. Online - просадка. 1. Point-in-time join - базовая защита Для каждой строки обучения естьt, а в фичах используемtransactions_last_30d, посчитанный после backfill’а из таблицы, куда транзакции доехали с задержкой или были пересчитаны с учетом будущих исправлений.
prediction_time. Фичи должны быть взяты в том состоянии, в котором они были доступны на этот момент.
Важно различать:
- event_time - когда событие реально произошло;
- ingestion_time / created_at - когда оно попало в систему;
- available_at - когда фича стала доступна модели;
- prediction_time - момент прогноза.
Правильный join должен учитывать не только event_time <= prediction_time, но и available_at <= prediction_time:
WITH ranked_features AS (
SELECT
l.entity_id,
l.prediction_time,
f.feature_value,
ROW_NUMBER() OVER (
PARTITION BY l.entity_id, l.prediction_time
ORDER BY f.event_time DESC
) AS rn
FROM labels l
JOIN features f
ON f.entity_id = l.entity_id
AND f.event_time <= l.prediction_time
AND f.available_at <= l.prediction_time
)
SELECT *
FROM ranked_features
WHERE rn = 1;
Если нет available_at, вы часто не можете доказать, что leakage отсутствует.
2. Backfill’ы - скрытый источник утечки
Backfill опасен тем, что создает иллюзию исторической полноты.
Например, сегодня вы пересчитали фичу за прошлый год:
- исправили старые события;
- добавили данные из нового источника;
- поменяли business logic;
- подтянули late-arriving events;
- использовали справочник, которого тогда еще не было.
В результате train получает историю, которой на самом деле не существовало в момент прогноза.
Корректный backfill должен отвечать на вопрос:
Какую фичу модель увидела бы тогда, если бы пайплайн работал с теми же задержками, источниками и правилами доступности?Если ответ неизвестен, это не
historical truth, а reconstructed truth. Для обучения моделей это разные вещи.
3. Проверка каузальности фичей
Перед обучением каждую фичу стоит прогнать через causality review.
Минимальный чеклист:
1. Фича доступна до prediction_time?
Не событие произошло, а именно значение фичи было доступно.
2. Нет ли в фиче label proxy?
Например, days_since_last_payment_failed для задачи дефолта может быть почти прямым следствием будущего таргета.
3. Окно агрегации строго в прошлом?
last_7d должно означать [t-7d, t), а не календарную неделю, которая включает будущее относительно t.
4. Нет ли future-aware справочников?
Сегменты, статусы, лимиты, antifraud-флаги и CRM-атрибуты часто обновляются задним числом.
5. Учитывается ли latency источника?
Если данные приезжают через 6 часов, то для прогноза в 10:00 нельзя использовать событие в 09:55, даже если event_time подходит.
Вывод:
В production ML фича считается валидной не тогда, когда она исторически верна, а тогда, когда доказуемо доступна модели в момент принятия решения.p_prod(x) != p_cal(x)
но предполагаем, что p(y|x) примерно сохраняется. Если считать обычный conformal quantile на старом calibration set, coverage на текущем трафике может просесть.
Наивное решение - увеличить correction глобально. Coverage частично вернется, но price prediction interval, ETA interval или forecast band станут настолько широкими, что downstream-система перестанет им доверять.
Базовый production-рецепт
1. Учим quantile-модель:
q_low(x), q_high(x)
2. На calibration set считаем nonconformity scores:
s_i = max(q_low(x_i)-y_i, y_i-q_high(x_i), 0)
3. Оцениваем importance weights:
w_i ~= p_prod(x_i) / p_cal(x_i)
4. Берем не обычный, а weighted quantile score'ов.
5. Для нового объекта строим:
C(x) = [q_low(x)-tau, q_high(x)+tau]
Минимальный скелет:
import numpy as np
def weighted_quantile(values, weights, q):
order = np.argsort(values)
v = np.asarray(values)[order]
w = np.asarray(weights)[order]
cw = np.cumsum(w)
return v[np.searchsorted(cw, q * cw[-1])]
alpha = 0.1
scores = np.maximum(q_low_cal - y_cal,
y_cal - q_high_cal,
0)
weights = ratio_model.predict_weight(X_cal)
tau = weighted_quantile(scores, weights, 1 - alpha)
low = q_low_prod - tau
high = q_high_prod + tau
Так calibration distribution становится ближе к production distribution без грубого раздувания всех интервалов.
Как не получить слишком широкие интервалы
Один глобальный tau часто переоценивает неопределенность, если ошибка модели сильно зависит от x.
Практически помогают:
- CQR вместо point prediction: Conformalized Quantile Regression уже моделирует heteroscedastic uncertainty, поэтому conformal correction обычно меньше.
- Нормализованный score: например s_i = |y_i - y_hat_i| / sigma_hat(x_i), а интервал строится как y_hat(x) +- tau * sigma_hat(x).
- Локальная калибровка: отдельный tau по geo, device, channel, price bucket или risk bucket. Это близко к Mondrian conformal, но требует достаточного числа calibration examples в каждом сегменте.
- Rolling calibration buffer: для рекомендаций, скоринга и forecasting старый calibration set быстро перестает описывать текущий traffic mix.
Главный риск - плохие веса
Density ratio model может быть шумной. Несколько объектов с огромными весами фактически “заменят” весь calibration set.
Контролируйте:
ESS = (sum w)^2 / sum(w^2)
Если ESS низкий, weighted quantile нестабилен, а интервалы начинают прыгать от релиза к релизу.
Практичные меры:
- clip weights и мониторить долю clipped weights;
- сглаживать density ratio;
- объединять редкие сегменты;
- не калибровать сегмент, где мало свежих labels;
- запускать перекалибровку при падении ESS или drift по X.
Production-чеклист
- отдельный calibration set, не смешанный с training;
- drift detection по feature distribution;
- density ratio model между prod traffic и calibration traffic;
- weighted conformal calibration;
- мониторинг coverage, average width, coverage по slices, ESS и latency;
- алерты на рост ширины интервалов без роста ошибки;
- A/B validation, если интервалы влияют на routing, fallback или human review.
Важно не путать marginal и conditional coverage. Conformal может держать 90% coverage на потоке в среднем, но проваливаться в отдельных микросегментах. В production это надо проверять явно.
Вывод:
При covariate shift цель не в том, чтобы слепо расширить интервалы, а в том, чтобы калибровать их под текущую смесь production-объектов и контролировать надежность этой калибровки.top-k могут стать мусором. Особенно если confidence logic завязана на score или gap между top-1 и top-2.
2. Anchor queries вместо labels
В production почти никогда нет labels вида «для этого query релевантны вот эти chunks». Но можно взять стабильный набор production-запросов: например, 500-5000 частых или бизнес-критичных query.
Это не разметка. Мы не знаем правильный chunk. Но знаем, что retrieval-поведение не должно хаотично меняться после каждого обновления корпуса.
Для каждого anchor query сохраняйте baseline:
- top-k doc/chunk ids;
- retrieval scores;
- rank positions;
- gap между top-1 и top-2;
- diversity top-k;
- source distribution.
После обновления корпуса сравнивайте новый retrieval с baseline.
Полезные proxy-метрики:
- Jaccard@k между старым и новым top-k;
- rank churn: сколько документов поменяло позиции;
- score distribution shift;
- падение top-1 score;
- уменьшение score gap;
- рост доли low-confidence retrieval;
- изменение источников в top-k;
- рост почти одинаковых чанков в top-k.
Минимальный набор, который уже даёт сигнал:
- mean_jaccard@10;
- p95_top1_score_drop;
- score_wasserstein между baseline и current scores.
3. Как интерпретировать сигналы
- mean_jaccard@10 резко упал: retriever стал приносить другой контекст;
- top-1 score системно падает: запросы хуже матчятся с корпусом;
- score distribution сильно сдвинулась: старые thresholds и confidence logic могли сломаться.
Практический совет: считайте эти метрики не только глобально, но и по сегментам - источникам, языкам, типам документов, продуктовым доменам. Глобальный average легко скрывает деградацию в критичном сегменте.
4. Retrieval confidence без ground truth
Даже без разметки можно смотреть на «уверенность» ретривера:
- высокий top-1 score;
- большой gap между top-1 и top-2;
- согласованность dense retrieval и BM25;
- стабильность top-k при query rewriting;
- низкая доля дубликатов в top-k;
- покрытие нужных источников.
Если dense и lexical retrieval внезапно начали расходиться, не стоит списывать это на шум. Часто это значит, что корпус или запросы изменились так, что одна из стратегий больше не работает как раньше.
Production minimum для RAG:
- хранить snapshot retrieval-результатов для anchor queries;
- считать overlap, score drift и rank churn после каждого обновления корпуса;
- отдельно мониторить дубликаты, новые чанки и распределения источников;
- заводить alerts не на один query, а на агрегаты по сегментам.
Corpus drift неприятен тем, что не выглядит как авария. Система отвечает, ошибок нет, latency нормальная. Просто контекст стал чуть менее релевантным. Потом ещё чуть менее. И качество RAG медленно проседает.
Вывод:
Без labels нельзя честно измерить relevance, но можно мониторить стабильность retrieval-поведения, уверенность ретривера и изменения корпуса, чтобы поймать деградацию раньше пользователей.