Наблюдаемость и дрейф
Собирай метрики, выявляющие незаметное снижение точности, проверяй распределения уверенности на предмет дрейфа и создавай оповещения только по сигналам, требующим действий.
Сервис компьютерного зрения может работать, быть быстрым и при этом ошибаться. Камеру задели, освещение изменилось, появилась новая линейка товаров, а модель продолжает возвращать боксы, пока все панели мониторинга инфраструктуры показывают зелёный статус. В этом уроке затемнение ролика для имитации сумерек изменило среднюю уверенность всего на 0,01 — на графике это незаметно, — но двухвыборочный тест KS выявил изменение при p = 0,007. Ты создашь эту проверку и настроишь сбор данных, которые она использует.
Практические занятия
Три уровня сигналов#
| Уровень | Вопрос | Сигналы |
|---|---|---|
| Инфраструктура | Сервис работает и успевает обрабатывать данные? | Задержка, пропускная способность, потерянные кадры, загрузка CPU и GPU |
| Поведение модели | Модель ведёт себя так же, как на прошлой неделе? | Детекции на кадр, средняя уверенность, распределение классов, размеры боксов |
| Точность модели | Модель по-прежнему выдаёт правильные результаты? | mAP на отложенной выборке (метрики производительности), выборочная проверка человеком, доля ложных срабатываний |
Большинство команд покрывают первый уровень стандартными инструментами и на этом останавливаются. Именно на втором и третьем уровнях системы компьютерного зрения незаметно дают сбои; руководство по мониторингу модели описывает полный цикл обслуживания.

Если ты развёртываешь модель через Ultralytics Platform, на странице развёртывания доступны метрики уровня инфраструктуры (запросы, частота ошибок, задержка P95) и журналы запросов, в которых можно отфильтровать ошибки, как показано выше; они рассматриваются в уроке «Мониторинг работоспособности и точности». Уровни поведения и точности тебе предстоит реализовать самостоятельно — этим и займёмся в оставшейся части урока.
Записываем метрики для каждого кадра#
Каждый объект Results содержит данные о времени обработки и боксах. Записывай по одной строке JSON на каждый кадр и отправляй её в используемую тобой систему метрик:
import json
import time
from ultralytics import YOLO
model = YOLO("yolo26n.pt")
video = "https://github.com/ultralytics/assets/releases/download/v0.0.0/solutions_ci_demo.mp4"
with open("metrics.jsonl", "w") as log:
for r in model.predict(video, stream=True, verbose=False):
conf = r.boxes.conf
record = {
"ts": time.time(),
"camera": "lobby-1",
"inference_ms": round(r.speed["inference"], 1),
"detections": len(r.boxes),
"mean_conf": round(conf.mean().item(), 3) if len(conf) else None,
"confs": [round(c, 3) for c in conf.tolist()], # исходные значения для проверки дрейфа
}
log.write(json.dumps(record) + "\n")
print({k: v for k, v in record.items() if k != "confs"}){'ts': 1792003254.33, 'camera': 'lobby-1', 'inference_ms': 34.7, 'detections': 22, 'mean_conf': 0.543}Для чего нужно каждое поле:
- inference_ms: отправляй оповещение, если p95 превышает заданный бюджет.
- detections: резкое падение почти до нуля обычно означает, что камера заблокирована или неисправна; всплеск указывает на смену сцены или ложные срабатывания.
- mean_conf: удобная для быстрого просмотра линия тренда, но сама по себе она слишком грубая, чтобы выявлять дрейф.
- confs: полное распределение, необходимое для теста на дрейф ниже.
Проверка дрейфа распределений confidence#
Дрейф данных проявляется в значениях confidence задолго до того, как кто-либо разметит новые данные. При запуске собери значения confidence для базового уровня, затем собери их за недавний период и сравни два распределения с помощью двухвыборочного критерия Колмогорова — Смирнова (pip install scipy). Этот скрипт имитирует сумерки, затемняя тот же видеоклип:
import cv2
import numpy as np
from scipy.stats import ks_2samp
from ultralytics import YOLO
from ultralytics.utils.downloads import safe_download
model = YOLO("yolo26n.pt")
video = str(safe_download("https://github.com/ultralytics/assets/releases/download/v0.0.0/solutions_ci_demo.mp4"))
def confidences(darken=1.0):
"""Collect every detection confidence from the clip, optionally darkened to simulate dusk."""
cap, out = cv2.VideoCapture(video), []
while True:
ok, frame = cap.read()
if not ok:
break
out += model((frame * darken).astype(np.uint8), verbose=False)[0].boxes.conf.tolist()
return np.array(out)
baseline = confidences() # what "normal" looked like at launch
for name, recent in (("same scene", confidences()), ("dusk (35% brightness)", confidences(darken=0.35))):
stat, p = ks_2samp(baseline, recent)
print(f"{name:22s} n={len(recent):4d} mean_conf={recent.mean():.3f} KS={stat:.3f} p={p:.2g}")same scene n=1376 mean_conf=0.519 KS=0.000 p=1
dusk (35% brightness) n=1337 mean_conf=0.508 KS=0.064 p=0.0068Разберём всё по пунктам:
- Та же сцена: идентичные распределения дают статистику KS, равную 0, и p = 1 — это контрольный результат.
- Сумерки: на 39 детекций меньше, а среднее значение всего на 0,011 ниже — этого никто не заметил бы на графике. Статистика KS (наибольший разрыв между двумя кумулятивными распределениями) равна 0,064, а p = 0,0068 < 0,01 говорит о том, что сдвиг реален.
Оповещение о дрейфе — повод просмотреть примеры кадров, а не доказательство сбоя. Если дрейф подтвердился, собери и разметь кадры с затронутых камер: в руководстве по сбору и аннотации данных описано, что собирать, а в руководстве по дообучению — как выполнять повторное обучение. Размеры боксов (ширина × высота) дают ещё один простой сигнал дрейфа: именно в них в первую очередь проявляются изменения крепления и оптики.
Еженедельная проверка на отложенной выборке#
Сигналы поведения показывают, что что-то изменилось; только разметка покажет, снизилась ли точность. Храни отложенную выборку из 200–500 размеченных производственных изображений и проверяй каждую версию модели, а также выполняй проверку каждую неделю:
yolo val model=yolo26n.pt data=coco8.yaml > weekly_metrics.txt # use your production model and holdout YAMLЗаписывай mAP50-95 и AP по классам с течением времени и отправляй оповещение, если mAP падает более чем на 1 пункт за неделю. Самая частая причина — замена модели, которая должна была ничего не изменить; вторая по частоте — дрейф. Матрица ошибок на той же отложенной выборке наглядно покажет регрессии по классам.

