Computer Vision Model Fine-Tuning for Custom Tasks

We design and deploy artificial intelligence systems: from prototype to production-ready solutions. Our team combines expertise in machine learning, data engineering and MLOps to make AI work not in the lab, but in real business.
Showing 1 of 1 servicesAll 1566 services
Computer Vision Model Fine-Tuning for Custom Tasks
Medium
~3-5 business days
FAQ
AI Development Areas
AI Solution Development Stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1212
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_logo-advance_0.png
    B2B Advance company logo design
    561
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822

Fine-tuning моделей Computer Vision под кастомные задачи

Взять ImageNet-pretrained модель и дообучить на своих данных — звучит просто. На практике большинство проектов спотыкается на одном и том же: обучение улучшает train mAP до 0.91, а production даёт 0.58. Причина почти всегда не в архитектуре, а в несоответствии распределений: аугментации не покрывают production-условия, train/val split сделан по файлам а не по сценам, leakage между похожими изображениями.

Почему переобучение — главная проблема fine-tuning CV

Типичный кейс: детекция дефектов на производстве. 3200 изображений, YOLOv8m, 100 эпох. val [email protected] = 0.89. Запускаем на новой смене — 0.53. Анализ confusion matrix показывает: модель научилась детектировать дефекты по фону (конкретная линия конвейера), а не по самому дефекту. Решение — аугментации, симулирующие смену условий:

import albumentations as A
from albumentations.pytorch import ToTensorV2

# Аугментации для производственного CV
# Имитируем смену освещения, камеры, угла съёмки
production_augments = A.Compose([
    # Геометрические — небольшой диапазон для детекции
    A.ShiftScaleRotate(
        shift_limit=0.05, scale_limit=0.1,
        rotate_limit=10, p=0.5
    ),
    A.HorizontalFlip(p=0.5),
    A.Perspective(scale=(0.02, 0.05), p=0.3),

    # Освещение — ключевое для производства
    A.OneOf([
        A.RandomBrightnessContrast(
            brightness_limit=0.3, contrast_limit=0.3
        ),
        A.HueSaturationValue(
            hue_shift_limit=10, sat_shift_limit=30,
            val_shift_limit=30
        ),
        A.CLAHE(clip_limit=4.0, tile_grid_size=(8, 8)),
    ], p=0.7),

    # Шум и артефакты камеры
    A.OneOf([
        A.GaussNoise(var_limit=(10, 50)),
        A.ISONoise(color_shift=(0.01, 0.05)),
        A.ImageCompression(quality_lower=75, quality_upper=100),
    ], p=0.4),

    # Имитация загрязнения объектива, запотевания
    A.RandomFog(fog_coef_lower=0.1, fog_coef_upper=0.3, p=0.15),
    A.RandomShadow(num_shadows_lower=1, num_shadows_upper=2, 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'],
    min_visibility=0.3   # удаляем bbox, если <30% видно после crop
))

Стратегия разбивки данных при fine-tuning

Стратифицированный split по файлам — ошибка, если изображения сняты сериями. Правильно: split по уникальным сценам/объектам/сессиям.

from sklearn.model_selection import GroupShuffleSplit
import pandas as pd

df = pd.read_csv('annotations.csv')
# scene_id — уникальный идентификатор сцены/объекта/сессии
gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42)

train_idx, val_idx = next(
    gss.split(df, df['label'], groups=df['scene_id'])
)

train_df = df.iloc[train_idx]
val_df   = df.iloc[val_idx]

# Проверка: нет пересечения scene_id между split'ами
assert len(
    set(train_df['scene_id']) & set(val_df['scene_id'])
) == 0, "Data leakage detected!"

Selection backbone и learning rate schedule

Задача Рекомендуемый backbone LR start Стратегия
Классификация, много данных (>5k/класс) EfficientNet-B4, ConvNeXt-S 1e-4 Cosine decay
Классификация, мало данных (<500/класс) ViT-B/16 (frozen → unfreeze) 1e-5 Warmup + cosine
Детекция, стандарт YOLOv8m/l 0.01 SGD + cosine
Детекция, мелкие объекты RT-DETR-L 1e-4 AdamW + step
Сегментация SegFormer-B2/B4 6e-5 Poly decay

Главная ошибка с ViT при малом датасете — обучать все слои сразу. Правильный подход: сначала замораживаем transformer blocks, обучаем только classifier head 10–15 эпох, потом постепенно размораживаем с LR в 10x меньше базового.

import timm
import torch

model = timm.create_model(
    'vit_base_patch16_224',
    pretrained=True,
    num_classes=num_classes
)

# Этап 1: только head
for name, param in model.named_parameters():
    if 'head' not in name:
        param.requires_grad = False

optimizer_stage1 = torch.optim.AdamW(
    filter(lambda p: p.requires_grad, model.parameters()),
    lr=1e-3, weight_decay=0.01
)

# После 15 эпох — этап 2: размораживаем последние 4 блока
for name, param in model.named_parameters():
    if any(f'blocks.{i}' in name for i in range(8, 12)):
        param.requires_grad = True

optimizer_stage2 = torch.optim.AdamW(
    [
        {'params': model.head.parameters(), 'lr': 1e-4},
        {'params': [p for n, p in model.named_parameters()
                    if 'blocks' in n and p.requires_grad],
         'lr': 1e-5}
    ],
    weight_decay=0.01
)

Дисбаланс классов

Precision 0.73 при recall 0.91 на редком классе — типичная картина при дисбалансе 1:50. Решения в порядке эффективности:

  1. Focal Loss (γ=2.0) — снижает вес лёгких примеров в функции потерь
  2. WeightedRandomSampler — oversample редких классов в DataLoader
  3. Class-aware augmentation — агрессивнее аугментировать редкие классы
from torch.utils.data import WeightedRandomSampler
import torch

# class_counts: [n_class0, n_class1, ...]
class_weights = 1.0 / torch.tensor(class_counts, dtype=torch.float)
sample_weights = class_weights[targets]  # targets: метки всего датасета

sampler = WeightedRandomSampler(
    weights=sample_weights,
    num_samples=len(sample_weights),
    replacement=True
)

Трекинг экспериментов

MLflow или Weights & Biases обязательны — без трекинга невозможно воспроизвести лучший результат:

import mlflow

mlflow.set_experiment('defect_detection_v3')
with mlflow.start_run(run_name='yolov8m_focal_weighted_sampler'):
    mlflow.log_params({
        'model': 'yolov8m',
        'img_size': 640,
        'epochs': 100,
        'batch_size': 16,
        'lr0': 0.01,
        'loss': 'focal',
        'augment_strategy': 'production_v2'
    })
    # ... обучение ...
    mlflow.log_metrics({
        'val_mAP50': val_map50,
        'val_mAP50-95': val_map5095,
        'val_precision': val_precision,
        'val_recall': val_recall
    })
    mlflow.pytorch.log_model(model, 'model')

Сроки

Работа Срок
Fine-tuning классификатора (готовые данные) 1–2 недели
Fine-tuning детектора + итерации 3–5 недель
Full pipeline: данные → fine-tuning → деплой 6–10 недель