yolov4模型训练

yolov4 模型训练

发现网上关于yolo模型的训练资料很少 所以在这里总结一下各种的坑 和 如何训练自己的数据集
和一些关于yolov4 参数的解释 方便快速的玩起来yolov4

关于为什么用yolov4而不用yolov5: yolov5的官方框架是pytorch 而我为了将模型部署到树莓派上而尽量少的装环境 所以选择了 基于darknet的yolov4 这样训练出来的模型就可以直接的被opencv的dnn模块读取

关于数据集

拖了快半年了 终于写完了

这里采用VOC的格式来组织数据结构
			 | --- Annotations   存放VOC数据集的标签标注格式 .xml
			 |                       -- train.txt
	VOC--------------| --- ImageSets--Main--|  train 和 val 的编号
			 |                       -- val.txt
			 | --- JPEGImages  存放训练图片
			 | --- labels      用来存放图片的标签  yolo类型标签
			 | --- TESTImages  测试图片(val和test的区别): val 训练时的测试集 test完成后自己用来验证的图片

1. 数据集文件夹生成
点击查看代码
import os
import time

'''
该程序用生成 VOC数据集文件夹结构
结构如下:

             | --- Annotations   存放voc格式标签 
             |                       -- train.txt
数据集名称    -----| --- ImageSets--Main--|  train 和 val 的编号
             |                       -- val.txt
             | --- JPEGImages  存放训练图片
             | --- labels      用来存放图片的标签  yolo类型标签
             | --- TESTImages  测试图片(val和test的区别): val 训练时的测试集 test完成后自己用来验证的图片


'''
# 本数据集名称
FNAME = "testData"
# 本数据集生成的位置
FPATH = "../"

def mkFolder(path,fname):
    floderName = fname  # 生成主文件夹的名称,比如今年是2021,则主文件夹的名称为VOC2021
    # path = os.path.normpath(path)
    # 主文件夹路径
    floderPath = os.path.join(path, floderName)
    if not os.path.exists(floderPath):
        # 创建文件夹
        os.makedirs(floderPath)
    print(floderPath)
    # 子文件夹名称
    subfloderName = ['Annotations', 'ImageSets', 'JPEGImages', 'labels', 'TESTImages']  # 生成的子文件夹名称
    for name in subfloderName:
        # 子文件夹路径
        subfloderPath = os.path.join(path, floderName, name)
        print(subfloderPath)
        if not os.path.exists(subfloderPath):
            # 不存在就创建
            os.makedirs(subfloderPath)
            # 如果是图片文件夹 就再创建Main
            # Main内用来存放 train.txt 和 val.txt 用来写路径 训练图片 和 验证图片的路径
        if name == 'ImageSets':
            secSubFolderName = 'Main'
            secSubFolderPath = os.path.join(path, floderName, name, secSubFolderName)
            print(secSubFolderPath)
            if not os.path.exists(secSubFolderPath):
                os.makedirs(secSubFolderPath)


if __name__ == '__main__':
    mkFolder(FPATH,FNAME)

2. 将待训练图片移入JPEGImages文件夹
3. 对图片进行改名 
点击查看代码
import os

'''
    本程序用于图形名称修改
'''

# 图片存放的路径
PATH = r"D:\yolov4\VOC\lj\JPEGImages"

# 遍历更改文件名
num = 1
for file in os.listdir(PATH):
    os.rename(os.path.join(PATH,file),os.path.join(PATH,str(num))+".jpg")
    num = num + 1
4. 对图片取标签 
	需要用到 labelImg
	生成的xml文件 保存到 Annotations文件夹内

5. 对图片和标签划分数据集与验证集(如果验证集不拿来训练和测试集是一样的)
	会生成 两个文件.txt 在ImageSets/Main在内 
	分别存储两个数据集的图片名称 
点击查看代码
import os

'''
    本程序用于分割数据集 和 训练集
'''

