yolov8逐步分解(1)--默认参数&超参配置文件加载_yolov8 加载yaml配置 预测-CSDN博客
yolov8逐步分解(2)_DetectionTrainer类初始化过程_train' and 'val' are required in all data yamls.-CSDN博客
yolov8逐步分解(3)_trainer训练之模型加载_yolov8 加载模型-CSDN博客
YOLOV8逐步分解(4)_模型的构建过程_m = getattr(torch.nn, m[3:]) if "nn." in m else gl-CSDN博客
YOLOV8逐步分解(5)_模型训练初始设置之混合精度训练AMP-CSDN博客
YOLOV8逐步分解(6)_模型训练初始设置之image size检测batch预设及dataloder初始化-CSDN博客
yolov8逐步分解(7)_模型训练初始设置之优化器Optimizer及学习率调度器Scheduler初始化-CSDN博客
yolov8逐步分解(8)_训练过程之Epoch迭代前初始准备
1. Epoch循环处理过程
for 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()
这段代码是模型训练循环的一部分。下面详细介绍一下它的功能:
- 外层循环遍历训练 epoch,从 self.start_epoch 开始,一直到 self.epochs。这确保了模型会按照指定的训练轮数进行训练。
- 在每个 epoch 开始时,它调用 self.run_callbacks('on_train_epoch_start') 来执行注册的回调函数。这可能会触发一些自定义的回调函数,用于在训练过程中执行特定的操作,例如记录指标、调整超参数等
- 它使用 self.model.train() 将模型设置为训练模式。确保模型在训练阶段正确地执行前向传播和反向传播。
- 如果 RANK 不等于 -1,它会将训练数据加载器的 sampler 的 epoch 设置为当前 epoch。确保在分布式训练中每个进程都能访问到正确的数据。
- 它通过枚举训练数据加载器创建一个进度条 (pbar),以便更好地监控训练过程,关于进度条的详细介绍可以参考文章python之tqdm函数使用总结。
- 如果当前 epoch 是最后 self.args.close_mosaic 个 epoch,它会禁用训练数据集中的"mosaic"数据增强技术,并重置训练数据加载器的状态。这是为了在训练的最后阶段提高模型的泛化性能
- 如果 RANK 是 -1 或 0(即主进程),它会记录进度字符串并创建一个 tqdm 进度条。
- 将 self.tloss 设置为 None,并将优化器的梯度重置为零。
这段代码负责设置训练循环,管理训练数据集,并为下一个训练步骤准备模型和优化器。
2. 每个batch循环处理过程
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])
这段代码是训练循环中的一部分,主要用于在训练的早期阶段进行学习率和动量参数的热身(warm-up)操作。
具体步骤如下:
1. 计算当前迭代的总步数ni,通过将当前批次数i与总批次数nb乘以训练轮次数epoch相加得到。
2. 判断ni是否小于等于热身步数nw,即判断当前迭代是否处于训练的早期阶段。
3. 如果当前迭代处于训练的早期阶段,则进行学习率和动量参数的热身操作。
4. 首先定义热身步数的起止范围xi为[0, nw]。
5. 通过插值计算,根据当前迭代步数ni在范围xi内,将梯度累积的步数self.accumulate进行计算。这里使用np.interp()函数进行线性插值,将ni在xi范围内映射到[1, self.args.nbs / self.batch_size]范围,并将结果四舍五入为整数。
6.对于优化器的每个参数组,根据当前迭代步数ni在范围xi内,进行学习率和动量参数的插值计算。
对于第一个参数组(索引为0),学习率从热身偏置学习率self.args.warmup_bias_lr逐渐下降到初始学习率x['initial_lr'] * self.lf(epoch)。
对于其他参数组,学习率从0.0逐渐上升到初始学习率x['initial_lr'] * self.lf(epoch)。
如果参数组中存在动量('momentum'),则动量参数从热身动量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
这段代码是混合精度训练(mixed precision training)(参考文章深度学习之混合精度训练AMP介绍)中的一部分,用于执行前向传播操作和计算损失。
具体步骤如下:
- 使用torch.cuda.amp.autocast(self.amp)上下文管理器,开启混合精度训练。在该上下文中,模型的输入和输出将自动转换为半精度(float16),以减少内存使用和计算量。
- 对批次数据进行预处理操作,包括归一化(将像素值除以255)等。self.preprocess_batch()函数用于执行这些预处理操作。
- 将预处理后的批次数据输入模型进行前向传播,通过调用self.model()函数。同时,该函数返回了损失值self.loss和其他损失项self.loss_items。
- 如果当前进程的排名RANK不等于-1(表示当前进程处于分布式训练中),则将损失值乘以总进程数world_size。这是为了在分布式训练中,根据进程数进行损失值的归一化。
- 计算平均损失。首先判断是否是第一个批次(self.tloss is None)。如果是第一个批次,则将当前批次的损失项作为初始总损失项。如果不是第一个批次,则将当前批次的损失项加权平均到之前的总损失项中,以得到更新后的总损失项。这样做是为了计算平均损失,以便在训练过程中跟踪模型的整体性能。
这段代码执行了混合精度训练的前向传播操作,包括输入数据的预处理、模型的前向计算和损失的计算。它还进行了损失的归一化和平均化,以便在训练过程中对模型的性能进行跟踪和监控。
# Backwardself.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
1. self.scaler.scale(self.loss).backward() 这行代码执行训练过程中的反向传播。self.loss代表计算得到的损失值,self.scaler是torch.cuda.amp.GradScaler的一个实例,用于梯度缩放。
2. 在反向传播之后,代码检查自上次优化器步骤以来的迭代次数(ni)是否大于或等于指定的累积值(self.accumulate)。如果是,则调用self.optimizer_step()函数,使用计算得到的梯度更新模型参数。这个步骤通常用于在应用优化器步骤之前累积多个迭代的梯度。
3. self.optimizer_step()代码如下:
def optimizer_step(self):"""Perform a single step of the training optimizer with gradient clipping and EMA update."""self.scaler.unscale_(self.optimizer) # unscale gradientstorch.nn.utils.clip_grad_norm_(self.model.parameters(), max_norm=10.0) # clip gradientsself.scaler.step(self.optimizer)self.scaler.update()self.optimizer.zero_grad()if self.ema:self.ema.update(self.model)
这段代码是一个用于训练优化器执行单个步骤的函数。它包括梯度缩放(scaling)、梯度剪裁(gradient clipping)和指数移动平均(exponential moving average)更新等步骤。
具体解释如下:
- self.scaler.unscale_(self.optimizer): 这行代码用于反缩放优化器的梯度。在使用混合精度训练时,梯度会进行缩放,此处将其还原为原始值。
- torch.nn.utils.clip_grad_norm_(self.model.parameters(), max_norm=10.0): 这行代码用于对梯度进行剪裁,限制其最大范数为10.0。剪裁梯度的目的是防止梯度爆炸问题。
- self.scaler.step(self.optimizer): 这行代码用于更新优化器的参数,执行一步优化器的更新操作。
- self.scaler.update(): 这行代码用于更新梯度缩放器的状态,以便在下一步训练中再次使用。
- self.optimizer.zero_grad(): 这行代码用于清空梯度,准备进行下一步的反向传播计算。
- if self.ema: self.ema.update(self.model): 这段代码用于更新指数移动平均模型(EMA)。如果指定了 EMA(exponential moving average),则将当前模型的参数更新到 EMA 模型中,用于模型参数的平滑更新。
mem = f'{torch.cuda.memory_reserved() / 1E9 if torch.cuda.is_available() else 0:.3g}G' # (GB) 计算cuda内存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')
这段代码是在训练过程中更新进度条并显示相关信息的部分。下面是代码的解释:
- mem = f'{torch.cuda.memory_reserved() / 1E9 if torch.cuda.is_available() else 0:.3g}G' 这行代码用于计算当前CUDA内存占用情况,并以GB为单位进行格式化显示。如果CUDA可用,它将通过torch.cuda.memory_reserved()获取已分配的内存大小,并将其除以1E9(即10^9)转换为GB。如果CUDA不可用,则显示0GB。
- loss_len = self.tloss.shape[0] if len(self.tloss.size()) else 1 这行代码计算损失值的长度。self.tloss是一个张量,通过检查其维度的数量来确定长度。如果维度数量大于0,则长度为self.tloss.shape[0];否则,长度为1。
- losses = self.tloss if loss_len > 1 else torch.unsqueeze(self.tloss, 0) 这行代码根据损失值的长度,将self.tloss张量处理为长度为1的张量。如果损失值长度大于1,则losses保持不变;否则,使用torch.unsqueeze()在维度0上添加一个维度,将self.tloss转换为长度为1的张量。
- if RANK in (-1, 0): 这个条件判断语句检查当前进程的排名(RANK)是否为-1或0,用于确定只在排名为-1或0的进程上执行下面的代码块。这通常用于在多个进程进行训练时,只有主进程(排名为0)或单进程训练时才执行特定的操作。
- pbar.set_description(...) 这行代码用于更新进度条的描述信息。它使用格式化字符串将训练进度、GPU内存占用和损失值等信息展示在进度条中。
- self.run_callbacks('on_batch_end') 这行代码在每个训练批次结束时运行回调函数。
- if self.args.plots and ni in self.plot_idx: 这个条件语句检查是否需要绘制训练样本的图像。self.args.plots是一个标志,表示是否启用绘图功能,ni是当前迭代的索引,self.plot_idx是包含要绘制图像的迭代索引的集合。
- self.run_callbacks('on_train_batch_end') 这行代码在每个训练批次结束时运行回调函数。
至此每个Batch训练过程结束。
继续epoch循环剩余部分讲解。
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')
这段代码是Epoch训练过程中batch循环结束后的一部分代码,主要包括以下内容:
1. self.lr = {f'lr/pg{ir}': x['lr'] for ir, x in enumerate(self.optimizer.param_groups)} 这行代码用于将优化器中每个参数组的学习率(lr)存储在字典self.lr中,以便后续日志记录使用。
2. self.scheduler.step() 这行代码用于更新学习率调度器(scheduler)的状态。学习率调度器根据预定义的策略在训练过程中调整学习率。
3. self.run_callbacks('on_train_epoch_end') 这行代码在每个训练轮次结束时运行回调函数。
- if RANK in (-1, 0): 这个条件语句检查当前进程的排名(RANK)是否为-1或0,用于确定只在排名为-1或0的进程上执行下面的代码块。
- self.ema.update_attr(self.model, include=['yaml', 'nc', 'args', 'names', 'stride', 'class_weights']) 这行代码使用指数移动平均(EMA)更新模型的属性。EMA可以用于在训练过程中保持模型参数的平滑版本,以提升模型的泛化能力。
6. final_epoch = (epoch + 1 == self.epochs) or self.stopper.possible_stop 这行代码判断是否为最后一个训练轮次或训练可能停止。epoch表示当前轮次,self.epochs表示总轮次,self.stopper.possible_stop表示训练是否可能停止的标志。
7. if self.args.val or final_epoch: 这个条件语句检查是否需要进行验证(validation)。self.args.val是一个标志,表示是否启用验证,final_epoch表示是否为最后一个训练轮次或训练可能停止。
8. self.metrics, self.fitness = self.validate() 这行代码用于进行验证,并将验证结果的指标(metrics)和适应度(fitness)存储在相应的变量中。
9. self.save_metrics(metrics={**self.label_loss_items(self.tloss), **self.metrics, **self.lr}) 这行代码用于保存训练过程中的指标(metrics),包括损失值(losses)、验证指标(metrics)和学习率(lr)。
10. self.stop = self.stopper(epoch + 1, self.fitness) 这行代码根据当前轮次和适应度值判断是否应该停止训练。self.stopper是一个停止条件判断器对象,根据特定的条件判断是否应该停止训练。
11. if self.args.save or (epoch + 1 == self.epochs): 这个条件语句检查是否需要保存模型。self.args.save是一个标志,表示是否启用模型保存,(epoch + 1 == self.epochs)表示是否为最后一个训练轮次。
12. self.save_model() 这行代码用于保存模型到硬盘。使用torch.save保存模型及其参数,保存内容如下:
13.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 ranks
这段代码是Epoch训练过程中最后一部分代码,主要包括以下内容:
- tnow = time.time() 这行代码用于获取当前时间。
- self.epoch_time = tnow - self.epoch_time_start 这行代码计算当前轮次的训练时间,通过当前时间减去上一个轮次开始的时间。
- self.epoch_time_start = tnow 这行代码更新当前轮次开始的时间为当前时间,为下一轮次的计时做准备。
- self.run_callbacks('on_fit_epoch_end') 这行代码在每个训练轮次结束时运行回调函数。
- torch.cuda.empty_cache() 这行代码用于清空GPU显存,以防止在训练过程中出现内存不足的错误。这个操作可以释放GPU显存中的临时变量和不再使用的内存。
- if RANK != -1: 这个条件语句检查当前进程的排名(RANK)是否不为-1,用于判断是否进行分布式训练。
- broadcast_list = [self.stop if RANK == 0 else None] 这行代码创建了一个广播列表,其中包含了停止训练的标志(self.stop)。如果当前进程的排名为0(即主进程),则将self.stop添加到广播列表中;否则,将列表中的元素设为None。
- dist.broadcast_object_list(broadcast_list, 0) 这行代码将广播列表中的对象(包括停止训练的标志)广播给所有进程。0表示广播的根进程,即主进程。
- if RANK != 0: 这个条件语句检查当前进程的排名是否不为0,即非主进程。
- self.stop = broadcast_list[0] 这行代码将广播列表中的第一个对象(即进程0广播的self.stop)赋值给当前进程的self.stop变量。
- if self.stop: 这个条件语句检查self.stop是否为True,即是否需要停止训练。
- break 如果需要停止训练,则使用break语句跳出当前的训练循环,结束训练过程。
这段代码主要涉及训练时间的计算、回调函数的执行、GPU显存的清空以及分布式训练中的停止信号广播和接收。
if 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')
上述代码是train代码的最后部分代码:
1. if RANK in (-1, 0):这个代码块的目的是在变量 "RANK" 的值为 -1 或 0 时执行特定的操作。这些操作包括打印日志信息、执行最终评估(final_eval())、绘制指标图表(如果指定了相关参数)、运行回调函数、清空 GPU 内存以及运行拆卸回调函数。
2.LOGGER.info() 函数用于记录信息日志。在这段代码中,它用于记录完成的轮次数、训练所花费的时间(以小时为单位),以及其他在字符串格式中指定的相关信息。
3. final_eval() 函数的目的是执行模型的最终评估。它在完成一定数量的轮次后被调用,通常用于在单独的验证数据集上评估模型的性能。
4.条件 "if self.args.plots:" 检查变量 "plots" 是否为真或非空值。如果条件为真,表示启用了指标图表的绘制,代码将继续执行 self.plot_metrics() 函数。
5.torch.cuda.empty_cache() 函数用于清空 GPU 的显存。它释放临时变量和不再使用的内存,帮助防止训练过程中出现内存不足的错误。
6.回调函数是在训练过程中特定时间点执行的函数,用于执行额外的操作或动作。在这段代码中,回调函数在每个轮次结束时执行("on_fit_epoch_end"),以及在训练过程结束时执行("on_train_end")。回调函数的具体操作取决于其实现,根据训练过程或模型的需求而有所不同。
至此,_do_train()循环过程逐步解析完成。