Files
fuze_task/vendor_reference/caption_test/src/training/trackers.py
Pikaliov 2c6a00a4ca fuse_proj: Initial operational package for 3 researchers (Pavlenko/Blizno/Moroz)
Multimodal fusion research on StripNet+GTA-UAV proxy:
- 3 independent fusion tracks: condition-aware (A), token/bottleneck (B), role-aware (C)
- Shared interfaces, protocol, dataset audit, baseline benchmarks
- Canonical version-chain references to vault (SPEC, ANALYSIS, TRIAGE)
- Personalized task plans and decision tables for each researcher
- 3 generated DOCX task assignment files with milestones and DoD checklist
- Full modality dropout diagnostics and missing-modality robustness requirements
- Data contract, benchmark registry, experiment tracking infrastructure

Operational documents:
- docs/00_project/: MERIDIAN context, protocol, repository reuse guide, experiment specification
- docs/01_tasks/: Master assignment + 3 individual researcher tracks + joint integration
- docs/02_references/: Core literature, version-chain bases, code maps
- docs/03_codebase_guides/: Existing code snapshots from vault
- scripts/: gen_task_plans.js (DOCX generation), placeholder infrastructure
- vendor_reference/: Snapshots of caption_test, depth_edges_annotate, existing SOFIA/SegModel code
- reports/, results/, experiments/: Shared output structure for all 3 researchers

3 DOCX files generated from gen_task_plans.js (Times New Roman 14pt, GOST format):
- План_заданий_Павленко_БВ.docx (Condition-Aware track, fusion API owner)
- План_заданий_Близно_МВ.docx (Token/Bottleneck track, benchmark owner)
- План_заданий_Мороз_ЕС.docx (Role-Aware track, data contract owner)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-11 17:16:57 +03:00

207 lines
7.4 KiB
Python

