loopkit 0.0.1a1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- loopkit-0.0.1a1/LICENSE +7 -0
- loopkit-0.0.1a1/PKG-INFO +44 -0
- loopkit-0.0.1a1/README.md +1 -0
- loopkit-0.0.1a1/pyproject.toml +68 -0
- loopkit-0.0.1a1/setup.cfg +4 -0
- loopkit-0.0.1a1/src/loopkit/__init__.py +85 -0
- loopkit-0.0.1a1/src/loopkit/cli/__init__.py +3 -0
- loopkit-0.0.1a1/src/loopkit/cli/compare.py +274 -0
- loopkit-0.0.1a1/src/loopkit/cli/main.py +62 -0
- loopkit-0.0.1a1/src/loopkit/cli/sweep.py +313 -0
- loopkit-0.0.1a1/src/loopkit/cli/visualize.py +285 -0
- loopkit-0.0.1a1/src/loopkit/config.py +1236 -0
- loopkit-0.0.1a1/src/loopkit/git.py +154 -0
- loopkit-0.0.1a1/src/loopkit/logger.py +729 -0
- loopkit-0.0.1a1/src/loopkit/monitor.py +259 -0
- loopkit-0.0.1a1/src/loopkit/torch/__init__.py +43 -0
- loopkit-0.0.1a1/src/loopkit/torch/checkpoint.py +339 -0
- loopkit-0.0.1a1/src/loopkit/torch/mp.py +346 -0
- loopkit-0.0.1a1/src/loopkit/tracking.py +203 -0
- loopkit-0.0.1a1/src/loopkit/utils.py +75 -0
- loopkit-0.0.1a1/src/loopkit.egg-info/PKG-INFO +44 -0
- loopkit-0.0.1a1/src/loopkit.egg-info/SOURCES.txt +24 -0
- loopkit-0.0.1a1/src/loopkit.egg-info/dependency_links.txt +1 -0
- loopkit-0.0.1a1/src/loopkit.egg-info/entry_points.txt +2 -0
- loopkit-0.0.1a1/src/loopkit.egg-info/requires.txt +23 -0
- loopkit-0.0.1a1/src/loopkit.egg-info/top_level.txt +1 -0
loopkit-0.0.1a1/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright (c) 2025 Kim Kunhee
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
loopkit-0.0.1a1/PKG-INFO
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: loopkit
|
|
3
|
+
Version: 0.0.1a1
|
|
4
|
+
Author-email: Kunhee Kim <kunhee.kim@kaist.ac.kr>
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Repository, https://github.com/kunheek/loopkit
|
|
7
|
+
Keywords: machine-learning,deep-learning,experiment-tracking,pytorch,configuration,logging,reproducibility,research,cli
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.8
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: numpy>=1.21.0
|
|
24
|
+
Requires-Dist: matplotlib>=3.5.0
|
|
25
|
+
Requires-Dist: polars>=0.20.0
|
|
26
|
+
Requires-Dist: psutil>=5.9.0
|
|
27
|
+
Requires-Dist: pyyaml>=6.0
|
|
28
|
+
Provides-Extra: wandb
|
|
29
|
+
Requires-Dist: wandb>=0.15.0; extra == "wandb"
|
|
30
|
+
Provides-Extra: tensorboard
|
|
31
|
+
Requires-Dist: tensorboard>=2.13.0; extra == "tensorboard"
|
|
32
|
+
Provides-Extra: tracking
|
|
33
|
+
Requires-Dist: wandb>=0.15.0; extra == "tracking"
|
|
34
|
+
Requires-Dist: tensorboard>=2.13.0; extra == "tracking"
|
|
35
|
+
Provides-Extra: dev
|
|
36
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
37
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
38
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
39
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
40
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
41
|
+
Requires-Dist: pre-commit>=3.0.0; extra == "dev"
|
|
42
|
+
Dynamic: license-file
|
|
43
|
+
|
|
44
|
+
# LoopKit
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# LoopKit
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "loopkit"
|
|
7
|
+
version = "0.0.1a1"
|
|
8
|
+
description = ""
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
authors = [
|
|
12
|
+
{name = "Kunhee Kim", email = "kunhee.kim@kaist.ac.kr"}
|
|
13
|
+
]
|
|
14
|
+
keywords = [
|
|
15
|
+
"machine-learning",
|
|
16
|
+
"deep-learning",
|
|
17
|
+
"experiment-tracking",
|
|
18
|
+
"pytorch",
|
|
19
|
+
"configuration",
|
|
20
|
+
"logging",
|
|
21
|
+
"reproducibility",
|
|
22
|
+
"research",
|
|
23
|
+
"cli"
|
|
24
|
+
]
|
|
25
|
+
classifiers = [
|
|
26
|
+
"Development Status :: 4 - Beta",
|
|
27
|
+
"Intended Audience :: Developers",
|
|
28
|
+
"Intended Audience :: Science/Research",
|
|
29
|
+
"License :: OSI Approved :: MIT License",
|
|
30
|
+
"Programming Language :: Python :: 3",
|
|
31
|
+
"Programming Language :: Python :: 3.8",
|
|
32
|
+
"Programming Language :: Python :: 3.9",
|
|
33
|
+
"Programming Language :: Python :: 3.10",
|
|
34
|
+
"Programming Language :: Python :: 3.11",
|
|
35
|
+
"Programming Language :: Python :: 3.12",
|
|
36
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
37
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
38
|
+
]
|
|
39
|
+
requires-python = ">=3.8"
|
|
40
|
+
dependencies = [
|
|
41
|
+
"numpy>=1.21.0",
|
|
42
|
+
"matplotlib>=3.5.0",
|
|
43
|
+
"polars>=0.20.0",
|
|
44
|
+
"psutil>=5.9.0",
|
|
45
|
+
"pyyaml>=6.0",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
[project.urls]
|
|
49
|
+
Repository = "https://github.com/kunheek/loopkit"
|
|
50
|
+
|
|
51
|
+
[project.scripts]
|
|
52
|
+
loopkit = "loopkit.cli:main"
|
|
53
|
+
|
|
54
|
+
[project.optional-dependencies]
|
|
55
|
+
wandb = ["wandb>=0.15.0"]
|
|
56
|
+
tensorboard = ["tensorboard>=2.13.0"]
|
|
57
|
+
tracking = ["wandb>=0.15.0", "tensorboard>=2.13.0"]
|
|
58
|
+
dev = [
|
|
59
|
+
"pytest>=7.0.0",
|
|
60
|
+
"pytest-cov>=4.0.0",
|
|
61
|
+
"black>=23.0.0",
|
|
62
|
+
"ruff>=0.1.0",
|
|
63
|
+
"mypy>=1.0.0",
|
|
64
|
+
"pre-commit>=3.0.0"
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
[tool.setuptools.packages.find]
|
|
68
|
+
where = ["src"]
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
__version__ = "0.0.1a1"
|
|
4
|
+
|
|
5
|
+
# Import submodules to make them available
|
|
6
|
+
from . import config, git, logger, monitor, tracking, utils
|
|
7
|
+
|
|
8
|
+
# Import optional modules
|
|
9
|
+
try:
|
|
10
|
+
from . import torch
|
|
11
|
+
except ImportError:
|
|
12
|
+
torch = None # type: ignore
|
|
13
|
+
|
|
14
|
+
# Import commonly used classes and functions
|
|
15
|
+
from .config import Config, override_config
|
|
16
|
+
from .logger import ExperimentLogger
|
|
17
|
+
from .utils import get_gpu_devices, set_seed, str2bool
|
|
18
|
+
|
|
19
|
+
# Export public API
|
|
20
|
+
__all__ = [
|
|
21
|
+
"__version__",
|
|
22
|
+
"Config",
|
|
23
|
+
"ExperimentLogger",
|
|
24
|
+
"config",
|
|
25
|
+
"debug",
|
|
26
|
+
"force_single_process",
|
|
27
|
+
"get_gpu_devices",
|
|
28
|
+
"git",
|
|
29
|
+
"log_level",
|
|
30
|
+
"logger",
|
|
31
|
+
"monitor",
|
|
32
|
+
"override_config",
|
|
33
|
+
"save_checkpoints",
|
|
34
|
+
"set_seed",
|
|
35
|
+
"str2bool",
|
|
36
|
+
"timer",
|
|
37
|
+
"torch",
|
|
38
|
+
"track_git",
|
|
39
|
+
"track_metrics",
|
|
40
|
+
"tracking",
|
|
41
|
+
"utils",
|
|
42
|
+
"verbose",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
# Global configuration flags
|
|
46
|
+
# These can be modified at runtime or set via environment variables
|
|
47
|
+
|
|
48
|
+
# Debug mode - enables debug logging and additional checks
|
|
49
|
+
# Set via: loopkit.debug = True or LK_DEBUG=1
|
|
50
|
+
debug = str2bool(os.environ.get("LK_DEBUG", "0"))
|
|
51
|
+
|
|
52
|
+
# Logging level - default log level for ExperimentLogger instances
|
|
53
|
+
# Options: DEBUG, INFO, WARNING, ERROR, CRITICAL
|
|
54
|
+
# Set via: loopkit.log_level = "DEBUG" or LK_LOG_LEVEL=DEBUG
|
|
55
|
+
log_level = os.environ.get("LK_LOG_LEVEL", "INFO").upper()
|
|
56
|
+
|
|
57
|
+
# Profiling - controls whether logger.timer() performs timing measurements
|
|
58
|
+
# Disable for production to eliminate profiling overhead
|
|
59
|
+
# Set via: loopkit.timer = False or LK_TIMER=0
|
|
60
|
+
timer = str2bool(os.environ.get("LK_TIMER", "1"))
|
|
61
|
+
|
|
62
|
+
# Verbose mode - controls console output verbosity
|
|
63
|
+
# Disable for cleaner logs when running many experiments
|
|
64
|
+
# Set via: loopkit.verbose = False or LK_VERBOSE=0
|
|
65
|
+
verbose = str2bool(os.environ.get("LK_VERBOSE", "1"))
|
|
66
|
+
|
|
67
|
+
# Checkpoint saving - controls whether to save model checkpoints
|
|
68
|
+
# Disable for quick experiments or testing to skip I/O
|
|
69
|
+
# Set via: loopkit.save_checkpoints = False or LK_SAVE_CHECKPOINTS=0
|
|
70
|
+
save_checkpoints = str2bool(os.environ.get("LK_SAVE_CHECKPOINTS", "1"))
|
|
71
|
+
|
|
72
|
+
# Metrics tracking - controls whether to log metrics to CSV
|
|
73
|
+
# Disable for faster iteration when metrics aren't needed
|
|
74
|
+
# Set via: loopkit.track_metrics = False or LK_TRACK_METRICS=0
|
|
75
|
+
track_metrics = str2bool(os.environ.get("LK_TRACK_METRICS", "1"))
|
|
76
|
+
|
|
77
|
+
# Git tracking - controls whether to collect git repository information
|
|
78
|
+
# Disable to skip git operations (useful in CI/CD or when git is slow)
|
|
79
|
+
# Set via: loopkit.track_git = False or LK_TRACK_GIT=0
|
|
80
|
+
track_git = str2bool(os.environ.get("LK_TRACK_GIT", "1"))
|
|
81
|
+
|
|
82
|
+
# Force single process - force single-process mode in distributed environments
|
|
83
|
+
# Useful for debugging distributed code without multiple processes
|
|
84
|
+
# Set via: loopkit.force_single_process = True or LK_FORCE_SINGLE=1
|
|
85
|
+
force_single_process = str2bool(os.environ.get("LK_FORCE_SINGLE", "0"))
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Dict, List, Optional, Union
|
|
5
|
+
|
|
6
|
+
import polars as pl
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def load_run_info(run_dir: Union[str, Path]) -> Dict:
|
|
11
|
+
"""Load information about a single run.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
run_dir: Path to run directory
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
Dict with run information including config, metrics, git info
|
|
18
|
+
"""
|
|
19
|
+
run_dir = Path(run_dir)
|
|
20
|
+
info = {"run_dir": str(run_dir), "run_id": run_dir.name}
|
|
21
|
+
|
|
22
|
+
# Load config
|
|
23
|
+
config_file = run_dir / "run.yaml"
|
|
24
|
+
if config_file.exists():
|
|
25
|
+
with open(config_file) as f:
|
|
26
|
+
info["config"] = yaml.safe_load(f)
|
|
27
|
+
|
|
28
|
+
# Load best metrics
|
|
29
|
+
best_file = run_dir / "best.json"
|
|
30
|
+
if best_file.exists():
|
|
31
|
+
with open(best_file) as f:
|
|
32
|
+
info["best_metrics"] = json.load(f)
|
|
33
|
+
|
|
34
|
+
# Load final metrics from CSV
|
|
35
|
+
metrics_file = run_dir / "metrics.csv"
|
|
36
|
+
if metrics_file.exists():
|
|
37
|
+
df = pl.read_csv(metrics_file)
|
|
38
|
+
if not df.is_empty():
|
|
39
|
+
# Get last values for each metric
|
|
40
|
+
final_metrics = {}
|
|
41
|
+
for name in df["name"].unique():
|
|
42
|
+
metric_df = df.filter(pl.col("name") == name)
|
|
43
|
+
if not metric_df.is_empty():
|
|
44
|
+
final_metrics[name] = metric_df.tail(1)["value"][0]
|
|
45
|
+
info["final_metrics"] = final_metrics
|
|
46
|
+
|
|
47
|
+
# Load git info
|
|
48
|
+
git_file = run_dir / "git_info.json"
|
|
49
|
+
if git_file.exists():
|
|
50
|
+
with open(git_file) as f:
|
|
51
|
+
info["git"] = json.load(f)
|
|
52
|
+
|
|
53
|
+
return info
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def compare_runs(
|
|
57
|
+
run_dirs: List[Union[str, Path]],
|
|
58
|
+
metrics: Optional[List[str]] = None,
|
|
59
|
+
show_config: bool = True,
|
|
60
|
+
show_git: bool = False,
|
|
61
|
+
) -> pl.DataFrame:
|
|
62
|
+
"""Compare multiple experiment runs.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
run_dirs: List of run directories to compare
|
|
66
|
+
metrics: Specific metrics to compare (None = all)
|
|
67
|
+
show_config: Whether to include config values
|
|
68
|
+
show_git: Whether to include git info
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
DataFrame with comparison
|
|
72
|
+
"""
|
|
73
|
+
runs_data = []
|
|
74
|
+
|
|
75
|
+
for run_dir in run_dirs:
|
|
76
|
+
info = load_run_info(run_dir)
|
|
77
|
+
row = {"run_id": info["run_id"], "run_dir": info["run_dir"]}
|
|
78
|
+
|
|
79
|
+
# Add metrics
|
|
80
|
+
if "best_metrics" in info:
|
|
81
|
+
for metric_name, metric_info in info["best_metrics"].items():
|
|
82
|
+
if metrics is None or metric_name in metrics:
|
|
83
|
+
row[f"best_{metric_name}"] = metric_info.get("value")
|
|
84
|
+
row[f"best_{metric_name}_step"] = metric_info.get("step")
|
|
85
|
+
|
|
86
|
+
if "final_metrics" in info:
|
|
87
|
+
for metric_name, value in info["final_metrics"].items():
|
|
88
|
+
if metrics is None or metric_name in metrics:
|
|
89
|
+
row[f"final_{metric_name}"] = value
|
|
90
|
+
|
|
91
|
+
# Add config
|
|
92
|
+
if show_config and "config" in info:
|
|
93
|
+
config = info["config"]
|
|
94
|
+
# Flatten config
|
|
95
|
+
for key, value in _flatten_dict(config).items():
|
|
96
|
+
row[f"config.{key}"] = value
|
|
97
|
+
|
|
98
|
+
# Add git info
|
|
99
|
+
if show_git and "git" in info:
|
|
100
|
+
row["git_sha"] = info["git"]["sha_short"]
|
|
101
|
+
row["git_dirty"] = info["git"]["dirty"]
|
|
102
|
+
|
|
103
|
+
runs_data.append(row)
|
|
104
|
+
|
|
105
|
+
return pl.DataFrame(runs_data)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _flatten_dict(d: Dict, parent_key: str = "", sep: str = ".") -> Dict:
|
|
109
|
+
"""Flatten nested dictionary."""
|
|
110
|
+
items = []
|
|
111
|
+
for k, v in d.items():
|
|
112
|
+
new_key = f"{parent_key}{sep}{k}" if parent_key else k
|
|
113
|
+
if isinstance(v, dict):
|
|
114
|
+
items.extend(_flatten_dict(v, new_key, sep=sep).items())
|
|
115
|
+
else:
|
|
116
|
+
items.append((new_key, v))
|
|
117
|
+
return dict(items)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def find_config_diff(run_dirs: List[Union[str, Path]]) -> Dict[str, List]:
|
|
121
|
+
"""Find configuration differences between runs.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
run_dirs: List of run directories
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Dict mapping config keys to their values across runs
|
|
128
|
+
"""
|
|
129
|
+
all_configs = []
|
|
130
|
+
for run_dir in run_dirs:
|
|
131
|
+
info = load_run_info(run_dir)
|
|
132
|
+
if "config" in info:
|
|
133
|
+
all_configs.append(_flatten_dict(info["config"]))
|
|
134
|
+
|
|
135
|
+
if not all_configs:
|
|
136
|
+
return {}
|
|
137
|
+
|
|
138
|
+
# Find keys that differ
|
|
139
|
+
all_keys = set()
|
|
140
|
+
for config in all_configs:
|
|
141
|
+
all_keys.update(config.keys())
|
|
142
|
+
|
|
143
|
+
diffs = {}
|
|
144
|
+
for key in sorted(all_keys):
|
|
145
|
+
values = [config.get(key, None) for config in all_configs]
|
|
146
|
+
# Only include if values differ
|
|
147
|
+
if len(set(str(v) for v in values)) > 1:
|
|
148
|
+
diffs[key] = values
|
|
149
|
+
|
|
150
|
+
return diffs
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def print_comparison(
|
|
154
|
+
run_dirs: List[Union[str, Path]],
|
|
155
|
+
metrics: Optional[List[str]] = None,
|
|
156
|
+
show_all_config: bool = False,
|
|
157
|
+
):
|
|
158
|
+
"""Print a formatted comparison of runs.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
run_dirs: List of run directories to compare
|
|
162
|
+
metrics: Specific metrics to compare (None = all)
|
|
163
|
+
show_all_config: If False, only show differing config values
|
|
164
|
+
"""
|
|
165
|
+
print(f"\n{'=' * 80}")
|
|
166
|
+
print(f"Comparing {len(run_dirs)} runs")
|
|
167
|
+
print(f"{'=' * 80}\n")
|
|
168
|
+
|
|
169
|
+
# Load run info
|
|
170
|
+
runs_info = [load_run_info(rd) for rd in run_dirs]
|
|
171
|
+
|
|
172
|
+
# Print run IDs
|
|
173
|
+
print("Run IDs:")
|
|
174
|
+
for i, info in enumerate(runs_info):
|
|
175
|
+
print(f" [{i}] {info['run_id']}")
|
|
176
|
+
print()
|
|
177
|
+
|
|
178
|
+
# Print config differences
|
|
179
|
+
if show_all_config:
|
|
180
|
+
print("Configuration:")
|
|
181
|
+
df = compare_runs(run_dirs, show_config=True, show_git=False)
|
|
182
|
+
config_cols = [c for c in df.columns if c.startswith("config.")]
|
|
183
|
+
if config_cols:
|
|
184
|
+
for col in sorted(config_cols):
|
|
185
|
+
print(f" {col}:")
|
|
186
|
+
for i, val in enumerate(df[col]):
|
|
187
|
+
print(f" [{i}] {val}")
|
|
188
|
+
print()
|
|
189
|
+
else:
|
|
190
|
+
diffs = find_config_diff(run_dirs)
|
|
191
|
+
if diffs:
|
|
192
|
+
print("Configuration Differences:")
|
|
193
|
+
for key, values in diffs.items():
|
|
194
|
+
print(f" {key}:")
|
|
195
|
+
for i, val in enumerate(values):
|
|
196
|
+
print(f" [{i}] {val}")
|
|
197
|
+
print()
|
|
198
|
+
else:
|
|
199
|
+
print("No configuration differences found.\n")
|
|
200
|
+
|
|
201
|
+
# Print metrics comparison
|
|
202
|
+
print("Metrics Comparison:")
|
|
203
|
+
df = compare_runs(run_dirs, metrics=metrics, show_config=False, show_git=False)
|
|
204
|
+
|
|
205
|
+
# Show best metrics
|
|
206
|
+
best_cols = [
|
|
207
|
+
c for c in df.columns if c.startswith("best_") and not c.endswith("_step")
|
|
208
|
+
]
|
|
209
|
+
if best_cols:
|
|
210
|
+
print("\n Best Metrics:")
|
|
211
|
+
for col in sorted(best_cols):
|
|
212
|
+
metric_name = col.replace("best_", "")
|
|
213
|
+
print(f" {metric_name}:")
|
|
214
|
+
for i, val in enumerate(df[col]):
|
|
215
|
+
step_col = f"best_{metric_name}_step"
|
|
216
|
+
step = df[step_col].iloc[i] if step_col in df.columns else "?"
|
|
217
|
+
print(f" [{i}] {val:.4f} (step {step})")
|
|
218
|
+
|
|
219
|
+
# Show final metrics
|
|
220
|
+
final_cols = [c for c in df.columns if c.startswith("final_")]
|
|
221
|
+
if final_cols:
|
|
222
|
+
print("\n Final Metrics:")
|
|
223
|
+
for col in sorted(final_cols):
|
|
224
|
+
metric_name = col.replace("final_", "")
|
|
225
|
+
print(f" {metric_name}:")
|
|
226
|
+
for i, val in enumerate(df[col]):
|
|
227
|
+
print(f" [{i}] {val:.4f}")
|
|
228
|
+
|
|
229
|
+
print(f"\n{'=' * 80}\n")
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def setup_compare_parser(subparsers):
|
|
233
|
+
"""Setup the compare subcommand parser."""
|
|
234
|
+
compare_parser = subparsers.add_parser(
|
|
235
|
+
"compare",
|
|
236
|
+
help="Compare multiple experiment runs",
|
|
237
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
238
|
+
epilog="""
|
|
239
|
+
Examples:
|
|
240
|
+
# Compare runs
|
|
241
|
+
loopkit compare runs/exp1 runs/exp2 runs/exp3
|
|
242
|
+
|
|
243
|
+
# Compare specific metrics
|
|
244
|
+
loopkit compare runs/exp_* --metrics loss accuracy
|
|
245
|
+
|
|
246
|
+
# Show all config (not just diffs)
|
|
247
|
+
loopkit compare runs/exp1 runs/exp2 --all-config
|
|
248
|
+
|
|
249
|
+
# Export to CSV
|
|
250
|
+
loopkit compare runs/exp_* --export comparison.csv
|
|
251
|
+
""",
|
|
252
|
+
)
|
|
253
|
+
compare_parser.add_argument(
|
|
254
|
+
"run_dirs", nargs="+", help="Run directories to compare"
|
|
255
|
+
)
|
|
256
|
+
compare_parser.add_argument(
|
|
257
|
+
"--metrics", "-m", nargs="+", help="Specific metrics to compare"
|
|
258
|
+
)
|
|
259
|
+
compare_parser.add_argument(
|
|
260
|
+
"--all-config", action="store_true", help="Show all config, not just diffs"
|
|
261
|
+
)
|
|
262
|
+
compare_parser.add_argument("--export", "-e", help="Export comparison to CSV file")
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def run_compare_command(args):
|
|
266
|
+
"""Run the compare command."""
|
|
267
|
+
if args.export:
|
|
268
|
+
df = compare_runs(args.run_dirs, metrics=args.metrics)
|
|
269
|
+
df.write_csv(args.export)
|
|
270
|
+
print(f"✅ Comparison exported to {args.export}")
|
|
271
|
+
else:
|
|
272
|
+
print_comparison(
|
|
273
|
+
args.run_dirs, metrics=args.metrics, show_all_config=args.all_config
|
|
274
|
+
)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import argparse
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main():
|
|
7
|
+
"""Main CLI entrypoint for LoopKit."""
|
|
8
|
+
parser = argparse.ArgumentParser(
|
|
9
|
+
prog="loopkit",
|
|
10
|
+
description="LoopKit CLI",
|
|
11
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
12
|
+
epilog="""
|
|
13
|
+
Commands:
|
|
14
|
+
sweep Run hyperparameter sweeps
|
|
15
|
+
viz Visualize experiment metrics
|
|
16
|
+
compare Compare multiple experiment runs
|
|
17
|
+
|
|
18
|
+
Examples:
|
|
19
|
+
loopkit sweep "python train.py {config}" --config config.yaml --sweep "lr=[0.001,0.01]"
|
|
20
|
+
loopkit viz runs/exp_* --metrics loss accuracy
|
|
21
|
+
loopkit compare runs/exp1 runs/exp2 runs/exp3
|
|
22
|
+
""",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
parser.add_argument("--version", action="version", version="%(prog)s 0.1.0")
|
|
26
|
+
|
|
27
|
+
subparsers = parser.add_subparsers(dest="command", help="Command to run")
|
|
28
|
+
|
|
29
|
+
# Import command setup functions
|
|
30
|
+
from .sweep import setup_sweep_parser
|
|
31
|
+
from .visualize import setup_visualize_parser
|
|
32
|
+
from .compare import setup_compare_parser
|
|
33
|
+
|
|
34
|
+
# Setup subcommands
|
|
35
|
+
setup_sweep_parser(subparsers)
|
|
36
|
+
setup_visualize_parser(subparsers)
|
|
37
|
+
setup_compare_parser(subparsers)
|
|
38
|
+
|
|
39
|
+
# Parse arguments
|
|
40
|
+
args = parser.parse_args()
|
|
41
|
+
|
|
42
|
+
if not args.command:
|
|
43
|
+
parser.print_help()
|
|
44
|
+
sys.exit(1)
|
|
45
|
+
|
|
46
|
+
# Dispatch to appropriate command
|
|
47
|
+
if args.command == "sweep":
|
|
48
|
+
from .sweep import run_sweep_command
|
|
49
|
+
|
|
50
|
+
run_sweep_command(args)
|
|
51
|
+
elif args.command in ("viz", "visualize"):
|
|
52
|
+
from .visualize import run_visualize_command
|
|
53
|
+
|
|
54
|
+
run_visualize_command(args)
|
|
55
|
+
elif args.command == "compare":
|
|
56
|
+
from .compare import run_compare_command
|
|
57
|
+
|
|
58
|
+
run_compare_command(args)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if __name__ == "__main__":
|
|
62
|
+
main()
|