|
import os |
|
from pathlib import Path |
|
from typing import List, Optional, Type, Union |
|
|
|
import torch |
|
import torch.distributed as dist |
|
from loguru import logger |
|
from omegaconf import ListConfig |
|
from torch import Tensor |
|
from torch.optim import Optimizer |
|
from torch.optim.lr_scheduler import LambdaLR, SequentialLR, _LRScheduler |
|
|
|
from yolo.config.config import IDX_TO_ID, NMSConfig, OptimizerConfig, SchedulerConfig |
|
from yolo.model.yolo import YOLO |
|
from yolo.utils.bounding_box_utils import bbox_nms, transform_bbox |
|
|
|
|
|
class ExponentialMovingAverage: |
|
def __init__(self, model: torch.nn.Module, decay: float): |
|
self.model = model |
|
self.decay = decay |
|
self.shadow = {name: param.clone().detach() for name, param in model.named_parameters()} |
|
|
|
def update(self): |
|
"""Update the shadow parameters using the current model parameters.""" |
|
for name, param in self.model.named_parameters(): |
|
assert name in self.shadow, "All model parameters should have a corresponding shadow parameter." |
|
new_average = (1.0 - self.decay) * param.data + self.decay * self.shadow[name] |
|
self.shadow[name] = new_average.clone() |
|
|
|
def apply_shadow(self): |
|
"""Apply the shadow parameters to the model.""" |
|
for name, param in self.model.named_parameters(): |
|
param.data.copy_(self.shadow[name]) |
|
|
|
def restore(self): |
|
"""Restore the original parameters from the shadow.""" |
|
for name, param in self.model.named_parameters(): |
|
self.shadow[name].copy_(param.data) |
|
|
|
|
|
def create_optimizer(model: YOLO, optim_cfg: OptimizerConfig) -> Optimizer: |
|
"""Create an optimizer for the given model parameters based on the configuration. |
|
|
|
Returns: |
|
An instance of the optimizer configured according to the provided settings. |
|
""" |
|
optimizer_class: Type[Optimizer] = getattr(torch.optim, optim_cfg.type) |
|
|
|
bias_params = [p for name, p in model.named_parameters() if "bias" in name] |
|
norm_params = [p for name, p in model.named_parameters() if "weight" in name and "bn" in name] |
|
conv_params = [p for name, p in model.named_parameters() if "weight" in name and "bn" not in name] |
|
|
|
model_parameters = [ |
|
{"params": bias_params, "nestrov": True, "momentum": 0.937}, |
|
{"params": conv_params, "weight_decay": 0.0}, |
|
{"params": norm_params, "weight_decay": 1e-5}, |
|
] |
|
return optimizer_class(model_parameters, **optim_cfg.args) |
|
|
|
|
|
def create_scheduler(optimizer: Optimizer, schedule_cfg: SchedulerConfig) -> _LRScheduler: |
|
"""Create a learning rate scheduler for the given optimizer based on the configuration. |
|
|
|
Returns: |
|
An instance of the scheduler configured according to the provided settings. |
|
""" |
|
scheduler_class: Type[_LRScheduler] = getattr(torch.optim.lr_scheduler, schedule_cfg.type) |
|
schedule = scheduler_class(optimizer, **schedule_cfg.args) |
|
if hasattr(schedule_cfg, "warmup"): |
|
wepoch = schedule_cfg.warmup.epochs |
|
lambda1 = lambda epoch: 0.1 + 0.9 * (epoch + 1 / wepoch) if epoch < wepoch else 1 |
|
lambda2 = lambda epoch: 10 - 9 * (epoch / wepoch) if epoch < wepoch else 1 |
|
warmup_schedule = LambdaLR(optimizer, lr_lambda=[lambda1, lambda2, lambda1]) |
|
schedule = SequentialLR(optimizer, schedulers=[warmup_schedule, schedule], milestones=[2]) |
|
return schedule |
|
|
|
|
|
def initialize_distributed() -> None: |
|
rank = int(os.getenv("RANK", "0")) |
|
local_rank = int(os.getenv("LOCAL_RANK", "0")) |
|
world_size = int(os.getenv("WORLD_SIZE", "1")) |
|
|
|
torch.cuda.set_device(local_rank) |
|
dist.init_process_group(backend="nccl", rank=rank, world_size=world_size) |
|
logger.info(f"Initialized process group; rank: {rank}, size: {world_size}") |
|
return local_rank |
|
|
|
|
|
def get_device(device_spec: Union[str, int, List[int]]) -> torch.device: |
|
ddp_flag = False |
|
if isinstance(device_spec, (list, ListConfig)): |
|
ddp_flag = True |
|
device_spec = initialize_distributed() |
|
device = torch.device(device_spec) |
|
return device, ddp_flag |
|
|
|
|
|
class PostProccess: |
|
""" |
|
TODO: function document |
|
scale back the prediction and do nms for pred_bbox |
|
""" |
|
|
|
def __init__(self, vec2box, nms_cfg: NMSConfig) -> None: |
|
self.vec2box = vec2box |
|
self.nms = nms_cfg |
|
|
|
def __call__(self, predict, rev_tensor: Optional[Tensor]): |
|
pred_class, _, pred_bbox = self.vec2box(predict["Main"]) |
|
if rev_tensor is not None: |
|
pred_bbox = (pred_bbox - rev_tensor[:, None, 1:]) / rev_tensor[:, 0:1, None] |
|
pred_bbox = bbox_nms(pred_class, pred_bbox, self.nms) |
|
return pred_bbox |
|
|
|
|
|
def predicts_to_json(img_paths, predicts): |
|
""" |
|
TODO: function document |
|
turn a batch of imagepath and predicts(n x 6 for each image) to a List of diction(Detection output) |
|
""" |
|
batch_json = [] |
|
for img_path, bboxes in zip(img_paths, predicts): |
|
bboxes[:, 1:5] = transform_bbox(bboxes[:, 1:5], "xyxy -> xywh") |
|
for cls, *pos, conf in bboxes: |
|
bbox = { |
|
"image_id": int(Path(img_path).stem), |
|
"category_id": IDX_TO_ID[int(cls)], |
|
"bbox": [float(p) for p in pos], |
|
"score": float(conf), |
|
} |
|
batch_json.append(bbox) |
|
return batch_json |
|
|