Files
caption-test/src/training/plot_metrics.py
pikaliov ce7892926f Add Recall@K and AP panels to train_metrics.png
train_metrics.png now has 6 panels (2x3):
  Row 1: Train Loss, Train R@1/5/10, Train AP
  Row 2: Temperature, Gates, Learning Rate

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 13:01:51 +03:00

250 lines
9.3 KiB
Python
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.
from __future__ import annotations
"""Generate training/validation metric plots from CSV logs.
Reads train.csv and val.csv, produces publication-quality plots
using seaborn + matplotlib. Called automatically after each eval epoch
or standalone via CLI.
Usage:
python -m src.training.plot_metrics --log-dir out/gtauav/with_text/logs
"""
import argparse
import logging
from pathlib import Path
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
LOGGER = logging.getLogger("caption_test.plot")
# Seaborn style.
sns.set_theme(style="whitegrid", palette="deep", font_scale=1.1)
def plot_train_metrics(train_df: pd.DataFrame, out_dir: Path) -> None:
"""Plot training metrics: loss, recall, AP, temperature, gates, lr."""
fig, axes = plt.subplots(2, 3, figsize=(20, 10))
fig.suptitle("Training Metrics", fontsize=16, fontweight="bold")
# 1. Loss.
ax = axes[0, 0]
col = "train_loss" if "train_loss" in train_df.columns else "total"
if col in train_df.columns:
sns.lineplot(data=train_df, x="epoch", y=col, ax=ax, marker="o", linewidth=2)
ax.set_title("Train Loss")
ax.set_xlabel("Epoch")
ax.set_ylabel("InfoNCE Loss")
# 2. Recall@K (train).
ax = axes[0, 1]
for k in [1, 5, 10]:
col = f"r@{k}_q2g"
if col in train_df.columns:
df_valid = train_df.dropna(subset=[col])
if not df_valid.empty:
sns.lineplot(data=df_valid, x="epoch", y=col, ax=ax, marker="o", linewidth=2, label=f"R@{k}")
ax.set_title("Train Recall@K (drone → sat)")
ax.set_xlabel("Epoch")
ax.set_ylabel("Recall")
ax.set_ylim(0, 1)
ax.legend()
# 3. Average Precision (train).
ax = axes[0, 2]
if "ap_q2g" in train_df.columns:
df_valid = train_df.dropna(subset=["ap_q2g"])
if not df_valid.empty:
sns.lineplot(data=df_valid, x="epoch", y="ap_q2g", ax=ax, marker="s", linewidth=2, color="purple")
ax.set_title("Train AP (drone → sat)")
ax.set_xlabel("Epoch")
ax.set_ylabel("AP")
ax.set_ylim(0, 1)
# 4. Temperature (tau).
ax = axes[1, 0]
if "temperature" in train_df.columns:
sns.lineplot(data=train_df, x="epoch", y="temperature", ax=ax, marker="s", linewidth=2, color="orange")
ax.set_title("Temperature (τ)")
ax.set_xlabel("Epoch")
ax.set_ylabel("τ")
# 5. Gate values.
ax = axes[1, 1]
if "gate_q" in train_df.columns and "gate_g" in train_df.columns:
sns.lineplot(data=train_df, x="epoch", y="gate_q", ax=ax, marker="o", linewidth=2, label="gate_q (drone)")
sns.lineplot(data=train_df, x="epoch", y="gate_g", ax=ax, marker="s", linewidth=2, label="gate_g (sat)")
ax.legend()
ax.set_title("Gate Values (σ(α))")
ax.set_xlabel("Epoch")
ax.set_ylabel("Image weight")
ax.set_ylim(0, 1)
# 6. Learning rate.
ax = axes[1, 2]
if "lr" in train_df.columns:
sns.lineplot(data=train_df, x="epoch", y="lr", ax=ax, marker="^", linewidth=2, color="green")
ax.set_title("Learning Rate")
ax.set_xlabel("Epoch")
ax.set_ylabel("LR")
ax.ticklabel_format(axis="y", style="scientific", scilimits=(0, 0))
plt.tight_layout()
path = out_dir / "train_metrics.png"
fig.savefig(path, dpi=150, bbox_inches="tight")
plt.close(fig)
LOGGER.info("📊 Saved %s", path)
def plot_val_metrics(val_df: pd.DataFrame, out_dir: Path, train_recall_df: pd.DataFrame | None = None) -> None:
"""Plot recall + AP metrics: train vs val."""
fig, axes = plt.subplots(1, 3, figsize=(20, 5))
fig.suptitle("Retrieval Metrics (train vs val)", fontsize=16, fontweight="bold")
# 1. Recall@K (q→g): train + val.
ax = axes[0]
for k in [1, 5, 10]:
col = f"r@{k}_q2g"
if col in val_df.columns:
sns.lineplot(data=val_df, x="epoch", y=col, ax=ax, marker="o", linewidth=2, label=f"val R@{k}")
if train_recall_df is not None and col in train_recall_df.columns:
sns.lineplot(data=train_recall_df, x="epoch", y=col, ax=ax, marker="x", linewidth=1.5, linestyle="--", label=f"train R@{k}")
ax.set_title("Recall@K (drone → satellite)")
ax.set_xlabel("Epoch")
ax.set_ylabel("Recall")
ax.set_ylim(0, 1)
ax.legend(fontsize=8)
# 2. Recall@K (g→q): train + val.
ax = axes[1]
for k in [1, 5, 10]:
col = f"r@{k}_g2q"
if col in val_df.columns:
sns.lineplot(data=val_df, x="epoch", y=col, ax=ax, marker="s", linewidth=2, label=f"val R@{k}")
if train_recall_df is not None and col in train_recall_df.columns:
sns.lineplot(data=train_recall_df, x="epoch", y=col, ax=ax, marker="x", linewidth=1.5, linestyle="--", label=f"train R@{k}")
ax.set_title("Recall@K (satellite → drone)")
ax.set_xlabel("Epoch")
ax.set_ylabel("Recall")
ax.set_ylim(0, 1)
ax.legend(fontsize=8)
# 3. AP (train vs val, both directions).
ax = axes[2]
if "ap_q2g" in val_df.columns:
sns.lineplot(data=val_df, x="epoch", y="ap_q2g", ax=ax, marker="o", linewidth=2, color="royalblue", label="val AP q→g")
if "ap_g2q" in val_df.columns:
sns.lineplot(data=val_df, x="epoch", y="ap_g2q", ax=ax, marker="s", linewidth=2, color="coral", label="val AP g→q")
if train_recall_df is not None:
if "ap_q2g" in train_recall_df.columns:
sns.lineplot(data=train_recall_df, x="epoch", y="ap_q2g", ax=ax, marker="x", linewidth=1.5, linestyle="--", color="royalblue", label="train AP q→g")
if "ap_g2q" in train_recall_df.columns:
sns.lineplot(data=train_recall_df, x="epoch", y="ap_g2q", ax=ax, marker="x", linewidth=1.5, linestyle="--", color="coral", label="train AP g→q")
ax.set_title("Average Precision")
ax.set_xlabel("Epoch")
ax.set_ylabel("AP")
ax.set_ylim(0, 1)
ax.legend(fontsize=8)
plt.tight_layout()
path = out_dir / "val_metrics.png"
fig.savefig(path, dpi=150, bbox_inches="tight")
plt.close(fig)
LOGGER.info("📊 Saved %s", path)
def plot_combined(train_df: pd.DataFrame, val_df: pd.DataFrame, out_dir: Path) -> None:
"""Combined overview: loss + R@1 on same figure."""
fig, axes = plt.subplots(1, 3, figsize=(18, 5))
fig.suptitle("Training Overview", fontsize=16, fontweight="bold")
# 1. Train + Val loss.
ax = axes[0]
sns.lineplot(data=train_df, x="epoch", y="total", ax=ax, marker="o", linewidth=2, color="crimson", label="train")
if "loss" in val_df.columns:
sns.lineplot(data=val_df, x="epoch", y="loss", ax=ax, marker="s", linewidth=2, color="royalblue", label="val")
ax.set_title("Loss (train vs val)")
ax.set_xlabel("Epoch")
ax.set_ylabel("InfoNCE Loss")
ax.legend()
# 2. Val R@1 q→g.
ax = axes[1]
if "r@1_q2g" in val_df.columns:
sns.lineplot(data=val_df, x="epoch", y="r@1_q2g", ax=ax, marker="o", linewidth=2, color="royalblue", label="R@1 q→g")
if "r@1_g2q" in val_df.columns:
sns.lineplot(data=val_df, x="epoch", y="r@1_g2q", ax=ax, marker="s", linewidth=2, color="coral", label="R@1 g→q")
ax.set_title("Validation R@1")
ax.set_xlabel("Epoch")
ax.set_ylabel("Recall@1")
ax.set_ylim(0, None)
ax.legend()
# 3. Gates + Temperature.
ax = axes[2]
if "gate_q" in train_df.columns:
sns.lineplot(data=train_df, x="epoch", y="gate_q", ax=ax, linewidth=2, label="gate_q")
sns.lineplot(data=train_df, x="epoch", y="gate_g", ax=ax, linewidth=2, label="gate_g")
ax2 = ax.twinx()
sns.lineplot(data=train_df, x="epoch", y="temperature", ax=ax2, linewidth=2, color="orange", linestyle="--", label="τ")
ax.set_title("Gates & Temperature")
ax.set_xlabel("Epoch")
ax.set_ylabel("Gate value")
ax2.set_ylabel("τ")
lines1, labels1 = ax.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
ax.legend(lines1 + lines2, labels1 + labels2, loc="best")
ax2.get_legend().remove() if ax2.get_legend() else None
plt.tight_layout()
path = out_dir / "overview.png"
fig.savefig(path, dpi=150, bbox_inches="tight")
plt.close(fig)
LOGGER.info("📊 Saved %s", path)
def generate_plots(log_dir: str | Path) -> None:
"""Generate all plots from CSV logs in log_dir."""
log_dir = Path(log_dir)
train_csv = log_dir / "train.csv"
val_csv = log_dir / "val.csv"
if train_csv.exists():
train_df = pd.read_csv(train_csv)
plot_train_metrics(train_df, log_dir)
else:
LOGGER.warning("No train.csv found in %s", log_dir)
train_df = None
train_recall_csv = log_dir / "train_recall.csv"
train_recall_df = None
if train_recall_csv.exists():
train_recall_df = pd.read_csv(train_recall_csv)
if val_csv.exists():
val_df = pd.read_csv(val_csv)
plot_val_metrics(val_df, log_dir, train_recall_df=train_recall_df)
else:
LOGGER.warning("No val.csv found in %s", log_dir)
val_df = None
if train_df is not None and val_df is not None:
plot_combined(train_df, val_df, log_dir)
def main() -> None:
import coloredlogs
coloredlogs.install(level="INFO", logger=LOGGER, fmt="%(asctime)s %(name)s %(levelname)s %(message)s")
parser = argparse.ArgumentParser(description="Generate metric plots from CSV logs.")
parser.add_argument("--log-dir", type=str, required=True, help="Path to logs/ directory.")
args = parser.parse_args()
generate_plots(args.log_dir)
if __name__ == "__main__":
main()