Именно отложенная выборка помогает ответить на вопрос на этом графике. Переобучай модель, когда выборка показывает, что качество начинает падать, или когда у тебя накопится достаточно новых размеченных данных для условий, в которых возник дрейф, а не по фиксированному календарному графику.
Настраивай оповещения по действиям, а не по числам#
| Сигнал | Условие срабатывания | Действие |
|---|---|---|
p95 inference_ms | Превышение бюджета в течение 3 минут или дольше | Увеличь число экземпляров или переключись на оптимизированный экспорт |
| Детекции на кадр | Падение на 50% или больше в течение 5 минут | Проверь видеопоток с камеры, затем версию модели |
| Пропущенные кадры | Более 5% в течение 10 минут | Добавь вычислительные ресурсы или снизь FPS для каждого потока |
| Тест KS для confidence | p < 0,01 на суточных интервалах | Просмотри кадры, затем запусти проверку на отложенной выборке |
| mAP50-95 на отложенной выборке | Падение на 1 пункт или больше за неделю | Собери данные, разметь их и выполни дообучение |
Подводные камни#
Симптом: проверка KS срабатывает каждый вечер и каждый понедельник. Решение: суточные и недельные циклы действительно меняют распределение. Сравнивай сопоставимые данные (те же камеры, часы и дни недели) и используй окна в несколько тысяч детекций.
Симптом: mAP на отложенной выборке выглядит нормально, но пользователи сообщают о пропусках. Решение: отложенная выборка должна отражать текущую рабочую среду. Обновляй её каждый квартал новыми размеченными кадрами с важных камер.
Для каждого оповещения нужно письменно указать следующее действие. Сигнал, за которым ты лишь хочешь наблюдать, должен быть на панели мониторинга, а не в системе оповещений.
Показать решение
Сохрани первое здоровое окно вывода monitor.py как baseline.jsonl, затем сравнивай с ним каждое новое окно:
import json
import numpy as np
from scipy.stats import ks_2samp
def load(path):
"""Flatten the per-frame 'confs' lists written by monitor.py."""
with open(path) as f:
return np.array([c for line in f for c in json.loads(line)["confs"]])
baseline, recent = load("baseline.jsonl"), load("metrics.jsonl")
stat, p = ks_2samp(baseline, recent)
print(f"KS={stat:.3f} p={p:.2g} -> {'review frames' if p < 0.01 else 'ok'}")KS=0.000 p=1 -> okЭто ожидаемый результат, если оба файла получены из одного и того же видео. Запускай проверку ежедневно по расписанию, отправляй статистику и p-value в ту же систему метрик, что и логи по кадрам, а review frames направляй человеку, а не в систему пейджинга.