from __future__ import annotations
"""Unified experiment tracking: W&B + TensorBoard + CSV.
Auto-detects available backends. Falls back gracefully if wandb/tensorboard
are not installed.
Usage:
tracker = ExperimentTracker(output_dir, config_dict, use_wandb=True, use_tb=True)
tracker.log_train(epoch, {"loss": 0.5, "lr": 1e-4})
tracker.log_val(epoch, {"r@1_q2g": 0.3})
tracker.log_gradients(epoch, grad_norms_dict)
tracker.log_image(epoch, "gradcam/drone", image_tensor)
tracker.close()
"""
import logging
from pathlib import Path
from typing import Any
import torch
LOGGER = logging.getLogger("caption_test.trackers")
def _try_import_wandb():
try:
import wandb
return wandb
except ImportError:
return None
def _try_import_tb():
try:
from torch.utils.tensorboard import SummaryWriter
return SummaryWriter
except ImportError:
return None
class ExperimentTracker:
"""Unified tracker dispatching to W&B, TensorBoard, and CSV.
Args:
output_dir: Base output directory.
config: Dict of hyperparameters to log.
use_wandb: Enable Weights & Biases tracking.
use_tb: Enable TensorBoard tracking.
wandb_project: W&B project name.
wandb_run_name: W&B run name (auto-generated if None).
wandb_entity: W&B entity (team/user).
"""
def __init__(
self,
output_dir: str | Path,
config: dict[str, Any] | None = None,
use_wandb: bool = False,
use_tb: bool = True,
wandb_project: str = "caption-test-gtauav",
wandb_run_name: str | None = None,
wandb_entity: str | None = None,
) -> None:
self.output_dir = Path(output_dir)
self._wandb_run = None
self._tb_writer = None
# W&B init.
if use_wandb:
wandb = _try_import_wandb()
if wandb is not None:
self._wandb_run = wandb.init(
project=wandb_project,
name=wandb_run_name,
entity=wandb_entity,
config=config or {},
dir=str(self.output_dir),
reinit=True,
)
LOGGER.info("W&B initialized: %s", self._wandb_run.url)
else:
LOGGER.warning("wandb not installed, skipping W&B tracking")
# TensorBoard init.
if use_tb:
SummaryWriter = _try_import_tb()
if SummaryWriter is not None:
tb_dir = self.output_dir / "tb_logs"
tb_dir.mkdir(parents=True, exist_ok=True)
self._tb_writer = SummaryWriter(log_dir=str(tb_dir))
LOGGER.info("TensorBoard initialized: %s", tb_dir)
else:
LOGGER.warning("tensorboard not installed, skipping TB tracking")
@property
def has_wandb(self) -> bool:
return self._wandb_run is not None
@property
def has_tb(self) -> bool:
return self._tb_writer is not None
def log_train(self, epoch: int, metrics: dict[str, float], step: int | None = None) -> None:
"""Log training metrics for an epoch."""
if self._wandb_run is not None:
self._wandb_run.log(
{f"train/{k}": v for k, v in metrics.items()},
step=step or epoch,
)
if self._tb_writer is not None:
for k, v in metrics.items():
self._tb_writer.add_scalar(f"train/{k}", v, global_step=step or epoch)
def log_val(self, epoch: int, metrics: dict[str, float], step: int | None = None) -> None:
"""Log validation metrics."""
if self._wandb_run is not None:
self._wandb_run.log(
{f"val/{k}": v for k, v in metrics.items()},
step=step or epoch,
)
if self._tb_writer is not None:
for k, v in metrics.items():
self._tb_writer.add_scalar(f"val/{k}", v, global_step=step or epoch)
def log_gradients(self, epoch: int, grad_norms: dict[str, float], step: int | None = None) -> None:
"""Log gradient norms per parameter group."""
if self._wandb_run is not None:
self._wandb_run.log(
{f"gradients/{k}": v for k, v in grad_norms.items()},
step=step or epoch,
)
if self._tb_writer is not None:
for k, v in grad_norms.items():
self._tb_writer.add_scalar(f"gradients/{k}", v, global_step=step or epoch)
def log_scalar(self, tag: str, value: float, step: int) -> None:
"""Log a single scalar."""
if self._wandb_run is not None:
self._wandb_run.log({tag: value}, step=step)
if self._tb_writer is not None:
self._tb_writer.add_scalar(tag, value, global_step=step)
def log_image(self, tag: str, image: Any, step: int, caption: str | None = None) -> None:
"""Log an image (numpy HWC or torch CHW).
Args:
tag: Image tag/name.
image: numpy array [H,W,C] or torch tensor [C,H,W].
step: Global step.
caption: Optional caption for W&B.
"""
if self._wandb_run is not None:
wandb = _try_import_wandb()
if isinstance(image, torch.Tensor):
image_np = image.detach().cpu().permute(1, 2, 0).numpy()
else:
image_np = image
self._wandb_run.log(
{tag: wandb.Image(image_np, caption=caption)},
step=step,
)
if self._tb_writer is not None:
if isinstance(image, torch.Tensor):
self._tb_writer.add_image(tag, image.detach().cpu(), global_step=step)
else:
self._tb_writer.add_image(tag, image, global_step=step, dataformats="HWC")
def log_histogram(self, tag: str, values: torch.Tensor, step: int) -> None:
"""Log a histogram of values (weights, activations, etc.)."""
if self._wandb_run is not None:
wandb = _try_import_wandb()
self._wandb_run.log(
{tag: wandb.Histogram(values.detach().cpu().numpy())},
step=step,
)
if self._tb_writer is not None:
self._tb_writer.add_histogram(tag, values.detach().cpu(), global_step=step)
def log_model_graph(self, model: torch.nn.Module, input_example: Any = None) -> None:
"""Log model graph to TensorBoard (if available)."""
if self._tb_writer is not None and input_example is not None:
try:
self._tb_writer.add_graph(model, input_example)
except Exception as e:
LOGGER.warning("Failed to log model graph: %s", e)
def watch_model(self, model: torch.nn.Module, log_freq: int = 100) -> None:
"""Enable W&B gradient/weight watching."""
if self._wandb_run is not None:
wandb = _try_import_wandb()
wandb.watch(model, log="all", log_freq=log_freq)
def log_summary(self, summary: dict[str, Any]) -> None:
"""Log final summary metrics (best R@1, etc.)."""
if self._wandb_run is not None:
for k, v in summary.items():
self._wandb_run.summary[k] = v
def close(self) -> None:
"""Flush and close all backends."""
if self._tb_writer is not None:
self._tb_writer.flush()
self._tb_writer.close()
if self._wandb_run is not None:
self._wandb_run.finish()