Files
PracticeClassif/_research/SECTION_classification.md
2026-06-30 15:17:40 +03:00

28 KiB
Raw Blame History

Задача классификации изображений: метрики, loss-функции, семплирование

Учебно-исследовательский раздел методички (научный руководитель — мнс Павленко Б.В.). Контекст применения: HPO (Optuna) для EdgeNeXt на датасете NarutoSign (multi-class классификация знаков-печатей).


1. Постановка задачи multi-class классификации

Дана обучающая выборка \{(x_i, y_i)\}_{i=1}^{N}, где x_i — изображение, y_i \in \{1, \dots, C\} — метка одного из C взаимоисключающих классов (multi-class, в отличие от multi-label, где классы не исключают друг друга). Модель f_\theta выдаёт вектор логитов (необработанных скоров) z = f_\theta(x) \in \mathbb{R}^{C}.

Softmax

Логиты переводятся в распределение вероятностей по классам функцией softmax:


p_k = \mathrm{softmax}(z)_k = \frac{e^{z_k}}{\sum_{j=1}^{C} e^{z_j}}, \qquad \sum_{k=1}^{C} p_k = 1,\; p_k \in (0,1).

Softmax инвариантен к сдвигу: \mathrm{softmax}(z) = \mathrm{softmax}(z + c), поэтому на практике вычитают \max_k z_k для численной устойчивости (log-sum-exp trick).

One-hot и целевое распределение

Истинная метка y кодируется one-hot вектором q \in \{0,1\}^{C}: q_k = \mathbb{1}[k = y]. Это «жёсткое» (hard) целевое распределение. «Мягкие» (soft) цели q_k \in [0,1] возникают при label smoothing, mixup/cutmix и knowledge distillation (см. §3).

Top-1 / top-k accuracy

Предсказание top-1: \hat{y} = \arg\max_k p_k. Top-k считает пример верным, если истинная метка попала в множество k наибольших по вероятности классов:


\text{top-}k\text{-acc} = \frac{1}{N}\sum_{i=1}^{N} \mathbb{1}\!\left[\, y_i \in \mathrm{TopK}_k\big(p^{(i)}\big)\,\right].

Top-5 — историческая метрика ImageNet (1000 классов). Для задач с малым C (например, 12 знаков-печатей) top-k при k \geq 2 малоинформативна; основная метрика — top-1.


2. Метрики

Базовые величины для класса c из confusion matrix (one-vs-rest): TP_c (true positive), FP_c, FN_c, TN_c. Обозначим n_c — число истинных объектов класса c, N = \sum_c n_c.

Метрика Формула Диапазон Чувствительность к дисбалансу
Accuracy (top-1) \dfrac{\sum_c TP_c}{N} [0,1] Высокая (доминирует majority-класс)
Balanced accuracy \dfrac{1}{C}\sum_c \dfrac{TP_c}{n_c} = \dfrac{1}{C}\sum_c \mathrm{Recall}_c [0,1] Низкая (макро-усреднение recall)
Top-k accuracy см. §1 [0,1] Высокая
Precision (класс c) P_c = \dfrac{TP_c}{TP_c + FP_c} [0,1]
Recall (класс c) R_c = \dfrac{TP_c}{TP_c + FN_c} [0,1]
F1 (класс c) F1_c = \dfrac{2 P_c R_c}{P_c + R_c} [0,1]
Macro-F1 \dfrac{1}{C}\sum_c F1_c [0,1] Низкая (классы равновесны)
Micro-F1 \dfrac{2\sum_c TP_c}{2\sum_c TP_c + \sum_c FP_c + \sum_c FN_c} [0,1] Высокая (= accuracy в multi-class)
Weighted-F1 \sum_c \dfrac{n_c}{N} F1_c [0,1] Высокая (вес \propto частоте)
MCC см. ниже [-1,1] Низкая (учитывает все 4 клетки)
Cohen's \kappa см. ниже [-1,1] Низкая (поправка на случай)
ROC-AUC (OvR) площадь под ROC, усреднённая [0,1] Зависит от усреднения
PR-AUC (OvR) площадь под precision-recall [0,1] Низкая (лучше для редких классов)
ECE см. §2.5 [0,1], ниже лучше — (про калибровку)

