【目标检测】YOLOv5算法实现(七):模型训练

  本系列文章记录本人硕士阶段YOLO系列目标检测算法自学及其代码实现的过程。其中算法具体实现借鉴于ultralytics YOLO源码Github,删减了源码中部分内容,满足个人科研需求。
  本系列文章主要以YOLOv5为例完成算法的实现,后续修改、增加相关模块即可实现其他版本的YOLO算法。

文章地址:
YOLOv5算法实现(一):算法框架概述
YOLOv5算法实现(二):模型加载
YOLOv5算法实现(三):数据集加载
YOLOv5算法实现(四):损失计算
YOLOv5算法实现(五):预测结果后处理
YOLOv5算法实现(六):评价指标及实现
YOLOv5算法实现(七):模型训练
YOLOv5算法实现(八):模型验证
YOLOv5算法实现(九):模型预测(编辑中…)

本文目录

  • 1 引言
  • 2 超参数文件
  • 3 模型训练(train.py)

1 引言

  本篇文章综合之前文章中的功能,实现模型的训练。模型训练的逻辑如图1所示。
在这里插入图片描述

图1 模型训练流程

2 超参数文件

  YOLOv5中超参数主要包括学习率、优化器、置信度以及数据增强,源码中某一超参数文件及各参数含义如下所示:

# YOLOv5 🚀 by Ultralytics, GPL-3.0 license
# Hyperparameters for VOC training# 学习率
lr0: 0.00334  # 初始学习率
lrf: 0.15135  # 最终学习率下降比例(lr0 * lrf)
# 优化器(SGD、Adam、AdamW)
momentum: 0.74832 # SGD momentum/Adam beta1
weight_decay: 0.00025 # optimizer weight decay 5e-4 ,权重衰变系数(防止过拟合)
# 热身训练(Warmup)
warmup_epochs: 3.3835 # 学习率热身epoch
warmup_momentum: 0.59462 # 学习率热身初始动量
warmup_bias_lr: 0.18657 # 学习率热身偏置学习率
# 损失增益
box: 0.02 # box loss gain
cls: 0.21638 # cls loss gain
cls_pw: 0.5 # cls BCELoss positive_weight
obj: 0.51728 # obj loss gain (scale with pixels)
obj_pw: 0.67198 # obj BCELoss positive_weight
fl_gamma: 0.0 # focal loss gamma (efficientDet default gamma=1.5)
# 正样本匹配阈值
iou_t: 0.2 # IoU training threshold
anchor_t: 3.3744 # anchor-multiple threshold
# 数据增强
## HSV色彩空间增强
hsv_h: 0.01041
hsv_s: 0.54703
hsv_v: 0.27739
## 仿射变换
degrees: 0.0 #图像旋转
translate: 0.04591 #图像平移
scale: 0.75544 #图像仿射变换的缩放比例
shear: 0.0 #设置裁剪的仿射矩阵系数

3 模型训练(train.py)

