Files
caption-test/refactor_v3_plan.md

38 KiB
Raw Blame History

Шаг 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 — общие поля

Поля, нужные всем бэкбонам:

@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

@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

@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:

@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 вижу:

@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 ветвится:

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

@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

# 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

# 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

# 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

# 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

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:

PipelineConfig.output_dir = 'out/gtauav/baseline_inbatch'

models.gin:

ModelsCommonConfig.baseline_mode = True

training.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:

PipelineConfig.output_dir = 'out/gtauav/balanced_asym'

models.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 (полностью):

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 (полностью):

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:

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

Один файл (одиночный, потому что один класс):

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-объектов.

  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():

  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

Аналогично:

  ...
- 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 удаляется как ветка — этот файл удаляется целиком, дифф не нужен.

- 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().

  ...
- # 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 пути → поля конфига.

  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

Аналогично:

  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/ удаляется целиком:

- 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 вопросов план становится готов к реализации без новых развилок.