init commit
This commit is contained in:
379
_research/SECTION_classification.md
Normal file
379
_research/SECTION_classification.md
Normal 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/)
|
||||||
281
_research/SECTION_edgenext.md
Normal file
281
_research/SECTION_edgenext.md
Normal 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) это критично: см. §5–6.
|
||||||
|
|
||||||
|
> Ключевой тезис: **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 — малый датасет) модель на 1–6M параметров с сильным 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.3–5.6M | 86–88M |
|
||||||
|
| Риск переобучения на малых данных | **низкий** | высокий |
|
||||||
|
| ImageNet-prior | сильный (IN-1k/USI/21k) | сильный |
|
||||||
|
| Стоимость fine-tune на RTX 4090 | минуты, большой batch | дольше, меньший batch |
|
||||||
|
| Inference latency | edge-уровень (19–49 ms Jetson Nano) | неприемлемо для edge |
|
||||||
|
|
||||||
|
Тезисно:
|
||||||
|
|
||||||
|
1. **Мало параметров → меньше переобучение.** Naruto Sign — малый датасет; модель на 1–6M параметров не «запоминает» обучающую выборку так агрессивно, как сеть на 80M+. Соотношение ёмкость/данные сбалансировано.
|
||||||
|
2. **Сильный ImageNet-prior.** Признаки краёв/текстур/частей объектов уже выучены — для распознавания конфигураций кистей рук (печатей) это переносится напрямую, дообучение быстро сходится.
|
||||||
|
3. **Быстро на одной RTX 4090.** Малая модель → большой batch (256–512), полный 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** | голова + последняя(-ие) стадия(-и); ранние стадии заморожены | малый датасет, нужна частичная адаптация признаков | стадии 3–4 |
|
||||||
|
| **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 1–2) или адаптеры.
|
||||||
|
|
||||||
|
Источники по 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)
|
||||||
268
_research/SECTION_freezing.md
Normal file
268
_research/SECTION_freezing.md
Normal 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 (1–2 верхние стадии) + сильная регуляризация/аугментации. LP-FT. |
|
||||||
|
| **Средний** (10²–10⁴) | Partial freeze (последние 1–2 стадии) с discriminative lr | Partial freeze (2–3 стадии) либо 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** общих признаков. Типичная схема по эпохам:
|
||||||
|
|
||||||
|
```
|
||||||
|
эпоха 0–2: обучаем только голову (backbone frozen)
|
||||||
|
эпоха 3–5: + последняя стадия 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.05–0.1% | [Ben-Zaken et al., 2021](https://huggingface.co/papers/2106.10199) |
|
||||||
|
| **Adapters** | вставные bottleneck-MLP внутри блоков | ~0.5–5% | [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/)
|
||||||
317
_research/SECTION_hpo_methods.md
Normal file
317
_research/SECTION_hpo_methods.md
Normal 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:281–305](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:2546–2554](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):1–9](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:281–305.](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:2546–2554.](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):1–9.](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
258
_research/SECTION_mona.md
Normal 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.1–5 %). Семейства 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-задачах** |
|
||||||
|
| Доля параметров | ~2–3 % | ~2.3–3.6 % | ~2.5–4.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.5–5 % параметров — меньше степеней свободы → меньше шансов запомнить шум малой выборки.
|
||||||
|
- **Доменная адаптация через 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)).
|
||||||
261
_research/SECTION_naruto_dataset.md
Normal file
261
_research/SECTION_naruto_dataset.md
Normal 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` (±10–15°) | ⚠️ Умеренно | Лёгкий наклон ОК; помнить, что часть кадров уже повёрнута автором (§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
558
_research/SECTION_optuna.md
Normal 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 на гладких непрерывных задачах; жадноват | Универсальный выбор, 100–1000 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, малый бюджет 10–100 trial |
|
||||||
|
| **CmaEsSampler** | CMA-ES (эволюционная стратегия) | Очень эффективен на непрерывных задачах большой размерности | Не поддерживает multi-objective; плохо с категориями; не любит условные пространства | Непрерывные пространства, бюджет 1000–10000 trial |
|
||||||
|
| **NSGAIISampler** | NSGA-II (генетический, multi-objective) | Pareto-фронт для 2–3 целей; масштабируется по числу trial; поддержка `constraints_func` | Не sample-efficient (нужно много trial); хуже many-objective | Multi-objective (2–3 цели), большой бюджет |
|
||||||
|
| **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)$ | 100–1000 |
|
||||||
|
| GPSampler / BoTorchSampler | $O(n^3)$ | ≲500 / 10–100 |
|
||||||
|
| CmaEsSampler | $O(d^3)$ | 1000–10000 |
|
||||||
|
| NSGA-II / NSGA-III | $O(mp^2)$ | 100–10000 |
|
||||||
|
|
||||||
|
### 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.2–4.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** | Ускорение вычислений до **6–800×**; практично до 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 ↓ (2–3 цели) | **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
266
_research/SECTION_umap.md
Normal 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`) |
|
||||||
|
| Размерность выхода | любая | практично 2–3 | любая (хорошо масштабируется) |
|
||||||
|
| Интерпретируемость расстояний | оси = главные компоненты, расстояния значимы | **расстояния/размеры НЕ значимы** | **абсолютные расстояния/размеры НЕ значимы** |
|
||||||
|
| Глобальные оси | да | нет | нет |
|
||||||
|
|
||||||
|
\* Важная оговорка: преимущество 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 и шаг предобработки (денойзинг до 50–100 компонент); t-SNE/UMAP — визуальный разведочный анализ; UMAP предпочтителен из-за скорости, масштабируемости и возможности давать промежуточные вложения для кластеризации.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Ключевые гиперпараметры UMAP
|
||||||
|
|
||||||
|
Определения и эффекты — по [официальной документации umap-learn (Basic Parameters)](https://umap-learn.readthedocs.io/en/latest/parameters.html).
|
||||||
|
|
||||||
|
| Параметр | По умолч. | Что контролирует | Эффект |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `n_neighbors` | 15 | баланс локальное ↔ глобальное | малые (2–5) — тонкая локальная детализация, рвёт глобальную картину; большие (50–200) — общая структура за счёт деталей |
|
||||||
|
| `min_dist` | 0.1 | минимальное расстояние между точками в выходе | малые (0.0–0.1) — плотные «комки», чёткие границы (для кластеризации); большие (0.5–0.99) — равномернее, для топологии/визуала |
|
||||||
|
| `n_components` | 2 | размерность вложения | 2–3 — визуализация; 10–50 — препроцессинг под кластеризацию (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 искажена сильнее, чем у 10–50-мерного вложения; 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 уверенно их различает.
|
||||||
|
- **Путающиеся классы** — перекрывающиеся облака, точки одного класса распределены по нескольким кластерам или один кластер содержит метки 2–3 классов. Это прямой предиктор **ошибок классификатора**: перекрытие в эмбеддинге → 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 и в идеале проверяйте устойчивость выводов на 2–3 разных seed.
|
||||||
|
4. **Параметры формируют картину.** Сильное изменение `n_neighbors`/`min_dist` радикально меняет вид; нельзя подбирать параметры под желаемый результат. Показывайте использованные значения.
|
||||||
|
5. **Ложные разрывы.** UMAP может «разорвать» один класс на несколько сгустков — это артефакт, не subclass. Проверяйте на промежуточном вложении и метриками.
|
||||||
|
6. **Не «доказывать» кластерами то, чего нет.** Наличие визуальных сгустков ≠ наличие классов. Кластеризация на 2D особенно подвержена артефактам — кластеризуйте на 10–50-мерном вложении (§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)
|
||||||
Reference in New Issue
Block a user