def train(hyp):# ----------------------------------------------------------------------------------# hyp超参数解析# ----------------------------------------------------------------------------------# 训练设备device = torch.device(opt.device if torch.cuda.is_available() else "cpu")print("Using {} device training.".format(device.type))# 权重保存文件wdir = "weights" + os.sep  # weights dir# 损失和学习率记录txt, 损失和学习率变化曲线, 权重文件保存路径results_file, save_file_path, save_path \= opt.results_file, opt.save_file_path, opt.save_path# 模型结构文件cfg = opt.yaml# data文件路径, 其中存储了训练和验证数据集的路径data = opt.data# 训练批次epochs = opt.epochsbatch_size = opt.batch_size# 初始化权重路径weights = opt.weights  # initial training weights# 训练和测试的图片大小imgsz_train = opt.img_sizeimgsz_test = opt.img_size# 图像要设置成32的倍数gs = 32  # (pixels) grid sizeassert math.fmod(imgsz_test, gs) == 0, "--img-size %g must be a %g-multiple" % (imgsz_test, gs)# ----------------------------------------------------------------------------------# ----------------------------------------------------------------------------------# 数据字典 "classes": 类别数, "train":训练数据集路径 "valid":验证数据集路径 "names": 类别名data_dict = parse_data_cfg(data)train_path = data_dict['train']val_path = data_dict['valid']# 类别数nc = opt.nc# Remove previous resultsfor f in glob.glob(results_file):os.remove(f)# ----------------------------------------------------------------------------------# 模型初始化(加载预训练权重)# ----------------------------------------------------------------------------------# 初始化模型model = Model(cfg=cfg, ch=3, nc=nc).to(device)# 开始训练的epochstart_epoch = 0best_map = 0pretrain = Falseif weights.endswith(".pt") or weights.endswith(".pth"):pretrain = Trueckpt = torch.load(weights, map_location=device)# load modeltry:ckpt["model"] = {k: v for k, v in ckpt["model"].items() if model.state_dict()[k].numel() == v.numel()}model.load_state_dict(ckpt["model"], strict=False)except KeyError as e:s = "%s is not compatible with %s. Specify --weights '' or specify a --cfg compatible with %s. " \"See https://github.com/ultralytics/yolov3/issues/657" % (opt.weights, opt.cfg, opt.weights)raise KeyError(s) from e# load resultsif ckpt.get("training_results") is not None:with open(results_file, "w") as file:file.write(ckpt["training_results"])  # write results.txt# 加载最好的mapif "best_map" in ckpt.keys():best_map = ckpt['best_map']# epochsstart_epoch = ckpt["epoch"] + 1if epochs < start_epoch:print('%s has been trained for %g epochs. Fine-tuning for %g additional epochs.' %(opt.weights, ckpt['epoch'], epochs))epochs += ckpt['epoch']  # finetune additional epochsdel ckpthyp['lr0'] = hyp['lr0'] * hyp['lrf']print(colorstr('Pretrain') + ': Successful load pretrained weights.')# ----------------------------------------------------------------------------------# ----------------------------------------------------------------------------------# ----------------------------------------------------------------------------------# 定义优化器和学习率策略# ----------------------------------------------------------------------------------nbs = 64  # 训练多少图片进行一次反向传播accumulate = max(round(nbs / batch_size), 1)  # accumulate n times before optimizer update (bs 64)hyp['weight_decay'] *= batch_size * accumulate / nbs  # scale weight_decay# 定义优化器optimizer = smart_optimizer(model, opt.optimizer, hyp['lr0'], hyp['momentum'], hyp['weight_decay'])# Schedulerif 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']  # linearscheduler = lr_scheduler.LambdaLR(optimizer, lr_lambda=lf)scheduler.last_epoch = start_epoch  # 指定从哪个epoch开始# ----------------------------------------------------------------------------------# ----------------------------------------------------------------------------------# ----------------------------------------------------------------------------------# 加载数据集# ----------------------------------------------------------------------------------train_dataset = LoadImagesAndLabels(train_path, imgsz_train, batch_size,augment=True,hyp=hyp,  # augmentation hyperparametersrect=False,  # rectangular trainingcache_images=opt.cache_images,)# dataloadernum_workers = 0  # number of workerstrain_dataloader = torch.utils.data.DataLoader(train_dataset,batch_size=batch_size,num_workers=num_workers,# Shuffle=True unless rectangular training is usedshuffle=not opt.rect,pin_memory=True,collate_fn=train_dataset.collate_fn)# 当需要根据map保存最好的权值时, 加载验证数据集val_dataset = LoadImagesAndLabels(val_path, imgsz_test, batch_size,hyp=hyp,rect=True,cache_images=opt.cache_images)val_dataloader = torch.utils.data.DataLoader(val_dataset,batch_size=batch_size,num_workers=num_workers,pin_memory=True,collate_fn=val_dataset.collate_fn)# train_dataset.labels (1203, ) 1203张图片中的所有标签(nt,5)# np.concatenate 在维度0上对所有标签进行拼接 (1880, 5)labels = np.concatenate(train_dataset.labels, 0)mlc = int(labels[:, 0].max())  # 最大的类别标签assert mlc < nc, f'Label class {mlc} exceeds nc={nc} in {data}. Possible class labels are 0-{nc - 1}'# ----------------------------------------------------------------------------------# ----------------------------------------------------------------------------------# ----------------------------------------------------------------------------------# 模型参数定义# ----------------------------------------------------------------------------------nl = model.model[-1].nl  # 输出特征图数量hyp['box'] *= 3 / nl  # scale to layershyp['cls'] *= nc / 80 * 3 / nl  # scale to classes and layershyp['obj'] *= (imgsz_test / 640) ** 2 * 3 / nl  # scale to image size and layershyp['label_smoothing'] = opt.label_smoothingmodel.nc = nc  # attach number of classes to modelmodel.hyp = hyp  # attach hyperparameters to model# ----------------------------------------------------------------------------------# ----------------------------------------------------------------------------------compute_loss = ComputeLoss(model)# ----------------------------------------------------------------------------------# 开始训练# ----------------------------------------------------------------------------------nb = len(train_dataloader)  # batches的数量, dataloader已经将data安装batch_size进行了打包nw = max(round(hyp['warmup_epochs'] * nb), 100)  # 热身训练的迭代次数, max(3 epochs, 100 iterations)last_opt_step = -1  # 最后一次更新参数的步数train_loss, train_box_loss, train_obj_loss, train_cls_loss = [], [], [], []learning_rate = []if opt.savebest:best_map = ComputeAP(model, val_dataloader, device=device)print(f"{colorstr('Initialize best_map')}: mAP@0.50 = {best_map: .5f}")print(f'Image sizes {imgsz_train} train, {imgsz_test} val\n'f'Using {num_workers} dataloader workers\n'f'Starting training for {epochs} epochs...')for epoch in range(start_epoch, epochs):model.train()# 训练过程中的信息打印metric_logger = utils.MetricLogger(delimiter="  ")metric_logger.add_meter('lr', utils.SmoothedValue(window_size=1, fmt='{value:.6f}'))header = 'Epoch: [{}]'.format(epoch)# 当前训练批次的平均损失mloss = torch.zeros(4, device=device)now_lr = 0.optimizer.zero_grad()# imgs: [batch_size, 3, img_size, img_size]# targets: [num_obj, 6] , that number 6 means -> (img_index, obj_index, x, y, w, h)# 其中(x, y, w, h)绝对作了缩放处理后的相对坐标# paths: list of img path(文件路径)for i, (imgs, targets, paths, _, _) in enumerate(metric_logger.log_every(train_dataloader, 50, header)):ni = i + nb * epoch  # number integrated batches (since train start)imgs = imgs.to(device).float() / 255.0  # 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 lr0x['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']])# ----------------------------------------------------------------------------------# ----------------------------------------------------------------------------------# 前向传播pred = model(imgs)# 损失计算loss_dict = compute_loss(pred, targets.to(device))losses = sum(loss for loss in loss_dict.values())loss_items = torch.cat((loss_dict["box_loss"],loss_dict["obj_loss"],loss_dict["class_loss"],losses)).detach()mloss = (mloss * i + loss_items) / (i + 1)  # update mean losses# 反向传播losses *= batch_sizelosses.backward()# 每训练accumulate次进行参数更新if ni - last_opt_step >= accumulate:optimizer.step()optimizer.zero_grad()metric_logger.update(loss=losses, **loss_dict)now_lr = optimizer.param_groups[0]["lr"]metric_logger.update(lr=now_lr)# end batch ----------------------------------------------------------------------scheduler.step()train_loss.append(mloss.tolist()[-1])learning_rate.append(now_lr)result_mAP = ComputeAP(model, val_dataloader, device=device)voc_mAP = result_mAP[1]  # @0.50# write into txtwith open(results_file, "a") as f:# box_loss, obj_clss, cls_loss, train_loss, lrresult_info = [str(round(i, 4)) for i in [mloss.tolist()[0]]] + \[str(round(i, 4)) for i in [mloss.tolist()[1]]] + \[str(round(i, 4)) for i in [mloss.tolist()[2]]] + \[str(round(i, 4)) for i in [mloss.tolist()[-1]]] + \[str(round(now_lr, 6))] + [str(round(voc_mAP, 6))]txt = "epoch:{} {}".format(epoch, '  '.join(result_info))f.write(txt + "\n")if opt.savebest:if voc_mAP > best_map:print(f"{colorstr('Save best_map Weight')}: update mAP@0.50 from {best_map: .5f} to {voc_mAP: .5f}")best_map = voc_mAPwith open(results_file, 'r') as f:save_files = {'model': model.state_dict(),'optimizer': optimizer.state_dict(),'training_results': f.read(),'epoch': epoch,'best_map': best_map}torch.save(save_files, save_path.format('best_map'))else:if (epoch + 1) % 20 == 0 or epoch == epochs - 1:with open(results_file, 'r') as f:save_files = {'model': model.state_dict(),'training_results': f.read(),'epoch': epoch}torch.save(save_files, save_path.format(epoch))if __name__ == '__main__':parser = argparse.ArgumentParser()# -----------------------------------------file = "yolov5s"weight_file = f"weights/{file}"  # 权重存储文件result_file = f'results/{file}'  # 训练损失、mAP等保存文件if not os.path.exists(weight_file):os.makedirs(weight_file)if not os.path.exists(result_file):os.makedirs(result_file)# -----------------------------------------parser.add_argument('--epochs', type=int, default=300)parser.add_argument('--batch-size', type=int, default=4)parser.add_argument('--nc', type=int, default=3)yaml_path = f'cfg/models/{file}.yaml'parser.add_argument('--yaml', type=str, default=yaml_path, help="model.yaml path")parser.add_argument('--data', type=str, default='data/my_data.data', help='*.data path')parser.add_argument('--hyp', type=str, default='cfg/hyps/hyp.scratch-med.yaml', help='hyperparameters path')parser.add_argument('--optimizer', type=str, choices=['SGD', 'Adam', 'AdamW'], default='SGD', help='optimizer')parser.add_argument('--cos-lr', type=bool, default=True, help='cosine LR scheduler')parser.add_argument('--label-smoothing', type=float, default=0.0, help='Label smoothing epsilon')parser.add_argument('--img-size', type=int, default=640, help='test size')parser.add_argument('--rect', action='store_true', help='rectangular training')parser.add_argument('--savebest', type=bool, default=False, help='only save best checkpoint')# 当内存足够时, 设置为True, 将数据集加载到内存中, 在训练时不用从磁盘中读取图片可以加快训练速度parser.add_argument('--cache-images', default=False, help='cache images for faster training')# 预训练权重weight = f'weights/{file}/{file}.pt'parser.add_argument('--weights', type=str, default=weight if os.path.exists(weight) else "", help='initial weights path')parser.add_argument('--device', default='cuda:0', help='device id (i.e. 0 or 0,1 or cpu)')# 结果保存路径time = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")results_file = f"./results/{file}/results{time}.txt"parser.add_argument("--results_file", default=results_file, help="save results files")save_file_path = f'./results/{file}/loss_and_lr{time}.png'parser.add_argument('--save_file_path', default=save_file_path, help="save loss and lr fig")save_path = f"./weights/{file}/{file}-" + "{}.pt"parser.add_argument('--save_path', default=save_path, help="weight save path")opt = parser.parse_args()# 检查文件是否存在opt.cfg = check_file(opt.yaml)opt.data = check_file(opt.data)opt.hyp = check_file(opt.hyp)print(opt)with open(opt.hyp) as f:hyp = yaml.load(f, Loader=yaml.FullLoader)train(hyp)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/617312.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2023一带一路暨金砖国家技能发展与技术创新大赛“网络安全”赛项省选拔赛样题卷①