if __name__ == '__main__':
    source_folder = r'D:/yolov4/VOC/lj/Annotations'  # 图片源目录
    train = r'../TestData/ImageSets/Main/train.txt'  # train.txt文件路径
    val = r'../TestData/ImageSets/Main/val.txt'  # val.txt文件路径
    # 拿到dir内的文件名
    file_list = os.listdir(source_folder)
    file_num = 0

    with open(train, mode='a', encoding='utf-8') as train_file:
        with open(val, mode='a', encoding='utf-8') as val_file:
            for file_obj in file_list:
                file_path = os.path.join(source_folder, file_obj)
                # 拿到路径和后缀
                file_name, file_extend = os.path.splitext(file_obj)
                file_num = file_num + 1
                if file_num % 4 == 0:  # 每隔4张选取一张验证集
                    # 可自由选择训练集与测试集比例如果想设为9:1则把4改成9
                    val_file.write(file_name + '\n')
                else:
                    train_file.write(file_name + '\n')


6. 生成yolov4可以识别的标签 .xml  --> .txt放入 labels文件夹内
并且根据 ImageSets生成 路径训练集和测试集 图片的路径文件
还生成.names文件
点击查看代码
import xml.etree.ElementTree as ET
import os

'''
本程序将voc的mxl文标签转换为yolo的txt标签 存储在labels文件内


并创建xx_train.txt,xx_val.txt 将图片名称转为图片路径存储 
'''

# 生成标签存储路径
LABELPATH = r'D:\yolov4\VOC\testData\labels'
# 图片名称文件 路径
IMGSETPATH = r'../testData/ImageSets/Main/%s.txt'
# 图片路径文件 的保存路径
SAVEPATH = r'../testData/testData_%s.txt'
# 这里是XML文件的路径
XMLPATH = r'../testData/Annotations/%s.xml'
# 这里是图片路径
IMGPATH= 'D:/yolov4/VOC/testData/JPEGImages/%s.jpg\n'

# 这里写入 标签的名称 标定时候的名称 转换成txt时候 会被消失 所以需要 .name文件
classes = ["lj"]

# 不需要配置
sets = ['train', 'val']  ##这里要与Main中的txt文件一致



# 位置转换函数
def convert(size, box):
    dw = 1. / (size[0])
    dh = 1. / (size[1])
    x = (box[0] + box[1]) / 2.0 - 1
    y = (box[2] + box[3]) / 2.0 - 1
    w = box[1] - box[0]
    h = box[3] - box[2]
    x = x * dw
    w = w * dw
    y = y * dh
    h = h * dh
    return (x, y, w, h)


# image_id图片的名称 xml转换成yolo文件
def convert_annotation(image_id):
    # 输入  文件xxx.xml 表示某张图片的xml标签
    in_file = open(XMLPATH % image_id, encoding="utf-8")
    # 输出 文件txt  yolo标签 名称和图片一样
    out_file = open(LABELPATH + "\%s.txt" % image_id, 'w', encoding="utf-8")
    tree = ET.parse(in_file)
    root = tree.getroot()
    size = root.find('size')
    w = int(size.find('width').text)
    h = int(size.find('height').text)

    for obj in root.iter('object'):
        difficult = obj.find('difficult').text
        cls = obj.find('name').text
        if cls not in classes or int(difficult) == 1:
            continue
        cls_id = classes.index(cls)
        xmlbox = obj.find('bndbox')
        b = (float(xmlbox.find('xmin').text), float(xmlbox.find('xmax').text), float(xmlbox.find('ymin').text),
             float(xmlbox.find('ymax').text))
        bb = convert((w, h), b)
        out_file.write(str(cls_id) + " " + " ".join([str(a) for a in bb]) + '\n')


# 循环 分别转换 train 和 val
for image_set in sets:
    # 如果不存在 labels创建labels
    if not os.path.exists(LABELPATH):
        os.makedirs(LABELPATH)
    # 读取图片名称 train.txt里面放的是图片的名称
    image_ids = open(IMGSETPATH % image_set,encoding='utf-8').read().strip('\n').split('\n')
    # 写入到 year_train.txt or year_test.txt
    # train和val的路径文件放在VOC文件夹内
    list_file = open(SAVEPATH % image_set, 'w')
    # 循环获取名称
    for image_id in image_ids:
        # 在year_train和year_val内写入图片的路径
        list_file.write(IMGPATH%image_id)
        convert_annotation(image_id)
    list_file.close()

