经典目标检测YOLOV1理论基础的编码实现(通过编码理解原理)

2024-04-24 16:20

本文主要是介绍经典目标检测YOLOV1理论基础的编码实现(通过编码理解原理),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

正文:

1、前期准备

1.1  创建目录结构

1.2  数据集介绍与下载

1.2.1 数据集介绍

1.2.1 数据集下载

1.3. 数据集处理

1.4. 构建数据加载器 

1.4.1定义初始化方法

1.4.2 定义增强图片方法

1.4.3 定义编码器

1.4.4 完整代码

1.5、定义主函数

2构建网络

2.1 ResNet架构介绍

2.2 构建ResNet

2.2.1 构建Base_Block

__init__ 函数

forward 函数

2.2.2 构建Senior_Block

__init__ 初始化函数

forward 前向传播函数

2.2.3 构建Output_Block

__init__ 初始化函数

forward 前向传播函数

2.2.4 构建ResNet

__init__ 初始化函数

_make_layer 方法

_make_out_layer 方法

forward 方法

初始化模型参数部分

2.3 定义模型

2.4 完整代码

 3、定义损失

4、定义训练过程

5、定义预测方法

6、验证图片

7、完整代码和权重下载
​​​​​​​


       为了更详细的了解Yolo的实现原理,通过YOLO的代码形式进行一步一步说明,在阅读代码的过程中,按照下面的目录的步骤去看代码。但在看代码之前建议先去简单的了解YOLO的原理,在了解之后通过下面的内容进一步验证yolo的实现过程,同时也了解了ResNet的实现过程。

       YOLO的原理参考通道1

       YOLO的原理参考通道2

正文:

1、前期准备

准备好目录结构、数据集和关于YOLOv1的基础认知

1.1  创建目录结构

        自己创建项目目录结构,结构目录如下:

network                    CNN Backbone 存放位置
weights                    权重存放的位置
test_images             测试用的图片
utils                          辅助功能的代码存放位置 

models                    保存模型位置

data                         训练的数据集

1.2  数据集介绍与下载

1.2.1 数据集介绍

       首先了解数据集,对数据集了解后方便对数据进行相应处理。数据集详细介绍直通车:https://blog.csdn.net/qq_41946216/article/details/137683750?spm=1001.2014.3001.5501

1.2.1 数据集下载

       本次采用数据集: VOC2012数据集。

       数据集下载方式一:http://host.robots.ox.ac.uk/pascal/VOC/voc2012/VOCtrainval_11-May-2012.tar

       数据集下载方式二:

     下载并构建VOC2012数据集,从:https://gitee.com/ppov-nuc/pascal-vocdataset_-for_-yolo.git, 下载get_data文件generate_csv.py文件到本地,放到创建的目录结构中,修改get_data中下载的内容和相应路径,然后运行批处理文件get_data,在get_dat中会自动执行generate_csv.py,如下图所示。


1.3. 数据集处理

       在utils目录下创建工具类 generate_txt_file.py,主要用于数据集的划分和解析 Annotations/xxxxx.xml 文件中的类别bbox信息,并将信息存入voctrain.txt和voctest.txt文件,如下图所示:

具体代码:

# author: baiCai
# 1. 导包
from xml.etree import ElementTree as ET
import os
import random# 2. 定义一些基本的参数
# 定义所有的类名
VOC_CLASSES = ('aeroplane', 'bicycle', 'bird', 'boat','bottle', 'bus', 'car', 'cat', 'chair','cow', 'diningtable', 'dog', 'horse','motorbike', 'person', 'pottedplant','sheep', 'sofa', 'train', 'tvmonitor')'''
读取所有 xml 文件,存入列表
'''
# 要读取的xml文件路径,记得自己修改路径
Annotations = '../data/VOC2012/Annotations/'
# 列出所有的xml文件
xml_files = os.listdir(Annotations)
# 打乱数据集
random.shuffle(xml_files)
'''
定义训练集和测试比例
划分Annotations中的训练集和测试集文件列表
'''
# 训练集数量
train_num = int(len(xml_files) * 0.7)
# 训练列表
train_file_list = xml_files[:train_num]
# 测测试列表
test_file_list = xml_files[train_num:]'''
定义 xml 解析后的信息存储路径和写对象
'''
# 训练集和测试集文件名字
train_set_path = './voctrain.txt'
test_set_path = './voctest.txt'# 3. 定义解析xml文件的函数
'''
主要解析 xml 获取 类别名字和bbox,如 
{'name': 'person','bbox': [174, 101, 349, 351]}
'''
def parse_rec(filename):# 参数:输入xml文件名# 创建xml对象tree = ET.parse(filename)objects = []# 迭代读取xml文件中的object节点,即物体信息for obj in tree.findall('object'):obj_struct = {}# difficult属性,即这里不需要那些难判断的对象difficult = int(obj.find('difficult').text)if difficult == 1:  # 若为1则跳过本次循环continue# 开始收集信息obj_struct['name'] = obj.find('name').textbbox = obj.find('bndbox')obj_struct['bbox'] =\[int(float(bbox.find('xmin').text)),int(float(bbox.find('ymin').text)),int(float(bbox.find('xmax').text)),int(float(bbox.find('ymax').text))]objects.append(obj_struct)return objects# 4. 把信息保存入文件中
def write_txt(file_list,set_path):# # 生成训练集txtcount = 0with  open(set_path, 'w') as wt:for xml_file in file_list:count += 1# 获取图片名字image_name = xml_file.split('.')[0] + '.jpg'  # 图片文件名# 对xml_file进行解析results = parse_rec(Annotations + xml_file)# 如果返回的对象为空,表示张图片难以检测,因此直接跳过if len(results) == 0:print(xml_file)continue# 否则,则写入文件中# 先写入图片名字wt.write(image_name)# 接着指定下面写入的格式for result in results:class_name = result['name']bbox = result['bbox']class_name = VOC_CLASSES.index(class_name)  # 名字在类别中是下标位置wt.write(' ' + str(bbox[0]) +' ' + str(bbox[1]) +' ' + str(bbox[2]) +' ' + str(bbox[3]) +' ' + str(class_name))wt.write('\n')wt.close()# 5. 运行
if __name__ == '__main__':write_txt(train_file_list,train_set_path)write_txt(test_file_list,test_set_path)

1.4. 构建数据加载器 

      在utils目录下创建工具类 yolo_dataset.py,主要用于数据集的构建,包含初始化、图片增强及归一化等功能。具体代码在下面完整代码中。

1.4.1定义初始化方法

       读取xxxx.xml解析后的文件
       对每行数据(每个图片信息)的所有中心点信息以【x,y,w,h】和标签分别存入box列表和label列表。
       当前图片的边界框和标签信息即box列表和label列表,转换为LongTensor格式添加到对应的boxex列表和labels列表。

1.4.2 定义增强图片方法

       增加方法如下列表:

增加方法名称定义的函数
随机翻转图片和边界框random_flip(img, boxes)
随机缩放图片和边界框randomScale(img, boxes)
随机模糊图片randomBlur(img)
随机调整图片亮度RandomBrightness(img)
随机调整图片色调RandomHue(img)
随机调整图片饱和度RandomSaturation(img)
随机移动图片和边界框randomShift(img, boxes, labels)        
随机裁剪图片和边界框randomCrop(img, boxes, labels)
用于从图像中减去均值subMean(self, bgr, mean)
将BGR图像转换为RGB图像BGR2RGB(self, img)
将BGR图像转换为HSV图像BGR2HSV(self, img)
将HSV图像转换为BGR图像HSV2BGR(self, img)

1.4.3 定义编码器

        定义编码器主要目的是用于将边界框(归一化后的边界框信息)和标签编码为目标张量。

1.4.4 完整代码