2023金砖国家职业技能竞赛"网络安全" 赛项省赛选拔赛样题 2023金砖国家职业技能竞赛 省赛选拔赛样题第一阶段&#xff1a;职业素养与理论技能项目1. 职业素养项目2. 网络安全项目3. 安全运营 第二阶段&#xff1a;安全运营项目1. 操作系统安全配置与加固任务一Linux …

【Oracle】Oracle编程PLSQL

Oracle编程 一、PL/SQL 1、PL/SQL概述 PL/SQL&#xff08;Procedure Language/SQL&#xff09;是 Oracle 对 sql 语言的过程化扩展&#xff0c;使 SQL 语言具有过程处理能力。 基本语法结构 [declare -- 声明变量 ]begin-- 代码逻辑 [exception-- 异常处理 ]end;2、变量 …

centos7下升级openssh9.4p1及openssl1.1.1v版本

背景&#xff1a;客户服务器扫描出一些漏洞&#xff0c;发现和版本有关&#xff0c;漏洞最高的版本是9.3p2&#xff0c;所以我们安装一个openssh9.4p1版本及openssl1.1.1v版本 虽然我们进行了镜像备份&#xff0c;为了安全先安装telnet以防止升级失败无法通过ssh连接服务器 一…

【会议征稿通知】第二届数字化经济与管理科学国际学术会议(CDEMS 2024)

