Observabilidade e deriva
Regista as métricas que detetam perdas de precisão silenciosas, testa a deriva das distribuições de confiança e gera alertas apenas para sinais que exigem ação.
Um serviço de visão pode estar disponível, ser rápido e estar errado. Uma câmera é deslocada, a iluminação muda, surge uma nova linha de produtos e o modelo continua a devolver caixas enquanto todos os painéis de infraestrutura permanecem verdes. Nesta lição, escurecer um clipe para simular o anoitecer alterou a confiança média em apenas 0,01 — algo impercetível num gráfico —, mas um teste KS de duas amostras assinalou a alteração com p = 0,007. Vais criar esse teste e o registo de dados que o alimenta.
Prático
Três camadas de sinais#
| Camada | Pergunta | Sinais |
|---|---|---|
| Infraestrutura | O serviço está disponível e acompanha o ritmo? | Latência, débito, frames descartados, utilização de CPU e GPU |
| Comportamento do modelo | O modelo está a comportar-se como na semana passada? | Deteções por frame, confiança média, distribuição de classes, tamanhos das caixas |
| Precisão do modelo | O modelo continua a acertar? | mAP no conjunto de teste reservado (métricas de desempenho), revisão humana por amostragem, taxa de falsos alarmes |
A maioria das equipas cobre a primeira camada com ferramentas padrão e fica por aí. A segunda e a terceira são onde os sistemas de visão falham silenciosamente; o guia de monitorização de modelos aborda todo o ciclo de manutenção.

Se implementares através da Ultralytics Platform, a página de implementação fornece a camada de infraestrutura (pedidos, taxa de erros, latência P95) e registos de pedidos que podes filtrar por erros, como acima; a lição Monitorizar a integridade e a precisão aborda esses aspetos. As camadas de comportamento e precisão ficam a teu cargo, e o resto desta lição ensina a criá-las.
Emite métricas por frame#
Cada objeto Results inclui tempos e caixas. Escreve uma linha JSON por frame e envia-a para o sistema de métricas que utilizas:
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()], # valores brutos para o teste de deriva
}
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}Para que serve cada campo:
- inference_ms: gera um alerta quando p95 exceder o limite definido.
- detections: uma queda repentina para perto de zero geralmente indica uma câmera bloqueada ou com defeito; um aumento indica uma mudança na cena ou falsos positivos.
- mean_conf: uma linha de tendência rápida, mas demasiado grosseira para detectar deriva por si só.
- confs: a distribuição completa, necessária para o teste de deriva abaixo.
Testar distribuições de confiança para detectar deriva#
A deriva de dados aparece nos níveis de confiança muito antes de alguém rotular dados novos. Recolhe os níveis de confiança no lançamento como linha de base, recolhe uma janela recente e compara as duas distribuições com um teste de Kolmogorov-Smirnov de duas amostras (pip install scipy). Este script simula o anoitecer escurecendo o mesmo clipe:
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.0068Lê linha a linha:
- Mesma cena: distribuições idênticas produzem uma estatística KS de 0 e p = 1, o resultado de controlo.
- Anoitecer: 39 deteções a menos e uma média apenas 0,011 inferior, algo que ninguém notaria num gráfico. A estatística KS (a maior diferença entre as duas distribuições cumulativas) é 0,064, e p = 0,0068 < 0,01 indica que a alteração é real.
Um alerta de deriva é um motivo para analisar fotogramas de amostra, não uma prova de falha. Quando a deriva for real, recolhe e rotula fotogramas das câmeras afetadas: o guia de recolha e anotação de dados explica o que recolher, e o guia de ajuste fino explica o retreino. Os tamanhos das caixas (largura × altura) são um segundo indicador simples de deriva: as novas montagens e óticas aparecem primeiro nesses dados.
Valida semanalmente um conjunto de validação reservado#
Os sinais comportamentais indicam que algo mudou; só os rótulos mostram se a precisão caiu. Mantém um conjunto de validação reservado com 200–500 imagens de produção rotuladas e valida cada versão do modelo, além de o fazer todas as semanas:
yolo val model=yolo26n.pt data=coco8.yaml > weekly_metrics.txt # use your production model and holdout YAMLRegista mAP50-95 e AP por classe ao longo do tempo e gera um alerta quando mAP cair mais de 1 ponto de uma semana para a seguinte. A causa mais comum é uma troca de modelo que deveria ser neutra; a segunda é a deriva. Uma matriz de confusão sobre o mesmo conjunto de validação reservado torna evidentes as regressões por classe.

O conjunto de validação reservado é o que responde à pergunta deste gráfico. Retreina quando mostrar que a qualidade começa a cair ou quando tiveres dados rotulados novos suficientes das condições com deriva, e não segundo um calendário fixo.
Gera alertas para ações, não para números#
| Sinal | Gerar alerta quando | Ação |
|---|---|---|
p95 inference_ms | Acima do limite por mais de 3 minutos | Escala horizontalmente ou muda para a exportação otimizada |
| Deteções por fotograma | Queda de 50% ou mais durante 5 minutos | Verifica o fluxo da câmera e, depois, a versão do modelo |
| Fotogramas perdidos | Acima de 5% durante 10 minutos | Aumenta a capacidade ou reduz os FPS por fluxo |
| Teste KS de confiança | p < 0,01 em janelas diárias | Analisa os fotogramas e, depois, executa a validação reservada |
| mAP50-95 do conjunto de validação reservado | Queda de 1 ponto ou mais de uma semana para a seguinte | Recolhe, rotula e ajusta o modelo |
Armadilhas#
Sintoma: o teste KS dispara todas as noites e todas as segundas-feiras. Solução: ciclos diários e semanais são mudanças reais de distribuição. Compare situações equivalentes (mesmas câmeras, mesmos horários, mesmo dia da semana) e use janelas de milhares de detecções.
Sintoma: a mAP do conjunto de teste parece boa, mas os usuários relatam objetos não detectados. Solução: o conjunto de teste precisa se parecer com a produção atual. Atualize-o a cada trimestre com novos quadros rotulados das câmeras importantes.
Todo alerta precisa de uma próxima ação descrita por escrito. Um sinal que você só quer monitorar deve ficar em um painel, não no sistema de alertas.
Mostrar solução
Guarda a primeira janela saudável da saída de monitor.py como baseline.jsonl e, depois, compara cada nova janela com ela:
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 -> okEsse é o resultado esperado quando os dois ficheiros provêm das mesmas imagens. Executa o teste diariamente com um agendador, envia a estatística e o valor p para o mesmo sistema de métricas dos registos por fotograma e encaminha review frames para uma pessoa, não para um sistema de pager.