yolov8逐步分解(1)--默认参数&超参配置文件加载
yolov8逐步分解(2)_DetectionTrainer类初始化过程
yolov8逐步分解(3)_trainer训练之模型加载
YOLOV8逐步分解(4)_模型的构建过程
YOLOV8逐步分解(5)_模型训练初始设置之混合精度训练AMP
YOLOV8逐步分解(6)_模型训练初始设置之image size检测batch预设及dataloder初始化
yolov8逐步分解(7)_模型训练初始设置之优化器Optimizer及学习率调度器Scheduler初始化
本系列之前的文章1-7均为训练初始化过程,本章节将讲解训练过程的代码。
这段代码是PyTorch训练循环实现的一部分。以下将对它进行逐步解释:
_do_train函数负责主要的训练过程。它可以接受一个可选参数world_size,用于指定用于分布式数据并行(DDP)训练的GPU数量。
def _do_train(self, world_size=1):"""Train completed, evaluate and plot if specified by arguments."""if world_size > 1: #当前world_size = 1,不采用多卡训练self._setup_ddp(world_size) #多卡训练配置self._setup_train(world_size)self.epoch_time = Noneself.epoch_time_start = time.time()self.train_time_start = time.time()nb = len(self.train_loader) # number of batchesnw = max(round(self.args.warmup_epochs *nb), 100) if self.args.warmup_epochs > 0 else -1 # number of warmup iterations # 预热迭代次数last_opt_step = -1self.run_callbacks('on_train_start')#输出一些训练信息,如图像大小、数据加载器worker数量、日志保存路径等LOGGER.info(f'Image sizes {self.args.imgsz} train, {self.args.imgsz} val\n'f'Using {self.train_loader.num_workers * (world_size or 1)} dataloader workers\n'f"Logging results to {colorstr('bold', self.save_dir)}\n"f'Starting training for {self.epochs} epochs...')if self.args.close_mosaic:base_idx = (self.epochs - self.args.close_mosaic) * nbself.plot_idx.extend([base_idx, base_idx + 1, base_idx + 2])epoch = self.epochs # predefine for resume fully trained model edge casesfor epoch in range(self.start_epoch, self.epochs):self.epoch = epochself.run_callbacks('on_train_epoch_start')self.model.train() #将模型设置为训练模式if RANK != -1:self.train_loader.sampler.set_epoch(epoch)pbar = enumerate(self.train_loader)# Update dataloader attributes (optional)if epoch == (self.epochs - self.args.close_mosaic): #最后10次训练关闭mosaic可以提升训练效果LOGGER.info('Closing dataloader mosaic')if hasattr(self.train_loader.dataset, 'mosaic'):self.train_loader.dataset.mosaic = Falseif hasattr(self.train_loader.dataset, 'close_mosaic'):self.train_loader.dataset.close_mosaic(hyp=self.args)self.train_loader.reset() #重置训练数据加载器的状态if RANK in (-1, 0):LOGGER.info(self.progress_string())pbar = tqdm(enumerate(self.train_loader), total=nb, bar_format=TQDM_BAR_FORMAT)self.tloss = Noneself.optimizer.zero_grad()#在反向传播过程中,梯度是累积的,而不是被替换掉。通过在每次迭代开始时将梯度清零,确保在当前迭代中计算梯度时是全新的,不会受到上一次迭代的梯度的影响。for i, batch in pbar:self.run_callbacks('on_train_batch_start')# Warmupni = i + nb * epoch #计算当前迭代的总步数if ni <= nw: #在训练的早期阶段逐渐调整学习率和动量参数,以帮助模型更好地收敛。这通常用于处理训练初期的不稳定性。xi = [0, nw] # x interp 热身步数的起止范围self.accumulate = max(1, np.interp(ni, xi, [1, self.args.nbs / self.batch_size]).round()) # 控制梯度累积的步数,根据当前迭代步数 ni 在范围 xi 内进行插值计算,并将结果四舍五入。for j, x in enumerate(self.optimizer.param_groups):# Bias lr falls from 0.1 to lr0, all other lrs rise from 0.0 to lr0x['lr'] = np.interp(ni, xi, [self.args.warmup_bias_lr if j == 0 else 0.0, x['initial_lr'] * self.lf(epoch)])if 'momentum' in x:x['momentum'] = np.interp(ni, xi, [self.args.warmup_momentum, self.args.momentum])# Forwardwith torch.cuda.amp.autocast(self.amp):batch = self.preprocess_batch(batch) #归一化 /255self.loss, self.loss_items = self.model(batch) #将预处理后的批次数据输入模型进行前向传播并获取损失值和其他损失项if RANK != -1:self.loss *= world_sizeself.tloss = (self.tloss * i + self.loss_items) / (i + 1) if self.tloss is not None \else self.loss_items# Backward#使用self.scaler对损失值进行缩放,并调用backward()方法进行反向传播。#这是自动混合精度(AMP)训练中的一步,用于计算梯度并将其传播回模型的参数self.scaler.scale(self.loss).backward()# Optimize - https://pytorch.org/docs/master/notes/amp_examples.htmlif ni - last_opt_step >= self.accumulate:self.optimizer_step() #执行一步优化器的更新,根据之前计算得到的梯度来更新模型的参数。last_opt_step = ni# Logmem = f'{torch.cuda.memory_reserved() / 1E9 if torch.cuda.is_available() else 0:.3g}G' # (GB) 计算cuda内存以GB为单位loss_len = self.tloss.shape[0] if len(self.tloss.size()) else 1losses = self.tloss if loss_len > 1 else torch.unsqueeze(self.tloss, 0)if RANK in (-1, 0): #使用格式化字符串将训练进度、GPU内存占用和损失值等信息展示在进度条中pbar.set_description(('%11s' * 2 + '%11.4g' * (2 + loss_len)) %(f'{epoch + 1}/{self.epochs}', mem, *losses, batch['cls'].shape[0], batch['img'].shape[-1]))self.run_callbacks('on_batch_end')if self.args.plots and ni in self.plot_idx:self.plot_training_samples(batch, ni)self.run_callbacks('on_train_batch_end')self.lr = {f'lr/pg{ir}': x['lr'] for ir, x in enumerate(self.optimizer.param_groups)} # for loggersself.scheduler.step()self.run_callbacks('on_train_epoch_end')if RANK in (-1, 0):# Validationself.ema.update_attr(self.model, include=['yaml', 'nc', 'args', 'names', 'stride', 'class_weights'])final_epoch = (epoch + 1 == self.epochs) or self.stopper.possible_stop #判断是否为最后一个训练轮次或训练可能停止if self.args.val or final_epoch:self.metrics, self.fitness = self.validate()self.save_metrics(metrics={**self.label_loss_items(self.tloss), **self.metrics, **self.lr})self.stop = self.stopper(epoch + 1, self.fitness)# Save modelif self.args.save or (epoch + 1 == self.epochs): #判断是否需要保存模型self.save_model()self.run_callbacks('on_model_save')tnow = time.time()self.epoch_time = tnow - self.epoch_time_startself.epoch_time_start = tnowself.run_callbacks('on_fit_epoch_end')#清空GPU的显存。这个操作可以帮助释放GPU显存,以防止在训练过程中出现内存不足的错误。torch.cuda.empty_cache() # clears GPU vRAM at end of epoch, can help with out of memory errors# Early Stoppingif RANK != -1: # if DDP training 分布训练中广播停止给其他进程broadcast_list = [self.stop if RANK == 0 else None]dist.broadcast_object_list(broadcast_list, 0) # broadcast 'stop' to all ranksif RANK != 0: #如果当前进程的RANK不为0,则将广播列表中的第一个对象(即进程0广播的self.stop)赋值self.stop = broadcast_list[0]if self.stop:break # must break all DDP ranksif RANK in (-1, 0):# Do final val with best.ptLOGGER.info(f'\n{epoch - self.start_epoch + 1} epochs completed in 'f'{(time.time() - self.train_time_start) / 3600:.3f} hours.')self.final_eval() #2.执行最终的验证(final_eval())操作。if self.args.plots:self.plot_metrics()self.run_callbacks('on_train_end')torch.cuda.empty_cache()#清空GPU的显存self.run_callbacks('teardown')
1. 初始准备代码讲解
"""Train completed, evaluate and plot if specified by arguments."""if world_size > 1: #当前world_size = 1,不采用多卡训练self._setup_ddp(world_size) #多卡训练配置self._setup_train(world_size)self.epoch_time = Noneself.epoch_time_start = time.time()self.train_time_start = time.time()nb = len(self.train_loader) # number of batchesnw = max(round(self.args.warmup_epochs *nb), 100) if self.args.warmup_epochs > 0 else -1 # number of warmup iterations # 预热迭代次数last_opt_step = -1self.run_callbacks('on_train_start')
1.1 首先检查world_size是否大于1,大于1表示正在使用DDP训练。如果是,它调用_setup_ddp方法来配置DDP设置。
1.2 调用_setup_train方法,该方法实现模型的加载初始化等过程,具体内容参考本系列文章《yolov8逐步分解(3)_trainer训练之模型加载》中的具体介绍。
1.3 接下来初始化一些与时间相关的变量:
self.epoch_time: 将保存当前训练epoch的持续时间。
self.epoch_time_start: 记录当前训练epoch的开始时间。
self.train_time_start: 记录整个训练过程的开始时间。
1.4 训练加载器中的批次数量存储在nb变量中。
1.5 nw变量计算预热迭代的数量,它是以下三个值中的最大值:
round(self.args.warmup_epochs * nb): 预热epoch数乘以每个epoch的批次数。
100: 至少100个预热迭代。
如果self.args.warmup_epochs是0或更小,nw被设置为-1,表示没有预热。
1.6 last_opt_step变量被初始化为-1。
1.7 最后,调用run_callbacks('on_train_start')方法,可能会触发训练开始时注册的任何回调函数。
总的来说,这段代码设置了训练循环的初始状态,为实际的训练迭代做好准备。
LOGGER.info(f'Image sizes {self.args.imgsz} train, {self.args.imgsz} val\n'f'Using {self.train_loader.num_workers * (world_size or 1)} dataloader workers\n'f"Logging results to {colorstr('bold', self.save_dir)}\n"f'Starting training for {self.epochs} epochs...')if self.args.close_mosaic:base_idx = (self.epochs - self.args.close_mosaic) * nbself.plot_idx.extend([base_idx, base_idx + 1, base_idx + 2])epoch = self.epochs # predefine for resume fully trained model edge cases
2.1 打印一些有关训练过程的信息:
训练和验证时使用的图像尺寸
用于数据加载的 worker 数量
训练结果的保存路径
训练将运行的 epoch 数
2.2 检查是否设置了 self.args.close_mosaic 参数。如果设置了,它会计算在最后 self.args.close_mosaic 个 epoch 中需要绘制的索引,并将它们添加到 self.plot_idx 列表中。这可能是为了在训练结束时绘制一些特定的图表或可视化效果。
2.3 将 epoch 变量设置为 self.epochs。这可能是为了处理在训练过程中恢复完全训练模型的边缘情况。
这段代码旨在记录训练过程中的一些关键信息,以便更好地理解和监控模型的训练情况。它还可以为训练结束后的数据分析和可视化提供一些有用的信息。