第二届数字化经济与管理科学国际学术会议&#xff08;CDEMS 2024&#xff09; 2024 2nd International Conference on Digital Economy and Management Science&#xff08;CDEMS 2024&#xff09; 2024年第二届数字经济与管理科学国际会议(CDEMS 2024) 定于2023年4月26-28日…

如何使用统计鸟网站统计分析网站流量来源?

统计鸟官网地址&#xff1a;https://www.tongjiniao.com/ 站长必备&#xff01;网站数据统计&#xff0c;流量监测平台 提供网站数据统计分析、搜索关键词、流量访问来源等服务 深入分析用户点击习惯&#xff0c;为智能化运营网站提供更好的用户体验 目录 一、注册账号信息 二…

基于博弈树的开源五子棋AI教程[3 极大极小搜索]

基于博弈树的开源五子棋AI教程[3 极大极小搜索] 引子极大极小搜索原理alpha-beta剪枝负极大搜索尾记 引子 极大极小搜索是博弈树搜索中最常用的算法&#xff0c;广泛应用于各类零和游戏中&#xff0c;例如象棋&#xff0c;围棋等棋类游戏。算法思想也是合乎人类的思考逻辑的&a…

Flask+ Dependency-injecter+pytest 写测试类

最近在使用这几个在做项目&#xff0c;因为第一次用这个&#xff0c;所以不免有些问题。总结下踩的坑 1.测试类位置 首先测试类约定会放在tests里面&#xff0c;不然有可能发生引入包的问题&#xff0c;会报错某些包找不到。 2. 测试类依赖注入 这里我就用的真实的数据库操作…

