tuner.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. # Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
  2. """
  3. Module provides functionalities for hyperparameter tuning of the Ultralytics YOLO models for object detection, instance
  4. segmentation, image classification, pose estimation, and multi-object tracking.
  5. Hyperparameter tuning is the process of systematically searching for the optimal set of hyperparameters
  6. that yield the best model performance. This is particularly crucial in deep learning models like YOLO,
  7. where small changes in hyperparameters can lead to significant differences in model accuracy and efficiency.
  8. Example:
  9. Tune hyperparameters for YOLOv8n on COCO8 at imgsz=640 and epochs=30 for 300 tuning iterations.
  10. ```python
  11. from ultralytics import YOLO
  12. model = YOLO("yolo11n.pt")
  13. model.tune(data="coco8.yaml", epochs=10, iterations=300, optimizer="AdamW", plots=False, save=False, val=False)
  14. ```
  15. """
  16. import random
  17. import shutil
  18. import subprocess
  19. import time
  20. import numpy as np
  21. import torch
  22. from ultralytics.cfg import get_cfg, get_save_dir
  23. from ultralytics.utils import DEFAULT_CFG, LOGGER, callbacks, colorstr, remove_colorstr, yaml_print, yaml_save
  24. from ultralytics.utils.plotting import plot_tune_results
  25. class Tuner:
  26. """
  27. Class responsible for hyperparameter tuning of YOLO models.
  28. The class evolves YOLO model hyperparameters over a given number of iterations
  29. by mutating them according to the search space and retraining the model to evaluate their performance.
  30. Attributes:
  31. space (dict): Hyperparameter search space containing bounds and scaling factors for mutation.
  32. tune_dir (Path): Directory where evolution logs and results will be saved.
  33. tune_csv (Path): Path to the CSV file where evolution logs are saved.
  34. Methods:
  35. _mutate(hyp: dict) -> dict:
  36. Mutates the given hyperparameters within the bounds specified in `self.space`.
  37. __call__():
  38. Executes the hyperparameter evolution across multiple iterations.
  39. Example:
  40. Tune hyperparameters for YOLOv8n on COCO8 at imgsz=640 and epochs=30 for 300 tuning iterations.
  41. ```python
  42. from ultralytics import YOLO
  43. model = YOLO("yolo11n.pt")
  44. model.tune(data="coco8.yaml", epochs=10, iterations=300, optimizer="AdamW", plots=False, save=False, val=False)
  45. ```
  46. Tune with custom search space.
  47. ```python
  48. from ultralytics import YOLO
  49. model = YOLO("yolo11n.pt")
  50. model.tune(space={key1: val1, key2: val2}) # custom search space dictionary
  51. ```
  52. """
  53. def __init__(self, args=DEFAULT_CFG, _callbacks=None):
  54. """
  55. Initialize the Tuner with configurations.
  56. Args:
  57. args (dict, optional): Configuration for hyperparameter evolution.
  58. """
  59. self.space = args.pop("space", None) or { # key: (min, max, gain(optional))
  60. # 'optimizer': tune.choice(['SGD', 'Adam', 'AdamW', 'NAdam', 'RAdam', 'RMSProp']),
  61. "lr0": (1e-5, 1e-1), # initial learning rate (i.e. SGD=1E-2, Adam=1E-3)
  62. "lrf": (0.0001, 0.1), # final OneCycleLR learning rate (lr0 * lrf)
  63. "momentum": (0.7, 0.98, 0.3), # SGD momentum/Adam beta1
  64. "weight_decay": (0.0, 0.001), # optimizer weight decay 5e-4
  65. "warmup_epochs": (0.0, 5.0), # warmup epochs (fractions ok)
  66. "warmup_momentum": (0.0, 0.95), # warmup initial momentum
  67. "box": (1.0, 20.0), # box loss gain
  68. "cls": (0.2, 4.0), # cls loss gain (scale with pixels)
  69. "dfl": (0.4, 6.0), # dfl loss gain
  70. "hsv_h": (0.0, 0.1), # image HSV-Hue augmentation (fraction)
  71. "hsv_s": (0.0, 0.9), # image HSV-Saturation augmentation (fraction)
  72. "hsv_v": (0.0, 0.9), # image HSV-Value augmentation (fraction)
  73. "degrees": (0.0, 45.0), # image rotation (+/- deg)
  74. "translate": (0.0, 0.9), # image translation (+/- fraction)
  75. "scale": (0.0, 0.95), # image scale (+/- gain)
  76. "shear": (0.0, 10.0), # image shear (+/- deg)
  77. "perspective": (0.0, 0.001), # image perspective (+/- fraction), range 0-0.001
  78. "flipud": (0.0, 1.0), # image flip up-down (probability)
  79. "fliplr": (0.0, 1.0), # image flip left-right (probability)
  80. "bgr": (0.0, 1.0), # image channel bgr (probability)
  81. "mosaic": (0.0, 1.0), # image mixup (probability)
  82. "mixup": (0.0, 1.0), # image mixup (probability)
  83. "copy_paste": (0.0, 1.0), # segment copy-paste (probability)
  84. }
  85. self.args = get_cfg(overrides=args)
  86. self.tune_dir = get_save_dir(self.args, name=self.args.name or "tune")
  87. self.args.name = None # reset to not affect training directory
  88. self.tune_csv = self.tune_dir / "tune_results.csv"
  89. self.callbacks = _callbacks or callbacks.get_default_callbacks()
  90. self.prefix = colorstr("Tuner: ")
  91. callbacks.add_integration_callbacks(self)
  92. LOGGER.info(
  93. f"{self.prefix}Initialized Tuner instance with 'tune_dir={self.tune_dir}'\n"
  94. f"{self.prefix}💡 Learn about tuning at https://docs.ultralytics.com/guides/hyperparameter-tuning"
  95. )
  96. def _mutate(self, parent="single", n=5, mutation=0.8, sigma=0.2):
  97. """
  98. Mutates the hyperparameters based on bounds and scaling factors specified in `self.space`.
  99. Args:
  100. parent (str): Parent selection method: 'single' or 'weighted'.
  101. n (int): Number of parents to consider.
  102. mutation (float): Probability of a parameter mutation in any given iteration.
  103. sigma (float): Standard deviation for Gaussian random number generator.
  104. Returns:
  105. (dict): A dictionary containing mutated hyperparameters.
  106. """
  107. if self.tune_csv.exists(): # if CSV file exists: select best hyps and mutate
  108. # Select parent(s)
  109. x = np.loadtxt(self.tune_csv, ndmin=2, delimiter=",", skiprows=1)
  110. fitness = x[:, 0] # first column
  111. n = min(n, len(x)) # number of previous results to consider
  112. x = x[np.argsort(-fitness)][:n] # top n mutations
  113. w = x[:, 0] - x[:, 0].min() + 1e-6 # weights (sum > 0)
  114. if parent == "single" or len(x) == 1:
  115. # x = x[random.randint(0, n - 1)] # random selection
  116. x = x[random.choices(range(n), weights=w)[0]] # weighted selection
  117. elif parent == "weighted":
  118. x = (x * w.reshape(n, 1)).sum(0) / w.sum() # weighted combination
  119. # Mutate
  120. r = np.random # method
  121. r.seed(int(time.time()))
  122. g = np.array([v[2] if len(v) == 3 else 1.0 for v in self.space.values()]) # gains 0-1
  123. ng = len(self.space)
  124. v = np.ones(ng)
  125. while all(v == 1): # mutate until a change occurs (prevent duplicates)
  126. v = (g * (r.random(ng) < mutation) * r.randn(ng) * r.random() * sigma + 1).clip(0.3, 3.0)
  127. hyp = {k: float(x[i + 1] * v[i]) for i, k in enumerate(self.space.keys())}
  128. else:
  129. hyp = {k: getattr(self.args, k) for k in self.space.keys()}
  130. # Constrain to limits
  131. for k, v in self.space.items():
  132. hyp[k] = max(hyp[k], v[0]) # lower limit
  133. hyp[k] = min(hyp[k], v[1]) # upper limit
  134. hyp[k] = round(hyp[k], 5) # significant digits
  135. return hyp
  136. def __call__(self, model=None, iterations=10, cleanup=True):
  137. """
  138. Executes the hyperparameter evolution process when the Tuner instance is called.
  139. This method iterates through the number of iterations, performing the following steps in each iteration:
  140. 1. Load the existing hyperparameters or initialize new ones.
  141. 2. Mutate the hyperparameters using the `mutate` method.
  142. 3. Train a YOLO model with the mutated hyperparameters.
  143. 4. Log the fitness score and mutated hyperparameters to a CSV file.
  144. Args:
  145. model (Model): A pre-initialized YOLO model to be used for training.
  146. iterations (int): The number of generations to run the evolution for.
  147. cleanup (bool): Whether to delete iteration weights to reduce storage space used during tuning.
  148. Note:
  149. The method utilizes the `self.tune_csv` Path object to read and log hyperparameters and fitness scores.
  150. Ensure this path is set correctly in the Tuner instance.
  151. """
  152. t0 = time.time()
  153. best_save_dir, best_metrics = None, None
  154. (self.tune_dir / "weights").mkdir(parents=True, exist_ok=True)
  155. for i in range(iterations):
  156. # Mutate hyperparameters
  157. mutated_hyp = self._mutate()
  158. LOGGER.info(f"{self.prefix}Starting iteration {i + 1}/{iterations} with hyperparameters: {mutated_hyp}")
  159. metrics = {}
  160. train_args = {**vars(self.args), **mutated_hyp}
  161. save_dir = get_save_dir(get_cfg(train_args))
  162. weights_dir = save_dir / "weights"
  163. try:
  164. # Train YOLO model with mutated hyperparameters (run in subprocess to avoid dataloader hang)
  165. cmd = ["yolo", "train", *(f"{k}={v}" for k, v in train_args.items())]
  166. return_code = subprocess.run(" ".join(cmd), check=True, shell=True).returncode
  167. ckpt_file = weights_dir / ("best.pt" if (weights_dir / "best.pt").exists() else "last.pt")
  168. metrics = torch.load(ckpt_file)["train_metrics"]
  169. assert return_code == 0, "training failed"
  170. except Exception as e:
  171. LOGGER.warning(f"WARNING ❌️ training failure for hyperparameter tuning iteration {i + 1}\n{e}")
  172. # Save results and mutated_hyp to CSV
  173. fitness = metrics.get("fitness", 0.0)
  174. log_row = [round(fitness, 5)] + [mutated_hyp[k] for k in self.space.keys()]
  175. headers = "" if self.tune_csv.exists() else (",".join(["fitness"] + list(self.space.keys())) + "\n")
  176. with open(self.tune_csv, "a") as f:
  177. f.write(headers + ",".join(map(str, log_row)) + "\n")
  178. # Get best results
  179. x = np.loadtxt(self.tune_csv, ndmin=2, delimiter=",", skiprows=1)
  180. fitness = x[:, 0] # first column
  181. best_idx = fitness.argmax()
  182. best_is_current = best_idx == i
  183. if best_is_current:
  184. best_save_dir = save_dir
  185. best_metrics = {k: round(v, 5) for k, v in metrics.items()}
  186. for ckpt in weights_dir.glob("*.pt"):
  187. shutil.copy2(ckpt, self.tune_dir / "weights")
  188. elif cleanup:
  189. shutil.rmtree(weights_dir, ignore_errors=True) # remove iteration weights/ dir to reduce storage space
  190. # Plot tune results
  191. plot_tune_results(self.tune_csv)
  192. # Save and print tune results
  193. header = (
  194. f"{self.prefix}{i + 1}/{iterations} iterations complete ✅ ({time.time() - t0:.2f}s)\n"
  195. f"{self.prefix}Results saved to {colorstr('bold', self.tune_dir)}\n"
  196. f"{self.prefix}Best fitness={fitness[best_idx]} observed at iteration {best_idx + 1}\n"
  197. f"{self.prefix}Best fitness metrics are {best_metrics}\n"
  198. f"{self.prefix}Best fitness model is {best_save_dir}\n"
  199. f"{self.prefix}Best fitness hyperparameters are printed below.\n"
  200. )
  201. LOGGER.info("\n" + header)
  202. data = {k: float(x[best_idx, i + 1]) for i, k in enumerate(self.space.keys())}
  203. yaml_save(
  204. self.tune_dir / "best_hyperparameters.yaml",
  205. data=data,
  206. header=remove_colorstr(header.replace(self.prefix, "# ")) + "\n",
  207. )
  208. yaml_print(self.tune_dir / "best_hyperparameters.yaml")