手把手教你用YOLOv5算法训练数据和检测目标(不会你捶我)

前言

本人从一个小白,一路走来,已能够熟练使用YOLOv5算法来帮助自己解决一些问题,早就想分析一下自己的学习心得,一直没有时间,最近工作暂时告一段落,今天抽空写点东西,一是为自己积累一些学习笔记,二是可以为一些刚接触YOLOv5算法的小白们提供一些参考,希望大家看之前能够动动你的小手,给我点个关注,给文章点个赞,如果此文确实给你提供了帮助,希望你能在留言区打两个字个“此文有用!”,以此来让这篇文章获得更多的流量,让更多小白能够看到。

YOLOv5

那么多深度学习算法,为什么要用YOLOv5?我觉得很简单,因为YOLOv5快、YOLOv5火、YOLOv5流行啊,为什么不用YOLOv7、YOLOv8,因为不成熟、不稳定,噱头大于改进。所以你要做深度学习,用YOLOv5算法是最稳妥的,就像做人一样,不会很优秀,也不会犯错。中庸之道才是王道。

数据集是什么?就是一堆包含你最终要检测的目标的照片,例如猫、狗、猪。

训练是什么?就是不停的告诉电脑这是猫,这是狗,这是猪的一个不断重复的过程。

检测是什么?就是你教了电脑300遍后,你再拿张有着猫、狗、猪的照片问电脑,哪是猫、哪是狗、哪是猪。

我这样讲,大家应该对这是个怎么回事有个初步的了解了吧,那我们继续往下走。

1.目标检测整体流程

获取算法源码 --------》 配置运行环境 --------》 建立数据集 --------》 标注数据集 --------》 数据集分组 --------》 文件、代码修改 --------》 开始训练、得到模型 --------》 用得到的模型、检测目标 --------》 OVER

2.获取算法源码

YOLOv5代码是开源的,可以免费下载不同的版本,我使用的是YOLOv5 6.1版本,如果你想参考我的步骤做,就下载这个版本的,别的版本我也不熟悉,操作步骤有可能不同。
yolov5-6.1版本代码下载地址

点开网址后,点击左上角的master,下拉框里选择v6.1版本,如下图:手把手教你用YOLOv5算法训练数据和检测目标(不会你捶我)_第1张图片
然后点击右边的克隆,下载源代码里选择zip,这样就把源代码下载下来了。如下图:
手把手教你用YOLOv5算法训练数据和检测目标(不会你捶我)_第2张图片

3.配置运行环境

有了代码,怎样让代码运行通,这需要配置环境,新手小白在这一步搞个三五天很正常,各种错误,各种bug,我也是这么过来的,但是你配置好环境,运行代码没有bug后,你会感觉非常有成就感。记住,不要放弃,就一点一点的解决bug,不要中途放弃。
这一块有其他博主写的很好很详细,大家可以点进去跟着做。
点击配置YOLOv5运行环境
我的环境贴出来给大家看看:
我用的是笔记本:联想拯救者Y9000P2022
操作系统:windows11
显卡:RTX 3060
编译软件:Pycharm
python版本:3.9.1
pytorch版本:1.9.1
cuda版本:11.1
怎样判断环境是否配置好了呢?那就是运行检测程序——detcet.py文件,然后会在runs文件夹里的detcet文件夹下生成一个exp文件夹,里面是一张框出几个人或者车的图片,具体什么我也忘了。反正就是能够运行detect.py文件。

4.建立数据集

在data目录下新建Annotations, images, ImageSets, labels 四个文件夹。
目录结构如下图所示:

手把手教你用YOLOv5算法训练数据和检测目标(不会你捶我)_第3张图片
其中images存放的是原始的图片数据集,Annotations存放的是标记后生成的xml文件,labels存放的是保存标记内容的txt文件,ImageSets存放的是训练数据集和测试数据集的分类情况。

巧妇难为无米之炊。做好准备工作之后,下一步要干什么?
那就是数据,数据从哪里来?一个就是下载网上公开的数据集,一个就是自己收集数据集。
基本上现在研究YOLOv5算法的主要有两个方向:一是在源代码上做文章,以此能够达到提高精度或者提高速度的目标,然后发论文。二是在数据集上做文章,用自己的数据集去解决某一实际问题,然后发论文。往往第一种方向会用公开的数据集,第二个方向会用自己的数据集。由于第一个方向需要对编程和数学有深厚的学识造诣,所以自然而然我就奔向了第二个方向。我使用的是自己的数据集。

