lm_human_preferences/utils/hyperparams.py (189 lines of code) (raw):

import json import sys import typing from dataclasses import fields, is_dataclass from functools import lru_cache from typeguard import check_type from lm_human_preferences.utils import gcs class HParams: """Used as a base class for hyperparameter structs. They also need to be annotated with @dataclass.""" def override_from_json_file(self, filename): if filename.startswith('gs://'): hparams_str = gcs.download_contents(filename) else: hparams_str = open(filename).read() self.parse_json(hparams_str) def override_from_str(self, hparam_str): """Overrides values from a string like 'x.y=1,name=foobar'. Like tensorflow.contrib.training.HParams, this method does not allow specifying string values containing commas. """ kvp_strs = hparam_str.split(',') flat_dict = {} for kvp_str in kvp_strs: k, sep, v = kvp_str.partition('=') if not sep: raise ValueError(f"Malformed hyperparameter value: '{kvp_str}'") flat_dict[k] = v self.override_from_str_dict(flat_dict) def override_from_str_dict(self, flat_dict, separator='.'): """Overrides values from a dict like {'x.y': "1", 'name': "foobar"}. Treats keys with dots as paths into nested HParams. Parses values according to the types in the HParams classes. """ typemap = _type_map(type(self), separator=separator) parsed = {} for flat_k, s in flat_dict.items(): if flat_k not in typemap: raise AttributeError(f"no field {flat_k} in {typemap}") parsed[flat_k] = _parse_typed_value(typemap[flat_k], s) self.override_from_dict(parsed, separator=separator) def parse_json(self, s: str): self.override_from_nested_dict(json.loads(s)) def override_from_dict(self, flat_dict, separator='.'): """Overrides values from a dict like {'x.y': 1, 'name': "foobar"}. Treats keys with dots as paths into nested HParams. Values should be parsed already. """ # Parse 'on' and 'off' values. typemap = _type_map(type(self), separator=separator) flat_dict_parsed = {} for flat_k, v in flat_dict.items(): cls = _type_to_class(typemap[flat_k]) if is_hparam_type(cls) and v == 'on': parsed_v = cls() elif is_hparam_type(cls) and v == 'off': parsed_v = None else: parsed_v = v flat_dict_parsed[flat_k] = parsed_v # Expand implicit nested 'on' values. For instance, {'x.y': 'on'} should mean {'x': 'on', 'x.y': 'on'}. flat_dict_expanded = {} for flat_k, v in flat_dict_parsed.items(): flat_dict_expanded[flat_k] = v cls = _type_to_class(typemap[flat_k]) if is_hparam_type(cls) and v is not None: parts = flat_k.split(separator) prefix = parts[0] for i in range(1, len(parts)): if prefix not in flat_dict_expanded: flat_dict_expanded[prefix] = _type_to_class(typemap[prefix])() prefix += separator + parts[i] # Set all the values. The sort ensures that outer classes get initialized before their fields. for flat_k in sorted(flat_dict_expanded.keys()): v = flat_dict_expanded[flat_k] *ks, f = flat_k.split(separator) hp = self for i, k in enumerate(ks): try: hp = getattr(hp, k) except AttributeError: raise AttributeError(f"{hp} {'(' + separator.join(ks[:i]) + ') ' if i else ''}has no field '{k}'") try: setattr(hp, f, v) except AttributeError: raise AttributeError(f"{hp} ({separator.join(ks)}) has no field '{f}'") def override_from_nested_dict(self, nested_dict): for k, v in nested_dict.items(): if isinstance(v, dict): if getattr(self, k) is None: cls = _type_to_class(_get_field(self, k).type) setattr(self, k, cls()) getattr(self, k).override_from_nested_dict(v) else: setattr(self, k, v) def to_nested_dict(self): d = {} for f in fields(self): fieldval = getattr(self, f.name) if isinstance(fieldval, HParams): fieldval = fieldval.to_nested_dict() d[f.name] = fieldval return d 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_union_type(ty): return getattr(ty, '__origin__', None) is typing.Union def dump(hparams, *, name='hparams', out=sys.stdout): out.write('%s:\n' % 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): out.write('%s%s:\n' % (indent, f.name)) dump_nested(v, indent=indent+' ') else: out.write('%s%s: %s\n' % (indent, f.name, v)) dump_nested(hparams, indent=' ') def _can_distinguish_unambiguously(type_set): """Whether it's always possible to tell which type in type_set a certain value is supposed to be""" if len(type_set) == 1: return True if type(None) in type_set: return True if str in type_set: return False if int in type_set and float in type_set: return False if any(_is_union_type(ty) for ty in type_set): # Nested unions *might* be unambiguous, but don't support for now return False return True def _parse_typed_value(ty, s): if ty is str: return s elif ty in (int, float): return ty(s) elif ty is bool: if s in ('t', 'true', 'True'): return True elif s in ('f', 'false', 'False'): return False else: raise ValueError(f"Invalid bool '{s}'") elif ty is type(None): if s in ('None', 'none', ''): return None else: raise ValueError(f"Invalid None value '{s}'") elif is_hparam_type(ty): if s in ('on', 'off'): # The class will be constructed later return s else: raise ValueError(f"Invalid hparam class value '{s}'") elif _is_union_type(ty): if not _can_distinguish_unambiguously(ty.__args__): raise TypeError(f"Can't always unambiguously parse a value of union '{ty}'") for ty_option in ty.__args__: try: return _parse_typed_value(ty_option, s) except ValueError: continue raise ValueError(f"Couldn't parse '{s}' as any of the types in '{ty}'") else: raise ValueError(f"Unsupported hparam type '{ty}'") def _get_field(data, fieldname): matching_fields = [f for f in fields(data) if f.name == fieldname] if len(matching_fields) != 1: raise AttributeError(f"couldn't find field '{fieldname}' in {data}") return matching_fields[0] def _update_disjoint(dst: dict, src: dict): for k, v in src.items(): assert k not in dst dst[k] = v @lru_cache() def _type_map(ty, separator): typemap = {} for f in fields(ty): typemap[f.name] = f.type if is_hparam_type(f.type): nested = _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, _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 def _type_to_class(ty): """Extract a constructible class from a type. For instance, `typing.Optional[int]` gives `int`""" if _is_union_type(ty): # Only typing.Optional supported: must be of form typing.Union[ty, None] assert len(ty.__args__) == 2 assert ty.__args__[1] is type(None) return ty.__args__[0] else: return ty