7. 创建.data文件 这个文件里面记录了我们所有需要的文件的路径
点击查看代码
# .data 文件的保存路径
DATASAVEPATH = r"D:\yolov4\VOC\testData\mydata.data"
CLASSNUM = 1
TRAINPATH =  r"D:\yolov4\VOC\testData\testData_train.txt"
VALIDPATH = r"D:\yolov4\VOC\testData\testData_val.txt"
NAMEPATH = r"D:\yolov4\VOC\testData\name.names"
BACKPATH = r"D:\yolov4\VOC\testData\mymodel"
with open(file=DATASAVEPATH,mode="wt",encoding='utf-8') as f:
    f.write("classes = {}\n".format(CLASSNUM))
    f.write("train =" + TRAINPATH + '\n')
    f.write("valid = " + VALIDPATH + '\n')
    f.write("names = " + NAMEPATH + '\n')
    f.write("backup = "+ BACKPATH + '\n')

8. 修改cfg文件(模型配置文件)
这是yolov4.cfg 其他的看着改

把第三行batch改为batch=64

把subdivisions那一行改为 subdivisions=16

将max_batch更改为(数据集标签种类数(classes)*2000 但不小于训练的图片数量以及不小于6000)

将第20的steps改为max_batch的0.8倍和0.9倍

把位于8-9行设为width=416 height=416 或者其他32的倍数:

将classes=80 改为你的类别数 (有三个地方,969行,1056行,1143行)

改正[filters=255] 为 filters=(classes + 5)x3 (位置为查找yolo,每个yolo前的[convolutional]里,注意只修改最接近yolo的那个filters需要修改,一共应该有三处)

如果使用 [Gaussian_yolo] 层,修改 filters=(classes + 9)x3 (位置为CRRL+F查找Gaussian_yolo,每个Gaussian_yolo前的[convolutional]里,注意只修改最接近Gaussian_yolo的那个filters需要修改,一共应该有三处)
————————————————
版权声明:本文为CSDN博主「雪回」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Mintary/article/details/112511251

9.训练模型
	darknet.exe detector train D:\yolov4\VOC\testData\mydata.data D:\yolov4\VOC\testData\mymodel\my_yolo_tiny.cfg D:\yolov4\VOC\testData\mymodel\yolov4-tiny.conv.29

 .data   .cfg   .conv.29

10. 利用opencv加载模型
点击查看代码
import numpy as np
import time
import cv2
'''
    模型测试程序 
    NAMEPATH 标签路径  
    MODELPATH 模型路径
    WEIGHTPATH 权值路径
'''

NAMEPATH = "../../I_train_model/my_voc.names"
MODELPATH = '../../I_train_model/my_yolo_tiny.cfg'
WEIGHTPATH = '../../I_train_model/my_yolo_tiny_last.weights'