5.标注数据集

Yolov5 使用精灵标注助手制作数据集详细教程

6.数据集分组

在yolov5-6.1根目录下新建一个文件makeTxt.py,(根目录大家都知道啥意思吧?我还真担心有人不知道)
代码如下:

import os
import random


trainval_percent = 0.9
train_percent = 0.9
xmlfilepath = 'data/Annotations'
txtsavepath = 'data/ImageSets'
total_xml = os.listdir(xmlfilepath)

num = len(total_xml)
list = range(num)
tv = int(num * trainval_percent)
tr = int(tv * train_percent)
trainval = random.sample(list, tv)
train = random.sample(trainval, tr)

ftrainval = open('data/ImageSets/trainval.txt', 'w')
ftest = open('data/ImageSets/test.txt', 'w')
ftrain = open('data/ImageSets/train.txt', 'w')
fval = open('data/ImageSets/val.txt', 'w')

for i in list:
    name = total_xml[i][:-4] + '\n'
    if i in trainval:
        ftrainval.write(name)
        if i in train:
            ftrain.write(name)
        else:
            fval.write(name)
    else:
        ftest.write(name)

ftrainval.close()
ftrain.close()
fval.close()
ftest.close()

接着再新建另一个文件voc_label.py,切记,代码中classes=[……] 中填入的一定要是自己在数据集中所标注的类别名称,标记了几个类别就填写几个类别名,填写错误的话会造成读取不出xml文件里的标注信息。代码如下:

# -*- coding: utf-8 -*-
# xml解析包
import xml.etree.ElementTree as ET
import pickle
import os
from os import listdir, getcwd
from os.path import join


sets = ['train', 'test', 'val']
classes = ['cat', 'dog', 'pig']


# 进行归一化操作
def convert(size, box): # size:(原图w,原图h) , box:(xmin,xmax,ymin,ymax)
    dw = 1./size[0]     # 1/w
    dh = 1./size[1]     # 1/h
    x = (box[0] + box[1])/2.0   # 物体在图中的中心点x坐标
    y = (box[2] + box[3])/2.0   # 物体在图中的中心点y坐标
    w = box[1] - box[0]         # 物体实际像素宽度
    h = box[3] - box[2]         # 物体实际像素高度
    x = x*dw    # 物体中心点x的坐标比(相当于 x/原图w)
    w = w*dw    # 物体宽度的宽度比(相当于 w/原图w)
    y = y*dh    # 物体中心点y的坐标比(相当于 y/原图h)
    h = h*dh    # 物体宽度的宽度比(相当于 h/原图h)
    return (x, y, w, h)    # 返回 相对于原图的物体中心点的x坐标比,y坐标比,宽度比,高度比,取值范围[0-1]


