π [Merge] branch 'SETUP' into TEST
Browse files- yolo/config/dataset/dev.yaml +1 -1
- yolo/config/general.yaml +1 -1
- yolo/config/task/validation.yaml +2 -2
- yolo/lazy.py +6 -10
- yolo/model/yolo.py +3 -8
- yolo/tools/data_augmentation.py +2 -6
- yolo/tools/data_loader.py +1 -1
- yolo/tools/solver.py +39 -19
- yolo/utils/bounding_box_utils.py +12 -9
- yolo/utils/logging_utils.py +81 -28
- yolo/utils/model_utils.py +5 -3
- yolo/utils/solver_utils.py +46 -0
yolo/config/dataset/dev.yaml
CHANGED
@@ -1,5 +1,5 @@
|
|
1 |
path: data/dev
|
2 |
train: train
|
3 |
-
validation:
|
4 |
|
5 |
auto_download:
|
|
|
1 |
path: data/dev
|
2 |
train: train
|
3 |
+
validation: val
|
4 |
|
5 |
auto_download:
|
yolo/config/general.yaml
CHANGED
@@ -9,7 +9,7 @@ out_path: runs
|
|
9 |
exist_ok: True
|
10 |
|
11 |
lucky_number: 10
|
12 |
-
use_wandb:
|
13 |
use_TensorBoard: False
|
14 |
|
15 |
weight: True # Path to weight or True for auto, False for no pretrained weight
|
|
|
9 |
exist_ok: True
|
10 |
|
11 |
lucky_number: 10
|
12 |
+
use_wandb: True
|
13 |
use_TensorBoard: False
|
14 |
|
15 |
weight: True # Path to weight or True for auto, False for no pretrained weight
|
yolo/config/task/validation.yaml
CHANGED
@@ -8,5 +8,5 @@ data:
|
|
8 |
pin_memory: True
|
9 |
data_augment: {}
|
10 |
nms:
|
11 |
-
min_confidence: 0.
|
12 |
-
min_iou: 0.
|
|
|
8 |
pin_memory: True
|
9 |
data_augment: {}
|
10 |
nms:
|
11 |
+
min_confidence: 0.05
|
12 |
+
min_iou: 0.9
|
yolo/lazy.py
CHANGED
@@ -28,18 +28,14 @@ def main(cfg: Config):
|
|
28 |
model = model.to(device)
|
29 |
|
30 |
vec2box = Vec2Box(model, cfg.image_size, device)
|
31 |
-
|
32 |
if cfg.task.task == "train":
|
33 |
-
|
34 |
-
trainer.solve(dataloader)
|
35 |
-
|
36 |
-
if cfg.task.task == "inference":
|
37 |
-
tester = ModelTester(cfg, model, vec2box, progress, device)
|
38 |
-
tester.solve(dataloader)
|
39 |
-
|
40 |
if cfg.task.task == "validation":
|
41 |
-
|
42 |
-
|
|
|
|
|
|
|
43 |
|
44 |
|
45 |
if __name__ == "__main__":
|
|
|
28 |
model = model.to(device)
|
29 |
|
30 |
vec2box = Vec2Box(model, cfg.image_size, device)
|
|
|
31 |
if cfg.task.task == "train":
|
32 |
+
solver = ModelTrainer(cfg, model, vec2box, progress, device, use_ddp)
|
|
|
|
|
|
|
|
|
|
|
|
|
33 |
if cfg.task.task == "validation":
|
34 |
+
solver = ModelValidator(cfg.task, model, vec2box, progress, device)
|
35 |
+
if cfg.task.task == "inference":
|
36 |
+
solver = ModelTester(cfg, model, vec2box, progress, device)
|
37 |
+
progress.start()
|
38 |
+
solver.solve(dataloader)
|
39 |
|
40 |
|
41 |
if __name__ == "__main__":
|
yolo/model/yolo.py
CHANGED
@@ -82,17 +82,14 @@ class YOLO(nn.Module):
|
|
82 |
return output
|
83 |
|
84 |
def get_out_channels(self, layer_type: str, layer_args: dict, output_dim: list, source: Union[int, list]):
|
85 |
-
|
86 |
-
if any(module in layer_type for module in ["Conv", "ELAN", "ADown", "AConv"]):
|
87 |
return layer_args["out_channels"]
|
88 |
if layer_type == "CBFuse":
|
89 |
return output_dim[source[-1]]
|
90 |
-
if
|
91 |
return output_dim[source]
|
92 |
-
if
|
93 |
return sum(output_dim[idx] for idx in source)
|
94 |
-
if layer_type == "IDetect":
|
95 |
-
return None
|
96 |
|
97 |
def get_source_idx(self, source: Union[ListConfig, str, int], layer_idx: int):
|
98 |
if isinstance(source, ListConfig):
|
@@ -128,7 +125,6 @@ def create_model(model_cfg: ModelConfig, weight_path: Union[bool, str] = True, c
|
|
128 |
Returns:
|
129 |
YOLO: An instance of the model defined by the given configuration.
|
130 |
"""
|
131 |
-
# TODO: "weight_path -> weight = [True|None-False|Path]: True should be default of model name?"
|
132 |
OmegaConf.set_struct(model_cfg, False)
|
133 |
model = YOLO(model_cfg, class_num)
|
134 |
if weight_path:
|
@@ -138,7 +134,6 @@ def create_model(model_cfg: ModelConfig, weight_path: Union[bool, str] = True, c
|
|
138 |
logger.info(f"π Weight {weight_path} not found, try downloading")
|
139 |
prepare_weight(weight_path=weight_path)
|
140 |
if os.path.exists(weight_path):
|
141 |
-
# TODO: fix map_location
|
142 |
model.model.load_state_dict(torch.load(weight_path, map_location=torch.device("cpu")), strict=False)
|
143 |
logger.info("β
Success load model & weight")
|
144 |
else:
|
|
|
82 |
return output
|
83 |
|
84 |
def get_out_channels(self, layer_type: str, layer_args: dict, output_dim: list, source: Union[int, list]):
|
85 |
+
if hasattr(layer_args, "out_channels"):
|
|
|
86 |
return layer_args["out_channels"]
|
87 |
if layer_type == "CBFuse":
|
88 |
return output_dim[source[-1]]
|
89 |
+
if isinstance(source, int):
|
90 |
return output_dim[source]
|
91 |
+
if isinstance(source, list):
|
92 |
return sum(output_dim[idx] for idx in source)
|
|
|
|
|
93 |
|
94 |
def get_source_idx(self, source: Union[ListConfig, str, int], layer_idx: int):
|
95 |
if isinstance(source, ListConfig):
|
|
|
125 |
Returns:
|
126 |
YOLO: An instance of the model defined by the given configuration.
|
127 |
"""
|
|
|
128 |
OmegaConf.set_struct(model_cfg, False)
|
129 |
model = YOLO(model_cfg, class_num)
|
130 |
if weight_path:
|
|
|
134 |
logger.info(f"π Weight {weight_path} not found, try downloading")
|
135 |
prepare_weight(weight_path=weight_path)
|
136 |
if os.path.exists(weight_path):
|
|
|
137 |
model.model.load_state_dict(torch.load(weight_path, map_location=torch.device("cpu")), strict=False)
|
138 |
logger.info("β
Success load model & weight")
|
139 |
else:
|
yolo/tools/data_augmentation.py
CHANGED
@@ -46,12 +46,8 @@ class PadAndResize:
|
|
46 |
padded_image = Image.new("RGB", (self.target_width, self.target_height), self.background_color)
|
47 |
padded_image.paste(resized_image, (pad_left, pad_top))
|
48 |
|
49 |
-
boxes[:, 1]
|
50 |
-
boxes[:, 2]
|
51 |
-
boxes[:, 3] *= scale # xmax
|
52 |
-
boxes[:, 4] *= scale # ymax
|
53 |
-
boxes[:, [1, 3]] += pad_left
|
54 |
-
boxes[:, [2, 4]] += pad_top
|
55 |
|
56 |
transform_info = torch.tensor([scale, pad_left, pad_top, pad_left, pad_top])
|
57 |
return padded_image, boxes, transform_info
|
|
|
46 |
padded_image = Image.new("RGB", (self.target_width, self.target_height), self.background_color)
|
47 |
padded_image.paste(resized_image, (pad_left, pad_top))
|
48 |
|
49 |
+
boxes[:, [1, 3]] = (boxes[:, [1, 3]] * new_width + pad_left) / self.target_width
|
50 |
+
boxes[:, [2, 4]] = (boxes[:, [2, 4]] * new_height + pad_top) / self.target_height
|
|
|
|
|
|
|
|
|
51 |
|
52 |
transform_info = torch.tensor([scale, pad_left, pad_top, pad_left, pad_top])
|
53 |
return padded_image, boxes, transform_info
|
yolo/tools/data_loader.py
CHANGED
@@ -199,7 +199,7 @@ class YoloDataLoader(DataLoader):
|
|
199 |
batch_images = torch.stack(batch_images)
|
200 |
batch_reverse = torch.stack(batch_reverse)
|
201 |
|
202 |
-
return batch_images, batch_targets, batch_reverse, batch_path
|
203 |
|
204 |
|
205 |
def create_dataloader(data_cfg: DataConfig, dataset_cfg: DatasetConfig, task: str = "train", use_ddp: bool = False):
|
|
|
199 |
batch_images = torch.stack(batch_images)
|
200 |
batch_reverse = torch.stack(batch_reverse)
|
201 |
|
202 |
+
return batch_size, batch_images, batch_targets, batch_reverse, batch_path
|
203 |
|
204 |
|
205 |
def create_dataloader(data_cfg: DataConfig, dataset_cfg: DatasetConfig, task: str = "train", use_ddp: bool = False):
|
yolo/tools/solver.py
CHANGED
@@ -1,9 +1,12 @@
|
|
1 |
import json
|
2 |
import os
|
|
|
3 |
import time
|
|
|
4 |
|
5 |
import torch
|
6 |
from loguru import logger
|
|
|
7 |
from torch import Tensor
|
8 |
|
9 |
# TODO: We may can't use CUDA?
|
@@ -16,7 +19,7 @@ from yolo.model.yolo import YOLO
|
|
16 |
from yolo.tools.data_loader import StreamDataLoader, create_dataloader
|
17 |
from yolo.tools.drawer import draw_bboxes, draw_model
|
18 |
from yolo.tools.loss_functions import create_loss_function
|
19 |
-
from yolo.utils.bounding_box_utils import Vec2Box
|
20 |
from yolo.utils.logging_utils import ProgressLogger, log_model_structure
|
21 |
from yolo.utils.model_utils import (
|
22 |
ExponentialMovingAverage,
|
@@ -25,6 +28,7 @@ from yolo.utils.model_utils import (
|
|
25 |
create_scheduler,
|
26 |
predicts_to_json,
|
27 |
)
|
|
|
28 |
|
29 |
|
30 |
class ModelTrainer:
|
@@ -69,22 +73,28 @@ class ModelTrainer:
|
|
69 |
self.scaler.step(self.optimizer)
|
70 |
self.scaler.update()
|
71 |
|
72 |
-
return
|
73 |
|
74 |
def train_one_epoch(self, dataloader):
|
75 |
self.model.train()
|
76 |
-
total_loss = 0
|
|
|
77 |
|
78 |
-
for images, targets, *_ in dataloader:
|
79 |
-
|
80 |
|
81 |
-
|
|
|
|
|
82 |
self.progress.one_batch(loss_each)
|
83 |
|
|
|
|
|
|
|
84 |
if self.scheduler:
|
85 |
self.scheduler.step()
|
86 |
|
87 |
-
return total_loss
|
88 |
|
89 |
def save_checkpoint(self, epoch: int, filename="checkpoint.pt"):
|
90 |
checkpoint = {
|
@@ -107,12 +117,11 @@ class ModelTrainer:
|
|
107 |
if self.use_ddp:
|
108 |
dataloader.sampler.set_epoch(epoch)
|
109 |
|
110 |
-
self.progress.start_one_epoch(len(dataloader), self.optimizer, epoch)
|
111 |
-
# TODO: calculate epoch loss
|
112 |
epoch_loss = self.train_one_epoch(dataloader)
|
113 |
-
self.progress.finish_one_epoch()
|
114 |
|
115 |
-
self.validator.solve(self.validation_dataloader)
|
116 |
|
117 |
|
118 |
class ModelTester:
|
@@ -187,19 +196,30 @@ class ModelValidator:
|
|
187 |
self.post_proccess = PostProccess(vec2box, validation_cfg.nms)
|
188 |
self.json_path = os.path.join(self.progress.save_path, f"predict.json")
|
189 |
|
190 |
-
|
|
|
|
|
|
|
|
|
|
|
191 |
# logger.info("π§ͺ Start Validation!")
|
192 |
self.model.eval()
|
193 |
-
predict_json = []
|
194 |
-
self.progress.start_one_epoch(len(dataloader))
|
195 |
-
for images, targets, rev_tensor, img_paths in dataloader:
|
196 |
images, targets, rev_tensor = images.to(self.device), targets.to(self.device), rev_tensor.to(self.device)
|
197 |
with torch.no_grad():
|
198 |
predicts = self.model(images)
|
199 |
-
predicts = self.post_proccess(predicts
|
200 |
-
|
|
|
|
|
201 |
|
202 |
-
predict_json.extend(predicts_to_json(img_paths, predicts))
|
203 |
-
self.progress.finish_one_epoch()
|
204 |
with open(self.json_path, "w") as f:
|
205 |
json.dump(predict_json, f)
|
|
|
|
|
|
|
|
|
|
1 |
import json
|
2 |
import os
|
3 |
+
import sys
|
4 |
import time
|
5 |
+
from collections import defaultdict
|
6 |
|
7 |
import torch
|
8 |
from loguru import logger
|
9 |
+
from pycocotools.coco import COCO
|
10 |
from torch import Tensor
|
11 |
|
12 |
# TODO: We may can't use CUDA?
|
|
|
19 |
from yolo.tools.data_loader import StreamDataLoader, create_dataloader
|
20 |
from yolo.tools.drawer import draw_bboxes, draw_model
|
21 |
from yolo.tools.loss_functions import create_loss_function
|
22 |
+
from yolo.utils.bounding_box_utils import Vec2Box, calculate_map
|
23 |
from yolo.utils.logging_utils import ProgressLogger, log_model_structure
|
24 |
from yolo.utils.model_utils import (
|
25 |
ExponentialMovingAverage,
|
|
|
28 |
create_scheduler,
|
29 |
predicts_to_json,
|
30 |
)
|
31 |
+
from yolo.utils.solver_utils import calculate_ap
|
32 |
|
33 |
|
34 |
class ModelTrainer:
|
|
|
73 |
self.scaler.step(self.optimizer)
|
74 |
self.scaler.update()
|
75 |
|
76 |
+
return loss_item
|
77 |
|
78 |
def train_one_epoch(self, dataloader):
|
79 |
self.model.train()
|
80 |
+
total_loss = defaultdict(lambda: torch.tensor(0.0, device=self.device))
|
81 |
+
total_samples = 0
|
82 |
|
83 |
+
for batch_size, images, targets, *_ in dataloader:
|
84 |
+
loss_each = self.train_one_batch(images, targets)
|
85 |
|
86 |
+
for loss_name, loss_val in loss_each.items():
|
87 |
+
total_loss[loss_name] += loss_val * batch_size
|
88 |
+
total_samples += batch_size
|
89 |
self.progress.one_batch(loss_each)
|
90 |
|
91 |
+
for loss_val in total_loss.values():
|
92 |
+
loss_val /= total_samples
|
93 |
+
|
94 |
if self.scheduler:
|
95 |
self.scheduler.step()
|
96 |
|
97 |
+
return total_loss
|
98 |
|
99 |
def save_checkpoint(self, epoch: int, filename="checkpoint.pt"):
|
100 |
checkpoint = {
|
|
|
117 |
if self.use_ddp:
|
118 |
dataloader.sampler.set_epoch(epoch)
|
119 |
|
120 |
+
self.progress.start_one_epoch(len(dataloader), "Train", self.optimizer, epoch)
|
|
|
121 |
epoch_loss = self.train_one_epoch(dataloader)
|
122 |
+
self.progress.finish_one_epoch(epoch_loss, epoch)
|
123 |
|
124 |
+
self.validator.solve(self.validation_dataloader, epoch_idx=epoch)
|
125 |
|
126 |
|
127 |
class ModelTester:
|
|
|
196 |
self.post_proccess = PostProccess(vec2box, validation_cfg.nms)
|
197 |
self.json_path = os.path.join(self.progress.save_path, f"predict.json")
|
198 |
|
199 |
+
sys.stdout = open(os.devnull, "w")
|
200 |
+
# TODO: load with config file
|
201 |
+
self.coco_gt = COCO("data/coco/annotations/instances_val2017.json")
|
202 |
+
sys.stdout = sys.__stdout__
|
203 |
+
|
204 |
+
def solve(self, dataloader, epoch_idx=-1):
|
205 |
# logger.info("π§ͺ Start Validation!")
|
206 |
self.model.eval()
|
207 |
+
mAPs, predict_json = [], []
|
208 |
+
self.progress.start_one_epoch(len(dataloader), task="Validate")
|
209 |
+
for batch_size, images, targets, rev_tensor, img_paths in dataloader:
|
210 |
images, targets, rev_tensor = images.to(self.device), targets.to(self.device), rev_tensor.to(self.device)
|
211 |
with torch.no_grad():
|
212 |
predicts = self.model(images)
|
213 |
+
predicts = self.post_proccess(predicts)
|
214 |
+
for idx, predict in enumerate(predicts):
|
215 |
+
mAPs.append(calculate_map(predict, targets[idx]))
|
216 |
+
self.progress.one_batch(Tensor(mAPs))
|
217 |
|
218 |
+
predict_json.extend(predicts_to_json(img_paths, predicts, rev_tensor))
|
219 |
+
self.progress.finish_one_epoch(Tensor(mAPs), epoch_idx=epoch_idx)
|
220 |
with open(self.json_path, "w") as f:
|
221 |
json.dump(predict_json, f)
|
222 |
+
|
223 |
+
self.progress.start_pycocotools()
|
224 |
+
result = calculate_ap(self.coco_gt, predict_json)
|
225 |
+
self.progress.finish_pycocotools(result, epoch_idx)
|
yolo/utils/bounding_box_utils.py
CHANGED
@@ -5,7 +5,7 @@ import torch
|
|
5 |
import torch.nn.functional as F
|
6 |
from einops import rearrange
|
7 |
from loguru import logger
|
8 |
-
from torch import Tensor
|
9 |
from torchvision.ops import batched_nms
|
10 |
|
11 |
from yolo.config.config import MatcherConfig, ModelConfig, NMSConfig
|
@@ -338,8 +338,8 @@ def bbox_nms(cls_dist: Tensor, bbox: Tensor, nms_cfg: NMSConfig):
|
|
338 |
return predicts_nms
|
339 |
|
340 |
|
341 |
-
def calculate_map(predictions, ground_truths, iou_thresholds):
|
342 |
-
# TODO: Refactor this block
|
343 |
device = predictions.device
|
344 |
n_preds = predictions.size(0)
|
345 |
n_gts = (ground_truths[:, 0] != -1).sum()
|
@@ -369,13 +369,16 @@ def calculate_map(predictions, ground_truths, iou_thresholds):
|
|
369 |
precision = tp_cumsum / (tp_cumsum + fp_cumsum + 1e-6)
|
370 |
recall = tp_cumsum / (n_gts + 1e-6)
|
371 |
|
372 |
-
|
373 |
-
|
374 |
-
|
375 |
-
|
|
|
|
|
|
|
|
|
376 |
|
377 |
-
ap = precision_at_recall.mean()
|
378 |
aps.append(ap)
|
379 |
|
380 |
mean_ap = torch.mean(torch.stack(aps))
|
381 |
-
return mean_ap, aps
|
|
|
5 |
import torch.nn.functional as F
|
6 |
from einops import rearrange
|
7 |
from loguru import logger
|
8 |
+
from torch import Tensor, arange
|
9 |
from torchvision.ops import batched_nms
|
10 |
|
11 |
from yolo.config.config import MatcherConfig, ModelConfig, NMSConfig
|
|
|
338 |
return predicts_nms
|
339 |
|
340 |
|
341 |
+
def calculate_map(predictions, ground_truths, iou_thresholds=arange(0.5, 1, 0.05)):
|
342 |
+
# TODO: Refactor this block, Flexible for calculate different mAP condition?
|
343 |
device = predictions.device
|
344 |
n_preds = predictions.size(0)
|
345 |
n_gts = (ground_truths[:, 0] != -1).sum()
|
|
|
369 |
precision = tp_cumsum / (tp_cumsum + fp_cumsum + 1e-6)
|
370 |
recall = tp_cumsum / (n_gts + 1e-6)
|
371 |
|
372 |
+
precision = torch.cat([torch.ones(1, device=device), precision, torch.zeros(1, device=device)])
|
373 |
+
recall = torch.cat([torch.zeros(1, device=device), recall, torch.ones(1, device=device)])
|
374 |
+
|
375 |
+
precision, _ = torch.cummax(precision.flip(0), dim=0)
|
376 |
+
precision = precision.flip(0)
|
377 |
+
|
378 |
+
indices = (recall[1:] != recall[:-1]).nonzero(as_tuple=True)[0]
|
379 |
+
ap = torch.sum((recall[indices + 1] - recall[indices]) * precision[indices + 1])
|
380 |
|
|
|
381 |
aps.append(ap)
|
382 |
|
383 |
mean_ap = torch.mean(torch.stack(aps))
|
384 |
+
return mean_ap, aps[0]
|
yolo/utils/logging_utils.py
CHANGED
@@ -13,18 +13,27 @@ Example:
|
|
13 |
|
14 |
import os
|
15 |
import sys
|
16 |
-
from
|
|
|
17 |
|
18 |
import wandb
|
19 |
import wandb.errors.term
|
20 |
from loguru import logger
|
21 |
-
from
|
22 |
-
from rich.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
23 |
from rich.table import Table
|
24 |
from torch import Tensor
|
25 |
from torch.optim import Optimizer
|
26 |
|
27 |
from yolo.config.config import Config, YOLOLayer
|
|
|
28 |
|
29 |
|
30 |
def custom_logger(quite: bool = False):
|
@@ -38,20 +47,24 @@ def custom_logger(quite: bool = False):
|
|
38 |
)
|
39 |
|
40 |
|
41 |
-
class ProgressLogger:
|
42 |
-
def __init__(self, cfg: Config, exp_name: str):
|
43 |
local_rank = int(os.getenv("LOCAL_RANK", "0"))
|
44 |
self.quite_mode = local_rank or getattr(cfg, "quite", False)
|
45 |
custom_logger(self.quite_mode)
|
46 |
self.save_path = validate_log_directory(cfg, exp_name=cfg.name)
|
47 |
|
48 |
-
|
|
|
49 |
TextColumn("[progress.description]{task.description}"),
|
50 |
BarColumn(bar_width=None),
|
51 |
TextColumn("{task.completed:.0f}/{task.total:.0f}"),
|
52 |
TimeRemainingColumn(),
|
53 |
)
|
54 |
-
self.
|
|
|
|
|
|
|
55 |
|
56 |
self.use_wandb = cfg.use_wandb
|
57 |
if self.use_wandb:
|
@@ -60,35 +73,71 @@ class ProgressLogger:
|
|
60 |
project="YOLO", resume="allow", mode="online", dir=self.save_path, id=None, name=exp_name
|
61 |
)
|
62 |
|
|
|
|
|
|
|
|
|
63 |
def start_train(self, num_epochs: int):
|
64 |
-
self.task_epoch = self.
|
65 |
|
66 |
-
def start_one_epoch(
|
|
|
|
|
67 |
self.num_batches = num_batches
|
68 |
-
|
|
|
|
|
|
|
|
|
69 |
lr_values = [params["lr"] for params in optimizer.param_groups]
|
70 |
-
lr_names = ["bias", "norm", "conv"]
|
71 |
for lr_name, lr_value in zip(lr_names, lr_values):
|
72 |
-
self.wandb.log({
|
73 |
-
self.batch_task = self.
|
74 |
-
|
75 |
-
def one_batch(self,
|
76 |
-
|
77 |
-
|
78 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
79 |
if self.use_wandb:
|
80 |
-
|
81 |
-
|
82 |
|
83 |
-
|
84 |
-
|
85 |
-
loss_str += f" {loss_val:2.2f} |"
|
86 |
|
87 |
-
|
88 |
-
|
|
|
|
|
89 |
|
90 |
-
|
91 |
-
|
|
|
|
|
|
|
92 |
|
93 |
def finish_train(self):
|
94 |
self.wandb.finish()
|
@@ -115,7 +164,11 @@ def log_model_structure(model: List[YOLOLayer]):
|
|
115 |
layer_param = sum(x.numel() for x in layer.parameters()) # number parameters
|
116 |
in_channels, out_channels = getattr(layer, "in_c", None), getattr(layer, "out_c", None)
|
117 |
if in_channels and out_channels:
|
118 |
-
|
|
|
|
|
|
|
|
|
119 |
else:
|
120 |
channels = "-"
|
121 |
table.add_row(str(idx), layer.layer_type, layer.tags, f"{layer_param:,}", channels)
|
|
|
13 |
|
14 |
import os
|
15 |
import sys
|
16 |
+
from collections import deque
|
17 |
+
from typing import Any, Dict, List
|
18 |
|
19 |
import wandb
|
20 |
import wandb.errors.term
|
21 |
from loguru import logger
|
22 |
+
from omegaconf import ListConfig
|
23 |
+
from rich.console import Console, Group
|
24 |
+
from rich.progress import (
|
25 |
+
BarColumn,
|
26 |
+
Progress,
|
27 |
+
SpinnerColumn,
|
28 |
+
TextColumn,
|
29 |
+
TimeRemainingColumn,
|
30 |
+
)
|
31 |
from rich.table import Table
|
32 |
from torch import Tensor
|
33 |
from torch.optim import Optimizer
|
34 |
|
35 |
from yolo.config.config import Config, YOLOLayer
|
36 |
+
from yolo.utils.solver_utils import make_ap_table
|
37 |
|
38 |
|
39 |
def custom_logger(quite: bool = False):
|
|
|
47 |
)
|
48 |
|
49 |
|
50 |
+
class ProgressLogger(Progress):
|
51 |
+
def __init__(self, cfg: Config, exp_name: str, *args, **kwargs):
|
52 |
local_rank = int(os.getenv("LOCAL_RANK", "0"))
|
53 |
self.quite_mode = local_rank or getattr(cfg, "quite", False)
|
54 |
custom_logger(self.quite_mode)
|
55 |
self.save_path = validate_log_directory(cfg, exp_name=cfg.name)
|
56 |
|
57 |
+
progress_bar = (
|
58 |
+
SpinnerColumn(),
|
59 |
TextColumn("[progress.description]{task.description}"),
|
60 |
BarColumn(bar_width=None),
|
61 |
TextColumn("{task.completed:.0f}/{task.total:.0f}"),
|
62 |
TimeRemainingColumn(),
|
63 |
)
|
64 |
+
self.ap_table = Table()
|
65 |
+
# TODO: load maxlen by config files
|
66 |
+
self.ap_past_list = deque(maxlen=5)
|
67 |
+
super().__init__(*args, *progress_bar, **kwargs)
|
68 |
|
69 |
self.use_wandb = cfg.use_wandb
|
70 |
if self.use_wandb:
|
|
|
73 |
project="YOLO", resume="allow", mode="online", dir=self.save_path, id=None, name=exp_name
|
74 |
)
|
75 |
|
76 |
+
def get_renderable(self):
|
77 |
+
renderable = Group(*self.get_renderables(), self.ap_table)
|
78 |
+
return renderable
|
79 |
+
|
80 |
def start_train(self, num_epochs: int):
|
81 |
+
self.task_epoch = self.add_task(f"[cyan]Start Training {num_epochs} epochs", total=num_epochs)
|
82 |
|
83 |
+
def start_one_epoch(
|
84 |
+
self, num_batches: int, task: str = "Train", optimizer: Optimizer = None, epoch_idx: int = None
|
85 |
+
):
|
86 |
self.num_batches = num_batches
|
87 |
+
self.task = task
|
88 |
+
if hasattr(self, "task_epoch"):
|
89 |
+
self.update(self.task_epoch, description=f"[cyan] Preparing Data")
|
90 |
+
|
91 |
+
if self.use_wandb and optimizer is not None:
|
92 |
lr_values = [params["lr"] for params in optimizer.param_groups]
|
93 |
+
lr_names = ["Learning Rate/bias", "Learning Rate/norm", "Learning Rate/conv"]
|
94 |
for lr_name, lr_value in zip(lr_names, lr_values):
|
95 |
+
self.wandb.log({lr_name: lr_value}, step=epoch_idx)
|
96 |
+
self.batch_task = self.add_task(f"[green] Phase: {task}", total=num_batches)
|
97 |
+
|
98 |
+
def one_batch(self, batch_info: Dict[str, Tensor] = None):
|
99 |
+
epoch_descript = "[cyan]" + self.task + "[white] |"
|
100 |
+
batch_descript = "|"
|
101 |
+
if self.task == "Train":
|
102 |
+
self.update(self.task_epoch, advance=1 / self.num_batches)
|
103 |
+
elif self.task == "Validate":
|
104 |
+
batch_info = {
|
105 |
+
"mAP.5": batch_info.mean(dim=0)[0],
|
106 |
+
"mAP.5:.95": batch_info.mean(dim=0)[1],
|
107 |
+
}
|
108 |
+
for info_name, info_val in batch_info.items():
|
109 |
+
epoch_descript += f"{info_name: ^9}|"
|
110 |
+
batch_descript += f" {info_val:2.2f} |"
|
111 |
+
self.update(self.batch_task, advance=1, description=f"[green]{self.task} [white]{batch_descript}")
|
112 |
+
if hasattr(self, "task_epoch"):
|
113 |
+
self.update(self.task_epoch, description=epoch_descript)
|
114 |
+
|
115 |
+
def finish_one_epoch(self, batch_info: Dict[str, Any] = None, epoch_idx: int = -1):
|
116 |
+
if self.task == "Train":
|
117 |
+
for loss_name in batch_info.keys():
|
118 |
+
batch_info["Loss/" + loss_name] = batch_info.pop(loss_name)
|
119 |
+
elif self.task == "Validate":
|
120 |
+
batch_info = {
|
121 |
+
"Metrics/mAP.5": batch_info.mean(dim=0)[0],
|
122 |
+
"Metrics/mAP.5:.95": batch_info.mean(dim=0)[1],
|
123 |
+
}
|
124 |
if self.use_wandb:
|
125 |
+
self.wandb.log(batch_info, step=epoch_idx)
|
126 |
+
self.remove_task(self.batch_task)
|
127 |
|
128 |
+
def start_pycocotools(self):
|
129 |
+
self.batch_task = self.add_task("[green] run pycocotools", total=1)
|
|
|
130 |
|
131 |
+
def finish_pycocotools(self, result, epoch_idx=-1):
|
132 |
+
ap_table, ap_main = make_ap_table(result, self.ap_past_list, epoch_idx)
|
133 |
+
self.ap_past_list.append((epoch_idx, ap_main))
|
134 |
+
self.ap_table = ap_table
|
135 |
|
136 |
+
if self.use_wandb:
|
137 |
+
self.wandb.log({"PyCOCO/AP @ .5:.95": ap_main[1], "PyCOCO/AP @ .5": ap_main[3]})
|
138 |
+
self.update(self.batch_task, advance=1)
|
139 |
+
self.refresh()
|
140 |
+
self.remove_task(self.batch_task)
|
141 |
|
142 |
def finish_train(self):
|
143 |
self.wandb.finish()
|
|
|
164 |
layer_param = sum(x.numel() for x in layer.parameters()) # number parameters
|
165 |
in_channels, out_channels = getattr(layer, "in_c", None), getattr(layer, "out_c", None)
|
166 |
if in_channels and out_channels:
|
167 |
+
if isinstance(in_channels, (list, ListConfig)):
|
168 |
+
in_channels = "M"
|
169 |
+
if isinstance(out_channels, (list, ListConfig)):
|
170 |
+
out_channels = "M"
|
171 |
+
channels = f"{str(in_channels): >4} -> {str(out_channels): >4}"
|
172 |
else:
|
173 |
channels = "-"
|
174 |
table.add_row(str(idx), layer.layer_type, layer.tags, f"{layer_param:,}", channels)
|
yolo/utils/model_utils.py
CHANGED
@@ -106,7 +106,7 @@ class PostProccess:
|
|
106 |
self.vec2box = vec2box
|
107 |
self.nms = nms_cfg
|
108 |
|
109 |
-
def __call__(self, predict, rev_tensor: Optional[Tensor]):
|
110 |
pred_class, _, pred_bbox = self.vec2box(predict["Main"])
|
111 |
if rev_tensor is not None:
|
112 |
pred_bbox = (pred_bbox - rev_tensor[:, None, 1:]) / rev_tensor[:, 0:1, None]
|
@@ -114,13 +114,15 @@ class PostProccess:
|
|
114 |
return pred_bbox
|
115 |
|
116 |
|
117 |
-
def predicts_to_json(img_paths, predicts):
|
118 |
"""
|
119 |
TODO: function document
|
120 |
turn a batch of imagepath and predicts(n x 6 for each image) to a List of diction(Detection output)
|
121 |
"""
|
122 |
batch_json = []
|
123 |
-
for img_path, bboxes in zip(img_paths, predicts):
|
|
|
|
|
124 |
bboxes[:, 1:5] = transform_bbox(bboxes[:, 1:5], "xyxy -> xywh")
|
125 |
for cls, *pos, conf in bboxes:
|
126 |
bbox = {
|
|
|
106 |
self.vec2box = vec2box
|
107 |
self.nms = nms_cfg
|
108 |
|
109 |
+
def __call__(self, predict, rev_tensor: Optional[Tensor] = None):
|
110 |
pred_class, _, pred_bbox = self.vec2box(predict["Main"])
|
111 |
if rev_tensor is not None:
|
112 |
pred_bbox = (pred_bbox - rev_tensor[:, None, 1:]) / rev_tensor[:, 0:1, None]
|
|
|
114 |
return pred_bbox
|
115 |
|
116 |
|
117 |
+
def predicts_to_json(img_paths, predicts, rev_tensor):
|
118 |
"""
|
119 |
TODO: function document
|
120 |
turn a batch of imagepath and predicts(n x 6 for each image) to a List of diction(Detection output)
|
121 |
"""
|
122 |
batch_json = []
|
123 |
+
for img_path, bboxes, box_reverse in zip(img_paths, predicts, rev_tensor):
|
124 |
+
scale, shift = box_reverse.split([1, 4])
|
125 |
+
bboxes[:, 1:5] = (bboxes[:, 1:5] - shift[None]) / scale[None]
|
126 |
bboxes[:, 1:5] = transform_bbox(bboxes[:, 1:5], "xyxy -> xywh")
|
127 |
for cls, *pos, conf in bboxes:
|
128 |
bbox = {
|
yolo/utils/solver_utils.py
ADDED
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import sys
|
3 |
+
|
4 |
+
from pycocotools.coco import COCO
|
5 |
+
from pycocotools.cocoeval import COCOeval
|
6 |
+
from rich.table import Table
|
7 |
+
|
8 |
+
|
9 |
+
def calculate_ap(coco_gt: COCO, pd_path):
|
10 |
+
sys.stdout = open(os.devnull, "w")
|
11 |
+
coco_dt = coco_gt.loadRes(pd_path)
|
12 |
+
coco_eval = COCOeval(coco_gt, coco_dt, "bbox")
|
13 |
+
coco_eval.evaluate()
|
14 |
+
coco_eval.accumulate()
|
15 |
+
coco_eval.summarize()
|
16 |
+
sys.stdout = sys.__stdout__
|
17 |
+
return coco_eval.stats
|
18 |
+
|
19 |
+
|
20 |
+
def make_ap_table(score, past_result=[], epoch=-1):
|
21 |
+
ap_table = Table()
|
22 |
+
ap_table.add_column("Epoch", justify="center", style="white", width=5)
|
23 |
+
ap_table.add_column("Avg. Precision", justify="left", style="cyan")
|
24 |
+
ap_table.add_column("", justify="right", style="green", width=5)
|
25 |
+
ap_table.add_column("Avg. Recall", justify="left", style="cyan")
|
26 |
+
ap_table.add_column("", justify="right", style="green", width=5)
|
27 |
+
|
28 |
+
for eps, (ap_name1, ap_value1, ap_name2, ap_value2) in past_result:
|
29 |
+
ap_table.add_row(f"{eps: 3d}", ap_name1, f"{ap_value1:.2f}", ap_name2, f"{ap_value2:.2f}")
|
30 |
+
if past_result:
|
31 |
+
ap_table.add_row()
|
32 |
+
|
33 |
+
this_ap = ("AP @ .5:.95", score[0], "AP @ .5", score[1])
|
34 |
+
metrics = [
|
35 |
+
("AP @ .5:.95", score[0], "AR maxDets 1", score[6]),
|
36 |
+
("AP @ .5", score[1], "AR maxDets 10", score[7]),
|
37 |
+
("AP @ .75", score[2], "AR maxDets 100", score[8]),
|
38 |
+
("AP (small)", score[3], "AR (small)", score[9]),
|
39 |
+
("AP (medium)", score[4], "AR (medium)", score[10]),
|
40 |
+
("AP (large)", score[5], "AR (large)", score[11]),
|
41 |
+
]
|
42 |
+
|
43 |
+
for ap_name, ap_value, ar_name, ar_value in metrics:
|
44 |
+
ap_table.add_row(f"{epoch: 3d}", ap_name, f"{ap_value:.2f}", ar_name, f"{ar_value:.2f}")
|
45 |
+
|
46 |
+
return ap_table, this_ap
|