LABELS = open(NAMEPATH).read().strip().split("\n")
np.random.seed(666)
COLORS = np.random.randint(0, 255, size=(len(LABELS), 3), dtype="uint8")
# 导入 YOLO 配置和权重文件并加载网络:
net = cv2.dnn.readNetFromDarknet(MODELPATH, WEIGHTPATH)
# 获取 YOLO 未连接的输出图层
layer = net.getUnconnectedOutLayersNames()
cap = cv2.VideoCapture(0)
while cap.isOpened():
    _,image = cap.read()
    # image = cv2.imread('I_train_model/img.png')
    # 获取图片尺寸
    (H, W) = image.shape[:2]
    # 从输入图像构造一个 blob,然后执行 YOLO 对象检测器的前向传递,给我们边界盒和相关概率
    blob = cv2.dnn.blobFromImage(image, 1/255.0, (320, 320),
                                 swapRB=True, crop=False)
    net.setInput(blob)
    start = time.time()
    # 前向传递,获得信息
    layerOutputs = net.forward(layer)
    # 用于得出检测时间
    end = time.time()
    print("YOLO took {:.6f} seconds".format(end - start))

    boxes = []
    confidences = []
    classIDs = []

    # 循环提取每个输出层
    for output in layerOutputs:
        # 循环提取每个框
        for detection in output:
            # 提取当前目标的类 ID 和置信度
            scores = detection[5:]
            classID = np.argmax(scores)
            confidence = scores[classID]
            # 通过确保检测概率大于最小概率来过滤弱预测
            if confidence > 0.5:
                # 将边界框坐标相对于图像的大小进行缩放,YOLO 返回的是边界框的中心(x, y)坐标,
                # 后面是边界框的宽度和高度
                box = detection[0:4] * np.array([W, H, W, H])
                (centerX, centerY, width, height) = box.astype("int")
                # 转换出边框左上角坐标
                x = int(centerX - (width / 2))
                y = int(centerY - (height / 2))
                # 更新边界框坐标、置信度和类 id 的列表
                boxes.append([x, y, int(width), int(height)])
                confidences.append(float(confidence))
                classIDs.append(classID)
    # 非最大值抑制,确定唯一边框
    idxs = cv2.dnn.NMSBoxes(boxes, confidences, 0.5, 0.3)
    # 确定每个对象至少有一个框存在
    if len(idxs) > 0:
        # 循环画出保存的边框
        for i in idxs.flatten():
            # 提取坐标和宽度
            (x, y) = (boxes[i][0], boxes[i][1])
            (w, h) = (boxes[i][2], boxes[i][3])
            # 画出边框和标签
            color = [int(c) for c in COLORS[classIDs[i]]]
            cv2.rectangle(image, (x, y), (x + w, y + h), color, 1, lineType=cv2.LINE_AA)
            text = "{}: {:.4f}".format(LABELS[classIDs[i]], confidences[i])
            cv2.putText(image, text, (x, y - 5), cv2.FONT_HERSHEY_SIMPLEX,
                0.5, color, 1, lineType=cv2.LINE_AA)
    cv2.imshow("Tag", image)
    cmd = cv2.waitKey(1)
    if cmd == ord('q'):
        break

cap.release()
cv2.destroyAllWindows()

cfg参数

出自 https://www.cnblogs.com/lh2n18/p/12986898.html

[net]
# Testing # 测试时,batch和subdivisions设置为1,否则可能出错。
#batch=1 # 大一些可以减小训练震荡及训练时NAN的出现。
#subdivisions=1 # 必须为为8的倍数,显存吃紧可以设成32或64。
# Training
batch=64 # 训练过程中将64张图一次性加载进内存,前向传播后将64张图的loss累加求平均,再一次性后向传播更新权重。
subdivisions=16 # 一个batch分16次完成前向传播,即每次计算4张。
width=608 # 网络输入的宽。
height=608 # 网络输入的高。
channels=3 # 网络输入的通道数。
momentum=0.949 # 动量梯度下降优化方法中的动量参数,更新的时候在一定程度上保留之前更新的方向。
decay=0.0005 # 权重衰减正则项,用于防止过拟合。
angle=0 # 数据增强参数,通过旋转角度来生成更多训练样本。
saturation = 1.5 # 数据增强参数,通过调整饱和度来生成更多训练样本。
exposure = 1.5 # 数据增强参数,通过调整曝光量来生成更多训练样本。
hue=.1 # 数据增强参数,通过调整色调来生成更多训练样本。
learning_rate=0.001 # 学习率。
burn_in=1000 # 在迭代次数小于burn_in时,学习率的更新为一种方式,大于burn_in时,采用policy的更新方式。
max_batches = 500500 #训练迭代次数,跑完一个batch为一次,一般为类别数*2000,训练样本少或train from scratch可适当增加。
policy=steps # 学习率调整的策略。
steps=400000,450000 # 动态调整学习率,steps可以取max_batches的0.8~0.9。
scales=.1,.1 # 迭代到steps(1)次时,学习率衰减十倍,steps(2)次时,学习率又会在前一个学习率的基础上衰减十倍。
#cutmix=1 # cutmix数据增强,将一部分区域cut掉但不填充0像素而是随机填充训练集中的其他数据的区域像素值,分类结果按一定的比例分配。
mosaic=1 # 马赛克数据增强,取四张图,随机缩放、随机裁剪、随机排布的方式拼接,详见上述代码分析。

https://kuekua.github.io/2017/10/20/YOLO算法学习及训练/

posted @ 2022-04-26 19:42  cc学习之路  阅读(342)  评论(0)    收藏  举报