You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2354 lines
100 KiB
2354 lines
100 KiB
# 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 __future__ import absolute_import |
|
|
|
import collections |
|
import copy |
|
import os |
|
import os.path as osp |
|
|
|
import numpy as np |
|
import paddle |
|
from paddle.static import InputSpec |
|
|
|
import paddlers.models.ppdet as ppdet |
|
from paddlers.models.ppdet.modeling.proposal_generator.target_layer import BBoxAssigner, MaskAssigner |
|
import paddlers |
|
import paddlers.utils.logging as logging |
|
from paddlers.transforms import decode_image |
|
from paddlers.transforms.operators import _NormalizeBox, _PadBox, _BboxXYXY2XYWH, Resize, Pad |
|
from paddlers.transforms.batch_operators import BatchCompose, BatchRandomResize, BatchRandomResizeByShort, \ |
|
_BatchPad, _Gt2YoloTarget |
|
from .base import BaseModel |
|
from .utils.det_metrics import VOCMetric, COCOMetric |
|
from paddlers.models.ppdet.optimizer import ModelEMA |
|
from paddlers.utils.checkpoint import det_pretrain_weights_dict |
|
|
|
__all__ = [ |
|
"YOLOv3", "FasterRCNN", "PPYOLO", "PPYOLOTiny", "PPYOLOv2", "MaskRCNN" |
|
] |
|
|
|
|
|
class BaseDetector(BaseModel): |
|
def __init__(self, model_name, num_classes=80, **params): |
|
self.init_params.update(locals()) |
|
if 'with_net' in self.init_params: |
|
del self.init_params['with_net'] |
|
super(BaseDetector, self).__init__('detector') |
|
if not hasattr(ppdet.modeling, model_name): |
|
raise ValueError("ERROR: There is no model named {}.".format( |
|
model_name)) |
|
|
|
self.model_name = model_name |
|
self.num_classes = num_classes |
|
self.labels = None |
|
if params.get('with_net', True): |
|
params.pop('with_net', None) |
|
self.net = self.build_net(**params) |
|
|
|
def build_net(self, **params): |
|
with paddle.utils.unique_name.guard(): |
|
net = ppdet.modeling.__dict__[self.model_name](**params) |
|
return net |
|
|
|
def _fix_transforms_shape(self, image_shape): |
|
raise NotImplementedError("_fix_transforms_shape: not implemented!") |
|
|
|
def _define_input_spec(self, image_shape): |
|
input_spec = [{ |
|
"image": InputSpec( |
|
shape=image_shape, name='image', dtype='float32'), |
|
"im_shape": InputSpec( |
|
shape=[image_shape[0], 2], name='im_shape', dtype='float32'), |
|
"scale_factor": InputSpec( |
|
shape=[image_shape[0], 2], name='scale_factor', dtype='float32') |
|
}] |
|
return input_spec |
|
|
|
def _check_image_shape(self, image_shape): |
|
if len(image_shape) == 2: |
|
image_shape = [1, 3] + image_shape |
|
if image_shape[-2] % 32 > 0 or image_shape[-1] % 32 > 0: |
|
raise ValueError( |
|
"Height and width in fixed_input_shape must be a multiple of 32, but received {}.". |
|
format(image_shape[-2:])) |
|
return image_shape |
|
|
|
def _get_test_inputs(self, image_shape): |
|
if image_shape is not None: |
|
image_shape = self._check_image_shape(image_shape) |
|
self._fix_transforms_shape(image_shape[-2:]) |
|
else: |
|
image_shape = [None, 3, -1, -1] |
|
self.fixed_input_shape = image_shape |
|
|
|
return self._define_input_spec(image_shape) |
|
|
|
def _get_backbone(self, backbone_name, **params): |
|
backbone = getattr(ppdet.modeling, backbone_name)(**params) |
|
return backbone |
|
|
|
def run(self, net, inputs, mode): |
|
net_out = net(inputs) |
|
if mode in ['train', 'eval']: |
|
outputs = net_out |
|
else: |
|
outputs = dict() |
|
for key in net_out: |
|
outputs[key] = net_out[key].numpy() |
|
|
|
return outputs |
|
|
|
def default_optimizer(self, |
|
parameters, |
|
learning_rate, |
|
warmup_steps, |
|
warmup_start_lr, |
|
lr_decay_epochs, |
|
lr_decay_gamma, |
|
num_steps_each_epoch, |
|
reg_coeff=1e-04, |
|
scheduler='Piecewise', |
|
num_epochs=None): |
|
if scheduler.lower() == 'piecewise': |
|
if warmup_steps > 0 and warmup_steps > lr_decay_epochs[ |
|
0] * num_steps_each_epoch: |
|
logging.error( |
|
"In function train(), parameters must satisfy: " |
|
"warmup_steps <= lr_decay_epochs[0] * num_samples_in_train_dataset. " |
|
"See this doc for more information: " |
|
"https://github.com/PaddlePaddle/PaddleRS/blob/develop/docs/parameters.md", |
|
exit=False) |
|
logging.error( |
|
"Either `warmup_steps` be less than {} or lr_decay_epochs[0] be greater than {} " |
|
"must be satisfied, please modify 'warmup_steps' or 'lr_decay_epochs' in train function". |
|
format(lr_decay_epochs[0] * num_steps_each_epoch, |
|
warmup_steps // num_steps_each_epoch), |
|
exit=True) |
|
boundaries = [b * num_steps_each_epoch for b in lr_decay_epochs] |
|
values = [(lr_decay_gamma**i) * learning_rate |
|
for i in range(len(lr_decay_epochs) + 1)] |
|
scheduler = paddle.optimizer.lr.PiecewiseDecay(boundaries, values) |
|
elif scheduler.lower() == 'cosine': |
|
if num_epochs is None: |
|
logging.error( |
|
"`num_epochs` must be set while using cosine annealing decay scheduler, but received {}". |
|
format(num_epochs), |
|
exit=False) |
|
if warmup_steps > 0 and warmup_steps > num_epochs * num_steps_each_epoch: |
|
logging.error( |
|
"In function train(), parameters must satisfy: " |
|
"warmup_steps <= num_epochs * num_samples_in_train_dataset. " |
|
"See this doc for more information: " |
|
"https://github.com/PaddlePaddle/PaddleRS/blob/develop/docs/parameters.md", |
|
exit=False) |
|
logging.error( |
|
"`warmup_steps` must be less than the total number of steps({}), " |
|
"please modify 'num_epochs' or 'warmup_steps' in train function". |
|
format(num_epochs * num_steps_each_epoch), |
|
exit=True) |
|
T_max = num_epochs * num_steps_each_epoch - warmup_steps |
|
scheduler = paddle.optimizer.lr.CosineAnnealingDecay( |
|
learning_rate=learning_rate, |
|
T_max=T_max, |
|
eta_min=0.0, |
|
last_epoch=-1) |
|
else: |
|
logging.error( |
|
"Invalid learning rate scheduler: {}!".format(scheduler), |
|
exit=True) |
|
|
|
if warmup_steps > 0: |
|
scheduler = paddle.optimizer.lr.LinearWarmup( |
|
learning_rate=scheduler, |
|
warmup_steps=warmup_steps, |
|
start_lr=warmup_start_lr, |
|
end_lr=learning_rate) |
|
optimizer = paddle.optimizer.Momentum( |
|
scheduler, |
|
momentum=.9, |
|
weight_decay=paddle.regularizer.L2Decay(coeff=reg_coeff), |
|
parameters=parameters) |
|
return optimizer |
|
|
|
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. |
|
""" |
|
|
|
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 train_dataset.__class__.__name__ == 'VOCDetDataset': |
|
train_dataset.data_fields = { |
|
'im_id', 'image_shape', 'image', 'gt_bbox', 'gt_class', |
|
'difficult' |
|
} |
|
elif train_dataset.__class__.__name__ == 'CocoDetection': |
|
if self.__class__.__name__ == 'MaskRCNN': |
|
train_dataset.data_fields = { |
|
'im_id', 'image_shape', 'image', 'gt_bbox', 'gt_class', |
|
'gt_poly', 'is_crowd' |
|
} |
|
else: |
|
train_dataset.data_fields = { |
|
'im_id', 'image_shape', 'image', 'gt_bbox', 'gt_class', |
|
'is_crowd' |
|
} |
|
|
|
if metric is None: |
|
if eval_dataset.__class__.__name__ == 'VOCDetDataset': |
|
self.metric = 'voc' |
|
elif eval_dataset.__class__.__name__ == 'COCODetDataset': |
|
self.metric = 'coco' |
|
else: |
|
assert metric.lower() in ['coco', 'voc'], \ |
|
"Evaluation metric {} is not supported. Please choose from 'COCO' and 'VOC'." |
|
self.metric = metric.lower() |
|
|
|
self.labels = train_dataset.labels |
|
self.num_max_boxes = train_dataset.num_max_boxes |
|
train_dataset.batch_transforms = self._compose_batch_transform( |
|
train_dataset.transforms, mode='train') |
|
|
|
# build optimizer if not defined |
|
if optimizer is None: |
|
num_steps_each_epoch = len(train_dataset) // train_batch_size |
|
self.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) |
|
else: |
|
self.optimizer = optimizer |
|
|
|
# initiate weights |
|
if pretrain_weights is not None and not osp.exists(pretrain_weights): |
|
if pretrain_weights not in det_pretrain_weights_dict['_'.join( |
|
[self.model_name, self.backbone_name])]: |
|
logging.warning( |
|
"Path of pretrain_weights('{}') does not exist!".format( |
|
pretrain_weights)) |
|
pretrain_weights = det_pretrain_weights_dict['_'.join( |
|
[self.model_name, self.backbone_name])][0] |
|
logging.warning("Pretrain_weights is forcibly set to '{}'. " |
|
"If you don't want to use pretrain weights, " |
|
"set pretrain_weights to be None.".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') |
|
self.net_initialize( |
|
pretrain_weights=pretrain_weights, |
|
save_dir=pretrained_dir, |
|
resume_checkpoint=resume_checkpoint, |
|
is_backbone_weights=(pretrain_weights == 'IMAGENET' and |
|
'ESNet_' in self.backbone_name)) |
|
|
|
if use_ema: |
|
ema = ModelEMA(model=self.net, decay=.9998, use_thres_step=True) |
|
else: |
|
ema = None |
|
# start train loop |
|
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, |
|
ema=ema, |
|
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=64, |
|
eval_dataset=None, |
|
optimizer=None, |
|
save_interval_epochs=1, |
|
log_interval_steps=10, |
|
save_dir='output', |
|
learning_rate=.00001, |
|
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, |
|
quant_config=None): |
|
""" |
|
Quantization-aware training. |
|
|
|
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 or 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'. |
|
learning_rate (float, optional): Learning rate for training. |
|
Defaults to .00001. |
|
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 or 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. |
|
quant_config (dict or 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, |
|
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) |
|
|
|
def evaluate(self, |
|
eval_dataset, |
|
batch_size=1, |
|
metric=None, |
|
return_details=False): |
|
""" |
|
Evaluate the model. |
|
|
|
Args: |
|
eval_dataset (paddlers.datasets.COCODetDataset|paddlers.datasets.VOCDetDataset): |
|
Evaluation dataset. |
|
batch_size (int, optional): Total batch size among all cards used for |
|
evaluation. 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. |
|
return_details (bool, optional): Whether to return evaluation details. |
|
Defaults to False. |
|
|
|
Returns: |
|
collections.OrderedDict with key-value pairs: |
|
{"mAP(0.50, 11point)":`mean average precision`}. |
|
""" |
|
|
|
if metric is None: |
|
if not hasattr(self, 'metric'): |
|
if eval_dataset.__class__.__name__ == 'VOCDetDataset': |
|
self.metric = 'voc' |
|
elif eval_dataset.__class__.__name__ == 'COCODetDataset': |
|
self.metric = 'coco' |
|
else: |
|
assert metric.lower() in ['coco', 'voc'], \ |
|
"Evaluation metric {} is not supported. Please choose from 'COCO' and 'VOC'." |
|
self.metric = metric.lower() |
|
|
|
if self.metric == 'voc': |
|
eval_dataset.data_fields = { |
|
'im_id', 'image_shape', 'image', 'gt_bbox', 'gt_class', |
|
'difficult' |
|
} |
|
elif self.metric == 'coco': |
|
if self.__class__.__name__ == 'MaskRCNN': |
|
eval_dataset.data_fields = { |
|
'im_id', 'image_shape', 'image', 'gt_bbox', 'gt_class', |
|
'gt_poly', 'is_crowd' |
|
} |
|
else: |
|
eval_dataset.data_fields = { |
|
'im_id', 'image_shape', 'image', 'gt_bbox', 'gt_class', |
|
'is_crowd' |
|
} |
|
eval_dataset.batch_transforms = self._compose_batch_transform( |
|
eval_dataset.transforms, mode='eval') |
|
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() |
|
|
|
if batch_size > 1: |
|
logging.warning( |
|
"Detector 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') |
|
is_bbox_normalized = False |
|
if eval_dataset.batch_transforms is not None: |
|
is_bbox_normalized = any( |
|
isinstance(t, _NormalizeBox) |
|
for t in eval_dataset.batch_transforms.batch_transforms) |
|
if self.metric == 'voc': |
|
eval_metric = VOCMetric( |
|
labels=eval_dataset.labels, |
|
coco_gt=copy.deepcopy(eval_dataset.coco_gt), |
|
is_bbox_normalized=is_bbox_normalized, |
|
classwise=False) |
|
else: |
|
eval_metric = COCOMetric( |
|
coco_gt=copy.deepcopy(eval_dataset.coco_gt), |
|
classwise=False) |
|
scores = collections.OrderedDict() |
|
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): |
|
outputs = self.run(self.net, data, 'eval') |
|
eval_metric.update(data, outputs) |
|
eval_metric.accumulate() |
|
self.eval_details = eval_metric.details |
|
scores.update(eval_metric.get()) |
|
eval_metric.reset() |
|
|
|
if return_details: |
|
return scores, self.eval_details |
|
return scores |
|
|
|
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 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`}. |
|
If `img_file` is a list, the result is a list composed of dicts with the |
|
corresponding fields: |
|
category_id(int): the 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 |
|
""" |
|
|
|
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_samples = self._preprocess(images, transforms) |
|
self.net.eval() |
|
outputs = self.run(self.net, batch_samples, 'test') |
|
prediction = self._postprocess(outputs) |
|
|
|
if isinstance(img_file, (str, np.ndarray)): |
|
prediction = prediction[0] |
|
return prediction |
|
|
|
def _preprocess(self, images, transforms, to_tensor=True): |
|
self._check_transforms(transforms, 'test') |
|
batch_samples = list() |
|
for im in images: |
|
if isinstance(im, str): |
|
im = decode_image(im, to_rgb=False) |
|
sample = {'image': im} |
|
sample = transforms(sample) |
|
batch_samples.append(sample) |
|
batch_transforms = self._compose_batch_transform(transforms, 'test') |
|
batch_samples = batch_transforms(batch_samples) |
|
if to_tensor: |
|
for k in batch_samples: |
|
batch_samples[k] = paddle.to_tensor(batch_samples[k]) |
|
|
|
return batch_samples |
|
|
|
def _postprocess(self, batch_pred): |
|
infer_result = {} |
|
if 'bbox' in batch_pred: |
|
bboxes = batch_pred['bbox'] |
|
bbox_nums = batch_pred['bbox_num'] |
|
det_res = [] |
|
k = 0 |
|
for i in range(len(bbox_nums)): |
|
det_nums = bbox_nums[i] |
|
for j in range(det_nums): |
|
dt = bboxes[k] |
|
k = k + 1 |
|
num_id, score, xmin, ymin, xmax, ymax = dt.tolist() |
|
if int(num_id) < 0: |
|
continue |
|
category = self.labels[int(num_id)] |
|
w = xmax - xmin |
|
h = ymax - ymin |
|
bbox = [xmin, ymin, w, h] |
|
dt_res = { |
|
'category_id': int(num_id), |
|
'category': category, |
|
'bbox': bbox, |
|
'score': score |
|
} |
|
det_res.append(dt_res) |
|
infer_result['bbox'] = det_res |
|
|
|
if 'mask' in batch_pred: |
|
masks = batch_pred['mask'] |
|
bboxes = batch_pred['bbox'] |
|
mask_nums = batch_pred['bbox_num'] |
|
seg_res = [] |
|
k = 0 |
|
for i in range(len(mask_nums)): |
|
det_nums = mask_nums[i] |
|
for j in range(det_nums): |
|
mask = masks[k].astype(np.uint8) |
|
score = float(bboxes[k][1]) |
|
label = int(bboxes[k][0]) |
|
k = k + 1 |
|
if label == -1: |
|
continue |
|
category = self.labels[int(label)] |
|
sg_res = { |
|
'category_id': int(label), |
|
'category': category, |
|
'mask': mask.astype('uint8'), |
|
'score': score |
|
} |
|
seg_res.append(sg_res) |
|
infer_result['mask'] = seg_res |
|
|
|
bbox_num = batch_pred['bbox_num'] |
|
results = [] |
|
start = 0 |
|
for num in bbox_num: |
|
end = start + num |
|
curr_res = infer_result['bbox'][start:end] |
|
if 'mask' in infer_result: |
|
mask_res = infer_result['mask'][start:end] |
|
for box, mask in zip(curr_res, mask_res): |
|
box.update(mask) |
|
results.append(curr_res) |
|
start = end |
|
|
|
return results |
|
|
|
def _check_transforms(self, transforms, mode): |
|
super()._check_transforms(transforms, mode) |
|
if not isinstance(transforms.arrange, |
|
paddlers.transforms.ArrangeDetector): |
|
raise TypeError( |
|
"`transforms.arrange` must be an ArrangeDetector object.") |
|
|
|
|
|
class PicoDet(BaseDetector): |
|
def __init__(self, |
|
num_classes=80, |
|
backbone='ESNet_m', |
|
nms_score_threshold=.025, |
|
nms_topk=1000, |
|
nms_keep_topk=100, |
|
nms_iou_threshold=.6, |
|
**params): |
|
self.init_params = locals() |
|
if backbone not in { |
|
'ESNet_s', 'ESNet_m', 'ESNet_l', 'LCNet', 'MobileNetV3', |
|
'ResNet18_vd' |
|
}: |
|
raise ValueError( |
|
"backbone: {} is not supported. Please choose one of " |
|
"{'ESNet_s', 'ESNet_m', 'ESNet_l', 'LCNet', 'MobileNetV3', 'ResNet18_vd'}.". |
|
format(backbone)) |
|
self.backbone_name = backbone |
|
if params.get('with_net', True): |
|
if backbone == 'ESNet_s': |
|
backbone = self._get_backbone( |
|
'ESNet', |
|
scale=.75, |
|
feature_maps=[4, 11, 14], |
|
act="hard_swish", |
|
channel_ratio=[ |
|
0.875, 0.5, 0.5, 0.5, 0.625, 0.5, 0.625, 0.5, 0.5, 0.5, |
|
0.5, 0.5, 0.5 |
|
]) |
|
neck_out_channels = 96 |
|
head_num_convs = 2 |
|
elif backbone == 'ESNet_m': |
|
backbone = self._get_backbone( |
|
'ESNet', |
|
scale=1.0, |
|
feature_maps=[4, 11, 14], |
|
act="hard_swish", |
|
channel_ratio=[ |
|
0.875, 0.5, 1.0, 0.625, 0.5, 0.75, 0.625, 0.625, 0.5, |
|
0.625, 1.0, 0.625, 0.75 |
|
]) |
|
neck_out_channels = 128 |
|
head_num_convs = 4 |
|
elif backbone == 'ESNet_l': |
|
backbone = self._get_backbone( |
|
'ESNet', |
|
scale=1.25, |
|
feature_maps=[4, 11, 14], |
|
act="hard_swish", |
|
channel_ratio=[ |
|
0.875, 0.5, 1.0, 0.625, 0.5, 0.75, 0.625, 0.625, 0.5, |
|
0.625, 1.0, 0.625, 0.75 |
|
]) |
|
neck_out_channels = 160 |
|
head_num_convs = 4 |
|
elif backbone == 'LCNet': |
|
backbone = self._get_backbone( |
|
'LCNet', scale=1.5, feature_maps=[3, 4, 5]) |
|
neck_out_channels = 128 |
|
head_num_convs = 4 |
|
elif backbone == 'MobileNetV3': |
|
backbone = self._get_backbone( |
|
'MobileNetV3', |
|
scale=1.0, |
|
with_extra_blocks=False, |
|
extra_block_filters=[], |
|
feature_maps=[7, 13, 16]) |
|
neck_out_channels = 128 |
|
head_num_convs = 4 |
|
else: |
|
backbone = self._get_backbone( |
|
'ResNet', |
|
depth=18, |
|
variant='d', |
|
return_idx=[1, 2, 3], |
|
freeze_at=-1, |
|
freeze_norm=False, |
|
norm_decay=0.) |
|
neck_out_channels = 128 |
|
head_num_convs = 4 |
|
|
|
neck = ppdet.modeling.CSPPAN( |
|
in_channels=[i.channels for i in backbone.out_shape], |
|
out_channels=neck_out_channels, |
|
num_features=4, |
|
num_csp_blocks=1, |
|
use_depthwise=True) |
|
|
|
head_conv_feat = ppdet.modeling.PicoFeat( |
|
feat_in=neck_out_channels, |
|
feat_out=neck_out_channels, |
|
num_fpn_stride=4, |
|
num_convs=head_num_convs, |
|
norm_type='bn', |
|
share_cls_reg=True, ) |
|
loss_class = ppdet.modeling.VarifocalLoss( |
|
use_sigmoid=True, iou_weighted=True, loss_weight=1.0) |
|
loss_dfl = ppdet.modeling.DistributionFocalLoss(loss_weight=.25) |
|
loss_bbox = ppdet.modeling.GIoULoss(loss_weight=2.0) |
|
assigner = ppdet.modeling.SimOTAAssigner( |
|
candidate_topk=10, iou_weight=6, num_classes=num_classes) |
|
nms = ppdet.modeling.MultiClassNMS( |
|
nms_top_k=nms_topk, |
|
keep_top_k=nms_keep_topk, |
|
score_threshold=nms_score_threshold, |
|
nms_threshold=nms_iou_threshold) |
|
head = ppdet.modeling.PicoHead( |
|
conv_feat=head_conv_feat, |
|
num_classes=num_classes, |
|
fpn_stride=[8, 16, 32, 64], |
|
prior_prob=0.01, |
|
reg_max=7, |
|
cell_offset=.5, |
|
loss_class=loss_class, |
|
loss_dfl=loss_dfl, |
|
loss_bbox=loss_bbox, |
|
assigner=assigner, |
|
feat_in_chan=neck_out_channels, |
|
nms=nms) |
|
params.update({ |
|
'backbone': backbone, |
|
'neck': neck, |
|
'head': head, |
|
}) |
|
super(PicoDet, self).__init__( |
|
model_name='PicoDet', num_classes=num_classes, **params) |
|
|
|
def _compose_batch_transform(self, transforms, mode='train'): |
|
default_batch_transforms = [_BatchPad(pad_to_stride=32)] |
|
if mode == 'eval': |
|
collate_batch = True |
|
else: |
|
collate_batch = False |
|
|
|
custom_batch_transforms = [] |
|
for i, op in enumerate(transforms.transforms): |
|
if isinstance(op, (BatchRandomResize, BatchRandomResizeByShort)): |
|
if mode != 'train': |
|
raise ValueError( |
|
"{} cannot be present in the {} transforms. ".format( |
|
op.__class__.__name__, mode) + |
|
"Please check the {} transforms.".format(mode)) |
|
custom_batch_transforms.insert(0, copy.deepcopy(op)) |
|
|
|
batch_transforms = BatchCompose( |
|
custom_batch_transforms + default_batch_transforms, |
|
collate_batch=collate_batch) |
|
|
|
return batch_transforms |
|
|
|
def _fix_transforms_shape(self, image_shape): |
|
if getattr(self, 'test_transforms', 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 == 'Resize': |
|
has_resize_op = True |
|
resize_op_idx = idx |
|
if name == 'Normalize': |
|
normalize_op_idx = idx |
|
|
|
if not has_resize_op: |
|
self.test_transforms.transforms.insert( |
|
normalize_op_idx, |
|
Resize( |
|
target_size=image_shape, interp='CUBIC')) |
|
else: |
|
self.test_transforms.transforms[ |
|
resize_op_idx].target_size = image_shape |
|
|
|
def _get_test_inputs(self, image_shape): |
|
if image_shape is not None: |
|
image_shape = self._check_image_shape(image_shape) |
|
self._fix_transforms_shape(image_shape[-2:]) |
|
else: |
|
image_shape = [None, 3, 320, 320] |
|
if getattr(self, 'test_transforms', None): |
|
for idx, op in enumerate(self.test_transforms.transforms): |
|
name = op.__class__.__name__ |
|
if name == 'Resize': |
|
image_shape = [None, 3] + list( |
|
self.test_transforms.transforms[idx].target_size) |
|
logging.warning( |
|
'[Important!!!] When exporting inference model for {}, ' |
|
'if fixed_input_shape is not set, it will be forcibly set to {}. ' |
|
'Please ensure image shape after transforms is {}, if not, ' |
|
'fixed_input_shape should be specified manually.' |
|
.format(self.__class__.__name__, image_shape, image_shape[1:])) |
|
|
|
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. |
|
""" |
|
|
|
if optimizer is None: |
|
num_steps_each_epoch = len(train_dataset) // 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, |
|
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) |
|
|
|
|
|
class YOLOv3(BaseDetector): |
|
def __init__(self, |
|
num_classes=80, |
|
backbone='MobileNetV1', |
|
anchors=[[10, 13], [16, 30], [33, 23], [30, 61], [62, 45], |
|
[59, 119], [116, 90], [156, 198], [373, 326]], |
|
anchor_masks=[[6, 7, 8], [3, 4, 5], [0, 1, 2]], |
|
ignore_threshold=0.7, |
|
nms_score_threshold=0.01, |
|
nms_topk=1000, |
|
nms_keep_topk=100, |
|
nms_iou_threshold=0.45, |
|
label_smooth=False, |
|
**params): |
|
self.init_params = locals() |
|
if backbone not in { |
|
'MobileNetV1', 'MobileNetV1_ssld', 'MobileNetV3', |
|
'MobileNetV3_ssld', 'DarkNet53', 'ResNet50_vd_dcn', 'ResNet34' |
|
}: |
|
raise ValueError( |
|
"backbone: {} is not supported. Please choose one of " |
|
"{'MobileNetV1', 'MobileNetV1_ssld', 'MobileNetV3', 'MobileNetV3_ssld', 'DarkNet53', " |
|
"'ResNet50_vd_dcn', 'ResNet34'}.".format(backbone)) |
|
|
|
self.backbone_name = backbone |
|
if params.get('with_net', True): |
|
if paddlers.env_info['place'] == 'gpu' and paddlers.env_info[ |
|
'num'] > 1 and not os.environ.get('PADDLERS_EXPORT_STAGE'): |
|
norm_type = 'sync_bn' |
|
else: |
|
norm_type = 'bn' |
|
|
|
if 'MobileNetV1' in backbone: |
|
norm_type = 'bn' |
|
backbone = self._get_backbone('MobileNet', norm_type=norm_type) |
|
elif 'MobileNetV3' in backbone: |
|
backbone = self._get_backbone( |
|
'MobileNetV3', |
|
norm_type=norm_type, |
|
feature_maps=[7, 13, 16]) |
|
elif backbone == 'ResNet50_vd_dcn': |
|
backbone = self._get_backbone( |
|
'ResNet', |
|
norm_type=norm_type, |
|
variant='d', |
|
return_idx=[1, 2, 3], |
|
dcn_v2_stages=[3], |
|
freeze_at=-1, |
|
freeze_norm=False) |
|
elif backbone == 'ResNet34': |
|
backbone = self._get_backbone( |
|
'ResNet', |
|
depth=34, |
|
norm_type=norm_type, |
|
return_idx=[1, 2, 3], |
|
freeze_at=-1, |
|
freeze_norm=False, |
|
norm_decay=0.) |
|
else: |
|
backbone = self._get_backbone('DarkNet', norm_type=norm_type) |
|
|
|
neck = ppdet.modeling.YOLOv3FPN( |
|
norm_type=norm_type, |
|
in_channels=[i.channels for i in backbone.out_shape]) |
|
loss = ppdet.modeling.YOLOv3Loss( |
|
num_classes=num_classes, |
|
ignore_thresh=ignore_threshold, |
|
label_smooth=label_smooth) |
|
yolo_head = ppdet.modeling.YOLOv3Head( |
|
in_channels=[i.channels for i in neck.out_shape], |
|
anchors=anchors, |
|
anchor_masks=anchor_masks, |
|
num_classes=num_classes, |
|
loss=loss) |
|
post_process = ppdet.modeling.BBoxPostProcess( |
|
decode=ppdet.modeling.YOLOBox(num_classes=num_classes), |
|
nms=ppdet.modeling.MultiClassNMS( |
|
score_threshold=nms_score_threshold, |
|
nms_top_k=nms_topk, |
|
keep_top_k=nms_keep_topk, |
|
nms_threshold=nms_iou_threshold)) |
|
params.update({ |
|
'backbone': backbone, |
|
'neck': neck, |
|
'yolo_head': yolo_head, |
|
'post_process': post_process |
|
}) |
|
super(YOLOv3, self).__init__( |
|
model_name='YOLOv3', num_classes=num_classes, **params) |
|
self.anchors = anchors |
|
self.anchor_masks = anchor_masks |
|
|
|
def _compose_batch_transform(self, transforms, mode='train'): |
|
if mode == 'train': |
|
default_batch_transforms = [ |
|
_BatchPad(pad_to_stride=-1), _NormalizeBox(), |
|
_PadBox(getattr(self, 'num_max_boxes', 50)), _BboxXYXY2XYWH(), |
|
_Gt2YoloTarget( |
|
anchor_masks=self.anchor_masks, |
|
anchors=self.anchors, |
|
downsample_ratios=getattr(self, 'downsample_ratios', |
|
[32, 16, 8]), |
|
num_classes=self.num_classes) |
|
] |
|
else: |
|
default_batch_transforms = [_BatchPad(pad_to_stride=-1)] |
|
if mode == 'eval' and self.metric == 'voc': |
|
collate_batch = False |
|
else: |
|
collate_batch = True |
|
|
|
custom_batch_transforms = [] |
|
for i, op in enumerate(transforms.transforms): |
|
if isinstance(op, (BatchRandomResize, BatchRandomResizeByShort)): |
|
if mode != 'train': |
|
raise ValueError( |
|
"{} cannot be present in the {} transforms. ".format( |
|
op.__class__.__name__, mode) + |
|
"Please check the {} transforms.".format(mode)) |
|
custom_batch_transforms.insert(0, copy.deepcopy(op)) |
|
|
|
batch_transforms = BatchCompose( |
|
custom_batch_transforms + default_batch_transforms, |
|
collate_batch=collate_batch) |
|
|
|
return batch_transforms |
|
|
|
def _fix_transforms_shape(self, image_shape): |
|
if getattr(self, 'test_transforms', 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 == 'Resize': |
|
has_resize_op = True |
|
resize_op_idx = idx |
|
if name == 'Normalize': |
|
normalize_op_idx = idx |
|
|
|
if not has_resize_op: |
|
self.test_transforms.transforms.insert( |
|
normalize_op_idx, |
|
Resize( |
|
target_size=image_shape, interp='CUBIC')) |
|
else: |
|
self.test_transforms.transforms[ |
|
resize_op_idx].target_size = image_shape |
|
|
|
|
|
class FasterRCNN(BaseDetector): |
|
def __init__(self, |
|
num_classes=80, |
|
backbone='ResNet50', |
|
with_fpn=True, |
|
with_dcn=False, |
|
aspect_ratios=[0.5, 1.0, 2.0], |
|
anchor_sizes=[[32], [64], [128], [256], [512]], |
|
keep_top_k=100, |
|
nms_threshold=0.5, |
|
score_threshold=0.05, |
|
fpn_num_channels=256, |
|
rpn_batch_size_per_im=256, |
|
rpn_fg_fraction=0.5, |
|
test_pre_nms_top_n=None, |
|
test_post_nms_top_n=1000, |
|
**params): |
|
self.init_params = locals() |
|
if backbone not in { |
|
'ResNet50', 'ResNet50_vd', 'ResNet50_vd_ssld', 'ResNet34', |
|
'ResNet34_vd', 'ResNet101', 'ResNet101_vd', 'HRNet_W18' |
|
}: |
|
raise ValueError( |
|
"backbone: {} is not supported. Please choose one of " |
|
"{'ResNet50', 'ResNet50_vd', 'ResNet50_vd_ssld', 'ResNet34', 'ResNet34_vd', " |
|
"'ResNet101', 'ResNet101_vd', 'HRNet_W18'}.".format(backbone)) |
|
self.backbone_name = backbone |
|
|
|
if params.get('with_net', True): |
|
dcn_v2_stages = [1, 2, 3] if with_dcn else [-1] |
|
if backbone == 'HRNet_W18': |
|
if not with_fpn: |
|
logging.warning( |
|
"Backbone {} should be used along with fpn enabled, 'with_fpn' is forcibly set to True". |
|
format(backbone)) |
|
with_fpn = True |
|
if with_dcn: |
|
logging.warning( |
|
"Backbone {} should be used along with dcn disabled, 'with_dcn' is forcibly set to False". |
|
format(backbone)) |
|
backbone = self._get_backbone( |
|
'HRNet', width=18, freeze_at=0, return_idx=[0, 1, 2, 3]) |
|
elif backbone == 'ResNet50_vd_ssld': |
|
if not with_fpn: |
|
logging.warning( |
|
"Backbone {} should be used along with fpn enabled, 'with_fpn' is forcibly set to True". |
|
format(backbone)) |
|
with_fpn = True |
|
backbone = self._get_backbone( |
|
'ResNet', |
|
variant='d', |
|
norm_type='bn', |
|
freeze_at=0, |
|
return_idx=[0, 1, 2, 3], |
|
num_stages=4, |
|
lr_mult_list=[0.05, 0.05, 0.1, 0.15], |
|
dcn_v2_stages=dcn_v2_stages) |
|
elif 'ResNet50' in backbone: |
|
if with_fpn: |
|
backbone = self._get_backbone( |
|
'ResNet', |
|
variant='d' if '_vd' in backbone else 'b', |
|
norm_type='bn', |
|
freeze_at=0, |
|
return_idx=[0, 1, 2, 3], |
|
num_stages=4, |
|
dcn_v2_stages=dcn_v2_stages) |
|
else: |
|
if with_dcn: |
|
logging.warning( |
|
"Backbone {} without fpn should be used along with dcn disabled, 'with_dcn' is forcibly set to False". |
|
format(backbone)) |
|
backbone = self._get_backbone( |
|
'ResNet', |
|
variant='d' if '_vd' in backbone else 'b', |
|
norm_type='bn', |
|
freeze_at=0, |
|
return_idx=[2], |
|
num_stages=3) |
|
elif 'ResNet34' in backbone: |
|
if not with_fpn: |
|
logging.warning( |
|
"Backbone {} should be used along with fpn enabled, 'with_fpn' is forcibly set to True". |
|
format(backbone)) |
|
with_fpn = True |
|
backbone = self._get_backbone( |
|
'ResNet', |
|
depth=34, |
|
variant='d' if 'vd' in backbone else 'b', |
|
norm_type='bn', |
|
freeze_at=0, |
|
return_idx=[0, 1, 2, 3], |
|
num_stages=4, |
|
dcn_v2_stages=dcn_v2_stages) |
|
else: |
|
if not with_fpn: |
|
logging.warning( |
|
"Backbone {} should be used along with fpn enabled, 'with_fpn' is forcibly set to True". |
|
format(backbone)) |
|
with_fpn = True |
|
backbone = self._get_backbone( |
|
'ResNet', |
|
depth=101, |
|
variant='d' if 'vd' in backbone else 'b', |
|
norm_type='bn', |
|
freeze_at=0, |
|
return_idx=[0, 1, 2, 3], |
|
num_stages=4, |
|
dcn_v2_stages=dcn_v2_stages) |
|
|
|
rpn_in_channel = backbone.out_shape[0].channels |
|
|
|
if with_fpn: |
|
self.backbone_name = self.backbone_name + '_fpn' |
|
|
|
if 'HRNet' in self.backbone_name: |
|
neck = ppdet.modeling.HRFPN( |
|
in_channels=[i.channels for i in backbone.out_shape], |
|
out_channel=fpn_num_channels, |
|
spatial_scales=[ |
|
1.0 / i.stride for i in backbone.out_shape |
|
], |
|
share_conv=False) |
|
else: |
|
neck = ppdet.modeling.FPN( |
|
in_channels=[i.channels for i in backbone.out_shape], |
|
out_channel=fpn_num_channels, |
|
spatial_scales=[ |
|
1.0 / i.stride for i in backbone.out_shape |
|
]) |
|
rpn_in_channel = neck.out_shape[0].channels |
|
anchor_generator_cfg = { |
|
'aspect_ratios': aspect_ratios, |
|
'anchor_sizes': anchor_sizes, |
|
'strides': [4, 8, 16, 32, 64] |
|
} |
|
train_proposal_cfg = { |
|
'min_size': 0.0, |
|
'nms_thresh': .7, |
|
'pre_nms_top_n': 2000, |
|
'post_nms_top_n': 1000, |
|
'topk_after_collect': True |
|
} |
|
test_proposal_cfg = { |
|
'min_size': 0.0, |
|
'nms_thresh': .7, |
|
'pre_nms_top_n': 1000 |
|
if test_pre_nms_top_n is None else test_pre_nms_top_n, |
|
'post_nms_top_n': test_post_nms_top_n |
|
} |
|
head = ppdet.modeling.TwoFCHead( |
|
in_channel=neck.out_shape[0].channels, out_channel=1024) |
|
roi_extractor_cfg = { |
|
'resolution': 7, |
|
'spatial_scale': [1. / i.stride for i in neck.out_shape], |
|
'sampling_ratio': 0, |
|
'aligned': True |
|
} |
|
with_pool = False |
|
|
|
else: |
|
neck = None |
|
anchor_generator_cfg = { |
|
'aspect_ratios': aspect_ratios, |
|
'anchor_sizes': anchor_sizes, |
|
'strides': [16] |
|
} |
|
train_proposal_cfg = { |
|
'min_size': 0.0, |
|
'nms_thresh': .7, |
|
'pre_nms_top_n': 12000, |
|
'post_nms_top_n': 2000, |
|
'topk_after_collect': False |
|
} |
|
test_proposal_cfg = { |
|
'min_size': 0.0, |
|
'nms_thresh': .7, |
|
'pre_nms_top_n': 6000 |
|
if test_pre_nms_top_n is None else test_pre_nms_top_n, |
|
'post_nms_top_n': test_post_nms_top_n |
|
} |
|
head = ppdet.modeling.Res5Head() |
|
roi_extractor_cfg = { |
|
'resolution': 14, |
|
'spatial_scale': |
|
[1. / i.stride for i in backbone.out_shape], |
|
'sampling_ratio': 0, |
|
'aligned': True |
|
} |
|
with_pool = True |
|
|
|
rpn_target_assign_cfg = { |
|
'batch_size_per_im': rpn_batch_size_per_im, |
|
'fg_fraction': rpn_fg_fraction, |
|
'negative_overlap': .3, |
|
'positive_overlap': .7, |
|
'use_random': True |
|
} |
|
|
|
rpn_head = ppdet.modeling.RPNHead( |
|
anchor_generator=anchor_generator_cfg, |
|
rpn_target_assign=rpn_target_assign_cfg, |
|
train_proposal=train_proposal_cfg, |
|
test_proposal=test_proposal_cfg, |
|
in_channel=rpn_in_channel) |
|
|
|
bbox_assigner = BBoxAssigner(num_classes=num_classes) |
|
|
|
bbox_head = ppdet.modeling.BBoxHead( |
|
head=head, |
|
in_channel=head.out_shape[0].channels, |
|
roi_extractor=roi_extractor_cfg, |
|
with_pool=with_pool, |
|
bbox_assigner=bbox_assigner, |
|
num_classes=num_classes) |
|
|
|
bbox_post_process = ppdet.modeling.BBoxPostProcess( |
|
num_classes=num_classes, |
|
decode=ppdet.modeling.RCNNBox(num_classes=num_classes), |
|
nms=ppdet.modeling.MultiClassNMS( |
|
score_threshold=score_threshold, |
|
keep_top_k=keep_top_k, |
|
nms_threshold=nms_threshold)) |
|
|
|
params.update({ |
|
'backbone': backbone, |
|
'neck': neck, |
|
'rpn_head': rpn_head, |
|
'bbox_head': bbox_head, |
|
'bbox_post_process': bbox_post_process |
|
}) |
|
else: |
|
if backbone not in {'ResNet50', 'ResNet50_vd'}: |
|
with_fpn = True |
|
|
|
self.with_fpn = with_fpn |
|
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. |
|
""" |
|
|
|
if train_dataset.pos_num < len(train_dataset.file_list): |
|
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) |
|
|
|
def _compose_batch_transform(self, transforms, mode='train'): |
|
if mode == 'train': |
|
default_batch_transforms = [ |
|
_BatchPad(pad_to_stride=32 if self.with_fpn else -1) |
|
] |
|
else: |
|
default_batch_transforms = [ |
|
_BatchPad(pad_to_stride=32 if self.with_fpn else -1) |
|
] |
|
custom_batch_transforms = [] |
|
for i, op in enumerate(transforms.transforms): |
|
if isinstance(op, (BatchRandomResize, BatchRandomResizeByShort)): |
|
if mode != 'train': |
|
raise ValueError( |
|
"{} cannot be present in the {} transforms. ".format( |
|
op.__class__.__name__, mode) + |
|
"Please check the {} transforms.".format(mode)) |
|
custom_batch_transforms.insert(0, copy.deepcopy(op)) |
|
|
|
batch_transforms = BatchCompose( |
|
custom_batch_transforms + default_batch_transforms, |
|
collate_batch=False) |
|
|
|
return batch_transforms |
|
|
|
def _fix_transforms_shape(self, image_shape): |
|
if getattr(self, 'test_transforms', 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 == 'ResizeByShort': |
|
has_resize_op = True |
|
resize_op_idx = idx |
|
if name == 'Normalize': |
|
normalize_op_idx = idx |
|
|
|
if not has_resize_op: |
|
self.test_transforms.transforms.insert( |
|
normalize_op_idx, |
|
Resize( |
|
target_size=image_shape, |
|
keep_ratio=True, |
|
interp='CUBIC')) |
|
else: |
|
self.test_transforms.transforms[resize_op_idx] = Resize( |
|
target_size=image_shape, keep_ratio=True, interp='CUBIC') |
|
self.test_transforms.transforms.append( |
|
Pad(im_padding_value=[0., 0., 0.])) |
|
|
|
def _get_test_inputs(self, image_shape): |
|
if image_shape is not None: |
|
image_shape = self._check_image_shape(image_shape) |
|
self._fix_transforms_shape(image_shape[-2:]) |
|
else: |
|
image_shape = [None, 3, -1, -1] |
|
if self.with_fpn: |
|
self.test_transforms.transforms.append( |
|
Pad(im_padding_value=[0., 0., 0.])) |
|
|
|
self.fixed_input_shape = image_shape |
|
return self._define_input_spec(image_shape) |
|
|
|
|
|
class PPYOLO(YOLOv3): |
|
def __init__(self, |
|
num_classes=80, |
|
backbone='ResNet50_vd_dcn', |
|
anchors=None, |
|
anchor_masks=None, |
|
use_coord_conv=True, |
|
use_iou_aware=True, |
|
use_spp=True, |
|
use_drop_block=True, |
|
scale_x_y=1.05, |
|
ignore_threshold=0.7, |
|
label_smooth=False, |
|
use_iou_loss=True, |
|
use_matrix_nms=True, |
|
nms_score_threshold=0.01, |
|
nms_topk=-1, |
|
nms_keep_topk=100, |
|
nms_iou_threshold=0.45, |
|
**params): |
|
self.init_params = locals() |
|
if backbone not in { |
|
'ResNet50_vd_dcn', 'ResNet18_vd', 'MobileNetV3_large', |
|
'MobileNetV3_small' |
|
}: |
|
raise ValueError( |
|
"backbone: {} is not supported. Please choose one of " |
|
"{'ResNet50_vd_dcn', 'ResNet18_vd', 'MobileNetV3_large', 'MobileNetV3_small'}.". |
|
format(backbone)) |
|
self.backbone_name = backbone |
|
self.downsample_ratios = [ |
|
32, 16, 8 |
|
] if backbone == 'ResNet50_vd_dcn' else [32, 16] |
|
|
|
if params.get('with_net', True): |
|
if paddlers.env_info['place'] == 'gpu' and paddlers.env_info[ |
|
'num'] > 1 and not os.environ.get('PADDLERS_EXPORT_STAGE'): |
|
norm_type = 'sync_bn' |
|
else: |
|
norm_type = 'bn' |
|
if anchors is None and anchor_masks is None: |
|
if 'MobileNetV3' in backbone: |
|
anchors = [[11, 18], [34, 47], [51, 126], [115, 71], |
|
[120, 195], [254, 235]] |
|
anchor_masks = [[3, 4, 5], [0, 1, 2]] |
|
elif backbone == 'ResNet50_vd_dcn': |
|
anchors = [[10, 13], [16, 30], [33, 23], [30, 61], |
|
[62, 45], [59, 119], [116, 90], [156, 198], |
|
[373, 326]] |
|
anchor_masks = [[6, 7, 8], [3, 4, 5], [0, 1, 2]] |
|
else: |
|
anchors = [[10, 14], [23, 27], [37, 58], [81, 82], |
|
[135, 169], [344, 319]] |
|
anchor_masks = [[3, 4, 5], [0, 1, 2]] |
|
elif anchors is None or anchor_masks is None: |
|
raise ValueError("Please define both anchors and anchor_masks.") |
|
|
|
if backbone == 'ResNet50_vd_dcn': |
|
backbone = self._get_backbone( |
|
'ResNet', |
|
variant='d', |
|
norm_type=norm_type, |
|
return_idx=[1, 2, 3], |
|
dcn_v2_stages=[3], |
|
freeze_at=-1, |
|
freeze_norm=False, |
|
norm_decay=0.) |
|
|
|
elif backbone == 'ResNet18_vd': |
|
backbone = self._get_backbone( |
|
'ResNet', |
|
depth=18, |
|
variant='d', |
|
norm_type=norm_type, |
|
return_idx=[2, 3], |
|
freeze_at=-1, |
|
freeze_norm=False, |
|
norm_decay=0.) |
|
|
|
elif backbone == 'MobileNetV3_large': |
|
backbone = self._get_backbone( |
|
'MobileNetV3', |
|
model_name='large', |
|
norm_type=norm_type, |
|
scale=1, |
|
with_extra_blocks=False, |
|
extra_block_filters=[], |
|
feature_maps=[13, 16]) |
|
|
|
elif backbone == 'MobileNetV3_small': |
|
backbone = self._get_backbone( |
|
'MobileNetV3', |
|
model_name='small', |
|
norm_type=norm_type, |
|
scale=1, |
|
with_extra_blocks=False, |
|
extra_block_filters=[], |
|
feature_maps=[9, 12]) |
|
|
|
neck = ppdet.modeling.PPYOLOFPN( |
|
norm_type=norm_type, |
|
in_channels=[i.channels for i in backbone.out_shape], |
|
coord_conv=use_coord_conv, |
|
drop_block=use_drop_block, |
|
spp=use_spp, |
|
conv_block_num=0 |
|
if ('MobileNetV3' in self.backbone_name or |
|
self.backbone_name == 'ResNet18_vd') else 2) |
|
|
|
loss = ppdet.modeling.YOLOv3Loss( |
|
num_classes=num_classes, |
|
ignore_thresh=ignore_threshold, |
|
downsample=self.downsample_ratios, |
|
label_smooth=label_smooth, |
|
scale_x_y=scale_x_y, |
|
iou_loss=ppdet.modeling.IouLoss( |
|
loss_weight=2.5, loss_square=True) |
|
if use_iou_loss else None, |
|
iou_aware_loss=ppdet.modeling.IouAwareLoss(loss_weight=1.0) |
|
if use_iou_aware else None) |
|
|
|
yolo_head = ppdet.modeling.YOLOv3Head( |
|
in_channels=[i.channels for i in neck.out_shape], |
|
anchors=anchors, |
|
anchor_masks=anchor_masks, |
|
num_classes=num_classes, |
|
loss=loss, |
|
iou_aware=use_iou_aware) |
|
|
|
if use_matrix_nms: |
|
nms = ppdet.modeling.MatrixNMS( |
|
keep_top_k=nms_keep_topk, |
|
score_threshold=nms_score_threshold, |
|
post_threshold=.05 |
|
if 'MobileNetV3' in self.backbone_name else .01, |
|
nms_top_k=nms_topk, |
|
background_label=-1) |
|
else: |
|
nms = ppdet.modeling.MultiClassNMS( |
|
score_threshold=nms_score_threshold, |
|
nms_top_k=nms_topk, |
|
keep_top_k=nms_keep_topk, |
|
nms_threshold=nms_iou_threshold) |
|
|
|
post_process = ppdet.modeling.BBoxPostProcess( |
|
decode=ppdet.modeling.YOLOBox( |
|
num_classes=num_classes, |
|
conf_thresh=.005 |
|
if 'MobileNetV3' in self.backbone_name else .01, |
|
scale_x_y=scale_x_y), |
|
nms=nms) |
|
|
|
params.update({ |
|
'backbone': backbone, |
|
'neck': neck, |
|
'yolo_head': yolo_head, |
|
'post_process': post_process |
|
}) |
|
|
|
super(YOLOv3, self).__init__( |
|
model_name='YOLOv3', num_classes=num_classes, **params) |
|
self.anchors = anchors |
|
self.anchor_masks = anchor_masks |
|
self.model_name = 'PPYOLO' |
|
|
|
def _get_test_inputs(self, image_shape): |
|
if image_shape is not None: |
|
image_shape = self._check_image_shape(image_shape) |
|
self._fix_transforms_shape(image_shape[-2:]) |
|
else: |
|
image_shape = [None, 3, 608, 608] |
|
if getattr(self, 'test_transforms', None): |
|
for idx, op in enumerate(self.test_transforms.transforms): |
|
name = op.__class__.__name__ |
|
if name == 'Resize': |
|
image_shape = [None, 3] + list( |
|
self.test_transforms.transforms[idx].target_size) |
|
logging.warning( |
|
'[Important!!!] When exporting inference model for {}, ' |
|
'if fixed_input_shape is not set, it will be forcibly set to {}. ' |
|
'Please ensure image shape after transforms is {}, if not, ' |
|
'fixed_input_shape should be specified manually.' |
|
.format(self.__class__.__name__, image_shape, image_shape[1:])) |
|
|
|
self.fixed_input_shape = image_shape |
|
return self._define_input_spec(image_shape) |
|
|
|
|
|
class PPYOLOTiny(YOLOv3): |
|
def __init__(self, |
|
num_classes=80, |
|
backbone='MobileNetV3', |
|
anchors=[[10, 15], [24, 36], [72, 42], [35, 87], [102, 96], |
|
[60, 170], [220, 125], [128, 222], [264, 266]], |
|
anchor_masks=[[6, 7, 8], [3, 4, 5], [0, 1, 2]], |
|
use_iou_aware=False, |
|
use_spp=True, |
|
use_drop_block=True, |
|
scale_x_y=1.05, |
|
ignore_threshold=0.5, |
|
label_smooth=False, |
|
use_iou_loss=True, |
|
use_matrix_nms=False, |
|
nms_score_threshold=0.005, |
|
nms_topk=1000, |
|
nms_keep_topk=100, |
|
nms_iou_threshold=0.45, |
|
**params): |
|
self.init_params = locals() |
|
if backbone != 'MobileNetV3': |
|
logging.warning("PPYOLOTiny only supports MobileNetV3 as backbone. " |
|
"Backbone is forcibly set to MobileNetV3.") |
|
self.backbone_name = 'MobileNetV3' |
|
self.downsample_ratios = [32, 16, 8] |
|
if params.get('with_net', True): |
|
if paddlers.env_info['place'] == 'gpu' and paddlers.env_info[ |
|
'num'] > 1 and not os.environ.get('PADDLERS_EXPORT_STAGE'): |
|
norm_type = 'sync_bn' |
|
else: |
|
norm_type = 'bn' |
|
|
|
backbone = self._get_backbone( |
|
'MobileNetV3', |
|
model_name='large', |
|
norm_type=norm_type, |
|
scale=.5, |
|
with_extra_blocks=False, |
|
extra_block_filters=[], |
|
feature_maps=[7, 13, 16]) |
|
|
|
neck = ppdet.modeling.PPYOLOTinyFPN( |
|
detection_block_channels=[160, 128, 96], |
|
in_channels=[i.channels for i in backbone.out_shape], |
|
spp=use_spp, |
|
drop_block=use_drop_block) |
|
|
|
loss = ppdet.modeling.YOLOv3Loss( |
|
num_classes=num_classes, |
|
ignore_thresh=ignore_threshold, |
|
downsample=self.downsample_ratios, |
|
label_smooth=label_smooth, |
|
scale_x_y=scale_x_y, |
|
iou_loss=ppdet.modeling.IouLoss( |
|
loss_weight=2.5, loss_square=True) |
|
if use_iou_loss else None, |
|
iou_aware_loss=ppdet.modeling.IouAwareLoss(loss_weight=1.0) |
|
if use_iou_aware else None) |
|
|
|
yolo_head = ppdet.modeling.YOLOv3Head( |
|
in_channels=[i.channels for i in neck.out_shape], |
|
anchors=anchors, |
|
anchor_masks=anchor_masks, |
|
num_classes=num_classes, |
|
loss=loss, |
|
iou_aware=use_iou_aware) |
|
|
|
if use_matrix_nms: |
|
nms = ppdet.modeling.MatrixNMS( |
|
keep_top_k=nms_keep_topk, |
|
score_threshold=nms_score_threshold, |
|
post_threshold=.05, |
|
nms_top_k=nms_topk, |
|
background_label=-1) |
|
else: |
|
nms = ppdet.modeling.MultiClassNMS( |
|
score_threshold=nms_score_threshold, |
|
nms_top_k=nms_topk, |
|
keep_top_k=nms_keep_topk, |
|
nms_threshold=nms_iou_threshold) |
|
|
|
post_process = ppdet.modeling.BBoxPostProcess( |
|
decode=ppdet.modeling.YOLOBox( |
|
num_classes=num_classes, |
|
conf_thresh=.005, |
|
downsample_ratio=32, |
|
clip_bbox=True, |
|
scale_x_y=scale_x_y), |
|
nms=nms) |
|
|
|
params.update({ |
|
'backbone': backbone, |
|
'neck': neck, |
|
'yolo_head': yolo_head, |
|
'post_process': post_process |
|
}) |
|
|
|
super(YOLOv3, self).__init__( |
|
model_name='YOLOv3', num_classes=num_classes, **params) |
|
self.anchors = anchors |
|
self.anchor_masks = anchor_masks |
|
self.model_name = 'PPYOLOTiny' |
|
|
|
def _get_test_inputs(self, image_shape): |
|
if image_shape is not None: |
|
image_shape = self._check_image_shape(image_shape) |
|
self._fix_transforms_shape(image_shape[-2:]) |
|
else: |
|
image_shape = [None, 3, 320, 320] |
|
if getattr(self, 'test_transforms', None): |
|
for idx, op in enumerate(self.test_transforms.transforms): |
|
name = op.__class__.__name__ |
|
if name == 'Resize': |
|
image_shape = [None, 3] + list( |
|
self.test_transforms.transforms[idx].target_size) |
|
logging.warning( |
|
'[Important!!!] When exporting inference model for {},'.format( |
|
self.__class__.__name__) + |
|
' if fixed_input_shape is not set, it will be forcibly set to {}. '. |
|
format(image_shape) + |
|
'Please check image shape after transforms is {}, if not, fixed_input_shape '. |
|
format(image_shape[1:]) + 'should be specified manually.') |
|
|
|
self.fixed_input_shape = image_shape |
|
return self._define_input_spec(image_shape) |
|
|
|
|
|
class PPYOLOv2(YOLOv3): |
|
def __init__(self, |
|
num_classes=80, |
|
backbone='ResNet50_vd_dcn', |
|
anchors=[[10, 13], [16, 30], [33, 23], [30, 61], [62, 45], |
|
[59, 119], [116, 90], [156, 198], [373, 326]], |
|
anchor_masks=[[6, 7, 8], [3, 4, 5], [0, 1, 2]], |
|
use_iou_aware=True, |
|
use_spp=True, |
|
use_drop_block=True, |
|
scale_x_y=1.05, |
|
ignore_threshold=0.7, |
|
label_smooth=False, |
|
use_iou_loss=True, |
|
use_matrix_nms=True, |
|
nms_score_threshold=0.01, |
|
nms_topk=-1, |
|
nms_keep_topk=100, |
|
nms_iou_threshold=0.45, |
|
**params): |
|
self.init_params = locals() |
|
if backbone not in {'ResNet50_vd_dcn', 'ResNet101_vd_dcn'}: |
|
raise ValueError( |
|
"backbone: {} is not supported. Please choose one of " |
|
"{'ResNet50_vd_dcn', 'ResNet101_vd_dcn'}.".format(backbone)) |
|
self.backbone_name = backbone |
|
self.downsample_ratios = [32, 16, 8] |
|
|
|
if params.get('with_net', True): |
|
if paddlers.env_info['place'] == 'gpu' and paddlers.env_info[ |
|
'num'] > 1 and not os.environ.get('PADDLERS_EXPORT_STAGE'): |
|
norm_type = 'sync_bn' |
|
else: |
|
norm_type = 'bn' |
|
|
|
if backbone == 'ResNet50_vd_dcn': |
|
backbone = self._get_backbone( |
|
'ResNet', |
|
variant='d', |
|
norm_type=norm_type, |
|
return_idx=[1, 2, 3], |
|
dcn_v2_stages=[3], |
|
freeze_at=-1, |
|
freeze_norm=False, |
|
norm_decay=0.) |
|
|
|
elif backbone == 'ResNet101_vd_dcn': |
|
backbone = self._get_backbone( |
|
'ResNet', |
|
depth=101, |
|
variant='d', |
|
norm_type=norm_type, |
|
return_idx=[1, 2, 3], |
|
dcn_v2_stages=[3], |
|
freeze_at=-1, |
|
freeze_norm=False, |
|
norm_decay=0.) |
|
|
|
neck = ppdet.modeling.PPYOLOPAN( |
|
norm_type=norm_type, |
|
in_channels=[i.channels for i in backbone.out_shape], |
|
drop_block=use_drop_block, |
|
block_size=3, |
|
keep_prob=.9, |
|
spp=use_spp) |
|
|
|
loss = ppdet.modeling.YOLOv3Loss( |
|
num_classes=num_classes, |
|
ignore_thresh=ignore_threshold, |
|
downsample=self.downsample_ratios, |
|
label_smooth=label_smooth, |
|
scale_x_y=scale_x_y, |
|
iou_loss=ppdet.modeling.IouLoss( |
|
loss_weight=2.5, loss_square=True) |
|
if use_iou_loss else None, |
|
iou_aware_loss=ppdet.modeling.IouAwareLoss(loss_weight=1.0) |
|
if use_iou_aware else None) |
|
|
|
yolo_head = ppdet.modeling.YOLOv3Head( |
|
in_channels=[i.channels for i in neck.out_shape], |
|
anchors=anchors, |
|
anchor_masks=anchor_masks, |
|
num_classes=num_classes, |
|
loss=loss, |
|
iou_aware=use_iou_aware, |
|
iou_aware_factor=.5) |
|
|
|
if use_matrix_nms: |
|
nms = ppdet.modeling.MatrixNMS( |
|
keep_top_k=nms_keep_topk, |
|
score_threshold=nms_score_threshold, |
|
post_threshold=.01, |
|
nms_top_k=nms_topk, |
|
background_label=-1) |
|
else: |
|
nms = ppdet.modeling.MultiClassNMS( |
|
score_threshold=nms_score_threshold, |
|
nms_top_k=nms_topk, |
|
keep_top_k=nms_keep_topk, |
|
nms_threshold=nms_iou_threshold) |
|
|
|
post_process = ppdet.modeling.BBoxPostProcess( |
|
decode=ppdet.modeling.YOLOBox( |
|
num_classes=num_classes, |
|
conf_thresh=.01, |
|
downsample_ratio=32, |
|
clip_bbox=True, |
|
scale_x_y=scale_x_y), |
|
nms=nms) |
|
|
|
params.update({ |
|
'backbone': backbone, |
|
'neck': neck, |
|
'yolo_head': yolo_head, |
|
'post_process': post_process |
|
}) |
|
|
|
super(YOLOv3, self).__init__( |
|
model_name='YOLOv3', num_classes=num_classes, **params) |
|
self.anchors = anchors |
|
self.anchor_masks = anchor_masks |
|
self.model_name = 'PPYOLOv2' |
|
|
|
def _get_test_inputs(self, image_shape): |
|
if image_shape is not None: |
|
image_shape = self._check_image_shape(image_shape) |
|
self._fix_transforms_shape(image_shape[-2:]) |
|
else: |
|
image_shape = [None, 3, 640, 640] |
|
if getattr(self, 'test_transforms', None): |
|
for idx, op in enumerate(self.test_transforms.transforms): |
|
name = op.__class__.__name__ |
|
if name == 'Resize': |
|
image_shape = [None, 3] + list( |
|
self.test_transforms.transforms[idx].target_size) |
|
logging.warning( |
|
'[Important!!!] When exporting inference model for {},'.format( |
|
self.__class__.__name__) + |
|
' if fixed_input_shape is not set, it will be forcibly set to {}. '. |
|
format(image_shape) + |
|
'Please check image shape after transforms is {}, if not, fixed_input_shape '. |
|
format(image_shape[1:]) + 'should be specified manually.') |
|
|
|
self.fixed_input_shape = image_shape |
|
return self._define_input_spec(image_shape) |
|
|
|
|
|
class MaskRCNN(BaseDetector): |
|
def __init__(self, |
|
num_classes=80, |
|
backbone='ResNet50_vd', |
|
with_fpn=True, |
|
with_dcn=False, |
|
aspect_ratios=[0.5, 1.0, 2.0], |
|
anchor_sizes=[[32], [64], [128], [256], [512]], |
|
keep_top_k=100, |
|
nms_threshold=0.5, |
|
score_threshold=0.05, |
|
fpn_num_channels=256, |
|
rpn_batch_size_per_im=256, |
|
rpn_fg_fraction=0.5, |
|
test_pre_nms_top_n=None, |
|
test_post_nms_top_n=1000, |
|
**params): |
|
self.init_params = locals() |
|
if backbone not in { |
|
'ResNet50', 'ResNet50_vd', 'ResNet50_vd_ssld', 'ResNet101', |
|
'ResNet101_vd' |
|
}: |
|
raise ValueError( |
|
"backbone: {} is not supported. Please choose one of " |
|
"{'ResNet50', 'ResNet50_vd', 'ResNet50_vd_ssld', 'ResNet101', 'ResNet101_vd'}.". |
|
format(backbone)) |
|
|
|
self.backbone_name = backbone + '_fpn' if with_fpn else backbone |
|
dcn_v2_stages = [1, 2, 3] if with_dcn else [-1] |
|
|
|
if params.get('with_net', True): |
|
if backbone == 'ResNet50': |
|
if with_fpn: |
|
backbone = self._get_backbone( |
|
'ResNet', |
|
norm_type='bn', |
|
freeze_at=0, |
|
return_idx=[0, 1, 2, 3], |
|
num_stages=4, |
|
dcn_v2_stages=dcn_v2_stages) |
|
else: |
|
if with_dcn: |
|
logging.warning( |
|
"Backbone {} should be used along with dcn disabled, 'with_dcn' is forcibly set to False". |
|
format(backbone)) |
|
backbone = self._get_backbone( |
|
'ResNet', |
|
norm_type='bn', |
|
freeze_at=0, |
|
return_idx=[2], |
|
num_stages=3) |
|
|
|
elif 'ResNet50_vd' in backbone: |
|
if not with_fpn: |
|
logging.warning( |
|
"Backbone {} should be used along with fpn enabled, 'with_fpn' is forcibly set to True". |
|
format(backbone)) |
|
with_fpn = True |
|
backbone = self._get_backbone( |
|
'ResNet', |
|
variant='d', |
|
norm_type='bn', |
|
freeze_at=0, |
|
return_idx=[0, 1, 2, 3], |
|
num_stages=4, |
|
lr_mult_list=[0.05, 0.05, 0.1, 0.15] |
|
if '_ssld' in backbone else [1.0, 1.0, 1.0, 1.0], |
|
dcn_v2_stages=dcn_v2_stages) |
|
|
|
else: |
|
if not with_fpn: |
|
logging.warning( |
|
"Backbone {} should be used along with fpn enabled, 'with_fpn' is forcibly set to True". |
|
format(backbone)) |
|
with_fpn = True |
|
backbone = self._get_backbone( |
|
'ResNet', |
|
variant='d' if '_vd' in backbone else 'b', |
|
depth=101, |
|
norm_type='bn', |
|
freeze_at=0, |
|
return_idx=[0, 1, 2, 3], |
|
num_stages=4, |
|
dcn_v2_stages=dcn_v2_stages) |
|
|
|
rpn_in_channel = backbone.out_shape[0].channels |
|
|
|
if with_fpn: |
|
neck = ppdet.modeling.FPN( |
|
in_channels=[i.channels for i in backbone.out_shape], |
|
out_channel=fpn_num_channels, |
|
spatial_scales=[ |
|
1.0 / i.stride for i in backbone.out_shape |
|
]) |
|
rpn_in_channel = neck.out_shape[0].channels |
|
anchor_generator_cfg = { |
|
'aspect_ratios': aspect_ratios, |
|
'anchor_sizes': anchor_sizes, |
|
'strides': [4, 8, 16, 32, 64] |
|
} |
|
train_proposal_cfg = { |
|
'min_size': 0.0, |
|
'nms_thresh': .7, |
|
'pre_nms_top_n': 2000, |
|
'post_nms_top_n': 1000, |
|
'topk_after_collect': True |
|
} |
|
test_proposal_cfg = { |
|
'min_size': 0.0, |
|
'nms_thresh': .7, |
|
'pre_nms_top_n': 1000 |
|
if test_pre_nms_top_n is None else test_pre_nms_top_n, |
|
'post_nms_top_n': test_post_nms_top_n |
|
} |
|
bb_head = ppdet.modeling.TwoFCHead( |
|
in_channel=neck.out_shape[0].channels, out_channel=1024) |
|
bb_roi_extractor_cfg = { |
|
'resolution': 7, |
|
'spatial_scale': [1. / i.stride for i in neck.out_shape], |
|
'sampling_ratio': 0, |
|
'aligned': True |
|
} |
|
with_pool = False |
|
m_head = ppdet.modeling.MaskFeat( |
|
in_channel=neck.out_shape[0].channels, |
|
out_channel=256, |
|
num_convs=4) |
|
m_roi_extractor_cfg = { |
|
'resolution': 14, |
|
'spatial_scale': [1. / i.stride for i in neck.out_shape], |
|
'sampling_ratio': 0, |
|
'aligned': True |
|
} |
|
mask_assigner = MaskAssigner( |
|
num_classes=num_classes, mask_resolution=28) |
|
share_bbox_feat = False |
|
|
|
else: |
|
neck = None |
|
anchor_generator_cfg = { |
|
'aspect_ratios': aspect_ratios, |
|
'anchor_sizes': anchor_sizes, |
|
'strides': [16] |
|
} |
|
train_proposal_cfg = { |
|
'min_size': 0.0, |
|
'nms_thresh': .7, |
|
'pre_nms_top_n': 12000, |
|
'post_nms_top_n': 2000, |
|
'topk_after_collect': False |
|
} |
|
test_proposal_cfg = { |
|
'min_size': 0.0, |
|
'nms_thresh': .7, |
|
'pre_nms_top_n': 6000 |
|
if test_pre_nms_top_n is None else test_pre_nms_top_n, |
|
'post_nms_top_n': test_post_nms_top_n |
|
} |
|
bb_head = ppdet.modeling.Res5Head() |
|
bb_roi_extractor_cfg = { |
|
'resolution': 14, |
|
'spatial_scale': |
|
[1. / i.stride for i in backbone.out_shape], |
|
'sampling_ratio': 0, |
|
'aligned': True |
|
} |
|
with_pool = True |
|
m_head = ppdet.modeling.MaskFeat( |
|
in_channel=bb_head.out_shape[0].channels, |
|
out_channel=256, |
|
num_convs=0) |
|
m_roi_extractor_cfg = { |
|
'resolution': 14, |
|
'spatial_scale': |
|
[1. / i.stride for i in backbone.out_shape], |
|
'sampling_ratio': 0, |
|
'aligned': True |
|
} |
|
mask_assigner = MaskAssigner( |
|
num_classes=num_classes, mask_resolution=14) |
|
share_bbox_feat = True |
|
|
|
rpn_target_assign_cfg = { |
|
'batch_size_per_im': rpn_batch_size_per_im, |
|
'fg_fraction': rpn_fg_fraction, |
|
'negative_overlap': .3, |
|
'positive_overlap': .7, |
|
'use_random': True |
|
} |
|
|
|
rpn_head = ppdet.modeling.RPNHead( |
|
anchor_generator=anchor_generator_cfg, |
|
rpn_target_assign=rpn_target_assign_cfg, |
|
train_proposal=train_proposal_cfg, |
|
test_proposal=test_proposal_cfg, |
|
in_channel=rpn_in_channel) |
|
|
|
bbox_assigner = BBoxAssigner(num_classes=num_classes) |
|
|
|
bbox_head = ppdet.modeling.BBoxHead( |
|
head=bb_head, |
|
in_channel=bb_head.out_shape[0].channels, |
|
roi_extractor=bb_roi_extractor_cfg, |
|
with_pool=with_pool, |
|
bbox_assigner=bbox_assigner, |
|
num_classes=num_classes) |
|
|
|
mask_head = ppdet.modeling.MaskHead( |
|
head=m_head, |
|
roi_extractor=m_roi_extractor_cfg, |
|
mask_assigner=mask_assigner, |
|
share_bbox_feat=share_bbox_feat, |
|
num_classes=num_classes) |
|
|
|
bbox_post_process = ppdet.modeling.BBoxPostProcess( |
|
num_classes=num_classes, |
|
decode=ppdet.modeling.RCNNBox(num_classes=num_classes), |
|
nms=ppdet.modeling.MultiClassNMS( |
|
score_threshold=score_threshold, |
|
keep_top_k=keep_top_k, |
|
nms_threshold=nms_threshold)) |
|
|
|
mask_post_process = ppdet.modeling.MaskPostProcess(binary_thresh=.5) |
|
|
|
params.update({ |
|
'backbone': backbone, |
|
'neck': neck, |
|
'rpn_head': rpn_head, |
|
'bbox_head': bbox_head, |
|
'mask_head': mask_head, |
|
'bbox_post_process': bbox_post_process, |
|
'mask_post_process': mask_post_process |
|
}) |
|
self.with_fpn = with_fpn |
|
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. |
|
""" |
|
|
|
if train_dataset.pos_num < len(train_dataset.file_list): |
|
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) |
|
|
|
def _compose_batch_transform(self, transforms, mode='train'): |
|
if mode == 'train': |
|
default_batch_transforms = [ |
|
_BatchPad(pad_to_stride=32 if self.with_fpn else -1) |
|
] |
|
else: |
|
default_batch_transforms = [ |
|
_BatchPad(pad_to_stride=32 if self.with_fpn else -1) |
|
] |
|
custom_batch_transforms = [] |
|
for i, op in enumerate(transforms.transforms): |
|
if isinstance(op, (BatchRandomResize, BatchRandomResizeByShort)): |
|
if mode != 'train': |
|
raise ValueError( |
|
"{} cannot be present in the {} transforms. ".format( |
|
op.__class__.__name__, mode) + |
|
"Please check the {} transforms.".format(mode)) |
|
custom_batch_transforms.insert(0, copy.deepcopy(op)) |
|
|
|
batch_transforms = BatchCompose( |
|
custom_batch_transforms + default_batch_transforms, |
|
collate_batch=False) |
|
|
|
return batch_transforms |
|
|
|
def _fix_transforms_shape(self, image_shape): |
|
if getattr(self, 'test_transforms', 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 == 'ResizeByShort': |
|
has_resize_op = True |
|
resize_op_idx = idx |
|
if name == 'Normalize': |
|
normalize_op_idx = idx |
|
|
|
if not has_resize_op: |
|
self.test_transforms.transforms.insert( |
|
normalize_op_idx, |
|
Resize( |
|
target_size=image_shape, |
|
keep_ratio=True, |
|
interp='CUBIC')) |
|
else: |
|
self.test_transforms.transforms[resize_op_idx] = Resize( |
|
target_size=image_shape, keep_ratio=True, interp='CUBIC') |
|
self.test_transforms.transforms.append( |
|
Pad(im_padding_value=[0., 0., 0.])) |
|
|
|
def _get_test_inputs(self, image_shape): |
|
if image_shape is not None: |
|
image_shape = self._check_image_shape(image_shape) |
|
self._fix_transforms_shape(image_shape[-2:]) |
|
else: |
|
image_shape = [None, 3, -1, -1] |
|
if self.with_fpn: |
|
self.test_transforms.transforms.append( |
|
Pad(im_padding_value=[0., 0., 0.])) |
|
self.fixed_input_shape = image_shape |
|
|
|
return self._define_input_spec(image_shape)
|
|
|