Update Actions with Lychee and GitHub Token (#12592)

* Add Ultralytics Actions

* Auto-format by Ultralytics actions

* Update format.yml

Signed-off-by: Glenn Jocher <glenn.jocher@ultralytics.com>

---------

Signed-off-by: Glenn Jocher <glenn.jocher@ultralytics.com>
Co-authored-by: UltralyticsAssistant <web@ultralytics.com>
Co-authored-by: Glenn Jocher <glenn.jocher@ultralytics.com>
This commit is contained in:
authored and GitHub committed 2024-01-08 01:29:14 +01:00
1 parent f56355d321
commit ad97dc6a6d
47 files changed
+4217 -3462

No files matched your search

+25
View File
@@ -0,0 +1,25 @@
# Ultralytics 🚀 - AGPL-3.0 license
# Ultralytics Actions https://github.com/ultralytics/actions
# This workflow automatically formats code and documentation in PRs to official Ultralytics standards
name: Ultralytics Actions
on:
push:
branches: [main,master]
pull_request_target:
branches: [main,master]
jobs:
format:
runs-on: ubuntu-latest
steps:
- name: Run Ultralytics Formatting
uses: ultralytics/actions@main
with:
token: ${{ secrets.GITHUB_TOKEN }} # automatically generated
python: true
docstrings: true
markdown: true
spelling: true
links: true
+58 -58
View File
@@ -1,6 +1,6 @@
# YOLOv5 🚀 by Ultralytics, AGPL-3.0 license # YOLOv5 🚀 by Ultralytics, AGPL-3.0 license
""" """
Run YOLOv5 benchmarks on all supported export formats Run YOLOv5 benchmarks on all supported export formats.
Format | `export.py --include` | Model Format | `export.py --include` | Model
--- | --- | --- --- | --- | ---
@@ -50,115 +50,115 @@ from val import run as val_det
def run( def run(
weights=ROOT / 'yolov5s.pt', # weights path weights=ROOT / "yolov5s.pt", # weights path
imgsz=640, # inference size (pixels) imgsz=640, # inference size (pixels)
batch_size=1, # batch size batch_size=1, # batch size
data=ROOT / 'data/coco128.yaml', # dataset.yaml path data=ROOT / "data/coco128.yaml", # dataset.yaml path
device='', # cuda device, i.e. 0 or 0,1,2,3 or cpu device="", # cuda device, i.e. 0 or 0,1,2,3 or cpu
half=False, # use FP16 half-precision inference half=False, # use FP16 half-precision inference
test=False, # test exports only test=False, # test exports only
pt_only=False, # test PyTorch only pt_only=False, # test PyTorch only
hard_fail=False, # throw error on benchmark failure hard_fail=False, # throw error on benchmark failure
): ):
y, t = [], time.time() y, t = [], time.time()
device = select_device(device) device = select_device(device)
model_type = type(attempt_load(weights, fuse=False)) # DetectionModel, SegmentationModel, etc. model_type = type(attempt_load(weights, fuse=False)) # DetectionModel, SegmentationModel, etc.
for i, (name, f, suffix, cpu, gpu) in export.export_formats().iterrows(): # index, (name, file, suffix, CPU, GPU) for i, (name, f, suffix, cpu, gpu) in export.export_formats().iterrows(): # index, (name, file, suffix, CPU, GPU)
try: try:
assert i not in (9, 10), 'inference not supported' # Edge TPU and TF.js are unsupported assert i not in (9, 10), "inference not supported" # Edge TPU and TF.js are unsupported
assert i != 5 or platform.system() == 'Darwin', 'inference only supported on macOS>=10.13' # CoreML assert i != 5 or platform.system() == "Darwin", "inference only supported on macOS>=10.13" # CoreML
if 'cpu' in device.type: if "cpu" in device.type:
assert cpu, 'inference not supported on CPU' assert cpu, "inference not supported on CPU"
if 'cuda' in device.type: if "cuda" in device.type:
assert gpu, 'inference not supported on GPU' assert gpu, "inference not supported on GPU"
# Export # Export
if f == '-': if f == "-":
w = weights # PyTorch format w = weights # PyTorch format
else: else:
w = export.run(weights=weights, w = export.run(
imgsz=[imgsz], weights=weights, imgsz=[imgsz], include=[f], batch_size=batch_size, device=device, half=half
include=[f], )[-1] # all others
batch_size=batch_size, assert suffix in str(w), "export failed"
device=device,
half=half)[-1] # all others
assert suffix in str(w), 'export failed'
# Validate # Validate
if model_type == SegmentationModel: if model_type == SegmentationModel:
result = val_seg(data, w, batch_size, imgsz, plots=False, device=device, task='speed', half=half) result = val_seg(data, w, batch_size, imgsz, plots=False, device=device, task="speed", half=half)
metric = result[0][7] # (box(p, r, map50, map), mask(p, r, map50, map), *loss(box, obj, cls)) metric = result[0][7] # (box(p, r, map50, map), mask(p, r, map50, map), *loss(box, obj, cls))
else: # DetectionModel: else: # DetectionModel:
result = val_det(data, w, batch_size, imgsz, plots=False, device=device, task='speed', half=half) result = val_det(data, w, batch_size, imgsz, plots=False, device=device, task="speed", half=half)
metric = result[0][3] # (p, r, map50, map, *loss(box, obj, cls)) metric = result[0][3] # (p, r, map50, map, *loss(box, obj, cls))
speed = result[2][1] # times (preprocess, inference, postprocess) speed = result[2][1] # times (preprocess, inference, postprocess)
y.append([name, round(file_size(w), 1), round(metric, 4), round(speed, 2)]) # MB, mAP, t_inference y.append([name, round(file_size(w), 1), round(metric, 4), round(speed, 2)]) # MB, mAP, t_inference
except Exception as e: except Exception as e:
if hard_fail: if hard_fail:
assert type(e) is AssertionError, f'Benchmark --hard-fail for {name}: {e}' assert type(e) is AssertionError, f"Benchmark --hard-fail for {name}: {e}"
LOGGER.warning(f'WARNING ⚠️ Benchmark failure for {name}: {e}') LOGGER.warning(f"WARNING ⚠️ Benchmark failure for {name}: {e}")
y.append([name, None, None, None]) # mAP, t_inference y.append([name, None, None, None]) # mAP, t_inference
if pt_only and i == 0: if pt_only and i == 0:
break # break after PyTorch break # break after PyTorch
# Print results # Print results
LOGGER.info('\n') LOGGER.info("\n")
parse_opt() parse_opt()
notebook_init() # print system info notebook_init() # print system info
c = ['Format', 'Size (MB)', 'mAP50-95', 'Inference time (ms)'] if map else ['Format', 'Export', '', ''] c = ["Format", "Size (MB)", "mAP50-95", "Inference time (ms)"] if map else ["Format", "Export", "", ""]
py = pd.DataFrame(y, columns=c) py = pd.DataFrame(y, columns=c)
LOGGER.info(f'\nBenchmarks complete ({time.time() - t:.2f}s)') LOGGER.info(f"\nBenchmarks complete ({time.time() - t:.2f}s)")
LOGGER.info(str(py if map else py.iloc[:, :2])) LOGGER.info(str(py if map else py.iloc[:, :2]))
if hard_fail and isinstance(hard_fail, str): if hard_fail and isinstance(hard_fail, str):
metrics = py['mAP50-95'].array # values to compare to floor metrics = py["mAP50-95"].array # values to compare to floor
floor = eval(hard_fail) # minimum metric floor to pass, i.e. = 0.29 mAP for YOLOv5n floor = eval(hard_fail) # minimum metric floor to pass, i.e. = 0.29 mAP for YOLOv5n
assert all(x > floor for x in metrics if pd.notna(x)), f'HARD FAIL: mAP50-95 < floor {floor}' assert all(x > floor for x in metrics if pd.notna(x)), f"HARD FAIL: mAP50-95 < floor {floor}"
return py return py
def test( def test(
weights=ROOT / 'yolov5s.pt', # weights path weights=ROOT / "yolov5s.pt", # weights path
imgsz=640, # inference size (pixels) imgsz=640, # inference size (pixels)
batch_size=1, # batch size batch_size=1, # batch size
data=ROOT / 'data/coco128.yaml', # dataset.yaml path data=ROOT / "data/coco128.yaml", # dataset.yaml path
device='', # cuda device, i.e. 0 or 0,1,2,3 or cpu device="", # cuda device, i.e. 0 or 0,1,2,3 or cpu
half=False, # use FP16 half-precision inference half=False, # use FP16 half-precision inference
test=False, # test exports only test=False, # test exports only
pt_only=False, # test PyTorch only pt_only=False, # test PyTorch only
hard_fail=False, # throw error on benchmark failure hard_fail=False, # throw error on benchmark failure
): ):
y, t = [], time.time() y, t = [], time.time()
device = select_device(device) device = select_device(device)
for i, (name, f, suffix, gpu) in export.export_formats().iterrows(): # index, (name, file, suffix, gpu-capable) for i, (name, f, suffix, gpu) in export.export_formats().iterrows(): # index, (name, file, suffix, gpu-capable)
try: try:
w = weights if f == '-' else \ w = (
export.run(weights=weights, imgsz=[imgsz], include=[f], device=device, half=half)[-1] # weights weights
assert suffix in str(w), 'export failed' if f == "-"
else export.run(weights=weights, imgsz=[imgsz], include=[f], device=device, half=half)[-1]
) # weights
assert suffix in str(w), "export failed"
y.append([name, True]) y.append([name, True])
except Exception: except Exception:
y.append([name, False]) # mAP, t_inference y.append([name, False]) # mAP, t_inference
# Print results # Print results
LOGGER.info('\n') LOGGER.info("\n")
parse_opt() parse_opt()
notebook_init() # print system info notebook_init() # print system info
py = pd.DataFrame(y, columns=['Format', 'Export']) py = pd.DataFrame(y, columns=["Format", "Export"])
LOGGER.info(f'\nExports complete ({time.time() - t:.2f}s)') LOGGER.info(f"\nExports complete ({time.time() - t:.2f}s)")
LOGGER.info(str(py)) LOGGER.info(str(py))
return py return py
def parse_opt(): def parse_opt():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('--weights', type=str, default=ROOT / 'yolov5s.pt', help='weights path') parser.add_argument("--weights", type=str, default=ROOT / "yolov5s.pt", help="weights path")
parser.add_argument('--imgsz', '--img', '--img-size', type=int, default=640, help='inference size (pixels)') parser.add_argument("--imgsz", "--img", "--img-size", type=int, default=640, help="inference size (pixels)")
parser.add_argument('--batch-size', type=int, default=1, help='batch size') parser.add_argument("--batch-size", type=int, default=1, help="batch size")
parser.add_argument('--data', type=str, default=ROOT / 'data/coco128.yaml', help='dataset.yaml path') parser.add_argument("--data", type=str, default=ROOT / "data/coco128.yaml", help="dataset.yaml path")
parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu') parser.add_argument("--device", default="", help="cuda device, i.e. 0 or 0,1,2,3 or cpu")
parser.add_argument('--half', action='store_true', help='use FP16 half-precision inference') parser.add_argument("--half", action="store_true", help="use FP16 half-precision inference")
parser.add_argument('--test', action='store_true', help='test exports only') parser.add_argument("--test", action="store_true", help="test exports only")
parser.add_argument('--pt-only', action='store_true', help='test PyTorch only') parser.add_argument("--pt-only", action="store_true", help="test PyTorch only")
parser.add_argument('--hard-fail', nargs='?', const=True, default=False, help='Exception on error or < min metric') parser.add_argument("--hard-fail", nargs="?", const=True, default=False, help="Exception on error or < min metric")
opt = parser.parse_args() opt = parser.parse_args()
opt.data = check_yaml(opt.data) # check YAML opt.data = check_yaml(opt.data) # check YAML
print_args(vars(opt)) print_args(vars(opt))
@@ -169,6 +169,6 @@ def main(opt):
test(**vars(opt)) if opt.test else run(**vars(opt)) test(**vars(opt)) if opt.test else run(**vars(opt))
if __name__ == '__main__': if __name__ == "__main__":
opt = parse_opt() opt = parse_opt()
main(opt) main(opt)
+69 -58
View File
@@ -48,43 +48,54 @@ from ultralytics.utils.plotting import Annotator
from models.common import DetectMultiBackend from models.common import DetectMultiBackend
from utils.augmentations import classify_transforms from utils.augmentations import classify_transforms
from utils.dataloaders import IMG_FORMATS, VID_FORMATS, LoadImages, LoadScreenshots, LoadStreams from utils.dataloaders import IMG_FORMATS, VID_FORMATS, LoadImages, LoadScreenshots, LoadStreams
from utils.general import (LOGGER, Profile, check_file, check_img_size, check_imshow, check_requirements, colorstr, cv2, from utils.general import (
increment_path, print_args, strip_optimizer) LOGGER,
Profile,
check_file,
check_img_size,
check_imshow,
check_requirements,
colorstr,
cv2,
increment_path,
print_args,
strip_optimizer,
)
from utils.torch_utils import select_device, smart_inference_mode from utils.torch_utils import select_device, smart_inference_mode
@smart_inference_mode() @smart_inference_mode()
def run( def run(
weights=ROOT / 'yolov5s-cls.pt', # model.pt path(s) weights=ROOT / "yolov5s-cls.pt", # model.pt path(s)
source=ROOT / 'data/images', # file/dir/URL/glob/screen/0(webcam) source=ROOT / "data/images", # file/dir/URL/glob/screen/0(webcam)
data=ROOT / 'data/coco128.yaml', # dataset.yaml path data=ROOT / "data/coco128.yaml", # dataset.yaml path
imgsz=(224, 224), # inference size (height, width) imgsz=(224, 224), # inference size (height, width)
device='', # cuda device, i.e. 0 or 0,1,2,3 or cpu device="", # cuda device, i.e. 0 or 0,1,2,3 or cpu
view_img=False, # show results view_img=False, # show results
save_txt=False, # save results to *.txt save_txt=False, # save results to *.txt
nosave=False, # do not save images/videos nosave=False, # do not save images/videos
augment=False, # augmented inference augment=False, # augmented inference
visualize=False, # visualize features visualize=False, # visualize features
update=False, # update all models update=False, # update all models
project=ROOT / 'runs/predict-cls', # save results to project/name project=ROOT / "runs/predict-cls", # save results to project/name
name='exp', # save results to project/name name="exp", # save results to project/name
exist_ok=False, # existing project/name ok, do not increment exist_ok=False, # existing project/name ok, do not increment
half=False, # use FP16 half-precision inference half=False, # use FP16 half-precision inference
dnn=False, # use OpenCV DNN for ONNX inference dnn=False, # use OpenCV DNN for ONNX inference
vid_stride=1, # video frame-rate stride vid_stride=1, # video frame-rate stride
): ):
source = str(source) source = str(source)
save_img = not nosave and not source.endswith('.txt') # save inference images save_img = not nosave and not source.endswith(".txt") # save inference images
is_file = Path(source).suffix[1:] in (IMG_FORMATS + VID_FORMATS) is_file = Path(source).suffix[1:] in (IMG_FORMATS + VID_FORMATS)
is_url = source.lower().startswith(('rtsp://', 'rtmp://', 'http://', 'https://')) is_url = source.lower().startswith(("rtsp://", "rtmp://", "http://", "https://"))
webcam = source.isnumeric() or source.endswith('.streams') or (is_url and not is_file) webcam = source.isnumeric() or source.endswith(".streams") or (is_url and not is_file)
screenshot = source.lower().startswith('screen') screenshot = source.lower().startswith("screen")
if is_url and is_file: if is_url and is_file:
source = check_file(source) # download source = check_file(source) # download
# Directories # Directories
save_dir = increment_path(Path(project) / name, exist_ok=exist_ok) # increment run save_dir = increment_path(Path(project) / name, exist_ok=exist_ok) # increment run
(save_dir / 'labels' if save_txt else save_dir).mkdir(parents=True, exist_ok=True) # make dir (save_dir / "labels" if save_txt else save_dir).mkdir(parents=True, exist_ok=True) # make dir
# Load model # Load model
device = select_device(device) device = select_device(device)
@@ -127,15 +138,15 @@ def run(
seen += 1 seen += 1
if webcam: # batch_size >= 1 if webcam: # batch_size >= 1
p, im0, frame = path[i], im0s[i].copy(), dataset.count p, im0, frame = path[i], im0s[i].copy(), dataset.count
s += f'{i}: ' s += f"{i}: "
else: else:
p, im0, frame = path, im0s.copy(), getattr(dataset, 'frame', 0) p, im0, frame = path, im0s.copy(), getattr(dataset, "frame", 0)
p = Path(p) # to Path p = Path(p) # to Path
save_path = str(save_dir / p.name) # im.jpg save_path = str(save_dir / p.name) # im.jpg
txt_path = str(save_dir / 'labels' / p.stem) + ('' if dataset.mode == 'image' else f'_{frame}') # im.txt txt_path = str(save_dir / "labels" / p.stem) + ("" if dataset.mode == "image" else f"_{frame}") # im.txt
s += '%gx%g ' % im.shape[2:] # print string s += "%gx%g " % im.shape[2:] # print string
annotator = Annotator(im0, example=str(names), pil=True) annotator = Annotator(im0, example=str(names), pil=True)
# Print results # Print results
@@ -143,17 +154,17 @@ def run(
s += f"{', '.join(f'{names[j]} {prob[j]:.2f}' for j in top5i)}, " s += f"{', '.join(f'{names[j]} {prob[j]:.2f}' for j in top5i)}, "
# Write results # Write results
text = '\n'.join(f'{prob[j]:.2f} {names[j]}' for j in top5i) text = "\n".join(f"{prob[j]:.2f} {names[j]}" for j in top5i)
if save_img or view_img: # Add bbox to image if save_img or view_img: # Add bbox to image
annotator.text([32, 32], text, txt_color=(255, 255, 255)) annotator.text([32, 32], text, txt_color=(255, 255, 255))
if save_txt: # Write to file if save_txt: # Write to file
with open(f'{txt_path}.txt', 'a') as f: with open(f"{txt_path}.txt", "a") as f:
f.write(text + '\n') f.write(text + "\n")
# Stream results # Stream results
im0 = annotator.result() im0 = annotator.result()
if view_img: if view_img:
if platform.system() == 'Linux' and p not in windows: if platform.system() == "Linux" and p not in windows:
windows.append(p) windows.append(p)
cv2.namedWindow(str(p), cv2.WINDOW_NORMAL | cv2.WINDOW_KEEPRATIO) # allow window resize (Linux) cv2.namedWindow(str(p), cv2.WINDOW_NORMAL | cv2.WINDOW_KEEPRATIO) # allow window resize (Linux)
cv2.resizeWindow(str(p), im0.shape[1], im0.shape[0]) cv2.resizeWindow(str(p), im0.shape[1], im0.shape[0])
@@ -162,7 +173,7 @@ def run(
# Save results (image with detections) # Save results (image with detections)
if save_img: if save_img:
if dataset.mode == 'image': if dataset.mode == "image":
cv2.imwrite(save_path, im0) cv2.imwrite(save_path, im0)
else: # 'video' or 'stream' else: # 'video' or 'stream'
if vid_path[i] != save_path: # new video if vid_path[i] != save_path: # new video
@@ -175,18 +186,18 @@ def run(
h = int(vid_cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) h = int(vid_cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
else: # stream else: # stream
fps, w, h = 30, im0.shape[1], im0.shape[0] fps, w, h = 30, im0.shape[1], im0.shape[0]
save_path = str(Path(save_path).with_suffix('.mp4')) # force *.mp4 suffix on results videos save_path = str(Path(save_path).with_suffix(".mp4")) # force *.mp4 suffix on results videos
vid_writer[i] = cv2.VideoWriter(save_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (w, h)) vid_writer[i] = cv2.VideoWriter(save_path, cv2.VideoWriter_fourcc(*"mp4v"), fps, (w, h))
vid_writer[i].write(im0) vid_writer[i].write(im0)
# Print time (inference-only) # Print time (inference-only)
LOGGER.info(f'{s}{dt[1].dt * 1E3:.1f}ms') LOGGER.info(f"{s}{dt[1].dt * 1E3:.1f}ms")
# Print results # Print results
t = tuple(x.t / seen * 1E3 for x in dt) # speeds per image t = tuple(x.t / seen * 1e3 for x in dt) # speeds per image
LOGGER.info(f'Speed: %.1fms pre-process, %.1fms inference, %.1fms NMS per image at shape {(1, 3, *imgsz)}' % t) LOGGER.info(f"Speed: %.1fms pre-process, %.1fms inference, %.1fms NMS per image at shape {(1, 3, *imgsz)}" % t)
if save_txt or save_img: if save_txt or save_img:
s = f"\n{len(list(save_dir.glob('labels/*.txt')))} labels saved to {save_dir / 'labels'}" if save_txt else '' s = f"\n{len(list(save_dir.glob('labels/*.txt')))} labels saved to {save_dir / 'labels'}" if save_txt else ""
LOGGER.info(f"Results saved to {colorstr('bold', save_dir)}{s}") LOGGER.info(f"Results saved to {colorstr('bold', save_dir)}{s}")
if update: if update:
strip_optimizer(weights[0]) # update model (to fix SourceChangeWarning) strip_optimizer(weights[0]) # update model (to fix SourceChangeWarning)
@@ -194,23 +205,23 @@ def run(
def parse_opt(): def parse_opt():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('--weights', nargs='+', type=str, default=ROOT / 'yolov5s-cls.pt', help='model path(s)') parser.add_argument("--weights", nargs="+", type=str, default=ROOT / "yolov5s-cls.pt", help="model path(s)")
parser.add_argument('--source', type=str, default=ROOT / 'data/images', help='file/dir/URL/glob/screen/0(webcam)') parser.add_argument("--source", type=str, default=ROOT / "data/images", help="file/dir/URL/glob/screen/0(webcam)")
parser.add_argument('--data', type=str, default=ROOT / 'data/coco128.yaml', help='(optional) dataset.yaml path') parser.add_argument("--data", type=str, default=ROOT / "data/coco128.yaml", help="(optional) dataset.yaml path")
parser.add_argument('--imgsz', '--img', '--img-size', nargs='+', type=int, default=[224], help='inference size h,w') parser.add_argument("--imgsz", "--img", "--img-size", nargs="+", type=int, default=[224], help="inference size h,w")
parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu') parser.add_argument("--device", default="", help="cuda device, i.e. 0 or 0,1,2,3 or cpu")
parser.add_argument('--view-img', action='store_true', help='show results') parser.add_argument("--view-img", action="store_true", help="show results")
parser.add_argument('--save-txt', action='store_true', help='save results to *.txt') parser.add_argument("--save-txt", action="store_true", help="save results to *.txt")
parser.add_argument('--nosave', action='store_true', help='do not save images/videos') parser.add_argument("--nosave", action="store_true", help="do not save images/videos")
parser.add_argument('--augment', action='store_true', help='augmented inference') parser.add_argument("--augment", action="store_true", help="augmented inference")
parser.add_argument('--visualize', action='store_true', help='visualize features') parser.add_argument("--visualize", action="store_true", help="visualize features")
parser.add_argument('--update', action='store_true', help='update all models') parser.add_argument("--update", action="store_true", help="update all models")
parser.add_argument('--project', default=ROOT / 'runs/predict-cls', help='save results to project/name') parser.add_argument("--project", default=ROOT / "runs/predict-cls", help="save results to project/name")
parser.add_argument('--name', default='exp', help='save results to project/name') parser.add_argument("--name", default="exp", help="save results to project/name")
parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment') parser.add_argument("--exist-ok", action="store_true", help="existing project/name ok, do not increment")
parser.add_argument('--half', action='store_true', help='use FP16 half-precision inference') parser.add_argument("--half", action="store_true", help="use FP16 half-precision inference")
parser.add_argument('--dnn', action='store_true', help='use OpenCV DNN for ONNX inference') parser.add_argument("--dnn", action="store_true", help="use OpenCV DNN for ONNX inference")
parser.add_argument('--vid-stride', type=int, default=1, help='video frame-rate stride') parser.add_argument("--vid-stride", type=int, default=1, help="video frame-rate stride")
opt = parser.parse_args() opt = parser.parse_args()
opt.imgsz *= 2 if len(opt.imgsz) == 1 else 1 # expand opt.imgsz *= 2 if len(opt.imgsz) == 1 else 1 # expand
print_args(vars(opt)) print_args(vars(opt))
@@ -218,10 +229,10 @@ def parse_opt():
def main(opt): def main(opt):
check_requirements(ROOT / 'requirements.txt', exclude=('tensorboard', 'thop')) check_requirements(ROOT / "requirements.txt", exclude=("tensorboard", "thop"))
run(**vars(opt)) run(**vars(opt))
if __name__ == '__main__': if __name__ == "__main__":
opt = parse_opt() opt = parse_opt()
main(opt) main(opt)
+144 -107
View File
@@ -1,6 +1,6 @@
# YOLOv5 🚀 by Ultralytics, AGPL-3.0 license # YOLOv5 🚀 by Ultralytics, AGPL-3.0 license
""" """
Train a YOLOv5 classifier model on a classification dataset Train a YOLOv5 classifier model on a classification dataset.
Usage - Single-GPU training: Usage - Single-GPU training:
$ python classify/train.py --model yolov5s-cls.pt --data imagenette160 --epochs 5 --img 224 $ python classify/train.py --model yolov5s-cls.pt --data imagenette160 --epochs 5 --img 224
@@ -40,33 +40,61 @@ from classify import val as validate
from models.experimental import attempt_load from models.experimental import attempt_load
from models.yolo import ClassificationModel, DetectionModel from models.yolo import ClassificationModel, DetectionModel
from utils.dataloaders import create_classification_dataloader from utils.dataloaders import create_classification_dataloader
from utils.general import (DATASETS_DIR, LOGGER, TQDM_BAR_FORMAT, WorkingDirectory, check_git_info, check_git_status, from utils.general import (
check_requirements, colorstr, download, increment_path, init_seeds, print_args, yaml_save) DATASETS_DIR,
LOGGER,
TQDM_BAR_FORMAT,
WorkingDirectory,
check_git_info,
check_git_status,
check_requirements,
colorstr,
download,
increment_path,
init_seeds,
print_args,
yaml_save,
)
from utils.loggers import GenericLogger from utils.loggers import GenericLogger
from utils.plots import imshow_cls from utils.plots import imshow_cls
from utils.torch_utils import (ModelEMA, de_parallel, model_info, reshape_classifier_output, select_device, smart_DDP, from utils.torch_utils import (
smart_optimizer, smartCrossEntropyLoss, torch_distributed_zero_first) ModelEMA,
de_parallel,
model_info,
reshape_classifier_output,
select_device,
smart_DDP,
smart_optimizer,
smartCrossEntropyLoss,
torch_distributed_zero_first,
)
LOCAL_RANK = int(os.getenv('LOCAL_RANK', -1)) # https://pytorch.org/docs/stable/elastic/run.html LOCAL_RANK = int(os.getenv("LOCAL_RANK", -1)) # https://pytorch.org/docs/stable/elastic/run.html
RANK = int(os.getenv('RANK', -1)) RANK = int(os.getenv("RANK", -1))
WORLD_SIZE = int(os.getenv('WORLD_SIZE', 1)) WORLD_SIZE = int(os.getenv("WORLD_SIZE", 1))
GIT_INFO = check_git_info() GIT_INFO = check_git_info()
def train(opt, device): def train(opt, device):
init_seeds(opt.seed + 1 + RANK, deterministic=True) init_seeds(opt.seed + 1 + RANK, deterministic=True)
save_dir, data, bs, epochs, nw, imgsz, pretrained = \ save_dir, data, bs, epochs, nw, imgsz, pretrained = (
opt.save_dir, Path(opt.data), opt.batch_size, opt.epochs, min(os.cpu_count() - 1, opt.workers), \ opt.save_dir,
opt.imgsz, str(opt.pretrained).lower() == 'true' Path(opt.data),
cuda = device.type != 'cpu' opt.batch_size,
opt.epochs,
min(os.cpu_count() - 1, opt.workers),
opt.imgsz,
str(opt.pretrained).lower() == "true",
)
cuda = device.type != "cpu"
# Directories # Directories
wdir = save_dir / 'weights' wdir = save_dir / "weights"
wdir.mkdir(parents=True, exist_ok=True) # make dir wdir.mkdir(parents=True, exist_ok=True) # make dir
last, best = wdir / 'last.pt', wdir / 'best.pt' last, best = wdir / "last.pt", wdir / "best.pt"
# Save run settings # Save run settings
yaml_save(save_dir / 'opt.yaml', vars(opt)) yaml_save(save_dir / "opt.yaml", vars(opt))
# Logger # Logger
logger = GenericLogger(opt=opt, console_logger=LOGGER) if RANK in {-1, 0} else None logger = GenericLogger(opt=opt, console_logger=LOGGER) if RANK in {-1, 0} else None
@@ -75,51 +103,55 @@ def train(opt, device):
with torch_distributed_zero_first(LOCAL_RANK), WorkingDirectory(ROOT): with torch_distributed_zero_first(LOCAL_RANK), WorkingDirectory(ROOT):
data_dir = data if data.is_dir() else (DATASETS_DIR / data) data_dir = data if data.is_dir() else (DATASETS_DIR / data)
if not data_dir.is_dir(): if not data_dir.is_dir():
LOGGER.info(f'\nDataset not found ⚠️, missing path {data_dir}, attempting download...') LOGGER.info(f"\nDataset not found ⚠️, missing path {data_dir}, attempting download...")
t = time.time() t = time.time()
if str(data) == 'imagenet': if str(data) == "imagenet":
subprocess.run(['bash', str(ROOT / 'data/scripts/get_imagenet.sh')], shell=True, check=True) subprocess.run(["bash", str(ROOT / "data/scripts/get_imagenet.sh")], shell=True, check=True)
else: else:
url = f'https://github.com/ultralytics/yolov5/releases/download/v1.0/{data}.zip' url = f"https://github.com/ultralytics/yolov5/releases/download/v1.0/{data}.zip"
download(url, dir=data_dir.parent) download(url, dir=data_dir.parent)
s = f"Dataset download success ✅ ({time.time() - t:.1f}s), saved to {colorstr('bold', data_dir)}\n" s = f"Dataset download success ✅ ({time.time() - t:.1f}s), saved to {colorstr('bold', data_dir)}\n"
LOGGER.info(s) LOGGER.info(s)
# Dataloaders # Dataloaders
nc = len([x for x in (data_dir / 'train').glob('*') if x.is_dir()]) # number of classes nc = len([x for x in (data_dir / "train").glob("*") if x.is_dir()]) # number of classes
trainloader = create_classification_dataloader(path=data_dir / 'train', trainloader = create_classification_dataloader(
imgsz=imgsz, path=data_dir / "train",
batch_size=bs // WORLD_SIZE, imgsz=imgsz,
augment=True, batch_size=bs // WORLD_SIZE,
cache=opt.cache, augment=True,
rank=LOCAL_RANK, cache=opt.cache,
workers=nw) rank=LOCAL_RANK,
workers=nw,
)
test_dir = data_dir / 'test' if (data_dir / 'test').exists() else data_dir / 'val' # data/test or data/val test_dir = data_dir / "test" if (data_dir / "test").exists() else data_dir / "val" # data/test or data/val
if RANK in {-1, 0}: if RANK in {-1, 0}:
testloader = create_classification_dataloader(path=test_dir, testloader = create_classification_dataloader(
imgsz=imgsz, path=test_dir,
batch_size=bs // WORLD_SIZE * 2, imgsz=imgsz,
augment=False, batch_size=bs // WORLD_SIZE * 2,
cache=opt.cache, augment=False,
rank=-1, cache=opt.cache,
workers=nw) rank=-1,
workers=nw,
)
# Model # Model
with torch_distributed_zero_first(LOCAL_RANK), WorkingDirectory(ROOT): with torch_distributed_zero_first(LOCAL_RANK), WorkingDirectory(ROOT):
if Path(opt.model).is_file() or opt.model.endswith('.pt'): if Path(opt.model).is_file() or opt.model.endswith(".pt"):
model = attempt_load(opt.model, device='cpu', fuse=False) model = attempt_load(opt.model, device="cpu", fuse=False)
elif opt.model in torchvision.models.__dict__: # TorchVision models i.e. resnet50, efficientnet_b0 elif opt.model in torchvision.models.__dict__: # TorchVision models i.e. resnet50, efficientnet_b0
model = torchvision.models.__dict__[opt.model](weights='IMAGENET1K_V1' if pretrained else None) model = torchvision.models.__dict__[opt.model](weights="IMAGENET1K_V1" if pretrained else None)
else: else:
m = hub.list('ultralytics/yolov5') # + hub.list('pytorch/vision') # models m = hub.list("ultralytics/yolov5") # + hub.list('pytorch/vision') # models
raise ModuleNotFoundError(f'--model {opt.model} not found. Available models are: \n' + '\n'.join(m)) raise ModuleNotFoundError(f"--model {opt.model} not found. Available models are: \n" + "\n".join(m))
if isinstance(model, DetectionModel): if isinstance(model, DetectionModel):
LOGGER.warning("WARNING ⚠️ pass YOLOv5 classifier model with '-cls' suffix, i.e. '--model yolov5s-cls.pt'") LOGGER.warning("WARNING ⚠️ pass YOLOv5 classifier model with '-cls' suffix, i.e. '--model yolov5s-cls.pt'")
model = ClassificationModel(model=model, nc=nc, cutoff=opt.cutoff or 10) # convert to classification model model = ClassificationModel(model=model, nc=nc, cutoff=opt.cutoff or 10) # convert to classification model
reshape_classifier_output(model, nc) # update class count reshape_classifier_output(model, nc) # update class count
for m in model.modules(): for m in model.modules():
if not pretrained and hasattr(m, 'reset_parameters'): if not pretrained and hasattr(m, "reset_parameters"):
m.reset_parameters() m.reset_parameters()
if isinstance(m, torch.nn.Dropout) and opt.dropout is not None: if isinstance(m, torch.nn.Dropout) and opt.dropout is not None:
m.p = opt.dropout # set dropout m.p = opt.dropout # set dropout
@@ -135,8 +167,8 @@ def train(opt, device):
if opt.verbose: if opt.verbose:
LOGGER.info(model) LOGGER.info(model)
images, labels = next(iter(trainloader)) images, labels = next(iter(trainloader))
file = imshow_cls(images[:25], labels[:25], names=model.names, f=save_dir / 'train_images.jpg') file = imshow_cls(images[:25], labels[:25], names=model.names, f=save_dir / "train_images.jpg")
logger.log_images(file, name='Train Examples') logger.log_images(file, name="Train Examples")
logger.log_graph(model, imgsz) # log model logger.log_graph(model, imgsz) # log model
# Optimizer # Optimizer
@@ -163,11 +195,13 @@ def train(opt, device):
best_fitness = 0.0 best_fitness = 0.0
scaler = amp.GradScaler(enabled=cuda) scaler = amp.GradScaler(enabled=cuda)
val = test_dir.stem # 'val' or 'test' val = test_dir.stem # 'val' or 'test'
LOGGER.info(f'Image sizes {imgsz} train, {imgsz} test\n' LOGGER.info(
f'Using {nw * WORLD_SIZE} dataloader workers\n' f'Image sizes {imgsz} train, {imgsz} test\n'
f"Logging results to {colorstr('bold', save_dir)}\n" f'Using {nw * WORLD_SIZE} dataloader workers\n'
f'Starting {opt.model} training on {data} dataset with {nc} classes for {epochs} epochs...\n\n' f"Logging results to {colorstr('bold', save_dir)}\n"
f"{'Epoch':>10}{'GPU_mem':>10}{'train_loss':>12}{f'{val}_loss':>12}{'top1_acc':>12}{'top5_acc':>12}") f'Starting {opt.model} training on {data} dataset with {nc} classes for {epochs} epochs...\n\n'
f"{'Epoch':>10}{'GPU_mem':>10}{'train_loss':>12}{f'{val}_loss':>12}{'top1_acc':>12}{'top5_acc':>12}"
)
for epoch in range(epochs): # loop over the dataset multiple times for epoch in range(epochs): # loop over the dataset multiple times
tloss, vloss, fitness = 0.0, 0.0, 0.0 # train loss, val loss, fitness tloss, vloss, fitness = 0.0, 0.0, 0.0 # train loss, val loss, fitness
model.train() model.train()
@@ -198,15 +232,14 @@ def train(opt, device):
if RANK in {-1, 0}: if RANK in {-1, 0}:
# Print # Print
tloss = (tloss * i + loss.item()) / (i + 1) # update mean losses tloss = (tloss * i + loss.item()) / (i + 1) # update mean losses
mem = '%.3gG' % (torch.cuda.memory_reserved() / 1E9 if torch.cuda.is_available() else 0) # (GB) mem = "%.3gG" % (torch.cuda.memory_reserved() / 1e9 if torch.cuda.is_available() else 0) # (GB)
pbar.desc = f"{f'{epoch + 1}/{epochs}':>10}{mem:>10}{tloss:>12.3g}" + ' ' * 36 pbar.desc = f"{f'{epoch + 1}/{epochs}':>10}{mem:>10}{tloss:>12.3g}" + " " * 36
# Test # Test
if i == len(pbar) - 1: # last batch if i == len(pbar) - 1: # last batch
top1, top5, vloss = validate.run(model=ema.ema, top1, top5, vloss = validate.run(
dataloader=testloader, model=ema.ema, dataloader=testloader, criterion=criterion, pbar=pbar
criterion=criterion, ) # test accuracy, loss
pbar=pbar) # test accuracy, loss
fitness = top1 # define fitness as top1 accuracy fitness = top1 # define fitness as top1 accuracy
# Scheduler # Scheduler
@@ -220,26 +253,28 @@ def train(opt, device):
# Log # Log
metrics = { metrics = {
'train/loss': tloss, "train/loss": tloss,
f'{val}/loss': vloss, f"{val}/loss": vloss,
'metrics/accuracy_top1': top1, "metrics/accuracy_top1": top1,
'metrics/accuracy_top5': top5, "metrics/accuracy_top5": top5,
'lr/0': optimizer.param_groups[0]['lr']} # learning rate "lr/0": optimizer.param_groups[0]["lr"],
} # learning rate
logger.log_metrics(metrics, epoch) logger.log_metrics(metrics, epoch)
# Save model # Save model
final_epoch = epoch + 1 == epochs final_epoch = epoch + 1 == epochs
if (not opt.nosave) or final_epoch: if (not opt.nosave) or final_epoch:
ckpt = { ckpt = {
'epoch': epoch, "epoch": epoch,
'best_fitness': best_fitness, "best_fitness": best_fitness,
'model': deepcopy(ema.ema).half(), # deepcopy(de_parallel(model)).half(), "model": deepcopy(ema.ema).half(), # deepcopy(de_parallel(model)).half(),
'ema': None, # deepcopy(ema.ema).half(), "ema": None, # deepcopy(ema.ema).half(),
'updates': ema.updates, "updates": ema.updates,
'optimizer': None, # optimizer.state_dict(), "optimizer": None, # optimizer.state_dict(),
'opt': vars(opt), "opt": vars(opt),
'git': GIT_INFO, # {remote, branch, commit} if a git repo "git": GIT_INFO, # {remote, branch, commit} if a git repo
'date': datetime.now().isoformat()} "date": datetime.now().isoformat(),
}
# Save last, best and delete # Save last, best and delete
torch.save(ckpt, last) torch.save(ckpt, last)
@@ -249,49 +284,51 @@ def train(opt, device):
# Train complete # Train complete
if RANK in {-1, 0} and final_epoch: if RANK in {-1, 0} and final_epoch:
LOGGER.info(f'\nTraining complete ({(time.time() - t0) / 3600:.3f} hours)' LOGGER.info(
f"\nResults saved to {colorstr('bold', save_dir)}" f'\nTraining complete ({(time.time() - t0) / 3600:.3f} hours)'
f'\nPredict: python classify/predict.py --weights {best} --source im.jpg' f"\nResults saved to {colorstr('bold', save_dir)}"
f'\nValidate: python classify/val.py --weights {best} --data {data_dir}' f'\nPredict: python classify/predict.py --weights {best} --source im.jpg'
f'\nExport: python export.py --weights {best} --include onnx' f'\nValidate: python classify/val.py --weights {best} --data {data_dir}'
f"\nPyTorch Hub: model = torch.hub.load('ultralytics/yolov5', 'custom', '{best}')" f'\nExport: python export.py --weights {best} --include onnx'
f'\nVisualize: https://netron.app\n') f"\nPyTorch Hub: model = torch.hub.load('ultralytics/yolov5', 'custom', '{best}')"
f'\nVisualize: https://netron.app\n'
)
# Plot examples # Plot examples
images, labels = (x[:25] for x in next(iter(testloader))) # first 25 images and labels images, labels = (x[:25] for x in next(iter(testloader))) # first 25 images and labels
pred = torch.max(ema.ema(images.to(device)), 1)[1] pred = torch.max(ema.ema(images.to(device)), 1)[1]
file = imshow_cls(images, labels, pred, de_parallel(model).names, verbose=False, f=save_dir / 'test_images.jpg') file = imshow_cls(images, labels, pred, de_parallel(model).names, verbose=False, f=save_dir / "test_images.jpg")
# Log results # Log results
meta = {'epochs': epochs, 'top1_acc': best_fitness, 'date': datetime.now().isoformat()} meta = {"epochs": epochs, "top1_acc": best_fitness, "date": datetime.now().isoformat()}
logger.log_images(file, name='Test Examples (true-predicted)', epoch=epoch) logger.log_images(file, name="Test Examples (true-predicted)", epoch=epoch)
logger.log_model(best, epochs, metadata=meta) logger.log_model(best, epochs, metadata=meta)
def parse_opt(known=False): def parse_opt(known=False):
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('--model', type=str, default='yolov5s-cls.pt', help='initial weights path') parser.add_argument("--model", type=str, default="yolov5s-cls.pt", help="initial weights path")
parser.add_argument('--data', type=str, default='imagenette160', help='cifar10, cifar100, mnist, imagenet, ...') parser.add_argument("--data", type=str, default="imagenette160", help="cifar10, cifar100, mnist, imagenet, ...")
parser.add_argument('--epochs', type=int, default=10, help='total training epochs') parser.add_argument("--epochs", type=int, default=10, help="total training epochs")
parser.add_argument('--batch-size', type=int, default=64, help='total batch size for all GPUs') parser.add_argument("--batch-size", type=int, default=64, help="total batch size for all GPUs")
parser.add_argument('--imgsz', '--img', '--img-size', type=int, default=224, help='train, val image size (pixels)') parser.add_argument("--imgsz", "--img", "--img-size", type=int, default=224, help="train, val image size (pixels)")
parser.add_argument('--nosave', action='store_true', help='only save final checkpoint') parser.add_argument("--nosave", action="store_true", help="only save final checkpoint")
parser.add_argument('--cache', type=str, nargs='?', const='ram', help='--cache images in "ram" (default) or "disk"') parser.add_argument("--cache", type=str, nargs="?", const="ram", help='--cache images in "ram" (default) or "disk"')
parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu') parser.add_argument("--device", default="", help="cuda device, i.e. 0 or 0,1,2,3 or cpu")
parser.add_argument('--workers', type=int, default=8, help='max dataloader workers (per RANK in DDP mode)') parser.add_argument("--workers", type=int, default=8, help="max dataloader workers (per RANK in DDP mode)")
parser.add_argument('--project', default=ROOT / 'runs/train-cls', help='save to project/name') parser.add_argument("--project", default=ROOT / "runs/train-cls", help="save to project/name")
parser.add_argument('--name', default='exp', help='save to project/name') parser.add_argument("--name", default="exp", help="save to project/name")
parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment') parser.add_argument("--exist-ok", action="store_true", help="existing project/name ok, do not increment")
parser.add_argument('--pretrained', nargs='?', const=True, default=True, help='start from i.e. --pretrained False') parser.add_argument("--pretrained", nargs="?", const=True, default=True, help="start from i.e. --pretrained False")
parser.add_argument('--optimizer', choices=['SGD', 'Adam', 'AdamW', 'RMSProp'], default='Adam', help='optimizer') parser.add_argument("--optimizer", choices=["SGD", "Adam", "AdamW", "RMSProp"], default="Adam", help="optimizer")
parser.add_argument('--lr0', type=float, default=0.001, help='initial learning rate') parser.add_argument("--lr0", type=float, default=0.001, help="initial learning rate")
parser.add_argument('--decay', type=float, default=5e-5, help='weight decay') parser.add_argument("--decay", type=float, default=5e-5, help="weight decay")
parser.add_argument('--label-smoothing', type=float, default=0.1, help='Label smoothing epsilon') parser.add_argument("--label-smoothing", type=float, default=0.1, help="Label smoothing epsilon")
parser.add_argument('--cutoff', type=int, default=None, help='Model layer cutoff index for Classify() head') parser.add_argument("--cutoff", type=int, default=None, help="Model layer cutoff index for Classify() head")
parser.add_argument('--dropout', type=float, default=None, help='Dropout (fraction)') parser.add_argument("--dropout", type=float, default=None, help="Dropout (fraction)")
parser.add_argument('--verbose', action='store_true', help='Verbose mode') parser.add_argument("--verbose", action="store_true", help="Verbose mode")
parser.add_argument('--seed', type=int, default=0, help='Global training seed') parser.add_argument("--seed", type=int, default=0, help="Global training seed")
parser.add_argument('--local_rank', type=int, default=-1, help='Automatic DDP Multi-GPU argument, do not modify') parser.add_argument("--local_rank", type=int, default=-1, help="Automatic DDP Multi-GPU argument, do not modify")
return parser.parse_known_args()[0] if known else parser.parse_args() return parser.parse_known_args()[0] if known else parser.parse_args()
@@ -300,17 +337,17 @@ def main(opt):
if RANK in {-1, 0}: if RANK in {-1, 0}:
print_args(vars(opt)) print_args(vars(opt))
check_git_status() check_git_status()
check_requirements(ROOT / 'requirements.txt') check_requirements(ROOT / "requirements.txt")
# DDP mode # DDP mode
device = select_device(opt.device, batch_size=opt.batch_size) device = select_device(opt.device, batch_size=opt.batch_size)
if LOCAL_RANK != -1: if LOCAL_RANK != -1:
assert opt.batch_size != -1, 'AutoBatch is coming soon for classification, please pass a valid --batch-size' assert opt.batch_size != -1, "AutoBatch is coming soon for classification, please pass a valid --batch-size"
assert opt.batch_size % WORLD_SIZE == 0, f'--batch-size {opt.batch_size} must be multiple of WORLD_SIZE' assert opt.batch_size % WORLD_SIZE == 0, f"--batch-size {opt.batch_size} must be multiple of WORLD_SIZE"
assert torch.cuda.device_count() > LOCAL_RANK, 'insufficient CUDA devices for DDP command' assert torch.cuda.device_count() > LOCAL_RANK, "insufficient CUDA devices for DDP command"
torch.cuda.set_device(LOCAL_RANK) torch.cuda.set_device(LOCAL_RANK)
device = torch.device('cuda', LOCAL_RANK) device = torch.device("cuda", LOCAL_RANK)
dist.init_process_group(backend='nccl' if dist.is_nccl_available() else 'gloo') dist.init_process_group(backend="nccl" if dist.is_nccl_available() else "gloo")
# Parameters # Parameters
opt.save_dir = increment_path(Path(opt.project) / opt.name, exist_ok=opt.exist_ok) # increment run opt.save_dir = increment_path(Path(opt.project) / opt.name, exist_ok=opt.exist_ok) # increment run
@@ -328,6 +365,6 @@ def run(**kwargs):
return opt return opt
if __name__ == '__main__': if __name__ == "__main__":
opt = parse_opt() opt = parse_opt()
main(opt) main(opt)
+43 -38
View File
@@ -1,6 +1,6 @@
# YOLOv5 🚀 by Ultralytics, AGPL-3.0 license # YOLOv5 🚀 by Ultralytics, AGPL-3.0 license
""" """
Validate a trained YOLOv5 classification model on a classification dataset Validate a trained YOLOv5 classification model on a classification dataset.
Usage: Usage:
$ bash data/scripts/get_imagenet.sh --val # download ImageNet val split (6.3G, 50000 images) $ bash data/scripts/get_imagenet.sh --val # download ImageNet val split (6.3G, 50000 images)
@@ -36,22 +36,30 @@ ROOT = Path(os.path.relpath(ROOT, Path.cwd())) # relative
from models.common import DetectMultiBackend from models.common import DetectMultiBackend
from utils.dataloaders import create_classification_dataloader from utils.dataloaders import create_classification_dataloader
from utils.general import (LOGGER, TQDM_BAR_FORMAT, Profile, check_img_size, check_requirements, colorstr, from utils.general import (
increment_path, print_args) LOGGER,
TQDM_BAR_FORMAT,
Profile,
check_img_size,
check_requirements,
colorstr,
increment_path,
print_args,
)
from utils.torch_utils import select_device, smart_inference_mode from utils.torch_utils import select_device, smart_inference_mode
@smart_inference_mode() @smart_inference_mode()
def run( def run(
data=ROOT / '../datasets/mnist', # dataset dir data=ROOT / "../datasets/mnist", # dataset dir
weights=ROOT / 'yolov5s-cls.pt', # model.pt path(s) weights=ROOT / "yolov5s-cls.pt", # model.pt path(s)
batch_size=128, # batch size batch_size=128, # batch size
imgsz=224, # inference size (pixels) imgsz=224, # inference size (pixels)
device='', # cuda device, i.e. 0 or 0,1,2,3 or cpu device="", # cuda device, i.e. 0 or 0,1,2,3 or cpu
workers=8, # max dataloader workers (per RANK in DDP mode) workers=8, # max dataloader workers (per RANK in DDP mode)
verbose=False, # verbose output verbose=False, # verbose output
project=ROOT / 'runs/val-cls', # save to project/name project=ROOT / "runs/val-cls", # save to project/name
name='exp', # save to project/name name="exp", # save to project/name
exist_ok=False, # existing project/name ok, do not increment exist_ok=False, # existing project/name ok, do not increment
half=False, # use FP16 half-precision inference half=False, # use FP16 half-precision inference
dnn=False, # use OpenCV DNN for ONNX inference dnn=False, # use OpenCV DNN for ONNX inference
@@ -64,7 +72,7 @@ def run(
training = model is not None training = model is not None
if training: # called by train.py if training: # called by train.py
device, pt, jit, engine = next(model.parameters()).device, True, False, False # get model device, PyTorch model device, pt, jit, engine = next(model.parameters()).device, True, False, False # get model device, PyTorch model
half &= device.type != 'cpu' # half precision only supported on CUDA half &= device.type != "cpu" # half precision only supported on CUDA
model.half() if half else model.float() model.half() if half else model.float()
else: # called directly else: # called directly
device = select_device(device, batch_size=batch_size) device = select_device(device, batch_size=batch_size)
@@ -84,25 +92,22 @@ def run(
device = model.device device = model.device
if not (pt or jit): if not (pt or jit):
batch_size = 1 # export.py models default to batch-size 1 batch_size = 1 # export.py models default to batch-size 1
LOGGER.info(f'Forcing --batch-size 1 square inference (1,3,{imgsz},{imgsz}) for non-PyTorch models') LOGGER.info(f"Forcing --batch-size 1 square inference (1,3,{imgsz},{imgsz}) for non-PyTorch models")
# Dataloader # Dataloader
data = Path(data) data = Path(data)
test_dir = data / 'test' if (data / 'test').exists() else data / 'val' # data/test or data/val test_dir = data / "test" if (data / "test").exists() else data / "val" # data/test or data/val
dataloader = create_classification_dataloader(path=test_dir, dataloader = create_classification_dataloader(
imgsz=imgsz, path=test_dir, imgsz=imgsz, batch_size=batch_size, augment=False, rank=-1, workers=workers
batch_size=batch_size, )
augment=False,
rank=-1,
workers=workers)
model.eval() model.eval()
pred, targets, loss, dt = [], [], 0, (Profile(device=device), Profile(device=device), Profile(device=device)) pred, targets, loss, dt = [], [], 0, (Profile(device=device), Profile(device=device), Profile(device=device))
n = len(dataloader) # number of batches n = len(dataloader) # number of batches
action = 'validating' if dataloader.dataset.root.stem == 'val' else 'testing' action = "validating" if dataloader.dataset.root.stem == "val" else "testing"
desc = f'{pbar.desc[:-36]}{action:>36}' if pbar else f'{action}' desc = f"{pbar.desc[:-36]}{action:>36}" if pbar else f"{action}"
bar = tqdm(dataloader, desc, n, not training, bar_format=TQDM_BAR_FORMAT, position=0) bar = tqdm(dataloader, desc, n, not training, bar_format=TQDM_BAR_FORMAT, position=0)
with torch.cuda.amp.autocast(enabled=device.type != 'cpu'): with torch.cuda.amp.autocast(enabled=device.type != "cpu"):
for images, labels in bar: for images, labels in bar:
with dt[0]: with dt[0]:
images, labels = images.to(device, non_blocking=True), labels.to(device) images, labels = images.to(device, non_blocking=True), labels.to(device)
@@ -123,19 +128,19 @@ def run(
top1, top5 = acc.mean(0).tolist() top1, top5 = acc.mean(0).tolist()
if pbar: if pbar:
pbar.desc = f'{pbar.desc[:-36]}{loss:>12.3g}{top1:>12.3g}{top5:>12.3g}' pbar.desc = f"{pbar.desc[:-36]}{loss:>12.3g}{top1:>12.3g}{top5:>12.3g}"
if verbose: # all classes if verbose: # all classes
LOGGER.info(f"{'Class':>24}{'Images':>12}{'top1_acc':>12}{'top5_acc':>12}") LOGGER.info(f"{'Class':>24}{'Images':>12}{'top1_acc':>12}{'top5_acc':>12}")
LOGGER.info(f"{'all':>24}{targets.shape[0]:>12}{top1:>12.3g}{top5:>12.3g}") LOGGER.info(f"{'all':>24}{targets.shape[0]:>12}{top1:>12.3g}{top5:>12.3g}")
for i, c in model.names.items(): for i, c in model.names.items():
acc_i = acc[targets == i] acc_i = acc[targets == i]
top1i, top5i = acc_i.mean(0).tolist() top1i, top5i = acc_i.mean(0).tolist()
LOGGER.info(f'{c:>24}{acc_i.shape[0]:>12}{top1i:>12.3g}{top5i:>12.3g}') LOGGER.info(f"{c:>24}{acc_i.shape[0]:>12}{top1i:>12.3g}{top5i:>12.3g}")
# Print results # Print results
t = tuple(x.t / len(dataloader.dataset.samples) * 1E3 for x in dt) # speeds per image t = tuple(x.t / len(dataloader.dataset.samples) * 1e3 for x in dt) # speeds per image
shape = (1, 3, imgsz, imgsz) shape = (1, 3, imgsz, imgsz)
LOGGER.info(f'Speed: %.1fms pre-process, %.1fms inference, %.1fms post-process per image at shape {shape}' % t) LOGGER.info(f"Speed: %.1fms pre-process, %.1fms inference, %.1fms post-process per image at shape {shape}" % t)
LOGGER.info(f"Results saved to {colorstr('bold', save_dir)}") LOGGER.info(f"Results saved to {colorstr('bold', save_dir)}")
return top1, top5, loss return top1, top5, loss
@@ -143,28 +148,28 @@ def run(
def parse_opt(): def parse_opt():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('--data', type=str, default=ROOT / '../datasets/mnist', help='dataset path') parser.add_argument("--data", type=str, default=ROOT / "../datasets/mnist", help="dataset path")
parser.add_argument('--weights', nargs='+', type=str, default=ROOT / 'yolov5s-cls.pt', help='model.pt path(s)') parser.add_argument("--weights", nargs="+", type=str, default=ROOT / "yolov5s-cls.pt", help="model.pt path(s)")
parser.add_argument('--batch-size', type=int, default=128, help='batch size') parser.add_argument("--batch-size", type=int, default=128, help="batch size")
parser.add_argument('--imgsz', '--img', '--img-size', type=int, default=224, help='inference size (pixels)') parser.add_argument("--imgsz", "--img", "--img-size", type=int, default=224, help="inference size (pixels)")
parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu') parser.add_argument("--device", default="", help="cuda device, i.e. 0 or 0,1,2,3 or cpu")
parser.add_argument('--workers', type=int, default=8, help='max dataloader workers (per RANK in DDP mode)') parser.add_argument("--workers", type=int, default=8, help="max dataloader workers (per RANK in DDP mode)")
parser.add_argument('--verbose', nargs='?', const=True, default=True, help='verbose output') parser.add_argument("--verbose", nargs="?", const=True, default=True, help="verbose output")
parser.add_argument('--project', default=ROOT / 'runs/val-cls', help='save to project/name') parser.add_argument("--project", default=ROOT / "runs/val-cls", help="save to project/name")
parser.add_argument('--name', default='exp', help='save to project/name') parser.add_argument("--name", default="exp", help="save to project/name")
parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment') parser.add_argument("--exist-ok", action="store_true", help="existing project/name ok, do not increment")
parser.add_argument('--half', action='store_true', help='use FP16 half-precision inference') parser.add_argument("--half", action="store_true", help="use FP16 half-precision inference")
parser.add_argument('--dnn', action='store_true', help='use OpenCV DNN for ONNX inference') parser.add_argument("--dnn", action="store_true", help="use OpenCV DNN for ONNX inference")
opt = parser.parse_args() opt = parser.parse_args()
print_args(vars(opt)) print_args(vars(opt))
return opt return opt
def main(opt): def main(opt):
check_requirements(ROOT / 'requirements.txt', exclude=('tensorboard', 'thop')) check_requirements(ROOT / "requirements.txt", exclude=("tensorboard", "thop"))
run(**vars(opt)) run(**vars(opt))
if __name__ == '__main__': if __name__ == "__main__":
opt = parse_opt() opt = parse_opt()
main(opt) main(opt)
+99 -85
View File
@@ -47,54 +47,68 @@ from ultralytics.utils.plotting import Annotator, colors, save_one_box
from models.common import DetectMultiBackend from models.common import DetectMultiBackend
from utils.dataloaders import IMG_FORMATS, VID_FORMATS, LoadImages, LoadScreenshots, LoadStreams from utils.dataloaders import IMG_FORMATS, VID_FORMATS, LoadImages, LoadScreenshots, LoadStreams
from utils.general import (LOGGER, Profile, check_file, check_img_size, check_imshow, check_requirements, colorstr, cv2, from utils.general import (
increment_path, non_max_suppression, print_args, scale_boxes, strip_optimizer, xyxy2xywh) LOGGER,
Profile,
check_file,
check_img_size,
check_imshow,
check_requirements,
colorstr,
cv2,
increment_path,
non_max_suppression,
print_args,
scale_boxes,
strip_optimizer,
xyxy2xywh,
)
from utils.torch_utils import select_device, smart_inference_mode from utils.torch_utils import select_device, smart_inference_mode
@smart_inference_mode() @smart_inference_mode()
def run( def run(
weights=ROOT / 'yolov5s.pt', # model path or triton URL weights=ROOT / "yolov5s.pt", # model path or triton URL
source=ROOT / 'data/images', # file/dir/URL/glob/screen/0(webcam) source=ROOT / "data/images", # file/dir/URL/glob/screen/0(webcam)
data=ROOT / 'data/coco128.yaml', # dataset.yaml path data=ROOT / "data/coco128.yaml", # dataset.yaml path
imgsz=(640, 640), # inference size (height, width) imgsz=(640, 640), # inference size (height, width)
conf_thres=0.25, # confidence threshold conf_thres=0.25, # confidence threshold
iou_thres=0.45, # NMS IOU threshold iou_thres=0.45, # NMS IOU threshold
max_det=1000, # maximum detections per image max_det=1000, # maximum detections per image
device='', # cuda device, i.e. 0 or 0,1,2,3 or cpu device="", # cuda device, i.e. 0 or 0,1,2,3 or cpu
view_img=False, # show results view_img=False, # show results
save_txt=False, # save results to *.txt save_txt=False, # save results to *.txt
save_csv=False, # save results in CSV format save_csv=False, # save results in CSV format
save_conf=False, # save confidences in --save-txt labels save_conf=False, # save confidences in --save-txt labels
save_crop=False, # save cropped prediction boxes save_crop=False, # save cropped prediction boxes
nosave=False, # do not save images/videos nosave=False, # do not save images/videos
classes=None, # filter by class: --class 0, or --class 0 2 3 classes=None, # filter by class: --class 0, or --class 0 2 3
agnostic_nms=False, # class-agnostic NMS agnostic_nms=False, # class-agnostic NMS
augment=False, # augmented inference augment=False, # augmented inference
visualize=False, # visualize features visualize=False, # visualize features
update=False, # update all models update=False, # update all models
project=ROOT / 'runs/detect', # save results to project/name project=ROOT / "runs/detect", # save results to project/name
name='exp', # save results to project/name name="exp", # save results to project/name
exist_ok=False, # existing project/name ok, do not increment exist_ok=False, # existing project/name ok, do not increment
line_thickness=3, # bounding box thickness (pixels) line_thickness=3, # bounding box thickness (pixels)
hide_labels=False, # hide labels hide_labels=False, # hide labels
hide_conf=False, # hide confidences hide_conf=False, # hide confidences
half=False, # use FP16 half-precision inference half=False, # use FP16 half-precision inference
dnn=False, # use OpenCV DNN for ONNX inference dnn=False, # use OpenCV DNN for ONNX inference
vid_stride=1, # video frame-rate stride vid_stride=1, # video frame-rate stride
): ):
source = str(source) source = str(source)
save_img = not nosave and not source.endswith('.txt') # save inference images save_img = not nosave and not source.endswith(".txt") # save inference images
is_file = Path(source).suffix[1:] in (IMG_FORMATS + VID_FORMATS) is_file = Path(source).suffix[1:] in (IMG_FORMATS + VID_FORMATS)
is_url = source.lower().startswith(('rtsp://', 'rtmp://', 'http://', 'https://')) is_url = source.lower().startswith(("rtsp://", "rtmp://", "http://", "https://"))
webcam = source.isnumeric() or source.endswith('.streams') or (is_url and not is_file) webcam = source.isnumeric() or source.endswith(".streams") or (is_url and not is_file)
screenshot = source.lower().startswith('screen') screenshot = source.lower().startswith("screen")
if is_url and is_file: if is_url and is_file:
source = check_file(source) # download source = check_file(source) # download
# Directories # Directories
save_dir = increment_path(Path(project) / name, exist_ok=exist_ok) # increment run save_dir = increment_path(Path(project) / name, exist_ok=exist_ok) # increment run
(save_dir / 'labels' if save_txt else save_dir).mkdir(parents=True, exist_ok=True) # make dir (save_dir / "labels" if save_txt else save_dir).mkdir(parents=True, exist_ok=True) # make dir
# Load model # Load model
device = select_device(device) device = select_device(device)
@@ -148,12 +162,12 @@ def run(
# pred = utils.general.apply_classifier(pred, classifier_model, im, im0s) # pred = utils.general.apply_classifier(pred, classifier_model, im, im0s)
# Define the path for the CSV file # Define the path for the CSV file
csv_path = save_dir / 'predictions.csv' csv_path = save_dir / "predictions.csv"
# Create or append to the CSV file # Create or append to the CSV file
def write_to_csv(image_name, prediction, confidence): def write_to_csv(image_name, prediction, confidence):
data = {'Image Name': image_name, 'Prediction': prediction, 'Confidence': confidence} data = {"Image Name": image_name, "Prediction": prediction, "Confidence": confidence}
with open(csv_path, mode='a', newline='') as f: with open(csv_path, mode="a", newline="") as f:
writer = csv.DictWriter(f, fieldnames=data.keys()) writer = csv.DictWriter(f, fieldnames=data.keys())
if not csv_path.is_file(): if not csv_path.is_file():
writer.writeheader() writer.writeheader()
@@ -164,14 +178,14 @@ def run(
seen += 1 seen += 1
if webcam: # batch_size >= 1 if webcam: # batch_size >= 1
p, im0, frame = path[i], im0s[i].copy(), dataset.count p, im0, frame = path[i], im0s[i].copy(), dataset.count
s += f'{i}: ' s += f"{i}: "
else: else:
p, im0, frame = path, im0s.copy(), getattr(dataset, 'frame', 0) p, im0, frame = path, im0s.copy(), getattr(dataset, "frame", 0)
p = Path(p) # to Path p = Path(p) # to Path
save_path = str(save_dir / p.name) # im.jpg save_path = str(save_dir / p.name) # im.jpg
txt_path = str(save_dir / 'labels' / p.stem) + ('' if dataset.mode == 'image' else f'_{frame}') # im.txt txt_path = str(save_dir / "labels" / p.stem) + ("" if dataset.mode == "image" else f"_{frame}") # im.txt
s += '%gx%g ' % im.shape[2:] # print string s += "%gx%g " % im.shape[2:] # print string
gn = torch.tensor(im0.shape)[[1, 0, 1, 0]] # normalization gain whwh gn = torch.tensor(im0.shape)[[1, 0, 1, 0]] # normalization gain whwh
imc = im0.copy() if save_crop else im0 # for save_crop imc = im0.copy() if save_crop else im0 # for save_crop
annotator = Annotator(im0, line_width=line_thickness, example=str(names)) annotator = Annotator(im0, line_width=line_thickness, example=str(names))
@@ -187,9 +201,9 @@ def run(
# Write results # Write results
for *xyxy, conf, cls in reversed(det): for *xyxy, conf, cls in reversed(det):
c = int(cls) # integer class c = int(cls) # integer class
label = names[c] if hide_conf else f'{names[c]}' label = names[c] if hide_conf else f"{names[c]}"
confidence = float(conf) confidence = float(conf)
confidence_str = f'{confidence:.2f}' confidence_str = f"{confidence:.2f}"
if save_csv: if save_csv:
write_to_csv(p.name, label, confidence_str) write_to_csv(p.name, label, confidence_str)
@@ -197,20 +211,20 @@ def run(
if save_txt: # Write to file if save_txt: # Write to file
xywh = (xyxy2xywh(torch.tensor(xyxy).view(1, 4)) / gn).view(-1).tolist() # normalized xywh xywh = (xyxy2xywh(torch.tensor(xyxy).view(1, 4)) / gn).view(-1).tolist() # normalized xywh
line = (cls, *xywh, conf) if save_conf else (cls, *xywh) # label format line = (cls, *xywh, conf) if save_conf else (cls, *xywh) # label format
with open(f'{txt_path}.txt', 'a') as f: with open(f"{txt_path}.txt", "a") as f:
f.write(('%g ' * len(line)).rstrip() % line + '\n') f.write(("%g " * len(line)).rstrip() % line + "\n")
if save_img or save_crop or view_img: # Add bbox to image if save_img or save_crop or view_img: # Add bbox to image
c = int(cls) # integer class c = int(cls) # integer class
label = None if hide_labels else (names[c] if hide_conf else f'{names[c]} {conf:.2f}') label = None if hide_labels else (names[c] if hide_conf else f"{names[c]} {conf:.2f}")
annotator.box_label(xyxy, label, color=colors(c, True)) annotator.box_label(xyxy, label, color=colors(c, True))
if save_crop: if save_crop:
save_one_box(xyxy, imc, file=save_dir / 'crops' / names[c] / f'{p.stem}.jpg', BGR=True) save_one_box(xyxy, imc, file=save_dir / "crops" / names[c] / f"{p.stem}.jpg", BGR=True)
# Stream results # Stream results
im0 = annotator.result() im0 = annotator.result()
if view_img: if view_img:
if platform.system() == 'Linux' and p not in windows: if platform.system() == "Linux" and p not in windows:
windows.append(p) windows.append(p)
cv2.namedWindow(str(p), cv2.WINDOW_NORMAL | cv2.WINDOW_KEEPRATIO) # allow window resize (Linux) cv2.namedWindow(str(p), cv2.WINDOW_NORMAL | cv2.WINDOW_KEEPRATIO) # allow window resize (Linux)
cv2.resizeWindow(str(p), im0.shape[1], im0.shape[0]) cv2.resizeWindow(str(p), im0.shape[1], im0.shape[0])
@@ -219,7 +233,7 @@ def run(
# Save results (image with detections) # Save results (image with detections)
if save_img: if save_img:
if dataset.mode == 'image': if dataset.mode == "image":
cv2.imwrite(save_path, im0) cv2.imwrite(save_path, im0)
else: # 'video' or 'stream' else: # 'video' or 'stream'
if vid_path[i] != save_path: # new video if vid_path[i] != save_path: # new video
@@ -232,18 +246,18 @@ def run(
h = int(vid_cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) h = int(vid_cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
else: # stream else: # stream
fps, w, h = 30, im0.shape[1], im0.shape[0] fps, w, h = 30, im0.shape[1], im0.shape[0]
save_path = str(Path(save_path).with_suffix('.mp4')) # force *.mp4 suffix on results videos save_path = str(Path(save_path).with_suffix(".mp4")) # force *.mp4 suffix on results videos
vid_writer[i] = cv2.VideoWriter(save_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (w, h)) vid_writer[i] = cv2.VideoWriter(save_path, cv2.VideoWriter_fourcc(*"mp4v"), fps, (w, h))
vid_writer[i].write(im0) vid_writer[i].write(im0)
# Print time (inference-only) # Print time (inference-only)
LOGGER.info(f"{s}{'' if len(det) else '(no detections), '}{dt[1].dt * 1E3:.1f}ms") LOGGER.info(f"{s}{'' if len(det) else '(no detections), '}{dt[1].dt * 1E3:.1f}ms")
# Print results # Print results
t = tuple(x.t / seen * 1E3 for x in dt) # speeds per image t = tuple(x.t / seen * 1e3 for x in dt) # speeds per image
LOGGER.info(f'Speed: %.1fms pre-process, %.1fms inference, %.1fms NMS per image at shape {(1, 3, *imgsz)}' % t) LOGGER.info(f"Speed: %.1fms pre-process, %.1fms inference, %.1fms NMS per image at shape {(1, 3, *imgsz)}" % t)
if save_txt or save_img: if save_txt or save_img:
s = f"\n{len(list(save_dir.glob('labels/*.txt')))} labels saved to {save_dir / 'labels'}" if save_txt else '' s = f"\n{len(list(save_dir.glob('labels/*.txt')))} labels saved to {save_dir / 'labels'}" if save_txt else ""
LOGGER.info(f"Results saved to {colorstr('bold', save_dir)}{s}") LOGGER.info(f"Results saved to {colorstr('bold', save_dir)}{s}")
if update: if update:
strip_optimizer(weights[0]) # update model (to fix SourceChangeWarning) strip_optimizer(weights[0]) # update model (to fix SourceChangeWarning)
@@ -251,34 +265,34 @@ def run(
def parse_opt(): def parse_opt():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('--weights', nargs='+', type=str, default=ROOT / 'yolov5s.pt', help='model path or triton URL') parser.add_argument("--weights", nargs="+", type=str, default=ROOT / "yolov5s.pt", help="model path or triton URL")
parser.add_argument('--source', type=str, default=ROOT / 'data/images', help='file/dir/URL/glob/screen/0(webcam)') parser.add_argument("--source", type=str, default=ROOT / "data/images", help="file/dir/URL/glob/screen/0(webcam)")
parser.add_argument('--data', type=str, default=ROOT / 'data/coco128.yaml', help='(optional) dataset.yaml path') parser.add_argument("--data", type=str, default=ROOT / "data/coco128.yaml", help="(optional) dataset.yaml path")
parser.add_argument('--imgsz', '--img', '--img-size', nargs='+', type=int, default=[640], help='inference size h,w') parser.add_argument("--imgsz", "--img", "--img-size", nargs="+", type=int, default=[640], help="inference size h,w")
parser.add_argument('--conf-thres', type=float, default=0.25, help='confidence threshold') parser.add_argument("--conf-thres", type=float, default=0.25, help="confidence threshold")
parser.add_argument('--iou-thres', type=float, default=0.45, help='NMS IoU threshold') parser.add_argument("--iou-thres", type=float, default=0.45, help="NMS IoU threshold")
parser.add_argument('--max-det', type=int, default=1000, help='maximum detections per image') parser.add_argument("--max-det", type=int, default=1000, help="maximum detections per image")
parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu') parser.add_argument("--device", default="", help="cuda device, i.e. 0 or 0,1,2,3 or cpu")
parser.add_argument('--view-img', action='store_true', help='show results') parser.add_argument("--view-img", action="store_true", help="show results")
parser.add_argument('--save-txt', action='store_true', help='save results to *.txt') parser.add_argument("--save-txt", action="store_true", help="save results to *.txt")
parser.add_argument('--save-csv', action='store_true', help='save results in CSV format') parser.add_argument("--save-csv", action="store_true", help="save results in CSV format")
parser.add_argument('--save-conf', action='store_true', help='save confidences in --save-txt labels') parser.add_argument("--save-conf", action="store_true", help="save confidences in --save-txt labels")
parser.add_argument('--save-crop', action='store_true', help='save cropped prediction boxes') parser.add_argument("--save-crop", action="store_true", help="save cropped prediction boxes")
parser.add_argument('--nosave', action='store_true', help='do not save images/videos') parser.add_argument("--nosave", action="store_true", help="do not save images/videos")
parser.add_argument('--classes', nargs='+', type=int, help='filter by class: --classes 0, or --classes 0 2 3') parser.add_argument("--classes", nargs="+", type=int, help="filter by class: --classes 0, or --classes 0 2 3")
parser.add_argument('--agnostic-nms', action='store_true', help='class-agnostic NMS') parser.add_argument("--agnostic-nms", action="store_true", help="class-agnostic NMS")
parser.add_argument('--augment', action='store_true', help='augmented inference') parser.add_argument("--augment", action="store_true", help="augmented inference")
parser.add_argument('--visualize', action='store_true', help='visualize features') parser.add_argument("--visualize", action="store_true", help="visualize features")
parser.add_argument('--update', action='store_true', help='update all models') parser.add_argument("--update", action="store_true", help="update all models")
parser.add_argument('--project', default=ROOT / 'runs/detect', help='save results to project/name') parser.add_argument("--project", default=ROOT / "runs/detect", help="save results to project/name")
parser.add_argument('--name', default='exp', help='save results to project/name') parser.add_argument("--name", default="exp", help="save results to project/name")
parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment') parser.add_argument("--exist-ok", action="store_true", help="existing project/name ok, do not increment")
parser.add_argument('--line-thickness', default=3, type=int, help='bounding box thickness (pixels)') parser.add_argument("--line-thickness", default=3, type=int, help="bounding box thickness (pixels)")
parser.add_argument('--hide-labels', default=False, action='store_true', help='hide labels') parser.add_argument("--hide-labels", default=False, action="store_true", help="hide labels")
parser.add_argument('--hide-conf', default=False, action='store_true', help='hide confidences') parser.add_argument("--hide-conf", default=False, action="store_true", help="hide confidences")
parser.add_argument('--half', action='store_true', help='use FP16 half-precision inference') parser.add_argument("--half", action="store_true", help="use FP16 half-precision inference")
parser.add_argument('--dnn', action='store_true', help='use OpenCV DNN for ONNX inference') parser.add_argument("--dnn", action="store_true", help="use OpenCV DNN for ONNX inference")
parser.add_argument('--vid-stride', type=int, default=1, help='video frame-rate stride') parser.add_argument("--vid-stride", type=int, default=1, help="video frame-rate stride")
opt = parser.parse_args() opt = parser.parse_args()
opt.imgsz *= 2 if len(opt.imgsz) == 1 else 1 # expand opt.imgsz *= 2 if len(opt.imgsz) == 1 else 1 # expand
print_args(vars(opt)) print_args(vars(opt))
@@ -286,10 +300,10 @@ def parse_opt():
def main(opt): def main(opt):
check_requirements(ROOT / 'requirements.txt', exclude=('tensorboard', 'thop')) check_requirements(ROOT / "requirements.txt", exclude=("tensorboard", "thop"))
run(**vars(opt)) run(**vars(opt))
if __name__ == '__main__': if __name__ == "__main__":
opt = parse_opt() opt = parse_opt()
main(opt) main(opt)
+328 -286
View File
File diff suppressed because it is too large. Load diff
+39 -33
View File
@@ -14,7 +14,8 @@ import torch
def _create(name, pretrained=True, channels=3, classes=80, autoshape=True, verbose=True, device=None): def _create(name, pretrained=True, channels=3, classes=80, autoshape=True, verbose=True, device=None):
"""Creates or loads a YOLOv5 model """
Creates or loads a YOLOv5 model.
Arguments: Arguments:
name (str): model name 'yolov5s' or path 'path/to/best.pt' name (str): model name 'yolov5s' or path 'path/to/best.pt'
@@ -39,9 +40,9 @@ def _create(name, pretrained=True, channels=3, classes=80, autoshape=True, verbo
if not verbose: if not verbose:
LOGGER.setLevel(logging.WARNING) LOGGER.setLevel(logging.WARNING)
check_requirements(ROOT / 'requirements.txt', exclude=('opencv-python', 'tensorboard', 'thop')) check_requirements(ROOT / "requirements.txt", exclude=("opencv-python", "tensorboard", "thop"))
name = Path(name) name = Path(name)
path = name.with_suffix('.pt') if name.suffix == '' and not name.is_dir() else name # checkpoint path path = name.with_suffix(".pt") if name.suffix == "" and not name.is_dir() else name # checkpoint path
try: try:
device = select_device(device) device = select_device(device)
if pretrained and channels == 3 and classes == 80: if pretrained and channels == 3 and classes == 80:
@@ -49,91 +50,95 @@ def _create(name, pretrained=True, channels=3, classes=80, autoshape=True, verbo
model = DetectMultiBackend(path, device=device, fuse=autoshape) # detection model model = DetectMultiBackend(path, device=device, fuse=autoshape) # detection model
if autoshape: if autoshape:
if model.pt and isinstance(model.model, ClassificationModel): if model.pt and isinstance(model.model, ClassificationModel):
LOGGER.warning('WARNING ⚠️ YOLOv5 ClassificationModel is not yet AutoShape compatible. ' LOGGER.warning(
'You must pass torch tensors in BCHW to this model, i.e. shape(1,3,224,224).') "WARNING ⚠️ YOLOv5 ClassificationModel is not yet AutoShape compatible. "
"You must pass torch tensors in BCHW to this model, i.e. shape(1,3,224,224)."
)
elif model.pt and isinstance(model.model, SegmentationModel): elif model.pt and isinstance(model.model, SegmentationModel):
LOGGER.warning('WARNING ⚠️ YOLOv5 SegmentationModel is not yet AutoShape compatible. ' LOGGER.warning(
'You will not be able to run inference with this model.') "WARNING ⚠️ YOLOv5 SegmentationModel is not yet AutoShape compatible. "
"You will not be able to run inference with this model."
)
else: else:
model = AutoShape(model) # for file/URI/PIL/cv2/np inputs and NMS model = AutoShape(model) # for file/URI/PIL/cv2/np inputs and NMS
except Exception: except Exception:
model = attempt_load(path, device=device, fuse=False) # arbitrary model model = attempt_load(path, device=device, fuse=False) # arbitrary model
else: else:
cfg = list((Path(__file__).parent / 'models').rglob(f'{path.stem}.yaml'))[0] # model.yaml path cfg = list((Path(__file__).parent / "models").rglob(f"{path.stem}.yaml"))[0] # model.yaml path
model = DetectionModel(cfg, channels, classes) # create model model = DetectionModel(cfg, channels, classes) # create model
if pretrained: if pretrained:
ckpt = torch.load(attempt_download(path), map_location=device) # load ckpt = torch.load(attempt_download(path), map_location=device) # load
csd = ckpt['model'].float().state_dict() # checkpoint state_dict as FP32 csd = ckpt["model"].float().state_dict() # checkpoint state_dict as FP32
csd = intersect_dicts(csd, model.state_dict(), exclude=['anchors']) # intersect csd = intersect_dicts(csd, model.state_dict(), exclude=["anchors"]) # intersect
model.load_state_dict(csd, strict=False) # load model.load_state_dict(csd, strict=False) # load
if len(ckpt['model'].names) == classes: if len(ckpt["model"].names) == classes:
model.names = ckpt['model'].names # set class names attribute model.names = ckpt["model"].names # set class names attribute
if not verbose: if not verbose:
LOGGER.setLevel(logging.INFO) # reset to default LOGGER.setLevel(logging.INFO) # reset to default
return model.to(device) return model.to(device)
except Exception as e: except Exception as e:
help_url = 'https://docs.ultralytics.com/yolov5/tutorials/pytorch_hub_model_loading' help_url = "https://docs.ultralytics.com/yolov5/tutorials/pytorch_hub_model_loading"
s = f'{e}. Cache may be out of date, try `force_reload=True` or see {help_url} for help.' s = f"{e}. Cache may be out of date, try `force_reload=True` or see {help_url} for help."
raise Exception(s) from e raise Exception(s) from e
def custom(path='path/to/model.pt', autoshape=True, _verbose=True, device=None): def custom(path="path/to/model.pt", autoshape=True, _verbose=True, device=None):
# YOLOv5 custom or local model # YOLOv5 custom or local model
return _create(path, autoshape=autoshape, verbose=_verbose, device=device) return _create(path, autoshape=autoshape, verbose=_verbose, device=device)
def yolov5n(pretrained=True, channels=3, classes=80, autoshape=True, _verbose=True, device=None): def yolov5n(pretrained=True, channels=3, classes=80, autoshape=True, _verbose=True, device=None):
# YOLOv5-nano model https://github.com/ultralytics/yolov5 # YOLOv5-nano model https://github.com/ultralytics/yolov5
return _create('yolov5n', pretrained, channels, classes, autoshape, _verbose, device) return _create("yolov5n", pretrained, channels, classes, autoshape, _verbose, device)
def yolov5s(pretrained=True, channels=3, classes=80, autoshape=True, _verbose=True, device=None): def yolov5s(pretrained=True, channels=3, classes=80, autoshape=True, _verbose=True, device=None):
# YOLOv5-small model https://github.com/ultralytics/yolov5 # YOLOv5-small model https://github.com/ultralytics/yolov5
return _create('yolov5s', pretrained, channels, classes, autoshape, _verbose, device) return _create("yolov5s", pretrained, channels, classes, autoshape, _verbose, device)
def yolov5m(pretrained=True, channels=3, classes=80, autoshape=True, _verbose=True, device=None): def yolov5m(pretrained=True, channels=3, classes=80, autoshape=True, _verbose=True, device=None):
# YOLOv5-medium model https://github.com/ultralytics/yolov5 # YOLOv5-medium model https://github.com/ultralytics/yolov5
return _create('yolov5m', pretrained, channels, classes, autoshape, _verbose, device) return _create("yolov5m", pretrained, channels, classes, autoshape, _verbose, device)
def yolov5l(pretrained=True, channels=3, classes=80, autoshape=True, _verbose=True, device=None): def yolov5l(pretrained=True, channels=3, classes=80, autoshape=True, _verbose=True, device=None):
# YOLOv5-large model https://github.com/ultralytics/yolov5 # YOLOv5-large model https://github.com/ultralytics/yolov5
return _create('yolov5l', pretrained, channels, classes, autoshape, _verbose, device) return _create("yolov5l", pretrained, channels, classes, autoshape, _verbose, device)
def yolov5x(pretrained=True, channels=3, classes=80, autoshape=True, _verbose=True, device=None): def yolov5x(pretrained=True, channels=3, classes=80, autoshape=True, _verbose=True, device=None):
# YOLOv5-xlarge model https://github.com/ultralytics/yolov5 # YOLOv5-xlarge model https://github.com/ultralytics/yolov5
return _create('yolov5x', pretrained, channels, classes, autoshape, _verbose, device) return _create("yolov5x", pretrained, channels, classes, autoshape, _verbose, device)
def yolov5n6(pretrained=True, channels=3, classes=80, autoshape=True, _verbose=True, device=None): def yolov5n6(pretrained=True, channels=3, classes=80, autoshape=True, _verbose=True, device=None):
# YOLOv5-nano-P6 model https://github.com/ultralytics/yolov5 # YOLOv5-nano-P6 model https://github.com/ultralytics/yolov5
return _create('yolov5n6', pretrained, channels, classes, autoshape, _verbose, device) return _create("yolov5n6", pretrained, channels, classes, autoshape, _verbose, device)
def yolov5s6(pretrained=True, channels=3, classes=80, autoshape=True, _verbose=True, device=None): def yolov5s6(pretrained=True, channels=3, classes=80, autoshape=True, _verbose=True, device=None):
# YOLOv5-small-P6 model https://github.com/ultralytics/yolov5 # YOLOv5-small-P6 model https://github.com/ultralytics/yolov5
return _create('yolov5s6', pretrained, channels, classes, autoshape, _verbose, device) return _create("yolov5s6", pretrained, channels, classes, autoshape, _verbose, device)
def yolov5m6(pretrained=True, channels=3, classes=80, autoshape=True, _verbose=True, device=None): def yolov5m6(pretrained=True, channels=3, classes=80, autoshape=True, _verbose=True, device=None):
# YOLOv5-medium-P6 model https://github.com/ultralytics/yolov5 # YOLOv5-medium-P6 model https://github.com/ultralytics/yolov5
return _create('yolov5m6', pretrained, channels, classes, autoshape, _verbose, device) return _create("yolov5m6", pretrained, channels, classes, autoshape, _verbose, device)
def yolov5l6(pretrained=True, channels=3, classes=80, autoshape=True, _verbose=True, device=None): def yolov5l6(pretrained=True, channels=3, classes=80, autoshape=True, _verbose=True, device=None):
# YOLOv5-large-P6 model https://github.com/ultralytics/yolov5 # YOLOv5-large-P6 model https://github.com/ultralytics/yolov5
return _create('yolov5l6', pretrained, channels, classes, autoshape, _verbose, device) return _create("yolov5l6", pretrained, channels, classes, autoshape, _verbose, device)
def yolov5x6(pretrained=True, channels=3, classes=80, autoshape=True, _verbose=True, device=None): def yolov5x6(pretrained=True, channels=3, classes=80, autoshape=True, _verbose=True, device=None):
# YOLOv5-xlarge-P6 model https://github.com/ultralytics/yolov5 # YOLOv5-xlarge-P6 model https://github.com/ultralytics/yolov5
return _create('yolov5x6', pretrained, channels, classes, autoshape, _verbose, device) return _create("yolov5x6", pretrained, channels, classes, autoshape, _verbose, device)
if __name__ == '__main__': if __name__ == "__main__":
import argparse import argparse
from pathlib import Path from pathlib import Path
@@ -144,7 +149,7 @@ if __name__ == '__main__':
# Argparser # Argparser
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('--model', type=str, default='yolov5s', help='model name') parser.add_argument("--model", type=str, default="yolov5s", help="model name")
opt = parser.parse_args() opt = parser.parse_args()
print_args(vars(opt)) print_args(vars(opt))
@@ -154,12 +159,13 @@ if __name__ == '__main__':
# Images # Images
imgs = [ imgs = [
'data/images/zidane.jpg', # filename "data/images/zidane.jpg", # filename
Path('data/images/zidane.jpg'), # Path Path("data/images/zidane.jpg"), # Path
'https://ultralytics.com/images/zidane.jpg', # URI "https://ultralytics.com/images/zidane.jpg", # URI
cv2.imread('data/images/bus.jpg')[:, :, ::-1], # OpenCV cv2.imread("data/images/bus.jpg")[:, :, ::-1], # OpenCV
Image.open('data/images/bus.jpg'), # PIL Image.open("data/images/bus.jpg"), # PIL
np.zeros((320, 640, 3))] # numpy np.zeros((320, 640, 3)),
] # numpy
# Inference # Inference
results = model(imgs, size=320) # batched inference results = model(imgs, size=320) # batched inference
+171 -145
View File
@@ -1,7 +1,5 @@
# YOLOv5 🚀 by Ultralytics, AGPL-3.0 license # YOLOv5 🚀 by Ultralytics, AGPL-3.0 license
""" """Common modules."""
Common modules
"""
import ast import ast
import contextlib import contextlib
@@ -28,20 +26,34 @@ from torch.cuda import amp
try: try:
import ultralytics import ultralytics
assert hasattr(ultralytics, '__version__') # verify package is not directory assert hasattr(ultralytics, "__version__") # verify package is not directory
except (ImportError, AssertionError): except (ImportError, AssertionError):
import os import os
os.system('pip install -U ultralytics') os.system("pip install -U ultralytics")
import ultralytics import ultralytics
from ultralytics.utils.plotting import Annotator, colors, save_one_box from ultralytics.utils.plotting import Annotator, colors, save_one_box
from utils import TryExcept from utils import TryExcept
from utils.dataloaders import exif_transpose, letterbox from utils.dataloaders import exif_transpose, letterbox
from utils.general import (LOGGER, ROOT, Profile, check_requirements, check_suffix, check_version, colorstr, from utils.general import (
increment_path, is_jupyter, make_divisible, non_max_suppression, scale_boxes, xywh2xyxy, LOGGER,
xyxy2xywh, yaml_load) ROOT,
Profile,
check_requirements,
check_suffix,
check_version,
colorstr,
increment_path,
is_jupyter,
make_divisible,
non_max_suppression,
scale_boxes,
xywh2xyxy,
xyxy2xywh,
yaml_load,
)
from utils.torch_utils import copy_attr, smart_inference_mode from utils.torch_utils import copy_attr, smart_inference_mode
@@ -223,7 +235,7 @@ class SPP(nn.Module):
def forward(self, x): def forward(self, x):
x = self.cv1(x) x = self.cv1(x)
with warnings.catch_warnings(): with warnings.catch_warnings():
warnings.simplefilter('ignore') # suppress torch 1.9.0 max_pool2d() warning warnings.simplefilter("ignore") # suppress torch 1.9.0 max_pool2d() warning
return self.cv2(torch.cat([x] + [m(x) for m in self.m], 1)) return self.cv2(torch.cat([x] + [m(x) for m in self.m], 1))
@@ -239,7 +251,7 @@ class SPPF(nn.Module):
def forward(self, x): def forward(self, x):
x = self.cv1(x) x = self.cv1(x)
with warnings.catch_warnings(): with warnings.catch_warnings():
warnings.simplefilter('ignore') # suppress torch 1.9.0 max_pool2d() warning warnings.simplefilter("ignore") # suppress torch 1.9.0 max_pool2d() warning
y1 = self.m(x) y1 = self.m(x)
y2 = self.m(y1) y2 = self.m(y1)
return self.cv2(torch.cat((x, y1, y2, self.m(y2)), 1)) return self.cv2(torch.cat((x, y1, y2, self.m(y2)), 1))
@@ -278,9 +290,11 @@ class GhostBottleneck(nn.Module):
self.conv = nn.Sequential( self.conv = nn.Sequential(
GhostConv(c1, c_, 1, 1), # pw GhostConv(c1, c_, 1, 1), # pw
DWConv(c_, c_, k, s, act=False) if s == 2 else nn.Identity(), # dw DWConv(c_, c_, k, s, act=False) if s == 2 else nn.Identity(), # dw
GhostConv(c_, c2, 1, 1, act=False)) # pw-linear GhostConv(c_, c2, 1, 1, act=False),
self.shortcut = nn.Sequential(DWConv(c1, c1, k, s, act=False), Conv(c1, c2, 1, 1, ) # pw-linear
act=False)) if s == 2 else nn.Identity() self.shortcut = (
nn.Sequential(DWConv(c1, c1, k, s, act=False), Conv(c1, c2, 1, 1, act=False)) if s == 2 else nn.Identity()
)
def forward(self, x): def forward(self, x):
return self.conv(x) + self.shortcut(x) return self.conv(x) + self.shortcut(x)
@@ -309,9 +323,9 @@ class Expand(nn.Module):
def forward(self, x): def forward(self, x):
b, c, h, w = x.size() # assert C / s ** 2 == 0, 'Indivisible gain' b, c, h, w = x.size() # assert C / s ** 2 == 0, 'Indivisible gain'
s = self.gain s = self.gain
x = x.view(b, s, s, c // s ** 2, h, w) # x(1,2,2,16,80,80) x = x.view(b, s, s, c // s**2, h, w) # x(1,2,2,16,80,80)
x = x.permute(0, 3, 4, 1, 5, 2).contiguous() # x(1,16,80,2,80,2) x = x.permute(0, 3, 4, 1, 5, 2).contiguous() # x(1,16,80,2,80,2)
return x.view(b, c // s ** 2, h * s, w * s) # x(1,16,160,160) return x.view(b, c // s**2, h * s, w * s) # x(1,16,160,160)
class Concat(nn.Module): class Concat(nn.Module):
@@ -326,7 +340,7 @@ class Concat(nn.Module):
class DetectMultiBackend(nn.Module): class DetectMultiBackend(nn.Module):
# YOLOv5 MultiBackend class for python inference on various backends # YOLOv5 MultiBackend class for python inference on various backends
def __init__(self, weights='yolov5s.pt', device=torch.device('cpu'), dnn=False, data=None, fp16=False, fuse=True): def __init__(self, weights="yolov5s.pt", device=torch.device("cpu"), dnn=False, data=None, fp16=False, fuse=True):
# Usage: # Usage:
# PyTorch: weights = *.pt # PyTorch: weights = *.pt
# TorchScript: *.torchscript # TorchScript: *.torchscript
@@ -348,65 +362,68 @@ class DetectMultiBackend(nn.Module):
fp16 &= pt or jit or onnx or engine or triton # FP16 fp16 &= pt or jit or onnx or engine or triton # FP16
nhwc = coreml or saved_model or pb or tflite or edgetpu # BHWC formats (vs torch BCWH) nhwc = coreml or saved_model or pb or tflite or edgetpu # BHWC formats (vs torch BCWH)
stride = 32 # default stride stride = 32 # default stride
cuda = torch.cuda.is_available() and device.type != 'cpu' # use CUDA cuda = torch.cuda.is_available() and device.type != "cpu" # use CUDA
if not (pt or triton): if not (pt or triton):
w = attempt_download(w) # download if not local w = attempt_download(w) # download if not local
if pt: # PyTorch if pt: # PyTorch
model = attempt_load(weights if isinstance(weights, list) else w, device=device, inplace=True, fuse=fuse) model = attempt_load(weights if isinstance(weights, list) else w, device=device, inplace=True, fuse=fuse)
stride = max(int(model.stride.max()), 32) # model stride stride = max(int(model.stride.max()), 32) # model stride
names = model.module.names if hasattr(model, 'module') else model.names # get class names names = model.module.names if hasattr(model, "module") else model.names # get class names
model.half() if fp16 else model.float() model.half() if fp16 else model.float()
self.model = model # explicitly assign for to(), cpu(), cuda(), half() self.model = model # explicitly assign for to(), cpu(), cuda(), half()
elif jit: # TorchScript elif jit: # TorchScript
LOGGER.info(f'Loading {w} for TorchScript inference...') LOGGER.info(f"Loading {w} for TorchScript inference...")
extra_files = {'config.txt': ''} # model metadata extra_files = {"config.txt": ""} # model metadata
model = torch.jit.load(w, _extra_files=extra_files, map_location=device) model = torch.jit.load(w, _extra_files=extra_files, map_location=device)
model.half() if fp16 else model.float() model.half() if fp16 else model.float()
if extra_files['config.txt']: # load metadata dict if extra_files["config.txt"]: # load metadata dict
d = json.loads(extra_files['config.txt'], d = json.loads(
object_hook=lambda d: { extra_files["config.txt"],
int(k) if k.isdigit() else k: v object_hook=lambda d: {int(k) if k.isdigit() else k: v for k, v in d.items()},
for k, v in d.items()}) )
stride, names = int(d['stride']), d['names'] stride, names = int(d["stride"]), d["names"]
elif dnn: # ONNX OpenCV DNN elif dnn: # ONNX OpenCV DNN
LOGGER.info(f'Loading {w} for ONNX OpenCV DNN inference...') LOGGER.info(f"Loading {w} for ONNX OpenCV DNN inference...")
check_requirements('opencv-python>=4.5.4') check_requirements("opencv-python>=4.5.4")
net = cv2.dnn.readNetFromONNX(w) net = cv2.dnn.readNetFromONNX(w)
elif onnx: # ONNX Runtime elif onnx: # ONNX Runtime
LOGGER.info(f'Loading {w} for ONNX Runtime inference...') LOGGER.info(f"Loading {w} for ONNX Runtime inference...")
check_requirements(('onnx', 'onnxruntime-gpu' if cuda else 'onnxruntime')) check_requirements(("onnx", "onnxruntime-gpu" if cuda else "onnxruntime"))
import onnxruntime import onnxruntime
providers = ['CUDAExecutionProvider', 'CPUExecutionProvider'] if cuda else ['CPUExecutionProvider']
providers = ["CUDAExecutionProvider", "CPUExecutionProvider"] if cuda else ["CPUExecutionProvider"]
session = onnxruntime.InferenceSession(w, providers=providers) session = onnxruntime.InferenceSession(w, providers=providers)
output_names = [x.name for x in session.get_outputs()] output_names = [x.name for x in session.get_outputs()]
meta = session.get_modelmeta().custom_metadata_map # metadata meta = session.get_modelmeta().custom_metadata_map # metadata
if 'stride' in meta: if "stride" in meta:
stride, names = int(meta['stride']), eval(meta['names']) stride, names = int(meta["stride"]), eval(meta["names"])
elif xml: # OpenVINO elif xml: # OpenVINO
LOGGER.info(f'Loading {w} for OpenVINO inference...') LOGGER.info(f"Loading {w} for OpenVINO inference...")
check_requirements('openvino>=2023.0') # requires openvino-dev: https://pypi.org/project/openvino-dev/ check_requirements("openvino>=2023.0") # requires openvino-dev: https://pypi.org/project/openvino-dev/
from openvino.runtime import Core, Layout, get_batch from openvino.runtime import Core, Layout, get_batch
core = Core() core = Core()
if not Path(w).is_file(): # if not *.xml if not Path(w).is_file(): # if not *.xml
w = next(Path(w).glob('*.xml')) # get *.xml file from *_openvino_model dir w = next(Path(w).glob("*.xml")) # get *.xml file from *_openvino_model dir
ov_model = core.read_model(model=w, weights=Path(w).with_suffix('.bin')) ov_model = core.read_model(model=w, weights=Path(w).with_suffix(".bin"))
if ov_model.get_parameters()[0].get_layout().empty: if ov_model.get_parameters()[0].get_layout().empty:
ov_model.get_parameters()[0].set_layout(Layout('NCHW')) ov_model.get_parameters()[0].set_layout(Layout("NCHW"))
batch_dim = get_batch(ov_model) batch_dim = get_batch(ov_model)
if batch_dim.is_static: if batch_dim.is_static:
batch_size = batch_dim.get_length() batch_size = batch_dim.get_length()
ov_compiled_model = core.compile_model(ov_model, device_name='AUTO') # AUTO selects best available device ov_compiled_model = core.compile_model(ov_model, device_name="AUTO") # AUTO selects best available device
stride, names = self._load_metadata(Path(w).with_suffix('.yaml')) # load metadata stride, names = self._load_metadata(Path(w).with_suffix(".yaml")) # load metadata
elif engine: # TensorRT elif engine: # TensorRT
LOGGER.info(f'Loading {w} for TensorRT inference...') LOGGER.info(f"Loading {w} for TensorRT inference...")
import tensorrt as trt # https://developer.nvidia.com/nvidia-tensorrt-download import tensorrt as trt # https://developer.nvidia.com/nvidia-tensorrt-download
check_version(trt.__version__, '7.0.0', hard=True) # require tensorrt>=7.0.0
if device.type == 'cpu': check_version(trt.__version__, "7.0.0", hard=True) # require tensorrt>=7.0.0
device = torch.device('cuda:0') if device.type == "cpu":
Binding = namedtuple('Binding', ('name', 'dtype', 'shape', 'data', 'ptr')) device = torch.device("cuda:0")
Binding = namedtuple("Binding", ("name", "dtype", "shape", "data", "ptr"))
logger = trt.Logger(trt.Logger.INFO) logger = trt.Logger(trt.Logger.INFO)
with open(w, 'rb') as f, trt.Runtime(logger) as runtime: with open(w, "rb") as f, trt.Runtime(logger) as runtime:
model = runtime.deserialize_cuda_engine(f.read()) model = runtime.deserialize_cuda_engine(f.read())
context = model.create_execution_context() context = model.create_execution_context()
bindings = OrderedDict() bindings = OrderedDict()
@@ -428,22 +445,24 @@ class DetectMultiBackend(nn.Module):
im = torch.from_numpy(np.empty(shape, dtype=dtype)).to(device) im = torch.from_numpy(np.empty(shape, dtype=dtype)).to(device)
bindings[name] = Binding(name, dtype, shape, im, int(im.data_ptr())) bindings[name] = Binding(name, dtype, shape, im, int(im.data_ptr()))
binding_addrs = OrderedDict((n, d.ptr) for n, d in bindings.items()) binding_addrs = OrderedDict((n, d.ptr) for n, d in bindings.items())
batch_size = bindings['images'].shape[0] # if dynamic, this is instead max batch size batch_size = bindings["images"].shape[0] # if dynamic, this is instead max batch size
elif coreml: # CoreML elif coreml: # CoreML
LOGGER.info(f'Loading {w} for CoreML inference...') LOGGER.info(f"Loading {w} for CoreML inference...")
import coremltools as ct import coremltools as ct
model = ct.models.MLModel(w) model = ct.models.MLModel(w)
elif saved_model: # TF SavedModel elif saved_model: # TF SavedModel
LOGGER.info(f'Loading {w} for TensorFlow SavedModel inference...') LOGGER.info(f"Loading {w} for TensorFlow SavedModel inference...")
import tensorflow as tf import tensorflow as tf
keras = False # assume TF1 saved_model keras = False # assume TF1 saved_model
model = tf.keras.models.load_model(w) if keras else tf.saved_model.load(w) model = tf.keras.models.load_model(w) if keras else tf.saved_model.load(w)
elif pb: # GraphDef https://www.tensorflow.org/guide/migrate#a_graphpb_or_graphpbtxt elif pb: # GraphDef https://www.tensorflow.org/guide/migrate#a_graphpb_or_graphpbtxt
LOGGER.info(f'Loading {w} for TensorFlow GraphDef inference...') LOGGER.info(f"Loading {w} for TensorFlow GraphDef inference...")
import tensorflow as tf import tensorflow as tf
def wrap_frozen_graph(gd, inputs, outputs): def wrap_frozen_graph(gd, inputs, outputs):
x = tf.compat.v1.wrap_function(lambda: tf.compat.v1.import_graph_def(gd, name=''), []) # wrapped x = tf.compat.v1.wrap_function(lambda: tf.compat.v1.import_graph_def(gd, name=""), []) # wrapped
ge = x.graph.as_graph_element ge = x.graph.as_graph_element
return x.prune(tf.nest.map_structure(ge, inputs), tf.nest.map_structure(ge, outputs)) return x.prune(tf.nest.map_structure(ge, inputs), tf.nest.map_structure(ge, outputs))
@@ -452,46 +471,50 @@ class DetectMultiBackend(nn.Module):
for node in gd.node: # tensorflow.core.framework.node_def_pb2.NodeDef for node in gd.node: # tensorflow.core.framework.node_def_pb2.NodeDef
name_list.append(node.name) name_list.append(node.name)
input_list.extend(node.input) input_list.extend(node.input)
return sorted(f'{x}:0' for x in list(set(name_list) - set(input_list)) if not x.startswith('NoOp')) return sorted(f"{x}:0" for x in list(set(name_list) - set(input_list)) if not x.startswith("NoOp"))
gd = tf.Graph().as_graph_def() # TF GraphDef gd = tf.Graph().as_graph_def() # TF GraphDef
with open(w, 'rb') as f: with open(w, "rb") as f:
gd.ParseFromString(f.read()) gd.ParseFromString(f.read())
frozen_func = wrap_frozen_graph(gd, inputs='x:0', outputs=gd_outputs(gd)) frozen_func = wrap_frozen_graph(gd, inputs="x:0", outputs=gd_outputs(gd))
elif tflite or edgetpu: # https://www.tensorflow.org/lite/guide/python#install_tensorflow_lite_for_python elif tflite or edgetpu: # https://www.tensorflow.org/lite/guide/python#install_tensorflow_lite_for_python
try: # https://coral.ai/docs/edgetpu/tflite-python/#update-existing-tf-lite-code-for-the-edge-tpu try: # https://coral.ai/docs/edgetpu/tflite-python/#update-existing-tf-lite-code-for-the-edge-tpu
from tflite_runtime.interpreter import Interpreter, load_delegate from tflite_runtime.interpreter import Interpreter, load_delegate
except ImportError: except ImportError:
import tensorflow as tf import tensorflow as tf
Interpreter, load_delegate = tf.lite.Interpreter, tf.lite.experimental.load_delegate,
Interpreter, load_delegate = (
tf.lite.Interpreter,
tf.lite.experimental.load_delegate,
)
if edgetpu: # TF Edge TPU https://coral.ai/software/#edgetpu-runtime if edgetpu: # TF Edge TPU https://coral.ai/software/#edgetpu-runtime
LOGGER.info(f'Loading {w} for TensorFlow Lite Edge TPU inference...') LOGGER.info(f"Loading {w} for TensorFlow Lite Edge TPU inference...")
delegate = { delegate = {"Linux": "libedgetpu.so.1", "Darwin": "libedgetpu.1.dylib", "Windows": "edgetpu.dll"}[
'Linux': 'libedgetpu.so.1', platform.system()
'Darwin': 'libedgetpu.1.dylib', ]
'Windows': 'edgetpu.dll'}[platform.system()]
interpreter = Interpreter(model_path=w, experimental_delegates=[load_delegate(delegate)]) interpreter = Interpreter(model_path=w, experimental_delegates=[load_delegate(delegate)])
else: # TFLite else: # TFLite
LOGGER.info(f'Loading {w} for TensorFlow Lite inference...') LOGGER.info(f"Loading {w} for TensorFlow Lite inference...")
interpreter = Interpreter(model_path=w) # load TFLite model interpreter = Interpreter(model_path=w) # load TFLite model
interpreter.allocate_tensors() # allocate interpreter.allocate_tensors() # allocate
input_details = interpreter.get_input_details() # inputs input_details = interpreter.get_input_details() # inputs
output_details = interpreter.get_output_details() # outputs output_details = interpreter.get_output_details() # outputs
# load metadata # load metadata
with contextlib.suppress(zipfile.BadZipFile): with contextlib.suppress(zipfile.BadZipFile):
with zipfile.ZipFile(w, 'r') as model: with zipfile.ZipFile(w, "r") as model:
meta_file = model.namelist()[0] meta_file = model.namelist()[0]
meta = ast.literal_eval(model.read(meta_file).decode('utf-8')) meta = ast.literal_eval(model.read(meta_file).decode("utf-8"))
stride, names = int(meta['stride']), meta['names'] stride, names = int(meta["stride"]), meta["names"]
elif tfjs: # TF.js elif tfjs: # TF.js
raise NotImplementedError('ERROR: YOLOv5 TF.js inference is not supported') raise NotImplementedError("ERROR: YOLOv5 TF.js inference is not supported")
elif paddle: # PaddlePaddle elif paddle: # PaddlePaddle
LOGGER.info(f'Loading {w} for PaddlePaddle inference...') LOGGER.info(f"Loading {w} for PaddlePaddle inference...")
check_requirements('paddlepaddle-gpu' if cuda else 'paddlepaddle') check_requirements("paddlepaddle-gpu" if cuda else "paddlepaddle")
import paddle.inference as pdi import paddle.inference as pdi
if not Path(w).is_file(): # if not *.pdmodel if not Path(w).is_file(): # if not *.pdmodel
w = next(Path(w).rglob('*.pdmodel')) # get *.pdmodel file from *_paddle_model dir w = next(Path(w).rglob("*.pdmodel")) # get *.pdmodel file from *_paddle_model dir
weights = Path(w).with_suffix('.pdiparams') weights = Path(w).with_suffix(".pdiparams")
config = pdi.Config(str(w), str(weights)) config = pdi.Config(str(w), str(weights))
if cuda: if cuda:
config.enable_use_gpu(memory_pool_init_size_mb=2048, device_id=0) config.enable_use_gpu(memory_pool_init_size_mb=2048, device_id=0)
@@ -499,19 +522,20 @@ class DetectMultiBackend(nn.Module):
input_handle = predictor.get_input_handle(predictor.get_input_names()[0]) input_handle = predictor.get_input_handle(predictor.get_input_names()[0])
output_names = predictor.get_output_names() output_names = predictor.get_output_names()
elif triton: # NVIDIA Triton Inference Server elif triton: # NVIDIA Triton Inference Server
LOGGER.info(f'Using {w} as Triton Inference Server...') LOGGER.info(f"Using {w} as Triton Inference Server...")
check_requirements('tritonclient[all]') check_requirements("tritonclient[all]")
from utils.triton import TritonRemoteModel from utils.triton import TritonRemoteModel
model = TritonRemoteModel(url=w) model = TritonRemoteModel(url=w)
nhwc = model.runtime.startswith('tensorflow') nhwc = model.runtime.startswith("tensorflow")
else: else:
raise NotImplementedError(f'ERROR: {w} is not a supported format') raise NotImplementedError(f"ERROR: {w} is not a supported format")
# class names # class names
if 'names' not in locals(): if "names" not in locals():
names = yaml_load(data)['names'] if data else {i: f'class{i}' for i in range(999)} names = yaml_load(data)["names"] if data else {i: f"class{i}" for i in range(999)}
if names[0] == 'n01440764' and len(names) == 1000: # ImageNet if names[0] == "n01440764" and len(names) == 1000: # ImageNet
names = yaml_load(ROOT / 'data/ImageNet.yaml')['names'] # human-readable names names = yaml_load(ROOT / "data/ImageNet.yaml")["names"] # human-readable names
self.__dict__.update(locals()) # assign all variables to self self.__dict__.update(locals()) # assign all variables to self
@@ -538,26 +562,26 @@ class DetectMultiBackend(nn.Module):
im = im.cpu().numpy() # FP32 im = im.cpu().numpy() # FP32
y = list(self.ov_compiled_model(im).values()) y = list(self.ov_compiled_model(im).values())
elif self.engine: # TensorRT elif self.engine: # TensorRT
if self.dynamic and im.shape != self.bindings['images'].shape: if self.dynamic and im.shape != self.bindings["images"].shape:
i = self.model.get_binding_index('images') i = self.model.get_binding_index("images")
self.context.set_binding_shape(i, im.shape) # reshape if dynamic self.context.set_binding_shape(i, im.shape) # reshape if dynamic
self.bindings['images'] = self.bindings['images']._replace(shape=im.shape) self.bindings["images"] = self.bindings["images"]._replace(shape=im.shape)
for name in self.output_names: for name in self.output_names:
i = self.model.get_binding_index(name) i = self.model.get_binding_index(name)
self.bindings[name].data.resize_(tuple(self.context.get_binding_shape(i))) self.bindings[name].data.resize_(tuple(self.context.get_binding_shape(i)))
s = self.bindings['images'].shape s = self.bindings["images"].shape
assert im.shape == s, f"input size {im.shape} {'>' if self.dynamic else 'not equal to'} max model size {s}" assert im.shape == s, f"input size {im.shape} {'>' if self.dynamic else 'not equal to'} max model size {s}"
self.binding_addrs['images'] = int(im.data_ptr()) self.binding_addrs["images"] = int(im.data_ptr())
self.context.execute_v2(list(self.binding_addrs.values())) self.context.execute_v2(list(self.binding_addrs.values()))
y = [self.bindings[x].data for x in sorted(self.output_names)] y = [self.bindings[x].data for x in sorted(self.output_names)]
elif self.coreml: # CoreML elif self.coreml: # CoreML
im = im.cpu().numpy() im = im.cpu().numpy()
im = Image.fromarray((im[0] * 255).astype('uint8')) im = Image.fromarray((im[0] * 255).astype("uint8"))
# im = im.resize((192, 320), Image.BILINEAR) # im = im.resize((192, 320), Image.BILINEAR)
y = self.model.predict({'image': im}) # coordinates are xywh normalized y = self.model.predict({"image": im}) # coordinates are xywh normalized
if 'confidence' in y: if "confidence" in y:
box = xywh2xyxy(y['coordinates'] * [[w, h, w, h]]) # xyxy pixels box = xywh2xyxy(y["coordinates"] * [[w, h, w, h]]) # xyxy pixels
conf, cls = y['confidence'].max(1), y['confidence'].argmax(1).astype(np.float) conf, cls = y["confidence"].max(1), y["confidence"].argmax(1).astype(np.float)
y = np.concatenate((box, conf.reshape(-1, 1), cls.reshape(-1, 1)), 1) y = np.concatenate((box, conf.reshape(-1, 1), cls.reshape(-1, 1)), 1)
else: else:
y = list(reversed(y.values())) # reversed for segmentation models (pred, proto) y = list(reversed(y.values())) # reversed for segmentation models (pred, proto)
@@ -576,17 +600,17 @@ class DetectMultiBackend(nn.Module):
y = self.frozen_func(x=self.tf.constant(im)) y = self.frozen_func(x=self.tf.constant(im))
else: # Lite or Edge TPU else: # Lite or Edge TPU
input = self.input_details[0] input = self.input_details[0]
int8 = input['dtype'] == np.uint8 # is TFLite quantized uint8 model int8 = input["dtype"] == np.uint8 # is TFLite quantized uint8 model
if int8: if int8:
scale, zero_point = input['quantization'] scale, zero_point = input["quantization"]
im = (im / scale + zero_point).astype(np.uint8) # de-scale im = (im / scale + zero_point).astype(np.uint8) # de-scale
self.interpreter.set_tensor(input['index'], im) self.interpreter.set_tensor(input["index"], im)
self.interpreter.invoke() self.interpreter.invoke()
y = [] y = []
for output in self.output_details: for output in self.output_details:
x = self.interpreter.get_tensor(output['index']) x = self.interpreter.get_tensor(output["index"])
if int8: if int8:
scale, zero_point = output['quantization'] scale, zero_point = output["quantization"]
x = (x.astype(np.float32) - zero_point) * scale # re-scale x = (x.astype(np.float32) - zero_point) * scale # re-scale
y.append(x) y.append(x)
y = [x if isinstance(x, np.ndarray) else x.numpy() for x in y] y = [x if isinstance(x, np.ndarray) else x.numpy() for x in y]
@@ -603,32 +627,33 @@ class DetectMultiBackend(nn.Module):
def warmup(self, imgsz=(1, 3, 640, 640)): def warmup(self, imgsz=(1, 3, 640, 640)):
# Warmup model by running inference once # Warmup model by running inference once
warmup_types = self.pt, self.jit, self.onnx, self.engine, self.saved_model, self.pb, self.triton warmup_types = self.pt, self.jit, self.onnx, self.engine, self.saved_model, self.pb, self.triton
if any(warmup_types) and (self.device.type != 'cpu' or self.triton): if any(warmup_types) and (self.device.type != "cpu" or self.triton):
im = torch.empty(*imgsz, dtype=torch.half if self.fp16 else torch.float, device=self.device) # input im = torch.empty(*imgsz, dtype=torch.half if self.fp16 else torch.float, device=self.device) # input
for _ in range(2 if self.jit else 1): # for _ in range(2 if self.jit else 1): #
self.forward(im) # warmup self.forward(im) # warmup
@staticmethod @staticmethod
def _model_type(p='path/to/model.pt'): def _model_type(p="path/to/model.pt"):
# Return model type from model path, i.e. path='path/to/model.onnx' -> type=onnx # Return model type from model path, i.e. path='path/to/model.onnx' -> type=onnx
# types = [pt, jit, onnx, xml, engine, coreml, saved_model, pb, tflite, edgetpu, tfjs, paddle] # types = [pt, jit, onnx, xml, engine, coreml, saved_model, pb, tflite, edgetpu, tfjs, paddle]
from export import export_formats from export import export_formats
from utils.downloads import is_url from utils.downloads import is_url
sf = list(export_formats().Suffix) # export suffixes sf = list(export_formats().Suffix) # export suffixes
if not is_url(p, check=False): if not is_url(p, check=False):
check_suffix(p, sf) # checks check_suffix(p, sf) # checks
url = urlparse(p) # if url may be Triton inference server url = urlparse(p) # if url may be Triton inference server
types = [s in Path(p).name for s in sf] types = [s in Path(p).name for s in sf]
types[8] &= not types[9] # tflite &= not edgetpu types[8] &= not types[9] # tflite &= not edgetpu
triton = not any(types) and all([any(s in url.scheme for s in ['http', 'grpc']), url.netloc]) triton = not any(types) and all([any(s in url.scheme for s in ["http", "grpc"]), url.netloc])
return types + [triton] return types + [triton]
@staticmethod @staticmethod
def _load_metadata(f=Path('path/to/meta.yaml')): def _load_metadata(f=Path("path/to/meta.yaml")):
# Load metadata from meta.yaml if it exists # Load metadata from meta.yaml if it exists
if f.exists(): if f.exists():
d = yaml_load(f) d = yaml_load(f)
return d['stride'], d['names'] # assign stride, names return d["stride"], d["names"] # assign stride, names
return None, None return None, None
@@ -645,8 +670,8 @@ class AutoShape(nn.Module):
def __init__(self, model, verbose=True): def __init__(self, model, verbose=True):
super().__init__() super().__init__()
if verbose: if verbose:
LOGGER.info('Adding AutoShape... ') LOGGER.info("Adding AutoShape... ")
copy_attr(self, model, include=('yaml', 'nc', 'hyp', 'names', 'stride', 'abc'), exclude=()) # copy attributes copy_attr(self, model, include=("yaml", "nc", "hyp", "names", "stride", "abc"), exclude=()) # copy attributes
self.dmb = isinstance(model, DetectMultiBackend) # DetectMultiBackend() instance self.dmb = isinstance(model, DetectMultiBackend) # DetectMultiBackend() instance
self.pt = not self.dmb or model.pt # PyTorch model self.pt = not self.dmb or model.pt # PyTorch model
self.model = model.eval() self.model = model.eval()
@@ -682,7 +707,7 @@ class AutoShape(nn.Module):
if isinstance(size, int): # expand if isinstance(size, int): # expand
size = (size, size) size = (size, size)
p = next(self.model.parameters()) if self.pt else torch.empty(1, device=self.model.device) # param p = next(self.model.parameters()) if self.pt else torch.empty(1, device=self.model.device) # param
autocast = self.amp and (p.device.type != 'cpu') # Automatic Mixed Precision (AMP) inference autocast = self.amp and (p.device.type != "cpu") # Automatic Mixed Precision (AMP) inference
if isinstance(ims, torch.Tensor): # torch if isinstance(ims, torch.Tensor): # torch
with amp.autocast(autocast): with amp.autocast(autocast):
return self.model(ims.to(p.device).type_as(p), augment=augment) # inference return self.model(ims.to(p.device).type_as(p), augment=augment) # inference
@@ -691,13 +716,13 @@ class AutoShape(nn.Module):
n, ims = (len(ims), list(ims)) if isinstance(ims, (list, tuple)) else (1, [ims]) # number, list of images n, ims = (len(ims), list(ims)) if isinstance(ims, (list, tuple)) else (1, [ims]) # number, list of images
shape0, shape1, files = [], [], [] # image and inference shapes, filenames shape0, shape1, files = [], [], [] # image and inference shapes, filenames
for i, im in enumerate(ims): for i, im in enumerate(ims):
f = f'image{i}' # filename f = f"image{i}" # filename
if isinstance(im, (str, Path)): # filename or uri if isinstance(im, (str, Path)): # filename or uri
im, f = Image.open(requests.get(im, stream=True).raw if str(im).startswith('http') else im), im im, f = Image.open(requests.get(im, stream=True).raw if str(im).startswith("http") else im), im
im = np.asarray(exif_transpose(im)) im = np.asarray(exif_transpose(im))
elif isinstance(im, Image.Image): # PIL Image elif isinstance(im, Image.Image): # PIL Image
im, f = np.asarray(exif_transpose(im)), getattr(im, 'filename', f) or f im, f = np.asarray(exif_transpose(im)), getattr(im, "filename", f) or f
files.append(Path(f).with_suffix('.jpg').name) files.append(Path(f).with_suffix(".jpg").name)
if im.shape[0] < 5: # image in CHW if im.shape[0] < 5: # image in CHW
im = im.transpose((1, 2, 0)) # reverse dataloader .transpose(2, 0, 1) im = im.transpose((1, 2, 0)) # reverse dataloader .transpose(2, 0, 1)
im = im[..., :3] if im.ndim == 3 else cv2.cvtColor(im, cv2.COLOR_GRAY2BGR) # enforce 3ch input im = im[..., :3] if im.ndim == 3 else cv2.cvtColor(im, cv2.COLOR_GRAY2BGR) # enforce 3ch input
@@ -718,13 +743,15 @@ class AutoShape(nn.Module):
# Post-process # Post-process
with dt[2]: with dt[2]:
y = non_max_suppression(y if self.dmb else y[0], y = non_max_suppression(
self.conf, y if self.dmb else y[0],
self.iou, self.conf,
self.classes, self.iou,
self.agnostic, self.classes,
self.multi_label, self.agnostic,
max_det=self.max_det) # NMS self.multi_label,
max_det=self.max_det,
) # NMS
for i in range(n): for i in range(n):
scale_boxes(shape1, y[i][:, :4], shape0[i]) scale_boxes(shape1, y[i][:, :4], shape0[i])
@@ -747,40 +774,44 @@ class Detections:
self.xyxyn = [x / g for x, g in zip(self.xyxy, gn)] # xyxy normalized self.xyxyn = [x / g for x, g in zip(self.xyxy, gn)] # xyxy normalized
self.xywhn = [x / g for x, g in zip(self.xywh, gn)] # xywh normalized self.xywhn = [x / g for x, g in zip(self.xywh, gn)] # xywh normalized
self.n = len(self.pred) # number of images (batch size) self.n = len(self.pred) # number of images (batch size)
self.t = tuple(x.t / self.n * 1E3 for x in times) # timestamps (ms) self.t = tuple(x.t / self.n * 1e3 for x in times) # timestamps (ms)
self.s = tuple(shape) # inference BCHW shape self.s = tuple(shape) # inference BCHW shape
def _run(self, pprint=False, show=False, save=False, crop=False, render=False, labels=True, save_dir=Path('')): def _run(self, pprint=False, show=False, save=False, crop=False, render=False, labels=True, save_dir=Path("")):
s, crops = '', [] s, crops = "", []
for i, (im, pred) in enumerate(zip(self.ims, self.pred)): for i, (im, pred) in enumerate(zip(self.ims, self.pred)):
s += f'\nimage {i + 1}/{len(self.pred)}: {im.shape[0]}x{im.shape[1]} ' # string s += f"\nimage {i + 1}/{len(self.pred)}: {im.shape[0]}x{im.shape[1]} " # string
if pred.shape[0]: if pred.shape[0]:
for c in pred[:, -1].unique(): for c in pred[:, -1].unique():
n = (pred[:, -1] == c).sum() # detections per class n = (pred[:, -1] == c).sum() # detections per class
s += f"{n} {self.names[int(c)]}{'s' * (n > 1)}, " # add to string s += f"{n} {self.names[int(c)]}{'s' * (n > 1)}, " # add to string
s = s.rstrip(', ') s = s.rstrip(", ")
if show or save or render or crop: if show or save or render or crop:
annotator = Annotator(im, example=str(self.names)) annotator = Annotator(im, example=str(self.names))
for *box, conf, cls in reversed(pred): # xyxy, confidence, class for *box, conf, cls in reversed(pred): # xyxy, confidence, class
label = f'{self.names[int(cls)]} {conf:.2f}' label = f"{self.names[int(cls)]} {conf:.2f}"
if crop: if crop:
file = save_dir / 'crops' / self.names[int(cls)] / self.files[i] if save else None file = save_dir / "crops" / self.names[int(cls)] / self.files[i] if save else None
crops.append({ crops.append(
'box': box, {
'conf': conf, "box": box,
'cls': cls, "conf": conf,
'label': label, "cls": cls,
'im': save_one_box(box, im, file=file, save=save)}) "label": label,
"im": save_one_box(box, im, file=file, save=save),
}
)
else: # all others else: # all others
annotator.box_label(box, label if labels else '', color=colors(cls)) annotator.box_label(box, label if labels else "", color=colors(cls))
im = annotator.im im = annotator.im
else: else:
s += '(no detections)' s += "(no detections)"
im = Image.fromarray(im.astype(np.uint8)) if isinstance(im, np.ndarray) else im # from np im = Image.fromarray(im.astype(np.uint8)) if isinstance(im, np.ndarray) else im # from np
if show: if show:
if is_jupyter(): if is_jupyter():
from IPython.display import display from IPython.display import display
display(im) display(im)
else: else:
im.show(self.files[i]) im.show(self.files[i])
@@ -792,22 +823,22 @@ class Detections:
if render: if render:
self.ims[i] = np.asarray(im) self.ims[i] = np.asarray(im)
if pprint: if pprint:
s = s.lstrip('\n') s = s.lstrip("\n")
return f'{s}\nSpeed: %.1fms pre-process, %.1fms inference, %.1fms NMS per image at shape {self.s}' % self.t return f"{s}\nSpeed: %.1fms pre-process, %.1fms inference, %.1fms NMS per image at shape {self.s}" % self.t
if crop: if crop:
if save: if save:
LOGGER.info(f'Saved results to {save_dir}\n') LOGGER.info(f"Saved results to {save_dir}\n")
return crops return crops
@TryExcept('Showing images is not supported in this environment') @TryExcept("Showing images is not supported in this environment")
def show(self, labels=True): def show(self, labels=True):
self._run(show=True, labels=labels) # show results self._run(show=True, labels=labels) # show results
def save(self, labels=True, save_dir='runs/detect/exp', exist_ok=False): def save(self, labels=True, save_dir="runs/detect/exp", exist_ok=False):
save_dir = increment_path(save_dir, exist_ok, mkdir=True) # increment save_dir save_dir = increment_path(save_dir, exist_ok, mkdir=True) # increment save_dir
self._run(save=True, labels=labels, save_dir=save_dir) # save results self._run(save=True, labels=labels, save_dir=save_dir) # save results
def crop(self, save=True, save_dir='runs/detect/exp', exist_ok=False): def crop(self, save=True, save_dir="runs/detect/exp", exist_ok=False):
save_dir = increment_path(save_dir, exist_ok, mkdir=True) if save else None save_dir = increment_path(save_dir, exist_ok, mkdir=True) if save else None
return self._run(crop=True, save=save, save_dir=save_dir) # crop results return self._run(crop=True, save=save, save_dir=save_dir) # crop results
@@ -818,9 +849,9 @@ class Detections:
def pandas(self): def pandas(self):
# return detections as pandas DataFrames, i.e. print(results.pandas().xyxy[0]) # return detections as pandas DataFrames, i.e. print(results.pandas().xyxy[0])
new = copy(self) # return copy new = copy(self) # return copy
ca = 'xmin', 'ymin', 'xmax', 'ymax', 'confidence', 'class', 'name' # xyxy columns ca = "xmin", "ymin", "xmax", "ymax", "confidence", "class", "name" # xyxy columns
cb = 'xcenter', 'ycenter', 'width', 'height', 'confidence', 'class', 'name' # xywh columns cb = "xcenter", "ycenter", "width", "height", "confidence", "class", "name" # xywh columns
for k, c in zip(['xyxy', 'xyxyn', 'xywh', 'xywhn'], [ca, ca, cb, cb]): for k, c in zip(["xyxy", "xyxyn", "xywh", "xywhn"], [ca, ca, cb, cb]):
a = [[x[:5] + [int(x[5]), self.names[int(x[5])]] for x in x.tolist()] for x in getattr(self, k)] # update a = [[x[:5] + [int(x[5]), self.names[int(x[5])]] for x in x.tolist()] for x in getattr(self, k)] # update
setattr(new, k, [pd.DataFrame(x, columns=c) for x in a]) setattr(new, k, [pd.DataFrame(x, columns=c) for x in a])
return new return new
@@ -844,7 +875,7 @@ class Detections:
return self._run(pprint=True) # print results return self._run(pprint=True) # print results
def __repr__(self): def __repr__(self):
return f'YOLOv5 {self.__class__} instance\n' + self.__str__() return f"YOLOv5 {self.__class__} instance\n" + self.__str__()
class Proto(nn.Module): class Proto(nn.Module):
@@ -852,7 +883,7 @@ class Proto(nn.Module):
def __init__(self, c1, c_=256, c2=32): # ch_in, number of protos, number of masks def __init__(self, c1, c_=256, c2=32): # ch_in, number of protos, number of masks
super().__init__() super().__init__()
self.cv1 = Conv(c1, c_, k=3) self.cv1 = Conv(c1, c_, k=3)
self.upsample = nn.Upsample(scale_factor=2, mode='nearest') self.upsample = nn.Upsample(scale_factor=2, mode="nearest")
self.cv2 = Conv(c_, c_, k=3) self.cv2 = Conv(c_, c_, k=3)
self.cv3 = Conv(c_, c2) self.cv3 = Conv(c_, c2)
@@ -862,14 +893,9 @@ class Proto(nn.Module):
class Classify(nn.Module): class Classify(nn.Module):
# YOLOv5 classification head, i.e. x(b,c1,20,20) to x(b,c2) # YOLOv5 classification head, i.e. x(b,c1,20,20) to x(b,c2)
def __init__(self, def __init__(
c1, self, c1, c2, k=1, s=1, p=None, g=1, dropout_p=0.0
c2, ): # ch_in, ch_out, kernel, stride, padding, groups, dropout probability
k=1,
s=1,
p=None,
g=1,
dropout_p=0.0): # ch_in, ch_out, kernel, stride, padding, groups, dropout probability
super().__init__() super().__init__()
c_ = 1280 # efficientnet_b0 size c_ = 1280 # efficientnet_b0 size
self.conv = Conv(c1, c_, k, s, autopad(k, p), g) self.conv = Conv(c1, c_, k, s, autopad(k, p), g)
+17 -18
View File
@@ -1,7 +1,5 @@
# YOLOv5 🚀 by Ultralytics, AGPL-3.0 license # YOLOv5 🚀 by Ultralytics, AGPL-3.0 license
""" """Experimental modules."""
Experimental modules
"""
import math import math
import numpy as np import numpy as np
@@ -38,7 +36,7 @@ class MixConv2d(nn.Module):
super().__init__() super().__init__()
n = len(k) # number of convolutions n = len(k) # number of convolutions
if equal_ch: # equal c_ per group if equal_ch: # equal c_ per group
i = torch.linspace(0, n - 1E-6, c2).floor() # c2 indices i = torch.linspace(0, n - 1e-6, c2).floor() # c2 indices
c_ = [(i == g).sum() for g in range(n)] # intermediate channels c_ = [(i == g).sum() for g in range(n)] # intermediate channels
else: # equal weight.numel() per group else: # equal weight.numel() per group
b = [c2] + [0] * n b = [c2] + [0] * n
@@ -48,8 +46,9 @@ class MixConv2d(nn.Module):
a[0] = 1 a[0] = 1
c_ = np.linalg.lstsq(a, b, rcond=None)[0].round() # solve for equal weight indices, ax = b c_ = np.linalg.lstsq(a, b, rcond=None)[0].round() # solve for equal weight indices, ax = b
self.m = nn.ModuleList([ self.m = nn.ModuleList(
nn.Conv2d(c1, int(c_), k, s, k // 2, groups=math.gcd(c1, int(c_)), bias=False) for k, c_ in zip(k, c_)]) [nn.Conv2d(c1, int(c_), k, s, k // 2, groups=math.gcd(c1, int(c_)), bias=False) for k, c_ in zip(k, c_)]
)
self.bn = nn.BatchNorm2d(c2) self.bn = nn.BatchNorm2d(c2)
self.act = nn.SiLU() self.act = nn.SiLU()
@@ -76,16 +75,16 @@ def attempt_load(weights, device=None, inplace=True, fuse=True):
model = Ensemble() model = Ensemble()
for w in weights if isinstance(weights, list) else [weights]: for w in weights if isinstance(weights, list) else [weights]:
ckpt = torch.load(attempt_download(w), map_location='cpu') # load ckpt = torch.load(attempt_download(w), map_location="cpu") # load
ckpt = (ckpt.get('ema') or ckpt['model']).to(device).float() # FP32 model ckpt = (ckpt.get("ema") or ckpt["model"]).to(device).float() # FP32 model
# Model compatibility updates # Model compatibility updates
if not hasattr(ckpt, 'stride'): if not hasattr(ckpt, "stride"):
ckpt.stride = torch.tensor([32.]) ckpt.stride = torch.tensor([32.0])
if hasattr(ckpt, 'names') and isinstance(ckpt.names, (list, tuple)): if hasattr(ckpt, "names") and isinstance(ckpt.names, (list, tuple)):
ckpt.names = dict(enumerate(ckpt.names)) # convert to dict ckpt.names = dict(enumerate(ckpt.names)) # convert to dict
model.append(ckpt.fuse().eval() if fuse and hasattr(ckpt, 'fuse') else ckpt.eval()) # model in eval mode model.append(ckpt.fuse().eval() if fuse and hasattr(ckpt, "fuse") else ckpt.eval()) # model in eval mode
# Module updates # Module updates
for m in model.modules(): for m in model.modules():
@@ -93,9 +92,9 @@ def attempt_load(weights, device=None, inplace=True, fuse=True):
if t in (nn.Hardswish, nn.LeakyReLU, nn.ReLU, nn.ReLU6, nn.SiLU, Detect, Model): if t in (nn.Hardswish, nn.LeakyReLU, nn.ReLU, nn.ReLU6, nn.SiLU, Detect, Model):
m.inplace = inplace m.inplace = inplace
if t is Detect and not isinstance(m.anchor_grid, list): if t is Detect and not isinstance(m.anchor_grid, list):
delattr(m, 'anchor_grid') delattr(m, "anchor_grid")
setattr(m, 'anchor_grid', [torch.zeros(1)] * m.nl) setattr(m, "anchor_grid", [torch.zeros(1)] * m.nl)
elif t is nn.Upsample and not hasattr(m, 'recompute_scale_factor'): elif t is nn.Upsample and not hasattr(m, "recompute_scale_factor"):
m.recompute_scale_factor = None # torch 1.11.0 compatibility m.recompute_scale_factor = None # torch 1.11.0 compatibility
# Return model # Return model
@@ -103,9 +102,9 @@ def attempt_load(weights, device=None, inplace=True, fuse=True):
return model[-1] return model[-1]
# Return detection ensemble # Return detection ensemble
print(f'Ensemble created with {weights}\n') print(f"Ensemble created with {weights}\n")
for k in 'names', 'nc', 'yaml': for k in "names", "nc", "yaml":
setattr(model, k, getattr(model[0], k)) setattr(model, k, getattr(model[0], k))
model.stride = model[torch.argmax(torch.tensor([m.stride.max() for m in model])).int()].stride # max stride model.stride = model[torch.argmax(torch.tensor([m.stride.max() for m in model])).int()].stride # max stride
assert all(model[0].nc == m.nc for m in model), f'Models have different class counts: {[m.nc for m in model]}' assert all(model[0].nc == m.nc for m in model), f"Models have different class counts: {[m.nc for m in model]}"
return model return model
+149 -103
View File
@@ -27,8 +27,21 @@ import torch
import torch.nn as nn import torch.nn as nn
from tensorflow import keras from tensorflow import keras
from models.common import (C3, SPP, SPPF, Bottleneck, BottleneckCSP, C3x, Concat, Conv, CrossConv, DWConv, from models.common import (
DWConvTranspose2d, Focus, autopad) C3,
SPP,
SPPF,
Bottleneck,
BottleneckCSP,
C3x,
Concat,
Conv,
CrossConv,
DWConv,
DWConvTranspose2d,
Focus,
autopad,
)
from models.experimental import MixConv2d, attempt_load from models.experimental import MixConv2d, attempt_load
from models.yolo import Detect, Segment from models.yolo import Detect, Segment
from utils.activations import SiLU from utils.activations import SiLU
@@ -44,7 +57,8 @@ class TFBN(keras.layers.Layer):
gamma_initializer=keras.initializers.Constant(w.weight.numpy()), gamma_initializer=keras.initializers.Constant(w.weight.numpy()),
moving_mean_initializer=keras.initializers.Constant(w.running_mean.numpy()), moving_mean_initializer=keras.initializers.Constant(w.running_mean.numpy()),
moving_variance_initializer=keras.initializers.Constant(w.running_var.numpy()), moving_variance_initializer=keras.initializers.Constant(w.running_var.numpy()),
epsilon=w.eps) epsilon=w.eps,
)
def call(self, inputs): def call(self, inputs):
return self.bn(inputs) return self.bn(inputs)
@@ -60,7 +74,7 @@ class TFPad(keras.layers.Layer):
self.pad = tf.constant([[0, 0], [pad[0], pad[0]], [pad[1], pad[1]], [0, 0]]) self.pad = tf.constant([[0, 0], [pad[0], pad[0]], [pad[1], pad[1]], [0, 0]])
def call(self, inputs): def call(self, inputs):
return tf.pad(inputs, self.pad, mode='constant', constant_values=0) return tf.pad(inputs, self.pad, mode="constant", constant_values=0)
class TFConv(keras.layers.Layer): class TFConv(keras.layers.Layer):
@@ -75,12 +89,13 @@ class TFConv(keras.layers.Layer):
filters=c2, filters=c2,
kernel_size=k, kernel_size=k,
strides=s, strides=s,
padding='SAME' if s == 1 else 'VALID', padding="SAME" if s == 1 else "VALID",
use_bias=not hasattr(w, 'bn'), use_bias=not hasattr(w, "bn"),
kernel_initializer=keras.initializers.Constant(w.conv.weight.permute(2, 3, 1, 0).numpy()), kernel_initializer=keras.initializers.Constant(w.conv.weight.permute(2, 3, 1, 0).numpy()),
bias_initializer='zeros' if hasattr(w, 'bn') else keras.initializers.Constant(w.conv.bias.numpy())) bias_initializer="zeros" if hasattr(w, "bn") else keras.initializers.Constant(w.conv.bias.numpy()),
)
self.conv = conv if s == 1 else keras.Sequential([TFPad(autopad(k, p)), conv]) self.conv = conv if s == 1 else keras.Sequential([TFPad(autopad(k, p)), conv])
self.bn = TFBN(w.bn) if hasattr(w, 'bn') else tf.identity self.bn = TFBN(w.bn) if hasattr(w, "bn") else tf.identity
self.act = activations(w.act) if act else tf.identity self.act = activations(w.act) if act else tf.identity
def call(self, inputs): def call(self, inputs):
@@ -92,17 +107,18 @@ class TFDWConv(keras.layers.Layer):
def __init__(self, c1, c2, k=1, s=1, p=None, act=True, w=None): def __init__(self, c1, c2, k=1, s=1, p=None, act=True, w=None):
# ch_in, ch_out, weights, kernel, stride, padding, groups # ch_in, ch_out, weights, kernel, stride, padding, groups
super().__init__() super().__init__()
assert c2 % c1 == 0, f'TFDWConv() output={c2} must be a multiple of input={c1} channels' assert c2 % c1 == 0, f"TFDWConv() output={c2} must be a multiple of input={c1} channels"
conv = keras.layers.DepthwiseConv2D( conv = keras.layers.DepthwiseConv2D(
kernel_size=k, kernel_size=k,
depth_multiplier=c2 // c1, depth_multiplier=c2 // c1,
strides=s, strides=s,
padding='SAME' if s == 1 else 'VALID', padding="SAME" if s == 1 else "VALID",
use_bias=not hasattr(w, 'bn'), use_bias=not hasattr(w, "bn"),
depthwise_initializer=keras.initializers.Constant(w.conv.weight.permute(2, 3, 1, 0).numpy()), depthwise_initializer=keras.initializers.Constant(w.conv.weight.permute(2, 3, 1, 0).numpy()),
bias_initializer='zeros' if hasattr(w, 'bn') else keras.initializers.Constant(w.conv.bias.numpy())) bias_initializer="zeros" if hasattr(w, "bn") else keras.initializers.Constant(w.conv.bias.numpy()),
)
self.conv = conv if s == 1 else keras.Sequential([TFPad(autopad(k, p)), conv]) self.conv = conv if s == 1 else keras.Sequential([TFPad(autopad(k, p)), conv])
self.bn = TFBN(w.bn) if hasattr(w, 'bn') else tf.identity self.bn = TFBN(w.bn) if hasattr(w, "bn") else tf.identity
self.act = activations(w.act) if act else tf.identity self.act = activations(w.act) if act else tf.identity
def call(self, inputs): def call(self, inputs):
@@ -114,19 +130,23 @@ class TFDWConvTranspose2d(keras.layers.Layer):
def __init__(self, c1, c2, k=1, s=1, p1=0, p2=0, w=None): def __init__(self, c1, c2, k=1, s=1, p1=0, p2=0, w=None):
# ch_in, ch_out, weights, kernel, stride, padding, groups # ch_in, ch_out, weights, kernel, stride, padding, groups
super().__init__() super().__init__()
assert c1 == c2, f'TFDWConv() output={c2} must be equal to input={c1} channels' assert c1 == c2, f"TFDWConv() output={c2} must be equal to input={c1} channels"
assert k == 4 and p1 == 1, 'TFDWConv() only valid for k=4 and p1=1' assert k == 4 and p1 == 1, "TFDWConv() only valid for k=4 and p1=1"
weight, bias = w.weight.permute(2, 3, 1, 0).numpy(), w.bias.numpy() weight, bias = w.weight.permute(2, 3, 1, 0).numpy(), w.bias.numpy()
self.c1 = c1 self.c1 = c1
self.conv = [ self.conv = [
keras.layers.Conv2DTranspose(filters=1, keras.layers.Conv2DTranspose(
kernel_size=k, filters=1,
strides=s, kernel_size=k,
padding='VALID', strides=s,
output_padding=p2, padding="VALID",
use_bias=True, output_padding=p2,
kernel_initializer=keras.initializers.Constant(weight[..., i:i + 1]), use_bias=True,
bias_initializer=keras.initializers.Constant(bias[i])) for i in range(c1)] kernel_initializer=keras.initializers.Constant(weight[..., i : i + 1]),
bias_initializer=keras.initializers.Constant(bias[i]),
)
for i in range(c1)
]
def call(self, inputs): def call(self, inputs):
return tf.concat([m(x) for m, x in zip(self.conv, tf.split(inputs, self.c1, 3))], 3)[:, 1:-1, 1:-1] return tf.concat([m(x) for m, x in zip(self.conv, tf.split(inputs, self.c1, 3))], 3)[:, 1:-1, 1:-1]
@@ -176,14 +196,15 @@ class TFConv2d(keras.layers.Layer):
def __init__(self, c1, c2, k, s=1, g=1, bias=True, w=None): def __init__(self, c1, c2, k, s=1, g=1, bias=True, w=None):
super().__init__() super().__init__()
assert g == 1, "TF v2.2 Conv2D does not support 'groups' argument" assert g == 1, "TF v2.2 Conv2D does not support 'groups' argument"
self.conv = keras.layers.Conv2D(filters=c2, self.conv = keras.layers.Conv2D(
kernel_size=k, filters=c2,
strides=s, kernel_size=k,
padding='VALID', strides=s,
use_bias=bias, padding="VALID",
kernel_initializer=keras.initializers.Constant( use_bias=bias,
w.weight.permute(2, 3, 1, 0).numpy()), kernel_initializer=keras.initializers.Constant(w.weight.permute(2, 3, 1, 0).numpy()),
bias_initializer=keras.initializers.Constant(w.bias.numpy()) if bias else None) bias_initializer=keras.initializers.Constant(w.bias.numpy()) if bias else None,
)
def call(self, inputs): def call(self, inputs):
return self.conv(inputs) return self.conv(inputs)
@@ -233,8 +254,9 @@ class TFC3x(keras.layers.Layer):
self.cv1 = TFConv(c1, c_, 1, 1, w=w.cv1) self.cv1 = TFConv(c1, c_, 1, 1, w=w.cv1)
self.cv2 = TFConv(c1, c_, 1, 1, w=w.cv2) self.cv2 = TFConv(c1, c_, 1, 1, w=w.cv2)
self.cv3 = TFConv(2 * c_, c2, 1, 1, w=w.cv3) self.cv3 = TFConv(2 * c_, c2, 1, 1, w=w.cv3)
self.m = keras.Sequential([ self.m = keras.Sequential(
TFCrossConv(c_, c_, k=3, s=1, g=g, e=1.0, shortcut=shortcut, w=w.m[j]) for j in range(n)]) [TFCrossConv(c_, c_, k=3, s=1, g=g, e=1.0, shortcut=shortcut, w=w.m[j]) for j in range(n)]
)
def call(self, inputs): def call(self, inputs):
return self.cv3(tf.concat((self.m(self.cv1(inputs)), self.cv2(inputs)), axis=3)) return self.cv3(tf.concat((self.m(self.cv1(inputs)), self.cv2(inputs)), axis=3))
@@ -247,7 +269,7 @@ class TFSPP(keras.layers.Layer):
c_ = c1 // 2 # hidden channels c_ = c1 // 2 # hidden channels
self.cv1 = TFConv(c1, c_, 1, 1, w=w.cv1) self.cv1 = TFConv(c1, c_, 1, 1, w=w.cv1)
self.cv2 = TFConv(c_ * (len(k) + 1), c2, 1, 1, w=w.cv2) self.cv2 = TFConv(c_ * (len(k) + 1), c2, 1, 1, w=w.cv2)
self.m = [keras.layers.MaxPool2D(pool_size=x, strides=1, padding='SAME') for x in k] self.m = [keras.layers.MaxPool2D(pool_size=x, strides=1, padding="SAME") for x in k]
def call(self, inputs): def call(self, inputs):
x = self.cv1(inputs) x = self.cv1(inputs)
@@ -261,7 +283,7 @@ class TFSPPF(keras.layers.Layer):
c_ = c1 // 2 # hidden channels c_ = c1 // 2 # hidden channels
self.cv1 = TFConv(c1, c_, 1, 1, w=w.cv1) self.cv1 = TFConv(c1, c_, 1, 1, w=w.cv1)
self.cv2 = TFConv(c_ * 4, c2, 1, 1, w=w.cv2) self.cv2 = TFConv(c_ * 4, c2, 1, 1, w=w.cv2)
self.m = keras.layers.MaxPool2D(pool_size=k, strides=1, padding='SAME') self.m = keras.layers.MaxPool2D(pool_size=k, strides=1, padding="SAME")
def call(self, inputs): def call(self, inputs):
x = self.cv1(inputs) x = self.cv1(inputs)
@@ -307,10 +329,10 @@ class TFDetect(keras.layers.Layer):
# Normalize xywh to 0-1 to reduce calibration error # Normalize xywh to 0-1 to reduce calibration error
xy /= tf.constant([[self.imgsz[1], self.imgsz[0]]], dtype=tf.float32) xy /= tf.constant([[self.imgsz[1], self.imgsz[0]]], dtype=tf.float32)
wh /= tf.constant([[self.imgsz[1], self.imgsz[0]]], dtype=tf.float32) wh /= tf.constant([[self.imgsz[1], self.imgsz[0]]], dtype=tf.float32)
y = tf.concat([xy, wh, tf.sigmoid(y[..., 4:5 + self.nc]), y[..., 5 + self.nc:]], -1) y = tf.concat([xy, wh, tf.sigmoid(y[..., 4 : 5 + self.nc]), y[..., 5 + self.nc :]], -1)
z.append(tf.reshape(y, [-1, self.na * ny * nx, self.no])) z.append(tf.reshape(y, [-1, self.na * ny * nx, self.no]))
return tf.transpose(x, [0, 2, 1, 3]) if self.training else (tf.concat(z, 1), ) return tf.transpose(x, [0, 2, 1, 3]) if self.training else (tf.concat(z, 1),)
@staticmethod @staticmethod
def _make_grid(nx=20, ny=20): def _make_grid(nx=20, ny=20):
@@ -340,11 +362,10 @@ class TFSegment(TFDetect):
class TFProto(keras.layers.Layer): class TFProto(keras.layers.Layer):
def __init__(self, c1, c_=256, c2=32, w=None): def __init__(self, c1, c_=256, c2=32, w=None):
super().__init__() super().__init__()
self.cv1 = TFConv(c1, c_, k=3, w=w.cv1) self.cv1 = TFConv(c1, c_, k=3, w=w.cv1)
self.upsample = TFUpsample(None, scale_factor=2, mode='nearest') self.upsample = TFUpsample(None, scale_factor=2, mode="nearest")
self.cv2 = TFConv(c_, c_, k=3, w=w.cv2) self.cv2 = TFConv(c_, c_, k=3, w=w.cv2)
self.cv3 = TFConv(c_, c2, w=w.cv3) self.cv3 = TFConv(c_, c2, w=w.cv3)
@@ -356,7 +377,7 @@ class TFUpsample(keras.layers.Layer):
# TF version of torch.nn.Upsample() # TF version of torch.nn.Upsample()
def __init__(self, size, scale_factor, mode, w=None): # warning: all arguments needed including 'w' def __init__(self, size, scale_factor, mode, w=None): # warning: all arguments needed including 'w'
super().__init__() super().__init__()
assert scale_factor % 2 == 0, 'scale_factor must be multiple of 2' assert scale_factor % 2 == 0, "scale_factor must be multiple of 2"
self.upsample = lambda x: tf.image.resize(x, (x.shape[1] * scale_factor, x.shape[2] * scale_factor), mode) self.upsample = lambda x: tf.image.resize(x, (x.shape[1] * scale_factor, x.shape[2] * scale_factor), mode)
# self.upsample = keras.layers.UpSampling2D(size=scale_factor, interpolation=mode) # self.upsample = keras.layers.UpSampling2D(size=scale_factor, interpolation=mode)
# with default arguments: align_corners=False, half_pixel_centers=False # with default arguments: align_corners=False, half_pixel_centers=False
@@ -371,7 +392,7 @@ class TFConcat(keras.layers.Layer):
# TF version of torch.concat() # TF version of torch.concat()
def __init__(self, dimension=1, w=None): def __init__(self, dimension=1, w=None):
super().__init__() super().__init__()
assert dimension == 1, 'convert only NCHW to NHWC concat' assert dimension == 1, "convert only NCHW to NHWC concat"
self.d = 3 self.d = 3
def call(self, inputs): def call(self, inputs):
@@ -380,15 +401,20 @@ class TFConcat(keras.layers.Layer):
def parse_model(d, ch, model, imgsz): # model_dict, input_channels(3) def parse_model(d, ch, model, imgsz): # model_dict, input_channels(3)
LOGGER.info(f"\n{'':>3}{'from':>18}{'n':>3}{'params':>10} {'module':<40}{'arguments':<30}") LOGGER.info(f"\n{'':>3}{'from':>18}{'n':>3}{'params':>10} {'module':<40}{'arguments':<30}")
anchors, nc, gd, gw, ch_mul = d['anchors'], d['nc'], d['depth_multiple'], d['width_multiple'], d.get( anchors, nc, gd, gw, ch_mul = (
'channel_multiple') d["anchors"],
d["nc"],
d["depth_multiple"],
d["width_multiple"],
d.get("channel_multiple"),
)
na = (len(anchors[0]) // 2) if isinstance(anchors, list) else anchors # number of anchors na = (len(anchors[0]) // 2) if isinstance(anchors, list) else anchors # number of anchors
no = na * (nc + 5) # number of outputs = anchors * (classes + 5) no = na * (nc + 5) # number of outputs = anchors * (classes + 5)
if not ch_mul: if not ch_mul:
ch_mul = 8 ch_mul = 8
layers, save, c2 = [], [], ch[-1] # layers, savelist, ch out layers, save, c2 = [], [], ch[-1] # layers, savelist, ch out
for i, (f, n, m, args) in enumerate(d['backbone'] + d['head']): # from, number, module, args for i, (f, n, m, args) in enumerate(d["backbone"] + d["head"]): # from, number, module, args
m_str = m m_str = m
m = eval(m) if isinstance(m, str) else m # eval strings m = eval(m) if isinstance(m, str) else m # eval strings
for j, a in enumerate(args): for j, a in enumerate(args):
@@ -399,8 +425,20 @@ def parse_model(d, ch, model, imgsz): # model_dict, input_channels(3)
n = max(round(n * gd), 1) if n > 1 else n # depth gain n = max(round(n * gd), 1) if n > 1 else n # depth gain
if m in [ if m in [
nn.Conv2d, Conv, DWConv, DWConvTranspose2d, Bottleneck, SPP, SPPF, MixConv2d, Focus, CrossConv, nn.Conv2d,
BottleneckCSP, C3, C3x]: Conv,
DWConv,
DWConvTranspose2d,
Bottleneck,
SPP,
SPPF,
MixConv2d,
Focus,
CrossConv,
BottleneckCSP,
C3,
C3x,
]:
c1, c2 = ch[f], args[0] c1, c2 = ch[f], args[0]
c2 = make_divisible(c2 * gw, ch_mul) if c2 != no else c2 c2 = make_divisible(c2 * gw, ch_mul) if c2 != no else c2
@@ -422,15 +460,18 @@ def parse_model(d, ch, model, imgsz): # model_dict, input_channels(3)
else: else:
c2 = ch[f] c2 = ch[f]
tf_m = eval('TF' + m_str.replace('nn.', '')) tf_m = eval("TF" + m_str.replace("nn.", ""))
m_ = keras.Sequential([tf_m(*args, w=model.model[i][j]) for j in range(n)]) if n > 1 \ m_ = (
else tf_m(*args, w=model.model[i]) # module keras.Sequential([tf_m(*args, w=model.model[i][j]) for j in range(n)])
if n > 1
else tf_m(*args, w=model.model[i])
) # module
torch_m_ = nn.Sequential(*(m(*args) for _ in range(n))) if n > 1 else m(*args) # module torch_m_ = nn.Sequential(*(m(*args) for _ in range(n))) if n > 1 else m(*args) # module
t = str(m)[8:-2].replace('__main__.', '') # module type t = str(m)[8:-2].replace("__main__.", "") # module type
np = sum(x.numel() for x in torch_m_.parameters()) # number params np = sum(x.numel() for x in torch_m_.parameters()) # number params
m_.i, m_.f, m_.type, m_.np = i, f, t, np # attach index, 'from' index, type, number params m_.i, m_.f, m_.type, m_.np = i, f, t, np # attach index, 'from' index, type, number params
LOGGER.info(f'{i:>3}{str(f):>18}{str(n):>3}{np:>10} {t:<40}{str(args):<30}') # print LOGGER.info(f"{i:>3}{str(f):>18}{str(n):>3}{np:>10} {t:<40}{str(args):<30}") # print
save.extend(x % i for x in ([f] if isinstance(f, int) else f) if x != -1) # append to savelist save.extend(x % i for x in ([f] if isinstance(f, int) else f) if x != -1) # append to savelist
layers.append(m_) layers.append(m_)
ch.append(c2) ch.append(c2)
@@ -439,30 +480,33 @@ def parse_model(d, ch, model, imgsz): # model_dict, input_channels(3)
class TFModel: class TFModel:
# TF YOLOv5 model # TF YOLOv5 model
def __init__(self, cfg='yolov5s.yaml', ch=3, nc=None, model=None, imgsz=(640, 640)): # model, channels, classes def __init__(self, cfg="yolov5s.yaml", ch=3, nc=None, model=None, imgsz=(640, 640)): # model, channels, classes
super().__init__() super().__init__()
if isinstance(cfg, dict): if isinstance(cfg, dict):
self.yaml = cfg # model dict self.yaml = cfg # model dict
else: # is *.yaml else: # is *.yaml
import yaml # for torch hub import yaml # for torch hub
self.yaml_file = Path(cfg).name self.yaml_file = Path(cfg).name
with open(cfg) as f: with open(cfg) as f:
self.yaml = yaml.load(f, Loader=yaml.FullLoader) # model dict self.yaml = yaml.load(f, Loader=yaml.FullLoader) # model dict
# Define model # Define model
if nc and nc != self.yaml['nc']: if nc and nc != self.yaml["nc"]:
LOGGER.info(f"Overriding {cfg} nc={self.yaml['nc']} with nc={nc}") LOGGER.info(f"Overriding {cfg} nc={self.yaml['nc']} with nc={nc}")
self.yaml['nc'] = nc # override yaml value self.yaml["nc"] = nc # override yaml value
self.model, self.savelist = parse_model(deepcopy(self.yaml), ch=[ch], model=model, imgsz=imgsz) self.model, self.savelist = parse_model(deepcopy(self.yaml), ch=[ch], model=model, imgsz=imgsz)
def predict(self, def predict(
inputs, self,
tf_nms=False, inputs,
agnostic_nms=False, tf_nms=False,
topk_per_class=100, agnostic_nms=False,
topk_all=100, topk_per_class=100,
iou_thres=0.45, topk_all=100,
conf_thres=0.25): iou_thres=0.45,
conf_thres=0.25,
):
y = [] # outputs y = [] # outputs
x = inputs x = inputs
for m in self.model.layers: for m in self.model.layers:
@@ -482,14 +526,10 @@ class TFModel:
nms = AgnosticNMS()((boxes, classes, scores), topk_all, iou_thres, conf_thres) nms = AgnosticNMS()((boxes, classes, scores), topk_all, iou_thres, conf_thres)
else: else:
boxes = tf.expand_dims(boxes, 2) boxes = tf.expand_dims(boxes, 2)
nms = tf.image.combined_non_max_suppression(boxes, nms = tf.image.combined_non_max_suppression(
scores, boxes, scores, topk_per_class, topk_all, iou_thres, conf_thres, clip_boxes=False
topk_per_class, )
topk_all, return (nms,)
iou_thres,
conf_thres,
clip_boxes=False)
return (nms, )
return x # output [1,6300,85] = [xywh, conf, class0, class1, ...] return x # output [1,6300,85] = [xywh, conf, class0, class1, ...]
# x = x[0] # [x(1,6300,85), ...] to x(6300,85) # x = x[0] # [x(1,6300,85), ...] to x(6300,85)
# xywh = x[..., :4] # x(6300,4) boxes # xywh = x[..., :4] # x(6300,4) boxes
@@ -508,36 +548,42 @@ class AgnosticNMS(keras.layers.Layer):
# TF Agnostic NMS # TF Agnostic NMS
def call(self, input, topk_all, iou_thres, conf_thres): def call(self, input, topk_all, iou_thres, conf_thres):
# wrap map_fn to avoid TypeSpec related error https://stackoverflow.com/a/65809989/3036450 # wrap map_fn to avoid TypeSpec related error https://stackoverflow.com/a/65809989/3036450
return tf.map_fn(lambda x: self._nms(x, topk_all, iou_thres, conf_thres), return tf.map_fn(
input, lambda x: self._nms(x, topk_all, iou_thres, conf_thres),
fn_output_signature=(tf.float32, tf.float32, tf.float32, tf.int32), input,
name='agnostic_nms') fn_output_signature=(tf.float32, tf.float32, tf.float32, tf.int32),
name="agnostic_nms",
)
@staticmethod @staticmethod
def _nms(x, topk_all=100, iou_thres=0.45, conf_thres=0.25): # agnostic NMS def _nms(x, topk_all=100, iou_thres=0.45, conf_thres=0.25): # agnostic NMS
boxes, classes, scores = x boxes, classes, scores = x
class_inds = tf.cast(tf.argmax(classes, axis=-1), tf.float32) class_inds = tf.cast(tf.argmax(classes, axis=-1), tf.float32)
scores_inp = tf.reduce_max(scores, -1) scores_inp = tf.reduce_max(scores, -1)
selected_inds = tf.image.non_max_suppression(boxes, selected_inds = tf.image.non_max_suppression(
scores_inp, boxes, scores_inp, max_output_size=topk_all, iou_threshold=iou_thres, score_threshold=conf_thres
max_output_size=topk_all, )
iou_threshold=iou_thres,
score_threshold=conf_thres)
selected_boxes = tf.gather(boxes, selected_inds) selected_boxes = tf.gather(boxes, selected_inds)
padded_boxes = tf.pad(selected_boxes, padded_boxes = tf.pad(
paddings=[[0, topk_all - tf.shape(selected_boxes)[0]], [0, 0]], selected_boxes,
mode='CONSTANT', paddings=[[0, topk_all - tf.shape(selected_boxes)[0]], [0, 0]],
constant_values=0.0) mode="CONSTANT",
constant_values=0.0,
)
selected_scores = tf.gather(scores_inp, selected_inds) selected_scores = tf.gather(scores_inp, selected_inds)
padded_scores = tf.pad(selected_scores, padded_scores = tf.pad(
paddings=[[0, topk_all - tf.shape(selected_boxes)[0]]], selected_scores,
mode='CONSTANT', paddings=[[0, topk_all - tf.shape(selected_boxes)[0]]],
constant_values=-1.0) mode="CONSTANT",
constant_values=-1.0,
)
selected_classes = tf.gather(class_inds, selected_inds) selected_classes = tf.gather(class_inds, selected_inds)
padded_classes = tf.pad(selected_classes, padded_classes = tf.pad(
paddings=[[0, topk_all - tf.shape(selected_boxes)[0]]], selected_classes,
mode='CONSTANT', paddings=[[0, topk_all - tf.shape(selected_boxes)[0]]],
constant_values=-1.0) mode="CONSTANT",
constant_values=-1.0,
)
valid_detections = tf.shape(selected_inds)[0] valid_detections = tf.shape(selected_inds)[0]
return padded_boxes, padded_scores, padded_classes, valid_detections return padded_boxes, padded_scores, padded_classes, valid_detections
@@ -551,7 +597,7 @@ def activations(act=nn.SiLU):
elif isinstance(act, (nn.SiLU, SiLU)): elif isinstance(act, (nn.SiLU, SiLU)):
return lambda x: keras.activations.swish(x) return lambda x: keras.activations.swish(x)
else: else:
raise Exception(f'no matching TensorFlow activation found for PyTorch activation {act}') raise Exception(f"no matching TensorFlow activation found for PyTorch activation {act}")
def representative_dataset_gen(dataset, ncalib=100): def representative_dataset_gen(dataset, ncalib=100):
@@ -566,14 +612,14 @@ def representative_dataset_gen(dataset, ncalib=100):
def run( def run(
weights=ROOT / 'yolov5s.pt', # weights path weights=ROOT / "yolov5s.pt", # weights path
imgsz=(640, 640), # inference size h,w imgsz=(640, 640), # inference size h,w
batch_size=1, # batch size batch_size=1, # batch size
dynamic=False, # dynamic batch size dynamic=False, # dynamic batch size
): ):
# PyTorch model # PyTorch model
im = torch.zeros((batch_size, 3, *imgsz)) # BCHW image im = torch.zeros((batch_size, 3, *imgsz)) # BCHW image
model = attempt_load(weights, device=torch.device('cpu'), inplace=True, fuse=False) model = attempt_load(weights, device=torch.device("cpu"), inplace=True, fuse=False)
_ = model(im) # inference _ = model(im) # inference
model.info() model.info()
@@ -587,15 +633,15 @@ def run(
keras_model = keras.Model(inputs=im, outputs=tf_model.predict(im)) keras_model = keras.Model(inputs=im, outputs=tf_model.predict(im))
keras_model.summary() keras_model.summary()
LOGGER.info('PyTorch, TensorFlow and Keras models successfully verified.\nUse export.py for TF model export.') LOGGER.info("PyTorch, TensorFlow and Keras models successfully verified.\nUse export.py for TF model export.")
def parse_opt(): def parse_opt():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('--weights', type=str, default=ROOT / 'yolov5s.pt', help='weights path') parser.add_argument("--weights", type=str, default=ROOT / "yolov5s.pt", help="weights path")
parser.add_argument('--imgsz', '--img', '--img-size', nargs='+', type=int, default=[640], help='inference size h,w') parser.add_argument("--imgsz", "--img", "--img-size", nargs="+", type=int, default=[640], help="inference size h,w")
parser.add_argument('--batch-size', type=int, default=1, help='batch size') parser.add_argument("--batch-size", type=int, default=1, help="batch size")
parser.add_argument('--dynamic', action='store_true', help='dynamic batch size') parser.add_argument("--dynamic", action="store_true", help="dynamic batch size")
opt = parser.parse_args() opt = parser.parse_args()
opt.imgsz *= 2 if len(opt.imgsz) == 1 else 1 # expand opt.imgsz *= 2 if len(opt.imgsz) == 1 else 1 # expand
print_args(vars(opt)) print_args(vars(opt))
@@ -606,6 +652,6 @@ def main(opt):
run(**vars(opt)) run(**vars(opt))
if __name__ == '__main__': if __name__ == "__main__":
opt = parse_opt() opt = parse_opt()
main(opt) main(opt)
+102 -48
View File
@@ -1,6 +1,6 @@
# YOLOv5 🚀 by Ultralytics, AGPL-3.0 license # YOLOv5 🚀 by Ultralytics, AGPL-3.0 license
""" """
YOLO-specific modules YOLO-specific modules.
Usage: Usage:
$ python models/yolo.py --cfg yolov5s.yaml $ python models/yolo.py --cfg yolov5s.yaml
@@ -22,18 +22,46 @@ FILE = Path(__file__).resolve()
ROOT = FILE.parents[1] # YOLOv5 root directory ROOT = FILE.parents[1] # YOLOv5 root directory
if str(ROOT) not in sys.path: if str(ROOT) not in sys.path:
sys.path.append(str(ROOT)) # add ROOT to PATH sys.path.append(str(ROOT)) # add ROOT to PATH
if platform.system() != 'Windows': if platform.system() != "Windows":
ROOT = Path(os.path.relpath(ROOT, Path.cwd())) # relative ROOT = Path(os.path.relpath(ROOT, Path.cwd())) # relative
from models.common import (C3, C3SPP, C3TR, SPP, SPPF, Bottleneck, BottleneckCSP, C3Ghost, C3x, Classify, Concat, from models.common import (
Contract, Conv, CrossConv, DetectMultiBackend, DWConv, DWConvTranspose2d, Expand, Focus, C3,
GhostBottleneck, GhostConv, Proto) C3SPP,
C3TR,
SPP,
SPPF,
Bottleneck,
BottleneckCSP,
C3Ghost,
C3x,
Classify,
Concat,
Contract,
Conv,
CrossConv,
DetectMultiBackend,
DWConv,
DWConvTranspose2d,
Expand,
Focus,
GhostBottleneck,
GhostConv,
Proto,
)
from models.experimental import MixConv2d from models.experimental import MixConv2d
from utils.autoanchor import check_anchor_order from utils.autoanchor import check_anchor_order
from utils.general import LOGGER, check_version, check_yaml, colorstr, make_divisible, print_args from utils.general import LOGGER, check_version, check_yaml, colorstr, make_divisible, print_args
from utils.plots import feature_visualization from utils.plots import feature_visualization
from utils.torch_utils import (fuse_conv_and_bn, initialize_weights, model_info, profile, scale_img, select_device, from utils.torch_utils import (
time_sync) fuse_conv_and_bn,
initialize_weights,
model_info,
profile,
scale_img,
select_device,
time_sync,
)
try: try:
import thop # for FLOPs computation import thop # for FLOPs computation
@@ -55,7 +83,7 @@ class Detect(nn.Module):
self.na = len(anchors[0]) // 2 # number of anchors self.na = len(anchors[0]) // 2 # number of anchors
self.grid = [torch.empty(0) for _ in range(self.nl)] # init grid self.grid = [torch.empty(0) for _ in range(self.nl)] # init grid
self.anchor_grid = [torch.empty(0) for _ in range(self.nl)] # init anchor grid self.anchor_grid = [torch.empty(0) for _ in range(self.nl)] # init anchor grid
self.register_buffer('anchors', torch.tensor(anchors).float().view(self.nl, -1, 2)) # shape(nl,na,2) self.register_buffer("anchors", torch.tensor(anchors).float().view(self.nl, -1, 2)) # shape(nl,na,2)
self.m = nn.ModuleList(nn.Conv2d(x, self.no * self.na, 1) for x in ch) # output conv self.m = nn.ModuleList(nn.Conv2d(x, self.no * self.na, 1) for x in ch) # output conv
self.inplace = inplace # use inplace ops (e.g. slice assignment) self.inplace = inplace # use inplace ops (e.g. slice assignment)
@@ -82,14 +110,14 @@ class Detect(nn.Module):
y = torch.cat((xy, wh, conf), 4) y = torch.cat((xy, wh, conf), 4)
z.append(y.view(bs, self.na * nx * ny, self.no)) z.append(y.view(bs, self.na * nx * ny, self.no))
return x if self.training else (torch.cat(z, 1), ) if self.export else (torch.cat(z, 1), x) return x if self.training else (torch.cat(z, 1),) if self.export else (torch.cat(z, 1), x)
def _make_grid(self, nx=20, ny=20, i=0, torch_1_10=check_version(torch.__version__, '1.10.0')): def _make_grid(self, nx=20, ny=20, i=0, torch_1_10=check_version(torch.__version__, "1.10.0")):
d = self.anchors[i].device d = self.anchors[i].device
t = self.anchors[i].dtype t = self.anchors[i].dtype
shape = 1, self.na, ny, nx, 2 # grid shape shape = 1, self.na, ny, nx, 2 # grid shape
y, x = torch.arange(ny, device=d, dtype=t), torch.arange(nx, device=d, dtype=t) y, x = torch.arange(ny, device=d, dtype=t), torch.arange(nx, device=d, dtype=t)
yv, xv = torch.meshgrid(y, x, indexing='ij') if torch_1_10 else torch.meshgrid(y, x) # torch>=0.7 compatibility yv, xv = torch.meshgrid(y, x, indexing="ij") if torch_1_10 else torch.meshgrid(y, x) # torch>=0.7 compatibility
grid = torch.stack((xv, yv), 2).expand(shape) - 0.5 # add grid offset, i.e. y = 2.0 * x - 0.5 grid = torch.stack((xv, yv), 2).expand(shape) - 0.5 # add grid offset, i.e. y = 2.0 * x - 0.5
anchor_grid = (self.anchors[i] * self.stride[i]).view((1, self.na, 1, 1, 2)).expand(shape) anchor_grid = (self.anchors[i] * self.stride[i]).view((1, self.na, 1, 1, 2)).expand(shape)
return grid, anchor_grid return grid, anchor_grid
@@ -132,23 +160,23 @@ class BaseModel(nn.Module):
def _profile_one_layer(self, m, x, dt): def _profile_one_layer(self, m, x, dt):
c = m == self.model[-1] # is final layer, copy input as inplace fix c = m == self.model[-1] # is final layer, copy input as inplace fix
o = thop.profile(m, inputs=(x.copy() if c else x, ), verbose=False)[0] / 1E9 * 2 if thop else 0 # FLOPs o = thop.profile(m, inputs=(x.copy() if c else x,), verbose=False)[0] / 1e9 * 2 if thop else 0 # FLOPs
t = time_sync() t = time_sync()
for _ in range(10): for _ in range(10):
m(x.copy() if c else x) m(x.copy() if c else x)
dt.append((time_sync() - t) * 100) dt.append((time_sync() - t) * 100)
if m == self.model[0]: if m == self.model[0]:
LOGGER.info(f"{'time (ms)':>10s} {'GFLOPs':>10s} {'params':>10s} module") LOGGER.info(f"{'time (ms)':>10s} {'GFLOPs':>10s} {'params':>10s} module")
LOGGER.info(f'{dt[-1]:10.2f} {o:10.2f} {m.np:10.0f} {m.type}') LOGGER.info(f"{dt[-1]:10.2f} {o:10.2f} {m.np:10.0f} {m.type}")
if c: if c:
LOGGER.info(f"{sum(dt):10.2f} {'-':>10s} {'-':>10s} Total") LOGGER.info(f"{sum(dt):10.2f} {'-':>10s} {'-':>10s} Total")
def fuse(self): # fuse model Conv2d() + BatchNorm2d() layers def fuse(self): # fuse model Conv2d() + BatchNorm2d() layers
LOGGER.info('Fusing layers... ') LOGGER.info("Fusing layers... ")
for m in self.model.modules(): for m in self.model.modules():
if isinstance(m, (Conv, DWConv)) and hasattr(m, 'bn'): if isinstance(m, (Conv, DWConv)) and hasattr(m, "bn"):
m.conv = fuse_conv_and_bn(m.conv, m.bn) # update conv m.conv = fuse_conv_and_bn(m.conv, m.bn) # update conv
delattr(m, 'bn') # remove batchnorm delattr(m, "bn") # remove batchnorm
m.forward = m.forward_fuse # update forward m.forward = m.forward_fuse # update forward
self.info() self.info()
return self return self
@@ -170,27 +198,28 @@ class BaseModel(nn.Module):
class DetectionModel(BaseModel): class DetectionModel(BaseModel):
# YOLOv5 detection model # YOLOv5 detection model
def __init__(self, cfg='yolov5s.yaml', ch=3, nc=None, anchors=None): # model, input channels, number of classes def __init__(self, cfg="yolov5s.yaml", ch=3, nc=None, anchors=None): # model, input channels, number of classes
super().__init__() super().__init__()
if isinstance(cfg, dict): if isinstance(cfg, dict):
self.yaml = cfg # model dict self.yaml = cfg # model dict
else: # is *.yaml else: # is *.yaml
import yaml # for torch hub import yaml # for torch hub
self.yaml_file = Path(cfg).name self.yaml_file = Path(cfg).name
with open(cfg, encoding='ascii', errors='ignore') as f: with open(cfg, encoding="ascii", errors="ignore") as f:
self.yaml = yaml.safe_load(f) # model dict self.yaml = yaml.safe_load(f) # model dict
# Define model # Define model
ch = self.yaml['ch'] = self.yaml.get('ch', ch) # input channels ch = self.yaml["ch"] = self.yaml.get("ch", ch) # input channels
if nc and nc != self.yaml['nc']: if nc and nc != self.yaml["nc"]:
LOGGER.info(f"Overriding model.yaml nc={self.yaml['nc']} with nc={nc}") LOGGER.info(f"Overriding model.yaml nc={self.yaml['nc']} with nc={nc}")
self.yaml['nc'] = nc # override yaml value self.yaml["nc"] = nc # override yaml value
if anchors: if anchors:
LOGGER.info(f'Overriding model.yaml anchors with anchors={anchors}') LOGGER.info(f"Overriding model.yaml anchors with anchors={anchors}")
self.yaml['anchors'] = round(anchors) # override yaml value self.yaml["anchors"] = round(anchors) # override yaml value
self.model, self.save = parse_model(deepcopy(self.yaml), ch=[ch]) # model, savelist self.model, self.save = parse_model(deepcopy(self.yaml), ch=[ch]) # model, savelist
self.names = [str(i) for i in range(self.yaml['nc'])] # default names self.names = [str(i) for i in range(self.yaml["nc"])] # default names
self.inplace = self.yaml.get('inplace', True) self.inplace = self.yaml.get("inplace", True)
# Build strides, anchors # Build strides, anchors
m = self.model[-1] # Detect() m = self.model[-1] # Detect()
@@ -207,7 +236,7 @@ class DetectionModel(BaseModel):
# Init weights, biases # Init weights, biases
initialize_weights(self) initialize_weights(self)
self.info() self.info()
LOGGER.info('') LOGGER.info("")
def forward(self, x, augment=False, profile=False, visualize=False): def forward(self, x, augment=False, profile=False, visualize=False):
if augment: if augment:
@@ -248,9 +277,9 @@ class DetectionModel(BaseModel):
def _clip_augmented(self, y): def _clip_augmented(self, y):
# Clip YOLOv5 augmented inference tails # Clip YOLOv5 augmented inference tails
nl = self.model[-1].nl # number of detection layers (P3-P5) nl = self.model[-1].nl # number of detection layers (P3-P5)
g = sum(4 ** x for x in range(nl)) # grid points g = sum(4**x for x in range(nl)) # grid points
e = 1 # exclude layer count e = 1 # exclude layer count
i = (y[0].shape[1] // g) * sum(4 ** x for x in range(e)) # indices i = (y[0].shape[1] // g) * sum(4**x for x in range(e)) # indices
y[0] = y[0][:, :-i] # large y[0] = y[0][:, :-i] # large
i = (y[-1].shape[1] // g) * sum(4 ** (nl - 1 - x) for x in range(e)) # indices i = (y[-1].shape[1] // g) * sum(4 ** (nl - 1 - x) for x in range(e)) # indices
y[-1] = y[-1][:, i:] # small y[-1] = y[-1][:, i:] # small
@@ -263,7 +292,9 @@ class DetectionModel(BaseModel):
for mi, s in zip(m.m, m.stride): # from for mi, s in zip(m.m, m.stride): # from
b = mi.bias.view(m.na, -1) # conv.bias(255) to (3,85) b = mi.bias.view(m.na, -1) # conv.bias(255) to (3,85)
b.data[:, 4] += math.log(8 / (640 / s) ** 2) # obj (8 objects per 640 image) b.data[:, 4] += math.log(8 / (640 / s) ** 2) # obj (8 objects per 640 image)
b.data[:, 5:5 + m.nc] += math.log(0.6 / (m.nc - 0.99999)) if cf is None else torch.log(cf / cf.sum()) # cls b.data[:, 5 : 5 + m.nc] += (
math.log(0.6 / (m.nc - 0.99999)) if cf is None else torch.log(cf / cf.sum())
) # cls
mi.bias = torch.nn.Parameter(b.view(-1), requires_grad=True) mi.bias = torch.nn.Parameter(b.view(-1), requires_grad=True)
@@ -272,7 +303,7 @@ Model = DetectionModel # retain YOLOv5 'Model' class for backwards compatibilit
class SegmentationModel(DetectionModel): class SegmentationModel(DetectionModel):
# YOLOv5 segmentation model # YOLOv5 segmentation model
def __init__(self, cfg='yolov5s-seg.yaml', ch=3, nc=None, anchors=None): def __init__(self, cfg="yolov5s-seg.yaml", ch=3, nc=None, anchors=None):
super().__init__(cfg, ch, nc, anchors) super().__init__(cfg, ch, nc, anchors)
@@ -288,9 +319,9 @@ class ClassificationModel(BaseModel):
model = model.model # unwrap DetectMultiBackend model = model.model # unwrap DetectMultiBackend
model.model = model.model[:cutoff] # backbone model.model = model.model[:cutoff] # backbone
m = model.model[-1] # last layer m = model.model[-1] # last layer
ch = m.conv.in_channels if hasattr(m, 'conv') else m.cv1.conv.in_channels # ch into module ch = m.conv.in_channels if hasattr(m, "conv") else m.cv1.conv.in_channels # ch into module
c = Classify(ch, nc) # Classify() c = Classify(ch, nc) # Classify()
c.i, c.f, c.type = m.i, m.f, 'models.common.Classify' # index, from, type c.i, c.f, c.type = m.i, m.f, "models.common.Classify" # index, from, type
model.model[-1] = c # replace model.model[-1] = c # replace
self.model = model.model self.model = model.model
self.stride = model.stride self.stride = model.stride
@@ -305,8 +336,14 @@ class ClassificationModel(BaseModel):
def parse_model(d, ch): # model_dict, input_channels(3) def parse_model(d, ch): # model_dict, input_channels(3)
# Parse a YOLOv5 model.yaml dictionary # Parse a YOLOv5 model.yaml dictionary
LOGGER.info(f"\n{'':>3}{'from':>18}{'n':>3}{'params':>10} {'module':<40}{'arguments':<30}") LOGGER.info(f"\n{'':>3}{'from':>18}{'n':>3}{'params':>10} {'module':<40}{'arguments':<30}")
anchors, nc, gd, gw, act, ch_mul = d['anchors'], d['nc'], d['depth_multiple'], d['width_multiple'], d.get( anchors, nc, gd, gw, act, ch_mul = (
'activation'), d.get('channel_multiple') d["anchors"],
d["nc"],
d["depth_multiple"],
d["width_multiple"],
d.get("activation"),
d.get("channel_multiple"),
)
if act: if act:
Conv.default_act = eval(act) # redefine default activation, i.e. Conv.default_act = nn.SiLU() Conv.default_act = eval(act) # redefine default activation, i.e. Conv.default_act = nn.SiLU()
LOGGER.info(f"{colorstr('activation:')} {act}") # print LOGGER.info(f"{colorstr('activation:')} {act}") # print
@@ -316,7 +353,7 @@ def parse_model(d, ch): # model_dict, input_channels(3)
no = na * (nc + 5) # number of outputs = anchors * (classes + 5) no = na * (nc + 5) # number of outputs = anchors * (classes + 5)
layers, save, c2 = [], [], ch[-1] # layers, savelist, ch out layers, save, c2 = [], [], ch[-1] # layers, savelist, ch out
for i, (f, n, m, args) in enumerate(d['backbone'] + d['head']): # from, number, module, args for i, (f, n, m, args) in enumerate(d["backbone"] + d["head"]): # from, number, module, args
m = eval(m) if isinstance(m, str) else m # eval strings m = eval(m) if isinstance(m, str) else m # eval strings
for j, a in enumerate(args): for j, a in enumerate(args):
with contextlib.suppress(NameError): with contextlib.suppress(NameError):
@@ -324,8 +361,25 @@ def parse_model(d, ch): # model_dict, input_channels(3)
n = n_ = max(round(n * gd), 1) if n > 1 else n # depth gain n = n_ = max(round(n * gd), 1) if n > 1 else n # depth gain
if m in { if m in {
Conv, GhostConv, Bottleneck, GhostBottleneck, SPP, SPPF, DWConv, MixConv2d, Focus, CrossConv, Conv,
BottleneckCSP, C3, C3TR, C3SPP, C3Ghost, nn.ConvTranspose2d, DWConvTranspose2d, C3x}: GhostConv,
Bottleneck,
GhostBottleneck,
SPP,
SPPF,
DWConv,
MixConv2d,
Focus,
CrossConv,
BottleneckCSP,
C3,
C3TR,
C3SPP,
C3Ghost,
nn.ConvTranspose2d,
DWConvTranspose2d,
C3x,
}:
c1, c2 = ch[f], args[0] c1, c2 = ch[f], args[0]
if c2 != no: # if not output if c2 != no: # if not output
c2 = make_divisible(c2 * gw, ch_mul) c2 = make_divisible(c2 * gw, ch_mul)
@@ -353,10 +407,10 @@ def parse_model(d, ch): # model_dict, input_channels(3)
c2 = ch[f] c2 = ch[f]
m_ = nn.Sequential(*(m(*args) for _ in range(n))) if n > 1 else m(*args) # module m_ = nn.Sequential(*(m(*args) for _ in range(n))) if n > 1 else m(*args) # module
t = str(m)[8:-2].replace('__main__.', '') # module type t = str(m)[8:-2].replace("__main__.", "") # module type
np = sum(x.numel() for x in m_.parameters()) # number params np = sum(x.numel() for x in m_.parameters()) # number params
m_.i, m_.f, m_.type, m_.np = i, f, t, np # attach index, 'from' index, type, number params m_.i, m_.f, m_.type, m_.np = i, f, t, np # attach index, 'from' index, type, number params
LOGGER.info(f'{i:>3}{str(f):>18}{n_:>3}{np:10.0f} {t:<40}{str(args):<30}') # print LOGGER.info(f"{i:>3}{str(f):>18}{n_:>3}{np:10.0f} {t:<40}{str(args):<30}") # print
save.extend(x % i for x in ([f] if isinstance(f, int) else f) if x != -1) # append to savelist save.extend(x % i for x in ([f] if isinstance(f, int) else f) if x != -1) # append to savelist
layers.append(m_) layers.append(m_)
if i == 0: if i == 0:
@@ -365,14 +419,14 @@ def parse_model(d, ch): # model_dict, input_channels(3)
return nn.Sequential(*layers), sorted(save) return nn.Sequential(*layers), sorted(save)
if __name__ == '__main__': if __name__ == "__main__":
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('--cfg', type=str, default='yolov5s.yaml', help='model.yaml') parser.add_argument("--cfg", type=str, default="yolov5s.yaml", help="model.yaml")
parser.add_argument('--batch-size', type=int, default=1, help='total batch size for all GPUs') parser.add_argument("--batch-size", type=int, default=1, help="total batch size for all GPUs")
parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu') parser.add_argument("--device", default="", help="cuda device, i.e. 0 or 0,1,2,3 or cpu")
parser.add_argument('--profile', action='store_true', help='profile model speed') parser.add_argument("--profile", action="store_true", help="profile model speed")
parser.add_argument('--line-profile', action='store_true', help='profile model speed layer by layer') parser.add_argument("--line-profile", action="store_true", help="profile model speed layer by layer")
parser.add_argument('--test', action='store_true', help='test all yolo*.yaml') parser.add_argument("--test", action="store_true", help="test all yolo*.yaml")
opt = parser.parse_args() opt = parser.parse_args()
opt.cfg = check_yaml(opt.cfg) # check YAML opt.cfg = check_yaml(opt.cfg) # check YAML
print_args(vars(opt)) print_args(vars(opt))
@@ -390,11 +444,11 @@ if __name__ == '__main__':
results = profile(input=im, ops=[model], n=3) results = profile(input=im, ops=[model], n=3)
elif opt.test: # test all models elif opt.test: # test all models
for cfg in Path(ROOT / 'models').rglob('yolo*.yaml'): for cfg in Path(ROOT / "models").rglob("yolo*.yaml"):
try: try:
_ = Model(cfg) _ = Model(cfg)
except Exception as e: except Exception as e:
print(f'Error in {cfg}: {e}') print(f"Error in {cfg}: {e}")
else: # report fused model summary else: # report fused model summary
model.fuse() model.fuse()
+80 -63
View File
@@ -46,23 +46,36 @@ from ultralytics.utils.plotting import Annotator, colors, save_one_box
from models.common import DetectMultiBackend from models.common import DetectMultiBackend
from utils.dataloaders import IMG_FORMATS, VID_FORMATS, LoadImages, LoadScreenshots, LoadStreams from utils.dataloaders import IMG_FORMATS, VID_FORMATS, LoadImages, LoadScreenshots, LoadStreams
from utils.general import (LOGGER, Profile, check_file, check_img_size, check_imshow, check_requirements, colorstr, cv2, from utils.general import (
increment_path, non_max_suppression, print_args, scale_boxes, scale_segments, LOGGER,
strip_optimizer) Profile,
check_file,
check_img_size,
check_imshow,
check_requirements,
colorstr,
cv2,
increment_path,
non_max_suppression,
print_args,
scale_boxes,
scale_segments,
strip_optimizer,
)
from utils.segment.general import masks2segments, process_mask, process_mask_native from utils.segment.general import masks2segments, process_mask, process_mask_native
from utils.torch_utils import select_device, smart_inference_mode from utils.torch_utils import select_device, smart_inference_mode
@smart_inference_mode() @smart_inference_mode()
def run( def run(
weights=ROOT / 'yolov5s-seg.pt', # model.pt path(s) weights=ROOT / "yolov5s-seg.pt", # model.pt path(s)
source=ROOT / 'data/images', # file/dir/URL/glob/screen/0(webcam) source=ROOT / "data/images", # file/dir/URL/glob/screen/0(webcam)
data=ROOT / 'data/coco128.yaml', # dataset.yaml path data=ROOT / "data/coco128.yaml", # dataset.yaml path
imgsz=(640, 640), # inference size (height, width) imgsz=(640, 640), # inference size (height, width)
conf_thres=0.25, # confidence threshold conf_thres=0.25, # confidence threshold
iou_thres=0.45, # NMS IOU threshold iou_thres=0.45, # NMS IOU threshold
max_det=1000, # maximum detections per image max_det=1000, # maximum detections per image
device='', # cuda device, i.e. 0 or 0,1,2,3 or cpu device="", # cuda device, i.e. 0 or 0,1,2,3 or cpu
view_img=False, # show results view_img=False, # show results
save_txt=False, # save results to *.txt save_txt=False, # save results to *.txt
save_conf=False, # save confidences in --save-txt labels save_conf=False, # save confidences in --save-txt labels
@@ -73,8 +86,8 @@ def run(
augment=False, # augmented inference augment=False, # augmented inference
visualize=False, # visualize features visualize=False, # visualize features
update=False, # update all models update=False, # update all models
project=ROOT / 'runs/predict-seg', # save results to project/name project=ROOT / "runs/predict-seg", # save results to project/name
name='exp', # save results to project/name name="exp", # save results to project/name
exist_ok=False, # existing project/name ok, do not increment exist_ok=False, # existing project/name ok, do not increment
line_thickness=3, # bounding box thickness (pixels) line_thickness=3, # bounding box thickness (pixels)
hide_labels=False, # hide labels hide_labels=False, # hide labels
@@ -85,17 +98,17 @@ def run(
retina_masks=False, retina_masks=False,
): ):
source = str(source) source = str(source)
save_img = not nosave and not source.endswith('.txt') # save inference images save_img = not nosave and not source.endswith(".txt") # save inference images
is_file = Path(source).suffix[1:] in (IMG_FORMATS + VID_FORMATS) is_file = Path(source).suffix[1:] in (IMG_FORMATS + VID_FORMATS)
is_url = source.lower().startswith(('rtsp://', 'rtmp://', 'http://', 'https://')) is_url = source.lower().startswith(("rtsp://", "rtmp://", "http://", "https://"))
webcam = source.isnumeric() or source.endswith('.streams') or (is_url and not is_file) webcam = source.isnumeric() or source.endswith(".streams") or (is_url and not is_file)
screenshot = source.lower().startswith('screen') screenshot = source.lower().startswith("screen")
if is_url and is_file: if is_url and is_file:
source = check_file(source) # download source = check_file(source) # download
# Directories # Directories
save_dir = increment_path(Path(project) / name, exist_ok=exist_ok) # increment run save_dir = increment_path(Path(project) / name, exist_ok=exist_ok) # increment run
(save_dir / 'labels' if save_txt else save_dir).mkdir(parents=True, exist_ok=True) # make dir (save_dir / "labels" if save_txt else save_dir).mkdir(parents=True, exist_ok=True) # make dir
# Load model # Load model
device = select_device(device) device = select_device(device)
@@ -143,14 +156,14 @@ def run(
seen += 1 seen += 1
if webcam: # batch_size >= 1 if webcam: # batch_size >= 1
p, im0, frame = path[i], im0s[i].copy(), dataset.count p, im0, frame = path[i], im0s[i].copy(), dataset.count
s += f'{i}: ' s += f"{i}: "
else: else:
p, im0, frame = path, im0s.copy(), getattr(dataset, 'frame', 0) p, im0, frame = path, im0s.copy(), getattr(dataset, "frame", 0)
p = Path(p) # to Path p = Path(p) # to Path
save_path = str(save_dir / p.name) # im.jpg save_path = str(save_dir / p.name) # im.jpg
txt_path = str(save_dir / 'labels' / p.stem) + ('' if dataset.mode == 'image' else f'_{frame}') # im.txt txt_path = str(save_dir / "labels" / p.stem) + ("" if dataset.mode == "image" else f"_{frame}") # im.txt
s += '%gx%g ' % im.shape[2:] # print string s += "%gx%g " % im.shape[2:] # print string
imc = im0.copy() if save_crop else im0 # for save_crop imc = im0.copy() if save_crop else im0 # for save_crop
annotator = Annotator(im0, line_width=line_thickness, example=str(names)) annotator = Annotator(im0, line_width=line_thickness, example=str(names))
if len(det): if len(det):
@@ -166,7 +179,8 @@ def run(
if save_txt: if save_txt:
segments = [ segments = [
scale_segments(im0.shape if retina_masks else im.shape[2:], x, im0.shape, normalize=True) scale_segments(im0.shape if retina_masks else im.shape[2:], x, im0.shape, normalize=True)
for x in reversed(masks2segments(masks))] for x in reversed(masks2segments(masks))
]
# Print results # Print results
for c in det[:, 5].unique(): for c in det[:, 5].unique():
@@ -177,39 +191,42 @@ def run(
annotator.masks( annotator.masks(
masks, masks,
colors=[colors(x, True) for x in det[:, 5]], colors=[colors(x, True) for x in det[:, 5]],
im_gpu=torch.as_tensor(im0, dtype=torch.float16).to(device).permute(2, 0, 1).flip(0).contiguous() / im_gpu=torch.as_tensor(im0, dtype=torch.float16).to(device).permute(2, 0, 1).flip(0).contiguous()
255 if retina_masks else im[i]) / 255
if retina_masks
else im[i],
)
# Write results # Write results
for j, (*xyxy, conf, cls) in enumerate(reversed(det[:, :6])): for j, (*xyxy, conf, cls) in enumerate(reversed(det[:, :6])):
if save_txt: # Write to file if save_txt: # Write to file
seg = segments[j].reshape(-1) # (n,2) to (n*2) seg = segments[j].reshape(-1) # (n,2) to (n*2)
line = (cls, *seg, conf) if save_conf else (cls, *seg) # label format line = (cls, *seg, conf) if save_conf else (cls, *seg) # label format
with open(f'{txt_path}.txt', 'a') as f: with open(f"{txt_path}.txt", "a") as f:
f.write(('%g ' * len(line)).rstrip() % line + '\n') f.write(("%g " * len(line)).rstrip() % line + "\n")
if save_img or save_crop or view_img: # Add bbox to image if save_img or save_crop or view_img: # Add bbox to image
c = int(cls) # integer class c = int(cls) # integer class
label = None if hide_labels else (names[c] if hide_conf else f'{names[c]} {conf:.2f}') label = None if hide_labels else (names[c] if hide_conf else f"{names[c]} {conf:.2f}")
annotator.box_label(xyxy, label, color=colors(c, True)) annotator.box_label(xyxy, label, color=colors(c, True))
# annotator.draw.polygon(segments[j], outline=colors(c, True), width=3) # annotator.draw.polygon(segments[j], outline=colors(c, True), width=3)
if save_crop: if save_crop:
save_one_box(xyxy, imc, file=save_dir / 'crops' / names[c] / f'{p.stem}.jpg', BGR=True) save_one_box(xyxy, imc, file=save_dir / "crops" / names[c] / f"{p.stem}.jpg", BGR=True)
# Stream results # Stream results
im0 = annotator.result() im0 = annotator.result()
if view_img: if view_img:
if platform.system() == 'Linux' and p not in windows: if platform.system() == "Linux" and p not in windows:
windows.append(p) windows.append(p)
cv2.namedWindow(str(p), cv2.WINDOW_NORMAL | cv2.WINDOW_KEEPRATIO) # allow window resize (Linux) cv2.namedWindow(str(p), cv2.WINDOW_NORMAL | cv2.WINDOW_KEEPRATIO) # allow window resize (Linux)
cv2.resizeWindow(str(p), im0.shape[1], im0.shape[0]) cv2.resizeWindow(str(p), im0.shape[1], im0.shape[0])
cv2.imshow(str(p), im0) cv2.imshow(str(p), im0)
if cv2.waitKey(1) == ord('q'): # 1 millisecond if cv2.waitKey(1) == ord("q"): # 1 millisecond
exit() exit()
# Save results (image with detections) # Save results (image with detections)
if save_img: if save_img:
if dataset.mode == 'image': if dataset.mode == "image":
cv2.imwrite(save_path, im0) cv2.imwrite(save_path, im0)
else: # 'video' or 'stream' else: # 'video' or 'stream'
if vid_path[i] != save_path: # new video if vid_path[i] != save_path: # new video
@@ -222,18 +239,18 @@ def run(
h = int(vid_cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) h = int(vid_cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
else: # stream else: # stream
fps, w, h = 30, im0.shape[1], im0.shape[0] fps, w, h = 30, im0.shape[1], im0.shape[0]
save_path = str(Path(save_path).with_suffix('.mp4')) # force *.mp4 suffix on results videos save_path = str(Path(save_path).with_suffix(".mp4")) # force *.mp4 suffix on results videos
vid_writer[i] = cv2.VideoWriter(save_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (w, h)) vid_writer[i] = cv2.VideoWriter(save_path, cv2.VideoWriter_fourcc(*"mp4v"), fps, (w, h))
vid_writer[i].write(im0) vid_writer[i].write(im0)
# Print time (inference-only) # Print time (inference-only)
LOGGER.info(f"{s}{'' if len(det) else '(no detections), '}{dt[1].dt * 1E3:.1f}ms") LOGGER.info(f"{s}{'' if len(det) else '(no detections), '}{dt[1].dt * 1E3:.1f}ms")
# Print results # Print results
t = tuple(x.t / seen * 1E3 for x in dt) # speeds per image t = tuple(x.t / seen * 1e3 for x in dt) # speeds per image
LOGGER.info(f'Speed: %.1fms pre-process, %.1fms inference, %.1fms NMS per image at shape {(1, 3, *imgsz)}' % t) LOGGER.info(f"Speed: %.1fms pre-process, %.1fms inference, %.1fms NMS per image at shape {(1, 3, *imgsz)}" % t)
if save_txt or save_img: if save_txt or save_img:
s = f"\n{len(list(save_dir.glob('labels/*.txt')))} labels saved to {save_dir / 'labels'}" if save_txt else '' s = f"\n{len(list(save_dir.glob('labels/*.txt')))} labels saved to {save_dir / 'labels'}" if save_txt else ""
LOGGER.info(f"Results saved to {colorstr('bold', save_dir)}{s}") LOGGER.info(f"Results saved to {colorstr('bold', save_dir)}{s}")
if update: if update:
strip_optimizer(weights[0]) # update model (to fix SourceChangeWarning) strip_optimizer(weights[0]) # update model (to fix SourceChangeWarning)
@@ -241,34 +258,34 @@ def run(
def parse_opt(): def parse_opt():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('--weights', nargs='+', type=str, default=ROOT / 'yolov5s-seg.pt', help='model path(s)') parser.add_argument("--weights", nargs="+", type=str, default=ROOT / "yolov5s-seg.pt", help="model path(s)")
parser.add_argument('--source', type=str, default=ROOT / 'data/images', help='file/dir/URL/glob/screen/0(webcam)') parser.add_argument("--source", type=str, default=ROOT / "data/images", help="file/dir/URL/glob/screen/0(webcam)")
parser.add_argument('--data', type=str, default=ROOT / 'data/coco128.yaml', help='(optional) dataset.yaml path') parser.add_argument("--data", type=str, default=ROOT / "data/coco128.yaml", help="(optional) dataset.yaml path")
parser.add_argument('--imgsz', '--img', '--img-size', nargs='+', type=int, default=[640], help='inference size h,w') parser.add_argument("--imgsz", "--img", "--img-size", nargs="+", type=int, default=[640], help="inference size h,w")
parser.add_argument('--conf-thres', type=float, default=0.25, help='confidence threshold') parser.add_argument("--conf-thres", type=float, default=0.25, help="confidence threshold")
parser.add_argument('--iou-thres', type=float, default=0.45, help='NMS IoU threshold') parser.add_argument("--iou-thres", type=float, default=0.45, help="NMS IoU threshold")
parser.add_argument('--max-det', type=int, default=1000, help='maximum detections per image') parser.add_argument("--max-det", type=int, default=1000, help="maximum detections per image")
parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu') parser.add_argument("--device", default="", help="cuda device, i.e. 0 or 0,1,2,3 or cpu")
parser.add_argument('--view-img', action='store_true', help='show results') parser.add_argument("--view-img", action="store_true", help="show results")
parser.add_argument('--save-txt', action='store_true', help='save results to *.txt') parser.add_argument("--save-txt", action="store_true", help="save results to *.txt")
parser.add_argument('--save-conf', action='store_true', help='save confidences in --save-txt labels') parser.add_argument("--save-conf", action="store_true", help="save confidences in --save-txt labels")
parser.add_argument('--save-crop', action='store_true', help='save cropped prediction boxes') parser.add_argument("--save-crop", action="store_true", help="save cropped prediction boxes")
parser.add_argument('--nosave', action='store_true', help='do not save images/videos') parser.add_argument("--nosave", action="store_true", help="do not save images/videos")
parser.add_argument('--classes', nargs='+', type=int, help='filter by class: --classes 0, or --classes 0 2 3') parser.add_argument("--classes", nargs="+", type=int, help="filter by class: --classes 0, or --classes 0 2 3")
parser.add_argument('--agnostic-nms', action='store_true', help='class-agnostic NMS') parser.add_argument("--agnostic-nms", action="store_true", help="class-agnostic NMS")
parser.add_argument('--augment', action='store_true', help='augmented inference') parser.add_argument("--augment", action="store_true", help="augmented inference")
parser.add_argument('--visualize', action='store_true', help='visualize features') parser.add_argument("--visualize", action="store_true", help="visualize features")
parser.add_argument('--update', action='store_true', help='update all models') parser.add_argument("--update", action="store_true", help="update all models")
parser.add_argument('--project', default=ROOT / 'runs/predict-seg', help='save results to project/name') parser.add_argument("--project", default=ROOT / "runs/predict-seg", help="save results to project/name")
parser.add_argument('--name', default='exp', help='save results to project/name') parser.add_argument("--name", default="exp", help="save results to project/name")
parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment') parser.add_argument("--exist-ok", action="store_true", help="existing project/name ok, do not increment")
parser.add_argument('--line-thickness', default=3, type=int, help='bounding box thickness (pixels)') parser.add_argument("--line-thickness", default=3, type=int, help="bounding box thickness (pixels)")
parser.add_argument('--hide-labels', default=False, action='store_true', help='hide labels') parser.add_argument("--hide-labels", default=False, action="store_true", help="hide labels")
parser.add_argument('--hide-conf', default=False, action='store_true', help='hide confidences') parser.add_argument("--hide-conf", default=False, action="store_true", help="hide confidences")
parser.add_argument('--half', action='store_true', help='use FP16 half-precision inference') parser.add_argument("--half", action="store_true", help="use FP16 half-precision inference")
parser.add_argument('--dnn', action='store_true', help='use OpenCV DNN for ONNX inference') parser.add_argument("--dnn", action="store_true", help="use OpenCV DNN for ONNX inference")
parser.add_argument('--vid-stride', type=int, default=1, help='video frame-rate stride') parser.add_argument("--vid-stride", type=int, default=1, help="video frame-rate stride")
parser.add_argument('--retina-masks', action='store_true', help='whether to plot masks in native resolution') parser.add_argument("--retina-masks", action="store_true", help="whether to plot masks in native resolution")
opt = parser.parse_args() opt = parser.parse_args()
opt.imgsz *= 2 if len(opt.imgsz) == 1 else 1 # expand opt.imgsz *= 2 if len(opt.imgsz) == 1 else 1 # expand
print_args(vars(opt)) print_args(vars(opt))
@@ -276,10 +293,10 @@ def parse_opt():
def main(opt): def main(opt):
check_requirements(ROOT / 'requirements.txt', exclude=('tensorboard', 'thop')) check_requirements(ROOT / "requirements.txt", exclude=("tensorboard", "thop"))
run(**vars(opt)) run(**vars(opt))
if __name__ == '__main__': if __name__ == "__main__":
opt = parse_opt() opt = parse_opt()
main(opt) main(opt)
+299 -219
View File
@@ -1,7 +1,7 @@
# YOLOv5 🚀 by Ultralytics, AGPL-3.0 license # YOLOv5 🚀 by Ultralytics, AGPL-3.0 license
""" """
Train a YOLOv5 segment model on a segment dataset Train a YOLOv5 segment model on a segment dataset Models and datasets download automatically from the latest YOLOv5
Models and datasets download automatically from the latest YOLOv5 release. release.
Usage - Single-GPU training: Usage - Single-GPU training:
$ python segment/train.py --data coco128-seg.yaml --weights yolov5s-seg.pt --img 640 # from pretrained (recommended) $ python segment/train.py --data coco128-seg.yaml --weights yolov5s-seg.pt --img 640 # from pretrained (recommended)
@@ -47,47 +47,104 @@ from utils.autoanchor import check_anchors
from utils.autobatch import check_train_batch_size from utils.autobatch import check_train_batch_size
from utils.callbacks import Callbacks from utils.callbacks import Callbacks
from utils.downloads import attempt_download, is_url from utils.downloads import attempt_download, is_url
from utils.general import (LOGGER, TQDM_BAR_FORMAT, check_amp, check_dataset, check_file, check_git_info, from utils.general import (
check_git_status, check_img_size, check_requirements, check_suffix, check_yaml, colorstr, LOGGER,
get_latest_run, increment_path, init_seeds, intersect_dicts, labels_to_class_weights, TQDM_BAR_FORMAT,
labels_to_image_weights, one_cycle, print_args, print_mutation, strip_optimizer, yaml_save) check_amp,
check_dataset,
check_file,
check_git_info,
check_git_status,
check_img_size,
check_requirements,
check_suffix,
check_yaml,
colorstr,
get_latest_run,
increment_path,
init_seeds,
intersect_dicts,
labels_to_class_weights,
labels_to_image_weights,
one_cycle,
print_args,
print_mutation,
strip_optimizer,
yaml_save,
)
from utils.loggers import GenericLogger from utils.loggers import GenericLogger
from utils.plots import plot_evolve, plot_labels from utils.plots import plot_evolve, plot_labels
from utils.segment.dataloaders import create_dataloader from utils.segment.dataloaders import create_dataloader
from utils.segment.loss import ComputeLoss from utils.segment.loss import ComputeLoss
from utils.segment.metrics import KEYS, fitness from utils.segment.metrics import KEYS, fitness
from utils.segment.plots import plot_images_and_masks, plot_results_with_masks from utils.segment.plots import plot_images_and_masks, plot_results_with_masks
from utils.torch_utils import (EarlyStopping, ModelEMA, de_parallel, select_device, smart_DDP, smart_optimizer, from utils.torch_utils import (
smart_resume, torch_distributed_zero_first) EarlyStopping,
ModelEMA,
de_parallel,
select_device,
smart_DDP,
smart_optimizer,
smart_resume,
torch_distributed_zero_first,
)
LOCAL_RANK = int(os.getenv('LOCAL_RANK', -1)) # https://pytorch.org/docs/stable/elastic/run.html LOCAL_RANK = int(os.getenv("LOCAL_RANK", -1)) # https://pytorch.org/docs/stable/elastic/run.html
RANK = int(os.getenv('RANK', -1)) RANK = int(os.getenv("RANK", -1))
WORLD_SIZE = int(os.getenv('WORLD_SIZE', 1)) WORLD_SIZE = int(os.getenv("WORLD_SIZE", 1))
GIT_INFO = check_git_info() GIT_INFO = check_git_info()
def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictionary def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictionary
save_dir, epochs, batch_size, weights, single_cls, evolve, data, cfg, resume, noval, nosave, workers, freeze, mask_ratio = \ (
Path(opt.save_dir), opt.epochs, opt.batch_size, opt.weights, opt.single_cls, opt.evolve, opt.data, opt.cfg, \ save_dir,
opt.resume, opt.noval, opt.nosave, opt.workers, opt.freeze, opt.mask_ratio epochs,
batch_size,
weights,
single_cls,
evolve,
data,
cfg,
resume,
noval,
nosave,
workers,
freeze,
mask_ratio,
) = (
Path(opt.save_dir),
opt.epochs,
opt.batch_size,
opt.weights,
opt.single_cls,
opt.evolve,
opt.data,
opt.cfg,
opt.resume,
opt.noval,
opt.nosave,
opt.workers,
opt.freeze,
opt.mask_ratio,
)
# callbacks.run('on_pretrain_routine_start') # callbacks.run('on_pretrain_routine_start')
# Directories # Directories
w = save_dir / 'weights' # weights dir w = save_dir / "weights" # weights dir
(w.parent if evolve else w).mkdir(parents=True, exist_ok=True) # make dir (w.parent if evolve else w).mkdir(parents=True, exist_ok=True) # make dir
last, best = w / 'last.pt', w / 'best.pt' last, best = w / "last.pt", w / "best.pt"
# Hyperparameters # Hyperparameters
if isinstance(hyp, str): if isinstance(hyp, str):
with open(hyp, errors='ignore') as f: with open(hyp, errors="ignore") as f:
hyp = yaml.safe_load(f) # load hyps dict hyp = yaml.safe_load(f) # load hyps dict
LOGGER.info(colorstr('hyperparameters: ') + ', '.join(f'{k}={v}' for k, v in hyp.items())) LOGGER.info(colorstr("hyperparameters: ") + ", ".join(f"{k}={v}" for k, v in hyp.items()))
opt.hyp = hyp.copy() # for saving hyps to checkpoints opt.hyp = hyp.copy() # for saving hyps to checkpoints
# Save run settings # Save run settings
if not evolve: if not evolve:
yaml_save(save_dir / 'hyp.yaml', hyp) yaml_save(save_dir / "hyp.yaml", hyp)
yaml_save(save_dir / 'opt.yaml', vars(opt)) yaml_save(save_dir / "opt.yaml", vars(opt))
# Loggers # Loggers
data_dict = None data_dict = None
@@ -97,39 +154,39 @@ def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictio
# Config # Config
plots = not evolve and not opt.noplots # create plots plots = not evolve and not opt.noplots # create plots
overlap = not opt.no_overlap overlap = not opt.no_overlap
cuda = device.type != 'cpu' cuda = device.type != "cpu"
init_seeds(opt.seed + 1 + RANK, deterministic=True) init_seeds(opt.seed + 1 + RANK, deterministic=True)
with torch_distributed_zero_first(LOCAL_RANK): with torch_distributed_zero_first(LOCAL_RANK):
data_dict = data_dict or check_dataset(data) # check if None data_dict = data_dict or check_dataset(data) # check if None
train_path, val_path = data_dict['train'], data_dict['val'] train_path, val_path = data_dict["train"], data_dict["val"]
nc = 1 if single_cls else int(data_dict['nc']) # number of classes nc = 1 if single_cls else int(data_dict["nc"]) # number of classes
names = {0: 'item'} if single_cls and len(data_dict['names']) != 1 else data_dict['names'] # class names names = {0: "item"} if single_cls and len(data_dict["names"]) != 1 else data_dict["names"] # class names
is_coco = isinstance(val_path, str) and val_path.endswith('coco/val2017.txt') # COCO dataset is_coco = isinstance(val_path, str) and val_path.endswith("coco/val2017.txt") # COCO dataset
# Model # Model
check_suffix(weights, '.pt') # check weights check_suffix(weights, ".pt") # check weights
pretrained = weights.endswith('.pt') pretrained = weights.endswith(".pt")
if pretrained: if pretrained:
with torch_distributed_zero_first(LOCAL_RANK): with torch_distributed_zero_first(LOCAL_RANK):
weights = attempt_download(weights) # download if not found locally weights = attempt_download(weights) # download if not found locally
ckpt = torch.load(weights, map_location='cpu') # load checkpoint to CPU to avoid CUDA memory leak ckpt = torch.load(weights, map_location="cpu") # load checkpoint to CPU to avoid CUDA memory leak
model = SegmentationModel(cfg or ckpt['model'].yaml, ch=3, nc=nc, anchors=hyp.get('anchors')).to(device) model = SegmentationModel(cfg or ckpt["model"].yaml, ch=3, nc=nc, anchors=hyp.get("anchors")).to(device)
exclude = ['anchor'] if (cfg or hyp.get('anchors')) and not resume else [] # exclude keys exclude = ["anchor"] if (cfg or hyp.get("anchors")) and not resume else [] # exclude keys
csd = ckpt['model'].float().state_dict() # checkpoint state_dict as FP32 csd = ckpt["model"].float().state_dict() # checkpoint state_dict as FP32
csd = intersect_dicts(csd, model.state_dict(), exclude=exclude) # intersect csd = intersect_dicts(csd, model.state_dict(), exclude=exclude) # intersect
model.load_state_dict(csd, strict=False) # load model.load_state_dict(csd, strict=False) # load
LOGGER.info(f'Transferred {len(csd)}/{len(model.state_dict())} items from {weights}') # report LOGGER.info(f"Transferred {len(csd)}/{len(model.state_dict())} items from {weights}") # report
else: else:
model = SegmentationModel(cfg, ch=3, nc=nc, anchors=hyp.get('anchors')).to(device) # create model = SegmentationModel(cfg, ch=3, nc=nc, anchors=hyp.get("anchors")).to(device) # create
amp = check_amp(model) # check AMP amp = check_amp(model) # check AMP
# Freeze # Freeze
freeze = [f'model.{x}.' for x in (freeze if len(freeze) > 1 else range(freeze[0]))] # layers to freeze freeze = [f"model.{x}." for x in (freeze if len(freeze) > 1 else range(freeze[0]))] # layers to freeze
for k, v in model.named_parameters(): for k, v in model.named_parameters():
v.requires_grad = True # train all layers v.requires_grad = True # train all layers
# v.register_hook(lambda x: torch.nan_to_num(x)) # NaN to 0 (commented for erratic training results) # v.register_hook(lambda x: torch.nan_to_num(x)) # NaN to 0 (commented for erratic training results)
if any(x in k for x in freeze): if any(x in k for x in freeze):
LOGGER.info(f'freezing {k}') LOGGER.info(f"freezing {k}")
v.requires_grad = False v.requires_grad = False
# Image size # Image size
@@ -139,20 +196,20 @@ def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictio
# Batch size # Batch size
if RANK == -1 and batch_size == -1: # single-GPU only, estimate best batch size if RANK == -1 and batch_size == -1: # single-GPU only, estimate best batch size
batch_size = check_train_batch_size(model, imgsz, amp) batch_size = check_train_batch_size(model, imgsz, amp)
logger.update_params({'batch_size': batch_size}) logger.update_params({"batch_size": batch_size})
# loggers.on_params_update({"batch_size": batch_size}) # loggers.on_params_update({"batch_size": batch_size})
# Optimizer # Optimizer
nbs = 64 # nominal batch size nbs = 64 # nominal batch size
accumulate = max(round(nbs / batch_size), 1) # accumulate loss before optimizing accumulate = max(round(nbs / batch_size), 1) # accumulate loss before optimizing
hyp['weight_decay'] *= batch_size * accumulate / nbs # scale weight_decay hyp["weight_decay"] *= batch_size * accumulate / nbs # scale weight_decay
optimizer = smart_optimizer(model, opt.optimizer, hyp['lr0'], hyp['momentum'], hyp['weight_decay']) optimizer = smart_optimizer(model, opt.optimizer, hyp["lr0"], hyp["momentum"], hyp["weight_decay"])
# Scheduler # Scheduler
if opt.cos_lr: if opt.cos_lr:
lf = one_cycle(1, hyp['lrf'], epochs) # cosine 1->hyp['lrf'] lf = one_cycle(1, hyp["lrf"], epochs) # cosine 1->hyp['lrf']
else: else:
lf = lambda x: (1 - x / epochs) * (1.0 - hyp['lrf']) + hyp['lrf'] # linear lf = lambda x: (1 - x / epochs) * (1.0 - hyp["lrf"]) + hyp["lrf"] # linear
scheduler = lr_scheduler.LambdaLR(optimizer, lr_lambda=lf) # plot_lr_scheduler(optimizer, scheduler, epochs) scheduler = lr_scheduler.LambdaLR(optimizer, lr_lambda=lf) # plot_lr_scheduler(optimizer, scheduler, epochs)
# EMA # EMA
@@ -168,15 +225,15 @@ def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictio
# DP mode # DP mode
if cuda and RANK == -1 and torch.cuda.device_count() > 1: if cuda and RANK == -1 and torch.cuda.device_count() > 1:
LOGGER.warning( LOGGER.warning(
'WARNING ⚠️ DP not recommended, use torch.distributed.run for best DDP Multi-GPU results.\n' "WARNING ⚠️ DP not recommended, use torch.distributed.run for best DDP Multi-GPU results.\n"
'See Multi-GPU Tutorial at https://docs.ultralytics.com/yolov5/tutorials/multi_gpu_training to get started.' "See Multi-GPU Tutorial at https://docs.ultralytics.com/yolov5/tutorials/multi_gpu_training to get started."
) )
model = torch.nn.DataParallel(model) model = torch.nn.DataParallel(model)
# SyncBatchNorm # SyncBatchNorm
if opt.sync_bn and cuda and RANK != -1: if opt.sync_bn and cuda and RANK != -1:
model = torch.nn.SyncBatchNorm.convert_sync_batchnorm(model).to(device) model = torch.nn.SyncBatchNorm.convert_sync_batchnorm(model).to(device)
LOGGER.info('Using SyncBatchNorm()') LOGGER.info("Using SyncBatchNorm()")
# Trainloader # Trainloader
train_loader, dataset = create_dataloader( train_loader, dataset = create_dataloader(
@@ -187,41 +244,43 @@ def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictio
single_cls, single_cls,
hyp=hyp, hyp=hyp,
augment=True, augment=True,
cache=None if opt.cache == 'val' else opt.cache, cache=None if opt.cache == "val" else opt.cache,
rect=opt.rect, rect=opt.rect,
rank=LOCAL_RANK, rank=LOCAL_RANK,
workers=workers, workers=workers,
image_weights=opt.image_weights, image_weights=opt.image_weights,
quad=opt.quad, quad=opt.quad,
prefix=colorstr('train: '), prefix=colorstr("train: "),
shuffle=True, shuffle=True,
mask_downsample_ratio=mask_ratio, mask_downsample_ratio=mask_ratio,
overlap_mask=overlap, overlap_mask=overlap,
) )
labels = np.concatenate(dataset.labels, 0) labels = np.concatenate(dataset.labels, 0)
mlc = int(labels[:, 0].max()) # max label class mlc = int(labels[:, 0].max()) # max label class
assert mlc < nc, f'Label class {mlc} exceeds nc={nc} in {data}. Possible class labels are 0-{nc - 1}' assert mlc < nc, f"Label class {mlc} exceeds nc={nc} in {data}. Possible class labels are 0-{nc - 1}"
# Process 0 # Process 0
if RANK in {-1, 0}: if RANK in {-1, 0}:
val_loader = create_dataloader(val_path, val_loader = create_dataloader(
imgsz, val_path,
batch_size // WORLD_SIZE * 2, imgsz,
gs, batch_size // WORLD_SIZE * 2,
single_cls, gs,
hyp=hyp, single_cls,
cache=None if noval else opt.cache, hyp=hyp,
rect=True, cache=None if noval else opt.cache,
rank=-1, rect=True,
workers=workers * 2, rank=-1,
pad=0.5, workers=workers * 2,
mask_downsample_ratio=mask_ratio, pad=0.5,
overlap_mask=overlap, mask_downsample_ratio=mask_ratio,
prefix=colorstr('val: '))[0] overlap_mask=overlap,
prefix=colorstr("val: "),
)[0]
if not resume: if not resume:
if not opt.noautoanchor: if not opt.noautoanchor:
check_anchors(dataset, model=model, thr=hyp['anchor_t'], imgsz=imgsz) # run AutoAnchor check_anchors(dataset, model=model, thr=hyp["anchor_t"], imgsz=imgsz) # run AutoAnchor
model.half().float() # pre-reduce anchor precision model.half().float() # pre-reduce anchor precision
if plots: if plots:
@@ -234,10 +293,10 @@ def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictio
# Model attributes # Model attributes
nl = de_parallel(model).model[-1].nl # number of detection layers (to scale hyps) nl = de_parallel(model).model[-1].nl # number of detection layers (to scale hyps)
hyp['box'] *= 3 / nl # scale to layers hyp["box"] *= 3 / nl # scale to layers
hyp['cls'] *= nc / 80 * 3 / nl # scale to classes and layers hyp["cls"] *= nc / 80 * 3 / nl # scale to classes and layers
hyp['obj'] *= (imgsz / 640) ** 2 * 3 / nl # scale to image size and layers hyp["obj"] *= (imgsz / 640) ** 2 * 3 / nl # scale to image size and layers
hyp['label_smoothing'] = opt.label_smoothing hyp["label_smoothing"] = opt.label_smoothing
model.nc = nc # attach number of classes to model model.nc = nc # attach number of classes to model
model.hyp = hyp # attach hyperparameters to model model.hyp = hyp # attach hyperparameters to model
model.class_weights = labels_to_class_weights(dataset.labels, nc).to(device) * nc # attach class weights model.class_weights = labels_to_class_weights(dataset.labels, nc).to(device) * nc # attach class weights
@@ -246,7 +305,7 @@ def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictio
# Start training # Start training
t0 = time.time() t0 = time.time()
nb = len(train_loader) # number of batches nb = len(train_loader) # number of batches
nw = max(round(hyp['warmup_epochs'] * nb), 100) # number of warmup iterations, max(3 epochs, 100 iterations) nw = max(round(hyp["warmup_epochs"] * nb), 100) # number of warmup iterations, max(3 epochs, 100 iterations)
# nw = min(nw, (epochs - start_epoch) / 2 * nb) # limit warmup to < 1/2 of training # nw = min(nw, (epochs - start_epoch) / 2 * nb) # limit warmup to < 1/2 of training
last_opt_step = -1 last_opt_step = -1
maps = np.zeros(nc) # mAP per class maps = np.zeros(nc) # mAP per class
@@ -256,10 +315,12 @@ def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictio
stopper, stop = EarlyStopping(patience=opt.patience), False stopper, stop = EarlyStopping(patience=opt.patience), False
compute_loss = ComputeLoss(model, overlap=overlap) # init loss class compute_loss = ComputeLoss(model, overlap=overlap) # init loss class
# callbacks.run('on_train_start') # callbacks.run('on_train_start')
LOGGER.info(f'Image sizes {imgsz} train, {imgsz} val\n' LOGGER.info(
f'Using {train_loader.num_workers * WORLD_SIZE} dataloader workers\n' f'Image sizes {imgsz} train, {imgsz} val\n'
f"Logging results to {colorstr('bold', save_dir)}\n" f'Using {train_loader.num_workers * WORLD_SIZE} dataloader workers\n'
f'Starting training for {epochs} epochs...') f"Logging results to {colorstr('bold', save_dir)}\n"
f'Starting training for {epochs} epochs...'
)
for epoch in range(start_epoch, epochs): # epoch ------------------------------------------------------------------ for epoch in range(start_epoch, epochs): # epoch ------------------------------------------------------------------
# callbacks.run('on_train_epoch_start') # callbacks.run('on_train_epoch_start')
model.train() model.train()
@@ -278,8 +339,10 @@ def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictio
if RANK != -1: if RANK != -1:
train_loader.sampler.set_epoch(epoch) train_loader.sampler.set_epoch(epoch)
pbar = enumerate(train_loader) pbar = enumerate(train_loader)
LOGGER.info(('\n' + '%11s' * 8) % LOGGER.info(
('Epoch', 'GPU_mem', 'box_loss', 'seg_loss', 'obj_loss', 'cls_loss', 'Instances', 'Size')) ("\n" + "%11s" * 8)
% ("Epoch", "GPU_mem", "box_loss", "seg_loss", "obj_loss", "cls_loss", "Instances", "Size")
)
if RANK in {-1, 0}: if RANK in {-1, 0}:
pbar = tqdm(pbar, total=nb, bar_format=TQDM_BAR_FORMAT) # progress bar pbar = tqdm(pbar, total=nb, bar_format=TQDM_BAR_FORMAT) # progress bar
optimizer.zero_grad() optimizer.zero_grad()
@@ -295,9 +358,9 @@ def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictio
accumulate = max(1, np.interp(ni, xi, [1, nbs / batch_size]).round()) accumulate = max(1, np.interp(ni, xi, [1, nbs / batch_size]).round())
for j, x in enumerate(optimizer.param_groups): for j, x in enumerate(optimizer.param_groups):
# bias lr falls from 0.1 to lr0, all other lrs rise from 0.0 to lr0 # bias lr falls from 0.1 to lr0, all other lrs rise from 0.0 to lr0
x['lr'] = np.interp(ni, xi, [hyp['warmup_bias_lr'] if j == 0 else 0.0, x['initial_lr'] * lf(epoch)]) x["lr"] = np.interp(ni, xi, [hyp["warmup_bias_lr"] if j == 0 else 0.0, x["initial_lr"] * lf(epoch)])
if 'momentum' in x: if "momentum" in x:
x['momentum'] = np.interp(ni, xi, [hyp['warmup_momentum'], hyp['momentum']]) x["momentum"] = np.interp(ni, xi, [hyp["warmup_momentum"], hyp["momentum"]])
# Multi-scale # Multi-scale
if opt.multi_scale: if opt.multi_scale:
@@ -305,7 +368,7 @@ def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictio
sf = sz / max(imgs.shape[2:]) # scale factor sf = sz / max(imgs.shape[2:]) # scale factor
if sf != 1: if sf != 1:
ns = [math.ceil(x * sf / gs) * gs for x in imgs.shape[2:]] # new shape (stretched to gs-multiple) ns = [math.ceil(x * sf / gs) * gs for x in imgs.shape[2:]] # new shape (stretched to gs-multiple)
imgs = nn.functional.interpolate(imgs, size=ns, mode='bilinear', align_corners=False) imgs = nn.functional.interpolate(imgs, size=ns, mode="bilinear", align_corners=False)
# Forward # Forward
with torch.cuda.amp.autocast(amp): with torch.cuda.amp.autocast(amp):
@@ -314,7 +377,7 @@ def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictio
if RANK != -1: if RANK != -1:
loss *= WORLD_SIZE # gradient averaged between devices in DDP mode loss *= WORLD_SIZE # gradient averaged between devices in DDP mode
if opt.quad: if opt.quad:
loss *= 4. loss *= 4.0
# Backward # Backward
scaler.scale(loss).backward() scaler.scale(loss).backward()
@@ -333,9 +396,11 @@ def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictio
# Log # Log
if RANK in {-1, 0}: if RANK in {-1, 0}:
mloss = (mloss * i + loss_items) / (i + 1) # update mean losses mloss = (mloss * i + loss_items) / (i + 1) # update mean losses
mem = f'{torch.cuda.memory_reserved() / 1E9 if torch.cuda.is_available() else 0:.3g}G' # (GB) mem = f"{torch.cuda.memory_reserved() / 1E9 if torch.cuda.is_available() else 0:.3g}G" # (GB)
pbar.set_description(('%11s' * 2 + '%11.4g' * 6) % pbar.set_description(
(f'{epoch}/{epochs - 1}', mem, *mloss, targets.shape[0], imgs.shape[-1])) ("%11s" * 2 + "%11.4g" * 6)
% (f"{epoch}/{epochs - 1}", mem, *mloss, targets.shape[0], imgs.shape[-1])
)
# callbacks.run('on_train_batch_end', model, ni, imgs, targets, paths) # callbacks.run('on_train_batch_end', model, ni, imgs, targets, paths)
# if callbacks.stop_training: # if callbacks.stop_training:
# return # return
@@ -343,35 +408,37 @@ def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictio
# Mosaic plots # Mosaic plots
if plots: if plots:
if ni < 3: if ni < 3:
plot_images_and_masks(imgs, targets, masks, paths, save_dir / f'train_batch{ni}.jpg') plot_images_and_masks(imgs, targets, masks, paths, save_dir / f"train_batch{ni}.jpg")
if ni == 10: if ni == 10:
files = sorted(save_dir.glob('train*.jpg')) files = sorted(save_dir.glob("train*.jpg"))
logger.log_images(files, 'Mosaics', epoch) logger.log_images(files, "Mosaics", epoch)
# end batch ------------------------------------------------------------------------------------------------ # end batch ------------------------------------------------------------------------------------------------
# Scheduler # Scheduler
lr = [x['lr'] for x in optimizer.param_groups] # for loggers lr = [x["lr"] for x in optimizer.param_groups] # for loggers
scheduler.step() scheduler.step()
if RANK in {-1, 0}: if RANK in {-1, 0}:
# mAP # mAP
# callbacks.run('on_train_epoch_end', epoch=epoch) # callbacks.run('on_train_epoch_end', epoch=epoch)
ema.update_attr(model, include=['yaml', 'nc', 'hyp', 'names', 'stride', 'class_weights']) ema.update_attr(model, include=["yaml", "nc", "hyp", "names", "stride", "class_weights"])
final_epoch = (epoch + 1 == epochs) or stopper.possible_stop final_epoch = (epoch + 1 == epochs) or stopper.possible_stop
if not noval or final_epoch: # Calculate mAP if not noval or final_epoch: # Calculate mAP
results, maps, _ = validate.run(data_dict, results, maps, _ = validate.run(
batch_size=batch_size // WORLD_SIZE * 2, data_dict,
imgsz=imgsz, batch_size=batch_size // WORLD_SIZE * 2,
half=amp, imgsz=imgsz,
model=ema.ema, half=amp,
single_cls=single_cls, model=ema.ema,
dataloader=val_loader, single_cls=single_cls,
save_dir=save_dir, dataloader=val_loader,
plots=False, save_dir=save_dir,
callbacks=callbacks, plots=False,
compute_loss=compute_loss, callbacks=callbacks,
mask_downsample_ratio=mask_ratio, compute_loss=compute_loss,
overlap=overlap) mask_downsample_ratio=mask_ratio,
overlap=overlap,
)
# Update best mAP # Update best mAP
fi = fitness(np.array(results).reshape(1, -1)) # weighted combination of [P, R, mAP@.5, mAP@.5-.95] fi = fitness(np.array(results).reshape(1, -1)) # weighted combination of [P, R, mAP@.5, mAP@.5-.95]
@@ -387,23 +454,24 @@ def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictio
# Save model # Save model
if (not nosave) or (final_epoch and not evolve): # if save if (not nosave) or (final_epoch and not evolve): # if save
ckpt = { ckpt = {
'epoch': epoch, "epoch": epoch,
'best_fitness': best_fitness, "best_fitness": best_fitness,
'model': deepcopy(de_parallel(model)).half(), "model": deepcopy(de_parallel(model)).half(),
'ema': deepcopy(ema.ema).half(), "ema": deepcopy(ema.ema).half(),
'updates': ema.updates, "updates": ema.updates,
'optimizer': optimizer.state_dict(), "optimizer": optimizer.state_dict(),
'opt': vars(opt), "opt": vars(opt),
'git': GIT_INFO, # {remote, branch, commit} if a git repo "git": GIT_INFO, # {remote, branch, commit} if a git repo
'date': datetime.now().isoformat()} "date": datetime.now().isoformat(),
}
# Save last, best and delete # Save last, best and delete
torch.save(ckpt, last) torch.save(ckpt, last)
if best_fitness == fi: if best_fitness == fi:
torch.save(ckpt, best) torch.save(ckpt, best)
if opt.save_period > 0 and epoch % opt.save_period == 0: if opt.save_period > 0 and epoch % opt.save_period == 0:
torch.save(ckpt, w / f'epoch{epoch}.pt') torch.save(ckpt, w / f"epoch{epoch}.pt")
logger.log_model(w / f'epoch{epoch}.pt') logger.log_model(w / f"epoch{epoch}.pt")
del ckpt del ckpt
# callbacks.run('on_model_save', last, epoch, final_epoch, best_fitness, fi) # callbacks.run('on_model_save', last, epoch, final_epoch, best_fitness, fi)
@@ -419,12 +487,12 @@ def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictio
# end epoch ---------------------------------------------------------------------------------------------------- # end epoch ----------------------------------------------------------------------------------------------------
# end training ----------------------------------------------------------------------------------------------------- # end training -----------------------------------------------------------------------------------------------------
if RANK in {-1, 0}: if RANK in {-1, 0}:
LOGGER.info(f'\n{epoch - start_epoch + 1} epochs completed in {(time.time() - t0) / 3600:.3f} hours.') LOGGER.info(f"\n{epoch - start_epoch + 1} epochs completed in {(time.time() - t0) / 3600:.3f} hours.")
for f in last, best: for f in last, best:
if f.exists(): if f.exists():
strip_optimizer(f) # strip optimizers strip_optimizer(f) # strip optimizers
if f is best: if f is best:
LOGGER.info(f'\nValidating {f}...') LOGGER.info(f"\nValidating {f}...")
results, _, _ = validate.run( results, _, _ = validate.run(
data_dict, data_dict,
batch_size=batch_size // WORLD_SIZE * 2, batch_size=batch_size // WORLD_SIZE * 2,
@@ -440,7 +508,8 @@ def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictio
callbacks=callbacks, callbacks=callbacks,
compute_loss=compute_loss, compute_loss=compute_loss,
mask_downsample_ratio=mask_ratio, mask_downsample_ratio=mask_ratio,
overlap=overlap) # val best model with plots overlap=overlap,
) # val best model with plots
if is_coco: if is_coco:
# callbacks.run('on_fit_epoch_end', list(mloss) + list(results) + lr, epoch, best_fitness, fi) # callbacks.run('on_fit_epoch_end', list(mloss) + list(results) + lr, epoch, best_fitness, fi)
metrics_dict = dict(zip(KEYS, list(mloss) + list(results) + lr)) metrics_dict = dict(zip(KEYS, list(mloss) + list(results) + lr))
@@ -452,56 +521,56 @@ def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictio
if not opt.evolve: if not opt.evolve:
logger.log_model(best, epoch) logger.log_model(best, epoch)
if plots: if plots:
plot_results_with_masks(file=save_dir / 'results.csv') # save results.png plot_results_with_masks(file=save_dir / "results.csv") # save results.png
files = ['results.png', 'confusion_matrix.png', *(f'{x}_curve.png' for x in ('F1', 'PR', 'P', 'R'))] files = ["results.png", "confusion_matrix.png", *(f"{x}_curve.png" for x in ("F1", "PR", "P", "R"))]
files = [(save_dir / f) for f in files if (save_dir / f).exists()] # filter files = [(save_dir / f) for f in files if (save_dir / f).exists()] # filter
LOGGER.info(f"Results saved to {colorstr('bold', save_dir)}") LOGGER.info(f"Results saved to {colorstr('bold', save_dir)}")
logger.log_images(files, 'Results', epoch + 1) logger.log_images(files, "Results", epoch + 1)
logger.log_images(sorted(save_dir.glob('val*.jpg')), 'Validation', epoch + 1) logger.log_images(sorted(save_dir.glob("val*.jpg")), "Validation", epoch + 1)
torch.cuda.empty_cache() torch.cuda.empty_cache()
return results return results
def parse_opt(known=False): def parse_opt(known=False):
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('--weights', type=str, default=ROOT / 'yolov5s-seg.pt', help='initial weights path') parser.add_argument("--weights", type=str, default=ROOT / "yolov5s-seg.pt", help="initial weights path")
parser.add_argument('--cfg', type=str, default='', help='model.yaml path') parser.add_argument("--cfg", type=str, default="", help="model.yaml path")
parser.add_argument('--data', type=str, default=ROOT / 'data/coco128-seg.yaml', help='dataset.yaml path') parser.add_argument("--data", type=str, default=ROOT / "data/coco128-seg.yaml", help="dataset.yaml path")
parser.add_argument('--hyp', type=str, default=ROOT / 'data/hyps/hyp.scratch-low.yaml', help='hyperparameters path') parser.add_argument("--hyp", type=str, default=ROOT / "data/hyps/hyp.scratch-low.yaml", help="hyperparameters path")
parser.add_argument('--epochs', type=int, default=100, help='total training epochs') parser.add_argument("--epochs", type=int, default=100, help="total training epochs")
parser.add_argument('--batch-size', type=int, default=16, help='total batch size for all GPUs, -1 for autobatch') parser.add_argument("--batch-size", type=int, default=16, help="total batch size for all GPUs, -1 for autobatch")
parser.add_argument('--imgsz', '--img', '--img-size', type=int, default=640, help='train, val image size (pixels)') parser.add_argument("--imgsz", "--img", "--img-size", type=int, default=640, help="train, val image size (pixels)")
parser.add_argument('--rect', action='store_true', help='rectangular training') parser.add_argument("--rect", action="store_true", help="rectangular training")
parser.add_argument('--resume', nargs='?', const=True, default=False, help='resume most recent training') parser.add_argument("--resume", nargs="?", const=True, default=False, help="resume most recent training")
parser.add_argument('--nosave', action='store_true', help='only save final checkpoint') parser.add_argument("--nosave", action="store_true", help="only save final checkpoint")
parser.add_argument('--noval', action='store_true', help='only validate final epoch') parser.add_argument("--noval", action="store_true", help="only validate final epoch")
parser.add_argument('--noautoanchor', action='store_true', help='disable AutoAnchor') parser.add_argument("--noautoanchor", action="store_true", help="disable AutoAnchor")
parser.add_argument('--noplots', action='store_true', help='save no plot files') parser.add_argument("--noplots", action="store_true", help="save no plot files")
parser.add_argument('--evolve', type=int, nargs='?', const=300, help='evolve hyperparameters for x generations') parser.add_argument("--evolve", type=int, nargs="?", const=300, help="evolve hyperparameters for x generations")
parser.add_argument('--bucket', type=str, default='', help='gsutil bucket') parser.add_argument("--bucket", type=str, default="", help="gsutil bucket")
parser.add_argument('--cache', type=str, nargs='?', const='ram', help='image --cache ram/disk') parser.add_argument("--cache", type=str, nargs="?", const="ram", help="image --cache ram/disk")
parser.add_argument('--image-weights', action='store_true', help='use weighted image selection for training') parser.add_argument("--image-weights", action="store_true", help="use weighted image selection for training")
parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu') parser.add_argument("--device", default="", help="cuda device, i.e. 0 or 0,1,2,3 or cpu")
parser.add_argument('--multi-scale', action='store_true', help='vary img-size +/- 50%%') parser.add_argument("--multi-scale", action="store_true", help="vary img-size +/- 50%%")
parser.add_argument('--single-cls', action='store_true', help='train multi-class data as single-class') parser.add_argument("--single-cls", action="store_true", help="train multi-class data as single-class")
parser.add_argument('--optimizer', type=str, choices=['SGD', 'Adam', 'AdamW'], default='SGD', help='optimizer') parser.add_argument("--optimizer", type=str, choices=["SGD", "Adam", "AdamW"], default="SGD", help="optimizer")
parser.add_argument('--sync-bn', action='store_true', help='use SyncBatchNorm, only available in DDP mode') parser.add_argument("--sync-bn", action="store_true", help="use SyncBatchNorm, only available in DDP mode")
parser.add_argument('--workers', type=int, default=8, help='max dataloader workers (per RANK in DDP mode)') parser.add_argument("--workers", type=int, default=8, help="max dataloader workers (per RANK in DDP mode)")
parser.add_argument('--project', default=ROOT / 'runs/train-seg', help='save to project/name') parser.add_argument("--project", default=ROOT / "runs/train-seg", help="save to project/name")
parser.add_argument('--name', default='exp', help='save to project/name') parser.add_argument("--name", default="exp", help="save to project/name")
parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment') parser.add_argument("--exist-ok", action="store_true", help="existing project/name ok, do not increment")
parser.add_argument('--quad', action='store_true', help='quad dataloader') parser.add_argument("--quad", action="store_true", help="quad dataloader")
parser.add_argument('--cos-lr', action='store_true', help='cosine LR scheduler') parser.add_argument("--cos-lr", action="store_true", help="cosine LR scheduler")
parser.add_argument('--label-smoothing', type=float, default=0.0, help='Label smoothing epsilon') parser.add_argument("--label-smoothing", type=float, default=0.0, help="Label smoothing epsilon")
parser.add_argument('--patience', type=int, default=100, help='EarlyStopping patience (epochs without improvement)') parser.add_argument("--patience", type=int, default=100, help="EarlyStopping patience (epochs without improvement)")
parser.add_argument('--freeze', nargs='+', type=int, default=[0], help='Freeze layers: backbone=10, first3=0 1 2') parser.add_argument("--freeze", nargs="+", type=int, default=[0], help="Freeze layers: backbone=10, first3=0 1 2")
parser.add_argument('--save-period', type=int, default=-1, help='Save checkpoint every x epochs (disabled if < 1)') parser.add_argument("--save-period", type=int, default=-1, help="Save checkpoint every x epochs (disabled if < 1)")
parser.add_argument('--seed', type=int, default=0, help='Global training seed') parser.add_argument("--seed", type=int, default=0, help="Global training seed")
parser.add_argument('--local_rank', type=int, default=-1, help='Automatic DDP Multi-GPU argument, do not modify') parser.add_argument("--local_rank", type=int, default=-1, help="Automatic DDP Multi-GPU argument, do not modify")
# Instance Segmentation Args # Instance Segmentation Args
parser.add_argument('--mask-ratio', type=int, default=4, help='Downsample the truth masks to saving memory') parser.add_argument("--mask-ratio", type=int, default=4, help="Downsample the truth masks to saving memory")
parser.add_argument('--no-overlap', action='store_true', help='Overlap masks train faster at slightly less mAP') parser.add_argument("--no-overlap", action="store_true", help="Overlap masks train faster at slightly less mAP")
return parser.parse_known_args()[0] if known else parser.parse_args() return parser.parse_known_args()[0] if known else parser.parse_args()
@@ -511,46 +580,51 @@ def main(opt, callbacks=Callbacks()):
if RANK in {-1, 0}: if RANK in {-1, 0}:
print_args(vars(opt)) print_args(vars(opt))
check_git_status() check_git_status()
check_requirements(ROOT / 'requirements.txt') check_requirements(ROOT / "requirements.txt")
# Resume # Resume
if opt.resume and not opt.evolve: # resume from specified or most recent last.pt if opt.resume and not opt.evolve: # resume from specified or most recent last.pt
last = Path(check_file(opt.resume) if isinstance(opt.resume, str) else get_latest_run()) last = Path(check_file(opt.resume) if isinstance(opt.resume, str) else get_latest_run())
opt_yaml = last.parent.parent / 'opt.yaml' # train options yaml opt_yaml = last.parent.parent / "opt.yaml" # train options yaml
opt_data = opt.data # original dataset opt_data = opt.data # original dataset
if opt_yaml.is_file(): if opt_yaml.is_file():
with open(opt_yaml, errors='ignore') as f: with open(opt_yaml, errors="ignore") as f:
d = yaml.safe_load(f) d = yaml.safe_load(f)
else: else:
d = torch.load(last, map_location='cpu')['opt'] d = torch.load(last, map_location="cpu")["opt"]
opt = argparse.Namespace(**d) # replace opt = argparse.Namespace(**d) # replace
opt.cfg, opt.weights, opt.resume = '', str(last), True # reinstate opt.cfg, opt.weights, opt.resume = "", str(last), True # reinstate
if is_url(opt_data): if is_url(opt_data):
opt.data = check_file(opt_data) # avoid HUB resume auth timeout opt.data = check_file(opt_data) # avoid HUB resume auth timeout
else: else:
opt.data, opt.cfg, opt.hyp, opt.weights, opt.project = \ opt.data, opt.cfg, opt.hyp, opt.weights, opt.project = (
check_file(opt.data), check_yaml(opt.cfg), check_yaml(opt.hyp), str(opt.weights), str(opt.project) # checks check_file(opt.data),
assert len(opt.cfg) or len(opt.weights), 'either --cfg or --weights must be specified' check_yaml(opt.cfg),
check_yaml(opt.hyp),
str(opt.weights),
str(opt.project),
) # checks
assert len(opt.cfg) or len(opt.weights), "either --cfg or --weights must be specified"
if opt.evolve: if opt.evolve:
if opt.project == str(ROOT / 'runs/train-seg'): # if default project name, rename to runs/evolve-seg if opt.project == str(ROOT / "runs/train-seg"): # if default project name, rename to runs/evolve-seg
opt.project = str(ROOT / 'runs/evolve-seg') opt.project = str(ROOT / "runs/evolve-seg")
opt.exist_ok, opt.resume = opt.resume, False # pass resume to exist_ok and disable resume opt.exist_ok, opt.resume = opt.resume, False # pass resume to exist_ok and disable resume
if opt.name == 'cfg': if opt.name == "cfg":
opt.name = Path(opt.cfg).stem # use model.yaml as name opt.name = Path(opt.cfg).stem # use model.yaml as name
opt.save_dir = str(increment_path(Path(opt.project) / opt.name, exist_ok=opt.exist_ok)) opt.save_dir = str(increment_path(Path(opt.project) / opt.name, exist_ok=opt.exist_ok))
# DDP mode # DDP mode
device = select_device(opt.device, batch_size=opt.batch_size) device = select_device(opt.device, batch_size=opt.batch_size)
if LOCAL_RANK != -1: if LOCAL_RANK != -1:
msg = 'is not compatible with YOLOv5 Multi-GPU DDP training' msg = "is not compatible with YOLOv5 Multi-GPU DDP training"
assert not opt.image_weights, f'--image-weights {msg}' assert not opt.image_weights, f"--image-weights {msg}"
assert not opt.evolve, f'--evolve {msg}' assert not opt.evolve, f"--evolve {msg}"
assert opt.batch_size != -1, f'AutoBatch with --batch-size -1 {msg}, please pass a valid --batch-size' assert opt.batch_size != -1, f"AutoBatch with --batch-size -1 {msg}, please pass a valid --batch-size"
assert opt.batch_size % WORLD_SIZE == 0, f'--batch-size {opt.batch_size} must be multiple of WORLD_SIZE' assert opt.batch_size % WORLD_SIZE == 0, f"--batch-size {opt.batch_size} must be multiple of WORLD_SIZE"
assert torch.cuda.device_count() > LOCAL_RANK, 'insufficient CUDA devices for DDP command' assert torch.cuda.device_count() > LOCAL_RANK, "insufficient CUDA devices for DDP command"
torch.cuda.set_device(LOCAL_RANK) torch.cuda.set_device(LOCAL_RANK)
device = torch.device('cuda', LOCAL_RANK) device = torch.device("cuda", LOCAL_RANK)
dist.init_process_group(backend='nccl' if dist.is_nccl_available() else 'gloo') dist.init_process_group(backend="nccl" if dist.is_nccl_available() else "gloo")
# Train # Train
if not opt.evolve: if not opt.evolve:
@@ -560,65 +634,69 @@ def main(opt, callbacks=Callbacks()):
else: else:
# Hyperparameter evolution metadata (mutation scale 0-1, lower_limit, upper_limit) # Hyperparameter evolution metadata (mutation scale 0-1, lower_limit, upper_limit)
meta = { meta = {
'lr0': (1, 1e-5, 1e-1), # initial learning rate (SGD=1E-2, Adam=1E-3) "lr0": (1, 1e-5, 1e-1), # initial learning rate (SGD=1E-2, Adam=1E-3)
'lrf': (1, 0.01, 1.0), # final OneCycleLR learning rate (lr0 * lrf) "lrf": (1, 0.01, 1.0), # final OneCycleLR learning rate (lr0 * lrf)
'momentum': (0.3, 0.6, 0.98), # SGD momentum/Adam beta1 "momentum": (0.3, 0.6, 0.98), # SGD momentum/Adam beta1
'weight_decay': (1, 0.0, 0.001), # optimizer weight decay "weight_decay": (1, 0.0, 0.001), # optimizer weight decay
'warmup_epochs': (1, 0.0, 5.0), # warmup epochs (fractions ok) "warmup_epochs": (1, 0.0, 5.0), # warmup epochs (fractions ok)
'warmup_momentum': (1, 0.0, 0.95), # warmup initial momentum "warmup_momentum": (1, 0.0, 0.95), # warmup initial momentum
'warmup_bias_lr': (1, 0.0, 0.2), # warmup initial bias lr "warmup_bias_lr": (1, 0.0, 0.2), # warmup initial bias lr
'box': (1, 0.02, 0.2), # box loss gain "box": (1, 0.02, 0.2), # box loss gain
'cls': (1, 0.2, 4.0), # cls loss gain "cls": (1, 0.2, 4.0), # cls loss gain
'cls_pw': (1, 0.5, 2.0), # cls BCELoss positive_weight "cls_pw": (1, 0.5, 2.0), # cls BCELoss positive_weight
'obj': (1, 0.2, 4.0), # obj loss gain (scale with pixels) "obj": (1, 0.2, 4.0), # obj loss gain (scale with pixels)
'obj_pw': (1, 0.5, 2.0), # obj BCELoss positive_weight "obj_pw": (1, 0.5, 2.0), # obj BCELoss positive_weight
'iou_t': (0, 0.1, 0.7), # IoU training threshold "iou_t": (0, 0.1, 0.7), # IoU training threshold
'anchor_t': (1, 2.0, 8.0), # anchor-multiple threshold "anchor_t": (1, 2.0, 8.0), # anchor-multiple threshold
'anchors': (2, 2.0, 10.0), # anchors per output grid (0 to ignore) "anchors": (2, 2.0, 10.0), # anchors per output grid (0 to ignore)
'fl_gamma': (0, 0.0, 2.0), # focal loss gamma (efficientDet default gamma=1.5) "fl_gamma": (0, 0.0, 2.0), # focal loss gamma (efficientDet default gamma=1.5)
'hsv_h': (1, 0.0, 0.1), # image HSV-Hue augmentation (fraction) "hsv_h": (1, 0.0, 0.1), # image HSV-Hue augmentation (fraction)
'hsv_s': (1, 0.0, 0.9), # image HSV-Saturation augmentation (fraction) "hsv_s": (1, 0.0, 0.9), # image HSV-Saturation augmentation (fraction)
'hsv_v': (1, 0.0, 0.9), # image HSV-Value augmentation (fraction) "hsv_v": (1, 0.0, 0.9), # image HSV-Value augmentation (fraction)
'degrees': (1, 0.0, 45.0), # image rotation (+/- deg) "degrees": (1, 0.0, 45.0), # image rotation (+/- deg)
'translate': (1, 0.0, 0.9), # image translation (+/- fraction) "translate": (1, 0.0, 0.9), # image translation (+/- fraction)
'scale': (1, 0.0, 0.9), # image scale (+/- gain) "scale": (1, 0.0, 0.9), # image scale (+/- gain)
'shear': (1, 0.0, 10.0), # image shear (+/- deg) "shear": (1, 0.0, 10.0), # image shear (+/- deg)
'perspective': (0, 0.0, 0.001), # image perspective (+/- fraction), range 0-0.001 "perspective": (0, 0.0, 0.001), # image perspective (+/- fraction), range 0-0.001
'flipud': (1, 0.0, 1.0), # image flip up-down (probability) "flipud": (1, 0.0, 1.0), # image flip up-down (probability)
'fliplr': (0, 0.0, 1.0), # image flip left-right (probability) "fliplr": (0, 0.0, 1.0), # image flip left-right (probability)
'mosaic': (1, 0.0, 1.0), # image mixup (probability) "mosaic": (1, 0.0, 1.0), # image mixup (probability)
'mixup': (1, 0.0, 1.0), # image mixup (probability) "mixup": (1, 0.0, 1.0), # image mixup (probability)
'copy_paste': (1, 0.0, 1.0)} # segment copy-paste (probability) "copy_paste": (1, 0.0, 1.0),
} # segment copy-paste (probability)
with open(opt.hyp, errors='ignore') as f: with open(opt.hyp, errors="ignore") as f:
hyp = yaml.safe_load(f) # load hyps dict hyp = yaml.safe_load(f) # load hyps dict
if 'anchors' not in hyp: # anchors commented in hyp.yaml if "anchors" not in hyp: # anchors commented in hyp.yaml
hyp['anchors'] = 3 hyp["anchors"] = 3
if opt.noautoanchor: if opt.noautoanchor:
del hyp['anchors'], meta['anchors'] del hyp["anchors"], meta["anchors"]
opt.noval, opt.nosave, save_dir = True, True, Path(opt.save_dir) # only val/save final epoch opt.noval, opt.nosave, save_dir = True, True, Path(opt.save_dir) # only val/save final epoch
# ei = [isinstance(x, (int, float)) for x in hyp.values()] # evolvable indices # ei = [isinstance(x, (int, float)) for x in hyp.values()] # evolvable indices
evolve_yaml, evolve_csv = save_dir / 'hyp_evolve.yaml', save_dir / 'evolve.csv' evolve_yaml, evolve_csv = save_dir / "hyp_evolve.yaml", save_dir / "evolve.csv"
if opt.bucket: if opt.bucket:
# download evolve.csv if exists # download evolve.csv if exists
subprocess.run([ subprocess.run(
'gsutil', [
'cp', "gsutil",
f'gs://{opt.bucket}/evolve.csv', "cp",
str(evolve_csv), ]) f"gs://{opt.bucket}/evolve.csv",
str(evolve_csv),
]
)
for _ in range(opt.evolve): # generations to evolve for _ in range(opt.evolve): # generations to evolve
if evolve_csv.exists(): # if evolve.csv exists: select best hyps and mutate if evolve_csv.exists(): # if evolve.csv exists: select best hyps and mutate
# Select parent(s) # Select parent(s)
parent = 'single' # parent selection method: 'single' or 'weighted' parent = "single" # parent selection method: 'single' or 'weighted'
x = np.loadtxt(evolve_csv, ndmin=2, delimiter=',', skiprows=1) x = np.loadtxt(evolve_csv, ndmin=2, delimiter=",", skiprows=1)
n = min(5, len(x)) # number of previous results to consider n = min(5, len(x)) # number of previous results to consider
x = x[np.argsort(-fitness(x))][:n] # top n mutations x = x[np.argsort(-fitness(x))][:n] # top n mutations
w = fitness(x) - fitness(x).min() + 1E-6 # weights (sum > 0) w = fitness(x) - fitness(x).min() + 1e-6 # weights (sum > 0)
if parent == 'single' or len(x) == 1: if parent == "single" or len(x) == 1:
# x = x[random.randint(0, n - 1)] # random selection # x = x[random.randint(0, n - 1)] # random selection
x = x[random.choices(range(n), weights=w)[0]] # weighted selection x = x[random.choices(range(n), weights=w)[0]] # weighted selection
elif parent == 'weighted': elif parent == "weighted":
x = (x * w.reshape(n, 1)).sum(0) / w.sum() # weighted combination x = (x * w.reshape(n, 1)).sum(0) / w.sum() # weighted combination
# Mutate # Mutate
@@ -647,9 +725,11 @@ def main(opt, callbacks=Callbacks()):
# Plot results # Plot results
plot_evolve(evolve_csv) plot_evolve(evolve_csv)
LOGGER.info(f'Hyperparameter evolution finished {opt.evolve} generations\n' LOGGER.info(
f"Results saved to {colorstr('bold', save_dir)}\n" f'Hyperparameter evolution finished {opt.evolve} generations\n'
f'Usage example: $ python train.py --hyp {evolve_yaml}') f"Results saved to {colorstr('bold', save_dir)}\n"
f'Usage example: $ python train.py --hyp {evolve_yaml}'
)
def run(**kwargs): def run(**kwargs):
@@ -661,6 +741,6 @@ def run(**kwargs):
return opt return opt
if __name__ == '__main__': if __name__ == "__main__":
opt = parse_opt() opt = parse_opt()
main(opt) main(opt)
+166 -131
View File
@@ -1,6 +1,6 @@
# YOLOv5 🚀 by Ultralytics, AGPL-3.0 license # YOLOv5 🚀 by Ultralytics, AGPL-3.0 license
""" """
Validate a trained YOLOv5 segment model on a segment dataset Validate a trained YOLOv5 segment model on a segment dataset.
Usage: Usage:
$ bash data/scripts/get_coco.sh --val --segments # download COCO-segments val split (1G, 5000 images) $ bash data/scripts/get_coco.sh --val --segments # download COCO-segments val split (1G, 5000 images)
@@ -43,9 +43,24 @@ import torch.nn.functional as F
from models.common import DetectMultiBackend from models.common import DetectMultiBackend
from models.yolo import SegmentationModel from models.yolo import SegmentationModel
from utils.callbacks import Callbacks from utils.callbacks import Callbacks
from utils.general import (LOGGER, NUM_THREADS, TQDM_BAR_FORMAT, Profile, check_dataset, check_img_size, from utils.general import (
check_requirements, check_yaml, coco80_to_coco91_class, colorstr, increment_path, LOGGER,
non_max_suppression, print_args, scale_boxes, xywh2xyxy, xyxy2xywh) NUM_THREADS,
TQDM_BAR_FORMAT,
Profile,
check_dataset,
check_img_size,
check_requirements,
check_yaml,
coco80_to_coco91_class,
colorstr,
increment_path,
non_max_suppression,
print_args,
scale_boxes,
xywh2xyxy,
xyxy2xywh,
)
from utils.metrics import ConfusionMatrix, box_iou from utils.metrics import ConfusionMatrix, box_iou
from utils.plots import output_to_target, plot_val_study from utils.plots import output_to_target, plot_val_study
from utils.segment.dataloaders import create_dataloader from utils.segment.dataloaders import create_dataloader
@@ -61,8 +76,8 @@ def save_one_txt(predn, save_conf, shape, file):
for *xyxy, conf, cls in predn.tolist(): for *xyxy, conf, cls in predn.tolist():
xywh = (xyxy2xywh(torch.tensor(xyxy).view(1, 4)) / gn).view(-1).tolist() # normalized xywh xywh = (xyxy2xywh(torch.tensor(xyxy).view(1, 4)) / gn).view(-1).tolist() # normalized xywh
line = (cls, *xywh, conf) if save_conf else (cls, *xywh) # label format line = (cls, *xywh, conf) if save_conf else (cls, *xywh) # label format
with open(file, 'a') as f: with open(file, "a") as f:
f.write(('%g ' * len(line)).rstrip() % line + '\n') f.write(("%g " * len(line)).rstrip() % line + "\n")
def save_one_json(predn, jdict, path, class_map, pred_masks): def save_one_json(predn, jdict, path, class_map, pred_masks):
@@ -70,8 +85,8 @@ def save_one_json(predn, jdict, path, class_map, pred_masks):
from pycocotools.mask import encode from pycocotools.mask import encode
def single_encode(x): def single_encode(x):
rle = encode(np.asarray(x[:, :, None], order='F', dtype='uint8'))[0] rle = encode(np.asarray(x[:, :, None], order="F", dtype="uint8"))[0]
rle['counts'] = rle['counts'].decode('utf-8') rle["counts"] = rle["counts"].decode("utf-8")
return rle return rle
image_id = int(path.stem) if path.stem.isnumeric() else path.stem image_id = int(path.stem) if path.stem.isnumeric() else path.stem
@@ -81,12 +96,15 @@ def save_one_json(predn, jdict, path, class_map, pred_masks):
with ThreadPool(NUM_THREADS) as pool: with ThreadPool(NUM_THREADS) as pool:
rles = pool.map(single_encode, pred_masks) rles = pool.map(single_encode, pred_masks)
for i, (p, b) in enumerate(zip(predn.tolist(), box.tolist())): for i, (p, b) in enumerate(zip(predn.tolist(), box.tolist())):
jdict.append({ jdict.append(
'image_id': image_id, {
'category_id': class_map[int(p[5])], "image_id": image_id,
'bbox': [round(x, 3) for x in b], "category_id": class_map[int(p[5])],
'score': round(p[4], 5), "bbox": [round(x, 3) for x in b],
'segmentation': rles[i]}) "score": round(p[4], 5),
"segmentation": rles[i],
}
)
def process_batch(detections, labels, iouv, pred_masks=None, gt_masks=None, overlap=False, masks=False): def process_batch(detections, labels, iouv, pred_masks=None, gt_masks=None, overlap=False, masks=False):
@@ -105,7 +123,7 @@ def process_batch(detections, labels, iouv, pred_masks=None, gt_masks=None, over
gt_masks = gt_masks.repeat(nl, 1, 1) # shape(1,640,640) -> (n,640,640) gt_masks = gt_masks.repeat(nl, 1, 1) # shape(1,640,640) -> (n,640,640)
gt_masks = torch.where(gt_masks == index, 1.0, 0.0) gt_masks = torch.where(gt_masks == index, 1.0, 0.0)
if gt_masks.shape[1:] != pred_masks.shape[1:]: if gt_masks.shape[1:] != pred_masks.shape[1:]:
gt_masks = F.interpolate(gt_masks[None], pred_masks.shape[1:], mode='bilinear', align_corners=False)[0] gt_masks = F.interpolate(gt_masks[None], pred_masks.shape[1:], mode="bilinear", align_corners=False)[0]
gt_masks = gt_masks.gt_(0.5) gt_masks = gt_masks.gt_(0.5)
iou = mask_iou(gt_masks.view(gt_masks.shape[0], -1), pred_masks.view(pred_masks.shape[0], -1)) iou = mask_iou(gt_masks.view(gt_masks.shape[0], -1), pred_masks.view(pred_masks.shape[0], -1))
else: # boxes else: # boxes
@@ -128,39 +146,39 @@ def process_batch(detections, labels, iouv, pred_masks=None, gt_masks=None, over
@smart_inference_mode() @smart_inference_mode()
def run( def run(
data, data,
weights=None, # model.pt path(s) weights=None, # model.pt path(s)
batch_size=32, # batch size batch_size=32, # batch size
imgsz=640, # inference size (pixels) imgsz=640, # inference size (pixels)
conf_thres=0.001, # confidence threshold conf_thres=0.001, # confidence threshold
iou_thres=0.6, # NMS IoU threshold iou_thres=0.6, # NMS IoU threshold
max_det=300, # maximum detections per image max_det=300, # maximum detections per image
task='val', # train, val, test, speed or study task="val", # train, val, test, speed or study
device='', # cuda device, i.e. 0 or 0,1,2,3 or cpu device="", # cuda device, i.e. 0 or 0,1,2,3 or cpu
workers=8, # max dataloader workers (per RANK in DDP mode) workers=8, # max dataloader workers (per RANK in DDP mode)
single_cls=False, # treat as single-class dataset single_cls=False, # treat as single-class dataset
augment=False, # augmented inference augment=False, # augmented inference
verbose=False, # verbose output verbose=False, # verbose output
save_txt=False, # save results to *.txt save_txt=False, # save results to *.txt
save_hybrid=False, # save label+prediction hybrid results to *.txt save_hybrid=False, # save label+prediction hybrid results to *.txt
save_conf=False, # save confidences in --save-txt labels save_conf=False, # save confidences in --save-txt labels
save_json=False, # save a COCO-JSON results file save_json=False, # save a COCO-JSON results file
project=ROOT / 'runs/val-seg', # save to project/name project=ROOT / "runs/val-seg", # save to project/name
name='exp', # save to project/name name="exp", # save to project/name
exist_ok=False, # existing project/name ok, do not increment exist_ok=False, # existing project/name ok, do not increment
half=True, # use FP16 half-precision inference half=True, # use FP16 half-precision inference
dnn=False, # use OpenCV DNN for ONNX inference dnn=False, # use OpenCV DNN for ONNX inference
model=None, model=None,
dataloader=None, dataloader=None,
save_dir=Path(''), save_dir=Path(""),
plots=True, plots=True,
overlap=False, overlap=False,
mask_downsample_ratio=1, mask_downsample_ratio=1,
compute_loss=None, compute_loss=None,
callbacks=Callbacks(), callbacks=Callbacks(),
): ):
if save_json: if save_json:
check_requirements('pycocotools>=2.0.6') check_requirements("pycocotools>=2.0.6")
process = process_mask_native # more accurate process = process_mask_native # more accurate
else: else:
process = process_mask # faster process = process_mask # faster
@@ -169,7 +187,7 @@ def run(
training = model is not None training = model is not None
if training: # called by train.py if training: # called by train.py
device, pt, jit, engine = next(model.parameters()).device, True, False, False # get model device, PyTorch model device, pt, jit, engine = next(model.parameters()).device, True, False, False # get model device, PyTorch model
half &= device.type != 'cpu' # half precision only supported on CUDA half &= device.type != "cpu" # half precision only supported on CUDA
model.half() if half else model.float() model.half() if half else model.float()
nm = de_parallel(model).model[-1].nm # number of masks nm = de_parallel(model).model[-1].nm # number of masks
else: # called directly else: # called directly
@@ -177,7 +195,7 @@ def run(
# Directories # Directories
save_dir = increment_path(Path(project) / name, exist_ok=exist_ok) # increment run save_dir = increment_path(Path(project) / name, exist_ok=exist_ok) # increment run
(save_dir / 'labels' if save_txt else save_dir).mkdir(parents=True, exist_ok=True) # make dir (save_dir / "labels" if save_txt else save_dir).mkdir(parents=True, exist_ok=True) # make dir
# Load model # Load model
model = DetectMultiBackend(weights, device=device, dnn=dnn, data=data, fp16=half) model = DetectMultiBackend(weights, device=device, dnn=dnn, data=data, fp16=half)
@@ -191,16 +209,16 @@ def run(
device = model.device device = model.device
if not (pt or jit): if not (pt or jit):
batch_size = 1 # export.py models default to batch-size 1 batch_size = 1 # export.py models default to batch-size 1
LOGGER.info(f'Forcing --batch-size 1 square inference (1,3,{imgsz},{imgsz}) for non-PyTorch models') LOGGER.info(f"Forcing --batch-size 1 square inference (1,3,{imgsz},{imgsz}) for non-PyTorch models")
# Data # Data
data = check_dataset(data) # check data = check_dataset(data) # check
# Configure # Configure
model.eval() model.eval()
cuda = device.type != 'cpu' cuda = device.type != "cpu"
is_coco = isinstance(data.get('val'), str) and data['val'].endswith(f'coco{os.sep}val2017.txt') # COCO dataset is_coco = isinstance(data.get("val"), str) and data["val"].endswith(f"coco{os.sep}val2017.txt") # COCO dataset
nc = 1 if single_cls else int(data['nc']) # number of classes nc = 1 if single_cls else int(data["nc"]) # number of classes
iouv = torch.linspace(0.5, 0.95, 10, device=device) # iou vector for mAP@0.5:0.95 iouv = torch.linspace(0.5, 0.95, 10, device=device) # iou vector for mAP@0.5:0.95
niou = iouv.numel() niou = iouv.numel()
@@ -208,31 +226,46 @@ def run(
if not training: if not training:
if pt and not single_cls: # check --weights are trained on --data if pt and not single_cls: # check --weights are trained on --data
ncm = model.model.nc ncm = model.model.nc
assert ncm == nc, f'{weights} ({ncm} classes) trained on different --data than what you passed ({nc} ' \ assert ncm == nc, (
f'classes). Pass correct combination of --weights and --data that are trained together.' f"{weights} ({ncm} classes) trained on different --data than what you passed ({nc} "
f"classes). Pass correct combination of --weights and --data that are trained together."
)
model.warmup(imgsz=(1 if pt else batch_size, 3, imgsz, imgsz)) # warmup model.warmup(imgsz=(1 if pt else batch_size, 3, imgsz, imgsz)) # warmup
pad, rect = (0.0, False) if task == 'speed' else (0.5, pt) # square inference for benchmarks pad, rect = (0.0, False) if task == "speed" else (0.5, pt) # square inference for benchmarks
task = task if task in ('train', 'val', 'test') else 'val' # path to train/val/test images task = task if task in ("train", "val", "test") else "val" # path to train/val/test images
dataloader = create_dataloader(data[task], dataloader = create_dataloader(
imgsz, data[task],
batch_size, imgsz,
stride, batch_size,
single_cls, stride,
pad=pad, single_cls,
rect=rect, pad=pad,
workers=workers, rect=rect,
prefix=colorstr(f'{task}: '), workers=workers,
overlap_mask=overlap, prefix=colorstr(f"{task}: "),
mask_downsample_ratio=mask_downsample_ratio)[0] overlap_mask=overlap,
mask_downsample_ratio=mask_downsample_ratio,
)[0]
seen = 0 seen = 0
confusion_matrix = ConfusionMatrix(nc=nc) confusion_matrix = ConfusionMatrix(nc=nc)
names = model.names if hasattr(model, 'names') else model.module.names # get class names names = model.names if hasattr(model, "names") else model.module.names # get class names
if isinstance(names, (list, tuple)): # old format if isinstance(names, (list, tuple)): # old format
names = dict(enumerate(names)) names = dict(enumerate(names))
class_map = coco80_to_coco91_class() if is_coco else list(range(1000)) class_map = coco80_to_coco91_class() if is_coco else list(range(1000))
s = ('%22s' + '%11s' * 10) % ('Class', 'Images', 'Instances', 'Box(P', 'R', 'mAP50', 'mAP50-95)', 'Mask(P', 'R', s = ("%22s" + "%11s" * 10) % (
'mAP50', 'mAP50-95)') "Class",
"Images",
"Instances",
"Box(P",
"R",
"mAP50",
"mAP50-95)",
"Mask(P",
"R",
"mAP50",
"mAP50-95)",
)
dt = Profile(device=device), Profile(device=device), Profile(device=device) dt = Profile(device=device), Profile(device=device), Profile(device=device)
metrics = Metrics() metrics = Metrics()
loss = torch.zeros(4, device=device) loss = torch.zeros(4, device=device)
@@ -263,14 +296,9 @@ def run(
targets[:, 2:] *= torch.tensor((width, height, width, height), device=device) # to pixels targets[:, 2:] *= torch.tensor((width, height, width, height), device=device) # to pixels
lb = [targets[targets[:, 0] == i, 1:] for i in range(nb)] if save_hybrid else [] # for autolabelling lb = [targets[targets[:, 0] == i, 1:] for i in range(nb)] if save_hybrid else [] # for autolabelling
with dt[2]: with dt[2]:
preds = non_max_suppression(preds, preds = non_max_suppression(
conf_thres, preds, conf_thres, iou_thres, labels=lb, multi_label=True, agnostic=single_cls, max_det=max_det, nm=nm
iou_thres, )
labels=lb,
multi_label=True,
agnostic=single_cls,
max_det=max_det,
nm=nm)
# Metrics # Metrics
plot_masks = [] # masks for plotting plot_masks = [] # masks for plotting
@@ -317,10 +345,11 @@ def run(
# Save/log # Save/log
if save_txt: if save_txt:
save_one_txt(predn, save_conf, shape, file=save_dir / 'labels' / f'{path.stem}.txt') save_one_txt(predn, save_conf, shape, file=save_dir / "labels" / f"{path.stem}.txt")
if save_json: if save_json:
pred_masks = scale_image(im[si].shape[1:], pred_masks = scale_image(
pred_masks.permute(1, 2, 0).contiguous().cpu().numpy(), shape, shapes[si][1]) im[si].shape[1:], pred_masks.permute(1, 2, 0).contiguous().cpu().numpy(), shape, shapes[si][1]
)
save_one_json(predn, jdict, path, class_map, pred_masks) # append to COCO-JSON dictionary save_one_json(predn, jdict, path, class_map, pred_masks) # append to COCO-JSON dictionary
# callbacks.run('on_val_image_end', pred, predn, path, names, im[si]) # callbacks.run('on_val_image_end', pred, predn, path, names, im[si])
@@ -328,9 +357,15 @@ def run(
if plots and batch_i < 3: if plots and batch_i < 3:
if len(plot_masks): if len(plot_masks):
plot_masks = torch.cat(plot_masks, dim=0) plot_masks = torch.cat(plot_masks, dim=0)
plot_images_and_masks(im, targets, masks, paths, save_dir / f'val_batch{batch_i}_labels.jpg', names) plot_images_and_masks(im, targets, masks, paths, save_dir / f"val_batch{batch_i}_labels.jpg", names)
plot_images_and_masks(im, output_to_target(preds, max_det=15), plot_masks, paths, plot_images_and_masks(
save_dir / f'val_batch{batch_i}_pred.jpg', names) # pred im,
output_to_target(preds, max_det=15),
plot_masks,
paths,
save_dir / f"val_batch{batch_i}_pred.jpg",
names,
) # pred
# callbacks.run('on_val_batch_end') # callbacks.run('on_val_batch_end')
@@ -342,10 +377,10 @@ def run(
nt = np.bincount(stats[4].astype(int), minlength=nc) # number of targets per class nt = np.bincount(stats[4].astype(int), minlength=nc) # number of targets per class
# Print results # Print results
pf = '%22s' + '%11i' * 2 + '%11.3g' * 8 # print format pf = "%22s" + "%11i" * 2 + "%11.3g" * 8 # print format
LOGGER.info(pf % ('all', seen, nt.sum(), *metrics.mean_results())) LOGGER.info(pf % ("all", seen, nt.sum(), *metrics.mean_results()))
if nt.sum() == 0: if nt.sum() == 0:
LOGGER.warning(f'WARNING ⚠️ no labels found in {task} set, can not compute metrics without labels') LOGGER.warning(f"WARNING ⚠️ no labels found in {task} set, can not compute metrics without labels")
# Print results per class # Print results per class
if (verbose or (nc < 50 and not training)) and nc > 1 and len(stats): if (verbose or (nc < 50 and not training)) and nc > 1 and len(stats):
@@ -353,10 +388,10 @@ def run(
LOGGER.info(pf % (names[c], seen, nt[c], *metrics.class_result(i))) LOGGER.info(pf % (names[c], seen, nt[c], *metrics.class_result(i)))
# Print speeds # Print speeds
t = tuple(x.t / seen * 1E3 for x in dt) # speeds per image t = tuple(x.t / seen * 1e3 for x in dt) # speeds per image
if not training: if not training:
shape = (batch_size, 3, imgsz, imgsz) shape = (batch_size, 3, imgsz, imgsz)
LOGGER.info(f'Speed: %.1fms pre-process, %.1fms inference, %.1fms NMS per image at shape {shape}' % t) LOGGER.info(f"Speed: %.1fms pre-process, %.1fms inference, %.1fms NMS per image at shape {shape}" % t)
# Plots # Plots
if plots: if plots:
@@ -367,11 +402,11 @@ def run(
# Save JSON # Save JSON
if save_json and len(jdict): if save_json and len(jdict):
w = Path(weights[0] if isinstance(weights, list) else weights).stem if weights is not None else '' # weights w = Path(weights[0] if isinstance(weights, list) else weights).stem if weights is not None else "" # weights
anno_json = str(Path('../datasets/coco/annotations/instances_val2017.json')) # annotations anno_json = str(Path("../datasets/coco/annotations/instances_val2017.json")) # annotations
pred_json = str(save_dir / f'{w}_predictions.json') # predictions pred_json = str(save_dir / f"{w}_predictions.json") # predictions
LOGGER.info(f'\nEvaluating pycocotools mAP... saving {pred_json}...') LOGGER.info(f"\nEvaluating pycocotools mAP... saving {pred_json}...")
with open(pred_json, 'w') as f: with open(pred_json, "w") as f:
json.dump(jdict, f) json.dump(jdict, f)
try: # https://github.com/cocodataset/cocoapi/blob/master/PythonAPI/pycocoEvalDemo.ipynb try: # https://github.com/cocodataset/cocoapi/blob/master/PythonAPI/pycocoEvalDemo.ipynb
@@ -381,7 +416,7 @@ def run(
anno = COCO(anno_json) # init annotations api anno = COCO(anno_json) # init annotations api
pred = anno.loadRes(pred_json) # init predictions api pred = anno.loadRes(pred_json) # init predictions api
results = [] results = []
for eval in COCOeval(anno, pred, 'bbox'), COCOeval(anno, pred, 'segm'): for eval in COCOeval(anno, pred, "bbox"), COCOeval(anno, pred, "segm"):
if is_coco: if is_coco:
eval.params.imgIds = [int(Path(x).stem) for x in dataloader.dataset.im_files] # img ID to evaluate eval.params.imgIds = [int(Path(x).stem) for x in dataloader.dataset.im_files] # img ID to evaluate
eval.evaluate() eval.evaluate()
@@ -390,12 +425,12 @@ def run(
results.extend(eval.stats[:2]) # update results (mAP@0.5:0.95, mAP@0.5) results.extend(eval.stats[:2]) # update results (mAP@0.5:0.95, mAP@0.5)
map_bbox, map50_bbox, map_mask, map50_mask = results map_bbox, map50_bbox, map_mask, map50_mask = results
except Exception as e: except Exception as e:
LOGGER.info(f'pycocotools unable to run: {e}') LOGGER.info(f"pycocotools unable to run: {e}")
# Return results # Return results
model.float() # for training model.float() # for training
if not training: if not training:
s = f"\n{len(list(save_dir.glob('labels/*.txt')))} labels saved to {save_dir / 'labels'}" if save_txt else '' s = f"\n{len(list(save_dir.glob('labels/*.txt')))} labels saved to {save_dir / 'labels'}" if save_txt else ""
LOGGER.info(f"Results saved to {colorstr('bold', save_dir)}{s}") LOGGER.info(f"Results saved to {colorstr('bold', save_dir)}{s}")
final_metric = mp_bbox, mr_bbox, map50_bbox, map_bbox, mp_mask, mr_mask, map50_mask, map_mask final_metric = mp_bbox, mr_bbox, map50_bbox, map_bbox, mp_mask, mr_mask, map50_mask, map_mask
return (*final_metric, *(loss.cpu() / len(dataloader)).tolist()), metrics.get_maps(nc), t return (*final_metric, *(loss.cpu() / len(dataloader)).tolist()), metrics.get_maps(nc), t
@@ -403,28 +438,28 @@ def run(
def parse_opt(): def parse_opt():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('--data', type=str, default=ROOT / 'data/coco128-seg.yaml', help='dataset.yaml path') parser.add_argument("--data", type=str, default=ROOT / "data/coco128-seg.yaml", help="dataset.yaml path")
parser.add_argument('--weights', nargs='+', type=str, default=ROOT / 'yolov5s-seg.pt', help='model path(s)') parser.add_argument("--weights", nargs="+", type=str, default=ROOT / "yolov5s-seg.pt", help="model path(s)")
parser.add_argument('--batch-size', type=int, default=32, help='batch size') parser.add_argument("--batch-size", type=int, default=32, help="batch size")
parser.add_argument('--imgsz', '--img', '--img-size', type=int, default=640, help='inference size (pixels)') parser.add_argument("--imgsz", "--img", "--img-size", type=int, default=640, help="inference size (pixels)")
parser.add_argument('--conf-thres', type=float, default=0.001, help='confidence threshold') parser.add_argument("--conf-thres", type=float, default=0.001, help="confidence threshold")
parser.add_argument('--iou-thres', type=float, default=0.6, help='NMS IoU threshold') parser.add_argument("--iou-thres", type=float, default=0.6, help="NMS IoU threshold")
parser.add_argument('--max-det', type=int, default=300, help='maximum detections per image') parser.add_argument("--max-det", type=int, default=300, help="maximum detections per image")
parser.add_argument('--task', default='val', help='train, val, test, speed or study') parser.add_argument("--task", default="val", help="train, val, test, speed or study")
parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu') parser.add_argument("--device", default="", help="cuda device, i.e. 0 or 0,1,2,3 or cpu")
parser.add_argument('--workers', type=int, default=8, help='max dataloader workers (per RANK in DDP mode)') parser.add_argument("--workers", type=int, default=8, help="max dataloader workers (per RANK in DDP mode)")
parser.add_argument('--single-cls', action='store_true', help='treat as single-class dataset') parser.add_argument("--single-cls", action="store_true", help="treat as single-class dataset")
parser.add_argument('--augment', action='store_true', help='augmented inference') parser.add_argument("--augment", action="store_true", help="augmented inference")
parser.add_argument('--verbose', action='store_true', help='report mAP by class') parser.add_argument("--verbose", action="store_true", help="report mAP by class")
parser.add_argument('--save-txt', action='store_true', help='save results to *.txt') parser.add_argument("--save-txt", action="store_true", help="save results to *.txt")
parser.add_argument('--save-hybrid', action='store_true', help='save label+prediction hybrid results to *.txt') parser.add_argument("--save-hybrid", action="store_true", help="save label+prediction hybrid results to *.txt")
parser.add_argument('--save-conf', action='store_true', help='save confidences in --save-txt labels') parser.add_argument("--save-conf", action="store_true", help="save confidences in --save-txt labels")
parser.add_argument('--save-json', action='store_true', help='save a COCO-JSON results file') parser.add_argument("--save-json", action="store_true", help="save a COCO-JSON results file")
parser.add_argument('--project', default=ROOT / 'runs/val-seg', help='save results to project/name') parser.add_argument("--project", default=ROOT / "runs/val-seg", help="save results to project/name")
parser.add_argument('--name', default='exp', help='save to project/name') parser.add_argument("--name", default="exp", help="save to project/name")
parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment') parser.add_argument("--exist-ok", action="store_true", help="existing project/name ok, do not increment")
parser.add_argument('--half', action='store_true', help='use FP16 half-precision inference') parser.add_argument("--half", action="store_true", help="use FP16 half-precision inference")
parser.add_argument('--dnn', action='store_true', help='use OpenCV DNN for ONNX inference') parser.add_argument("--dnn", action="store_true", help="use OpenCV DNN for ONNX inference")
opt = parser.parse_args() opt = parser.parse_args()
opt.data = check_yaml(opt.data) # check YAML opt.data = check_yaml(opt.data) # check YAML
# opt.save_json |= opt.data.endswith('coco.yaml') # opt.save_json |= opt.data.endswith('coco.yaml')
@@ -434,40 +469,40 @@ def parse_opt():
def main(opt): def main(opt):
check_requirements(ROOT / 'requirements.txt', exclude=('tensorboard', 'thop')) check_requirements(ROOT / "requirements.txt", exclude=("tensorboard", "thop"))
if opt.task in ('train', 'val', 'test'): # run normally if opt.task in ("train", "val", "test"): # run normally
if opt.conf_thres > 0.001: # https://github.com/ultralytics/yolov5/issues/1466 if opt.conf_thres > 0.001: # https://github.com/ultralytics/yolov5/issues/1466
LOGGER.warning(f'WARNING ⚠️ confidence threshold {opt.conf_thres} > 0.001 produces invalid results') LOGGER.warning(f"WARNING ⚠️ confidence threshold {opt.conf_thres} > 0.001 produces invalid results")
if opt.save_hybrid: if opt.save_hybrid:
LOGGER.warning('WARNING ⚠️ --save-hybrid returns high mAP from hybrid labels, not from predictions alone') LOGGER.warning("WARNING ⚠️ --save-hybrid returns high mAP from hybrid labels, not from predictions alone")
run(**vars(opt)) run(**vars(opt))
else: else:
weights = opt.weights if isinstance(opt.weights, list) else [opt.weights] weights = opt.weights if isinstance(opt.weights, list) else [opt.weights]
opt.half = torch.cuda.is_available() and opt.device != 'cpu' # FP16 for fastest results opt.half = torch.cuda.is_available() and opt.device != "cpu" # FP16 for fastest results
if opt.task == 'speed': # speed benchmarks if opt.task == "speed": # speed benchmarks
# python val.py --task speed --data coco.yaml --batch 1 --weights yolov5n.pt yolov5s.pt... # python val.py --task speed --data coco.yaml --batch 1 --weights yolov5n.pt yolov5s.pt...
opt.conf_thres, opt.iou_thres, opt.save_json = 0.25, 0.45, False opt.conf_thres, opt.iou_thres, opt.save_json = 0.25, 0.45, False
for opt.weights in weights: for opt.weights in weights:
run(**vars(opt), plots=False) run(**vars(opt), plots=False)
elif opt.task == 'study': # speed vs mAP benchmarks elif opt.task == "study": # speed vs mAP benchmarks
# python val.py --task study --data coco.yaml --iou 0.7 --weights yolov5n.pt yolov5s.pt... # python val.py --task study --data coco.yaml --iou 0.7 --weights yolov5n.pt yolov5s.pt...
for opt.weights in weights: for opt.weights in weights:
f = f'study_{Path(opt.data).stem}_{Path(opt.weights).stem}.txt' # filename to save to f = f"study_{Path(opt.data).stem}_{Path(opt.weights).stem}.txt" # filename to save to
x, y = list(range(256, 1536 + 128, 128)), [] # x axis (image sizes), y axis x, y = list(range(256, 1536 + 128, 128)), [] # x axis (image sizes), y axis
for opt.imgsz in x: # img-size for opt.imgsz in x: # img-size
LOGGER.info(f'\nRunning {f} --imgsz {opt.imgsz}...') LOGGER.info(f"\nRunning {f} --imgsz {opt.imgsz}...")
r, _, t = run(**vars(opt), plots=False) r, _, t = run(**vars(opt), plots=False)
y.append(r + t) # results and times y.append(r + t) # results and times
np.savetxt(f, y, fmt='%10.4g') # save np.savetxt(f, y, fmt="%10.4g") # save
subprocess.run(['zip', '-r', 'study.zip', 'study_*.txt']) subprocess.run(["zip", "-r", "study.zip", "study_*.txt"])
plot_val_study(x=x) # plot plot_val_study(x=x) # plot
else: else:
raise NotImplementedError(f'--task {opt.task} not in ("train", "val", "test", "speed", "study")') raise NotImplementedError(f'--task {opt.task} not in ("train", "val", "test", "speed", "study")')
if __name__ == '__main__': if __name__ == "__main__":
opt = parse_opt() opt = parse_opt()
main(opt) main(opt)
+334 -259
View File
@@ -1,7 +1,6 @@
# YOLOv5 🚀 by Ultralytics, AGPL-3.0 license # YOLOv5 🚀 by Ultralytics, AGPL-3.0 license
""" """
Train a YOLOv5 model on a custom dataset. Train a YOLOv5 model on a custom dataset. Models and datasets download automatically from the latest YOLOv5 release.
Models and datasets download automatically from the latest YOLOv5 release.
Usage - Single-GPU training: Usage - Single-GPU training:
$ python train.py --data coco128.yaml --weights yolov5s.pt --img 640 # from pretrained (recommended) $ python train.py --data coco128.yaml --weights yolov5s.pt --img 640 # from pretrained (recommended)
@@ -53,56 +52,97 @@ from utils.autobatch import check_train_batch_size
from utils.callbacks import Callbacks from utils.callbacks import Callbacks
from utils.dataloaders import create_dataloader from utils.dataloaders import create_dataloader
from utils.downloads import attempt_download, is_url from utils.downloads import attempt_download, is_url
from utils.general import (LOGGER, TQDM_BAR_FORMAT, check_amp, check_dataset, check_file, check_git_info, from utils.general import (
check_git_status, check_img_size, check_requirements, check_suffix, check_yaml, colorstr, LOGGER,
get_latest_run, increment_path, init_seeds, intersect_dicts, labels_to_class_weights, TQDM_BAR_FORMAT,
labels_to_image_weights, methods, one_cycle, print_args, print_mutation, strip_optimizer, check_amp,
yaml_save) check_dataset,
check_file,
check_git_info,
check_git_status,
check_img_size,
check_requirements,
check_suffix,
check_yaml,
colorstr,
get_latest_run,
increment_path,
init_seeds,
intersect_dicts,
labels_to_class_weights,
labels_to_image_weights,
methods,
one_cycle,
print_args,
print_mutation,
strip_optimizer,
yaml_save,
)
from utils.loggers import LOGGERS, Loggers from utils.loggers import LOGGERS, Loggers
from utils.loggers.comet.comet_utils import check_comet_resume from utils.loggers.comet.comet_utils import check_comet_resume
from utils.loss import ComputeLoss from utils.loss import ComputeLoss
from utils.metrics import fitness from utils.metrics import fitness
from utils.plots import plot_evolve from utils.plots import plot_evolve
from utils.torch_utils import (EarlyStopping, ModelEMA, de_parallel, select_device, smart_DDP, smart_optimizer, from utils.torch_utils import (
smart_resume, torch_distributed_zero_first) EarlyStopping,
ModelEMA,
de_parallel,
select_device,
smart_DDP,
smart_optimizer,
smart_resume,
torch_distributed_zero_first,
)
LOCAL_RANK = int(os.getenv('LOCAL_RANK', -1)) # https://pytorch.org/docs/stable/elastic/run.html LOCAL_RANK = int(os.getenv("LOCAL_RANK", -1)) # https://pytorch.org/docs/stable/elastic/run.html
RANK = int(os.getenv('RANK', -1)) RANK = int(os.getenv("RANK", -1))
WORLD_SIZE = int(os.getenv('WORLD_SIZE', 1)) WORLD_SIZE = int(os.getenv("WORLD_SIZE", 1))
GIT_INFO = check_git_info() GIT_INFO = check_git_info()
def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictionary def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictionary
save_dir, epochs, batch_size, weights, single_cls, evolve, data, cfg, resume, noval, nosave, workers, freeze = \ save_dir, epochs, batch_size, weights, single_cls, evolve, data, cfg, resume, noval, nosave, workers, freeze = (
Path(opt.save_dir), opt.epochs, opt.batch_size, opt.weights, opt.single_cls, opt.evolve, opt.data, opt.cfg, \ Path(opt.save_dir),
opt.resume, opt.noval, opt.nosave, opt.workers, opt.freeze opt.epochs,
callbacks.run('on_pretrain_routine_start') opt.batch_size,
opt.weights,
opt.single_cls,
opt.evolve,
opt.data,
opt.cfg,
opt.resume,
opt.noval,
opt.nosave,
opt.workers,
opt.freeze,
)
callbacks.run("on_pretrain_routine_start")
# Directories # Directories
w = save_dir / 'weights' # weights dir w = save_dir / "weights" # weights dir
(w.parent if evolve else w).mkdir(parents=True, exist_ok=True) # make dir (w.parent if evolve else w).mkdir(parents=True, exist_ok=True) # make dir
last, best = w / 'last.pt', w / 'best.pt' last, best = w / "last.pt", w / "best.pt"
# Hyperparameters # Hyperparameters
if isinstance(hyp, str): if isinstance(hyp, str):
with open(hyp, errors='ignore') as f: with open(hyp, errors="ignore") as f:
hyp = yaml.safe_load(f) # load hyps dict hyp = yaml.safe_load(f) # load hyps dict
LOGGER.info(colorstr('hyperparameters: ') + ', '.join(f'{k}={v}' for k, v in hyp.items())) LOGGER.info(colorstr("hyperparameters: ") + ", ".join(f"{k}={v}" for k, v in hyp.items()))
opt.hyp = hyp.copy() # for saving hyps to checkpoints opt.hyp = hyp.copy() # for saving hyps to checkpoints
# Save run settings # Save run settings
if not evolve: if not evolve:
yaml_save(save_dir / 'hyp.yaml', hyp) yaml_save(save_dir / "hyp.yaml", hyp)
yaml_save(save_dir / 'opt.yaml', vars(opt)) yaml_save(save_dir / "opt.yaml", vars(opt))
# Loggers # Loggers
data_dict = None data_dict = None
if RANK in {-1, 0}: if RANK in {-1, 0}:
include_loggers = list(LOGGERS) include_loggers = list(LOGGERS)
if getattr(opt, 'ndjson_console', False): if getattr(opt, "ndjson_console", False):
include_loggers.append('ndjson_console') include_loggers.append("ndjson_console")
if getattr(opt, 'ndjson_file', False): if getattr(opt, "ndjson_file", False):
include_loggers.append('ndjson_file') include_loggers.append("ndjson_file")
loggers = Loggers( loggers = Loggers(
save_dir=save_dir, save_dir=save_dir,
@@ -124,39 +164,39 @@ def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictio
# Config # Config
plots = not evolve and not opt.noplots # create plots plots = not evolve and not opt.noplots # create plots
cuda = device.type != 'cpu' cuda = device.type != "cpu"
init_seeds(opt.seed + 1 + RANK, deterministic=True) init_seeds(opt.seed + 1 + RANK, deterministic=True)
with torch_distributed_zero_first(LOCAL_RANK): with torch_distributed_zero_first(LOCAL_RANK):
data_dict = data_dict or check_dataset(data) # check if None data_dict = data_dict or check_dataset(data) # check if None
train_path, val_path = data_dict['train'], data_dict['val'] train_path, val_path = data_dict["train"], data_dict["val"]
nc = 1 if single_cls else int(data_dict['nc']) # number of classes nc = 1 if single_cls else int(data_dict["nc"]) # number of classes
names = {0: 'item'} if single_cls and len(data_dict['names']) != 1 else data_dict['names'] # class names names = {0: "item"} if single_cls and len(data_dict["names"]) != 1 else data_dict["names"] # class names
is_coco = isinstance(val_path, str) and val_path.endswith('coco/val2017.txt') # COCO dataset is_coco = isinstance(val_path, str) and val_path.endswith("coco/val2017.txt") # COCO dataset
# Model # Model
check_suffix(weights, '.pt') # check weights check_suffix(weights, ".pt") # check weights
pretrained = weights.endswith('.pt') pretrained = weights.endswith(".pt")
if pretrained: if pretrained:
with torch_distributed_zero_first(LOCAL_RANK): with torch_distributed_zero_first(LOCAL_RANK):
weights = attempt_download(weights) # download if not found locally weights = attempt_download(weights) # download if not found locally
ckpt = torch.load(weights, map_location='cpu') # load checkpoint to CPU to avoid CUDA memory leak ckpt = torch.load(weights, map_location="cpu") # load checkpoint to CPU to avoid CUDA memory leak
model = Model(cfg or ckpt['model'].yaml, ch=3, nc=nc, anchors=hyp.get('anchors')).to(device) # create model = Model(cfg or ckpt["model"].yaml, ch=3, nc=nc, anchors=hyp.get("anchors")).to(device) # create
exclude = ['anchor'] if (cfg or hyp.get('anchors')) and not resume else [] # exclude keys exclude = ["anchor"] if (cfg or hyp.get("anchors")) and not resume else [] # exclude keys
csd = ckpt['model'].float().state_dict() # checkpoint state_dict as FP32 csd = ckpt["model"].float().state_dict() # checkpoint state_dict as FP32
csd = intersect_dicts(csd, model.state_dict(), exclude=exclude) # intersect csd = intersect_dicts(csd, model.state_dict(), exclude=exclude) # intersect
model.load_state_dict(csd, strict=False) # load model.load_state_dict(csd, strict=False) # load
LOGGER.info(f'Transferred {len(csd)}/{len(model.state_dict())} items from {weights}') # report LOGGER.info(f"Transferred {len(csd)}/{len(model.state_dict())} items from {weights}") # report
else: else:
model = Model(cfg, ch=3, nc=nc, anchors=hyp.get('anchors')).to(device) # create model = Model(cfg, ch=3, nc=nc, anchors=hyp.get("anchors")).to(device) # create
amp = check_amp(model) # check AMP amp = check_amp(model) # check AMP
# Freeze # Freeze
freeze = [f'model.{x}.' for x in (freeze if len(freeze) > 1 else range(freeze[0]))] # layers to freeze freeze = [f"model.{x}." for x in (freeze if len(freeze) > 1 else range(freeze[0]))] # layers to freeze
for k, v in model.named_parameters(): for k, v in model.named_parameters():
v.requires_grad = True # train all layers v.requires_grad = True # train all layers
# v.register_hook(lambda x: torch.nan_to_num(x)) # NaN to 0 (commented for erratic training results) # v.register_hook(lambda x: torch.nan_to_num(x)) # NaN to 0 (commented for erratic training results)
if any(x in k for x in freeze): if any(x in k for x in freeze):
LOGGER.info(f'freezing {k}') LOGGER.info(f"freezing {k}")
v.requires_grad = False v.requires_grad = False
# Image size # Image size
@@ -166,19 +206,19 @@ def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictio
# Batch size # Batch size
if RANK == -1 and batch_size == -1: # single-GPU only, estimate best batch size if RANK == -1 and batch_size == -1: # single-GPU only, estimate best batch size
batch_size = check_train_batch_size(model, imgsz, amp) batch_size = check_train_batch_size(model, imgsz, amp)
loggers.on_params_update({'batch_size': batch_size}) loggers.on_params_update({"batch_size": batch_size})
# Optimizer # Optimizer
nbs = 64 # nominal batch size nbs = 64 # nominal batch size
accumulate = max(round(nbs / batch_size), 1) # accumulate loss before optimizing accumulate = max(round(nbs / batch_size), 1) # accumulate loss before optimizing
hyp['weight_decay'] *= batch_size * accumulate / nbs # scale weight_decay hyp["weight_decay"] *= batch_size * accumulate / nbs # scale weight_decay
optimizer = smart_optimizer(model, opt.optimizer, hyp['lr0'], hyp['momentum'], hyp['weight_decay']) optimizer = smart_optimizer(model, opt.optimizer, hyp["lr0"], hyp["momentum"], hyp["weight_decay"])
# Scheduler # Scheduler
if opt.cos_lr: if opt.cos_lr:
lf = one_cycle(1, hyp['lrf'], epochs) # cosine 1->hyp['lrf'] lf = one_cycle(1, hyp["lrf"], epochs) # cosine 1->hyp['lrf']
else: else:
lf = lambda x: (1 - x / epochs) * (1.0 - hyp['lrf']) + hyp['lrf'] # linear lf = lambda x: (1 - x / epochs) * (1.0 - hyp["lrf"]) + hyp["lrf"] # linear
scheduler = lr_scheduler.LambdaLR(optimizer, lr_lambda=lf) # plot_lr_scheduler(optimizer, scheduler, epochs) scheduler = lr_scheduler.LambdaLR(optimizer, lr_lambda=lf) # plot_lr_scheduler(optimizer, scheduler, epochs)
# EMA # EMA
@@ -194,58 +234,62 @@ def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictio
# DP mode # DP mode
if cuda and RANK == -1 and torch.cuda.device_count() > 1: if cuda and RANK == -1 and torch.cuda.device_count() > 1:
LOGGER.warning( LOGGER.warning(
'WARNING ⚠️ DP not recommended, use torch.distributed.run for best DDP Multi-GPU results.\n' "WARNING ⚠️ DP not recommended, use torch.distributed.run for best DDP Multi-GPU results.\n"
'See Multi-GPU Tutorial at https://docs.ultralytics.com/yolov5/tutorials/multi_gpu_training to get started.' "See Multi-GPU Tutorial at https://docs.ultralytics.com/yolov5/tutorials/multi_gpu_training to get started."
) )
model = torch.nn.DataParallel(model) model = torch.nn.DataParallel(model)
# SyncBatchNorm # SyncBatchNorm
if opt.sync_bn and cuda and RANK != -1: if opt.sync_bn and cuda and RANK != -1:
model = torch.nn.SyncBatchNorm.convert_sync_batchnorm(model).to(device) model = torch.nn.SyncBatchNorm.convert_sync_batchnorm(model).to(device)
LOGGER.info('Using SyncBatchNorm()') LOGGER.info("Using SyncBatchNorm()")
# Trainloader # Trainloader
train_loader, dataset = create_dataloader(train_path, train_loader, dataset = create_dataloader(
imgsz, train_path,
batch_size // WORLD_SIZE, imgsz,
gs, batch_size // WORLD_SIZE,
single_cls, gs,
hyp=hyp, single_cls,
augment=True, hyp=hyp,
cache=None if opt.cache == 'val' else opt.cache, augment=True,
rect=opt.rect, cache=None if opt.cache == "val" else opt.cache,
rank=LOCAL_RANK, rect=opt.rect,
workers=workers, rank=LOCAL_RANK,
image_weights=opt.image_weights, workers=workers,
quad=opt.quad, image_weights=opt.image_weights,
prefix=colorstr('train: '), quad=opt.quad,
shuffle=True, prefix=colorstr("train: "),
seed=opt.seed) shuffle=True,
seed=opt.seed,
)
labels = np.concatenate(dataset.labels, 0) labels = np.concatenate(dataset.labels, 0)
mlc = int(labels[:, 0].max()) # max label class mlc = int(labels[:, 0].max()) # max label class
assert mlc < nc, f'Label class {mlc} exceeds nc={nc} in {data}. Possible class labels are 0-{nc - 1}' assert mlc < nc, f"Label class {mlc} exceeds nc={nc} in {data}. Possible class labels are 0-{nc - 1}"
# Process 0 # Process 0
if RANK in {-1, 0}: if RANK in {-1, 0}:
val_loader = create_dataloader(val_path, val_loader = create_dataloader(
imgsz, val_path,
batch_size // WORLD_SIZE * 2, imgsz,
gs, batch_size // WORLD_SIZE * 2,
single_cls, gs,
hyp=hyp, single_cls,
cache=None if noval else opt.cache, hyp=hyp,
rect=True, cache=None if noval else opt.cache,
rank=-1, rect=True,
workers=workers * 2, rank=-1,
pad=0.5, workers=workers * 2,
prefix=colorstr('val: '))[0] pad=0.5,
prefix=colorstr("val: "),
)[0]
if not resume: if not resume:
if not opt.noautoanchor: if not opt.noautoanchor:
check_anchors(dataset, model=model, thr=hyp['anchor_t'], imgsz=imgsz) # run AutoAnchor check_anchors(dataset, model=model, thr=hyp["anchor_t"], imgsz=imgsz) # run AutoAnchor
model.half().float() # pre-reduce anchor precision model.half().float() # pre-reduce anchor precision
callbacks.run('on_pretrain_routine_end', labels, names) callbacks.run("on_pretrain_routine_end", labels, names)
# DDP mode # DDP mode
if cuda and RANK != -1: if cuda and RANK != -1:
@@ -253,10 +297,10 @@ def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictio
# Model attributes # Model attributes
nl = de_parallel(model).model[-1].nl # number of detection layers (to scale hyps) nl = de_parallel(model).model[-1].nl # number of detection layers (to scale hyps)
hyp['box'] *= 3 / nl # scale to layers hyp["box"] *= 3 / nl # scale to layers
hyp['cls'] *= nc / 80 * 3 / nl # scale to classes and layers hyp["cls"] *= nc / 80 * 3 / nl # scale to classes and layers
hyp['obj'] *= (imgsz / 640) ** 2 * 3 / nl # scale to image size and layers hyp["obj"] *= (imgsz / 640) ** 2 * 3 / nl # scale to image size and layers
hyp['label_smoothing'] = opt.label_smoothing hyp["label_smoothing"] = opt.label_smoothing
model.nc = nc # attach number of classes to model model.nc = nc # attach number of classes to model
model.hyp = hyp # attach hyperparameters to model model.hyp = hyp # attach hyperparameters to model
model.class_weights = labels_to_class_weights(dataset.labels, nc).to(device) * nc # attach class weights model.class_weights = labels_to_class_weights(dataset.labels, nc).to(device) * nc # attach class weights
@@ -265,7 +309,7 @@ def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictio
# Start training # Start training
t0 = time.time() t0 = time.time()
nb = len(train_loader) # number of batches nb = len(train_loader) # number of batches
nw = max(round(hyp['warmup_epochs'] * nb), 100) # number of warmup iterations, max(3 epochs, 100 iterations) nw = max(round(hyp["warmup_epochs"] * nb), 100) # number of warmup iterations, max(3 epochs, 100 iterations)
# nw = min(nw, (epochs - start_epoch) / 2 * nb) # limit warmup to < 1/2 of training # nw = min(nw, (epochs - start_epoch) / 2 * nb) # limit warmup to < 1/2 of training
last_opt_step = -1 last_opt_step = -1
maps = np.zeros(nc) # mAP per class maps = np.zeros(nc) # mAP per class
@@ -274,13 +318,15 @@ def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictio
scaler = torch.cuda.amp.GradScaler(enabled=amp) scaler = torch.cuda.amp.GradScaler(enabled=amp)
stopper, stop = EarlyStopping(patience=opt.patience), False stopper, stop = EarlyStopping(patience=opt.patience), False
compute_loss = ComputeLoss(model) # init loss class compute_loss = ComputeLoss(model) # init loss class
callbacks.run('on_train_start') callbacks.run("on_train_start")
LOGGER.info(f'Image sizes {imgsz} train, {imgsz} val\n' LOGGER.info(
f'Using {train_loader.num_workers * WORLD_SIZE} dataloader workers\n' f'Image sizes {imgsz} train, {imgsz} val\n'
f"Logging results to {colorstr('bold', save_dir)}\n" f'Using {train_loader.num_workers * WORLD_SIZE} dataloader workers\n'
f'Starting training for {epochs} epochs...') f"Logging results to {colorstr('bold', save_dir)}\n"
f'Starting training for {epochs} epochs...'
)
for epoch in range(start_epoch, epochs): # epoch ------------------------------------------------------------------ for epoch in range(start_epoch, epochs): # epoch ------------------------------------------------------------------
callbacks.run('on_train_epoch_start') callbacks.run("on_train_epoch_start")
model.train() model.train()
# Update image weights (optional, single-GPU only) # Update image weights (optional, single-GPU only)
@@ -297,12 +343,12 @@ def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictio
if RANK != -1: if RANK != -1:
train_loader.sampler.set_epoch(epoch) train_loader.sampler.set_epoch(epoch)
pbar = enumerate(train_loader) pbar = enumerate(train_loader)
LOGGER.info(('\n' + '%11s' * 7) % ('Epoch', 'GPU_mem', 'box_loss', 'obj_loss', 'cls_loss', 'Instances', 'Size')) LOGGER.info(("\n" + "%11s" * 7) % ("Epoch", "GPU_mem", "box_loss", "obj_loss", "cls_loss", "Instances", "Size"))
if RANK in {-1, 0}: if RANK in {-1, 0}:
pbar = tqdm(pbar, total=nb, bar_format=TQDM_BAR_FORMAT) # progress bar pbar = tqdm(pbar, total=nb, bar_format=TQDM_BAR_FORMAT) # progress bar
optimizer.zero_grad() optimizer.zero_grad()
for i, (imgs, targets, paths, _) in pbar: # batch ------------------------------------------------------------- for i, (imgs, targets, paths, _) in pbar: # batch -------------------------------------------------------------
callbacks.run('on_train_batch_start') callbacks.run("on_train_batch_start")
ni = i + nb * epoch # number integrated batches (since train start) ni = i + nb * epoch # number integrated batches (since train start)
imgs = imgs.to(device, non_blocking=True).float() / 255 # uint8 to float32, 0-255 to 0.0-1.0 imgs = imgs.to(device, non_blocking=True).float() / 255 # uint8 to float32, 0-255 to 0.0-1.0
@@ -313,9 +359,9 @@ def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictio
accumulate = max(1, np.interp(ni, xi, [1, nbs / batch_size]).round()) accumulate = max(1, np.interp(ni, xi, [1, nbs / batch_size]).round())
for j, x in enumerate(optimizer.param_groups): for j, x in enumerate(optimizer.param_groups):
# bias lr falls from 0.1 to lr0, all other lrs rise from 0.0 to lr0 # bias lr falls from 0.1 to lr0, all other lrs rise from 0.0 to lr0
x['lr'] = np.interp(ni, xi, [hyp['warmup_bias_lr'] if j == 0 else 0.0, x['initial_lr'] * lf(epoch)]) x["lr"] = np.interp(ni, xi, [hyp["warmup_bias_lr"] if j == 0 else 0.0, x["initial_lr"] * lf(epoch)])
if 'momentum' in x: if "momentum" in x:
x['momentum'] = np.interp(ni, xi, [hyp['warmup_momentum'], hyp['momentum']]) x["momentum"] = np.interp(ni, xi, [hyp["warmup_momentum"], hyp["momentum"]])
# Multi-scale # Multi-scale
if opt.multi_scale: if opt.multi_scale:
@@ -323,7 +369,7 @@ def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictio
sf = sz / max(imgs.shape[2:]) # scale factor sf = sz / max(imgs.shape[2:]) # scale factor
if sf != 1: if sf != 1:
ns = [math.ceil(x * sf / gs) * gs for x in imgs.shape[2:]] # new shape (stretched to gs-multiple) ns = [math.ceil(x * sf / gs) * gs for x in imgs.shape[2:]] # new shape (stretched to gs-multiple)
imgs = nn.functional.interpolate(imgs, size=ns, mode='bilinear', align_corners=False) imgs = nn.functional.interpolate(imgs, size=ns, mode="bilinear", align_corners=False)
# Forward # Forward
with torch.cuda.amp.autocast(amp): with torch.cuda.amp.autocast(amp):
@@ -332,7 +378,7 @@ def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictio
if RANK != -1: if RANK != -1:
loss *= WORLD_SIZE # gradient averaged between devices in DDP mode loss *= WORLD_SIZE # gradient averaged between devices in DDP mode
if opt.quad: if opt.quad:
loss *= 4. loss *= 4.0
# Backward # Backward
scaler.scale(loss).backward() scaler.scale(loss).backward()
@@ -351,35 +397,39 @@ def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictio
# Log # Log
if RANK in {-1, 0}: if RANK in {-1, 0}:
mloss = (mloss * i + loss_items) / (i + 1) # update mean losses mloss = (mloss * i + loss_items) / (i + 1) # update mean losses
mem = f'{torch.cuda.memory_reserved() / 1E9 if torch.cuda.is_available() else 0:.3g}G' # (GB) mem = f"{torch.cuda.memory_reserved() / 1E9 if torch.cuda.is_available() else 0:.3g}G" # (GB)
pbar.set_description(('%11s' * 2 + '%11.4g' * 5) % pbar.set_description(
(f'{epoch}/{epochs - 1}', mem, *mloss, targets.shape[0], imgs.shape[-1])) ("%11s" * 2 + "%11.4g" * 5)
callbacks.run('on_train_batch_end', model, ni, imgs, targets, paths, list(mloss)) % (f"{epoch}/{epochs - 1}", mem, *mloss, targets.shape[0], imgs.shape[-1])
)
callbacks.run("on_train_batch_end", model, ni, imgs, targets, paths, list(mloss))
if callbacks.stop_training: if callbacks.stop_training:
return return
# end batch ------------------------------------------------------------------------------------------------ # end batch ------------------------------------------------------------------------------------------------
# Scheduler # Scheduler
lr = [x['lr'] for x in optimizer.param_groups] # for loggers lr = [x["lr"] for x in optimizer.param_groups] # for loggers
scheduler.step() scheduler.step()
if RANK in {-1, 0}: if RANK in {-1, 0}:
# mAP # mAP
callbacks.run('on_train_epoch_end', epoch=epoch) callbacks.run("on_train_epoch_end", epoch=epoch)
ema.update_attr(model, include=['yaml', 'nc', 'hyp', 'names', 'stride', 'class_weights']) ema.update_attr(model, include=["yaml", "nc", "hyp", "names", "stride", "class_weights"])
final_epoch = (epoch + 1 == epochs) or stopper.possible_stop final_epoch = (epoch + 1 == epochs) or stopper.possible_stop
if not noval or final_epoch: # Calculate mAP if not noval or final_epoch: # Calculate mAP
results, maps, _ = validate.run(data_dict, results, maps, _ = validate.run(
batch_size=batch_size // WORLD_SIZE * 2, data_dict,
imgsz=imgsz, batch_size=batch_size // WORLD_SIZE * 2,
half=amp, imgsz=imgsz,
model=ema.ema, half=amp,
single_cls=single_cls, model=ema.ema,
dataloader=val_loader, single_cls=single_cls,
save_dir=save_dir, dataloader=val_loader,
plots=False, save_dir=save_dir,
callbacks=callbacks, plots=False,
compute_loss=compute_loss) callbacks=callbacks,
compute_loss=compute_loss,
)
# Update best mAP # Update best mAP
fi = fitness(np.array(results).reshape(1, -1)) # weighted combination of [P, R, mAP@.5, mAP@.5-.95] fi = fitness(np.array(results).reshape(1, -1)) # weighted combination of [P, R, mAP@.5, mAP@.5-.95]
@@ -387,29 +437,30 @@ def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictio
if fi > best_fitness: if fi > best_fitness:
best_fitness = fi best_fitness = fi
log_vals = list(mloss) + list(results) + lr log_vals = list(mloss) + list(results) + lr
callbacks.run('on_fit_epoch_end', log_vals, epoch, best_fitness, fi) callbacks.run("on_fit_epoch_end", log_vals, epoch, best_fitness, fi)
# Save model # Save model
if (not nosave) or (final_epoch and not evolve): # if save if (not nosave) or (final_epoch and not evolve): # if save
ckpt = { ckpt = {
'epoch': epoch, "epoch": epoch,
'best_fitness': best_fitness, "best_fitness": best_fitness,
'model': deepcopy(de_parallel(model)).half(), "model": deepcopy(de_parallel(model)).half(),
'ema': deepcopy(ema.ema).half(), "ema": deepcopy(ema.ema).half(),
'updates': ema.updates, "updates": ema.updates,
'optimizer': optimizer.state_dict(), "optimizer": optimizer.state_dict(),
'opt': vars(opt), "opt": vars(opt),
'git': GIT_INFO, # {remote, branch, commit} if a git repo "git": GIT_INFO, # {remote, branch, commit} if a git repo
'date': datetime.now().isoformat()} "date": datetime.now().isoformat(),
}
# Save last, best and delete # Save last, best and delete
torch.save(ckpt, last) torch.save(ckpt, last)
if best_fitness == fi: if best_fitness == fi:
torch.save(ckpt, best) torch.save(ckpt, best)
if opt.save_period > 0 and epoch % opt.save_period == 0: if opt.save_period > 0 and epoch % opt.save_period == 0:
torch.save(ckpt, w / f'epoch{epoch}.pt') torch.save(ckpt, w / f"epoch{epoch}.pt")
del ckpt del ckpt
callbacks.run('on_model_save', last, epoch, final_epoch, best_fitness, fi) callbacks.run("on_model_save", last, epoch, final_epoch, best_fitness, fi)
# EarlyStopping # EarlyStopping
if RANK != -1: # if DDP training if RANK != -1: # if DDP training
@@ -423,12 +474,12 @@ def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictio
# end epoch ---------------------------------------------------------------------------------------------------- # end epoch ----------------------------------------------------------------------------------------------------
# end training ----------------------------------------------------------------------------------------------------- # end training -----------------------------------------------------------------------------------------------------
if RANK in {-1, 0}: if RANK in {-1, 0}:
LOGGER.info(f'\n{epoch - start_epoch + 1} epochs completed in {(time.time() - t0) / 3600:.3f} hours.') LOGGER.info(f"\n{epoch - start_epoch + 1} epochs completed in {(time.time() - t0) / 3600:.3f} hours.")
for f in last, best: for f in last, best:
if f.exists(): if f.exists():
strip_optimizer(f) # strip optimizers strip_optimizer(f) # strip optimizers
if f is best: if f is best:
LOGGER.info(f'\nValidating {f}...') LOGGER.info(f"\nValidating {f}...")
results, _, _ = validate.run( results, _, _ = validate.run(
data_dict, data_dict,
batch_size=batch_size // WORLD_SIZE * 2, batch_size=batch_size // WORLD_SIZE * 2,
@@ -442,11 +493,12 @@ def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictio
verbose=True, verbose=True,
plots=plots, plots=plots,
callbacks=callbacks, callbacks=callbacks,
compute_loss=compute_loss) # val best model with plots compute_loss=compute_loss,
) # val best model with plots
if is_coco: if is_coco:
callbacks.run('on_fit_epoch_end', list(mloss) + list(results) + lr, epoch, best_fitness, fi) callbacks.run("on_fit_epoch_end", list(mloss) + list(results) + lr, epoch, best_fitness, fi)
callbacks.run('on_train_end', last, best, epoch, results) callbacks.run("on_train_end", last, best, epoch, results)
torch.cuda.empty_cache() torch.cuda.empty_cache()
return results return results
@@ -454,55 +506,54 @@ def train(hyp, opt, device, callbacks): # hyp is path/to/hyp.yaml or hyp dictio
def parse_opt(known=False): def parse_opt(known=False):
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('--weights', type=str, default=ROOT / 'yolov5s.pt', help='initial weights path') parser.add_argument("--weights", type=str, default=ROOT / "yolov5s.pt", help="initial weights path")
parser.add_argument('--cfg', type=str, default='', help='model.yaml path') parser.add_argument("--cfg", type=str, default="", help="model.yaml path")
parser.add_argument('--data', type=str, default=ROOT / 'data/coco128.yaml', help='dataset.yaml path') parser.add_argument("--data", type=str, default=ROOT / "data/coco128.yaml", help="dataset.yaml path")
parser.add_argument('--hyp', type=str, default=ROOT / 'data/hyps/hyp.scratch-low.yaml', help='hyperparameters path') parser.add_argument("--hyp", type=str, default=ROOT / "data/hyps/hyp.scratch-low.yaml", help="hyperparameters path")
parser.add_argument('--epochs', type=int, default=100, help='total training epochs') parser.add_argument("--epochs", type=int, default=100, help="total training epochs")
parser.add_argument('--batch-size', type=int, default=16, help='total batch size for all GPUs, -1 for autobatch') parser.add_argument("--batch-size", type=int, default=16, help="total batch size for all GPUs, -1 for autobatch")
parser.add_argument('--imgsz', '--img', '--img-size', type=int, default=640, help='train, val image size (pixels)') parser.add_argument("--imgsz", "--img", "--img-size", type=int, default=640, help="train, val image size (pixels)")
parser.add_argument('--rect', action='store_true', help='rectangular training') parser.add_argument("--rect", action="store_true", help="rectangular training")
parser.add_argument('--resume', nargs='?', const=True, default=False, help='resume most recent training') parser.add_argument("--resume", nargs="?", const=True, default=False, help="resume most recent training")
parser.add_argument('--nosave', action='store_true', help='only save final checkpoint') parser.add_argument("--nosave", action="store_true", help="only save final checkpoint")
parser.add_argument('--noval', action='store_true', help='only validate final epoch') parser.add_argument("--noval", action="store_true", help="only validate final epoch")
parser.add_argument('--noautoanchor', action='store_true', help='disable AutoAnchor') parser.add_argument("--noautoanchor", action="store_true", help="disable AutoAnchor")
parser.add_argument('--noplots', action='store_true', help='save no plot files') parser.add_argument("--noplots", action="store_true", help="save no plot files")
parser.add_argument('--evolve', type=int, nargs='?', const=300, help='evolve hyperparameters for x generations') parser.add_argument("--evolve", type=int, nargs="?", const=300, help="evolve hyperparameters for x generations")
parser.add_argument('--evolve_population', parser.add_argument(
type=str, "--evolve_population", type=str, default=ROOT / "data/hyps", help="location for loading population"
default=ROOT / 'data/hyps', )
help='location for loading population') parser.add_argument("--resume_evolve", type=str, default=None, help="resume evolve from last generation")
parser.add_argument('--resume_evolve', type=str, default=None, help='resume evolve from last generation') parser.add_argument("--bucket", type=str, default="", help="gsutil bucket")
parser.add_argument('--bucket', type=str, default='', help='gsutil bucket') parser.add_argument("--cache", type=str, nargs="?", const="ram", help="image --cache ram/disk")
parser.add_argument('--cache', type=str, nargs='?', const='ram', help='image --cache ram/disk') parser.add_argument("--image-weights", action="store_true", help="use weighted image selection for training")
parser.add_argument('--image-weights', action='store_true', help='use weighted image selection for training') parser.add_argument("--device", default="", help="cuda device, i.e. 0 or 0,1,2,3 or cpu")
parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu') parser.add_argument("--multi-scale", action="store_true", help="vary img-size +/- 50%%")
parser.add_argument('--multi-scale', action='store_true', help='vary img-size +/- 50%%') parser.add_argument("--single-cls", action="store_true", help="train multi-class data as single-class")
parser.add_argument('--single-cls', action='store_true', help='train multi-class data as single-class') parser.add_argument("--optimizer", type=str, choices=["SGD", "Adam", "AdamW"], default="SGD", help="optimizer")
parser.add_argument('--optimizer', type=str, choices=['SGD', 'Adam', 'AdamW'], default='SGD', help='optimizer') parser.add_argument("--sync-bn", action="store_true", help="use SyncBatchNorm, only available in DDP mode")
parser.add_argument('--sync-bn', action='store_true', help='use SyncBatchNorm, only available in DDP mode') parser.add_argument("--workers", type=int, default=8, help="max dataloader workers (per RANK in DDP mode)")
parser.add_argument('--workers', type=int, default=8, help='max dataloader workers (per RANK in DDP mode)') parser.add_argument("--project", default=ROOT / "runs/train", help="save to project/name")
parser.add_argument('--project', default=ROOT / 'runs/train', help='save to project/name') parser.add_argument("--name", default="exp", help="save to project/name")
parser.add_argument('--name', default='exp', help='save to project/name') parser.add_argument("--exist-ok", action="store_true", help="existing project/name ok, do not increment")
parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment') parser.add_argument("--quad", action="store_true", help="quad dataloader")
parser.add_argument('--quad', action='store_true', help='quad dataloader') parser.add_argument("--cos-lr", action="store_true", help="cosine LR scheduler")
parser.add_argument('--cos-lr', action='store_true', help='cosine LR scheduler') parser.add_argument("--label-smoothing", type=float, default=0.0, help="Label smoothing epsilon")
parser.add_argument('--label-smoothing', type=float, default=0.0, help='Label smoothing epsilon') parser.add_argument("--patience", type=int, default=100, help="EarlyStopping patience (epochs without improvement)")
parser.add_argument('--patience', type=int, default=100, help='EarlyStopping patience (epochs without improvement)') parser.add_argument("--freeze", nargs="+", type=int, default=[0], help="Freeze layers: backbone=10, first3=0 1 2")
parser.add_argument('--freeze', nargs='+', type=int, default=[0], help='Freeze layers: backbone=10, first3=0 1 2') parser.add_argument("--save-period", type=int, default=-1, help="Save checkpoint every x epochs (disabled if < 1)")
parser.add_argument('--save-period', type=int, default=-1, help='Save checkpoint every x epochs (disabled if < 1)') parser.add_argument("--seed", type=int, default=0, help="Global training seed")
parser.add_argument('--seed', type=int, default=0, help='Global training seed') parser.add_argument("--local_rank", type=int, default=-1, help="Automatic DDP Multi-GPU argument, do not modify")
parser.add_argument('--local_rank', type=int, default=-1, help='Automatic DDP Multi-GPU argument, do not modify')
# Logger arguments # Logger arguments
parser.add_argument('--entity', default=None, help='Entity') parser.add_argument("--entity", default=None, help="Entity")
parser.add_argument('--upload_dataset', nargs='?', const=True, default=False, help='Upload data, "val" option') parser.add_argument("--upload_dataset", nargs="?", const=True, default=False, help='Upload data, "val" option')
parser.add_argument('--bbox_interval', type=int, default=-1, help='Set bounding-box image logging interval') parser.add_argument("--bbox_interval", type=int, default=-1, help="Set bounding-box image logging interval")
parser.add_argument('--artifact_alias', type=str, default='latest', help='Version of dataset artifact to use') parser.add_argument("--artifact_alias", type=str, default="latest", help="Version of dataset artifact to use")
# NDJSON logging # NDJSON logging
parser.add_argument('--ndjson-console', action='store_true', help='Log ndjson to console') parser.add_argument("--ndjson-console", action="store_true", help="Log ndjson to console")
parser.add_argument('--ndjson-file', action='store_true', help='Log ndjson to file') parser.add_argument("--ndjson-file", action="store_true", help="Log ndjson to file")
return parser.parse_known_args()[0] if known else parser.parse_args() return parser.parse_known_args()[0] if known else parser.parse_args()
@@ -512,47 +563,53 @@ def main(opt, callbacks=Callbacks()):
if RANK in {-1, 0}: if RANK in {-1, 0}:
print_args(vars(opt)) print_args(vars(opt))
check_git_status() check_git_status()
check_requirements(ROOT / 'requirements.txt') check_requirements(ROOT / "requirements.txt")
# Resume (from specified or most recent last.pt) # Resume (from specified or most recent last.pt)
if opt.resume and not check_comet_resume(opt) and not opt.evolve: if opt.resume and not check_comet_resume(opt) and not opt.evolve:
last = Path(check_file(opt.resume) if isinstance(opt.resume, str) else get_latest_run()) last = Path(check_file(opt.resume) if isinstance(opt.resume, str) else get_latest_run())
opt_yaml = last.parent.parent / 'opt.yaml' # train options yaml opt_yaml = last.parent.parent / "opt.yaml" # train options yaml
opt_data = opt.data # original dataset opt_data = opt.data # original dataset
if opt_yaml.is_file(): if opt_yaml.is_file():
with open(opt_yaml, errors='ignore') as f: with open(opt_yaml, errors="ignore") as f:
d = yaml.safe_load(f) d = yaml.safe_load(f)
else: else:
d = torch.load(last, map_location='cpu')['opt'] d = torch.load(last, map_location="cpu")["opt"]
opt = argparse.Namespace(**d) # replace opt = argparse.Namespace(**d) # replace
opt.cfg, opt.weights, opt.resume = '', str(last), True # reinstate opt.cfg, opt.weights, opt.resume = "", str(last), True # reinstate
if is_url(opt_data): if is_url(opt_data):
opt.data = check_file(opt_data) # avoid HUB resume auth timeout opt.data = check_file(opt_data) # avoid HUB resume auth timeout
else: else:
opt.data, opt.cfg, opt.hyp, opt.weights, opt.project = \ opt.data, opt.cfg, opt.hyp, opt.weights, opt.project = (
check_file(opt.data), check_yaml(opt.cfg), check_yaml(opt.hyp), str(opt.weights), str(opt.project) # checks check_file(opt.data),
assert len(opt.cfg) or len(opt.weights), 'either --cfg or --weights must be specified' check_yaml(opt.cfg),
check_yaml(opt.hyp),
str(opt.weights),
str(opt.project),
) # checks
assert len(opt.cfg) or len(opt.weights), "either --cfg or --weights must be specified"
if opt.evolve: if opt.evolve:
if opt.project == str(ROOT / 'runs/train'): # if default project name, rename to runs/evolve if opt.project == str(ROOT / "runs/train"): # if default project name, rename to runs/evolve
opt.project = str(ROOT / 'runs/evolve') opt.project = str(ROOT / "runs/evolve")
opt.exist_ok, opt.resume = opt.resume, False # pass resume to exist_ok and disable resume opt.exist_ok, opt.resume = opt.resume, False # pass resume to exist_ok and disable resume
if opt.name == 'cfg': if opt.name == "cfg":
opt.name = Path(opt.cfg).stem # use model.yaml as name opt.name = Path(opt.cfg).stem # use model.yaml as name
opt.save_dir = str(increment_path(Path(opt.project) / opt.name, exist_ok=opt.exist_ok)) opt.save_dir = str(increment_path(Path(opt.project) / opt.name, exist_ok=opt.exist_ok))
# DDP mode # DDP mode
device = select_device(opt.device, batch_size=opt.batch_size) device = select_device(opt.device, batch_size=opt.batch_size)
if LOCAL_RANK != -1: if LOCAL_RANK != -1:
msg = 'is not compatible with YOLOv5 Multi-GPU DDP training' msg = "is not compatible with YOLOv5 Multi-GPU DDP training"
assert not opt.image_weights, f'--image-weights {msg}' assert not opt.image_weights, f"--image-weights {msg}"
assert not opt.evolve, f'--evolve {msg}' assert not opt.evolve, f"--evolve {msg}"
assert opt.batch_size != -1, f'AutoBatch with --batch-size -1 {msg}, please pass a valid --batch-size' assert opt.batch_size != -1, f"AutoBatch with --batch-size -1 {msg}, please pass a valid --batch-size"
assert opt.batch_size % WORLD_SIZE == 0, f'--batch-size {opt.batch_size} must be multiple of WORLD_SIZE' assert opt.batch_size % WORLD_SIZE == 0, f"--batch-size {opt.batch_size} must be multiple of WORLD_SIZE"
assert torch.cuda.device_count() > LOCAL_RANK, 'insufficient CUDA devices for DDP command' assert torch.cuda.device_count() > LOCAL_RANK, "insufficient CUDA devices for DDP command"
torch.cuda.set_device(LOCAL_RANK) torch.cuda.set_device(LOCAL_RANK)
device = torch.device('cuda', LOCAL_RANK) device = torch.device("cuda", LOCAL_RANK)
dist.init_process_group(backend='nccl' if dist.is_nccl_available() else 'gloo', dist.init_process_group(
timeout=timedelta(seconds=10800)) backend="nccl" if dist.is_nccl_available() else "gloo", timeout=timedelta(seconds=10800)
)
# Train # Train
if not opt.evolve: if not opt.evolve:
@@ -562,35 +619,36 @@ def main(opt, callbacks=Callbacks()):
else: else:
# Hyperparameter evolution metadata (including this hyperparameter True-False, lower_limit, upper_limit) # Hyperparameter evolution metadata (including this hyperparameter True-False, lower_limit, upper_limit)
meta = { meta = {
'lr0': (False, 1e-5, 1e-1), # initial learning rate (SGD=1E-2, Adam=1E-3) "lr0": (False, 1e-5, 1e-1), # initial learning rate (SGD=1E-2, Adam=1E-3)
'lrf': (False, 0.01, 1.0), # final OneCycleLR learning rate (lr0 * lrf) "lrf": (False, 0.01, 1.0), # final OneCycleLR learning rate (lr0 * lrf)
'momentum': (False, 0.6, 0.98), # SGD momentum/Adam beta1 "momentum": (False, 0.6, 0.98), # SGD momentum/Adam beta1
'weight_decay': (False, 0.0, 0.001), # optimizer weight decay "weight_decay": (False, 0.0, 0.001), # optimizer weight decay
'warmup_epochs': (False, 0.0, 5.0), # warmup epochs (fractions ok) "warmup_epochs": (False, 0.0, 5.0), # warmup epochs (fractions ok)
'warmup_momentum': (False, 0.0, 0.95), # warmup initial momentum "warmup_momentum": (False, 0.0, 0.95), # warmup initial momentum
'warmup_bias_lr': (False, 0.0, 0.2), # warmup initial bias lr "warmup_bias_lr": (False, 0.0, 0.2), # warmup initial bias lr
'box': (False, 0.02, 0.2), # box loss gain "box": (False, 0.02, 0.2), # box loss gain
'cls': (False, 0.2, 4.0), # cls loss gain "cls": (False, 0.2, 4.0), # cls loss gain
'cls_pw': (False, 0.5, 2.0), # cls BCELoss positive_weight "cls_pw": (False, 0.5, 2.0), # cls BCELoss positive_weight
'obj': (False, 0.2, 4.0), # obj loss gain (scale with pixels) "obj": (False, 0.2, 4.0), # obj loss gain (scale with pixels)
'obj_pw': (False, 0.5, 2.0), # obj BCELoss positive_weight "obj_pw": (False, 0.5, 2.0), # obj BCELoss positive_weight
'iou_t': (False, 0.1, 0.7), # IoU training threshold "iou_t": (False, 0.1, 0.7), # IoU training threshold
'anchor_t': (False, 2.0, 8.0), # anchor-multiple threshold "anchor_t": (False, 2.0, 8.0), # anchor-multiple threshold
'anchors': (False, 2.0, 10.0), # anchors per output grid (0 to ignore) "anchors": (False, 2.0, 10.0), # anchors per output grid (0 to ignore)
'fl_gamma': (False, 0.0, 2.0), # focal loss gamma (efficientDet default gamma=1.5) "fl_gamma": (False, 0.0, 2.0), # focal loss gamma (efficientDet default gamma=1.5)
'hsv_h': (True, 0.0, 0.1), # image HSV-Hue augmentation (fraction) "hsv_h": (True, 0.0, 0.1), # image HSV-Hue augmentation (fraction)
'hsv_s': (True, 0.0, 0.9), # image HSV-Saturation augmentation (fraction) "hsv_s": (True, 0.0, 0.9), # image HSV-Saturation augmentation (fraction)
'hsv_v': (True, 0.0, 0.9), # image HSV-Value augmentation (fraction) "hsv_v": (True, 0.0, 0.9), # image HSV-Value augmentation (fraction)
'degrees': (True, 0.0, 45.0), # image rotation (+/- deg) "degrees": (True, 0.0, 45.0), # image rotation (+/- deg)
'translate': (True, 0.0, 0.9), # image translation (+/- fraction) "translate": (True, 0.0, 0.9), # image translation (+/- fraction)
'scale': (True, 0.0, 0.9), # image scale (+/- gain) "scale": (True, 0.0, 0.9), # image scale (+/- gain)
'shear': (True, 0.0, 10.0), # image shear (+/- deg) "shear": (True, 0.0, 10.0), # image shear (+/- deg)
'perspective': (True, 0.0, 0.001), # image perspective (+/- fraction), range 0-0.001 "perspective": (True, 0.0, 0.001), # image perspective (+/- fraction), range 0-0.001
'flipud': (True, 0.0, 1.0), # image flip up-down (probability) "flipud": (True, 0.0, 1.0), # image flip up-down (probability)
'fliplr': (True, 0.0, 1.0), # image flip left-right (probability) "fliplr": (True, 0.0, 1.0), # image flip left-right (probability)
'mosaic': (True, 0.0, 1.0), # image mixup (probability) "mosaic": (True, 0.0, 1.0), # image mixup (probability)
'mixup': (True, 0.0, 1.0), # image mixup (probability) "mixup": (True, 0.0, 1.0), # image mixup (probability)
'copy_paste': (True, 0.0, 1.0)} # segment copy-paste (probability) "copy_paste": (True, 0.0, 1.0),
} # segment copy-paste (probability)
# GA configs # GA configs
pop_size = 50 pop_size = 50
@@ -603,22 +661,25 @@ def main(opt, callbacks=Callbacks()):
tournament_size_min = 2 tournament_size_min = 2
tournament_size_max = 10 tournament_size_max = 10
with open(opt.hyp, errors='ignore') as f: with open(opt.hyp, errors="ignore") as f:
hyp = yaml.safe_load(f) # load hyps dict hyp = yaml.safe_load(f) # load hyps dict
if 'anchors' not in hyp: # anchors commented in hyp.yaml if "anchors" not in hyp: # anchors commented in hyp.yaml
hyp['anchors'] = 3 hyp["anchors"] = 3
if opt.noautoanchor: if opt.noautoanchor:
del hyp['anchors'], meta['anchors'] del hyp["anchors"], meta["anchors"]
opt.noval, opt.nosave, save_dir = True, True, Path(opt.save_dir) # only val/save final epoch opt.noval, opt.nosave, save_dir = True, True, Path(opt.save_dir) # only val/save final epoch
# ei = [isinstance(x, (int, float)) for x in hyp.values()] # evolvable indices # ei = [isinstance(x, (int, float)) for x in hyp.values()] # evolvable indices
evolve_yaml, evolve_csv = save_dir / 'hyp_evolve.yaml', save_dir / 'evolve.csv' evolve_yaml, evolve_csv = save_dir / "hyp_evolve.yaml", save_dir / "evolve.csv"
if opt.bucket: if opt.bucket:
# download evolve.csv if exists # download evolve.csv if exists
subprocess.run([ subprocess.run(
'gsutil', [
'cp', "gsutil",
f'gs://{opt.bucket}/evolve.csv', "cp",
str(evolve_csv), ]) f"gs://{opt.bucket}/evolve.csv",
str(evolve_csv),
]
)
# Delete the items in meta dictionary whose first value is False # Delete the items in meta dictionary whose first value is False
del_ = [] del_ = []
@@ -644,8 +705,8 @@ def main(opt, callbacks=Callbacks()):
# If resuming evolution from a previous checkpoint # If resuming evolution from a previous checkpoint
if opt.resume_evolve is not None: if opt.resume_evolve is not None:
assert os.path.isfile(ROOT / opt.resume_evolve), 'evolve population path is wrong!' assert os.path.isfile(ROOT / opt.resume_evolve), "evolve population path is wrong!"
with open(ROOT / opt.resume_evolve, errors='ignore') as f: with open(ROOT / opt.resume_evolve, errors="ignore") as f:
evolve_population = yaml.safe_load(f) evolve_population = yaml.safe_load(f)
for value in evolve_population.values(): for value in evolve_population.values():
value = np.array([value[k] for k in hyp_GA.keys()]) value = np.array([value[k] for k in hyp_GA.keys()])
@@ -653,7 +714,7 @@ def main(opt, callbacks=Callbacks()):
# If not resuming from a previous checkpoint, generate initial values from .yaml files in opt.evolve_population # If not resuming from a previous checkpoint, generate initial values from .yaml files in opt.evolve_population
else: else:
yaml_files = [f for f in os.listdir(opt.evolve_population) if f.endswith('.yaml')] yaml_files = [f for f in os.listdir(opt.evolve_population) if f.endswith(".yaml")]
for file_name in yaml_files: for file_name in yaml_files:
with open(os.path.join(opt.evolve_population, file_name)) as yaml_file: with open(os.path.join(opt.evolve_population, file_name)) as yaml_file:
value = yaml.safe_load(yaml_file) value = yaml.safe_load(yaml_file)
@@ -661,27 +722,28 @@ def main(opt, callbacks=Callbacks()):
initial_values.append(list(value)) initial_values.append(list(value))
# Generate random values within the search space for the rest of the population # Generate random values within the search space for the rest of the population
if (initial_values is None): if initial_values is None:
population = [generate_individual(gene_ranges, len(hyp_GA)) for i in range(pop_size)] population = [generate_individual(gene_ranges, len(hyp_GA)) for i in range(pop_size)]
else: else:
if (pop_size > 1): if pop_size > 1:
population = [ population = [
generate_individual(gene_ranges, len(hyp_GA)) for i in range(pop_size - len(initial_values))] generate_individual(gene_ranges, len(hyp_GA)) for i in range(pop_size - len(initial_values))
]
for initial_value in initial_values: for initial_value in initial_values:
population = [initial_value] + population population = [initial_value] + population
# Run the genetic algorithm for a fixed number of generations # Run the genetic algorithm for a fixed number of generations
list_keys = list(hyp_GA.keys()) list_keys = list(hyp_GA.keys())
for generation in range(opt.evolve): for generation in range(opt.evolve):
if (generation >= 1): if generation >= 1:
save_dict = {} save_dict = {}
for i in range(len(population)): for i in range(len(population)):
little_dict = {} little_dict = {}
for j in range(len(population[i])): for j in range(len(population[i])):
little_dict[list_keys[j]] = float(population[i][j]) little_dict[list_keys[j]] = float(population[i][j])
save_dict['gen' + str(generation) + 'number' + str(i)] = little_dict save_dict["gen" + str(generation) + "number" + str(i)] = little_dict
with open(save_dir / 'evolve_population.yaml', 'w') as outfile: with open(save_dir / "evolve_population.yaml", "w") as outfile:
yaml.dump(save_dict, outfile, default_flow_style=False) yaml.dump(save_dict, outfile, default_flow_style=False)
# Adaptive elite size # Adaptive elite size
@@ -695,8 +757,15 @@ def main(opt, callbacks=Callbacks()):
results = train(hyp.copy(), opt, device, callbacks) results = train(hyp.copy(), opt, device, callbacks)
callbacks = Callbacks() callbacks = Callbacks()
# Write mutation results # Write mutation results
keys = ('metrics/precision', 'metrics/recall', 'metrics/mAP_0.5', 'metrics/mAP_0.5:0.95', keys = (
'val/box_loss', 'val/obj_loss', 'val/cls_loss') "metrics/precision",
"metrics/recall",
"metrics/mAP_0.5",
"metrics/mAP_0.5:0.95",
"val/box_loss",
"val/obj_loss",
"val/cls_loss",
)
print_mutation(keys, results, hyp.copy(), save_dir, opt.bucket) print_mutation(keys, results, hyp.copy(), save_dir, opt.bucket)
fitness_scores.append(results[2]) fitness_scores.append(results[2])
@@ -704,8 +773,10 @@ def main(opt, callbacks=Callbacks()):
selected_indices = [] selected_indices = []
for i in range(pop_size - elite_size): for i in range(pop_size - elite_size):
# Adaptive tournament size # Adaptive tournament size
tournament_size = max(max(2, tournament_size_min), tournament_size = max(
int(min(tournament_size_max, pop_size) - (generation / (opt.evolve / 10)))) max(2, tournament_size_min),
int(min(tournament_size_max, pop_size) - (generation / (opt.evolve / 10))),
)
# Perform tournament selection to choose the best individual # Perform tournament selection to choose the best individual
tournament_indices = random.sample(range(pop_size), tournament_size) tournament_indices = random.sample(range(pop_size), tournament_size)
tournament_fitness = [fitness_scores[j] for j in tournament_indices] tournament_fitness = [fitness_scores[j] for j in tournament_indices]
@@ -721,16 +792,18 @@ def main(opt, callbacks=Callbacks()):
parent1_index = selected_indices[random.randint(0, pop_size - 1)] parent1_index = selected_indices[random.randint(0, pop_size - 1)]
parent2_index = selected_indices[random.randint(0, pop_size - 1)] parent2_index = selected_indices[random.randint(0, pop_size - 1)]
# Adaptive crossover rate # Adaptive crossover rate
crossover_rate = max(crossover_rate_min, crossover_rate = max(
min(crossover_rate_max, crossover_rate_max - (generation / opt.evolve))) crossover_rate_min, min(crossover_rate_max, crossover_rate_max - (generation / opt.evolve))
)
if random.uniform(0, 1) < crossover_rate: if random.uniform(0, 1) < crossover_rate:
crossover_point = random.randint(1, len(hyp_GA) - 1) crossover_point = random.randint(1, len(hyp_GA) - 1)
child = population[parent1_index][:crossover_point] + population[parent2_index][crossover_point:] child = population[parent1_index][:crossover_point] + population[parent2_index][crossover_point:]
else: else:
child = population[parent1_index] child = population[parent1_index]
# Adaptive mutation rate # Adaptive mutation rate
mutation_rate = max(mutation_rate_min, mutation_rate = max(
min(mutation_rate_max, mutation_rate_max - (generation / opt.evolve))) mutation_rate_min, min(mutation_rate_max, mutation_rate_max - (generation / opt.evolve))
)
for j in range(len(hyp_GA)): for j in range(len(hyp_GA)):
if random.uniform(0, 1) < mutation_rate: if random.uniform(0, 1) < mutation_rate:
child[j] += random.uniform(-0.1, 0.1) child[j] += random.uniform(-0.1, 0.1)
@@ -741,12 +814,14 @@ def main(opt, callbacks=Callbacks()):
# Print the best solution found # Print the best solution found
best_index = fitness_scores.index(max(fitness_scores)) best_index = fitness_scores.index(max(fitness_scores))
best_individual = population[best_index] best_individual = population[best_index]
print('Best solution found:', best_individual) print("Best solution found:", best_individual)
# Plot results # Plot results
plot_evolve(evolve_csv) plot_evolve(evolve_csv)
LOGGER.info(f'Hyperparameter evolution finished {opt.evolve} generations\n' LOGGER.info(
f"Results saved to {colorstr('bold', save_dir)}\n" f'Hyperparameter evolution finished {opt.evolve} generations\n'
f'Usage example: $ python train.py --hyp {evolve_yaml}') f"Results saved to {colorstr('bold', save_dir)}\n"
f'Usage example: $ python train.py --hyp {evolve_yaml}'
)
def generate_individual(input_ranges, individual_length): def generate_individual(input_ranges, individual_length):
@@ -766,6 +841,6 @@ def run(**kwargs):
return opt return opt
if __name__ == '__main__': if __name__ == "__main__":
opt = parse_opt() opt = parse_opt()
main(opt) main(opt)
+14 -15
View File
@@ -1,21 +1,19 @@
# YOLOv5 🚀 by Ultralytics, AGPL-3.0 license # YOLOv5 🚀 by Ultralytics, AGPL-3.0 license
""" """utils/initialization."""
utils/initialization
"""
import contextlib import contextlib
import platform import platform
import threading import threading
def emojis(str=''): def emojis(str=""):
# Return platform-dependent emoji-safe version of string # Return platform-dependent emoji-safe version of string
return str.encode().decode('ascii', 'ignore') if platform.system() == 'Windows' else str return str.encode().decode("ascii", "ignore") if platform.system() == "Windows" else str
class TryExcept(contextlib.ContextDecorator): class TryExcept(contextlib.ContextDecorator):
# YOLOv5 TryExcept class. Usage: @TryExcept() decorator or 'with TryExcept():' context manager # YOLOv5 TryExcept class. Usage: @TryExcept() decorator or 'with TryExcept():' context manager
def __init__(self, msg=''): def __init__(self, msg=""):
self.msg = msg self.msg = msg
def __enter__(self): def __enter__(self):
@@ -43,13 +41,13 @@ def join_threads(verbose=False):
for t in threading.enumerate(): for t in threading.enumerate():
if t is not main_thread: if t is not main_thread:
if verbose: if verbose:
print(f'Joining thread {t.name}') print(f"Joining thread {t.name}")
t.join() t.join()
def notebook_init(verbose=True): def notebook_init(verbose=True):
# Check system software and hardware # Check system software and hardware
print('Checking setup...') print("Checking setup...")
import os import os
import shutil import shutil
@@ -63,24 +61,25 @@ def notebook_init(verbose=True):
import psutil import psutil
if check_requirements('wandb', install=False): if check_requirements("wandb", install=False):
os.system('pip uninstall -y wandb') # eliminate unexpected account creation prompt with infinite hang os.system("pip uninstall -y wandb") # eliminate unexpected account creation prompt with infinite hang
if is_colab(): if is_colab():
shutil.rmtree('/content/sample_data', ignore_errors=True) # remove colab /sample_data directory shutil.rmtree("/content/sample_data", ignore_errors=True) # remove colab /sample_data directory
# System info # System info
display = None display = None
if verbose: if verbose:
gb = 1 << 30 # bytes to GiB (1024 ** 3) gb = 1 << 30 # bytes to GiB (1024 ** 3)
ram = psutil.virtual_memory().total ram = psutil.virtual_memory().total
total, used, free = shutil.disk_usage('/') total, used, free = shutil.disk_usage("/")
with contextlib.suppress(Exception): # clear display if ipython is installed with contextlib.suppress(Exception): # clear display if ipython is installed
from IPython import display from IPython import display
display.clear_output() display.clear_output()
s = f'({os.cpu_count()} CPUs, {ram / gb:.1f} GB RAM, {(total - free) / gb:.1f}/{total / gb:.1f} GB disk)' s = f"({os.cpu_count()} CPUs, {ram / gb:.1f} GB RAM, {(total - free) / gb:.1f}/{total / gb:.1f} GB disk)"
else: else:
s = '' s = ""
select_device(newline=False) select_device(newline=False)
print(emojis(f'Setup complete ✅ {s}')) print(emojis(f"Setup complete ✅ {s}"))
return display return display
+3 -6
View File
@@ -1,7 +1,5 @@
# YOLOv5 🚀 by Ultralytics, AGPL-3.0 license # YOLOv5 🚀 by Ultralytics, AGPL-3.0 license
""" """Activation functions."""
Activation functions
"""
import torch import torch
import torch.nn as nn import torch.nn as nn
@@ -33,7 +31,6 @@ class Mish(nn.Module):
class MemoryEfficientMish(nn.Module): class MemoryEfficientMish(nn.Module):
# Mish activation memory-efficient # Mish activation memory-efficient
class F(torch.autograd.Function): class F(torch.autograd.Function):
@staticmethod @staticmethod
def forward(ctx, x): def forward(ctx, x):
ctx.save_for_backward(x) ctx.save_for_backward(x)
@@ -62,7 +59,7 @@ class FReLU(nn.Module):
class AconC(nn.Module): class AconC(nn.Module):
r""" ACON activation (activate or not) r"""ACON activation (activate or not)
AconC: (p1*x-p2*x) * sigmoid(beta*(p1*x-p2*x)) + p2*x, beta is a learnable parameter AconC: (p1*x-p2*x) * sigmoid(beta*(p1*x-p2*x)) + p2*x, beta is a learnable parameter
according to "Activate or Not: Learning Customized Activation" <https://arxiv.org/pdf/2009.04759.pdf>. according to "Activate or Not: Learning Customized Activation" <https://arxiv.org/pdf/2009.04759.pdf>.
""" """
@@ -79,7 +76,7 @@ class AconC(nn.Module):
class MetaAconC(nn.Module): class MetaAconC(nn.Module):
r""" ACON activation (activate or not) r"""ACON activation (activate or not)
MetaAconC: (p1*x-p2*x) * sigmoid(beta*(p1*x-p2*x)) + p2*x, beta is generated by a small network MetaAconC: (p1*x-p2*x) * sigmoid(beta*(p1*x-p2*x)) + p2*x, beta is generated by a small network
according to "Activate or Not: Learning Customized Activation" <https://arxiv.org/pdf/2009.04759.pdf>. according to "Activate or Not: Learning Customized Activation" <https://arxiv.org/pdf/2009.04759.pdf>.
""" """
+36 -40
View File
@@ -1,7 +1,5 @@
# YOLOv5 🚀 by Ultralytics, AGPL-3.0 license # YOLOv5 🚀 by Ultralytics, AGPL-3.0 license
""" """Image augmentation functions."""
Image augmentation functions
"""
import math import math
import random import random
@@ -23,10 +21,11 @@ class Albumentations:
# YOLOv5 Albumentations class (optional, only used if package is installed) # YOLOv5 Albumentations class (optional, only used if package is installed)
def __init__(self, size=640): def __init__(self, size=640):
self.transform = None self.transform = None
prefix = colorstr('albumentations: ') prefix = colorstr("albumentations: ")
try: try:
import albumentations as A import albumentations as A
check_version(A.__version__, '1.0.3', hard=True) # version requirement
check_version(A.__version__, "1.0.3", hard=True) # version requirement
T = [ T = [
A.RandomResizedCrop(height=size, width=size, scale=(0.8, 1.0), ratio=(0.9, 1.11), p=0.0), A.RandomResizedCrop(height=size, width=size, scale=(0.8, 1.0), ratio=(0.9, 1.11), p=0.0),
@@ -36,19 +35,20 @@ class Albumentations:
A.CLAHE(p=0.01), A.CLAHE(p=0.01),
A.RandomBrightnessContrast(p=0.0), A.RandomBrightnessContrast(p=0.0),
A.RandomGamma(p=0.0), A.RandomGamma(p=0.0),
A.ImageCompression(quality_lower=75, p=0.0)] # transforms A.ImageCompression(quality_lower=75, p=0.0),
self.transform = A.Compose(T, bbox_params=A.BboxParams(format='yolo', label_fields=['class_labels'])) ] # transforms
self.transform = A.Compose(T, bbox_params=A.BboxParams(format="yolo", label_fields=["class_labels"]))
LOGGER.info(prefix + ', '.join(f'{x}'.replace('always_apply=False, ', '') for x in T if x.p)) LOGGER.info(prefix + ", ".join(f"{x}".replace("always_apply=False, ", "") for x in T if x.p))
except ImportError: # package not installed, skip except ImportError: # package not installed, skip
pass pass
except Exception as e: except Exception as e:
LOGGER.info(f'{prefix}{e}') LOGGER.info(f"{prefix}{e}")
def __call__(self, im, labels, p=1.0): def __call__(self, im, labels, p=1.0):
if self.transform and random.random() < p: if self.transform and random.random() < p:
new = self.transform(image=im, bboxes=labels[:, 1:], class_labels=labels[:, 0]) # transformed new = self.transform(image=im, bboxes=labels[:, 1:], class_labels=labels[:, 0]) # transformed
im, labels = new['image'], np.array([[c, *b] for c, b in zip(new['class_labels'], new['bboxes'])]) im, labels = new["image"], np.array([[c, *b] for c, b in zip(new["class_labels"], new["bboxes"])])
return im, labels return im, labels
@@ -97,7 +97,7 @@ def replicate(im, labels):
boxes = labels[:, 1:].astype(int) boxes = labels[:, 1:].astype(int)
x1, y1, x2, y2 = boxes.T x1, y1, x2, y2 = boxes.T
s = ((x2 - x1) + (y2 - y1)) / 2 # side length (pixels) s = ((x2 - x1) + (y2 - y1)) / 2 # side length (pixels)
for i in s.argsort()[:round(s.size * 0.5)]: # smallest indices for i in s.argsort()[: round(s.size * 0.5)]: # smallest indices
x1b, y1b, x2b, y2b = boxes[i] x1b, y1b, x2b, y2b = boxes[i]
bh, bw = y2b - y1b, x2b - x1b bh, bw = y2b - y1b, x2b - x1b
yc, xc = int(random.uniform(0, h - bh)), int(random.uniform(0, w - bw)) # offset x, y yc, xc = int(random.uniform(0, h - bh)), int(random.uniform(0, w - bw)) # offset x, y
@@ -141,15 +141,9 @@ def letterbox(im, new_shape=(640, 640), color=(114, 114, 114), auto=True, scaleF
return im, ratio, (dw, dh) return im, ratio, (dw, dh)
def random_perspective(im, def random_perspective(
targets=(), im, targets=(), segments=(), degrees=10, translate=0.1, scale=0.1, shear=10, perspective=0.0, border=(0, 0)
segments=(), ):
degrees=10,
translate=.1,
scale=.1,
shear=10,
perspective=0.0,
border=(0, 0)):
# torchvision.transforms.RandomAffine(degrees=(-10, 10), translate=(0.1, 0.1), scale=(0.9, 1.1), shear=(-10, 10)) # torchvision.transforms.RandomAffine(degrees=(-10, 10), translate=(0.1, 0.1), scale=(0.9, 1.1), shear=(-10, 10))
# targets = [cls, xyxy] # targets = [cls, xyxy]
@@ -303,50 +297,52 @@ def box_candidates(box1, box2, wh_thr=2, ar_thr=100, area_thr=0.1, eps=1e-16):
def classify_albumentations( def classify_albumentations(
augment=True, augment=True,
size=224, size=224,
scale=(0.08, 1.0), scale=(0.08, 1.0),
ratio=(0.75, 1.0 / 0.75), # 0.75, 1.33 ratio=(0.75, 1.0 / 0.75), # 0.75, 1.33
hflip=0.5, hflip=0.5,
vflip=0.0, vflip=0.0,
jitter=0.4, jitter=0.4,
mean=IMAGENET_MEAN, mean=IMAGENET_MEAN,
std=IMAGENET_STD, std=IMAGENET_STD,
auto_aug=False): auto_aug=False,
):
# YOLOv5 classification Albumentations (optional, only used if package is installed) # YOLOv5 classification Albumentations (optional, only used if package is installed)
prefix = colorstr('albumentations: ') prefix = colorstr("albumentations: ")
try: try:
import albumentations as A import albumentations as A
from albumentations.pytorch import ToTensorV2 from albumentations.pytorch import ToTensorV2
check_version(A.__version__, '1.0.3', hard=True) # version requirement
check_version(A.__version__, "1.0.3", hard=True) # version requirement
if augment: # Resize and crop if augment: # Resize and crop
T = [A.RandomResizedCrop(height=size, width=size, scale=scale, ratio=ratio)] T = [A.RandomResizedCrop(height=size, width=size, scale=scale, ratio=ratio)]
if auto_aug: if auto_aug:
# TODO: implement AugMix, AutoAug & RandAug in albumentation # TODO: implement AugMix, AutoAug & RandAug in albumentation
LOGGER.info(f'{prefix}auto augmentations are currently not supported') LOGGER.info(f"{prefix}auto augmentations are currently not supported")
else: else:
if hflip > 0: if hflip > 0:
T += [A.HorizontalFlip(p=hflip)] T += [A.HorizontalFlip(p=hflip)]
if vflip > 0: if vflip > 0:
T += [A.VerticalFlip(p=vflip)] T += [A.VerticalFlip(p=vflip)]
if jitter > 0: if jitter > 0:
color_jitter = (float(jitter), ) * 3 # repeat value for brightness, contrast, satuaration, 0 hue color_jitter = (float(jitter),) * 3 # repeat value for brightness, contrast, satuaration, 0 hue
T += [A.ColorJitter(*color_jitter, 0)] T += [A.ColorJitter(*color_jitter, 0)]
else: # Use fixed crop for eval set (reproducibility) else: # Use fixed crop for eval set (reproducibility)
T = [A.SmallestMaxSize(max_size=size), A.CenterCrop(height=size, width=size)] T = [A.SmallestMaxSize(max_size=size), A.CenterCrop(height=size, width=size)]
T += [A.Normalize(mean=mean, std=std), ToTensorV2()] # Normalize and convert to Tensor T += [A.Normalize(mean=mean, std=std), ToTensorV2()] # Normalize and convert to Tensor
LOGGER.info(prefix + ', '.join(f'{x}'.replace('always_apply=False, ', '') for x in T if x.p)) LOGGER.info(prefix + ", ".join(f"{x}".replace("always_apply=False, ", "") for x in T if x.p))
return A.Compose(T) return A.Compose(T)
except ImportError: # package not installed, skip except ImportError: # package not installed, skip
LOGGER.warning(f'{prefix}⚠️ not found, install with `pip install albumentations` (recommended)') LOGGER.warning(f"{prefix}⚠️ not found, install with `pip install albumentations` (recommended)")
except Exception as e: except Exception as e:
LOGGER.info(f'{prefix}{e}') LOGGER.info(f"{prefix}{e}")
def classify_transforms(size=224): def classify_transforms(size=224):
# Transforms to apply if albumentations not installed # Transforms to apply if albumentations not installed
assert isinstance(size, int), f'ERROR: classify_transforms size {size} must be integer, not (list, tuple)' assert isinstance(size, int), f"ERROR: classify_transforms size {size} must be integer, not (list, tuple)"
# T.Compose([T.ToTensor(), T.Resize(size), T.CenterCrop(size), T.Normalize(IMAGENET_MEAN, IMAGENET_STD)]) # T.Compose([T.ToTensor(), T.Resize(size), T.CenterCrop(size), T.Normalize(IMAGENET_MEAN, IMAGENET_STD)])
return T.Compose([CenterCrop(size), ToTensor(), T.Normalize(IMAGENET_MEAN, IMAGENET_STD)]) return T.Compose([CenterCrop(size), ToTensor(), T.Normalize(IMAGENET_MEAN, IMAGENET_STD)])
@@ -366,7 +362,7 @@ class LetterBox:
hs, ws = (math.ceil(x / self.stride) * self.stride for x in (h, w)) if self.auto else self.h, self.w hs, ws = (math.ceil(x / self.stride) * self.stride for x in (h, w)) if self.auto else self.h, self.w
top, left = round((hs - h) / 2 - 0.1), round((ws - w) / 2 - 0.1) top, left = round((hs - h) / 2 - 0.1), round((ws - w) / 2 - 0.1)
im_out = np.full((self.h, self.w, 3), 114, dtype=im.dtype) im_out = np.full((self.h, self.w, 3), 114, dtype=im.dtype)
im_out[top:top + h, left:left + w] = cv2.resize(im, (w, h), interpolation=cv2.INTER_LINEAR) im_out[top : top + h, left : left + w] = cv2.resize(im, (w, h), interpolation=cv2.INTER_LINEAR)
return im_out return im_out
@@ -380,7 +376,7 @@ class CenterCrop:
imh, imw = im.shape[:2] imh, imw = im.shape[:2]
m = min(imh, imw) # min dimension m = min(imh, imw) # min dimension
top, left = (imh - m) // 2, (imw - m) // 2 top, left = (imh - m) // 2, (imw - m) // 2
return cv2.resize(im[top:top + m, left:left + m], (self.w, self.h), interpolation=cv2.INTER_LINEAR) return cv2.resize(im[top : top + m, left : left + m], (self.w, self.h), interpolation=cv2.INTER_LINEAR)
class ToTensor: class ToTensor:
+37 -35
View File
@@ -1,7 +1,5 @@
# YOLOv5 🚀 by Ultralytics, AGPL-3.0 license # YOLOv5 🚀 by Ultralytics, AGPL-3.0 license
""" """AutoAnchor utils."""
AutoAnchor utils
"""
import random import random
@@ -13,7 +11,7 @@ from tqdm import tqdm
from utils import TryExcept from utils import TryExcept
from utils.general import LOGGER, TQDM_BAR_FORMAT, colorstr from utils.general import LOGGER, TQDM_BAR_FORMAT, colorstr
PREFIX = colorstr('AutoAnchor: ') PREFIX = colorstr("AutoAnchor: ")
def check_anchor_order(m): def check_anchor_order(m):
@@ -22,14 +20,14 @@ def check_anchor_order(m):
da = a[-1] - a[0] # delta a da = a[-1] - a[0] # delta a
ds = m.stride[-1] - m.stride[0] # delta s ds = m.stride[-1] - m.stride[0] # delta s
if da and (da.sign() != ds.sign()): # same order if da and (da.sign() != ds.sign()): # same order
LOGGER.info(f'{PREFIX}Reversing anchor order') LOGGER.info(f"{PREFIX}Reversing anchor order")
m.anchors[:] = m.anchors.flip(0) m.anchors[:] = m.anchors.flip(0)
@TryExcept(f'{PREFIX}ERROR') @TryExcept(f"{PREFIX}ERROR")
def check_anchors(dataset, model, thr=4.0, imgsz=640): def check_anchors(dataset, model, thr=4.0, imgsz=640):
# Check anchor fit to data, recompute if necessary # Check anchor fit to data, recompute if necessary
m = model.module.model[-1] if hasattr(model, 'module') else model.model[-1] # Detect() m = model.module.model[-1] if hasattr(model, "module") else model.model[-1] # Detect()
shapes = imgsz * dataset.shapes / dataset.shapes.max(1, keepdims=True) shapes = imgsz * dataset.shapes / dataset.shapes.max(1, keepdims=True)
scale = np.random.uniform(0.9, 1.1, size=(shapes.shape[0], 1)) # augment scale scale = np.random.uniform(0.9, 1.1, size=(shapes.shape[0], 1)) # augment scale
wh = torch.tensor(np.concatenate([l[:, 3:5] * s for s, l in zip(shapes * scale, dataset.labels)])).float() # wh wh = torch.tensor(np.concatenate([l[:, 3:5] * s for s, l in zip(shapes * scale, dataset.labels)])).float() # wh
@@ -45,11 +43,11 @@ def check_anchors(dataset, model, thr=4.0, imgsz=640):
stride = m.stride.to(m.anchors.device).view(-1, 1, 1) # model strides stride = m.stride.to(m.anchors.device).view(-1, 1, 1) # model strides
anchors = m.anchors.clone() * stride # current anchors anchors = m.anchors.clone() * stride # current anchors
bpr, aat = metric(anchors.cpu().view(-1, 2)) bpr, aat = metric(anchors.cpu().view(-1, 2))
s = f'\n{PREFIX}{aat:.2f} anchors/target, {bpr:.3f} Best Possible Recall (BPR). ' s = f"\n{PREFIX}{aat:.2f} anchors/target, {bpr:.3f} Best Possible Recall (BPR). "
if bpr > 0.98: # threshold to recompute if bpr > 0.98: # threshold to recompute
LOGGER.info(f'{s}Current anchors are a good fit to dataset ✅') LOGGER.info(f"{s}Current anchors are a good fit to dataset ✅")
else: else:
LOGGER.info(f'{s}Anchors are a poor fit to dataset ⚠️, attempting to improve...') LOGGER.info(f"{s}Anchors are a poor fit to dataset ⚠️, attempting to improve...")
na = m.anchors.numel() // 2 # number of anchors na = m.anchors.numel() // 2 # number of anchors
anchors = kmean_anchors(dataset, n=na, img_size=imgsz, thr=thr, gen=1000, verbose=False) anchors = kmean_anchors(dataset, n=na, img_size=imgsz, thr=thr, gen=1000, verbose=False)
new_bpr = metric(anchors)[0] new_bpr = metric(anchors)[0]
@@ -58,28 +56,29 @@ def check_anchors(dataset, model, thr=4.0, imgsz=640):
m.anchors[:] = anchors.clone().view_as(m.anchors) m.anchors[:] = anchors.clone().view_as(m.anchors)
check_anchor_order(m) # must be in pixel-space (not grid-space) check_anchor_order(m) # must be in pixel-space (not grid-space)
m.anchors /= stride m.anchors /= stride
s = f'{PREFIX}Done ✅ (optional: update model *.yaml to use these anchors in the future)' s = f"{PREFIX}Done ✅ (optional: update model *.yaml to use these anchors in the future)"
else: else:
s = f'{PREFIX}Done ⚠️ (original anchors better than new anchors, proceeding with original anchors)' s = f"{PREFIX}Done ⚠️ (original anchors better than new anchors, proceeding with original anchors)"
LOGGER.info(s) LOGGER.info(s)
def kmean_anchors(dataset='./data/coco128.yaml', n=9, img_size=640, thr=4.0, gen=1000, verbose=True): def kmean_anchors(dataset="./data/coco128.yaml", n=9, img_size=640, thr=4.0, gen=1000, verbose=True):
""" Creates kmeans-evolved anchors from training dataset """
Creates kmeans-evolved anchors from training dataset.
Arguments: Arguments:
dataset: path to data.yaml, or a loaded dataset dataset: path to data.yaml, or a loaded dataset
n: number of anchors n: number of anchors
img_size: image size used for training img_size: image size used for training
thr: anchor-label wh ratio threshold hyperparameter hyp['anchor_t'] used for training, default=4.0 thr: anchor-label wh ratio threshold hyperparameter hyp['anchor_t'] used for training, default=4.0
gen: generations to evolve anchors using genetic algorithm gen: generations to evolve anchors using genetic algorithm
verbose: print all results verbose: print all results
Return: Return:
k: kmeans evolved anchors k: kmeans evolved anchors
Usage: Usage:
from utils.autoanchor import *; _ = kmean_anchors() from utils.autoanchor import *; _ = kmean_anchors()
""" """
from scipy.cluster.vq import kmeans from scipy.cluster.vq import kmeans
@@ -100,20 +99,23 @@ def kmean_anchors(dataset='./data/coco128.yaml', n=9, img_size=640, thr=4.0, gen
k = k[np.argsort(k.prod(1))] # sort small to large k = k[np.argsort(k.prod(1))] # sort small to large
x, best = metric(k, wh0) x, best = metric(k, wh0)
bpr, aat = (best > thr).float().mean(), (x > thr).float().mean() * n # best possible recall, anch > thr bpr, aat = (best > thr).float().mean(), (x > thr).float().mean() * n # best possible recall, anch > thr
s = f'{PREFIX}thr={thr:.2f}: {bpr:.4f} best possible recall, {aat:.2f} anchors past thr\n' \ s = (
f'{PREFIX}n={n}, img_size={img_size}, metric_all={x.mean():.3f}/{best.mean():.3f}-mean/best, ' \ f"{PREFIX}thr={thr:.2f}: {bpr:.4f} best possible recall, {aat:.2f} anchors past thr\n"
f'past_thr={x[x > thr].mean():.3f}-mean: ' f"{PREFIX}n={n}, img_size={img_size}, metric_all={x.mean():.3f}/{best.mean():.3f}-mean/best, "
f"past_thr={x[x > thr].mean():.3f}-mean: "
)
for x in k: for x in k:
s += '%i,%i, ' % (round(x[0]), round(x[1])) s += "%i,%i, " % (round(x[0]), round(x[1]))
if verbose: if verbose:
LOGGER.info(s[:-2]) LOGGER.info(s[:-2])
return k return k
if isinstance(dataset, str): # *.yaml file if isinstance(dataset, str): # *.yaml file
with open(dataset, errors='ignore') as f: with open(dataset, errors="ignore") as f:
data_dict = yaml.safe_load(f) # model dict data_dict = yaml.safe_load(f) # model dict
from utils.dataloaders import LoadImagesAndLabels from utils.dataloaders import LoadImagesAndLabels
dataset = LoadImagesAndLabels(data_dict['train'], augment=True, rect=True)
dataset = LoadImagesAndLabels(data_dict["train"], augment=True, rect=True)
# Get label wh # Get label wh
shapes = img_size * dataset.shapes / dataset.shapes.max(1, keepdims=True) shapes = img_size * dataset.shapes / dataset.shapes.max(1, keepdims=True)
@@ -122,19 +124,19 @@ def kmean_anchors(dataset='./data/coco128.yaml', n=9, img_size=640, thr=4.0, gen
# Filter # Filter
i = (wh0 < 3.0).any(1).sum() i = (wh0 < 3.0).any(1).sum()
if i: if i:
LOGGER.info(f'{PREFIX}WARNING ⚠️ Extremely small objects found: {i} of {len(wh0)} labels are <3 pixels in size') LOGGER.info(f"{PREFIX}WARNING ⚠️ Extremely small objects found: {i} of {len(wh0)} labels are <3 pixels in size")
wh = wh0[(wh0 >= 2.0).any(1)].astype(np.float32) # filter > 2 pixels wh = wh0[(wh0 >= 2.0).any(1)].astype(np.float32) # filter > 2 pixels
# wh = wh * (npr.rand(wh.shape[0], 1) * 0.9 + 0.1) # multiply by random scale 0-1 # wh = wh * (npr.rand(wh.shape[0], 1) * 0.9 + 0.1) # multiply by random scale 0-1
# Kmeans init # Kmeans init
try: try:
LOGGER.info(f'{PREFIX}Running kmeans for {n} anchors on {len(wh)} points...') LOGGER.info(f"{PREFIX}Running kmeans for {n} anchors on {len(wh)} points...")
assert n <= len(wh) # apply overdetermined constraint assert n <= len(wh) # apply overdetermined constraint
s = wh.std(0) # sigmas for whitening s = wh.std(0) # sigmas for whitening
k = kmeans(wh / s, n, iter=30)[0] * s # points k = kmeans(wh / s, n, iter=30)[0] * s # points
assert n == len(k) # kmeans may return fewer points than requested if wh is insufficient or too similar assert n == len(k) # kmeans may return fewer points than requested if wh is insufficient or too similar
except Exception: except Exception:
LOGGER.warning(f'{PREFIX}WARNING ⚠️ switching strategies from kmeans to random init') LOGGER.warning(f"{PREFIX}WARNING ⚠️ switching strategies from kmeans to random init")
k = np.sort(npr.rand(n * 2)).reshape(n, 2) * img_size # random init k = np.sort(npr.rand(n * 2)).reshape(n, 2) * img_size # random init
wh, wh0 = (torch.tensor(x, dtype=torch.float32) for x in (wh, wh0)) wh, wh0 = (torch.tensor(x, dtype=torch.float32) for x in (wh, wh0))
k = print_results(k, verbose=False) k = print_results(k, verbose=False)
@@ -162,7 +164,7 @@ def kmean_anchors(dataset='./data/coco128.yaml', n=9, img_size=640, thr=4.0, gen
fg = anchor_fitness(kg) fg = anchor_fitness(kg)
if fg > f: if fg > f:
f, k = fg, kg.copy() f, k = fg, kg.copy()
pbar.desc = f'{PREFIX}Evolving anchors with Genetic Algorithm: fitness = {f:.4f}' pbar.desc = f"{PREFIX}Evolving anchors with Genetic Algorithm: fitness = {f:.4f}"
if verbose: if verbose:
print_results(k, verbose) print_results(k, verbose)
+11 -13
View File
@@ -1,7 +1,5 @@
# YOLOv5 🚀 by Ultralytics, AGPL-3.0 license # YOLOv5 🚀 by Ultralytics, AGPL-3.0 license
""" """Auto-batch utils."""
Auto-batch utils
"""
from copy import deepcopy from copy import deepcopy
@@ -27,14 +25,14 @@ def autobatch(model, imgsz=640, fraction=0.8, batch_size=16):
# print(autobatch(model)) # print(autobatch(model))
# Check device # Check device
prefix = colorstr('AutoBatch: ') prefix = colorstr("AutoBatch: ")
LOGGER.info(f'{prefix}Computing optimal batch size for --imgsz {imgsz}') LOGGER.info(f"{prefix}Computing optimal batch size for --imgsz {imgsz}")
device = next(model.parameters()).device # get model device device = next(model.parameters()).device # get model device
if device.type == 'cpu': if device.type == "cpu":
LOGGER.info(f'{prefix}CUDA not detected, using default CPU batch-size {batch_size}') LOGGER.info(f"{prefix}CUDA not detected, using default CPU batch-size {batch_size}")
return batch_size return batch_size
if torch.backends.cudnn.benchmark: if torch.backends.cudnn.benchmark:
LOGGER.info(f'{prefix} ⚠️ Requires torch.backends.cudnn.benchmark=False, using default batch-size {batch_size}') LOGGER.info(f"{prefix} ⚠️ Requires torch.backends.cudnn.benchmark=False, using default batch-size {batch_size}")
return batch_size return batch_size
# Inspect CUDA memory # Inspect CUDA memory
@@ -45,7 +43,7 @@ def autobatch(model, imgsz=640, fraction=0.8, batch_size=16):
r = torch.cuda.memory_reserved(device) / gb # GiB reserved r = torch.cuda.memory_reserved(device) / gb # GiB reserved
a = torch.cuda.memory_allocated(device) / gb # GiB allocated a = torch.cuda.memory_allocated(device) / gb # GiB allocated
f = t - (r + a) # GiB free f = t - (r + a) # GiB free
LOGGER.info(f'{prefix}{d} ({properties.name}) {t:.2f}G total, {r:.2f}G reserved, {a:.2f}G allocated, {f:.2f}G free') LOGGER.info(f"{prefix}{d} ({properties.name}) {t:.2f}G total, {r:.2f}G reserved, {a:.2f}G allocated, {f:.2f}G free")
# Profile batch sizes # Profile batch sizes
batch_sizes = [1, 2, 4, 8, 16] batch_sizes = [1, 2, 4, 8, 16]
@@ -53,11 +51,11 @@ def autobatch(model, imgsz=640, fraction=0.8, batch_size=16):
img = [torch.empty(b, 3, imgsz, imgsz) for b in batch_sizes] img = [torch.empty(b, 3, imgsz, imgsz) for b in batch_sizes]
results = profile(img, model, n=3, device=device) results = profile(img, model, n=3, device=device)
except Exception as e: except Exception as e:
LOGGER.warning(f'{prefix}{e}') LOGGER.warning(f"{prefix}{e}")
# Fit a solution # Fit a solution
y = [x[2] for x in results if x] # memory [2] y = [x[2] for x in results if x] # memory [2]
p = np.polyfit(batch_sizes[:len(y)], y, deg=1) # first degree polynomial fit p = np.polyfit(batch_sizes[: len(y)], y, deg=1) # first degree polynomial fit
b = int((f * fraction - p[1]) / p[0]) # y intercept (optimal batch size) b = int((f * fraction - p[1]) / p[0]) # y intercept (optimal batch size)
if None in results: # some sizes failed if None in results: # some sizes failed
i = results.index(None) # first fail index i = results.index(None) # first fail index
@@ -65,8 +63,8 @@ def autobatch(model, imgsz=640, fraction=0.8, batch_size=16):
b = batch_sizes[max(i - 1, 0)] # select prior safe point b = batch_sizes[max(i - 1, 0)] # select prior safe point
if b < 1 or b > 1024: # b outside of safe range if b < 1 or b > 1024: # b outside of safe range
b = batch_size b = batch_size
LOGGER.warning(f'{prefix}WARNING ⚠️ CUDA anomaly detected, recommend restart environment and retry command.') LOGGER.warning(f"{prefix}WARNING ⚠️ CUDA anomaly detected, recommend restart environment and retry command.")
fraction = (np.polyval(p, b) + r + a) / t # actual fraction predicted fraction = (np.polyval(p, b) + r + a) / t # actual fraction predicted
LOGGER.info(f'{prefix}Using batch-size {b} for {d} {t * fraction:.2f}G/{t:.2f}G ({fraction * 100:.0f}%) ✅') LOGGER.info(f"{prefix}Using batch-size {b} for {d} {t * fraction:.2f}G/{t:.2f}G ({fraction * 100:.0f}%) ✅")
return b return b
+8 -8
View File
@@ -14,27 +14,27 @@ if str(ROOT) not in sys.path:
sys.path.append(str(ROOT)) # add ROOT to PATH sys.path.append(str(ROOT)) # add ROOT to PATH
port = 0 # --master_port port = 0 # --master_port
path = Path('').resolve() path = Path("").resolve()
for last in path.rglob('*/**/last.pt'): for last in path.rglob("*/**/last.pt"):
ckpt = torch.load(last) ckpt = torch.load(last)
if ckpt['optimizer'] is None: if ckpt["optimizer"] is None:
continue continue
# Load opt.yaml # Load opt.yaml
with open(last.parent.parent / 'opt.yaml', errors='ignore') as f: with open(last.parent.parent / "opt.yaml", errors="ignore") as f:
opt = yaml.safe_load(f) opt = yaml.safe_load(f)
# Get device count # Get device count
d = opt['device'].split(',') # devices d = opt["device"].split(",") # devices
nd = len(d) # number of devices nd = len(d) # number of devices
ddp = nd > 1 or (nd == 0 and torch.cuda.device_count() > 1) # distributed data parallel ddp = nd > 1 or (nd == 0 and torch.cuda.device_count() > 1) # distributed data parallel
if ddp: # multi-GPU if ddp: # multi-GPU
port += 1 port += 1
cmd = f'python -m torch.distributed.run --nproc_per_node {nd} --master_port {port} train.py --resume {last}' cmd = f"python -m torch.distributed.run --nproc_per_node {nd} --master_port {port} train.py --resume {last}"
else: # single-GPU else: # single-GPU
cmd = f'python train.py --resume {last}' cmd = f"python train.py --resume {last}"
cmd += ' > /dev/null 2>&1 &' # redirect output to dev/null and run in daemon thread cmd += " > /dev/null 2>&1 &" # redirect output to dev/null and run in daemon thread
print(cmd) print(cmd)
os.system(cmd) os.system(cmd)
+30 -33
View File
@@ -1,43 +1,40 @@
# YOLOv5 🚀 by Ultralytics, AGPL-3.0 license # YOLOv5 🚀 by Ultralytics, AGPL-3.0 license
""" """Callback utils."""
Callback utils
"""
import threading import threading
class Callbacks: class Callbacks:
"""" """" Handles all registered callbacks for YOLOv5 Hooks."""
Handles all registered callbacks for YOLOv5 Hooks
"""
def __init__(self): def __init__(self):
# Define the available callbacks # Define the available callbacks
self._callbacks = { self._callbacks = {
'on_pretrain_routine_start': [], "on_pretrain_routine_start": [],
'on_pretrain_routine_end': [], "on_pretrain_routine_end": [],
'on_train_start': [], "on_train_start": [],
'on_train_epoch_start': [], "on_train_epoch_start": [],
'on_train_batch_start': [], "on_train_batch_start": [],
'optimizer_step': [], "optimizer_step": [],
'on_before_zero_grad': [], "on_before_zero_grad": [],
'on_train_batch_end': [], "on_train_batch_end": [],
'on_train_epoch_end': [], "on_train_epoch_end": [],
'on_val_start': [], "on_val_start": [],
'on_val_batch_start': [], "on_val_batch_start": [],
'on_val_image_end': [], "on_val_image_end": [],
'on_val_batch_end': [], "on_val_batch_end": [],
'on_val_end': [], "on_val_end": [],
'on_fit_epoch_end': [], # fit = train + val "on_fit_epoch_end": [], # fit = train + val
'on_model_save': [], "on_model_save": [],
'on_train_end': [], "on_train_end": [],
'on_params_update': [], "on_params_update": [],
'teardown': [], } "teardown": [],
}
self.stop_training = False # set True to interrupt training self.stop_training = False # set True to interrupt training
def register_action(self, hook, name='', callback=None): def register_action(self, hook, name="", callback=None):
""" """
Register a new action to a callback hook Register a new action to a callback hook.
Args: Args:
hook: The callback hook name to register the action to hook: The callback hook name to register the action to
@@ -46,11 +43,11 @@ class Callbacks:
""" """
assert hook in self._callbacks, f"hook '{hook}' not found in callbacks {self._callbacks}" assert hook in self._callbacks, f"hook '{hook}' not found in callbacks {self._callbacks}"
assert callable(callback), f"callback '{callback}' is not callable" assert callable(callback), f"callback '{callback}' is not callable"
self._callbacks[hook].append({'name': name, 'callback': callback}) self._callbacks[hook].append({"name": name, "callback": callback})
def get_registered_actions(self, hook=None): def get_registered_actions(self, hook=None):
"""" """
Returns all the registered actions by callback hook " Returns all the registered actions by callback hook.
Args: Args:
hook: The name of the hook to check, defaults to all hook: The name of the hook to check, defaults to all
@@ -59,7 +56,7 @@ class Callbacks:
def run(self, hook, *args, thread=False, **kwargs): def run(self, hook, *args, thread=False, **kwargs):
""" """
Loop through the registered actions and fire all callbacks on main thread Loop through the registered actions and fire all callbacks on main thread.
Args: Args:
hook: The name of the hook to check, defaults to all hook: The name of the hook to check, defaults to all
@@ -71,6 +68,6 @@ class Callbacks:
assert hook in self._callbacks, f"hook '{hook}' not found in callbacks {self._callbacks}" assert hook in self._callbacks, f"hook '{hook}' not found in callbacks {self._callbacks}"
for logger in self._callbacks[hook]: for logger in self._callbacks[hook]:
if thread: if thread:
threading.Thread(target=logger['callback'], args=args, kwargs=kwargs, daemon=True).start() threading.Thread(target=logger["callback"], args=args, kwargs=kwargs, daemon=True).start()
else: else:
logger['callback'](*args, **kwargs) logger["callback"](*args, **kwargs)
+326 -276
View File
File diff suppressed because it is too large. Load diff
+46 -45
View File
@@ -1,7 +1,5 @@
# YOLOv5 🚀 by Ultralytics, AGPL-3.0 license # YOLOv5 🚀 by Ultralytics, AGPL-3.0 license
""" """Download utils."""
Download utils
"""
import logging import logging
import subprocess import subprocess
@@ -23,89 +21,90 @@ def is_url(url, check=True):
return False return False
def gsutil_getsize(url=''): def gsutil_getsize(url=""):
# gs://bucket/file size https://cloud.google.com/storage/docs/gsutil/commands/du # gs://bucket/file size https://cloud.google.com/storage/docs/gsutil/commands/du
output = subprocess.check_output(['gsutil', 'du', url], shell=True, encoding='utf-8') output = subprocess.check_output(["gsutil", "du", url], shell=True, encoding="utf-8")
if output: if output:
return int(output.split()[0]) return int(output.split()[0])
return 0 return 0
def url_getsize(url='https://ultralytics.com/images/bus.jpg'): def url_getsize(url="https://ultralytics.com/images/bus.jpg"):
# Return downloadable file size in bytes # Return downloadable file size in bytes
response = requests.head(url, allow_redirects=True) response = requests.head(url, allow_redirects=True)
return int(response.headers.get('content-length', -1)) return int(response.headers.get("content-length", -1))
def curl_download(url, filename, *, silent: bool = False) -> bool: def curl_download(url, filename, *, silent: bool = False) -> bool:
""" """Download a file from a url to a filename using curl."""
Download a file from a url to a filename using curl. silent_option = "sS" if silent else "" # silent
""" proc = subprocess.run(
silent_option = 'sS' if silent else '' # silent [
proc = subprocess.run([ "curl",
'curl', "-#",
'-#', f"-{silent_option}L",
f'-{silent_option}L', url,
url, "--output",
'--output', filename,
filename, "--retry",
'--retry', "9",
'9', "-C",
'-C', "-",
'-', ]) ]
)
return proc.returncode == 0 return proc.returncode == 0
def safe_download(file, url, url2=None, min_bytes=1E0, error_msg=''): def safe_download(file, url, url2=None, min_bytes=1e0, error_msg=""):
# Attempts to download file from url or url2, checks and removes incomplete downloads < min_bytes # Attempts to download file from url or url2, checks and removes incomplete downloads < min_bytes
from utils.general import LOGGER from utils.general import LOGGER
file = Path(file) file = Path(file)
assert_msg = f"Downloaded file '{file}' does not exist or size is < min_bytes={min_bytes}" assert_msg = f"Downloaded file '{file}' does not exist or size is < min_bytes={min_bytes}"
try: # url1 try: # url1
LOGGER.info(f'Downloading {url} to {file}...') LOGGER.info(f"Downloading {url} to {file}...")
torch.hub.download_url_to_file(url, str(file), progress=LOGGER.level <= logging.INFO) torch.hub.download_url_to_file(url, str(file), progress=LOGGER.level <= logging.INFO)
assert file.exists() and file.stat().st_size > min_bytes, assert_msg # check assert file.exists() and file.stat().st_size > min_bytes, assert_msg # check
except Exception as e: # url2 except Exception as e: # url2
if file.exists(): if file.exists():
file.unlink() # remove partial downloads file.unlink() # remove partial downloads
LOGGER.info(f'ERROR: {e}\nRe-attempting {url2 or url} to {file}...') LOGGER.info(f"ERROR: {e}\nRe-attempting {url2 or url} to {file}...")
# curl download, retry and resume on fail # curl download, retry and resume on fail
curl_download(url2 or url, file) curl_download(url2 or url, file)
finally: finally:
if not file.exists() or file.stat().st_size < min_bytes: # check if not file.exists() or file.stat().st_size < min_bytes: # check
if file.exists(): if file.exists():
file.unlink() # remove partial downloads file.unlink() # remove partial downloads
LOGGER.info(f'ERROR: {assert_msg}\n{error_msg}') LOGGER.info(f"ERROR: {assert_msg}\n{error_msg}")
LOGGER.info('') LOGGER.info("")
def attempt_download(file, repo='ultralytics/yolov5', release='v7.0'): def attempt_download(file, repo="ultralytics/yolov5", release="v7.0"):
# Attempt file download from GitHub release assets if not found locally. release = 'latest', 'v7.0', etc. # Attempt file download from GitHub release assets if not found locally. release = 'latest', 'v7.0', etc.
from utils.general import LOGGER from utils.general import LOGGER
def github_assets(repository, version='latest'): def github_assets(repository, version="latest"):
# Return GitHub repo tag (i.e. 'v7.0') and assets (i.e. ['yolov5s.pt', 'yolov5m.pt', ...]) # Return GitHub repo tag (i.e. 'v7.0') and assets (i.e. ['yolov5s.pt', 'yolov5m.pt', ...])
if version != 'latest': if version != "latest":
version = f'tags/{version}' # i.e. tags/v7.0 version = f"tags/{version}" # i.e. tags/v7.0
response = requests.get(f'https://api.github.com/repos/{repository}/releases/{version}').json() # github api response = requests.get(f"https://api.github.com/repos/{repository}/releases/{version}").json() # github api
return response['tag_name'], [x['name'] for x in response['assets']] # tag, assets return response["tag_name"], [x["name"] for x in response["assets"]] # tag, assets
file = Path(str(file).strip().replace("'", '')) file = Path(str(file).strip().replace("'", ""))
if not file.exists(): if not file.exists():
# URL specified # URL specified
name = Path(urllib.parse.unquote(str(file))).name # decode '%2F' to '/' etc. name = Path(urllib.parse.unquote(str(file))).name # decode '%2F' to '/' etc.
if str(file).startswith(('http:/', 'https:/')): # download if str(file).startswith(("http:/", "https:/")): # download
url = str(file).replace(':/', '://') # Pathlib turns :// -> :/ url = str(file).replace(":/", "://") # Pathlib turns :// -> :/
file = name.split('?')[0] # parse authentication https://url.com/file.txt?auth... file = name.split("?")[0] # parse authentication https://url.com/file.txt?auth...
if Path(file).is_file(): if Path(file).is_file():
LOGGER.info(f'Found {url} locally at {file}') # file already exists LOGGER.info(f"Found {url} locally at {file}") # file already exists
else: else:
safe_download(file=file, url=url, min_bytes=1E5) safe_download(file=file, url=url, min_bytes=1e5)
return file return file
# GitHub assets # GitHub assets
assets = [f'yolov5{size}{suffix}.pt' for size in 'nsmlx' for suffix in ('', '6', '-cls', '-seg')] # default assets = [f"yolov5{size}{suffix}.pt" for size in "nsmlx" for suffix in ("", "6", "-cls", "-seg")] # default
try: try:
tag, assets = github_assets(repo, release) tag, assets = github_assets(repo, release)
except Exception: except Exception:
@@ -113,15 +112,17 @@ def attempt_download(file, repo='ultralytics/yolov5', release='v7.0'):
tag, assets = github_assets(repo) # latest release tag, assets = github_assets(repo) # latest release
except Exception: except Exception:
try: try:
tag = subprocess.check_output('git tag', shell=True, stderr=subprocess.STDOUT).decode().split()[-1] tag = subprocess.check_output("git tag", shell=True, stderr=subprocess.STDOUT).decode().split()[-1]
except Exception: except Exception:
tag = release tag = release
if name in assets: if name in assets:
file.parent.mkdir(parents=True, exist_ok=True) # make parent dir (if required) file.parent.mkdir(parents=True, exist_ok=True) # make parent dir (if required)
safe_download(file, safe_download(
url=f'https://github.com/{repo}/releases/download/{tag}/{name}', file,
min_bytes=1E5, url=f"https://github.com/{repo}/releases/download/{tag}/{name}",
error_msg=f'{file} missing, try downloading from https://github.com/{repo}/releases/{tag}') min_bytes=1e5,
error_msg=f"{file} missing, try downloading from https://github.com/{repo}/releases/{tag}",
)
return str(file) return str(file)
+5 -7
View File
@@ -1,19 +1,17 @@
# YOLOv5 🚀 by Ultralytics, AGPL-3.0 license # YOLOv5 🚀 by Ultralytics, AGPL-3.0 license
""" """Perform test request."""
Perform test request
"""
import pprint import pprint
import requests import requests
DETECTION_URL = 'http://localhost:5000/v1/object-detection/yolov5s' DETECTION_URL = "http://localhost:5000/v1/object-detection/yolov5s"
IMAGE = 'zidane.jpg' IMAGE = "zidane.jpg"
# Read image # Read image
with open(IMAGE, 'rb') as f: with open(IMAGE, "rb") as f:
image_data = f.read() image_data = f.read()
response = requests.post(DETECTION_URL, files={'image': image_data}).json() response = requests.post(DETECTION_URL, files={"image": image_data}).json()
pprint.pprint(response) pprint.pprint(response)
+13 -15
View File
@@ -1,7 +1,5 @@
# YOLOv5 🚀 by Ultralytics, AGPL-3.0 license # YOLOv5 🚀 by Ultralytics, AGPL-3.0 license
""" """Run a Flask REST API exposing one or more YOLOv5s models."""
Run a Flask REST API exposing one or more YOLOv5s models
"""
import argparse import argparse
import io import io
@@ -13,36 +11,36 @@ from PIL import Image
app = Flask(__name__) app = Flask(__name__)
models = {} models = {}
DETECTION_URL = '/v1/object-detection/<model>' DETECTION_URL = "/v1/object-detection/<model>"
@app.route(DETECTION_URL, methods=['POST']) @app.route(DETECTION_URL, methods=["POST"])
def predict(model): def predict(model):
if request.method != 'POST': if request.method != "POST":
return return
if request.files.get('image'): if request.files.get("image"):
# Method 1 # Method 1
# with request.files["image"] as f: # with request.files["image"] as f:
# im = Image.open(io.BytesIO(f.read())) # im = Image.open(io.BytesIO(f.read()))
# Method 2 # Method 2
im_file = request.files['image'] im_file = request.files["image"]
im_bytes = im_file.read() im_bytes = im_file.read()
im = Image.open(io.BytesIO(im_bytes)) im = Image.open(io.BytesIO(im_bytes))
if model in models: if model in models:
results = models[model](im, size=640) # reduce size=320 for faster inference results = models[model](im, size=640) # reduce size=320 for faster inference
return results.pandas().xyxy[0].to_json(orient='records') return results.pandas().xyxy[0].to_json(orient="records")
if __name__ == '__main__': if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Flask API exposing YOLOv5 model') parser = argparse.ArgumentParser(description="Flask API exposing YOLOv5 model")
parser.add_argument('--port', default=5000, type=int, help='port number') parser.add_argument("--port", default=5000, type=int, help="port number")
parser.add_argument('--model', nargs='+', default=['yolov5s'], help='model(s) to run, i.e. --model yolov5n yolov5s') parser.add_argument("--model", nargs="+", default=["yolov5s"], help="model(s) to run, i.e. --model yolov5n yolov5s")
opt = parser.parse_args() opt = parser.parse_args()
for m in opt.model: for m in opt.model:
models[m] = torch.hub.load('ultralytics/yolov5', m, force_reload=True, skip_validation=True) models[m] = torch.hub.load("ultralytics/yolov5", m, force_reload=True, skip_validation=True)
app.run(host='0.0.0.0', port=opt.port) # debug=True causes Restarting with stat app.run(host="0.0.0.0", port=opt.port) # debug=True causes Restarting with stat
+338 -234
View File
File diff suppressed because it is too large. Load diff
+103 -92
View File
@@ -1,7 +1,5 @@
# YOLOv5 🚀 by Ultralytics, AGPL-3.0 license # YOLOv5 🚀 by Ultralytics, AGPL-3.0 license
""" """Logging utils."""
Logging utils
"""
import json import json
import os import os
import warnings import warnings
@@ -16,8 +14,8 @@ from utils.loggers.wandb.wandb_utils import WandbLogger
from utils.plots import plot_images, plot_labels, plot_results from utils.plots import plot_images, plot_labels, plot_results
from utils.torch_utils import de_parallel from utils.torch_utils import de_parallel
LOGGERS = ('csv', 'tb', 'wandb', 'clearml', 'comet') # *.csv, TensorBoard, Weights & Biases, ClearML LOGGERS = ("csv", "tb", "wandb", "clearml", "comet") # *.csv, TensorBoard, Weights & Biases, ClearML
RANK = int(os.getenv('RANK', -1)) RANK = int(os.getenv("RANK", -1))
try: try:
from torch.utils.tensorboard import SummaryWriter from torch.utils.tensorboard import SummaryWriter
@@ -27,8 +25,8 @@ except ImportError:
try: try:
import wandb import wandb
assert hasattr(wandb, '__version__') # verify package import not local dir assert hasattr(wandb, "__version__") # verify package import not local dir
if pkg.parse_version(wandb.__version__) >= pkg.parse_version('0.12.2') and RANK in {0, -1}: if pkg.parse_version(wandb.__version__) >= pkg.parse_version("0.12.2") and RANK in {0, -1}:
try: try:
wandb_login_success = wandb.login(timeout=30) wandb_login_success = wandb.login(timeout=30)
except wandb.errors.UsageError: # known non-TTY terminal issue except wandb.errors.UsageError: # known non-TTY terminal issue
@@ -41,7 +39,7 @@ except (ImportError, AssertionError):
try: try:
import clearml import clearml
assert hasattr(clearml, '__version__') # verify package import not local dir assert hasattr(clearml, "__version__") # verify package import not local dir
except (ImportError, AssertionError): except (ImportError, AssertionError):
clearml = None clearml = None
@@ -49,7 +47,7 @@ try:
if RANK in {0, -1}: if RANK in {0, -1}:
import comet_ml import comet_ml
assert hasattr(comet_ml, '__version__') # verify package import not local dir assert hasattr(comet_ml, "__version__") # verify package import not local dir
from utils.loggers.comet import CometLogger from utils.loggers.comet import CometLogger
else: else:
@@ -59,7 +57,11 @@ except (ImportError, AssertionError):
def _json_default(value): def _json_default(value):
"""Format `value` for JSON serialization (e.g. unwrap tensors). Fall back to strings.""" """
Format `value` for JSON serialization (e.g. unwrap tensors).
Fall back to strings.
"""
if isinstance(value, torch.Tensor): if isinstance(value, torch.Tensor):
try: try:
value = value.item() value = value.item()
@@ -70,7 +72,7 @@ def _json_default(value):
return str(value) return str(value)
class Loggers(): class Loggers:
# YOLOv5 Loggers class # YOLOv5 Loggers class
def __init__(self, save_dir=None, weights=None, opt=None, hyp=None, logger=None, include=LOGGERS): def __init__(self, save_dir=None, weights=None, opt=None, hyp=None, logger=None, include=LOGGERS):
self.save_dir = save_dir self.save_dir = save_dir
@@ -81,62 +83,65 @@ class Loggers():
self.logger = logger # for printing results to console self.logger = logger # for printing results to console
self.include = include self.include = include
self.keys = [ self.keys = [
'train/box_loss', "train/box_loss",
'train/obj_loss', "train/obj_loss",
'train/cls_loss', # train loss "train/cls_loss", # train loss
'metrics/precision', "metrics/precision",
'metrics/recall', "metrics/recall",
'metrics/mAP_0.5', "metrics/mAP_0.5",
'metrics/mAP_0.5:0.95', # metrics "metrics/mAP_0.5:0.95", # metrics
'val/box_loss', "val/box_loss",
'val/obj_loss', "val/obj_loss",
'val/cls_loss', # val loss "val/cls_loss", # val loss
'x/lr0', "x/lr0",
'x/lr1', "x/lr1",
'x/lr2'] # params "x/lr2",
self.best_keys = ['best/epoch', 'best/precision', 'best/recall', 'best/mAP_0.5', 'best/mAP_0.5:0.95'] ] # params
self.best_keys = ["best/epoch", "best/precision", "best/recall", "best/mAP_0.5", "best/mAP_0.5:0.95"]
for k in LOGGERS: for k in LOGGERS:
setattr(self, k, None) # init empty logger dictionary setattr(self, k, None) # init empty logger dictionary
self.csv = True # always log to csv self.csv = True # always log to csv
self.ndjson_console = ('ndjson_console' in self.include) # log ndjson to console self.ndjson_console = "ndjson_console" in self.include # log ndjson to console
self.ndjson_file = ('ndjson_file' in self.include) # log ndjson to file self.ndjson_file = "ndjson_file" in self.include # log ndjson to file
# Messages # Messages
if not comet_ml: if not comet_ml:
prefix = colorstr('Comet: ') prefix = colorstr("Comet: ")
s = f"{prefix}run 'pip install comet_ml' to automatically track and visualize YOLOv5 🚀 runs in Comet" s = f"{prefix}run 'pip install comet_ml' to automatically track and visualize YOLOv5 🚀 runs in Comet"
self.logger.info(s) self.logger.info(s)
# TensorBoard # TensorBoard
s = self.save_dir s = self.save_dir
if 'tb' in self.include and not self.opt.evolve: if "tb" in self.include and not self.opt.evolve:
prefix = colorstr('TensorBoard: ') prefix = colorstr("TensorBoard: ")
self.logger.info(f"{prefix}Start with 'tensorboard --logdir {s.parent}', view at http://localhost:6006/") self.logger.info(f"{prefix}Start with 'tensorboard --logdir {s.parent}', view at http://localhost:6006/")
self.tb = SummaryWriter(str(s)) self.tb = SummaryWriter(str(s))
# W&B # W&B
if wandb and 'wandb' in self.include: if wandb and "wandb" in self.include:
self.opt.hyp = self.hyp # add hyperparameters self.opt.hyp = self.hyp # add hyperparameters
self.wandb = WandbLogger(self.opt) self.wandb = WandbLogger(self.opt)
else: else:
self.wandb = None self.wandb = None
# ClearML # ClearML
if clearml and 'clearml' in self.include: if clearml and "clearml" in self.include:
try: try:
self.clearml = ClearmlLogger(self.opt, self.hyp) self.clearml = ClearmlLogger(self.opt, self.hyp)
except Exception: except Exception:
self.clearml = None self.clearml = None
prefix = colorstr('ClearML: ') prefix = colorstr("ClearML: ")
LOGGER.warning(f'{prefix}WARNING ⚠️ ClearML is installed but not configured, skipping ClearML logging.' LOGGER.warning(
f' See https://docs.ultralytics.com/yolov5/tutorials/clearml_logging_integration#readme') f"{prefix}WARNING ⚠️ ClearML is installed but not configured, skipping ClearML logging."
f" See https://docs.ultralytics.com/yolov5/tutorials/clearml_logging_integration#readme"
)
else: else:
self.clearml = None self.clearml = None
# Comet # Comet
if comet_ml and 'comet' in self.include: if comet_ml and "comet" in self.include:
if isinstance(self.opt.resume, str) and self.opt.resume.startswith('comet://'): if isinstance(self.opt.resume, str) and self.opt.resume.startswith("comet://"):
run_id = self.opt.resume.split('/')[-1] run_id = self.opt.resume.split("/")[-1]
self.comet_logger = CometLogger(self.opt, self.hyp, run_id=run_id) self.comet_logger = CometLogger(self.opt, self.hyp, run_id=run_id)
else: else:
@@ -170,9 +175,9 @@ class Loggers():
# Callback runs on pre-train routine end # Callback runs on pre-train routine end
if self.plots: if self.plots:
plot_labels(labels, names, self.save_dir) plot_labels(labels, names, self.save_dir)
paths = self.save_dir.glob('*labels*.jpg') # training labels paths = self.save_dir.glob("*labels*.jpg") # training labels
if self.wandb: if self.wandb:
self.wandb.log({'Labels': [wandb.Image(str(x), caption=x.name) for x in paths]}) self.wandb.log({"Labels": [wandb.Image(str(x), caption=x.name) for x in paths]})
if self.comet_logger: if self.comet_logger:
self.comet_logger.on_pretrain_routine_end(paths) self.comet_logger.on_pretrain_routine_end(paths)
if self.clearml: if self.clearml:
@@ -185,16 +190,16 @@ class Loggers():
# ni: number integrated batches (since train start) # ni: number integrated batches (since train start)
if self.plots: if self.plots:
if ni < 3: if ni < 3:
f = self.save_dir / f'train_batch{ni}.jpg' # filename f = self.save_dir / f"train_batch{ni}.jpg" # filename
plot_images(imgs, targets, paths, f) plot_images(imgs, targets, paths, f)
if ni == 0 and self.tb and not self.opt.sync_bn: if ni == 0 and self.tb and not self.opt.sync_bn:
log_tensorboard_graph(self.tb, model, imgsz=(self.opt.imgsz, self.opt.imgsz)) log_tensorboard_graph(self.tb, model, imgsz=(self.opt.imgsz, self.opt.imgsz))
if ni == 10 and (self.wandb or self.clearml): if ni == 10 and (self.wandb or self.clearml):
files = sorted(self.save_dir.glob('train*.jpg')) files = sorted(self.save_dir.glob("train*.jpg"))
if self.wandb: if self.wandb:
self.wandb.log({'Mosaics': [wandb.Image(str(f), caption=f.name) for f in files if f.exists()]}) self.wandb.log({"Mosaics": [wandb.Image(str(f), caption=f.name) for f in files if f.exists()]})
if self.clearml: if self.clearml:
self.clearml.log_debug_samples(files, title='Mosaics') self.clearml.log_debug_samples(files, title="Mosaics")
if self.comet_logger: if self.comet_logger:
self.comet_logger.on_train_batch_end(log_dict, step=ni) self.comet_logger.on_train_batch_end(log_dict, step=ni)
@@ -225,11 +230,11 @@ class Loggers():
def on_val_end(self, nt, tp, fp, p, r, f1, ap, ap50, ap_class, confusion_matrix): def on_val_end(self, nt, tp, fp, p, r, f1, ap, ap50, ap_class, confusion_matrix):
# Callback runs on val end # Callback runs on val end
if self.wandb or self.clearml: if self.wandb or self.clearml:
files = sorted(self.save_dir.glob('val*.jpg')) files = sorted(self.save_dir.glob("val*.jpg"))
if self.wandb: if self.wandb:
self.wandb.log({'Validation': [wandb.Image(str(f), caption=f.name) for f in files]}) self.wandb.log({"Validation": [wandb.Image(str(f), caption=f.name) for f in files]})
if self.clearml: if self.clearml:
self.clearml.log_debug_samples(files, title='Validation') self.clearml.log_debug_samples(files, title="Validation")
if self.comet_logger: if self.comet_logger:
self.comet_logger.on_val_end(nt, tp, fp, p, r, f1, ap, ap50, ap_class, confusion_matrix) self.comet_logger.on_val_end(nt, tp, fp, p, r, f1, ap, ap50, ap_class, confusion_matrix)
@@ -238,18 +243,18 @@ class Loggers():
# Callback runs at the end of each fit (train+val) epoch # Callback runs at the end of each fit (train+val) epoch
x = dict(zip(self.keys, vals)) x = dict(zip(self.keys, vals))
if self.csv: if self.csv:
file = self.save_dir / 'results.csv' file = self.save_dir / "results.csv"
n = len(x) + 1 # number of cols n = len(x) + 1 # number of cols
s = '' if file.exists() else (('%20s,' * n % tuple(['epoch'] + self.keys)).rstrip(',') + '\n') # add header s = "" if file.exists() else (("%20s," * n % tuple(["epoch"] + self.keys)).rstrip(",") + "\n") # add header
with open(file, 'a') as f: with open(file, "a") as f:
f.write(s + ('%20.5g,' * n % tuple([epoch] + vals)).rstrip(',') + '\n') f.write(s + ("%20.5g," * n % tuple([epoch] + vals)).rstrip(",") + "\n")
if self.ndjson_console or self.ndjson_file: if self.ndjson_console or self.ndjson_file:
json_data = json.dumps(dict(epoch=epoch, **x), default=_json_default) json_data = json.dumps(dict(epoch=epoch, **x), default=_json_default)
if self.ndjson_console: if self.ndjson_console:
print(json_data) print(json_data)
if self.ndjson_file: if self.ndjson_file:
file = self.save_dir / 'results.ndjson' file = self.save_dir / "results.ndjson"
with open(file, 'a') as f: with open(file, "a") as f:
print(json_data, file=f) print(json_data, file=f)
if self.tb: if self.tb:
@@ -279,9 +284,9 @@ class Loggers():
if self.wandb: if self.wandb:
self.wandb.log_model(last.parent, self.opt, epoch, fi, best_model=best_fitness == fi) self.wandb.log_model(last.parent, self.opt, epoch, fi, best_model=best_fitness == fi)
if self.clearml: if self.clearml:
self.clearml.task.update_output_model(model_path=str(last), self.clearml.task.update_output_model(
model_name='Latest Model', model_path=str(last), model_name="Latest Model", auto_delete_file=False
auto_delete_file=False) )
if self.comet_logger: if self.comet_logger:
self.comet_logger.on_model_save(last, epoch, final_epoch, best_fitness, fi) self.comet_logger.on_model_save(last, epoch, final_epoch, best_fitness, fi)
@@ -289,31 +294,34 @@ class Loggers():
def on_train_end(self, last, best, epoch, results): def on_train_end(self, last, best, epoch, results):
# Callback runs on training end, i.e. saving best model # Callback runs on training end, i.e. saving best model
if self.plots: if self.plots:
plot_results(file=self.save_dir / 'results.csv') # save results.png plot_results(file=self.save_dir / "results.csv") # save results.png
files = ['results.png', 'confusion_matrix.png', *(f'{x}_curve.png' for x in ('F1', 'PR', 'P', 'R'))] files = ["results.png", "confusion_matrix.png", *(f"{x}_curve.png" for x in ("F1", "PR", "P", "R"))]
files = [(self.save_dir / f) for f in files if (self.save_dir / f).exists()] # filter files = [(self.save_dir / f) for f in files if (self.save_dir / f).exists()] # filter
self.logger.info(f"Results saved to {colorstr('bold', self.save_dir)}") self.logger.info(f"Results saved to {colorstr('bold', self.save_dir)}")
if self.tb and not self.clearml: # These images are already captured by ClearML by now, we don't want doubles if self.tb and not self.clearml: # These images are already captured by ClearML by now, we don't want doubles
for f in files: for f in files:
self.tb.add_image(f.stem, cv2.imread(str(f))[..., ::-1], epoch, dataformats='HWC') self.tb.add_image(f.stem, cv2.imread(str(f))[..., ::-1], epoch, dataformats="HWC")
if self.wandb: if self.wandb:
self.wandb.log(dict(zip(self.keys[3:10], results))) self.wandb.log(dict(zip(self.keys[3:10], results)))
self.wandb.log({'Results': [wandb.Image(str(f), caption=f.name) for f in files]}) self.wandb.log({"Results": [wandb.Image(str(f), caption=f.name) for f in files]})
# Calling wandb.log. TODO: Refactor this into WandbLogger.log_model # Calling wandb.log. TODO: Refactor this into WandbLogger.log_model
if not self.opt.evolve: if not self.opt.evolve:
wandb.log_artifact(str(best if best.exists() else last), wandb.log_artifact(
type='model', str(best if best.exists() else last),
name=f'run_{self.wandb.wandb_run.id}_model', type="model",
aliases=['latest', 'best', 'stripped']) name=f"run_{self.wandb.wandb_run.id}_model",
aliases=["latest", "best", "stripped"],
)
self.wandb.finish_run() self.wandb.finish_run()
if self.clearml and not self.opt.evolve: if self.clearml and not self.opt.evolve:
self.clearml.log_summary(dict(zip(self.keys[3:10], results))) self.clearml.log_summary(dict(zip(self.keys[3:10], results)))
[self.clearml.log_plot(title=f.stem, plot_path=f) for f in files] [self.clearml.log_plot(title=f.stem, plot_path=f) for f in files]
self.clearml.log_model(str(best if best.exists() else last), self.clearml.log_model(
"Best Model" if best.exists() else "Last Model", epoch) str(best if best.exists() else last), "Best Model" if best.exists() else "Last Model", epoch
)
if self.comet_logger: if self.comet_logger:
final_results = dict(zip(self.keys[3:10], results)) final_results = dict(zip(self.keys[3:10], results))
@@ -339,38 +347,41 @@ class GenericLogger:
include: loggers to include include: loggers to include
""" """
def __init__(self, opt, console_logger, include=('tb', 'wandb', 'clearml')): def __init__(self, opt, console_logger, include=("tb", "wandb", "clearml")):
# init default loggers # init default loggers
self.save_dir = Path(opt.save_dir) self.save_dir = Path(opt.save_dir)
self.include = include self.include = include
self.console_logger = console_logger self.console_logger = console_logger
self.csv = self.save_dir / 'results.csv' # CSV logger self.csv = self.save_dir / "results.csv" # CSV logger
if 'tb' in self.include: if "tb" in self.include:
prefix = colorstr('TensorBoard: ') prefix = colorstr("TensorBoard: ")
self.console_logger.info( self.console_logger.info(
f"{prefix}Start with 'tensorboard --logdir {self.save_dir.parent}', view at http://localhost:6006/") f"{prefix}Start with 'tensorboard --logdir {self.save_dir.parent}', view at http://localhost:6006/"
)
self.tb = SummaryWriter(str(self.save_dir)) self.tb = SummaryWriter(str(self.save_dir))
if wandb and 'wandb' in self.include: if wandb and "wandb" in self.include:
self.wandb = wandb.init(project=web_project_name(str(opt.project)), self.wandb = wandb.init(
name=None if opt.name == 'exp' else opt.name, project=web_project_name(str(opt.project)), name=None if opt.name == "exp" else opt.name, config=opt
config=opt) )
else: else:
self.wandb = None self.wandb = None
if clearml and 'clearml' in self.include: if clearml and "clearml" in self.include:
try: try:
# Hyp is not available in classification mode # Hyp is not available in classification mode
if 'hyp' not in opt: if "hyp" not in opt:
hyp = {} hyp = {}
else: else:
hyp = opt.hyp hyp = opt.hyp
self.clearml = ClearmlLogger(opt, hyp) self.clearml = ClearmlLogger(opt, hyp)
except Exception: except Exception:
self.clearml = None self.clearml = None
prefix = colorstr('ClearML: ') prefix = colorstr("ClearML: ")
LOGGER.warning(f'{prefix}WARNING ⚠️ ClearML is installed but not configured, skipping ClearML logging.' LOGGER.warning(
f' See https://github.com/ultralytics/yolov5/tree/master/utils/loggers/clearml#readme') f"{prefix}WARNING ⚠️ ClearML is installed but not configured, skipping ClearML logging."
f" See https://github.com/ultralytics/yolov5/tree/master/utils/loggers/clearml#readme"
)
else: else:
self.clearml = None self.clearml = None
@@ -379,9 +390,9 @@ class GenericLogger:
if self.csv: if self.csv:
keys, vals = list(metrics.keys()), list(metrics.values()) keys, vals = list(metrics.keys()), list(metrics.values())
n = len(metrics) + 1 # number of cols n = len(metrics) + 1 # number of cols
s = '' if self.csv.exists() else (('%23s,' * n % tuple(['epoch'] + keys)).rstrip(',') + '\n') # header s = "" if self.csv.exists() else (("%23s," * n % tuple(["epoch"] + keys)).rstrip(",") + "\n") # header
with open(self.csv, 'a') as f: with open(self.csv, "a") as f:
f.write(s + ('%23.5g,' * n % tuple([epoch] + vals)).rstrip(',') + '\n') f.write(s + ("%23.5g," * n % tuple([epoch] + vals)).rstrip(",") + "\n")
if self.tb: if self.tb:
for k, v in metrics.items(): for k, v in metrics.items():
@@ -393,20 +404,20 @@ class GenericLogger:
if self.clearml: if self.clearml:
self.clearml.log_scalars(metrics, epoch) self.clearml.log_scalars(metrics, epoch)
def log_images(self, files, name='Images', epoch=0): def log_images(self, files, name="Images", epoch=0):
# Log images to all loggers # Log images to all loggers
files = [Path(f) for f in (files if isinstance(files, (tuple, list)) else [files])] # to Path files = [Path(f) for f in (files if isinstance(files, (tuple, list)) else [files])] # to Path
files = [f for f in files if f.exists()] # filter by exists files = [f for f in files if f.exists()] # filter by exists
if self.tb: if self.tb:
for f in files: for f in files:
self.tb.add_image(f.stem, cv2.imread(str(f))[..., ::-1], epoch, dataformats='HWC') self.tb.add_image(f.stem, cv2.imread(str(f))[..., ::-1], epoch, dataformats="HWC")
if self.wandb: if self.wandb:
self.wandb.log({name: [wandb.Image(str(f), caption=f.name) for f in files]}, step=epoch) self.wandb.log({name: [wandb.Image(str(f), caption=f.name) for f in files]}, step=epoch)
if self.clearml: if self.clearml:
if name == 'Results': if name == "Results":
[self.clearml.log_plot(f.stem, f) for f in files] [self.clearml.log_plot(f.stem, f) for f in files]
else: else:
self.clearml.log_debug_samples(files, title=name) self.clearml.log_debug_samples(files, title=name)
@@ -419,7 +430,7 @@ class GenericLogger:
def log_model(self, model_path, epoch=0, metadata={}): def log_model(self, model_path, epoch=0, metadata={}):
# Log model to all loggers # Log model to all loggers
if self.wandb: if self.wandb:
art = wandb.Artifact(name=f'run_{wandb.run.id}_model', type='model', metadata=metadata) art = wandb.Artifact(name=f"run_{wandb.run.id}_model", type="model", metadata=metadata)
art.add_file(str(model_path)) art.add_file(str(model_path))
wandb.log_artifact(art) wandb.log_artifact(art)
if self.clearml: if self.clearml:
@@ -440,15 +451,15 @@ def log_tensorboard_graph(tb, model, imgsz=(640, 640)):
imgsz = (imgsz, imgsz) if isinstance(imgsz, int) else imgsz # expand imgsz = (imgsz, imgsz) if isinstance(imgsz, int) else imgsz # expand
im = torch.zeros((1, 3, *imgsz)).to(p.device).type_as(p) # input image (WARNING: must be zeros, not empty) im = torch.zeros((1, 3, *imgsz)).to(p.device).type_as(p) # input image (WARNING: must be zeros, not empty)
with warnings.catch_warnings(): with warnings.catch_warnings():
warnings.simplefilter('ignore') # suppress jit trace warning warnings.simplefilter("ignore") # suppress jit trace warning
tb.add_graph(torch.jit.trace(de_parallel(model), im, strict=False), []) tb.add_graph(torch.jit.trace(de_parallel(model), im, strict=False), [])
except Exception as e: except Exception as e:
LOGGER.warning(f'WARNING ⚠️ TensorBoard graph visualization failure {e}') LOGGER.warning(f"WARNING ⚠️ TensorBoard graph visualization failure {e}")
def web_project_name(project): def web_project_name(project):
# Convert local project name to web project name # Convert local project name to web project name
if not project.startswith('runs/train'): if not project.startswith("runs/train"):
return project return project
suffix = '-Classify' if project.endswith('-cls') else '-Segment' if project.endswith('-seg') else '' suffix = "-Classify" if project.endswith("-cls") else "-Segment" if project.endswith("-seg") else ""
return f'YOLOv5{suffix}' return f"YOLOv5{suffix}"
+64 -59
View File
@@ -13,55 +13,63 @@ try:
import clearml import clearml
from clearml import Dataset, Task from clearml import Dataset, Task
assert hasattr(clearml, '__version__') # verify package import not local dir assert hasattr(clearml, "__version__") # verify package import not local dir
except (ImportError, AssertionError): except (ImportError, AssertionError):
clearml = None clearml = None
def construct_dataset(clearml_info_string): def construct_dataset(clearml_info_string):
"""Load in a clearml dataset and fill the internal data_dict with its contents. """Load in a clearml dataset and fill the internal data_dict with its contents."""
""" dataset_id = clearml_info_string.replace("clearml://", "")
dataset_id = clearml_info_string.replace('clearml://', '')
dataset = Dataset.get(dataset_id=dataset_id) dataset = Dataset.get(dataset_id=dataset_id)
dataset_root_path = Path(dataset.get_local_copy()) dataset_root_path = Path(dataset.get_local_copy())
# We'll search for the yaml file definition in the dataset # We'll search for the yaml file definition in the dataset
yaml_filenames = list(glob.glob(str(dataset_root_path / '*.yaml')) + glob.glob(str(dataset_root_path / '*.yml'))) yaml_filenames = list(glob.glob(str(dataset_root_path / "*.yaml")) + glob.glob(str(dataset_root_path / "*.yml")))
if len(yaml_filenames) > 1: if len(yaml_filenames) > 1:
raise ValueError('More than one yaml file was found in the dataset root, cannot determine which one contains ' raise ValueError(
'the dataset definition this way.') "More than one yaml file was found in the dataset root, cannot determine which one contains "
"the dataset definition this way."
)
elif len(yaml_filenames) == 0: elif len(yaml_filenames) == 0:
raise ValueError('No yaml definition found in dataset root path, check that there is a correct yaml file ' raise ValueError(
'inside the dataset root path.') "No yaml definition found in dataset root path, check that there is a correct yaml file "
"inside the dataset root path."
)
with open(yaml_filenames[0]) as f: with open(yaml_filenames[0]) as f:
dataset_definition = yaml.safe_load(f) dataset_definition = yaml.safe_load(f)
assert set(dataset_definition.keys()).issuperset( assert set(
{'train', 'test', 'val', 'nc', 'names'} dataset_definition.keys()
).issuperset(
{"train", "test", "val", "nc", "names"}
), "The right keys were not found in the yaml file, make sure it at least has the following keys: ('train', 'test', 'val', 'nc', 'names')" ), "The right keys were not found in the yaml file, make sure it at least has the following keys: ('train', 'test', 'val', 'nc', 'names')"
data_dict = dict() data_dict = dict()
data_dict['train'] = str( data_dict["train"] = (
(dataset_root_path / dataset_definition['train']).resolve()) if dataset_definition['train'] else None str((dataset_root_path / dataset_definition["train"]).resolve()) if dataset_definition["train"] else None
data_dict['test'] = str( )
(dataset_root_path / dataset_definition['test']).resolve()) if dataset_definition['test'] else None data_dict["test"] = (
data_dict['val'] = str( str((dataset_root_path / dataset_definition["test"]).resolve()) if dataset_definition["test"] else None
(dataset_root_path / dataset_definition['val']).resolve()) if dataset_definition['val'] else None )
data_dict['nc'] = dataset_definition['nc'] data_dict["val"] = (
data_dict['names'] = dataset_definition['names'] str((dataset_root_path / dataset_definition["val"]).resolve()) if dataset_definition["val"] else None
)
data_dict["nc"] = dataset_definition["nc"]
data_dict["names"] = dataset_definition["names"]
return data_dict return data_dict
class ClearmlLogger: class ClearmlLogger:
"""Log training runs, datasets, models, and predictions to ClearML. """
Log training runs, datasets, models, and predictions to ClearML.
This logger sends information to ClearML at app.clear.ml or to your own hosted server. By default, This logger sends information to ClearML at app.clear.ml or to your own hosted server. By default, this information
this information includes hyperparameters, system configuration and metrics, model metrics, code information and includes hyperparameters, system configuration and metrics, model metrics, code information and basic data metrics
basic data metrics and analyses. and analyses.
By providing additional command line arguments to train.py, datasets, By providing additional command line arguments to train.py, datasets, models and predictions can also be logged.
models and predictions can also be logged.
""" """
def __init__(self, opt, hyp): def __init__(self, opt, hyp):
@@ -81,36 +89,36 @@ class ClearmlLogger:
self.max_imgs_to_log_per_epoch = 16 self.max_imgs_to_log_per_epoch = 16
# Get the interval of epochs when bounding box images should be logged # Get the interval of epochs when bounding box images should be logged
# Only for detection task though! # Only for detection task though!
if 'bbox_interval' in opt: if "bbox_interval" in opt:
self.bbox_interval = opt.bbox_interval self.bbox_interval = opt.bbox_interval
self.clearml = clearml self.clearml = clearml
self.task = None self.task = None
self.data_dict = None self.data_dict = None
if self.clearml: if self.clearml:
self.task = Task.init( self.task = Task.init(
project_name=opt.project if not str(opt.project).startswith('runs/') else 'YOLOv5', project_name=opt.project if not str(opt.project).startswith("runs/") else "YOLOv5",
task_name=opt.name if opt.name != 'exp' else 'Training', task_name=opt.name if opt.name != "exp" else "Training",
tags=['YOLOv5'], tags=["YOLOv5"],
output_uri=True, output_uri=True,
reuse_last_task_id=opt.exist_ok, reuse_last_task_id=opt.exist_ok,
auto_connect_frameworks={ auto_connect_frameworks={"pytorch": False, "matplotlib": False},
'pytorch': False,
'matplotlib': False}
# We disconnect pytorch auto-detection, because we added manual model save points in the code # We disconnect pytorch auto-detection, because we added manual model save points in the code
) )
# ClearML's hooks will already grab all general parameters # ClearML's hooks will already grab all general parameters
# Only the hyperparameters coming from the yaml config file # Only the hyperparameters coming from the yaml config file
# will have to be added manually! # will have to be added manually!
self.task.connect(hyp, name='Hyperparameters') self.task.connect(hyp, name="Hyperparameters")
self.task.connect(opt, name='Args') self.task.connect(opt, name="Args")
# Make sure the code is easily remotely runnable by setting the docker image to use by the remote agent # Make sure the code is easily remotely runnable by setting the docker image to use by the remote agent
self.task.set_base_docker('ultralytics/yolov5:latest', self.task.set_base_docker(
docker_arguments='--ipc=host -e="CLEARML_AGENT_SKIP_PYTHON_ENV_INSTALL=1"', "ultralytics/yolov5:latest",
docker_setup_bash_script='pip install clearml') docker_arguments='--ipc=host -e="CLEARML_AGENT_SKIP_PYTHON_ENV_INSTALL=1"',
docker_setup_bash_script="pip install clearml",
)
# Get ClearML Dataset Version if requested # Get ClearML Dataset Version if requested
if opt.data.startswith('clearml://'): if opt.data.startswith("clearml://"):
# data_dict should have the following keys: # data_dict should have the following keys:
# names, nc (number of classes), test, train, val (all three relative paths to ../datasets) # names, nc (number of classes), test, train, val (all three relative paths to ../datasets)
self.data_dict = construct_dataset(opt.data) self.data_dict = construct_dataset(opt.data)
@@ -120,33 +128,32 @@ class ClearmlLogger:
def log_scalars(self, metrics, epoch): def log_scalars(self, metrics, epoch):
""" """
Log scalars/metrics to ClearML Log scalars/metrics to ClearML.
arguments: arguments:
metrics (dict) Metrics in dict format: {"metrics/mAP": 0.8, ...} metrics (dict) Metrics in dict format: {"metrics/mAP": 0.8, ...}
epoch (int) iteration number for the current set of metrics epoch (int) iteration number for the current set of metrics
""" """
for k, v in metrics.items(): for k, v in metrics.items():
title, series = k.split('/') title, series = k.split("/")
self.task.get_logger().report_scalar(title, series, v, epoch) self.task.get_logger().report_scalar(title, series, v, epoch)
def log_model(self, model_path, model_name, epoch=0): def log_model(self, model_path, model_name, epoch=0):
""" """
Log model weights to ClearML Log model weights to ClearML.
arguments: arguments:
model_path (PosixPath or str) Path to the model weights model_path (PosixPath or str) Path to the model weights
model_name (str) Name of the model visible in ClearML model_name (str) Name of the model visible in ClearML
epoch (int) Iteration / epoch of the model weights epoch (int) Iteration / epoch of the model weights
""" """
self.task.update_output_model(model_path=str(model_path), self.task.update_output_model(
name=model_name, model_path=str(model_path), name=model_name, iteration=epoch, auto_delete_file=False
iteration=epoch, )
auto_delete_file=False)
def log_summary(self, metrics): def log_summary(self, metrics):
""" """
Log final metrics to a summary table Log final metrics to a summary table.
arguments: arguments:
metrics (dict) Metrics in dict format: {"metrics/mAP": 0.8, ...} metrics (dict) Metrics in dict format: {"metrics/mAP": 0.8, ...}
@@ -156,7 +163,7 @@ class ClearmlLogger:
def log_plot(self, title, plot_path): def log_plot(self, title, plot_path):
""" """
Log image as plot in the plot section of ClearML Log image as plot in the plot section of ClearML.
arguments: arguments:
title (str) Title of the plot title (str) Title of the plot
@@ -164,12 +171,12 @@ class ClearmlLogger:
""" """
img = mpimg.imread(plot_path) img = mpimg.imread(plot_path)
fig = plt.figure() fig = plt.figure()
ax = fig.add_axes([0, 0, 1, 1], frameon=False, aspect='auto', xticks=[], yticks=[]) # no ticks ax = fig.add_axes([0, 0, 1, 1], frameon=False, aspect="auto", xticks=[], yticks=[]) # no ticks
ax.imshow(img) ax.imshow(img)
self.task.get_logger().report_matplotlib_figure(title, "", figure=fig, report_interactive=False) self.task.get_logger().report_matplotlib_figure(title, "", figure=fig, report_interactive=False)
def log_debug_samples(self, files, title='Debug Samples'): def log_debug_samples(self, files, title="Debug Samples"):
""" """
Log files (images) as debug samples in the ClearML task. Log files (images) as debug samples in the ClearML task.
@@ -179,12 +186,11 @@ class ClearmlLogger:
""" """
for f in files: for f in files:
if f.exists(): if f.exists():
it = re.search(r'_batch(\d+)', f.name) it = re.search(r"_batch(\d+)", f.name)
iteration = int(it.groups()[0]) if it else 0 iteration = int(it.groups()[0]) if it else 0
self.task.get_logger().report_image(title=title, self.task.get_logger().report_image(
series=f.name.replace(f"_batch{iteration}", ''), title=title, series=f.name.replace(f"_batch{iteration}", ""), local_path=str(f), iteration=iteration
local_path=str(f), )
iteration=iteration)
def log_image_with_boxes(self, image_path, boxes, class_names, image, conf_threshold=0.25): def log_image_with_boxes(self, image_path, boxes, class_names, image, conf_threshold=0.25):
""" """
@@ -206,15 +212,14 @@ class ClearmlLogger:
class_name = class_names[int(class_nr)] class_name = class_names[int(class_nr)]
confidence_percentage = round(float(conf) * 100, 2) confidence_percentage = round(float(conf) * 100, 2)
label = f'{class_name}: {confidence_percentage}%' label = f"{class_name}: {confidence_percentage}%"
if conf > conf_threshold: if conf > conf_threshold:
annotator.rectangle(box.cpu().numpy(), outline=color) annotator.rectangle(box.cpu().numpy(), outline=color)
annotator.box_label(box.cpu().numpy(), label=label, color=color) annotator.box_label(box.cpu().numpy(), label=label, color=color)
annotated_image = annotator.result() annotated_image = annotator.result()
self.task.get_logger().report_image(title='Bounding Boxes', self.task.get_logger().report_image(
series=image_path.name, title="Bounding Boxes", series=image_path.name, iteration=self.current_epoch, image=annotated_image
iteration=self.current_epoch, )
image=annotated_image)
self.current_epoch_logged_images.add(image_path) self.current_epoch_logged_images.add(image_path)
+41 -37
View File
@@ -1,18 +1,21 @@
from clearml import Task from clearml import Task
# Connecting ClearML with the current process, # Connecting ClearML with the current process,
# from here on everything is logged automatically # from here on everything is logged automatically
from clearml.automation import HyperParameterOptimizer, UniformParameterRange from clearml.automation import HyperParameterOptimizer, UniformParameterRange
from clearml.automation.optuna import OptimizerOptuna from clearml.automation.optuna import OptimizerOptuna
task = Task.init(project_name='Hyper-Parameter Optimization', task = Task.init(
task_name='YOLOv5', project_name="Hyper-Parameter Optimization",
task_type=Task.TaskTypes.optimizer, task_name="YOLOv5",
reuse_last_task_id=False) task_type=Task.TaskTypes.optimizer,
reuse_last_task_id=False,
)
# Example use case: # Example use case:
optimizer = HyperParameterOptimizer( optimizer = HyperParameterOptimizer(
# This is the experiment we want to optimize # This is the experiment we want to optimize
base_task_id='<your_template_task_id>', base_task_id="<your_template_task_id>",
# here we define the hyper-parameters to optimize # here we define the hyper-parameters to optimize
# Notice: The parameter name should exactly match what you see in the UI: <section_name>/<parameter> # Notice: The parameter name should exactly match what you see in the UI: <section_name>/<parameter>
# For Example, here we see in the base experiment a section Named: "General" # For Example, here we see in the base experiment a section Named: "General"
@@ -20,39 +23,40 @@ optimizer = HyperParameterOptimizer(
# If you have `argparse` for example, then arguments will appear under the "Args" section, # If you have `argparse` for example, then arguments will appear under the "Args" section,
# and you should instead pass "Args/batch_size" # and you should instead pass "Args/batch_size"
hyper_parameters=[ hyper_parameters=[
UniformParameterRange('Hyperparameters/lr0', min_value=1e-5, max_value=1e-1), UniformParameterRange("Hyperparameters/lr0", min_value=1e-5, max_value=1e-1),
UniformParameterRange('Hyperparameters/lrf', min_value=0.01, max_value=1.0), UniformParameterRange("Hyperparameters/lrf", min_value=0.01, max_value=1.0),
UniformParameterRange('Hyperparameters/momentum', min_value=0.6, max_value=0.98), UniformParameterRange("Hyperparameters/momentum", min_value=0.6, max_value=0.98),
UniformParameterRange('Hyperparameters/weight_decay', min_value=0.0, max_value=0.001), UniformParameterRange("Hyperparameters/weight_decay", min_value=0.0, max_value=0.001),
UniformParameterRange('Hyperparameters/warmup_epochs', min_value=0.0, max_value=5.0), UniformParameterRange("Hyperparameters/warmup_epochs", min_value=0.0, max_value=5.0),
UniformParameterRange('Hyperparameters/warmup_momentum', min_value=0.0, max_value=0.95), UniformParameterRange("Hyperparameters/warmup_momentum", min_value=0.0, max_value=0.95),
UniformParameterRange('Hyperparameters/warmup_bias_lr', min_value=0.0, max_value=0.2), UniformParameterRange("Hyperparameters/warmup_bias_lr", min_value=0.0, max_value=0.2),
UniformParameterRange('Hyperparameters/box', min_value=0.02, max_value=0.2), UniformParameterRange("Hyperparameters/box", min_value=0.02, max_value=0.2),
UniformParameterRange('Hyperparameters/cls', min_value=0.2, max_value=4.0), UniformParameterRange("Hyperparameters/cls", min_value=0.2, max_value=4.0),
UniformParameterRange('Hyperparameters/cls_pw', min_value=0.5, max_value=2.0), UniformParameterRange("Hyperparameters/cls_pw", min_value=0.5, max_value=2.0),
UniformParameterRange('Hyperparameters/obj', min_value=0.2, max_value=4.0), UniformParameterRange("Hyperparameters/obj", min_value=0.2, max_value=4.0),
UniformParameterRange('Hyperparameters/obj_pw', min_value=0.5, max_value=2.0), UniformParameterRange("Hyperparameters/obj_pw", min_value=0.5, max_value=2.0),
UniformParameterRange('Hyperparameters/iou_t', min_value=0.1, max_value=0.7), UniformParameterRange("Hyperparameters/iou_t", min_value=0.1, max_value=0.7),
UniformParameterRange('Hyperparameters/anchor_t', min_value=2.0, max_value=8.0), UniformParameterRange("Hyperparameters/anchor_t", min_value=2.0, max_value=8.0),
UniformParameterRange('Hyperparameters/fl_gamma', min_value=0.0, max_value=4.0), UniformParameterRange("Hyperparameters/fl_gamma", min_value=0.0, max_value=4.0),
UniformParameterRange('Hyperparameters/hsv_h', min_value=0.0, max_value=0.1), UniformParameterRange("Hyperparameters/hsv_h", min_value=0.0, max_value=0.1),
UniformParameterRange('Hyperparameters/hsv_s', min_value=0.0, max_value=0.9), UniformParameterRange("Hyperparameters/hsv_s", min_value=0.0, max_value=0.9),
UniformParameterRange('Hyperparameters/hsv_v', min_value=0.0, max_value=0.9), UniformParameterRange("Hyperparameters/hsv_v", min_value=0.0, max_value=0.9),
UniformParameterRange('Hyperparameters/degrees', min_value=0.0, max_value=45.0), UniformParameterRange("Hyperparameters/degrees", min_value=0.0, max_value=45.0),
UniformParameterRange('Hyperparameters/translate', min_value=0.0, max_value=0.9), UniformParameterRange("Hyperparameters/translate", min_value=0.0, max_value=0.9),
UniformParameterRange('Hyperparameters/scale', min_value=0.0, max_value=0.9), UniformParameterRange("Hyperparameters/scale", min_value=0.0, max_value=0.9),
UniformParameterRange('Hyperparameters/shear', min_value=0.0, max_value=10.0), UniformParameterRange("Hyperparameters/shear", min_value=0.0, max_value=10.0),
UniformParameterRange('Hyperparameters/perspective', min_value=0.0, max_value=0.001), UniformParameterRange("Hyperparameters/perspective", min_value=0.0, max_value=0.001),
UniformParameterRange('Hyperparameters/flipud', min_value=0.0, max_value=1.0), UniformParameterRange("Hyperparameters/flipud", min_value=0.0, max_value=1.0),
UniformParameterRange('Hyperparameters/fliplr', min_value=0.0, max_value=1.0), UniformParameterRange("Hyperparameters/fliplr", min_value=0.0, max_value=1.0),
UniformParameterRange('Hyperparameters/mosaic', min_value=0.0, max_value=1.0), UniformParameterRange("Hyperparameters/mosaic", min_value=0.0, max_value=1.0),
UniformParameterRange('Hyperparameters/mixup', min_value=0.0, max_value=1.0), UniformParameterRange("Hyperparameters/mixup", min_value=0.0, max_value=1.0),
UniformParameterRange('Hyperparameters/copy_paste', min_value=0.0, max_value=1.0)], UniformParameterRange("Hyperparameters/copy_paste", min_value=0.0, max_value=1.0),
],
# this is the objective metric we want to maximize/minimize # this is the objective metric we want to maximize/minimize
objective_metric_title='metrics', objective_metric_title="metrics",
objective_metric_series='mAP_0.5', objective_metric_series="mAP_0.5",
# now we decide if we want to maximize it or minimize it (accuracy we maximize) # now we decide if we want to maximize it or minimize it (accuracy we maximize)
objective_metric_sign='max', objective_metric_sign="max",
# let us limit the number of concurrent experiments, # let us limit the number of concurrent experiments,
# this in turn will make sure we do dont bombard the scheduler with experiments. # this in turn will make sure we do dont bombard the scheduler with experiments.
# if we have an auto-scaler connected, this, by proxy, will limit the number of machine # if we have an auto-scaler connected, this, by proxy, will limit the number of machine
@@ -81,4 +85,4 @@ optimizer.wait()
# make sure background optimization stopped # make sure background optimization stopped
optimizer.stop() optimizer.stop()
print('We are done, good bye') print("We are done, good bye")
+125 -118
View File
@@ -17,7 +17,7 @@ try:
# Project Configuration # Project Configuration
config = comet_ml.config.get_config() config = comet_ml.config.get_config()
COMET_PROJECT_NAME = config.get_string(os.getenv('COMET_PROJECT_NAME'), 'comet.project_name', default='yolov5') COMET_PROJECT_NAME = config.get_string(os.getenv("COMET_PROJECT_NAME"), "comet.project_name", default="yolov5")
except ImportError: except ImportError:
comet_ml = None comet_ml = None
COMET_PROJECT_NAME = None COMET_PROJECT_NAME = None
@@ -31,42 +31,40 @@ from utils.dataloaders import img2label_paths
from utils.general import check_dataset, scale_boxes, xywh2xyxy from utils.general import check_dataset, scale_boxes, xywh2xyxy
from utils.metrics import box_iou from utils.metrics import box_iou
COMET_PREFIX = 'comet://' COMET_PREFIX = "comet://"
COMET_MODE = os.getenv('COMET_MODE', 'online') COMET_MODE = os.getenv("COMET_MODE", "online")
# Model Saving Settings # Model Saving Settings
COMET_MODEL_NAME = os.getenv('COMET_MODEL_NAME', 'yolov5') COMET_MODEL_NAME = os.getenv("COMET_MODEL_NAME", "yolov5")
# Dataset Artifact Settings # Dataset Artifact Settings
COMET_UPLOAD_DATASET = os.getenv('COMET_UPLOAD_DATASET', 'false').lower() == 'true' COMET_UPLOAD_DATASET = os.getenv("COMET_UPLOAD_DATASET", "false").lower() == "true"
# Evaluation Settings # Evaluation Settings
COMET_LOG_CONFUSION_MATRIX = (os.getenv('COMET_LOG_CONFUSION_MATRIX', 'true').lower() == 'true') COMET_LOG_CONFUSION_MATRIX = os.getenv("COMET_LOG_CONFUSION_MATRIX", "true").lower() == "true"
COMET_LOG_PREDICTIONS = os.getenv('COMET_LOG_PREDICTIONS', 'true').lower() == 'true' COMET_LOG_PREDICTIONS = os.getenv("COMET_LOG_PREDICTIONS", "true").lower() == "true"
COMET_MAX_IMAGE_UPLOADS = int(os.getenv('COMET_MAX_IMAGE_UPLOADS', 100)) COMET_MAX_IMAGE_UPLOADS = int(os.getenv("COMET_MAX_IMAGE_UPLOADS", 100))
# Confusion Matrix Settings # Confusion Matrix Settings
CONF_THRES = float(os.getenv('CONF_THRES', 0.001)) CONF_THRES = float(os.getenv("CONF_THRES", 0.001))
IOU_THRES = float(os.getenv('IOU_THRES', 0.6)) IOU_THRES = float(os.getenv("IOU_THRES", 0.6))
# Batch Logging Settings # Batch Logging Settings
COMET_LOG_BATCH_METRICS = (os.getenv('COMET_LOG_BATCH_METRICS', 'false').lower() == 'true') COMET_LOG_BATCH_METRICS = os.getenv("COMET_LOG_BATCH_METRICS", "false").lower() == "true"
COMET_BATCH_LOGGING_INTERVAL = os.getenv('COMET_BATCH_LOGGING_INTERVAL', 1) COMET_BATCH_LOGGING_INTERVAL = os.getenv("COMET_BATCH_LOGGING_INTERVAL", 1)
COMET_PREDICTION_LOGGING_INTERVAL = os.getenv('COMET_PREDICTION_LOGGING_INTERVAL', 1) COMET_PREDICTION_LOGGING_INTERVAL = os.getenv("COMET_PREDICTION_LOGGING_INTERVAL", 1)
COMET_LOG_PER_CLASS_METRICS = (os.getenv('COMET_LOG_PER_CLASS_METRICS', 'false').lower() == 'true') COMET_LOG_PER_CLASS_METRICS = os.getenv("COMET_LOG_PER_CLASS_METRICS", "false").lower() == "true"
RANK = int(os.getenv('RANK', -1)) RANK = int(os.getenv("RANK", -1))
to_pil = T.ToPILImage() to_pil = T.ToPILImage()
class CometLogger: class CometLogger:
"""Log metrics, parameters, source code, models and much more """Log metrics, parameters, source code, models and much more with Comet."""
with Comet
"""
def __init__(self, opt, hyp, run_id=None, job_type='Training', **experiment_kwargs) -> None: def __init__(self, opt, hyp, run_id=None, job_type="Training", **experiment_kwargs) -> None:
self.job_type = job_type self.job_type = job_type
self.opt = opt self.opt = opt
self.hyp = hyp self.hyp = hyp
@@ -87,57 +85,58 @@ class CometLogger:
# Default parameters to pass to Experiment objects # Default parameters to pass to Experiment objects
self.default_experiment_kwargs = { self.default_experiment_kwargs = {
'log_code': False, "log_code": False,
'log_env_gpu': True, "log_env_gpu": True,
'log_env_cpu': True, "log_env_cpu": True,
'project_name': COMET_PROJECT_NAME, } "project_name": COMET_PROJECT_NAME,
}
self.default_experiment_kwargs.update(experiment_kwargs) self.default_experiment_kwargs.update(experiment_kwargs)
self.experiment = self._get_experiment(self.comet_mode, run_id) self.experiment = self._get_experiment(self.comet_mode, run_id)
self.experiment.set_name(self.opt.name) self.experiment.set_name(self.opt.name)
self.data_dict = self.check_dataset(self.opt.data) self.data_dict = self.check_dataset(self.opt.data)
self.class_names = self.data_dict['names'] self.class_names = self.data_dict["names"]
self.num_classes = self.data_dict['nc'] self.num_classes = self.data_dict["nc"]
self.logged_images_count = 0 self.logged_images_count = 0
self.max_images = COMET_MAX_IMAGE_UPLOADS self.max_images = COMET_MAX_IMAGE_UPLOADS
if run_id is None: if run_id is None:
self.experiment.log_other('Created from', 'YOLOv5') self.experiment.log_other("Created from", "YOLOv5")
if not isinstance(self.experiment, comet_ml.OfflineExperiment): if not isinstance(self.experiment, comet_ml.OfflineExperiment):
workspace, project_name, experiment_id = self.experiment.url.split('/')[-3:] workspace, project_name, experiment_id = self.experiment.url.split("/")[-3:]
self.experiment.log_other( self.experiment.log_other(
'Run Path', "Run Path",
f'{workspace}/{project_name}/{experiment_id}', f"{workspace}/{project_name}/{experiment_id}",
) )
self.log_parameters(vars(opt)) self.log_parameters(vars(opt))
self.log_parameters(self.opt.hyp) self.log_parameters(self.opt.hyp)
self.log_asset_data( self.log_asset_data(
self.opt.hyp, self.opt.hyp,
name='hyperparameters.json', name="hyperparameters.json",
metadata={'type': 'hyp-config-file'}, metadata={"type": "hyp-config-file"},
) )
self.log_asset( self.log_asset(
f'{self.opt.save_dir}/opt.yaml', f"{self.opt.save_dir}/opt.yaml",
metadata={'type': 'opt-config-file'}, metadata={"type": "opt-config-file"},
) )
self.comet_log_confusion_matrix = COMET_LOG_CONFUSION_MATRIX self.comet_log_confusion_matrix = COMET_LOG_CONFUSION_MATRIX
if hasattr(self.opt, 'conf_thres'): if hasattr(self.opt, "conf_thres"):
self.conf_thres = self.opt.conf_thres self.conf_thres = self.opt.conf_thres
else: else:
self.conf_thres = CONF_THRES self.conf_thres = CONF_THRES
if hasattr(self.opt, 'iou_thres'): if hasattr(self.opt, "iou_thres"):
self.iou_thres = self.opt.iou_thres self.iou_thres = self.opt.iou_thres
else: else:
self.iou_thres = IOU_THRES self.iou_thres = IOU_THRES
self.log_parameters({'val_iou_threshold': self.iou_thres, 'val_conf_threshold': self.conf_thres}) self.log_parameters({"val_iou_threshold": self.iou_thres, "val_conf_threshold": self.conf_thres})
self.comet_log_predictions = COMET_LOG_PREDICTIONS self.comet_log_predictions = COMET_LOG_PREDICTIONS
if self.opt.bbox_interval == -1: if self.opt.bbox_interval == -1:
self.comet_log_prediction_interval = (1 if self.opt.epochs < 10 else self.opt.epochs // 10) self.comet_log_prediction_interval = 1 if self.opt.epochs < 10 else self.opt.epochs // 10
else: else:
self.comet_log_prediction_interval = self.opt.bbox_interval self.comet_log_prediction_interval = self.opt.bbox_interval
@@ -147,30 +146,35 @@ class CometLogger:
self.comet_log_per_class_metrics = COMET_LOG_PER_CLASS_METRICS self.comet_log_per_class_metrics = COMET_LOG_PER_CLASS_METRICS
self.experiment.log_others({ self.experiment.log_others(
'comet_mode': COMET_MODE, {
'comet_max_image_uploads': COMET_MAX_IMAGE_UPLOADS, "comet_mode": COMET_MODE,
'comet_log_per_class_metrics': COMET_LOG_PER_CLASS_METRICS, "comet_max_image_uploads": COMET_MAX_IMAGE_UPLOADS,
'comet_log_batch_metrics': COMET_LOG_BATCH_METRICS, "comet_log_per_class_metrics": COMET_LOG_PER_CLASS_METRICS,
'comet_log_confusion_matrix': COMET_LOG_CONFUSION_MATRIX, "comet_log_batch_metrics": COMET_LOG_BATCH_METRICS,
'comet_model_name': COMET_MODEL_NAME, }) "comet_log_confusion_matrix": COMET_LOG_CONFUSION_MATRIX,
"comet_model_name": COMET_MODEL_NAME,
}
)
# Check if running the Experiment with the Comet Optimizer # Check if running the Experiment with the Comet Optimizer
if hasattr(self.opt, 'comet_optimizer_id'): if hasattr(self.opt, "comet_optimizer_id"):
self.experiment.log_other('optimizer_id', self.opt.comet_optimizer_id) self.experiment.log_other("optimizer_id", self.opt.comet_optimizer_id)
self.experiment.log_other('optimizer_objective', self.opt.comet_optimizer_objective) self.experiment.log_other("optimizer_objective", self.opt.comet_optimizer_objective)
self.experiment.log_other('optimizer_metric', self.opt.comet_optimizer_metric) self.experiment.log_other("optimizer_metric", self.opt.comet_optimizer_metric)
self.experiment.log_other('optimizer_parameters', json.dumps(self.hyp)) self.experiment.log_other("optimizer_parameters", json.dumps(self.hyp))
def _get_experiment(self, mode, experiment_id=None): def _get_experiment(self, mode, experiment_id=None):
if mode == 'offline': if mode == "offline":
if experiment_id is not None: if experiment_id is not None:
return comet_ml.ExistingOfflineExperiment( return comet_ml.ExistingOfflineExperiment(
previous_experiment=experiment_id, previous_experiment=experiment_id,
**self.default_experiment_kwargs, **self.default_experiment_kwargs,
) )
return comet_ml.OfflineExperiment(**self.default_experiment_kwargs, ) return comet_ml.OfflineExperiment(
**self.default_experiment_kwargs,
)
else: else:
try: try:
@@ -183,11 +187,13 @@ class CometLogger:
return comet_ml.Experiment(**self.default_experiment_kwargs) return comet_ml.Experiment(**self.default_experiment_kwargs)
except ValueError: except ValueError:
logger.warning('COMET WARNING: ' logger.warning(
'Comet credentials have not been set. ' "COMET WARNING: "
'Comet will default to offline logging. ' "Comet credentials have not been set. "
'Please set your credentials to enable online logging.') "Comet will default to offline logging. "
return self._get_experiment('offline', experiment_id) "Please set your credentials to enable online logging."
)
return self._get_experiment("offline", experiment_id)
return return
@@ -211,12 +217,13 @@ class CometLogger:
return return
model_metadata = { model_metadata = {
'fitness_score': fitness_score[-1], "fitness_score": fitness_score[-1],
'epochs_trained': epoch + 1, "epochs_trained": epoch + 1,
'save_period': opt.save_period, "save_period": opt.save_period,
'total_epochs': opt.epochs, } "total_epochs": opt.epochs,
}
model_files = glob.glob(f'{path}/*.pt') model_files = glob.glob(f"{path}/*.pt")
for model_path in model_files: for model_path in model_files:
name = Path(model_path).name name = Path(model_path).name
@@ -232,14 +239,14 @@ class CometLogger:
with open(data_file) as f: with open(data_file) as f:
data_config = yaml.safe_load(f) data_config = yaml.safe_load(f)
path = data_config.get('path') path = data_config.get("path")
if path and path.startswith(COMET_PREFIX): if path and path.startswith(COMET_PREFIX):
path = data_config['path'].replace(COMET_PREFIX, '') path = data_config["path"].replace(COMET_PREFIX, "")
data_dict = self.download_dataset_artifact(path) data_dict = self.download_dataset_artifact(path)
return data_dict return data_dict
self.log_asset(self.opt.data, metadata={'type': 'data-config-file'}) self.log_asset(self.opt.data, metadata={"type": "data-config-file"})
return check_dataset(data_file) return check_dataset(data_file)
@@ -255,8 +262,8 @@ class CometLogger:
filtered_detections = detections[mask] filtered_detections = detections[mask]
filtered_labels = labelsn[mask] filtered_labels = labelsn[mask]
image_id = path.split('/')[-1].split('.')[0] image_id = path.split("/")[-1].split(".")[0]
image_name = f'{image_id}_curr_epoch_{self.experiment.curr_epoch}' image_name = f"{image_id}_curr_epoch_{self.experiment.curr_epoch}"
if image_name not in self.logged_image_names: if image_name not in self.logged_image_names:
native_scale_image = PIL.Image.open(path) native_scale_image = PIL.Image.open(path)
self.log_image(native_scale_image, name=image_name) self.log_image(native_scale_image, name=image_name)
@@ -264,23 +271,21 @@ class CometLogger:
metadata = [] metadata = []
for cls, *xyxy in filtered_labels.tolist(): for cls, *xyxy in filtered_labels.tolist():
metadata.append({ metadata.append(
'label': f'{self.class_names[int(cls)]}-gt', {
'score': 100, "label": f"{self.class_names[int(cls)]}-gt",
'box': { "score": 100,
'x': xyxy[0], "box": {"x": xyxy[0], "y": xyxy[1], "x2": xyxy[2], "y2": xyxy[3]},
'y': xyxy[1], }
'x2': xyxy[2], )
'y2': xyxy[3]}, })
for *xyxy, conf, cls in filtered_detections.tolist(): for *xyxy, conf, cls in filtered_detections.tolist():
metadata.append({ metadata.append(
'label': f'{self.class_names[int(cls)]}', {
'score': conf * 100, "label": f"{self.class_names[int(cls)]}",
'box': { "score": conf * 100,
'x': xyxy[0], "box": {"x": xyxy[0], "y": xyxy[1], "x2": xyxy[2], "y2": xyxy[3]},
'y': xyxy[1], }
'x2': xyxy[2], )
'y2': xyxy[3]}, })
self.metadata_dict[image_name] = metadata self.metadata_dict[image_name] = metadata
self.logged_images_count += 1 self.logged_images_count += 1
@@ -307,7 +312,7 @@ class CometLogger:
return predn, labelsn return predn, labelsn
def add_assets_to_artifact(self, artifact, path, asset_path, split): def add_assets_to_artifact(self, artifact, path, asset_path, split):
img_paths = sorted(glob.glob(f'{asset_path}/*')) img_paths = sorted(glob.glob(f"{asset_path}/*"))
label_paths = img2label_paths(img_paths) label_paths = img2label_paths(img_paths)
for image_file, label_file in zip(img_paths, label_paths): for image_file, label_file in zip(img_paths, label_paths):
@@ -317,33 +322,33 @@ class CometLogger:
artifact.add( artifact.add(
image_file, image_file,
logical_path=image_logical_path, logical_path=image_logical_path,
metadata={'split': split}, metadata={"split": split},
) )
artifact.add( artifact.add(
label_file, label_file,
logical_path=label_logical_path, logical_path=label_logical_path,
metadata={'split': split}, metadata={"split": split},
) )
except ValueError as e: except ValueError as e:
logger.error('COMET ERROR: Error adding file to Artifact. Skipping file.') logger.error("COMET ERROR: Error adding file to Artifact. Skipping file.")
logger.error(f'COMET ERROR: {e}') logger.error(f"COMET ERROR: {e}")
continue continue
return artifact return artifact
def upload_dataset_artifact(self): def upload_dataset_artifact(self):
dataset_name = self.data_dict.get('dataset_name', 'yolov5-dataset') dataset_name = self.data_dict.get("dataset_name", "yolov5-dataset")
path = str((ROOT / Path(self.data_dict['path'])).resolve()) path = str((ROOT / Path(self.data_dict["path"])).resolve())
metadata = self.data_dict.copy() metadata = self.data_dict.copy()
for key in ['train', 'val', 'test']: for key in ["train", "val", "test"]:
split_path = metadata.get(key) split_path = metadata.get(key)
if split_path is not None: if split_path is not None:
metadata[key] = split_path.replace(path, '') metadata[key] = split_path.replace(path, "")
artifact = comet_ml.Artifact(name=dataset_name, artifact_type='dataset', metadata=metadata) artifact = comet_ml.Artifact(name=dataset_name, artifact_type="dataset", metadata=metadata)
for key in metadata.keys(): for key in metadata.keys():
if key in ['train', 'val', 'test']: if key in ["train", "val", "test"]:
if isinstance(self.upload_dataset, str) and (key != self.upload_dataset): if isinstance(self.upload_dataset, str) and (key != self.upload_dataset):
continue continue
@@ -362,26 +367,27 @@ class CometLogger:
metadata = logged_artifact.metadata metadata = logged_artifact.metadata
data_dict = metadata.copy() data_dict = metadata.copy()
data_dict['path'] = artifact_save_dir data_dict["path"] = artifact_save_dir
metadata_names = metadata.get('names') metadata_names = metadata.get("names")
if isinstance(metadata_names, dict): if isinstance(metadata_names, dict):
data_dict['names'] = {int(k): v for k, v in metadata.get('names').items()} data_dict["names"] = {int(k): v for k, v in metadata.get("names").items()}
elif isinstance(metadata_names, list): elif isinstance(metadata_names, list):
data_dict['names'] = {int(k): v for k, v in zip(range(len(metadata_names)), metadata_names)} data_dict["names"] = {int(k): v for k, v in zip(range(len(metadata_names)), metadata_names)}
else: else:
raise "Invalid 'names' field in dataset yaml file. Please use a list or dictionary" raise "Invalid 'names' field in dataset yaml file. Please use a list or dictionary"
return self.update_data_paths(data_dict) return self.update_data_paths(data_dict)
def update_data_paths(self, data_dict): def update_data_paths(self, data_dict):
path = data_dict.get('path', '') path = data_dict.get("path", "")
for split in ['train', 'val', 'test']: for split in ["train", "val", "test"]:
if data_dict.get(split): if data_dict.get(split):
split_path = data_dict.get(split) split_path = data_dict.get(split)
data_dict[split] = (f'{path}/{split_path}' if isinstance(split, str) else [ data_dict[split] = (
f'{path}/{x}' for x in split_path]) f"{path}/{split_path}" if isinstance(split, str) else [f"{path}/{x}" for x in split_path]
)
return data_dict return data_dict
@@ -422,11 +428,11 @@ class CometLogger:
def on_train_end(self, files, save_dir, last, best, epoch, results): def on_train_end(self, files, save_dir, last, best, epoch, results):
if self.comet_log_predictions: if self.comet_log_predictions:
curr_epoch = self.experiment.curr_epoch curr_epoch = self.experiment.curr_epoch
self.experiment.log_asset_data(self.metadata_dict, 'image-metadata.json', epoch=curr_epoch) self.experiment.log_asset_data(self.metadata_dict, "image-metadata.json", epoch=curr_epoch)
for f in files: for f in files:
self.log_asset(f, metadata={'epoch': epoch}) self.log_asset(f, metadata={"epoch": epoch})
self.log_asset(f'{save_dir}/results.csv', metadata={'epoch': epoch}) self.log_asset(f"{save_dir}/results.csv", metadata={"epoch": epoch})
if not self.opt.evolve: if not self.opt.evolve:
model_path = str(best if best.exists() else last) model_path = str(best if best.exists() else last)
@@ -440,9 +446,9 @@ class CometLogger:
) )
# Check if running Experiment with Comet Optimizer # Check if running Experiment with Comet Optimizer
if hasattr(self.opt, 'comet_optimizer_id'): if hasattr(self.opt, "comet_optimizer_id"):
metric = results.get(self.opt.comet_optimizer_metric) metric = results.get(self.opt.comet_optimizer_metric)
self.experiment.log_other('optimizer_metric_value', metric) self.experiment.log_other("optimizer_metric_value", metric)
self.finish_run() self.finish_run()
@@ -477,21 +483,22 @@ class CometLogger:
class_name = self.class_names[c] class_name = self.class_names[c]
self.experiment.log_metrics( self.experiment.log_metrics(
{ {
'mAP@.5': ap50[i], "mAP@.5": ap50[i],
'mAP@.5:.95': ap[i], "mAP@.5:.95": ap[i],
'precision': p[i], "precision": p[i],
'recall': r[i], "recall": r[i],
'f1': f1[i], "f1": f1[i],
'true_positives': tp[i], "true_positives": tp[i],
'false_positives': fp[i], "false_positives": fp[i],
'support': nt[c], }, "support": nt[c],
},
prefix=class_name, prefix=class_name,
) )
if self.comet_log_confusion_matrix: if self.comet_log_confusion_matrix:
epoch = self.experiment.curr_epoch epoch = self.experiment.curr_epoch
class_names = list(self.class_names.values()) class_names = list(self.class_names.values())
class_names.append('background') class_names.append("background")
num_classes = len(class_names) num_classes = len(class_names)
self.experiment.log_confusion_matrix( self.experiment.log_confusion_matrix(
@@ -499,9 +506,9 @@ class CometLogger:
max_categories=num_classes, max_categories=num_classes,
labels=class_names, labels=class_names,
epoch=epoch, epoch=epoch,
column_label='Actual Category', column_label="Actual Category",
row_label='Predicted Category', row_label="Predicted Category",
file_name=f'confusion-matrix-epoch-{epoch}.json', file_name=f"confusion-matrix-epoch-{epoch}.json",
) )
def on_fit_epoch_end(self, result, epoch): def on_fit_epoch_end(self, result, epoch):
+27 -27
View File
@@ -11,28 +11,28 @@ import yaml
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
COMET_PREFIX = 'comet://' COMET_PREFIX = "comet://"
COMET_MODEL_NAME = os.getenv('COMET_MODEL_NAME', 'yolov5') COMET_MODEL_NAME = os.getenv("COMET_MODEL_NAME", "yolov5")
COMET_DEFAULT_CHECKPOINT_FILENAME = os.getenv('COMET_DEFAULT_CHECKPOINT_FILENAME', 'last.pt') COMET_DEFAULT_CHECKPOINT_FILENAME = os.getenv("COMET_DEFAULT_CHECKPOINT_FILENAME", "last.pt")
def download_model_checkpoint(opt, experiment): def download_model_checkpoint(opt, experiment):
model_dir = f'{opt.project}/{experiment.name}' model_dir = f"{opt.project}/{experiment.name}"
os.makedirs(model_dir, exist_ok=True) os.makedirs(model_dir, exist_ok=True)
model_name = COMET_MODEL_NAME model_name = COMET_MODEL_NAME
model_asset_list = experiment.get_model_asset_list(model_name) model_asset_list = experiment.get_model_asset_list(model_name)
if len(model_asset_list) == 0: if len(model_asset_list) == 0:
logger.error(f'COMET ERROR: No checkpoints found for model name : {model_name}') logger.error(f"COMET ERROR: No checkpoints found for model name : {model_name}")
return return
model_asset_list = sorted( model_asset_list = sorted(
model_asset_list, model_asset_list,
key=lambda x: x['step'], key=lambda x: x["step"],
reverse=True, reverse=True,
) )
logged_checkpoint_map = {asset['fileName']: asset['assetId'] for asset in model_asset_list} logged_checkpoint_map = {asset["fileName"]: asset["assetId"] for asset in model_asset_list}
resource_url = urlparse(opt.weights) resource_url = urlparse(opt.weights)
checkpoint_filename = resource_url.query checkpoint_filename = resource_url.query
@@ -44,28 +44,28 @@ def download_model_checkpoint(opt, experiment):
checkpoint_filename = COMET_DEFAULT_CHECKPOINT_FILENAME checkpoint_filename = COMET_DEFAULT_CHECKPOINT_FILENAME
if asset_id is None: if asset_id is None:
logger.error(f'COMET ERROR: Checkpoint {checkpoint_filename} not found in the given Experiment') logger.error(f"COMET ERROR: Checkpoint {checkpoint_filename} not found in the given Experiment")
return return
try: try:
logger.info(f'COMET INFO: Downloading checkpoint {checkpoint_filename}') logger.info(f"COMET INFO: Downloading checkpoint {checkpoint_filename}")
asset_filename = checkpoint_filename asset_filename = checkpoint_filename
model_binary = experiment.get_asset(asset_id, return_type='binary', stream=False) model_binary = experiment.get_asset(asset_id, return_type="binary", stream=False)
model_download_path = f'{model_dir}/{asset_filename}' model_download_path = f"{model_dir}/{asset_filename}"
with open(model_download_path, 'wb') as f: with open(model_download_path, "wb") as f:
f.write(model_binary) f.write(model_binary)
opt.weights = model_download_path opt.weights = model_download_path
except Exception as e: except Exception as e:
logger.warning('COMET WARNING: Unable to download checkpoint from Comet') logger.warning("COMET WARNING: Unable to download checkpoint from Comet")
logger.exception(e) logger.exception(e)
def set_opt_parameters(opt, experiment): def set_opt_parameters(opt, experiment):
"""Update the opts Namespace with parameters """
from Comet's ExistingExperiment when resuming a run Update the opts Namespace with parameters from Comet's ExistingExperiment when resuming a run.
Args: Args:
opt (argparse.Namespace): Namespace of command line options opt (argparse.Namespace): Namespace of command line options
@@ -75,9 +75,9 @@ def set_opt_parameters(opt, experiment):
resume_string = opt.resume resume_string = opt.resume
for asset in asset_list: for asset in asset_list:
if asset['fileName'] == 'opt.yaml': if asset["fileName"] == "opt.yaml":
asset_id = asset['assetId'] asset_id = asset["assetId"]
asset_binary = experiment.get_asset(asset_id, return_type='binary', stream=False) asset_binary = experiment.get_asset(asset_id, return_type="binary", stream=False)
opt_dict = yaml.safe_load(asset_binary) opt_dict = yaml.safe_load(asset_binary)
for key, value in opt_dict.items(): for key, value in opt_dict.items():
setattr(opt, key, value) setattr(opt, key, value)
@@ -85,18 +85,18 @@ def set_opt_parameters(opt, experiment):
# Save hyperparameters to YAML file # Save hyperparameters to YAML file
# Necessary to pass checks in training script # Necessary to pass checks in training script
save_dir = f'{opt.project}/{experiment.name}' save_dir = f"{opt.project}/{experiment.name}"
os.makedirs(save_dir, exist_ok=True) os.makedirs(save_dir, exist_ok=True)
hyp_yaml_path = f'{save_dir}/hyp.yaml' hyp_yaml_path = f"{save_dir}/hyp.yaml"
with open(hyp_yaml_path, 'w') as f: with open(hyp_yaml_path, "w") as f:
yaml.dump(opt.hyp, f) yaml.dump(opt.hyp, f)
opt.hyp = hyp_yaml_path opt.hyp = hyp_yaml_path
def check_comet_weights(opt): def check_comet_weights(opt):
"""Downloads model weights from Comet and updates the """
weights path to point to saved weights location Downloads model weights from Comet and updates the weights path to point to saved weights location.
Args: Args:
opt (argparse.Namespace): Command Line arguments passed opt (argparse.Namespace): Command Line arguments passed
@@ -113,7 +113,7 @@ def check_comet_weights(opt):
if opt.weights.startswith(COMET_PREFIX): if opt.weights.startswith(COMET_PREFIX):
api = comet_ml.API() api = comet_ml.API()
resource = urlparse(opt.weights) resource = urlparse(opt.weights)
experiment_path = f'{resource.netloc}{resource.path}' experiment_path = f"{resource.netloc}{resource.path}"
experiment = api.get(experiment_path) experiment = api.get(experiment_path)
download_model_checkpoint(opt, experiment) download_model_checkpoint(opt, experiment)
return True return True
@@ -122,8 +122,8 @@ def check_comet_weights(opt):
def check_comet_resume(opt): def check_comet_resume(opt):
"""Restores run parameters to its original state based on the model checkpoint """
and logged Experiment parameters. Restores run parameters to its original state based on the model checkpoint and logged Experiment parameters.
Args: Args:
opt (argparse.Namespace): Command Line arguments passed opt (argparse.Namespace): Command Line arguments passed
@@ -140,7 +140,7 @@ def check_comet_resume(opt):
if opt.resume.startswith(COMET_PREFIX): if opt.resume.startswith(COMET_PREFIX):
api = comet_ml.API() api = comet_ml.API()
resource = urlparse(opt.resume) resource = urlparse(opt.resume)
experiment_path = f'{resource.netloc}{resource.path}' experiment_path = f"{resource.netloc}{resource.path}"
experiment = api.get(experiment_path) experiment = api.get(experiment_path)
set_opt_parameters(opt, experiment) set_opt_parameters(opt, experiment)
download_model_checkpoint(opt, experiment) download_model_checkpoint(opt, experiment)
+58 -56
View File
@@ -21,77 +21,79 @@ from utils.torch_utils import select_device
# Project Configuration # Project Configuration
config = comet_ml.config.get_config() config = comet_ml.config.get_config()
COMET_PROJECT_NAME = config.get_string(os.getenv('COMET_PROJECT_NAME'), 'comet.project_name', default='yolov5') COMET_PROJECT_NAME = config.get_string(os.getenv("COMET_PROJECT_NAME"), "comet.project_name", default="yolov5")
def get_args(known=False): def get_args(known=False):
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('--weights', type=str, default=ROOT / 'yolov5s.pt', help='initial weights path') parser.add_argument("--weights", type=str, default=ROOT / "yolov5s.pt", help="initial weights path")
parser.add_argument('--cfg', type=str, default='', help='model.yaml path') parser.add_argument("--cfg", type=str, default="", help="model.yaml path")
parser.add_argument('--data', type=str, default=ROOT / 'data/coco128.yaml', help='dataset.yaml path') parser.add_argument("--data", type=str, default=ROOT / "data/coco128.yaml", help="dataset.yaml path")
parser.add_argument('--hyp', type=str, default=ROOT / 'data/hyps/hyp.scratch-low.yaml', help='hyperparameters path') parser.add_argument("--hyp", type=str, default=ROOT / "data/hyps/hyp.scratch-low.yaml", help="hyperparameters path")
parser.add_argument('--epochs', type=int, default=300, help='total training epochs') parser.add_argument("--epochs", type=int, default=300, help="total training epochs")
parser.add_argument('--batch-size', type=int, default=16, help='total batch size for all GPUs, -1 for autobatch') parser.add_argument("--batch-size", type=int, default=16, help="total batch size for all GPUs, -1 for autobatch")
parser.add_argument('--imgsz', '--img', '--img-size', type=int, default=640, help='train, val image size (pixels)') parser.add_argument("--imgsz", "--img", "--img-size", type=int, default=640, help="train, val image size (pixels)")
parser.add_argument('--rect', action='store_true', help='rectangular training') parser.add_argument("--rect", action="store_true", help="rectangular training")
parser.add_argument('--resume', nargs='?', const=True, default=False, help='resume most recent training') parser.add_argument("--resume", nargs="?", const=True, default=False, help="resume most recent training")
parser.add_argument('--nosave', action='store_true', help='only save final checkpoint') parser.add_argument("--nosave", action="store_true", help="only save final checkpoint")
parser.add_argument('--noval', action='store_true', help='only validate final epoch') parser.add_argument("--noval", action="store_true", help="only validate final epoch")
parser.add_argument('--noautoanchor', action='store_true', help='disable AutoAnchor') parser.add_argument("--noautoanchor", action="store_true", help="disable AutoAnchor")
parser.add_argument('--noplots', action='store_true', help='save no plot files') parser.add_argument("--noplots", action="store_true", help="save no plot files")
parser.add_argument('--evolve', type=int, nargs='?', const=300, help='evolve hyperparameters for x generations') parser.add_argument("--evolve", type=int, nargs="?", const=300, help="evolve hyperparameters for x generations")
parser.add_argument('--bucket', type=str, default='', help='gsutil bucket') parser.add_argument("--bucket", type=str, default="", help="gsutil bucket")
parser.add_argument('--cache', type=str, nargs='?', const='ram', help='--cache images in "ram" (default) or "disk"') parser.add_argument("--cache", type=str, nargs="?", const="ram", help='--cache images in "ram" (default) or "disk"')
parser.add_argument('--image-weights', action='store_true', help='use weighted image selection for training') parser.add_argument("--image-weights", action="store_true", help="use weighted image selection for training")
parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu') parser.add_argument("--device", default="", help="cuda device, i.e. 0 or 0,1,2,3 or cpu")
parser.add_argument('--multi-scale', action='store_true', help='vary img-size +/- 50%%') parser.add_argument("--multi-scale", action="store_true", help="vary img-size +/- 50%%")
parser.add_argument('--single-cls', action='store_true', help='train multi-class data as single-class') parser.add_argument("--single-cls", action="store_true", help="train multi-class data as single-class")
parser.add_argument('--optimizer', type=str, choices=['SGD', 'Adam', 'AdamW'], default='SGD', help='optimizer') parser.add_argument("--optimizer", type=str, choices=["SGD", "Adam", "AdamW"], default="SGD", help="optimizer")
parser.add_argument('--sync-bn', action='store_true', help='use SyncBatchNorm, only available in DDP mode') parser.add_argument("--sync-bn", action="store_true", help="use SyncBatchNorm, only available in DDP mode")
parser.add_argument('--workers', type=int, default=8, help='max dataloader workers (per RANK in DDP mode)') parser.add_argument("--workers", type=int, default=8, help="max dataloader workers (per RANK in DDP mode)")
parser.add_argument('--project', default=ROOT / 'runs/train', help='save to project/name') parser.add_argument("--project", default=ROOT / "runs/train", help="save to project/name")
parser.add_argument('--name', default='exp', help='save to project/name') parser.add_argument("--name", default="exp", help="save to project/name")
parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment') parser.add_argument("--exist-ok", action="store_true", help="existing project/name ok, do not increment")
parser.add_argument('--quad', action='store_true', help='quad dataloader') parser.add_argument("--quad", action="store_true", help="quad dataloader")
parser.add_argument('--cos-lr', action='store_true', help='cosine LR scheduler') parser.add_argument("--cos-lr", action="store_true", help="cosine LR scheduler")
parser.add_argument('--label-smoothing', type=float, default=0.0, help='Label smoothing epsilon') parser.add_argument("--label-smoothing", type=float, default=0.0, help="Label smoothing epsilon")
parser.add_argument('--patience', type=int, default=100, help='EarlyStopping patience (epochs without improvement)') parser.add_argument("--patience", type=int, default=100, help="EarlyStopping patience (epochs without improvement)")
parser.add_argument('--freeze', nargs='+', type=int, default=[0], help='Freeze layers: backbone=10, first3=0 1 2') parser.add_argument("--freeze", nargs="+", type=int, default=[0], help="Freeze layers: backbone=10, first3=0 1 2")
parser.add_argument('--save-period', type=int, default=-1, help='Save checkpoint every x epochs (disabled if < 1)') parser.add_argument("--save-period", type=int, default=-1, help="Save checkpoint every x epochs (disabled if < 1)")
parser.add_argument('--seed', type=int, default=0, help='Global training seed') parser.add_argument("--seed", type=int, default=0, help="Global training seed")
parser.add_argument('--local_rank', type=int, default=-1, help='Automatic DDP Multi-GPU argument, do not modify') parser.add_argument("--local_rank", type=int, default=-1, help="Automatic DDP Multi-GPU argument, do not modify")
# Weights & Biases arguments # Weights & Biases arguments
parser.add_argument('--entity', default=None, help='W&B: Entity') parser.add_argument("--entity", default=None, help="W&B: Entity")
parser.add_argument('--upload_dataset', nargs='?', const=True, default=False, help='W&B: Upload data, "val" option') parser.add_argument("--upload_dataset", nargs="?", const=True, default=False, help='W&B: Upload data, "val" option')
parser.add_argument('--bbox_interval', type=int, default=-1, help='W&B: Set bounding-box image logging interval') parser.add_argument("--bbox_interval", type=int, default=-1, help="W&B: Set bounding-box image logging interval")
parser.add_argument('--artifact_alias', type=str, default='latest', help='W&B: Version of dataset artifact to use') parser.add_argument("--artifact_alias", type=str, default="latest", help="W&B: Version of dataset artifact to use")
# Comet Arguments # Comet Arguments
parser.add_argument('--comet_optimizer_config', type=str, help='Comet: Path to a Comet Optimizer Config File.') parser.add_argument("--comet_optimizer_config", type=str, help="Comet: Path to a Comet Optimizer Config File.")
parser.add_argument('--comet_optimizer_id', type=str, help='Comet: ID of the Comet Optimizer sweep.') parser.add_argument("--comet_optimizer_id", type=str, help="Comet: ID of the Comet Optimizer sweep.")
parser.add_argument('--comet_optimizer_objective', type=str, help="Comet: Set to 'minimize' or 'maximize'.") parser.add_argument("--comet_optimizer_objective", type=str, help="Comet: Set to 'minimize' or 'maximize'.")
parser.add_argument('--comet_optimizer_metric', type=str, help='Comet: Metric to Optimize.') parser.add_argument("--comet_optimizer_metric", type=str, help="Comet: Metric to Optimize.")
parser.add_argument('--comet_optimizer_workers', parser.add_argument(
type=int, "--comet_optimizer_workers",
default=1, type=int,
help='Comet: Number of Parallel Workers to use with the Comet Optimizer.') default=1,
help="Comet: Number of Parallel Workers to use with the Comet Optimizer.",
)
return parser.parse_known_args()[0] if known else parser.parse_args() return parser.parse_known_args()[0] if known else parser.parse_args()
def run(parameters, opt): def run(parameters, opt):
hyp_dict = {k: v for k, v in parameters.items() if k not in ['epochs', 'batch_size']} hyp_dict = {k: v for k, v in parameters.items() if k not in ["epochs", "batch_size"]}
opt.save_dir = str(increment_path(Path(opt.project) / opt.name, exist_ok=opt.exist_ok or opt.evolve)) opt.save_dir = str(increment_path(Path(opt.project) / opt.name, exist_ok=opt.exist_ok or opt.evolve))
opt.batch_size = parameters.get('batch_size') opt.batch_size = parameters.get("batch_size")
opt.epochs = parameters.get('epochs') opt.epochs = parameters.get("epochs")
device = select_device(opt.device, batch_size=opt.batch_size) device = select_device(opt.device, batch_size=opt.batch_size)
train(hyp_dict, opt, device, callbacks=Callbacks()) train(hyp_dict, opt, device, callbacks=Callbacks())
if __name__ == '__main__': if __name__ == "__main__":
opt = get_args(known=True) opt = get_args(known=True)
opt.weights = str(opt.weights) opt.weights = str(opt.weights)
@@ -99,7 +101,7 @@ if __name__ == '__main__':
opt.data = str(opt.data) opt.data = str(opt.data)
opt.project = str(opt.project) opt.project = str(opt.project)
optimizer_id = os.getenv('COMET_OPTIMIZER_ID') optimizer_id = os.getenv("COMET_OPTIMIZER_ID")
if optimizer_id is None: if optimizer_id is None:
with open(opt.comet_optimizer_config) as f: with open(opt.comet_optimizer_config) as f:
optimizer_config = json.load(f) optimizer_config = json.load(f)
@@ -110,9 +112,9 @@ if __name__ == '__main__':
opt.comet_optimizer_id = optimizer.id opt.comet_optimizer_id = optimizer.id
status = optimizer.status() status = optimizer.status()
opt.comet_optimizer_objective = status['spec']['objective'] opt.comet_optimizer_objective = status["spec"]["objective"]
opt.comet_optimizer_metric = status['spec']['metric'] opt.comet_optimizer_metric = status["spec"]["metric"]
logger.info('COMET INFO: Starting Hyperparameter Sweep') logger.info("COMET INFO: Starting Hyperparameter Sweep")
for parameter in optimizer.get_parameters(): for parameter in optimizer.get_parameters():
run(parameter['parameters'], opt) run(parameter["parameters"], opt)
+62 -47
View File
@@ -15,34 +15,35 @@ FILE = Path(__file__).resolve()
ROOT = FILE.parents[3] # YOLOv5 root directory ROOT = FILE.parents[3] # YOLOv5 root directory
if str(ROOT) not in sys.path: if str(ROOT) not in sys.path:
sys.path.append(str(ROOT)) # add ROOT to PATH sys.path.append(str(ROOT)) # add ROOT to PATH
RANK = int(os.getenv('RANK', -1)) RANK = int(os.getenv("RANK", -1))
DEPRECATION_WARNING = f"{colorstr('wandb')}: WARNING ⚠️ wandb is deprecated and will be removed in a future release. " \ DEPRECATION_WARNING = (
f'See supported integrations at https://github.com/ultralytics/yolov5#integrations.' f"{colorstr('wandb')}: WARNING ⚠️ wandb is deprecated and will be removed in a future release. "
f'See supported integrations at https://github.com/ultralytics/yolov5#integrations.'
)
try: try:
import wandb import wandb
assert hasattr(wandb, '__version__') # verify package import not local dir assert hasattr(wandb, "__version__") # verify package import not local dir
LOGGER.warning(DEPRECATION_WARNING) LOGGER.warning(DEPRECATION_WARNING)
except (ImportError, AssertionError): except (ImportError, AssertionError):
wandb = None wandb = None
class WandbLogger(): class WandbLogger:
"""Log training runs, datasets, models, and predictions to Weights & Biases. """
Log training runs, datasets, models, and predictions to Weights & Biases.
This logger sends information to W&B at wandb.ai. By default, this information This logger sends information to W&B at wandb.ai. By default, this information includes hyperparameters, system
includes hyperparameters, system configuration and metrics, model metrics, configuration and metrics, model metrics, and basic data metrics and analyses.
and basic data metrics and analyses.
By providing additional command line arguments to train.py, datasets, By providing additional command line arguments to train.py, datasets, models and predictions can also be logged.
models and predictions can also be logged.
For more on how this logger is used, see the Weights & Biases documentation: For more on how this logger is used, see the Weights & Biases documentation:
https://docs.wandb.com/guides/integrations/yolov5 https://docs.wandb.com/guides/integrations/yolov5
""" """
def __init__(self, opt, run_id=None, job_type='Training'): def __init__(self, opt, run_id=None, job_type="Training"):
""" """
- Initialize WandbLogger instance - Initialize WandbLogger instance
- Upload dataset if opt.upload_dataset is True - Upload dataset if opt.upload_dataset is True
@@ -53,7 +54,7 @@ class WandbLogger():
run_id (str) -- Run ID of W&B run to be resumed run_id (str) -- Run ID of W&B run to be resumed
job_type (str) -- To set the job_type for this run job_type (str) -- To set the job_type for this run
""" """
# Pre-training routine -- # Pre-training routine --
self.job_type = job_type self.job_type = job_type
self.wandb, self.wandb_run = wandb, wandb.run if wandb else None self.wandb, self.wandb_run = wandb, wandb.run if wandb else None
@@ -64,17 +65,23 @@ class WandbLogger():
self.max_imgs_to_log = 16 self.max_imgs_to_log = 16
self.data_dict = None self.data_dict = None
if self.wandb: if self.wandb:
self.wandb_run = wandb.init(config=opt, self.wandb_run = (
resume='allow', wandb.init(
project='YOLOv5' if opt.project == 'runs/train' else Path(opt.project).stem, config=opt,
entity=opt.entity, resume="allow",
name=opt.name if opt.name != 'exp' else None, project="YOLOv5" if opt.project == "runs/train" else Path(opt.project).stem,
job_type=job_type, entity=opt.entity,
id=run_id, name=opt.name if opt.name != "exp" else None,
allow_val_change=True) if not wandb.run else wandb.run job_type=job_type,
id=run_id,
allow_val_change=True,
)
if not wandb.run
else wandb.run
)
if self.wandb_run: if self.wandb_run:
if self.job_type == 'Training': if self.job_type == "Training":
if isinstance(opt.data, dict): if isinstance(opt.data, dict):
# This means another dataset manager has already processed the dataset info (e.g. ClearML) # This means another dataset manager has already processed the dataset info (e.g. ClearML)
# and they will have stored the already processed dict in opt.data # and they will have stored the already processed dict in opt.data
@@ -97,11 +104,17 @@ class WandbLogger():
if isinstance(opt.resume, str): if isinstance(opt.resume, str):
model_dir, _ = self.download_model_artifact(opt) model_dir, _ = self.download_model_artifact(opt)
if model_dir: if model_dir:
self.weights = Path(model_dir) / 'last.pt' self.weights = Path(model_dir) / "last.pt"
config = self.wandb_run.config config = self.wandb_run.config
opt.weights, opt.save_period, opt.batch_size, opt.bbox_interval, opt.epochs, opt.hyp, opt.imgsz = str( opt.weights, opt.save_period, opt.batch_size, opt.bbox_interval, opt.epochs, opt.hyp, opt.imgsz = (
self.weights), config.save_period, config.batch_size, config.bbox_interval, config.epochs, \ str(self.weights),
config.hyp, config.imgsz config.save_period,
config.batch_size,
config.bbox_interval,
config.epochs,
config.hyp,
config.imgsz,
)
if opt.bbox_interval == -1: if opt.bbox_interval == -1:
self.bbox_interval = opt.bbox_interval = (opt.epochs // 10) if opt.epochs > 10 else 1 self.bbox_interval = opt.bbox_interval = (opt.epochs // 10) if opt.epochs > 10 else 1
@@ -110,7 +123,7 @@ class WandbLogger():
def log_model(self, path, opt, epoch, fitness_score, best_model=False): def log_model(self, path, opt, epoch, fitness_score, best_model=False):
""" """
Log the model checkpoint as W&B artifact Log the model checkpoint as W&B artifact.
arguments: arguments:
path (Path) -- Path of directory containing the checkpoints path (Path) -- Path of directory containing the checkpoints
@@ -119,26 +132,30 @@ class WandbLogger():
fitness_score (float) -- fitness score for current epoch fitness_score (float) -- fitness score for current epoch
best_model (boolean) -- Boolean representing if the current checkpoint is the best yet. best_model (boolean) -- Boolean representing if the current checkpoint is the best yet.
""" """
model_artifact = wandb.Artifact('run_' + wandb.run.id + '_model', model_artifact = wandb.Artifact(
type='model', "run_" + wandb.run.id + "_model",
metadata={ type="model",
'original_url': str(path), metadata={
'epochs_trained': epoch + 1, "original_url": str(path),
'save period': opt.save_period, "epochs_trained": epoch + 1,
'project': opt.project, "save period": opt.save_period,
'total_epochs': opt.epochs, "project": opt.project,
'fitness_score': fitness_score}) "total_epochs": opt.epochs,
model_artifact.add_file(str(path / 'last.pt'), name='last.pt') "fitness_score": fitness_score,
wandb.log_artifact(model_artifact, },
aliases=['latest', 'last', 'epoch ' + str(self.current_epoch), 'best' if best_model else '']) )
LOGGER.info(f'Saving model artifact on epoch {epoch + 1}') model_artifact.add_file(str(path / "last.pt"), name="last.pt")
wandb.log_artifact(
model_artifact, aliases=["latest", "last", "epoch " + str(self.current_epoch), "best" if best_model else ""]
)
LOGGER.info(f"Saving model artifact on epoch {epoch + 1}")
def val_one_image(self, pred, predn, path, names, im): def val_one_image(self, pred, predn, path, names, im):
pass pass
def log(self, log_dict): def log(self, log_dict):
""" """
save the metrics to the logging dictionary Save the metrics to the logging dictionary.
arguments: arguments:
log_dict (Dict) -- metrics/media to be logged in current step log_dict (Dict) -- metrics/media to be logged in current step
@@ -149,7 +166,7 @@ class WandbLogger():
def end_epoch(self): def end_epoch(self):
""" """
commit the log_dict, model artifacts and Tables to W&B and flush the log_dict. Commit the log_dict, model artifacts and Tables to W&B and flush the log_dict.
arguments: arguments:
best_result (boolean): Boolean representing if the result of this evaluation is best or not best_result (boolean): Boolean representing if the result of this evaluation is best or not
@@ -160,16 +177,14 @@ class WandbLogger():
wandb.log(self.log_dict) wandb.log(self.log_dict)
except BaseException as e: except BaseException as e:
LOGGER.info( LOGGER.info(
f'An error occurred in wandb logger. The training will proceed without interruption. More info\n{e}' f"An error occurred in wandb logger. The training will proceed without interruption. More info\n{e}"
) )
self.wandb_run.finish() self.wandb_run.finish()
self.wandb_run = None self.wandb_run = None
self.log_dict = {} self.log_dict = {}
def finish_run(self): def finish_run(self):
""" """Log metrics if any and finish the current W&B run."""
Log metrics if any and finish the current W&B run
"""
if self.wandb_run: if self.wandb_run:
if self.log_dict: if self.log_dict:
with all_logging_disabled(): with all_logging_disabled():
@@ -180,7 +195,7 @@ class WandbLogger():
@contextmanager @contextmanager
def all_logging_disabled(highest_level=logging.CRITICAL): def all_logging_disabled(highest_level=logging.CRITICAL):
""" source - https://gist.github.com/simon-weber/7853144 """source - https://gist.github.com/simon-weber/7853144
A context manager that will prevent any logging messages triggered during the body from being processed. A context manager that will prevent any logging messages triggered during the body from being processed.
:param highest_level: the maximum logging level in use. :param highest_level: the maximum logging level in use.
This would only need to be changed if a custom level greater than CRITICAL is defined. This would only need to be changed if a custom level greater than CRITICAL is defined.
+30 -28
View File
@@ -1,7 +1,5 @@
# YOLOv5 🚀 by Ultralytics, AGPL-3.0 license # YOLOv5 🚀 by Ultralytics, AGPL-3.0 license
""" """Loss functions."""
Loss functions
"""
import torch import torch
import torch.nn as nn import torch.nn as nn
@@ -19,7 +17,7 @@ class BCEBlurWithLogitsLoss(nn.Module):
# BCEwithLogitLoss() with reduced missing label effects. # BCEwithLogitLoss() with reduced missing label effects.
def __init__(self, alpha=0.05): def __init__(self, alpha=0.05):
super().__init__() super().__init__()
self.loss_fcn = nn.BCEWithLogitsLoss(reduction='none') # must be nn.BCEWithLogitsLoss() self.loss_fcn = nn.BCEWithLogitsLoss(reduction="none") # must be nn.BCEWithLogitsLoss()
self.alpha = alpha self.alpha = alpha
def forward(self, pred, true): def forward(self, pred, true):
@@ -40,7 +38,7 @@ class FocalLoss(nn.Module):
self.gamma = gamma self.gamma = gamma
self.alpha = alpha self.alpha = alpha
self.reduction = loss_fcn.reduction self.reduction = loss_fcn.reduction
self.loss_fcn.reduction = 'none' # required to apply FL to each element self.loss_fcn.reduction = "none" # required to apply FL to each element
def forward(self, pred, true): def forward(self, pred, true):
loss = self.loss_fcn(pred, true) loss = self.loss_fcn(pred, true)
@@ -54,9 +52,9 @@ class FocalLoss(nn.Module):
modulating_factor = (1.0 - p_t) ** self.gamma modulating_factor = (1.0 - p_t) ** self.gamma
loss *= alpha_factor * modulating_factor loss *= alpha_factor * modulating_factor
if self.reduction == 'mean': if self.reduction == "mean":
return loss.mean() return loss.mean()
elif self.reduction == 'sum': elif self.reduction == "sum":
return loss.sum() return loss.sum()
else: # 'none' else: # 'none'
return loss return loss
@@ -70,7 +68,7 @@ class QFocalLoss(nn.Module):
self.gamma = gamma self.gamma = gamma
self.alpha = alpha self.alpha = alpha
self.reduction = loss_fcn.reduction self.reduction = loss_fcn.reduction
self.loss_fcn.reduction = 'none' # required to apply FL to each element self.loss_fcn.reduction = "none" # required to apply FL to each element
def forward(self, pred, true): def forward(self, pred, true):
loss = self.loss_fcn(pred, true) loss = self.loss_fcn(pred, true)
@@ -80,9 +78,9 @@ class QFocalLoss(nn.Module):
modulating_factor = torch.abs(true - pred_prob) ** self.gamma modulating_factor = torch.abs(true - pred_prob) ** self.gamma
loss *= alpha_factor * modulating_factor loss *= alpha_factor * modulating_factor
if self.reduction == 'mean': if self.reduction == "mean":
return loss.mean() return loss.mean()
elif self.reduction == 'sum': elif self.reduction == "sum":
return loss.sum() return loss.sum()
else: # 'none' else: # 'none'
return loss return loss
@@ -97,14 +95,14 @@ class ComputeLoss:
h = model.hyp # hyperparameters h = model.hyp # hyperparameters
# Define criteria # Define criteria
BCEcls = nn.BCEWithLogitsLoss(pos_weight=torch.tensor([h['cls_pw']], device=device)) BCEcls = nn.BCEWithLogitsLoss(pos_weight=torch.tensor([h["cls_pw"]], device=device))
BCEobj = nn.BCEWithLogitsLoss(pos_weight=torch.tensor([h['obj_pw']], device=device)) BCEobj = nn.BCEWithLogitsLoss(pos_weight=torch.tensor([h["obj_pw"]], device=device))
# Class label smoothing https://arxiv.org/pdf/1902.04103.pdf eqn 3 # Class label smoothing https://arxiv.org/pdf/1902.04103.pdf eqn 3
self.cp, self.cn = smooth_BCE(eps=h.get('label_smoothing', 0.0)) # positive, negative BCE targets self.cp, self.cn = smooth_BCE(eps=h.get("label_smoothing", 0.0)) # positive, negative BCE targets
# Focal loss # Focal loss
g = h['fl_gamma'] # focal loss gamma g = h["fl_gamma"] # focal loss gamma
if g > 0: if g > 0:
BCEcls, BCEobj = FocalLoss(BCEcls, g), FocalLoss(BCEobj, g) BCEcls, BCEobj = FocalLoss(BCEcls, g), FocalLoss(BCEobj, g)
@@ -167,9 +165,9 @@ class ComputeLoss:
if self.autobalance: if self.autobalance:
self.balance = [x / self.balance[self.ssi] for x in self.balance] self.balance = [x / self.balance[self.ssi] for x in self.balance]
lbox *= self.hyp['box'] lbox *= self.hyp["box"]
lobj *= self.hyp['obj'] lobj *= self.hyp["obj"]
lcls *= self.hyp['cls'] lcls *= self.hyp["cls"]
bs = tobj.shape[0] # batch size bs = tobj.shape[0] # batch size
return (lbox + lobj + lcls) * bs, torch.cat((lbox, lobj, lcls)).detach() return (lbox + lobj + lcls) * bs, torch.cat((lbox, lobj, lcls)).detach()
@@ -183,16 +181,20 @@ class ComputeLoss:
targets = torch.cat((targets.repeat(na, 1, 1), ai[..., None]), 2) # append anchor indices targets = torch.cat((targets.repeat(na, 1, 1), ai[..., None]), 2) # append anchor indices
g = 0.5 # bias g = 0.5 # bias
off = torch.tensor( off = (
[ torch.tensor(
[0, 0], [
[1, 0], [0, 0],
[0, 1], [1, 0],
[-1, 0], [0, 1],
[0, -1], # j,k,l,m [-1, 0],
# [1, 1], [1, -1], [-1, 1], [-1, -1], # jk,jm,lk,lm [0, -1], # j,k,l,m
], # [1, 1], [1, -1], [-1, 1], [-1, -1], # jk,jm,lk,lm
device=self.device).float() * g # offsets ],
device=self.device,
).float()
* g
) # offsets
for i in range(self.nl): for i in range(self.nl):
anchors, shape = self.anchors[i], p[i].shape anchors, shape = self.anchors[i], p[i].shape
@@ -203,7 +205,7 @@ class ComputeLoss:
if nt: if nt:
# Matches # Matches
r = t[..., 4:6] / anchors[:, None] # wh ratio r = t[..., 4:6] / anchors[:, None] # wh ratio
j = torch.max(r, 1 / r).max(2)[0] < self.hyp['anchor_t'] # compare j = torch.max(r, 1 / r).max(2)[0] < self.hyp["anchor_t"] # compare
# j = wh_iou(anchors, t[:, 4:6]) > model.hyp['iou_t'] # iou(3,n)=wh_iou(anchors(3,2), gwh(n,2)) # j = wh_iou(anchors, t[:, 4:6]) > model.hyp['iou_t'] # iou(3,n)=wh_iou(anchors(3,2), gwh(n,2))
t = t[j] # filter t = t[j] # filter
+63 -55
View File
@@ -1,7 +1,5 @@
# YOLOv5 🚀 by Ultralytics, AGPL-3.0 license # YOLOv5 🚀 by Ultralytics, AGPL-3.0 license
""" """Model validation metrics."""
Model validation metrics
"""
import math import math
import warnings import warnings
@@ -25,11 +23,13 @@ def smooth(y, f=0.05):
nf = round(len(y) * f * 2) // 2 + 1 # number of filter elements (must be odd) nf = round(len(y) * f * 2) // 2 + 1 # number of filter elements (must be odd)
p = np.ones(nf // 2) # ones padding p = np.ones(nf // 2) # ones padding
yp = np.concatenate((p * y[0], y, p * y[-1]), 0) # y padded yp = np.concatenate((p * y[0], y, p * y[-1]), 0) # y padded
return np.convolve(yp, np.ones(nf) / nf, mode='valid') # y-smoothed return np.convolve(yp, np.ones(nf) / nf, mode="valid") # y-smoothed
def ap_per_class(tp, conf, pred_cls, target_cls, plot=False, save_dir='.', names=(), eps=1e-16, prefix=''): def ap_per_class(tp, conf, pred_cls, target_cls, plot=False, save_dir=".", names=(), eps=1e-16, prefix=""):
""" Compute the average precision, given the recall and precision curves. """
Compute the average precision, given the recall and precision curves.
Source: https://github.com/rafaelpadilla/Object-Detection-Metrics. Source: https://github.com/rafaelpadilla/Object-Detection-Metrics.
# Arguments # Arguments
tp: True positives (nparray, nx1 or nx10). tp: True positives (nparray, nx1 or nx10).
@@ -83,10 +83,10 @@ def ap_per_class(tp, conf, pred_cls, target_cls, plot=False, save_dir='.', names
names = [v for k, v in names.items() if k in unique_classes] # list: only classes that have data names = [v for k, v in names.items() if k in unique_classes] # list: only classes that have data
names = dict(enumerate(names)) # to dict names = dict(enumerate(names)) # to dict
if plot: if plot:
plot_pr_curve(px, py, ap, Path(save_dir) / f'{prefix}PR_curve.png', names) plot_pr_curve(px, py, ap, Path(save_dir) / f"{prefix}PR_curve.png", names)
plot_mc_curve(px, f1, Path(save_dir) / f'{prefix}F1_curve.png', names, ylabel='F1') plot_mc_curve(px, f1, Path(save_dir) / f"{prefix}F1_curve.png", names, ylabel="F1")
plot_mc_curve(px, p, Path(save_dir) / f'{prefix}P_curve.png', names, ylabel='Precision') plot_mc_curve(px, p, Path(save_dir) / f"{prefix}P_curve.png", names, ylabel="Precision")
plot_mc_curve(px, r, Path(save_dir) / f'{prefix}R_curve.png', names, ylabel='Recall') plot_mc_curve(px, r, Path(save_dir) / f"{prefix}R_curve.png", names, ylabel="Recall")
i = smooth(f1.mean(0), 0.1).argmax() # max F1 index i = smooth(f1.mean(0), 0.1).argmax() # max F1 index
p, r, f1 = p[:, i], r[:, i], f1[:, i] p, r, f1 = p[:, i], r[:, i], f1[:, i]
@@ -96,7 +96,7 @@ def ap_per_class(tp, conf, pred_cls, target_cls, plot=False, save_dir='.', names
def compute_ap(recall, precision): def compute_ap(recall, precision):
""" Compute the average precision, given the recall and precision curves """Compute the average precision, given the recall and precision curves
# Arguments # Arguments
recall: The recall curve (list) recall: The recall curve (list)
precision: The precision curve (list) precision: The precision curve (list)
@@ -112,8 +112,8 @@ def compute_ap(recall, precision):
mpre = np.flip(np.maximum.accumulate(np.flip(mpre))) mpre = np.flip(np.maximum.accumulate(np.flip(mpre)))
# Integrate area under curve # Integrate area under curve
method = 'interp' # methods: 'continuous', 'interp' method = "interp" # methods: 'continuous', 'interp'
if method == 'interp': if method == "interp":
x = np.linspace(0, 1, 101) # 101-point interp (COCO) x = np.linspace(0, 1, 101) # 101-point interp (COCO)
ap = np.trapz(np.interp(x, mrec, mpre), x) # integrate ap = np.trapz(np.interp(x, mrec, mpre), x) # integrate
else: # 'continuous' else: # 'continuous'
@@ -134,6 +134,7 @@ class ConfusionMatrix:
def process_batch(self, detections, labels): def process_batch(self, detections, labels):
""" """
Return intersection-over-union (Jaccard index) of boxes. Return intersection-over-union (Jaccard index) of boxes.
Both sets of boxes are expected to be in (x1, y1, x2, y2) format. Both sets of boxes are expected to be in (x1, y1, x2, y2) format.
Arguments: Arguments:
detections (Array[N, 6]), x1, y1, x2, y2, conf, class detections (Array[N, 6]), x1, y1, x2, y2, conf, class
@@ -183,40 +184,41 @@ class ConfusionMatrix:
# fn = self.matrix.sum(0) - tp # false negatives (missed detections) # fn = self.matrix.sum(0) - tp # false negatives (missed detections)
return tp[:-1], fp[:-1] # remove background class return tp[:-1], fp[:-1] # remove background class
@TryExcept('WARNING ⚠️ ConfusionMatrix plot failure') @TryExcept("WARNING ⚠️ ConfusionMatrix plot failure")
def plot(self, normalize=True, save_dir='', names=()): def plot(self, normalize=True, save_dir="", names=()):
import seaborn as sn import seaborn as sn
array = self.matrix / ((self.matrix.sum(0).reshape(1, -1) + 1E-9) if normalize else 1) # normalize columns array = self.matrix / ((self.matrix.sum(0).reshape(1, -1) + 1e-9) if normalize else 1) # normalize columns
array[array < 0.005] = np.nan # don't annotate (would appear as 0.00) array[array < 0.005] = np.nan # don't annotate (would appear as 0.00)
fig, ax = plt.subplots(1, 1, figsize=(12, 9), tight_layout=True) fig, ax = plt.subplots(1, 1, figsize=(12, 9), tight_layout=True)
nc, nn = self.nc, len(names) # number of classes, names nc, nn = self.nc, len(names) # number of classes, names
sn.set(font_scale=1.0 if nc < 50 else 0.8) # for label size sn.set(font_scale=1.0 if nc < 50 else 0.8) # for label size
labels = (0 < nn < 99) and (nn == nc) # apply names to ticklabels labels = (0 < nn < 99) and (nn == nc) # apply names to ticklabels
ticklabels = (names + ['background']) if labels else 'auto' ticklabels = (names + ["background"]) if labels else "auto"
with warnings.catch_warnings(): with warnings.catch_warnings():
warnings.simplefilter('ignore') # suppress empty matrix RuntimeWarning: All-NaN slice encountered warnings.simplefilter("ignore") # suppress empty matrix RuntimeWarning: All-NaN slice encountered
sn.heatmap(array, sn.heatmap(
ax=ax, array,
annot=nc < 30, ax=ax,
annot_kws={ annot=nc < 30,
'size': 8}, annot_kws={"size": 8},
cmap='Blues', cmap="Blues",
fmt='.2f', fmt=".2f",
square=True, square=True,
vmin=0.0, vmin=0.0,
xticklabels=ticklabels, xticklabels=ticklabels,
yticklabels=ticklabels).set_facecolor((1, 1, 1)) yticklabels=ticklabels,
ax.set_xlabel('True') ).set_facecolor((1, 1, 1))
ax.set_ylabel('Predicted') ax.set_xlabel("True")
ax.set_title('Confusion Matrix') ax.set_ylabel("Predicted")
fig.savefig(Path(save_dir) / 'confusion_matrix.png', dpi=250) ax.set_title("Confusion Matrix")
fig.savefig(Path(save_dir) / "confusion_matrix.png", dpi=250)
plt.close(fig) plt.close(fig)
def print(self): def print(self):
for i in range(self.nc + 1): for i in range(self.nc + 1):
print(' '.join(map(str, self.matrix[i]))) print(" ".join(map(str, self.matrix[i])))
def bbox_iou(box1, box2, xywh=True, GIoU=False, DIoU=False, CIoU=False, eps=1e-7): def bbox_iou(box1, box2, xywh=True, GIoU=False, DIoU=False, CIoU=False, eps=1e-7):
@@ -235,8 +237,9 @@ def bbox_iou(box1, box2, xywh=True, GIoU=False, DIoU=False, CIoU=False, eps=1e-7
w2, h2 = b2_x2 - b2_x1, (b2_y2 - b2_y1).clamp(eps) w2, h2 = b2_x2 - b2_x1, (b2_y2 - b2_y1).clamp(eps)
# Intersection area # Intersection area
inter = (b1_x2.minimum(b2_x2) - b1_x1.maximum(b2_x1)).clamp(0) * \ inter = (b1_x2.minimum(b2_x2) - b1_x1.maximum(b2_x1)).clamp(0) * (
(b1_y2.minimum(b2_y2) - b1_y1.maximum(b2_y1)).clamp(0) b1_y2.minimum(b2_y2) - b1_y1.maximum(b2_y1)
).clamp(0)
# Union Area # Union Area
union = w1 * h1 + w2 * h2 - inter + eps union = w1 * h1 + w2 * h2 - inter + eps
@@ -247,10 +250,10 @@ def bbox_iou(box1, box2, xywh=True, GIoU=False, DIoU=False, CIoU=False, eps=1e-7
cw = b1_x2.maximum(b2_x2) - b1_x1.minimum(b2_x1) # convex (smallest enclosing box) width cw = b1_x2.maximum(b2_x2) - b1_x1.minimum(b2_x1) # convex (smallest enclosing box) width
ch = b1_y2.maximum(b2_y2) - b1_y1.minimum(b2_y1) # convex height ch = b1_y2.maximum(b2_y2) - b1_y1.minimum(b2_y1) # convex height
if CIoU or DIoU: # Distance or Complete IoU https://arxiv.org/abs/1911.08287v1 if CIoU or DIoU: # Distance or Complete IoU https://arxiv.org/abs/1911.08287v1
c2 = cw ** 2 + ch ** 2 + eps # convex diagonal squared c2 = cw**2 + ch**2 + eps # convex diagonal squared
rho2 = ((b2_x1 + b2_x2 - b1_x1 - b1_x2) ** 2 + (b2_y1 + b2_y2 - b1_y1 - b1_y2) ** 2) / 4 # center dist ** 2 rho2 = ((b2_x1 + b2_x2 - b1_x1 - b1_x2) ** 2 + (b2_y1 + b2_y2 - b1_y1 - b1_y2) ** 2) / 4 # center dist ** 2
if CIoU: # https://github.com/Zzh-tju/DIoU-SSD-pytorch/blob/master/utils/box/box_utils.py#L47 if CIoU: # https://github.com/Zzh-tju/DIoU-SSD-pytorch/blob/master/utils/box/box_utils.py#L47
v = (4 / math.pi ** 2) * (torch.atan(w2 / h2) - torch.atan(w1 / h1)).pow(2) v = (4 / math.pi**2) * (torch.atan(w2 / h2) - torch.atan(w1 / h1)).pow(2)
with torch.no_grad(): with torch.no_grad():
alpha = v / (v - iou + (1 + eps)) alpha = v / (v - iou + (1 + eps))
return iou - (rho2 / c2 + v * alpha) # CIoU return iou - (rho2 / c2 + v * alpha) # CIoU
@@ -264,6 +267,7 @@ def box_iou(box1, box2, eps=1e-7):
# https://github.com/pytorch/vision/blob/master/torchvision/ops/boxes.py # https://github.com/pytorch/vision/blob/master/torchvision/ops/boxes.py
""" """
Return intersection-over-union (Jaccard index) of boxes. Return intersection-over-union (Jaccard index) of boxes.
Both sets of boxes are expected to be in (x1, y1, x2, y2) format. Both sets of boxes are expected to be in (x1, y1, x2, y2) format.
Arguments: Arguments:
box1 (Tensor[N, 4]) box1 (Tensor[N, 4])
@@ -282,7 +286,10 @@ def box_iou(box1, box2, eps=1e-7):
def bbox_ioa(box1, box2, eps=1e-7): def bbox_ioa(box1, box2, eps=1e-7):
""" Returns the intersection over box2 area given box1, box2. Boxes are x1y1x2y2 """
Returns the intersection over box2 area given box1, box2.
Boxes are x1y1x2y2
box1: np.array of shape(4) box1: np.array of shape(4)
box2: np.array of shape(nx4) box2: np.array of shape(nx4)
returns: np.array of shape(n) returns: np.array of shape(n)
@@ -293,8 +300,9 @@ def bbox_ioa(box1, box2, eps=1e-7):
b2_x1, b2_y1, b2_x2, b2_y2 = box2.T b2_x1, b2_y1, b2_x2, b2_y2 = box2.T
# Intersection area # Intersection area
inter_area = (np.minimum(b1_x2, b2_x2) - np.maximum(b1_x1, b2_x1)).clip(0) * \ inter_area = (np.minimum(b1_x2, b2_x2) - np.maximum(b1_x1, b2_x1)).clip(0) * (
(np.minimum(b1_y2, b2_y2) - np.maximum(b1_y1, b2_y1)).clip(0) np.minimum(b1_y2, b2_y2) - np.maximum(b1_y1, b2_y1)
).clip(0)
# box2 area # box2 area
box2_area = (b2_x2 - b2_x1) * (b2_y2 - b2_y1) + eps box2_area = (b2_x2 - b2_x1) * (b2_y2 - b2_y1) + eps
@@ -315,46 +323,46 @@ def wh_iou(wh1, wh2, eps=1e-7):
@threaded @threaded
def plot_pr_curve(px, py, ap, save_dir=Path('pr_curve.png'), names=()): def plot_pr_curve(px, py, ap, save_dir=Path("pr_curve.png"), names=()):
# Precision-recall curve # Precision-recall curve
fig, ax = plt.subplots(1, 1, figsize=(9, 6), tight_layout=True) fig, ax = plt.subplots(1, 1, figsize=(9, 6), tight_layout=True)
py = np.stack(py, axis=1) py = np.stack(py, axis=1)
if 0 < len(names) < 21: # display per-class legend if < 21 classes if 0 < len(names) < 21: # display per-class legend if < 21 classes
for i, y in enumerate(py.T): for i, y in enumerate(py.T):
ax.plot(px, y, linewidth=1, label=f'{names[i]} {ap[i, 0]:.3f}') # plot(recall, precision) ax.plot(px, y, linewidth=1, label=f"{names[i]} {ap[i, 0]:.3f}") # plot(recall, precision)
else: else:
ax.plot(px, py, linewidth=1, color='grey') # plot(recall, precision) ax.plot(px, py, linewidth=1, color="grey") # plot(recall, precision)
ax.plot(px, py.mean(1), linewidth=3, color='blue', label='all classes %.3f mAP@0.5' % ap[:, 0].mean()) ax.plot(px, py.mean(1), linewidth=3, color="blue", label="all classes %.3f mAP@0.5" % ap[:, 0].mean())
ax.set_xlabel('Recall') ax.set_xlabel("Recall")
ax.set_ylabel('Precision') ax.set_ylabel("Precision")
ax.set_xlim(0, 1) ax.set_xlim(0, 1)
ax.set_ylim(0, 1) ax.set_ylim(0, 1)
ax.legend(bbox_to_anchor=(1.04, 1), loc='upper left') ax.legend(bbox_to_anchor=(1.04, 1), loc="upper left")
ax.set_title('Precision-Recall Curve') ax.set_title("Precision-Recall Curve")
fig.savefig(save_dir, dpi=250) fig.savefig(save_dir, dpi=250)
plt.close(fig) plt.close(fig)
@threaded @threaded
def plot_mc_curve(px, py, save_dir=Path('mc_curve.png'), names=(), xlabel='Confidence', ylabel='Metric'): def plot_mc_curve(px, py, save_dir=Path("mc_curve.png"), names=(), xlabel="Confidence", ylabel="Metric"):
# Metric-confidence curve # Metric-confidence curve
fig, ax = plt.subplots(1, 1, figsize=(9, 6), tight_layout=True) fig, ax = plt.subplots(1, 1, figsize=(9, 6), tight_layout=True)
if 0 < len(names) < 21: # display per-class legend if < 21 classes if 0 < len(names) < 21: # display per-class legend if < 21 classes
for i, y in enumerate(py): for i, y in enumerate(py):
ax.plot(px, y, linewidth=1, label=f'{names[i]}') # plot(confidence, metric) ax.plot(px, y, linewidth=1, label=f"{names[i]}") # plot(confidence, metric)
else: else:
ax.plot(px, py.T, linewidth=1, color='grey') # plot(confidence, metric) ax.plot(px, py.T, linewidth=1, color="grey") # plot(confidence, metric)
y = smooth(py.mean(0), 0.05) y = smooth(py.mean(0), 0.05)
ax.plot(px, y, linewidth=3, color='blue', label=f'all classes {y.max():.2f} at {px[y.argmax()]:.3f}') ax.plot(px, y, linewidth=3, color="blue", label=f"all classes {y.max():.2f} at {px[y.argmax()]:.3f}")
ax.set_xlabel(xlabel) ax.set_xlabel(xlabel)
ax.set_ylabel(ylabel) ax.set_ylabel(ylabel)
ax.set_xlim(0, 1) ax.set_xlim(0, 1)
ax.set_ylim(0, 1) ax.set_ylim(0, 1)
ax.legend(bbox_to_anchor=(1.04, 1), loc='upper left') ax.legend(bbox_to_anchor=(1.04, 1), loc="upper left")
ax.set_title(f'{ylabel}-Confidence Curve') ax.set_title(f"{ylabel}-Confidence Curve")
fig.savefig(save_dir, dpi=250) fig.savefig(save_dir, dpi=250)
plt.close(fig) plt.close(fig)
+138 -114
View File
@@ -1,7 +1,5 @@
# YOLOv5 🚀 by Ultralytics, AGPL-3.0 license # YOLOv5 🚀 by Ultralytics, AGPL-3.0 license
""" """Plotting utils."""
Plotting utils
"""
import contextlib import contextlib
import math import math
@@ -25,18 +23,38 @@ from utils.general import LOGGER, clip_boxes, increment_path, xywh2xyxy, xyxy2xy
from utils.metrics import fitness from utils.metrics import fitness
# Settings # Settings
RANK = int(os.getenv('RANK', -1)) RANK = int(os.getenv("RANK", -1))
matplotlib.rc('font', **{'size': 11}) matplotlib.rc("font", **{"size": 11})
matplotlib.use('Agg') # for writing to files only matplotlib.use("Agg") # for writing to files only
class Colors: class Colors:
# Ultralytics color palette https://ultralytics.com/ # Ultralytics color palette https://ultralytics.com/
def __init__(self): def __init__(self):
# hex = matplotlib.colors.TABLEAU_COLORS.values() # hex = matplotlib.colors.TABLEAU_COLORS.values()
hexs = ('FF3838', 'FF9D97', 'FF701F', 'FFB21D', 'CFD231', '48F90A', '92CC17', '3DDB86', '1A9334', '00D4BB', hexs = (
'2C99A8', '00C2FF', '344593', '6473FF', '0018EC', '8438FF', '520085', 'CB38FF', 'FF95C8', 'FF37C7') "FF3838",
self.palette = [self.hex2rgb(f'#{c}') for c in hexs] "FF9D97",
"FF701F",
"FFB21D",
"CFD231",
"48F90A",
"92CC17",
"3DDB86",
"1A9334",
"00D4BB",
"2C99A8",
"00C2FF",
"344593",
"6473FF",
"0018EC",
"8438FF",
"520085",
"CB38FF",
"FF95C8",
"FF37C7",
)
self.palette = [self.hex2rgb(f"#{c}") for c in hexs]
self.n = len(self.palette) self.n = len(self.palette)
def __call__(self, i, bgr=False): def __call__(self, i, bgr=False):
@@ -45,13 +63,13 @@ class Colors:
@staticmethod @staticmethod
def hex2rgb(h): # rgb order (PIL) def hex2rgb(h): # rgb order (PIL)
return tuple(int(h[1 + i:1 + i + 2], 16) for i in (0, 2, 4)) return tuple(int(h[1 + i : 1 + i + 2], 16) for i in (0, 2, 4))
colors = Colors() # create instance for 'from utils.plots import colors' colors = Colors() # create instance for 'from utils.plots import colors'
def feature_visualization(x, module_type, stage, n=32, save_dir=Path('runs/detect/exp')): def feature_visualization(x, module_type, stage, n=32, save_dir=Path("runs/detect/exp")):
""" """
x: Features to be visualized x: Features to be visualized
module_type: Module type module_type: Module type
@@ -59,9 +77,9 @@ def feature_visualization(x, module_type, stage, n=32, save_dir=Path('runs/detec
n: Maximum number of feature maps to plot n: Maximum number of feature maps to plot
save_dir: Directory to save results save_dir: Directory to save results
""" """
if ('Detect' if ("Detect" not in module_type) and (
not in module_type) and ('Segment' "Segment" not in module_type
not in module_type): # 'Detect' for Object Detect task,'Segment' for Segment task ): # 'Detect' for Object Detect task,'Segment' for Segment task
batch, channels, height, width = x.shape # batch, channels, height, width batch, channels, height, width = x.shape # batch, channels, height, width
if height > 1 and width > 1: if height > 1 and width > 1:
f = save_dir / f"stage{stage}_{module_type.split('.')[-1]}_features.png" # filename f = save_dir / f"stage{stage}_{module_type.split('.')[-1]}_features.png" # filename
@@ -73,12 +91,12 @@ def feature_visualization(x, module_type, stage, n=32, save_dir=Path('runs/detec
plt.subplots_adjust(wspace=0.05, hspace=0.05) plt.subplots_adjust(wspace=0.05, hspace=0.05)
for i in range(n): for i in range(n):
ax[i].imshow(blocks[i].squeeze()) # cmap='gray' ax[i].imshow(blocks[i].squeeze()) # cmap='gray'
ax[i].axis('off') ax[i].axis("off")
LOGGER.info(f'Saving {f}... ({n}/{channels})') LOGGER.info(f"Saving {f}... ({n}/{channels})")
plt.savefig(f, dpi=300, bbox_inches='tight') plt.savefig(f, dpi=300, bbox_inches="tight")
plt.close() plt.close()
np.save(str(f.with_suffix('.npy')), x[0].cpu().numpy()) # npy save np.save(str(f.with_suffix(".npy")), x[0].cpu().numpy()) # npy save
def hist2d(x, y, n=100): def hist2d(x, y, n=100):
@@ -97,7 +115,7 @@ def butter_lowpass_filtfilt(data, cutoff=1500, fs=50000, order=5):
def butter_lowpass(cutoff, fs, order): def butter_lowpass(cutoff, fs, order):
nyq = 0.5 * fs nyq = 0.5 * fs
normal_cutoff = cutoff / nyq normal_cutoff = cutoff / nyq
return butter(order, normal_cutoff, btype='low', analog=False) return butter(order, normal_cutoff, btype="low", analog=False)
b, a = butter_lowpass(cutoff, fs, order=order) b, a = butter_lowpass(cutoff, fs, order=order)
return filtfilt(b, a, data) # forward-backward filter return filtfilt(b, a, data) # forward-backward filter
@@ -114,7 +132,7 @@ def output_to_target(output, max_det=300):
@threaded @threaded
def plot_images(images, targets, paths=None, fname='images.jpg', names=None): def plot_images(images, targets, paths=None, fname="images.jpg", names=None):
# Plot image grid with labels # Plot image grid with labels
if isinstance(images, torch.Tensor): if isinstance(images, torch.Tensor):
images = images.cpu().float().numpy() images = images.cpu().float().numpy()
@@ -125,7 +143,7 @@ def plot_images(images, targets, paths=None, fname='images.jpg', names=None):
max_subplots = 16 # max image subplots, i.e. 4x4 max_subplots = 16 # max image subplots, i.e. 4x4
bs, _, h, w = images.shape # batch size, _, height, width bs, _, h, w = images.shape # batch size, _, height, width
bs = min(bs, max_subplots) # limit plot images bs = min(bs, max_subplots) # limit plot images
ns = np.ceil(bs ** 0.5) # number of subplots (square) ns = np.ceil(bs**0.5) # number of subplots (square)
if np.max(images[0]) <= 1: if np.max(images[0]) <= 1:
images *= 255 # de-normalise (optional) images *= 255 # de-normalise (optional)
@@ -136,7 +154,7 @@ def plot_images(images, targets, paths=None, fname='images.jpg', names=None):
break break
x, y = int(w * (i // ns)), int(h * (i % ns)) # block origin x, y = int(w * (i // ns)), int(h * (i % ns)) # block origin
im = im.transpose(1, 2, 0) im = im.transpose(1, 2, 0)
mosaic[y:y + h, x:x + w, :] = im mosaic[y : y + h, x : x + w, :] = im
# Resize (optional) # Resize (optional)
scale = max_size / ns / max(h, w) scale = max_size / ns / max(h, w)
@@ -156,7 +174,7 @@ def plot_images(images, targets, paths=None, fname='images.jpg', names=None):
if len(targets) > 0: if len(targets) > 0:
ti = targets[targets[:, 0] == i] # image targets ti = targets[targets[:, 0] == i] # image targets
boxes = xywh2xyxy(ti[:, 2:6]).T boxes = xywh2xyxy(ti[:, 2:6]).T
classes = ti[:, 1].astype('int') classes = ti[:, 1].astype("int")
labels = ti.shape[1] == 6 # labels if no conf column labels = ti.shape[1] == 6 # labels if no conf column
conf = None if labels else ti[:, 6] # check for confidence presence (label vs pred) conf = None if labels else ti[:, 6] # check for confidence presence (label vs pred)
@@ -173,59 +191,59 @@ def plot_images(images, targets, paths=None, fname='images.jpg', names=None):
color = colors(cls) color = colors(cls)
cls = names[cls] if names else cls cls = names[cls] if names else cls
if labels or conf[j] > 0.25: # 0.25 conf thresh if labels or conf[j] > 0.25: # 0.25 conf thresh
label = f'{cls}' if labels else f'{cls} {conf[j]:.1f}' label = f"{cls}" if labels else f"{cls} {conf[j]:.1f}"
annotator.box_label(box, label, color=color) annotator.box_label(box, label, color=color)
annotator.im.save(fname) # save annotator.im.save(fname) # save
def plot_lr_scheduler(optimizer, scheduler, epochs=300, save_dir=''): def plot_lr_scheduler(optimizer, scheduler, epochs=300, save_dir=""):
# Plot LR simulating training for full epochs # Plot LR simulating training for full epochs
optimizer, scheduler = copy(optimizer), copy(scheduler) # do not modify originals optimizer, scheduler = copy(optimizer), copy(scheduler) # do not modify originals
y = [] y = []
for _ in range(epochs): for _ in range(epochs):
scheduler.step() scheduler.step()
y.append(optimizer.param_groups[0]['lr']) y.append(optimizer.param_groups[0]["lr"])
plt.plot(y, '.-', label='LR') plt.plot(y, ".-", label="LR")
plt.xlabel('epoch') plt.xlabel("epoch")
plt.ylabel('LR') plt.ylabel("LR")
plt.grid() plt.grid()
plt.xlim(0, epochs) plt.xlim(0, epochs)
plt.ylim(0) plt.ylim(0)
plt.savefig(Path(save_dir) / 'LR.png', dpi=200) plt.savefig(Path(save_dir) / "LR.png", dpi=200)
plt.close() plt.close()
def plot_val_txt(): # from utils.plots import *; plot_val() def plot_val_txt(): # from utils.plots import *; plot_val()
# Plot val.txt histograms # Plot val.txt histograms
x = np.loadtxt('val.txt', dtype=np.float32) x = np.loadtxt("val.txt", dtype=np.float32)
box = xyxy2xywh(x[:, :4]) box = xyxy2xywh(x[:, :4])
cx, cy = box[:, 0], box[:, 1] cx, cy = box[:, 0], box[:, 1]
fig, ax = plt.subplots(1, 1, figsize=(6, 6), tight_layout=True) fig, ax = plt.subplots(1, 1, figsize=(6, 6), tight_layout=True)
ax.hist2d(cx, cy, bins=600, cmax=10, cmin=0) ax.hist2d(cx, cy, bins=600, cmax=10, cmin=0)
ax.set_aspect('equal') ax.set_aspect("equal")
plt.savefig('hist2d.png', dpi=300) plt.savefig("hist2d.png", dpi=300)
fig, ax = plt.subplots(1, 2, figsize=(12, 6), tight_layout=True) fig, ax = plt.subplots(1, 2, figsize=(12, 6), tight_layout=True)
ax[0].hist(cx, bins=600) ax[0].hist(cx, bins=600)
ax[1].hist(cy, bins=600) ax[1].hist(cy, bins=600)
plt.savefig('hist1d.png', dpi=200) plt.savefig("hist1d.png", dpi=200)
def plot_targets_txt(): # from utils.plots import *; plot_targets_txt() def plot_targets_txt(): # from utils.plots import *; plot_targets_txt()
# Plot targets.txt histograms # Plot targets.txt histograms
x = np.loadtxt('targets.txt', dtype=np.float32).T x = np.loadtxt("targets.txt", dtype=np.float32).T
s = ['x targets', 'y targets', 'width targets', 'height targets'] s = ["x targets", "y targets", "width targets", "height targets"]
fig, ax = plt.subplots(2, 2, figsize=(8, 8), tight_layout=True) fig, ax = plt.subplots(2, 2, figsize=(8, 8), tight_layout=True)
ax = ax.ravel() ax = ax.ravel()
for i in range(4): for i in range(4):
ax[i].hist(x[i], bins=100, label=f'{x[i].mean():.3g} +/- {x[i].std():.3g}') ax[i].hist(x[i], bins=100, label=f"{x[i].mean():.3g} +/- {x[i].std():.3g}")
ax[i].legend() ax[i].legend()
ax[i].set_title(s[i]) ax[i].set_title(s[i])
plt.savefig('targets.jpg', dpi=200) plt.savefig("targets.jpg", dpi=200)
def plot_val_study(file='', dir='', x=None): # from utils.plots import *; plot_val_study() def plot_val_study(file="", dir="", x=None): # from utils.plots import *; plot_val_study()
# Plot file=study.txt generated by val.py (or plot all study*.txt in dir) # Plot file=study.txt generated by val.py (or plot all study*.txt in dir)
save_dir = Path(file).parent if file else Path(dir) save_dir = Path(file).parent if file else Path(dir)
plot2 = False # plot additional results plot2 = False # plot additional results
@@ -234,69 +252,74 @@ def plot_val_study(file='', dir='', x=None): # from utils.plots import *; plot_
fig2, ax2 = plt.subplots(1, 1, figsize=(8, 4), tight_layout=True) fig2, ax2 = plt.subplots(1, 1, figsize=(8, 4), tight_layout=True)
# for f in [save_dir / f'study_coco_{x}.txt' for x in ['yolov5n6', 'yolov5s6', 'yolov5m6', 'yolov5l6', 'yolov5x6']]: # for f in [save_dir / f'study_coco_{x}.txt' for x in ['yolov5n6', 'yolov5s6', 'yolov5m6', 'yolov5l6', 'yolov5x6']]:
for f in sorted(save_dir.glob('study*.txt')): for f in sorted(save_dir.glob("study*.txt")):
y = np.loadtxt(f, dtype=np.float32, usecols=[0, 1, 2, 3, 7, 8, 9], ndmin=2).T y = np.loadtxt(f, dtype=np.float32, usecols=[0, 1, 2, 3, 7, 8, 9], ndmin=2).T
x = np.arange(y.shape[1]) if x is None else np.array(x) x = np.arange(y.shape[1]) if x is None else np.array(x)
if plot2: if plot2:
s = ['P', 'R', 'mAP@.5', 'mAP@.5:.95', 't_preprocess (ms/img)', 't_inference (ms/img)', 't_NMS (ms/img)'] s = ["P", "R", "mAP@.5", "mAP@.5:.95", "t_preprocess (ms/img)", "t_inference (ms/img)", "t_NMS (ms/img)"]
for i in range(7): for i in range(7):
ax[i].plot(x, y[i], '.-', linewidth=2, markersize=8) ax[i].plot(x, y[i], ".-", linewidth=2, markersize=8)
ax[i].set_title(s[i]) ax[i].set_title(s[i])
j = y[3].argmax() + 1 j = y[3].argmax() + 1
ax2.plot(y[5, 1:j], ax2.plot(
y[3, 1:j] * 1E2, y[5, 1:j],
'.-', y[3, 1:j] * 1e2,
linewidth=2, ".-",
markersize=8, linewidth=2,
label=f.stem.replace('study_coco_', '').replace('yolo', 'YOLO')) markersize=8,
label=f.stem.replace("study_coco_", "").replace("yolo", "YOLO"),
)
ax2.plot(1E3 / np.array([209, 140, 97, 58, 35, 18]), [34.6, 40.5, 43.0, 47.5, 49.7, 51.5], ax2.plot(
'k.-', 1e3 / np.array([209, 140, 97, 58, 35, 18]),
linewidth=2, [34.6, 40.5, 43.0, 47.5, 49.7, 51.5],
markersize=8, "k.-",
alpha=.25, linewidth=2,
label='EfficientDet') markersize=8,
alpha=0.25,
label="EfficientDet",
)
ax2.grid(alpha=0.2) ax2.grid(alpha=0.2)
ax2.set_yticks(np.arange(20, 60, 5)) ax2.set_yticks(np.arange(20, 60, 5))
ax2.set_xlim(0, 57) ax2.set_xlim(0, 57)
ax2.set_ylim(25, 55) ax2.set_ylim(25, 55)
ax2.set_xlabel('GPU Speed (ms/img)') ax2.set_xlabel("GPU Speed (ms/img)")
ax2.set_ylabel('COCO AP val') ax2.set_ylabel("COCO AP val")
ax2.legend(loc='lower right') ax2.legend(loc="lower right")
f = save_dir / 'study.png' f = save_dir / "study.png"
print(f'Saving {f}...') print(f"Saving {f}...")
plt.savefig(f, dpi=300) plt.savefig(f, dpi=300)
@TryExcept() # known issue https://github.com/ultralytics/yolov5/issues/5395 @TryExcept() # known issue https://github.com/ultralytics/yolov5/issues/5395
def plot_labels(labels, names=(), save_dir=Path('')): def plot_labels(labels, names=(), save_dir=Path("")):
# plot dataset labels # plot dataset labels
LOGGER.info(f"Plotting labels to {save_dir / 'labels.jpg'}... ") LOGGER.info(f"Plotting labels to {save_dir / 'labels.jpg'}... ")
c, b = labels[:, 0], labels[:, 1:].transpose() # classes, boxes c, b = labels[:, 0], labels[:, 1:].transpose() # classes, boxes
nc = int(c.max() + 1) # number of classes nc = int(c.max() + 1) # number of classes
x = pd.DataFrame(b.transpose(), columns=['x', 'y', 'width', 'height']) x = pd.DataFrame(b.transpose(), columns=["x", "y", "width", "height"])
# seaborn correlogram # seaborn correlogram
sn.pairplot(x, corner=True, diag_kind='auto', kind='hist', diag_kws=dict(bins=50), plot_kws=dict(pmax=0.9)) sn.pairplot(x, corner=True, diag_kind="auto", kind="hist", diag_kws=dict(bins=50), plot_kws=dict(pmax=0.9))
plt.savefig(save_dir / 'labels_correlogram.jpg', dpi=200) plt.savefig(save_dir / "labels_correlogram.jpg", dpi=200)
plt.close() plt.close()
# matplotlib labels # matplotlib labels
matplotlib.use('svg') # faster matplotlib.use("svg") # faster
ax = plt.subplots(2, 2, figsize=(8, 8), tight_layout=True)[1].ravel() ax = plt.subplots(2, 2, figsize=(8, 8), tight_layout=True)[1].ravel()
y = ax[0].hist(c, bins=np.linspace(0, nc, nc + 1) - 0.5, rwidth=0.8) y = ax[0].hist(c, bins=np.linspace(0, nc, nc + 1) - 0.5, rwidth=0.8)
with contextlib.suppress(Exception): # color histogram bars by class with contextlib.suppress(Exception): # color histogram bars by class
[y[2].patches[i].set_color([x / 255 for x in colors(i)]) for i in range(nc)] # known issue #3195 [y[2].patches[i].set_color([x / 255 for x in colors(i)]) for i in range(nc)] # known issue #3195
ax[0].set_ylabel('instances') ax[0].set_ylabel("instances")
if 0 < len(names) < 30: if 0 < len(names) < 30:
ax[0].set_xticks(range(len(names))) ax[0].set_xticks(range(len(names)))
ax[0].set_xticklabels(list(names.values()), rotation=90, fontsize=10) ax[0].set_xticklabels(list(names.values()), rotation=90, fontsize=10)
else: else:
ax[0].set_xlabel('classes') ax[0].set_xlabel("classes")
sn.histplot(x, x='x', y='y', ax=ax[2], bins=50, pmax=0.9) sn.histplot(x, x="x", y="y", ax=ax[2], bins=50, pmax=0.9)
sn.histplot(x, x='width', y='height', ax=ax[3], bins=50, pmax=0.9) sn.histplot(x, x="width", y="height", ax=ax[3], bins=50, pmax=0.9)
# rectangles # rectangles
labels[:, 1:3] = 0.5 # center labels[:, 1:3] = 0.5 # center
@@ -305,47 +328,48 @@ def plot_labels(labels, names=(), save_dir=Path('')):
for cls, *box in labels[:1000]: for cls, *box in labels[:1000]:
ImageDraw.Draw(img).rectangle(box, width=1, outline=colors(cls)) # plot ImageDraw.Draw(img).rectangle(box, width=1, outline=colors(cls)) # plot
ax[1].imshow(img) ax[1].imshow(img)
ax[1].axis('off') ax[1].axis("off")
for a in [0, 1, 2, 3]: for a in [0, 1, 2, 3]:
for s in ['top', 'right', 'left', 'bottom']: for s in ["top", "right", "left", "bottom"]:
ax[a].spines[s].set_visible(False) ax[a].spines[s].set_visible(False)
plt.savefig(save_dir / 'labels.jpg', dpi=200) plt.savefig(save_dir / "labels.jpg", dpi=200)
matplotlib.use('Agg') matplotlib.use("Agg")
plt.close() plt.close()
def imshow_cls(im, labels=None, pred=None, names=None, nmax=25, verbose=False, f=Path('images.jpg')): def imshow_cls(im, labels=None, pred=None, names=None, nmax=25, verbose=False, f=Path("images.jpg")):
# Show classification image grid with labels (optional) and predictions (optional) # Show classification image grid with labels (optional) and predictions (optional)
from utils.augmentations import denormalize from utils.augmentations import denormalize
names = names or [f'class{i}' for i in range(1000)] names = names or [f"class{i}" for i in range(1000)]
blocks = torch.chunk(denormalize(im.clone()).cpu().float(), len(im), blocks = torch.chunk(
dim=0) # select batch index 0, block by channels denormalize(im.clone()).cpu().float(), len(im), dim=0
) # select batch index 0, block by channels
n = min(len(blocks), nmax) # number of plots n = min(len(blocks), nmax) # number of plots
m = min(8, round(n ** 0.5)) # 8 x 8 default m = min(8, round(n**0.5)) # 8 x 8 default
fig, ax = plt.subplots(math.ceil(n / m), m) # 8 rows x n/8 cols fig, ax = plt.subplots(math.ceil(n / m), m) # 8 rows x n/8 cols
ax = ax.ravel() if m > 1 else [ax] ax = ax.ravel() if m > 1 else [ax]
# plt.subplots_adjust(wspace=0.05, hspace=0.05) # plt.subplots_adjust(wspace=0.05, hspace=0.05)
for i in range(n): for i in range(n):
ax[i].imshow(blocks[i].squeeze().permute((1, 2, 0)).numpy().clip(0.0, 1.0)) ax[i].imshow(blocks[i].squeeze().permute((1, 2, 0)).numpy().clip(0.0, 1.0))
ax[i].axis('off') ax[i].axis("off")
if labels is not None: if labels is not None:
s = names[labels[i]] + (f'—{names[pred[i]]}' if pred is not None else '') s = names[labels[i]] + (f"—{names[pred[i]]}" if pred is not None else "")
ax[i].set_title(s, fontsize=8, verticalalignment='top') ax[i].set_title(s, fontsize=8, verticalalignment="top")
plt.savefig(f, dpi=300, bbox_inches='tight') plt.savefig(f, dpi=300, bbox_inches="tight")
plt.close() plt.close()
if verbose: if verbose:
LOGGER.info(f'Saving {f}') LOGGER.info(f"Saving {f}")
if labels is not None: if labels is not None:
LOGGER.info('True: ' + ' '.join(f'{names[i]:3s}' for i in labels[:nmax])) LOGGER.info("True: " + " ".join(f"{names[i]:3s}" for i in labels[:nmax]))
if pred is not None: if pred is not None:
LOGGER.info('Predicted:' + ' '.join(f'{names[i]:3s}' for i in pred[:nmax])) LOGGER.info("Predicted:" + " ".join(f"{names[i]:3s}" for i in pred[:nmax]))
return f return f
def plot_evolve(evolve_csv='path/to/evolve.csv'): # from utils.plots import *; plot_evolve() def plot_evolve(evolve_csv="path/to/evolve.csv"): # from utils.plots import *; plot_evolve()
# Plot evolve.csv hyp evolution results # Plot evolve.csv hyp evolution results
evolve_csv = Path(evolve_csv) evolve_csv = Path(evolve_csv)
data = pd.read_csv(evolve_csv) data = pd.read_csv(evolve_csv)
@@ -354,83 +378,83 @@ def plot_evolve(evolve_csv='path/to/evolve.csv'): # from utils.plots import *;
f = fitness(x) f = fitness(x)
j = np.argmax(f) # max fitness index j = np.argmax(f) # max fitness index
plt.figure(figsize=(10, 12), tight_layout=True) plt.figure(figsize=(10, 12), tight_layout=True)
matplotlib.rc('font', **{'size': 8}) matplotlib.rc("font", **{"size": 8})
print(f'Best results from row {j} of {evolve_csv}:') print(f"Best results from row {j} of {evolve_csv}:")
for i, k in enumerate(keys[7:]): for i, k in enumerate(keys[7:]):
v = x[:, 7 + i] v = x[:, 7 + i]
mu = v[j] # best single result mu = v[j] # best single result
plt.subplot(6, 5, i + 1) plt.subplot(6, 5, i + 1)
plt.scatter(v, f, c=hist2d(v, f, 20), cmap='viridis', alpha=.8, edgecolors='none') plt.scatter(v, f, c=hist2d(v, f, 20), cmap="viridis", alpha=0.8, edgecolors="none")
plt.plot(mu, f.max(), 'k+', markersize=15) plt.plot(mu, f.max(), "k+", markersize=15)
plt.title(f'{k} = {mu:.3g}', fontdict={'size': 9}) # limit to 40 characters plt.title(f"{k} = {mu:.3g}", fontdict={"size": 9}) # limit to 40 characters
if i % 5 != 0: if i % 5 != 0:
plt.yticks([]) plt.yticks([])
print(f'{k:>15}: {mu:.3g}') print(f"{k:>15}: {mu:.3g}")
f = evolve_csv.with_suffix('.png') # filename f = evolve_csv.with_suffix(".png") # filename
plt.savefig(f, dpi=200) plt.savefig(f, dpi=200)
plt.close() plt.close()
print(f'Saved {f}') print(f"Saved {f}")
def plot_results(file='path/to/results.csv', dir=''): def plot_results(file="path/to/results.csv", dir=""):
# Plot training results.csv. Usage: from utils.plots import *; plot_results('path/to/results.csv') # Plot training results.csv. Usage: from utils.plots import *; plot_results('path/to/results.csv')
save_dir = Path(file).parent if file else Path(dir) save_dir = Path(file).parent if file else Path(dir)
fig, ax = plt.subplots(2, 5, figsize=(12, 6), tight_layout=True) fig, ax = plt.subplots(2, 5, figsize=(12, 6), tight_layout=True)
ax = ax.ravel() ax = ax.ravel()
files = list(save_dir.glob('results*.csv')) files = list(save_dir.glob("results*.csv"))
assert len(files), f'No results.csv files found in {save_dir.resolve()}, nothing to plot.' assert len(files), f"No results.csv files found in {save_dir.resolve()}, nothing to plot."
for f in files: for f in files:
try: try:
data = pd.read_csv(f) data = pd.read_csv(f)
s = [x.strip() for x in data.columns] s = [x.strip() for x in data.columns]
x = data.values[:, 0] x = data.values[:, 0]
for i, j in enumerate([1, 2, 3, 4, 5, 8, 9, 10, 6, 7]): for i, j in enumerate([1, 2, 3, 4, 5, 8, 9, 10, 6, 7]):
y = data.values[:, j].astype('float') y = data.values[:, j].astype("float")
# y[y == 0] = np.nan # don't show zero values # y[y == 0] = np.nan # don't show zero values
ax[i].plot(x, y, marker='.', label=f.stem, linewidth=2, markersize=8) # actual results ax[i].plot(x, y, marker=".", label=f.stem, linewidth=2, markersize=8) # actual results
ax[i].plot(x, gaussian_filter1d(y, sigma=3), ':', label='smooth', linewidth=2) # smoothing line ax[i].plot(x, gaussian_filter1d(y, sigma=3), ":", label="smooth", linewidth=2) # smoothing line
ax[i].set_title(s[j], fontsize=12) ax[i].set_title(s[j], fontsize=12)
# if j in [8, 9, 10]: # share train and val loss y axes # if j in [8, 9, 10]: # share train and val loss y axes
# ax[i].get_shared_y_axes().join(ax[i], ax[i - 5]) # ax[i].get_shared_y_axes().join(ax[i], ax[i - 5])
except Exception as e: except Exception as e:
LOGGER.info(f'Warning: Plotting error for {f}: {e}') LOGGER.info(f"Warning: Plotting error for {f}: {e}")
ax[1].legend() ax[1].legend()
fig.savefig(save_dir / 'results.png', dpi=200) fig.savefig(save_dir / "results.png", dpi=200)
plt.close() plt.close()
def profile_idetection(start=0, stop=0, labels=(), save_dir=''): def profile_idetection(start=0, stop=0, labels=(), save_dir=""):
# Plot iDetection '*.txt' per-image logs. from utils.plots import *; profile_idetection() # Plot iDetection '*.txt' per-image logs. from utils.plots import *; profile_idetection()
ax = plt.subplots(2, 4, figsize=(12, 6), tight_layout=True)[1].ravel() ax = plt.subplots(2, 4, figsize=(12, 6), tight_layout=True)[1].ravel()
s = ['Images', 'Free Storage (GB)', 'RAM Usage (GB)', 'Battery', 'dt_raw (ms)', 'dt_smooth (ms)', 'real-world FPS'] s = ["Images", "Free Storage (GB)", "RAM Usage (GB)", "Battery", "dt_raw (ms)", "dt_smooth (ms)", "real-world FPS"]
files = list(Path(save_dir).glob('frames*.txt')) files = list(Path(save_dir).glob("frames*.txt"))
for fi, f in enumerate(files): for fi, f in enumerate(files):
try: try:
results = np.loadtxt(f, ndmin=2).T[:, 90:-30] # clip first and last rows results = np.loadtxt(f, ndmin=2).T[:, 90:-30] # clip first and last rows
n = results.shape[1] # number of rows n = results.shape[1] # number of rows
x = np.arange(start, min(stop, n) if stop else n) x = np.arange(start, min(stop, n) if stop else n)
results = results[:, x] results = results[:, x]
t = (results[0] - results[0].min()) # set t0=0s t = results[0] - results[0].min() # set t0=0s
results[0] = x results[0] = x
for i, a in enumerate(ax): for i, a in enumerate(ax):
if i < len(results): if i < len(results):
label = labels[fi] if len(labels) else f.stem.replace('frames_', '') label = labels[fi] if len(labels) else f.stem.replace("frames_", "")
a.plot(t, results[i], marker='.', label=label, linewidth=1, markersize=5) a.plot(t, results[i], marker=".", label=label, linewidth=1, markersize=5)
a.set_title(s[i]) a.set_title(s[i])
a.set_xlabel('time (s)') a.set_xlabel("time (s)")
# if fi == len(files) - 1: # if fi == len(files) - 1:
# a.set_ylim(bottom=0) # a.set_ylim(bottom=0)
for side in ['top', 'right']: for side in ["top", "right"]:
a.spines[side].set_visible(False) a.spines[side].set_visible(False)
else: else:
a.remove() a.remove()
except Exception as e: except Exception as e:
print(f'Warning: Plotting error for {f}; {e}') print(f"Warning: Plotting error for {f}; {e}")
ax[1].legend() ax[1].legend()
plt.savefig(Path(save_dir) / 'idetection_profile.png', dpi=200) plt.savefig(Path(save_dir) / "idetection_profile.png", dpi=200)
def save_one_box(xyxy, im, file=Path('im.jpg'), gain=1.02, pad=10, square=False, BGR=False, save=True): def save_one_box(xyxy, im, file=Path("im.jpg"), gain=1.02, pad=10, square=False, BGR=False, save=True):
# Save image crop as {file} with crop size multiple {gain} and {pad} pixels. Save and/or return crop # Save image crop as {file} with crop size multiple {gain} and {pad} pixels. Save and/or return crop
xyxy = torch.tensor(xyxy).view(-1, 4) xyxy = torch.tensor(xyxy).view(-1, 4)
b = xyxy2xywh(xyxy) # boxes b = xyxy2xywh(xyxy) # boxes
@@ -439,10 +463,10 @@ def save_one_box(xyxy, im, file=Path('im.jpg'), gain=1.02, pad=10, square=False,
b[:, 2:] = b[:, 2:] * gain + pad # box wh * gain + pad b[:, 2:] = b[:, 2:] * gain + pad # box wh * gain + pad
xyxy = xywh2xyxy(b).long() xyxy = xywh2xyxy(b).long()
clip_boxes(xyxy, im.shape) clip_boxes(xyxy, im.shape)
crop = im[int(xyxy[0, 1]):int(xyxy[0, 3]), int(xyxy[0, 0]):int(xyxy[0, 2]), ::(1 if BGR else -1)] crop = im[int(xyxy[0, 1]) : int(xyxy[0, 3]), int(xyxy[0, 0]) : int(xyxy[0, 2]), :: (1 if BGR else -1)]
if save: if save:
file.parent.mkdir(parents=True, exist_ok=True) # make directory file.parent.mkdir(parents=True, exist_ok=True) # make directory
f = str(increment_path(file).with_suffix('.jpg')) f = str(increment_path(file).with_suffix(".jpg"))
# cv2.imwrite(f, crop) # save BGR, https://github.com/ultralytics/yolov5/issues/7007 chroma subsampling issue # cv2.imwrite(f, crop) # save BGR, https://github.com/ultralytics/yolov5/issues/7007 chroma subsampling issue
Image.fromarray(crop[..., ::-1]).save(f, quality=95, subsampling=0) # save RGB Image.fromarray(crop[..., ::-1]).save(f, quality=95, subsampling=0) # save RGB
return crop return crop
+7 -15
View File
@@ -1,7 +1,5 @@
# YOLOv5 🚀 by Ultralytics, AGPL-3.0 license # YOLOv5 🚀 by Ultralytics, AGPL-3.0 license
""" """Image augmentation functions."""
Image augmentation functions
"""
import math import math
import random import random
@@ -22,15 +20,9 @@ def mixup(im, labels, segments, im2, labels2, segments2):
return im, labels, segments return im, labels, segments
def random_perspective(im, def random_perspective(
targets=(), im, targets=(), segments=(), degrees=10, translate=0.1, scale=0.1, shear=10, perspective=0.0, border=(0, 0)
segments=(), ):
degrees=10,
translate=.1,
scale=.1,
shear=10,
perspective=0.0,
border=(0, 0)):
# torchvision.transforms.RandomAffine(degrees=(-10, 10), translate=(.1, .1), scale=(.9, 1.1), shear=(-10, 10)) # torchvision.transforms.RandomAffine(degrees=(-10, 10), translate=(.1, .1), scale=(.9, 1.1), shear=(-10, 10))
# targets = [cls, xyxy] # targets = [cls, xyxy]
@@ -62,8 +54,8 @@ def random_perspective(im,
# Translation # Translation
T = np.eye(3) T = np.eye(3)
T[0, 2] = (random.uniform(0.5 - translate, 0.5 + translate) * width) # x translation (pixels) T[0, 2] = random.uniform(0.5 - translate, 0.5 + translate) * width # x translation (pixels)
T[1, 2] = (random.uniform(0.5 - translate, 0.5 + translate) * height) # y translation (pixels) T[1, 2] = random.uniform(0.5 - translate, 0.5 + translate) * height # y translation (pixels)
# Combined rotation matrix # Combined rotation matrix
M = T @ S @ R @ P @ C # order of operations (right to left) is IMPORTANT M = T @ S @ R @ P @ C # order of operations (right to left) is IMPORTANT
@@ -89,7 +81,7 @@ def random_perspective(im,
xy = np.ones((len(segment), 3)) xy = np.ones((len(segment), 3))
xy[:, :2] = segment xy[:, :2] = segment
xy = xy @ M.T # transform xy = xy @ M.T # transform
xy = (xy[:, :2] / xy[:, 2:3] if perspective else xy[:, :2]) # perspective rescale or affine xy = xy[:, :2] / xy[:, 2:3] if perspective else xy[:, :2] # perspective rescale or affine
# clip # clip
new[i] = segment2box(xy, width, height) new[i] = segment2box(xy, width, height)
+85 -60
View File
@@ -1,7 +1,5 @@
# YOLOv5 🚀 by Ultralytics, AGPL-3.0 license # YOLOv5 🚀 by Ultralytics, AGPL-3.0 license
""" """Dataloaders."""
Dataloaders
"""
import os import os
import random import random
@@ -17,30 +15,32 @@ from ..general import LOGGER, xyn2xy, xywhn2xyxy, xyxy2xywhn
from ..torch_utils import torch_distributed_zero_first from ..torch_utils import torch_distributed_zero_first
from .augmentations import mixup, random_perspective from .augmentations import mixup, random_perspective
RANK = int(os.getenv('RANK', -1)) RANK = int(os.getenv("RANK", -1))
def create_dataloader(path, def create_dataloader(
imgsz, path,
batch_size, imgsz,
stride, batch_size,
single_cls=False, stride,
hyp=None, single_cls=False,
augment=False, hyp=None,
cache=False, augment=False,
pad=0.0, cache=False,
rect=False, pad=0.0,
rank=-1, rect=False,
workers=8, rank=-1,
image_weights=False, workers=8,
quad=False, image_weights=False,
prefix='', quad=False,
shuffle=False, prefix="",
mask_downsample_ratio=1, shuffle=False,
overlap_mask=False, mask_downsample_ratio=1,
seed=0): overlap_mask=False,
seed=0,
):
if rect and shuffle: if rect and shuffle:
LOGGER.warning('WARNING ⚠️ --rect is incompatible with DataLoader shuffle, setting shuffle=False') LOGGER.warning("WARNING ⚠️ --rect is incompatible with DataLoader shuffle, setting shuffle=False")
shuffle = False shuffle = False
with torch_distributed_zero_first(rank): # init dataset *.cache only once if DDP with torch_distributed_zero_first(rank): # init dataset *.cache only once if DDP
dataset = LoadImagesAndLabelsAndMasks( dataset = LoadImagesAndLabelsAndMasks(
@@ -58,7 +58,8 @@ def create_dataloader(path,
prefix=prefix, prefix=prefix,
downsample_ratio=mask_downsample_ratio, downsample_ratio=mask_downsample_ratio,
overlap=overlap_mask, overlap=overlap_mask,
rank=rank) rank=rank,
)
batch_size = min(batch_size, len(dataset)) batch_size = min(batch_size, len(dataset))
nd = torch.cuda.device_count() # number of CUDA devices nd = torch.cuda.device_count() # number of CUDA devices
@@ -81,7 +82,6 @@ def create_dataloader(path,
class LoadImagesAndLabelsAndMasks(LoadImagesAndLabels): # for training/testing class LoadImagesAndLabelsAndMasks(LoadImagesAndLabels): # for training/testing
def __init__( def __init__(
self, self,
path, path,
@@ -96,14 +96,29 @@ class LoadImagesAndLabelsAndMasks(LoadImagesAndLabels): # for training/testing
stride=32, stride=32,
pad=0, pad=0,
min_items=0, min_items=0,
prefix='', prefix="",
downsample_ratio=1, downsample_ratio=1,
overlap=False, overlap=False,
rank=-1, rank=-1,
seed=0, seed=0,
): ):
super().__init__(path, img_size, batch_size, augment, hyp, rect, image_weights, cache_images, single_cls, super().__init__(
stride, pad, min_items, prefix, rank, seed) path,
img_size,
batch_size,
augment,
hyp,
rect,
image_weights,
cache_images,
single_cls,
stride,
pad,
min_items,
prefix,
rank,
seed,
)
self.downsample_ratio = downsample_ratio self.downsample_ratio = downsample_ratio
self.overlap = overlap self.overlap = overlap
@@ -111,7 +126,7 @@ class LoadImagesAndLabelsAndMasks(LoadImagesAndLabels): # for training/testing
index = self.indices[index] # linear, shuffled, or image_weights index = self.indices[index] # linear, shuffled, or image_weights
hyp = self.hyp hyp = self.hyp
mosaic = self.mosaic and random.random() < hyp['mosaic'] mosaic = self.mosaic and random.random() < hyp["mosaic"]
masks = [] masks = []
if mosaic: if mosaic:
# Load mosaic # Load mosaic
@@ -119,7 +134,7 @@ class LoadImagesAndLabelsAndMasks(LoadImagesAndLabels): # for training/testing
shapes = None shapes = None
# MixUp augmentation # MixUp augmentation
if random.random() < hyp['mixup']: if random.random() < hyp["mixup"]:
img, labels, segments = mixup(img, labels, segments, *self.load_mosaic(random.randint(0, self.n - 1))) img, labels, segments = mixup(img, labels, segments, *self.load_mosaic(random.randint(0, self.n - 1)))
else: else:
@@ -147,30 +162,36 @@ class LoadImagesAndLabelsAndMasks(LoadImagesAndLabels): # for training/testing
labels[:, 1:] = xywhn2xyxy(labels[:, 1:], ratio[0] * w, ratio[1] * h, padw=pad[0], padh=pad[1]) labels[:, 1:] = xywhn2xyxy(labels[:, 1:], ratio[0] * w, ratio[1] * h, padw=pad[0], padh=pad[1])
if self.augment: if self.augment:
img, labels, segments = random_perspective(img, img, labels, segments = random_perspective(
labels, img,
segments=segments, labels,
degrees=hyp['degrees'], segments=segments,
translate=hyp['translate'], degrees=hyp["degrees"],
scale=hyp['scale'], translate=hyp["translate"],
shear=hyp['shear'], scale=hyp["scale"],
perspective=hyp['perspective']) shear=hyp["shear"],
perspective=hyp["perspective"],
)
nl = len(labels) # number of labels nl = len(labels) # number of labels
if nl: if nl:
labels[:, 1:5] = xyxy2xywhn(labels[:, 1:5], w=img.shape[1], h=img.shape[0], clip=True, eps=1e-3) labels[:, 1:5] = xyxy2xywhn(labels[:, 1:5], w=img.shape[1], h=img.shape[0], clip=True, eps=1e-3)
if self.overlap: if self.overlap:
masks, sorted_idx = polygons2masks_overlap(img.shape[:2], masks, sorted_idx = polygons2masks_overlap(
segments, img.shape[:2], segments, downsample_ratio=self.downsample_ratio
downsample_ratio=self.downsample_ratio) )
masks = masks[None] # (640, 640) -> (1, 640, 640) masks = masks[None] # (640, 640) -> (1, 640, 640)
labels = labels[sorted_idx] labels = labels[sorted_idx]
else: else:
masks = polygons2masks(img.shape[:2], segments, color=1, downsample_ratio=self.downsample_ratio) masks = polygons2masks(img.shape[:2], segments, color=1, downsample_ratio=self.downsample_ratio)
masks = (torch.from_numpy(masks) if len(masks) else torch.zeros(1 if self.overlap else nl, img.shape[0] // masks = (
self.downsample_ratio, img.shape[1] // torch.from_numpy(masks)
self.downsample_ratio)) if len(masks)
else torch.zeros(
1 if self.overlap else nl, img.shape[0] // self.downsample_ratio, img.shape[1] // self.downsample_ratio
)
)
# TODO: albumentations support # TODO: albumentations support
if self.augment: if self.augment:
# Albumentations # Albumentations
@@ -180,17 +201,17 @@ class LoadImagesAndLabelsAndMasks(LoadImagesAndLabels): # for training/testing
nl = len(labels) # update after albumentations nl = len(labels) # update after albumentations
# HSV color-space # HSV color-space
augment_hsv(img, hgain=hyp['hsv_h'], sgain=hyp['hsv_s'], vgain=hyp['hsv_v']) augment_hsv(img, hgain=hyp["hsv_h"], sgain=hyp["hsv_s"], vgain=hyp["hsv_v"])
# Flip up-down # Flip up-down
if random.random() < hyp['flipud']: if random.random() < hyp["flipud"]:
img = np.flipud(img) img = np.flipud(img)
if nl: if nl:
labels[:, 2] = 1 - labels[:, 2] labels[:, 2] = 1 - labels[:, 2]
masks = torch.flip(masks, dims=[1]) masks = torch.flip(masks, dims=[1])
# Flip left-right # Flip left-right
if random.random() < hyp['fliplr']: if random.random() < hyp["fliplr"]:
img = np.fliplr(img) img = np.fliplr(img)
if nl: if nl:
labels[:, 1] = 1 - labels[:, 1] labels[:, 1] = 1 - labels[:, 1]
@@ -254,16 +275,18 @@ class LoadImagesAndLabelsAndMasks(LoadImagesAndLabels): # for training/testing
# img4, labels4 = replicate(img4, labels4) # replicate # img4, labels4 = replicate(img4, labels4) # replicate
# Augment # Augment
img4, labels4, segments4 = copy_paste(img4, labels4, segments4, p=self.hyp['copy_paste']) img4, labels4, segments4 = copy_paste(img4, labels4, segments4, p=self.hyp["copy_paste"])
img4, labels4, segments4 = random_perspective(img4, img4, labels4, segments4 = random_perspective(
labels4, img4,
segments4, labels4,
degrees=self.hyp['degrees'], segments4,
translate=self.hyp['translate'], degrees=self.hyp["degrees"],
scale=self.hyp['scale'], translate=self.hyp["translate"],
shear=self.hyp['shear'], scale=self.hyp["scale"],
perspective=self.hyp['perspective'], shear=self.hyp["shear"],
border=self.mosaic_border) # border to remove perspective=self.hyp["perspective"],
border=self.mosaic_border,
) # border to remove
return img4, labels4, segments4 return img4, labels4, segments4
@staticmethod @staticmethod
@@ -312,8 +335,10 @@ def polygons2masks(img_size, polygons, color, downsample_ratio=1):
def polygons2masks_overlap(img_size, segments, downsample_ratio=1): def polygons2masks_overlap(img_size, segments, downsample_ratio=1):
"""Return a (640, 640) overlap mask.""" """Return a (640, 640) overlap mask."""
masks = np.zeros((img_size[0] // downsample_ratio, img_size[1] // downsample_ratio), masks = np.zeros(
dtype=np.int32 if len(segments) > 255 else np.uint8) (img_size[0] // downsample_ratio, img_size[1] // downsample_ratio),
dtype=np.int32 if len(segments) > 255 else np.uint8,
)
areas = [] areas = []
ms = [] ms = []
for si in range(len(segments)): for si in range(len(segments)):
+9 -10
View File
@@ -6,8 +6,7 @@ import torch.nn.functional as F
def crop_mask(masks, boxes): def crop_mask(masks, boxes):
""" """
"Crop" predicted masks by zeroing out everything not in the predicted bbox. "Crop" predicted masks by zeroing out everything not in the predicted bbox. Vectorized by Chong (thanks Chong).
Vectorized by Chong (thanks Chong).
Args: Args:
- masks should be a size [n, h, w] tensor of masks - masks should be a size [n, h, w] tensor of masks
@@ -35,7 +34,7 @@ def process_mask_upsample(protos, masks_in, bboxes, shape):
c, mh, mw = protos.shape # CHW c, mh, mw = protos.shape # CHW
masks = (masks_in @ protos.float().view(c, -1)).sigmoid().view(-1, mh, mw) masks = (masks_in @ protos.float().view(c, -1)).sigmoid().view(-1, mh, mw)
masks = F.interpolate(masks[None], shape, mode='bilinear', align_corners=False)[0] # CHW masks = F.interpolate(masks[None], shape, mode="bilinear", align_corners=False)[0] # CHW
masks = crop_mask(masks, bboxes) # CHW masks = crop_mask(masks, bboxes) # CHW
return masks.gt_(0.5) return masks.gt_(0.5)
@@ -63,7 +62,7 @@ def process_mask(protos, masks_in, bboxes, shape, upsample=False):
masks = crop_mask(masks, downsampled_bboxes) # CHW masks = crop_mask(masks, downsampled_bboxes) # CHW
if upsample: if upsample:
masks = F.interpolate(masks[None], shape, mode='bilinear', align_corners=False)[0] # CHW masks = F.interpolate(masks[None], shape, mode="bilinear", align_corners=False)[0] # CHW
return masks.gt_(0.5) return masks.gt_(0.5)
@@ -85,7 +84,7 @@ def process_mask_native(protos, masks_in, bboxes, shape):
bottom, right = int(mh - pad[1]), int(mw - pad[0]) bottom, right = int(mh - pad[1]), int(mw - pad[0])
masks = masks[:, top:bottom, left:right] masks = masks[:, top:bottom, left:right]
masks = F.interpolate(masks[None], shape, mode='bilinear', align_corners=False)[0] # CHW masks = F.interpolate(masks[None], shape, mode="bilinear", align_corners=False)[0] # CHW
masks = crop_mask(masks, bboxes) # CHW masks = crop_mask(masks, bboxes) # CHW
return masks.gt_(0.5) return masks.gt_(0.5)
@@ -144,17 +143,17 @@ def masks_iou(mask1, mask2, eps=1e-7):
return intersection / (union + eps) return intersection / (union + eps)
def masks2segments(masks, strategy='largest'): def masks2segments(masks, strategy="largest"):
# Convert masks(n,160,160) into segments(n,xy) # Convert masks(n,160,160) into segments(n,xy)
segments = [] segments = []
for x in masks.int().cpu().numpy().astype('uint8'): for x in masks.int().cpu().numpy().astype("uint8"):
c = cv2.findContours(x, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0] c = cv2.findContours(x, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0]
if c: if c:
if strategy == 'concat': # concatenate all segments if strategy == "concat": # concatenate all segments
c = np.concatenate([x.reshape(-1, 2) for x in c]) c = np.concatenate([x.reshape(-1, 2) for x in c])
elif strategy == 'largest': # select largest segment elif strategy == "largest": # select largest segment
c = np.array(c[np.array([len(x) for x in c]).argmax()]).reshape(-1, 2) c = np.array(c[np.array([len(x) for x in c]).argmax()]).reshape(-1, 2)
else: else:
c = np.zeros((0, 2)) # no segments found c = np.zeros((0, 2)) # no segments found
segments.append(c.astype('float32')) segments.append(c.astype("float32"))
return segments return segments
+25 -21
View File
@@ -18,14 +18,14 @@ class ComputeLoss:
h = model.hyp # hyperparameters h = model.hyp # hyperparameters
# Define criteria # Define criteria
BCEcls = nn.BCEWithLogitsLoss(pos_weight=torch.tensor([h['cls_pw']], device=device)) BCEcls = nn.BCEWithLogitsLoss(pos_weight=torch.tensor([h["cls_pw"]], device=device))
BCEobj = nn.BCEWithLogitsLoss(pos_weight=torch.tensor([h['obj_pw']], device=device)) BCEobj = nn.BCEWithLogitsLoss(pos_weight=torch.tensor([h["obj_pw"]], device=device))
# Class label smoothing https://arxiv.org/pdf/1902.04103.pdf eqn 3 # Class label smoothing https://arxiv.org/pdf/1902.04103.pdf eqn 3
self.cp, self.cn = smooth_BCE(eps=h.get('label_smoothing', 0.0)) # positive, negative BCE targets self.cp, self.cn = smooth_BCE(eps=h.get("label_smoothing", 0.0)) # positive, negative BCE targets
# Focal loss # Focal loss
g = h['fl_gamma'] # focal loss gamma g = h["fl_gamma"] # focal loss gamma
if g > 0: if g > 0:
BCEcls, BCEobj = FocalLoss(BCEcls, g), FocalLoss(BCEobj, g) BCEcls, BCEobj = FocalLoss(BCEcls, g), FocalLoss(BCEobj, g)
@@ -82,7 +82,7 @@ class ComputeLoss:
# Mask regression # Mask regression
if tuple(masks.shape[-2:]) != (mask_h, mask_w): # downsample if tuple(masks.shape[-2:]) != (mask_h, mask_w): # downsample
masks = F.interpolate(masks[None], (mask_h, mask_w), mode='nearest')[0] masks = F.interpolate(masks[None], (mask_h, mask_w), mode="nearest")[0]
marea = xywhn[i][:, 2:].prod(1) # mask width, height normalized marea = xywhn[i][:, 2:].prod(1) # mask width, height normalized
mxyxy = xywh2xyxy(xywhn[i] * torch.tensor([mask_w, mask_h, mask_w, mask_h], device=self.device)) mxyxy = xywh2xyxy(xywhn[i] * torch.tensor([mask_w, mask_h, mask_w, mask_h], device=self.device))
for bi in b.unique(): for bi in b.unique():
@@ -100,10 +100,10 @@ class ComputeLoss:
if self.autobalance: if self.autobalance:
self.balance = [x / self.balance[self.ssi] for x in self.balance] self.balance = [x / self.balance[self.ssi] for x in self.balance]
lbox *= self.hyp['box'] lbox *= self.hyp["box"]
lobj *= self.hyp['obj'] lobj *= self.hyp["obj"]
lcls *= self.hyp['cls'] lcls *= self.hyp["cls"]
lseg *= self.hyp['box'] / bs lseg *= self.hyp["box"] / bs
loss = lbox + lobj + lcls + lseg loss = lbox + lobj + lcls + lseg
return loss * bs, torch.cat((lbox, lseg, lobj, lcls)).detach() return loss * bs, torch.cat((lbox, lseg, lobj, lcls)).detach()
@@ -111,7 +111,7 @@ class ComputeLoss:
def single_mask_loss(self, gt_mask, pred, proto, xyxy, area): def single_mask_loss(self, gt_mask, pred, proto, xyxy, area):
# Mask loss for one image # Mask loss for one image
pred_mask = (pred @ proto.view(self.nm, -1)).view(-1, *proto.shape[1:]) # (n,32) @ (32,80,80) -> (n,80,80) pred_mask = (pred @ proto.view(self.nm, -1)).view(-1, *proto.shape[1:]) # (n,32) @ (32,80,80) -> (n,80,80)
loss = F.binary_cross_entropy_with_logits(pred_mask, gt_mask, reduction='none') loss = F.binary_cross_entropy_with_logits(pred_mask, gt_mask, reduction="none")
return (crop_mask(loss, xyxy).mean(dim=(1, 2)) / area).mean() return (crop_mask(loss, xyxy).mean(dim=(1, 2)) / area).mean()
def build_targets(self, p, targets): def build_targets(self, p, targets):
@@ -132,16 +132,20 @@ class ComputeLoss:
targets = torch.cat((targets.repeat(na, 1, 1), ai[..., None], ti[..., None]), 2) # append anchor indices targets = torch.cat((targets.repeat(na, 1, 1), ai[..., None], ti[..., None]), 2) # append anchor indices
g = 0.5 # bias g = 0.5 # bias
off = torch.tensor( off = (
[ torch.tensor(
[0, 0], [
[1, 0], [0, 0],
[0, 1], [1, 0],
[-1, 0], [0, 1],
[0, -1], # j,k,l,m [-1, 0],
# [1, 1], [1, -1], [-1, 1], [-1, -1], # jk,jm,lk,lm [0, -1], # j,k,l,m
], # [1, 1], [1, -1], [-1, 1], [-1, -1], # jk,jm,lk,lm
device=self.device).float() * g # offsets ],
device=self.device,
).float()
* g
) # offsets
for i in range(self.nl): for i in range(self.nl):
anchors, shape = self.anchors[i], p[i].shape anchors, shape = self.anchors[i], p[i].shape
@@ -152,7 +156,7 @@ class ComputeLoss:
if nt: if nt:
# Matches # Matches
r = t[..., 4:6] / anchors[:, None] # wh ratio r = t[..., 4:6] / anchors[:, None] # wh ratio
j = torch.max(r, 1 / r).max(2)[0] < self.hyp['anchor_t'] # compare j = torch.max(r, 1 / r).max(2)[0] < self.hyp["anchor_t"] # compare
# j = wh_iou(anchors, t[:, 4:6]) > model.hyp['iou_t'] # iou(3,n)=wh_iou(anchors(3,2), gwh(n,2)) # j = wh_iou(anchors, t[:, 4:6]) > model.hyp['iou_t'] # iou(3,n)=wh_iou(anchors(3,2), gwh(n,2))
t = t[j] # filter t = t[j] # filter
+79 -77
View File
@@ -1,7 +1,5 @@
# YOLOv5 🚀 by Ultralytics, AGPL-3.0 license # YOLOv5 🚀 by Ultralytics, AGPL-3.0 license
""" """Model validation metrics."""
Model validation metrics
"""
import numpy as np import numpy as np
@@ -15,14 +13,14 @@ def fitness(x):
def ap_per_class_box_and_mask( def ap_per_class_box_and_mask(
tp_m, tp_m,
tp_b, tp_b,
conf, conf,
pred_cls, pred_cls,
target_cls, target_cls,
plot=False, plot=False,
save_dir='.', save_dir=".",
names=(), names=(),
): ):
""" """
Args: Args:
@@ -30,41 +28,33 @@ def ap_per_class_box_and_mask(
tp_m: tp of masks. tp_m: tp of masks.
other arguments see `func: ap_per_class`. other arguments see `func: ap_per_class`.
""" """
results_boxes = ap_per_class(tp_b, results_boxes = ap_per_class(
conf, tp_b, conf, pred_cls, target_cls, plot=plot, save_dir=save_dir, names=names, prefix="Box"
pred_cls, )[2:]
target_cls, results_masks = ap_per_class(
plot=plot, tp_m, conf, pred_cls, target_cls, plot=plot, save_dir=save_dir, names=names, prefix="Mask"
save_dir=save_dir, )[2:]
names=names,
prefix='Box')[2:]
results_masks = ap_per_class(tp_m,
conf,
pred_cls,
target_cls,
plot=plot,
save_dir=save_dir,
names=names,
prefix='Mask')[2:]
results = { results = {
'boxes': { "boxes": {
'p': results_boxes[0], "p": results_boxes[0],
'r': results_boxes[1], "r": results_boxes[1],
'ap': results_boxes[3], "ap": results_boxes[3],
'f1': results_boxes[2], "f1": results_boxes[2],
'ap_class': results_boxes[4]}, "ap_class": results_boxes[4],
'masks': { },
'p': results_masks[0], "masks": {
'r': results_masks[1], "p": results_masks[0],
'ap': results_masks[3], "r": results_masks[1],
'f1': results_masks[2], "ap": results_masks[3],
'ap_class': results_masks[4]}} "f1": results_masks[2],
"ap_class": results_masks[4],
},
}
return results return results
class Metric: class Metric:
def __init__(self) -> None: def __init__(self) -> None:
self.p = [] # (nc, ) self.p = [] # (nc, )
self.r = [] # (nc, ) self.r = [] # (nc, )
@@ -74,7 +64,9 @@ class Metric:
@property @property
def ap50(self): def ap50(self):
"""AP@0.5 of all classes. """
AP@0.5 of all classes.
Return: Return:
(nc, ) or []. (nc, ) or [].
""" """
@@ -90,7 +82,9 @@ class Metric:
@property @property
def mp(self): def mp(self):
"""mean precision of all classes. """
Mean precision of all classes.
Return: Return:
float. float.
""" """
@@ -98,7 +92,9 @@ class Metric:
@property @property
def mr(self): def mr(self):
"""mean recall of all classes. """
Mean recall of all classes.
Return: Return:
float. float.
""" """
@@ -106,7 +102,9 @@ class Metric:
@property @property
def map50(self): def map50(self):
"""Mean AP@0.5 of all classes. """
Mean AP@0.5 of all classes.
Return: Return:
float. float.
""" """
@@ -114,18 +112,20 @@ class Metric:
@property @property
def map(self): def map(self):
"""Mean AP@0.5:0.95 of all classes. """
Mean AP@0.5:0.95 of all classes.
Return: Return:
float. float.
""" """
return self.all_ap.mean() if len(self.all_ap) else 0.0 return self.all_ap.mean() if len(self.all_ap) else 0.0
def mean_results(self): def mean_results(self):
"""Mean of results, return mp, mr, map50, map""" """Mean of results, return mp, mr, map50, map."""
return (self.mp, self.mr, self.map50, self.map) return (self.mp, self.mr, self.map50, self.map)
def class_result(self, i): def class_result(self, i):
"""class-aware result, return p[i], r[i], ap50[i], ap[i]""" """Class-aware result, return p[i], r[i], ap50[i], ap[i]"""
return (self.p[i], self.r[i], self.ap50[i], self.ap[i]) return (self.p[i], self.r[i], self.ap50[i], self.ap[i])
def get_maps(self, nc): def get_maps(self, nc):
@@ -159,8 +159,8 @@ class Metrics:
Args: Args:
results: Dict{'boxes': Dict{}, 'masks': Dict{}} results: Dict{'boxes': Dict{}, 'masks': Dict{}}
""" """
self.metric_box.update(list(results['boxes'].values())) self.metric_box.update(list(results["boxes"].values()))
self.metric_mask.update(list(results['masks'].values())) self.metric_mask.update(list(results["masks"].values()))
def mean_results(self): def mean_results(self):
return self.metric_box.mean_results() + self.metric_mask.mean_results() return self.metric_box.mean_results() + self.metric_mask.mean_results()
@@ -178,33 +178,35 @@ class Metrics:
KEYS = [ KEYS = [
'train/box_loss', "train/box_loss",
'train/seg_loss', # train loss "train/seg_loss", # train loss
'train/obj_loss', "train/obj_loss",
'train/cls_loss', "train/cls_loss",
'metrics/precision(B)', "metrics/precision(B)",
'metrics/recall(B)', "metrics/recall(B)",
'metrics/mAP_0.5(B)', "metrics/mAP_0.5(B)",
'metrics/mAP_0.5:0.95(B)', # metrics "metrics/mAP_0.5:0.95(B)", # metrics
'metrics/precision(M)', "metrics/precision(M)",
'metrics/recall(M)', "metrics/recall(M)",
'metrics/mAP_0.5(M)', "metrics/mAP_0.5(M)",
'metrics/mAP_0.5:0.95(M)', # metrics "metrics/mAP_0.5:0.95(M)", # metrics
'val/box_loss', "val/box_loss",
'val/seg_loss', # val loss "val/seg_loss", # val loss
'val/obj_loss', "val/obj_loss",
'val/cls_loss', "val/cls_loss",
'x/lr0', "x/lr0",
'x/lr1', "x/lr1",
'x/lr2', ] "x/lr2",
]
BEST_KEYS = [ BEST_KEYS = [
'best/epoch', "best/epoch",
'best/precision(B)', "best/precision(B)",
'best/recall(B)', "best/recall(B)",
'best/mAP_0.5(B)', "best/mAP_0.5(B)",
'best/mAP_0.5:0.95(B)', "best/mAP_0.5:0.95(B)",
'best/precision(M)', "best/precision(M)",
'best/recall(M)', "best/recall(M)",
'best/mAP_0.5(M)', "best/mAP_0.5(M)",
'best/mAP_0.5:0.95(M)', ] "best/mAP_0.5:0.95(M)",
]
+21 -18
View File
@@ -14,7 +14,7 @@ from ..plots import Annotator, colors
@threaded @threaded
def plot_images_and_masks(images, targets, masks, paths=None, fname='images.jpg', names=None): def plot_images_and_masks(images, targets, masks, paths=None, fname="images.jpg", names=None):
# Plot image grid with labels # Plot image grid with labels
if isinstance(images, torch.Tensor): if isinstance(images, torch.Tensor):
images = images.cpu().float().numpy() images = images.cpu().float().numpy()
@@ -27,7 +27,7 @@ def plot_images_and_masks(images, targets, masks, paths=None, fname='images.jpg'
max_subplots = 16 # max image subplots, i.e. 4x4 max_subplots = 16 # max image subplots, i.e. 4x4
bs, _, h, w = images.shape # batch size, _, height, width bs, _, h, w = images.shape # batch size, _, height, width
bs = min(bs, max_subplots) # limit plot images bs = min(bs, max_subplots) # limit plot images
ns = np.ceil(bs ** 0.5) # number of subplots (square) ns = np.ceil(bs**0.5) # number of subplots (square)
if np.max(images[0]) <= 1: if np.max(images[0]) <= 1:
images *= 255 # de-normalise (optional) images *= 255 # de-normalise (optional)
@@ -38,7 +38,7 @@ def plot_images_and_masks(images, targets, masks, paths=None, fname='images.jpg'
break break
x, y = int(w * (i // ns)), int(h * (i % ns)) # block origin x, y = int(w * (i // ns)), int(h * (i % ns)) # block origin
im = im.transpose(1, 2, 0) im = im.transpose(1, 2, 0)
mosaic[y:y + h, x:x + w, :] = im mosaic[y : y + h, x : x + w, :] = im
# Resize (optional) # Resize (optional)
scale = max_size / ns / max(h, w) scale = max_size / ns / max(h, w)
@@ -60,7 +60,7 @@ def plot_images_and_masks(images, targets, masks, paths=None, fname='images.jpg'
ti = targets[idx] # image targets ti = targets[idx] # image targets
boxes = xywh2xyxy(ti[:, 2:6]).T boxes = xywh2xyxy(ti[:, 2:6]).T
classes = ti[:, 1].astype('int') classes = ti[:, 1].astype("int")
labels = ti.shape[1] == 6 # labels if no conf column labels = ti.shape[1] == 6 # labels if no conf column
conf = None if labels else ti[:, 6] # check for confidence presence (label vs pred) conf = None if labels else ti[:, 6] # check for confidence presence (label vs pred)
@@ -77,7 +77,7 @@ def plot_images_and_masks(images, targets, masks, paths=None, fname='images.jpg'
color = colors(cls) color = colors(cls)
cls = names[cls] if names else cls cls = names[cls] if names else cls
if labels or conf[j] > 0.25: # 0.25 conf thresh if labels or conf[j] > 0.25: # 0.25 conf thresh
label = f'{cls}' if labels else f'{cls} {conf[j]:.1f}' label = f"{cls}" if labels else f"{cls} {conf[j]:.1f}"
annotator.box_label(box, label, color=color) annotator.box_label(box, label, color=color)
# Plot masks # Plot masks
@@ -103,41 +103,44 @@ def plot_images_and_masks(images, targets, masks, paths=None, fname='images.jpg'
else: else:
mask = image_masks[j].astype(bool) mask = image_masks[j].astype(bool)
with contextlib.suppress(Exception): with contextlib.suppress(Exception):
im[y:y + h, x:x + w, :][mask] = im[y:y + h, x:x + w, :][mask] * 0.4 + np.array(color) * 0.6 im[y : y + h, x : x + w, :][mask] = (
im[y : y + h, x : x + w, :][mask] * 0.4 + np.array(color) * 0.6
)
annotator.fromarray(im) annotator.fromarray(im)
annotator.im.save(fname) # save annotator.im.save(fname) # save
def plot_results_with_masks(file='path/to/results.csv', dir='', best=True): def plot_results_with_masks(file="path/to/results.csv", dir="", best=True):
# Plot training results.csv. Usage: from utils.plots import *; plot_results('path/to/results.csv') # Plot training results.csv. Usage: from utils.plots import *; plot_results('path/to/results.csv')
save_dir = Path(file).parent if file else Path(dir) save_dir = Path(file).parent if file else Path(dir)
fig, ax = plt.subplots(2, 8, figsize=(18, 6), tight_layout=True) fig, ax = plt.subplots(2, 8, figsize=(18, 6), tight_layout=True)
ax = ax.ravel() ax = ax.ravel()
files = list(save_dir.glob('results*.csv')) files = list(save_dir.glob("results*.csv"))
assert len(files), f'No results.csv files found in {save_dir.resolve()}, nothing to plot.' assert len(files), f"No results.csv files found in {save_dir.resolve()}, nothing to plot."
for f in files: for f in files:
try: try:
data = pd.read_csv(f) data = pd.read_csv(f)
index = np.argmax(0.9 * data.values[:, 8] + 0.1 * data.values[:, 7] + 0.9 * data.values[:, 12] + index = np.argmax(
0.1 * data.values[:, 11]) 0.9 * data.values[:, 8] + 0.1 * data.values[:, 7] + 0.9 * data.values[:, 12] + 0.1 * data.values[:, 11]
)
s = [x.strip() for x in data.columns] s = [x.strip() for x in data.columns]
x = data.values[:, 0] x = data.values[:, 0]
for i, j in enumerate([1, 2, 3, 4, 5, 6, 9, 10, 13, 14, 15, 16, 7, 8, 11, 12]): for i, j in enumerate([1, 2, 3, 4, 5, 6, 9, 10, 13, 14, 15, 16, 7, 8, 11, 12]):
y = data.values[:, j] y = data.values[:, j]
# y[y == 0] = np.nan # don't show zero values # y[y == 0] = np.nan # don't show zero values
ax[i].plot(x, y, marker='.', label=f.stem, linewidth=2, markersize=2) ax[i].plot(x, y, marker=".", label=f.stem, linewidth=2, markersize=2)
if best: if best:
# best # best
ax[i].scatter(index, y[index], color='r', label=f'best:{index}', marker='*', linewidth=3) ax[i].scatter(index, y[index], color="r", label=f"best:{index}", marker="*", linewidth=3)
ax[i].set_title(s[j] + f'\n{round(y[index], 5)}') ax[i].set_title(s[j] + f"\n{round(y[index], 5)}")
else: else:
# last # last
ax[i].scatter(x[-1], y[-1], color='r', label='last', marker='*', linewidth=3) ax[i].scatter(x[-1], y[-1], color="r", label="last", marker="*", linewidth=3)
ax[i].set_title(s[j] + f'\n{round(y[-1], 5)}') ax[i].set_title(s[j] + f"\n{round(y[-1], 5)}")
# if j in [8, 9, 10]: # share train and val loss y axes # if j in [8, 9, 10]: # share train and val loss y axes
# ax[i].get_shared_y_axes().join(ax[i], ax[i - 5]) # ax[i].get_shared_y_axes().join(ax[i], ax[i - 5])
except Exception as e: except Exception as e:
print(f'Warning: Plotting error for {f}: {e}') print(f"Warning: Plotting error for {f}: {e}")
ax[1].legend() ax[1].legend()
fig.savefig(save_dir / 'results.png', dpi=200) fig.savefig(save_dir / "results.png", dpi=200)
plt.close() plt.close()
+123 -105
View File
@@ -1,7 +1,5 @@
# YOLOv5 🚀 by Ultralytics, AGPL-3.0 license # YOLOv5 🚀 by Ultralytics, AGPL-3.0 license
""" """PyTorch utils."""
PyTorch utils
"""
import math import math
import os import os
@@ -21,9 +19,9 @@ from torch.nn.parallel import DistributedDataParallel as DDP
from utils.general import LOGGER, check_version, colorstr, file_date, git_describe from utils.general import LOGGER, check_version, colorstr, file_date, git_describe
LOCAL_RANK = int(os.getenv('LOCAL_RANK', -1)) # https://pytorch.org/docs/stable/elastic/run.html LOCAL_RANK = int(os.getenv("LOCAL_RANK", -1)) # https://pytorch.org/docs/stable/elastic/run.html
RANK = int(os.getenv('RANK', -1)) RANK = int(os.getenv("RANK", -1))
WORLD_SIZE = int(os.getenv('WORLD_SIZE', 1)) WORLD_SIZE = int(os.getenv("WORLD_SIZE", 1))
try: try:
import thop # for FLOPs computation import thop # for FLOPs computation
@@ -31,11 +29,11 @@ except ImportError:
thop = None thop = None
# Suppress PyTorch warnings # Suppress PyTorch warnings
warnings.filterwarnings('ignore', message='User provided device_type of \'cuda\', but CUDA is not available. Disabling') warnings.filterwarnings("ignore", message="User provided device_type of 'cuda', but CUDA is not available. Disabling")
warnings.filterwarnings('ignore', category=UserWarning) warnings.filterwarnings("ignore", category=UserWarning)
def smart_inference_mode(torch_1_9=check_version(torch.__version__, '1.9.0')): def smart_inference_mode(torch_1_9=check_version(torch.__version__, "1.9.0")):
# Applies torch.inference_mode() decorator if torch>=1.9.0 else torch.no_grad() decorator # Applies torch.inference_mode() decorator if torch>=1.9.0 else torch.no_grad() decorator
def decorate(fn): def decorate(fn):
return (torch.inference_mode if torch_1_9 else torch.no_grad)()(fn) return (torch.inference_mode if torch_1_9 else torch.no_grad)()(fn)
@@ -45,19 +43,20 @@ def smart_inference_mode(torch_1_9=check_version(torch.__version__, '1.9.0')):
def smartCrossEntropyLoss(label_smoothing=0.0): def smartCrossEntropyLoss(label_smoothing=0.0):
# Returns nn.CrossEntropyLoss with label smoothing enabled for torch>=1.10.0 # Returns nn.CrossEntropyLoss with label smoothing enabled for torch>=1.10.0
if check_version(torch.__version__, '1.10.0'): if check_version(torch.__version__, "1.10.0"):
return nn.CrossEntropyLoss(label_smoothing=label_smoothing) return nn.CrossEntropyLoss(label_smoothing=label_smoothing)
if label_smoothing > 0: if label_smoothing > 0:
LOGGER.warning(f'WARNING ⚠️ label smoothing {label_smoothing} requires torch>=1.10.0') LOGGER.warning(f"WARNING ⚠️ label smoothing {label_smoothing} requires torch>=1.10.0")
return nn.CrossEntropyLoss() return nn.CrossEntropyLoss()
def smart_DDP(model): def smart_DDP(model):
# Model DDP creation with checks # Model DDP creation with checks
assert not check_version(torch.__version__, '1.12.0', pinned=True), \ assert not check_version(torch.__version__, "1.12.0", pinned=True), (
'torch==1.12.0 torchvision==0.13.0 DDP training is not supported due to a known issue. ' \ "torch==1.12.0 torchvision==0.13.0 DDP training is not supported due to a known issue. "
'Please upgrade or downgrade torch to use DDP. See https://github.com/ultralytics/yolov5/issues/8395' "Please upgrade or downgrade torch to use DDP. See https://github.com/ultralytics/yolov5/issues/8395"
if check_version(torch.__version__, '1.11.0'): )
if check_version(torch.__version__, "1.11.0"):
return DDP(model, device_ids=[LOCAL_RANK], output_device=LOCAL_RANK, static_graph=True) return DDP(model, device_ids=[LOCAL_RANK], output_device=LOCAL_RANK, static_graph=True)
else: else:
return DDP(model, device_ids=[LOCAL_RANK], output_device=LOCAL_RANK) return DDP(model, device_ids=[LOCAL_RANK], output_device=LOCAL_RANK)
@@ -66,7 +65,8 @@ def smart_DDP(model):
def reshape_classifier_output(model, n=1000): def reshape_classifier_output(model, n=1000):
# Update a TorchVision classification model to class count 'n' if required # Update a TorchVision classification model to class count 'n' if required
from models.common import Classify from models.common import Classify
name, m = list((model.model if hasattr(model, 'model') else model).named_children())[-1] # last module
name, m = list((model.model if hasattr(model, "model") else model).named_children())[-1] # last module
if isinstance(m, Classify): # YOLOv5 Classify() head if isinstance(m, Classify): # YOLOv5 Classify() head
if m.linear.out_features != n: if m.linear.out_features != n:
m.linear = nn.Linear(m.linear.in_features, n) m.linear = nn.Linear(m.linear.in_features, n)
@@ -97,43 +97,44 @@ def torch_distributed_zero_first(local_rank: int):
def device_count(): def device_count():
# Returns number of CUDA devices available. Safe version of torch.cuda.device_count(). Supports Linux and Windows # Returns number of CUDA devices available. Safe version of torch.cuda.device_count(). Supports Linux and Windows
assert platform.system() in ('Linux', 'Windows'), 'device_count() only supported on Linux or Windows' assert platform.system() in ("Linux", "Windows"), "device_count() only supported on Linux or Windows"
try: try:
cmd = 'nvidia-smi -L | wc -l' if platform.system() == 'Linux' else 'nvidia-smi -L | find /c /v ""' # Windows cmd = "nvidia-smi -L | wc -l" if platform.system() == "Linux" else 'nvidia-smi -L | find /c /v ""' # Windows
return int(subprocess.run(cmd, shell=True, capture_output=True, check=True).stdout.decode().split()[-1]) return int(subprocess.run(cmd, shell=True, capture_output=True, check=True).stdout.decode().split()[-1])
except Exception: except Exception:
return 0 return 0
def select_device(device='', batch_size=0, newline=True): def select_device(device="", batch_size=0, newline=True):
# device = None or 'cpu' or 0 or '0' or '0,1,2,3' # device = None or 'cpu' or 0 or '0' or '0,1,2,3'
s = f'YOLOv5 🚀 {git_describe() or file_date()} Python-{platform.python_version()} torch-{torch.__version__} ' s = f"YOLOv5 🚀 {git_describe() or file_date()} Python-{platform.python_version()} torch-{torch.__version__} "
device = str(device).strip().lower().replace('cuda:', '').replace('none', '') # to string, 'cuda:0' to '0' device = str(device).strip().lower().replace("cuda:", "").replace("none", "") # to string, 'cuda:0' to '0'
cpu = device == 'cpu' cpu = device == "cpu"
mps = device == 'mps' # Apple Metal Performance Shaders (MPS) mps = device == "mps" # Apple Metal Performance Shaders (MPS)
if cpu or mps: if cpu or mps:
os.environ['CUDA_VISIBLE_DEVICES'] = '-1' # force torch.cuda.is_available() = False os.environ["CUDA_VISIBLE_DEVICES"] = "-1" # force torch.cuda.is_available() = False
elif device: # non-cpu device requested elif device: # non-cpu device requested
os.environ['CUDA_VISIBLE_DEVICES'] = device # set environment variable - must be before assert is_available() os.environ["CUDA_VISIBLE_DEVICES"] = device # set environment variable - must be before assert is_available()
assert torch.cuda.is_available() and torch.cuda.device_count() >= len(device.replace(',', '')), \ assert torch.cuda.is_available() and torch.cuda.device_count() >= len(
f"Invalid CUDA '--device {device}' requested, use '--device cpu' or pass valid CUDA device(s)" device.replace(",", "")
), f"Invalid CUDA '--device {device}' requested, use '--device cpu' or pass valid CUDA device(s)"
if not cpu and not mps and torch.cuda.is_available(): # prefer GPU if available if not cpu and not mps and torch.cuda.is_available(): # prefer GPU if available
devices = device.split(',') if device else '0' # range(torch.cuda.device_count()) # i.e. 0,1,6,7 devices = device.split(",") if device else "0" # range(torch.cuda.device_count()) # i.e. 0,1,6,7
n = len(devices) # device count n = len(devices) # device count
if n > 1 and batch_size > 0: # check batch_size is divisible by device_count if n > 1 and batch_size > 0: # check batch_size is divisible by device_count
assert batch_size % n == 0, f'batch-size {batch_size} not multiple of GPU count {n}' assert batch_size % n == 0, f"batch-size {batch_size} not multiple of GPU count {n}"
space = ' ' * (len(s) + 1) space = " " * (len(s) + 1)
for i, d in enumerate(devices): for i, d in enumerate(devices):
p = torch.cuda.get_device_properties(i) p = torch.cuda.get_device_properties(i)
s += f"{'' if i == 0 else space}CUDA:{d} ({p.name}, {p.total_memory / (1 << 20):.0f}MiB)\n" # bytes to MB s += f"{'' if i == 0 else space}CUDA:{d} ({p.name}, {p.total_memory / (1 << 20):.0f}MiB)\n" # bytes to MB
arg = 'cuda:0' arg = "cuda:0"
elif mps and getattr(torch, 'has_mps', False) and torch.backends.mps.is_available(): # prefer MPS if available elif mps and getattr(torch, "has_mps", False) and torch.backends.mps.is_available(): # prefer MPS if available
s += 'MPS\n' s += "MPS\n"
arg = 'mps' arg = "mps"
else: # revert to CPU else: # revert to CPU
s += 'CPU\n' s += "CPU\n"
arg = 'cpu' arg = "cpu"
if not newline: if not newline:
s = s.rstrip() s = s.rstrip()
@@ -149,7 +150,7 @@ def time_sync():
def profile(input, ops, n=10, device=None): def profile(input, ops, n=10, device=None):
""" YOLOv5 speed/memory/FLOPs profiler """YOLOv5 speed/memory/FLOPs profiler
Usage: Usage:
input = torch.randn(16, 3, 640, 640) input = torch.randn(16, 3, 640, 640)
m1 = lambda x: x * torch.sigmoid(x) m1 = lambda x: x * torch.sigmoid(x)
@@ -159,18 +160,20 @@ def profile(input, ops, n=10, device=None):
results = [] results = []
if not isinstance(device, torch.device): if not isinstance(device, torch.device):
device = select_device(device) device = select_device(device)
print(f"{'Params':>12s}{'GFLOPs':>12s}{'GPU_mem (GB)':>14s}{'forward (ms)':>14s}{'backward (ms)':>14s}" print(
f"{'input':>24s}{'output':>24s}") f"{'Params':>12s}{'GFLOPs':>12s}{'GPU_mem (GB)':>14s}{'forward (ms)':>14s}{'backward (ms)':>14s}"
f"{'input':>24s}{'output':>24s}"
)
for x in input if isinstance(input, list) else [input]: for x in input if isinstance(input, list) else [input]:
x = x.to(device) x = x.to(device)
x.requires_grad = True x.requires_grad = True
for m in ops if isinstance(ops, list) else [ops]: for m in ops if isinstance(ops, list) else [ops]:
m = m.to(device) if hasattr(m, 'to') else m # device m = m.to(device) if hasattr(m, "to") else m # device
m = m.half() if hasattr(m, 'half') and isinstance(x, torch.Tensor) and x.dtype is torch.float16 else m m = m.half() if hasattr(m, "half") and isinstance(x, torch.Tensor) and x.dtype is torch.float16 else m
tf, tb, t = 0, 0, [0, 0, 0] # dt forward, backward tf, tb, t = 0, 0, [0, 0, 0] # dt forward, backward
try: try:
flops = thop.profile(m, inputs=(x, ), verbose=False)[0] / 1E9 * 2 # GFLOPs flops = thop.profile(m, inputs=(x,), verbose=False)[0] / 1e9 * 2 # GFLOPs
except Exception: except Exception:
flops = 0 flops = 0
@@ -184,13 +187,13 @@ def profile(input, ops, n=10, device=None):
t[2] = time_sync() t[2] = time_sync()
except Exception: # no backward method except Exception: # no backward method
# print(e) # for debug # print(e) # for debug
t[2] = float('nan') t[2] = float("nan")
tf += (t[1] - t[0]) * 1000 / n # ms per op forward tf += (t[1] - t[0]) * 1000 / n # ms per op forward
tb += (t[2] - t[1]) * 1000 / n # ms per op backward tb += (t[2] - t[1]) * 1000 / n # ms per op backward
mem = torch.cuda.memory_reserved() / 1E9 if torch.cuda.is_available() else 0 # (GB) mem = torch.cuda.memory_reserved() / 1e9 if torch.cuda.is_available() else 0 # (GB)
s_in, s_out = (tuple(x.shape) if isinstance(x, torch.Tensor) else 'list' for x in (x, y)) # shapes s_in, s_out = (tuple(x.shape) if isinstance(x, torch.Tensor) else "list" for x in (x, y)) # shapes
p = sum(x.numel() for x in m.parameters()) if isinstance(m, nn.Module) else 0 # parameters p = sum(x.numel() for x in m.parameters()) if isinstance(m, nn.Module) else 0 # parameters
print(f'{p:12}{flops:12.4g}{mem:>14.3f}{tf:14.4g}{tb:14.4g}{str(s_in):>24s}{str(s_out):>24s}') print(f"{p:12}{flops:12.4g}{mem:>14.3f}{tf:14.4g}{tb:14.4g}{str(s_in):>24s}{str(s_out):>24s}")
results.append([p, flops, mem, tf, tb, s_in, s_out]) results.append([p, flops, mem, tf, tb, s_in, s_out])
except Exception as e: except Exception as e:
print(e) print(e)
@@ -238,23 +241,30 @@ def sparsity(model):
def prune(model, amount=0.3): def prune(model, amount=0.3):
# Prune model to requested global sparsity # Prune model to requested global sparsity
import torch.nn.utils.prune as prune import torch.nn.utils.prune as prune
for name, m in model.named_modules(): for name, m in model.named_modules():
if isinstance(m, nn.Conv2d): if isinstance(m, nn.Conv2d):
prune.l1_unstructured(m, name='weight', amount=amount) # prune prune.l1_unstructured(m, name="weight", amount=amount) # prune
prune.remove(m, 'weight') # make permanent prune.remove(m, "weight") # make permanent
LOGGER.info(f'Model pruned to {sparsity(model):.3g} global sparsity') LOGGER.info(f"Model pruned to {sparsity(model):.3g} global sparsity")
def fuse_conv_and_bn(conv, bn): def fuse_conv_and_bn(conv, bn):
# Fuse Conv2d() and BatchNorm2d() layers https://tehnokv.com/posts/fusing-batchnorm-and-conv/ # Fuse Conv2d() and BatchNorm2d() layers https://tehnokv.com/posts/fusing-batchnorm-and-conv/
fusedconv = nn.Conv2d(conv.in_channels, fusedconv = (
conv.out_channels, nn.Conv2d(
kernel_size=conv.kernel_size, conv.in_channels,
stride=conv.stride, conv.out_channels,
padding=conv.padding, kernel_size=conv.kernel_size,
dilation=conv.dilation, stride=conv.stride,
groups=conv.groups, padding=conv.padding,
bias=True).requires_grad_(False).to(conv.weight.device) dilation=conv.dilation,
groups=conv.groups,
bias=True,
)
.requires_grad_(False)
.to(conv.weight.device)
)
# Prepare filters # Prepare filters
w_conv = conv.weight.clone().view(conv.out_channels, -1) w_conv = conv.weight.clone().view(conv.out_channels, -1)
@@ -276,22 +286,24 @@ def model_info(model, verbose=False, imgsz=640):
if verbose: if verbose:
print(f"{'layer':>5} {'name':>40} {'gradient':>9} {'parameters':>12} {'shape':>20} {'mu':>10} {'sigma':>10}") print(f"{'layer':>5} {'name':>40} {'gradient':>9} {'parameters':>12} {'shape':>20} {'mu':>10} {'sigma':>10}")
for i, (name, p) in enumerate(model.named_parameters()): for i, (name, p) in enumerate(model.named_parameters()):
name = name.replace('module_list.', '') name = name.replace("module_list.", "")
print('%5g %40s %9s %12g %20s %10.3g %10.3g' % print(
(i, name, p.requires_grad, p.numel(), list(p.shape), p.mean(), p.std())) "%5g %40s %9s %12g %20s %10.3g %10.3g"
% (i, name, p.requires_grad, p.numel(), list(p.shape), p.mean(), p.std())
)
try: # FLOPs try: # FLOPs
p = next(model.parameters()) p = next(model.parameters())
stride = max(int(model.stride.max()), 32) if hasattr(model, 'stride') else 32 # max stride stride = max(int(model.stride.max()), 32) if hasattr(model, "stride") else 32 # max stride
im = torch.empty((1, p.shape[1], stride, stride), device=p.device) # input image in BCHW format im = torch.empty((1, p.shape[1], stride, stride), device=p.device) # input image in BCHW format
flops = thop.profile(deepcopy(model), inputs=(im, ), verbose=False)[0] / 1E9 * 2 # stride GFLOPs flops = thop.profile(deepcopy(model), inputs=(im,), verbose=False)[0] / 1e9 * 2 # stride GFLOPs
imgsz = imgsz if isinstance(imgsz, list) else [imgsz, imgsz] # expand if int/float imgsz = imgsz if isinstance(imgsz, list) else [imgsz, imgsz] # expand if int/float
fs = f', {flops * imgsz[0] / stride * imgsz[1] / stride:.1f} GFLOPs' # 640x640 GFLOPs fs = f", {flops * imgsz[0] / stride * imgsz[1] / stride:.1f} GFLOPs" # 640x640 GFLOPs
except Exception: except Exception:
fs = '' fs = ""
name = Path(model.yaml_file).stem.replace('yolov5', 'YOLOv5') if hasattr(model, 'yaml_file') else 'Model' name = Path(model.yaml_file).stem.replace("yolov5", "YOLOv5") if hasattr(model, "yaml_file") else "Model"
LOGGER.info(f'{name} summary: {len(list(model.modules()))} layers, {n_p} parameters, {n_g} gradients{fs}') LOGGER.info(f"{name} summary: {len(list(model.modules()))} layers, {n_p} parameters, {n_g} gradients{fs}")
def scale_img(img, ratio=1.0, same_shape=False, gs=32): # img(16,3,256,416) def scale_img(img, ratio=1.0, same_shape=False, gs=32): # img(16,3,256,416)
@@ -300,7 +312,7 @@ def scale_img(img, ratio=1.0, same_shape=False, gs=32): # img(16,3,256,416)
return img return img
h, w = img.shape[2:] h, w = img.shape[2:]
s = (int(h * ratio), int(w * ratio)) # new size s = (int(h * ratio), int(w * ratio)) # new size
img = F.interpolate(img, size=s, mode='bilinear', align_corners=False) # resize img = F.interpolate(img, size=s, mode="bilinear", align_corners=False) # resize
if not same_shape: # pad/crop img if not same_shape: # pad/crop img
h, w = (math.ceil(x * ratio / gs) * gs for x in (h, w)) h, w = (math.ceil(x * ratio / gs) * gs for x in (h, w))
return F.pad(img, [0, w - s[1], 0, h - s[0]], value=0.447) # value = imagenet mean return F.pad(img, [0, w - s[1], 0, h - s[0]], value=0.447) # value = imagenet mean
@@ -309,72 +321,76 @@ def scale_img(img, ratio=1.0, same_shape=False, gs=32): # img(16,3,256,416)
def copy_attr(a, b, include=(), exclude=()): def copy_attr(a, b, include=(), exclude=()):
# Copy attributes from b to a, options to only include [...] and to exclude [...] # Copy attributes from b to a, options to only include [...] and to exclude [...]
for k, v in b.__dict__.items(): for k, v in b.__dict__.items():
if (len(include) and k not in include) or k.startswith('_') or k in exclude: if (len(include) and k not in include) or k.startswith("_") or k in exclude:
continue continue
else: else:
setattr(a, k, v) setattr(a, k, v)
def smart_optimizer(model, name='Adam', lr=0.001, momentum=0.9, decay=1e-5): def smart_optimizer(model, name="Adam", lr=0.001, momentum=0.9, decay=1e-5):
# YOLOv5 3-param group optimizer: 0) weights with decay, 1) weights no decay, 2) biases no decay # YOLOv5 3-param group optimizer: 0) weights with decay, 1) weights no decay, 2) biases no decay
g = [], [], [] # optimizer parameter groups g = [], [], [] # optimizer parameter groups
bn = tuple(v for k, v in nn.__dict__.items() if 'Norm' in k) # normalization layers, i.e. BatchNorm2d() bn = tuple(v for k, v in nn.__dict__.items() if "Norm" in k) # normalization layers, i.e. BatchNorm2d()
for v in model.modules(): for v in model.modules():
for p_name, p in v.named_parameters(recurse=0): for p_name, p in v.named_parameters(recurse=0):
if p_name == 'bias': # bias (no decay) if p_name == "bias": # bias (no decay)
g[2].append(p) g[2].append(p)
elif p_name == 'weight' and isinstance(v, bn): # weight (no decay) elif p_name == "weight" and isinstance(v, bn): # weight (no decay)
g[1].append(p) g[1].append(p)
else: else:
g[0].append(p) # weight (with decay) g[0].append(p) # weight (with decay)
if name == 'Adam': if name == "Adam":
optimizer = torch.optim.Adam(g[2], lr=lr, betas=(momentum, 0.999)) # adjust beta1 to momentum optimizer = torch.optim.Adam(g[2], lr=lr, betas=(momentum, 0.999)) # adjust beta1 to momentum
elif name == 'AdamW': elif name == "AdamW":
optimizer = torch.optim.AdamW(g[2], lr=lr, betas=(momentum, 0.999), weight_decay=0.0) optimizer = torch.optim.AdamW(g[2], lr=lr, betas=(momentum, 0.999), weight_decay=0.0)
elif name == 'RMSProp': elif name == "RMSProp":
optimizer = torch.optim.RMSprop(g[2], lr=lr, momentum=momentum) optimizer = torch.optim.RMSprop(g[2], lr=lr, momentum=momentum)
elif name == 'SGD': elif name == "SGD":
optimizer = torch.optim.SGD(g[2], lr=lr, momentum=momentum, nesterov=True) optimizer = torch.optim.SGD(g[2], lr=lr, momentum=momentum, nesterov=True)
else: else:
raise NotImplementedError(f'Optimizer {name} not implemented.') raise NotImplementedError(f"Optimizer {name} not implemented.")
optimizer.add_param_group({'params': g[0], 'weight_decay': decay}) # add g0 with weight_decay optimizer.add_param_group({"params": g[0], "weight_decay": decay}) # add g0 with weight_decay
optimizer.add_param_group({'params': g[1], 'weight_decay': 0.0}) # add g1 (BatchNorm2d weights) optimizer.add_param_group({"params": g[1], "weight_decay": 0.0}) # add g1 (BatchNorm2d weights)
LOGGER.info(f"{colorstr('optimizer:')} {type(optimizer).__name__}(lr={lr}) with parameter groups " LOGGER.info(
f'{len(g[1])} weight(decay=0.0), {len(g[0])} weight(decay={decay}), {len(g[2])} bias') f"{colorstr('optimizer:')} {type(optimizer).__name__}(lr={lr}) with parameter groups "
f'{len(g[1])} weight(decay=0.0), {len(g[0])} weight(decay={decay}), {len(g[2])} bias'
)
return optimizer return optimizer
def smart_hub_load(repo='ultralytics/yolov5', model='yolov5s', **kwargs): def smart_hub_load(repo="ultralytics/yolov5", model="yolov5s", **kwargs):
# YOLOv5 torch.hub.load() wrapper with smart error/issue handling # YOLOv5 torch.hub.load() wrapper with smart error/issue handling
if check_version(torch.__version__, '1.9.1'): if check_version(torch.__version__, "1.9.1"):
kwargs['skip_validation'] = True # validation causes GitHub API rate limit errors kwargs["skip_validation"] = True # validation causes GitHub API rate limit errors
if check_version(torch.__version__, '1.12.0'): if check_version(torch.__version__, "1.12.0"):
kwargs['trust_repo'] = True # argument required starting in torch 0.12 kwargs["trust_repo"] = True # argument required starting in torch 0.12
try: try:
return torch.hub.load(repo, model, **kwargs) return torch.hub.load(repo, model, **kwargs)
except Exception: except Exception:
return torch.hub.load(repo, model, force_reload=True, **kwargs) return torch.hub.load(repo, model, force_reload=True, **kwargs)
def smart_resume(ckpt, optimizer, ema=None, weights='yolov5s.pt', epochs=300, resume=True): def smart_resume(ckpt, optimizer, ema=None, weights="yolov5s.pt", epochs=300, resume=True):
# Resume training from a partially trained checkpoint # Resume training from a partially trained checkpoint
best_fitness = 0.0 best_fitness = 0.0
start_epoch = ckpt['epoch'] + 1 start_epoch = ckpt["epoch"] + 1
if ckpt['optimizer'] is not None: if ckpt["optimizer"] is not None:
optimizer.load_state_dict(ckpt['optimizer']) # optimizer optimizer.load_state_dict(ckpt["optimizer"]) # optimizer
best_fitness = ckpt['best_fitness'] best_fitness = ckpt["best_fitness"]
if ema and ckpt.get('ema'): if ema and ckpt.get("ema"):
ema.ema.load_state_dict(ckpt['ema'].float().state_dict()) # EMA ema.ema.load_state_dict(ckpt["ema"].float().state_dict()) # EMA
ema.updates = ckpt['updates'] ema.updates = ckpt["updates"]
if resume: if resume:
assert start_epoch > 0, f'{weights} training to {epochs} epochs is finished, nothing to resume.\n' \ assert start_epoch > 0, (
f"Start a new training without --resume, i.e. 'python train.py --weights {weights}'" f"{weights} training to {epochs} epochs is finished, nothing to resume.\n"
LOGGER.info(f'Resuming training from {weights} from epoch {start_epoch} to {epochs} total epochs') f"Start a new training without --resume, i.e. 'python train.py --weights {weights}'"
)
LOGGER.info(f"Resuming training from {weights} from epoch {start_epoch} to {epochs} total epochs")
if epochs < start_epoch: if epochs < start_epoch:
LOGGER.info(f"{weights} has been trained for {ckpt['epoch']} epochs. Fine-tuning for {epochs} more epochs.") LOGGER.info(f"{weights} has been trained for {ckpt['epoch']} epochs. Fine-tuning for {epochs} more epochs.")
epochs += ckpt['epoch'] # finetune additional epochs epochs += ckpt["epoch"] # finetune additional epochs
return best_fitness, start_epoch, epochs return best_fitness, start_epoch, epochs
@@ -383,7 +399,7 @@ class EarlyStopping:
def __init__(self, patience=30): def __init__(self, patience=30):
self.best_fitness = 0.0 # i.e. mAP self.best_fitness = 0.0 # i.e. mAP
self.best_epoch = 0 self.best_epoch = 0
self.patience = patience or float('inf') # epochs to wait after fitness stops improving to stop self.patience = patience or float("inf") # epochs to wait after fitness stops improving to stop
self.possible_stop = False # possible stop may occur next epoch self.possible_stop = False # possible stop may occur next epoch
def __call__(self, epoch, fitness): def __call__(self, epoch, fitness):
@@ -394,15 +410,17 @@ class EarlyStopping:
self.possible_stop = delta >= (self.patience - 1) # possible stop may occur next epoch self.possible_stop = delta >= (self.patience - 1) # possible stop may occur next epoch
stop = delta >= self.patience # stop training if patience exceeded stop = delta >= self.patience # stop training if patience exceeded
if stop: if stop:
LOGGER.info(f'Stopping training early as no improvement observed in last {self.patience} epochs. ' LOGGER.info(
f'Best results observed at epoch {self.best_epoch}, best model saved as best.pt.\n' f"Stopping training early as no improvement observed in last {self.patience} epochs. "
f'To update EarlyStopping(patience={self.patience}) pass a new patience value, ' f"Best results observed at epoch {self.best_epoch}, best model saved as best.pt.\n"
f'i.e. `python train.py --patience 300` or use `--patience 0` to disable EarlyStopping.') f"To update EarlyStopping(patience={self.patience}) pass a new patience value, "
f"i.e. `python train.py --patience 300` or use `--patience 0` to disable EarlyStopping."
)
return stop return stop
class ModelEMA: class ModelEMA:
""" Updated Exponential Moving Average (EMA) from https://github.com/rwightman/pytorch-image-models """Updated Exponential Moving Average (EMA) from https://github.com/rwightman/pytorch-image-models
Keeps a moving average of everything in the model state_dict (parameters and buffers) Keeps a moving average of everything in the model state_dict (parameters and buffers)
For EMA details see https://www.tensorflow.org/api_docs/python/tf/train/ExponentialMovingAverage For EMA details see https://www.tensorflow.org/api_docs/python/tf/train/ExponentialMovingAverage
""" """
@@ -427,6 +445,6 @@ class ModelEMA:
v += (1 - d) * msd[k].detach() v += (1 - d) * msd[k].detach()
# assert v.dtype == msd[k].dtype == torch.float32, f'{k}: EMA {v.dtype} and model {msd[k].dtype} must be FP32' # assert v.dtype == msd[k].dtype == torch.float32, f'{k}: EMA {v.dtype} and model {msd[k].dtype} must be FP32'
def update_attr(self, model, include=(), exclude=('process_group', 'reducer')): def update_attr(self, model, include=(), exclude=("process_group", "reducer")):
# Update EMA attributes # Update EMA attributes
copy_attr(self.ema, model, include, exclude) copy_attr(self.ema, model, include, exclude)
+24 -19
View File
@@ -1,6 +1,5 @@
# YOLOv5 🚀 by Ultralytics, AGPL-3.0 license # YOLOv5 🚀 by Ultralytics, AGPL-3.0 license
""" Utils to interact with the Triton Inference Server """Utils to interact with the Triton Inference Server."""
"""
import typing import typing
from urllib.parse import urlparse from urllib.parse import urlparse
@@ -9,9 +8,11 @@ import torch
class TritonRemoteModel: class TritonRemoteModel:
""" A wrapper over a model served by the Triton Inference Server. It can """
be configured to communicate over GRPC or HTTP. It accepts Torch Tensors A wrapper over a model served by the Triton Inference Server.
as input and returns them as outputs.
It can be configured to communicate over GRPC or HTTP. It accepts Torch Tensors as input and returns them as
outputs.
""" """
def __init__(self, url: str): def __init__(self, url: str):
@@ -21,7 +22,7 @@ class TritonRemoteModel:
""" """
parsed_url = urlparse(url) parsed_url = urlparse(url)
if parsed_url.scheme == 'grpc': if parsed_url.scheme == "grpc":
from tritonclient.grpc import InferenceServerClient, InferInput from tritonclient.grpc import InferenceServerClient, InferInput
self.client = InferenceServerClient(parsed_url.netloc) # Triton GRPC client self.client = InferenceServerClient(parsed_url.netloc) # Triton GRPC client
@@ -31,51 +32,55 @@ class TritonRemoteModel:
def create_input_placeholders() -> typing.List[InferInput]: def create_input_placeholders() -> typing.List[InferInput]:
return [ return [
InferInput(i['name'], [int(s) for s in i['shape']], i['datatype']) for i in self.metadata['inputs']] InferInput(i["name"], [int(s) for s in i["shape"]], i["datatype"]) for i in self.metadata["inputs"]
]
else: else:
from tritonclient.http import InferenceServerClient, InferInput from tritonclient.http import InferenceServerClient, InferInput
self.client = InferenceServerClient(parsed_url.netloc) # Triton HTTP client self.client = InferenceServerClient(parsed_url.netloc) # Triton HTTP client
model_repository = self.client.get_model_repository_index() model_repository = self.client.get_model_repository_index()
self.model_name = model_repository[0]['name'] self.model_name = model_repository[0]["name"]
self.metadata = self.client.get_model_metadata(self.model_name) self.metadata = self.client.get_model_metadata(self.model_name)
def create_input_placeholders() -> typing.List[InferInput]: def create_input_placeholders() -> typing.List[InferInput]:
return [ return [
InferInput(i['name'], [int(s) for s in i['shape']], i['datatype']) for i in self.metadata['inputs']] InferInput(i["name"], [int(s) for s in i["shape"]], i["datatype"]) for i in self.metadata["inputs"]
]
self._create_input_placeholders_fn = create_input_placeholders self._create_input_placeholders_fn = create_input_placeholders
@property @property
def runtime(self): def runtime(self):
"""Returns the model runtime""" """Returns the model runtime."""
return self.metadata.get('backend', self.metadata.get('platform')) return self.metadata.get("backend", self.metadata.get("platform"))
def __call__(self, *args, **kwargs) -> typing.Union[torch.Tensor, typing.Tuple[torch.Tensor, ...]]: def __call__(self, *args, **kwargs) -> typing.Union[torch.Tensor, typing.Tuple[torch.Tensor, ...]]:
""" Invokes the model. Parameters can be provided via args or kwargs. """
args, if provided, are assumed to match the order of inputs of the model. Invokes the model.
kwargs are matched with the model input names.
Parameters can be provided via args or kwargs. args, if provided, are assumed to match the order of inputs of
the model. kwargs are matched with the model input names.
""" """
inputs = self._create_inputs(*args, **kwargs) inputs = self._create_inputs(*args, **kwargs)
response = self.client.infer(model_name=self.model_name, inputs=inputs) response = self.client.infer(model_name=self.model_name, inputs=inputs)
result = [] result = []
for output in self.metadata['outputs']: for output in self.metadata["outputs"]:
tensor = torch.as_tensor(response.as_numpy(output['name'])) tensor = torch.as_tensor(response.as_numpy(output["name"]))
result.append(tensor) result.append(tensor)
return result[0] if len(result) == 1 else result return result[0] if len(result) == 1 else result
def _create_inputs(self, *args, **kwargs): def _create_inputs(self, *args, **kwargs):
args_len, kwargs_len = len(args), len(kwargs) args_len, kwargs_len = len(args), len(kwargs)
if not args_len and not kwargs_len: if not args_len and not kwargs_len:
raise RuntimeError('No inputs provided.') raise RuntimeError("No inputs provided.")
if args_len and kwargs_len: if args_len and kwargs_len:
raise RuntimeError('Cannot specify args and kwargs at the same time') raise RuntimeError("Cannot specify args and kwargs at the same time")
placeholders = self._create_input_placeholders_fn() placeholders = self._create_input_placeholders_fn()
if args_len: if args_len:
if args_len != len(placeholders): if args_len != len(placeholders):
raise RuntimeError(f'Expected {len(placeholders)} inputs, got {args_len}.') raise RuntimeError(f"Expected {len(placeholders)} inputs, got {args_len}.")
for input, value in zip(placeholders, args): for input, value in zip(placeholders, args):
input.set_data_from_numpy(value.cpu().numpy()) input.set_data_from_numpy(value.cpu().numpy())
else: else:
+143 -126
View File
@@ -1,6 +1,6 @@
# YOLOv5 🚀 by Ultralytics, AGPL-3.0 license # YOLOv5 🚀 by Ultralytics, AGPL-3.0 license
""" """
Validate a trained YOLOv5 detection model on a detection dataset Validate a trained YOLOv5 detection model on a detection dataset.
Usage: Usage:
$ python val.py --weights yolov5s.pt --data coco128.yaml --img 640 $ python val.py --weights yolov5s.pt --data coco128.yaml --img 640
@@ -39,9 +39,23 @@ ROOT = Path(os.path.relpath(ROOT, Path.cwd())) # relative
from models.common import DetectMultiBackend from models.common import DetectMultiBackend
from utils.callbacks import Callbacks from utils.callbacks import Callbacks
from utils.dataloaders import create_dataloader from utils.dataloaders import create_dataloader
from utils.general import (LOGGER, TQDM_BAR_FORMAT, Profile, check_dataset, check_img_size, check_requirements, from utils.general import (
check_yaml, coco80_to_coco91_class, colorstr, increment_path, non_max_suppression, LOGGER,
print_args, scale_boxes, xywh2xyxy, xyxy2xywh) TQDM_BAR_FORMAT,
Profile,
check_dataset,
check_img_size,
check_requirements,
check_yaml,
coco80_to_coco91_class,
colorstr,
increment_path,
non_max_suppression,
print_args,
scale_boxes,
xywh2xyxy,
xyxy2xywh,
)
from utils.metrics import ConfusionMatrix, ap_per_class, box_iou from utils.metrics import ConfusionMatrix, ap_per_class, box_iou
from utils.plots import output_to_target, plot_images, plot_val_study from utils.plots import output_to_target, plot_images, plot_val_study
from utils.torch_utils import select_device, smart_inference_mode from utils.torch_utils import select_device, smart_inference_mode
@@ -53,8 +67,8 @@ def save_one_txt(predn, save_conf, shape, file):
for *xyxy, conf, cls in predn.tolist(): for *xyxy, conf, cls in predn.tolist():
xywh = (xyxy2xywh(torch.tensor(xyxy).view(1, 4)) / gn).view(-1).tolist() # normalized xywh xywh = (xyxy2xywh(torch.tensor(xyxy).view(1, 4)) / gn).view(-1).tolist() # normalized xywh
line = (cls, *xywh, conf) if save_conf else (cls, *xywh) # label format line = (cls, *xywh, conf) if save_conf else (cls, *xywh) # label format
with open(file, 'a') as f: with open(file, "a") as f:
f.write(('%g ' * len(line)).rstrip() % line + '\n') f.write(("%g " * len(line)).rstrip() % line + "\n")
def save_one_json(predn, jdict, path, class_map): def save_one_json(predn, jdict, path, class_map):
@@ -63,11 +77,14 @@ def save_one_json(predn, jdict, path, class_map):
box = xyxy2xywh(predn[:, :4]) # xywh box = xyxy2xywh(predn[:, :4]) # xywh
box[:, :2] -= box[:, 2:] / 2 # xy center to top-left corner box[:, :2] -= box[:, 2:] / 2 # xy center to top-left corner
for p, b in zip(predn.tolist(), box.tolist()): for p, b in zip(predn.tolist(), box.tolist()):
jdict.append({ jdict.append(
'image_id': image_id, {
'category_id': class_map[int(p[5])], "image_id": image_id,
'bbox': [round(x, 3) for x in b], "category_id": class_map[int(p[5])],
'score': round(p[4], 5)}) "bbox": [round(x, 3) for x in b],
"score": round(p[4], 5),
}
)
def process_batch(detections, labels, iouv): def process_batch(detections, labels, iouv):
@@ -98,47 +115,47 @@ def process_batch(detections, labels, iouv):
@smart_inference_mode() @smart_inference_mode()
def run( def run(
data, data,
weights=None, # model.pt path(s) weights=None, # model.pt path(s)
batch_size=32, # batch size batch_size=32, # batch size
imgsz=640, # inference size (pixels) imgsz=640, # inference size (pixels)
conf_thres=0.001, # confidence threshold conf_thres=0.001, # confidence threshold
iou_thres=0.6, # NMS IoU threshold iou_thres=0.6, # NMS IoU threshold
max_det=300, # maximum detections per image max_det=300, # maximum detections per image
task='val', # train, val, test, speed or study task="val", # train, val, test, speed or study
device='', # cuda device, i.e. 0 or 0,1,2,3 or cpu device="", # cuda device, i.e. 0 or 0,1,2,3 or cpu
workers=8, # max dataloader workers (per RANK in DDP mode) workers=8, # max dataloader workers (per RANK in DDP mode)
single_cls=False, # treat as single-class dataset single_cls=False, # treat as single-class dataset
augment=False, # augmented inference augment=False, # augmented inference
verbose=False, # verbose output verbose=False, # verbose output
save_txt=False, # save results to *.txt save_txt=False, # save results to *.txt
save_hybrid=False, # save label+prediction hybrid results to *.txt save_hybrid=False, # save label+prediction hybrid results to *.txt
save_conf=False, # save confidences in --save-txt labels save_conf=False, # save confidences in --save-txt labels
save_json=False, # save a COCO-JSON results file save_json=False, # save a COCO-JSON results file
project=ROOT / 'runs/val', # save to project/name project=ROOT / "runs/val", # save to project/name
name='exp', # save to project/name name="exp", # save to project/name
exist_ok=False, # existing project/name ok, do not increment exist_ok=False, # existing project/name ok, do not increment
half=True, # use FP16 half-precision inference half=True, # use FP16 half-precision inference
dnn=False, # use OpenCV DNN for ONNX inference dnn=False, # use OpenCV DNN for ONNX inference
model=None, model=None,
dataloader=None, dataloader=None,
save_dir=Path(''), save_dir=Path(""),
plots=True, plots=True,
callbacks=Callbacks(), callbacks=Callbacks(),
compute_loss=None, compute_loss=None,
): ):
# Initialize/load model and set device # Initialize/load model and set device
training = model is not None training = model is not None
if training: # called by train.py if training: # called by train.py
device, pt, jit, engine = next(model.parameters()).device, True, False, False # get model device, PyTorch model device, pt, jit, engine = next(model.parameters()).device, True, False, False # get model device, PyTorch model
half &= device.type != 'cpu' # half precision only supported on CUDA half &= device.type != "cpu" # half precision only supported on CUDA
model.half() if half else model.float() model.half() if half else model.float()
else: # called directly else: # called directly
device = select_device(device, batch_size=batch_size) device = select_device(device, batch_size=batch_size)
# Directories # Directories
save_dir = increment_path(Path(project) / name, exist_ok=exist_ok) # increment run save_dir = increment_path(Path(project) / name, exist_ok=exist_ok) # increment run
(save_dir / 'labels' if save_txt else save_dir).mkdir(parents=True, exist_ok=True) # make dir (save_dir / "labels" if save_txt else save_dir).mkdir(parents=True, exist_ok=True) # make dir
# Load model # Load model
model = DetectMultiBackend(weights, device=device, dnn=dnn, data=data, fp16=half) model = DetectMultiBackend(weights, device=device, dnn=dnn, data=data, fp16=half)
@@ -151,16 +168,16 @@ def run(
device = model.device device = model.device
if not (pt or jit): if not (pt or jit):
batch_size = 1 # export.py models default to batch-size 1 batch_size = 1 # export.py models default to batch-size 1
LOGGER.info(f'Forcing --batch-size 1 square inference (1,3,{imgsz},{imgsz}) for non-PyTorch models') LOGGER.info(f"Forcing --batch-size 1 square inference (1,3,{imgsz},{imgsz}) for non-PyTorch models")
# Data # Data
data = check_dataset(data) # check data = check_dataset(data) # check
# Configure # Configure
model.eval() model.eval()
cuda = device.type != 'cpu' cuda = device.type != "cpu"
is_coco = isinstance(data.get('val'), str) and data['val'].endswith(f'coco{os.sep}val2017.txt') # COCO dataset is_coco = isinstance(data.get("val"), str) and data["val"].endswith(f"coco{os.sep}val2017.txt") # COCO dataset
nc = 1 if single_cls else int(data['nc']) # number of classes nc = 1 if single_cls else int(data["nc"]) # number of classes
iouv = torch.linspace(0.5, 0.95, 10, device=device) # iou vector for mAP@0.5:0.95 iouv = torch.linspace(0.5, 0.95, 10, device=device) # iou vector for mAP@0.5:0.95
niou = iouv.numel() niou = iouv.numel()
@@ -168,36 +185,40 @@ def run(
if not training: if not training:
if pt and not single_cls: # check --weights are trained on --data if pt and not single_cls: # check --weights are trained on --data
ncm = model.model.nc ncm = model.model.nc
assert ncm == nc, f'{weights} ({ncm} classes) trained on different --data than what you passed ({nc} ' \ assert ncm == nc, (
f'classes). Pass correct combination of --weights and --data that are trained together.' f"{weights} ({ncm} classes) trained on different --data than what you passed ({nc} "
f"classes). Pass correct combination of --weights and --data that are trained together."
)
model.warmup(imgsz=(1 if pt else batch_size, 3, imgsz, imgsz)) # warmup model.warmup(imgsz=(1 if pt else batch_size, 3, imgsz, imgsz)) # warmup
pad, rect = (0.0, False) if task == 'speed' else (0.5, pt) # square inference for benchmarks pad, rect = (0.0, False) if task == "speed" else (0.5, pt) # square inference for benchmarks
task = task if task in ('train', 'val', 'test') else 'val' # path to train/val/test images task = task if task in ("train", "val", "test") else "val" # path to train/val/test images
dataloader = create_dataloader(data[task], dataloader = create_dataloader(
imgsz, data[task],
batch_size, imgsz,
stride, batch_size,
single_cls, stride,
pad=pad, single_cls,
rect=rect, pad=pad,
workers=workers, rect=rect,
prefix=colorstr(f'{task}: '))[0] workers=workers,
prefix=colorstr(f"{task}: "),
)[0]
seen = 0 seen = 0
confusion_matrix = ConfusionMatrix(nc=nc) confusion_matrix = ConfusionMatrix(nc=nc)
names = model.names if hasattr(model, 'names') else model.module.names # get class names names = model.names if hasattr(model, "names") else model.module.names # get class names
if isinstance(names, (list, tuple)): # old format if isinstance(names, (list, tuple)): # old format
names = dict(enumerate(names)) names = dict(enumerate(names))
class_map = coco80_to_coco91_class() if is_coco else list(range(1000)) class_map = coco80_to_coco91_class() if is_coco else list(range(1000))
s = ('%22s' + '%11s' * 6) % ('Class', 'Images', 'Instances', 'P', 'R', 'mAP50', 'mAP50-95') s = ("%22s" + "%11s" * 6) % ("Class", "Images", "Instances", "P", "R", "mAP50", "mAP50-95")
tp, fp, p, r, f1, mp, mr, map50, ap50, map = 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 tp, fp, p, r, f1, mp, mr, map50, ap50, map = 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
dt = Profile(device=device), Profile(device=device), Profile(device=device) # profiling times dt = Profile(device=device), Profile(device=device), Profile(device=device) # profiling times
loss = torch.zeros(3, device=device) loss = torch.zeros(3, device=device)
jdict, stats, ap, ap_class = [], [], [], [] jdict, stats, ap, ap_class = [], [], [], []
callbacks.run('on_val_start') callbacks.run("on_val_start")
pbar = tqdm(dataloader, desc=s, bar_format=TQDM_BAR_FORMAT) # progress bar pbar = tqdm(dataloader, desc=s, bar_format=TQDM_BAR_FORMAT) # progress bar
for batch_i, (im, targets, paths, shapes) in enumerate(pbar): for batch_i, (im, targets, paths, shapes) in enumerate(pbar):
callbacks.run('on_val_batch_start') callbacks.run("on_val_batch_start")
with dt[0]: with dt[0]:
if cuda: if cuda:
im = im.to(device, non_blocking=True) im = im.to(device, non_blocking=True)
@@ -218,13 +239,9 @@ def run(
targets[:, 2:] *= torch.tensor((width, height, width, height), device=device) # to pixels targets[:, 2:] *= torch.tensor((width, height, width, height), device=device) # to pixels
lb = [targets[targets[:, 0] == i, 1:] for i in range(nb)] if save_hybrid else [] # for autolabelling lb = [targets[targets[:, 0] == i, 1:] for i in range(nb)] if save_hybrid else [] # for autolabelling
with dt[2]: with dt[2]:
preds = non_max_suppression(preds, preds = non_max_suppression(
conf_thres, preds, conf_thres, iou_thres, labels=lb, multi_label=True, agnostic=single_cls, max_det=max_det
iou_thres, )
labels=lb,
multi_label=True,
agnostic=single_cls,
max_det=max_det)
# Metrics # Metrics
for si, pred in enumerate(preds): for si, pred in enumerate(preds):
@@ -259,18 +276,18 @@ def run(
# Save/log # Save/log
if save_txt: if save_txt:
(save_dir / 'labels').mkdir(parents=True, exist_ok=True) (save_dir / "labels").mkdir(parents=True, exist_ok=True)
save_one_txt(predn, save_conf, shape, file=save_dir / 'labels' / f'{path.stem}.txt') save_one_txt(predn, save_conf, shape, file=save_dir / "labels" / f"{path.stem}.txt")
if save_json: if save_json:
save_one_json(predn, jdict, path, class_map) # append to COCO-JSON dictionary save_one_json(predn, jdict, path, class_map) # append to COCO-JSON dictionary
callbacks.run('on_val_image_end', pred, predn, path, names, im[si]) callbacks.run("on_val_image_end", pred, predn, path, names, im[si])
# Plot images # Plot images
if plots and batch_i < 3: if plots and batch_i < 3:
plot_images(im, targets, paths, save_dir / f'val_batch{batch_i}_labels.jpg', names) # labels plot_images(im, targets, paths, save_dir / f"val_batch{batch_i}_labels.jpg", names) # labels
plot_images(im, output_to_target(preds), paths, save_dir / f'val_batch{batch_i}_pred.jpg', names) # pred plot_images(im, output_to_target(preds), paths, save_dir / f"val_batch{batch_i}_pred.jpg", names) # pred
callbacks.run('on_val_batch_end', batch_i, im, targets, paths, shapes, preds) callbacks.run("on_val_batch_end", batch_i, im, targets, paths, shapes, preds)
# Compute metrics # Compute metrics
stats = [torch.cat(x, 0).cpu().numpy() for x in zip(*stats)] # to numpy stats = [torch.cat(x, 0).cpu().numpy() for x in zip(*stats)] # to numpy
@@ -281,10 +298,10 @@ def run(
nt = np.bincount(stats[3].astype(int), minlength=nc) # number of targets per class nt = np.bincount(stats[3].astype(int), minlength=nc) # number of targets per class
# Print results # Print results
pf = '%22s' + '%11i' * 2 + '%11.3g' * 4 # print format pf = "%22s" + "%11i" * 2 + "%11.3g" * 4 # print format
LOGGER.info(pf % ('all', seen, nt.sum(), mp, mr, map50, map)) LOGGER.info(pf % ("all", seen, nt.sum(), mp, mr, map50, map))
if nt.sum() == 0: if nt.sum() == 0:
LOGGER.warning(f'WARNING ⚠️ no labels found in {task} set, can not compute metrics without labels') LOGGER.warning(f"WARNING ⚠️ no labels found in {task} set, can not compute metrics without labels")
# Print results per class # Print results per class
if (verbose or (nc < 50 and not training)) and nc > 1 and len(stats): if (verbose or (nc < 50 and not training)) and nc > 1 and len(stats):
@@ -292,35 +309,35 @@ def run(
LOGGER.info(pf % (names[c], seen, nt[c], p[i], r[i], ap50[i], ap[i])) LOGGER.info(pf % (names[c], seen, nt[c], p[i], r[i], ap50[i], ap[i]))
# Print speeds # Print speeds
t = tuple(x.t / seen * 1E3 for x in dt) # speeds per image t = tuple(x.t / seen * 1e3 for x in dt) # speeds per image
if not training: if not training:
shape = (batch_size, 3, imgsz, imgsz) shape = (batch_size, 3, imgsz, imgsz)
LOGGER.info(f'Speed: %.1fms pre-process, %.1fms inference, %.1fms NMS per image at shape {shape}' % t) LOGGER.info(f"Speed: %.1fms pre-process, %.1fms inference, %.1fms NMS per image at shape {shape}" % t)
# Plots # Plots
if plots: if plots:
confusion_matrix.plot(save_dir=save_dir, names=list(names.values())) confusion_matrix.plot(save_dir=save_dir, names=list(names.values()))
callbacks.run('on_val_end', nt, tp, fp, p, r, f1, ap, ap50, ap_class, confusion_matrix) callbacks.run("on_val_end", nt, tp, fp, p, r, f1, ap, ap50, ap_class, confusion_matrix)
# Save JSON # Save JSON
if save_json and len(jdict): if save_json and len(jdict):
w = Path(weights[0] if isinstance(weights, list) else weights).stem if weights is not None else '' # weights w = Path(weights[0] if isinstance(weights, list) else weights).stem if weights is not None else "" # weights
anno_json = str(Path('../datasets/coco/annotations/instances_val2017.json')) # annotations anno_json = str(Path("../datasets/coco/annotations/instances_val2017.json")) # annotations
if not os.path.exists(anno_json): if not os.path.exists(anno_json):
anno_json = os.path.join(data['path'], 'annotations', 'instances_val2017.json') anno_json = os.path.join(data["path"], "annotations", "instances_val2017.json")
pred_json = str(save_dir / f'{w}_predictions.json') # predictions pred_json = str(save_dir / f"{w}_predictions.json") # predictions
LOGGER.info(f'\nEvaluating pycocotools mAP... saving {pred_json}...') LOGGER.info(f"\nEvaluating pycocotools mAP... saving {pred_json}...")
with open(pred_json, 'w') as f: with open(pred_json, "w") as f:
json.dump(jdict, f) json.dump(jdict, f)
try: # https://github.com/cocodataset/cocoapi/blob/master/PythonAPI/pycocoEvalDemo.ipynb try: # https://github.com/cocodataset/cocoapi/blob/master/PythonAPI/pycocoEvalDemo.ipynb
check_requirements('pycocotools>=2.0.6') check_requirements("pycocotools>=2.0.6")
from pycocotools.coco import COCO from pycocotools.coco import COCO
from pycocotools.cocoeval import COCOeval from pycocotools.cocoeval import COCOeval
anno = COCO(anno_json) # init annotations api anno = COCO(anno_json) # init annotations api
pred = anno.loadRes(pred_json) # init predictions api pred = anno.loadRes(pred_json) # init predictions api
eval = COCOeval(anno, pred, 'bbox') eval = COCOeval(anno, pred, "bbox")
if is_coco: if is_coco:
eval.params.imgIds = [int(Path(x).stem) for x in dataloader.dataset.im_files] # image IDs to evaluate eval.params.imgIds = [int(Path(x).stem) for x in dataloader.dataset.im_files] # image IDs to evaluate
eval.evaluate() eval.evaluate()
@@ -328,12 +345,12 @@ def run(
eval.summarize() eval.summarize()
map, map50 = eval.stats[:2] # update results (mAP@0.5:0.95, mAP@0.5) map, map50 = eval.stats[:2] # update results (mAP@0.5:0.95, mAP@0.5)
except Exception as e: except Exception as e:
LOGGER.info(f'pycocotools unable to run: {e}') LOGGER.info(f"pycocotools unable to run: {e}")
# Return results # Return results
model.float() # for training model.float() # for training
if not training: if not training:
s = f"\n{len(list(save_dir.glob('labels/*.txt')))} labels saved to {save_dir / 'labels'}" if save_txt else '' s = f"\n{len(list(save_dir.glob('labels/*.txt')))} labels saved to {save_dir / 'labels'}" if save_txt else ""
LOGGER.info(f"Results saved to {colorstr('bold', save_dir)}{s}") LOGGER.info(f"Results saved to {colorstr('bold', save_dir)}{s}")
maps = np.zeros(nc) + map maps = np.zeros(nc) + map
for i, c in enumerate(ap_class): for i, c in enumerate(ap_class):
@@ -343,71 +360,71 @@ def run(
def parse_opt(): def parse_opt():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('--data', type=str, default=ROOT / 'data/coco128.yaml', help='dataset.yaml path') parser.add_argument("--data", type=str, default=ROOT / "data/coco128.yaml", help="dataset.yaml path")
parser.add_argument('--weights', nargs='+', type=str, default=ROOT / 'yolov5s.pt', help='model path(s)') parser.add_argument("--weights", nargs="+", type=str, default=ROOT / "yolov5s.pt", help="model path(s)")
parser.add_argument('--batch-size', type=int, default=32, help='batch size') parser.add_argument("--batch-size", type=int, default=32, help="batch size")
parser.add_argument('--imgsz', '--img', '--img-size', type=int, default=640, help='inference size (pixels)') parser.add_argument("--imgsz", "--img", "--img-size", type=int, default=640, help="inference size (pixels)")
parser.add_argument('--conf-thres', type=float, default=0.001, help='confidence threshold') parser.add_argument("--conf-thres", type=float, default=0.001, help="confidence threshold")
parser.add_argument('--iou-thres', type=float, default=0.6, help='NMS IoU threshold') parser.add_argument("--iou-thres", type=float, default=0.6, help="NMS IoU threshold")
parser.add_argument('--max-det', type=int, default=300, help='maximum detections per image') parser.add_argument("--max-det", type=int, default=300, help="maximum detections per image")
parser.add_argument('--task', default='val', help='train, val, test, speed or study') parser.add_argument("--task", default="val", help="train, val, test, speed or study")
parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu') parser.add_argument("--device", default="", help="cuda device, i.e. 0 or 0,1,2,3 or cpu")
parser.add_argument('--workers', type=int, default=8, help='max dataloader workers (per RANK in DDP mode)') parser.add_argument("--workers", type=int, default=8, help="max dataloader workers (per RANK in DDP mode)")
parser.add_argument('--single-cls', action='store_true', help='treat as single-class dataset') parser.add_argument("--single-cls", action="store_true", help="treat as single-class dataset")
parser.add_argument('--augment', action='store_true', help='augmented inference') parser.add_argument("--augment", action="store_true", help="augmented inference")
parser.add_argument('--verbose', action='store_true', help='report mAP by class') parser.add_argument("--verbose", action="store_true", help="report mAP by class")
parser.add_argument('--save-txt', action='store_true', help='save results to *.txt') parser.add_argument("--save-txt", action="store_true", help="save results to *.txt")
parser.add_argument('--save-hybrid', action='store_true', help='save label+prediction hybrid results to *.txt') parser.add_argument("--save-hybrid", action="store_true", help="save label+prediction hybrid results to *.txt")
parser.add_argument('--save-conf', action='store_true', help='save confidences in --save-txt labels') parser.add_argument("--save-conf", action="store_true", help="save confidences in --save-txt labels")
parser.add_argument('--save-json', action='store_true', help='save a COCO-JSON results file') parser.add_argument("--save-json", action="store_true", help="save a COCO-JSON results file")
parser.add_argument('--project', default=ROOT / 'runs/val', help='save to project/name') parser.add_argument("--project", default=ROOT / "runs/val", help="save to project/name")
parser.add_argument('--name', default='exp', help='save to project/name') parser.add_argument("--name", default="exp", help="save to project/name")
parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment') parser.add_argument("--exist-ok", action="store_true", help="existing project/name ok, do not increment")
parser.add_argument('--half', action='store_true', help='use FP16 half-precision inference') parser.add_argument("--half", action="store_true", help="use FP16 half-precision inference")
parser.add_argument('--dnn', action='store_true', help='use OpenCV DNN for ONNX inference') parser.add_argument("--dnn", action="store_true", help="use OpenCV DNN for ONNX inference")
opt = parser.parse_args() opt = parser.parse_args()
opt.data = check_yaml(opt.data) # check YAML opt.data = check_yaml(opt.data) # check YAML
opt.save_json |= opt.data.endswith('coco.yaml') opt.save_json |= opt.data.endswith("coco.yaml")
opt.save_txt |= opt.save_hybrid opt.save_txt |= opt.save_hybrid
print_args(vars(opt)) print_args(vars(opt))
return opt return opt
def main(opt): def main(opt):
check_requirements(ROOT / 'requirements.txt', exclude=('tensorboard', 'thop')) check_requirements(ROOT / "requirements.txt", exclude=("tensorboard", "thop"))
if opt.task in ('train', 'val', 'test'): # run normally if opt.task in ("train", "val", "test"): # run normally
if opt.conf_thres > 0.001: # https://github.com/ultralytics/yolov5/issues/1466 if opt.conf_thres > 0.001: # https://github.com/ultralytics/yolov5/issues/1466
LOGGER.info(f'WARNING ⚠️ confidence threshold {opt.conf_thres} > 0.001 produces invalid results') LOGGER.info(f"WARNING ⚠️ confidence threshold {opt.conf_thres} > 0.001 produces invalid results")
if opt.save_hybrid: if opt.save_hybrid:
LOGGER.info('WARNING ⚠️ --save-hybrid will return high mAP from hybrid labels, not from predictions alone') LOGGER.info("WARNING ⚠️ --save-hybrid will return high mAP from hybrid labels, not from predictions alone")
run(**vars(opt)) run(**vars(opt))
else: else:
weights = opt.weights if isinstance(opt.weights, list) else [opt.weights] weights = opt.weights if isinstance(opt.weights, list) else [opt.weights]
opt.half = torch.cuda.is_available() and opt.device != 'cpu' # FP16 for fastest results opt.half = torch.cuda.is_available() and opt.device != "cpu" # FP16 for fastest results
if opt.task == 'speed': # speed benchmarks if opt.task == "speed": # speed benchmarks
# python val.py --task speed --data coco.yaml --batch 1 --weights yolov5n.pt yolov5s.pt... # python val.py --task speed --data coco.yaml --batch 1 --weights yolov5n.pt yolov5s.pt...
opt.conf_thres, opt.iou_thres, opt.save_json = 0.25, 0.45, False opt.conf_thres, opt.iou_thres, opt.save_json = 0.25, 0.45, False
for opt.weights in weights: for opt.weights in weights:
run(**vars(opt), plots=False) run(**vars(opt), plots=False)
elif opt.task == 'study': # speed vs mAP benchmarks elif opt.task == "study": # speed vs mAP benchmarks
# python val.py --task study --data coco.yaml --iou 0.7 --weights yolov5n.pt yolov5s.pt... # python val.py --task study --data coco.yaml --iou 0.7 --weights yolov5n.pt yolov5s.pt...
for opt.weights in weights: for opt.weights in weights:
f = f'study_{Path(opt.data).stem}_{Path(opt.weights).stem}.txt' # filename to save to f = f"study_{Path(opt.data).stem}_{Path(opt.weights).stem}.txt" # filename to save to
x, y = list(range(256, 1536 + 128, 128)), [] # x axis (image sizes), y axis x, y = list(range(256, 1536 + 128, 128)), [] # x axis (image sizes), y axis
for opt.imgsz in x: # img-size for opt.imgsz in x: # img-size
LOGGER.info(f'\nRunning {f} --imgsz {opt.imgsz}...') LOGGER.info(f"\nRunning {f} --imgsz {opt.imgsz}...")
r, _, t = run(**vars(opt), plots=False) r, _, t = run(**vars(opt), plots=False)
y.append(r + t) # results and times y.append(r + t) # results and times
np.savetxt(f, y, fmt='%10.4g') # save np.savetxt(f, y, fmt="%10.4g") # save
subprocess.run(['zip', '-r', 'study.zip', 'study_*.txt']) subprocess.run(["zip", "-r", "study.zip", "study_*.txt"])
plot_val_study(x=x) # plot plot_val_study(x=x) # plot
else: else:
raise NotImplementedError(f'--task {opt.task} not in ("train", "val", "test", "speed", "study")') raise NotImplementedError(f'--task {opt.task} not in ("train", "val", "test", "speed", "study")')
if __name__ == '__main__': if __name__ == "__main__":
opt = parse_opt() opt = parse_opt()
main(opt) main(opt)