init commit

This commit is contained in:
2026-06-30 15:17:40 +03:00
commit e622129c30
8 changed files with 2588 additions and 0 deletions

View File

@@ -0,0 +1,379 @@
# Задача классификации изображений: метрики, 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](https://scikit-learn.org/stable/modules/model_evaluation.html#precision-recall-f-measure-metrics)
### 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](https://scikit-learn.org/stable/modules/generated/sklearn.metrics.confusion_matrix.html)
### 2.3 Matthews Correlation Coefficient (MCC)
Корреляция Пирсона между предсказанным и истинным классом. Multi-class форма ([sklearn.matthews_corrcoef](https://scikit-learn.org/stable/modules/generated/sklearn.metrics.matthews_corrcoef.html)):
$$
\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](https://scikit-learn.org/stable/modules/generated/sklearn.metrics.cohen_kappa_score.html)
### 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')](https://scikit-learn.org/stable/modules/generated/sklearn.metrics.roc_auc_score.html) · [average_precision_score](https://scikit-learn.org/stable/modules/generated/sklearn.metrics.average_precision_score.html)
### 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?»](https://arxiv.org/abs/1906.02629); Guo et al., 2017, [«On Calibration of Modern Neural Networks»](https://arxiv.org/abs/1706.04599)).
### 2.7 Главная метрика при дисбалансе
При дисбалансе **accuracy вводит в заблуждение**: модель, предсказывающая только majority-класс, даёт высокую accuracy при нулевом recall на редких классах. Главными метриками выбирают **macro-F1** или **balanced accuracy** (= macro-recall, [sklearn.balanced_accuracy_score](https://scikit-learn.org/stable/modules/generated/sklearn.metrics.balanced_accuracy_score.html)), т.к. они усредняют по классам с равным весом. Различие: 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](https://arxiv.org/abs/1512.00567); анализ — Müller et al., 2019, [arXiv:1906.02629](https://arxiv.org/abs/1906.02629)). Не борется напрямую с дисбалансом.
### 3.3 Focal Loss
Снижает вклад «лёгких» (хорошо классифицированных) примеров фактором $(1-p_y)^\gamma$ (Lin et al., 2017, [«Focal Loss for Dense Object Detection» / RetinaNet, arXiv:1708.02002](https://arxiv.org/abs/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](https://arxiv.org/abs/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](https://arxiv.org/abs/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](https://arxiv.org/abs/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](https://pytorch.org/docs/stable/data.html#torch.utils.data.WeightedRandomSampler)
### 4.2 Class-balanced batch sampling
Гарантирует фиксированное число объектов на класс в каждом батче (например, $P$ классов × $K$ объектов). Полезно для metric learning и когда нужна детерминированная балансировка батча, а не вероятностная как у WeightedRandomSampler.
### 4.3 Oversampling / undersampling
- **Oversampling** minority (дублирование / random oversampling): риск **переобучения** на повторённых картинках — смягчается аугментацией.
- **Undersampling** majority: теряется информация, но ускоряет эпоху.
- Библиотека [imbalanced-learn](https://imbalanced-learn.org/stable/) (`RandomOverSampler`, `RandomUnderSampler`) — для табличных/признаковых данных.
### 4.4 Почему SMOTE плохо подходит для сырых изображений
SMOTE (Chawla et al., 2002, [JAIR](https://www.jair.org/index.php/jair/article/view/10302)) генерирует синтетические объекты линейной интерполяцией между соседями в **признаковом пространстве**. На сырых пикселях это даёт нефизичные, «призрачные» изображения (наложение/двоение): пиксельное пространство невыпукло и не семантично — линейная смесь двух картинок печатей не является валидной картинкой. Если применять — то в пространстве эмбеддингов предобученной сети, не на пикселях. Для изображений предпочтительнее **аугментация** и sampler-балансировка.
### 4.5 Аугментация как «семплирование»
Аугментация расширяет эффективное распределение, заменяя грубое дублирование. Особенно полезна для редких классов как «умный oversampling».
| Метод | Суть | Цель | Источник |
|---|---|---|---|
| RandAugment | случайный выбор $N$ операций силой $M$ из набора | сильная регуляризация, 2 гиперпараметра | Cubuk et al., 2019, [arXiv:1909.13719](https://arxiv.org/abs/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](https://arxiv.org/abs/1710.09412) |
| CutMix | вырезает патч из $x_j$ и вставляет в $x_i$, метка $\propto$ площади | локальные признаки + регуляризация | Yun et al., 2019, [arXiv:1905.04899](https://arxiv.org/abs/1905.04899) |
mixup/cutmix дают **мягкие метки** → совместимы с CE по soft-target и сами по себе улучшают калибровку.
### 4.6 Стратифицированный split
Train/val/test делятся с сохранением пропорций классов (stratified) — иначе редкий класс может полностью отсутствовать в валидации, и метрика по нему неопределена. [sklearn StratifiedShuffleSplit / train_test_split(stratify=y)](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.StratifiedShuffleSplit.html). Для маленьких датасетов — **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](https://optuna.readthedocs.io/en/stable/).
---
## 7. Практические PyTorch-сниппеты
### 7.1 WeightedRandomSampler
```python
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)
```python
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
```python
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)
```python
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))
```
---
## Источники
- Lin et al., 2017 — Focal Loss / RetinaNet: [arXiv:1708.02002](https://arxiv.org/abs/1708.02002)
- Cui et al., 2019 — Class-Balanced Loss (effective number): [arXiv:1901.05555](https://arxiv.org/abs/1901.05555)
- Cao et al., 2019 — LDAM: [arXiv:1906.07413](https://arxiv.org/abs/1906.07413)
- Szegedy et al., 2016 — Label Smoothing (Inception-v3): [arXiv:1512.00567](https://arxiv.org/abs/1512.00567)
- Müller et al., 2019 — When Does Label Smoothing Help?: [arXiv:1906.02629](https://arxiv.org/abs/1906.02629)
- Guo et al., 2017 — Calibration of Modern Neural Networks (ECE): [arXiv:1706.04599](https://arxiv.org/abs/1706.04599)
- Hinton et al., 2015 — Knowledge Distillation: [arXiv:1503.02531](https://arxiv.org/abs/1503.02531)
- Zhang et al., 2018 — mixup: [arXiv:1710.09412](https://arxiv.org/abs/1710.09412)
- Yun et al., 2019 — CutMix: [arXiv:1905.04899](https://arxiv.org/abs/1905.04899)
- Cubuk et al., 2019 — RandAugment: [arXiv:1909.13719](https://arxiv.org/abs/1909.13719)
- Chawla et al., 2002 — SMOTE: [JAIR](https://www.jair.org/index.php/jair/article/view/10302)
- scikit-learn — [Model evaluation metrics](https://scikit-learn.org/stable/modules/model_evaluation.html), [balanced_accuracy_score](https://scikit-learn.org/stable/modules/generated/sklearn.metrics.balanced_accuracy_score.html), [matthews_corrcoef](https://scikit-learn.org/stable/modules/generated/sklearn.metrics.matthews_corrcoef.html)
- [imbalanced-learn](https://imbalanced-learn.org/stable/) · [PyTorch data sampling](https://pytorch.org/docs/stable/data.html) · [Optuna](https://optuna.readthedocs.io/en/stable/)

View File

@@ -0,0 +1,281 @@
---
tags: [backbone, edgenext, feature_encoding, transfer_learning, timm, edge]
aliases: [EdgeNeXt, SDTA, visual_feature_encoding]
section: Кодирование визуальных признаков и модель EdgeNeXt
supervisor: мнс Павленко Б.В.
---
# Кодирование визуальных признаков и модель EdgeNeXt
## 1. Кодирование визуальных признаков (visual feature encoding)
**Кодирование визуальных признаков** — это преобразование сырого изображения (тензор пикселей $x \in \mathbb{R}^{H \times W \times 3}$) в компактное численное представление (эмбеддинг, feature vector), пригодное для решения downstream-задачи (классификация, retrieval, сегментация). Цель — отбросить избыточную низкоуровневую информацию (точные значения пикселей, освещение, шум) и сохранить семантически значимые абстракции (формы, текстуры, объекты, в нашем случае — конфигурацию кистей рук в дзюцу-печатях).
### От пикселей к эмбеддингу
Backbone (свёрточная или гибридная сеть) реализует иерархическое отображение:
$$
x \in \mathbb{R}^{H \times W \times 3} \;\xrightarrow{\ f_\theta\ }\; F \in \mathbb{R}^{H' \times W' \times C}
\;\xrightarrow{\ \text{pool}\ }\; z \in \mathbb{R}^{C}
$$
где $f_\theta$ — backbone с параметрами $\theta$, $F$ — карта признаков (feature map) последней стадии (для EdgeNeXt-S: $C = 304$, пространственный размер $H/32 \times W/32 = 8 \times 8$ при входе $256$), а $z$ — глобальный дескриптор после пулинга.
### Карты признаков vs глобальный дескриптор
| Представление | Что это | Размерность (EdgeNeXt-S, вход 256) | Когда нужно |
|---|---|---|---|
| **Feature maps** ($F$) | Пространственно-разрешённые признаки по стадиям | $\{48\times64^2,\ 96\times32^2,\ 160\times16^2,\ 304\times8^2\}$ | Сегментация, детекция, FPN, attention-fusion, CVGL-retrieval по патчам |
| **Global descriptor** ($z$) | Один вектор на изображение (GAP по $H',W'$) | $\mathbb{R}^{304}$ | Классификация, image-level retrieval, метрик-обучение |
Глобальный дескриптор получают **global average pooling**:
$$
z_c = \frac{1}{H' W'} \sum_{i=1}^{H'} \sum_{j=1}^{W'} F_{i,j,c}, \qquad c = 1, \dots, C
$$
Для классификации поверх $z$ ставится линейный классификатор (head): $\hat{y} = \mathrm{softmax}(W z + b)$, $W \in \mathbb{R}^{K \times C}$, $K$ — число классов.
### Роль предобучения (ImageNet) и переноса (transfer learning)
- **Backbone = feature encoder.** Сам по себе backbone не «классифицирует» — он **кодирует** изображение в $z$. Голова (classifier head) уже решает конкретную задачу. Поэтому один и тот же предобученный encoder переиспользуется для разных задач сменой головы (`reset_classifier`).
- **ImageNet-prior.** Веса $\theta$, обученные на ImageNet-1k (1.28M изображений, 1000 классов) или ImageNet-21k (~14M, ~21k классов), кодируют универсальные визуальные примитивы (края, текстуры, части объектов). Эти признаки переносимы на задачи, где у нас мало собственных данных.
- **Transfer learning.** Вместо обучения $f_\theta$ с нуля (требует сотни тысяч изображений) мы инициализируем $\theta$ предобученными весами и до-обучаем (fine-tuning) на целевом датасете. На малом датасете (Naruto Sign) это критично: см. §56.
> Ключевой тезис: **backbone — это feature encoder**. Качество эмбеддинга $z$ определяет потолок точности downstream-задачи, а предобучение задаёт качественную стартовую точку для $\theta$.
Источники: [EdgeNeXt, Maaz et al., ECCV CADL Workshop 2022, arXiv:2206.10589](https://arxiv.org/abs/2206.10589) · [timm feature extraction docs](https://huggingface.co/docs/timm/feature_extraction).
---
## 2. EdgeNeXt: архитектура
**EdgeNeXt** (Maaz, Shaker, Cholakkal, Khan, Zamir, Anwer, Khan — MBZUAI; [arXiv:2206.10589](https://arxiv.org/abs/2206.10589), [github.com/mmaaz60/EdgeNeXt](https://github.com/mmaaz60/EdgeNeXt)) — light-weight гибридная CNN-Transformer сеть, ориентированная на edge-устройства. Идея: брать дешёвую свёрточную базу (ConvNeXt-блоки) и точечно добавлять **линейное по сложности** внимание (SDTA), чтобы получить глобальный рецептивный охват без квадратичной стоимости классического self-attention.
### 2.1. Общая иерархия (4 стадии)
EdgeNeXt следует пирамидальной схеме в стиле ConvNeXt:
```
Input 3×256×256
│ Stem (Conv 4×4, stride 4)
Stage 1 (1/4 res, C1=48) : Conv-encoders ×N1, kernel 3
│ Downsample (Conv 2×2, stride 2)
Stage 2 (1/8 res, C2=96) : Conv-encoders + [PE] + SDTA-encoder, kernel 5
│ Downsample
Stage 3 (1/16 res, C3=160): Conv-encoders + SDTA-encoder, kernel 7
│ Downsample
Stage 4 (1/32 res, C4=304): Conv-encoders + SDTA-encoder, kernel 9
│ LayerNorm → GAP → Linear head
logits ∈ ^K
```
- **Stem:** неперекрывающаяся свёртка $4\times4$, stride 4 → понижение разрешения в 4 раза (patchify).
- **Downsample между стадиями:** LayerNorm + свёртка $2\times2$, stride 2.
- **Адаптивные ядра** Conv-encoder растут по стадиям $\{3,5,7,9\}$: на ранних стадиях (высокое разрешение) — малые ядра (дёшево), на поздних — крупные (для большего рецептивного поля при малом разрешении).
- **Positional Encoding (PE):** добавляется только перед SDTA в **stage 2** (раннее введение позиционной информации; дальше она распространяется по сети) — экономит вычисления.
### 2.2. Conv-encoder (ConvNeXt-блок с адаптивным ядром)
Каждый Conv-encoder — это инвертированный bottleneck в стиле ConvNeXt:
$$
\begin{aligned}
u &= \mathrm{DWConv}_{k\times k}(x) \quad (\text{depth-wise, } k \in \{3,5,7,9\}) \\
u &= \mathrm{LayerNorm}(u) \\
u &= \mathrm{Linear}_{C \to 4C}(u),\quad u = \mathrm{GELU}(u),\quad u = \mathrm{Linear}_{4C \to C}(u) \\
y &= x + \gamma \odot u
\end{aligned}
$$
где $\gamma$ — обучаемый layer-scale, $\odot$ — поканальное умножение. Depth-wise свёртка обеспечивает пространственное смешивание, а $1\times1$-свёртки (Linear) — канальное.
### 2.3. SDTA-encoder (Split Depth-wise Transpose Attention)
Ключевая новация. SDTA состоит из двух частей.
**(а) Multi-scale ветвь (Res2Net-style split-DWConv).** Вход делится по каналам на $s$ подмножеств $\{x_1, \dots, x_s\}$. Каждое (кроме первого) пропускается через depth-wise свёртку, причём вход свёртки складывается с выходом предыдущей подгруппы — иерархический каскад, неявно увеличивающий рецептивное поле и кодирующий **multi-scale** признаки:
$$
y_i =
\begin{cases}
x_i, & i = 1 \\
\mathrm{DWConv}_{3\times3}(x_i), & i = 2 \\
\mathrm{DWConv}_{3\times3}(x_i + y_{i-1}), & 3 \le i \le s
\end{cases}
\qquad
y = \mathrm{Concat}(y_1, \dots, y_s)
$$
**(б) Transposed / cross-covariance attention (XCA).** Вместо классического self-attention по токенам (сложность $O(N^2 d)$, $N=HW$), EdgeNeXt применяет внимание **по каналам** (как в XCiT / Restormer). Карта внимания строится на ковариации каналов $d \times d$, а не токенов $N \times N$, что даёт **линейную сложность $O(N \cdot d^2)$** по числу токенов. Для стабильности $Q$ и $K$ L2-нормируются по токенной оси, а температура $\tau$ — обучаемая:
$$
\mathrm{XCA}(Q,K,V) = V \cdot \mathrm{softmax}\!\left( \frac{\hat{Q}^{\top} \hat{K}}{\tau} \right),
\qquad
\hat{Q} = \frac{Q}{\lVert Q \rVert_2},\ \ \hat{K} = \frac{K}{\lVert K \rVert_2}
$$
где $Q,K,V \in \mathbb{R}^{N \times d}$, а произведение $\hat{Q}^{\top}\hat{K} \in \mathbb{R}^{d \times d}$ — поэтому softmax и attention считаются в канальном (а не токенном) пространстве. Завершается блок LayerNorm + позиционным FFN с residual-связями.
> Итог: SDTA даёт глобальный (по каналам) и multi-scale (по пространству) охват **без квадратичного взрыва** по числу токенов — идеально для высокого разрешения на edge.
Источники: [EdgeNeXt arXiv:2206.10589](https://arxiv.org/abs/2206.10589) · [ar5iv full text](https://ar5iv.labs.arxiv.org/html/2206.10589) · [project page](https://amshaker.github.io/EdgeNeXt/) · XCA-механизм восходит к [XCiT, Ali et al., NeurIPS 2021, arXiv:2106.09681](https://arxiv.org/abs/2106.09681).
---
## 3. Варианты EdgeNeXt и числа
Числа на ImageNet-1k, вход $256\times256$ (если не указано иное), по таблицам статьи и карточкам timm на Hugging Face.
| Вариант | Params | MAdds (GMACs) | Top-1 (%) | Предобучение | Latency (Jetson Nano) |
|---|---|---|---|---|---|
| **EdgeNeXt-XXS** | 1.33M | ~0.26 | 71.2 | IN-1k | ~19 ms |
| **EdgeNeXt-XS** | 2.34M | ~0.54 | 75.0 | IN-1k | ~31 ms |
| **EdgeNeXt-S** | 5.59M | ~1.3 | 79.4 | IN-1k (USI) | ~49 ms |
| **EdgeNeXt-B** | 18.51M | ~3.8 | 82.5 / **83.3** | IN-1k / **IN-21k→1k** | — |
- USI = «Useful Information Set» рецепт обучения (knowledge-distillation), даёт более сильные веса для `edgenext_small.usi_in1k` и `edgenext_base.usi_in1k`.
- Variant-B с предобучением на ImageNet-21k достигает 83.3% Top-1 — лучший вариант по точности, но 18.5M параметров избыточны для очень малого датасета.
**Почему XXS/XS/S подходят для маленьких датасетов и edge:**
1. **Мало параметров → меньше переобучение.** При $N_{\text{train}}$ порядка сотен-тысяч примеров (Naruto Sign — малый датасет) модель на 16M параметров с сильным ImageNet-prior гораздо устойчивее к overfitting, чем ViT-B (86M) или ConvNeXt-B (88M).
2. **Линейная сложность SDTA** делает inference дешёвым на одном GPU/edge-чипе.
3. **Сильный prior при низкой ёмкости** — оптимальный баланс «обобщающая способность / риск переобучения».
Источники: [EdgeNeXt таблицы, arXiv:2206.10589](https://arxiv.org/abs/2206.10589) · [timm/edgenext_xx_small.in1k](https://huggingface.co/timm/edgenext_xx_small.in1k) · [timm/edgenext_small.usi_in1k](https://huggingface.co/timm/edgenext_small.usi_in1k) · [timm/edgenext_base.in21k_ft_in1k](https://huggingface.co/timm/edgenext_base.in21k_ft_in1k).
---
## 4. Практика через `timm`
### 4.1. Актуальные имена моделей (verified, timm `edgenext.py`)
Реестр моделей в [timm/models/edgenext.py](https://github.com/huggingface/pytorch-image-models/blob/main/timm/models/edgenext.py). Имя = `<arch>.<pretrained_tag>`:
| `arch` (имя функции) | Доступные веса (тег) | Params | train / test size |
|---|---|---|---|
| `edgenext_xx_small` | `.in1k` | 1.33M | 256 / 288 |
| `edgenext_x_small` | `.in1k` | 2.34M | 256 / 288 |
| `edgenext_small` | `.usi_in1k` | 5.59M | 256 / 320 |
| `edgenext_base` | `.usi_in1k`, `.in21k_ft_in1k` | 18.51M | 256 / 320 |
| `edgenext_small_rw` | `.sw_in1k` (timm re-implementation) | ~7.8M | 256 / 320 |
> Проверка доступных тегов в коде: `timm.list_models('edgenext*', pretrained=True)`.
### 4.2. Создание модели и адаптация головы
```python
import timm
import torch
NUM_CLASSES = 12 # число классов Naruto Sign (пример)
# Создать с предобученными ImageNet-весами и сразу заменить голову на N классов.
# Голова инициализируется заново под NUM_CLASSES, encoder = pretrained.
model = timm.create_model(
'edgenext_small.usi_in1k',
pretrained=True,
num_classes=NUM_CLASSES,
)
# Эквивалентно: создать с дефолтной головой и заменить через API.
model = timm.create_model('edgenext_xx_small.in1k', pretrained=True)
model.reset_classifier(num_classes=NUM_CLASSES) # пересоздать только классификатор
```
### 4.3. Извлечение признаков (encoder-режим)
```python
# (а) Глобальный дескриптор z (pre-logits embedding):
model = timm.create_model('edgenext_small.usi_in1k', pretrained=True, num_classes=0)
x = torch.randn(1, 3, 256, 256)
feat_maps = model.forward_features(x) # карта признаков последней стадии
z = model.forward_head(feat_maps, pre_logits=True) # эмбеддинг z ∈ ^C (после GAP+norm)
print(z.shape, model.num_features) # torch.Size([1, 304]) 304
# (б) Многоуровневые карты признаков (для fusion / FPN / сегментации):
fmodel = timm.create_model('edgenext_small.usi_in1k', pretrained=True, features_only=True)
feats = fmodel(x) # список из 4 тензоров по стадиям
print([f.shape for f in feats])
# [1,48,64,64], [1,96,32,32], [1,160,16,16], [1,304,8,8]
print(fmodel.feature_info.channels()) # [48, 96, 160, 304]
```
- `model.num_features` — размерность эмбеддинга $C$ (для `edgenext_small` = 304).
- `forward_head(..., pre_logits=True)` возвращает $z$ **до** линейного классификатора (то, что нужно для retrieval / метрик-обучения / переноса).
### 4.4. Дефолтный input size и data config
```python
# Корректные нормализация/resize/crop берутся из конфигурации весов — НЕ хардкодить.
data_config = timm.data.resolve_model_data_config(model)
print(data_config)
# {'input_size': (3, 256, 256), 'crop_pct': 0.9, 'crop_mode': 'center',
# 'mean': (0.485, 0.456, 0.406), 'std': (0.229, 0.224, 0.225), ...}
train_tf = timm.data.create_transform(**data_config, is_training=True)
eval_tf = timm.data.create_transform(**data_config, is_training=False)
```
> `resolve_model_data_config(model)` — актуальный API (вместо устаревшего `resolve_data_config(model.pretrained_cfg)`). Дефолтный train-вход EdgeNeXt — $256\times256$; test-разрешение выше (288/320), но для собственного датасета обычно дообучают на 256.
Источники: [timm/edgenext_small.usi_in1k card](https://huggingface.co/timm/edgenext_small.usi_in1k) · [timm feature extraction](https://huggingface.co/docs/timm/feature_extraction) · [timm models source](https://github.com/huggingface/pytorch-image-models/blob/main/timm/models/edgenext.py).
---
## 5. Почему EdgeNeXt — хороший выбор для классификации на МАЛЕНЬКОМ датасете (Naruto Sign)
| Фактор | EdgeNeXt-XXS/XS/S | Большие сети (ViT-B/ConvNeXt-B) |
|---|---|---|
| Параметры | 1.35.6M | 8688M |
| Риск переобучения на малых данных | **низкий** | высокий |
| ImageNet-prior | сильный (IN-1k/USI/21k) | сильный |
| Стоимость fine-tune на RTX 4090 | минуты, большой batch | дольше, меньший batch |
| Inference latency | edge-уровень (1949 ms Jetson Nano) | неприемлемо для edge |
Тезисно:
1. **Мало параметров → меньше переобучение.** Naruto Sign — малый датасет; модель на 16M параметров не «запоминает» обучающую выборку так агрессивно, как сеть на 80M+. Соотношение ёмкость/данные сбалансировано.
2. **Сильный ImageNet-prior.** Признаки краёв/текстур/частей объектов уже выучены — для распознавания конфигураций кистей рук (печатей) это переносится напрямую, дообучение быстро сходится.
3. **Быстро на одной RTX 4090.** Малая модель → большой batch (256512), полный fine-tune за минуты, что позволяет провести **HPO** (hyperparameter optimization, тема методички у Павленко Б.В.) — десятки прогонов lr/wd/augment в разумное время.
4. **Edge-готовность.** Если конечная цель — деплой (камера/телефон/Jetson), EdgeNeXt уже спроектирован под latency-бюджет.
---
## 6. Мостик к freezing / частичной заморозке / адаптерам
На малых данных полный fine-tune всех $\theta$ может переобучить encoder и «стереть» полезный ImageNet-prior. Стандартные стратегии переноса (от более «бережных» к более «агрессивным»):
| Стратегия | Что обучается | Когда применять | Параметров обучается |
|---|---|---|---|
| **Linear probing** (full freeze) | только голова `reset_classifier` | очень мало данных, prior близок к домену | $\sim C \cdot K$ |
| **Partial freeze** | голова + последняя(-ие) стадия(-и); ранние стадии заморожены | малый датасет, нужна частичная адаптация признаков | стадии 34 |
| **Adapters / PEFT (MONA, LoRA, FiLM/SSF)** | вставленные лёгкие модули; backbone заморожен | малые данные + хочется адаптации без переобучения | <5% весов |
| **Full fine-tune** | все $\theta$ + голова | данных достаточно, домен далёк от ImageNet | 100% |
- **Заморозка (freezing):** `for p in model.parameters(): p.requires_grad = False`, затем разморозить голову (и при partial — поздние стадии). Снижает число обучаемых параметров → меньше overfitting и быстрее обучение.
- **Адаптеры (MONA — Multi-cognitive visual adapter, и др.):** в замороженный backbone вставляются обучаемые мини-модули, адаптирующие признаки под целевой домен почти без роста параметров. Это лучший компромисс «адаптация vs переобучение» именно для малых датасетов.
> Детали — в разделах методички про **freezing** и **MONA** (PEFT-адаптеры). Для EdgeNeXt на Naruto Sign разумный старт: full fine-tune с сильной аугментацией + early stopping; при признаках переобучения → partial freeze (заморозить stage 12) или адаптеры.
Источники по PEFT-контексту проекта: SSF — [Lian et al., NeurIPS 2022 / TPAMI](https://arxiv.org/abs/2210.08823) · FiLM — [Perez et al., AAAI 2018, arXiv:1709.07871](https://arxiv.org/abs/1709.07871) · LoRA — [Hu et al., ICLR 2022, arXiv:2106.09685](https://arxiv.org/abs/2106.09685).
---
### Сводка источников
- [EdgeNeXt, Maaz et al., ECCV CADL Workshop 2022 — arXiv:2206.10589](https://arxiv.org/abs/2206.10589)
- [Официальный репозиторий — github.com/mmaaz60/EdgeNeXt](https://github.com/mmaaz60/EdgeNeXt)
- [Project page — amshaker.github.io/EdgeNeXt](https://amshaker.github.io/EdgeNeXt/)
- [timm EdgeNeXt source](https://github.com/huggingface/pytorch-image-models/blob/main/timm/models/edgenext.py)
- [timm/edgenext_small.usi_in1k](https://huggingface.co/timm/edgenext_small.usi_in1k) · [timm/edgenext_xx_small.in1k](https://huggingface.co/timm/edgenext_xx_small.in1k) · [timm/edgenext_base.in21k_ft_in1k](https://huggingface.co/timm/edgenext_base.in21k_ft_in1k)
- [timm feature extraction docs](https://huggingface.co/docs/timm/feature_extraction)
- [XCiT (cross-covariance attention), Ali et al., NeurIPS 2021 — arXiv:2106.09681](https://arxiv.org/abs/2106.09681)

View File

@@ -0,0 +1,268 @@
# Заморозка, частичная заморозка и стратегии fine-tuning при transfer learning
> Раздел методички. Научный руководитель — мнс Павленко Б.В.
> Контекст: подбор глубины заморозки backbone (EdgeNeXt) как гиперпараметра HPO.
## 1. Три режима адаптации: full FT / feature extraction / partial freeze
При переносе предобученной сети (backbone $f_\theta$ + голова $g_\phi$) на целевую задачу существует континуум стратегий — от «обучаем только голову» до «обучаем всё». Граничные и промежуточные точки:
| Режим | Что обучается | Trainable params | Риск переобучения | Сохранение pretrained features |
|-------|---------------|------------------|-------------------|-------------------------------|
| **Linear probing** (feature extraction) | только голова $g_\phi$; backbone заморожен | минимум (доли %) | низкий | максимальное (backbone не трогаем) |
| **Partial freeze** | голова + последние $k$ стадий backbone | средний | средний | частичное (нижние слои фиксированы) |
| **Full fine-tuning** | все параметры $\theta, \phi$ | 100% | высокий (на малых данных) | минимальное (всё дрейфует) |
| **LP-FT** (linear probe → fine-tune) | сначала голова, затем всё с малым lr | 100% (на 2-й фазе) | средний | высокое в ID, существенно лучше OOD |
**Ключевой нюанс — full FT не всегда лучше.** В работе [Kumar et al., ICLR 2022 «Fine-Tuning can Distort Pretrained Features…»](https://arxiv.org/abs/2202.10054) показано: при хороших предобученных признаках и большом сдвиге домена полный fine-tuning **искажает** нижние слои (feature distortion), пока голова ещё «недоучена» с случайной инициализации. На 10 датасетах с distribution shift full FT давал в среднем +2% in-distribution (ID), но 7% out-of-distribution (OOD) против linear probing. Рекомендованный фикс — **LP-FT**: сначала linear probing (голова сходится к разумному решению), затем full FT с малым lr; даёт +1% ID и +10% OOD относительно чистого full FT.
### Что когда выбирать: размер данных × близость домена
Классическое правило (CS231n Transfer Learning, [cs231n.github.io/transfer-learning](https://cs231n.github.io/transfer-learning/)) расширенное с учётом Kumar et al.:
| Размер датасета \ Домен | **Близкий** к источнику (ImageNet-подобный) | **Далёкий** от источника (спутник/UAV/медицина) |
|--------------------------|---------------------------------------------|-------------------------------------------------|
| **Малый** (сотни–тысячи) | Linear probing / разморозить 1 последнюю стадию. Full FT переобучится. | Опасная зона. Partial freeze (12 верхние стадии) + сильная регуляризация/аугментации. LP-FT. |
| **Средний** (10²10⁴) | Partial freeze (последние 12 стадии) с discriminative lr | Partial freeze (23 стадии) либо LP-FT; следить за OOD-метрикой |
| **Большой** (10⁵+) | Full FT (можно с layer-wise lr decay) | Full FT — данных хватает, чтобы переучить нижние слои без распада признаков |
Интуиция: чем **меньше данных** — тем больше слоёв замораживаем (меньше степеней свободы → меньше переобучение); чем **дальше домен** — тем глубже придётся разморозить (нижние/средние фичи источника плохо переносятся), но компенсируем регуляризацией. Граничные случаи лучше решать через HPO глубины заморозки (§5).
## 2. Механика заморозки во фреймворке (PyTorch)
### `requires_grad = False`
Заморозка параметра = отключение накопления градиента:
```python
for p in model.backbone.parameters():
p.requires_grad = False # градиент не считается, .grad остаётся None
```
При этом autograd не строит обратный путь к этим тензорам — экономия памяти и времени backward.
### Исключение замороженных параметров из optimizer
Optimizer должен получать **только обучаемые** параметры. Иначе для замороженных весов всё равно могут применяться weight decay и моментум-обновления (особенно у AdamW: decoupled weight decay двигает веса даже при нулевом градиенте), что тихо ломает «заморозку».
```python
optimizer = torch.optim.AdamW(
(p for p in model.parameters() if p.requires_grad), # ТОЛЬКО trainable
lr=1e-3, weight_decay=1e-2,
)
```
### `model.eval()` vs `model.train()` для замороженных модулей
`requires_grad=False` управляет **градиентом весов**, но НЕ управляет режимом модулей с состоянием — BatchNorm и Dropout. Их поведение определяется флагом `.training` (`model.train()` / `model.eval()`):
| Модуль | `train()` | `eval()` |
|--------|-----------|----------|
| **BatchNorm** | нормирует по статистике **текущего батча**; обновляет `running_mean/running_var` (EMA) | нормирует по **накопленной** `running_mean/var`; статистику НЕ обновляет |
| **LayerNorm** | по статистике текущего сэмпла (нет running stats) | то же — режим не меняет поведение |
| **Dropout** | активен (зануляет случайные активации) | отключён (identity) |
### Почему важно не обновлять BN-статистику замороженных слоёв
Это самая частая и коварная ошибка. Если backbone заморожен через `requires_grad=False`, но модель оставлена в `model.train()`, то **веса BN ($\gamma, \beta$) не обучаются, НО `running_mean`/`running_var` продолжают дрейфовать** под целевой батч. Это:
1. меняет распределение активаций, на котором обучалась голова → «заморозка» лишь частичная и нестабильная;
2. на малых батчах целевого домена статистика шумная → деградация;
3. рассинхрон train/eval: на инференсе используется уже «уехавшая» running-статистика.
Из обсуждений [PyTorch Forums «Should I use model.eval() when I freeze BatchNorm»](https://discuss.pytorch.org/t/should-i-use-model-eval-when-i-freeze-batchnorm-layers-to-finetune/39495) и [«Frozen BN still differs train vs eval»](https://discuss.pytorch.org/t/frozen-batch-normalization-still-shows-different-performance-in-model-train-from-model-eval/25176): чтобы по-настоящему заморозить BN, недостаточно `requires_grad=False` — нужно перевести BN-модули в `.eval()`. Это и есть концепция **FrozenBN** (constant affine transform с фиксированной population-статистикой), широко применяемая в detection (Detectron2, mmdetection) при переносе backbone.
Замечание: [«Rethinking Batch in BatchNorm», Wu & Johnson, 2021](https://arxiv.org/pdf/2105.07576) показывает обратное для **полного** fine-tuning при достатке данных — там НЕ замораживать BN-статистику обычно лучше. То есть правило «BN в eval» относится именно к **замороженным** слоям, а не ко всем сценариям.
> LayerNorm-сети (ViT, ConvNeXt, EdgeNeXt) этой ловушки лишены: LN не хранит running stats, его поведение одинаково в train/eval. Поэтому для EdgeNeXt главный риск BN отсутствует — но если в гибридной голове/стеме встречается BN, правило выше остаётся в силе.
## 3. Постепенная разморозка и layer-wise learning rates
Эти приёмы введены в [ULMFiT, Howard & Ruder, ACL 2018](https://aclanthology.org/P18-1031.pdf) и стали стандартом transfer learning.
### Gradual / progressive unfreezing
Размораживать не всё сразу, а по одной стадии, начиная с **верхних** (ближе к голове) слоёв. Нижние слои дольше остаются замороженными, что предотвращает **catastrophic forgetting** общих признаков. Типичная схема по эпохам:
```
эпоха 02: обучаем только голову (backbone frozen)
эпоха 35: + последняя стадия backbone
эпоха 6+ : + предпоследняя стадия ...
```
### Discriminative / layer-wise learning rates
Разные слои несут разную информацию: нижние — общие низкоуровневые фичи (их менять рискованно), верхние — task-specific. Поэтому lr **убывает** от головы к стему. Геометрическое затухание (layer-wise LR decay, LLRD):
$$
\eta_\ell = \eta_{\text{base}} \cdot \xi^{\,(L-\ell)}, \qquad \xi \in (0, 1]
$$
где $\ell$ — индекс слоя/стадии (0 — стем, $L$ — голова), $\xi$ — decay factor (типично $0.6$$0.9$). Голова получает $\eta_{\text{base}}$, стем — наименьший lr. В ULMFiT правило проще: $\eta_{\ell-1} = \eta_\ell / 2.6$. LLRD — стандарт для тонкой настройки BERT/ViT.
### Warm-up головы перед разморозкой
Случайно инициализированная голова в начале даёт большие, «шумные» градиенты, которые при немедленном full FT распространяются вниз и **искажают** pretrained-фичи (тот самый механизм из Kumar et al.). Поэтому:
1. сначала **warm-up головы** при замороженном backbone (= фаза linear probing в LP-FT);
2. затем разморозка нижних слоёв с малым lr.
Это совмещает gradual unfreezing, LP-FT и discriminative lr в единую практику.
## 4. Связь с PEFT — «средний путь» между заморозкой и full FT
PEFT (Parameter-Efficient Fine-Tuning) замораживает backbone полностью, но добавляет/обучает крошечное число параметров — компромисс между жёсткой заморозкой (мало ёмкости адаптации) и full FT (переобучение + дорого). Обзор: [«Scaling Down to Scale Up», Lialin et al., 2023](https://arxiv.org/pdf/2303.15647).
| Метод | Что обучается | Доля параметров | Ссылка |
|-------|---------------|------------------|--------|
| **BitFit** | только bias-термы (+ голова) | ~0.050.1% | [Ben-Zaken et al., 2021](https://huggingface.co/papers/2106.10199) |
| **Adapters** | вставные bottleneck-MLP внутри блоков | ~0.55% | [Houlsby et al., ICML 2019](https://arxiv.org/abs/1902.00751) |
| **LoRA** | низкоранговые $\Delta W = BA$, добавляемые к замороженным матрицам | ≪1% (до 10000× меньше) | [Hu et al., 2021](https://openreview.net/pdf?id=nZeVKeeFYf9) |
| **VPT** | обучаемые prompt-токены на входе ViT, backbone заморожен | <1% | [Jia et al., ECCV 2022](https://arxiv.org/pdf/2203.12119) |
| **Mona** | vision-friendly adapter (мульти-масштабные фильтры + scaled norm) | <5% | [Yin et al., CVPR 2025](https://arxiv.org/abs/2408.08345) |
Замечания:
- **LoRA**: $W_{\text{eff}} = W_0 + \frac{\alpha}{r} B A$, где $W_0$ заморожена, $A\in\mathbb{R}^{r\times d}, B\in\mathbb{R}^{d\times r}$, ранг $r \ll d$. На инференсе $\Delta W$ сливается в $W$ — нулевая задержка.
- **BitFit** конкурентоспособен с full FT на малых/средних данных, но проседает на моделях >1B и больших датасетах.
- **Mona** — единственный delta-tuning метод, превзошедший full FT на **всех** рассмотренных visual-задачах (классификация, детекция, сегментация), обучая <5% параметров: ключ — vision-friendly фильтры вместо «языковых» линейных и scaled normalization внутри адаптера. Для CV-backbone (как EdgeNeXt) это наиболее релевантный PEFT.
Связь с темой раздела: PEFT — это «заморозка backbone + малая обучаемая надстройка». Глубина заморозки (§5) и выбор PEFT-модуля можно рассматривать в одном пространстве HPO как разные точки на оси «ёмкость адаптации vs риск переобучения».
## 5. Глубина заморозки как гиперпараметр
Идея: **число размороженных стадий** `n_unfrozen ∈ {0,1,2,3,4}` (для 4-стадийного backbone типа EdgeNeXt/ConvNeXt) — это категориальный/ординальный гиперпараметр, подбираемый наравне с lr, weight decay, аугментациями.
Способы параметризации:
| Параметр | Тип | Диапазон | Смысл |
|----------|-----|----------|-------|
| `n_unfrozen` | int (ordinal) | 0..4 | сколько верхних стадий разморожено (0 = linear probing, 4 = full FT) |
| `llrd_decay` $\xi$ | float | 0.5..0.95 | затухание lr вглубь |
| `head_lr` | log-uniform | 1e-4..1e-2 | lr головы |
| `backbone_lr_mult` | log-uniform | 1e-3..1.0 | множитель lr backbone относительно головы |
| `freeze_bn` | bool | {T,F} | переводить ли BN в eval (если BN присутствует) |
| `warmup_head_epochs` | int | 0..5 | фаза LP перед разморозкой |
Подбор через [Optuna](https://optuna.readthedocs.io/) — каждая стадия как отдельная lr-группа:
```python
import optuna
def objective(trial):
n_unfrozen = trial.suggest_int("n_unfrozen", 0, 4)
head_lr = trial.suggest_float("head_lr", 1e-4, 1e-2, log=True)
bb_mult = trial.suggest_float("backbone_lr_mult", 1e-3, 1.0, log=True)
xi = trial.suggest_float("llrd_decay", 0.5, 0.95)
freeze_bn = trial.suggest_categorical("freeze_bn", [True, False])
model = build_model()
set_trainable_stages(model, n_unfrozen, freeze_bn=freeze_bn)
pgroups = build_param_groups(model, head_lr, bb_mult, xi)
opt = torch.optim.AdamW(pgroups, weight_decay=1e-2)
val_metric = train_and_eval(model, opt) # вернуть метрику на валидации (OOD!)
return val_metric
study = optuna.create_study(direction="maximize")
study.optimize(objective, n_trials=40)
```
Рекомендация: **валидационную метрику для HPO считать на домен-сдвинутой выборке** (OOD-подобной), иначе HPO выберет full FT по ID-метрике и проиграет на проде (урок Kumar et al.). Для ускорения — `optuna.pruners.MedianPruner` + чекпойнт предобученного backbone.
## 6. Практический PyTorch-скелет
```python
import torch
import torch.nn as nn
def set_trainable_stages(model, n_unfrozen: int, freeze_bn: bool = True):
"""Размораживает последние n_unfrozen стадий backbone + всю голову.
n_unfrozen=0 -> linear probing; =len(stages) -> full FT.
Предполагается backbone.stages = [stage0(стем-ближе) ... stageN(голова-ближе)].
"""
stages = list(model.backbone.stages)
n_frozen = len(stages) - n_unfrozen
# 1) backbone: замораживаем нижние стадии, размораживаем верхние
for i, stage in enumerate(stages):
requires = i >= n_frozen
for p in stage.parameters():
p.requires_grad = requires
# 2) голова всегда обучается
for p in model.head.parameters():
p.requires_grad = True
# 3) корректная заморозка BatchNorm в ЗАМОРОЖЕННЫХ стадиях
if freeze_bn:
for i, stage in enumerate(stages):
if i < n_frozen: # только замороженные стадии
for m in stage.modules():
if isinstance(m, (nn.BatchNorm1d, nn.BatchNorm2d,
nn.BatchNorm3d, nn.SyncBatchNorm)):
m.eval() # не обновлять running stats
m.momentum = 0 # доп. страховка (на случай train())
# m.weight/bias уже requires_grad=False из шага 1
return model
def build_param_groups(model, head_lr: float, bb_mult: float, xi: float):
"""Discriminative LR: голова = head_lr, стадии backbone = head_lr*bb_mult*xi^depth."""
groups = [{"params": [p for p in model.head.parameters()
if p.requires_grad], "lr": head_lr}]
stages = list(model.backbone.stages)
L = len(stages)
for i, stage in enumerate(stages):
params = [p for p in stage.parameters() if p.requires_grad]
if not params: # стадия заморожена -> пропускаем
continue
depth = L - i # чем глубже (ближе к стему) — тем меньше lr
lr_i = head_lr * bb_mult * (xi ** depth)
groups.append({"params": params, "lr": lr_i})
return groups
# Важно: при каждом model.train() BN замороженных стадий снова уходит в train.
# Поэтому после .train() надо повторно фиксировать BN:
def freeze_bn_eval(module):
for m in module.modules():
if isinstance(m, (nn.BatchNorm1d, nn.BatchNorm2d, nn.BatchNorm3d)):
m.eval()
# использование в train-цикле:
# model.train()
# freeze_bn_eval(model.backbone.stages[:n_frozen]) # вернуть FrozenBN
```
Тонкость, которую часто упускают: `model.train()` в начале каждой эпохи **рекурсивно** переводит все подмодули (включая «замороженные» BN) обратно в train-режим. Поэтому FrozenBN нужно восстанавливать **после** каждого `model.train()`, либо переопределить `model.train()` так, чтобы он оставлял замороженные BN в eval (паттерн `train()`-override в Detectron2).
## 7. Типичные ошибки
| Ошибка | Симптом | Как избежать |
|--------|---------|--------------|
| Забыли `eval()` для BN в замороженном backbone | метрика «плывёт», train/eval расходятся, нестабильность на малых батчах | `set_trainable_stages(..., freeze_bn=True)` + повторная фиксация после `model.train()` |
| Замороженные веса всё равно обновляются | веса меняются несмотря на `requires_grad=False` | передавать в optimizer **только** `p.requires_grad`; помнить про decoupled weight decay в AdamW |
| `model.train()` сбрасывает FrozenBN | BN снова обновляет running stats каждую эпоху | override `train()` или вызывать `freeze_bn_eval()` после каждого `.train()` |
| Один lr на всё | нижние слои либо «недоучены», либо распадаются | discriminative / layer-wise LR decay (§3) |
| Full FT на малых данных | высокая train-, низкая val/OOD-метрика; feature distortion | linear probing / partial freeze; LP-FT ([Kumar et al., 2022](https://arxiv.org/abs/2202.10054)) |
| Разморозили всё сразу со случайной головой | большие шумные градиенты искажают pretrained-фичи | warm-up головы (LP) → gradual unfreezing |
| HPO по ID-метрике | на проде/OOD хуже, чем простой linear probe | валидация на домен-сдвинутой выборке |
| Dropout/BN активны в feature extraction без необходимости | несогласованность с инференсом | осознанно выставлять режим модулей, а не только `requires_grad` |
## Источники
- [Kumar, Raghunathan, Jones, Ma, Liang. Fine-Tuning can Distort Pretrained Features and Underperform Out-of-Distribution. ICLR 2022](https://arxiv.org/abs/2202.10054)
- [Howard, Ruder. Universal Language Model Fine-tuning for Text Classification (ULMFiT). ACL 2018](https://aclanthology.org/P18-1031.pdf)
- [Ben-Zaken, Goldberg, Ravfogel. BitFit: Simple Parameter-efficient Fine-tuning. 2021](https://huggingface.co/papers/2106.10199)
- [Houlsby et al. Parameter-Efficient Transfer Learning for NLP (Adapters). ICML 2019](https://arxiv.org/abs/1902.00751)
- [Hu et al. LoRA: Low-Rank Adaptation of Large Language Models. 2021](https://openreview.net/pdf?id=nZeVKeeFYf9)
- [Jia et al. Visual Prompt Tuning (VPT). ECCV 2022](https://arxiv.org/pdf/2203.12119)
- [Yin et al. 5%>100%: Breaking Performance Shackles of Full Fine-Tuning on Visual Recognition Tasks (Mona). CVPR 2025](https://arxiv.org/abs/2408.08345)
- [Lialin, Deshpande, Rumshisky. Scaling Down to Scale Up: A Guide to Parameter-Efficient Fine-Tuning. 2023](https://arxiv.org/pdf/2303.15647)
- [Wu, Johnson. Rethinking "Batch" in BatchNorm. 2021](https://arxiv.org/pdf/2105.07576)
- [PyTorch Forums: Should I use model.eval() when I freeze BatchNorm layers to finetune?](https://discuss.pytorch.org/t/should-i-use-model-eval-when-i-freeze-batchnorm-layers-to-finetune/39495)
- [PyTorch Forums: Frozen BatchNorm still differs train vs eval](https://discuss.pytorch.org/t/frozen-batch-normalization-still-shows-different-performance-in-model-train-from-model-eval/25176)
- [CS231n: Transfer Learning](https://cs231n.github.io/transfer-learning/)
- [Optuna documentation](https://optuna.readthedocs.io/)

View File

@@ -0,0 +1,317 @@
---
title: "Методы оптимизации и подбора гиперпараметров — таксономия, теория, источники"
tags: [HPO, hyperparameter-optimization, methodology, automl]
supervisor: "мнс Павленко Б.В."
status: draft
---
# Методы оптимизации / подбора гиперпараметров
> Раздел учебно-исследовательской методички. Цель — дать самодостаточную таксономию методов HPO (Hyper-Parameter Optimization), их теоретические основы, формулы, сравнительные таблицы и практическую методологию для DL-классификации.
## 1. Гиперпараметры vs параметры. Пространство поиска, бюджет, цель
**Параметры (parameters)** — переменные модели, которые настраиваются оптимизатором *в ходе обучения* напрямую минимизацией функции потерь (веса $W$, смещения $b$ свёрток и линейных слоёв, $\gamma,\beta$ в BatchNorm). Их градиент $\partial \mathcal{L}/\partial W$ вычислим аналитически (backprop).
**Гиперпараметры (hyper-parameters)** — переменные, которые задаются *до/над* обучением и управляют либо самим процессом обучения (learning rate, weight decay, batch size, оптимизатор, расписание), либо ёмкостью и структурой модели (глубина, ширина, dropout, label smoothing), либо данными (аугментации). Градиент целевой метрики по ним обычно недоступен в замкнутой форме — отсюда отдельная дисциплина HPO.
Формально HPO — это задача оптимизации «чёрного ящика»:
$$
\lambda^{\*} = \arg\min_{\lambda \in \Lambda}\; \mathcal{L}_{\text{val}}\big(A_\lambda(D_{\text{train}}),\, D_{\text{val}}\big),
$$
где $\lambda$ — вектор гиперпараметров из пространства поиска $\Lambda$; $A_\lambda$ — алгоритм обучения, обученный с этой конфигурацией; $\mathcal{L}_{\text{val}}$ — целевая функция (валидационный loss или $1-\text{accuracy}$, $1-\text{R@1}$ и т. д.). Функция $\mathcal{L}_{\text{val}}(\lambda)$ — **дорогая** (один replay = полное обучение), **зашумлённая** (random seed, недетерминизм GPU), **недифференцируемая** по $\lambda$ и часто **немонотонная**.
### Типы переменных в пространстве поиска $\Lambda$
| Тип | Пример | Шкала | Примечание |
|-----|--------|-------|------------|
| Непрерывный (continuous / real) | `lr` $\in[10^{-5},10^{-1}]$, `weight_decay`, `dropout` | log / linear | для `lr`, `wd`**логарифмическая** |
| Целочисленный (integer / ordinal) | `batch_size`, число эпох заморозки, ширина слоя | log2 / linear | порядок имеет смысл |
| Категориальный (categorical / nominal) | `optimizer` ∈ {SGD, AdamW, Lion}, тип scheduler | — | порядка нет |
| Условный / иерархический (conditional) | `momentum` существует только если `optimizer=SGD`; `betas` — только для Adam | — | дерево зависимостей → tree-structured space |
**Условность** (conditional space) — ключевая черта реальных задач: подпространство активно только при определённом значении родителя. Метод HPO должен это моделировать (TPE, SMAC — могут; чистый GP — хуже).
**Бюджет (budget)** — ограничение ресурсов: число trial'ов $N$, суммарное GPU-время, либо «fidelity» (число эпох / доля данных / разрешение). Цель методологии — найти хороший $\lambda$ при **фиксированном бюджете**, а не глобальный оптимум любой ценой.
**Цель (objective)** — скаляр (single-objective) или вектор (multi-objective: accuracy ↔ latency/FLOPs — критично для edge-моделей вроде EdgeNeXt на Jetson). При multi-objective говорят о Pareto-фронте.
---
## 2. Базовые методы: Manual, Grid Search, Random Search
### 2.1 Manual / Babysitting («graduate student descent»)
Эксперт вручную меняет один-два гиперпараметра, смотрит на кривые обучения, корректирует. Плюсы: дёшево на старте, накапливает интуицию. Минусы: невоспроизводимо, не масштабируется, систематический bias, не параллелится.
### 2.2 Grid Search
Декартово произведение заранее заданных значений по каждой оси. Для $d$ осей по $k$ значений — $k^d$ конфигураций (комбинаторный взрыв). Полностью параллелизуем, прост, воспроизводим. Главная беда — **проклятие размерности** и неэффективное покрытие при наличии «неважных» осей.
### 2.3 Random Search
Каждая конфигурация сэмплируется независимо из заданных распределений по осям ($\log$-uniform для `lr`/`wd`, uniform/categorical для остальных).
**Ключевой источник:** [Bergstra & Bengio, 2012, «Random Search for Hyper-Parameter Optimization», JMLR 13:281305](https://jmlr.org/papers/v13/bergstra12a.html).
**Почему random эффективнее grid в высокой размерности.** На практике целевая функция имеет **низкую эффективную размерность** (low effective dimensionality): из $d$ гиперпараметров реально важны лишь $d_{\text{eff}} \ll d$. Grid тратит бюджет на повторную дискретизацию неважных осей: при $k^d$ точках по каждой *важной* оси пробуется только $k$ различных значений. Random Search при $N$ trial'ах пробует $N$ **различных** значений по *каждой* оси (с вероятностью 1), то есть гораздо плотнее покрывает проекцию на важные оси.
Интуиция «вероятности промаха»: если хорошая область занимает долю $v$ объёма $\Lambda$, то вероятность хотя бы одного попадания за $N$ случайных trial'ов:
$$
P(\text{hit}) = 1 - (1 - v)^N .
$$
Чтобы получить $P \ge 1-\epsilon$, нужно $N \ge \dfrac{\log \epsilon}{\log(1-v)}$ — это **не зависит от размерности** $d$, в отличие от grid. Bergstra & Bengio эмпирически показали, что random search находит модели не хуже, чем тщательный grid+manual search из предыдущих работ, за малую долю вычислений.
```python
# Минимальный Random Search в лог-шкале для lr/wd (PyTorch-агностично)
import numpy as np
def sample_config(rng):
return {
"lr": 10 ** rng.uniform(-4, -1), # log-uniform
"weight_decay": 10 ** rng.uniform(-6, -2), # log-uniform
"batch_size": int(rng.choice([64, 128, 256])),
"optimizer": rng.choice(["sgd", "adamw"]),
}
rng = np.random.default_rng(seed=42) # фиксируем seed → воспроизводимость
configs = [sample_config(rng) for _ in range(64)] # бюджет N=64 trial'ов
```
---
## 3. Bayesian Optimization (BO)
Идея: построить дешёвую **суррогатную модель** (surrogate) $p(\mathcal{L}\mid\lambda)$ по уже наблюдённым $(\lambda_i,\mathcal{L}_i)$ и выбирать следующую точку, максимизируя **acquisition function** — это Sequential Model-Based Optimization (SMBO). BO выборочно-эффективна (sample-efficient), но **последовательна** по природе (плохо параллелится без модификаций) и хуже масштабируется по $d$.
### 3.1 Суррогатные модели
**Gaussian Process (GP).** Апостериор по функции — гауссов: $\mathcal{L}(\lambda)\sim \mathcal{GP}(\mu(\lambda),k(\lambda,\lambda'))$. Даёт замкнутые предсказательные среднее $\mu(\lambda)$ и дисперсию $\sigma^2(\lambda)$. Минусы: $O(n^3)$ по числу наблюдений, плохо с категориальными/условными осями и высокой размерностью.
**TPE (Tree-structured Parzen Estimator).** Источник: [Bergstra, Bardenet, Bengio, Kégl, 2011, «Algorithms for Hyper-Parameter Optimization», NeurIPS 24:25462554](https://www.researchgate.net/publication/216816964_Algorithms_for_Hyper-Parameter_Optimization). Вместо $p(y\mid\lambda)$ моделируется $p(\lambda\mid y)$ двумя плотностями, разделяя trial'ы порогом $y^{\*}$ (квантиль):
$$
p(\lambda \mid y) =
\begin{cases}
\ell(\lambda), & y < y^{\*} \quad (\text{«хорошие»})\\[2pt]
g(\lambda), & y \ge y^{\*} \quad (\text{«плохие»})
\end{cases}
$$
Можно показать, что максимизация Expected Improvement эквивалентна максимизации отношения $\ell(\lambda)/g(\lambda)$. TPE естественно поддерживает **категориальные и условные** пространства (отсюда «tree-structured») и хорошо масштабируется по $d$. Open-source реализация — `hyperopt` (2013), а также ядро `Optuna`.
**SMAC (random forest surrogate).** Источник: [Lindauer et al., 2022, «SMAC3», JMLR 23(54):19](http://jmlr.org/papers/v23/21-0888.html). Суррогат — random forest, дающий среднее и дисперсию по разбросу деревьев. RF хорошо работает с категориальными/условными осями, целочисленными переменными и высоким $d$; плюс агрессивный racing-механизм для сравнения конфигураций на нескольких instance.
### 3.2 Acquisition functions
Пусть $f^{\*}$ — лучшее наблюдённое значение (минимизация), $\mu,\sigma$ — апостериорные среднее и СКО суррогата.
| Acquisition | Формула | Поведение |
|-------------|---------|-----------|
| **PI** (Probability of Improvement) | $\mathrm{PI}(\lambda)=\Phi\!\Big(\dfrac{f^{\*}-\mu(\lambda)-\xi}{\sigma(\lambda)}\Big)$ | жадная, склонна к exploitation |
| **EI** (Expected Improvement) | $\mathrm{EI}(\lambda)=(f^{\*}-\mu-\xi)\Phi(z)+\sigma\,\phi(z),\;\; z=\dfrac{f^{\*}-\mu-\xi}{\sigma}$ | баланс, индустриальный стандарт |
| **UCB/LCB** (Upper/Lower Confidence Bound) | $\mathrm{LCB}(\lambda)=\mu(\lambda)-\beta_t\,\sigma(\lambda)$ | $\beta_t$ напрямую регулирует exploration |
Здесь $\Phi,\phi$ — CDF и PDF стандартного нормального распределения, $\xi\ge 0$ — параметр jitter для exploration.
**Exploration vs exploitation.** Это центральный компромисс BO: $\sigma(\lambda)$ велика в неисследованных областях (exploration), $\mu(\lambda)$ мала в перспективных (exploitation). UCB/LCB даёт теоретические гарантии: [Srinivas et al., 2010, «Gaussian Process Optimization in the Bandit Setting: No Regret and Experimental Design», ICML](https://www.researchgate.net/publication/221345649_Gaussian_Process_Optimization_in_the_Bandit_Setting_No_Regret_and_Experimental_Design) доказали сублинейную границу кумулятивного regret $\tilde{O}(\sqrt{N\,\gamma_N})$ для GP-UCB, где $\gamma_N$ — максимальный information gain, а $\beta_t$ выбирается по информационно-теоретическому правилу.
---
## 4. Эволюционные / популяционные методы
### 4.1 CMA-ES (Covariance Matrix Adaptation Evolution Strategy)
Поддерживает многомерное гауссово распределение $\mathcal{N}(m,\,\sigma^2 C)$ над пространством поиска; на каждой итерации сэмплирует популяцию $\lambda_i$, оценивает, и адаптирует среднее $m$, шаг $\sigma$ и **матрицу ковариаций** $C$ по лучшим особям:
$$
\lambda_i \sim m + \sigma\,\mathcal{N}(0,C),\qquad
m \leftarrow \sum_{i=1}^{\mu} w_i\,\lambda_{i:\text{best}} .
$$
Сильна на непрерывных, не-сепарабельных, мультимодальных ландшафтах; не требует градиента. Минусы: категориальные/условные оси — неестественны; затраты на $C$ растут с $d^2$.
### 4.2 Генетические алгоритмы (GA)
Популяция «особей» (конфигураций) эволюционирует через **selection → crossover → mutation**. Гибки к смешанным пространствам, легко параллелятся, но требуют много оценок и тонкой настройки операторов.
### 4.3 Population Based Training (PBT)
Источник: [Jaderberg et al., 2017, «Population Based Training of Neural Networks», DeepMind, arXiv:1711.09846](https://arxiv.org/abs/1711.09846).
PBT обучает **популяцию** моделей параллельно и периодически выполняет две операции:
- **exploit** — слабые модели копируют веса И гиперпараметры лучших (truncation selection);
- **explore** — скопированные гиперпараметры возмущаются (perturb / resample).
Ключевая особенность: PBT находит **расписание** (schedule) гиперпараметров во времени, а не одну фиксированную точку — что часто оптимальнее (например, убывающий `lr`, меняющаяся сила аугментаций). Асинхронен, эффективно использует фиксированный бюджет, переиспользует частично обученные веса (не стартует с нуля).
---
## 5. Multi-fidelity / early-stopping
Базовая идея: **дёшево отсеять заведомо плохих**, не доводя их до полного обучения. «Fidelity» (точность оценки) = число эпох / доля данных / разрешение. Плохие конфигурации убиваются рано, бюджет переливается перспективным.
### 5.1 Successive Halving (SHA)
Запустить $n$ конфигураций с малым бюджетом $r$; оставить верхние $1/\eta$; увеличить их бюджет в $\eta$ раз; повторять. За $\log_\eta n$ «раундов» бюджет на конфигурацию растёт геометрически.
Проблема SHA — компромисс **«$n$ vs $B/n$»**: при фиксированном суммарном бюджете $B$ неясно, брать много конфигураций с малым бюджетом каждой или мало с большим (риск рано убить «медленный старт»).
### 5.2 Hyperband
Источник: [Li, Jamieson, DeSalvo, Rostamizadeh, Talwalkar, 2018, «Hyperband: A Novel Bandit-Based Approach to Hyperparameter Optimization», JMLR (arXiv:1603.06560)](https://arxiv.org/abs/1603.06560).
Hyperband решает дилемму $n$ vs $B/n$, запуская **несколько «брекетов» (brackets)** SHA с разными стартовыми $(n,r)$: от «много конфигураций / агрессивное отсеивание» до «мало / щадящее». Это страхует от misleading early performance. Внешний цикл по $s$, внутренний — SHA:
$$
s_{\max}=\lfloor\log_\eta(R)\rfloor,\qquad n_s=\Big\lceil \tfrac{s_{\max}+1}{s+1}\eta^{s}\Big\rceil,\qquad r_s=R\,\eta^{-s},
$$
где $R$ — максимальный бюджет на одну конфигурацию, $\eta$ — фактор отсева (обычно 3). Hyperband даёт ускорение более чем на порядок против random search и model-based методов на DL/kernel-задачах.
### 5.3 ASHA (Asynchronous Successive Halving)
Источник: [Li et al., 2020, «A System for Massively Parallel Hyperparameter Tuning», MLSys (arXiv:1810.05934)](https://arxiv.org/abs/1810.05934).
SHA синхронна: «раунд» ждёт завершения всех конфигураций (stragglers тормозят). ASHA **асинхронна**: как только конфигурация дошла до уровня и попадает в топ-$1/\eta$ на текущий момент — её сразу промотируют, не дожидаясь остальных; освободившийся ресурс берёт новую конфигурацию. Это даёт почти линейное масштабирование на сотни-тысячи воркеров. ASHA превосходит PBT, BOHB и Vizier в массово-параллельном режиме.
### 5.4 BOHB (Bayesian Optimization + HyperBand)
Источник: [Falkner, Klein, Hutter, 2018, «BOHB: Robust and Efficient Hyperparameter Optimization at Scale», ICML (arXiv:1807.01774)](https://arxiv.org/abs/1807.01774).
Hyperband сэмплирует конфигурации **случайно** — это его слабость на «больших» бюджетах. BOHB заменяет random-сэмплинг внутри Hyperband на **TPE-суррогат** (модель строится по уже виденным результатам на разных fidelity). Получает: anytime-производительность и параллелизм Hyperband + sample-efficiency BO. Ускорение поиска до ~50× по wall-clock. Реализация — `HpBandSter` (AutoML).
---
## 6. Gradient-based / hypergradients и meta-learning (кратко)
Если бы метрика была дифференцируема по $\lambda$, можно было бы делать градиентный спуск по гиперпараметрам. Подходы:
- **Reversible learning / hypergradients.** [Maclaurin, Duvenaud, Adams, 2015, «Gradient-based Hyperparameter Optimization through Reversible Learning», ICML (arXiv:1502.03492)](https://arxiv.org/abs/1502.03492): вычисляют точный градиент валидационной потери по тысячам гиперпараметров, разворачивая (reverse-mode) всю динамику SGD-with-momentum назад во времени. Позволяет настраивать расписания `lr`/momentum, схемы регуляризации, инициализацию.
- **Implicit differentiation.** Дифференцирование через условие стационарности обученных весов — избегает хранения всей траектории (масштабируется лучше).
- **Meta-learning связь.** Та же машинерия (внешний/внутренний цикл) лежит в основе MAML-подобных подходов: «гиперпараметром» становятся инициализация или сам алгоритм обучения.
Ограничение: применимо к непрерывным дифференцируемым гиперпараметрам (`lr`, `wd`, веса лоссов); не работает для категориальных/архитектурных. На практике в edge-классификации используется редко (дорого, нестабильно), но важно концептуально.
---
## 7. Инструменты-фреймворки
| Фреймворк | Основные алгоритмы | Плюсы | Минусы | Лицензия |
|-----------|--------------------|-------|--------|----------|
| **[Optuna](https://github.com/optuna/optuna)** ([Akiba 2019, KDD](https://arxiv.org/abs/1907.10902)) | TPE, CMA-ES, GP, Random; pruners (ASHA/Hyperband/Median) | define-by-run API (динамич. условные пространства), отличная визуализация, importance | surrogate-GP слабее спец-BO; sampler по умолчанию последовательный | MIT |
| **[Ray Tune](https://docs.ray.io/en/latest/tune/index.html)** | ASHA, Hyperband, PBT, BOHB; интегрирует Optuna/HyperOpt/Ax/Nevergrad | масштаб от ноутбука до кластера без правки кода, лучший параллелизм | overhead Ray, кривая входа | Apache-2.0 |
| **[Hyperopt](https://github.com/hyperopt/hyperopt)** ([Bergstra 2011/2013](https://www.researchgate.net/publication/216816964_Algorithms_for_Hyper-Parameter_Optimization)) | TPE, Random, (Anneal) | классика TPE, условные пространства, MongoDB-параллелизм | вялое развитие, устаревший API, нет GP | BSD |
| **[scikit-optimize](https://scikit-optimize.github.io/)** | GP-BO, RF, GBRT (`gp_minimize`) | sklearn-совместим, прост | малый параллелизм, проект почти заморожен | BSD-3 |
| **[Ax](https://ax.dev) / [BoTorch](https://botorch.org)** ([arXiv:1912.05686](https://arxiv.org/abs/1912.05686)) | GP-BO на PyTorch, multi-objective (qEHVI), constraints | SOTA-BO, multi-objective/Pareto, neural-aware | избыточен для простого поиска; нужен GPyTorch | MIT |
| **[Nevergrad](https://github.com/facebookresearch/nevergrad)** | CMA-ES, DE, PSO, TBPSA, oneshot (ask-and-tell) | богатый набор gradient-free, хорош для непрерывных/шумных | слабая поддержка условных, мало DL-инфраструктуры | MIT |
| **[SMAC3](https://github.com/automl/SMAC3)** ([Lindauer 2022, JMLR](http://jmlr.org/papers/v23/21-0888.html)) | RF-surrogate BO + racing, multi-fidelity (Hyperband) | силён на категор./условных/смешанных, AutoML-зрелость | сложнее в освоении, тяжелее зависимости | BSD-3 |
| **[NNI](https://github.com/microsoft/nni)** (Microsoft) | TPE, SMAC, Hyperband, PBT, NAS, Anneal, Evolution | широкий охват (HPO + NAS + compression), web-UI | объёмный, реже обновляется | MIT |
| **[W&B Sweeps](https://docs.wandb.ai/guides/sweeps)** | Grid, Random, Bayes (+early-terminate Hyperband) | трекинг + поиск в одном, агенты-воркеры, дашборды | BO базовый; SaaS-зависимость для UI | проприет. (SaaS) + open client (MIT) |
| **[KerasTuner](https://keras.io/keras_tuner/)** | Random, Hyperband, BO | прост, нативно Keras/TF | только TF/Keras-экосистема, базовые алгоритмы | Apache-2.0 |
> Практический выбор для edge-DL (EdgeNeXt и т. п.): **Optuna** (TPE + ASHA pruner) как дефолт; **Ray Tune** при многоузловом кластере; **Ax/BoTorch** при явной multi-objective (accuracy ↔ latency).
---
## 8. Практическая методология для DL-классификации
### 8.1 Что подбирать и в какой шкале
| Гиперпараметр | Типичный диапазон | Шкала | Приоритет |
|---------------|-------------------|-------|-----------|
| `learning_rate` | $10^{-4}\ldots10^{-1}$ (SGD) / $10^{-5}\ldots10^{-3}$ (AdamW) | **log** | ★★★ высший |
| `weight_decay` | $10^{-6}\ldots10^{-2}$ | **log** | ★★★ |
| `batch_size` | {64,128,256,512} | log2 | ★★ (связан с `lr`) |
| `optimizer` | {SGD+momentum, AdamW, Lion} | categorical | ★★ |
| `lr_scheduler` | {cosine, step, one-cycle, warmup+cosine} | categorical / cond | ★★ |
| `warmup_epochs` | 0…10 | linear | ★ |
| `dropout` / `drop_path` | 0.0…0.5 | linear | ★★ |
| `label_smoothing` | 0.0…0.2 | linear | ★ |
| аугментации (RandAugment $N,M$; mixup $\alpha$; cutmix) | $N\in[1,3]$, $M\in[5,15]$, $\alpha\in[0.1,1.0]$ | mixed | ★★ |
| ёмкость / глубина модели (если NAS-lite) | пресеты {XS,S,M} | ordinal/cat | ★★ |
| глубина заморозки (freeze depth при fine-tuning) | 0…$L$ блоков | integer | ★★ (для transfer) |
**Почему лог-шкала для `lr`/`wd`.** Их эффект мультипликативен, а «хорошие» значения охватывают несколько порядков. Линейный сэмплинг между $10^{-5}$ и $10^{-1}$ отдал бы ~99 % проб диапазону $[10^{-2},10^{-1}]$ и почти не пробовал малые значения. Лог-uniform делает плотность равномерной по порядкам: $\log_{10}(\text{lr})\sim U(-4,-1)$.
### 8.2 Воспроизводимость и валидация
- **Фиксация seed.** Задавать `seed` для Python/NumPy/torch и `torch.use_deterministic_algorithms(True)` (где возможно). Помнить: $\mathcal{L}_{\text{val}}(\lambda)$ зашумлена — один seed даёт смещённую оценку.
- **Реплики (replicas).** Топ-кандидатов переоценивать на $\ge 3$ seed'ах и сравнивать по среднему ± std; иначе «победитель» может оказаться счастливым seed'ом.
- **Валидационная стратегия.** Hold-out (фикс. train/val) — стандарт для больших DL-датасетов (быстро, дёшево). **k-fold CV** — для малых датасетов (надёжнее, но в $k$ раз дороже; в DL применяют редко). Test-сет трогать **только однократно** в самом конце.
- **Бюджет.** Заранее зафиксировать $N$ trial'ов или GPU-часы; multi-fidelity (ASHA) при тесном бюджете.
- **Отчёт о важности (importance).** После поиска — fANOVA / permutation importance (есть в Optuna `plot_param_importances`), чтобы понять, какие оси реально влияют, и сузить $\Lambda$ в следующей итерации.
```python
# Optuna: TPE + ASHA-pruner, log-шкала для lr/wd, медианный pruning по эпохам
import optuna
def objective(trial):
lr = trial.suggest_float("lr", 1e-4, 1e-1, log=True)
wd = trial.suggest_float("weight_decay", 1e-6, 1e-2, log=True)
opt = trial.suggest_categorical("optimizer", ["sgd", "adamw"])
model, loader_tr, loader_val = build(lr, wd, opt) # пользовательская сборка
for epoch in range(MAX_EPOCHS):
train_one_epoch(model, loader_tr)
acc = evaluate(model, loader_val)
trial.report(acc, epoch) # для pruner'а
if trial.should_prune(): # ранний отсев
raise optuna.TrialPruned()
return acc
study = optuna.create_study(
direction="maximize",
sampler=optuna.samplers.TPESampler(seed=42), # фикс. seed
pruner=optuna.pruners.SuccessiveHalvingPruner(), # multi-fidelity
)
study.optimize(objective, n_trials=64) # бюджет
print(study.best_params)
optuna.importance.get_param_importances(study) # отчёт о важности
```
---
## 9. Типичные ошибки
| Ошибка | Суть | Как избежать |
|--------|------|--------------|
| **Утечка test в подбор** | гиперпараметры выбираются по test-метрике → test перестаёт быть честной оценкой | test трогать один раз; подбор — только на val |
| **Переобучение на валидации** | при большом $N$ и одном val-сете выбирается $\lambda$, случайно хороший на этом val | nested CV / отдельный «дев»-сет; репорт ± std по seed'ам; ограничить $N$ |
| **Нечестное сравнение методов** | сравнивать random vs BO при разном числе trial'ов или GPU-часов | фиксировать **одинаковый бюджет** (trial'ы или wall-clock) и одинаковое $\Lambda$ |
| **Линейная шкала для `lr`/`wd`** | почти не пробуются малые значения | log-uniform |
| **Один seed как «доказательство»** | разброс по seed'ам > разницы между конфигурациями | $\ge 3$ реплик, сравнение по средним |
| **Сравнение на разных $\Lambda$** | у random search границы шире/уже, чем у grid | единое пространство поиска для всех методов |
| **Игнор зашумлённости objective** | принятие решения по разнице в 0.1 % | стат-тест (напр., paired t-test, p<0.05) перед выводом |
| **Подбор архитектуры по test (NAS-leak)** | выбор размера/глубины по тестовой точности | архитектурный поиск — на val/dev |
---
## Итоговая сравнительная таблица методов
| Метод | Размерность $d$ | Бюджет (sample-eff.) | Параллелизм | Категориальные | Условные | Anytime |
|-------|:---:|:---:|:---:|:---:|:---:|:---:|
| Manual / Babysitting | низкая | низкая эфф. | нет | да (вручную) | да | да |
| Grid Search | низкая ($k^d$) | очень низкая | полный (embarrassingly) | да | плохо | нет |
| Random Search | высокая ✓ | низкая–средняя | полный ✓ | да | да | да |
| BO — GP | низкая–средняя | **высокая** ✓ | слабый (последоват.) | плохо | плохо | да |
| BO — TPE | средняя–высокая ✓ | высокая | средний | да ✓ | да ✓ | да |
| BO — SMAC (RF) | средняя–высокая | высокая | средний | да ✓ | да ✓ | да |
| CMA-ES | средняя (непрер.) | средняя | да (популяция) | плохо | плохо | да |
| Genetic Algorithms | средняя–высокая | низкая–средняя | да ✓ | да | да | да |
| PBT | средняя | высокая (reuse весов) | да ✓ | ограниченно | ограниченно | **да (расписание)** |
| Successive Halving | высокая | высокая ✓ | средний (синхр.) | да | да | да |
| Hyperband | высокая ✓ | высокая ✓ | хороший | да | да | **да** ✓ |
| ASHA | высокая ✓ | высокая | **массовый** ✓ | да | да | **да** ✓ |
| BOHB | высокая ✓ | **высокая** ✓ (BO+HB) | хороший ✓ | да ✓ | да ✓ | **да** ✓ |
| Gradient/hypergradient | очень высокая (непрер.) | высокая | слабый | **нет** | нет | частично |
**Anytime-свойство** — способность выдать разумный «лучший на текущий момент» результат при остановке в любой момент (важно при ограниченном/непредсказуемом бюджете). Им обладают random search, BO и особенно multi-fidelity (Hyperband/ASHA/BOHB), тогда как grid search anytime-свойством не обладает.
---
## Источники
1. [Bergstra J., Bengio Y. (2012). Random Search for Hyper-Parameter Optimization. JMLR 13:281305.](https://jmlr.org/papers/v13/bergstra12a.html)
2. [Bergstra J., Bardenet R., Bengio Y., Kégl B. (2011). Algorithms for Hyper-Parameter Optimization. NeurIPS 24:25462554.](https://www.researchgate.net/publication/216816964_Algorithms_for_Hyper-Parameter_Optimization)
3. [Srinivas N., Krause A., Kakade S., Seeger M. (2010). Gaussian Process Optimization in the Bandit Setting: No Regret and Experimental Design. ICML.](https://www.researchgate.net/publication/221345649_Gaussian_Process_Optimization_in_the_Bandit_Setting_No_Regret_and_Experimental_Design)
4. [Lindauer M. et al. (2022). SMAC3: A Versatile Bayesian Optimization Package for HPO. JMLR 23(54):19.](http://jmlr.org/papers/v23/21-0888.html)
5. [Jaderberg M. et al. (2017). Population Based Training of Neural Networks. arXiv:1711.09846.](https://arxiv.org/abs/1711.09846)
6. [Li L., Jamieson K., DeSalvo G., Rostamizadeh A., Talwalkar A. (2018). Hyperband: A Novel Bandit-Based Approach to Hyperparameter Optimization. arXiv:1603.06560.](https://arxiv.org/abs/1603.06560)
7. [Li L. et al. (2020). A System for Massively Parallel Hyperparameter Tuning (ASHA). MLSys. arXiv:1810.05934.](https://arxiv.org/abs/1810.05934)
8. [Falkner S., Klein A., Hutter F. (2018). BOHB: Robust and Efficient Hyperparameter Optimization at Scale. ICML. arXiv:1807.01774.](https://arxiv.org/abs/1807.01774)
9. [Maclaurin D., Duvenaud D., Adams R.P. (2015). Gradient-based Hyperparameter Optimization through Reversible Learning. ICML. arXiv:1502.03492.](https://arxiv.org/abs/1502.03492)
10. [Akiba T. et al. (2019). Optuna: A Next-generation Hyperparameter Optimization Framework. KDD. arXiv:1907.10902.](https://arxiv.org/abs/1907.10902)
11. [Balandat M. et al. (2019). BoTorch / Ax: Bayesian Hyperparameter Optimization. arXiv:1912.05686.](https://arxiv.org/abs/1912.05686)

258
_research/SECTION_mona.md Normal file
View File

@@ -0,0 +1,258 @@
# Mona-layer (Multi-cognitive Visual Adapter) — adapter-based PEFT для зрения
> Источники: статья [Yin, Hu, Li, Zhang, Yang — «5%>100%: Breaking Performance Shackles of Full Fine-Tuning on Visual Recognition Tasks», arXiv:2408.08345 (CVPR 2025)](https://arxiv.org/abs/2408.08345); официальная реализация [GitHub Leiyi-Hu/mona](https://github.com/Leiyi-Hu/mona).
> Все формулы блока и псевдокод ниже выверены по файлу [`swin_transformer_mona.py`](https://raw.githubusercontent.com/Leiyi-Hu/mona/master/Swin-Transformer-Classification/mmcls/models/backbones/swin_transformer_mona.py) (классы `Mona` и `MonaOp`).
---
## 1. Проблема: full fine-tuning против PEFT
Классическая схема переноса — *pre-train & fine-tune*: большой backbone обучается на крупном корпусе (ImageNet-21k, LVD-142M и т.п.), затем **все** его веса дообучаются на целевой задаче. На больших целевых датасетах это работает, но имеет два дефекта на **малых** данных:
- **Стоимость.** Для Swin-L это ~198M обучаемых параметров и отдельная полная копия весов на каждую задачу/чекпойнт ([Yin et al., 2024](https://arxiv.org/abs/2408.08345)).
- **Переобучение.** При малом числе примеров обновление всех параметров подгоняет backbone под шум обучающей выборки; обобщение падает.
**PEFT (parameter-efficient fine-tuning, она же delta-tuning)** решает обе проблемы: backbone **заморожен** ($\nabla_\theta = 0$), обучается лишь малая доля новых параметров $\Delta$ (обычно 0.15 %). Семейства PEFT:
| Семейство | Что обучается | Представитель |
|-----------|---------------|---------------|
| Bias-tuning | только bias-векторы | BitFit |
| Low-rank | низкоранговые добавки $BA$ к линейным слоям | LoRA |
| Adapter (последовательный) | bottleneck-MLP внутри блока | Houlsby Adapter, AdaptFormer |
| Prompt-tuning | обучаемые prompt-токены на входе | VPT |
| Side-tuning | внешняя «боковая» сеть | LST |
Главная претензия статьи Mona: до неё **ни один visual delta-tuning не превосходил full fine-tuning на сложных плотных задачах** (detection, segmentation). Mona — первый метод, который это делает на всех протестированных задачах ([arXiv:2408.08345, abstract](https://arxiv.org/abs/2408.08345)).
---
## 2. Что такое Mona-блок
Mona — это **последовательный adapter** (вставляется в residual-ветку трансформерного блока), но с двумя ключевыми отличиями от языковых адаптеров:
1. **Multi-cognitive фильтры** — внутри bottleneck стоят не линейные слои, а набор **depth-wise свёрток разных масштабов** (3×3, 5×5, 7×7), которые усредняются. Это даёт «vision-friendly» обработку (локальность, разные рецептивные поля).
2. **Scaled LayerNorm на входе** — нормализация с **двумя обучаемыми покомпонентными масштабами** $\gamma$ и $\gamma_x$, регулирующая распределение признаков перед визуальными фильтрами.
### 2.1. Структура (по реализации)
Внутренний модуль `MonaOp` (multi-scale conv-агрегатор), работает на тензоре $z \in \mathbb{R}^{B\times C_b\times H\times W}$ с $C_b = 64$ (bottleneck), все свёртки **depth-wise** (`groups = C_b`):
$$
\mathrm{MonaOp}(z) = z' + \mathrm{Proj}_{1\times1}(z'),\qquad
z' = \frac{\mathrm{DW}_{3}(z) + \mathrm{DW}_{5}(z) + \mathrm{DW}_{7}(z)}{3} + z
$$
где $\mathrm{DW}_{k}$ — depth-wise conv с ядром $k\times k$ и padding $k//2$, $\mathrm{Proj}_{1\times1}$ — pointwise conv $1\times1$. Обе агрегации обёрнуты в residual.
Внешний модуль `Mona` (вход — последовательность токенов $x \in \mathbb{R}^{B\times N\times C}$, $N = H\cdot W$):
$$
\hat{x} = \mathrm{LayerNorm}(x)\odot \gamma + x \odot \gamma_x \qquad\text{(scaled norm, обучаемые } \gamma,\gamma_x\in\mathbb{R}^{C})
$$
$$
u = W_{\text{down}}\,\hat{x} \in \mathbb{R}^{B\times N\times 64}\quad\text{(down-projection, Linear } C\to 64)
$$
$$
u' = \mathrm{MonaOp}\big(\mathrm{reshape}(u)\big)\quad\text{(токены} \to \text{карта } H\times W \to \text{токены)}
$$
$$
y = W_{\text{up}}\,\mathrm{Dropout}\big(\mathrm{GELU}(u')\big)\in\mathbb{R}^{B\times N\times C}\quad\text{(up-projection, Linear } 64\to C)
$$
$$
\boxed{\;\mathrm{Mona}(x) = x + y\;}\quad\text{(внешний residual)}
$$
Инициализация масштабов в коде: $\gamma = 10^{-6}\cdot\mathbf{1}$ (LayerNorm-ветка стартует почти «выключенной»), $\gamma_x = \mathbf{1}$ (тождественный проброс входа). Это делает Mona при инициализации **близкой к identity** — стабильный старт для дообучения.
> Замечание по терминологии: «scaled LayerNorm» здесь = `LayerNorm(x)*γ + x*γx`, то есть **выпуклоподобная** смесь нормированного и исходного признака с обучаемыми покомпонентными весами, а не нормировка с фиксированным множителем.
---
## 3. Куда вставляется и сколько добавляет
В Swin-блоке Mona вставляется **дважды на каждый трансформерный блок** — после attention-ветки и после FFN-ветки (см. `SwinTransformerBlock.forward`):
```
shortcut → (W-MSA/SW-MSA) → x = shortcut + drop_path(x)
x = Mona_1(x, (H,W)) # adapter после attention
identity = x; x = norm2(x); x = MLP(x)
x = identity + drop_path(x)
x = Mona_2(x, (H,W)) # adapter после FFN
```
- **Замороженным остаётся весь backbone** (attention, MLP, patch-merging, embeddings). Обучаются: параметры всех `Mona`-модулей (down/up Linear, 3 depth-wise conv + 1×1 projector, $\gamma$, $\gamma_x$) и task-head/decoder.
- **Доля обучаемых параметров backbone:** ~**2.56 %** на Swin-L и ~**4.67 %** на Swin-B (см. таблицы §4) ([arXiv:2408.08345, Tables](https://arxiv.org/abs/2408.08345)). Bottleneck фиксирован на 64 каналов, поэтому стоимость adapter не растёт квадратично с $C$.
---
## 4. Результаты статьи
Сравнение проведено на Swin-B/Swin-L (ImageNet-21k pre-train) против full fine-tuning и набора PEFT. Ниже — выверенные числа из [arXiv:2408.08345](https://arxiv.org/html/2408.08345v1).
### 4.1. COCO instance segmentation (Cascade Mask R-CNN, Swin-B)
| Метод | Обуч. параметры | % | AP$_{box}$ | AP$_{mask}$ |
|-------|------------------|------|-----------|-------------|
| Full fine-tuning | 89.14M | 100.0 | 52.40 | 45.10 |
| Fixed (frozen) | 0M | 0.0 | 48.00 | 41.60 |
| BitFit | 0.21M | 0.23 | 50.10 | 43.60 |
| LoRA | 3.06M | 3.43 | 50.40 | 43.90 |
| Adapter (Houlsby) | 3.19M | 3.58 | 52.10 | 45.00 |
| AdaptFormer | 1.60M | 1.79 | 51.70 | 44.60 |
| **Mona** | **4.16M** | **4.67** | **53.40** | **46.00** |
### 4.2. ADE20K semantic segmentation (UperNet, Swin-L)
| Метод | Обуч. параметры | % | mIoU |
|-------|------------------|------|------|
| Full fine-tuning | 198.58M | 100.0 | 51.18 |
| Fixed | 0M | 0.0 | 46.84 |
| BitFit | 0.30M | 0.15 | 48.37 |
| LoRA | 4.57M | 2.31 | 50.34 |
| Adapter | 4.61M | 2.33 | 50.78 |
| AdaptFormer | 2.34M | 1.18 | 50.83 |
| **Mona** | **5.08M** | **2.56** | **51.36** |
### 4.3. Pascal VOC detection (RetinaNet, Swin-L) и DOTA oriented (Oriented R-CNN, Swin-B)
| Метод | VOC AP$_{box}$ | DOTA mAP |
|-------|----------------|----------|
| Full fine-tuning | 83.70 | 78.31 |
| BitFit | 85.40 | 76.05 |
| LoRA | 85.40 | 75.91 |
| Adapter | 86.70 | 78.27 |
| AdaptFormer | 86.60 | 77.43 |
| **Mona** | **87.30** | **78.44** |
### 4.4. Image classification (Swin-L, усреднение по датасетам)
| Метод | Top-1 Acc | Top-5 Acc |
|-------|-----------|-----------|
| Full fine-tuning | 92.79 | 98.81 |
| BitFit | 93.51 | 99.69 |
| LoRA | 93.47 | 99.69 |
| Adapter | 93.99 | 99.61 |
| AdaptFormer | 93.70 | 99.65 |
| **Mona** | **94.04** | **99.76** |
### 4.5. Почему «5 % > 100 % full FT»
- На **плотных** задачах (detection, segmentation) full FT — сильный baseline, и линейные PEFT (LoRA, BitFit, AdaptFormer) его **не достают**. Mona обходит full FT на всех задачах: +1.0 AP$_{box}$ / +0.9 AP$_{mask}$ на COCO, +0.18 mIoU на ADE20K, +3.6 AP на VOC.
- Причина: **conv-фильтры разных масштабов** дают индуктивное смещение, подходящее под изображения (локальность, многомасштабность), которого нет у линейных адаптеров; а **заморозка backbone** регуляризует — на малых данных полная подгонка переобучается, а 5 % параметров «достаточно гибки, но не настолько, чтобы запомнить шум».
---
## 5. Mona vs LoRA vs обычный Adapter
| Критерий | LoRA | Adapter (Houlsby) | **Mona** |
|----------|------|-------------------|----------|
| Куда вставляется | добавка $\Delta W = \frac{\alpha}{r}BA$ к линейным слоям (q,v) | bottleneck-MLP внутри блока | bottleneck **с conv** в residual-ветке (после attn и после FFN) |
| Внутренние операторы | две линейные матрицы $A,B$ | down-Linear → нелинейность → up-Linear | down-Linear → **3× depth-wise conv (3/5/7) + 1×1** → GELU → up-Linear |
| Учёт пространства | нет (работает потокенно) | нет | **да** (reshape в карту $H\times W$, multi-scale свёртки) |
| Нормализация входа | нет | обычно нет | **scaled LayerNorm** с обучаемыми $\gamma,\gamma_x$ |
| Слияние в инференсе | можно слить в веса (zero-cost) | нет (доп. слой) | нет (доп. слой) |
| Сильная сторона | дёшево, мерджится | универсален | **vision-friendly, превосходит full FT на dense-задачах** |
| Доля параметров | ~23 % | ~2.33.6 % | ~2.54.7 % |
Ключевое отличие: LoRA и Houlsby-Adapter оперируют **только линейными** преобразованиями признаков и не используют 2D-структуру; Mona добавляет **визуальное индуктивное смещение** (multi-scale depth-wise conv) и **adaptive scaled norm**, поэтому выигрывает именно на изображениях.
---
## 6. Минимальный PyTorch-скелет Mona
Рабочий минимум, эквивалентный официальному коду (`bottleneck=64` по умолчанию, как в репозитории; здесь параметризован).
```python
import torch
import torch.nn as nn
import torch.nn.functional as F
class MonaOp(nn.Module):
"""Multi-cognitive conv aggregator: depthwise 3x3/5x5/7x7 + 1x1 projector."""
def __init__(self, dim: int):
super().__init__()
self.conv1 = nn.Conv2d(dim, dim, 3, padding=1, groups=dim) # depthwise 3x3
self.conv2 = nn.Conv2d(dim, dim, 5, padding=2, groups=dim) # depthwise 5x5
self.conv3 = nn.Conv2d(dim, dim, 7, padding=3, groups=dim) # depthwise 7x7
self.projector = nn.Conv2d(dim, dim, 1) # pointwise 1x1
def forward(self, x): # x: (B, dim, H, W)
identity = x
x = (self.conv1(x) + self.conv2(x) + self.conv3(x)) / 3.0 + identity
identity = x
return identity + self.projector(x) # second residual
class Mona(nn.Module):
"""Multi-cognitive visual adapter (Yin et al., CVPR 2025)."""
def __init__(self, in_dim: int, bottleneck: int = 64):
super().__init__()
self.norm = nn.LayerNorm(in_dim)
self.gamma = nn.Parameter(torch.ones(in_dim) * 1e-6) # scaled-norm branch
self.gammax = nn.Parameter(torch.ones(in_dim)) # identity branch
self.project1 = nn.Linear(in_dim, bottleneck) # down-projection
self.adapter_conv = MonaOp(bottleneck) # multi-scale conv
self.nonlinear = F.gelu
self.dropout = nn.Dropout(0.1)
self.project2 = nn.Linear(bottleneck, in_dim) # up-projection
def forward(self, x, hw_shape): # x: (B, N, C), hw_shape=(H, W)
identity = x
x = self.norm(x) * self.gamma + x * self.gammax # scaled LayerNorm
u = self.project1(x) # (B, N, bottleneck)
b, n, c = u.shape
h, w = hw_shape
u = u.reshape(b, h, w, c).permute(0, 3, 1, 2) # -> (B, c, H, W)
u = self.adapter_conv(u)
u = u.permute(0, 2, 3, 1).reshape(b, n, c) # -> (B, N, c)
u = self.dropout(self.nonlinear(u))
return identity + self.project2(u) # outer residual
```
Вставка в трансформерный блок (frozen backbone, обучается только Mona):
```python
# внутри SwinBlock.forward после attention и после FFN:
x = shortcut + drop_path(attn_out)
x = mona_after_attn(x, (H, W))
x = x + drop_path(mlp_out)
x = mona_after_ffn(x, (H, W))
```
---
## 7. Применимость к маленькому датасету Naruto Sign на EdgeNeXt
Naruto Sign (распознавание ручных «печатей»/жестов) — **малый** датасет, EdgeNeXt — компактный backbone. Здесь Mona особенно уместен:
- **Снижение переобучения.** Backbone EdgeNeXt замораживается (предобученные веса сохраняют общие визуальные признаки), обучается лишь ~2.55 % параметров — меньше степеней свободы → меньше шансов запомнить шум малой выборки.
- **Доменная адаптация через conv.** Жесты рук — локальные многомасштабные паттерны (пальцы, контуры). Multi-scale depth-wise свёртки (3/5/7) подстраивают предобученные признаки под этот домен лучше линейных адаптеров.
- **Дёшево по памяти.** Хранится один frozen backbone + лёгкие Mona-веса; для edge-сценария это критично.
> Замечание по совместимости: EdgeNeXt — гибрид conv+SDTA, а не чистый Swin. Mona-блок (`Mona`/`MonaOp`) переносим почти без изменений: ему нужны лишь токенизированные признаки `(B, N, C)` + `(H, W)` для reshape, либо его можно применять прямо к feature-map `(B, C, H, W)`, опустив permute. Точку вставки (после каждой стадии / после SDTA-блоков) выносим в гиперпараметры.
### Гиперпараметры для Optuna
| Гиперпараметр | Диапазон / варианты поиска | Комментарий |
|---------------|----------------------------|-------------|
| `bottleneck` (down-dim) | {16, 32, 64, 96} | меньше → сильнее регуляризация, дешевле |
| набор ядер conv | {(3), (3,5), (3,5,7), (5,7)} | trade-off рецептивное поле vs параметры |
| агрегация ветвей | {mean, learnable-weighted sum} | взвешенная сумма даёт ещё ~$k$ обучаемых скаляров |
| init `gamma` | {1e-6, 1e-4, 1e-2} | насколько «включена» scaled-norm-ветка на старте |
| init `gammax` | {1.0, 0.5} | сила identity-проброса |
| dropout в bottleneck | [0.0, 0.3] | регуляризация под малый датасет |
| стадии вставки | подмножество {stage1..stage4} | в ранние/поздние стадии или только в часть |
| позиции в блоке | {after-attn, after-ffn, both} | both = как в оригинале |
| LR для Mona-параметров | log-uniform [1e-4, 5e-3] | adapter-веса часто требуют LR выше backbone |
| `weight_decay` Mona | log-uniform [1e-5, 1e-2] | — |
Эмпирическое правило (из статьи): на малых/плотных задачах лучше вставлять Mona **во все стадии и обе позиции** (after-attn + after-ffn); сокращать набор ядер до (3,5) или bottleneck до 32 имеет смысл, если Optuna показывает переобучение или упор в бюджет параметров.
---
## Источники
- [Yin D., Hu L., Li B., Zhang Y., Yang X. «5%>100%: Breaking Performance Shackles of Full Fine-Tuning on Visual Recognition Tasks». arXiv:2408.08345, 2024 (CVPR 2025)](https://arxiv.org/abs/2408.08345) — [HTML-версия](https://arxiv.org/html/2408.08345v1).
- [Официальная реализация — GitHub Leiyi-Hu/mona](https://github.com/Leiyi-Hu/mona); исходник блока — [`swin_transformer_mona.py`](https://raw.githubusercontent.com/Leiyi-Hu/mona/master/Swin-Transformer-Classification/mmcls/models/backbones/swin_transformer_mona.py).
- PEFT-baselines: LoRA ([Hu et al., 2021, arXiv:2106.09685](https://arxiv.org/abs/2106.09685)); Adapter/Houlsby ([Houlsby et al., 2019, arXiv:1902.00751](https://arxiv.org/abs/1902.00751)); AdaptFormer ([Chen et al., 2022, arXiv:2205.13535](https://arxiv.org/abs/2205.13535)); VPT ([Jia et al., 2022, arXiv:2203.12119](https://arxiv.org/abs/2203.12119)); BitFit ([Zaken et al., 2021, arXiv:2106.10199](https://arxiv.org/abs/2106.10199)).

View File

@@ -0,0 +1,261 @@
---
title: Набор данных Naruto Sign — описание и статистика
tags: [dataset, classification, naruto-hand-sign, edgenext, hpo]
status: draft
supervisor: мнс Павленко Б.В.
date: 2026-06-26
---
# Набор данных Naruto Sign — описание и статистика
> [!warning] Дисклеймер о точности чисел
> Карточки датасетов на Kaggle закрыты для анонимного доступа (HTTP 403), а Roboflow-страницы — для серверного фетча. Поэтому **точные per-class счётчики и разрешение исходных кадров нужно сверить ПОСЛЕ скачивания** скриптом из §7. Все приведённые ниже агрегированные числа (число классов, порядок величин по объёму) подтверждены минимум двумя независимыми источниками; там, где источник даёт лишь приблизительную цифру, это явно помечено «≈».
---
## 1. Что это за датасет и задача
Задача — **многоклассовая классификация изображений (single-label image classification)**: на вход подаётся RGB-кадр с руками, выполняющими ручную печать (hand seal / hand sign) из аниме *Naruto*; модель предсказывает один класс из набора. Это не детекция и не сегментация — для целей HPO-эксперимента с EdgeNeXt используется именно классификационная постановка (ImageFolder → softmax).
Канонический набор печатей в *Naruto* соответствует **12 знакам китайского/японского зодиака** (Jūnishi). Каноничный список с японскими названиями:
| # | EN (class label) | JP (rōmaji) | Зодиак |
|----|------------------|-------------|--------|
| 1 | Bird | Tori | Птица |
| 2 | Boar | I | Кабан |
| 3 | Dog | Inu | Собака |
| 4 | Dragon | Tatsu | Дракон |
| 5 | Hare | U | Заяц |
| 6 | Horse | Uma | Лошадь |
| 7 | Monkey | Saru | Обезьяна |
| 8 | Ox | Ushi | Бык |
| 9 | Ram | Hitsuji | Овен |
| 10 | Rat | Ne | Крыса |
| 11 | Snake | Mi | Змея |
| 12 | Tiger | Tora | Тигр |
> [!note] Фактический список зависит от источника
> - **Kaggle (vikranthkanumuru)** и большинство Roboflow-наборов используют ровно **12 классов** выше ([GitHub kanlanc, README](https://github.com/kanlanc/naruto-hand-sign-detection): «one of the 12 signs»; [Roboflow vgu](https://universe.roboflow.com/vgu-aeaes/naruto-hand-sign): `dog, bird, horse, tiger, monkey, snake, boar, dragon, hare, ox, ram, rat`).
> - В части реализаций добавляют **служебные классы**: «Zero» (исходная позиция/ладони сложены) — см. [nupuriya, GitHub](https://github.com/nupuriya/Naruto-Hand-Sign-Detection) (13 меток: 12 + «Zero»); либо «Unknown» (нет печати в кадре) — см. [renzobenemerito](https://renzobenemerito.github.io/Naruto-Hand-Seals-Classification/) (**13 классов** = 12 + Unknown).
> - **Вывод для студента:** число классов = 12 (базовый случай) либо 13 (если в скачанном наборе есть `Zero`/`Unknown`). Проверить по числу подпапок ImageFolder скриптом §7.
---
## 2. Статистика (по найденным источникам)
Сводка по основным публичным кандидатам:
| Источник | Задача | Классов | Всего изображений | Формат | Лицензия | Ссылка |
|----------|--------|---------|-------------------|--------|----------|--------|
| **Kaggle — vikranthkanumuru** (рекоменд. для классификации) | Classification | 12 | не указано публично (≈ тысячи кадров после аугментации) | ImageFolder (папка/класс) | см. карточку Kaggle (сверить) | [Kaggle](https://www.kaggle.com/datasets/vikranthkanumuru/naruto-hand-sign-dataset) |
| renzobenemerito (GitHub Pages) | Classification | 13 (12+Unknown) | ≈ 3 250+ (≈ 250/класс до аугментации) | ImageFolder | не указана | [сайт](https://renzobenemerito.github.io/Naruto-Hand-Seals-Classification/) |
| Roboflow — **vgu** | Object Detection | 12 | 15 891 | COCO/YOLO/… (экспорт) | Roboflow Universe (CC, сверить) | [Roboflow](https://universe.roboflow.com/vgu-aeaes/naruto-hand-sign) |
| Roboflow — yylunxie | Object Detection | 12 | 1 200 | экспорт | сверить | [Roboflow](https://universe.roboflow.com/yylunxie/naruto-hand-sign-p8toe) |
| Roboflow — Eddy Miner | Object Detection | 12 | 6 142 | экспорт | сверить | [Roboflow](https://universe.roboflow.com/eddy-miner-mwie9/naruto-t7o4a) |
> [!important] Рекомендация
> Для классификационного HPO-эксперимента с **EdgeNeXt** берём **Kaggle (vikranthkanumuru)** — он изначально в формате ImageFolder и заточен под классификацию (ResNet18/50, MobileNetV2, FastAI-ноутбуки строятся прямо на нём — [Kaggle notebooks](https://www.kaggle.com/code/vikranthkanumuru/naruto-hand-sign-detection-from-scratch/data)). Roboflow-наборы — детекционные (bbox), для классификации потребуется crop по bbox; используйте их как **запасной источник** или для расширения объёма.
### Что известно достоверно
- **Число классов:** 12 (базовый), подтверждено [GitHub kanlanc](https://github.com/kanlanc/naruto-hand-sign-detection) и [Roboflow vgu](https://universe.roboflow.com/vgu-aeaes/naruto-hand-sign).
- **Происхождение данных:** видеозаписи выполнения печатей, кадры извлечены через **OpenCV** «frame by frame», нечёткие кадры удалены вручную ([Medium, Vikranth Kanumuru](https://medium.com/kanlanc/how-to-make-a-naruto-hand-signs-classifier-using-deep-learning-eb91367cb131)). Часть кадров — вклад сообщества (студенты VIT) + кадры с YouTube.
- **Уже применённая автором аугментация:** повороты «left, right, up, down» (Medium). То есть в скачанном наборе **часть кадров уже синтетически повёрнута** — это важно для §6.
### Что НЕ известно публично (заполнить после скачивания)
- Точное число изображений всего и по классам (Kaggle карточка закрыта → 403).
- Разрешение исходных кадров (вероятно неоднородное, т.к. из видео разных источников).
- Готовый train/val/test split — у Kaggle-набора **штатного сплита, скорее всего, нет** (плоский ImageFolder). Сплит делаем сами (§5).
- Точная лицензия Kaggle-набора — посмотреть в блоке «License» на странице датасета.
---
## 3. Особенности и сложности для классификации
| Фактор | Влияние | Митигиция |
|--------|---------|-----------|
| **Происхождение из видео** | Соседние кадры почти идентичны → утечка train↔test, если бить случайно | Сплит по видео/последовательности, не по кадру (§5) |
| **Дисбаланс классов** | Часть печатей сложнее выполнять/снимать → меньше кадров | Стратифицированный сплит + взвешенный loss / oversampling |
| **Схожесть печатей** | Ram/Hitsuji ↔ Snake/Mi ↔ Dragon/Tatsu визуально близки (сложенные ладони) | Confusion matrix, label smoothing, достаточное разрешение |
| **Вариативность фона/освещения** | Кадры из разных видео и от разных контрибьюторов | Color jitter, нормализация; домен-агностичность |
| **Ракурс рук, лево/право** | Зеркальные конфигурации меняют семантику печати | Осторожно с horizontal flip (§6) |
| **Малый объём (≈ сотни–тысячи)** | Высокий риск переобучения, особенно для capacity-моделей | Сильная регуляризация, transfer learning (ImageNet-pretrained EdgeNeXt), ранняя остановка |
| **Уже повёрнутые автором кадры** | Двойная аугментация поворотами раздувает корреляции | Не накручивать большие rotation поверх |
Косвенное подтверждение «лёгкости» при правильном сетапе и «домен-переноса»: модель, обученная на реальных руках, классифицировала аниме-скриншоты ([Medium](https://medium.com/kanlanc/how-to-make-a-naruto-hand-signs-classifier-using-deep-learning-eb91367cb131)); Roboflow-детектор vgu даёт mAP@50 ≈ 99.5% — но это детекция на крупном (15.9k) наборе, не классификация на малом.
---
## 4. Как скачать
### 4.1. Kaggle — основной источник (ImageFolder, классификация)
```bash
# kagglehub (рекомендуется — кэширует и отдаёт локальный путь)
pip install kagglehub
```
```python
import kagglehub
# вернёт путь к распакованному набору
path = kagglehub.dataset_download("vikranthkanumuru/naruto-hand-sign-dataset")
print("Dataset path:", path) # внутри — папки-классы (ImageFolder)
```
```bash
# Альтернатива: официальный Kaggle API (нужен ~/.kaggle/kaggle.json)
pip install kaggle
kaggle datasets download -d vikranthkanumuru/naruto-hand-sign-dataset -p ./data --unzip
```
### 4.2. Roboflow — запасной/детекционный источник
```python
# pip install roboflow
from roboflow import Roboflow
rf = Roboflow(api_key="YOUR_KEY")
# workspace/project/version берутся из URL датасета на Universe
project = rf.workspace("vgu-aeaes").project("naruto-hand-sign")
dataset = project.version(1).download("folder") # 'folder' ≈ классификационный экспорт
```
> Для классификации из детекционного набора: либо экспорт `folder`/`multiclass`, либо crop изображений по bbox в отдельные папки-классы.
### 4.3. HuggingFace — на момент проверки (июнь 2026) канонического `narutosign` датасета в HF Hub не найдено
```python
# Если появится зеркало — типовой паттерн:
from datasets import load_dataset
ds = load_dataset("imagefolder", data_dir="./data") # локальный ImageFolder
```
> [!warning]
> Точные числа (объём, per-class, разрешение, лицензия) **обязательно сверить после скачивания** скриптом §7. Наиболее вероятные кандидаты-источники: ① [Kaggle vikranthkanumuru](https://www.kaggle.com/datasets/vikranthkanumuru/naruto-hand-sign-dataset) (классификация, приоритет), ② [Roboflow vgu](https://universe.roboflow.com/vgu-aeaes/naruto-hand-sign) (детекция, объём 15.9k).
---
## 5. Рекомендованный протокол подготовки данных
1. **Фиксированный seed** для воспроизводимости:
```python
import random, numpy as np, torch
SEED = 42
random.seed(SEED); np.random.seed(SEED)
torch.manual_seed(SEED); torch.cuda.manual_seed_all(SEED)
```
2. **Стратифицированный split** (сохраняет долю классов): 70/15/15 или 80/10/10.
3. **Без утечки (no leakage):** кадры из одного видео/последовательности **не должны попадать одновременно в train и test**. Если в именах файлов есть идентификатор видео/серии — группировать по нему (`GroupShuffleSplit` / `StratifiedGroupKFold`), а не по отдельным кадрам. Если идентификатора нет — дедупликация near-duplicate (perceptual hash) и/или предупреждение в отчёте, что оценка может быть оптимистичной.
4. **Стандартизация размера под EdgeNeXt:** EdgeNeXt штатно работает на **256×256** (или 224×224 для меньших вариантов). Train: `RandomResizedCrop(256)`; Val/Test: `Resize(256) → CenterCrop(256)`. Нормализация — статистика ImageNet (`mean=[0.485,0.456,0.406]`, `std=[0.229,0.224,0.225]`), т.к. EdgeNeXt берётся ImageNet-pretrained.
```python
# Стратифицированный групповой split без утечки по видео
from sklearn.model_selection import StratifiedGroupKFold
# paths, labels, groups (group = id видео/последовательности из имени файла)
sgkf = StratifiedGroupKFold(n_splits=5, shuffle=True, random_state=SEED)
train_idx, test_idx = next(sgkf.split(paths, labels, groups))
```
---
## 6. Аугментации для ручных знаков
> [!danger] Horizontal flip — НЕ применять (по умолчанию)
> Печати — это **конкретная конфигурация левой/правой руки** (какая рука сверху, направление пальцев). Зеркалирование меняет семантику жеста и может перевести один знак в визуально-другую/невалидную конфигурацию. Для hand-seal классификации `RandomHorizontalFlip` создаёт **label noise** → отключить. Вертикальный flip тем более не физичен.
| Аугментация | Применять? | Комментарий |
|-------------|-----------|-------------|
| `RandomHorizontalFlip` | ❌ Нет | Меняет лево/право руки → искажает класс |
| `RandomVerticalFlip` | ❌ Нет | Нефизично для рук |
| `RandomRotation` (±1015°) | ⚠️ Умеренно | Лёгкий наклон ОК; помнить, что часть кадров уже повёрнута автором (§2) — не накручивать большие углы |
| `ColorJitter` (brightness/contrast/saturation) | ✅ Да | Робастность к освещению/камере; hue — слабо |
| `RandomResizedCrop` | ✅ Да | Масштаб/положение рук варьируется; scale=(0.7,1.0) |
| `RandAugment` | ✅ Да (без flip-операций) | Сильная регуляризация на малом наборе; исключить горизонтальные отражения из набора политик при возможности |
| `mixup` | ⚠️ Осторожно | Помогает против оверфита, но смешивает руки → менее интерпретируемо; полезен как HPO-гиперпараметр |
| `cutmix` | ⚠️ Осторожно | Может «отрезать» дискриминативную руку → шум; пробовать с малой вероятностью |
| `RandomErasing` | ✅ Да | Дешёвая регуляризация, обычно безопасна |
```python
import torchvision.transforms as T
IMAGENET_MEAN, IMAGENET_STD = [0.485,0.456,0.406], [0.229,0.224,0.225]
train_tf = T.Compose([
T.RandomResizedCrop(256, scale=(0.7, 1.0)),
T.RandomRotation(12), # лёгкий наклон, без flip
T.ColorJitter(0.2, 0.2, 0.2, 0.02),
T.RandAugment(num_ops=2, magnitude=7), # без горизонтального отражения
T.ToTensor(),
T.Normalize(IMAGENET_MEAN, IMAGENET_STD),
T.RandomErasing(p=0.25),
])
eval_tf = T.Compose([
T.Resize(256), T.CenterCrop(256),
T.ToTensor(), T.Normalize(IMAGENET_MEAN, IMAGENET_STD),
])
# ВАЖНО: НЕ добавлять T.RandomHorizontalFlip() — меняет семантику печати.
```
---
## 7. Скрипт подсчёта статистики (запустить после скачивания)
Поля, которые студент **обязан заполнить** в отчёте по факту загрузки: число классов, всего изображений, изображений на класс (+мин/макс/коэф. дисбаланса), распределение разрешений, формат, наличие/отсутствие штатного сплита, лицензия.
```python
"""Подсчёт статистики ImageFolder-набора Naruto Sign."""
from pathlib import Path
from collections import Counter
from PIL import Image
DATA_DIR = Path("./data") # корень с подпапками-классами
EXTS = {".jpg", ".jpeg", ".png", ".bmp", ".webp"}
per_class = Counter()
resolutions = Counter()
for cls_dir in sorted(p for p in DATA_DIR.iterdir() if p.is_dir()):
imgs = [f for f in cls_dir.rglob("*") if f.suffix.lower() in EXTS]
per_class[cls_dir.name] = len(imgs)
for f in imgs:
try:
with Image.open(f) as im:
resolutions[im.size] += 1 # (W, H)
except Exception as e:
print(f"[BAD] {f}: {e}")
total = sum(per_class.values())
n_cls = len(per_class)
print(f"Классов: {n_cls} | Всего изображений: {total}")
for c, n in per_class.most_common():
print(f" {c:12s}: {n:5d} ({100*n/total:.1f}%)")
if per_class:
mx, mn = max(per_class.values()), min(per_class.values())
print(f"Дисбаланс max/min = {mx/mn:.2f} (max={mx}, min={mn})")
print("ТОП-5 разрешений (W x H):")
for (w, h), n in resolutions.most_common(5):
print(f" {w}x{h}: {n}")
```
> [!note] Чек-лист отчёта студента
> - [ ] Источник и версия датасета (URL + дата скачивания)
> - [ ] Число классов (12 или 13?) и точные метки папок
> - [ ] Всего изображений + таблица per-class + коэф. дисбаланса
> - [ ] Распределение разрешений (однородно?)
> - [ ] Лицензия (скопировать формулировку с карточки)
> - [ ] Есть ли group-id (видео) для no-leak split? Если нет — отметить риск
> - [ ] Зафиксированный seed и доли train/val/test
---
## Источники
- [Kaggle — Naruto Hand Sign Dataset (vikranthkanumuru)](https://www.kaggle.com/datasets/vikranthkanumuru/naruto-hand-sign-dataset)
- [GitHub — kanlanc/naruto-hand-sign-detection (ResNet18/50, 12 classes)](https://github.com/kanlanc/naruto-hand-sign-detection)
- [Medium — Vikranth Kanumuru: How to Make a Naruto Hand Signs Classifier (создание датасета, аугментации)](https://medium.com/kanlanc/how-to-make-a-naruto-hand-signs-classifier-using-deep-learning-eb91367cb131)
- [renzobenemerito — Naruto Hand Seals Classification (13 классов: 12 + Unknown, ≈250/класс)](https://renzobenemerito.github.io/Naruto-Hand-Seals-Classification/)
- [GitHub — nupuriya/Naruto-Hand-Sign-Detection (12 + «Zero»)](https://github.com/nupuriya/Naruto-Hand-Sign-Detection)
- [Roboflow Universe — vgu/naruto-hand-sign (15 891 img, 12 classes, mAP@50 ≈ 99.5%, detection)](https://universe.roboflow.com/vgu-aeaes/naruto-hand-sign)
- [Roboflow Universe — yylunxie/naruto-hand-sign (1 200 img, detection)](https://universe.roboflow.com/yylunxie/naruto-hand-sign-p8toe)
- [Roboflow Universe — Eddy Miner/naruto (6 142 img, detection)](https://universe.roboflow.com/eddy-miner-mwie9/naruto-t7o4a)
- [Kaggle notebook — Naruto Hand Sign Detection from scratch](https://www.kaggle.com/code/vikranthkanumuru/naruto-hand-sign-detection-from-scratch/data)

558
_research/SECTION_optuna.md Normal file
View File

@@ -0,0 +1,558 @@
---
title: "Optuna — подбор гиперпараметров: полный разбор"
section: HPO
tool: Optuna
supervisor: "мнс Павленко Б.В."
status: draft
tags: [hpo, optuna, hyperparameter-tuning, bayesian-optimization, pruning]
---
# Optuna — библиотека для подбора гиперпараметров
> Источники: официальный сайт [optuna.org](https://optuna.org/), документация [optuna.readthedocs.io](https://optuna.readthedocs.io/en/stable/), а также блог команды Optuna на Medium (релизы [v4.5](https://medium.com/optuna/optuna-v4-5-81e78d8e077a), [v4.6](https://medium.com/optuna/announcing-optuna-4-6-a9e82183ab07) и [AutoSampler](https://medium.com/optuna/autosampler-full-support-for-multi-objective-constrained-optimization-c1c4fc957ba2)).
**Optuna** — это open-source фреймворк для автоматической оптимизации гиперпараметров, не привязанный к конкретной ML-библиотеке (framework-agnostic): одинаково применим к PyTorch, TensorFlow/Keras, scikit-learn, XGBoost, LightGBM и др. Ключевые архитектурные принципы по [optuna.org](https://optuna.org/):
- **Eager / define-by-run search spaces** — пространство поиска описывается обычным Python-кодом (условия, циклы) прямо внутри objective-функции, а не статической конфигурацией заранее.
- **State-of-the-art алгоритмы** — эффективные сэмплеры (TPE, GP, CMA-ES) + прунинг бесперспективных trial-ов.
- **Простая параллелизация** — распараллеливание по потокам/процессам/узлам без изменения кода objective.
- **Визуализация и Dashboard** — встроенные графики истории, важности параметров, Pareto-фронта.
Установка (Python 3.9+):
```bash
pip install optuna # ядро
pip install optuna-dashboard # веб-дашборд (опционально)
pip install optunahub # доступ к OptunaHub (AutoSampler и др.)
```
---
## 1. Базовая модель: Study, Trial, define-by-run API
Три фундаментальных объекта:
| Понятие | Что это | Роль |
|---------|---------|------|
| **Study** | Сессия оптимизации | Хранит все trial-ы, направление(я) оптимизации, sampler, pruner, storage |
| **Trial** | Одна попытка (один набор гиперпараметров) | Через него `suggest_*` запрашивает значения и `report` отдаёт промежуточные метрики |
| **objective(trial)** | Пользовательская функция | Возвращает метрику (или кортеж метрик), которую Study минимизирует/максимизирует |
### 1.1 define-by-run: методы `trial.suggest_*`
Пространство поиска формируется в момент исполнения — каждый вызов `suggest_*` регистрирует гиперпараметр:
```python
import optuna
def objective(trial: optuna.Trial) -> float:
# вещественный, равномерно
lr = trial.suggest_float("lr", 1e-5, 1e-1, log=True) # log-uniform
# вещественный с шагом
dropout = trial.suggest_float("dropout", 0.0, 0.5, step=0.05)
# целочисленный (можно log=True и step)
n_layers = trial.suggest_int("n_layers", 1, 8)
# категориальный
optimizer = trial.suggest_categorical("optimizer", ["adam", "sgd", "adamw"])
# define-by-run: пространство зависит от уже выбранных значений
units = [trial.suggest_int(f"units_l{i}", 16, 256, log=True)
for i in range(n_layers)]
score = train_and_eval(lr, dropout, optimizer, units) # ваша логика
return score
```
Сводка методов suggest:
| Метод | Назначение | Полезные аргументы |
|-------|-----------|--------------------|
| `suggest_float(name, low, high, *, step, log)` | Вещественный параметр | `log=True` (лог-равномерно, для lr/wd); `step` (дискретизация) |
| `suggest_int(name, low, high, *, step, log)` | Целочисленный | `log=True`, `step` |
| `suggest_categorical(name, choices)` | Категориальный (строки/числа/bool) | список вариантов |
> `log=True` критичен для масштабно-чувствительных параметров (learning rate, weight decay): диапазон $[10^{-5}, 10^{-1}]$ при логарифмическом сэмплировании покрывается равномерно по порядкам величины. **Нельзя** одновременно задавать `step` и `log=True`.
### 1.2 Создание Study и запуск
```python
study = optuna.create_study(
direction="maximize", # или "minimize"
sampler=optuna.samplers.TPESampler(seed=42),
pruner=optuna.pruners.MedianPruner(),
storage="sqlite:///hpo.db", # опц. — персистентность/параллелизм
study_name="edgenext_hpo",
load_if_exists=True,
)
study.optimize(
objective,
n_trials=100, # сколько trial-ов выполнить
timeout=3600, # ИЛИ ограничение по времени (сек)
n_jobs=1, # параллелизм внутри процесса (потоки)
gc_after_trial=True, # сборка мусора между trial (важно для GPU)
)
print(study.best_value, study.best_params, study.best_trial.number)
```
- `direction` / `directions``"minimize"` (loss, latency, params) или `"maximize"` (accuracy, R@1).
- Остановка по `n_trials`, по `timeout` или по любому из них (что наступит раньше).
- `study.best_params`, `study.best_value`, `study.best_trial`, `study.trials` (полный список), `study.trials_dataframe()` (экспорт в pandas).
---
## 2. Samplers (алгоритмы выбора следующего набора)
Sampler решает, **какие значения гиперпараметров пробовать дальше**. По [optuna.readthedocs.io — samplers](https://optuna.readthedocs.io/en/stable/reference/samplers/index.html):
| Sampler | Алгоритм / назначение | Сильные стороны | Слабые стороны | Когда применять |
|---------|----------------------|-----------------|----------------|-----------------|
| **TPESampler** (default) | Tree-structured Parzen Estimator (Bayesian) | Работает с категориями, условными пространствами; `multivariate=True` ловит зависимости между параметрами; `group=True` — групповая факторизация | Слабее GP на гладких непрерывных задачах; жадноват | Универсальный выбор, 1001000 trial; смешанные/условные пространства |
| **GPSampler** | Gaussian Process Bayesian optimization | Высокая sample-efficiency на непрерывных/целочисленных; поддержка constrained + multi-objective (с v4.4/v4.5); в v4.6 ускорен ~8× | $O(n^3)$ — дорог при больших $n$; хуже с категориями | Дорогие objective (обучение сети), малый бюджет (≲500 trial) |
| **BoTorchSampler** (`optuna-integration`) | BO поверх BoTorch/GPyTorch | Гибкий BO, кастомные acquisition | $O(n^3)$, тяжёлые зависимости (torch) | Исследовательский BO, малый бюджет 10100 trial |
| **CmaEsSampler** | CMA-ES (эволюционная стратегия) | Очень эффективен на непрерывных задачах большой размерности | Не поддерживает multi-objective; плохо с категориями; не любит условные пространства | Непрерывные пространства, бюджет 100010000 trial |
| **NSGAIISampler** | NSGA-II (генетический, multi-objective) | Pareto-фронт для 23 целей; масштабируется по числу trial; поддержка `constraints_func` | Не sample-efficient (нужно много trial); хуже many-objective | Multi-objective (23 цели), большой бюджет |
| **NSGAIIISampler** | NSGA-III (reference-points) | Для **many-objective** (≥4 целей) | Нужно много trial; настройка reference points | Multi-objective при ≥4 целях |
| **QMCSampler** | Quasi-Monte Carlo (low-discrepancy: Sobol/Halton) | Лучшее покрытие пространства, чем random; масштабируется неограниченно | Не использует историю (не адаптивный) | Стартовая разведка, baseline, генерация init-точек |
| **RandomSampler** | Равномерный random search | Простой, без допущений, эталон сравнения | Неэффективен в больших пространствах | Baseline, sanity-check, очень малая размерность |
| **GridSampler** | Полный перебор заданной сетки | Детерминированный, исчерпывающий | Комбинаторный взрыв | Малое дискретное пространство, нужна полнота |
| **BruteForceSampler** | Define-by-run полный перебор | Перебирает всё дерево вариантов автоматически | Только для конечных малых пространств | Перебор небольшого условного дискретного пространства |
| **PartialFixedSampler** | Часть параметров зафиксирована, остальное — другим sampler | Дообследование подпространства | Вспомогательный, не самостоятельный | Зафиксировать найденное и доискать остальное |
| **AutoSampler** (OptunaHub) | Автоматический выбор из TPE/GP/NSGA-II/NSGA-III | Не нужно выбирать sampler вручную; ≥ default по качеству | Зависимость от `optunahub`; внутренний overhead анализа | Когда не хочется выбирать вручную (см. §2.2) |
### 2.1 Таблица поддержки возможностей (из официальной документации)
Легенда: ✓ — поддерживается, △ — работает, но неэффективно, ✗ — ошибка/нет интерфейса.
| Возможность | Auto | Random | TPE | GP | CMA-ES | NSGA-II | NSGA-III | Grid | QMC | BoTorch | BruteForce |
|-------------|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|
| Float | ✓ | ✓ | ✓ | ✓ | ✓ | △ | △ | ✓ | ✓ | ✓ | ✓ |
| Integer | ✓ | ✓ | ✓ | ✓ | ✓ | △ | △ | ✓ | ✓ | △ | ✓ |
| Categorical | ✓ | ✓ | ✓ | ✓ | △ | ✓ | ✓ | ✓ | △ | △ | ✓ |
| Pruning | △ | ✓ | ✓ | △ | △ | ✗/△ | ✗/△ | ✓ | ✓ | △ | ✓ |
| Multivariate | ✓ | △ | ✓ | ✓ | ✓ | △ | △ | △ | △ | ✓ | △ |
| Conditional space | ✓ | ✓ | ✓ | △ | △ | △ | △ | △ | △ | △ | ✓ |
| Multi-objective | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Batch optimization | ✓ | ✓ | ✓ | △ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Distributed | ✓ | ✓ | ✓ | △ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Constrained | ✓ | ✗ | ✓ | ✓ | ✗ | ✓ | ✓ | ✗ | ✗ | ✓ | ✗ |
Ориентировочная вычислительная сложность и бюджет ($d$ — размерность, $n$ — число trial, $m$ — число целей, $p$ — размер популяции):
| Sampler | Сложность | Рекоменд. число trial |
|---------|-----------|----------------------|
| RandomSampler / QMCSampler | $O(d)$ / $O(dn)$ | не ограничено |
| TPESampler | $O(dn\log n)$ | 1001000 |
| GPSampler / BoTorchSampler | $O(n^3)$ | ≲500 / 10100 |
| CmaEsSampler | $O(d^3)$ | 100010000 |
| NSGA-II / NSGA-III | $O(mp^2)$ | 10010000 |
### 2.2 AutoSampler (OptunaHub) — автоматический выбор алгоритма
Согласно статье [AutoSampler: full support for multi-objective & constrained optimization](https://medium.com/optuna/autosampler-full-support-for-multi-objective-constrained-optimization-c1c4fc957ba2), AutoSampler анализирует характеристики задачи (**бюджет вычислений, тип/структуру пространства, число целей, наличие ограничений**) и динамически переключается между четырьмя алгоритмами:
| Внутренний sampler | Когда AutoSampler его выбирает |
|--------------------|--------------------------------|
| **TPESampler** | сложные пространства с категориями и условными ветвлениями |
| **GPSampler** | непрерывные/целочисленные пространства; теперь с полной поддержкой multi-objective и constraints |
| **NSGAIISampler** | multi-objective при большом числе trial |
| **NSGAIIISampler** | many-objective (много целей) |
Гарантия: AutoSampler выбирает алгоритм, который «эмпирически даёт результат не хуже default». Поддержка multi-objective + constrained стала возможна благодаря наращиванию возможностей GPSampler в версиях 4.24.5 и финализирована в **v4.6**. Бенчмарки: превосходство на WFG1 (multi-objective) и на rotated Rastrigin (constrained).
```python
import optuna, optunahub
def objective(trial):
x = trial.suggest_float("x", 0, 5)
y = trial.suggest_float("y", 0, 3)
return 4 * x**2 + 4 * y**2, (x - 5)**2 + (y - 5)**2 # 2 цели
module = optunahub.load_module("samplers/auto_sampler") # подгрузка с OptunaHub
study = optuna.create_study(
directions=["minimize", "minimize"],
sampler=module.AutoSampler(),
)
study.optimize(objective, n_trials=300)
```
Установка: `pip install -U optuna optunahub` + `pip install -r https://hub.optuna.org/samplers/auto_sampler/requirements.txt`. Принудительное обновление кэша: `optunahub.load_module("samplers/auto_sampler", force_reload=True)`.
---
## 3. Pruners (ранняя остановка бесперспективных trial-ов)
Pruner отслеживает **промежуточные** метрики (например, val-accuracy по эпохам) и досрочно прерывает trial, который заведомо хуже остальных, экономя GPU-время. По [optuna.readthedocs.io — pruners](https://optuna.readthedocs.io/en/stable/reference/pruners.html).
> **Важное ограничение:** модуль pruners рассчитан **только на single-objective** оптимизацию.
| Pruner | Принцип | Ключевые параметры | Когда использовать |
|--------|---------|--------------------|--------------------|
| **MedianPruner** (default) | Median stopping rule: режет trial, если его промежуточное значение хуже медианы предыдущих на этом шаге | `n_startup_trials`, `n_warmup_steps`, `interval_steps` | Универсальный, простой и надёжный baseline |
| **PercentilePruner** | Обобщение медианного: режет хуже заданного перцентиля | `percentile`, `n_startup_trials`, `n_warmup_steps` | Когда нужен более/менее агрессивный порог, чем медиана |
| **SuccessiveHalvingPruner** | ASHA (Asynchronous Successive Halving): ресурсы удваиваются у «выживших» | `min_resource`, `reduction_factor`, `min_early_stopping_rate` | Большой бюджет, параллельный/распределённый поиск |
| **HyperbandPruner** | Несколько «скобок» (brackets) SuccessiveHalving с разным балансом explore/exploit | `min_resource`, `max_resource`, `reduction_factor` | Когда неизвестен оптимальный бюджет на trial — часто лучший выбор |
| **PatientPruner** | Обёртка над другим pruner: добавляет «терпение» (patience), не режет при временной просадке | `wrapped_pruner`, `patience`, `min_delta` | Шумные кривые обучения, чтобы не зарезать поздно-сходящиеся |
| **ThresholdPruner** | Режет, если метрика вышла за абсолютный порог (детекция выбросов/расходимости) | `lower`, `upper`, `n_warmup_steps` | Отсев расходящихся trial (NaN, взрыв loss) |
| **WilcoxonPruner** | Wilcoxon signed-rank test: статистически сравнивает trial с лучшим по набору инстансов | `p_threshold`, `n_startup_steps` | Когда objective усредняется по многим примерам/фолдам |
| **NopPruner** | Никогда не режет (no-op) | — | Отключить прунинг, эталон сравнения |
### 3.1 Встраивание в цикл обучения: `report()` + `should_prune()`
Внутри objective на каждой эпохе сообщаем промежуточную метрику через `trial.report(value, step)` и проверяем `trial.should_prune()`:
```python
import optuna
def objective(trial):
model, loader_tr, loader_val = build(trial)
for epoch in range(N_EPOCHS):
train_one_epoch(model, loader_tr)
acc = evaluate(model, loader_val)
trial.report(acc, step=epoch) # отдать промежуточную метрику
if trial.should_prune(): # спросить pruner
raise optuna.TrialPruned() # прервать trial
return acc
```
Логика: `report()` сохраняет $(step, value)$ в storage; `should_prune()` вызывает `pruner.prune(study, trial)`, который сравнивает кривую текущего trial с историей. Прерывание выполняется через исключение `optuna.TrialPruned` — Optuna помечает trial как `PRUNED`, а не `FAIL`.
> Для согласованности **шаги `step` должны совпадать между trial-ами** (например, номер эпохи), иначе медиана/перцентиль считаются по разным точкам.
---
## 4. Multi-objective и constrained optimization
### 4.1 Несколько целей
Задаётся через `directions` (список), objective возвращает **кортеж**:
```python
def objective(trial):
model = build(trial)
acc = evaluate(model) # хотим максимизировать
n_params = count_params(model) / 1e6 # хотим минимизировать (млн)
return acc, n_params
study = optuna.create_study(
directions=["maximize", "minimize"], # accuracy ↑, params ↓
sampler=optuna.samplers.NSGAIISampler(),
)
study.optimize(objective, n_trials=200)
best_trials = study.best_trials # МНОЖЕСТВО Парето-оптимальных, не один best
```
При нескольких целях единственного «лучшего» нет — есть **Pareto-фронт**: множество недоминируемых решений. Точка $a$ доминирует $b$, если не хуже по всем целям и строго лучше хотя бы по одной. `study.best_trials` возвращает весь фронт; выбор финальной точки — компромисс инженера (например, max accuracy при params ≤ заданного бюджета).
### 4.2 Ограничения (constraints)
Поддерживается в TPESampler, NSGAII/NSGAIIISampler, GPSampler, BoTorchSampler (см. таблицу §2.1). Паттерн по [Optuna FAQ](https://optuna.readthedocs.io/en/stable/faq.html): ограничения формулируются так, что **допустимое решение ⇔ значение ≤ 0**, сохраняются в `user_attrs`, а `constraints_func` их извлекает.
```python
def objective(trial):
x = trial.suggest_float("x", 0, 10)
y = trial.suggest_float("y", 0, 10)
# ограничения: c_i <= 0 означает "выполнено"
c0 = (x - 5)**2 + y**2 - 25
c1 = -(x - 8)**2 - (y + 3)**2 + 7.7
trial.set_user_attr("constraint", (c0, c1)) # сохранить
return 4*x**2 + y**2, (x - 5)**2 + (y - 5)**2 # 2 цели
def constraints(trial):
return trial.user_attrs["constraint"] # извлечь для sampler
study = optuna.create_study(
directions=["minimize", "minimize"],
sampler=optuna.samplers.NSGAIISampler(constraints_func=constraints),
)
study.optimize(objective, n_trials=300)
```
Sampler штрафует/отодвигает недопустимые области (где $c_i > 0$), концентрируя бюджет в feasible-зоне. В v4.5 GPSampler получил **constrained multi-objective** на базе log-EHVI (Expected Hypervolume Improvement), с моделированием и целей, и ограничений отдельными GP (см. §5).
---
## 5. Что нового в Optuna v4.5 и v4.6
### v4.5 (по [блогу Optuna](https://medium.com/optuna/optuna-v4-5-81e78d8e077a))
| Фича | Суть |
|------|------|
| **Constrained multi-objective в GPSampler** | GPSampler теперь умеет одновременно несколько целей + inequality-ограничения |
| **Acquisition = log-EHVI** | Логарифм Expected Hypervolume Improvement как функция приобретения |
| **GP для целей и для ограничений** | Отдельные гауссовские процессы; feasibility оценивается через Probability of Improvement (PI) |
| **Box decomposition** | Ускорение вычислений до **6800×**; практично до 4 целей |
| **Эффективность** | Меньше «впустую» оценок в infeasible-областях; быстрее сходимости, чем TPE/NSGA-II на бенчмарках (целевой уровень за <50 trial) |
### v4.6 (по [блогу Optuna](https://medium.com/optuna/announcing-optuna-4-6-a9e82183ab07))
| Фича | Суть |
|------|------|
| **LLM-интеграция в Dashboard** | Фильтрация trial на естественном языке + автогенерация графиков (Plotly.js) |
| **GPSampler ускорен ~8×** | Кэширование, параллелизация, оптимизация внутренних операций при сохранении качества |
| **AutoSampler: full multi-objective + constrained** | Автоматический выбор среди GPSampler / NSGAIIISampler и др. по характеру задачи (см. §2.2) |
| **Robust BO: CARBO и Value-at-Risk (VaR)** | Устойчивая байесовская оптимизация при шуме параметров (реальные приложения) |
| **OpenTelemetry-мониторинг** | Туториал по экспорту метрик Dashboard в Prometheus/Grafana |
| **LaTeX в OptunaHub** | Рендер математики (GitHub-совместимый синтаксис) |
| **MCP Server: Structured Output** | Валидация и schema-based обработка результатов |
> Breaking-изменений API, формата storage и pruner-ов в v4.6 не заявлено.
---
## 6. OptunaHub, storage, распределённый поиск, warm-start
### 6.1 OptunaHub
Платформа обмена компонентами (samplers, pruners, visualization) — например, **AutoSampler** ставится не из ядра, а через `optunahub.load_module("samplers/auto_sampler")`. Позволяет публиковать и подключать сторонние алгоритмы без раздувания ядра Optuna.
### 6.2 Storage (персистентность и параллелизм)
| Backend | Описание | Когда |
|---------|----------|-------|
| **InMemoryStorage** | По умолчанию, в ОЗУ | Один процесс, не нужна персистентность |
| **RDBStorage** | SQL-БД (SQLite / PostgreSQL / MySQL) | Персистентность, дашборд, многопроцессный/многоузловой поиск |
| **JournalStorage** | Журнальный файл (file / Redis backend) | Распределённость без полноценной СУБД; устойчивость к гонкам записи |
```python
# RDB (sqlite — для одной машины; postgres/mysql — для кластера)
study = optuna.create_study(
storage="sqlite:///hpo.db",
study_name="edgenext_hpo",
load_if_exists=True, # докинуть trial к существующему study
)
# JournalStorage (рекомендован для распределённого/NFS)
from optuna.storages import JournalStorage
from optuna.storages.journal import JournalFileBackend
storage = JournalStorage(JournalFileBackend("./hpo_journal.log"))
```
### 6.3 Распределённый / параллельный поиск
- **Внутри процесса:** `study.optimize(objective, n_jobs=-1)` — потоки (полезно, если objective отпускает GIL / I/O-bound).
- **Между процессами/узлами:** запустить тот же скрипт N раз с **общим storage** (RDB/Journal) и одинаковым `study_name` — воркеры сами координируются через БД. CLI-вариант: `optuna study optimize ... --n-jobs ...`.
### 6.4 Warm-start: `enqueue_trial` / `add_trial`
| Метод | Назначение |
|-------|-----------|
| `study.enqueue_trial(params)` | Поставить конкретный набор параметров в очередь на следующий запуск (например, известный хороший конфиг, baseline) |
| `study.add_trial(trial)` | Добавить уже завершённый trial с известным результатом (перенос истории, тёплый старт sampler) |
```python
# заранее проверить «ручной» хороший конфиг
study.enqueue_trial({"lr": 3e-4, "dropout": 0.1, "optimizer": "adamw"})
study.optimize(objective, n_trials=100)
# перенести результат внешнего эксперимента в историю
from optuna.trial import create_trial
from optuna.distributions import FloatDistribution
study.add_trial(create_trial(
params={"lr": 1e-3},
distributions={"lr": FloatDistribution(1e-5, 1e-1, log=True)},
value=0.81,
))
```
---
## 7. Визуализация
По [optuna.readthedocs.io — visualization](https://optuna.readthedocs.io/en/stable/reference/visualization/index.html). Два backend: `optuna.visualization` (**Plotly**, интерактивно, default) и `optuna.visualization.matplotlib` (статично, для статей/отчётов).
| Функция | Что показывает | Применение |
|---------|----------------|-----------|
| `plot_optimization_history` | Лучшее значение по ходу trial | Скорость сходимости, плато |
| `plot_param_importances` | Важность гиперпараметров | Какие параметры реально влияют (см. ниже) |
| `plot_slice` | Срез: параметр vs objective | Влияние одного параметра, перспективные диапазоны |
| `plot_contour` | 2D-контур пары параметров | Взаимодействие двух параметров |
| `plot_parallel_coordinate` | Параллельные координаты | Многомерные паттерны хороших trial |
| `plot_pareto_front` | Pareto-фронт | Компромиссы в multi-objective |
| `plot_intermediate_values` | Промежуточные кривые (по эпохам) | Диагностика прунинга |
| `plot_edf` | Эмпирическая функция распределения | Сравнение study между собой |
| `plot_rank` | Ранги trial по параметрам | Локализация хороших зон |
| `plot_timeline` | Временная диаграмма исполнения | Параллелизм, длительность trial |
| `plot_hypervolume_history` | Рост hypervolume | Прогресс multi-objective |
| `plot_terminator_improvement` | Оценка критерия остановки | Когда прекращать study |
**Важность параметров** (`plot_param_importances`, требует scikit-learn) считается оценщиками:
- **fANOVA** (`FanovaImportanceEvaluator`, default) — функциональный дисперсионный анализ через random forest: доля дисперсии objective, объясняемая параметром/взаимодействием.
- **PED-ANOVA** (`PedAnovaImportanceEvaluator`) — быстрый оценщик, фокусируется на важности в **хорошей** области пространства (по перцентилю); полезен для больших историй.
```python
from optuna.visualization import plot_optimization_history, plot_param_importances
plot_optimization_history(study).show()
plot_param_importances(study).show()
# с PED-ANOVA:
from optuna.importance import PedAnovaImportanceEvaluator
plot_param_importances(study, evaluator=PedAnovaImportanceEvaluator()).show()
```
---
## 8. Интеграция с PyTorch
Базовый objective с прунингом по эпохам:
```python
import optuna, torch, torch.nn as nn
def objective(trial):
# define-by-run пространство
lr = trial.suggest_float("lr", 1e-5, 1e-1, log=True)
wd = trial.suggest_float("wd", 1e-6, 1e-2, log=True)
dropout = trial.suggest_float("dropout", 0.0, 0.5, step=0.1)
n_units = trial.suggest_int("n_units", 64, 512, log=True)
model = build_model(n_units, dropout).cuda()
opt = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=wd)
for epoch in range(EPOCHS):
train_one_epoch(model, train_loader, opt)
val_acc = evaluate(model, val_loader)
trial.report(val_acc, step=epoch) # промежуточная метрика
if trial.should_prune(): # запрос pruner
raise optuna.TrialPruned()
return val_acc
study = optuna.create_study(
direction="maximize",
sampler=optuna.samplers.TPESampler(multivariate=True),
pruner=optuna.pruners.HyperbandPruner(min_resource=1, max_resource=EPOCHS),
)
study.optimize(objective, n_trials=100, gc_after_trial=True)
```
### 8.1 Callbacks и интеграции
- **Callbacks:** `study.optimize(objective, callbacks=[cb])`, где `cb(study, trial)` вызывается после каждого trial (логирование, ранний стоп всего study, чекпойнты). Готовый `MaxTrialsCallback` ограничивает число завершённых trial.
- **Готовые интеграции** (пакет `optuna-integration`): `PyTorchLightningPruningCallback`, `TorchDistributedTrial`, callbacks для Keras/XGBoost/LightGBM/MLflow/W&B.
### 8.2 TorchDistributedTrial (DDP)
При обучении в `torch.distributed` (DDP) гиперпараметры и решение о прунинге должны быть **согласованы между всеми ранками**. `TorchDistributedTrial` оборачивает trial: реальные `suggest_*`/`report`/`should_prune` исполняет rank 0 и **broadcast-ит** результат остальным процессам.
```python
import optuna
from optuna.integration import TorchDistributedTrial
import torch.distributed as dist
def objective(single_trial):
# rank0 владеет trial; остальные получают None
trial = TorchDistributedTrial(single_trial if dist.get_rank() == 0 else None)
lr = trial.suggest_float("lr", 1e-5, 1e-1, log=True) # одинаково на всех ранках
for epoch in range(EPOCHS):
train_ddp_epoch(lr, epoch)
acc = evaluate_ddp()
trial.report(acc, epoch)
if trial.should_prune(): # согласованное решение
raise optuna.TrialPruned()
return acc
```
Запускают одно study только на rank 0 (`if dist.get_rank() == 0: study.optimize(...)`), либо общий storage + барьеры — детали в `optuna-integration`.
---
## 9. Практический полный пример: image-classification + pruning (skeleton)
```python
import optuna
from optuna.trial import TrialState
import torch, torch.nn as nn, torch.nn.functional as F
from torchvision import datasets, transforms
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
EPOCHS, N_TRIALS = 15, 100
def get_loaders(batch_size):
tf = transforms.Compose([transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))])
tr = datasets.MNIST("./data", train=True, download=True, transform=tf)
va = datasets.MNIST("./data", train=False, download=True, transform=tf)
return (torch.utils.data.DataLoader(tr, batch_size=batch_size, shuffle=True),
torch.utils.data.DataLoader(va, batch_size=256))
def build_model(trial):
n_layers = trial.suggest_int("n_layers", 1, 3)
layers, in_f = [], 28 * 28
for i in range(n_layers):
out_f = trial.suggest_int(f"units_l{i}", 64, 512, log=True)
p = trial.suggest_float(f"dropout_l{i}", 0.0, 0.5, step=0.1)
layers += [nn.Linear(in_f, out_f), nn.ReLU(), nn.Dropout(p)]
in_f = out_f
layers.append(nn.Linear(in_f, 10))
return nn.Sequential(nn.Flatten(), *layers)
def objective(trial):
lr = trial.suggest_float("lr", 1e-5, 1e-1, log=True)
bs = trial.suggest_categorical("batch_size", [32, 64, 128])
opt_name = trial.suggest_categorical("optimizer", ["Adam", "SGD", "AdamW"])
model = build_model(trial).to(DEVICE)
optimizer = getattr(torch.optim, opt_name)(model.parameters(), lr=lr)
train_loader, val_loader = get_loaders(bs)
for epoch in range(EPOCHS):
model.train()
for x, y in train_loader:
x, y = x.to(DEVICE), y.to(DEVICE)
optimizer.zero_grad()
F.cross_entropy(model(x), y).backward()
optimizer.step()
model.eval(); correct = 0
with torch.no_grad():
for x, y in val_loader:
x, y = x.to(DEVICE), y.to(DEVICE)
correct += (model(x).argmax(1) == y).sum().item()
acc = correct / len(val_loader.dataset)
trial.report(acc, epoch) # отдать метрику pruner-у
if trial.should_prune(): # ранняя остановка
raise optuna.TrialPruned()
return acc
if __name__ == "__main__":
study = optuna.create_study(
direction="maximize",
sampler=optuna.samplers.TPESampler(multivariate=True, seed=42),
pruner=optuna.pruners.MedianPruner(n_startup_trials=5, n_warmup_steps=3),
storage="sqlite:///mnist_hpo.db",
study_name="mnist", load_if_exists=True,
)
study.optimize(objective, n_trials=N_TRIALS, timeout=3600, gc_after_trial=True)
pruned = study.get_trials(deepcopy=False, states=[TrialState.PRUNED])
complete = study.get_trials(deepcopy=False, states=[TrialState.COMPLETE])
print(f"pruned={len(pruned)} complete={len(complete)}")
print("best acc:", study.best_value, "| params:", study.best_params)
```
---
## 10. Шпаргалка: «когда какой sampler + pruner»
| Сценарий | Sampler | Pruner | Обоснование |
|----------|---------|--------|-------------|
| Универсальный старт (DL, смешанное пространство) | **TPESampler** (`multivariate=True`) | **MedianPruner** | Надёжный default, ловит зависимости параметров |
| Дорогой objective, малый бюджет (≲500), непрерывные параметры | **GPSampler** | PatientPruner / без прунинга | Максимальная sample-efficiency BO |
| Большой бюджет (>1000), непрерывное пространство высокой размерности | **CmaEsSampler** | **SuccessiveHalvingPruner (ASHA)** | Эффективная эволюционная стратегия + дешёвый прунинг |
| Не знаю бюджет на trial, длинное обучение | TPESampler | **HyperbandPruner** | Несколько brackets закрывают разный explore/exploit |
| Multi-objective: accuracy ↑ vs params/latency ↓ (23 цели) | **NSGAIISampler** (+`constraints_func`) | — (multi-obj не прунится) | Строит Pareto-фронт; поддержка ограничений |
| Many-objective (≥4 целей) | **NSGAIIISampler** | — | Reference-points масштабируют many-objective |
| Шумные кривые обучения (поздняя сходимость) | TPESampler / GPSampler | **PatientPruner** (обёртка) | Не режет временные просадки |
| Отсев расходящихся trial (NaN, взрыв loss) | любой | **ThresholdPruner** | Жёсткий абсолютный порог |
| Objective усредняется по многим примерам/фолдам | TPESampler | **WilcoxonPruner** | Статистически корректное сравнение с лучшим |
| Малое дискретное пространство, нужна полнота | **GridSampler / BruteForceSampler** | MedianPruner | Исчерпывающий перебор |
| Стартовая разведка / baseline | **QMC / RandomSampler** | NopPruner | Равномерное покрытие, эталон |
| Не хочу выбирать вручную, есть `optunahub` | **AutoSampler** | — (multi-obj) / MedianPruner (single) | Автовыбор TPE/GP/NSGA-II/III под задачу |
| Распределённый кластер, много воркеров | TPESampler / RandomSampler + RDB/Journal storage | **SuccessiveHalvingPruner (ASHA)** | Асинхронность дружит с общим storage |
> Практический дефолт для одной цели в edge-CV задачах (как EdgeNeXt-style сети): **TPESampler(multivariate=True) + HyperbandPruner**, storage = SQLite, `gc_after_trial=True`. Для совместной оптимизации accuracy и числа параметров/latency — **NSGAIISampler + `constraints_func`** и анализ `plot_pareto_front`.

266
_research/SECTION_umap.md Normal file
View File

@@ -0,0 +1,266 @@
# UMAP — кластеризация и визуализация признаков по классам
> Раздел методички. Научный руководитель — мнс Павленко Б.В.
> Задача-носитель: анализ эмбеддингов классификатора **EdgeNeXt** на датасете жестов **Naruto Sign**.
---
## 1. Что такое UMAP и интуиция
**UMAP** (Uniform Manifold Approximation and Projection) — алгоритм нелинейного снижения размерности, предложенный в [McInnes, Healy, Melville, 2018, arXiv:1802.03426](https://arxiv.org/abs/1802.03426). Теоретическая база — риманова геометрия и алгебраическая топология: предполагается, что данные лежат на многообразии (manifold) равномерно распределённой плотности, а локальная метрика искажается так, чтобы это допущение выполнялось.
**Интуиция (два шага):**
1. **Построение взвешенного графа соседства в высокой размерности.** Для каждой точки $x_i$ берутся её $k = $ `n_neighbors` ближайших соседей. Вес ребра — «вероятность» связи, спадающая с расстоянием. Локальный радиус нормируется на расстояние до 1-го соседа $\rho_i$ (так гарантируется связность) и масштаб $\sigma_i$ (калибруется так, чтобы сумма весов соответствовала $\log_2 k$):
$$
w_{i\to j} = \exp\!\left(-\frac{\max(0,\; d(x_i,x_j) - \rho_i)}{\sigma_i}\right)
$$
Ориентированные веса симметризуются (fuzzy-union): $w_{ij} = w_{i\to j} + w_{j\to i} - w_{i\to j}\,w_{j\to i}$. Получается «нечёткий топологический граф».
2. **Оптимизация низкоразмерного вложения.** Ищется расположение точек $y_i \in \mathbb{R}^{d}$ (`n_components`), при котором граф соседства в низкой размерности максимально похож на исходный. Минимизируется кросс-энтропия между нечёткими множествами рёбер; притяжение тянет соседей друг к другу, отталкивание (через negative sampling) разводит несоседей. Низкоразмерное ядро задаётся семейством $\frac{1}{1 + a\,\|y_i-y_j\|^{2b}}$, где $a,b$ подбираются под `min_dist`.
Инициализация по умолчанию — спектральная (Laplacian eigenmaps), что важно для сохранения глобальной структуры (см. ниже).
### Сравнение PCA / t-SNE / UMAP
| Критерий | PCA | t-SNE | UMAP |
|---|---|---|---|
| Тип | линейный | нелинейный | нелинейный (топологический) |
| Локальная структура | слабо | **очень хорошо** | **очень хорошо** |
| Глобальная структура | **хорошо** (дисперсия) | слабо | умеренно лучше t-SNE* |
| Сложность | $O(n d^2)$ | $\sim O(n^2)$ (Barnes-Hut $O(n\log n)$) | $\approx O(n^{1.14})$, на практике $O(n\log n)$ |
| Скорость на $10^5$$10^6$ точек | высокая | низкая | высокая |
| Стохастичность | детерминирован | стохастичен | стохастичен (нужен `random_state`) |
| Размерность выхода | любая | практично 23 | любая (хорошо масштабируется) |
| Интерпретируемость расстояний | оси = главные компоненты, расстояния значимы | **расстояния/размеры НЕ значимы** | **абсолютные расстояния/размеры НЕ значимы** |
| Глобальные оси | да | нет | нет |
\* Важная оговорка: преимущество UMAP по глобальной структуре во многом обусловлено спектральной инициализацией, а не самим алгоритмом оптимизации. При случайной инициализации UMAP сохраняет глобальную структуру не лучше t-SNE — см. [Kobak & Linderman, Nature Biotechnology 2021](https://www.nature.com/articles/s41587-020-00809-z) и интерактивный разбор [«Understanding UMAP», pair-code](https://pair-code.github.io/understanding-umap/).
**Практический вывод для нашей задачи:** PCA — быстрый baseline и шаг предобработки (денойзинг до 50100 компонент); t-SNE/UMAP — визуальный разведочный анализ; UMAP предпочтителен из-за скорости, масштабируемости и возможности давать промежуточные вложения для кластеризации.
---
## 2. Ключевые гиперпараметры UMAP
Определения и эффекты — по [официальной документации umap-learn (Basic Parameters)](https://umap-learn.readthedocs.io/en/latest/parameters.html).
| Параметр | По умолч. | Что контролирует | Эффект |
|---|---|---|---|
| `n_neighbors` | 15 | баланс локальное ↔ глобальное | малые (25) — тонкая локальная детализация, рвёт глобальную картину; большие (50200) — общая структура за счёт деталей |
| `min_dist` | 0.1 | минимальное расстояние между точками в выходе | малые (0.00.1) — плотные «комки», чёткие границы (для кластеризации); большие (0.50.99) — равномернее, для топологии/визуала |
| `n_components` | 2 | размерность вложения | 23 — визуализация; 1050 — препроцессинг под кластеризацию (UMAP хорошо масштабируется по размерности, в отличие от t-SNE) |
| `metric` | `euclidean` | метрика в исходном пространстве | для **L2-нормированных** эмбеддингов `cosine` эквивалентен углу; см. ниже |
| `random_state` | `None` | фиксация ГСЧ | задание делает результат воспроизводимым (но отключает параллелизм → медленнее) |
**`n_neighbors` (локальное vs глобальное).** Это число точек, формирующих локальную окрестность. Малое значение — UMAP «смотрит» только на ближайшее окружение; большое — усредняет по широкой области.
**`min_dist` (плотность кластеров).** Управляет ТОЛЬКО раскладкой в выходном пространстве (через $a,b$), не топологией графа. Для последующей кластеризации авторы рекомендуют `min_dist = 0.0`.
**`metric`.** Для эмбеддингов CNN после **L2-нормализации** косинусное расстояние монотонно связано с евклидовым:
$$
\|u - v\|_2^2 = 2 - 2\cos(u,v),\quad \text{при } \|u\|=\|v\|=1.
$$
Поэтому на нормированных признаках `metric='euclidean'` и `metric='cosine'` дают близкие, но не идентичные результаты (различие в нормировках весов и `n_neighbors`-калибровке). Рекомендация: для эмбеддингов брать `metric='cosine'` (или сначала L2-нормировать и оставить `euclidean`).
**`random_state`.** UMAP стохастичен (negative sampling, инициализация). Для отчётов/методички ОБЯЗАТЕЛЬНО фиксировать seed; форма «облака» при разных seed меняется, выводы — не должны.
---
## 3. Пайплайн: извлечение признаков EdgeNeXt → UMAP → раскраска по классам
Логика: `forward_features` → global pooling → L2-norm → матрица $[N, D]$ → UMAP → 2D scatter, цвет = истинный класс Naruto Sign.
```python
# deps: torch, timm, umap-learn, scikit-learn, matplotlib, numpy
import numpy as np
import torch
import torch.nn.functional as F
import timm
import umap
import matplotlib.pyplot as plt
device = "cuda" if torch.cuda.is_available() else "cpu"
# --- 1. EdgeNeXt feature extractor (pooled embedding, no classifier head) ---
# num_classes=0 -> timm returns pooled features from forward()
model = timm.create_model("edgenext_small", pretrained=True, num_classes=0)
model.eval().to(device)
@torch.no_grad()
def extract_embeddings(loader):
"""Return L2-normalized embeddings [N, D] and integer labels [N]."""
feats, labels = [], []
for imgs, ys in loader: # loader yields (image, class_idx)
imgs = imgs.to(device, non_blocking=True)
emb = model.forward_features(imgs) # spatial feature map [B, C, H, W]
emb = model.forward_head(emb, pre_logits=True) # pooled embedding [B, D]
emb = F.normalize(emb, p=2, dim=1) # L2-norm: cosine == euclidean geometry
feats.append(emb.cpu().numpy())
labels.append(ys.numpy())
return np.concatenate(feats), np.concatenate(labels)
X, y = extract_embeddings(val_loader) # X: [N, D], y: [N]
print("embeddings:", X.shape)
# --- 2. UMAP -> 2D for VISUALIZATION (seed fixed!) ---
reducer_2d = umap.UMAP(
n_neighbors=15,
min_dist=0.1,
n_components=2,
metric="cosine", # X already L2-normalized
random_state=42, # reproducibility
)
emb_2d = reducer_2d.fit_transform(X) # [N, 2]
# --- 3. Scatter colored by TRUE class ---
class_names = val_loader.dataset.classes # list[str], len == n_classes
fig, ax = plt.subplots(figsize=(9, 8))
sc = ax.scatter(emb_2d[:, 0], emb_2d[:, 1], c=y, cmap="tab20", s=8, alpha=0.7)
handles, _ = sc.legend_elements(num=len(class_names))
ax.legend(handles, class_names, title="Naruto Sign", loc="best",
fontsize=7, ncol=2, markerscale=1.5)
ax.set_title("EdgeNeXt embeddings — UMAP 2D (seed=42)")
ax.set_xlabel("UMAP-1"); ax.set_ylabel("UMAP-2")
ax.set_xticks([]); ax.set_yticks([]) # axes are not interpretable
plt.tight_layout()
plt.savefig("umap_edgenext_naruto.png", dpi=200)
```
> Примечание: точные имена методов извлечения признаков зависят от версии `timm`. Для EdgeNeXt подходит связка `forward_features` → `forward_head(..., pre_logits=True)`; альтернатива — `timm.create_model(..., num_classes=0, global_pool='avg')` и затем просто `model(imgs)`.
---
## 4. Кластеризация в пространстве признаков / на UMAP
**Главная рекомендация авторов** ([umap-learn → Clustering](https://umap-learn.readthedocs.io/en/latest/clustering.html)): кластеризовать НЕ на 2D-картинке, а на **промежуточном вложении** (например, 10 компонент), либо параметрами, заточенными под кластеризацию.
**Параметры под кластеризацию (рекомендация документации):**
| Параметр | Визуализация | Кластеризация |
|---|---|---|
| `n_components` | 2 | 10 (исследовать диапазон) |
| `n_neighbors` | 15 | 30 (больше — меньше шумовых «разрывов») |
| `min_dist` | 0.1 | 0.0 (плотные кластеры) |
**Два алгоритма:**
- **k-means** — требует заранее знать $k$, ищет сферические кластеры одинаковой плотности, относит ВСЕ точки (нет «шума»). Уместен, если число классов известно (а здесь оно известно — это число жестов Naruto Sign).
- **HDBSCAN** — плотностный, не требует $k$, выделяет шум/выбросы, работает с кластерами произвольной формы и разной плотности. Авторы UMAP в туториале демонстрируют связку **UMAP(intermediate) + HDBSCAN**.
```python
import hdbscan
from sklearn.cluster import KMeans
# UMAP -> intermediate (NOT 2D) embedding for clustering
reducer_clu = umap.UMAP(
n_neighbors=30, min_dist=0.0, n_components=10,
metric="cosine", random_state=42,
)
emb_clu = reducer_clu.fit_transform(X) # [N, 10]
# HDBSCAN (no need to set k; label -1 == noise)
hdb = hdbscan.HDBSCAN(min_cluster_size=15, min_samples=5)
labels_hdb = hdb.fit_predict(emb_clu)
# k-means with known number of gesture classes
k = len(np.unique(y))
labels_km = KMeans(n_clusters=k, n_init=10, random_state=42).fit_predict(emb_clu)
```
**Риски кластеризации прямо на 2D:**
1. UMAP-2D не сохраняет плотность полностью — плотностные алгоритмы (HDBSCAN) делают ложные выводы.
2. UMAP может создавать **ложные разрывы** («false tears») внутри кластеров → переоценка числа кластеров.
3. Геометрия 2D искажена сильнее, чем у 1050-мерного вложения; 2D — только для глаз, не для алгоритма.
Авторы прямо предупреждают, что использование UMAP перед кластеризацией «несколько спорно и требует осторожности».
---
## 5. Метрики качества
**Внутренние (без меток) — оценивают саму геометрию кластеров:**
| Метрика | Диапазон | Лучше | Идея |
|---|---|---|---|
| Silhouette | $[-1, 1]$ | выше | $(b-a)/\max(a,b)$: внутрикласт. сплочённость $a$ vs разделимость $b$ |
| Davies-Bouldin | $[0, \infty)$ | ниже | средн. отношение внутрикласт. разброса к межкласт. расстоянию |
**Внешние (с истинными метками) — согласованность кластеров с классами Naruto Sign:**
| Метрика | Диапазон | Adj. for chance | Основа | Источник |
|---|---|---|---|---|
| ARI | $[-0.5, 1]$ | да | подсчёт пар | [Hubert & Arabie 1985] |
| NMI | $[0, 1]$ | **нет** | теория информации | [scikit-learn clustering](https://scikit-learn.org/stable/modules/clustering.html) |
| AMI | $\sim[0, 1]$ | да | теория информации | [Vinh, Epps, Bailey, JMLR 2010](https://jmlr.csail.mit.edu/papers/volume17/15-627/15-627) |
- **ARI** и **AMI** скорректированы на случайность (≈0 для случайной разметки) — предпочтительны как итоговые. **NMI** не скорректирован: при малом числе точек/большом числе кластеров завышается.
- Формула AMI: $\mathrm{AMI} = \dfrac{\mathrm{MI} - \mathbb{E}[\mathrm{MI}]}{\overline{H} - \mathbb{E}[\mathrm{MI}]}$, где $\overline{H}$ — среднее энтропий разбиений.
- Подробно о выборе ARI vs AMI: [Vinh et al. 2010](https://jmlr.csail.mit.edu/papers/volume17/15-627/15-627).
**Trustworthiness** ([sklearn.manifold.trustworthiness](https://scikit-learn.org/stable/modules/generated/sklearn.manifold.trustworthiness.html)) — мера сохранения локальной структуры при снижении размерности: $\in [0,1]$, выше = меньше ложных близких соседей в проекции. Применяется к самому вложению (а не к кластеризации).
```python
from sklearn.metrics import (silhouette_score, davies_bouldin_score,
adjusted_rand_score, normalized_mutual_info_score,
adjusted_mutual_info_score)
from sklearn.manifold import trustworthiness
# internal (geometry); exclude HDBSCAN noise points (label == -1)
mask = labels_hdb != -1
print("silhouette :", silhouette_score(emb_clu[mask], labels_hdb[mask]))
print("davies-bouldin:", davies_bouldin_score(emb_clu[mask], labels_hdb[mask]))
# external (agreement with true Naruto Sign labels)
print("ARI:", adjusted_rand_score(y, labels_km))
print("NMI:", normalized_mutual_info_score(y, labels_km))
print("AMI:", adjusted_mutual_info_score(y, labels_km))
# embedding quality vs original feature space
print("trustworthiness:", trustworthiness(X, emb_2d, n_neighbors=15))
```
---
## 6. Что покажет анализ и какие гипотезы мотивирует
- **Хорошо отделимые классы** — компактные, изолированные «острова» на UMAP; высокие per-class silhouette; кластер HDBSCAN ≈ один класс. Ожидание: классификатор EdgeNeXt уверенно их различает.
- **Путающиеся классы** — перекрывающиеся облака, точки одного класса распределены по нескольким кластерам или один кластер содержит метки 23 классов. Это прямой предиктор **ошибок классификатора**: перекрытие в эмбеддинге → confusion в матрице ошибок.
- **Сверка с confusion matrix:** пары классов с высоким перекрытием в UMAP должны коррелировать с off-diagonal-массой матрицы ошибок. Совпадение валидирует, что причина ошибок — в признаковом представлении, а не в голове-классификаторе.
**Мотивируемые гипотезы (для HPO/обучения EdgeNeXt):**
1. **Целевая аугментация трудных классов** — для перекрывающихся жестов усилить аугментации, разводящие их (ракурс/освещение/фон), чтобы увеличить межклассовую дистанцию.
2. **Class-balanced / hard-class семплирование** — повысить вес/частоту трудноразделимых классов в батче.
3. **Метрик-обучение** — добавить contrastive/triplet/ArcFace-член, явно растягивающий перекрывающиеся пары.
4. **Ревизия разметки** — точки одного класса в чужом кластере могут быть mislabeled / out-of-distribution кадрами.
> Важно: UMAP здесь — **инструмент генерации гипотез**, а не доказательство. Любой вывод о разделимости подтверждается метриками (§5) и confusion matrix.
---
## 7. Предостережения
1. **Не интерпретировать абсолютные расстояния.** Расстояние между двумя кластерами на UMAP-картинке НЕ отражает их реальную близость в исходном пространстве. Глобальные дистанции искажены ([umap-learn parameters](https://umap-learn.readthedocs.io/en/latest/parameters.html), [pair-code](https://pair-code.github.io/understanding-umap/)).
2. **Не интерпретировать размеры/плотность кластеров.** UMAP, как и t-SNE, не сохраняет плотность полностью; размер «облака» не равен дисперсии класса.
3. **Стохастичность — фиксировать seed.** Без `random_state` форма меняется от запуска к запуску. Фиксируйте seed и в идеале проверяйте устойчивость выводов на 23 разных seed.
4. **Параметры формируют картину.** Сильное изменение `n_neighbors`/`min_dist` радикально меняет вид; нельзя подбирать параметры под желаемый результат. Показывайте использованные значения.
5. **Ложные разрывы.** UMAP может «разорвать» один класс на несколько сгустков — это артефакт, не subclass. Проверяйте на промежуточном вложении и метриками.
6. **Не «доказывать» кластерами то, чего нет.** Наличие визуальных сгустков ≠ наличие классов. Кластеризация на 2D особенно подвержена артефактам — кластеризуйте на 1050-мерном вложении (§4) и валидируйте ARI/AMI/silhouette.
7. **UMAP перед кластеризацией — спорно.** Сами авторы советуют делать это «с осторожностью» и сравнивать с кластеризацией в исходном (или PCA-сжатом) пространстве как sanity-check.
---
### Источники
- [McInnes, Healy, Melville. UMAP: Uniform Manifold Approximation and Projection for Dimension Reduction. arXiv:1802.03426 (2018)](https://arxiv.org/abs/1802.03426)
- [umap-learn — Basic Parameters](https://umap-learn.readthedocs.io/en/latest/parameters.html)
- [umap-learn — Using UMAP for Clustering](https://umap-learn.readthedocs.io/en/latest/clustering.html)
- [Kobak & Linderman. Initialization is critical for preserving global data structure. Nature Biotechnology (2021)](https://www.nature.com/articles/s41587-020-00809-z)
- [pair-code — Understanding UMAP](https://pair-code.github.io/understanding-umap/)
- [Vinh, Epps, Bailey. Information Theoretic Measures for Clusterings Comparison. JMLR (2010)](https://jmlr.csail.mit.edu/papers/volume17/15-627/15-627)
- [scikit-learn — Clustering metrics](https://scikit-learn.org/stable/modules/clustering.html)
- [scikit-learn — trustworthiness](https://scikit-learn.org/stable/modules/generated/sklearn.manifold.trustworthiness.html)