# author: baiCai
# 导入所需的库
import os
import random
import numpy as np
import torch
import torchvision.transforms as T
from torch.utils.data import Dataset
import cv2# 定义一个Yolo数据加载器类
class Yolo_Dataset(Dataset):# 初始化默认的图片大小image_size = 448def __init__(self, root, list_file, train=True, transforms=None):''"""初始化函数:param root: 图片的根目录:param list_file: 包含图片路径和标注信息的txt文件:param train: 是否为训练集,默认为True:param transforms: 预处理方法,默认为None"""# 保存传入的参数self.root = rootself.train = trainself.transform = transformsself.fnames = []  # 存储图片文件名self.boxes = []  # 存储边界框信息self.labels = []  # 存储标签信息self.mean = (123, 117, 104)  # RGB通道的均值,用于归一化# 打开并读取txt文件with open(list_file) as f:lines = f.readlines()  # 读取所有行# 遍历文件中的每一行for line in lines:splited = line.strip().split()  # 去除行尾的空格并按空格分割self.fnames.append(splited[0])  # 添加图片文件名到列表中# 计算当前行包含的对象数量,每五个数据表示一个对象num_boxes = (len(splited) - 1) // 5# 初始化当前图片的边界框和标签列表box = []label = []# 遍历当前行的所有对象for i in range(num_boxes):# 读取边界框的坐标信息x = float(splited[1 + 5 * i])y = float(splited[2 + 5 * i])x2 = float(splited[3 + 5 * i])y2 = float(splited[4 + 5 * i])# 读取对象的类别标签c = splited[5 + 5 * i]# 将边界框坐标添加到列表中,并转换为Tensor格式box.append([x, y, x2, y2])# 将标签转换为整数并加1(因为标签通常从1开始计数),然后添加到列表中label.append(int(c) + 1)# 将当前图片的边界框和标签信息转换为LongTensor格式添加到对应的列表中self.boxes.append(torch.Tensor(box))self.labels.append(torch.LongTensor(label))# 记录数据集中的样本数量self.num_samples = len(self.boxes)def __len__(self):# 返回数据集中的样本数量return self.num_samples# 如果是训练模式,需要进行图像的增强,idx表示每个图像在fnames列表中索引def __getitem__(self, idx):# 根据索引获取图片的文件名fname = self.fnames[idx]# 拼接完整的图片路径并读取图片img = cv2.imread(os.path.join(self.root, fname))# 获取当前图片的边界框和标签信息boxes = self.boxes[idx].clone()labels = self.labels[idx].clone()# 如果是训练模式,需要进行图像的增强# 需要注意的是,同时处理图像和boxif self.train:# 随机翻转图片和边界框img, boxes = self.random_flip(img, boxes)# 随机缩放图片和边界框img, boxes = self.randomScale(img, boxes)# 随机模糊图片img = self.randomBlur(img)# 随机调整图片亮度img = self.RandomBrightness(img)# 随机调整图片色调img = self.RandomHue(img)# 随机调整图片饱和度img = self.RandomSaturation(img)# 随机移动图片和边界框img, boxes, labels = self.randomShift(img, boxes, labels)# 随机裁剪图片和边界框img, boxes, labels = self.randomCrop(img, boxes, labels)# 获取图片的高度、宽度和通道数h, w, _ = img.shape# 将边界框坐标除以图片宽高,进行归一化boxes /= torch.Tensor([w, h, w, h]).expand_as(boxes)# 由于cv2读取图片为BGR格式,需要转换为RGB格式img = self.BGR2RGB(img)# 从图片中减去RGB通道的均值img = self.subMean(img, self.mean)# 将图片缩放到指定的大小,这里是448x448img = cv2.resize(img, (self.image_size, self.image_size))# 将边界框和标签信息编码为YOLOv1需要的格式,即7x7x30的张量target = self.encoder(boxes, labels)# 应用预定义的数据变换for t in self.transform:img = t(img)# 返回处理后的图片和编码后的目标信息return img, target# 定义一个encoder方法,用于将边界框和标签编码为目标张量def encoder(self, boxes, labels):'''将边界框和标签编码为7x7x30的目标张量参数:boxes (tensor): 形状为[[x1,y1,x2,y2],[]]的边界框张量labels (tensor): 标签张量返回:target (tensor): 形状为7x7x30的目标张量'''grid_num = 7  # 定义网格数量# 创建一个形状为(grid_num, grid_num, 30)的全零张量作为目标张量target = torch.zeros((grid_num, grid_num, 30))# 计算每个网格的缩放因子cell_size = 1. / grid_numprint(cell_size)print(boxes)# 计算边界框的宽度、高度和中心点坐标,boxes 是归一化后数据wh = boxes[:, 2:] - boxes[:, :2]# 即 xmin ymin ,xmax,ymax 中心点坐标  ((xmax-xmin)/2,(ymax-ymin)/2))cxcy = (boxes[:, 2:] + boxes[:, :2]) / 2# 遍历每个边界框的中心点for i in range(cxcy.size()[0]):cxcy_sample = cxcy[i]  # 获取当前边界框的中心点坐标# 计算中心点所在的网格索引,并进行向上取整和减一操作ij = (cxcy_sample / cell_size).ceil() - 1# 标记目标张量中对应网格的objectness为1target[int(ij[1]), int(ij[0]), 4] = 1# 标记目标张量中对应网格的类别为1target[int(ij[1]), int(ij[0]), 9] = 1# 标记目标张量中对应网格的标签位置为1target[int(ij[1]), int(ij[0]), int(labels[i]) + 9] = 1# 计算中心点在当前网格内的相对坐标xy = ij * cell_size# 计算中心点相对于网格左上角的偏移量delta_xy = (cxcy_sample - xy) / cell_size# 填充目标张量中对应网格的宽度、高度信息target[int(ij[1]), int(ij[0]), 2:4] = wh[i]# 填充目标张量中对应网格的中心点偏移量信息target[int(ij[1]), int(ij[0]), :2] = delta_xy# 重复填充宽度、高度信息到目标张量的其他位置target[int(ij[1]), int(ij[0]), 7:9] = wh[i]# 重复填充中心点偏移量信息到目标张量的其他位置target[int(ij[1]), int(ij[0]), 5:7] = delta_xy# 返回填充好的目标张量return target# 以下是各种图像预处理方法的定义# 将BGR图像转换为RGB图像def BGR2RGB(self, img):return cv2.cvtColor(img, cv2.COLOR_BGR2RGB)# 将BGR图像转换为HSV图像def BGR2HSV(self, img):return cv2.cvtColor(img, cv2.COLOR_BGR2HSV)# 将HSV图像转换为BGR图像def HSV2BGR(self, img):return cv2.cvtColor(img, cv2.COLOR_HSV2BGR)# 随机调整图像的亮度def RandomBrightness(self, bgr):# 以0.5的概率调整图像的亮度if random.random() < 0.5:hsv = self.BGR2HSV(bgr)  # 将BGR图像转换为HSV图像h, s, v = cv2.split(hsv)  # 分割HSV图像的通道# 随机选择亮度调整系数adjust = random.choice([0.5, 1.5])v = v * adjust  # 调整亮度通道的值# 确保亮度值在0到255之间v = np.clip(v, 0, 255).astype(hsv.dtype)# 合并通道得到调整后的HSV图像hsv = cv2.merge((h, s, v))# 将调整后的HSV图像转换回BGR图像bgr = self.HSV2BGR(hsv)return bgr# 定义一个随机调整图像饱和度的方法def RandomSaturation(self, bgr):# 以0.5的概率决定是否调整图像的饱和度if random.random() < 0.5:# 将BGR图像转换为HSV图像hsv = self.BGR2HSV(bgr)# 分割HSV图像的通道h, s, v = cv2.split(hsv)# 随机选择饱和度调整系数adjust = random.choice([0.5, 1.5])# 调整饱和度通道的值s = s * adjust# 确保饱和度值在0到255之间s = np.clip(s, 0, 255).astype(hsv.dtype)# 合并通道得到调整后的HSV图像hsv = cv2.merge((h, s, v))# 将调整后的HSV图像转换回BGR图像bgr = self.HSV2BGR(hsv)# 返回调整后的BGR图像return bgr# 定义一个随机调整图像色调的方法# 随机调整图片色调def RandomHue(self, bgr):# 以0.5的概率决定是否调整图像的色调if random.random() < 0.5:# 将BGR图像转换为HSV图像hsv = self.BGR2HSV(bgr)# 分割HSV图像的通道h, s, v = cv2.split(hsv)# 随机选择色调调整系数adjust = random.choice([0.5, 1.5])# 调整色调通道的值h = h * adjust# 确保色调值在0到179之间(因为色调是循环的)h = h % 180# 确保色调值的数据类型与hsv图像一致h = h.astype(hsv.dtype)# 合并通道得到调整后的HSV图像hsv = cv2.merge((h, s, v))# 将调整后的HSV图像转换回BGR图像bgr = self.HSV2BGR(hsv)# 返回调整后的BGR图像return bgr# 定义一个随机模糊图像的方法def randomBlur(self, bgr):# 以0.5的概率决定是否对图像进行模糊处理if random.random() < 0.5:# 使用cv2.blur函数对图像进行模糊处理,核大小为(5, 5)bgr = cv2.blur(bgr, (5, 5))# 返回模糊处理后的图像或原图像return bgr# 定义一个随机平移图像的方法,同时更新边界框和标签的位置def randomShift(self, bgr, boxes, labels):# 计算边界框的中心点坐标center = (boxes[:, 2:] + boxes[:, :2]) / 2# 以0.5的概率决定是否对图像进行平移if random.random() < 0.5:# 获取图像的高度、宽度和通道数height, width, c = bgr.shape# 创建一个与原图同样大小的零矩阵,并填充为特定的BGR值(这里可能用于填充平移后的空白区域)after_shfit_image = np.zeros((height, width, c), dtype=bgr.dtype)after_shfit_image[:, :, :] = (104, 117, 123)  # 填充BGR颜色值# 生成随机的x和y方向的平移量,限制在图像尺寸的20%以内shift_x = random.uniform(-width * 0.2, width * 0.2)shift_y = random.uniform(-height * 0.2, height * 0.2)# 根据平移量的正负值,对图像进行不同方向的平移if shift_x >= 0 and shift_y >= 0:# 当x和y都大于等于0时,从原图的右下角开始平移after_shfit_image[int(shift_y):, int(shift_x):, :] = bgr[:height - int(shift_y), :width - int(shift_x),:]elif shift_x >= 0 and shift_y < 0:# 当x大于等于0且y小于0时,从原图的右上方开始平移after_shfit_image[:height + int(shift_y), int(shift_x):, :] = bgr[-int(shift_y):, :width - int(shift_x),:]elif shift_x < 0 and shift_y >= 0:# 当x小于0且y大于等于0时,从原图的左下方开始平移after_shfit_image[int(shift_y):, :width + int(shift_x), :] = bgr[:height - int(shift_y), -int(shift_x):,:]elif shift_x < 0 and shift_y < 0:# 当x和y都小于0时,从原图的左上方开始平移after_shfit_image[:height + int(shift_y), :width + int(shift_x), :] = bgr[-int(shift_y):,-int(shift_x):, :]# 将平移量转换为Tensor格式,并扩展到与中心点坐标相同的形状shift_xy = torch.FloatTensor([[int(shift_x), int(shift_y)]]).expand_as(center)# 更新中心点坐标,加上平移量center = center + shift_xy# 创建掩码,确保更新后的中心点坐标在图像尺寸范围内mask1 = (center[:, 0] > 0) & (center[:, 0] < width)mask2 = (center[:, 1] > 0) & (center[:, 1] < height)# 合并两个掩码,得到同时满足两个条件的掩码mask = (mask1 & mask2).view(-1, 1)# 根据掩码筛选出更新后仍在图像内的边界框boxes_in = boxes[mask.expand_as(boxes)].view(-1, 4)# 如果经过平移后,没有边界框留在图像内,则直接返回原图像、原边界框和原标签if len(boxes_in) == 0:return bgr, boxes, labels# 创建一个Tensor,用于存储平移量,其中x和y的平移量分别对应边界框的左上角和右下角box_shift = torch.FloatTensor([[int(shift_x), int(shift_y), int(shift_x), int(shift_y)]]).expand_as(boxes_in)# 将平移量加到更新后的边界框上,注意这里是对边界框的四个坐标都进行了平移boxes_in = boxes_in + box_shift# 根据掩码筛选出更新后仍在图像内的标签labels_in = labels[mask.view(-1)]# 返回处理后的图像、更新后的边界框和标签return after_shfit_image, boxes_in, labels_in# 如果不需要进行平移,或者平移后逻辑没有进入if条件块,则直接返回原图像、原边界框和原标签return bgr, boxes, labels# 定义randomScale方法,用于对图像进行随机尺度变换def randomScale(self, bgr, boxes):# 如果随机生成的小于0.5的数,则执行尺度变换if random.random() < 0.5:# 生成0.8到1.2之间的随机尺度因子scale = random.uniform(0.8, 1.2)# 获取图像的高度、宽度和通道数height, width, c = bgr.shape# 使用cv2的resize函数,固定高度不变,按照生成的尺度因子调整宽度bgr = cv2.resize(bgr, (int(width * scale), height))# 创建一个Tensor,用于存储尺度因子,扩展为与boxes相同的形状scale_tensor = torch.FloatTensor([[scale, 1, scale, 1]]).expand_as(boxes)# 将尺度因子应用到中心点信息boxes上,进行尺度变换boxes = boxes * scale_tensor# 返回尺度变换后的图像和边界框return bgr, boxes# 如果不进行尺度变换,则返回原图像和边界框return bgr, boxes# 定义randomCrop方法,用于对图像进行随机裁剪# 随机裁剪图片和边界框def randomCrop(self, bgr, boxes, labels):# 如果随机生成的小于0.5的数,则执行随机裁剪if random.random() < 0.5:# 计算边界框的中心点center = (boxes[:, 2:] + boxes[:, :2]) / 2# 获取图像的高度、宽度和通道数height, width, c = bgr.shape# 生成随机裁剪的高度和宽度,至少为原图像的60%h = random.uniform(0.6 * height, height)w = random.uniform(0.6 * width, width)# 生成随机裁剪的起始坐标x = random.uniform(0, width - w)y = random.uniform(0, height - h)# 将坐标和裁剪的尺寸转换为整数x, y, h, w = int(x), int(y), int(h), int(w)# 将裁剪起始坐标转换为Tensor,并从中心点中减去center = center - torch.FloatTensor([[x, y]]).expand_as(center)# 创建掩码,筛选出中心点落在裁剪区域内的边界框mask1 = (center[:, 0] > 0) & (center[:, 0] < w)mask2 = (center[:, 1] > 0) & (center[:, 1] < h)# 将两个掩码合并,并展平为一维Tensormask = (mask1 & mask2).view(-1, 1)# 根据掩码筛选出裁剪区域内的边界框boxes_in = boxes[mask.expand_as(boxes)].view(-1, 4)# 如果没有边界框落在裁剪区域内,则返回原图像、原边界框和原标签if (len(boxes_in) == 0):return bgr, boxes, labels# 创建一个Tensor,用于存储裁剪起始坐标,扩展为与boxes_in相同的形状box_shift = torch.FloatTensor([[x, y, x, y]]).expand_as(boxes_in)# 将裁剪起始坐标从边界框中减去boxes_in = boxes_in - box_shift# 确保裁剪后的边界框坐标在裁剪区域内boxes_in[:, 0] = boxes_in[:, 0].clamp_(min=0, max=w)boxes_in[:, 2] = boxes_in[:, 2].clamp_(min=0, max=w)boxes_in[:, 1] = boxes_in[:, 1].clamp_(min=0, max=h)boxes_in[:, 3] = boxes_in[:, 3].clamp_(min=0, max=h)# 根据掩码筛选出裁剪区域内的标签labels_in = labels[mask.view(-1)]# 裁剪图像img_croped = bgr[y:y + h, x:x + w, :]# 返回裁剪后的图像、裁剪区域内的边界框和标签return img_croped, boxes_in, labels_in# 如果不进行裁剪,则返回原图像、原边界框和原标签return bgr, boxes, labels# 定义subMean方法,用于从图像中减去均值def subMean(self, bgr, mean):# 将均值转换为float32类型的numpy数组mean = np.array(mean, dtype=np.float32)# 从图像bgr中减去均值bgr = bgr - mean# 返回减去均值后的图像return bgr# 定义random_flip方法,用于随机对图像进行水平翻转,并相应地更新边界框# 随机翻转图片和边界框def random_flip(self, im, boxes):# 以0.5的概率决定是否进行翻转if random.random() < 0.5:# 对图像进行水平翻转im_lr = np.fliplr(im).copy()# 获取图像的高度、宽度h, w, _ = im.shape# 更新边界框的左边界为原右边界与图像宽度的差值xmin = w - boxes[:, 2]# 更新边界框的右边界为原左边界与图像宽度的差值xmax = w - boxes[:, 0]# 将更新后的左右边界赋值回边界框boxes[:, 0] = xminboxes[:, 2] = xmax# 返回翻转后的图像和更新后的边界框return im_lr, boxes# 如果不进行翻转,则返回原图像和原边界框return im, boxes# 定义random_bright方法,用于随机调整图像的亮度def random_bright(self, im, delta=16):# 生成一个0到1之间的随机数alpha = random.random()# 如果该随机数大于0.3if alpha > 0.3:# 调整图像的亮度,乘以随机数alpha并加上一个-delta到delta之间的随机整数im = im * alpha + random.randrange(-delta, delta)# 将调整亮度后的图像像素值限制在0到255之间,并转换为uint8类型im = im.clip(min=0, max=255).astype(np.uint8)# 返回调整亮度后的图像return im# 3. 调试代码
def main():from torch.utils.data import DataLoaderimport torchvision.transforms as transformsfile_root = '../data/VOC2012/JPEGImages/' # 记得改为自己的路径train_dataset = Yolo_Dataset(root=file_root,list_file='voctrain.txt',train=True,transforms = [T.ToTensor()] )train_loader = DataLoader(train_dataset,batch_size=1,shuffle=False,num_workers=0)train_iter = iter(train_loader)for i in range(100):img,target = next(train_iter)print(img.shape)print(target.shape)breakif __name__ == '__main__':main()

