- 小白学习阶段,写文为了整理思路并方便自查,注释结合了网络上各位大佬的观点,加上自己的理解,若有理解不到位之处,还望指正。
本文主要借鉴了以下两篇文章:
1.YOLOv5源码解读1.3-训练train.py_汉卿HanQ的博客-CSDN博客
2.YOLOv5-6.2源码解析-train.py(超级无敌巨详细版)_yolov5源码_Seven、K的博客-CSDN博客
1.获取文件路径
FILE = Path(__file__).resolve()#path().resolve:将路径设置为绝对路径
ROOT = FILE.parents[0] # YOLOv5 root directory得到YOLOv5的根路径
if str(ROOT) not in sys.path:
sys.path.append(str(ROOT)) # add ROOT to PATH添加到系统路径
ROOT = Path(os.path.relpath(ROOT, Path.cwd()))
- os.path.relpath:是求相对路径的,从Path.cwd()后面第一个文件夹或者文件开始计算相对路径,Path.cwd()是root的一部分。
2.分布式训练初始化
LOCAL_RANK = int(os.getenv('LOCAL_RANK', -1)) # https://pytorch.org/docs/stable/elastic/run.html
RANK = int(os.getenv('RANK', -1))
WORLD_SIZE = int(os.getenv('WORLD_SIZE', 1))
GIT_INFO = check_git_info()
- os.getenv:获取环境变量的值(若存在则为一个字符串),否则返回默认值(此处为-1和1)
- check_git_info() :获取当前 git 仓库的分支 (branch)、提交号 (commit hash)、提交信息 (commit message) 等相关信息,以便记录实验记录和结果
- rank和local_rank区别在于前者用于进程间通讯, 后者用于本地设备分配
3.train函数
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')
- callbacks.run():这是一个在训练过程中调用的回调函数,它会在预训练过程开始时被触发。具体来说,它会遍历已注册的操作并在主线程中触发所有回调,参数包括要检查的钩子的名称、从 YOLOv5 接收的参数、是否在守护线程中运行回调以及从 YOLOv5 接收的关键字参数。在这个回调函数中,可以编写一些代码来在预训练过程开始时执行一些操作。
3.1.目录(Directories)
w = save_dir / 'weights' # weights dir
(w.parent if evolve else w).mkdir(parents=True, exist_ok=True) # make dir
last, best = w / 'last.pt', w / 'best.pt'
mkdir(parents=True, exist_ok=True):
parents:如果父目录不存在,是否创建父目录
exist_ok:只有在目录不存在时创建目录,目录已存在时不会抛出异常。
- w.parent if evolve else w:如果evolve非零,取w.parent;否则取w
3.2.超参数(Hyperparameters)
if isinstance(hyp, str):
with open(hyp, errors='ignore') as f:
hyp = yaml.safe_load(f) # load hyps dict
LOGGER.info(colorstr('hyperparameters: ') + ', '.join(f'{k}={v}' for k, v in hyp.items()))
opt.hyp = hyp.copy() # for saving hyps to checkpoints
- isinstance(hyp, str): 判断hyp是不是字符串
- with open() as f:是python文件的读写语句,打开hyp文件,参数errors遇到有些编码不规范的文件,你可能会遇到UnicodeDecodeError,因为在文本文件中可能夹杂了一些非法编码的字符。遇到这种情况,open()函数还接收一个errors参数,表示如果遇到编码错误后如何处理。最简单的方式是直接忽略
- yaml.safe_load(f):yaml是专门用来写配置文件的,hpy返回一个python的字典,如{'gama': 0.001, 'sigma': 8.5}
- LOGGER.info:日志模块打印,其中{}表示占位符,colorstr变换颜色
3.3.保存运行设置(Save run settings)
if not evolve:
yaml_save(save_dir / 'hyp.yaml', hyp)
yaml_save(save_dir / 'opt.yaml', vars(opt))
- yaml_save是general.py中自定义函数,其中yaml.safe_dump将yaml文件序列化
- yaml_save(save_dir / 'hyp.yaml', hyp):保存超参数hyp为yaml文件
-
yaml_save(save_dir / 'opt.yaml', vars(opt)):保存命令行参数为yaml文件
3.4.加载日志(Loggers)
data_dict = None#定义数据字典
if RANK in {-1, 0}:#编号为-1、0
loggers = Loggers(save_dir, weights, opt, hyp, LOGGER) # loggers instance初始化
# 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
-
methods():general.py中自定义函数,获取类/实例方法
-
register_action:记录新操作到callback hook中,相当于将方法与一个字符串绑定
-
loggers.remote_dataset:获取自定义数据集链接
-
if resume:如果从远程恢复运行
3.5.配置(Config)
plots = not evolve and not opt.noplots # create plots
cuda = device.type != 'cpu'
init_seeds(opt.seed + 1 + RANK, deterministic=True)#初始化随机种子
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']
nc = 1 if single_cls else int(data_dict['nc']) # number of classes
names = {0: 'item'} if single_cls and len(data_dict['names']) != 1 else data_dict['names'] # class names
is_coco = isinstance(val_path, str) and val_path.endswith('coco/val2017.txt') # COCO dataset
-
with语句:一种用于管理资源的语法结构,它提供了一种简洁的方式来处理文件、网络连接、数据库连接等需要手动关闭的资源。使用with语句,可以确保在资源使用完毕后被正确的释放,无论是否发生异常
-
data_dict:为自定义数据集否则如果在本地找不到数据集,请下载、检查和/或解压缩数据集
-
train_path,val_path:训练集、验证集地址
-
nc:类别数 names:类别名称
-
is_coco:bool类型,判断数据集是否为coco数据集
3.6.网络模型(Model)
check_suffix(weights, '.pt') # check weights,检查文件是否可接收(.pt文件)
pretrained = weights.endswith('.pt')
if pretrained:
with torch_distributed_zero_first(LOCAL_RANK):
weights = attempt_download(weights) # download if not found locally
ckpt = torch.load(weights, map_location='cpu') # load checkpoint to CPU to avoid CUDA memory leak
model = Model(cfg or ckpt['model'].yaml, ch=3, nc=nc, anchors=hyp.get('anchors')).to(device) # create,加载模型
exclude = ['anchor'] if (cfg or hyp.get('anchors')) and not resume else [] # exclude keys获取anchor
csd = ckpt['model'].float().state_dict() # checkpoint state_dict as FP32,保存预训练模型参数
csd = intersect_dicts(csd, model.state_dict(), exclude=exclude) # intersect,判断csd, model.state_dict()参数多少相同(交集),筛选字典中键值对,省略exclude
model.load_state_dict(csd, strict=False) # load
LOGGER.info(f'Transferred {len(csd)}/{len(model.state_dict())} items from {weights}') # report,显示加载与训练权重的键值对和创建模型的键值对 如果pre=true则减少两个键值对
else:
model = Model(cfg, ch=3, nc=nc, anchors=hyp.get('anchors')).to(device) # create
amp = check_amp(model) # 检查PyTorch自动混合精度(AMP)功能。正确操作时返回True
- 该部分判断有无使用预训练,若使用了则导入预训练的模型参数,否则直接构建模型
3.7.冻结层(Freeze)
freeze = [f'model.{x}.' for x in (freeze if len(freeze) > 1 else range(freeze[0]))] # layers to freeze
for k, v in model.named_parameters():
v.requires_grad = True # train all layers
# 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):
LOGGER.info(f'freezing {k}')
v.requires_grad = False
- 冻结的层在反向传播中不再更新参数
- freeze中存放了需要被冻结的层
-
named_parameters:返回参数的姓名和参数本身
-
any() :用于判断给定的可迭代参数 iterable 是否全部为 False,则返回 False,如果有一个为 True,则返回 True
-
any(x in k for x in freeze):判断freeze列表中的任何一个元素是否包含在模型参数的名称k中,若有一个存在,则执行if下的两条语句
-
初始把所有的层设置为需要更新梯度,然后判断每个层是否在冻结列表里,如果在,则设置该层不更新梯度
3.8.图像大小(Image size)
gs = max(int(model.stride.max()), 32) # grid size (max stride)
imgsz = check_img_size(opt.imgsz, gs, floor=gs * 2) # verify imgsz is gs-multiple
- 为什么是32:yolov5中采用5次下采样,每次下采样输入的特征图的宽和高都会缩小两倍,如果输入的图片大小不能被32整除,则会导致特征图大小不再符合要求。因为feature map中没有非整数的像素点区域,因此输入图像必须为32(max stride)的整数倍,确保输出的feature map大小为:整数×整数的形式
3.9.批量大小(Batch size)
if RANK == -1 and batch_size == -1: # single-GPU only, estimate best batch size
batch_size = check_train_batch_size(model, imgsz, amp)
loggers.on_params_update({'batch_size': batch_size})#更新超参数或配置文件
- 计算最优化的Batch size,确保其满足要求
3.10.优化器(Optimizer)
nbs = 64 # nominal batch size
accumulate = max(round(nbs / batch_size), 1) # accumulate loss before optimizing
hyp['weight_decay'] *= batch_size * accumulate / nbs # scale weight_decay
optimizer = smart_optimizer(model, opt.optimizer, hyp['lr0'], hyp['momentum'], hyp['weight_decay'])
- nbs:名义上的batch size
- round()函数将nbs/batch_size的结果四舍五入为整数,然后使用max()函数将结果和1取最大值,确保结果不会小于1
- accumulate:储存一个倍数关系,看多少批batch_size才能达到nbs
3.11.学习率调度器(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)
- 采用余弦退火率和线性学习率来衰减学习率
3.12.指数移动平均(EMA)
ema = ModelEMA(model) if RANK in {-1, 0} else None
- EMA:将前面模型训练的w,b以指数加权,离本次训练距离更近的,指数系数越小,所占比重越大。移动平均得到的值使收敛曲线更加平缓光滑,抖动性更小,不会因为某次的异常取值而使得滑动平均值波动很大 。
3.13.断点续训(Resume)
best_fitness, start_epoch = 0.0, 0# 这两个变量分别用来存储模型训练到目前为止在验证集上达到的最佳效果(best_fitness)以及模型开始训练的轮数(start_epoch)
if pretrained:
if resume:#检查是否需要从断点处恢复训练
best_fitness, start_epoch, epochs = smart_resume(ckpt, optimizer, ema, weights, epochs, resume)
del ckpt, csd# 删除不需要的变量释放内存空间
- 断点续训:可以理解为把上次中断结束时的模型,作为新的预训练模型,然后从中获取上次训练时的参数并恢复训练状态。
- smar_resume:从部分训练的节点开始训练,加载训练轮次信息,以及之前训练过程中存储的模型参数、优化器状态、指数滑动平均模型等状态信息,并将其恢复到当前状态中。
3.14.单机多卡模式(DP mode)
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://docs.ultralytics.com/yolov5/tutorials/multi_gpu_training to get started.'
)
model = torch.nn.DataParallel(model)
- DataParallel单机多卡模式自动将数据切分 load 到相应 GPU,将模型复制到相应 GPU,进行正向传播计算梯度并汇总。
- rank为进程编号
3.15.多卡归一化(SyncBatchNorm)
if opt.sync_bn and cuda and RANK != -1:#把不同卡的数据做个同步
model = torch.nn.SyncBatchNorm.convert_sync_batchnorm(model).to(device)
LOGGER.info('Using SyncBatchNorm()')
- torch.nn.SyncBatchNorm.convert_sync_batchnorm(model).to(device):是一个将模型中的BatchNorm层转换为SyncBatchNorm层的函数。这个函数可以帮助我们在使用Distributed-DataParallel时,使得每个进程只有一个GPU的情况下,使用SyncBatchNorm层来进行批量归一化。这个函数会遍历模型中的所有BatchNorm层,并将其转换为SyncBatchNorm层。同时,这个函数还可以将转换后的模型移动到指定的设备上。需要注意的是,这个函数只能在使用单GPU的情况下使用。如果你使用的是多GPU,那么你需要使用torch.nn.SyncBatchNorm来代替torch.nn.BatchNorm。
3.16.训练数据加载(Trainloader)
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,#是否打乱数据集顺序
seed=opt.seed)#随机种子
labels = np.concatenate(dataset.labels, 0)
mlc = int(labels[:, 0].max()) # max label class,存储标签编号的最大值
#训练集标签类别小于类别总数就没问题
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]
if not resume:#没有使用断点续训
if not opt.noautoanchor: #采用自动锚框检查
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 回调函数
# 用于对预训练模型进行必要的调整或者处理
# DDP mode,多GPU训练
if cuda and RANK != -1:
model = smart_DDP(model)
- create_dataloader:得到两个对象train_loader训练数据加载器、 dataset数据集对象
-
np.concatenate(list, axis=0) :将数据进行串接,这里主要是可以将列表进行x轴获得y轴的串接,list表示需要串接的列表,axis=0,表示从上到下进行串接
-
check_anchors:计算默认锚点anchor与数据集标签的长宽比值,标签的长h宽w与anchor的h_a w_a的比值,如果标签框满足条件总数的99%,则根据k-mean算法聚类新的锚点
-
thr=hyp['anchor_t'] :anchor_t当配置文件中的anchor计算bpr(best possible recall)大于0.98才计算新anchor,小于0.98 程序会根据数据集的label自动学习anchor的尺寸
3.17.初始化模型属性(Model attributes)
nl = de_parallel(model).model[-1].nl # number of detection layers (to scale hyps)
hyp['box'] *= 3 / nl # scale to layers,预测框损失
hyp['cls'] *= nc / 80 * 3 / nl # scale to classes and layers 分类损失
hyp['obj'] *= (imgsz / 640) ** 2 * 3 / nl # scale to image size and layers 置信度损失
hyp['label_smoothing'] = opt.label_smoothing
model.nc = nc # attach number of classes to model 数据集中模型数量赋值给模型nc属性
model.hyp = hyp # attach hyperparameters to model 设置模型超参数
model.class_weights = labels_to_class_weights(dataset.labels, nc).to(device) * nc # attach class weights
# 根据数据集的标签计算每个类别的权重,并将其赋值给模型的 class_weights 属性
# 这个属性在训练过程中用于动态调整损失函数中各个类别的权重,从而更加关注重要的类别
model.names = names# 获取类别的名字,然后将分类标签保存到模型
- de_parallel(model) 函数用于将模型转换为可以在多个GPU上并行运行的形式。函数会对模型的参数进行划分,使得不同的部分可以并行地计算,代码通过取 model 的最后一个模块的 nl 属性,获得检测层的数量
- 根据 nl 的值,代码更新了超参数 hyp 中的三个值:box,cls,obj,以及 label_smoothing
-
因为不同层的输出尺寸不同,为了保证超参数在不同层之间的一致性,这里对 hyp 中的三个超参数进行了缩放,使得它们与层数和类别数量成正比
3.18.开始训练
开始训练
t0 = time.time()
nb = len(train_loader) # number of batches
nw = max(round(hyp['warmup_epochs'] * nb), 100) # number of warmup iterations, max(3 epochs, 100 iterations)
# nw = min(nw, (epochs - start_epoch) / 2 * nb) # limit warmup to < 1/2 of training
last_opt_step = -1
#初始化MAP和results
maps = np.zeros(nc) # mAP per class
results = (0, 0, 0, 0, 0, 0, 0) # P, R, mAP@.5, mAP@.5-.95, val_loss(box, obj, cls)
scheduler.last_epoch = start_epoch - 1 # do not move,设置学习率衰减所进行到的伦茨,即使打断训练,使用resume接着训练也能正常衔接之前的训练进行学习率衰减
scaler = torch.cuda.amp.GradScaler(enabled=amp)# 设置amp混合精度训练
stopper, stop = EarlyStopping(patience=opt.patience), False# 连续训练几轮 loss没有下降就会停止,初始化stop为否
compute_loss = ComputeLoss(model) # init loss class
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'
f"Logging results to {colorstr('bold', save_dir)}\n"
f'Starting training for {epochs} epochs...')
for epoch in range(start_epoch, epochs): # epoch ------------------------------------------------------------------
callbacks.run('on_train_epoch_start')
model.train()
更新权重
# Update image weights (optional, single-GPU only)
if opt.image_weights: #获取权重信息
cw = model.class_weights.cpu().numpy() * (1 - maps) ** 2 / nc # class weight 一轮训练后 精度低的类分配一个较高的权重,更容易在random.choices()中被选到
iw = labels_to_image_weights(dataset.labels, nc=nc, class_weights=cw) # image weights,将class weight转换为image weight
dataset.indices = random.choices(range(dataset.n), weights=iw, k=dataset.n) # rand weighted idx
- random.choices():更新图像权重后,利用随机选择的方式从训练集中选取图片进行训练。其中,dataset.n表示训练集中图片的数量,weights参数表示每张图片被选中的权重,k参数表示选取的图片数量。具体来说,权重越大的图片被选中的概率越大,而权重为0的图片则不会被选中。
分布式训练设置以及控制台显示
# Update mosaic border (optional)
# b = int(random.uniform(0.25 * imgsz, 0.75 * imgsz + gs) // gs * gs)
# dataset.mosaic_border = [b - imgsz, -b] # height, width borders
mloss = torch.zeros(3, device=device) # mean losses
if RANK != -1:
train_loader.sampler.set_epoch(epoch)
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}:
pbar = tqdm(pbar, total=nb, bar_format=TQDM_BAR_FORMAT) # progress bar,创建进度条,展示训练信息
optimizer.zero_grad()#梯度清零
- train_loader.sampler.set_epoch(epoch):是在分布式训练中用来设置数据加载器的随机种子的。在每个epoch开始时,设置随机种子可以确保每个进程都使用相同的随机顺序来加载数据,从而保证模型的训练结果一致性。这个操作通常在分布式训练的第一个epoch开始前执行。
小批量加载和热身训练
for i, (imgs, targets, paths, _) in pbar: # batch -------------------------------------------------------------
callbacks.run('on_train_batch_start')
ni = i + nb * epoch # number integrated batches (since train start),训练过的batch个数即迭代次数
imgs = imgs.to(device, non_blocking=True).float() / 255 # uint8 to float32, 0-255 to 0.0-1.0
# Warmup
if ni <= nw:
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)
accumulate = max(1, np.interp(ni, xi, [1, nbs / batch_size]).round())
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
x['lr'] = np.interp(ni, xi, [hyp['warmup_bias_lr'] if j == 0 else 0.0, x['initial_lr'] * lf(epoch)])
if 'momentum' in x:
x['momentum'] = np.interp(ni, xi, [hyp['warmup_momentum'], hyp['momentum']])
- accumulate = max(1, np.interp(ni, xi, [1, nbs / batch_size]).round()):这段代码是用来计算梯度累积的参数的,梯度累积是指在更新模型参数时,将多个小批量数据的梯度累加起来再进行一次参数更新,这样可以减小显存的压力,同时也可以增加模型的稳定性和泛化能力。accumulate的计算方式是根据当前的batch size和总的batch size计算出需要累积多少个小批量数据的梯度,以达到与指定的nominal batch size相同的效果。具体来说,这段代码使用了np.interp函数来进行线性插值,将当前的batch size映射到一个在[1, nbs/batch_size]之间的值,然后再四舍五入取整,得到需要累积的小批量数据的个数。
- x['lr'] = np.interp(ni, xi, [hyp['warmup_bias_lr'] if j == 2 else 0.0, x['initial_lr'] * lf(epoch)]):这行代码是在使用Python中的NumPy库中的interp函数来进行插值计算,用于计算学习率。其中,ni和xi是插值函数的输入和输出,[hyp['warmup_bias_lr'] if j == 2 else 0.0, x['initial_lr'] * lf(epoch)]是插值函数的输出。这个插值函数的作用是根据当前的epoch来计算学习率lr,其中lf(epoch)是一个学习率函数,用于根据epoch来调整学习率大小。具体的学习率计算方式可以参考3.11.节
多尺度训练
# Multi-scale
if opt.multi_scale:
sz = random.randrange(int(imgsz * 0.5), int(imgsz * 1.5) + gs) // gs * gs # size,随机改变图片大小,从imagsz(默认尺寸)*0.5 imgsz*1.5+gs(模型最大stride=32)随机选取尺寸
sf = sz / max(imgs.shape[2:]) # scale factor
if sf != 1:
ns = [math.ceil(x * sf / gs) * gs for x in imgs.shape[2:]] # new shape (stretched to gs-multiple)拉伸到gs的倍数
imgs = nn.functional.interpolate(imgs, size=ns, mode='bilinear', align_corners=False) #线性插值下采样
前向传播、反向传播
# Forward
with torch.cuda.amp.autocast(amp):#混合精度训练
pred = model(imgs) # forward
loss, loss_items = compute_loss(pred, targets.to(device)) # loss scaled by batch_size,其中 loss为总损失 loss_items是一个损失元组,包括分类,objectness,预测框的损失
if RANK != -1:
loss *= WORLD_SIZE # gradient averaged between devices in DDP mode
if opt.quad:
loss *= 4.# 如果采用collate-Fn4取出的mosaic4数据loss也要翻倍*4
# Backward
scaler.scale(loss).backward()# scale为使用自动混合精度运算
优化器
# Optimize - https://pytorch.org/docs/master/notes/amp_examples.html
if ni - last_opt_step >= accumulate:#累积到一定的次数更新一次权重
scaler.unscale_(optimizer) # unscale gradients 参数更新 首先把梯度值unscale返回 如果梯度不是infs/NaNs调用optimizer.step更新权重 否则不更新权重
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=10.0) # clip gradients
scaler.step(optimizer) # optimizer.step 参数更新 首先把梯度值unscale返回 如果梯度不是infs/NaNs调用optimizer.step更新权重 否则不更新权重
scaler.update() #更新参数
optimizer.zero_grad() #梯度清零
if ema:
ema.update(model) #将计算出来的weight加到model中
last_opt_step = ni #更新轮数
- torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=10.0):是一个用于梯度裁剪的函数。它将模型的梯度进行归一化,以防止梯度爆炸的问题。其中,model.parameters()是一个基于变量的迭代器,max_norm是梯度的最大范数,超过这个范数的梯度将被裁剪。这个函数会返回被裁剪后的梯度范数,如果梯度范数小于等于max_norm,则不进行裁剪。
相关信息打印
# Log
if RANK in {-1, 0}:
mloss = (mloss * i + loss_items) / (i + 1) # update mean losses
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])) #打印相关信息
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,取出学习率,方便打印
scheduler.step()# 根据前面设置的学习率更新策略更新学习率
计算mAP、保存模型、更新模型
if RANK in {-1, 0}:
# mAP
callbacks.run('on_train_epoch_end', epoch=epoch)
ema.update_attr(model, include=['yaml', 'nc', 'hyp', 'names', 'stride', 'class_weights']) #将model属性给ema
final_epoch = (epoch + 1 == epochs) or stopper.possible_stop #判断是否为最后一轮或提前停止训练
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, #使用ema模型
single_cls=single_cls,
dataloader=val_loader,
save_dir=save_dir,
plots=False,
callbacks=callbacks,
compute_loss=compute_loss)
# Update best mAP
fi = fitness(np.array(results).reshape(1, -1)) # weighted combination of [P, R, mAP@.5, mAP@.5-.95],fi: [P, R, mAP@.5, mAP@.5-.95]的一个加权值 = 0.1*mAP@.5 + 0.9*mAP@.5-.95
stop = stopper(epoch=epoch, fitness=fi) # early stop check
if fi > best_fitness:
best_fitness = fi #更新
log_vals = list(mloss) + list(results) + lr #保存验证结果
callbacks.run('on_fit_epoch_end', log_vals, epoch, best_fitness, fi)
# Save model
if (not nosave) or (final_epoch and not evolve): # if save
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 repo
'date': datetime.now().isoformat()}
# Save last, best and delete
torch.save(ckpt, last) #保存每轮训练结束后的模型,将ckpt保存到last.pt
if best_fitness == fi:
torch.save(ckpt, best) #保存最佳模型
if opt.save_period > 0 and epoch % opt.save_period == 0:
torch.save(ckpt, w / f'epoch{epoch}.pt')
del ckpt #保存完删除ckpt,释放内存
callbacks.run('on_model_save', last, epoch, final_epoch, best_fitness, fi) #保存到日志
EMA:普通的参数权重相当于一直累积更新整个训练过程的梯度,使用EMA的参数权重相当于使用训练过程梯度的加权平均(刚开始的梯度权值很小)。由于刚开始训练不稳定,得到的梯度给更小的权值更为合理,所以EMA会有效。
- AP:average precision 单个类别的平均精度值
- mAP:所有类别的平均精度
- mAP@0.5: mean Average Precision(IoU=0.5),即将IoU设为0.5时,计算每一类的所有图片的AP,然后所有类别求平均,即mAP。
- mAP@.5:.95(mAP@[.5:.95]),表示在不同IoU阈值(从0.5到0.95,步长0.05)(0.5、0.55、0.6、0.65、0.7、0.75、0.8、0.85、0.9、0.95)上的平均mAP
停止分布式训练
# EarlyStopping
if RANK != -1: # if DDP training,使用分布式训练
broadcast_list = [stop if RANK == 0 else None]
dist.broadcast_object_list(broadcast_list, 0) # broadcast 'stop' to all ranks
if RANK != 0:
stop = broadcast_list[0]
if stop:
break # must break all DDP ranks
# end epoch ----------------------------------------------------------------------------------------------------
# end training -----------------------------------------------------------------------------------------------------
- 这段代码是使用分布式训练时的广播操作,其中RANK表示当前进程的排名,-1表示不使用分布式训练。如果当前进程的排名不是0,则会等待进程0广播一个stop信号,如果收到了stop信号,则会跳出循环。这个操作的目的是确保所有进程都能够同步地停止训练。
打印信息、释放显存
if RANK in {-1, 0}:
LOGGER.info(f'\n{epoch - start_epoch + 1} epochs completed in {(time.time() - t0) / 3600:.3f} hours.')#停止训练,向控制台输出信息
for f in last, best:
if f.exists():
strip_optimizer(f) # strip optimizers #在训练完模型后,从保存的模型文件中删除optimizer、training_results、updates等变量,以优化模型大小,减少存储空间和传输时间。
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:
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() #释放显存
return results
4.解析命令行参数
def parse_opt(known=False):
parser = argparse.ArgumentParser()
parser.add_argument('--weights', type=str, default= 'yolov5s.pt', help='initial weights path')#预训练模型
parser.add_argument('--cfg', type=str, default='own_data/yolov5s.yaml', help='model.yaml path')#模型配置文件 网络结构
parser.add_argument('--data', type=str, default= 'own_data/data.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=10, help='total training epochs')#训练轮数
parser.add_argument('--batch-size', type=int, default=16, help='total batch size for all GPUs, -1 for autobatch')# 每批次的输入数据量;default=-1将时自动调节batchsize大小
parser.add_argument('--imgsz', '--img', '--img-size', type=int, default=640, help='train, val image size (pixels)')#设置图片大小,这个参数在你选择yolov5l那些大一点的权重的时候,要进行适当的调整,这样才能达到好的效果
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')#是否只保存last.pt不保存best.pt
parser.add_argument('--noval', action='store_true', help='only validate final epoch')#是否只在最后一轮测试;正常情况下每个epoch都会计算mAP
parser.add_argument('--noautoanchor', action='store_true', help='disable AutoAnchor')#不自动调整anchor
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')# evolve: 参数进化, 遗传算法调参,一般不使用
parser.add_argument('--bucket', type=str, default='', help='gsutil bucket')# 谷歌云盘的相关项,一般不会用到
parser.add_argument('--cache', type=str, nargs='?', const='ram', help='image --cache ram/disk')#是否提前缓存图片到内存,以加快训练速度,默认False
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%%')# 是否启用多尺度训练,默认是不开启的;多尺度训练是指设置几种不同的图片输入尺度,训练时每隔一定iterations随机选取一种尺度训练,这样训练出来的模型鲁棒性更强
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')# 是否开启跨卡同步BN;开启参数后即可使用 SyncBatchNorm多 GPU 进行分布式训练,仅在DDP(分布式训练)模式下有效
parser.add_argument('--workers', type=int, default=0, help='max dataloader workers (per RANK in DDP mode)') # dataloader的最大worker数量 (使用多线程加载图片)
parser.add_argument('--project', default=ROOT / 'runs/train', help='save to project/name')# 指定训练好的模型的保存路径;默认在runs / train
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')#四元数据加载器,比默认 640 大的数据集上训练效果更好,在 640 大小的数据集上训练效果可能会差一些
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')# 标签平滑 / 默认不增强, 用户可以根据自己标签的实际情况设置这个参数,建议设置小一点 0.1 / 0.05
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)')#多少个epoch保存一下checkpoint
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')# DDP模式,单机多卡训练,单GPU设备rank为-1
# Logger arguments 可视化工具
parser.add_argument('--entity', default=None, help='Entity')#这是一个Python脚本中的命令行参数,用于设置W&B(Weights & Biases)的实体。W&B是一个机器学习实验平台,可以帮助用户跟踪和可视化模型的训练过程。
parser.add_argument('--upload_dataset', nargs='?', const=True, default=False, help='Upload data, "val" option')# 是否上传dataset到wandb tabel(将数据集作为交互式 dsviz表 在浏览器中查看、查询、筛选和分析数据集) 默认False
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()
'''这段代码是一个函数,它的作用是解析命令行参数并返回解析结果。如果命令行参数已知,则使用 parse_known_args() 方法解析参数并返回第一个元素;
否则使用 parse_args() 方法解析参数并返回结果。其中,parser 是一个 ArgumentParser 对象,可以通过该对象的 add_argument() 方法来添加需要的参数。'''
5.main函数
5.1.检查环境
# Checks
if RANK in {-1, 0}:
print_args(vars(opt))#打印参数
check_git_status()#检查github库是否更新
check_requirements(ROOT / 'requirements.txt')#检查环境,工具包等是否安装好
5.2.判断是否断点续训
# Resume (from specified or most recent last.pt)
if opt.resume and not check_comet_resume(opt) and not opt.evolve:
last = Path(check_file(opt.resume) if isinstance(opt.resume, str) else get_latest_run())#如果给定就参数就接着参数训练,否则就取last.py
opt_yaml = last.parent.parent / 'opt.yaml' # train options yaml
opt_data = opt.data # original dataset
if opt_yaml.is_file():
with open(opt_yaml, errors='ignore') as f:
d = yaml.safe_load(f)
else:
d = torch.load(last, map_location='cpu')['opt']
opt = argparse.Namespace(**d) # replace # 超参数替换,将训练时的命令行参数加载到opt参数对象中
opt.cfg, opt.weights, opt.resume = '', str(last), True # reinstate
if is_url(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))
5.3.分布式并行训练(单机多卡)
# DDP mode
device = select_device(opt.device, batch_size=opt.batch_size)#选择设备,不给值自动挑选
if LOCAL_RANK != -1:#采用分布式训练
msg = 'is not compatible with YOLOv5 Multi-GPU DDP training' #与YOLOv5多GPU DDP训练不兼容
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'#批量大小无效,需使用autobatch
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')
- dist.init_process_group是PyTorch中用于初始化分布式训练的函数。它的作用是初始化进程组,使得多个进程可以进行通信和同步。其中,参数backend指定通信所用的后端,可以是nccl或者是gloo或者是mpi。nccl用于GPU,gloo用于CPU。而在只设置os.environ[‘CUDA_VISIBLE_DEVICES’] = str(rank)时,是为了指定当前进程可见的GPU设备。这样可以避免多个进程同时使用同一块GPU的情况,从而保证分布式训练的正确性和效率。
5.4.开始训练
# Train
if not opt.evolve: #是否进行超参数进化
train(opt.hyp, opt, device, callbacks) #直接调用train()函数
# Evolve hyperparameters (optional)
else:
# Hyperparameter evolution metadata (mutation scale 0-1, lower_limit, upper_limit),超参数进化元数据(变异范围0-1,下限,上限)
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
if 'anchors' not in hyp: # anchors commented in hyp.yaml anchors不在超参数中
hyp['anchors'] = 3
if opt.noautoanchor: #不使用autoanchor
del hyp['anchors'], meta['anchors']
opt.noval, opt.nosave, save_dir = True, True, Path(opt.save_dir) # only val/save final epoch# 使用遗传算法时,仅在最后的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:
# download evolve.csv if exists
subprocess.run([
'gsutil',
'cp',
f'gs://{opt.bucket}/evolve.csv',
str(evolve_csv), ])
for _ in range(opt.evolve): # generations to evolve,default=300
if evolve_csv.exists(): # if evolve.csv exists: select best hyps and mutate
# Select parent(s)
parent = 'single' # parent selection method: 'single' or 'weighted' 选择进化方式 有: 'single' or 'weighted'
x = np.loadtxt(evolve_csv, ndmin=2, delimiter=',', skiprows=1)#加载evolve.txt
n = min(5, len(x)) # number of previous results to consider,最多考虑前五的进化结果,假设n=5
x = x[np.argsort(-fitness(x))][:n] # top n mutations#选取前五个
w = fitness(x) - fitness(x).min() + 1E-6 # weights (sum > 0)
#fitness(x)表示对x进行适应度计算,fitness(x).min()表示所有x的适应度中的最小值。这个公式的作用是将适应度值进行归一化,使得最小的适应度值变为1E-6,从而避免出现0的情况。
if parent == 'single' or len(x) == 1:# 根据不同进化方式获得base hyp
# x = x[random.randint(0, n - 1)] # random selection
x = x[random.choices(range(n), weights=w)[0]] # weighted selection# 根据权重的几率随机挑选适应历史前5的其中一个
elif parent == 'weighted':
x = (x * w.reshape(n, 1)).sum(0) / w.sum() # weighted combination# 对hyp乘上对应的权重融合成一个hpy 再取平均 /权重和
# 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,获取突变初始值(meta三个值中的第一个) meta=[变异初始概率,最低限制,最大限制]
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)将突变添加到base hyp上
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)# 将结果写入到results中,并将byp写入到evolve.txt(每一行为一次进化结果
# 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}')
6.run函数
def run(**kwargs):
# Usage: import train; train.run(data='coco128.yaml', imgsz=320, weights='yolov5m.pt')
opt = parse_opt(True)
for k, v in kwargs.items():
setattr(opt, k, v)# 属性赋值,属性不存在则创建一个赋值
main(opt)
return opt
7.程序入口
if __name__ == '__main__':
opt = parse_opt()
main(opt)