# year ='2012', 对应图片的id(文件名)
def convert_annotation(image_id):
    '''
    将对应文件名的xml文件转化为label文件,xml文件包含了对应的bunding框以及图片长款大小等信息,
    通过对其解析,然后进行归一化最终读到label文件中去,也就是说
    一张图片文件对应一个xml文件,然后通过解析和归一化,能够将对应的信息保存到唯一一个label文件中去
    labal文件中的格式:calss x y w h  同时,一张图片对应的类别有多个,所以对应的bunding的信息也有多个
    '''
    # 对应的通过year 找到相应的文件夹,并且打开相应image_id的xml文件,其对应bund文件
    in_file = open('data/Annotations/%s.xml' % (image_id), encoding='utf-8')
    # 准备在对应的image_id 中写入对应的label,分别为
    #     
    out_file = open('data/labels/%s.txt' % (image_id), 'w', encoding='utf-8')
    # 解析xml文件
    tree = ET.parse(in_file)
    # 获得对应的键值对
    root = tree.getroot()
    # 获得图片的尺寸大小
    size = root.find('size')
    # 如果xml内的标记为空,增加判断条件
    if size != None:
        # 获得宽
        w = int(size.find('width').text)
        # 获得高
        h = int(size.find('height').text)
        # 遍历目标obj
        for obj in root.iter('object'):
            # 获得difficult ??
            difficult = obj.find('difficult').text
            # 获得类别 =string 类型
            cls = obj.find('name').text
            # 如果类别不是对应在我们预定好的class文件中,或difficult==1则跳过
            if cls not in classes or int(difficult) == 1:
                continue
            # 通过类别名称找到id
            cls_id = classes.index(cls)
            # 找到bndbox 对象
            xmlbox = obj.find('bndbox')
            # 获取对应的bndbox的数组 = ['xmin','xmax','ymin','ymax']
            b = (float(xmlbox.find('xmin').text), float(xmlbox.find('xmax').text), float(xmlbox.find('ymin').text),
                 float(xmlbox.find('ymax').text))
            print(image_id, cls, b)
            # 带入进行归一化操作
            # w = 宽, h = 高, b= bndbox的数组 = ['xmin','xmax','ymin','ymax']
            bb = convert((w, h), b)
            # bb 对应的是归一化后的(x,y,w,h)
            # 生成 calss x y w h 在label文件中
            out_file.write(str(cls_id) + " " + " ".join([str(a) for a in bb]) + '\n')


# 返回当前工作目录
wd = getcwd()
print(wd)


for image_set in sets:
    '''
    对所有的文件数据集进行遍历
    做了两个工作:
    1.将所有图片文件都遍历一遍,并且将其所有的全路径都写在对应的txt文件中去,方便定位
    2.同时对所有的图片文件进行解析和转化,将其对应的bundingbox 以及类别的信息全部解析写到label 文件中去
         最后再通过直接读取文件,就能找到对应的label 信息
    '''
    # 先找labels文件夹如果不存在则创建
    if not os.path.exists('data/labels/'):
        os.makedirs('data/labels/')
    # 读取在ImageSets/Main 中的train、test..等文件的内容
    # 包含对应的文件名称
    image_ids = open('data/ImageSets/%s.txt' % (image_set)).read().strip().split()
    # 打开对应的2012_train.txt 文件对其进行写入准备
    list_file = open('data/%s.txt' % (image_set), 'w')
    # 将对应的文件_id以及全路径写进去并换行
    for image_id in image_ids:
        list_file.write('data/images/%s.jpg\n' % (image_id))
        # 调用  year = 年份  image_id = 对应的文件名_id
        convert_annotation(image_id)
    # 关闭文件
    list_file.close()

# os.system(‘comand’) 会执行括号中的命令,如果命令成功执行,这条语句返回0,否则返回1
# os.system("cat 2007_train.txt 2007_val.txt 2012_train.txt 2012_val.txt > train.txt")
# os.system("cat 2007_train.txt 2007_val.txt 2007_test.txt 2012_train.txt 2012_val.txt > train.all.txt")

先运行makeTxt.py,makeTxt.py主要是将数据集分类成训练数据集和测试数据集,默认train,val,test按照8:1:1的比例进行随机分类,运行后ImagesSets文件夹中会出现四个文件,主要是生成的训练数据集和测试数据集的图片名称,如下图:
手把手教你用YOLOv5算法训练数据和检测目标(不会你捶我)_第4张图片
这几个txt你要点开看看,如果时空白的,说明你出错了,如果里面是下图这样的内容,说明没问题。
手把手教你用YOLOv5算法训练数据和检测目标(不会你捶我)_第5张图片

再运行voc_label.py文件,主要是将图片数据集标注后的xml文件中的标注信息读取出来并写入txt文件,运行后在labels文件夹中出现所有图片数据集的标注信息,如下图:
手把手教你用YOLOv5算法训练数据和检测目标(不会你捶我)_第6张图片
到这一步,已经把米准备好了,下一步就是修改几个参数就可以训练了。是不是感觉很麻烦,其实不是,你以后这些步骤都不用再做了,主要做的就是train和detect,也就是训练和检测。

7.文件、代码修改

7.1 文件修改