1.5、定义主函数

       在utils目录下创建工具类 yolo_dataset.py,中定义主函数进行测试,包含设置和加载一个自定义的Yolo_Dataset数据集,该数据集来源于VOC2012的JPEGImages文件夹。代码中首先导入了必要的模块和类,然后定义了数据集的根目录和预处理操作。接着,创建了一个Yolo_Dataset对象,并通过DataLoader类将其加载为可迭代的数据集。DataLoader的参数包括数据集、批次大小、是否打乱数据以及使用的子进程数量。最后,通过迭代器遍历数据集,获取图像和对应的目标数据,并打印它们的维度信息。

# 3. 调试代码
# 定义一个名为main的函数
def main():# 从torch.utils.data模块中导入DataLoader类,用于加载数据集from torch.utils.data import DataLoader# 导入torchvision.transforms模块,用于图像预处理import torchvision.transforms as transforms# 设置文件根目录路径,这里假设是VOC2012数据集的JPEGImages文件夹file_root = '../data/VOC2012/JPEGImages/'  # 记得改为自己的路径# 创建一个Yolo_Dataset对象,该对象可能是自定义的,用于加载VOC2012数据集# 参数包括:# root: 数据集的根目录# list_file: 包含训练图像文件名的列表文件# train: 是否为训练模式# transforms: 图像预处理操作列表,这里只有一个操作,即将图像转换为Tensortrain_dataset = Yolo_Dataset(root=file_root, list_file='voctrain.txt', train=True,transforms=[transforms.ToTensor()])# 使用DataLoader类加载训练数据集# 参数包括:# dataset: 要加载的数据集# batch_size: 每个批次的大小# shuffle: 是否在每个epoch开始时打乱数据# num_workers: 加载数据使用的子进程数量train_loader = DataLoader(train_dataset, batch_size=1, shuffle=False, num_workers=0)# 创建一个迭代器,用于遍历train_loader中的数据train_iter = iter(train_loader)# 遍历前100个批次的数据(但由于下面的break语句,实际上只会处理一个批次)for i in range(100):# 使用迭代器获取下一个批次的数据,包括图像和目标(可能是标签或边界框)img, target = next(train_iter)# 打印图像的shape(维度)print(img.shape)# 打印目标的shape(维度)print(target.shape)# 跳出循环,因此只处理一个批次的数据breakif __name__ == '__main__':main()

2构建网络

       在YOLO(You Only Look Once)系列的目标检测算法中,Backbone是一个核心组成部分,主要负责提取输入图像的特征。具体来说,Backbone是一种卷积神经网络,它通过多层次的特征提取,从低层次到高层次逐渐提取出图像的不同特征,如边缘、纹理、形状等,最终生成一个高维度的特征向量。这些特征向量在后续的网络层中被用于目标检测和分类等任务。

       在YOLOv1中,大部分人都选用的ResNet作为自己的backbone,因为可以方便调用官方的预训练权重。

2.1 ResNet架构介绍

      ResNet架构使用残差的模式,即输出值为F(x)+x模式,简单理解就是至少不比上次结果差。不了解可以参考其他人博客.

2.2 构建ResNet

       在创建的目录结构network中创建Yolo_ResNet.py文件,用于创建 Base_Block、Senior_Block、Output_Block 和 ResNet。

      ResNet是一个基于残差块(Block)的卷积神经网络结构。它使用多个层级,每个层级包含多个残差块(Block)。构建残差块时候需要注意的是,ResNet18—ResNet152共有两种类型的Block块,一种不涉及1*1卷积网络,另外一种涉及1*1卷积网络,如下图所示:

       自己实现Block时候也要有涉及1*1卷积网络 和 不涉及1*1卷积网络 两种方式。

2.2.1 构建Base_Block

       Base_Block类是一个神经网络的基础块,它定义了一个具有残差连接的结构,这在深度神经网络中(如ResNet)是非常常见的。这个类中不包含 1*1 的卷积,以下是该类中几个函数的主要作用:

__init__ 函数
  • 作用:初始化Base_Block对象。
  • 功能
    • 接收输入通道数(in_planes)、输出通道数(out_planes)、步长(stride)以及下采样函数(downsample)作为参数。
    • 定义并初始化两个卷积层(conv1 和 conv2)、两个批量归一化层(bn1 和 bn2)以及ReLU激活函数(relu)。其中,conv1 和 conv2  使用 3x3 的卷积核。
    • 保存传入的下采样函数(downsample)和步长(stride)以便后续使用。
forward 函数
  • 作用:定义网络块的前向传播过程。
  • 功能
    • 接收输入数据x
    • 保留输入x作为残差(res),用于后续与卷积处理后的输出相加。
    • 将输入x通过两个卷积层、两个批量归一化层以及ReLU激活函数。
    • 如果提供了下采样函数,则对残差res进行下采样,使其维度与卷积后的输出一致。
    • 将卷积处理后的输出与残差res相加,实现残差连接。
    • 对相加后的结果再次应用ReLU激活函数。
    • 返回前向传播的结果。
'''
# 2. 构建block: 不含有1*1
# 定义一个基础块类,继承自nn.Module,用于构建神经网络的基础结构  
'''
class Base_Block(nn.Module):# 用于扩充的变量,表示扩大几倍# 在某些网络结构中,如ResNet的Bottleneck块,可能需要通过该变量来扩充通道数expansion = 1def __init__(self, in_planes, out_planes, stride=1, downsample=None):# 初始化函数,传入输入通道数、输出通道数、步长以及下采样函数''':param in_planes: 输入的通道数:param planes: 输出的通道数:param stride: 默认步长:param downsample: 是否进行下采样'''# 调用父类nn.Module的初始化函数super(Base_Block, self).__init__()# 定义网络结构 + 初始化参数# 第一个卷积层self.conv1 = nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride, padding=1, bias=False)# 第一个批量归一化层self.bn1 = nn.BatchNorm2d(out_planes)# ReLU激活函数,inplace=True表示直接改变原始参数值,节省内存self.relu = nn.ReLU(inplace=True)# 第二个卷积层self.conv2 = nn.Conv2d(out_planes, out_planes, kernel_size=3, stride=1, padding=1, bias=False)# 第二个批量归一化层self.bn2 = nn.BatchNorm2d(out_planes)# 下采样函数,用于调整输入x的维度,以便与卷积后的输出维度一致self.downsample = downsample# 存储步长,以便后续可能用到self.stride = stride# 定义前向传播函数def forward(self, x):# 残差,即直接输入x,后续会与卷积处理后的输出相加res = x# 正常传播# 通过第一个卷积层out = self.conv1(x)# 通过第一个批量归一化层out = self.bn1(out)# 通过ReLU激活函数out = self.relu(out)# 通过第二个卷积层out = self.conv2(out)# 通过第二个批量归一化层out = self.bn2(out)# 判断是否下采样# 如果需要下采样,则调整残差res的维度if self.downsample is not None:res = self.downsample(res)# 残差相加# 将残差res与卷积处理后的输出out相加,实现残差连接out += res# 再次通过ReLU激活函数out = self.relu(out)# 返回结果return out

2.2.2 构建Senior_Block

        Senior_Block 类是用于构建更复杂的网络结构。类中包含1*1卷积核3*3卷积,下面是该类中几个函数的主要作用:

__init__ 初始化函数
  • 作用:初始化 Senior_Block 对象,设置网络层的结构和参数。
  • 功能
    • 接收输入通道数(in_planes)、中间通道数(planes)、步长(stride)以及下采样方法(downsample)作为参数。
    • 定义三个卷积层(conv1conv2conv3)和对应的批量归一化层(bn1bn2bn3)。其中,conv1 和 conv3 使用 1x1 的卷积核,conv2 使用 3x3 的卷积核。
    • 定义 ReLU 激活函数(relu)。
    • 保存传入的下采样方法(downsample)和步长(stride)以便后续使用。
forward 前向传播函数
  • 作用:定义网络块的前向传播过程,即数据从输入到输出的计算流程。
  • 功能
    • 接收输入数据 x
    • 保留输入 x 作为残差(res),用于后续与卷积处理后的输出相加。
    • 将输入 x 通过三个卷积层(conv1conv2conv3)和对应的批量归一化层(bn1bn2bn3),以及 ReLU 激活函数。
    • 如果提供了下采样方法,则对残差 res 进行下采样,使其维度与卷积后的输出一致。
    • 将卷积处理后的输出与残差 res 相加,实现残差连接。
    • 对相加后的结果再次应用 ReLU 激活函数。
    • 返回前向传播的结果。
'''
# 3. 构建Block:含有1*1
# 定义高级块类,继承自nn.Module,用于构建更复杂的网络结构 
'''
class Senior_Block(nn.Module):expansion = 4def __init__(self, in_planes, planes, stride=1, downsample=None):''':param in_planes: 输入通道数:param planes: 中间通道数,最终的输出通道数还需要乘以扩大系数,即expansion:param stride: 步长:param downsample: 下采样方法'''super(Senior_Block, self).__init__()self.conv1 = nn.Conv2d(in_planes, planes, kernel_size=1, bias=False)self.bn1 = nn.BatchNorm2d(planes)self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=stride, padding=1, bias=False)self.bn2 = nn.BatchNorm2d(planes)self.conv3 = nn.Conv2d(planes, planes * 4, kernel_size=1, bias=False)self.bn3 = nn.BatchNorm2d(planes * 4)self.relu = nn.ReLU(inplace=True)self.downsample = downsampleself.stride = stridedef forward(self, x):# 残差res = x# 前向传播out = self.conv1(x)out = self.bn1(out)out = self.relu(out)out = self.conv2(out)out = self.bn2(out)out = self.relu(out)out = self.conv3(out)out = self.bn3(out)# 是否下采样if self.downsample is not None:res = self.downsample(x)# 相加out += resout = self.relu(out)return out

2.2.3 构建Output_Block

       Output_Block 类是用于构建神经网络的一部分。下面是该类中几个函数的主要作用:

__init__ 初始化函数
  • 作用:初始化 Output_Block 对象,设置网络层的结构和参数。
  • 功能
    • 接收输入通道数(in_planes)、中间通道数(planes)、步长(stride)以及块类型(block_type)作为参数。
    • 定义三个卷积层(conv1conv2conv3)和对应的批量归一化层(bn1bn2bn3)。其中,conv1 和 conv3 使用 1x1 的卷积核,conv2 使用 3x3 的卷积核。。
    • 根据步长、输入通道数和块类型,判断是否需要进行下采样,并初始化下采样层(downsample)。
forward 前向传播函数
  • 作用:定义网络块的前向传播过程,即数据从输入到输出的计算流程。
  • 功能
    • 接收输入数据 x
    • 将输入 x 通过三个卷积层(conv1conv2conv3)和对应的批量归一化层(bn1bn2bn3),并在每个卷积层之后应用 ReLU 激活函数。
    • 将输入 x 通过下采样层(downsample),以便与卷积处理后的输出 out 的维度相匹配。
    • 将卷积处理后的输出 out 与下采样后的输入相加,实现残差连接。
    • 返回前向传播的结果。

