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>
250 lines
9.3 KiB
Python
250 lines
9.3 KiB
Python
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()
|