2.1 Macro / micro / weighted — когда какая

  • Micro: агрегирует TP/FP/FN по всем классам, затем считает метрику. В multi-class с single-label micro-F1 = micro-precision = micro-recall = accuracy. Отражает производительность «на объект», доминирует majority-класс.
  • Macro: метрика считается по каждому классу, затем простое среднее. Каждый класс весит одинаково → редкие классы влияют наравне с частыми. Главный выбор при дисбалансе.
  • Weighted: macro со взвешиванием по n_c → возвращает доминирование majority-класса, маскирует провал на редких классах.
Сценарий Рекомендуемое усреднение
Сбалансированные классы, важна общая доля верных micro / accuracy
Дисбаланс, все классы одинаково важны macro
Дисбаланс, но важность \propto частоте в проде weighted

scikit-learn — precision_recall_fscore_support / averaging

2.2 Confusion matrix

M \in \mathbb{N}^{C \times C}, M_{ij} = число объектов истинного класса i, предсказанных как j. Диагональ — верные. Нормировка по строкам (M_{ij}/n_i) даёт per-class accuracy = recall класса i. Самый информативный диагностический инструмент: показывает какие классы путаются (для NarutoSign — визуально похожие печати). sklearn.confusion_matrix

2.3 Matthews Correlation Coefficient (MCC)

Корреляция Пирсона между предсказанным и истинным классом. Multi-class форма (sklearn.matthews_corrcoef):


\mathrm{MCC} = \frac{\sum_k\sum_l\sum_m (M_{kk}M_{lm} - M_{kl}M_{mk})}
{\sqrt{\sum_k\big(\sum_l M_{kl}\big)\big(\sum_{k'\neq k}\sum_{l'} M_{k'l'}\big)}\;\sqrt{\sum_k\big(\sum_l M_{lk}\big)\big(\sum_{k'\neq k}\sum_{l'} M_{l'k'}\big)}}.

\mathrm{MCC}=1 — идеал, 0 — случай, -1 — полное расхождение. Высокий MCC требует хороших результатов по всем классам, поэтому это надёжная сводная метрика при дисбалансе.

2.4 Cohen's kappa


\kappa = \frac{p_o - p_e}{1 - p_e},

где p_o — наблюдаемая accuracy, p_e = \sum_c \frac{(\text{предсказано } c)\cdot(\text{истинно } c)}{N^2} — ожидаемое согласие «по случайности». Учитывает базовый уровень случайного угадывания. sklearn.cohen_kappa_score

2.5 ROC-AUC / PR-AUC (one-vs-rest)

Для multi-class каждый класс берётся как «positive», остальные — «negative» (OvR), по вероятностям p_c строится кривая, площади усредняются (macro / weighted). ROC-AUC = P(p_c^{+} > p_c^{-}) для случайной пары. При сильном дисбалансе ROC-AUC оптимистична (огромное число TN распухает specificity), поэтому для редких классов предпочтительна PR-AUC (average precision), не использующая TN. sklearn.roc_auc_score (multi_class='ovr') · average_precision_score

2.6 Калибровка (Expected Calibration Error, ECE)

Уверенность \hat{p}_i = \max_k p_k^{(i)} должна совпадать с фактической accuracy. Разбиваем [0,1] на B бинов \{b_m\}:


\mathrm{ECE} = \sum_{m=1}^{B} \frac{|b_m|}{N}\,\big|\,\mathrm{acc}(b_m) - \mathrm{conf}(b_m)\,\big|.

Современные сети переуверены (over-confident); label smoothing и temperature scaling снижают ECE (Müller et al., 2019, «When Does Label Smoothing Help?»; Guo et al., 2017, «On Calibration of Modern Neural Networks»).

2.7 Главная метрика при дисбалансе

При дисбалансе accuracy вводит в заблуждение: модель, предсказывающая только majority-класс, даёт высокую accuracy при нулевом recall на редких классах. Главными метриками выбирают macro-F1 или balanced accuracy (= macro-recall, sklearn.balanced_accuracy_score), т.к. они усредняют по классам с равным весом. Различие: balanced accuracy смотрит только на recall; macro-F1 балансирует precision и recall — предпочтительна, когда важны и пропуски, и ложные срабатывания. MCC — надёжная вторичная сводка. Для целей Optuna в качестве objective рекомендуется macro-F1 (или balanced accuracy) на валидации.


3. Loss-функции

Везде p_k — softmax-вероятность, y — истинный класс, p_y — вероятность истинного класса.

3.1 Cross-Entropy (CE)


