trainer.py 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820
  1. # Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
  2. """
  3. Train a model on a dataset.
  4. Usage:
  5. $ yolo mode=train model=yolov8n.pt data=coco8.yaml imgsz=640 epochs=100 batch=16
  6. """
  7. import gc
  8. import math
  9. import os
  10. import subprocess
  11. import time
  12. import warnings
  13. from copy import copy, deepcopy
  14. from datetime import datetime, timedelta
  15. from pathlib import Path
  16. import numpy as np
  17. import torch
  18. from torch import distributed as dist
  19. from torch import nn, optim
  20. from ultralytics.cfg import get_cfg, get_save_dir
  21. from ultralytics.data.utils import check_cls_dataset, check_det_dataset
  22. from ultralytics.nn.tasks import attempt_load_one_weight, attempt_load_weights
  23. from ultralytics.utils import (
  24. DEFAULT_CFG,
  25. LOCAL_RANK,
  26. LOGGER,
  27. RANK,
  28. TQDM,
  29. __version__,
  30. callbacks,
  31. clean_url,
  32. colorstr,
  33. emojis,
  34. yaml_save,
  35. )
  36. from ultralytics.utils.autobatch import check_train_batch_size
  37. from ultralytics.utils.checks import check_amp, check_file, check_imgsz, check_model_file_from_stem, print_args
  38. from ultralytics.utils.dist import ddp_cleanup, generate_ddp_command
  39. from ultralytics.utils.files import get_latest_run
  40. from ultralytics.utils.torch_utils import (
  41. TORCH_2_4,
  42. EarlyStopping,
  43. ModelEMA,
  44. autocast,
  45. convert_optimizer_state_dict_to_fp16,
  46. init_seeds,
  47. one_cycle,
  48. select_device,
  49. strip_optimizer,
  50. torch_distributed_zero_first,
  51. )
  52. class BaseTrainer:
  53. """
  54. A base class for creating trainers.
  55. Attributes:
  56. args (SimpleNamespace): Configuration for the trainer.
  57. validator (BaseValidator): Validator instance.
  58. model (nn.Module): Model instance.
  59. callbacks (defaultdict): Dictionary of callbacks.
  60. save_dir (Path): Directory to save results.
  61. wdir (Path): Directory to save weights.
  62. last (Path): Path to the last checkpoint.
  63. best (Path): Path to the best checkpoint.
  64. save_period (int): Save checkpoint every x epochs (disabled if < 1).
  65. batch_size (int): Batch size for training.
  66. epochs (int): Number of epochs to train for.
  67. start_epoch (int): Starting epoch for training.
  68. device (torch.device): Device to use for training.
  69. amp (bool): Flag to enable AMP (Automatic Mixed Precision).
  70. scaler (amp.GradScaler): Gradient scaler for AMP.
  71. data (str): Path to data.
  72. trainset (torch.utils.data.Dataset): Training dataset.
  73. testset (torch.utils.data.Dataset): Testing dataset.
  74. ema (nn.Module): EMA (Exponential Moving Average) of the model.
  75. resume (bool): Resume training from a checkpoint.
  76. lf (nn.Module): Loss function.
  77. scheduler (torch.optim.lr_scheduler._LRScheduler): Learning rate scheduler.
  78. best_fitness (float): The best fitness value achieved.
  79. fitness (float): Current fitness value.
  80. loss (float): Current loss value.
  81. tloss (float): Total loss value.
  82. loss_names (list): List of loss names.
  83. csv (Path): Path to results CSV file.
  84. """
  85. def __init__(self, cfg=DEFAULT_CFG, overrides=None, _callbacks=None):
  86. """
  87. Initializes the BaseTrainer class.
  88. Args:
  89. cfg (str, optional): Path to a configuration file. Defaults to DEFAULT_CFG.
  90. overrides (dict, optional): Configuration overrides. Defaults to None.
  91. """
  92. self.args = get_cfg(cfg, overrides)
  93. self.check_resume(overrides)
  94. self.device = select_device(self.args.device, self.args.batch)
  95. self.validator = None
  96. self.metrics = None
  97. self.plots = {}
  98. init_seeds(self.args.seed + 1 + RANK, deterministic=self.args.deterministic)
  99. # Dirs
  100. self.save_dir = get_save_dir(self.args)
  101. self.args.name = self.save_dir.name # update name for loggers
  102. self.wdir = self.save_dir / "weights" # weights dir
  103. if RANK in {-1, 0}:
  104. self.wdir.mkdir(parents=True, exist_ok=True) # make dir
  105. self.args.save_dir = str(self.save_dir)
  106. yaml_save(self.save_dir / "args.yaml", vars(self.args)) # save run args
  107. self.last, self.best = self.wdir / "last.pt", self.wdir / "best.pt" # checkpoint paths
  108. self.save_period = self.args.save_period
  109. self.batch_size = self.args.batch
  110. self.epochs = self.args.epochs or 100 # in case users accidentally pass epochs=None with timed training
  111. self.start_epoch = 0
  112. if RANK == -1:
  113. print_args(vars(self.args))
  114. # Device
  115. if self.device.type in {"cpu", "mps"}:
  116. self.args.workers = 0 # faster CPU training as time dominated by inference, not dataloading
  117. # Model and Dataset
  118. self.model = check_model_file_from_stem(self.args.model) # add suffix, i.e. yolov8n -> yolov8n.pt
  119. with torch_distributed_zero_first(LOCAL_RANK): # avoid auto-downloading dataset multiple times
  120. self.trainset, self.testset = self.get_dataset()
  121. self.ema = None
  122. # Optimization utils init
  123. self.lf = None
  124. self.scheduler = None
  125. # Epoch level metrics
  126. self.best_fitness = None
  127. self.fitness = None
  128. self.loss = None
  129. self.tloss = None
  130. self.loss_names = ["Loss"]
  131. self.csv = self.save_dir / "results.csv"
  132. self.plot_idx = [0, 1, 2]
  133. # HUB
  134. self.hub_session = None
  135. # Callbacks
  136. self.callbacks = _callbacks or callbacks.get_default_callbacks()
  137. if RANK in {-1, 0}:
  138. callbacks.add_integration_callbacks(self)
  139. def add_callback(self, event: str, callback):
  140. """Appends the given callback."""
  141. self.callbacks[event].append(callback)
  142. def set_callback(self, event: str, callback):
  143. """Overrides the existing callbacks with the given callback."""
  144. self.callbacks[event] = [callback]
  145. def run_callbacks(self, event: str):
  146. """Run all existing callbacks associated with a particular event."""
  147. for callback in self.callbacks.get(event, []):
  148. callback(self)
  149. def train(self):
  150. """Allow device='', device=None on Multi-GPU systems to default to device=0."""
  151. if isinstance(self.args.device, str) and len(self.args.device): # i.e. device='0' or device='0,1,2,3'
  152. world_size = len(self.args.device.split(","))
  153. elif isinstance(self.args.device, (tuple, list)): # i.e. device=[0, 1, 2, 3] (multi-GPU from CLI is list)
  154. world_size = len(self.args.device)
  155. elif self.args.device in {"cpu", "mps"}: # i.e. device='cpu' or 'mps'
  156. world_size = 0
  157. elif torch.cuda.is_available(): # i.e. device=None or device='' or device=number
  158. world_size = 1 # default to device 0
  159. else: # i.e. device=None or device=''
  160. world_size = 0
  161. # Run subprocess if DDP training, else train normally
  162. if world_size > 1 and "LOCAL_RANK" not in os.environ:
  163. # Argument checks
  164. if self.args.rect:
  165. LOGGER.warning("WARNING ⚠️ 'rect=True' is incompatible with Multi-GPU training, setting 'rect=False'")
  166. self.args.rect = False
  167. if self.args.batch < 1.0:
  168. LOGGER.warning(
  169. "WARNING ⚠️ 'batch<1' for AutoBatch is incompatible with Multi-GPU training, setting "
  170. "default 'batch=16'"
  171. )
  172. self.args.batch = 16
  173. # Command
  174. cmd, file = generate_ddp_command(world_size, self)
  175. try:
  176. LOGGER.info(f"{colorstr('DDP:')} debug command {' '.join(cmd)}")
  177. subprocess.run(cmd, check=True)
  178. except Exception as e:
  179. raise e
  180. finally:
  181. ddp_cleanup(self, str(file))
  182. else:
  183. self._do_train(world_size)
  184. def _setup_scheduler(self):
  185. """Initialize training learning rate scheduler."""
  186. if self.args.cos_lr:
  187. self.lf = one_cycle(1, self.args.lrf, self.epochs) # cosine 1->hyp['lrf']
  188. else:
  189. self.lf = lambda x: max(1 - x / self.epochs, 0) * (1.0 - self.args.lrf) + self.args.lrf # linear
  190. self.scheduler = optim.lr_scheduler.LambdaLR(self.optimizer, lr_lambda=self.lf)
  191. def _setup_ddp(self, world_size):
  192. """Initializes and sets the DistributedDataParallel parameters for training."""
  193. torch.cuda.set_device(RANK)
  194. self.device = torch.device("cuda", RANK)
  195. # LOGGER.info(f'DDP info: RANK {RANK}, WORLD_SIZE {world_size}, DEVICE {self.device}')
  196. os.environ["TORCH_NCCL_BLOCKING_WAIT"] = "1" # set to enforce timeout
  197. dist.init_process_group(
  198. backend="nccl" if dist.is_nccl_available() else "gloo",
  199. timeout=timedelta(seconds=10800), # 3 hours
  200. rank=RANK,
  201. world_size=world_size,
  202. )
  203. def _setup_train(self, world_size):
  204. """Builds dataloaders and optimizer on correct rank process."""
  205. # Model
  206. self.run_callbacks("on_pretrain_routine_start")
  207. ckpt = self.setup_model()
  208. self.model = self.model.to(self.device)
  209. self.set_model_attributes()
  210. # Freeze layers
  211. freeze_list = (
  212. self.args.freeze
  213. if isinstance(self.args.freeze, list)
  214. else range(self.args.freeze)
  215. if isinstance(self.args.freeze, int)
  216. else []
  217. )
  218. always_freeze_names = [".dfl"] # always freeze these layers
  219. freeze_layer_names = [f"model.{x}." for x in freeze_list] + always_freeze_names
  220. for k, v in self.model.named_parameters():
  221. # v.register_hook(lambda x: torch.nan_to_num(x)) # NaN to 0 (commented for erratic training results)
  222. if any(x in k for x in freeze_layer_names):
  223. LOGGER.info(f"Freezing layer '{k}'")
  224. v.requires_grad = False
  225. elif not v.requires_grad and v.dtype.is_floating_point: # only floating point Tensor can require gradients
  226. LOGGER.info(
  227. f"WARNING ⚠️ setting 'requires_grad=True' for frozen layer '{k}'. "
  228. "See ultralytics.engine.trainer for customization of frozen layers."
  229. )
  230. v.requires_grad = True
  231. # Check AMP
  232. self.amp = torch.tensor(self.args.amp).to(self.device) # True or False
  233. if self.amp and RANK in {-1, 0}: # Single-GPU and DDP
  234. callbacks_backup = callbacks.default_callbacks.copy() # backup callbacks as check_amp() resets them
  235. self.amp = torch.tensor(check_amp(self.model), device=self.device)
  236. callbacks.default_callbacks = callbacks_backup # restore callbacks
  237. if RANK > -1 and world_size > 1: # DDP
  238. dist.broadcast(self.amp, src=0) # broadcast the tensor from rank 0 to all other ranks (returns None)
  239. self.amp = bool(self.amp) # as boolean
  240. self.scaler = (
  241. torch.amp.GradScaler("cuda", enabled=self.amp) if TORCH_2_4 else torch.cuda.amp.GradScaler(enabled=self.amp)
  242. )
  243. if world_size > 1:
  244. self.model = nn.parallel.DistributedDataParallel(self.model, device_ids=[RANK], find_unused_parameters=True)
  245. self.set_model_attributes() # set again after DDP wrapper
  246. # Check imgsz
  247. gs = max(int(self.model.stride.max() if hasattr(self.model, "stride") else 32), 32) # grid size (max stride)
  248. self.args.imgsz = check_imgsz(self.args.imgsz, stride=gs, floor=gs, max_dim=1)
  249. self.stride = gs # for multiscale training
  250. # Batch size
  251. if self.batch_size < 1 and RANK == -1: # single-GPU only, estimate best batch size
  252. self.args.batch = self.batch_size = self.auto_batch()
  253. # Dataloaders
  254. batch_size = self.batch_size // max(world_size, 1)
  255. self.train_loader = self.get_dataloader(self.trainset, batch_size=batch_size, rank=LOCAL_RANK, mode="train")
  256. if RANK in {-1, 0}:
  257. # Note: When training DOTA dataset, double batch size could get OOM on images with >2000 objects.
  258. self.test_loader = self.get_dataloader(
  259. self.testset, batch_size=batch_size if self.args.task == "obb" else batch_size * 2, rank=-1, mode="val"
  260. )
  261. self.validator = self.get_validator()
  262. metric_keys = self.validator.metrics.keys + self.label_loss_items(prefix="val")
  263. self.metrics = dict(zip(metric_keys, [0] * len(metric_keys)))
  264. self.ema = ModelEMA(self.model)
  265. if self.args.plots:
  266. self.plot_training_labels()
  267. # Optimizer
  268. self.accumulate = max(round(self.args.nbs / self.batch_size), 1) # accumulate loss before optimizing
  269. weight_decay = self.args.weight_decay * self.batch_size * self.accumulate / self.args.nbs # scale weight_decay
  270. iterations = math.ceil(len(self.train_loader.dataset) / max(self.batch_size, self.args.nbs)) * self.epochs
  271. self.optimizer = self.build_optimizer(
  272. model=self.model,
  273. name=self.args.optimizer,
  274. lr=self.args.lr0,
  275. momentum=self.args.momentum,
  276. decay=weight_decay,
  277. iterations=iterations,
  278. )
  279. # Scheduler
  280. self._setup_scheduler()
  281. self.stopper, self.stop = EarlyStopping(patience=self.args.patience), False
  282. self.resume_training(ckpt)
  283. self.scheduler.last_epoch = self.start_epoch - 1 # do not move
  284. self.run_callbacks("on_pretrain_routine_end")
  285. def _do_train(self, world_size=1):
  286. """Train completed, evaluate and plot if specified by arguments."""
  287. if world_size > 1:
  288. self._setup_ddp(world_size)
  289. self._setup_train(world_size)
  290. nb = len(self.train_loader) # number of batches
  291. nw = max(round(self.args.warmup_epochs * nb), 100) if self.args.warmup_epochs > 0 else -1 # warmup iterations
  292. last_opt_step = -1
  293. self.epoch_time = None
  294. self.epoch_time_start = time.time()
  295. self.train_time_start = time.time()
  296. self.run_callbacks("on_train_start")
  297. LOGGER.info(
  298. f"Image sizes {self.args.imgsz} train, {self.args.imgsz} val\n"
  299. f"Using {self.train_loader.num_workers * (world_size or 1)} dataloader workers\n"
  300. f"Logging results to {colorstr('bold', self.save_dir)}\n"
  301. f"Starting training for " + (f"{self.args.time} hours..." if self.args.time else f"{self.epochs} epochs...")
  302. )
  303. if self.args.close_mosaic:
  304. base_idx = (self.epochs - self.args.close_mosaic) * nb
  305. self.plot_idx.extend([base_idx, base_idx + 1, base_idx + 2])
  306. epoch = self.start_epoch
  307. self.optimizer.zero_grad() # zero any resumed gradients to ensure stability on train start
  308. while True:
  309. self.epoch = epoch
  310. self.run_callbacks("on_train_epoch_start")
  311. with warnings.catch_warnings():
  312. warnings.simplefilter("ignore") # suppress 'Detected lr_scheduler.step() before optimizer.step()'
  313. self.scheduler.step()
  314. self.model.train()
  315. if RANK != -1:
  316. self.train_loader.sampler.set_epoch(epoch)
  317. pbar = enumerate(self.train_loader)
  318. # Update dataloader attributes (optional)
  319. if epoch == (self.epochs - self.args.close_mosaic):
  320. self._close_dataloader_mosaic()
  321. self.train_loader.reset()
  322. if RANK in {-1, 0}:
  323. LOGGER.info(self.progress_string())
  324. pbar = TQDM(enumerate(self.train_loader), total=nb)
  325. self.tloss = None
  326. for i, batch in pbar:
  327. self.run_callbacks("on_train_batch_start")
  328. # Warmup
  329. ni = i + nb * epoch
  330. if ni <= nw:
  331. xi = [0, nw] # x interp
  332. self.accumulate = max(1, int(np.interp(ni, xi, [1, self.args.nbs / self.batch_size]).round()))
  333. for j, x in enumerate(self.optimizer.param_groups):
  334. # Bias lr falls from 0.1 to lr0, all other lrs rise from 0.0 to lr0
  335. x["lr"] = np.interp(
  336. ni, xi, [self.args.warmup_bias_lr if j == 0 else 0.0, x["initial_lr"] * self.lf(epoch)]
  337. )
  338. if "momentum" in x:
  339. x["momentum"] = np.interp(ni, xi, [self.args.warmup_momentum, self.args.momentum])
  340. # Forward
  341. with autocast(self.amp):
  342. batch = self.preprocess_batch(batch)
  343. self.loss, self.loss_items = self.model(batch)
  344. if RANK != -1:
  345. self.loss *= world_size
  346. self.tloss = (
  347. (self.tloss * i + self.loss_items) / (i + 1) if self.tloss is not None else self.loss_items
  348. )
  349. # Backward
  350. self.scaler.scale(self.loss).backward()
  351. # Optimize - https://pytorch.org/docs/master/notes/amp_examples.html
  352. if ni - last_opt_step >= self.accumulate:
  353. self.optimizer_step()
  354. last_opt_step = ni
  355. # Timed stopping
  356. if self.args.time:
  357. self.stop = (time.time() - self.train_time_start) > (self.args.time * 3600)
  358. if RANK != -1: # if DDP training
  359. broadcast_list = [self.stop if RANK == 0 else None]
  360. dist.broadcast_object_list(broadcast_list, 0) # broadcast 'stop' to all ranks
  361. self.stop = broadcast_list[0]
  362. if self.stop: # training time exceeded
  363. break
  364. # Log
  365. if RANK in {-1, 0}:
  366. loss_length = self.tloss.shape[0] if len(self.tloss.shape) else 1
  367. pbar.set_description(
  368. ("%11s" * 2 + "%11.4g" * (2 + loss_length))
  369. % (
  370. f"{epoch + 1}/{self.epochs}",
  371. f"{self._get_memory():.3g}G", # (GB) GPU memory util
  372. *(self.tloss if loss_length > 1 else torch.unsqueeze(self.tloss, 0)), # losses
  373. batch["cls"].shape[0], # batch size, i.e. 8
  374. batch["img"].shape[-1], # imgsz, i.e 640
  375. )
  376. )
  377. self.run_callbacks("on_batch_end")
  378. if self.args.plots and ni in self.plot_idx:
  379. self.plot_training_samples(batch, ni)
  380. self.run_callbacks("on_train_batch_end")
  381. self.lr = {f"lr/pg{ir}": x["lr"] for ir, x in enumerate(self.optimizer.param_groups)} # for loggers
  382. self.run_callbacks("on_train_epoch_end")
  383. if RANK in {-1, 0}:
  384. final_epoch = epoch + 1 >= self.epochs
  385. self.ema.update_attr(self.model, include=["yaml", "nc", "args", "names", "stride", "class_weights"])
  386. # Validation
  387. if self.args.val or final_epoch or self.stopper.possible_stop or self.stop:
  388. self.metrics, self.fitness = self.validate()
  389. self.save_metrics(metrics={**self.label_loss_items(self.tloss), **self.metrics, **self.lr})
  390. self.stop |= self.stopper(epoch + 1, self.fitness) or final_epoch
  391. if self.args.time:
  392. self.stop |= (time.time() - self.train_time_start) > (self.args.time * 3600)
  393. # Save model
  394. if self.args.save or final_epoch:
  395. self.save_model()
  396. self.run_callbacks("on_model_save")
  397. # Scheduler
  398. t = time.time()
  399. self.epoch_time = t - self.epoch_time_start
  400. self.epoch_time_start = t
  401. if self.args.time:
  402. mean_epoch_time = (t - self.train_time_start) / (epoch - self.start_epoch + 1)
  403. self.epochs = self.args.epochs = math.ceil(self.args.time * 3600 / mean_epoch_time)
  404. self._setup_scheduler()
  405. self.scheduler.last_epoch = self.epoch # do not move
  406. self.stop |= epoch >= self.epochs # stop if exceeded epochs
  407. self.run_callbacks("on_fit_epoch_end")
  408. self._clear_memory()
  409. # Early Stopping
  410. if RANK != -1: # if DDP training
  411. broadcast_list = [self.stop if RANK == 0 else None]
  412. dist.broadcast_object_list(broadcast_list, 0) # broadcast 'stop' to all ranks
  413. self.stop = broadcast_list[0]
  414. if self.stop:
  415. break # must break all DDP ranks
  416. epoch += 1
  417. if RANK in {-1, 0}:
  418. # Do final val with best.pt
  419. seconds = time.time() - self.train_time_start
  420. LOGGER.info(f"\n{epoch - self.start_epoch + 1} epochs completed in {seconds / 3600:.3f} hours.")
  421. self.final_eval()
  422. if self.args.plots:
  423. self.plot_metrics()
  424. self.run_callbacks("on_train_end")
  425. self._clear_memory()
  426. self.run_callbacks("teardown")
  427. def auto_batch(self, max_num_obj=0):
  428. """Get batch size by calculating memory occupation of model."""
  429. return check_train_batch_size(
  430. model=self.model,
  431. imgsz=self.args.imgsz,
  432. amp=self.amp,
  433. batch=self.batch_size,
  434. max_num_obj=max_num_obj,
  435. ) # returns batch size
  436. def _get_memory(self):
  437. """Get accelerator memory utilization in GB."""
  438. if self.device.type == "mps":
  439. memory = torch.mps.driver_allocated_memory()
  440. elif self.device.type == "cpu":
  441. memory = 0
  442. else:
  443. memory = torch.cuda.memory_reserved()
  444. return memory / 1e9
  445. def _clear_memory(self):
  446. """Clear accelerator memory on different platforms."""
  447. gc.collect()
  448. if self.device.type == "mps":
  449. torch.mps.empty_cache()
  450. elif self.device.type == "cpu":
  451. return
  452. else:
  453. torch.cuda.empty_cache()
  454. def read_results_csv(self):
  455. """Read results.csv into a dict using pandas."""
  456. import pandas as pd # scope for faster 'import ultralytics'
  457. return pd.read_csv(self.csv).to_dict(orient="list")
  458. def save_model(self):
  459. """Save model training checkpoints with additional metadata."""
  460. import io
  461. # Serialize ckpt to a byte buffer once (faster than repeated torch.save() calls)
  462. buffer = io.BytesIO()
  463. torch.save(
  464. {
  465. "epoch": self.epoch,
  466. "best_fitness": self.best_fitness,
  467. "model": None, # resume and final checkpoints derive from EMA
  468. "ema": deepcopy(self.ema.ema).half(),
  469. "updates": self.ema.updates,
  470. "optimizer": convert_optimizer_state_dict_to_fp16(deepcopy(self.optimizer.state_dict())),
  471. "train_args": vars(self.args), # save as dict
  472. "train_metrics": {**self.metrics, **{"fitness": self.fitness}},
  473. "train_results": self.read_results_csv(),
  474. "date": datetime.now().isoformat(),
  475. "version": __version__,
  476. "license": "AGPL-3.0 (https://ultralytics.com/license)",
  477. "docs": "https://docs.ultralytics.com",
  478. },
  479. buffer,
  480. )
  481. serialized_ckpt = buffer.getvalue() # get the serialized content to save
  482. # Save checkpoints
  483. self.last.write_bytes(serialized_ckpt) # save last.pt
  484. if self.best_fitness == self.fitness:
  485. self.best.write_bytes(serialized_ckpt) # save best.pt
  486. if (self.save_period > 0) and (self.epoch % self.save_period == 0):
  487. (self.wdir / f"epoch{self.epoch}.pt").write_bytes(serialized_ckpt) # save epoch, i.e. 'epoch3.pt'
  488. # if self.args.close_mosaic and self.epoch == (self.epochs - self.args.close_mosaic - 1):
  489. # (self.wdir / "last_mosaic.pt").write_bytes(serialized_ckpt) # save mosaic checkpoint
  490. def get_dataset(self):
  491. """
  492. Get train, val path from data dict if it exists.
  493. Returns None if data format is not recognized.
  494. """
  495. try:
  496. if self.args.task == "classify":
  497. data = check_cls_dataset(self.args.data)
  498. elif self.args.data.split(".")[-1] in {"yaml", "yml"} or self.args.task in {
  499. "detect",
  500. "segment",
  501. "pose",
  502. "obb",
  503. }:
  504. data = check_det_dataset(self.args.data)
  505. if "yaml_file" in data:
  506. self.args.data = data["yaml_file"] # for validating 'yolo train data=url.zip' usage
  507. except Exception as e:
  508. raise RuntimeError(emojis(f"Dataset '{clean_url(self.args.data)}' error ❌ {e}")) from e
  509. self.data = data
  510. return data["train"], data.get("val") or data.get("test")
  511. def setup_model(self):
  512. """Load/create/download model for any task."""
  513. if isinstance(self.model, torch.nn.Module): # if model is loaded beforehand. No setup needed
  514. return
  515. cfg, weights = self.model, None
  516. ckpt = None
  517. if str(self.model).endswith(".pt"):
  518. weights, ckpt = attempt_load_one_weight(self.model)
  519. cfg = weights.yaml
  520. elif isinstance(self.args.pretrained, (str, Path)):
  521. weights, _ = attempt_load_one_weight(self.args.pretrained)
  522. self.model = self.get_model(cfg=cfg, weights=weights, verbose=RANK == -1) # calls Model(cfg, weights)
  523. return ckpt
  524. def optimizer_step(self):
  525. """Perform a single step of the training optimizer with gradient clipping and EMA update."""
  526. self.scaler.unscale_(self.optimizer) # unscale gradients
  527. torch.nn.utils.clip_grad_norm_(self.model.parameters(), max_norm=10.0) # clip gradients
  528. self.scaler.step(self.optimizer)
  529. self.scaler.update()
  530. self.optimizer.zero_grad()
  531. if self.ema:
  532. self.ema.update(self.model)
  533. def preprocess_batch(self, batch):
  534. """Allows custom preprocessing model inputs and ground truths depending on task type."""
  535. return batch
  536. def validate(self):
  537. """
  538. Runs validation on test set using self.validator.
  539. The returned dict is expected to contain "fitness" key.
  540. """
  541. metrics = self.validator(self)
  542. fitness = metrics.pop("fitness", -self.loss.detach().cpu().numpy()) # use loss as fitness measure if not found
  543. if not self.best_fitness or self.best_fitness < fitness:
  544. self.best_fitness = fitness
  545. return metrics, fitness
  546. def get_model(self, cfg=None, weights=None, verbose=True):
  547. """Get model and raise NotImplementedError for loading cfg files."""
  548. raise NotImplementedError("This task trainer doesn't support loading cfg files")
  549. def get_validator(self):
  550. """Returns a NotImplementedError when the get_validator function is called."""
  551. raise NotImplementedError("get_validator function not implemented in trainer")
  552. def get_dataloader(self, dataset_path, batch_size=16, rank=0, mode="train"):
  553. """Returns dataloader derived from torch.data.Dataloader."""
  554. raise NotImplementedError("get_dataloader function not implemented in trainer")
  555. def build_dataset(self, img_path, mode="train", batch=None):
  556. """Build dataset."""
  557. raise NotImplementedError("build_dataset function not implemented in trainer")
  558. def label_loss_items(self, loss_items=None, prefix="train"):
  559. """
  560. Returns a loss dict with labelled training loss items tensor.
  561. Note:
  562. This is not needed for classification but necessary for segmentation & detection
  563. """
  564. return {"loss": loss_items} if loss_items is not None else ["loss"]
  565. def set_model_attributes(self):
  566. """To set or update model parameters before training."""
  567. self.model.names = self.data["names"]
  568. def build_targets(self, preds, targets):
  569. """Builds target tensors for training YOLO model."""
  570. pass
  571. def progress_string(self):
  572. """Returns a string describing training progress."""
  573. return ""
  574. # TODO: may need to put these following functions into callback
  575. def plot_training_samples(self, batch, ni):
  576. """Plots training samples during YOLO training."""
  577. pass
  578. def plot_training_labels(self):
  579. """Plots training labels for YOLO model."""
  580. pass
  581. def save_metrics(self, metrics):
  582. """Saves training metrics to a CSV file."""
  583. keys, vals = list(metrics.keys()), list(metrics.values())
  584. n = len(metrics) + 2 # number of cols
  585. s = "" if self.csv.exists() else (("%s," * n % tuple(["epoch", "time"] + keys)).rstrip(",") + "\n") # header
  586. t = time.time() - self.train_time_start
  587. with open(self.csv, "a") as f:
  588. f.write(s + ("%.6g," * n % tuple([self.epoch + 1, t] + vals)).rstrip(",") + "\n")
  589. def plot_metrics(self):
  590. """Plot and display metrics visually."""
  591. pass
  592. def on_plot(self, name, data=None):
  593. """Registers plots (e.g. to be consumed in callbacks)."""
  594. path = Path(name)
  595. self.plots[path] = {"data": data, "timestamp": time.time()}
  596. def final_eval(self):
  597. """Performs final evaluation and validation for object detection YOLO model."""
  598. ckpt = {}
  599. for f in self.last, self.best:
  600. if f.exists():
  601. if f is self.last:
  602. ckpt = strip_optimizer(f)
  603. elif f is self.best:
  604. k = "train_results" # update best.pt train_metrics from last.pt
  605. strip_optimizer(f, updates={k: ckpt[k]} if k in ckpt else None)
  606. LOGGER.info(f"\nValidating {f}...")
  607. self.validator.args.plots = self.args.plots
  608. self.metrics = self.validator(model=f)
  609. self.metrics.pop("fitness", None)
  610. self.run_callbacks("on_fit_epoch_end")
  611. def check_resume(self, overrides):
  612. """Check if resume checkpoint exists and update arguments accordingly."""
  613. resume = self.args.resume
  614. if resume:
  615. try:
  616. exists = isinstance(resume, (str, Path)) and Path(resume).exists()
  617. last = Path(check_file(resume) if exists else get_latest_run())
  618. # Check that resume data YAML exists, otherwise strip to force re-download of dataset
  619. ckpt_args = attempt_load_weights(last).args
  620. if not Path(ckpt_args["data"]).exists():
  621. ckpt_args["data"] = self.args.data
  622. resume = True
  623. self.args = get_cfg(ckpt_args)
  624. self.args.model = self.args.resume = str(last) # reinstate model
  625. for k in (
  626. "imgsz",
  627. "batch",
  628. "device",
  629. "close_mosaic",
  630. ): # allow arg updates to reduce memory or update device on resume
  631. if k in overrides:
  632. setattr(self.args, k, overrides[k])
  633. except Exception as e:
  634. raise FileNotFoundError(
  635. "Resume checkpoint not found. Please pass a valid checkpoint to resume from, "
  636. "i.e. 'yolo train resume model=path/to/last.pt'"
  637. ) from e
  638. self.resume = resume
  639. def resume_training(self, ckpt):
  640. """Resume YOLO training from given epoch and best fitness."""
  641. if ckpt is None or not self.resume:
  642. return
  643. best_fitness = 0.0
  644. start_epoch = ckpt.get("epoch", -1) + 1
  645. if ckpt.get("optimizer", None) is not None:
  646. self.optimizer.load_state_dict(ckpt["optimizer"]) # optimizer
  647. best_fitness = ckpt["best_fitness"]
  648. if self.ema and ckpt.get("ema"):
  649. self.ema.ema.load_state_dict(ckpt["ema"].float().state_dict()) # EMA
  650. self.ema.updates = ckpt["updates"]
  651. assert start_epoch > 0, (
  652. f"{self.args.model} training to {self.epochs} epochs is finished, nothing to resume.\n"
  653. f"Start a new training without resuming, i.e. 'yolo train model={self.args.model}'"
  654. )
  655. LOGGER.info(f"Resuming training {self.args.model} from epoch {start_epoch + 1} to {self.epochs} total epochs")
  656. if self.epochs < start_epoch:
  657. LOGGER.info(
  658. f"{self.model} has been trained for {ckpt['epoch']} epochs. Fine-tuning for {self.epochs} more epochs."
  659. )
  660. self.epochs += ckpt["epoch"] # finetune additional epochs
  661. self.best_fitness = best_fitness
  662. self.start_epoch = start_epoch
  663. if start_epoch > (self.epochs - self.args.close_mosaic):
  664. self._close_dataloader_mosaic()
  665. def _close_dataloader_mosaic(self):
  666. """Update dataloaders to stop using mosaic augmentation."""
  667. if hasattr(self.train_loader.dataset, "mosaic"):
  668. self.train_loader.dataset.mosaic = False
  669. if hasattr(self.train_loader.dataset, "close_mosaic"):
  670. LOGGER.info("Closing dataloader mosaic")
  671. self.train_loader.dataset.close_mosaic(hyp=copy(self.args))
  672. def build_optimizer(self, model, name="auto", lr=0.001, momentum=0.9, decay=1e-5, iterations=1e5):
  673. """
  674. Constructs an optimizer for the given model, based on the specified optimizer name, learning rate, momentum,
  675. weight decay, and number of iterations.
  676. Args:
  677. model (torch.nn.Module): The model for which to build an optimizer.
  678. name (str, optional): The name of the optimizer to use. If 'auto', the optimizer is selected
  679. based on the number of iterations. Default: 'auto'.
  680. lr (float, optional): The learning rate for the optimizer. Default: 0.001.
  681. momentum (float, optional): The momentum factor for the optimizer. Default: 0.9.
  682. decay (float, optional): The weight decay for the optimizer. Default: 1e-5.
  683. iterations (float, optional): The number of iterations, which determines the optimizer if
  684. name is 'auto'. Default: 1e5.
  685. Returns:
  686. (torch.optim.Optimizer): The constructed optimizer.
  687. """
  688. g = [], [], [] # optimizer parameter groups
  689. bn = tuple(v for k, v in nn.__dict__.items() if "Norm" in k) # normalization layers, i.e. BatchNorm2d()
  690. if name == "auto":
  691. LOGGER.info(
  692. f"{colorstr('optimizer:')} 'optimizer=auto' found, "
  693. f"ignoring 'lr0={self.args.lr0}' and 'momentum={self.args.momentum}' and "
  694. f"determining best 'optimizer', 'lr0' and 'momentum' automatically... "
  695. )
  696. nc = getattr(model, "nc", 10) # number of classes
  697. lr_fit = round(0.002 * 5 / (4 + nc), 6) # lr0 fit equation to 6 decimal places
  698. name, lr, momentum = ("SGD", 0.01, 0.9) if iterations > 10000 else ("AdamW", lr_fit, 0.9)
  699. self.args.warmup_bias_lr = 0.0 # no higher than 0.01 for Adam
  700. for module_name, module in model.named_modules():
  701. for param_name, param in module.named_parameters(recurse=False):
  702. fullname = f"{module_name}.{param_name}" if module_name else param_name
  703. if "bias" in fullname: # bias (no decay)
  704. g[2].append(param)
  705. elif isinstance(module, bn): # weight (no decay)
  706. g[1].append(param)
  707. else: # weight (with decay)
  708. g[0].append(param)
  709. optimizers = {"Adam", "Adamax", "AdamW", "NAdam", "RAdam", "RMSProp", "SGD", "auto"}
  710. name = {x.lower(): x for x in optimizers}.get(name.lower())
  711. if name in {"Adam", "Adamax", "AdamW", "NAdam", "RAdam"}:
  712. optimizer = getattr(optim, name, optim.Adam)(g[2], lr=lr, betas=(momentum, 0.999), weight_decay=0.0)
  713. elif name == "RMSProp":
  714. optimizer = optim.RMSprop(g[2], lr=lr, momentum=momentum)
  715. elif name == "SGD":
  716. optimizer = optim.SGD(g[2], lr=lr, momentum=momentum, nesterov=True)
  717. else:
  718. raise NotImplementedError(
  719. f"Optimizer '{name}' not found in list of available optimizers {optimizers}. "
  720. "Request support for addition optimizers at https://github.com/ultralytics/ultralytics."
  721. )
  722. optimizer.add_param_group({"params": g[0], "weight_decay": decay}) # add g0 with weight_decay
  723. optimizer.add_param_group({"params": g[1], "weight_decay": 0.0}) # add g1 (BatchNorm2d weights)
  724. LOGGER.info(
  725. f"{colorstr('optimizer:')} {type(optimizer).__name__}(lr={lr}, momentum={momentum}) with parameter groups "
  726. f"{len(g[1])} weight(decay=0.0), {len(g[0])} weight(decay={decay}), {len(g[2])} bias(decay=0.0)"
  727. )
  728. return optimizer