# YOLOv5 🚀 by Ultralytics, GPL-3.0 license
"""
Train a YOLOv5 model on a custom dataset.
Models and datasets download automatically from the latest YOLOv5 release.
Usage - Single-GPU training:
$ python train.py --data coco128.yaml --weights yolov5s.pt --img 640 # from pretrained (recommended)
$ python train.py --data coco128.yaml --weights '' --cfg yolov5s.yaml --img 640 # from scratch
Usage - Multi-GPU DDP training:
$ python -m torch.distributed.run --nproc_per_node 4 --master_port 1 train.py --data coco128.yaml --weights yolov5s.pt --img 640 --device 0,1,2,3
Models: https://github.com/ultralytics/yolov5/tree/master/models
Datasets: https://github.com/ultralytics/yolov5/tree/master/data
Tutorial: https://github.com/ultralytics/yolov5/wiki/Train-Custom-Data
这里是开头作者注释的一个部分,意在说明一些项目基本情况。第一行表示我们用的模型是YOLOv5;
第二行表示我们传入的data数据集是coco128数据集,有128张图片,80个类别,使用的权重模型是yolov5s模型,-img表示图片大小640。
"""
'''======================1.导入安装好的python库====================='''
import argparse # 解析命令行参数模块 它是一个用于命令项选项与参数解析的模块,通过在程序中定义好我们需要的参数,argparse 将会从 sys.argv 中解析出这些参数,并自动生成帮助和使用信息
import math # 数学公式模块 调用这个库进行数学运算
import os # 与操作系统进行交互的模块 包含文件路径操作和解析 它提供了多种操作系统的接口。通过OS模块提供的操作系统接口,我们可以对操作系统里文件、终端、进程等进行操作
import random # 生成随机数模块 是使用随机数的Pvthon标准库。random库主要用于生成随机数
import sys # sys系统模块 包含了与Python解释器和它的环境有关的函数 它是与python解释器交互的一个接口,该模块提供对解释器使用或维护的一些变量的访问和获取,它提供了许多函数和变量来处理 Python 运行时环境的不同部分
import time # 时间模块 更底层 Python中处理时间的标准库,是最基础的时间处理库
from copy import deepcopy # 深度拷贝模块 Python 中赋值语句不复制对象,而是在目标和对象之间创建绑定(bindings) 关系。cop模块提供了通用的浅层复制和深层复制操作
from datetime import datetime # datetime模块能以更方便的格式显示日期或对日期进行运算。 是Python常用的一个库,主要用于时间解析和计算。
from pathlib import Path # Path将str转换为Path对象 使字符串路径易于操作的模块 这个库提供了一种面向对象的方式来与文件系统交互,可以让代码更简洁、更易读
import numpy as np # numpy数组操作模块 科学计算库,提供了矩阵,线性代数,傅立叶变换等等的解决方案,最常用的是它的N维数组对象
import torch # 引入torch 这是主要的Pytorch库。它提供了构建、训练和评估神经网络的工具
import torch.distributed as dist # 分布式训练模块 torch.distributed包提供Pytorch支持和通信基元,对多进程并行,在一个或多个机器上运行的若干个计算阶段
import torch.nn as nn # 对torch.nn.functional的类的封装 有很多和torch.nn.functional相同的函数 torch下包含用于搭建神经网络的modules和可用于继承的类的一个子包
import yaml # yaml是一种直观的能够被电脑识别的的数据序列化格式,容易被人类阅读,并且容易和脚本语言交互。一般用于存储配置文件。
from torch.cuda import amp # PyTorch amp自动混合精度训练模块 自动混合精度训练 -- 节省显存并加快推理速度。
from torch.nn.parallel import DistributedDataParallel as DDP # 多卡训练模块 构建分布式模型,并行加速程度更高,且支持多节点多gpu的硬件拓扑结构t
from torch.optim import SGD, Adam, lr_scheduler # tensorboard模块 优化器 Optimizer,主要是在模型训练阶段对模型可学习参数进行更新,常用优化器有 SGD,RMSprop,Adam等
from tqdm import tqdm # 进度条模块 就是我们看到的训练时进度条显示
'''===================2.获取当前文件的绝对路径========================'''
"""
这段代码会获取当前文件的绝对路径,并使用Path库将其转换为Path对象
这一部分的主要作用有两个:
1.将当前项目添加到系统路径上,以使得项目中的模块可以调用
2.将当前项目的相对路径保存在ROOT中,便于寻找项目中的文件.
"""
FILE = Path(__file__).resolve() # __file__指的是当前文件(即train.py),FILE最终保存着当前文件的绝对路径,比如D://yolov5/train.py
ROOT = FILE.parents[0] # YOLOv5 root directory ROOT保存着当前项目的父目录,比如 D://yolov5
if str(ROOT) not in sys.path: # sys.path即当前python环境可以运行的路径,假如当前项目不在该路径中,就无法运行其中的模块,所以就需要加载路径
sys.path.append(str(ROOT)) # add ROOT to PATH 把ROOT添加到运行路径上
ROOT = Path(os.path.relpath(ROOT, Path.cwd())) # relative ROOT设置为相对路径
#####################加载自定义模块###########
'''===================3..加载自定义模块============================'''
"""
这些都是用户自定义的库,由于上一步已经把路径加载上了,所以现在可以导入,这个顺序不可以调换。具体来说,代码从如下几个文件中导入了部分函数和类。
通过导入这些模块,可以更方便地进行目标检测的相关任务,并且减少了代码的复杂度和冗余,
"""
from models1.utils1.loggers.comet.comet_utils import check_comet_resume
import val # for end-of-epoch mAP##这个是测试集,我们下一篇再具体讲
from models.experimental import attempt_load #实验性质的代码,包括MixConv2d、跨层权重Sum等
from models.yolo import Model ##yolo的特定模块,包括BaseModel,DetectionModel,ClassificationModel,parse model等
from models1.utils1.autoanchor import check_anchors##定义了自动生成锚框的方法
from models1.utils1.autobatch import check_train_batch_size#定义了自动生成批量大小的方法
from models1.utils1.callbacks import Callbacks#定义了回调函数,主要为logger服务
from models1.utils1.dataloaders import create_dataloader #dateset和dateloader定义代码
from models1.utils1.downloads import attempt_download##谷歌云盘内容下载
from models1.utils1.general import (LOGGER, TQDM_BAR_FORMAT, check_amp, check_dataset, check_file, check_git_info,
check_git_status, check_img_size, check_requirements, check_suffix, check_yaml, colorstr,
get_latest_run, increment_path, init_seeds, intersect_dicts, labels_to_class_weights,
labels_to_image_weights, methods, one_cycle, print_args, print_mutation, strip_optimizer,
yaml_save)#定义了一些常用的工具函数,比如检查文件是否存在、检查图像大小是否符合要求、打印命令行参数等等
from models1.utils1.loggers import Loggers#日志打印
from models1.utils1.loggers.wandb.wandb_utils import check_wandb_resume
from models1.utils1.loss import ComputeLoss#存放各种损失函数
from models1.utils1.metrics import fitness##模型验证指标,包括ap,混淆矩阵等
from models1.utils1.plots import plot_evolve, plot_labels#定义了Annotator类,可以在图像上绘制矩形框和标注信息
from models1.utils1.torch_utils import EarlyStopping, ModelEMA, de_parallel, select_device, torch_distributed_zero_first#定义了一些与PyTorch有关的工具函数,比如选择设备、同步时间等
from models1.utils1.torch_utils import (EarlyStopping, ModelEMA, de_parallel, select_device, smart_DDP, smart_optimizer,
smart_resume, torch_distributed_zero_first)
from models1.utils1.downloads import attempt_download, is_url
"""
查找名为LOCAL_RANK,RANK,WORLD_SIZE的环境变量,若存在则返回环境变量的值,若不存在则返回第二个参数《-1,默认None)
rank和local_rank的区别: 两者的区别在于前者用于进程间通讯,后者用于本地设备分配.
接下来是设置分布式训练时所需的环境变量。分布式训练指的是多GPU训练,将训练参数分布在多个GPU上进行训练,有利于提升训练效率。
"""
###################分布式训练初始化#############
LOCAL_RANK = int(os.getenv('LOCAL_RANK', -1)) # https://pytorch.org/docs/stable/elastic/run.html##本地序列号。这个worker是这台机器上第几个worker
RANK = int(os.getenv('RANK', -1))#进程序列号。这个worker是全程第几个worker
WORLD_SIZE = int(os.getenv('WORLD_SIZE', 1))##总共有几个worker-->进程
GIT_INFO = check_git_info()
"""
这段代码主要是检查分布式训练的环境:
若RANK为-1或0,会执行下面三行代码,打印参数并检查github仓库和依赖库。
第一行代码,负责打印文件所用到的参数信息,这个参数包括命令行传入进去的参数以及默认参数
第二行代码,检查yolov5的github仓库是否更新,如果更新的话,会有一个提示
第三行代码,检查requirements中要求的安装包有没有正确安装成功,没有成功的话会给予一定的提示
"""
def train():
pass
def main(opt,callbacks=Callbacks()):
"""
检查分布式训练环境
"""
if RANK in [-1,0]:##若进程编号为-1,0
###输出所有的训练参数/参数以彩色的方式表现
print_args(FILE.stem,opt)##filename返回路径中的文件名,stem返回路径中的文件名,但是不带扩展名,extension返回路径中文件的扩展名。 负责打印文件所用到的参数信息,这个参数信息包括命令行传入进去的参数以及默认参数
##检查YOLO v5的github仓库是否更新,若已更新,给出提示
check_git_status()
##检查requirements.txt所需包是否都满足
check_requirements(exclude=['thop'])##没有成功的话,会给予一定的提示
"""
这段代码主要是关于断点训练的判断和准备。
断点训练是当训练异常终止或想调节超参数时,系统会保留训练异常终止前的超参数与训练参数,当下次训练开始时,并不会从头开始而是从上次中断的地方继续训练。
。使用断点续训,就从last.pt中读取相关参数
。不使用断点续训,就从文件中读取相关参数
"""
# Resume (from specified or most recent last.pt)
# 初始化可视化工具wandb,wandb使用教程看https://zhuanlan.zhihu.com/p/266337608
# 断点训练使用教程可以查看:https://blog.csdn.net/CharmsLUO/article/details/123410081
# Resume 2.从中断恢复(接着上一次继续训练)
if opt.resume and not check_comet_resume(opt) and not opt.evolve:##resume an
# isinstance()是否是已经知道的类型
# 如果resume是True,则通过get_lastest_run()函数找到runs文件夹中最近的权重文件last.pt
# 如果opt.resume为True表示需要恢复中断的任务,
# check_wandb_resume(opt)返回False表示训练没有被wandb恢复,
# opt.evolve返回False表示不是在执行遗传算法
last = Path(check_file(opt.resume) if isinstance(opt.resume, str) else get_latest_run())
opt_yaml = last.parent.parent / 'opt.yaml' # train options yaml# 构造一个路径,指向最近运行结果所在的路径的父级目录的父级目录下的opt.yaml文件。
# 将程序所使用的数据集存储到变量opt_data中,以便后续使用。
opt_data = opt.data # original dataset
if opt_yaml.is_file():# 检查opt.yaml是否存在
with open(opt_yaml, errors='ignore') as f:# 存在则打开该文件
d = yaml.safe_load(f)# 解析文件的内容并以字典的形式加载,存储在d变量中
else: #若opt.yaml不存在
# 读取最近运行结果的文件并加载其中保存的PyTorch模型数据及其它信息
d = torch.load(last, map_location='cpu')['opt']
# 将之前从文件中读取到的训练选项信息转换成一个argparse.Namespace对象
# 使用argparse.Namespace()构造一个命名空间对象opt
# 并且将之前从文件中读取到的训练选项信息以字典的形式传给Namespace的构造函数
# **是用来对一个字典进行解包的操作
# # replace注释说明将opt对象更新为从文件中读取到的训练选项
opt = argparse.Namespace(**d) # replace
# opt.cfg属性设置为空字符串(''),opt.weights属性设置为last文件名,opt.resume属性设置为True
# 这些属性指定配置文件的路径、权重文件的路径以及是否恢复模型训练过程等选项。
# opt.cfg设置为"",对应着train函数里面的操作(加载权重时是否加载权重里的anchor)
opt.cfg, opt.weights, opt.resume = '', str(last), True # reinstate
if is_url(opt_data):# 将文件路径保存在opt.data属性中
opt.data = check_file(opt_data) # avoid HUB resume auth timeout
else:# 不恢复训练,这里不说了
opt.data, opt.cfg, opt.hyp, opt.weights, opt.project = \
check_file(opt.data), check_yaml(opt.cfg), check_yaml(opt.hyp), str(opt.weights), str(opt.project) # checks
assert len(opt.cfg) or len(opt.weights), 'either --cfg or --weights must be specified'
if opt.evolve:
if opt.project == str(ROOT / 'runs/train'): # if default project name, rename to runs/evolve
opt.project = str(ROOT / 'runs/evolve')
opt.exist_ok, opt.resume = opt.resume, False # pass resume to exist_ok and disable resume
if opt.name == 'cfg':
opt.name = Path(opt.cfg).stem # use model.yaml as name
opt.save_dir = str(increment_path(Path(opt.project) / opt.name, exist_ok=opt.exist_ok))
# DDP mode-->支持多机多卡、分布式训练
"""
这段代码主要是检查DDP训练的配置,并设置GPU
DDP (Distributed Data Parallel) 用于单机或多机的多GPU分布式训练,但目前DDP只能在Linux下使用。这部分它会选择你是使用cpu
还是gpu,假如你采用的是分布式训练的话,它就会额外执行下面的一些操作,我们这里一般不会用到分布式,所以也就没有执行什么东西。
接下来是关于断点训练的判断和准备。断点训练是当训练异常终止或想调节超参数时,系统会保留训练异常终止前的超参数与训练参数,当下次训练开始时,并不会从头开始,而是从上次中断的地方继续训练。
超参数进化:超参数进化是一种使用遗传算法,进行超参数优化的方法。遗传算法是用于解决最优化问题的一种搜索算法,它将要解决的问题模拟为一个生物进化的过程,过复制、交叉、突变等操作产生下一代的解,并逐步淘汰掉适应度函数值低的解,增加适应度函数值高的解。YOLO v5的超参数比较多,用传统的网格搜索会让超参数的调整变得很棘手。这时使用遗传算法调整超参数就是一种合适的方式。
"""
##选择远程装载的位置
device = select_device(opt.device, batch_size=opt.batch_size)
##当进程中的GPU编号不为-1时,才会进入DPP
if LOCAL_RANK != -1:
msg = 'is not compatible with YOLOv5 Multi-GPU DDP training'
##不能使用图片采样策略
assert not opt.image_weights, f'--image-weights {msg}'
##不能使用超参数进化
assert not opt.evolve, f'--evolve {msg}'
assert opt.batch_size != -1, f'AutoBatch with --batch-size -1 {msg}, please pass a valid --batch-size'
assert opt.batch_size % WORLD_SIZE == 0, f'--batch-size {opt.batch_size} must be multiple of WORLD_SIZE'
assert torch.cuda.device_count() > LOCAL_RANK, 'insufficient CUDA devices for DDP command'
torch.cuda.set_device(LOCAL_RANK)
device = torch.device('cuda', LOCAL_RANK)
dist.init_process_group(backend="nccl" if dist.is_nccl_available() else "gloo")
"""
检查DDP训练的配置,并设置GPU。DDP(Distributed Data Parallel)用于单机或多机的多GPU分布式训练,但目前DDP只能在Linux下使用
这段代码是不进行进化训练的情况,此时正常训练
如果输入evolve会执行ese下面这些代码,因为我们没有输入evolve并且不是分布式训练,因此会执行train函数。
也就是说,当不使用超参数进化训练时,直接把命令行参数传入train函数,训练完成后销毁所有进程。
当不使用超参数进化时,直接把命令行参数传入train函数,训练完成后销毁所有进程。train函数暂时跳过,先分析main()函数。
"""
# Train
if not opt.evolve: # 不采用进化超参数
train(opt.hyp, opt, device, callbacks)
"""接下来我们再看看使用超参数进化训练的情况:这段代码是使用超参数进化训练的前期准备
首先指定每个超参数的突变范围、最大值、最小值,再为超参数的结果保存做好准备。
遗传算法调参: 遵循适者生存、优胜劣汰的法则,即寻优过程中保留有用的,去除无用的。遗传算法需要提前设置4个参数: 群体大小/进化代数/交叉概率/变异概率
这段代码是使用超参数进化训练的前期准备。
首先指定每个超参数的突变范围、最大值、最小值,再为超参数的结果保存做好准备。
"""
# Evolve hyperparameters (optional)
else:
# 超参数列表(突变范围 - 最小值 - 最大值)
# Hyperparameter evolution metadata (mutation scale 0-1, lower_limit, upper_limit)
meta = {
'lr0': (1, 1e-5, 1e-1), # initial learning rate (SGD=1E-2, Adam=1E-3)
'lrf': (1, 0.01, 1.0), # final OneCycleLR learning rate (lr0 * lrf)
'momentum': (0.3, 0.6, 0.98), # SGD momentum/Adam beta1
'weight_decay': (1, 0.0, 0.001), # optimizer weight decay
'warmup_epochs': (1, 0.0, 5.0), # warmup epochs (fractions ok)
'warmup_momentum': (1, 0.0, 0.95), # warmup initial momentum
'warmup_bias_lr': (1, 0.0, 0.2), # warmup initial bias lr
'box': (1, 0.02, 0.2), # box loss gain
'cls': (1, 0.2, 4.0), # cls loss gain
'cls_pw': (1, 0.5, 2.0), # cls BCELoss positive_weight
'obj': (1, 0.2, 4.0), # obj loss gain (scale with pixels)
'obj_pw': (1, 0.5, 2.0), # obj BCELoss positive_weight
'iou_t': (0, 0.1, 0.7), # IoU training threshold
'anchor_t': (1, 2.0, 8.0), # anchor-multiple threshold
'anchors': (2, 2.0, 10.0), # anchors per output grid (0 to ignore)
'fl_gamma': (0, 0.0, 2.0), # focal loss gamma (efficientDet default gamma=1.5)
'hsv_h': (1, 0.0, 0.1), # image HSV-Hue augmentation (fraction)
'hsv_s': (1, 0.0, 0.9), # image HSV-Saturation augmentation (fraction)
'hsv_v': (1, 0.0, 0.9), # image HSV-Value augmentation (fraction)
'degrees': (1, 0.0, 45.0), # image rotation (+/- deg)
'translate': (1, 0.0, 0.9), # image translation (+/- fraction)
'scale': (1, 0.0, 0.9), # image scale (+/- gain)
'shear': (1, 0.0, 10.0), # image shear (+/- deg)
'perspective': (0, 0.0, 0.001), # image perspective (+/- fraction), range 0-0.001
'flipud': (1, 0.0, 1.0), # image flip up-down (probability)
'fliplr': (0, 0.0, 1.0), # image flip left-right (probability)
'mosaic': (1, 0.0, 1.0), # image mixup (probability)
'mixup': (1, 0.0, 1.0), # image mixup (probability)
'copy_paste': (1, 0.0, 1.0)} # segment copy-paste (probability)
##加载默认超参数
with open(opt.hyp, errors='ignore') as f:
hyp = yaml.safe_load(f) # load hyps dict
###如果超参数文件中没有“anchors’,则设为3
if 'anchors' not in hyp: # anchors commented in hyp.yaml
hyp['anchors'] = 3
if opt.noautoanchor:
del hyp['anchors'], meta['anchors']
##使用进化算法时,仅在最后的epoch测试和保存
opt.noval, opt.nosave, save_dir = True, True, Path(opt.save_dir) # only val/save final epoch
# ei = [isinstance(x, (int, float)) for x in hyp.values()] # evolvable indices
evolve_yaml, evolve_csv = save_dir / 'hyp_evolve.yaml', save_dir / 'evolve.csv'
if opt.bucket:
os.system(f'gsutil cp gs://{opt.bucket}/evolve.csv {evolve_csv}') # download evolve.csv if exists
"""
若使用超参数进化,以上代码为超参数训练的前期准备,首先指定每个超参数的突变范围、最大值、最小值,再为超参数的结果保存做好准备。
这段代码是开始超参数进化训练。
超参数进化的步骤如下:
。1.若存在evolve.csv文件,读取文件中的训练数据,选择超参进化方式,结果最优的训练数据突变超参数
。2.限制超参进化参数hyp在规定范围内
。3.使用突变后的超参数进行训练,测试其效果
。4.训练结束后,将训练结果可视化,输出保存信息保存至evolution.csv,用于下一次的超参数突变
原理:根据生物进化,优胜劣汰,适者生存的原则,每次迭代都会保存更优秀的结果,直至迭代结束。最后的结果即为最优的超参数注意:使用超参数进化时要经过至少300次跌代,每次跌代都会经过一次完整的训练。因此超参数进化及其耗时,大家需要根据自己需求慎用
"""
for _ in range(opt.evolve): # generations to evolve
if evolve_csv.exists(): # if evolve.csv exists: select best hyps and mutate
# Select parent(s)
parent = 'single' # parent selection method: 'single' or 'weighted'
x = np.loadtxt(evolve_csv, ndmin=2, delimiter=',', skiprows=1)
n = min(5, len(x)) # number of previous results to consider
x = x[np.argsort(-fitness(x))][:n] # top n mutations
w = fitness(x) - fitness(x).min() + 1E-6 # weights (sum > 0)
if parent == 'single' or len(x) == 1:
# x = x[random.randint(0, n - 1)] # random selection
x = x[random.choices(range(n), weights=w)[0]] # weighted selection
elif parent == 'weighted':
x = (x * w.reshape(n, 1)).sum(0) / w.sum() # weighted combination
# Mutate
mp, s = 0.8, 0.2 # mutation probability, sigma
npr = np.random
npr.seed(int(time.time()))
g = np.array([meta[k][0] for k in hyp.keys()]) # gains 0-1
ng = len(meta)
v = np.ones(ng)
while all(v == 1): # mutate until a change occurs (prevent duplicates)
v = (g * (npr.random(ng) < mp) * npr.randn(ng) * npr.random() * s + 1).clip(0.3, 3.0)
for i, k in enumerate(hyp.keys()): # plt.hist(v.ravel(), 300)
hyp[k] = float(x[i + 7] * v[i]) # mutate
# Constrain to limits
for k, v in meta.items():
hyp[k] = max(hyp[k], v[1]) # lower limit
hyp[k] = min(hyp[k], v[2]) # upper limit
hyp[k] = round(hyp[k], 5) # significant digits
# Train mutation
results = train(hyp.copy(), opt, device, callbacks)
callbacks = Callbacks()
# Write mutation results
keys = ('metrics/precision', 'metrics/recall', 'metrics/mAP_0.5', 'metrics/mAP_0.5:0.95', 'val/box_loss',
'val/obj_loss', 'val/cls_loss')
print_mutation(keys, results, hyp.copy(), save_dir, opt.bucket)
# Plot results
plot_evolve(evolve_csv)
LOGGER.info(f'Hyperparameter evolution finished {opt.evolve} generations\n'
f"Results saved to {colorstr('bold', save_dir)}\n"
f'Usage example: $ python train.py --hyp {evolve_yaml}')
"""
接下来开始超参数进化,我将超参数进化的步骤总结为以下几步:1)若存在训练数据文件,读取文件中的训练数据,选择结果最优的训练数据突变超参数;2)将突变后的超参数限定值阈值范围内;3)使用突变后的超参数进行训练;4)训练结束后,将训练结果保存至evolution.csv,用于下一次的超参数突变。根据生物进化,优胜劣汰,适者生存的原则,每次迭代都会保存更优秀的结果,直至迭代结束。最后的结果即为最优的超参数。
使用超参数进化时至少要经过至少300次迭代,每次迭代都会经过一次完整的训练。因此超参数进化及其耗时,根据自己需求慎用。
"""
def parse_opt(known=False):
parser = argparse.ArgumentParser()
##预训练权重文件
parser.add_argument('--weights', type=str, default=ROOT / 'yolov5s.pt', help='initial weights path')
##训练模型 模型配置文件,网络结构
parser.add_argument('--cfg', type=str, default='', help='model.yaml path')
#训练路径,包括训练集、验证集、测试集的路径,类别总数等 数据集配置文件,数据集路径,类名等
parser.add_argument('--data', type=str, default=ROOT / 'data/coco128.yaml', help='dataset.yaml path')
##hpy超参数设置文件(lr/sgd/mixup),deta/hyps/下面有5个超参数设置文件,每个文件的超参数初始值有细微区别,用户可以根据自己的需求选择其中一个
parser.add_argument('--hyp', type=str, default=ROOT / 'data/hyps/hyp.scratch-low.yaml', help='hyperparameters path')
##epochs:训练总轮数,默认为300轮
parser.add_argument('--epochs', type=int, default=100, help='total training epochs')
##batchsize:训练批次,默认bs=16
parser.add_argument('--batch-size', type=int, default=16, help='total batch size for all GPUs, -1 for autobatch')
##imagesize:设置图片分辨率大小,默认640*640
parser.add_argument('--imgsz', '--img', '--img-size', type=int, default=640, help='train, val image size (pixels)')
##rect:是否采用矩形训练,默认为False
parser.add_argument('--rect', action='store_true', help='rectangular training')
##resume:是否接着上次的训练结果,继续训练
parser.add_argument('--resume', nargs='?', const=True, default=False, help='resume most recent training')
#nosave:不保存模型,默认为False(保存) 在./runs/exp*/train/weights/保存两个模型 一个是最后一次的模型 一个是最好的模型
##best.pt/ last.pt 不建议运行代码添加 --nosave
parser.add_argument('--nosave', action='store_true', help='only save final checkpoint')
#noval:最后进行测试,设置了之后,就是训练结束都测试一下,不设置每轮都计算map,建议不设置
parser.add_argument('--noval', action='store_true', help='only validate final epoch')
##noautoanchor:不自动调整anchor,默认False,自动调整anchor
parser.add_argument('--noautoanchor', action='store_true', help='disable AutoAnchor')
parser.add_argument('--noplots', action='store_true', help='save no plot files')
##evolve:参数进化,遗传算法调参
parser.add_argument('--evolve', type=int, nargs='?', const=300, help='evolve hyperparameters for x generations')
##bucket:谷歌优盘/一般用不上
parser.add_argument('--bucket', type=str, default='', help='gsutil bucket')
##cache:是否提前缓存图片到内存,以加快训练速度,默认为False
parser.add_argument('--cache', type=str, nargs='?', const='ram', help='image --cache ram/disk')
##image-weight:使用图片采样策略:默认不采用
parser.add_argument('--image-weights', action='store_true', help='use weighted image selection for training')
##device:设备选择
parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')
#multi-scale:是否进行多尺度训练
parser.add_argument('--multi-scale', action='store_true', help='vary img-size +/- 50%%')
##single-cls:数据集是否多类/默认true
parser.add_argument('--single-cls', action='store_true', help='train multi-class data as single-class')
##optimizer:提供了3种优化器
parser.add_argument('--optimizer', type=str, choices=['SGD', 'Adam', 'AdamW'], default='SGD', help='optimizer')
##sync-bn:是否使用跨卡同步BN,在DDP模式使用
parser.add_argument('--sync-bn', action='store_true', help='use SyncBatchNorm, only available in DDP mode')
##dataloader的最大worker数量(使用多线程加载图片)
parser.add_argument('--workers', type=int, default=8, help='max dataloader workers (per RANK in DDP mode)')
##训练结果的保存路径
parser.add_argument('--project', default=ROOT / 'runs/train', help='save to project/name')
##训练结果的文件名称
parser.add_argument('--name', default='exp', help='save to project/name')
##项目位置是否存在 默认不存在
parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment')
##四元数据加载器 :允许在较低 --img 尺寸下进行更高--img尺寸训练的一些好处
parser.add_argument('--quad', action='store_true', help='quad dataloader')
##cos-lr:余炫学习率
parser.add_argument('--cos-lr', action='store_true', help='cosine LR scheduler')
##标签平滑/默认不增强,用户可以根据自己标签的实际情况设置这个参数,建议设置小一点0.1/0.05
parser.add_argument('--label-smoothing', type=float, default=0.0, help='Label smoothing epsilon')
##早停止耐心次数/100次不更新就停止训练
parser.add_argument('--patience', type=int, default=100, help='EarlyStopping patience (epochs without improvement)')
##freeze冻结训练 可以设置default=[0] 数据量大的情况下,建议不设置此参数
parser.add_argument('--freeze', nargs='+', type=int, default=[0], help='Freeze layers: backbone=10, first3=0 1 2')
##save-preid:多少个epoch保存一个checkpoint
parser.add_argument('--save-period', type=int, default=-1, help='Save checkpoint every x epochs (disabled if < 1)')
##--local_rank 进程编号/多卡使用
parser.add_argument('--seed', type=int, default=0, help='Global training seed')
##GPU编号
parser.add_argument('--local_rank', type=int, default=-1, help='Automatic DDP Multi-GPU argument, do not modify')
##在线可视化工具,类似于tensorboard工具
# Logger arguments
parser.add_argument('--entity', default=None, help='Entity')
# upload dataset: 是否上传dataset到wandb tabel(将数据集作为交互式 dsviz表 在浏览器中查看、查询、筛选和分析数据集)默认False
parser.add_argument('--upload_dataset', nargs='?', const=True, default=False, help='Upload data, "val" option')
# bbox interval: 设置界框图像记录间隔 Set bounding-box image logging interval for W&B 默认-1 opt.epochs //10
parser.add_argument('--bbox_interval', type=int, default=-1, help='Set bounding-box image logging interval')
##使用数据的版本
parser.add_argument('--artifact_alias', type=str, default='latest', help='Version of dataset artifact to use')
# 作用就是当仅获取到基本设置时,如果运行命令中传入了之后才会获取到的其他配置,不会报错:而是将多出来的部分保存起来,留到后面使用
return parser.parse_known_args()[0] if known else parser.parse_args()
def train(hyp,##超参数 可以是超参数配置文件的路径或超参数字典 path/to/hyp.yaml
opt,##main中的opt参数
device,##当前设备
callbacks##用于存储Loggers日志记录器中的函数,方便在每个训练阶段控制日志的记录情况
):
##从opt获取参数。日志保存路径、轮次、批次、权重、进程序号(主要用于分布式训练)等
# save_dir,epochs,batch_size,weights,single_cls,evolve,data,cfg,resume,noval,nosave,workers,freeze=\
# Path(opt.save_dir),opt.epochs,opt.batch_size,opt.weights,opt.single_cls,opt.evolve,opt.data,opt.cfg,\
# opt.resume,opt.noval,opt.nosave,opt.wprkers,opt.freeze
##创建一些变量存储解析到的参数
save_dir, epochs, batch_size, weights, single_cls, evolve, data, cfg, resume, noval, nosave, workers, freeze = \
Path(opt.save_dir), opt.epochs, opt.batch_size, opt.weights, opt.single_cls, opt.evolve, opt.data, opt.cfg, \
opt.resume, opt.noval, opt.nosave, opt.workers, opt.freeze
##回调函数:对象列表或对象,用于在不同的训练过程中进行一些数据的处理和展示
callbacks.run('on_pretrain_routine_start')##训练过程即将开始,用于执行一些训练预处理或初始化的操作
"""
train()函数首先做的就是接收参数。hyp为超参数,不使用超参数进化的前提下也可以从opt中获取;opt指的是全部的命令行参数;device指的是装载程序的设备;
callbacks指的是训练过程中产生的一些参数。
hyp:超参数,不使用超参数进化的前提下也可以从opt中获取
opt:全部的命令行参数
device:指的是装载程序的设备
callbacks:指的是训练过程中产生的一些参数
"""
"""
创建训练权重目录,设置模型、txt等保存的路径
这段代码主要是创建权重文件保存路径,权重名宇和训练日志txt文件
每次训练结束后,系统会产生两个模型,一个是last,pt,一个是best.pt,
顾名思义,last.pt即为训练最后一轮产生的模型,而bestpt是训练过程中,效果最好的模型。
然后创建文件夹,保存训练结果的模型文件路径 以及验证集输出结果的txt文件路径,包含迭代的次数,
占用显存大小,图片尺寸,精确率,召回率,位置损失,类别损失,置信度损失和map等。
"""
##Directories 获取记录训练日志的保存路径
##设置保存权重路径 如runs/train/exp1/weights
w=save_dir/'weights'##weightd dir
##新建文件夹 weights train evolve
(w.parent if evolve else w).mkdir(parents=True,exist_ok=True)##mkdir dir 权重保存的路径
last,best=w/'last.pt',w/'best.pt'##保存最后一次和最好一次权重
##Hyperameters 超参数
##hyp是字典形式或字符串形式
#若是字典形式,则保存了超参数的键值对,无需解析
##若是字符串,则保存了以yaml格式保存的文件路径
# Hyperparameters
"""
这段代码主要是加载一些训练过程中需要使用的超参数,并打印出来
首先,检查超参数是宁典还是宁符串,若为字符串,则认定为.yam文件路径,再将yaml文件加载为字典。这里导致超参数的数据类型不同的原因是,超参数进化时,传入train0函数的超参数即为宁典。
而从命令行参数中读取的则为文件路径
然后将打印这些超参数。
接下来设置模型保存的路径。训练结束后,系统会产生两个模型,一个是last.pt,一个是best.pt。顾名思义,last.pt即为训练最后一轮产生的模型,而best.pt是训练过程中,
效果最好的模型。那么何谓训练效果最好,评判效果最好的标准又是什么?
在这里评判best.pt的训练效果好坏的标准,绝大部分依赖的是mAP@0.5:0.95,是根据上文在超参数的突变时,提到的fitness()函数得出,后面遇到了再细讲。
"""
# 判断hyp是字典还是字符串
if isinstance(hyp, str):##isinstance()是否是已知类型。判断hyp是字典还是字符串
##若hyp是字符串,则认定为路径,则加载超参数为字典
with open(hyp, errors='ignore') as f:
hyp = yaml.safe_load(f) # load hyps dict
"""
检查超参数是字典还是字符串,若为字符串,则认定为.yaml文件路径,再将yaml文件加载为字典。这里导致超参数的数据类型不同的原因是,超参数进化时,
传入train()函数的超参数即为字典。而从命令行参数中读取的则为文件路径。
"""
##打印超参数 彩色字体
LOGGER.info(colorstr('hyperparameters: ') + ', '.join(f'{k}={v}' for k, v in hyp.items()))
opt.hyp = hyp.copy() # for saving hyps to checkpoints
# Save run settings
if not evolve: # 如果不使用进化超参数
yaml_save(save_dir / 'hyp.yaml', hyp) # 将超参数信息以yaml形式保存
yaml_save(save_dir / 'opt.yaml', vars(opt)) # 将参数信息转换为字典形式并以yaml形式保存
# Loggers 日志记录 具体可以看一下Loggers这个类
# 使用哪种形式进行记录 clearml形式或wandb形式
# 记录结果就是result.png中的结果
# Loggers
"""
这段代码主要是打印日志信息(logger + wandb)
首先设置wandb和tb两种日志,并开始初始化日志记录器实例。
然后将日志记录器中的函数记录到callbacks内,方便在训练的不同阶段,利用callbacks.run()函数对日志的记录做统一处理。
在日志文件中,基于wandb与tensorboard这两个库来完成训练过程中的可视化操作。在这个文件中完成对于程序训练日志的记录过程
"""
#1.5 加载相关日志功能:如tensorboard,logger,wandb
#Loggers 设置wandb和tb两种日志,wandb和tensorboard都是模型信息,指标可视化工具
data_dict = None
if RANK in {-1, 0}: ##如果进程编号为-1或0
##初始化日志记录器实例
loggers = Loggers(save_dir, weights, opt, hyp, LOGGER) # loggers instance
# Register actions
# 将Loggers类中定义的非私有方法注册到回调函数中,以在训练过程中对每个回调函数进行调用。
# Register actions
for k in methods(loggers):
callbacks.register_action(k, callback=getattr(loggers, k))
# Process custom dataset artifact link
data_dict = loggers.remote_dataset
if resume: # If resuming runs from remote artifact
weights, epochs, hyp, batch_size = opt.weights, opt.epochs, opt.hyp, opt.batch_size
##加载日志记录器,并将日志记录器中的函数记录到callbacks内,方便在训练的不同阶段,
# 利用callbacks.run()函数对日志的记录做统一处理。
# Config 画图
"""
这段代码主要作用是做一些变量的配置
首先根据plots的true或者false来判断是否将训练过程及结果绘画出来,紧接着判断电脑是否支持cuda
然后设置随机种子,下一行的torch distributed zero frst(LOCAL RANK)与分布式训练相关的,如果不进行分布式训练则不执行,利
用check_dataset会进行数据集检查读取操作,获取训练集和测试集图片路径
接着利用nc获取数据集的种类,names会进行类的种数以及类的名称是否相同的判断,不相同会进行报错处理,然后保存类别数量以及类别名,并完成检查。
最后会进行是否是coco数据集进行判断,如果是的话会进行一些额外的操作,如果不是,则输出false.
"""
##是否绘制训练、测试图片、指标图等。使用进化算法则不绘制
plots = not evolve and not opt.noplots # create plots
##判断电脑是否有CUda
cuda = device.type != 'cpu'
##设置随机种子,初始化随机数种子,训练过程可复现
init_seeds(opt.seed + 1 + RANK, deterministic=True)
##加载数据配置信息,以下两行是进行分布式并行训练时,检查数据集的格式是否符合要求,
#再将检查结果保存在data_dict变量中,data_dict是字典形式,内容根据data文件夹下指定的yaml文件解析得到的
with torch_distributed_zero_first(LOCAL_RANK):
data_dict = data_dict or check_dataset(data) # check if None
##获取训练集和测试集图片路径
train_path, val_path = data_dict['train'], data_dict['val']
##number of class 数据集有多少类别数
"""
single_cls=False,
data_dict={'path': WindowsPath('E:/AI/datasets/coco128'),
'train': 'E:\\AI\\datasets\\coco128\\images\\train2017',
'val': 'E:\\AI\\datasets\\coco128\\images\\train2017', 'test': None,
'names': {0: 'person', 1: 'bicycle', 2: 'car', 3: 'motorcycle', 4: 'airplane', 5: 'bus', 6: 'train', 7: 'truck', 8: 'boat', 9: 'traffic light', 10: 'fire hydrant', 11: 'stop sign', 12: 'parking meter', 13: 'bench', 14: 'bird', 15: 'cat', 16: 'dog', 17: 'horse', 18: 'sheep', 19: 'cow', 20: 'elephant', 21: 'bear', 22: 'zebra', 23: 'giraffe', 24: 'backpack', 25: 'umbrella', 26: 'handbag', 27: 'tie', 28: 'suitcase', 29: 'frisbee', 30: 'skis', 31: 'snowboard', 32: 'sports ball', 33: 'kite', 34: 'baseball bat', 35: 'baseball glove', 36: 'skateboard', 37: 'surfboard', 38: 'tennis racket', 39: 'bottle', 40: 'wine glass', 41: 'cup', 42: 'fork', 43: 'knife', 44: 'spoon', 45: 'bowl', 46: 'banana', 47: 'apple', 48: 'sandwich', 49: 'orange', 50: 'broccoli', 51: 'carrot', 52: 'hot dog', 53: 'pizza', 54: 'donut', 55: 'cake', 56: 'chair', 57: 'couch', 58: 'potted plant', 59: 'bed', 60: 'dining table', 61: 'toilet', 62: 'tv', 63: 'laptop', 64: 'mouse', 65: 'remote', 66: 'keyboard', 67: 'cell phone', 68: 'microwave', 69: 'oven', 70: 'toaster', 71: 'sink', 72: 'refrigerator', 73: 'book', 74: 'clock', 75: 'vase', 76: 'scissors', 77: 'teddy bear', 78: 'hair drier', 79: 'toothbrush'}, 'download': 'https://ultralytics.com/assets/coco128.zip', 'nc': 80}
"""
nc = 1 if single_cls else int(data_dict['nc']) # number of classes
##names:数据集所有类别名
names = {0: 'item'} if single_cls and len(data_dict['names']) != 1 else data_dict['names'] # class names
##当前数据集是否是coco数据集(80类别)
is_coco = isinstance(val_path, str) and val_path.endswith('coco/val2017.txt') # COCO dataset
"""
4.1 加载参数和初始化配置信息小结:
解析各种vaml的参数 +创建训练权重目录和保存路径+ 读取超参数配置文件 + 设置保存参数保存路径 + 加载数据配置信息 + 加载日志信息(logger + wandb) +
加载其他参数(plots、cuda、nc、names、is coco)
"""
##做一些变量的配置,包括是否绘图,设置随机数种子,
# 保存训练集、验证集路径,保存类别数量以及类别名,并完成检查。
########################进入核心部分:加载网络模型用于迁移学习###################
#Model载入模型
##检查文件后缀是.pt
check_suffix(weights,'.pt')##check weights
##加载预训练权重yolov5提供了5个不同的预训练权重,可以根据自己的模型选择预训练权重
pretrained=weights.endswith('.pt')###存储预训练权重
if pretrained:
##torch_distributer_zero_first(RANK):用于同步不同进程对数据读取的上下文管理器
with torch_distributed_zero_first(LOCAL_RANK):
##检查有没有权重,没有就下载
### 如果本地不存在就从google云盘中自动下载模型
# 通常会下载失败,建议提前下载下来放进weights目录
weights = attempt_download(weights) # download if not found locally
###########################加载模型以及参数#######
ckpt=torch.load(weights,map_location=device)##加载模型以及参数
"""
两种加载模型的方式: opt.cfg / ckpt['model'].yaml
这两种方式的区别:区别在于是否是使用resume如果使用resume-断点训练:
将opt.cfg设为空,选择ckpt['model']yaml创建模型,且不加载anchor。这也影响了下面是否除去anchor的key(也就是不加载anchor),如果resume则不加载anchor
原因:
使用断点训练时,保存的模型会保存anchor,所以不需要加载
主要是预训练权重里面保存了默认coco数据集对应的anchor,
如果用户自定义了anchor,再加载预训练权重进行训练,会覆盖掉用户自定义的anchor。
加载模型,分为使用预训练权重参数文件与不使用预训练权重参数。这里使用预训练权重参数,是类似于迁移学习。预训练的模型是检测coco数据集的模型,数据集中有80个类别,而自己的训练集类别以及类别的数量,
并不与coco数据集相同。所以要先加载一个新的模型,把预训练的参数加载至模型作为初始参数,再把识别的类别改成自己的数据集要识别的类别。
接下来将预训练参数中与新模型中相同的参数加载至模型。
新模型与预训练模型中为何会出现相同的参数?这里是因为预训练的概念类似于迁移学习,预训练模型中很多负责特征提取的参数,
可能会对训练的新模型有所帮助,所以就会把这些参数加载至新的模型中
这段代码主要是加载模型,分为使用预训练权重参数文件与不使用预训练权重参数文件。
首先会去检测传进来的权重参数后缀名是否以.pt结尾,分两类。
如果传入权重文件,直接model.load state dict加载模型
如果没有传入权重文件,就回去会尝试去yolov5官方仓库去下载权重文件,加载权重文件,
紧接着会根据你的权重文件中会带着一个volov5s.vaml文件,代码根据volov5s.vaml进行模型的训练。
(通俗的理解就是我们预训练模型是yolov5s.pt,我们的新模型是基于我们自己的识别检测需求在yolov5s的基础上完成的。)
"""
# 创建模型
# 若cfg为空 则从预训练权重参数文件中加载模型 ch为通道数 nc为检测的类别数 achors为超参数,从超参数字典中加载
# to(device)将程序装载至对应的位置
# 这里预训练模型是coco数据集,检测80个类别,这里新建模型检测类别修改为自己的类别数量
model=Model(cfg or ckpt['model'].yaml,ch=3,nc=nc,anchors=hyp.get('anchors')).to(device)##创建新模型
#以下三行是获得anchor
# 若cfg 或 hyp.get('anchors')不为空且不使用中断训练 exclude=['anchor'] 否则 exclude=[]
# 若cfg 或 hyp;get('anchors')不为空且不使用中断训练 exclude=['anchor'] 否则 exclude=[]
# excLude列表包含了需要排除的参数名称,这些参数需要重新训练,保证它们不受预训练模型的影响
# 若cfg 或 hyp.get('anchors')不为空 且 不使用中断训练 exclude=['anchor'] 否则 exclude=[]
exclude=['anchor'] if (cfg or hyp.get('anchors')) and not resume else []
# 在训练过程中,有些参数是可以从预训练模型直接加载,而有些参数则需要重新训练
# 加载预训练模型的所有参数,并转换为FLoat类型,以字典形式存储在csd变量中
# 将预训练模型中的所有参数保存下来 赋值给csd
csd=ckpt['model'].float().state_dict()
# 这一部分类似迁移学习
# 判断预训练参数和新创建的模型参数有多少是相同的
# 筛选字典中的键值对 把exclude删除
# 对预训练模型和新模型中的权重参数取交集,并排除exclude中的内容
# 交集中的参数在后续权重更新时会用到,exclude中的内容需要训练
csd = intersect_dicts(csd, model.state_dict(), exclude=exclude) # intersect
# 加载相同的参数
model.load_state_dict(csd,strict=False)
##把所有相同的参数加载到新模型 预训练模型中某些参数对自己的模型是有帮助的
# 把所有相同的参数加载到新模型 预训练模型中某些参数对自己的模型是有帮助的
# 给出提示
LOGGER.info(
f'Transferred {len(csd)}/{len(model.state_dict())} items from {weights}') # report 记录从预训练模型中转移了多少参数,以及新模型中一共有多少参数
else: # 如果没有预训练权重,直接加载模型,ch为输入图片通道数
# 不使用预训练 则直接从网络配置文件中加载模型
model = Model(cfg, ch=3, nc=nc, anchors=hyp.get('anchors')).to(device) # create
amp = check_amp(model) # check AMP # check AMP 检查是否启用混合精度训练,启用则amp返回Ture
"""
最后,获取的train_path和test_path分别表示在data.yaml中训练数据集和测试数据集的地址。
这里使用预训练权重参数,是类似于迁移学习。预训练的模型是检测cOco数据集的模型,数据集中有80个类别,而自己的训练集类别以及类别的数量,
并不与coco数据集相同。所以要先加载一个新的模型,
把预训练的参数加载至模型作为初始参数,再把识别的类别改成自己的数据集要识别的类别。接下来将预训练参数中与新模型中相同的参数加载至模型。
"""
"""
冻结模型层,设置冻结层名字即可freeze为命令行参数,默认为0,表示不冻结
作用:冰冻一些层,就使得这些层在反向传播的时候不再更新权重,需要冻结的层,可以写在freeze列表中
freeze为命令行参数,默认为0,表示不冻结
"""
# Freeze 控制冻结哪些层 创建了一个冻结参数列表freeze,用于决定需要冻结的参数层数
# 参数freeze是一个整数列表或单个整数,用于表示需要冻结的参数层数
# 如果freeze是一个整数,则表示要冻结模型中的前freeze层参数
# 如果freeze是一个整数列表,则表示要冻结模型中包含在这些层数中的所有参数
freeze = [f'model.{x}.' for x in (freeze if len(freeze) > 1 else range(freeze[0]))] # layers to freeze
"""
DetectionModel(
(model): Sequential(
(0): Conv(
(conv): Conv2d(3, 32, kernel_size=(6, 6), stride=(2, 2), padding=(2, 2), bias=False)
(bn): BatchNorm2d(32, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(1): Conv(
(conv): Conv2d(32, 64, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn): BatchNorm2d(64, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(2): C3(
(cv1): Conv(
(conv): Conv2d(64, 32, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn): BatchNorm2d(32, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(cv2): Conv(
(conv): Conv2d(64, 32, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn): BatchNorm2d(32, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(cv3): Conv(
(conv): Conv2d(64, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn): BatchNorm2d(64, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(m): Sequential(
(0): Bottleneck(
(cv1): Conv(
(conv): Conv2d(32, 32, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn): BatchNorm2d(32, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(cv2): Conv(
(conv): Conv2d(32, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn): BatchNorm2d(32, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
)
)
)
(3): Conv(
(conv): Conv2d(64, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn): BatchNorm2d(128, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(4): C3(
(cv1): Conv(
(conv): Conv2d(128, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn): BatchNorm2d(64, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(cv2): Conv(
(conv): Conv2d(128, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn): BatchNorm2d(64, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(cv3): Conv(
(conv): Conv2d(128, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn): BatchNorm2d(128, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(m): Sequential(
(0): Bottleneck(
(cv1): Conv(
(conv): Conv2d(64, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn): BatchNorm2d(64, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(cv2): Conv(
(conv): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn): BatchNorm2d(64, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
)
(1): Bottleneck(
(cv1): Conv(
(conv): Conv2d(64, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn): BatchNorm2d(64, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(cv2): Conv(
(conv): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn): BatchNorm2d(64, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
)
)
)
(5): Conv(
(conv): Conv2d(128, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn): BatchNorm2d(256, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(6): C3(
(cv1): Conv(
(conv): Conv2d(256, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn): BatchNorm2d(128, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(cv2): Conv(
(conv): Conv2d(256, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn): BatchNorm2d(128, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(cv3): Conv(
(conv): Conv2d(256, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn): BatchNorm2d(256, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(m): Sequential(
(0): Bottleneck(
(cv1): Conv(
(conv): Conv2d(128, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn): BatchNorm2d(128, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(cv2): Conv(
(conv): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn): BatchNorm2d(128, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
)
(1): Bottleneck(
(cv1): Conv(
(conv): Conv2d(128, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn): BatchNorm2d(128, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(cv2): Conv(
(conv): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn): BatchNorm2d(128, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
)
(2): Bottleneck(
(cv1): Conv(
(conv): Conv2d(128, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn): BatchNorm2d(128, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(cv2): Conv(
(conv): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn): BatchNorm2d(128, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
)
)
)
(7): Conv(
(conv): Conv2d(256, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn): BatchNorm2d(512, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(8): C3(
(cv1): Conv(
(conv): Conv2d(512, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn): BatchNorm2d(256, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(cv2): Conv(
(conv): Conv2d(512, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn): BatchNorm2d(256, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(cv3): Conv(
(conv): Conv2d(512, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn): BatchNorm2d(512, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(m): Sequential(
(0): Bottleneck(
(cv1): Conv(
(conv): Conv2d(256, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn): BatchNorm2d(256, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(cv2): Conv(
(conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn): BatchNorm2d(256, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
)
)
)
(9): SPPF(
(cv1): Conv(
(conv): Conv2d(512, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn): BatchNorm2d(256, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(cv2): Conv(
(conv): Conv2d(1024, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn): BatchNorm2d(512, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(m): MaxPool2d(kernel_size=5, stride=1, padding=2, dilation=1, ceil_mode=False)
)
(10): Conv(
(conv): Conv2d(512, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn): BatchNorm2d(256, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(11): Upsample(scale_factor=2.0, mode='nearest')
(12): Concat()
(13): C3(
(cv1): Conv(
(conv): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn): BatchNorm2d(128, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(cv2): Conv(
(conv): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn): BatchNorm2d(128, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(cv3): Conv(
(conv): Conv2d(256, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn): BatchNorm2d(256, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(m): Sequential(
(0): Bottleneck(
(cv1): Conv(
(conv): Conv2d(128, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn): BatchNorm2d(128, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(cv2): Conv(
(conv): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn): BatchNorm2d(128, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
)
)
)
(14): Conv(
(conv): Conv2d(256, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn): BatchNorm2d(128, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(15): Upsample(scale_factor=2.0, mode='nearest')
(16): Concat()
(17): C3(
(cv1): Conv(
(conv): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn): BatchNorm2d(64, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(cv2): Conv(
(conv): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn): BatchNorm2d(64, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(cv3): Conv(
(conv): Conv2d(128, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn): BatchNorm2d(128, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(m): Sequential(
(0): Bottleneck(
(cv1): Conv(
(conv): Conv2d(64, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn): BatchNorm2d(64, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(cv2): Conv(
(conv): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn): BatchNorm2d(64, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
)
)
)
(18): Conv(
(conv): Conv2d(128, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn): BatchNorm2d(128, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(19): Concat()
(20): C3(
(cv1): Conv(
(conv): Conv2d(256, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn): BatchNorm2d(128, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(cv2): Conv(
(conv): Conv2d(256, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn): BatchNorm2d(128, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(cv3): Conv(
(conv): Conv2d(256, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn): BatchNorm2d(256, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(m): Sequential(
(0): Bottleneck(
(cv1): Conv(
(conv): Conv2d(128, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn): BatchNorm2d(128, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(cv2): Conv(
(conv): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn): BatchNorm2d(128, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
)
)
)
(21): Conv(
(conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn): BatchNorm2d(256, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(22): Concat()
(23): C3(
(cv1): Conv(
(conv): Conv2d(512, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn): BatchNorm2d(256, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(cv2): Conv(
(conv): Conv2d(512, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn): BatchNorm2d(256, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(cv3): Conv(
(conv): Conv2d(512, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn): BatchNorm2d(512, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(m): Sequential(
(0): Bottleneck(
(cv1): Conv(
(conv): Conv2d(256, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn): BatchNorm2d(256, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
(cv2): Conv(
(conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn): BatchNorm2d(256, eps=0.001, momentum=0.03, affine=True, track_running_stats=True)
(act): SiLU(inplace=True)
)
)
)
)
(24): Detect(
(m): ModuleList(
(0): Conv2d(128, 255, kernel_size=(1, 1), stride=(1, 1))
(1): Conv2d(256, 255, kernel_size=(1, 1), stride=(1, 1))
(2): Conv2d(512, 255, kernel_size=(1, 1), stride=(1, 1))
)
)
)
)
"""
"""
Freeze会冻结模型的某些层,被冻结的层训练时不会更新参数。冻结层的原理是通过设置每个层参数中的requires_grad属性实现的。
requires_grad属性在上一篇detect模块的精读中提到过。若require_grad为True,在反向传播时就会求出此tensor的梯度,
若require_grad为False,则不会求该tensor的梯度。冻结就是通过对某些层不求梯度实现的。默认不进行参数冻结。
这段代码是设置冻结层的,即将模型的部分权重冻结,在模型训练过程中不会变化,只训练冻结层以为的权重参数
冻结层的原理是通过设置每个层参数中的requires grad属性实现的。
若require grad为True,在反向传播时就会求出此tensor的梯度
。若require grad为False,则不会求该tnsor的梯度。冻结就是通过对某些层不求梯度实现的。默认不进行参数冻结
通过Freeze这部分代码,我们可以手动去控制想冻结哪些层。但是作者这里列出来这部分代码的目的其实并不是鼓励使用冻结指定层,因为作者认为这样效果其实并不是很好。
"""
##k0='model.0.conv.weight' v0={Parameter:(32,3,6,6)}--->具体的参数值
##k1='model.0.bn.weight' v1=(32,)
##k2='model.0.bn.bias' v2=(32,)
##k3='model.1.conv.weight' v3=(64,32,3,3)
for k, v in model.named_parameters(): # 遍历模型中所有参数,返回键和值
v.requires_grad = True # train all layers 更新梯度设为True,训练过程中更新参数
# v.register_hook(lambda x: torch.nan_to_num(x)) # NaN to 0 (commented for erratic training results)
if any(x in k for x in freeze): # 如果x表示的参数所在的层在需要冻结的层中
LOGGER.info(f'freezing {k}')
v.requires_grad = False # 则将对应的更新梯度设为False,训练过程中不更新参数
# Image size 调整输入图片的尺寸
# 计算当前模型的最大stride,并取stride和32的较大值
"""
保存一些关于图片预测的信息,gs表示将图片划分的grid_size大小,gs最小为32;nl表示图片检测层的个数,这里是三个层,
分别对应着不同的图片感受野,用于检测图片中不同大小的物体;imgz表示输入模型图片的尺寸。
"""
# 设置grid size大小,最小为32
gs=max(int(model.stride.max()),32)# grid size(max stride)
# 检查用户指定的输入图像的大小是否是步长gs的整数倍,调整输入模型的图片大小
# 输入模型的尺寸大小
imgsz=check_img_size(opt.imgsz,gs,floor=gs*2)
"""
训练时图像的高度和宽度是相等的。
推理时图像的高度和宽度是可以不相等的。
batchsize的大小必须能够被GPU个数整除
如果输入图片大小不是32的整数倍,程序会自动调整到32的整数倍。
训练图像的大小的参数如下
parser.add_argument('--img-size', nargs='+', type=int, default=[640, 640], help='[train, test] image sizes')
opt = parser.parse_args()
YOLO的卷积层采用32这个值来下采样图片,所以通过选择416*416用作输入尺寸最终能输出一个13*13的Feature Map。
由于网络的下采样倍数是32,因此选择的输入尺寸都是32的倍数,最小是320,最大608。
经过stride后会变换特征图大小一共有5次,每次都是2倍所以确定imgsize大小要是2^5=32倍数-->32/2/2/2/2/2=1
"""
# batch_size设置一次训练所选取的样本数 Batch_size bs=-1表示自动计算batch_size大小
# Batch size
if RANK == -1 and batch_size == -1: # single-GPU only, estimate best batch size
##确保batch_size满足需求
batch_size = check_train_batch_size(model, imgsz, amp)## single-GPU only,estimate best batch size
loggers.on_params_update({"batch_size": batch_size})
# Optimizer
# nominal batch size 名义上bs 一批数据越多,梯度更新方向越准确
nbs = 64 # nominal batch size 名义上的batch size。这里的nbs跟命令行参数中的batch size不同,命令行中的batch size默认为16,nbs指的是nominal batch size,nbs设置为64。
"""
nbs = 64
batchsize = 16
accumulate = 64 / 16 = 4
模型梯度累计accumulate次之后就更新一次模型 相当于使用更大batch size
accumulate 为累计次数,在这里 nbs/batch size (64/16) 计算出 pt.batch size输入多少批才达到nbs的水平简单来说,nbs为64,代表根要达到的batch size.这里的数值是64: batch size为opt.batch size.这里的数值是16。64/16等于4.也就
是opt.batch size需要输入4批才能达到nbs,accumulate等于4。(round表示四舍五入取整数,而max表示accumulate不能低于1
当给模型喂了4批图片数据后batch size=64时相同的效果,这样就实现了与将四批图片数据得到的梯度值,做累积。当每累积到4批数据时,才会对参数做更新,:
最后还要做权重参数的缩放,因为batch size发生了变化,所有权重参数也要做相应的缩放
"""
# 计算梯度累计的步数accumulate
accumulate = max(round(nbs / batch_size), 1) # accumulate loss before optimizing
# 根据nbs、bs、accumulate调整权重衰减系数
# 用于控制过拟合的权重衰减参数,使其在不同的批次大小和梯度累积步数下,具有相似的作用,避免影响模型的训练效果
hyp['weight_decay'] *= batch_size * accumulate / nbs # scale weight_decay
"""
接下来是优化器的设置。这里的nbs = 64,nbs指的是nominal batch size,名义上的batch_size。这里的nbs跟命令行参数中的batch_size不同,命令行中的batch_size默认为16,nbs设置为64。由于yolo在训练时占用的内存比较多,因此batch_size可设置的值就会比较小,batch_size小时会降低训练的速度,那么就要想办法,用opt.batch_size=16的情况,实现batch_size=64的效果。
accumulate 为累计次数,在这里 nbs/batch_size计算出 opt.batch_size输入多少批才达到nbs的水平。简单来说,nbs为64,代表想要达到的batch_size,这里的数值是64;batch_size为opt.batch_size,这里的数值是16。64/16等于4,也就是opt.batch_size需要输入4批才能达到nbs,accumulate等于4。round表示四舍五入取整数,而max表示accumulate不能低于1。
当给模型喂了4批图片数据后,将四批图片数据得到的梯度值,做累积。当每累积到4批数据时,才会对参数做更新,这样就实现了与batch_size=64时相同的效果。
还要做权重参数的缩放,因为batch_size发生了变化,所有权重参数也要做相应的缩放。
如果nbs和bs不成比例,要缩减 hyp['weight_decay']
1.nbs=64
{'lr0': 0.01, 'lrf': 0.01, 'momentum': 0.937, 'weight_decay': 0.0005, 'warmup_epochs': 3.0, 'warmup_momentum': 0.8,
'warmup_bias_lr': 0.1, 'box': 0.05, 'cls': 0.5, 'cls_pw': 1.0, 'obj': 1.0, 'obj_pw': 1.0, 'iou_t': 0.2, 'anchor_t': 4.0,
'fl_gamma': 0.0, 'hsv_h': 0.015, 'hsv_s': 0.7, 'hsv_v': 0.4, 'degrees': 0.0, 'translate': 0.1, 'scale': 0.5, 'shear': 0.0,
'perspective': 0.0, 'flipud': 0.0, 'fliplr': 0.5, 'mosaic': 1.0, 'mixup': 0.0, 'copy_paste': 0.0}
2.当nbs=69
hyp={'lr0': 0.01, 'lrf': 0.01, 'momentum': 0.937, 'weight_decay': 0.000463768115942029, 'warmup_epochs': 3.0,
'warmup_momentum': 0.8, 'warmup_bias_lr': 0.1, 'box': 0.05, 'cls': 0.5, 'cls_pw': 1.0, 'obj': 1.0,
'obj_pw': 1.0, 'iou_t': 0.2, 'anchor_t': 4.0, 'fl_gamma': 0.0, 'hsv_h': 0.015, 'hsv_s': 0.7, 'hsv_v': 0.4,
'degrees': 0.0, 'translate': 0.1, 'scale': 0.5, 'shear': 0.0, 'perspective': 0.0, 'flipud': 0.0,
'fliplr': 0.5, 'mosaic': 1.0, 'mixup': 0.0, 'copy_paste': 0.0}
"""
"""
weight_decay:
假设我们已经拥有尽可能多的高质量数据,我们便可以将重点放在正则化技术上。
在多项式回归的例子中, 我们可以通过调整拟合多项式的阶数来限制模型的容量。实际上,限制特征的数量是缓解过拟合的一种常用技术。 然而,简单地丢弃特征对这项工作来说可能过于生硬。
ps: 不断更新w和b这两个模型参数使得损失函数最小,w和b不是唯一的。也就是说,达到局部最优解可以有多组值。有可能一组w和b很大,但仍然能达到局部最优解,但是参数很大,误差和噪声也会被放大,预测会不准确。因此,可以人为地控制参数的取值范围。
权重衰退是一种最常见的处理过拟合的方法,通常也被称为L2正则化
w权重是模型中未知数的系数,它的取值情况直接决定了模型曲线到底是什么样子的,而偏置b的取值,不会改变模型曲线的样子,只会改变模型的平移情况。L1和L2正则化,针对的是w进行,对b的处理意义不大。
使得w的每个项的平方和是小于一个特定的值,也就是说每个项的值都小于特定值的开根号。
但一般来说,我们不会直接用这个优化函数,因为相对来说麻烦一点。常用的是下面的函数。
不过,其实一般情况下,不加入正则项,且数据因为有噪音的存在,为了学习参数而去拟合数据(包括噪音),会把参数w学习得很大,离数学上的最优解很远,那加入正则项也是在限制w的取值。加入惩罚,使得w变小,可以减小底部的震荡,收敛到一个靠近真实底部的值。
并且,换个角度理解,越接近loss函数的最优点,代表模型复杂度越高,拟合程度越大,当引入惩罚项来拉动远离最低点,就使得模型复杂度越低,模型容量更小,可以防止过拟合。
至于为什么是两个函数曲线相切的点呢?
答: 因为两个曲线相切时,黄线和绿线梯度一样,并且由于二者梯度方向相反,此时梯度绝对值最小为0,参数就不会再更新下去了,得到了平衡,某种意义上来说也就得到了最优解。
所以,新的更新梯度,每次是wt乘以一个小于1的数,再沿着梯度的反方向走一点。
为什么叫权重衰退?
答:是因为λ的引入,使得在更新参数前,把当前的权重进行了一次缩小(一次衰退)。
权重衰退通过L2正则项使得模型参数不会过大,从而控制模型复杂度
正则项权重是控制模型复杂度的超参数
以上图中的函数图像来说,要去拟合那些红点,如果可以允许模型参数选的比较大的话,可以做一个任何一个很复杂的曲线(如蓝色曲线)去拟合。
比如说,同样是二次模型或者三次模型这种简单模型,假设我的权重可以取到很大,会造成一个非常不平滑的曲线。那现在限制w的取值范围,也就是只能去学习比较平滑的曲线,而不去学复杂的曲线,就意味着模型复杂度变低。
或者这么理解,一个多项式中的高次项的系数变小了或者为0,函数也就变平滑了。
weight_decay的值一般怎么选择?
答: 0.01 或者0.001或者0.0001
当超参数=0,就是没有起作用;当超参数趋向于无穷大时,表示惩罚项越来越大;如果想使得模型复杂度低,可以通过增加λ(weight_decay)满足需求
W可以选择的范围越大哦,表示模型越复杂。
较小的λ值对应较少约束的w, 而较大的λ值对w的约束更大。
"""
# 创建一个优化器对象,以便在模型的训练阶段中通过梯度反向传播算法
# 对模型参数进行更新,达到训练集优化的目的
# smart_optimizer中定义了优化器类型以及要优化的参数变量
optimizer = smart_optimizer(model, opt.optimizer, hyp['lr0'], hyp['momentum'], hyp['weight_decay'])
"""
优化器的核心目的----》训练过程中调整优化达到损失函数最小值,比如带动量冲下局部最小值、比如构建楼梯型学习率等
优化器的作用:
用来更新和计算影响模型训练和模型输出的网络参数,使其逼近或达到最优值,从而最小化(或最大化)损失函数。 在深度学习中,几乎所有流行的优化器都基于梯度下降。这意味着他们反复估计给定的损失函数L的斜率,并将参数向相反的方向移动(因此向下爬升到一个假设的全局最小值)。
1、在SGD中,优化器基于一个小batch估计最陡下降的方向,并在这个方向前进一步。由于步长是固定的,SGD会很快陷入平坦区或陷入局部极小值。
2、带动量的SGD的更新规则,使用了动量,SGD可以在持续的方向上进行加速(这就是为什么也被叫做“重球方法”)。这个加速可以帮助模型摆脱平坦区,使它更不容易陷入局部最小值。
3、AdaGrad是首个成功的利用自适应学习率的方法之一(因此得名)。AdaGrad根据梯度的平方和的倒数的平方根来衡量每个参数的学习速率。这个过程将稀疏梯度方向上的梯度放大,从而允许在这些方向上执行更大的步骤。其结果是:AdaGrad在具有稀疏特征的场景中收敛速度更快。
4、RMSprop是一个未发布的优化器,在过去几年中被过度使用。这个想法与AdaGrad相似,但是梯度的重新缩放不那么激进:梯度的平方的总和被梯度平方的移动平均值所取代。RMSprop通常与动量一起使用,可以理解为Rprop对mini-batch设置的适应。
5、Adam将AdaGrad,RMSprop和动量法结合在一起。步长方向由梯度的移动平均值决定,步长约为全局步长的上界。此外,梯度的每个维度都被重新缩放,类似于RMSprop。Adam和RMSprop(或AdaGrad)之间的一个关键区别是,矩估计m和v被纠正为偏向于零。Adam以通过少量的超参数调优就能获得良好性能而闻名。
6、LARS是使用动量的SGD的一种扩展,具有适应每层学习率的能力。它最近引起了研究界的注意。原因是由于可用数据量的稳步增长,机器学习模型的分布式训练已经流行起来。其结果是批大小开始增长。然而,这导致了训练中的不稳定。Yang等人认为,这些不稳定性源于某些层的梯度范数和权重范数之间的不平衡。因此,他们提出了一个优化器,该优化器基于一个“trust”参数η < 1和该层的梯度的范数的倒数,对每一层的学习率进行缩放。
"""
###学习率设置
##Scheduler 设置学习率策略:两者可供选择,线性学习率和余弦退火学习率
# Scheduler 学习率调整策略
if opt.cos_lr: # 余弦函数学习率调整
lf = one_cycle(1, hyp['lrf'], epochs) # cosine 1->hyp['lrf']
else: # 线性函数学习率调整
lf = lambda x: (1 - x / epochs) * (1.0 - hyp['lrf']) + hyp['lrf'] # linear
scheduler = lr_scheduler.LambdaLR(optimizer, lr_lambda=lf) # plot_lr_scheduler(optimizer, scheduler, epochs)
"""
这段代码主要是设置学习率衰减方式
在训练过程中变更学习率可能会让训练效果更好,YOLOv5提供了两种学习率变化的策略:
一种是linear Ir (线性学习率) ,是通过线性插值的方式调整学习率
另一种则是One Cycle (余弦退火学习率),即周期性学习率调整中,周期被设置为1。
在一周期策略中最大学习率被设置为 LR Range test 中可以找到的最高值,最小学习率比最大学习率小几个数量级。这里默认one cycle。
设置学习率调整策略。在训练过程中变更学习率可能会让训练效果更好,yolo v5提供了两种学习率变化的策略。
一种是linear_lr,是通过线性插值的方式调整学习率;另一种则是One Cycle学习率调整策略,即周期性学习率调整中,周期被设置为1。
在一周期策略中,最大学习率被设置为 LR Range test 中可以找到的最高值,最小学习率比最大学习率小几个数量级。这里默认one_cycle。
"""
# EMA
# 创建一个指数移动平均模型对象(ema),以用于在训练期间计算模型权重的指数滑动平均值,并在验证期间使用这个平均值对模型进行评估
#EMA 设置ema(指数移动平均》,考虑历史值对参数的影响,目的是为了收敛的曲线更加平滑
# 为模型创建EMA指数滑动平均,如果GPU进程数大于1,则不创建
ema = ModelEMA(model) if RANK in {-1, 0} else None
#Resume断电续训
##断点续训其实就是把上次训练结束的模型作为预训练模型,并从中加载参数
best_fitness, start_epoch = 0.0, 0
if pretrained:##如果需要预训练
if resume:##检查是否需要从断点处恢复
best_fitness,start_epoch,epochs=smart_resume(ckpt,optimizer,ema,weights,epochs,resume)
##调用smatr_resume()函数加载训练轮次信息,以及之前训练过程中存储的模型参数、优化器状态、指数滑动平均模型等状态信息,并将其恢复到当前状态中
del ckpt,csd##删除不需要的变量释放内存空间
##DP mode 多GPU训练--单机多卡模型训练,目前一般不使用
# Dp mode 使用单机多卡模式训练,目前一般不使用
# rank为进程编号。如果rank=-1且gpu数量>1则使用DataParalle1单机多卡模式,效果并不好(分布不平均)
# rank=-1且gpu数量=1时,不会进行分布式
if cuda and RANK==-1 and torch.cuda.device_count() >1:
LOGGER.warning('WARNING: DP not recommended, use torch.distributed.run for best DDP Multi-GPU results.\n'
'See Multi-GPU Tutorial at https://github.com/ultralytics/yolov5/issues/475 to get started.')
model=torch.nn.DataParallel(model)
# SyncBatchNorm 与多分布式训练相关
# 若采用多分布式训练则将banchnorm层替换为SyncBatchNorm
##SyncBatchNorm 多卡归一化
if opt.sync_bn and cuda and RANK!=-1:#多卡训练,把不同卡的数据做个同步
model=torch.nn.SyncBatchNorm.convert_sync_batchnorm(model).to(device)
LOGGER.info("Using SyncBatchNorm()")
"""
这段代码主要是训练前最后的准备工作(EMA + 断点续训+ 代次数的加载 + DP + SyncBatchNorm)
EMA为指数加权平均或滑动平均。其将前面模型训练权重,偏差进行保存,在本次训练过程中,假设为第n次,将第一次到第n-1次以指数权重进行加和,
再加上本次的结果,且越远离第n次,指数系数越大,其所占的比重越小。
断点续训。可以理解为把上次中断结束时的模型,作为新的预训练模型,然后从中获取上次训练时的参数,并恢复训练状态。
epoch迭代次数。1个epoch等于使用训练集中的全部样本训练一次,epoch的大小跟迭代次数有着密切的关系,通常在迭代次数处于2000-3000之间损失已经处于平稳。
DP mode。DataParallel单机多卡模式自动将数据切分load 到相应 GPU,将模型复制到相应 GPU,进行正向传播计算梯度并汇总。值得注意的是,
模型和数据都需要先导入进 GPU 中,DataParallel 的 module才能对其进行处理,否则会报错.
syncBatchNorm。SyncBatchNorm主要用于解决多卡归一化同步问题,每张卡单独计算均值,然后同步,得到全局均值。用全局均值计算每张卡的方差,
然后同步即可得到全局方差,但两次会消耗时间挺长
"""
# Trainloader 训练数据加载
# 根据输入参数从指定路径加载训练数据,并生成一个数据加载器和一个数据集对象,用于模型训练
##train_loader:数据加载器 dataset:数据集对象
"""
# Trainloader
# 加载训练数据
# 返回一个训练数据加载器,一个数据集对象
# 训练数据加载器是一个可迭代的对象 可以通过for循环加载1个batch_size的数据
# 数据集对象包括数据集的一些参数 包括所有标签值、所有的训练数据路径、每张图片的尺寸等等
返回一个训练数据加载器,一个数据集对象:
训练数据加载器是一个可迭代对象,可以通过for循环加载一个batch_size的数据
数据集对象包括数据集的一些参数,包括标签值、所有的训练数据路径、每张图片的尺寸等
"""
train_loader, dataset = create_dataloader(train_path,##训练数据集路径
imgsz,##输入图像尺寸
batch_size // WORLD_SIZE,##每批图像数
gs,##global_size
single_cls,##是否单类别
hyp=hyp,##控制模型训练的超参数
augment=True,##是否图像增强
cache=None if opt.cache == 'val' else opt.cache,## 数据是否需要缓存。当被设置为 'val' 时,表示训练和验证使用同一个数据集
rect=opt.rect,##是否采用矩形训练方式
rank=LOCAL_RANK,#分布式训练,表示当前进程在节点中的排名
workers=workers,##指定Dataloader使用的工作线程数
image_weights=opt.image_weights,##图像权重
quad=opt.quad,
prefix=colorstr('train: '),
shuffle=True##是否打乱数据集顺序
)
# 将数据集中所有标签数据(即类别信息)按照纵向顺序拼接成一个新的一维数组 labels
# 用于后续在模型训练过程中进行分类准确率计算
labels=np.concatenata(dataset.labels,0)#(929, 5) 929个样本
"""
标签、中心点xy位置、长宽wh
[[ 45 0.47949 0.68877 0.95561 0.5955]
[ 45 0.73652 0.24719 0.49887 0.47642]
[ 50 0.63706 0.73294 0.49413 0.51058]
...
[ 39 0.43892 0.21547 0.05436 0.17637]
[ 15 0.5194 0.54409 0.47636 0.57206]
[ 2 0.50186 0.82073 0.99628 0.33218]]
"""
"""
concatenate:按行进行拼接
con = np.concatenate([x1,x2],axis=0)
print(con)
print(con.shape)
输出结果为:
[[ 2.22806658 0.15277615 2.21245262 1.63831116]
[ 1.30131232 -1.09226289 -0.65959394 1.16066688]
[ 1.52737722 0.84587186 1.53041503 0.4584277 ]
[ 1.56096219 1.29506244 3.08048523 2.06008988]
[ 1.79964236 0.95087117 1.30845477 -0.2644263 ]]
(5, 4)
[[0.89383392 1.49502055 2.90571116 1.71943997]
[1.44451535 1.87838383 1.4763242 0.82597179]
[0.72629108 1.42406398 1.35519112 0.58121617]]
(3, 4)
[[ 2.22806658 0.15277615 2.21245262 1.63831116]
[ 1.30131232 -1.09226289 -0.65959394 1.16066688]
[ 1.52737722 0.84587186 1.53041503 0.4584277 ]
[ 1.56096219 1.29506244 3.08048523 2.06008988]
[ 1.79964236 0.95087117 1.30845477 -0.2644263 ]
[ 0.89383392 1.49502055 2.90571116 1.71943997]
[ 1.44451535 1.87838383 1.4763242 0.82597179]
[ 0.72629108 1.42406398 1.35519112 0.58121617]]
(8, 4)
"""
##标签编号最大值
mlc=int(np.concatenate(dataset.labels,0)[:,0].max())##79 一共80类
##类别总数
nb=len(train_loader)##批次的数量
assert mlc<nc,f'Label class {mlc} exceeds nc={nc} in {data}. Possible class labels are 0-{nc - 1}'
# 判断最大类别号是否小于类别数量,若大于等于则抛出异常
#process 0 验证集数据集加载
if RANK in {-1,0}:##加载 验证集数据加载器
val_loader=create_dataloader(val_path,# 路径
imgsz,# 图像尺寸
batch_size // WORLD_SIZE * 2,# 每批图像数量
gs,# 全局步数
single_cls,# 是否单类别
hyp=hyp,# 超参数
cache=None if noval else opt.cache,# 缓存文件的路径,用于加载之前处理过的图像
rect=True,# 是否矩形验证
rank=-1,# 分布式训练中进程的排名,不使用分布式训练
workers=workers * 2,#数据加载器使用的进程数,用于并行加载数据
pad=0.5,# 图像填充比例,用于处理图像大小不一致的情况
prefix=colorstr('val: '))[0]# 数据加载器的名称)[0]
if not resume:##如果没有使用resume
##Anchors计算锚框anchor与数据标签框的高宽比
# 在训练模型之前,自动计算并显示建议的锚框大小和比例
if not opt.noautoanchor:
"""
参数dataset代表的是训练集,hyp['anchor_t']是从配置文件hpy.scratch.yaml读取的超参数,
anchor _t:4.0当配置文件中的anchor计算bpr (best possible recal1) 小于0.98时才会
重新计算anchor。best possible recal1最大值1,如果bpr小于0.98,程序会根据数据集的label自动学习anchor的尺寸
"""
check_anchors(dataset, model=model, thr=hyp['anchor_t'], imgsz=imgsz) # run AutoAnchor
##半进度
# 将模型的权重从浮点数格式转换为半精度浮点数格式
model.half().float() # pre-reduce anchor precision
##在每个训练前例行程序结束时触发所有已注册的回调
callbacks.run('on_pretrain_routine_end', labels, names)
# 在每个预训练迭代结束后执行 on_pretrain_routine_end 回调函数
# 用于对预训练模型进行必要的调整或者处理
"""
这段代码主要是创建训练用的数据集
首先,通过create dataloader()函数得到两个对象。一个为train loader,另一个为dataset.
。train loader为训练数据加载器,可以通过for循环遍历出每个batch的训练数据
dataset为数据集对象,包括所有训练图片的路径,所有标签,每张图片的大小,图片的配置,超参数等线
然后将所有样本的标签拼接到一起,统计后做可视化,同时获得所有样本的类别,根据上面的统计对所有样本的类别,中心点xy位置,长宽wh做可视化。
"""
# DDP mode
##DDP mode不过rank不等于-1,则使用DistributedDataParallel模式
if cuda and RANK != -1:
# local_rank为gpu编号,rank为进程,例如rank=3,local_rank=0 表示第 3 个进程内的第 1 块 GPU。
model = smart_DDP(model)
"""
这段代码主要是计算默认锚点anchor与数据集标签框的长宽比值
check_anchors计算默认锚点anchor与数据集标签框的长宽比值,标签的长h宽w与anchor的长h_a宽w_a的比值,即h/h_a, w/w_a都要在(1/hyp['anchor_t'],
hyp['anchor_t'])是可以接受的,如果标签框满足上面条件的数量小于总数的99%,则根据k-mean算法聚类新的锚点anchor。
"""
"""
4.3 加载数据集小结:
加载训练集dataloader、dataset + 参数(mlc、 nb) + 加载验证集testloader + 设置labels相关参数(labels.c) + plots可视化数据集
labels信息+检查anchors(k-means + 遗传进化算法)+ model半精度
"""
##Model attributes模型属性 根据自己数据集的类别数和网络FPN层数设置各个损失的系数
nl=de_parallel(model).model[-1].nl# number of detection layers (to scale hyps)
# de_parallel(model) 函数用于将模型转换为可以在多个GPU上并行运行的形式
# 函数会对模型的参数进行划分,使得不同的部分可以并行地计算
# 代码通过取 model 的最后一个模块的 nl 属性,获得检测层的数量
# 根据 nl 的值,代码更新了超参数 hyp 中的三个值:box,cls,obj,以及 label_smoothing
# 因为不同层的输出尺寸不同,为了保证超参数在不同层之间的一致性
# 这里对 hyp 中的三个超参数进行了缩放,使得它们与层数和类别数量成正比
##box的预测框的损失
hyp['box']*=3/nl##scale to layer
##cls 为分类的损失
hyp['cls']*=nc/80*3/nl#scale to classes and layers
##obj为置信度损失
hyp['obj']*=(imgsz/640)**2*3/nl#scale to image size and layers
##标签平滑
hyp['layer_smoothing']=opt.label_smoothing
# 以下4行代码的作用是为模型的训练做准备,为模型的属性赋值,并计算各个类别的权重
##设置模型的类别,然后将检测的类别个数保存到模型
model.nc=nc##attach numbere of classes of model 数据集中模型数量赋值给模型nc属性
#设置模型的超参数,然后将超参保存到模型
model.hyp=hyp##attach hyperparameters to model 加载超参数,训练使用
#从训练的样本标签得到类别权重,然后将类别权重保存至模型
model.class_weights=labels_to_class_weights(dataset.labels, nc)# attach class weights
# 根据数据集的标签计算每个类别的权重,并将其赋值给模型的 class_weights 属性
# 这个属性在训练过程中用于动态调整损失函数中各个类别的权重,从而更加关注重要的类别
#获取类别的名字,然后将分类标签保存至模型
# 将数据集中的类别名称列表 names 赋值给模型的 names 属性,表示每个输出通道对应的类别名称
model.names=names
"""
这段代码主要是根据自己数据集的类别数设置分类损失的系数,位置损失的系数。设置类别数,超参数等操作
其中,
box: 预测框的损失.
cls:分类的损失
obj:置信度损失学
label smoothing :标签平滑
"""
########################开始训练
"""
这段代码是训练前的热身准备,做一些参数的初始化
这里要提到两个点:
第一个是warmup。warmup是一种学习率的优化方法,最早出现在ResNet的论文中。简单来说,在模型刚开始训练时,
使用较小的学习率开始摸索,经过几轮迭代后使用大的学习率加速收敛,在快接近目标时,再使用小学习率,避免错过目标。
第二个是早停机制。当训练一定的轮数后,如果模型效果未提升,就让模型提前停止训练。这里的默认轮数为100轮,
判新模型的效果为fitness,fitness为 0.1乘mAP@0.5加上0.9乘mAP@0.5;0.95.
"""
# Start training
t0 = time.time()##获取当前时间
nb = len(train_loader) # number of batches number of batches batch_size的长度,表示一共传入了几次batch
##获取热身训练的迭代次数
nw = max(round(hyp['warmup_epochs'] * nb), 100) # number of warmup iterations, max(3 epochs, 100 iterations)
# 计算预热期的迭代次数nw,训练过程开始时逐渐增加学习率,直到训练过程稳定
# 预热期的目的是避免模型在初始的训练阶段过快地收敛到次优解,从而提高模型收敛到更优解的概率。
# 预热期的迭代数量应该根据数据集的大小和超参数的具体设置进行调整,以取得最佳效果。
# nw = min(nw, (epochs - start_epoch) / 2 * nb) # limit warmup to < 1/2 of training
last_opt_step = -1
# 在训练过程中,last_opt_step 变量用于记录上一次进行梯度下降更新的迭代次数
# 在进行优化器状态恢复时,可以使用该变量作为开始迭代次数,并继续训练。
# 如果没有上一次的迭代记录,可以将该变量设置为 -1。
# 这行代码的作用是初始化 last_opt_step 变量,并在训练过程中用于更新,以便在需要时进行优化器状态恢复
"""
加载训练数据时,通过create_dataloader()函数得到两个对象。一个为train_loader,另一个为dataset。
train_loader为训练数据迭代器,可以通过for循环遍历出每个batch的训练数据;dataset为数据集的一些基本信息,
包括所有训练图片的路径,所有标签,每张图片的大小,图片的配置,超参数等等。
"""
##初始化maps和results
maps = np.zeros(nc) # mAP per class 创建了一个长度为 nc 的数组 maps,并将其所有元素初始化为 0
# 初始化一个数组 maps,作为记录模型 mAP 值的容器。
results = (0, 0, 0, 0, 0, 0, 0) # P, R, mAP@.5, mAP@.5-.95, val_loss(box, obj, cls)
# 初始化一个元组 results,作为记录模型性能指标的容器,方便在训练过程中进行记录和更新
scheduler.last_epoch = start_epoch - 1 # do not move
# 初始化学习率调度器 scheduler 的 last_epoch 属性,以便在训练开始时自动计算出下一个 epoch 的学习率
scaler = torch.cuda.amp.GradScaler(enabled=amp)
# 创建一个 PyTorch 自动混合精度训练的梯度缩放器 scaler,并根据布尔变量 amp 是否启用自动混合精度进行配置
# 在训练模型的过程中,我们可以使用scaler对象来缩放梯度,并执行正常的反向传播和优化更新操作
# 由于采用了优化的计算方式,自动混合精度训练可以让模型在不影响性能的情况下,更快地完成训练
##早停止,不更新结束训练
stopper, stop = EarlyStopping(patience=opt.patience), False
# 创建一个 EarlyStopping 对象 stopper,并将布尔变量 stop 的值初始化为 False
# 在训练过程中,EarlyStopping 是一个常用的策略,用于避免模型训练过度拟合
# 通过在每个 epoch 结束后计算验证集上的性能表现,如果模型在连续若干 epoch 上没有明显的改善,就可以终止训练,避免进一步过度拟合
# stopper 对象是一个EarlyStopping类的实例,用于在模型训练过程中进行性能验证和终止训练操作
# patience 参数指定了在连续多少个 epoch 上没有出现性能改善时,就会触发 EarlyStopping 策略
# stop 变量表示是否需要终止训练
#初始化损失函数
compute_loss = ComputeLoss(model) # init loss class
# 创建一个 ComputeLoss 对象 compute_loss,用于计算模型在每个 epoch 上的损失函数值,
# 并将模型 model 作为其输入参数进行初始化
callbacks.run('on_train_start')
# 打印日志信息
LOGGER.info(f'Image sizes {imgsz} train, {imgsz} val\n'##打印训练和测试输入图片分辨率
f'Using {train_loader.num_workers * WORLD_SIZE} dataloader workers\n'##加载图片时调用的cpu进程数
f"Logging results to {colorstr('bold', save_dir)}\n"##日志目录
f'Starting training for {epochs} epochs...')##从哪个epoch开始训练
"""
这段代码主要是释放训练开始命令和更新权重
首先训练过程走起,通过model.train()函数告诉模型已经进入了训练阶段。因为有些层或模型在训练阶段与预测阶段进行的操作是不一样的,
所以要通过model.train()函数用来声明,接下来是训练。
然后是更新图片的权重。训练时有些类的准确率可能比较难以识别,准确率并不会很高。在更新图片权重时就会把这些难以识别的类挑出来,
并为这个类产生一些权重高的图片,以这种方式来增加识别率低的类别的数据提高准确率
(准确率低,就让它学的更多)
训前准备,做一些参数的初始化。这里要提到两个点:
第一个是warmup。warmup是一种学习率的优化方法,最早出现在ResNet的论文中。简单来说,在模型刚开始训练时,使用较小的学习率开始摸索,经过几轮迭代后使用大的学习率加速收敛,在快接近目标时,再使用小学习率,避免错过目标。
第二个是早停机制。当训练一定的轮数后,如果模型效果未提升,就让模型提前停止训练。这里的默认轮数为100轮,判断模型的效果为fitness,fitness为0.1乘mAP@0.5加上0.9乘mAP@0.5:0.95。
"""
for epoch in range(start_epoch,epochs):##开始一轮一轮训练epoch
callbacks.run('on_train_epoch_start')# 训练过程即将开始,用于执行一些训练预处理或初始化的操作
# 回调函数:对象列表或对象,用于在不同的训练过程中进行一些数据的处理或展示
# 告诉模型现在是训练阶段 因为BN层、DropOut层、两阶段目标检测模型等
# 训练阶段阶段和预测阶段进行的运算是不同的,所以要将二者分开
# model.eval()指的是预测推断阶段
model.train()##启用了模型的训练模式
# 在训练模式下,模型启用了一些特定的模块,比如 Dropout 和 Batch Normalization,用于防止模型的过拟合和稳定梯度更新
# 在测试模式下,模型禁用了这些特定的模块,以便对新数据进行精确的预测
# Update image weights (optional, single-GPU only)
# 用于从数据集标签中计算并更新图像权重
# 一批一批传数据时,难识别的目标希望它多传入几次,数据集的每张图片分配一个采样权重,难识别的权重大
# 更新图片的权重
if opt.image_weights:#获取图片采样的权重
##经过一轮训练,若哪一类的不精确度高,那么这个类就会被分配一个较高的权重,来增加它被采样的概率
cw = model.class_weights.cpu().numpy() * (1 - maps) ** 2 / nc # class weights
##将计算出的权重换算到图片维度,将类别的权重换算为图片的权重
# 算出来类别权重,但传给模型的是图片,而不是检测框,所以需要把类别权重转换为图片权重
iw = labels_to_image_weights(dataset.labels, nc=nc, class_weights=cw) # image weights
##通过random.choices生成图片索引indices,从而进行采样,这时候图像中包含一些难识别的样本
# 选择一些样本进行训练的过程:基于每个样本的图像权重,从数据集中随机选取相应数量的样本
# 可以确保训练过程中每个类别都得到了足够的关注,从而减少类别不平衡的影响
dataset.indices = random.choices(range(dataset.n), weights=iw, k=dataset.n)# rand weighted idx
"""
训练过程开始,首先通过model.train()函数告诉模型,现在是训练阶段。因为有些层或模型在训练阶段与预测阶段进行的操作是不一样的,
所以要通过model.train()函数用来声明,接下来是训练。若是预测阶段,则可以用model.eval().
然后是更新图片的权重。训练时有些类的准确率可能比较难以识别,准确率并不会很高。在更新图片权重时就会把这些难以识别的类挑出来,
并为这个类产生一些权重高的图片,以这种方式来增加识别率低的类别的数据量。提高准确率。
"""
"""
这段代码主要是分布式训练的设置,以及训练时控制台的显示
首先DDP模式打乱数据,并进行随机采样然后设置训练时控制台的显示。
LOGGER.info是输出的表头,tqdm 显示进度条效果
最后将优化器中所有的参数梯度设为0
"""
# 用于存放三个损失
##初始化训练时打印的平均损失信息
# 初始化mloss变量,用于缓存在训练过程中的损失值,并且将其存储在指定的设备上,以便更高效地计算和更新
mloss = torch.zeros(3, device=device) # mean losses
##分布式训练的设置
##DDP模式打乱数据,并且dpp.sampler的随机采样数据是基于epoch+seed作为随机种子,每次epoch不同,随机种子不同
if RANK != -1:# 分布式训练
train_loader.sampler.set_epoch(epoch)# 需要设置sampler的随机数种子,以保证每个epoch中样本的随机性
##将训练数据迭代器做枚举,可以遍历出索引值 # 遍历train_loader时获取进度条信息
pbar = enumerate(train_loader)
##训练参数的表头
# 日志记录
LOGGER.info(('\n' + '%11s' * 7) % ('Epoch', 'GPU_mem', 'box_loss', 'obj_loss', 'cls_loss', 'Instances', 'Size'))
if RANK in {-1, 0}:
##通过tqdm创建进度条,方便训练信息的展示
pbar = tqdm(pbar, total=nb, bar_format=TQDM_BAR_FORMAT) # progress bar
##将优化器中的所有梯度参数设为0
# 显示训练进度
# 清空梯度缓存,准备下一次新的梯度计算
# 通常在每个batch的训练开始前,我们都会调用这个方法
# 以清空上一个batch的梯度缓存,并开始当前batch的正向传播和相应的反向传播计算
optimizer.zero_grad()
#定义一些训练中的参数,制作控制台输出的表头以及进度条效果waa清零
"""
这段代码主要是分批加载数据和热身训练
首先分批加载训练数据,用ni计算当前迭代的次数,并作图片的归一化。
然后进行热身训练(warmup) ,这里只对训练初期使用较小的学习率。对于bias参数组的学习率策略是从0.1逐渐降低至初始学习率,其余参数组则从0开始逐渐增长至初始学习率。
最后进行多尺度训练:
imgz: 默认训练尺寸
模型最大stride=32
"""
for i ,(imgs,targets,paths,_) in pbar:###一批一批的取数据,每次取16batch
# 遍历train_loader中的所有数据,并获得当前batch的输入、标签以及对应的路径信息,从而进行模型的训练
callbacks.run('on_train_batch_start')
# 从第0轮开始,到目前为止一共训练了多少批数据,起到记录批次的功能
##ni:计算当前迭代次数iteration
ni=i+nb*epoch# number integrated batches (since train start)
# 将imgs转换为PyTorch张量,并将其移动到GPU设备上进行加速计算
# non_blocking=True表示数据转移过程是非阻塞的,这意味着转移操作将在后台异步进行,而不会影响后续的代码执行。这样可以提高数据转移和模型计算的效率
# .float() / 255是将数据类型转换为float,并进行归一化操作,
# 将像素值从[0, 255]范围缩放到[0, 1]范围内,以便更好地用于训练和优化模型
##将图片加载至设备,并作归一化
imgs=imgs.to(device,non_blocking=True).float()/255 # uint8 to float32, 0-255 to 0.0-1.0
"""
热身训练(前nw次选代),热身训练选代的次数iteration范围[1:nw].
在前nw次选代中,根据以下方式选取accumulate和学习率
Iteration是指模型在训练期间重复运行的次数,而epoch是指模型在训练期间看到整个数据集的次数。
iteration迭代(iteration)是指模型在训练过程中,每次使用一个训练样本来更新模型参数的过程,一次迭代可以包括多个步骤,比如计算损失函数、更新参数等。
简单来说,神经网络训练一个batch的过程称之为一个iteration。
"""
# Warmup
# warmup为热身训练 是一种学习率的优化方法
# 若总批次再warmup批次内 则进行热身训练
# Warmup 热身训练,开始使用小学习率,慢慢升到设置的学习率
if ni<=nw:##当前批次小于设置的wp所需的批次时,不需要更新学习率
xi=[0,nw]##x interp
# compute_loss.gr = np.interp(ni, xi, [0.0, 1.0]) # iou loss ratio (obj_loss = 1.0 or iou)
# 计算在梯度累积方式下,需要累积多少个batch的梯度
accumulate=max(1,np.interp(ni,xi,[1,nbs/batch_size])).round()
"""
线性插值是指插值函数为一次多项式的插值方式,其在插值节点上的插值误差为零。线性插值相比其他插值方式,如抛物线插值,具有简单、方便的特点。线性插值的几何意义即为概述图中利用过A点和B点的直线来近似表示原函数。
线性插值法是认为现象的变化发展是线性的、均匀的,所以可利用两点式的直线方程式进行线性插值。估算的是两点之间的点的对应值。
y = np.interp(x, xp, fp)
x: 数组 待插入数据的横坐标.
xp: 一维浮点数序列
原始数据点的横坐标,如果period参数没有指定那么就必须是递增的。否则,在使用xp = xp % period正则化之后,xp在内部进行排序.
fp: 一维浮点数或复数序列,原始数据点的纵坐标,和xp序列等长.
"""
# 遍历优化器中的所有参数组
for j,x in enumerate(optimizer.param_groups):# 循环更新优化器参数
# bias lr falls from 0.1 to lr0, all other lrs rise from 0.0 to lr0
#计算当前参数组的学习率lr
# 需要计算当前学习率下降的幅度,可以使学习率在训练的初期快速增加,帮助模型更快地收敛;
# 然后,随着训练的进行,逐渐减小学习率,避免训练过程中震荡不收敛
# 如果当前为第一次学习率更新(即j为0),使用’warmup_bias_lr’这个超参数作为学习率的下降幅度,否则下降幅度为0
# 根据下降幅度,接着将其乘上当前参数组的初始学习率x[‘initial_lr’]
# 并使用一个类型为lf的函数对学习率进行动态调整。最终得到的结果就是当前参数组的学习率lr
# bias lr falls from 0.1 to lr0, all other lrs rise from 0.0 to lr0
# 对于bias参数组的学习率 会从0.1逐渐降低到初始学习率 使用线性插值的方式更新
# 对于其他参数组的学习率,将会从0逐渐升高到初始学习率 使用线性插值的方式更新
"""
bias的学习率从0.1下降到基准学习率lr*lf(epoch),
其他的参数学习率从0增加到lr*lf(epoch)
lf为上面设置的余弦退火的衰减函数
"""
x['lr']=np.interp(ni,xi,[hyp['warmup_bias_lr'] if j==2 else 0.0 ,x['initial_lr']*lf(epoch)])
# 若参数组中有动量值存在
if 'momentum' in x:
# 动力值也会按照线性插值的方式进行更新
x['momentum'] = np.interp(ni, xi, [hyp['warmup_momentum'], hyp['momentum']])
# 根据全局训练步数的值来动态调整当前参数组的动量
# 在全局训练步数ni小于阈值nw时,动量的值逐渐增加,以加速模型参数的更新过程;
# 当全局训练步数ni超过阈值nw时,动量的值逐渐减少,以减缓模型参数的更新速度,避免在minima处震荡。-->冲的速度快,可以理解为走的快或者步长大-->能冲过小坡
# Multi-scale 多尺度训练 数据增强过程中,对图像进行随机尺寸缩放
# Multi-scale 设置多尺度训练,从imgsz * 0.5,imgsz * 1.5 + gs随机选取尺寸
# imgsz: 默认训练尺寸 gs: 模型最大stride = 32[32,16,8]
if opt.multi_scale:##随机改变图片的尺寸
sz = random.randrange(imgsz * 0.5, imgsz * 1.5 + gs) // gs * gs # size
sf = sz / max(imgs.shape[2:]) # scale factor
# 训练过程随机化得到一个比例因子 用这个因子改变输入图片的尺度,起到多尺度训练的效果
if sf != 1:#首先判断缩放比例sf是否等于1,如果不等于1则说明需要对图像进行缩放
ns = [math.ceil(x * sf / gs) * gs for x in
imgs.shape[2:]] # new shape (stretched to gs-multiple)
# 计算出缩放后的新尺寸ns,将其对齐到gs的整数倍
##下采样
imgs = nn.functional.interpolate(imgs, size=ns, mode='bilinear', align_corners=False)
# 使用nn.functional.interpolate函数对图像进行插值操作,缩放到新尺寸ns
# 最终得到的imgs就是缩放后的图像数据
"""
这段代码主要是分批加载数据和热身训练
首先分批加载训练数据,用ni计算当前迭代的次数,并作图片的归一化。
然后进行热身训练(warmup),这里只对训练初期使用较小的学习率。对于bias参数组的学习率策略是从0.1逐渐降低至初始学习率,其余参数组则从0开始逐渐增长至初始学习率
最后进行多尺度训练:
imgz: 默认训练尺寸
模型最大stride=32gs:
"""
"""
这段代码主要是正向传播、反向传播、以及更新参数
首先正向传播即将图片输入模型,并做一次正向传播,最后得到一个结果。这个结果在训练初期的效果可能会比较差,将这个结果与图片的标签值求损失,目的就是让这个损失越来越小。
接下来将这个误差,通过链式求导法则,反向传播回每一层,求出每层的梯度
最后利用optimizer.step更新参数。但是要注意,在更新参数时这里有一个不一样的地方,并不会在每次反向传播时更新参数,
而是做一定的累积,反向传播的结果并不会顶替上一次反向传播结果,而是做一个累积。完成一次积累后,再将梯度清零,方便下一次清零。
这样做是为了以更小的batch size实现更高的batch size效果。
"""
# 接下来就开始分批加载训练数据,并作图片的归一化。然后进行热身训练(warmup),这里只对训练初期使用较小的学习率。对于bias参数组的学习率策略是从
# 0.1逐渐降低至初始学习率,其余参数组则从0开始逐渐增长至初始学习率。有助于使模型收敛速度变快,效果更佳.
##Forward:前向传播
with torch.cuda.amp.autocast(amp):##开启混合精度训练
pred=model(imgs)##forward 将图片输入网络前向传播得到预测结果
# 计算损失,包括分类损失,objectness损失,框的回归损失
"""
box: 预测框的损失.
cls:分类的损失
obj:置信度损失
"""
# 再将预测的结果与标签值计算损失
# 得到的loss为每项加权后的损失之和 loss_item是每一项的损失
# 每项损失分别为加权后的 预测框损失 置信度损失 分类损失
# loss为总损失值,loss_items为一个元组,包含分类损失,objectness损失,框的回归损失和总损失
loss,loss_items=compute_loss(pred,targets.to(device))##loss scaled by batch_size
# 利用模型预测信息和标注信息计算损失值和损失组件
# 字典类型的loss_items,该字典记录了模型的损失值相对于不同组件的损失贡献度
if RANK != -1: # 分布式训练
##采样DDP训练,平均不同gpu之间的梯度
loss *= WORLD_SIZE # gradient averaged between devices in DDP mode
if opt.quad:
### 如果采用collate_fn4取出mosaic4数据Loss也要翻4倍
loss *= 4.
# loss将会乘以4倍以增大惩罚项的强度,进一步影响模型训练的收敛速度和结果
# 某些任务通常要求模型在高频率信息的保留方面做得更好,同时往往要付出更大的计算代价
# 模型的损失函数加入一些惩罚项,比如L2损失项,以此来约束预测结果与真实值之间的平滑程度。
# Backward 反向传播
# Backward
# scale为使用自动混合精度运算
# 这一步将误差反传
# 使用混合精度进行反向传播之前,我们需要把损失值通过scaler.scale()乘上比例因子,以确保数值的稳定性
scaler.scale(loss).backward()
##0ptimize 模型会对多批数据进行梯度累积(对应参数的梯度相加),只有达到累计次数的时候才会更新参数,再还没有达到累积次数时 loss会不断的叠加 不会被新的反传替代
# Optimize - https://pytorch.org/docs/master/notes/amp_examples.html
# Optimize
# 这里会对多批数据进行累积
# 只有达到累计次数的时候才会更新参数
# 再还没有达到累积次数时 loss会不断的叠加 不会被新的反传替代
# 当前训练步数(ni)和上一次优化步数(last_opt_step)
# 之差大于等于指定的累积梯度步数(accumulate)时,执行优化器的step操作,进行参数更新
if ni - last_opt_step>=accumulate:
# 执行了自动混合精度的反向传播操作之后,使梯度返回到原始的32位浮点型格式
# (反向自动混合精度),以便进行进一步的梯度处理或优化器更新操作。
scaler.unscale_(optimizer) # unscale gradients
# 对模型的梯度进行裁剪,避免其过大而导致梯度爆炸的问题
# 梯度爆炸指的是在反向传播过程中神经网络的梯度变得非常大,从而导致模型的训练变得不稳定
# 这种情况可能会导致梯度消失或梯度耗散,从而影响模型的收敛速度和效果
# 当梯度的范数(也称为“L2范数”)超过了指定的最大值max_norm时,
# 裁剪操作将按比例缩小所有的梯度,以确保梯度的大小不超过max_norm
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=10.0) # clip gradients
# optimizer.step 更新优化器中的权重参数
# 使用自动混合精度(AMP)功能更新scaler的状态
# 使用scaler.step()函数更新参数后,我们需要通过scaler.update()函数更新缩放比例因子,以便在下一次batch中使用
scaler.step(optimizer)##optimizer.step参数更新
##更新参数
scaler.update()
##完成一次累计后,再梯度清零,方便梯度更新
# 将所有模型参数的梯度信息归零,以确保当前batch训练使用的是新的梯度信息
optimizer.zero_grad()
if ema:
ema.update(model)
# 在训练过程中,每次更新后,EMA算法会对模型参数进行平均值计算,
# 并将平均值应用到训练好的模型参数中,以平滑模型参数的变化并防止过拟合
##计数
last_opt_step=ni
# 把last_opt_step更新为当前的训练步数,用于下一次的优化器step操作
# Log 日志记录
"""
这段代码主要是打印训练相关信息,结束后做权重衰减
首先将每批最后的数据输出至控制台。到此每批循环体结束
然后在每所有批训练结束时,做学习率衰减,进入下一轮的训练.
权重衰减 (weight decay),是一种正则化的方法,应用了权重衰减的神经网络,最终某些权重会变成零 [1],
相当于输入在这个神经元上会被抛弃。神经网络中某些权重归零,表示模型的复杂度下降了,多了一个零,少了一个参数。
L2正则化的目的就是为了让权重衰减到更小的值,在一定程度上减少模型过拟合的问题,所以权重衰减也叫L2正则化。
学习率衰减 (learning rate decay) 就是调小学习率,使到损失可以进一步减小。如果整个优化过程,学习率一直保持较大的数值,
那么在损失可能在局部最小值的周围震荡,而又到不了局部最小。将学习率变小,学习的步长变小了,使到损失可以到达局部最小。
# 根据nbs、bs、accumulate调整权重衰减系数
# 用于控制过拟合的权重衰减参数,使其在不同的批次大小和梯度累积步数下,具有相似的作用,避免影响模型的训练效果
为什么叫权重衰退?
答:是因为λ的引入(权重衰减系数),使得在更新参数前,把当前的权重进行了一次缩小(一次衰退)。
"""
##打印print一些信息 包括当前epoch、显存、损失(box、obj、cIs、total)、当前batch的target的数量和图片的size等信息
if RANK in {-1, 0}:
##打印显存,进行的轮次,损失,target的数量和图片size等信息
mloss = (mloss * i + loss_items) / (i + 1) # update mean losses
# 该公式通过计算之前所有的平均值和当前batch中的损失值来更新新的平均值 计算显存
mem = f'{torch.cuda.memory_reserved() / 1E9 if torch.cuda.is_available() else 0:.3g}G' # (GB)
##进度条显示以上信息
pbar.set_description(('%11s' * 2 + '%11.4g' * 5) %
(f'{epoch}/{epochs - 1}', mem, *mloss, targets.shape[0], imgs.shape[-1]))
##调用loggers中的on_train_batch_end方法,将日志记录并生成一些记录的图片
callbacks.run('on_train_batch_end', model, ni, imgs, targets, paths, list(mloss))
if callbacks.stop_training:# 训练结束
return
# end batch ------------------------------------------------------------------------------------------------
#接下来就是将每批最后的数据输出至控制台。到此每批循环体结束
# Scheduler
lr = [x['lr'] for x in optimizer.param_groups] # for loggers
# 获取优化器(optimizer)中每个参数组(param_groups)的学习率(lr)
scheduler.step() # 一轮所有批次训练完后,根据之前的学习率更新策略更新学习率
"""
这段代码主要是得到results,mAps相关信息
首先判断是否应当结束训练,若选择每轮验证或当前已是最后一轮的情况下,做一次验证。
然后计算出最好的模型。这里“最好”的评判标准即为ftness。fi:[P,R, mAP@.5, mAP@.5.95]的一个加权值=0.1*mAP@.5 + 0.9*mAP@.5-.95,
在评判标准中,更加强调mAP@0.5:0.95的作用。mAP@0.5:0.95大-->代表模型在多个IOU闽值的情况下,都可以较好的识别物体。
"""
#在每所有批训练结束时,做权重衰减,进入下一轮的训练。
##训练完成后,保存模型
if RANK in {-1,0}:
##mAP 计算平均精度 # 调用Loggers中的on_train_epoch_end方法 做一次epoch自增的记录
callbacks.run('on_train_epoch_end',epoch=epoch)
# 更新ema的后面几个属性
ema.update_attr(model,include=['yaml','nc','hyp','names','stride','class_weights'])
# 判断当前是否为最后一个epoch,以便在训练结束时进行相关的操作
# 当前epoch数是否等于总的epoch数减1(epochs-1),或者调用了早停机制而停止了训练
final_epoch=(epoch+1==epochs) or stopper.possible_stop
# 如果使用每轮的验证 或 已经是训练的最后一轮
# 判断noval变量是否为False,如果是False则表示当前需要进行验证操作
# 判断了final_epoch变量的值,如果final_epoch为True表示当前是最后一个epoch,则需要进行验证操作
if not noval or final_epoch:##calculate mAP
# 验证模型效果 返回验证结果以及 mAP
results,maps,_=validate.run(data_dict,
batch_size=batch_size // WORLD_SIZE * 2,
imgsz=imgsz,
half=amp,
model=ema.ema,
single_cls=single_cls,
dataloader=val_loader,
save_dir=save_dir,
plots=False,
callbacks=callbacks,
compute_loss=compute_loss)
# Update best mAP
# 计算fitness
# fitness是 0.1乘mAP@0.5 加上 0.9乘mAP@0.5:0.95
# 更加看重mAPA0.5:0.95的作用
##update best map
# Update best mAP 更新最优map
# 计算fitness
# fitness是 0.1乘mAP@0.5 加上 0.9乘mAP@0.5:0.95
# 更加看重mAPA0.5:0.95的作用
fi=fitness(np.array(results).reshape(1,-1))##weighted combination of [P,R,mAP@.5,mAP@.5-.95]
# 根据给定的评价指标来计算当前训练的模型对于验证集的表现
stop=stopper(epoch=epoch,fitness=fi)# early stop check 检查是否需要提前停止
# 若当前的fitness大于最佳的fitness
if fi>best_fitness:
# 将最佳fitness更新为当前fitness
best_fitness=fi
# 保存验证结果
log_vals = list(mloss)+list(results)+lr
# 记录验证数据
# 将当前epoch的各个指标(例如损失函数值、验证结果、学习率)记录下来,进行可视
callbacks.run('on_fit_epoch_end',log_vals,epoch,best_fitness,fi)
##Save model
# Save model
"""
判断是否应当结束训练,若选择每轮验证或当前已是最后一轮的情况下,做一次验证。
并计算出最好的模型。这里“最好”的评判标准即为fitness。fitness是 0.1乘mAP@0.5 加上
0.9乘mAP@0.5:0.95,在评判标准中,更加强调mAP@0.5:0.95的作用。mAP@0.5:0.95大代表模型在多个IOU阈值的情况下,都可以较好的识别物体。
"""
"""
这段代码主要是保存模型
首先将当前训练过程中的所有参数赋值给ckpt。
然后判断这个模型的fitness是否是最佳,如果是,就保存这个最佳模型,保存完毕将变量从内存中删除.
保存带checkpoint的模型用于inference或resuming training
保存模型,还保存了epoch,results,optimizer等信息
optimizer将不会在最后一轮完成后保存
mode1保存的是EMA的模型
"""
# 如果保存模型
if (not nosave) or (final_epoch and not evolve): ##if save
# 将当前训练过程中的所有参数赋值给ckpt
# 如果nosave为False,则表示需要保存模型参数
# 如果final_epoch为True,且evolve为False,
# 则表示当前是最后一个epoch,且不处于进化算法(evolution)中,此时也需要保存模型参数。
ckpt = {
'epoch':epoch,
'best_fitness':best_fitness,
'model':deepcopy(de_parallel(model)).half(),
'ema':deepcopy(ema.ema).half(),
'updates':ema.updates,
'optimizer':optimizer.state_dict(),
'opt':vars(opt),
'git':GIT_INFO,##{remote,branch,commit} if a git reop
'date':datetime.now().isoformat()
}
##Save last ,best and delete
# Save last, best and delete
# 将当前训练的各项参数存储到一个字典ckpt中,以便在保存模型参数的时候使用
# 将ckpt保存为一个文件,以便在训练结束后重新使用模型参数
# Save last, best and delete 保存最后一次以及最好的一次
# 保存每轮的模型
torch.save(ckpt,last)# 使用torch.save函数将ckpt字典保存到文件中
# 如果这个模型的fitness是最佳的
# last表示指定的路径
if best_fitness ==fi:
# 保存这个最佳的模型
torch.save(ckpt,best)# 如果本轮拟合度最好,就保存best.pt
if opt.save_peroid>0 and epoch % opt.save_period ==0:
torch.save(ckpt, w / f'epoch{epoch}.pt')
# 按照一定的周期自动保存训练过程中的模型参数,方便我们进行后续的模型调试和评估
# 模型保存完毕 将变量从内存中删除
del ckpt
# 记录保存模型时的日志
callbacks.run('on_model_save',last,epoch,final_epoch,best_fitness,fi)
# Stop Single-GPU
# 停止单卡训练
##EarlyStopping 是否提前停止训练
if RANK != -1: ##if DDP training
broadcast_list=[stop if RANK == 0 else None]
dist.broadcast_object_list(broadcast_list,o) ##broadcast 'stop' to all ranks
if RANK !=0:
stop= broadcast_list[0]
if stop:# 如果满足停止训练条件,则跳出训练,防止过拟合
break ##must break all DDP ranks
"""
4.4 训练过程小结:(1)初始化训练需要的模型参数: 设置/初始化一些训练要用的参数(hyp[box]、hyp[cls]、 hyp[obj]、hyp[label_smoothing')
+从训练样本标签得到类别权重model.class weights、model.names。
(2) 热身部分: 热身选代的次数iterationsnw、last opt step、初始化maps和results、学习率衰减所进行到的轮次scheduler.last epoch +
设置amp混合精度训练scaler + 初始化损失函数compute loss + 打印日志信息)
(3)开始训练: 图片采样策略 + Warmup热身训练 + multi scale多尺度训练 + amp混合精度训练+accumulate 梯度更新策略+
打印训练相关信息(包括当前epoch、显存、损失(box、obi、cs、total)+当前batch的target的数量和图片的size等 + 调整学习率、scheduler.step()、emp val.run()得到results,
maps相关信息(4)训练完成保存模型: 将测试结果results写入result.txt中、wandb_logger、 Update best mAP 以加权mAP fitness为衡量标准+保存模型
"""
#接下来将最后一轮的模型,以及fitness最佳的模型保存下来。至此训练结束。
##end epoch------------------------------
#接下来将最后一轮的模型,以及fitness最佳的模型保存下来。至此训练结束。
#end training_________________________
if RANK in {-1,0}:
# 训练停止 向控制台输出信息
LOGGER.info(f'\n{epoch - start_epoch+1} epoch completed in {(time.time() - t0)/3600 : .3f} hours.')
# 可视化训练结果: results1.png confusion_matrix.png以及('F1',PR',p',R')曲线变化 日志信息
# 遍历两个文件名
for f in last,best:
# 如果文件存在
if f.exists():
strip_optimizer(f)##strip optimizers
# 训练结束之后,将模型文件中的优化器信息删除,以便模型可以被更方便地加载、测试和部署
##模型训练结束之后,strip_optimizer函数将optimizer从ckpt中删除
if f is best:#用效果最好的权重在验证集上再跑一遍 并输出最终的指标以及每一类的各个指标
# 把最好的模型在验证集上跑一边 并绘图
LOGGER.info(f'\nValidating {f}...')
results,_,_=validate.run(
data_dict,
batch_size=batch_size // WORLD_SIZE * 2,
imgsz=imgsz,
model=attempt_load(f,device).half(),
iou_thres=0.65 if is_coco else 0.60,##best pycocotools at iou 0.65
single_cls=single_cls,
dataloader=val_loader,
save_dir=save_dir,
save_json=is_coco,
verbose=True,
plots=plots,
callbacks=callbacks,
compute_loss=compute_loss
)##val best model with plots
# 对模型进行验证,并获得模型在验证集上的性能表现,从而对模型进行调整和优化
if is_coco:##如果是coco数据集
callbacks.run('on_fit_epoch_end',list(mloss)+list(results)+lr,epoch,best_fitness,fi)
# 记录训练终止时的日志
callbacks.run('on_train_end',last,best,epoch,results)
##释放显存
torch.cuda.empty_cache()# 清空PyTorch在当前显存中所占用的缓存空间
return results##返回结果
"""
train()函数的最后,把最佳的模型取出,用这个最佳的模型跑一边验证集。再将结果保存下来,至此训练完成。
若使用了超参数进化,还会进行多次训练,来完成超参数的调整。
"""
"""
这段函数主要打印信息并释放显存
首先当训练停止的时候回向控制台输出信息(打印训练时间、plots可视化训练结果results1.png.confusion matrix.png 以及(F1,PR,P,R曲线变化、日志信息)
然后把最佳的模型取出,用这个最佳的模型跑一边验证集。再将结果保存下来,至此训练完成。若使用了超参数进化,还会进行多次训练,来完成超参数的调整
接着进行coco评价,也就是说只在coco数据集才会运行
最后释放显存
return results~
"""
#########执行run()函数
def run(**kwargs):##执行这个脚本/调用train函数/开启训练
opt=parse_opt(True)
for k,v in kwargs.items():
setattr(opt,k,v)##setattr()复值属性,属性不存在则创建一个复值
main(opt)
return opt
"""
因为是为导入提供的,这里无法使用命令行设置参数。这里通过打包传参的方式,将参数传入run()函数,
再用setattr函数,通过设置属性值的方式将参数设置给opt对象。
train模块中还有一个函数 run()函数,run()函数内的内容与主函数差不多,都是调用了parse_opt()函数与main()函数,只不过run()函数是为导入时提供的,
别的模块导入了train模块,即可通过调用run()函数执行训练过程。
这段代码主要是使得支持指令执行这个脚本
大家也可以看出来哈,run函数内的内容与主函数差不多呢,都是调用了parse_opt()函数
与main(函数)(其实写到这时我以为我出现了幻觉),我去查了一下,run()函数是为导入时提供的,别的模块导入了train模块即可通过调用run)函数执行训练过程。
"""
def parse_opt(known=False):
parser=argparse.ArgumentParser()
parser.add_argument('--weights', type=str, default=ROOT / 'yolov5s.pt', help='initial weights path')
parser.add_argument('--cfg', type=str, default='', help='model.yaml path')
parser.add_argument('--data', type=str, default=ROOT / 'data/coco128.yaml', help='dataset.yaml path')
parser.add_argument('--hyp', type=str, default=ROOT / 'data/hyps/hyp.scratch-low.yaml', help='hyperparameters path')
parser.add_argument('--epochs', type=int, default=100, help='total training epochs')
parser.add_argument('--batch-size', type=int, default=16, help='total batch size for all GPUs, -1 for autobatch')
parser.add_argument('--imgsz', '--img', '--img-size', type=int, default=640, help='train, val image size (pixels)')
parser.add_argument('--rect', action='store_true', help='rectangular training')
parser.add_argument('--resume', nargs='?', const=True, default=False, help='resume most recent training')
parser.add_argument('--nosave', action='store_true', help='only save final checkpoint')
parser.add_argument('--noval', action='store_true', help='only validate final epoch')
parser.add_argument('--noautoanchor', action='store_true', help='disable AutoAnchor')
parser.add_argument('--noplots', action='store_true', help='save no plot files')
parser.add_argument('--evolve', type=int, nargs='?', const=300, help='evolve hyperparameters for x generations')
parser.add_argument('--bucket', type=str, default='', help='gsutil bucket')
parser.add_argument('--cache', type=str, nargs='?', const='ram', help='image --cache ram/disk')
parser.add_argument('--image-weights', action='store_true', help='use weighted image selection for training')
parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')
parser.add_argument('--multi-scale', action='store_true', help='vary img-size +/- 50%%')
parser.add_argument('--single-cls', action='store_true', help='train multi-class data as single-class')
parser.add_argument('--optimizer', type=str, choices=['SGD', 'Adam', 'AdamW'], default='SGD', help='optimizer')
parser.add_argument('--sync-bn', action='store_true', help='use SyncBatchNorm, only available in DDP mode')
parser.add_argument('--workers', type=int, default=8, help='max dataloader workers (per RANK in DDP mode)')
parser.add_argument('--project', default=ROOT / 'runs/train', help='save to project/name')
parser.add_argument('--name', default='exp', help='save to project/name')
parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment')
parser.add_argument('--quad', action='store_true', help='quad dataloader')
parser.add_argument('--cos-lr', action='store_true', help='cosine LR scheduler')
parser.add_argument('--label-smoothing', type=float, default=0.0, help='Label smoothing epsilon')
parser.add_argument('--patience', type=int, default=100, help='EarlyStopping patience (epochs without improvement)')
parser.add_argument('--freeze', nargs='+', type=int, default=[0], help='Freeze layers: backbone=10, first3=0 1 2')
parser.add_argument('--save-period', type=int, default=-1, help='Save checkpoint every x epochs (disabled if < 1)')
parser.add_argument('--seed', type=int, default=0, help='Global training seed')
parser.add_argument('--local_rank', type=int, default=-1, help='Automatic DDP Multi-GPU argument, do not modify')
# Logger arguments
parser.add_argument('--entity', default=None, help='Entity')
parser.add_argument('--upload_dataset', nargs='?', const=True, default=False, help='Upload data, "val" option')
parser.add_argument('--bbox_interval', type=int, default=-1, help='Set bounding-box image logging interval')
parser.add_argument('--artifact_alias', type=str, default='latest', help='Version of dataset artifact to use')
return parser.parse_known_args()[0] if known else parser.parse_args()
"""
main()函数的参数中opt为命令行参数对象,callbacks中记录了Loggers类中的所有函数名。例如,可以通过callbacks.run("on_train_epoch_end")调用Loggers类中的on_train_epoch_end()函数。这样做方便在不同训练阶段,对日志的记录做统一管理。
前期准备
首先看main函数接收的参数,opt为命令行参数;callbacks为训练过程中保存的一些参数
主函数中调用了两个函数,第parse_opt()函数用于接收命令行参数,并将包含参数的对象传递给main()函数。
parse_opt()函数
parse_opt()函数用于添加自定义的命令行参数。
接下来是设置分布式训练时所需的环境变量。分布式训练指的是多GPU训练,将训练参数分布在多个GPU上进行训练,有利于提升训练效率。
主函数
接下来按照程序的运行顺序,跳过函数定义部分,先看主函数
"""
if __name__ == '__main__':
opt=parse_opt()
main(opt)
yolov5--train.py
最新推荐文章于 2023-12-26 10:35:24 发布