\mathcal{L}_{\mathrm{CE}} = -\sum_{k=1}^{C} q_k \log p_k = -\log p_y \quad (\text{для one-hot } q).

Базовый loss. В PyTorch nn.CrossEntropyLoss принимает логиты (применяет log-softmax внутри).

3.2 Label Smoothing CE

Smooth-цель: q_k^{LS} = (1-\varepsilon)\,\mathbb{1}[k=y] + \dfrac{\varepsilon}{C} (равномерно размазывает \varepsilon по всем классам).


\mathcal{L}_{\mathrm{LS}} = -\sum_{k=1}^{C} q_k^{LS}\log p_k = (1-\varepsilon)\big(-\log p_y\big) + \frac{\varepsilon}{C}\sum_{k=1}^{C}\big(-\log p_k\big).

Типично \varepsilon \in [0.05, 0.1]. Уменьшает переуверенность, улучшает калибровку и обобщение (Szegedy et al., 2016, Inception-v3/ Rethinking the Inception Architecture; анализ — Müller et al., 2019, arXiv:1906.02629). Не борется напрямую с дисбалансом.

3.3 Focal Loss

Снижает вклад «лёгких» (хорошо классифицированных) примеров фактором (1-p_y)^\gamma (Lin et al., 2017, «Focal Loss for Dense Object Detection» / RetinaNet, arXiv:1708.02002):


\mathcal{L}_{\mathrm{FL}} = -\alpha_y\,(1 - p_y)^{\gamma}\,\log p_y.
  • \gamma — focusing parameter (типично \gamma = 2); при \gamma=0 сводится к CE.
  • \alpha_y — балансирующий вес класса (опционально).

При p_y \to 1 множитель (1-p_y)^\gamma \to 0 → лёгкие примеры почти не дают градиента, обучение фокусируется на трудных. Хорош при дисбалансе и большом числе «лёгких» фоновых примеров.

3.4 Class-Balanced Loss (effective number)

Переход от «числа сэмплов» к «эффективному числу» — учёт перекрытия близких изображений (Cui et al., 2019, CVPR, arXiv:1901.05555):


E_{n_c} = \frac{1 - \beta^{\,n_c}}{1 - \beta}, \qquad \beta = \frac{N-1}{N} \in [0,1).

Вес класса \propto 1/E_{n_c}, нормированный так, что \sum_c w_c = C:


w_c = \frac{1 - \beta}{1 - \beta^{\,n_c}}.

Подставляется как мультипликатор в любой базовый loss (CB-CE, CB-Focal):


\mathcal{L}_{\mathrm{CB}} = w_y \cdot \mathcal{L}_{\mathrm{base}}(p, y).

При \beta \to 0 → нет ребалансировки; \beta \to 1 → вес \propto 1/n_c (обратная частота). Типично \beta \in \{0.99, 0.999, 0.9999\}.

3.5 Weighted CE (class weights)

Частный случай: вес \propto обратной частоте, w_c = \dfrac{N}{C\, n_c} (нормировка опциональна):


\mathcal{L}_{\mathrm{wCE}} = -w_y \log p_y.

В PyTorch — аргумент weight в nn.CrossEntropyLoss. Простейшая мера против дисбаланса.

3.6 LDAM (кратко)

Label-Distribution-Aware Margin (Cao et al., 2019, NeurIPS, arXiv:1906.07413) добавляет класс-зависимый отступ в логит истинного класса перед softmax, увеличивая margin для редких классов:


\mathcal{L}_{\mathrm{LDAM}} = -\log \frac{e^{\,z_y - \Delta_y}}{e^{\,z_y - \Delta_y} + \sum_{k \neq y} e^{z_k}}, \qquad \Delta_c = \frac{K}{n_c^{1/4}}.

Маленький n_c → больший margin \Delta_c → строже decision boundary для minority. Обычно в связке с DRW (deferred re-weighting). Теоретически обоснован margin-based generalization bound.

3.7 Soft-target / KD-loss (кратко)

Knowledge Distillation (Hinton et al., 2015, arXiv:1503.02531): студент учится на «мягких» вероятностях teacher с температурой T:


\mathcal{L}_{\mathrm{KD}} = (1-\lambda)\,\mathcal{L}_{\mathrm{CE}}(p^{S}, y) + \lambda\,T^2\,\mathrm{KL}\!\big(p^{T}_{/T}\,\|\,p^{S}_{/T}\big),

