From cdc9d93884f5e4cabe81db048881a88c7a8da102 Mon Sep 17 00:00:00 2001 From: Bobholamovic Date: Tue, 16 Aug 2022 10:35:54 +0800 Subject: [PATCH 01/52] _preprocess and _postprocess as public methods --- paddlers/deploy/predictor.py | 12 ++++++------ paddlers/tasks/change_detector.py | 12 ++++++------ paddlers/tasks/classifier.py | 20 +++++++++----------- paddlers/tasks/object_detector.py | 8 ++++---- paddlers/tasks/segmenter.py | 14 +++++++------- test_tipc/infer.py | 12 ++++++------ 6 files changed, 38 insertions(+), 40 deletions(-) diff --git a/paddlers/deploy/predictor.py b/paddlers/deploy/predictor.py index 5c39305..2d6f9e0 100644 --- a/paddlers/deploy/predictor.py +++ b/paddlers/deploy/predictor.py @@ -146,7 +146,7 @@ class Predictor(object): return predictor def preprocess(self, images, transforms): - preprocessed_samples = self._model._preprocess( + preprocessed_samples = self._model.preprocess( images, transforms, to_tensor=False) if self._model.model_type == 'classifier': preprocessed_samples = {'image': preprocessed_samples[0]} @@ -172,12 +172,12 @@ class Predictor(object): def postprocess(self, net_outputs, topk=1, ori_shape=None, transforms=None): if self._model.model_type == 'classifier': true_topk = min(self._model.num_classes, topk) - if self._model._postprocess is None: + if self._model.postprocess is None: self._model.build_postprocess_from_labels(topk) - # XXX: Convert ndarray to tensor as self._model._postprocess requires + # XXX: Convert ndarray to tensor as self._model.postprocess requires assert len(net_outputs) == 1 net_outputs = paddle.to_tensor(net_outputs[0]) - outputs = self._model._postprocess(net_outputs) + outputs = self._model.postprocess(net_outputs) class_ids = map(itemgetter('class_ids'), outputs) scores = map(itemgetter('scores'), outputs) label_names = map(itemgetter('label_names'), outputs) @@ -187,7 +187,7 @@ class Predictor(object): 'label_names_map': n, } for l, s, n in zip(class_ids, scores, label_names)] elif self._model.model_type in ('segmenter', 'change_detector'): - label_map, score_map = self._model._postprocess( + label_map, score_map = self._model.postprocess( net_outputs, batch_origin_shape=ori_shape, transforms=transforms.transforms) @@ -200,7 +200,7 @@ class Predictor(object): k: v for k, v in zip(['bbox', 'bbox_num', 'mask'], net_outputs) } - preds = self._model._postprocess(net_outputs) + preds = self._model.postprocess(net_outputs) else: logging.error( "Invalid model type {}.".format(self._model.model_type), diff --git a/paddlers/tasks/change_detector.py b/paddlers/tasks/change_detector.py index fa4a127..9f682fb 100644 --- a/paddlers/tasks/change_detector.py +++ b/paddlers/tasks/change_detector.py @@ -111,10 +111,10 @@ class BaseChangeDetector(BaseModel): if mode == 'test': origin_shape = inputs[2] if self.status == 'Infer': - label_map_list, score_map_list = self._postprocess( + label_map_list, score_map_list = self.postprocess( net_out, origin_shape, transforms=inputs[3]) else: - logit_list = self._postprocess( + logit_list = self.postprocess( logit, origin_shape, transforms=inputs[3]) label_map_list = [] score_map_list = [] @@ -142,7 +142,7 @@ class BaseChangeDetector(BaseModel): raise ValueError("Expected label.ndim == 4 but got {}".format( label.ndim)) origin_shape = [label.shape[-2:]] - pred = self._postprocess( + pred = self.postprocess( pred, origin_shape, transforms=inputs[3])[0] # NCHW intersect_area, pred_area, label_area = paddleseg.utils.metrics.calculate_area( pred, label, self.num_classes) @@ -553,7 +553,7 @@ class BaseChangeDetector(BaseModel): images = [img_file] else: images = img_file - batch_im1, batch_im2, batch_origin_shape = self._preprocess( + batch_im1, batch_im2, batch_origin_shape = self.preprocess( images, transforms, self.model_type) self.net.eval() data = (batch_im1, batch_im2, batch_origin_shape, transforms.transforms) @@ -664,7 +664,7 @@ class BaseChangeDetector(BaseModel): dst_data = None print("GeoTiff saved in {}.".format(save_file)) - def _preprocess(self, images, transforms, to_tensor=True): + def preprocess(self, images, transforms, to_tensor=True): self._check_transforms(transforms, 'test') batch_im1, batch_im2 = list(), list() batch_ori_shape = list() @@ -736,7 +736,7 @@ class BaseChangeDetector(BaseModel): batch_restore_list.append(restore_list) return batch_restore_list - def _postprocess(self, batch_pred, batch_origin_shape, transforms): + def postprocess(self, batch_pred, batch_origin_shape, transforms): batch_restore_list = BaseChangeDetector.get_transforms_shape_info( batch_origin_shape, transforms) if isinstance(batch_pred, (tuple, list)) and self.status == 'Infer': diff --git a/paddlers/tasks/classifier.py b/paddlers/tasks/classifier.py index 7e6c109..1e113de 100644 --- a/paddlers/tasks/classifier.py +++ b/paddlers/tasks/classifier.py @@ -61,7 +61,7 @@ class BaseClassifier(BaseModel): self.metrics = None self.losses = None self.labels = None - self._postprocess = None + self.postprocess = None if params.get('with_net', True): params.pop('with_net', None) self.net = self.build_net(**params) @@ -121,13 +121,12 @@ class BaseClassifier(BaseModel): net_out = net(inputs[0]) if mode == 'test': - return self._postprocess(net_out) + return self.postprocess(net_out) outputs = OrderedDict() label = paddle.to_tensor(inputs[1], dtype="int64") if mode == 'eval': - # print(self._postprocess(net_out)[0]) # for test label = paddle.unsqueeze(label, axis=-1) metric_dict = self.metrics(net_out, label) outputs['top1'] = metric_dict["top1"] @@ -176,13 +175,13 @@ class BaseClassifier(BaseModel): label_dict = dict() for i, label in enumerate(self.labels): label_dict[i] = label - self._postprocess = build_postprocess({ + self.postprocess = build_postprocess({ "name": "Topk", "topk": topk, "class_id_map_file": None }) # Add class_id_map from model.yml - self._postprocess.class_id_map = label_dict + self.postprocess.class_id_map = label_dict def train(self, num_epochs, @@ -247,8 +246,7 @@ class BaseClassifier(BaseModel): if self.losses is None: self.losses = self.default_loss() self.metrics = self.default_metric() - self._postprocess = self.default_postprocess(train_dataset.label_list) - # print(self._postprocess.class_id_map) + self.postprocess = self.default_postprocess(train_dataset.label_list) if optimizer is None: num_steps_each_epoch = train_dataset.num_samples // train_batch_size @@ -454,12 +452,12 @@ class BaseClassifier(BaseModel): images = [img_file] else: images = img_file - batch_im, batch_origin_shape = self._preprocess(images, transforms, - self.model_type) + batch_im, batch_origin_shape = self.preprocess(images, transforms, + self.model_type) self.net.eval() data = (batch_im, batch_origin_shape, transforms.transforms) - if self._postprocess is None: + if self.postprocess is None: self.build_postprocess_from_labels() outputs = self.run(self.net, data, 'test') @@ -480,7 +478,7 @@ class BaseClassifier(BaseModel): } return prediction - def _preprocess(self, images, transforms, to_tensor=True): + def preprocess(self, images, transforms, to_tensor=True): self._check_transforms(transforms, 'test') batch_im = list() batch_ori_shape = list() diff --git a/paddlers/tasks/object_detector.py b/paddlers/tasks/object_detector.py index 4dc8ae5..945ac81 100644 --- a/paddlers/tasks/object_detector.py +++ b/paddlers/tasks/object_detector.py @@ -580,16 +580,16 @@ class BaseDetector(BaseModel): else: images = img_file - batch_samples = self._preprocess(images, transforms) + batch_samples = self.preprocess(images, transforms) self.net.eval() outputs = self.run(self.net, batch_samples, 'test') - prediction = self._postprocess(outputs) + prediction = self.postprocess(outputs) if isinstance(img_file, (str, np.ndarray)): prediction = prediction[0] return prediction - def _preprocess(self, images, transforms, to_tensor=True): + def preprocess(self, images, transforms, to_tensor=True): self._check_transforms(transforms, 'test') batch_samples = list() for im in images: @@ -606,7 +606,7 @@ class BaseDetector(BaseModel): return batch_samples - def _postprocess(self, batch_pred): + def postprocess(self, batch_pred): infer_result = {} if 'bbox' in batch_pred: bboxes = batch_pred['bbox'] diff --git a/paddlers/tasks/segmenter.py b/paddlers/tasks/segmenter.py index 900f481..3c052d7 100644 --- a/paddlers/tasks/segmenter.py +++ b/paddlers/tasks/segmenter.py @@ -110,10 +110,10 @@ class BaseSegmenter(BaseModel): if mode == 'test': origin_shape = inputs[1] if self.status == 'Infer': - label_map_list, score_map_list = self._postprocess( + label_map_list, score_map_list = self.postprocess( net_out, origin_shape, transforms=inputs[2]) else: - logit_list = self._postprocess( + logit_list = self.postprocess( logit, origin_shape, transforms=inputs[2]) label_map_list = [] score_map_list = [] @@ -141,7 +141,7 @@ class BaseSegmenter(BaseModel): raise ValueError("Expected label.ndim == 4 but got {}".format( label.ndim)) origin_shape = [label.shape[-2:]] - pred = self._postprocess( + pred = self.postprocess( pred, origin_shape, transforms=inputs[2])[0] # NCHW intersect_area, pred_area, label_area = paddleseg.utils.metrics.calculate_area( pred, label, self.num_classes) @@ -526,8 +526,8 @@ class BaseSegmenter(BaseModel): images = [img_file] else: images = img_file - batch_im, batch_origin_shape = self._preprocess(images, transforms, - self.model_type) + batch_im, batch_origin_shape = self.preprocess(images, transforms, + self.model_type) self.net.eval() data = (batch_im, batch_origin_shape, transforms.transforms) outputs = self.run(self.net, data, 'test') @@ -631,7 +631,7 @@ class BaseSegmenter(BaseModel): dst_data = None print("GeoTiff saved in {}.".format(save_file)) - def _preprocess(self, images, transforms, to_tensor=True): + def preprocess(self, images, transforms, to_tensor=True): self._check_transforms(transforms, 'test') batch_im = list() batch_ori_shape = list() @@ -698,7 +698,7 @@ class BaseSegmenter(BaseModel): batch_restore_list.append(restore_list) return batch_restore_list - def _postprocess(self, batch_pred, batch_origin_shape, transforms): + def postprocess(self, batch_pred, batch_origin_shape, transforms): batch_restore_list = BaseSegmenter.get_transforms_shape_info( batch_origin_shape, transforms) if isinstance(batch_pred, (tuple, list)) and self.status == 'Infer': diff --git a/test_tipc/infer.py b/test_tipc/infer.py index 9ad6123..3672940 100644 --- a/test_tipc/infer.py +++ b/test_tipc/infer.py @@ -141,7 +141,7 @@ class TIPCPredictor(object): return config def preprocess(self, images, transforms): - preprocessed_samples = self._model._preprocess( + preprocessed_samples = self._model.preprocess( images, transforms, to_tensor=False) if self._model.model_type == 'classifier': preprocessed_samples = {'image': preprocessed_samples[0]} @@ -167,12 +167,12 @@ class TIPCPredictor(object): def postprocess(self, net_outputs, topk=1, ori_shape=None, transforms=None): if self._model.model_type == 'classifier': true_topk = min(self._model.num_classes, topk) - if self._model._postprocess is None: + if self._model.postprocess is None: self._model.build_postprocess_from_labels(topk) - # XXX: Convert ndarray to tensor as self._model._postprocess requires + # XXX: Convert ndarray to tensor as self._model.postprocess requires assert len(net_outputs) == 1 net_outputs = paddle.to_tensor(net_outputs[0]) - outputs = self._model._postprocess(net_outputs) + outputs = self._model.postprocess(net_outputs) class_ids = map(itemgetter('class_ids'), outputs) scores = map(itemgetter('scores'), outputs) label_names = map(itemgetter('label_names'), outputs) @@ -182,7 +182,7 @@ class TIPCPredictor(object): 'label_names_map': n, } for l, s, n in zip(class_ids, scores, label_names)] elif self._model.model_type in ('segmenter', 'change_detector'): - label_map, score_map = self._model._postprocess( + label_map, score_map = self._model.postprocess( net_outputs, batch_origin_shape=ori_shape, transforms=transforms.transforms) @@ -195,7 +195,7 @@ class TIPCPredictor(object): k: v for k, v in zip(['bbox', 'bbox_num', 'mask'], net_outputs) } - preds = self._model._postprocess(net_outputs) + preds = self._model.postprocess(net_outputs) else: logging.error( "Invalid model type {}.".format(self._model.model_type), From 7696de288cda4efc698103d4199144b541485641 Mon Sep 17 00:00:00 2001 From: Bobholamovic Date: Tue, 16 Aug 2022 11:54:34 +0800 Subject: [PATCH 02/52] input_channel->in_channels --- paddlers/tasks/segmenter.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/paddlers/tasks/segmenter.py b/paddlers/tasks/segmenter.py index 3c052d7..460d725 100644 --- a/paddlers/tasks/segmenter.py +++ b/paddlers/tasks/segmenter.py @@ -781,7 +781,7 @@ class BaseSegmenter(BaseModel): class UNet(BaseSegmenter): def __init__(self, - input_channel=3, + in_channels=3, num_classes=2, use_mixed_loss=False, use_deconv=False, @@ -793,7 +793,7 @@ class UNet(BaseSegmenter): }) super(UNet, self).__init__( model_name='UNet', - input_channel=input_channel, + input_channel=in_channels, num_classes=num_classes, use_mixed_loss=use_mixed_loss, **params) @@ -801,7 +801,7 @@ class UNet(BaseSegmenter): class DeepLabV3P(BaseSegmenter): def __init__(self, - input_channel=3, + in_channels=3, num_classes=2, backbone='ResNet50_vd', use_mixed_loss=False, @@ -819,7 +819,7 @@ class DeepLabV3P(BaseSegmenter): if params.get('with_net', True): with DisablePrint(): backbone = getattr(paddleseg.models, backbone)( - input_channel=input_channel, output_stride=output_stride) + input_channel=in_channels, output_stride=output_stride) else: backbone = None params.update({ From 8ee7f9e049ea04e525f5ae3227c4534b1658fde5 Mon Sep 17 00:00:00 2001 From: Bobholamovic Date: Tue, 16 Aug 2022 11:55:22 +0800 Subject: [PATCH 03/52] train->_pre_train+_real_train --- paddlers/tasks/object_detector.py | 294 +++++------------------------- 1 file changed, 49 insertions(+), 245 deletions(-) diff --git a/paddlers/tasks/object_detector.py b/paddlers/tasks/object_detector.py index 945ac81..3abad47 100644 --- a/paddlers/tasks/object_detector.py +++ b/paddlers/tasks/object_detector.py @@ -251,6 +251,34 @@ class BaseDetector(BaseModel): Defaults to None. """ + args = self._pre_train(locals()) + return self._real_train(**args) + + def _pre_train(self, in_args): + return in_args + + def _real_train(self, + num_epochs, + train_dataset, + train_batch_size=64, + eval_dataset=None, + optimizer=None, + save_interval_epochs=1, + log_interval_steps=10, + save_dir='output', + pretrain_weights='IMAGENET', + learning_rate=.001, + warmup_steps=0, + warmup_start_lr=0.0, + lr_decay_epochs=(216, 243), + lr_decay_gamma=0.1, + metric=None, + use_ema=False, + early_stop=False, + early_stop_patience=5, + use_vdl=True, + resume_checkpoint=None): + if self.status == 'Infer': logging.error( "Exported inference model does not support training.", @@ -877,108 +905,24 @@ class PicoDet(BaseDetector): self.fixed_input_shape = image_shape return self._define_input_spec(image_shape) - def train(self, - num_epochs, - train_dataset, - train_batch_size=64, - eval_dataset=None, - optimizer=None, - save_interval_epochs=1, - log_interval_steps=10, - save_dir='output', - pretrain_weights='IMAGENET', - learning_rate=.001, - warmup_steps=0, - warmup_start_lr=0.0, - lr_decay_epochs=(216, 243), - lr_decay_gamma=0.1, - metric=None, - use_ema=False, - early_stop=False, - early_stop_patience=5, - use_vdl=True, - resume_checkpoint=None): - """ - Train the model. - - Args: - num_epochs (int): Number of epochs. - train_dataset (paddlers.datasets.COCODetDataset|paddlers.datasets.VOCDetDataset): - Training dataset. - train_batch_size (int, optional): Total batch size among all cards used in - training. Defaults to 64. - eval_dataset (paddlers.datasets.COCODetDataset|paddlers.datasets.VOCDetDataset, optional): - Evaluation dataset. If None, the model will not be evaluated during training - process. Defaults to None. - optimizer (paddle.optimizer.Optimizer|None, optional): Optimizer used for - training. If None, a default optimizer will be used. Defaults to None. - save_interval_epochs (int, optional): Epoch interval for saving the model. - Defaults to 1. - log_interval_steps (int, optional): Step interval for printing training - information. Defaults to 10. - save_dir (str, optional): Directory to save the model. Defaults to 'output'. - pretrain_weights (str|None, optional): None or name/path of pretrained - weights. If None, no pretrained weights will be loaded. - Defaults to 'IMAGENET'. - learning_rate (float, optional): Learning rate for training. Defaults to .001. - warmup_steps (int, optional): Number of steps of warm-up training. - Defaults to 0. - warmup_start_lr (float, optional): Start learning rate of warm-up training. - Defaults to 0.. - lr_decay_epochs (list|tuple, optional): Epoch milestones for learning - rate decay. Defaults to (216, 243). - lr_decay_gamma (float, optional): Gamma coefficient of learning rate decay. - Defaults to .1. - metric (str|None, optional): Evaluation metric. Choices are {'VOC', 'COCO', None}. - If None, determine the metric according to the dataset format. - Defaults to None. - use_ema (bool, optional): Whether to use exponential moving average - strategy. Defaults to False. - early_stop (bool, optional): Whether to adopt early stop strategy. - Defaults to False. - early_stop_patience (int, optional): Early stop patience. Defaults to 5. - use_vdl(bool, optional): Whether to use VisualDL to monitor the training - process. Defaults to True. - resume_checkpoint (str|None, optional): Path of the checkpoint to resume - training from. If None, no training checkpoint will be resumed. At most - Aone of `resume_checkpoint` and `pretrain_weights` can be set simultaneously. - Defaults to None. - """ - + def _pre_train(self, in_args): + optimizer = in_args['optimizer'] if optimizer is None: - num_steps_each_epoch = len(train_dataset) // train_batch_size + num_steps_each_epoch = len(in_args['train_dataset']) // in_args[ + 'train_batch_size'] optimizer = self.default_optimizer( parameters=self.net.parameters(), - learning_rate=learning_rate, - warmup_steps=warmup_steps, - warmup_start_lr=warmup_start_lr, - lr_decay_epochs=lr_decay_epochs, - lr_decay_gamma=lr_decay_gamma, - num_steps_each_epoch=num_steps_each_epoch, + learning_rate=in_args['learning_rate'], + warmup_steps=in_args['warmup_steps'], + warmup_start_lr=in_args['warmup_start_lr'], + lr_decay_epochs=in_args['lr_decay_epochs'], + lr_decay_gamma=in_args['lr_decay_gamma'], + num_steps_each_epoch=in_args['num_steps_each_epoch'], reg_coeff=4e-05, scheduler='Cosine', - num_epochs=num_epochs) - super(PicoDet, self).train( - num_epochs=num_epochs, - train_dataset=train_dataset, - train_batch_size=train_batch_size, - eval_dataset=eval_dataset, - optimizer=optimizer, - save_interval_epochs=save_interval_epochs, - log_interval_steps=log_interval_steps, - save_dir=save_dir, - pretrain_weights=pretrain_weights, - learning_rate=learning_rate, - warmup_steps=warmup_steps, - warmup_start_lr=warmup_start_lr, - lr_decay_epochs=lr_decay_epochs, - lr_decay_gamma=lr_decay_gamma, - metric=metric, - use_ema=use_ema, - early_stop=early_stop, - early_stop_patience=early_stop_patience, - use_vdl=use_vdl, - resume_checkpoint=resume_checkpoint) + num_epochs=in_args['num_epochs']) + in_args['optimizer'] = optimizer + return in_args class YOLOv3(BaseDetector): @@ -1370,82 +1314,12 @@ class FasterRCNN(BaseDetector): super(FasterRCNN, self).__init__( model_name='FasterRCNN', num_classes=num_classes, **params) - def train(self, - num_epochs, - train_dataset, - train_batch_size=64, - eval_dataset=None, - optimizer=None, - save_interval_epochs=1, - log_interval_steps=10, - save_dir='output', - pretrain_weights='IMAGENET', - learning_rate=.001, - warmup_steps=0, - warmup_start_lr=0.0, - lr_decay_epochs=(216, 243), - lr_decay_gamma=0.1, - metric=None, - use_ema=False, - early_stop=False, - early_stop_patience=5, - use_vdl=True, - resume_checkpoint=None): - """ - Train the model. - - Args: - num_epochs (int): Number of epochs. - train_dataset (paddlers.datasets.COCODetDataset|paddlers.datasets.VOCDetDataset): - Training dataset. - train_batch_size (int, optional): Total batch size among all cards used in - training. Defaults to 64. - eval_dataset (paddlers.datasets.COCODetDataset|paddlers.datasets.VOCDetDataset, optional): - Evaluation dataset. If None, the model will not be evaluated during training - process. Defaults to None. - optimizer (paddle.optimizer.Optimizer|None, optional): Optimizer used for - training. If None, a default optimizer will be used. Defaults to None. - save_interval_epochs (int, optional): Epoch interval for saving the model. - Defaults to 1. - log_interval_steps (int, optional): Step interval for printing training - information. Defaults to 10. - save_dir (str, optional): Directory to save the model. Defaults to 'output'. - pretrain_weights (str|None, optional): None or name/path of pretrained - weights. If None, no pretrained weights will be loaded. - Defaults to 'IMAGENET'. - learning_rate (float, optional): Learning rate for training. Defaults to .001. - warmup_steps (int, optional): Number of steps of warm-up training. - Defaults to 0. - warmup_start_lr (float, optional): Start learning rate of warm-up training. - Defaults to 0.. - lr_decay_epochs (list|tuple, optional): Epoch milestones for learning - rate decay. Defaults to (216, 243). - lr_decay_gamma (float, optional): Gamma coefficient of learning rate decay. - Defaults to .1. - metric (str|None, optional): Evaluation metric. Choices are {'VOC', 'COCO', None}. - If None, determine the metric according to the dataset format. - Defaults to None. - use_ema (bool, optional): Whether to use exponential moving average - strategy. Defaults to False. - early_stop (bool, optional): Whether to adopt early stop strategy. - Defaults to False. - early_stop_patience (int, optional): Early stop patience. Defaults to 5. - use_vdl(bool, optional): Whether to use VisualDL to monitor the training - process. Defaults to True. - resume_checkpoint (str|None, optional): Path of the checkpoint to resume - training from. If None, no training checkpoint will be resumed. At most - Aone of `resume_checkpoint` and `pretrain_weights` can be set simultaneously. - Defaults to None. - """ - + def _pre_train(self, in_args): + train_dataset = in_args['train_dataset'] if train_dataset.pos_num < len(train_dataset.file_list): + # In-place modification train_dataset.num_workers = 0 - super(FasterRCNN, self).train( - num_epochs, train_dataset, train_batch_size, eval_dataset, - optimizer, save_interval_epochs, log_interval_steps, save_dir, - pretrain_weights, learning_rate, warmup_steps, warmup_start_lr, - lr_decay_epochs, lr_decay_gamma, metric, use_ema, early_stop, - early_stop_patience, use_vdl, resume_checkpoint) + return in_args def _compose_batch_transform(self, transforms, mode='train'): if mode == 'train': @@ -2212,82 +2086,12 @@ class MaskRCNN(BaseDetector): super(MaskRCNN, self).__init__( model_name='MaskRCNN', num_classes=num_classes, **params) - def train(self, - num_epochs, - train_dataset, - train_batch_size=64, - eval_dataset=None, - optimizer=None, - save_interval_epochs=1, - log_interval_steps=10, - save_dir='output', - pretrain_weights='IMAGENET', - learning_rate=.001, - warmup_steps=0, - warmup_start_lr=0.0, - lr_decay_epochs=(216, 243), - lr_decay_gamma=0.1, - metric=None, - use_ema=False, - early_stop=False, - early_stop_patience=5, - use_vdl=True, - resume_checkpoint=None): - """ - Train the model. - - Args: - num_epochs (int): Number of epochs. - train_dataset (paddlers.datasets.COCODetDataset|paddlers.datasets.VOCDetDataset): - Training dataset. - train_batch_size (int, optional): Total batch size among all cards used in - training. Defaults to 64. - eval_dataset (paddlers.datasets.COCODetDataset|paddlers.datasets.VOCDetDataset, optional): - Evaluation dataset. If None, the model will not be evaluated during training - process. Defaults to None. - optimizer (paddle.optimizer.Optimizer|None, optional): Optimizer used for - training. If None, a default optimizer will be used. Defaults to None. - save_interval_epochs (int, optional): Epoch interval for saving the model. - Defaults to 1. - log_interval_steps (int, optional): Step interval for printing training - information. Defaults to 10. - save_dir (str, optional): Directory to save the model. Defaults to 'output'. - pretrain_weights (str|None, optional): None or name/path of pretrained - weights. If None, no pretrained weights will be loaded. - Defaults to 'IMAGENET'. - learning_rate (float, optional): Learning rate for training. Defaults to .001. - warmup_steps (int, optional): Number of steps of warm-up training. - Defaults to 0. - warmup_start_lr (float, optional): Start learning rate of warm-up training. - Defaults to 0.. - lr_decay_epochs (list|tuple, optional): Epoch milestones for learning - rate decay. Defaults to (216, 243). - lr_decay_gamma (float, optional): Gamma coefficient of learning rate decay. - Defaults to .1. - metric (str|None, optional): Evaluation metric. Choices are {'VOC', 'COCO', None}. - If None, determine the metric according to the dataset format. - Defaults to None. - use_ema (bool, optional): Whether to use exponential moving average - strategy. Defaults to False. - early_stop (bool, optional): Whether to adopt early stop strategy. - Defaults to False. - early_stop_patience (int, optional): Early stop patience. Defaults to 5. - use_vdl(bool, optional): Whether to use VisualDL to monitor the training - process. Defaults to True. - resume_checkpoint (str|None, optional): Path of the checkpoint to resume - training from. If None, no training checkpoint will be resumed. At most - Aone of `resume_checkpoint` and `pretrain_weights` can be set simultaneously. - Defaults to None. - """ - + def _pre_train(self, in_args): + train_dataset = in_args['train_dataset'] if train_dataset.pos_num < len(train_dataset.file_list): + # In-place modification train_dataset.num_workers = 0 - super(MaskRCNN, self).train( - num_epochs, train_dataset, train_batch_size, eval_dataset, - optimizer, save_interval_epochs, log_interval_steps, save_dir, - pretrain_weights, learning_rate, warmup_steps, warmup_start_lr, - lr_decay_epochs, lr_decay_gamma, metric, use_ema, early_stop, - early_stop_patience, use_vdl, resume_checkpoint) + return in_args def _compose_batch_transform(self, transforms, mode='train'): if mode == 'train': From a263ebdc68bb34b56c4c909dd1b55b90fa39902d Mon Sep 17 00:00:00 2001 From: Bobholamovic Date: Tue, 16 Aug 2022 17:09:49 +0800 Subject: [PATCH 04/52] Init examples --- README.md | 5 ++-- examples/README.md | 26 +++++++++++++++++++ examples/rs_competition/README.md | 11 ++++++++ examples/rs_competition/main.ipynb | 0 examples/rs_research/README.md | 9 +++++++ examples/rs_research/configs/levircd/bit.yaml | 0 .../configs/levircd/changeformer.yaml | 0 .../configs/levircd/custom_model.yaml | 0 .../rs_research/configs/levircd/fc_ef.yaml | 0 .../configs/levircd/fc_siam_conc.yaml | 0 .../configs/levircd/fc_siam_diff.yaml | 0 .../rs_research/configs/levircd/levircd.yaml | 0 .../rs_research/configs/levircd/snunet.yaml | 0 .../rs_research/configs/levircd/stanet.yaml | 0 examples/rs_research/configs/svcd/bit.yaml | 0 .../configs/svcd/changeformer.yaml | 0 .../configs/svcd/custom_model.yaml | 0 examples/rs_research/configs/svcd/fc_ef.yaml | 0 .../configs/svcd/fc_siam_conc.yaml | 0 .../configs/svcd/fc_siam_diff.yaml | 0 examples/rs_research/configs/svcd/snunet.yaml | 0 examples/rs_research/configs/svcd/stanet.yaml | 0 examples/rs_research/configs/svcd/svcd.yaml | 0 examples/rs_research/custom_model.py | 0 examples/rs_research/custom_trainer.py | 0 examples/rs_research/test.py | 0 examples/rs_research/train.py | 0 tools/prepare_dataset/prepare_levircd.py | 0 tools/prepare_dataset/prepare_svcd.py | 0 29 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 examples/README.md create mode 100644 examples/rs_competition/README.md create mode 100644 examples/rs_competition/main.ipynb create mode 100644 examples/rs_research/README.md create mode 100644 examples/rs_research/configs/levircd/bit.yaml create mode 100644 examples/rs_research/configs/levircd/changeformer.yaml create mode 100644 examples/rs_research/configs/levircd/custom_model.yaml create mode 100644 examples/rs_research/configs/levircd/fc_ef.yaml create mode 100644 examples/rs_research/configs/levircd/fc_siam_conc.yaml create mode 100644 examples/rs_research/configs/levircd/fc_siam_diff.yaml create mode 100644 examples/rs_research/configs/levircd/levircd.yaml create mode 100644 examples/rs_research/configs/levircd/snunet.yaml create mode 100644 examples/rs_research/configs/levircd/stanet.yaml create mode 100644 examples/rs_research/configs/svcd/bit.yaml create mode 100644 examples/rs_research/configs/svcd/changeformer.yaml create mode 100644 examples/rs_research/configs/svcd/custom_model.yaml create mode 100644 examples/rs_research/configs/svcd/fc_ef.yaml create mode 100644 examples/rs_research/configs/svcd/fc_siam_conc.yaml create mode 100644 examples/rs_research/configs/svcd/fc_siam_diff.yaml create mode 100644 examples/rs_research/configs/svcd/snunet.yaml create mode 100644 examples/rs_research/configs/svcd/stanet.yaml create mode 100644 examples/rs_research/configs/svcd/svcd.yaml create mode 100644 examples/rs_research/custom_model.py create mode 100644 examples/rs_research/custom_trainer.py create mode 100644 examples/rs_research/test.py create mode 100644 examples/rs_research/train.py create mode 100644 tools/prepare_dataset/prepare_levircd.py create mode 100644 tools/prepare_dataset/prepare_svcd.py diff --git a/README.md b/README.md index 59b3232..e3e1f0d 100644 --- a/README.md +++ b/README.md @@ -195,9 +195,8 @@ PaddleRS是遥感科研院所、相关高校共同基于飞桨开发的遥感处 * 推理部署 * 模型导出 * 推理预测 -* 应用案例 - * [变化检测示例](./docs/cases/csc_cd_cn.md) - * [超分模块示例](./docs/cases/sr_seg_cn.md) +* 实践案例 + * [PaddleRS实践案例库](./examples/README.md) * 代码贡献 * [PaddleRS代码注释规范](https://github.com/PaddlePaddle/PaddleRS/wiki/PaddleRS代码注释规范) diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..e6f9acb --- /dev/null +++ b/examples/README.md @@ -0,0 +1,26 @@ +# PaddleRS实践案例 + +PaddleRS提供从科学研究到产业应用的丰富示例,希望帮助遥感领域科研从业者快速完成算法的研发、验证和调优,以及帮助投身于产业实践的开发者便捷地实现从数据预处理到模型部署的全流程遥感深度学习应用。 + +## 官方案例 + +- [PaddleRS竞赛实战:第十一届中国软件杯遥感赛项](./rs_competition/) +- [PaddleRS科研实战:设计深度学习变化检测模型](./rs_research/) + +## 社区贡献案例 + +[AI Studio](https://aistudio.baidu.com/aistudio/index)是基于百度深度学习平台飞桨的人工智能学习与实训社区,提供在线编程环境、免费GPU算力、海量开源算法和开放数据,帮助开发者快速创建和部署模型。您可以在AI Studio上探索PaddleRS的更多玩法: + +[AI Studio上的PaddleRS相关项目](https://aistudio.baidu.com/aistudio/projectoverview/public?kw=PaddleRS) + +本文档收集了部分由开源爱好者贡献的精品项目: + +- [手把手教你PaddleRS实现变化检测](https://aistudio.baidu.com/aistudio/projectdetail/3737991) +- [【PPSIG】PaddleRS变化检测模型部署:以BIT为例](https://aistudio.baidu.com/aistudio/projectdetail/4184759) +- [PaddleRS:使用超分模型提高真实的低分辨率无人机影像的分割精度](https://aistudio.baidu.com/aistudio/projectdetail/3696814) +- [PaddleRS:无人机汽车识别](https://aistudio.baidu.com/aistudio/projectdetail/3713122) +- [PaddleRS:高光谱卫星影像场景分类](https://aistudio.baidu.com/aistudio/projectdetail/3711240) +- [PaddleRS:利用卫星影像与数字高程模型进行滑坡识别](https://aistudio.baidu.com/aistudio/projectdetail/4066570) +- [【PPSIG】PaddleRS实现遥感影像场景分类](https://aistudio.baidu.com/aistudio/projectdetail/4198965) +- [为PaddleRS添加一个袖珍配置系统](https://aistudio.baidu.com/aistudio/projectdetail/4203534) +- [万丈高楼平地起 基于PaddleGAN与PaddleRS的建筑物生成](https://aistudio.baidu.com/aistudio/projectdetail/3716885) diff --git a/examples/rs_competition/README.md b/examples/rs_competition/README.md new file mode 100644 index 0000000..05371d2 --- /dev/null +++ b/examples/rs_competition/README.md @@ -0,0 +1,11 @@ +# PaddleRS竞赛实战:第十一届中国软件杯遥感赛项 + +## 环境配置 + +## 数据准备 + +## 模型训练、评估与推理 + +请参考[main.ipynb](./main.ipynb)中的内容。 + +## 结果导出 diff --git a/examples/rs_competition/main.ipynb b/examples/rs_competition/main.ipynb new file mode 100644 index 0000000..e69de29 diff --git a/examples/rs_research/README.md b/examples/rs_research/README.md new file mode 100644 index 0000000..2c7cee4 --- /dev/null +++ b/examples/rs_research/README.md @@ -0,0 +1,9 @@ +# PaddleRS科研实战:设计深度学习变化检测模型 + +## 环境配置 + +## 数据准备 + +## 模型设计与验证 + +## 获取对比算法指标 diff --git a/examples/rs_research/configs/levircd/bit.yaml b/examples/rs_research/configs/levircd/bit.yaml new file mode 100644 index 0000000..e69de29 diff --git a/examples/rs_research/configs/levircd/changeformer.yaml b/examples/rs_research/configs/levircd/changeformer.yaml new file mode 100644 index 0000000..e69de29 diff --git a/examples/rs_research/configs/levircd/custom_model.yaml b/examples/rs_research/configs/levircd/custom_model.yaml new file mode 100644 index 0000000..e69de29 diff --git a/examples/rs_research/configs/levircd/fc_ef.yaml b/examples/rs_research/configs/levircd/fc_ef.yaml new file mode 100644 index 0000000..e69de29 diff --git a/examples/rs_research/configs/levircd/fc_siam_conc.yaml b/examples/rs_research/configs/levircd/fc_siam_conc.yaml new file mode 100644 index 0000000..e69de29 diff --git a/examples/rs_research/configs/levircd/fc_siam_diff.yaml b/examples/rs_research/configs/levircd/fc_siam_diff.yaml new file mode 100644 index 0000000..e69de29 diff --git a/examples/rs_research/configs/levircd/levircd.yaml b/examples/rs_research/configs/levircd/levircd.yaml new file mode 100644 index 0000000..e69de29 diff --git a/examples/rs_research/configs/levircd/snunet.yaml b/examples/rs_research/configs/levircd/snunet.yaml new file mode 100644 index 0000000..e69de29 diff --git a/examples/rs_research/configs/levircd/stanet.yaml b/examples/rs_research/configs/levircd/stanet.yaml new file mode 100644 index 0000000..e69de29 diff --git a/examples/rs_research/configs/svcd/bit.yaml b/examples/rs_research/configs/svcd/bit.yaml new file mode 100644 index 0000000..e69de29 diff --git a/examples/rs_research/configs/svcd/changeformer.yaml b/examples/rs_research/configs/svcd/changeformer.yaml new file mode 100644 index 0000000..e69de29 diff --git a/examples/rs_research/configs/svcd/custom_model.yaml b/examples/rs_research/configs/svcd/custom_model.yaml new file mode 100644 index 0000000..e69de29 diff --git a/examples/rs_research/configs/svcd/fc_ef.yaml b/examples/rs_research/configs/svcd/fc_ef.yaml new file mode 100644 index 0000000..e69de29 diff --git a/examples/rs_research/configs/svcd/fc_siam_conc.yaml b/examples/rs_research/configs/svcd/fc_siam_conc.yaml new file mode 100644 index 0000000..e69de29 diff --git a/examples/rs_research/configs/svcd/fc_siam_diff.yaml b/examples/rs_research/configs/svcd/fc_siam_diff.yaml new file mode 100644 index 0000000..e69de29 diff --git a/examples/rs_research/configs/svcd/snunet.yaml b/examples/rs_research/configs/svcd/snunet.yaml new file mode 100644 index 0000000..e69de29 diff --git a/examples/rs_research/configs/svcd/stanet.yaml b/examples/rs_research/configs/svcd/stanet.yaml new file mode 100644 index 0000000..e69de29 diff --git a/examples/rs_research/configs/svcd/svcd.yaml b/examples/rs_research/configs/svcd/svcd.yaml new file mode 100644 index 0000000..e69de29 diff --git a/examples/rs_research/custom_model.py b/examples/rs_research/custom_model.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/rs_research/custom_trainer.py b/examples/rs_research/custom_trainer.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/rs_research/test.py b/examples/rs_research/test.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/rs_research/train.py b/examples/rs_research/train.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/prepare_dataset/prepare_levircd.py b/tools/prepare_dataset/prepare_levircd.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/prepare_dataset/prepare_svcd.py b/tools/prepare_dataset/prepare_svcd.py new file mode 100644 index 0000000..e69de29 From e637b92c0c81df077868a347d5c062aa9007a3e4 Mon Sep 17 00:00:00 2001 From: Bobholamovic Date: Wed, 17 Aug 2022 21:34:26 +0800 Subject: [PATCH 05/52] Update rs_research example --- docs/data/coco_tools.md | 52 ++++---- examples/README.md | 4 +- examples/rs_research/.gitignore | 2 + examples/rs_research/README.md | 86 +++++++++++- .../configs/levircd/changeformer.yaml | 0 .../rs_research/configs/levircd/snunet.yaml | 0 .../configs/svcd/changeformer.yaml | 0 examples/rs_research/configs/svcd/snunet.yaml | 0 tools/prepare_dataset/common.py | 122 ++++++++++++++++++ tools/prepare_dataset/prepare_levircd.py | 42 ++++++ tools/prepare_dataset/prepare_svcd.py | 31 +++++ 11 files changed, 307 insertions(+), 32 deletions(-) create mode 100644 examples/rs_research/.gitignore delete mode 100644 examples/rs_research/configs/levircd/changeformer.yaml delete mode 100644 examples/rs_research/configs/levircd/snunet.yaml delete mode 100644 examples/rs_research/configs/svcd/changeformer.yaml delete mode 100644 examples/rs_research/configs/svcd/snunet.yaml create mode 100644 tools/prepare_dataset/common.py diff --git a/docs/data/coco_tools.md b/docs/data/coco_tools.md index 3e8f81e..6e2af18 100644 --- a/docs/data/coco_tools.md +++ b/docs/data/coco_tools.md @@ -17,7 +17,7 @@ coco_tools是PaddleRS提供的用于处理COCO格式标注文件的工具集, ## 3 使用示例 -## 3.1 示例数据集 +### 3.1 示例数据集 本文档以COCO 2017数据集作为示例数据进行演示。您可以在以下链接下载该数据集: @@ -47,11 +47,11 @@ coco_tools是PaddleRS提供的用于处理COCO格式标注文件的工具集, | |--... ``` -## 3.2 打印json信息 +### 3.2 打印json信息 使用`json_InfoShow.py`可以打印json文件中的各个键值对的key, 并输出value中排列靠前的元素,从而帮助您快速了解标注信息。对于COCO格式标注数据而言,您应该特别留意`'image'`和`'annotation'`字段的内容。 -### 3.2.1 命令演示 +#### 3.2.1 命令演示 执行如下命令,打印`instances_val2017.json`中的信息: @@ -61,7 +61,7 @@ python ./coco_tools/json_InfoShow.py \ --show_num 5 ``` -### 3.2.2 参数说明 +#### 3.2.2 参数说明 | 参数名 | 含义 | 默认值 | @@ -70,7 +70,7 @@ python ./coco_tools/json_InfoShow.py \ | `--show_num` | (可选)输出value中排列靠前的元素的个数 | `5` | | `--Args_show` | (可选)是否打印输入参数信息 | `True` | -### 3.2.3 结果展示 +#### 3.2.3 结果展示 执行上述命令后,输出结果如下: @@ -151,7 +151,7 @@ contributor : COCO Consortium ``` -### 3.2.4 结果说明 +#### 3.2.4 结果说明 `instances_val2017.json`的key有5个,分别为: @@ -166,11 +166,11 @@ contributor : COCO Consortium - `annotations`键对应的值为列表,共有36781个元素,输出展示了前5个; - `categories`键对应的值为列表,共有80个元素,输出展示了前5个。 -## 3.3 统计图像信息 +### 3.3 统计图像信息 使用`json_ImgSta.py`可以从`instances_val2017.json`中快速提取图像信息,生成csv表格,并生成统计图。 -### 3.3.1 命令演示 +#### 3.3.1 命令演示 执行如下命令,打印`instances_val2017.json`信息: @@ -182,7 +182,7 @@ python ./coco_tools/json_ImgSta.py \ --png_shapeRate_path=./img_sta/images_shapeRate.png ``` -### 3.3.2 参数说明 +#### 3.3.2 参数说明 | 参数名 | 含义 | 默认值 | | ---------------------- | --------------------------------------------------------------------- | -------- | @@ -193,7 +193,7 @@ python ./coco_tools/json_ImgSta.py \ | `--image_keyname` | (可选)json文件中,图像所对应的key |`'images'`| | `--Args_show` | (可选)是否打印输入参数信息 |`True` | -### 3.3.3 结果展示 +#### 3.3.3 结果展示 执行上述命令后,输出结果如下: @@ -232,11 +232,11 @@ csv save to ./img_sta/images.csv 所有图像shape比例(宽/高)的一维分布: ![image.png](./assets/1650011634205-image.png) -## 3.4 统计目标检测标注框信息 +### 3.4 统计目标检测标注框信息 使用`json_AnnoSta.py`,可以从`instances_val2017.json`中快速提取标注信息,生成csv表格,并生成统计图。 -### 3.4.1 命令演示 +#### 3.4.1 命令演示 执行如下命令,打印`instances_val2017.json`信息: @@ -253,7 +253,7 @@ python ./coco_tools/json_AnnoSta.py \ --get_relative=True ``` -### 3.4.2 参数说明 +#### 3.4.2 参数说明 | 参数名 | 含义 | 默认值 | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------- | @@ -270,7 +270,7 @@ python ./coco_tools/json_AnnoSta.py \ | `--anno_keyname` | (可选)json文件中,标注所对应的key | `'annotations'`| | `--Args_show` | (可选)是否打印输入参数信息 | `True` | -### 3.4.3 结果展示 +#### 3.4.3 结果展示 执行上述命令后,输出结果如下: @@ -344,11 +344,11 @@ csv save to ./anno_sta/annos.csv ![image.png](./assets/1650026559309-image.png) -## 3.5 统计图像信息生成json +### 3.5 统计图像信息生成json 使用`json_Test2Json.py`,可以根据`test2017`中的文件信息与训练集json文件快速提取图像信息,生成测试集json文件。 -### 3.5.1 命令演示 +#### 3.5.1 命令演示 执行如下命令,统计并生成`test2017`信息: @@ -359,7 +359,7 @@ python ./coco_tools/json_Img2Json.py \ --json_test_path=./test.json ``` -### 3.5.2 参数说明 +#### 3.5.2 参数说明 | 参数名 | 含义 | 默认值 | @@ -371,7 +371,7 @@ python ./coco_tools/json_Img2Json.py \ | `--cat_keyname` | (可选)json文件中,类别对应的key | `'categories'`| | `--Args_show` | (可选)是否打印输入参数信息 | `True` | -### 3.5.3 结果展示 +#### 3.5.3 结果展示 执行上述命令后,输出结果如下: @@ -431,11 +431,11 @@ json keys: dict_keys(['images', 'categories']) ... ``` -## 3.6 json文件拆分 +### 3.6 json文件拆分 使用`json_Split.py`,可以将`instances_val2017.json`文件拆分为2个子集。 -### 3.6.1 命令演示 +#### 3.6.1 命令演示 执行如下命令,拆分`instances_val2017.json`文件: @@ -446,7 +446,7 @@ python ./coco_tools/json_Split.py \ --json_val_path=./instances_val2017_val.json ``` -### 3.6.2 参数说明 +#### 3.6.2 参数说明 | 参数名 | 含义 | 默认值 | @@ -461,7 +461,7 @@ python ./coco_tools/json_Split.py \ | `--cat_keyname` | (可选)json文件中,类别对应的key | `'categories'`| | `--Args_show` | (可选)是否打印输入参数信息 | `'True'` | -### 3.6.3 结果展示 +#### 3.6.3 结果展示 执行上述命令后,输出结果如下: @@ -485,11 +485,11 @@ image total 5000, train 4500, val 500 anno total 36781, train 33119, val 3662 ``` -## 3.7 json文件合并 +### 3.7 json文件合并 使用`json_Merge.py`,可以合并2个json文件。 -### 3.7.1 命令演示 +#### 3.7.1 命令演示 执行如下命令,合并`instances_train2017.json`与`instances_val2017.json`: @@ -500,7 +500,7 @@ python ./coco_tools/json_Merge.py \ --save_path=./instances_trainval2017.json ``` -### 3.7.2 参数说明 +#### 3.7.2 参数说明 | 参数名 | 含义 | 默认值 | @@ -511,7 +511,7 @@ python ./coco_tools/json_Merge.py \ | `--merge_keys` | (可选)合并过程中需要合并的key | `['images', 'annotations']` | | `--Args_show` | (可选)是否打印输入参数信息 | `True` | -### 3.7.3 结果展示 +#### 3.7.3 结果展示 执行上述命令后,输出结果如下: diff --git a/examples/README.md b/examples/README.md index e6f9acb..41e7598 100644 --- a/examples/README.md +++ b/examples/README.md @@ -2,12 +2,12 @@ PaddleRS提供从科学研究到产业应用的丰富示例,希望帮助遥感领域科研从业者快速完成算法的研发、验证和调优,以及帮助投身于产业实践的开发者便捷地实现从数据预处理到模型部署的全流程遥感深度学习应用。 -## 官方案例 +## 1 官方案例 - [PaddleRS竞赛实战:第十一届中国软件杯遥感赛项](./rs_competition/) - [PaddleRS科研实战:设计深度学习变化检测模型](./rs_research/) -## 社区贡献案例 +## 2 社区贡献案例 [AI Studio](https://aistudio.baidu.com/aistudio/index)是基于百度深度学习平台飞桨的人工智能学习与实训社区,提供在线编程环境、免费GPU算力、海量开源算法和开放数据,帮助开发者快速创建和部署模型。您可以在AI Studio上探索PaddleRS的更多玩法: diff --git a/examples/rs_research/.gitignore b/examples/rs_research/.gitignore new file mode 100644 index 0000000..30ed70a --- /dev/null +++ b/examples/rs_research/.gitignore @@ -0,0 +1,2 @@ +/data/ +/exp/ \ No newline at end of file diff --git a/examples/rs_research/README.md b/examples/rs_research/README.md index 2c7cee4..4501578 100644 --- a/examples/rs_research/README.md +++ b/examples/rs_research/README.md @@ -1,9 +1,87 @@ # PaddleRS科研实战:设计深度学习变化检测模型 -## 环境配置 +本案例演示如何使用PaddleRS设计变化检测模型,并开展消融实验和对比实验。 -## 数据准备 +## 1 环境配置 -## 模型设计与验证 +根据[教程](https://github.com/PaddlePaddle/PaddleRS/tree/develop/tutorials/train#环境准备)安装PaddleRS及相关依赖。在本项目中,GDAL库并不是必需的。 -## 获取对比算法指标 +配置好环境后,在PaddleRS仓库根目录中执行如下指令切换到本案例所在目录: + +```shell +cd examples/rs_research +``` + +## 2 数据准备 + +本案例在[LEVIR-CD数据集](https://www.mdpi.com/2072-4292/12/10/1662)[1]和[synthetic images and real season-varying remote sensing images(SVCD)数据集](https://www.int-arch-photogramm-remote-sens-spatial-inf-sci.net/XLII-2/565/2018/isprs-archives-XLII-2-565-2018.pdf)[2]上开展实验。请在[LEVIR-CD数据集下载链接](https://justchenhao.github.io/LEVIR/)和[SVCD数据集下载链接](https://drive.google.com/file/d/1GX656JqqOyBi_Ef0w65kDGVto-nHrNs9/edit)分别下载这两个数据集,解压至本地目录,并执行如下指令: + +```shell +mkdir data/ +python ../../tools/prepare_dataset/prepare_levircd.py \ + --in_dataset_dir {LEVIR-CD数据集存放目录路径} \ + --out_dataset_dir "data/levircd" \ + --crop_size 256 \ + --crop_stride 256 +python ../../tools/prepare_dataset/prepare_svcd.py \ + --in_dataset_dir {SVCD数据集存放目录路径} \ + --out_dataset_dir "data/svcd" +``` + +以上指令利用PaddleRS提供的数据集准备工具完成数据集切分、file list创建等操作。具体而言,对于LEVIR-CD数据集,使用官方的训练/验证/测试集划分,并将原始的`1024x1024`大小的影像切分为无重叠的`256x256`的小块(参考[3]中的做法);对于SVCD数据集,使用官方的训练/验证/测试集划分,不做其它额外处理。 + +## 3 模型设计与验证 + +### 3.1 问题分析与思路拟定 + +科学研究是为了解决实际问题的,本案例也不例外。本案例的研究动机如下:随着深度学习技术应用的不断深入,变化检测领域涌现了许多。与之相对应的是,模型的参数量也越来越大。 + +[近年来变化检测模型]() + +诚然,。 + +1. 存储开销。 +2. 过拟合。 + +为了解决上述问题,本案例拟提出一种基于网络迭代优化思想的深度学习变化检测算法。本案例的基本思路是,构造一个轻量级的变化检测模型,并以其作为基础迭代单元。每次迭代开始时,由上一次迭代输出的概率图以及原始的输入影像对构造新的输入,实现coarse-to-fine优化。考虑到增加迭代单元的数量将使模型参数量成倍增加,在迭代过程中始终复用同一迭代单元的参数,充分挖掘变化检测网络的拟合能力,迫使其学习到更加有效的特征。这一做法类似[循环神经网络](https://baike.baidu.com/item/%E5%BE%AA%E7%8E%AF%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C/23199490)。根据此思路可以绘制框图如下: + +[思路展示]() + +### 3.2 确定baseline + +科研工作往往需要“站在巨人的肩膀上”,在前人工作的基础上做“增量创新”。因此,对模型设计类工作而言,选用一个合适的baseline网络至关重要。考虑到本案例的出发点是解决,并且使用了。 + +### 3.3 定义新模型 + +[算法整体框图]() + +### 3.4 进行参数分析与消融实验 + +#### 3.4.1 实验设置 + +#### 3.4.2 实验结果 + +### 3.5 开展特征可视化实验 + +## 4 对比实验 + +### 4.1 确定对比算法 + +### 4.2 准备对比算法配置文件 + +### 4.3 实验结果 + +#### 4.3.1 LEVIR-CD数据集上的对比结果 + +#### 4.3.2 SVCD数据集上的对比结果 + +精度、FLOPs、运行时间 + +## 5 总结与展望 + +## 参考文献 + +> [1] Chen, Hao, and Zhenwei Shi. "A spatial-temporal attention-based method and a new dataset for remote sensing image change detection." *Remote Sensing* 12.10 (2020): 1662. +[2] Lebedev, M. A., et al. "CHANGE DETECTION IN REMOTE SENSING IMAGES USING CONDITIONAL ADVERSARIAL NETWORKS." *International Archives of the Photogrammetry, Remote Sensing & Spatial Information Sciences* 42.2 (2018). +[3] Chen, Hao, Zipeng Qi, and Zhenwei Shi. "Remote sensing image change detection with transformers." *IEEE Transactions on Geoscience and Remote Sensing* 60 (2021): 1-14. +[4] Daudt, Rodrigo Caye, Bertr Le Saux, and Alexandre Boulch. "Fully convolutional siamese networks for change detection." *2018 25th IEEE International Conference on Image Processing (ICIP)*. IEEE, 2018. diff --git a/examples/rs_research/configs/levircd/changeformer.yaml b/examples/rs_research/configs/levircd/changeformer.yaml deleted file mode 100644 index e69de29..0000000 diff --git a/examples/rs_research/configs/levircd/snunet.yaml b/examples/rs_research/configs/levircd/snunet.yaml deleted file mode 100644 index e69de29..0000000 diff --git a/examples/rs_research/configs/svcd/changeformer.yaml b/examples/rs_research/configs/svcd/changeformer.yaml deleted file mode 100644 index e69de29..0000000 diff --git a/examples/rs_research/configs/svcd/snunet.yaml b/examples/rs_research/configs/svcd/snunet.yaml deleted file mode 100644 index e69de29..0000000 diff --git a/tools/prepare_dataset/common.py b/tools/prepare_dataset/common.py new file mode 100644 index 0000000..b9e1d82 --- /dev/null +++ b/tools/prepare_dataset/common.py @@ -0,0 +1,122 @@ +import argparse +import os +import os.path as osp +from glob import glob +from itertools import count +from functools import partial +from concurrent.futures import ThreadPoolExecutor + +from skimage.io import imread, imsave +from tqdm import tqdm + + +def get_default_parser(): + parser = argparse.ArgumentParser() + parser.add_argument( + '--in_dataset_dir', + type=str, + required=True, + help="Input dataset directory.") + parser.add_argument( + '--out_dataset_dir', type=str, help="Output dataset directory.") + return parser + + +def add_crop_options(parser): + parser.add_argument( + '--crop_size', type=int, help="Size of cropped patches.") + parser.add_argument( + '--crop_stride', + type=int, + help="Stride of sliding windows when cropping patches. `crop_size` will be used only if `crop_size` is not None.", + ) + return parser + + +def crop_and_save(path, out_subdir, crop_size, stride): + name, ext = osp.splitext(osp.basename(path)) + out_subsubdir = osp.join(out_subdir, name) + if not osp.exists(out_subsubdir): + os.makedirs(out_subsubdir) + img = imread(path) + w, h = img.shape[:2] + counter = count() + for i in range(0, h - crop_size + 1, stride): + for j in range(0, w - crop_size + 1, stride): + imsave( + osp.join(out_subsubdir, '{}_{}{}'.format(name, + next(counter), ext)), + img[i:i + crop_size, j:j + crop_size], + check_contrast=False) + + +def crop_patches(crop_size, + stride, + data_dir, + out_dir, + subsets=('train', 'val', 'test'), + subdirs=('A', 'B', 'label'), + glob_pattern='*', + max_workers=0): + if max_workers < 0: + raise ValueError("`max_workers` must be a non-negative integer!") + + if max_workers == 0: + for subset in subsets: + for subdir in subdirs: + paths = glob( + osp.join(data_dir, subset, subdir, glob_pattern), + recursive=True) + out_subdir = osp.join(out_dir, subset, subdir) + for p in tqdm(paths): + crop_and_save( + p, + out_subdir=out_subdir, + crop_size=crop_size, + stride=stride) + else: + # Concurrently crop image patches + with ThreadPoolExecutor(max_workers=max_workers) as executor: + for subset in subsets: + for subdir in subdirs: + paths = glob( + osp.join(data_dir, subset, subdir, glob_pattern), + recursive=True) + out_subdir = osp.join(out_dir, subset, subdir) + for _ in tqdm( + executor.map(partial( + crop_and_save, + out_subdir=out_subdir, + crop_size=crop_size, + stride=stride), + paths), + total=len(paths)): + pass + + +def get_path_tuples(*dirs, glob_pattern='*', data_dir=None): + all_paths = [] + for dir_ in dirs: + paths = glob(osp.join(dir_, glob_pattern), recursive=True) + paths = sorted(paths) + if data_dir is not None: + paths = [osp.relpath(p, data_dir) for p in paths] + all_paths.append(paths) + all_paths = list(zip(*all_paths)) + return all_paths + + +def create_file_list(file_list, path_tuples, sep=' '): + with open(file_list, 'w') as f: + for tup in path_tuples: + line = sep.join(tup) + f.write(line + '\n') + + +def link_dataset(src, dst): + if osp.exists(dst) and not osp.isdir(dst): + raise ValueError(f"{dst} exists and is not a directory.") + elif not osp.exists(dst): + os.makedirs(dst) + name = osp.basename(osp.normpath(src)) + os.symlink(src, osp.join(dst, name), target_is_directory=True) diff --git a/tools/prepare_dataset/prepare_levircd.py b/tools/prepare_dataset/prepare_levircd.py index e69de29..a09b1d9 100644 --- a/tools/prepare_dataset/prepare_levircd.py +++ b/tools/prepare_dataset/prepare_levircd.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python + +import os.path as osp + +from common import (get_default_parser, add_crop_options, crop_patches, + get_path_tuples, create_file_list, link_dataset) + +SUBSETS = ('train', 'val', 'test') +SUBDIRS = ('A', 'B', 'label') +FILE_LIST_PATTERN = "{subset}.txt" +URL = "" + +if __name__ == '__main__': + parser = get_default_parser() + parser = add_crop_options(parser) + args = parser.parse_args() + + out_dir = osp.join(args.out_dataset_dir, + osp.basename(osp.normpath(args.in_dataset_dir))) + + if args.crop_size is not None: + crop_patches( + args.crop_size, + args.crop_stride, + data_dir=args.in_dataset_dir, + out_dir=out_dir, + subsets=SUBSETS, + subdirs=SUBDIRS, + glob_pattern='*.png', + max_workers=0) + else: + link_dataset(args.in_dataset_dir, args.out_dataset_dir) + + for subset in SUBSETS: + path_tuples = get_path_tuples( + *(osp.join(out_dir, subset, subdir) for subdir in SUBDIRS), + glob_pattern='**/*.png', + data_dir=args.out_dataset_dir) + file_list = osp.join( + args.out_dataset_dir, FILE_LIST_PATTERN.format(subset=subset)) + create_file_list(file_list, path_tuples) + print(f"Write file list to {file_list}.") diff --git a/tools/prepare_dataset/prepare_svcd.py b/tools/prepare_dataset/prepare_svcd.py index e69de29..5351a90 100644 --- a/tools/prepare_dataset/prepare_svcd.py +++ b/tools/prepare_dataset/prepare_svcd.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python + +import os.path as osp + +from common import (get_default_parser, get_path_tuples, create_file_list, + link_dataset) + +SUBSETS = ('train', 'val', 'test') +SUBDIRS = ('A', 'B', 'OUT') +FILE_LIST_PATTERN = "{subset}.txt" +URL = "" + +if __name__ == '__main__': + parser = get_default_parser() + args = parser.parse_args() + + out_dir = osp.join(args.out_dataset_dir, + osp.basename(osp.normpath(args.in_dataset_dir))) + + link_dataset(args.in_dataset_dir, args.out_dataset_dir) + + for subset in SUBSETS: + # NOTE: Only use cropped real samples. + path_tuples = get_path_tuples( + *(osp.join(out_dir, 'Real', 'subset', subset, subdir) + for subdir in SUBDIRS), + data_dir=args.out_dataset_dir) + file_list = osp.join( + args.out_dataset_dir, FILE_LIST_PATTERN.format(subset=subset)) + create_file_list(file_list, path_tuples) + print(f"Write file list to {file_list}.") From 4b84fcbebd0f2353b31c8eae7d43d28b7493dcbc Mon Sep 17 00:00:00 2001 From: Bobholamovic Date: Thu, 18 Aug 2022 02:08:43 +0800 Subject: [PATCH 06/52] Curate community cases --- examples/README.md | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/examples/README.md b/examples/README.md index 41e7598..14d3864 100644 --- a/examples/README.md +++ b/examples/README.md @@ -4,7 +4,6 @@ PaddleRS提供从科学研究到产业应用的丰富示例,希望帮助遥感 ## 1 官方案例 -- [PaddleRS竞赛实战:第十一届中国软件杯遥感赛项](./rs_competition/) - [PaddleRS科研实战:设计深度学习变化检测模型](./rs_research/) ## 2 社区贡献案例 @@ -15,12 +14,20 @@ PaddleRS提供从科学研究到产业应用的丰富示例,希望帮助遥感 本文档收集了部分由开源爱好者贡献的精品项目: -- [手把手教你PaddleRS实现变化检测](https://aistudio.baidu.com/aistudio/projectdetail/3737991) -- [【PPSIG】PaddleRS变化检测模型部署:以BIT为例](https://aistudio.baidu.com/aistudio/projectdetail/4184759) -- [PaddleRS:使用超分模型提高真实的低分辨率无人机影像的分割精度](https://aistudio.baidu.com/aistudio/projectdetail/3696814) -- [PaddleRS:无人机汽车识别](https://aistudio.baidu.com/aistudio/projectdetail/3713122) -- [PaddleRS:高光谱卫星影像场景分类](https://aistudio.baidu.com/aistudio/projectdetail/3711240) -- [PaddleRS:利用卫星影像与数字高程模型进行滑坡识别](https://aistudio.baidu.com/aistudio/projectdetail/4066570) -- [【PPSIG】PaddleRS实现遥感影像场景分类](https://aistudio.baidu.com/aistudio/projectdetail/4198965) -- [为PaddleRS添加一个袖珍配置系统](https://aistudio.baidu.com/aistudio/projectdetail/4203534) -- [万丈高楼平地起 基于PaddleGAN与PaddleRS的建筑物生成](https://aistudio.baidu.com/aistudio/projectdetail/3716885) +|项目链接|项目作者|项目类型|关键词| +|-|-|-|-| +|[手把手教你PaddleRS实现变化检测](https://aistudio.baidu.com/aistudio/projectdetail/3737991)|奔向未来的样子|入门教程|变化检测| +|[【PPSIG】PaddleRS变化检测模型部署:以BIT为例](https://aistudio.baidu.com/aistudio/projectdetail/4184759)|古代飞|入门教程|变化检测,模型部署| +|[【PPSIG】PaddleRS实现遥感影像场景分类](https://aistudio.baidu.com/aistudio/projectdetail/4198965)|古代飞|入门教程|场景分类| +|[PaddleRS:使用超分模型提高真实的低分辨率无人机影像的分割精度](https://aistudio.baidu.com/aistudio/projectdetail/3696814)|KeyK-小胡之父|应用案例|超分辨率重建,无人机影像| +|[PaddleRS:无人机汽车识别](https://aistudio.baidu.com/aistudio/projectdetail/3713122)|geoyee|应用案例|目标检测,无人机影像| +|[PaddleRS:高光谱卫星影像场景分类](https://aistudio.baidu.com/aistudio/projectdetail/3711240)|geoyee|应用案例|场景分类,高光谱影像| +|[PaddleRS:利用卫星影像与数字高程模型进行滑坡识别](https://aistudio.baidu.com/aistudio/projectdetail/4066570)|KeyK-小胡之父|应用案例|图像分割,DEM| +|[为PaddleRS添加一个袖珍配置系统](https://aistudio.baidu.com/aistudio/projectdetail/4203534)|古代飞|创意开发|| +|[万丈高楼平地起 基于PaddleGAN与PaddleRS的建筑物生成](https://aistudio.baidu.com/aistudio/projectdetail/3716885)|奔向未来的样子|创意开发|超分辨率重建| +|[【官方】第十一届 “中国软件杯”百度遥感赛项:变化检测功能](https://aistudio.baidu.com/aistudio/projectdetail/3684588)|古代飞|竞赛打榜|变化检测,比赛基线| +|[【官方】第十一届 “中国软件杯”百度遥感赛项:目标提取功能](https://aistudio.baidu.com/aistudio/projectdetail/3792610)|古代飞|竞赛打榜|图像分割,比赛基线| +|[【官方】第十一届 “中国软件杯”百度遥感赛项:地物分类功能](https://aistudio.baidu.com/aistudio/projectdetail/3792606)|古代飞|竞赛打榜|图像分割,比赛基线| +|[【官方】第十一届 “中国软件杯”百度遥感赛项:目标检测功能](https://aistudio.baidu.com/aistudio/projectdetail/3792609)|古代飞|竞赛打榜|目标检测,比赛基线| +|[【十一届软件杯】遥感解译赛道:变化检测任务——预赛第四名方案分享](https://aistudio.baidu.com/aistudio/projectdetail/4116895)|lzzzzzm|竞赛打榜|变化检测,高分方案| +|[【方案分享】第十一届 “中国软件杯”大学生软件设计大赛遥感解译赛道 比赛方案分享](https://aistudio.baidu.com/aistudio/projectdetail/4146154)|trainer|竞赛打榜|变化检测,高分方案| From b174b99ada3a8eb97e583490780e270f99bcc711 Mon Sep 17 00:00:00 2001 From: Bobholamovic Date: Thu, 18 Aug 2022 02:09:12 +0800 Subject: [PATCH 07/52] Remove examples/rs_competition --- examples/rs_competition/README.md | 11 ----------- examples/rs_competition/main.ipynb | 0 2 files changed, 11 deletions(-) delete mode 100644 examples/rs_competition/README.md delete mode 100644 examples/rs_competition/main.ipynb diff --git a/examples/rs_competition/README.md b/examples/rs_competition/README.md deleted file mode 100644 index 05371d2..0000000 --- a/examples/rs_competition/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# PaddleRS竞赛实战:第十一届中国软件杯遥感赛项 - -## 环境配置 - -## 数据准备 - -## 模型训练、评估与推理 - -请参考[main.ipynb](./main.ipynb)中的内容。 - -## 结果导出 diff --git a/examples/rs_competition/main.ipynb b/examples/rs_competition/main.ipynb deleted file mode 100644 index e69de29..0000000 From 0ac304641b386137dfdd897209f4115e8f51f654 Mon Sep 17 00:00:00 2001 From: Bobholamovic Date: Thu, 18 Aug 2022 02:09:55 +0800 Subject: [PATCH 08/52] Fix typos and indents --- paddlers/tasks/change_detector.py | 6 ++++-- paddlers/transforms/operators.py | 6 +++--- test_tipc/config_utils.py | 5 +++-- test_tipc/configs/cd/bit/bit.yaml | 2 +- test_tipc/configs/cd/changeformer/changeformer.yaml | 2 +- test_tipc/configs/clas/_base_/ucmerced.yaml | 2 +- test_tipc/configs/clas/hrnet/hrnet.yaml | 6 +++--- test_tipc/configs/det/ppyolo/ppyolo.yaml | 6 +++--- 8 files changed, 19 insertions(+), 16 deletions(-) diff --git a/paddlers/tasks/change_detector.py b/paddlers/tasks/change_detector.py index 9d631e6..b3fa32c 100644 --- a/paddlers/tasks/change_detector.py +++ b/paddlers/tasks/change_detector.py @@ -1066,11 +1066,12 @@ class ChangeStar(BaseChangeDetector): class ChangeFormer(BaseChangeDetector): def __init__(self, - in_channels=3, num_classes=2, + use_mixed_loss=False, + losses=None, + in_channels=3, decoder_softmax=False, embed_dim=256, - use_mixed_loss=False, **params): params.update({ 'in_channels': in_channels, @@ -1081,4 +1082,5 @@ class ChangeFormer(BaseChangeDetector): model_name='ChangeFormer', num_classes=num_classes, use_mixed_loss=use_mixed_loss, + losses=losses, **params) diff --git a/paddlers/transforms/operators.py b/paddlers/transforms/operators.py index ec9b424..c7603b1 100644 --- a/paddlers/transforms/operators.py +++ b/paddlers/transforms/operators.py @@ -616,10 +616,10 @@ class RandomFlipOrRotate(Transform): probs (list[float]): Probabilities of performing flipping and rotation. Default: [0.35,0.25]. probsf (list[float]): Probabilities of 5 flipping modes (horizontal, - vertical, both horizontal diction and vertical, diagonal, - anti-diagonal). Default: [0.3, 0.3, 0.2, 0.1, 0.1]. + vertical, both horizontal and vertical, diagonal, anti-diagonal). + Default: [0.3, 0.3, 0.2, 0.1, 0.1]. probsr (list[float]): Probabilities of 3 rotation modes (90°, 180°, 270° - clockwise). Default: [0.25,0.5,0.25]. + clockwise). Default: [0.25, 0.5, 0.25]. Examples: diff --git a/test_tipc/config_utils.py b/test_tipc/config_utils.py index fa137f8..9f1b6fc 100644 --- a/test_tipc/config_utils.py +++ b/test_tipc/config_utils.py @@ -152,6 +152,7 @@ def parse_args(*args, **kwargs): if osp.exists(cfg_path): cfg = parse_configs(cfg_path, inherit_on) parser, node_keys = _cfg2args(cfg, parser, '') + node_keys = sorted(node_keys, reverse=True) args = parser.parse_args(*args, **kwargs) return _args2cfg(dict(), args, node_keys) elif cfg_path != '': @@ -178,7 +179,7 @@ class CfgNode(yaml.YAMLObject, metaclass=_CfgNodeMeta): super().__init__() self.type = dict_['type'] self.args = dict_.get('args', []) - self.module = self._get_module(dict_.get('module', '')) + self.module = dict_.get('module', '') @classmethod def set_context(cls, ctx): @@ -189,7 +190,7 @@ class CfgNode(yaml.YAMLObject, metaclass=_CfgNodeMeta): def build_object(self, mod=None): if mod is None: - mod = self.module + mod = self._get_module(self.module) cls = getattr(mod, self.type) if isinstance(self.args, list): args = build_objects(self.args) diff --git a/test_tipc/configs/cd/bit/bit.yaml b/test_tipc/configs/cd/bit/bit.yaml index 735a318..3d3c62b 100644 --- a/test_tipc/configs/cd/bit/bit.yaml +++ b/test_tipc/configs/cd/bit/bit.yaml @@ -5,4 +5,4 @@ _base_: ../_base_/airchange.yaml save_dir: ./test_tipc/output/cd/bit/ model: !Node - type: BIT \ No newline at end of file + type: BIT \ No newline at end of file diff --git a/test_tipc/configs/cd/changeformer/changeformer.yaml b/test_tipc/configs/cd/changeformer/changeformer.yaml index 072678b..785749d 100644 --- a/test_tipc/configs/cd/changeformer/changeformer.yaml +++ b/test_tipc/configs/cd/changeformer/changeformer.yaml @@ -5,4 +5,4 @@ _base_: ../_base_/airchange.yaml save_dir: ./test_tipc/output/cd/changeformer/ model: !Node - type: ChangeFormer \ No newline at end of file + type: ChangeFormer \ No newline at end of file diff --git a/test_tipc/configs/clas/_base_/ucmerced.yaml b/test_tipc/configs/clas/_base_/ucmerced.yaml index 424f45e..bff0b8f 100644 --- a/test_tipc/configs/clas/_base_/ucmerced.yaml +++ b/test_tipc/configs/clas/_base_/ucmerced.yaml @@ -64,7 +64,7 @@ num_epochs: 2 train_batch_size: 16 save_interval_epochs: 5 log_interval_steps: 50 -save_dir: e./test_tipc/output/clas/ +save_dir: ./test_tipc/output/clas/ learning_rate: 0.01 early_stop: False early_stop_patience: 5 diff --git a/test_tipc/configs/clas/hrnet/hrnet.yaml b/test_tipc/configs/clas/hrnet/hrnet.yaml index 2f0a000..f402c26 100644 --- a/test_tipc/configs/clas/hrnet/hrnet.yaml +++ b/test_tipc/configs/clas/hrnet/hrnet.yaml @@ -5,6 +5,6 @@ _base_: ../_base_/ucmerced.yaml save_dir: ./test_tipc/output/clas/hrnet/ model: !Node - type: HRNet_W18_C - args: - num_classes: 21 \ No newline at end of file + type: HRNet_W18_C + args: + num_classes: 21 \ No newline at end of file diff --git a/test_tipc/configs/det/ppyolo/ppyolo.yaml b/test_tipc/configs/det/ppyolo/ppyolo.yaml index 6d9ef3d..f36919c 100644 --- a/test_tipc/configs/det/ppyolo/ppyolo.yaml +++ b/test_tipc/configs/det/ppyolo/ppyolo.yaml @@ -5,6 +5,6 @@ _base_: ../_base_/sarship.yaml save_dir: ./test_tipc/output/det/ppyolo/ model: !Node - type: PPYOLO - args: - num_classes: 1 \ No newline at end of file + type: PPYOLO + args: + num_classes: 1 \ No newline at end of file From 5f353e6c51362b42a2f38c409cbb496463ca1817 Mon Sep 17 00:00:00 2001 From: Bobholamovic Date: Thu, 18 Aug 2022 02:10:33 +0800 Subject: [PATCH 09/52] Update examples/rs_research --- examples/rs_research/README.md | 134 +++++++++- examples/rs_research/attach_tools.py | 20 ++ examples/rs_research/config_utils.py | 253 ++++++++++++++++++ examples/rs_research/configs/levircd/bit.yaml | 6 + .../iterative_bit_iter2_gamma01.yaml | 12 + .../iterative_bit_iter2_gamma02.yaml | 12 + .../iterative_bit_iter2_gamma05.yaml | 12 + .../iterative_bit_iter3_gamma01.yaml | 12 + .../iterative_bit_iter3_gamma02.yaml | 12 + .../iterative_bit_iter3_gamma05.yaml | 12 + .../iterative_bit_iter3_gamma10.yaml | 12 + .../rs_research/configs/levircd/levircd.yaml | 74 +++++ examples/rs_research/custom_model.py | 58 ++++ examples/rs_research/custom_trainer.py | 29 ++ examples/rs_research/params_versus_f1.png | Bin 0 -> 48900 bytes examples/rs_research/run_task.py | 115 ++++++++ .../run_benchmark.sh} | 0 .../run_parameter_analysis.sh} | 0 examples/rs_research/train.py | 0 test_tipc/configs/seg/unet/unet.yaml | 8 +- 20 files changed, 765 insertions(+), 16 deletions(-) create mode 100644 examples/rs_research/attach_tools.py create mode 100644 examples/rs_research/config_utils.py create mode 100644 examples/rs_research/configs/levircd/custom_model/iterative_bit_iter2_gamma01.yaml create mode 100644 examples/rs_research/configs/levircd/custom_model/iterative_bit_iter2_gamma02.yaml create mode 100644 examples/rs_research/configs/levircd/custom_model/iterative_bit_iter2_gamma05.yaml create mode 100644 examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma01.yaml create mode 100644 examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma02.yaml create mode 100644 examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma05.yaml create mode 100644 examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma10.yaml create mode 100644 examples/rs_research/params_versus_f1.png create mode 100644 examples/rs_research/run_task.py rename examples/rs_research/{configs/levircd/custom_model.yaml => scripts/run_benchmark.sh} (100%) rename examples/rs_research/{test.py => scripts/run_parameter_analysis.sh} (100%) delete mode 100644 examples/rs_research/train.py diff --git a/examples/rs_research/README.md b/examples/rs_research/README.md index 4501578..77ae5cf 100644 --- a/examples/rs_research/README.md +++ b/examples/rs_research/README.md @@ -34,32 +34,136 @@ python ../../tools/prepare_dataset/prepare_svcd.py \ ### 3.1 问题分析与思路拟定 -科学研究是为了解决实际问题的,本案例也不例外。本案例的研究动机如下:随着深度学习技术应用的不断深入,变化检测领域涌现了许多。与之相对应的是,模型的参数量也越来越大。 +随着深度学习技术应用的不断深入,近年来,变化检测领域涌现了许多基于全卷积神经网络(fully convolutional network, FCN)的遥感影像变化检测算法。与基于特征和基于影像块的方法相比,基于FCN的方法具有处理效率高、依赖超参数少等优势,但其缺点在于参数量往往较大,因而对训练样本的数量更为依赖。尽管中、大型变化检测数据集的数量与日俱增,训练样本日益丰富,但深度学习变化检测模型的参数量也越来越大。下图显示了从2018年到2021年一些已发表的文献中提出的基于FCN的变化检测模型的参数量与其在SVCD数据集上取得的F1分数(柱状图中bar的高度与模型参数量成正比): -[近年来变化检测模型]() +![params_versus_f1](params_versus_f1.png) -诚然,。 +诚然,增大参数数量在大多数情况下等同于增加模型容量,而模型容量的增加意味着模型拟合能力的提升,从而有助于模型在实验数据集上取得更高的精度指标。但是,“更大”一定意味着“更好”吗?答案显然是否定的。在实际应用中,“更大”的遥感影像变化检测模型常常遭遇如下问题: -1. 存储开销。 -2. 过拟合。 +1. 巨大的参数量意味着巨大的存储开销。在许多实际场景中,硬件资源往往是有限的,过多的模型参数将给部署造成困难。 +2. 在数据有限的情况下,大模型更易遭受过拟合,其在实验数据集上看起来良好的结果也难以泛化到真实场景。 -为了解决上述问题,本案例拟提出一种基于网络迭代优化思想的深度学习变化检测算法。本案例的基本思路是,构造一个轻量级的变化检测模型,并以其作为基础迭代单元。每次迭代开始时,由上一次迭代输出的概率图以及原始的输入影像对构造新的输入,实现coarse-to-fine优化。考虑到增加迭代单元的数量将使模型参数量成倍增加,在迭代过程中始终复用同一迭代单元的参数,充分挖掘变化检测网络的拟合能力,迫使其学习到更加有效的特征。这一做法类似[循环神经网络](https://baike.baidu.com/item/%E5%BE%AA%E7%8E%AF%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C/23199490)。根据此思路可以绘制框图如下: +本案例认为,上述问题的根源在于参数量与数据量的失衡所导致的特征冗余。既然模型的特征存在冗余,是否存在某种手段,能够在固定模型参数量的前提下对特征进行优化,从而“榨取”小模型的更多潜力?基于这个观点,本案例的基本思路是设计一种基于网络迭代优化思想的深度学习变化检测算法。首先,构造一个轻量级的变化检测模型,并以其作为基础迭代单元。在每次迭代开始时,由上一次迭代输出的概率图以及原始的输入影像对构造新的输入,如此逐级实现coarse-to-fine优化。考虑到增加迭代单元的数量将使模型参数量成倍增加,在迭代过程中应始终复用同一迭代单元的参数以充分挖掘变化检测网络的拟合能力,迫使其学习到更加有效的特征。这一做法类似[循环神经网络](https://baike.baidu.com/item/%E5%BE%AA%E7%8E%AF%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C/23199490)。根据此思路可以绘制框图如下: -[思路展示]() +![draft](draft.png) -### 3.2 确定baseline +### 3.2 确定baseline模型 -科研工作往往需要“站在巨人的肩膀上”,在前人工作的基础上做“增量创新”。因此,对模型设计类工作而言,选用一个合适的baseline网络至关重要。考虑到本案例的出发点是解决,并且使用了。 +科研工作往往需要“站在巨人的肩膀上”,在前人工作的基础上做“增量创新”。因此,对模型设计类工作而言,选用一个合适的baseline模型至关重要。考虑到本案例的出发点是解决现有模型参数量过大、冗余特征过多的问题,并且在拟定的解决方案中使用到了循环结构,用作baseline的网络结构必须足够轻量和高效(因为最直接的思路是使用baseline作为基础迭代单元)。为此,本案例选用Bitemporal Image Transformer(BIT)作为baseline。BIT是一个轻量级的深度学习变化检测模型,其基本结构如图所示: + +![bit](bit.png) + +BIT的核心思想在于, ### 3.3 定义新模型 -[算法整体框图]() +确定了基本思路和baseline模型之后,可以绘制如下的算法整体框图: + +![framework](framework.png) + +依据此框图,即可在。 + +#### 3.3.1 自定义模型组网 + +在`custom_model.py`中定义模型的宏观(macro)结构以及组成模型的各个微观(micro)模块。例如,当前`custom_model.py`中定义了迭代版本的BIT模型`IterativeBIT`: +```python +@attach +class IterativeBIT(nn.Layer): + def __init__(self, num_iters=1, gamma=0.1, num_classes=2, bit_kwargs=None): + super().__init__() + + if num_iters <= 0: + raise ValueError(f"`num_iters` should have positive value, but got {num_iters}.") + + self.num_iters = num_iters + self.gamma = gamma + + if bit_kwargs is None: + bit_kwargs = dict() + + if 'num_classes' in bit_kwargs: + raise KeyError("'num_classes' should not be set in `bit_kwargs`.") + bit_kwargs['num_classes'] = num_classes + + self.bit = BIT(**bit_kwargs) + + def forward(self, t1, t2): + rate_map = self._init_rate_map(t1.shape) + + for it in range(self.num_iters): + # Construct inputs + x1 = self._constr_iter_input(t1, rate_map) + x2 = self._constr_iter_input(t2, rate_map) + # Get logits + logits_list = self.bit(x1, x2) + # Construct rate map + prob_map = F.softmax(logits_list[0], axis=1) + rate_map = self._constr_rate_map(prob_map) + + return logits_list + ... +``` + +在编写组网相关代码时请注意以下两点: + +1. 所有模型必须为`paddle.nn.Layer`的子类; +2. 包含模型整体逻辑结构的最外层模块须用`@attach`装饰; +3. 对于变化检测任务,`forward()`方法除`self`参数外还接受两个参数`t1`、`t2`,分别表示第一时相和第二时相影像。 + +关于模型定义的更多细节请参考[API文档]()。 + +#### 3.3.2 自定义训练器 + +在`custom_trainer.py`中定义训练器。例如,当前`custom_trainer.py`中定义了与`IterativeBIT`模型对应的训练器: +```python +@attach +class IterativeBIT(BaseChangeDetector): + def __init__(self, + num_classes=2, + use_mixed_loss=False, + losses=None, + num_iters=1, + gamma=0.1, + bit_kwargs=None, + **params): + params.update({ + 'num_iters': num_iters, + 'gamma': gamma, + 'bit_kwargs': bit_kwargs + }) + super().__init__( + model_name='IterativeBIT', + num_classes=num_classes, + use_mixed_loss=use_mixed_loss, + losses=losses, + **params) +``` + +在编写训练器定义相关代码时请注意以下两点: + +1. 对于变化检测任务,训练器必须为`paddlers.tasks.cd.BaseChangeDetector`的子类; +2. 与模型一样,训练器也须用`@attach`装饰; +3. 训练器和模型可以同名。 + +关于训练器定义的更多细节请参考[API文档]()。 ### 3.4 进行参数分析与消融实验 #### 3.4.1 实验设置 -#### 3.4.2 实验结果 +#### 3.4.2 编写配置文件 + +#### 3.4.3 实验结果 + +### 3.5 \*Magic Behind + +本小节涉及技术细节,对于本案例来说属于进阶内容,您可以选择性了解。 + +#### 3.5.1 延迟属性绑定 + +PaddleRS提供了,只需要。`attach_tools.Attach`对象自动。 + +#### 3.5.2 非侵入式轻量级配置系统 ### 3.5 开展特征可视化实验 @@ -75,10 +179,16 @@ python ../../tools/prepare_dataset/prepare_svcd.py \ #### 4.3.2 SVCD数据集上的对比结果 -精度、FLOPs、运行时间 +精度 ## 5 总结与展望 +### 5.1 总结 + +### 5.2 展望 + +耗时,模型大小,FLOPs + ## 参考文献 > [1] Chen, Hao, and Zhenwei Shi. "A spatial-temporal attention-based method and a new dataset for remote sensing image change detection." *Remote Sensing* 12.10 (2020): 1662. diff --git a/examples/rs_research/attach_tools.py b/examples/rs_research/attach_tools.py new file mode 100644 index 0000000..3b737cf --- /dev/null +++ b/examples/rs_research/attach_tools.py @@ -0,0 +1,20 @@ +class Attach(object): + def __init__(self, dst): + self.dst = dst + + def __call__(self, obj, name=None): + if name is None: + # Automatically get names of functions and classes + name = obj.__name__ + if hasattr(self.dst, name): + raise RuntimeError( + f"{self.dst} already has the attribute {name}, which is {getattr(self.dst, name)}." + ) + setattr(self.dst, name, obj) + if hasattr(self.dst, '__all__'): + self.dst.__all__.append(name) + return obj + + @staticmethod + def to(dst): + return Attach(dst) diff --git a/examples/rs_research/config_utils.py b/examples/rs_research/config_utils.py new file mode 100644 index 0000000..9f1b6fc --- /dev/null +++ b/examples/rs_research/config_utils.py @@ -0,0 +1,253 @@ +#!/usr/bin/env python + +import argparse +import os.path as osp +from collections.abc import Mapping + +import yaml + + +def _chain_maps(*maps): + chained = dict() + keys = set().union(*maps) + for key in keys: + vals = [m[key] for m in maps if key in m] + if isinstance(vals[0], Mapping): + chained[key] = _chain_maps(*vals) + else: + chained[key] = vals[0] + return chained + + +def read_config(config_path): + with open(config_path, 'r', encoding='utf-8') as f: + cfg = yaml.safe_load(f) + return cfg or {} + + +def parse_configs(cfg_path, inherit=True): + if inherit: + cfgs = [] + cfgs.append(read_config(cfg_path)) + while cfgs[-1].get('_base_'): + base_path = cfgs[-1].pop('_base_') + curr_dir = osp.dirname(cfg_path) + cfgs.append( + read_config(osp.normpath(osp.join(curr_dir, base_path)))) + return _chain_maps(*cfgs) + else: + return read_config(cfg_path) + + +def _cfg2args(cfg, parser, prefix=''): + node_keys = set() + for k, v in cfg.items(): + opt = prefix + k + if isinstance(v, list): + if len(v) == 0: + parser.add_argument( + '--' + opt, type=object, nargs='*', default=v) + else: + # Only apply to homogeneous lists + if isinstance(v[0], CfgNode): + node_keys.add(opt) + parser.add_argument( + '--' + opt, type=type(v[0]), nargs='*', default=v) + elif isinstance(v, dict): + # Recursively parse a dict + _, new_node_keys = _cfg2args(v, parser, opt + '.') + node_keys.update(new_node_keys) + elif isinstance(v, CfgNode): + node_keys.add(opt) + _, new_node_keys = _cfg2args(v.to_dict(), parser, opt + '.') + node_keys.update(new_node_keys) + elif isinstance(v, bool): + parser.add_argument('--' + opt, action='store_true', default=v) + else: + parser.add_argument('--' + opt, type=type(v), default=v) + return parser, node_keys + + +def _args2cfg(cfg, args, node_keys): + args = vars(args) + for k, v in args.items(): + pos = k.find('.') + if pos != -1: + # Iteratively parse a dict + dict_ = cfg + while pos != -1: + dict_.setdefault(k[:pos], {}) + dict_ = dict_[k[:pos]] + k = k[pos + 1:] + pos = k.find('.') + dict_[k] = v + else: + cfg[k] = v + + for k in node_keys: + pos = k.find('.') + if pos != -1: + # Iteratively parse a dict + dict_ = cfg + while pos != -1: + dict_.setdefault(k[:pos], {}) + dict_ = dict_[k[:pos]] + k = k[pos + 1:] + pos = k.find('.') + v = dict_[k] + dict_[k] = [CfgNode(v_) for v_ in v] if isinstance( + v, list) else CfgNode(v) + else: + v = cfg[k] + cfg[k] = [CfgNode(v_) for v_ in v] if isinstance( + v, list) else CfgNode(v) + + return cfg + + +def parse_args(*args, **kwargs): + cfg_parser = argparse.ArgumentParser(add_help=False) + cfg_parser.add_argument('--config', type=str, default='') + cfg_parser.add_argument('--inherit_off', action='store_true') + cfg_args = cfg_parser.parse_known_args()[0] + cfg_path = cfg_args.config + inherit_on = not cfg_args.inherit_off + + # Main parser + parser = argparse.ArgumentParser( + conflict_handler='resolve', parents=[cfg_parser]) + # Global settings + parser.add_argument('cmd', choices=['train', 'eval']) + parser.add_argument('task', choices=['cd', 'clas', 'det', 'seg']) + + # Data + parser.add_argument('--datasets', type=dict, default={}) + parser.add_argument('--transforms', type=dict, default={}) + parser.add_argument('--download_on', action='store_true') + parser.add_argument('--download_url', type=str, default='') + parser.add_argument('--download_path', type=str, default='./') + + # Optimizer + parser.add_argument('--optimizer', type=dict, default={}) + + # Training related + parser.add_argument('--num_epochs', type=int, default=100) + parser.add_argument('--train_batch_size', type=int, default=8) + parser.add_argument('--save_interval_epochs', type=int, default=1) + parser.add_argument('--log_interval_steps', type=int, default=1) + parser.add_argument('--save_dir', default='../exp/') + parser.add_argument('--learning_rate', type=float, default=0.01) + parser.add_argument('--early_stop', action='store_true') + parser.add_argument('--early_stop_patience', type=int, default=5) + parser.add_argument('--use_vdl', action='store_true') + parser.add_argument('--resume_checkpoint', type=str) + parser.add_argument('--train', type=dict, default={}) + + # Loss + parser.add_argument('--losses', type=dict, nargs='+', default={}) + + # Model + parser.add_argument('--model', type=dict, default={}) + + if osp.exists(cfg_path): + cfg = parse_configs(cfg_path, inherit_on) + parser, node_keys = _cfg2args(cfg, parser, '') + node_keys = sorted(node_keys, reverse=True) + args = parser.parse_args(*args, **kwargs) + return _args2cfg(dict(), args, node_keys) + elif cfg_path != '': + raise FileNotFoundError + else: + args = parser.parse_args() + return _args2cfg(dict(), args, set()) + + +class _CfgNodeMeta(yaml.YAMLObjectMetaclass): + def __call__(cls, obj): + if isinstance(obj, CfgNode): + return obj + return super(_CfgNodeMeta, cls).__call__(obj) + + +class CfgNode(yaml.YAMLObject, metaclass=_CfgNodeMeta): + yaml_tag = u'!Node' + yaml_loader = yaml.SafeLoader + # By default use a lexical scope + ctx = globals() + + def __init__(self, dict_): + super().__init__() + self.type = dict_['type'] + self.args = dict_.get('args', []) + self.module = dict_.get('module', '') + + @classmethod + def set_context(cls, ctx): + # TODO: Implement dynamic scope with inspect.stack() + old_ctx = cls.ctx + cls.ctx = ctx + return old_ctx + + def build_object(self, mod=None): + if mod is None: + mod = self._get_module(self.module) + cls = getattr(mod, self.type) + if isinstance(self.args, list): + args = build_objects(self.args) + obj = cls(*args) + elif isinstance(self.args, dict): + args = build_objects(self.args) + obj = cls(**args) + else: + raise NotImplementedError + return obj + + def _get_module(self, s): + mod = None + while s: + idx = s.find('.') + if idx == -1: + next_ = s + s = '' + else: + next_ = s[:idx] + s = s[idx + 1:] + if mod is None: + mod = self.ctx[next_] + else: + mod = getattr(mod, next_) + return mod + + @staticmethod + def build_objects(cfg, mod=None): + if isinstance(cfg, list): + return [CfgNode.build_objects(c, mod=mod) for c in cfg] + elif isinstance(cfg, CfgNode): + return cfg.build_object(mod=mod) + elif isinstance(cfg, dict): + return { + k: CfgNode.build_objects( + v, mod=mod) + for k, v in cfg.items() + } + else: + return cfg + + def __repr__(self): + return f"(type={self.type}, args={self.args}, module={self.module or ' '})" + + @classmethod + def from_yaml(cls, loader, node): + map_ = loader.construct_mapping(node) + return cls(map_) + + def items(self): + yield from [('type', self.type), ('args', self.args), ('module', + self.module)] + + def to_dict(self): + return dict(self.items()) + + +def build_objects(cfg, mod=None): + return CfgNode.build_objects(cfg, mod=mod) diff --git a/examples/rs_research/configs/levircd/bit.yaml b/examples/rs_research/configs/levircd/bit.yaml index e69de29..f63b0c3 100644 --- a/examples/rs_research/configs/levircd/bit.yaml +++ b/examples/rs_research/configs/levircd/bit.yaml @@ -0,0 +1,6 @@ +_base_: ./levircd.yaml + +save_dir: ./exp/bit/ + +model: !Node + type: BIT diff --git a/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter2_gamma01.yaml b/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter2_gamma01.yaml new file mode 100644 index 0000000..0526e94 --- /dev/null +++ b/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter2_gamma01.yaml @@ -0,0 +1,12 @@ +_base_: ../levircd.yaml + +save_dir: ./exp/custom_model/iter2_gamma01/ + +model: !Node + type: IterativeBIT + args: + num_iters: 2 + gamma: 0.1 + num_classes: 2 + bit_kwargs: + in_channels: 4 diff --git a/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter2_gamma02.yaml b/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter2_gamma02.yaml new file mode 100644 index 0000000..c139498 --- /dev/null +++ b/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter2_gamma02.yaml @@ -0,0 +1,12 @@ +_base_: ../levircd.yaml + +save_dir: ./exp/custom_model/iter2_gamma02/ + +model: !Node + type: IterativeBIT + args: + num_iters: 2 + gamma: 0.2 + num_classes: 2 + bit_kwargs: + in_channels: 4 diff --git a/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter2_gamma05.yaml b/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter2_gamma05.yaml new file mode 100644 index 0000000..54e87fd --- /dev/null +++ b/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter2_gamma05.yaml @@ -0,0 +1,12 @@ +_base_: ../levircd.yaml + +save_dir: ./exp/custom_model/iter2_gamma05/ + +model: !Node + type: IterativeBIT + args: + num_iters: 2 + gamma: 0.5 + num_classes: 2 + bit_kwargs: + in_channels: 4 diff --git a/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma01.yaml b/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma01.yaml new file mode 100644 index 0000000..7369ff0 --- /dev/null +++ b/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma01.yaml @@ -0,0 +1,12 @@ +_base_: ../levircd.yaml + +save_dir: ./exp/custom_model/iter3_gamma01/ + +model: !Node + type: IterativeBIT + args: + num_iters: 3 + gamma: 0.1 + num_classes: 2 + bit_kwargs: + in_channels: 4 diff --git a/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma02.yaml b/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma02.yaml new file mode 100644 index 0000000..7a1a55a --- /dev/null +++ b/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma02.yaml @@ -0,0 +1,12 @@ +_base_: ../levircd.yaml + +save_dir: ./exp/custom_model/iter3_gamma02/ + +model: !Node + type: IterativeBIT + args: + num_iters: 3 + gamma: 0.2 + num_classes: 2 + bit_kwargs: + in_channels: 4 diff --git a/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma05.yaml b/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma05.yaml new file mode 100644 index 0000000..52d7f51 --- /dev/null +++ b/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma05.yaml @@ -0,0 +1,12 @@ +_base_: ../levircd.yaml + +save_dir: ./exp/custom_model/iter3_gamma05/ + +model: !Node + type: IterativeBIT + args: + num_iters: 3 + gamma: 0.5 + num_classes: 2 + bit_kwargs: + in_channels: 4 diff --git a/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma10.yaml b/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma10.yaml new file mode 100644 index 0000000..a195e55 --- /dev/null +++ b/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma10.yaml @@ -0,0 +1,12 @@ +_base_: ../levircd.yaml + +save_dir: ./exp/custom_model/iter3_gamma10/ + +model: !Node + type: IterativeBIT + args: + num_iters: 3 + gamma: 1.0 + num_classes: 2 + bit_kwargs: + in_channels: 4 diff --git a/examples/rs_research/configs/levircd/levircd.yaml b/examples/rs_research/configs/levircd/levircd.yaml index e69de29..cdc4404 100644 --- a/examples/rs_research/configs/levircd/levircd.yaml +++ b/examples/rs_research/configs/levircd/levircd.yaml @@ -0,0 +1,74 @@ +# Basic configurations of LEVIR-CD dataset + +datasets: + train: !Node + type: CDDataset + args: + data_dir: ./data/levircd/ + file_list: ./data/levircd/train.txt + label_list: null + num_workers: 2 + shuffle: True + with_seg_labels: False + binarize_labels: True + eval: !Node + type: CDDataset + args: + data_dir: ./data/levircd/ + file_list: ./data/levircd/val.txt + label_list: null + num_workers: 0 + shuffle: False + with_seg_labels: False + binarize_labels: True +transforms: + train: + - !Node + type: DecodeImg + - !Node + type: RandomFlipOrRotate + args: + probs: [0.35, 0.35] + probsf: [0.5, 0.5, 0, 0, 0] + probsr: [0.33, 0.34, 0.33] + - !Node + type: Normalize + args: + mean: [0.5, 0.5, 0.5] + std: [0.5, 0.5, 0.5] + - !Node + type: ArrangeChangeDetector + args: ['train'] + eval: + - !Node + type: DecodeImg + - !Node + type: Normalize + args: + mean: [0.5, 0.5, 0.5] + std: [0.5, 0.5, 0.5] + - !Node + type: ArrangeChangeDetector + args: ['eval'] +download_on: False + +num_epochs: 40 +train_batch_size: 8 +optimizer: !Node + type: Adam + args: + learning_rate: !Node + type: StepDecay + module: paddle.optimizer.lr + args: + learning_rate: 0.002 + step_size: 30 + gamma: 0.2 +save_interval_epochs: 10 +log_interval_steps: 500 +save_dir: ./exp/ +learning_rate: 0.002 +early_stop: False +early_stop_patience: 5 +use_vdl: True +resume_checkpoint: '' diff --git a/examples/rs_research/custom_model.py b/examples/rs_research/custom_model.py index e69de29..feb4238 100644 --- a/examples/rs_research/custom_model.py +++ b/examples/rs_research/custom_model.py @@ -0,0 +1,58 @@ +import paddle +import paddle.nn as nn +import paddle.nn.functional as F +import paddlers +from paddlers.rs_models.cd import BIT +from attach_tools import Attach + +attach = Attach.to(paddlers.rs_models.cd) + + +@attach +class IterativeBIT(nn.Layer): + def __init__(self, num_iters=1, gamma=0.1, num_classes=2, bit_kwargs=None): + super().__init__() + + if num_iters <= 0: + raise ValueError( + f"`num_iters` should have positive value, but got {num_iters}.") + + self.num_iters = num_iters + self.gamma = gamma + + if bit_kwargs is None: + bit_kwargs = dict() + + if 'num_classes' in bit_kwargs: + raise KeyError("'num_classes' should not be set in `bit_kwargs`.") + bit_kwargs['num_classes'] = num_classes + + self.bit = BIT(**bit_kwargs) + + def forward(self, t1, t2): + rate_map = self._init_rate_map(t1.shape) + + for it in range(self.num_iters): + # Construct inputs + x1 = self._constr_iter_input(t1, rate_map) + x2 = self._constr_iter_input(t2, rate_map) + # Get logits + logits_list = self.bit(x1, x2) + # Construct rate map + prob_map = F.softmax(logits_list[0], axis=1) + rate_map = self._constr_rate_map(prob_map) + + return logits_list + + def _constr_iter_input(self, im, rate_map): + return paddle.concat([im.rate_map], axis=1) + + def _init_rate_map(self, im_shape): + b, _, h, w = im_shape + return paddle.zeros((b, 1, h, w)) + + def _constr_rate_map(self, prob_map): + if prob_map.shape[1] != 2: + raise ValueError( + f"`prob_map.shape[1]` must be 2, but got {prob_map.shape[1]}.") + return (prob_map[:, 1:2] * self.gamma) diff --git a/examples/rs_research/custom_trainer.py b/examples/rs_research/custom_trainer.py index e69de29..2829863 100644 --- a/examples/rs_research/custom_trainer.py +++ b/examples/rs_research/custom_trainer.py @@ -0,0 +1,29 @@ +import paddlers +from paddlers.tasks.change_detector import BaseChangeDetector + +from attach_tools import Attach + +attach = Attach.to(paddlers.tasks.change_detector) + + +@attach +class IterativeBIT(BaseChangeDetector): + def __init__(self, + num_classes=2, + use_mixed_loss=False, + losses=None, + num_iters=1, + gamma=0.1, + bit_kwargs=None, + **params): + params.update({ + 'num_iters': num_iters, + 'gamma': gamma, + 'bit_kwargs': bit_kwargs + }) + super().__init__( + model_name='IterativeBIT', + num_classes=num_classes, + use_mixed_loss=use_mixed_loss, + losses=losses, + **params) diff --git a/examples/rs_research/params_versus_f1.png b/examples/rs_research/params_versus_f1.png new file mode 100644 index 0000000000000000000000000000000000000000..f5ba61f44debe0ea4a48e05d09602b3eeb5759f0 GIT binary patch literal 48900 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 Date: Thu, 18 Aug 2022 20:17:48 +0800 Subject: [PATCH 10/52] Add SVCD configs --- examples/rs_research/README.md | 9 ++- examples/rs_research/configs/levircd/bit.yaml | 2 +- .../iterative_bit_iter2_gamma01.yaml | 2 +- .../iterative_bit_iter2_gamma02.yaml | 2 +- .../iterative_bit_iter2_gamma05.yaml | 2 +- .../iterative_bit_iter3_gamma01.yaml | 2 +- .../iterative_bit_iter3_gamma02.yaml | 2 +- .../iterative_bit_iter3_gamma05.yaml | 2 +- .../iterative_bit_iter3_gamma10.yaml | 2 +- .../rs_research/configs/levircd/fc_ef.yaml | 6 ++ .../configs/levircd/fc_siam_conc.yaml | 6 ++ .../configs/levircd/fc_siam_diff.yaml | 6 ++ .../rs_research/configs/levircd/levircd.yaml | 10 +-- .../rs_research/configs/levircd/stanet.yaml | 6 ++ examples/rs_research/configs/svcd/bit.yaml | 6 ++ .../configs/svcd/custom_model.yaml | 12 +++ examples/rs_research/configs/svcd/fc_ef.yaml | 6 ++ .../configs/svcd/fc_siam_conc.yaml | 6 ++ .../configs/svcd/fc_siam_diff.yaml | 6 ++ examples/rs_research/configs/svcd/stanet.yaml | 6 ++ examples/rs_research/configs/svcd/svcd.yaml | 74 +++++++++++++++++++ examples/rs_research/custom_model.py | 2 +- examples/rs_research/run_task.py | 4 + examples/rs_research/scripts/run_benchmark.sh | 18 +++++ .../scripts/run_parameter_analysis.sh | 16 ++++ 25 files changed, 198 insertions(+), 17 deletions(-) diff --git a/examples/rs_research/README.md b/examples/rs_research/README.md index 77ae5cf..e57b2d2 100644 --- a/examples/rs_research/README.md +++ b/examples/rs_research/README.md @@ -110,7 +110,7 @@ class IterativeBIT(nn.Layer): 2. 包含模型整体逻辑结构的最外层模块须用`@attach`装饰; 3. 对于变化检测任务,`forward()`方法除`self`参数外还接受两个参数`t1`、`t2`,分别表示第一时相和第二时相影像。 -关于模型定义的更多细节请参考[API文档]()。 +关于模型定义的更多细节请参考[文档](https://github.com/PaddlePaddle/PaddleRS/blob/develop/docs/dev/dev_guide.md)。 #### 3.3.2 自定义训练器 @@ -145,7 +145,9 @@ class IterativeBIT(BaseChangeDetector): 2. 与模型一样,训练器也须用`@attach`装饰; 3. 训练器和模型可以同名。 -关于训练器定义的更多细节请参考[API文档]()。 +在本案例中,仅仅重写了训练器的`__init__()`方法。在实际科研过程中,可以通过重写`train()`、`evaluate()`、`default_loss()`等方法定制更加复杂的训练、评估策略或更换默认损失函数。 + +关于训练器的更多细节请参考[API文档](https://github.com/PaddlePaddle/PaddleRS/blob/develop/docs/apis/train.md)。 ### 3.4 进行参数分析与消融实验 @@ -187,7 +189,8 @@ PaddleRS提供了,只需要。`attach_tools.Attach`对象自动。 ### 5.2 展望 -耗时,模型大小,FLOPs +- 本案例对所有参与比较的算法使用了相同的训练超参数,但由于模型之间存在差异,使用统一的超参训练往往难以保证所有模型都能取得较好的效果。在后续工作中,可以对每个对比算法进行调参,使其获得最优精度。 +- 在评估算法效果时,仅仅对比了精度指标,而未对耗时、模型大小、FLOPs等指标进行考量。后续应当从精度和性能两个方面对算法进行综合评估。 ## 参考文献 diff --git a/examples/rs_research/configs/levircd/bit.yaml b/examples/rs_research/configs/levircd/bit.yaml index f63b0c3..046f760 100644 --- a/examples/rs_research/configs/levircd/bit.yaml +++ b/examples/rs_research/configs/levircd/bit.yaml @@ -1,6 +1,6 @@ _base_: ./levircd.yaml -save_dir: ./exp/bit/ +save_dir: ./exp/levircd/bit/ model: !Node type: BIT diff --git a/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter2_gamma01.yaml b/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter2_gamma01.yaml index 0526e94..bac3925 100644 --- a/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter2_gamma01.yaml +++ b/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter2_gamma01.yaml @@ -1,6 +1,6 @@ _base_: ../levircd.yaml -save_dir: ./exp/custom_model/iter2_gamma01/ +save_dir: ./exp/levircd/custom_model/iter2_gamma01/ model: !Node type: IterativeBIT diff --git a/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter2_gamma02.yaml b/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter2_gamma02.yaml index c139498..72c7683 100644 --- a/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter2_gamma02.yaml +++ b/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter2_gamma02.yaml @@ -1,6 +1,6 @@ _base_: ../levircd.yaml -save_dir: ./exp/custom_model/iter2_gamma02/ +save_dir: ./exp/levircd/custom_model/iter2_gamma02/ model: !Node type: IterativeBIT diff --git a/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter2_gamma05.yaml b/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter2_gamma05.yaml index 54e87fd..7a259f3 100644 --- a/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter2_gamma05.yaml +++ b/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter2_gamma05.yaml @@ -1,6 +1,6 @@ _base_: ../levircd.yaml -save_dir: ./exp/custom_model/iter2_gamma05/ +save_dir: ./exp/levircd/custom_model/iter2_gamma05/ model: !Node type: IterativeBIT diff --git a/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma01.yaml b/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma01.yaml index 7369ff0..c0679aa 100644 --- a/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma01.yaml +++ b/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma01.yaml @@ -1,6 +1,6 @@ _base_: ../levircd.yaml -save_dir: ./exp/custom_model/iter3_gamma01/ +save_dir: ./exp/levircd/custom_model/iter3_gamma01/ model: !Node type: IterativeBIT diff --git a/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma02.yaml b/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma02.yaml index 7a1a55a..fce2be1 100644 --- a/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma02.yaml +++ b/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma02.yaml @@ -1,6 +1,6 @@ _base_: ../levircd.yaml -save_dir: ./exp/custom_model/iter3_gamma02/ +save_dir: ./exp/levircd/custom_model/iter3_gamma02/ model: !Node type: IterativeBIT diff --git a/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma05.yaml b/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma05.yaml index 52d7f51..4103af3 100644 --- a/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma05.yaml +++ b/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma05.yaml @@ -1,6 +1,6 @@ _base_: ../levircd.yaml -save_dir: ./exp/custom_model/iter3_gamma05/ +save_dir: ./exp/levircd/custom_model/iter3_gamma05/ model: !Node type: IterativeBIT diff --git a/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma10.yaml b/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma10.yaml index a195e55..2e5481c 100644 --- a/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma10.yaml +++ b/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma10.yaml @@ -1,6 +1,6 @@ _base_: ../levircd.yaml -save_dir: ./exp/custom_model/iter3_gamma10/ +save_dir: ./exp/levircd/custom_model/iter3_gamma10/ model: !Node type: IterativeBIT diff --git a/examples/rs_research/configs/levircd/fc_ef.yaml b/examples/rs_research/configs/levircd/fc_ef.yaml index e69de29..6fdbc2f 100644 --- a/examples/rs_research/configs/levircd/fc_ef.yaml +++ b/examples/rs_research/configs/levircd/fc_ef.yaml @@ -0,0 +1,6 @@ +_base_: ./levircd.yaml + +save_dir: ./exp/levircd/fc_ef/ + +model: !Node + type: FCEarlyFusion diff --git a/examples/rs_research/configs/levircd/fc_siam_conc.yaml b/examples/rs_research/configs/levircd/fc_siam_conc.yaml index e69de29..50a8c8a 100644 --- a/examples/rs_research/configs/levircd/fc_siam_conc.yaml +++ b/examples/rs_research/configs/levircd/fc_siam_conc.yaml @@ -0,0 +1,6 @@ +_base_: ./levircd.yaml + +save_dir: ./exp/levircd/fc_siam_conc/ + +model: !Node + type: FCSiamConc diff --git a/examples/rs_research/configs/levircd/fc_siam_diff.yaml b/examples/rs_research/configs/levircd/fc_siam_diff.yaml index e69de29..4ab8874 100644 --- a/examples/rs_research/configs/levircd/fc_siam_diff.yaml +++ b/examples/rs_research/configs/levircd/fc_siam_diff.yaml @@ -0,0 +1,6 @@ +_base_: ./levircd.yaml + +save_dir: ./exp/levircd/fc_siam_diff/ + +model: !Node + type: FCSiamDiff diff --git a/examples/rs_research/configs/levircd/levircd.yaml b/examples/rs_research/configs/levircd/levircd.yaml index cdc4404..2ee7bfa 100644 --- a/examples/rs_research/configs/levircd/levircd.yaml +++ b/examples/rs_research/configs/levircd/levircd.yaml @@ -52,7 +52,7 @@ transforms: args: ['eval'] download_on: False -num_epochs: 40 +num_epochs: 50 train_batch_size: 8 optimizer: !Node type: Adam @@ -62,11 +62,11 @@ optimizer: !Node module: paddle.optimizer.lr args: learning_rate: 0.002 - step_size: 30 + step_size: 35000 gamma: 0.2 -save_interval_epochs: 10 -log_interval_steps: 500 -save_dir: ./exp/ +save_interval_epochs: 5 +log_interval_steps: 50 +save_dir: ./exp/levircd/ learning_rate: 0.002 early_stop: False early_stop_patience: 5 diff --git a/examples/rs_research/configs/levircd/stanet.yaml b/examples/rs_research/configs/levircd/stanet.yaml index e69de29..c03e2ed 100644 --- a/examples/rs_research/configs/levircd/stanet.yaml +++ b/examples/rs_research/configs/levircd/stanet.yaml @@ -0,0 +1,6 @@ +_base_: ./levircd.yaml + +save_dir: ./exp/levircd/stanet/ + +model: !Node + type: STANet diff --git a/examples/rs_research/configs/svcd/bit.yaml b/examples/rs_research/configs/svcd/bit.yaml index e69de29..2171205 100644 --- a/examples/rs_research/configs/svcd/bit.yaml +++ b/examples/rs_research/configs/svcd/bit.yaml @@ -0,0 +1,6 @@ +_base_: ./svcd.yaml + +save_dir: ./exp/svcd/bit/ + +model: !Node + type: BIT diff --git a/examples/rs_research/configs/svcd/custom_model.yaml b/examples/rs_research/configs/svcd/custom_model.yaml index e69de29..5d95079 100644 --- a/examples/rs_research/configs/svcd/custom_model.yaml +++ b/examples/rs_research/configs/svcd/custom_model.yaml @@ -0,0 +1,12 @@ +_base_: ./svcd.yaml + +save_dir: ./exp/svcd/custom_model/ + +model: !Node + type: IterativeBIT + args: + num_iters: 3 + gamma: 0.5 + num_classes: 2 + bit_kwargs: + in_channels: 4 diff --git a/examples/rs_research/configs/svcd/fc_ef.yaml b/examples/rs_research/configs/svcd/fc_ef.yaml index e69de29..81bbb34 100644 --- a/examples/rs_research/configs/svcd/fc_ef.yaml +++ b/examples/rs_research/configs/svcd/fc_ef.yaml @@ -0,0 +1,6 @@ +_base_: ./svcd.yaml + +save_dir: ./exp/svcd/fc_ef/ + +model: !Node + type: FCEarlyFusion diff --git a/examples/rs_research/configs/svcd/fc_siam_conc.yaml b/examples/rs_research/configs/svcd/fc_siam_conc.yaml index e69de29..fb4eed8 100644 --- a/examples/rs_research/configs/svcd/fc_siam_conc.yaml +++ b/examples/rs_research/configs/svcd/fc_siam_conc.yaml @@ -0,0 +1,6 @@ +_base_: ./svcd.yaml + +save_dir: ./exp/svcd/fc_siam_conc/ + +model: !Node + type: FCSiamConc diff --git a/examples/rs_research/configs/svcd/fc_siam_diff.yaml b/examples/rs_research/configs/svcd/fc_siam_diff.yaml index e69de29..fde20b9 100644 --- a/examples/rs_research/configs/svcd/fc_siam_diff.yaml +++ b/examples/rs_research/configs/svcd/fc_siam_diff.yaml @@ -0,0 +1,6 @@ +_base_: ./svcd.yaml + +save_dir: ./exp/svcd/fc_siam_diff/ + +model: !Node + type: FCSiamDiff diff --git a/examples/rs_research/configs/svcd/stanet.yaml b/examples/rs_research/configs/svcd/stanet.yaml index e69de29..8ff29f9 100644 --- a/examples/rs_research/configs/svcd/stanet.yaml +++ b/examples/rs_research/configs/svcd/stanet.yaml @@ -0,0 +1,6 @@ +_base_: ./svcd.yaml + +save_dir: ./exp/svcd/stanet/ + +model: !Node + type: STANet diff --git a/examples/rs_research/configs/svcd/svcd.yaml b/examples/rs_research/configs/svcd/svcd.yaml index e69de29..a8a19fa 100644 --- a/examples/rs_research/configs/svcd/svcd.yaml +++ b/examples/rs_research/configs/svcd/svcd.yaml @@ -0,0 +1,74 @@ +# Basic configurations of SVCD dataset + +datasets: + train: !Node + type: CDDataset + args: + data_dir: ./data/svcd/ + file_list: ./data/svcd/train.txt + label_list: null + num_workers: 2 + shuffle: True + with_seg_labels: False + binarize_labels: True + eval: !Node + type: CDDataset + args: + data_dir: ./data/svcd/ + file_list: ./data/svcd/val.txt + label_list: null + num_workers: 0 + shuffle: False + with_seg_labels: False + binarize_labels: True +transforms: + train: + - !Node + type: DecodeImg + - !Node + type: RandomFlipOrRotate + args: + probs: [0.35, 0.35] + probsf: [0.5, 0.5, 0, 0, 0] + probsr: [0.33, 0.34, 0.33] + - !Node + type: Normalize + args: + mean: [0.5, 0.5, 0.5] + std: [0.5, 0.5, 0.5] + - !Node + type: ArrangeChangeDetector + args: ['train'] + eval: + - !Node + type: DecodeImg + - !Node + type: Normalize + args: + mean: [0.5, 0.5, 0.5] + std: [0.5, 0.5, 0.5] + - !Node + type: ArrangeChangeDetector + args: ['eval'] +download_on: False + +num_epochs: 200 +train_batch_size: 8 +optimizer: !Node + type: Adam + args: + learning_rate: !Node + type: StepDecay + module: paddle.optimizer.lr + args: + learning_rate: 0.0004 + step_size: 87500 + gamma: 0.1 +save_interval_epochs: 20 +log_interval_steps: 50 +save_dir: ./exp/ +learning_rate: 0.0004 +early_stop: False +early_stop_patience: 5 +use_vdl: True +resume_checkpoint: '' diff --git a/examples/rs_research/custom_model.py b/examples/rs_research/custom_model.py index feb4238..f34dab7 100644 --- a/examples/rs_research/custom_model.py +++ b/examples/rs_research/custom_model.py @@ -45,7 +45,7 @@ class IterativeBIT(nn.Layer): return logits_list def _constr_iter_input(self, im, rate_map): - return paddle.concat([im.rate_map], axis=1) + return paddle.concat([im, rate_map], axis=1) def _init_rate_map(self, im_shape): b, _, h, w = im_shape diff --git a/examples/rs_research/run_task.py b/examples/rs_research/run_task.py index 1023dbc..b4b90bc 100644 --- a/examples/rs_research/run_task.py +++ b/examples/rs_research/run_task.py @@ -2,6 +2,10 @@ import os +# Import cv2 and sklearn before paddlers to solve the +# "ImportError: dlopen: cannot load any more object with static TLS" issue. +import cv2 +import sklearn import paddle import paddlers from paddlers import transforms as T diff --git a/examples/rs_research/scripts/run_benchmark.sh b/examples/rs_research/scripts/run_benchmark.sh index e69de29..3c66a56 100644 --- a/examples/rs_research/scripts/run_benchmark.sh +++ b/examples/rs_research/scripts/run_benchmark.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +set -e + +for dataset in levircd svcd; do + config_dir="configs/${dataset}" + log_dir="exp/logs/${dataset}" + + mkdir -p "${log_dir}" + + for config_file in $(ls ${config_dir}); do + printf '=%.0s' {1..100} && echo + echo -e "\033[33m ${config_file} \033[0m" + printf '=%.0s' {1..100} && echo + python run_task.py train cd --config "${config_dir}/${config_file}" 2>&1 | tee "${log_dir}/${config_file%.*}" + echo + done +done diff --git a/examples/rs_research/scripts/run_parameter_analysis.sh b/examples/rs_research/scripts/run_parameter_analysis.sh index e69de29..0f53055 100644 --- a/examples/rs_research/scripts/run_parameter_analysis.sh +++ b/examples/rs_research/scripts/run_parameter_analysis.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +set -e + +CONFIG_DIR='configs/levircd/custom_model' +LOG_DIR='exp/logs/parameter_analysis' + +mkdir -p "${LOG_DIR}" + +for config_file in $(ls ${CONFIG_DIR}); do + printf '=%.0s' {1..100} && echo + echo -e "\033[33m ${config_file} \033[0m" + printf '=%.0s' {1..100} && echo + python run_task.py train cd --config "${CONFIG_DIR}/${config_file}" 2>&1 | tee "${LOG_DIR}/${config_file%.*}" + echo +done From 1e39c7a3528a4f03db710b6b3942399df0eea0b1 Mon Sep 17 00:00:00 2001 From: Liu Yi Date: Fri, 19 Aug 2022 09:43:57 +0800 Subject: [PATCH 11/52] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 19be9c5..b67b4b1 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@

- **基于飞桨框架开发的高性能遥感影像处理开发套件,帮助您端到端地完成从数据预处理到模型部署的全流程遥感深度学习应用。** + **飞桨高性能遥感影像开发套件,端到端完成从数据到部署的全流程遥感应用。** [![license](https://img.shields.io/badge/license-Apache%202-blue.svg)](LICENSE) From 2e5e59583a3c12d3c953e4da83aa64aa77d15102 Mon Sep 17 00:00:00 2001 From: Bobholamovic Date: Fri, 19 Aug 2022 10:43:17 +0800 Subject: [PATCH 12/52] Update whole picture --- docs/images/whole_picture.png | Bin 247128 -> 234493 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/images/whole_picture.png b/docs/images/whole_picture.png index 780fce0a3a334c1b9f86bb6bd755328732ea813d..dcd5945a90e60336ed2181a37329b50a735d719d 100644 GIT binary patch literal 234493 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�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

7L=L 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 literal 247128 zcmd43WmsEJw?15#g zkN^n;`$K={dCvKA-t&BXb6xw|J3DJ;&z@N`Ypr|TlV~kXCBpku_ix;|L8ziEuXE!D zKFf_8xA_R}UfmgR9=UyWy6L5(^y)@6lxE}Vi@&Xrik*hW4fd;Rf*W`@sczi47P&g4 zZc_jITJa|9jlV^2+`4fi((wk~f5~WGov%OXSI2dkf6lkR-25-;t9xH={Yx63<;(4V zUEk)rE+*$1e_g}9x5~y|H*Qcoxjt^%7mQyuTjqv}ysV!8&F#E9{x9^WA^>X1-{yTh z{OrSJDf}|}<|)bj{QT@a{IK-Rea1B8ei_l;*s!khId313*aq|k<6b^erM!K`0G5-* zp~TOebw1r%r9~x-`^iHPw~dEDST+&$UzW{Uqjy`VpZLn$xb^s*jP&DD!JVJ`1HVHZ z@|d69xJhtzI}Oi*9yK;Zo407RTl~g~1@HR)L&oriqes?I-dC%h*Fuj=Rla!5K?YcC z0QDV&)d76?&v-qFpKd1}7-5zEEPD7cZ-?KwDj6R7jCW>MnbL1tt3Em5`QvwgZ(cC+ z4$!NamWL-FX&J)7t-0$Nx_hSkZCWMrwLgz~Mj9iKo1`iYh%kQMUD%;rb8THM9q} z??y)aCmM3K45*r5noNc5k#uu5aF_l^^JS2d0(NUt`f)c`+H?GpHXt*LPbu{)@iV%D z5c4|vP;U72VotnNDeh-~xBa}l#MC+^wL49H^uVZ5nT_w^9a@7!S3q6QHl=Ccx7-k{ zE~M-HtIgu2%@ks4SV2-SQzU)V-sP6$C|=fjmK3hiW_rO?Irp0qfs72^lrgun#>M68 zIUojuo@H{T?+Im%Z}IyeG!RF5x}tWZ=4k(LTWUTmCYpcYGJU&Z_b$c)GG(7DW^Em3 zhxrC#Iz4G#A(oQ6QsJjQ1Ss)Ug`QE-%;fNj*h1t<7Rf(YLK#GSr@z_@)WxZihA$ zIwQS95{3iXcva0i(YJrC>z40VIs#vw76#?;19EB&-JELE*Wx76X5Z4`2*Jr-6}tAy zKIWW3dZ_JQdKG5oL0b0yiQR}%TT0ZASySn%krw}fe^RgFRxiFEiOfTp|ToH{o|tTyJhd^aay&21efJ_SIqBETo@&1 z2yKX}@5jWj-S%Y_03S5dj($OWu3suX<#2EDW`F}QBs=v&=YFguUNCtTBAsGM|0~h#VRt5LD`@? z(iffL@3ufJ0}~z26aDnFc|XsQ>5=zuZ*p4he-8d@R{t&;VNb3{Tt}NPh-TUjv@>gy zrCymiwfKfD@mcB{>rW_Ce4F=YC&H2(D!7A1In$sr!`|W@Y!Nj>M25qg1exEXs;_sl z>V#%aADjN#CKP^HLdnu7lJBuu*@+}h9_=(}80~&}iXuuRXG`VG7@pzjPTT%YeOm2L z<5wA{!Dn^Gzbrs0j_$xAsH_`2rH_Br_k|i(b2lqb?X-XCD}LOvSMsR+aO%}G!+Bn! zDyDl>O(=z$4WB2o(UE0zM(LtcTAui&VZpC$EIXX}w8<}aNL$)5y>?VG`5bP@kw&Vk zkz9Lvp0x_=oUKLqev3b%%sE|db-aD;w=m)fnQ~Ex`fu0kr?|E`E!Y_alu~bun?4;0 zvAVyI2OA>gOUDL;M2F_ur@IIwaPc*hR!)8P@NC+s7pms46*hY5?#!)QCr-y9Z*7gE zF5cD0d_J*@<I_`iX66Av2K6A;b(_NMI(lAa&BGwZWO2v;7H? zqvjKDoOc$SMmrEQPfLhfrpW5mGiur9TkVHbyrk?)eGC`@KU=T$gNpHshc%M$!1scX zRx*Kzf(Bt@T1{3FL8{Wdti5qwQ@H9k_so4ngL2mSfY~(gYwk#MXuNF2fwr)vr2T|u zQZygne8u=yDieFu4)CQ-urNV>h=x)_*ZqNpd7qTJHmB+=$@+vht zOWLo>d4}lU)<{wCR{Ut2;(K9<^YZtAouAy|<&f;)H#9ckqIF5(o+qxRsMAnQ_v2G7 zVfizQx|e3*e813D-KLU-0@b~05y;MnX>kvF0=!!gg-w+VAcrturUDZGwibPOwk7To z$++BCsk5)aTMfSrpolD69D5@<2y7RTpB1dvF z$7Ikl%+o6a{VEC5eR|Ju0d8*ms~07TclDmHd+sS)wW6Qww=hJ;@=M-VVAGk9FGDSK zI)Bz!f>Lzjf)6v~#hhuW?KxxPa<_KiPQsE#8#PH&7M_a|`~#y}hk>_p=#7?dCR2Ea zPR02f2>0B9`xi7t5T>*ltloQX7U=A^>MfE!=hSfzUBr={=K4ijTkLRpkt@*Y&}D_@ za2?1sX5@7=e@EOyc>&|S+R*PmLX0RsoYpz!(6Mt#{B7nA7HWqVDOMJxc%VD7cuEX9hb9?2H&zyIrsDK6c7gJ`JF%EV5>Fj_Ko+EEv&{?|#Tr!F!pSvEagK7(h;?pR{Z^N0=(H>osUVIau~54KF=IqxYKLXa^Y!cN7HEvaW1h{ z&mW69-cAJhFU1#P_kpENd*ofE84OmVBt!Qfdt?m0rIcrLSUu^Cnohds*&FgnIf=Nd z{@UT>Exe~L7CsOX6y^h!9nq!-5uDjday1pAZEf^kbmHo5oOEH{G}DPH{oYEV3K1a4 zx)19+pS{?v?TBJAo2yl!Vm`pPo=XTo z^DPjzKjQY*HPMNUgX4W}Vqom#hU&{MnW@YCe77mx9~>1jTJN?f$90OZhb}bNfeddY z9y6V^b?hiLHZ68Er#=Yk+?3|4EUb007~3}}x*TJ@ysa0rmQlNx89U^h4`GSX?cQva zI;nGb;67(!IPVwFUli~InEQ6&+|rhQ@eliFN(uh>Vw1%h=QLRWlW!n}{1Z;w z;4~7Q&&iQlLpKE^yk7ztojfithNh=-)lZ@@Z0@ zcC|66fcHgw9D1e7FnJncRU~aYp-JS_7;xT6TQ(+Ap2&O~DDfVY!|uElSl&s!1Fk38 zTC#HAXH5;!+=$xr*$@C4ju%SH+uhxnmczddQMChtvT zox8cHX<}}GOp5E8;A~1A;&<*jI+sFMb5N;(4CT&IGMZwjA+ludF;GMJp08_@FnQVI z&r5bGx;nY8!iEoz=(aS&o23y{Fsf>i)(jqyvF?Veg-CW!q-r{c;=C|!79C3S$DE7USS9(lvwziMN?v&+7b)Pj;!l$$od zq8%cNdFR@xxK?Ov`WjQmA9N&WE%N=|z_5J6!PYiBYRcG-l_X=Bh zD12+PUar9kAe{ECt_?sG15ik}403O!&-LoB^WbxQyj6A{m}eX)1~3+S;d!@pp=dHP zl2gJy_mvqoaM4*t`cQprpddKa_q+#%4pH4Lk4hEvbFYt0bf}9rgeoL(#r}j*LY8AT z8eBL{>hYF~$G4plz6i$26Dt7xH^2Fa>83ba8pEpG39gl4N$&T~Kx`BI0%b8E2^DJm zapv|#0t7c$C>u#P7jfR=EQnp(R+X#jswpbxu6RdU;K8 zB%us$8#Og@^t4fAc#++Qd8}9O94*fqVnH$Td+W=-ABv!y*TDTX*?s#^&%v&+Z3oL^njv>K-tWPq(!RlX{?;Oe0i%vT8R=3$e zw@lK5!%gcRFHw?~wp}Vj3L7h{anP3ORH=@3`qn8MdU6V z5XN||jEX|HbS~J%LbcTTNnQOHe9}T-h+#)=3Z*JNP}GCF)%iGd;6VB3Ce4j{in*E~ zo0U5jV8aBbMW)Rh_hrGTjKfKw>JiI_xi<$tyr98SBnr*=Q&eI7`|DKk(%_)PDhJVZGCxFw2Du=j)C=#1G$^3y?qZ;1S_W=rHxX2`29LlJZPFL2gP` ze7I<|m7vp?9`Fb%+3h}FOK2PSGHj;ks>la#+C=DqFVDZMTWdnplvg;MxY^VEc-1#e z;_*Fw!Rd|X5}jb32xoQJQ%XJ;jX!Kten!&vZAel&_(=nZ1zZge-t1~DBCFUHe7s2S z-}ag$NU@?(JkNY3)oYbD?I42rQkSO2^$hFJpEPg(EI(4{-=K#~7|D8o5Mt493n&Q- zQJeBPnRD(AIgr!pJM{Fp9jOnpzlD|2ji*9mr}4Q@{K|IkL*L)POU1Z0ZSXULSFOlk zCFZXa^3#B7Tt>4MO~lw+KyRJ<&mrRUWT=&4Cnb#ttWKw?LN04#g_C?a_ug&5^yNZK zsNT7900oOeOavjut$x-_dw=~vL45OUXVf!^iSbMT!>4x+ zPs0gJuUujI$Hy>dO*Y0)sZlXvS?*17c^>+6?ixt#C$g`8gdR(k>ME5dew zfH3Fs4wbt@m{PO%h4b{6 zfR}uOA;*p{W4!gauuh=+qB0SWU+4lQ>O zVX1nah@wjsmoM@ngpWp%i&{0&{ruv)zw5@dj#lYIo`;Oo?z5k;iV2`D%DsZ!3iER4 zSX(^m_?R|h^UGu!8JwdZMoyd}?7zr2rn-lgj$HU#X{}6pU+OA|Z_f=i{@U)Edf2aW zp7;3Qpd~Lm@zb)n6j)g)=>{WFQ}`3M0D*fu6|BG%Qcg!cxV-IZasptr%*j5#$Hh>xvde!8k-VbzLlG*qS5!tktX;q%rrUYdJ9`MsD`wGV-pVM%X%^Noit zg(=Vu_Y`E**B@9aggSi1#qz{$5yPP7KL~AxIa3?cG6sDpNmz_GiB55+fhMIPP+cC1 zqEFrA=a4~2A94ioGmq_4^+DTblOOoTzDboD`~g#owUb(e50Pkbpt^PbW z<;%{sI%oh!R@<_2J&y$A6cbwR&In5NF!Lx()z;eMbngTeGQv+Y0Raq z4>FV9kH?yzzS?sjJC-{3ILouRDG>&y+Eh?io1lrZk&?g^)h(9Z*=7s5m&$0$S1&-D zR#z@++9(y2$W%eUNx;7FLSp-YsX`AcmC}7QFP7+F{%?jQi*Bn zKhB%;r)G%4NOVHmdF=oT`kV2%T5s7ier+xf47jbdpO3W=fw|K@2iw`cO45yz z7i13F!lq0!OAo)Sp@Z9@f(=oYdEHnQ)Y#M79-7+c=!cdC9T-hW{ezVvhgW$AWz(>^ zjZ)PQg&6#?cBh}8d)sx2D*Ox}1o1DEo24gl>?F3ks#A$^k8oh7W(QR5+DdS}-FDv= z{-+6Q1QfNU9q$TD9!Z}DF3n6YX1F+wzF^GLY5ZYq`=?W0(_msr{9kb!qj)$~Nh9&W zjI6Bo?z|E<;5U?djq&!f2L-lLa#}#sViOXT^|cXM>m&Ky9Qi}_I8bw8ifj`{sVus+ z`DhkKc<-Fex()wWe%U@)C!r81-8w!M#ZbQ06X1_6osCJvDu+FLeH#of@eiA)72 z&r39}H(6x7czTrV>Rap$C@1z!q(+l=7P3ACIuq6{PH8lyQ_nXTr6(O$5veu(dF1N5 zt*{h_#uQY~b)9AUk98sUQJB8wRcp}h?8+z^=r_kM>SUrTAL^3^d;^Zpq34_l+4*V; zo;#2qtL_?RWSv(tKJRDDXqfSI`x1PDLbn4O$;4xR&;WZf-y}EGJMBJ*P|Zs^%k}-# z(ROfqmJBqX9&&9!+laO9j~id4ADDcy(B1mNoa0r6c%I}LdJjkxde~5Y&c>%aBvKZ~ zptLh?BTknlm7ynQ7)^tp0dGU997;mR*RW`DgyX?-j#-RUQ3w=A@;DcKP%!q*y35gA1jM z>ri5?gW>$}ghPH=y?TqWjds`M`t}R3!?(9>J=?Vh(ftuDU{1?tN{?*~Rm3JgHhuTy zp~pW=Nu3A|X=n$}+RWtI9{JC~NFubqSXevNX@pEs6%lLq=j7KdyqHa1UYkrg5wlOu zv_;R*bd@|rmrpOq|I1T)*HbG0x>#q-0Q98nj*ibw-zyAEr;sBpLZXFi{%~i&N)>*t zO6HYOC~xteVqu1ulKOq57q%WwLU6W87tvi({Px}e{MBo{7mqgurw+FJ$^{qx9IS1d z88Yy^3=>j?XD1o7>b|!^;~Vy~-_klyksk|$6;6)p%0KvtK3bxooGHm|pqVnS42^nQ z=$`Um-6W*c&`8Bh`*hjY&5TgcMo(O=AF61ZBR5+l_A*9BUu->++pTuB*|qWVtQo^U zSaI^RLafl?8EoL;#@!TbzS~e7sxbh#Btnxxe3XQ|UpAJ$L<3vQ{pC!xHA+Uhz4Y|T z%-*=g_`aut?k0L`_tRj2EAwb!?e0{-X-$8wGltS;Fm@o8NUChU0JADtd}G0}x3J8& zHP;2&Db-ihPpw&jgw39hS8Lywf>oX!R)_?JuN`=e&~SW9yD8(i#eB4{YmHD18_j0e zm$O+F^z)n9XL}SKvu9~PDiky`kZW8r+_wCtUvm}=pEx1gehp?kF=ALPD3n#&dgwjY zdW~o1Y|q;HwefYNU%BN@1rXlys4%>%Sh}Ymyyq$@eEjpaME3mY9KD3xFu3dP(}^u) zx!wnx@B4hdKe*GJ7)8kwmNipNbRnX}jgd?rXkNYW6v?t}Lxh0h68TOcpUpzNe)Ix;j(x(h;ZFs9P~JZz=#-oDr{ zvaszoeTYE=KbZR%pkyOY;CfEkq{>s*3=%(qPIx*pRG8$KucoVyz-EJnQbHsOGKbYH z6-l`ae5KmX0R!d2k7zEvp3LkIVoH6#>YTCOvb6wg;pQ4^OVISQjf$%3K!ov(bJb5? zpMD|pM#xTz~J>T*TynE9;nep$#aFV$9-o3vAIZ|m27SICTDk{f= zFZ`y@jiCan1%9(xTuIvQqa$sJ9zMbb1JOFnEM!A+LOhGRNyr#q2-ltA(=ED{=}qmSV9I zwu5N`2Gh?DAWTOc5_I|^l=C*Hk9(a9R3d;h@;*7ik=H1c7M=}r`vRf!rdl@+>MY#$ z?d7gmiVAZBYv+bSjAn4xR zw4Q*aqr$w^h{4+aJ#`JY8=Zd={Lg|GrHf1Y*IG1!?Nq&&J8xX66G5bxN<5-9-)nv-@wK(cBJ2@(n zV^`+U{-NXkAny2Qf9S5yt$s*AOLLx=U#tA?CsSf8$B#%@lV}sM#|ejUW52yA@Hew;zKlPbNQA?69=4(>)-$k zRNmkqTB!gyV7$S1z{V4HSBYGJvt1J7&_eznp|LQIaz!6^H_6Uc2D0RiIKb27Mn=Dp zSsyI9x?^=gSWy?Ude>3p!FAH-lH_<(Ja!E{u)RP!!X{{6MXr#?Y8|x_RzQIwOKeeB z_3SWqjCLtLO#5Rom+PG};M)P8D_fyJS zy|pH@ckD*xHKWA1mDclDhQv8eCuxNTPejuAfzLY6FhkZbltWz0_Iin%c8b{UZ`y1I z+R>|g2HXpbJ$nd4F?aKb`AwyM_mxikkvey&_s#BzV3em@-}iRKMlM&swf{XD(KiYF zB{UAW5?xg(wW{kML#1IqgO|^bPf1B~G`12>lLs#O-(tZ>X4}{E=^qwHuj=bmUQJcv zXLo}49#80P|K^`XJpxmZ$g3VP`eN>tgS^Yh6(k_ToNP?fD`uPda8+8Ep*EM0f;144 zJ)AlxVl0JDrqeP#7Tx#m+(4aw(7Ctgh_53!Ap z9`xvrpcC#6ZD+%ki1D%W5#0m>wJ3+m=^De3kj_OpSMEIUdWND6yP=t7sCo)lrR(6PN$2IO>7A(mop=ZX*BKDIsH}E!_~gQ8`x|Pb2jb@nEk@~ip6nI!vZv_n$dh7 z7+~0sY;y{{K$}8*B}+QhqKO$kZ1{HA1A4TdTbp&NDTLYzV0l3xodFb6o?WZ`(dOUe|dek>2z@2+e5$@bGo22;(T zN-EL^=;s|zj}CSw-#`n03%;n=K7Ydo<>>@6Qe_yp{r2#cD(8y%6|lEa$hPX=7>p9^ zNM9(2ReK<_U>Q&aTZVnU&VTlP9#k@!~2NW_;HSzsMe!N%x_R`t;J>6q43{wyG-9C5yyC zHl)lbbIlfW0FM$OlMlSLH2vn6(u@qpr%W{{W4n2r;du^!in+RKQa} z0=4~zMANJJ`b0tU#^3-Sq;=ZI@4clz)F=$NY&f^X6j_d2Ha71*0&`g(4AUl(F`TDD z6|M#-vq#xIH#HeIWSzsfyz-v9$ycLTpF&Y}L%|I4M1D*V{U_)vyNTk){fgCRSO)2S zMlK~=$`CDJIU|?PO6ggsggBn!i*S6iEYS-QTHns;ex2;r98K4ztBjh0_Qxa6zz+xS zbl8cC@fb^Y#t=PXfq}xMU2n}}LC@H+jQ9Z!Kld}dXNKqk`1@Yu)4{f+*}%=%OUY1iIr+S%TTRpufNI{2_H#9%bcc30f=Q(mWy@xMwZKbfC7e_ zuD~U=p?WZRQQ&Kc0O0v*a^d?&(!$lU%uLvPx;tXChx;uxrPC#?-@+Cyiya06!gzMK z6$x7)$lv_ey4v+b1X9$qDw9MyVw#e)lvAB9K^4}u#|sllIF+X)1NcP7Iwd8cYP=9d z-hZqUECPr|(&V~9pN{5{>9nVl2~MJ+N14uGrFX2>GEoE?3l?zclAJn+Shw-bEE&(S zr<@8$bU6W8@1c6@js4;y@hsl>)U?vOxcBx-Y{gR{kFcln1M7`ghXa?JC#C7dG(;2f zqwJ10)p>CaRrU*y7sD5?gJuI0zWD>mRxGJI( z;Ctq}=D6(j{W#e-0mCI>m@COdV;QKn`@=a37a)pqsd>YVqnzKexI1R@dg2_h-3iSe zHUXK3de?)>M&vgJ{oo?*IBg8Y(J7yv!PduDrdHy$(?X9noTUAhiw~9Y1J`I2-bWk( zM4I!fhW?b&B&aUaY+8GUO@#$-$tidQa%s^oHybg!N9NC4DbQCpnQ-LO2k&b@eh~b_ zXS`skSvfPF^{ko+M(5&lNL(rcNk-pG`F>j7Jr&V+&CRv$M|<2chXl}F$q(TvvR?t* z514HukGz_yIm&x~=zW@zYa`49HRuwVo0ZwX{VJGE-IW6vxOw&)=e~SS1xwsl9TO;O z8_bbn>puRd?+FZYIy+P|6(~#3_~WNCZ>$U5ULna4ha#)Xwu#b`mHA-B z#z9DEI_nc1;;4y>A{bc*Q_zHDCWY5>o_q|dh$rr}ubrMJJ2bg&pIua#*RJr>{`0P9 z)lA~9nSQ-?%;kO+Ua6|?F9CU=HsKfou~l>eYu{@$uK66E>eOzcM}7P>tVGS7%Ny2r z52?=UcINh+-6EwU){T3n@1nj9WGB|Q^`s&_( z=7+i@PzXh=#?Ha_?$O!KQO%~J)VfZHwBe`^|4f9l-oeQuKNsU~TQXrd{f=hauCM0W zd)94|`pZ3+U923i-bMs_PLz{-$c2WdJI-DqZ6_=c<)L914J;;Jw(-8%eQh=4Z18L* z%b8By=525rJlgS)XMTYVgVq*z83z}U!-ysERmQvz4962gFP7By-%^bV#|ZQkbVh8xB_M{;Jnp!KE)6%9eb1MlI%&pCy?io3_9@ z2OdKaVkv^ps`M-jn`;jgMNJzwjlDJfGgeMw5Z$Mj_nR(a_Qjn0y6ubh*WGpdcMDmK zH#L}}_bLA?4;rrhsYls1m!@;b!?f%qxsZ2#mYxlvyK{#-Iu76Bz%}dBaQ{GK`jI!2 z6dR!eJj0?iNOgLmdV5`!ILD>Cc~mtdjWhroFK*xoIPvlk<0K0N`&4&q;TT{Y-&ZaT zu@RLlv}*3yRPBQg03U<4KcMC<(MRFkeBoaP&(0IAPA`i>S)TqbPn&`TBMUk`Yx5+k zv*VI$#dMEUtlC%BT$_+~Ck`)Xb9(zHQ~J&)@$=}0Ax$X-X;P|%pF$r4gG_voO{EuE zacQu+Sdeq)`OmfLzLvOTB}*;wacuwt*zV|?s(u=2Nc+e@^l5>UkNbKDYT41xmf&;p zpR=tiDiLJs_~A9*d^919g*F|JYN%=ufyiM5bm@;VKG_W^uU3%xq%LB`o)!q?hV!&xI`*aPKeE(c7 z6h0jjjoW1FgvN&RNprx_{xvU3DsVIP{vd>Y~au`_CIV|#Dx??07(D6v6iDzJBqIx}w;Vu9UD&y!KmQ18z&m|^1>B^^O!p@fD znk%46euMPj9&g<}I>V#+<^jQacOX6`b-S z7gLe<0NKz1ZAj7Ev$W=RdiqRIi9SJ`7~Y$z9%Xcg)e=1&vS(tpGhMbz&5DbM52lo3 zKR;E?DisjJ5}%U2f~xa?Vnmq?UKxVC({yA-3r`tfcLyk{EAE?bf7OQFD6hUf*}LIG zOEseF!DP#CD8QC$HkNF8n%LA!)d<80j^)qjCTaXB0~{VeFJsJo9wlRuSB6!K`7uLI z*hyyxn5Tf0EdV1(yFgT;q?ylKygh&RL7bE(C%M{0)mc=Tb!eseW3ArMY>Gs3t#A_Y z+sc=JE)?&-&wp{a{LPplmm_geptm?-aaHy3wuF=IfuN_hjjj8JA@E%$bYlEslrRtA zCsy(C^kWrH3TCVey)5!|MXwJ(I=w)?P5m2JEU7)4T+xxYd~1=!aM&V=o?3ZCx*Im` zL?oHhWn}L9R;avL#3mM35CEh#X@`j!c0U~_;A&T{d~BdM)U>tM>E_O@8s1#0)TY*T z(S_{lx?oX}_KKQ*@IaFv*}6RFW?`t#X1BQCcOG-GooIo00vnV#%uQg0<&V#x0$v5# zL`}{~`63`QYjh!Hr65oeL-VQ~G6axe?roD69aPBdF=qwGoQ)J(^ELZOr}t z!U*(f)Pwd{B%*1raimhY*HjKLs@U zrMy|PZZ|vgiJC9?Xr3qVvWtIh07VweE|4j-oHC0FzZ3EqR{erJ-{z=yCE}Q7G^m1< zrGw2Atu6ihCEP}t0n`?CX}+BHo8?D4ov5(GllOT$ng=k?;f;;m%0YluXM0En-egJ^ zjtneT$~Z~_UrX6ZQSgS=tI zh8A1#F%li7*&GGnF(KUPnUi(dX61-bNZ2AUD9F95;F}1X_Ce4yE!o%k#($*PC}oX2 zUgcR9>*?EAWwQ=3I_9R{k|F*a68%)gS$aFCgx^VLQTn(6^T+VKICVSOl0@;BgAz}- z-jSmNP7)6RQa~iP~GRr_>ER*NBOT^=k9QSOT^zjDSzpaj(9`a?;47F11vqi zf=w@vV*>;<+N+s}4F^<4Bak{OpUmE$->&W8wC!KT*EGeqk z1=8gu%Hm+Dx9uI`VXO)j)Pj;RAd>)MDB7gj8^t{L9%&?5q1%EaZ5Qe2mi`>2>bt9L;NA<@J5l3ntM;H0*XMaf zS6|pYpNP>}V<-HobD%;xQie%_XARGo-S_HUm-M6O>XOHtj@BZe+<;b z{)5XT^nB*Zme2|NA)S6GLlQu%3d9*Or<&9aCAXGVcP0$)8F6LI>D08^ZALu|ROM)& zw{p#2Xe`SK=LyJjveH~JE=s48ws+j36RjNmtUSPuR0rzO4-B76tn~kELL}yNXv`O^ z#^Jt!fUFOC_9>c`w(>uR%WjoCaNk5yPo3N368zPjf8wAXW$RqM?2667AN1y;d+i(= zO=2zc6XI!GNgIuT*=7hiqEt{P;p`OiNZbzFQf(wt2MUs*DEAh=^aX4Q+)-{R0l6wW zW=hy&ml4~3*w<<)v{R!>Wy;@cBlFp28tLxmGspN(9tbgzBseq6$?Hg+@EJ_Lz+StA z4sks+{6jq%%_9b}&K4V+S9~D%F+WuFNRv+5w>?M(l`tv0?M65qVIji$IlI|LX+?ZW z1*g8j_lV{>pXCl}mLS9ah=ZWna`y#~4@S!-tf_@KhsNrSSPz{CI7Z(MNiMyrp!_Dw zRw2_WuW^jp^NVhtHjbC$(7%+>@C$}70w=P?dJ;d=gVpMc z{1#A|YSEFm0q~jFudL8l0a;$1U+epy9-|A8|*&_e54|}dFCq)VQG_HS*?11P=54S zKE(nkm-Nc@J|F`am`;Fqp^jH_7&a}d33%zdajDCtt@o)If^zngwkYM*T)09iZSb}< z?zV=w?gtECg0P5=_FBy&&y>3Fg~rq!uji2Fp*bMWqpJ+Ruav4!vx5C%rbIWm09cxD zhdzeuQKNOiqdIEIWvF7kQl$>(1k%YijBs~JbJVa(pmOh_K2aEpWGp$lyt6D+dx+w1 z!fKD1cOY)H%c5Ui!Irhoy8Q^9k7CrcHaS;UVKh9dHCYU5XMVc)YRCxI*|xpD#wx}* zsctX}S?@N%miu)p4)cUOD#NCK-Nys0nkVkRUDQPs@ez8G>Xl2UIkfEcC$I``i&UrP zxi?jEb-otTA>f%dRy(*NRhm;81O`eeD7Ya7g_&a7v}24RwG@rB=KzYr{WnXZ!(Mo7 z?<_m2nfNTqt3idvQoc$QwnjF>V2Pl>?NV!_ETbSr`S$_0TQt->e`)bgFdLa3rqV#iMMedum z0?ANvn7BbxRz-sIPGu`ibE(hzIfcON6F1T-dkNHrsL0)aFGz}b;Ys}>lseDu5~y0G zRpu~{ZgNfigwI&_lL+L2Ol}A-SneV0>?i`n9#FE_J`{mUmXFSKLz3@*x#Gbf+Uc?8 zl${?zSmmLpgqE!4`h0W$671Q0zUM9)WN|Nuy*Uy!IXS{x#&2NGKApyHdkdiE14$Zb zLj10*k}e)`^R|=iI_Ms#*?WMx!iooFWMbPtA<+cHm(5*>-)4Ff<+xUh(2A!D;+DHd zS}QlS)r!^AS>L=GgCC4+5+~$49kx!#GFw|@uU}LIP8N;dJ)e(H_Ox->sf1-pSZb18 z?PcKuKELA0dR_t#Ybj{8g4g-NH*8+6JWlfFQ*N3av=2btZS6_>CK>%O zOg0ofm=Y-OW66AW`m5B!sBRR{ZjkTQErub3B(MKNo-oxhDEq{HKiFhBZ6&+?0p^0G zszk5elj`0$g@SWSQ(%RiKEFkOKUZhTv_kc}YNamk)f$~qx)yU?(r+!h1_86X4UfQ` zFsn-hAKio1n$pz3Z})bSO#|t`-$T+iUdSs98I|K~l97w7CxGWM$r8R;kymR+ez8fq zpLHOrns5;0id-c~1rP|G$GrV!Gm}>Iu9o!FPHA@ z@`Fpjc@e90@n+(+pkzYF4*g-Ht%#M((k3x6RUr(+>RV0GaDa#f^X8=`&i%~|jn-QK zz9KN3n;O|6JMl%eR# z!ieIJ+Bo*;IPi4tFB%2K%~P^`->n1iEW-y-7voh#{W#MxJ5F+cH=R{q>qz`Bovh#B z{@NFTYAKcz^VqT1dbu|5)f=A8ZGPSf8R_RhGTgS-3S_@Cm$q|~oz88-_L0uN%|TdF z>+P~#o7Le;n4m&q(;=tdsVJb0&)9RbTf9S4U3YRPZ9ouSXWC4dLhMEj4r@Am<*~TG zPodC`+#d=RZFBrHjTb$*y|9_b#9cqNGngFNF6G{BQ9aCi62?R7k`gj&PW(BVe3L}{ z@)YXc)Fo*UX4_%B$+I z&Vh{Cjm+-{Zp4cYJiig1l-SdfS5|stqqJQ(PM#L{p@HR58#VApx>r1|M^S;&@-T%o z?T9j|4#Mt{?IYRy%F7PbmXhsy1^BX0v3n}2l$cH<-E}w3Bd{~p6J{%<;=Ovteo-U- z%o+abNpg;FN$(?v;CykSEG-wCJ^W$}_?lG^47|J&fA(TeC^3~&K`_^=RXsKRJfS|@ zwT~9Tp3l`V{NLLK;wtgj?LRQ?vGw8!OME||^U>o1wfr%^AN_l&a=9Gle?fTXblnS3 zu@a%8ag=;lJ@4QP$To0)qzmA!pP}Svk}{ql<_Xz~T&?e@Gc>C79NWtd@&ZMZKcJx3 zHU{e=&%MIHoxh2#WT%X!rfoAPrGNyj^DHfBf?ck$b->BL{tMs>=v;_%1+0Um46e5@*#CozM+30ka!}zU>o5 zY>n@_Cnt_S>A_ToIX@JqmR*xAW9A54KUN`<$6x38{HZp5FjMu2jYE#;ty26d`A8OS zqHhfX@1+n{8XE5^KG7y@I3VKUd7mf&F3Lf^^{=kU1M)vnT1;si{l0GnciYhZ$~SaZ z^k6fgbZu`9zX|>z|BZV@S$&B^`sEz&;1+2!T=Js%FX<;v9WV8jfJl4iN$4UH*o)JA z;Gf;1WxYS2s1)C&!`A11^yJYNZl+l9z2aL+9~QGW4l7Ru(m?i>`@8#Q=Lt%&)vP;t z<7IRwPjf$#Hkz1&U0_W0QVabDu9Q6bs;`ihZS~^2=I|%x6(=oqWZS{sc@{#Q(&YBn zEAqPGtxcY3BW7m~A54<2`X4Ckr5i?2Y0PbpQ*ZeNpRS{nWbb7Byv10=W?~Qt@IWdU z6fDf*X^#EIq98ucxWZFbM5E}A-PrOF-ElFu<%DJO!-~0@koQ7%mnjZC<*BPvt9?(u zO&1UO9_)GD30i4=ld9UZoF=QyOan~Shd+9$w%NRr=@o82lEg~=?IbcVaCHyHsHA!d&AWhKHFZ4nT$Du>GiUUe2M0N;b5_YP7d>_>wL(~w}*GqB2>(q zuM(+l??(LR+v~=>-h1%-Vq0s&>*$yOh;ul8cvAd3ovZD6E572`-*)Yr)sGTCvI?YL zG_>)EbVefT1F_v2NzphW7))}jhgn#&EvPD1XR@lsXsWui`86^~AYy##=mkk2Zl)s9 zJ;6ye;=m?Ted?9=%J9_&PdL9}bEcCB_vYQ9E3!Accq2TAeiBSzIi~xN4b3+x(Ymsh zkS*xdTVJYl-zO}sx~4mqYUTCwJ7_3%T4Y7+c|dBpX`zQzt1Du=+AF*0up_3l`*e={ znqO0D`CzPk403kGp?=1D>z>QX0YobYdjT3py7n&bH@Dh04dXb0@`OA?2D$R_?a}Cnjp|ugK8Lj;GGRLb6!6ebhj;y=45RKb>}YY8m?- zhR9S!c^p^O@C)|SE2{FUhhc<~!V-rX^g;eB2~?*~9^c{ITh)3j?4H#R9vfxx3<9*!nHlu*me<*5VG?my_=jY!^O0+U+d^OJlLJB@n8`(&f!ne zYFM49U1MK)6-guUChs<<%OS>S{iUMKP;q4j6_lU>gcFBiEike>|Dg!1@nf}YF@JbL zAH`LFqoGYJ@1;WMRP*_T{B#@t)C~$;YWB;2sFKE_}h`CcWIWqBZp($%!dja!s{lmOIZzz_UeNfu3pYxNiM<9t2#`fB7H{Me-Kd(*b(6lLZ*f9OW@$h&ePdHbuHt(Em zjP(y~@t)&ylKDBT9UC#ZU3#7epM#|#n_3GVNCd2ggvlo#&t{#b9s~{cBLVXer$NM$ z^UDZ6WBn8)OZOxnQvJdFxKAW#ZNHtQWjrWm9?tfuG4-bpserld$ddb2s)n_)+&qi( zY#NS?y$p+%ngICJ#=Yg8K})Lf$@K^<=CZH;FrSiASe13?-_nUUv2`le^7GCWw(pkz z$xXV@aL1DlneA@+6b^|4<;Z#-d*gJN))cz1XY>)eo15ZQkL>_v3_F`LX8zAV`(r>q zj}mG~H5&PrpBy^Kf+qPD%T5RFP%5_3wwrR5W}cc)eyPwG82IOHUh6QmHLv6o&PjPQ6I zF|@`LqWR>Ru_A|V(I{I^;UFUK9GVru>q zG^;iXFNR5qT4Wc=ZaJ`5D9pP)O~_0j4fR;*KHqKi zwkR-)p=khpBPcCwu&W# z9Q8|c*M#HwaUt&4F&oKm0&*bDysr39tYv9}87UX9jocYNo;rB+Kd?wa_c_w@%Hg=S zTryO4vsv;lBr?Sf(hkMHH?VC;h|FvUxQzL?bk~L4{{x zb@g8l)O`1_2LTp>?5eNN@y}{oqYGRWaDro(HA8N`$nqa7gVMF^q>f*91ES%NyAp)- zw&i2`#woy^Z7V@C+4p0??nx8<+aVkZnc2QyZM&l`()h1N$F7aVgRmoFajojQb{v8Z zu&mN##Z5V!uECoD#W9)`z@JaL3OFEMw@hRpj}St$j#JQHkO+Vhxm7lYblZ>C~ShuHb7N$rKD5sy~iNcVG)n(D)IG* zi>2-sH;n=O2bWk6(x9q2T#I@?BM27LdG}Z*##H*{%e4+Eymp86;Pr>JF@}Se2FLy( zohXQe^XFOy`J9J+ms?^d>&8Al805NL?Z#X7bx5^SF7-Xsri4bc z1&yhghA6g-$(_FYR{FVDc1aUlq>rkDv0a^$+=eF6lri)CT5v^!smo&vHts4XojABB zlVuz5a&9jeDpOKaBR_OtNFmGG}jT6=C&o4<_F^@TxEaCW%R!4yca?g5a z=jWRaT;K7_R$7!)jl72uV95^B` z^3!*mTnh`Zzsa8CCw5csOLlt(?YfOdM|}y|X5y6W^$Lfv$4ZPS436Gg&Xl5&1^L>@ zl6_>Prl`w0kO-KmM06u>Rr!SAB^y{#rKXk;)z~ZnnorD;-wF%q2{B>NqqNG!{XJQ;Oqz@KP*gNtFbw%da_xJG+i}IM-3xvc)6Sd7UWD&<$BrJLar^GmG zvw{vdHHE0YV03$6C^DWQ5>`B@uAOhDwp-G$wUPz1$d=(iL3lDkyj*=27R70fC=ilu zD|;PyewkPASd@s3mm2$Ad_l02KNU=_TcPPPe458#N6;1->QQU}#9GJKB@_yWalj~p z{zcjRLCAGJX=oP7P&(1CJoSvO=`!+cfv2$$ zE3{)xDLbXI+r48I&&OVI1S}Jo#PM5Y>jlL=EA~P^02(y;`@o`na zAOWFDu9tYsBB$*)Ve`+W*iHzmRUBYNR~C#JACAo-CDx61d4<=*utl~Xcztp-7W|(~ z)C6XPw?pSsNb{T{X+0J3o|aF!3a*6Wb)?iWQLi3Mr6xIwbI`Vdx#*+O8_v8;q2aIV zRt6R0rDwK3du`J(nItpN55)b=N*Y|pI;#|mxeQcE*V~%k{bQ8o3xv{&%%9jo92uPJ z^j$Fdo%NW*&pjho=4LXtAVLD8L^c!w0dj$&>~Aq!eCI)Rl2LCZOr#YW=*R-nrjc0% z+GUA;I-jrnQMtmLwGwl+vzFN{z#pulJDOJ9H>nd`G)xOEcxH-NqI5M0^+`KCr6fx4 z8v_zW2Q0T3i@gh&^!kS*vyYQfEcu}jLr9s068qYpHHsgeCY7%uC)DN1iQMmIbN(l*jJ>TW%qYlA<#jn|tax`!G zN^dgH(KeM}Nn3_?B?UB9Okpe&B4pc5b(T|v2P-!Od)Ckguv8}FgCnDKA4P5~<8_~R zVM4Bhus8z*POh5uZnzfF1O_XPWjGF7bT+EMA=gJ{J##0O94PZZ?&iq}aWL%2UcSm; zBnN+P3WgQn9|*iI6O!@RirObgGsDlT0yn#D-GM(^$&VrTur6^OJT1H6z9*H@Ql( z`Xe84dpM@5ZUXiLxoCy?3HMb>4XvJ`;i=lZSeH(gOmNKg= zDC$a4#0O#qeldFap;4k|#U9lKDh?@lR=;SA9*QLliZT1?->mFj(YQqqTiskm&rinA z*se@gX+)SbXsC1NfJK7goP9aTWlb9KE`&wPx-gM#q%T2%omSgRwRSNkD~z}|0_{$E z7dPd@y#IwQe+7N;;Mb>*K0gq}S9SGVcqeGmnjvb*CUJ>gjT)+ejOFRY9p>~&<(esG zlcLa7>*xw-#wkvj@|uH9DP8SzTZ{85GtAPeeRPZ7RpaI@VCT_`tZfyuv)}j9W*Oc7 zj^Zs!_Y!@o^}~7}?GKygS&Ci@R1`jseb-y$aXI9BCx_!_s$#ZiYf9~sELN0vFGy!U zU?xk}%w;E##PQoLxI6l06RQCsST^3zjdXU9nyU#fYf9#exmDPea)Z&Aw=dx>oRI7Q z!q)%&0Fm2lp6&HS%}>&$+D+)jHxQ5#nRAWw{df_Ls+NCv*3+*ECNiL6!*Xc$ckeGS zikD^E`b(v30PwMpi>M|DbDiK1I$@;B(ks8FCC!Ly!7C5v#yCF@nOWyx z%Q-a>#y`k5g|CHv+q??$F$mT^oSlf$R`%T#F!bM6;-4~6yyi{#B44VJZZWGl2I;LO z6aAAsfWu`}qHaYRV1j7HskBhnJ1Ni-oxHf}ryuA!(NXHI=+F)As2dIQ&q!GDZTT`= zRwVgSc!4UAhnL6AF=@6x;MF`Iv@h7*!fAW{slqeI=i=o?H*emJXuRSx%`)!Xu1qdf zdVbz#7wqB%w%5KvCinY+W#uQ-*Pj7#NNMZSiywq_+&XTor?9_<$FIK85n@Q)>%V>h z1ljSy8`&^b{=N_-bsRXAds3ys6+HHl(K%L9&-n}Sl~DOkS{ayq&a8OV*|Uq9D|@iK zwZ@ak^_r`wN3yxJ2f&u=Yq?L`vBhq43t~_!h)I}8nz~E{tz=4xzN$ipxmo-8600j_%(hWbF0kFb!JUhWzjOn-RlzXzNLoqv0C+ z%g*Z!{XRFG9lpM6|9}6M|5f+=H@g2H9MS*z5C0#l_5XuNGTFVwYt62;!1X^&R!#yI zA-$s_dDI?;ue)x!!~CTBi2i#*=e@KyY%J*UH8CtW6ZMtNej5(5xq5$#G(i^>jp$jhYc zS2ya(3gP<{_Ap`eyu)(V{i?Xlu=*`rXF$LA-)nB?n@^oAtX|e8nE%0QQh*s+#juOy z-#a57?Kc2BH8xKp!z)1Z-?^?>U@c|M2Ficn6KTBwZ-juap6vf0sL~LyHeTh=`~Tq8 zzVHC--!Mqj60Za!|4wWMz}jKT+yBHX!{Gy-77{ig|6f!<|NXwb>43EcXzs6FtN&vZ z0qhSt5+J93>nAq-Z#N4J%Dxg=(|zRn@6W;i^TLqAZ??AIbmy7;`>)26`AWQ<^e*Gy zZWd7Fw_m+%Y~O57G5q^i13>lwQTT|D)PL}5|D(SDI9~swzW+Ed{}zps)>%N zA1(>`*7&FVDxUeD^TlBfxk)%R=>Gt{&ZTUNBwd_GpRF0|<7kXD5rtaj}d= z7W#7FNc!V>M;T~vJ+3ugr`eX5Nys%ue9)6PZuku_;YrhYqnBG$0d4i_Ua$!BYA9>+ z>*({sFmwGv6Aa^`v)=r&?H1VNnPpUezioVE+VJQG9eS}J5uw^l@(Vx}+)r&*m0*dj z&&^4nOjXC@BzZBKT-9VK>Uq*fX5Gt!y!V`dRmeU`4V+hRWFD`#oc>%V%7){b$B;Mh zR7^ecaAQOp8H5dfox&2@V0gRh8Pw7COylZRropgCqt(^ZhrTi@$9|PjCjEeFZxQj%lcwt>kTHywJ}CV$Ly-+6`pfW)%&Y01G$h5b>Z*>%6shcWmk7JiP6Wa7bCb{LMo;!&j>p zuKYoSy4ML4M%U;fOTgTvKY(N~(MZ2;w;M~Wn!d518x6|aKIvFvJGN{)Wj8s^ z7vAK$?B%^a(V)t|dTbsxI&J{NEDBPDB{sR9N{wP5eYt|&d%4fWJ!6&*f3(d_Gv=s? z&%a+Z9%XqsQdu_MY2~yL2uTBJ--Nu>8LSbfHazL+ik`31C#>mp9@ z^vAWPb(hOhN~{q)z&X)Rrx&vJ2OIBd^6MNtgA!NmO=~pVbYlQIP}QZStp|^1hrx7E zowAK`RO*e^gzmTlmt8M!d#F4V^oiDq@KrJg9Dr|_fMR$g^?T{p8u72cdRb-2H2d*< zQDInid{u`BeMwuGVP_F=>~6o)tHYxdf;j<9iRhDP&dIF9V{(W=-h;SU+5rY~>eiwj z>b5%Vob%w^; zR*nDM+*Ko&u;WrK5RKoIrN!nizVq(*>V394Y$F}AD7Dt{bS<=w6uOxnb3o+z#A#VD zskoa=W)?CQbAeYQ)n0l;f@M)s0Ul>Jn=?$&Lbku-ts?R;KOqO==v^JF1xu?J<`33$M%DRrJ(axA&VyRl%rFjA*GudMcg^ zi$`Q!-!eI7cB|0~#T|cJ(bUqzwARq>aNP`0upqU+ z6m2*64BeGxe~B4uWyhX0L|>KPquUViWD7rp?!%zpy7!I-##EQ4XFIG8>$bou+To?6 z%xW>5H>#7G9_*Um4sTTE?H4c}nAM15syxfnYly~5+}G|;#O+3FADs%bWFHEY zL_Kgd%5wu44)&6NV>Sh5@$<g8;EldQNGYhILUx~I zTPv#NQaCToK^I38XaJE^W^|`=kn2xd=t!bYovcw074bi_)4r}`XIb|=}A zGpA{IugGqUJ$KRbvZ$r?+;MVOM5Vxyn;tX{)jp?Tw^&_e|3qM+;Xi2obGJe0R53W! zpZRr6{{0RKf)Qgo2w=PxVa69)2e9BmU9@iL(u(nq=W6cg7}UPTVIEys67_*bOs}NZ z@7&J6-4Au@=`ZW1VlO@uav(wp$^gj@=gX$xwpTfc9RHqfO8wGoI;2Nk*O`wXN@9T) zhG|9n^g=D~nh0-sV~!K#50(S~Z4xQT^Dz`(wlG35{UKo0!3rqofub@NmN3qqTikS% zD9&D(1o{w`$*zelawXCp))Dnnk1T7&%)ml7vI)YQYl=ei)pwY2nfl5c;3c0y{Dr_& z8wCQ{TXk3c7)C}iS5Bi~+Vx>{uB~t!8p8ggOXsrLlWmlKq0jFqa`po<;#O$YsxvgI za&Ke-Ol@`-Oo;>iHQ>yPpw_NyWdSa0)A_U+a|%kd8$>$n_J@wvV<%OdG*QzM&eIi_ z9B0YAnziRjV*q5dR`w#x-?E)@DyFz6)CLPO9Ek%c5O}`5o!g1y=x)dZdDQP0v$}Po z&P2HG;*HKmpU1FozH>QV^*Ac!%YnD_ykd2Ui~u1lZnje$Hpy&Eim8fEiqU1S>_P|M zF&i_6c>+^ za8XH{R!H^a7uK^cHBR6DFezv`uii~+Tgr;i#qKf`8D~aIV}6ZGLb{6hI+&Ur`Tc9~ zh@SjcOn&2uQeLtTal4agFNf^lW2Z6Bo0YTG8#t@7_0u-H6O8LMF#F9(+q|Y3DCA5r zsvt{R{*XDL9dmuxe2M7hGdzTDDkY|Ro;+{nF9Li>*K;LJb;$c(Y@8{y0yU7($RL0#yD_w|24n*uTfsDiX$hyznEj-#+FK z*~D(Li{Cv?u=JqN-f2`B3E73G#z@(l`nN66CxNO%mY@`v3E7a5--DqhgukJX9EuvS zW+Sa-naXf2ULCBgxnt#Lw*E1Hg{60VSl0^m`hwda%{HW1{yQmSUdqx@B66t4W z9Jg1v8OGMQ>&80UaWr}dC!^Z&Sgto!Q6m&W*9)?0MadUiekbm)rI2j??kz7q3e*_) zDDV87lSzjRi!A<69=fF7SP7%EY>B+{`yWHp;mWO7$;w917f%B^kfz5u=(O4Em8mfT&m#RHiS`suNNtSdds`<<_8b-Q$IhfYyrqD>yAWX8w z=*Z$IEz3XEHB+x1$Dy;itQE80btNWB-U(x6=I*{n7@~(a(7xIGHTFHz?Wf)EZ(tVK zEu?eI5LgHw9z$6EXsPwWV+ZQ78skem92ls*UwUCzOkZda!7Jegto=mTAe8-!q66L% zsge`?Tp6Ft3TMNjaw$TX!3QK?%FXANB3!Js#|8aMx9-kC>%4PDxC`2 zsTLgB^u1#Ywr~9FPl=5Ycgbnr1g*=RSPpeLZD&BZro*g+b_A=e-%!uZHGBMZ225rr z^OaX0amO&snZ$iYp%4ZLAGjg<8XRA&N@}H%kf5&hpk(1|=`zvBy`E9z{K+sliGB(8 zE+nC$O>Wxwk!Ivo`0t~9G=SeDP-UJDfM$+(z@t7laY>2%@^~;NPSg`(D2gQTjSv%9ak>PO(|_V9VBc7?8&L9FwYpG20&28uIKH(uex#|U%BL&aPza-p z(!;GUMEBOYEm@4{$8JB7w|2+`zsfPyl_lzXdg>XXO8XRkv!iBmP_O@o9SE@F8q#sX ziV7t)K|V|l(kH<>HQhao4k{OMY}F!Ru0h?um@7d0$pE^hmL%{Rkm!Z=d02O+=|+w% z{~`pl*F{zFeK-Hqy1lQwp|RG>wKRc&*qG=nUpEKRIL&n#3Hw($MCX)s90O~D1;7rco_X6UX+U;tzKJq|;6 zy9z=eCT{TvoN

L&6?TlI!&$?n3GFpD`nvp|+{4^j49am6Bh=s|60vS~Kt$w-Z?J=uh z6+JYmhLi!#&_`v0q#15EDTy8L;u{ZDg?BGYTBlPS>)eZ&v$klcsRQfZv6ws_=yCkY zz6}@w%27!;;prae&y6FxptNzX*I2P|M_yrES?e;P9NAbK9;2C3PD9vbve`y;fBNYo zO0m|YY5T=py z$#<#j7fLZTkS7UT^ccaml7P6Tcr$Ykc8q51jiV$wjTzY^o*v!UjCXdEGd!a1v@IiJ zWud%(3uJPzmyf!Ph-rTo6WX}7?s<7MnQl`ZHn;YUv1}f)X7S=%D-Z+8w*{Xl>$JBY zUs;;EwcQCnK6jQ^2`Z?2Z}{;GvYWl1Xo~aIiV!-ILsvfa?mDFow{_na0iAFj%2f1; zUw%IvE6P<=fB{wiD!`DddaeX3xuNA_hQbi`6ZGTeb)+1YTg#*^x%e?CS~FqWR0uuP z%mrOd(cK}J$I+~($QkjNX`Hiype`Gdp08~4@bQ78jPV*+oFVK9B@s);MOgpBAaSx= z`}S=Y8|xr3i3?g&Oq9nYalk9HJgN*>kNpMKy?sTf-}>dv5)BJ(=j6tha0s7R&K2JE zw&;H3=>e=FA?&tVrja{~#6#Ku2%^q%r3>XdYvVhL3FwLHACWi5rxGrua8ZuWUEg^z z`)U`e|3xyd*8RfThEuL@N4Jp~7*w zTtBS`ffAN&^0)>vA?0}ikwW+;X+e`Zh8TDFSVnauNef!n7l^qxk05g${%Un{yw|t~ z5{hjpp=l9SBK*}Sj!;6bD?Yu%aleMAk3G3an}uUIx9 z@f0y2XQURnq@~zi!g@{P#p8%|#f_^0^^VTd5W3EO+aupjA!^ZimtVfl(SSf<=m?$F zr@Bfl=D* ztny6y?;?3qWGE=_Xm2mLyGnBusAYUBy9 zLuIwLXPyI{Rro-C2hl~&fYHaEuTUY3iZcG9*h1mB{H&uGo3q)3HgF#W6)^?2`&ys8 z-`s$jbptpY{*<Zs zIx+q|rR*#}gP+>xaf@LmMFaMLuax_d_!<~r;4-IT_l+Y|LGoVT6AVF9m8d*#du$C! zdboz?ASRT)pPof;f<^YB<(jj?I%H|xLdC;zij85S=M!#Yssc;zilMZrs($9z%a4|1 zZmzW_YeWb~eZfj!!(LXm40y{+rL^Nxz{<$B$SCzts3XK}@l1Kv{5yPA29ueB-;FhI zW;|8w0f@s;;5uyL`jkL)FQ9KH52+m)l`iMXpl{sA{#h%SvT& zu7G}Tc<}XFuBOMfm>o>iYF;#UbA29vKudiv7407Huz6-okL|f-DeA~luWRPc#DE-*BS>#$ z-z&j63+5$0?#f0Y{Eoa@2*pZS1&fF|v!2+D^qF$nf6oH;XZB1!-V!V(9}EQsGC=al zHcCB@{^w=WEqMi1$|bNmp$#`HJ>s2-yj5Fijg-w4iFkl72S=vIlP~(uSp4cwKCQT! zZM@Wk94(AL#5#f>`1}b55U_9^7o2Zmx^=RLVqXoUet4&KzNwFntjN{YF_@Jlq8Fyd zeL!>hqmsY};~L!4oypXi`!IS3>0K!p7RSVao?;!+E>kEBMV>gh!BfH`O@f2L7}kC~oM!lxXKF#i4zoCnO1C z@-Wtl-?z0u4>{%_R6!(p!rss^VLrSL#DyS=R-v_OzojH;yAb)h**fdqcJ!*i_pr5A zC*+y*Plp+#4!C~m0nNWzvN94Z+sVZzzR$8PZ{O_;@vEtMYh5RD;hMT8Z5A4~Lw}x8 z0&*WLn&0cKX{m~Mg5|lY|amGgGuOW+M)HJ60dD{{Bssa@l9{Q538Va?}4y8Fl+M7pU$A^uzndgMsp3GGGC z`JxVT>^HacK*E!hI+izp(rMs5-pqO-e1LU!*Zs7~%!-F&=eN0f z$AtCKW3a%%`j7p=A3hPaRC*z194{?SlWKhHPQpLkSURS&{g=L@8zP#i8{f7k1_hA} z%5ver<1U7ULMcZ*Uy4XIvcx|NhLdJIKBEyslG&D|{bEodsU)m+=a>fHhPp*LL4Nph zZn&_s!Go2h?#*_%-X{29gAH1C%n$kgF*~n)6=N1I$+Cg(B6)oPH=Ts}szxs0P85VU zQ@%&ROHc8dw)KYaz+CGGFf(VT@%FE@cUUI!Q}DPI|H7sUDzx}^z=9@f>4{&+s;0-kd;b|If_Dqy)$YB|(+*JLwuW?vsh?P%%}%9DbabRioUSwM;s z#YK-&yRFKbsZS&gBvw~_QD*1e?gvNcF6n6&Tr3Kj^=fgL8{PD&mmSLzkSOSZ-{HgE zcbvbRh$-I#L_3LP@bN+xqt09|2KJSVEQWMjnny9vR8YM8Tk)ds>GUfpNQ_L!Has zb$`TeOy8+!M&wcDV$`oM*A!5nJoRo$3`O-`>o7cgJka*pAlA$V)1#@z{j;{WQmwMe z8&$d64~#BHPr06Ke(2{>?wBDU#)_-yxgS$3#ccW%z^8j)hERs<`q8AK%4|2i&zL$> z>gJ`XiMOd9${YT5#%^Afh=jKy@bQaUd-z07JckJ_TBlj{4-BDHnF)2grA$&a8|*t` z=Fu~g55vU`gzp%bQFi_IT*qoA&-yM4D(DjeSh9z*$ikJ@_X}>gBN!#K+lxYaE;+%A z0)E8jHa>k6Q2{N%-LfF^6RB^aCbG-&^TRH{kq8vhMiOerhfozcjUq^{T2Y?xa;pEt z&r43$GX^EvdgPV)J6$K!{7G9n1+T72@>DK>Cvy9rTmU?$k0>;7+=G|bhODRZFlEWV zr(4@?9z(1M?GeH2-Zpe{O~vIg@s$Y;s`@S_7RqKq1PuelZ7a2Oj21 ztr&C04N?{Rx#JB2dzSeOz1TESkM{vMIqF-ReGoBxF-~2 zyI(8u`96m<&9fv!Q`^XN`{JjzMzz>Sr%;=z;psDaGgeqhY-R@g$#39aV>Qg6&y%v1 z?iSfA)WhMkNZfKpJzdtf(*(_7X|m7#GMNSP(}zC3LlDot-WZ3dwC1|%pBFumggqUM zUQB9g`2kmoKntbML(~3N1H{OaSD0&0ruQi?Tqnf%Yn+_~PQTx9Yhgk7u;S}NNhn}e ztUAwEU@h%x+=bN!rb$@v276Uoy}US^H0KAjABD;VU5S%0yyW0Bl%lrWk*_ZJe3y;DPm2?3;u3?4@LI7#>K6 z7Nh81JJsvRpvPzNEkaaGq%B7UXEPxxw%nWDW+OA%=&wy@a5^A{tX7E#dYQkd%4%_5uYvB4f8BKlOtyP@rTxV`=Ls?OQH@b}5p*`^CCe_hVbD ziU_}NP&YuG{PSydnJ)7*1<<>eUF)$%oR%ldD?h>Gp>+Pp{DfxX-+awG=Ob9xw$a8< zEVh!mI*1(CYVC6oMb&=|A`A&cCin3gxd-_z0ZnphSIF0XoSj?;dUwCTo}2p55*=Q> zIk;uIZG@;xnZ?OWze)t8$szZDmObi+(-zD8B*ICGpDhl|f|IW39)J}wOq{8}A3enO z_tBRnm*OtT9*fbS0RO}Jkh+@0GU=%VA~Bl8RTk+<%LFy{VZST0y-GaowMc`uwZBa# zwOGYH)wiLfDNPJZ&h(LtznD|6Uv1AW@<>82NnS@8NQ4l-7iX8>Nd!`auY8$b_jf)7 zh#YO?PvLlB7{8%uJQeFo#qxQyKVBf~Bj3XPGog`+^_20VJ8n)d06V)yn(aappqHu+ zxVzqw5WUlQsnX+{k+)IN|@wa*!Zi!iH$q+OORjMC|4d5tQ618FcSy>W!xZA5OOP@Imcu6Jk{E2#6 z``bqFbG6t(fdN1TPL!sc5iY#t8i{=mI?-~e?CM*N;ji(k(NTc9c7Ta#j2MNshWA;U zT>YCcr-Mbq8y(BC?Cx1s^HbgCIfJcG_aj-Q^%K`Qq$k3s|Sebw2s-4RSv#Dh<6FtYFBul!YZh?)g_qIeF7< zH%s@DJ}_A-9^fEOLRclKD-)UEKR>W-(P{qJ!y}!`;IIEKAjAH0U~S1X(Xl{=v)f#W zxP(e-jpQ7nw}~s-vx2JA91!<)V5^Pea;*T_HLXo5?rAeolwS&d=E9tC%4VqBxk4V` zzWE?>u^xrW!u+iQG&ddo@XeGa5yZ4fL~|X4Gd6l%HElU0Im9~a8dPt zMdKLh6WUJkR=4qvIY`T4*VQ4|Lmlgae+!b2P;{kU;#(Mgs&@1^ZU|qLB0N3qH6yea zcz7cLR&Uk1_%X6I{xxIE1U2b7YrXRN@S~EAhfa7qoW5eLV)11Xv9*B`APiYT>8VTG zH@0JB2&U^R$_J_A&~5gqGqOG^fgfY_f>{x(n^+8LX(LQn5HWkCNsDFZ=C%e%zL2z3 z+CV|%P!I||N{vt%Oi7`$0jl(9}dB^wF$)#h3JD71%~J?`w4t{*<0R(W(0ow8 zU!eni0-8!vu!n=ZRd}$zsw#h?)xiAQ5eFSS7alxJyzC^v92q+y-XsyIajv^&xKnFF zg(9~&AI2@cMr8Pce>-Chg)o<+7a;kX;+VUQ9KRK7&ljxa_;Z8M@nyx`Udn?_xNOKw z4sX7fTr;|nJ&Vs&Zm%cs9mGJHHD8ZAa}VXo_%<44BDZZs`bUVT-pg+~2dNzkZ!7u^ zU2pf(np~1eem~_nAfCQ|;+XNvsnA;eJcqWYR2t&s)p%iu-u3Vx7S6+#VPd*L8YMhr z%QYl)vFx?dhywnp>@vM)GYP|A&Yx?j-v4hxyh`@)Sh;ueOj z(FC($UHoF|@k4>(jJtP5+DLkC5nOR`7~mrrt;Sp}9#bonaAq5Zs-QBiAJIx8iv;o( z*tcU|OD!L~S&u5cxCby8dByNL8ly!tl>gM({ivh>E>$K}<+6Mni}Uqcb(qsLnwiJ} zE?~h)6`Bzj3ZEJ2bNO*EPSO3h@G4928ueF#e>11g|NN6rgZM(Qg~T~MEL>ud z)C8?`HcJCmFp*K+sn4znd&P1!3Spk>CNLqqwD87cnPr|(Hm`K^P|uCguBhmyWLrq$ z_mIW98k_X=u{OC3^h}3J|2Ti-;U~}i0hQE@Pqp=Rjau&B@V$djW+==zblJ+U4kZ}x z?RQBu?p@g&eCr7!pz!`%Nqq*=b>F?kA8obA>@nD9qFg}uN18BgQ`AaK&_vXhPp5YT z5BeBtQS`5IEsVKr)KRH-!3ljDPxkySoJvG!d`Kqm-!hKaw0hP2Ub+=XBEHf{cDf;( z#ZH~>;-hlwms2Wo#ocYi7B%^y3CpiW?v3fIZY`aSByhJ#ml03%y9LkQH9pK-`SyGn zc9x~9XeYlhr$~%my7&RBCR_PRnPhs7Es1zoz(Ms^PWHPGVf|p9#wr%8y=_OQ~7Rxz; z9MkaWqhL~?YN99@cl*X=PqHCCM3`!9quq%s$oNUsaF{_bY|02zc_1D5%DDkNBD2H1 z0nNt`*1YbubB?yTZ{=5U-?)_ zHdBjHk!s$a+mGPr$BT^Hw>a`BX_iwh&*gj&mrOLVN#7zkJIUkqq)6JKTvlF9cIGXT z@`GjK35c@ho!xA6QR>Rmh-x2{22R7WPr8G&66X(O@TLqxPPPKqEd zwd*ru7^{|F+NC>3;ZAo~_c&G9%HYhk!v&_T+HFb&VXCgv(xzwWY$=ydK&kiLk;ECSipDPZ;~91 zgbqGPaR4bfWhuV`^0f7}oxE zcvC!L9vY+Ui9J%JUmZts{|nP}nhFMCYB}jdQGcBf2(&>V9W&Ihv81mF-qKqfEn-p{ z(Mn;2pEHz)E?p=7{`VUnbGibwFd@GaW9y)tLUB|#tqU6mXLf5io z!i8@;&XNe8MBo$J<#?i)U; z-R+w_r;#^hdB>17+Sj3XoR;$)r%}vYEt;QN?o1U{^U@f^-%;?{OCH}ow(pwB9*bop zeIQ_^`CEwvBB^sckIg1ZFpcE@qUF~`T|_b`WThw@tc zeDTAMy0PRT^K5Xaw`X;K4SD=2qf%5_*=wOjGWg;CIVP-H{h6>zSZ4~2UTjNysAp zehKg1TVodi`F!`z^KiErsZYDgu+I-U2nNIeT%3jY7jHuX=m(CkP$x>__RtTBnNMIG zPc>2I931s<4`HrmlsKjCs)_v9wXytjjeSru(_KTiPv$D#2fUI=05i87XzDu1&t4nM z?|F2esf+94j;Y(gDifE0EuHx1)rZFL_y4*IJB&pJQQHzHD@;*>El$YwX(QwQNYkzZrC7@GH zNIOF++c!|x$@37rXI*Km6qm5Tb(?!f{|2y11VZH1<8RF5b^O39a1 zYD(uyv#d=`&#`?_mixDNNp0$ZMj)Bj;;}@tP2SQc9Uw7GqTRWe{cz-Gecspe{{EMG ze3$#g2IbU^B8Jc&v0EVpX}?bv!`Y-M_e;{smBrBP>&mBt+lfktyP+3?=a!buFS`} zUFBvV@DeD(5Be#n&Oo=_BSKoH^rc7<#>FHqD!wnI1`)R`)uzA^c{;vCPEnrwvZ>{^ z5-q-w6wrwNVUB%H-?@bL?UB&T<(-b^eostB(WvThO(&vpTOnm7ETx1^J;b1Zj^$qw zN?kJI&und}+3mzz@1h}Qtq3Qv&wanBSh%~%N?#R-rw(2zh0&ByjdNiB@o?64f|BAR zd;J81C>&ZKPZ8o!rMH9Rzd_{ID{5fwXEai9Z`gK6Uz{X|F)-hSL#o&#s{Qqt(yf-( zXn-$U{4fz9;ii`CAg{|^a-T4OizSoTm|KGsO(27b+Z^eCH1Bg1)b2&L7;gQ>t|Btw zyC_N%m#}kQc4?C=Sl(}8dG~Y@+QB!O=?m1;<8eX9L7I4B) zQ9Jq3SS1_@AcvDQlyM#hn&kf$8SJCeDun^f7*13@Fs|4T-5<+kLXfj z7>YZ`(Ol&+IdF`ew3g{PjVCTh+NDJe8Erb)?Y+FnqIAd}W7`S;*Y34iK2f?g`87DT zK{Invpmdheh+SVjIY>U}8s};~NU-E3l8%@T4^+(ZRr9?er{zA_9n#}%`oNr@s^{q05B-d$2f+R!iXn(1?%+r{cGB~(cgim%aFDRbGZGz zmhgNj!$I6043cu`7j`-_pz~OGMT79BEztxhFy#UNi*Bnc41+i?C)+*@3{dRh-omdR zufFns!Vje3$#%=tIkDC4tOcY03+L`r?+DJz*!Wbc1a`knBiR)D5!amkJImI zF%FON{o6M5C8bzJuoN!FhA}iHI)wo*Z)fm@7Gd%m4UNeFH7PORm^H*^16Jva_7TNo zg8@zsOK)jhx2)2c{BEx3=q1)Fryg>+Q{>a@b;aP)W98XdmEFs7^Q7}!v5l`_KiU5J z5@UXV>3!xoiiYg5{gK$;kO8ChP!{JikwFCx-ZI7dWn6%xS*2|| z#$i&l2ynWP64!+sY@c|ihLO->yc2~-g2|T;7pL4!sa9jJ@_ivRffd(qF@O=+9PVWQ zE|apvHJ`}7!uDO}=;oXK{@F6vb3=<4WV|5!F;p*2a7M*o1GjISh}jJ}8$0jtez$#< zSDm8_Hyrm2>rDf*B>@pZ5$1CnYUk4ulD=WBm~gqA9YoiO$5bNRW1j2qh*=~K#PYzb z6*_o=75D`4&W@IK>dPE`w<4R2r@hC}BuzNzdoPO z^Owdfy0}#!LJw3MqGA__>>uC?d9<3GwNo+;CosiS1pQaAs~l(c);)7izCv3^Hm~@Uo<4)8aM|vlz1}#B#5|ZoJ)fJ2!bQ0 z*^@d}J$m%nLYT>3pV{3&WCKEMqa{B>{sTrkMBE|yoxG}F>Y7!T4>i-4kv#!AvQ%lG zA4Jg4W3s(v#GA*c51_G%oF4Ah4GFKBz7PFB=`s#T(s|C? zv_x~MJ9{kP&vpp*g_=;QPE}&#vuT@Y&I{T2Z{NPkxSsW1tv*j z>ek;h55_X*yqNL!Kh+ZQhZIA;K#ed6;}0NP4f7<}a>wc#FEjOkL{(q`bT?L>o>;F{ zG-{M%+S#3%&bS38bDnTg4ytRjiFWw47sxTnLl@b)mD(7|^J%J9xQAZ3J1RTLu?-rG zADW{(@wboXR)P@*vno`+c`-jV3ww(lwOuWu(F5MWl4TAYs06n21*!M9mJF->eL zut#93*&t=ZE=_`4Sd4w>sKQ-qsL;C4l?e-D1Q1<+gWoB*WL;r%zxTNn6OVxJ?e%WPdJl)9FnNAjjJC`m~rL%--ASJ8-*n~$ZI1Wqx zXB_-p*lmc+(e6K zH9{JWyOy5u!x07Xw3S4ogoWE)zAx>_yO|{_9)yE@r8%b?_%Dq;^xmedGrJiiiVkR~ zof+o+%(n8MJc`zXKgJjgCm)om+KK5GlJ;o^`|yOM^Os9s+Lv_zbs-zXp&oe`NyY?1(MSzneK0b$q}Iz z<$z;`lp8)B7A>)bu!8OaV;2etSCAq-yM?UXRde=5`-Fd zcv%`sk9={TmFQ>79E}dR3p`mM%*Cj_DbBD>A4z)76A_M#mk2CRpoLH1h8cyfL^|l- z2w@Vn|8TI!MA*Td=*A=Bx=TJ=yrXkvEd6ycg^HihHj8QNloBE5DmFj{0y-Go|>Zzqg`M`jzySChw&IRo~x| zDwMkUHdY0w>h+gMOEC9vN$9aQGi;^12*R)$U-S?0vsLi`T|59)K8)$kOWV48PUELd z#Cc^LBdQ1+{<#gbG6pd{tChMl!4{dZRC5HRrB&*Pq|a0^9Mu-iy5EJ2LJ%i4uJLhp zsO6fQ;Gxp!cg|QxlB&tUe-ku|k*?eu_4{|=zj3)L&l^UpwBI&5Tg9!K^C6WD?EsQh zb^}qo@3DZuKN1l5Q`f?m@DM^!4|&{oJwOqRc-#iWNwR}**2PPxrkN?0lA~04ZdS zm>wG?VmhjFHzdlEJ52Exi|fBkr31RXPhfRAio^7Da{GRngZxalE-NZKkK-QIk`qxY zS8X;gvQ7wsxZ8((a1VwE-jy!5h4E&-ikK7nbR`-3lP70r*FeK8-Mj<>0j)J%9Zx91 zVDZJ1CeKSM)8ZXz25W#t;VkPq-1hOTd`Q$a%w#(my2^F3CQeJMnJ`kkY0u53&y(eq zpmn6`8{z=R2=MU7=g3*|kCwt*zMioV+C!o@rg(gGwRr(ST^A@vYr0sU`EQP1?CVn} zR=;JyWd1HO!$>YYOMEMY3M3(v{6qa98_k;>Eck?|JY9_mGc|H6^w{`2;@XuY(6pDY z`1j50@u;3NfxXYfs$17u9A^K9584Qg zhWZ>ui@5u1`syV8i9I(vq=~JNJdc(1N*~$rx3M2l=Rm#$d!vEv>~01ebz({;M^1`i z+z`E6bA&n$5xHY(9TR-QCm03(xl$#5t!(#YS2PBw-NpSrJc07-j%zkD4av(Vlbw2A z@7dXt&#ODiRA0yRb`D%o?|&-Hgt=b~T9-7;g-k4W#VGHN+$tZNGL}uCWxMn5_CjH0 zh)fp$>cdw`2#s_I6aGFUfX&Noljr z4A;(vEo=1GaJMn18q9owKdz8g`McOe%{Lv~I78W9=k*Tv>ZtK%IuJ0ruoOkD_L>x- zGb3g$v-W+;H}#oOVsd=troBVVP53eR9*I5v@|?SQ5nq_#eH=Q~M#EXc|FYG20|G z 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

|tQ0b1)27Ub6erovH~+ev~7qT3SBV^F6f1`o8h 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

8pTi(vV*?oHFd(G6lJ~AY$@&gpah`Q-G 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 Date: Fri, 19 Aug 2022 18:04:47 +0800 Subject: [PATCH 13/52] Change model structure --- examples/rs_research/README.md | 16 +++- .../custom_model/iterative_bit_iter2.yaml | 11 +++ .../custom_model/iterative_bit_iter3.yaml | 11 +++ .../custom_model/iterative_bit_iter4.yaml | 11 +++ .../rs_research/configs/levircd/fc_ef.yaml | 2 + .../configs/levircd/fc_siam_conc.yaml | 2 + .../configs/levircd/fc_siam_diff.yaml | 2 + .../configs/svcd/custom_model.yaml | 3 +- examples/rs_research/configs/svcd/fc_ef.yaml | 2 + .../configs/svcd/fc_siam_conc.yaml | 2 + .../configs/svcd/fc_siam_diff.yaml | 2 + examples/rs_research/custom_model.py | 75 +++++++++++----- examples/rs_research/custom_trainer.py | 4 +- examples/rs_research/scripts/run_benchmark.sh | 8 +- .../scripts/run_parameter_analysis.sh | 5 +- test_tipc/common_func.sh | 5 +- test_tipc/configs/cd/_base_/levircd.yaml | 62 +++++++++++++ test_tipc/configs/cd/bit/bit_airchange.yaml | 8 ++ test_tipc/configs/cd/bit/bit_levircd.yaml | 8 ++ .../configs/cd/bit/train_infer_python.txt | 6 +- test_tipc/prepare.sh | 12 ++- test_tipc/test_train_inference_python.sh | 86 +++++++++---------- 22 files changed, 258 insertions(+), 85 deletions(-) create mode 100644 examples/rs_research/configs/levircd/custom_model/iterative_bit_iter2.yaml create mode 100644 examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3.yaml create mode 100644 examples/rs_research/configs/levircd/custom_model/iterative_bit_iter4.yaml create mode 100644 test_tipc/configs/cd/_base_/levircd.yaml create mode 100644 test_tipc/configs/cd/bit/bit_airchange.yaml create mode 100644 test_tipc/configs/cd/bit/bit_levircd.yaml diff --git a/examples/rs_research/README.md b/examples/rs_research/README.md index e57b2d2..ec83444 100644 --- a/examples/rs_research/README.md +++ b/examples/rs_research/README.md @@ -73,7 +73,8 @@ class IterativeBIT(nn.Layer): super().__init__() if num_iters <= 0: - raise ValueError(f"`num_iters` should have positive value, but got {num_iters}.") + raise ValueError( + f"`num_iters` should have positive value, but got {num_iters}.") self.num_iters = num_iters self.gamma = gamma @@ -97,8 +98,7 @@ class IterativeBIT(nn.Layer): # Get logits logits_list = self.bit(x1, x2) # Construct rate map - prob_map = F.softmax(logits_list[0], axis=1) - rate_map = self._constr_rate_map(prob_map) + rate_map = self._constr_rate_map(logits_list[0]) return logits_list ... @@ -157,6 +157,8 @@ class IterativeBIT(BaseChangeDetector): #### 3.4.3 实验结果 +VisualDL、定量指标 + ### 3.5 \*Magic Behind 本小节涉及技术细节,对于本案例来说属于进阶内容,您可以选择性了解。 @@ -179,9 +181,15 @@ PaddleRS提供了,只需要。`attach_tools.Attach`对象自动。 #### 4.3.1 LEVIR-CD数据集上的对比结果 +**目视效果对比** + +**定量指标对比** + #### 4.3.2 SVCD数据集上的对比结果 -精度 +**目视效果对比** + +**定量指标对比** ## 5 总结与展望 diff --git a/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter2.yaml b/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter2.yaml new file mode 100644 index 0000000..5b19c71 --- /dev/null +++ b/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter2.yaml @@ -0,0 +1,11 @@ +_base_: ../levircd.yaml + +save_dir: ./exp/levircd/custom_model/iter2/ + +model: !Node + type: IterativeBIT + args: + num_iters: 2 + num_classes: 2 + bit_kwargs: + in_channels: 3 diff --git a/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3.yaml b/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3.yaml new file mode 100644 index 0000000..4753c43 --- /dev/null +++ b/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3.yaml @@ -0,0 +1,11 @@ +_base_: ../levircd.yaml + +save_dir: ./exp/levircd/custom_model/iter3/ + +model: !Node + type: IterativeBIT + args: + num_iters: 3 + num_classes: 2 + bit_kwargs: + in_channels: 3 diff --git a/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter4.yaml b/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter4.yaml new file mode 100644 index 0000000..72b9b0a --- /dev/null +++ b/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter4.yaml @@ -0,0 +1,11 @@ +_base_: ../levircd.yaml + +save_dir: ./exp/levircd/custom_model/iter4/ + +model: !Node + type: IterativeBIT + args: + num_iters: 4 + num_classes: 2 + bit_kwargs: + in_channels: 3 diff --git a/examples/rs_research/configs/levircd/fc_ef.yaml b/examples/rs_research/configs/levircd/fc_ef.yaml index 6fdbc2f..b4bc998 100644 --- a/examples/rs_research/configs/levircd/fc_ef.yaml +++ b/examples/rs_research/configs/levircd/fc_ef.yaml @@ -4,3 +4,5 @@ save_dir: ./exp/levircd/fc_ef/ model: !Node type: FCEarlyFusion + args: + use_dropout: True diff --git a/examples/rs_research/configs/levircd/fc_siam_conc.yaml b/examples/rs_research/configs/levircd/fc_siam_conc.yaml index 50a8c8a..426bdcc 100644 --- a/examples/rs_research/configs/levircd/fc_siam_conc.yaml +++ b/examples/rs_research/configs/levircd/fc_siam_conc.yaml @@ -4,3 +4,5 @@ save_dir: ./exp/levircd/fc_siam_conc/ model: !Node type: FCSiamConc + args: + use_dropout: True diff --git a/examples/rs_research/configs/levircd/fc_siam_diff.yaml b/examples/rs_research/configs/levircd/fc_siam_diff.yaml index 4ab8874..704e528 100644 --- a/examples/rs_research/configs/levircd/fc_siam_diff.yaml +++ b/examples/rs_research/configs/levircd/fc_siam_diff.yaml @@ -4,3 +4,5 @@ save_dir: ./exp/levircd/fc_siam_diff/ model: !Node type: FCSiamDiff + args: + use_dropout: True diff --git a/examples/rs_research/configs/svcd/custom_model.yaml b/examples/rs_research/configs/svcd/custom_model.yaml index 5d95079..7262637 100644 --- a/examples/rs_research/configs/svcd/custom_model.yaml +++ b/examples/rs_research/configs/svcd/custom_model.yaml @@ -6,7 +6,6 @@ model: !Node type: IterativeBIT args: num_iters: 3 - gamma: 0.5 num_classes: 2 bit_kwargs: - in_channels: 4 + in_channels: 3 diff --git a/examples/rs_research/configs/svcd/fc_ef.yaml b/examples/rs_research/configs/svcd/fc_ef.yaml index 81bbb34..ed86ab8 100644 --- a/examples/rs_research/configs/svcd/fc_ef.yaml +++ b/examples/rs_research/configs/svcd/fc_ef.yaml @@ -4,3 +4,5 @@ save_dir: ./exp/svcd/fc_ef/ model: !Node type: FCEarlyFusion + args: + use_dropout: True diff --git a/examples/rs_research/configs/svcd/fc_siam_conc.yaml b/examples/rs_research/configs/svcd/fc_siam_conc.yaml index fb4eed8..3c46d03 100644 --- a/examples/rs_research/configs/svcd/fc_siam_conc.yaml +++ b/examples/rs_research/configs/svcd/fc_siam_conc.yaml @@ -4,3 +4,5 @@ save_dir: ./exp/svcd/fc_siam_conc/ model: !Node type: FCSiamConc + args: + use_dropout: True diff --git a/examples/rs_research/configs/svcd/fc_siam_diff.yaml b/examples/rs_research/configs/svcd/fc_siam_diff.yaml index fde20b9..3aa30c6 100644 --- a/examples/rs_research/configs/svcd/fc_siam_diff.yaml +++ b/examples/rs_research/configs/svcd/fc_siam_diff.yaml @@ -4,3 +4,5 @@ save_dir: ./exp/svcd/fc_siam_diff/ model: !Node type: FCSiamDiff + args: + use_dropout: True diff --git a/examples/rs_research/custom_model.py b/examples/rs_research/custom_model.py index f34dab7..27b99dd 100644 --- a/examples/rs_research/custom_model.py +++ b/examples/rs_research/custom_model.py @@ -9,16 +9,17 @@ attach = Attach.to(paddlers.rs_models.cd) @attach -class IterativeBIT(nn.Layer): - def __init__(self, num_iters=1, gamma=0.1, num_classes=2, bit_kwargs=None): - super().__init__() - +class IterativeBIT(BIT): + def __init__(self, + num_iters=1, + feat_channels=32, + num_classes=2, + bit_kwargs=None): if num_iters <= 0: raise ValueError( f"`num_iters` should have positive value, but got {num_iters}.") self.num_iters = num_iters - self.gamma = gamma if bit_kwargs is None: bit_kwargs = dict() @@ -27,32 +28,62 @@ class IterativeBIT(nn.Layer): raise KeyError("'num_classes' should not be set in `bit_kwargs`.") bit_kwargs['num_classes'] = num_classes - self.bit = BIT(**bit_kwargs) + super().__init__(**bit_kwargs) + + self.conv_fuse = nn.Sequential( + nn.Conv2D(feat_channels + 1, feat_channels, 1), nn.Sigmoid()) def forward(self, t1, t2): - rate_map = self._init_rate_map(t1.shape) + # Extract features via shared backbone. + x1 = self.backbone(t1) + x2 = self.backbone(t2) + + # Tokenization + if self.use_tokenizer: + token1 = self._get_semantic_tokens(x1) + token2 = self._get_semantic_tokens(x2) + else: + token1 = self._get_reshaped_tokens(x1) + token2 = self._get_reshaped_tokens(x2) + + # Transformer encoder forward + token = paddle.concat([token1, token2], axis=1) + token = self.encode(token) + token1, token2 = paddle.chunk(token, 2, axis=1) + + # Get initial rate map + rate_map = self._init_rate_map(x1.shape) for it in range(self.num_iters): # Construct inputs - x1 = self._constr_iter_input(t1, rate_map) - x2 = self._constr_iter_input(t2, rate_map) - # Get logits - logits_list = self.bit(x1, x2) + x1_iter = self._constr_iter_input(x1, rate_map) + x2_iter = self._constr_iter_input(x2, rate_map) + + # Transformer decoder forward + y1 = self.decode(x1_iter, token1) + y2 = self.decode(x2_iter, token2) + + # Feature differencing + y = paddle.abs(y1 - y2) + # Construct rate map - prob_map = F.softmax(logits_list[0], axis=1) - rate_map = self._constr_rate_map(prob_map) + rate_map = self._constr_rate_map(y) - return logits_list + y = self.upsample(y) + pred = self.conv_out(y) - def _constr_iter_input(self, im, rate_map): - return paddle.concat([im, rate_map], axis=1) + return [pred] def _init_rate_map(self, im_shape): b, _, h, w = im_shape - return paddle.zeros((b, 1, h, w)) + return paddle.full((b, 1, h, w), 0.5) - def _constr_rate_map(self, prob_map): - if prob_map.shape[1] != 2: - raise ValueError( - f"`prob_map.shape[1]` must be 2, but got {prob_map.shape[1]}.") - return (prob_map[:, 1:2] * self.gamma) + def _constr_iter_input(self, x, rate_map): + return self.conv_fuse(paddle.concat([x, rate_map], axis=1)) + + def _constr_rate_map(self, x): + rate_map = x.mean(1, keepdim=True).detach() # Cut off gradient workflow + # min-max normalization + rate_map -= rate_map.min() + rate_map /= rate_map.max() + return rate_map diff --git a/examples/rs_research/custom_trainer.py b/examples/rs_research/custom_trainer.py index 2829863..18e0d20 100644 --- a/examples/rs_research/custom_trainer.py +++ b/examples/rs_research/custom_trainer.py @@ -13,12 +13,12 @@ class IterativeBIT(BaseChangeDetector): use_mixed_loss=False, losses=None, num_iters=1, - gamma=0.1, + feat_channels=32, bit_kwargs=None, **params): params.update({ 'num_iters': num_iters, - 'gamma': gamma, + 'feat_channels': feat_channels, 'bit_kwargs': bit_kwargs }) super().__init__( diff --git a/examples/rs_research/scripts/run_benchmark.sh b/examples/rs_research/scripts/run_benchmark.sh index 3c66a56..42f7c39 100644 --- a/examples/rs_research/scripts/run_benchmark.sh +++ b/examples/rs_research/scripts/run_benchmark.sh @@ -8,11 +8,15 @@ for dataset in levircd svcd; do mkdir -p "${log_dir}" - for config_file in $(ls ${config_dir}); do + for config_file in $(ls "${config_dir}"/*.yaml); do + filename="$(basename ${config_file})" + if [ "${filename}" = "${dataset}.yaml" ]; then + continue + fi printf '=%.0s' {1..100} && echo echo -e "\033[33m ${config_file} \033[0m" printf '=%.0s' {1..100} && echo - python run_task.py train cd --config "${config_dir}/${config_file}" 2>&1 | tee "${log_dir}/${config_file%.*}" + python run_task.py train cd --config "${config_file}" 2>&1 | tee "${log_dir}/${filename%.*}.log" echo done done diff --git a/examples/rs_research/scripts/run_parameter_analysis.sh b/examples/rs_research/scripts/run_parameter_analysis.sh index 0f53055..2bb32f7 100644 --- a/examples/rs_research/scripts/run_parameter_analysis.sh +++ b/examples/rs_research/scripts/run_parameter_analysis.sh @@ -7,10 +7,11 @@ LOG_DIR='exp/logs/parameter_analysis' mkdir -p "${LOG_DIR}" -for config_file in $(ls ${CONFIG_DIR}); do +for config_file in $(ls "${CONFIG_DIR}"/*.yaml); do + filename="$(basename ${config_file})" printf '=%.0s' {1..100} && echo echo -e "\033[33m ${config_file} \033[0m" printf '=%.0s' {1..100} && echo - python run_task.py train cd --config "${CONFIG_DIR}/${config_file}" 2>&1 | tee "${LOG_DIR}/${config_file%.*}" + python run_task.py train cd --config "${config_file}" 2>&1 | tee "${LOG_DIR}/${filename%.*}.log" echo done diff --git a/test_tipc/common_func.sh b/test_tipc/common_func.sh index 506ac82..0690d87 100644 --- a/test_tipc/common_func.sh +++ b/test_tipc/common_func.sh @@ -87,7 +87,10 @@ function download_and_unzip_dataset() { fi wget -nc -P "${ds_dir}" "${url}" --no-check-certificate - cd "${ds_dir}" && unzip "${zip_name}" && cd - \ + + # The extracted file/directory must have the same name as the zip file. + cd "${ds_dir}" && unzip "${zip_name}" \ + && mv "${zip_name%.*}" ${ds_name} && cd - \ && echo "Successfully downloaded ${zip_name} from ${url}. File saved in ${ds_path}. " } diff --git a/test_tipc/configs/cd/_base_/levircd.yaml b/test_tipc/configs/cd/_base_/levircd.yaml new file mode 100644 index 0000000..f14607d --- /dev/null +++ b/test_tipc/configs/cd/_base_/levircd.yaml @@ -0,0 +1,62 @@ +# Basic configurations of LEVIR-CD dataset + +datasets: + train: !Node + type: CDDataset + args: + data_dir: ./test_tipc/data/levircd/ + file_list: ./test_tipc/data/levircd/train.txt + label_list: null + num_workers: 0 + shuffle: True + with_seg_labels: False + binarize_labels: True + eval: !Node + type: CDDataset + args: + data_dir: ./test_tipc/data/levircd/ + file_list: ./test_tipc/data/levircd/val.txt + label_list: null + num_workers: 0 + shuffle: False + with_seg_labels: False + binarize_labels: True +transforms: + train: + - !Node + type: DecodeImg + - !Node + type: RandomHorizontalFlip + args: + prob: 0.5 + - !Node + type: Normalize + args: + mean: [0.5, 0.5, 0.5] + std: [0.5, 0.5, 0.5] + - !Node + type: ArrangeChangeDetector + args: ['train'] + eval: + - !Node + type: DecodeImg + - !Node + type: Normalize + args: + mean: [0.5, 0.5, 0.5] + std: [0.5, 0.5, 0.5] + - !Node + type: ArrangeChangeDetector + args: ['eval'] +download_on: False + +num_epochs: 10 +train_batch_size: 8 +save_interval_epochs: 5 +log_interval_steps: 50 +save_dir: ./test_tipc/output/cd/ +learning_rate: 0.002 +early_stop: False +early_stop_patience: 5 +use_vdl: False +resume_checkpoint: '' \ No newline at end of file diff --git a/test_tipc/configs/cd/bit/bit_airchange.yaml b/test_tipc/configs/cd/bit/bit_airchange.yaml new file mode 100644 index 0000000..efd6fbb --- /dev/null +++ b/test_tipc/configs/cd/bit/bit_airchange.yaml @@ -0,0 +1,8 @@ +# Basic configurations of BIT with AirChange dataset + +_base_: ../_base_/airchange.yaml + +save_dir: ./test_tipc/output/cd/bit/ + +model: !Node + type: BIT \ No newline at end of file diff --git a/test_tipc/configs/cd/bit/bit_levircd.yaml b/test_tipc/configs/cd/bit/bit_levircd.yaml new file mode 100644 index 0000000..8008901 --- /dev/null +++ b/test_tipc/configs/cd/bit/bit_levircd.yaml @@ -0,0 +1,8 @@ +# Basic configurations of BIT with LEVIR-CD dataset + +_base_: ../_base_/levircd.yaml + +save_dir: ./test_tipc/output/cd/bit/ + +model: !Node + type: BIT \ No newline at end of file diff --git a/test_tipc/configs/cd/bit/train_infer_python.txt b/test_tipc/configs/cd/bit/train_infer_python.txt index 69ddeae..33ee2f3 100644 --- a/test_tipc/configs/cd/bit/train_infer_python.txt +++ b/test_tipc/configs/cd/bit/train_infer_python.txt @@ -8,12 +8,12 @@ use_gpu:null|null --save_dir:adaptive --train_batch_size:lite_train_lite_infer=4|lite_train_whole_infer=4|whole_train_whole_infer=4 --model_path:null +--config:lite_train_lite_infer=./test_tipc/configs/cd/bit/bit_airchange.yaml|lite_train_whole_infer=./test_tipc/configs/cd/bit/bit_airchange.yaml|whole_train_whole_infer=./test_tipc/configs/cd/bit/bit_levircd.yaml train_model_name:best_model -train_infer_file_list:./test_tipc/data/airchange/:./test_tipc/data/airchange/eval.txt null:null ## trainer:norm -norm_train:test_tipc/run_task.py train cd --config ./test_tipc/configs/cd/bit/bit.yaml +norm_train:test_tipc/run_task.py train cd pact_train:null fpgm_train:null distill_train:null @@ -46,7 +46,7 @@ inference:test_tipc/infer.py --use_trt:False --precision:fp32 --model_dir:null ---file_list:null:null +--config:null --save_log_path:null --benchmark:True --model_name:bit diff --git a/test_tipc/prepare.sh b/test_tipc/prepare.sh index cc4c807..ead48af 100644 --- a/test_tipc/prepare.sh +++ b/test_tipc/prepare.sh @@ -27,7 +27,6 @@ DATA_DIR='./test_tipc/data/' mkdir -p "${DATA_DIR}" if [[ ${MODE} == 'lite_train_lite_infer' \ || ${MODE} == 'lite_train_whole_infer' \ - || ${MODE} == 'whole_train_whole_infer' \ || ${MODE} == 'whole_infer' ]]; then if [[ ${task_name} == 'cd' ]]; then @@ -40,4 +39,15 @@ if [[ ${MODE} == 'lite_train_lite_infer' \ download_and_unzip_dataset "${DATA_DIR}" rsseg https://paddlers.bj.bcebos.com/datasets/rsseg_mini.zip fi +elif [[ ${MODE} == 'whole_train_whole_infer' ]]; then + + if [[ ${task_name} == 'cd' ]]; then + download_and_unzip_dataset "${DATA_DIR}" raw_levircd https://paddlers.bj.bcebos.com/datasets/raw/LEVIR-CD.zip \ + && python tools/prepare_dataset/prepare_levircd.py \ + --in_dataset_dir "${DATA_DIR}/raw_levircd" \ + --out_dataset_dir "${DATA_DIR}/levircd" \ + --crop_size 256 \ + --crop_stride 256 + fi + fi diff --git a/test_tipc/test_train_inference_python.sh b/test_tipc/test_train_inference_python.sh index 3fb300b..dff46fe 100644 --- a/test_tipc/test_train_inference_python.sh +++ b/test_tipc/test_train_inference_python.sh @@ -22,15 +22,15 @@ train_use_gpu_value=$(func_parser_value "${lines[4]}") autocast_list=$(func_parser_value "${lines[5]}") autocast_key=$(func_parser_key "${lines[5]}") epoch_key=$(func_parser_key "${lines[6]}") -epoch_num=$(func_parser_params "${lines[6]}") +epoch_value=$(func_parser_params "${lines[6]}") save_model_key=$(func_parser_key "${lines[7]}") train_batch_key=$(func_parser_key "${lines[8]}") train_batch_value=$(func_parser_params "${lines[8]}") pretrain_model_key=$(func_parser_key "${lines[9]}") pretrain_model_value=$(func_parser_value "${lines[9]}") -train_model_name=$(func_parser_value "${lines[10]}") -train_infer_img_dir=$(parse_first_value "${lines[11]}") -train_infer_img_file_list=$(parse_second_value "${lines[11]}") +train_config_key=$(func_parser_key "${lines[10]}") +train_config_value=$(func_parser_params "${lines[10]}") +train_model_name=$(func_parser_value "${lines[11]}") train_param_key1=$(func_parser_key "${lines[12]}") train_param_value1=$(func_parser_value "${lines[12]}") @@ -85,9 +85,8 @@ use_trt_list=$(func_parser_value "${lines[45]}") precision_key=$(func_parser_key "${lines[46]}") precision_list=$(func_parser_value "${lines[46]}") infer_model_key=$(func_parser_key "${lines[47]}") -file_list_key=$(func_parser_key "${lines[48]}") -infer_img_dir=$(parse_first_value "${lines[48]}") -infer_img_file_list=$(parse_second_value "${lines[48]}") +infer_config_key=$(func_parser_key "${lines[48]}") +infer_config_value=$(func_parser_value "${lines[48]}") save_log_key=$(func_parser_key "${lines[49]}") benchmark_key=$(func_parser_key "${lines[50]}") benchmark_value=$(func_parser_value "${lines[50]}") @@ -117,37 +116,37 @@ function func_inference() { local _script="$2" local _model_dir="$3" local _log_path="$4" - local _img_dir="$5" - local _file_list="$6" + local _config="$5" + + local set_infer_config=$(func_set_params "${infer_config_key}" "${_config}") + local set_model_dir=$(func_set_params "${infer_model_key}" "${_model_dir}") + local set_benchmark=$(func_set_params "${benchmark_key}" "${benchmark_value}") + local set_infer_params1=$(func_set_params "${infer_key1}" "${infer_value1}") + local set_infer_params2=$(func_set_params "${infer_key2}" "${infer_value2}") # Do inference for use_gpu in ${use_gpu_list[*]}; do + local set_device=$(func_set_params "${use_gpu_key}" "${use_gpu}") if [ ${use_gpu} = 'False' ] || [ ${use_gpu} = 'cpu' ]; then for use_mkldnn in ${use_mkldnn_list[*]}; do if [ ${use_mkldnn} = 'False' ]; then continue fi - for threads in ${cpu_threads_list[*]}; do - for batch_size in ${batch_size_list[*]}; do - for precision in ${precision_list[*]}; do - if [ ${use_mkldnn} = 'False' ] && [ ${precision} = 'fp16' ]; then - continue - fi # Skip when enable fp16 but disable mkldnn - - set_precision=$(func_set_params "${precision_key}" "${precision}") + for precision in ${precision_list[*]}; do + if [ ${use_mkldnn} = 'False' ] && [ ${precision} = 'fp16' ]; then + continue + fi # Skip when enable fp16 but disable mkldnn - _save_log_path="${_log_path}/python_infer_cpu_usemkldnn_${use_mkldnn}_threads_${threads}_precision_${precision}_batchsize_${batch_size}.log" - infer_value1="${_log_path}/python_infer_cpu_usemkldnn_${use_mkldnn}_threads_${threads}_precision_${precision}_batchsize_${batch_size}_results" - set_device=$(func_set_params "${use_gpu_key}" "${use_gpu}") - set_mkldnn=$(func_set_params "${use_mkldnn_key}" "${use_mkldnn}") - set_benchmark=$(func_set_params "${benchmark_key}" "${benchmark_value}") - set_batchsize=$(func_set_params "${batch_size_key}" "${batch_size}") - set_cpu_threads=$(func_set_params "${cpu_threads_key}" "${threads}") - set_model_dir=$(func_set_params "${infer_model_key}" "${_model_dir}") - set_infer_params1=$(func_set_params "${infer_key1}" "${infer_value1}") - set_infer_params2=$(func_set_params "${infer_key2}" "${infer_value2}") + for threads in ${cpu_threads_list[*]}; do + for batch_size in ${batch_size_list[*]}; do + local _save_log_path="${_log_path}/python_infer_cpu_usemkldnn_${use_mkldnn}_threads_${threads}_precision_${precision}_batchsize_${batch_size}.log" + local infer_value1="${_log_path}/python_infer_cpu_usemkldnn_${use_mkldnn}_threads_${threads}_precision_${precision}_batchsize_${batch_size}_results" + local set_mkldnn=$(func_set_params "${use_mkldnn_key}" "${use_mkldnn}") + local set_precision=$(func_set_params "${precision_key}" "${precision}") + local set_cpu_threads=$(func_set_params "${cpu_threads_key}" "${threads}") + local set_batchsize=$(func_set_params "${batch_size_key}" "${batch_size}") - cmd="${_python} ${_script} ${file_list_key} ${_img_dir} ${_file_list} ${set_device} ${set_mkldnn} ${set_cpu_threads} ${set_model_dir} ${set_batchsize} ${set_benchmark} ${set_precision} ${set_infer_params1} ${set_infer_params2}" + local cmd="${_python} ${_script} ${set_config} ${set_device} ${set_mkldnn} ${set_cpu_threads} ${set_model_dir} ${set_batchsize} ${set_benchmark} ${set_precision} ${set_infer_params1} ${set_infer_params2}" echo ${cmd} run_command "${cmd}" "${_save_log_path}" @@ -165,24 +164,18 @@ function func_inference() { fi # Skip when enable fp16 but disable trt for batch_size in ${batch_size_list[*]}; do - _save_log_path="${_log_path}/python_infer_gpu_usetrt_${use_trt}_precision_${precision}_batchsize_${batch_size}.log" - infer_value1="${_log_path}/python_infer_gpu_usetrt_${use_trt}_precision_${precision}_batchsize_${batch_size}_results" - set_device=$(func_set_params "${use_gpu_key}" "${use_gpu}") - set_benchmark=$(func_set_params "${benchmark_key}" "${benchmark_value}") - set_batchsize=$(func_set_params "${batch_size_key}" "${batch_size}") - set_tensorrt=$(func_set_params "${use_trt_key}" "${use_trt}") - set_precision=$(func_set_params "${precision_key}" "${precision}") - set_model_dir=$(func_set_params "${infer_model_key}" "${_model_dir}") - set_infer_params1=$(func_set_params "${infer_key1}" "${infer_value1}") - set_infer_params2=$(func_set_params "${infer_key2}" "${infer_value2}") + local _save_log_path="${_log_path}/python_infer_gpu_usetrt_${use_trt}_precision_${precision}_batchsize_${batch_size}.log" + local infer_value1="${_log_path}/python_infer_gpu_usetrt_${use_trt}_precision_${precision}_batchsize_${batch_size}_results" + local set_tensorrt=$(func_set_params "${use_trt_key}" "${use_trt}") + local set_precision=$(func_set_params "${precision_key}" "${precision}") + local set_batchsize=$(func_set_params "${batch_size_key}" "${batch_size}") - cmd="${_python} ${_script} ${file_list_key} ${_img_dir} ${_file_list} ${set_device} ${set_tensorrt} ${set_precision} ${set_model_dir} ${set_batchsize} ${set_benchmark} ${set_infer_params2}" + local cmd="${_python} ${_script} ${set_config} ${set_device} ${set_tensorrt} ${set_precision} ${set_model_dir} ${set_batchsize} ${set_benchmark} ${set_infer_params2}" echo ${cmd} run_command "${cmd}" "${_save_log_path}" last_status=${PIPESTATUS[0]} status_check $last_status "${cmd}" "${status_log}" "${model_name}" - done done done @@ -226,7 +219,7 @@ if [ ${MODE} = 'whole_infer' ]; then save_infer_dir=${infer_model} fi # Run inference - func_inference "${python}" "${inference_py}" "${save_infer_dir}" "${OUT_PATH}" "${infer_img_dir}" "${infer_img_file_list}" + func_inference "${python}" "${inference_py}" "${save_infer_dir}" "${OUT_PATH}" "${infer_config_value}" count=$((${count} + 1)) done else @@ -285,8 +278,9 @@ else if [ ${run_train} = 'null' ]; then continue fi + set_config=$(func_set_params "${train_config_key}" "${train_config_value}") set_autocast=$(func_set_params "${autocast_key}" "${autocast}") - set_epoch=$(func_set_params "${epoch_key}" "${epoch_num}") + set_epoch=$(func_set_params "${epoch_key}" "${epoch_value}") set_pretrain=$(func_set_params "${pretrain_model_key}" "${pretrain_model_value}") set_batchsize=$(func_set_params "${train_batch_key}" "${train_batch_value}") set_train_params1=$(func_set_params "${train_param_key1}" "${train_param_value1}") @@ -312,11 +306,11 @@ else set_save_model=$(func_set_params "${save_model_key}" "${save_dir}") if [ ${#gpu} -le 2 ]; then # Train with cpu or single gpu - cmd="${python} ${run_train} ${set_use_gpu} ${set_save_model} ${set_epoch} ${set_pretrain} ${set_autocast} ${set_batchsize} ${set_train_params1} ${set_amp_config}" + cmd="${python} ${run_train} ${set_config} ${set_use_gpu} ${set_save_model} ${set_epoch} ${set_pretrain} ${set_autocast} ${set_batchsize} ${set_train_params1} ${set_amp_config}" elif [ ${#ips} -le 15 ]; then # Train with multi-gpu - cmd="${python} -m paddle.distributed.launch --gpus=${gpu} ${run_train} ${set_use_gpu} ${set_save_model} ${set_epoch} ${set_pretrain} ${set_autocast} ${set_batchsize} ${set_train_params1} ${set_amp_config}" + cmd="${python} -m paddle.distributed.launch --gpus=${gpu} ${run_train} ${set_config} ${set_use_gpu} ${set_save_model} ${set_epoch} ${set_pretrain} ${set_autocast} ${set_batchsize} ${set_train_params1} ${set_amp_config}" else # Train with multi-machine - cmd="${python} -m paddle.distributed.launch --ips=${ips} --gpus=${gpu} ${run_train} ${set_use_gpu} ${set_save_model} ${set_pretrain} ${set_epoch} ${set_autocast} ${set_batchsize} ${set_train_params1} ${set_amp_config}" + cmd="${python} -m paddle.distributed.launch --ips=${ips} --gpus=${gpu} ${run_train} ${set_config} ${set_use_gpu} ${set_save_model} ${set_pretrain} ${set_epoch} ${set_autocast} ${set_batchsize} ${set_train_params1} ${set_amp_config}" fi echo ${cmd} @@ -359,7 +353,7 @@ else else infer_model_dir=${save_infer_path} fi - func_inference "${python}" "${inference_py}" "${infer_model_dir}" "${OUT_PATH}" "${train_infer_img_dir}" "${train_infer_img_file_list}" + func_inference "${python}" "${inference_py}" "${infer_model_dir}" "${OUT_PATH}" "${train_config_value}" eval "unset CUDA_VISIBLE_DEVICES" fi From b86f9d435ce8fa06f851a4532f7d0062f5bca3b0 Mon Sep 17 00:00:00 2001 From: Bobholamovic Date: Sat, 20 Aug 2022 21:24:21 +0800 Subject: [PATCH 14/52] [Refactor] Refactor image restoration --- paddlers/datasets/__init__.py | 2 +- paddlers/datasets/cd_dataset.py | 10 +- paddlers/datasets/res_dataset.py | 83 ++ paddlers/datasets/seg_dataset.py | 2 +- paddlers/datasets/sr_dataset.py | 99 --- paddlers/models/__init__.py | 1 + paddlers/rs_models/res/__init__.py | 2 +- paddlers/rs_models/res/generators/builder.py | 26 - paddlers/rs_models/res/generators/rcan.py | 26 +- paddlers/rs_models/res/rcan_model.py | 106 --- paddlers/tasks/__init__.py | 2 +- paddlers/tasks/base.py | 49 +- paddlers/tasks/change_detector.py | 21 +- paddlers/tasks/classifier.py | 26 +- paddlers/tasks/image_restorer.py | 786 ----------------- paddlers/tasks/object_detector.py | 31 +- paddlers/tasks/restorer.py | 818 ++++++++++++++++++ paddlers/tasks/segmenter.py | 21 +- paddlers/tasks/utils/res_adapters.py | 128 +++ paddlers/transforms/functions.py | 4 + paddlers/transforms/operators.py | 112 ++- tutorials/train/README.md | 7 +- tutorials/train/classification/hrnet.py | 2 +- tutorials/train/classification/mobilenetv3.py | 2 +- tutorials/train/classification/resnet50_vd.py | 2 +- .../train/image_restoration/data/.gitignore | 3 + tutorials/train/image_restoration/drn.py | 86 ++ .../train/image_restoration/drn_train.py | 80 -- tutorials/train/image_restoration/esrgan.py | 86 ++ .../train/image_restoration/esrgan_train.py | 80 -- tutorials/train/image_restoration/lesrcnn.py | 86 ++ .../train/image_restoration/lesrcnn_train.py | 78 -- tutorials/train/image_restoration/rcan.py | 86 ++ 33 files changed, 1589 insertions(+), 1364 deletions(-) create mode 100644 paddlers/datasets/res_dataset.py delete mode 100644 paddlers/datasets/sr_dataset.py delete mode 100644 paddlers/rs_models/res/generators/builder.py delete mode 100644 paddlers/rs_models/res/rcan_model.py delete mode 100644 paddlers/tasks/image_restorer.py create mode 100644 paddlers/tasks/restorer.py create mode 100644 paddlers/tasks/utils/res_adapters.py create mode 100644 tutorials/train/image_restoration/data/.gitignore create mode 100644 tutorials/train/image_restoration/drn.py delete mode 100644 tutorials/train/image_restoration/drn_train.py create mode 100644 tutorials/train/image_restoration/esrgan.py delete mode 100644 tutorials/train/image_restoration/esrgan_train.py create mode 100644 tutorials/train/image_restoration/lesrcnn.py delete mode 100644 tutorials/train/image_restoration/lesrcnn_train.py create mode 100644 tutorials/train/image_restoration/rcan.py diff --git a/paddlers/datasets/__init__.py b/paddlers/datasets/__init__.py index 1fecd96..1fab4c4 100644 --- a/paddlers/datasets/__init__.py +++ b/paddlers/datasets/__init__.py @@ -17,4 +17,4 @@ from .coco import COCODetDataset from .seg_dataset import SegDataset from .cd_dataset import CDDataset from .clas_dataset import ClasDataset -from .sr_dataset import SRdataset, ComposeTrans +from .res_dataset import ResDataset diff --git a/paddlers/datasets/cd_dataset.py b/paddlers/datasets/cd_dataset.py index 2a2d85a..048b23c 100644 --- a/paddlers/datasets/cd_dataset.py +++ b/paddlers/datasets/cd_dataset.py @@ -95,23 +95,23 @@ class CDDataset(BaseDataset): full_path_label))): continue if not osp.exists(full_path_im_t1): - raise IOError('Image file {} does not exist!'.format( + raise IOError("Image file {} does not exist!".format( full_path_im_t1)) if not osp.exists(full_path_im_t2): - raise IOError('Image file {} does not exist!'.format( + raise IOError("Image file {} does not exist!".format( full_path_im_t2)) if not osp.exists(full_path_label): - raise IOError('Label file {} does not exist!'.format( + raise IOError("Label file {} does not exist!".format( full_path_label)) if with_seg_labels: full_path_seg_label_t1 = osp.join(data_dir, items[3]) full_path_seg_label_t2 = osp.join(data_dir, items[4]) if not osp.exists(full_path_seg_label_t1): - raise IOError('Label file {} does not exist!'.format( + raise IOError("Label file {} does not exist!".format( full_path_seg_label_t1)) if not osp.exists(full_path_seg_label_t2): - raise IOError('Label file {} does not exist!'.format( + raise IOError("Label file {} does not exist!".format( full_path_seg_label_t2)) item_dict = dict( diff --git a/paddlers/datasets/res_dataset.py b/paddlers/datasets/res_dataset.py new file mode 100644 index 0000000..aaab8b2 --- /dev/null +++ b/paddlers/datasets/res_dataset.py @@ -0,0 +1,83 @@ +# Copyright (c) 2022 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 os.path as osp +import copy + +from .base import BaseDataset +from paddlers.utils import logging, get_encoding, norm_path, is_pic + + +class ResDataset(BaseDataset): + """ + Dataset for image restoration tasks. + + Args: + data_dir (str): Root directory of the dataset. + file_list (str): Path of the file that contains relative paths of source and target image files. + transforms (paddlers.transforms.Compose): Data preprocessing and data augmentation operators to apply. + num_workers (int|str, optional): Number of processes used for data loading. If `num_workers` is 'auto', + the number of workers will be automatically determined according to the number of CPU cores: If + there are more than 16 cores,8 workers will be used. Otherwise, the number of workers will be half + the number of CPU cores. Defaults: 'auto'. + shuffle (bool, optional): Whether to shuffle the samples. Defaults to False. + sr_factor (int|None, optional): Scaling factor of image super-resolution task. None for other image + restoration tasks. Defaults to None. + """ + + def __init__(self, + data_dir, + file_list, + transforms, + num_workers='auto', + shuffle=False, + sr_factor=None): + super(ResDataset, self).__init__(data_dir, None, transforms, + num_workers, shuffle) + self.batch_transforms = None + self.file_list = list() + + with open(file_list, encoding=get_encoding(file_list)) as f: + for line in f: + items = line.strip().split() + if len(items) > 2: + raise ValueError( + "A space is defined as the delimiter to separate the source and target image path, " \ + "so the space cannot be in the source image or target image path, but the line[{}] of " \ + " file_list[{}] has a space in the two paths.".format(line, file_list)) + items[0] = norm_path(items[0]) + items[1] = norm_path(items[1]) + full_path_im = osp.join(data_dir, items[0]) + full_path_tar = osp.join(data_dir, items[1]) + if not is_pic(full_path_im) or not is_pic(full_path_tar): + continue + if not osp.exists(full_path_im): + raise IOError("Source image file {} does not exist!".format( + full_path_im)) + if not osp.exists(full_path_tar): + raise IOError("Target image file {} does not exist!".format( + full_path_tar)) + sample = { + 'image': full_path_im, + 'target': full_path_tar, + } + if sr_factor is not None: + sample['sr_factor'] = sr_factor + self.file_list.append(sample) + self.num_samples = len(self.file_list) + logging.info("{} samples in file {}".format( + len(self.file_list), file_list)) + + def __len__(self): + return len(self.file_list) diff --git a/paddlers/datasets/seg_dataset.py b/paddlers/datasets/seg_dataset.py index 0bfab96..656777c 100644 --- a/paddlers/datasets/seg_dataset.py +++ b/paddlers/datasets/seg_dataset.py @@ -44,7 +44,7 @@ class SegDataset(BaseDataset): shuffle=False): super(SegDataset, self).__init__(data_dir, label_list, transforms, num_workers, shuffle) - # TODO batch padding + # TODO: batch padding self.batch_transforms = None self.file_list = list() self.labels = list() diff --git a/paddlers/datasets/sr_dataset.py b/paddlers/datasets/sr_dataset.py deleted file mode 100644 index 17748bf..0000000 --- a/paddlers/datasets/sr_dataset.py +++ /dev/null @@ -1,99 +0,0 @@ -# Copyright (c) 2022 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. - - -# 超分辨率数据集定义 -class SRdataset(object): - def __init__(self, - mode, - gt_floder, - lq_floder, - transforms, - scale, - num_workers=4, - batch_size=8): - if mode == 'train': - preprocess = [] - preprocess.append({ - 'name': 'LoadImageFromFile', - 'key': 'lq' - }) # 加载方式 - preprocess.append({'name': 'LoadImageFromFile', 'key': 'gt'}) - preprocess.append(transforms) # 变换方式 - self.dataset = { - 'name': 'SRDataset', - 'gt_folder': gt_floder, - 'lq_folder': lq_floder, - 'num_workers': num_workers, - 'batch_size': batch_size, - 'scale': scale, - 'preprocess': preprocess - } - - if mode == "test": - preprocess = [] - preprocess.append({'name': 'LoadImageFromFile', 'key': 'lq'}) - preprocess.append({'name': 'LoadImageFromFile', 'key': 'gt'}) - preprocess.append(transforms) - self.dataset = { - 'name': 'SRDataset', - 'gt_folder': gt_floder, - 'lq_folder': lq_floder, - 'scale': scale, - 'preprocess': preprocess - } - - def __call__(self): - return self.dataset - - -# 对定义的transforms处理方式组合,返回字典 -class ComposeTrans(object): - def __init__(self, input_keys, output_keys, pipelines): - if not isinstance(pipelines, list): - raise TypeError( - 'Type of transforms is invalid. Must be List, but received is {}' - .format(type(pipelines))) - if len(pipelines) < 1: - raise ValueError( - 'Length of transforms must not be less than 1, but received is {}' - .format(len(pipelines))) - self.transforms = pipelines - self.output_length = len(output_keys) # 当output_keys的长度为3时,是DRN训练 - self.input_keys = input_keys - self.output_keys = output_keys - - def __call__(self): - pipeline = [] - for op in self.transforms: - if op['name'] == 'SRPairedRandomCrop': - op['keys'] = ['image'] * 2 - else: - op['keys'] = ['image'] * self.output_length - pipeline.append(op) - if self.output_length == 2: - transform_dict = { - 'name': 'Transforms', - 'input_keys': self.input_keys, - 'pipeline': pipeline - } - else: - transform_dict = { - 'name': 'Transforms', - 'input_keys': self.input_keys, - 'output_keys': self.output_keys, - 'pipeline': pipeline - } - - return transform_dict diff --git a/paddlers/models/__init__.py b/paddlers/models/__init__.py index 952821f..bf2f708 100644 --- a/paddlers/models/__init__.py +++ b/paddlers/models/__init__.py @@ -16,3 +16,4 @@ from . import ppcls, ppdet, ppseg, ppgan import paddlers.models.ppseg.models.losses as seg_losses import paddlers.models.ppdet.modeling.losses as det_losses import paddlers.models.ppcls.loss as clas_losses +import paddlers.models.ppgan.models.criterions as res_losses diff --git a/paddlers/rs_models/res/__init__.py b/paddlers/rs_models/res/__init__.py index 4dec1be..583dd10 100644 --- a/paddlers/rs_models/res/__init__.py +++ b/paddlers/rs_models/res/__init__.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .rcan_model import RCANModel +from .generators import * diff --git a/paddlers/rs_models/res/generators/builder.py b/paddlers/rs_models/res/generators/builder.py deleted file mode 100644 index 8e4b884..0000000 --- a/paddlers/rs_models/res/generators/builder.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve. -# -# 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 copy - -from ....models.ppgan.utils.registry import Registry - -GENERATORS = Registry("GENERATOR") - - -def build_generator(cfg): - cfg_copy = copy.deepcopy(cfg) - name = cfg_copy.pop('name') - generator = GENERATORS.get(name)(**cfg_copy) - return generator diff --git a/paddlers/rs_models/res/generators/rcan.py b/paddlers/rs_models/res/generators/rcan.py index 17f9ee8..db66b92 100644 --- a/paddlers/rs_models/res/generators/rcan.py +++ b/paddlers/rs_models/res/generators/rcan.py @@ -4,8 +4,6 @@ import math import paddle import paddle.nn as nn -from .builder import GENERATORS - def default_conv(in_channels, out_channels, kernel_size, bias=True): weight_attr = paddle.ParamAttr( @@ -128,21 +126,19 @@ class Upsampler(nn.Sequential): super(Upsampler, self).__init__(*m) -@GENERATORS.register() class RCAN(nn.Layer): - def __init__( - self, - scale, - n_resgroups, - n_resblocks, - n_feats=64, - n_colors=3, - rgb_range=255, - kernel_size=3, - reduction=16, - conv=default_conv, ): + def __init__(self, + sr_factor=4, + n_resgroups=10, + n_resblocks=20, + n_feats=64, + n_colors=3, + rgb_range=255, + kernel_size=3, + reduction=16, + conv=default_conv): super(RCAN, self).__init__() - self.scale = scale + self.scale = sr_factor act = nn.ReLU() n_resgroups = n_resgroups diff --git a/paddlers/rs_models/res/rcan_model.py b/paddlers/rs_models/res/rcan_model.py deleted file mode 100644 index 691fb12..0000000 --- a/paddlers/rs_models/res/rcan_model.py +++ /dev/null @@ -1,106 +0,0 @@ -# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserve. -# -# 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 paddle -import paddle.nn as nn - -from .generators.builder import build_generator -from ...models.ppgan.models.criterions.builder import build_criterion -from ...models.ppgan.models.base_model import BaseModel -from ...models.ppgan.models.builder import MODELS -from ...models.ppgan.utils.visual import tensor2img -from ...models.ppgan.modules.init import reset_parameters - - -@MODELS.register() -class RCANModel(BaseModel): - """ - Base SR model for single image super-resolution. - """ - - def __init__(self, generator, pixel_criterion=None, use_init_weight=False): - """ - Args: - generator (dict): config of generator. - pixel_criterion (dict): config of pixel criterion. - """ - super(RCANModel, self).__init__() - - self.nets['generator'] = build_generator(generator) - self.error_last = 1e8 - self.batch = 0 - if pixel_criterion: - self.pixel_criterion = build_criterion(pixel_criterion) - if use_init_weight: - init_sr_weight(self.nets['generator']) - - def setup_input(self, input): - self.lq = paddle.to_tensor(input['lq']) - self.visual_items['lq'] = self.lq - if 'gt' in input: - self.gt = paddle.to_tensor(input['gt']) - self.visual_items['gt'] = self.gt - self.image_paths = input['lq_path'] - - def forward(self): - pass - - def train_iter(self, optims=None): - optims['optim'].clear_grad() - - self.output = self.nets['generator'](self.lq) - self.visual_items['output'] = self.output - # pixel loss - loss_pixel = self.pixel_criterion(self.output, self.gt) - self.losses['loss_pixel'] = loss_pixel - - skip_threshold = 1e6 - - if loss_pixel.item() < skip_threshold * self.error_last: - loss_pixel.backward() - optims['optim'].step() - else: - print('Skip this batch {}! (Loss: {})'.format(self.batch + 1, - loss_pixel.item())) - self.batch += 1 - - if self.batch % 1000 == 0: - self.error_last = loss_pixel.item() / 1000 - print("update error_last:{}".format(self.error_last)) - - def test_iter(self, metrics=None): - self.nets['generator'].eval() - with paddle.no_grad(): - self.output = self.nets['generator'](self.lq) - self.visual_items['output'] = self.output - self.nets['generator'].train() - - out_img = [] - gt_img = [] - for out_tensor, gt_tensor in zip(self.output, self.gt): - out_img.append(tensor2img(out_tensor, (0., 255.))) - gt_img.append(tensor2img(gt_tensor, (0., 255.))) - - if metrics is not None: - for metric in metrics.values(): - metric.update(out_img, gt_img) - - -def init_sr_weight(net): - def reset_func(m): - if hasattr(m, 'weight') and ( - not isinstance(m, (nn.BatchNorm, nn.BatchNorm2D))): - reset_parameters(m) - - net.apply(reset_func) diff --git a/paddlers/tasks/__init__.py b/paddlers/tasks/__init__.py index 5c1f428..ffa023f 100644 --- a/paddlers/tasks/__init__.py +++ b/paddlers/tasks/__init__.py @@ -16,7 +16,7 @@ import paddlers.tasks.object_detector as detector import paddlers.tasks.segmenter as segmenter import paddlers.tasks.change_detector as change_detector import paddlers.tasks.classifier as classifier -import paddlers.tasks.image_restorer as restorer +import paddlers.tasks.restorer as restorer from .load_model import load_model # Shorter aliases diff --git a/paddlers/tasks/base.py b/paddlers/tasks/base.py index 5250058..809c385 100644 --- a/paddlers/tasks/base.py +++ b/paddlers/tasks/base.py @@ -35,7 +35,6 @@ from paddlers.utils import (seconds_to_hms, get_single_card_bs, dict2str, load_checkpoint, SmoothedValue, TrainingStats, _get_shared_memory_size_in_M, EarlyStop) from .slim.prune import _pruner_eval_fn, _pruner_template_input, sensitive_prune -from .utils.infer_nets import InferNet, InferCDNet class ModelMeta(type): @@ -268,7 +267,7 @@ class BaseModel(metaclass=ModelMeta): 'The volume of dataset({}) must be larger than batch size({}).' .format(dataset.num_samples, batch_size)) batch_size_each_card = get_single_card_bs(batch_size=batch_size) - # TODO detection eval阶段需做判断 + # TODO: Make judgement in detection eval phase. batch_sampler = DistributedBatchSampler( dataset, batch_size=batch_size_each_card, @@ -365,24 +364,12 @@ class BaseModel(metaclass=ModelMeta): for step, data in enumerate(self.train_data_loader()): if nranks > 1: - outputs = self.run(ddp_net, data, mode='train') + outputs = self.train_step(step, data, ddp_net) else: - outputs = self.run(self.net, data, mode='train') - loss = outputs['loss'] - loss.backward() - self.optimizer.step() - self.optimizer.clear_grad() - lr = self.optimizer.get_lr() - if isinstance(self.optimizer._learning_rate, - paddle.optimizer.lr.LRScheduler): - # If ReduceOnPlateau is used as the scheduler, use the loss value as the metric. - if isinstance(self.optimizer._learning_rate, - paddle.optimizer.lr.ReduceOnPlateau): - self.optimizer._learning_rate.step(loss.item()) - else: - self.optimizer._learning_rate.step() + outputs = self.train_step(step, data, self.net) train_avg_metrics.update(outputs) + lr = self.optimizer.get_lr() outputs['lr'] = lr if ema is not None: ema.update(self.net) @@ -622,14 +609,7 @@ class BaseModel(metaclass=ModelMeta): return pipeline_info def _build_inference_net(self): - if self.model_type in ('classifier', 'detector'): - infer_net = self.net - elif self.model_type == 'change_detector': - infer_net = InferCDNet(self.net) - else: - infer_net = InferNet(self.net, self.model_type) - infer_net.eval() - return infer_net + raise NotImplementedError def _export_inference_model(self, save_dir, image_shape=None): self.test_inputs = self._get_test_inputs(image_shape) @@ -674,6 +654,25 @@ class BaseModel(metaclass=ModelMeta): logging.info("The inference model for deployment is saved in {}.". format(save_dir)) + def train_step(self, step, data, net): + outputs = self.run(net, data, mode='train') + + loss = outputs['loss'] + loss.backward() + self.optimizer.step() + self.optimizer.clear_grad() + + if isinstance(self.optimizer._learning_rate, + paddle.optimizer.lr.LRScheduler): + # If ReduceOnPlateau is used as the scheduler, use the loss value as the metric. + if isinstance(self.optimizer._learning_rate, + paddle.optimizer.lr.ReduceOnPlateau): + self.optimizer._learning_rate.step(loss.item()) + else: + self.optimizer._learning_rate.step() + + return outputs + def _check_transforms(self, transforms, mode): # NOTE: Check transforms and transforms.arrange and give user-friendly error messages. if not isinstance(transforms, paddlers.transforms.Compose): diff --git a/paddlers/tasks/change_detector.py b/paddlers/tasks/change_detector.py index 9d631e6..9a0b16e 100644 --- a/paddlers/tasks/change_detector.py +++ b/paddlers/tasks/change_detector.py @@ -30,10 +30,11 @@ import paddlers.rs_models.cd as cmcd import paddlers.utils.logging as logging from paddlers.models import seg_losses from paddlers.transforms import Resize, decode_image -from paddlers.utils import get_single_card_bs, DisablePrint +from paddlers.utils import get_single_card_bs from paddlers.utils.checkpoint import seg_pretrain_weights_dict from .base import BaseModel from .utils import seg_metrics as metrics +from .utils.infer_nets import InferCDNet __all__ = [ "CDNet", "FCEarlyFusion", "FCSiamConc", "FCSiamDiff", "STANet", "BIT", @@ -71,6 +72,11 @@ class BaseChangeDetector(BaseModel): **params) return net + def _build_inference_net(self): + infer_net = InferCDNet(self.net) + infer_net.eval() + return infer_net + def _fix_transforms_shape(self, image_shape): if hasattr(self, 'test_transforms'): if self.test_transforms is not None: @@ -401,7 +407,8 @@ class BaseChangeDetector(BaseModel): Defaults to False. Returns: - collections.OrderedDict with key-value pairs: + If `return_details` is False, return collections.OrderedDict with + key-value pairs: For binary change detection (number of classes == 2), the key-value pairs are like: {"iou": `intersection over union for the change class`, @@ -529,12 +536,12 @@ class BaseChangeDetector(BaseModel): Returns: If `img_file` is a tuple of string or np.array, the result is a dict with - key-value pairs: - {"label map": `label map`, "score_map": `score map`}. + the following key-value pairs: + label_map (np.ndarray): Predicted label map (HW). + score_map (np.ndarray): Prediction score map (HWC). + If `img_file` is a list, the result is a list composed of dicts with the - corresponding fields: - label_map (np.ndarray): the predicted label map (HW) - score_map (np.ndarray): the prediction score map (HWC) + above keys. """ if transforms is None and not hasattr(self, 'test_transforms'): diff --git a/paddlers/tasks/classifier.py b/paddlers/tasks/classifier.py index bad2756..06702d9 100644 --- a/paddlers/tasks/classifier.py +++ b/paddlers/tasks/classifier.py @@ -83,6 +83,11 @@ class BaseClassifier(BaseModel): self.in_channels = 3 return net + def _build_inference_net(self): + infer_net = self.net + infer_net.eval() + return infer_net + def _fix_transforms_shape(self, image_shape): if hasattr(self, 'test_transforms'): if self.test_transforms is not None: @@ -375,7 +380,8 @@ class BaseClassifier(BaseModel): Defaults to False. Returns: - collections.OrderedDict with key-value pairs: + If `return_details` is False, return collections.OrderedDict with + key-value pairs: {"top1": `acc of top1`, "top5": `acc of top5`}. """ @@ -420,7 +426,7 @@ class BaseClassifier(BaseModel): top5 = np.mean(top5s) eval_metrics = OrderedDict(zip(['top1', 'top5'], [top1, top5])) if return_details: - # TODO: add details + # TODO: Add details return eval_metrics, None return eval_metrics @@ -437,16 +443,14 @@ class BaseClassifier(BaseModel): Defaults to None. Returns: - If `img_file` is a string or np.array, the result is a dict with key-value - pairs: - {"label map": `class_ids_map`, - "scores_map": `scores_map`, - "label_names_map": `label_names_map`}. + If `img_file` is a string or np.array, the result is a dict with the + following key-value pairs: + class_ids_map (np.ndarray): IDs of predicted classes. + scores_map (np.ndarray): Scores of predicted classes. + label_names_map (np.ndarray): Names of predicted classes. + If `img_file` is a list, the result is a list composed of dicts with the - corresponding fields: - class_ids_map (np.ndarray): class_ids - scores_map (np.ndarray): scores - label_names_map (np.ndarray): label_names + above keys. """ if transforms is None and not hasattr(self, 'test_transforms'): diff --git a/paddlers/tasks/image_restorer.py b/paddlers/tasks/image_restorer.py deleted file mode 100644 index ec41dd3..0000000 --- a/paddlers/tasks/image_restorer.py +++ /dev/null @@ -1,786 +0,0 @@ -# Copyright (c) 2022 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 os -import time -import datetime - -import paddle -from paddle.distributed import ParallelEnv - -from ..models.ppgan.datasets.builder import build_dataloader -from ..models.ppgan.models.builder import build_model -from ..models.ppgan.utils.visual import tensor2img, save_image -from ..models.ppgan.utils.filesystem import makedirs, save, load -from ..models.ppgan.utils.timer import TimeAverager -from ..models.ppgan.utils.profiler import add_profiler_step -from ..models.ppgan.utils.logger import setup_logger - - -# 定义AttrDict类实现动态属性 -class AttrDict(dict): - def __getattr__(self, key): - try: - return self[key] - except KeyError: - raise AttributeError(key) - - def __setattr__(self, key, value): - if key in self.__dict__: - self.__dict__[key] = value - else: - self[key] = value - - -# 创建AttrDict类 -def create_attr_dict(config_dict): - from ast import literal_eval - for key, value in config_dict.items(): - if type(value) is dict: - config_dict[key] = value = AttrDict(value) - if isinstance(value, str): - try: - value = literal_eval(value) - except BaseException: - pass - if isinstance(value, AttrDict): - create_attr_dict(config_dict[key]) - else: - config_dict[key] = value - - -# 数据加载类 -class IterLoader: - def __init__(self, dataloader): - self._dataloader = dataloader - self.iter_loader = iter(self._dataloader) - self._epoch = 1 - - @property - def epoch(self): - return self._epoch - - def __next__(self): - try: - data = next(self.iter_loader) - except StopIteration: - self._epoch += 1 - self.iter_loader = iter(self._dataloader) - data = next(self.iter_loader) - - return data - - def __len__(self): - return len(self._dataloader) - - -# 基础训练类 -class Restorer: - """ - # trainer calling logic: - # - # build_model || model(BaseModel) - # | || - # build_dataloader || dataloader - # | || - # model.setup_lr_schedulers || lr_scheduler - # | || - # model.setup_optimizers || optimizers - # | || - # train loop (model.setup_input + model.train_iter) || train loop - # | || - # print log (model.get_current_losses) || - # | || - # save checkpoint (model.nets) \/ - """ - - def __init__(self, cfg, logger): - # base config - # self.logger = logging.getLogger(__name__) - self.logger = logger - self.cfg = cfg - self.output_dir = cfg.output_dir - self.max_eval_steps = cfg.model.get('max_eval_steps', None) - - self.local_rank = ParallelEnv().local_rank - self.world_size = ParallelEnv().nranks - self.log_interval = cfg.log_config.interval - self.visual_interval = cfg.log_config.visiual_interval - self.weight_interval = cfg.snapshot_config.interval - - self.start_epoch = 1 - self.current_epoch = 1 - self.current_iter = 1 - self.inner_iter = 1 - self.batch_id = 0 - self.global_steps = 0 - - # build model - self.model = build_model(cfg.model) - # multiple gpus prepare - if ParallelEnv().nranks > 1: - self.distributed_data_parallel() - - # build metrics - self.metrics = None - self.is_save_img = True - validate_cfg = cfg.get('validate', None) - if validate_cfg and 'metrics' in validate_cfg: - self.metrics = self.model.setup_metrics(validate_cfg['metrics']) - if validate_cfg and 'save_img' in validate_cfg: - self.is_save_img = validate_cfg['save_img'] - - self.enable_visualdl = cfg.get('enable_visualdl', False) - if self.enable_visualdl: - import visualdl - self.vdl_logger = visualdl.LogWriter(logdir=cfg.output_dir) - - # evaluate only - if not cfg.is_train: - return - - # build train dataloader - self.train_dataloader = build_dataloader(cfg.dataset.train) - self.iters_per_epoch = len(self.train_dataloader) - - # build lr scheduler - # TODO: has a better way? - if 'lr_scheduler' in cfg and 'iters_per_epoch' in cfg.lr_scheduler: - cfg.lr_scheduler.iters_per_epoch = self.iters_per_epoch - self.lr_schedulers = self.model.setup_lr_schedulers(cfg.lr_scheduler) - - # build optimizers - self.optimizers = self.model.setup_optimizers(self.lr_schedulers, - cfg.optimizer) - - self.epochs = cfg.get('epochs', None) - if self.epochs: - self.total_iters = self.epochs * self.iters_per_epoch - self.by_epoch = True - else: - self.by_epoch = False - self.total_iters = cfg.total_iters - - if self.by_epoch: - self.weight_interval *= self.iters_per_epoch - - self.validate_interval = -1 - if cfg.get('validate', None) is not None: - self.validate_interval = cfg.validate.get('interval', -1) - - self.time_count = {} - self.best_metric = {} - self.model.set_total_iter(self.total_iters) - self.profiler_options = cfg.profiler_options - - def distributed_data_parallel(self): - paddle.distributed.init_parallel_env() - find_unused_parameters = self.cfg.get('find_unused_parameters', False) - for net_name, net in self.model.nets.items(): - self.model.nets[net_name] = paddle.DataParallel( - net, find_unused_parameters=find_unused_parameters) - - def learning_rate_scheduler_step(self): - if isinstance(self.model.lr_scheduler, dict): - for lr_scheduler in self.model.lr_scheduler.values(): - lr_scheduler.step() - elif isinstance(self.model.lr_scheduler, - paddle.optimizer.lr.LRScheduler): - self.model.lr_scheduler.step() - else: - raise ValueError( - 'lr schedulter must be a dict or an instance of LRScheduler') - - def train(self): - reader_cost_averager = TimeAverager() - batch_cost_averager = TimeAverager() - - iter_loader = IterLoader(self.train_dataloader) - - # set model.is_train = True - self.model.setup_train_mode(is_train=True) - while self.current_iter < (self.total_iters + 1): - self.current_epoch = iter_loader.epoch - self.inner_iter = self.current_iter % self.iters_per_epoch - - add_profiler_step(self.profiler_options) - - start_time = step_start_time = time.time() - data = next(iter_loader) - reader_cost_averager.record(time.time() - step_start_time) - # unpack data from dataset and apply preprocessing - # data input should be dict - self.model.setup_input(data) - self.model.train_iter(self.optimizers) - - batch_cost_averager.record( - time.time() - step_start_time, - num_samples=self.cfg['dataset']['train'].get('batch_size', 1)) - - step_start_time = time.time() - - if self.current_iter % self.log_interval == 0: - self.data_time = reader_cost_averager.get_average() - self.step_time = batch_cost_averager.get_average() - self.ips = batch_cost_averager.get_ips_average() - self.print_log() - - reader_cost_averager.reset() - batch_cost_averager.reset() - - if self.current_iter % self.visual_interval == 0 and self.local_rank == 0: - self.visual('visual_train') - - self.learning_rate_scheduler_step() - - if self.validate_interval > -1 and self.current_iter % self.validate_interval == 0: - self.test() - - if self.current_iter % self.weight_interval == 0: - self.save(self.current_iter, 'weight', keep=-1) - self.save(self.current_iter) - - self.current_iter += 1 - - def test(self): - if not hasattr(self, 'test_dataloader'): - self.test_dataloader = build_dataloader( - self.cfg.dataset.test, is_train=False) - iter_loader = IterLoader(self.test_dataloader) - if self.max_eval_steps is None: - self.max_eval_steps = len(self.test_dataloader) - - if self.metrics: - for metric in self.metrics.values(): - metric.reset() - - # set model.is_train = False - self.model.setup_train_mode(is_train=False) - - for i in range(self.max_eval_steps): - if self.max_eval_steps < self.log_interval or i % self.log_interval == 0: - self.logger.info('Test iter: [%d/%d]' % ( - i * self.world_size, self.max_eval_steps * self.world_size)) - - data = next(iter_loader) - self.model.setup_input(data) - self.model.test_iter(metrics=self.metrics) - - if self.is_save_img: - visual_results = {} - current_paths = self.model.get_image_paths() - current_visuals = self.model.get_current_visuals() - - if len(current_visuals) > 0 and list(current_visuals.values())[ - 0].shape == 4: - num_samples = list(current_visuals.values())[0].shape[0] - else: - num_samples = 1 - - for j in range(num_samples): - if j < len(current_paths): - short_path = os.path.basename(current_paths[j]) - basename = os.path.splitext(short_path)[0] - else: - basename = '{:04d}_{:04d}'.format(i, j) - for k, img_tensor in current_visuals.items(): - name = '%s_%s' % (basename, k) - if len(img_tensor.shape) == 4: - visual_results.update({name: img_tensor[j]}) - else: - visual_results.update({name: img_tensor}) - - self.visual( - 'visual_test', - visual_results=visual_results, - step=self.batch_id, - is_save_image=True) - - if self.metrics: - for metric_name, metric in self.metrics.items(): - self.logger.info("Metric {}: {:.4f}".format( - metric_name, metric.accumulate())) - - def print_log(self): - losses = self.model.get_current_losses() - - message = '' - if self.by_epoch: - message += 'Epoch: %d/%d, iter: %d/%d ' % ( - self.current_epoch, self.epochs, self.inner_iter, - self.iters_per_epoch) - else: - message += 'Iter: %d/%d ' % (self.current_iter, self.total_iters) - - message += f'lr: {self.current_learning_rate:.3e} ' - - for k, v in losses.items(): - message += '%s: %.3f ' % (k, v) - if self.enable_visualdl: - self.vdl_logger.add_scalar(k, v, step=self.global_steps) - - if hasattr(self, 'step_time'): - message += 'batch_cost: %.5f sec ' % self.step_time - - if hasattr(self, 'data_time'): - message += 'reader_cost: %.5f sec ' % self.data_time - - if hasattr(self, 'ips'): - message += 'ips: %.5f images/s ' % self.ips - - if hasattr(self, 'step_time'): - eta = self.step_time * (self.total_iters - self.current_iter) - eta = eta if eta > 0 else 0 - - eta_str = str(datetime.timedelta(seconds=int(eta))) - message += f'eta: {eta_str}' - - # print the message - self.logger.info(message) - - @property - def current_learning_rate(self): - for optimizer in self.model.optimizers.values(): - return optimizer.get_lr() - - def visual(self, - results_dir, - visual_results=None, - step=None, - is_save_image=False): - """ - visual the images, use visualdl or directly write to the directory - Parameters: - results_dir (str) -- directory name which contains saved images - visual_results (dict) -- the results images dict - step (int) -- global steps, used in visualdl - is_save_image (bool) -- weather write to the directory or visualdl - """ - self.model.compute_visuals() - - if visual_results is None: - visual_results = self.model.get_current_visuals() - - min_max = self.cfg.get('min_max', None) - if min_max is None: - min_max = (-1., 1.) - - image_num = self.cfg.get('image_num', None) - if (image_num is None) or (not self.enable_visualdl): - image_num = 1 - for label, image in visual_results.items(): - image_numpy = tensor2img(image, min_max, image_num) - if (not is_save_image) and self.enable_visualdl: - self.vdl_logger.add_image( - results_dir + '/' + label, - image_numpy, - step=step if step else self.global_steps, - dataformats="HWC" if image_num == 1 else "NCHW") - else: - if self.cfg.is_train: - if self.by_epoch: - msg = 'epoch%.3d_' % self.current_epoch - else: - msg = 'iter%.3d_' % self.current_iter - else: - msg = '' - makedirs(os.path.join(self.output_dir, results_dir)) - img_path = os.path.join(self.output_dir, results_dir, - msg + '%s.png' % (label)) - save_image(image_numpy, img_path) - - def save(self, epoch, name='checkpoint', keep=1): - if self.local_rank != 0: - return - - assert name in ['checkpoint', 'weight'] - - state_dicts = {} - if self.by_epoch: - save_filename = 'epoch_%s_%s.pdparams' % ( - epoch // self.iters_per_epoch, name) - else: - save_filename = 'iter_%s_%s.pdparams' % (epoch, name) - - os.makedirs(self.output_dir, exist_ok=True) - save_path = os.path.join(self.output_dir, save_filename) - for net_name, net in self.model.nets.items(): - state_dicts[net_name] = net.state_dict() - - if name == 'weight': - save(state_dicts, save_path) - return - - state_dicts['epoch'] = epoch - - for opt_name, opt in self.model.optimizers.items(): - state_dicts[opt_name] = opt.state_dict() - - save(state_dicts, save_path) - - if keep > 0: - try: - if self.by_epoch: - checkpoint_name_to_be_removed = os.path.join( - self.output_dir, 'epoch_%s_%s.pdparams' % ( - (epoch - keep * self.weight_interval) // - self.iters_per_epoch, name)) - else: - checkpoint_name_to_be_removed = os.path.join( - self.output_dir, 'iter_%s_%s.pdparams' % - (epoch - keep * self.weight_interval, name)) - - if os.path.exists(checkpoint_name_to_be_removed): - os.remove(checkpoint_name_to_be_removed) - - except Exception as e: - self.logger.info('remove old checkpoints error: {}'.format(e)) - - def resume(self, checkpoint_path): - state_dicts = load(checkpoint_path) - if state_dicts.get('epoch', None) is not None: - self.start_epoch = state_dicts['epoch'] + 1 - self.global_steps = self.iters_per_epoch * state_dicts['epoch'] - - self.current_iter = state_dicts['epoch'] + 1 - - for net_name, net in self.model.nets.items(): - net.set_state_dict(state_dicts[net_name]) - - for opt_name, opt in self.model.optimizers.items(): - opt.set_state_dict(state_dicts[opt_name]) - - def load(self, weight_path): - state_dicts = load(weight_path) - - for net_name, net in self.model.nets.items(): - if net_name in state_dicts: - net.set_state_dict(state_dicts[net_name]) - self.logger.info('Loaded pretrained weight for net {}'.format( - net_name)) - else: - self.logger.warning( - 'Can not find state dict of net {}. Skip load pretrained weight for net {}' - .format(net_name, net_name)) - - def close(self): - """ - when finish the training need close file handler or other. - """ - if self.enable_visualdl: - self.vdl_logger.close() - - -# 基础超分模型训练类 -class BasicSRNet: - def __init__(self): - self.model = {} - self.optimizer = {} - self.lr_scheduler = {} - self.min_max = '' - - def train( - self, - total_iters, - train_dataset, - test_dataset, - output_dir, - validate, - snapshot, - log, - lr_rate, - evaluate_weights='', - resume='', - pretrain_weights='', - periods=[100000], - restart_weights=[1], ): - self.lr_scheduler['learning_rate'] = lr_rate - - if self.lr_scheduler['name'] == 'CosineAnnealingRestartLR': - self.lr_scheduler['periods'] = periods - self.lr_scheduler['restart_weights'] = restart_weights - - validate = { - 'interval': validate, - 'save_img': False, - 'metrics': { - 'psnr': { - 'name': 'PSNR', - 'crop_border': 4, - 'test_y_channel': True - }, - 'ssim': { - 'name': 'SSIM', - 'crop_border': 4, - 'test_y_channel': True - } - } - } - log_config = {'interval': log, 'visiual_interval': 500} - snapshot_config = {'interval': snapshot} - - cfg = { - 'total_iters': total_iters, - 'output_dir': output_dir, - 'min_max': self.min_max, - 'model': self.model, - 'dataset': { - 'train': train_dataset, - 'test': test_dataset - }, - 'lr_scheduler': self.lr_scheduler, - 'optimizer': self.optimizer, - 'validate': validate, - 'log_config': log_config, - 'snapshot_config': snapshot_config - } - - cfg = AttrDict(cfg) - create_attr_dict(cfg) - - cfg.is_train = True - cfg.profiler_options = None - cfg.timestamp = time.strftime('-%Y-%m-%d-%H-%M', time.localtime()) - - if cfg.model.name == 'BaseSRModel': - floderModelName = cfg.model.generator.name - else: - floderModelName = cfg.model.name - cfg.output_dir = os.path.join(cfg.output_dir, - floderModelName + cfg.timestamp) - - logger_cfg = setup_logger(cfg.output_dir) - logger_cfg.info('Configs: {}'.format(cfg)) - - if paddle.is_compiled_with_cuda(): - paddle.set_device('gpu') - else: - paddle.set_device('cpu') - - # build trainer - trainer = Restorer(cfg, logger_cfg) - - # continue train or evaluate, checkpoint need contain epoch and optimizer info - if len(resume) > 0: - trainer.resume(resume) - # evaluate or finute, only load generator weights - elif len(pretrain_weights) > 0: - trainer.load(pretrain_weights) - if len(evaluate_weights) > 0: - trainer.load(evaluate_weights) - trainer.test() - return - # training, when keyboard interrupt save weights - try: - trainer.train() - except KeyboardInterrupt as e: - trainer.save(trainer.current_epoch) - - trainer.close() - - -# DRN模型训练 -class DRNet(BasicSRNet): - def __init__(self, - n_blocks=30, - n_feats=16, - n_colors=3, - rgb_range=255, - negval=0.2): - super(DRNet, self).__init__() - self.min_max = '(0., 255.)' - self.generator = { - 'name': 'DRNGenerator', - 'scale': (2, 4), - 'n_blocks': n_blocks, - 'n_feats': n_feats, - 'n_colors': n_colors, - 'rgb_range': rgb_range, - 'negval': negval - } - self.pixel_criterion = {'name': 'L1Loss'} - self.model = { - 'name': 'DRN', - 'generator': self.generator, - 'pixel_criterion': self.pixel_criterion - } - self.optimizer = { - 'optimG': { - 'name': 'Adam', - 'net_names': ['generator'], - 'weight_decay': 0.0, - 'beta1': 0.9, - 'beta2': 0.999 - }, - 'optimD': { - 'name': 'Adam', - 'net_names': ['dual_model_0', 'dual_model_1'], - 'weight_decay': 0.0, - 'beta1': 0.9, - 'beta2': 0.999 - } - } - self.lr_scheduler = { - 'name': 'CosineAnnealingRestartLR', - 'eta_min': 1e-07 - } - - -# 轻量化超分模型LESRCNN训练 -class LESRCNNet(BasicSRNet): - def __init__(self, scale=4, multi_scale=False, group=1): - super(LESRCNNet, self).__init__() - self.min_max = '(0., 1.)' - self.generator = { - 'name': 'LESRCNNGenerator', - 'scale': scale, - 'multi_scale': False, - 'group': 1 - } - self.pixel_criterion = {'name': 'L1Loss'} - self.model = { - 'name': 'BaseSRModel', - 'generator': self.generator, - 'pixel_criterion': self.pixel_criterion - } - self.optimizer = { - 'name': 'Adam', - 'net_names': ['generator'], - 'beta1': 0.9, - 'beta2': 0.99 - } - self.lr_scheduler = { - 'name': 'CosineAnnealingRestartLR', - 'eta_min': 1e-07 - } - - -# ESRGAN模型训练 -# 若loss_type='gan' 使用感知损失、对抗损失和像素损失 -# 若loss_type = 'pixel' 只使用像素损失 -class ESRGANet(BasicSRNet): - def __init__(self, loss_type='gan', in_nc=3, out_nc=3, nf=64, nb=23): - super(ESRGANet, self).__init__() - self.min_max = '(0., 1.)' - self.generator = { - 'name': 'RRDBNet', - 'in_nc': in_nc, - 'out_nc': out_nc, - 'nf': nf, - 'nb': nb - } - - if loss_type == 'gan': - # 定义损失函数 - self.pixel_criterion = {'name': 'L1Loss', 'loss_weight': 0.01} - self.discriminator = { - 'name': 'VGGDiscriminator128', - 'in_channels': 3, - 'num_feat': 64 - } - self.perceptual_criterion = { - 'name': 'PerceptualLoss', - 'layer_weights': { - '34': 1.0 - }, - 'perceptual_weight': 1.0, - 'style_weight': 0.0, - 'norm_img': False - } - self.gan_criterion = { - 'name': 'GANLoss', - 'gan_mode': 'vanilla', - 'loss_weight': 0.005 - } - # 定义模型 - self.model = { - 'name': 'ESRGAN', - 'generator': self.generator, - 'discriminator': self.discriminator, - 'pixel_criterion': self.pixel_criterion, - 'perceptual_criterion': self.perceptual_criterion, - 'gan_criterion': self.gan_criterion - } - self.optimizer = { - 'optimG': { - 'name': 'Adam', - 'net_names': ['generator'], - 'weight_decay': 0.0, - 'beta1': 0.9, - 'beta2': 0.99 - }, - 'optimD': { - 'name': 'Adam', - 'net_names': ['discriminator'], - 'weight_decay': 0.0, - 'beta1': 0.9, - 'beta2': 0.99 - } - } - self.lr_scheduler = { - 'name': 'MultiStepDecay', - 'milestones': [50000, 100000, 200000, 300000], - 'gamma': 0.5 - } - else: - self.pixel_criterion = {'name': 'L1Loss'} - self.model = { - 'name': 'BaseSRModel', - 'generator': self.generator, - 'pixel_criterion': self.pixel_criterion - } - self.optimizer = { - 'name': 'Adam', - 'net_names': ['generator'], - 'beta1': 0.9, - 'beta2': 0.99 - } - self.lr_scheduler = { - 'name': 'CosineAnnealingRestartLR', - 'eta_min': 1e-07 - } - - -# RCAN模型训练 -class RCANet(BasicSRNet): - def __init__( - self, - scale=2, - n_resgroups=10, - n_resblocks=20, ): - super(RCANet, self).__init__() - self.min_max = '(0., 255.)' - self.generator = { - 'name': 'RCAN', - 'scale': scale, - 'n_resgroups': n_resgroups, - 'n_resblocks': n_resblocks - } - self.pixel_criterion = {'name': 'L1Loss'} - self.model = { - 'name': 'RCANModel', - 'generator': self.generator, - 'pixel_criterion': self.pixel_criterion - } - self.optimizer = { - 'name': 'Adam', - 'net_names': ['generator'], - 'beta1': 0.9, - 'beta2': 0.99 - } - self.lr_scheduler = { - 'name': 'MultiStepDecay', - 'milestones': [250000, 500000, 750000, 1000000], - 'gamma': 0.5 - } diff --git a/paddlers/tasks/object_detector.py b/paddlers/tasks/object_detector.py index 261453f..bf28393 100644 --- a/paddlers/tasks/object_detector.py +++ b/paddlers/tasks/object_detector.py @@ -61,6 +61,11 @@ class BaseDetector(BaseModel): net = ppdet.modeling.__dict__[self.model_name](**params) return net + def _build_inference_net(self): + infer_net = self.net + infer_net.eval() + return infer_net + def _fix_transforms_shape(self, image_shape): raise NotImplementedError("_fix_transforms_shape: not implemented!") @@ -457,7 +462,7 @@ class BaseDetector(BaseModel): Defaults to False. Returns: - collections.OrderedDict with key-value pairs: + If `return_details` is False, return collections.OrderedDict with key-value pairs: {"bbox_mmap":`mean average precision (0.50, 11point)`}. """ @@ -556,21 +561,17 @@ class BaseDetector(BaseModel): Returns: If `img_file` is a string or np.array, the result is a list of dict with - key-value pairs: - {"category_id": `category_id`, - "category": `category`, - "bbox": `[x, y, w, h]`, - "score": `score`, - "mask": `mask`}. - If `img_file` is a list, the result is a list composed of list of dicts - with the corresponding fields: - category_id(int): the predicted category ID. 0 represents the first + the following key-value pairs: + category_id (int): Predicted category ID. 0 represents the first category in the dataset, and so on. - category(str): category name - bbox(list): bounding box in [x, y, w, h] format - score(str): confidence - mask(dict): Only for instance segmentation task. Mask of the object in - RLE format + category (str): Category name. + bbox (list): Bounding box in [x, y, w, h] format. + score (str): Confidence. + mask (dict): Only for instance segmentation task. Mask of the object in + RLE format. + + If `img_file` is a list, the result is a list composed of list of dicts + with the above keys. """ if transforms is None and not hasattr(self, 'test_transforms'): diff --git a/paddlers/tasks/restorer.py b/paddlers/tasks/restorer.py new file mode 100644 index 0000000..a7468ae --- /dev/null +++ b/paddlers/tasks/restorer.py @@ -0,0 +1,818 @@ +# Copyright (c) 2022 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 os +import os.path as osp +from collections import OrderedDict + +import numpy as np +import cv2 +import paddle +import paddle.nn.functional as F +from paddle.static import InputSpec + +import paddlers +import paddlers.models.ppgan as ppgan +import paddlers.rs_models.res as cmres +import paddlers.utils.logging as logging +from paddlers.models import res_losses +from paddlers.transforms import Resize, decode_image +from paddlers.transforms.functions import calc_hr_shape +from paddlers.utils import get_single_card_bs +from .base import BaseModel +from .utils.res_adapters import GANAdapter, OptimizerAdapter + +__all__ = [] + + +class BaseRestorer(BaseModel): + MIN_MAX = (0., 255.) + + def __init__(self, model_name, losses=None, sr_factor=None, **params): + self.init_params = locals() + if 'with_net' in self.init_params: + del self.init_params['with_net'] + super(BaseRestorer, self).__init__('restorer') + self.model_name = model_name + self.losses = losses + self.sr_factor = sr_factor + if params.get('with_net', True): + params.pop('with_net', None) + self.net = self.build_net(**params) + self.find_unused_parameters = True + + def build_net(self, **params): + # Currently, only use models from cmres. + if not hasattr(cmres, model_name): + raise ValueError("ERROR: There is no model named {}.".format( + model_name)) + net = dict(**cmres.__dict__)[self.model_name](**params) + return net + + def _build_inference_net(self): + # For GAN models, only the generator will be used for inference. + if isinstance(self.net, GANAdapter): + infer_net = self.net.generator + else: + infer_net = self.net + infer_net.eval() + return infer_net + + def _fix_transforms_shape(self, image_shape): + if hasattr(self, 'test_transforms'): + if self.test_transforms is not None: + has_resize_op = False + resize_op_idx = -1 + normalize_op_idx = len(self.test_transforms.transforms) + for idx, op in enumerate(self.test_transforms.transforms): + name = op.__class__.__name__ + if name == 'Normalize': + normalize_op_idx = idx + if 'Resize' in name: + has_resize_op = True + resize_op_idx = idx + + if not has_resize_op: + self.test_transforms.transforms.insert( + normalize_op_idx, Resize(target_size=image_shape)) + else: + self.test_transforms.transforms[resize_op_idx] = Resize( + target_size=image_shape) + + def _get_test_inputs(self, image_shape): + if image_shape is not None: + if len(image_shape) == 2: + image_shape = [1, 3] + image_shape + self._fix_transforms_shape(image_shape[-2:]) + else: + image_shape = [None, 3, -1, -1] + self.fixed_input_shape = image_shape + input_spec = [ + InputSpec( + shape=image_shape, name='image', dtype='float32') + ] + return input_spec + + def run(self, net, inputs, mode): + outputs = OrderedDict() + + if mode == 'test': + if isinstance(net, GANAdapter): + net_out = net.generator(inputs[0]) + else: + net_out = net(inputs[0]) + tar_shape = inputs[1] + if self.status == 'Infer': + res_map_list = self._postprocess( + net_out, tar_shape, transforms=inputs[2]) + else: + pred = self._postprocess( + net_out, tar_shape, transforms=inputs[2]) + res_map_list = [] + for res_map in pred: + res_map = self._tensor_to_images(res_map) + res_map_list.append(res_map) + outputs['res_map'] = res_map_list + + if mode == 'eval': + if isinstance(net, GANAdapter): + net_out = net.generator(inputs[0]) + else: + net_out = net(inputs[0]) + tar = inputs[1] + tar_shape = [tar.shape[-2:]] + pred = self._postprocess( + net_out, tar_shape, transforms=inputs[2])[0] # NCHW + pred = self._tensor_to_images(pred) + outputs['pred'] = pred + tar = self.tensor_to_images(tar) + outputs['tar'] = tar + + if mode == 'train': + # This is used by non-GAN models. + # For GAN models, self.run_gan() should be used. + net_out = net(inputs[0]) + loss = self.losses(net_out, inputs[1]) + outputs['loss'] = loss + return outputs + + def run_gan(self, net, inputs, mode, gan_mode): + raise NotImplementedError + + def default_loss(self): + return res_losses.L1Loss() + + def default_optimizer(self, + parameters, + learning_rate, + num_epochs, + num_steps_each_epoch, + lr_decay_power=0.9): + decay_step = num_epochs * num_steps_each_epoch + lr_scheduler = paddle.optimizer.lr.PolynomialDecay( + learning_rate, decay_step, end_lr=0, power=lr_decay_power) + optimizer = paddle.optimizer.Momentum( + learning_rate=lr_scheduler, + parameters=parameters, + momentum=0.9, + weight_decay=4e-5) + return optimizer + + def train(self, + num_epochs, + train_dataset, + train_batch_size=2, + eval_dataset=None, + optimizer=None, + save_interval_epochs=1, + log_interval_steps=2, + save_dir='output', + pretrain_weights=None, + learning_rate=0.01, + lr_decay_power=0.9, + early_stop=False, + early_stop_patience=5, + use_vdl=True, + resume_checkpoint=None): + """ + Train the model. + + Args: + num_epochs (int): Number of epochs. + train_dataset (paddlers.datasets.ResDataset): Training dataset. + train_batch_size (int, optional): Total batch size among all cards used in + training. Defaults to 2. + eval_dataset (paddlers.datasets.ResDataset|None, optional): Evaluation dataset. + If None, the model will not be evaluated during training process. + Defaults to None. + optimizer (paddle.optimizer.Optimizer|None, optional): Optimizer used in + training. If None, a default optimizer will be used. Defaults to None. + save_interval_epochs (int, optional): Epoch interval for saving the model. + Defaults to 1. + log_interval_steps (int, optional): Step interval for printing training + information. Defaults to 2. + save_dir (str, optional): Directory to save the model. Defaults to 'output'. + pretrain_weights (str|None, optional): None or name/path of pretrained + weights. If None, no pretrained weights will be loaded. + Defaults to None. + learning_rate (float, optional): Learning rate for training. Defaults to .01. + lr_decay_power (float, optional): Learning decay power. Defaults to .9. + early_stop (bool, optional): Whether to adopt early stop strategy. Defaults + to False. + early_stop_patience (int, optional): Early stop patience. Defaults to 5. + use_vdl (bool, optional): Whether to use VisualDL to monitor the training + process. Defaults to True. + resume_checkpoint (str|None, optional): Path of the checkpoint to resume + training from. If None, no training checkpoint will be resumed. At most + Aone of `resume_checkpoint` and `pretrain_weights` can be set simultaneously. + Defaults to None. + """ + + if self.status == 'Infer': + logging.error( + "Exported inference model does not support training.", + exit=True) + if pretrain_weights is not None and resume_checkpoint is not None: + logging.error( + "pretrain_weights and resume_checkpoint cannot be set simultaneously.", + exit=True) + + if self.losses is None: + self.losses = self.default_loss() + + if optimizer is None: + num_steps_each_epoch = train_dataset.num_samples // train_batch_size + if isinstance(self.net, GANAdapter): + parameters = {'params_g': [], 'params_d': []} + for net_g in self.net.generators: + parameters['params_g'].append(net_g.parameters()) + for net_d in self.net.discriminators: + parameters['params_d'].append(net_d.parameters()) + else: + parameters = self.net.parameters() + self.optimizer = self.default_optimizer( + parameters, learning_rate, num_epochs, num_steps_each_epoch, + lr_decay_power) + else: + self.optimizer = optimizer + + if pretrain_weights is not None and not osp.exists(pretrain_weights): + logging.warning("Path of pretrain_weights('{}') does not exist!". + format(pretrain_weights)) + elif pretrain_weights is not None and osp.exists(pretrain_weights): + if osp.splitext(pretrain_weights)[-1] != '.pdparams': + logging.error( + "Invalid pretrain weights. Please specify a '.pdparams' file.", + exit=True) + pretrained_dir = osp.join(save_dir, 'pretrain') + is_backbone_weights = pretrain_weights == 'IMAGENET' + self.net_initialize( + pretrain_weights=pretrain_weights, + save_dir=pretrained_dir, + resume_checkpoint=resume_checkpoint, + is_backbone_weights=is_backbone_weights) + + self.train_loop( + num_epochs=num_epochs, + train_dataset=train_dataset, + train_batch_size=train_batch_size, + eval_dataset=eval_dataset, + save_interval_epochs=save_interval_epochs, + log_interval_steps=log_interval_steps, + save_dir=save_dir, + early_stop=early_stop, + early_stop_patience=early_stop_patience, + use_vdl=use_vdl) + + def quant_aware_train(self, + num_epochs, + train_dataset, + train_batch_size=2, + eval_dataset=None, + optimizer=None, + save_interval_epochs=1, + log_interval_steps=2, + save_dir='output', + learning_rate=0.0001, + lr_decay_power=0.9, + early_stop=False, + early_stop_patience=5, + use_vdl=True, + resume_checkpoint=None, + quant_config=None): + """ + Quantization-aware training. + + Args: + num_epochs (int): Number of epochs. + train_dataset (paddlers.datasets.ResDataset): Training dataset. + train_batch_size (int, optional): Total batch size among all cards used in + training. Defaults to 2. + eval_dataset (paddlers.datasets.ResDataset|None, optional): Evaluation dataset. + If None, the model will not be evaluated during training process. + Defaults to None. + optimizer (paddle.optimizer.Optimizer|None, optional): Optimizer used in + training. If None, a default optimizer will be used. Defaults to None. + save_interval_epochs (int, optional): Epoch interval for saving the model. + Defaults to 1. + log_interval_steps (int, optional): Step interval for printing training + information. Defaults to 2. + save_dir (str, optional): Directory to save the model. Defaults to 'output'. + learning_rate (float, optional): Learning rate for training. + Defaults to .0001. + lr_decay_power (float, optional): Learning decay power. Defaults to .9. + early_stop (bool, optional): Whether to adopt early stop strategy. + Defaults to False. + early_stop_patience (int, optional): Early stop patience. Defaults to 5. + use_vdl (bool, optional): Whether to use VisualDL to monitor the training + process. Defaults to True. + quant_config (dict|None, optional): Quantization configuration. If None, + a default rule of thumb configuration will be used. Defaults to None. + resume_checkpoint (str|None, optional): Path of the checkpoint to resume + quantization-aware training from. If None, no training checkpoint will + be resumed. Defaults to None. + """ + + self._prepare_qat(quant_config) + self.train( + num_epochs=num_epochs, + train_dataset=train_dataset, + train_batch_size=train_batch_size, + eval_dataset=eval_dataset, + optimizer=optimizer, + save_interval_epochs=save_interval_epochs, + log_interval_steps=log_interval_steps, + save_dir=save_dir, + pretrain_weights=None, + learning_rate=learning_rate, + lr_decay_power=lr_decay_power, + early_stop=early_stop, + early_stop_patience=early_stop_patience, + use_vdl=use_vdl, + resume_checkpoint=resume_checkpoint) + + def evaluate(self, eval_dataset, batch_size=1, return_details=False): + """ + Evaluate the model. + + Args: + eval_dataset (paddlers.datasets.ResDataset): Evaluation dataset. + batch_size (int, optional): Total batch size among all cards used for + evaluation. Defaults to 1. + return_details (bool, optional): Whether to return evaluation details. + Defaults to False. + + Returns: + If `return_details` is False, return collections.OrderedDict with + key-value pairs: + {"psnr": `peak signal-to-noise ratio`, + "ssim": `structural similarity`}. + + """ + + self._check_transforms(eval_dataset.transforms, 'eval') + + self.net.eval() + nranks = paddle.distributed.get_world_size() + local_rank = paddle.distributed.get_rank() + if nranks > 1: + # Initialize parallel environment if not done. + if not paddle.distributed.parallel.parallel_helper._is_parallel_ctx_initialized( + ): + paddle.distributed.init_parallel_env() + + batch_size_each_card = get_single_card_bs(batch_size) + if batch_size_each_card > 1: + batch_size_each_card = 1 + batch_size = batch_size_each_card * paddlers.env_info['num'] + logging.warning( + "Restorer only supports batch_size=1 for each gpu/cpu card " \ + "during evaluation, so batch_size " \ + "is forcibly set to {}.".format(batch_size)) + + # TODO: Distributed evaluation + if nranks < 2 or local_rank == 0: + self.eval_data_loader = self.build_data_loader( + eval_dataset, batch_size=batch_size, mode='eval') + # XXX: Hard-code crop_border and test_y_channel + psnr = ppgan.metrics.PSNR(crop_border=4, test_y_channel=True) + ssim = ppgan.metrics.SSIM(crop_border=4, test_y_channel=True) + with paddle.no_grad(): + for step, data in enumerate(self.eval_data_loader): + outputs = self.run(self.net, data, 'eval') + psnr.update(outputs['pred'], outputs['tar']) + ssim.update(outputs['pred'], outputs['tar']) + + eval_metrics = OrderedDict( + zip(['psnr', 'ssim'], [psnr.accumulate(), ssim.accumulate()])) + + if return_details: + # TODO: Add details + return eval_metrics, None + + return eval_metrics + + def predict(self, img_file, transforms=None): + """ + Do inference. + + Args: + img_file (list[np.ndarray|str] | str | np.ndarray): Image path or decoded + image data, which also could constitute a list, meaning all images to be + predicted as a mini-batch. + transforms (paddlers.transforms.Compose|None, optional): Transforms for + inputs. If None, the transforms for evaluation process will be used. + Defaults to None. + + Returns: + If `img_file` is a tuple of string or np.array, the result is a dict with + the following key-value pairs: + res_map (np.ndarray): Restored image (HWC). + + If `img_file` is a list, the result is a list composed of dicts with the + above keys. + """ + + if transforms is None and not hasattr(self, 'test_transforms'): + raise ValueError("transforms need to be defined, now is None.") + if transforms is None: + transforms = self.test_transforms + if isinstance(img_file, (str, np.ndarray)): + images = [img_file] + else: + images = img_file + batch_im, batch_tar_shape = self._preprocess(images, transforms, + self.model_type) + self.net.eval() + data = (batch_im, batch_tar_shape, transforms.transforms) + outputs = self.run(self.net, data, 'test') + res_map_list = outputs['res_map'] + if isinstance(img_file, list): + prediction = [{'res_map': m} for m in res_map_list] + else: + prediction = {'res_map': res_map_list[0]} + return prediction + + def _preprocess(self, images, transforms, to_tensor=True): + self._check_transforms(transforms, 'test') + batch_im = list() + batch_tar_shape = list() + for im in images: + if isinstance(im, str): + im = decode_image(im, to_rgb=False) + ori_shape = im.shape[:2] + sample = {'image': im} + im = transforms(sample)[0] + batch_im.append(im) + batch_tar_shape.append(self._get_target_shape(ori_shape)) + if to_tensor: + batch_im = paddle.to_tensor(batch_im) + else: + batch_im = np.asarray(batch_im) + + return batch_im, batch_tar_shape + + def _get_target_shape(self, ori_shape): + if self.sr_factor is None: + return ori_shape + else: + return calc_hr_shape(ori_shape, self.sr_factor) + + @staticmethod + def get_transforms_shape_info(batch_tar_shape, transforms): + batch_restore_list = list() + for tar_shape in batch_tar_shape: + restore_list = list() + h, w = tar_shape[0], tar_shape[1] + for op in transforms: + if op.__class__.__name__ == 'Resize': + restore_list.append(('resize', (h, w))) + h, w = op.target_size + elif op.__class__.__name__ == 'ResizeByShort': + restore_list.append(('resize', (h, w))) + im_short_size = min(h, w) + im_long_size = max(h, w) + scale = float(op.short_size) / float(im_short_size) + if 0 < op.max_size < np.round(scale * im_long_size): + scale = float(op.max_size) / float(im_long_size) + h = int(round(h * scale)) + w = int(round(w * scale)) + elif op.__class__.__name__ == 'ResizeByLong': + restore_list.append(('resize', (h, w))) + im_long_size = max(h, w) + scale = float(op.long_size) / float(im_long_size) + h = int(round(h * scale)) + w = int(round(w * scale)) + elif op.__class__.__name__ == 'Pad': + if op.target_size: + target_h, target_w = op.target_size + else: + target_h = int( + (np.ceil(h / op.size_divisor) * op.size_divisor)) + target_w = int( + (np.ceil(w / op.size_divisor) * op.size_divisor)) + + if op.pad_mode == -1: + offsets = op.offsets + elif op.pad_mode == 0: + offsets = [0, 0] + elif op.pad_mode == 1: + offsets = [(target_h - h) // 2, (target_w - w) // 2] + else: + offsets = [target_h - h, target_w - w] + restore_list.append(('padding', (h, w), offsets)) + h, w = target_h, target_w + + batch_restore_list.append(restore_list) + return batch_restore_list + + def _postprocess(self, batch_pred, batch_tar_shape, transforms): + batch_restore_list = BaseRestorer.get_transforms_shape_info( + batch_tar_shape, transforms) + if isinstance(batch_pred, (tuple, list)) and self.status == 'Infer': + return self._infer_postprocess( + batch_res_map=batch_pred[0], + batch_restore_list=batch_restore_list) + results = [] + if batch_pred.dtype == paddle.float32: + mode = 'bilinear' + else: + mode = 'nearest' + for pred, restore_list in zip(batch_pred, batch_restore_list): + pred = paddle.unsqueeze(pred, axis=0) + for item in restore_list[::-1]: + h, w = item[1][0], item[1][1] + if item[0] == 'resize': + pred = F.interpolate( + pred, (h, w), mode=mode, data_format='NCHW') + elif item[0] == 'padding': + x, y = item[2] + pred = pred[:, :, y:y + h, x:x + w] + else: + pass + results.append(pred) + return results + + def _infer_postprocess(self, batch_res_map, batch_restore_list): + res_maps = [] + for score_map, restore_list in zip(batch_res_map, batch_restore_list): + if not isinstance(res_map, np.ndarray): + res_map = paddle.unsqueeze(res_map, axis=0) + for item in restore_list[::-1]: + h, w = item[1][0], item[1][1] + if item[0] == 'resize': + if isinstance(res_map, np.ndarray): + res_map = cv2.resize( + res_map, (w, h), interpolation=cv2.INTER_LINEAR) + else: + res_map = F.interpolate( + score_map, (h, w), + mode='bilinear', + data_format='NHWC') + elif item[0] == 'padding': + x, y = item[2] + if isinstance(res_map, np.ndarray): + res_map = res_map[..., y:y + h, x:x + w] + else: + res_map = res_map[:, :, y:y + h, x:x + w] + else: + pass + res_map = res_map.squeeze() + if not isinstance(res_map, np.ndarray): + res_map = res_map.numpy() + res_map = self._normalize(res_map) + res_maps.append(res_map.squeeze()) + return res_maps + + def _check_transforms(self, transforms, mode): + super()._check_transforms(transforms, mode) + if not isinstance(transforms.arrange, + paddlers.transforms.ArrangeRestorer): + raise TypeError( + "`transforms.arrange` must be an ArrangeRestorer object.") + + def set_losses(self, losses): + self.losses = losses + + def _tensor_to_images(self, tensor, squeeze=True, quantize=True): + tensor = paddle.transpose(tensor, perm=[0, 2, 3, 1]) # NHWC + if squeeze: + tensor = tensor.squeeze() + images = tensor.numpy().astype('float32') + images = np.clip(images, self.MIN_MAX[0], self.MIN_MAX[1]) + images = self._normalize(images, copy=True, quantize=quantize) + return images + + def _normalize(self, im, copy=False, quantize=True): + if copy: + im = im.copy() + im -= im.min() + im /= im.max() + 1e-32 + if quantize: + im *= 255 + im = im.astype('uint8') + return im + + +class RCAN(BaseRestorer): + def __init__(self, + losses=None, + sr_factor=4, + n_resgroups=10, + n_resblocks=20, + n_feats=64, + n_colors=3, + rgb_range=255, + kernel_size=3, + reduction=16, + **params): + params.update({ + 'factor': sr_factor, + 'n_resgroups': n_resgroups, + 'n_resblocks': n_resblocks, + 'n_feats': n_feats, + 'n_colors': n_colors, + 'rgb_range': rgb_range, + 'kernel_size': kernel_size, + 'reduction': reduction + }) + super(RCAN, self).__init__( + model_name='RCAN', losses=losses, sr_factor=sr_factor, **params) + + +class DRN(BaseRestorer): + def __init__(self, + losses=None, + sr_factor=4, + scale=(2, 4), + n_blocks=30, + n_feats=16, + n_colors=3, + rgb_range=255, + negval=0.2, + **params): + if sr_factor != max(scale): + raise ValueError(f"`sr_factor` must be equal to `max(scale)`.") + params.update({ + 'scale': scale, + 'n_blocks': n_blocks, + 'n_feats': n_feats, + 'n_colors': n_colors, + 'rgb_range': rgb_range, + 'negval': negval + }) + super(DRN, self).__init__( + model_name='DRN', losses=losses, sr_factor=sr_factor, **params) + + def build_net(self, **params): + net = ppgan.models.generators.DRNGenerator(**params) + return net + + +class LESRCNN(BaseRestorer): + def __init__(self, losses=None, sr_factor=4, multi_scale=False, group=1): + params.update({'scale': sr_factor, 'multi_scale': False, 'group': 1}) + super(LESRCNN, self).__init__( + model_name='LESRCNN', losses=losses, sr_factor=sr_factor, **params) + + def build_net(self, **params): + net = ppgan.models.generators.LESRCNNGenerator(**params) + return net + + +class ESRGAN(BaseRestorer): + MIN_MAX = (0., 1.) + + def __init__(self, + losses=None, + sr_factor=4, + use_gan=True, + in_channels=3, + out_channels=3, + nf=64, + nb=23): + params.update({ + 'scale': sr_factor, + 'in_nc': in_channels, + 'out_nc': out_channels, + 'nf': nf, + 'nb': nb + }) + self.use_gan = use_gan + super(ESRGAN, self).__init__( + model_name='ESRGAN', losses=losses, sr_factor=sr_factor, **params) + + def build_net(self, **params): + generator = ppgan.models.generators.RRDBNet(**params) + if self.use_gan: + discriminator = ppgan.models.discriminators.VGGDiscrinimator128( + in_channels=params['out_nc'], num_feat=64) + net = GANAdapter( + generators=[generator], discriminators=[discriminator]) + else: + net = generator + return net + + def default_loss(self): + if self.use_gan: + self.losses = { + 'pixel': res_losses.L1Loss(loss_weight=0.01), + 'perceptual': + res_losses.PerceptualLoss(layer_weights={'34': 1.0}), + 'gan': res_losses.GANLoss( + gan_mode='vanilla', loss_weight=0.005) + } + else: + return res_losses.L1Loss() + + def default_optimizer(self, parameters, *args, **kwargs): + if self.use_gan: + optim_g = super(ESRGAN, self).default_optimizer( + parameters['optims_g'][0], *args, **kwargs) + optim_d = super(ESRGAN, self).default_optimizer( + parameters['optims_d'][0], *args, **kwargs) + return OptimizerAdapter(optim_g, optim_d) + else: + return super(ESRGAN, self).default_optimizer(params, *args, + **kwargs) + + def run_gan(self, net, inputs, mode, gan_mode='forward_g'): + if mode != 'train': + raise ValueError("`mode` is not 'train'.") + outputs = OrderedDict() + if gan_mode == 'forward_g': + loss_g = 0 + g_pred = net.generator(inputs[0]) + loss_pix = self.losses['pixel'](g_pred, tar) + loss_perc, loss_sty = self.losses['perceptual'](g_pred, tar) + loss_g += loss_pix + if loss_perc is not None: + loss_g += loss_perc + if loss_sty is not None: + loss_g += loss_sty + self._set_requires_grad(net.discriminator, False) + real_d_pred = net.discriminator(inputs[1]).detach() + fake_g_pred = net.discriminator(g_pred) + loss_g_real = self.losses['gan']( + real_d_pred - paddle.mean(fake_g_pred), False, + is_disc=False) * 0.5 + loss_g_fake = self.losses['gan']( + fake_g_pred - paddle.mean(real_d_pred), True, + is_disc=False) * 0.5 + loss_g_gan = loss_g_real + loss_g_fake + outputs['g_pred'] = g_pred.detach() + outputs['loss_g_pps'] = loss_g + outputs['loss_g_gan'] = loss_g_gan + elif gan_mode == 'forward_d': + self._set_requires_grad(net.discriminator, True) + # Real + fake_d_pred = net.discriminator(data[0]).detach() + real_d_pred = net.discriminator(data[1]) + loss_d_real = self.losses['gan']( + real_d_pred - paddle.mean(fake_d_pred), True, + is_disc=True) * 0.5 + # Fake + fake_d_pred = self.nets['discriminator'](self.output.detach()) + loss_d_fake = self.gan_criterion( + fake_d_pred - paddle.mean(real_d_pred.detach()), + False, + is_disc=True) * 0.5 + outputs['loss_d'] = loss_d_real + loss_d_fake + else: + raise ValueError("Invalid `gan_mode`!") + return outputs + + def train_step(self, step, data, net): + if self.use_gan: + optim_g, optim_d = self.optimizer + + outputs = self.run_gan(net, data, gan_mode='forward_g') + optim_g.clear_grad() + (outputs['loss_g_pps'] + outputs['loss_g_gan']).backward() + optim_g.step() + + outputs.update( + self.run_gan( + net, (outputs['g_pred'], data[1]), gan_mode='forward_d')) + optim_d.clear_grad() + outputs['loss_d'].backward() + optim_d.step() + + outputs['loss'] = outupts['loss_g_pps'] + outputs[ + 'loss_g_gan'] + outputs['loss_d'] + + if isinstance(optim_g._learning_rate, + paddle.optimizer.lr.LRScheduler): + # If ReduceOnPlateau is used as the scheduler, use the loss value as the metric. + if isinstance(optim_g._learning_rate, + paddle.optimizer.lr.ReduceOnPlateau): + optim_g._learning_rate.step(loss.item()) + else: + optim_g._learning_rate.step() + + if isinstance(optim_d._learning_rate, + paddle.optimizer.lr.LRScheduler): + if isinstance(optim_d._learning_rate, + paddle.optimizer.lr.ReduceOnPlateau): + optim_d._learning_rate.step(loss.item()) + else: + optim_d._learning_rate.step() + + return outputs + else: + super(ESRGAN, self).train_step(step, data, net) + + def _set_requires_grad(self, net, requires_grad): + for p in net.parameters(): + p.trainable = requires_grad diff --git a/paddlers/tasks/segmenter.py b/paddlers/tasks/segmenter.py index 95c67a5..7589a9d 100644 --- a/paddlers/tasks/segmenter.py +++ b/paddlers/tasks/segmenter.py @@ -33,6 +33,7 @@ from paddlers.utils import get_single_card_bs, DisablePrint from paddlers.utils.checkpoint import seg_pretrain_weights_dict from .base import BaseModel from .utils import seg_metrics as metrics +from .utils.infer_nets import InferNet __all__ = ["UNet", "DeepLabV3P", "FastSCNN", "HRNet", "BiSeNetV2", "FarSeg"] @@ -64,11 +65,16 @@ class BaseSegmenter(BaseModel): def build_net(self, **params): # TODO: when using paddle.utils.unique_name.guard, - # DeepLabv3p and HRNet will raise a error + # DeepLabv3p and HRNet will raise an error. net = dict(ppseg.models.__dict__, **cmseg.__dict__)[self.model_name]( num_classes=self.num_classes, **params) return net + def _build_inference_net(self): + infer_net = InferNet(self.net, self.model_type) + infer_net.eval() + return infer_net + def _fix_transforms_shape(self, image_shape): if hasattr(self, 'test_transforms'): if self.test_transforms is not None: @@ -472,7 +478,6 @@ class BaseSegmenter(BaseModel): conf_mat_all.append(conf_mat) class_iou, miou = ppseg.utils.metrics.mean_iou( intersect_area_all, pred_area_all, label_area_all) - # TODO 确认是按oacc还是macc class_acc, oacc = ppseg.utils.metrics.accuracy(intersect_area_all, pred_area_all) kappa = ppseg.utils.metrics.kappa(intersect_area_all, pred_area_all, @@ -504,13 +509,13 @@ class BaseSegmenter(BaseModel): Defaults to None. Returns: - If `img_file` is a string or np.array, the result is a dict with key-value - pairs: - {"label map": `label map`, "score_map": `score map`}. + If `img_file` is a tuple of string or np.array, the result is a dict with + the following key-value pairs: + label_map (np.ndarray): Predicted label map (HW). + score_map (np.ndarray): Prediction score map (HWC). + If `img_file` is a list, the result is a list composed of dicts with the - corresponding fields: - label_map (np.ndarray): the predicted label map (HW) - score_map (np.ndarray): the prediction score map (HWC) + above keys. """ if transforms is None and not hasattr(self, 'test_transforms'): diff --git a/paddlers/tasks/utils/res_adapters.py b/paddlers/tasks/utils/res_adapters.py new file mode 100644 index 0000000..bcca4c0 --- /dev/null +++ b/paddlers/tasks/utils/res_adapters.py @@ -0,0 +1,128 @@ +from functools import wraps +from inspect import isfunction, isgeneratorfunction, getmembers +from collections.abc import Sequence +from abc import ABC + +import paddle +import paddle.nn as nn + +__all__ = ['GANAdapter', 'OptimizerAdapter'] + + +class _AttrDesc: + def __init__(self, key): + self.key = key + + def __get__(self, instance, owner): + return tuple(getattr(ele, self.key) for ele in instance) + + def __set__(self, instance, value): + for ele in instance: + setattr(ele, self.key, value) + + +def _func_deco(cls, func_name): + @wraps(getattr(cls.__ducktype__, func_name)) + def _wrapper(self, *args, **kwargs): + return tuple(getattr(ele, func_name)(*args, **kwargs) for ele in self) + + return _wrapper + + +def _generator_deco(cls, func_name): + @wraps(getattr(cls.__ducktype__, func_name)) + def _wrapper(self, *args, **kwargs): + for ele in self: + yield from getattr(ele, func_name)(*args, **kwargs) + + return _wrapper + + +class Adapter(Sequence, ABC): + __ducktype__ = object + __ava__ = () + + def __init__(self, *args): + if not all(map(self._check, args)): + raise TypeError("Please check the input type.") + self._seq = tuple(args) + + def __getitem__(self, key): + return self._seq[key] + + def __len__(self): + return len(self._seq) + + def __repr__(self): + return repr(self._seq) + + @classmethod + def _check(cls, obj): + for attr in cls.__ava__: + try: + getattr(obj, attr) + # TODO: Check function signature + except AttributeError: + return False + return True + + +def make_adapter(cls): + members = dict(getmembers(cls.__ducktype__)) + for k in cls.__ava__: + if hasattr(cls, k): + continue + if k in members: + v = members[k] + if isgeneratorfunction(v): + setattr(cls, k, _generator_deco(cls, k)) + elif isfunction(v): + setattr(cls, k, _func_deco(cls, k)) + else: + setattr(cls, k, _AttrDesc(k)) + return cls + + +class GANAdapter(nn.Layer): + __ducktype__ = nn.Layer + __ava__ = ('state_dict', 'set_state_dict', 'train', 'eval') + + def __init__(self, generators, discriminators): + super(GANAdapter, self).__init__() + self.generators = nn.LayerList(generators) + self.discriminators = nn.LayerList(discriminators) + self._m = [*generators, *discriminators] + + def __len__(self): + return len(self._m) + + def __getitem__(self, key): + return self._m[key] + + def __contains__(self, m): + return m in self._m + + def __repr__(self): + return repr(self._m) + + @property + def generator(self): + return self.generators[0] + + @property + def discriminator(self): + return self.discriminators[0] + + +Adapter.register(GANAdapter) + + +@make_adapter +class OptimizerAdapter(Adapter): + __ducktype__ = paddle.optimizer.Optimizer + __ava__ = ('state_dict', 'set_state_dict', 'clear_grad', 'step', 'get_lr') + + # Special dispatching rule + def set_state_dict(self, state_dicts): + for optim, state_dict in zip(self, state_dicts): + optim.set_state_dict(state_dict) diff --git a/paddlers/transforms/functions.py b/paddlers/transforms/functions.py index 12c3e9a..5550e33 100644 --- a/paddlers/transforms/functions.py +++ b/paddlers/transforms/functions.py @@ -638,3 +638,7 @@ def decode_seg_mask(mask_path): mask = np.asarray(Image.open(mask_path)) mask = mask.astype('int64') return mask + + +def calc_hr_shape(lr_shape, sr_factor): + return tuple(int(s * sr_factor) for s in lr_shape) diff --git a/paddlers/transforms/operators.py b/paddlers/transforms/operators.py index ec9b424..cb36f14 100644 --- a/paddlers/transforms/operators.py +++ b/paddlers/transforms/operators.py @@ -35,7 +35,7 @@ from .functions import ( horizontal_flip_poly, horizontal_flip_rle, vertical_flip_poly, vertical_flip_rle, crop_poly, crop_rle, expand_poly, expand_rle, resize_poly, resize_rle, dehaze, select_bands, to_intensity, to_uint8, - img_flip, img_simple_rotate, decode_seg_mask) + img_flip, img_simple_rotate, decode_seg_mask, calc_hr_shape) __all__ = [ "Compose", "DecodeImg", "Resize", "RandomResize", "ResizeByShort", @@ -44,7 +44,7 @@ __all__ = [ "RandomScaleAspect", "RandomExpand", "Pad", "MixupImage", "RandomDistort", "RandomBlur", "RandomSwap", "Dehaze", "ReduceDim", "SelectBand", "ArrangeSegmenter", "ArrangeChangeDetector", "ArrangeClassifier", - "ArrangeDetector", "RandomFlipOrRotate", "ReloadMask" + "ArrangeDetector", "ArrangeRestorer", "RandomFlipOrRotate", "ReloadMask" ] interp_dict = { @@ -154,6 +154,8 @@ class Transform(object): if 'aux_masks' in sample: sample['aux_masks'] = list( map(self.apply_mask, sample['aux_masks'])) + if 'target' in sample: + sample['target'] = self.apply_im(sample['target']) return sample @@ -336,6 +338,14 @@ class DecodeImg(Transform): map(self.apply_mask, sample['aux_masks'])) # TODO: check the shape of auxiliary masks + if 'target' in sample: + if self.read_geo_info: + target, geo_info_dict = self.apply_im(sample['target']) + sample['target'] = target + sample['geo_info_dict_tar'] = geo_info_dict + else: + sample['target'] = self.apply_im(sample['target']) + sample['im_shape'] = np.array( sample['image'].shape[:2], dtype=np.float32) sample['scale_factor'] = np.array([1., 1.], dtype=np.float32) @@ -457,6 +467,17 @@ class Resize(Transform): if 'gt_poly' in sample and len(sample['gt_poly']) > 0: sample['gt_poly'] = self.apply_segm( sample['gt_poly'], [im_h, im_w], [im_scale_x, im_scale_y]) + if 'target' in sample: + if 'sr_factor' in sample: + # For SR tasks + sample['target'] = self.apply_im( + sample['target'], interp, + calc_hr_shape(target_size, sample['sr_factor'])) + else: + # For non-SR tasks + sample['target'] = self.apply_im(sample['target'], interp, + target_size) + sample['im_shape'] = np.asarray( sample['image'].shape[:2], dtype=np.float32) if 'scale_factor' in sample: @@ -730,6 +751,9 @@ class RandomFlipOrRotate(Transform): if 'gt_poly' in sample and len(sample['gt_poly']) > 0: sample['gt_poly'] = self.apply_segm(sample['gt_poly'], mode_id, True) + if 'target' in sample: + sample['target'] = self.apply_im(sample['target'], mode_id, + True) elif p_m < self.probs[1]: mode_p = random.random() mode_id = self.judge_probs_range(mode_p, self.probsr) @@ -750,6 +774,9 @@ class RandomFlipOrRotate(Transform): if 'gt_poly' in sample and len(sample['gt_poly']) > 0: sample['gt_poly'] = self.apply_segm(sample['gt_poly'], mode_id, False) + if 'target' in sample: + sample['target'] = self.apply_im(sample['target'], mode_id, + False) return sample @@ -809,6 +836,8 @@ class RandomHorizontalFlip(Transform): if 'gt_poly' in sample and len(sample['gt_poly']) > 0: sample['gt_poly'] = self.apply_segm(sample['gt_poly'], im_h, im_w) + if 'target' in sample: + sample['target'] = self.apply_im(sample['target']) return sample @@ -867,6 +896,8 @@ class RandomVerticalFlip(Transform): if 'gt_poly' in sample and len(sample['gt_poly']) > 0: sample['gt_poly'] = self.apply_segm(sample['gt_poly'], im_h, im_w) + if 'target' in sample: + sample['target'] = self.apply_im(sample['target']) return sample @@ -886,13 +917,16 @@ class Normalize(Transform): image(s). Defaults to [0, 0, 0, ]. max_val (list[float] | tuple[float], optional): Max value of input image(s). Defaults to [255., 255., 255.]. + apply_to_tar (bool, optional): Whether to apply transformation to the target + image. Defaults to True. """ def __init__(self, mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225], min_val=None, - max_val=None): + max_val=None, + apply_to_tar=True): super(Normalize, self).__init__() channel = len(mean) if min_val is None: @@ -914,6 +948,7 @@ class Normalize(Transform): self.std = std self.min_val = min_val self.max_val = max_val + self.apply_to_tar = apply_to_tar def apply_im(self, image): image = image.astype(np.float32) @@ -927,6 +962,8 @@ class Normalize(Transform): sample['image'] = self.apply_im(sample['image']) if 'image2' in sample: sample['image2'] = self.apply_im(sample['image2']) + if 'target' in sample and self.apply_to_tar: + sample['target'] = self.apply_im(sample['target']) return sample @@ -964,6 +1001,8 @@ class CenterCrop(Transform): if 'aux_masks' in sample: sample['aux_masks'] = list( map(self.apply_mask, sample['aux_masks'])) + if 'target' in sample: + sample['target'] = self.apply_im(sample['target']) return sample @@ -1165,6 +1204,14 @@ class RandomCrop(Transform): self.apply_mask, crop=crop_box), sample['aux_masks'])) + if 'target' in sample: + if 'sr_factor' in sample: + sample['target'] = self.apply_im( + sample['image'], + calc_hr_shape(crop_box, sample['sr_factor'])) + else: + sample['target'] = self.apply_im(sample['image'], crop_box) + if self.crop_size is not None: sample = Resize(self.crop_size)(sample) @@ -1266,6 +1313,7 @@ class Pad(Transform): pad_mode (int, optional): Pad mode. Currently only four modes are supported: [-1, 0, 1, 2]. if -1, use specified offsets. If 0, only pad to right and bottom If 1, pad according to center. If 2, only pad left and top. Defaults to 0. + offsets (list[int]|None, optional): Padding offsets. Defaults to None. im_padding_value (list[float] | tuple[float]): RGB value of padded area. Defaults to (127.5, 127.5, 127.5). label_padding_value (int, optional): Filling value for the mask. @@ -1332,6 +1380,17 @@ class Pad(Transform): expand_rle(segm, x, y, height, width, h, w)) return expanded_segms + def _get_offsets(self, im_h, im_w, h, w): + if self.pad_mode == -1: + offsets = self.offsets + elif self.pad_mode == 0: + offsets = [0, 0] + elif self.pad_mode == 1: + offsets = [(w - im_w) // 2, (h - im_h) // 2] + else: + offsets = [w - im_w, h - im_h] + return offsets + def apply(self, sample): im_h, im_w = sample['image'].shape[:2] if self.target_size: @@ -1349,14 +1408,7 @@ class Pad(Transform): if h == im_h and w == im_w: return sample - if self.pad_mode == -1: - offsets = self.offsets - elif self.pad_mode == 0: - offsets = [0, 0] - elif self.pad_mode == 1: - offsets = [(w - im_w) // 2, (h - im_h) // 2] - else: - offsets = [w - im_w, h - im_h] + offsets = self._get_offsets(im_h, im_w, h, w) sample['image'] = self.apply_im(sample['image'], offsets, (h, w)) if 'image2' in sample: @@ -1373,6 +1425,16 @@ class Pad(Transform): if 'gt_poly' in sample and len(sample['gt_poly']) > 0: sample['gt_poly'] = self.apply_segm( sample['gt_poly'], offsets, im_size=[im_h, im_w], size=[h, w]) + if 'target' in sample: + if 'sr_factor' in sample: + hr_shape = calc_hr_shape((h, w), sample['sr_factor']) + hr_offsets = self._get_offsets(*sample['target'].shape[:2], + *hr_shape) + sample['target'] = self.apply_im(sample['target'], hr_offsets, + hr_shape) + else: + sample['target'] = self.apply_im(sample['target'], offsets, + (h, w)) return sample @@ -1688,15 +1750,18 @@ class ReduceDim(Transform): Args: joblib_path (str): Path of *.joblib file of PCA. + apply_to_tar (bool, optional): Whether to apply transformation to the target + image. Defaults to True. """ - def __init__(self, joblib_path): + def __init__(self, joblib_path, apply_to_tar=True): super(ReduceDim, self).__init__() ext = joblib_path.split(".")[-1] if ext != "joblib": raise ValueError("`joblib_path` must be *.joblib, not *.{}.".format( ext)) self.pca = load(joblib_path) + self.apply_to_tar = apply_to_tar def apply_im(self, image): H, W, C = image.shape @@ -1709,6 +1774,8 @@ class ReduceDim(Transform): sample['image'] = self.apply_im(sample['image']) if 'image2' in sample: sample['image2'] = self.apply_im(sample['image2']) + if 'target' in sample and self.apply_to_tar: + sample['target'] = self.apply_im(sample['target']) return sample @@ -1719,11 +1786,14 @@ class SelectBand(Transform): Args: band_list (list, optional): Bands to select (band index starts from 1). Defaults to [1, 2, 3]. + apply_to_tar (bool, optional): Whether to apply transformation to the target + image. Defaults to True. """ - def __init__(self, band_list=[1, 2, 3]): + def __init__(self, band_list=[1, 2, 3], apply_to_tar=True): super(SelectBand, self).__init__() self.band_list = band_list + self.appy_to_tar = apply_to_tar def apply_im(self, image): image = select_bands(image, self.band_list) @@ -1733,6 +1803,8 @@ class SelectBand(Transform): sample['image'] = self.apply_im(sample['image']) if 'image2' in sample: sample['image2'] = self.apply_im(sample['image2']) + if 'target' in sample and self.apply_to_tar: + sample['target'] = self.apply_im(sample['target']) return sample @@ -1820,6 +1892,8 @@ class _Permute(Transform): sample['image'] = permute(sample['image'], False) if 'image2' in sample: sample['image2'] = permute(sample['image2'], False) + if 'target' in sample: + sample['target'] = permute(sample['target'], False) return sample @@ -1915,3 +1989,15 @@ class ArrangeDetector(Arrange): if self.mode == 'eval' and 'gt_poly' in sample: del sample['gt_poly'] return sample + + +class ArrangeRestorer(Arrange): + def apply(self, sample): + image = permute(sample['image'], False) + target = permute(sample['target'], False) + if self.mode == 'train': + return image, target + if self.mode == 'eval': + return image, target + if self.mode == 'test': + return image, diff --git a/tutorials/train/README.md b/tutorials/train/README.md index c63cf26..6c553ce 100644 --- a/tutorials/train/README.md +++ b/tutorials/train/README.md @@ -17,9 +17,10 @@ |classification/hrnet.py | 场景分类 | HRNet | |classification/mobilenetv3.py | 场景分类 | MobileNetV3 | |classification/resnet50_vd.py | 场景分类 | ResNet50-vd | -|image_restoration/drn.py | 超分辨率 | DRN | -|image_restoration/esrgan.py | 超分辨率 | ESRGAN | -|image_restoration/lesrcnn.py | 超分辨率 | LESRCNN | +|image_restoration/drn.py | 图像复原 | DRN | +|image_restoration/esrgan.py | 图像复原 | ESRGAN | +|image_restoration/lesrcnn.py | 图像复原 | LESRCNN | +|image_restoration/rcan.py | 图像复原 | RCAN | |object_detection/faster_rcnn.py | 目标检测 | Faster R-CNN | |object_detection/ppyolo.py | 目标检测 | PP-YOLO | |object_detection/ppyolotiny.py | 目标检测 | PP-YOLO Tiny | diff --git a/tutorials/train/classification/hrnet.py b/tutorials/train/classification/hrnet.py index 658dcef..7a89843 100644 --- a/tutorials/train/classification/hrnet.py +++ b/tutorials/train/classification/hrnet.py @@ -65,7 +65,7 @@ eval_dataset = pdrs.datasets.ClasDataset( num_workers=0, shuffle=False) -# 使用默认参数构建HRNet模型 +# 构建HRNet模型 # 目前已支持的模型请参考:https://github.com/PaddlePaddle/PaddleRS/blob/develop/docs/intro/model_zoo.md # 模型输入参数请参考:https://github.com/PaddlePaddle/PaddleRS/blob/develop/paddlers/tasks/classifier.py model = pdrs.tasks.clas.HRNet_W18_C(num_classes=len(train_dataset.labels)) diff --git a/tutorials/train/classification/mobilenetv3.py b/tutorials/train/classification/mobilenetv3.py index 1d85a06..36efe29 100644 --- a/tutorials/train/classification/mobilenetv3.py +++ b/tutorials/train/classification/mobilenetv3.py @@ -65,7 +65,7 @@ eval_dataset = pdrs.datasets.ClasDataset( num_workers=0, shuffle=False) -# 使用默认参数构建MobileNetV3模型 +# 构建MobileNetV3模型 # 目前已支持的模型请参考:https://github.com/PaddlePaddle/PaddleRS/blob/develop/docs/intro/model_zoo.md # 模型输入参数请参考:https://github.com/PaddlePaddle/PaddleRS/blob/develop/paddlers/tasks/classifier.py model = pdrs.tasks.clas.MobileNetV3_small_x1_0( diff --git a/tutorials/train/classification/resnet50_vd.py b/tutorials/train/classification/resnet50_vd.py index 40891e6..a0957f2 100644 --- a/tutorials/train/classification/resnet50_vd.py +++ b/tutorials/train/classification/resnet50_vd.py @@ -65,7 +65,7 @@ eval_dataset = pdrs.datasets.ClasDataset( num_workers=0, shuffle=False) -# 使用默认参数构建ResNet50-vd模型 +# 构建ResNet50-vd模型 # 目前已支持的模型请参考:https://github.com/PaddlePaddle/PaddleRS/blob/develop/docs/intro/model_zoo.md # 模型输入参数请参考:https://github.com/PaddlePaddle/PaddleRS/blob/develop/paddlers/tasks/classifier.py model = pdrs.tasks.clas.ResNet50_vd(num_classes=len(train_dataset.labels)) diff --git a/tutorials/train/image_restoration/data/.gitignore b/tutorials/train/image_restoration/data/.gitignore new file mode 100644 index 0000000..2d1d39b --- /dev/null +++ b/tutorials/train/image_restoration/data/.gitignore @@ -0,0 +1,3 @@ +*.zip +*.tar.gz +rssr/ \ No newline at end of file diff --git a/tutorials/train/image_restoration/drn.py b/tutorials/train/image_restoration/drn.py new file mode 100644 index 0000000..abd7e1d --- /dev/null +++ b/tutorials/train/image_restoration/drn.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python + +# 图像复原模型DRN训练示例脚本 +# 执行此脚本前,请确认已正确安装PaddleRS库 + +import paddlers as pdrs +from paddlers import transforms as T + +# 数据集存放目录 +DATA_DIR = './data/rssr/' +# 训练集`file_list`文件路径 +TRAIN_FILE_LIST_PATH = './data/rssr/train.txt' +# 验证集`file_list`文件路径 +EVAL_FILE_LIST_PATH = './data/rssr/val.txt' +# 实验目录,保存输出的模型权重和结果 +EXP_DIR = './output/drn/' + +# 下载和解压遥感影像超分辨率数据集 +pdrs.utils.download_and_decompress( + 'https://paddlers.bj.bcebos.com/datasets/rssr.zip', path='./data/') + +# 定义训练和验证时使用的数据变换(数据增强、预处理等) +# 使用Compose组合多种变换方式。Compose中包含的变换将按顺序串行执行 +# API说明:https://github.com/PaddlePaddle/PaddleRS/blob/develop/docs/apis/data.md +train_transforms = T.Compose([ + # 读取影像 + T.DecodeImg(), + # 将输入影像缩放到256x256大小 + T.Resize(target_size=256), + # 以50%的概率实施随机水平翻转 + T.RandomHorizontalFlip(prob=0.5), + # 以50%的概率实施随机垂直翻转 + T.RandomVerticalFlip(prob=0.5), + # 将数据归一化到[0,1] + T.Normalize( + mean=[0.0, 0.0, 0.0], std=[1.0, 1.0, 1.0]), + T.ArrangeRestorer('train') +]) + +eval_transforms = T.Compose([ + T.DecodeImg(), + T.Resize(target_size=256), + # 验证阶段与训练阶段的数据归一化方式必须相同 + T.Normalize( + mean=[0.0, 0.0, 0.0], std=[1.0, 1.0, 1.0]), + T.ArrangeRestorer('eval') +]) + +# 分别构建训练和验证所用的数据集 +train_dataset = pdrs.datasets.ResDataset( + data_dir=DATA_DIR, + file_list=TRAIN_FILE_LIST_PATH, + transforms=train_transforms, + num_workers=0, + shuffle=True) + +eval_dataset = pdrs.datasets.ResDataset( + data_dir=DATA_DIR, + file_list=EVAL_FILE_LIST_PATH, + transforms=eval_transforms, + num_workers=0, + shuffle=False) + +# 使用默认参数构建DRN模型 +# 目前已支持的模型请参考:https://github.com/PaddlePaddle/PaddleRS/blob/develop/docs/intro/model_zoo.md +# 模型输入参数请参考:https://github.com/PaddlePaddle/PaddleRS/blob/develop/paddlers/tasks/restorer.py +model = pdrs.tasks.res.DRN() + +# 执行模型训练 +model.train( + num_epochs=10, + train_dataset=train_dataset, + train_batch_size=8, + eval_dataset=eval_dataset, + save_interval_epochs=1, + # 每多少次迭代记录一次日志 + log_interval_steps=50, + save_dir=EXP_DIR, + # 初始学习率大小 + learning_rate=0.01, + # 是否使用early stopping策略,当精度不再改善时提前终止训练 + early_stop=False, + # 是否启用VisualDL日志功能 + use_vdl=True, + # 指定从某个检查点继续训练 + resume_checkpoint=None) diff --git a/tutorials/train/image_restoration/drn_train.py b/tutorials/train/image_restoration/drn_train.py deleted file mode 100644 index 2d871a5..0000000 --- a/tutorials/train/image_restoration/drn_train.py +++ /dev/null @@ -1,80 +0,0 @@ -import os -import sys -sys.path.append(os.path.abspath('../PaddleRS')) - -import paddle -import paddlers as pdrs - -# 定义训练和验证时的transforms -train_transforms = pdrs.datasets.ComposeTrans( - input_keys=['lq', 'gt'], - output_keys=['lq', 'lqx2', 'gt'], - pipelines=[{ - 'name': 'SRPairedRandomCrop', - 'gt_patch_size': 192, - 'scale': 4, - 'scale_list': True - }, { - 'name': 'PairedRandomHorizontalFlip' - }, { - 'name': 'PairedRandomVerticalFlip' - }, { - 'name': 'PairedRandomTransposeHW' - }, { - 'name': 'Transpose' - }, { - 'name': 'Normalize', - 'mean': [0.0, 0.0, 0.0], - 'std': [1.0, 1.0, 1.0] - }]) - -test_transforms = pdrs.datasets.ComposeTrans( - input_keys=['lq', 'gt'], - output_keys=['lq', 'gt'], - pipelines=[{ - 'name': 'Transpose' - }, { - 'name': 'Normalize', - 'mean': [0.0, 0.0, 0.0], - 'std': [1.0, 1.0, 1.0] - }]) - -# 定义训练集 -train_gt_floder = r"../work/RSdata_for_SR/trian_HR" # 高分辨率影像所在路径 -train_lq_floder = r"../work/RSdata_for_SR/train_LR/x4" # 低分辨率影像所在路径 -num_workers = 4 -batch_size = 8 -scale = 4 -train_dataset = pdrs.datasets.SRdataset( - mode='train', - gt_floder=train_gt_floder, - lq_floder=train_lq_floder, - transforms=train_transforms(), - scale=scale, - num_workers=num_workers, - batch_size=batch_size) -train_dict = train_dataset() - -# 定义测试集 -test_gt_floder = r"../work/RSdata_for_SR/test_HR" -test_lq_floder = r"../work/RSdata_for_SR/test_LR/x4" -test_dataset = pdrs.datasets.SRdataset( - mode='test', - gt_floder=test_gt_floder, - lq_floder=test_lq_floder, - transforms=test_transforms(), - scale=scale) - -# 初始化模型,可以对网络结构的参数进行调整 -model = pdrs.tasks.res.DRNet( - n_blocks=30, n_feats=16, n_colors=3, rgb_range=255, negval=0.2) - -model.train( - total_iters=100000, - train_dataset=train_dataset(), - test_dataset=test_dataset(), - output_dir='output_dir', - validate=5000, - snapshot=5000, - lr_rate=0.0001, - log=10) diff --git a/tutorials/train/image_restoration/esrgan.py b/tutorials/train/image_restoration/esrgan.py new file mode 100644 index 0000000..5d97def --- /dev/null +++ b/tutorials/train/image_restoration/esrgan.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python + +# 图像复原模型ESRGAN训练示例脚本 +# 执行此脚本前,请确认已正确安装PaddleRS库 + +import paddlers as pdrs +from paddlers import transforms as T + +# 数据集存放目录 +DATA_DIR = './data/rssr/' +# 训练集`file_list`文件路径 +TRAIN_FILE_LIST_PATH = './data/rssr/train.txt' +# 验证集`file_list`文件路径 +EVAL_FILE_LIST_PATH = './data/rssr/val.txt' +# 实验目录,保存输出的模型权重和结果 +EXP_DIR = './output/esrgan/' + +# 下载和解压遥感影像超分辨率数据集 +pdrs.utils.download_and_decompress( + 'https://paddlers.bj.bcebos.com/datasets/rssr.zip', path='./data/') + +# 定义训练和验证时使用的数据变换(数据增强、预处理等) +# 使用Compose组合多种变换方式。Compose中包含的变换将按顺序串行执行 +# API说明:https://github.com/PaddlePaddle/PaddleRS/blob/develop/docs/apis/data.md +train_transforms = T.Compose([ + # 读取影像 + T.DecodeImg(), + # 将输入影像缩放到256x256大小 + T.Resize(target_size=256), + # 以50%的概率实施随机水平翻转 + T.RandomHorizontalFlip(prob=0.5), + # 以50%的概率实施随机垂直翻转 + T.RandomVerticalFlip(prob=0.5), + # 将数据归一化到[0,1] + T.Normalize( + mean=[0.0, 0.0, 0.0], std=[1.0, 1.0, 1.0]), + T.ArrangeRestorer('train') +]) + +eval_transforms = T.Compose([ + T.DecodeImg(), + T.Resize(target_size=256), + # 验证阶段与训练阶段的数据归一化方式必须相同 + T.Normalize( + mean=[0.0, 0.0, 0.0], std=[1.0, 1.0, 1.0]), + T.ArrangeRestorer('eval') +]) + +# 分别构建训练和验证所用的数据集 +train_dataset = pdrs.datasets.ResDataset( + data_dir=DATA_DIR, + file_list=TRAIN_FILE_LIST_PATH, + transforms=train_transforms, + num_workers=0, + shuffle=True) + +eval_dataset = pdrs.datasets.ResDataset( + data_dir=DATA_DIR, + file_list=EVAL_FILE_LIST_PATH, + transforms=eval_transforms, + num_workers=0, + shuffle=False) + +# 使用默认参数构建ESRGAN模型 +# 目前已支持的模型请参考:https://github.com/PaddlePaddle/PaddleRS/blob/develop/docs/intro/model_zoo.md +# 模型输入参数请参考:https://github.com/PaddlePaddle/PaddleRS/blob/develop/paddlers/tasks/restorer.py +model = pdrs.tasks.res.ESRGAN() + +# 执行模型训练 +model.train( + num_epochs=10, + train_dataset=train_dataset, + train_batch_size=8, + eval_dataset=eval_dataset, + save_interval_epochs=1, + # 每多少次迭代记录一次日志 + log_interval_steps=50, + save_dir=EXP_DIR, + # 初始学习率大小 + learning_rate=0.01, + # 是否使用early stopping策略,当精度不再改善时提前终止训练 + early_stop=False, + # 是否启用VisualDL日志功能 + use_vdl=True, + # 指定从某个检查点继续训练 + resume_checkpoint=None) diff --git a/tutorials/train/image_restoration/esrgan_train.py b/tutorials/train/image_restoration/esrgan_train.py deleted file mode 100644 index a972f03..0000000 --- a/tutorials/train/image_restoration/esrgan_train.py +++ /dev/null @@ -1,80 +0,0 @@ -import os -import sys -sys.path.append(os.path.abspath('../PaddleRS')) - -import paddlers as pdrs - -# 定义训练和验证时的transforms -train_transforms = pdrs.datasets.ComposeTrans( - input_keys=['lq', 'gt'], - output_keys=['lq', 'gt'], - pipelines=[{ - 'name': 'SRPairedRandomCrop', - 'gt_patch_size': 128, - 'scale': 4 - }, { - 'name': 'PairedRandomHorizontalFlip' - }, { - 'name': 'PairedRandomVerticalFlip' - }, { - 'name': 'PairedRandomTransposeHW' - }, { - 'name': 'Transpose' - }, { - 'name': 'Normalize', - 'mean': [0.0, 0.0, 0.0], - 'std': [255.0, 255.0, 255.0] - }]) - -test_transforms = pdrs.datasets.ComposeTrans( - input_keys=['lq', 'gt'], - output_keys=['lq', 'gt'], - pipelines=[{ - 'name': 'Transpose' - }, { - 'name': 'Normalize', - 'mean': [0.0, 0.0, 0.0], - 'std': [255.0, 255.0, 255.0] - }]) - -# 定义训练集 -train_gt_floder = r"../work/RSdata_for_SR/trian_HR" # 高分辨率影像所在路径 -train_lq_floder = r"../work/RSdata_for_SR/train_LR/x4" # 低分辨率影像所在路径 -num_workers = 6 -batch_size = 32 -scale = 4 -train_dataset = pdrs.datasets.SRdataset( - mode='train', - gt_floder=train_gt_floder, - lq_floder=train_lq_floder, - transforms=train_transforms(), - scale=scale, - num_workers=num_workers, - batch_size=batch_size) - -# 定义测试集 -test_gt_floder = r"../work/RSdata_for_SR/test_HR" -test_lq_floder = r"../work/RSdata_for_SR/test_LR/x4" -test_dataset = pdrs.datasets.SRdataset( - mode='test', - gt_floder=test_gt_floder, - lq_floder=test_lq_floder, - transforms=test_transforms(), - scale=scale) - -# 初始化模型,可以对网络结构的参数进行调整 -# 若loss_type='gan' 使用感知损失、对抗损失和像素损失 -# 若loss_type = 'pixel' 只使用像素损失 -model = pdrs.tasks.res.ESRGANet(loss_type='pixel') - -model.train( - total_iters=1000000, - train_dataset=train_dataset(), - test_dataset=test_dataset(), - output_dir='output_dir', - validate=5000, - snapshot=5000, - log=100, - lr_rate=0.0001, - periods=[250000, 250000, 250000, 250000], - restart_weights=[1, 1, 1, 1]) diff --git a/tutorials/train/image_restoration/lesrcnn.py b/tutorials/train/image_restoration/lesrcnn.py new file mode 100644 index 0000000..6a97b10 --- /dev/null +++ b/tutorials/train/image_restoration/lesrcnn.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python + +# 图像复原模型LESRCNN训练示例脚本 +# 执行此脚本前,请确认已正确安装PaddleRS库 + +import paddlers as pdrs +from paddlers import transforms as T + +# 数据集存放目录 +DATA_DIR = './data/rssr/' +# 训练集`file_list`文件路径 +TRAIN_FILE_LIST_PATH = './data/rssr/train.txt' +# 验证集`file_list`文件路径 +EVAL_FILE_LIST_PATH = './data/rssr/val.txt' +# 实验目录,保存输出的模型权重和结果 +EXP_DIR = './output/lesrcnn/' + +# 下载和解压遥感影像超分辨率数据集 +pdrs.utils.download_and_decompress( + 'https://paddlers.bj.bcebos.com/datasets/rssr.zip', path='./data/') + +# 定义训练和验证时使用的数据变换(数据增强、预处理等) +# 使用Compose组合多种变换方式。Compose中包含的变换将按顺序串行执行 +# API说明:https://github.com/PaddlePaddle/PaddleRS/blob/develop/docs/apis/data.md +train_transforms = T.Compose([ + # 读取影像 + T.DecodeImg(), + # 将输入影像缩放到256x256大小 + T.Resize(target_size=256), + # 以50%的概率实施随机水平翻转 + T.RandomHorizontalFlip(prob=0.5), + # 以50%的概率实施随机垂直翻转 + T.RandomVerticalFlip(prob=0.5), + # 将数据归一化到[0,1] + T.Normalize( + mean=[0.0, 0.0, 0.0], std=[1.0, 1.0, 1.0]), + T.ArrangeRestorer('train') +]) + +eval_transforms = T.Compose([ + T.DecodeImg(), + T.Resize(target_size=256), + # 验证阶段与训练阶段的数据归一化方式必须相同 + T.Normalize( + mean=[0.0, 0.0, 0.0], std=[1.0, 1.0, 1.0]), + T.ArrangeRestorer('eval') +]) + +# 分别构建训练和验证所用的数据集 +train_dataset = pdrs.datasets.ResDataset( + data_dir=DATA_DIR, + file_list=TRAIN_FILE_LIST_PATH, + transforms=train_transforms, + num_workers=0, + shuffle=True) + +eval_dataset = pdrs.datasets.ResDataset( + data_dir=DATA_DIR, + file_list=EVAL_FILE_LIST_PATH, + transforms=eval_transforms, + num_workers=0, + shuffle=False) + +# 使用默认参数构建LESRCNN模型 +# 目前已支持的模型请参考:https://github.com/PaddlePaddle/PaddleRS/blob/develop/docs/intro/model_zoo.md +# 模型输入参数请参考:https://github.com/PaddlePaddle/PaddleRS/blob/develop/paddlers/tasks/restorer.py +model = pdrs.tasks.res.LESRCNN() + +# 执行模型训练 +model.train( + num_epochs=10, + train_dataset=train_dataset, + train_batch_size=8, + eval_dataset=eval_dataset, + save_interval_epochs=1, + # 每多少次迭代记录一次日志 + log_interval_steps=50, + save_dir=EXP_DIR, + # 初始学习率大小 + learning_rate=0.01, + # 是否使用early stopping策略,当精度不再改善时提前终止训练 + early_stop=False, + # 是否启用VisualDL日志功能 + use_vdl=True, + # 指定从某个检查点继续训练 + resume_checkpoint=None) diff --git a/tutorials/train/image_restoration/lesrcnn_train.py b/tutorials/train/image_restoration/lesrcnn_train.py deleted file mode 100644 index 7f34f84..0000000 --- a/tutorials/train/image_restoration/lesrcnn_train.py +++ /dev/null @@ -1,78 +0,0 @@ -import os -import sys -sys.path.append(os.path.abspath('../PaddleRS')) - -import paddlers as pdrs - -# 定义训练和验证时的transforms -train_transforms = pdrs.datasets.ComposeTrans( - input_keys=['lq', 'gt'], - output_keys=['lq', 'gt'], - pipelines=[{ - 'name': 'SRPairedRandomCrop', - 'gt_patch_size': 192, - 'scale': 4 - }, { - 'name': 'PairedRandomHorizontalFlip' - }, { - 'name': 'PairedRandomVerticalFlip' - }, { - 'name': 'PairedRandomTransposeHW' - }, { - 'name': 'Transpose' - }, { - 'name': 'Normalize', - 'mean': [0.0, 0.0, 0.0], - 'std': [255.0, 255.0, 255.0] - }]) - -test_transforms = pdrs.datasets.ComposeTrans( - input_keys=['lq', 'gt'], - output_keys=['lq', 'gt'], - pipelines=[{ - 'name': 'Transpose' - }, { - 'name': 'Normalize', - 'mean': [0.0, 0.0, 0.0], - 'std': [255.0, 255.0, 255.0] - }]) - -# 定义训练集 -train_gt_floder = r"../work/RSdata_for_SR/trian_HR" # 高分辨率影像所在路径 -train_lq_floder = r"../work/RSdata_for_SR/train_LR/x4" # 低分辨率影像所在路径 -num_workers = 4 -batch_size = 16 -scale = 4 -train_dataset = pdrs.datasets.SRdataset( - mode='train', - gt_floder=train_gt_floder, - lq_floder=train_lq_floder, - transforms=train_transforms(), - scale=scale, - num_workers=num_workers, - batch_size=batch_size) - -# 定义测试集 -test_gt_floder = r"../work/RSdata_for_SR/test_HR" -test_lq_floder = r"../work/RSdata_for_SR/test_LR/x4" -test_dataset = pdrs.datasets.SRdataset( - mode='test', - gt_floder=test_gt_floder, - lq_floder=test_lq_floder, - transforms=test_transforms(), - scale=scale) - -# 初始化模型,可以对网络结构的参数进行调整 -model = pdrs.tasks.res.LESRCNNet(scale=4, multi_scale=False, group=1) - -model.train( - total_iters=1000000, - train_dataset=train_dataset(), - test_dataset=test_dataset(), - output_dir='output_dir', - validate=5000, - snapshot=5000, - log=100, - lr_rate=0.0001, - periods=[250000, 250000, 250000, 250000], - restart_weights=[1, 1, 1, 1]) diff --git a/tutorials/train/image_restoration/rcan.py b/tutorials/train/image_restoration/rcan.py new file mode 100644 index 0000000..e792ac6 --- /dev/null +++ b/tutorials/train/image_restoration/rcan.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python + +# 图像复原模型RCAN训练示例脚本 +# 执行此脚本前,请确认已正确安装PaddleRS库 + +import paddlers as pdrs +from paddlers import transforms as T + +# 数据集存放目录 +DATA_DIR = './data/rssr/' +# 训练集`file_list`文件路径 +TRAIN_FILE_LIST_PATH = './data/rssr/train.txt' +# 验证集`file_list`文件路径 +EVAL_FILE_LIST_PATH = './data/rssr/val.txt' +# 实验目录,保存输出的模型权重和结果 +EXP_DIR = './output/rcan/' + +# 下载和解压遥感影像超分辨率数据集 +pdrs.utils.download_and_decompress( + 'https://paddlers.bj.bcebos.com/datasets/rssr.zip', path='./data/') + +# 定义训练和验证时使用的数据变换(数据增强、预处理等) +# 使用Compose组合多种变换方式。Compose中包含的变换将按顺序串行执行 +# API说明:https://github.com/PaddlePaddle/PaddleRS/blob/develop/docs/apis/data.md +train_transforms = T.Compose([ + # 读取影像 + T.DecodeImg(), + # 将输入影像缩放到256x256大小 + T.Resize(target_size=256), + # 以50%的概率实施随机水平翻转 + T.RandomHorizontalFlip(prob=0.5), + # 以50%的概率实施随机垂直翻转 + T.RandomVerticalFlip(prob=0.5), + # 将数据归一化到[0,1] + T.Normalize( + mean=[0.0, 0.0, 0.0], std=[1.0, 1.0, 1.0]), + T.ArrangeRestorer('train') +]) + +eval_transforms = T.Compose([ + T.DecodeImg(), + T.Resize(target_size=256), + # 验证阶段与训练阶段的数据归一化方式必须相同 + T.Normalize( + mean=[0.0, 0.0, 0.0], std=[1.0, 1.0, 1.0]), + T.ArrangeRestorer('eval') +]) + +# 分别构建训练和验证所用的数据集 +train_dataset = pdrs.datasets.ResDataset( + data_dir=DATA_DIR, + file_list=TRAIN_FILE_LIST_PATH, + transforms=train_transforms, + num_workers=0, + shuffle=True) + +eval_dataset = pdrs.datasets.ResDataset( + data_dir=DATA_DIR, + file_list=EVAL_FILE_LIST_PATH, + transforms=eval_transforms, + num_workers=0, + shuffle=False) + +# 使用默认参数构建RCAN模型 +# 目前已支持的模型请参考:https://github.com/PaddlePaddle/PaddleRS/blob/develop/docs/intro/model_zoo.md +# 模型输入参数请参考:https://github.com/PaddlePaddle/PaddleRS/blob/develop/paddlers/tasks/restorer.py +model = pdrs.tasks.res.RCAN() + +# 执行模型训练 +model.train( + num_epochs=10, + train_dataset=train_dataset, + train_batch_size=8, + eval_dataset=eval_dataset, + save_interval_epochs=1, + # 每多少次迭代记录一次日志 + log_interval_steps=50, + save_dir=EXP_DIR, + # 初始学习率大小 + learning_rate=0.01, + # 是否使用early stopping策略,当精度不再改善时提前终止训练 + early_stop=False, + # 是否启用VisualDL日志功能 + use_vdl=True, + # 指定从某个检查点继续训练 + resume_checkpoint=None) From ceddb51a8044a53f250b347e9c81f2a3f7acbc80 Mon Sep 17 00:00:00 2001 From: Bobholamovic Date: Sat, 20 Aug 2022 21:25:08 +0800 Subject: [PATCH 15/52] Init res docs --- docs/apis/data.md | 4 ++++ docs/apis/infer.md | 10 ++++++--- docs/apis/train.md | 12 +++++++++- docs/dev/dev_guide.md | 6 ++--- tests/deploy/test_predictor.py | 10 ++++++++- tests/rs_models/test_res_models.py | 36 ++++++++++++++++++++++++++++++ 6 files changed, 70 insertions(+), 8 deletions(-) create mode 100644 tests/rs_models/test_res_models.py diff --git a/docs/apis/data.md b/docs/apis/data.md index 8f8bc4d..ab8a523 100644 --- a/docs/apis/data.md +++ b/docs/apis/data.md @@ -84,6 +84,9 @@ - file list中的每一行应该包含2个以空格分隔的项,依次表示输入影像相对`data_dir`的路径以及[Pascal VOC格式](http://host.robots.ox.ac.uk/pascal/VOC/)标注文件相对`data_dir`的路径。 +### 图像复原数据集`ResDataset` + + ### 图像分割数据集`SegDataset` `SegDataset`定义在:https://github.com/PaddlePaddle/PaddleRS/blob/develop/paddlers/datasets/seg_dataset.py @@ -143,6 +146,7 @@ |`'aux_masks'`|图像分割/变化检测任务中的辅助标签路径或数据。| |`'gt_bbox'`|目标检测任务中的检测框标注数据。| |`'gt_poly'`|目标检测任务中的多边形标注数据。| +|`'target'`|图像复原中的目标影像路径或数据。| ### 组合数据变换算子 diff --git a/docs/apis/infer.md b/docs/apis/infer.md index c7f3d1c..2a5b62d 100644 --- a/docs/apis/infer.md +++ b/docs/apis/infer.md @@ -26,7 +26,7 @@ def predict(self, img_file, transforms=None): 若`img_file`是一个元组,则返回对象为包含下列键值对的字典: ``` -{"label map": 输出类别标签(以[h, w]格式排布),"score_map": 模型输出的各类别概率(以[h, w, c]格式排布)} +{"label_map": 输出类别标签(以[h, w]格式排布),"score_map": 模型输出的各类别概率(以[h, w, c]格式排布)} ``` 若`img_file`是一个列表,则返回对象为与`img_file`等长的列表,其中的每一项为一个字典(键值对如上所示),顺序对应`img_file`中的每个元素。 @@ -51,7 +51,7 @@ def predict(self, img_file, transforms=None): 若`img_file`是一个字符串或NumPy数组,则返回对象为包含下列键值对的字典: ``` -{"label map": 输出类别标签, +{"label_map": 输出类别标签, "scores_map": 输出类别概率, "label_names_map": 输出类别名称} ``` @@ -87,6 +87,10 @@ def predict(self, img_file, transforms=None): 若`img_file`是一个列表,则返回对象为与`img_file`等长的列表,其中的每一项为一个由字典(键值对如上所示)构成的列表,顺序对应`img_file`中的每个元素。 +#### `BaseRestorer.predict()` + + + #### `BaseSegmenter.predict()` 接口形式: @@ -107,7 +111,7 @@ def predict(self, img_file, transforms=None): 若`img_file`是一个字符串或NumPy数组,则返回对象为包含下列键值对的字典: ``` -{"label map": 输出类别标签(以[h, w]格式排布),"score_map": 模型输出的各类别概率(以[h, w, c]格式排布)} +{"label_map": 输出类别标签(以[h, w]格式排布),"score_map": 模型输出的各类别概率(以[h, w, c]格式排布)} ``` 若`img_file`是一个列表,则返回对象为与`img_file`等长的列表,其中的每一项为一个字典(键值对如上所示),顺序对应`img_file`中的每个元素。 diff --git a/docs/apis/train.md b/docs/apis/train.md index 97f55b1..b5572db 100644 --- a/docs/apis/train.md +++ b/docs/apis/train.md @@ -18,11 +18,15 @@ - `use_mixed_loss`参将在未来被弃用,因此不建议使用。 - 不同的子类支持与模型相关的输入参数,详情请参考[模型定义](https://github.com/PaddlePaddle/PaddleRS/blob/develop/paddlers/rs_models/clas)和[训练器定义](https://github.com/PaddlePaddle/PaddleRS/blob/develop/paddlers/tasks/classifier.py)。 -### 初始化`Baseetector`子类对象 +### 初始化`BaseDetector`子类对象 - 一般支持设置`num_classes`和`backbone`参数,分别表示模型输出类别数以及所用的骨干网络类型。相比其它任务,目标检测任务的训练器支持设置的初始化参数较多,囊括网络结构、损失函数、后处理策略等方面。 - 不同的子类支持与模型相关的输入参数,详情请参考[模型定义](https://github.com/PaddlePaddle/PaddleRS/blob/develop/paddlers/rs_models/det)和[训练器定义](https://github.com/PaddlePaddle/PaddleRS/blob/develop/paddlers/tasks/object_detector.py)。 +### 初始化`BaseRestorer`子类对象 + + + ### 初始化`BaseSegmenter`子类对象 - 一般支持设置`input_channel`、`num_classes`以及`use_mixed_loss`参数,分别表示输入通道数、输出类别数以及是否使用预置的混合损失。部分模型如`FarSeg`暂不支持对`input_channel`参数的设置。 @@ -170,6 +174,9 @@ def train(self, |`use_vdl`|`bool`|是否启用VisualDL日志。|`True`| |`resume_checkpoint`|`str` \| `None`|检查点路径。PaddleRS支持从检查点(包含先前训练过程中存储的模型权重和优化器权重)继续训练,但需注意`resume_checkpoint`与`pretrain_weights`不得同时设置为`None`以外的值。|`None`| +### `BaseRestorer.train()` + + ### `BaseSegmenter.train()` 接口形式: @@ -311,6 +318,9 @@ def evaluate(self, "mask": 预测得到的掩模图信息} ``` +### `BaseRestorer.evaluate()` + + ### `BaseSegmenter.evaluate()` 接口形式: diff --git a/docs/dev/dev_guide.md b/docs/dev/dev_guide.md index 55f3980..9f678af 100644 --- a/docs/dev/dev_guide.md +++ b/docs/dev/dev_guide.md @@ -22,7 +22,7 @@ 在子目录中新建文件,以`{模型名称小写}.py`命名。在文件中编写完整的模型定义。 -新模型必须是`paddle.nn.Layer`的子类。对于图像分割、目标检测和场景分类任务,分别需要遵循[PaddleSeg](https://github.com/PaddlePaddle/PaddleSeg)、[PaddleDetection](https://github.com/PaddlePaddle/PaddleDetection)和[PaddleClas](https://github.com/PaddlePaddle/PaddleClas)套件中制定的相关规范。**对于变化检测、场景分类和图像分割任务,模型构造时必须传入`num_classes`参数以指定输出的类别数目**。对于变化检测任务,模型定义需遵循的规范与分割模型类似,但有以下几点不同: +新模型必须是`paddle.nn.Layer`的子类。对于图像分割、目标检测、场景分类和图像复原任务,分别需要遵循[PaddleSeg](https://github.com/PaddlePaddle/PaddleSeg)、[PaddleDetection](https://github.com/PaddlePaddle/PaddleDetection)、[PaddleClas](https://github.com/PaddlePaddle/PaddleClas)和[PaddleGAN](https://github.com/PaddlePaddle/PaddleGAN)套件中制定的相关规范。**对于变化检测、场景分类和图像分割任务,模型构造时必须传入`num_classes`参数以指定输出的类别数目。对于图像复原任务,模型构造时必须传入`rs_factor`参数以指定超分辨率缩放倍数(对于非超分辨率模型,将此参数设置为`None`)。**对于变化检测任务,模型定义需遵循的规范与分割模型类似,但有以下几点不同: - `forward()`方法接受3个输入参数,分别是`self`、`t1`和`t2`,其中`t1`和`t2`分别表示前、后两个时相的输入影像。 - 对于多任务变化检测模型(例如模型同时输出变化检测结果与两个时相的建筑物提取结果),需要指定类的`USE_MULTITASK_DECODER`属性为`True`,同时在`OUT_TYPES`属性中设置模型前向输出的列表中每一个元素对应的标签类型。可参考`ChangeStar`模型的定义。 @@ -64,7 +64,7 @@ Args: 2. 在`paddlers/tasks`目录中找到任务对应的训练器定义文件(例如变化检测任务对应`paddlers/tasks/change_detector.py`)。 3. 在文件尾部追加新的训练器定义。训练器需要继承自相关的基类(例如`BaseChangeDetector`),重写`__init__()`方法,并根据需要重写其他方法。对训练器`__init__()`方法编写的要求如下: - - 对于变化检测、场景分类、目标检测、图像分割任务,`__init__()`方法的第1个输入参数是`num_classes`,表示模型输出类别数;对于变化检测、场景分类、图像分割任务,第2个输入参数是`use_mixed_loss`,表示用户是否使用默认定义的混合损失。 + - 对于变化检测、场景分类、目标检测、图像分割任务,`__init__()`方法的第1个输入参数是`num_classes`,表示模型输出类别数。对于变化检测、场景分类、图像分割任务,第2个输入参数是`use_mixed_loss`,表示用户是否使用默认定义的混合损失;第3个输入参数是`losses`,表示训练时使用的损失函数。对于图像复原任务,第1个参数是`losses`,含义同上;第2个参数是`rs_factor`,表示超分辨率缩放倍数。 - `__init__()`的所有输入参数都必须有默认值,且在**取默认值的情况下,模型接收3通道RGB输入**。 - 在`__init__()`中需要更新`params`字典,该字典中的键值对将被用作模型构造时的输入参数。 @@ -78,7 +78,7 @@ Args: ### 2.2 新增数据预处理/数据增强算子 -在`paddlers/transforms/operators.py`中定义新算子,所有算子均继承自`paddlers.transforms.Transform`类。算子的`apply()`方法接收一个字典`sample`作为输入,取出其中存储的相关对象,处理后对字典进行in-place修改,最后返回修改后的字典。在定义算子时,只有极少数的情况需要重写`apply()`方法。大多数情况下,只需要重写`apply_im()`、`apply_mask()`、`apply_bbox()`和`apply_segm()`方法就分别可以实现对输入图像、分割标签、目标框以及目标多边形的处理。 +在`paddlers/transforms/operators.py`中定义新算子,所有算子均继承自`paddlers.transforms.Transform`类。算子的`apply()`方法接收一个字典`sample`作为输入,取出其中存储的相关对象,处理后对字典进行in-place修改,最后返回修改后的字典。在定义算子时,只有极少数的情况需要重写`apply()`方法。大多数情况下,只需要重写`apply_im()`、`apply_mask()`、`apply_bbox()`和`apply_segm()`方法就分别可以实现对图像、分割标签、目标框以及目标多边形的处理。 如果处理逻辑较为复杂,建议先封装为函数,添加到`paddlers/transforms/functions.py`中,然后在算子的`apply*()`方法中调用函数。 diff --git a/tests/deploy/test_predictor.py b/tests/deploy/test_predictor.py index 6283951..c18c51a 100644 --- a/tests/deploy/test_predictor.py +++ b/tests/deploy/test_predictor.py @@ -24,7 +24,7 @@ from testing_utils import CommonTest, run_script __all__ = [ 'TestCDPredictor', 'TestClasPredictor', 'TestDetPredictor', - 'TestSegPredictor' + 'TestResPredictor', 'TestSegPredictor' ] @@ -302,6 +302,14 @@ class TestDetPredictor(TestPredictor): self.assertEqual(len(out_multi_array_t), num_inputs) +@TestPredictor.add_tests +class TestResPredictor(TestPredictor): + MODULE = pdrs.tasks.restorer + + def check_predictor(self, predictor, trainer): + pass + + @TestPredictor.add_tests class TestSegPredictor(TestPredictor): MODULE = pdrs.tasks.segmenter diff --git a/tests/rs_models/test_res_models.py b/tests/rs_models/test_res_models.py new file mode 100644 index 0000000..205696d --- /dev/null +++ b/tests/rs_models/test_res_models.py @@ -0,0 +1,36 @@ +# Copyright (c) 2022 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 paddlers +from rs_models.test_model import TestModel + +__all__ = ['TestRCANModel'] + + +class TestResModel(TestModel): + def check_output(self, output, target): + pass + + def set_inputs(self): + pass + + def set_targets(self): + pass + + +class TestRCANModel(TestSegModel): + MODEL_CLASS = paddlers.rs_models.res.RCAN + + def set_specs(self): + pass From 5dcc6cd0782b0e61cbac8b8e25bbe72b98dad62e Mon Sep 17 00:00:00 2001 From: Bobholamovic Date: Sat, 20 Aug 2022 23:49:46 +0800 Subject: [PATCH 16/52] Fix bugs --- paddlers/rs_models/res/generators/rcan.py | 3 +- paddlers/tasks/base.py | 2 +- paddlers/tasks/restorer.py | 35 ++++++++++++++++------- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/paddlers/rs_models/res/generators/rcan.py b/paddlers/rs_models/res/generators/rcan.py index db66b92..1c40882 100644 --- a/paddlers/rs_models/res/generators/rcan.py +++ b/paddlers/rs_models/res/generators/rcan.py @@ -146,7 +146,6 @@ class RCAN(nn.Layer): n_feats = n_feats kernel_size = kernel_size reduction = reduction - scale = scale act = nn.ReLU() rgb_mean = (0.4488, 0.4371, 0.4040) @@ -167,7 +166,7 @@ class RCAN(nn.Layer): # Define tail module modules_tail = [ Upsampler( - conv, scale, n_feats, act=False), + conv, self.scale, n_feats, act=False), conv(n_feats, n_colors, kernel_size) ] diff --git a/paddlers/tasks/base.py b/paddlers/tasks/base.py index 809c385..b1ca097 100644 --- a/paddlers/tasks/base.py +++ b/paddlers/tasks/base.py @@ -307,7 +307,7 @@ class BaseModel(metaclass=ModelMeta): use_vdl=True): self._check_transforms(train_dataset.transforms, 'train') - if "RCNN" in self.__class__.__name__ and train_dataset.pos_num < len( + if self.model_type == 'detector' and 'RCNN' in self.__class__.__name__ and train_dataset.pos_num < len( train_dataset.file_list): nranks = 1 else: diff --git a/paddlers/tasks/restorer.py b/paddlers/tasks/restorer.py index a7468ae..cef1627 100644 --- a/paddlers/tasks/restorer.py +++ b/paddlers/tasks/restorer.py @@ -54,7 +54,7 @@ class BaseRestorer(BaseModel): def build_net(self, **params): # Currently, only use models from cmres. - if not hasattr(cmres, model_name): + if not hasattr(cmres, self.model_name): raise ValueError("ERROR: There is no model named {}.".format( model_name)) net = dict(**cmres.__dict__)[self.model_name](**params) @@ -618,7 +618,6 @@ class RCAN(BaseRestorer): reduction=16, **params): params.update({ - 'factor': sr_factor, 'n_resgroups': n_resgroups, 'n_resblocks': n_resblocks, 'n_feats': n_feats, @@ -661,8 +660,17 @@ class DRN(BaseRestorer): class LESRCNN(BaseRestorer): - def __init__(self, losses=None, sr_factor=4, multi_scale=False, group=1): - params.update({'scale': sr_factor, 'multi_scale': False, 'group': 1}) + def __init__(self, + losses=None, + sr_factor=4, + multi_scale=False, + group=1, + **params): + params.update({ + 'scale': sr_factor, + 'multi_scale': multi_scale, + 'group': group + }) super(LESRCNN, self).__init__( model_name='LESRCNN', losses=losses, sr_factor=sr_factor, **params) @@ -681,9 +689,11 @@ class ESRGAN(BaseRestorer): in_channels=3, out_channels=3, nf=64, - nb=23): + nb=23, + **params): + if sr_factor != 4: + raise ValueError("`sr_factor` must be 4.") params.update({ - 'scale': sr_factor, 'in_nc': in_channels, 'out_nc': out_channels, 'nf': nf, @@ -696,7 +706,7 @@ class ESRGAN(BaseRestorer): def build_net(self, **params): generator = ppgan.models.generators.RRDBNet(**params) if self.use_gan: - discriminator = ppgan.models.discriminators.VGGDiscrinimator128( + discriminator = ppgan.models.discriminators.VGGDiscriminator128( in_channels=params['out_nc'], num_feat=64) net = GANAdapter( generators=[generator], discriminators=[discriminator]) @@ -719,9 +729,9 @@ class ESRGAN(BaseRestorer): def default_optimizer(self, parameters, *args, **kwargs): if self.use_gan: optim_g = super(ESRGAN, self).default_optimizer( - parameters['optims_g'][0], *args, **kwargs) + parameters['params_g'][0], *args, **kwargs) optim_d = super(ESRGAN, self).default_optimizer( - parameters['optims_d'][0], *args, **kwargs) + parameters['params_d'][0], *args, **kwargs) return OptimizerAdapter(optim_g, optim_d) else: return super(ESRGAN, self).default_optimizer(params, *args, @@ -777,14 +787,17 @@ class ESRGAN(BaseRestorer): if self.use_gan: optim_g, optim_d = self.optimizer - outputs = self.run_gan(net, data, gan_mode='forward_g') + outputs = self.run_gan( + net, data, mode='train', gan_mode='forward_g') optim_g.clear_grad() (outputs['loss_g_pps'] + outputs['loss_g_gan']).backward() optim_g.step() outputs.update( self.run_gan( - net, (outputs['g_pred'], data[1]), gan_mode='forward_d')) + net, (outputs['g_pred'], data[1]), + mode='train', + gan_mode='forward_d')) optim_d.clear_grad() outputs['loss_d'].backward() optim_d.step() From 752d8e41cf184c0b7a7731eae3c1957979d06b56 Mon Sep 17 00:00:00 2001 From: Bobholamovic Date: Sun, 21 Aug 2022 15:49:16 +0800 Subject: [PATCH 17/52] Fix bugs --- paddlers/deploy/predictor.py | 21 +- paddlers/models/ppdet/metrics/json_results.py | 0 .../ppdet/modeling/architectures/centernet.py | 0 .../ppdet/modeling/architectures/fairmot.py | 0 .../ppdet/modeling/backbones/darknet.py | 0 .../models/ppdet/modeling/backbones/dla.py | 0 .../models/ppdet/modeling/backbones/resnet.py | 0 .../models/ppdet/modeling/backbones/vgg.py | 0 .../ppdet/modeling/heads/centernet_head.py | 0 .../ppdet/modeling/losses/fairmot_loss.py | 0 .../ppdet/modeling/necks/centernet_fpn.py | 0 .../modeling/reid/fairmot_embedding_head.py | 0 .../models/ppseg/models/losses/focal_loss.py | 0 .../models/ppseg/models/losses/kl_loss.py | 0 .../rs_models/res/generators/param_init.py | 27 ++ paddlers/rs_models/res/generators/rcan.py | 24 +- paddlers/tasks/base.py | 23 +- paddlers/tasks/change_detector.py | 8 +- paddlers/tasks/restorer.py | 243 ++++++++++++------ paddlers/tasks/segmenter.py | 12 +- paddlers/tasks/utils/infer_nets.py | 39 ++- paddlers/tasks/utils/res_adapters.py | 6 +- paddlers/transforms/operators.py | 5 +- paddlers/utils/__init__.py | 2 +- paddlers/utils/utils.py | 33 ++- tutorials/train/README.md | 1 - tutorials/train/image_restoration/drn.py | 15 +- tutorials/train/image_restoration/esrgan.py | 15 +- tutorials/train/image_restoration/lesrcnn.py | 15 +- 29 files changed, 344 insertions(+), 145 deletions(-) mode change 100755 => 100644 paddlers/models/ppdet/metrics/json_results.py mode change 100755 => 100644 paddlers/models/ppdet/modeling/architectures/centernet.py mode change 100755 => 100644 paddlers/models/ppdet/modeling/architectures/fairmot.py mode change 100755 => 100644 paddlers/models/ppdet/modeling/backbones/darknet.py mode change 100755 => 100644 paddlers/models/ppdet/modeling/backbones/dla.py mode change 100755 => 100644 paddlers/models/ppdet/modeling/backbones/resnet.py mode change 100755 => 100644 paddlers/models/ppdet/modeling/backbones/vgg.py mode change 100755 => 100644 paddlers/models/ppdet/modeling/heads/centernet_head.py mode change 100755 => 100644 paddlers/models/ppdet/modeling/losses/fairmot_loss.py mode change 100755 => 100644 paddlers/models/ppdet/modeling/necks/centernet_fpn.py mode change 100755 => 100644 paddlers/models/ppdet/modeling/reid/fairmot_embedding_head.py mode change 100755 => 100644 paddlers/models/ppseg/models/losses/focal_loss.py mode change 100755 => 100644 paddlers/models/ppseg/models/losses/kl_loss.py create mode 100644 paddlers/rs_models/res/generators/param_init.py diff --git a/paddlers/deploy/predictor.py b/paddlers/deploy/predictor.py index 47924ef..a5b3a57 100644 --- a/paddlers/deploy/predictor.py +++ b/paddlers/deploy/predictor.py @@ -163,17 +163,27 @@ class Predictor(object): 'image2': preprocessed_samples[1], 'ori_shape': preprocessed_samples[2] } + elif self._model.model_type == 'restorer': + preprocessed_samples = { + 'image': preprocessed_samples[0], + 'tar_shape': preprocessed_samples[1] + } else: logging.error( "Invalid model type {}".format(self._model.model_type), exit=True) return preprocessed_samples - def postprocess(self, net_outputs, topk=1, ori_shape=None, transforms=None): + def postprocess(self, + net_outputs, + topk=1, + ori_shape=None, + tar_shape=None, + transforms=None): if self._model.model_type == 'classifier': true_topk = min(self._model.num_classes, topk) if self._model._postprocess is None: - self._model.build_postprocess_from_labels(topk) + self._model.build_postprocess_from_labels(true_topk) # XXX: Convert ndarray to tensor as self._model._postprocess requires assert len(net_outputs) == 1 net_outputs = paddle.to_tensor(net_outputs[0]) @@ -201,6 +211,12 @@ class Predictor(object): for k, v in zip(['bbox', 'bbox_num', 'mask'], net_outputs) } preds = self._model._postprocess(net_outputs) + elif self._model.model_type == 'restorer': + res_maps = self._model._postprocess( + net_outputs[0], + batch_tar_shape=tar_shape, + transforms=transforms.transforms) + preds = [{'res_map': res_map} for res_map in res_maps] else: logging.error( "Invalid model type {}.".format(self._model.model_type), @@ -244,6 +260,7 @@ class Predictor(object): net_outputs, topk, ori_shape=preprocessed_input.get('ori_shape', None), + tar_shape=preprocessed_input.get('tar_shape', None), transforms=transforms) self.timer.postprocess_time_s.end(iter_num=len(images)) diff --git a/paddlers/models/ppdet/metrics/json_results.py b/paddlers/models/ppdet/metrics/json_results.py old mode 100755 new mode 100644 diff --git a/paddlers/models/ppdet/modeling/architectures/centernet.py b/paddlers/models/ppdet/modeling/architectures/centernet.py old mode 100755 new mode 100644 diff --git a/paddlers/models/ppdet/modeling/architectures/fairmot.py b/paddlers/models/ppdet/modeling/architectures/fairmot.py old mode 100755 new mode 100644 diff --git a/paddlers/models/ppdet/modeling/backbones/darknet.py b/paddlers/models/ppdet/modeling/backbones/darknet.py old mode 100755 new mode 100644 diff --git a/paddlers/models/ppdet/modeling/backbones/dla.py b/paddlers/models/ppdet/modeling/backbones/dla.py old mode 100755 new mode 100644 diff --git a/paddlers/models/ppdet/modeling/backbones/resnet.py b/paddlers/models/ppdet/modeling/backbones/resnet.py old mode 100755 new mode 100644 diff --git a/paddlers/models/ppdet/modeling/backbones/vgg.py b/paddlers/models/ppdet/modeling/backbones/vgg.py old mode 100755 new mode 100644 diff --git a/paddlers/models/ppdet/modeling/heads/centernet_head.py b/paddlers/models/ppdet/modeling/heads/centernet_head.py old mode 100755 new mode 100644 diff --git a/paddlers/models/ppdet/modeling/losses/fairmot_loss.py b/paddlers/models/ppdet/modeling/losses/fairmot_loss.py old mode 100755 new mode 100644 diff --git a/paddlers/models/ppdet/modeling/necks/centernet_fpn.py b/paddlers/models/ppdet/modeling/necks/centernet_fpn.py old mode 100755 new mode 100644 diff --git a/paddlers/models/ppdet/modeling/reid/fairmot_embedding_head.py b/paddlers/models/ppdet/modeling/reid/fairmot_embedding_head.py old mode 100755 new mode 100644 diff --git a/paddlers/models/ppseg/models/losses/focal_loss.py b/paddlers/models/ppseg/models/losses/focal_loss.py old mode 100755 new mode 100644 diff --git a/paddlers/models/ppseg/models/losses/kl_loss.py b/paddlers/models/ppseg/models/losses/kl_loss.py old mode 100755 new mode 100644 diff --git a/paddlers/rs_models/res/generators/param_init.py b/paddlers/rs_models/res/generators/param_init.py new file mode 100644 index 0000000..003c3c2 --- /dev/null +++ b/paddlers/rs_models/res/generators/param_init.py @@ -0,0 +1,27 @@ +# Copyright (c) 2022 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 paddle +import paddle.nn as nn + +from paddlers.models.ppgan.modules.init import reset_parameters + + +def init_sr_weight(net): + def reset_func(m): + if hasattr(m, 'weight') and ( + not isinstance(m, (nn.BatchNorm, nn.BatchNorm2D))): + reset_parameters(m) + + net.apply(reset_func) diff --git a/paddlers/rs_models/res/generators/rcan.py b/paddlers/rs_models/res/generators/rcan.py index 1c40882..6b32621 100644 --- a/paddlers/rs_models/res/generators/rcan.py +++ b/paddlers/rs_models/res/generators/rcan.py @@ -1,9 +1,26 @@ +# Copyright (c) 2022 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. + # Based on https://github.com/kongdebug/RCAN-Paddle + import math import paddle import paddle.nn as nn +from .param_init import init_sr_weight + def default_conv(in_channels, out_channels, kernel_size, bias=True): weight_attr = paddle.ParamAttr( @@ -61,8 +78,10 @@ class RCAB(nn.Layer): bias=True, bn=False, act=nn.ReLU(), - res_scale=1): + res_scale=1, + use_init_weight=False): super(RCAB, self).__init__() + modules_body = [] for i in range(2): modules_body.append(conv(n_feat, n_feat, kernel_size, bias=bias)) @@ -72,6 +91,9 @@ class RCAB(nn.Layer): self.body = nn.Sequential(*modules_body) self.res_scale = res_scale + if use_init_weight: + init_sr_weight(self) + def forward(self, x): res = self.body(x) res += x diff --git a/paddlers/tasks/base.py b/paddlers/tasks/base.py index b1ca097..2b7c295 100644 --- a/paddlers/tasks/base.py +++ b/paddlers/tasks/base.py @@ -30,10 +30,10 @@ from paddleslim import L1NormFilterPruner, FPGMFilterPruner import paddlers import paddlers.utils.logging as logging -from paddlers.utils import (seconds_to_hms, get_single_card_bs, dict2str, - get_pretrain_weights, load_pretrain_weights, - load_checkpoint, SmoothedValue, TrainingStats, - _get_shared_memory_size_in_M, EarlyStop) +from paddlers.utils import ( + seconds_to_hms, get_single_card_bs, dict2str, get_pretrain_weights, + load_pretrain_weights, load_checkpoint, SmoothedValue, TrainingStats, + _get_shared_memory_size_in_M, EarlyStop, to_data_parallel, scheduler_step) from .slim.prune import _pruner_eval_fn, _pruner_template_input, sensitive_prune @@ -320,10 +320,10 @@ class BaseModel(metaclass=ModelMeta): if not paddle.distributed.parallel.parallel_helper._is_parallel_ctx_initialized( ): paddle.distributed.init_parallel_env() - ddp_net = paddle.DataParallel( + ddp_net = to_data_parallel( self.net, find_unused_parameters=find_unused_parameters) else: - ddp_net = paddle.DataParallel( + ddp_net = to_data_parallel( self.net, find_unused_parameters=find_unused_parameters) if use_vdl: @@ -368,6 +368,8 @@ class BaseModel(metaclass=ModelMeta): else: outputs = self.train_step(step, data, self.net) + scheduler_step(self.optimizer) + train_avg_metrics.update(outputs) lr = self.optimizer.get_lr() outputs['lr'] = lr @@ -662,15 +664,6 @@ class BaseModel(metaclass=ModelMeta): self.optimizer.step() self.optimizer.clear_grad() - if isinstance(self.optimizer._learning_rate, - paddle.optimizer.lr.LRScheduler): - # If ReduceOnPlateau is used as the scheduler, use the loss value as the metric. - if isinstance(self.optimizer._learning_rate, - paddle.optimizer.lr.ReduceOnPlateau): - self.optimizer._learning_rate.step(loss.item()) - else: - self.optimizer._learning_rate.step() - return outputs def _check_transforms(self, transforms, mode): diff --git a/paddlers/tasks/change_detector.py b/paddlers/tasks/change_detector.py index 9a0b16e..d4ef0af 100644 --- a/paddlers/tasks/change_detector.py +++ b/paddlers/tasks/change_detector.py @@ -796,11 +796,11 @@ class BaseChangeDetector(BaseModel): elif item[0] == 'padding': x, y = item[2] if isinstance(label_map, np.ndarray): - label_map = label_map[..., y:y + h, x:x + w] - score_map = score_map[..., y:y + h, x:x + w] + label_map = label_map[y:y + h, x:x + w] + score_map = score_map[y:y + h, x:x + w] else: - label_map = label_map[:, :, y:y + h, x:x + w] - score_map = score_map[:, :, y:y + h, x:x + w] + label_map = label_map[:, y:y + h, x:x + w, :] + score_map = score_map[:, y:y + h, x:x + w, :] else: pass label_map = label_map.squeeze() diff --git a/paddlers/tasks/restorer.py b/paddlers/tasks/restorer.py index cef1627..c3aa59a 100644 --- a/paddlers/tasks/restorer.py +++ b/paddlers/tasks/restorer.py @@ -25,6 +25,7 @@ from paddle.static import InputSpec import paddlers import paddlers.models.ppgan as ppgan import paddlers.rs_models.res as cmres +import paddlers.models.ppgan.metrics as metrics import paddlers.utils.logging as logging from paddlers.models import res_losses from paddlers.transforms import Resize, decode_image @@ -32,12 +33,14 @@ from paddlers.transforms.functions import calc_hr_shape from paddlers.utils import get_single_card_bs from .base import BaseModel from .utils.res_adapters import GANAdapter, OptimizerAdapter +from .utils.infer_nets import InferResNet __all__ = [] class BaseRestorer(BaseModel): - MIN_MAX = (0., 255.) + MIN_MAX = (0., 1.) + TEST_OUT_KEY = None def __init__(self, model_name, losses=None, sr_factor=None, **params): self.init_params = locals() @@ -63,9 +66,10 @@ class BaseRestorer(BaseModel): def _build_inference_net(self): # For GAN models, only the generator will be used for inference. if isinstance(self.net, GANAdapter): - infer_net = self.net.generator + infer_net = InferResNet( + self.net.generator, out_key=self.TEST_OUT_KEY) else: - infer_net = self.net + infer_net = InferResNet(self.net, out_key=self.TEST_OUT_KEY) infer_net.eval() return infer_net @@ -108,15 +112,18 @@ class BaseRestorer(BaseModel): outputs = OrderedDict() if mode == 'test': - if isinstance(net, GANAdapter): - net_out = net.generator(inputs[0]) - else: - net_out = net(inputs[0]) tar_shape = inputs[1] if self.status == 'Infer': + net_out = net(inputs[0]) res_map_list = self._postprocess( net_out, tar_shape, transforms=inputs[2]) else: + if isinstance(net, GANAdapter): + net_out = net.generator(inputs[0]) + else: + net_out = net(inputs[0]) + if self.TEST_OUT_KEY is not None: + net_out = net_out[self.TEST_OUT_KEY] pred = self._postprocess( net_out, tar_shape, transforms=inputs[2]) res_map_list = [] @@ -130,13 +137,15 @@ class BaseRestorer(BaseModel): net_out = net.generator(inputs[0]) else: net_out = net(inputs[0]) + if self.TEST_OUT_KEY is not None: + net_out = net_out[self.TEST_OUT_KEY] tar = inputs[1] tar_shape = [tar.shape[-2:]] pred = self._postprocess( net_out, tar_shape, transforms=inputs[2])[0] # NCHW pred = self._tensor_to_images(pred) outputs['pred'] = pred - tar = self.tensor_to_images(tar) + tar = self._tensor_to_images(tar) outputs['tar'] = tar if mode == 'train': @@ -386,10 +395,11 @@ class BaseRestorer(BaseModel): self.eval_data_loader = self.build_data_loader( eval_dataset, batch_size=batch_size, mode='eval') # XXX: Hard-code crop_border and test_y_channel - psnr = ppgan.metrics.PSNR(crop_border=4, test_y_channel=True) - ssim = ppgan.metrics.SSIM(crop_border=4, test_y_channel=True) + psnr = metrics.PSNR(crop_border=4, test_y_channel=True) + ssim = metrics.SSIM(crop_border=4, test_y_channel=True) with paddle.no_grad(): for step, data in enumerate(self.eval_data_loader): + data.append(eval_dataset.transforms.transforms) outputs = self.run(self.net, data, 'eval') psnr.update(outputs['pred'], outputs['tar']) ssim.update(outputs['pred'], outputs['tar']) @@ -520,10 +530,9 @@ class BaseRestorer(BaseModel): def _postprocess(self, batch_pred, batch_tar_shape, transforms): batch_restore_list = BaseRestorer.get_transforms_shape_info( batch_tar_shape, transforms) - if isinstance(batch_pred, (tuple, list)) and self.status == 'Infer': + if self.status == 'Infer': return self._infer_postprocess( - batch_res_map=batch_pred[0], - batch_restore_list=batch_restore_list) + batch_res_map=batch_pred, batch_restore_list=batch_restore_list) results = [] if batch_pred.dtype == paddle.float32: mode = 'bilinear' @@ -546,7 +555,7 @@ class BaseRestorer(BaseModel): def _infer_postprocess(self, batch_res_map, batch_restore_list): res_maps = [] - for score_map, restore_list in zip(batch_res_map, batch_restore_list): + for res_map, restore_list in zip(batch_res_map, batch_restore_list): if not isinstance(res_map, np.ndarray): res_map = paddle.unsqueeze(res_map, axis=0) for item in restore_list[::-1]: @@ -557,15 +566,15 @@ class BaseRestorer(BaseModel): res_map, (w, h), interpolation=cv2.INTER_LINEAR) else: res_map = F.interpolate( - score_map, (h, w), + res_map, (h, w), mode='bilinear', data_format='NHWC') elif item[0] == 'padding': x, y = item[2] if isinstance(res_map, np.ndarray): - res_map = res_map[..., y:y + h, x:x + w] + res_map = res_map[y:y + h, x:x + w] else: - res_map = res_map[:, :, y:y + h, x:x + w] + res_map = res_map[:, y:y + h, x:x + w, :] else: pass res_map = res_map.squeeze() @@ -585,18 +594,25 @@ class BaseRestorer(BaseModel): def set_losses(self, losses): self.losses = losses - def _tensor_to_images(self, tensor, squeeze=True, quantize=True): - tensor = paddle.transpose(tensor, perm=[0, 2, 3, 1]) # NHWC + def _tensor_to_images(self, + tensor, + transpose=True, + squeeze=True, + quantize=True): + if transpose: + tensor = paddle.transpose(tensor, perm=[0, 2, 3, 1]) # NHWC if squeeze: tensor = tensor.squeeze() images = tensor.numpy().astype('float32') - images = np.clip(images, self.MIN_MAX[0], self.MIN_MAX[1]) - images = self._normalize(images, copy=True, quantize=quantize) + images = self._normalize( + images, copy=True, clip=True, quantize=quantize) return images - def _normalize(self, im, copy=False, quantize=True): + def _normalize(self, im, copy=False, clip=True, quantize=True): if copy: im = im.copy() + if clip: + im = np.clip(im, self.MIN_MAX[0], self.MIN_MAX[1]) im -= im.min() im /= im.max() + 1e-32 if quantize: @@ -605,32 +621,9 @@ class BaseRestorer(BaseModel): return im -class RCAN(BaseRestorer): - def __init__(self, - losses=None, - sr_factor=4, - n_resgroups=10, - n_resblocks=20, - n_feats=64, - n_colors=3, - rgb_range=255, - kernel_size=3, - reduction=16, - **params): - params.update({ - 'n_resgroups': n_resgroups, - 'n_resblocks': n_resblocks, - 'n_feats': n_feats, - 'n_colors': n_colors, - 'rgb_range': rgb_range, - 'kernel_size': kernel_size, - 'reduction': reduction - }) - super(RCAN, self).__init__( - model_name='RCAN', losses=losses, sr_factor=sr_factor, **params) - - class DRN(BaseRestorer): + TEST_OUT_KEY = -1 + def __init__(self, losses=None, sr_factor=4, @@ -638,8 +631,10 @@ class DRN(BaseRestorer): n_blocks=30, n_feats=16, n_colors=3, - rgb_range=255, + rgb_range=1.0, negval=0.2, + lq_loss_weight=0.1, + dual_loss_weight=0.1, **params): if sr_factor != max(scale): raise ValueError(f"`sr_factor` must be equal to `max(scale)`.") @@ -651,12 +646,80 @@ class DRN(BaseRestorer): 'rgb_range': rgb_range, 'negval': negval }) + self.lq_loss_weight = lq_loss_weight + self.dual_loss_weight = dual_loss_weight super(DRN, self).__init__( model_name='DRN', losses=losses, sr_factor=sr_factor, **params) def build_net(self, **params): - net = ppgan.models.generators.DRNGenerator(**params) - return net + from ppgan.modules.init import init_weights + generators = [ppgan.models.generators.DRNGenerator(**params)] + init_weights(generators[-1]) + for scale in params['scale']: + dual_model = ppgan.models.generators.drn.DownBlock( + params['negval'], params['n_feats'], params['n_colors'], 2) + generators.append(dual_model) + init_weights(generators[-1]) + return GANAdapter(generators, []) + + def default_optimizer(self, parameters, *args, **kwargs): + optims_g = [ + super(DRN, self).default_optimizer(params_g, *args, **kwargs) + for params_g in parameters['params_g'] + ] + return OptimizerAdapter(*optims_g) + + def run_gan(self, net, inputs, mode, gan_mode='forward_primary'): + if mode != 'train': + raise ValueError("`mode` is not 'train'.") + outputs = OrderedDict() + if gan_mode == 'forward_primary': + sr = net.generator(inputs[0]) + lr = [inputs[0]] + lr.extend([ + F.interpolate( + inputs[0], scale_factor=s, mode='bicubic') + for s in net.generator.scale[:-1] + ]) + loss = self.losses(sr[-1], inputs[1]) + for i in range(1, len(sr)): + if self.lq_loss_weight > 0: + loss += self.losses(sr[i - 1 - len(sr)], + lr[i - len(sr)]) * self.lq_loss_weight + outputs['loss_prim'] = loss + outputs['sr'] = sr + outputs['lr'] = lr + elif gan_mode == 'forward_dual': + sr, lr = inputs[0], inputs[1] + sr2lr = [] + n_scales = len(net.generator.scale) + for i in range(n_scales): + sr2lr_i = net.generators[1 + i](sr[i - n_scales]) + sr2lr.append(sr2lr_i) + loss = self.losses(sr2lr[0], lr[0]) + for i in range(1, n_scales): + if self.dual_loss_weight > 0.0: + loss += self.losses(sr2lr[i], lr[i]) * self.dual_loss_weight + outputs['loss_dual'] = loss + else: + raise ValueError("Invalid `gan_mode`!") + return outputs + + def train_step(self, step, data, net): + outputs = self.run_gan( + net, data, mode='train', gan_mode='forward_primary') + outputs.update( + self.run_gan( + net, (outputs['sr'], outputs['lr']), + mode='train', + gan_mode='forward_dual')) + self.optimizer.clear_grad() + (outputs['loss_prim'] + outputs['loss_dual']).backward() + self.optimizer.step() + return { + 'loss_prim': outputs['loss_prim'], + 'loss_dual': outputs['loss_dual'] + } class LESRCNN(BaseRestorer): @@ -680,8 +743,6 @@ class LESRCNN(BaseRestorer): class ESRGAN(BaseRestorer): - MIN_MAX = (0., 1.) - def __init__(self, losses=None, sr_factor=4, @@ -704,7 +765,9 @@ class ESRGAN(BaseRestorer): model_name='ESRGAN', losses=losses, sr_factor=sr_factor, **params) def build_net(self, **params): + from ppgan.modules.init import init_weights generator = ppgan.models.generators.RRDBNet(**params) + init_weights(generator) if self.use_gan: discriminator = ppgan.models.discriminators.VGGDiscriminator128( in_channels=params['out_nc'], num_feat=64) @@ -716,10 +779,13 @@ class ESRGAN(BaseRestorer): def default_loss(self): if self.use_gan: - self.losses = { + return { 'pixel': res_losses.L1Loss(loss_weight=0.01), - 'perceptual': - res_losses.PerceptualLoss(layer_weights={'34': 1.0}), + 'perceptual': res_losses.PerceptualLoss( + layer_weights={'34': 1.0}, + perceptual_weight=1.0, + style_weight=0.0, + norm_img=False), 'gan': res_losses.GANLoss( gan_mode='vanilla', loss_weight=0.005) } @@ -734,7 +800,7 @@ class ESRGAN(BaseRestorer): parameters['params_d'][0], *args, **kwargs) return OptimizerAdapter(optim_g, optim_d) else: - return super(ESRGAN, self).default_optimizer(params, *args, + return super(ESRGAN, self).default_optimizer(parameters, *args, **kwargs) def run_gan(self, net, inputs, mode, gan_mode='forward_g'): @@ -744,8 +810,8 @@ class ESRGAN(BaseRestorer): if gan_mode == 'forward_g': loss_g = 0 g_pred = net.generator(inputs[0]) - loss_pix = self.losses['pixel'](g_pred, tar) - loss_perc, loss_sty = self.losses['perceptual'](g_pred, tar) + loss_pix = self.losses['pixel'](g_pred, inputs[1]) + loss_perc, loss_sty = self.losses['perceptual'](g_pred, inputs[1]) loss_g += loss_pix if loss_perc is not None: loss_g += loss_perc @@ -767,14 +833,14 @@ class ESRGAN(BaseRestorer): elif gan_mode == 'forward_d': self._set_requires_grad(net.discriminator, True) # Real - fake_d_pred = net.discriminator(data[0]).detach() - real_d_pred = net.discriminator(data[1]) + fake_d_pred = net.discriminator(inputs[0]).detach() + real_d_pred = net.discriminator(inputs[1]) loss_d_real = self.losses['gan']( real_d_pred - paddle.mean(fake_d_pred), True, is_disc=True) * 0.5 # Fake - fake_d_pred = self.nets['discriminator'](self.output.detach()) - loss_d_fake = self.gan_criterion( + fake_d_pred = net.discriminator(inputs[0].detach()) + loss_d_fake = self.losses['gan']( fake_d_pred - paddle.mean(real_d_pred.detach()), False, is_disc=True) * 0.5 @@ -802,30 +868,43 @@ class ESRGAN(BaseRestorer): outputs['loss_d'].backward() optim_d.step() - outputs['loss'] = outupts['loss_g_pps'] + outputs[ + outputs['loss'] = outputs['loss_g_pps'] + outputs[ 'loss_g_gan'] + outputs['loss_d'] - if isinstance(optim_g._learning_rate, - paddle.optimizer.lr.LRScheduler): - # If ReduceOnPlateau is used as the scheduler, use the loss value as the metric. - if isinstance(optim_g._learning_rate, - paddle.optimizer.lr.ReduceOnPlateau): - optim_g._learning_rate.step(loss.item()) - else: - optim_g._learning_rate.step() - - if isinstance(optim_d._learning_rate, - paddle.optimizer.lr.LRScheduler): - if isinstance(optim_d._learning_rate, - paddle.optimizer.lr.ReduceOnPlateau): - optim_d._learning_rate.step(loss.item()) - else: - optim_d._learning_rate.step() - - return outputs + return { + 'loss': outputs['loss'], + 'loss_g_pps': outputs['loss_g_pps'], + 'loss_g_gan': outputs['loss_g_gan'], + 'loss_d': outputs['loss_d'] + } else: - super(ESRGAN, self).train_step(step, data, net) + return super(ESRGAN, self).train_step(step, data, net) def _set_requires_grad(self, net, requires_grad): for p in net.parameters(): p.trainable = requires_grad + + +class RCAN(BaseRestorer): + def __init__(self, + losses=None, + sr_factor=4, + n_resgroups=10, + n_resblocks=20, + n_feats=64, + n_colors=3, + rgb_range=1.0, + kernel_size=3, + reduction=16, + **params): + params.update({ + 'n_resgroups': n_resgroups, + 'n_resblocks': n_resblocks, + 'n_feats': n_feats, + 'n_colors': n_colors, + 'rgb_range': rgb_range, + 'kernel_size': kernel_size, + 'reduction': reduction + }) + super(RCAN, self).__init__( + model_name='RCAN', losses=losses, sr_factor=sr_factor, **params) diff --git a/paddlers/tasks/segmenter.py b/paddlers/tasks/segmenter.py index 7589a9d..137ba43 100644 --- a/paddlers/tasks/segmenter.py +++ b/paddlers/tasks/segmenter.py @@ -33,7 +33,7 @@ from paddlers.utils import get_single_card_bs, DisablePrint from paddlers.utils.checkpoint import seg_pretrain_weights_dict from .base import BaseModel from .utils import seg_metrics as metrics -from .utils.infer_nets import InferNet +from .utils.infer_nets import InferSegNet __all__ = ["UNet", "DeepLabV3P", "FastSCNN", "HRNet", "BiSeNetV2", "FarSeg"] @@ -71,7 +71,7 @@ class BaseSegmenter(BaseModel): return net def _build_inference_net(self): - infer_net = InferNet(self.net, self.model_type) + infer_net = InferSegNet(self.net) infer_net.eval() return infer_net @@ -755,11 +755,11 @@ class BaseSegmenter(BaseModel): elif item[0] == 'padding': x, y = item[2] if isinstance(label_map, np.ndarray): - label_map = label_map[..., y:y + h, x:x + w] - score_map = score_map[..., y:y + h, x:x + w] + label_map = label_map[y:y + h, x:x + w] + score_map = score_map[y:y + h, x:x + w] else: - label_map = label_map[:, :, y:y + h, x:x + w] - score_map = score_map[:, :, y:y + h, x:x + w] + label_map = label_map[:, y:y + h, x:x + w, :] + score_map = score_map[:, y:y + h, x:x + w, :] else: pass label_map = label_map.squeeze() diff --git a/paddlers/tasks/utils/infer_nets.py b/paddlers/tasks/utils/infer_nets.py index e35731c..f20b94b 100644 --- a/paddlers/tasks/utils/infer_nets.py +++ b/paddlers/tasks/utils/infer_nets.py @@ -15,30 +15,36 @@ import paddle -class PostProcessor(paddle.nn.Layer): - def __init__(self, model_type): - super(PostProcessor, self).__init__() - self.model_type = model_type - +class SegPostProcessor(paddle.nn.Layer): def forward(self, net_outputs): # label_map [NHW], score_map [NHWC] logit = net_outputs[0] outputs = paddle.argmax(logit, axis=1, keepdim=False, dtype='int32'), \ paddle.transpose(paddle.nn.functional.softmax(logit, axis=1), perm=[0, 2, 3, 1]) + return outputs + + +class ResPostProcessor(paddle.nn.Layer): + def __init__(self, out_key=None): + super(ResPostProcessor, self).__init__() + self.out_key = out_key + def forward(self, net_outputs): + if self.out_key is not None: + net_outputs = net_outputs[self.out_key] + outputs = paddle.transpose(net_outputs, perm=[0, 2, 3, 1]) return outputs -class InferNet(paddle.nn.Layer): - def __init__(self, net, model_type): - super(InferNet, self).__init__() +class InferSegNet(paddle.nn.Layer): + def __init__(self, net): + super(InferSegNet, self).__init__() self.net = net - self.postprocessor = PostProcessor(model_type) + self.postprocessor = SegPostProcessor() def forward(self, x): net_outputs = self.net(x) outputs = self.postprocessor(net_outputs) - return outputs @@ -46,10 +52,21 @@ class InferCDNet(paddle.nn.Layer): def __init__(self, net): super(InferCDNet, self).__init__() self.net = net - self.postprocessor = PostProcessor('change_detector') + self.postprocessor = SegPostProcessor() def forward(self, x1, x2): net_outputs = self.net(x1, x2) outputs = self.postprocessor(net_outputs) + return outputs + + +class InferResNet(paddle.nn.Layer): + def __init__(self, net, out_key=None): + super(InferResNet, self).__init__() + self.net = net + self.postprocessor = ResPostProcessor(out_key=out_key) + def forward(self, x): + net_outputs = self.net(x) + outputs = self.postprocessor(net_outputs) return outputs diff --git a/paddlers/tasks/utils/res_adapters.py b/paddlers/tasks/utils/res_adapters.py index bcca4c0..eba8106 100644 --- a/paddlers/tasks/utils/res_adapters.py +++ b/paddlers/tasks/utils/res_adapters.py @@ -122,7 +122,11 @@ class OptimizerAdapter(Adapter): __ducktype__ = paddle.optimizer.Optimizer __ava__ = ('state_dict', 'set_state_dict', 'clear_grad', 'step', 'get_lr') - # Special dispatching rule def set_state_dict(self, state_dicts): + # Special dispatching rule for optim, state_dict in zip(self, state_dicts): optim.set_state_dict(state_dict) + + def get_lr(self): + # Return the lr of the first optimizer + return self[0].get_lr() diff --git a/paddlers/transforms/operators.py b/paddlers/transforms/operators.py index cb36f14..6267239 100644 --- a/paddlers/transforms/operators.py +++ b/paddlers/transforms/operators.py @@ -1207,7 +1207,7 @@ class RandomCrop(Transform): if 'target' in sample: if 'sr_factor' in sample: sample['target'] = self.apply_im( - sample['image'], + sample['target'], calc_hr_shape(crop_box, sample['sr_factor'])) else: sample['target'] = self.apply_im(sample['image'], crop_box) @@ -1993,8 +1993,9 @@ class ArrangeDetector(Arrange): class ArrangeRestorer(Arrange): def apply(self, sample): + if 'target' in sample: + target = permute(sample['target'], False) image = permute(sample['image'], False) - target = permute(sample['target'], False) if self.mode == 'train': return image, target if self.mode == 'eval': diff --git a/paddlers/utils/__init__.py b/paddlers/utils/__init__.py index 950ea73..8be069c 100644 --- a/paddlers/utils/__init__.py +++ b/paddlers/utils/__init__.py @@ -16,7 +16,7 @@ from . import logging from . import utils from .utils import (seconds_to_hms, get_encoding, get_single_card_bs, dict2str, EarlyStop, norm_path, is_pic, MyEncoder, DisablePrint, - Timer) + Timer, to_data_parallel, scheduler_step) from .checkpoint import get_pretrain_weights, load_pretrain_weights, load_checkpoint from .env import get_environ_info, get_num_workers, init_parallel_env from .download import download_and_decompress, decompress diff --git a/paddlers/utils/utils.py b/paddlers/utils/utils.py index 692a1c6..5f0794a 100644 --- a/paddlers/utils/utils.py +++ b/paddlers/utils/utils.py @@ -20,11 +20,12 @@ import math import imghdr import chardet import json +import platform import numpy as np +import paddle from . import logging -import platform import paddlers @@ -237,3 +238,33 @@ class Timer(Times): self.postprocess_time_s.reset() self.img_num = 0 self.repeats = 0 + + +def to_data_parallel(layers, *args, **kwargs): + from paddlers.tasks.utils.res_adapters import GANAdapter + if isinstance(layers, GANAdapter): + # Inplace modification for efficiency + layers.generators = [ + paddle.DataParallel(g, *args, **kwargs) for g in layers.generators + ] + layers.discriminators = [ + paddle.DataParallel(d, *args, **kwargs) + for d in layers.discriminators + ] + else: + layers = paddle.DataParallel(layers, *args, **kwargs) + return layers + + +def scheduler_step(optimizer): + from paddlers.tasks.utils.res_adapters import OptimizerAdapter + if not isinstance(optimizer, OptimizerAdapter): + optimizer = [optimizer] + for optim in optimizer: + if isinstance(optim._learning_rate, paddle.optimizer.lr.LRScheduler): + # If ReduceOnPlateau is used as the scheduler, use the loss value as the metric. + if isinstance(optim._learning_rate, + paddle.optimizer.lr.ReduceOnPlateau): + optim._learning_rate.step(loss.item()) + else: + optim._learning_rate.step() diff --git a/tutorials/train/README.md b/tutorials/train/README.md index 6c553ce..350f3dc 100644 --- a/tutorials/train/README.md +++ b/tutorials/train/README.md @@ -20,7 +20,6 @@ |image_restoration/drn.py | 图像复原 | DRN | |image_restoration/esrgan.py | 图像复原 | ESRGAN | |image_restoration/lesrcnn.py | 图像复原 | LESRCNN | -|image_restoration/rcan.py | 图像复原 | RCAN | |object_detection/faster_rcnn.py | 目标检测 | Faster R-CNN | |object_detection/ppyolo.py | 目标检测 | PP-YOLO | |object_detection/ppyolotiny.py | 目标检测 | PP-YOLO Tiny | diff --git a/tutorials/train/image_restoration/drn.py b/tutorials/train/image_restoration/drn.py index abd7e1d..400a4ad 100644 --- a/tutorials/train/image_restoration/drn.py +++ b/tutorials/train/image_restoration/drn.py @@ -25,8 +25,8 @@ pdrs.utils.download_and_decompress( train_transforms = T.Compose([ # 读取影像 T.DecodeImg(), - # 将输入影像缩放到256x256大小 - T.Resize(target_size=256), + # 从输入影像中裁剪96x96大小的影像块 + T.RandomCrop(crop_size=96), # 以50%的概率实施随机水平翻转 T.RandomHorizontalFlip(prob=0.5), # 以50%的概率实施随机垂直翻转 @@ -39,6 +39,7 @@ train_transforms = T.Compose([ eval_transforms = T.Compose([ T.DecodeImg(), + # 将输入影像缩放到256x256大小 T.Resize(target_size=256), # 验证阶段与训练阶段的数据归一化方式必须相同 T.Normalize( @@ -52,14 +53,16 @@ train_dataset = pdrs.datasets.ResDataset( file_list=TRAIN_FILE_LIST_PATH, transforms=train_transforms, num_workers=0, - shuffle=True) + shuffle=True, + sr_factor=4) eval_dataset = pdrs.datasets.ResDataset( data_dir=DATA_DIR, file_list=EVAL_FILE_LIST_PATH, transforms=eval_transforms, num_workers=0, - shuffle=False) + shuffle=False, + sr_factor=4) # 使用默认参数构建DRN模型 # 目前已支持的模型请参考:https://github.com/PaddlePaddle/PaddleRS/blob/develop/docs/intro/model_zoo.md @@ -74,10 +77,10 @@ model.train( eval_dataset=eval_dataset, save_interval_epochs=1, # 每多少次迭代记录一次日志 - log_interval_steps=50, + log_interval_steps=5, save_dir=EXP_DIR, # 初始学习率大小 - learning_rate=0.01, + learning_rate=0.001, # 是否使用early stopping策略,当精度不再改善时提前终止训练 early_stop=False, # 是否启用VisualDL日志功能 diff --git a/tutorials/train/image_restoration/esrgan.py b/tutorials/train/image_restoration/esrgan.py index 5d97def..7e9bb89 100644 --- a/tutorials/train/image_restoration/esrgan.py +++ b/tutorials/train/image_restoration/esrgan.py @@ -25,8 +25,8 @@ pdrs.utils.download_and_decompress( train_transforms = T.Compose([ # 读取影像 T.DecodeImg(), - # 将输入影像缩放到256x256大小 - T.Resize(target_size=256), + # 从输入影像中裁剪32x32大小的影像块 + T.RandomCrop(crop_size=32), # 以50%的概率实施随机水平翻转 T.RandomHorizontalFlip(prob=0.5), # 以50%的概率实施随机垂直翻转 @@ -39,6 +39,7 @@ train_transforms = T.Compose([ eval_transforms = T.Compose([ T.DecodeImg(), + # 将输入影像缩放到256x256大小 T.Resize(target_size=256), # 验证阶段与训练阶段的数据归一化方式必须相同 T.Normalize( @@ -52,14 +53,16 @@ train_dataset = pdrs.datasets.ResDataset( file_list=TRAIN_FILE_LIST_PATH, transforms=train_transforms, num_workers=0, - shuffle=True) + shuffle=True, + sr_factor=4) eval_dataset = pdrs.datasets.ResDataset( data_dir=DATA_DIR, file_list=EVAL_FILE_LIST_PATH, transforms=eval_transforms, num_workers=0, - shuffle=False) + shuffle=False, + sr_factor=4) # 使用默认参数构建ESRGAN模型 # 目前已支持的模型请参考:https://github.com/PaddlePaddle/PaddleRS/blob/develop/docs/intro/model_zoo.md @@ -74,10 +77,10 @@ model.train( eval_dataset=eval_dataset, save_interval_epochs=1, # 每多少次迭代记录一次日志 - log_interval_steps=50, + log_interval_steps=5, save_dir=EXP_DIR, # 初始学习率大小 - learning_rate=0.01, + learning_rate=0.001, # 是否使用early stopping策略,当精度不再改善时提前终止训练 early_stop=False, # 是否启用VisualDL日志功能 diff --git a/tutorials/train/image_restoration/lesrcnn.py b/tutorials/train/image_restoration/lesrcnn.py index 6a97b10..0689c01 100644 --- a/tutorials/train/image_restoration/lesrcnn.py +++ b/tutorials/train/image_restoration/lesrcnn.py @@ -25,8 +25,8 @@ pdrs.utils.download_and_decompress( train_transforms = T.Compose([ # 读取影像 T.DecodeImg(), - # 将输入影像缩放到256x256大小 - T.Resize(target_size=256), + # 从输入影像中裁剪32x32大小的影像块 + T.RandomCrop(crop_size=32), # 以50%的概率实施随机水平翻转 T.RandomHorizontalFlip(prob=0.5), # 以50%的概率实施随机垂直翻转 @@ -39,6 +39,7 @@ train_transforms = T.Compose([ eval_transforms = T.Compose([ T.DecodeImg(), + # 将输入影像缩放到256x256大小 T.Resize(target_size=256), # 验证阶段与训练阶段的数据归一化方式必须相同 T.Normalize( @@ -52,14 +53,16 @@ train_dataset = pdrs.datasets.ResDataset( file_list=TRAIN_FILE_LIST_PATH, transforms=train_transforms, num_workers=0, - shuffle=True) + shuffle=True, + sr_factor=4) eval_dataset = pdrs.datasets.ResDataset( data_dir=DATA_DIR, file_list=EVAL_FILE_LIST_PATH, transforms=eval_transforms, num_workers=0, - shuffle=False) + shuffle=False, + sr_factor=4) # 使用默认参数构建LESRCNN模型 # 目前已支持的模型请参考:https://github.com/PaddlePaddle/PaddleRS/blob/develop/docs/intro/model_zoo.md @@ -74,10 +77,10 @@ model.train( eval_dataset=eval_dataset, save_interval_epochs=1, # 每多少次迭代记录一次日志 - log_interval_steps=50, + log_interval_steps=5, save_dir=EXP_DIR, # 初始学习率大小 - learning_rate=0.01, + learning_rate=0.001, # 是否使用early stopping策略,当精度不再改善时提前终止训练 early_stop=False, # 是否启用VisualDL日志功能 From 3fc53c9454f32891854b384c7f80ffea92f0360d Mon Sep 17 00:00:00 2001 From: Bobholamovic Date: Mon, 22 Aug 2022 10:23:39 +0800 Subject: [PATCH 18/52] Update example --- .../custom_model/iterative_bit_iter2_gamma01.yaml | 12 ------------ .../custom_model/iterative_bit_iter2_gamma02.yaml | 12 ------------ .../custom_model/iterative_bit_iter2_gamma05.yaml | 12 ------------ .../custom_model/iterative_bit_iter3_gamma01.yaml | 12 ------------ .../custom_model/iterative_bit_iter3_gamma02.yaml | 12 ------------ .../custom_model/iterative_bit_iter3_gamma05.yaml | 12 ------------ .../custom_model/iterative_bit_iter3_gamma10.yaml | 12 ------------ examples/rs_research/configs/svcd/custom_model.yaml | 2 +- examples/rs_research/configs/svcd/fc_ef.yaml | 2 -- examples/rs_research/configs/svcd/fc_siam_conc.yaml | 2 -- examples/rs_research/configs/svcd/fc_siam_diff.yaml | 2 -- 11 files changed, 1 insertion(+), 91 deletions(-) delete mode 100644 examples/rs_research/configs/levircd/custom_model/iterative_bit_iter2_gamma01.yaml delete mode 100644 examples/rs_research/configs/levircd/custom_model/iterative_bit_iter2_gamma02.yaml delete mode 100644 examples/rs_research/configs/levircd/custom_model/iterative_bit_iter2_gamma05.yaml delete mode 100644 examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma01.yaml delete mode 100644 examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma02.yaml delete mode 100644 examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma05.yaml delete mode 100644 examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma10.yaml diff --git a/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter2_gamma01.yaml b/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter2_gamma01.yaml deleted file mode 100644 index bac3925..0000000 --- a/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter2_gamma01.yaml +++ /dev/null @@ -1,12 +0,0 @@ -_base_: ../levircd.yaml - -save_dir: ./exp/levircd/custom_model/iter2_gamma01/ - -model: !Node - type: IterativeBIT - args: - num_iters: 2 - gamma: 0.1 - num_classes: 2 - bit_kwargs: - in_channels: 4 diff --git a/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter2_gamma02.yaml b/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter2_gamma02.yaml deleted file mode 100644 index 72c7683..0000000 --- a/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter2_gamma02.yaml +++ /dev/null @@ -1,12 +0,0 @@ -_base_: ../levircd.yaml - -save_dir: ./exp/levircd/custom_model/iter2_gamma02/ - -model: !Node - type: IterativeBIT - args: - num_iters: 2 - gamma: 0.2 - num_classes: 2 - bit_kwargs: - in_channels: 4 diff --git a/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter2_gamma05.yaml b/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter2_gamma05.yaml deleted file mode 100644 index 7a259f3..0000000 --- a/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter2_gamma05.yaml +++ /dev/null @@ -1,12 +0,0 @@ -_base_: ../levircd.yaml - -save_dir: ./exp/levircd/custom_model/iter2_gamma05/ - -model: !Node - type: IterativeBIT - args: - num_iters: 2 - gamma: 0.5 - num_classes: 2 - bit_kwargs: - in_channels: 4 diff --git a/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma01.yaml b/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma01.yaml deleted file mode 100644 index c0679aa..0000000 --- a/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma01.yaml +++ /dev/null @@ -1,12 +0,0 @@ -_base_: ../levircd.yaml - -save_dir: ./exp/levircd/custom_model/iter3_gamma01/ - -model: !Node - type: IterativeBIT - args: - num_iters: 3 - gamma: 0.1 - num_classes: 2 - bit_kwargs: - in_channels: 4 diff --git a/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma02.yaml b/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma02.yaml deleted file mode 100644 index fce2be1..0000000 --- a/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma02.yaml +++ /dev/null @@ -1,12 +0,0 @@ -_base_: ../levircd.yaml - -save_dir: ./exp/levircd/custom_model/iter3_gamma02/ - -model: !Node - type: IterativeBIT - args: - num_iters: 3 - gamma: 0.2 - num_classes: 2 - bit_kwargs: - in_channels: 4 diff --git a/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma05.yaml b/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma05.yaml deleted file mode 100644 index 4103af3..0000000 --- a/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma05.yaml +++ /dev/null @@ -1,12 +0,0 @@ -_base_: ../levircd.yaml - -save_dir: ./exp/levircd/custom_model/iter3_gamma05/ - -model: !Node - type: IterativeBIT - args: - num_iters: 3 - gamma: 0.5 - num_classes: 2 - bit_kwargs: - in_channels: 4 diff --git a/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma10.yaml b/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma10.yaml deleted file mode 100644 index 2e5481c..0000000 --- a/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3_gamma10.yaml +++ /dev/null @@ -1,12 +0,0 @@ -_base_: ../levircd.yaml - -save_dir: ./exp/levircd/custom_model/iter3_gamma10/ - -model: !Node - type: IterativeBIT - args: - num_iters: 3 - gamma: 1.0 - num_classes: 2 - bit_kwargs: - in_channels: 4 diff --git a/examples/rs_research/configs/svcd/custom_model.yaml b/examples/rs_research/configs/svcd/custom_model.yaml index 7262637..aac6748 100644 --- a/examples/rs_research/configs/svcd/custom_model.yaml +++ b/examples/rs_research/configs/svcd/custom_model.yaml @@ -5,7 +5,7 @@ save_dir: ./exp/svcd/custom_model/ model: !Node type: IterativeBIT args: - num_iters: 3 + num_iters: 2 num_classes: 2 bit_kwargs: in_channels: 3 diff --git a/examples/rs_research/configs/svcd/fc_ef.yaml b/examples/rs_research/configs/svcd/fc_ef.yaml index ed86ab8..81bbb34 100644 --- a/examples/rs_research/configs/svcd/fc_ef.yaml +++ b/examples/rs_research/configs/svcd/fc_ef.yaml @@ -4,5 +4,3 @@ save_dir: ./exp/svcd/fc_ef/ model: !Node type: FCEarlyFusion - args: - use_dropout: True diff --git a/examples/rs_research/configs/svcd/fc_siam_conc.yaml b/examples/rs_research/configs/svcd/fc_siam_conc.yaml index 3c46d03..fb4eed8 100644 --- a/examples/rs_research/configs/svcd/fc_siam_conc.yaml +++ b/examples/rs_research/configs/svcd/fc_siam_conc.yaml @@ -4,5 +4,3 @@ save_dir: ./exp/svcd/fc_siam_conc/ model: !Node type: FCSiamConc - args: - use_dropout: True diff --git a/examples/rs_research/configs/svcd/fc_siam_diff.yaml b/examples/rs_research/configs/svcd/fc_siam_diff.yaml index 3aa30c6..fde20b9 100644 --- a/examples/rs_research/configs/svcd/fc_siam_diff.yaml +++ b/examples/rs_research/configs/svcd/fc_siam_diff.yaml @@ -4,5 +4,3 @@ save_dir: ./exp/svcd/fc_siam_diff/ model: !Node type: FCSiamDiff - args: - use_dropout: True From 09d3dd1202d3dff4088a3b7c6246fb90172ab04c Mon Sep 17 00:00:00 2001 From: Bobholamovic Date: Mon, 22 Aug 2022 16:18:49 +0800 Subject: [PATCH 19/52] Change custom model --- examples/rs_research/README.md | 146 +++++++------- examples/rs_research/custom_model.py | 266 ++++++++++++++++++------- examples/rs_research/custom_trainer.py | 16 +- 3 files changed, 273 insertions(+), 155 deletions(-) diff --git a/examples/rs_research/README.md b/examples/rs_research/README.md index ec83444..f0c2aa4 100644 --- a/examples/rs_research/README.md +++ b/examples/rs_research/README.md @@ -43,64 +43,73 @@ python ../../tools/prepare_dataset/prepare_svcd.py \ 1. 巨大的参数量意味着巨大的存储开销。在许多实际场景中,硬件资源往往是有限的,过多的模型参数将给部署造成困难。 2. 在数据有限的情况下,大模型更易遭受过拟合,其在实验数据集上看起来良好的结果也难以泛化到真实场景。 -本案例认为,上述问题的根源在于参数量与数据量的失衡所导致的特征冗余。既然模型的特征存在冗余,是否存在某种手段,能够在固定模型参数量的前提下对特征进行优化,从而“榨取”小模型的更多潜力?基于这个观点,本案例的基本思路是设计一种基于网络迭代优化思想的深度学习变化检测算法。首先,构造一个轻量级的变化检测模型,并以其作为基础迭代单元。在每次迭代开始时,由上一次迭代输出的概率图以及原始的输入影像对构造新的输入,如此逐级实现coarse-to-fine优化。考虑到增加迭代单元的数量将使模型参数量成倍增加,在迭代过程中应始终复用同一迭代单元的参数以充分挖掘变化检测网络的拟合能力,迫使其学习到更加有效的特征。这一做法类似[循环神经网络](https://baike.baidu.com/item/%E5%BE%AA%E7%8E%AF%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C/23199490)。根据此思路可以绘制框图如下: +本案例认为,上述问题的根源在于参数量与数据量的失衡所导致的特征冗余。既然模型的特征存在冗余,也即存在一部分“无用”的特征,是否存在某种手段,能够在固定模型参数量的前提下对特征进行优化,从而“榨取”小模型的更多潜力,获取更多更加有效的特征?基于这个观点,本案例的基本思路是为现有的变化检测模型添加一个“插件式”的特征优化模块,在仅引入较少额外的参数数量的情况下,实现变化特征增强。本案例计划以变化检测领域经典的FC-Siam-diff[4]为baseline网络,利用时间、空间、通道注意力模块对网络的中间层特征进行优化,从而减小特征冗余,提升检测效果。在具体的模块设计方面,对于时间与通道维度,选用论文[5]中提出的通道注意力模块;对于空间维度,选用论文[5]中提出的空间注意力模块。 -![draft](draft.png) +### 3.2 模型定义 -### 3.2 确定baseline模型 +#### 3.2.1 自定义模型组网 -科研工作往往需要“站在巨人的肩膀上”,在前人工作的基础上做“增量创新”。因此,对模型设计类工作而言,选用一个合适的baseline模型至关重要。考虑到本案例的出发点是解决现有模型参数量过大、冗余特征过多的问题,并且在拟定的解决方案中使用到了循环结构,用作baseline的网络结构必须足够轻量和高效(因为最直接的思路是使用baseline作为基础迭代单元)。为此,本案例选用Bitemporal Image Transformer(BIT)作为baseline。BIT是一个轻量级的深度学习变化检测模型,其基本结构如图所示: - -![bit](bit.png) - -BIT的核心思想在于, - -### 3.3 定义新模型 - -确定了基本思路和baseline模型之后,可以绘制如下的算法整体框图: - -![framework](framework.png) +在`custom_model.py`中定义模型的宏观(macro)结构以及组成模型的各个微观(micro)模块。例如,本案例中,`custom_model.py`中定义了改进后的FC-EF结构,其核心部分实现如下: +```python +... +# PaddleRS提供了许多开箱即用的模块,其中有对底层基础模块的封装(如conv-bn-relu结构等),也有注意力模块等较高层级的结构 +from paddlers.rs_models.cd.layers import Conv3x3, MaxPool2x2, ConvTransposed3x3, Identity +from paddlers.rs_models.cd.layers import ChannelAttention, SpatialAttention -依据此框图,即可在。 +from attach_tools import Attach -#### 3.3.1 自定义模型组网 +attach = Attach.to(paddlers.rs_models.cd) -在`custom_model.py`中定义模型的宏观(macro)结构以及组成模型的各个微观(micro)模块。例如,当前`custom_model.py`中定义了迭代版本的BIT模型`IterativeBIT`: -```python @attach -class IterativeBIT(nn.Layer): - def __init__(self, num_iters=1, gamma=0.1, num_classes=2, bit_kwargs=None): +class CustomModel(nn.Layer): + def __init__(self, + in_channels, + num_classes, + att_types='cst', + use_dropout=False): super().__init__() - - if num_iters <= 0: - raise ValueError( - f"`num_iters` should have positive value, but got {num_iters}.") - - self.num_iters = num_iters - self.gamma = gamma - - if bit_kwargs is None: - bit_kwargs = dict() - - if 'num_classes' in bit_kwargs: - raise KeyError("'num_classes' should not be set in `bit_kwargs`.") - bit_kwargs['num_classes'] = num_classes - - self.bit = BIT(**bit_kwargs) + ... + + # 从`att_types`参数中获取要使用的注意力类型 + # 每个注意力模块都是可选的 + if 'c' in att_types: + self.att_c = ChannelAttention(C4) + else: + self.att_c = Identity() + if 's' in att_types: + self.att_s = SpatialAttention() + else: + self.att_s = Identity() + # 时间注意力模块部分复用通道注意力的逻辑,在`forward()`中将具体解释 + if 't' in att_types: + self.att_t = ChannelAttention(2, ratio=1) + else: + self.att_t = Identity() + + self.init_weight() def forward(self, t1, t2): - rate_map = self._init_rate_map(t1.shape) - - for it in range(self.num_iters): - # Construct inputs - x1 = self._constr_iter_input(t1, rate_map) - x2 = self._constr_iter_input(t2, rate_map) - # Get logits - logits_list = self.bit(x1, x2) - # Construct rate map - rate_map = self._constr_rate_map(logits_list[0]) - - return logits_list + ... + # 以下是本案例在FC-EF基础上新增的部分 + # x43_1和x43_2分别是FC-EF的两路编码器提取的特征 + # 首先使用通道和空间注意力模块对特征进行优化 + x43_1 = self.att_c(x43_1) * x43_1 + x43_1 = self.att_s(x43_1) * x43_1 + x43_2 = self.att_c(x43_2) * x43_2 + x43_2 = self.att_s(x43_2) * x43_2 + # 为了复用通道注意力模块执行时间维度的注意力操作,首先将两个时相的特征堆叠 + x43 = paddle.stack([x43_1, x43_2], axis=1) + # 堆叠后的x43形状为[b, t, c, h, w],其中b表示batch size,t为2(时相数目),c为通道数,h和w分别为特征图高宽 + # 将t和c维度交换,输出tensor形状为[b, c, t, h, w] + x43 = paddle.transpose(x43, [0, 2, 1, 3, 4]) + # 将b和c两个维度合并,输出tensor形状为[b*c, t, h, w] + x43 = paddle.flatten(x43, stop_axis=1) + # 此时,时间维度已经替代了原先的通道维度,将四维tensor输入ChannelAttention模块进行处理 + x43 = self.att_t(x43) * x43 + # 从处理结果中分离两个时相的信息 + x43 = x43.reshape((x43_1.shape[0], -1, 2, *x43.shape[2:])) + x43_1, x43_2 = x43[:,:,0], x43[:,:,1] + ... ... ``` @@ -112,27 +121,27 @@ class IterativeBIT(nn.Layer): 关于模型定义的更多细节请参考[文档](https://github.com/PaddlePaddle/PaddleRS/blob/develop/docs/dev/dev_guide.md)。 -#### 3.3.2 自定义训练器 +#### 3.2.2 自定义训练器 -在`custom_trainer.py`中定义训练器。例如,当前`custom_trainer.py`中定义了与`IterativeBIT`模型对应的训练器: +在`custom_trainer.py`中定义训练器。例如,本案例中,`custom_trainer.py`中定义了与`CustomModel`模型对应的训练器: ```python @attach -class IterativeBIT(BaseChangeDetector): +class CustomTrainer(BaseChangeDetector): def __init__(self, num_classes=2, use_mixed_loss=False, losses=None, - num_iters=1, - gamma=0.1, - bit_kwargs=None, + in_channels=3, + att_types='cst', + use_dropout=False, **params): params.update({ - 'num_iters': num_iters, - 'gamma': gamma, - 'bit_kwargs': bit_kwargs + 'in_channels': in_channels, + 'att_types': att_types, + 'use_dropout': use_dropout }) super().__init__( - model_name='IterativeBIT', + model_name='CustomModel', num_classes=num_classes, use_mixed_loss=use_mixed_loss, losses=losses, @@ -149,27 +158,17 @@ class IterativeBIT(BaseChangeDetector): 关于训练器的更多细节请参考[API文档](https://github.com/PaddlePaddle/PaddleRS/blob/develop/docs/apis/train.md)。 -### 3.4 进行参数分析与消融实验 +### 3.3 消融实验 -#### 3.4.1 实验设置 +#### 3.3.1 实验设置 -#### 3.4.2 编写配置文件 +#### 3.3.2 编写配置文件 -#### 3.4.3 实验结果 +#### 3.3.3 实验结果 VisualDL、定量指标 -### 3.5 \*Magic Behind - -本小节涉及技术细节,对于本案例来说属于进阶内容,您可以选择性了解。 - -#### 3.5.1 延迟属性绑定 - -PaddleRS提供了,只需要。`attach_tools.Attach`对象自动。 - -#### 3.5.2 非侵入式轻量级配置系统 - -### 3.5 开展特征可视化实验 +### 3.4 特征可视化实验 ## 4 对比实验 @@ -206,3 +205,4 @@ PaddleRS提供了,只需要。`attach_tools.Attach`对象自动。 [2] Lebedev, M. A., et al. "CHANGE DETECTION IN REMOTE SENSING IMAGES USING CONDITIONAL ADVERSARIAL NETWORKS." *International Archives of the Photogrammetry, Remote Sensing & Spatial Information Sciences* 42.2 (2018). [3] Chen, Hao, Zipeng Qi, and Zhenwei Shi. "Remote sensing image change detection with transformers." *IEEE Transactions on Geoscience and Remote Sensing* 60 (2021): 1-14. [4] Daudt, Rodrigo Caye, Bertr Le Saux, and Alexandre Boulch. "Fully convolutional siamese networks for change detection." *2018 25th IEEE International Conference on Image Processing (ICIP)*. IEEE, 2018. +[5] Woo, Sanghyun, et al. "Cbam: Convolutional block attention module." *Proceedings of the European conference on computer vision (ECCV)*. 2018. diff --git a/examples/rs_research/custom_model.py b/examples/rs_research/custom_model.py index 27b99dd..63e2f60 100644 --- a/examples/rs_research/custom_model.py +++ b/examples/rs_research/custom_model.py @@ -2,88 +2,206 @@ import paddle import paddle.nn as nn import paddle.nn.functional as F import paddlers -from paddlers.rs_models.cd import BIT +from paddlers.rs_models.cd.layers import Conv3x3, MaxPool2x2, ConvTransposed3x3, Identity +from paddlers.rs_models.cd.layers import ChannelAttention, SpatialAttention + from attach_tools import Attach attach = Attach.to(paddlers.rs_models.cd) @attach -class IterativeBIT(BIT): +class CustomModel(nn.Layer): def __init__(self, - num_iters=1, - feat_channels=32, - num_classes=2, - bit_kwargs=None): - if num_iters <= 0: - raise ValueError( - f"`num_iters` should have positive value, but got {num_iters}.") - - self.num_iters = num_iters - - if bit_kwargs is None: - bit_kwargs = dict() - - if 'num_classes' in bit_kwargs: - raise KeyError("'num_classes' should not be set in `bit_kwargs`.") - bit_kwargs['num_classes'] = num_classes - - super().__init__(**bit_kwargs) + in_channels, + num_classes, + att_types='cst', + use_dropout=False): + super(CustomModel, self).__init__() + + C1, C2, C3, C4, C5 = 16, 32, 64, 128, 256 + + self.use_dropout = use_dropout + + self.conv11 = Conv3x3(in_channels, C1, norm=True, act=True) + self.do11 = self._make_dropout() + self.conv12 = Conv3x3(C1, C1, norm=True, act=True) + self.do12 = self._make_dropout() + self.pool1 = MaxPool2x2() + + self.conv21 = Conv3x3(C1, C2, norm=True, act=True) + self.do21 = self._make_dropout() + self.conv22 = Conv3x3(C2, C2, norm=True, act=True) + self.do22 = self._make_dropout() + self.pool2 = MaxPool2x2() + + self.conv31 = Conv3x3(C2, C3, norm=True, act=True) + self.do31 = self._make_dropout() + self.conv32 = Conv3x3(C3, C3, norm=True, act=True) + self.do32 = self._make_dropout() + self.conv33 = Conv3x3(C3, C3, norm=True, act=True) + self.do33 = self._make_dropout() + self.pool3 = MaxPool2x2() + + self.conv41 = Conv3x3(C3, C4, norm=True, act=True) + self.do41 = self._make_dropout() + self.conv42 = Conv3x3(C4, C4, norm=True, act=True) + self.do42 = self._make_dropout() + self.conv43 = Conv3x3(C4, C4, norm=True, act=True) + self.do43 = self._make_dropout() + self.pool4 = MaxPool2x2() + + self.upconv4 = ConvTransposed3x3(C4, C4, output_padding=1) + + self.conv43d = Conv3x3(C5, C4, norm=True, act=True) + self.do43d = self._make_dropout() + self.conv42d = Conv3x3(C4, C4, norm=True, act=True) + self.do42d = self._make_dropout() + self.conv41d = Conv3x3(C4, C3, norm=True, act=True) + self.do41d = self._make_dropout() + + self.upconv3 = ConvTransposed3x3(C3, C3, output_padding=1) + + self.conv33d = Conv3x3(C4, C3, norm=True, act=True) + self.do33d = self._make_dropout() + self.conv32d = Conv3x3(C3, C3, norm=True, act=True) + self.do32d = self._make_dropout() + self.conv31d = Conv3x3(C3, C2, norm=True, act=True) + self.do31d = self._make_dropout() + + self.upconv2 = ConvTransposed3x3(C2, C2, output_padding=1) + + self.conv22d = Conv3x3(C3, C2, norm=True, act=True) + self.do22d = self._make_dropout() + self.conv21d = Conv3x3(C2, C1, norm=True, act=True) + self.do21d = self._make_dropout() + + self.upconv1 = ConvTransposed3x3(C1, C1, output_padding=1) + + self.conv12d = Conv3x3(C2, C1, norm=True, act=True) + self.do12d = self._make_dropout() + self.conv11d = Conv3x3(C1, num_classes) + + if 'c' in att_types: + self.att_c = ChannelAttention(C4) + else: + self.att_c = Identity() + if 's' in att_types: + self.att_s = SpatialAttention() + else: + self.att_s = Identity() + if 't' in att_types: + self.att_t = ChannelAttention(2, ratio=1) + else: + self.att_t = Identity() - self.conv_fuse = nn.Sequential( - nn.Conv2D(feat_channels + 1, feat_channels, 1), nn.Sigmoid()) + self.init_weight() def forward(self, t1, t2): - # Extract features via shared backbone. - x1 = self.backbone(t1) - x2 = self.backbone(t2) - - # Tokenization - if self.use_tokenizer: - token1 = self._get_semantic_tokens(x1) - token2 = self._get_semantic_tokens(x2) + # Encode t1 + # Stage 1 + x11 = self.do11(self.conv11(t1)) + x12_1 = self.do12(self.conv12(x11)) + x1p = self.pool1(x12_1) + + # Stage 2 + x21 = self.do21(self.conv21(x1p)) + x22_1 = self.do22(self.conv22(x21)) + x2p = self.pool2(x22_1) + + # Stage 3 + x31 = self.do31(self.conv31(x2p)) + x32 = self.do32(self.conv32(x31)) + x33_1 = self.do33(self.conv33(x32)) + x3p = self.pool3(x33_1) + + # Stage 4 + x41 = self.do41(self.conv41(x3p)) + x42 = self.do42(self.conv42(x41)) + x43_1 = self.do43(self.conv43(x42)) + x4p = self.pool4(x43_1) + + # Encode t2 + # Stage 1 + x11 = self.do11(self.conv11(t2)) + x12_2 = self.do12(self.conv12(x11)) + x1p = self.pool1(x12_2) + + # Stage 2 + x21 = self.do21(self.conv21(x1p)) + x22_2 = self.do22(self.conv22(x21)) + x2p = self.pool2(x22_2) + + # Stage 3 + x31 = self.do31(self.conv31(x2p)) + x32 = self.do32(self.conv32(x31)) + x33_2 = self.do33(self.conv33(x32)) + x3p = self.pool3(x33_2) + + # Stage 4 + x41 = self.do41(self.conv41(x3p)) + x42 = self.do42(self.conv42(x41)) + x43_2 = self.do43(self.conv43(x42)) + x4p = self.pool4(x43_2) + + # Attend + x43_1 = self.att_c(x43_1) * x43_1 + x43_1 = self.att_s(x43_1) * x43_1 + x43_2 = self.att_c(x43_2) * x43_2 + x43_2 = self.att_s(x43_2) * x43_2 + x43 = paddle.stack([x43_1, x43_2], axis=1) + x43 = paddle.transpose(x43, [0, 2, 1, 3, 4]) + x43 = paddle.flatten(x43, stop_axis=1) + x43 = self.att_t(x43) * x43 + x43 = x43.reshape((x43_1.shape[0], -1, 2, *x43.shape[2:])) + x43_1, x43_2 = x43[:, :, 0], x43[:, :, 1] + + # Decode + # Stage 4d + x4d = self.upconv4(x4p) + pad4 = (0, x43_1.shape[3] - x4d.shape[3], 0, + x43_1.shape[2] - x4d.shape[2]) + x4d = F.pad(x4d, pad=pad4, mode='replicate') + x4d = paddle.concat([x4d, paddle.abs(x43_1 - x43_2)], 1) + x43d = self.do43d(self.conv43d(x4d)) + x42d = self.do42d(self.conv42d(x43d)) + x41d = self.do41d(self.conv41d(x42d)) + + # Stage 3d + x3d = self.upconv3(x41d) + pad3 = (0, x33_1.shape[3] - x3d.shape[3], 0, + x33_1.shape[2] - x3d.shape[2]) + x3d = F.pad(x3d, pad=pad3, mode='replicate') + x3d = paddle.concat([x3d, paddle.abs(x33_1 - x33_2)], 1) + x33d = self.do33d(self.conv33d(x3d)) + x32d = self.do32d(self.conv32d(x33d)) + x31d = self.do31d(self.conv31d(x32d)) + + # Stage 2d + x2d = self.upconv2(x31d) + pad2 = (0, x22_1.shape[3] - x2d.shape[3], 0, + x22_1.shape[2] - x2d.shape[2]) + x2d = F.pad(x2d, pad=pad2, mode='replicate') + x2d = paddle.concat([x2d, paddle.abs(x22_1 - x22_2)], 1) + x22d = self.do22d(self.conv22d(x2d)) + x21d = self.do21d(self.conv21d(x22d)) + + # Stage 1d + x1d = self.upconv1(x21d) + pad1 = (0, x12_1.shape[3] - x1d.shape[3], 0, + x12_1.shape[2] - x1d.shape[2]) + x1d = F.pad(x1d, pad=pad1, mode='replicate') + x1d = paddle.concat([x1d, paddle.abs(x12_1 - x12_2)], 1) + x12d = self.do12d(self.conv12d(x1d)) + x11d = self.conv11d(x12d) + + return [x11d] + + def init_weight(self): + pass + + def _make_dropout(self): + if self.use_dropout: + return nn.Dropout2D(p=0.2) else: - token1 = self._get_reshaped_tokens(x1) - token2 = self._get_reshaped_tokens(x2) - - # Transformer encoder forward - token = paddle.concat([token1, token2], axis=1) - token = self.encode(token) - token1, token2 = paddle.chunk(token, 2, axis=1) - - # Get initial rate map - rate_map = self._init_rate_map(x1.shape) - - for it in range(self.num_iters): - # Construct inputs - x1_iter = self._constr_iter_input(x1, rate_map) - x2_iter = self._constr_iter_input(x2, rate_map) - - # Transformer decoder forward - y1 = self.decode(x1_iter, token1) - y2 = self.decode(x2_iter, token2) - - # Feature differencing - y = paddle.abs(y1 - y2) - - # Construct rate map - rate_map = self._constr_rate_map(y) - - y = self.upsample(y) - pred = self.conv_out(y) - - return [pred] - - def _init_rate_map(self, im_shape): - b, _, h, w = im_shape - return paddle.full((b, 1, h, w), 0.5) - - def _constr_iter_input(self, x, rate_map): - return self.conv_fuse(paddle.concat([x, rate_map], axis=1)) - - def _constr_rate_map(self, x): - rate_map = x.mean(1, keepdim=True).detach() # Cut off gradient workflow - # min-max normalization - rate_map -= rate_map.min() - rate_map /= rate_map.max() - return rate_map + return Identity() diff --git a/examples/rs_research/custom_trainer.py b/examples/rs_research/custom_trainer.py index 18e0d20..b3ddb41 100644 --- a/examples/rs_research/custom_trainer.py +++ b/examples/rs_research/custom_trainer.py @@ -7,22 +7,22 @@ attach = Attach.to(paddlers.tasks.change_detector) @attach -class IterativeBIT(BaseChangeDetector): +class CustomTrainer(BaseChangeDetector): def __init__(self, num_classes=2, use_mixed_loss=False, losses=None, - num_iters=1, - feat_channels=32, - bit_kwargs=None, + in_channels=3, + att_types='cst', + use_dropout=False, **params): params.update({ - 'num_iters': num_iters, - 'feat_channels': feat_channels, - 'bit_kwargs': bit_kwargs + 'in_channels': in_channels, + 'att_types': att_types, + 'use_dropout': use_dropout }) super().__init__( - model_name='IterativeBIT', + model_name='CustomModel', num_classes=num_classes, use_mixed_loss=use_mixed_loss, losses=losses, From 5a24513136dc06310c17a12310caa31e7ab26647 Mon Sep 17 00:00:00 2001 From: Bobholamovic Date: Mon, 22 Aug 2022 16:19:20 +0800 Subject: [PATCH 20/52] Refactor run_task.py --- examples/rs_research/run_task.py | 32 ++++++++++++++------------------ test_tipc/run_task.py | 32 ++++++++++++++------------------ 2 files changed, 28 insertions(+), 36 deletions(-) diff --git a/examples/rs_research/run_task.py b/examples/rs_research/run_task.py index b4b90bc..f3f1c54 100644 --- a/examples/rs_research/run_task.py +++ b/examples/rs_research/run_task.py @@ -53,6 +53,17 @@ if __name__ == '__main__': paddlers.utils.download_and_decompress( cfg['download_url'], path=cfg['download_path']) + if not isinstance(cfg['datasets']['eval'].args, dict): + raise ValueError("args of eval dataset must be a dict!") + if cfg['datasets']['eval'].args.get('transforms', None) is not None: + raise ValueError( + "Found key 'transforms' in args of eval dataset and the value is not None." + ) + eval_transforms = T.Compose(build_objects(cfg['transforms']['eval'], mod=T)) + # Inplace modification + cfg['datasets']['eval'].args['transforms'] = eval_transforms + eval_dataset = build_objects(cfg['datasets']['eval'], mod=paddlers.datasets) + if cfg['cmd'] == 'train': if not isinstance(cfg['datasets']['train'].args, dict): raise ValueError("args of train dataset must be a dict!") @@ -67,21 +78,8 @@ if __name__ == '__main__': cfg['datasets']['train'].args['transforms'] = train_transforms train_dataset = build_objects( cfg['datasets']['train'], mod=paddlers.datasets) - if not isinstance(cfg['datasets']['eval'].args, dict): - raise ValueError("args of eval dataset must be a dict!") - if cfg['datasets']['eval'].args.get('transforms', None) is not None: - raise ValueError( - "Found key 'transforms' in args of eval dataset and the value is not None." - ) - eval_transforms = T.Compose(build_objects(cfg['transforms']['eval'], mod=T)) - # Inplace modification - cfg['datasets']['eval'].args['transforms'] = eval_transforms - eval_dataset = build_objects(cfg['datasets']['eval'], mod=paddlers.datasets) - - model = build_objects( - cfg['model'], mod=getattr(paddlers.tasks, cfg['task'])) - - if cfg['cmd'] == 'train': + model = build_objects( + cfg['model'], mod=getattr(paddlers.tasks, cfg['task'])) if cfg['optimizer']: if len(cfg['optimizer'].args) == 0: cfg['optimizer'].args = {} @@ -112,8 +110,6 @@ if __name__ == '__main__': resume_checkpoint=cfg['resume_checkpoint'] or None, **cfg['train']) elif cfg['cmd'] == 'eval': - state_dict = paddle.load( - os.path.join(cfg['resume_checkpoint'], 'model.pdparams')) - model.net.set_state_dict(state_dict) + model = paddlers.tasks.load_model(cfg['resume_checkpoint']) res = model.evaluate(eval_dataset) print(res) diff --git a/test_tipc/run_task.py b/test_tipc/run_task.py index 88378f8..923415b 100644 --- a/test_tipc/run_task.py +++ b/test_tipc/run_task.py @@ -51,6 +51,17 @@ if __name__ == '__main__': paddlers.utils.download_and_decompress( cfg['download_url'], path=cfg['download_path']) + if not isinstance(cfg['datasets']['eval'].args, dict): + raise ValueError("args of eval dataset must be a dict!") + if cfg['datasets']['eval'].args.get('transforms', None) is not None: + raise ValueError( + "Found key 'transforms' in args of eval dataset and the value is not None." + ) + eval_transforms = T.Compose(build_objects(cfg['transforms']['eval'], mod=T)) + # Inplace modification + cfg['datasets']['eval'].args['transforms'] = eval_transforms + eval_dataset = build_objects(cfg['datasets']['eval'], mod=paddlers.datasets) + if cfg['cmd'] == 'train': if not isinstance(cfg['datasets']['train'].args, dict): raise ValueError("args of train dataset must be a dict!") @@ -65,21 +76,8 @@ if __name__ == '__main__': cfg['datasets']['train'].args['transforms'] = train_transforms train_dataset = build_objects( cfg['datasets']['train'], mod=paddlers.datasets) - if not isinstance(cfg['datasets']['eval'].args, dict): - raise ValueError("args of eval dataset must be a dict!") - if cfg['datasets']['eval'].args.get('transforms', None) is not None: - raise ValueError( - "Found key 'transforms' in args of eval dataset and the value is not None." - ) - eval_transforms = T.Compose(build_objects(cfg['transforms']['eval'], mod=T)) - # Inplace modification - cfg['datasets']['eval'].args['transforms'] = eval_transforms - eval_dataset = build_objects(cfg['datasets']['eval'], mod=paddlers.datasets) - - model = build_objects( - cfg['model'], mod=getattr(paddlers.tasks, cfg['task'])) - - if cfg['cmd'] == 'train': + model = build_objects( + cfg['model'], mod=getattr(paddlers.tasks, cfg['task'])) if cfg['optimizer']: if len(cfg['optimizer'].args) == 0: cfg['optimizer'].args = {} @@ -110,8 +108,6 @@ if __name__ == '__main__': resume_checkpoint=cfg['resume_checkpoint'] or None, **cfg['train']) elif cfg['cmd'] == 'eval': - state_dict = paddle.load( - os.path.join(cfg['resume_checkpoint'], 'model.pdparams')) - model.net.set_state_dict(state_dict) + model = paddlers.tasks.load_model(cfg['resume_checkpoint']) res = model.evaluate(eval_dataset) print(res) From 0f06d5d1cec46960fe37ac3f4c09b2af9c778c78 Mon Sep 17 00:00:00 2001 From: Bobholamovic Date: Mon, 22 Aug 2022 16:22:16 +0800 Subject: [PATCH 21/52] Add TIPC whole_train_whole_infer --- paddlers/tasks/change_detector.py | 4 +- test_tipc/configs/cd/bit/bit.yaml | 8 --- test_tipc/configs/cd/bit/bit_airchange.yaml | 2 +- test_tipc/configs/cd/bit/bit_levircd.yaml | 2 +- .../configs/cd/bit/train_infer_python.txt | 2 +- .../configs/cd/cdnet/cdnet_airchange.yaml | 8 +++ test_tipc/configs/cd/cdnet/cdnet_levircd.yaml | 8 +++ .../configs/cd/cdnet/train_infer_python.txt | 53 +++++++++++++++++++ ...ormer.yaml => changeformer_airchange.yaml} | 2 +- .../cd/changeformer/changeformer_levircd.yaml | 8 +++ .../cd/changeformer/train_infer_python.txt | 10 ++-- .../configs/cd/dsamnet/dsamnet_airchange.yaml | 8 +++ .../configs/cd/dsamnet/dsamnet_levircd.yaml | 8 +++ .../configs/cd/dsamnet/train_infer_python.txt | 53 +++++++++++++++++++ .../configs/cd/dsifn/dsifn_airchange.yaml | 8 +++ test_tipc/configs/cd/dsifn/dsifn_levircd.yaml | 8 +++ .../configs/cd/dsifn/train_infer_python.txt | 53 +++++++++++++++++++ .../configs/cd/fc_ef/fc_ef_airchange.yaml | 8 +++ test_tipc/configs/cd/fc_ef/fc_ef_levircd.yaml | 8 +++ .../configs/cd/fc_ef/train_infer_python.txt | 53 +++++++++++++++++++ .../fc_siam_conc/fc_siam_conc_airchange.yaml | 8 +++ .../cd/fc_siam_conc/fc_siam_conc_levircd.yaml | 8 +++ .../cd/fc_siam_conc/train_infer_python.txt | 53 +++++++++++++++++++ .../fc_siam_diff/fc_siam_diff_airchange.yaml | 8 +++ .../cd/fc_siam_diff/fc_siam_diff_levircd.yaml | 8 +++ .../cd/fc_siam_diff/train_infer_python.txt | 53 +++++++++++++++++++ .../configs/cd/snunet/snunet_airchange.yaml | 8 +++ .../configs/cd/snunet/snunet_levircd.yaml | 8 +++ .../configs/cd/snunet/train_infer_python.txt | 53 +++++++++++++++++++ .../configs/cd/stanet/stanet_airchange.yaml | 8 +++ .../configs/cd/stanet/stanet_levircd.yaml | 8 +++ .../configs/cd/stanet/train_infer_python.txt | 53 +++++++++++++++++++ .../hrnet/{hrnet.yaml => hrnet_ucmerced.yaml} | 2 +- .../configs/clas/hrnet/train_infer_python.txt | 6 +-- test_tipc/infer.py | 12 ++++- test_tipc/prepare.sh | 2 + tutorials/train/README.md | 4 +- 37 files changed, 588 insertions(+), 28 deletions(-) delete mode 100644 test_tipc/configs/cd/bit/bit.yaml create mode 100644 test_tipc/configs/cd/cdnet/cdnet_airchange.yaml create mode 100644 test_tipc/configs/cd/cdnet/cdnet_levircd.yaml create mode 100644 test_tipc/configs/cd/cdnet/train_infer_python.txt rename test_tipc/configs/cd/changeformer/{changeformer.yaml => changeformer_airchange.yaml} (54%) create mode 100644 test_tipc/configs/cd/changeformer/changeformer_levircd.yaml create mode 100644 test_tipc/configs/cd/dsamnet/dsamnet_airchange.yaml create mode 100644 test_tipc/configs/cd/dsamnet/dsamnet_levircd.yaml create mode 100644 test_tipc/configs/cd/dsamnet/train_infer_python.txt create mode 100644 test_tipc/configs/cd/dsifn/dsifn_airchange.yaml create mode 100644 test_tipc/configs/cd/dsifn/dsifn_levircd.yaml create mode 100644 test_tipc/configs/cd/dsifn/train_infer_python.txt create mode 100644 test_tipc/configs/cd/fc_ef/fc_ef_airchange.yaml create mode 100644 test_tipc/configs/cd/fc_ef/fc_ef_levircd.yaml create mode 100644 test_tipc/configs/cd/fc_ef/train_infer_python.txt create mode 100644 test_tipc/configs/cd/fc_siam_conc/fc_siam_conc_airchange.yaml create mode 100644 test_tipc/configs/cd/fc_siam_conc/fc_siam_conc_levircd.yaml create mode 100644 test_tipc/configs/cd/fc_siam_conc/train_infer_python.txt create mode 100644 test_tipc/configs/cd/fc_siam_diff/fc_siam_diff_airchange.yaml create mode 100644 test_tipc/configs/cd/fc_siam_diff/fc_siam_diff_levircd.yaml create mode 100644 test_tipc/configs/cd/fc_siam_diff/train_infer_python.txt create mode 100644 test_tipc/configs/cd/snunet/snunet_airchange.yaml create mode 100644 test_tipc/configs/cd/snunet/snunet_levircd.yaml create mode 100644 test_tipc/configs/cd/snunet/train_infer_python.txt create mode 100644 test_tipc/configs/cd/stanet/stanet_airchange.yaml create mode 100644 test_tipc/configs/cd/stanet/stanet_levircd.yaml create mode 100644 test_tipc/configs/cd/stanet/train_infer_python.txt rename test_tipc/configs/clas/hrnet/{hrnet.yaml => hrnet_ucmerced.yaml} (62%) diff --git a/paddlers/tasks/change_detector.py b/paddlers/tasks/change_detector.py index b3fa32c..8718cd1 100644 --- a/paddlers/tasks/change_detector.py +++ b/paddlers/tasks/change_detector.py @@ -52,9 +52,7 @@ class BaseChangeDetector(BaseModel): if 'with_net' in self.init_params: del self.init_params['with_net'] super(BaseChangeDetector, self).__init__('change_detector') - if model_name not in __all__: - raise ValueError("ERROR: There is no model named {}.".format( - model_name)) + self.model_name = model_name self.num_classes = num_classes self.use_mixed_loss = use_mixed_loss diff --git a/test_tipc/configs/cd/bit/bit.yaml b/test_tipc/configs/cd/bit/bit.yaml deleted file mode 100644 index 3d3c62b..0000000 --- a/test_tipc/configs/cd/bit/bit.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# Basic configurations of BIT - -_base_: ../_base_/airchange.yaml - -save_dir: ./test_tipc/output/cd/bit/ - -model: !Node - type: BIT \ No newline at end of file diff --git a/test_tipc/configs/cd/bit/bit_airchange.yaml b/test_tipc/configs/cd/bit/bit_airchange.yaml index efd6fbb..27e0bb4 100644 --- a/test_tipc/configs/cd/bit/bit_airchange.yaml +++ b/test_tipc/configs/cd/bit/bit_airchange.yaml @@ -1,4 +1,4 @@ -# Basic configurations of BIT with AirChange dataset +# Configurations of BIT with AirChange dataset _base_: ../_base_/airchange.yaml diff --git a/test_tipc/configs/cd/bit/bit_levircd.yaml b/test_tipc/configs/cd/bit/bit_levircd.yaml index 8008901..d9a5dd9 100644 --- a/test_tipc/configs/cd/bit/bit_levircd.yaml +++ b/test_tipc/configs/cd/bit/bit_levircd.yaml @@ -1,4 +1,4 @@ -# Basic configurations of BIT with LEVIR-CD dataset +# Configurations of BIT with LEVIR-CD dataset _base_: ../_base_/levircd.yaml diff --git a/test_tipc/configs/cd/bit/train_infer_python.txt b/test_tipc/configs/cd/bit/train_infer_python.txt index 33ee2f3..3cd2de1 100644 --- a/test_tipc/configs/cd/bit/train_infer_python.txt +++ b/test_tipc/configs/cd/bit/train_infer_python.txt @@ -6,7 +6,7 @@ use_gpu:null|null --precision:null --num_epochs:lite_train_lite_infer=5|lite_train_whole_infer=5|whole_train_whole_infer=10 --save_dir:adaptive ---train_batch_size:lite_train_lite_infer=4|lite_train_whole_infer=4|whole_train_whole_infer=4 +--train_batch_size:lite_train_lite_infer=4|lite_train_whole_infer=4|whole_train_whole_infer=8 --model_path:null --config:lite_train_lite_infer=./test_tipc/configs/cd/bit/bit_airchange.yaml|lite_train_whole_infer=./test_tipc/configs/cd/bit/bit_airchange.yaml|whole_train_whole_infer=./test_tipc/configs/cd/bit/bit_levircd.yaml train_model_name:best_model diff --git a/test_tipc/configs/cd/cdnet/cdnet_airchange.yaml b/test_tipc/configs/cd/cdnet/cdnet_airchange.yaml new file mode 100644 index 0000000..28d3f7a --- /dev/null +++ b/test_tipc/configs/cd/cdnet/cdnet_airchange.yaml @@ -0,0 +1,8 @@ +# Configurations of CDNet with AirChange dataset + +_base_: ../_base_/airchange.yaml + +save_dir: ./test_tipc/output/cd/cdnet/ + +model: !Node + type: CDNet \ No newline at end of file diff --git a/test_tipc/configs/cd/cdnet/cdnet_levircd.yaml b/test_tipc/configs/cd/cdnet/cdnet_levircd.yaml new file mode 100644 index 0000000..586e4e3 --- /dev/null +++ b/test_tipc/configs/cd/cdnet/cdnet_levircd.yaml @@ -0,0 +1,8 @@ +# Configurations of cdnet with LEVIR-CD dataset + +_base_: ../_base_/levircd.yaml + +save_dir: ./test_tipc/output/cd/cdnet/ + +model: !Node + type: CDNet \ No newline at end of file diff --git a/test_tipc/configs/cd/cdnet/train_infer_python.txt b/test_tipc/configs/cd/cdnet/train_infer_python.txt new file mode 100644 index 0000000..00ff523 --- /dev/null +++ b/test_tipc/configs/cd/cdnet/train_infer_python.txt @@ -0,0 +1,53 @@ +===========================train_params=========================== +model_name:cd:cdnet +python:python +gpu_list:0|0,1 +use_gpu:null|null +--precision:null +--num_epochs:lite_train_lite_infer=5|lite_train_whole_infer=5|whole_train_whole_infer=10 +--save_dir:adaptive +--train_batch_size:lite_train_lite_infer=4|lite_train_whole_infer=4|whole_train_whole_infer=8 +--model_path:null +--config:lite_train_lite_infer=./test_tipc/configs/cd/cdnet/cdnet_airchange.yaml|lite_train_whole_infer=./test_tipc/configs/cd/cdnet/cdnet_airchange.yaml|whole_train_whole_infer=./test_tipc/configs/cd/cdnet/cdnet_levircd.yaml +train_model_name:best_model +null:null +## +trainer:norm +norm_train:test_tipc/run_task.py train cd +pact_train:null +fpgm_train:null +distill_train:null +null:null +null:null +## +===========================eval_params=========================== +eval:null +null:null +## +===========================export_params=========================== +--save_dir:adaptive +--model_dir:adaptive +--fixed_input_shape:[-1,3,256,256] +norm_export:deploy/export/export_model.py +quant_export:null +fpgm_export:null +distill_export:null +export1:null +export2:null +===========================infer_params=========================== +infer_model:null +infer_export:null +infer_quant:False +inference:test_tipc/infer.py +--device:cpu|gpu +--enable_mkldnn:True +--cpu_threads:6 +--batch_size:1 +--use_trt:False +--precision:fp32 +--model_dir:null +--config:null +--save_log_path:null +--benchmark:True +--model_name:cdnet +null:null \ No newline at end of file diff --git a/test_tipc/configs/cd/changeformer/changeformer.yaml b/test_tipc/configs/cd/changeformer/changeformer_airchange.yaml similarity index 54% rename from test_tipc/configs/cd/changeformer/changeformer.yaml rename to test_tipc/configs/cd/changeformer/changeformer_airchange.yaml index 785749d..15a37ea 100644 --- a/test_tipc/configs/cd/changeformer/changeformer.yaml +++ b/test_tipc/configs/cd/changeformer/changeformer_airchange.yaml @@ -1,4 +1,4 @@ -# Basic configurations of ChangeFormer +# Configurations of ChangeFormer with AirChange dataset _base_: ../_base_/airchange.yaml diff --git a/test_tipc/configs/cd/changeformer/changeformer_levircd.yaml b/test_tipc/configs/cd/changeformer/changeformer_levircd.yaml new file mode 100644 index 0000000..931a3e8 --- /dev/null +++ b/test_tipc/configs/cd/changeformer/changeformer_levircd.yaml @@ -0,0 +1,8 @@ +# Configurations of ChangeFormer with LEVIR-CD dataset + +_base_: ../_base_/levircd.yaml + +save_dir: ./test_tipc/output/cd/changeformer/ + +model: !Node + type: ChangeFormer \ No newline at end of file diff --git a/test_tipc/configs/cd/changeformer/train_infer_python.txt b/test_tipc/configs/cd/changeformer/train_infer_python.txt index 9ac2cdc..47fe600 100644 --- a/test_tipc/configs/cd/changeformer/train_infer_python.txt +++ b/test_tipc/configs/cd/changeformer/train_infer_python.txt @@ -6,14 +6,14 @@ use_gpu:null|null --precision:null --num_epochs:lite_train_lite_infer=5|lite_train_whole_infer=5|whole_train_whole_infer=10 --save_dir:adaptive ---train_batch_size:lite_train_lite_infer=4|lite_train_whole_infer=4|whole_train_whole_infer=4 +--train_batch_size:lite_train_lite_infer=4|lite_train_whole_infer=4|whole_train_whole_infer=8 --model_path:null +--config:lite_train_lite_infer=./test_tipc/configs/cd/changeformer/changeformer_airchange.yaml|lite_train_whole_infer=./test_tipc/configs/cd/changeformer/changeformer_airchange.yaml|whole_train_whole_infer=./test_tipc/configs/cd/changeformer/changeformer_levircd.yaml train_model_name:best_model -train_infer_file_list:./test_tipc/data/airchange/:./test_tipc/data/airchange/eval.txt null:null ## trainer:norm -norm_train:test_tipc/run_task.py train cd --config ./test_tipc/configs/cd/changeformer/changeformer.yaml +norm_train:test_tipc/run_task.py train cd pact_train:null fpgm_train:null distill_train:null @@ -27,7 +27,7 @@ null:null ===========================export_params=========================== --save_dir:adaptive --model_dir:adaptive ---fixed_input_shape:[1,3,256,256] +--fixed_input_shape:[-1,3,256,256] norm_export:deploy/export/export_model.py quant_export:null fpgm_export:null @@ -46,7 +46,7 @@ inference:test_tipc/infer.py --use_trt:False --precision:fp32 --model_dir:null ---file_list:null:null +--config:null --save_log_path:null --benchmark:True --model_name:changeformer diff --git a/test_tipc/configs/cd/dsamnet/dsamnet_airchange.yaml b/test_tipc/configs/cd/dsamnet/dsamnet_airchange.yaml new file mode 100644 index 0000000..1ede33f --- /dev/null +++ b/test_tipc/configs/cd/dsamnet/dsamnet_airchange.yaml @@ -0,0 +1,8 @@ +# Configurations of DSAMNet with AirChange dataset + +_base_: ../_base_/airchange.yaml + +save_dir: ./test_tipc/output/cd/dsamnet/ + +model: !Node + type: DSAMNet \ No newline at end of file diff --git a/test_tipc/configs/cd/dsamnet/dsamnet_levircd.yaml b/test_tipc/configs/cd/dsamnet/dsamnet_levircd.yaml new file mode 100644 index 0000000..0fa9900 --- /dev/null +++ b/test_tipc/configs/cd/dsamnet/dsamnet_levircd.yaml @@ -0,0 +1,8 @@ +# Configurations of DSAMNet with LEVIR-CD dataset + +_base_: ../_base_/levircd.yaml + +save_dir: ./test_tipc/output/cd/dsamnet/ + +model: !Node + type: DSAMNet \ No newline at end of file diff --git a/test_tipc/configs/cd/dsamnet/train_infer_python.txt b/test_tipc/configs/cd/dsamnet/train_infer_python.txt new file mode 100644 index 0000000..bce8cab --- /dev/null +++ b/test_tipc/configs/cd/dsamnet/train_infer_python.txt @@ -0,0 +1,53 @@ +===========================train_params=========================== +model_name:cd:dsamnet +python:python +gpu_list:0|0,1 +use_gpu:null|null +--precision:null +--num_epochs:lite_train_lite_infer=5|lite_train_whole_infer=5|whole_train_whole_infer=10 +--save_dir:adaptive +--train_batch_size:lite_train_lite_infer=4|lite_train_whole_infer=4|whole_train_whole_infer=8 +--model_path:null +--config:lite_train_lite_infer=./test_tipc/configs/cd/dsamnet/dsamnet_airchange.yaml|lite_train_whole_infer=./test_tipc/configs/cd/dsamnet/dsamnet_airchange.yaml|whole_train_whole_infer=./test_tipc/configs/cd/dsamnet/dsamnet_levircd.yaml +train_model_name:best_model +null:null +## +trainer:norm +norm_train:test_tipc/run_task.py train cd +pact_train:null +fpgm_train:null +distill_train:null +null:null +null:null +## +===========================eval_params=========================== +eval:null +null:null +## +===========================export_params=========================== +--save_dir:adaptive +--model_dir:adaptive +--fixed_input_shape:[-1,3,256,256] +norm_export:deploy/export/export_model.py +quant_export:null +fpgm_export:null +distill_export:null +export1:null +export2:null +===========================infer_params=========================== +infer_model:null +infer_export:null +infer_quant:False +inference:test_tipc/infer.py +--device:cpu|gpu +--enable_mkldnn:True +--cpu_threads:6 +--batch_size:1 +--use_trt:False +--precision:fp32 +--model_dir:null +--config:null +--save_log_path:null +--benchmark:True +--model_name:dsamnet +null:null \ No newline at end of file diff --git a/test_tipc/configs/cd/dsifn/dsifn_airchange.yaml b/test_tipc/configs/cd/dsifn/dsifn_airchange.yaml new file mode 100644 index 0000000..7fc661a --- /dev/null +++ b/test_tipc/configs/cd/dsifn/dsifn_airchange.yaml @@ -0,0 +1,8 @@ +# Configurations of DSIFN with AirChange dataset + +_base_: ../_base_/airchange.yaml + +save_dir: ./test_tipc/output/cd/dsifn/ + +model: !Node + type: DSIFN \ No newline at end of file diff --git a/test_tipc/configs/cd/dsifn/dsifn_levircd.yaml b/test_tipc/configs/cd/dsifn/dsifn_levircd.yaml new file mode 100644 index 0000000..c4454a1 --- /dev/null +++ b/test_tipc/configs/cd/dsifn/dsifn_levircd.yaml @@ -0,0 +1,8 @@ +# Configurations of DSIFN with LEVIR-CD dataset + +_base_: ../_base_/levircd.yaml + +save_dir: ./test_tipc/output/cd/dsifn/ + +model: !Node + type: DSIFN \ No newline at end of file diff --git a/test_tipc/configs/cd/dsifn/train_infer_python.txt b/test_tipc/configs/cd/dsifn/train_infer_python.txt new file mode 100644 index 0000000..e491797 --- /dev/null +++ b/test_tipc/configs/cd/dsifn/train_infer_python.txt @@ -0,0 +1,53 @@ +===========================train_params=========================== +model_name:cd:dsifn +python:python +gpu_list:0|0,1 +use_gpu:null|null +--precision:null +--num_epochs:lite_train_lite_infer=5|lite_train_whole_infer=5|whole_train_whole_infer=10 +--save_dir:adaptive +--train_batch_size:lite_train_lite_infer=4|lite_train_whole_infer=4|whole_train_whole_infer=8 +--model_path:null +--config:lite_train_lite_infer=./test_tipc/configs/cd/dsifn/dsifn_airchange.yaml|lite_train_whole_infer=./test_tipc/configs/cd/dsifn/dsifn_airchange.yaml|whole_train_whole_infer=./test_tipc/configs/cd/dsifn/dsifn_levircd.yaml +train_model_name:best_model +null:null +## +trainer:norm +norm_train:test_tipc/run_task.py train cd +pact_train:null +fpgm_train:null +distill_train:null +null:null +null:null +## +===========================eval_params=========================== +eval:null +null:null +## +===========================export_params=========================== +--save_dir:adaptive +--model_dir:adaptive +--fixed_input_shape:[-1,3,256,256] +norm_export:deploy/export/export_model.py +quant_export:null +fpgm_export:null +distill_export:null +export1:null +export2:null +===========================infer_params=========================== +infer_model:null +infer_export:null +infer_quant:False +inference:test_tipc/infer.py +--device:cpu|gpu +--enable_mkldnn:True +--cpu_threads:6 +--batch_size:1 +--use_trt:False +--precision:fp32 +--model_dir:null +--config:null +--save_log_path:null +--benchmark:True +--model_name:dsifn +null:null \ No newline at end of file diff --git a/test_tipc/configs/cd/fc_ef/fc_ef_airchange.yaml b/test_tipc/configs/cd/fc_ef/fc_ef_airchange.yaml new file mode 100644 index 0000000..fc47737 --- /dev/null +++ b/test_tipc/configs/cd/fc_ef/fc_ef_airchange.yaml @@ -0,0 +1,8 @@ +# Configurations of FC-EF with AirChange dataset + +_base_: ../_base_/airchange.yaml + +save_dir: ./test_tipc/output/cd/fc_ef/ + +model: !Node + type: FCEarlyFusion \ No newline at end of file diff --git a/test_tipc/configs/cd/fc_ef/fc_ef_levircd.yaml b/test_tipc/configs/cd/fc_ef/fc_ef_levircd.yaml new file mode 100644 index 0000000..758d4a0 --- /dev/null +++ b/test_tipc/configs/cd/fc_ef/fc_ef_levircd.yaml @@ -0,0 +1,8 @@ +# Configurations of FC-EF with LEVIR-CD dataset + +_base_: ../_base_/levircd.yaml + +save_dir: ./test_tipc/output/cd/fc_ef/ + +model: !Node + type: FCEarlyFusion \ No newline at end of file diff --git a/test_tipc/configs/cd/fc_ef/train_infer_python.txt b/test_tipc/configs/cd/fc_ef/train_infer_python.txt new file mode 100644 index 0000000..fec5049 --- /dev/null +++ b/test_tipc/configs/cd/fc_ef/train_infer_python.txt @@ -0,0 +1,53 @@ +===========================train_params=========================== +model_name:cd:fc_ef +python:python +gpu_list:0|0,1 +use_gpu:null|null +--precision:null +--num_epochs:lite_train_lite_infer=5|lite_train_whole_infer=5|whole_train_whole_infer=10 +--save_dir:adaptive +--train_batch_size:lite_train_lite_infer=4|lite_train_whole_infer=4|whole_train_whole_infer=8 +--model_path:null +--config:lite_train_lite_infer=./test_tipc/configs/cd/fc_ef/fc_ef_airchange.yaml|lite_train_whole_infer=./test_tipc/configs/cd/fc_ef/fc_ef_airchange.yaml|whole_train_whole_infer=./test_tipc/configs/cd/fc_ef/fc_ef_levircd.yaml +train_model_name:best_model +null:null +## +trainer:norm +norm_train:test_tipc/run_task.py train cd +pact_train:null +fpgm_train:null +distill_train:null +null:null +null:null +## +===========================eval_params=========================== +eval:null +null:null +## +===========================export_params=========================== +--save_dir:adaptive +--model_dir:adaptive +--fixed_input_shape:[-1,3,256,256] +norm_export:deploy/export/export_model.py +quant_export:null +fpgm_export:null +distill_export:null +export1:null +export2:null +===========================infer_params=========================== +infer_model:null +infer_export:null +infer_quant:False +inference:test_tipc/infer.py +--device:cpu|gpu +--enable_mkldnn:True +--cpu_threads:6 +--batch_size:1 +--use_trt:False +--precision:fp32 +--model_dir:null +--config:null +--save_log_path:null +--benchmark:True +--model_name:fc_ef +null:null \ No newline at end of file diff --git a/test_tipc/configs/cd/fc_siam_conc/fc_siam_conc_airchange.yaml b/test_tipc/configs/cd/fc_siam_conc/fc_siam_conc_airchange.yaml new file mode 100644 index 0000000..f4a8111 --- /dev/null +++ b/test_tipc/configs/cd/fc_siam_conc/fc_siam_conc_airchange.yaml @@ -0,0 +1,8 @@ +# Configurations of FC-Siam-conc with AirChange dataset + +_base_: ../_base_/airchange.yaml + +save_dir: ./test_tipc/output/cd/fc_siam_conc/ + +model: !Node + type: FCSiamConc \ No newline at end of file diff --git a/test_tipc/configs/cd/fc_siam_conc/fc_siam_conc_levircd.yaml b/test_tipc/configs/cd/fc_siam_conc/fc_siam_conc_levircd.yaml new file mode 100644 index 0000000..1d49a5d --- /dev/null +++ b/test_tipc/configs/cd/fc_siam_conc/fc_siam_conc_levircd.yaml @@ -0,0 +1,8 @@ +# Configurations of FC-Siam-conc with LEVIR-CD dataset + +_base_: ../_base_/levircd.yaml + +save_dir: ./test_tipc/output/cd/fc_siam_conc/ + +model: !Node + type: FCSiamConc \ No newline at end of file diff --git a/test_tipc/configs/cd/fc_siam_conc/train_infer_python.txt b/test_tipc/configs/cd/fc_siam_conc/train_infer_python.txt new file mode 100644 index 0000000..47e9bdb --- /dev/null +++ b/test_tipc/configs/cd/fc_siam_conc/train_infer_python.txt @@ -0,0 +1,53 @@ +===========================train_params=========================== +model_name:cd:fc_siam_conc +python:python +gpu_list:0|0,1 +use_gpu:null|null +--precision:null +--num_epochs:lite_train_lite_infer=5|lite_train_whole_infer=5|whole_train_whole_infer=10 +--save_dir:adaptive +--train_batch_size:lite_train_lite_infer=4|lite_train_whole_infer=4|whole_train_whole_infer=8 +--model_path:null +--config:lite_train_lite_infer=./test_tipc/configs/cd/fc_siam_conc/fc_siam_conc_airchange.yaml|lite_train_whole_infer=./test_tipc/configs/cd/fc_siam_conc/fc_siam_conc_airchange.yaml|whole_train_whole_infer=./test_tipc/configs/cd/fc_siam_conc/fc_siam_conc_levircd.yaml +train_model_name:best_model +null:null +## +trainer:norm +norm_train:test_tipc/run_task.py train cd +pact_train:null +fpgm_train:null +distill_train:null +null:null +null:null +## +===========================eval_params=========================== +eval:null +null:null +## +===========================export_params=========================== +--save_dir:adaptive +--model_dir:adaptive +--fixed_input_shape:[-1,3,256,256] +norm_export:deploy/export/export_model.py +quant_export:null +fpgm_export:null +distill_export:null +export1:null +export2:null +===========================infer_params=========================== +infer_model:null +infer_export:null +infer_quant:False +inference:test_tipc/infer.py +--device:cpu|gpu +--enable_mkldnn:True +--cpu_threads:6 +--batch_size:1 +--use_trt:False +--precision:fp32 +--model_dir:null +--config:null +--save_log_path:null +--benchmark:True +--model_name:fc_siam_conc +null:null \ No newline at end of file diff --git a/test_tipc/configs/cd/fc_siam_diff/fc_siam_diff_airchange.yaml b/test_tipc/configs/cd/fc_siam_diff/fc_siam_diff_airchange.yaml new file mode 100644 index 0000000..3453d82 --- /dev/null +++ b/test_tipc/configs/cd/fc_siam_diff/fc_siam_diff_airchange.yaml @@ -0,0 +1,8 @@ +# Configurations of FC-Siam-diff with AirChange dataset + +_base_: ../_base_/airchange.yaml + +save_dir: ./test_tipc/output/cd/fc_siam_diff/ + +model: !Node + type: FCSiamDiff \ No newline at end of file diff --git a/test_tipc/configs/cd/fc_siam_diff/fc_siam_diff_levircd.yaml b/test_tipc/configs/cd/fc_siam_diff/fc_siam_diff_levircd.yaml new file mode 100644 index 0000000..2588cb9 --- /dev/null +++ b/test_tipc/configs/cd/fc_siam_diff/fc_siam_diff_levircd.yaml @@ -0,0 +1,8 @@ +# Configurations of FC-Siam-diff with LEVIR-CD dataset + +_base_: ../_base_/levircd.yaml + +save_dir: ./test_tipc/output/cd/fc_siam_diff/ + +model: !Node + type: FCSiamDiff \ No newline at end of file diff --git a/test_tipc/configs/cd/fc_siam_diff/train_infer_python.txt b/test_tipc/configs/cd/fc_siam_diff/train_infer_python.txt new file mode 100644 index 0000000..cba8b57 --- /dev/null +++ b/test_tipc/configs/cd/fc_siam_diff/train_infer_python.txt @@ -0,0 +1,53 @@ +===========================train_params=========================== +model_name:cd:fc_siam_diff +python:python +gpu_list:0|0,1 +use_gpu:null|null +--precision:null +--num_epochs:lite_train_lite_infer=5|lite_train_whole_infer=5|whole_train_whole_infer=10 +--save_dir:adaptive +--train_batch_size:lite_train_lite_infer=4|lite_train_whole_infer=4|whole_train_whole_infer=8 +--model_path:null +--config:lite_train_lite_infer=./test_tipc/configs/cd/fc_siam_diff/fc_siam_diff_airchange.yaml|lite_train_whole_infer=./test_tipc/configs/cd/fc_siam_diff/fc_siam_diff_airchange.yaml|whole_train_whole_infer=./test_tipc/configs/cd/fc_siam_diff/fc_siam_diff_levircd.yaml +train_model_name:best_model +null:null +## +trainer:norm +norm_train:test_tipc/run_task.py train cd +pact_train:null +fpgm_train:null +distill_train:null +null:null +null:null +## +===========================eval_params=========================== +eval:null +null:null +## +===========================export_params=========================== +--save_dir:adaptive +--model_dir:adaptive +--fixed_input_shape:[-1,3,256,256] +norm_export:deploy/export/export_model.py +quant_export:null +fpgm_export:null +distill_export:null +export1:null +export2:null +===========================infer_params=========================== +infer_model:null +infer_export:null +infer_quant:False +inference:test_tipc/infer.py +--device:cpu|gpu +--enable_mkldnn:True +--cpu_threads:6 +--batch_size:1 +--use_trt:False +--precision:fp32 +--model_dir:null +--config:null +--save_log_path:null +--benchmark:True +--model_name:fc_siam_diff +null:null \ No newline at end of file diff --git a/test_tipc/configs/cd/snunet/snunet_airchange.yaml b/test_tipc/configs/cd/snunet/snunet_airchange.yaml new file mode 100644 index 0000000..eee3b1d --- /dev/null +++ b/test_tipc/configs/cd/snunet/snunet_airchange.yaml @@ -0,0 +1,8 @@ +# Configurations of SNUNet with AirChange dataset + +_base_: ../_base_/airchange.yaml + +save_dir: ./test_tipc/output/cd/snunet/ + +model: !Node + type: SNUNet \ No newline at end of file diff --git a/test_tipc/configs/cd/snunet/snunet_levircd.yaml b/test_tipc/configs/cd/snunet/snunet_levircd.yaml new file mode 100644 index 0000000..7af3bcb --- /dev/null +++ b/test_tipc/configs/cd/snunet/snunet_levircd.yaml @@ -0,0 +1,8 @@ +# Configurations of SNUNet with LEVIR-CD dataset + +_base_: ../_base_/levircd.yaml + +save_dir: ./test_tipc/output/cd/snunet/ + +model: !Node + type: SNUNet \ No newline at end of file diff --git a/test_tipc/configs/cd/snunet/train_infer_python.txt b/test_tipc/configs/cd/snunet/train_infer_python.txt new file mode 100644 index 0000000..264ffd9 --- /dev/null +++ b/test_tipc/configs/cd/snunet/train_infer_python.txt @@ -0,0 +1,53 @@ +===========================train_params=========================== +model_name:cd:snunet +python:python +gpu_list:0|0,1 +use_gpu:null|null +--precision:null +--num_epochs:lite_train_lite_infer=5|lite_train_whole_infer=5|whole_train_whole_infer=10 +--save_dir:adaptive +--train_batch_size:lite_train_lite_infer=4|lite_train_whole_infer=4|whole_train_whole_infer=8 +--model_path:null +--config:lite_train_lite_infer=./test_tipc/configs/cd/snunet/snunet_airchange.yaml|lite_train_whole_infer=./test_tipc/configs/cd/snunet/snunet_airchange.yaml|whole_train_whole_infer=./test_tipc/configs/cd/snunet/snunet_levircd.yaml +train_model_name:best_model +null:null +## +trainer:norm +norm_train:test_tipc/run_task.py train cd +pact_train:null +fpgm_train:null +distill_train:null +null:null +null:null +## +===========================eval_params=========================== +eval:null +null:null +## +===========================export_params=========================== +--save_dir:adaptive +--model_dir:adaptive +--fixed_input_shape:[-1,3,256,256] +norm_export:deploy/export/export_model.py +quant_export:null +fpgm_export:null +distill_export:null +export1:null +export2:null +===========================infer_params=========================== +infer_model:null +infer_export:null +infer_quant:False +inference:test_tipc/infer.py +--device:cpu|gpu +--enable_mkldnn:True +--cpu_threads:6 +--batch_size:1 +--use_trt:False +--precision:fp32 +--model_dir:null +--config:null +--save_log_path:null +--benchmark:True +--model_name:snunet +null:null \ No newline at end of file diff --git a/test_tipc/configs/cd/stanet/stanet_airchange.yaml b/test_tipc/configs/cd/stanet/stanet_airchange.yaml new file mode 100644 index 0000000..7c7c05a --- /dev/null +++ b/test_tipc/configs/cd/stanet/stanet_airchange.yaml @@ -0,0 +1,8 @@ +# Configurations of STANet with AirChange dataset + +_base_: ../_base_/airchange.yaml + +save_dir: ./test_tipc/output/cd/stanet/ + +model: !Node + type: STANet \ No newline at end of file diff --git a/test_tipc/configs/cd/stanet/stanet_levircd.yaml b/test_tipc/configs/cd/stanet/stanet_levircd.yaml new file mode 100644 index 0000000..b439ff1 --- /dev/null +++ b/test_tipc/configs/cd/stanet/stanet_levircd.yaml @@ -0,0 +1,8 @@ +# Configurations of STANet with LEVIR-CD dataset + +_base_: ../_base_/levircd.yaml + +save_dir: ./test_tipc/output/cd/stanet/ + +model: !Node + type: STANet \ No newline at end of file diff --git a/test_tipc/configs/cd/stanet/train_infer_python.txt b/test_tipc/configs/cd/stanet/train_infer_python.txt new file mode 100644 index 0000000..0bff7df --- /dev/null +++ b/test_tipc/configs/cd/stanet/train_infer_python.txt @@ -0,0 +1,53 @@ +===========================train_params=========================== +model_name:cd:stanet +python:python +gpu_list:0|0,1 +use_gpu:null|null +--precision:null +--num_epochs:lite_train_lite_infer=5|lite_train_whole_infer=5|whole_train_whole_infer=10 +--save_dir:adaptive +--train_batch_size:lite_train_lite_infer=4|lite_train_whole_infer=4|whole_train_whole_infer=8 +--model_path:null +--config:lite_train_lite_infer=./test_tipc/configs/cd/stanet/stanet_airchange.yaml|lite_train_whole_infer=./test_tipc/configs/cd/stanet/stanet_airchange.yaml|whole_train_whole_infer=./test_tipc/configs/cd/stanet/stanet_levircd.yaml +train_model_name:best_model +null:null +## +trainer:norm +norm_train:test_tipc/run_task.py train cd +pact_train:null +fpgm_train:null +distill_train:null +null:null +null:null +## +===========================eval_params=========================== +eval:null +null:null +## +===========================export_params=========================== +--save_dir:adaptive +--model_dir:adaptive +--fixed_input_shape:[-1,3,256,256] +norm_export:deploy/export/export_model.py +quant_export:null +fpgm_export:null +distill_export:null +export1:null +export2:null +===========================infer_params=========================== +infer_model:null +infer_export:null +infer_quant:False +inference:test_tipc/infer.py +--device:cpu|gpu +--enable_mkldnn:True +--cpu_threads:6 +--batch_size:1 +--use_trt:False +--precision:fp32 +--model_dir:null +--config:null +--save_log_path:null +--benchmark:True +--model_name:stanet +null:null \ No newline at end of file diff --git a/test_tipc/configs/clas/hrnet/hrnet.yaml b/test_tipc/configs/clas/hrnet/hrnet_ucmerced.yaml similarity index 62% rename from test_tipc/configs/clas/hrnet/hrnet.yaml rename to test_tipc/configs/clas/hrnet/hrnet_ucmerced.yaml index f402c26..088e722 100644 --- a/test_tipc/configs/clas/hrnet/hrnet.yaml +++ b/test_tipc/configs/clas/hrnet/hrnet_ucmerced.yaml @@ -1,4 +1,4 @@ -# Basic configurations of HRNet +# Configurations of HRNet with UCMerced dataset _base_: ../_base_/ucmerced.yaml diff --git a/test_tipc/configs/clas/hrnet/train_infer_python.txt b/test_tipc/configs/clas/hrnet/train_infer_python.txt index 23f3820..1116c77 100644 --- a/test_tipc/configs/clas/hrnet/train_infer_python.txt +++ b/test_tipc/configs/clas/hrnet/train_infer_python.txt @@ -8,12 +8,12 @@ use_gpu:null|null --save_dir:adaptive --train_batch_size:lite_train_lite_infer=16|lite_train_whole_infer=16|whole_train_whole_infer=16 --model_path:null +--config:lite_train_lite_infer=./test_tipc/configs/clas/hrnet/hrnet_ucmerced.yaml|lite_train_whole_infer=./test_tipc/configs/clas/hrnet/hrnet_ucmerced.yaml|whole_train_whole_infer=./test_tipc/configs/clas/hrnet/hrnet_ucmerced.yaml train_model_name:best_model -train_infer_file_list:./test_tipc/data/ucmerced/:./test_tipc/data/ucmerced/val.txt null:null ## trainer:norm -norm_train:test_tipc/run_task.py train clas --config ./test_tipc/configs/clas/hrnet/hrnet.yaml +norm_train:test_tipc/run_task.py train clas pact_train:null fpgm_train:null distill_train:null @@ -46,7 +46,7 @@ inference:test_tipc/infer.py --use_trt:False --precision:fp32 --model_dir:null ---file_list:null:null +--config:null --save_log_path:null --benchmark:True --model_name:hrnet diff --git a/test_tipc/infer.py b/test_tipc/infer.py index 9ad6123..0be439b 100644 --- a/test_tipc/infer.py +++ b/test_tipc/infer.py @@ -13,6 +13,8 @@ from paddle.inference import PrecisionType from paddlers.tasks import load_model from paddlers.utils import logging +from config_utils import parse_configs + class _bool(object): def __new__(cls, x): @@ -285,7 +287,8 @@ class TIPCPredictor(object): if __name__ == '__main__': parser = argparse.ArgumentParser() - parser.add_argument('--file_list', type=str, nargs=2) + parser.add_argument('--config', type=str) + parser.add_argument('--inherit_off', action='store_true') parser.add_argument('--model_dir', type=str, default='./') parser.add_argument( '--device', type=str, choices=['cpu', 'gpu'], default='cpu') @@ -300,6 +303,11 @@ if __name__ == '__main__': args = parser.parse_args() + cfg = parse_configs(args.config, not args.inherit_off) + eval_dataset = cfg['datasets']['eval'] + data_dir = eval_dataset.args['data_dir'] + file_list = eval_dataset.args['file_list'] + predictor = TIPCPredictor( args.model_dir, device=args.device, @@ -310,7 +318,7 @@ if __name__ == '__main__': trt_precision_mode=args.precision, benchmark=args.benchmark) - predictor.predict(args.file_list[0], args.file_list[1]) + predictor.predict(data_dir, file_list) if args.benchmark: predictor.autolog.report() diff --git a/test_tipc/prepare.sh b/test_tipc/prepare.sh index ead48af..ac8267b 100644 --- a/test_tipc/prepare.sh +++ b/test_tipc/prepare.sh @@ -48,6 +48,8 @@ elif [[ ${MODE} == 'whole_train_whole_infer' ]]; then --out_dataset_dir "${DATA_DIR}/levircd" \ --crop_size 256 \ --crop_stride 256 + elif [[ ${task_name} == 'clas' ]]; then + download_and_unzip_dataset "${DATA_DIR}" ucmerced https://paddlers.bj.bcebos.com/datasets/ucmerced.zip fi fi diff --git a/tutorials/train/README.md b/tutorials/train/README.md index c63cf26..9c72107 100644 --- a/tutorials/train/README.md +++ b/tutorials/train/README.md @@ -9,11 +9,11 @@ |change_detection/changeformer.py | 变化检测 | ChangeFormer | |change_detection/dsamnet.py | 变化检测 | DSAMNet | |change_detection/dsifn.py | 变化检测 | DSIFN | -|change_detection/snunet.py | 变化检测 | SNUNet | -|change_detection/stanet.py | 变化检测 | STANet | |change_detection/fc_ef.py | 变化检测 | FC-EF | |change_detection/fc_siam_conc.py | 变化检测 | FC-Siam-conc | |change_detection/fc_siam_diff.py | 变化检测 | FC-Siam-diff | +|change_detection/snunet.py | 变化检测 | SNUNet | +|change_detection/stanet.py | 变化检测 | STANet | |classification/hrnet.py | 场景分类 | HRNet | |classification/mobilenetv3.py | 场景分类 | MobileNetV3 | |classification/resnet50_vd.py | 场景分类 | ResNet50-vd | From 87596b5f1ffe9223d0d5afbb95b1dc3084f003f2 Mon Sep 17 00:00:00 2001 From: Bobholamovic Date: Mon, 22 Aug 2022 16:22:51 +0800 Subject: [PATCH 22/52] Update experiment scripts --- examples/rs_research/configs/levircd/bit.yaml | 6 ------ .../configs/levircd/custom_model/custom_model_c.yaml | 8 ++++++++ .../configs/levircd/custom_model/custom_model_cs.yaml | 8 ++++++++ .../levircd/custom_model/custom_model_cst.yaml | 8 ++++++++ .../configs/levircd/custom_model/custom_model_ct.yaml | 8 ++++++++ .../configs/levircd/custom_model/custom_model_s.yaml | 8 ++++++++ .../configs/levircd/custom_model/custom_model_st.yaml | 8 ++++++++ .../configs/levircd/custom_model/custom_model_t.yaml | 8 ++++++++ .../levircd/custom_model/iterative_bit_iter2.yaml | 11 ----------- .../levircd/custom_model/iterative_bit_iter3.yaml | 11 ----------- .../levircd/custom_model/iterative_bit_iter4.yaml | 11 ----------- examples/rs_research/configs/levircd/fc_ef.yaml | 2 -- .../rs_research/configs/levircd/fc_siam_conc.yaml | 2 -- .../rs_research/configs/levircd/fc_siam_diff.yaml | 2 -- examples/rs_research/configs/levircd/stanet.yaml | 6 ------ examples/rs_research/configs/svcd/bit.yaml | 6 ------ examples/rs_research/configs/svcd/custom_model.yaml | 7 +------ examples/rs_research/configs/svcd/stanet.yaml | 6 ------ .../{run_parameter_analysis.sh => run_ablation.sh} | 2 +- 19 files changed, 58 insertions(+), 70 deletions(-) delete mode 100644 examples/rs_research/configs/levircd/bit.yaml create mode 100644 examples/rs_research/configs/levircd/custom_model/custom_model_c.yaml create mode 100644 examples/rs_research/configs/levircd/custom_model/custom_model_cs.yaml create mode 100644 examples/rs_research/configs/levircd/custom_model/custom_model_cst.yaml create mode 100644 examples/rs_research/configs/levircd/custom_model/custom_model_ct.yaml create mode 100644 examples/rs_research/configs/levircd/custom_model/custom_model_s.yaml create mode 100644 examples/rs_research/configs/levircd/custom_model/custom_model_st.yaml create mode 100644 examples/rs_research/configs/levircd/custom_model/custom_model_t.yaml delete mode 100644 examples/rs_research/configs/levircd/custom_model/iterative_bit_iter2.yaml delete mode 100644 examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3.yaml delete mode 100644 examples/rs_research/configs/levircd/custom_model/iterative_bit_iter4.yaml delete mode 100644 examples/rs_research/configs/levircd/stanet.yaml delete mode 100644 examples/rs_research/configs/svcd/bit.yaml delete mode 100644 examples/rs_research/configs/svcd/stanet.yaml rename examples/rs_research/scripts/{run_parameter_analysis.sh => run_ablation.sh} (91%) diff --git a/examples/rs_research/configs/levircd/bit.yaml b/examples/rs_research/configs/levircd/bit.yaml deleted file mode 100644 index 046f760..0000000 --- a/examples/rs_research/configs/levircd/bit.yaml +++ /dev/null @@ -1,6 +0,0 @@ -_base_: ./levircd.yaml - -save_dir: ./exp/levircd/bit/ - -model: !Node - type: BIT diff --git a/examples/rs_research/configs/levircd/custom_model/custom_model_c.yaml b/examples/rs_research/configs/levircd/custom_model/custom_model_c.yaml new file mode 100644 index 0000000..13db635 --- /dev/null +++ b/examples/rs_research/configs/levircd/custom_model/custom_model_c.yaml @@ -0,0 +1,8 @@ +_base_: ../levircd.yaml + +save_dir: ./exp/levircd/custom_model/att_c/ + +model: !Node + type: CustomTrainer + args: + att_types: c diff --git a/examples/rs_research/configs/levircd/custom_model/custom_model_cs.yaml b/examples/rs_research/configs/levircd/custom_model/custom_model_cs.yaml new file mode 100644 index 0000000..63229ee --- /dev/null +++ b/examples/rs_research/configs/levircd/custom_model/custom_model_cs.yaml @@ -0,0 +1,8 @@ +_base_: ../levircd.yaml + +save_dir: ./exp/levircd/custom_model/att_cs/ + +model: !Node + type: CustomTrainer + args: + att_types: cs diff --git a/examples/rs_research/configs/levircd/custom_model/custom_model_cst.yaml b/examples/rs_research/configs/levircd/custom_model/custom_model_cst.yaml new file mode 100644 index 0000000..cd2915c --- /dev/null +++ b/examples/rs_research/configs/levircd/custom_model/custom_model_cst.yaml @@ -0,0 +1,8 @@ +_base_: ../levircd.yaml + +save_dir: ./exp/levircd/custom_model/att_cst/ + +model: !Node + type: CustomTrainer + args: + att_types: cst diff --git a/examples/rs_research/configs/levircd/custom_model/custom_model_ct.yaml b/examples/rs_research/configs/levircd/custom_model/custom_model_ct.yaml new file mode 100644 index 0000000..5df0795 --- /dev/null +++ b/examples/rs_research/configs/levircd/custom_model/custom_model_ct.yaml @@ -0,0 +1,8 @@ +_base_: ../levircd.yaml + +save_dir: ./exp/levircd/custom_model/att_ct/ + +model: !Node + type: CustomTrainer + args: + att_types: ct diff --git a/examples/rs_research/configs/levircd/custom_model/custom_model_s.yaml b/examples/rs_research/configs/levircd/custom_model/custom_model_s.yaml new file mode 100644 index 0000000..525151e --- /dev/null +++ b/examples/rs_research/configs/levircd/custom_model/custom_model_s.yaml @@ -0,0 +1,8 @@ +_base_: ../levircd.yaml + +save_dir: ./exp/levircd/custom_model/att_s/ + +model: !Node + type: CustomTrainer + args: + att_types: s diff --git a/examples/rs_research/configs/levircd/custom_model/custom_model_st.yaml b/examples/rs_research/configs/levircd/custom_model/custom_model_st.yaml new file mode 100644 index 0000000..6ba149c --- /dev/null +++ b/examples/rs_research/configs/levircd/custom_model/custom_model_st.yaml @@ -0,0 +1,8 @@ +_base_: ../levircd.yaml + +save_dir: ./exp/levircd/custom_model/att_st/ + +model: !Node + type: CustomTrainer + args: + att_types: st diff --git a/examples/rs_research/configs/levircd/custom_model/custom_model_t.yaml b/examples/rs_research/configs/levircd/custom_model/custom_model_t.yaml new file mode 100644 index 0000000..984bd19 --- /dev/null +++ b/examples/rs_research/configs/levircd/custom_model/custom_model_t.yaml @@ -0,0 +1,8 @@ +_base_: ../levircd.yaml + +save_dir: ./exp/levircd/custom_model/att_t/ + +model: !Node + type: CustomTrainer + args: + att_types: t diff --git a/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter2.yaml b/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter2.yaml deleted file mode 100644 index 5b19c71..0000000 --- a/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter2.yaml +++ /dev/null @@ -1,11 +0,0 @@ -_base_: ../levircd.yaml - -save_dir: ./exp/levircd/custom_model/iter2/ - -model: !Node - type: IterativeBIT - args: - num_iters: 2 - num_classes: 2 - bit_kwargs: - in_channels: 3 diff --git a/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3.yaml b/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3.yaml deleted file mode 100644 index 4753c43..0000000 --- a/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter3.yaml +++ /dev/null @@ -1,11 +0,0 @@ -_base_: ../levircd.yaml - -save_dir: ./exp/levircd/custom_model/iter3/ - -model: !Node - type: IterativeBIT - args: - num_iters: 3 - num_classes: 2 - bit_kwargs: - in_channels: 3 diff --git a/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter4.yaml b/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter4.yaml deleted file mode 100644 index 72b9b0a..0000000 --- a/examples/rs_research/configs/levircd/custom_model/iterative_bit_iter4.yaml +++ /dev/null @@ -1,11 +0,0 @@ -_base_: ../levircd.yaml - -save_dir: ./exp/levircd/custom_model/iter4/ - -model: !Node - type: IterativeBIT - args: - num_iters: 4 - num_classes: 2 - bit_kwargs: - in_channels: 3 diff --git a/examples/rs_research/configs/levircd/fc_ef.yaml b/examples/rs_research/configs/levircd/fc_ef.yaml index b4bc998..6fdbc2f 100644 --- a/examples/rs_research/configs/levircd/fc_ef.yaml +++ b/examples/rs_research/configs/levircd/fc_ef.yaml @@ -4,5 +4,3 @@ save_dir: ./exp/levircd/fc_ef/ model: !Node type: FCEarlyFusion - args: - use_dropout: True diff --git a/examples/rs_research/configs/levircd/fc_siam_conc.yaml b/examples/rs_research/configs/levircd/fc_siam_conc.yaml index 426bdcc..50a8c8a 100644 --- a/examples/rs_research/configs/levircd/fc_siam_conc.yaml +++ b/examples/rs_research/configs/levircd/fc_siam_conc.yaml @@ -4,5 +4,3 @@ save_dir: ./exp/levircd/fc_siam_conc/ model: !Node type: FCSiamConc - args: - use_dropout: True diff --git a/examples/rs_research/configs/levircd/fc_siam_diff.yaml b/examples/rs_research/configs/levircd/fc_siam_diff.yaml index 704e528..4ab8874 100644 --- a/examples/rs_research/configs/levircd/fc_siam_diff.yaml +++ b/examples/rs_research/configs/levircd/fc_siam_diff.yaml @@ -4,5 +4,3 @@ save_dir: ./exp/levircd/fc_siam_diff/ model: !Node type: FCSiamDiff - args: - use_dropout: True diff --git a/examples/rs_research/configs/levircd/stanet.yaml b/examples/rs_research/configs/levircd/stanet.yaml deleted file mode 100644 index c03e2ed..0000000 --- a/examples/rs_research/configs/levircd/stanet.yaml +++ /dev/null @@ -1,6 +0,0 @@ -_base_: ./levircd.yaml - -save_dir: ./exp/levircd/stanet/ - -model: !Node - type: STANet diff --git a/examples/rs_research/configs/svcd/bit.yaml b/examples/rs_research/configs/svcd/bit.yaml deleted file mode 100644 index 2171205..0000000 --- a/examples/rs_research/configs/svcd/bit.yaml +++ /dev/null @@ -1,6 +0,0 @@ -_base_: ./svcd.yaml - -save_dir: ./exp/svcd/bit/ - -model: !Node - type: BIT diff --git a/examples/rs_research/configs/svcd/custom_model.yaml b/examples/rs_research/configs/svcd/custom_model.yaml index aac6748..20357a7 100644 --- a/examples/rs_research/configs/svcd/custom_model.yaml +++ b/examples/rs_research/configs/svcd/custom_model.yaml @@ -3,9 +3,4 @@ _base_: ./svcd.yaml save_dir: ./exp/svcd/custom_model/ model: !Node - type: IterativeBIT - args: - num_iters: 2 - num_classes: 2 - bit_kwargs: - in_channels: 3 + type: CustomTrainer diff --git a/examples/rs_research/configs/svcd/stanet.yaml b/examples/rs_research/configs/svcd/stanet.yaml deleted file mode 100644 index 8ff29f9..0000000 --- a/examples/rs_research/configs/svcd/stanet.yaml +++ /dev/null @@ -1,6 +0,0 @@ -_base_: ./svcd.yaml - -save_dir: ./exp/svcd/stanet/ - -model: !Node - type: STANet diff --git a/examples/rs_research/scripts/run_parameter_analysis.sh b/examples/rs_research/scripts/run_ablation.sh similarity index 91% rename from examples/rs_research/scripts/run_parameter_analysis.sh rename to examples/rs_research/scripts/run_ablation.sh index 2bb32f7..d54066a 100644 --- a/examples/rs_research/scripts/run_parameter_analysis.sh +++ b/examples/rs_research/scripts/run_ablation.sh @@ -3,7 +3,7 @@ set -e CONFIG_DIR='configs/levircd/custom_model' -LOG_DIR='exp/logs/parameter_analysis' +LOG_DIR='exp/logs/ablation' mkdir -p "${LOG_DIR}" From 4d58b9561ddd6b32830797a62d9734e5a471be8f Mon Sep 17 00:00:00 2001 From: Bobholamovic Date: Mon, 22 Aug 2022 19:48:19 +0800 Subject: [PATCH 23/52] Add unittests for restoration tasks --- paddlers/tasks/base.py | 2 +- paddlers/tasks/classifier.py | 81 ++++++++++------- paddlers/tasks/object_detector.py | 20 +++++ paddlers/tasks/restorer.py | 54 ++++++++---- paddlers/transforms/operators.py | 2 +- tests/data/data_utils.py | 54 ++++++------ tests/deploy/test_predictor.py | 61 +++++++++++-- tests/rs_models/test_cd_models.py | 4 +- tests/rs_models/test_det_models.py | 3 + tests/rs_models/test_res_models.py | 32 ++++--- tests/rs_models/test_seg_models.py | 4 +- tests/transforms/test_operators.py | 43 ++++++++++ tutorials/train/README.md | 4 +- .../train/change_detection/changeformer.py | 2 +- tutorials/train/image_restoration/drn.py | 4 +- tutorials/train/image_restoration/esrgan.py | 4 +- tutorials/train/image_restoration/lesrcnn.py | 4 +- tutorials/train/image_restoration/rcan.py | 86 ------------------- 18 files changed, 270 insertions(+), 194 deletions(-) delete mode 100644 tutorials/train/image_restoration/rcan.py diff --git a/paddlers/tasks/base.py b/paddlers/tasks/base.py index 2b7c295..8eb8b14 100644 --- a/paddlers/tasks/base.py +++ b/paddlers/tasks/base.py @@ -267,7 +267,7 @@ class BaseModel(metaclass=ModelMeta): 'The volume of dataset({}) must be larger than batch size({}).' .format(dataset.num_samples, batch_size)) batch_size_each_card = get_single_card_bs(batch_size=batch_size) - # TODO: Make judgement in detection eval phase. + batch_sampler = DistributedBatchSampler( dataset, batch_size=batch_size_each_card, diff --git a/paddlers/tasks/classifier.py b/paddlers/tasks/classifier.py index 06702d9..c8a2c7a 100644 --- a/paddlers/tasks/classifier.py +++ b/paddlers/tasks/classifier.py @@ -397,38 +397,37 @@ class BaseClassifier(BaseModel): ): paddle.distributed.init_parallel_env() - batch_size_each_card = get_single_card_bs(batch_size) - if batch_size_each_card > 1: - batch_size_each_card = 1 - batch_size = batch_size_each_card * paddlers.env_info['num'] + if batch_size > 1: logging.warning( - "Classifier only supports batch_size=1 for each gpu/cpu card " \ - "during evaluation, so batch_size " \ - "is forcibly set to {}.".format(batch_size)) - self.eval_data_loader = self.build_data_loader( - eval_dataset, batch_size=batch_size, mode='eval') - - logging.info( - "Start to evaluate(total_samples={}, total_steps={})...".format( - eval_dataset.num_samples, - math.ceil(eval_dataset.num_samples * 1.0 / batch_size))) - - top1s = [] - top5s = [] - with paddle.no_grad(): - for step, data in enumerate(self.eval_data_loader): - data.append(eval_dataset.transforms.transforms) - outputs = self.run(self.net, data, 'eval') - top1s.append(outputs["top1"]) - top5s.append(outputs["top5"]) - - top1 = np.mean(top1s) - top5 = np.mean(top5s) - eval_metrics = OrderedDict(zip(['top1', 'top5'], [top1, top5])) - if return_details: - # TODO: Add details - return eval_metrics, None - return eval_metrics + "Classifier only supports single card evaluation with batch_size=1 " + "during evaluation, so batch_size is forcibly set to 1.") + batch_size = 1 + + if nranks < 2 or local_rank == 0: + self.eval_data_loader = self.build_data_loader( + eval_dataset, batch_size=batch_size, mode='eval') + logging.info( + "Start to evaluate(total_samples={}, total_steps={})...".format( + eval_dataset.num_samples, eval_dataset.num_samples)) + + top1s = [] + top5s = [] + with paddle.no_grad(): + for step, data in enumerate(self.eval_data_loader): + data.append(eval_dataset.transforms.transforms) + outputs = self.run(self.net, data, 'eval') + top1s.append(outputs["top1"]) + top5s.append(outputs["top5"]) + + top1 = np.mean(top1s) + top5 = np.mean(top5s) + eval_metrics = OrderedDict(zip(['top1', 'top5'], [top1, top5])) + + if return_details: + # TODO: Add details + return eval_metrics, None + + return eval_metrics def predict(self, img_file, transforms=None): """ @@ -561,6 +560,26 @@ class BaseClassifier(BaseModel): raise TypeError( "`transforms.arrange` must be an ArrangeClassifier object.") + def build_data_loader(self, dataset, batch_size, mode='train'): + if dataset.num_samples < batch_size: + raise ValueError( + 'The volume of dataset({}) must be larger than batch size({}).' + .format(dataset.num_samples, batch_size)) + + if mode != 'train': + return paddle.io.DataLoader( + dataset, + batch_size=batch_size, + shuffle=dataset.shuffle, + drop_last=False, + collate_fn=dataset.batch_transforms, + num_workers=dataset.num_workers, + return_list=True, + use_shared_memory=False) + else: + return super(BaseClassifier, self).build_data_loader( + dataset, batch_size, mode) + class ResNet50_vd(BaseClassifier): def __init__(self, diff --git a/paddlers/tasks/object_detector.py b/paddlers/tasks/object_detector.py index bf28393..d6ce624 100644 --- a/paddlers/tasks/object_detector.py +++ b/paddlers/tasks/object_detector.py @@ -983,6 +983,26 @@ class PicoDet(BaseDetector): use_vdl=use_vdl, resume_checkpoint=resume_checkpoint) + def build_data_loader(self, dataset, batch_size, mode='train'): + if dataset.num_samples < batch_size: + raise ValueError( + 'The volume of dataset({}) must be larger than batch size({}).' + .format(dataset.num_samples, batch_size)) + + if mode != 'train': + return paddle.io.DataLoader( + dataset, + batch_size=batch_size, + shuffle=dataset.shuffle, + drop_last=False, + collate_fn=dataset.batch_transforms, + num_workers=dataset.num_workers, + return_list=True, + use_shared_memory=False) + else: + return super(BaseDetector, self).build_data_loader(dataset, + batch_size, mode) + class YOLOv3(BaseDetector): def __init__(self, diff --git a/paddlers/tasks/restorer.py b/paddlers/tasks/restorer.py index c3aa59a..3c57403 100644 --- a/paddlers/tasks/restorer.py +++ b/paddlers/tasks/restorer.py @@ -35,7 +35,7 @@ from .base import BaseModel from .utils.res_adapters import GANAdapter, OptimizerAdapter from .utils.infer_nets import InferResNet -__all__ = [] +__all__ = ["DRN", "LESRCNN", "ESRGAN"] class BaseRestorer(BaseModel): @@ -381,22 +381,22 @@ class BaseRestorer(BaseModel): ): paddle.distributed.init_parallel_env() - batch_size_each_card = get_single_card_bs(batch_size) - if batch_size_each_card > 1: - batch_size_each_card = 1 - batch_size = batch_size_each_card * paddlers.env_info['num'] + # TODO: Distributed evaluation + if batch_size > 1: logging.warning( - "Restorer only supports batch_size=1 for each gpu/cpu card " \ - "during evaluation, so batch_size " \ - "is forcibly set to {}.".format(batch_size)) + "Restorer only supports single card evaluation with batch_size=1 " + "during evaluation, so batch_size is forcibly set to 1.") + batch_size = 1 - # TODO: Distributed evaluation if nranks < 2 or local_rank == 0: self.eval_data_loader = self.build_data_loader( eval_dataset, batch_size=batch_size, mode='eval') # XXX: Hard-code crop_border and test_y_channel psnr = metrics.PSNR(crop_border=4, test_y_channel=True) ssim = metrics.SSIM(crop_border=4, test_y_channel=True) + logging.info( + "Start to evaluate(total_samples={}, total_steps={})...".format( + eval_dataset.num_samples, eval_dataset.num_samples)) with paddle.no_grad(): for step, data in enumerate(self.eval_data_loader): data.append(eval_dataset.transforms.transforms) @@ -404,14 +404,18 @@ class BaseRestorer(BaseModel): psnr.update(outputs['pred'], outputs['tar']) ssim.update(outputs['pred'], outputs['tar']) - eval_metrics = OrderedDict( - zip(['psnr', 'ssim'], [psnr.accumulate(), ssim.accumulate()])) + # DO NOT use psnr.accumulate() here, otherwise the program hangs in multi-card training. + assert len(psnr.results) > 0 + assert len(ssim.results) > 0 + eval_metrics = OrderedDict( + zip(['psnr', 'ssim'], + [np.mean(psnr.results), np.mean(ssim.results)])) - if return_details: - # TODO: Add details - return eval_metrics, None + if return_details: + # TODO: Add details + return eval_metrics, None - return eval_metrics + return eval_metrics def predict(self, img_file, transforms=None): """ @@ -591,6 +595,26 @@ class BaseRestorer(BaseModel): raise TypeError( "`transforms.arrange` must be an ArrangeRestorer object.") + def build_data_loader(self, dataset, batch_size, mode='train'): + if dataset.num_samples < batch_size: + raise ValueError( + 'The volume of dataset({}) must be larger than batch size({}).' + .format(dataset.num_samples, batch_size)) + + if mode != 'train': + return paddle.io.DataLoader( + dataset, + batch_size=batch_size, + shuffle=dataset.shuffle, + drop_last=False, + collate_fn=dataset.batch_transforms, + num_workers=dataset.num_workers, + return_list=True, + use_shared_memory=False) + else: + return super(BaseRestorer, self).build_data_loader(dataset, + batch_size, mode) + def set_losses(self, losses): self.losses = losses diff --git a/paddlers/transforms/operators.py b/paddlers/transforms/operators.py index 6267239..e1fef8a 100644 --- a/paddlers/transforms/operators.py +++ b/paddlers/transforms/operators.py @@ -1793,7 +1793,7 @@ class SelectBand(Transform): def __init__(self, band_list=[1, 2, 3], apply_to_tar=True): super(SelectBand, self).__init__() self.band_list = band_list - self.appy_to_tar = apply_to_tar + self.apply_to_tar = apply_to_tar def apply_im(self, image): image = select_bands(image, self.band_list) diff --git a/tests/data/data_utils.py b/tests/data/data_utils.py index 404e04e..afd9a0e 100644 --- a/tests/data/data_utils.py +++ b/tests/data/data_utils.py @@ -14,7 +14,6 @@ import os.path as osp import re -import imghdr import platform from collections import OrderedDict from functools import partial, wraps @@ -34,20 +33,6 @@ def norm_path(path): return path -def is_pic(im_path): - valid_suffix = [ - 'JPEG', 'jpeg', 'JPG', 'jpg', 'BMP', 'bmp', 'PNG', 'png', 'npy' - ] - suffix = im_path.split('.')[-1] - if suffix in valid_suffix: - return True - im_format = imghdr.what(im_path) - _, ext = osp.splitext(im_path) - if im_format == 'tiff' or ext == '.img': - return True - return False - - def get_full_path(p, prefix=''): p = norm_path(p) return osp.join(prefix, p) @@ -323,15 +308,34 @@ class ConstrDetSample(ConstrSample): return samples -def build_input_from_file(file_list, prefix='', task='auto', label_list=None): +class ConstrResSample(ConstrSample): + def __init__(self, prefix, label_list, sr_factor=None): + super().__init__(prefix, label_list) + self.sr_factor = sr_factor + + def __call__(self, src_path, tar_path): + sample = { + 'image': self.get_full_path(src_path), + 'target': self.get_full_path(tar_path) + } + if self.sr_factor is not None: + sample['sr_factor'] = self.sr_factor + return sample + + +def build_input_from_file(file_list, + prefix='', + task='auto', + label_list=None, + **kwargs): """ Construct a list of dictionaries from file. Each dict in the list can be used as the input to paddlers.transforms.Transform objects. Args: - file_list (str): Path of file_list. + file_list (str): Path of file list. prefix (str, optional): A nonempty `prefix` specifies the directory that stores the images and annotation files. Default: ''. - task (str, optional): Supported values are 'seg', 'det', 'cd', 'clas', and 'auto'. When `task` is set to 'auto', automatically determine the task based on the input. - Default: 'auto'. + task (str, optional): Supported values are 'seg', 'det', 'cd', 'clas', 'res', and 'auto'. When `task` is set to 'auto', + automatically determine the task based on the input. Default: 'auto'. label_list (str|None, optional): Path of label_list. Default: None. Returns: @@ -339,22 +343,21 @@ def build_input_from_file(file_list, prefix='', task='auto', label_list=None): """ def _determine_task(parts): + task = 'unknown' if len(parts) in (3, 5): task = 'cd' elif len(parts) == 2: if parts[1].isdigit(): task = 'clas' - elif is_pic(osp.join(prefix, parts[1])): - task = 'seg' - else: + elif parts[1].endswith('.xml'): task = 'det' - else: + if task == 'unknown': raise RuntimeError( "Cannot automatically determine the task type. Please specify `task` manually." ) return task - if task not in ('seg', 'det', 'cd', 'clas', 'auto'): + if task not in ('seg', 'det', 'cd', 'clas', 'res', 'auto'): raise ValueError("Invalid value of `task`") samples = [] @@ -366,9 +369,8 @@ def build_input_from_file(file_list, prefix='', task='auto', label_list=None): if task == 'auto': task = _determine_task(parts) if ctor is None: - # Select and build sample constructor ctor_class = globals()['Constr' + task.capitalize() + 'Sample'] - ctor = ctor_class(prefix, label_list) + ctor = ctor_class(prefix, label_list, **kwargs) sample = ctor(*parts) if isinstance(sample, list): samples.extend(sample) diff --git a/tests/deploy/test_predictor.py b/tests/deploy/test_predictor.py index c18c51a..24db0ff 100644 --- a/tests/deploy/test_predictor.py +++ b/tests/deploy/test_predictor.py @@ -105,7 +105,7 @@ class TestPredictor(CommonTest): dict_[key], expected_dict[key], rtol=1.e-4, atol=1.e-6) -@TestPredictor.add_tests +# @TestPredictor.add_tests class TestCDPredictor(TestPredictor): MODULE = pdrs.tasks.change_detector TRAINER_NAME_TO_EXPORT_OPTS = { @@ -177,7 +177,7 @@ class TestCDPredictor(TestPredictor): self.assertEqual(len(out_multi_array_t), num_inputs) -@TestPredictor.add_tests +# @TestPredictor.add_tests class TestClasPredictor(TestPredictor): MODULE = pdrs.tasks.classifier TRAINER_NAME_TO_EXPORT_OPTS = { @@ -185,7 +185,7 @@ class TestClasPredictor(TestPredictor): } def check_predictor(self, predictor, trainer): - single_input = "data/ssmt/optical_t1.bmp" + single_input = "data/ssst/optical.bmp" num_inputs = 2 transforms = pdrs.transforms.Compose([ pdrs.transforms.DecodeImg(), pdrs.transforms.Normalize(), @@ -242,7 +242,7 @@ class TestClasPredictor(TestPredictor): self.check_dict_equal(out_multi_array_p, out_multi_array_t) -@TestPredictor.add_tests +# @TestPredictor.add_tests class TestDetPredictor(TestPredictor): MODULE = pdrs.tasks.object_detector TRAINER_NAME_TO_EXPORT_OPTS = { @@ -253,7 +253,7 @@ class TestDetPredictor(TestPredictor): # For detection tasks, do NOT ensure the consistence of bboxes. # This is because the coordinates of bboxes were observed to be very sensitive to numeric errors, # given that the network is (partially?) randomly initialized. - single_input = "data/ssmt/optical_t1.bmp" + single_input = "data/ssst/optical.bmp" num_inputs = 2 transforms = pdrs.transforms.Compose([ pdrs.transforms.DecodeImg(), pdrs.transforms.Normalize(), @@ -307,10 +307,55 @@ class TestResPredictor(TestPredictor): MODULE = pdrs.tasks.restorer def check_predictor(self, predictor, trainer): - pass + # For restoration tasks, do NOT ensure the consistence of numeric values, + # because the output is of uint8 type. + single_input = "data/ssst/optical.bmp" + num_inputs = 2 + transforms = pdrs.transforms.Compose([ + pdrs.transforms.DecodeImg(), pdrs.transforms.Normalize(), + pdrs.transforms.ArrangeRestorer('test') + ]) + # Single input (file path) + input_ = single_input + predictor.predict(input_, transforms=transforms) + trainer.predict(input_, transforms=transforms) + out_single_file_list_p = predictor.predict( + [input_], transforms=transforms) + self.assertEqual(len(out_single_file_list_p), 1) + out_single_file_list_t = trainer.predict( + [input_], transforms=transforms) + self.assertEqual(len(out_single_file_list_t), 1) -@TestPredictor.add_tests + # Single input (ndarray) + input_ = decode_image( + single_input, to_rgb=False) # Reuse the name `input_` + predictor.predict(input_, transforms=transforms) + trainer.predict(input_, transforms=transforms) + out_single_array_list_p = predictor.predict( + [input_], transforms=transforms) + self.assertEqual(len(out_single_array_list_p), 1) + out_single_array_list_t = trainer.predict( + [input_], transforms=transforms) + self.assertEqual(len(out_single_array_list_t), 1) + + # Multiple inputs (file paths) + input_ = [single_input] * num_inputs # Reuse the name `input_` + out_multi_file_p = predictor.predict(input_, transforms=transforms) + self.assertEqual(len(out_multi_file_p), num_inputs) + out_multi_file_t = trainer.predict(input_, transforms=transforms) + self.assertEqual(len(out_multi_file_t), num_inputs) + + # Multiple inputs (ndarrays) + input_ = [decode_image( + single_input, to_rgb=False)] * num_inputs # Reuse the name `input_` + out_multi_array_p = predictor.predict(input_, transforms=transforms) + self.assertEqual(len(out_multi_array_p), num_inputs) + out_multi_array_t = trainer.predict(input_, transforms=transforms) + self.assertEqual(len(out_multi_array_t), num_inputs) + + +# @TestPredictor.add_tests class TestSegPredictor(TestPredictor): MODULE = pdrs.tasks.segmenter TRAINER_NAME_TO_EXPORT_OPTS = { @@ -318,7 +363,7 @@ class TestSegPredictor(TestPredictor): } def check_predictor(self, predictor, trainer): - single_input = "data/ssmt/optical_t1.bmp" + single_input = "data/ssst/optical.bmp" num_inputs = 2 transforms = pdrs.transforms.Compose([ pdrs.transforms.DecodeImg(), pdrs.transforms.Normalize(), diff --git a/tests/rs_models/test_cd_models.py b/tests/rs_models/test_cd_models.py index f712fd3..f1d1e6e 100644 --- a/tests/rs_models/test_cd_models.py +++ b/tests/rs_models/test_cd_models.py @@ -34,9 +34,7 @@ class TestCDModel(TestModel): self.check_output_equal(len(output), len(target)) for o, t in zip(output, target): o = o.numpy() - self.check_output_equal(o.shape[0], t.shape[0]) - self.check_output_equal(len(o.shape), 4) - self.check_output_equal(o.shape[2:], t.shape[2:]) + self.check_output_equal(o.shape, t.shape) def set_inputs(self): if self.EF_MODE == 'Concat': diff --git a/tests/rs_models/test_det_models.py b/tests/rs_models/test_det_models.py index 5aed6ef..112c6cd 100644 --- a/tests/rs_models/test_det_models.py +++ b/tests/rs_models/test_det_models.py @@ -32,3 +32,6 @@ class TestDetModel(TestModel): def set_inputs(self): self.inputs = cycle([self.get_randn_tensor(3)]) + + def set_targets(self): + self.targets = cycle([None]) diff --git a/tests/rs_models/test_res_models.py b/tests/rs_models/test_res_models.py index 205696d..8b6ec56 100644 --- a/tests/rs_models/test_res_models.py +++ b/tests/rs_models/test_res_models.py @@ -15,22 +15,32 @@ import paddlers from rs_models.test_model import TestModel -__all__ = ['TestRCANModel'] +__all__ = [] class TestResModel(TestModel): def check_output(self, output, target): - pass + output = output.numpy() + self.check_output_equal(output.shape, target.shape) def set_inputs(self): - pass - - def set_targets(self): - pass + def _gen_data(specs): + for spec in specs: + c = spec.get('in_channels', 3) + yield self.get_randn_tensor(c) + self.inputs = _gen_data(self.specs) -class TestRCANModel(TestSegModel): - MODEL_CLASS = paddlers.rs_models.res.RCAN - - def set_specs(self): - pass + def set_targets(self): + def _gen_data(specs): + for spec in specs: + # XXX: Hard coding + if 'out_channels' in spec: + c = spec['out_channels'] + elif 'in_channels' in spec: + c = spec['in_channels'] + else: + c = 3 + yield [self.get_zeros_array(c)] + + self.targets = _gen_data(self.specs) diff --git a/tests/rs_models/test_seg_models.py b/tests/rs_models/test_seg_models.py index cc3415d..e2fe6a6 100644 --- a/tests/rs_models/test_seg_models.py +++ b/tests/rs_models/test_seg_models.py @@ -26,9 +26,7 @@ class TestSegModel(TestModel): self.check_output_equal(len(output), len(target)) for o, t in zip(output, target): o = o.numpy() - self.check_output_equal(o.shape[0], t.shape[0]) - self.check_output_equal(len(o.shape), 4) - self.check_output_equal(o.shape[2:], t.shape[2:]) + self.check_output_equal(o.shape, t.shape) def set_inputs(self): def _gen_data(specs): diff --git a/tests/transforms/test_operators.py b/tests/transforms/test_operators.py index 6ddac53..cff8428 100644 --- a/tests/transforms/test_operators.py +++ b/tests/transforms/test_operators.py @@ -164,12 +164,15 @@ class TestTransform(CpuCommonTest): prefix="./data/ssst"), build_input_from_file( "data/ssst/test_optical_seg.txt", + task='seg', prefix="./data/ssst"), build_input_from_file( "data/ssst/test_sar_seg.txt", + task='seg', prefix="./data/ssst"), build_input_from_file( "data/ssst/test_multispectral_seg.txt", + task='seg', prefix="./data/ssst"), build_input_from_file( "data/ssst/test_optical_det.txt", @@ -185,7 +188,23 @@ class TestTransform(CpuCommonTest): label_list="data/ssst/labels_det.txt"), build_input_from_file( "data/ssst/test_det_coco.txt", + task='det', prefix="./data/ssst"), + build_input_from_file( + "data/ssst/test_optical_res.txt", + task='res', + prefix="./data/ssst", + sr_factor=4), + build_input_from_file( + "data/ssst/test_sar_res.txt", + task='res', + prefix="./data/ssst", + sr_factor=4), + build_input_from_file( + "data/ssst/test_multispectral_res.txt", + task='res', + prefix="./data/ssst", + sr_factor=4), build_input_from_file( "data/ssmt/test_mixed_binary.txt", prefix="./data/ssmt"), @@ -227,6 +246,8 @@ class TestTransform(CpuCommonTest): self.aux_mask_values = [ set(aux_mask.ravel()) for aux_mask in sample['aux_masks'] ] + if 'target' in sample: + self.target_shape = sample['target'].shape return sample def _out_hook_not_keep_ratio(sample): @@ -243,6 +264,21 @@ class TestTransform(CpuCommonTest): for aux_mask, amv in zip(sample['aux_masks'], self.aux_mask_values): self.assertLessEqual(set(aux_mask.ravel()), amv) + if 'target' in sample: + if 'sr_factor' in sample: + self.check_output_equal( + sample['target'].shape[:2], + T.functions.calc_hr_shape(TARGET_SIZE, + sample['sr_factor'])) + else: + self.check_output_equal(sample['target'].shape[:2], + TARGET_SIZE) + self.check_output_equal( + sample['target'].shape[0] / self.target_shape[0], + sample['image'].shape[0] / self.image_shape[0]) + self.check_output_equal( + sample['target'].shape[1] / self.target_shape[1], + sample['image'].shape[1] / self.image_shape[1]) # TODO: Test gt_bbox and gt_poly return sample @@ -260,6 +296,13 @@ class TestTransform(CpuCommonTest): for aux_mask, ori_aux_mask_shape in zip(sample['aux_masks'], self.aux_mask_shapes): __check_ratio(aux_mask.shape, ori_aux_mask_shape) + if 'target' in sample: + self.check_output_equal( + sample['target'].shape[0] / self.target_shape[0], + sample['image'].shape[0] / self.image_shape[0]) + self.check_output_equal( + sample['target'].shape[1] / self.target_shape[1], + sample['image'].shape[1] / self.image_shape[1]) # TODO: Test gt_bbox and gt_poly return sample diff --git a/tutorials/train/README.md b/tutorials/train/README.md index 350f3dc..09d67cd 100644 --- a/tutorials/train/README.md +++ b/tutorials/train/README.md @@ -9,11 +9,11 @@ |change_detection/changeformer.py | 变化检测 | ChangeFormer | |change_detection/dsamnet.py | 变化检测 | DSAMNet | |change_detection/dsifn.py | 变化检测 | DSIFN | -|change_detection/snunet.py | 变化检测 | SNUNet | -|change_detection/stanet.py | 变化检测 | STANet | |change_detection/fc_ef.py | 变化检测 | FC-EF | |change_detection/fc_siam_conc.py | 变化检测 | FC-Siam-conc | |change_detection/fc_siam_diff.py | 变化检测 | FC-Siam-diff | +|change_detection/snunet.py | 变化检测 | SNUNet | +|change_detection/stanet.py | 变化检测 | STANet | |classification/hrnet.py | 场景分类 | HRNet | |classification/mobilenetv3.py | 场景分类 | MobileNetV3 | |classification/resnet50_vd.py | 场景分类 | ResNet50-vd | diff --git a/tutorials/train/change_detection/changeformer.py b/tutorials/train/change_detection/changeformer.py index 7afbf96..58606c1 100644 --- a/tutorials/train/change_detection/changeformer.py +++ b/tutorials/train/change_detection/changeformer.py @@ -72,7 +72,7 @@ eval_dataset = pdrs.datasets.CDDataset( binarize_labels=True) # 使用默认参数构建ChangeFormer模型 -# 目前已支持的模型请参考:https://github.com/PaddlePaddle/PaddleRS/blob/develop/docs/apis/model_zoo.md +# 目前已支持的模型请参考:https://github.com/PaddlePaddle/PaddleRS/blob/develop/docs/intro/model_zoo.md # 模型输入参数请参考:https://github.com/PaddlePaddle/PaddleRS/blob/develop/paddlers/tasks/change_detector.py model = pdrs.tasks.cd.ChangeFormer() diff --git a/tutorials/train/image_restoration/drn.py b/tutorials/train/image_restoration/drn.py index 400a4ad..6af93ac 100644 --- a/tutorials/train/image_restoration/drn.py +++ b/tutorials/train/image_restoration/drn.py @@ -75,9 +75,9 @@ model.train( train_dataset=train_dataset, train_batch_size=8, eval_dataset=eval_dataset, - save_interval_epochs=1, + save_interval_epochs=5, # 每多少次迭代记录一次日志 - log_interval_steps=5, + log_interval_steps=10, save_dir=EXP_DIR, # 初始学习率大小 learning_rate=0.001, diff --git a/tutorials/train/image_restoration/esrgan.py b/tutorials/train/image_restoration/esrgan.py index 7e9bb89..33ff3f8 100644 --- a/tutorials/train/image_restoration/esrgan.py +++ b/tutorials/train/image_restoration/esrgan.py @@ -75,9 +75,9 @@ model.train( train_dataset=train_dataset, train_batch_size=8, eval_dataset=eval_dataset, - save_interval_epochs=1, + save_interval_epochs=5, # 每多少次迭代记录一次日志 - log_interval_steps=5, + log_interval_steps=10, save_dir=EXP_DIR, # 初始学习率大小 learning_rate=0.001, diff --git a/tutorials/train/image_restoration/lesrcnn.py b/tutorials/train/image_restoration/lesrcnn.py index 0689c01..0c27823 100644 --- a/tutorials/train/image_restoration/lesrcnn.py +++ b/tutorials/train/image_restoration/lesrcnn.py @@ -75,9 +75,9 @@ model.train( train_dataset=train_dataset, train_batch_size=8, eval_dataset=eval_dataset, - save_interval_epochs=1, + save_interval_epochs=5, # 每多少次迭代记录一次日志 - log_interval_steps=5, + log_interval_steps=10, save_dir=EXP_DIR, # 初始学习率大小 learning_rate=0.001, diff --git a/tutorials/train/image_restoration/rcan.py b/tutorials/train/image_restoration/rcan.py deleted file mode 100644 index e792ac6..0000000 --- a/tutorials/train/image_restoration/rcan.py +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env python - -# 图像复原模型RCAN训练示例脚本 -# 执行此脚本前,请确认已正确安装PaddleRS库 - -import paddlers as pdrs -from paddlers import transforms as T - -# 数据集存放目录 -DATA_DIR = './data/rssr/' -# 训练集`file_list`文件路径 -TRAIN_FILE_LIST_PATH = './data/rssr/train.txt' -# 验证集`file_list`文件路径 -EVAL_FILE_LIST_PATH = './data/rssr/val.txt' -# 实验目录,保存输出的模型权重和结果 -EXP_DIR = './output/rcan/' - -# 下载和解压遥感影像超分辨率数据集 -pdrs.utils.download_and_decompress( - 'https://paddlers.bj.bcebos.com/datasets/rssr.zip', path='./data/') - -# 定义训练和验证时使用的数据变换(数据增强、预处理等) -# 使用Compose组合多种变换方式。Compose中包含的变换将按顺序串行执行 -# API说明:https://github.com/PaddlePaddle/PaddleRS/blob/develop/docs/apis/data.md -train_transforms = T.Compose([ - # 读取影像 - T.DecodeImg(), - # 将输入影像缩放到256x256大小 - T.Resize(target_size=256), - # 以50%的概率实施随机水平翻转 - T.RandomHorizontalFlip(prob=0.5), - # 以50%的概率实施随机垂直翻转 - T.RandomVerticalFlip(prob=0.5), - # 将数据归一化到[0,1] - T.Normalize( - mean=[0.0, 0.0, 0.0], std=[1.0, 1.0, 1.0]), - T.ArrangeRestorer('train') -]) - -eval_transforms = T.Compose([ - T.DecodeImg(), - T.Resize(target_size=256), - # 验证阶段与训练阶段的数据归一化方式必须相同 - T.Normalize( - mean=[0.0, 0.0, 0.0], std=[1.0, 1.0, 1.0]), - T.ArrangeRestorer('eval') -]) - -# 分别构建训练和验证所用的数据集 -train_dataset = pdrs.datasets.ResDataset( - data_dir=DATA_DIR, - file_list=TRAIN_FILE_LIST_PATH, - transforms=train_transforms, - num_workers=0, - shuffle=True) - -eval_dataset = pdrs.datasets.ResDataset( - data_dir=DATA_DIR, - file_list=EVAL_FILE_LIST_PATH, - transforms=eval_transforms, - num_workers=0, - shuffle=False) - -# 使用默认参数构建RCAN模型 -# 目前已支持的模型请参考:https://github.com/PaddlePaddle/PaddleRS/blob/develop/docs/intro/model_zoo.md -# 模型输入参数请参考:https://github.com/PaddlePaddle/PaddleRS/blob/develop/paddlers/tasks/restorer.py -model = pdrs.tasks.res.RCAN() - -# 执行模型训练 -model.train( - num_epochs=10, - train_dataset=train_dataset, - train_batch_size=8, - eval_dataset=eval_dataset, - save_interval_epochs=1, - # 每多少次迭代记录一次日志 - log_interval_steps=50, - save_dir=EXP_DIR, - # 初始学习率大小 - learning_rate=0.01, - # 是否使用early stopping策略,当精度不再改善时提前终止训练 - early_stop=False, - # 是否启用VisualDL日志功能 - use_vdl=True, - # 指定从某个检查点继续训练 - resume_checkpoint=None) From 9abefa7155ff14814d00b8f044f057317593ce4d Mon Sep 17 00:00:00 2001 From: Bobholamovic Date: Mon, 22 Aug 2022 20:51:12 +0800 Subject: [PATCH 24/52] Add tqdm in requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 706bb43..52841e5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,5 +19,6 @@ natsort geojson colorama filelock +tqdm # # Self installation # GDAL >= 3.1.3 From 328c1a1177587663274256356cf63111a0b748cf Mon Sep 17 00:00:00 2001 From: Bobholamovic Date: Mon, 22 Aug 2022 20:54:49 +0800 Subject: [PATCH 25/52] Add pyyaml to requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 52841e5..b852af5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,5 +20,6 @@ geojson colorama filelock tqdm +pyyaml # # Self installation # GDAL >= 3.1.3 From e4c0b553b71a93214bfdccc13fa935f89f2daa0d Mon Sep 17 00:00:00 2001 From: Bobholamovic Date: Mon, 22 Aug 2022 21:02:04 +0800 Subject: [PATCH 26/52] Update requirements.txt --- requirements.txt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index b852af5..3554dcb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -paddleslim >= 2.2.1 +paddleslim >= 2.2.1,<2.3.3 visualdl >= 2.1.1 opencv-contrib-python == 4.3.0.38 numba == 0.53.1 @@ -19,7 +19,5 @@ natsort geojson colorama filelock -tqdm -pyyaml # # Self installation # GDAL >= 3.1.3 From 9e6e6c717c2d6dc95f468e1782a7a9dfeacc57e2 Mon Sep 17 00:00:00 2001 From: Bobholamovic Date: Mon, 22 Aug 2022 21:29:13 +0800 Subject: [PATCH 27/52] Update requirements.txt --- tests/rs_models/test_seg_models.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/rs_models/test_seg_models.py b/tests/rs_models/test_seg_models.py index e2fe6a6..88fb6e1 100644 --- a/tests/rs_models/test_seg_models.py +++ b/tests/rs_models/test_seg_models.py @@ -52,3 +52,7 @@ class TestFarSegModel(TestSegModel): self.specs = [ dict(), dict(num_classes=20), dict(encoder_pretrained=False) ] + + def set_targets(self): + self.targets = [[self.get_zeros_array(16)], [self.get_zeros_array(20)], + [self.get_zeros_array(16)]] From d02d4527068d2decd0e3e7e9a72f07f9e1eb79ff Mon Sep 17 00:00:00 2001 From: Bobholamovic Date: Tue, 23 Aug 2022 10:46:43 +0800 Subject: [PATCH 28/52] Remove underscore in _*process --- paddlers/tasks/restorer.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/paddlers/tasks/restorer.py b/paddlers/tasks/restorer.py index 3c57403..f68264d 100644 --- a/paddlers/tasks/restorer.py +++ b/paddlers/tasks/restorer.py @@ -115,7 +115,7 @@ class BaseRestorer(BaseModel): tar_shape = inputs[1] if self.status == 'Infer': net_out = net(inputs[0]) - res_map_list = self._postprocess( + res_map_list = self.postprocess( net_out, tar_shape, transforms=inputs[2]) else: if isinstance(net, GANAdapter): @@ -124,7 +124,7 @@ class BaseRestorer(BaseModel): net_out = net(inputs[0]) if self.TEST_OUT_KEY is not None: net_out = net_out[self.TEST_OUT_KEY] - pred = self._postprocess( + pred = self.postprocess( net_out, tar_shape, transforms=inputs[2]) res_map_list = [] for res_map in pred: @@ -141,7 +141,7 @@ class BaseRestorer(BaseModel): net_out = net_out[self.TEST_OUT_KEY] tar = inputs[1] tar_shape = [tar.shape[-2:]] - pred = self._postprocess( + pred = self.postprocess( net_out, tar_shape, transforms=inputs[2])[0] # NCHW pred = self._tensor_to_images(pred) outputs['pred'] = pred @@ -446,8 +446,8 @@ class BaseRestorer(BaseModel): images = [img_file] else: images = img_file - batch_im, batch_tar_shape = self._preprocess(images, transforms, - self.model_type) + batch_im, batch_tar_shape = self.preprocess(images, transforms, + self.model_type) self.net.eval() data = (batch_im, batch_tar_shape, transforms.transforms) outputs = self.run(self.net, data, 'test') @@ -458,7 +458,7 @@ class BaseRestorer(BaseModel): prediction = {'res_map': res_map_list[0]} return prediction - def _preprocess(self, images, transforms, to_tensor=True): + def preprocess(self, images, transforms, to_tensor=True): self._check_transforms(transforms, 'test') batch_im = list() batch_tar_shape = list() @@ -531,7 +531,7 @@ class BaseRestorer(BaseModel): batch_restore_list.append(restore_list) return batch_restore_list - def _postprocess(self, batch_pred, batch_tar_shape, transforms): + def postprocess(self, batch_pred, batch_tar_shape, transforms): batch_restore_list = BaseRestorer.get_transforms_shape_info( batch_tar_shape, transforms) if self.status == 'Infer': From 69b5398972f6fc462168040ee2c21ca3dbc9ec94 Mon Sep 17 00:00:00 2001 From: Lin Manhui Date: Tue, 23 Aug 2022 13:43:34 +0800 Subject: [PATCH 29/52] [Test] Add CI Script (#25) --- tests/run_ci_dev.sh | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 tests/run_ci_dev.sh diff --git a/tests/run_ci_dev.sh b/tests/run_ci_dev.sh new file mode 100644 index 0000000..73e1c91 --- /dev/null +++ b/tests/run_ci_dev.sh @@ -0,0 +1,34 @@ +#!/bin bash + +rm -rf /usr/local/python2.7.15/bin/python +rm -rf /usr/local/python2.7.15/bin/pip +ln -s /usr/local/bin/python3.7 /usr/local/python2.7.15/bin/python +ln -s /usr/local/bin/pip3.7 /usr/local/python2.7.15/bin/pip +export PYTHONPATH=`pwd` + +python -m pip install --upgrade pip --ignore-installed +# python -m pip install --upgrade numpy --ignore-installed +python -m pip uninstall paddlepaddle-gpu -y +if [[ ${branch} == 'develop' ]];then +echo "checkout develop !" +python -m pip install ${paddle_dev} --no-cache-dir +else +echo "checkout release !" +python -m pip install ${paddle_release} --no-cache-dir +fi + +echo -e '*****************paddle_version*****' +python -c 'import paddle;print(paddle.version.commit)' +echo -e '*****************paddleseg_version****' +git rev-parse HEAD + +pip install -r requirements.txt --ignore-installed +pip install -e . + +cd tests/ +bash run_fast_tests.sh + +cd .. +for config in $(ls test_tipc/configs/*/*/train_infer_python.txt); do + bash test_tipc/test_train_inference_python.sh ${config} lite_train_lite_infer +done From c8fdc54886c02bfceedd15d954c6d56b3bcaa60b Mon Sep 17 00:00:00 2001 From: Lin Manhui Date: Tue, 23 Aug 2022 14:16:08 +0800 Subject: [PATCH 30/52] Update requirements.txt (#26) --- requirements.txt | 2 +- tests/run_ci_dev.sh | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 706bb43..3554dcb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -paddleslim >= 2.2.1 +paddleslim >= 2.2.1,<2.3.3 visualdl >= 2.1.1 opencv-contrib-python == 4.3.0.38 numba == 0.53.1 diff --git a/tests/run_ci_dev.sh b/tests/run_ci_dev.sh index 73e1c91..77153a1 100644 --- a/tests/run_ci_dev.sh +++ b/tests/run_ci_dev.sh @@ -25,6 +25,10 @@ git rev-parse HEAD pip install -r requirements.txt --ignore-installed pip install -e . +unset http_proxy https_proxy + +set -e + cd tests/ bash run_fast_tests.sh From aa59a143b9922fc8cf6ec69d310fae413bf98b67 Mon Sep 17 00:00:00 2001 From: Lin Manhui Date: Tue, 23 Aug 2022 14:52:38 +0800 Subject: [PATCH 31/52] Update run_ci_dev.sh --- tests/run_ci_dev.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/run_ci_dev.sh b/tests/run_ci_dev.sh index 77153a1..d8584f0 100644 --- a/tests/run_ci_dev.sh +++ b/tests/run_ci_dev.sh @@ -24,6 +24,7 @@ git rev-parse HEAD pip install -r requirements.txt --ignore-installed pip install -e . +pip install https://versaweb.dl.sourceforge.net/project/gdal-wheels-for-linux/GDAL-3.4.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl unset http_proxy https_proxy From a599d85395ce26def2b93858a1175275c7414605 Mon Sep 17 00:00:00 2001 From: Lin Manhui Date: Tue, 23 Aug 2022 15:02:36 +0800 Subject: [PATCH 32/52] Remove CUDA10.2 workflow --- .github/workflows/build_and_test.yaml | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/.github/workflows/build_and_test.yaml b/.github/workflows/build_and_test.yaml index 339634d..9a8d9ce 100644 --- a/.github/workflows/build_and_test.yaml +++ b/.github/workflows/build_and_test.yaml @@ -58,24 +58,3 @@ jobs: cd tests bash run_fast_tests.sh shell: bash - - build_and_test_cuda102: - runs-on: ubuntu-18.04 - container: - image: registry.baidubce.com/paddlepaddle/paddle:2.3.1-gpu-cuda10.2-cudnn7 - steps: - - uses: actions/checkout@v3 - - name: Upgrade pip - run: python3.7 -m pip install pip --upgrade --user - - name: Install PaddleRS - run: | - python3.7 -m pip install -r requirements.txt - python3.7 -m pip install -e . - - name: Install GDAL - run: python3.7 -m pip install https://versaweb.dl.sourceforge.net/project/gdal-wheels-for-linux/GDAL-3.4.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl - # Do not run unittests, because there is NO GPU in the machine. - # - name: Run unittests - # run: | - # cd tests - # bash run_fast_tests.sh - # shell: bash \ No newline at end of file From 66cf12d3a14e28864f03520c3332f031c1718d68 Mon Sep 17 00:00:00 2001 From: Lin Manhui Date: Tue, 23 Aug 2022 15:22:52 +0800 Subject: [PATCH 33/52] Update run_ci_dev.sh --- tests/run_ci_dev.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/run_ci_dev.sh b/tests/run_ci_dev.sh index d8584f0..184397a 100644 --- a/tests/run_ci_dev.sh +++ b/tests/run_ci_dev.sh @@ -35,5 +35,6 @@ bash run_fast_tests.sh cd .. for config in $(ls test_tipc/configs/*/*/train_infer_python.txt); do + bash test_tipc/prepare.sh ${config} lite_train_lite_infer bash test_tipc/test_train_inference_python.sh ${config} lite_train_lite_infer done From ee05f40d72b57440c9ff3679d0f4cbe727c2d088 Mon Sep 17 00:00:00 2001 From: Lin Manhui Date: Tue, 23 Aug 2022 17:05:13 +0800 Subject: [PATCH 34/52] Bypass OOM error for some models (#27) --- tests/rs_models/test_cd_models.py | 11 +++------- tests/rs_models/test_model.py | 34 +++++++++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/tests/rs_models/test_cd_models.py b/tests/rs_models/test_cd_models.py index f712fd3..8478ea4 100644 --- a/tests/rs_models/test_cd_models.py +++ b/tests/rs_models/test_cd_models.py @@ -12,11 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -import platform from itertools import cycle import paddlers -from rs_models.test_model import TestModel +from rs_models.test_model import TestModel, allow_oom __all__ = [ 'TestBITModel', 'TestCDNetModel', 'TestChangeStarModel', 'TestDSAMNetModel', @@ -202,6 +201,7 @@ class TestSNUNetModel(TestCDModel): ] # yapf: disable +@allow_oom class TestSTANetModel(TestCDModel): MODEL_CLASS = paddlers.rs_models.cd.STANet @@ -216,6 +216,7 @@ class TestSTANetModel(TestCDModel): ] # yapf: disable +@allow_oom class TestChangeFormerModel(TestCDModel): MODEL_CLASS = paddlers.rs_models.cd.ChangeFormer @@ -226,9 +227,3 @@ class TestChangeFormerModel(TestCDModel): dict(**base_spec, decoder_softmax=True), dict(**base_spec, embed_dim=56) ] # yapf: disable - - -# HACK:FIXME: We observe an OOM error when running TestSTANetModel.test_forward() on a Windows machine. -# Currently, we do not perform this test. -if platform.system() == 'Windows': - TestSTANetModel.test_forward = lambda self: None diff --git a/tests/rs_models/test_model.py b/tests/rs_models/test_model.py index 06c4777..3c1c555 100644 --- a/tests/rs_models/test_model.py +++ b/tests/rs_models/test_model.py @@ -18,6 +18,7 @@ import paddle import numpy as np from paddle.static import InputSpec +from paddlers.utils import logging from testing_utils import CommonTest @@ -37,20 +38,26 @@ class _TestModelNamespace: for i, ( input, model, target ) in enumerate(zip(self.inputs, self.models, self.targets)): - with self.subTest(i=i): + try: if isinstance(input, list): output = model(*input) else: output = model(input) self.check_output(output, target) + except: + logging.warning(f"Model built with spec{i} failed!") + raise def test_to_static(self): for i, ( input, model, target ) in enumerate(zip(self.inputs, self.models, self.targets)): - with self.subTest(i=i): + try: static_model = paddle.jit.to_static( model, input_spec=self.get_input_spec(model, input)) + except: + logging.warning(f"Model built with spec{i} failed!") + raise def check_output(self, output, target): pass @@ -117,4 +124,27 @@ class _TestModelNamespace: return input_spec +def allow_oom(cls): + def _deco(func): + def _wrapper(self, *args, **kwargs): + try: + func(self, *args, **kwargs) + except (SystemError, RuntimeError, OSError) as e: + msg = str(e) + if "Out of memory error" in msg \ + or "(External) CUDNN error(4), CUDNN_STATUS_INTERNAL_ERROR." in msg: + logging.warning("An OOM error has been ignored.") + else: + raise + + return _wrapper + + for key, value in inspect.getmembers(cls): + if key.startswith('test'): + value = _deco(value) + setattr(cls, key, value) + + return cls + + TestModel = _TestModelNamespace.TestModel From 7371bf76b9a2755c442c4d67dc5eebabaa874733 Mon Sep 17 00:00:00 2001 From: Lin Manhui Date: Tue, 23 Aug 2022 17:36:37 +0800 Subject: [PATCH 35/52] Update run_ci_dev.sh --- tests/run_ci_dev.sh | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/run_ci_dev.sh b/tests/run_ci_dev.sh index 184397a..12ff7f5 100644 --- a/tests/run_ci_dev.sh +++ b/tests/run_ci_dev.sh @@ -32,9 +32,3 @@ set -e cd tests/ bash run_fast_tests.sh - -cd .. -for config in $(ls test_tipc/configs/*/*/train_infer_python.txt); do - bash test_tipc/prepare.sh ${config} lite_train_lite_infer - bash test_tipc/test_train_inference_python.sh ${config} lite_train_lite_infer -done From f267908f368d4bfa2f5c2031dca71db1744cd7c2 Mon Sep 17 00:00:00 2001 From: Lin Manhui Date: Tue, 23 Aug 2022 21:29:57 +0800 Subject: [PATCH 36/52] [Test] Optimize CI (#28) --- .../workflows/{build_and_test.yaml => build.yaml} | 9 ++------- README.md | 2 +- docs/apis/train.md | 2 +- paddlers/tasks/segmenter.py | 2 +- test_tipc/configs/seg/unet/unet.yaml | 2 +- tests/rs_models/test_model.py | 6 ++++-- tests/run_ci_dev.sh | 7 +++++++ tests/run_tipc_lite.sh | 13 +++++++++++++ 8 files changed, 30 insertions(+), 13 deletions(-) rename .github/workflows/{build_and_test.yaml => build.yaml} (92%) create mode 100644 tests/run_tipc_lite.sh diff --git a/.github/workflows/build_and_test.yaml b/.github/workflows/build.yaml similarity index 92% rename from .github/workflows/build_and_test.yaml rename to .github/workflows/build.yaml index 9a8d9ce..63f7bc7 100644 --- a/.github/workflows/build_and_test.yaml +++ b/.github/workflows/build.yaml @@ -1,4 +1,4 @@ -name: build and test +name: build on: push: @@ -17,7 +17,7 @@ concurrency: cancel-in-progress: true jobs: - build_and_test_cpu: + build_cpu: runs-on: ${{ matrix.os }} strategy: matrix: @@ -53,8 +53,3 @@ jobs: python -m pip install -e . - name: Install GDAL run: python -m pip install ${{ matrix.gdal-whl-url }} - - name: Run unittests - run: | - cd tests - bash run_fast_tests.sh - shell: bash diff --git a/README.md b/README.md index b67b4b1..8e26833 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ [![license](https://img.shields.io/badge/license-Apache%202-blue.svg)](LICENSE) - [![build status](https://github.com/PaddlePaddle/PaddleRS/actions/workflows/build_and_test.yaml/badge.svg?branch=develop)](https://github.com/PaddlePaddle/PaddleRS/actions) + [![build status](https://github.com/PaddlePaddle/PaddleRS/actions/workflows/build.yaml/badge.svg?branch=develop)](https://github.com/PaddlePaddle/PaddleRS/actions) ![python version](https://img.shields.io/badge/python-3.7+-orange.svg) ![support os](https://img.shields.io/badge/os-linux%2C%20win%2C%20mac-yellow.svg) diff --git a/docs/apis/train.md b/docs/apis/train.md index 97f55b1..944b0b3 100644 --- a/docs/apis/train.md +++ b/docs/apis/train.md @@ -25,7 +25,7 @@ ### 初始化`BaseSegmenter`子类对象 -- 一般支持设置`input_channel`、`num_classes`以及`use_mixed_loss`参数,分别表示输入通道数、输出类别数以及是否使用预置的混合损失。部分模型如`FarSeg`暂不支持对`input_channel`参数的设置。 +- 一般支持设置`in_channels`、`num_classes`以及`use_mixed_loss`参数,分别表示输入通道数、输出类别数以及是否使用预置的混合损失。部分模型如`FarSeg`暂不支持对`in_channels`参数的设置。 - `use_mixed_loss`参将在未来被弃用,因此不建议使用。 - 不同的子类支持与模型相关的输入参数,详情请参考[模型定义](https://github.com/PaddlePaddle/PaddleRS/blob/develop/paddlers/rs_models/seg)和[训练器定义](https://github.com/PaddlePaddle/PaddleRS/blob/develop/paddlers/tasks/segmentor.py)。 diff --git a/paddlers/tasks/segmenter.py b/paddlers/tasks/segmenter.py index dea068a..9800a3c 100644 --- a/paddlers/tasks/segmenter.py +++ b/paddlers/tasks/segmenter.py @@ -822,7 +822,7 @@ class DeepLabV3P(BaseSegmenter): if params.get('with_net', True): with DisablePrint(): backbone = getattr(ppseg.models, backbone)( - input_channel=input_channel, output_stride=output_stride) + input_channel=in_channels, output_stride=output_stride) else: backbone = None params.update({ diff --git a/test_tipc/configs/seg/unet/unet.yaml b/test_tipc/configs/seg/unet/unet.yaml index 8d2af9c..4077bf0 100644 --- a/test_tipc/configs/seg/unet/unet.yaml +++ b/test_tipc/configs/seg/unet/unet.yaml @@ -7,5 +7,5 @@ save_dir: ./test_tipc/output/seg/unet/ model: !Node type: UNet args: - input_channel: 10 + in_channels: 10 num_classes: 5 \ No newline at end of file diff --git a/tests/rs_models/test_model.py b/tests/rs_models/test_model.py index 3c1c555..cd1d623 100644 --- a/tests/rs_models/test_model.py +++ b/tests/rs_models/test_model.py @@ -129,10 +129,12 @@ def allow_oom(cls): def _wrapper(self, *args, **kwargs): try: func(self, *args, **kwargs) - except (SystemError, RuntimeError, OSError) as e: + except (SystemError, RuntimeError, OSError, MemoryError) as e: + # XXX: This may not cover all OOM cases. msg = str(e) if "Out of memory error" in msg \ - or "(External) CUDNN error(4), CUDNN_STATUS_INTERNAL_ERROR." in msg: + or "(External) CUDNN error(4), CUDNN_STATUS_INTERNAL_ERROR." in msg \ + or isinstance(e, MemoryError): logging.warning("An OOM error has been ignored.") else: raise diff --git a/tests/run_ci_dev.sh b/tests/run_ci_dev.sh index 12ff7f5..d789511 100644 --- a/tests/run_ci_dev.sh +++ b/tests/run_ci_dev.sh @@ -26,6 +26,13 @@ pip install -r requirements.txt --ignore-installed pip install -e . pip install https://versaweb.dl.sourceforge.net/project/gdal-wheels-for-linux/GDAL-3.4.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl +git clone https://github.com/LDOUBLEV/AutoLog +cd AutoLog +pip install -r requirements.txt +python setup.py bdist_wheel +pip install ./dist/auto_log*.whl +cd .. + unset http_proxy https_proxy set -e diff --git a/tests/run_tipc_lite.sh b/tests/run_tipc_lite.sh new file mode 100644 index 0000000..50a8f9f --- /dev/null +++ b/tests/run_tipc_lite.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +cd .. + +for config in $(ls test_tipc/configs/*/*/train_infer_python.txt); do + bash test_tipc/prepare.sh ${config} lite_train_lite_infer + bash test_tipc/test_train_inference_python.sh ${config} lite_train_lite_infer + task="$(basename $(dirname $(dirname ${config})))" + model="$(basename $(dirname ${config}))" + if grep -q 'failed' "test_tipc/output/${task}/${model}/lite_train_lite_infer/results_python.log"; then + exit 1 + fi +done \ No newline at end of file From 4868629179c0dd370b20e3d1baab04f120c7b6cb Mon Sep 17 00:00:00 2001 From: Lin Manhui Date: Tue, 23 Aug 2022 23:14:16 +0800 Subject: [PATCH 37/52] Update build.yaml --- .github/workflows/build.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 63f7bc7..f9d3d3f 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -53,3 +53,5 @@ jobs: python -m pip install -e . - name: Install GDAL run: python -m pip install ${{ matrix.gdal-whl-url }} + - name: Test installation + run: python -c "import paddlers; print(paddlers.__version__)" From cc0788fc57b0cacde6c96ad6f94e45434c5dfb49 Mon Sep 17 00:00:00 2001 From: Lin Manhui Date: Wed, 24 Aug 2022 10:59:35 +0800 Subject: [PATCH 38/52] Update QR Code --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8e26833..7f38bec 100644 --- a/README.md +++ b/README.md @@ -179,7 +179,7 @@ PaddleRS目录树中关键部分如下: * 如果您发现任何PaddleRS存在的问题或是对PaddleRS有建议, 欢迎通过[GitHub Issues](https://github.com/PaddlePaddle/PaddleRS/issues)向我们提出。 * 欢迎加入PaddleRS微信群

- +
## 使用教程 From 5a6f19da8b65d9577248ffdfae57756be184b493 Mon Sep 17 00:00:00 2001 From: Bobholamovic Date: Thu, 25 Aug 2022 10:47:24 +0800 Subject: [PATCH 39/52] Add tools and experimental results --- examples/rs_research/README.md | 303 ++++++++++++++---- .../levircd/ablation/custom_model_c.yaml | 8 + .../levircd/ablation/custom_model_t.yaml | 8 + .../configs/levircd/custom_model.yaml | 6 + examples/rs_research/custom_model.py | 96 ++++-- examples/rs_research/custom_trainer.py | 2 +- examples/rs_research/predict_cd.py | 68 ++++ examples/rs_research/scripts/run_ablation.sh | 5 +- examples/rs_research/tools/analyze_model.py | 134 ++++++++ examples/rs_research/tools/collect_imgs.py | 61 ++++ examples/rs_research/tools/visualize_feats.py | 193 +++++++++++ 11 files changed, 792 insertions(+), 92 deletions(-) create mode 100644 examples/rs_research/configs/levircd/ablation/custom_model_c.yaml create mode 100644 examples/rs_research/configs/levircd/ablation/custom_model_t.yaml create mode 100644 examples/rs_research/configs/levircd/custom_model.yaml create mode 100644 examples/rs_research/predict_cd.py create mode 100644 examples/rs_research/tools/analyze_model.py create mode 100644 examples/rs_research/tools/collect_imgs.py create mode 100644 examples/rs_research/tools/visualize_feats.py diff --git a/examples/rs_research/README.md b/examples/rs_research/README.md index f0c2aa4..715e61b 100644 --- a/examples/rs_research/README.md +++ b/examples/rs_research/README.md @@ -12,25 +12,27 @@ cd examples/rs_research ``` +请注意,本文档仅所提供的所有指令遵循bash语法。 + ## 2 数据准备 本案例在[LEVIR-CD数据集](https://www.mdpi.com/2072-4292/12/10/1662)[1]和[synthetic images and real season-varying remote sensing images(SVCD)数据集](https://www.int-arch-photogramm-remote-sens-spatial-inf-sci.net/XLII-2/565/2018/isprs-archives-XLII-2-565-2018.pdf)[2]上开展实验。请在[LEVIR-CD数据集下载链接](https://justchenhao.github.io/LEVIR/)和[SVCD数据集下载链接](https://drive.google.com/file/d/1GX656JqqOyBi_Ef0w65kDGVto-nHrNs9/edit)分别下载这两个数据集,解压至本地目录,并执行如下指令: -```shell +```bash mkdir data/ python ../../tools/prepare_dataset/prepare_levircd.py \ - --in_dataset_dir {LEVIR-CD数据集存放目录路径} \ - --out_dataset_dir "data/levircd" \ + --in_dataset_dir "{LEVIR-CD数据集存放目录路径}" \ + --out_dataset_dir 'data/levircd' \ --crop_size 256 \ --crop_stride 256 python ../../tools/prepare_dataset/prepare_svcd.py \ - --in_dataset_dir {SVCD数据集存放目录路径} \ - --out_dataset_dir "data/svcd" + --in_dataset_dir "{SVCD数据集存放目录路径}" \ + --out_dataset_dir 'data/svcd' ``` 以上指令利用PaddleRS提供的数据集准备工具完成数据集切分、file list创建等操作。具体而言,对于LEVIR-CD数据集,使用官方的训练/验证/测试集划分,并将原始的`1024x1024`大小的影像切分为无重叠的`256x256`的小块(参考[3]中的做法);对于SVCD数据集,使用官方的训练/验证/测试集划分,不做其它额外处理。 -## 3 模型设计与验证 +## 3 模型设计 ### 3.1 问题分析与思路拟定 @@ -43,18 +45,21 @@ python ../../tools/prepare_dataset/prepare_svcd.py \ 1. 巨大的参数量意味着巨大的存储开销。在许多实际场景中,硬件资源往往是有限的,过多的模型参数将给部署造成困难。 2. 在数据有限的情况下,大模型更易遭受过拟合,其在实验数据集上看起来良好的结果也难以泛化到真实场景。 -本案例认为,上述问题的根源在于参数量与数据量的失衡所导致的特征冗余。既然模型的特征存在冗余,也即存在一部分“无用”的特征,是否存在某种手段,能够在固定模型参数量的前提下对特征进行优化,从而“榨取”小模型的更多潜力,获取更多更加有效的特征?基于这个观点,本案例的基本思路是为现有的变化检测模型添加一个“插件式”的特征优化模块,在仅引入较少额外的参数数量的情况下,实现变化特征增强。本案例计划以变化检测领域经典的FC-Siam-diff[4]为baseline网络,利用时间、空间、通道注意力模块对网络的中间层特征进行优化,从而减小特征冗余,提升检测效果。在具体的模块设计方面,对于时间与通道维度,选用论文[5]中提出的通道注意力模块;对于空间维度,选用论文[5]中提出的空间注意力模块。 +本案例认为,上述问题的根源在于参数量与数据量的失衡所导致的特征冗余。既然模型的特征存在冗余,也即存在一部分“无用”的特征,是否存在某种手段,能够在固定模型参数量的前提下对特征进行优化,从而“榨取”小模型的更多潜力,获取更多更加有效的特征?基于这个观点,本案例的基本思路是为现有的变化检测模型添加一个“插件式”的特征优化模块,在仅引入较少额外的参数数量的情况下,实现变化特征增强。本案例计划以变化检测领域经典的FC-Siam-conc[4]为baseline网络,利用通道和时间注意力模块对网络的中间层特征进行优化,从而减小特征冗余,提升检测效果。在具体的模块设计方面,选用论文[5]中提出的通道注意力模块实现通道和时间维度的特征增强。 ### 3.2 模型定义 +本小节基于PaddlePaddle框架与PaddleRS库实现[3.1节](#3.1-问题分析与思路拟定)中提出的想法。 + #### 3.2.1 自定义模型组网 -在`custom_model.py`中定义模型的宏观(macro)结构以及组成模型的各个微观(micro)模块。例如,本案例中,`custom_model.py`中定义了改进后的FC-EF结构,其核心部分实现如下: +在`custom_model.py`中定义模型的宏观(macro)结构以及组成模型的各个微观(micro)模块。本案例在`custom_model.py`中定义了改进后的FC-Siam-conc结构,其核心部分实现如下: + ```python ... # PaddleRS提供了许多开箱即用的模块,其中有对底层基础模块的封装(如conv-bn-relu结构等),也有注意力模块等较高层级的结构 from paddlers.rs_models.cd.layers import Conv3x3, MaxPool2x2, ConvTransposed3x3, Identity -from paddlers.rs_models.cd.layers import ChannelAttention, SpatialAttention +from paddlers.rs_models.cd.layers import ChannelAttention from attach_tools import Attach @@ -65,52 +70,90 @@ class CustomModel(nn.Layer): def __init__(self, in_channels, num_classes, - att_types='cst', + att_types='ct', use_dropout=False): super().__init__() ... + # 构建一个混合注意力模块att4,用于处理两个编码器最终输出的特征 + self.att4 = MixedAttention(C4, att_types) + + self.init_weight() + + def forward(self, t1, t2): + ... + x4d = self.upconv4(x4p) + pad4 = (0, x43_1.shape[3] - x4d.shape[3], 0, + x43_1.shape[2] - x4d.shape[2]) + x4d = F.pad(x4d, pad=pad4, mode='replicate') + # 将注意力模块接入第一个解码单元 + x43_1, x43_2 = self.att4(x43_1, x43_2) + x4d = paddle.concat([x4d, x43_1, x43_2], 1) + x43d = self.do43d(self.conv43d(x4d)) + x42d = self.do42d(self.conv42d(x43d)) + x41d = self.do41d(self.conv41d(x42d)) + ... + + +class MixedAttention(nn.Layer): + def __init__(self, in_channels, att_types='ct'): + super(MixedAttention, self).__init__() + + self.att_types = att_types # 从`att_types`参数中获取要使用的注意力类型 # 每个注意力模块都是可选的 - if 'c' in att_types: - self.att_c = ChannelAttention(C4) + if self.has_att_c: + self.att_c = ChannelAttention(in_channels, ratio=1) + # 在时间注意力模块之后增加归一化层 + # 利用BN层中的可学习参数增强模型的拟合能力 + self.norm_c1 = nn.BatchNorm(in_channels) + self.norm_c2 = nn.BatchNorm(in_channels) else: self.att_c = Identity() - if 's' in att_types: - self.att_s = SpatialAttention() - else: - self.att_s = Identity() + self.norm_c1 = Identity() + self.norm_c2 = Identity() + # 时间注意力模块部分复用通道注意力的逻辑,在`forward()`中将具体解释 - if 't' in att_types: + if has_att_t: self.att_t = ChannelAttention(2, ratio=1) else: self.att_t = Identity() - self.init_weight() + def forward(x1, x2): + # x1和x2分别是FC-Siam-conc的两路编码器提取的特征 + + if self.has_att_c: + # 首先使用通道注意力模块对特征进行优化 + # 两个时相的编码特征共享通道注意力模块,但使用各自的归一化层 + x1 = self.att_c(x1) * x1 + x1 = self.norm_c1(x1) + x2 = self.att_c(x2) * x2 + x2 = self.norm_c2(x2) + + if self.has_att_t: + b, c = x1.shape[:2] + # 为了复用通道注意力模块执行时间维度的注意力操作,首先将两个时相的特征堆叠 + y = paddle.stack([x1, x2], axis=2) + # 堆叠后的y形状为[b, c, t, h, w],其中b表示batch size,c为特征通道数,t为2(时相数目),h和w分别为特征图高宽 + # 将b和c两个维度合并,输出tensor形状为[b*c, t, h, w] + y = paddle.flatten(y, stop_axis=1) + # 此时,时间维度已经替代了原先的通道维度,将四维tensor输入ChannelAttention模块进行处理 + y = self.att_t(y) * y + # 从处理结果中分离两个时相的信息 + y = y.reshape((b, c, 2, *y.shape[2:])) + y1, y2 = y[:, :, 0], y[:, :, 1] + else: + y1, y2 = x1, x2 - def forward(self, t1, t2): - ... - # 以下是本案例在FC-EF基础上新增的部分 - # x43_1和x43_2分别是FC-EF的两路编码器提取的特征 - # 首先使用通道和空间注意力模块对特征进行优化 - x43_1 = self.att_c(x43_1) * x43_1 - x43_1 = self.att_s(x43_1) * x43_1 - x43_2 = self.att_c(x43_2) * x43_2 - x43_2 = self.att_s(x43_2) * x43_2 - # 为了复用通道注意力模块执行时间维度的注意力操作,首先将两个时相的特征堆叠 - x43 = paddle.stack([x43_1, x43_2], axis=1) - # 堆叠后的x43形状为[b, t, c, h, w],其中b表示batch size,t为2(时相数目),c为通道数,h和w分别为特征图高宽 - # 将t和c维度交换,输出tensor形状为[b, c, t, h, w] - x43 = paddle.transpose(x43, [0, 2, 1, 3, 4]) - # 将b和c两个维度合并,输出tensor形状为[b*c, t, h, w] - x43 = paddle.flatten(x43, stop_axis=1) - # 此时,时间维度已经替代了原先的通道维度,将四维tensor输入ChannelAttention模块进行处理 - x43 = self.att_t(x43) * x43 - # 从处理结果中分离两个时相的信息 - x43 = x43.reshape((x43_1.shape[0], -1, 2, *x43.shape[2:])) - x43_1, x43_2 = x43[:,:,0], x43[:,:,1] - ... - ... + return y1, y2 + + @property + def has_att_c(self): + return 'c' in self.att_types + + @property + def has_att_t(self): + return 't' in self.att_types ``` 在编写组网相关代码时请注意以下两点: @@ -132,7 +175,7 @@ class CustomTrainer(BaseChangeDetector): use_mixed_loss=False, losses=None, in_channels=3, - att_types='cst', + att_types='ct', use_dropout=False, **params): params.update({ @@ -158,46 +201,196 @@ class CustomTrainer(BaseChangeDetector): 关于训练器的更多细节请参考[API文档](https://github.com/PaddlePaddle/PaddleRS/blob/develop/docs/apis/train.md)。 -### 3.3 消融实验 +## 4 对比实验 -#### 3.3.1 实验设置 +为了验证模型设计的有效性,通常需要开展对比实验,在一个或多个数据集上比较所提出模型与其它模型的精度和性能。在本案例中,将自定义模型与FC-EF、FC-Siam-diff、FC-Siam-conc三种结构进行比较,这三个模型均来自论文[4]。 -#### 3.3.2 编写配置文件 +### 4.1 实验过程 -#### 3.3.3 实验结果 +使用如下指令在LEVIR-CD与SVCD数据集上执行对所有参与对比的模型的训练: -VisualDL、定量指标 +```bash +bash scripts/run_benchmark.sh +``` -### 3.4 特征可视化实验 +或者,可以按照以下格式执行对某个模型在某一数据集上的训练: -## 4 对比实验 +```bash +python run_task.py train cd \ + --config "configs/{数据集名称}/{配置文件名称}" \ + 2>&1 | tee "{日志路径}" +``` + +训练完成后,使用如下指令对验证集上最优的模型在测试集上计算指标: + +```bash +python run_task.py eval cd \ + --config "configs/{数据集名称}/{配置文件名称}" \ + --datasets.eval.args.file_list "data/{数据集名称}/test.txt" \ + --resume_checkpoint "exp/{数据集名称}/{模型名称}/best_model" +``` + +训练程序默认开启VisualDL日志记录功能。训练过程中或训练完成后,可使用VisualDL观察损失函数和精度指标的变化情况。在PaddleRS中使用VisualDL的方式请参考[使用教程](https://github.com/PaddlePaddle/PaddleRS/blob/develop/tutorials/train/README.md#visualdl%E5%8F%AF%E8%A7%86%E5%8C%96%E8%AE%AD%E7%BB%83%E6%8C%87%E6%A0%87)。 + +在训练和精度指标验证完成后,可以通过如下指令保存模型输出的二值变化图: + +```bash +python predict_cd.py \ + --model_dir "exp/{数据集名称}/{模型名称}/best_model" \ + --data_dir "data/{数据集名称}" \ + --file_list "data/{数据集名称}/test.txt" \ + --save_dir "exp/predict/{数据集名称}/{模型名称}" +``` + +之后,可在`exp/predict/{数据集名称}/{模型名称}`目录查看保存的输出结果。 + +可以通过`tools/collect_imgs.py`脚本将输入图像、真值标签以及多个模型的预测结果放置在一个目录下以便于观察比较。该脚本接受三个命令行选项: +- 使用`--globs`指定一系列通配符(可用于Python的[`glob.glob()`函数](https://docs.python.org/zh-cn/3/library/glob.html#glob.glob),用于匹配需要收集的图像; +- 使用`--tags`为`--globs`中的每一项指定一个别名,在存储目录中,相应的图像名将被替换为存储的别名; +- 使用`--save_dir`指定输出目录路径,若目录不存在将被自动创建。 -### 4.1 确定对比算法 +例如,对于LEVIR-CD数据集,执行如下指令: -### 4.2 准备对比算法配置文件 +```bash +python tools/collect_imgs.py \ + --globs "data/levircd/LEVIR-CD/test/A/*/*.png" "data/levircd/LEVIR-CD/test/B/*/*.png" "data/levircd/LEVIR-CD/test/label/*/*.png" \ + "exp/predict/levircd/fc_ef/*.png" "exp/predict/levircd/fc_siam_conc/*.png" "exp/predict/levircd/fc_siam_diff/*.png" \ + "exp/predict/levircd/custom_model/*.png" \ + --tags 'A' 'B' 'GT' \ + 'fc_ef' 'fc_siam_conc' 'fc_siam_diff' \ + 'custom_model' \ + --save_dir "exp/collect/levircd" +``` + +执行完毕后,可在`exp/collect/levircd`目录中找到两个时相的输入影像、真值标签以及各个模型的预测结果。当新增模型后,可以再次调用`tools/collect_imgs.py`脚本补充结果到`exp/collect/levircd`目录中: + +```bash +python tools/collect_imgs.py --globs "exp/predict/levircd/{新增模型名称}/*.png" --tags '{新增模型名称}' --save_dir "exp/collect/levircd" +``` + +对于SVCD数据集,执行如下指令: + +```bash +python tools/collect_imgs.py \ + --globs "data/svcd/ChangeDetectionDataset/Real/subset/test/A/*.jpg" "data/svcd/ChangeDetectionDataset/Real/subset/test/B/*.jpg" "data/svcd/ChangeDetectionDataset/Real/subset/test/OUT/*.jpg" \ + "exp/predict/svcd/fc_ef/*.png" "exp/predict/svcd/fc_siam_conc/*.png" "exp/predict/svcd/fc_siam_diff/*.png" \ + "exp/predict/svcd/custom_model/*.png" \ + --tags 'A' 'B' 'GT' \ + 'fc_ef' 'fc_siam_conc' 'fc_siam_diff' \ + 'custom_model' \ + --save_dir "exp/collect/svcd" +``` + +此外,为了从精度和性能两个方面综合评估变化检测算法,可以通过如下指令计算变化检测模型的[浮点计算数(floating point operations, FLOPs)](https://blog.csdn.net/IT_flying625/article/details/104898152)和模型参数量: + +```bash +python tools/analyze_model.py --model_dir "exp/{数据集名称}/{模型名称}/best_model" +``` -### 4.3 实验结果 +### 4.2 实验结果 -#### 4.3.1 LEVIR-CD数据集上的对比结果 +本案例使用变化类的[交并比(intersection over union, IoU)](https://paddlepedia.readthedocs.io/en/latest/tutorials/computer_vision/semantic_segmentation/Overview/Overview.html#id6)和[F1分数](https://baike.baidu.com/item/F1%E5%88%86%E6%95%B0/13864979)作为定量评价指标。在每个数据集上,从目视效果和定量指标两个方面对算法效果进行评判。 +#### 4.2.1 LEVIR-CD数据集上的对比结果 **目视效果对比** +|时相1影像|时相2影像|FC-EF|FC-Siam-diff|FC-Siam-conc|CustomModel|真值标签| +|:-:|:-:|:-:|:-:|:-:|:-:|:-:| +|![]()|![]()|![]()|![]()|![]()|![]()|![]()| + **定量指标对比** -#### 4.3.2 SVCD数据集上的对比结果 +|模型名称|FLOPs(G)|参数量(M)|IoU%|F1%| +|:-:|:-:|:-:|:-:|:-:| +|FC-EF|3.57|1.35|79.05|88.30| +|FC-Siam-diff|4.71|1.35|81.33|89.70| +|FC-Siam-conc|5.31|1.55|81.31|89.69| +|CustomModel|5.31|1.58|**82.27**|**90.27**| + +#### 4.2.2 SVCD数据集上的对比结果 **目视效果对比** +|时相1影像|时相2影像|FC-EF|FC-Siam-diff|FC-Siam-conc|CustomModel|真值标签| +|:-:|:-:|:-:|:-:|:-:|:-:|:-:| +|![]()|![]()|![]()|![]()|![]()|![]()|![]()| + **定量指标对比** +|模型名称|FLOPs(G)|参数量(M)|IoU%|F1%| +|:-:|:-:|:-:|:-:|:-:| +|FC-EF|3.57|1.35|84.11|91.37| +|FC-Siam-diff|4.71|1.35|88.75|94.04| +|FC-Siam-conc|5.31|1.55|88.29|93.78| +|CustomModel|5.31|1.58||| +## 5 消融实验 + +在科研过程中,为了验证在baseline上所做修改的有效性,常常需要开展消融实验。例如,在本案例中,自定义模型在FC-Siam-conc模型的基础上添加了通道和时间两种注意力模块,因此需要通过消融实验探讨各个注意力模块对最终精度的贡献。具体而言,包括以下4种实验情形(配置文件均存储在`configs/levircd/ablation`目录): + +1. 基础情况:不使用任何注意力模块,即baseline模型FC-Siam-conc。 +2. 仅添加通道注意力模块,对应的配置文件名称为`custom_model_c.yaml`。 +3. 仅添加时间注意力模块,对应的配置文件名称为`custom_model_t.yaml`。 +4. 标准情况:同时添加通道和时间注意力模块的完整模型。 + +其中第1和第4个模型,即baseline和完整模型,在[第4节](#4-对比实验)中已经得到了训练、验证和测试。因此,本节只需要关注情形2、3。 + +### 5.1 实验过程 + +使用如下指令执行全部消融模型的训练: + +```bash +bash scripts/run_ablation.sh +``` + +或者,可以按照以下格式执行对某一个模型的训练: + +```bash +python run_task.py train cd \ + --config "configs/levircd/ablation/{配置文件名称}" \ + 2>&1 | tee {日志路径} +``` + +训练完成后,使用如下指令对验证集上最优的模型在测试集上计算指标: + +```bash +python run_task.py eval cd \ + --config "configs/levircd/ablation/{配置文件名称}" \ + --datasets.eval.args.file_list data/levircd/test.txt \ + --resume_checkpoint "exp/levircd/ablation/{消融模型名称}/best_model" +``` + +注意,形如`custom_model_c.yaml`的配置文件默认对应的消融模型名称为`att_c`。 + +训练程序默认开启VisualDL日志记录功能。训练过程中或训练完成后,可使用VisualDL观察损失函数和精度指标的变化情况。在PaddleRS中使用VisualDL的方式请参考[使用教程](https://github.com/PaddlePaddle/PaddleRS/blob/develop/tutorials/train/README.md#visualdl%E5%8F%AF%E8%A7%86%E5%8C%96%E8%AE%AD%E7%BB%83%E6%8C%87%E6%A0%87)。 + +### 5.2 实验结果 + +实验得到的定量指标如下表所示: + +|通道注意力模块|时间注意力模块|IoU%|F1%| +|:-:|:-:|:-:|:-:| +|||81.31|89.69| +|✓||81.32|89.70| +||✓|81.61|89.88| +|✓|✓|**82.27**|**90.27**| + +其中,最高的指标用粗体表示。从表中数据可知,有限。 + +## 6 特征可视化实验 + +为了更好地探究。 + ## 5 总结与展望 ### 5.1 总结 +本案例以为经典的FC-Siam-conc模型添加注意力模块为例,演示了使用PaddleRS开展科研实验的典型流程。 +- 精度提升十分有限,算法设计。 + ### 5.2 展望 - 本案例对所有参与比较的算法使用了相同的训练超参数,但由于模型之间存在差异,使用统一的超参训练往往难以保证所有模型都能取得较好的效果。在后续工作中,可以对每个对比算法进行调参,使其获得最优精度。 -- 在评估算法效果时,仅仅对比了精度指标,而未对耗时、模型大小、FLOPs等指标进行考量。后续应当从精度和性能两个方面对算法进行综合评估。 +- 本案例只作为 ## 参考文献 diff --git a/examples/rs_research/configs/levircd/ablation/custom_model_c.yaml b/examples/rs_research/configs/levircd/ablation/custom_model_c.yaml new file mode 100644 index 0000000..d66cf44 --- /dev/null +++ b/examples/rs_research/configs/levircd/ablation/custom_model_c.yaml @@ -0,0 +1,8 @@ +_base_: ../levircd.yaml + +save_dir: ./exp/levircd/ablation/att_c/ + +model: !Node + type: CustomTrainer + args: + att_types: c diff --git a/examples/rs_research/configs/levircd/ablation/custom_model_t.yaml b/examples/rs_research/configs/levircd/ablation/custom_model_t.yaml new file mode 100644 index 0000000..028953b --- /dev/null +++ b/examples/rs_research/configs/levircd/ablation/custom_model_t.yaml @@ -0,0 +1,8 @@ +_base_: ../levircd.yaml + +save_dir: ./exp/levircd/ablation/att_t/ + +model: !Node + type: CustomTrainer + args: + att_types: t diff --git a/examples/rs_research/configs/levircd/custom_model.yaml b/examples/rs_research/configs/levircd/custom_model.yaml new file mode 100644 index 0000000..07699c4 --- /dev/null +++ b/examples/rs_research/configs/levircd/custom_model.yaml @@ -0,0 +1,6 @@ +_base_: ./levircd.yaml + +save_dir: ./exp/levircd/custom_model/ + +model: !Node + type: CustomTrainer diff --git a/examples/rs_research/custom_model.py b/examples/rs_research/custom_model.py index 63e2f60..bd11198 100644 --- a/examples/rs_research/custom_model.py +++ b/examples/rs_research/custom_model.py @@ -3,7 +3,7 @@ import paddle.nn as nn import paddle.nn.functional as F import paddlers from paddlers.rs_models.cd.layers import Conv3x3, MaxPool2x2, ConvTransposed3x3, Identity -from paddlers.rs_models.cd.layers import ChannelAttention, SpatialAttention +from paddlers.rs_models.cd.layers import ChannelAttention from attach_tools import Attach @@ -15,7 +15,7 @@ class CustomModel(nn.Layer): def __init__(self, in_channels, num_classes, - att_types='cst', + att_types='ct', use_dropout=False): super(CustomModel, self).__init__() @@ -53,7 +53,7 @@ class CustomModel(nn.Layer): self.upconv4 = ConvTransposed3x3(C4, C4, output_padding=1) - self.conv43d = Conv3x3(C5, C4, norm=True, act=True) + self.conv43d = Conv3x3(C5 + C4, C4, norm=True, act=True) self.do43d = self._make_dropout() self.conv42d = Conv3x3(C4, C4, norm=True, act=True) self.do42d = self._make_dropout() @@ -62,7 +62,7 @@ class CustomModel(nn.Layer): self.upconv3 = ConvTransposed3x3(C3, C3, output_padding=1) - self.conv33d = Conv3x3(C4, C3, norm=True, act=True) + self.conv33d = Conv3x3(C4 + C3, C3, norm=True, act=True) self.do33d = self._make_dropout() self.conv32d = Conv3x3(C3, C3, norm=True, act=True) self.do32d = self._make_dropout() @@ -71,32 +71,21 @@ class CustomModel(nn.Layer): self.upconv2 = ConvTransposed3x3(C2, C2, output_padding=1) - self.conv22d = Conv3x3(C3, C2, norm=True, act=True) + self.conv22d = Conv3x3(C3 + C2, C2, norm=True, act=True) self.do22d = self._make_dropout() self.conv21d = Conv3x3(C2, C1, norm=True, act=True) self.do21d = self._make_dropout() self.upconv1 = ConvTransposed3x3(C1, C1, output_padding=1) - self.conv12d = Conv3x3(C2, C1, norm=True, act=True) + self.conv12d = Conv3x3(C2 + C1, C1, norm=True, act=True) self.do12d = self._make_dropout() self.conv11d = Conv3x3(C1, num_classes) - if 'c' in att_types: - self.att_c = ChannelAttention(C4) - else: - self.att_c = Identity() - if 's' in att_types: - self.att_s = SpatialAttention() - else: - self.att_s = Identity() - if 't' in att_types: - self.att_t = ChannelAttention(2, ratio=1) - else: - self.att_t = Identity() - self.init_weight() + self.att4 = MixedAttention(C4, att_types) + def forward(self, t1, t2): # Encode t1 # Stage 1 @@ -144,25 +133,14 @@ class CustomModel(nn.Layer): x43_2 = self.do43(self.conv43(x42)) x4p = self.pool4(x43_2) - # Attend - x43_1 = self.att_c(x43_1) * x43_1 - x43_1 = self.att_s(x43_1) * x43_1 - x43_2 = self.att_c(x43_2) * x43_2 - x43_2 = self.att_s(x43_2) * x43_2 - x43 = paddle.stack([x43_1, x43_2], axis=1) - x43 = paddle.transpose(x43, [0, 2, 1, 3, 4]) - x43 = paddle.flatten(x43, stop_axis=1) - x43 = self.att_t(x43) * x43 - x43 = x43.reshape((x43_1.shape[0], -1, 2, *x43.shape[2:])) - x43_1, x43_2 = x43[:, :, 0], x43[:, :, 1] - # Decode # Stage 4d x4d = self.upconv4(x4p) pad4 = (0, x43_1.shape[3] - x4d.shape[3], 0, x43_1.shape[2] - x4d.shape[2]) x4d = F.pad(x4d, pad=pad4, mode='replicate') - x4d = paddle.concat([x4d, paddle.abs(x43_1 - x43_2)], 1) + x43_1, x43_2 = self.att4(x43_1, x43_2) + x4d = paddle.concat([x4d, x43_1, x43_2], 1) x43d = self.do43d(self.conv43d(x4d)) x42d = self.do42d(self.conv42d(x43d)) x41d = self.do41d(self.conv41d(x42d)) @@ -172,7 +150,7 @@ class CustomModel(nn.Layer): pad3 = (0, x33_1.shape[3] - x3d.shape[3], 0, x33_1.shape[2] - x3d.shape[2]) x3d = F.pad(x3d, pad=pad3, mode='replicate') - x3d = paddle.concat([x3d, paddle.abs(x33_1 - x33_2)], 1) + x3d = paddle.concat([x3d, x33_1, x33_2], 1) x33d = self.do33d(self.conv33d(x3d)) x32d = self.do32d(self.conv32d(x33d)) x31d = self.do31d(self.conv31d(x32d)) @@ -182,7 +160,7 @@ class CustomModel(nn.Layer): pad2 = (0, x22_1.shape[3] - x2d.shape[3], 0, x22_1.shape[2] - x2d.shape[2]) x2d = F.pad(x2d, pad=pad2, mode='replicate') - x2d = paddle.concat([x2d, paddle.abs(x22_1 - x22_2)], 1) + x2d = paddle.concat([x2d, x22_1, x22_2], 1) x22d = self.do22d(self.conv22d(x2d)) x21d = self.do21d(self.conv21d(x22d)) @@ -191,7 +169,7 @@ class CustomModel(nn.Layer): pad1 = (0, x12_1.shape[3] - x1d.shape[3], 0, x12_1.shape[2] - x1d.shape[2]) x1d = F.pad(x1d, pad=pad1, mode='replicate') - x1d = paddle.concat([x1d, paddle.abs(x12_1 - x12_2)], 1) + x1d = paddle.concat([x1d, x12_1, x12_2], 1) x12d = self.do12d(self.conv12d(x1d)) x11d = self.conv11d(x12d) @@ -205,3 +183,51 @@ class CustomModel(nn.Layer): return nn.Dropout2D(p=0.2) else: return Identity() + + +class MixedAttention(nn.Layer): + def __init__(self, in_channels, att_types='ct'): + super(MixedAttention, self).__init__() + + self.att_types = att_types + + if self.has_att_c: + self.att_c = ChannelAttention(in_channels, ratio=1) + self.norm_c1 = nn.BatchNorm(in_channels) + self.norm_c2 = nn.BatchNorm(in_channels) + else: + self.att_c = Identity() + self.norm_c1 = Identity() + self.norm_c2 = Identity() + + if self.has_att_t: + self.att_t = ChannelAttention(2, ratio=1) + else: + self.att_t = Identity() + + def forward(self, x1, x2): + if self.has_att_c: + x1 = self.att_c(x1) * x1 + x1 = self.norm_c1(x1) + x2 = self.att_c(x2) * x2 + x2 = self.norm_c2(x2) + + if self.has_att_t: + b, c = x1.shape[:2] + y = paddle.stack([x1, x2], axis=2) + y = paddle.flatten(y, stop_axis=1) + y = self.att_t(y) * y + y = y.reshape((b, c, 2, *y.shape[2:])) + y1, y2 = y[:, :, 0], y[:, :, 1] + else: + y1, y2 = x1, x2 + + return y1, y2 + + @property + def has_att_c(self): + return 'c' in self.att_types + + @property + def has_att_t(self): + return 't' in self.att_types diff --git a/examples/rs_research/custom_trainer.py b/examples/rs_research/custom_trainer.py index b3ddb41..f0bdb3f 100644 --- a/examples/rs_research/custom_trainer.py +++ b/examples/rs_research/custom_trainer.py @@ -13,7 +13,7 @@ class CustomTrainer(BaseChangeDetector): use_mixed_loss=False, losses=None, in_channels=3, - att_types='cst', + att_types='ct', use_dropout=False, **params): params.update({ diff --git a/examples/rs_research/predict_cd.py b/examples/rs_research/predict_cd.py new file mode 100644 index 0000000..df0b8b4 --- /dev/null +++ b/examples/rs_research/predict_cd.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python + +import argparse +import os +import os.path as osp + +import cv2 +import paddle +import paddlers +from tqdm import tqdm + +import custom_model +import custom_trainer + + +def read_file_list(file_list, sep=' '): + with open(file_list, 'r') as f: + for line in f: + line = line.strip() + parts = line.split(sep) + yield parts + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--model_dir", default=None, type=str, help="Path of saved model.") + parser.add_argument("--data_dir", type=str, help="Path of input dataset.") + parser.add_argument("--file_list", type=str, help="Path of file list.") + parser.add_argument( + "--save_dir", + default='./exp/predict', + type=str, + help="Path of directory to save prediction results.") + parser.add_argument( + "--ext", + default='.png', + type=str, + help="Extension name of the saved image file.") + return parser.parse_args() + + +if __name__ == '__main__': + args = parse_args() + + model = paddlers.tasks.load_model(args.model_dir) + + if not osp.exists(args.save_dir): + os.makedirs(args.save_dir) + + with paddle.no_grad(): + for parts in tqdm(read_file_list(args.file_list)): + im1_path = osp.join(args.data_dir, parts[0]) + im2_path = osp.join(args.data_dir, parts[1]) + + pred = model.predict((im1_path, im2_path)) + cm = pred['label_map'] + # {0,1} -> {0,255} + cm[cm > 0] = 255 + cm = cm.astype('uint8') + + if len(parts) > 2: + name = osp.basename(parts[2]) + else: + name = osp.basename(im1_path) + name = osp.splitext(name)[0] + args.ext + out_path = osp.join(args.save_dir, name) + cv2.imwrite(out_path, cm) diff --git a/examples/rs_research/scripts/run_ablation.sh b/examples/rs_research/scripts/run_ablation.sh index d54066a..b7848ef 100644 --- a/examples/rs_research/scripts/run_ablation.sh +++ b/examples/rs_research/scripts/run_ablation.sh @@ -2,7 +2,7 @@ set -e -CONFIG_DIR='configs/levircd/custom_model' +CONFIG_DIR='configs/levircd/ablation' LOG_DIR='exp/logs/ablation' mkdir -p "${LOG_DIR}" @@ -12,6 +12,9 @@ for config_file in $(ls "${CONFIG_DIR}"/*.yaml); do printf '=%.0s' {1..100} && echo echo -e "\033[33m ${config_file} \033[0m" printf '=%.0s' {1..100} && echo + if [ ${filename} = 'custom_model_cs.yaml' ] || [ ${filename} = 'custom_model_ct.yaml' ]; then + continue + fi python run_task.py train cd --config "${config_file}" 2>&1 | tee "${LOG_DIR}/${filename%.*}.log" echo done diff --git a/examples/rs_research/tools/analyze_model.py b/examples/rs_research/tools/analyze_model.py new file mode 100644 index 0000000..3eec5b4 --- /dev/null +++ b/examples/rs_research/tools/analyze_model.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python + +# Refer to https://github.com/PaddlePaddle/PaddleSeg/blob/release/2.6/tools/analyze_model.py + +import argparse +import os +import os.path as osp +import sys + +import paddle +import numpy as np +import paddlers +from paddle.hapi.dynamic_flops import (count_parameters, register_hooks, + count_io_info) +from paddle.hapi.static_flops import Table + +_dir = osp.dirname(osp.abspath(__file__)) +sys.path.append(osp.abspath(osp.join(_dir, '../'))) +import custom_model +import custom_trainer + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--model_dir", default=None, type=str, help="Path of saved model.") + parser.add_argument( + "--input_shape", + nargs='+', + type=int, + default=[1, 3, 256, 256], + help="Shape of each input tensor.") + return parser.parse_args() + + +def analyze(model, inputs, custom_ops=None, print_detail=False): + handler_collection = [] + types_collection = set() + if custom_ops is None: + custom_ops = {} + + def add_hooks(m): + if len(list(m.children())) > 0: + return + m.register_buffer('total_ops', paddle.zeros([1], dtype='int64')) + m.register_buffer('total_params', paddle.zeros([1], dtype='int64')) + m_type = type(m) + + flops_fn = None + if m_type in custom_ops: + flops_fn = custom_ops[m_type] + if m_type not in types_collection: + print("Customized function has been applied to {}".format( + m_type)) + elif m_type in register_hooks: + flops_fn = register_hooks[m_type] + if m_type not in types_collection: + print("{}'s FLOPs metric has been counted".format(m_type)) + else: + if m_type not in types_collection: + print( + "Cannot find suitable counting function for {}. Treat it as zero FLOPs." + .format(m_type)) + + if flops_fn is not None: + flops_handler = m.register_forward_post_hook(flops_fn) + handler_collection.append(flops_handler) + params_handler = m.register_forward_post_hook(count_parameters) + io_handler = m.register_forward_post_hook(count_io_info) + handler_collection.append(params_handler) + handler_collection.append(io_handler) + types_collection.add(m_type) + + training = model.training + + model.eval() + model.apply(add_hooks) + + with paddle.framework.no_grad(): + model(*inputs) + + total_ops = 0 + total_params = 0 + for m in model.sublayers(): + if len(list(m.children())) > 0: + continue + if set(['total_ops', 'total_params', 'input_shape', + 'output_shape']).issubset(set(list(m._buffers.keys()))): + total_ops += m.total_ops + total_params += m.total_params + + if training: + model.train() + for handler in handler_collection: + handler.remove() + + table = Table( + ["Layer Name", "Input Shape", "Output Shape", "Params(M)", "FLOPs(G)"]) + + for n, m in model.named_sublayers(): + if len(list(m.children())) > 0: + continue + if set(['total_ops', 'total_params', 'input_shape', + 'output_shape']).issubset(set(list(m._buffers.keys()))): + table.add_row([ + m.full_name(), list(m.input_shape.numpy()), + list(m.output_shape.numpy()), + round(float(m.total_params / 1e6), 3), + round(float(m.total_ops / 1e9), 3) + ]) + m._buffers.pop("total_ops") + m._buffers.pop("total_params") + m._buffers.pop('input_shape') + m._buffers.pop('output_shape') + if print_detail: + table.print_table() + print('Total FLOPs: {}G Total Params: {}M'.format( + round(float(total_ops / 1e9), 3), round(float(total_params / 1e6), 3))) + return int(total_ops) + + +if __name__ == '__main__': + args = parse_args() + + # Enforce the use of CPU + paddle.set_device('cpu') + + model = paddlers.tasks.load_model(args.model_dir) + net = model.net + + # Construct bi-temporal inputs + inputs = [paddle.randn(args.input_shape), paddle.randn(args.input_shape)] + + analyze(model.net, inputs) diff --git a/examples/rs_research/tools/collect_imgs.py b/examples/rs_research/tools/collect_imgs.py new file mode 100644 index 0000000..2e7d0ff --- /dev/null +++ b/examples/rs_research/tools/collect_imgs.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python + +import argparse +import os +import os.path as osp +import shutil +from glob import glob + +from tqdm import tqdm + + +def get_subdir_name(src_path): + basename = osp.basename(src_path) + subdir_name, _ = osp.splitext(basename) + return subdir_name + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--mode", + default='copy', + type=str, + choices=['copy', 'link'], + help="Copy or link images.") + parser.add_argument( + "--globs", + nargs='+', + type=str, + help="Glob patterns used to find the images to be copied.") + parser.add_argument( + "--tags", nargs='+', type=str, help="Tags of each source directory.") + parser.add_argument( + "--save_dir", + default='./', + type=str, + help="Path of directory to save collected results.") + return parser.parse_args() + + +if __name__ == '__main__': + args = parse_args() + + if len(args.globs) != len(args.tags): + raise ValueError( + "The number of globs does not match the number of tags!") + + for pat, tag in zip(args.globs, args.tags): + im_paths = glob(pat) + print(f"Glob: {pat}\tTag: {tag}") + for p in tqdm(im_paths): + subdir_name = get_subdir_name(p) + ext = osp.splitext(p)[1] + subdir_path = osp.join(args.save_dir, subdir_name) + subdir_path = osp.abspath(osp.normpath(subdir_path)) + if not osp.exists(subdir_path): + os.makedirs(subdir_path) + if args.mode == 'copy': + shutil.copyfile(p, osp.join(subdir_path, tag + ext)) + elif args.mode == 'link': + os.symlink(p, osp.join(subdir_path, tag + ext)) diff --git a/examples/rs_research/tools/visualize_feats.py b/examples/rs_research/tools/visualize_feats.py new file mode 100644 index 0000000..62ba93c --- /dev/null +++ b/examples/rs_research/tools/visualize_feats.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python + +import argparse +import sys +import os +import os.path as osp +from collections import OrderedDict + +import numpy as np +import cv2 +import paddle +import paddlers + +_dir = osp.dirname(osp.abspath(__file__)) +sys.path.append(osp.abspath(osp.join(_dir, '../'))) +import custom_model +import custom_trainer + +FILENAME_PATTERN = "{key}_{idx}_vis.png" + + +class FeatureContainer: + def __init__(self): + self._dict = OrderedDict() + + def __setitem__(self, key, val): + if key not in self._dict: + self._dict[key] = list() + self._dict[key].append(val) + + def __getitem__(self, key): + return self._dict[key] + + def __repr__(self): + return self._dict.__repr__() + + def items(self): + return self._dict.items() + + def keys(self): + return self._dict.keys() + + def values(self): + return self._dict.values() + + +class HookHelper: + def __init__(self, model, fetch_dict, out_dict, hook_type='forward_out'): + # XXX: A HookHelper object should only be used as a context manager and should not + # persist in memory since it may keep references to some very large objects. + self.model = model + self.fetch_dict = fetch_dict + self.out_dict = out_dict + self._handles = [] + self.hook_type = hook_type + + def __enter__(self): + def _hook_proto(x, entry): + # `x` should be a tensor or a tuple; + # entry is expected to be a string or a non-nested tuple. + if isinstance(entry, tuple): + for key, f in zip(entry, x): + self.out_dict[key] = f.detach().clone() + else: + self.out_dict[entry] = x.detach().clone() + + if self.hook_type == 'forward_in': + # NOTE: Register forward hooks for LAYERs + for name, layer in self.model.named_sublayers(): + if name in self.fetch_dict: + entry = self.fetch_dict[name] + self._handles.append( + layer.register_forward_pre_hook( + lambda l, x, entry=entry: + # x is a tuple + _hook_proto(x[0] if len(x)==1 else x, entry) + ) + ) + elif self.hook_type == 'forward_out': + # NOTE: Register forward hooks for LAYERs. + for name, module in self.model.named_sublayers(): + if name in self.fetch_dict: + entry = self.fetch_dict[name] + self._handles.append( + module.register_forward_post_hook( + lambda l, x, y, entry=entry: + # y is a tensor or a tuple + _hook_proto(y, entry) + ) + ) + elif self.hook_type == 'backward': + # NOTE: Register backward hooks for TENSORs. + for name, param in self.model.named_parameters(): + if name in self.fetch_dict: + entry = self.fetch_dict[name] + self._handles.append( + param.register_hook( + lambda grad, entry=entry: _hook_proto(grad, entry))) + else: + raise RuntimeError("Hook type is not implemented.") + + def __exit__(self, exc_type, exc_val, ext_tb): + for handle in self._handles: + handle.remove() + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--model_dir", default=None, type=str, help="Path of saved model.") + parser.add_argument( + "--hook_type", default='forward_out', type=str, help="Type of hook.") + parser.add_argument( + "--layer_names", + nargs='+', + default=[], + type=str, + help="Layers that accepts or produces the features to visualize.") + parser.add_argument( + "--im_paths", nargs='+', type=str, help="Paths of input images.") + parser.add_argument( + "--save_dir", + type=str, + help="Path of directory to save prediction results.") + parser.add_argument( + "--to_pseudo_color", + action='store_true', + help="Whether to save pseudo-color images.") + parser.add_argument( + "--output_size", + nargs='+', + type=int, + default=None, + help="Resize the visualized image to `output_size`.") + return parser.parse_args() + + +def normalize_minmax(x): + EPS = 1e-32 + return (x - x.min()) / (x.max() - x.min() + EPS) + + +def quantize_8bit(x): + # [0.0,1.0] float => [0,255] uint8 + # or [0,1] int => [0,255] uint8 + return (x * 255).astype('uint8') + + +def to_pseudo_color(gray, color_map=cv2.COLORMAP_JET): + return cv2.applyColorMap(gray, color_map) + + +def process_fetched_feat(feat, to_pcolor=True): + # Convert tensor to array + feat = feat.squeeze(0).numpy() + # Average along channel dimension + feat = normalize_minmax(feat.mean(0)) + feat = quantize_8bit(feat) + if to_pcolor: + feat = to_pseudo_color(feat) + return feat + + +if __name__ == '__main__': + args = parse_args() + + # Load model + model = paddlers.tasks.load_model(args.model_dir) + + fetch_dict = dict(zip(args.layer_names, args.layer_names)) + out_dict = FeatureContainer() + + with HookHelper(model.net, fetch_dict, out_dict, hook_type=args.hook_type): + if len(args.im_paths) == 1: + model.predict(args.im_paths[0]) + else: + if len(args.im_paths) != 2: + raise ValueError + model.predict(tuple(args.im_paths)) + + if not osp.exists(args.save_dir): + os.makedirs(args.save_dir) + + for key, feats in out_dict.items(): + for idx, feat in enumerate(feats): + im_vis = process_fetched_feat(feat, to_pcolor=args.to_pseudo_color) + if args.output_size is not None: + im_vis = cv2.resize(im_vis, tuple(args.output_size)) + out_path = osp.join( + args.save_dir, + FILENAME_PATTERN.format( + key=key.replace('.', '_'), idx=idx)) + cv2.imwrite(out_path, im_vis) From 132df1a8ddf6c232a570b7243f4078e314c5c812 Mon Sep 17 00:00:00 2001 From: Bobholamovic Date: Thu, 25 Aug 2022 11:03:44 +0800 Subject: [PATCH 40/52] Rename custom_model/->ablation --- .../configs/levircd/custom_model/custom_model_c.yaml | 8 -------- .../configs/levircd/custom_model/custom_model_cs.yaml | 8 -------- .../configs/levircd/custom_model/custom_model_cst.yaml | 8 -------- .../configs/levircd/custom_model/custom_model_ct.yaml | 8 -------- .../configs/levircd/custom_model/custom_model_s.yaml | 8 -------- .../configs/levircd/custom_model/custom_model_st.yaml | 8 -------- .../configs/levircd/custom_model/custom_model_t.yaml | 8 -------- 7 files changed, 56 deletions(-) delete mode 100644 examples/rs_research/configs/levircd/custom_model/custom_model_c.yaml delete mode 100644 examples/rs_research/configs/levircd/custom_model/custom_model_cs.yaml delete mode 100644 examples/rs_research/configs/levircd/custom_model/custom_model_cst.yaml delete mode 100644 examples/rs_research/configs/levircd/custom_model/custom_model_ct.yaml delete mode 100644 examples/rs_research/configs/levircd/custom_model/custom_model_s.yaml delete mode 100644 examples/rs_research/configs/levircd/custom_model/custom_model_st.yaml delete mode 100644 examples/rs_research/configs/levircd/custom_model/custom_model_t.yaml diff --git a/examples/rs_research/configs/levircd/custom_model/custom_model_c.yaml b/examples/rs_research/configs/levircd/custom_model/custom_model_c.yaml deleted file mode 100644 index 13db635..0000000 --- a/examples/rs_research/configs/levircd/custom_model/custom_model_c.yaml +++ /dev/null @@ -1,8 +0,0 @@ -_base_: ../levircd.yaml - -save_dir: ./exp/levircd/custom_model/att_c/ - -model: !Node - type: CustomTrainer - args: - att_types: c diff --git a/examples/rs_research/configs/levircd/custom_model/custom_model_cs.yaml b/examples/rs_research/configs/levircd/custom_model/custom_model_cs.yaml deleted file mode 100644 index 63229ee..0000000 --- a/examples/rs_research/configs/levircd/custom_model/custom_model_cs.yaml +++ /dev/null @@ -1,8 +0,0 @@ -_base_: ../levircd.yaml - -save_dir: ./exp/levircd/custom_model/att_cs/ - -model: !Node - type: CustomTrainer - args: - att_types: cs diff --git a/examples/rs_research/configs/levircd/custom_model/custom_model_cst.yaml b/examples/rs_research/configs/levircd/custom_model/custom_model_cst.yaml deleted file mode 100644 index cd2915c..0000000 --- a/examples/rs_research/configs/levircd/custom_model/custom_model_cst.yaml +++ /dev/null @@ -1,8 +0,0 @@ -_base_: ../levircd.yaml - -save_dir: ./exp/levircd/custom_model/att_cst/ - -model: !Node - type: CustomTrainer - args: - att_types: cst diff --git a/examples/rs_research/configs/levircd/custom_model/custom_model_ct.yaml b/examples/rs_research/configs/levircd/custom_model/custom_model_ct.yaml deleted file mode 100644 index 5df0795..0000000 --- a/examples/rs_research/configs/levircd/custom_model/custom_model_ct.yaml +++ /dev/null @@ -1,8 +0,0 @@ -_base_: ../levircd.yaml - -save_dir: ./exp/levircd/custom_model/att_ct/ - -model: !Node - type: CustomTrainer - args: - att_types: ct diff --git a/examples/rs_research/configs/levircd/custom_model/custom_model_s.yaml b/examples/rs_research/configs/levircd/custom_model/custom_model_s.yaml deleted file mode 100644 index 525151e..0000000 --- a/examples/rs_research/configs/levircd/custom_model/custom_model_s.yaml +++ /dev/null @@ -1,8 +0,0 @@ -_base_: ../levircd.yaml - -save_dir: ./exp/levircd/custom_model/att_s/ - -model: !Node - type: CustomTrainer - args: - att_types: s diff --git a/examples/rs_research/configs/levircd/custom_model/custom_model_st.yaml b/examples/rs_research/configs/levircd/custom_model/custom_model_st.yaml deleted file mode 100644 index 6ba149c..0000000 --- a/examples/rs_research/configs/levircd/custom_model/custom_model_st.yaml +++ /dev/null @@ -1,8 +0,0 @@ -_base_: ../levircd.yaml - -save_dir: ./exp/levircd/custom_model/att_st/ - -model: !Node - type: CustomTrainer - args: - att_types: st diff --git a/examples/rs_research/configs/levircd/custom_model/custom_model_t.yaml b/examples/rs_research/configs/levircd/custom_model/custom_model_t.yaml deleted file mode 100644 index 984bd19..0000000 --- a/examples/rs_research/configs/levircd/custom_model/custom_model_t.yaml +++ /dev/null @@ -1,8 +0,0 @@ -_base_: ../levircd.yaml - -save_dir: ./exp/levircd/custom_model/att_t/ - -model: !Node - type: CustomTrainer - args: - att_types: t From fc56e9afbafbd802b88d3419d582293fc24f32aa Mon Sep 17 00:00:00 2001 From: Bobholamovic Date: Thu, 25 Aug 2022 20:55:29 +0800 Subject: [PATCH 41/52] FInish rs_research example --- examples/rs_research/README.md | 205 ++++++++++-------- examples/rs_research/custom_model.py | 12 +- examples/rs_research/params_versus_f1.png | Bin 48900 -> 0 bytes examples/rs_research/scripts/run_ablation.sh | 3 - examples/rs_research/scripts/run_benchmark.sh | 30 +-- examples/rs_research/tools/visualize_feats.py | 29 ++- 6 files changed, 157 insertions(+), 122 deletions(-) delete mode 100644 examples/rs_research/params_versus_f1.png diff --git a/examples/rs_research/README.md b/examples/rs_research/README.md index 715e61b..c7b45cf 100644 --- a/examples/rs_research/README.md +++ b/examples/rs_research/README.md @@ -16,27 +16,24 @@ cd examples/rs_research ## 2 数据准备 -本案例在[LEVIR-CD数据集](https://www.mdpi.com/2072-4292/12/10/1662)[1]和[synthetic images and real season-varying remote sensing images(SVCD)数据集](https://www.int-arch-photogramm-remote-sens-spatial-inf-sci.net/XLII-2/565/2018/isprs-archives-XLII-2-565-2018.pdf)[2]上开展实验。请在[LEVIR-CD数据集下载链接](https://justchenhao.github.io/LEVIR/)和[SVCD数据集下载链接](https://drive.google.com/file/d/1GX656JqqOyBi_Ef0w65kDGVto-nHrNs9/edit)分别下载这两个数据集,解压至本地目录,并执行如下指令: +本案例在[LEVIR-CD数据集](https://www.mdpi.com/2072-4292/12/10/1662)[1]上开展实验。请在[LEVIR-CD数据集下载链接](https://justchenhao.github.io/LEVIR/)下载数据集,解压至本地目录,并执行如下指令: ```bash mkdir data/ python ../../tools/prepare_dataset/prepare_levircd.py \ --in_dataset_dir "{LEVIR-CD数据集存放目录路径}" \ - --out_dataset_dir 'data/levircd' \ + --out_dataset_dir "data/levircd" \ --crop_size 256 \ --crop_stride 256 -python ../../tools/prepare_dataset/prepare_svcd.py \ - --in_dataset_dir "{SVCD数据集存放目录路径}" \ - --out_dataset_dir 'data/svcd' ``` -以上指令利用PaddleRS提供的数据集准备工具完成数据集切分、file list创建等操作。具体而言,对于LEVIR-CD数据集,使用官方的训练/验证/测试集划分,并将原始的`1024x1024`大小的影像切分为无重叠的`256x256`的小块(参考[3]中的做法);对于SVCD数据集,使用官方的训练/验证/测试集划分,不做其它额外处理。 +以上指令利用PaddleRS提供的数据集准备工具完成数据集切分、file list创建等操作。具体而言,使用LEVIR-CD数据集官方的训练/验证/测试集划分,并将原始的`1024x1024`大小的影像切分为无重叠的`256x256`的小块(参考[2]中的做法). ## 3 模型设计 ### 3.1 问题分析与思路拟定 -随着深度学习技术应用的不断深入,近年来,变化检测领域涌现了许多基于全卷积神经网络(fully convolutional network, FCN)的遥感影像变化检测算法。与基于特征和基于影像块的方法相比,基于FCN的方法具有处理效率高、依赖超参数少等优势,但其缺点在于参数量往往较大,因而对训练样本的数量更为依赖。尽管中、大型变化检测数据集的数量与日俱增,训练样本日益丰富,但深度学习变化检测模型的参数量也越来越大。下图显示了从2018年到2021年一些已发表的文献中提出的基于FCN的变化检测模型的参数量与其在SVCD数据集上取得的F1分数(柱状图中bar的高度与模型参数量成正比): +随着深度学习技术应用的不断深入,近年来,变化检测领域涌现了许多基于全卷积神经网络(fully convolutional network, FCN)的遥感影像变化检测算法。与基于特征和基于影像块的方法相比,基于FCN的方法具有处理效率高、依赖超参数少等优势,但其缺点在于参数量往往较大,因而对训练样本的数量更为依赖。尽管中、大型变化检测数据集的数量与日俱增,训练样本日益丰富,但深度学习变化检测模型的参数量也越来越大。下图显示了从2018年到2021年一些已发表的文献中提出的基于FCN的变化检测模型的参数量与其在SVCD数据集[3]上取得的F1分数(柱状图中bar的高度与模型参数量成正比): ![params_versus_f1](params_versus_f1.png) @@ -47,6 +44,12 @@ python ../../tools/prepare_dataset/prepare_svcd.py \ 本案例认为,上述问题的根源在于参数量与数据量的失衡所导致的特征冗余。既然模型的特征存在冗余,也即存在一部分“无用”的特征,是否存在某种手段,能够在固定模型参数量的前提下对特征进行优化,从而“榨取”小模型的更多潜力,获取更多更加有效的特征?基于这个观点,本案例的基本思路是为现有的变化检测模型添加一个“插件式”的特征优化模块,在仅引入较少额外的参数数量的情况下,实现变化特征增强。本案例计划以变化检测领域经典的FC-Siam-conc[4]为baseline网络,利用通道和时间注意力模块对网络的中间层特征进行优化,从而减小特征冗余,提升检测效果。在具体的模块设计方面,选用论文[5]中提出的通道注意力模块实现通道和时间维度的特征增强。 +FC-Siam-conc的网络结构如图所示: + +![fc_siam_conc](fc_siam_conc.png) + +本案例计划在解码器中首个Concat模块之前添加通道与时间注意力模块组合而成的混合注意力模块以优化从编码器传来的特征,并将新模型称为CustomModel。 + ### 3.2 模型定义 本小节基于PaddlePaddle框架与PaddleRS库实现[3.1节](#3.1-问题分析与思路拟定)中提出的想法。 @@ -104,17 +107,11 @@ class MixedAttention(nn.Layer): # 每个注意力模块都是可选的 if self.has_att_c: self.att_c = ChannelAttention(in_channels, ratio=1) - # 在时间注意力模块之后增加归一化层 - # 利用BN层中的可学习参数增强模型的拟合能力 - self.norm_c1 = nn.BatchNorm(in_channels) - self.norm_c2 = nn.BatchNorm(in_channels) else: self.att_c = Identity() - self.norm_c1 = Identity() - self.norm_c2 = Identity() - # 时间注意力模块部分复用通道注意力的逻辑,在`forward()`中将具体解释 if has_att_t: + # 时间注意力模块部分复用通道注意力的逻辑,在`forward()`中将具体解释 self.att_t = ChannelAttention(2, ratio=1) else: self.att_t = Identity() @@ -124,11 +121,10 @@ class MixedAttention(nn.Layer): if self.has_att_c: # 首先使用通道注意力模块对特征进行优化 - # 两个时相的编码特征共享通道注意力模块,但使用各自的归一化层 - x1 = self.att_c(x1) * x1 - x1 = self.norm_c1(x1) - x2 = self.att_c(x2) * x2 - x2 = self.norm_c2(x2) + # 两个时相的编码特征共享通道注意力模块 + # 添加残差连接以加速收敛 + x1 = (1 + self.att_c(x1)) * x1 + x2 = (1 + self.att_c(x2)) * x2 if self.has_att_t: b, c = x1.shape[:2] @@ -138,7 +134,8 @@ class MixedAttention(nn.Layer): # 将b和c两个维度合并,输出tensor形状为[b*c, t, h, w] y = paddle.flatten(y, stop_axis=1) # 此时,时间维度已经替代了原先的通道维度,将四维tensor输入ChannelAttention模块进行处理 - y = self.att_t(y) * y + # 同样添加残差连接 + y = (1 + self.att_t(y)) * y # 从处理结果中分离两个时相的信息 y = y.reshape((b, c, 2, *y.shape[2:])) y1, y2 = y[:, :, 0], y[:, :, 1] @@ -159,10 +156,10 @@ class MixedAttention(nn.Layer): 在编写组网相关代码时请注意以下两点: 1. 所有模型必须为`paddle.nn.Layer`的子类; -2. 包含模型整体逻辑结构的最外层模块须用`@attach`装饰; -3. 对于变化检测任务,`forward()`方法除`self`参数外还接受两个参数`t1`、`t2`,分别表示第一时相和第二时相影像。 +2. 包含模型整体逻辑结构的最外层模块(如本例中的`CustomModel`类)须用`@attach`装饰; +3. 对于变化检测任务,最外层模块的`forward()`方法除`self`参数外还接受两个参数`t1`、`t2`,分别表示第一时相和第二时相影像。 -关于模型定义的更多细节请参考[文档](https://github.com/PaddlePaddle/PaddleRS/blob/develop/docs/dev/dev_guide.md)。 +关于模型定义的更多细节请参考[开发指南](https://github.com/PaddlePaddle/PaddleRS/blob/develop/docs/dev/dev_guide.md)。 #### 3.2.2 自定义训练器 @@ -203,21 +200,21 @@ class CustomTrainer(BaseChangeDetector): ## 4 对比实验 -为了验证模型设计的有效性,通常需要开展对比实验,在一个或多个数据集上比较所提出模型与其它模型的精度和性能。在本案例中,将自定义模型与FC-EF、FC-Siam-diff、FC-Siam-conc三种结构进行比较,这三个模型均来自论文[4]。 +为了验证模型设计的有效性,通常需要开展对比实验,在一个或多个数据集上比较所提出模型与其它模型的精度和性能。在本案例中,将自定义模型CustomModel与FC-EF、FC-Siam-diff、FC-Siam-conc三种结构进行比较,这三个模型均来自论文[4]。 ### 4.1 实验过程 -使用如下指令在LEVIR-CD与SVCD数据集上执行对所有参与对比的模型的训练: +使用如下指令在LEVIR-CD数据集上执行对所有参与对比的模型的训练: ```bash bash scripts/run_benchmark.sh ``` -或者,可以按照以下格式执行对某个模型在某一数据集上的训练: +或者,可以按照以下格式执行对某个模型的训练: ```bash python run_task.py train cd \ - --config "configs/{数据集名称}/{配置文件名称}" \ + --config "configs/levircd/{配置文件名称}" \ 2>&1 | tee "{日志路径}" ``` @@ -225,9 +222,9 @@ python run_task.py train cd \ ```bash python run_task.py eval cd \ - --config "configs/{数据集名称}/{配置文件名称}" \ - --datasets.eval.args.file_list "data/{数据集名称}/test.txt" \ - --resume_checkpoint "exp/{数据集名称}/{模型名称}/best_model" + --config "configs/levircd/{配置文件名称}" \ + --datasets.eval.args.file_list "data/levircd/test.txt" \ + --resume_checkpoint "exp/levircd/{模型名称}/best_model" ``` 训练程序默认开启VisualDL日志记录功能。训练过程中或训练完成后,可使用VisualDL观察损失函数和精度指标的变化情况。在PaddleRS中使用VisualDL的方式请参考[使用教程](https://github.com/PaddlePaddle/PaddleRS/blob/develop/tutorials/train/README.md#visualdl%E5%8F%AF%E8%A7%86%E5%8C%96%E8%AE%AD%E7%BB%83%E6%8C%87%E6%A0%87)。 @@ -236,18 +233,18 @@ python run_task.py eval cd \ ```bash python predict_cd.py \ - --model_dir "exp/{数据集名称}/{模型名称}/best_model" \ - --data_dir "data/{数据集名称}" \ - --file_list "data/{数据集名称}/test.txt" \ - --save_dir "exp/predict/{数据集名称}/{模型名称}" + --model_dir "exp/levircd/{模型名称}/best_model" \ + --data_dir "data/levircd" \ + --file_list "data/levircd/test.txt" \ + --save_dir "exp/predict/levircd/{模型名称}" ``` -之后,可在`exp/predict/{数据集名称}/{模型名称}`目录查看保存的输出结果。 +之后,可在`exp/predict/levircd/{模型名称}`目录查看保存的输出结果。 -可以通过`tools/collect_imgs.py`脚本将输入图像、真值标签以及多个模型的预测结果放置在一个目录下以便于观察比较。该脚本接受三个命令行选项: -- 使用`--globs`指定一系列通配符(可用于Python的[`glob.glob()`函数](https://docs.python.org/zh-cn/3/library/glob.html#glob.glob),用于匹配需要收集的图像; -- 使用`--tags`为`--globs`中的每一项指定一个别名,在存储目录中,相应的图像名将被替换为存储的别名; -- 使用`--save_dir`指定输出目录路径,若目录不存在将被自动创建。 +可以通过`tools/collect_imgs.py`脚本将输入图像、变化标签以及多个模型的预测结果放置在一个目录下以便于观察比较。该脚本接受三个命令行选项: +- `--globs`指定一系列通配符(可用于Python的[`glob.glob()`函数](https://docs.python.org/zh-cn/3/library/glob.html#glob.glob),用于匹配需要收集的图像; +- `--tags`为`--globs`中的每一项指定一个别名,在存储目录中,相应的图像名将被替换为存储的别名; +- `--save_dir`指定输出目录路径,若目录不存在将被自动创建。 例如,对于LEVIR-CD数据集,执行如下指令: @@ -262,74 +259,51 @@ python tools/collect_imgs.py \ --save_dir "exp/collect/levircd" ``` -执行完毕后,可在`exp/collect/levircd`目录中找到两个时相的输入影像、真值标签以及各个模型的预测结果。当新增模型后,可以再次调用`tools/collect_imgs.py`脚本补充结果到`exp/collect/levircd`目录中: +执行完毕后,可在`exp/collect/levircd`目录中找到两个时相的输入影像、变化标签以及各个模型的预测结果。当新增模型后,可以再次调用`tools/collect_imgs.py`脚本补充结果到`exp/collect/levircd`目录中: ```bash python tools/collect_imgs.py --globs "exp/predict/levircd/{新增模型名称}/*.png" --tags '{新增模型名称}' --save_dir "exp/collect/levircd" ``` -对于SVCD数据集,执行如下指令: - -```bash -python tools/collect_imgs.py \ - --globs "data/svcd/ChangeDetectionDataset/Real/subset/test/A/*.jpg" "data/svcd/ChangeDetectionDataset/Real/subset/test/B/*.jpg" "data/svcd/ChangeDetectionDataset/Real/subset/test/OUT/*.jpg" \ - "exp/predict/svcd/fc_ef/*.png" "exp/predict/svcd/fc_siam_conc/*.png" "exp/predict/svcd/fc_siam_diff/*.png" \ - "exp/predict/svcd/custom_model/*.png" \ - --tags 'A' 'B' 'GT' \ - 'fc_ef' 'fc_siam_conc' 'fc_siam_diff' \ - 'custom_model' \ - --save_dir "exp/collect/svcd" -``` - 此外,为了从精度和性能两个方面综合评估变化检测算法,可以通过如下指令计算变化检测模型的[浮点计算数(floating point operations, FLOPs)](https://blog.csdn.net/IT_flying625/article/details/104898152)和模型参数量: ```bash -python tools/analyze_model.py --model_dir "exp/{数据集名称}/{模型名称}/best_model" +python tools/analyze_model.py --model_dir "exp/levircd/{模型名称}/best_model" ``` ### 4.2 实验结果 -本案例使用变化类的[交并比(intersection over union, IoU)](https://paddlepedia.readthedocs.io/en/latest/tutorials/computer_vision/semantic_segmentation/Overview/Overview.html#id6)和[F1分数](https://baike.baidu.com/item/F1%E5%88%86%E6%95%B0/13864979)作为定量评价指标。在每个数据集上,从目视效果和定量指标两个方面对算法效果进行评判。 -#### 4.2.1 LEVIR-CD数据集上的对比结果 +本案例使用变化类的[交并比(intersection over union, IoU)](https://paddlepedia.readthedocs.io/en/latest/tutorials/computer_vision/semantic_segmentation/Overview/Overview.html#id6)和[F1分数](https://baike.baidu.com/item/F1%E5%88%86%E6%95%B0/13864979)作为定量评价指标,这两个指标越高,表示算法的检测效果越好。在每个数据集上,从目视效果和定量指标两个方面对算法效果进行评判。 -**目视效果对比** +#### 4.2.1 目视效果对比 -|时相1影像|时相2影像|FC-EF|FC-Siam-diff|FC-Siam-conc|CustomModel|真值标签| +下图展示了两个时相的输入影像、各算法输出的二值变化图(binary change map)以及变化标签。所选取的样本均来自LEVIR-CD数据集的测试集。 + +|时相1影像|时相2影像|FC-EF|FC-Siam-diff|FC-Siam-conc|CustomModel|变化标签| |:-:|:-:|:-:|:-:|:-:|:-:|:-:| |![]()|![]()|![]()|![]()|![]()|![]()|![]()| +|![]()|![]()|![]()|![]()|![]()|![]()|![]()| -**定量指标对比** +从图中可以看出,虽然结果中仍存在一定程度的漏检与误检,但相比其它算法,CustomModel对变化区域的刻画相对更为准确。 + +#### 4.2.2 定量指标对比 |模型名称|FLOPs(G)|参数量(M)|IoU%|F1%| |:-:|:-:|:-:|:-:|:-:| |FC-EF|3.57|1.35|79.05|88.30| -|FC-Siam-diff|4.71|1.35|81.33|89.70| +|FC-Siam-diff|4.71|1.35|81.33|89.70| |FC-Siam-conc|5.31|1.55|81.31|89.69| -|CustomModel|5.31|1.58|**82.27**|**90.27**| - -#### 4.2.2 SVCD数据集上的对比结果 +|CustomModel|5.31|1.58|**82.14**|**90.19**| -**目视效果对比** +表中最高的精度指标用粗体表示、次高的指标用下划线标示。从表中可以看出,CustomModel取得了所有算法中最高的IoU和F1分数指标(与FC-EF对比IoU增加3.09%,F1增加1.89%),而其相比baseline FC-Siam-conc仅仅引入0.03 M的额外参数量。 -|时相1影像|时相2影像|FC-EF|FC-Siam-diff|FC-Siam-conc|CustomModel|真值标签| -|:-:|:-:|:-:|:-:|:-:|:-:|:-:| -|![]()|![]()|![]()|![]()|![]()|![]()|![]()| - -**定量指标对比** - -|模型名称|FLOPs(G)|参数量(M)|IoU%|F1%| -|:-:|:-:|:-:|:-:|:-:| -|FC-EF|3.57|1.35|84.11|91.37| -|FC-Siam-diff|4.71|1.35|88.75|94.04| -|FC-Siam-conc|5.31|1.55|88.29|93.78| -|CustomModel|5.31|1.58||| ## 5 消融实验 -在科研过程中,为了验证在baseline上所做修改的有效性,常常需要开展消融实验。例如,在本案例中,自定义模型在FC-Siam-conc模型的基础上添加了通道和时间两种注意力模块,因此需要通过消融实验探讨各个注意力模块对最终精度的贡献。具体而言,包括以下4种实验情形(配置文件均存储在`configs/levircd/ablation`目录): +在科研过程中,为了验证在baseline上所做修改的有效性,常常需要开展消融实验。例如,在本案例中,CustomModel在FC-Siam-conc模型的基础上添加了通道和时间两种注意力模块,因此需要通过消融实验探讨各个注意力模块对最终精度的贡献。具体而言,包括以下4种实验情形(配置文件均存储在`configs/levircd/ablation`目录): -1. 基础情况:不使用任何注意力模块,即baseline模型FC-Siam-conc。 -2. 仅添加通道注意力模块,对应的配置文件名称为`custom_model_c.yaml`。 -3. 仅添加时间注意力模块,对应的配置文件名称为`custom_model_t.yaml`。 +1. 基础情况:不使用任何注意力模块,即baseline模型FC-Siam-conc; +2. 仅添加通道注意力模块,对应的配置文件名称为`custom_model_c.yaml`; +3. 仅添加时间注意力模块,对应的配置文件名称为`custom_model_t.yaml`; 4. 标准情况:同时添加通道和时间注意力模块的完整模型。 其中第1和第4个模型,即baseline和完整模型,在[第4节](#4-对比实验)中已经得到了训练、验证和测试。因此,本节只需要关注情形2、3。 @@ -355,7 +329,7 @@ python run_task.py train cd \ ```bash python run_task.py eval cd \ --config "configs/levircd/ablation/{配置文件名称}" \ - --datasets.eval.args.file_list data/levircd/test.txt \ + --datasets.eval.args.file_list "data/levircd/test.txt" \ --resume_checkpoint "exp/levircd/ablation/{消融模型名称}/best_model" ``` @@ -370,32 +344,81 @@ python run_task.py eval cd \ |通道注意力模块|时间注意力模块|IoU%|F1%| |:-:|:-:|:-:|:-:| |||81.31|89.69| -|✓||81.32|89.70| -||✓|81.61|89.88| -|✓|✓|**82.27**|**90.27**| +|✓||81.97|90.09| +||✓|81.59|89.86| +|✓|✓|**82.14**|**90.19**| -其中,最高的指标用粗体表示。从表中数据可知,有限。 +从表中数据可知,无论是通道注意力模块还是时间注意力模块都能对算法的IoU和F1分数指标带来正面贡献,而同时添加两种注意力模块带来的增益是最大的(相比baseline模型IoU增加0.83%,F1分数增加0.50%)。 ## 6 特征可视化实验 -为了更好地探究。 +本节主要对模型的中间特征进行可视化,以进一步验证对baseline模型所做的修改的确实现了增强特征的效果。 + +### 6.1 实验过程 + +通过`tools/visualize_feats.py`脚本实现对模型中间特征的可视化。该脚本接受如下命令行选项: +- `--model_dir`指定需要加载的模型的存储路径。 +- `--im_path`指定输入影像的路径,对于变化检测任务,需要依次指定两幅输入影像的路径。 +- `--save_dir`指定输出目录路径,若目录不存在将被自动创建。 +- `--hook_type`指定抓取的特征类型,有三种取值:当为`forward_in`时,表示抓取指定模块的前向输入特征;当为`forward_out`时,表示抓取指定模块的前向输出特征;当为`backward`时,表示抓取指定参数的梯度。 +- `--layer_names`指定一系列接受或产生需要抓取特征的模块的名称(父模块与子模块间使用`.`分隔)或是模型中权重参数的名称(即[state_dict](https://www.paddlepaddle.org.cn/documentation/docs/zh/api/paddle/load_cn.html)中的key)。 +- `--to_pseudo_color`指定是否将特征图存储为伪彩色图。 +- `--output_size`指定将特征图缩放到的尺寸。 + +`tools/visualize_feats.py`生成的文件遵照`{layer_name}_{j}_vis.png`或`{layer_name}_{i}_{j}_vis.png`格式命名。其中,`{layer_name}`对应`--layer_names`选项中指定的值;`{i}`的数值表示一次抓取到多个输入、输出特征时当前特征所对应的编号;`{j}`的数值在`--hook_type`指定为`forward_in`或`forward_out`时分别表示当前特征图是第几次调用该模块时输入或输出的(模型中的一些模块可能被重复调用,如FC-Siam-conc模型中的`conv4`)。例如,如下指令获取并存储CustomModel模型中`att4`模块的输入与输出特征的可视化结果: + +```bash +IM1_PATH="data/levircd/LEVIR-CD/test/A/test_13/test_13_3.png" +IM2_PATH="data/levircd/LEVIR-CD/test/B/test_13/test_13_3.png" + +python tools/visualize_feats.py \ + --model_dir "exp/levircd/custom_model/best_model" \ + --im_path "${IM1_PATH}" "${IM2_PATH}" \ + --save_dir "exp/vis/test_13_3/in" \ + --hook_type 'forward_in' \ + --layer_names 'att4' \ + --to_pseudo_color \ + --output_size 256 256 + +python tools/visualize_feats.py \ + --model_dir "exp/levircd/custom_model/best_model" \ + --im_path "${IM1_PATH}" "${IM2_PATH}" \ + --save_dir "exp/vis/test_13_3/out" \ + --hook_type 'forward_out' \ + --layer_names 'att4' \ + --to_pseudo_color \ + --output_size 256 256 +``` + +执行上述指令将在`exp/vis/test_13_3/{模型名称}`目录中产生2个子目录,每个子目录中有2个文件,其中`in/att4_0_0_vis.png`和`in/att4_1_0_vis.png`分别表示输入`att4`模块的两个时相特征的可视化结果,`out/att4_0_0_vis.png`和`out/att4_1_0_vis.png`分别表示`att4`模块输出的两个时相特征的可视化结果。 + +### 6.2 实验结果 + +下图从左往右分别为两个时相的输入影像、变化标签、输入混合注意力模块`att4`的两个时相特征图的可视化结果(分别用x1和x2代指)以及`att4`输出的两个时相特征图的可视化结果(分别用y1和y2代指): + +|时相1影像|时相2影像|变化标签|x1|x2|y1|y2| +|:-:|:-:|:-:|:-:|:-:|:-:|:-:| +|||||||| + +对比x2和y2可以看出,经过通道和时间注意力模块处理后,变化特征得到了增强,发生变化的区域在特征图中更加凸显。 ## 5 总结与展望 ### 5.1 总结 -本案例以为经典的FC-Siam-conc模型添加注意力模块为例,演示了使用PaddleRS开展科研实验的典型流程。 -- 精度提升十分有限,算法设计。 +- 本案例以为经典的FC-Siam-conc模型添加注意力模块为例,演示了使用PaddleRS开展科研工作的典型流程。 +- 本案例中对模型的改进带来了一定的目视效果的改善和检测精度提升。 +- 本案例通过消融实验和特征可视化实验证实了所提出改进的有效性。 ### 5.2 展望 - 本案例对所有参与比较的算法使用了相同的训练超参数,但由于模型之间存在差异,使用统一的超参训练往往难以保证所有模型都能取得较好的效果。在后续工作中,可以对每个对比算法进行调参,使其获得最优精度。 -- 本案例只作为 +- 本案例作为使用PaddleRS开展科研工作的简单例子,并未在算法设计上做出较大改进,因此所提出算法相比baseline的精度提升也较为有限。未来可以考虑更复杂的算法设计,以及使用更加先进的模型结构。 ## 参考文献 > [1] Chen, Hao, and Zhenwei Shi. "A spatial-temporal attention-based method and a new dataset for remote sensing image change detection." *Remote Sensing* 12.10 (2020): 1662. -[2] Lebedev, M. A., et al. "CHANGE DETECTION IN REMOTE SENSING IMAGES USING CONDITIONAL ADVERSARIAL NETWORKS." *International Archives of the Photogrammetry, Remote Sensing & Spatial Information Sciences* 42.2 (2018). -[3] Chen, Hao, Zipeng Qi, and Zhenwei Shi. "Remote sensing image change detection with transformers." *IEEE Transactions on Geoscience and Remote Sensing* 60 (2021): 1-14. +[2] Chen, Hao, Zipeng Qi, and Zhenwei Shi. "Remote sensing image change detection with transformers." *IEEE Transactions on Geoscience and Remote Sensing* 60 (2021): 1-14. +[3] Lebedev, M. A., et al. "CHANGE DETECTION IN REMOTE SENSING IMAGES USING CONDITIONAL ADVERSARIAL NETWORKS." *International Archives of the Photogrammetry, Remote Sensing & Spatial Information Sciences* 42.2 (2018). [4] Daudt, Rodrigo Caye, Bertr Le Saux, and Alexandre Boulch. "Fully convolutional siamese networks for change detection." *2018 25th IEEE International Conference on Image Processing (ICIP)*. IEEE, 2018. [5] Woo, Sanghyun, et al. "Cbam: Convolutional block attention module." *Proceedings of the European conference on computer vision (ECCV)*. 2018. diff --git a/examples/rs_research/custom_model.py b/examples/rs_research/custom_model.py index bd11198..b0dbda7 100644 --- a/examples/rs_research/custom_model.py +++ b/examples/rs_research/custom_model.py @@ -193,12 +193,8 @@ class MixedAttention(nn.Layer): if self.has_att_c: self.att_c = ChannelAttention(in_channels, ratio=1) - self.norm_c1 = nn.BatchNorm(in_channels) - self.norm_c2 = nn.BatchNorm(in_channels) else: self.att_c = Identity() - self.norm_c1 = Identity() - self.norm_c2 = Identity() if self.has_att_t: self.att_t = ChannelAttention(2, ratio=1) @@ -207,16 +203,14 @@ class MixedAttention(nn.Layer): def forward(self, x1, x2): if self.has_att_c: - x1 = self.att_c(x1) * x1 - x1 = self.norm_c1(x1) - x2 = self.att_c(x2) * x2 - x2 = self.norm_c2(x2) + x1 = (1 + self.att_c(x1)) * x1 + x2 = (1 + self.att_c(x2)) * x2 if self.has_att_t: b, c = x1.shape[:2] y = paddle.stack([x1, x2], axis=2) y = paddle.flatten(y, stop_axis=1) - y = self.att_t(y) * y + y = (1 + self.att_t(y)) * y y = y.reshape((b, c, 2, *y.shape[2:])) y1, y2 = y[:, :, 0], y[:, :, 1] else: diff --git a/examples/rs_research/params_versus_f1.png b/examples/rs_research/params_versus_f1.png deleted file mode 100644 index f5ba61f44debe0ea4a48e05d09602b3eeb5759f0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 48900 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 | tee "${LOG_DIR}/${filename%.*}.log" echo done diff --git a/examples/rs_research/scripts/run_benchmark.sh b/examples/rs_research/scripts/run_benchmark.sh index 42f7c39..4b1d375 100644 --- a/examples/rs_research/scripts/run_benchmark.sh +++ b/examples/rs_research/scripts/run_benchmark.sh @@ -2,21 +2,21 @@ set -e -for dataset in levircd svcd; do - config_dir="configs/${dataset}" - log_dir="exp/logs/${dataset}" +DATASET='levircd' - mkdir -p "${log_dir}" +config_dir="configs/${DATASET}" +log_dir="exp/logs/${DATASET}" - for config_file in $(ls "${config_dir}"/*.yaml); do - filename="$(basename ${config_file})" - if [ "${filename}" = "${dataset}.yaml" ]; then - continue - fi - printf '=%.0s' {1..100} && echo - echo -e "\033[33m ${config_file} \033[0m" - printf '=%.0s' {1..100} && echo - python run_task.py train cd --config "${config_file}" 2>&1 | tee "${log_dir}/${filename%.*}.log" - echo - done +mkdir -p "${log_dir}" + +for config_file in $(ls "${config_dir}"/*.yaml); do + filename="$(basename ${config_file})" + if [ "${filename}" = "${DATASET}.yaml" ]; then + continue + fi + printf '=%.0s' {1..100} && echo + echo -e "\033[33m ${config_file} \033[0m" + printf '=%.0s' {1..100} && echo + python run_task.py train cd --config "${config_file}" 2>&1 | tee "${log_dir}/${filename%.*}.log" + echo done diff --git a/examples/rs_research/tools/visualize_feats.py b/examples/rs_research/tools/visualize_feats.py index 62ba93c..bf4ed16 100644 --- a/examples/rs_research/tools/visualize_feats.py +++ b/examples/rs_research/tools/visualize_feats.py @@ -10,6 +10,7 @@ import numpy as np import cv2 import paddle import paddlers +from sklearn.decomposition import PCA _dir = osp.dirname(osp.abspath(__file__)) sys.path.append(osp.abspath(osp.join(_dir, '../'))) @@ -45,7 +46,12 @@ class FeatureContainer: class HookHelper: - def __init__(self, model, fetch_dict, out_dict, hook_type='forward_out'): + def __init__(self, + model, + fetch_dict, + out_dict, + hook_type='forward_out', + auto_key=True): # XXX: A HookHelper object should only be used as a context manager and should not # persist in memory since it may keep references to some very large objects. self.model = model @@ -53,6 +59,7 @@ class HookHelper: self.out_dict = out_dict self._handles = [] self.hook_type = hook_type + self.auto_key = auto_key def __enter__(self): def _hook_proto(x, entry): @@ -62,7 +69,12 @@ class HookHelper: for key, f in zip(entry, x): self.out_dict[key] = f.detach().clone() else: - self.out_dict[entry] = x.detach().clone() + if isinstance(x, tuple) and self.auto_key: + for i, f in enumerate(x): + key = self._gen_key(entry, i) + self.out_dict[key] = f.detach().clone() + else: + self.out_dict[entry] = x.detach().clone() if self.hook_type == 'forward_in': # NOTE: Register forward hooks for LAYERs @@ -103,6 +115,9 @@ class HookHelper: for handle in self._handles: handle.remove() + def _gen_key(self, key, i): + return key + f'_{i}' + def parse_args(): parser = argparse.ArgumentParser() @@ -153,8 +168,13 @@ def to_pseudo_color(gray, color_map=cv2.COLORMAP_JET): def process_fetched_feat(feat, to_pcolor=True): # Convert tensor to array feat = feat.squeeze(0).numpy() - # Average along channel dimension - feat = normalize_minmax(feat.mean(0)) + # Get principal component + shape = feat.shape + x = feat.reshape(shape[0], -1).transpose((1, 0)) + pca = PCA(n_components=1) + y = pca.fit_transform(x) + feat = y.reshape(shape[1:]) + feat = normalize_minmax(feat) feat = quantize_8bit(feat) if to_pcolor: feat = to_pseudo_color(feat) @@ -191,3 +211,4 @@ if __name__ == '__main__': FILENAME_PATTERN.format( key=key.replace('.', '_'), idx=idx)) cv2.imwrite(out_path, im_vis) + print(f"Write feature map to {out_path}") From b40367cc97f6e67a2823bb8b7e257eb8eb214394 Mon Sep 17 00:00:00 2001 From: Bobholamovic Date: Thu, 25 Aug 2022 21:15:10 +0800 Subject: [PATCH 42/52] Update image links --- examples/rs_research/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/rs_research/README.md b/examples/rs_research/README.md index c7b45cf..8a98b2b 100644 --- a/examples/rs_research/README.md +++ b/examples/rs_research/README.md @@ -35,7 +35,7 @@ python ../../tools/prepare_dataset/prepare_levircd.py \ 随着深度学习技术应用的不断深入,近年来,变化检测领域涌现了许多基于全卷积神经网络(fully convolutional network, FCN)的遥感影像变化检测算法。与基于特征和基于影像块的方法相比,基于FCN的方法具有处理效率高、依赖超参数少等优势,但其缺点在于参数量往往较大,因而对训练样本的数量更为依赖。尽管中、大型变化检测数据集的数量与日俱增,训练样本日益丰富,但深度学习变化检测模型的参数量也越来越大。下图显示了从2018年到2021年一些已发表的文献中提出的基于FCN的变化检测模型的参数量与其在SVCD数据集[3]上取得的F1分数(柱状图中bar的高度与模型参数量成正比): -![params_versus_f1](params_versus_f1.png) +![params_versus_f1](https://user-images.githubusercontent.com/21275753/186670936-5f79983c-914c-4e81-8f01-11df2beadf09.png) 诚然,增大参数数量在大多数情况下等同于增加模型容量,而模型容量的增加意味着模型拟合能力的提升,从而有助于模型在实验数据集上取得更高的精度指标。但是,“更大”一定意味着“更好”吗?答案显然是否定的。在实际应用中,“更大”的遥感影像变化检测模型常常遭遇如下问题: @@ -46,7 +46,7 @@ python ../../tools/prepare_dataset/prepare_levircd.py \ FC-Siam-conc的网络结构如图所示: -![fc_siam_conc](fc_siam_conc.png) +![fc_siam_conc](https://user-images.githubusercontent.com/21275753/186671480-d869a500-6409-4f97-b48b-50ce95ea3a71.jpg) 本案例计划在解码器中首个Concat模块之前添加通道与时间注意力模块组合而成的混合注意力模块以优化从编码器传来的特征,并将新模型称为CustomModel。 @@ -281,8 +281,8 @@ python tools/analyze_model.py --model_dir "exp/levircd/{模型名称}/best_model |时相1影像|时相2影像|FC-EF|FC-Siam-diff|FC-Siam-conc|CustomModel|变化标签| |:-:|:-:|:-:|:-:|:-:|:-:|:-:| -|![]()|![]()|![]()|![]()|![]()|![]()|![]()| -|![]()|![]()|![]()|![]()|![]()|![]()|![]()| +|![](https://user-images.githubusercontent.com/21275753/186671764-2dc990a8-b297-43a2-ae81-e31f2d5582e5.png)|![](https://user-images.githubusercontent.com/21275753/186672204-e8e46e9a-7f29-4506-9ed4-31314284a6fb.png)|![](https://user-images.githubusercontent.com/21275753/186672237-ee5f67d8-8966-457d-8a80-0452bdb7af89.png)|![](https://user-images.githubusercontent.com/21275753/186671987-7da0023a-0c96-413f-9088-0f6730ab54dd.png)|![](https://user-images.githubusercontent.com/21275753/186671895-c6c40196-b86a-49d1-a4b0-48a7f40cba06.png)|![](https://user-images.githubusercontent.com/21275753/186672068-89a60f8c-c80e-4f73-bb3e-b9ad146e795d.png)|![](https://user-images.githubusercontent.com/21275753/186672106-37e8dcd0-b0f0-46e1-90a1-bd5f566ef97b.png)| +|![](https://user-images.githubusercontent.com/21275753/186672287-efa1209d-2786-4543-b136-5f50b7b0dd8c.png)|![](https://user-images.githubusercontent.com/21275753/186671791-beb82760-8c3f-480f-8ada-9c1081860691.png)|![](https://user-images.githubusercontent.com/21275753/186671861-7b7989e4-15d8-4342-9abe-2d6efa82811a.png)|![](https://user-images.githubusercontent.com/21275753/186672362-94993c68-7c31-4501-b009-755c193a00a8.png)|![](https://user-images.githubusercontent.com/21275753/186672348-3134129c-e2cd-4011-8894-901ef332a43d.png)|![](https://user-images.githubusercontent.com/21275753/186672415-da3984b2-0354-49ad-8dba-9c796a18d282.png)|![](https://user-images.githubusercontent.com/21275753/186672449-fd225e4f-ac58-4506-8b66-3a255567998a.png)| 从图中可以看出,虽然结果中仍存在一定程度的漏检与误检,但相比其它算法,CustomModel对变化区域的刻画相对更为准确。 @@ -398,7 +398,7 @@ python tools/visualize_feats.py \ |时相1影像|时相2影像|变化标签|x1|x2|y1|y2| |:-:|:-:|:-:|:-:|:-:|:-:|:-:| -|||||||| +|![](https://user-images.githubusercontent.com/21275753/186672741-45c819f0-2591-4b97-ad32-05d787be1a0a.png)|![](https://user-images.githubusercontent.com/21275753/186672761-eb6958be-688d-4bc2-839b-6a60cb6cc3b5.png)|![](https://user-images.githubusercontent.com/21275753/186672791-ceb78cf7-5029-4991-88c2-6c4550fb27d8.png)|![](https://user-images.githubusercontent.com/21275753/186672835-7fda3499-33e0-4af1-b990-8d82f6c5c410.png)|![](https://user-images.githubusercontent.com/21275753/186672870-dba57441-509f-4cd0-bcc9-af343ddf07df.png)|![](https://user-images.githubusercontent.com/21275753/186672893-7bc692a7-c963-4686-b93c-895b5c51fecb.png)|![](https://user-images.githubusercontent.com/21275753/186672914-b99ffee3-9eb4-4f95-96f4-93cb00e0b109.png)| 对比x2和y2可以看出,经过通道和时间注意力模块处理后,变化特征得到了增强,发生变化的区域在特征图中更加凸显。 From 2b17dbf4e4fb89c11b0f21c797066bd59528f8c7 Mon Sep 17 00:00:00 2001 From: Lin Manhui Date: Thu, 25 Aug 2022 21:57:56 +0800 Subject: [PATCH 43/52] Update README.md --- examples/rs_research/README.md | 53 +++++++++++++++++----------------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/examples/rs_research/README.md b/examples/rs_research/README.md index 8a98b2b..b944ca5 100644 --- a/examples/rs_research/README.md +++ b/examples/rs_research/README.md @@ -1,6 +1,6 @@ # PaddleRS科研实战:设计深度学习变化检测模型 -本案例演示如何使用PaddleRS设计变化检测模型,并开展消融实验和对比实验。 +本案例演示如何使用PaddleRS设计变化检测模型,并开展对比实验、消融实验和特征可视化实验。 ## 1 环境配置 @@ -35,28 +35,32 @@ python ../../tools/prepare_dataset/prepare_levircd.py \ 随着深度学习技术应用的不断深入,近年来,变化检测领域涌现了许多基于全卷积神经网络(fully convolutional network, FCN)的遥感影像变化检测算法。与基于特征和基于影像块的方法相比,基于FCN的方法具有处理效率高、依赖超参数少等优势,但其缺点在于参数量往往较大,因而对训练样本的数量更为依赖。尽管中、大型变化检测数据集的数量与日俱增,训练样本日益丰富,但深度学习变化检测模型的参数量也越来越大。下图显示了从2018年到2021年一些已发表的文献中提出的基于FCN的变化检测模型的参数量与其在SVCD数据集[3]上取得的F1分数(柱状图中bar的高度与模型参数量成正比): -![params_versus_f1](https://user-images.githubusercontent.com/21275753/186670936-5f79983c-914c-4e81-8f01-11df2beadf09.png) +

+ +

诚然,增大参数数量在大多数情况下等同于增加模型容量,而模型容量的增加意味着模型拟合能力的提升,从而有助于模型在实验数据集上取得更高的精度指标。但是,“更大”一定意味着“更好”吗?答案显然是否定的。在实际应用中,“更大”的遥感影像变化检测模型常常遭遇如下问题: 1. 巨大的参数量意味着巨大的存储开销。在许多实际场景中,硬件资源往往是有限的,过多的模型参数将给部署造成困难。 -2. 在数据有限的情况下,大模型更易遭受过拟合,其在实验数据集上看起来良好的结果也难以泛化到真实场景。 +2. 在数据有限的情况下,大模型更易遭受过拟合,其在实验数据集上看起来良好的检测效果也难以泛化到真实场景。 -本案例认为,上述问题的根源在于参数量与数据量的失衡所导致的特征冗余。既然模型的特征存在冗余,也即存在一部分“无用”的特征,是否存在某种手段,能够在固定模型参数量的前提下对特征进行优化,从而“榨取”小模型的更多潜力,获取更多更加有效的特征?基于这个观点,本案例的基本思路是为现有的变化检测模型添加一个“插件式”的特征优化模块,在仅引入较少额外的参数数量的情况下,实现变化特征增强。本案例计划以变化检测领域经典的FC-Siam-conc[4]为baseline网络,利用通道和时间注意力模块对网络的中间层特征进行优化,从而减小特征冗余,提升检测效果。在具体的模块设计方面,选用论文[5]中提出的通道注意力模块实现通道和时间维度的特征增强。 +本案例认为,上述问题的根源在于参数量与数据量的失衡所导致的特征冗余。既然模型的特征存在冗余,也即存在一部分“无用”的特征,是否存在某种手段,能够在固定模型参数量的前提下对特征进行优化,从而“榨取”小模型的更多潜力,获取更多更加有效的特征?基于这个观点,本案例的基本思路是为现有的变化检测模型添加一个“插件式”的特征优化模块,在仅引入较少额外的参数数量的情况下,实现变化特征增强。本案例计划以变化检测领域经典的FC-Siam-conc[4]为基线(baseline)网络,利用通道和时间注意力模块对网络的中间层特征进行优化,从而减小特征冗余,提升检测效果。在具体的模块设计方面,选用论文[5]中提出的通道注意力模块实现通道和时间维度的特征增强。 FC-Siam-conc的网络结构如图所示: -![fc_siam_conc](https://user-images.githubusercontent.com/21275753/186671480-d869a500-6409-4f97-b48b-50ce95ea3a71.jpg) +

+ +

本案例计划在解码器中首个Concat模块之前添加通道与时间注意力模块组合而成的混合注意力模块以优化从编码器传来的特征,并将新模型称为CustomModel。 ### 3.2 模型定义 -本小节基于PaddlePaddle框架与PaddleRS库实现[3.1节](#3.1-问题分析与思路拟定)中提出的想法。 +本小节基于PaddlePaddle框架与PaddleRS库实现[3.1节](#31-问题分析与思路拟定)中提出的想法。 #### 3.2.1 自定义模型组网 -在`custom_model.py`中定义模型的宏观(macro)结构以及组成模型的各个微观(micro)模块。本案例在`custom_model.py`中定义了改进后的FC-Siam-conc结构,其核心部分实现如下: +在`custom_model.py`中定义模型的整体结构以及组成模型的各个模块。本案例在`custom_model.py`中定义了改进后的FC-Siam-conc结构,其核心部分实现如下: ```python ... @@ -103,7 +107,6 @@ class MixedAttention(nn.Layer): self.att_types = att_types - # 从`att_types`参数中获取要使用的注意力类型 # 每个注意力模块都是可选的 if self.has_att_c: self.att_c = ChannelAttention(in_channels, ratio=1) @@ -159,7 +162,7 @@ class MixedAttention(nn.Layer): 2. 包含模型整体逻辑结构的最外层模块(如本例中的`CustomModel`类)须用`@attach`装饰; 3. 对于变化检测任务,最外层模块的`forward()`方法除`self`参数外还接受两个参数`t1`、`t2`,分别表示第一时相和第二时相影像。 -关于模型定义的更多细节请参考[开发指南](https://github.com/PaddlePaddle/PaddleRS/blob/develop/docs/dev/dev_guide.md)。 +关于模型定义的更多细节请参考[《开发指南》](https://github.com/PaddlePaddle/PaddleRS/blob/develop/docs/dev/dev_guide.md)。 #### 3.2.2 自定义训练器 @@ -196,7 +199,7 @@ class CustomTrainer(BaseChangeDetector): 在本案例中,仅仅重写了训练器的`__init__()`方法。在实际科研过程中,可以通过重写`train()`、`evaluate()`、`default_loss()`等方法定制更加复杂的训练、评估策略或更换默认损失函数。 -关于训练器的更多细节请参考[API文档](https://github.com/PaddlePaddle/PaddleRS/blob/develop/docs/apis/train.md)。 +关于训练器的更多细节请参考[《API文档》](https://github.com/PaddlePaddle/PaddleRS/blob/develop/docs/apis/train.md)。 ## 4 对比实验 @@ -242,8 +245,8 @@ python predict_cd.py \ 之后,可在`exp/predict/levircd/{模型名称}`目录查看保存的输出结果。 可以通过`tools/collect_imgs.py`脚本将输入图像、变化标签以及多个模型的预测结果放置在一个目录下以便于观察比较。该脚本接受三个命令行选项: -- `--globs`指定一系列通配符(可用于Python的[`glob.glob()`函数](https://docs.python.org/zh-cn/3/library/glob.html#glob.glob),用于匹配需要收集的图像; -- `--tags`为`--globs`中的每一项指定一个别名,在存储目录中,相应的图像名将被替换为存储的别名; +- `--globs`指定一系列通配符(可用于Python的[`glob.glob()`函数](https://docs.python.org/zh-cn/3/library/glob.html#glob.glob)),用于匹配需要收集的图像; +- `--tags`为`--globs`中的每一项指定一个别名,在存储目录中,相应的图像名将被替换为指定的别名; - `--save_dir`指定输出目录路径,若目录不存在将被自动创建。 例如,对于LEVIR-CD数据集,执行如下指令: @@ -281,8 +284,8 @@ python tools/analyze_model.py --model_dir "exp/levircd/{模型名称}/best_model |时相1影像|时相2影像|FC-EF|FC-Siam-diff|FC-Siam-conc|CustomModel|变化标签| |:-:|:-:|:-:|:-:|:-:|:-:|:-:| -|![](https://user-images.githubusercontent.com/21275753/186671764-2dc990a8-b297-43a2-ae81-e31f2d5582e5.png)|![](https://user-images.githubusercontent.com/21275753/186672204-e8e46e9a-7f29-4506-9ed4-31314284a6fb.png)|![](https://user-images.githubusercontent.com/21275753/186672237-ee5f67d8-8966-457d-8a80-0452bdb7af89.png)|![](https://user-images.githubusercontent.com/21275753/186671987-7da0023a-0c96-413f-9088-0f6730ab54dd.png)|![](https://user-images.githubusercontent.com/21275753/186671895-c6c40196-b86a-49d1-a4b0-48a7f40cba06.png)|![](https://user-images.githubusercontent.com/21275753/186672068-89a60f8c-c80e-4f73-bb3e-b9ad146e795d.png)|![](https://user-images.githubusercontent.com/21275753/186672106-37e8dcd0-b0f0-46e1-90a1-bd5f566ef97b.png)| -|![](https://user-images.githubusercontent.com/21275753/186672287-efa1209d-2786-4543-b136-5f50b7b0dd8c.png)|![](https://user-images.githubusercontent.com/21275753/186671791-beb82760-8c3f-480f-8ada-9c1081860691.png)|![](https://user-images.githubusercontent.com/21275753/186671861-7b7989e4-15d8-4342-9abe-2d6efa82811a.png)|![](https://user-images.githubusercontent.com/21275753/186672362-94993c68-7c31-4501-b009-755c193a00a8.png)|![](https://user-images.githubusercontent.com/21275753/186672348-3134129c-e2cd-4011-8894-901ef332a43d.png)|![](https://user-images.githubusercontent.com/21275753/186672415-da3984b2-0354-49ad-8dba-9c796a18d282.png)|![](https://user-images.githubusercontent.com/21275753/186672449-fd225e4f-ac58-4506-8b66-3a255567998a.png)| +|||||||| +|||||||| 从图中可以看出,虽然结果中仍存在一定程度的漏检与误检,但相比其它算法,CustomModel对变化区域的刻画相对更为准确。 @@ -291,15 +294,15 @@ python tools/analyze_model.py --model_dir "exp/levircd/{模型名称}/best_model |模型名称|FLOPs(G)|参数量(M)|IoU%|F1%| |:-:|:-:|:-:|:-:|:-:| |FC-EF|3.57|1.35|79.05|88.30| -|FC-Siam-diff|4.71|1.35|81.33|89.70| +|FC-Siam-diff|4.71|1.35|81.33|89.70| |FC-Siam-conc|5.31|1.55|81.31|89.69| |CustomModel|5.31|1.58|**82.14**|**90.19**| -表中最高的精度指标用粗体表示、次高的指标用下划线标示。从表中可以看出,CustomModel取得了所有算法中最高的IoU和F1分数指标(与FC-EF对比IoU增加3.09%,F1增加1.89%),而其相比baseline FC-Siam-conc仅仅引入0.03 M的额外参数量。 +最高的精度指标用粗体表示。从表中可以看出,CustomModel取得了所有算法中最高的IoU和F1分数指标(与FC-EF对比IoU增加3.09%,F1增加1.89%),而其相比baseline模型FC-Siam-conc仅仅引入0.03 M的额外参数量。 ## 5 消融实验 -在科研过程中,为了验证在baseline上所做修改的有效性,常常需要开展消融实验。例如,在本案例中,CustomModel在FC-Siam-conc模型的基础上添加了通道和时间两种注意力模块,因此需要通过消融实验探讨各个注意力模块对最终精度的贡献。具体而言,包括以下4种实验情形(配置文件均存储在`configs/levircd/ablation`目录): +在科研过程中,为了验证在baseline上所做修改的有效性,常常需要开展消融实验。在本案例中,CustomModel在FC-Siam-conc模型的基础上添加了通道和时间两种注意力模块,因此需要通过消融实验探讨各个注意力模块对最终精度的贡献。具体而言,包括以下4种实验情形(消融模型相关的配置文件存储在`configs/levircd/ablation`目录): 1. 基础情况:不使用任何注意力模块,即baseline模型FC-Siam-conc; 2. 仅添加通道注意力模块,对应的配置文件名称为`custom_model_c.yaml`; @@ -335,8 +338,6 @@ python run_task.py eval cd \ 注意,形如`custom_model_c.yaml`的配置文件默认对应的消融模型名称为`att_c`。 -训练程序默认开启VisualDL日志记录功能。训练过程中或训练完成后,可使用VisualDL观察损失函数和精度指标的变化情况。在PaddleRS中使用VisualDL的方式请参考[使用教程](https://github.com/PaddlePaddle/PaddleRS/blob/develop/tutorials/train/README.md#visualdl%E5%8F%AF%E8%A7%86%E5%8C%96%E8%AE%AD%E7%BB%83%E6%8C%87%E6%A0%87)。 - ### 5.2 实验结果 实验得到的定量指标如下表所示: @@ -344,7 +345,7 @@ python run_task.py eval cd \ |通道注意力模块|时间注意力模块|IoU%|F1%| |:-:|:-:|:-:|:-:| |||81.31|89.69| -|✓||81.97|90.09| +|✓||81.97|90.09| ||✓|81.59|89.86| |✓|✓|**82.14**|**90.19**| @@ -352,7 +353,7 @@ python run_task.py eval cd \ ## 6 特征可视化实验 -本节主要对模型的中间特征进行可视化,以进一步验证对baseline模型所做的修改的确实现了增强特征的效果。 +本节主要对模型的中间特征进行可视化,以进一步验证对baseline模型所做的修改是否实现了增强特征的效果。 ### 6.1 实验过程 @@ -398,19 +399,19 @@ python tools/visualize_feats.py \ |时相1影像|时相2影像|变化标签|x1|x2|y1|y2| |:-:|:-:|:-:|:-:|:-:|:-:|:-:| -|![](https://user-images.githubusercontent.com/21275753/186672741-45c819f0-2591-4b97-ad32-05d787be1a0a.png)|![](https://user-images.githubusercontent.com/21275753/186672761-eb6958be-688d-4bc2-839b-6a60cb6cc3b5.png)|![](https://user-images.githubusercontent.com/21275753/186672791-ceb78cf7-5029-4991-88c2-6c4550fb27d8.png)|![](https://user-images.githubusercontent.com/21275753/186672835-7fda3499-33e0-4af1-b990-8d82f6c5c410.png)|![](https://user-images.githubusercontent.com/21275753/186672870-dba57441-509f-4cd0-bcc9-af343ddf07df.png)|![](https://user-images.githubusercontent.com/21275753/186672893-7bc692a7-c963-4686-b93c-895b5c51fecb.png)|![](https://user-images.githubusercontent.com/21275753/186672914-b99ffee3-9eb4-4f95-96f4-93cb00e0b109.png)| +|||||||| 对比x2和y2可以看出,经过通道和时间注意力模块处理后,变化特征得到了增强,发生变化的区域在特征图中更加凸显。 -## 5 总结与展望 +## 7 总结与展望 -### 5.1 总结 +### 7.1 总结 - 本案例以为经典的FC-Siam-conc模型添加注意力模块为例,演示了使用PaddleRS开展科研工作的典型流程。 -- 本案例中对模型的改进带来了一定的目视效果的改善和检测精度提升。 +- 本案例中对模型的改进带来了一定的目视效果的改善和检测精度的提升。 - 本案例通过消融实验和特征可视化实验证实了所提出改进的有效性。 -### 5.2 展望 +### 7.2 展望 - 本案例对所有参与比较的算法使用了相同的训练超参数,但由于模型之间存在差异,使用统一的超参训练往往难以保证所有模型都能取得较好的效果。在后续工作中,可以对每个对比算法进行调参,使其获得最优精度。 - 本案例作为使用PaddleRS开展科研工作的简单例子,并未在算法设计上做出较大改进,因此所提出算法相比baseline的精度提升也较为有限。未来可以考虑更复杂的算法设计,以及使用更加先进的模型结构。 From 3d942f9868ae85289ba4c14aed5465eba34104c4 Mon Sep 17 00:00:00 2001 From: Bobholamovic Date: Thu, 25 Aug 2022 22:11:02 +0800 Subject: [PATCH 44/52] Revert "Add TIPC whole_train_whole_infer" This reverts commit 0f06d5d1cec46960fe37ac3f4c09b2af9c778c78. --- paddlers/tasks/change_detector.py | 4 +- test_tipc/configs/cd/bit/bit.yaml | 8 +++ test_tipc/configs/cd/bit/bit_airchange.yaml | 2 +- test_tipc/configs/cd/bit/bit_levircd.yaml | 2 +- .../configs/cd/bit/train_infer_python.txt | 2 +- .../configs/cd/cdnet/cdnet_airchange.yaml | 8 --- test_tipc/configs/cd/cdnet/cdnet_levircd.yaml | 8 --- .../configs/cd/cdnet/train_infer_python.txt | 53 ------------------- ...ormer_airchange.yaml => changeformer.yaml} | 2 +- .../cd/changeformer/changeformer_levircd.yaml | 8 --- .../cd/changeformer/train_infer_python.txt | 10 ++-- .../configs/cd/dsamnet/dsamnet_airchange.yaml | 8 --- .../configs/cd/dsamnet/dsamnet_levircd.yaml | 8 --- .../configs/cd/dsamnet/train_infer_python.txt | 53 ------------------- .../configs/cd/dsifn/dsifn_airchange.yaml | 8 --- test_tipc/configs/cd/dsifn/dsifn_levircd.yaml | 8 --- .../configs/cd/dsifn/train_infer_python.txt | 53 ------------------- .../configs/cd/fc_ef/fc_ef_airchange.yaml | 8 --- test_tipc/configs/cd/fc_ef/fc_ef_levircd.yaml | 8 --- .../configs/cd/fc_ef/train_infer_python.txt | 53 ------------------- .../fc_siam_conc/fc_siam_conc_airchange.yaml | 8 --- .../cd/fc_siam_conc/fc_siam_conc_levircd.yaml | 8 --- .../cd/fc_siam_conc/train_infer_python.txt | 53 ------------------- .../fc_siam_diff/fc_siam_diff_airchange.yaml | 8 --- .../cd/fc_siam_diff/fc_siam_diff_levircd.yaml | 8 --- .../cd/fc_siam_diff/train_infer_python.txt | 53 ------------------- .../configs/cd/snunet/snunet_airchange.yaml | 8 --- .../configs/cd/snunet/snunet_levircd.yaml | 8 --- .../configs/cd/snunet/train_infer_python.txt | 53 ------------------- .../configs/cd/stanet/stanet_airchange.yaml | 8 --- .../configs/cd/stanet/stanet_levircd.yaml | 8 --- .../configs/cd/stanet/train_infer_python.txt | 53 ------------------- .../hrnet/{hrnet_ucmerced.yaml => hrnet.yaml} | 2 +- .../configs/clas/hrnet/train_infer_python.txt | 6 +-- test_tipc/infer.py | 12 +---- test_tipc/prepare.sh | 2 - tutorials/train/README.md | 4 +- 37 files changed, 28 insertions(+), 588 deletions(-) create mode 100644 test_tipc/configs/cd/bit/bit.yaml delete mode 100644 test_tipc/configs/cd/cdnet/cdnet_airchange.yaml delete mode 100644 test_tipc/configs/cd/cdnet/cdnet_levircd.yaml delete mode 100644 test_tipc/configs/cd/cdnet/train_infer_python.txt rename test_tipc/configs/cd/changeformer/{changeformer_airchange.yaml => changeformer.yaml} (54%) delete mode 100644 test_tipc/configs/cd/changeformer/changeformer_levircd.yaml delete mode 100644 test_tipc/configs/cd/dsamnet/dsamnet_airchange.yaml delete mode 100644 test_tipc/configs/cd/dsamnet/dsamnet_levircd.yaml delete mode 100644 test_tipc/configs/cd/dsamnet/train_infer_python.txt delete mode 100644 test_tipc/configs/cd/dsifn/dsifn_airchange.yaml delete mode 100644 test_tipc/configs/cd/dsifn/dsifn_levircd.yaml delete mode 100644 test_tipc/configs/cd/dsifn/train_infer_python.txt delete mode 100644 test_tipc/configs/cd/fc_ef/fc_ef_airchange.yaml delete mode 100644 test_tipc/configs/cd/fc_ef/fc_ef_levircd.yaml delete mode 100644 test_tipc/configs/cd/fc_ef/train_infer_python.txt delete mode 100644 test_tipc/configs/cd/fc_siam_conc/fc_siam_conc_airchange.yaml delete mode 100644 test_tipc/configs/cd/fc_siam_conc/fc_siam_conc_levircd.yaml delete mode 100644 test_tipc/configs/cd/fc_siam_conc/train_infer_python.txt delete mode 100644 test_tipc/configs/cd/fc_siam_diff/fc_siam_diff_airchange.yaml delete mode 100644 test_tipc/configs/cd/fc_siam_diff/fc_siam_diff_levircd.yaml delete mode 100644 test_tipc/configs/cd/fc_siam_diff/train_infer_python.txt delete mode 100644 test_tipc/configs/cd/snunet/snunet_airchange.yaml delete mode 100644 test_tipc/configs/cd/snunet/snunet_levircd.yaml delete mode 100644 test_tipc/configs/cd/snunet/train_infer_python.txt delete mode 100644 test_tipc/configs/cd/stanet/stanet_airchange.yaml delete mode 100644 test_tipc/configs/cd/stanet/stanet_levircd.yaml delete mode 100644 test_tipc/configs/cd/stanet/train_infer_python.txt rename test_tipc/configs/clas/hrnet/{hrnet_ucmerced.yaml => hrnet.yaml} (62%) diff --git a/paddlers/tasks/change_detector.py b/paddlers/tasks/change_detector.py index 60ff25e..9af34f8 100644 --- a/paddlers/tasks/change_detector.py +++ b/paddlers/tasks/change_detector.py @@ -52,7 +52,9 @@ class BaseChangeDetector(BaseModel): if 'with_net' in self.init_params: del self.init_params['with_net'] super(BaseChangeDetector, self).__init__('change_detector') - + if model_name not in __all__: + raise ValueError("ERROR: There is no model named {}.".format( + model_name)) self.model_name = model_name self.num_classes = num_classes self.use_mixed_loss = use_mixed_loss diff --git a/test_tipc/configs/cd/bit/bit.yaml b/test_tipc/configs/cd/bit/bit.yaml new file mode 100644 index 0000000..3d3c62b --- /dev/null +++ b/test_tipc/configs/cd/bit/bit.yaml @@ -0,0 +1,8 @@ +# Basic configurations of BIT + +_base_: ../_base_/airchange.yaml + +save_dir: ./test_tipc/output/cd/bit/ + +model: !Node + type: BIT \ No newline at end of file diff --git a/test_tipc/configs/cd/bit/bit_airchange.yaml b/test_tipc/configs/cd/bit/bit_airchange.yaml index 27e0bb4..efd6fbb 100644 --- a/test_tipc/configs/cd/bit/bit_airchange.yaml +++ b/test_tipc/configs/cd/bit/bit_airchange.yaml @@ -1,4 +1,4 @@ -# Configurations of BIT with AirChange dataset +# Basic configurations of BIT with AirChange dataset _base_: ../_base_/airchange.yaml diff --git a/test_tipc/configs/cd/bit/bit_levircd.yaml b/test_tipc/configs/cd/bit/bit_levircd.yaml index d9a5dd9..8008901 100644 --- a/test_tipc/configs/cd/bit/bit_levircd.yaml +++ b/test_tipc/configs/cd/bit/bit_levircd.yaml @@ -1,4 +1,4 @@ -# Configurations of BIT with LEVIR-CD dataset +# Basic configurations of BIT with LEVIR-CD dataset _base_: ../_base_/levircd.yaml diff --git a/test_tipc/configs/cd/bit/train_infer_python.txt b/test_tipc/configs/cd/bit/train_infer_python.txt index 3cd2de1..33ee2f3 100644 --- a/test_tipc/configs/cd/bit/train_infer_python.txt +++ b/test_tipc/configs/cd/bit/train_infer_python.txt @@ -6,7 +6,7 @@ use_gpu:null|null --precision:null --num_epochs:lite_train_lite_infer=5|lite_train_whole_infer=5|whole_train_whole_infer=10 --save_dir:adaptive ---train_batch_size:lite_train_lite_infer=4|lite_train_whole_infer=4|whole_train_whole_infer=8 +--train_batch_size:lite_train_lite_infer=4|lite_train_whole_infer=4|whole_train_whole_infer=4 --model_path:null --config:lite_train_lite_infer=./test_tipc/configs/cd/bit/bit_airchange.yaml|lite_train_whole_infer=./test_tipc/configs/cd/bit/bit_airchange.yaml|whole_train_whole_infer=./test_tipc/configs/cd/bit/bit_levircd.yaml train_model_name:best_model diff --git a/test_tipc/configs/cd/cdnet/cdnet_airchange.yaml b/test_tipc/configs/cd/cdnet/cdnet_airchange.yaml deleted file mode 100644 index 28d3f7a..0000000 --- a/test_tipc/configs/cd/cdnet/cdnet_airchange.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# Configurations of CDNet with AirChange dataset - -_base_: ../_base_/airchange.yaml - -save_dir: ./test_tipc/output/cd/cdnet/ - -model: !Node - type: CDNet \ No newline at end of file diff --git a/test_tipc/configs/cd/cdnet/cdnet_levircd.yaml b/test_tipc/configs/cd/cdnet/cdnet_levircd.yaml deleted file mode 100644 index 586e4e3..0000000 --- a/test_tipc/configs/cd/cdnet/cdnet_levircd.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# Configurations of cdnet with LEVIR-CD dataset - -_base_: ../_base_/levircd.yaml - -save_dir: ./test_tipc/output/cd/cdnet/ - -model: !Node - type: CDNet \ No newline at end of file diff --git a/test_tipc/configs/cd/cdnet/train_infer_python.txt b/test_tipc/configs/cd/cdnet/train_infer_python.txt deleted file mode 100644 index 00ff523..0000000 --- a/test_tipc/configs/cd/cdnet/train_infer_python.txt +++ /dev/null @@ -1,53 +0,0 @@ -===========================train_params=========================== -model_name:cd:cdnet -python:python -gpu_list:0|0,1 -use_gpu:null|null ---precision:null ---num_epochs:lite_train_lite_infer=5|lite_train_whole_infer=5|whole_train_whole_infer=10 ---save_dir:adaptive ---train_batch_size:lite_train_lite_infer=4|lite_train_whole_infer=4|whole_train_whole_infer=8 ---model_path:null ---config:lite_train_lite_infer=./test_tipc/configs/cd/cdnet/cdnet_airchange.yaml|lite_train_whole_infer=./test_tipc/configs/cd/cdnet/cdnet_airchange.yaml|whole_train_whole_infer=./test_tipc/configs/cd/cdnet/cdnet_levircd.yaml -train_model_name:best_model -null:null -## -trainer:norm -norm_train:test_tipc/run_task.py train cd -pact_train:null -fpgm_train:null -distill_train:null -null:null -null:null -## -===========================eval_params=========================== -eval:null -null:null -## -===========================export_params=========================== ---save_dir:adaptive ---model_dir:adaptive ---fixed_input_shape:[-1,3,256,256] -norm_export:deploy/export/export_model.py -quant_export:null -fpgm_export:null -distill_export:null -export1:null -export2:null -===========================infer_params=========================== -infer_model:null -infer_export:null -infer_quant:False -inference:test_tipc/infer.py ---device:cpu|gpu ---enable_mkldnn:True ---cpu_threads:6 ---batch_size:1 ---use_trt:False ---precision:fp32 ---model_dir:null ---config:null ---save_log_path:null ---benchmark:True ---model_name:cdnet -null:null \ No newline at end of file diff --git a/test_tipc/configs/cd/changeformer/changeformer_airchange.yaml b/test_tipc/configs/cd/changeformer/changeformer.yaml similarity index 54% rename from test_tipc/configs/cd/changeformer/changeformer_airchange.yaml rename to test_tipc/configs/cd/changeformer/changeformer.yaml index 15a37ea..785749d 100644 --- a/test_tipc/configs/cd/changeformer/changeformer_airchange.yaml +++ b/test_tipc/configs/cd/changeformer/changeformer.yaml @@ -1,4 +1,4 @@ -# Configurations of ChangeFormer with AirChange dataset +# Basic configurations of ChangeFormer _base_: ../_base_/airchange.yaml diff --git a/test_tipc/configs/cd/changeformer/changeformer_levircd.yaml b/test_tipc/configs/cd/changeformer/changeformer_levircd.yaml deleted file mode 100644 index 931a3e8..0000000 --- a/test_tipc/configs/cd/changeformer/changeformer_levircd.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# Configurations of ChangeFormer with LEVIR-CD dataset - -_base_: ../_base_/levircd.yaml - -save_dir: ./test_tipc/output/cd/changeformer/ - -model: !Node - type: ChangeFormer \ No newline at end of file diff --git a/test_tipc/configs/cd/changeformer/train_infer_python.txt b/test_tipc/configs/cd/changeformer/train_infer_python.txt index 47fe600..9ac2cdc 100644 --- a/test_tipc/configs/cd/changeformer/train_infer_python.txt +++ b/test_tipc/configs/cd/changeformer/train_infer_python.txt @@ -6,14 +6,14 @@ use_gpu:null|null --precision:null --num_epochs:lite_train_lite_infer=5|lite_train_whole_infer=5|whole_train_whole_infer=10 --save_dir:adaptive ---train_batch_size:lite_train_lite_infer=4|lite_train_whole_infer=4|whole_train_whole_infer=8 +--train_batch_size:lite_train_lite_infer=4|lite_train_whole_infer=4|whole_train_whole_infer=4 --model_path:null ---config:lite_train_lite_infer=./test_tipc/configs/cd/changeformer/changeformer_airchange.yaml|lite_train_whole_infer=./test_tipc/configs/cd/changeformer/changeformer_airchange.yaml|whole_train_whole_infer=./test_tipc/configs/cd/changeformer/changeformer_levircd.yaml train_model_name:best_model +train_infer_file_list:./test_tipc/data/airchange/:./test_tipc/data/airchange/eval.txt null:null ## trainer:norm -norm_train:test_tipc/run_task.py train cd +norm_train:test_tipc/run_task.py train cd --config ./test_tipc/configs/cd/changeformer/changeformer.yaml pact_train:null fpgm_train:null distill_train:null @@ -27,7 +27,7 @@ null:null ===========================export_params=========================== --save_dir:adaptive --model_dir:adaptive ---fixed_input_shape:[-1,3,256,256] +--fixed_input_shape:[1,3,256,256] norm_export:deploy/export/export_model.py quant_export:null fpgm_export:null @@ -46,7 +46,7 @@ inference:test_tipc/infer.py --use_trt:False --precision:fp32 --model_dir:null ---config:null +--file_list:null:null --save_log_path:null --benchmark:True --model_name:changeformer diff --git a/test_tipc/configs/cd/dsamnet/dsamnet_airchange.yaml b/test_tipc/configs/cd/dsamnet/dsamnet_airchange.yaml deleted file mode 100644 index 1ede33f..0000000 --- a/test_tipc/configs/cd/dsamnet/dsamnet_airchange.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# Configurations of DSAMNet with AirChange dataset - -_base_: ../_base_/airchange.yaml - -save_dir: ./test_tipc/output/cd/dsamnet/ - -model: !Node - type: DSAMNet \ No newline at end of file diff --git a/test_tipc/configs/cd/dsamnet/dsamnet_levircd.yaml b/test_tipc/configs/cd/dsamnet/dsamnet_levircd.yaml deleted file mode 100644 index 0fa9900..0000000 --- a/test_tipc/configs/cd/dsamnet/dsamnet_levircd.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# Configurations of DSAMNet with LEVIR-CD dataset - -_base_: ../_base_/levircd.yaml - -save_dir: ./test_tipc/output/cd/dsamnet/ - -model: !Node - type: DSAMNet \ No newline at end of file diff --git a/test_tipc/configs/cd/dsamnet/train_infer_python.txt b/test_tipc/configs/cd/dsamnet/train_infer_python.txt deleted file mode 100644 index bce8cab..0000000 --- a/test_tipc/configs/cd/dsamnet/train_infer_python.txt +++ /dev/null @@ -1,53 +0,0 @@ -===========================train_params=========================== -model_name:cd:dsamnet -python:python -gpu_list:0|0,1 -use_gpu:null|null ---precision:null ---num_epochs:lite_train_lite_infer=5|lite_train_whole_infer=5|whole_train_whole_infer=10 ---save_dir:adaptive ---train_batch_size:lite_train_lite_infer=4|lite_train_whole_infer=4|whole_train_whole_infer=8 ---model_path:null ---config:lite_train_lite_infer=./test_tipc/configs/cd/dsamnet/dsamnet_airchange.yaml|lite_train_whole_infer=./test_tipc/configs/cd/dsamnet/dsamnet_airchange.yaml|whole_train_whole_infer=./test_tipc/configs/cd/dsamnet/dsamnet_levircd.yaml -train_model_name:best_model -null:null -## -trainer:norm -norm_train:test_tipc/run_task.py train cd -pact_train:null -fpgm_train:null -distill_train:null -null:null -null:null -## -===========================eval_params=========================== -eval:null -null:null -## -===========================export_params=========================== ---save_dir:adaptive ---model_dir:adaptive ---fixed_input_shape:[-1,3,256,256] -norm_export:deploy/export/export_model.py -quant_export:null -fpgm_export:null -distill_export:null -export1:null -export2:null -===========================infer_params=========================== -infer_model:null -infer_export:null -infer_quant:False -inference:test_tipc/infer.py ---device:cpu|gpu ---enable_mkldnn:True ---cpu_threads:6 ---batch_size:1 ---use_trt:False ---precision:fp32 ---model_dir:null ---config:null ---save_log_path:null ---benchmark:True ---model_name:dsamnet -null:null \ No newline at end of file diff --git a/test_tipc/configs/cd/dsifn/dsifn_airchange.yaml b/test_tipc/configs/cd/dsifn/dsifn_airchange.yaml deleted file mode 100644 index 7fc661a..0000000 --- a/test_tipc/configs/cd/dsifn/dsifn_airchange.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# Configurations of DSIFN with AirChange dataset - -_base_: ../_base_/airchange.yaml - -save_dir: ./test_tipc/output/cd/dsifn/ - -model: !Node - type: DSIFN \ No newline at end of file diff --git a/test_tipc/configs/cd/dsifn/dsifn_levircd.yaml b/test_tipc/configs/cd/dsifn/dsifn_levircd.yaml deleted file mode 100644 index c4454a1..0000000 --- a/test_tipc/configs/cd/dsifn/dsifn_levircd.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# Configurations of DSIFN with LEVIR-CD dataset - -_base_: ../_base_/levircd.yaml - -save_dir: ./test_tipc/output/cd/dsifn/ - -model: !Node - type: DSIFN \ No newline at end of file diff --git a/test_tipc/configs/cd/dsifn/train_infer_python.txt b/test_tipc/configs/cd/dsifn/train_infer_python.txt deleted file mode 100644 index e491797..0000000 --- a/test_tipc/configs/cd/dsifn/train_infer_python.txt +++ /dev/null @@ -1,53 +0,0 @@ -===========================train_params=========================== -model_name:cd:dsifn -python:python -gpu_list:0|0,1 -use_gpu:null|null ---precision:null ---num_epochs:lite_train_lite_infer=5|lite_train_whole_infer=5|whole_train_whole_infer=10 ---save_dir:adaptive ---train_batch_size:lite_train_lite_infer=4|lite_train_whole_infer=4|whole_train_whole_infer=8 ---model_path:null ---config:lite_train_lite_infer=./test_tipc/configs/cd/dsifn/dsifn_airchange.yaml|lite_train_whole_infer=./test_tipc/configs/cd/dsifn/dsifn_airchange.yaml|whole_train_whole_infer=./test_tipc/configs/cd/dsifn/dsifn_levircd.yaml -train_model_name:best_model -null:null -## -trainer:norm -norm_train:test_tipc/run_task.py train cd -pact_train:null -fpgm_train:null -distill_train:null -null:null -null:null -## -===========================eval_params=========================== -eval:null -null:null -## -===========================export_params=========================== ---save_dir:adaptive ---model_dir:adaptive ---fixed_input_shape:[-1,3,256,256] -norm_export:deploy/export/export_model.py -quant_export:null -fpgm_export:null -distill_export:null -export1:null -export2:null -===========================infer_params=========================== -infer_model:null -infer_export:null -infer_quant:False -inference:test_tipc/infer.py ---device:cpu|gpu ---enable_mkldnn:True ---cpu_threads:6 ---batch_size:1 ---use_trt:False ---precision:fp32 ---model_dir:null ---config:null ---save_log_path:null ---benchmark:True ---model_name:dsifn -null:null \ No newline at end of file diff --git a/test_tipc/configs/cd/fc_ef/fc_ef_airchange.yaml b/test_tipc/configs/cd/fc_ef/fc_ef_airchange.yaml deleted file mode 100644 index fc47737..0000000 --- a/test_tipc/configs/cd/fc_ef/fc_ef_airchange.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# Configurations of FC-EF with AirChange dataset - -_base_: ../_base_/airchange.yaml - -save_dir: ./test_tipc/output/cd/fc_ef/ - -model: !Node - type: FCEarlyFusion \ No newline at end of file diff --git a/test_tipc/configs/cd/fc_ef/fc_ef_levircd.yaml b/test_tipc/configs/cd/fc_ef/fc_ef_levircd.yaml deleted file mode 100644 index 758d4a0..0000000 --- a/test_tipc/configs/cd/fc_ef/fc_ef_levircd.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# Configurations of FC-EF with LEVIR-CD dataset - -_base_: ../_base_/levircd.yaml - -save_dir: ./test_tipc/output/cd/fc_ef/ - -model: !Node - type: FCEarlyFusion \ No newline at end of file diff --git a/test_tipc/configs/cd/fc_ef/train_infer_python.txt b/test_tipc/configs/cd/fc_ef/train_infer_python.txt deleted file mode 100644 index fec5049..0000000 --- a/test_tipc/configs/cd/fc_ef/train_infer_python.txt +++ /dev/null @@ -1,53 +0,0 @@ -===========================train_params=========================== -model_name:cd:fc_ef -python:python -gpu_list:0|0,1 -use_gpu:null|null ---precision:null ---num_epochs:lite_train_lite_infer=5|lite_train_whole_infer=5|whole_train_whole_infer=10 ---save_dir:adaptive ---train_batch_size:lite_train_lite_infer=4|lite_train_whole_infer=4|whole_train_whole_infer=8 ---model_path:null ---config:lite_train_lite_infer=./test_tipc/configs/cd/fc_ef/fc_ef_airchange.yaml|lite_train_whole_infer=./test_tipc/configs/cd/fc_ef/fc_ef_airchange.yaml|whole_train_whole_infer=./test_tipc/configs/cd/fc_ef/fc_ef_levircd.yaml -train_model_name:best_model -null:null -## -trainer:norm -norm_train:test_tipc/run_task.py train cd -pact_train:null -fpgm_train:null -distill_train:null -null:null -null:null -## -===========================eval_params=========================== -eval:null -null:null -## -===========================export_params=========================== ---save_dir:adaptive ---model_dir:adaptive ---fixed_input_shape:[-1,3,256,256] -norm_export:deploy/export/export_model.py -quant_export:null -fpgm_export:null -distill_export:null -export1:null -export2:null -===========================infer_params=========================== -infer_model:null -infer_export:null -infer_quant:False -inference:test_tipc/infer.py ---device:cpu|gpu ---enable_mkldnn:True ---cpu_threads:6 ---batch_size:1 ---use_trt:False ---precision:fp32 ---model_dir:null ---config:null ---save_log_path:null ---benchmark:True ---model_name:fc_ef -null:null \ No newline at end of file diff --git a/test_tipc/configs/cd/fc_siam_conc/fc_siam_conc_airchange.yaml b/test_tipc/configs/cd/fc_siam_conc/fc_siam_conc_airchange.yaml deleted file mode 100644 index f4a8111..0000000 --- a/test_tipc/configs/cd/fc_siam_conc/fc_siam_conc_airchange.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# Configurations of FC-Siam-conc with AirChange dataset - -_base_: ../_base_/airchange.yaml - -save_dir: ./test_tipc/output/cd/fc_siam_conc/ - -model: !Node - type: FCSiamConc \ No newline at end of file diff --git a/test_tipc/configs/cd/fc_siam_conc/fc_siam_conc_levircd.yaml b/test_tipc/configs/cd/fc_siam_conc/fc_siam_conc_levircd.yaml deleted file mode 100644 index 1d49a5d..0000000 --- a/test_tipc/configs/cd/fc_siam_conc/fc_siam_conc_levircd.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# Configurations of FC-Siam-conc with LEVIR-CD dataset - -_base_: ../_base_/levircd.yaml - -save_dir: ./test_tipc/output/cd/fc_siam_conc/ - -model: !Node - type: FCSiamConc \ No newline at end of file diff --git a/test_tipc/configs/cd/fc_siam_conc/train_infer_python.txt b/test_tipc/configs/cd/fc_siam_conc/train_infer_python.txt deleted file mode 100644 index 47e9bdb..0000000 --- a/test_tipc/configs/cd/fc_siam_conc/train_infer_python.txt +++ /dev/null @@ -1,53 +0,0 @@ -===========================train_params=========================== -model_name:cd:fc_siam_conc -python:python -gpu_list:0|0,1 -use_gpu:null|null ---precision:null ---num_epochs:lite_train_lite_infer=5|lite_train_whole_infer=5|whole_train_whole_infer=10 ---save_dir:adaptive ---train_batch_size:lite_train_lite_infer=4|lite_train_whole_infer=4|whole_train_whole_infer=8 ---model_path:null ---config:lite_train_lite_infer=./test_tipc/configs/cd/fc_siam_conc/fc_siam_conc_airchange.yaml|lite_train_whole_infer=./test_tipc/configs/cd/fc_siam_conc/fc_siam_conc_airchange.yaml|whole_train_whole_infer=./test_tipc/configs/cd/fc_siam_conc/fc_siam_conc_levircd.yaml -train_model_name:best_model -null:null -## -trainer:norm -norm_train:test_tipc/run_task.py train cd -pact_train:null -fpgm_train:null -distill_train:null -null:null -null:null -## -===========================eval_params=========================== -eval:null -null:null -## -===========================export_params=========================== ---save_dir:adaptive ---model_dir:adaptive ---fixed_input_shape:[-1,3,256,256] -norm_export:deploy/export/export_model.py -quant_export:null -fpgm_export:null -distill_export:null -export1:null -export2:null -===========================infer_params=========================== -infer_model:null -infer_export:null -infer_quant:False -inference:test_tipc/infer.py ---device:cpu|gpu ---enable_mkldnn:True ---cpu_threads:6 ---batch_size:1 ---use_trt:False ---precision:fp32 ---model_dir:null ---config:null ---save_log_path:null ---benchmark:True ---model_name:fc_siam_conc -null:null \ No newline at end of file diff --git a/test_tipc/configs/cd/fc_siam_diff/fc_siam_diff_airchange.yaml b/test_tipc/configs/cd/fc_siam_diff/fc_siam_diff_airchange.yaml deleted file mode 100644 index 3453d82..0000000 --- a/test_tipc/configs/cd/fc_siam_diff/fc_siam_diff_airchange.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# Configurations of FC-Siam-diff with AirChange dataset - -_base_: ../_base_/airchange.yaml - -save_dir: ./test_tipc/output/cd/fc_siam_diff/ - -model: !Node - type: FCSiamDiff \ No newline at end of file diff --git a/test_tipc/configs/cd/fc_siam_diff/fc_siam_diff_levircd.yaml b/test_tipc/configs/cd/fc_siam_diff/fc_siam_diff_levircd.yaml deleted file mode 100644 index 2588cb9..0000000 --- a/test_tipc/configs/cd/fc_siam_diff/fc_siam_diff_levircd.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# Configurations of FC-Siam-diff with LEVIR-CD dataset - -_base_: ../_base_/levircd.yaml - -save_dir: ./test_tipc/output/cd/fc_siam_diff/ - -model: !Node - type: FCSiamDiff \ No newline at end of file diff --git a/test_tipc/configs/cd/fc_siam_diff/train_infer_python.txt b/test_tipc/configs/cd/fc_siam_diff/train_infer_python.txt deleted file mode 100644 index cba8b57..0000000 --- a/test_tipc/configs/cd/fc_siam_diff/train_infer_python.txt +++ /dev/null @@ -1,53 +0,0 @@ -===========================train_params=========================== -model_name:cd:fc_siam_diff -python:python -gpu_list:0|0,1 -use_gpu:null|null ---precision:null ---num_epochs:lite_train_lite_infer=5|lite_train_whole_infer=5|whole_train_whole_infer=10 ---save_dir:adaptive ---train_batch_size:lite_train_lite_infer=4|lite_train_whole_infer=4|whole_train_whole_infer=8 ---model_path:null ---config:lite_train_lite_infer=./test_tipc/configs/cd/fc_siam_diff/fc_siam_diff_airchange.yaml|lite_train_whole_infer=./test_tipc/configs/cd/fc_siam_diff/fc_siam_diff_airchange.yaml|whole_train_whole_infer=./test_tipc/configs/cd/fc_siam_diff/fc_siam_diff_levircd.yaml -train_model_name:best_model -null:null -## -trainer:norm -norm_train:test_tipc/run_task.py train cd -pact_train:null -fpgm_train:null -distill_train:null -null:null -null:null -## -===========================eval_params=========================== -eval:null -null:null -## -===========================export_params=========================== ---save_dir:adaptive ---model_dir:adaptive ---fixed_input_shape:[-1,3,256,256] -norm_export:deploy/export/export_model.py -quant_export:null -fpgm_export:null -distill_export:null -export1:null -export2:null -===========================infer_params=========================== -infer_model:null -infer_export:null -infer_quant:False -inference:test_tipc/infer.py ---device:cpu|gpu ---enable_mkldnn:True ---cpu_threads:6 ---batch_size:1 ---use_trt:False ---precision:fp32 ---model_dir:null ---config:null ---save_log_path:null ---benchmark:True ---model_name:fc_siam_diff -null:null \ No newline at end of file diff --git a/test_tipc/configs/cd/snunet/snunet_airchange.yaml b/test_tipc/configs/cd/snunet/snunet_airchange.yaml deleted file mode 100644 index eee3b1d..0000000 --- a/test_tipc/configs/cd/snunet/snunet_airchange.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# Configurations of SNUNet with AirChange dataset - -_base_: ../_base_/airchange.yaml - -save_dir: ./test_tipc/output/cd/snunet/ - -model: !Node - type: SNUNet \ No newline at end of file diff --git a/test_tipc/configs/cd/snunet/snunet_levircd.yaml b/test_tipc/configs/cd/snunet/snunet_levircd.yaml deleted file mode 100644 index 7af3bcb..0000000 --- a/test_tipc/configs/cd/snunet/snunet_levircd.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# Configurations of SNUNet with LEVIR-CD dataset - -_base_: ../_base_/levircd.yaml - -save_dir: ./test_tipc/output/cd/snunet/ - -model: !Node - type: SNUNet \ No newline at end of file diff --git a/test_tipc/configs/cd/snunet/train_infer_python.txt b/test_tipc/configs/cd/snunet/train_infer_python.txt deleted file mode 100644 index 264ffd9..0000000 --- a/test_tipc/configs/cd/snunet/train_infer_python.txt +++ /dev/null @@ -1,53 +0,0 @@ -===========================train_params=========================== -model_name:cd:snunet -python:python -gpu_list:0|0,1 -use_gpu:null|null ---precision:null ---num_epochs:lite_train_lite_infer=5|lite_train_whole_infer=5|whole_train_whole_infer=10 ---save_dir:adaptive ---train_batch_size:lite_train_lite_infer=4|lite_train_whole_infer=4|whole_train_whole_infer=8 ---model_path:null ---config:lite_train_lite_infer=./test_tipc/configs/cd/snunet/snunet_airchange.yaml|lite_train_whole_infer=./test_tipc/configs/cd/snunet/snunet_airchange.yaml|whole_train_whole_infer=./test_tipc/configs/cd/snunet/snunet_levircd.yaml -train_model_name:best_model -null:null -## -trainer:norm -norm_train:test_tipc/run_task.py train cd -pact_train:null -fpgm_train:null -distill_train:null -null:null -null:null -## -===========================eval_params=========================== -eval:null -null:null -## -===========================export_params=========================== ---save_dir:adaptive ---model_dir:adaptive ---fixed_input_shape:[-1,3,256,256] -norm_export:deploy/export/export_model.py -quant_export:null -fpgm_export:null -distill_export:null -export1:null -export2:null -===========================infer_params=========================== -infer_model:null -infer_export:null -infer_quant:False -inference:test_tipc/infer.py ---device:cpu|gpu ---enable_mkldnn:True ---cpu_threads:6 ---batch_size:1 ---use_trt:False ---precision:fp32 ---model_dir:null ---config:null ---save_log_path:null ---benchmark:True ---model_name:snunet -null:null \ No newline at end of file diff --git a/test_tipc/configs/cd/stanet/stanet_airchange.yaml b/test_tipc/configs/cd/stanet/stanet_airchange.yaml deleted file mode 100644 index 7c7c05a..0000000 --- a/test_tipc/configs/cd/stanet/stanet_airchange.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# Configurations of STANet with AirChange dataset - -_base_: ../_base_/airchange.yaml - -save_dir: ./test_tipc/output/cd/stanet/ - -model: !Node - type: STANet \ No newline at end of file diff --git a/test_tipc/configs/cd/stanet/stanet_levircd.yaml b/test_tipc/configs/cd/stanet/stanet_levircd.yaml deleted file mode 100644 index b439ff1..0000000 --- a/test_tipc/configs/cd/stanet/stanet_levircd.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# Configurations of STANet with LEVIR-CD dataset - -_base_: ../_base_/levircd.yaml - -save_dir: ./test_tipc/output/cd/stanet/ - -model: !Node - type: STANet \ No newline at end of file diff --git a/test_tipc/configs/cd/stanet/train_infer_python.txt b/test_tipc/configs/cd/stanet/train_infer_python.txt deleted file mode 100644 index 0bff7df..0000000 --- a/test_tipc/configs/cd/stanet/train_infer_python.txt +++ /dev/null @@ -1,53 +0,0 @@ -===========================train_params=========================== -model_name:cd:stanet -python:python -gpu_list:0|0,1 -use_gpu:null|null ---precision:null ---num_epochs:lite_train_lite_infer=5|lite_train_whole_infer=5|whole_train_whole_infer=10 ---save_dir:adaptive ---train_batch_size:lite_train_lite_infer=4|lite_train_whole_infer=4|whole_train_whole_infer=8 ---model_path:null ---config:lite_train_lite_infer=./test_tipc/configs/cd/stanet/stanet_airchange.yaml|lite_train_whole_infer=./test_tipc/configs/cd/stanet/stanet_airchange.yaml|whole_train_whole_infer=./test_tipc/configs/cd/stanet/stanet_levircd.yaml -train_model_name:best_model -null:null -## -trainer:norm -norm_train:test_tipc/run_task.py train cd -pact_train:null -fpgm_train:null -distill_train:null -null:null -null:null -## -===========================eval_params=========================== -eval:null -null:null -## -===========================export_params=========================== ---save_dir:adaptive ---model_dir:adaptive ---fixed_input_shape:[-1,3,256,256] -norm_export:deploy/export/export_model.py -quant_export:null -fpgm_export:null -distill_export:null -export1:null -export2:null -===========================infer_params=========================== -infer_model:null -infer_export:null -infer_quant:False -inference:test_tipc/infer.py ---device:cpu|gpu ---enable_mkldnn:True ---cpu_threads:6 ---batch_size:1 ---use_trt:False ---precision:fp32 ---model_dir:null ---config:null ---save_log_path:null ---benchmark:True ---model_name:stanet -null:null \ No newline at end of file diff --git a/test_tipc/configs/clas/hrnet/hrnet_ucmerced.yaml b/test_tipc/configs/clas/hrnet/hrnet.yaml similarity index 62% rename from test_tipc/configs/clas/hrnet/hrnet_ucmerced.yaml rename to test_tipc/configs/clas/hrnet/hrnet.yaml index 088e722..f402c26 100644 --- a/test_tipc/configs/clas/hrnet/hrnet_ucmerced.yaml +++ b/test_tipc/configs/clas/hrnet/hrnet.yaml @@ -1,4 +1,4 @@ -# Configurations of HRNet with UCMerced dataset +# Basic configurations of HRNet _base_: ../_base_/ucmerced.yaml diff --git a/test_tipc/configs/clas/hrnet/train_infer_python.txt b/test_tipc/configs/clas/hrnet/train_infer_python.txt index 1116c77..23f3820 100644 --- a/test_tipc/configs/clas/hrnet/train_infer_python.txt +++ b/test_tipc/configs/clas/hrnet/train_infer_python.txt @@ -8,12 +8,12 @@ use_gpu:null|null --save_dir:adaptive --train_batch_size:lite_train_lite_infer=16|lite_train_whole_infer=16|whole_train_whole_infer=16 --model_path:null ---config:lite_train_lite_infer=./test_tipc/configs/clas/hrnet/hrnet_ucmerced.yaml|lite_train_whole_infer=./test_tipc/configs/clas/hrnet/hrnet_ucmerced.yaml|whole_train_whole_infer=./test_tipc/configs/clas/hrnet/hrnet_ucmerced.yaml train_model_name:best_model +train_infer_file_list:./test_tipc/data/ucmerced/:./test_tipc/data/ucmerced/val.txt null:null ## trainer:norm -norm_train:test_tipc/run_task.py train clas +norm_train:test_tipc/run_task.py train clas --config ./test_tipc/configs/clas/hrnet/hrnet.yaml pact_train:null fpgm_train:null distill_train:null @@ -46,7 +46,7 @@ inference:test_tipc/infer.py --use_trt:False --precision:fp32 --model_dir:null ---config:null +--file_list:null:null --save_log_path:null --benchmark:True --model_name:hrnet diff --git a/test_tipc/infer.py b/test_tipc/infer.py index 8a8983c..3672940 100644 --- a/test_tipc/infer.py +++ b/test_tipc/infer.py @@ -13,8 +13,6 @@ from paddle.inference import PrecisionType from paddlers.tasks import load_model from paddlers.utils import logging -from config_utils import parse_configs - class _bool(object): def __new__(cls, x): @@ -287,8 +285,7 @@ class TIPCPredictor(object): if __name__ == '__main__': parser = argparse.ArgumentParser() - parser.add_argument('--config', type=str) - parser.add_argument('--inherit_off', action='store_true') + parser.add_argument('--file_list', type=str, nargs=2) parser.add_argument('--model_dir', type=str, default='./') parser.add_argument( '--device', type=str, choices=['cpu', 'gpu'], default='cpu') @@ -303,11 +300,6 @@ if __name__ == '__main__': args = parser.parse_args() - cfg = parse_configs(args.config, not args.inherit_off) - eval_dataset = cfg['datasets']['eval'] - data_dir = eval_dataset.args['data_dir'] - file_list = eval_dataset.args['file_list'] - predictor = TIPCPredictor( args.model_dir, device=args.device, @@ -318,7 +310,7 @@ if __name__ == '__main__': trt_precision_mode=args.precision, benchmark=args.benchmark) - predictor.predict(data_dir, file_list) + predictor.predict(args.file_list[0], args.file_list[1]) if args.benchmark: predictor.autolog.report() diff --git a/test_tipc/prepare.sh b/test_tipc/prepare.sh index ac8267b..ead48af 100644 --- a/test_tipc/prepare.sh +++ b/test_tipc/prepare.sh @@ -48,8 +48,6 @@ elif [[ ${MODE} == 'whole_train_whole_infer' ]]; then --out_dataset_dir "${DATA_DIR}/levircd" \ --crop_size 256 \ --crop_stride 256 - elif [[ ${task_name} == 'clas' ]]; then - download_and_unzip_dataset "${DATA_DIR}" ucmerced https://paddlers.bj.bcebos.com/datasets/ucmerced.zip fi fi diff --git a/tutorials/train/README.md b/tutorials/train/README.md index 9c72107..c63cf26 100644 --- a/tutorials/train/README.md +++ b/tutorials/train/README.md @@ -9,11 +9,11 @@ |change_detection/changeformer.py | 变化检测 | ChangeFormer | |change_detection/dsamnet.py | 变化检测 | DSAMNet | |change_detection/dsifn.py | 变化检测 | DSIFN | +|change_detection/snunet.py | 变化检测 | SNUNet | +|change_detection/stanet.py | 变化检测 | STANet | |change_detection/fc_ef.py | 变化检测 | FC-EF | |change_detection/fc_siam_conc.py | 变化检测 | FC-Siam-conc | |change_detection/fc_siam_diff.py | 变化检测 | FC-Siam-diff | -|change_detection/snunet.py | 变化检测 | SNUNet | -|change_detection/stanet.py | 变化检测 | STANet | |classification/hrnet.py | 场景分类 | HRNet | |classification/mobilenetv3.py | 场景分类 | MobileNetV3 | |classification/resnet50_vd.py | 场景分类 | ResNet50-vd | From bbbbd3c7c14bcab4dec1ff2a0070f2b971304a43 Mon Sep 17 00:00:00 2001 From: liuxtakeoff <71876619+liuxtakeoff@users.noreply.github.com> Date: Fri, 26 Aug 2022 10:39:59 +0800 Subject: [PATCH 45/52] =?UTF-8?q?[=E8=AE=BA=E6=96=87=E5=A4=8D=E7=8E=B0?= =?UTF-8?q?=E8=B5=9B]=20FCCDN=20(#23)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 精度验收通过,代码符合规范,论文复现成功。 Co-authored-by: liuxtakeoff <763848861.qq.com> --- paddlers/rs_models/cd/__init__.py | 2 + paddlers/rs_models/cd/fccdn.py | 478 ++++++++++++++++++ paddlers/rs_models/cd/losses/__init__.py | 15 + paddlers/rs_models/cd/losses/fccdn_loss.py | 170 +++++++ paddlers/tasks/change_detector.py | 32 +- test_tipc/configs/cd/fccdn/fccdn.yaml | 13 + .../configs/cd/fccdn/train_infer_python.txt | 53 ++ tutorials/train/change_detection/fccdn.py | 94 ++++ 8 files changed, 855 insertions(+), 2 deletions(-) create mode 100644 paddlers/rs_models/cd/fccdn.py create mode 100644 paddlers/rs_models/cd/losses/__init__.py create mode 100644 paddlers/rs_models/cd/losses/fccdn_loss.py create mode 100644 test_tipc/configs/cd/fccdn/fccdn.yaml create mode 100644 test_tipc/configs/cd/fccdn/train_infer_python.txt create mode 100644 tutorials/train/change_detection/fccdn.py diff --git a/paddlers/rs_models/cd/__init__.py b/paddlers/rs_models/cd/__init__.py index c3d75b5..274b2e9 100644 --- a/paddlers/rs_models/cd/__init__.py +++ b/paddlers/rs_models/cd/__init__.py @@ -23,3 +23,5 @@ from .fc_ef import FCEarlyFusion from .fc_siam_conc import FCSiamConc from .fc_siam_diff import FCSiamDiff from .changeformer import ChangeFormer +from .fccdn import FCCDN +from .losses import fccdn_ssl_loss diff --git a/paddlers/rs_models/cd/fccdn.py b/paddlers/rs_models/cd/fccdn.py new file mode 100644 index 0000000..17d1673 --- /dev/null +++ b/paddlers/rs_models/cd/fccdn.py @@ -0,0 +1,478 @@ +# Copyright (c) 2022 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 paddle +import paddle.nn as nn +import paddle.nn.functional as F + +from .layers import BasicConv, MaxPool2x2, Conv1x1, Conv3x3 + +bn_mom = 1 - 0.0003 + + +class NLBlock(nn.Layer): + def __init__(self, in_channels): + super(NLBlock, self).__init__() + self.conv_v = BasicConv( + in_ch=in_channels, + out_ch=in_channels, + kernel_size=3, + norm=nn.BatchNorm2D( + in_channels, momentum=0.9)) + self.W = BasicConv( + in_ch=in_channels, + out_ch=in_channels, + kernel_size=3, + norm=nn.BatchNorm2D( + in_channels, momentum=0.9), + act=nn.ReLU()) + + def forward(self, x): + batch_size, c, h, w = x.shape[0], x.shape[1], x.shape[2], x.shape[3] + value = self.conv_v(x) + value = value.reshape([batch_size, c, value.shape[2] * value.shape[3]]) + value = value.transpose([0, 2, 1]) # B * (H*W) * value_channels + key = x.reshape([batch_size, c, h * w]) # B * key_channels * (H*W) + query = x.reshape([batch_size, c, h * w]) + query = query.transpose([0, 2, 1]) + + sim_map = paddle.matmul(query, key) # B * (H*W) * (H*W) + sim_map = (c**-.5) * sim_map # B * (H*W) * (H*W) + sim_map = nn.functional.softmax(sim_map, axis=-1) # B * (H*W) * (H*W) + + context = paddle.matmul(sim_map, value) + context = context.transpose([0, 2, 1]) + context = context.reshape([batch_size, c, *x.shape[2:]]) + context = self.W(context) + + return context + + +class NLFPN(nn.Layer): + """ Non-local feature parymid network""" + + def __init__(self, in_dim, reduction=True): + super(NLFPN, self).__init__() + if reduction: + self.reduction = BasicConv( + in_ch=in_dim, + out_ch=in_dim // 4, + kernel_size=1, + norm=nn.BatchNorm2D( + in_dim // 4, momentum=bn_mom), + act=nn.ReLU()) + self.re_reduction = BasicConv( + in_ch=in_dim // 4, + out_ch=in_dim, + kernel_size=1, + norm=nn.BatchNorm2D( + in_dim, momentum=bn_mom), + act=nn.ReLU()) + in_dim = in_dim // 4 + else: + self.reduction = None + self.re_reduction = None + self.conv_e1 = BasicConv( + in_dim, + in_dim, + kernel_size=3, + norm=nn.BatchNorm2D( + in_dim, momentum=bn_mom), + act=nn.ReLU()) + self.conv_e2 = BasicConv( + in_dim, + in_dim * 2, + kernel_size=3, + norm=nn.BatchNorm2D( + in_dim * 2, momentum=bn_mom), + act=nn.ReLU()) + self.conv_e3 = BasicConv( + in_dim * 2, + in_dim * 4, + kernel_size=3, + norm=nn.BatchNorm2D( + in_dim * 4, momentum=bn_mom), + act=nn.ReLU()) + self.conv_d1 = BasicConv( + in_dim, + in_dim, + kernel_size=3, + norm=nn.BatchNorm2D( + in_dim, momentum=bn_mom), + act=nn.ReLU()) + self.conv_d2 = BasicConv( + in_dim * 2, + in_dim, + kernel_size=3, + norm=nn.BatchNorm2D( + in_dim, momentum=bn_mom), + act=nn.ReLU()) + self.conv_d3 = BasicConv( + in_dim * 4, + in_dim * 2, + kernel_size=3, + norm=nn.BatchNorm2D( + in_dim * 2, momentum=bn_mom), + act=nn.ReLU()) + self.nl3 = NLBlock(in_dim * 2) + self.nl2 = NLBlock(in_dim) + self.nl1 = NLBlock(in_dim) + + self.downsample_x2 = nn.MaxPool2D(stride=2, kernel_size=2) + self.upsample_x2 = nn.UpsamplingBilinear2D(scale_factor=2) + + def forward(self, x): + if self.reduction is not None: + x = self.reduction(x) + e1 = self.conv_e1(x) # C,H,W + e2 = self.conv_e2(self.downsample_x2(e1)) # 2C,H/2,W/2 + e3 = self.conv_e3(self.downsample_x2(e2)) # 4C,H/4,W/4 + + d3 = self.conv_d3(e3) # 2C,H/4,W/4 + nl = self.nl3(d3) + d3 = self.upsample_x2(paddle.multiply(d3, nl)) ##2C,H/2,W/2 + d2 = self.conv_d2(e2 + d3) # C,H/2,W/2 + nl = self.nl2(d2) + d2 = self.upsample_x2(paddle.multiply(d2, nl)) # C,H,W + d1 = self.conv_d1(e1 + d2) + nl = self.nl1(d1) + d1 = paddle.multiply(d1, nl) # C,H,W + if self.re_reduction is not None: + d1 = self.re_reduction(d1) + + return d1 + + +class Cat(nn.Layer): + def __init__(self, in_chn_high, in_chn_low, out_chn, upsample=False): + super(Cat, self).__init__() + self.do_upsample = upsample + self.upsample = nn.Upsample(scale_factor=2, mode="nearest") + self.conv2d = BasicConv( + in_chn_high + in_chn_low, + out_chn, + kernel_size=1, + norm=nn.BatchNorm2D( + out_chn, momentum=bn_mom), + act=nn.ReLU()) + + def forward(self, x, y): + if self.do_upsample: + x = self.upsample(x) + + x = paddle.concat((x, y), 1) + + return self.conv2d(x) + + +class DoubleConv(nn.Layer): + def __init__(self, in_chn, out_chn, stride=1, dilation=1): + super(DoubleConv, self).__init__() + self.conv = nn.Sequential( + nn.Conv2D( + in_chn, + out_chn, + kernel_size=3, + stride=stride, + dilation=dilation, + padding=dilation), + nn.BatchNorm2D( + out_chn, momentum=bn_mom), + nn.ReLU(), + nn.Conv2D( + out_chn, out_chn, kernel_size=3, stride=1, padding=1), + nn.BatchNorm2D( + out_chn, momentum=bn_mom), + nn.ReLU()) + + def forward(self, x): + x = self.conv(x) + return x + + +class SEModule(nn.Layer): + def __init__(self, channels, reduction_channels): + super(SEModule, self).__init__() + self.fc1 = nn.Conv2D( + channels, + reduction_channels, + kernel_size=1, + padding=0, + bias_attr=True) + self.ReLU = nn.ReLU() + self.fc2 = nn.Conv2D( + reduction_channels, + channels, + kernel_size=1, + padding=0, + bias_attr=True) + + def forward(self, x): + x_se = x.reshape( + [x.shape[0], x.shape[1], x.shape[2] * x.shape[3]]).mean(-1).reshape( + [x.shape[0], x.shape[1], 1, 1]) + + x_se = self.fc1(x_se) + x_se = self.ReLU(x_se) + x_se = self.fc2(x_se) + return x * F.sigmoid(x_se) + + +class BasicBlock(nn.Layer): + expansion = 1 + + def __init__(self, + inplanes, + planes, + downsample=None, + use_se=False, + stride=1, + dilation=1): + super(BasicBlock, self).__init__() + first_planes = planes + outplanes = planes * self.expansion + + self.conv1 = DoubleConv(inplanes, first_planes) + self.conv2 = DoubleConv( + first_planes, outplanes, stride=stride, dilation=dilation) + self.se = SEModule(outplanes, planes // 4) if use_se else None + self.downsample = MaxPool2x2() if downsample else None + self.ReLU = nn.ReLU() + + def forward(self, x): + out = self.conv1(x) + residual = out + out = self.conv2(out) + + if self.se is not None: + out = self.se(out) + + if self.downsample is not None: + residual = self.downsample(residual) + + out = out + residual + out = self.ReLU(out) + return out + + +class DenseCatAdd(nn.Layer): + def __init__(self, in_chn, out_chn): + super(DenseCatAdd, self).__init__() + self.conv1 = BasicConv(in_chn, in_chn, kernel_size=3, act=nn.ReLU()) + self.conv2 = BasicConv(in_chn, in_chn, kernel_size=3, act=nn.ReLU()) + self.conv3 = BasicConv(in_chn, in_chn, kernel_size=3, act=nn.ReLU()) + self.conv_out = BasicConv( + in_chn, + out_chn, + kernel_size=1, + norm=nn.BatchNorm2D( + out_chn, momentum=bn_mom), + act=nn.ReLU()) + + def forward(self, x, y): + x1 = self.conv1(x) + x2 = self.conv2(x1) + x3 = self.conv3(x2 + x1) + + y1 = self.conv1(y) + y2 = self.conv2(y1) + y3 = self.conv3(y2 + y1) + + return self.conv_out(x1 + x2 + x3 + y1 + y2 + y3) + + +class DenseCatDiff(nn.Layer): + def __init__(self, in_chn, out_chn): + super(DenseCatDiff, self).__init__() + self.conv1 = BasicConv(in_chn, in_chn, kernel_size=3, act=nn.ReLU()) + self.conv2 = BasicConv(in_chn, in_chn, kernel_size=3, act=nn.ReLU()) + self.conv3 = BasicConv(in_chn, in_chn, kernel_size=3, act=nn.ReLU()) + self.conv_out = BasicConv( + in_ch=in_chn, + out_ch=out_chn, + kernel_size=1, + norm=nn.BatchNorm2D( + out_chn, momentum=bn_mom), + act=nn.ReLU()) + + def forward(self, x, y): + x1 = self.conv1(x) + x2 = self.conv2(x1) + x3 = self.conv3(x2 + x1) + + y1 = self.conv1(y) + y2 = self.conv2(y1) + y3 = self.conv3(y2 + y1) + out = self.conv_out(paddle.abs(x1 + x2 + x3 - y1 - y2 - y3)) + return out + + +class DFModule(nn.Layer): + """Dense connection-based feature fusion module""" + + def __init__(self, dim_in, dim_out, reduction=True): + super(DFModule, self).__init__() + if reduction: + self.reduction = Conv1x1( + dim_in, + dim_in // 2, + norm=nn.BatchNorm2D( + dim_in // 2, momentum=bn_mom), + act=nn.ReLU()) + dim_in = dim_in // 2 + else: + self.reduction = None + self.cat1 = DenseCatAdd(dim_in, dim_out) + self.cat2 = DenseCatDiff(dim_in, dim_out) + self.conv1 = Conv3x3( + dim_out, + dim_out, + norm=nn.BatchNorm2D( + dim_out, momentum=bn_mom), + act=nn.ReLU()) + + def forward(self, x1, x2): + if self.reduction is not None: + x1 = self.reduction(x1) + x2 = self.reduction(x2) + x_add = self.cat1(x1, x2) + x_diff = self.cat2(x1, x2) + y = self.conv1(x_diff) + x_add + return y + + +class FCCDN(nn.Layer): + """ + The FCCDN implementation based on PaddlePaddle. + + The original article refers to + Pan Chen, et al., "FCCDN: Feature Constraint Network for VHR Image Change Detection" + (https://arxiv.org/pdf/2105.10860.pdf). + + Args: + in_channels (int): Number of input channels. Default: 3. + num_classes (int): Number of target classes. Default: 2. + os (int): Number of output stride. Default: 16. + use_se (bool): Whether to use SEModule. Default: True. + """ + + def __init__(self, in_channels=3, num_classes=2, os=16, use_se=True): + super(FCCDN, self).__init__() + if os >= 16: + dilation_list = [1, 1, 1, 1] + stride_list = [2, 2, 2, 2] + pool_list = [True, True, True, True] + elif os == 8: + dilation_list = [2, 1, 1, 1] + stride_list = [1, 2, 2, 2] + pool_list = [False, True, True, True] + else: + dilation_list = [2, 2, 1, 1] + stride_list = [1, 1, 2, 2] + pool_list = [False, False, True, True] + se_list = [use_se, use_se, use_se, use_se] + channel_list = [256, 128, 64, 32] + # Encoder + self.block1 = BasicBlock(in_channels, channel_list[3], pool_list[3], + se_list[3], stride_list[3], dilation_list[3]) + self.block2 = BasicBlock(channel_list[3], channel_list[2], pool_list[2], + se_list[2], stride_list[2], dilation_list[2]) + self.block3 = BasicBlock(channel_list[2], channel_list[1], pool_list[1], + se_list[1], stride_list[1], dilation_list[1]) + self.block4 = BasicBlock(channel_list[1], channel_list[0], pool_list[0], + se_list[0], stride_list[0], dilation_list[0]) + + # Center + self.center = NLFPN(channel_list[0], True) + + # Decoder + self.decoder3 = Cat(channel_list[0], + channel_list[1], + channel_list[1], + upsample=pool_list[0]) + self.decoder2 = Cat(channel_list[1], + channel_list[2], + channel_list[2], + upsample=pool_list[1]) + self.decoder1 = Cat(channel_list[2], + channel_list[3], + channel_list[3], + upsample=pool_list[2]) + + self.df1 = DFModule(channel_list[3], channel_list[3], True) + self.df2 = DFModule(channel_list[2], channel_list[2], True) + self.df3 = DFModule(channel_list[1], channel_list[1], True) + self.df4 = DFModule(channel_list[0], channel_list[0], True) + + self.catc3 = Cat(channel_list[0], + channel_list[1], + channel_list[1], + upsample=pool_list[0]) + self.catc2 = Cat(channel_list[1], + channel_list[2], + channel_list[2], + upsample=pool_list[1]) + self.catc1 = Cat(channel_list[2], + channel_list[3], + channel_list[3], + upsample=pool_list[2]) + + self.upsample_x2 = nn.Sequential( + nn.Conv2D( + channel_list[3], 8, kernel_size=3, stride=1, padding=1), + nn.BatchNorm2D( + 8, momentum=bn_mom), + nn.ReLU(), + nn.UpsamplingBilinear2D(scale_factor=2)) + + self.conv_out = nn.Conv2D( + 8, num_classes, kernel_size=3, stride=1, padding=1) + self.conv_out_class = nn.Conv2D( + channel_list[3], 1, kernel_size=1, stride=1, padding=0) + + def forward(self, t1, t2): + e1_1 = self.block1(t1) + e2_1 = self.block2(e1_1) + e3_1 = self.block3(e2_1) + y1 = self.block4(e3_1) + + e1_2 = self.block1(t2) + e2_2 = self.block2(e1_2) + e3_2 = self.block3(e2_2) + y2 = self.block4(e3_2) + + y1 = self.center(y1) + y2 = self.center(y2) + c = self.df4(y1, y2) + + y1 = self.decoder3(y1, e3_1) + y2 = self.decoder3(y2, e3_2) + c = self.catc3(c, self.df3(y1, y2)) + + y1 = self.decoder2(y1, e2_1) + y2 = self.decoder2(y2, e2_2) + c = self.catc2(c, self.df2(y1, y2)) + + y1 = self.decoder1(y1, e1_1) + y2 = self.decoder1(y2, e1_2) + + c = self.catc1(c, self.df1(y1, y2)) + y = self.conv_out(self.upsample_x2(c)) + + if self.training: + y1 = self.conv_out_class(y1) + y2 = self.conv_out_class(y2) + return [y, [y1, y2]] + else: + return [y] diff --git a/paddlers/rs_models/cd/losses/__init__.py b/paddlers/rs_models/cd/losses/__init__.py new file mode 100644 index 0000000..49465ff --- /dev/null +++ b/paddlers/rs_models/cd/losses/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2022 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. + +from .fccdn_loss import fccdn_ssl_loss diff --git a/paddlers/rs_models/cd/losses/fccdn_loss.py b/paddlers/rs_models/cd/losses/fccdn_loss.py new file mode 100644 index 0000000..49d2b4c --- /dev/null +++ b/paddlers/rs_models/cd/losses/fccdn_loss.py @@ -0,0 +1,170 @@ +# Copyright (c) 2022 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 paddle +import paddle.nn as nn +import paddle.nn.functional as F + + +class DiceLoss(nn.Layer): + def __init__(self, batch=True): + super(DiceLoss, self).__init__() + self.batch = batch + + def soft_dice_coeff(self, y_pred, y_true): + smooth = 0.00001 + if self.batch: + i = paddle.sum(y_true) + j = paddle.sum(y_pred) + intersection = paddle.sum(y_true * y_pred) + else: + i = y_true.sum(1).sum(1).sum(1) + j = y_pred.sum(1).sum(1).sum(1) + intersection = (y_true * y_pred).sum(1).sum(1).sum(1) + score = (2. * intersection + smooth) / (i + j + smooth) + return score.mean() + + def soft_dice_loss(self, y_pred, y_true): + loss = 1 - self.soft_dice_coeff(y_pred, y_true) + return loss + + def forward(self, y_pred, y_true): + return self.soft_dice_loss(y_pred.astype(paddle.float32), y_true) + + +class MultiClassDiceLoss(nn.Layer): + def __init__( + self, + weight, + batch=True, + ignore_index=-1, + do_softmax=False, + **kwargs, ): + super(MultiClassDiceLoss, self).__init__() + self.ignore_index = ignore_index + self.weight = weight + self.do_softmax = do_softmax + self.binary_diceloss = DiceLoss(batch) + + def forward(self, y_pred, y_true): + if self.do_softmax: + y_pred = paddle.nn.functional.softmax(y_pred, axis=1) + y_true = F.one_hot(y_true.long(), y_pred.shape[1]).permute(0, 3, 1, 2) + total_loss = 0.0 + tmp_i = 0.0 + for i in range(y_pred.shape[1]): + if i != self.ignore_index: + diceloss = self.binary_diceloss(y_pred[:, i, :, :], + y_true[:, i, :, :]) + total_loss += paddle.multiply(diceloss, self.weight[i]) + tmp_i += 1.0 + return total_loss / tmp_i + + +class DiceBCELoss(nn.Layer): + """Binary change detection task loss""" + + def __init__(self): + super(DiceBCELoss, self).__init__() + self.bce_loss = nn.BCELoss() + self.binnary_dice = DiceLoss() + + def forward(self, scores, labels, do_sigmoid=True): + if len(scores.shape) > 3: + scores = scores.squeeze(1) + if len(labels.shape) > 3: + labels = labels.squeeze(1) + if do_sigmoid: + scores = paddle.nn.functional.sigmoid(scores.clone()) + diceloss = self.binnary_dice(scores, labels) + bceloss = self.bce_loss(scores, labels) + return diceloss + bceloss + + +class McDiceBCELoss(nn.Layer): + """Multi-class change detection task loss""" + + def __init__(self, weight, do_sigmoid=True): + super(McDiceBCELoss, self).__init__() + self.ce_loss = nn.CrossEntropyLoss(weight) + self.dice = MultiClassDiceLoss(weight, do_sigmoid) + + def forward(self, scores, labels): + if len(scores.shape) < 4: + scores = scores.unsqueeze(1) + if len(labels.shape) < 4: + labels = labels.unsqueeze(1) + diceloss = self.dice(scores, labels) + bceloss = self.ce_loss(scores, labels) + return diceloss + bceloss + + +def fccdn_ssl_loss(logits_list, labels): + """ + Self-supervised learning loss for change detection. + + The original article refers to + Pan Chen, et al., "FCCDN: Feature Constraint Network for VHR Image Change Detection" + (https://arxiv.org/pdf/2105.10860.pdf). + + Args: + logits_list (list[paddle.Tensor]): Single-channel segmentation logit maps for each of the two temporal phases. + labels (paddle.Tensor): Binary change labels. + """ + + # Create loss + criterion_ssl = DiceBCELoss() + + # Get downsampled change map + h, w = logits_list[0].shape[-2], logits_list[0].shape[-1] + labels_downsample = F.interpolate(x=labels.unsqueeze(1), size=[h, w]) + labels_type = str(labels_downsample.dtype) + assert "int" in labels_type or "bool" in labels_type,\ + f"Expected dtype of labels to be int or bool, but got {labels_type}" + + # Seg map + out1 = paddle.nn.functional.sigmoid(logits_list[0]).clone() + out2 = paddle.nn.functional.sigmoid(logits_list[1]).clone() + out3 = out1.clone() + out4 = out2.clone() + + out1 = paddle.where(labels_downsample == 1, paddle.zeros_like(out1), out1) + out2 = paddle.where(labels_downsample == 1, paddle.zeros_like(out2), out2) + out3 = paddle.where(labels_downsample != 1, paddle.zeros_like(out3), out3) + out4 = paddle.where(labels_downsample != 1, paddle.zeros_like(out4), out4) + + pred_seg_pre_tmp1 = paddle.where(out1 <= 0.5, + paddle.zeros_like(out1), + paddle.ones_like(out1)) + pred_seg_post_tmp1 = paddle.where(out2 <= 0.5, + paddle.zeros_like(out2), + paddle.ones_like(out2)) + + pred_seg_pre_tmp2 = paddle.where(out3 <= 0.5, + paddle.zeros_like(out3), + paddle.ones_like(out3)) + pred_seg_post_tmp2 = paddle.where(out4 <= 0.5, + paddle.zeros_like(out4), + paddle.ones_like(out4)) + + # Seg loss + labels_downsample = labels_downsample.astype(paddle.float32) + loss_aux = 0.2 * criterion_ssl(out1, pred_seg_post_tmp1, False) + loss_aux += 0.2 * criterion_ssl(out2, pred_seg_pre_tmp1, False) + loss_aux += 0.2 * criterion_ssl( + out3, labels_downsample - pred_seg_post_tmp2, False) + loss_aux += 0.2 * criterion_ssl(out4, labels_downsample - pred_seg_pre_tmp2, + False) + + return loss_aux diff --git a/paddlers/tasks/change_detector.py b/paddlers/tasks/change_detector.py index 1eef8eb..cea15ec 100644 --- a/paddlers/tasks/change_detector.py +++ b/paddlers/tasks/change_detector.py @@ -37,7 +37,7 @@ from .utils import seg_metrics as metrics __all__ = [ "CDNet", "FCEarlyFusion", "FCSiamConc", "FCSiamDiff", "STANet", "BIT", - "SNUNet", "DSIFN", "DSAMNet", "ChangeStar", "ChangeFormer" + "SNUNet", "DSIFN", "DSAMNet", "ChangeStar", "ChangeFormer", "FCCDN" ] @@ -1055,7 +1055,7 @@ class ChangeStar(BaseChangeDetector): if self.use_mixed_loss is False: return { # XXX: make sure the shallow copy works correctly here. - 'types': [seglosses.CrossEntropyLoss()] * 4, + 'types': [seg_losses.CrossEntropyLoss()] * 4, 'coef': [1.0] * 4 } else: @@ -1082,3 +1082,31 @@ class ChangeFormer(BaseChangeDetector): num_classes=num_classes, use_mixed_loss=use_mixed_loss, **params) + + +class FCCDN(BaseChangeDetector): + def __init__(self, + in_channels=3, + num_classes=2, + use_mixed_loss=False, + losses=None, + **params): + params.update({'in_channels': in_channels}) + super(FCCDN, self).__init__( + model_name='FCCDN', + num_classes=num_classes, + use_mixed_loss=use_mixed_loss, + losses=losses, + **params) + + def default_loss(self): + if self.use_mixed_loss is False: + return { + 'types': + [seg_losses.CrossEntropyLoss(), cmcd.losses.fccdn_ssl_loss], + 'coef': [1.0, 1.0] + } + else: + raise ValueError( + f"Currently `use_mixed_loss` must be set to False for {self.__class__}" + ) diff --git a/test_tipc/configs/cd/fccdn/fccdn.yaml b/test_tipc/configs/cd/fccdn/fccdn.yaml new file mode 100644 index 0000000..8b93717 --- /dev/null +++ b/test_tipc/configs/cd/fccdn/fccdn.yaml @@ -0,0 +1,13 @@ +# Basic configurations of FCCDN + +_base_: ../_base_/airchange.yaml + +save_dir: ./test_tipc/output/cd/fccdn/ + +model: !Node + type: FCCDN + +learning_rate: 0.07 +lr_decay_power: 0.6 +log_interval_steps: 100 +save_interval_epochs: 3 diff --git a/test_tipc/configs/cd/fccdn/train_infer_python.txt b/test_tipc/configs/cd/fccdn/train_infer_python.txt new file mode 100644 index 0000000..26147e5 --- /dev/null +++ b/test_tipc/configs/cd/fccdn/train_infer_python.txt @@ -0,0 +1,53 @@ +===========================train_params=========================== +model_name:cd:fccdn +python:python +gpu_list:0 +use_gpu:null|null +--precision:null +--num_epochs:lite_train_lite_infer=15|lite_train_whole_infer=15|whole_train_whole_infer=15 +--save_dir:adaptive +--train_batch_size:lite_train_lite_infer=4|lite_train_whole_infer=4|whole_train_whole_infer=4 +--model_path:null +train_model_name:best_model +train_infer_file_list:./test_tipc/data/airchange/:./test_tipc/data/airchange/eval.txt +null:null +## +trainer:norm +norm_train:test_tipc/run_task.py train cd --config ./test_tipc/configs/cd/fccdn/fccdn.yaml +pact_train:null +fpgm_train:null +distill_train:null +null:null +null:null +## +===========================eval_params=========================== +eval:null +null:null +## +===========================export_params=========================== +--save_dir:adaptive +--model_dir:adaptive +--fixed_input_shape:[1,3,256,256] +norm_export:deploy/export/export_model.py +quant_export:null +fpgm_export:null +distill_export:null +export1:null +export2:null +===========================infer_params=========================== +infer_model:null +infer_export:null +infer_quant:False +inference:test_tipc/infer.py +--device:cpu|gpu +--enable_mkldnn:True +--cpu_threads:6 +--batch_size:1 +--use_trt:False +--precision:fp32 +--model_dir:null +--file_list:null:null +--save_log_path:null +--benchmark:True +--model_name:fccdn +null:null diff --git a/tutorials/train/change_detection/fccdn.py b/tutorials/train/change_detection/fccdn.py new file mode 100644 index 0000000..62abbba --- /dev/null +++ b/tutorials/train/change_detection/fccdn.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python + +# 变化检测模型FCCDN训练示例脚本 +# 执行此脚本前,请确认已正确安装PaddleRS库 + +import paddlers as pdrs +from paddlers import transforms as T + +# 数据集存放目录 +DATA_DIR = './data/airchange/' +# 训练集`file_list`文件路径 +TRAIN_FILE_LIST_PATH = './data/airchange/train.txt' +# 验证集`file_list`文件路径 +EVAL_FILE_LIST_PATH = './data/airchange/eval.txt' +# 实验目录,保存输出的模型权重和结果 +EXP_DIR = './output/fccdn/' + +# 下载和解压AirChange数据集 +pdrs.utils.download_and_decompress( + 'https://paddlers.bj.bcebos.com/datasets/airchange.zip', path='./data/') + +# 定义训练和验证时使用的数据变换(数据增强、预处理等) +# 使用Compose组合多种变换方式。Compose中包含的变换将按顺序串行执行 +# API说明:https://github.com/PaddlePaddle/PaddleRS/blob/develop/docs/apis/transforms.md +train_transforms = T.Compose([ + # 读取影像 + T.DecodeImg(), + # 随机裁剪 + T.RandomCrop( + # 裁剪区域将被缩放到256x256 + crop_size=256, + # 裁剪区域的横纵比在0.5-2之间变动 + aspect_ratio=[0.5, 2.0], + # 裁剪区域相对原始影像长宽比例在一定范围内变动,最小不低于原始长宽的1/5 + scaling=[0.2, 1.0]), + # 以50%的概率实施随机水平翻转 + T.RandomHorizontalFlip(prob=0.5), + # 将数据归一化到[-1,1] + T.Normalize( + mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]), + T.ArrangeChangeDetector('train') +]) + +eval_transforms = T.Compose([ + T.DecodeImg(), + # 验证阶段与训练阶段的数据归一化方式必须相同 + T.Normalize( + mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]), + T.ReloadMask(), + T.ArrangeChangeDetector('eval') +]) + +# 分别构建训练和验证所用的数据集 +train_dataset = pdrs.datasets.CDDataset( + data_dir=DATA_DIR, + file_list=TRAIN_FILE_LIST_PATH, + label_list=None, + transforms=train_transforms, + num_workers=0, + shuffle=True, + with_seg_labels=False, + binarize_labels=True) + +eval_dataset = pdrs.datasets.CDDataset( + data_dir=DATA_DIR, + file_list=EVAL_FILE_LIST_PATH, + label_list=None, + transforms=eval_transforms, + num_workers=0, + shuffle=False, + with_seg_labels=False, + binarize_labels=True) + +# 使用默认参数构建FCCDN模型 +# 目前已支持的模型及模型输入参数请参考: +# https://github.com/PaddlePaddle/PaddleRS/blob/develop/paddlers/tasks/change_detector.py +model = pdrs.tasks.cd.FCCDN() + +# 执行模型训练 +model.train( + num_epochs=5, + train_dataset=train_dataset, + train_batch_size=4, + eval_dataset=eval_dataset, + save_interval_epochs=2, + # 每多少次迭代记录一次日志 + log_interval_steps=50, + save_dir=EXP_DIR, + # 是否使用early stopping策略,当精度不再改善时提前终止训练 + early_stop=False, + # 是否启用VisualDL日志功能 + use_vdl=True, + # 指定从某个检查点继续训练 + resume_checkpoint=None) From d818c0c4521281cb9e58419bb7f7662631c9ef3a Mon Sep 17 00:00:00 2001 From: Lin Manhui Date: Fri, 26 Aug 2022 11:19:59 +0800 Subject: [PATCH 46/52] Update operators.py --- paddlers/transforms/operators.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/paddlers/transforms/operators.py b/paddlers/transforms/operators.py index ec9b424..94ffa92 100644 --- a/paddlers/transforms/operators.py +++ b/paddlers/transforms/operators.py @@ -883,9 +883,9 @@ class Normalize(Transform): std (list[float] | tuple[float], optional): Standard deviation of input image(s). Defaults to [0.229, 0.224, 0.225]. min_val (list[float] | tuple[float], optional): Minimum value of input - image(s). Defaults to [0, 0, 0, ]. + image(s). If None, use 0 for all channels. Defaults to None. max_val (list[float] | tuple[float], optional): Max value of input image(s). - Defaults to [255., 255., 255.]. + If None, use 255. for all channels. Defaults to None. """ def __init__(self, From 156179b7fc09fd0a6174d6c7abd6e1d0bf23ffaf Mon Sep 17 00:00:00 2001 From: Bobholamovic Date: Fri, 26 Aug 2022 13:51:43 +0800 Subject: [PATCH 47/52] Add comments --- tools/prepare_dataset/common.py | 93 +++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/tools/prepare_dataset/common.py b/tools/prepare_dataset/common.py index b9e1d82..1eb1b37 100644 --- a/tools/prepare_dataset/common.py +++ b/tools/prepare_dataset/common.py @@ -11,6 +11,15 @@ from tqdm import tqdm def get_default_parser(): + """ + Get argument parser with commonly used options. + + Returns: + argparse.ArgumentParser: Argument parser with the following arguments: + --in_dataset_dir: Input dataset directory. + --out_dataset_dir: Output dataset directory. + """ + parser = argparse.ArgumentParser() parser.add_argument( '--in_dataset_dir', @@ -23,6 +32,19 @@ def get_default_parser(): def add_crop_options(parser): + """ + Add patch cropping related arguments to an argument parser. The parser will be + modified in place. + + Args: + parser (argparse.ArgumentParser): Argument parser. + + Returns: + argparse.ArgumentParser: Argument parser with the following arguments: + --crop_size: Size of cropped patches. + --crop_stride: Stride of sliding windows when cropping patches. + """ + parser.add_argument( '--crop_size', type=int, help="Size of cropped patches.") parser.add_argument( @@ -58,9 +80,34 @@ def crop_patches(crop_size, subdirs=('A', 'B', 'label'), glob_pattern='*', max_workers=0): + """ + Crop patches from images in specific directories. + + Args: + crop_size (int): Height and width of the cropped patches will be `crop_size`. + stride (int): Stride of sliding windows when cropping patches. + data_dir (str): Root directory of the dataset that contains the input images. + out_dir (str): Directory to save the cropped patches. + subsets (tuple|list|None, optional): List or tuple of names of subdirectories + or None. Images to be cropped should be stored in `data_dir/subset/subdir/` + or `data_dir/subdir/` (when `subsets` is set to None), where `subset` is an + element of `subsets`. Defaults to ('train', 'val', 'test'). + subdirs (tuple|list, optional): List or tuple of names of subdirectories. Images + to be cropped should be stored in `data_dir/subset/subdir/` or + `data_dir/subdir/` (when `subsets` is set to None), where `subdir` is an + element of `subdirs`. Defaults to ('A', 'B', 'label'). + glob_pattern (str, optional): Glob pattern used to match image files. + Defaults to '*', which matches arbitrary file. + max_workers (int, optional): Number of worker threads to perform the cropping + operation. Deafults to 0. + """ + if max_workers < 0: raise ValueError("`max_workers` must be a non-negative integer!") + if subset is None: + subsets = ('', ) + if max_workers == 0: for subset in subsets: for subdir in subdirs: @@ -95,6 +142,34 @@ def crop_patches(crop_size, def get_path_tuples(*dirs, glob_pattern='*', data_dir=None): + """ + Get tuples of image paths. Each tuple corresponds to a sample in the dataset. + + Args: + *dirs (str): Directories that contains the images. + glob_pattern (str, optional): Glob pattern used to match image files. + Defaults to '*', which matches arbitrary file. + data_dir (str|None, optional): Root directory of the dataset that + contains the images. If not None, `data_dir` will be used to + determine relative paths of images. Defaults to None. + + Returns: + list[tuple]: For directories with the following structure: + ├── img + │ ├── im1.png + │ ├── im2.png + │ └── im3.png + │ + ├── mask + │ ├── im1.png + │ ├── im2.png + │ └── im3.png + └── ... + + `get_path_tuples('img', 'mask', '*.png')` will return list of tuples: + [('img/im1.png', 'mask/im1.png'), ('img/im2.png', 'mask/im2.png'), ('img/im3.png', 'mask/im3.png')] + """ + all_paths = [] for dir_ in dirs: paths = glob(osp.join(dir_, glob_pattern), recursive=True) @@ -107,6 +182,16 @@ def get_path_tuples(*dirs, glob_pattern='*', data_dir=None): def create_file_list(file_list, path_tuples, sep=' '): + """ + Create file list. + + Args: + file_list (str): Path of file list to create. + path_tuples (list[tuple]): See get_path_tuples(). + sep (str, optional): Delimiter to use when writing lines to file list. + Defaults to ' '. + """ + with open(file_list, 'w') as f: for tup in path_tuples: line = sep.join(tup) @@ -114,6 +199,14 @@ def create_file_list(file_list, path_tuples, sep=' '): def link_dataset(src, dst): + """ + Make a symbolic link to a dataset. + + Args: + src (str): Path of the original dataset. + dst (str): Path of the symbolic link. + """ + if osp.exists(dst) and not osp.isdir(dst): raise ValueError(f"{dst} exists and is not a directory.") elif not osp.exists(dst): From bdd3656de50212efda1d8d71c02d41cd9420bdca Mon Sep 17 00:00:00 2001 From: Bobholamovic Date: Fri, 26 Aug 2022 15:26:06 +0800 Subject: [PATCH 48/52] Add training based on scripts --- examples/rs_research/README.md | 101 +++++++++++----- .../configs/svcd/custom_model.yaml | 6 - examples/rs_research/configs/svcd/fc_ef.yaml | 6 - .../configs/svcd/fc_siam_conc.yaml | 6 - .../configs/svcd/fc_siam_diff.yaml | 6 - examples/rs_research/configs/svcd/svcd.yaml | 74 ------------ examples/rs_research/custom_trainer.py | 25 ++++ examples/rs_research/train_cd.py | 112 ++++++++++++++++++ 8 files changed, 209 insertions(+), 127 deletions(-) delete mode 100644 examples/rs_research/configs/svcd/custom_model.yaml delete mode 100644 examples/rs_research/configs/svcd/fc_ef.yaml delete mode 100644 examples/rs_research/configs/svcd/fc_siam_conc.yaml delete mode 100644 examples/rs_research/configs/svcd/fc_siam_diff.yaml delete mode 100644 examples/rs_research/configs/svcd/svcd.yaml create mode 100644 examples/rs_research/train_cd.py diff --git a/examples/rs_research/README.md b/examples/rs_research/README.md index b944ca5..f00b42c 100644 --- a/examples/rs_research/README.md +++ b/examples/rs_research/README.md @@ -4,7 +4,7 @@ ## 1 环境配置 -根据[教程](https://github.com/PaddlePaddle/PaddleRS/tree/develop/tutorials/train#环境准备)安装PaddleRS及相关依赖。在本项目中,GDAL库并不是必需的。 +根据[教程](https://github.com/PaddlePaddle/PaddleRS/tree/develop/tutorials/train#环境准备)安装PaddleRS及相关依赖。在本案例中,GDAL库并不是必需的。 配置好环境后,在PaddleRS仓库根目录中执行如下指令切换到本案例所在目录: @@ -58,8 +58,6 @@ FC-Siam-conc的网络结构如图所示: 本小节基于PaddlePaddle框架与PaddleRS库实现[3.1节](#31-问题分析与思路拟定)中提出的想法。 -#### 3.2.1 自定义模型组网 - 在`custom_model.py`中定义模型的整体结构以及组成模型的各个模块。本案例在`custom_model.py`中定义了改进后的FC-Siam-conc结构,其核心部分实现如下: ```python @@ -164,9 +162,39 @@ class MixedAttention(nn.Layer): 关于模型定义的更多细节请参考[《开发指南》](https://github.com/PaddlePaddle/PaddleRS/blob/develop/docs/dev/dev_guide.md)。 -#### 3.2.2 自定义训练器 +## 4 模型训练 + +本案例提供两种模型训练方式:基于脚本编写的方式与基于配置文件的方式。 + +- 对于初学者,建议使用脚本编写的方式:该方式更易理解,代码逻辑简单,且无需编写自定义训练器。 +- 对于较为熟练的科研者,或者是有开展大量对比实验、消融实验需求的科研者,建议使用基于配置文件的方式:该方式能够更方便地管理模型的不同配置,且易于并行执行多组实验。 + +需要说明的是,本文档中的实验结果均来自以基于配置文件方式训练的模型。本案例提供了本文档中涉及的全部实验的配置文件,存储在`configs`目录中。 + +### 4.1 基于脚本编写的方式 + +本案例提供`train_cd.py`脚本对模型进行训练和验证,并汇报验证集上最优模型在测试集上的精度。通过如下指令执行脚本: + +```bash +python train_cd.py +``` + +阅读脚本中的注释有助于使用者理解每个步骤的含义。脚本默认实现LEVIR-CD数据集上对自定义模型CustomModel的训练和验证。在实验过程中,可以根据需要修改脚本中的部分代码,以实现超参数调优或是对不同模型进行训练的功能。 + +训练程序默认开启VisualDL日志记录功能。训练过程中或训练完成后,可使用VisualDL观察损失函数和精度指标的变化情况。在PaddleRS中使用VisualDL的方式请参考[使用教程](https://github.com/PaddlePaddle/PaddleRS/blob/develop/tutorials/train/README.md#visualdl%E5%8F%AF%E8%A7%86%E5%8C%96%E8%AE%AD%E7%BB%83%E6%8C%87%E6%A0%87)。 + +### 4.2 基于配置文件的方式 + +#### 4.2.1 配置文件编写 + +本案例提供一个基于[YAML][https://yaml.org/]的轻量级配置系统,使用者可以通过修改yaml文件达到调整超参数、更换模型、更换数据集等目的,或通过编写yaml文件增加新的配置。 + +关于本案例中配置文件的编写规则,请参考[此项目](https://aistudio.baidu.com/aistudio/projectdetail/4203534)。 + +#### 4.2.2 自定义训练器 + +在使用基于配置文件方式进行模型训练时,需要在`custom_trainer.py`中定义训练器。例如,本案例在`custom_trainer.py`中定义了与`CustomModel`模型对应的训练器: -在`custom_trainer.py`中定义训练器。例如,本案例中,`custom_trainer.py`中定义了与`CustomModel`模型对应的训练器: ```python @attach class CustomTrainer(BaseChangeDetector): @@ -201,19 +229,20 @@ class CustomTrainer(BaseChangeDetector): 关于训练器的更多细节请参考[《API文档》](https://github.com/PaddlePaddle/PaddleRS/blob/develop/docs/apis/train.md)。 -## 4 对比实验 +配置文件中的`model`项可以指定训练器名称与构造参数。例如: -为了验证模型设计的有效性,通常需要开展对比实验,在一个或多个数据集上比较所提出模型与其它模型的精度和性能。在本案例中,将自定义模型CustomModel与FC-EF、FC-Siam-diff、FC-Siam-conc三种结构进行比较,这三个模型均来自论文[4]。 +```yaml +model: !Node + type: CustomTrainer + args: + att_types: c +``` -### 4.1 实验过程 +上述配置指定构造这样的一个训练器对象:`CustomTrainer(att_types=c)`。 -使用如下指令在LEVIR-CD数据集上执行对所有参与对比的模型的训练: +#### 4.2.3 训练指令 -```bash -bash scripts/run_benchmark.sh -``` - -或者,可以按照以下格式执行对某个模型的训练: +按照以下格式执行对某个模型的训练: ```bash python run_task.py train cd \ @@ -230,7 +259,19 @@ python run_task.py eval cd \ --resume_checkpoint "exp/levircd/{模型名称}/best_model" ``` -训练程序默认开启VisualDL日志记录功能。训练过程中或训练完成后,可使用VisualDL观察损失函数和精度指标的变化情况。在PaddleRS中使用VisualDL的方式请参考[使用教程](https://github.com/PaddlePaddle/PaddleRS/blob/develop/tutorials/train/README.md#visualdl%E5%8F%AF%E8%A7%86%E5%8C%96%E8%AE%AD%E7%BB%83%E6%8C%87%E6%A0%87)。 +## 5 对比实验 + +为了验证模型设计的有效性,通常需要开展对比实验,在一个或多个数据集上比较所提出模型与其它模型的精度和性能。在本案例中,将自定义模型CustomModel与FC-EF、FC-Siam-diff、FC-Siam-conc三种结构进行比较,这三个模型均来自论文[4]。 + +### 5.1 实验过程 + +**当使用基于配置文件的方式进行模型训练和验证时**,可以通过如下指令在LEVIR-CD数据集上执行对所有参与对比的模型的训练: + +```bash +bash scripts/run_benchmark.sh +``` + +**当使用`train_cd.py`脚本进行模型训练和验证时**,需要为每个实验手动更改模型的类型和构造参数。此外,可通过修改`EXP_DIR`变量为不同值,将每个模型对应的结果保存到不同的目录中,方便比较。本小节中的指令示例均假设实验过程中将`EXP_DIR`设置为`exp/levircd/{模型名称}`。 在训练和精度指标验证完成后,可以通过如下指令保存模型输出的二值变化图: @@ -274,11 +315,11 @@ python tools/collect_imgs.py --globs "exp/predict/levircd/{新增模型名称}/* python tools/analyze_model.py --model_dir "exp/levircd/{模型名称}/best_model" ``` -### 4.2 实验结果 +### 5.2 实验结果 本案例使用变化类的[交并比(intersection over union, IoU)](https://paddlepedia.readthedocs.io/en/latest/tutorials/computer_vision/semantic_segmentation/Overview/Overview.html#id6)和[F1分数](https://baike.baidu.com/item/F1%E5%88%86%E6%95%B0/13864979)作为定量评价指标,这两个指标越高,表示算法的检测效果越好。在每个数据集上,从目视效果和定量指标两个方面对算法效果进行评判。 -#### 4.2.1 目视效果对比 +#### 5.2.1 目视效果对比 下图展示了两个时相的输入影像、各算法输出的二值变化图(binary change map)以及变化标签。所选取的样本均来自LEVIR-CD数据集的测试集。 @@ -289,7 +330,7 @@ python tools/analyze_model.py --model_dir "exp/levircd/{模型名称}/best_model 从图中可以看出,虽然结果中仍存在一定程度的漏检与误检,但相比其它算法,CustomModel对变化区域的刻画相对更为准确。 -#### 4.2.2 定量指标对比 +#### 5.2.2 定量指标对比 |模型名称|FLOPs(G)|参数量(M)|IoU%|F1%| |:-:|:-:|:-:|:-:|:-:| @@ -300,7 +341,7 @@ python tools/analyze_model.py --model_dir "exp/levircd/{模型名称}/best_model 最高的精度指标用粗体表示。从表中可以看出,CustomModel取得了所有算法中最高的IoU和F1分数指标(与FC-EF对比IoU增加3.09%,F1增加1.89%),而其相比baseline模型FC-Siam-conc仅仅引入0.03 M的额外参数量。 -## 5 消融实验 +## 6 消融实验 在科研过程中,为了验证在baseline上所做修改的有效性,常常需要开展消融实验。在本案例中,CustomModel在FC-Siam-conc模型的基础上添加了通道和时间两种注意力模块,因此需要通过消融实验探讨各个注意力模块对最终精度的贡献。具体而言,包括以下4种实验情形(消融模型相关的配置文件存储在`configs/levircd/ablation`目录): @@ -309,11 +350,11 @@ python tools/analyze_model.py --model_dir "exp/levircd/{模型名称}/best_model 3. 仅添加时间注意力模块,对应的配置文件名称为`custom_model_t.yaml`; 4. 标准情况:同时添加通道和时间注意力模块的完整模型。 -其中第1和第4个模型,即baseline和完整模型,在[第4节](#4-对比实验)中已经得到了训练、验证和测试。因此,本节只需要关注情形2、3。 +其中第1和第4个模型,即baseline和完整模型,在[第4节](#4-模型训练)和[第5节](#5-对比实验)中已经得到了训练、验证和测试。因此,本节只需要关注情形2、3。 -### 5.1 实验过程 +### 6.1 实验过程 -使用如下指令执行全部消融模型的训练: +**当使用基于配置文件的方式进行模型训练时**,可通过如下指令训练全部消融模型: ```bash bash scripts/run_ablation.sh @@ -338,7 +379,9 @@ python run_task.py eval cd \ 注意,形如`custom_model_c.yaml`的配置文件默认对应的消融模型名称为`att_c`。 -### 5.2 实验结果 +**当使用`train_cd.py`进行模型训练时**,需要修改模型构造时的`att_types`参数,以得到不同消融模型的结果。例如,对于仅添加通道注意力模块的消融模型,应设置`att_types='c'`。此外,可通过修改`EXP_DIR`变量为不同值,将每个实验的结果保存到不同的目录中,方便比较。 + +### 6.2 实验结果 实验得到的定量指标如下表所示: @@ -351,11 +394,11 @@ python run_task.py eval cd \ 从表中数据可知,无论是通道注意力模块还是时间注意力模块都能对算法的IoU和F1分数指标带来正面贡献,而同时添加两种注意力模块带来的增益是最大的(相比baseline模型IoU增加0.83%,F1分数增加0.50%)。 -## 6 特征可视化实验 +## 7 特征可视化实验 本节主要对模型的中间特征进行可视化,以进一步验证对baseline模型所做的修改是否实现了增强特征的效果。 -### 6.1 实验过程 +### 7.1 实验过程 通过`tools/visualize_feats.py`脚本实现对模型中间特征的可视化。该脚本接受如下命令行选项: - `--model_dir`指定需要加载的模型的存储路径。 @@ -393,7 +436,7 @@ python tools/visualize_feats.py \ 执行上述指令将在`exp/vis/test_13_3/{模型名称}`目录中产生2个子目录,每个子目录中有2个文件,其中`in/att4_0_0_vis.png`和`in/att4_1_0_vis.png`分别表示输入`att4`模块的两个时相特征的可视化结果,`out/att4_0_0_vis.png`和`out/att4_1_0_vis.png`分别表示`att4`模块输出的两个时相特征的可视化结果。 -### 6.2 实验结果 +### 7.2 实验结果 下图从左往右分别为两个时相的输入影像、变化标签、输入混合注意力模块`att4`的两个时相特征图的可视化结果(分别用x1和x2代指)以及`att4`输出的两个时相特征图的可视化结果(分别用y1和y2代指): @@ -403,15 +446,15 @@ python tools/visualize_feats.py \ 对比x2和y2可以看出,经过通道和时间注意力模块处理后,变化特征得到了增强,发生变化的区域在特征图中更加凸显。 -## 7 总结与展望 +## 8 总结与展望 -### 7.1 总结 +### 8.1 总结 - 本案例以为经典的FC-Siam-conc模型添加注意力模块为例,演示了使用PaddleRS开展科研工作的典型流程。 - 本案例中对模型的改进带来了一定的目视效果的改善和检测精度的提升。 - 本案例通过消融实验和特征可视化实验证实了所提出改进的有效性。 -### 7.2 展望 +### 8.2 展望 - 本案例对所有参与比较的算法使用了相同的训练超参数,但由于模型之间存在差异,使用统一的超参训练往往难以保证所有模型都能取得较好的效果。在后续工作中,可以对每个对比算法进行调参,使其获得最优精度。 - 本案例作为使用PaddleRS开展科研工作的简单例子,并未在算法设计上做出较大改进,因此所提出算法相比baseline的精度提升也较为有限。未来可以考虑更复杂的算法设计,以及使用更加先进的模型结构。 diff --git a/examples/rs_research/configs/svcd/custom_model.yaml b/examples/rs_research/configs/svcd/custom_model.yaml deleted file mode 100644 index 20357a7..0000000 --- a/examples/rs_research/configs/svcd/custom_model.yaml +++ /dev/null @@ -1,6 +0,0 @@ -_base_: ./svcd.yaml - -save_dir: ./exp/svcd/custom_model/ - -model: !Node - type: CustomTrainer diff --git a/examples/rs_research/configs/svcd/fc_ef.yaml b/examples/rs_research/configs/svcd/fc_ef.yaml deleted file mode 100644 index 81bbb34..0000000 --- a/examples/rs_research/configs/svcd/fc_ef.yaml +++ /dev/null @@ -1,6 +0,0 @@ -_base_: ./svcd.yaml - -save_dir: ./exp/svcd/fc_ef/ - -model: !Node - type: FCEarlyFusion diff --git a/examples/rs_research/configs/svcd/fc_siam_conc.yaml b/examples/rs_research/configs/svcd/fc_siam_conc.yaml deleted file mode 100644 index fb4eed8..0000000 --- a/examples/rs_research/configs/svcd/fc_siam_conc.yaml +++ /dev/null @@ -1,6 +0,0 @@ -_base_: ./svcd.yaml - -save_dir: ./exp/svcd/fc_siam_conc/ - -model: !Node - type: FCSiamConc diff --git a/examples/rs_research/configs/svcd/fc_siam_diff.yaml b/examples/rs_research/configs/svcd/fc_siam_diff.yaml deleted file mode 100644 index fde20b9..0000000 --- a/examples/rs_research/configs/svcd/fc_siam_diff.yaml +++ /dev/null @@ -1,6 +0,0 @@ -_base_: ./svcd.yaml - -save_dir: ./exp/svcd/fc_siam_diff/ - -model: !Node - type: FCSiamDiff diff --git a/examples/rs_research/configs/svcd/svcd.yaml b/examples/rs_research/configs/svcd/svcd.yaml deleted file mode 100644 index a8a19fa..0000000 --- a/examples/rs_research/configs/svcd/svcd.yaml +++ /dev/null @@ -1,74 +0,0 @@ -# Basic configurations of SVCD dataset - -datasets: - train: !Node - type: CDDataset - args: - data_dir: ./data/svcd/ - file_list: ./data/svcd/train.txt - label_list: null - num_workers: 2 - shuffle: True - with_seg_labels: False - binarize_labels: True - eval: !Node - type: CDDataset - args: - data_dir: ./data/svcd/ - file_list: ./data/svcd/val.txt - label_list: null - num_workers: 0 - shuffle: False - with_seg_labels: False - binarize_labels: True -transforms: - train: - - !Node - type: DecodeImg - - !Node - type: RandomFlipOrRotate - args: - probs: [0.35, 0.35] - probsf: [0.5, 0.5, 0, 0, 0] - probsr: [0.33, 0.34, 0.33] - - !Node - type: Normalize - args: - mean: [0.5, 0.5, 0.5] - std: [0.5, 0.5, 0.5] - - !Node - type: ArrangeChangeDetector - args: ['train'] - eval: - - !Node - type: DecodeImg - - !Node - type: Normalize - args: - mean: [0.5, 0.5, 0.5] - std: [0.5, 0.5, 0.5] - - !Node - type: ArrangeChangeDetector - args: ['eval'] -download_on: False - -num_epochs: 200 -train_batch_size: 8 -optimizer: !Node - type: Adam - args: - learning_rate: !Node - type: StepDecay - module: paddle.optimizer.lr - args: - learning_rate: 0.0004 - step_size: 87500 - gamma: 0.1 -save_interval_epochs: 20 -log_interval_steps: 50 -save_dir: ./exp/ -learning_rate: 0.0004 -early_stop: False -early_stop_patience: 5 -use_vdl: True -resume_checkpoint: '' diff --git a/examples/rs_research/custom_trainer.py b/examples/rs_research/custom_trainer.py index f0bdb3f..6771b36 100644 --- a/examples/rs_research/custom_trainer.py +++ b/examples/rs_research/custom_trainer.py @@ -1,3 +1,4 @@ +import paddle import paddlers from paddlers.tasks.change_detector import BaseChangeDetector @@ -6,6 +7,30 @@ from attach_tools import Attach attach = Attach.to(paddlers.tasks.change_detector) +def make_trainer(net_type, *args, **kwargs): + def _init_func(self, + num_classes=2, + use_mixed_loss=False, + losses=None, + **params): + super().__init__( + model_name=net_type.__name__, + num_classes=num_classes, + use_mixed_loss=use_mixed_loss, + losses=losses, + **params) + + if not issubclass(net_type, paddle.nn.Layer): + raise TypeError("net must be a subclass of paddle.nn.Layer") + + trainer_name = net_type.__name__ + + trainer_type = type(trainer_name, (BaseChangeDetector, ), + {'__init__': _init_func}) + + return trainer_type(*args, **kwargs) + + @attach class CustomTrainer(BaseChangeDetector): def __init__(self, diff --git a/examples/rs_research/train_cd.py b/examples/rs_research/train_cd.py new file mode 100644 index 0000000..85fdebc --- /dev/null +++ b/examples/rs_research/train_cd.py @@ -0,0 +1,112 @@ +#!/usr/bin/env bash + +import os.path as osp + +import paddle +import paddlers as pdrs +from paddlers import transforms as T + +from custom_model import CustomModel +from custom_trainer import make_trainer + +# 数据集路径 +DATA_DIR = 'data/levircd/' +# 保存实验结果的路径 +EXP_DIR = 'exp/levircd/custom_model/' + +# 定义训练和验证时使用的数据变换(数据增强、预处理等) +# 使用Compose组合多种变换方式。Compose中包含的变换将按顺序串行执行 +# API说明:https://github.com/PaddlePaddle/PaddleRS/blob/develop/docs/apis/data.md +train_transforms = T.Compose([ + # 读取影像 + T.DecodeImg(), + # 随机翻转和旋转 + T.RandomFlipOrRotate( + # 以0.35的概率执行随机翻转,0.35的概率执行随机旋转 + probs=[0.35, 0.35], + # 以0.5的概率执行随机水平翻转,0.5的概率执行随机垂直翻转 + probsf=[0.5, 0.5, 0, 0, 0], + # 分别以0.33、0.34和0.33的概率执行90°、180°和270°旋转 + probsr=[0.33, 0.34, 0.33]), + # 将数据归一化到[-1,1] + T.Normalize( + mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]), + T.ArrangeChangeDetector('train') +]) + +eval_transforms = T.Compose([ + T.DecodeImg(), + # 验证阶段与训练阶段的数据归一化方式必须相同 + T.Normalize( + mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]), + T.ArrangeChangeDetector('eval') +]) + +# 分别构建训练、验证和测试所用的数据集 +train_dataset = pdrs.datasets.CDDataset( + data_dir=DATA_DIR, + file_list=osp.join(DATA_DIR, 'train.txt'), + label_list=None, + transforms=train_transforms, + num_workers=0, + shuffle=True, + with_seg_labels=False, + binarize_labels=True) + +val_dataset = pdrs.datasets.CDDataset( + data_dir=DATA_DIR, + file_list=osp.join(DATA_DIR, 'val.txt'), + label_list=None, + transforms=eval_transforms, + num_workers=0, + shuffle=False, + with_seg_labels=False, + binarize_labels=True) + +test_dataset = pdrs.datasets.CDDataset( + data_dir=DATA_DIR, + file_list=osp.join(DATA_DIR, 'test.txt'), + label_list=None, + # 与验证阶段使用相同的数据变换算子 + transforms=eval_transforms, + num_workers=0, + shuffle=False, + with_seg_labels=False, + binarize_labels=True) + +# 构建自定义模型CustomModel并为其自动生成训练器 +# make_trainer()的首个参数为模型类型,剩余参数为模型构造所需参数 +# 这里使用默认参数构造 +model = make_trainer(CustomModel) + +# 构建学习率调度器 +# 使用定步长学习率衰减策略 +lr_scheduler = paddle.optimizer.lr.StepDecay( + learning_rate=0.002, step_size=35000, gamma=0.2) + +# 构建优化器 +optimizer = paddle.optimizer.Adam( + model.net.parameters(), learning_rate=lr_scheduler) + +# 执行模型训练 +model.train( + num_epochs=50, + train_dataset=train_dataset, + train_batch_size=8, + eval_dataset=eval_dataset, + # 每多少个epoch验证并保存一次模型 + save_interval_epochs=5, + # 每多少次迭代记录一次日志 + log_interval_steps=50, + save_dir=EXP_DIR, + # 是否使用early stopping策略,当精度不再改善时提前终止训练 + early_stop=False, + # 是否启用VisualDL日志功能 + use_vdl=True, + # 指定从某个检查点继续训练 + resume_checkpoint=None) + +# 加载验证集上效果最好的模型 +model = pdrs.tasks.load_model(osp.join(EXP_DIR, 'best_model')) +# 在测试集上计算精度指标 +model.evaluate(test_dataset) From 7cccfe93c352510804ec14a5adc939d410b2a23d Mon Sep 17 00:00:00 2001 From: Bobholamovic Date: Fri, 26 Aug 2022 15:27:24 +0800 Subject: [PATCH 49/52] Add header comment --- examples/rs_research/attach_tools.py | 15 +++++++++++++++ examples/rs_research/config_utils.py | 14 ++++++++++++++ examples/rs_research/custom_model.py | 14 ++++++++++++++ examples/rs_research/custom_trainer.py | 14 ++++++++++++++ examples/rs_research/predict_cd.py | 14 ++++++++++++++ examples/rs_research/run_task.py | 14 ++++++++++++++ examples/rs_research/tools/analyze_model.py | 14 ++++++++++++++ examples/rs_research/tools/collect_imgs.py | 14 ++++++++++++++ examples/rs_research/tools/visualize_feats.py | 14 ++++++++++++++ 9 files changed, 127 insertions(+) diff --git a/examples/rs_research/attach_tools.py b/examples/rs_research/attach_tools.py index 3b737cf..ce69cc1 100644 --- a/examples/rs_research/attach_tools.py +++ b/examples/rs_research/attach_tools.py @@ -1,3 +1,18 @@ +# Copyright (c) 2022 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. + + class Attach(object): def __init__(self, dst): self.dst = dst diff --git a/examples/rs_research/config_utils.py b/examples/rs_research/config_utils.py index 9f1b6fc..1effc1a 100644 --- a/examples/rs_research/config_utils.py +++ b/examples/rs_research/config_utils.py @@ -1,5 +1,19 @@ #!/usr/bin/env python +# Copyright (c) 2022 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 argparse import os.path as osp from collections.abc import Mapping diff --git a/examples/rs_research/custom_model.py b/examples/rs_research/custom_model.py index b0dbda7..c061029 100644 --- a/examples/rs_research/custom_model.py +++ b/examples/rs_research/custom_model.py @@ -1,3 +1,17 @@ +# Copyright (c) 2022 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 paddle import paddle.nn as nn import paddle.nn.functional as F diff --git a/examples/rs_research/custom_trainer.py b/examples/rs_research/custom_trainer.py index 6771b36..fd970f5 100644 --- a/examples/rs_research/custom_trainer.py +++ b/examples/rs_research/custom_trainer.py @@ -1,3 +1,17 @@ +# Copyright (c) 2022 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 paddle import paddlers from paddlers.tasks.change_detector import BaseChangeDetector diff --git a/examples/rs_research/predict_cd.py b/examples/rs_research/predict_cd.py index df0b8b4..aced776 100644 --- a/examples/rs_research/predict_cd.py +++ b/examples/rs_research/predict_cd.py @@ -1,5 +1,19 @@ #!/usr/bin/env python +# Copyright (c) 2022 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 argparse import os import os.path as osp diff --git a/examples/rs_research/run_task.py b/examples/rs_research/run_task.py index f3f1c54..a487c45 100644 --- a/examples/rs_research/run_task.py +++ b/examples/rs_research/run_task.py @@ -1,5 +1,19 @@ #!/usr/bin/env python +# Copyright (c) 2022 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 os # Import cv2 and sklearn before paddlers to solve the diff --git a/examples/rs_research/tools/analyze_model.py b/examples/rs_research/tools/analyze_model.py index 3eec5b4..5d00cad 100644 --- a/examples/rs_research/tools/analyze_model.py +++ b/examples/rs_research/tools/analyze_model.py @@ -1,5 +1,19 @@ #!/usr/bin/env python +# Copyright (c) 2022 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. + # Refer to https://github.com/PaddlePaddle/PaddleSeg/blob/release/2.6/tools/analyze_model.py import argparse diff --git a/examples/rs_research/tools/collect_imgs.py b/examples/rs_research/tools/collect_imgs.py index 2e7d0ff..326d491 100644 --- a/examples/rs_research/tools/collect_imgs.py +++ b/examples/rs_research/tools/collect_imgs.py @@ -1,5 +1,19 @@ #!/usr/bin/env python +# Copyright (c) 2022 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 argparse import os import os.path as osp diff --git a/examples/rs_research/tools/visualize_feats.py b/examples/rs_research/tools/visualize_feats.py index bf4ed16..8dd38b5 100644 --- a/examples/rs_research/tools/visualize_feats.py +++ b/examples/rs_research/tools/visualize_feats.py @@ -1,5 +1,19 @@ #!/usr/bin/env python +# Copyright (c) 2022 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 argparse import sys import os From a373e1183517382546967dcab32dd2b31c22c23d Mon Sep 17 00:00:00 2001 From: Bobholamovic Date: Fri, 26 Aug 2022 15:56:42 +0800 Subject: [PATCH 50/52] Fix bugs --- docs/intro/transforms.md | 2 +- examples/rs_research/custom_trainer.py | 15 +++++++++++++-- examples/rs_research/train_cd.py | 5 ++--- paddlers/tasks/change_detector.py | 4 +--- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/docs/intro/transforms.md b/docs/intro/transforms.md index 6144704..c7234de 100644 --- a/docs/intro/transforms.md +++ b/docs/intro/transforms.md @@ -12,7 +12,7 @@ PaddleRS对不同遥感任务需要的数据预处理/数据增强(合称为 | RandomResizeByShort | 随机调整输入影像大小,保持纵横比不变(根据短边计算缩放系数)。 | 所有任务 | ... | | ResizeByLong | 调整输入影像大小,保持纵横比不变(根据长边计算缩放系数)。 | 所有任务 | ... | | RandomHorizontalFlip | 随机水平翻转输入影像。 | 所有任务 | ... | -| RandomVerticalFlip | 随机竖直翻转输入影像。 | 所有任务 | ... | +| RandomVerticalFlip | 随机垂直翻转输入影像。 | 所有任务 | ... | | Normalize | 对输入影像应用标准化。 | 所有任务 | ... | | CenterCrop | 对输入影像进行中心裁剪。 | 所有任务 | ... | | RandomCrop | 对输入影像进行随机中心裁剪。 | 所有任务 | ... | diff --git a/examples/rs_research/custom_trainer.py b/examples/rs_research/custom_trainer.py index fd970f5..07577c8 100644 --- a/examples/rs_research/custom_trainer.py +++ b/examples/rs_research/custom_trainer.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import inspect + import paddle import paddlers from paddlers.tasks.change_detector import BaseChangeDetector @@ -27,12 +29,21 @@ def make_trainer(net_type, *args, **kwargs): use_mixed_loss=False, losses=None, **params): - super().__init__( + sig = inspect.signature(net_type.__init__) + net_params = { + k: p.default + for k, p in sig.parameters.items() if not p.default is p.empty + } + net_params.pop('self', None) + net_params.pop('num_classes', None) + net_params.update(params) + + super(trainer_type, self).__init__( model_name=net_type.__name__, num_classes=num_classes, use_mixed_loss=use_mixed_loss, losses=losses, - **params) + **net_params) if not issubclass(net_type, paddle.nn.Layer): raise TypeError("net must be a subclass of paddle.nn.Layer") diff --git a/examples/rs_research/train_cd.py b/examples/rs_research/train_cd.py index 85fdebc..f0c0410 100644 --- a/examples/rs_research/train_cd.py +++ b/examples/rs_research/train_cd.py @@ -76,8 +76,7 @@ test_dataset = pdrs.datasets.CDDataset( # 构建自定义模型CustomModel并为其自动生成训练器 # make_trainer()的首个参数为模型类型,剩余参数为模型构造所需参数 -# 这里使用默认参数构造 -model = make_trainer(CustomModel) +model = make_trainer(CustomModel, in_channels=3) # 构建学习率调度器 # 使用定步长学习率衰减策略 @@ -86,7 +85,7 @@ lr_scheduler = paddle.optimizer.lr.StepDecay( # 构建优化器 optimizer = paddle.optimizer.Adam( - model.net.parameters(), learning_rate=lr_scheduler) + parameters=model.net.parameters(), learning_rate=lr_scheduler) # 执行模型训练 model.train( diff --git a/paddlers/tasks/change_detector.py b/paddlers/tasks/change_detector.py index 9af34f8..60ff25e 100644 --- a/paddlers/tasks/change_detector.py +++ b/paddlers/tasks/change_detector.py @@ -52,9 +52,7 @@ class BaseChangeDetector(BaseModel): if 'with_net' in self.init_params: del self.init_params['with_net'] super(BaseChangeDetector, self).__init__('change_detector') - if model_name not in __all__: - raise ValueError("ERROR: There is no model named {}.".format( - model_name)) + self.model_name = model_name self.num_classes = num_classes self.use_mixed_loss = use_mixed_loss From f9ba55c686971c347d0ed09774ae336ba3bbeac0 Mon Sep 17 00:00:00 2001 From: Lin Manhui Date: Sat, 27 Aug 2022 19:43:38 +0800 Subject: [PATCH 51/52] [Doc] Fix typo --- examples/rs_research/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/rs_research/README.md b/examples/rs_research/README.md index f00b42c..73b52cf 100644 --- a/examples/rs_research/README.md +++ b/examples/rs_research/README.md @@ -187,7 +187,7 @@ python train_cd.py #### 4.2.1 配置文件编写 -本案例提供一个基于[YAML][https://yaml.org/]的轻量级配置系统,使用者可以通过修改yaml文件达到调整超参数、更换模型、更换数据集等目的,或通过编写yaml文件增加新的配置。 +本案例提供一个基于[YAML](https://yaml.org/)的轻量级配置系统,使用者可以通过修改yaml文件达到调整超参数、更换模型、更换数据集等目的,或通过编写yaml文件增加新的配置。 关于本案例中配置文件的编写规则,请参考[此项目](https://aistudio.baidu.com/aistudio/projectdetail/4203534)。 From d0189ba6232975784d78b6467200bc8ee0f4c3dc Mon Sep 17 00:00:00 2001 From: Lin Manhui Date: Tue, 30 Aug 2022 15:51:13 +0800 Subject: [PATCH 52/52] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 11e2f9c..fd886ce 100644 --- a/README.md +++ b/README.md @@ -179,7 +179,7 @@ PaddleRS目录树中关键部分如下: * 如果您发现任何PaddleRS存在的问题或是对PaddleRS有建议, 欢迎通过[GitHub Issues](https://github.com/PaddlePaddle/PaddleRS/issues)向我们提出。 * 欢迎加入PaddleRS微信群
- +
## 使用教程