首先在data目录下,找到存在的coco.yaml,然后复制一份重命名为animal.yaml,这个animal是我的名字,你的名字自己取就行,打开animal.yaml,其中path,train,val,test分别为数据集的路径, nc为数据集的类别数,我这里只分了3类,names为类别的名称。这几个参数均按照自己的实际需求来修改。总之,这一步只用修改nc后面的数字,别的不要动。
animal.yaml的代码如下:

# Train command: python train.py --data data/cat.yaml
# Dataset should be placed next to yolov5 folder:
# parent
# ├── yolov5
#     └── data


# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
path: data  # dataset root dir
train: train.txt  # train images (relative to 'path')
val: val.txt  # val images (relative to 'path')
test: test.txt  # test images (optional)

# number of classes
nc: 3

# class names
names: ['cat', 'dog', 'pig']

7.2 代码修改
yolov5提供了好几个训练模型,让你作为“基模型”进行训练,我用的是yolov5s模型,所以我找到models目录下提供了yolov5s.yaml文件进行修改,只用修改类的个数,我的类是3,所以nc:3.你根据你的实际情况决定n是多少,其余的不用修改。代码如下:

# Parameters
nc: 3  # number of classes
depth_multiple: 1.0  # model depth multiple
width_multiple: 1.0  # layer channel multiple

最后要对train.py中的一些参数进行修改,train.py顾名思义就是用来训练的,
我们平时训练的话,主要用到的只有这几个参数而已:–weights,–cfg,–data,–epochs,–batch-size,–img-size,–project。

parser = argparse.ArgumentParser()
# 加载预训练的模型权重文件,如果文件夹下没有该文件,则在训练前会自动下载
parser.add_argument('--weights', type=str, default=ROOT / 'yolov5s.pt', help='initial weights path')
# 模型配置文件,网络结构,使用修改好的yolov5l.yaml文件
parser.add_argument('--cfg', type=str, default='models/yolov5s.yaml', help='model.yaml path')
# 数据集配置文件,数据集路径,类名等,使用配置好的animal.yaml文件
parser.add_argument('--data', type=str, default=ROOT / 'data/animal.yaml', help='dataset.yaml path')
# 超参数文件
parser.add_argument('--hyp', type=str, default=ROOT / 'data/hyps/hyp.scratch.yaml', help='hyperparameters path')
# 训练总轮次,1个epoch等于使用训练集中的全部样本训练一次,值越大模型越精确,训练时间也越长,默认为300
parser.add_argument('--epochs', type=int, default=300)
# 批次大小,一次训练所选取的样本数,显卡不太行的话,就调小点,反正3060是带不动batch-size=16的,传-1的话就是autobatch
parser.add_argument('--batch-size', type=int, default=16, help='total batch size for all GPUs')
# 输入图片分辨率大小,默认为640
parser.add_argument('--imgsz', '--img', '--img-size', type=int, default=640, help='train, val image size (pixels)')
# 是否采用矩形训练,默认False,开启后可显著的减少推理时间
parser.add_argument('--rect', action='store_true', help='rectangular training')
# 继续训练,默认从打断后的最后一次训练继续,需开启default=True
parser.add_argument('--resume', nargs='?', const=True, default=False, help='resume most recent training')
# 仅保存最终一次epoch所产生的模型
parser.add_argument('--nosave', action='store_true', help='only save final checkpoint')
# 仅在最终一次epoch后进行测试
parser.add_argument('--noval', action='store_true', help='only validate final epoch')
# 禁用自动锚点检查
parser.add_argument('--noautoanchor', action='store_true', help='disable autoanchor check')
# 超参数演变
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')
# 是否提前缓存图片到内存,以加快训练速度,默认False
parser.add_argument('--cache', type=str, nargs='?', const='ram', help='--cache images in "ram" (default) or "disk"')
# 选用加权图像进行训练
parser.add_argument('--image-weights', action='store_true', help='use weighted image selection for training')
# 训练的设备,cpu;0(表示一个gpu设备cuda:0);0,1,2,3(多个gpu设备)。值为空时,训练时默认使用计算机自带的显卡或CPU
parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')
# 是否进行多尺度训练,默认False
parser.add_argument('--multi-scale', action='store_true', help='vary img-size +/- 50%%')
# 数据集是否只有一个类别,默认False
parser.add_argument('--single-cls', action='store_true', help='train multi-class data as single-class')
# 是否使用adam优化器,默认False
parser.add_argument('--adam', action='store_true', help='use torch.optim.Adam() optimizer')
# 是否使用跨卡同步BN,在DDP模式使用
parser.add_argument('--sync-bn', action='store_true', help='use SyncBatchNorm, only available in DDP mode')
# dataloader的最大worker数量,大于0时使用子进程读取数据,训练程序有可能会卡住
parser.add_argument('--workers', type=int, default=8, help='maximum number of dataloader workers')
# 训练结果所存放的路径,默认为runs/train
parser.add_argument('--project', default=ROOT / 'runs/train', help='save to project/name')
# 训练结果所在文件夹的名称,默认为exp
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')
# 使用四合一dataloader
parser.add_argument('--quad', action='store_true', help='quad dataloader')
# 线性学习率
parser.add_argument('--linear-lr', action='store_true', help='linear LR')
# 标签平滑处理,默认0.0
parser.add_argument('--label-smoothing', type=float, default=0.0, help='Label smoothing epsilon')
# 已训练多少次epoch后结果仍没有提升就终止训练,默认100
parser.add_argument('--patience', type=int, default=100, help='EarlyStopping patience (epochs without improvement)')
# 冻结模型层数,默认0不冻结,冻结主干网就传10,冻结所有就传24
parser.add_argument('--freeze', type=int, default=0, help='Number of layers to freeze. backbone=10, all=24')
# 设置多少次epoch保存一次模型
parser.add_argument('--save-period', type=int, default=-1, help='Save checkpoint every x epochs (disabled if < 1)')
# 分布式训练参数,请勿修改
parser.add_argument('--local_rank', type=int, default=-1, help='DDP parameter, do not modify')

