ho22joshua commited on
Commit
7997c78
·
1 Parent(s): 565354b

feat: add Hydra configuration and CLI workflows

Browse files
configs/checkpoint/default.yaml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ directory: ${environment.output_root}/checkpoints
2
+ resume: null
3
+ pretrained: null
4
+ save_every_epochs: 1
5
+ save_best: false
6
+ save_last: true
configs/config.yaml ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ defaults:
2
+ - data: local_test
3
+ - model: root_gnn/edge_network
4
+ - task: pretraining_multiclass
5
+ - trainer: default
6
+ - checkpoint: default
7
+ - inference: default
8
+ - environment: local
9
+ - _self_
10
+ experiment:
11
+ name: pretraining_multiclass
12
+ logging:
13
+ level: INFO
14
+ hydra:
15
+ run:
16
+ dir: .
17
+ job:
18
+ chdir: false
configs/data/delphes.yaml ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ defaults:
2
+ - /data/local_test
3
+ - _self_
4
+ tree_name: Events
configs/data/local_test.yaml ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ files: []
2
+ tree_name: Events
3
+ batch_size: 32
4
+ num_workers: 0
5
+ shuffle: true
6
+ seed: 42
7
+ cache:
8
+ path: null
9
+ splits:
10
+ train_folds: [0, 1, 2]
11
+ validation_folds: [3]
12
+ test_folds: [4]
configs/environment/local.yaml ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ device: cpu
2
+ data_root: null
3
+ output_root: outputs/${experiment.name}
configs/environment/perlmutter.yaml ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ device: cuda
2
+ data_root: null
3
+ output_root: outputs/${experiment.name}
configs/inference/default.yaml ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ checkpoint: null
2
+ output: ${environment.output_root}/predictions/predictions.npz
3
+ format: npz
4
+ split: test
configs/model/root_gnn/edge_network.yaml ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ family: root_gnn
2
+ name: edge_network
3
+ hid_size: 128
4
+ out_size: 12
5
+ n_layers: 2
6
+ n_proc_steps: 4
7
+ dropout: 0.1
configs/model/root_gnn/fine_tuned_edge_network.yaml ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ family: root_gnn
2
+ name: fine_tuned_edge_network
3
+ hid_size: 128
4
+ out_size: 1
5
+ n_layers: 2
6
+ n_proc_steps: 4
7
+ dropout: 0.1
8
+ freeze_backbone: true
9
+ pretrained_checkpoint: null
configs/task/binary_classification.yaml ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ type: binary_classification
2
+ threshold: 0.5
3
+ absolute_weights: false
configs/task/pretraining_multiclass.yaml ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ type: multiclass_classification
2
+ num_classes: 12
3
+ absolute_weights: false
configs/task/tth_cp_finetune.yaml ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ defaults:
2
+ - /task/binary_classification
3
+ - _self_
configs/trainer/debug.yaml ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ defaults:
2
+ - /trainer/default
3
+ - _self_
4
+ max_epochs: 1
configs/trainer/default.yaml ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ max_epochs: 100
2
+ device: ${environment.device}
3
+ seed: 42
4
+ optimizer:
5
+ name: adam
6
+ learning_rate: 0.001
7
+ weight_decay: 0.0
8
+ scheduler:
9
+ name: null
10
+ early_stopping:
11
+ enabled: false
12
+ monitor: loss
13
+ mode: min
14
+ patience: 10
15
+ min_delta: 1.0e-8
pyproject.toml CHANGED
@@ -22,6 +22,9 @@ ml = [
22
  "torch>=2.0",
23
  ]
24
 
 
 
 
25
  [dependency-groups]
26
  dev = [
27
  "pytest>=8",
 
22
  "torch>=2.0",
23
  ]
24
 
25
+ [project.scripts]
26
+ gnn4colliders = "gnn4colliders.cli:main"
27
+
28
  [dependency-groups]
