commit e622129c30269e09296fcc57b8f57ebc0ee244fc Author: Pavlenko_B Date: Tue Jun 30 15:17:40 2026 +0300 init commit diff --git a/_research/SECTION_classification.md b/_research/SECTION_classification.md new file mode 100644 index 0000000..7d30044 --- /dev/null +++ b/_research/SECTION_classification.md @@ -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/) diff --git a/_research/SECTION_edgenext.md b/_research/SECTION_edgenext.md new file mode 100644 index 0000000..8913ab0 --- /dev/null +++ b/_research/SECTION_edgenext.md @@ -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` (имя функции) | Доступные веса (тег) | 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) diff --git a/_research/SECTION_freezing.md b/_research/SECTION_freezing.md new file mode 100644 index 0000000..bd54cfd --- /dev/null +++ b/_research/SECTION_freezing.md @@ -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/) diff --git a/_research/SECTION_hpo_methods.md b/_research/SECTION_hpo_methods.md new file mode 100644 index 0000000..65fe3ae --- /dev/null +++ b/_research/SECTION_hpo_methods.md @@ -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) diff --git a/_research/SECTION_mona.md b/_research/SECTION_mona.md new file mode 100644 index 0000000..2d05750 --- /dev/null +++ b/_research/SECTION_mona.md @@ -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)). diff --git a/_research/SECTION_naruto_dataset.md b/_research/SECTION_naruto_dataset.md new file mode 100644 index 0000000..2963231 --- /dev/null +++ b/_research/SECTION_naruto_dataset.md @@ -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) diff --git a/_research/SECTION_optuna.md b/_research/SECTION_optuna.md new file mode 100644 index 0000000..41cad2b --- /dev/null +++ b/_research/SECTION_optuna.md @@ -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`. diff --git a/_research/SECTION_umap.md b/_research/SECTION_umap.md new file mode 100644 index 0000000..c7d7210 --- /dev/null +++ b/_research/SECTION_umap.md @@ -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)