Pipeline = contrato treino↔inferência
Regra de ouro: a mesma normalização usada no treino precisa ser aplicada em inferência, pixel por pixel. Discrepância aqui é a causa mais comum de modelo “bom em validação, ruim em produção”.
Se seu treino normaliza com mean=[0.485, 0.456, 0.406] e std=[0.229, 0.224, 0.225] (ImageNet) mas a inferência esquece — acurácia cai 5–15% silenciosamente. Commit no repositório: arquivo preprocess.py único importado pelo treino e pela API.
Augmentation com Albumentations
import albumentations as A
from albumentations.pytorch import ToTensorV2
train_transform = A.Compose([
A.LongestMaxSize(max_size=640),
A.PadIfNeeded(min_height=640, min_width=640, border_mode=0),
A.HorizontalFlip(p=0.5),
A.RandomBrightnessContrast(p=0.3),
A.HueSaturationValue(p=0.3),
A.GaussNoise(p=0.2),
A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),
ToTensorV2(),
], bbox_params=A.BboxParams(format="yolo", label_fields=["class_labels"]))
val_transform = A.Compose([
A.LongestMaxSize(max_size=640),
A.PadIfNeeded(min_height=640, min_width=640, border_mode=0),
A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),
ToTensorV2(),
], bbox_params=A.BboxParams(format="yolo", label_fields=["class_labels"]))Validação/inferência nunca têm augmentation aleatório — só resize + normalize determinísticos. Violar isso destrói comparabilidade entre épocas.
Data loading: CPU vs DALI
# Pipeline DALI minimal: decode JPEG + resize + normalize 100% na GPU
from nvidia.dali import pipeline_def, fn, types
@pipeline_def(batch_size=64, num_threads=4, device_id=0)
def cv_pipeline(file_root):
jpegs, labels = fn.readers.file(file_root=file_root, random_shuffle=True)
images = fn.decoders.image(jpegs, device="mixed") # mixed = GPU decode
images = fn.resize(images, resize_x=224, resize_y=224)
images = fn.crop_mirror_normalize(
images,
mean=[0.485 * 255, 0.456 * 255, 0.406 * 255],
std=[0.229 * 255, 0.224 * 255, 0.225 * 255],
output_layout="CHW",
)
return images, labelsPreprocessing determinístico para inferência
# preprocess.py — importado pelo treino e pela API FastAPI
import cv2
import numpy as np
MEAN = np.array([0.485, 0.456, 0.406], dtype=np.float32)
STD = np.array([0.229, 0.224, 0.225], dtype=np.float32)
def preprocess(img_bgr: np.ndarray, size: int = 640) -> np.ndarray:
img = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
h, w = img.shape[:2]
scale = size / max(h, w)
nh, nw = int(h * scale), int(w * scale)
img = cv2.resize(img, (nw, nh), interpolation=cv2.INTER_LINEAR)
canvas = np.zeros((size, size, 3), dtype=np.uint8)
canvas[:nh, :nw] = img
x = canvas.astype(np.float32) / 255.0
x = (x - MEAN) / STD
return x.transpose(2, 0, 1) # HWC -> CHWUm arquivo, uma função, dois consumidores. Se o treino mudar de 640 pra 800, a API não quebra silenciosamente — uma mudança, um deploy sincronizado.