Js--数组(三)

1.什么是数组&#xff1f; 数组&#xff1a;(Array)是一种可以按顺序保存数据的数据类型 2.为什么要数组&#xff1f; 思考&#xff1a;如果我想保存一个班里所有同学的姓名怎么办&#xff1f; 场景&#xff1a;如果有多个数据可以用数组保存起来&#xff0c;然后放到一个变量…

【AI视野·今日CV 计算机视觉论文速览 第285期】Mon, 8 Jan 2024

AI视野今日CS.CV 计算机视觉论文速览 Mon, 8 Jan 2024 Totally 66 papers &#x1f449;上期速览✈更多精彩请移步主页 Daily Computer Vision Papers Denoising Vision Transformers Authors Jiawei Yang, Katie Z Luo, Jiefeng Li, Kilian Q Weinberger, Yonglong Tian, Yue…

【漏洞复现】Apache Tomcat AJP文件包含漏洞(CVE-2020-1938)

Nx01 产品简介 Apache Tomcat 是一个免费的开源 Web 应用服务器&#xff0c;在中小型企业和个人开发用户中有着广泛的应用。 Nx02 漏洞描述 默认情况下&#xff0c;Apache Tomcat会开启AJP连接器&#xff0c;由于AJP服务&#xff08;8009端口&#xff09;存在文件包含缺陷&…

打造高性价比小程序,轻松降低成本