'''
# 4. 构建输出层
# 定义输出块类,继承自nn.Module,用于构建更复杂的网络结构 
'''
class Output_Block(nn.Module):# 定义扩大系数,该块最终输出的通道数将与中间通道数相同expansion = 1def __init__(self, in_planes, planes, stride=1, block_type='A'):# 初始化函数,传入输入通道数、中间通道数、步长以及块类型''':param in_planes: 输入通道数:param planes: 中间通道数:param stride: 步长:param block_type: 块类型,为'A'表示不需要下采样,为'B'则需要'''# 调用父类nn.Module的初始化函数super(Output_Block, self).__init__()# 定义第一个卷积层,用于改变通道数self.conv1 = nn.Conv2d(in_planes, planes, kernel_size=1, bias=False)# 定义第一个批量归一化层self.bn1 = nn.BatchNorm2d(planes)# 定义第二个卷积层,使用3x3的卷积核,带有2的空洞率(dilation),可以增大感受野self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=stride, padding=2, bias=False, dilation=2)# 定义第二个批量归一化层self.bn2 = nn.BatchNorm2d(planes)# 定义第三个卷积层,用于调整通道数至最终的输出通道数self.conv3 = nn.Conv2d(planes, self.expansion * planes, kernel_size=1, bias=False)# 定义第三个批量归一化层self.bn3 = nn.BatchNorm2d(self.expansion * planes)# 初始化下采样层,默认是一个空的序列self.downsample = nn.Sequential()# 判断是否需要下采样,这里综合了步长、通道数以及块类型if stride != 1 or in_planes != self.expansion * planes or block_type == 'B':# 如果需要下采样,则设置下采样层self.downsample = nn.Sequential(# 定义一个卷积层,用于调整输入的通道数和维度nn.Conv2d(in_planes, self.expansion * planes, kernel_size=1, stride=stride, bias=False),# 定义一个批量归一化层nn.BatchNorm2d(self.expansion * planes))# 定义前向传播函数def forward(self, x):# 通过第一个卷积层和批量归一化层,并应用ReLU激活函数out = F.relu(self.bn1(self.conv1(x)))# 通过第二个卷积层和批量归一化层,并再次应用ReLU激活函数out = F.relu(self.bn2(self.conv2(out)))# 通过第三个卷积层和批量归一化层out = self.bn3(self.conv3(out))# 将输入x通过下采样层,以便与out的维度相匹配# 如果不需要下采样,则下采样层是一个空操作out += self.downsample(x)# 应用ReLU激活函数out = F.relu(out)# 返回输出结果return out

2.2.4 构建ResNet

   ResNet类定义了一个基于残差块(block)的卷积神经网络结构。它使用了多个层级(layer1layer4)和一个输出层(layer5),以及一个平均池化层和一个最终的卷积层。下面是该类中几个函数的主要作用:

__init__ 初始化函数
  • 作用:初始化ResNet对象,设置网络的结构和参数。
  • 功能
    • 设置初始通道数self.inplanes为64。
    • 定义初始卷积层self.conv1,用于从输入图像中提取特征。
    • 定义批标准化层self.bn1和ReLU激活函数self.relu,用于处理conv1的输出。
    • 定义最大池化层self.maxpool,用于进一步减小特征图的尺寸。
    • 使用_make_layer方法创建四个层级(layer1layer4),每个层级包含多个残差块。
    • 使用_make_out_layer方法创建输出层self.layer5
    • 定义平均池化层self.avgpool,用于减小特征图的尺寸。
    • 定义最终的卷积层self.conv_end和批标准化层self.bn_end,用于输出层的处理。
    • 初始化模型参数,使用正态分布初始化卷积层的权重,并将批标准化层的权重初始化为1,偏置初始化为0。
_make_layer 方法
  • 作用:根据输入的block类型、输出通道数、block数量和步长,创建并返回一个层级(layer)。
  • 功能
    • 循环创建指定数量的block对象,并将它们添加到一个nn.Sequential容器中。
    • 在第一个block中,根据步长设置是否进行下采样。
    • 返回包含多个block的层级。
_make_out_layer 方法
  • 作用:创建输出层。
  • 功能
    • 根据输入的通道数,定义输出层的结构。
    • 输出层的具体结构在代码中没有给出,但通常会包含卷积层、批标准化层等。
forward 方法
  • 作用:定义网络的前向传播过程,即从输入到输出的计算流程。
  • 功能
    • 接收输入数据,并通过初始卷积层、批标准化层、ReLU激活函数和最大池化层进行处理。
    • 将处理后的数据依次通过四个层级(layer1layer4)和输出层(layer5)。
    • 通过平均池化层减小特征图的尺寸。
    • 通过最终的卷积层和批标准化层进行处理,得到网络的输出。
初始化模型参数部分
  • 作用:对模型中的参数进行初始化。
  • 功能
    • 遍历模型中的所有模块。
    • 对于卷积层,使用正态分布初始化权重,标准差根据卷积核的大小和输出通道数计算得出。
    • 对于批标准化层,将权重初始化为1,偏置初始化为0。
'''
# 5. 构建ResNet
# 定义ResNet类,继承自nn.Module,用于构建更复杂的网络结构 
'''
class ResNet(nn.Module):def __init__(self, block, layers):'''初始化ResNet网络。:param block: 基本的Block块对象,如BasicBlock或Bottleneck。:param layers: 不同层级的block数量,例如ResNet50为[3, 4, 6, 3]。'''super(ResNet, self).__init__()  # 调用父类nn.Module的初始化方法。# 初始通道数设置为64self.inplanes = 64# 初始卷积层,输入通道为3(RGB图像),输出通道为64,卷积核大小为7x7,步长为2,填充为3,无偏置。self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False)# 批标准化层,用于conv1的输出self.bn1 = nn.BatchNorm2d(64)# ReLU激活函数,inplace=True表示直接在原变量上进行操作,节省内存。self.relu = nn.ReLU(inplace=True)# 最大池化层,池化核大小为3x3,步长为2,填充为1。self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)# 根据输入的block和layers创建不同的层级# 创建四个层级(layer1至layer4),每个层级包含多个残差块self.layer1 = self._make_layer(block, 64, layers[0])  # 第一层,输出通道为64,block数量由layers[0]决定。self.layer2 = self._make_layer(block, 128, layers[1], stride=2)  # 第二层,输出通道为128,block数量由layers[1]决定,步长为2。self.layer3 = self._make_layer(block, 256, layers[2], stride=2)  # 第三层,输出通道为256,block数量由layers[2]决定,步长为2。self.layer4 = self._make_layer(block, 512, layers[3], stride=2)  # 第四层,输出通道为512,block数量由layers[3]决定,步长为2。# 创建输出层self.layer5 = self._make_out_layer(in_channels=2048)  # 输出层的输入通道为2048。# 平均池化层,池化核大小和步长均为2。self.avgpool = nn.AvgPool2d(2)# 最后的卷积层,将输出通道变为30,卷积核大小为3x3,步长为1,填充为1,无偏置。self.conv_end = nn.Conv2d(256, 30, kernel_size=3, stride=1, padding=1, bias=False)# 批标准化层,用于conv_end的输出self.bn_end = nn.BatchNorm2d(30)# 初始化模型参数for m in self.modules():  # 遍历模型中的所有模块if isinstance(m, nn.Conv2d):  # 如果是卷积层n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels  # 计算权重初始化的标准差m.weight.data.normal_(0, math.sqrt(2. / n))  # 使用正态分布初始化权重elif isinstance(m, nn.BatchNorm2d):  # 如果是批标准化层m.weight.data.fill_(1)  # 权重初始化为1m.bias.data.zero_()  # 偏置初始化为0# 根据传入的layer个数和block创建# 输入层的构建def _make_layer(self, block, planes, blocks, stride=1):'''根据给定的block类型、输出通道数、block数量和步长来创建一系列block组成的层。:param block: 要使用的block类型,是一个类,如BasicBlock或Bottleneck:param planes: 每个block的输出通道数:param blocks: 需要搭建的block数量:param stride: 第一个block的卷积层步长,用于控制是否进行下采样'''# 初始化下采样变量,用于第一个block可能需要的下采样操作downsample = None# 判断是否需要进行下采样# 如果步长不为1,或者当前层的输入通道数(self.inplanes)与block输出通道数(planes * block.expansion)不匹配,则进行下采样if stride != 1 or self.inplanes != planes * block.expansion:downsample = nn.Sequential(# 使用1x1的卷积层进行通道数转换和步长调整,使得残差连接可以相加nn.Conv2d(self.inplanes, planes * block.expansion, kernel_size=1, stride=stride, bias=False),# 批量归一化,稳定训练过程nn.BatchNorm2d(planes * block.expansion),)# 初始化层的列表layers = []# 添加第一个block,并可能进行下采样# 第一个block的输入通道数是self.inplanes,输出通道数是planes,步长是stride,可能包含下采样操作layers.append(block(self.inplanes, planes, stride, downsample))# 更新当前层的输入通道数为block的输出通道数self.inplanes = planes * block.expansion# 添加剩余的blocks,它们的输入通道数已经是更新后的self.inplanes,输出通道数保持不变for i in range(1, blocks):layers.append(block(self.inplanes, planes))# 将层的列表转换为Sequential模型,便于作为神经网络的一部分进行前向传播return nn.Sequential(*layers)  # *操作符用于将列表中的元素解包为函数参数# 输出层的构建def _make_out_layer(self, in_channels):'''根据输入通道数构建输出层,包含多个Output_Block。:param in_channels: 输入通道数'''# 初始化层的列表layers = []# 添加第一个Output_Block,并指定其输入通道数和输出通道数# 注意:这里使用了block_type参数来区分不同的Output_Block类型layers.append(Output_Block(in_planes=in_channels, planes=256, block_type='B'))# 添加两个相同类型的Output_Block,输入通道数和输出通道数都保持不变layers.append(Output_Block(in_planes=256, planes=256, block_type='A'))layers.append(Output_Block(in_planes=256, planes=256, block_type='A'))# 将层的列表转换为Sequential模型return nn.Sequential(*layers)  # *操作符用于将列表中的元素解包为函数参数def forward(self, x):# x 为网络的输入,这里的前向传播方法描述了网络如何从输入x获得最终的输出# 经历共有的卷积和池化层# 第一个卷积层,对输入x进行卷积操作x = self.conv1(x)# 批量归一化层,对卷积后的结果进行归一化处理x = self.bn1(x)# ReLU激活函数,对归一化后的结果进行非线性变换x = self.relu(x)# 最大池化层,对激活后的结果进行下采样x = self.maxpool(x)# 经历各个block块# 第一个block层,对池化后的结果进行进一步的特征提取x = self.layer1(x)# 第二个block层,继续提取特征x = self.layer2(x)# 第三个block层x = self.layer3(x)# 第四个block层x = self.layer4(x)# 第五个block层,这里需要注意,通常标准的ResNet结构中只有4个layer,这里可能是自定义的扩展x = self.layer5(x)# 经历最终的输出# 平均池化层,对最后一个block的输出进行全局平均池化x = self.avgpool(x)# 卷积层,对池化后的结果进行进一步的卷积变换x = self.conv_end(x)# 批量归一化层,对卷积后的结果进行归一化处理x = self.bn_end(x)# Sigmoid激活函数,将特征映射到0-1之间x = F.sigmoid(x)  # 归一化到0-1# 将输出构建为正确的shape# permute函数用于改变张量的维度顺序,这里将通道维度移动到最后一个位置# 假设输出shape原本为(-1, C, H, W),经过permute后变为(-1, H, W, C)x = x.permute(0, 2, 3, 1)  # (-1,7,7,30)# 返回最终的输出xreturn x

2.3 定义模型

      构建不同的ResNet函数。即不同的ResNet模型。如下:

# 6. 构建不同的ResNet函数
# 预训练下载链接
model_urls = {'resnet18': 'https://download.pytorch.org/models/resnet18-5c106cde.pth','resnet34': 'https://download.pytorch.org/models/resnet34-333f7ec4.pth','resnet50': 'https://download.pytorch.org/models/resnet50-19c8e357.pth','resnet101': 'https://download.pytorch.org/models/resnet101-5d3b4d8f.pth','resnet152': 'https://download.pytorch.org/models/resnet152-b121ed2d.pth',
}# 构建ResNet18
def resnet18(pretrained=False, **kwargs):model = ResNet(Base_Block, [2, 2, 2, 2], **kwargs)# 是否预训练if pretrained:model.load_state_dict(model_zoo.load_url(model_urls['resnet18']))return model# 构建ResNet34
def resnet34(pretrained=False, **kwargs):model = ResNet(Base_Block, [3, 4, 6, 3], **kwargs)if pretrained:model.load_state_dict(model_zoo.load_url(model_urls['resnet34']))return model# 构建ResNet50
def resnet50(pretrained=False, **kwargs):model = ResNet(Senior_Block, [3, 4, 6, 3], **kwargs)if pretrained:model.load_state_dict(model_zoo.load_url(model_urls['resnet50']))return model# 构建ResNet101
def resnet101(pretrained=False, **kwargs):model = ResNet(Senior_Block, [3, 4, 23, 3], **kwargs)if pretrained:model.load_state_dict(model_zoo.load_url(model_urls['resnet101']))return model# 构建ResNet152
def resnet152(pretrained=False, **kwargs):model = ResNet(Senior_Block, [3, 8, 36, 3], **kwargs)if pretrained:model.load_state_dict(model_zoo.load_url(model_urls['resnet152']))return model

2.4 完整代码

Yolo_ResNet卷积神经网络