29
  dev = [
30
  "pytest>=8",
src/gnn4colliders/cli/__init__.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Single, thin command-line entry point for GNN4Colliders."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ from hydra import compose, initialize_config_dir
10
+ from omegaconf import OmegaConf
11
+
12
+ from gnn4colliders.config.application import predict_or_evaluate, prepare, train
13
+
14
+ _COMMANDS = {"prepare", "train", "evaluate", "predict"}
15
+
16
+
17
+ def _help(command: str | None = None) -> None:
18
+ if command:
19
+ print(f"Usage: gnn4colliders {command} [key=value ...]")
20
+ print("Compose Hydra configuration with semantic overrides.")
21
+ else:
22
+ print("Usage: gnn4colliders <prepare|train|evaluate|predict> [key=value ...]")
23
+ print("Use gnn4colliders <command> --help for command help.")
24
+
25
+
26
+ def _config(overrides: list[str]):
27
+ config_dir = Path(__file__).resolve().parents[3] / "configs"
28
+ with initialize_config_dir(version_base=None, config_dir=str(config_dir)):
29
+ return compose(config_name="config", overrides=overrides)
30
+
31
+
32
+ def main(argv: list[str] | None = None) -> int:
33
+ args = list(sys.argv[1:] if argv is None else argv)
34
+ if not args or args[0] in {"-h", "--help"}:
35
+ _help()
36
+ return 0
37
+ command = args.pop(0)
38
+ if command not in _COMMANDS:
39
+ print(
40
+ f"unknown command {command!r}; choose from {', '.join(sorted(_COMMANDS))}",
41
+ file=sys.stderr,
42
+ )
43
+ return 2
44
+ if "--help" in args or "-h" in args:
45
+ _help(command)
46
+ return 0
47
+ try:
48
+ config = _config(args)
49
+ logging.basicConfig(
50
+ level=getattr(logging, str(config.logging.level).upper(), logging.INFO)
51
+ )
52
+ if command == "prepare":
53
+ print(f"prepared cache: {prepare(config)}")
54
+ elif command == "train":
55
+ print(f"saved checkpoint: {train(config)}")
56
+ elif command == "evaluate":
57
+ result = predict_or_evaluate(config, evaluate=True)
58
+ print(OmegaConf.to_yaml(result.metrics))
59
+ else:
60
+ result = predict_or_evaluate(config)
61
+ print(
62
+ f"wrote predictions: {config.inference.output} "
63
+ f"({len(result.sample_ids)} events)"
64
+ )
65
+ return 0
66
+ except (ValueError, FileNotFoundError, KeyError, ImportError) as error:
67
+ print(f"gnn4colliders: {error}", file=sys.stderr)
68
+ return 2
69
+
70
+
71
+ __all__ = ["main"]
src/gnn4colliders/config/__init__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ """Semantic configuration factories used by the command-line applications."""
2
+
3
+ from .factories import build_model, build_task, build_trainer
4
+ from .validation import validate_config
5
+
6
+ __all__ = ["build_model", "build_task", "build_trainer", "validate_config"]
src/gnn4colliders/config/application.py ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Application-level orchestration kept separate from the CLI parser."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ import torch
10
+ from omegaconf import DictConfig, OmegaConf
11
+
12
+ from gnn4colliders.data import (
13
+ GraphDataLoader,
14
+ GraphDataset,
15
+ GraphSample,
16
+ GraphSampleCache,
17
+ RootEventDataset,
18
+ SplitDefinition,
19
+ select_split,
20
+ )
21
+ from gnn4colliders.features import build_node_features
22
+ from gnn4colliders.graphs import build_dgl_graph
23
+ from gnn4colliders.inference import Predictor, write_npz
24
+ from gnn4colliders.training import (
25
+ CheckpointManager,
26
+ restore_training_state,
27
+ seed_everything,
28
+ )
29
+
30
+ from .factories import build_model, build_task, build_trainer
31
+ from .validation import validate_config
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+
36
+ def plain(config: DictConfig | dict[str, Any]) -> dict[str, Any]:
37
+ return OmegaConf.to_container(config, resolve=True) # type: ignore[return-value]
38
+
39
+
40
+ def save_resolved(config: DictConfig, output_root: str | Path) -> Path:
41
+ target = Path(output_root) / "resolved_config.yaml"
42
+ target.parent.mkdir(parents=True, exist_ok=True)
43
+ OmegaConf.save(config, target, resolve=True)
44
+ return target
45
+
46
+
47
+ def load_samples(config: DictConfig) -> GraphDataset:
48
+ data = config.data
49
+ cache_path = data.cache.path
50
+ if not cache_path:
51
+ raise ValueError("data.cache.path is required for train/evaluate/predict")
52
+ cache = GraphSampleCache(cache_path)
53
+ if not cache.exists():
54
+ raise FileNotFoundError(f"graph sample cache does not exist: {cache_path}")
55
+ return GraphDataset(cache.load())
56
+
57
+
58
+ def loaders(config: DictConfig) -> dict[str, GraphDataLoader]:
59
+ samples = load_samples(config)
60
+ split = SplitDefinition(
61
+ train_folds=frozenset(config.data.splits.train_folds),
62
+ validation_folds=frozenset(config.data.splits.validation_folds),
63
+ test_folds=frozenset(config.data.splits.test_folds),
64
+ )
65
+ result = {}
66
+ for name in ("train", "validation", "test"):
67
+ selected = GraphDataset(select_split(samples.samples, split, name))
68
+ result[name] = GraphDataLoader(
69
+ selected,
70
+ int(config.data.batch_size),
71
+ shuffle=bool(config.data.shuffle) if name == "train" else False,
72
+ seed=int(config.data.seed),
73
+ )
74
+ return result
75
+
76
+
77
+ def prepare(config: DictConfig) -> Path:
78
+ data = config.data
79
+ if not data.files:
80
+ raise ValueError("prepare requires data.files")
81
+ feature_branches = data.get("feature_branches")
82
+ object_types = data.get("object_types")
83
+ scales = data.get("scales")
84
+ if feature_branches is None or object_types is None or scales is None:
85
+ raise ValueError(
86
+ "prepare requires data.feature_branches, object_types, and scales"
87
+ )
88
+ source = RootEventDataset(
89
+ data.files,
90
+ tree_name=str(data.tree_name),
91
+ label=data.get("label", 1),
92
+ feature_branches=feature_branches,
93
+ tracking_info=data.get("tracking_info", []),
94
+ global_features=data.get("global_features", []),
95
+ fold_var=str(data.get("fold_var", "eventNumber")),
96
+ weight_var=data.get("weight_var"),
97
+ )
98
+ samples: list[GraphSample] = []
99
+ for event in source:
100
+ features, _ = build_node_features(
101
+ event.objects, feature_branches, object_types, scales
102
+ )
103
+ graph = build_dgl_graph(features)
104
+ globals_ = torch.as_tensor(event.global_features, dtype=torch.float32)
105
+ samples.append(
106
+ GraphSample(
107
+ graph,
108
+ torch.as_tensor(event.label),
109
+ globals_ if globals_.numel() else None,
110
+ event.event_metadata,
111
+ )
112
+ )
113
+ path = Path(data.cache.path)
114
+ GraphSampleCache(path).save(samples)
115
+ return path
116
+
117
+
118
+ def _model_and_task(config: DictConfig, loader: GraphDataLoader):
119
+ first = next(iter(loader), None)
120
+ if first is None:
121
+ raise ValueError("selected data split is empty")
122
+ model_config = plain(config.model)
123
+ pretrained = config.checkpoint.pretrained
124
+ if pretrained:
125
+ model_config["pretrained_checkpoint"] = pretrained
126
+ model = build_model(
127
+ model_config, sample_graph=first.graph, sample_global=first.global_features
128
+ )
129
+ return model, build_task(plain(config.task))
130
+
131
+
132
+ def train(config: DictConfig) -> Path:
133
+ validate_config(plain(config))
134
+ seed_everything(int(config.trainer.seed))
135
+ run_root = Path(config.environment.output_root)
136
+ save_resolved(config, run_root)
137
+ split_loaders = loaders(config)
138
+ model, task = _model_and_task(config, split_loaders["train"])
139
+ trainer = build_trainer(plain(config.trainer), model=model, task=task)
140
+ if config.checkpoint.resume:
141
+ payload = CheckpointManager.load(config.checkpoint.resume, map_location="cpu")
142
+ restore_training_state(
143
+ payload,
144
+ model=model,
145
+ trainer=trainer,
146
+ optimizer=trainer.optimizer,
147
+ scheduler=trainer.scheduler,
148
+ early_stopping=trainer.early_stopping,
149
+ )
150
+ validation_loader = (
151
+ split_loaders["validation"] if len(split_loaders["validation"]) else None
152
+ )
153
+ trainer.fit(
154
+ split_loaders["train"], validation_loader, epochs=int(config.trainer.max_epochs)
155
+ )
156
+ manager = CheckpointManager(config.checkpoint.directory)
157
+ return manager.save(
158
+ model=model,
159
+ trainer_state=trainer.state,
160
+ optimizer=trainer.optimizer,
161
+ scheduler=trainer.scheduler,
162
+ early_stopping=trainer.early_stopping,
163
+ task_config=plain(config.task),
164
+ model_config=plain(config.model),
165
+ )
166
+
167
+
168
+ def predict_or_evaluate(config: DictConfig, *, evaluate: bool = False) -> Any:
169
+ validate_config(plain(config))
170
+ split_loaders = loaders(config)
171
+ model, task = _model_and_task(config, split_loaders[config.inference.split])
172
+ payload = CheckpointManager.load(config.inference.checkpoint, map_location="cpu")
173
+ from gnn4colliders.training import load_model_weights
174
+
175
+ load_model_weights(model, payload)
176
+ predictor = Predictor(model, task, device=str(config.trainer.device))
177
+ result = (
178
+ predictor.evaluate(split_loaders[config.inference.split])
179
+ if evaluate
180
+ else predictor.predict(split_loaders[config.inference.split])
181
+ )
182
+ if not evaluate:
183
+ output = Path(config.inference.output)
184
+ if str(config.inference.format).lower() != "npz":
185
+ raise ValueError("only npz inference output is currently supported")
186
+ write_npz(result, output)
187
+ return result
src/gnn4colliders/config/factories.py ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Small allow-listed factories for configured application components."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from typing import Any
7
+
8
+ import torch
9
+
10
+ from gnn4colliders.models.root_gnn import EdgeNetwork, FineTunedEdgeNetwork
11
+ from gnn4colliders.tasks import BinaryClassificationTask, MulticlassClassificationTask
12
+ from gnn4colliders.training import (
13
+ EarlyStopping,
14
+ Trainer,
15
+ build_optimizer,
16
+ build_scheduler,
17
+ )
18
+
19
+
20
+ def _get(config: Mapping[str, Any], name: str, default: Any = None) -> Any:
21
+ value = config.get(name, default)
22
+ return value
23
+
24
+
25
+ def build_model(
26
+ config: Mapping[str, Any],
27
+ *,
28
+ sample_graph: Any,
29
+ sample_global: torch.Tensor | None = None,
30
+ ) -> torch.nn.Module:
31
+ """Build one of the supported models from semantic configuration."""
32
+ family = str(_get(config, "family", "root_gnn")).lower()
33
+ name = str(_get(config, "name", "")).lower()
34
+ if not name:
35
+ name = (
36
+ "fine_tuned_edge_network"
37
+ if "fine" in str(_get(config, "class", "")).lower()
38
+ else "edge_network"
39
+ )
40
+ if family != "root_gnn":
41
+ raise ValueError(f"unsupported model family {family!r}")
42
+ kwargs = {
43
+ "hid_size": int(_get(config, "hid_size", _get(config, "hidden_dim", 128))),
44
+ "out_size": int(_get(config, "out_size", 1)),
45
+ "n_layers": int(_get(config, "n_layers", 2)),
46
+ "n_proc_steps": int(
47
+ _get(config, "n_proc_steps", _get(config, "processing_steps", 4))
48
+ ),
49
+ "dropout": float(_get(config, "dropout", 0.0)),
50
+ }
51
+ if name in {"edge_network", "edge"}:
52
+ return EdgeNetwork(sample_graph, sample_global, **kwargs)
53
+ if name in {"fine_tuned_edge_network", "finetuned_edge_network", "fine_tuned"}:
54
+ checkpoint = _get(config, "pretrained_checkpoint")
55
+ if not checkpoint:
56
+ raise ValueError(
57
+ "fine-tuned model requires model.pretrained_checkpoint or "
58
+ "checkpoint.pretrained"
59
+ )
60
+ from gnn4colliders.training import CheckpointManager, load_model_weights
61
+
62
+ payload = CheckpointManager.load(checkpoint, map_location="cpu")
63
+ base_config = dict(payload.get("model_config") or {})
64
+ base_config.update(
65
+ {key: value for key, value in kwargs.items() if key != "out_size"}
66
+ )
67
+ backbone = EdgeNetwork(
68
+ sample_graph,
69
+ sample_global,
70
+ hid_size=int(base_config.get("hid_size", kwargs["hid_size"])),
71
+ out_size=int(base_config.get("out_size", 1)),
72
+ n_layers=int(base_config.get("n_layers", kwargs["n_layers"])),
73
+ n_proc_steps=int(base_config.get("n_proc_steps", kwargs["n_proc_steps"])),
74
+ dropout=float(base_config.get("dropout", kwargs["dropout"])),
75
+ )
76
+ load_model_weights(backbone, payload)
77
+ return FineTunedEdgeNetwork(
78
+ backbone,
79
+ kwargs["out_size"],
80
+ freeze_backbone=bool(_get(config, "freeze_backbone", False)),
81
+ )
82
+ raise ValueError(f"unsupported root_gnn model {name!r}")
83
+
84
+
85
+ def build_task(config: Mapping[str, Any]) -> Any:
86
+ task_type = str(_get(config, "type", "multiclass_classification")).lower()
87
+ kwargs = {
88
+ "absolute_weights": bool(
89
+ _get(
90
+ config, "absolute_weights", _get(config, "use_absolute_weights", False)
91
+ )
92
+ )
93
+ }
94
+ if task_type in {"binary", "binary_classification"}:
95
+ return BinaryClassificationTask(
96
+ threshold=float(_get(config, "threshold", 0.5)), **kwargs
97
+ )
98
+ if task_type in {"multiclass", "multiclass_classification"}:
99
+ return MulticlassClassificationTask(**kwargs)
100
+ raise ValueError(f"unsupported task type {task_type!r}")
101
+
102
+
103
+ def build_trainer(
104
+ config: Mapping[str, Any], *, model: torch.nn.Module, task: Any
105
+ ) -> Trainer:
106
+ optimizer_config = _get(config, "optimizer", {}) or {}
107
+ optimizer = build_optimizer(model, **dict(optimizer_config))
108
+ scheduler_config = _get(config, "scheduler", {}) or {}
109
+ scheduler = None
110
+ scheduler_step = "epoch"
111
+ if _get(scheduler_config, "name") not in (None, "", "none"):
112
+ scheduler_step = str(_get(scheduler_config, "step", "epoch"))
113
+ scheduler_kwargs = {
114
+ key: value
115
+ for key, value in dict(scheduler_config).items()
116
+ if key not in {"name", "step"}
117
+ }
118
+ scheduler = build_scheduler(
119
+ optimizer, name=str(_get(scheduler_config, "name")), **scheduler_kwargs
120
+ )
121
+ early_config = _get(config, "early_stopping", {}) or {}
122
+ early = None
123
+ if bool(_get(early_config, "enabled", False)):
124
+ early = EarlyStopping(
125
+ **{
126
+ key: value
127
+ for key, value in dict(early_config).items()
128
+ if key != "enabled"
129
+ }
130
+ )
131
+ return Trainer(
132
+ model,
133
+ task,
134
+ optimizer,
135
+ scheduler,
136
+ device=str(_get(config, "device", "cpu")),
137
+ early_stopping=early,
138
+ scheduler_step=scheduler_step,
139
+ )
src/gnn4colliders/config/validation.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Early validation for cross-group configuration relationships."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+
7
+
8
+ def validate_config(config: Mapping[str, object]) -> None:
9
+ data = config.get("data", {}) or {}
10
+ trainer = config.get("trainer", {}) or {}
11
+ model = config.get("model", {}) or {}
12
+ task = config.get("task", {}) or {}
13
+ if int(data.get("batch_size", 1)) < 1:
14
+ raise ValueError("data.batch_size must be positive")
15
+ if int(trainer.get("max_epochs", 1)) < 1:
16
+ raise ValueError("trainer.max_epochs must be positive")
17
+ task_type = str(task.get("type", "")).lower()
18
+ out_size = int(model.get("out_size", 1))
19
+ if "binary" in task_type and out_size != 1:
20
+ raise ValueError("binary classification requires model.out_size=1")
21
+ if "multi" in task_type and out_size != int(task.get("num_classes", out_size)):
22
+ raise ValueError("multiclass model.out_size must equal task.num_classes")
23
+ if str(model.get("name", "")).lower() in {"fine_tuned_edge_network", "fine_tuned"}:
24
+ checkpoint = model.get("pretrained_checkpoint") or (
25
+ config.get("checkpoint", {}) or {}
26
+ ).get("pretrained")
27
+ if not checkpoint:
28
+ raise ValueError("fine-tuned models require checkpoint.pretrained")
29
+ checkpoint = config.get("checkpoint", {}) or {}
30
+ if checkpoint.get("resume") and checkpoint.get("pretrained"):
31
+ raise ValueError(
32
+ "checkpoint.resume and checkpoint.pretrained are distinct "
33
+ "workflows and cannot both be set"
34
+ )
35
+ splits = data.get("splits", {}) or {}
36
+ groups = [
37
+ set(splits.get(name, []))
38
+ for name in ("train_folds", "validation_folds", "test_folds")
39
+ ]
40
+ if any(groups[i] & groups[j] for i in range(3) for j in range(i + 1, 3)):
41
+ raise ValueError("train, validation, and test folds must be disjoint")