Spaces:
Configuration error
Configuration error
# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. | |
# | |
# Licensed under the Apache License, Version 2.0 (the "License"); | |
# you may not use this file except in compliance with the License. | |
# You may obtain a copy of the License at | |
# | |
# http://www.apache.org/licenses/LICENSE-2.0 | |
# | |
# Unless required by applicable law or agreed to in writing, software | |
# distributed under the License is distributed on an "AS IS" BASIS, | |
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
# See the License for the specific language governing permissions and | |
# limitations under the License. | |
import codecs | |
import os | |
from typing import Any, Dict, Generic | |
import warnings | |
import paddle | |
import yaml | |
from paddleseg.cvlibs import manager | |
from paddleseg.utils import logger | |
class Config(object): | |
''' | |
Training configuration parsing. The only yaml/yml file is supported. | |
The following hyper-parameters are available in the config file: | |
batch_size: The number of samples per gpu. | |
iters: The total training steps. | |
train_dataset: A training data config including type/data_root/transforms/mode. | |
For data type, please refer to paddleseg.datasets. | |
For specific transforms, please refer to paddleseg.transforms.transforms. | |
val_dataset: A validation data config including type/data_root/transforms/mode. | |
optimizer: A optimizer config, but currently PaddleSeg only supports sgd with momentum in config file. | |
In addition, weight_decay could be set as a regularization. | |
learning_rate: A learning rate config. If decay is configured, learning _rate value is the starting learning rate, | |
where only poly decay is supported using the config file. In addition, decay power and end_lr are tuned experimentally. | |
loss: A loss config. Multi-loss config is available. The loss type order is consistent with the seg model outputs, | |
where the coef term indicates the weight of corresponding loss. Note that the number of coef must be the same as the number of | |
model outputs, and there could be only one loss type if using the same loss type among the outputs, otherwise the number of | |
loss type must be consistent with coef. | |
model: A model config including type/backbone and model-dependent arguments. | |
For model type, please refer to paddleseg.models. | |
For backbone, please refer to paddleseg.models.backbones. | |
Args: | |
path (str) : The path of config file, supports yaml format only. | |
Examples: | |
from paddleseg.cvlibs.config import Config | |
# Create a cfg object with yaml file path. | |
cfg = Config(yaml_cfg_path) | |
# Parsing the argument when its property is used. | |
train_dataset = cfg.train_dataset | |
# the argument of model should be parsed after dataset, | |
# since the model builder uses some properties in dataset. | |
model = cfg.model | |
... | |
''' | |
def __init__(self, | |
path: str, | |
learning_rate: float=None, | |
batch_size: int=None, | |
iters: int=None): | |
if not path: | |
raise ValueError('Please specify the configuration file path.') | |
if not os.path.exists(path): | |
raise FileNotFoundError('File {} does not exist'.format(path)) | |
self._model = None | |
self._losses = None | |
if path.endswith('yml') or path.endswith('yaml'): | |
self.dic = self._parse_from_yaml(path) | |
else: | |
raise RuntimeError('Config file should in yaml format!') | |
self.update( | |
learning_rate=learning_rate, batch_size=batch_size, iters=iters) | |
def _update_dic(self, dic, base_dic): | |
""" | |
Update config from dic based base_dic | |
""" | |
base_dic = base_dic.copy() | |
dic = dic.copy() | |
if dic.get('_inherited_', True) == False: | |
dic.pop('_inherited_') | |
return dic | |
for key, val in dic.items(): | |
if isinstance(val, dict) and key in base_dic: | |
base_dic[key] = self._update_dic(val, base_dic[key]) | |
else: | |
base_dic[key] = val | |
dic = base_dic | |
return dic | |
def _parse_from_yaml(self, path: str): | |
'''Parse a yaml file and build config''' | |
with codecs.open(path, 'r', 'utf-8') as file: | |
dic = yaml.load(file, Loader=yaml.FullLoader) | |
if '_base_' in dic: | |
cfg_dir = os.path.dirname(path) | |
base_path = dic.pop('_base_') | |
base_path = os.path.join(cfg_dir, base_path) | |
base_dic = self._parse_from_yaml(base_path) | |
dic = self._update_dic(dic, base_dic) | |
return dic | |
def update(self, | |
learning_rate: float=None, | |
batch_size: int=None, | |
iters: int=None): | |
'''Update config''' | |
if learning_rate: | |
if 'lr_scheduler' in self.dic: | |
self.dic['lr_scheduler']['learning_rate'] = learning_rate | |
else: | |
self.dic['learning_rate']['value'] = learning_rate | |
if batch_size: | |
self.dic['batch_size'] = batch_size | |
if iters: | |
self.dic['iters'] = iters | |
def batch_size(self) -> int: | |
return self.dic.get('batch_size', 1) | |
def iters(self) -> int: | |
iters = self.dic.get('iters') | |
if not iters: | |
raise RuntimeError('No iters specified in the configuration file.') | |
return iters | |
def lr_scheduler(self) -> paddle.optimizer.lr.LRScheduler: | |
if 'lr_scheduler' not in self.dic: | |
raise RuntimeError( | |
'No `lr_scheduler` specified in the configuration file.') | |
params = self.dic.get('lr_scheduler') | |
use_warmup = False | |
if 'warmup_iters' in params: | |
use_warmup = True | |
warmup_iters = params.pop('warmup_iters') | |
assert 'warmup_start_lr' in params, \ | |
"When use warmup, please set warmup_start_lr and warmup_iters in lr_scheduler" | |
warmup_start_lr = params.pop('warmup_start_lr') | |
end_lr = params['learning_rate'] | |
lr_type = params.pop('type') | |
if lr_type == 'PolynomialDecay': | |
iters = self.iters - warmup_iters if use_warmup else self.iters | |
iters = max(iters, 1) | |
params.setdefault('decay_steps', iters) | |
params.setdefault('end_lr', 0) | |
params.setdefault('power', 0.9) | |
lr_sche = getattr(paddle.optimizer.lr, lr_type)(**params) | |
if use_warmup: | |
lr_sche = paddle.optimizer.lr.LinearWarmup( | |
learning_rate=lr_sche, | |
warmup_steps=warmup_iters, | |
start_lr=warmup_start_lr, | |
end_lr=end_lr) | |
return lr_sche | |
def learning_rate(self) -> paddle.optimizer.lr.LRScheduler: | |
logger.warning( | |
'''`learning_rate` in configuration file will be deprecated, please use `lr_scheduler` instead. E.g | |
lr_scheduler: | |
type: PolynomialDecay | |
learning_rate: 0.01''') | |
_learning_rate = self.dic.get('learning_rate', {}) | |
if isinstance(_learning_rate, float): | |
return _learning_rate | |
_learning_rate = self.dic.get('learning_rate', {}).get('value') | |
if not _learning_rate: | |
raise RuntimeError( | |
'No learning rate specified in the configuration file.') | |
args = self.decay_args | |
decay_type = args.pop('type') | |
if decay_type == 'poly': | |
lr = _learning_rate | |
return paddle.optimizer.lr.PolynomialDecay(lr, **args) | |
elif decay_type == 'piecewise': | |
values = _learning_rate | |
return paddle.optimizer.lr.PiecewiseDecay(values=values, **args) | |
elif decay_type == 'stepdecay': | |
lr = _learning_rate | |
return paddle.optimizer.lr.StepDecay(lr, **args) | |
else: | |
raise RuntimeError('Only poly and piecewise decay support.') | |
def optimizer(self) -> paddle.optimizer.Optimizer: | |
if 'lr_scheduler' in self.dic: | |
lr = self.lr_scheduler | |
else: | |
lr = self.learning_rate | |
args = self.optimizer_args | |
optimizer_type = args.pop('type') | |
if optimizer_type == 'sgd': | |
return paddle.optimizer.Momentum( | |
lr, parameters=self.model.parameters(), **args) | |
elif optimizer_type == 'adam': | |
return paddle.optimizer.Adam( | |
lr, parameters=self.model.parameters(), **args) | |
elif optimizer_type in paddle.optimizer.__all__: | |
return getattr(paddle.optimizer, | |
optimizer_type)(lr, | |
parameters=self.model.parameters(), | |
**args) | |
raise RuntimeError('Unknown optimizer type {}.'.format(optimizer_type)) | |
def optimizer_args(self) -> dict: | |
args = self.dic.get('optimizer', {}).copy() | |
if args['type'] == 'sgd': | |
args.setdefault('momentum', 0.9) | |
return args | |
def decay_args(self) -> dict: | |
args = self.dic.get('learning_rate', {}).get( | |
'decay', {'type': 'poly', | |
'power': 0.9}).copy() | |
if args['type'] == 'poly': | |
args.setdefault('decay_steps', self.iters) | |
args.setdefault('end_lr', 0) | |
return args | |
def loss(self) -> dict: | |
if self._losses is None: | |
self._losses = self._prepare_loss('loss') | |
return self._losses | |
def distill_loss(self) -> dict: | |
if not hasattr(self, '_distill_losses'): | |
self._distill_losses = self._prepare_loss('distill_loss') | |
return self._distill_losses | |
def _prepare_loss(self, loss_name): | |
""" | |
Parse the loss parameters and load the loss layers. | |
Args: | |
loss_name (str): The root name of loss in the yaml file. | |
Returns: | |
dict: A dict including the loss parameters and layers. | |
""" | |
args = self.dic.get(loss_name, {}).copy() | |
if 'types' in args and 'coef' in args: | |
len_types = len(args['types']) | |
len_coef = len(args['coef']) | |
if len_types != len_coef: | |
if len_types == 1: | |
args['types'] = args['types'] * len_coef | |
else: | |
raise ValueError( | |
'The length of types should equal to coef or equal to 1 in loss config, but they are {} and {}.' | |
.format(len_types, len_coef)) | |
else: | |
raise ValueError( | |
'Loss config should contain keys of "types" and "coef"') | |
losses = dict() | |
for key, val in args.items(): | |
if key == 'types': | |
losses['types'] = [] | |
for item in args['types']: | |
if item['type'] != 'MixedLoss': | |
if 'ignore_index' in item: | |
assert item['ignore_index'] == self.train_dataset.ignore_index, 'If ignore_index of loss is set, '\ | |
'the ignore_index of loss and train_dataset must be the same. \nCurrently, loss ignore_index = {}, '\ | |
'train_dataset ignore_index = {}. \nIt is recommended not to set loss ignore_index, so it is consistent with '\ | |
'train_dataset by default.'.format(item['ignore_index'], self.train_dataset.ignore_index) | |
item['ignore_index'] = \ | |
self.train_dataset.ignore_index | |
losses['types'].append(self._load_object(item)) | |
else: | |
losses[key] = val | |
if len(losses['coef']) != len(losses['types']): | |
raise RuntimeError( | |
'The length of coef should equal to types in loss config: {} != {}.' | |
.format(len(losses['coef']), len(losses['types']))) | |
return losses | |
def model(self) -> paddle.nn.Layer: | |
model_cfg = self.dic.get('model').copy() | |
if not model_cfg: | |
raise RuntimeError('No model specified in the configuration file.') | |
if not 'num_classes' in model_cfg: | |
num_classes = None | |
try: | |
if self.train_dataset_config: | |
if hasattr(self.train_dataset_class, 'NUM_CLASSES'): | |
num_classes = self.train_dataset_class.NUM_CLASSES | |
elif 'num_classes' in self.train_dataset_config: | |
num_classes = self.train_dataset_config['num_classes'] | |
elif hasattr(self.train_dataset, 'num_classes'): | |
num_classes = self.train_dataset.num_classes | |
elif self.val_dataset_config: | |
if hasattr(self.val_dataset_class, 'NUM_CLASSES'): | |
num_classes = self.val_dataset_class.NUM_CLASSES | |
elif 'num_classes' in self.val_dataset_config: | |
num_classes = self.val_dataset_config['num_classes'] | |
elif hasattr(self.val_dataset, 'num_classes'): | |
num_classes = self.val_dataset.num_classes | |
except FileNotFoundError: | |
warnings.warn("`dataset_root` is not found. Is it correct?") | |
if num_classes is not None: | |
model_cfg['num_classes'] = num_classes | |
if not self._model: | |
self._model = self._load_object(model_cfg) | |
return self._model | |
def train_dataset_config(self) -> Dict: | |
return self.dic.get('train_dataset', {}).copy() | |
def val_dataset_config(self) -> Dict: | |
return self.dic.get('val_dataset', {}).copy() | |
def train_dataset_class(self) -> Generic: | |
dataset_type = self.train_dataset_config['type'] | |
return self._load_component(dataset_type) | |
def val_dataset_class(self) -> Generic: | |
dataset_type = self.val_dataset_config['type'] | |
return self._load_component(dataset_type) | |
def train_dataset(self) -> paddle.io.Dataset: | |
_train_dataset = self.train_dataset_config | |
if not _train_dataset: | |
return None | |
return self._load_object(_train_dataset) | |
def val_dataset(self) -> paddle.io.Dataset: | |
_val_dataset = self.val_dataset_config | |
if not _val_dataset: | |
return None | |
return self._load_object(_val_dataset) | |
def _load_component(self, com_name: str) -> Any: | |
com_list = [ | |
manager.MODELS, manager.BACKBONES, manager.DATASETS, | |
manager.TRANSFORMS, manager.LOSSES | |
] | |
for com in com_list: | |
if com_name in com.components_dict: | |
return com[com_name] | |
else: | |
raise RuntimeError( | |
'The specified component was not found {}.'.format(com_name)) | |
def _load_object(self, cfg: dict) -> Any: | |
cfg = cfg.copy() | |
if 'type' not in cfg: | |
raise RuntimeError('No object information in {}.'.format(cfg)) | |
component = self._load_component(cfg.pop('type')) | |
params = {} | |
for key, val in cfg.items(): | |
if self._is_meta_type(val): | |
params[key] = self._load_object(val) | |
elif isinstance(val, list): | |
params[key] = [ | |
self._load_object(item) | |
if self._is_meta_type(item) else item for item in val | |
] | |
else: | |
params[key] = val | |
return component(**params) | |
def test_config(self) -> Dict: | |
return self.dic.get('test_config', {}) | |
def export_config(self) -> Dict: | |
return self.dic.get('export', {}) | |
def to_static_training(self) -> bool: | |
'''Whether to use @to_static for training''' | |
return self.dic.get('to_static_training', False) | |
def _is_meta_type(self, item: Any) -> bool: | |
return isinstance(item, dict) and 'type' in item | |
def __str__(self) -> str: | |
return yaml.dump(self.dic) | |
def val_transforms(self) -> list: | |
"""Get val_transform from val_dataset""" | |
_val_dataset = self.val_dataset_config | |
if not _val_dataset: | |
return [] | |
_transforms = _val_dataset.get('transforms', []) | |
transforms = [] | |
for i in _transforms: | |
transforms.append(self._load_object(i)) | |
return transforms | |