随着移动互联网的普及&#xff0c;小程序已经成为一个热门的应用开发方向。然而&#xff0c;对于许多企业和个人而言&#xff0c;制作一个小程序的费用却让人望而却步。那么&#xff0c;如何以最低的成本制作一款高性价比的小程序呢&#xff1f; 答案很简单&#xff0c;只需要找…

Spark SQL基础

SparkSQL基本介绍 什么是Spark SQL Spark SQL是Spark多种组件中其中一个,主要是用于处理大规模的结构化数据 什么是结构化数据: 一份数据, 每一行都有固定的列, 每一列的类型都是一致的 我们将这样的数据称为结构化的数据 例如: mysql的表数据 1 张三 20 2 李四 15 3 王五 1…

支付宝异步验签踩的坑

最近公司要做支付宝小程序 我作为服务端就要给小程序配置下单啊&#xff0c;异步回调同步支付状态等功能 就不可避免的使用到了支付宝异步验签 首先背景是我是PHP语言&#xff0c;然后验签方式是RSA2 一开始写原生验签方法&#xff0c;验签失败&#xff0c;后面又搞sdk 验签…

Java进阶十—JDBC

Java进阶十—JDBC 一.说明 用Java语言操作Mysql&#xff0c;首先需要学习Mysql MySQL入门教程-CSDN博客 二.JDBC的由来以及定义 JDBC是什么&#xff1f; Java数据库连接(Java Database Connectivity)简称JDBCJDBC是Java操作各数据库的一种规范&#xff0c;是Java语言中用来…

ChatGPT:人工智能与人类交流的桥梁

在人工智能的浪潮中&#xff0c;ChatGPT以其独特的交流能力成为了一个亮点。作为一个基于强大的GPT-4模型的聊天机器人&#xff0c;ChatGPT不仅仅是技术的展示&#xff0c;它更是人工智能与人类交流的桥梁。 人工智能的语言理解革命 ChatGPT的出现标志着人工智能在语言理解和…

攻防实战-手把手带你打穿内网

六朝何事&#xff0c;只成门户私计&#xff01; 目录 环境配置 网络配置 本次实战绘制出来的网络拓扑图如下&#xff1a; 第一层&#xff1a;12server-web1 信息搜集 网站url&#xff1a; 目录扫描 扫到后台地址&#xff1a; 发现有注册功能&#xff0c; 先注册一下尝试能…

预约上门按摩系统目前面临的挑战有哪些

按摩预约上门服务系统上线之后在运营的过程中主要面临的挑战主要有以下几个方面&#xff1a; 1.技师管理和培训&#xff1a;为了保证服务的质量&#xff0c;需要对技师进行管理和培训。这包括确保技师具备必要的技能和资格&#xff0c;以及提供必要的培训&#xff0c;以确保他们…

[金融支付]EMV是什么?

文章目录 EMVCoEMVCo是谁&#xff1f;EMVCo是做什么的&#xff1f;EMVCo是如何运作的&#xff1f;EMVCo 是否强制要求 EMV 规范&#xff1f; EMVEMV的历史背景EMV技术的一些关键点 EMV TechnologiesEMV 认证EMV的三层认证 EMV规范在全球各地存在差异参考 EMVCo EMVCo是谁&…

系列二、Spring Security中的核心类

一、Spring Security中的核心类 1.1、自动配置类 UserDetailsServiceAutoConfiguration 1.2、密码加密器 1.2.1、概述 Spring Security 提供了多种密码加密方案&#xff0c;官方推荐使用 BCryptPasswordEncoder&#xff0c;BCryptPasswordEncoder 使用 BCrypt 强哈希函数&a…

《BackTrader量化交易图解》第10章:Trade 交易操作

文章目录 10 Trade 交易操作10.1 量化回测分析流程10.2 Cerebro 类模块10.3 案例&#xff1a;Trade 交易10.4 实盘交易机器隐性规则10.5 Stake 交易数额和 Trade 交易执行价格 10 Trade 交易操作 10.1 量化回测分析流程 从本章开始讲解 BackTrader 的实盘操作。前面的章节讲过…