Files
commander-ai-lab/ml/scripts/ml_cli.py
T
2026-03-10 12:13:56 -04:00

372 lines
13 KiB
Python

"""
Commander AI Lab — ML Pipeline CLI
════════════════════════════════════
Unified CLI for ML training data workflows.
Commands:
status — Show ML logging status and available training data
build — Build training dataset from decision logs
train — Train policy network on built dataset (supervised)
eval — Evaluate a trained model on test data
inspect — Inspect a JSONL decision file (sample entries)
stats — Show action label distribution from decisions
ppo — Train with PPO (self-play reinforcement learning)
tournament — Run round-robin tournament evaluation
Usage:
python -m ml.scripts.ml_cli status
python -m ml.scripts.ml_cli build --results-dir results/
python -m ml.scripts.ml_cli train --epochs 50 --lr 1e-3
python -m ml.scripts.ml_cli eval
python -m ml.scripts.ml_cli inspect --file results/ml-decisions-abc12345.jsonl
python -m ml.scripts.ml_cli stats --results-dir results/
python -m ml.scripts.ml_cli ppo --iterations 100 --opponent heuristic
python -m ml.scripts.ml_cli tournament --episodes 50
"""
import argparse
import json
import os
import sys
from pathlib import Path
# Add project root to path
project_root = str(Path(__file__).parent.parent.parent)
if project_root not in sys.path:
sys.path.insert(0, project_root)
def cmd_status(args):
"""Show ML logging status and available data files."""
results_dir = Path(args.results_dir)
print("\n╔══════════════════════════════════════════╗")
print("║ Commander AI Lab — ML Training Status ║")
print("╚══════════════════════════════════════════╝\n")
if not results_dir.exists():
print(f" Results directory not found: {results_dir}")
print(" Run batch simulations first.\n")
return
# Find decision files
jsonl_files = sorted(results_dir.glob("ml-decisions-*.jsonl"))
if not jsonl_files:
print(" No ML decision files found.")
print(" Enable ML logging and run batch simulations:")
print(" • Web UI: POST /api/ml/toggle?enable=true")
print(" • CLI: java -jar ... --ml-log")
print()
return
total_decisions = 0
total_size = 0
print(f" {'File':<40} {'Decisions':>10} {'Size':>10}")
print(" " + "─" * 62)
for f in jsonl_files:
lines = 0
try:
with open(f) as fh:
lines = sum(1 for _ in fh)
except Exception:
pass
size = f.stat().st_size
total_decisions += lines
total_size += size
print(f" {f.name:<40} {lines:>10,} {size/1024:>9.1f}K")
print(" " + "─" * 62)
print(f" {'TOTAL':<40} {total_decisions:>10,} {total_size/1024:>9.1f}K")
# Check for embeddings
emb_path = Path(project_root) / "embeddings" / "mtg_embeddings.npz"
emb_status = "✓ Loaded" if emb_path.exists() else "✗ Not found"
print(f"\n Embeddings: {emb_status}")
# Check for existing datasets
models_dir = Path(project_root) / "ml" / "models"
npz_files = sorted(models_dir.glob("*.npz")) if models_dir.exists() else []
if npz_files:
print(f"\n Existing datasets:")
for f in npz_files:
print(f" {f.name} ({f.stat().st_size/1024:.1f}K)")
print()
def cmd_build(args):
"""Build training dataset."""
from ml.data.dataset_builder import build_dataset, split_dataset, save_dataset
dataset = build_dataset(
results_dir=args.results_dir,
embeddings_dir=args.embeddings_dir,
max_samples=args.max_samples,
)
if not dataset:
print("\nNo data to build. Enable ML logging and run simulations first.")
return
if args.no_split:
save_dataset(dataset, args.output)
else:
train, val, test = split_dataset(dataset)
base = args.output.replace(".npz", "")
save_dataset(train, f"{base}-train.npz")
save_dataset(val, f"{base}-val.npz")
save_dataset(test, f"{base}-test.npz")
print("\n✓ Dataset build complete.")
def cmd_inspect(args):
"""Inspect a JSONL decision file."""
filepath = Path(args.file)
if not filepath.exists():
print(f"File not found: {filepath}")
return
print(f"\n Inspecting: {filepath.name}\n")
count = 0
with open(filepath) as f:
for i, line in enumerate(f):
if i >= args.limit:
break
try:
d = json.loads(line.strip())
action = d.get("action", {})
players = d.get("players", [])
p0_life = players[0]["life"] if players else "?"
p1_life = players[1]["life"] if len(players) > 1 else "?"
print(f" [{i:>4}] Turn {d.get('turn', '?'):>2} "
f"| Phase: {d.get('phase', '?'):<8} "
f"| Seat {d.get('active_seat', '?')} "
f"| Life: {p0_life}/{p1_life} "
f"| {action.get('type', '?')}: {action.get('card', '-')}")
count += 1
except Exception as e:
print(f" [{i:>4}] ERROR: {e}")
with open(filepath) as f:
total = sum(1 for _ in f)
print(f"\n Showing {count} of {total:,} total decisions.\n")
def cmd_train(args):
"""Train policy network."""
from ml.training.trainer import main as trainer_main
# Re-construct sys.argv for the trainer CLI
argv = ["trainer"]
if args.data_dir:
argv += ["--data-dir", args.data_dir]
if args.epochs:
argv += ["--epochs", str(args.epochs)]
if args.batch_size:
argv += ["--batch-size", str(args.batch_size)]
if args.lr:
argv += ["--lr", str(args.lr)]
if args.patience:
argv += ["--patience", str(args.patience)]
if args.checkpoint_dir:
argv += ["--checkpoint-dir", args.checkpoint_dir]
if args.device:
argv += ["--device", args.device]
sys.argv = argv
trainer_main()
def cmd_eval(args):
"""Evaluate trained model."""
from ml.training.trainer import main as trainer_main
argv = ["trainer", "--eval-only"]
if args.data_dir:
argv += ["--data-dir", args.data_dir]
if args.checkpoint_dir:
argv += ["--checkpoint-dir", args.checkpoint_dir]
if args.checkpoint:
argv += ["--checkpoint", args.checkpoint]
if args.device:
argv += ["--device", args.device]
sys.argv = argv
trainer_main()
def cmd_ppo(args):
"""Train with PPO (self-play RL)."""
import logging
logging.basicConfig(level=logging.INFO, format="%(message)s")
from ml.training.ppo_trainer import PPOTrainer, PPOConfig
config = PPOConfig(
iterations=args.iterations,
episodes_per_iter=args.episodes_per_iter,
ppo_epochs=args.ppo_epochs,
batch_size=args.batch_size,
clip_epsilon=args.clip_epsilon,
entropy_coeff=args.entropy_coeff,
learning_rate=args.lr,
lr_schedule=args.lr_schedule,
buffer_size=args.buffer_size,
gamma=args.gamma,
gae_lambda=args.gae_lambda,
opponent=args.opponent,
playstyle=args.playstyle,
checkpoint_dir=args.checkpoint_dir or "ml/models/checkpoints",
save_every=args.save_every,
eval_every=args.eval_every,
eval_episodes=args.eval_episodes,
load_supervised=args.load_supervised,
)
trainer = PPOTrainer(config)
summary = trainer.train()
print(json.dumps(summary, indent=2))
def cmd_tournament(args):
"""Run round-robin tournament evaluation."""
import logging
logging.basicConfig(level=logging.INFO, format="%(message)s")
from ml.eval.tournament import main as tournament_main
argv = ["tournament"]
if args.episodes:
argv += ["--episodes", str(args.episodes)]
if args.checkpoint:
argv += ["--checkpoint", args.checkpoint]
if args.ppo_checkpoint:
argv += ["--ppo-checkpoint", args.ppo_checkpoint]
if args.playstyle:
argv += ["--playstyle", args.playstyle]
if args.output:
argv += ["--output", args.output]
sys.argv = argv
tournament_main()
def cmd_stats(args):
"""Show action label distribution."""
from ml.actions.labeler import label_decisions_file, print_label_distribution
import numpy as np
results_dir = Path(args.results_dir)
jsonl_files = sorted(results_dir.glob("ml-decisions-*.jsonl"))
if not jsonl_files:
print("No ML decision files found.")
return
all_labels = []
for f in jsonl_files:
labels, _ = label_decisions_file(str(f))
all_labels.append(labels)
combined = np.concatenate(all_labels)
print(f"\nCombined stats from {len(jsonl_files)} files:")
print_label_distribution(combined)
print()
def main():
parser = argparse.ArgumentParser(
description="Commander AI Lab — ML Pipeline CLI"
)
subparsers = parser.add_subparsers(dest="command")
# status
sp_status = subparsers.add_parser("status", help="Show ML training data status")
sp_status.add_argument("--results-dir", default="results")
# build
sp_build = subparsers.add_parser("build", help="Build training dataset")
sp_build.add_argument("--results-dir", default="results")
sp_build.add_argument("--embeddings-dir", default=None)
sp_build.add_argument("--output", default="ml/models/dataset.npz")
sp_build.add_argument("--max-samples", type=int, default=None)
sp_build.add_argument("--no-split", action="store_true")
# inspect
sp_inspect = subparsers.add_parser("inspect", help="Inspect a decision file")
sp_inspect.add_argument("--file", required=True)
sp_inspect.add_argument("--limit", type=int, default=20)
# train
sp_train = subparsers.add_parser("train", help="Train policy network")
sp_train.add_argument("--data-dir", default="ml/models")
sp_train.add_argument("--epochs", type=int, default=None)
sp_train.add_argument("--batch-size", type=int, default=None)
sp_train.add_argument("--lr", type=float, default=None)
sp_train.add_argument("--patience", type=int, default=None)
sp_train.add_argument("--checkpoint-dir", default=None)
sp_train.add_argument("--device", default=None)
# eval
sp_eval = subparsers.add_parser("eval", help="Evaluate trained model on test set")
sp_eval.add_argument("--data-dir", default="ml/models")
sp_eval.add_argument("--checkpoint-dir", default=None)
sp_eval.add_argument("--checkpoint", default=None)
sp_eval.add_argument("--device", default=None)
# stats
sp_stats = subparsers.add_parser("stats", help="Show action label distribution")
sp_stats.add_argument("--results-dir", default="results")
# ppo
sp_ppo = subparsers.add_parser("ppo", help="Train with PPO (self-play RL)")
sp_ppo.add_argument("--iterations", type=int, default=100)
sp_ppo.add_argument("--episodes-per-iter", type=int, default=64)
sp_ppo.add_argument("--ppo-epochs", type=int, default=4)
sp_ppo.add_argument("--batch-size", type=int, default=256)
sp_ppo.add_argument("--clip-epsilon", type=float, default=0.2)
sp_ppo.add_argument("--entropy-coeff", type=float, default=0.01)
sp_ppo.add_argument("--lr", type=float, default=3e-4)
sp_ppo.add_argument("--lr-schedule", default="constant", choices=["constant", "linear", "cosine"])
sp_ppo.add_argument("--buffer-size", type=int, default=4096)
sp_ppo.add_argument("--gamma", type=float, default=0.99)
sp_ppo.add_argument("--gae-lambda", type=float, default=0.95)
sp_ppo.add_argument("--opponent", default="heuristic", choices=["heuristic", "random", "self"])
sp_ppo.add_argument("--playstyle", default="midrange")
sp_ppo.add_argument("--checkpoint-dir", default=None)
sp_ppo.add_argument("--save-every", type=int, default=10)
sp_ppo.add_argument("--eval-every", type=int, default=5)
sp_ppo.add_argument("--eval-episodes", type=int, default=50)
sp_ppo.add_argument("--load-supervised", default=None,
help="Path to supervised checkpoint to initialize from")
# tournament
sp_tourney = subparsers.add_parser("tournament", help="Run round-robin tournament evaluation")
sp_tourney.add_argument("--episodes", type=int, default=50)
sp_tourney.add_argument("--checkpoint", default=None)
sp_tourney.add_argument("--ppo-checkpoint", default=None)
sp_tourney.add_argument("--playstyle", default="midrange")
sp_tourney.add_argument("--output", default=None)
args = parser.parse_args()
if args.command == "status":
cmd_status(args)
elif args.command == "build":
cmd_build(args)
elif args.command == "train":
cmd_train(args)
elif args.command == "eval":
cmd_eval(args)
elif args.command == "inspect":
cmd_inspect(args)
elif args.command == "stats":
cmd_stats(args)
elif args.command == "ppo":
cmd_ppo(args)
elif args.command == "tournament":
cmd_tournament(args)
else:
parser.print_help()
if __name__ == "__main__":
main()