Files
caption-test/refactor_v3_plan.md

942 lines
38 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Шаг 2 — План разделения с диффами
> Решения, на которых строится этот план:
> 1. **Плоские `.gin`** без `include` — каждый эксперимент = самодостаточный набор файлов
> 2. **`TrainConfigGTAUAV` разделяем сразу**
> 3. **Отдельные `ModelsConfig`-классы на каждый бэкбон** (DINOv3, StripNet, SOFIAv1, SOFIAv71)
> 4. **Скрипты переводим на gin**
>
> Этот документ — **план**, не финальный полный набор диффов. Он отвечает на вопрос «какие конфиг-классы будут, какие гин-файлы, какая иерархия пресетов». Для каждого нового файла — содержимое. Для каждого правимого файла — diff. Это ответ на вопрос «что делать с конфигом», без переписывания `Trainer` (это будет следующим шагом).
---
## Часть A — Раскладка `TrainConfigGTAUAV` по 4 «универсальным» конфигам
`TrainConfigGTAUAV` содержит 50+ полей. Разделяю их по принципу из «Рекомендуемые_gin-config_категории.md» (одна ось изменчивости = один конфиг):
| Поле текущего `TrainConfigGTAUAV` | Куда едет |
|---|---|
| `train_json`, `test_json`, `rgb_root`, `caption_root`, `filter_meta`, `output_dir`, `resume_from`, `epochs`, `eval_every`, `warmup_epochs`, `seed` | **`PipelineConfig`** |
| `batch_size`, `num_workers`, `grad_accum_steps`, `use_amp`, `gradient_checkpointing`, `device` | **`HardwareConfig`** |
| `loss_type`, `tau_init`, `label_smoothing`, `learnable_temperature`, `weight_q2g`, `weight_g2q`, `neg_bank_size`, `learning_rate`, `text_lr_factor`, `weight_decay`, `grad_clip`, `sampler_type`, `dss_*`, `use_mutex_sampler` | **`TrainingConfig`** |
| `use_wandb`, `use_tb`, `wandb_project`, `wandb_run_name`, `wandb_entity`, `log_grad_norms`, `use_gradcam`, `gradcam_every`, `gradcam_samples`, `use_profiler`, `profiler_warmup`, `profiler_active` | **`TrackingConfig`** |
| `dino_web_path`, `dino_sat_path`, `lrsclip_path`, `init_gate`, `baseline_mode`, `shared_encoder`, `mona_bottleneck`, `mona_last_n_blocks`, `backbone`, `stripnet_path`, `stripnet_mona_last_n_stages`, `stripnet_freeze`, `stripnet_backbone_lr_factor` | **`Models*Config`** (см. Часть B) |
**Итого 4 «универсальных» конфига** (Pipeline / Hardware / Training / Tracking) + плюс семейство Models-классов из Части B.
---
## Часть B — Семейство `Models*Config` (по бэкбону)
Каждый бэкбон → собственный gin-configurable класс. **Один на эксперимент** — какой именно загружается, определяется тем, какой `models_*.gin` положен в директорию пресета.
### `ModelsCommonConfig` — общие поля
Поля, нужные **всем** бэкбонам:
```python
@gin.configurable
class ModelsCommonConfig:
"""Common architecture switches shared by all backbones."""
def __init__(
self,
backbone: str = "dinov3", # 'dinov3' | 'stripnet' | 'sofia_v1' | 'sofia_v71'
baseline_mode: bool = False, # text disabled, gate forced 1.0
init_gate: float = 0.7,
lrsclip_path: str = "nn_models/LRSCLIP/DGTRS-CLIP-ViT-L-14.pt",
) -> None:
self.backbone = backbone
self.baseline_mode = baseline_mode
self.init_gate = init_gate
self.lrsclip_path = lrsclip_path
```
### `DINOv3ModelsConfig`
```python
@gin.configurable
class DINOv3ModelsConfig:
def __init__(
self,
dino_web_path: str = "nn_models/DINO_WEB/dinov3-vitl16-pretrain-lvd1689m.pth",
dino_sat_path: str = "nn_models/DINO_SAT/model.safetensors",
shared_encoder: bool = True,
mona_bottleneck: int = 64,
mona_last_n_blocks: int = 12,
) -> None:
self.dino_web_path = dino_web_path
self.dino_sat_path = dino_sat_path
self.shared_encoder = shared_encoder
self.mona_bottleneck = mona_bottleneck
self.mona_last_n_blocks = mona_last_n_blocks
```
### `StripNetModelsConfig`
```python
@gin.configurable
class StripNetModelsConfig:
def __init__(
self,
stripnet_path: str = "nn_models/STRIPNET/stripnet_s.pth",
stripnet_freeze: bool = True,
stripnet_mona_last_n_stages: int = 2,
stripnet_backbone_lr_factor: float = 0.1,
) -> None:
self.stripnet_path = stripnet_path
self.stripnet_freeze = stripnet_freeze
self.stripnet_mona_last_n_stages = stripnet_mona_last_n_stages
self.stripnet_backbone_lr_factor = stripnet_backbone_lr_factor
```
### `SOFIAv1ModelsConfig`
Покрывает всё, что сейчас в `src/models/sofia_v1/config.py::SOFIAv1Config`:
```python
@gin.configurable
class SOFIAv1ModelsConfig:
def __init__(
self,
# Backbone.
variant: str = "small", # 'tiny_tiny' | 'tiny' | 'small' | 'small_v2'
in_channels: int = 3,
input_size: int = 256,
dcn_variant: str = "v2", # 'v2' (stable) | 'v4' (faster, leaks)
# Heads.
d_descriptor: int = 1024,
return_normalized: bool = False,
# Altitude-FiLM.
use_film_altitude: bool = True,
altitude_norm: float = 500.0,
# Text-FiLM.
use_text_film_uav: bool = True,
use_text_film_sat: bool = True,
text_film_dim: int = 1024,
text_film_hidden: int = 256,
) -> None:
self.variant = variant
self.in_channels = in_channels
self.input_size = input_size
self.dcn_variant = dcn_variant
self.d_descriptor = d_descriptor
self.return_normalized = return_normalized
self.use_film_altitude = use_film_altitude
self.altitude_norm = altitude_norm
self.use_text_film_uav = use_text_film_uav
self.use_text_film_sat = use_text_film_sat
self.text_film_dim = text_film_dim
self.text_film_hidden = text_film_hidden
```
> **`SOFIAv1Config` (`@dataclass`) в `src/models/sofia_v1/config.py` остаётся** — это внутренняя структура для модели. В `Trainer._build_model` создаём `SOFIAv1Config(...)` из полей `models_cfg` (где `models_cfg: SOFIAv1ModelsConfig`). Один источник правды — gin, а dataclass это просто адаптер на границе модельного слоя.
### `SOFIAv71ModelsConfig`
Покрывает поля из `src/models/sofia_v71/config.py`. По README вижу:
```python
@gin.configurable
class SOFIAv71ModelsConfig:
def __init__(
self,
# Preset.
preset: str = "M", # 'M' | 'L' | 'Tiny'
# Mamba backend.
mamba_variant: str = "mamba2", # 'mamba1' | 'mamba2' | 'efficient_vmamba'
mamba_backend: str = "auto", # 'auto' | 'torch'
# Heads.
d_descriptor: int = 512,
# Altitude-FiLM.
use_film_altitude: bool = True,
altitude_norm: float = 500.0,
# KD taps.
return_features: bool = False,
# Quantization (PTQ/QAT — for production deploy, not training).
# Not adding here unless an experiment toggles them; can be added later.
) -> None:
self.preset = preset
self.mamba_variant = mamba_variant
self.mamba_backend = mamba_backend
self.d_descriptor = d_descriptor
self.use_film_altitude = use_film_altitude
self.altitude_norm = altitude_norm
self.return_features = return_features
```
> ⚠️ Точные поля `SOFIAv71` нужно сверить с `src/models/sofia_v71/config.py` (я его не открыл целиком). Это **открытый пункт** — заполняется при создании файла.
### Как загружать «правильный» Models config?
Ровно один `models.gin` лежит в директории пресета и определяет один из 4 классов. `config_loader` ветвится:
```python
def load_all_configs(path2cfg: str) -> dict[str, Any]:
cfg_dir = Path(path2cfg)
gin.clear_config()
gin.parse_config_files_and_bindings(
config_files=[str(f) for f in sorted(cfg_dir.glob("*.gin"))],
bindings=[],
)
# Build common first to learn which backbone to use.
common = ModelsCommonConfig()
backbone_to_cls = {
"dinov3": DINOv3ModelsConfig,
"stripnet": StripNetModelsConfig,
"sofia_v1": SOFIAv1ModelsConfig,
"sofia_v71": SOFIAv71ModelsConfig,
}
if common.backbone not in backbone_to_cls:
raise ValueError(
f"Unknown backbone={common.backbone!r}; expected one of {list(backbone_to_cls)}",
)
models_specific = backbone_to_cls[common.backbone]() # gin fills it
return {
"pipeline": PipelineConfig(),
"hardware": HardwareConfig(),
"models_common": common,
"models": models_specific, # one of 4 classes
"training": TrainingConfig(),
"tracking": TrackingConfig(),
}
```
**В `models.gin` пресета** прописаны биндинги **только** для активного бэкбона + `ModelsCommonConfig`. Биндинги для других бэкбонов в этот файл не попадают (плоский стиль).
---
## Часть C — Конфиг для скриптов препроцессинга (Часть 4 ваших ответов)
Скрипты `make_split.py` и `filter_segmentation.py` переводятся на gin. Возникает **5-й универсальный конфиг**:
### `PreprocessConfig`
```python
@gin.configurable
class PreprocessConfig:
"""Preprocessing utilities: train/test split + segmentation filter.
Used only by scripts/make_split.py and scripts/filter_segmentation.py.
Not consumed by the training pipeline directly.
"""
def __init__(
self,
# Inputs (read from PipelineConfig if you want consistency, but having
# them here lets preprocess run independently).
rgb_root: str = "/home/servml/Документы/datasets/GTA-UAV-LR",
segm_root: str = "/home/servml/Документы/datasets/GTA-UAV-LR-aug/segm",
# make_split params.
split_ratio: float = 0.8,
split_seed: int = 42,
split_input_train: str = "cross-area-drone2sate-train.json",
split_input_test: str = "cross-area-drone2sate-test.json",
split_output_dir: str = "meta",
split_output_train: str = "train_80.json",
split_output_test: str = "test_20.json",
# filter_segmentation params.
seg_threshold: float = 0.90,
seg_exclude_classes: list[int] | None = None, # default [0, 4]
seg_filter_output: str = "meta/seg_filter.json",
) -> None:
self.rgb_root = rgb_root
self.segm_root = segm_root
self.split_ratio = split_ratio
self.split_seed = split_seed
self.split_input_train = split_input_train
self.split_input_test = split_input_test
self.split_output_dir = split_output_dir
self.split_output_train = split_output_train
self.split_output_test = split_output_test
self.seg_threshold = seg_threshold
self.seg_exclude_classes = seg_exclude_classes or [0, 4]
self.seg_filter_output = seg_filter_output
```
> **Вопрос дизайна:** держать `rgb_root` отдельно в `PreprocessConfig` (как выше) или брать из `PipelineConfig.rgb_root`?
>
> **Решение:** держать **отдельно**, потому что `PreprocessConfig` живёт в **другом** наборе .gin (отдельный пресет `preprocess/`). Это согласуется с принципом плоских конфигов без ссылок между файлами. Дублирование одного пути на 2 файла — приемлемая цена за изоляцию.
---
## Часть D — Что и где будет лежать (структура каталогов)
```
caption-test/
├── in/
│ └── config_files/ # АКТИВНЫЙ пресет, копируется из presets/
│ ├── pipeline.gin
│ ├── hardware.gin
│ ├── models.gin # биндинги ModelsCommonConfig + одного из Models*Config
│ ├── training.gin
│ └── tracking.gin
├── presets/
│ ├── gtauav_balanced/ # все 5 файлов, БЕЗ include
│ │ ├── pipeline.gin
│ │ ├── hardware.gin
│ │ ├── models.gin # backbone='dinov3', shared, mona_12
│ │ ├── training.gin
│ │ └── tracking.gin
│ ├── gtauav_baseline/ # 5 файлов, baseline_mode=True
│ ├── gtauav_balanced_asym/ # shared_encoder=False, mona_24
│ ├── gtauav_baseline_asym/
│ ├── gtauav_balanced_stripnet/ # backbone='stripnet'
│ ├── gtauav_balanced_stripnet_unfrozen/
│ ├── gtauav_baseline_stripnet/
│ ├── gtauav_baseline_stripnet_unfrozen/
│ ├── gtauav_text_heavy/ # init_gate=0.3
│ ├── gtauav_image_heavy/ # init_gate=0.9
│ ├── gtauav_balanced_sofia/ # backbone='sofia_v71'
│ ├── gtauav_balanced_sofia_v1/ # backbone='sofia_v1'
│ ├── gtauav_baseline_sofia/
│ ├── gtauav_baseline_sofia_v1/
│ └── preprocess/ # отдельный пресет для скриптов
│ └── preprocess.gin # PreprocessConfig.* — одиночный файл достаточен
├── src/
│ └── conf/
│ ├── __init__.py
│ ├── pipeline_conf.py # PipelineConfig + get_pipeline_cfg
│ ├── hardware_conf.py # HardwareConfig + get_hardware_cfg
│ ├── training_conf.py # TrainingConfig + get_training_cfg
│ ├── tracking_conf.py # TrackingConfig + get_tracking_cfg
│ ├── models_common_conf.py # ModelsCommonConfig + get_models_common_cfg
│ ├── models_dinov3_conf.py # DINOv3ModelsConfig + get_models_dinov3_cfg
│ ├── models_stripnet_conf.py # StripNetModelsConfig + get_models_stripnet_cfg
│ ├── models_sofia_v1_conf.py # SOFIAv1ModelsConfig + get_models_sofia_v1_cfg
│ ├── models_sofia_v71_conf.py # SOFIAv71ModelsConfig + get_models_sofia_v71_cfg
│ ├── preprocess_conf.py # PreprocessConfig + get_preprocess_cfg
│ └── config_loader.py # load_all_configs() с разводкой по backbone
```
---
## Часть E — Содержимое `presets/gtauav_balanced/` (точно)
Это эталонный пресет — остальные диффятся от него.
### `presets/gtauav_balanced/pipeline.gin`
```gin
# What to train on, where to save, schedule.
PipelineConfig.train_json = 'meta/train_80.json'
PipelineConfig.test_json = 'meta/test_20.json'
PipelineConfig.rgb_root = '/home/servml/Документы/datasets/GTA-UAV-LR'
PipelineConfig.caption_root = '/home/servml/Документы/datasets/GTA-UAV-LR-captions'
PipelineConfig.filter_meta = None
PipelineConfig.output_dir = 'out/gtauav/with_text'
PipelineConfig.resume_from = None
PipelineConfig.epochs = 10
PipelineConfig.warmup_epochs = 2
PipelineConfig.eval_every = 1
PipelineConfig.seed = 42
```
### `presets/gtauav_balanced/hardware.gin`
```gin
# RTX 4090 profile.
HardwareConfig.device = 'cuda'
HardwareConfig.batch_size = 8
HardwareConfig.grad_accum_steps = 8
HardwareConfig.num_workers = 4
HardwareConfig.use_amp = True
HardwareConfig.gradient_checkpointing = True
HardwareConfig.reserve_gb = 2.0
```
### `presets/gtauav_balanced/models.gin`
```gin
# DINOv3 shared encoder + MONA in last 12 of 24 blocks + DGTRS-CLIP text.
ModelsCommonConfig.backbone = 'dinov3'
ModelsCommonConfig.baseline_mode = False
ModelsCommonConfig.init_gate = 0.7
ModelsCommonConfig.lrsclip_path = 'nn_models/LRSCLIP/DGTRS-CLIP-ViT-L-14.pt'
DINOv3ModelsConfig.dino_web_path = 'nn_models/DINO_WEB/dinov3-vitl16-pretrain-lvd1689m.pth'
DINOv3ModelsConfig.dino_sat_path = 'nn_models/DINO_SAT/model.safetensors'
DINOv3ModelsConfig.shared_encoder = True
DINOv3ModelsConfig.mona_bottleneck = 64
DINOv3ModelsConfig.mona_last_n_blocks = 12
```
### `presets/gtauav_balanced/training.gin`
```gin
# Loss + optimizer + sampler.
TrainingConfig.loss_type = 'symmetric'
TrainingConfig.tau_init = 0.07
TrainingConfig.label_smoothing = 0.1
TrainingConfig.learnable_temperature = True
TrainingConfig.weight_q2g = 0.6
TrainingConfig.weight_g2q = 0.4
TrainingConfig.neg_bank_size = 0
TrainingConfig.learning_rate = 1e-4
TrainingConfig.text_lr_factor = 0.1
TrainingConfig.weight_decay = 1e-4
TrainingConfig.grad_clip = 1.0
TrainingConfig.sampler_type = 'mutex'
TrainingConfig.dss_warmup_epochs = 1
TrainingConfig.dss_reembed_every = 1
TrainingConfig.dss_knn_device = 'cuda'
TrainingConfig.dss_use_lsh = False
TrainingConfig.dss_lsh_num_tables = 8
TrainingConfig.dss_lsh_num_bits = 14
TrainingConfig.dss_cache_dir = None
TrainingConfig.use_mutex_sampler = True
```
### `presets/gtauav_balanced/tracking.gin`
```gin
TrackingConfig.use_wandb = False
TrackingConfig.use_tb = True
TrackingConfig.wandb_project = 'caption-test-gtauav'
TrackingConfig.wandb_run_name = None
TrackingConfig.wandb_entity = None
TrackingConfig.log_grad_norms = True
TrackingConfig.use_gradcam = False
TrackingConfig.gradcam_every = 5
TrackingConfig.gradcam_samples = 8
TrackingConfig.use_profiler = False
TrackingConfig.profiler_warmup = 3
TrackingConfig.profiler_active = 5
```
---
## Часть F — Дельты остальных пресетов от `gtauav_balanced/`
Каждый пресет — **полная копия** `gtauav_balanced/` с точечными изменениями в указанных файлах. Никакого `include`.
### `gtauav_baseline/`
**Дельта от `gtauav_balanced/`:**
`pipeline.gin`:
```gin
PipelineConfig.output_dir = 'out/gtauav/baseline_inbatch'
```
`models.gin`:
```gin
ModelsCommonConfig.baseline_mode = True
```
`training.gin`:
```gin
TrainingConfig.sampler_type = 'mutex' # was already, kept explicit per diagnostic notes in old conf/gtauav_baseline.gin
TrainingConfig.neg_bank_size = 0 # explicitly disabled (no MoCo queue)
```
(Остальные 3 файла — побайтовая копия из `gtauav_balanced/`.)
### `gtauav_balanced_asym/`
`pipeline.gin`:
```gin
PipelineConfig.output_dir = 'out/gtauav/balanced_asym'
```
`models.gin`:
```gin
DINOv3ModelsConfig.shared_encoder = False
DINOv3ModelsConfig.mona_last_n_blocks = 24
```
### `gtauav_baseline_asym/`
Объединяет дельту `gtauav_baseline/` и `gtauav_balanced_asym/`:
`pipeline.gin`: `output_dir = 'out/gtauav/baseline_asym'`
`models.gin`: `baseline_mode = True`, `shared_encoder = False`, `mona_last_n_blocks = 24`
### `gtauav_balanced_stripnet/`
`pipeline.gin`: `output_dir = 'out/gtauav/balanced_stripnet'`
`models.gin` (полностью):
```gin
ModelsCommonConfig.backbone = 'stripnet'
ModelsCommonConfig.baseline_mode = False
ModelsCommonConfig.init_gate = 0.7
ModelsCommonConfig.lrsclip_path = 'nn_models/LRSCLIP/DGTRS-CLIP-ViT-L-14.pt'
StripNetModelsConfig.stripnet_path = 'nn_models/STRIPNET/stripnet_s.pth'
StripNetModelsConfig.stripnet_freeze = True
StripNetModelsConfig.stripnet_mona_last_n_stages = 2
StripNetModelsConfig.stripnet_backbone_lr_factor = 0.1
```
(Биндинги `DINOv3ModelsConfig.*` НЕ попадают в этот файл — другой бэкбон.)
### `gtauav_balanced_stripnet_unfrozen/`
Дельта от `gtauav_balanced_stripnet/`:
`pipeline.gin`: `output_dir = 'out/gtauav/balanced_stripnet_unfrozen'`
`models.gin`: `StripNetModelsConfig.stripnet_freeze = False`
### `gtauav_baseline_stripnet/`, `gtauav_baseline_stripnet_unfrozen/`
Аналогично — `baseline_mode = True` поверх stripnet-вариантов.
### `gtauav_text_heavy/`, `gtauav_image_heavy/`
`pipeline.gin`: соответствующие `output_dir`
`models.gin`: `ModelsCommonConfig.init_gate = 0.3` (text-heavy) или `0.9` (image-heavy)
### `gtauav_balanced_sofia_v1/`
`pipeline.gin`: `output_dir = 'out/gtauav/balanced_sofia_v1'`
`models.gin` (полностью):
```gin
ModelsCommonConfig.backbone = 'sofia_v1'
ModelsCommonConfig.baseline_mode = False
ModelsCommonConfig.init_gate = 0.7
ModelsCommonConfig.lrsclip_path = 'nn_models/LRSCLIP/DGTRS-CLIP-ViT-L-14.pt'
SOFIAv1ModelsConfig.variant = 'small'
SOFIAv1ModelsConfig.in_channels = 3
SOFIAv1ModelsConfig.input_size = 256
SOFIAv1ModelsConfig.dcn_variant = 'v2'
SOFIAv1ModelsConfig.d_descriptor = 1024
SOFIAv1ModelsConfig.return_normalized = False
SOFIAv1ModelsConfig.use_film_altitude = True
SOFIAv1ModelsConfig.altitude_norm = 500.0
SOFIAv1ModelsConfig.use_text_film_uav = True
SOFIAv1ModelsConfig.use_text_film_sat = True
SOFIAv1ModelsConfig.text_film_dim = 1024
SOFIAv1ModelsConfig.text_film_hidden = 256
```
### `gtauav_balanced_sofia/` (= sofia_v71)
`pipeline.gin`: `output_dir = 'out/gtauav/balanced_sofia'`
`models.gin`:
```gin
ModelsCommonConfig.backbone = 'sofia_v71'
ModelsCommonConfig.baseline_mode = False
ModelsCommonConfig.init_gate = 0.7
ModelsCommonConfig.lrsclip_path = 'nn_models/LRSCLIP/DGTRS-CLIP-ViT-L-14.pt'
SOFIAv71ModelsConfig.preset = 'M'
SOFIAv71ModelsConfig.mamba_variant = 'mamba2'
SOFIAv71ModelsConfig.mamba_backend = 'auto'
SOFIAv71ModelsConfig.d_descriptor = 512
SOFIAv71ModelsConfig.use_film_altitude = True
SOFIAv71ModelsConfig.altitude_norm = 500.0
SOFIAv71ModelsConfig.return_features = False
```
> ⚠️ Точные дефолты для sofia_v71 пресета зависят от того, как сейчас выглядит `gtauav_balanced_sofia.gin` в локальной копии. **Нужны сами файлы**, чтобы воспроизвести один-в-один.
### `gtauav_baseline_sofia/`, `gtauav_baseline_sofia_v1/`
`baseline_mode = True` поверх sofia-вариантов.
---
## Часть G — `presets/preprocess/preprocess.gin`
Один файл (одиночный, потому что один класс):
```gin
PreprocessConfig.rgb_root = '/home/servml/Документы/datasets/GTA-UAV-LR'
PreprocessConfig.segm_root = '/home/servml/Документы/datasets/GTA-UAV-LR-aug/segm'
PreprocessConfig.split_ratio = 0.8
PreprocessConfig.split_seed = 42
PreprocessConfig.split_input_train = 'cross-area-drone2sate-train.json'
PreprocessConfig.split_input_test = 'cross-area-drone2sate-test.json'
PreprocessConfig.split_output_dir = 'meta'
PreprocessConfig.split_output_train = 'train_80.json'
PreprocessConfig.split_output_test = 'test_20.json'
PreprocessConfig.seg_threshold = 0.90
PreprocessConfig.seg_exclude_classes = [0, 4]
PreprocessConfig.seg_filter_output = 'meta/seg_filter.json'
```
---
## Часть H — Диффы для существующих файлов
> Здесь только то, что нужно поменять **в конфигурационной части**. Внутренности `Trainer`, `_evaluate`, `CSVLogger` — не трогаются на этом шаге.
### Файл: `src/training/train_gtauav.py`
**Полностью убрать `TrainConfigGTAUAV` и module-level path constants.** Функция `train()` получает не `cfg: TrainConfigGTAUAV`, а пять config-объектов.
```diff
from __future__ import annotations
...
- import argparse
...
- from dataclasses import dataclass, field
- from pathlib import Path
+ from pathlib import Path
- import gin
...
- # Default paths.
- _RGB_ROOT = "/home/servml/Документы/datasets/GTA-UAV-LR"
- _CAPTION_ROOT = "/home/servml/Документы/datasets/GTA-UAV-LR-captions"
- _TRAIN_JSON = "meta/train_80.json"
- _TEST_JSON = "meta/test_20.json"
-
- _DINO_WEB = "nn_models/DINO_WEB/dinov3-vitl16-pretrain-lvd1689m.pth"
- _DINO_SAT = "nn_models/DINO_SAT/model.safetensors"
- _LRSCLIP = "nn_models/LRSCLIP/DGTRS-CLIP-ViT-L-14.pt"
-
-
- @gin.configurable(module="src.training.train_gtauav")
- @dataclass
- class TrainConfigGTAUAV:
- """Training configuration for GTA-UAV experiment."""
- # Data.
- train_json: str = _TRAIN_JSON
- test_json: str = _TEST_JSON
- rgb_root: str = _RGB_ROOT
- # ... ВСЕ 50+ полей удаляются ...
- profiler_active: int = 5
-
-
- def train(cfg: TrainConfigGTAUAV) -> None:
+ def train(
+ pipeline_cfg: "PipelineConfig",
+ hardware_cfg: "HardwareConfig",
+ models_common_cfg: "ModelsCommonConfig",
+ models_cfg: "DINOv3ModelsConfig | StripNetModelsConfig | SOFIAv1ModelsConfig | SOFIAv71ModelsConfig",
+ training_cfg: "TrainingConfig",
+ tracking_cfg: "TrackingConfig",
+ ) -> None:
"""Run full training loop."""
# Inside the function body, every `cfg.<field>` reference is rewritten to
# the appropriate <kind>_cfg.<field>. Mapping:
# cfg.train_json → pipeline_cfg.train_json
# cfg.batch_size → hardware_cfg.batch_size
# cfg.tau_init → training_cfg.tau_init
# cfg.use_wandb → tracking_cfg.use_wandb
# cfg.dino_web_path → models_cfg.dino_web_path (when DINOv3)
# cfg.stripnet_path → models_cfg.stripnet_path (when StripNet)
# cfg.backbone → models_common_cfg.backbone
# cfg.baseline_mode → models_common_cfg.baseline_mode
# cfg.init_gate → models_common_cfg.init_gate
# cfg.lrsclip_path → models_common_cfg.lrsclip_path
...
- def main() -> None:
- parser = argparse.ArgumentParser(description="GTA-UAV caption test training.")
- parser.add_argument("--config", type=str, default=None, ...)
- parser.add_argument("--baseline", action="store_true", ...)
- # ... все 15 argparse флагов удаляются ...
- args = parser.parse_args()
-
- if args.config is not None:
- gin.parse_config_file(args.config)
- if args.gin_param:
- gin.parse_config(args.gin_param)
-
- cfg = TrainConfigGTAUAV()
-
- if args.baseline:
- cfg.baseline_mode = True
- # ... все CLI overrides удаляются ...
- train(cfg)
+ def main() -> None:
+ """Entry point: read configs from in/config_files/ and run training."""
+ from src.conf.config_loader import load_all_configs
+ from src.utils.path_utils import get_proj_dir
+
+ proj_dir = get_proj_dir()
+ path2cfg = f"{proj_dir}in/config_files/"
+ configs = load_all_configs(path2cfg)
+
+ train(
+ pipeline_cfg=configs["pipeline"],
+ hardware_cfg=configs["hardware"],
+ models_common_cfg=configs["models_common"],
+ models_cfg=configs["models"],
+ training_cfg=configs["training"],
+ tracking_cfg=configs["tracking"],
+ )
```
### Файл: `src/losses/multi_infonce.py`
Снять `@gin.configurable` (см. Шаг 1, Нарушение #2). Параметры будут приходить из `TrainingConfig` через явный вызов в `train()`:
```diff
from __future__ import annotations
...
import math
- import gin
import torch
...
- @gin.configurable
class InfoNCELoss(nn.Module):
"""Symmetric InfoNCE with learnable or scheduled temperature.
+
+ Note: NOT @gin.configurable. Parameters arrive explicitly from
+ train() via TrainingConfig.* — single source of truth.
...
"""
```
### Файл: `src/losses/weighted_infonce.py`
Аналогично:
```diff
...
- import gin
import torch
...
- @gin.configurable
class WeightedInfoNCELoss(nn.Module):
"""Weighted InfoNCE with adaptive per-sample label smoothing.
+
+ Note: NOT @gin.configurable. Parameters arrive explicitly from
+ train() via TrainingConfig.* — single source of truth.
...
"""
```
### Файл: `src/datasets/visloc_with_captions.py` (legacy v2)
Аналогично — снять `@gin.configurable` с `GeoLocCaptionDataset`. Если v2 удаляется как ветка — этот файл удаляется целиком, дифф не нужен.
```diff
- import gin
...
- @gin.configurable
class GeoLocCaptionDataset(Dataset):
...
```
### Файл: `src/datasets/gtauav_dataset.py`
Убрать module-level пути. `rgb_root` и `caption_root` становятся обязательными в `__init__` — они придут из `pipeline_cfg.rgb_root` / `pipeline_cfg.caption_root` в `train()`.
```diff
...
- # Default paths.
- _RGB_ROOT = Path("/home/servml/Документы/datasets/GTA-UAV-LR")
- _CAPTION_ROOT = Path("/home/servml/Документы/datasets/GTA-UAV-LR-captions")
_EMPTY_CAPTION = ""
...
class GTAUAVDataset(Dataset):
def __init__(
self,
pair_json: str,
- rgb_root: str = str(_RGB_ROOT),
- caption_root: str = str(_CAPTION_ROOT),
+ rgb_root: str,
+ caption_root: str,
drone_transform: Callable | None = None,
...
) -> None:
```
### Файл: `scripts/make_split.py`
Полностью переписывается на gin: argparse → `get_preprocess_cfg`, module-level пути → поля конфига.
```diff
from __future__ import annotations
...
- import argparse
import json
import logging
import random
from pathlib import Path
import coloredlogs
+ from src.conf.preprocess_conf import get_preprocess_cfg
+ from src.utils.path_utils import get_proj_dir
LOGGER = logging.getLogger("caption_test.make_split")
- _RGB_ROOT = Path("/home/servml/Документы/datasets/GTA-UAV-LR")
-
-
def main() -> None:
- parser = argparse.ArgumentParser(description="Create 80/20 split for GTA-UAV-LR.")
- parser.add_argument("--ratio", type=float, default=0.8, help="Train ratio (default 0.8).")
- parser.add_argument("--seed", type=int, default=42, help="Random seed.")
- parser.add_argument("--output-dir", type=str, default="meta", help="Output directory.")
- args = parser.parse_args()
-
- coloredlogs.install(level="INFO", logger=LOGGER, ...)
+ coloredlogs.install(level="INFO", logger=LOGGER, ...)
+
+ # Load config from a separate preprocess preset directory.
+ proj_dir = get_proj_dir()
+ cfg = get_preprocess_cfg(f"{proj_dir}presets/preprocess/")
+
+ rgb_root = Path(cfg.rgb_root)
+ train_path = rgb_root / cfg.split_input_train
+ test_path = rgb_root / cfg.split_input_test
- train_path = _RGB_ROOT / "cross-area-drone2sate-train.json"
- test_path = _RGB_ROOT / "cross-area-drone2sate-test.json"
-
LOGGER.info("📂 Loading %s", train_path.name)
with open(train_path) as f:
part1 = json.load(f)
...
- rng = random.Random(args.seed)
+ rng = random.Random(cfg.split_seed)
rng.shuffle(all_pairs)
- n_train = int(len(all_pairs) * args.ratio)
+ n_train = int(len(all_pairs) * cfg.split_ratio)
...
- out_dir = Path(args.output_dir)
+ out_dir = Path(cfg.split_output_dir)
out_dir.mkdir(parents=True, exist_ok=True)
- train_out = out_dir / "train_80.json"
- test_out = out_dir / "test_20.json"
+ train_out = out_dir / cfg.split_output_train
+ test_out = out_dir / cfg.split_output_test
...
```
### Файл: `scripts/filter_segmentation.py`
Аналогично:
```diff
from __future__ import annotations
...
- import argparse
import json
import logging
from pathlib import Path
import coloredlogs
import numpy as np
from PIL import Image
from tqdm import tqdm
+ from src.conf.preprocess_conf import get_preprocess_cfg
+ from src.utils.path_utils import get_proj_dir
LOGGER = logging.getLogger("caption_test.filter_seg")
- SEGM_ROOT = Path("/home/servml/Документы/datasets/GTA-UAV-LR-aug/segm")
- EXCLUDE_CLASSES = {0, 4} # background, water
- DEFAULT_THRESHOLD = 0.90
-
...
def main() -> None:
- parser = argparse.ArgumentParser(...)
- parser.add_argument("--segm-root", ...)
- parser.add_argument("--threshold", ...)
- parser.add_argument("--output", ...)
- args = parser.parse_args()
+ coloredlogs.install(level="INFO", logger=LOGGER, ...)
+
+ proj_dir = get_proj_dir()
+ cfg = get_preprocess_cfg(f"{proj_dir}presets/preprocess/")
+
+ segm_root = Path(cfg.segm_root)
+ exclude_classes = set(cfg.seg_exclude_classes)
+ threshold = cfg.seg_threshold
+ output_path = Path(cfg.seg_filter_output)
- coloredlogs.install(level="INFO", logger=LOGGER, ...)
- LOGGER.info("🚀 Starting segmentation filter (threshold=%.2f)", args.threshold)
- segm_root = Path(args.segm_root)
- results = scan_masks(segm_root, EXCLUDE_CLASSES, args.threshold)
+ LOGGER.info("🚀 Starting segmentation filter (threshold=%.2f)", threshold)
+ results = scan_masks(segm_root, exclude_classes, threshold)
...
- output_path = Path(args.output)
output_path.parent.mkdir(parents=True, exist_ok=True)
out = {
- "threshold": args.threshold,
- "exclude_classes": sorted(EXCLUDE_CLASSES),
+ "threshold": threshold,
+ "exclude_classes": sorted(exclude_classes),
...
}
...
```
### Файл: `src/training/train.py` (legacy v2)
Если v2 оставляем — снять `@gin.configurable` с `TrainConfig` и переписать на 5 конфигов аналогично `train_gtauav.py`.
Если удаляем — файл уходит вместе с веткой.
> **Я бы советовал удалить v2** — он создаёт двойную работу при каждом изменении. Но это **отдельный** разговор, не блокер для текущего шага.
### Файл: `conf/` (старые .gin) — удаляются после миграции
После того как **все 14 пресетов в `presets/`** созданы и проверены — старая директория `conf/` удаляется целиком:
```diff
- conf/balanced.gin
- conf/baseline_no_text.gin
- conf/text_heavy.gin
- conf/gtauav_balanced.gin
- conf/gtauav_baseline.gin
- conf/gtauav_balanced_asym.gin
- conf/gtauav_baseline_asym.gin
- conf/gtauav_balanced_stripnet.gin
- conf/gtauav_balanced_stripnet_unfrozen.gin
- conf/gtauav_baseline_stripnet.gin
- conf/gtauav_baseline_stripnet_unfrozen.gin
- conf/gtauav_text_heavy.gin
- conf/gtauav_image_heavy.gin
- conf/gtauav_balanced_sofia.gin # из локальной копии
- conf/gtauav_balanced_sofia_v1.gin # из локальной копии
- conf/gtauav_baseline_sofia.gin # из локальной копии
- conf/gtauav_baseline_sofia_v1.gin # из локальной копии
```
---
## Часть I — Что НЕ делаем на этом шаге
Чтобы шаг был обозримым, **не трогаем**:
- ❌ Декомпозиция `train()` (1296 строк) на `Trainer.run()` + методы — **отдельный шаг**
- ❌ Перенос `_evaluate` в `src/eval/evaluator.py`**отдельный шаг**
- ❌ Перенос `CSVLogger` в `src/training/csv_logger.py`**отдельный шаг**
- ❌ Замена `@torch.no_grad()` на `@torch.inference_mode()`**отдельный шаг (косметика)**
-`_atomic_save` cleanup на ошибке — **отдельный шаг (косметика)**
- ❌ Логика sofia_v1/v71 моделей и их `dataclass`-конфиги — **внутренний слой не трогаем**
После этого шага получаем: **гин-конфиг разделён, никаких `@gin.configurable + @dataclass`, никаких `@gin.configurable` на бизнес-классах, никаких argparse, плоские пресеты с дублированием полных биндингов вместо `include`**. Структура `train()` остаётся прежней (одна большая функция), но получает 6 объектов конфига вместо одного `cfg`.
---
## Часть J — Открытые вопросы для уточнения
1. **SOFIAv71 fields** — точный список полей `SOFIAv71Config` (`@dataclass` в `src/models/sofia_v71/config.py`) для построения `SOFIAv71ModelsConfig`. Я выписал поля по README, но в `config.py` могут быть ещё (mamba `headdim`, `d_state`, `kernel_size`, `num_bins` для квантизации). Нужно открыть файл и составить полный список.
2. **`gtauav_*_sofia*.gin` локальные пресеты** — содержимое 4 sofia-гинов из локальной копии (на скриншоте видны, в репо ещё нет). Нужны как эталон для воспроизведения дефолтов один-в-один.
3. **`use_mutex_sampler`** в `TrainingConfig` — текущий код помечает поле как «legacy alias». Сохранить ли его на этапе разделения, или сразу убрать (тогда `effective_sampler_type` берётся напрямую из `sampler_type`)?
4. **Legacy v2** (`train.py`, `visloc_with_captions.py`, `conf/balanced.gin`/`baseline_no_text.gin`/`text_heavy.gin`) — удаляем или приводим к новому стилю? Я склоняюсь к удалению. Если оставлять — добавляется ещё 3 пресета и переписывание `TrainConfig` на 5 конфигов.
5. **Расположение `presets/`** — в корне проекта или внутри `in/` (как `in/presets/`)? У вас сейчас лежит в корне (`presets/gtauav_balanced/` рядом с `src/`). Оставляем там же.
После ответов на эти 5 вопросов план становится готов к реализации без новых развилок.