Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/metatrain/cli/train.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ def train_model(
# MERGE OPTIONS ###########
###########################

input_options = copy.deepcopy(options)
options = OmegaConf.merge(
BASE_OPTIONS,
{"architecture": get_default_hypers(architecture_name)},
Expand Down Expand Up @@ -631,6 +632,8 @@ def train_model(
else:
training_context = None

new_model_hypers = input_options.get("architecture", {}).get("model", {})

try:
if training_context == "restart" and restart_from is not None:
logging.info(f"Restarting training from '{restart_from}'")
Expand All @@ -644,7 +647,7 @@ def train_model(
f"The file {restart_from} does not contain a valid checkpoint for "
f"the '{architecture_name}' architecture"
) from e
model = model.restart(dataset_info)
model = model.restart(dataset_info, model_hypers=new_model_hypers)
try:
trainer = trainer_from_checkpoint(
checkpoint=checkpoint,
Expand All @@ -668,7 +671,7 @@ def train_model(
f"The file {restart_from} does not contain a valid checkpoint for "
f"the '{architecture_name}' architecture"
) from e
model = model.restart(dataset_info)
model = model.restart(dataset_info, model_hypers=new_model_hypers)
trainer = Trainer(hypers["training"])
else:
logging.info("Starting training from scratch")
Expand Down
9 changes: 8 additions & 1 deletion src/metatrain/pet/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
sparsify_atomic_basis_target,
)
from metatrain.utils.dtype import dtype_to_str
from metatrain.utils.hypers import raise_if_hypers_mismatch
from metatrain.utils.long_range import DummyLongRangeFeaturizer, LongRangeFeaturizer
from metatrain.utils.metadata import merge_metadata
from metatrain.utils.scaler import Scaler
Expand Down Expand Up @@ -202,7 +203,13 @@ def __init__(self, hypers: ModelHypers, dataset_info: DatasetInfo) -> None:
def supported_outputs(self) -> Dict[str, ModelOutput]:
return self.outputs

def restart(self, dataset_info: DatasetInfo) -> "PET":
def restart(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think this this is the canoncial place! Very good.

self, dataset_info: DatasetInfo, model_hypers: Optional[dict] = None
) -> "PET":

if model_hypers is not None:
raise_if_hypers_mismatch(self.hypers, model_hypers)

# merge old and new dataset info
merged_info = self.dataset_info.union(dataset_info)
new_atomic_types = [
Expand Down
6 changes: 5 additions & 1 deletion src/metatrain/utils/abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,9 @@ def supported_outputs(self) -> Dict[str, ModelOutput]:
"""

@abstractmethod
def restart(self, dataset_info: DatasetInfo) -> "ModelInterface":
def restart(
self, dataset_info: DatasetInfo, model_hypers: Optional[dict] = None
) -> "ModelInterface":
"""
Update a model to restart training, potentially with different dataset and/or
targets.
Expand All @@ -147,6 +149,8 @@ def restart(self, dataset_info: DatasetInfo) -> "ModelInterface":
:param dataset_info: Information about the new dataset, including the targets
that will be used for training.

:param model_hypers: The new hyperparameters for the model.

:return: The updated model, or a new instance of the model, that is able to
handle the new dataset.
"""
Expand Down
59 changes: 58 additions & 1 deletion src/metatrain/utils/hypers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Type, TypedDict, TypeVar
from typing import Any, Type, TypedDict, TypeVar

from typing_extensions import TypedDict as TE_TypedDict

Expand Down Expand Up @@ -97,3 +97,60 @@ def overwrite_defaults(
:param new_defaults: A dict with the new default hyperparameters.
"""
_OVERWRITTEN_DEFAULTS[hypers_cls] = new_defaults


def get_hypers_diff(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't there a function already from the libraries that we use?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I will check Omegaconf, this was just so that it was clear what I propose to do

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, so apparently neither omegaconf or pydantic have such a functionality, and since we are just checking a shallow diff I think it is fine to have this simple function here

old_hypers: dict,
new_hypers: dict,
) -> dict[str, tuple[Any, Any]]:
"""Get the difference between two hypers dictionaries.

:param old_hypers: The old hyperparameters.
:param new_hypers: The new hyperparameters.

:return: A dict with the hyperparameters that are different in the new
hypers compared to the old hypers. It is assumed that every key in
the new hypers is also present in the old hypers.
"""
diff = {}
for key, new_value in new_hypers.items():
old_value = old_hypers[key]
if old_value != new_value:
diff[key] = (old_value, new_value)
Comment on lines +121 to +132

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does a single loop work if there are deeply nested dictionaries?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depends on what you mean by "work". It will detect that the dictionaries are different if there is a difference somewhere deep in the keys. But it will include still all the deep keys that are the same. My idea was that dealing with arbitrary nested keys is a bit of a mess and perhaps we can introduce it whenever some model needs it? For example, for the user the top-level key that changed is likely enough to report in the error.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I think this answers my other question: order doesn't matter, right?

return diff


def raise_hypers_mismatch(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here. I would think there is already something. But maybe I am wrong.

hypers_diff: dict[str, tuple[Any, Any]],
) -> None:
"""Raise an error if the hypers diff is not empty.

The hypers diff can be computed using :func:`get_hypers_diff`.
"""
if hypers_diff:
n_mismatches = len(hypers_diff)
raise ValueError(
f"Found {n_mismatches} mismatch{(n_mismatches != 1) * 'es'} in model hyperparameters.\n"
f"Mismatched hypers: {list(hypers_diff.keys())}\n"
"\n-------- Mismatches --------\n\n"
+ "\n".join(
f"[Mismatch {i + 1}] {key}\n Previous: {old}\n New: {new}"
for i, (key, (old, new)) in enumerate(hypers_diff.items())
)
)


def raise_if_hypers_mismatch(
old_hypers: dict,
new_hypers: dict,
) -> None:
"""Raise an error if the new hypers do not match the old hypers.

:param old_hypers: The old hyperparameters.
:param new_hypers: The new hyperparameters.
"""
# Gather mismatchs
mismatches = get_hypers_diff(old_hypers, new_hypers)

if mismatches:
raise_hypers_mismatch(mismatches)
Loading