372 lines
13 KiB
Python
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()
|