Skip to content

Commit 5bcb6e1

Browse files
committed
First draft
1 parent 1838062 commit 5bcb6e1

4 files changed

Lines changed: 76 additions & 5 deletions

File tree

src/metatrain/cli/train.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,7 @@ def train_model(
298298
# MERGE OPTIONS ###########
299299
###########################
300300

301+
input_options = copy.deepcopy(options)
301302
options = OmegaConf.merge(
302303
BASE_OPTIONS,
303304
{"architecture": get_default_hypers(architecture_name)},
@@ -631,6 +632,8 @@ def train_model(
631632
else:
632633
training_context = None
633634

635+
new_model_hypers = input_options.get("architecture", {}).get("model", {})
636+
634637
try:
635638
if training_context == "restart" and restart_from is not None:
636639
logging.info(f"Restarting training from '{restart_from}'")
@@ -644,7 +647,7 @@ def train_model(
644647
f"The file {restart_from} does not contain a valid checkpoint for "
645648
f"the '{architecture_name}' architecture"
646649
) from e
647-
model = model.restart(dataset_info)
650+
model = model.restart(dataset_info, model_hypers=new_model_hypers)
648651
try:
649652
trainer = trainer_from_checkpoint(
650653
checkpoint=checkpoint,
@@ -668,7 +671,7 @@ def train_model(
668671
f"The file {restart_from} does not contain a valid checkpoint for "
669672
f"the '{architecture_name}' architecture"
670673
) from e
671-
model = model.restart(dataset_info)
674+
model = model.restart(dataset_info, model_hypers=new_model_hypers)
672675
trainer = Trainer(hypers["training"])
673676
else:
674677
logging.info("Starting training from scratch")

src/metatrain/pet/model.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
sparsify_atomic_basis_target,
3030
)
3131
from metatrain.utils.dtype import dtype_to_str
32+
from metatrain.utils.hypers import raise_if_hypers_mismatch
3233
from metatrain.utils.long_range import DummyLongRangeFeaturizer, LongRangeFeaturizer
3334
from metatrain.utils.metadata import merge_metadata
3435
from metatrain.utils.scaler import Scaler
@@ -202,7 +203,13 @@ def __init__(self, hypers: ModelHypers, dataset_info: DatasetInfo) -> None:
202203
def supported_outputs(self) -> Dict[str, ModelOutput]:
203204
return self.outputs
204205

205-
def restart(self, dataset_info: DatasetInfo) -> "PET":
206+
def restart(
207+
self, dataset_info: DatasetInfo, model_hypers: Optional[dict] = None
208+
) -> "PET":
209+
210+
if model_hypers is not None:
211+
raise_if_hypers_mismatch(self.hypers, model_hypers)
212+
206213
# merge old and new dataset info
207214
merged_info = self.dataset_info.union(dataset_info)
208215
new_atomic_types = [

src/metatrain/utils/abc.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,9 @@ def supported_outputs(self) -> Dict[str, ModelOutput]:
135135
"""
136136

137137
@abstractmethod
138-
def restart(self, dataset_info: DatasetInfo) -> "ModelInterface":
138+
def restart(
139+
self, dataset_info: DatasetInfo, model_hypers: Optional[dict] = None
140+
) -> "ModelInterface":
139141
"""
140142
Update a model to restart training, potentially with different dataset and/or
141143
targets.
@@ -147,6 +149,8 @@ def restart(self, dataset_info: DatasetInfo) -> "ModelInterface":
147149
:param dataset_info: Information about the new dataset, including the targets
148150
that will be used for training.
149151
152+
:param model_hypers: The new hyperparameters for the model.
153+
150154
:return: The updated model, or a new instance of the model, that is able to
151155
handle the new dataset.
152156
"""

src/metatrain/utils/hypers.py

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Type, TypedDict, TypeVar
1+
from typing import Any, Type, TypedDict, TypeVar
22

33
from typing_extensions import TypedDict as TE_TypedDict
44

@@ -97,3 +97,60 @@ def overwrite_defaults(
9797
:param new_defaults: A dict with the new default hyperparameters.
9898
"""
9999
_OVERWRITTEN_DEFAULTS[hypers_cls] = new_defaults
100+
101+
102+
def get_hypers_diff(
103+
old_hypers: dict,
104+
new_hypers: dict,
105+
) -> dict[str, tuple[Any, Any]]:
106+
"""Get the difference between two hypers dictionaries.
107+
108+
:param old_hypers: The old hyperparameters.
109+
:param new_hypers: The new hyperparameters.
110+
111+
:return: A dict with the hyperparameters that are different in the new
112+
hypers compared to the old hypers. It is assumed that every key in
113+
the new hypers is also present in the old hypers.
114+
"""
115+
diff = {}
116+
for key, new_value in new_hypers.items():
117+
old_value = old_hypers[key]
118+
if old_value != new_value:
119+
diff[key] = (old_value, new_value)
120+
return diff
121+
122+
123+
def raise_hypers_mismatch(
124+
hypers_diff: dict[str, tuple[Any, Any]],
125+
) -> None:
126+
"""Raise an error if the hypers diff is not empty.
127+
128+
The hypers diff can be computed using :func:`get_hypers_diff`.
129+
"""
130+
if hypers_diff:
131+
n_mismatches = len(hypers_diff)
132+
raise ValueError(
133+
f"Found {n_mismatches} mismatch{(n_mismatches != 1) * 'es'} in model hyperparameters.\n"
134+
f"Mismatched hypers: {list(hypers_diff.keys())}\n"
135+
"\n-------- Mismatches --------\n\n"
136+
+ "\n".join(
137+
f"[Mismatch {i + 1}] {key}\n Previous: {old}\n New: {new}"
138+
for i, (key, (old, new)) in enumerate(hypers_diff.items())
139+
)
140+
)
141+
142+
143+
def raise_if_hypers_mismatch(
144+
old_hypers: dict,
145+
new_hypers: dict,
146+
) -> None:
147+
"""Raise an error if the new hypers do not match the old hypers.
148+
149+
:param old_hypers: The old hyperparameters.
150+
:param new_hypers: The new hyperparameters.
151+
"""
152+
# Gather mismatchs
153+
mismatches = get_hypers_diff(old_hypers, new_hypers)
154+
155+
if mismatches:
156+
raise_hypers_mismatch(mismatches)

0 commit comments

Comments
 (0)