summarize_from_feedback/utils/hyperparams.py (198 lines of code) (raw):
import typing
from dataclasses import fields, is_dataclass
from functools import lru_cache
from typeguard import check_type
class HParams:
"""Used as a base class for hyperparameter structs. They also need to be annotated with @dataclass."""
def override_from_pair(self, flat_k, v, separator="."):
"""Overrides values from a key-value pair (flat_k, v) = ('x.y', 1) or ('name', "foobar").
Treats keys with separators as paths into nested HParams.
"""
typemap = _flat_type_map(type(self), separator=separator)
*ks, f = flat_k.split(separator)
# Traverse down to the nested hparam value which the field will be set on
hp = self
for i, k in enumerate(ks):
try:
hp = getattr(hp, k)
except AttributeError:
raise AttributeError(
f"{separator.join(ks[:i]) if i else 'hparams'} is {hp} which has no field '{k}'"
)
try:
old_v = getattr(hp, f)
except AttributeError:
raise AttributeError(f"{separator.join(ks)} is {hp} which has no field '{f}'")
# Figure out what to set; handle the special 'on' and 'off' values for nested hparams
hps_cls = _hparam_constructible_class(typemap[flat_k])
if hps_cls is not None:
if v == "on":
if old_v is None:
# Set the nested hparam class to its default values
v = hps_cls()
else:
# The field is already set; skip it so we don't override any of its values
# with the defaults
return
elif v == "off":
v = None
# Set it!
check_type(flat_k, v, typemap[flat_k])
setattr(hp, f, v)
@classmethod
def has_param(cls, flat_k, separator="."):
return flat_k in _flat_type_map(cls, separator=separator)
def override_from_pairs(self, pairs, separator="."):
"""Overrides values from a list of key-value pairs like [('x.y', 1), ('name', "foobar")].
Treats keys with separators as paths into nested HParams.
Uses a list rather than a dict because order can matter for nested hyperparameters.
In particular, you have to set x on before you set x.y; you also want to allow x to later be
overridden to None even if x.y has already been set.
"""
for flat_k, v in pairs:
self.override_from_pair(flat_k, v, separator=separator)
def clone(self):
return type(self).from_json(self.to_json())
@classmethod
def from_json(cls, json_val):
return _construct_from_json(cls, json_val)
def override_from_json(self, json_val, key=""):
typemap = _type_map(type(self))
for k, v in json_val.items():
if k not in typemap:
raise AttributeError(f"{self} has no attribute {k}")
t = typemap[k]
hps_cls = _hparam_constructible_class(t)
if hps_cls is not None and isinstance(v, dict):
old_v = getattr(self, k)
# initialize constructor, overwriting value if it's not the correct class (can happen with Unions)
if old_v is None or not isinstance(old_v, hps_cls):
setattr(self, k, hps_cls())
getattr(self, k).override_from_json(v, key + "." + k)
else:
new_v = _construct_from_json(t, v, key + "." + k)
check_type(k, new_v, t)
setattr(self, k, new_v)
def to_json(self):
return to_json(self)
def validate(self, *, prefix=""):
assert is_dataclass(self), f"You forgot to annotate {type(self)} with @dataclass"
for f in fields(self):
fieldval = getattr(self, f.name)
check_type(prefix + f.name, fieldval, f.type)
if isinstance(fieldval, HParams):
fieldval.validate(prefix=prefix + f.name + ".")
def is_hparam_type(ty):
if isinstance(ty, type) and issubclass(ty, HParams):
assert is_dataclass(ty)
return True
else:
return False
def is_hparam(x):
return is_hparam_type(type(x))
def to_json(x):
if is_hparam(x):
return {f.name: to_json(getattr(x, f.name)) for f in fields(x)}
if isinstance(x, list):
return [to_json(y) for y in x]
if isinstance(x, dict):
return {k: to_json(v) for k, v in x.items()}
return x
def _construct_from_json(ty, json_val, key=""):
"""
Construct a value of type `ty` based on the json value `json_val`.
"""
if json_val is None:
return json_val
if is_hparam_type(ty):
if isinstance(json_val, ty):
return json_val
if not isinstance(json_val, dict):
raise TypeError(
f"Tried to construct attribute {key} of type {ty} with value {json_val}"
)
x = ty()
x.override_from_json(json_val, key=key)
return x
if _is_list_type(ty):
subtype = ty.__args__[0]
return [_construct_from_json(subtype, y, key + ".listitem") for y in json_val]
if _is_dict_type(ty):
ktype = ty.__args__[0]
vtype = ty.__args__[1]
return {
_construct_from_json(ktype, k, key + ".dictkey"): _construct_from_json(
vtype, v, key + ".dictitem"
)
for k, v in json_val.items()
}
check_type(key, json_val, ty)
return json_val
def _is_union_type(ty):
return getattr(ty, "__origin__", None) is typing.Union
def _is_list_type(ty):
return getattr(ty, "__origin__", None) is list
def _is_dict_type(ty):
return getattr(ty, "__origin__", None) is dict
def _union_subtypes(ty):
if _is_union_type(ty):
results = []
for x in ty.__args__:
results.extend(_union_subtypes(x))
return results
return [ty]
def _hparam_constructible_class(ty):
"""
Given a type, returns:
- an unambiguous HParam subtype, if one exists,
- None, if there are no HParam subtypes
If there is ambiguity, throws a TypeError.
"""
subtypes = _union_subtypes(ty)
hparam_subtypes = [ty for ty in subtypes if is_hparam_type(ty)]
if len(hparam_subtypes) > 1:
raise TypeError(f"Unions with multiple HParam subtypes unsupported")
if len(hparam_subtypes) == 0:
return None
assert len(hparam_subtypes) == 1
hparam_ty = hparam_subtypes[0]
if dict in subtypes:
# avoid ambiguity for nested dict construction
raise TypeError(f"Unions with both HParam and dict subtypes unsupported")
if str in subtypes:
# avoid ambiguity for "on"/"off"
raise TypeError(f"Unions with both HParam and str subtypes unsupported")
return hparam_ty
def dump(hparams, *, name="hparams"):
print("%s:" % name)
def dump_nested(hp, indent):
for f in sorted(fields(hp), key=lambda f: f.name):
v = getattr(hp, f.name)
if isinstance(v, HParams):
print("%s%s:" % (indent, f.name))
dump_nested(v, indent=indent + " ")
else:
print("%s%s: %s" % (indent, f.name, v))
dump_nested(hparams, indent=" ")
@lru_cache()
def _type_map(ty):
return {f.name: f.type for f in fields(ty)}
def flat_to_nested(flat_dict, separator="."):
nested_dict = dict()
for k, v in flat_dict.items():
parts = k.split(separator)
d = nested_dict
subkey = None
for part in parts[:-1]:
subkey = part if subkey is None else subkey + separator + part
if part not in d:
d[part] = dict()
if not isinstance(d[part], dict):
raise ValueError(f"Set conflicting values for {subkey}")
d = d[part]
if parts[-1] in d:
raise ValueError(f"Set conflicting values for {k}")
d[parts[-1]] = v
return nested_dict
def nested_to_flat(nested_dict, separator="."):
flat_dict = dict()
def helper(val, subkey_prefix=""):
for k, v in val.items():
if isinstance(v, dict):
helper(v, subkey_prefix=subkey_prefix + k + separator)
else:
flat_dict[subkey_prefix + k] = v
helper(nested_dict)
return flat_dict
def _update_disjoint(dst: dict, src: dict):
for k, v in src.items():
assert k not in dst
dst[k] = v
@lru_cache()
def _flat_type_map(ty, separator):
typemap = {}
for f in fields(ty):
typemap[f.name] = f.type
if is_hparam_type(f.type):
nested = _flat_type_map(f.type, separator=separator)
elif _is_union_type(f.type):
nested = {}
for ty_option in f.type.__args__:
if is_hparam_type(ty_option):
_update_disjoint(nested, _flat_type_map(ty_option, separator=separator))
else:
nested = {}
_update_disjoint(typemap, {f"{f.name}{separator}{k}": t for k, t in nested.items()})
return typemap