|
|
|
""" |
|
This module provides functionalities for hyperparameter tuning of the Ultralytics YOLO models for object detection, |
|
instance segmentation, image classification, pose estimation, and multi-object tracking. |
|
|
|
Hyperparameter tuning is the process of systematically searching for the optimal set of hyperparameters |
|
that yield the best model performance. This is particularly crucial in deep learning models like YOLO, |
|
where small changes in hyperparameters can lead to significant differences in model accuracy and efficiency. |
|
|
|
Example: |
|
Tune hyperparameters for YOLOv8n on COCO8 at imgsz=640 and epochs=30 for 300 tuning iterations. |
|
```python |
|
from ultralytics import YOLO |
|
|
|
model = YOLO('yolov8n.pt') |
|
model.tune(data='coco8.yaml', imgsz=640, epochs=100, iterations=10) |
|
``` |
|
""" |
|
import random |
|
import time |
|
|
|
import numpy as np |
|
|
|
from ultralytics import YOLO |
|
from ultralytics.cfg import get_cfg, get_save_dir |
|
from ultralytics.utils import DEFAULT_CFG, LOGGER, callbacks, colorstr, yaml_print, yaml_save |
|
|
|
|
|
class Tuner: |
|
""" |
|
Class responsible for hyperparameter tuning of YOLO models. |
|
|
|
The class evolves YOLO model hyperparameters over a given number of iterations |
|
by mutating them according to the search space and retraining the model to evaluate their performance. |
|
|
|
Attributes: |
|
space (dict): Hyperparameter search space containing bounds and scaling factors for mutation. |
|
tune_dir (Path): Directory where evolution logs and results will be saved. |
|
evolve_csv (Path): Path to the CSV file where evolution logs are saved. |
|
|
|
Methods: |
|
_mutate(hyp: dict) -> dict: |
|
Mutates the given hyperparameters within the bounds specified in `self.space`. |
|
|
|
__call__(): |
|
Executes the hyperparameter evolution across multiple iterations. |
|
|
|
Example: |
|
Tune hyperparameters for YOLOv8n on COCO8 at imgsz=640 and epochs=30 for 300 tuning iterations. |
|
```python |
|
from ultralytics import YOLO |
|
|
|
model = YOLO('yolov8n.pt') |
|
model.tune(data='coco8.yaml', imgsz=640, epochs=100, iterations=10) |
|
``` |
|
""" |
|
|
|
def __init__(self, args=DEFAULT_CFG, _callbacks=None): |
|
""" |
|
Initialize the Tuner with configurations. |
|
|
|
Args: |
|
args (dict, optional): Configuration for hyperparameter evolution. |
|
""" |
|
self.args = get_cfg(overrides=args) |
|
self.space = { |
|
|
|
'lr0': (1e-5, 1e-1), |
|
'lrf': (0.01, 1.0), |
|
'momentum': (0.6, 0.98), |
|
'weight_decay': (0.0, 0.001), |
|
'warmup_epochs': (0.0, 5.0), |
|
'warmup_momentum': (0.0, 0.95), |
|
'box': (0.02, 0.2), |
|
'cls': (0.2, 4.0), |
|
'hsv_h': (0.0, 0.1), |
|
'hsv_s': (0.0, 0.9), |
|
'hsv_v': (0.0, 0.9), |
|
'degrees': (0.0, 45.0), |
|
'translate': (0.0, 0.9), |
|
'scale': (0.0, 0.9), |
|
'shear': (0.0, 10.0), |
|
'perspective': (0.0, 0.001), |
|
'flipud': (0.0, 1.0), |
|
'fliplr': (0.0, 1.0), |
|
'mosaic': (0.0, 1.0), |
|
'mixup': (0.0, 1.0), |
|
'copy_paste': (0.0, 1.0)} |
|
self.tune_dir = get_save_dir(self.args, name='tune') |
|
self.evolve_csv = self.tune_dir / 'evolve.csv' |
|
self.callbacks = _callbacks or callbacks.get_default_callbacks() |
|
callbacks.add_integration_callbacks(self) |
|
LOGGER.info(f"Initialized Tuner instance with 'tune_dir={self.tune_dir}'.") |
|
|
|
def _mutate(self, parent='single', n=5, mutation=0.8, sigma=0.2, return_best=False): |
|
""" |
|
Mutates the hyperparameters based on bounds and scaling factors specified in `self.space`. |
|
|
|
Args: |
|
parent (str): Parent selection method: 'single' or 'weighted'. |
|
n (int): Number of parents to consider. |
|
mutation (float): Probability of a parameter mutation in any given iteration. |
|
sigma (float): Standard deviation for Gaussian random number generator. |
|
|
|
Returns: |
|
(dict): A dictionary containing mutated hyperparameters. |
|
""" |
|
if self.evolve_csv.exists(): |
|
|
|
x = np.loadtxt(self.evolve_csv, ndmin=2, delimiter=',', skiprows=1) |
|
fitness = x[:, 0] |
|
n = min(n, len(x)) |
|
x = x[np.argsort(-fitness)][:n] |
|
if return_best: |
|
return {k: float(x[0, i + 1]) for i, k in enumerate(self.space.keys())} |
|
fitness = x[:, 0] |
|
w = fitness - fitness.min() + 1E-6 |
|
if parent == 'single' or len(x) == 1: |
|
|
|
x = x[random.choices(range(n), weights=w)[0]] |
|
elif parent == 'weighted': |
|
x = (x * w.reshape(n, 1)).sum(0) / w.sum() |
|
|
|
|
|
r = np.random |
|
r.seed(int(time.time())) |
|
g = np.array([self.space[k][0] for k in self.space.keys()]) |
|
ng = len(self.space) |
|
v = np.ones(ng) |
|
while all(v == 1): |
|
v = (g * (r.random(ng) < mutation) * r.randn(ng) * r.random() * sigma + 1).clip(0.3, 3.0) |
|
hyp = {k: float(x[i + 1] * v[i]) for i, k in enumerate(self.space.keys())} |
|
else: |
|
hyp = {k: getattr(self.args, k) for k in self.space.keys()} |
|
|
|
|
|
for k, v in self.space.items(): |
|
hyp[k] = max(hyp[k], v[0]) |
|
hyp[k] = min(hyp[k], v[1]) |
|
hyp[k] = round(hyp[k], 5) |
|
|
|
return hyp |
|
|
|
def __call__(self, model=None, iterations=10, prefix=colorstr('Tuner:')): |
|
""" |
|
Executes the hyperparameter evolution process when the Tuner instance is called. |
|
|
|
This method iterates through the number of iterations, performing the following steps in each iteration: |
|
1. Load the existing hyperparameters or initialize new ones. |
|
2. Mutate the hyperparameters using the `mutate` method. |
|
3. Train a YOLO model with the mutated hyperparameters. |
|
4. Log the fitness score and mutated hyperparameters to a CSV file. |
|
|
|
Args: |
|
model (YOLO): A pre-initialized YOLO model to be used for training. |
|
iterations (int): The number of generations to run the evolution for. |
|
|
|
Note: |
|
The method utilizes the `self.evolve_csv` Path object to read and log hyperparameters and fitness scores. |
|
Ensure this path is set correctly in the Tuner instance. |
|
""" |
|
|
|
self.tune_dir.mkdir(parents=True, exist_ok=True) |
|
for i in range(iterations): |
|
|
|
mutated_hyp = self._mutate() |
|
LOGGER.info(f'{prefix} Starting iteration {i + 1}/{iterations} with hyperparameters: {mutated_hyp}') |
|
|
|
|
|
model = YOLO('yolov8n.pt') |
|
train_args = {**vars(self.args), **mutated_hyp} |
|
results = model.train(**train_args) |
|
|
|
|
|
headers = '' if self.evolve_csv.exists() else (','.join(['fitness_score'] + list(self.space.keys())) + '\n') |
|
log_row = [results.fitness] + [mutated_hyp[k] for k in self.space.keys()] |
|
with open(self.evolve_csv, 'a') as f: |
|
f.write(headers + ','.join(map(str, log_row)) + '\n') |
|
|
|
LOGGER.info(f'{prefix} All iterations complete. Results saved to {colorstr("bold", self.tune_dir)}') |
|
best_hyp = self._mutate(return_best=True) |
|
yaml_save(self.tune_dir / 'best.yaml', best_hyp) |
|
yaml_print(self.tune_dir / 'best.yaml') |
|
|