看不懂的代码可以复制进讯飞星火问问AI。以下是逐语句调试得出的执行顺序。
首先在根目录新建一个py文件,能够训练数据。
from ultralytics import YOLO
from ultralytics.utils import DEFAULT_CFG
from datetime import datetime
controller=1
def traindata():
current_time = datetime.now()
time_str = current_time.strftime("%Y-%m-%d_%H-%M-%S") # 个人习惯, 用训练时间命名保存路径, 或者你自己自定义
info=r'_only_yolov8'
DEFAULT_CFG.save_dir = f"./Vincent/{time_str+info}"
# 加载模型
model = YOLO(r"E:\DeepLearning\ultralytics\ultralytics\cfg\models\v8\yolov8.yaml") # 从头开始构建新模型
# model = YOLO("yolov8n.pt") # 加载预训练模型(推荐用于训练)
# Use the model
results = model.train(data=r"E:\DeepLearning\ultralytics\ultralytics\data\DVOR2024830_yolov8\data.yaml",epochs=200,batch=30,lr0=0.1) # 训练模型
metrics = model.val() # 在验证集上评估模型性能
# results = model("https://ultralytics.com/images/bus.jpg") # 预测图像
# success = model.export(format="onnx") # 将模型导出为 ONNX 格式1
def predict():
model = YOLO(r"E:\DeepLearning\ultralytics\runs\detect\train86\weights\best.pt") # pretrained YOLOv8n model
metrics = model.val(data=r"E:\DeepLearning\ultralytics\ultralytics\data\DVOR2024830_yolov8\data.yaml")
vpath=r"E:\DeepLearning\ultralytics\ultralytics\data\ceshi/"
# results = model([vpath+"vor (1).jpg",vpath+"vor (2).jpg",vpath+"vor (3).jpg",vpath+"VORBlack (1).jpg",vpath+"VORBlack (2).jpg",vpath+"VORBlack (3).jpg"], save=True,conf=0.1) # return a list of Results objects
# 视频路径
file_path = r"E:\DeepLearning\ultralytics\ultralytics\data\video\vor3.mp4"
file_path2 = r"E:\DeepLearning\ultralytics\ultralytics\data\video\vor.mp4"
file_path3 = r"E:\DeepLearning\ultralytics\ultralytics\data\video\vor2.mp4"
# 检测视频
# results = model.predict(source=file_path3, device=0, show=False, save=True, conf=0.1)
# # Process results list
# for result in results:
# boxes = result.boxes # Boxes object for bounding box outputs
# masks = result.masks # Masks object for segmentation masks outputs
# keypoints = result.keypoints # Keypoints object for pose outputs
# probs = result.probs # Probs object for classification outputs
# obb = result.obb # Oriented boxes object for OBB outputs
# result.show() # display to screen
# result.save(filename="result.jpg") # save to disk
def exportVincent():
# Load the YOLOv8 model
model = YOLO(r"E:\DeepLearning\ultralytics\runs\detect\train21\weights\best.pt")
# Export the model to ONNX format
model.export(format="onnx",dynamic=True)
if __name__ == '__main__':
if(controller==1):
traindata()
elif controller==2:
predict()
else:
print("null")
#
# exportVincent()
创建Yolov8模型结构
-
根据vincentTrain.py中。代码
model = YOLO("yolov8n.yaml")
初始化时,调用ultralytics/models/yolo/model.py
中的YOLO类初始化函数。 -
ultralytics/models/yolo/model.py 中
def __init__(self, model="yolov8n.pt", task=None, verbose=False):
由于传入参数为"yolov8n.yaml",执行super().__init__(model=model, task=task, verbose=verbose)
super().init()函数调用基类ultralytics/engine/model.py中Model类的构造函数,这里的self参数传入的是子类也就是YOLO类。 -
ultralytics/engine/model.py中。在基类Model的构造函数中,识别到yaml文件,需要新创建模型,则执行代码self._new(model, task=task, verbose=verbose)。该代码会调用Model类中的_new()函数。
-
ultralytics/engine/model.py中。在_new函数中通过cfg_dict = yaml_model_load(cfg)读取模型结构yaml文件。
-
ultralytics/engine/model.py。读取结构配置后,通过self.task = task or guess_model_task(cfg_dict),确定任务类型。guess_model_task()函数用于猜测任务类型检测,分割,分类等。
-
ultralytics/engine/model.py。
self.model = (model or self._smart_load("model"))(cfg_dict, verbose=verbose and RANK == -1)
self._smart_load()只读属性表示调用YOLO类中的_smart_load创建只读属性,该属性变量在基类中并未实现。self._smart_load(“model”)获取到需要实例化的哪个类,然后传入参数(cfg_dict, verbose=verbose and RANK == -1)。 -
ultralytics/models/yolo/model.py。
"detect": { "model": DetectionModel, "trainer": yolo.detect.DetectionTrainer, "validator": yolo.detect.DetectionValidator, "predictor": yolo.detect.DetectionPredictor, },
根据代码self.model返回DetectionModel类,该类定义在ultralytics/nn/tasks.py中。 -
ultralytics/nn/tasks.py。调用DetectionModel的构造函数def init(self, cfg=“yolov8n.yaml”, ch=3, nc=None, verbose=True)
-
ultralytics/nn/tasks.py。
self.model, self.save = parse_model(deepcopy(self.yaml), ch=ch, verbose=verbose)
parse_model()函数对模型进行生成。deepcopy函数用于深度拷贝yaml模型配置信息。ch表示通道数。是后来加上去的参数。
开始遍历yaml中的backbone+head内容
- ultralytics/nn/tasks.py。在parse_model(d, ch, verbose=True)函数中,for i, (f, n, m, args) in enumerate(d[“backbone”] + d[“head”])对yaml内容解析。enumerate函数使得d[“backbone”] + d[“head”]将backbone与head拼接到一个list中。i表示list的索引位置,(f, n, m, args)表示yaml中的参数。
- ultralytics/cfg/models/v8/yolov8.yaml中。在yaml文件中标注如下
backbone:
# [from, repeats, module, args]
- [-1, 1, Conv, [64, 3, 2]] # 0-P1/2
f表示from -1,n表示repeats 1,m表示模块module Conv,args代表参数[64, 3, 2]
- ultralytics/nn/tasks.py的parse_model()。
m = getattr(torch.nn, m[3:]) if "nn." in m else globals()[m]
。getattr函数用于获取它某个属性的值。该语句判断m中是否包含nn.字段,1)如果m为nn.Upsample,则m=torch.nn中Upsample对应的值。2)如果m为Conv,则globals()找到当前位置的全局变量(返回的可以是类型,也可以是具体的变量,或者函数,此处返回类型),返回名为Conv对应的值。class Conv(nn.Module)定义在ultralytics/nn/modules/conv.py
。
# ultralytics/nn/modules/conv.py
class Conv(nn.Module):
"""Standard convolution with args(ch_in, ch_out, kernel, stride, padding, groups, dilation, activation)."""
default_act = nn.SiLU() # default activation
def __init__(self, c1, c2, k=1, s=1, p=None, g=1, d=1, act=True):
"""Initialize Conv layer with given arguments including activation."""
super().__init__()
self.conv = nn.Conv2d(c1, c2, k, s, autopad(k, p, d), groups=g, dilation=d, bias=False)
self.bn = nn.BatchNorm2d(c2)
self.act = self.default_act if act is True else act if isinstance(act, nn.Module) else nn.Identity()
def forward(self, x):
"""Apply convolution, batch normalization and activation to input tensor."""
return self.act(self.bn(self.conv(x)))
- ultralytics/nn/tasks.py的parse_model()。
c2 = make_divisible(min(c2, max_channels) * width, 8)
。make_divisible函数将yaml模型结构中arg参数中第一个表示通道数的数据处理,确保是8的倍数。若不是,则c2设置为比参数1大的8倍数。 - ultralytics/nn/tasks.py的parse_model()。
m_ = nn.Sequential(*(m(*args) for _ in range(n))) if n > 1 else m(*args)
m存放了当前需要创建的模块类型,当前是Conv,m(*args)相当于Conv(*args),创建Conv类实例,调用ultralytics/nn/modules/conv.py中的构造函数,配置该类。并放入nn.Sequential类型的m_中。 - ultralytics/nn/tasks.py的parse_model()。
m_ = nn.Sequential(*(m(*args) for _ in range(n))) if n > 1 else m(*args)
。m存放了当前需要创建的模块类型,当前是Conv,m(*args)相当于Conv(*args),创建Conv类实例,调用ultralytics/nn/modules/conv.py中的构造函数,配置该类。并放入nn.Sequential类型的m_中。 - ultralytics/nn/tasks.py的parse_model()。
m.np = sum(x.numel() for x in m_.parameters())
。m_表示整个神经网络模型,x作为模型中所有参数的迭代项,使用numel()函数计算每个参数的元素数量,最后求和得总数。 - ultralytics/nn/tasks.py的parse_model()。
save.extend(x % i for x in ([f] if isinstance(f, int) else f) if x != -1)
。该语句存储yaml模型结构文件中,f参数不为-1的参数,最终存储[4, 6, 9, 12, 15, 18, 21]。layers.append(m_)
将每个模块存放如layers中。
第三次循环出现m出现C2f模块
- ultralytics/nn/tasks.py的parse_model()。
if m in {BottleneckCSP, C1, C2, C2f, C2fAttn, C3, C3TR, C3Ghost, C3x, RepC3}:
args.insert(2, n) # number of repeats
n = 1
m_ = nn.Sequential(*(m(*args) for _ in range(n))) if n > 1 else m(*args)
识别到模型为C2f后,在arg中插入参数n。代用m(*args)创建C2f模块。
- ultralytics/nn/modules/block.py。
class C2f(nn.Module),创建C2f模块,使用构造函数def __init__(self, c1, c2, n=1, shortcut=False, g=1, e=0.5)
。
# ultralytics/nn/modules/block.py
class C2f(nn.Module):
"""Faster Implementation of CSP Bottleneck with 2 convolutions."""
def __init__(self, c1, c2, n=1, shortcut=False, g=1, e=0.5):
"""Initialize CSP bottleneck layer with two convolutions with arguments ch_in, ch_out, number, shortcut, groups,
expansion.
"""
super().__init__()
self.c = int(c2 * e) # hidden channels
self.cv1 = Conv(c1, 2 * self.c, 1, 1)
self.cv2 = Conv((2 + n) * self.c, c2, 1) # optional act=FReLU(c2)
self.m = nn.ModuleList(Bottleneck(self.c, self.c, shortcut, g, k=((3, 3), (3, 3)), e=1.0) for _ in range(n))
def forward(self, x):
"""Forward pass through C2f layer."""
y = list(self.cv1(x).chunk(2, 1))
y.extend(m(y[-1]) for m in self.m)
return self.cv2(torch.cat(y, 1))
- ultralytics/nn/modules/block.py。
self.m = nn.ModuleList(Bottleneck(self.c, self.c, shortcut, g, k=((3, 3), (3, 3)), e=1.0) for _ in range(n))
。构造函数中实例化Bottleneck类,该类同样定义在ultralytics/nn/modules/block.py中。
# ultralytics/nn/modules/block.py
class Bottleneck(nn.Module):
"""Standard bottleneck."""
def __init__(self, c1, c2, shortcut=True, g=1, k=(3, 3), e=0.5):
"""Initializes a bottleneck module with given input/output channels, shortcut option, group, kernels, and
expansion.
"""
super().__init__()
c_ = int(c2 * e) # hidden channels
self.cv1 = Conv(c1, c_, k[0], 1)
self.cv2 = Conv(c_, c2, k[1], 1, g=g)
self.add = shortcut and c1 == c2
def forward(self, x):
"""'forward()' applies the YOLO FPN to input data."""
return x + self.cv2(self.cv1(x)) if self.add else self.cv2(self.cv1(x))
-
ultralytics/nn/modules/block.py。在Bottleneck中关注构造函数__init__(),以及forward()函数。
-
模型创建迭代到SPPF模块时,ultralytics/nn/tasks.py的parse_model()。ultralytics/nn/modules/block.py中的class SPPF(nn.Module)。定义了该类的结构。关注构造函数__init__(),以及forward()函数即可。
-
接下来创建模型中head内容。nn.Upsample创建方式与前面相似。接下来详细说Concat。
-
ultralytics/cfg/models/v8/yolov8.yaml。在模型结构配置文件head中 - [[-1, 6], 1, Concat, [1]] 。其中[-1, 6]的-1表示前面一层,6表示第六层,在ultralytics/nn/tasks.py的parse_model()中layers变量按顺序存放了之前创建的每个层级模块。在yolov8的模型结构图中已经标识了层级的编号。这里的concat使用了6层的C2f输出,也使用了-1层也就是第10层的Upsample输出。
-
创建concat时,concat类定义在ultralytics/nn/modules/conv.py中。在concat类中,无论是构造函数,还是前向传播函数,均没有涉及到使用参数[-1,6],只是返回了一个torch.cat(x, self.d)的拼接操作。
detect检测层
- ultralytics/nn/tasks.py的parse_model()。最后,创建detect检测层时候。
args[j] = locals()[a] if a in locals() else ast.literal_eval(a)
。a表示-[[15,18,21],1,Detect,[nc]]中的nc,为了映射为前面的nc值,使用了locals()查询当前局部变量有哪些,若存在有名字为nc的变量,则将变量的值赋值给args[j]。所谓检测的种类数nc被放入了args中。
#ultralytics/nn/tasks.py的parse_model()
elif m in {Detect, WorldDetect, Segment, Pose, OBB, ImagePoolingAttn}:
args.append([ch[x] for x in f])
if m is Segment:
args[2] = make_divisible(min(args[2], max_channels) * width, 8)
-
f表示yaml中的from参数,这里是[15,18,21],分别指第15层,18层,21层的输出。将这些输出的通道数量存放入args中。
-
ultralytics/nn/tasks.py的parse_model()。使用m_ = nn.Sequential(*(m(*args) for _ in range(n))) if n > 1 else m(*args)。其中m(*args)语句创建detect模块。调用Detect类。
-
Detect类构造函数如下
#ultralytics/nn/modules/head.py的class Detect(nn.Module)
#class Detect(nn.Module):
def __init__(self, nc=80, ch=()):
"""Initializes the YOLOv8 detection layer with specified number of classes and channels."""
super().__init__()
self.nc = nc # number of classes
self.nl = len(ch) # number of detection layers
self.reg_max = 16 # DFL channels (ch[0] // 16 to scale 4/8/12/16/20 for n/s/m/l/x)
self.no = nc + self.reg_max * 4 # number of outputs per anchor
self.stride = torch.zeros(self.nl) # strides computed during build
c2, c3 = max((16, ch[0] // 4, self.reg_max * 4)), max(ch[0], min(self.nc, 100)) # channels
self.cv2 = nn.ModuleList(
nn.Sequential(Conv(x, c2, 3), Conv(c2, c2, 3), nn.Conv2d(c2, 4 * self.reg_max, 1)) for x in ch
)
self.cv3 = nn.ModuleList(nn.Sequential(Conv(x, c3, 3), Conv(c3, c3, 3), nn.Conv2d(c3, self.nc, 1)) for x in ch)
self.dfl = DFL(self.reg_max) if self.reg_max > 1 else nn.Identity()
nc: 整数,表示图像分类问题中的类别数;
nl: 整数,表示检测模型中使用的检测层数;
reg_max: 整数,表示每个锚点输出的通道数;
no: 整数,表示每个锚点的输出数量,其中包括类别数和位置信息;
stride: 一个形状为(nl,)的张量,表示每个检测层的步长(stride);
cv2: 一个 nn.ModuleList 对象,包含多个卷积层,用于预测每个锚点的位置信息;
cv3: 一个 nn.ModuleList 对象,包含多个卷积层,用于预测每个锚点的类别信息;
dfl: DFL(Distribution Focal Loss)
shape属性表示模型期望的输入形状,如果模型只接受固定形状的输入,则 self.shape 存储该形状。
在这个Detect类构造函数中,定义了c2,c3。分别对应yolov8模型结构图中的Detect模块,分别用于检测框损失计算,以及分类损失计算。
self.dfl = DFL(self.reg_max) if self.reg_max > 1 else nn.Identity()代码将通过DFL类创建dfl模块。dfl相关论文(https://ieeexplore.ieee.org/document/9792391)
- ultralytics/nn/modules/block.py中的class DFL(nn.Module)。
class DFL(nn.Module):
"""
Integral module of Distribution Focal Loss (DFL).
Proposed in Generalized Focal Loss https://ieeexplore.ieee.org/document/9792391
"""
def __init__(self, c1=16):
"""Initialize a convolutional layer with a given number of input channels."""
super().__init__()
self.conv = nn.Conv2d(c1, 1, 1, bias=False).requires_grad_(False)
x = torch.arange(c1, dtype=torch.float)
self.conv.weight.data[:] = nn.Parameter(x.view(1, c1, 1, 1))
self.c1 = c1
def forward(self, x):
"""Applies a transformer layer on input tensor 'x' and returns a tensor."""
b, _, a = x.shape # batch, channels, anchors
return self.conv(x.view(b, 4, self.c1, a).transpose(2, 1).softmax(1)).view(b, 4, a)
# return self.conv(x.view(b, self.c1, 4, a).softmax(1)).view(b, 4, a)
到此,模型结构的构建完成,最终回到了ultralytics/nn/tasks.py中的class DetectionModel(BaseModel)
类构造函数中。
- ultralytics/nn/tasks.py的class DetectionModel(BaseModel)。
# Build strides
m = self.model[-1] # Detect()
if isinstance(m, Detect): # includes all Detect subclasses like Segment, Pose, OBB, WorldDetect
s = 256 # 2x min stride
m.inplace = self.inplace
def _forward(x):
"""Performs a forward pass through the model, handling different Detect subclass types accordingly."""
return self.forward(x)[0] if isinstance(m, (Segment, Pose, OBB)) else self.forward(x)
m.stride = torch.tensor([s / x.shape[-2] for x in _forward(torch.zeros(1, ch, s, s))]) # forward
self.stride = m.stride
m.bias_init() # only run once
else:
self.stride = torch.Tensor([32]) # default stride for i.e. RTDETR
-
ultralytics/nn/tasks.py的class DetectionModel(BaseModel)。
self.stride = m.stride
。m.stride存储了三个输出特征图的缩放比例,yolov8接收输入的图片大小640640,输出特征图大小分别为:8080,4040,2020。缩放比例分别为8,16,32。m.stride存储内容为[8,16,32]。 -
到此,后面就进入到资源加载并训练的语句中。model.train(data=“coco8.yaml”, epochs=3) # 训练模型。调用ultralytics/engine/model.py中的class Model(nn.Module)。def train(self,trainer=None, **kwargs,)。
-
ultralytics/data/utils.py。函数def check_det_dataset(dataset, autodownload=True)中的data[“path”]存储了数据集路径。
-
ultralytics/engine/model.py中train函数。self.trainer = (trainer or self._smart_load(“trainer”))(overrides=args, _callbacks=self.callbacks)加载训练器。
-
self.trainer.model = self.trainer.get_model(weights=self.model if self.ckpt else None, cfg=self.model.yaml)
。self指的是当前这个Model类实例对象,_smart_load()函数将"trainer"方式,选择一个trainer类型的训练器。trainer方式则获得<class ‘ultralytics.models.yolo.detect.train.DetectionTrainer’>。所有的类型存放在self.task_map中,通过任务类型,trainer条件选取。选取后,通过(overrides=args, _callbacks=self.callbacks)参数进行初始化。 -
ultralytics/models/yolo/detect/train.py。class DetectionTrainer(BaseTrainer):继承自BaseTrainer,且没有定义构造函数,构造函数将调用BaseTrainer中的构造函数。
-
class BaseTrainer:中的构造函数self.trainset, self.testset = self.get_dataset()获取数据集路径。用于初始化DetectionTrainer类。
-
ultralytics/engine/model.py中train函数。self.trainer.model = self.trainer.get_model(weights=self.model if self.ckpt else None, cfg=self.model.yaml)。get_model()函数来自DetectionTrainer中。该函数重新创建了一个DetectionModel,并返回该实例。
-
现在回过头来梳理一下。首先我们自己用YOLO类实例化了一个对象,用来创建网络的结构。该YOLO类中存放了一个model属性,存放了具体的DetectionModel,也就是根据配置文件创建的网络模型结构。而刚刚的语句,将在该实例中的trainer属性中,继续创建一个相同的模型结构。然后使用self.model = self.trainer.model,替换之前创建的网络结构。(不明白有什么意义,可能是为了兼容.pt文件或者继续训练的原因)。
-
ultralytics/engine/model.py中train函数。self.trainer.train()。这里的self.trainer是DetectionTrainer,该类中没有train()函数,所以调用基类BaseTrainer中的train()函数,正式开始训练
开始训练
未完待续。。。。。