''
'''
# 1. 导入所需要的包
'''
import torch
import math
from torch import nn
import torch.utils.model_zoo as model_zoo
import torch.nn.functional as F'''
# 2. 构建block: 不含有1*1
# 定义一个基础块类,继承自nn.Module,用于构建神经网络的基础结构  
'''
class Base_Block(nn.Module):# 用于扩充的变量,表示扩大几倍# 在某些网络结构中,如ResNet的Bottleneck块,可能需要通过该变量来扩充通道数expansion = 1def __init__(self, in_planes, out_planes, stride=1, downsample=None):# 初始化函数,传入输入通道数、输出通道数、步长以及下采样函数''':param in_planes: 输入的通道数:param planes: 输出的通道数:param stride: 默认步长:param downsample: 是否进行下采样'''# 调用父类nn.Module的初始化函数super(Base_Block, self).__init__()# 定义网络结构 + 初始化参数# 第一个卷积层self.conv1 = nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride, padding=1, bias=False)# 第一个批量归一化层self.bn1 = nn.BatchNorm2d(out_planes)# ReLU激活函数,inplace=True表示直接改变原始参数值,节省内存self.relu = nn.ReLU(inplace=True)# 第二个卷积层self.conv2 = nn.Conv2d(out_planes, out_planes, kernel_size=3, stride=1, padding=1, bias=False)# 第二个批量归一化层self.bn2 = nn.BatchNorm2d(out_planes)# 下采样函数,用于调整输入x的维度,以便与卷积后的输出维度一致self.downsample = downsample# 存储步长,以便后续可能用到self.stride = stride# 定义前向传播函数def forward(self, x):# 残差,即直接输入x,后续会与卷积处理后的输出相加res = x# 正常传播# 通过第一个卷积层out = self.conv1(x)# 通过第一个批量归一化层out = self.bn1(out)# 通过ReLU激活函数out = self.relu(out)# 通过第二个卷积层out = self.conv2(out)# 通过第二个批量归一化层out = self.bn2(out)# 判断是否下采样# 如果需要下采样,则调整残差res的维度if self.downsample is not None:res = self.downsample(res)# 残差相加# 将残差res与卷积处理后的输出out相加,实现残差连接out += res# 再次通过ReLU激活函数out = self.relu(out)# 返回结果return out'''
# 3. 构建Block:含有1*1
# 定义高级块类,继承自nn.Module,用于构建更复杂的网络结构 
'''
class Senior_Block(nn.Module):expansion = 4def __init__(self, in_planes, planes, stride=1, downsample=None):''':param in_planes: 输入通道数:param planes: 中间通道数,最终的输出通道数还需要乘以扩大系数,即expansion:param stride: 步长:param downsample: 下采样方法'''super(Senior_Block, self).__init__()self.conv1 = nn.Conv2d(in_planes, planes, kernel_size=1, bias=False)self.bn1 = nn.BatchNorm2d(planes)self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=stride, padding=1, bias=False)self.bn2 = nn.BatchNorm2d(planes)self.conv3 = nn.Conv2d(planes, planes * 4, kernel_size=1, bias=False)self.bn3 = nn.BatchNorm2d(planes * 4)self.relu = nn.ReLU(inplace=True)self.downsample = downsampleself.stride = stridedef forward(self, x):# 残差res = x# 前向传播out = self.conv1(x)out = self.bn1(out)out = self.relu(out)out = self.conv2(out)out = self.bn2(out)out = self.relu(out)out = self.conv3(out)out = self.bn3(out)# 是否下采样if self.downsample is not None:res = self.downsample(x)# 相加out += resout = self.relu(out)return out'''
# 4. 构建输出层
# 定义输出块类,继承自nn.Module,用于构建更复杂的网络结构 
'''
class Output_Block(nn.Module):# 定义扩大系数,该块最终输出的通道数将与中间通道数相同expansion = 1def __init__(self, in_planes, planes, stride=1, block_type='A'):# 初始化函数,传入输入通道数、中间通道数、步长以及块类型''':param in_planes: 输入通道数:param planes: 中间通道数:param stride: 步长:param block_type: 块类型,为'A'表示不需要下采样,为'B'则需要'''# 调用父类nn.Module的初始化函数super(Output_Block, self).__init__()# 定义第一个卷积层,用于改变通道数self.conv1 = nn.Conv2d(in_planes, planes, kernel_size=1, bias=False)# 定义第一个批量归一化层self.bn1 = nn.BatchNorm2d(planes)# 定义第二个卷积层,使用3x3的卷积核,带有2的空洞率(dilation),可以增大感受野self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=stride, padding=2, bias=False, dilation=2)# 定义第二个批量归一化层self.bn2 = nn.BatchNorm2d(planes)# 定义第三个卷积层,用于调整通道数至最终的输出通道数self.conv3 = nn.Conv2d(planes, self.expansion * planes, kernel_size=1, bias=False)# 定义第三个批量归一化层self.bn3 = nn.BatchNorm2d(self.expansion * planes)# 初始化下采样层,默认是一个空的序列self.downsample = nn.Sequential()# 判断是否需要下采样,这里综合了步长、通道数以及块类型if stride != 1 or in_planes != self.expansion * planes or block_type == 'B':# 如果需要下采样,则设置下采样层self.downsample = nn.Sequential(# 定义一个卷积层,用于调整输入的通道数和维度nn.Conv2d(in_planes, self.expansion * planes, kernel_size=1, stride=stride, bias=False),# 定义一个批量归一化层nn.BatchNorm2d(self.expansion * planes))# 定义前向传播函数def forward(self, x):# 通过第一个卷积层和批量归一化层,并应用ReLU激活函数out = F.relu(self.bn1(self.conv1(x)))# 通过第二个卷积层和批量归一化层,并再次应用ReLU激活函数out = F.relu(self.bn2(self.conv2(out)))# 通过第三个卷积层和批量归一化层out = self.bn3(self.conv3(out))# 将输入x通过下采样层,以便与out的维度相匹配# 如果不需要下采样,则下采样层是一个空操作out += self.downsample(x)# 应用ReLU激活函数out = F.relu(out)# 返回输出结果return out'''
# 5. 构建ResNet
# 定义ResNet类,继承自nn.Module,用于构建更复杂的网络结构 
'''
class ResNet(nn.Module):def __init__(self, block, layers):'''初始化ResNet网络。:param block: 基本的Block块对象,如BasicBlock或Bottleneck。:param layers: 不同层级的block数量,例如ResNet50为[3, 4, 6, 3]。'''super(ResNet, self).__init__()  # 调用父类nn.Module的初始化方法。# 初始通道数设置为64self.inplanes = 64# 初始卷积层,输入通道为3(RGB图像),输出通道为64,卷积核大小为7x7,步长为2,填充为3,无偏置。self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False)# 批标准化层,用于conv1的输出self.bn1 = nn.BatchNorm2d(64)# ReLU激活函数,inplace=True表示直接在原变量上进行操作,节省内存。self.relu = nn.ReLU(inplace=True)# 最大池化层,池化核大小为3x3,步长为2,填充为1。self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)# 根据输入的block和layers创建不同的层级# 创建四个层级(layer1至layer4),每个层级包含多个残差块self.layer1 = self._make_layer(block, 64, layers[0])  # 第一层,输出通道为64,block数量由layers[0]决定。self.layer2 = self._make_layer(block, 128, layers[1], stride=2)  # 第二层,输出通道为128,block数量由layers[1]决定,步长为2。self.layer3 = self._make_layer(block, 256, layers[2], stride=2)  # 第三层,输出通道为256,block数量由layers[2]决定,步长为2。self.layer4 = self._make_layer(block, 512, layers[3], stride=2)  # 第四层,输出通道为512,block数量由layers[3]决定,步长为2。# 创建输出层self.layer5 = self._make_out_layer(in_channels=2048)  # 输出层的输入通道为2048。# 平均池化层,池化核大小和步长均为2。self.avgpool = nn.AvgPool2d(2)# 最后的卷积层,将输出通道变为30,卷积核大小为3x3,步长为1,填充为1,无偏置。self.conv_end = nn.Conv2d(256, 30, kernel_size=3, stride=1, padding=1, bias=False)# 批标准化层,用于conv_end的输出self.bn_end = nn.BatchNorm2d(30)# 初始化模型参数for m in self.modules():  # 遍历模型中的所有模块if isinstance(m, nn.Conv2d):  # 如果是卷积层n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels  # 计算权重初始化的标准差m.weight.data.normal_(0, math.sqrt(2. / n))  # 使用正态分布初始化权重elif isinstance(m, nn.BatchNorm2d):  # 如果是批标准化层m.weight.data.fill_(1)  # 权重初始化为1m.bias.data.zero_()  # 偏置初始化为0# 根据传入的layer个数和block创建# 输入层的构建def _make_layer(self, block, planes, blocks, stride=1):'''根据给定的block类型、输出通道数、block数量和步长来创建一系列block组成的层。:param block: 要使用的block类型,是一个类,如BasicBlock或Bottleneck:param planes: 每个block的输出通道数:param blocks: 需要搭建的block数量:param stride: 第一个block的卷积层步长,用于控制是否进行下采样'''# 初始化下采样变量,用于第一个block可能需要的下采样操作downsample = None# 判断是否需要进行下采样# 如果步长不为1,或者当前层的输入通道数(self.inplanes)与block输出通道数(planes * block.expansion)不匹配,则进行下采样if stride != 1 or self.inplanes != planes * block.expansion:downsample = nn.Sequential(# 使用1x1的卷积层进行通道数转换和步长调整,使得残差连接可以相加nn.Conv2d(self.inplanes, planes * block.expansion, kernel_size=1, stride=stride, bias=False),# 批量归一化,稳定训练过程nn.BatchNorm2d(planes * block.expansion),)# 初始化层的列表layers = []# 添加第一个block,并可能进行下采样# 第一个block的输入通道数是self.inplanes,输出通道数是planes,步长是stride,可能包含下采样操作layers.append(block(self.inplanes, planes, stride, downsample))# 更新当前层的输入通道数为block的输出通道数self.inplanes = planes * block.expansion# 添加剩余的blocks,它们的输入通道数已经是更新后的self.inplanes,输出通道数保持不变for i in range(1, blocks):layers.append(block(self.inplanes, planes))# 将层的列表转换为Sequential模型,便于作为神经网络的一部分进行前向传播return nn.Sequential(*layers)  # *操作符用于将列表中的元素解包为函数参数# 输出层的构建def _make_out_layer(self, in_channels):'''根据输入通道数构建输出层,包含多个Output_Block。:param in_channels: 输入通道数'''# 初始化层的列表layers = []# 添加第一个Output_Block,并指定其输入通道数和输出通道数# 注意:这里使用了block_type参数来区分不同的Output_Block类型layers.append(Output_Block(in_planes=in_channels, planes=256, block_type='B'))# 添加两个相同类型的Output_Block,输入通道数和输出通道数都保持不变layers.append(Output_Block(in_planes=256, planes=256, block_type='A'))layers.append(Output_Block(in_planes=256, planes=256, block_type='A'))# 将层的列表转换为Sequential模型return nn.Sequential(*layers)  # *操作符用于将列表中的元素解包为函数参数def forward(self, x):# x 为网络的输入,这里的前向传播方法描述了网络如何从输入x获得最终的输出# 经历共有的卷积和池化层# 第一个卷积层,对输入x进行卷积操作x = self.conv1(x)# 批量归一化层,对卷积后的结果进行归一化处理x = self.bn1(x)# ReLU激活函数,对归一化后的结果进行非线性变换x = self.relu(x)# 最大池化层,对激活后的结果进行下采样x = self.maxpool(x)# 经历各个block块# 第一个block层,对池化后的结果进行进一步的特征提取x = self.layer1(x)# 第二个block层,继续提取特征x = self.layer2(x)# 第三个block层x = self.layer3(x)# 第四个block层x = self.layer4(x)# 第五个block层,这里需要注意,通常标准的ResNet结构中只有4个layer,这里可能是自定义的扩展x = self.layer5(x)# 经历最终的输出# 平均池化层,对最后一个block的输出进行全局平均池化x = self.avgpool(x)# 卷积层,对池化后的结果进行进一步的卷积变换x = self.conv_end(x)# 批量归一化层,对卷积后的结果进行归一化处理x = self.bn_end(x)# Sigmoid激活函数,将特征映射到0-1之间x = F.sigmoid(x)  # 归一化到0-1# 将输出构建为正确的shape# permute函数用于改变张量的维度顺序,这里将通道维度移动到最后一个位置# 假设输出shape原本为(-1, C, H, W),经过permute后变为(-1, H, W, C)x = x.permute(0, 2, 3, 1)  # (-1,7,7,30)# 返回最终的输出xreturn x# 6. 构建不同的ResNet函数
# 预训练下载链接
model_urls = {'resnet18': 'https://download.pytorch.org/models/resnet18-5c106cde.pth','resnet34': 'https://download.pytorch.org/models/resnet34-333f7ec4.pth','resnet50': 'https://download.pytorch.org/models/resnet50-19c8e357.pth','resnet101': 'https://download.pytorch.org/models/resnet101-5d3b4d8f.pth','resnet152': 'https://download.pytorch.org/models/resnet152-b121ed2d.pth',
}# 构建ResNet18
def resnet18(pretrained=False, **kwargs):model = ResNet(Base_Block, [2, 2, 2, 2], **kwargs)# 是否预训练if pretrained:model.load_state_dict(model_zoo.load_url(model_urls['resnet18']))return model# 构建ResNet34
def resnet34(pretrained=False, **kwargs):model = ResNet(Base_Block, [3, 4, 6, 3], **kwargs)if pretrained:model.load_state_dict(model_zoo.load_url(model_urls['resnet34']))return model# 构建ResNet50
def resnet50(pretrained=False, **kwargs):model = ResNet(Senior_Block, [3, 4, 6, 3], **kwargs)if pretrained:model.load_state_dict(model_zoo.load_url(model_urls['resnet50']))return model# 构建ResNet101
def resnet101(pretrained=False, **kwargs):model = ResNet(Senior_Block, [3, 4, 23, 3], **kwargs)if pretrained:model.load_state_dict(model_zoo.load_url(model_urls['resnet101']))return model# 构建ResNet152
def resnet152(pretrained=False, **kwargs):model = ResNet(Senior_Block, [3, 8, 36, 3], **kwargs)if pretrained:model.load_state_dict(model_zoo.load_url(model_urls['resnet152']))return model

 3、定义损失

       在创建的目录结构 network 中创建Yolo_Loss.py文件,用于创建损失。

       YOLO的损失函数通常包括以下几个部分:

  1. 定位损失(Localization Loss):即长宽损失和坐标损失,用于衡量模型预测的目标边界框与真实边界框之间的差异。这通常使用均方误差(MSE)或其他类似的回归损失函数来计算。定位损失关注于边界框的中心点坐标(x, y)、宽度(w)和高度(h)的预测精度。
  2. 置信度损失(Confidence Loss):用于衡量模型预测的目标置信度与真实置信度之间的差异。置信度表示模型对预测目标存在的信心程度。对于每个边界框,模型会预测一个置信度分数,这个分数与真实标签(目标存在与否)进行比较,计算损失。
  3. 分类损失(Classification Loss):用于衡量模型预测的目标类别与真实类别之间的差异。这通常使用交叉熵损失函数来计算。对于每个目标,模型会预测一个类别概率分布,这个分布与真实标签进行比较,计算损失。

      在YOLO的损失函数中,这些部分通常会根据它们的重要性进行加权。例如,由于定位的准确性对于目标检测至关重要,因此定位损失通常会赋予较高的权重。而置信度损失和分类损失的权重则可以根据具体任务进行调整。

       此外,YOLO的损失函数还需要考虑如何处理没有目标的背景区域。这通常通过在损失函数中加入一个额外的项来实现,该项用于惩罚模型在背景区域产生高置信度的预测。

总的来说,YOLO的损失函数是一个综合了定位、置信度和分类损失的复合函数,旨在全面地衡量模型在目标检测任务上的性能。通过优化这个损失函数,我们可以训练出更加准确和鲁棒的目标检测模型。


# 1. 导包
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.autograd import Variable# 2. 损失函数类
class Yolo_Loss(nn.Module):def __init__(self, S=7, B=2, l_coord=5, l_noobj=0.5):''':param S: Yolov1论文中的S,即划分的网格,默认为7:param B: Yolov1论文中的B,即多少个框预测,默认为2:param l_coord:  损失函数中的超参数,默认为5:param l_noobj:  同上,默认为0.5'''super(Yolo_Loss, self).__init__()# 初始化各个参数self.S = Sself.B = Bself.l_coord = l_coordself.l_noobj = l_noobj# 前向传播def forward(self, pred_tensor, target_tensor):# 获取batchsize大小 ,即每批次数据的个数N = pred_tensor.size()[0]# target_tensor[:, :, :, 4]是目标张量中用于标记 目标 是否存在的部分(通常第五列是置信度或目标存在标志)。# 这行代码生成一个布尔掩码,其中值为True的位置表示对应网格中存在目标,值为False则表示不存在。# 具有目标标签的索引值,此时shape为[batch,7,7]coo_mask = target_tensor[:, :, :, 4] > 0# 与上一行相反,这行代码生成一个布尔掩码,表示哪些网格中没有目标。# 不具有目标的标签索引值,此时shape为[batch,7,7]noo_mask = target_tensor[:, :, :, 4] == 0# unsqueeze(-1)在最后一个维度上增加一个维度,使得coo_mask的形状从[batch, 7, 7]变为[batch, 7, 7, 1]。# expand_as(target_tensor)将这个新维度扩展至与target_tensor相同的形状,这样便于后续操作。# 将shape变为[batch,7,7,30]coo_mask = coo_mask.unsqueeze(-1).expand_as(target_tensor)# 同上,对noo_mask也进行相同的操作。noo_mask = noo_mask.unsqueeze(-1).expand_as(target_tensor)# 使用coo_mask从pred_tensor中索引出存在目标的预测值,然后通过view操作改变形状,使得每个存在目标的预测框成为一个长度为30的向量。# 获取预测值中包含对象的所有点(共7*7个点),并转为[x,30]的形式,其中x表示有多少点中的框包含有对象coo_pred = pred_tensor[coo_mask].view(-1, 30)# 对上面获取的值进行处理# 从coo_pred中取前10个元素(对应预测框的坐标和宽度高度信息),然后改变形状为[2x, 5],其中x表示存在目标的网格数量。# 1. 转为box形式:box[x1,y1,w1,h1,c1],shape为[2x,5],因为每个单元格/点有两个预测框box_pred = coo_pred[:, :10].contiguous().view(-1, 5)# 从coo_pred中取后20个元素(对应类别预测信息)。# 2. 转为class信息,即30中后面的20个值class_pred = coo_pred[:, 10:]# 对目标张量进行与预测张量相同的操作,获取存在目标的真实值。# 同理,对真实值进行操作,方便对比计算损失值coo_target = target_tensor[coo_mask].view(-1, 30)# 从coo_target中取出坐标和宽度高度信息。box_target = coo_target[:, :10].contiguous().view(-1, 5)# 从coo_target中取出类别信息。class_target = coo_target[:, 10:]# 同上的操作,获取不包含对象的预测值、真实值# 使用noo_mask从pred_tensor中索引出不存在目标的预测值,并改变形状。noo_pred = pred_tensor[noo_mask].view(-1, 30)# 对目标张量执行相同的操作,获取不存在目标的真实值。noo_target = target_tensor[noo_mask].view(-1, 30)# 注意:上述代码只是将预测值和真实值进行了预处理,以便后续计算损失。实际损失的计算(比如坐标损失、置信度损失和类别损失)在这段代码之后,通常还需要根据YOLO的具体定义来编写。# 不包含物体grid ceil的置信度损失:即图中的D部分# 这部分代码计算的是那些不包含目标物体的网格中预测框的置信度损失。# 1. 自己创建一个索引# 创建一个与noo_pred形状相同的全零的ByteTensor(布尔掩码),用于标识需要计算的预测框置信度。noo_pred_mask = torch.cuda.ByteTensor(noo_pred.size())# 将这个掩码的所有元素初始化为0(即False),意味着初始时所有预测框的置信度都不被考虑。noo_pred_mask.zero_()  # 将全部元素变为Flase的意思# 2. 将其它位置的索引置为0,唯独两个框的置信度位置变为1# 将所有网格中第一个预测框的置信度位置(索引为4,因为通常置信度是预测框向量的最后一个元素,且前面有4个坐标值)设置为1。noo_pred_mask[:, 4] = 1# 将所有网格中第二个预测框的置信度位置(索引为9,因为每个网格有两个预测框,每个预测框有5个值:4个坐标和1个置信度)设置为1。noo_pred_mask[:, 9] = 1# 3. 获取对应的值# 使用上面创建的掩码从noo_pred中提取出所有预测框的置信度值。noo_pred_c = noo_pred[noo_pred_mask]  # noo pred只需要计算 c 的损失 size[-1,2]# 同样地,使用相同的掩码从noo_target中提取出目标置信度值,用于与预测值比较。noo_target_c = noo_target[noo_pred_mask]# 4. 计算损失值:均方误差# 使用均方误差(Mean Squared Error, MSE)计算预测置信度与目标置信度之间的差异,作为不包含物体网格的置信度损失。nooobj_loss = F.mse_loss(noo_pred_c, noo_target_c, size_average=False)# 计算包含物体的损失值# 创建几个全为False/0的变量,用于后期存储值# 创建一个与box_target形状相同的全零的ByteTensor,用于标识负责预测物体的网格中的预测框。coo_response_mask = torch.cuda.ByteTensor(box_target.size())  # 负责预测框# 将这个掩码的所有元素初始化为0。coo_response_mask.zero_()# 创建一个与box_target形状相同的全零的ByteTensor,用于标识不负责预测物体的网格中的预测框。coo_not_response_mask = torch.cuda.ByteTensor(box_target.size())  # 不负责预测的框的索引(因为一个cell两个预测框,而只有IOU最大的负责索引)# 将这个掩码的所有元素初始化为0。coo_not_response_mask.zero_()# 创建一个与box_target形状相同的全零的Tensor,用于存放每个预测框与目标框之间的IOU(交并比)值。这个值在后续计算损失时会用到。box_target_iou = torch.zeros(box_target.size()).cuda()  # 具体的IOU值存放处# 由于一个单元格两个预测框,因此step=2# 在YOLO系列算法中,每个网格单元通常预测固定数量的预测框。这里假设每个单元有两个预测框,因此迭代步长为2。# 遍历所有目标框,因为每个目标框与一个单元格关联,且每个单元格有两个预测框。# 选择具有最佳IOU的预测框。for i in range(0, box_target.size()[0], 2):  # choose the best iou box# 获取预测值中的两个box# 从预测框集合中取出当前单元格的两个预测框。这里box1是一个包含两个预测框的tensor,每个预测框有五个值:[x, y, w, h, c],分别代表中心坐标、宽度、高度和置信度。box1 = box_pred[i:i + 2]  # [x,y,w,h,c]# 创建一个临时变量box1形状相同的tensor,用于存放左上角和右下角角坐标值,因为计算IOU需要box1_xyxy = Variable(torch.FloatTensor(box1.size()))# 将预测框的中心坐标和宽高转换为左上角和右下角的坐标,并进行归一化处理。self.S通常表示网格的大小,比如7x7的网格。box1_xyxy[:, :2] = box1[:, :2] / float(self.S) - 0.5 * box1[:, 2:4]  # 原本(xc,yc)为7*7 所以要除以7# 同样地,计算预测框的右下角坐标。box1_xyxy[:, 2:4] = box1[:, :2] / float(self.S) + 0.5 * box1[:, 2:4]# 用同样的思路对真实值进行处理,不过不同的是真实值一个对象只有一个框# 从目标框集合中取出与当前单元格关联的目标框,并将其reshape为[-1, 5]的形式。box2 = box_target[i].view(-1, 5)# 创建一个与目标框形状相同的tensor,用于存放左上角和右下角坐标形式的目标框。box2_xyxy = Variable(torch.FloatTensor(box2.size()))# 将目标框的中心坐标和宽高转换为左上角和右下角的坐标,并进行归一化处理。box2_xyxy[:, :2] = box2[:, :2] / float(self.S) - 0.5 * box2[:, 2:4]# 计算目标框的右下角坐标。box2_xyxy[:, 2:4] = box2[:, :2] / float(self.S) + 0.5 * box2[:, 2:4]# 计算两者的IOU# 调用compute_iou函数计算两个预测框与目标框之间的IOU值。这里box1_xyxy[:, :4]包含两个预测框的坐标,box2_xyxy[:, :4]包含一个目标框的坐标。iou = self.compute_iou(box1_xyxy[:, :4], box2_xyxy[:, :4])  # 前者shape为[2,4],后者为[1,4]# 获取两者IOU最大的值(max_iou)和对应的索引(max_index),因为一个cell有两个预测框,一般而言取IOU最大的作为预测框max_iou, max_index = iou.max(0)# 将索引移至GPU上(如果之前不在GPU上的话)。max_index = max_index.data.cuda()# 将IOU最大的索引设置为1,即表示这个框负责预测coo_response_mask[i + max_index] = 1# 将box_target_iou转为PyTorch的Variable,并移到GPU上(如果有的话),以支持自动微分。box_target_iou = Variable(box_target_iou).cuda()# 获取负责预测框的值、IOU值和真实框的值# 从box_pred中筛选出负责预测的框(即IOU最大的框),并改变其形状为[-1, 5]。box_pred_response = box_pred[coo_response_mask].view(-1, 5)# 从box_target_iou中筛选出负责预测的框的IOU值,并改变其形状。box_target_response_iou = box_target_iou[coo_response_mask].view(-1, 5)# 从box_target中筛选出负责预测的框的真实值,并改变其形状。box_target_response = box_target[coo_response_mask].view(-1, 5)# 这个对应的是图中的部分C,负责预测框的损失# 计算负责预测框的置信度损失,使用均方误差损失函数(MSE Loss)。contain_loss = F.mse_loss(box_pred_response[:, 4], box_target_response_iou[:, 4], size_average=False)# 1. 计算坐标损失,即图中的A和B部分# 计算负责预测框的坐标损失,包括中心坐标和宽高的损失。注意,宽高损失计算时使用了开方,这通常是为了改善损失函数的性能。loc_loss = F.mse_loss(box_pred_response[:, :2], box_target_response[:, :2], size_average=False) + F.mse_loss(torch.sqrt(box_pred_response[:, 2:4]), torch.sqrt(box_target_response[:, 2:4]), size_average=False)# 获取不负责预测框的值、真实值# 从box_pred中筛选出不负责预测的框,并改变其形状。box_pred_not_response = box_pred[coo_not_response_mask].view(-1, 5)# 从box_target中筛选出不负责预测的框的真实值,并改变其形状。box_target_not_response = box_target[coo_not_response_mask].view(-1, 5)# 将不负责预测框的置信度真实值设为0,因为这些框不应包含任何对象。box_target_not_response[:, 4] = 0  # 将真实值置为0# 2. 计算不负责预测框的损失值,即图中的部分C# 计算不负责预测框的置信度损失。not_contain_loss = F.mse_loss(box_pred_not_response[:, 4], box_target_not_response[:, 4], size_average=False)# 3. 类别损失,即图中的E部分# 计算类别损失,使用均方误差损失函数。class_loss = F.mse_loss(class_pred, class_target, size_average=False)# 返回总损失,它是坐标损失、负责预测框的置信度损失、不负责预测框的置信度损失、类别损失以及一个未显示的`nooobj_loss`的加权和,最后除以N进行归一化。return (self.l_coord * loc_loss + contain_loss + not_contain_loss + self.l_noobj * nooobj_loss + class_loss) / N# 计算IOU的函数def compute_iou(self, box1, box2):''':param box1: 预测的box,一般为[2,4]:param box2: 真实的box,一般为[1,4]:return:'''# 获取各box个数N = box1.size(0)M = box2.size(0)# 计算两者中左上角左边较大的lt = torch.max(box1[:, :2].unsqueeze(1).expand(N, M, 2),  # [N,2] -> [N,1,2] -> [N,M,2]box2[:, :2].unsqueeze(0).expand(N, M, 2),  # [M,2] -> [1,M,2] -> [N,M,2])# 计算两者右下角左边较小的rb = torch.min(box1[:, 2:].unsqueeze(1).expand(N, M, 2),  # [N,2] -> [N,1,2] -> [N,M,2]box2[:, 2:].unsqueeze(0).expand(N, M, 2),  # [M,2] -> [1,M,2] -> [N,M,2])# 计算两者相交部分的长、宽wh = rb - lt  # [N,M,2]# 如果长、宽中有小于0的,表示可能没有相交趋于,置为0即可wh[wh < 0] = 0  # clip at 0inter = wh[:, :, 0] * wh[:, :, 1]  # [N,M]# 计算各个的面积# box1的面积area1 = (box1[:, 2] - box1[:, 0]) * (box1[:, 3] - box1[:, 1])  # [N,]# box2的面积area2 = (box2[:, 2] - box2[:, 0]) * (box2[:, 3] - box2[:, 1])  # [M,]area1 = area1.unsqueeze(1).expand_as(inter)  # [N,] -> [N,1] -> [N,M]area2 = area2.unsqueeze(0).expand_as(inter)  # [M,] -> [1,M] -> [N,M]# IOu值,交集除以并集,其中并集为两者的面积和减去交集部分iou = inter / (area1 + area2 - inter)return iou

4、定义训练过程

      在创建的目录结构 network 中创建train.py文件,用于进行训练。

  1. 导入所需的包
  2. 定义基本参数
  3. 创建模型并使用预训练参数
  4. 损失函数,优化器,并将模型、损失函数放入GPU中
  5. 加载数据
  6. 训练:循环50次,第20和第35次调整学习率,每次对数据集中按批次执行训练和验证
# 1. 导入所需的包
import warningsfrom YOLOv1.network.Yolo_ResNet import resnet50
from YOLOv1.utils.yolo_dataset import Yolo_Dataset
from YOLOv1_pytorch.utils.Yolo_Loss import Yolo_Losswarnings.filterwarnings("ignore")
from tqdm import tqdm
import torch
from torch.utils.data import DataLoader
import torchvision.transforms as T
from torchvision import models# 2. 定义基本参数
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
batch_size = 2      # 根据自己的电脑设定
epochs = 50
lr = 0.01
file_root = './data/VOC2012/JPEGImages/'   # 需要根据的实际路径修改# 3. 创建模型并使用预训练参数
pytorch_resnet = models.resnet50(pretrained=True)   # 官方的resnet50预训练模型
model = resnet50()   # 创建自己的resnet50,# 接下来就是让自己的模型去继承官方的权重参数
pytorch_state_dict = pytorch_resnet.state_dict()
model_state_dict = model.state_dict()for k in pytorch_state_dict.keys():# 调试: 看看模型哪些有没有问题# print(k)# 如果自己的模型和官方的模型key相同,并且不是fc层,则继承过来if k in model_state_dict.keys() and not k.startswith('fc'):model_state_dict[k] = pytorch_state_dict[k]# 4. 损失函数,优化器,并将模型、损失函数放入GPU中
loss = Yolo_Loss()
optimizer = torch.optim.SGD(model.parameters(),lr=lr,momentum=0.9,weight_decay=5e-4)
model.to(device)
loss.to(device)# 5. 加载数据
train_dataset = Yolo_Dataset(root=file_root, list_file='./utils/voctrain.txt', train=True, transforms = [T.ToTensor()])
train_loader = DataLoader(train_dataset,batch_size=batch_size,shuffle=True,drop_last=True)
test_dataset = Yolo_Dataset(root=file_root, list_file='./utils/voctest.txt', train=False, transforms = [T.ToTensor()])
test_loader = DataLoader(test_dataset,batch_size=batch_size,shuffle=True,drop_last=True)# 6. 训练
# 打印一些基本的信息
print('starting train the model')
print('the train_dataset has %d images' % len(train_dataset))
print('the batch_size is ',batch_size)
# 定义一个最佳损失值
best_test_loss = 0
# 开始训练
for e in range(epochs):model.train()# 调整学习率if e == 20:print('change the lr')optimizer.param_groups[0]['lr'] /= 10if e == 35:print('change the lr')optimizer.param_groups[0]['lr'] /= 10# 进度条显示tqdm_tarin = tqdm(train_loader)# 定义损失变量total_loss = 0.for i,(images,target) in enumerate(tqdm_tarin):# 将变量放入设备中images,target = images.to(device),target.to(device)# 训练--损失等pred = model(images)loss_value = loss(pred,target)total_loss += loss_value.item()#在开始新的梯度计算之前,调用优化器的zero_grad()方法将模型参数的梯度清零。这是因为在PyTorch中,梯度是累积的,如果不清零,新的梯度会累加到旧的梯度上。optimizer.zero_grad()#对损失值loss_value调用backward()方法,计算损失相对于模型参数的梯度。这些梯度将被用于接下来的参数更新步骤。loss_value.backward()#调用优化器的step()方法,根据计算出的梯度更新模型的参数。这一步是模型训练的关键,它使得模型在训练过程中逐渐优化。optimizer.step()# 打印一下损失值if (i+1) % 5 == 0:tqdm_tarin.desc = 'train epoch[{}/{}] loss:{:.6f}'.format(e+1,epochs,total_loss/(i+1))# 启用验证模式model.eval()validation_loss = 0.0tqdm_test = tqdm(test_loader)for i, (images, target) in enumerate(tqdm_test):images, target = images.to(device),target.to(device)pred = model(images)loss_value = loss(pred, target)validation_loss += loss_value.item()validation_loss /= len(test_loader)# 显示验证集的损失值print('In the test step,the average loss is %.6f' % validation_loss)# 如果最佳损失值大于验证集的损失,意味着当前训练很好# 这一点需要设置好最佳的损失值,不容易设置# 是否启用看大家心情# if best_test_loss > validation_loss:#     best_test_loss = validation_loss#     print('get best test loss %.5f' % best_test_loss)#     torch.save(model.state_dict(), './weights/best.pth')# 记得最后保存参数torch.save(model.state_dict(), './weights/yolo.pth')

5、定义预测方法

       在创建的目录结构 network 中创建predict.py文件,用于进行预测。同时在目录test_images中存入预测的图片(在最下面,自己进行保存,并进行命名为1、2、3、4、5、6)

# author: baiCai
# 1. 导包
import os
import random
import torch
from torch.autograd import Variable
import torchvision.transforms as transforms
import cv2
from matplotlib import pyplot as plt
import numpy as np
import warningsfrom YOLOv1_pytorch.network.My_ResNet import resnet50warnings.filterwarnings('ignore')# 2. 定义一些基本的参数
# 类别索引
VOC_CLASSES = ('aeroplane', 'bicycle', 'bird', 'boat','bottle', 'bus', 'car', 'cat', 'chair','cow', 'diningtable', 'dog', 'horse','motorbike', 'person', 'pottedplant',
'sheep', 'sofa', 'train', 'tvmonitor')
# 画矩形框的时候用到的颜色变量
Color = [[0, 0, 0],[128, 0, 0],[0, 128, 0],[128, 128, 0],[0, 0, 128],[128, 0, 128],[0, 128, 128],[128, 128, 128],[64, 0, 0],[192, 0, 0],[64, 128, 0],[192, 128, 0],[64, 0, 128],[192, 0, 128],[64, 128, 128],[192, 128, 128],[0, 64, 0],[128, 64, 0],[0, 192, 0],[128, 192, 0],[0, 64, 128]]# 3. 解码函数
def decoder(pred):''':param pred: batchx7x7x30,但是预测的时候一般一张图片一张的放,因此batch=1:return: box[[x1,y1,x2,y2]] label[...]'''# 定义一些基本的参数grid_num = 7        # 网格划分标准大小#初始化三个空列表,用于存储解码后的边界框坐标、类别索引和对应的概率值。boxes=[]cls_indexs=[]probs = []#计算网格单元的大小(或称为缩放因子),这对于将模型输出的坐标从网格空间转换到图像空间是必要的。cell_size = 1./grid_num # 缩放因子# 获取一些值pred = pred.data    # 预测值的数据:1*7*7*30pred = pred.squeeze(0) # 预测值的数据:7x7x30contain1 = pred[:,:,4].unsqueeze(2)  # 先获取第一个框的置信度,然后升维变为7*7*1contain2 = pred[:,:,9].unsqueeze(2) # 同上,只是为第二个框contain = torch.cat((contain1,contain2),2) # 拼接在一起,变为7*7*2mask1 = contain > 0.1 #大于阈值0.1,设置为Truemask2 = (contain==contain.max()) # 找出置信度最大的,设置为Truemask = (mask1+mask2).gt(0) # 将mask1+mask2,让其中大于0的设置为True# 开始迭代每个单元格,即7*7个for i in range(grid_num):for j in range(grid_num):# 迭代两个预测框for b in range(2):# 如果mask为1,表示这个框是最大的置信度框if mask[i,j,b] == 1:# 获取坐标值box = pred[i,j,b*5:b*5+4]# 获取置信度值contain_prob = torch.FloatTensor([pred[i,j,b*5+4]])# 将7*7的坐标,归一化xy = torch.FloatTensor([j,i])*cell_size #cell左上角  up left of cell#box[:2] = box[:2]*cell_size + xy# 将[cx,cy,w,h]转为[x1,xy1,x2,y2]box_xy = torch.FloatTensor(box.size())      # 重新创建一个变量存储值box_xy[:2] = box[:2] - 0.5*box[2:] # 这个就是中心坐标加减宽度/高度得到左上角/右下角坐标box_xy[2:] = box[:2] + 0.5*box[2:]# 获取最大的概率和类别索引值max_prob,cls_index = torch.max(pred[i,j,10:],0)# 如果置信度 * 类别概率 > 0.1,即说明有一定的可信度# 那么把值加入各个变量列表中if float((contain_prob*max_prob)[0]) > 0.1:boxes.append(box_xy.view(1,4))cls_indexs.append(torch.tensor([cls_index.item()]))probs.append(contain_prob*max_prob)# 如果boxes为0,表示没有框,返回0if len(boxes) ==0:boxes = torch.zeros((1,4))probs = torch.zeros(1)cls_indexs = torch.zeros(1)# 否则,进行处理,就是简单把原来的列表值[tensor,tensor]转为tensor的形式# 里面的值不变else:boxes = torch.cat(boxes,0) #(n,4)probs = torch.cat(probs,0) #(n,)cls_indexs = torch.cat(cls_indexs,0) #(n,)# 后处理——NMSkeep = mns(boxes,probs)# 返回值return boxes[keep],cls_indexs[keep],probs[keep]# 4. NMS处理
def mns(bboxes,scores,threshold=0.5):''':param bboxes:  bboxes(tensor) [N,4]:param scores:  scores(tensor) [N,]:param threshold: 阈值:return: 返回过滤后的框'''# 获取各个框的坐标值x1 = bboxes[:,0]y1 = bboxes[:,1]x2 = bboxes[:,2]y2 = bboxes[:,3]# 计算面积areas = (x2-x1) * (y2-y1)# 将置信度按照降序排序,并获取排序后的各个置信度在这个顺序中的索引_,order = scores.sort(0,descending=True)keep = []# 判断order中的元素个数是否大于0while order.numel() > 0:# 如果元素个数只剩下一个了,结束循环if order.numel() == 1:i = order.item()keep.append(i)break# 获取最大置信度的索引i = order[0]keep.append(i)# 对后面的元素坐标进行截断处理xx1 = x1[order[1:]].clamp(min=x1[i]) # min指的是小于它的设置为它的值,大于它的不管yy1 = y1[order[1:]].clamp(min=y1[i])xx2 = x2[order[1:]].clamp(max=x2[i])yy2 = y2[order[1:]].clamp(max=y2[i])# 此时的xx1,yy1等是排除了目前选中的框的,即假设x1有三个元素,那么xx1只有2个元素# 获取排序后的长和宽以及面积,如果小于0则设置为0w = (xx2-xx1).clamp(min=0)h = (yy2-yy1).clamp(min=0)inter = w*h# 准备更新order、# 计算选中的框和剩下框的IOU值ovr = inter / (areas[i] + areas[order[1:]] - inter)# 如果 IOU小于设定的阈值,说明需要保存下来继续筛选(NMS原理)ids = (ovr<=threshold).nonzero().squeeze()if ids.numel() == 0:breakorder = order[ids+1]return torch.LongTensor(keep)# 5. 预测函数
def predict_single(model, image_name, root_path=''):result = []  # 保存结果的变量# 打开图片image = cv2.imread(root_path + image_name)h, w, _ = image.shape# resize为模型的输入大小,即448*448img = cv2.resize(image, (448, 448))# 由于我们模型那里定义的颜色模式为RGB,因此这里需要转换mean = (123, 117, 104)  # RGB均值img = img - np.array(mean, dtype=np.float32)# 预处理transform = transforms.Compose([transforms.ToTensor(), ])img = transform(img)img = Variable(img[None, :, :, :], volatile=True)img = img.cuda()# 开始预测pred = model(img)  # 1x7x7x30pred = pred.cpu()# 解码boxes, cls_indexs, probs = decoder(pred)# 开始迭代每个框for i, box in enumerate(boxes):# 获取相关坐标,只是需要把原来归一化后的坐标转回去x1 = int(box[0] * w)x2 = int(box[2] * w)y1 = int(box[1] * h)y2 = int(box[3] * h)# 获取类别索引、概率等值cls_index = cls_indexs[i]cls_index = int(cls_index)  # convert LongTensor to intprob = probs[i]prob = float(prob)# 把这些值集中放入一个变量中返回result.append([(x1, y1), (x2, y2), VOC_CLASSES[cls_index], image_name, prob])return resultif __name__ == '__main__':# 慢慢的显示import time# 创建模型,加载参数model = resnet50()model.load_state_dict(torch.load('./weights/yolo.pth'))model.eval()model.cuda()# 设置图片路径base_path = './test_images/'# base_path = '../data/VOC2012/JPEGImages/'image_name_list = [base_path+i for i in os.listdir(base_path)]# 打乱顺序random.shuffle(image_name_list)print('stating predicting....')for image_name in image_name_list:image = cv2.imread(image_name)image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)result = predict_single(model, image_name)# 画矩形框和对应的类别信息for left_up, right_bottom, class_name, _, prob in result:# 获取颜色color = Color[VOC_CLASSES.index(class_name)]# 画矩形cv2.rectangle(image, left_up, right_bottom, color, 2)# 获取类型信息和对应概率,此时为str类型label = class_name + str(round(prob, 2))# 把类别和概率信息写上,还要为这个信息加上一个矩形框text_size, baseline = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.4, 1)p1 = (left_up[0], left_up[1] - text_size[1])cv2.rectangle(image, (p1[0] - 2 // 2, p1[1] - 2 - baseline), (p1[0] + text_size[0], p1[1] + text_size[1]),color, -1)cv2.putText(image, label, (p1[0], p1[1] + baseline), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 255, 255), 1, 8)# 显示图片plt.figure()plt.imshow(image)plt.show()time.sleep(2)# 是否保存结果图片cv2.imwrite('./test_images/result.jpg', image)

6、验证图片

 

7、完整代码和权重下载

链接:https://pan.baidu.com/s/1nJbfAxTUshCy4giFPoy7sA 
提取码:yov1 

8、总结

需要改进:

  首先,肯定是batch_size的大小,如果有得选,我也想把batch_size改大一点_
  其次,是优化器的选择和其参数的设置,本次选用的SGD,参数都是默认设置的,我在想是否改为Adam会好一点
  另外,本次学习率从0.01开始,分别在20和35epoch时除以10,感觉初始的学习率有点大了,是否可以减小学习率?
  最后,所谓微调,我们继承了ResNet50的权重,但是这个ResNet50是在尺度为224*224下训练的,而我们的目标检测将分辨率改为了448*448,是否需要先进行一定程度的微调才来训练yolov1值得思考。

这篇关于经典目标检测YOLOV1理论基础的编码实现(通过编码理解原理)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/932229

相关文章

Python实现AVIF图片与其他图片格式间的批量转换

《Python实现AVIF图片与其他图片格式间的批量转换》这篇文章主要为大家详细介绍了如何使用Pillow库实现AVIF与其他格式的相互转换,即将AVIF转换为常见的格式,比如JPG或PNG,需要的小... 目录环境配置1.将单个 AVIF 图片转换为 JPG 和 PNG2.批量转换目录下所有 AVIF 图

Pydantic中model_validator的实现

《Pydantic中model_validator的实现》本文主要介绍了Pydantic中model_validator的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价... 目录引言基础知识创建 Pydantic 模型使用 model_validator 装饰器高级用法mo

AJAX请求上传下载进度监控实现方式

《AJAX请求上传下载进度监控实现方式》在日常Web开发中,AJAX(AsynchronousJavaScriptandXML)被广泛用于异步请求数据,而无需刷新整个页面,:本文主要介绍AJAX请... 目录1. 前言2. 基于XMLHttpRequest的进度监控2.1 基础版文件上传监控2.2 增强版多

Redis分片集群的实现

《Redis分片集群的实现》Redis分片集群是一种将Redis数据库分散到多个节点上的方式,以提供更高的性能和可伸缩性,本文主要介绍了Redis分片集群的实现,具有一定的参考价值,感兴趣的可以了解一... 目录1. Redis Cluster的核心概念哈希槽(Hash Slots)主从复制与故障转移2.

springboot+dubbo实现时间轮算法

《springboot+dubbo实现时间轮算法》时间轮是一种高效利用线程资源进行批量化调度的算法,本文主要介绍了springboot+dubbo实现时间轮算法,文中通过示例代码介绍的非常详细,对大家... 目录前言一、参数说明二、具体实现1、HashedwheelTimer2、createWheel3、n

使用Python实现一键隐藏屏幕并锁定输入

《使用Python实现一键隐藏屏幕并锁定输入》本文主要介绍了使用Python编写一个一键隐藏屏幕并锁定输入的黑科技程序,能够在指定热键触发后立即遮挡屏幕,并禁止一切键盘鼠标输入,这样就再也不用担心自己... 目录1. 概述2. 功能亮点3.代码实现4.使用方法5. 展示效果6. 代码优化与拓展7. 总结1.

Mybatis 传参与排序模糊查询功能实现

《Mybatis传参与排序模糊查询功能实现》:本文主要介绍Mybatis传参与排序模糊查询功能实现,本文通过实例代码给大家介绍的非常详细,感兴趣的朋友跟随小编一起看看吧... 目录一、#{ }和${ }传参的区别二、排序三、like查询四、数据库连接池五、mysql 开发企业规范一、#{ }和${ }传参的

Docker镜像修改hosts及dockerfile修改hosts文件的实现方式

《Docker镜像修改hosts及dockerfile修改hosts文件的实现方式》:本文主要介绍Docker镜像修改hosts及dockerfile修改hosts文件的实现方式,具有很好的参考价... 目录docker镜像修改hosts及dockerfile修改hosts文件准备 dockerfile 文

Python基础文件操作方法超详细讲解(详解版)

《Python基础文件操作方法超详细讲解(详解版)》文件就是操作系统为用户或应用程序提供的一个读写硬盘的虚拟单位,文件的核心操作就是读和写,:本文主要介绍Python基础文件操作方法超详细讲解的相... 目录一、文件操作1. 文件打开与关闭1.1 打开文件1.2 关闭文件2. 访问模式及说明二、文件读写1.

基于SpringBoot+Mybatis实现Mysql分表

《基于SpringBoot+Mybatis实现Mysql分表》这篇文章主要为大家详细介绍了基于SpringBoot+Mybatis实现Mysql分表的相关知识,文中的示例代码讲解详细,感兴趣的小伙伴可... 目录基本思路定义注解创建ThreadLocal创建拦截器业务处理基本思路1.根据创建时间字段按年进