diff --git a/.gitignore b/.gitignore index 724f0dd..43b91b0 100644 --- a/.gitignore +++ b/.gitignore @@ -131,6 +131,7 @@ dmypy.json # testdata tutorials/train/change_detection/DataSet/ +tutorials/train/classification/DataSet/ optic_disc_seg.tar optic_disc_seg/ output/ diff --git a/paddlers/datasets/clas_dataset.py b/paddlers/datasets/clas_dataset.py index f6c8379..45a00d9 100644 --- a/paddlers/datasets/clas_dataset.py +++ b/paddlers/datasets/clas_dataset.py @@ -25,7 +25,7 @@ class ClasDataset(Dataset): Args: data_dir (str): 数据集所在的目录路径。 file_list (str): 描述数据集图片文件和对应标注序号(文本内每行路径为相对data_dir的相对路)。 - label_list (str): 描述数据集包含的类别信息文件路径。默认值为None。 + label_list (str): 描述数据集包含的类别信息文件路径,文件格式为(类别 说明)。默认值为None。 transforms (paddlers.transforms): 数据集中每个样本的预处理/增强算子。 num_workers (int|str): 数据集中样本在预处理过程中的线程或进程数。默认为'auto'。 shuffle (bool): 是否需要对数据集中样本打乱顺序。默认为False。 @@ -45,6 +45,7 @@ class ClasDataset(Dataset): self.num_workers = get_num_workers(num_workers) self.shuffle = shuffle self.file_list = list() + self.label_list = label_list self.labels = list() # TODO:非None时,让用户跳转数据集分析生成label_list diff --git a/paddlers/models/ppcls/loss/__init__.py b/paddlers/models/ppcls/loss/__init__.py index d15dab9..19e6a69 100644 --- a/paddlers/models/ppcls/loss/__init__.py +++ b/paddlers/models/ppcls/loss/__init__.py @@ -63,5 +63,5 @@ class CombinedLoss(nn.Layer): def build_loss(config): module_class = CombinedLoss(copy.deepcopy(config)) - logger.debug("build loss {} success.".format(module_class)) + # logger.debug("build loss {} success.".format(module_class)) return module_class diff --git a/paddlers/tasks/classifier.py b/paddlers/tasks/classifier.py index 8c961f9..f25bcf5 100644 --- a/paddlers/tasks/classifier.py +++ b/paddlers/tasks/classifier.py @@ -15,7 +15,6 @@ import math import os.path as osp import numpy as np -import cv2 from collections import OrderedDict import paddle import paddle.nn.functional as F @@ -26,8 +25,10 @@ from paddlers.transforms import arrange_transforms from paddlers.utils import get_single_card_bs, DisablePrint import paddlers.utils.logging as logging from .base import BaseModel -from .utils import seg_metrics as metrics -from paddlers.utils.checkpoint import seg_pretrain_weights_dict +from paddlers.models.ppcls.metric import build_metrics +from paddlers.models.ppcls.loss import build_loss +from paddlers.models.ppcls.data.postprocess import build_postprocess +from paddlers.utils.checkpoint import imagenet_weights from paddlers.transforms import Decode, Resize __all__ = ["ResNet50_vd", "MobileNetV3_small_x1_0", "HRNet_W18_C"] @@ -49,8 +50,10 @@ class BaseClassifier(BaseModel): self.model_name = model_name self.num_classes = num_classes self.use_mixed_loss = use_mixed_loss + self.metrics = None self.losses = None self.labels = None + self._postprocess = None if params.get('with_net', True): params.pop('with_net', None) self.net = self.build_net(**params) @@ -97,95 +100,35 @@ class BaseClassifier(BaseModel): ] return input_spec - # FIXME: use ppcls instead of ppseg, in infet / metrics and etc. def run(self, net, inputs, mode): net_out = net(inputs[0]) - logit = net_out[0] + label = paddle.to_tensor(inputs[1], dtype="int64") outputs = OrderedDict() if mode == 'test': - origin_shape = inputs[1] - if self.status == 'Infer': - label_map_list, score_map_list = self._postprocess( - net_out, origin_shape, transforms=inputs[2]) - else: - logit_list = self._postprocess( - logit, origin_shape, transforms=inputs[2]) - label_map_list = [] - score_map_list = [] - for logit in logit_list: - logit = paddle.transpose(logit, perm=[0, 2, 3, 1]) # NHWC - label_map_list.append( - paddle.argmax( - logit, axis=-1, keepdim=False, dtype='int32') - .squeeze().numpy()) - score_map_list.append( - F.softmax( - logit, axis=-1).squeeze().numpy().astype( - 'float32')) - outputs['label_map'] = label_map_list - outputs['score_map'] = score_map_list + result = self._postprocess(net_out) + outputs = result[0] if mode == 'eval': - if self.status == 'Infer': - pred = paddle.unsqueeze(net_out[0], axis=1) # NCHW - else: - pred = paddle.argmax( - logit, axis=1, keepdim=True, dtype='int32') - label = inputs[1] - origin_shape = [label.shape[-2:]] - 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) - outputs['intersect_area'] = intersect_area - outputs['pred_area'] = pred_area - outputs['label_area'] = label_area - outputs['conf_mat'] = metrics.confusion_matrix(pred, label, - self.num_classes) + # 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"] + outputs['top5'] = metric_dict["top5"] + if mode == 'train': - loss_list = metrics.loss_computation( - logits_list=net_out, labels=inputs[1], losses=self.losses) - loss = sum(loss_list) - outputs['loss'] = loss + loss_list = self.losses(net_out, label) + outputs['loss'] = loss_list['loss'] return outputs - # FIXME: use ppcls instead of ppseg, in loss. + def default_metric(self): + # TODO: other metrics + default_config = [{"TopkAcc":{"topk": [1, 5]}}] + return build_metrics(default_config) + def default_loss(self): - if isinstance(self.use_mixed_loss, bool): - if self.use_mixed_loss: - losses = [ - paddleseg.models.CrossEntropyLoss(), - paddleseg.models.LovaszSoftmaxLoss() - ] - coef = [.8, .2] - loss_type = [ - paddleseg.models.MixedLoss( - losses=losses, coef=coef), - ] - else: - loss_type = [paddleseg.models.CrossEntropyLoss()] - else: - losses, coef = list(zip(*self.use_mixed_loss)) - if not set(losses).issubset( - ['CrossEntropyLoss', 'DiceLoss', 'LovaszSoftmaxLoss']): - raise ValueError( - "Only 'CrossEntropyLoss', 'DiceLoss', 'LovaszSoftmaxLoss' are supported." - ) - losses = [getattr(paddleseg.models, loss)() for loss in losses] - loss_type = [ - paddleseg.models.MixedLoss( - losses=losses, coef=list(coef)) - ] - if self.model_name == 'FastSCNN': - loss_type *= 2 - loss_coef = [1.0, 0.4] - elif self.model_name == 'BiSeNetV2': - loss_type *= 5 - loss_coef = [1.0] * 5 - else: - loss_coef = [1.0] - losses = {'types': loss_type, 'coef': loss_coef} - return losses + # TODO: mixed_loss + default_config = [{"CELoss":{"weight": 1.0}}] + return build_loss(default_config) def default_optimizer(self, parameters, @@ -203,6 +146,14 @@ class BaseClassifier(BaseModel): weight_decay=4e-5) return optimizer + def default_postprocess(self, class_id_map_file): + default_config = { + "name": "Topk", + "topk": 1, + "class_id_map_file": class_id_map_file + } + return build_postprocess(default_config) + def train(self, num_epochs, train_dataset, @@ -212,7 +163,7 @@ class BaseClassifier(BaseModel): save_interval_epochs=1, log_interval_steps=2, save_dir='output', - pretrain_weights='CITYSCAPES', # FIXME: fix clas's pretrain weights + pretrain_weights='IMAGENET', learning_rate=0.01, lr_decay_power=0.9, early_stop=False, @@ -255,6 +206,9 @@ class BaseClassifier(BaseModel): self.labels = train_dataset.labels 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) if optimizer is None: num_steps_each_epoch = train_dataset.num_samples // train_batch_size @@ -265,7 +219,7 @@ class BaseClassifier(BaseModel): self.optimizer = optimizer if pretrain_weights is not None and not osp.exists(pretrain_weights): - if pretrain_weights not in seg_pretrain_weights_dict[ + if pretrain_weights not in imagenet_weights[ self.model_name]: logging.warning( "Path of pretrain_weights('{}') does not exist!".format( @@ -273,9 +227,9 @@ class BaseClassifier(BaseModel): logging.warning("Pretrain_weights is forcibly set to '{}'. " "If don't want to use pretrain weights, " "set pretrain_weights to be None.".format( - seg_pretrain_weights_dict[self.model_name][ + imagenet_weights[self.model_name][ 0])) - pretrain_weights = seg_pretrain_weights_dict[self.model_name][ + pretrain_weights = imagenet_weights[self.model_name][ 0] elif pretrain_weights is not None and osp.exists(pretrain_weights): if osp.splitext(pretrain_weights)[-1] != '.pdparams': @@ -370,12 +324,8 @@ class BaseClassifier(BaseModel): Returns: collections.OrderedDict with key-value pairs: - {"miou": `mean intersection over union`, - "category_iou": `category-wise mean intersection over union`, - "oacc": `overall accuracy`, - "category_acc": `category-wise accuracy`, - "kappa": ` kappa coefficient`, - "category_F1-score": `F1 score`}. + {"top1": `acc of top1`, + "top5": `acc of top5`}. """ arrange_transforms( @@ -403,73 +353,26 @@ class BaseClassifier(BaseModel): self.eval_data_loader = self.build_data_loader( eval_dataset, batch_size=batch_size, mode='eval') - intersect_area_all = 0 - pred_area_all = 0 - label_area_all = 0 - conf_mat_all = [] 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') - pred_area = outputs['pred_area'] - label_area = outputs['label_area'] - intersect_area = outputs['intersect_area'] - conf_mat = outputs['conf_mat'] - - # Gather from all ranks - if nranks > 1: - intersect_area_list = [] - pred_area_list = [] - label_area_list = [] - conf_mat_list = [] - paddle.distributed.all_gather(intersect_area_list, - intersect_area) - paddle.distributed.all_gather(pred_area_list, pred_area) - paddle.distributed.all_gather(label_area_list, label_area) - paddle.distributed.all_gather(conf_mat_list, conf_mat) - - # Some image has been evaluated and should be eliminated in last iter - if (step + 1) * nranks > len(eval_dataset): - valid = len(eval_dataset) - step * nranks - intersect_area_list = intersect_area_list[:valid] - pred_area_list = pred_area_list[:valid] - label_area_list = label_area_list[:valid] - conf_mat_list = conf_mat_list[:valid] - - intersect_area_all += sum(intersect_area_list) - pred_area_all += sum(pred_area_list) - label_area_all += sum(label_area_list) - conf_mat_all.extend(conf_mat_list) - - else: - intersect_area_all = intersect_area_all + intersect_area - pred_area_all = pred_area_all + pred_area - label_area_all = label_area_all + label_area - conf_mat_all.append(conf_mat) - # FIXME: fix metrics - class_iou, miou = paddleseg.utils.metrics.mean_iou( - intersect_area_all, pred_area_all, label_area_all) - # TODO 确认是按oacc还是macc - class_acc, oacc = paddleseg.utils.metrics.accuracy(intersect_area_all, - pred_area_all) - kappa = paddleseg.utils.metrics.kappa(intersect_area_all, - pred_area_all, label_area_all) - category_f1score = metrics.f1_score(intersect_area_all, pred_area_all, - label_area_all) - eval_metrics = OrderedDict( - zip([ - 'miou', 'category_iou', 'oacc', 'category_acc', 'kappa', - 'category_F1-score' - ], [miou, class_iou, oacc, class_acc, kappa, category_f1score])) + 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: - conf_mat = sum(conf_mat_all) - eval_details = {'confusion_matrix': conf_mat.tolist()} - return eval_metrics, eval_details + # TODO: add details + return eval_metrics, None return eval_metrics def predict(self, img_file, transforms=None): @@ -485,10 +388,11 @@ class BaseClassifier(BaseModel): 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`}. + {"label map": `class_ids_map`, "scores_map": `label_names_map`}. 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) + class_ids_map(np.ndarray): class_ids + scores_map(np.ndarray): scores + label_names_map(np.ndarray): label_names """ if transforms is None and not hasattr(self, 'test_transforms'): @@ -504,21 +408,23 @@ class BaseClassifier(BaseModel): self.net.eval() data = (batch_im, batch_origin_shape, transforms.transforms) outputs = self.run(self.net, data, 'test') - label_map_list = outputs['label_map'] - score_map_list = outputs['score_map'] + label_list = outputs['class_ids'] + score_list = outputs['scores'] + name_list = outputs['label_names'] if isinstance(img_file, list): prediction = [{ - 'label_map': l, - 'score_map': s - } for l, s in zip(label_map_list, score_map_list)] + 'class_ids_map': l, + 'scores_map': s, + 'label_names_map': n, + } for l, s, n in zip(label_list, score_list, name_list)] else: prediction = { - 'label_map': label_map_list[0], - 'score_map': score_map_list[0] + 'class_ids': label_list[0], + 'scores': score_list[0], + 'label_names': name_list[0] } return prediction - # FIXME: adaptive clas def _preprocess(self, images, transforms, to_tensor=True): arrange_transforms( model_type=self.model_type, transforms=transforms, mode='test') @@ -587,84 +493,6 @@ class BaseClassifier(BaseModel): batch_restore_list.append(restore_list) return batch_restore_list - # FIXME: adaptive clas - def _postprocess(self, batch_pred, batch_origin_shape, transforms): - batch_restore_list = BaseClassifier.get_transforms_shape_info( - batch_origin_shape, transforms) - if isinstance(batch_pred, (tuple, list)) and self.status == 'Infer': - return self._infer_postprocess( - batch_label_map=batch_pred[0], - batch_score_map=batch_pred[1], - 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 - - # FIXME: adaptive clas - def _infer_postprocess(self, batch_label_map, batch_score_map, - batch_restore_list): - label_maps = [] - score_maps = [] - for label_map, score_map, restore_list in zip( - batch_label_map, batch_score_map, batch_restore_list): - if not isinstance(label_map, np.ndarray): - label_map = paddle.unsqueeze(label_map, axis=[0, 3]) - score_map = paddle.unsqueeze(score_map, axis=0) - for item in restore_list[::-1]: - h, w = item[1][0], item[1][1] - if item[0] == 'resize': - if isinstance(label_map, np.ndarray): - label_map = cv2.resize( - label_map, (w, h), interpolation=cv2.INTER_NEAREST) - score_map = cv2.resize( - score_map, (w, h), interpolation=cv2.INTER_LINEAR) - else: - label_map = F.interpolate( - label_map, (h, w), - mode='nearest', - data_format='NHWC') - score_map = F.interpolate( - score_map, (h, w), - mode='bilinear', - data_format='NHWC') - 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] - else: - 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() - score_map = score_map.squeeze() - if not isinstance(label_map, np.ndarray): - label_map = label_map.numpy() - score_map = score_map.numpy() - label_maps.append(label_map.squeeze()) - score_maps.append(score_map.squeeze()) - return label_maps, score_maps - -__all__ = ["ResNet50_vd", "MobileNetV3_small_x1_0", "HRNet_W18_C"] - - class ResNet50_vd(BaseClassifier): def __init__(self, num_classes=2, diff --git a/tutorials/train/classification/resnet50_vd_rs.py b/tutorials/train/classification/resnet50_vd_rs.py new file mode 100644 index 0000000..13b1ffd --- /dev/null +++ b/tutorials/train/classification/resnet50_vd_rs.py @@ -0,0 +1,58 @@ +import sys + +sys.path.append("E:/dataFiles/github/PaddleRS") + +import paddlers as pdrs +from paddlers import transforms as T + +# 下载aistudio的数据到当前文件夹并解压、整理 +# https://aistudio.baidu.com/aistudio/datasetdetail/63189 + +# 定义训练和验证时的transforms +# API说明:https://github.com/PaddlePaddle/paddlers/blob/develop/docs/apis/transforms/transforms.md +train_transforms = T.Compose([ + T.Resize(target_size=512), + T.RandomHorizontalFlip(), + T.Normalize( + mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]), +]) + +eval_transforms = T.Compose([ + T.Resize(target_size=512), + T.Normalize( + mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]), +]) + +# 定义训练和验证所用的数据集 +# API说明:https://github.com/PaddlePaddle/paddlers/blob/develop/docs/apis/datasets.md +train_dataset = pdrs.datasets.ClasDataset( + data_dir='E:/dataFiles/github/PaddleRS/tutorials/train/classification/DataSet', + file_list='tutorials/train/classification/DataSet/train_list.txt', + label_list='tutorials/train/classification/DataSet/label_list.txt', + transforms=train_transforms, + num_workers=0, + shuffle=True) + +eval_dataset = pdrs.datasets.ClasDataset( + data_dir='E:/dataFiles/github/PaddleRS/tutorials/train/classification/DataSet', + file_list='tutorials/train/classification/DataSet/test_list.txt', + label_list='tutorials/train/classification/DataSet/label_list.txt', + transforms=eval_transforms, + num_workers=0, + shuffle=False) + +# 初始化模型,并进行训练 +# 可使用VisualDL查看训练指标,参考https://github.com/PaddlePaddle/paddlers/blob/develop/docs/visualdl.md +num_classes = len(train_dataset.labels) +model = pdrs.tasks.ResNet50_vd(num_classes=num_classes) + +# API说明:https://github.com/PaddlePaddle/paddlers/blob/develop/docs/apis/models/semantic_segmentation.md +# 各参数介绍与调整说明:https://github.com/PaddlePaddle/paddlers/blob/develop/docs/parameters.md +model.train( + num_epochs=10, + train_dataset=train_dataset, + train_batch_size=4, + eval_dataset=eval_dataset, + learning_rate=0.01, + pretrain_weights=None, + save_dir='output/resnet_vd')