где p_{/T} = \mathrm{softmax}(z/T). Множитель T^2 компенсирует масштаб градиентов. Soft-target несёт «dark knowledge» о близости классов (релевантно для визуально похожих печатей).

3.8 Таблица «когда какой loss»

Loss Когда применять Гиперпараметры Борется с дисбалансом
CE baseline, сбалансированные данные нет
Label Smoothing CE переуверенность, регуляризация, калибровка \varepsilon нет
Focal дисбаланс + много «лёгких» примеров \gamma, \alpha частично (по трудности)
Weighted CE умеренный дисбаланс, быстрый фикс веса w_c да (по частоте)
Class-Balanced (eff. number) сильный/long-tail дисбаланс \beta, базовый loss да (эфф. число)
LDAM (+DRW) long-tail, нужен margin для minority K, расписание DRW да (margin)
KD / soft-target есть teacher, похожие классы T, \lambda косвенно

4. Семплирование и борьба с дисбалансом

4.1 Random shuffle vs WeightedRandomSampler

  • Random shuffle (shuffle=True): равномерная выборка без замены → батчи отражают исходное распределение классов → редкие классы редко попадают в батч.
  • WeightedRandomSampler: семплирование с вероятностью \propto весу объекта. Для класс-балансировки вес объекта i класса c: w_i = \dfrac{1}{n_c} (обратная частота). Тогда вероятность вытянуть любой класс одинакова → батчи приблизительно сбалансированы. Семплирование с заменой (replacement=True) → minority-объекты повторяются (фактический oversampling). PyTorch WeightedRandomSampler

4.2 Class-balanced batch sampling

Гарантирует фиксированное число объектов на класс в каждом батче (например, P классов × K объектов). Полезно для metric learning и когда нужна детерминированная балансировка батча, а не вероятностная как у WeightedRandomSampler.

4.3 Oversampling / undersampling

  • Oversampling minority (дублирование / random oversampling): риск переобучения на повторённых картинках — смягчается аугментацией.
  • Undersampling majority: теряется информация, но ускоряет эпоху.
  • Библиотека imbalanced-learn (RandomOverSampler, RandomUnderSampler) — для табличных/признаковых данных.

4.4 Почему SMOTE плохо подходит для сырых изображений

SMOTE (Chawla et al., 2002, JAIR) генерирует синтетические объекты линейной интерполяцией между соседями в признаковом пространстве. На сырых пикселях это даёт нефизичные, «призрачные» изображения (наложение/двоение): пиксельное пространство невыпукло и не семантично — линейная смесь двух картинок печатей не является валидной картинкой. Если применять — то в пространстве эмбеддингов предобученной сети, не на пикселях. Для изображений предпочтительнее аугментация и sampler-балансировка.

4.5 Аугментация как «семплирование»

Аугментация расширяет эффективное распределение, заменяя грубое дублирование. Особенно полезна для редких классов как «умный oversampling».

Метод Суть Цель Источник
RandAugment случайный выбор N операций силой M из набора сильная регуляризация, 2 гиперпараметра Cubuk et al., 2019, arXiv:1909.13719
mixup \tilde{x}=\lambda x_i+(1-\lambda)x_j, \tilde{y}=\lambda y_i+(1-\lambda)y_j, \lambda\sim\mathrm{Beta}(\alpha,\alpha) линейная регуляризация, soft-target Zhang et al., 2018, arXiv:1710.09412
CutMix вырезает патч из x_j и вставляет в x_i, метка \propto площади локальные признаки + регуляризация Yun et al., 2019, arXiv:1905.04899

mixup/cutmix дают мягкие метки → совместимы с CE по soft-target и сами по себе улучшают калибровку.

4.6 Стратифицированный split

Train/val/test делятся с сохранением пропорций классов (stratified) — иначе редкий класс может полностью отсутствовать в валидации, и метрика по нему неопределена. sklearn StratifiedShuffleSplit / train_test_split(stratify=y). Для маленьких датасетов — StratifiedKFold.


5. Связка loss × sampler (не дублировать компенсацию)

Дисбаланс компенсируют либо на уровне данных (sampler/oversampling), либо на уровне loss (class weights / CB / focal \alpha) — но не обоими сразу с одинаковой силой, иначе minority-классы переусиливаются: модель переобучается на них, recall редких растёт ценой обвала precision и общего качества.

