diff --git a/autogluon/contrib/enas/enas.py b/autogluon/contrib/enas/enas.py index 7e11eaa6590a..728b10fa4ee1 100644 --- a/autogluon/contrib/enas/enas.py +++ b/autogluon/contrib/enas/enas.py @@ -12,6 +12,9 @@ def enas_unit(**kwvars): def registered_class(Cls): + + + class enas_unit(ENAS_Unit): def __init__(self, *args, **kwargs): kwvars.update(kwargs) @@ -32,7 +35,7 @@ def node(self): arg = self._args[self.index] if arg is None: return arg summary = {} - name = self.module_list[self.index].__class__.__name__ + '(' + name = f'{self.module_list[self.index].__class__.__name__}(' for k, v in json.loads(arg).items(): if 'kernel' in k.lower(): cm = ("#8dd3c7", "#fb8072", "#ffffb3", "#bebada", "#80b1d3", @@ -61,11 +64,16 @@ def get_config_grid(dict_space): config.update(constants) return configs + return enas_unit + return registered_class def enas_net(**kwvars): def registered_class(Cls): + + + class ENAS_Net(Cls): def __init__(self, *args, **kwargs): kwvars.update(kwargs) @@ -134,7 +142,7 @@ def kwspaces(self): return self._kwspaces def sample(self, **configs): - striped_keys = [k.split('.')[0] for k in configs.keys()] + striped_keys = [k.split('.')[0] for k in configs] for k in striped_keys: if isinstance(self._modules[k], ENAS_Unit): self._modules[k].sample(configs[k]) @@ -168,7 +176,9 @@ def evaluate_latency(self, x): self._avg_latency = avg_latency self.latency_evaluated = True + return ENAS_Net + return registered_class class ENAS_Sequential(gluon.HybridBlock): @@ -279,10 +289,7 @@ def evaluate_latency(self, x): import time # evaluate submodule latency for k, op in self._modules.items(): - if hasattr(op, 'evaluate_latency'): - x = op.evaluate_latency(x) - else: - x = op(x) + x = op.evaluate_latency(x) if hasattr(op, 'evaluate_latency') else op(x) # calc avg_latency avg_latency = 0.0 for k, op in self._modules.items(): @@ -297,9 +304,9 @@ def sample(self, **configs): self._modules[k].sample(v) def __repr__(self): - reprstr = self.__class__.__name__ + '(' + reprstr = f'{self.__class__.__name__}(' for i, op in self._modules.items(): - reprstr += '\n\t{}: {}'.format(i, op) + reprstr += f'\n\t{i}: {op}' reprstr += ')\n' return reprstr @@ -334,10 +341,10 @@ def kwspaces(self): @property def nparams(self): - nparams = 0 - for _, v in self.module_list[self.index].collect_params().items(): - nparams += v.data().size - return nparams + return sum( + v.data().size + for _, v in self.module_list[self.index].collect_params().items() + ) @property def latency(self): @@ -374,6 +381,4 @@ def __len__(self): return len(self.module_list) def __repr__(self): - reprstr = self.__class__.__name__ + '(num of choices: {}), current architecture:\n\t {}' \ - .format(len(self.module_list), self.module_list[self.index]) - return reprstr + return f'{self.__class__.__name__}(num of choices: {len(self.module_list)}), current architecture:\n\t {self.module_list[self.index]}' diff --git a/autogluon/contrib/enas/enas_net.py b/autogluon/contrib/enas/enas_net.py index dff0836f66fe..27385c49ddea 100644 --- a/autogluon/contrib/enas/enas_net.py +++ b/autogluon/contrib/enas/enas_net.py @@ -44,8 +44,10 @@ def __init__(self, blocks_args=[], dropout_rate=0.2, num_classes=1000, input_siz block_arg.update(in_channels=out_channels, stride=1, input_size=input_size) - for _ in range(block_arg.num_repeat - 1): - _blocks.append(ENAS_MbBlock(**block_arg)) + _blocks.extend( + ENAS_MbBlock(**block_arg) + for _ in range(block_arg.num_repeat - 1) + ) if blocks is not None: self._blocks = ENAS_Sequential(blocks) @@ -98,19 +100,128 @@ def hybrid_forward(self, F, x): def get_enas_blockargs(): """ Creates a predefined efficientnet model, which searched by original paper. """ - blocks_args = [ - Dict(kernel=3, num_repeat=1, channels=16, expand_ratio=1, stride=1, se_ratio=0.25, in_channels=32), - Dict(kernel=3, num_repeat=1, channels=16, expand_ratio=1, stride=1, se_ratio=0.25, in_channels=16, with_zero=True), - Dict(kernel=Categorical(3, 5, 7), num_repeat=1, channels=24, expand_ratio=Categorical(3, 6), stride=2, se_ratio=0.25, in_channels=16), - Dict(kernel=Categorical(3, 5, 7), num_repeat=3, channels=24, expand_ratio=Categorical(3, 6), stride=1, se_ratio=0.25, in_channels=24, with_zero=True), - Dict(kernel=Categorical(3, 5, 7), num_repeat=1, channels=40, expand_ratio=Categorical(3, 6), stride=2, se_ratio=0.25, in_channels=24), - Dict(kernel=Categorical(3, 5, 7), num_repeat=3, channels=40, expand_ratio=Categorical(3, 6), stride=1, se_ratio=0.25, in_channels=40, with_zero=True), - Dict(kernel=Categorical(3, 5, 7), num_repeat=1, channels=80, expand_ratio=Categorical(3, 6), stride=2, se_ratio=0.25, in_channels=40), - Dict(kernel=Categorical(3, 5, 7), num_repeat=4, channels=80, expand_ratio=Categorical(3, 6), stride=1, se_ratio=0.25, in_channels=80, with_zero=True), - Dict(kernel=Categorical(3, 5, 7), num_repeat=1, channels=112, expand_ratio=Categorical(3, 6), stride=1, se_ratio=0.25, in_channels=80), - Dict(kernel=Categorical(3, 5, 7), num_repeat=4, channels=112, expand_ratio=Categorical(3, 6), stride=1, se_ratio=0.25, in_channels=112, with_zero=True), - Dict(kernel=Categorical(3, 5, 7), num_repeat=1, channels=192, expand_ratio=Categorical(3, 6), stride=2, se_ratio=0.25, in_channels=112), - Dict(kernel=Categorical(3, 5, 7), num_repeat=5, channels=192, expand_ratio=Categorical(3, 6), stride=1, se_ratio=0.25, in_channels=192, with_zero=True), - Dict(kernel=3, num_repeat=1, channels=320, expand_ratio=6, stride=1, se_ratio=0.25, in_channels=192), + return [ + Dict( + kernel=3, + num_repeat=1, + channels=16, + expand_ratio=1, + stride=1, + se_ratio=0.25, + in_channels=32, + ), + Dict( + kernel=3, + num_repeat=1, + channels=16, + expand_ratio=1, + stride=1, + se_ratio=0.25, + in_channels=16, + with_zero=True, + ), + Dict( + kernel=Categorical(3, 5, 7), + num_repeat=1, + channels=24, + expand_ratio=Categorical(3, 6), + stride=2, + se_ratio=0.25, + in_channels=16, + ), + Dict( + kernel=Categorical(3, 5, 7), + num_repeat=3, + channels=24, + expand_ratio=Categorical(3, 6), + stride=1, + se_ratio=0.25, + in_channels=24, + with_zero=True, + ), + Dict( + kernel=Categorical(3, 5, 7), + num_repeat=1, + channels=40, + expand_ratio=Categorical(3, 6), + stride=2, + se_ratio=0.25, + in_channels=24, + ), + Dict( + kernel=Categorical(3, 5, 7), + num_repeat=3, + channels=40, + expand_ratio=Categorical(3, 6), + stride=1, + se_ratio=0.25, + in_channels=40, + with_zero=True, + ), + Dict( + kernel=Categorical(3, 5, 7), + num_repeat=1, + channels=80, + expand_ratio=Categorical(3, 6), + stride=2, + se_ratio=0.25, + in_channels=40, + ), + Dict( + kernel=Categorical(3, 5, 7), + num_repeat=4, + channels=80, + expand_ratio=Categorical(3, 6), + stride=1, + se_ratio=0.25, + in_channels=80, + with_zero=True, + ), + Dict( + kernel=Categorical(3, 5, 7), + num_repeat=1, + channels=112, + expand_ratio=Categorical(3, 6), + stride=1, + se_ratio=0.25, + in_channels=80, + ), + Dict( + kernel=Categorical(3, 5, 7), + num_repeat=4, + channels=112, + expand_ratio=Categorical(3, 6), + stride=1, + se_ratio=0.25, + in_channels=112, + with_zero=True, + ), + Dict( + kernel=Categorical(3, 5, 7), + num_repeat=1, + channels=192, + expand_ratio=Categorical(3, 6), + stride=2, + se_ratio=0.25, + in_channels=112, + ), + Dict( + kernel=Categorical(3, 5, 7), + num_repeat=5, + channels=192, + expand_ratio=Categorical(3, 6), + stride=1, + se_ratio=0.25, + in_channels=192, + with_zero=True, + ), + Dict( + kernel=3, + num_repeat=1, + channels=320, + expand_ratio=6, + stride=1, + se_ratio=0.25, + in_channels=192, + ), ] - return blocks_args diff --git a/autogluon/contrib/enas/enas_scheduler.py b/autogluon/contrib/enas/enas_scheduler.py index 42f7496ad101..733f7c3e0991 100644 --- a/autogluon/contrib/enas/enas_scheduler.py +++ b/autogluon/contrib/enas/enas_scheduler.py @@ -113,8 +113,7 @@ def run(self): # for recordio data if hasattr(self.train_data, 'reset'): self.train_data.reset() tbar = tqdm(self.train_data) - idx = 0 - for batch in tbar: + for idx, batch in enumerate(tbar): # sample network configuration config = self.controller.pre_sample()[0] self.supernet.sample(**config) @@ -129,7 +128,6 @@ def run(self): tbar.set_svg(graph._repr_svg_()) if self.baseline: tbar.set_description('avg reward: {:.2f}'.format(self.baseline)) - idx += 1 self.validation() self.save() msg = 'epoch {}, val_acc: {:.2f}'.format(epoch, self.val_acc) @@ -150,7 +148,7 @@ def validation(self): for batch in tbar: self.eval_fn(self.supernet, batch, metric=metric, **self.val_args) reward = metric.get()[1] - tbar.set_description('Val Acc: {}'.format(reward)) + tbar.set_description(f'Val Acc: {reward}') self.val_acc = reward self.training_history.append(reward) @@ -195,7 +193,7 @@ def train_controller(self): self.eval_fn(self.supernet, batch, metric=metric, **self.val_args) reward = metric.get()[1] reward = self.reward_fn(reward, self.supernet) - self.baseline = reward if not self.baseline else self.baseline + self.baseline = self.baseline or reward # substract baseline avg_rewards = mx.nd.array([reward - self.baseline], ctx=self.controller.context) @@ -213,12 +211,12 @@ def train_controller(self): self._prefetch_controller() def load(self, checkname=None): - checkname = checkname if checkname else self.checkname + checkname = checkname or self.checkname state_dict = load(checkname) self.load_state_dict(state_dict) def save(self, checkname=None): - checkname = checkname if checkname else self.checkname + checkname = checkname or self.checkname mkdir(os.path.dirname(checkname)) save(self.state_dict(), checkname) diff --git a/autogluon/contrib/enas/enas_utils.py b/autogluon/contrib/enas/enas_utils.py index 19c3b16e7fea..4c658c91304e 100644 --- a/autogluon/contrib/enas/enas_utils.py +++ b/autogluon/contrib/enas/enas_utils.py @@ -5,16 +5,17 @@ from ...scheduler.resource import get_gpu_count def default_reward_fn(metric, net): - reward = metric * ((net.avg_latency / net.latency) ** 0.07) - return reward + return metric * ((net.avg_latency / net.latency) ** 0.07) def init_default_train_args(batch_size, net, epochs, iters_per_epoch): - train_args = {} base_lr = 0.1 * batch_size / 256 lr_scheduler = gcv.utils.LRScheduler('cosine', base_lr=base_lr, target_lr=0.0001, nepochs=epochs, iters_per_epoch=iters_per_epoch) optimizer_params = {'wd': 1e-4, 'momentum': 0.9, 'lr_scheduler': lr_scheduler} - train_args['trainer'] = gluon.Trainer(net.collect_params(), 'sgd', optimizer_params) + train_args = { + 'trainer': gluon.Trainer(net.collect_params(), 'sgd', optimizer_params) + } + train_args['batch_size'] = batch_size train_args['criterion'] = gluon.loss.SoftmaxCrossEntropyLoss() return train_args diff --git a/autogluon/core/decorator.py b/autogluon/core/decorator.py index 0807eddd2a9e..02f0b00fc41d 100644 --- a/autogluon/core/decorator.py +++ b/autogluon/core/decorator.py @@ -32,9 +32,7 @@ def sample_config(args, config): if isinstance(v, NestedSpace): sub_config = _strip_config_space(config, prefix=k) args_dict[k] = v.sample(**sub_config) - else: - if SPLITTER in k: - continue + elif SPLITTER not in k: args_dict[k] = config[k] elif isinstance(v, AutoGluonObject): args_dict[k] = v.init() @@ -75,13 +73,11 @@ def update(self, **kwargs): """ self.kwvars.update(kwargs) for k, v in self.kwvars.items(): - if isinstance(v, (NestedSpace)): + if isinstance(v, (NestedSpace)) or not isinstance(v, Space): self.args.update({k: v}) - elif isinstance(v, Space): + else: hp = v.get_hp(name=k) self.args.update({k: hp.default_value}) - else: - self.args.update({k: v}) @property def cs(self): @@ -104,9 +100,9 @@ def kwspaces(self): for k, v in self.kwvars.items(): if isinstance(v, NestedSpace): if isinstance(v, Categorical): - kw_spaces['{}{}choice'.format(k, SPLITTER)] = v + kw_spaces[f'{k}{SPLITTER}choice'] = v for sub_k, sub_v in v.kwspaces.items(): - new_k = '{}{}{}'.format(k, SPLITTER, sub_k) + new_k = f'{k}{SPLITTER}{sub_k}' kw_spaces[new_k] = sub_v elif isinstance(v, Space): kw_spaces[k] = v @@ -134,7 +130,7 @@ def args(default=None, **kwvars): ... print('Batch size is {}, LR is {}'.format(args.batch_size, arg.lr)) """ if default is None: - default = dict() + default = {} kwvars['_default_config'] = default def registered_func(func): @_autogluon_method @@ -263,6 +259,9 @@ def wrapper_call(*args, **kwargs): return registered_func def registered_class(Cls): + + + class autogluonobject(AutoGluonObject): @_autogluon_kwargs_obj(**kwvars) def __init__(self, *args, **kwargs): @@ -284,7 +283,8 @@ def sample(self, **config): return Cls(*args, **kwargs) def __repr__(self): - return 'AutoGluonObject -- ' + Cls.__name__ + return f'AutoGluonObject -- {Cls.__name__}' + autogluonobject.kwvars = autogluonobject.__init__.kwvars autogluonobject.__doc__ = Cls.__doc__ diff --git a/autogluon/core/space.py b/autogluon/core/space.py index aa9d4f0cc9d1..ebf762b28c12 100644 --- a/autogluon/core/space.py +++ b/autogluon/core/space.py @@ -22,9 +22,9 @@ class SimpleSpace(Space): def __repr__(self): reprstr = self.__class__.__name__ if hasattr(self, 'lower') and hasattr(self, 'upper'): - reprstr += ': lower={}, upper={}'.format(self.lower, self.upper) + reprstr += f': lower={self.lower}, upper={self.upper}' if hasattr(self, 'value'): - reprstr += ': value={}'.format(self.value) + reprstr += f': value={self.value}' return reprstr def get_hp(self, name): @@ -42,8 +42,7 @@ def hp(self): def default(self): """Return default value of hyperparameter corresponding to this search space. """ - default = self._default if self._default else self.hp.default_value - return default + return self._default or self.hp.default_value @default.setter def default(self, value): @@ -129,10 +128,10 @@ def cs(self): return cs @classproperty - def kwspaces(cls): + def kwspaces(self): """ OrderedDict representation of this search space. """ - return cls.__init__.kwspaces + return self.__init__.kwspaces def sample(self): """Sample a configuration from this search space. @@ -163,8 +162,7 @@ def __init__(self, *args): self.data = [*args] def __iter__(self): - for elem in self.data: - yield elem + yield from self.data def __getitem__(self, index): return self.data[index] @@ -196,7 +194,7 @@ def sample(self, **config): """ ret = [] kwspaces = self.kwspaces - striped_keys = [k.split(SPLITTER)[0] for k in config.keys()] + striped_keys = [k.split(SPLITTER)[0] for k in config] for idx, obj in enumerate(self.data): if isinstance(obj, NestedSpace): sub_config = _strip_config_space(config, prefix=str(idx)) @@ -230,15 +228,14 @@ def kwspaces(self): if isinstance(obj, NestedSpace): kw_spaces[k] = obj for sub_k, sub_v in obj.kwspaces.items(): - new_k = '{}{}{}'.format(k, SPLITTER, sub_k) + new_k = f'{k}{SPLITTER}{sub_k}' kw_spaces[new_k] = sub_v elif isinstance(obj, Space): kw_spaces[k] = obj return kw_spaces def __repr__(self): - reprstr = self.__class__.__name__ + str(self.data) - return reprstr + return self.__class__.__name__ + str(self.data) class Dict(NestedSpace): """Nested search spaceĀ for dictionary containing multiple hyperparameters. @@ -298,7 +295,7 @@ def kwspaces(self): if isinstance(obj, NestedSpace): kw_spaces[k] = obj for sub_k, sub_v in obj.kwspaces.items(): - new_k = '{}{}{}'.format(k, SPLITTER, sub_k) + new_k = f'{k}{SPLITTER}{sub_k}' kw_spaces[new_k] = sub_v kw_spaces[new_k] = sub_v elif isinstance(obj, Space): @@ -309,10 +306,10 @@ def sample(self, **config): """Sample a configuration from this search space. """ ret = {} - ret.update(self.data) + ret |= self.data kwspaces = self.kwspaces kwspaces.update(config) - striped_keys = [k.split(SPLITTER)[0] for k in config.keys()] + striped_keys = [k.split(SPLITTER)[0] for k in config] for k, v in kwspaces.items(): if k in striped_keys: if isinstance(v, NestedSpace): @@ -323,8 +320,7 @@ def sample(self, **config): return ret def __repr__(self): - reprstr = self.__class__.__name__ + str(self.data) - return reprstr + return self.__class__.__name__ + str(self.data) class Categorical(NestedSpace): @@ -344,8 +340,7 @@ def __init__(self, *data): self.data = [*data] def __iter__(self): - for elem in self.data: - yield elem + yield from self.data def __getitem__(self, index): return self.data[index] @@ -389,13 +384,12 @@ def kwspaces(self): for idx, obj in enumerate(self.data): if isinstance(obj, NestedSpace): for sub_k, sub_v in obj.kwspaces.items(): - new_k = '{}{}{}'.format(idx, SPLITTER, sub_k) + new_k = f'{idx}{SPLITTER}{sub_k}' kw_spaces[new_k] = sub_v return kw_spaces def __repr__(self): - reprstr = self.__class__.__name__ + str(self.data) - return reprstr + return self.__class__.__name__ + str(self.data) Choice = DeprecationHelper(Categorical, 'Choice') @@ -467,12 +461,11 @@ def __init__(self): super(Bool, self).__init__(0, 1) def _strip_config_space(config, prefix): - # filter out the config with the corresponding prefix - new_config = {} - for k, v in config.items(): - if k.startswith(prefix): - new_config[k[len(prefix)+1:]] = v - return new_config + return { + k[len(prefix) + 1 :]: v + for k, v in config.items() + if k.startswith(prefix) + } def _add_hp(cs, hp): if hp.name in cs._hyperparameters: @@ -487,8 +480,8 @@ def _add_cs(master_cs, sub_cs, prefix, delimiter='.', parent_hp=None): # Allow for an empty top-level parameter if new_parameter.name == '': new_parameter.name = prefix - elif not prefix == '': - new_parameter.name = "{}{}{}".format(prefix, SPLITTER, new_parameter.name) + elif prefix != '': + new_parameter.name = f"{prefix}{SPLITTER}{new_parameter.name}" new_parameters.append(new_parameter) for hp in new_parameters: _add_hp(master_cs, hp) @@ -497,5 +490,5 @@ def _rm_hp(cs, k): if k in cs._hyperparameters: cs._hyperparameters.pop(k) for hp in cs.get_hyperparameters(): - if hp.name.startswith('{}'.format(k)): + if hp.name.startswith(f'{k}'): cs._hyperparameters.pop(hp.name) diff --git a/autogluon/core/task.py b/autogluon/core/task.py index 1065602c356b..8ea1c00896b8 100644 --- a/autogluon/core/task.py +++ b/autogluon/core/task.py @@ -40,17 +40,17 @@ def __init__(self, fn, args, resources): @classmethod def set_id(cls, taskid): - logger.info('Setting TASK ID: {}'.format(taskid)) + logger.info(f'Setting TASK ID: {taskid}') cls.TASK_ID.value = taskid def __repr__(self): reprstr = self.__class__.__name__ + \ - ' (' + 'task_id: ' + str(self.task_id) + \ - ',\n\tfn: ' + str(self.fn) + \ - ',\n\targs: {' + ' (' + 'task_id: ' + str(self.task_id) + \ + ',\n\tfn: ' + str(self.fn) + \ + ',\n\targs: {' for k, v in self.args.items(): data = str(v) - info = (data[:100] + '..') if len(data) > 100 else data - reprstr += '{}'.format(k) + ': ' + info + ', ' + info = f'{data[:100]}..' if len(data) > 100 else data + reprstr += f'{k}: {info}, ' reprstr += '},\n\tresource: ' + str(self.resources) + ')\n' return reprstr diff --git a/autogluon/model_zoo/model_store.py b/autogluon/model_zoo/model_store.py index f108deec891a..b163783ce590 100644 --- a/autogluon/model_zoo/model_store.py +++ b/autogluon/model_zoo/model_store.py @@ -47,7 +47,7 @@ def get_model_file(name, root=os.path.join('~', '.autogluon', 'models')): """ file_name = '{name}-{short_hash}'.format(name=name, short_hash=short_hash(name)) root = os.path.expanduser(root) - file_path = os.path.join(root, file_name+'.params') + file_path = os.path.join(root, f'{file_name}.params') sha1_hash = _model_sha1[name] if os.path.exists(file_path): if check_sha1(file_path, sha1_hash): @@ -56,15 +56,15 @@ def get_model_file(name, root=os.path.join('~', '.autogluon', 'models')): print('Mismatch in the content of model file {} detected.' + ' Downloading again.'.format(file_path)) else: - print('Model file {} is not found. Downloading.'.format(file_path)) + print(f'Model file {file_path} is not found. Downloading.') if not os.path.exists(root): os.makedirs(root) - zip_file_path = os.path.join(root, file_name+'.zip') + zip_file_path = os.path.join(root, f'{file_name}.zip') repo_url = os.environ.get('ENCODING_REPO', autogluon_repo_url) if repo_url[-1] != '/': - repo_url = repo_url + '/' + repo_url = f'{repo_url}/' download(_url_format.format(repo_url=repo_url, file_name=file_name), path=zip_file_path, overwrite=True) diff --git a/autogluon/model_zoo/models/efficientnet.py b/autogluon/model_zoo/models/efficientnet.py index e905f35d058b..f5f78f995342 100644 --- a/autogluon/model_zoo/models/efficientnet.py +++ b/autogluon/model_zoo/models/efficientnet.py @@ -97,25 +97,88 @@ def hybrid_forward(self, F, x): def get_efficientnet_blockargs(): """ Creates a predefined efficientnet model, which searched by original paper. """ - blocks_args = [ - EasyDict(kernel=3, num_repeat=1, channels=16, expand_ratio=1, stride=1, se_ratio=0.25, in_channels=32), - EasyDict(kernel=3, num_repeat=2, channels=24, expand_ratio=6, stride=2, se_ratio=0.25, in_channels=16), - EasyDict(kernel=5, num_repeat=2, channels=40, expand_ratio=6, stride=2, se_ratio=0.25, in_channels=24), - EasyDict(kernel=3, num_repeat=3, channels=80, expand_ratio=6, stride=2, se_ratio=0.25, in_channels=40), - EasyDict(kernel=5, num_repeat=3, channels=112, expand_ratio=6, stride=1, se_ratio=0.25, in_channels=80), - EasyDict(kernel=5, num_repeat=4, channels=192, expand_ratio=6, stride=2, se_ratio=0.25, in_channels=112), - EasyDict(kernel=3, num_repeat=1, channels=320, expand_ratio=6, stride=1, se_ratio=0.25, in_channels=192), + return [ + EasyDict( + kernel=3, + num_repeat=1, + channels=16, + expand_ratio=1, + stride=1, + se_ratio=0.25, + in_channels=32, + ), + EasyDict( + kernel=3, + num_repeat=2, + channels=24, + expand_ratio=6, + stride=2, + se_ratio=0.25, + in_channels=16, + ), + EasyDict( + kernel=5, + num_repeat=2, + channels=40, + expand_ratio=6, + stride=2, + se_ratio=0.25, + in_channels=24, + ), + EasyDict( + kernel=3, + num_repeat=3, + channels=80, + expand_ratio=6, + stride=2, + se_ratio=0.25, + in_channels=40, + ), + EasyDict( + kernel=5, + num_repeat=3, + channels=112, + expand_ratio=6, + stride=1, + se_ratio=0.25, + in_channels=80, + ), + EasyDict( + kernel=5, + num_repeat=4, + channels=192, + expand_ratio=6, + stride=2, + se_ratio=0.25, + in_channels=112, + ), + EasyDict( + kernel=3, + num_repeat=1, + channels=320, + expand_ratio=6, + stride=1, + se_ratio=0.25, + in_channels=192, + ), ] - return blocks_args def get_efficientnet(dropout_rate=None, num_classes=None, width_coefficient=None, depth_coefficient=None, depth_divisor=None, min_depth=None, drop_connect_rate=None, input_size=224, **kwargs): blocks_args = get_efficientnet_blockargs() - model = EfficientNet(blocks_args, dropout_rate, num_classes, width_coefficient, - depth_coefficient, depth_divisor, min_depth, drop_connect_rate, - input_size, **kwargs) - return model + return EfficientNet( + blocks_args, + dropout_rate, + num_classes, + width_coefficient, + depth_coefficient, + depth_divisor, + min_depth, + drop_connect_rate, + input_size, + **kwargs + ) def get_efficientnet_b0(pretrained=False, dropout_rate=0.2, classes=1000, width_coefficient=1.0, depth_coefficient=1.0, depth_divisor=8, min_depth=None, drop_connect_rate=0.2, input_size=224, ctx=mx.cpu(), diff --git a/autogluon/model_zoo/models/utils.py b/autogluon/model_zoo/models/utils.py index 59ba21e8cba4..c253a399b74d 100644 --- a/autogluon/model_zoo/models/utils.py +++ b/autogluon/model_zoo/models/utils.py @@ -9,9 +9,7 @@ def round_repeats(repeats, depth_coefficient=None): """ Round number of filters based on depth multiplier. """ multiplier = depth_coefficient - if not multiplier: - return repeats - return int(math.ceil(multiplier * repeats)) + return int(math.ceil(multiplier * repeats)) if multiplier else repeats def round_filters(filters, width_coefficient=None, depth_divisor=None, min_depth=None): """ Calculate and round number of filters based on depth multiplier. """ @@ -98,7 +96,7 @@ def hybrid_forward(self, F, x): #return swish(self._beta)(x) def __repr__(self): - return '{} (beta={})'.format(self.__class__.__name__, self._beta) + return f'{self.__class__.__name__} (beta={self._beta})' def _add_conv(out, channels=1, kernel=1, stride=1, pad=0, num_group=1, activation='swish', batchnorm=True, input_size=None, diff --git a/autogluon/scheduler/fifo.py b/autogluon/scheduler/fifo.py index ff51d9bcaf61..9ec1ad3ee59c 100644 --- a/autogluon/scheduler/fifo.py +++ b/autogluon/scheduler/fifo.py @@ -150,7 +150,7 @@ def __init__(self, train_fn, **kwargs): search_options = kwargs.get('search_options') if isinstance(searcher, str): if search_options is None: - search_options = dict() + search_options = {} _search_options = search_options.copy() _search_options['configspace'] = train_fn.cs _search_options['reward_attribute'] = kwargs['reward_attr'] @@ -168,12 +168,12 @@ def __init__(self, train_fn, **kwargs): assert isinstance(train_fn, _autogluon_method) self.train_fn = train_fn args = kwargs.get('args') - self.args = args if args else train_fn.args + self.args = args or train_fn.args num_trials = kwargs.get('num_trials') time_out = kwargs.get('time_out') if num_trials is None: assert time_out is not None, \ - "Need stopping criterion: Either num_trials or time_out" + "Need stopping criterion: Either num_trials or time_out" self.num_trials = num_trials self.time_out = time_out self.max_reward = kwargs.get('max_reward') @@ -191,7 +191,7 @@ def __init__(self, train_fn, **kwargs): self._reward_attr = kwargs['reward_attr'] self._time_attr = kwargs['time_attr'] self.visualizer = kwargs['visualizer'].lower() - if self.visualizer == 'tensorboard' or self.visualizer == 'mxboard': + if self.visualizer in ['tensorboard', 'mxboard']: assert checkpoint is not None, "Need checkpoint to be set" try_import_mxboard() from mxboard import SummaryWriter @@ -215,12 +215,12 @@ def __init__(self, train_fn, **kwargs): self._training_history_callback_last_len = None self.training_history_callback = kwargs.get('training_history_callback') self.training_history_callback_delta_secs = \ - kwargs['training_history_callback_delta_secs'] + kwargs['training_history_callback_delta_secs'] self._delay_get_config = kwargs['delay_get_config'] # Resume experiment from checkpoint? if kwargs['resume']: assert checkpoint is not None, \ - "Need checkpoint to be set if resume = True" + "Need checkpoint to be set if resume = True" if os.path.isfile(checkpoint): self.load_state_dict(load(checkpoint)) else: @@ -254,7 +254,7 @@ def run(self, **kwargs): logger.info(f'Time out (secs) is {time_out}') for _ in tbar: if (time_out and time.time() - start_time >= time_out) or \ - (self.max_reward and self.get_best_reward() >= self.max_reward): + (self.max_reward and self.get_best_reward() >= self.max_reward): break self.schedule_next() @@ -314,9 +314,8 @@ def run_with_config(self, config): def _dict_from_task(self, task): if isinstance(task, Task): return {'TASK_ID': task.task_id, 'Config': task.args['config']} - else: - assert isinstance(task, dict) - return {'TASK_ID': task['TASK_ID'], 'Config': task['Config']} + assert isinstance(task, dict) + return {'TASK_ID': task['TASK_ID'], 'Config': task['Config']} def add_job(self, task, **kwargs): """Adding a training task to the scheduler. @@ -420,9 +419,9 @@ def _run_reporter(self, task, task_job, reporter): if dataset_size > 0: reported_result['searcher_data_size'] = dataset_size for k, v in self.searcher.cumulative_profile_record().items(): - reported_result['searcher_profile_' + k] = v + reported_result[f'searcher_profile_{k}'] = v for k, v in self.searcher.model_parameters().items(): - reported_result['searcher_params_' + k] = v + reported_result[f'searcher_params_{k}'] = v self._add_training_result( task.task_id, reported_result, config=task.args['config']) reporter.move_on() @@ -439,7 +438,7 @@ def _promote_config(self): :return: config, extra_args """ config = None - extra_args = dict() + extra_args = {} return config, extra_args def _elapsed_time(self): @@ -461,7 +460,7 @@ def get_best_reward(self): return self.searcher.get_best_reward() def _add_training_result(self, task_id, reported_result, config=None): - if self.visualizer == 'mxboard' or self.visualizer == 'tensorboard': + if self.visualizer in ['mxboard', 'tensorboard']: if 'loss' in reported_result: self.mxboard.add_scalar( tag='loss', @@ -553,7 +552,7 @@ def state_dict(self, destination=None): destination['searcher'] = pickle.dumps(self.searcher.get_state()) destination['training_history'] = json.dumps(self.training_history) destination['config_history'] = json.dumps(self.config_history) - if self.visualizer == 'mxboard' or self.visualizer == 'tensorboard': + if self.visualizer in ['mxboard', 'tensorboard']: destination['visualizer'] = json.dumps(self.mxboard._scalar_dict) return destination @@ -576,6 +575,6 @@ def load_state_dict(self, state_dict): pickle.loads(state_dict['searcher'])) self.training_history = json.loads(state_dict['training_history']) self.config_history = json.loads(state_dict['config_history']) - if self.visualizer == 'mxboard' or self.visualizer == 'tensorboard': + if self.visualizer in ['mxboard', 'tensorboard']: self.mxboard._scalar_dict = json.loads(state_dict['visualizer']) logger.debug(f'Loading Searcher State {self.searcher}') diff --git a/autogluon/scheduler/hyperband.py b/autogluon/scheduler/hyperband.py index e523a8883168..d8961a5e2096 100644 --- a/autogluon/scheduler/hyperband.py +++ b/autogluon/scheduler/hyperband.py @@ -211,16 +211,16 @@ def __init__(self, train_fn, **kwargs): # exception inferred_max_t = self._infer_max_t(train_fn.args) max_t = kwargs.get('max_t') - if max_t is not None: - if inferred_max_t is not None and max_t != inferred_max_t: - logger.warning( - "max_t = {} is different from the value {} inferred from train_fn.args (train_fn.args.epochs, train_fn.args.max_t)".format(max_t, inferred_max_t)) - else: + if max_t is None: assert inferred_max_t is not None, \ - "Either max_t must be specified, or it has to be specified via train_fn (as train_fn.args.epochs or train_fn.args.max_t)" - logger.info("max_t = {}, as inferred from train_fn.args".format( - inferred_max_t)) + "Either max_t must be specified, or it has to be specified via train_fn (as train_fn.args.epochs or train_fn.args.max_t)" + logger.info(f"max_t = {inferred_max_t}, as inferred from train_fn.args") max_t = inferred_max_t + elif inferred_max_t is not None and max_t != inferred_max_t: + logger.warning( + f"max_t = {max_t} is different from the value {inferred_max_t} inferred from train_fn.args (train_fn.args.epochs, train_fn.args.max_t)" + ) + # Check values and impute default values (only for arguments new to # this class) kwargs = check_and_merge_defaults( @@ -235,11 +235,8 @@ def __init__(self, train_fn, **kwargs): # Adjoin information about scheduler to search_options search_options = kwargs.get('search_options') - if search_options is None: - _search_options = dict() - else: - _search_options = search_options.copy() - _search_options['scheduler'] = 'hyperband_{}'.format(type) + _search_options = {} if search_options is None else search_options.copy() + _search_options['scheduler'] = f'hyperband_{type}' _search_options['min_epochs'] = grace_period _search_options['max_epochs'] = max_t kwargs['search_options'] = _search_options @@ -259,15 +256,16 @@ def __init__(self, train_fn, **kwargs): reduction_factor, brackets) elif type == 'promotion': assert not do_snapshots, \ - "Snapshots are supported only for type = 'stopping'" + "Snapshots are supported only for type = 'stopping'" self.terminator = HyperbandPromotion_Manager( self._time_attr, self._reward_attr, max_t, grace_period, reduction_factor, brackets, keep_size_ratios=kwargs['keep_size_ratios']) else: raise AssertionError( - "type '{}' not supported, must be 'stopping' or 'promotion'".format( - type)) + f"type '{type}' not supported, must be 'stopping' or 'promotion'" + ) + self.do_snapshots = do_snapshots self.searcher_data = kwargs['searcher_data'] # Maintains a snapshot of currently running tasks, needed by several @@ -283,14 +281,14 @@ def __init__(self, train_fn, **kwargs): # self._time_attr). # - bracket: Bracket number # - keep_case: See _run_reporter - self._running_tasks = dict() + self._running_tasks = {} # This lock protects both _running_tasks and terminator, the latter # does not define its own lock self._hyperband_lock = mp.Lock() if resume: checkpoint = kwargs.get('checkpoint') assert checkpoint is not None, \ - "Need checkpoint to be set if resume = True" + "Need checkpoint to be set if resume = True" if os.path.isfile(checkpoint): self.load_state_dict(load(checkpoint)) else: @@ -334,8 +332,10 @@ def add_job(self, task, **kwargs): # Register task task_key = str(task.task_id) with self._hyperband_lock: - assert task_key not in self._running_tasks, \ - "Task {} is already registered as running".format(task_key) + assert ( + task_key not in self._running_tasks + ), f"Task {task_key} is already registered as running" + self._running_tasks[task_key] = { 'config': task.args['config'], 'time_stamp': kwargs['elapsed_time'], @@ -345,8 +345,7 @@ def add_job(self, task, **kwargs): milestones = self.terminator.on_task_add(task, **kwargs) if kwargs.get('new_config', True): first_milestone = milestones[-1] - logger.debug("Adding new task (first milestone = {}):\n{}".format( - first_milestone, task)) + logger.debug(f"Adding new task (first milestone = {first_milestone}):\n{task}") self.searcher.register_pending( task.args['config'], milestone=first_milestone) if self.maxt_pending: @@ -361,8 +360,7 @@ def add_job(self, task, **kwargs): # pause and resume: task.args['resume_from'] = kwargs['resume_from'] next_milestone = kwargs['milestone'] - logger.debug("Promotion task (next milestone = {}):\n{}".format( - next_milestone, task)) + logger.debug(f"Promotion task (next milestone = {next_milestone}):\n{task}") self.searcher.register_pending( task.args['config'], milestone=next_milestone) @@ -379,7 +377,7 @@ def add_job(self, task, **kwargs): # Checkpoint thread. This is also used for training_history # callback if self._checkpoint is not None or \ - self.training_history_callback is not None: + self.training_history_callback is not None: self._add_checkpointing_to_job(job) with self.LOCK: self.scheduled_tasks.append(task_dict) @@ -389,12 +387,12 @@ def _update_searcher(self, task, result): if self.searcher_data == 'rungs_and_last': with self._hyperband_lock: task_info = self._running_tasks[str(task.task_id)] - if task_info['reported_result'] is not None: - # Remove last recently added result for this task, - # unless it fell on a rung level - if not task_info['keep_case']: - rem_result = task_info['reported_result'] - self.searcher.remove_case(config, **rem_result) + if ( + task_info['reported_result'] is not None + and not task_info['keep_case'] + ): + rem_result = task_info['reported_result'] + self.searcher.remove_case(config, **rem_result) self.searcher.update(config, **result) def _run_reporter(self, task, task_job, reporter): @@ -440,15 +438,15 @@ def _run_reporter(self, task, task_job, reporter): reported_result['bracket'] = task_info['bracket_id'] if 'rung_counts' in task_info: for k, v in task_info['rung_counts'].items(): - key = 'count_at_{}'.format(k) + key = f'count_at_{k}' reported_result[key] = v dataset_size = self.searcher.dataset_size() if dataset_size > 0: reported_result['searcher_data_size'] = dataset_size for k, v in self.searcher.cumulative_profile_record().items(): - reported_result['searcher_profile_' + k] = v + reported_result[f'searcher_profile_{k}'] = v for k, v in self.searcher.model_parameters().items(): - reported_result['searcher_params_' + k] = v + reported_result[f'searcher_params_{k}'] = v self._add_training_result( task.task_id, reported_result, config=task.args['config']) @@ -479,7 +477,8 @@ def _run_reporter(self, task, task_job, reporter): if task_continues: self.searcher.register_pending( task.args['config'], - milestone=int(reported_result[self._time_attr]) + 1) + milestone=int(last_updated[self._time_attr]) + 1, + ) # Change snapshot entry for task # Note: This must not be done above, because what _update_searcher @@ -506,10 +505,9 @@ def _run_reporter(self, task, task_job, reporter): config_id = debug_log.config_id(task.args['config']) milestone = int(reported_result[self._time_attr]) next_milestone = task_info['next_milestone'] - msg = "config_id {}: Reaches {}, continues".format( - config_id, milestone) + msg = f"config_id {config_id}: Reaches {milestone}, continues" if next_milestone is not None: - msg += " to {}".format(next_milestone) + msg += f" to {next_milestone}" logger.info(msg) reporter.move_on() else: @@ -526,8 +524,7 @@ def _run_reporter(self, task, task_job, reporter): act_str = 'Terminating' else: act_str = 'Pausing' - msg = "config_id {}: {} evaluation at {}".format( - config_id, act_str, resource) + msg = f"config_id {config_id}: {act_str} evaluation at {resource}" logger.info(msg) with self._hyperband_lock: self.terminator.on_task_remove(task) @@ -566,12 +563,12 @@ def load_state_dict(self, state_dict): """ with self._hyperband_lock: assert len(self._running_tasks) == 0, \ - "load_state_dict must only be called as part of scheduler construction" + "load_state_dict must only be called as part of scheduler construction" super().load_state_dict(state_dict) # Note: _running_tasks is empty from __init__, it is not recreated, # since running tasks are not part of the checkpoint self.terminator = pickle.loads(state_dict['terminator']) - logger.info('Loading Terminator State {}'.format(self.terminator)) + logger.info(f'Loading Terminator State {self.terminator}') def _snapshot_tasks(self, bracket_id): return { @@ -609,9 +606,8 @@ def _promote_config(self): if (debug_log is not None) and (config is not None): # Debug log output config_id = debug_log.config_id(config) - msg = "config_id {}: Promotion from {} to {}".format( - config_id, extra_kwargs['resume_from'], - extra_kwargs['milestone']) + msg = f"config_id {config_id}: Promotion from {extra_kwargs['resume_from']} to {extra_kwargs['milestone']}" + logger.info(msg) return config, extra_kwargs @@ -624,6 +620,4 @@ def fun(resource): return fun def __repr__(self): - reprstr = self.__class__.__name__ + '(' + \ - 'terminator: ' + str(self.terminator) - return reprstr + return f'{self.__class__.__name__}(terminator: {str(self.terminator)}' diff --git a/autogluon/scheduler/hyperband_promotion.py b/autogluon/scheduler/hyperband_promotion.py index 405ba9afc7a2..7a937c1b0d6b 100644 --- a/autogluon/scheduler/hyperband_promotion.py +++ b/autogluon/scheduler/hyperband_promotion.py @@ -61,7 +61,7 @@ def __init__( self._max_t = max_t self._min_t = grace_period # Maps str(task_id) -> bracket_id - self._task_info = dict() + self._task_info = {} self._brackets = [] for s in range(brackets): bracket = PromotionBracket( @@ -105,7 +105,7 @@ def on_task_report(self, task, result): rung_counts = bracket.get_rung_counts() if result[self._time_attr] < self._max_t: action, update_searcher, next_milestone, ignore_data = \ - bracket.on_result(task, result[self._time_attr], + bracket.on_result(task, result[self._time_attr], result[self._reward_attr]) return { 'task_continues': action, @@ -140,7 +140,7 @@ def on_task_schedule(self): bracket = self._brackets[bracket_id] # Check whether config can be promoted in that bracket config, config_key, milestone, next_milestone = \ - bracket.on_task_schedule() + bracket.on_task_schedule() if config is not None: extra_kwargs['milestone'] = next_milestone extra_kwargs['config_key'] = config_key @@ -158,14 +158,7 @@ def snapshot_rungs(self, bracket_id): return self._brackets[bracket_id].snapshot_rungs() def __repr__(self): - reprstr = self.__class__.__name__ + '(' + \ - 'reward_attr: ' + self._reward_attr + \ - ', time_attr: ' + self._time_attr + \ - ', reduction_factor: ' + str(self._reduction_factor) + \ - ', max_t: ' + str(self._max_t) + \ - ', brackets: ' + str(self._brackets) + \ - ')' - return reprstr + return f'{self.__class__.__name__}(reward_attr: {self._reward_attr}, time_attr: {self._time_attr}, reduction_factor: {str(self._reduction_factor)}, max_t: {str(self._max_t)}, brackets: {str(self._brackets)})' class PromotionBracket(object): @@ -186,10 +179,12 @@ def __init__( MAX_RUNGS = int(np.log(max_t / min_t) / np.log(self.rf) - s + 1) # The second entry in each tuple in _rungs is a dict mapping # config_key to (reward_value, was_promoted) - self._rungs = [(min_t * self.rf ** (k + s), dict()) - for k in reversed(range(MAX_RUNGS))] + self._rungs = [ + (min_t * self.rf ** (k + s), {}) for k in reversed(range(MAX_RUNGS)) + ] + # Note: config_key are positions into _config, cast to str - self._config = list() + self._config = [] # _running maps str(task_id) to tuples # (config_key, milestone, resume_from), # which means task task_id runs evaluation of config_key until @@ -197,13 +192,11 @@ def __init__( # not, the task is running a config which has been promoted from # rung level resume_from. This info is required for on_result to # properly report ignore_data. - self._running = dict() + self._running = {} # _count_tasks[m] counts the number of tasks started with target # milestone m. Here, m includes max_t. Used to implement the # keep_size_ratios rule - self._count_tasks = dict() - for milestone, _ in self._rungs: - self._count_tasks[str(milestone)] = 0 + self._count_tasks = {str(milestone): 0 for milestone, _ in self._rungs} if self._rungs and max_t > self._rungs[0][0]: self._count_tasks[str(max_t)] = 0 @@ -267,8 +260,12 @@ def on_task_schedule(self): if _milestone < self.max_t: skip_promotion = self._do_skip_promotion( _milestone, next_milestone) - config_key = self._find_promotable_config(_recorded) \ - if not skip_promotion else None + config_key = ( + None + if skip_promotion + else self._find_promotable_config(_recorded) + ) + if config_key is not None: recorded = _recorded milestone = _milestone @@ -278,12 +275,11 @@ def on_task_schedule(self): if config_key is None: # No promotable config in any rung return None, None, None, None - else: - # Mark config as promoted - reward = recorded[config_key][0] - assert not recorded[config_key][1] - recorded[config_key] = (reward, True) - return self._config[int(config_key)], config_key, milestone, \ + # Mark config as promoted + reward = recorded[config_key][0] + assert not recorded[config_key][1] + recorded[config_key] = (reward, True) + return self._config[int(config_key)], config_key, milestone, \ next_milestone def on_task_add(self, task, **kwargs): @@ -293,8 +289,7 @@ def on_task_add(self, task, **kwargs): to the next milestone (False). In the latter case, kwargs contains additional information about the promotion. """ - new_config = kwargs.get('new_config', True) - if new_config: + if new_config := kwargs.get('new_config', True): # New config config_key = str(len(self._config)) self._config.append(copy.copy(task.args['config'])) @@ -328,16 +323,17 @@ def on_result(self, task, cur_iter, cur_rew): :return: action, milestone_reached, next_milestone, ignore_data """ assert cur_rew is not None, \ - "Reward attribute must be a numerical value, not None" + "Reward attribute must be a numerical value, not None" task_key = str(task.task_id) action = True milestone_reached = False next_milestone = None milestone = self._running[task_key][1] if cur_iter >= milestone: - assert cur_iter == milestone, \ - "cur_iter = {} > {} = milestone. Make sure to report time attributes covering all milestones".format( - cur_iter, milestone) + assert ( + cur_iter == milestone + ), f"cur_iter = {cur_iter} > {milestone} = milestone. Make sure to report time attributes covering all milestones" + action = False milestone_reached = True config_key = self._running[task_key][0] @@ -349,7 +345,7 @@ def on_result(self, task, cur_iter, cur_rew): recorded = self._rungs[rung_pos][1] recorded[config_key] = (cur_rew, False) next_milestone = self._rungs[rung_pos - 1][0] \ - if rung_pos > 0 else self.max_t + if rung_pos > 0 else self.max_t # Check whether config can be promoted immediately. If so, # we do not have to stop the task if milestone < self.max_t: @@ -402,4 +398,4 @@ def __repr__(self): milestone, *self._num_promotable_config(recorded)) for milestone, recorded in self._rungs ]) - return "Bracket: " + iters + return f"Bracket: {iters}" diff --git a/autogluon/scheduler/hyperband_stopping.py b/autogluon/scheduler/hyperband_stopping.py index 0c8267c0609b..f1cbda93ea31 100644 --- a/autogluon/scheduler/hyperband_stopping.py +++ b/autogluon/scheduler/hyperband_stopping.py @@ -58,7 +58,7 @@ def __init__( self._max_t = max_t self._min_t = grace_period # Maps str(task_id) -> bracket_id - self._task_info = dict() + self._task_info = {} self._num_stopped = 0 self._brackets = [] for s in range(brackets): @@ -161,14 +161,7 @@ def resource_to_index(self, resource): resource, self._reduction_factor, self._min_t, self._max_t) def __repr__(self): - reprstr = self.__class__.__name__ + '(' + \ - 'reward_attr: ' + self._reward_attr + \ - ', time_attr: ' + self._time_attr + \ - ', reduction_factor: ' + str(self._reduction_factor) + \ - ', max_t: ' + str(self._max_t) + \ - ', brackets: ' + str(self._brackets) + \ - ')' - return reprstr + return f'{self.__class__.__name__}(reward_attr: {self._reward_attr}, time_attr: {self._time_attr}, reduction_factor: {str(self._reduction_factor)}, max_t: {str(self._max_t)}, brackets: {str(self._brackets)})' class StoppingBracket(object): @@ -180,13 +173,16 @@ class StoppingBracket(object): def __init__(self, min_t, max_t, reduction_factor, s): self.rf = reduction_factor MAX_RUNGS = int(np.log(max_t / min_t) / np.log(self.rf) - s + 1) - self._rungs = [(min_t * self.rf ** (k + s), dict()) - for k in reversed(range(MAX_RUNGS))] + self._rungs = [ + (min_t * self.rf ** (k + s), {}) for k in reversed(range(MAX_RUNGS)) + ] def cutoff(self, recorded): - if not recorded: - return None - return np.percentile(list(recorded.values()), (1 - 1 / self.rf) * 100) + return ( + np.percentile(list(recorded.values()), (1 - 1 / self.rf) * 100) + if recorded + else None + ) def on_result(self, task, cur_iter, cur_rew): """ @@ -202,21 +198,22 @@ def on_result(self, task, cur_iter, cur_rew): :return: action, milestone_reached, next_milestone """ assert cur_rew is not None, \ - "Reward attribute must be a numerical value, not None" + "Reward attribute must be a numerical value, not None" action = True milestone_reached = False next_milestone = None task_key = str(task.task_id) for milestone, recorded in self._rungs: - if not (cur_iter < milestone or task_key in recorded): + if cur_iter >= milestone and task_key not in recorded: # Note: It is important for model-based searchers that # milestones are reached exactly, not jumped over. In # particular, if a future milestone is reported via # register_pending, its reward value has to be passed # later on via update. - assert cur_iter == milestone, \ - "cur_iter = {} > {} = milestone. Make sure to report time attributes covering all milestones".format( - cur_iter, milestone) + assert ( + cur_iter == milestone + ), f"cur_iter = {cur_iter} > {milestone} = milestone. Make sure to report time attributes covering all milestones" + milestone_reached = True cutoff = self.cutoff(recorded) if cutoff is not None and cur_rew < cutoff: @@ -237,4 +234,4 @@ def __repr__(self): "Iter {:.3f}: {}".format(milestone, self.cutoff(recorded)) for milestone, recorded in self._rungs ]) - return "Bracket: " + iters + return f"Bracket: {iters}" diff --git a/autogluon/scheduler/remote/remote.py b/autogluon/scheduler/remote/remote.py index 216890c847ee..80567a9b31c7 100644 --- a/autogluon/scheduler/remote/remote.py +++ b/autogluon/scheduler/remote/remote.py @@ -66,7 +66,7 @@ def __init__(self, remote_ip=None, port=None, local=False, ssh_username=None, if local: super().__init__(processes=False) else: - remote_addr = (remote_ip + ':{}'.format(port)) + remote_addr = f'{remote_ip}:{port}' self.service = start_service(remote_ip, port) _set_global_remote_service(self.service) import time @@ -87,9 +87,7 @@ def upload_files(self, files, **kwargs): self.upload_file(filename, **kwargs) def __repr__(self): - reprstr = self.__class__.__name__ + ' REMOTE_ID: {}, \n\t'.format(self.remote_id) + \ - super().__repr__() - return reprstr + return f'{self.__class__.__name__} REMOTE_ID: {self.remote_id}, \n\t{super().__repr__()}' class DaskRemoteService(object): def __init__(self, remote_addr, scheduler_port, ssh_username=None, diff --git a/autogluon/scheduler/remote/remote_manager.py b/autogluon/scheduler/remote/remote_manager.py index 5ab006a41899..3e6e340f974f 100644 --- a/autogluon/scheduler/remote/remote_manager.py +++ b/autogluon/scheduler/remote/remote_manager.py @@ -72,7 +72,7 @@ def add_remote_nodes(cls, ip_addrs): remotes = [] for node_ip in ip_addrs: if node_ip in cls.NODES.keys(): - logger.warning('Already added remote {}'.format(node_ip)) + logger.warning(f'Already added remote {node_ip}') continue port = cls.get_port_id() remote = Remote(node_ip, port) @@ -104,6 +104,6 @@ def __exit__(self, *args): def __repr__(self): reprstr = self.__class__.__name__ + '(\n' for node in self.NODES.values(): - reprstr += '{}, \n'.format(node) + reprstr += f'{node}, \n' reprstr += ')\n' return reprstr diff --git a/autogluon/scheduler/reporter.py b/autogluon/scheduler/reporter.py index b4592315cc80..ca0bb72c7ca8 100644 --- a/autogluon/scheduler/reporter.py +++ b/autogluon/scheduler/reporter.py @@ -59,7 +59,7 @@ def __call__(self, **kwargs): kwargs['time_this_iter'] = report_time - self._last_report_time self._last_report_time = report_time - logger.debug('Reporting {}'.format(json.dumps(kwargs))) + logger.debug(f'Reporting {json.dumps(kwargs)}') try: self._queue.put(kwargs.copy()) except RuntimeError: @@ -94,8 +94,7 @@ def get_dict(self): raise NotImplementedError def __repr__(self): - reprstr = self.__class__.__name__ - return reprstr + return self.__class__.__name__ class LocalStatusReporter(object): @@ -132,15 +131,14 @@ def __call__(self, **kwargs): self._last_report_time = report_time self._queue.put(kwargs.copy(), block=True) - logger.debug('StatusReporter reporting: {}'.format(json.dumps(kwargs))) + logger.debug(f'StatusReporter reporting: {json.dumps(kwargs)}') self._continue_semaphore.acquire() if self._stop.value: raise AutoGluonEarlyStop def fetch(self, block=True): - kwargs = self._queue.get(block=block) - return kwargs + return self._queue.get(block=block) def move_on(self): self._continue_semaphore.release() @@ -157,19 +155,18 @@ def _start(self): def save_dict(self, **state_dict): """Save the serializable state_dict """ - logger.debug('Saving the task dict to {}'.format(self.dict_path)) + logger.debug(f'Saving the task dict to {self.dict_path}') save(state_dict, self.dict_path) def has_dict(self): - logger.debug('has_dict {}'.format(os.path.isfile(self.dict_path))) + logger.debug(f'has_dict {os.path.isfile(self.dict_path)}') return os.path.isfile(self.dict_path) def get_dict(self): return load(self.dict_path) def __repr__(self): - reprstr = self.__class__.__name__ - return reprstr + return self.__class__.__name__ class Communicator(threading.Thread): @@ -222,14 +219,13 @@ def Create(cls, process, local_reporter, dist_reporter): return communicator def __repr__(self): - reprstr = self.__class__.__name__ - return reprstr + return self.__class__.__name__ class DistSemaphore(object): def __init__(self, value, remote=None): self._queue = Queue(client=remote) - for i in range(value): + for _ in range(value): self._queue.put(1) def acquire(self): @@ -242,5 +238,4 @@ def release(self): self._queue.put(1) def __repr__(self): - reprstr = self.__class__.__name__ - return reprstr + return self.__class__.__name__ diff --git a/autogluon/scheduler/resource/dist_manager.py b/autogluon/scheduler/resource/dist_manager.py index dc395b24a2e6..0ce62dea9722 100644 --- a/autogluon/scheduler/resource/dist_manager.py +++ b/autogluon/scheduler/resource/dist_manager.py @@ -36,7 +36,7 @@ def reserve_resource(cls, remote, resource): if not node_manager.check_availability(resource): return False node_manager._request(remote, resource) - logger.info('Reserved {} in {}'.format(resource, remote)) + logger.info(f'Reserved {resource} in {remote}') return True @classmethod @@ -47,26 +47,32 @@ def release_reserved_resource(cls, remote, resource): @classmethod def _refresh_resource(cls): - cls.MAX_CPU_COUNT = max([x.get_all_resources()[0] for x in cls.NODE_RESOURCE_MANAGER.values()]) - cls.MAX_GPU_COUNT = max([x.get_all_resources()[1] for x in cls.NODE_RESOURCE_MANAGER.values()]) + cls.MAX_CPU_COUNT = max( + x.get_all_resources()[0] for x in cls.NODE_RESOURCE_MANAGER.values() + ) + + cls.MAX_GPU_COUNT = max( + x.get_all_resources()[1] for x in cls.NODE_RESOURCE_MANAGER.values() + ) @classmethod def _request(cls, resource): """ResourceManager, we recommand using scheduler instead of creating your own resource manager. """ - assert cls.check_possible(resource), \ - 'Requested num_cpu={} and num_gpu={} should be less than or equal to' + \ - 'largest node availability CPUs={}, GPUs={}'. \ - format(resource.num_cpus, resource.num_gpus, cls.MAX_GPU_COUNT, cls.MAX_CPU_COUNT) - + assert cls.check_possible(resource), ( + 'Requested num_cpu={} and num_gpu={} should be less than or equal to' + + f'largest node availability CPUs={resource.num_cpus}, GPUs={resource.num_gpus}' + ) + + with cls.LOCK: node = cls.check_availability(resource) if node is not None: cls.NODE_RESOURCE_MANAGER[node]._request(node, resource) return - logger.debug('Appending {} to Request Stack'.format(resource)) + logger.debug(f'Appending {resource} to Request Stack') request_semaphore = mp.Semaphore(0) with cls.LOCK: cls.REQUESTING_STACK.append((resource, request_semaphore)) @@ -75,7 +81,7 @@ def _request(cls, resource): @classmethod def _release(cls, resource): - logger.debug('\nReleasing resource {}'.format(resource)) + logger.debug(f'\nReleasing resource {resource}') cls.NODE_RESOURCE_MANAGER[resource.node]._release(resource) cls._evoke_request() @@ -88,7 +94,7 @@ def _evoke_request(cls): node = cls.check_availability(resource) if node is not None: cls.NODE_RESOURCE_MANAGER[node]._request(node, resource) - logger.debug('\nEvoking requesting resource {}'.format(resource)) + logger.debug(f'\nEvoking requesting resource {resource}') request_semaphore.release() succeed = True else: @@ -102,19 +108,23 @@ def check_availability(cls, resource): """Unsafe check """ candidate_nodes = cls._get_possible_nodes(resource) - for node in candidate_nodes: - if cls.NODE_RESOURCE_MANAGER[node].check_availability(resource): - #logger.debug('\nSuccessfully find node {}'.format(node)) - return node - return None + return next( + ( + node + for node in candidate_nodes + if cls.NODE_RESOURCE_MANAGER[node].check_availability(resource) + ), + None, + ) @classmethod def check_possible(cls, resource): assert isinstance(resource, DistributedResource), \ - 'Only support autogluon.resource.DistributedResource' - if resource.num_cpus > cls.MAX_CPU_COUNT or resource.num_gpus > cls.MAX_GPU_COUNT: - return False - return True + 'Only support autogluon.resource.DistributedResource' + return ( + resource.num_cpus <= cls.MAX_CPU_COUNT + and resource.num_gpus <= cls.MAX_GPU_COUNT + ) @classmethod def remove_remote(cls, remotes): @@ -122,20 +132,19 @@ def remove_remote(cls, remotes): """Enables dynamically removing nodes """ cls._refresh_resource() - pass @classmethod def _get_possible_nodes(cls, resource): - candidates = [] - for remote, manager in cls.NODE_RESOURCE_MANAGER.items(): - if manager.check_possible(resource): - candidates.append(remote) - return candidates + return [ + remote + for remote, manager in cls.NODE_RESOURCE_MANAGER.items() + if manager.check_possible(resource) + ] def __repr__(self): reprstr = self.__class__.__name__ + '{\n' for remote, manager in self.NODE_RESOURCE_MANAGER.items(): - reprstr += '(Remote: {}, Resource: {})\n'.format(remote, manager) + reprstr += f'(Remote: {remote}, Resource: {manager})\n' reprstr += '}' return reprstr @@ -158,14 +167,15 @@ def _request(self, remote, resource): """ResourceManager, we recommand using scheduler instead of creating your own resource manager. """ - assert self.check_possible(resource), \ - 'Requested num_cpu={} and num_gpu={} should be less than or equal to' + \ - 'system availability CPUs={}, GPUs={}'. \ - format(resource.num_cpus, resource.num_gpus, self.MAX_GPU_COUNT, self.MAX_CPU_COUNT) + assert self.check_possible(resource), ( + 'Requested num_cpu={} and num_gpu={} should be less than or equal to' + + f'system availability CPUs={resource.num_cpus}, GPUs={resource.num_gpus}' + ) + with self.LOCK: - cpu_ids = [self.CPU_QUEUE.get() for i in range(resource.num_cpus)] - gpu_ids = [self.GPU_QUEUE.get() for i in range(resource.num_gpus)] + cpu_ids = [self.CPU_QUEUE.get() for _ in range(resource.num_cpus)] + gpu_ids = [self.GPU_QUEUE.get() for _ in range(resource.num_gpus)] resource._ready(remote, cpu_ids, gpu_ids) #logger.debug("\nReqeust succeed {}".format(resource)) return @@ -187,18 +197,19 @@ def get_all_resources(self): def check_availability(self, resource): """Unsafe check """ - if resource.num_cpus > self.CPU_QUEUE.qsize() or resource.num_gpus > self.GPU_QUEUE.qsize(): - return False - return True + return ( + resource.num_cpus <= self.CPU_QUEUE.qsize() + and resource.num_gpus <= self.GPU_QUEUE.qsize() + ) def check_possible(self, resource): assert isinstance(resource, DistributedResource), 'Only support autogluon.resource.Resources' - if resource.num_cpus > self.MAX_CPU_COUNT or resource.num_gpus > self.MAX_GPU_COUNT: - return False - return True + return ( + resource.num_cpus <= self.MAX_CPU_COUNT + and resource.num_gpus <= self.MAX_GPU_COUNT + ) def __repr__(self): - reprstr = self.__class__.__name__ + '(' + \ - '{} CPUs, '.format(self.MAX_CPU_COUNT) + \ - '{} GPUs)'.format(self.MAX_GPU_COUNT) - return reprstr + return ( + f'{self.__class__.__name__}(' + f'{self.MAX_CPU_COUNT} CPUs, ' + ) + f'{self.MAX_GPU_COUNT} GPUs)' diff --git a/autogluon/scheduler/resource/nvutil.py b/autogluon/scheduler/resource/nvutil.py index cd9629f7f989..433dec74344c 100644 --- a/autogluon/scheduler/resource/nvutil.py +++ b/autogluon/scheduler/resource/nvutil.py @@ -53,14 +53,14 @@ def _LoadNvmlLibrary(): Load the library if it isn't loaded already ''' global cudaLib - + ret = True - if (cudaLib == None): + if cudaLib is None: # lock to ensure only one caller loads the library libLoadLock.acquire() try: # ensure the library still isn't loaded - if (cudaLib == None): + if cudaLib is None: try: if (sys.platform[:3] == "win"): # cdecl calling convention @@ -72,8 +72,8 @@ def _LoadNvmlLibrary(): except OSError as ose: pass - if (cudaLib == None): - ret = False + if cudaLib is None: + ret = False finally: # lock is always freed libLoadLock.release() @@ -88,17 +88,17 @@ def cudaSystemGetNVMLVersion(): return c_version.value.decode('UTF-8') ## Function access ## -_cudaGetFunctionPointer_cache = dict() # function pointers are cached to prevent unnecessary libLoadLock locking +_cudaGetFunctionPointer_cache = {} def _cudaGetFunctionPointer(name): global cudaLib if name in _cudaGetFunctionPointer_cache: return _cudaGetFunctionPointer_cache[name] - + libLoadLock.acquire() try: # ensure library was loaded - if (cudaLib == None): + if cudaLib is None: raise NVMLError(NVML_ERROR_UNINITIALIZED) try: _cudaGetFunctionPointer_cache[name] = getattr(cudaLib, name) @@ -121,14 +121,14 @@ class NVMLError(Exception): NVML_ERROR_UNINITIALIZED: "Uninitialized", NVML_ERROR_LIBRARY_NOT_FOUND: "NVML Shared Library Not Found", } - def __new__(typ, value): + def __new__(cls, value): ''' Maps value to a proper subclass of NVMLError. See _extractNVMLErrorsAsClasses function for more details ''' - if typ == NVMLError: - typ = NVMLError._valClassMapping.get(value, typ) - obj = Exception.__new__(typ) + if cls == NVMLError: + cls = NVMLError._valClassMapping.get(value, cls) + obj = Exception.__new__(cls) obj.value = value return obj @@ -150,11 +150,11 @@ def cudaShutdown(): fn = _cudaGetFunctionPointer("nvmlShutdown") ret = fn() _cudaCheckReturn(ret) - + # Atomically update refcount global _cudaLib_refcount libLoadLock.acquire() - if (0 < _cudaLib_refcount): + if _cudaLib_refcount > 0: _cudaLib_refcount -= 1 libLoadLock.release() return None diff --git a/autogluon/scheduler/resource/resource.py b/autogluon/scheduler/resource/resource.py index 065820a8b99d..3fba67cd88ce 100644 --- a/autogluon/scheduler/resource/resource.py +++ b/autogluon/scheduler/resource/resource.py @@ -38,12 +38,11 @@ def _ready(self, cids, gids): self.ready = True def __repr__(self): - reprstr = self.__class__.__name__ + '(' \ - + 'nCPUs = ' + str(self.num_cpus) + reprstr = f'{self.__class__.__name__}(nCPUs = {str(self.num_cpus)}' if len(self.cpu_ids) > 0: reprstr += ', CPU_IDs = {' + str(self.cpu_ids) + '}' if self.num_gpus > 0: - reprstr += ', nGPUs = ' + str(self.num_gpus) + reprstr += f', nGPUs = {str(self.num_gpus)}' if len(self.gpu_ids) > 0: reprstr += ', GPU_IDs = {' + str(self.gpu_ids) + '}' reprstr += ')' @@ -78,12 +77,13 @@ def _release(self): def __repr__(self): reprstr = self.__class__.__name__ + '(\n\t' - if self.node: reprstr += 'Node = ' + str(self.node) + if self.node: + reprstr += f'Node = {str(self.node)}' reprstr += '\n\tnCPUs = ' + str(self.num_cpus) if len(self.cpu_ids) > 0: reprstr += ', CPU_IDs = {' + str(self.cpu_ids) + '}' if self.num_gpus > 0: - reprstr += ', nGPUs = ' + str(self.num_gpus) + reprstr += f', nGPUs = {str(self.num_gpus)}' if len(self.gpu_ids) > 0: reprstr += ', GPU_IDs = {' + str(self.gpu_ids) + '}' reprstr += ')' diff --git a/autogluon/scheduler/rl_scheduler.py b/autogluon/scheduler/rl_scheduler.py index 7543a495fa50..68079bfe2058 100644 --- a/autogluon/scheduler/rl_scheduler.py +++ b/autogluon/scheduler/rl_scheduler.py @@ -99,7 +99,7 @@ class RLScheduler(FIFOScheduler): """ def __init__(self, train_fn, **kwargs): assert isinstance(train_fn, _autogluon_method), 'Please use @ag.args ' + \ - 'to decorate your training script.' + 'to decorate your training script.' # Check values and impute default values (only for arguments new to # this class) kwargs = check_and_merge_defaults( @@ -127,7 +127,7 @@ def __init__(self, train_fn, **kwargs): self.controller_resource = DistributedResource(**controller_resource) assert self.resource_manager.reserve_resource( master_node, self.controller_resource),\ - "Not Enough Resource on Master Node for Training Controller" + "Not Enough Resource on Master Node for Training Controller" if controller_resource['num_gpus'] > 0: self.controller_ctx = [ mx.gpu(i) for i in self.controller_resource.gpu_ids] @@ -154,7 +154,7 @@ def __init__(self, train_fn, **kwargs): if os.path.isfile(checkpoint): self.load_state_dict(load(checkpoint)) else: - msg = 'checkpoint path {} is not available for resume.'.format(checkpoint) + msg = f'checkpoint path {checkpoint} is not available for resume.' logger.exception(msg) def run(self, **kwargs): @@ -162,8 +162,11 @@ def run(self, **kwargs): """ self.num_trials = kwargs.get('num_trials', self.num_trials) logger.info('Starting Experiments') - logger.info('Num of Finished Tasks is {}'.format(self.num_finished_tasks)) - logger.info('Num of Pending Tasks is {}'.format(self.num_trials - self.num_finished_tasks)) + logger.info(f'Num of Finished Tasks is {self.num_finished_tasks}') + logger.info( + f'Num of Pending Tasks is {self.num_trials - self.num_finished_tasks}' + ) + if self.sync: self._run_sync() else: @@ -175,8 +178,8 @@ def _run_sync(self): with mx.autograd.record(): # sample controller_batch_size number of configurations batch_size = self.num_trials % self.num_trials \ - if i == self.num_trials // self.controller_batch_size \ - else self.controller_batch_size + if i == self.num_trials // self.controller_batch_size \ + else self.controller_batch_size if batch_size == 0: continue configs, log_probs, entropies = self.controller.sample( batch_size, with_details=True) @@ -198,7 +201,7 @@ def _run_sync(self): # update loss.backward() self.controller_optimizer.step(batch_size) - logger.debug('controller loss: {}'.format(loss.asscalar())) + logger.debug(f'controller loss: {loss.asscalar()}') def _run_async(self): def _async_run_trial(): @@ -337,9 +340,7 @@ def add_job(self, task, **kwargs): """ cls = RLScheduler cls.resource_manager._request(task.resources) - # main process - job = cls._start_distributed_job(task, cls.resource_manager) - return job + return cls._start_distributed_job(task, cls.resource_manager) def join_tasks(self): pass @@ -354,13 +355,13 @@ def state_dict(self, destination=None): if destination is None: destination = OrderedDict() destination._metadata = OrderedDict() - logger.debug('\nState_Dict self.finished_tasks: {}'.format(self.finished_tasks)) + logger.debug(f'\nState_Dict self.finished_tasks: {self.finished_tasks}') destination['finished_tasks'] = pickle.dumps(self.finished_tasks) destination['baseline'] = pickle.dumps(self.baseline) destination['TASK_ID'] = Task.TASK_ID.value destination['searcher'] = self.searcher.state_dict() destination['training_history'] = json.dumps(self.training_history) - if self.visualizer == 'mxboard' or self.visualizer == 'tensorboard': + if self.visualizer in ['mxboard', 'tensorboard']: destination['visualizer'] = json.dumps(self.mxboard._scalar_dict) return destination @@ -376,6 +377,6 @@ def load_state_dict(self, state_dict): Task.set_id(state_dict['TASK_ID']) self.searcher.load_state_dict(state_dict['searcher']) self.training_history = json.loads(state_dict['training_history']) - if self.visualizer == 'mxboard' or self.visualizer == 'tensorboard': + if self.visualizer in ['mxboard', 'tensorboard']: self.mxboard._scalar_dict = json.loads(state_dict['visualizer']) - logger.debug('Loading Searcher State {}'.format(self.searcher)) + logger.debug(f'Loading Searcher State {self.searcher}') diff --git a/autogluon/scheduler/scheduler.py b/autogluon/scheduler/scheduler.py index 0bd4bb81b8d7..8e52f4387ff1 100644 --- a/autogluon/scheduler/scheduler.py +++ b/autogluon/scheduler/scheduler.py @@ -34,16 +34,16 @@ class TaskScheduler(object): _remote_manager = None @ClassProperty - def resource_manager(cls): - if cls._resource_manager is None: - cls._resource_manager = DistributedResourceManager() - return cls._resource_manager + def resource_manager(self): + if self._resource_manager is None: + self._resource_manager = DistributedResourceManager() + return self._resource_manager @ClassProperty - def remote_manager(cls): - if cls._remote_manager is None: - cls._remote_manager = RemoteManager() - return cls._remote_manager + def remote_manager(self): + if self._remote_manager is None: + self._remote_manager = RemoteManager() + return self._remote_manager def __init__(self, dist_ip_addrs=None): if dist_ip_addrs is None: @@ -71,9 +71,8 @@ def upload_files(cls, files, **kwargs): def _dict_from_task(self, task): if isinstance(task, Task): return {'TASK_ID': task.task_id, 'Args': task.args} - else: - assert isinstance(task, dict) - return {'TASK_ID': task['TASK_ID'], 'Args': task['Args']} + assert isinstance(task, dict) + return {'TASK_ID': task['TASK_ID'], 'Args': task['Args']} def add_task(self, task, **kwargs): """add_task() is now deprecated in favor of add_job(). @@ -232,13 +231,11 @@ def load_state_dict(self, state_dict): """ self.finished_tasks = pickle.loads(state_dict['finished_tasks']) Task.set_id(state_dict['TASK_ID']) - logger.debug('\nLoading finished_tasks: {} '.format(self.finished_tasks)) + logger.debug(f'\nLoading finished_tasks: {self.finished_tasks} ') @property def num_finished_tasks(self): return len(self.finished_tasks) def __repr__(self): - reprstr = self.__class__.__name__ + '(\n' + \ - str(self.resource_manager) +')\n' - return reprstr + return self.__class__.__name__ + '(\n' + str(self.resource_manager) + ')\n' diff --git a/autogluon/searcher/bayesopt/autogluon/debug_log.py b/autogluon/searcher/bayesopt/autogluon/debug_log.py index 0107a125a799..a1b281f28834 100644 --- a/autogluon/searcher/bayesopt/autogluon/debug_log.py +++ b/autogluon/searcher/bayesopt/autogluon/debug_log.py @@ -49,40 +49,40 @@ class DebugLogPrinter(object): def __init__( self, configspace_ext: Optional[ExtendedConfiguration] = None): self.config_counter = 0 - self._config_id = dict() - self.block_info = dict() + self._config_id = {} + self.block_info = {} self.get_config_type = None self.configspace_ext = configspace_ext def config_id(self, config: Union[CS.Configuration, dict]) -> str: config_key, _, resource = _to_key(config, self.configspace_ext) _id = str(self._config_id[config_key]) - if resource is None: - return _id - else: - return _id + ':{}'.format(resource) + return _id if resource is None else f'{_id}:{resource}' def start_get_config(self, gc_type): assert gc_type in {'random', 'BO'} - assert self.get_config_type is None, \ - "Block for get_config of type '{}' is currently open".format( - self.get_config_type) + assert ( + self.get_config_type is None + ), f"Block for get_config of type '{self.get_config_type}' is currently open" + self.get_config_type = gc_type - logger.info("Starting get_config[{}] for config_id {}".format( - gc_type, self.config_counter)) + logger.info( + f"Starting get_config[{gc_type}] for config_id {self.config_counter}" + ) def set_final_config(self, config: Union[CS.Configuration, dict]): assert self.get_config_type is not None, "No block open right now" config_key, config, resource = _to_key(config, self.configspace_ext) assert resource is None, \ - "set_final_config: config must not be extended" + "set_final_config: config must not be extended" _id = self._config_id.get(config_key) - assert _id is None, \ - "Config {} already has been assigned a config ID = {}".format( - config, _id) + assert ( + _id is None + ), f"Config {config} already has been assigned a config ID = {_id}" + # Counter is advanced in write_block self._config_id[config_key] = self.config_counter - entries = ['{}: {}'.format(k, v) for k, v in config.items()] + entries = [f'{k}: {v}' for k, v in config.items()] msg = '\n'.join(entries) self.block_info['final_config'] = msg @@ -94,17 +94,17 @@ def set_state(self, state: TuningJobState): [self.config_id(x) for x in labeled_configs]) pending_str = ', '.join( [self.config_id(x) for x in state.pending_candidates]) - msg = 'Labeled: ' + labeled_str + '. Pending: ' + pending_str + msg = f'Labeled: {labeled_str}. Pending: {pending_str}' self.block_info['state'] = msg def set_targets(self, targets: np.ndarray): assert self.get_config_type == 'BO', "Need to be in 'BO' block" - msg = 'Targets: ' + str(targets.reshape((-1,))) + msg = f'Targets: {str(targets.reshape((-1, )))}' self.block_info['targets'] = msg def set_gp_params(self, params: dict): assert self.get_config_type == 'BO', "Need to be in 'BO' block" - msg = 'GP params:' + str(params) + msg = f'GP params:{params}' self.block_info['params'] = msg def set_fantasies(self, fantasies: np.ndarray): @@ -116,7 +116,7 @@ def set_init_config(self, config: Union[CS.Configuration, dict], top_scores: np.ndarray = None): assert self.get_config_type == 'BO', "Need to be in 'BO' block" _, config, _ = _to_key(config, self.configspace_ext) - entries = ['{}: {}'.format(k, v) for k, v in config.items()] + entries = [f'{k}: {v}' for k, v in config.items()] msg = "Started BO from (top scorer):\n" + '\n'.join(entries) if top_scores is not None: msg += ("\nTop score values: " + str(top_scores.reshape((-1,)))) @@ -130,11 +130,12 @@ def write_block(self): assert self.get_config_type is not None, "No block open right now" info = self.block_info if 'num_evals' in info: - parts = ['[{}: {}] ({} evaluations)'.format( - self.config_counter, self.get_config_type, info['num_evals'])] + parts = [ + f"[{self.config_counter}: {self.get_config_type}] ({info['num_evals']} evaluations)" + ] + else: - parts = ['[{}: {}]'.format( - self.config_counter, self.get_config_type)] + parts = [f'[{self.config_counter}: {self.get_config_type}]'] parts.append(info['final_config']) if self.get_config_type == 'BO': parts.extend( @@ -147,7 +148,7 @@ def write_block(self): # Advance counter self.config_counter += 1 self.get_config_type = None - self.block_info = dict() + self.block_info = {} def get_mutable_state(self) -> dict: return { @@ -155,9 +156,10 @@ def get_mutable_state(self) -> dict: 'config_id': self._config_id} def set_mutable_state(self, state: dict): - assert self.get_config_type is None, \ - "Block for get_config of type '{}' is currently open".format( - self.get_config_type) + assert ( + self.get_config_type is None + ), f"Block for get_config of type '{self.get_config_type}' is currently open" + self.config_counter = state['config_counter'] self._config_id = state['config_id'] - self.block_info = dict() + self.block_info = {} diff --git a/autogluon/searcher/bayesopt/autogluon/gp_fifo_searcher.py b/autogluon/searcher/bayesopt/autogluon/gp_fifo_searcher.py index 7f27cc1c42ad..e74494843a10 100644 --- a/autogluon/searcher/bayesopt/autogluon/gp_fifo_searcher.py +++ b/autogluon/searcher/bayesopt/autogluon/gp_fifo_searcher.py @@ -89,11 +89,11 @@ def create_initial_candidates_scorer( def check_initial_candidates_scorer(initial_scoring: str) -> str: if initial_scoring is None: return DEFAULT_INITIAL_SCORING - else: - assert initial_scoring in SUPPORTED_INITIAL_SCORING, \ - "initial_scoring = '{}' is not supported".format( - initial_scoring) - return initial_scoring + assert ( + initial_scoring in SUPPORTED_INITIAL_SCORING + ), f"initial_scoring = '{initial_scoring}' is not supported" + + return initial_scoring class GPFIFOSearcher(object): @@ -166,7 +166,7 @@ def __init__( pending_evaluations=[]) else: assert hp_ranges is init_state.hp_ranges, \ - "hp_ranges and init_state.hp_ranges must be same object" + "hp_ranges and init_state.hp_ranges must be same object" self.state_transformer = GPMXNetPendingCandidateStateTransformer( gpmodel=gpmodel, init_state=init_state, @@ -182,20 +182,23 @@ def __init__( self.first_is_default = first_is_default if first_is_default: assert isinstance(hp_ranges, HyperparameterRanges_CS), \ - "If first_is_default, must have hp_ranges of HyperparameterRanges_CS type" + "If first_is_default, must have hp_ranges of HyperparameterRanges_CS type" if debug_log is not None: assert isinstance(hp_ranges, HyperparameterRanges_CS), \ - "If debug_log is given, must have hp_ranges of HyperparameterRanges_CS type" + "If debug_log is given, must have hp_ranges of HyperparameterRanges_CS type" # Sums up profiling records across all get_config calls - self._profile_record = dict() + self._profile_record = {} if debug_log is not None: - deb_msg = "[GPFIFOSearcher.__init__]\n" - deb_msg += ("- acquisition_class = {}\n".format(acquisition_class)) - deb_msg += ("- local_minimizer_class = {}\n".format(local_minimizer_class)) - deb_msg += ("- num_initial_candidates = {}\n".format(num_initial_candidates)) - deb_msg += ("- num_initial_random_choices = {}\n".format(num_initial_random_choices)) - deb_msg += ("- initial_scoring = {}\n".format(self.initial_scoring)) - deb_msg += ("- first_is_default = {}".format(first_is_default)) + deb_msg = ( + "[GPFIFOSearcher.__init__]\n" + + f"- acquisition_class = {acquisition_class}\n" + ) + + deb_msg += f"- local_minimizer_class = {local_minimizer_class}\n" + deb_msg += f"- num_initial_candidates = {num_initial_candidates}\n" + deb_msg += f"- num_initial_random_choices = {num_initial_random_choices}\n" + deb_msg += f"- initial_scoring = {self.initial_scoring}\n" + deb_msg += f"- first_is_default = {first_is_default}" logger.info(deb_msg) def update(self, config: Candidate, reward: float): @@ -214,8 +217,8 @@ def update(self, config: Candidate, reward: float): metrics=dictionarize_objective(crit_val))) if self.debug_log is not None: config_id = self.debug_log.config_id(config) - msg = "Update for config_id {}: reward = {}, crit_val = {}".format( - config_id, reward, crit_val) + msg = f"Update for config_id {config_id}: reward = {reward}, crit_val = {crit_val}" + logger.info(msg) def register_pending(self, config: Candidate): @@ -256,7 +259,7 @@ def get_config(self) -> Candidate: self.profiler.set_state(state, fit_hyperparams) blacklisted_candidates = compute_blacklisted_candidates(state) pick_random = (len(blacklisted_candidates) < self.num_initial_random_choices) or \ - (not state.candidate_evaluations) + (not state.candidate_evaluations) if self.debug_log is not None: self.debug_log.start_get_config('random' if pick_random else 'BO') if pick_random: @@ -267,8 +270,7 @@ def get_config(self) -> Candidate: if default_config and len(default_config.get_dictionary()) > 0: config = default_config if self.debug_log is not None: - logger.info("Start with default config:\n{}".format( - candidate_for_print(config))) + logger.info(f"Start with default config:\n{candidate_for_print(config)}") if config is None: if self.do_profile: self.profiler.start('random') @@ -279,9 +281,9 @@ def get_config(self) -> Candidate: break if config is None: raise AssertionError( - "Failed to sample a configuration not already chosen " - "before. Maybe there are no free configurations left? " - "The blacklist size is {}".format(len(blacklisted_candidates))) + f"Failed to sample a configuration not already chosen before. Maybe there are no free configurations left? The blacklist size is {len(blacklisted_candidates)}" + ) + if self.do_profile: self.profiler.stop('random') else: @@ -322,9 +324,9 @@ def get_config(self) -> Candidate: _config = bo_algorithm.next_candidates() if len(_config) == 0: raise AssertionError( - "Failed to find a configuration not already chosen " - "before. Maybe there are no free configurations left? " - "The blacklist size is {}".format(len(blacklisted_candidates))) + f"Failed to find a configuration not already chosen before. Maybe there are no free configurations left? The blacklist size is {len(blacklisted_candidates)}" + ) + config = _config[0] if self.do_profile: self.profiler.stop('total_nextcand') diff --git a/autogluon/searcher/bayesopt/autogluon/gp_multifidelity_searcher.py b/autogluon/searcher/bayesopt/autogluon/gp_multifidelity_searcher.py index cd407b82a212..e8bdc261bcae 100644 --- a/autogluon/searcher/bayesopt/autogluon/gp_multifidelity_searcher.py +++ b/autogluon/searcher/bayesopt/autogluon/gp_multifidelity_searcher.py @@ -158,15 +158,18 @@ def __init__( self.do_profile = (profiler is not None) self.first_is_default = first_is_default # Sums up profiling records across all get_config calls - self._profile_record = dict() + self._profile_record = {} if debug_log is not None: - deb_msg = "[GPMultiFidelitySearcher.__init__]\n" - deb_msg += ("- acquisition_class = {}\n".format(acquisition_class)) - deb_msg += ("- local_minimizer_class = {}\n".format(local_minimizer_class)) - deb_msg += ("- num_initial_candidates = {}\n".format(num_initial_candidates)) - deb_msg += ("- num_initial_random_choices = {}\n".format(num_initial_random_choices)) - deb_msg += ("- initial_scoring = {}\n".format(self.initial_scoring)) - deb_msg += ("- first_is_default = {}".format(first_is_default)) + deb_msg = ( + "[GPMultiFidelitySearcher.__init__]\n" + + f"- acquisition_class = {acquisition_class}\n" + ) + + deb_msg += f"- local_minimizer_class = {local_minimizer_class}\n" + deb_msg += f"- num_initial_candidates = {num_initial_candidates}\n" + deb_msg += f"- num_initial_random_choices = {num_initial_random_choices}\n" + deb_msg += f"- initial_scoring = {self.initial_scoring}\n" + deb_msg += f"- first_is_default = {first_is_default}" logger.info(deb_msg) def update(self, config: CS.Configuration, reward: float, resource: int): @@ -186,8 +189,8 @@ def update(self, config: CS.Configuration, reward: float, resource: int): candidate=config_ext, metrics=dictionarize_objective(crit_val))) if self.debug_log is not None: config_id = self.debug_log.config_id(config_ext) - msg = "Update for config_id {}: reward = {}, crit_val = {}".format( - config_id, reward, crit_val) + msg = f"Update for config_id {config_id}: reward = {reward}, crit_val = {crit_val}" + logger.info(msg) def register_pending(self, config: CS.Configuration, milestone: int): @@ -266,7 +269,7 @@ def get_config(self, **kwargs) -> CS.Configuration: target_resource = self.configspace_ext.resource_attr_range[0] blacklisted_candidates = self._get_blacklisted_candidates(target_resource) pick_random = (len(blacklisted_candidates) < self.num_initial_random_choices) or \ - (not state.candidate_evaluations) + (not state.candidate_evaluations) if self.debug_log is not None: self.debug_log.start_get_config('random' if pick_random else 'BO') if pick_random: @@ -277,8 +280,7 @@ def get_config(self, **kwargs) -> CS.Configuration: if default_config and len(default_config.get_dictionary()) > 0: config = default_config if self.debug_log is not None: - logger.info("Start with default config:\n{}".format( - candidate_for_print(config))) + logger.info(f"Start with default config:\n{candidate_for_print(config)}") if config is None: if self.do_profile: self.profiler.start('random') @@ -326,10 +328,10 @@ def get_config(self, **kwargs) -> CS.Configuration: if self.do_profile: self.profiler.start('total_nextcand') _config = bo_algorithm.next_candidates() - assert len(_config) > 0, \ - ("Failed to find a configuration not already chosen " - "before. Maybe there are no free configurations left? " - "The blacklist size is {}".format(len(blacklisted_candidates))) + assert ( + len(_config) > 0 + ), f"Failed to find a configuration not already chosen before. Maybe there are no free configurations left? The blacklist size is {len(blacklisted_candidates)}" + next_config = _config[0] if self.do_profile: self.profiler.stop('total_nextcand') @@ -494,9 +496,9 @@ def draw_random_candidate( break if config is None: raise AssertionError( - "Failed to sample a configuration not already chosen " - "before. Maybe there are no free configurations left? " - "The blacklist size is {}".format(len(blacklisted_candidates))) + f"Failed to sample a configuration not already chosen before. Maybe there are no free configurations left? The blacklist size is {len(blacklisted_candidates)}" + ) + return config, config_ext diff --git a/autogluon/searcher/bayesopt/autogluon/gp_profiling.py b/autogluon/searcher/bayesopt/autogluon/gp_profiling.py index be02a4196651..1eb9b062058d 100644 --- a/autogluon/searcher/bayesopt/autogluon/gp_profiling.py +++ b/autogluon/searcher/bayesopt/autogluon/gp_profiling.py @@ -36,16 +36,17 @@ def field_names(): class GPMXNetSimpleProfiler(object): def __init__(self): - self.records = list() + self.records = [] self.block = None - self.start_time = dict() + self.start_time = {} self.id_counter = 0 def set_state(self, state: TuningJobState, fit_hyperparams: bool): - assert not self.start_time, \ - "Timers for these tags still running:\n{}".format( - self.start_time.keys()) + assert ( + not self.start_time + ), f"Timers for these tags still running:\n{self.start_time.keys()}" + self.block = ProfilingData( id=self.id_counter, tag='', duration=0., num_labeled=len(state.candidate_evaluations), @@ -56,21 +57,20 @@ def set_state(self, state: TuningJobState, def start(self, tag: str): assert self.block is not None - assert tag not in self.start_time, \ - "Timer for '{}' already running".format(tag) + assert tag not in self.start_time, f"Timer for '{tag}' already running" self.start_time[tag] = time.process_time() def stop(self, tag: str): - assert tag in self.start_time, \ - "Timer for '{}' does not exist".format(tag) + assert tag in self.start_time, f"Timer for '{tag}' does not exist" duration = time.process_time() - self.start_time[tag] self.records.append( self.block._replace(duration=duration, tag=tag)) del self.start_time[tag] def clear(self): - remaining_tags = list(self.start_time.keys()) - if remaining_tags: - logger.warning("Timers for these tags not stopped (will be removed):\n{}".format( - remaining_tags)) - self.start_time = dict() + if remaining_tags := list(self.start_time.keys()): + logger.warning( + f"Timers for these tags not stopped (will be removed):\n{remaining_tags}" + ) + + self.start_time = {} diff --git a/autogluon/searcher/bayesopt/autogluon/hp_ranges.py b/autogluon/searcher/bayesopt/autogluon/hp_ranges.py index 7e46c339e768..a8ac03c50411 100644 --- a/autogluon/searcher/bayesopt/autogluon/hp_ranges.py +++ b/autogluon/searcher/bayesopt/autogluon/hp_ranges.py @@ -53,8 +53,10 @@ def __init__(self, config_space: CS.ConfigurationSpace, categ_trg.append(trg_pos) categ_card.append(card) trg_pos += card - elif isinstance(hp, CS.UniformIntegerHyperparameter) or \ - isinstance(hp, CS.UniformFloatHyperparameter): + elif isinstance( + hp, + (CS.UniformIntegerHyperparameter, CS.UniformFloatHyperparameter), + ): if hp.name == name_last_pos: assert append_at_end is None append_at_end = (src_pos, 1, False) @@ -100,13 +102,15 @@ def ndarray_size(self) -> int: return self._ndarray_size def from_ndarray(self, cand_ndarray: np.ndarray) -> Candidate: - assert cand_ndarray.size == self._ndarray_size, \ - "Internal vector [{}] must have size {}".format( - cand_ndarray, self._ndarray_size) + assert ( + cand_ndarray.size == self._ndarray_size + ), f"Internal vector [{cand_ndarray}] must have size {self._ndarray_size}" + cand_ndarray = cand_ndarray.reshape((-1,)) - assert cand_ndarray.min() >= 0. and cand_ndarray.max() <= 1., \ - "Internal vector [{}] must have entries in [0, 1]".format( - cand_ndarray) + assert ( + cand_ndarray.min() >= 0.0 and cand_ndarray.max() <= 1.0 + ), f"Internal vector [{cand_ndarray}] must have entries in [0, 1]" + # Deal with categoricals by using argmax srcvec = np.zeros(self.__len__(), dtype=cand_ndarray.dtype) srcvec.put( @@ -131,18 +135,17 @@ def get_ndarray_bounds(self) -> List[Tuple[float, float]]: final_bound = None for hp in self.config_space.get_hyperparameters(): if isinstance(hp, CS.CategoricalHyperparameter): - if not self._fix_attribute_value(hp.name): - bound = [(0., 1.)] * len(hp.choices) - else: + if self._fix_attribute_value(hp.name): bound = [(0., 0.)] * len(hp.choices) bound[int(self.value_for_last_pos)] = (1., 1.) - else: - if not self._fix_attribute_value(hp.name): - bound = [(0., 1.)] else: - val_int = float(hp._inverse_transform( - np.array([self.value_for_last_pos])).item()) - bound = [(val_int, val_int)] + bound = [(0., 1.)] * len(hp.choices) + elif not self._fix_attribute_value(hp.name): + bound = [(0., 1.)] + else: + val_int = float(hp._inverse_transform( + np.array([self.value_for_last_pos])).item()) + bound = [(val_int, val_int)] if hp.name == self.name_last_pos: final_bound = bound else: @@ -173,8 +176,7 @@ def random_candidates( return rnd_configs def __repr__(self) -> str: - return "{}{}".format( - self.__class__.__name__, repr(self.config_space)) + return f"{self.__class__.__name__}{repr(self.config_space)}" def __eq__(self, other: object) -> bool: if isinstance(other, HyperparameterRanges_CS): diff --git a/autogluon/searcher/bayesopt/autogluon/model_factories.py b/autogluon/searcher/bayesopt/autogluon/model_factories.py index 3ffcb04de8de..81d26c77f7df 100644 --- a/autogluon/searcher/bayesopt/autogluon/model_factories.py +++ b/autogluon/searcher/bayesopt/autogluon/model_factories.py @@ -40,7 +40,7 @@ def resource_kernel_factory( elif name == 'exp-decay-delta1': delta_fixed_value = 1.0 else: - raise AssertionError("name = '{}' not supported".format(name)) + raise AssertionError(f"name = '{name}' not supported") res_kernel = ExponentialDecayResourcesKernelFunction( kernel_x, mean_x, gamma_init=0.5 * max_metric_value, delta_fixed_value=delta_fixed_value, diff --git a/autogluon/searcher/bayesopt/autogluon/searcher_factory.py b/autogluon/searcher/bayesopt/autogluon/searcher_factory.py index a8ee42c4f24d..a63ebb4ccf1b 100644 --- a/autogluon/searcher/bayesopt/autogluon/searcher_factory.py +++ b/autogluon/searcher/bayesopt/autogluon/searcher_factory.py @@ -69,17 +69,16 @@ def _create_common_objects(**kwargs): else: skip_optimization = None # Profiler - if kwargs.get('profiler', False): - profiler = GPMXNetSimpleProfiler() - else: - profiler = None + profiler = GPMXNetSimpleProfiler() if kwargs.get('profiler', False) else None # Conversion from reward to metric (strictly decreasing) and back _map_reward = kwargs.get('map_reward', '1_minus_x') if isinstance(_map_reward, str): _map_reward_name = _map_reward supp_map_reward = {'1_minus_x', 'minus_x'} - assert _map_reward_name in supp_map_reward, \ - "This factory needs map_reward in {}".format(supp_map_reward) + assert ( + _map_reward_name in supp_map_reward + ), f"This factory needs map_reward in {supp_map_reward}" + _map_reward: MapReward = map_reward( const=1.0 if _map_reward_name == '1_minus_x' else 0.0) else: @@ -146,15 +145,16 @@ def gp_fifo_searcher_factory(**kwargs) -> GPFIFOSearcher: :return: GPFIFOSearcher object """ - assert kwargs['scheduler'] == 'fifo', \ - "This factory needs scheduler = 'fifo' (instead of '{}')".format( - kwargs['scheduler']) + assert ( + kwargs['scheduler'] == 'fifo' + ), f"This factory needs scheduler = 'fifo' (instead of '{kwargs['scheduler']}')" + # Common objects hp_ranges_cs, random_seed, gpmodel, model_args, profiler, _map_reward, \ skip_optimization, debug_log = \ _create_common_objects(**kwargs) - gp_searcher = GPFIFOSearcher( + return GPFIFOSearcher( hp_ranges=hp_ranges_cs, random_seed=random_seed, gpmodel=gpmodel, @@ -167,8 +167,8 @@ def gp_fifo_searcher_factory(**kwargs) -> GPFIFOSearcher: initial_scoring=kwargs['initial_scoring'], profiler=profiler, first_is_default=kwargs['first_is_default'], - debug_log=debug_log) - return gp_searcher + debug_log=debug_log, + ) def gp_multifidelity_searcher_factory(**kwargs) -> GPMultiFidelitySearcher: @@ -181,9 +181,10 @@ def gp_multifidelity_searcher_factory(**kwargs) -> GPMultiFidelitySearcher: """ supp_schedulers = {'hyperband_stopping', 'hyperband_promotion'} - assert kwargs['scheduler'] in supp_schedulers, \ - "This factory needs scheduler in {} (instead of '{}')".format( - supp_schedulers, kwargs['scheduler']) + assert ( + kwargs['scheduler'] in supp_schedulers + ), f"This factory needs scheduler in {supp_schedulers} (instead of '{kwargs['scheduler']}')" + # Common objects hp_ranges_cs, random_seed, gpmodel, model_args, profiler, _map_reward,\ skip_optimization, debug_log = \ @@ -198,7 +199,7 @@ def gp_multifidelity_searcher_factory(**kwargs) -> GPMultiFidelitySearcher: "resource_acq must be 'bohb' or 'first'" resource_for_acquisition = resource_for_acquisition_first_milestone epoch_range = (kwargs['min_epochs'], kwargs['max_epochs']) - gp_searcher = GPMultiFidelitySearcher( + return GPMultiFidelitySearcher( hp_ranges=hp_ranges_cs, resource_attr_key=kwargs['resource_attribute'], resource_attr_range=epoch_range, @@ -214,8 +215,8 @@ def gp_multifidelity_searcher_factory(**kwargs) -> GPMultiFidelitySearcher: initial_scoring=kwargs['initial_scoring'], profiler=profiler, first_is_default=kwargs['first_is_default'], - debug_log=debug_log) - return gp_searcher + debug_log=debug_log, + ) def _common_defaults(is_hyperband: bool) -> (Set[str], dict, dict): diff --git a/autogluon/searcher/bayesopt/datatypes/hp_ranges.py b/autogluon/searcher/bayesopt/datatypes/hp_ranges.py index a58c92365ce2..6186d4e816b6 100644 --- a/autogluon/searcher/bayesopt/datatypes/hp_ranges.py +++ b/autogluon/searcher/bayesopt/datatypes/hp_ranges.py @@ -59,19 +59,16 @@ def scale_from_zero_one( assert 0.0 <= value <= 1.0, value if lower_bound == upper_bound: return lower_bound - else: - lower = scaling.to_internal(lower_bound) - upper = scaling.to_internal(upper_bound) - range = upper - lower - assert range > 0, (lower, upper) - internal_value = value * range + lower - hp = scaling.from_internal(internal_value) + lower = scaling.to_internal(lower_bound) + upper = scaling.to_internal(upper_bound) + range = upper - lower + assert range > 0, (lower, upper) + internal_value = value * range + lower + hp = scaling.from_internal(internal_value) # set value in case it is off due to numerical rounding - if hp < lower_bound: - hp = lower_bound - if hp > upper_bound: - hp = upper_bound - return hp + hp = max(hp, lower_bound) + hp = min(hp, upper_bound) + return hp class HyperparameterRangeContinuous(HyperparameterRange): @@ -131,18 +128,14 @@ def from_ndarray(self, ndarray: np.ndarray) -> float: self.upper_bound, self.scaling) def __repr__(self) -> str: - return "{}({}, {}, {}, {}, {}, {})".format( - self.__class__.__name__, repr(self.name), - repr(self.active_lower_bound), repr(self.active_upper_bound), - repr(self.scaling), repr(self.lower_bound), repr(self.upper_bound) - ) + return f"{self.__class__.__name__}({repr(self.name)}, {repr(self.active_lower_bound)}, {repr(self.active_upper_bound)}, {repr(self.scaling)}, {repr(self.lower_bound)}, {repr(self.upper_bound)})" def __eq__(self, other: object) -> bool: if isinstance(other, HyperparameterRangeContinuous): return self.name == other.name \ - and self.lower_bound == other.lower_bound \ - and self.upper_bound == other.upper_bound \ - and self.scaling == other.scaling + and self.lower_bound == other.lower_bound \ + and self.upper_bound == other.upper_bound \ + and self.scaling == other.scaling return False def from_zero_one(self, normalized_value: float) -> float: @@ -162,9 +155,9 @@ def __init__(self, name: str, lower_bound: int, upper_bound: int, scaling: Scali # reduce the range by epsilon at both ends, to avoid corner cases where numerical rounding # would cause a value that would end up out of range by one active_lower_bound = lower_bound if active_lower_bound is None \ - else active_lower_bound + else active_lower_bound active_upper_bound = upper_bound if active_upper_bound is None \ - else active_upper_bound + else active_upper_bound self.lower_bound = lower_bound self.upper_bound = upper_bound @@ -190,28 +183,23 @@ def from_ndarray(self, ndarray: np.ndarray) -> int: return result def __repr__(self) -> str: - return "{}({}, {}, {}, {}, {}, {})".format( - self.__class__.__name__, repr(self.name), - repr(self.active_lower_bound), repr(self.active_upper_bound), - repr(self.scaling), repr(self.lower_bound), repr(self.upper_bound) - ) + return f"{self.__class__.__name__}({repr(self.name)}, {repr(self.active_lower_bound)}, {repr(self.active_upper_bound)}, {repr(self.scaling)}, {repr(self.lower_bound)}, {repr(self.upper_bound)})" def __eq__(self, other): if isinstance(other, HyperparameterRangeInteger): return self.name == other.name \ - and self.lower_bound == other.lower_bound \ - and self.upper_bound == other.upper_bound \ - and self.active_lower_bound == other.active_lower_bound \ - and self.active_upper_bound == other.active_upper_bound \ - and self.scaling == other.scaling + and self.lower_bound == other.lower_bound \ + and self.upper_bound == other.upper_bound \ + and self.active_lower_bound == other.active_lower_bound \ + and self.active_upper_bound == other.active_upper_bound \ + and self.scaling == other.scaling return False def from_zero_one(self, normalized_value: float) -> Hyperparameter: scaling_result = scale_from_zero_one( normalized_value, self._continuous_range.active_lower_bound, self._continuous_range.active_upper_bound, self.scaling) - result = int(round(scaling_result)) - return result + return int(round(scaling_result)) class HyperparameterRangeCategorical(HyperparameterRange): @@ -233,7 +221,7 @@ def __init__(self, name: str, choices: Tuple[str, ...], assert set(self.choices).issuperset(self.active_choices) def to_ndarray(self, hp: str) -> np.ndarray: - assert hp in self.choices, "{} not in {}".format(hp, self) + assert hp in self.choices, f"{hp} not in {self}" idx = self.choices.index(hp) result = np.zeros(shape=(len(self.choices),)) result[idx] = 1.0 @@ -253,14 +241,12 @@ def from_zero_one(self, normalized_value: float) -> str: return self.active_choices[int(floor(normalized_value * len(self.active_choices)))] def __repr__(self) -> str: - return "{}({}, {})".format( - self.__class__.__name__, repr(self.name), repr(self.choices) - ) + return f"{self.__class__.__name__}({repr(self.name)}, {repr(self.choices)})" def __eq__(self, other) -> bool: if isinstance(other, HyperparameterRangeCategorical): return self.name == other.name \ - and self.choices == other.choices + and self.choices == other.choices return False @@ -342,7 +328,7 @@ def __init__(self, *args: HyperparameterRange): names = [hp_range.name for hp_range in args] if len(set(names)) != len(names): - raise ValueError("duplicate names in {}".format(names)) + raise ValueError(f"duplicate names in {names}") def to_ndarray(self, cand_tuple: Candidate) -> np.ndarray: pieces = [hp_range.to_ndarray(hp) for hp_range, hp in zip(self._hp_ranges, cand_tuple)] @@ -406,8 +392,7 @@ def refine_ndarray_bounds( bound_it = iter(bounds) for i, hp_range in enumerate(self._hp_ranges): if isinstance(hp_range, HyperparameterRangeCategorical): - for _ in range(len(hp_range.choices)): - new_bounds.append(next(bound_it)) + new_bounds.extend(next(bound_it) for _ in range(len(hp_range.choices))) else: low, high = next(bound_it) x = hp_range.to_ndarray(candidate[i]).item() @@ -429,9 +414,7 @@ def random_candidates(self, random_state, num_configs: int) -> \ for _ in range(num_configs)] def __repr__(self) -> str: - return "{}{}".format( - self.__class__.__name__, repr(self._hp_ranges) - ) + return f"{self.__class__.__name__}{repr(self._hp_ranges)}" def __eq__(self, other: object) -> bool: if isinstance(other, HyperparameterRanges_Impl): diff --git a/autogluon/searcher/bayesopt/datatypes/scaling.py b/autogluon/searcher/bayesopt/datatypes/scaling.py index 00d266075a11..c27face7315e 100644 --- a/autogluon/searcher/bayesopt/datatypes/scaling.py +++ b/autogluon/searcher/bayesopt/datatypes/scaling.py @@ -12,7 +12,7 @@ def from_internal(self, value: float) -> float: pass def __repr__(self): - return "{}()".format(self.__class__.__name__) + return f"{self.__class__.__name__}()" def __eq__(self, other): # For usage in tests. Make sure to edit if parameters are added. diff --git a/autogluon/searcher/bayesopt/gpmxnet/comparison_gpy.py b/autogluon/searcher/bayesopt/gpmxnet/comparison_gpy.py index bde4873ef8a8..478897d53c39 100644 --- a/autogluon/searcher/bayesopt/gpmxnet/comparison_gpy.py +++ b/autogluon/searcher/bayesopt/gpmxnet/comparison_gpy.py @@ -54,7 +54,7 @@ def evaluate(self, x1, x2): ssq = (x1 ** 2) + (x2 ** 2) scos = np.cos(c * x1) + np.cos(c * x2) return -a * np.exp(-b * np.sqrt(0.5 * ssq)) - np.exp(0.5 * scos) + \ - (a + np.exp(1)) + (a + np.exp(1)) def _decode_input(x, lim): @@ -64,9 +64,13 @@ def _decode_input(x, lim): def evaluate_blackbox(bb_func, inputs: np.ndarray) -> np.ndarray: num_dims = inputs.shape[1] - input_list = [] - for x, lim in zip(np.split(inputs, num_dims, axis=1), bb_func.search_space): - input_list.append(_decode_input(x, lim)) + input_list = [ + _decode_input(x, lim) + for x, lim in zip( + np.split(inputs, num_dims, axis=1), bb_func.search_space + ) + ] + return bb_func.evaluate(*input_list) @@ -111,7 +115,7 @@ def sample_data( # native ranges for candidate_evaluations def data_to_state(data: dict) -> TuningJobState: cs = CS.ConfigurationSpace() - cs_names = ['x{}'.format(i) for i in range(len(data['ss_limits']))] + cs_names = [f'x{i}' for i in range(len(data['ss_limits']))] cs.add_hyperparameters([ CSH.UniformFloatHyperparameter( name=name, lower=lims['min'], upper=lims['max']) @@ -168,8 +172,9 @@ def compare_gpy_predict_posterior_marginals( num_data = test_intermediates['features'].shape[0] num_dims = test_intermediates['features'].shape[1] lengthscales = [ - 1.0 / test_intermediates['inv_bw{}'.format(i)] - for i in range(num_dims)] + 1.0 / test_intermediates[f'inv_bw{i}'] for i in range(num_dims) + ] + kernel = GPy.kern.Matern52( num_dims, variance=test_intermediates['covariance_scale'], diff --git a/autogluon/searcher/bayesopt/gpmxnet/custom_op.py b/autogluon/searcher/bayesopt/gpmxnet/custom_op.py index 06f248507b45..4788694162a8 100644 --- a/autogluon/searcher/bayesopt/gpmxnet/custom_op.py +++ b/autogluon/searcher/bayesopt/gpmxnet/custom_op.py @@ -86,19 +86,20 @@ def forward(self, is_train, req, in_data, out_data, aux): must_increase_jitter = False except mx.base.MXNetError: if self._debug_log == 'true': - logger.info("sigsq = {} does not work".format( - sigsq_init.asscalar() + jitter)) + logger.info(f"sigsq = {sigsq_init.asscalar() + jitter} does not work") if jitter == 0.0: jitter = self._initial_jitter_factor * mx.nd.mean( mx.nd.diag(x)).asscalar() else: jitter *= self._jitter_growth - assert not must_increase_jitter,\ - "The jitter ({}) has reached its upperbound ({}) while the Cholesky of the input matrix still cannot be computed.".format(jitter, jitter_upperbound) + assert ( + not must_increase_jitter + ), f"The jitter ({jitter}) has reached its upperbound ({jitter_upperbound}) while the Cholesky of the input matrix still cannot be computed." + if self._debug_log == 'true': _sigsq_init = sigsq_init.asscalar() - logger.info("sigsq_final = {}".format(_sigsq_init + jitter)) + logger.info(f"sigsq_final = {_sigsq_init + jitter}") self.assign(out_data[0], req[0], x_plus_constant) def backward(self, req, out_grad, in_data, out_data, in_grad, aux): diff --git a/autogluon/searcher/bayesopt/gpmxnet/debug_gp_regression.py b/autogluon/searcher/bayesopt/gpmxnet/debug_gp_regression.py index 254ac19cdf08..9b3bc83fc9f7 100644 --- a/autogluon/searcher/bayesopt/gpmxnet/debug_gp_regression.py +++ b/autogluon/searcher/bayesopt/gpmxnet/debug_gp_regression.py @@ -32,16 +32,16 @@ def store_args(self, params, X, Y, param_encoding_pairs): for param in params: arg_dict[param.name] = param.data(ctx=mx.cpu()) fname = self._filename() - mx.nd.save(fname + '_args.nd', arg_dict) - with open(fname + '_args.txt', 'w') as f: + mx.nd.save(f'{fname}_args.nd', arg_dict) + with open(f'{fname}_args.txt', 'w') as f: self._write_meta(f) for param, encoding in param_encoding_pairs: f.write(param_to_pretty_string(param, encoding) + '\n') def store_value(self, value): fname = self._filename() - with open(fname + '_value.txt', 'w') as f: - f.write('value = {}\n'.format(value)) + with open(f'{fname}_value.txt', 'w') as f: + f.write(f'value = {value}\n') self._write_meta(f) # Advance counters self.global_counter += 1 @@ -51,7 +51,7 @@ def _filename(self): return self.fname_msk.format(self.global_counter % self.rolling_size) def _write_meta(self, f): - f.write('optim_counter = {}\n'.format(self.optim_counter)) - f.write('local_counter = {}\n'.format(self.local_counter)) - f.write('global_counter = {}\n'.format(self.global_counter)) - f.write('time = {}\n'.format(time.time())) + f.write(f'optim_counter = {self.optim_counter}\n') + f.write(f'local_counter = {self.local_counter}\n') + f.write(f'global_counter = {self.global_counter}\n') + f.write(f'time = {time.time()}\n') diff --git a/autogluon/searcher/bayesopt/gpmxnet/distribution.py b/autogluon/searcher/bayesopt/gpmxnet/distribution.py index 939d0f5591d4..5e5f34b4d951 100644 --- a/autogluon/searcher/bayesopt/gpmxnet/distribution.py +++ b/autogluon/searcher/bayesopt/gpmxnet/distribution.py @@ -43,8 +43,9 @@ def __init__(self, mean, alpha): @staticmethod def _assert_positive_number(x, name): - assert isinstance(x, numbers.Real) and x > 0.0, \ - "{} = {}, must be positive number".format(name, x) + assert ( + isinstance(x, numbers.Real) and x > 0.0 + ), f"{name} = {x}, must be positive number" def negative_log_density(self, F, x): x_safe = F.maximum(x, MIN_POSTERIOR_VARIANCE) diff --git a/autogluon/searcher/bayesopt/gpmxnet/gluon_blocks_helpers.py b/autogluon/searcher/bayesopt/gpmxnet/gluon_blocks_helpers.py index e6efa1b12b6e..87ccf742471e 100644 --- a/autogluon/searcher/bayesopt/gpmxnet/gluon_blocks_helpers.py +++ b/autogluon/searcher/bayesopt/gpmxnet/gluon_blocks_helpers.py @@ -44,9 +44,11 @@ def __init__(self, param_name, encoding, size_cols, **kwargs): init_val_int = encoding.init_val_int # Note: The initialization values are bogus! self.param_internal = self.params.get( - param_name + '_internal', + f'{param_name}_internal', init=mx.init.Constant(init_val_int), - shape=(1,), dtype=DATA_TYPE) + shape=(1,), + dtype=DATA_TYPE, + ) def hybrid_forward(self, F, features, param_internal): """Returns constant positive vector @@ -81,7 +83,7 @@ def get_box_constraints_internal(self): self.param_internal) def log_parameters(self): - return '{} = {}'.format(self.param_name, self.get()) + return f'{self.param_name} = {self.get()}' def get_parameters(self): return {self.param_name: self.get()} @@ -207,18 +209,15 @@ def _check_or_set_init_val(init_val, constr_lower, constr_upper): assert init_val <= constr_upper if constr_lower is not None: assert constr_lower <= init_val + elif constr_lower is None: + init_val = 0.9 * constr_upper if constr_upper is not None else 1.0 else: - # Just pick some value which is feasible - if constr_lower is not None: - if constr_upper is not None: - init_val = 0.5 * (constr_upper + constr_lower) - else: - init_val = 1.1 * constr_lower - else: - if constr_upper is not None: - init_val = 0.9 * constr_upper - else: - init_val = 1.0 + init_val = ( + 0.5 * (constr_upper + constr_lower) + if constr_upper is not None + else 1.1 * constr_lower + ) + return init_val @@ -259,9 +258,7 @@ def get(self, F, param_internal): param_internal, act_type='softrelu') + self.lower def decode(self, val, name): - assert val > self.lower, \ - '{} = {} must be > self.lower = {}'.format( - name, val, self.lower) + assert val > self.lower, f'{name} = {val} must be > self.lower = {self.lower}' # Inverse of encoding: Careful with numerics: # val_int = log(exp(arg) - 1) = arg + log(1 - exp(-arg)) # = arg + log1p(-exp(-arg)) @@ -317,8 +314,7 @@ def get(self, F, param_internal): return F.exp(param_internal) def decode(self, val, name): - assert val > 0.0, \ - '{} = {} must be positive'.format(name, val) + assert val > 0.0, f'{name} = {val} must be positive' return np.log(np.maximum(val, 1e-15)) @@ -327,12 +323,10 @@ def decode(self, val, name): def unwrap_parameter(F, param_internal, some_arg=None): assert isinstance(param_internal, mx.gluon.Parameter) - if mxnet_is_ndarray(F): - ctx = some_arg.context if some_arg is not None else mx.cpu() - val = param_internal.data(ctx=ctx) - else: - val = param_internal.var() - return val + if not mxnet_is_ndarray(F): + return param_internal.var() + ctx = some_arg.context if some_arg is not None else mx.cpu() + return param_internal.data(ctx=ctx) def encode_unwrap_parameter(F, param_internal, encoding, some_arg=None): diff --git a/autogluon/searcher/bayesopt/gpmxnet/gp_model.py b/autogluon/searcher/bayesopt/gpmxnet/gp_model.py index a2592b976b71..1c81302a808c 100644 --- a/autogluon/searcher/bayesopt/gpmxnet/gp_model.py +++ b/autogluon/searcher/bayesopt/gpmxnet/gp_model.py @@ -65,11 +65,12 @@ def predict(self, X_test): def _assert_check_xtest(self, X_test): assert self.states is not None, \ - "Posterior state does not exist (run 'fit')" + "Posterior state does not exist (run 'fit')" X_test = self._check_and_format_input(X_test) - assert X_test.shape[1] == self.states[0].num_features, \ - "X_test and X_train should have the same number of columns (received {}, expected {})".format( - X_test.shape[1], self.states[0].num_features) + assert ( + X_test.shape[1] == self.states[0].num_features + ), f"X_test and X_train should have the same number of columns (received {X_test.shape[1]}, expected {self.states[0].num_features})" + return X_test def multiple_targets(self): diff --git a/autogluon/searcher/bayesopt/gpmxnet/gp_regression.py b/autogluon/searcher/bayesopt/gpmxnet/gp_regression.py index 93d16fe1fe37..0fc3e4147cb0 100644 --- a/autogluon/searcher/bayesopt/gpmxnet/gp_regression.py +++ b/autogluon/searcher/bayesopt/gpmxnet/gp_regression.py @@ -145,11 +145,12 @@ def fit(self, X, Y, profiler: GPMXNetSimpleProfiler = None): X = self._check_and_format_input(X) Y = self._check_and_format_input(Y) - assert X.shape[0] == Y.shape[0], \ - "X and Y should have the same number of points (received {} and {})".format( - X.shape[0], Y.shape[0]) + assert ( + X.shape[0] == Y.shape[0] + ), f"X and Y should have the same number of points (received {X.shape[0]} and {Y.shape[0]})" + assert Y.shape[1] == 1, \ - "Y cannot be a matrix if parameters are to be fit" + "Y cannot be a matrix if parameters are to be fit" if self.fit_reset_params: self.reset_params() @@ -172,15 +173,16 @@ def fit(self, X, Y, profiler: GPMXNetSimpleProfiler = None): # Logging in response to failures of optimization runs n_succeeded = sum(x is None for x in ret_infos) if n_succeeded < n_starts: - log_msg = "[GaussianProcessRegression.fit]\n" - log_msg += ("{} of the {} restarts failed with the following exceptions:\n".format( - n_starts - n_succeeded, n_starts)) + log_msg = ( + "[GaussianProcessRegression.fit]\n" + + f"{n_starts - n_succeeded} of the {n_starts} restarts failed with the following exceptions:\n" + ) + for i, ret_info in enumerate(ret_infos): if ret_info is not None: - log_msg += ("- Restart {}: Exception {}\n".format( - i, ret_info['type'])) - log_msg += (" Message: {}\n".format(ret_info['msg'])) - log_msg += (" Args: {}\n".format(ret_info['args'])) + log_msg += f"- Restart {i}: Exception {ret_info['type']}\n" + log_msg += f" Message: {ret_info['msg']}\n" + log_msg += f" Args: {ret_info['args']}\n" logger.info(log_msg) if n_succeeded == 0: logger.info("All restarts failed: Skipping hyperparameter fitting for now") @@ -194,9 +196,10 @@ def recompute_states(self, X, Y, profiler: GPMXNetSimpleProfiler = None): """ X = self._check_and_format_input(X) Y = self._check_and_format_input(Y) - assert X.shape[0] == Y.shape[0], \ - "X and Y should have the same number of points (received {} and {})".format( - X.shape[0], Y.shape[0]) + assert ( + X.shape[0] == Y.shape[0] + ), f"X and Y should have the same number of points (received {X.shape[0]} and {Y.shape[0]})" + assert self._states is not None, self._states self._recompute_states(X, Y, profiler=profiler) diff --git a/autogluon/searcher/bayesopt/gpmxnet/gpr_mcmc.py b/autogluon/searcher/bayesopt/gpmxnet/gpr_mcmc.py index 7115f73afbbb..d056fce9d01b 100644 --- a/autogluon/searcher/bayesopt/gpmxnet/gpr_mcmc.py +++ b/autogluon/searcher/bayesopt/gpmxnet/gpr_mcmc.py @@ -87,10 +87,10 @@ def _is_feasible(self, hp_values: np.ndarray) -> bool: dim = encoding.dimension if lower is not None or upper is not None: values = hp_values[pos:(pos + dim)] - if (lower is not None) and any(values < lower): - return False - if (upper is not None) and any(values > upper): - return False + if (lower is not None) and any(values < lower): + return False + if (upper is not None) and any(values > upper): + return False pos += dim return True @@ -108,10 +108,11 @@ def _create_posterior_states(self, samples, X, Y): def _get_gp_hps(likelihood: MarginalLikelihood) -> np.ndarray: """Get GP hyper-parameters as numpy array for a given likelihood object.""" - hp_values = [] - for param_int, encoding in likelihood.param_encoding_pairs(): - hp_values.append( - encode_unwrap_parameter(mx.nd, param_int, encoding).asnumpy()) + hp_values = [ + encode_unwrap_parameter(mx.nd, param_int, encoding).asnumpy() + for param_int, encoding in likelihood.param_encoding_pairs() + ] + return np.concatenate(hp_values) diff --git a/autogluon/searcher/bayesopt/gpmxnet/kernel/base.py b/autogluon/searcher/bayesopt/gpmxnet/kernel/base.py index 0f9f0e4bae9c..ca1873b08f51 100644 --- a/autogluon/searcher/bayesopt/gpmxnet/kernel/base.py +++ b/autogluon/searcher/bayesopt/gpmxnet/kernel/base.py @@ -79,7 +79,7 @@ def __init__(self, dimension, ARD=False, encoding_type=DEFAULT_ENCODING, **kwarg super(SquaredDistance, self).__init__(**kwargs) self.ARD = ARD - inverse_bandwidths_dimension = 1 if not ARD else dimension + inverse_bandwidths_dimension = dimension if ARD else 1 self.encoding = create_encoding( encoding_type, INITIAL_INVERSE_BANDWIDTHS, INVERSE_BANDWIDTHS_LOWER_BOUND, INVERSE_BANDWIDTHS_UPPER_BOUND, @@ -138,18 +138,18 @@ def get_params(self): return {'inv_bw': inverse_bandwidths[0]} else: return { - 'inv_bw{}'.format(k): inverse_bandwidths[k] - for k in range(inverse_bandwidths.size)} + f'inv_bw{k}': inverse_bandwidths[k] + for k in range(inverse_bandwidths.size) + } def set_params(self, param_dict): dimension = self.encoding.dimension if dimension == 1: inverse_bandwidths = [param_dict['inv_bw']] else: - keys = ['inv_bw{}'.format(k) for k in range(dimension)] + keys = [f'inv_bw{k}' for k in range(dimension)] for k in keys: - assert k in param_dict, \ - "'{}' not in param_dict = {}".format(k, param_dict) + assert k in param_dict, f"'{k}' not in param_dict = {param_dict}" inverse_bandwidths = [param_dict[k] for k in keys] self.encoding.set(self.inverse_bandwidths_internal, inverse_bandwidths) @@ -203,10 +203,9 @@ def hybrid_forward(self, F, X1, X2, covariance_scale_internal): # (non-differentiability) # that's why we add NUMERICAL_JITTER B = F.sqrt(5.0 * D + NUMERICAL_JITTER) - K = F.broadcast_mul( - (1.0 + B + 5.0 / 3.0 * D) * F.exp(-B), covariance_scale) - - return K + return F.broadcast_mul( + (1.0 + B + 5.0 / 3.0 * D) * F.exp(-B), covariance_scale + ) def _covariance_scale(self, F, X): return encode_unwrap_parameter( diff --git a/autogluon/searcher/bayesopt/gpmxnet/kernel/exponential_decay.py b/autogluon/searcher/bayesopt/gpmxnet/kernel/exponential_decay.py index 5f3aa5fde74d..8eda1a738a71 100644 --- a/autogluon/searcher/bayesopt/gpmxnet/kernel/exponential_decay.py +++ b/autogluon/searcher/bayesopt/gpmxnet/kernel/exponential_decay.py @@ -95,9 +95,10 @@ def __init__( self.encoding_delta = IdentityScalarEncoding( constr_lower=0.0, constr_upper=1.0, init_val=delta_init) else: - assert 0.0 <= delta_fixed_value <= 1.0, \ - "delta_fixed_value = {}, must lie in [0, 1]".format( - delta_fixed_value) + assert ( + 0.0 <= delta_fixed_value <= 1.0 + ), f"delta_fixed_value = {delta_fixed_value}, must lie in [0, 1]" + self.encoding_delta = None self.delta_fixed_value = delta_fixed_value @@ -253,8 +254,8 @@ def get_params(self): keys.pop() result = {k: v.reshape((1,)).asscalar() for k, v in zip(keys, values)} for pref, func in [('kernelx_', self.kernel_x), ('meanx_', self.mean_x)]: - result.update({ - (pref + k): v for k, v in func.get_params().items()}) + result |= {(pref + k): v for k, v in func.get_params().items()} + return result def set_params(self, param_dict): @@ -286,7 +287,7 @@ def param_encoding_pairs(self): return [] def get_params(self): - return dict() + return {} def set_params(self, param_dict): pass diff --git a/autogluon/searcher/bayesopt/gpmxnet/kernel/fabolas.py b/autogluon/searcher/bayesopt/gpmxnet/kernel/fabolas.py index a2df57d69fe8..17f9000c099c 100644 --- a/autogluon/searcher/bayesopt/gpmxnet/kernel/fabolas.py +++ b/autogluon/searcher/bayesopt/gpmxnet/kernel/fabolas.py @@ -65,10 +65,9 @@ def hybrid_forward(self, F, X1, X2, u1_internal, u2_internal, u3_internal): mat1 = self._compute_factor(F, X1, u1, u2, u3) if X2 is X1: return F.linalg.syrk(mat1, transpose=False) - else: - X2 = self._check_input_shape(F, X2) - mat2 = self._compute_factor(F, X2, u1, u2, u3) - return F.dot(mat1, mat2, transpose_a=False, transpose_b=True) + X2 = self._check_input_shape(F, X2) + mat2 = self._compute_factor(F, X2, u1, u2, u3) + return F.dot(mat1, mat2, transpose_a=False, transpose_b=True) def _get_pars(self, F, X): u1 = encode_unwrap_parameter(F, self.u1_internal, self.encoding_u12, X) diff --git a/autogluon/searcher/bayesopt/gpmxnet/kernel/product_kernel.py b/autogluon/searcher/bayesopt/gpmxnet/kernel/product_kernel.py index 6b2b093c8c99..3378dac81f6b 100644 --- a/autogluon/searcher/bayesopt/gpmxnet/kernel/product_kernel.py +++ b/autogluon/searcher/bayesopt/gpmxnet/kernel/product_kernel.py @@ -70,15 +70,15 @@ def param_encoding_pairs(self): self.kernel2.param_encoding_pairs() def get_params(self): - result = dict() - prefs = [k + '_' for k in self.name_prefixes] + result = {} + prefs = [f'{k}_' for k in self.name_prefixes] for pref, kernel in zip(prefs, [self.kernel1, self.kernel2]): - result.update({ - (pref + k): v for k, v in kernel.get_params().items()}) + result |= {(pref + k): v for k, v in kernel.get_params().items()} + return result def set_params(self, param_dict): - prefs = [k + '_' for k in self.name_prefixes] + prefs = [f'{k}_' for k in self.name_prefixes] for pref, kernel in zip(prefs, [self.kernel1, self.kernel2]): len_pref = len(pref) stripped_dict = { diff --git a/autogluon/searcher/bayesopt/gpmxnet/likelihood.py b/autogluon/searcher/bayesopt/gpmxnet/likelihood.py index 3ca1bb0abaf6..d5ba6e77606c 100644 --- a/autogluon/searcher/bayesopt/gpmxnet/likelihood.py +++ b/autogluon/searcher/bayesopt/gpmxnet/likelihood.py @@ -83,9 +83,11 @@ def box_constraints_internal(self): all_box_constraints = {} for param, encoding in self.param_encoding_pairs(): - assert encoding is not None, \ - "encoding of param {} should not be None".format(param.name) - all_box_constraints.update(encoding.box_constraints_internal(param)) + assert ( + encoding is not None + ), f"encoding of param {param.name} should not be None" + + all_box_constraints |= encoding.box_constraints_internal(param) return all_box_constraints def get_noise_variance(self, as_ndarray=False): @@ -99,8 +101,8 @@ def set_noise_variance(self, val): def get_params(self): result = {'noise_variance': self.get_noise_variance()} for pref, func in [('kernel_', self.kernel), ('mean_', self.mean)]: - result.update({ - (pref + k): v for k, v in func.get_params().items()}) + result |= {(pref + k): v for k, v in func.get_params().items()} + return result def set_params(self, param_dict): diff --git a/autogluon/searcher/bayesopt/gpmxnet/mean.py b/autogluon/searcher/bayesopt/gpmxnet/mean.py index e57f7c59f956..a564570edfa6 100644 --- a/autogluon/searcher/bayesopt/gpmxnet/mean.py +++ b/autogluon/searcher/bayesopt/gpmxnet/mean.py @@ -117,7 +117,7 @@ def param_encoding_pairs(self): return [] def get_params(self): - return dict() + return {} def set_params(self, param_dict): pass diff --git a/autogluon/searcher/bayesopt/gpmxnet/optimization_utils.py b/autogluon/searcher/bayesopt/gpmxnet/optimization_utils.py index cfc841b40de9..62b7b7807801 100644 --- a/autogluon/searcher/bayesopt/gpmxnet/optimization_utils.py +++ b/autogluon/searcher/bayesopt/gpmxnet/optimization_utils.py @@ -135,7 +135,7 @@ def _apply_lbfgs_internal( # Run L-BFGS-B LBFGS_tol = kwargs.get("tol", default_LBFGS_tol) LBFGS_maxiter = kwargs.get("maxiter", default_LBFGS_maxiter) - LBFGS_callback = kwargs.get("callback", None) + LBFGS_callback = kwargs.get("callback") ret_info = None try: output = optimize.minimize(mx_objective, @@ -194,10 +194,7 @@ def _deep_copy_arg_dict(input_arg_dict): :param input_arg_dict: :return: deep copy of input_arg_dict """ - output_arg_dict = {} - for name, param in input_arg_dict.items(): - output_arg_dict[name] = param.copy() - return output_arg_dict + return {name: param.copy() for name, param in input_arg_dict.items()} def _inplace_arg_dict_randomization(arg_dict, mean_arg_dict, bounds, std=STARTING_POINT_RANDOMIZATION_STD): diff --git a/autogluon/searcher/bayesopt/gpmxnet/poster_state_incremental.py b/autogluon/searcher/bayesopt/gpmxnet/poster_state_incremental.py index 56f33fbb6e37..c54a50aab291 100644 --- a/autogluon/searcher/bayesopt/gpmxnet/poster_state_incremental.py +++ b/autogluon/searcher/bayesopt/gpmxnet/poster_state_incremental.py @@ -41,7 +41,7 @@ class GPPosteriorStateIncrementalUpdater(object): def __init__(self, mean: MeanFunction, kernel: KernelFunction): self.mean = mean self.kernel = kernel - self._executors = dict() + self._executors = {} self.last_recent_exec_n = None def sample_and_update( @@ -95,9 +95,10 @@ def reset(self): def _get_executor(self, args): n = args['features'].shape[0] # Sanity check - assert self.last_recent_exec_n is None or n > self.last_recent_exec_n, \ - "Updater is not used in sequence in a single simulation thread [n = {}, last_recent_exec_n = {}]".format( - n, self.last_recent_exec_n) + assert ( + self.last_recent_exec_n is None or n > self.last_recent_exec_n + ), f"Updater is not used in sequence in a single simulation thread [n = {n}, last_recent_exec_n = {self.last_recent_exec_n}]" + if n not in self._executors: # Bind executor to sample_and_cholesky_update ex_args = { diff --git a/autogluon/searcher/bayesopt/gpmxnet/posterior_state.py b/autogluon/searcher/bayesopt/gpmxnet/posterior_state.py index 97ed0bf660cb..3089fbe2b3a2 100644 --- a/autogluon/searcher/bayesopt/gpmxnet/posterior_state.py +++ b/autogluon/searcher/bayesopt/gpmxnet/posterior_state.py @@ -132,8 +132,7 @@ def __init__( self.noise_variance = noise_variance if self.F == mx.sym \ else noise_variance.copy() - def update(self, feature: Tensor, target: Tensor) -> \ - 'IncrementalUpdateGPPosteriorState': + def update(self, feature: Tensor, target: Tensor) -> 'IncrementalUpdateGPPosteriorState': """ :param feature: Additional input xstar, shape (1, d) :param target: Additional target ystar, shape (1, m) @@ -144,29 +143,29 @@ def update(self, feature: Tensor, target: Tensor) -> \ feature = F.reshape(feature, shape=(1, -1)) target = F.reshape(target, shape=(1, -1)) if self.is_ndarray(): - assert feature.shape[1] == self.features.shape[1], \ - "feature.shape[1] = {} != {} = self.features.shape[1]".format( - feature.shape[1], self.features.shape[1]) - assert target.shape[1] == self.pred_mat.shape[1], \ - "target.shape[1] = {} != {} = self.pred_mat.shape[1]".format( - target.shape[1], self.pred_mat.shape[1]) + assert ( + feature.shape[1] == self.features.shape[1] + ), f"feature.shape[1] = {feature.shape[1]} != {self.features.shape[1]} = self.features.shape[1]" + + assert ( + target.shape[1] == self.pred_mat.shape[1] + ), f"target.shape[1] = {target.shape[1]} != {self.pred_mat.shape[1]} = self.pred_mat.shape[1]" + chol_fact_new, pred_mat_new = cholesky_update( F, self.features, self.chol_fact, self.pred_mat, self.mean, self.kernel, self.noise_variance, feature, target) features_new = F.concat(self.features, feature, dim=0) - # Create object by calling internal constructor - state_new = IncrementalUpdateGPPosteriorState( + return IncrementalUpdateGPPosteriorState( features=features_new, targets=None, mean=self.mean, kernel=self.kernel, noise_variance=self.noise_variance, chol_fact=chol_fact_new, - pred_mat=pred_mat_new) - return state_new + pred_mat=pred_mat_new, + ) - def expand_fantasies(self, num_fantasies: int) -> \ - 'IncrementalUpdateGPPosteriorState': + def expand_fantasies(self, num_fantasies: int) -> 'IncrementalUpdateGPPosteriorState': """ If this posterior has been created with a single targets vector, shape (n, 1), use this to duplicate this vector m = num_fantasies @@ -180,14 +179,14 @@ def expand_fantasies(self, num_fantasies: int) -> \ F = self.F if self.is_ndarray(): assert self.pred_mat.shape[1] == 1, \ - "Method requires posterior without fantasy samples" + "Method requires posterior without fantasy samples" pred_mat_new = F.concat(*([self.pred_mat] * num_fantasies), dim=1) - state_new = IncrementalUpdateGPPosteriorState( + return IncrementalUpdateGPPosteriorState( features=self.features, targets=None, mean=self.mean, kernel=self.kernel, noise_variance=self.noise_variance, chol_fact=self.chol_fact, - pred_mat=pred_mat_new) - return state_new + pred_mat=pred_mat_new, + ) diff --git a/autogluon/searcher/bayesopt/gpmxnet/posterior_utils.py b/autogluon/searcher/bayesopt/gpmxnet/posterior_utils.py index b1f1d39d3977..72e4ad1200f5 100644 --- a/autogluon/searcher/bayesopt/gpmxnet/posterior_utils.py +++ b/autogluon/searcher/bayesopt/gpmxnet/posterior_utils.py @@ -13,10 +13,7 @@ def mxnet_F(x: Tensor): - if isinstance(x, mx.nd.NDArray): - return mx.nd - else: - return mx.sym + return mx.nd if isinstance(x, mx.nd.NDArray) else mx.sym def mxnet_is_ndarray(F): diff --git a/autogluon/searcher/bayesopt/gpmxnet/slice.py b/autogluon/searcher/bayesopt/gpmxnet/slice.py index 0ea9262d96cd..091926155f44 100644 --- a/autogluon/searcher/bayesopt/gpmxnet/slice.py +++ b/autogluon/searcher/bayesopt/gpmxnet/slice.py @@ -73,7 +73,8 @@ def slice_sampler_step_in(lower_bound: float, upper_bound: float, log_pivot: flo raise SliceException("The interval for slice sampling has reduced to zero in step in") if sliced_log_density(movement) > log_pivot: return movement - else: - lower_bound = movement if movement < 0.0 else lower_bound - upper_bound = movement if movement > 0.0 else upper_bound - raise SliceException("Reach maximum iteration ({}) while stepping in".format(MAX_STEP_LOOP)) + lower_bound = movement if movement < 0.0 else lower_bound + upper_bound = movement if movement > 0.0 else upper_bound + raise SliceException( + f"Reach maximum iteration ({MAX_STEP_LOOP}) while stepping in" + ) diff --git a/autogluon/searcher/bayesopt/gpmxnet/utils.py b/autogluon/searcher/bayesopt/gpmxnet/utils.py index c2d18adc40ce..34b917b0d195 100644 --- a/autogluon/searcher/bayesopt/gpmxnet/utils.py +++ b/autogluon/searcher/bayesopt/gpmxnet/utils.py @@ -16,8 +16,10 @@ def param_to_pretty_string(gluon_param, encoding): """ assert isinstance(gluon_param, gluon.Parameter) - assert encoding is not None, \ - "encoding of param {} should not be None".format(gluon_param.name) + assert ( + encoding is not None + ), f"encoding of param {gluon_param.name} should not be None" + param_as_numpy = encode_unwrap_parameter( mx.nd, gluon_param, encoding).asnumpy() diff --git a/autogluon/searcher/bayesopt/gpmxnet/warping.py b/autogluon/searcher/bayesopt/gpmxnet/warping.py index 90ceeadd5647..5b95ce9890fd 100644 --- a/autogluon/searcher/bayesopt/gpmxnet/warping.py +++ b/autogluon/searcher/bayesopt/gpmxnet/warping.py @@ -127,14 +127,14 @@ def __init__(self, dimension, index_to_range, encoding_type=DEFAULT_ENCODING, # and managed by Warping, we register it as a child. self.register_child(transformation, name=transformation.name) self._params_encoding_pairs += \ - transformation.param_encoding_pairs() + transformation.param_encoding_pairs() some_are_warped = True else: # if a column is not warped, we do not apply any transformation transformation = lambda x: x self.transformations.append(transformation) assert some_are_warped, \ - "At least one of the dimensions must be warped" + "At least one of the dimensions must be warped" def hybrid_forward(self, F, X): """ @@ -167,7 +167,7 @@ def get_params(self): if len(self.transformations) == 1: result = self.transformations[0].get_params() else: - result = dict() + result = {} for i, warping in enumerate(self.transformations): if isinstance(warping, OneDimensionalWarping): istr = str(i) @@ -185,9 +185,7 @@ def set_params(self, param_dict): if transf_keys is None: transf_keys = warping.get_params().keys() istr = str(i) - stripped_dict = dict() - for k in transf_keys: - stripped_dict[k] = param_dict[k + istr] + stripped_dict = {k: param_dict[k + istr] for k in transf_keys} warping.set_params(stripped_dict) @@ -216,10 +214,7 @@ def hybrid_forward(self, F, X1, X2): """ warped_X1 = self.warping(X1) - if X1 is X2: - warped_X2 = warped_X1 - else: - warped_X2 = self.warping(X2) + warped_X2 = warped_X1 if X1 is X2 else self.warping(X2) return self.kernel(warped_X1, warped_X2) def diagonal(self, F, X): diff --git a/autogluon/searcher/bayesopt/models/gpmxnet.py b/autogluon/searcher/bayesopt/models/gpmxnet.py index 936c581ad12e..b174bc3d954b 100644 --- a/autogluon/searcher/bayesopt/models/gpmxnet.py +++ b/autogluon/searcher/bayesopt/models/gpmxnet.py @@ -73,9 +73,10 @@ def get_internal_candidate_evaluations( assert isinstance(pending_eval, FantasizedPendingEvaluation), \ "state.pending_evaluations has to contain FantasizedPendingEvaluation" fantasies = pending_eval.fantasies[active_metric] - assert fantasies.size == num_fantasize_samples, \ - "All state.pending_evaluations entries must have length {}".format( - num_fantasize_samples) + assert ( + fantasies.size == num_fantasize_samples + ), f"All state.pending_evaluations entries must have length {num_fantasize_samples}" + fanta_lst.append(fantasies.reshape((1, -1))) cand_lst.append(state.hp_ranges.to_ndarray(pending_eval.candidate)) y = np.vstack([y * np.ones((1, num_fantasize_samples))] + fanta_lst) @@ -114,10 +115,9 @@ def default_gpmodel_mcmc( ) -def dimensionality_and_warping_ranges(hp_ranges: HyperparameterRanges) -> \ - Tuple[int, Dict[int, Tuple[float, float]]]: +def dimensionality_and_warping_ranges(hp_ranges: HyperparameterRanges) -> Tuple[int, Dict[int, Tuple[float, float]]]: dims = 0 - warping_ranges = dict() + warping_ranges = {} # NOTE: This explicit loop over hp_ranges will fail if # HyperparameterRanges.hp_ranges is not implemented! Needs to be fixed if # it becomes an issue, either by moving the functionality here into @@ -228,10 +228,7 @@ def get_params(self): :return: Hyperparameter dictionary """ - if not self.does_mcmc(): - return self._gpmodel.get_params() - else: - return dict() + return {} if self.does_mcmc() else self._gpmodel.get_params() def set_params(self, param_dict): self._gpmodel.set_params(param_dict) @@ -293,7 +290,7 @@ def _posterior_for_state( """ assert state.candidate_evaluations, \ - "Cannot compute posterior: state has no labeled datapoints" + "Cannot compute posterior: state has no labeled datapoints" internal_candidate_evaluations = get_internal_candidate_evaluations( state, self.active_metric, self.normalize_targets, self.num_fantasy_samples) @@ -313,9 +310,12 @@ def _posterior_for_state( if self._debug_log is not None: self._debug_log.set_gp_params(self.get_params()) if not state.pending_evaluations: - deb_msg = "[GPMXNetModel._posterior_for_state]\n" - deb_msg += ("- self.mean = {}\n".format(self.mean)) - deb_msg += ("- self.std = {}".format(self.std)) + deb_msg = ( + "[GPMXNetModel._posterior_for_state]\n" + + f"- self.mean = {self.mean}\n" + ) + + deb_msg += f"- self.std = {self.std}" logger.info(deb_msg) self._debug_log.set_targets(internal_candidate_evaluations.y) else: @@ -323,8 +323,7 @@ def _posterior_for_state( fantasies = internal_candidate_evaluations.y[-num_pending:, :] self._debug_log.set_fantasies(fantasies) - def _draw_fantasy_values(self, candidates: List[Candidate]) \ - -> List[FantasizedPendingEvaluation]: + def _draw_fantasy_values(self, candidates: List[Candidate]) -> List[FantasizedPendingEvaluation]: """ Note: The fantasy values need not be de-normalized, because they are only used internally here (e.g., get_internal_candidate_evaluations). @@ -337,38 +336,37 @@ def _draw_fantasy_values(self, candidates: List[Candidate]) \ In this case, we draw num_fantasy_samples i.i.d. """ - if candidates: - logger.debug("Fantasizing target values for candidates:\n{}" - .format(candidates)) - X_new = self.state.hp_ranges.to_ndarray_matrix(candidates) - X_new_mx = self.convert_np_to_nd(X_new) - # Special case (see header comment): If the current posterior state - # does not contain pending candidates (no fantasies), we sample - # num_fantasy_samples times i.i.d. - num_samples = 1 if self._gpmodel.multiple_targets() \ + if not candidates: + return [] + logger.debug(f"Fantasizing target values for candidates:\n{candidates}") + X_new = self.state.hp_ranges.to_ndarray_matrix(candidates) + X_new_mx = self.convert_np_to_nd(X_new) + # Special case (see header comment): If the current posterior state + # does not contain pending candidates (no fantasies), we sample + # num_fantasy_samples times i.i.d. + num_samples = 1 if self._gpmodel.multiple_targets() \ else self.num_fantasy_samples - # We need joint sampling for >1 new candidates - num_candidates = len(candidates) - sample_func = self._gpmodel.sample_joint if num_candidates > 1 else \ + # We need joint sampling for >1 new candidates + num_candidates = len(candidates) + sample_func = self._gpmodel.sample_joint if num_candidates > 1 else \ self._gpmodel.sample_marginals - Y_new_mx = sample_func(X_new_mx, num_samples=num_samples) - Y_new = Y_new_mx.asnumpy().astype(X_new.dtype, copy=False).reshape( - (num_candidates, -1)) - return [ - FantasizedPendingEvaluation( - candidate, {self.active_metric: y_new.reshape((1, -1))}) - for candidate, y_new in zip(candidates, Y_new) - ] - else: - return [] + Y_new_mx = sample_func(X_new_mx, num_samples=num_samples) + Y_new = Y_new_mx.asnumpy().astype(X_new.dtype, copy=False).reshape( + (num_candidates, -1)) + return [ + FantasizedPendingEvaluation( + candidate, {self.active_metric: y_new.reshape((1, -1))}) + for candidate, y_new in zip(candidates, Y_new) + ] def _current_best_filter_candidates(self, candidates): hp_ranges = self.state.hp_ranges if isinstance(hp_ranges, HyperparameterRanges_CS): candidates = hp_ranges.filter_for_last_pos_value(candidates) - assert candidates, \ - "state.hp_ranges does not contain any candidates " + \ - "(labeled or pending) with resource attribute " + \ - "'{}' = {}".format( - hp_ranges.name_last_pos, hp_ranges.value_for_last_pos) + assert candidates, ( + "state.hp_ranges does not contain any candidates " + + "(labeled or pending) with resource attribute " + + f"'{hp_ranges.name_last_pos}' = {hp_ranges.value_for_last_pos}" + ) + return candidates diff --git a/autogluon/searcher/bayesopt/models/gpmxnet_skipopt.py b/autogluon/searcher/bayesopt/models/gpmxnet_skipopt.py index cb5a6e0ebdd9..c20865933bc8 100644 --- a/autogluon/searcher/bayesopt/models/gpmxnet_skipopt.py +++ b/autogluon/searcher/bayesopt/models/gpmxnet_skipopt.py @@ -67,10 +67,10 @@ def reset(self): def __call__(self, state: TuningJobState) -> bool: num_labeled = len(state.candidate_evaluations) - assert self.lastrec_key is None or \ - num_labeled >= self.lastrec_key, \ - "num_labeled = {} < {} = lastrec_key".format( - num_labeled, self.lastrec_key) + assert ( + self.lastrec_key is None or num_labeled >= self.lastrec_key + ), f"num_labeled = {num_labeled} < {self.lastrec_key} = lastrec_key" + # self.lastrec_XYZ is needed in order to allow __call__ # to be called several times with the same num_labeled if num_labeled == self.lastrec_key: @@ -82,7 +82,7 @@ def __call__(self, state: TuningJobState) -> bool: # optimization # Smallest init_length + k*period > num_labeled: self.current_bound = num_labeled + self.period -\ - ((num_labeled - self.init_length) % self.period) + ((num_labeled - self.init_length) % self.period) ret_value = False self.lastrec_key = num_labeled self.lastrec_value = ret_value @@ -117,7 +117,7 @@ def reset(self): def _num_max_resource_cases(self, state: TuningJobState): def is_max_resource(config: Candidate) -> int: if isinstance(config, CS.Configuration) and \ - (config.get_dictionary()[self.resource_attr_name] == + (config.get_dictionary()[self.resource_attr_name] == self.max_resource): return 1 else: @@ -131,7 +131,7 @@ def __call__(self, state: TuningJobState) -> bool: return False num_max_resource_cases = self._num_max_resource_cases(state) if self.lastrec_max_resource_cases is None or \ - num_max_resource_cases > self.lastrec_max_resource_cases: + num_max_resource_cases > self.lastrec_max_resource_cases: self.lastrec_max_resource_cases = num_max_resource_cases return False else: diff --git a/autogluon/searcher/bayesopt/models/gpmxnet_transformers.py b/autogluon/searcher/bayesopt/models/gpmxnet_transformers.py index 7e0361a3166f..0c25a2487f21 100644 --- a/autogluon/searcher/bayesopt/models/gpmxnet_transformers.py +++ b/autogluon/searcher/bayesopt/models/gpmxnet_transformers.py @@ -129,28 +129,33 @@ def drop_candidate(self, candidate: Candidate): pos = self._find_candidate( candidate, self._state.candidate_evaluations) if pos != -1: - self._model = None # Invalidate self._state.candidate_evaluations.pop(pos) if self._debug_log is not None: - deb_msg = "[GPMXNetAsyncPendingCandidateStateTransformer.drop_candidate]\n" - deb_msg += ("- len(candidate_evaluations) afterwards = {}".format( - len(self.state.candidate_evaluations))) + deb_msg = ( + "[GPMXNetAsyncPendingCandidateStateTransformer.drop_candidate]\n" + + f"- len(candidate_evaluations) afterwards = {len(self.state.candidate_evaluations)}" + ) + logger.info(deb_msg) else: # Try pending pos = self._find_candidate( candidate, self._state.pending_evaluations) - assert pos != -1, \ - "Candidate {} not registered (neither labeled, nor pending)".format( - candidate) - self._model = None # Invalidate + assert ( + pos != -1 + ), f"Candidate {candidate} not registered (neither labeled, nor pending)" + self._state.pending_evaluations.pop(pos) if self._debug_log is not None: - deb_msg = "[GPMXNetAsyncPendingCandidateStateTransformer.drop_candidate]\n" - deb_msg += ("- len(pending_evaluations) afterwards = {}\n".format( - len(self.state.pending_evaluations))) + deb_msg = ( + "[GPMXNetAsyncPendingCandidateStateTransformer.drop_candidate]\n" + + f"- len(pending_evaluations) afterwards = {len(self.state.pending_evaluations)}\n" + ) + logger.info(deb_msg) + self._model = None # Invalidate + def label_candidate(self, data: CandidateEvaluation): """ Adds a labeled candidate. If it was pending before, it is removed as @@ -178,9 +183,11 @@ def filter_pending_evaluations( filter_pred, self._state.pending_evaluations)) if len(new_pending_evaluations) != len(self._state.pending_evaluations): if self._debug_log is not None: - deb_msg = "[GPMXNetAsyncPendingCandidateStateTransformer.filter_pending_evaluations]\n" - deb_msg += ("- from len {} to {}".format( - len(self.state.pending_evaluations), len(new_pending_evaluations))) + deb_msg = ( + "[GPMXNetAsyncPendingCandidateStateTransformer.filter_pending_evaluations]\n" + + f"- from len {len(self.state.pending_evaluations)} to {len(new_pending_evaluations)}" + ) + logger.info(deb_msg) self._model = None # Invalidate del self._state.pending_evaluations[:] @@ -194,14 +201,15 @@ def _compute_model(self, skip_optimization: bool = None): if skip_optimization is None: skip_optimization = self.skip_optimization(self._state) fit_parameters = not skip_optimization - if fit_parameters and self._candidate_evaluations: - # Did the labeled data really change since the last recent refit? - # If not, skip the refitting - if self._state.candidate_evaluations == self._candidate_evaluations: - fit_parameters = False - logger.warning( - "Skipping the refitting of GP hyperparameters, since the " - "labeled data did not change since the last recent fit") + if ( + fit_parameters + and self._candidate_evaluations + and self._state.candidate_evaluations == self._candidate_evaluations + ): + fit_parameters = False + logger.warning( + "Skipping the refitting of GP hyperparameters, since the " + "labeled data did not change since the last recent fit") self._model = GPMXNetModel( state=self._state, active_metric=args.active_metric, diff --git a/autogluon/searcher/bayesopt/models/mxhead_acqfunc.py b/autogluon/searcher/bayesopt/models/mxhead_acqfunc.py index c821c98441f1..0d2aa71a7c2d 100644 --- a/autogluon/searcher/bayesopt/models/mxhead_acqfunc.py +++ b/autogluon/searcher/bayesopt/models/mxhead_acqfunc.py @@ -42,9 +42,10 @@ def compute_acq(self, x: np.ndarray, # average them do_avg = (mean.ndim == 2 and mean.shape[1] > 1) if do_avg: - assert mean.shape[1] == current_best.size, \ - "mean.shape[1] = {}, current_best.size = {} (must be the same)".format( - mean.shape[1], current_best.size) + assert ( + mean.shape[1] == current_best.size + ), f"mean.shape[1] = {mean.shape[1]}, current_best.size = {current_best.size} (must be the same)" + std = std.reshape((-1, 1)) fvals = self._compute_head(mean, std, current_best) if do_avg: @@ -55,7 +56,7 @@ def compute_acq(self, x: np.ndarray, def compute_acq_with_gradients( self, x: np.ndarray, model: Optional[SurrogateModel] = None) -> \ - Tuple[np.ndarray, np.ndarray]: + Tuple[np.ndarray, np.ndarray]: if model is None: model = self.model dtype_np = x.dtype diff --git a/autogluon/searcher/bayesopt/models/mxnet_base.py b/autogluon/searcher/bayesopt/models/mxnet_base.py index b488cb613898..b9457e193817 100644 --- a/autogluon/searcher/bayesopt/models/mxnet_base.py +++ b/autogluon/searcher/bayesopt/models/mxnet_base.py @@ -29,7 +29,7 @@ def __init__( dtype_nd = DATA_TYPE super(SurrogateModelMXNet, self).__init__( state, active_metric, random_seed) - assert dtype_nd == np.float32 or dtype_nd == np.float64 + assert dtype_nd in [np.float32, np.float64] self.ctx_nd = ctx_nd self.dtype_nd = dtype_nd self._current_best = None @@ -47,12 +47,13 @@ def predict(self, X: np.ndarray) -> List[Tuple[np.ndarray, np.ndarray]]: X_nd = self.convert_np_to_nd(X) predictions_nd = self.predict_nd(X_nd) - predictions_list = [ - (m_nd.asnumpy().astype(dtype_np, copy=False), - s_nd.asnumpy().astype(dtype_np, copy=False)) + return [ + ( + m_nd.asnumpy().astype(dtype_np, copy=False), + s_nd.asnumpy().astype(dtype_np, copy=False), + ) for m_nd, s_nd in predictions_nd ] - return predictions_list def context_for_nd(self) -> mx.Context: return self.ctx_nd @@ -62,9 +63,9 @@ def dtype_for_nd(self): def _get_dtypes(self, x: np.ndarray): dtype_np = x.dtype - if dtype_np == np.int32 or dtype_np == np.int64: + if dtype_np in [np.int32, np.int64]: dtype_np = np.float64 - assert dtype_np == np.float32 or dtype_np == np.float64 + assert dtype_np in [np.float32, np.float64] dtype_nd = self.dtype_nd return dtype_np, dtype_nd @@ -83,10 +84,12 @@ def convert(c): candidates = [ x.candidate for x in self.state.candidate_evaluations] + \ - self.state.pending_candidates + self.state.pending_candidates if self._debug_log is not None: - deb_msg = "[GPMXNetModel.current_best -- RECOMPUTING]\n" - deb_msg += ("- len(candidates) = {}".format(len(candidates))) + deb_msg = "[GPMXNetModel.current_best -- RECOMPUTING]\n" + ( + "- len(candidates) = {}".format(len(candidates)) + ) + logger.info(deb_msg) if len(candidates) == 0: diff --git a/autogluon/searcher/bayesopt/models/nphead_acqfunc.py b/autogluon/searcher/bayesopt/models/nphead_acqfunc.py index 081ba15e3131..53ddfc3fc072 100644 --- a/autogluon/searcher/bayesopt/models/nphead_acqfunc.py +++ b/autogluon/searcher/bayesopt/models/nphead_acqfunc.py @@ -50,9 +50,10 @@ def compute_acq(self, x: np.ndarray, # average them do_avg = (mean.ndim == 2 and mean.shape[1] > 1) if do_avg: - assert mean.shape[1] == current_best.size, \ - "mean.shape[1] = {}, current_best.size = {} (must be the same)".format( - mean.shape[1], current_best.size) + assert ( + mean.shape[1] == current_best.size + ), f"mean.shape[1] = {mean.shape[1]}, current_best.size = {current_best.size} (must be the same)" + std = std.reshape((-1, 1)) fvals = self._compute_head(mean, std, current_best).hvals if do_avg: @@ -63,7 +64,7 @@ def compute_acq(self, x: np.ndarray, def compute_acq_with_gradients( self, x: np.ndarray, model: Optional[SurrogateModel] = None) -> \ - Tuple[np.ndarray, np.ndarray]: + Tuple[np.ndarray, np.ndarray]: if model is None: model = self.model dtype_nd = model.dtype_for_nd() diff --git a/autogluon/searcher/bayesopt/tuning_algorithms/bo_algorithm.py b/autogluon/searcher/bayesopt/tuning_algorithms/bo_algorithm.py index 896e6b5ce7c3..0f8a5e377063 100644 --- a/autogluon/searcher/bayesopt/tuning_algorithms/bo_algorithm.py +++ b/autogluon/searcher/bayesopt/tuning_algorithms/bo_algorithm.py @@ -103,7 +103,7 @@ def next_candidates(self) -> List[Candidate]: num_outer_iterations = 1 num_inner_candidates = self.num_requested_candidates assert num_outer_iterations == 1 or self.pending_candidate_state_transformer, \ - "Need pending_candidate_state_transformer for greedy batch selection" + "Need pending_candidate_state_transformer for greedy batch selection" candidates = [] model = None # GPMXNetModel, if num_outer_iterations > 1 for outer_iter in range(num_outer_iterations): @@ -137,7 +137,7 @@ def _get_next_candidates(self, num_candidates: int, self.num_initial_candidates, self.blacklisted_candidates) else: initial_candidates = \ - self.initial_candidates_generator.generate_candidates_en_bulk( + self.initial_candidates_generator.generate_candidates_en_bulk( self.num_initial_candidates) if self.profiler is not None: self.profiler.stop('nextcand_genrandom') @@ -162,7 +162,7 @@ def _get_next_candidates(self, num_candidates: int, initial_candidates, self.local_optimizer, model=model) logger.info("BO Algorithm: Selecting final set of candidates.") if self.debug_log is not None and \ - isinstance(self.local_optimizer, LBFGSOptimizeAcquisition): + isinstance(self.local_optimizer, LBFGSOptimizeAcquisition): # We would like to get num_evaluations from the first run (usually # the only one). This requires peeking at the first entry of the # iterator @@ -184,15 +184,12 @@ def _order_candidates( scoring_function: ScoringFunction, model: Optional[SurrogateModel], with_scores: bool = False) -> List[Candidate]: - if len(candidates) == 0: + if not candidates: return [] # scored in batch as this can be more efficient scores = scoring_function.score(candidates, model=model) sorted_list = sorted(zip(scores, candidates), key=lambda x: x[0]) - if with_scores: - return sorted_list - else: - return [cand for score, cand in sorted_list] + return sorted_list if with_scores else [cand for score, cand in sorted_list] def _lazily_locally_optimize( @@ -221,9 +218,9 @@ def _pick_from_locally_optimized( result = [] for original_candidate, optimized_candidate in candidates_with_optimization: insert_candidate = None - optimized_is_duplicate = duplicate_detector.contains( - updated_blacklist, optimized_candidate) - if optimized_is_duplicate: + if optimized_is_duplicate := duplicate_detector.contains( + updated_blacklist, optimized_candidate + ): # in the unlikely case that the optimized candidate ended at a # place that caused a duplicate we try to return the original instead original_also_duplicate = duplicate_detector.contains( diff --git a/autogluon/searcher/bayesopt/utils/test_objects.py b/autogluon/searcher/bayesopt/utils/test_objects.py index be901fdbd8e5..3df295465cf0 100644 --- a/autogluon/searcher/bayesopt/utils/test_objects.py +++ b/autogluon/searcher/bayesopt/utils/test_objects.py @@ -23,8 +23,7 @@ class RepeatedCandidateGenerator(CandidateGenerator): """Generates candidates from a fixed set. Used to test the deduplication logic.""" def __init__(self, n_unique_candidates: int): self.all_unique_candidates = [ - (1.0*j, j, "value_" + str(j)) - for j in range(n_unique_candidates) + (1.0 * j, j, f"value_{str(j)}") for j in range(n_unique_candidates) ] def generate_candidates(self) -> Iterator[StateIdAndCandidate]: diff --git a/autogluon/searcher/grid_searcher.py b/autogluon/searcher/grid_searcher.py index d75d3e146c20..841b9404f7b3 100644 --- a/autogluon/searcher/grid_searcher.py +++ b/autogluon/searcher/grid_searcher.py @@ -29,12 +29,14 @@ def __init__(self, configspace, **kwargs): for hp in hp_ordering: hp_obj = configspace.get_hyperparameter(hp) hp_type = str(type(hp_obj)).lower() - assert 'categorical' in hp_type, \ - 'Only Categorical is supported, but {} is {}'.format(hp, hp_type) + assert ( + 'categorical' in hp_type + ), f'Only Categorical is supported, but {hp} is {hp_type}' + param_grid[hp] = hp_obj.choices self._configs = list(ParameterGrid(param_grid)) - print('Number of configurations for grid search is {}'.format(len(self._configs))) + print(f'Number of configurations for grid search is {len(self._configs)}') def __len__(self): return len(self._configs) diff --git a/autogluon/searcher/rl_controller.py b/autogluon/searcher/rl_controller.py index 8684138e6873..aa2df8eb2b64 100644 --- a/autogluon/searcher/rl_controller.py +++ b/autogluon/searcher/rl_controller.py @@ -50,12 +50,16 @@ def __init__(self, kwspaces, ctx=mx.cpu(), controller_type='lstm', self.controller._prefetch() def __repr__(self): - reprstr = self.__class__.__name__ + '(' + \ - 'Number of Trials: {}.'.format(len(self._results)) + \ - 'Best Config: {}'.format(self.get_best_config()) + \ - 'Best Reward: {}'.format(self.get_best_reward()) + \ - ')' - return reprstr + return ( + ( + ( + f'{self.__class__.__name__}(' + + f'Number of Trials: {len(self._results)}.' + ) + + f'Best Config: {self.get_best_config()}' + ) + + f'Best Reward: {self.get_best_reward()}' + ) + ')' def get_config(self, **kwargs): return self.controller.sample()[0] @@ -156,10 +160,7 @@ def _get_default_hidden(key): self.static_inputs = keydefaultdict(_get_default_hidden) def forward(self, inputs, hidden, block_idx, is_embed): - if not is_embed: - embed = self.encoder(inputs) - else: - embed = inputs + embed = inputs if is_embed else self.encoder(inputs) _, (hx, cx) = self.lstm(embed, hidden) logits = self.decoders[block_idx](hx) @@ -231,11 +232,10 @@ def sample(self, batch_size=1, with_details=False, with_entropy=False): config[k] = int(choice) configs.append(config) - if with_details: - entropies = F.stack(*entropies, axis=1) if with_entropy else entropies - return configs, F.stack(*log_probs, axis=1), entropies - else: + if not with_details: return configs + entropies = F.stack(*entropies, axis=1) if with_entropy else entropies + return configs, F.stack(*log_probs, axis=1), entropies class Alpha(mx.gluon.Block): @@ -338,11 +338,10 @@ def sample(self, batch_size=1, with_details=False, with_entropy=False): config[k] = int(choice) configs.append(config) - if with_details: - entropies = F.stack(*entropies, axis=1) if with_entropy else entropies - return configs, F.stack(*log_probs, axis=1), entropies - else: + if not with_details: return configs + entropies = F.stack(*entropies, axis=1) if with_entropy else entropies + return configs, F.stack(*log_probs, axis=1), entropies class AlphaController(BaseController): @@ -360,7 +359,7 @@ def __init__(self, kwspaces, softmax_temperature=1.0, ctx=mx.cpu(), **kwargs): # controller lstm self.decoders = nn.Sequential() - for idx, size in enumerate(self.num_tokens): + for size in self.num_tokens: self.decoders.add(Alpha((size,))) def inference(self): @@ -411,8 +410,7 @@ def sample(self, batch_size=1, with_details=False, with_entropy=False): config[k] = int(choice) configs.append(config) - if with_details: - entropies = F.stack(*entropies, axis=1) if with_entropy else entropies - return configs, F.stack(*log_probs, axis=1), entropies - else: + if not with_details: return configs + entropies = F.stack(*entropies, axis=1) if with_entropy else entropies + return configs, F.stack(*log_probs, axis=1), entropies diff --git a/autogluon/searcher/searcher.py b/autogluon/searcher/searcher.py index 3a7a1363e5ce..8bbcd618bdc4 100644 --- a/autogluon/searcher/searcher.py +++ b/autogluon/searcher/searcher.py @@ -96,8 +96,10 @@ def update(self, config, **kwargs): """ reward = kwargs.get(self._reward_attribute) - assert reward is not None, \ - "Missing reward attribute '{}'".format(self._reward_attribute) + assert ( + reward is not None + ), f"Missing reward attribute '{self._reward_attribute}'" + with self.LOCK: # _results is updated if reward is larger than the previous entry. # This is the correct behaviour for multi-fidelity schedulers, @@ -149,14 +151,14 @@ def cumulative_profile_record(self): profiling information over get_config calls, the corresponding dict is returned here. """ - return dict() + return {} def model_parameters(self): """ :return: Dictionary with current model (hyper)parameter values if this is supported; otherwise empty """ - return dict() + return {} def get_best_reward(self): """Calculates the reward (i.e. validation performance) produced by training under the best configuration identified so far. @@ -183,17 +185,16 @@ def get_best_config(self): config_pkl = max(self._results, key=self._results.get) return pickle.loads(config_pkl) else: - return dict() + return {} def get_best_config_reward(self): """Returns the best configuration found so far, as well as the reward associated with this best config. """ with self.LOCK: - if self._results: - config_pkl = max(self._results, key=self._results.get) - return pickle.loads(config_pkl), self._results[config_pkl] - else: - return dict(), self._reward_while_pending() + if not self._results: + return {}, self._reward_while_pending() + config_pkl = max(self._results, key=self._results.get) + return pickle.loads(config_pkl), self._results[config_pkl] def get_state(self): """ @@ -238,15 +239,14 @@ def debug_log(self): def __repr__(self): config, reward = self.get_best_config_reward() - reprstr = ( - f'{self.__class__.__name__}(' + - f'\nConfigSpace: {self.configspace}.' + - f'\nNumber of Trials: {len(self._results)}.' + - f'\nBest Config: {config}' + - f'\nBest Reward: {reward}' + - f')' + return ( + f'{self.__class__.__name__}(' + + f'\nConfigSpace: {self.configspace}.' + + f'\nNumber of Trials: {len(self._results)}.' + + f'\nBest Config: {config}' + + f'\nBest Reward: {reward}' + + ')' ) - return reprstr class RandomSearcher(BaseSearcher): @@ -298,10 +298,7 @@ def __init__(self, configspace, **kwargs): self._debug_log = kwargs.get('debug_log') if self._debug_log is not None: if isinstance(self._debug_log, bool): - if self._debug_log: - self._debug_log = DebugLogPrinter() - else: - self._debug_log = None + self._debug_log = DebugLogPrinter() if self._debug_log else None else: assert isinstance(self._debug_log, DebugLogPrinter) @@ -347,7 +344,7 @@ def get_config(self, **kwargs): num_tries = 1 while pickle.dumps(new_config) in self._results: assert num_tries <= self.MAX_RETRIES, \ - f"Cannot find new config in BaseSearcher, even after {self.MAX_RETRIES} trials" + f"Cannot find new config in BaseSearcher, even after {self.MAX_RETRIES} trials" new_config = self.configspace.sample_configuration().get_dictionary() num_tries += 1 self._results[pickle.dumps(new_config)] = self._reward_while_pending() @@ -365,9 +362,8 @@ def update(self, config, **kwargs): if self._resource_attribute is not None: # For HyperbandScheduler, also add the resource attribute resource = int(kwargs[self._resource_attribute]) - config_id = config_id + ':{}'.format(resource) - msg = "Update for config_id {}: reward = {}".format( - config_id, reward) + config_id = f'{config_id}:{resource}' + msg = f"Update for config_id {config_id}: reward = {reward}" logger.info(msg) def get_state(self): diff --git a/autogluon/searcher/searcher_factory.py b/autogluon/searcher/searcher_factory.py index a3e84883860b..360effa1de1d 100644 --- a/autogluon/searcher/searcher_factory.py +++ b/autogluon/searcher/searcher_factory.py @@ -79,13 +79,14 @@ def searcher_factory(name, **kwargs): else: return GPMultiFidelitySearcher(**kwargs) else: - raise AssertionError("name = '{}' not supported".format(name)) + raise AssertionError(f"name = '{name}' not supported") def _check_supported_scheduler(name, scheduler, supp_schedulers): assert scheduler is not None, \ "Scheduler must set search_options['scheduler']" - assert scheduler in supp_schedulers, \ - "Searcher '{}' only works with schedulers {} (not with '{}')".format( - name, supp_schedulers, scheduler) + assert ( + scheduler in supp_schedulers + ), f"Searcher '{name}' only works with schedulers {supp_schedulers} (not with '{scheduler}')" + return scheduler diff --git a/autogluon/searcher/skopt_searcher.py b/autogluon/searcher/skopt_searcher.py index 25d2b69967b5..36c3e5363467 100644 --- a/autogluon/searcher/skopt_searcher.py +++ b/autogluon/searcher/skopt_searcher.py @@ -102,7 +102,7 @@ def __init__(self, configspace, **kwargs): elif 'ordinal' in hp_type: hp_dimension = Categorical(hp_obj.sequence, name=hp) else: - raise ValueError("unknown hyperparameter type: %s" % hp) + raise ValueError(f"unknown hyperparameter type: {hp}") skopt_hpspace.append(hp_dimension) skopt_keys = { 'base_estimator', 'n_random_starts', 'n_initial_points', @@ -208,10 +208,7 @@ def config2skopt(self, config): ------- Object of same type as: `skOpt.Optimizer.ask()` """ - point = [] - for hp in self.hp_ordering: - point.append(config[hp]) - return point + return [config[hp] for hp in self.hp_ordering] def skopt2config(self, point): """ Converts skopt point (list object) to autogluon config format (dict object. diff --git a/autogluon/task/base/base_predictor.py b/autogluon/task/base/base_predictor.py index 388cf7a9feea..f818bcdc0a6c 100644 --- a/autogluon/task/base/base_predictor.py +++ b/autogluon/task/base/base_predictor.py @@ -71,7 +71,6 @@ def load(cls, output_directory): results_file = output_directory + RESULTS_FILENAME predictor = pickle.load(open(filepath, "rb")) predictor.results = json.load(open(results_file, 'r')) - pass # Need to load models and set them = predictor.model @abstractmethod def save(self, output_directory): @@ -84,7 +83,7 @@ def save(self, output_directory): self.model = None # Save model separately from Predictor object self.results = None # Save results separately from Predictor object pickle.dump(self, open(filepath, 'wb')) - logger.info("Predictor saved to file: %s " % filepath) + logger.info(f"Predictor saved to file: {filepath} ") def _save_results(self, output_directory): """ Internal helper function: Save results in human-readable file JSON format """ @@ -132,10 +131,12 @@ def fit_summary(self, output_directory=None, verbosity=2): <= 0 for no output printing, 1 for just high-level summary, 2 for summary and plot, >= 3 for all information contained in results object. """ if verbosity > 0: - summary = {} - for k in self.results.keys(): - if k not in ['metadata', 'trial_info']: - summary[k] = self.results[k] + summary = { + k: self.results[k] + for k in self.results.keys() + if k not in ['metadata', 'trial_info'] + } + print("Summary of Fit Process: ") print(summary) if len(self.results['metadata']) > 0: @@ -146,7 +147,7 @@ def fit_summary(self, output_directory=None, verbosity=2): if verbosity > 2: for trial_id in ordered_trials: print("Information about each trial: ") - print("Trial ID: %s" % trial_id) + print(f"Trial ID: {trial_id}") print(self.results['trial_info'][trial_id]) if verbosity > 3: # Create plot summaries: @@ -159,20 +160,18 @@ def _createResults(self): Empty for now, but should be updated during task.fit(). All tasks should adhere to this same template for consistency. """ - results = {} - results['time'] = None # run-time of task.fit() - results['reward_attr'] = 'none' # (str), the reward attribute used to measure the performance - results[results['reward_attr']] = None # performance of the best trials - results['num_trials_completed'] = None # number of trials completed during task.fit() - results['best_hyperparameters'] = None # hyperparameter values corresponding to the chosen model in self.model - results['search_space'] = None # hyperparameter search space considered in task.fit() - results['search_strategy'] = None # HPO algorithm used (ie. Hyperband, random, BayesOpt). If the HPO algorithm used kwargs, then this should be tuple (HPO_algorithm_string, HPO_kwargs) - - results['metadata'] = {} # dict containing other optional metadata with keys. For example: - # latency = inference-time of self.model (time for feedforward pass) - # memory = amount of memory required by self.model - - results['trial_info'] = {} # dict with keys = trial_IDs, values = dict of information about each individual trial (length = results['num_trials_completed']) + results = { + 'time': None, + 'reward_attr': 'none', + results['reward_attr']: None, + 'num_trials_completed': None, + 'best_hyperparameters': None, + 'search_space': None, + 'search_strategy': None, + 'metadata': {}, + 'trial_info': {}, + } + """ Example of what one element of this dict must look like: results['trial_info'][trial_id] = { diff --git a/autogluon/task/base/base_task.py b/autogluon/task/base/base_task.py index b717daf613b9..6f962041bbfd 100644 --- a/autogluon/task/base/base_task.py +++ b/autogluon/task/base/base_task.py @@ -127,12 +127,12 @@ def compile_scheduler_options( """ if scheduler_options is None: - scheduler_options = dict() + scheduler_options = {} else: assert isinstance(scheduler_options, dict) assert isinstance(search_strategy, str) if search_options is None: - search_options = dict() + search_options = {} if visualizer is None: visualizer = 'none' if time_attr is None: diff --git a/autogluon/task/image_classification/classifier.py b/autogluon/task/image_classification/classifier.py index dc9c6ae403bb..0f1227a49a97 100644 --- a/autogluon/task/image_classification/classifier.py +++ b/autogluon/task/image_classification/classifier.py @@ -61,13 +61,12 @@ def load(cls, checkpoint): model_params = state_dict['model_params'] ensemble = state_dict['ensemble'] - if ensemble <= 1: - model_args = copy.deepcopy(args) - model_args.update(results['best_config']) - model = get_network(args.net, num_classes=results['num_classes'], ctx=mx.cpu(0)) - update_params(model, model_params) - else: + if ensemble > 1: raise NotImplemented + model_args = copy.deepcopy(args) + model_args.update(results['best_config']) + model = get_network(args.net, num_classes=results['num_classes'], ctx=mx.cpu(0)) + update_params(model, model_params) return cls(model, results, eval_func, scheduler_checkpoint, args, ensemble, format_results=False) @@ -131,11 +130,10 @@ def predict_img(img, ensemble=False): proba = self.predict_proba(img) if ensemble: return proba - else: - ind = mx.nd.argmax(proba, axis=1).astype('int') - idx = mx.nd.stack(mx.nd.arange(proba.shape[0], ctx=proba.context), ind.astype('float32')) - probai = mx.nd.gather_nd(proba, idx) - return ind, probai, proba + ind = mx.nd.argmax(proba, axis=1).astype('int') + idx = mx.nd.stack(mx.nd.arange(proba.shape[0], ctx=proba.context), ind.astype('float32')) + probai = mx.nd.gather_nd(proba, idx) + return ind, probai, proba def avg_prediction(different_dataset, threshold=0.001): result = defaultdict(list) @@ -144,7 +142,7 @@ def avg_prediction(different_dataset, threshold=0.001): for j in range(len(different_dataset[0])): result[j].append(different_dataset[i][j]) - for c in result.keys(): + for c in result: proba_all = sum([*result[c]]) / len(different_dataset) proba_all = (proba_all >= threshold) * proba_all ind = mx.nd.argmax(proba_all, axis=1).astype('int') @@ -248,7 +246,7 @@ def evaluate(self, dataset, input_size=224, ctx=[mx.cpu()]): for batch in tbar: self.eval_func(net, batch, batch_fn, metric, ctx) _, test_reward = metric.get() - tbar.set_description('{}: {}'.format(args.metric, test_reward)) + tbar.set_description(f'{args.metric}: {test_reward}') _, test_reward = metric.get() return test_reward diff --git a/autogluon/task/image_classification/dataset.py b/autogluon/task/image_classification/dataset.py index e09da1d8e74a..aa54e69360cf 100644 --- a/autogluon/task/image_classification/dataset.py +++ b/autogluon/task/image_classification/dataset.py @@ -43,59 +43,66 @@ def __init__(self, fn): self._fn = fn def __call__(self, x, *args): - if args: - return (self._fn(x),) + args - return self._fn(x) + return (self._fn(x),) + args if args else self._fn(x) def generate_transform(train, resize, _is_osx, input_size, jitter_param): if _is_osx: # using PIL to load image (slow) - if train: - transform = Compose( + return ( + Compose( [ RandomResizedCrop(input_size), RandomHorizontalFlip(), ColorJitter(0.4, 0.4, 0.4), ToTensor(), - transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) + transforms.Normalize( + [0.485, 0.456, 0.406], [0.229, 0.224, 0.225] + ), ] ) - else: - transform = Compose( + if train + else Compose( [ Resize(resize), CenterCrop(input_size), ToTensor(), - transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) - ] - ) - else: - if train: - transform = transforms.Compose( - [ - transforms.RandomResizedCrop(input_size), - transforms.RandomFlipLeftRight(), - transforms.RandomColorJitter( - brightness=jitter_param, - contrast=jitter_param, - saturation=jitter_param + transforms.Normalize( + [0.485, 0.456, 0.406], [0.229, 0.224, 0.225] ), - transforms.RandomLighting(0.1), - transforms.ToTensor(), - transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) - ] - ) - else: - transform = transforms.Compose( - [ - transforms.Resize(resize), - transforms.CenterCrop(input_size), - transforms.ToTensor(), - transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ] ) - return transform + ) + + elif train: + return transforms.Compose( + [ + transforms.RandomResizedCrop(input_size), + transforms.RandomFlipLeftRight(), + transforms.RandomColorJitter( + brightness=jitter_param, + contrast=jitter_param, + saturation=jitter_param, + ), + transforms.RandomLighting(0.1), + transforms.ToTensor(), + transforms.Normalize( + [0.485, 0.456, 0.406], [0.229, 0.224, 0.225] + ), + ] + ) + + else: + return transforms.Compose( + [ + transforms.Resize(resize), + transforms.CenterCrop(input_size), + transforms.ToTensor(), + transforms.Normalize( + [0.485, 0.456, 0.406], [0.229, 0.224, 0.225] + ), + ] + ) @func() @@ -282,15 +289,16 @@ def __init__(self, filename, gray_scale=False, transform=None, classes=None): flag = 0 if gray_scale else 1 # retrieve number of classes without decoding images td = RecordFileDataset(filename) - s = set([recordio.unpack(td.__getitem__(i))[0].label[0] for i in range(len(td))]) + s = {recordio.unpack(td.__getitem__(i))[0].label[0] for i in range(len(td))} self._num_classes = len(s) if not classes: self._classes = [str(i) for i in range(self._num_classes)] - else: - if len(self._num_classes) != len(classes): - warnings.warn('Provided class names do not match data, expected "num_class" is {} ' - 'vs. provided: {}'.format(self._num_classes, len(classes))) - self._classes = list(classes) + \ + elif len(self._num_classes) != len(classes): + warnings.warn( + f'Provided class names do not match data, expected "num_class" is {self._num_classes} vs. provided: {len(classes)}' + ) + + self._classes = list(classes) + \ [str(i) for i in range(len(classes), self._num_classes)] self._dataset = ImageRecordDataset(filename, flag=flag) if transform: @@ -337,7 +345,7 @@ def _list_images(self, root): self.items = [] path = os.path.expanduser(root) if not os.path.isdir(path): - raise ValueError('Ignoring %s, which is not a directory.' % path, stacklevel=3) + raise ValueError(f'Ignoring {path}, which is not a directory.', stacklevel=3) for filename in sorted(os.listdir(path)): filename = os.path.join(path, filename) if os.path.isfile(filename): # add @@ -416,7 +424,7 @@ class ImageFolderDataset(object): def __init__(self, root, extensions=None, transform=None, is_valid_file=None): root = os.path.expanduser(root) self.root = root - extensions = extensions if extensions else self.IMG_EXTENSIONS + extensions = extensions or self.IMG_EXTENSIONS self._transform = transform classes, class_to_idx = self._find_classes(self.root) @@ -530,7 +538,7 @@ def __len__(self): return len(self.samples) def __repr__(self): - head = "Dataset " + self.__class__.__name__ + head = f"Dataset {self.__class__.__name__}" body = [f"Number of datapoints: {self.__len__()}"] if self.root is not None: body.append(f"Root location: {self.root}") diff --git a/autogluon/task/image_classification/image_classification.py b/autogluon/task/image_classification/image_classification.py index 65ac51d9bc78..e5181b25f3d2 100644 --- a/autogluon/task/image_classification/image_classification.py +++ b/autogluon/task/image_classification/image_classification.py @@ -52,10 +52,9 @@ def Dataset(path=None, name=None, train=True, input_size=224, crop_ratio=0.875, Dataset object that can be passed to `task.fit()`, which is actually an :class:`autogluon.space.AutoGluonObject`. To interact with such an object yourself, you must first call `Dataset.init()` to instantiate the object in Python. """ - if name is None: - if path is None: - raise ValueError("Either `path` or `name` must be present in Dataset(). " - "If `name` is provided, it will override the rest of the arguments.") + if name is None and path is None: + raise ValueError("Either `path` or `name` must be present in Dataset(). " + "If `name` is provided, it will override the rest of the arguments.") return get_dataset(path=path, train=train, name=name, input_size=input_size, crop_ratio=crop_ratio, *args, **kwargs) @@ -244,7 +243,7 @@ def fit(dataset, whether to use group norm. """ assert search_strategy not in {'bayesopt', 'bayesopt_hyperband'}, \ - "search_strategy == 'bayesopt' or 'bayesopt_hyperband' not yet supported" + "search_strategy == 'bayesopt' or 'bayesopt_hyperband' not yet supported" checkpoint = os.path.join(output_directory, 'exp1.ag') if auto_search: # The strategies can be injected here, for example: automatic suggest some hps @@ -259,7 +258,7 @@ def fit(dataset, if num_trials is None and time_limits is None: num_trials = 2 - final_fit_epochs = final_fit_epochs if final_fit_epochs else epochs + final_fit_epochs = final_fit_epochs or epochs train_image_classification.register_args( dataset=dataset, net=net, @@ -287,7 +286,7 @@ def fit(dataset, scheduler_options = {'grace_period': grace_period} else: assert 'grace_period' not in scheduler_options, \ - "grace_period appears both in scheduler_options and as direct argument" + "grace_period appears both in scheduler_options and as direct argument" scheduler_options = copy.copy(scheduler_options) scheduler_options['grace_period'] = grace_period logger.warning( @@ -322,7 +321,7 @@ def fit(dataset, models = [model] scheduler = create_scheduler( train_image_classification, search_strategy, scheduler_options) - for i in range(1, ensemble): + for _ in range(1, ensemble): resultsi = scheduler.run_with_config(results['best_config']) kwargs = { 'num_classes': resultsi['num_classes'], 'ctx': mx.cpu(0)} diff --git a/autogluon/task/image_classification/losses.py b/autogluon/task/image_classification/losses.py index 8fb35168e016..2f8c68d49c5a 100644 --- a/autogluon/task/image_classification/losses.py +++ b/autogluon/task/image_classification/losses.py @@ -31,6 +31,5 @@ def get_loss_instance(name, **kwargs): err_str = '"%s" is not among the following loss list:\n\t' % (name) err_str += '%s' % ('\n\t'.join(sorted(losses.keys()))) raise ValueError(err_str) - loss = losses[name]() - return loss + return losses[name]() diff --git a/autogluon/task/image_classification/metrics.py b/autogluon/task/image_classification/metrics.py index b0e090f190d4..fea7ba1db206 100644 --- a/autogluon/task/image_classification/metrics.py +++ b/autogluon/task/image_classification/metrics.py @@ -24,5 +24,4 @@ def get_metric_instance(name, **kwargs): err_str = '"%s" is not among the following metric list:\n\t' % (name) err_str += '%s' % ('\n\t'.join(sorted(metrics.keys()))) raise ValueError(err_str) - metric = metrics[name]() - return metric + return metrics[name]() diff --git a/autogluon/task/image_classification/nets.py b/autogluon/task/image_classification/nets.py index d24f34f05367..2c554372f279 100644 --- a/autogluon/task/image_classification/nets.py +++ b/autogluon/task/image_classification/nets.py @@ -73,21 +73,18 @@ def auto_suggest_network(dataset, net): return net dataset_name = dataset_name.lower() if 'mnist' in dataset_name: - if isinstance(net, str) or isinstance(net, Categorical): + if isinstance(net, (str, Categorical)): net = mnist_net() - logger.info('Auto suggesting network net for dataset {}'.format(net, dataset_name)) + logger.info(f'Auto suggesting network net for dataset {net}') return net elif 'cifar' in dataset_name: if isinstance(net, str): if 'cifar' not in net: net = 'cifar_resnet20_v1' elif isinstance(net, Categorical): - newdata = [] - for x in net.data: - if 'cifar' in x: - newdata.append(x) - net.data = newdata if len(newdata) > 0 else ['cifar_resnet20_v1', 'cifar_resnet56_v1'] - logger.info('Auto suggesting network net for dataset {}'.format(net, dataset_name)) + newdata = [x for x in net.data if 'cifar' in x] + net.data = newdata or ['cifar_resnet20_v1', 'cifar_resnet56_v1'] + logger.info(f'Auto suggesting network net for dataset {net}') return net def get_network(net, **kwargs): diff --git a/autogluon/task/image_classification/pipeline.py b/autogluon/task/image_classification/pipeline.py index b296b0ad787a..bb889afb0aa9 100644 --- a/autogluon/task/image_classification/pipeline.py +++ b/autogluon/task/image_classification/pipeline.py @@ -47,11 +47,7 @@ def train_image_classification(args, reporter): for k, v in net.collect_params('.*beta|.*gamma|.*bias').items(): v.wd_mult = 0.0 - if args.tricks.label_smoothing or args.tricks.mixup: - sparse_label_loss = False - else: - sparse_label_loss = True - + sparse_label_loss = not args.tricks.label_smoothing and not args.tricks.mixup if distillation: teacher = target_kwargs.get_teacher diff --git a/autogluon/task/image_classification/processing_params.py b/autogluon/task/image_classification/processing_params.py index 91d3d6b4cf46..c9a8427611a6 100644 --- a/autogluon/task/image_classification/processing_params.py +++ b/autogluon/task/image_classification/processing_params.py @@ -45,11 +45,7 @@ def __init__(self, self._hybridize = hybridize self._hard_weight = hard_weight - if multi_precision: - self._dtype = 'float16' - else: - self._dtype = 'float32' - + self._dtype = 'float16' if multi_precision else 'float32' if use_gn: from gluoncv.nn import GroupNorm self._kwargs['norm_layer'] = GroupNorm @@ -63,10 +59,7 @@ def __init__(self, if last_gamma: self._kwargs['last_gamma'] = True - if self._model_teacher is not None and self._hard_weight < 1.0: - self.distillation = True - else: - self.distillation = False + self.distillation = self._model_teacher is not None and self._hard_weight < 1.0 @property def dtype(self): diff --git a/autogluon/task/image_classification/utils.py b/autogluon/task/image_classification/utils.py index e483fb714f5a..edc5e70065a5 100644 --- a/autogluon/task/image_classification/utils.py +++ b/autogluon/task/image_classification/utils.py @@ -36,12 +36,17 @@ def get_data_loader(dataset, input_size, batch_size, num_workers, final_fit, spl train_dataset, batch_size=batch_size, shuffle=True, last_batch="discard", num_workers=num_workers ) - val_data = None - if not final_fit: - val_data = DataLoader( - val_dataset, batch_size=batch_size, - shuffle=False, num_workers=num_workers + val_data = ( + None + if final_fit + else DataLoader( + val_dataset, + batch_size=batch_size, + shuffle=False, + num_workers=num_workers, ) + ) + batch_fn = default_batch_fn num_batches = len(train_data) return train_data, val_data, batch_fn, num_batches @@ -80,11 +85,12 @@ def mixup_transform(label, classes, lam=1, eta=0.0): def smooth(label, classes, eta=0.1): if isinstance(label, nd.NDArray): label = [label] - smoothed = [ - l.one_hot(classes, on_value=1 - eta + eta / classes, off_value=eta / classes) + return [ + l.one_hot( + classes, on_value=1 - eta + eta / classes, off_value=eta / classes + ) for l in label ] - return smoothed def default_train_fn(epoch, num_epochs, net, batch, batch_size, criterion, trainer, batch_fn, ctx, @@ -97,10 +103,7 @@ def default_train_fn(epoch, num_epochs, net, batch, batch_size, criterion, train if epoch >= num_epochs - mixup_off_epoch: lam = 1 data = [lam * X + (1 - lam) * X[::-1] for X in data] - if label_smoothing: - eta = 0.1 - else: - eta = 0.0 + eta = 0.1 if label_smoothing else 0.0 label = mixup_transform(label, classes, lam, eta) elif label_smoothing: hard_label = label @@ -124,21 +127,19 @@ def default_train_fn(epoch, num_epochs, net, batch, batch_size, criterion, train l.backward() trainer.step(batch_size, ignore_stale_grad=True) - if metric: - if mixup: - output_softmax = [ - nd.SoftmaxActivation(out.astype('float32', copy=False)) - for out in outputs - ] - metric.update(label, output_softmax) - else: - if label_smoothing: - metric.update(hard_label, outputs) - else: - metric.update(label, outputs) - return metric - else: + if not metric: return + if mixup: + output_softmax = [ + nd.SoftmaxActivation(out.astype('float32', copy=False)) + for out in outputs + ] + metric.update(label, output_softmax) + elif label_smoothing: + metric.update(hard_label, outputs) + else: + metric.update(label, outputs) + return metric def _train_val_split(train_dataset, split_ratio=0.2): diff --git a/autogluon/task/object_detection/data_parallel.py b/autogluon/task/object_detection/data_parallel.py index 5a7b98230697..5abae7cd401f 100644 --- a/autogluon/task/object_detection/data_parallel.py +++ b/autogluon/task/object_detection/data_parallel.py @@ -21,7 +21,7 @@ def forward_backward(self, x): gt_label = label[:, :, 4:5] gt_box = label[:, :, :4] cls_pred, box_pred, roi, samples, matches, rpn_score, rpn_box, anchors, cls_targets, \ - box_targets, box_masks, _ = self.net(data, gt_box, gt_label) + box_targets, box_masks, _ = self.net(data, gt_box, gt_label) # losses of rpn rpn_score = rpn_score.squeeze(axis=-1) num_rpn_pos = (rpn_cls_targets >= 0).sum() @@ -35,9 +35,9 @@ def forward_backward(self, x): num_rcnn_pos = (cls_targets >= 0).sum() rcnn_loss1 = self.rcnn_cls_loss(cls_pred, cls_targets, cls_targets.expand_dims(-1) >= 0) * cls_targets.size / \ - num_rcnn_pos + num_rcnn_pos rcnn_loss2 = self.rcnn_box_loss(box_pred, box_targets, box_masks) * box_pred.size / \ - num_rcnn_pos + num_rcnn_pos rcnn_loss = rcnn_loss1 + rcnn_loss2 # overall losses total_loss = rpn_loss.sum() * self.mix_ratio + rcnn_loss.sum() * self.mix_ratio @@ -59,4 +59,4 @@ def forward_backward(self, x): total_loss.backward() return rpn_loss1_metric, rpn_loss2_metric, rcnn_loss1_metric, rcnn_loss2_metric, \ - rpn_acc_metric, rpn_l1_loss_metric, rcnn_acc_metric, rcnn_l1_loss_metric + rpn_acc_metric, rpn_l1_loss_metric, rcnn_acc_metric, rcnn_l1_loss_metric diff --git a/autogluon/task/object_detection/dataset/coco.py b/autogluon/task/object_detection/dataset/coco.py index 601907f25f92..eef6e401c2bc 100644 --- a/autogluon/task/object_detection/dataset/coco.py +++ b/autogluon/task/object_detection/dataset/coco.py @@ -17,9 +17,13 @@ def __init__(self): self.train_dataset = gdata.COCODetection(splits='instances_train2017') self.val_dataset = gdata.COCODetection(splits='instances_val2017', skip_empty=False) self.val_metric = COCODetectionMetric( - self.val_dataset, args.save_prefix + '_eval', cleanup=True, - data_shape=(args.data_shape, args.data_shape)) - + self.val_dataset, + f'{args.save_prefix}_eval', + cleanup=True, + data_shape=(args.data_shape, args.data_shape), + ) + + #TODO: whether to use the code below """ # coco validation is slow, consider increase the validation interval diff --git a/autogluon/task/object_detection/dataset/voc.py b/autogluon/task/object_detection/dataset/voc.py index 0fad118de83a..31d88a1bb56d 100644 --- a/autogluon/task/object_detection/dataset/voc.py +++ b/autogluon/task/object_detection/dataset/voc.py @@ -75,7 +75,7 @@ def _load_items(self, splits): ids = [] for subfolder, name in splits: root = os.path.join(self._root, subfolder) if subfolder else self._root - lf = os.path.join(root, 'ImageSets', 'Main', name + '.txt') + lf = os.path.join(root, 'ImageSets', 'Main', f'{name}.txt') with open(lf, 'r') as f: ids += [(root, line.strip()) for line in f.readlines()] return ids @@ -108,7 +108,7 @@ def _check_valid(self, idx): ymax = (float(xml_box.find('ymax').text) - 1) if not ((0 <= xmin < width) and (0 <= ymin < height) \ - and (xmin < xmax <= width) and (ymin < ymax <= height)): + and (xmin < xmax <= width) and (ymin < ymax <= height)): return False return True @@ -139,11 +139,11 @@ class CustomVOCDetection(): def __init__(self, root, splits, name, classes, **kwargs): super().__init__() self.root = root - + # search classes from gt files for custom dataset if not (classes or name): classes = self.generate_gt() - + self.dataset = CustomVOCDetectionBase(classes=classes, root=root, splits=splits) diff --git a/autogluon/task/object_detection/detector.py b/autogluon/task/object_detection/detector.py index 98fbdc3941a0..3b9c2cb283cf 100644 --- a/autogluon/task/object_detection/detector.py +++ b/autogluon/task/object_detection/detector.py @@ -44,7 +44,7 @@ def evaluate(self, dataset, ctx=[mx.cpu()]): net.collect_params().reset_ctx(ctx) def _get_dataloader(net, test_dataset, data_shape, batch_size, num_workers, num_devices, - args): + args): """Get dataloader.""" if args.meta_arch == 'yolo3': width, height = data_shape, data_shape @@ -73,7 +73,7 @@ def _get_dataloader(net, test_dataset, data_shape, batch_size, num_workers, num_ ) return test_loader else: - raise NotImplementedError('%s not implemented.' % args.meta_arch) + raise NotImplementedError(f'{args.meta_arch} not implemented.') def _validate(net, val_data, ctx, eval_metric): """Test on validation dataset.""" @@ -94,7 +94,7 @@ def _validate(net, val_data, ctx, eval_metric): split_batch = rcnn_split_and_load(batch, ctx_list=ctx) clipper = gcv.nn.bbox.BBoxClipToImage() else: - raise NotImplementedError('%s not implemented.' % args.meta_arch) + raise NotImplementedError(f'{args.meta_arch} not implemented.') det_bboxes = [] det_ids = [] det_scores = [] @@ -107,7 +107,7 @@ def _validate(net, val_data, ctx, eval_metric): elif args.meta_arch == 'faster_rcnn': x, y, im_scale = data else: - raise NotImplementedError('%s not implemented.' % args.meta_arch) + raise NotImplementedError(f'{args.meta_arch} not implemented.') # get prediction results ids, scores, bboxes = net(x) det_ids.append(ids) @@ -121,7 +121,7 @@ def _validate(net, val_data, ctx, eval_metric): im_scale = im_scale.reshape((-1)).asscalar() det_bboxes[-1] *= im_scale else: - raise NotImplementedError('%s not implemented.' % args.meta_arch) + raise NotImplementedError(f'{args.meta_arch} not implemented.') # split ground truths gt_ids.append(y.slice_axis(axis=-1, begin=4, end=5)) gt_bboxes.append(y.slice_axis(axis=-1, begin=0, end=4)) @@ -135,10 +135,10 @@ def _validate(net, val_data, ctx, eval_metric): gt_difficults) elif args.meta_arch == 'faster_rcnn': for det_bbox, det_id, det_score, gt_bbox, gt_id, gt_diff in \ - zip(det_bboxes, det_ids, det_scores, gt_bboxes, gt_ids, gt_difficults): + zip(det_bboxes, det_ids, det_scores, gt_bboxes, gt_ids, gt_difficults): eval_metric.update(det_bbox, det_id, det_score, gt_bbox, gt_id, gt_diff) else: - raise NotImplementedError('%s not implemented.' % args.meta_arch) + raise NotImplementedError(f'{args.meta_arch} not implemented.') return eval_metric.get() if isinstance(dataset, AutoGluonObject): diff --git a/autogluon/task/object_detection/nets.py b/autogluon/task/object_detection/nets.py index 9f3e5741708c..9290359c4c12 100644 --- a/autogluon/task/object_detection/nets.py +++ b/autogluon/task/object_detection/nets.py @@ -37,7 +37,7 @@ def _get_network(meta_arch, name, transfer_classes, transfer, ctx=mx.cpu(), sync transfer=transfer, **kwargs) net.initialize(ctx=ctx, force_reinit=True) else: - raise NotImplementedError('%s not implemented.' % meta_arch) + raise NotImplementedError(f'{meta_arch} not implemented.') return net name = name.lower() diff --git a/autogluon/task/object_detection/object_detection.py b/autogluon/task/object_detection/object_detection.py index 6e7579818e6a..187c8f23a256 100644 --- a/autogluon/task/object_detection/object_detection.py +++ b/autogluon/task/object_detection/object_detection.py @@ -202,12 +202,7 @@ def fit(dataset='voc', >>> time_limits = 600, ngpus_per_trial = 1, num_trials = 1) """ assert search_strategy not in {'bayesopt', 'bayesopt_hyperband'}, \ - "search_strategy == 'bayesopt' or 'bayesopt_hyperband' not yet supported" - if auto_search: - # The strategies can be injected here, for example: automatic suggest some hps - # based on the dataset statistics - pass - + "search_strategy == 'bayesopt' or 'bayesopt_hyperband' not yet supported" nthreads_per_trial = get_cpu_count() if nthreads_per_trial > get_cpu_count() else nthreads_per_trial if ngpus_per_trial > get_gpu_count(): logger.warning( @@ -268,7 +263,7 @@ def fit(dataset='voc', scheduler_options = {'grace_period': grace_period} else: assert 'grace_period' not in scheduler_options, \ - "grace_period appears both in scheduler_options and as direct argument" + "grace_period appears both in scheduler_options and as direct argument" scheduler_options = copy.copy(scheduler_options) scheduler_options['grace_period'] = grace_period logger.warning( @@ -293,7 +288,7 @@ def fit(dataset='voc', train_object_detection, search_strategy, scheduler_options) logger.info(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> finish model fitting") args = sample_config(train_object_detection.args, results['best_config']) - logger.info('The best config: {}'.format(results['best_config'])) + logger.info(f"The best config: {results['best_config']}") ctx = [mx.gpu(i) for i in range(get_gpu_count())] model = get_network(args.meta_arch, args.net, dataset.init().get_classes(), transfer, ctx, diff --git a/autogluon/task/object_detection/pipeline.py b/autogluon/task/object_detection/pipeline.py index f678244202a3..82d7e7655742 100644 --- a/autogluon/task/object_detection/pipeline.py +++ b/autogluon/task/object_detection/pipeline.py @@ -107,7 +107,7 @@ def validate(net, val_data, ctx, eval_metric, args): split_batch = rcnn_split_and_load(batch, ctx_list=ctx) clipper = gcv.nn.bbox.BBoxClipToImage() else: - raise NotImplementedError('%s not implemented.' % args.meta_arch) + raise NotImplementedError(f'{args.meta_arch} not implemented.') det_bboxes = [] det_ids = [] det_scores = [] @@ -120,7 +120,7 @@ def validate(net, val_data, ctx, eval_metric, args): elif args.meta_arch == 'faster_rcnn': x, y, im_scale = data else: - raise NotImplementedError('%s not implemented.' % args.meta_arch) + raise NotImplementedError(f'{args.meta_arch} not implemented.') # get prediction results ids, scores, bboxes = net(x) det_ids.append(ids) @@ -134,7 +134,7 @@ def validate(net, val_data, ctx, eval_metric, args): im_scale = im_scale.reshape((-1)).asscalar() det_bboxes[-1] *= im_scale else: - raise NotImplementedError('%s not implemented.' % args.meta_arch) + raise NotImplementedError(f'{args.meta_arch} not implemented.') # split ground truths gt_ids.append(y.slice_axis(axis=-1, begin=4, end=5)) gt_bboxes.append(y.slice_axis(axis=-1, begin=0, end=4)) @@ -151,7 +151,7 @@ def validate(net, val_data, ctx, eval_metric, args): gt_ids, gt_difficults): eval_metric.update(det_bbox, det_id, det_score, gt_bbox, gt_id, gt_diff) else: - raise NotImplementedError('%s not implemented.' % args.meta_arch) + raise NotImplementedError(f'{args.meta_arch} not implemented.') return eval_metric.get() @@ -182,13 +182,13 @@ def train(net, train_data, val_data, eval_metric, ctx, args, reporter, final_fit raw_metrics, metrics = get_faster_rcnn_metrics() rpn_cls_loss, rpn_box_loss, rcnn_cls_loss, rcnn_box_loss = get_rcnn_losses(args) else: - raise NotImplementedError('%s not implemented.' % args.meta_arch) + raise NotImplementedError(f'{args.meta_arch} not implemented.') # set up logger logging.basicConfig() logger = logging.getLogger() logger.setLevel(logging.INFO) - log_file_path = args.save_prefix + '_train.log' + log_file_path = f'{args.save_prefix}_train.log' log_dir = os.path.dirname(log_file_path) if log_dir and not os.path.exists(log_dir): os.makedirs(log_dir) @@ -249,17 +249,19 @@ def train(net, train_data, val_data, eval_metric, ctx, args, reporter, final_fit if executor is not None: for data in zip(*batch): executor.put(data) - for j in range(len(ctx)): - if executor is not None: - result = executor.get() - else: - result = rcnn_task.forward_backward(list(zip(*batch))[0]) + for _ in range(len(ctx)): + result = ( + executor.get() + if executor is not None + else rcnn_task.forward_backward(list(zip(*batch))[0]) + ) + for k in range(len(raw_metrics_values)): raw_metrics_values[k].append(result[k]) for k in range(len(metrics_values)): metrics_values[k].append(result[len(metrics_values) + k]) else: - raise NotImplementedError('%s not implemented.' % args.meta_arch) + raise NotImplementedError(f'{args.meta_arch} not implemented.') trainer.step(batch_size) for metric, record in zip(raw_metrics, raw_metrics_values): @@ -281,9 +283,9 @@ def train(net, train_data, val_data, eval_metric, ctx, args, reporter, final_fit if (not (epoch + 1) % args.val_interval) and not final_fit: # consider reduce the frequency of validation to save time map_name, mean_ap = validate(net, val_data, ctx, eval_metric, args) - val_msg = ' '.join(['{}={}'.format(k, v) for k, v in zip(map_name, mean_ap)]) + val_msg = ' '.join([f'{k}={v}' for k, v in zip(map_name, mean_ap)]) # $tbar.set_description('[Epoch {}] Validation: {}'.format(epoch, val_msg)) - logger.info('[Epoch {}] Validation: {}'.format(epoch, val_msg)) + logger.info(f'[Epoch {epoch}] Validation: {val_msg}') current_map = float(mean_ap[-1]) pre_current_map = current_map else: diff --git a/autogluon/task/object_detection/utils.py b/autogluon/task/object_detection/utils.py index 77e3cbc77243..08fb5b9e87ea 100644 --- a/autogluon/task/object_detection/utils.py +++ b/autogluon/task/object_detection/utils.py @@ -59,10 +59,12 @@ def get_rcnn_losses(args): def rcnn_split_and_load(batch, ctx_list): """Split data to 1 batch each device.""" new_batch = [] - for i, data in enumerate(batch): - if isinstance(data, (list, tuple)): - new_data = [x.as_in_context(ctx) for x, ctx in zip(data, ctx_list)] - else: - new_data = [data.as_in_context(ctx_list[0])] + for data in batch: + new_data = ( + [x.as_in_context(ctx) for x, ctx in zip(data, ctx_list)] + if isinstance(data, (list, tuple)) + else [data.as_in_context(ctx_list[0])] + ) + new_batch.append(new_data) return new_batch diff --git a/autogluon/task/tabular_prediction/dataset.py b/autogluon/task/tabular_prediction/dataset.py index 7da231287cce..a9bc44a560a7 100644 --- a/autogluon/task/tabular_prediction/dataset.py +++ b/autogluon/task/tabular_prediction/dataset.py @@ -66,11 +66,11 @@ def _constructor_sliced(self): return pd.Series def __init__(self, *args, **kwargs): - file_path = kwargs.get('file_path', None) - name = kwargs.get('name', None) - feature_types = kwargs.get('feature_types', None) - df = kwargs.get('df', None) - subsample = kwargs.get('subsample', None) + file_path = kwargs.get('file_path') + name = kwargs.get('name') + feature_types = kwargs.get('feature_types') + df = kwargs.get('df') + subsample = kwargs.get('subsample') construct_from_df = False # whether or not we are constructing new dataset object from scratch based on provided DataFrame. # if df is None and file_path is None: # Cannot be used currently! # raise ValueError("Must specify either named argument 'file_path' or 'df' in order to construct tabular Dataset") diff --git a/autogluon/task/tabular_prediction/predictor.py b/autogluon/task/tabular_prediction/predictor.py index d6f567429175..46e3564f6ee3 100644 --- a/autogluon/task/tabular_prediction/predictor.py +++ b/autogluon/task/tabular_prediction/predictor.py @@ -186,7 +186,7 @@ def evaluate(self, dataset, silent=False): sign = self._learner.eval_metric._sign perf = perf * sign # flip negative once again back to positive (so higher is no longer necessarily better) if not silent: - print("Predictive performance on given dataset: %s = %s" % (self.eval_metric, perf)) + print(f"Predictive performance on given dataset: {self.eval_metric} = {perf}") return perf def evaluate_predictions(self, y_true, y_pred, silent=False, auxiliary_metrics=False, detailed_report=True): @@ -319,23 +319,23 @@ def fit_summary(self, verbosity=3): # self._summarize('model_performance', 'Validation performance of individual models', results) # self._summarize('model_best', 'Best model (based on validation performance)', results) # self._summarize('hyperparameter_tune', 'Hyperparameter-tuning used', results) - print("Number of models trained: %s" % len(results['model_performance'])) + print(f"Number of models trained: {len(results['model_performance'])}") print("Types of models trained:") print(unique_model_types) num_fold_str = "" bagging_used = results['num_bagging_folds'] > 0 if bagging_used: num_fold_str = f" (with {results['num_bagging_folds']} folds)" - print("Bagging used: %s %s" % (bagging_used, num_fold_str)) + print(f"Bagging used: {bagging_used} {num_fold_str}") num_stack_str = "" stacking_used = results['stack_ensemble_levels'] > 0 if stacking_used: num_stack_str = f" (with {results['stack_ensemble_levels']} levels)" - print("Stack-ensembling used: %s %s" % (stacking_used, num_stack_str)) + print(f"Stack-ensembling used: {stacking_used} {num_stack_str}") hpo_str = "" if hpo_used and verbosity <= 2: hpo_str = " (call fit_summary() with verbosity >= 3 to see detailed HPO info)" - print("Hyperparameter-tuning used: %s %s" % (hpo_used, hpo_str)) + print(f"Hyperparameter-tuning used: {hpo_used} {hpo_str}") # TODO: uncomment once feature_prune is functional: self._summarize('feature_prune', 'feature-selection used', results) print("User-specified hyperparameters:") print(results['hyperparameters_userspecified']) @@ -481,16 +481,16 @@ def transform_labels(self, labels, inverse=False, proba=False): """ if inverse: - if proba: - labels_transformed = self._learner.label_cleaner.inverse_transform_proba(y=labels) - else: - labels_transformed = self._learner.label_cleaner.inverse_transform(y=labels) + return ( + self._learner.label_cleaner.inverse_transform_proba(y=labels) + if proba + else self._learner.label_cleaner.inverse_transform(y=labels) + ) + + elif proba: + return self._learner.label_cleaner.transform_proba(y=labels) else: - if proba: - labels_transformed = self._learner.label_cleaner.transform_proba(y=labels) - else: - labels_transformed = self._learner.label_cleaner.transform(y=labels) - return labels_transformed + return self._learner.label_cleaner.transform(y=labels) # TODO: Consider adding time_limit option to early stop the feature importance process # TODO: Add option to specify list of features within features list, to check importances of groups of features. Make tuple to specify new feature name associated with group. @@ -549,10 +549,10 @@ def feature_importance(self, dataset=None, model=None, features=None, feature_st """ allowed_kwarg_names = {'raw'} - for kwarg_name in kwargs.keys(): + for kwarg_name in kwargs: if kwarg_name not in allowed_kwarg_names: - raise ValueError("Unknown keyword argument specified: %s" % kwarg_name) - if 'raw' in kwargs.keys(): + raise ValueError(f"Unknown keyword argument specified: {kwarg_name}") + if 'raw' in kwargs: logger.log(30, 'Warning: `raw` is a deprecated parameter. Use `feature_stage` instead. Starting from AutoGluon 0.1.0, specifying `raw` as a parameter will cause an exception.') logger.log(30, 'Overriding `feature_stage` value with `raw` value.') raw = kwargs['raw'] @@ -604,8 +604,7 @@ def refit_full(self, model='all'): ------- Dictionary of original model names -> refit_full model names. """ - refit_full_dict = self._learner.refit_ensemble_full(model=model) - return refit_full_dict + return self._learner.refit_ensemble_full(model=model) def get_model_best(self): """ @@ -707,11 +706,18 @@ def fit_weighted_ensemble(self, base_models: list = None, name_suffix='_custom', models_to_check = leaderboard['model'].tolist() for i in range(1, len(models_to_check) - 1): models_to_check_now = models_to_check[:i+1] - max_base_model_level = max([trainer.get_model_level(base_model) for base_model in models_to_check_now]) + max_base_model_level = max( + trainer.get_model_level(base_model) + for base_model in models_to_check_now + ) + weighted_ensemble_level = max_base_model_level + 1 models += trainer.generate_weighted_ensemble(X=X_train_stack_preds, y=y, level=weighted_ensemble_level, stack_name=stack_name, base_model_names=models_to_check_now, name_suffix=name_suffix + '_pareto' + str(i), time_limit=time_limits) - max_base_model_level = max([trainer.get_model_level(base_model) for base_model in base_models]) + max_base_model_level = max( + trainer.get_model_level(base_model) for base_model in base_models + ) + weighted_ensemble_level = max_base_model_level + 1 models += trainer.generate_weighted_ensemble(X=X_train_stack_preds, y=y, level=weighted_ensemble_level, stack_name=stack_name, base_model_names=base_models, name_suffix=name_suffix, time_limit=time_limits) diff --git a/autogluon/task/tabular_prediction/presets_configs.py b/autogluon/task/tabular_prediction/presets_configs.py index 2843b082df93..fce4c169c6af 100644 --- a/autogluon/task/tabular_prediction/presets_configs.py +++ b/autogluon/task/tabular_prediction/presets_configs.py @@ -62,11 +62,10 @@ def set_presets(**kwargs): preset = preset_dict.get(preset, None) if preset is None: raise ValueError(f'Preset \'{preset_orig}\' was not found. Valid presets: {list(preset_dict.keys())}') - if isinstance(preset, dict): - for key in preset: - preset_kwargs[key] = preset[key] - else: + if not isinstance(preset, dict): raise TypeError(f'Preset of type {type(preset)} was given, but only presets of type [dict, str] are valid.') + for key in preset: + preset_kwargs[key] = preset[key] for key in preset_kwargs: if key not in kwargs: kwargs[key] = preset_kwargs[key] diff --git a/autogluon/task/tabular_prediction/tabular_prediction.py b/autogluon/task/tabular_prediction/tabular_prediction.py index f99dcfd4063c..5f05e04574eb 100644 --- a/autogluon/task/tabular_prediction/tabular_prediction.py +++ b/autogluon/task/tabular_prediction/tabular_prediction.py @@ -444,7 +444,7 @@ def fit(train_data, >>> predictor = task.fit(train_data=train_data, label=label_column, eval_metric=eval_metric, auto_stack=True, time_limits=time_limits) """ assert search_strategy != 'bayesopt_hyperband', \ - "search_strategy == 'bayesopt_hyperband' not yet supported" + "search_strategy == 'bayesopt_hyperband' not yet supported" if verbosity < 0: verbosity = 0 elif verbosity > 4: @@ -468,9 +468,9 @@ def fit(train_data, 'random_seed', 'enable_fit_continuation' # TODO: Remove on 0.1.0 release } - for kwarg_name in kwargs.keys(): + for kwarg_name in kwargs: if kwarg_name not in allowed_kwarg_names: - raise ValueError("Unknown keyword argument specified: %s" % kwarg_name) + raise ValueError(f"Unknown keyword argument specified: {kwarg_name}") if isinstance(train_data, str): train_data = TabularDataset(file_path=train_data) @@ -493,7 +493,7 @@ def fit(train_data, cache_data = kwargs.get('cache_data', True) refit_full = kwargs.get('refit_full', False) # TODO: Remove on 0.1.0 release - if 'enable_fit_continuation' in kwargs.keys(): + if 'enable_fit_continuation' in kwargs: logger.log(30, 'Warning: `enable_fit_continuation` is a deprecated parameter. It has been renamed to `cache_data`. Starting from AutoGluon 0.1.0, specifying `enable_fit_continuation` as a parameter will cause an exception.') logger.log(30, 'Setting `cache_data` value equal to `enable_fit_continuation` value.') cache_data = kwargs['enable_fit_continuation'] @@ -515,7 +515,7 @@ def fit(train_data, dist_ip_addrs = [] if search_options is None: - search_options = dict() + search_options = {} if hyperparameters is None: hyperparameters = 'default' @@ -541,14 +541,10 @@ def fit(train_data, stack_ensemble_levels = min(1, max(0, math.floor(num_train_rows / 750))) if num_bagging_sets is None: - if num_bagging_folds >= 2: - if time_limits is not None: - num_bagging_sets = 20 - else: - num_bagging_sets = 1 + if num_bagging_folds >= 2 and time_limits is not None: + num_bagging_sets = 20 else: num_bagging_sets = 1 - label_count_threshold = kwargs.get('label_count_threshold', 10) if num_bagging_folds is not None: # Ensure there exist sufficient labels for stratified splits across all bags label_count_threshold = max(label_count_threshold, num_bagging_folds) @@ -628,8 +624,7 @@ def fit(train_data, if keep_only_best: predictor.delete_models(models_to_keep='best', dry_run=False) - save_space = kwargs.get('save_space', False) - if save_space: + if save_space := kwargs.get('save_space', False): predictor.save_space() return predictor diff --git a/autogluon/task/text_classification/dataset.py b/autogluon/task/text_classification/dataset.py index a74a94e38790..d70e7fd0d5c6 100644 --- a/autogluon/task/text_classification/dataset.py +++ b/autogluon/task/text_classification/dataset.py @@ -26,7 +26,7 @@ def get_dataset(name=None, *args, **kwargs): kwargs['label']: str or int Default: last column Index or column name of the label/target column. """ - path = kwargs.get('filepath', None) + path = kwargs.get('filepath') if path is not None: if path.endswith('.tsv') or path.endswith('.csv'): return CustomTSVClassificationTask(*args, **kwargs) @@ -219,7 +219,10 @@ def __init__(self, *args, **kwargs): self._read(**kwargs) self.is_pair = False self._label_column_id = self._index_label_column(**kwargs) - self.class_labels = list(set([sample[self._label_column_id] for sample in self.dataset])) + self.class_labels = list( + {sample[self._label_column_id] for sample in self.dataset} + ) + self.metric = Accuracy() super(CustomTSVClassificationTask, self).__init__(self.class_labels, self.metric, self.is_pair) @@ -243,7 +246,7 @@ def get_dataset(self, segment='train'): raise ValueError('Please specify either train or dev dataset.') def _read(self, **kwargs): - low_memory = kwargs.get('low_memory', None) + low_memory = kwargs.get('low_memory') if low_memory is None: kwargs['low_memory'] = False split_ratio = kwargs.pop('split_ratio', 0.8) @@ -265,20 +268,19 @@ def _read(self, **kwargs): self.train_sampler, self.dev_sampler = get_split_samplers(self.dataset, split_ratio=split_ratio) def _index_label_column(self, **kwargs): - label_column_id = kwargs.get('label', None) + label_column_id = kwargs.get('label') if label_column_id is None: warnings.warn('By default we are using the last column as the label column, ' 'if you wish to specify the label column by yourself or the column is not label column,' 'please specify label column using either the column name or index by using label=x.') return len(self.dataset[0]) - 1 if isinstance(label_column_id, str): - usecols = kwargs.get('usecols', None) + usecols = kwargs.get('usecols') if usecols is None: raise ValueError('Please specify the `usecols` when specifying the label column.') - else: - for i, col in enumerate(usecols): - if col == label_column_id: - return i + for i, col in enumerate(usecols): + if col == label_column_id: + return i elif isinstance(label_column_id, int): return label_column_id else: diff --git a/autogluon/task/text_classification/network.py b/autogluon/task/text_classification/network.py index 63d718c289d5..70abbbdfc220 100644 --- a/autogluon/task/text_classification/network.py +++ b/autogluon/task/text_classification/network.py @@ -11,12 +11,11 @@ def get_network(bert, class_labels, use_roberta=False): do_regression = not class_labels num_classes = 1 if do_regression else len(class_labels) - # reuse the BERTClassifier class with num_classes=1 for regression - if use_roberta: - model = RoBERTaClassifier(bert, dropout=0.0, num_classes=num_classes) - else: - model = BERTClassifier(bert, dropout=0.1, num_classes=num_classes) - return model + return ( + RoBERTaClassifier(bert, dropout=0.0, num_classes=num_classes) + if use_roberta + else BERTClassifier(bert, dropout=0.1, num_classes=num_classes) + ) class LMClassifier(gluon.Block): @@ -39,8 +38,7 @@ def forward(self, data, valid_length): # pylint: disable=arguments-differ masked_encoded = mx.ndarray.SequenceMask(encoded, sequence_length=valid_length, use_sequence_length=True) self.pool_out = mx.ndarray.broadcast_div(mx.ndarray.sum(masked_encoded, axis=0), mx.ndarray.expand_dims(valid_length, axis=1)) - out = self.classifier(self.pool_out) - return out + return self.classifier(self.pool_out) class BERTClassifier(gluon.Block): diff --git a/autogluon/task/text_classification/pipeline.py b/autogluon/task/text_classification/pipeline.py index e30659213639..f37b7d5c59d0 100644 --- a/autogluon/task/text_classification/pipeline.py +++ b/autogluon/task/text_classification/pipeline.py @@ -28,7 +28,7 @@ def preprocess_data(tokenizer, task, batch_size, dev_batch_size, max_len, vocab, pool = multiprocessing.Pool() nlp = try_import_gluonnlp() # transformation for data train and dev - label_dtype = 'float32' if not task.class_labels else 'int32' + label_dtype = 'int32' if task.class_labels else 'float32' trans = BERTDatasetTransform(tokenizer, max_len, vocab=vocab, class_labels=task.class_labels, @@ -205,8 +205,10 @@ def log_train(batch_id, batch_num, metric, step_loss, log_interval, epoch_id, le if not isinstance(metric_nm, list): metric_nm, metric_val = [metric_nm], [metric_val] - train_str = '[Epoch %d] loss=%.4f, lr=%.7f, metrics:' + \ - ','.join([i + ':%.4f' for i in metric_nm]) + train_str = '[Epoch %d] loss=%.4f, lr=%.7f, metrics:' + ','.join( + [f'{i}:%.4f' for i in metric_nm] + ) + tbar.set_description(train_str % (epoch_id, step_loss / log_interval, learning_rate, *metric_val)) def log_eval(batch_id, batch_num, metric, step_loss, log_interval, tbar): @@ -215,8 +217,7 @@ def log_eval(batch_id, batch_num, metric, step_loss, log_interval, tbar): if not isinstance(metric_nm, list): metric_nm, metric_val = [metric_nm], [metric_val] - eval_str = 'loss=%.4f, metrics:' + \ - ','.join([i + ':%.4f' for i in metric_nm]) + eval_str = ('loss=%.4f, metrics:' + ','.join([f'{i}:%.4f' for i in metric_nm])) tbar.set_description(eval_str % (step_loss / log_interval, *metric_val)) def evaluate(loader_dev, metric, segment): @@ -245,7 +246,7 @@ def evaluate(loader_dev, metric, segment): metric_nm, metric_val = metric.get() if not isinstance(metric_nm, list): metric_nm, metric_val = [metric_nm], [metric_val] - metric_str = 'validation metrics:' + ','.join([i + ':%.4f' for i in metric_nm]) + metric_str = 'validation metrics:' + ','.join([f'{i}:%.4f' for i in metric_nm]) logger.info(metric_str, *metric_val) mx.nd.waitall() @@ -324,7 +325,7 @@ def evaluate(loader_dev, metric, segment): if not accumulate or (batch_id + 1) % accumulate == 0: trainer.allreduce_grads() nlp.utils.clip_grad_global_norm(params, 1) - trainer.update(accumulate if accumulate else 1) + trainer.update(accumulate or 1) step_num += 1 if accumulate and accumulate > 1: # set grad to zero for gradient accumulation diff --git a/autogluon/task/text_classification/predictor.py b/autogluon/task/text_classification/predictor.py index 94887ac05a5d..1880b8f9f848 100644 --- a/autogluon/task/text_classification/predictor.py +++ b/autogluon/task/text_classification/predictor.py @@ -46,8 +46,7 @@ def predict(self, X): Int corresponding to index of the predicted class. """ proba = self.predict_proba(X) - ind = mx.nd.argmax(proba, axis=1).astype('int') - return ind + return mx.nd.argmax(proba, axis=1).astype('int') def predict_proba(self, X): """Predict class-probabilities of a given sentence / text-snippet. @@ -93,7 +92,7 @@ def evaluate(self, dataset, ctx=[mx.cpu()]): net = self.model if isinstance(dataset, AutoGluonObject): dataset = dataset.init() - if isinstance(dataset, AbstractGlueTask) or isinstance(dataset, AbstractCustomTask): + if isinstance(dataset, (AbstractGlueTask, AbstractCustomTask)): dataset = dataset.get_dataset('dev') if isinstance(ctx, list): ctx = ctx[0] @@ -122,15 +121,17 @@ def evaluate(self, dataset, ctx=[mx.cpu()]): def eval_func(model, loader_dev, metric, ctx, use_roberta): """Evaluate the model on validation dataset.""" metric.reset() - for batch_id, seqs in enumerate(loader_dev): + for seqs in loader_dev: input_ids, valid_length, segment_ids, label = seqs input_ids = input_ids.as_in_context(ctx) valid_length = valid_length.as_in_context(ctx).astype('float32') label = label.as_in_context(ctx) - if use_roberta: - out = model(input_ids, valid_length) - else: - out = model(input_ids, segment_ids.as_in_context(ctx), valid_length) + out = ( + model(input_ids, valid_length) + if use_roberta + else model(input_ids, segment_ids.as_in_context(ctx), valid_length) + ) + metric.update([label], [out]) metric_nm, metric_val = metric.get() diff --git a/autogluon/task/text_classification/text_classification.py b/autogluon/task/text_classification/text_classification.py index bc73cc83de98..34bf0a35950f 100644 --- a/autogluon/task/text_classification/text_classification.py +++ b/autogluon/task/text_classification/text_classification.py @@ -146,18 +146,13 @@ def fit(dataset='SST', >>> predictor = task.fit(dataset) """ assert search_strategy not in {'bayesopt', 'bayesopt_hyperband'}, \ - "search_strategy == 'bayesopt' or 'bayesopt_hyperband' not yet supported" + "search_strategy == 'bayesopt' or 'bayesopt_hyperband' not yet supported" logger.warning('`TextClassification` is in preview mode.' 'Please feel free to request new features in issues ' 'if it is not covered in the current implementation. ' 'If your dataset is in tabular format, you could also try out our `TabularPrediction` module.') - if auto_search: - # The strategies can be injected here, for example: automatic suggest some hps - # based on the dataset statistics - pass - nthreads_per_trial = get_cpu_count() if nthreads_per_trial > get_cpu_count() else nthreads_per_trial ngpus_per_trial = get_gpu_count() if ngpus_per_trial > get_gpu_count() else ngpus_per_trial @@ -197,7 +192,7 @@ def fit(dataset='SST', scheduler_options = {'grace_period': grace_period} else: assert 'grace_period' not in scheduler_options, \ - "grace_period appears both in scheduler_options and as direct argument" + "grace_period appears both in scheduler_options and as direct argument" scheduler_options = copy.copy(scheduler_options) scheduler_options['grace_period'] = grace_period logger.warning( diff --git a/autogluon/task/text_classification/transforms.py b/autogluon/task/text_classification/transforms.py index ba4a5e272158..c2bcd8919786 100644 --- a/autogluon/task/text_classification/transforms.py +++ b/autogluon/task/text_classification/transforms.py @@ -57,9 +57,7 @@ def __init__(self, self._label_dtype = 'int32' if class_labels else 'float32' self.vocab = vocab if has_label and class_labels: - self._label_map = {} - for (i, label) in enumerate(class_labels): - self._label_map[label] = i + self._label_map = {label: i for i, label in enumerate(class_labels)} if label_alias: for key in label_alias: self._label_map[key] = self._label_map[label_alias[key]] @@ -120,13 +118,12 @@ def __call__(self, line): np.array: classification task: label id in 'int32', shape (batch_size, 1), regression task: label in 'float32', shape (batch_size, 1) """ - if self.has_label: - input_ids, valid_length, segment_ids = self._bert_xform(line[:-1]) - label = line[-1] - # map to int if class labels are available - if self.class_labels: - label = self._label_map[label] - label = np.array([label], dtype=self._label_dtype) - return input_ids, valid_length, segment_ids, label - else: + if not self.has_label: return self._bert_xform(line) + input_ids, valid_length, segment_ids = self._bert_xform(line[:-1]) + label = line[-1] + # map to int if class labels are available + if self.class_labels: + label = self._label_map[label] + label = np.array([label], dtype=self._label_dtype) + return input_ids, valid_length, segment_ids, label diff --git a/autogluon/utils/augment.py b/autogluon/utils/augment.py index 6b86e0e7c24b..5cfcfe59f2ee 100644 --- a/autogluon/utils/augment.py +++ b/autogluon/utils/augment.py @@ -265,7 +265,7 @@ def low_high(name, prev_value): def rand_augment_list(): # 16 oeprations and their ranges - l = [ + return [ (AutoContrast, 0, 1), (Equalize, 0, 1), (Invert, 0, 1), @@ -277,15 +277,13 @@ def rand_augment_list(): # 16 oeprations and their ranges (Contrast, 0.1, 1.9), (Brightness, 0.1, 1.9), (Sharpness, 0.1, 1.9), - (ShearX, 0., 0.3), - (ShearY, 0., 0.3), + (ShearX, 0.0, 0.3), + (ShearY, 0.0, 0.3), (CutoutAbs, 0, 40), - (TranslateXabs, 0., 100), - (TranslateYabs, 0., 100), + (TranslateXabs, 0.0, 100), + (TranslateYabs, 0.0, 100), ] - return l - @autoaug2fastaa def autoaug_cifar10_policies(): diff --git a/autogluon/utils/dataloader.py b/autogluon/utils/dataloader.py index f90c6ca2f87b..14a5ee22c880 100644 --- a/autogluon/utils/dataloader.py +++ b/autogluon/utils/dataloader.py @@ -206,12 +206,12 @@ def __init__(self, dataset, batch_size=None, shuffle=False, sampler=None, self._thread_pool = thread_pool self._timeout = timeout self._sample_times = sample_times - assert timeout > 0, "timeout must be positive, given {}".format(timeout) + assert timeout > 0, f"timeout must be positive, given {timeout}" if batch_sampler is None: if batch_size is None: raise ValueError("batch_size must be specified unless " \ - "batch_sampler is specified") + "batch_sampler is specified") if sampler is None: if shuffle: sampler = _sampler.RandomSampler(len(dataset)) @@ -221,14 +221,16 @@ def __init__(self, dataset, batch_size=None, shuffle=False, sampler=None, raise ValueError("shuffle must not be specified if sampler is specified") batch_sampler = _sampler.BatchSampler( - sampler, batch_size, last_batch if last_batch else 'keep') + sampler, batch_size, last_batch or 'keep' + ) + elif batch_size is not None or shuffle or sampler is not None or \ - last_batch is not None: + last_batch is not None: raise ValueError("batch_size, shuffle, sampler and last_batch must " \ - "not be specified if batch_sampler is specified.") + "not be specified if batch_sampler is specified.") self._batch_sampler = batch_sampler - self._num_workers = num_workers if num_workers >= 0 else 0 + self._num_workers = max(num_workers, 0) self._worker_pool = None self._prefetch = max(0, int(prefetch) if prefetch is not None else 2 * self._num_workers) if self._num_workers > 0: diff --git a/autogluon/utils/dataset.py b/autogluon/utils/dataset.py index 8f65db9e1e7b..95c247a2c52e 100644 --- a/autogluon/utils/dataset.py +++ b/autogluon/utils/dataset.py @@ -13,7 +13,7 @@ def get_split_samplers(train_dataset, split_ratio=0.8): indices = list(range(num_samples)) np.random.seed(SPLIT_SEED) np.random.shuffle(indices) - train_sampler = SplitSampler(indices[0: split_idx]) + train_sampler = SplitSampler(indices[:split_idx]) val_sampler = SplitSampler(indices[split_idx:num_samples]) return train_sampler, val_sampler diff --git a/autogluon/utils/default_arguments.py b/autogluon/utils/default_arguments.py index 56cd4bbfa8b0..a53811a6b0b7 100644 --- a/autogluon/utils/default_arguments.py +++ b/autogluon/utils/default_arguments.py @@ -18,12 +18,17 @@ def __init__(self, lower: float = None, upper: float = None): self.upper = upper def assert_valid(self, key: str, value): - assert isinstance(value, numbers.Real), \ - "{}: Value = {} must be of type float".format(key, value) - assert (not self.lower) or value >= self.lower, \ - "{}: Value = {} must be >= {}".format(key, value, self.lower) - assert (not self.upper) or value <= self.upper, \ - "{}: Value = {} must be <= {}".format(key, value, self.upper) + assert isinstance( + value, numbers.Real + ), f"{key}: Value = {value} must be of type float" + + assert ( + not self.lower + ) or value >= self.lower, f"{key}: Value = {value} must be >= {self.lower}" + + assert ( + not self.upper + ) or value <= self.upper, f"{key}: Value = {value} must be <= {self.upper}" class Integer(CheckType): @@ -34,12 +39,17 @@ def __init__(self, lower: int = None, upper: int = None): self.upper = upper def assert_valid(self, key: str, value): - assert isinstance(value, numbers.Integral), \ - "{}: Value = {} must be of type int".format(key, value) - assert (not self.lower) or value >= self.lower, \ - "{}: Value = {} must be >= {}".format(key, value, self.lower) - assert (not self.upper) or value <= self.upper, \ - "{}: Value = {} must be <= {}".format(key, value, self.upper) + assert isinstance( + value, numbers.Integral + ), f"{key}: Value = {value} must be of type int" + + assert ( + not self.lower + ) or value >= self.lower, f"{key}: Value = {value} must be >= {self.lower}" + + assert ( + not self.upper + ) or value <= self.upper, f"{key}: Value = {value} must be <= {self.upper}" class Categorical(CheckType): @@ -47,8 +57,9 @@ def __init__(self, choices: Tuple[str, ...]): self.choices = set(choices) def assert_valid(self, key: str, value): - assert isinstance(value, str) and value in self.choices, \ - "{}: Value = {} must be in {}".format(key, value, self.choices) + assert ( + isinstance(value, str) and value in self.choices + ), f"{key}: Value = {value} must be in {self.choices}" class String(CheckType): @@ -58,8 +69,7 @@ def assert_valid(self, key: str, value): class Boolean(CheckType): def assert_valid(self, key: str, value): - assert isinstance(value, bool), \ - "{}: Value = {} must be boolean".format(key, value) + assert isinstance(value, bool), f"{key}: Value = {value} must be boolean" def check_and_merge_defaults( @@ -78,26 +88,23 @@ def check_and_merge_defaults( :param dict_name: :return: result_options """ - prefix = "" if dict_name is None else "{}: ".format(dict_name) + prefix = "" if dict_name is None else f"{dict_name}: " for key in mandatory: - assert key in options, \ - prefix + "Key '{}' is missing (but is mandatory)".format(key) + assert key in options, f"{prefix}Key '{key}' is missing (but is mandatory)" log_msg = "" result_options = { k: v for k, v in options.items() if v is not None} for key, value in default_options.items(): if key not in result_options: - log_msg += (prefix + "Key '{}': Imputing default value {}\n".format( - key, value)) + log_msg += f"{prefix}Key '{key}': Imputing default value {value}\n" result_options[key] = value if log_msg: logger.info(log_msg) # Check constraints if constraints: for key, value in result_options.items(): - check = constraints.get(key) - if check: - check.assert_valid(prefix + "Key '{}'".format(key), value) + if check := constraints.get(key): + check.assert_valid(f"{prefix}Key '{key}'", value) return result_options @@ -118,5 +125,4 @@ def filter_by_key(options: dict, remove_keys: Set[str]) -> dict: def assert_no_invalid_options(options: dict, all_keys: Set[str], name: str): for k in options: - assert k in all_keys, \ - "{}: Invalid argument '{}'".format(name, k) + assert k in all_keys, f"{name}: Invalid argument '{k}'" diff --git a/autogluon/utils/defaultdict.py b/autogluon/utils/defaultdict.py index 7756be092bdc..30fe9c1cf1c5 100644 --- a/autogluon/utils/defaultdict.py +++ b/autogluon/utils/defaultdict.py @@ -4,6 +4,5 @@ class keydefaultdict(defaultdict): def __missing__(self, key): if self.default_factory is None: raise KeyError(key) - else: - ret = self[key] = self.default_factory(key) - return ret + ret = self[key] = self.default_factory(key) + return ret diff --git a/autogluon/utils/deprecate.py b/autogluon/utils/deprecate.py index 8f62bcf08801..1b0f197279f5 100644 --- a/autogluon/utils/deprecate.py +++ b/autogluon/utils/deprecate.py @@ -40,8 +40,10 @@ def __init__(self, new_class, new_name): self.old_name = new_name def _warn(self): - warn("autogluon.{} is now deprecated in favor of autogluon.{}." \ - .format(self.old_name, self.new_name), AutoGluonWarning) + warn( + f"autogluon.{self.old_name} is now deprecated in favor of autogluon.{self.new_name}.", + AutoGluonWarning, + ) def __call__(self, *args, **kwargs): self._warn() diff --git a/autogluon/utils/edict.py b/autogluon/utils/edict.py index 120769306c53..172223262566 100644 --- a/autogluon/utils/edict.py +++ b/autogluon/utils/edict.py @@ -9,7 +9,10 @@ def __init__(self, d=None, **kwargs): setattr(self, k, v) # Class attributes for k in self.__class__.__dict__.keys(): - if not (k.startswith('__') and k.endswith('__')) and not k in ('update', 'pop'): + if not (k.startswith('__') and k.endswith('__')) and k not in ( + 'update', + 'pop', + ): setattr(self, k, getattr(self, k)) def __setattr__(self, name, value): @@ -24,7 +27,7 @@ def __setattr__(self, name, value): __setitem__ = __setattr__ def update(self, e=None, **f): - d = e or dict() + d = e or {} d.update(f) for k in d: setattr(self, k, d[k]) diff --git a/autogluon/utils/file_helper.py b/autogluon/utils/file_helper.py index 14fef388c1a5..26dec5bbfd12 100644 --- a/autogluon/utils/file_helper.py +++ b/autogluon/utils/file_helper.py @@ -84,10 +84,12 @@ def generate_csv_submission(dataset_path, dataset, local_path, inds, preds, clas save_csv_path = os.path.join(local_path, dataset, save_csv_name) if csv_config['need_sample']: # plant\fish\dog df = pd.read_csv(csv_path) - if not csv_config['fullname']: - imagename_list = [name_id[:-4] for name_id in ids] - else: - imagename_list = ids + imagename_list = ( + ids + if csv_config['fullname'] + else [name_id[:-4] for name_id in ids] + ) + row_index_group = [] for i in imagename_list: if csv_config['content'] == 'str': @@ -141,10 +143,7 @@ def get_name(name): print('generate_csv B is done') def filter_value(prob, Threshold): - if prob > Threshold: - prob = prob - else: - prob = 0 + prob = prob if prob > Threshold else 0 return prob def generate_prob_csv(test_dataset, preds, set_prob_thresh=0, ensemble_list='', custom='./submission.csv', scale_min_max=True): @@ -159,7 +158,7 @@ def generate_prob_csv(test_dataset, preds, set_prob_thresh=0, ensemble_list='', row_index_group = [] for i in imagename_list: row_index = df[df['id'] == str(i)].index.tolist() - if not len(row_index) == 0: + if len(row_index) != 0: row_index_group.append(row_index[0]) df.loc[row_index_group, 1:] = preds df.to_csv(custom, index=False) @@ -174,7 +173,8 @@ def ensemble_csv(glob_files): for i in w.columns.values: w[i] = w[i].apply(filter_value, Threshold=set_prob_thresh) w.to_csv(custom) - if not ensemble_list.strip() == '': + + if ensemble_list.strip() != '': ensemble_csv(ensemble_list) print('dog_generate_csv is done') @@ -183,10 +183,8 @@ def generate_csv(inds, path): row = ['id', 'category'] writer = csv.writer(csvFile) writer.writerow(row) - id = 1 - for ind in inds: + for id, ind in enumerate(inds, start=1): row = [id, ind] writer = csv.writer(csvFile) writer.writerow(row) - id += 1 csvFile.close() diff --git a/autogluon/utils/files.py b/autogluon/utils/files.py index d9f3c25ebd4f..e83d14266748 100644 --- a/autogluon/utils/files.py +++ b/autogluon/utils/files.py @@ -50,20 +50,16 @@ def download(url, path=None, overwrite=False, sha1_hash=None): fname = url.split('/')[-1] else: path = os.path.expanduser(path) - if os.path.isdir(path): - fname = os.path.join(path, url.split('/')[-1]) - else: - fname = path - + fname = os.path.join(path, url.split('/')[-1]) if os.path.isdir(path) else path if overwrite or not os.path.exists(fname) or (sha1_hash and not check_sha1(fname, sha1_hash)): dirname = os.path.dirname(os.path.abspath(os.path.expanduser(fname))) if not os.path.exists(dirname): os.makedirs(dirname) - logger.info('Downloading %s from %s...'%(fname, url)) + logger.info(f'Downloading {fname} from {url}...') r = requests.get(url, stream=True) if r.status_code != 200: - raise RuntimeError("Failed downloading url %s"%url) + raise RuntimeError(f"Failed downloading url {url}") total_length = r.headers.get('content-length') with open(fname, 'wb') as f: if total_length is None: # no content length header @@ -78,10 +74,10 @@ def download(url, path=None, overwrite=False, sha1_hash=None): f.write(chunk) if sha1_hash and not check_sha1(fname, sha1_hash): - raise UserWarning('File {} is downloaded but the content hash does not match. ' \ - 'The repo may be outdated or download may be incomplete. ' \ - 'If the "repo_url" is overridden, consider switching to ' \ - 'the default repo.'.format(fname)) + raise UserWarning( + f'File {fname} is downloaded but the content hash does not match. The repo may be outdated or download may be incomplete. If the "repo_url" is overridden, consider switching to the default repo.' + ) + return fname @@ -104,11 +100,11 @@ def check_sha1(filename, sha1_hash): sha1 = hashlib.sha1() with open(filename, 'rb') as f: while True: - data = f.read(1048576) - if not data: - break - sha1.update(data) + if data := f.read(1048576): + sha1.update(data) + else: + break return sha1.hexdigest() == sha1_hash @@ -118,9 +114,7 @@ def mkdir(path): try: os.makedirs(path) except OSError as exc: # Python >2.5 - if exc.errno == errno.EEXIST and os.path.isdir(path): - pass - else: + if exc.errno != errno.EEXIST or not os.path.isdir(path): raise def raise_num_file(nofile_atleast=4096): @@ -145,9 +139,12 @@ def raise_num_file(nofile_atleast=4096): res.setrlimit(res.RLIMIT_NOFILE,(soft,hard)) except (ValueError,res.error): try: - hard = soft - logger.warning('trouble with max limit, retrying with soft,hard {},{}'.format(soft,hard)) - res.setrlimit(res.RLIMIT_NOFILE,(soft,hard)) + hard = soft + logger.warning( + f'trouble with max limit, retrying with soft,hard {soft},{hard}' + ) + + res.setrlimit(res.RLIMIT_NOFILE,(soft,hard)) except Exception: logger.warning('failed to set ulimit') soft,hard = res.getrlimit(res.RLIMIT_NOFILE) diff --git a/autogluon/utils/learning_rate.py b/autogluon/utils/learning_rate.py index 9e0a48a03c8e..2365de5605c4 100644 --- a/autogluon/utils/learning_rate.py +++ b/autogluon/utils/learning_rate.py @@ -3,7 +3,7 @@ class LR_params: def __init__(self, *args): lr, lr_mode, num_epochs, num_batches, lr_decay_epoch, \ - lr_decay, lr_decay_period, warmup_epochs, warmup_lr= args + lr_decay, lr_decay_period, warmup_epochs, warmup_lr= args self._num_batches = num_batches self._lr_decay = lr_decay self._lr_decay_period = lr_decay_period diff --git a/autogluon/utils/miscs.py b/autogluon/utils/miscs.py index ed1a54ae57ea..0f55a85c3f1c 100644 --- a/autogluon/utils/miscs.py +++ b/autogluon/utils/miscs.py @@ -7,11 +7,8 @@ def in_ipynb(): if 'AG_DOCS' in os.environ and os.environ['AG_DOCS']: return False try: - cfg = get_ipython().config - if 'IPKernelApp' in cfg: - return True - else: - return False + cfg = get_ipython().config + return 'IPKernelApp' in cfg except NameError: return False @@ -32,14 +29,12 @@ def verbosity2loglevel(verbosity): if verbosity <= 0: # only errors warnings.filterwarnings("ignore") # print("Caution: all warnings suppressed") - log_level = 40 + return 40 elif verbosity == 1: # only warnings and critical print statements - log_level = 25 + return 25 elif verbosity == 2: # key print statements which should be shown by default - log_level = 20 + return 20 elif verbosity == 3: # more-detailed printing - log_level = 15 + return 15 else: - log_level = 10 # print everything (ie. debug mode) - - return log_level \ No newline at end of file + return 10 \ No newline at end of file diff --git a/autogluon/utils/mxutils.py b/autogluon/utils/mxutils.py index 4893e6c85761..c4ec6e08fdbb 100644 --- a/autogluon/utils/mxutils.py +++ b/autogluon/utils/mxutils.py @@ -17,8 +17,7 @@ def update_params(net, params, multi_precision=False, ctx=mx.cpu(0)): def collect_params(net): params = net._collect_params_with_prefix() - param_dict = {key : val._reduce() for key, val in params.items()} - return param_dict + return {key : val._reduce() for key, val in params.items()} def get_data_rec(input_size, crop_ratio, rec_file, rec_file_idx, @@ -36,51 +35,49 @@ def get_data_rec(input_size, crop_ratio, mean_rgb = [123.68, 116.779, 103.939] std_rgb = [58.393, 57.12, 57.375] - if train: - data_loader = mx.io.ImageRecordIter( - path_imgrec = rec_file, - path_imgidx = rec_file_idx, - preprocess_threads = num_workers, - shuffle = True, - batch_size = batch_size, - - data_shape = (3, input_size, input_size), - mean_r = mean_rgb[0], - mean_g = mean_rgb[1], - mean_b = mean_rgb[2], - std_r = std_rgb[0], - std_g = std_rgb[1], - std_b = std_rgb[2], - rand_mirror = True, - random_resized_crop = True, - max_aspect_ratio = 4. / 3., - min_aspect_ratio = 3. / 4., - max_random_area = 1, - min_random_area = 0.08, - max_rotate_angle = max_rotate_angle, - brightness = jitter_param, - saturation = jitter_param, - contrast = jitter_param, - pca_noise = lighting_param, + return ( + mx.io.ImageRecordIter( + path_imgrec=rec_file, + path_imgidx=rec_file_idx, + preprocess_threads=num_workers, + shuffle=True, + batch_size=batch_size, + data_shape=(3, input_size, input_size), + mean_r=mean_rgb[0], + mean_g=mean_rgb[1], + mean_b=mean_rgb[2], + std_r=std_rgb[0], + std_g=std_rgb[1], + std_b=std_rgb[2], + rand_mirror=True, + random_resized_crop=True, + max_aspect_ratio=4.0 / 3.0, + min_aspect_ratio=3.0 / 4.0, + max_random_area=1, + min_random_area=0.08, + max_rotate_angle=max_rotate_angle, + brightness=jitter_param, + saturation=jitter_param, + contrast=jitter_param, + pca_noise=lighting_param, ) - else: - data_loader = mx.io.ImageRecordIter( - path_imgrec = rec_file, - path_imgidx = rec_file_idx, - preprocess_threads = num_workers, - shuffle = False, - batch_size = batch_size, - - resize = resize, - data_shape = (3, input_size, input_size), - mean_r = mean_rgb[0], - mean_g = mean_rgb[1], - mean_b = mean_rgb[2], - std_r = std_rgb[0], - std_g = std_rgb[1], - std_b = std_rgb[2], + if train + else mx.io.ImageRecordIter( + path_imgrec=rec_file, + path_imgidx=rec_file_idx, + preprocess_threads=num_workers, + shuffle=False, + batch_size=batch_size, + resize=resize, + data_shape=(3, input_size, input_size), + mean_r=mean_rgb[0], + mean_g=mean_rgb[1], + mean_b=mean_rgb[2], + std_r=std_rgb[0], + std_g=std_rgb[1], + std_b=std_rgb[2], ) - return data_loader + ) def read_remote_ips(filename): ip_addrs = [] diff --git a/autogluon/utils/openml_download.py b/autogluon/utils/openml_download.py index c8a7234d8759..8c2371352ad8 100644 --- a/autogluon/utils/openml_download.py +++ b/autogluon/utils/openml_download.py @@ -26,8 +26,8 @@ def load_and_split_openml_data( # download from OpenML, which fails too often src_url = 'https://autogluon.s3.amazonaws.com/' trg_path = './' - data_path = 'org/openml/www/datasets/{}/'.format(openml_task_id) - task_path = 'org/openml/www/tasks/{}/'.format(openml_task_id) + data_path = f'org/openml/www/datasets/{openml_task_id}/' + task_path = f'org/openml/www/tasks/{openml_task_id}/' data_files = [ data_path + x for x in [ 'dataset.arff', 'dataset.pkl.py3', 'description.xml', diff --git a/autogluon/utils/pil_transforms.py b/autogluon/utils/pil_transforms.py index 5b40fc09b944..9e345e7de396 100644 --- a/autogluon/utils/pil_transforms.py +++ b/autogluon/utils/pil_transforms.py @@ -30,7 +30,7 @@ def __call__(self, img): return img def __repr__(self): - format_string = self.__class__.__name__ + '(' + format_string = f'{self.__class__.__name__}(' for t in self.transforms: format_string += '\n' format_string += ' {0}'.format(t) @@ -41,13 +41,11 @@ class ToPIL(object): """Convert image from ndarray format to PIL """ def __call__(self, img): - x = Image.fromarray(img.asnumpy()) - return x + return Image.fromarray(img.asnumpy()) class ToNDArray(object): def __call__(self, img): - x = mx.nd.array(np.array(img), mx.cpu(0)) - return x + return mx.nd.array(np.array(img), mx.cpu(0)) class ToTensor(object): def __call__(self, img): @@ -68,10 +66,7 @@ class RandomResizedCrop(object): """ def __init__(self, size, scale=(0.08, 1.0), ratio=(3. / 4., 4. / 3.), interpolation=Image.BILINEAR): - if isinstance(size, tuple): - self.size = size - else: - self.size = (size, size) + self.size = size if isinstance(size, tuple) else (size, size) if (scale[0] > scale[1]) or (ratio[0] > ratio[1]): warnings.warn("range should be of kind (min, max)") @@ -84,7 +79,7 @@ def get_params(img, scale, ratio): width, height = img.size area = height * width - for attempt in range(10): + for _ in range(10): target_area = random.uniform(*scale) * area log_ratio = (math.log(ratio[0]), math.log(ratio[1])) aspect_ratio = math.exp(random.uniform(*log_ratio)) @@ -251,7 +246,7 @@ def __call__(self, img): return img def __repr__(self): - return self.__class__.__name__ + '(p={})'.format(self.p) + return f'{self.__class__.__name__}(p={self.p})' class Lambda(object): """Apply a user-defined lambda as a transform. @@ -260,14 +255,14 @@ class Lambda(object): """ def __init__(self, lambd): - assert callable(lambd), repr(type(lambd).__name__) + " object is not callable" + assert callable(lambd), f"{repr(type(lambd).__name__)} object is not callable" self.lambd = lambd def __call__(self, img): return self.lambd(img) def __repr__(self): - return self.__class__.__name__ + '()' + return f'{self.__class__.__name__}()' class ColorJitter(object): """Randomly change the brightness, contrast and saturation of an image. @@ -295,15 +290,18 @@ def __init__(self, brightness=0, contrast=0, saturation=0, hue=0): def _check_input(self, value, name, center=1, bound=(0, float('inf')), clip_first_on_zero=True): if isinstance(value, numbers.Number): if value < 0: - raise ValueError("If {} is a single number, it must be non negative.".format(name)) + raise ValueError(f"If {name} is a single number, it must be non negative.") value = [center - value, center + value] if clip_first_on_zero: value[0] = max(value[0], 0) elif isinstance(value, (tuple, list)) and len(value) == 2: if not bound[0] <= value[0] <= value[1] <= bound[1]: - raise ValueError("{} values should be between {}".format(name, bound)) + raise ValueError(f"{name} values should be between {bound}") else: - raise TypeError("{} should be a single number or a list/tuple with lenght 2.".format(name)) + raise TypeError( + f"{name} should be a single number or a list/tuple with lenght 2." + ) + # if value is 0 or (1., 1.) for brightness/contrast/saturation # or (0., 0.) for hue, do nothing @@ -338,9 +336,7 @@ def get_params(brightness, contrast, saturation, hue): transforms.append(Lambda(lambda img: adjust_hue(img, hue_factor))) random.shuffle(transforms) - transform = Compose(transforms) - - return transform + return Compose(transforms) def __call__(self, img): """ @@ -354,7 +350,7 @@ def __call__(self, img): return transform(img) def __repr__(self): - format_string = self.__class__.__name__ + '(' + format_string = f'{self.__class__.__name__}(' format_string += 'brightness={0}'.format(self.brightness) format_string += ', contrast={0}'.format(self.contrast) format_string += ', saturation={0}'.format(self.saturation) @@ -432,23 +428,23 @@ def crop(img, top, left, height, width): return img.crop((left, top, left + width, top + height)) def resize(img, size, interpolation=Image.BILINEAR): - if not (isinstance(size, int) or (isinstance(size, collections.Iterable) and len(size) == 2)): - raise TypeError('Got inappropriate size arg: {}'.format(size)) + if not isinstance(size, int) and ( + not isinstance(size, collections.Iterable) or len(size) != 2 + ): + raise TypeError(f'Got inappropriate size arg: {size}') - if isinstance(size, int): - w, h = img.size - if (w <= h and w == size) or (h <= w and h == size): - return img - if w < h: - ow = size - oh = int(size * h / w) - return img.resize((ow, oh), interpolation) - else: - oh = size - ow = int(size * w / h) - return img.resize((ow, oh), interpolation) - else: + if not isinstance(size, int): return img.resize(size[::-1], interpolation) + w, h = img.size + if (w <= h and w == size) or (h <= w and h == size): + return img + if w < h: + ow = size + oh = int(size * h / w) + else: + oh = size + ow = int(size * w / h) + return img.resize((ow, oh), interpolation) def adjust_brightness(img, brightness_factor): enhancer = ImageEnhance.Brightness(img) @@ -492,8 +488,10 @@ def pad(img, padding, fill=0, padding_mode='constant'): raise TypeError('Got inappropriate padding_mode arg') if isinstance(padding, Sequence) and len(padding) not in [2, 4]: - raise ValueError("Padding must be an int or a 2, or 4 element tuple, not a " + - "{} element tuple".format(len(padding))) + raise ValueError( + f"Padding must be an int or a 2, or 4 element tuple, not a {len(padding)} element tuple" + ) + assert padding_mode in ['constant', 'edge', 'reflect', 'symmetric'], \ 'Padding mode should be either constant, edge, reflect or symmetric' diff --git a/autogluon/utils/plot_network.py b/autogluon/utils/plot_network.py index a10f47479440..2904903e5b82 100644 --- a/autogluon/utils/plot_network.py +++ b/autogluon/utils/plot_network.py @@ -24,7 +24,7 @@ def plot_network(block, shape=(1, 3, 224, 224), savefile=False): if graphviz is None: raise RuntimeError("Cannot import graphviz.") if not isinstance(block, mx.gluon.HybridBlock): - raise ValueError("block must be HybridBlock, given {}".format(type(block))) + raise ValueError(f"block must be HybridBlock, given {type(block)}") data = mx.sym.var('data') sym = block(data) if isinstance(sym, tuple): diff --git a/autogluon/utils/plots.py b/autogluon/utils/plots.py index 8a8ff4baccad..992e9a4ca5d3 100644 --- a/autogluon/utils/plots.py +++ b/autogluon/utils/plots.py @@ -17,24 +17,22 @@ def plot_performance_vs_trials(results, output_directory, save_file="Performance matplotlib_imported = True except ImportError: matplotlib_imported = False - + if not matplotlib_imported: warnings.warn('AutoGluon summary plots cannot be created because matplotlib is not installed. Please do: "pip install matplotlib"') return None - + ordered_trials = sorted(list(results['trial_info'].keys())) ordered_val_perfs = [results['trial_info'][trial_id][results['reward_attr']] for trial_id in ordered_trials] x = range(1, len(ordered_trials)+1) - y = [] - for i in x: - y.append(max([ordered_val_perfs[j] for j in range(i)])) # best validation performance in trials up until ith one (assuming higher = better) + y = [max(ordered_val_perfs[j] for j in range(i)) for i in x] fig, ax = plt.subplots() ax.plot(x, y) ax.set(xlabel='Completed Trials', ylabel='Best Performance', title = plot_title) if output_directory is not None: outputfile = os.path.join(output_directory, save_file) fig.savefig(outputfile) - print("Plot of HPO performance saved to file: %s" % outputfile) + print(f"Plot of HPO performance saved to file: {outputfile}") plt.show() def plot_summary_of_models(results, output_directory, save_file='SummaryOfModels.html', plot_title="Models produced during fit()"): @@ -59,7 +57,7 @@ def plot_summary_of_models(results, output_directory, save_file='SummaryOfModels if 'memory' in results['metadata']: datadict['memory'] = [results['trial_info'][trial_id]['metadata']['memory'] for trial_id in datadict['trial_id']] attr_size = 'memory' - + # Determine color attribute: if 'training_loss' in results: datadict['training_loss'] = [results['trial_info'][trial_id]['training_loss'] for trial_id in datadict['trial_id']] @@ -69,7 +67,7 @@ def plot_summary_of_models(results, output_directory, save_file='SummaryOfModels mousover_plot(datadict, attr_x=attr_x, attr_y='performance', attr_color=attr_color, attr_size=attr_size, save_file=save_path, plot_title=plot_title, hidden_keys=hidden_keys) if save_path is not None: - print("Plot summary of models saved to file: %s" % save_file) + print(f"Plot summary of models saved to file: {save_file}") def plot_tabular_models(results, output_directory=None, save_file="SummaryOfModels.html", plot_title="Models produced during fit()"): """ Plot dynamic scatterplot of every single model trained during tabular_prediction.fit() @@ -79,12 +77,11 @@ def plot_tabular_models(results, output_directory=None, save_file="SummaryOfMode Must at least contain key: 'model_performance'. """ save_path = output_directory + save_file if output_directory else None - hidden_keys = [] model_performancedict = results['model_performance'] model_names = list(model_performancedict.keys()) val_perfs = [model_performancedict[key] for key in model_names] model_types = [results['model_types'][key] for key in model_names] - hidden_keys.append(model_types) + hidden_keys = [model_types] model_hyperparams = [_formatDict(results['model_hyperparams'][key]) for key in model_names] datadict = {'performance': val_perfs, 'model': model_names, 'model_type': model_types, 'hyperparameters': model_hyperparams} hpo_used = results['hyperparameter_tune'] @@ -105,7 +102,7 @@ def _formatDict(d): """ Returns dict as string with HTML new-line tags
between key-value pairs. """ s = '' for key in d: - new_s = str(key) + ": " + str(d[key]) + "
" + new_s = f"{str(key)}: {str(d[key])}
" s += new_s return s[:-4] @@ -136,27 +133,27 @@ def mousover_plot(datadict, attr_x, attr_y, attr_color=None, attr_size=None, sav bokeh_imported = True except ImportError: bokeh_imported = False - + if not bokeh_imported: warnings.warn('AutoGluon summary plots cannot be created because bokeh is not installed. To see plots, please do: "pip install bokeh==2.0.1"') return None - + n = len(datadict[attr_x]) for key in datadict.keys(): # Check lengths are all the same if len(datadict[key]) != n: - raise ValueError("Key %s in datadict has different length than %s" % (key, attr_x)) - - attr_x_is_string = any([type(val)==str for val in datadict[attr_x]]) + raise ValueError(f"Key {key} in datadict has different length than {attr_x}") + + attr_x_is_string = any(type(val)==str for val in datadict[attr_x]) if attr_x_is_string: attr_x_levels = list(set(datadict[attr_x])) # use this to translate between int-indices and x-values og_x_vals = datadict[attr_x][:] - attr_x2 = attr_x + "___" # this key must not already be in datadict. + attr_x2 = f"{attr_x}___" hidden_keys.append(attr_x2) datadict[attr_x2] = [attr_x_levels.index(category) for category in og_x_vals] # convert to ints - + legend = None if attr_color is not None: - attr_color_is_string = any([type(val)==str for val in datadict[attr_color]]) + attr_color_is_string = any(type(val)==str for val in datadict[attr_color]) color_datavals = datadict[attr_color] if attr_color_is_string: attr_color_levels = list(set(color_datavals)) @@ -166,9 +163,9 @@ def mousover_plot(datadict, attr_x, attr_y, attr_color=None, attr_size=None, sav else: color_mapper = LinearColorMapper(palette='Magma256', low=min(datadict[attr_color]), high=max(datadict[attr_color])*1.25) default_color = {'field': attr_color, 'transform': color_mapper} - + if attr_size is not None: # different size for each point, ensure mean-size == point_size - attr_size2 = attr_size + "____" + attr_size2 = f"{attr_size}____" hidden_keys.append(attr_size2) og_sizevals = np.array(datadict[attr_size]) sizevals = point_size + (og_sizevals - np.mean(og_sizevals))/np.std(og_sizevals) * (point_size/2) @@ -176,11 +173,11 @@ def mousover_plot(datadict, attr_x, attr_y, attr_color=None, attr_size=None, sav sizevals = -np.min(sizevals) + sizevals + 1.0 datadict[attr_size2] = list(sizevals) point_size = attr_size2 - + if save_file is not None: output_file(save_file, title=plot_title) - print("Plot summary of models saved to file: %s" % save_file) - + print(f"Plot summary of models saved to file: {save_file}") + source = ColumnDataSource(datadict) TOOLS="crosshair,pan,wheel_zoom,box_zoom,reset,hover,save" p = figure(title=plot_title, tools=TOOLS) @@ -191,29 +188,42 @@ def mousover_plot(datadict, attr_x, attr_y, attr_color=None, attr_size=None, sav circ = p.circle(attr_x, attr_y, line_color=default_color, line_alpha = point_transparency, fill_color = default_color, fill_alpha=point_transparency, size=point_size, source=source) hover = p.select(dict(type=HoverTool)) - hover.tooltips = OrderedDict([(key,'@'+key+'{safe}') for key in datadict.keys() if key not in hidden_keys]) + hover.tooltips = OrderedDict( + [ + (key, f'@{key}' + '{safe}') + for key in datadict.keys() + if key not in hidden_keys + ] + ) + # Format axes: p.xaxis.axis_label = attr_x p.yaxis.axis_label = attr_y if attr_x_is_string: # add x-ticks: p.xaxis.ticker = list(range(len(attr_x_levels))) p.xaxis.major_label_overrides = {i: attr_x_levels[i] for i in range(len(attr_x_levels))} - + # Legend additions: if attr_color is not None and attr_color_is_string: - legend_it = [] - for i in range(len(attr_color_levels)): - legend_it.append(LegendItem(label=attr_color_levels[i], renderers = [circ], index=datadict[attr_color].index(attr_color_levels[i]))) + legend_it = [ + LegendItem( + label=attr_color_levels[i], + renderers=[circ], + index=datadict[attr_color].index(attr_color_levels[i]), + ) + for i in range(len(attr_color_levels)) + ] + legend = Legend(items=legend_it, location=(0, 0)) p.add_layout(legend, 'right') - + if attr_color is not None and not attr_color_is_string: color_bar = ColorBar(color_mapper=color_mapper, title = attr_color, label_standoff=12, border_line_color=None, location=(0,0)) p.add_layout(color_bar, 'right') - + if attr_size is not None: p.add_layout(Legend(items=[LegendItem(label='Size of points based on "'+attr_size + '"')]), 'below') - + show(p) diff --git a/autogluon/utils/serialization.py b/autogluon/utils/serialization.py index 36490cdb5c74..aaa0a512ddcb 100644 --- a/autogluon/utils/serialization.py +++ b/autogluon/utils/serialization.py @@ -147,9 +147,8 @@ def _check_seekable(f): def raise_err_msg(patterns, e): for p in patterns: if p in str(e): - msg = (str(e) + ". You can only load from a file that is seekable." + - " Please pre-load the data into a buffer like io.BytesIO and" + - " try to load from it instead.") + msg = f"{str(e)}. You can only load from a file that is seekable. Please pre-load the data into a buffer like io.BytesIO and try to load from it instead." + raise type(e)(msg) raise e @@ -225,7 +224,7 @@ def _check_container_source(container_type, source_file, original_source): return if original_source != current_source: if container_type.dump_patches: - file_name = container_type.__name__ + '.patch' + file_name = f'{container_type.__name__}.patch' diff = difflib.unified_diff(current_source.split('\n'), original_source.split('\n'), source_file, @@ -239,9 +238,14 @@ def _check_container_source(container_type, source_file, original_source): f.write(lines) elif file_size != len(lines) or f.read() != lines: raise IOError - msg = ("Saved a reverse patch to " + file_name + ". " - "Run `patch -p0 < " + file_name + "` to revert your " - "changes.") + msg = ( + ( + f"Saved a reverse patch to {file_name}" + ". " + "Run `patch -p0 < " + ) + + file_name + ) + "` to revert your " "changes." + except IOError: msg = ("Tried to save a patch, but couldn't create a " "writable file " + file_name + ". Make sure it " @@ -263,9 +267,7 @@ def maybe_decode_ascii(bytes_str): # # NOTE: This should only be used on internal keys (e.g., `typename` and # `location` in `persistent_load` below! - if isinstance(bytes_str, bytes): - return bytes_str.decode('ascii') - return bytes_str + return bytes_str.decode('ascii') if isinstance(bytes_str, bytes) else bytes_str def persistent_load(saved_id): assert isinstance(saved_id, tuple) @@ -292,7 +294,7 @@ def persistent_load(saved_id): else: return storage else: - raise RuntimeError("Unknown saved id type: %s" % saved_id[0]) + raise RuntimeError(f"Unknown saved id type: {saved_id[0]}") def legacy_load(f): deserialized_objects = {} @@ -363,7 +365,7 @@ def persistent_load(saved_id): raise RuntimeError("Invalid magic number; corrupt file?") protocol_version = pickle_module.load(f, **pickle_load_args) if protocol_version != PROTOCOL_VERSION: - raise RuntimeError("Invalid protocol version: %s" % protocol_version) + raise RuntimeError(f"Invalid protocol version: {protocol_version}") _sys_info = pickle_module.load(f, **pickle_load_args) unpickler = pickle_module.Unpickler(f, **pickle_load_args) diff --git a/autogluon/utils/sync_remote.py b/autogluon/utils/sync_remote.py index 8820f81f71fa..2b08f7d73929 100644 --- a/autogluon/utils/sync_remote.py +++ b/autogluon/utils/sync_remote.py @@ -23,7 +23,7 @@ def sagemaker_setup(): def _wait_for_worker_nodes_to_start_sshd(hosts, interval=1, timeout_in_seconds=180): with timeout(seconds=timeout_in_seconds): while hosts: - print("hosts that aren't SSHable yet: %s", str(hosts)) + print("hosts that aren't SSHable yet: %s", hosts) for host in hosts: ssh_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) if _can_connect(host, 22, ssh_socket): diff --git a/autogluon/utils/tabular/data/cleaner.py b/autogluon/utils/tabular/data/cleaner.py index c79ab946187d..d55cf2f313f0 100644 --- a/autogluon/utils/tabular/data/cleaner.py +++ b/autogluon/utils/tabular/data/cleaner.py @@ -57,15 +57,22 @@ def transform(self, X: DataFrame) -> DataFrame: def get_valid_classes(X, label, threshold): class_counts = X[label].value_counts() class_counts_valid = class_counts[class_counts >= threshold] - valid_classes = list(class_counts_valid.index) sum_prior = sum(class_counts) sum_after = sum(class_counts_valid) percent = sum_after / sum_prior + valid_classes = list(class_counts_valid.index) if len(valid_classes) < len(class_counts): - logger.log(25, 'Warning: Some classes in the training set have fewer than %s examples. AutoGluon will only keep %s out of %s classes for training and will not try to predict the rare classes. ' - 'To keep more classes, increase the number of datapoints from these rare classes in the training data or reduce label_count_threshold.' % (threshold, len(valid_classes), len(class_counts))) + logger.log( + 25, + f'Warning: Some classes in the training set have fewer than {threshold} examples. AutoGluon will only keep {len(valid_classes)} out of {len(class_counts)} classes for training and will not try to predict the rare classes. To keep more classes, increase the number of datapoints from these rare classes in the training data or reduce label_count_threshold.', + ) + if percent < 1.0: - logger.log(25, 'Fraction of data from classes with at least %s examples that will be kept for training models: %s' % (threshold, percent)) + logger.log( + 25, + f'Fraction of data from classes with at least {threshold} examples that will be kept for training models: {percent}', + ) + return valid_classes @staticmethod diff --git a/autogluon/utils/tabular/data/label_cleaner.py b/autogluon/utils/tabular/data/label_cleaner.py index 7f7716dca85c..21360e1e0c53 100644 --- a/autogluon/utils/tabular/data/label_cleaner.py +++ b/autogluon/utils/tabular/data/label_cleaner.py @@ -83,18 +83,16 @@ def transform_proba(self, y): return y def inverse_transform_proba(self, y): - if self.invalid_class_count > 0: - y_transformed = np.zeros([len(y), len(self.ordered_class_labels)]) - y_transformed[:, self.label_index_to_keep] = y - return y_transformed - else: + if self.invalid_class_count <= 0: return y + y_transformed = np.zeros([len(y), len(self.ordered_class_labels)]) + y_transformed[:, self.label_index_to_keep] = y + return y_transformed @staticmethod def _generate_categorical_mapping(y: Series) -> dict: categories = y.astype('category') - cat_mappings_dependent_var = dict(enumerate(categories.cat.categories)) - return cat_mappings_dependent_var + return dict(enumerate(categories.cat.categories)) # TODO: Expand print statement to multiclass as well @@ -133,7 +131,11 @@ def __init__(self, y: Series): logger.log(15, 'Note: For your binary classification, AutoGluon arbitrarily selects which label-value represents positive vs negative class') poslabel = [lbl for lbl in self.inv_map.keys() if self.inv_map[lbl] == 1][0] neglabel = [lbl for lbl in self.inv_map.keys() if self.inv_map[lbl] == 0][0] - logger.log(20, 'Selected class <--> label mapping: class 1 = %s, class 0 = %s' % (poslabel, neglabel)) + logger.log( + 20, + f'Selected class <--> label mapping: class 1 = {poslabel}, class 0 = {neglabel}', + ) + self.cat_mappings_dependent_var: dict = {v: k for k, v in self.inv_map.items()} self.ordered_class_labels_transformed = [0, 1] self.ordered_class_labels = [self.cat_mappings_dependent_var[label_transformed] for label_transformed in self.ordered_class_labels_transformed] diff --git a/autogluon/utils/tabular/features/abstract_feature_generator.py b/autogluon/utils/tabular/features/abstract_feature_generator.py index 155cc1f53014..bc4b032fd21f 100644 --- a/autogluon/utils/tabular/features/abstract_feature_generator.py +++ b/autogluon/utils/tabular/features/abstract_feature_generator.py @@ -26,7 +26,7 @@ def __init__(self): self.features_init_to_keep = [] self.features_to_remove = [] self.features_to_remove_post = [] - self.features_init_types = dict() # Initial feature types prior to transformation + self.features_init_types = {} self.feature_type_family_init_raw = defaultdict(list) # Feature types of original features, without inferring. self.feature_type_family = defaultdict(list) # Feature types of original features, after inferring. self.feature_type_family_generated = defaultdict(list) # Feature types (special) of generated features. @@ -134,9 +134,12 @@ def fit_transform(self, X: DataFrame, y=None, banned_features=None, fix_categori logger.warning(f'Warning: Data size post feature transformation consumes {round(post_memory_usage_percent*100, 1)}% of available memory. Consider increasing memory or subsampling the data to avoid instability.') X_features.index = X_index - if len(list(X_features.columns)) == 0: + if not list(X_features.columns): self.is_dummy = True - logger.warning(f'WARNING: No useful features were detected in the data! AutoGluon will train using 0 features, and will always predict the same value. Ensure that you are passing the correct data to AutoGluon!') + logger.warning( + 'WARNING: No useful features were detected in the data! AutoGluon will train using 0 features, and will always predict the same value. Ensure that you are passing the correct data to AutoGluon!' + ) + X_features['__dummy__'] = 0 self.feature_type_family_generated['int'] = ['__dummy__'] @@ -145,7 +148,11 @@ def fit_transform(self, X: DataFrame, y=None, banned_features=None, fix_categori self.features = list(X_features.columns) self.fit = True - logger.log(20, 'Feature Generator processed %s data points with %s features' % (X_len, len(self.features))) + logger.log( + 20, + f'Feature Generator processed {X_len} data points with {len(self.features)} features', + ) + logger.log(20, 'Original Features (raw dtypes):') for key, val in self.feature_type_family_init_raw.items(): if val: logger.log(20, '\t%s features: %s' % (key, len(val))) @@ -174,12 +181,9 @@ def transform(self, X: DataFrame): X.columns = X.columns.astype(str) # Ensure all column names are strings X = X.drop(self.features_to_remove, axis=1, errors='ignore') X_columns = X.columns.tolist() - # Create any columns present in the training dataset that are now missing from this dataframe: - missing_cols = [] - for col in self.features_init_to_keep: - if col not in X_columns: - missing_cols.append(col) - if len(missing_cols) > 0: + if missing_cols := [ + col for col in self.features_init_to_keep if col not in X_columns + ]: raise ValueError(f'Required columns are missing from the provided dataset. Missing columns: {missing_cols}') X = X.astype(self.features_init_types) @@ -203,9 +207,7 @@ def transform(self, X: DataFrame): def bin_column(series, mapping): mapping_dict = {k: v for v, k in enumerate(list(mapping))} series_out = pd.cut(series, mapping) - # series_out.cat.categories = [str(g) for g in series_out.cat.categories] # LightGBM crashes at end of training without this - series_out_int = [mapping_dict[val] for val in series_out] - return series_out_int + return [mapping_dict[val] for val in series_out] # TODO: Rewrite with normalized value counts as binning technique, will be more performant and optimal @staticmethod @@ -236,19 +238,15 @@ def generate_bins(X_features: DataFrame, features_to_bin): max_desired_bins = min(ideal_cats, max_bins) min_desired_bins = min(ideal_cats, max_bins) - if (len(interval_index) >= min_desired_bins) and (len(interval_index) <= max_desired_bins): - is_satisfied = True - else: - is_satisfied = False + is_satisfied = ( + len(interval_index) >= min_desired_bins + and len(interval_index) <= max_desired_bins + ) num_cats_current = num_cats_initial # print(column, min_desired_bins, max_desired_bins) cur_iteration = 0 while not is_satisfied: - if len(interval_index) > max_desired_bins: - pass - elif len(interval_index) < min_desired_bins: - pass ratio_reduction = max_desired_bins / len(interval_index) num_cats_current = int(np.floor(num_cats_current * ratio_reduction)) bin_index = [np.floor(cur_len * (num + 1) / num_cats_current) for num in range(num_cats_current - 1)] @@ -271,10 +269,9 @@ def generate_bins(X_features: DataFrame, features_to_bin): def get_approximate_df_mem_usage(df, sample_ratio=0.2): if sample_ratio >= 1: return df.memory_usage(deep=True) - else: - num_rows = len(df) - num_rows_sample = math.ceil(sample_ratio * num_rows) - return df.head(num_rows_sample).memory_usage(deep=True) / sample_ratio + num_rows = len(df) + num_rows_sample = math.ceil(sample_ratio * num_rows) + return df.head(num_rows_sample).memory_usage(deep=True) / sample_ratio # TODO: Clean code # bins is a sorted int/float series, ascending=True @@ -284,14 +281,22 @@ def get_bins(bins: Series, bin_index, bin_epsilon): bins_2 = bins.iloc[bin_index] bins_3 = list(bins_2.values) bins_unique = sorted(list(set(bins_3))) - bins_with_epsilon_max = set([i for i in bins_unique] + [i - bin_epsilon for i in bins_unique if i == max_val]) - removal_bins = set([bins_unique[index - 1] for index, i in enumerate(bins_unique[1:], start=1) if i == max_val]) + bins_with_epsilon_max = set( + list(bins_unique) + + [i - bin_epsilon for i in bins_unique if i == max_val] + ) + + removal_bins = { + bins_unique[index - 1] + for index, i in enumerate(bins_unique[1:], start=1) + if i == max_val + } + bins_4 = sorted(list(bins_with_epsilon_max - removal_bins)) bins_5 = [np.inf if (x == max_val) else x for x in bins_4] bins_6 = sorted(list(set([-np.inf] + bins_5 + [np.inf]))) bins_7 = [(bins_6[i], bins_6[i + 1]) for i in range(len(bins_6) - 1)] - interval_index = pd.IntervalIndex.from_tuples(bins_7) - return interval_index + return pd.IntervalIndex.from_tuples(bins_7) def get_feature_types(self, X: DataFrame): self.features_init = list(X.columns) @@ -372,25 +377,34 @@ def check_if_nlp_feature(self, X: Series): return False avg_words = np.mean([len(re.sub(' +', ' ', value).split(' ')) if isinstance(value, str) else 0 for value in X_unique]) # print(avg_words) - if avg_words < 3: - return False - - return True + return avg_words >= 3 def generate_text_features(self, X: Series, feature: str) -> DataFrame: X: DataFrame = X.to_frame(name=feature) - X[feature + '.char_count'] = [self.char_count(value) for value in X[feature]] - X[feature + '.word_count'] = [self.word_count(value) for value in X[feature]] - X[feature + '.capital_ratio'] = [self.capital_ratio(value) for value in X[feature]] - X[feature + '.lower_ratio'] = [self.lower_ratio(value) for value in X[feature]] - X[feature + '.digit_ratio'] = [self.digit_ratio(value) for value in X[feature]] - X[feature + '.special_ratio'] = [self.special_ratio(value) for value in X[feature]] + X[f'{feature}.char_count'] = [self.char_count(value) for value in X[feature]] + X[f'{feature}.word_count'] = [self.word_count(value) for value in X[feature]] + X[f'{feature}.capital_ratio'] = [ + self.capital_ratio(value) for value in X[feature] + ] + + X[f'{feature}.lower_ratio'] = [self.lower_ratio(value) for value in X[feature]] + X[f'{feature}.digit_ratio'] = [self.digit_ratio(value) for value in X[feature]] + X[f'{feature}.special_ratio'] = [ + self.special_ratio(value) for value in X[feature] + ] + symbols = ['!', '?', '@', '%', '$', '*', '&', '#', '^', '.', ':', ' ', '/', ';', '-', '='] for symbol in symbols: - X[feature + '.symbol_count.' + symbol] = [self.symbol_in_string_count(value, symbol) for value in X[feature]] - X[feature + '.symbol_ratio.' + symbol] = X[feature + '.symbol_count.' + symbol] / X[feature + '.char_count'] - X[feature + '.symbol_ratio.' + symbol].fillna(0, inplace=True) + X[f'{feature}.symbol_count.' + symbol] = [ + self.symbol_in_string_count(value, symbol) for value in X[feature] + ] + + X[f'{feature}.symbol_ratio.' + symbol] = ( + X[f'{feature}.symbol_count.' + symbol] / X[f'{feature}.char_count'] + ) + + X[f'{feature}.symbol_ratio.' + symbol].fillna(0, inplace=True) X = X.drop(feature, axis=1) @@ -460,29 +474,21 @@ def special_ratio(string): @staticmethod def digit_ratio(string): string = string.replace(' ', '') - if not string: - return 0 - return sum(c.isdigit() for c in string) / len(string) + return sum(c.isdigit() for c in string) / len(string) if string else 0 @staticmethod def lower_ratio(string): string = string.replace(' ', '') - if not string: - return 0 - return sum(c.islower() for c in string) / len(string) + return sum(c.islower() for c in string) / len(string) if string else 0 @staticmethod def capital_ratio(string): string = string.replace(' ', '') - if not string: - return 0 - return sum(1 for c in string if c.isupper()) / len(string) + return sum(bool(c.isupper()) for c in string) / len(string) if string else 0 @staticmethod def symbol_in_string_count(string, character): - if not string: - return 0 - return sum(1 for c in string if c == character) + return sum(c == character for c in string) if string else 0 # TODO: optimize by not considering columns with unique sums/means # TODO: Multithread? diff --git a/autogluon/utils/tabular/features/add_datepart_helper.py b/autogluon/utils/tabular/features/add_datepart_helper.py index f5d0d9e19505..3d556e814196 100644 --- a/autogluon/utils/tabular/features/add_datepart_helper.py +++ b/autogluon/utils/tabular/features/add_datepart_helper.py @@ -24,7 +24,7 @@ def add_datepart(df: DataFrame, field_name: str, prefix: str = None, drop: bool 'Is_quarter_end', 'Is_quarter_start', 'Is_year_end', 'Is_year_start' ] if time: - attr = attr + ['Hour', 'Minute', 'Second'] + attr += ['Hour', 'Minute', 'Second'] for n in attr: df[prefix + n] = getattr(field.dt, n.lower()) df[prefix + 'Elapsed'] = field.astype(np.int64) // 10 ** 9 diff --git a/autogluon/utils/tabular/features/auto_ml_feature_generator.py b/autogluon/utils/tabular/features/auto_ml_feature_generator.py index 78ebf5c8a433..60d73c87fbaf 100644 --- a/autogluon/utils/tabular/features/auto_ml_feature_generator.py +++ b/autogluon/utils/tabular/features/auto_ml_feature_generator.py @@ -46,10 +46,12 @@ def _compute_feature_transformations(self): if self.enable_nlp_features: self.feature_transformations['text_ngram'] += text_features - if 'datetime' in self.feature_type_family: + if ( + 'datetime' in self.feature_type_family + and self.enable_datetime_features + ): datetime_features = self.feature_type_family['datetime'] - if self.enable_datetime_features: - self.feature_transformations['datetime'] += datetime_features + self.feature_transformations['datetime'] += datetime_features if self.enable_raw_features: for type_family in self.feature_type_family: @@ -111,7 +113,15 @@ def generate_features(self, X: DataFrame): for nlp_feature in features_nlp_current: # TODO: Preprocess text? if nlp_feature == '__nlp__': - text_list = list(set(['. '.join(row) for row in X[self.feature_transformations['text_ngram']].values])) + text_list = list( + { + '. '.join(row) + for row in X[ + self.feature_transformations['text_ngram'] + ].values + } + ) + else: text_list = list(X[nlp_feature].drop_duplicates().values) vectorizer_raw = copy.deepcopy(self.vectorizer_default_raw) @@ -193,10 +203,12 @@ def generate_text_ngrams(self, X, features_nlp_current, downsample_ratio: int = max_memory_percentage = 0.15 # TODO: Finetune this, or find a better metric predicted_rss = mem_rss + predicted_ngrams_memory_usage_bytes predicted_percentage = predicted_rss / mem_avail - if downsample_ratio is None: - if predicted_percentage > max_memory_percentage: - downsample_ratio = max_memory_percentage / predicted_percentage - logger.warning('Warning: Due to memory constraints, ngram feature count is being reduced. Allocate more memory to maximize model quality.') + if ( + downsample_ratio is None + and predicted_percentage > max_memory_percentage + ): + downsample_ratio = max_memory_percentage / predicted_percentage + logger.warning('Warning: Due to memory constraints, ngram feature count is being reduced. Allocate more memory to maximize model quality.') if downsample_ratio is not None: if (downsample_ratio >= 1) or (downsample_ratio <= 0): diff --git a/autogluon/utils/tabular/features/vectorizers.py b/autogluon/utils/tabular/features/vectorizers.py index fc2f1422b8cf..e6c04340ca7c 100644 --- a/autogluon/utils/tabular/features/vectorizers.py +++ b/autogluon/utils/tabular/features/vectorizers.py @@ -11,8 +11,7 @@ def vectorizer_auto_ml_default(): def get_ngram_freq(vectorizer, transform_matrix): names = vectorizer.get_feature_names() frequencies = transform_matrix.sum(axis=0).tolist()[0] - ngram_freq = {ngram: freq for ngram, freq in zip(names, frequencies)} - return ngram_freq + return dict(zip(names, frequencies)) # Reduces vectorizer vocabulary size to vocab_size, keeping highest frequency ngrams diff --git a/autogluon/utils/tabular/metrics/__init__.py b/autogluon/utils/tabular/metrics/__init__.py index 8d8adc4d4273..653a989960a6 100644 --- a/autogluon/utils/tabular/metrics/__init__.py +++ b/autogluon/utils/tabular/metrics/__init__.py @@ -160,12 +160,9 @@ def __call__(self, y_true, y_pred, sample_weight=None): def scorer_expects_y_pred(scorer: Scorer): - if isinstance(scorer, _ProbaScorer): - return False - elif isinstance(scorer, _ThresholdScorer): - return False - else: - return True + return not isinstance(scorer, _ProbaScorer) and not isinstance( + scorer, _ThresholdScorer + ) def make_scorer(name, score_func, optimum=1, greater_is_better=True, @@ -309,7 +306,7 @@ def calculate_score(solution, prediction, task_type, metric, raise NotImplementedError(task_type) if all_scoring_functions: - score = dict() + score = {} if task_type in PROBLEM_TYPES_REGRESSION: # TODO put this into the regression metric itself cprediction = sanitize_array(prediction) @@ -331,25 +328,22 @@ def calculate_score(solution, prediction, task_type, metric, try: score[func.name] = func(solution, prediction) except ValueError as e: - if e.args[0] == 'multiclass format is not supported': - continue - elif e.args[0] == "Samplewise metrics are not available "\ - "outside of multilabel classification.": - continue - elif e.args[0] == "Target is multiclass but "\ - "average='binary'. Please choose another average "\ - "setting, one of [None, 'micro', 'macro', 'weighted'].": - continue - else: + if e.args[0] not in [ + 'multiclass format is not supported', + "Samplewise metrics are not available " + "outside of multilabel classification.", + "Target is multiclass but " + "average='binary'. Please choose another average " + "setting, one of [None, 'micro', 'macro', 'weighted'].", + ]: raise e + elif task_type in PROBLEM_TYPES_REGRESSION: + # TODO put this into the regression metric itself + cprediction = sanitize_array(prediction) + score = metric(solution, cprediction) else: - if task_type in PROBLEM_TYPES_REGRESSION: - # TODO put this into the regression metric itself - cprediction = sanitize_array(prediction) - score = metric(solution, cprediction) - else: - score = metric(solution, prediction) + score = metric(solution, prediction) return score @@ -357,21 +351,20 @@ def calculate_score(solution, prediction, task_type, metric, def get_metric(metric, problem_type, metric_type): """Returns metric function by using its name if the metric is str. Performs basic check for metric compatibility with given problem type.""" - if metric is not None and isinstance(metric, str): - if metric in CLASSIFICATION_METRICS: - if problem_type is not None and problem_type not in PROBLEM_TYPES_CLASSIFICATION: - raise ValueError(f"{metric_type}={metric} can only be used for classification problems") - return CLASSIFICATION_METRICS[metric] - elif metric in REGRESSION_METRICS: - if problem_type is not None and problem_type not in PROBLEM_TYPES_REGRESSION: - raise ValueError(f"{metric_type}={metric} can only be used for regression problems") - return REGRESSION_METRICS[metric] - elif metric == 'soft_log_loss': - return soft_log_loss - else: - raise ValueError( - f"{metric} is an unknown metric, see autogluon/utils/tabular/metrics/ for available options " - f"or how to define your own {metric_type} function" - ) - else: + if metric is None or not isinstance(metric, str): return metric + if metric in CLASSIFICATION_METRICS: + if problem_type is not None and problem_type not in PROBLEM_TYPES_CLASSIFICATION: + raise ValueError(f"{metric_type}={metric} can only be used for classification problems") + return CLASSIFICATION_METRICS[metric] + elif metric in REGRESSION_METRICS: + if problem_type is not None and problem_type not in PROBLEM_TYPES_REGRESSION: + raise ValueError(f"{metric_type}={metric} can only be used for regression problems") + return REGRESSION_METRICS[metric] + elif metric == 'soft_log_loss': + return soft_log_loss + else: + raise ValueError( + f"{metric} is an unknown metric, see autogluon/utils/tabular/metrics/ for available options " + f"or how to define your own {metric_type} function" + ) diff --git a/autogluon/utils/tabular/metrics/classification_metrics.py b/autogluon/utils/tabular/metrics/classification_metrics.py index bcb4d1f1c4fc..1e75163121ea 100644 --- a/autogluon/utils/tabular/metrics/classification_metrics.py +++ b/autogluon/utils/tabular/metrics/classification_metrics.py @@ -219,7 +219,7 @@ def prior_log_loss(frac_pos, task): elif len(solution.shape) == 1: pass else: - raise ValueError('Solution.shape %s' % solution.shape) + raise ValueError(f'Solution.shape {solution.shape}') # Need to create a multiclass solution and a multiclass predictions max_class = prediction.shape[1] - 1 diff --git a/autogluon/utils/tabular/ml/learner/abstract_learner.py b/autogluon/utils/tabular/ml/learner/abstract_learner.py index 678fc69e6c22..1db1777073ed 100644 --- a/autogluon/utils/tabular/ml/learner/abstract_learner.py +++ b/autogluon/utils/tabular/ml/learner/abstract_learner.py @@ -156,9 +156,8 @@ def predict(self, X: DataFrame, model=None, as_pandas=False, use_pred_cache=Fals return y_pred def get_inputs_to_stacker(self, dataset=None, model=None, base_models: list = None, use_orig_features=True): - if model is not None or base_models is not None: - if model is not None and base_models is not None: - raise AssertionError('Only one of `model`, `base_models` is allowed to be set.') + if model is not None and base_models is not None: + raise AssertionError('Only one of `model`, `base_models` is allowed to be set.') trainer = self.load_trainer() if dataset is None: @@ -280,9 +279,11 @@ def score_debug(self, X: DataFrame, y=None, compute_oracle=False, silent=False): if len(base_model_set) == 1: pred_time_test[model] = pred_time_test_marginal[base_model_set[0]] else: - pred_time_test_full_num = 0 - for base_model in base_model_set: - pred_time_test_full_num += pred_time_test_marginal[base_model] + pred_time_test_full_num = sum( + pred_time_test_marginal[base_model] + for base_model in base_model_set + ) + pred_time_test[model] = pred_time_test_full_num else: pred_time_test[model] = None @@ -385,16 +386,16 @@ def evaluate(self, y_true, y_pred, silent=False, auxiliary_metrics=False, detail if self.problem_type == MULTICLASS: y_true_cleaned = y_true_cleaned.fillna(-1) # map unknown classes to -1 performance = self.eval_metric(y_true_cleaned, y_pred_cleaned) - else: - if self.problem_type == MULTICLASS: - y_true_cleaned = self.label_cleaner.transform(y_true) - y_true_cleaned = y_true_cleaned.fillna(-1) - if (not trainer.eval_metric_expects_y_pred) and (-1 in y_true_cleaned.unique()): - # log_loss / pac_score - raise ValueError(f'Multiclass scoring with eval_metric=\'{self.eval_metric.name}\' does not support unknown classes.') - performance = self.eval_metric(y_true_cleaned, y_pred) + elif self.problem_type == MULTICLASS: + y_true_cleaned = self.label_cleaner.transform(y_true) + y_true_cleaned = y_true_cleaned.fillna(-1) + if (not trainer.eval_metric_expects_y_pred) and (-1 in y_true_cleaned.unique()): + # log_loss / pac_score + raise ValueError(f'Multiclass scoring with eval_metric=\'{self.eval_metric.name}\' does not support unknown classes.') else: - performance = self.eval_metric(y_true, y_pred) + performance = self.eval_metric(y_true_cleaned, y_pred) + else: + performance = self.eval_metric(y_true, y_pred) metric = self.eval_metric.name @@ -526,9 +527,9 @@ def get_feature_importance(self, model=None, X=None, y=None, features: list = No if feature_stage == 'original': return trainer._get_feature_importance_raw(model=model, X=X, y=y, features_to_use=features, subsample_size=subsample_size, transform_func=self.transform_features, silent=silent) X = self.transform_features(X) + elif feature_stage == 'original': + raise AssertionError('Feature importance `dataset` cannot be None if `feature_stage==\'original\'`. A test dataset must be specified.') else: - if feature_stage == 'original': - raise AssertionError('Feature importance `dataset` cannot be None if `feature_stage==\'original\'`. A test dataset must be specified.') y = None raw = feature_stage == 'transformed' return trainer.get_feature_importance(X=X, y=y, model=model, features=features, raw=raw, subsample_size=subsample_size, silent=silent) @@ -556,7 +557,7 @@ def get_info(self, include_model_info=False): 'version': self.version, } - learner_info.update(trainer_info) + learner_info |= trainer_info return learner_info @staticmethod @@ -577,11 +578,11 @@ def load(cls, path_context, reset_paths=True): obj.set_contexts(path_context) obj.trainer_path = obj.model_context obj.reset_paths = reset_paths - # TODO: Still have to change paths of models in trainer + trainer object path variables - return obj else: obj.set_contexts(obj.path_context) - return obj + + # TODO: Still have to change paths of models in trainer + trainer object path variables + return obj def save_trainer(self, trainer): if self.is_trainer_present: diff --git a/autogluon/utils/tabular/ml/learner/default_learner.py b/autogluon/utils/tabular/ml/learner/default_learner.py index cc4616cd1e79..43cfc03726e7 100644 --- a/autogluon/utils/tabular/ml/learner/default_learner.py +++ b/autogluon/utils/tabular/ml/learner/default_learner.py @@ -110,13 +110,9 @@ def fit(self, X: DataFrame, X_val: DataFrame = None, scheduler_options=None, hyp def general_data_processing(self, X: DataFrame, X_val: DataFrame, holdout_frac: float, num_bagging_folds: int): """ General data processing steps used for all models. """ X = copy.deepcopy(X) - # TODO: We should probably uncomment the below lines, NaN label should be treated as just another value in multiclass classification -> We will have to remove missing, compute problem type, and add back missing if multiclass - # if self.problem_type == MULTICLASS: - # X[self.label] = X[self.label].fillna('') - - # Remove all examples with missing labels from this dataset: - missinglabel_inds = [index for index, x in X[self.label].isna().iteritems() if x] - if len(missinglabel_inds) > 0: + if missinglabel_inds := [ + index for index, x in X[self.label].isna().iteritems() if x + ]: logger.warning(f"Warning: Ignoring {len(missinglabel_inds)} (out of {len(X)}) training examples for which the label value in column '{self.label}' is missing") X = X.drop(missinglabel_inds, axis=0) @@ -179,12 +175,10 @@ def general_data_processing(self, X: DataFrame, X_val: DataFrame, holdout_frac: def adjust_threshold_if_necessary(self, y, threshold, holdout_frac, num_bagging_folds): new_threshold, new_holdout_frac, new_num_bagging_folds = self._adjust_threshold_if_necessary(y, threshold, holdout_frac, num_bagging_folds) - if new_threshold != threshold: - if new_threshold < threshold: - logger.warning(f'Warning: Updated label_count_threshold from {threshold} to {new_threshold} to avoid cutting too many classes.') - if new_holdout_frac != holdout_frac: - if new_holdout_frac > holdout_frac: - logger.warning(f'Warning: Updated holdout_frac from {holdout_frac} to {new_holdout_frac} to avoid cutting too many classes.') + if new_threshold != threshold and new_threshold < threshold: + logger.warning(f'Warning: Updated label_count_threshold from {threshold} to {new_threshold} to avoid cutting too many classes.') + if new_holdout_frac != holdout_frac and new_holdout_frac > holdout_frac: + logger.warning(f'Warning: Updated holdout_frac from {holdout_frac} to {new_holdout_frac} to avoid cutting too many classes.') if new_num_bagging_folds != num_bagging_folds: logger.warning(f'Warning: Updated num_bagging_folds from {num_bagging_folds} to {new_num_bagging_folds} to avoid cutting too many classes.') return new_threshold, new_holdout_frac, new_num_bagging_folds diff --git a/autogluon/utils/tabular/ml/models/abstract/abstract_model.py b/autogluon/utils/tabular/ml/models/abstract/abstract_model.py index 95d3d081e842..dc9bbe0bd37c 100644 --- a/autogluon/utils/tabular/ml/models/abstract/abstract_model.py +++ b/autogluon/utils/tabular/ml/models/abstract/abstract_model.py @@ -35,9 +35,7 @@ def fixedvals_from_searchspaces(params): params = params.copy() for hyperparam in bad_keys: params[hyperparam] = hp_default_value(params[hyperparam]) - return params - else: - return params + return params def hp_default_value(hp_value): @@ -50,7 +48,10 @@ def hp_default_value(hp_value): elif isinstance(hp_value, List): return [z[0] for z in hp_value] elif isinstance(hp_value, NestedSpace): - raise ValueError("Cannot extract default value from NestedSpace. Please specify fixed value instead of: %s" % str(hp_value)) + raise ValueError( + f"Cannot extract default value from NestedSpace. Please specify fixed value instead of: {str(hp_value)}" + ) + else: return hp_value.get_hp('dummy_name').default_value @@ -130,7 +131,7 @@ def __init__(self, path: str, name: str, problem_type: str, eval_metric: Union[s if hyperparameters is not None: self.params.update(hyperparameters) self.nondefault_params = list(hyperparameters.keys())[:] # These are hyperparameters that user has specified. - self.params_trained = dict() + self.params_trained = {} # Checks if model is capable of inference on new data (if normal model) or has produced out-of-fold predictions (if bagged model) def is_valid(self): @@ -207,8 +208,7 @@ def set_contexts(self, path_context): @staticmethod def create_contexts(path_context): - path = path_context - return path + return path_context def rename(self, name): self.path = self.path[:-len(self.name) - 1] + name + os.path.sep @@ -218,11 +218,7 @@ def rename(self, name): # This means preprocess cannot be used for normalization # TODO: Add preprocess_stateful() to enable stateful preprocessing for models such as KNN def preprocess(self, X): - if self.features is not None: - # TODO: In online-inference this becomes expensive, add option to remove it (only safe in controlled environment where it is already known features are present - if list(X.columns) != self.features: - return X[self.features] - else: + if self.features is None: self.features = list(X.columns) # TODO: add fit and transform versions of preprocess instead of doing this ignored_feature_types_raw = self.params_aux.get('ignored_feature_types_raw', []) if ignored_feature_types_raw: @@ -234,13 +230,16 @@ def preprocess(self, X): self.features = [feature for feature in self.features if feature not in self.feature_types_metadata.feature_types_special[ignored_feature_type]] if not self.features: raise NoValidFeatures - if ignored_feature_types_raw or ignored_feature_types_special: - if list(X.columns) != self.features: - X = X[self.features] + if ( + ignored_feature_types_raw or ignored_feature_types_special + ) and list(X.columns) != self.features: + X = X[self.features] + elif list(X.columns) != self.features: + return X[self.features] return X def _preprocess_fit_args(self, **kwargs): - time_limit = kwargs.get('time_limit', None) + time_limit = kwargs.get('time_limit') max_time_limit_ratio = self.params_aux.get('max_time_limit_ratio', 1) if time_limit is not None: time_limit *= max_time_limit_ratio @@ -273,8 +272,9 @@ def _fit(self, X_train, y_train, **kwargs): def predict(self, X, preprocess=True): y_pred_proba = self.predict_proba(X, preprocess=preprocess) - y_pred = get_pred_from_proba(y_pred_proba=y_pred_proba, problem_type=self.problem_type) - return y_pred + return get_pred_from_proba( + y_pred_proba=y_pred_proba, problem_type=self.problem_type + ) def predict_proba(self, X, preprocess=True, normalize=None): if normalize is None: @@ -293,12 +293,10 @@ def _predict_proba(self, X, preprocess=True): y_pred_proba = self.model.predict_proba(X) if self.problem_type == BINARY: - if len(y_pred_proba.shape) == 1: + if len(y_pred_proba.shape) == 1 or y_pred_proba.shape[1] <= 1: return y_pred_proba - elif y_pred_proba.shape[1] > 1: - return y_pred_proba[:, 1] else: - return y_pred_proba + return y_pred_proba[:, 1] elif y_pred_proba.shape[1] > 2: return y_pred_proba else: @@ -321,11 +319,10 @@ def score_with_y_pred_proba(self, y, y_pred_proba, eval_metric=None, metric_need eval_metric = self.eval_metric if metric_needs_y_pred is None: metric_needs_y_pred = self.metric_needs_y_pred - if metric_needs_y_pred: - y_pred = get_pred_from_proba(y_pred_proba=y_pred_proba, problem_type=self.problem_type) - return eval_metric(y, y_pred) - else: + if not metric_needs_y_pred: return eval_metric(y, y_pred_proba) + y_pred = get_pred_from_proba(y_pred_proba=y_pred_proba, problem_type=self.problem_type) + return eval_metric(y, y_pred) def save(self, file_prefix="", directory=None, return_filename=False, verbose=True): if directory is None: @@ -340,10 +337,9 @@ def load(cls, path, file_prefix="", reset_paths=False, verbose=True): load_path = path + file_prefix + cls.model_file_name if not reset_paths: return load_pkl.load(path=load_path, verbose=verbose) - else: - obj = load_pkl.load(path=load_path, verbose=verbose) - obj.set_contexts(path) - return obj + obj = load_pkl.load(path=load_path, verbose=verbose) + obj.set_contexts(path) + return obj # TODO: Consider disabling feature pruning when num_features is high (>1000 for example), or using a faster feature importance calculation method def compute_feature_importance(self, X, y, features_to_use=None, preprocess=True, subsample_size=10000, silent=False, **kwargs): @@ -357,11 +353,7 @@ def compute_feature_importance(self, X, y, features_to_use=None, preprocess=True if preprocess: X = self.preprocess(X) - if not features_to_use: - features = list(X.columns.values) - else: - features = list(features_to_use) - + features = list(features_to_use) if features_to_use else list(X.columns.values) feature_importance_quick_dict = self.get_model_feature_importance() # TODO: Also consider banning features with close to 0 importance # TODO: Consider adding 'golden' features if the importance is high enough to avoid unnecessary computation when doing feature selection @@ -460,7 +452,7 @@ def compute_permutation_importance(self, X, y, features: list, preprocess=True, # Custom feature importance values for a model (such as those calculated from training) def get_model_feature_importance(self) -> dict: - return dict() + return {} # Hyperparameters of trained model def get_trained_params(self): @@ -489,7 +481,11 @@ def convert_to_refitfull_template(self): def hyperparameter_tune(self, X_train, y_train, X_val, y_val, scheduler_options, **kwargs): # verbosity = kwargs.get('verbosity', 2) time_start = time.time() - logger.log(15, "Starting generic AbstractModel hyperparameter tuning for %s model..." % self.name) + logger.log( + 15, + f"Starting generic AbstractModel hyperparameter tuning for {self.name} model...", + ) + self._set_default_searchspace() params_copy = self.params.copy() directory = self.path # also create model directory if it doesn't exist @@ -510,7 +506,7 @@ def hyperparameter_tune(self, X_train, y_train, X_val, y_val, scheduler_options, if not any(isinstance(params_copy[hyperparam], Space) for hyperparam in params_copy): logger.warning("Attempting to do hyperparameter optimization without any search space (all hyperparameters are already fixed values)") else: - logger.log(15, "Hyperparameter search space for %s model: " % self.name) + logger.log(15, f"Hyperparameter search space for {self.name} model: ") for hyperparam in params_copy: if isinstance(params_copy[hyperparam], Space): logger.log(15, f"{hyperparam}: {params_copy[hyperparam]}") @@ -563,14 +559,17 @@ def _get_hpo_results(self, scheduler, scheduler_options, time_start): hpo_model_performances = {} for trial in sorted(hpo_results['trial_info'].keys()): # TODO: ignore models which were killed early by scheduler (eg. in Hyperband). How to ID these? - file_id = "trial_" + str(trial) # unique identifier to files from this trial + file_id = f"trial_{str(trial)}" trial_model_name = self.name + os.path.sep + file_id trial_model_path = self.path_root + trial_model_name + os.path.sep hpo_models[trial_model_name] = trial_model_path hpo_model_performances[trial_model_name] = hpo_results['trial_info'][trial][scheduler._reward_attr] - logger.log(15, "Time for %s model HPO: %s" % (self.name, str(hpo_results['total_time']))) - logger.log(15, "Best hyperparameter configuration for %s model: " % self.name) + logger.log( + 15, f"Time for {self.name} model HPO: {str(hpo_results['total_time'])}" + ) + + logger.log(15, f"Best hyperparameter configuration for {self.name} model: ") logger.log(15, str(best_hp)) return hpo_models, hpo_model_performances, hpo_results @@ -587,7 +586,7 @@ def reset_metrics(self): self.fit_time = None self.predict_time = None self.val_score = None - self.params_trained = dict() + self.params_trained = {} # TODO: Experimental, currently unused # Has not been tested on Windows @@ -597,8 +596,7 @@ def get_disk_size(self): # Taken from https://stackoverflow.com/a/1392549 from pathlib import Path model_path = Path(self.path) - model_disk_size = sum(f.stat().st_size for f in model_path.glob('**/*') if f.is_file()) - return model_disk_size + return sum(f.stat().st_size for f in model_path.glob('**/*') if f.is_file()) # TODO: This results in a doubling of memory usage of the model to calculate its size. # If the model takes ~40%+ of memory, this may result in an OOM error. @@ -626,7 +624,7 @@ def delete_from_disk(self): shutil.rmtree(path=model_path, ignore_errors=True) def get_info(self): - info = dict( + return dict( name=self.name, model_type=type(self).__name__, problem_type=self.problem_type, @@ -641,7 +639,6 @@ def get_info(self): # disk_size=self.get_disk_size(), memory_size=self.get_memory_size(), # Memory usage of model in bytes ) - return info @classmethod def load_info(cls, path, load_model_if_required=True): diff --git a/autogluon/utils/tabular/ml/models/catboost/catboost_model.py b/autogluon/utils/tabular/ml/models/catboost/catboost_model.py index e49779c163ed..40c9e2c4712f 100644 --- a/autogluon/utils/tabular/ml/models/catboost/catboost_model.py +++ b/autogluon/utils/tabular/ml/models/catboost/catboost_model.py @@ -37,8 +37,7 @@ def _get_default_searchspace(self): def preprocess(self, X): X = super().preprocess(X) - categoricals = list(X.select_dtypes(include='category').columns) - if categoricals: + if categoricals := list(X.select_dtypes(include='category').columns): X = X.copy() for category in categoricals: current_categories = X[category].cat.categories @@ -70,10 +69,7 @@ def _fit(self, X_train, y_train, X_val=None, y_val=None, time_limit=None, **kwar num_rows_train = len(X_train) num_cols_train = len(X_train.columns) if self.problem_type == MULTICLASS: - if self.num_classes is not None: - num_classes = self.num_classes - else: - num_classes = 10 # Guess if not given, can do better by looking at y_train + num_classes = self.num_classes if self.num_classes is not None else 10 elif self.problem_type == SOFTCLASS: # TODO: delete this elif if it's unnecessary. num_classes = y_train.shape[1] self.num_classes = num_classes @@ -101,10 +97,7 @@ def _fit(self, X_train, y_train, X_val=None, y_val=None, time_limit=None, **kwar X_val = self.preprocess(X_val) X_val = Pool(data=X_val, label=y_val, cat_features=cat_features) eval_set = X_val - if num_rows_train <= 10000: - modifier = 1 - else: - modifier = 10000/num_rows_train + modifier = 1 if num_rows_train <= 10000 else 10000/num_rows_train early_stopping_rounds = max(round(modifier*150), 10) num_sample_iter_max = max(round(modifier*50), 2) else: @@ -117,22 +110,23 @@ def _fit(self, X_train, y_train, X_val=None, y_val=None, time_limit=None, **kwar if invalid in self.params: self.params.pop(invalid) train_dir = None - if 'allow_writing_files' in self.params and self.params['allow_writing_files']: - if 'train_dir' not in self.params: - try: - # TODO: What if path is in S3? - os.makedirs(os.path.dirname(self.path), exist_ok=True) - except: - pass - else: - train_dir = self.path + 'catboost_info' + if ( + 'allow_writing_files' in self.params + and self.params['allow_writing_files'] + and 'train_dir' not in self.params + ): + try: + # TODO: What if path is in S3? + os.makedirs(os.path.dirname(self.path), exist_ok=True) + except: + pass + else: + train_dir = f'{self.path}catboost_info' logger.log(15, f'\tCatboost model hyperparameters: {self.params}') # TODO: Add more control over these params (specifically early_stopping_rounds) verbosity = kwargs.get('verbosity', 2) - if verbosity <= 1: - verbose = False - elif verbosity == 2: + if verbosity <= 1 or verbosity == 2: verbose = False elif verbosity == 3: verbose = 20 @@ -237,17 +231,17 @@ def _fit(self, X_train, y_train, X_val=None, y_val=None, time_limit=None, **kwar if init_model is not None: final_model_best_score = self.model.get_best_score()['validation'][metric_name] - if self.stopping_metric._optimum > final_model_best_score: - if final_model_best_score > init_model_best_score: - best_iteration = init_model_tree_count + self.model.get_best_iteration() - else: - best_iteration = init_model_best_iteration + if ( + self.stopping_metric._optimum + > final_model_best_score + > init_model_best_score + or self.stopping_metric._optimum + <= final_model_best_score + < init_model_best_score + ): + best_iteration = init_model_tree_count + self.model.get_best_iteration() else: - if final_model_best_score < init_model_best_score: - best_iteration = init_model_tree_count + self.model.get_best_iteration() - else: - best_iteration = init_model_best_iteration - + best_iteration = init_model_best_iteration self.model.shrink(ntree_start=0, ntree_end=best_iteration+1) self.params_trained['iterations'] = self.model.tree_count_ @@ -269,5 +263,4 @@ def get_model_feature_importance(self): importance_df = self.model.get_feature_importance(prettified=True) importance_df['Importances'] = importance_df['Importances'] / 100 importance_series = importance_df.set_index('Feature Id')['Importances'] - importance_dict = importance_series.to_dict() - return importance_dict + return importance_series.to_dict() diff --git a/autogluon/utils/tabular/ml/models/catboost/catboost_utils.py b/autogluon/utils/tabular/ml/models/catboost/catboost_utils.py index 88780c9a92c3..9902cde8773e 100644 --- a/autogluon/utils/tabular/ml/models/catboost/catboost_utils.py +++ b/autogluon/utils/tabular/ml/models/catboost/catboost_utils.py @@ -61,11 +61,8 @@ def evaluate(self, approxes, target, weight): y_pred_proba = self._get_y_pred_proba(approxes=approxes) if self.needs_pred_proba: raise NotImplementedError('Custom Catboost Multiclass proba metrics are not supported by AutoGluon.') - # y_pred_proba = y_pred_proba.reshape(len(np.unique(np.array(target))), -1).T - # score = self.metric(np.array(target), y_pred_proba) # This doesn't work at the moment because catboost returns some strange valeus in approxes which are not the probabilities - else: - y_pred = self._get_y_pred(y_pred_proba=y_pred_proba) - score = self.metric(np.array(target), y_pred) + y_pred = self._get_y_pred(y_pred_proba=y_pred_proba) + score = self.metric(np.array(target), y_pred) return score, 1 diff --git a/autogluon/utils/tabular/ml/models/catboost/hyperparameters/parameters.py b/autogluon/utils/tabular/ml/models/catboost/hyperparameters/parameters.py index 4c4fa94554aa..0e2566d606e8 100644 --- a/autogluon/utils/tabular/ml/models/catboost/hyperparameters/parameters.py +++ b/autogluon/utils/tabular/ml/models/catboost/hyperparameters/parameters.py @@ -4,35 +4,34 @@ def get_param_baseline(problem_type, num_classes=None): - if problem_type == BINARY: + if problem_type == BINARY or problem_type not in [ + MULTICLASS, + SOFTCLASS, + REGRESSION, + ]: return get_param_binary_baseline() elif problem_type in [MULTICLASS, SOFTCLASS]: return get_param_multiclass_baseline(num_classes=num_classes) - elif problem_type == REGRESSION: - return get_param_regression_baseline() else: - return get_param_binary_baseline() + return get_param_regression_baseline() def get_param_binary_baseline(): - params = { + return { 'iterations': DEFAULT_ITERATIONS, 'learning_rate': 0.1, } - return params def get_param_multiclass_baseline(num_classes): - params = { + return { 'iterations': DEFAULT_ITERATIONS, 'learning_rate': 0.1, } - return params def get_param_regression_baseline(): - params = { + return { 'iterations': DEFAULT_ITERATIONS, 'learning_rate': 0.1, } - return params diff --git a/autogluon/utils/tabular/ml/models/catboost/hyperparameters/searchspaces.py b/autogluon/utils/tabular/ml/models/catboost/hyperparameters/searchspaces.py index d934c282a229..329dd91e5d0c 100644 --- a/autogluon/utils/tabular/ml/models/catboost/hyperparameters/searchspaces.py +++ b/autogluon/utils/tabular/ml/models/catboost/hyperparameters/searchspaces.py @@ -4,38 +4,33 @@ def get_default_searchspace(problem_type, num_classes=None): - if problem_type == BINARY: + if problem_type == BINARY or problem_type not in [MULTICLASS, REGRESSION]: return get_searchspace_binary_baseline() elif problem_type == MULTICLASS: return get_searchspace_multiclass_baseline(num_classes=num_classes) - elif problem_type == REGRESSION: - return get_searchspace_regression_baseline() else: - return get_searchspace_binary_baseline() + return get_searchspace_regression_baseline() def get_searchspace_multiclass_baseline(num_classes): - params = { + return { 'learning_rate': Real(lower=5e-3, upper=0.2, default=0.1, log=True), 'depth': Int(lower=5, upper=8, default=6), 'l2_leaf_reg': Real(lower=1, upper=5, default=3), } - return params def get_searchspace_binary_baseline(): - params = { + return { 'learning_rate': Real(lower=5e-3, upper=0.2, default=0.1, log=True), 'depth': Int(lower=5, upper=8, default=6), 'l2_leaf_reg': Real(lower=1, upper=5, default=3), } - return params def get_searchspace_regression_baseline(): - params = { + return { 'learning_rate': Real(lower=5e-3, upper=0.2, default=0.1, log=True), 'depth': Int(lower=5, upper=8, default=6), 'l2_leaf_reg': Real(lower=1, upper=5, default=3), } - return params diff --git a/autogluon/utils/tabular/ml/models/ensemble/bagged_ensemble_model.py b/autogluon/utils/tabular/ml/models/ensemble/bagged_ensemble_model.py index f0dfc6a6946c..13da74e074a6 100644 --- a/autogluon/utils/tabular/ml/models/ensemble/bagged_ensemble_model.py +++ b/autogluon/utils/tabular/ml/models/ensemble/bagged_ensemble_model.py @@ -54,10 +54,7 @@ def can_infer(self): return self.is_fit() and self.save_bagged_folds def is_stratified(self): - if self.problem_type == REGRESSION or self.problem_type == SOFTCLASS: - return False - else: - return True + return self.problem_type not in [REGRESSION, SOFTCLASS] def is_fit(self): return len(self.models) != 0 @@ -85,8 +82,7 @@ def preprocess(self, X, model=None): return model.preprocess(X) def _fit(self, X, y, k_fold=5, k_fold_start=0, k_fold_end=None, n_repeats=1, n_repeat_start=0, time_limit=None, **kwargs): - if k_fold < 1: - k_fold = 1 + k_fold = max(k_fold, 1) if k_fold_end is None: k_fold_end = k_fold @@ -190,14 +186,13 @@ def _fit(self, X, y, k_fold=5, k_fold_start=0, k_fold_end=None, n_repeats=1, n_r fold_model.set_contexts(self.path + fold_model.name + os.path.sep) fold_model.fit(X_train=X_train, y_train=y_train, X_val=X_val, y_val=y_val, time_limit=time_limit_fold, **kwargs) time_train_end_fold = time.time() - if time_limit is not None: # Check to avoid unnecessarily predicting and saving a model when an Exception is going to be raised later - if i != (fold_end - 1): - time_elapsed = time.time() - time_start - time_left = time_limit - time_elapsed - expected_time_required = time_elapsed * folds_to_fit / (folds_finished + 1) - expected_remaining_time_required = expected_time_required * (folds_left - 1) / folds_to_fit - if expected_remaining_time_required > time_left: - raise TimeLimitExceeded + if time_limit is not None and i != (fold_end - 1): + time_elapsed = time.time() - time_start + time_left = time_limit - time_elapsed + expected_time_required = time_elapsed * folds_to_fit / (folds_finished + 1) + expected_remaining_time_required = expected_time_required * (folds_left - 1) / folds_to_fit + if expected_remaining_time_required > time_left: + raise TimeLimitExceeded pred_proba = fold_model.predict_proba(X_val) time_predict_end_fold = time.time() fold_model.fit_time = time_train_end_fold - time_start_fold @@ -270,7 +265,10 @@ def compute_feature_importance(self, X, y, features_to_use=None, preprocess=True for n_repeat, k in enumerate(self._k_per_n_repeat): if is_oof: if not self.bagged_mode: - raise AssertionError('Model trained with no validation data cannot get feature importances on training data, please specify new test data to compute feature importances (model=%s)' % self.name) + raise AssertionError( + f'Model trained with no validation data cannot get feature importances on training data, please specify new test data to compute feature importances (model={self.name})' + ) + kfolds = generate_kfold(X=X, y=y, n_splits=k, stratified=self.is_stratified(), random_state=self._random_state, n_repeats=n_repeat + 1) cur_kfolds = kfolds[n_repeat * k:(n_repeat+1) * k] else: @@ -289,8 +287,6 @@ def compute_feature_importance(self, X, y, features_to_use=None, preprocess=True for i, result in enumerate(feature_importance_fold_list): feature_importance_fold_list[i] = feature_importance_fold_list[i] * fold_weights[i] - feature_importance = pd.concat(feature_importance_fold_list, axis=1, sort=True).sum(1).sort_values(ascending=False) - # TODO: Consider utilizing z scores and stddev to make threshold decisions # stddev = pd.concat(feature_importance_fold_list, axis=1, sort=True).std(1).sort_values(ascending=False) # feature_importance_df = pd.DataFrame(index=feature_importance.index) @@ -298,7 +294,11 @@ def compute_feature_importance(self, X, y, features_to_use=None, preprocess=True # feature_importance_df['stddev'] = stddev # feature_importance_df['z'] = feature_importance_df['importance'] / feature_importance_df['stddev'] - return feature_importance + return ( + pd.concat(feature_importance_fold_list, axis=1, sort=True) + .sum(1) + .sort_values(ascending=False) + ) def load_child(self, model, verbose=False) -> AbstractModel: if isinstance(model, str): @@ -328,7 +328,7 @@ def _get_compressed_params(self): for child in self.models ] - model_params_compressed = dict() + model_params_compressed = {} for param in model_params_list[0].keys(): model_param_vals = [model_params[param] for model_params in model_params_list] if all(isinstance(val, bool) for val in model_param_vals): @@ -348,10 +348,7 @@ def _get_compressed_params(self): return model_params_compressed def _get_model_base(self): - if self.model_base is None: - return self.load_model_base() - else: - return self.model_base + return self.load_model_base() if self.model_base is None else self.model_base def _add_child_times_to_bag(self, model): if self.fit_time is None: @@ -391,9 +388,7 @@ def load_oof(cls, path, verbose=True): return cls._oof_pred_proba_func(oof_pred_proba=oof_pred_proba, oof_pred_model_repeats=oof_pred_model_repeats) def _load_oof(self): - if self._oof_pred_proba is not None: - pass - else: + if self._oof_pred_proba is None: oof = load_pkl.load(path=self.path + 'utils' + os.path.sep + self._oof_filename) self._oof_pred_proba = oof['_oof_pred_proba'] self._oof_pred_model_repeats = oof['_oof_pred_model_repeats'] @@ -481,10 +476,7 @@ def get_info(self): children_info = self._get_child_info() child_memory_sizes = [child['memory_size'] for child in children_info.values()] sum_memory_size_child = sum(child_memory_sizes) - if child_memory_sizes: - max_memory_size_child = max(child_memory_sizes) - else: - max_memory_size_child = 0 + max_memory_size_child = max(child_memory_sizes, default=0) if self.low_memory: max_memory_size = info['memory_size'] + sum_memory_size_child min_memory_size = info['memory_size'] + max_memory_size_child @@ -513,7 +505,7 @@ def get_info(self): return info def _get_child_info(self): - child_info_dict = dict() + child_info_dict = {} for model in self.models: if isinstance(model, str): child_path = self.create_contexts(self.path + model + os.path.sep) diff --git a/autogluon/utils/tabular/ml/models/ensemble/greedy_weighted_ensemble_model.py b/autogluon/utils/tabular/ml/models/ensemble/greedy_weighted_ensemble_model.py index 3ef27d390a22..c97bf5a7344f 100644 --- a/autogluon/utils/tabular/ml/models/ensemble/greedy_weighted_ensemble_model.py +++ b/autogluon/utils/tabular/ml/models/ensemble/greedy_weighted_ensemble_model.py @@ -16,8 +16,7 @@ def __init__(self, base_model_names, model_base=EnsembleSelection, **kwargs): self.features, self.num_pred_cols_per_model = self.set_stack_columns(base_model_names=self.base_model_names) def _get_default_searchspace(self): - spaces = {} - return spaces + return {} def _set_default_params(self): default_params = {'ensemble_size': 100} @@ -64,7 +63,12 @@ def remove_zero_weight_models(base_model_names, base_model_weights): def set_stack_columns(self, base_model_names): if self.problem_type == MULTICLASS: - stack_columns = [model_name + '_' + str(cls) for model_name in base_model_names for cls in range(self.num_classes)] + stack_columns = [ + f'{model_name}_{str(cls)}' + for model_name in base_model_names + for cls in range(self.num_classes) + ] + num_pred_cols_per_model = self.num_classes else: stack_columns = base_model_names @@ -73,8 +77,7 @@ def set_stack_columns(self, base_model_names): def _get_model_weights(self): num_models = len(self.base_model_names) - model_weight_dict = {self.base_model_names[i]: self.weights_[i] for i in range(num_models)} - return model_weight_dict + return {self.base_model_names[i]: self.weights_[i] for i in range(num_models)} def get_info(self): info = super().get_info() diff --git a/autogluon/utils/tabular/ml/models/ensemble/stacker_ensemble_model.py b/autogluon/utils/tabular/ml/models/ensemble/stacker_ensemble_model.py index 7ce7cfd94c00..7ccefa56e7a2 100644 --- a/autogluon/utils/tabular/ml/models/ensemble/stacker_ensemble_model.py +++ b/autogluon/utils/tabular/ml/models/ensemble/stacker_ensemble_model.py @@ -59,10 +59,9 @@ def limit_models_per_type(models, model_types, model_scores, max_models_per_type for key in model_type_groups: model_type_groups[key] = model_type_groups[key][:max_models_per_type] models_remain = [] - for key in model_type_groups: - models_remain += model_type_groups[key] - models_valid = [model for model, score in models_remain] - return models_valid + for value in model_type_groups.values(): + models_remain += value + return [model for model, score in models_remain] def limit_models(self, models, model_scores, max_models): model_types = {model: '' for model in models} @@ -75,9 +74,8 @@ def _set_default_params(self): def preprocess(self, X, preprocess=True, fit=False, compute_base_preds=True, infer=True, model=None, model_pred_proba_dict=None): if self.stack_column_prefix_lst: - if infer: - if set(self.stack_columns).issubset(set(list(X.columns))): - compute_base_preds = False # TODO: Consider removing, this can be dangerous but the code to make this work otherwise is complex (must rewrite predict_proba) + if infer and set(self.stack_columns).issubset(set(list(X.columns))): + compute_base_preds = False # TODO: Consider removing, this can be dangerous but the code to make this work otherwise is complex (must rewrite predict_proba) if compute_base_preds: X_stacker = [] for stack_column_prefix in self.stack_column_prefix_lst: @@ -93,10 +91,7 @@ def preprocess(self, X, preprocess=True, fit=False, compute_base_preds=True, inf y_pred_proba = base_model.predict_proba(X) X_stacker.append(y_pred_proba) # TODO: This could get very large on a high class count problem. Consider capping to top N most frequent classes and merging least frequent X_stacker = self.pred_probas_to_df(X_stacker, index=X.index) - if self.use_orig_features: - X = pd.concat([X_stacker, X], axis=1) - else: - X = X_stacker + X = pd.concat([X_stacker, X], axis=1) if self.use_orig_features else X_stacker elif not self.use_orig_features: X = X[self.stack_columns] if preprocess: @@ -150,19 +145,21 @@ def set_stack_columns(self, stack_column_prefix_lst): # TODO: Currently double disk usage, saving model in HPO and also saving model in stacker def hyperparameter_tune(self, X, y, k_fold, scheduler_options=None, compute_base_preds=True, **kwargs): if len(self.models) != 0: - raise ValueError('self.models must be empty to call hyperparameter_tune, value: %s' % self.models) + raise ValueError( + f'self.models must be empty to call hyperparameter_tune, value: {self.models}' + ) - if len(self.models) == 0: - if self.feature_types_metadata is None: # TODO: This is probably not the best way to do this - feature_types_raw = defaultdict(list) - feature_types_raw['float'] = self.stack_columns - feature_types_special = defaultdict(list) - feature_types_special['stack'] = self.stack_columns - self.feature_types_metadata = FeatureTypesMetadata(feature_types_raw=feature_types_raw, feature_types_special=feature_types_special) - else: - self.feature_types_metadata = copy.deepcopy(self.feature_types_metadata) - self.feature_types_metadata.feature_types_raw['float'] += self.stack_columns - self.feature_types_metadata.feature_types_special['stack'] += self.stack_columns + + if self.feature_types_metadata is None: # TODO: This is probably not the best way to do this + feature_types_raw = defaultdict(list) + feature_types_raw['float'] = self.stack_columns + feature_types_special = defaultdict(list) + feature_types_special['stack'] = self.stack_columns + self.feature_types_metadata = FeatureTypesMetadata(feature_types_raw=feature_types_raw, feature_types_special=feature_types_special) + else: + self.feature_types_metadata = copy.deepcopy(self.feature_types_metadata) + self.feature_types_metadata.feature_types_raw['float'] += self.stack_columns + self.feature_types_metadata.feature_types_special['stack'] += self.stack_columns self.model_base.feature_types_metadata = self.feature_types_metadata # TODO: Move this # TODO: Preprocess data here instead of repeatedly @@ -226,12 +223,10 @@ def hyperparameter_tune(self, X, y, k_fold, scheduler_options=None, compute_base def load_base_model(self, model_name): if model_name in self.base_models_dict.keys(): - model = self.base_models_dict[model_name] - else: - model_type = self.base_model_types_dict[model_name] - model_path = self.base_model_paths_dict[model_name] - model = model_type.load(model_path) - return model + return self.base_models_dict[model_name] + model_type = self.base_model_types_dict[model_name] + model_path = self.base_model_paths_dict[model_name] + return model_type.load(model_path) def get_info(self): info = super().get_info() diff --git a/autogluon/utils/tabular/ml/models/ensemble/weighted_ensemble_model.py b/autogluon/utils/tabular/ml/models/ensemble/weighted_ensemble_model.py index 8b4e55d0d975..1bc4b3e0e051 100644 --- a/autogluon/utils/tabular/ml/models/ensemble/weighted_ensemble_model.py +++ b/autogluon/utils/tabular/ml/models/ensemble/weighted_ensemble_model.py @@ -14,7 +14,7 @@ class WeightedEnsembleModel(StackerEnsembleModel): def __init__(self, base_model_names, base_model_paths_dict, base_model_types_dict, **kwargs): model_0 = base_model_types_dict[base_model_names[0]].load(path=base_model_paths_dict[base_model_names[0]], verbose=False) super().__init__(model_base=model_0, base_model_names=base_model_names, base_model_paths_dict=base_model_paths_dict, base_model_types_dict=base_model_types_dict, use_orig_features=False, **kwargs) - child_hyperparameters = kwargs.get('_tmp_greedy_hyperparameters', None) # TODO: Rework to avoid this hack + child_hyperparameters = kwargs.get('_tmp_greedy_hyperparameters') self.model_base = GreedyWeightedEnsembleModel(path='', name='greedy_ensemble', num_classes=self.num_classes, base_model_names=self.stack_column_prefix_lst, problem_type=self.problem_type, eval_metric=self.eval_metric, stopping_metric=self.stopping_metric, hyperparameters=child_hyperparameters) self._child_type = type(self.model_base) self.low_memory = False @@ -45,11 +45,6 @@ def _get_model_weights(self): def compute_feature_importance(self, X, y, features_to_use=None, preprocess=True, is_oof=True, **kwargs): logger.warning('Warning: non-raw feature importance calculation is not valid for weighted ensemble since it does not have features, returning ensemble weights instead...') - if is_oof: - feature_importance = pd.Series(self._get_model_weights()).sort_values(ascending=False) - else: + if not is_oof: logger.warning('Warning: Feature importance calculation is not yet implemented for WeightedEnsembleModel on unseen data, returning generic feature importance...') - feature_importance = pd.Series(self._get_model_weights()).sort_values(ascending=False) - # TODO: Rewrite preprocess() in greedy_weighted_ensemble_model to enable - # feature_importance = super().compute_feature_importance(X=X, y=y, features_to_use=features_to_use, preprocess=preprocess, is_oof=is_oof, **kwargs) - return feature_importance + return pd.Series(self._get_model_weights()).sort_values(ascending=False) diff --git a/autogluon/utils/tabular/ml/models/knn/knn_model.py b/autogluon/utils/tabular/ml/models/knn/knn_model.py index 117824f7cc4d..70857607c67d 100644 --- a/autogluon/utils/tabular/ml/models/knn/knn_model.py +++ b/autogluon/utils/tabular/ml/models/knn/knn_model.py @@ -48,8 +48,7 @@ def _set_default_auxiliary_params(self): # TODO: Enable HPO for KNN def _get_default_searchspace(self): - spaces = {} - return spaces + return {} def _fit(self, X_train, y_train, **kwargs): X_train = self.preprocess(X_train) @@ -70,7 +69,16 @@ def _fit(self, X_train, y_train, **kwargs): def hyperparameter_tune(self, X_train, y_train, X_val, y_val, scheduler_options=None, **kwargs): fit_model_args = dict(X_train=X_train, y_train=y_train, **kwargs) predict_proba_args = dict(X=X_val) - model_trial.fit_and_save_model(model=self, params=dict(), fit_args=fit_model_args, predict_proba_args=predict_proba_args, y_val=y_val, time_start=time.time(), time_limit=None) + model_trial.fit_and_save_model( + model=self, + params={}, + fit_args=fit_model_args, + predict_proba_args=predict_proba_args, + y_val=y_val, + time_start=time.time(), + time_limit=None, + ) + hpo_results = {'total_time': self.fit_time} hpo_model_performances = {self.name: self.val_score} hpo_models = {self.name: self.path} diff --git a/autogluon/utils/tabular/ml/models/lgb/callbacks.py b/autogluon/utils/tabular/ml/models/lgb/callbacks.py index ce0e4865a843..b9a0965d85dd 100644 --- a/autogluon/utils/tabular/ml/models/lgb/callbacks.py +++ b/autogluon/utils/tabular/ml/models/lgb/callbacks.py @@ -160,17 +160,13 @@ def _callback(env): best_iter[i] = env.iteration best_score_list[i] = env.evaluation_result_list best_trainloss[i] = train_loss_val - if reporter is not None: # Report current best scores for iteration, used in HPO - if i == indices_to_check[0]: # TODO: documentation needs to note that we assume 0th index is the 'official' validation performance metric. - if cmp_op[i] == gt: - validation_perf = score - else: - validation_perf = -score - reporter(epoch=env.iteration + 1, - validation_performance=validation_perf, - train_loss=best_trainloss[i], - best_iter_sofar=best_iter[i] + 1, - best_valperf_sofar=best_score[i]) + if reporter is not None and i == indices_to_check[0]: + validation_perf = score if cmp_op[i] == gt else -score + reporter(epoch=env.iteration + 1, + validation_performance=validation_perf, + train_loss=best_trainloss[i], + best_iter_sofar=best_iter[i] + 1, + best_valperf_sofar=best_score[i]) if env.iteration - best_iter[i] >= stopping_rounds: if verbose: logger.log(15, 'Early stopping, best iteration is:\n[%d]\t%s' % ( @@ -190,12 +186,11 @@ def _callback(env): raise EarlyStopException(best_iter[i], best_score_list[i]) if verbose: logger.debug((env.iteration - best_iter[i], env.evaluation_result_list[i])) - if manual_stop_file: - if os.path.exists(manual_stop_file): - i = indices_to_check[0] - logger.log(20, 'Found manual stop file, early stopping. Best iteration is:\n[%d]\t%s' % ( - best_iter[i] + 1, '\t'.join([_format_eval_result(x) for x in best_score_list[i]]))) - raise EarlyStopException(best_iter[i], best_score_list[i]) + if manual_stop_file and os.path.exists(manual_stop_file): + i = indices_to_check[0] + logger.log(20, 'Found manual stop file, early stopping. Best iteration is:\n[%d]\t%s' % ( + best_iter[i] + 1, '\t'.join([_format_eval_result(x) for x in best_score_list[i]]))) + raise EarlyStopException(best_iter[i], best_score_list[i]) if time_limit: time_elapsed = time.time() - start_time time_left = time_limit - time_elapsed @@ -218,21 +213,21 @@ def _callback(env): model_size_memory_ratio = estimated_model_size_mb / available_mb if verbose or (model_size_memory_ratio > 0.25): - logging.debug('Available Memory: '+str(available_mb)+' MB') - logging.debug('Estimated Model Size: '+str(estimated_model_size_mb)+' MB') + logging.debug(f'Available Memory: {str(available_mb)} MB') + logging.debug(f'Estimated Model Size: {str(estimated_model_size_mb)} MB') early_stop = False if model_size_memory_ratio > 1.0: logger.warning('Warning: Large GBM model size may cause OOM error if training continues') - logger.warning('Available Memory: '+str(available_mb)+' MB') - logger.warning('Estimated GBM model size: '+str(estimated_model_size_mb)+' MB') + logger.warning(f'Available Memory: {str(available_mb)} MB') + logger.warning(f'Estimated GBM model size: {str(estimated_model_size_mb)} MB') early_stop = True # TODO: We will want to track size of model as well, even if we early stop before OOM, we will still crash when saving if the model is large enough if available_mb < 512: # Less than 500 MB logger.warning('Warning: Low available memory may cause OOM error if training continues') - logger.warning('Available Memory: '+str(available_mb)+' MB') - logger.warning('Estimated GBM model size: '+str(estimated_model_size_mb)+' MB') + logger.warning(f'Available Memory: {str(available_mb)} MB') + logger.warning(f'Estimated GBM model size: {str(estimated_model_size_mb)} MB') early_stop = True if early_stop: diff --git a/autogluon/utils/tabular/ml/models/lgb/hyperparameters/parameters.py b/autogluon/utils/tabular/ml/models/lgb/hyperparameters/parameters.py index 8eb8aae8f40a..00aebac27ee4 100644 --- a/autogluon/utils/tabular/ml/models/lgb/hyperparameters/parameters.py +++ b/autogluon/utils/tabular/ml/models/lgb/hyperparameters/parameters.py @@ -6,29 +6,33 @@ def get_param_baseline_custom(problem_type, num_classes=None): - if problem_type == BINARY: + if problem_type == BINARY or problem_type not in [ + MULTICLASS, + REGRESSION, + SOFTCLASS, + ]: return get_param_binary_baseline_custom() elif problem_type == MULTICLASS: return get_param_multiclass_baseline_custom(num_classes=num_classes) elif problem_type == REGRESSION: return get_param_regression_baseline_custom() - elif problem_type == SOFTCLASS: - return get_param_softclass_baseline_custom(num_classes=num_classes) else: - return get_param_binary_baseline_custom() + return get_param_softclass_baseline_custom(num_classes=num_classes) def get_param_baseline(problem_type, num_classes=None): - if problem_type == BINARY: + if problem_type == BINARY or problem_type not in [ + MULTICLASS, + REGRESSION, + SOFTCLASS, + ]: return get_param_binary_baseline() elif problem_type == MULTICLASS: return get_param_multiclass_baseline(num_classes=num_classes) elif problem_type == REGRESSION: return get_param_regression_baseline() - elif problem_type == SOFTCLASS: - return get_param_softclass_baseline(num_classes=num_classes) else: - return get_param_binary_baseline() + return get_param_softclass_baseline(num_classes=num_classes) def get_param_multiclass_baseline_custom(num_classes): @@ -52,7 +56,7 @@ def get_param_multiclass_baseline_custom(num_classes): def get_param_binary_baseline(): - params = { + return { 'num_boost_round': DEFAULT_NUM_BOOST_ROUND, 'num_threads': -1, 'objective': 'binary', @@ -60,11 +64,10 @@ def get_param_binary_baseline(): 'boosting_type': 'gbdt', 'two_round': True, } - return params def get_param_multiclass_baseline(num_classes): - params = { + return { 'num_boost_round': DEFAULT_NUM_BOOST_ROUND, 'num_threads': -1, 'objective': 'multiclass', @@ -73,11 +76,10 @@ def get_param_multiclass_baseline(num_classes): 'boosting_type': 'gbdt', 'two_round': True, } - return params def get_param_regression_baseline(): - params = { + return { 'num_boost_round': DEFAULT_NUM_BOOST_ROUND, 'num_threads': -1, 'objective': 'regression', @@ -85,11 +87,10 @@ def get_param_regression_baseline(): 'boosting_type': 'gbdt', 'two_round': True, } - return params def get_param_binary_baseline_dummy_gpu(): - params = { + return { 'num_boost_round': DEFAULT_NUM_BOOST_ROUND, 'num_threads': -1, 'objective': 'binary', @@ -98,7 +99,6 @@ def get_param_binary_baseline_dummy_gpu(): 'two_round': True, 'device_type': 'gpu', } - return params def get_param_binary_baseline_custom(): diff --git a/autogluon/utils/tabular/ml/models/lgb/hyperparameters/searchspaces.py b/autogluon/utils/tabular/ml/models/lgb/hyperparameters/searchspaces.py index aa269f85529a..f582c481241b 100644 --- a/autogluon/utils/tabular/ml/models/lgb/hyperparameters/searchspaces.py +++ b/autogluon/utils/tabular/ml/models/lgb/hyperparameters/searchspaces.py @@ -6,24 +6,26 @@ def get_default_searchspace(problem_type, num_classes=None): - if problem_type == BINARY: + if problem_type == BINARY or problem_type not in [MULTICLASS, REGRESSION]: return get_searchspace_binary_baseline() elif problem_type == MULTICLASS: return get_searchspace_multiclass_baseline(num_classes=num_classes) - elif problem_type == REGRESSION: - return get_searchspace_regression_baseline() else: - return get_searchspace_binary_baseline() + return get_searchspace_regression_baseline() def get_searchspace_multiclass_baseline(num_classes): - params = { + return { 'objective': 'multiclass', 'num_classes': num_classes, 'learning_rate': Real(lower=5e-3, upper=0.2, default=0.1, log=True), 'feature_fraction': Real(lower=0.75, upper=1.0, default=1.0), - 'min_data_in_leaf': Int(lower=2, upper=30, default=20), # TODO: Use size of dataset to set upper, if row count is small upper should be small - 'num_leaves': Int(lower=16, upper=96, default=31), # TODO: Use row count and feature count to set this, the higher feature count the higher num_leaves upper + 'min_data_in_leaf': Int( + lower=2, upper=30, default=20 + ), # TODO: Use size of dataset to set upper, if row count is small upper should be small + 'num_leaves': Int( + lower=16, upper=96, default=31 + ), # TODO: Use row count and feature count to set this, the higher feature count the higher num_leaves upper 'num_boost_round': DEFAULT_NUM_BOOST_ROUND, 'boosting_type': 'gbdt', 'verbose': -1, @@ -32,11 +34,10 @@ def get_searchspace_multiclass_baseline(num_classes): # 'device': 'gpu' # needs GPU-enabled lightGBM build # TODO: Bin size max increase } - return params def get_searchspace_binary_baseline(): - params = { + return { 'objective': 'binary', 'learning_rate': Real(lower=5e-3, upper=0.2, default=0.1, log=True), 'feature_fraction': Real(lower=0.75, upper=1.0, default=1.0), @@ -48,11 +49,10 @@ def get_searchspace_binary_baseline(): 'two_round': True, 'seed_value': None, } - return params def get_searchspace_regression_baseline(): - params = { + return { 'objective': 'regression', 'learning_rate': Real(lower=5e-3, upper=0.2, default=0.1, log=True), 'feature_fraction': Real(lower=0.75, upper=1.0, default=1.0), @@ -64,6 +64,5 @@ def get_searchspace_regression_baseline(): 'two_round': True, 'seed_value': None, } - return params diff --git a/autogluon/utils/tabular/ml/models/lgb/lgb_model.py b/autogluon/utils/tabular/ml/models/lgb/lgb_model.py index 421d06206a44..c52ebafffa63 100644 --- a/autogluon/utils/tabular/ml/models/lgb/lgb_model.py +++ b/autogluon/utils/tabular/ml/models/lgb/lgb_model.py @@ -79,9 +79,11 @@ def _fit(self, X_train=None, y_train=None, X_val=None, y_val=None, dataset_train logger.log(15, params) num_rows_train = len(dataset_train.data) - if 'min_data_in_leaf' in params: - if params['min_data_in_leaf'] > num_rows_train: # TODO: may not be necessary - params['min_data_in_leaf'] = max(1, int(num_rows_train / 5.0)) + if ( + 'min_data_in_leaf' in params + and params['min_data_in_leaf'] > num_rows_train + ): + params['min_data_in_leaf'] = max(1, int(num_rows_train / 5.0)) # TODO: Better solution: Track trend to early stop when score is far worse than best score, or score is trending worse over time if (dataset_val is not None) and (dataset_train is not None): @@ -94,7 +96,7 @@ def _fit(self, X_train=None, y_train=None, X_val=None, y_val=None, dataset_train valid_names = ['train_set'] valid_sets = [dataset_train] if dataset_val is not None: - reporter = kwargs.get('reporter', None) + reporter = kwargs.get('reporter') train_loss_name = self._get_train_loss_name() if reporter is not None else None if train_loss_name is not None: if 'metric' not in params or params['metric'] == '': @@ -121,11 +123,10 @@ def _fit(self, X_train=None, y_train=None, X_val=None, y_val=None, dataset_train } if not isinstance(eval_metric, str): train_params['feval'] = eval_metric - else: - if 'metric' not in train_params['params'] or train_params['params']['metric'] == '': - train_params['params']['metric'] = eval_metric - elif eval_metric not in train_params['params']['metric']: - train_params['params']['metric'] = f'{train_params["params"]["metric"]},{eval_metric}' + elif 'metric' not in train_params['params'] or train_params['params']['metric'] == '': + train_params['params']['metric'] = eval_metric + elif eval_metric not in train_params['params']['metric']: + train_params['params']['metric'] = f'{train_params["params"]["metric"]},{eval_metric}' if self.problem_type == SOFTCLASS: train_params['fobj'] = lgb_utils.softclass_lgbobj if seed_val is not None: @@ -147,12 +148,10 @@ def _predict_proba(self, X, preprocess=True): y_pred_proba = self.model.predict(X) if self.problem_type == BINARY: - if len(y_pred_proba.shape) == 1: + if len(y_pred_proba.shape) == 1 or y_pred_proba.shape[1] <= 1: return y_pred_proba - elif y_pred_proba.shape[1] > 1: - return y_pred_proba[:, 1] else: - return y_pred_proba + return y_pred_proba[:, 1] elif self.problem_type == MULTICLASS: return y_pred_proba elif self.problem_type == SOFTCLASS: # apply softmax @@ -160,9 +159,7 @@ def _predict_proba(self, X, preprocess=True): y_pred_proba = np.multiply(y_pred_proba, 1/np.sum(y_pred_proba, axis=1)[:, np.newaxis]) return y_pred_proba else: - if len(y_pred_proba.shape) == 1: - return y_pred_proba - elif y_pred_proba.shape[1] > 2: # Should this ever happen? + if len(y_pred_proba.shape) == 1 or y_pred_proba.shape[1] > 2: return y_pred_proba else: # Should this ever happen? return y_pred_proba[:, 1] @@ -336,7 +333,7 @@ def _get_train_loss_name(self): def get_model_feature_importance(self, use_original_feature_names=False): feature_names = self.model.feature_name() importances = self.model.feature_importance() - importance_dict = {feature_name: importance for (feature_name, importance) in zip(feature_names, importances)} + importance_dict = dict(zip(feature_names, importances)) if use_original_feature_names and (self._internal_feature_map is not None): inverse_internal_feature_map = {i: feature for feature, i in self._internal_feature_map.items()} importance_dict = {inverse_internal_feature_map[i]: importance for i, importance in importance_dict.items()} diff --git a/autogluon/utils/tabular/ml/models/lgb/lgb_utils.py b/autogluon/utils/tabular/ml/models/lgb/lgb_utils.py index 302d0cd51d8f..7f480e3ca61a 100644 --- a/autogluon/utils/tabular/ml/models/lgb/lgb_utils.py +++ b/autogluon/utils/tabular/ml/models/lgb/lgb_utils.py @@ -27,7 +27,7 @@ def convert_ag_metric_to_lgbm(ag_metric_name, problem_type): - return _ag_to_lgbm_metric_dict.get(problem_type, dict()).get(ag_metric_name, None) + return _ag_to_lgbm_metric_dict.get(problem_type, {}).get(ag_metric_name, None) diff --git a/autogluon/utils/tabular/ml/models/lr/hyperparameters/parameters.py b/autogluon/utils/tabular/ml/models/lr/hyperparameters/parameters.py index aba1001a8ce8..fb578e4f7855 100644 --- a/autogluon/utils/tabular/ml/models/lr/hyperparameters/parameters.py +++ b/autogluon/utils/tabular/ml/models/lr/hyperparameters/parameters.py @@ -15,16 +15,18 @@ def get_param_baseline(): - default_params = { + return { 'C': 1, 'vectorizer_dict_size': 75000, # size of TFIDF vectorizer dictionary; used only in text model - 'proc.ngram_range': (1, 5), # range of n-grams for TFIDF vectorizer dictionary; used only in text model + 'proc.ngram_range': ( + 1, + 5, + ), # range of n-grams for TFIDF vectorizer dictionary; used only in text model 'proc.skew_threshold': 0.99, # numerical features whose absolute skewness is greater than this receive special power-transform preprocessing. Choose big value to avoid using power-transforms 'proc.impute_strategy': 'median', # strategy argument of sklearn.SimpleImputer() used to impute missing numeric values 'penalty': L2, # regularization to use with regression models - 'handle_text': IGNORE, # how text should be handled: `ignore` - don't use NLP features; `only` - only use NLP features; `include` - use both regular and NLP features + 'handle_text': IGNORE, # how text should be handled: `ignore` - don't use NLP features; `only` - only use NLP features; `include` - use both regular and NLP features } - return default_params def get_model_params(problem_type: str, hyperparameters): @@ -36,7 +38,10 @@ def get_model_params(problem_type: str, hyperparameters): elif penalty == L1: model_class = Lasso else: - logger.warning('Unknown value for penalty {} - supported types are [l1, l2] - falling back to l2'.format(penalty)) + logger.warning( + f'Unknown value for penalty {penalty} - supported types are [l1, l2] - falling back to l2' + ) + penalty = L2 model_class = Ridge else: @@ -58,9 +63,4 @@ def get_default_params(problem_type: str, penalty: str): def _get_solver(problem_type): - if problem_type == BINARY: - # TODO explore using liblinear for smaller datasets - solver = 'lbfgs' - else: - solver = 'lbfgs' - return solver + return 'lbfgs' diff --git a/autogluon/utils/tabular/ml/models/lr/hyperparameters/searchspaces.py b/autogluon/utils/tabular/ml/models/lr/hyperparameters/searchspaces.py index 516d627b942d..763a51412c0f 100644 --- a/autogluon/utils/tabular/ml/models/lr/hyperparameters/searchspaces.py +++ b/autogluon/utils/tabular/ml/models/lr/hyperparameters/searchspaces.py @@ -1,7 +1,6 @@ def get_default_searchspace(problem_type, num_classes=None): - spaces = { + return { # 'C': Real(lower=1e-4, upper=1e5, default=1), # 'tokenizer': Categorical('split', 'sentencepiece'), # 'fit_intercept': Categorical(True', False), } - return spaces diff --git a/autogluon/utils/tabular/ml/models/lr/lr_model.py b/autogluon/utils/tabular/ml/models/lr/lr_model.py index 7bf5180c8184..169a1696382b 100644 --- a/autogluon/utils/tabular/ml/models/lr/lr_model.py +++ b/autogluon/utils/tabular/ml/models/lr/lr_model.py @@ -133,7 +133,7 @@ def _fit(self, X_train, y_train, X_val=None, y_val=None, time_limit=None, **kwar # TODO: copy_X=True currently set during regression problem type, could potentially set to False to avoid unnecessary data copy. model = self.model_class(**params) - logger.log(15, f'Training Model with the following hyperparameter settings:') + logger.log(15, 'Training Model with the following hyperparameter settings:') logger.log(15, model) self.model = model.fit(X_train, y_train) diff --git a/autogluon/utils/tabular/ml/models/lr/lr_preprocessing_utils.py b/autogluon/utils/tabular/ml/models/lr/lr_preprocessing_utils.py index 5906b3cd7a5b..d4fc8a9339da 100644 --- a/autogluon/utils/tabular/ml/models/lr/lr_preprocessing_utils.py +++ b/autogluon/utils/tabular/ml/models/lr/lr_preprocessing_utils.py @@ -27,9 +27,7 @@ def transform(self, X, y=None): # Update feature names self._feature_names = [] for k, v in self.labels.items(): - for f in k + '_' + v[0]: - self._feature_names.append(f) - + self._feature_names.extend(iter(f'{k}_{v[0]}')) return hstack(Xs) def _normalize(self, col): diff --git a/autogluon/utils/tabular/ml/models/rf/rf_model.py b/autogluon/utils/tabular/ml/models/rf/rf_model.py index 647f8123c2a1..c00ad25878b7 100644 --- a/autogluon/utils/tabular/ml/models/rf/rf_model.py +++ b/autogluon/utils/tabular/ml/models/rf/rf_model.py @@ -42,12 +42,11 @@ def _set_default_params(self): # TODO: Add in documentation that Categorical default is the first index # TODO: enable HPO for RF models def _get_default_searchspace(self): - spaces = { + return { # 'n_estimators': Int(lower=10, upper=1000, default=300), # 'max_features': Categorical(['auto', 0.5, 0.25]), # 'criterion': Categorical(['gini', 'entropy']), } - return spaces def _fit(self, X_train, y_train, time_limit=None, **kwargs): time_start = time.time() @@ -66,10 +65,7 @@ def _fit(self, X_train, y_train, time_limit=None, **kwargs): # Very rough guess to size of a single tree before training if self.problem_type == MULTICLASS: - if self.num_classes is None: - num_trees_per_estimator = 10 # Guess since it wasn't passed in, could also check y_train for a better value - else: - num_trees_per_estimator = self.num_classes + num_trees_per_estimator = 10 if self.num_classes is None else self.num_classes else: num_trees_per_estimator = 1 bytes_per_estimator = num_trees_per_estimator * len(X_train) / 60000 * 1e6 # Underestimates by 3x on ExtraTrees @@ -80,15 +76,14 @@ def _fit(self, X_train, y_train, time_limit=None, **kwargs): logger.warning(f'\tWarning: Model is expected to require {expected_min_memory_usage * 100} percent of available memory (Estimated before training)...') raise NotEnoughMemoryError - if n_estimators_final > n_estimators_test * 2: - if self.problem_type == MULTICLASS: - n_estimator_increments = [n_estimators_test, n_estimators_final] - hyperparams['warm_start'] = True - else: - if expected_memory_usage > (0.05 * max_memory_usage_ratio): # Somewhat arbitrary, consider finding a better value, should it scale by cores? - # Causes ~10% training slowdown, so try to avoid if memory is not an issue - n_estimator_increments = [n_estimators_test, n_estimators_final] - hyperparams['warm_start'] = True + if n_estimators_final > n_estimators_test * 2 and ( + self.problem_type != MULTICLASS + and expected_memory_usage > (0.05 * max_memory_usage_ratio) + or self.problem_type == MULTICLASS + ): + # Causes ~10% training slowdown, so try to avoid if memory is not an issue + n_estimator_increments = [n_estimators_test, n_estimators_final] + hyperparams['warm_start'] = True hyperparams['n_estimators'] = n_estimator_increments[0] self.model = self._model_type(**hyperparams) @@ -134,7 +129,16 @@ def _fit(self, X_train, y_train, time_limit=None, **kwargs): def hyperparameter_tune(self, X_train, y_train, X_val, y_val, scheduler_options=None, **kwargs): fit_model_args = dict(X_train=X_train, y_train=y_train, **kwargs) predict_proba_args = dict(X=X_val) - model_trial.fit_and_save_model(model=self, params=dict(), fit_args=fit_model_args, predict_proba_args=predict_proba_args, y_val=y_val, time_start=time.time(), time_limit=None) + model_trial.fit_and_save_model( + model=self, + params={}, + fit_args=fit_model_args, + predict_proba_args=predict_proba_args, + y_val=y_val, + time_start=time.time(), + time_limit=None, + ) + hpo_results = {'total_time': self.fit_time} hpo_model_performances = {self.name: self.val_score} hpo_models = {self.name: self.path} @@ -144,5 +148,5 @@ def get_model_feature_importance(self): if self.features is None: # TODO: Consider making this raise an exception logger.warning('Warning: get_model_feature_importance called when self.features is None!') - return dict() + return {} return dict(zip(self.features, self.model.feature_importances_)) diff --git a/autogluon/utils/tabular/ml/models/tabular_nn/categorical_encoders.py b/autogluon/utils/tabular/ml/models/tabular_nn/categorical_encoders.py index 7b68a39f6b24..91fc20777b5f 100644 --- a/autogluon/utils/tabular/ml/models/tabular_nn/categorical_encoders.py +++ b/autogluon/utils/tabular/ml/models/tabular_nn/categorical_encoders.py @@ -22,18 +22,15 @@ def _encode_numpy(values, uniques=None, encode=False, check_unknown=True): # only used in _encode below, see docstring there for details if uniques is None: - if encode: - uniques, encoded = np.unique(values, return_inverse=True) - return uniques, encoded - else: + if not encode: # unique sorts return np.unique(values) + uniques, encoded = np.unique(values, return_inverse=True) + return uniques, encoded if encode: if check_unknown: - diff = _encode_check_unknown(values, uniques) - if diff: - raise ValueError("y contains previously unseen labels: %s" - % str(diff)) + if diff := _encode_check_unknown(values, uniques): + raise ValueError(f"y contains previously unseen labels: {str(diff)}") encoded = np.searchsorted(uniques, values) return uniques, encoded else: @@ -45,16 +42,14 @@ def _encode_python(values, uniques=None, encode=False): if uniques is None: uniques = sorted(set(values)) uniques = np.array(uniques, dtype=values.dtype) - if encode: - table = {val: i for i, val in enumerate(uniques)} - try: - encoded = np.array([table[v] for v in values]) - except KeyError as e: - raise ValueError("y contains previously unseen labels: %s" - % str(e)) - return uniques, encoded - else: + if not encode: return uniques + table = {val: i for i, val in enumerate(uniques)} + try: + encoded = np.array([table[v] for v in values]) + except KeyError as e: + raise ValueError(f"y contains previously unseen labels: {str(e)}") + return uniques, encoded def _encode(values, uniques=None, encode=False, check_unknown=True): @@ -89,15 +84,14 @@ def _encode(values, uniques=None, encode=False, check_unknown=True): (uniques, encoded) If ``encode=True``. """ - if values.dtype == object: - try: - res = _encode_python(values, uniques, encode) - except TypeError: - raise TypeError("argument must be a string or number") - return res - else: + if values.dtype != object: return _encode_numpy(values, uniques, encode, check_unknown=check_unknown) + try: + res = _encode_python(values, uniques, encode) + except TypeError: + raise TypeError("argument must be a string or number") + return res def _encode_check_unknown(values, uniques, return_mask=False): @@ -125,25 +119,26 @@ def _encode_check_unknown(values, uniques, return_mask=False): if values.dtype == object: uniques_set = set(uniques) diff = list(set(values) - uniques_set) - if return_mask: - if diff: - valid_mask = np.array([val in uniques_set for val in values]) - else: - valid_mask = np.ones(len(values), dtype=bool) - return diff, valid_mask - else: + if not return_mask: return diff + valid_mask = ( + np.array([val in uniques_set for val in values]) + if diff + else np.ones(len(values), dtype=bool) + ) + else: unique_values = np.unique(values) diff = list(np.setdiff1d(unique_values, uniques, assume_unique=True)) - if return_mask: - if diff: - valid_mask = np.in1d(values, uniques) - else: - valid_mask = np.ones(len(values), dtype=bool) - return diff, valid_mask - else: + if not return_mask: return diff + valid_mask = ( + np.in1d(values, uniques) + if diff + else np.ones(len(values), dtype=bool) + ) + + return diff, valid_mask class _BaseEncoder(BaseEstimator, TransformerMixin): @@ -177,61 +172,55 @@ def _check_X(self, X): # pandas dataframe, do validation later column by column, in order # to keep the dtype information to be used in the encoder. needs_validation = True - + n_samples, n_features = X.shape X_columns = [] - + for i in range(n_features): Xi = self._get_feature(X, feature_idx=i) Xi = check_array(Xi, ensure_2d=False, dtype=None, force_all_finite=needs_validation) X_columns.append(Xi) - + return X_columns, n_samples, n_features def _get_feature(self, X, feature_idx): - if hasattr(X, 'iloc'): - # pandas dataframes - return X.iloc[:, feature_idx] - # numpy arrays, sparse arrays - return X[:, feature_idx] + return X.iloc[:, feature_idx] if hasattr(X, 'iloc') else X[:, feature_idx] def _fit(self, X, handle_unknown='error'): X_list, n_samples, n_features = self._check_X(X) - - if self.categories != 'auto': - if len(self.categories) != n_features: - raise ValueError("Shape mismatch: if categories is an array," - " it has to be of shape (n_features,).") - - if self.max_levels is not None: - if (not isinstance(self.max_levels, Integral) or - self.max_levels <= 0): - raise ValueError("max_levels must be None or a strictly " - "positive int, got {}.".format( - self.max_levels)) - + + if self.categories != 'auto' and len(self.categories) != n_features: + raise ValueError("Shape mismatch: if categories is an array," + " it has to be of shape (n_features,).") + + if self.max_levels is not None and ( + (not isinstance(self.max_levels, Integral) or self.max_levels <= 0) + ): + raise ValueError( + f"max_levels must be None or a strictly positive int, got {self.max_levels}." + ) + + self.categories_ = [] self.infrequent_indices_ = [] - + for i in range(n_features): Xi = X_list[i] if self.categories == 'auto': cats = _encode(Xi) else: cats = np.array(self.categories[i], dtype=Xi.dtype) - if Xi.dtype != object: - if not np.all(np.sort(cats) == cats): - raise ValueError("Unsorted categories are not " - "supported for numerical categories") + if Xi.dtype != object and not np.all(np.sort(cats) == cats): + raise ValueError("Unsorted categories are not " + "supported for numerical categories") if handle_unknown == 'error': - diff = _encode_check_unknown(Xi, cats) - if diff: + if diff := _encode_check_unknown(Xi, cats): msg = ("Found unknown categories {0} in column {1}" " during fit".format(diff, i)) raise ValueError(msg) self.categories_.append(cats) - + if self.max_levels is not None: infrequent_indices = self._find_infrequent_category_indices(Xi) else: @@ -246,23 +235,21 @@ def _find_infrequent_category_indices(self, Xi): def _transform(self, X, handle_unknown='error'): X_list, n_samples, n_features = self._check_X(X) - + X_int = np.zeros((n_samples, n_features), dtype=np.int) X_mask = np.ones((n_samples, n_features), dtype=np.bool) - + if n_features != len(self.categories_): raise ValueError( - "The number of features in X is different to the number of " - "features of the fitted data. The fitted data had {} features " - "and the X has {} features." - .format(len(self.categories_,), n_features) + f"The number of features in X is different to the number of features of the fitted data. The fitted data had {len(self.categories_)} features and the X has {n_features} features." ) - + + for i in range(n_features): Xi = X_list[i] diff, valid_mask = _encode_check_unknown(Xi, self.categories_[i], return_mask=True) - + if not np.all(valid_mask): if handle_unknown == 'error': msg = ("Found unknown categories {0} in column {1}" @@ -280,14 +267,14 @@ def _transform(self, X, handle_unknown='error'): Xi = Xi.astype(self.categories_[i].dtype) else: Xi = Xi.copy() - + Xi[~valid_mask] = self.categories_[i][0] # We use check_unknown=False, since _encode_check_unknown was # already called above. _, encoded = _encode(Xi, self.categories_[i], encode=True, check_unknown=False) X_int[:, i] = encoded - + # We need to take care of infrequent categories here. We want all the # infrequent categories to end up in a specific column, after all the # frequent ones. Let's say we have 4 categories with 2 infrequent @@ -306,11 +293,11 @@ def _transform(self, X, handle_unknown='error'): for ordinal_cat in self.infrequent_indices_[feature_idx]: mapping[ordinal_cat] = huge_int _, mapping = _encode_numpy(mapping, encode=True) - + # update X_int and save mapping for later (for dropping logic) X_int[:, feature_idx] = mapping[X_int[:, feature_idx]] self._infrequent_mappings[feature_idx] = mapping - + return X_int, X_mask def _more_tags(self): @@ -437,12 +424,19 @@ def _compute_drop_idx(self): missing_drops = [(i, val) for i, val in enumerate(self.drop) if val not in self.categories_[i]] if any(missing_drops): - msg = ("The following categories were supposed to be " - "dropped, but were not found in the training " - "data.\n{}".format( - "\n".join( - ["Category: {}, Feature: {}".format(c, v) - for c, v in missing_drops]))) + msg = ( + "The following categories were supposed to be " + "dropped, but were not found in the training " + "data.\n{}".format( + "\n".join( + [ + f"Category: {c}, Feature: {v}" + for c, v in missing_drops + ] + ) + ) + ) + raise ValueError(msg) return np.array([np.where(cat_list == val)[0][0] for (val, cat_list) in @@ -475,13 +469,9 @@ def fit(self, X, y=None): zip(self.infrequent_indices_, self.drop_idx_)): if drop_idx in infrequent_indices: raise ValueError( - "Category {} of feature {} is infrequent and thus " - "cannot be dropped. Use drop='infrequent' " - "instead.".format( - self.categories_[feature_idx][drop_idx], - feature_idx - ) + f"Category {self.categories_[feature_idx][drop_idx]} of feature {feature_idx} is infrequent and thus cannot be dropped. Use drop='infrequent' instead." ) + return self def fit_transform(self, X, y=None): @@ -521,7 +511,7 @@ def transform(self, X): # validation of X happens in _check_X called by _transform X_int, X_mask = self._transform(X, handle_unknown=self.handle_unknown) n_samples, n_features = X_int.shape - + # n_columns indicates, for each feature, how many columns are used in # X_trans. By default this corresponds to the number of categories, but # will differ if we drop some of them, or if there are infrequent @@ -540,7 +530,7 @@ def transform(self, X): if (isinstance(self.drop, str) and self.drop == 'infrequent' and n_infrequent == 0): n_columns[feature_idx] += 1 # revert decrement from above - + if self.drop is not None: to_drop = self.drop_idx_.copy() if isinstance(self.drop, str): @@ -561,14 +551,14 @@ def transform(self, X): if self.infrequent_indices_[feature_idx].size > 0: mapping = self._infrequent_mappings[feature_idx] to_drop[feature_idx] = mapping[to_drop[feature_idx]] - + # We remove all the dropped categories from mask, and decrement # all categories that occur after them to avoid an empty column. to_drop = to_drop.reshape(1, -1) keep_cells = (X_int != to_drop) | (to_drop == -1) X_mask &= keep_cells X_int[(X_int > to_drop) & (to_drop != -1)] -= 1 - + mask = X_mask.ravel() n_values = np.array([0] + n_columns) feature_indices = np.cumsum(n_values) @@ -576,14 +566,11 @@ def transform(self, X): indptr = X_mask.sum(axis=1).cumsum() indptr = np.insert(indptr, 0, 0) data = np.ones(n_samples * n_features)[mask] - + out = sparse.csr_matrix((data, indices, indptr), shape=(n_samples, feature_indices[-1]), dtype=self.dtype) - if not self.sparse: - return out.toarray() - else: - return out + return out if self.sparse else out.toarray() def inverse_transform(self, X): """Convert the back data to the original representation. @@ -604,7 +591,7 @@ def inverse_transform(self, X): """ check_is_fitted(self, 'categories_') X = check_array(X, accept_sparse='csr') - + n_samples, _ = X.shape n_features = len(self.categories_) if self.drop is None: @@ -613,26 +600,26 @@ def inverse_transform(self, X): else: n_transformed_features = sum(len(cats) - 1 for cats in self.categories_) - - # validate shape of passed X - msg = ("Shape of the passed X data is not correct. Expected {0} " - "columns, got {1}.") + if X.shape[1] != n_transformed_features: + # validate shape of passed X + msg = ("Shape of the passed X data is not correct. Expected {0} " + "columns, got {1}.") raise ValueError(msg.format(n_transformed_features, X.shape[1])) - + # create resulting array of appropriate dtype dt = np.find_common_type([cat.dtype for cat in self.categories_], []) X_tr = np.empty((n_samples, n_features), dtype=dt) j = 0 found_unknown = {} - + for i in range(n_features): if self.drop is None: cats = self.categories_[i] else: cats = np.delete(self.categories_[i], self.drop_idx_[i]) n_categories = len(cats) - + # Only happens if there was a column with a unique # category. In this case we just fill the column with this # unique category value. @@ -655,18 +642,18 @@ def inverse_transform(self, X): dropped = np.asarray(sub.sum(axis=1) == 0).flatten() if dropped.any(): X_tr[dropped, i] = self.categories_[i][self.drop_idx_[i]] - + j += n_categories - + # if ignored are found: potentially need to upcast result to # insert None values if found_unknown: if X_tr.dtype != object: X_tr = X_tr.astype(object) - + for idx, mask in found_unknown.items(): X_tr[mask, idx] = None - + return X_tr def get_feature_names(self, input_features=None): @@ -689,18 +676,17 @@ def get_feature_names(self, input_features=None): input_features = ['x%d' % i for i in range(len(cats))] elif len(input_features) != len(self.categories_): raise ValueError( - "input_features should have length equal to number of " - "features ({}), got {}".format(len(self.categories_), - len(input_features))) - + f"input_features should have length equal to number of features ({len(self.categories_)}), got {len(input_features)}" + ) + + feature_names = [] for i in range(len(cats)): - names = [ - input_features[i] + '_' + str(t) for t in cats[i]] + names = [f'{input_features[i]}_{str(t)}' for t in cats[i]] if self.drop is not None: names.pop(self.drop_idx_[i]) feature_names.extend(names) - + return np.array(feature_names, dtype=object) @@ -811,25 +797,25 @@ def inverse_transform(self, X): """ check_is_fitted(self, 'categories_') X = check_array(X, accept_sparse='csr') - + n_samples, _ = X.shape n_features = len(self.categories_) - - # validate shape of passed X - msg = ("Shape of the passed X data is not correct. Expected {0} " - "columns, got {1}.") + if X.shape[1] != n_features: + # validate shape of passed X + msg = ("Shape of the passed X data is not correct. Expected {0} " + "columns, got {1}.") raise ValueError(msg.format(n_features, X.shape[1])) - + # create resulting array of appropriate dtype dt = np.find_common_type([cat.dtype for cat in self.categories_], []) X_tr = np.empty((n_samples, n_features), dtype=dt) - + for i in range(n_features): possible_categories = np.append(self.categories_[i], None) labels = X[:, i].astype('int64', copy=False) X_tr[:, i] = self.categories_[i][labels] - + return X_tr diff --git a/autogluon/utils/tabular/ml/models/tabular_nn/embednet.py b/autogluon/utils/tabular/ml/models/tabular_nn/embednet.py index 047e429f332f..0c2808a95c47 100644 --- a/autogluon/utils/tabular/ml/models/tabular_nn/embednet.py +++ b/autogluon/utils/tabular/ml/models/tabular_nn/embednet.py @@ -95,7 +95,7 @@ def __init__(self, train_dataset=None, params=None, num_net_outputs=None, archit if self.has_embed_features: num_categs_per_feature = architecture_desc['num_categs_per_feature'] embed_dims = architecture_desc['embed_dims'] - + # Define neural net parameters: if self.has_vector_features: self.numeric_block = NumericBlock(params) @@ -111,8 +111,8 @@ def __init__(self, train_dataset=None, params=None, num_net_outputs=None, archit elif params['network_type'] == 'widedeep': self.output_block = WideAndDeepBlock(params, num_net_outputs) else: - raise ValueError("unknown network_type specified: %s" % params['network_type']) - + raise ValueError(f"unknown network_type specified: {params['network_type']}") + y_range = params['y_range'] # Used specifically for regression. = None for classification. self.y_constraint = None # determines if Y-predictions should be constrained if y_range is not None: @@ -130,7 +130,7 @@ def __init__(self, train_dataset=None, params=None, num_net_outputs=None, archit self.y_lower.as_in_context(ctx) self.y_upper.as_in_context(ctx) self.y_span = self.y_upper - self.y_lower - + if architecture_desc is None: # Save Architecture description self.architecture_desc = {'has_vector_features': self.has_vector_features, 'has_embed_features': self.has_embed_features, @@ -195,10 +195,12 @@ def forward(self, data_batch): # # embed_activations.append(self.embed_blocks[i](embed_data[i])) # embed_activations = nd.concat(*embed_activations, dim=2) embed_activations = embed_activations.flatten() - if not self.has_vector_features: - input_activations = embed_activations - else: - input_activations = nd.concat(embed_activations, input_activations) + input_activations = ( + nd.concat(embed_activations, input_activations) + if self.has_vector_features + else embed_activations + ) + if self.has_language_features: language_data = data_batch['language'] language_activations = self.text_block(language_data) # TODO: create block to embed text fields @@ -208,14 +210,13 @@ def forward(self, data_batch): input_activations = nd.concat(language_activations, input_activations) if self.y_constraint is None: return self.output_block(input_activations) + unscaled_pred = self.output_block(input_activations) + if self.y_constraint == 'nonnegative': + return self.y_lower + nd.abs(unscaled_pred) + elif self.y_constraint == 'nonpositive': + return self.y_upper - nd.abs(unscaled_pred) else: - unscaled_pred = self.output_block(input_activations) - if self.y_constraint == 'nonnegative': - return self.y_lower + nd.abs(unscaled_pred) - elif self.y_constraint == 'nonpositive': - return self.y_upper - nd.abs(unscaled_pred) - else: - """ + """ print("unscaled_pred",unscaled_pred) print("nd.sigmoid(unscaled_pred)", nd.sigmoid(unscaled_pred)) print("self.y_span", self.y_span) @@ -223,7 +224,7 @@ def forward(self, data_batch): print("self.y_lower.shape", self.y_lower.shape) print("nd.sigmoid(unscaled_pred).shape", nd.sigmoid(unscaled_pred).shape) """ - return nd.sigmoid(unscaled_pred) * self.y_span + self.y_lower + return nd.sigmoid(unscaled_pred) * self.y_span + self.y_lower """ OLD @@ -252,7 +253,7 @@ def _create_embednet_from_architecture(architecture_desc): """ -def getEmbedSizes(train_dataset, params, num_categs_per_feature): +def getEmbedSizes(train_dataset, params, num_categs_per_feature): """ Returns list of embedding sizes for each categorical variable. Selects this adaptively based on training_datset. Note: Assumes there is at least one embed feature. @@ -260,7 +261,16 @@ def getEmbedSizes(train_dataset, params, num_categs_per_feature): max_embedding_dim = params['max_embedding_dim'] embed_exponent = params['embed_exponent'] size_factor = params['embedding_size_factor'] - embed_dims = [int(size_factor*max(2, min(max_embedding_dim, - 1.6 * num_categs_per_feature[i]**embed_exponent))) - for i in range(len(num_categs_per_feature))] - return embed_dims + return [ + int( + size_factor + * max( + 2, + min( + max_embedding_dim, + 1.6 * num_categs_per_feature[i] ** embed_exponent, + ), + ) + ) + for i in range(len(num_categs_per_feature)) + ] diff --git a/autogluon/utils/tabular/ml/models/tabular_nn/hyperparameters/parameters.py b/autogluon/utils/tabular/ml/models/tabular_nn/hyperparameters/parameters.py index 1a5aa84e75fc..db3e28cef1ad 100644 --- a/autogluon/utils/tabular/ml/models/tabular_nn/hyperparameters/parameters.py +++ b/autogluon/utils/tabular/ml/models/tabular_nn/hyperparameters/parameters.py @@ -5,7 +5,7 @@ def get_fixed_params(): """ Parameters that currently cannot be searched during HPO """ - fixed_params = { + return { 'num_epochs': 500, # maximum number of epochs for training NN 'epochs_wo_improve': 20, # we terminate training if validation performance hasn't improved in the last 'epochs_wo_improve' # of epochs # TODO: Epochs could take a very long time, we may want smarter logic than simply # of epochs without improvement (slope, difference in score, etc.) @@ -20,15 +20,14 @@ def get_fixed_params(): 'proc.skew_threshold': 0.99, # numerical features whose absolute skewness is greater than this receive special power-transform preprocessing. Choose big value to avoid using power-transforms # Options: [0.2, 0.3, 0.5, 0.8, 1.0, 10.0, 100.0] # Old params: These are now set based off of nthreads_per_trial, ngpus_per_trial. - # 'num_dataloading_workers': 1, # Will be overwritten by nthreads_per_trial, can be >= 1 + # 'num_dataloading_workers': 1, # Will be overwritten by nthreads_per_trial, can be >= 1 # 'ctx': mx.cpu(), # Will be overwritten by ngpus_per_trial if unspecified (can alternatively be: mx.gpu()) } - return fixed_params def get_hyper_params(): """ Parameters that currently can be tuned during HPO """ - hyper_params = { + return { ## Hyperparameters for neural net architecture: 'network_type': 'widedeep', # Type of neural net used to produce predictions # Options: ['widedeep', 'feedforward'] @@ -73,20 +72,17 @@ def get_hyper_params(): 'use_ngram_features': False, # If False, will drop automatically generated ngram features from language features. This results in worse model quality but far faster inference and training times. # Options: [True, False] } - return hyper_params # Note: params for original NNTabularModel were: # weight_decay=0.01, dropout_prob = 0.1, batch_size = 2048, lr = 1e-2, epochs=30, layers= [200, 100] (semi-equivalent to our layers = [100],numeric_embed_dim=200) def get_default_param(problem_type, num_classes=None): - if problem_type == BINARY: + if problem_type == BINARY or problem_type not in [MULTICLASS, REGRESSION]: return get_param_binary() elif problem_type == MULTICLASS: return get_param_multiclass(num_classes=num_classes) - elif problem_type == REGRESSION: - return get_param_regression() else: - return get_param_binary() + return get_param_regression() def get_param_multiclass(num_classes): diff --git a/autogluon/utils/tabular/ml/models/tabular_nn/hyperparameters/searchspaces.py b/autogluon/utils/tabular/ml/models/tabular_nn/hyperparameters/searchspaces.py index ec544e357159..854cfbc61760 100644 --- a/autogluon/utils/tabular/ml/models/tabular_nn/hyperparameters/searchspaces.py +++ b/autogluon/utils/tabular/ml/models/tabular_nn/hyperparameters/searchspaces.py @@ -4,60 +4,78 @@ def get_default_searchspace(problem_type, num_classes=None): - if problem_type == BINARY: + if problem_type == BINARY or problem_type not in [MULTICLASS, REGRESSION]: return get_searchspace_binary().copy() elif problem_type == MULTICLASS: return get_searchspace_multiclass(num_classes=num_classes) - elif problem_type == REGRESSION: - return get_searchspace_regression().copy() else: - return get_searchspace_binary().copy() + return get_searchspace_regression().copy() def get_searchspace_multiclass(num_classes): - # Search space we use by default (only specify non-fixed hyperparameters here): # TODO: move to separate file - params = { + return { 'learning_rate': Real(1e-4, 3e-2, default=3e-4, log=True), 'weight_decay': Real(1e-12, 0.1, default=1e-6, log=True), 'dropout_prob': Real(0.0, 0.5, default=0.1), # 'layers': Categorical(None, [200, 100], [256], [2056], [1024, 512, 128], [1024, 1024, 1024]), - 'layers': Categorical(None, [200, 100], [256], [100, 50], [200, 100, 50], [50, 25], [300, 150]), + 'layers': Categorical( + None, + [200, 100], + [256], + [100, 50], + [200, 100, 50], + [50, 25], + [300, 150], + ), 'embedding_size_factor': Real(0.5, 1.5, default=1.0), 'network_type': Categorical('widedeep', 'feedforward'), 'use_batchnorm': Categorical(True, False), 'activation': Categorical('relu', 'softrelu'), # 'batch_size': Categorical(512, 1024, 2056, 128), # this is used in preprocessing so cannot search atm } - return params def get_searchspace_binary(): - params = { + return { 'learning_rate': Real(1e-4, 3e-2, default=3e-4, log=True), 'weight_decay': Real(1e-12, 0.1, default=1e-6, log=True), 'dropout_prob': Real(0.0, 0.5, default=0.1), # 'layers': Categorical(None, [200, 100], [256], [2056], [1024, 512, 128], [1024, 1024, 1024]), - 'layers': Categorical(None, [200, 100], [256], [100, 50], [200, 100, 50], [50, 25], [300, 150]), + 'layers': Categorical( + None, + [200, 100], + [256], + [100, 50], + [200, 100, 50], + [50, 25], + [300, 150], + ), 'embedding_size_factor': Real(0.5, 1.5, default=1.0), 'network_type': Categorical('widedeep', 'feedforward'), 'use_batchnorm': Categorical(True, False), 'activation': Categorical('relu', 'softrelu'), # 'batch_size': Categorical(512, 1024, 2056, 128), # this is used in preprocessing so cannot search atm } - return params def get_searchspace_regression(): - params = { + return { 'learning_rate': Real(1e-4, 3e-2, default=3e-4, log=True), 'weight_decay': Real(1e-12, 0.1, default=1e-6, log=True), 'dropout_prob': Real(0.0, 0.5, default=0.1), # 'layers': Categorical(None, [200, 100], [256], [2056], [1024, 512, 128], [1024, 1024, 1024]), - 'layers': Categorical(None, [200, 100], [256], [100, 50], [200, 100, 50], [50, 25], [300, 150]), + 'layers': Categorical( + None, + [200, 100], + [256], + [100, 50], + [200, 100, 50], + [50, 25], + [300, 150], + ), 'embedding_size_factor': Real(0.5, 1.5, default=1.0), 'network_type': Categorical('widedeep', 'feedforward'), 'use_batchnorm': Categorical(True, False), 'activation': Categorical('relu', 'softrelu', 'tanh'), # 'batch_size': Categorical(512, 1024, 2056, 128), # this is used in preprocessing so cannot search atm } - return params diff --git a/autogluon/utils/tabular/ml/models/tabular_nn/tabular_nn_dataset.py b/autogluon/utils/tabular/ml/models/tabular_nn/tabular_nn_dataset.py index 941ca92aae7b..6587a4a642bd 100644 --- a/autogluon/utils/tabular/ml/models/tabular_nn/tabular_nn_dataset.py +++ b/autogluon/utils/tabular/ml/models/tabular_nn/tabular_nn_dataset.py @@ -86,7 +86,7 @@ def __init__(self, processed_array, feature_arraycol_map, feature_type_map, batc elif feature_type_map[feature] == 'language': self.feature_groups['language'].append(feature) else: - raise ValueError("unknown feature type: %s" % feature) + raise ValueError(f"unknown feature type: {feature}") if not self.is_test and labels is None: raise ValueError("labels must be provided when is_test = False") @@ -175,13 +175,12 @@ def num_vector_features(self): def get_labels(self): """ Returns numpy array of labels for this dataset """ - if self.label_index is not None: - if self.problem_type == SOFTCLASS: - return self.dataset._data[self.label_index].asnumpy() - else: - return self.dataset._data[self.label_index].asnumpy().flatten() - else: + if self.label_index is None: return None + if self.problem_type == SOFTCLASS: + return self.dataset._data[self.label_index].asnumpy() + else: + return self.dataset._data[self.label_index].asnumpy().flatten() def getNumCategoriesEmbeddings(self): """ Returns number of categories for each embedding feature. @@ -191,15 +190,14 @@ def getNumCategoriesEmbeddings(self): """ if self.num_categories_per_embed_feature is not None: return self.num_categories_per_embedfeature - else: - num_embed_feats = self.num_embed_features() - num_categories_per_embedfeature = [0] * num_embed_feats - for i in range(num_embed_feats): - feat_i = self.feature_groups['embed'][i] - feat_i_data = self.get_feature_data(feat_i).flatten().tolist() - num_categories_i = len(set(feat_i_data)) # number of categories for ith feature - num_categories_per_embedfeature[i] = num_categories_i + 1 # to account for unknown test-time categories - return num_categories_per_embedfeature + num_embed_feats = self.num_embed_features() + num_categories_per_embedfeature = [0] * num_embed_feats + for i in range(num_embed_feats): + feat_i = self.feature_groups['embed'][i] + feat_i_data = self.get_feature_data(feat_i).flatten().tolist() + num_categories_i = len(set(feat_i_data)) # number of categories for ith feature + num_categories_per_embedfeature[i] = num_categories_i + 1 # to account for unknown test-time categories + return num_categories_per_embedfeature def get_feature_data(self, feature, asnumpy=True): """ Returns all data for this feature. @@ -207,9 +205,9 @@ def get_feature_data(self, feature, asnumpy=True): feature (str): name of feature of interest (in processed dataframe) asnumpy (bool): should we return 2D numpy array or MXNet NDarray """ - nonvector_featuretypes = set(['embed', 'language']) + nonvector_featuretypes = {'embed', 'language'} if feature not in self.feature_type_map: - raise ValueError("unknown feature encountered: %s" % feature) + raise ValueError(f"unknown feature encountered: {feature}") if self.feature_type_map[feature] == 'vector': vector_datamatrix = self.dataset._data[self.vectordata_index] # does not work for one-hot... feature_data = vector_datamatrix[:, self.vecfeature_col_map[feature]] @@ -218,10 +216,7 @@ def get_feature_data(self, feature, asnumpy=True): feature_data = self.dataset._data[feature_idx] else: raise ValueError("Unknown feature specified: " % feature) - if asnumpy: - return feature_data.asnumpy() - else: - return feature_data + return feature_data.asnumpy() if asnumpy else feature_data def get_feature_batch(self, feature, data_batch, asnumpy=False): """ Returns part of this batch corresponding to data from a single feature @@ -230,9 +225,9 @@ def get_feature_batch(self, feature, data_batch, asnumpy=False): Returns: """ - nonvector_featuretypes = set(['embed', 'language']) + nonvector_featuretypes = {'embed', 'language'} if feature not in self.feature_type_map: - raise ValueError("unknown feature encountered: %s" % feature) + raise ValueError(f"unknown feature encountered: {feature}") if self.feature_type_map[feature] == 'vector': vector_datamatrix = data_batch[self.vectordata_index] feature_data = vector_datamatrix[:, self.vecfeature_col_map[feature]] @@ -241,10 +236,7 @@ def get_feature_batch(self, feature, data_batch, asnumpy=False): feature_data = data_batch[feature_idx] else: raise ValueError("Unknown feature specified: " % feature) - if asnumpy: - return feature_data.asnumpy() - else: - return feature_data + return feature_data.asnumpy() if asnumpy else feature_data def format_batch_data(self, data_batch, ctx): """ Partitions data from this batch into different data types. diff --git a/autogluon/utils/tabular/ml/models/tabular_nn/tabular_nn_model.py b/autogluon/utils/tabular/ml/models/tabular_nn/tabular_nn_model.py index d31087914ca2..07ed39d1b3a3 100644 --- a/autogluon/utils/tabular/ml/models/tabular_nn/tabular_nn_model.py +++ b/autogluon/utils/tabular/ml/models/tabular_nn/tabular_nn_model.py @@ -115,7 +115,7 @@ def _get_default_searchspace(self): def set_net_defaults(self, train_dataset, params): """ Sets dataset-adaptive default values to use for our neural network """ - if (self.problem_type == MULTICLASS) or (self.problem_type == SOFTCLASS): + if self.problem_type in [MULTICLASS, SOFTCLASS]: self.num_net_outputs = train_dataset.num_classes elif self.problem_type == REGRESSION: self.num_net_outputs = 1 @@ -125,19 +125,13 @@ def set_net_defaults(self, train_dataset, params): max_y = float(max(y_vals)) std_y = np.std(y_vals) y_ext = params['y_range_extend'] * std_y - if min_y >= 0: # infer y must be nonnegative - min_y = max(0, min_y-y_ext) - else: - min_y = min_y-y_ext - if max_y <= 0: # infer y must be non-positive - max_y = min(0, max_y+y_ext) - else: - max_y = max_y+y_ext + min_y = max(0, min_y-y_ext) if min_y >= 0 else min_y-y_ext + max_y = min(0, max_y+y_ext) if max_y <= 0 else max_y+y_ext params['y_range'] = (min_y, max_y) elif self.problem_type == BINARY: self.num_net_outputs = 2 else: - raise ValueError("unknown problem_type specified: %s" % self.problem_type) + raise ValueError(f"unknown problem_type specified: {self.problem_type}") if params['layers'] is None: # Use default choices for MLP architecture if self.problem_type == REGRESSION: @@ -240,7 +234,11 @@ def train_net(self, train_dataset, params, val_dataset=None, initialize=True, se setup_trainer (bool): set = False to reuse the same trainer from a previous training run, otherwise creates new trainer from scratch """ start_time = time.time() - logger.log(15, "Training neural network for up to %s epochs..." % params['num_epochs']) + logger.log( + 15, + f"Training neural network for up to {params['num_epochs']} epochs...", + ) + seed_value = params.get('seed_value') if seed_value is not None: # Set seed random.seed(seed_value) @@ -254,7 +252,7 @@ def train_net(self, train_dataset, params, val_dataset=None, initialize=True, se if setup_trainer: # Also setup mxboard to monitor training if visualizer has been specified: visualizer = params.get('visualizer', 'none') - if visualizer == 'tensorboard' or visualizer == 'mxboard': + if visualizer in ['tensorboard', 'mxboard']: try_import_mxboard() from mxboard import SummaryWriter self.summary_writer = SummaryWriter(logdir=self.path, flush_secs=5, verbose=False) @@ -263,11 +261,7 @@ def train_net(self, train_dataset, params, val_dataset=None, initialize=True, se val_metric = None best_val_epoch = 0 num_epochs = params['num_epochs'] - if val_dataset is not None: - y_val = val_dataset.get_labels() - else: - y_val = None - + y_val = val_dataset.get_labels() if val_dataset is not None else None if params['loss_function'] is None: if self.problem_type == REGRESSION: params['loss_function'] = gluon.loss.L1Loss() @@ -279,15 +273,19 @@ def train_net(self, train_dataset, params, val_dataset=None, initialize=True, se loss_func = params['loss_function'] epochs_wo_improve = params['epochs_wo_improve'] loss_scaling_factor = 1.0 # we divide loss by this quantity to stabilize gradients - loss_torescale = [key for key in self.rescale_losses if isinstance(loss_func, key)] - if len(loss_torescale) > 0: + if loss_torescale := [ + key for key in self.rescale_losses if isinstance(loss_func, key) + ]: loss_torescale = loss_torescale[0] if self.rescale_losses[loss_torescale] == 'std': loss_scaling_factor = np.std(train_dataset.get_labels())/5.0 + EPS # std-dev of labels elif self.rescale_losses[loss_torescale] == 'var': loss_scaling_factor = np.var(train_dataset.get_labels())/5.0 + EPS # variance of labels else: - raise ValueError("Unknown loss-rescaling type %s specified for loss_func==%s" % (self.rescale_losses[loss_torescale], loss_func)) + raise ValueError( + f"Unknown loss-rescaling type {self.rescale_losses[loss_torescale]} specified for loss_func=={loss_func}" + ) + if self.verbosity <= 1: verbose_eval = -1 # Print losses every verbose epochs, Never if -1 @@ -322,7 +320,7 @@ def train_net(self, train_dataset, params, val_dataset=None, initialize=True, se logger.log(15, "Neural network architecture:") logger.log(15, str(self.model)) # TODO: remove? cumulative_loss = 0 - for batch_idx, data_batch in enumerate(train_dataset.dataloader): + for data_batch in train_dataset.dataloader: data_batch = train_dataset.format_batch_data(data_batch, self.ctx) with autograd.record(): output = self.model(data_batch) @@ -337,30 +335,33 @@ def train_net(self, train_dataset, params, val_dataset=None, initialize=True, se # val_metric = self.evaluate_metric(test_dataset) # Evaluate after each epoch val_metric = self.score(X=val_dataset, y=y_val, eval_metric=self.stopping_metric, metric_needs_y_pred=self.stopping_metric_needs_y_pred) if (val_dataset is None) or (val_metric >= best_val_metric) or (e == 0): # keep training if score has improved - if val_dataset is not None: - if not np.isnan(val_metric): - best_val_metric = val_metric + if val_dataset is not None and not np.isnan(val_metric): + best_val_metric = val_metric best_val_epoch = e # Until functionality is added to restart training from a particular epoch, there is no point in saving params without test_dataset if val_dataset is not None: self.model.save_parameters(net_filename) if val_dataset is not None: if verbose_eval > 0 and e % verbose_eval == 0: - logger.log(15, "Epoch %s. Train loss: %s, Val %s: %s" % - (e, train_loss.asscalar(), self.eval_metric_name, val_metric)) + logger.log( + 15, + f"Epoch {e}. Train loss: {train_loss.asscalar()}, Val {self.eval_metric_name}: {val_metric}", + ) + if self.summary_writer is not None: self.summary_writer.add_scalar(tag='val_'+self.eval_metric_name, value=val_metric, global_step=e) - else: - if verbose_eval > 0 and e % verbose_eval == 0: - logger.log(15, "Epoch %s. Train loss: %s" % (e, train_loss.asscalar())) + elif verbose_eval > 0 and e % verbose_eval == 0: + logger.log(15, f"Epoch {e}. Train loss: {train_loss.asscalar()}") if self.summary_writer is not None: self.summary_writer.add_scalar(tag='train_loss', value=train_loss.asscalar(), global_step=e) # TODO: do we want to keep mxboard support? - if reporter is not None: - # TODO: Ensure reporter/scheduler properly handle None/nan values after refactor - if val_dataset is not None and (not np.isnan(val_metric)): # TODO: This might work without the if statement - # epoch must be number of epochs done (starting at 1) - reporter(epoch=e+1, validation_performance=val_metric, train_loss=float(train_loss.asscalar())) # Higher val_metric = better + if ( + reporter is not None + and val_dataset is not None + and (not np.isnan(val_metric)) + ): + # epoch must be number of epochs done (starting at 1) + reporter(epoch=e+1, validation_performance=val_metric, train_loss=float(train_loss.asscalar())) # Higher val_metric = better if e - best_val_epoch > epochs_wo_improve: break if time_limit: @@ -401,9 +402,11 @@ def _predict_proba(self, X, preprocess=True): X = self.preprocess(X) return self._predict_tabular_data(new_data=X, process=True, predict_proba=True) else: - raise ValueError("X must be of type pd.DataFrame or TabularNNDataset, not type: %s" % type(X)) + raise ValueError( + f"X must be of type pd.DataFrame or TabularNNDataset, not type: {type(X)}" + ) - def _predict_tabular_data(self, new_data, process=True, predict_proba=True): # TODO ensure API lines up with tabular.Model class. + def _predict_tabular_data(self, new_data, process=True, predict_proba=True): # TODO ensure API lines up with tabular.Model class. """ Specific TabularNN method to produce predictions on new (unprocessed) data. Returns 1D numpy array unless predict_proba=True and task is multi-class classification (not binary). Args: @@ -426,15 +429,17 @@ def _predict_tabular_data(self, new_data, process=True, predict_proba=True): # preds_batch = self.model(data_batch) batch_size = len(preds_batch) if self.problem_type != REGRESSION: - if not predict_proba: # need to take argmax - preds_batch = nd.argmax(preds_batch, axis=1, keepdims=True) - else: # need to take softmax - preds_batch = nd.softmax(preds_batch, axis=1) + preds_batch = ( + nd.softmax(preds_batch, axis=1) + if predict_proba + else nd.argmax(preds_batch, axis=1, keepdims=True) + ) + preds[i:(i+batch_size)] = preds_batch i = i+batch_size if self.problem_type == REGRESSION or not predict_proba: return preds.asnumpy().flatten() # return 1D numpy array - elif self.problem_type == BINARY and predict_proba: + elif self.problem_type == BINARY: return preds[:,1].asnumpy() # for binary problems, only return P(Y==+1) return preds.asnumpy() # return 2D numpy array @@ -456,14 +461,13 @@ def generate_datasets(self, X_train, y_train, params, X_val=None, y_val=None): df=X_train, labels=y_train, batch_size=self.batch_size, num_dataloading_workers=self.num_dataloading_workers, impute_strategy=impute_strategy, max_category_levels=max_category_levels, skew_threshold=skew_threshold, embed_min_categories=embed_min_categories, use_ngram_features=use_ngram_features, ) - if X_val is not None: - if isinstance(X_val, TabularNNDataset): - val_dataset = X_val - else: - X_val = self.preprocess(X_val) - val_dataset = self.process_test_data(df=X_val, labels=y_val, batch_size=self.batch_size, num_dataloading_workers=self.num_dataloading_workers_inference) - else: + if X_val is None: val_dataset = None + elif isinstance(X_val, TabularNNDataset): + val_dataset = X_val + else: + X_val = self.preprocess(X_val) + val_dataset = self.process_test_data(df=X_val, labels=y_val, batch_size=self.batch_size, num_dataloading_workers=self.num_dataloading_workers_inference) return train_dataset, val_dataset def process_test_data(self, df, batch_size, num_dataloading_workers, labels=None): @@ -558,7 +562,7 @@ def setup_trainer(self, params, train_dataset=None): elif params['optimizer'] == 'adam': # TODO: Can we try AdamW? optimizer = gluon.Trainer(self.model.collect_params(), 'adam', optimizer_opts) else: - raise ValueError("Unknown optimizer specified: %s" % params['optimizer']) + raise ValueError(f"Unknown optimizer specified: {params['optimizer']}") return optimizer @staticmethod @@ -611,7 +615,7 @@ def _get_types_of_features(self, df, skew_threshold, embed_min_categories, use_n def _get_feature_arraycol_map(self, max_category_levels): """ Returns OrderedDict of feature-name -> list of column-indices in processed data array corresponding to this feature """ - feature_preserving_transforms = set(['continuous','skewed', 'ordinal', 'language']) # these transforms do not alter dimensionality of feature + feature_preserving_transforms = {'continuous', 'skewed', 'ordinal', 'language'} feature_arraycol_map = {} # unordered version current_colindex = 0 for transformer in self.processor.transformers_: @@ -620,7 +624,10 @@ def _get_feature_arraycol_map(self, max_category_levels): if transformer_name in feature_preserving_transforms: for feature in transformed_features: if feature in feature_arraycol_map: - raise ValueError("same feature is processed by two different column transformers: %s" % feature) + raise ValueError( + f"same feature is processed by two different column transformers: {feature}" + ) + feature_arraycol_map[feature] = [current_colindex] current_colindex += 1 elif transformer_name == 'onehot': @@ -628,13 +635,16 @@ def _get_feature_arraycol_map(self, max_category_levels): for i in range(len(transformed_features)): feature = transformed_features[i] if feature in feature_arraycol_map: - raise ValueError("same feature is processed by two different column transformers: %s" % feature) + raise ValueError( + f"same feature is processed by two different column transformers: {feature}" + ) + oh_dimensionality = min(len(oh_encoder.categories_[i]), max_category_levels+1) # print("feature: %s, oh_dimensionality: %s" % (feature, oh_dimensionality)) # TODO! debug feature_arraycol_map[feature] = list(range(current_colindex, current_colindex+oh_dimensionality)) current_colindex += oh_dimensionality else: - raise ValueError("unknown transformer encountered: %s" % transformer_name) + raise ValueError(f"unknown transformer encountered: {transformer_name}") if set(feature_arraycol_map.keys()) != set(self.features): raise ValueError("failed to account for all features when determining column indices in processed array") return OrderedDict([(key, feature_arraycol_map[key]) for key in feature_arraycol_map]) @@ -769,7 +779,7 @@ def hyperparameter_tune(self, X_train, y_train, X_val, y_val, scheduler_options, logger.log(15, "Hyperparameter search space for Neural Network: ") for hyperparam in params_copy: if isinstance(params_copy[hyperparam], Space): - logger.log(15, str(hyperparam)+ ": "+str(params_copy[hyperparam])) + logger.log(15, f"{str(hyperparam)}: {str(params_copy[hyperparam])}") util_args = dict( train_path=train_path, diff --git a/autogluon/utils/tabular/ml/trainer/abstract_trainer.py b/autogluon/utils/tabular/ml/trainer/abstract_trainer.py index 52f711151b54..6836754432ce 100644 --- a/autogluon/utils/tabular/ml/trainer/abstract_trainer.py +++ b/autogluon/utils/tabular/ml/trainer/abstract_trainer.py @@ -61,20 +61,28 @@ def __init__(self, path: str, problem_type: str, scheduler_options=None, eval_me self.stopping_metric = self.eval_metric self.eval_metric_expects_y_pred = scorer_expects_y_pred(scorer=self.eval_metric) - logger.log(25, "AutoGluon will gauge predictive performance using evaluation metric: %s" % self.eval_metric.name) + logger.log( + 25, + f"AutoGluon will gauge predictive performance using evaluation metric: {self.eval_metric.name}", + ) + if not self.eval_metric_expects_y_pred: logger.log(25, "This metric expects predicted probabilities rather than predicted class labels, so you'll need to use predict_proba() instead of predict()") logger.log(20, "To change this, specify the eval_metric argument of fit()") - logger.log(25, "AutoGluon will early stop models using evaluation metric: %s" % self.stopping_metric.name) + logger.log( + 25, + f"AutoGluon will early stop models using evaluation metric: {self.stopping_metric.name}", + ) + self.num_classes = num_classes self.feature_prune = False # will be set to True if feature-pruning is turned on. self.low_memory = low_memory - self.bagged_mode = True if kfolds >= 2 else False + self.bagged_mode = kfolds >= 2 if self.bagged_mode: self.kfolds = kfolds # int number of folds to do model bagging, < 2 means disabled self.stack_ensemble_levels = stack_ensemble_levels - self.stack_mode = True if self.stack_ensemble_levels >= 1 else False + self.stack_mode = self.stack_ensemble_levels >= 1 self.n_repeats = n_repeats else: self.kfolds = 0 @@ -208,7 +216,7 @@ def get_model_level(self, model_name): for level in self.models_level[stack_name].keys(): if model_name in self.models_level[stack_name][level]: return level - raise ValueError('Model' + str(model_name) + 'does not exist in trainer.') + raise ValueError(f'Model{str(model_name)}does not exist in trainer.') def set_contexts(self, path_context): self.path, self.model_paths = self.create_contexts(path_context) @@ -251,32 +259,35 @@ def train_and_save(self, X_train, y_train, X_val, y_val, model: AbstractModel, s try: if time_limit is not None: if time_limit <= 0: - logging.log(15, 'Skipping ' + str(model.name) + ' due to lack of time remaining.') + logging.log(15, f'Skipping {str(model.name)} due to lack of time remaining.') return model_names_trained time_left_total = self.time_limit - (fit_start_time - self.time_train_start) - logging.log(20, 'Fitting model: ' + str(model.name) + ' ...' + ' Training model for up to ' + str(round(time_limit, 2)) + 's of the ' + str(round(time_left_total, 2)) + 's of remaining time.') + logging.log( + 20, + f'Fitting model: {str(model.name)} ... Training model for up to {str(round(time_limit, 2))}s of the {str(round(time_left_total, 2))}s of remaining time.', + ) + else: - logging.log(20, 'Fitting model: ' + str(model.name) + ' ...') + logging.log(20, f'Fitting model: {str(model.name)} ...') model = self.train_single(X_train, y_train, X_val, y_val, model, kfolds=kfolds, k_fold_start=k_fold_start, k_fold_end=k_fold_end, n_repeats=n_repeats, n_repeat_start=n_repeat_start, level=level, time_limit=time_limit) fit_end_time = time.time() if isinstance(model, BaggedEnsembleModel): - if model.bagged_mode or isinstance(model, WeightedEnsembleModel): - score = model.score_with_oof(y=y_train) - else: - score = None + score = ( + model.score_with_oof(y=y_train) + if model.bagged_mode + or isinstance(model, WeightedEnsembleModel) + else None + ) + + elif X_val is not None and y_val is not None: + score = model.score(X=X_val, y=y_val) else: - if X_val is not None and y_val is not None: - score = model.score(X=X_val, y=y_val) - else: - score = None + score = None pred_end_time = time.time() if model.fit_time is None: model.fit_time = fit_end_time - fit_start_time if model.predict_time is None: - if score is None: - model.predict_time = None - else: - model.predict_time = pred_end_time - fit_end_time + model.predict_time = None if score is None else pred_end_time - fit_end_time model.val_score = score # TODO: Add recursive=True to avoid repeatedly loading models each time this is called for bagged ensembles (especially during repeated bagging) self.save_model(model=model) @@ -293,7 +304,10 @@ def train_and_save(self, X_train, y_train, X_val, y_val, model: AbstractModel, s except Exception as err: if self.verbosity >= 1: traceback.print_tb(err.__traceback__) - logger.exception('Warning: Exception caused %s to fail during training... Skipping this model.' % model.name) + logger.exception( + f'Warning: Exception caused {model.name} to fail during training... Skipping this model.' + ) + logger.log(20, err) del model else: @@ -336,17 +350,29 @@ def train_single_full(self, X_train, y_train, X_val, y_val, model: AbstractModel model.feature_types_metadata = copy.deepcopy(self.feature_types_metadata) # TODO: Don't set feature_types_metadata here if feature_prune: if n_repeat_start != 0: - raise ValueError('n_repeat_start must be 0 to feature_prune, value = ' + str(n_repeat_start)) + raise ValueError( + f'n_repeat_start must be 0 to feature_prune, value = {str(n_repeat_start)}' + ) + elif k_fold_start != 0: - raise ValueError('k_fold_start must be 0 to feature_prune, value = ' + str(k_fold_start)) + raise ValueError( + f'k_fold_start must be 0 to feature_prune, value = {str(k_fold_start)}' + ) + self.autotune(X_train=X_train, X_holdout=X_val, y_train=y_train, y_holdout=y_val, model_base=model) # TODO: Update to use CV instead of holdout if hyperparameter_tune: if self.scheduler_func is None or self.scheduler_options is None: raise ValueError("scheduler_options cannot be None when hyperparameter_tune = True") if n_repeat_start != 0: - raise ValueError('n_repeat_start must be 0 to hyperparameter_tune, value = ' + str(n_repeat_start)) + raise ValueError( + f'n_repeat_start must be 0 to hyperparameter_tune, value = {str(n_repeat_start)}' + ) + elif k_fold_start != 0: - raise ValueError('k_fold_start must be 0 to hyperparameter_tune, value = ' + str(k_fold_start)) + raise ValueError( + f'k_fold_start must be 0 to hyperparameter_tune, value = {str(k_fold_start)}' + ) + # hpo_models (dict): keys = model_names, values = model_paths try: if isinstance(model, BaggedEnsembleModel): @@ -392,7 +418,7 @@ def train_multi_repeats(self, X_train, y_train, X_val, y_val, models, kfolds, n_ if time_left < time_required: logger.log(15, 'Not enough time left to finish repeated k-fold bagging, stopping early ...') break - logger.log(20, 'Repeating k-fold bagging: ' + str(n+1) + '/' + str(n_repeats)) + logger.log(20, f'Repeating k-fold bagging: {str(n + 1)}/{str(n_repeats)}') for i, model in enumerate(models_valid): if isinstance(model, str): model = self.load_model(model) @@ -405,7 +431,11 @@ def train_multi_repeats(self, X_train, y_train, X_val, y_val, models, kfolds, n_ models_valid = copy.deepcopy(models_valid_next) models_valid_next = [] repeats_completed += 1 - logger.log(20, 'Completed ' + str(n_repeat_start + repeats_completed) + '/' + str(n_repeats) + ' k-fold bagging repeats ...') + logger.log( + 20, + f'Completed {str(n_repeat_start + repeats_completed)}/{str(n_repeats)} k-fold bagging repeats ...', + ) + return models_valid def train_multi_initial(self, X_train, y_train, X_val, y_val, models: List[AbstractModel], kfolds, n_repeats, hyperparameter_tune=True, feature_prune=False, stack_name='core', level=0, time_limit=None): @@ -468,11 +498,11 @@ def train_multi(self, X_train, y_train, X_val, y_val, models: List[AbstractModel if n_repeats is None: n_repeats = self.n_repeats if (kfolds == 0) and (n_repeats != 1): - raise ValueError('n_repeats must be 1 when kfolds is 0, values: (%s, %s)' % (n_repeats, kfolds)) - if time_limit is None: - n_repeats_initial = n_repeats - else: - n_repeats_initial = 1 + raise ValueError( + f'n_repeats must be 1 when kfolds is 0, values: ({n_repeats}, {kfolds})' + ) + + n_repeats_initial = n_repeats if time_limit is None else 1 if n_repeat_start == 0: time_start = time.time() model_names_trained = self.train_multi_initial(X_train=X_train, y_train=y_train, X_val=X_val, y_val=y_val, models=models, kfolds=kfolds, n_repeats=n_repeats_initial, hyperparameter_tune=hyperparameter_tune, feature_prune=feature_prune, @@ -532,7 +562,6 @@ def stack_new_level(self, X, y, X_val=None, y_val=None, level=0, models=None, hy return core_models + aux_models def stack_new_level_core(self, X, y, X_val=None, y_val=None, models=None, level=1, stack_name='core', kfolds=None, n_repeats=None, hyperparameter_tune=False, feature_prune=False, time_limit=None, save_bagged_folds=None, stacker_type=StackerEnsembleModel, extra_ag_args_fit=None): - use_orig_features = True if models is None: models = self.get_models(self.hyperparameters, level=level, extra_ag_args_fit=extra_ag_args_fit) if kfolds is None: @@ -551,7 +580,8 @@ def stack_new_level_core(self, X, y, X_val=None, y_val=None, models=None, level= logger.log(20, 'No base models to train on, skipping stack level...') return else: - raise AssertionError('Stack level cannot be negative! level = %s' % level) + raise AssertionError(f'Stack level cannot be negative! level = {level}') + use_orig_features = True models = [ stacker_type(path=self.path, name=model.name + '_STACKER_l' + str(level), model_base=model, base_model_names=base_model_names, base_model_paths_dict=base_model_paths, base_model_types_dict=base_model_types, use_orig_features=use_orig_features, @@ -672,11 +702,10 @@ def score(self, X, y, model=None): return self.eval_metric(y, y_pred_proba_ensemble) def score_with_y_pred_proba(self, y, y_pred_proba): - if self.eval_metric_expects_y_pred: - y_pred = get_pred_from_proba(y_pred_proba=y_pred_proba, problem_type=self.problem_type) - return self.eval_metric(y, y_pred) - else: + if not self.eval_metric_expects_y_pred: return self.eval_metric(y, y_pred_proba) + y_pred = get_pred_from_proba(y_pred_proba=y_pred_proba, problem_type=self.problem_type) + return self.eval_metric(y, y_pred) def autotune(self, X_train, X_holdout, y_train, y_holdout, model_base: AbstractModel): model_base.feature_prune(X_train, X_holdout, y_train, y_holdout) @@ -738,11 +767,10 @@ def get_model_pred_proba_dict(self, X, models, model_pred_proba_dict=None, model if fit: model_type = self.model_types[model_name] - if issubclass(model_type, BaggedEnsembleModel): - model_path = self.model_paths[model_name] - model_pred_proba_dict[model_name] = model_type.load_oof(path=model_path) - else: + if not issubclass(model_type, BaggedEnsembleModel): raise AssertionError(f'Model {model.name} must be a BaggedEnsembleModel to return oof_pred_proba') + model_path = self.model_paths[model_name] + model_pred_proba_dict[model_name] = model_type.load_oof(path=model_path) else: model = self.load_model(model_name=model_name) if isinstance(model, StackerEnsembleModel): @@ -778,7 +806,10 @@ def get_inputs_to_stacker_v2(self, X, base_models, model_pred_proba_dict=None, f # Remove in future as it limits flexibility in stacker inputs during training def get_inputs_to_stacker(self, X, level_start, level_end, model_levels=None, y_pred_probas=None, fit=False): if level_start > level_end: - raise AssertionError('level_start cannot be greater than level end:' + str(level_start) + ', ' + str(level_end)) + raise AssertionError( + f'level_start cannot be greater than level end:{str(level_start)}, {str(level_end)}' + ) + if (level_start == 0) and (level_end == 0): return X if fit: @@ -802,15 +833,16 @@ def get_inputs_to_stacker(self, X, level_start, level_end, model_levels=None, y_ else: X = X_stacker else: - dummy_stackers = {} - for level in range(level_start, level_end+1): - if level >= 1: - dummy_stackers[level] = self._get_dummy_stacker(level=level, model_levels=model_levels, use_orig_features=True) + dummy_stackers = { + level: self._get_dummy_stacker( + level=level, model_levels=model_levels, use_orig_features=True + ) + for level in range(level_start, level_end + 1) + if level >= 1 + } + for level in range(level_start, level_end): - if level >= 1: - cols_to_drop = dummy_stackers[level].stack_columns - else: - cols_to_drop = [] + cols_to_drop = dummy_stackers[level].stack_columns if level >= 1 else [] X = dummy_stackers[level+1].preprocess(X=X, preprocess=False, fit=False, compute_base_preds=True) if len(cols_to_drop) > 0: X = X.drop(cols_to_drop, axis=1) diff --git a/autogluon/utils/tabular/ml/trainer/auto_trainer.py b/autogluon/utils/tabular/ml/trainer/auto_trainer.py index 8f4ad9834fba..2b0e6aa530f4 100644 --- a/autogluon/utils/tabular/ml/trainer/auto_trainer.py +++ b/autogluon/utils/tabular/ml/trainer/auto_trainer.py @@ -27,7 +27,6 @@ def train(self, X_train, y_train, X_val=None, y_val=None, hyperparameter_tune=Tr y_train = pd.concat([y_train, y_val], ignore_index=True) X_val = None y_val = None - else: - if (y_val is None) or (X_val is None): - X_train, X_val, y_train, y_val = generate_train_test_split(X_train, y_train, problem_type=self.problem_type, test_size=holdout_frac, random_state=self.random_seed) + elif (y_val is None) or (X_val is None): + X_train, X_val, y_train, y_val = generate_train_test_split(X_train, y_train, problem_type=self.problem_type, test_size=holdout_frac, random_state=self.random_seed) self.train_multi_and_ensemble(X_train, y_train, X_val, y_val, models, hyperparameter_tune=hyperparameter_tune, feature_prune=feature_prune) diff --git a/autogluon/utils/tabular/ml/trainer/model_presets/presets.py b/autogluon/utils/tabular/ml/trainer/model_presets/presets.py index 2269593f0b71..106aea8edce5 100644 --- a/autogluon/utils/tabular/ml/trainer/model_presets/presets.py +++ b/autogluon/utils/tabular/ml/trainer/model_presets/presets.py @@ -98,17 +98,14 @@ def get_preset_models(path, problem_type, eval_metric, hyperparameters, stopping if problem_type not in [BINARY, MULTICLASS, REGRESSION, SOFTCLASS]: raise NotImplementedError - if level in hyperparameters.keys(): - level_key = level - else: - level_key = 'default' + level_key = level if level in hyperparameters.keys() else 'default' hp_level = hyperparameters[level_key] priority_dict = defaultdict(list) for model_type in hp_level: for model in hp_level[model_type]: model = copy.deepcopy(model) if AG_ARGS not in model: - model[AG_ARGS] = dict() + model[AG_ARGS] = {} if 'model_type' not in model[AG_ARGS]: model[AG_ARGS]['model_type'] = model_type model_priority = model[AG_ARGS].get('priority', default_priorities.get(model_type, DEFAULT_CUSTOM_MODEL_PRIORITY)) @@ -160,15 +157,27 @@ def get_preset_models(path, problem_type, eval_metric, hyperparameters, stopping def get_preset_stacker_model(path, problem_type, eval_metric, num_classes=None, hyperparameters={'NN': {}, 'GBM': {}}, hyperparameter_tune=False): - # TODO: Expand options to RF and NN - if problem_type == REGRESSION: - model = RFModel(path=path, name='LinearRegression', model=LinearRegression(), - problem_type=problem_type, eval_metric=eval_metric) - else: - model = RFModel(path=path, name='LogisticRegression', model=LogisticRegression( - solver='liblinear', multi_class='auto', max_iter=500, # n_jobs=-1 # TODO: HP set to hide warnings, but we should find optimal HP for this - ), problem_type=problem_type, eval_metric=eval_metric) - return model + return ( + RFModel( + path=path, + name='LinearRegression', + model=LinearRegression(), + problem_type=problem_type, + eval_metric=eval_metric, + ) + if problem_type == REGRESSION + else RFModel( + path=path, + name='LogisticRegression', + model=LogisticRegression( + solver='liblinear', + multi_class='auto', + max_iter=500, # n_jobs=-1 # TODO: HP set to hide warnings, but we should find optimal HP for this + ), + problem_type=problem_type, + eval_metric=eval_metric, + ) + ) def get_preset_models_softclass(path, hyperparameters, num_classes=None, hyperparameter_tune=False, name_suffix=''): diff --git a/autogluon/utils/tabular/ml/trainer/model_presets/presets_custom.py b/autogluon/utils/tabular/ml/trainer/model_presets/presets_custom.py index 5b142c2feb09..8ef05c5b2bf7 100644 --- a/autogluon/utils/tabular/ml/trainer/model_presets/presets_custom.py +++ b/autogluon/utils/tabular/ml/trainer/model_presets/presets_custom.py @@ -7,10 +7,8 @@ def get_preset_custom(name, problem_type, num_classes): if not isinstance(name, str): raise ValueError(f'Expected string value for custom model, but was given {name}') - # Custom model - if name == 'GBM': - model = get_param_baseline_custom(problem_type, num_classes=num_classes) - model[AG_ARGS] = dict(model_type='GBM', name_suffix='Custom', disable_in_hpo=True) - return [model] - else: + if name != 'GBM': raise ValueError(f'Unknown custom model preset: {name}') + model = get_param_baseline_custom(problem_type, num_classes=num_classes) + model[AG_ARGS] = dict(model_type='GBM', name_suffix='Custom', disable_in_hpo=True) + return [model] diff --git a/autogluon/utils/tabular/ml/trainer/model_presets/presets_distill.py b/autogluon/utils/tabular/ml/trainer/model_presets/presets_distill.py index c3c287757c54..8919e0c0d37a 100644 --- a/autogluon/utils/tabular/ml/trainer/model_presets/presets_distill.py +++ b/autogluon/utils/tabular/ml/trainer/model_presets/presets_distill.py @@ -62,7 +62,7 @@ def get_preset_models_distillation(path, problem_type, eval_metric, hyperparamet elif 'default' in hyperparameters and 'RF' in hyperparameters['default']: hyperparameters['default']['RF'] = rf_hyperparameters - if problem_type == REGRESSION or problem_type == BINARY: + if problem_type in [REGRESSION, BINARY]: models = get_preset_models(path=path, problem_type=REGRESSION, eval_metric=eval_metric, stopping_metric=stopping_metric, hyperparameters=hyperparameters, hyperparameter_tune=hyperparameter_tune, name_suffix=name_suffix, default_priorities=DEFAULT_DISTILL_PRIORITY) diff --git a/autogluon/utils/tabular/ml/tuning/ensemble_selection.py b/autogluon/utils/tabular/ml/tuning/ensemble_selection.py index 9f1266129229..5400a6a091e9 100644 --- a/autogluon/utils/tabular/ml/tuning/ensemble_selection.py +++ b/autogluon/utils/tabular/ml/tuning/ensemble_selection.py @@ -29,19 +29,16 @@ def __init__( self.random_state = random_state else: self.random_state = np.random.RandomState(seed=0) - if isinstance(metric, _ProbaScorer): - self.eval_metric_expects_y_pred = False - elif isinstance(metric, _ThresholdScorer): - self.eval_metric_expects_y_pred = False - else: - self.eval_metric_expects_y_pred = True + self.eval_metric_expects_y_pred = not isinstance( + metric, _ProbaScorer + ) and not isinstance(metric, _ThresholdScorer) def fit(self, predictions, labels, time_limit=None, identifiers=None): self.ensemble_size = int(self.ensemble_size) if self.ensemble_size < 1: raise ValueError('Ensemble size cannot be less than one!') - if not self.problem_type in PROBLEM_TYPES: - raise ValueError('Unknown problem type %s.' % self.problem_type) + if self.problem_type not in PROBLEM_TYPES: + raise ValueError(f'Unknown problem type {self.problem_type}.') # if not isinstance(self.metric, Scorer): # raise ValueError('Metric must be of type scorer') @@ -86,7 +83,7 @@ def _fit(self, predictions, labels, time_limit=None): ensemble_prediction /= s weighted_ensemble_prediction = (s / float(s + 1)) * \ - ensemble_prediction + ensemble_prediction fant_ensemble_prediction = np.zeros(weighted_ensemble_prediction.shape) for j, pred in enumerate(predictions): fant_ensemble_prediction[:] = weighted_ensemble_prediction + (1. / float(s + 1)) * pred @@ -121,7 +118,10 @@ def _fit(self, predictions, labels, time_limit=None): time_elapsed = time.time() - time_start time_left = time_limit - time_elapsed if time_left <= 0: - logger.warning('Warning: Ensemble Selection ran out of time, early stopping at iteration %s. This may mean that the time_limit specified is very small for this problem.' % (i+1)) + logger.warning( + f'Warning: Ensemble Selection ran out of time, early stopping at iteration {i + 1}. This may mean that the time_limit specified is very small for this problem.' + ) + break min_score = np.min(trajectory) @@ -136,9 +136,9 @@ def _fit(self, predictions, labels, time_limit=None): self.trajectory_ = trajectory[:first_index_of_best+1] self.train_score_ = trajectory[first_index_of_best] # TODO: Select best iteration or select final iteration? Earlier iteration could have a better score! self.ensemble_size = first_index_of_best + 1 - logger.log(15, 'Ensemble size: %s' % self.ensemble_size) + logger.log(15, f'Ensemble size: {self.ensemble_size}') - logger.debug("Ensemble indices: "+str(self.indices_)) + logger.debug(f"Ensemble indices: {str(self.indices_)}") def _calculate_weights(self): ensemble_members = Counter(self.indices_).most_common() @@ -162,5 +162,4 @@ def predict_proba(self, X): @staticmethod def weight_pred_probas(pred_probas, weights): preds_norm = [pred * weight for pred, weight in zip(pred_probas, weights)] - preds_ensemble = np.sum(preds_norm, axis=0) - return preds_ensemble + return np.sum(preds_norm, axis=0) diff --git a/autogluon/utils/tabular/ml/utils.py b/autogluon/utils/tabular/ml/utils.py index f7d08ef4f7f9..d9f06c85d4ea 100644 --- a/autogluon/utils/tabular/ml/utils.py +++ b/autogluon/utils/tabular/ml/utils.py @@ -17,12 +17,11 @@ def get_pred_from_proba(y_pred_proba, problem_type=BINARY): if problem_type == BINARY: - y_pred = [1 if pred >= 0.5 else 0 for pred in y_pred_proba] + return [1 if pred >= 0.5 else 0 for pred in y_pred_proba] elif problem_type == REGRESSION: - y_pred = y_pred_proba + return y_pred_proba else: - y_pred = np.argmax(y_pred_proba, axis=1) - return y_pred + return np.argmax(y_pred_proba, axis=1) def generate_kfold(X, y=None, n_splits=5, random_state=0, stratified=False, n_repeats=1): @@ -48,11 +47,7 @@ def generate_train_test_split(X: DataFrame, y: Series, problem_type: str, test_s if (test_size <= 0.0) or (test_size >= 1.0): raise ValueError("fraction of data to hold-out must be specified between 0 and 1") - if problem_type in [REGRESSION, SOFTCLASS]: - stratify = None - else: - stratify = y - + stratify = None if problem_type in [REGRESSION, SOFTCLASS] else y # TODO: Enable stratified split when y class would result in 0 samples in test. # One approach: extract low frequency classes from X/y, add back (1-test_size)% to X_train, y_train, rest to X_test # Essentially stratify the high frequency classes, random the low frequency (While ensuring at least 1 example stays for each low frequency in train!) @@ -142,8 +137,7 @@ def get_leaderboard_pareto_frontier(leaderboard: DataFrame, score_col='score_val elif (inference_time_min is None) or (row[inference_time_col] < inference_time_min): inference_time_min = row[inference_time_col] pareto_frontier.append(index) - leaderboard_pareto_frontier = leaderboard_unique.loc[pareto_frontier].reset_index(drop=True) - return leaderboard_pareto_frontier + return leaderboard_unique.loc[pareto_frontier].reset_index(drop=True) def shuffle_df_rows(X: DataFrame, seed=0, reset_index=True): @@ -178,7 +172,7 @@ def normalize_pred_probas(y_predprob, problem_type, eps=1e-7): else: return normalize_multi_probas(y_predprob, eps) else: - raise ValueError(f"Invalid problem_type") + raise ValueError("Invalid problem_type") def normalize_binary_probas(y_predprob, eps): @@ -222,11 +216,7 @@ def infer_problem_type(y: Series): logger.log(20, f'Here are the {unique_count} unique label values in your data: {list(unique_values)}') MULTICLASS_LIMIT = 1000 # if numeric and class count would be above this amount, assume it is regression - if num_rows > 1000: - REGRESS_THRESHOLD = 0.05 # if the unique-ratio is less than this, we assume multiclass classification, even when labels are integers - else: - REGRESS_THRESHOLD = 0.1 - + REGRESS_THRESHOLD = 0.05 if num_rows > 1000 else 0.1 if unique_count == 2: problem_type = BINARY reason = "only two unique label-values observed" @@ -268,9 +258,7 @@ def infer_problem_type(y: Series): def infer_eval_metric(problem_type: str) -> Scorer: """Infers appropriate default eval metric based on problem_type. Useful when no eval_metric was provided.""" - if problem_type == BINARY: - return accuracy - elif problem_type == MULTICLASS: + if problem_type in [BINARY, MULTICLASS]: return accuracy else: return root_mean_squared_error diff --git a/autogluon/utils/tqdm.py b/autogluon/utils/tqdm.py index dc15cb14213f..87981827d916 100644 --- a/autogluon/utils/tqdm.py +++ b/autogluon/utils/tqdm.py @@ -6,63 +6,61 @@ __all__ = ['tqdm'] -if True: # pragma: no cover - # import IPython/Jupyter base widget and display utilities - IPY = 0 - IPYW = 0 - try: # IPython 4.x - import ipywidgets - IPY = 4 +# import IPython/Jupyter base widget and display utilities +IPY = 0 +IPYW = 0 +try: # IPython 4.x + import ipywidgets + IPY = 4 + try: + IPYW = int(ipywidgets.__version__.split('.')[0]) + except AttributeError: # __version__ may not exist in old versions + pass +except ImportError: # IPython 3.x / 2.x + IPY = 32 + import warnings + with warnings.catch_warnings(): + ipy_deprecation_msg = "The `IPython.html` package" \ + " has been deprecated" + warnings.filterwarnings('error', message=f".*{ipy_deprecation_msg}.*") try: - IPYW = int(ipywidgets.__version__.split('.')[0]) - except AttributeError: # __version__ may not exist in old versions - pass - except ImportError: # IPython 3.x / 2.x - IPY = 32 - import warnings - with warnings.catch_warnings(): - ipy_deprecation_msg = "The `IPython.html` package" \ - " has been deprecated" - warnings.filterwarnings('error', - message=".*" + ipy_deprecation_msg + ".*") + import IPython.html.widgets as ipywidgets + except Warning as e: + if ipy_deprecation_msg not in str(e): + raise + warnings.simplefilter('ignore') try: - import IPython.html.widgets as ipywidgets - except Warning as e: - if ipy_deprecation_msg not in str(e): - raise - warnings.simplefilter('ignore') - try: - import IPython.html.widgets as ipywidgets # NOQA - except ImportError: - pass + import IPython.html.widgets as ipywidgets # NOQA except ImportError: pass - - try: # IPython 4.x / 3.x - if IPY == 32: - from IPython.html.widgets import IntProgress, HBox, HTML, VBox - IPY = 3 - else: - from ipywidgets import IntProgress, HBox, HTML, VBox - except ImportError: - try: # IPython 2.x - from IPython.html.widgets import IntProgressWidget as IntProgress - from IPython.html.widgets import ContainerWidget as HBox - from IPython.html.widgets import HTML - IPY = 2 except ImportError: - IPY = 0 + pass - try: - from IPython.display import display # , clear_output +try: # IPython 4.x / 3.x + if IPY == 32: + from IPython.html.widgets import IntProgress, HBox, HTML, VBox + IPY = 3 + else: + from ipywidgets import IntProgress, HBox, HTML, VBox +except ImportError: + try: # IPython 2.x + from IPython.html.widgets import IntProgressWidget as IntProgress + from IPython.html.widgets import ContainerWidget as HBox + from IPython.html.widgets import HTML + IPY = 2 except ImportError: - pass + IPY = 0 + +try: + from IPython.display import display # , clear_output +except ImportError: + pass - # HTML encoding - try: # Py3 - from html import escape - except ImportError: # Py2 - from cgi import escape +# HTML encoding +try: # Py3 + from html import escape +except ImportError: # Py2 + from cgi import escape class mytqdm(base): @@ -101,7 +99,7 @@ def status_printer(_, total=None, desc=None, ncols=None, img=None): ptext = HTML() timg = HTML() if img: - timg.value = "
%s
" % img + timg.value = f"
{img}
" # Only way to place text to the right of the bar is to use a container container = VBox([HBox(children=[pbar, ptext]), timg]) # Prepare layout @@ -139,7 +137,7 @@ def display(self, msg=None, pos=None, pbar.value = self.n if self.img: - timg.value = "
%s
" % self.img + timg.value = f"
{self.img}
" if msg: # html escape special characters (like '&') @@ -164,11 +162,8 @@ def display(self, msg=None, pos=None, ptext.value = right # Change bar style - if bar_style: - # Hack-ish way to avoid the danger bar_style being overridden by - # success because the bar gets closed after the error... - if not (pbar.bar_style == 'danger' and bar_style == 'success'): - pbar.bar_style = bar_style + if bar_style and (pbar.bar_style != 'danger' or bar_style != 'success'): + pbar.bar_style = bar_style # Special signal to close the bar if close and pbar.bar_style != 'danger': # hide only if no error @@ -211,10 +206,7 @@ def __init__(self, *args, **kwargs): def __iter__(self, *args, **kwargs): try: - for obj in super(mytqdm, self).__iter__(*args, **kwargs): - # return super(tqdm...) will not catch exception - yield obj - # NB: except ... [ as ...] breaks IPython async KeyboardInterrupt + yield from super(mytqdm, self).__iter__(*args, **kwargs) except: # NOQA self.sp(bar_style='danger') raise @@ -236,11 +228,10 @@ def close(self, *args, **kwargs): # in manual mode: if n < total, things probably got wrong if self.total and self.n < self.total: self.sp(bar_style='danger') + elif self.leave: + self.sp(bar_style='success') else: - if self.leave: - self.sp(bar_style='success') - else: - self.sp(close=True) + self.sp(close=True) def moveto(self, *args, **kwargs): # void -> avoid extraneous `\n` in IPython output cell diff --git a/autogluon/utils/try_import.py b/autogluon/utils/try_import.py index 3c51a4c28f3c..178e50621afa 100644 --- a/autogluon/utils/try_import.py +++ b/autogluon/utils/try_import.py @@ -5,24 +5,26 @@ def try_import_catboost(): try: import catboost except ValueError as e: - raise ImportError("Import catboost failed. Numpy version may be outdated, " - "Please ensure numpy version >=1.16.0. If it is not, please try 'pip uninstall numpy; pip install numpy>=1.17.0' Detailed info: {}".format(str(e))) + raise ImportError( + f"Import catboost failed. Numpy version may be outdated, Please ensure numpy version >=1.16.0. If it is not, please try 'pip uninstall numpy; pip install numpy>=1.17.0' Detailed info: {str(e)}" + ) def try_import_catboostdev(): # TODO: remove once Catboost 0.24 is released. try: import catboost # Need to first import catboost before catboost_dev and not vice-versa import catboost_dev except (ValueError, ImportError) as e: - raise ImportError("Import catboost_dev failed (needed for distillation with CatBoost models). " - "Make sure you can import catboost and then run: 'pip install catboost-dev'." - "Detailed info: {}".format(str(e))) + raise ImportError( + f"Import catboost_dev failed (needed for distillation with CatBoost models). Make sure you can import catboost and then run: 'pip install catboost-dev'.Detailed info: {str(e)}" + ) def try_import_lightgbm(): try: import lightgbm except OSError as e: - raise ImportError("Import lightgbm failed. If you are using Mac OSX, " - "Please try 'brew install libomp'. Detailed info: {}".format(str(e))) + raise ImportError( + f"Import lightgbm failed. If you are using Mac OSX, Please try 'brew install libomp'. Detailed info: {str(e)}" + ) def try_import_mxboard(): try: @@ -39,11 +41,8 @@ def try_import_mxnet(): from distutils.version import LooseVersion if LooseVersion(mx.__version__) < LooseVersion(mx_version): - msg = ( - "Legacy mxnet-mkl=={} detected, some new modules may not work properly. " - "mxnet-mkl>={} is required. You can use pip to upgrade mxnet " - "`pip install mxnet-mkl --pre --upgrade` " - "or `pip install mxnet-cu90mkl --pre --upgrade`").format(mx.__version__, mx_version) + msg = f"Legacy mxnet-mkl=={mx.__version__} detected, some new modules may not work properly. mxnet-mkl>={mx_version} is required. You can use pip to upgrade mxnet `pip install mxnet-mkl --pre --upgrade` or `pip install mxnet-cu90mkl --pre --upgrade`" + raise ImportError(msg) except ImportError: raise ImportError( diff --git a/setup.py b/setup.py index ad388736ce23..7cb5678fb0de 100644 --- a/setup.py +++ b/setup.py @@ -24,11 +24,11 @@ def create_version_file(): global version, cwd - print('-- Building version ' + version) + print(f'-- Building version {version}') version_path = os.path.join(cwd, 'autogluon', 'version.py') with open(version_path, 'w') as f: f.write('"""This is autogluon version file."""\n') - f.write("__version__ = '{}'\n".format(version)) + f.write(f"__version__ = '{version}'\n") long_description = open('README.md').read()