# Weights & Biases arguments(一般上用不着)
parser.add_argument('--entity', default=None, help='W&B: Entity')
parser.add_argument('--upload_dataset', action='store_true', help='W&B: Upload dataset as artifact table')
parser.add_argument('--bbox_interval', type=int, default=-1, help='W&B: Set bounding-box image logging interval')
parser.add_argument('--artifact_alias', type=str, default='latest', help='W&B: Version of dataset artifact to use')

opt = parser.parse_known_args()[0] if known else parser.parse_args()

6.开始训练、得到模型

完成以上步骤后,把电脑所以运行的程序都关掉,就留一个P有Charm,运行train.py文件开始训练,这是就是检验你电脑性能的时候了,主要是看你的显卡性能,时间长短由你的显卡性能决定,也由你的数据集的数量决定。

当程序出现如下界面时,说明程序开始训练,没有错误。
手把手教你用YOLOv5算法训练数据和检测目标(不会你捶我)_第7张图片

为了截图,我的训练轮次epochs设置的时2次,默认是300次。

当训练结束后,会在runs文件夹下train文件里生成一个exp文件夹,里面有一堆文件,其中的weights里的best.pt就是训练得到的检测模型,用于下一步的检测之中。如下图所示:
手把手教你用YOLOv5算法训练数据和检测目标(不会你捶我)_第8张图片

6.用得到的模型、检测目标

有了训练得到的模型后,下一步我们就要干嘛了?检测试验,看训练得到的检测模型好不好用?那就要在detect.py文件里做文章了,这里面也有一些参数需要设置一下,主要用到的有这几个参数:–weights,–source,–img-size,–conf-thres,–project。详细如下图所示:

parser = argparse.ArgumentParser()
# 选用训练的权重,不指定的话会使用yolov5l.pt预训练权重
parser.add_argument('--weights', nargs='+', type=str, default=ROOT / 'runs/train/exp/weights/best.pt', help='model path(s)')
# 检测数据,可以是图片/视频路径,也可以是'0'(电脑自带摄像头),也可以是rtsp等视频流
parser.add_argument('--source', type=str, default=ROOT / 'inference/videos/动物识别.mp4', help='file/dir/URL/glob, 0 for webcam')
# 指定推理图片分辨率,默认640
parser.add_argument('--imgsz', '--img', '--img-size', nargs='+', type=int, default=[640], help='inference size h,w')
# 置信度阈值,检测到的对象属于特定类(狗,猫,香蕉,汽车等)的概率,默认为0.25
parser.add_argument('--conf-thres', type=float, default=0.25, help='confidence threshold')
# 指定NMS(非极大值抑制)的IOU阈值,默认为0.45
parser.add_argument('--iou-thres', type=float, default=0.45, help='NMS IoU threshold')
# 每张图最多检测多少目标,默认为1000个
parser.add_argument('--max-det', type=int, default=1000, help='maximum detections per image')
# 检测的设备,cpu;0(表示一个gpu设备cuda:0);0,1,2,3(多个gpu设备)。值为空时,训练时默认使用计算机自带的显卡或CPU
parser.add_argument('--device', default='', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')
# 是否展示检测之后的图片/视频,默认False
parser.add_argument('--view-img', action='store_true', help='show results')
# 是否将检测的框坐标以txt文件形式保存(yolo格式),默认False
parser.add_argument('--save-txt', action='store_true', help='save results to *.txt')
# 在输出标签结果txt中同样写入每个目标的置信度,默认False
parser.add_argument('--save-conf', action='store_true', help='save confidences in --save-txt labels')
# 从图片\视频上把检测到的目标抠出来保存,默认False
parser.add_argument('--save-crop', action='store_true', help='save cropped prediction boxes')
# 不保存图片/视频,默认False
parser.add_argument('--nosave', action='store_true', help='do not save images/videos')
# 设置只检测特定的类,如--classes 0 2 4 6 8,默认False
parser.add_argument('--classes', nargs='+', type=int, help='filter by class: --classes 0, or --classes 0 2 3')
# 使用agnostic NMS(前背景),默认False
parser.add_argument('--agnostic-nms', action='store_true', help='class-agnostic NMS')
# 推理的时候进行多尺度,翻转等操作(TTA)推理,属于增强识别,速度会慢不少,默认False
parser.add_argument('--augment', action='store_true', help='augmented inference')
# 特征可视化,默认False
parser.add_argument('--visualize', action='store_true', help='visualize features')
# 更新所有模型,默认False
parser.add_argument('--update', action='store_true', help='update all models')
# 检测结果所存放的路径,默认为runs/detect
parser.add_argument('--project', default=ROOT / 'runs/detect', help='save results to project/name')
# 检测结果所在文件夹的名称,默认为exp
parser.add_argument('--name', default='exp', help='save results to project/name')
# 若现有的project/name存在,则不进行递增
parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment')
# 画图时线条宽度
parser.add_argument('--line-thickness', default=3, type=int, help='bounding box thickness (pixels)')
# 隐藏标签
parser.add_argument('--hide-labels', default=False, action='store_true', help='hide labels')
# 隐藏置信度
parser.add_argument('--hide-conf', default=False, action='store_true', help='hide confidences')
# 半精度检测(FP16)
parser.add_argument('--half', action='store_true', help='use FP16 half-precision inference')
# 在onnx推理中使用OpenCV DNN
parser.add_argument('--dnn', action='store_true', help='use OpenCV DNN for ONNX inference')
opt = parser.parse_args()

修改好参数后,直接执行detect.py文件,如果不更改检测结果所产生的路径的话,检测完成后会在runs/detect/exp文件夹得到检测后的图片。如下图所示:
手把手教你用YOLOv5算法训练数据和检测目标(不会你捶我)_第9张图片

至此,整个从0到最后能检测你想要的目标就结束了,到这里是不感觉一点都不难了。万事开头难真的是句真理,等你掌握了这一整套流程后,你会发现你有很多好想法,利用YOLOv5算法来解决生活中很多的现实问题。

大家过程中有什么问题,可以在文章下面留言,我看到会及时回复的,别的朋友看到也会回复的!
再次提醒,别忘了点赞、关注、留言!

我告诉大家一个很残酷的现实,那就是你在学习过程中,出现bug了,你想让哪位博主给你解决,或者你想加别人联系方式,然后想有问题就问,在无偿情况下是不可能的,因为大家都很忙,所以我告诉大家一个方法,那就是遇到问题,就在CSDN上查,没有什么解决不了的,反正我一路走来,遇到的所以问题都是在CSDN上自己查的解决方法,相信我,你也可以的。

在这里插入图片描述

你可能感兴趣的:(YOLOv5,算法,YOLO,深度学习)