Комбинация Эффект Рекомендация
shuffle + CE без компенсации baseline
WeightedSampler + CE компенсация в данных OK
shuffle + weighted/CB/focal CE компенсация в loss OK
WeightedSampler + weighted CE двойная компенсация избегать
WeightedSampler + focal ($\alpha$=1) sampler + фокус по трудности приемлемо (разные механизмы)
mild sampler + mild class weights частичная × частичная допустимо при настройке

Влияние на метрики: ребалансировка (любым способом) поднимает macro-F1 / balanced accuracy / recall редких классов, но обычно слегка снижает overall accuracy / micro-F1. Это ожидаемый и приемлемый trade-off при дисбалансе. Решение — какую метрику оптимизировать (см. §2.7).


6. Гиперпараметры для Optuna

Гиперпараметр Тип Optuna Диапазон / значения Комментарий
loss_type categorical {ce, ls_ce, focal, weighted_ce, cb_focal} выбор семейства loss
label_smoothing float [0.0, 0.2] активен для ce/ls_ce
focal_gamma float [0.5, 5.0] активен для focal/cb_focal
focal_alpha float [0.25, 1.0] балансирующий вес
cb_beta categorical / float \{0.99, 0.999, 0.9999\} для class-balanced
use_weighted_sampler bool (categorical) {True, False} sampler-балансировка
mixup_alpha float [0.0, 0.4] (0 = off) \mathrm{Beta}(\alpha,\alpha)
cutmix_alpha float [0.0, 1.0] (0 = off) вероятность через отд. флаг
randaug_N / randaug_M int N\in[1,3], M\in[5,15] сила аугментации

Важно: задать условную логику (Optuna conditional / pruning), чтобы не подбирать одновременно use_weighted_sampler=True и loss_type=weighted_ce (см. §5). Целевая функция (direction='maximize') — macro-F1 или balanced accuracy на валидации. Optuna docs.


7. Практические PyTorch-сниппеты

7.1 WeightedRandomSampler

import numpy as np
import torch
from torch.utils.data import DataLoader, WeightedRandomSampler

labels = np.array(train_labels)                  # shape (N,), class indices
class_counts = np.bincount(labels)               # n_c per class
class_weights = 1.0 / class_counts               # w_c = 1 / n_c (inverse freq)
sample_weights = class_weights[labels]           # per-sample weight

sampler = WeightedRandomSampler(
    weights=torch.as_tensor(sample_weights, dtype=torch.double),
    num_samples=len(sample_weights),             # one epoch length
    replacement=True,                            # required for oversampling
)
# NB: shuffle must be False when a sampler is passed
loader = DataLoader(train_ds, batch_size=64, sampler=sampler)

7.2 Focal Loss (multi-class)

import torch
import torch.nn as nn
import torch.nn.functional as F

class FocalLoss(nn.Module):
    """Multi-class focal loss; expects raw logits."""
    def __init__(self, gamma: float = 2.0, alpha: torch.Tensor | None = None):
        super().__init__()
        self.gamma = gamma
        self.alpha = alpha                       # per-class weights, shape (C,)

    def forward(self, logits: torch.Tensor, target: torch.Tensor) -> torch.Tensor:
        logp = F.log_softmax(logits, dim=1)      # (B, C)
        logp_t = logp.gather(1, target[:, None]).squeeze(1)   # log p_y
        p_t = logp_t.exp()                        # p_y
        loss = -((1 - p_t) ** self.gamma) * logp_t
        if self.alpha is not None:
            loss = self.alpha.to(logits.device)[target] * loss
        return loss.mean()

7.3 Label smoothing в nn.CrossEntropyLoss

import torch.nn as nn

# Built-in since PyTorch 1.10; optional per-class weight for imbalance.
criterion = nn.CrossEntropyLoss(label_smoothing=0.1, weight=class_weight_tensor)
loss = criterion(logits, target)                 # logits: raw (B, C)

7.4 Расчёт macro-F1 / balanced accuracy (sklearn)

from sklearn.metrics import f1_score, balanced_accuracy_score, classification_report

y_pred = logits.argmax(dim=1).cpu().numpy()
y_true = target.cpu().numpy()

macro_f1 = f1_score(y_true, y_pred, average="macro")
bal_acc  = balanced_accuracy_score(y_true, y_pred)        # = macro-recall

# Per-class breakdown (precision / recall / F1 / support):
print(classification_report(y_true, y_pred, digits=4))

Источники