【PyTorch】单目标检测项目

对象检测是在图像中查找特定对象位置的过程,用于处理单对象或多对象检测问题。单对象检测在给定图像中仅定位一个对象。对象的位置可以通过边界框定义。单对象检测使用四个数字预测边界框。对于正方形物体,可以固定宽度和高度,并简化问题以仅预测两个数字,例如使用两个数字来定位眼睛视网膜的中央凹。

目录

边界框定位格式

Training400数据集

探索性数据分析

用于对象检测的数据转换

创建自定义数据集

搭建残差网络模型

定义损失函数、优化器和 IOU 指标

模型训练与评估


边界框定位格式

以下列格式之一可以表示一个包含四个数字的边界框:

  • [x0,y0,w,h]
  • [x0、y0、x1、y1]
  • [xc、yc、w、h]

  • x0, y0: 边界框左上角的坐标
  • x1, y1: 边界框右下角的坐标
  • xc, yc: 边界框质心的坐标
  • w, h: 边界框的宽度和高度

Training400数据集

年龄相关性黄斑变性,简称AMD,是黄斑区的一种退行性疾病。主要发生在45岁以上,发病率甚至高于老年糖尿病视网膜病变。
AMD的早期诊断对治疗和预后至关重要。眼底照相是眼底检查的基本方法之一。当前的数据集由AMD和非AMD(近视、正常对照等)照片组成。在这些照片中可以发现AMD的典型症状包括脉络膜小疣、渗出、出血等。

通过链接下载iChallenge-AMD-Training400.zip文件,将 .zip 文件移动到与代码位于同一位置的名为 data 的文件夹中。将 .zip 文件解压缩到名为 data/Training400 的文件夹中。文件夹应包含一个名为 AMD 的文件夹(89张图像),一个名为 Non-AMD 的文件夹( 311 张图像),以及一个名为 Fovea_location.xlsx 的 Excel 文件(400张图像中中央凹的质心位置)

Home - Grand Challenge Age-related Macular Degeneration Challengeicon-default.png?t=N7T8https://amd.grand-challenge.org/也可借助以下链接下载

Baidu Research Open-Access Dataset - DownloadDownload Baidu Research Open-Access Dataseticon-default.png?t=N7T8https://ai.baidu.com/broad/download

探索性数据分析

通常进行探索性数据分析以了解数据的特征。在探索性数据分析中,检查数据集并使用箱线图、直方图和其他可视化工具可视化数据的样本或统计特征。

import os
import pandas as pdpath2data="./data/"
path2labels=os.path.join(path2data,"Training400","Fovea_location.xlsx")# 读取Excel文件,将ID列作为索引。注意,1.2.0之后的xlrd不能直接读取xlsx文件,需指定引擎为openpyxl,
labels_df=pd.read_excel(path2labels,engine='openpyxl',index_col="ID")# conda install seaborn
import seaborn as sns
# 设置matplotlib在jupyter中显示图像
%matplotlib inline# 从labels_df中提取imgName列的第一个元素,并赋值给AorN
AorN=[imn[0] for imn in labels_df.imgName]
# 使用seaborn绘制散点图,x轴为labels_df中的Fovea_X列,y轴为labels_df中的Fovea_Y列,颜色映射依据为AorN
sns.scatterplot(x=labels_df['Fovea_X'], y=labels_df['Fovea_Y'],hue=AorN)

import numpy as np
from PIL import Image, ImageDraw
import matplotlib.pylab as plt# 设置随机数生成器的种子,相同的种子将生成相同的随机数序列。有利于调试和重现结果。
np.random.seed(2019)# 设置图形大小
plt.rcParams['figure.figsize'] = (14, 14)
# 调整子图之间的间距
plt.subplots_adjust(wspace=0, hspace=0)
# 设置子图的行数和列数
nrows,ncols=4,4# 从labels_df中获取imgName列
imgName=labels_df["imgName"]# 获取labels_df的索引
ids=labels_df.index# 从ids中随机选择nrows*ncols个元素
rndIds=np.random.choice(ids,nrows*ncols)
print(rndIds)def load_img_label(labels_df,id_):    # 获取图片名称imgName=labels_df["imgName"]    # 判断图片名称是否以"A"开头if imgName[id_][0]=="A":# 如果是,则前缀为"AMD"prefix="AMD"else:# 否则,前缀为"Non-AMD"prefix="Non-AMD"# 拼接图片路径fullPath2img=os.path.join(path2data,"Training400",prefix,imgName[id_])# 打开图片img = Image.open(fullPath2img)# 获取图片中心点坐标x=labels_df["Fovea_X"][id_]y=labels_df["Fovea_Y"][id_]# 返回图片和中心点坐标label=(x,y)return img,labeldef show_img_label(img,label,w_h=(50,50),thickness=2):   # 定义函数show_img_label,用于在图像上绘制矩形框,并显示图像w,h=w_h                   # 获取矩形框的宽度和高度cx,cy=labeldraw = ImageDraw.Draw(img)# 创建一个绘图对象draw.rectangle(((cx-w/2, cy-h/2), (cx+w/2, cy+h/2)),outline="green",width=thickness)# 在图像上绘制矩形框,颜色为绿色,线宽为thicknessplt.imshow(np.asarray(img))# 显示图像# 遍历rndIds中的每个元素
for i,id_ in enumerate(rndIds):# 加载图片和标签img,label=load_img_label(labels_df,id_)   # 打印图片和标签的大小print(img.size,label)# 在子图中显示图片和标签plt.subplot(nrows, ncols, i+1) show_img_label(img,label,w_h=(150,150),thickness=20)# 设置子图的标题为imgName中对应id_的值plt.title(imgName[id_])

 

# 定义两个空列表,用于存储图像的高度和宽度
h_list,w_list=[],[]
# 遍历ids列表中的每个元素
for id_ in ids:# 如果imgName[id_][0]等于"A",则prefix为"AMD",否则为"Non-AMD"if imgName[id_][0]=="A":prefix="AMD"else:prefix="Non-AMD"# 图像的完整路径fullPath2img=os.path.join(path2data,"Training400",prefix,imgName[id_])# 加载图像img = Image.open(fullPath2img)# 获取图像的高度和宽度h,w=img.size# 将高度和宽度添加到列表中h_list.append(h)w_list.append(w)# 使用seaborn库中的distplot函数绘制直方图,参数a为数据列表,kde参数设置为False表示不绘制核密度估计曲线
sns.distplot(a=h_list, kde=False)

用于对象检测的数据转换

数据增强和转换是训练深度学习算法的关键步骤,尤其是对于小型数据集。这里的iChallenge-AMD数据集只有 400 张图像,可视为小数据集。由于图像的大小不同,需要将所有图像的大小调整为预先确定的大小。然后可以利用各种增强技术,例如水平翻转、垂直翻转和平移,在训练期间扩展数据集。

在对象检测任务中,当对图像进行转换时还需要更新标签。例如水平翻转图像时,图像中对象的位置会发生变化。需要构建一个用于在单对象检测中转换图像和标签的函数,包括水平翻转、垂直翻转、平移和调整大小等。

import torchvision.transforms.functional as TF# 定义一个函数,用于调整图像和标签的大小
def resize_img_label(image,label=(0.,0.),target_size=(256,256)):# 获取原始图像的宽度和高度w_orig,h_orig = image.size   # 获取目标图像的宽度和高度w_target,h_target = target_size# 获取标签的坐标cx, cy= label# 调整图像大小image_new = TF.resize(image,target_size)# 调整标签大小label_new= cx/w_orig*w_target, cy/h_orig*h_target# 返回调整后的图像和标签return image_new,label_new# 加载图片和标签
img, label=load_img_label(labels_df,1)   
# 打印图片和标签的大小
print(img.size,label)# 调整图片和标签的大小
img_r,label_r=resize_img_label(img,label)
# 打印调整后图片和标签的大小
print(img_r.size,label_r)# 在子图1中显示原始图片和标签
plt.subplot(1,2,1)
show_img_label(img,label,w_h=(150,150),thickness=20)
# 在子图2中显示调整后图片和标签
plt.subplot(1,2,2)
show_img_label(img_r,label_r)

# 定义一个函数,用于随机水平翻转图像和标签
def random_hflip(image,label):# 获取图像的宽度和高度w,h=image.size# 获取标签的坐标x,y=label        # 对图像进行水平翻转image = TF.hflip(image)# 更新标签的坐标label = w-x, y# 返回翻转后的图像和标签return image,label# 加载图片和标签
img, label=load_img_label(labels_df,1)   # 调整图片和标签的大小
img_r,label_r=resize_img_label(img,label)# 随机水平翻转图片和标签
img_fh,label_fh=random_hflip(img_r,label_r)# 显示原始图片和标签
plt.subplot(1,2,1)
show_img_label(img_r,label_r)
# 显示翻转后的图片和标签
plt.subplot(1,2,2)
show_img_label(img_fh,label_fh)

# 定义一个函数,用于随机垂直翻转图像和标签
def random_vflip(image,label):# 获取图像的宽度和高度w,h=image.size# 获取标签的坐标x,y=label# 对图像进行垂直翻转image = TF.vflip(image)# 翻转标签的坐标label = x, h-y# 返回翻转后的图像和标签return image, label# 加载图片和标签
img, label=load_img_label(labels_df,7)   # 调整图片和标签的大小
img_r,label_r=resize_img_label(img,label)# 随机翻转图片和标签
img_fv,label_fv=random_vflip(img_r,label_r)# 显示原始图片和标签
plt.subplot(1,2,1)
show_img_label(img_r,label_r)
# 显示翻转后的图片和标签
plt.subplot(1,2,2)
show_img_label(img_fv,label_fv)

import numpy as np
np.random.seed(1)# 定义一个函数,用于对图像和标签进行随机平移
def random_shift(image,label,max_translate=(0.2,0.2)):# 获取图像的宽度和高度w,h=image.size# 获取最大平移量max_t_w, max_t_h=max_translate# 获取标签的坐标cx, cy=label# 生成一个随机数,范围在-1到1之间trans_coef=np.random.rand()*2-1# 计算平移量w_t = int(trans_coef*max_t_w*w)h_t = int(trans_coef*max_t_h*h)# 对图像进行平移image=TF.affine(image,translate=(w_t, h_t),shear=0,angle=0,scale=1)# 更新标签的坐标label = cx+w_t, cy+h_t# 返回平移后的图像和标签return image,label# 加载图片和标签
img, label=load_img_label(labels_df,1)   # 调整图片和标签的大小
img_r,label_r=resize_img_label(img,label)# 随机平移图片和标签
img_t,label_t=random_shift(img_r,label_r,max_translate=(.5,.5))# 显示原始图片和标签
plt.subplot(1,2,1)
show_img_label(img_r,label_r)
# 显示平移后的图片和标签
plt.subplot(1,2,2)
show_img_label(img_t,label_t)

# 定义一个transformer函数,用于对图像和标签进行变换
def transformer(image, label, params):# 调用resize_img_label函数,对图像和标签进行resize操作image,label=resize_img_label(image,label,params["target_size"])# 随机生成一个0-1之间的随机数,如果小于p_hflip,则对图像和标签进行水平翻转if random.random() < params["p_hflip"]:image,label=random_hflip(image,label)# 随机生成一个0-1之间的随机数,如果小于p_vflip,则对图像和标签进行垂直翻转if random.random() < params["p_vflip"]:            image,label=random_vflip(image,label)# 随机生成一个0-1之间的随机数,如果小于p_shift,则对图像和标签进行平移if random.random() < params["p_shift"]:                            image,label=random_shift(image,label, params["max_translate"])# 将图像转换为tensor类型image=TF.to_tensor(image)# 返回变换后的图像和标签return image, labelimport random
# 设置随机数种子
np.random.seed(0)
random.seed(0)# 从数据集中加载一张图片和对应的标签
img, label=load_img_label(labels_df,1)   # 定义图像变换参数
params={"target_size" : (256, 256),  # 目标图像大小"p_hflip" : 1.0,  # 水平翻转概率"p_vflip" : 1.0,  # 垂直翻转概率"p_shift" : 1.0,  # 平移概率"max_translate": (0.2, 0.2),  # 最大平移距离
}
# 对图像进行变换
img_t,label_t=transformer(img,label,params)# 创建一个1行2列的子图,并选择第一个子图
plt.subplot(1,2,1)
# 显示图片和标签,图片大小为150x150,线条粗细为20
show_img_label(img,label,w_h=(150,150),thickness=20)
# 创建一个1行2列的子图,并选择第二个子图
plt.subplot(1,2,2)
# 显示图片和标签,图片为img_t,标签为label_t
show_img_label(TF.to_pil_image(img_t),label_t)

调整图片亮度

# 加载图片和标签
img, label=load_img_label(labels_df,1)   # 调整图片和标签的大小
img_r,label_r=resize_img_label(img,label)# 调整图片的亮度
img_t=TF.adjust_brightness(img_r,brightness_factor=0.5)
label_t=label_r# 显示原始图片和标签
plt.subplot(1,2,1)
show_img_label(img_r,label_r)
# 显示调整亮度后的图片和标签
plt.subplot(1,2,2)
show_img_label(img_t,label_t)

 

# 定义一个函数,用于将两个列表中的元素相除
def scale_label(a,b):# 使用zip函数将两个列表中的元素一一对应div = [ai/bi for ai,bi in zip(a,b)]# 返回相除后的结果return divdef transformer(image, label, params):# 调整图像和标签的大小image,label=resize_img_label(image,label,params["target_size"])# 随机水平翻转图像和标签if random.random() < params["p_hflip"]:image,label=random_hflip(image,label)# 随机垂直翻转图像和标签if random.random() < params["p_vflip"]:            image,label=random_vflip(image,label)# 随机平移图像和标签if random.random() < params["p_shift"]:                            image,label=random_shift(image,label, params["max_translate"])# 随机调整图像的亮度if random.random() < params["p_brightness"]:brightness_factor=1+(np.random.rand()*2-1)*params["brightness_factor"]image=TF.adjust_brightness(image,brightness_factor)# 随机调整图像的对比度if random.random() < params["p_contrast"]:contrast_factor=1+(np.random.rand()*2-1)*params["contrast_factor"]image=TF.adjust_contrast(image,contrast_factor)# 随机调整图像的gamma值if random.random() < params["p_gamma"]:gamma=1+(np.random.rand()*2-1)*params["gamma"]image=TF.adjust_gamma(image,gamma)# 如果需要,调整标签的大小if params["scale_label"]:label=scale_label(label,params["target_size"])# 将图像转换为张量image=TF.to_tensor(image)return image, label# 设置随机种子,以保证结果的可重复性
np.random.seed(0)
random.seed(0)# 从labels_df中加载第1张图片和对应的标签
img, label=load_img_label(labels_df,1)#设定参数
params={"target_size" : (256, 256),"p_hflip" : 1.0,"p_vflip" : 1.0,"p_shift" : 1.0,"max_translate": (0.5, 0.5),"p_brightness": 1.0,"brightness_factor": 0.8,"p_contrast": 1.0,"contrast_factor": 0.8,"p_gamma": 1.0,"gamma": 0.4,"scale_label": False,
}
img_t,label_t=transformer(img,label,params)# 创建一个1行2列的子图,并选择第一个子图
plt.subplot(1,2,1)
# 显示图片和标签,图片大小为150x150,线条粗细为20
show_img_label(img,label,w_h=(150,150),thickness=20)
# 创建一个1行2列的子图,并选择第二个子图
plt.subplot(1,2,2)
# 显示图片和标签,图片为img_t,标签为label_t
show_img_label(TF.to_pil_image(img_t),label_t)

 

创建自定义数据集

使用 torch.utils.data 中的 Dataset 类来创建用于加载和处理数据的自定义数据集,对 Dataset 类进行子类化并重写 __init__ 和 __getitem__ 函数。__len__ 函数返回数据集长度,可使用 Python len 函数调用。__getitem__ 函数返回指定索引处的图像。使用 torch.utils.data 中的 Dataloader 类来创建数据加载器。使用数据加载器自动获取小批量数据并进行处理。

from torch.utils.data import Dataset
from PIL import Imageclass AMD_dataset(Dataset):def __init__(self, path2data, transform, trans_params):      # 初始化函数,传入数据路径、转换函数和转换参数pass    def __len__(self):# 返回数据集的大小return len(self.labels)def __getitem__(self, idx):# 根据索引获取数据集中的一个样本passdef __init__(self, path2data, transform, trans_params):      # 获取标签文件的路径path2labels=os.path.join(path2data,"Training400","Fovea_location.xlsx")# 读取标签文件,并将ID列作为索引labels_df=pd.read_excel(path2labels,engine='openpyxl',index_col="ID")# 获取标签数据,包括Fovea_X和Fovea_Y两列self.labels = labels_df[["Fovea_X","Fovea_Y"]].values# 获取图片名称self.imgName=labels_df["imgName"]# 获取图片IDself.ids=labels_df.index# 获取图片的完整路径self.fullPath2img=[0]*len(self.ids)for id_ in self.ids:# 根据图片名称的前缀,确定图片所在的文件夹if self.imgName[id_][0]=="A":prefix="AMD"else:prefix="Non-AMD"# 获取图片的完整路径self.fullPath2img[id_-1]=os.path.join(path2data,"Training400",prefix,self.imgName[id_])# 获取数据转换函数和数据转换参数self.transform = transformself.trans_params=trans_paramsdef __getitem__(self, idx):# 打开指定索引的图像image = Image.open(self.fullPath2img[idx])  # 获取指定索引的标签label= self.labels[idx]# 对图像和标签进行变换image,label = self.transform(image,label,self.trans_params)# 返回变换后的图像和标签return image, label# 重写函数
AMD_dataset.__init__=__init__
AMD_dataset.__getitem__=__getitem__# 定义训练过程中的参数
trans_params_train={"target_size" : (256, 256),  # 目标图像大小"p_hflip" : 0.5,  # 水平翻转概率"p_vflip" : 0.5,  # 垂直翻转概率"p_shift" : 0.5,  # 平移概率"max_translate": (0.2, 0.2),  # 最大平移量"p_brightness": 0.5,  # 亮度调整概率"brightness_factor": 0.2,  # 亮度调整因子"p_contrast": 0.5,  # 对比度调整概率"contrast_factor": 0.2,  # 对比度调整因子"p_gamma": 0.5,  # 伽马调整概率"gamma": 0.2,  # 伽马调整因子"scale_label": True,  # 是否调整标签大小
}# 定义验证过程中的参数
trans_params_val={"target_size" : (256, 256),"p_hflip" : 0.0,"p_vflip" : 0.0,"p_shift" : 0.0,"p_brightness": 0.0,"p_contrast": 0.0,"p_gamma": 0.0,"gamma": 0.0,"scale_label": True,    
}# 创建AMD_dataset类的实例,用于训练集
amd_ds1=AMD_dataset(path2data,transformer,trans_params_train)
# 创建AMD_dataset类的实例,用于验证集
amd_ds2=AMD_dataset(path2data,transformer,trans_params_val)from sklearn.model_selection import ShuffleSplit# 创建ShuffleSplit对象,设置分割次数为1,测试集大小为0.2,随机种子为0
sss = ShuffleSplit(n_splits=1, test_size=0.2, random_state=0)# 创建索引列表
indices=range(len(amd_ds1))# 遍历ShuffleSplit对象,获取训练集和验证集的索引
for train_index, val_index in sss.split(indices):# 打印训练集的长度print(len(train_index))# 打印分隔符print("-"*10)# 打印验证集的长度print(len(val_index))

from torch.utils.data import Subset# 创建训练集子集
train_ds=Subset(amd_ds1,train_index)
# 打印训练集子集的长度
print(len(train_ds))
print("-"*10)
# 创建验证集子集
val_ds=Subset(amd_ds2,val_index)
# 打印验证集子集的长度
print(len(val_ds))

import matplotlib.pyplot as plt
import numpy as np
%matplotlib inlinenp.random.seed(0)def show(img,label=None):# 将img转换为numpy数组,并转置维度npimg = img.numpy().transpose((1,2,0))# 显示图片plt.imshow(npimg)# 如果label不为空if label is not None:# 将label缩放到图片尺寸label=rescale_label(label,img.shape[1:])        # 获取label的x和y坐标x,y=label# 在图片上绘制labelplt.plot(x,y,'b+',markersize=20) # 创建一个5x5大小的图像
plt.figure(figsize=(5,5))
# 遍历训练数据集
for img,label in train_ds:# 显示图像和标签show(img,label)# 只显示第一张图像和标签break

plt.figure(figsize=(5,5))
# 遍历验证数据集
for img,label in val_ds:show(img,label)break

from torch.utils.data import DataLoader
# 创建训练数据集的DataLoader,batch_size为8,shuffle为True,表示每次迭代时都会打乱数据集
train_dl = DataLoader(train_ds, batch_size=8, shuffle=True)
# 创建验证数据集的DataLoader,batch_size为16,shuffle为False,表示每次迭代时不会打乱数据集
val_dl = DataLoader(val_ds, batch_size=16, shuffle=False) # 遍历训练数据集
for img_b, label_b in train_dl:# 打印图像的形状和数据类型print(img_b.shape,img_b.dtype)print(label_b)break 

import torch# 遍历训练数据集
for img_b, label_b in train_dl:# 打印图片的形状和数据类型print(img_b.shape,img_b.dtype)# 将标签堆叠成一维向量label_b=torch.stack(label_b,1)# 将标签的数据类型转换为浮点型label_b=label_b.type(torch.float32)# 打印标签的形状和数据类型print(label_b.shape,label_b.dtype)break

# 遍历验证集数据
for img_b, label_b in val_dl:# 打印图片的形状和数据类型print(img_b.shape,img_b.dtype)# 将标签堆叠成一维向量label_b=torch.stack(label_b,1)# 将标签的数据类型转换为浮点型label_b=label_b.type(torch.float32)# 打印标签的形状和数据类型print(label_b.shape,label_b.dtype)break

搭建残差网络模型

残差网络(Residual Network),也被称为ResNet,是一种深度神经网络架构,旨在解决梯度消失和训练困难的问题。核心思想是通过引入残差块(residual blocks)来构建网络,并通过跳跃连接将输入直接添加到层输出上。残差块就是包含了跳跃连接的块。具体而言,在每个块或子模块内部,输入被加到该块/子模块计算后得到的输出上,并且这两者尺寸必须相同。然后再将此结果送入下一个块/子模块进行处理。

将中央凹中心预测为眼睛图像中的x和y坐标,构建一个由多个卷积层和池化层组成的模型。

该模型将接收调整好的RGB图像,提供与中央凹坐标相对应的两个线性输出。模型将利用ResNet论文Deep Residual Learning for Image Recognition中介绍的skip connection跳跃连接技术。

 

跳跃连接指的是将输入数据直接添加到网络某一层输出之上。这种设计使得信息可以更自由地流动,并且保留了原始输入数据中的细节和语义信息。 使信息更容易传播到后面的层次,避免了信息丢失。跳跃连接通常会通过求和操作或拼接操作来实现。

 

import torch.nn as nn
import torch.nn.functional as F# 定义一个Net类,继承自nn.Module类
class Net(nn.Module):# 初始化函数,接收一个参数paramsdef __init__(self, params):# 调用父类的初始化函数super(Net, self).__init__()# 定义前向传播函数,接收一个参数xdef forward(self, x):return x# 定义一个初始化函数,接收参数params
def __init__(self, params):# 初始化父类Netsuper(Net, self).__init__()# 获取输入形状C_in,H_in,W_in=params["input_shape"]# 获取初始滤波器数量init_f=params["initial_filters"] # 获取输出数量num_outputs=params["num_outputs"] # 定义卷积层self.conv1 = nn.Conv2d(C_in, init_f, kernel_size=3,stride=2,padding=1)self.conv2 = nn.Conv2d(init_f+C_in, 2*init_f, kernel_size=3,stride=1,padding=1)self.conv3 = nn.Conv2d(3*init_f+C_in, 4*init_f, kernel_size=3,padding=1)self.conv4 = nn.Conv2d(7*init_f+C_in, 8*init_f, kernel_size=3,padding=1)self.conv5 = nn.Conv2d(15*init_f+C_in, 16*init_f, kernel_size=3,padding=1)# 定义全连接层1self.fc1 = nn.Linear(16*init_f, num_outputs)# 定义前向传播函数
def forward(self, x):# 对输入进行平均池化,池化核大小为4,步长为4identity=F.avg_pool2d(x,4,4)# 对输入进行卷积操作,并使用ReLU激活函数x = F.relu(self.conv1(x))# 对输入进行最大池化,池化核大小为2,步长为2x = F.max_pool2d(x, 2, 2)# 将卷积后的结果与池化后的结果进行拼接x = torch.cat((x, identity), dim=1)# 对输入进行平均池化,池化核大小为2,步长为2identity=F.avg_pool2d(x,2,2)# 对输入进行卷积操作,并使用ReLU激活函数x = F.relu(self.conv2(x))# 对输入进行最大池化,池化核大小为2,步长为2x = F.max_pool2d(x, 2, 2)# 将卷积后的结果与池化后的结果进行拼接x = torch.cat((x, identity), dim=1)# 对输入进行平均池化,池化核大小为2,步长为2identity=F.avg_pool2d(x,2,2)# 对输入进行卷积操作,并使用ReLU激活函数x = F.relu(self.conv3(x))# 对输入进行最大池化,池化核大小为2,步长为2x = F.max_pool2d(x, 2, 2)# 将卷积后的结果与池化后的结果进行拼接x = torch.cat((x, identity), dim=1)# 对输入进行平均池化,池化核大小为2,步长为2identity=F.avg_pool2d(x,2,2)# 对输入进行卷积操作,并使用ReLU激活函数x = F.relu(self.conv4(x))# 对输入进行最大池化,池化核大小为2,步长为2x = F.max_pool2d(x, 2, 2)# 将卷积后的结果与池化后的结果进行拼接x = torch.cat((x, identity), dim=1)# 对输入进行卷积操作,并使用ReLU激活函数x = F.relu(self.conv5(x))# 对输入进行自适应平均池化,池化核大小为1x=F.adaptive_avg_pool2d(x,1)# 将输入展平成一维向量x = x.reshape(x.size(0), -1)# 对输入进行全连接操作x = self.fc1(x)return x    # 重写
Net.__init__=__init__
Net.forward=forward# 定义模型参数
params_model={"input_shape": (3,256,256), "initial_filters": 16, "num_outputs": 2,  }# 创建模型
model = Net(params_model)# 检查CUDA是否可用
if torch.cuda.is_available():# 如果可用,将设备设置为CUDAdevice = torch.device("cuda")# 将模型移动到CUDA设备上model=model.to(device) 
print(model)

定义损失函数、优化器和 IOU 指标

单对象/多对象检测问题的常见损耗函数是均方误差(MSE)和平滑L1损耗。如果绝对元素误差低于 1,则平滑 L1 损失使用平方项,否则使用 L1 项。它对异常值的敏感度低于 MSE,并且在某些情况下可以防止梯度爆炸。这里将使用平滑的 L1 损失。平滑 L1 损失可以参照:

torch.nn — PyTorch 2.3 documentationicon-default.png?t=N7T8https://pytorch.org/docs/stable/nn.html#smoothl1loss为自动更新模型参数还需要定义优化器。最后为对象检测问题定义一个性能指标,称为 Jaccard 指数,即Intersection over Union(IOU)。

# 定义损失函数,使用SmoothL1Loss,reduction参数设置为"sum",表示将所有样本的损失相加
loss_func = nn.SmoothL1Loss(reduction="sum")    # 定义变量n和c
n,c=8,2
# 定义变量y,形状为(n, c),值为0.5,requires_grad=True表示需要计算梯度
y = 0.5 * torch.ones(n, c, requires_grad=True)
# 打印y的形状
print(y.shape)# 定义变量target,形状为(n, c),值为0,requires_grad=False表示不需要计算梯度
target = torch.zeros(n, c, requires_grad=False)
# 打印target的形状
print(target.shape)# 计算损失函数,loss_func为自定义的损失函数
loss = loss_func(y, target)
# 打印损失函数的值
print(loss.item())# 重新定义变量y,形状为(n, c),值为2,requires_grad=True表示需要计算梯度
y = 2 * torch.ones(n, c, requires_grad=True)
# 重新定义变量target,形状为(n, c),值为0,requires_grad=False表示不需要计算梯度
target = torch.zeros(n, c, requires_grad=False)
# 重新计算损失函数
loss = loss_func(y, target)
# 打印损失函数的值
print(loss.item())

from torch import optim# 定义优化器,使用Adam优化算法,优化模型参数,学习率为3e-4
opt = optim.Adam(model.parameters(), lr=3e-4)# 定义一个函数,用于获取当前的学习率
def get_lr(opt):# 遍历opt中的param_groupsfor param_group in opt.param_groups:# 返回param_group中的学习率return param_group['lr']# 调用get_lr函数,获取当前的学习率
current_lr=get_lr(opt)
# 打印当前的学习率
print('current lr={}'.format(current_lr))

 

from torch.optim.lr_scheduler import ReduceLROnPlateau# 定义学习率调度器,当验证集上的损失不再下降时,将学习率降低为原来的0.5倍,等待20个epoch后再次降低学习率
lr_scheduler = ReduceLROnPlateau(opt, mode='min',factor=0.5, patience=20,verbose=1)
# 遍历100次
for i in range(100):# 每次循环调用lr_scheduler.step(1)方法lr_scheduler.step(1)

# 定义一个函数,将中心坐标和宽高转换为边界框
def cxcy2bbox(cxcy,w=50./256,h=50./256):# 创建一个与cxcy形状相同的张量,每个元素都为ww_tensor=torch.ones(cxcy.shape[0],1,device=cxcy.device)*w# 创建一个与cxcy形状相同的张量,每个元素都为hh_tensor=torch.ones(cxcy.shape[0],1,device=cxcy.device)*h# 将cxcy的第一列提取出来,并增加一个维度cx=cxcy[:,0].unsqueeze(1)# 将cxcy的第二列提取出来,并增加一个维度cy=cxcy[:,1].unsqueeze(1)# 将cx、cy、w_tensor、h_tensor按列拼接起来boxes=torch.cat((cx,cy, w_tensor, h_tensor), -1) # 将boxes的第二列和第三列相减,第四列和第五列相加,并按行拼接起来return torch.cat((boxes[:, :2] - boxes[:, 2:]/2, boxes[:, :2] + boxes[:, 2:]/2), 1) # 设置随机种子
torch.manual_seed(0)# 生成一个1行2列的随机数矩阵
cxcy=torch.rand(1,2)
# 打印中心点坐标
print("center:", cxcy*256)# 将中心点坐标转换为边界框
bb=cxcy2bbox(cxcy)
# 打印边界框
print("bounding box", bb*256)

import torchvision
# 定义一个函数,用于计算输出和目标之间的IOU
def metrics_batch(output, target):# 将输出和目标转换为边界框格式output=cxcy2bbox(output)target=cxcy2bbox(target)# 计算输出和目标之间的IOUiou=torchvision.ops.box_iou(output, target)# 返回对角线上的IOU之和return torch.diagonal(iou, 0).sum().item()# 定义n和c的值
n,c=8,2
# 生成一个随机张量,形状为(n, c),并将其放在device上
target = torch.rand(n, c, device=device)
# 将cxcy格式的目标转换为bbox格式的目标
target=cxcy2bbox(target)
# 计算batch中的指标
metrics_batch(target,target)

# 定义一个函数loss_batch,用于计算损失函数
def loss_batch(loss_func, output, target, opt=None): # target:真实值pass  # 计算损失函数的输出loss = loss_func(output, target)# 计算批量的度量metric_b = metrics_batch(output,target)# 如果有优化器,则进行反向传播和参数更新if opt is not None:opt.zero_grad()  # 将梯度清零loss.backward()  # 反向传播opt.step()  # 更新参数# 返回损失和度量return loss.item(), metric_b# 遍历训练数据集
for xb,label_b in train_dl:# 将标签数据堆叠成一维张量label_b=torch.stack(label_b,1)# 将标签数据转换为浮点型label_b=label_b.type(torch.float32)# 将标签数据移动到指定设备上label_b=label_b.to(device)# 计算损失函数l,m=loss_batch(loss_func,label_b,label_b)# 打印损失函数的值print(l,m)break

模型训练与评估

为提高代码的可读性,定义一些辅助函数。

# 定义一个函数,用于计算模型在数据集上的损失和指标
def loss_epoch(model,loss_func,dataset_dl,sanity_check=False,opt=None):# 初始化运行损失和运行指标running_loss=0.0running_metric=0.0# 获取数据集的长度len_data=len(dataset_dl.dataset)# 遍历数据集for xb, yb in dataset_dl:# 将标签堆叠成一维张量yb=torch.stack(yb,1)# 将标签转换为浮点型,并移动到设备上yb=yb.type(torch.float32).to(device)# 将输入数据移动到设备上,并获取模型输出output=model(xb.to(device))# 计算当前批次的损失和指标loss_b,metric_b=loss_batch(loss_func, output, yb, opt)# 累加损失running_loss+=loss_b# 如果指标不为空,则累加指标if metric_b is not None:running_metric+=metric_b# 计算平均损失loss=running_loss/float(len_data)# 计算平均指标metric=running_metric/float(len_data)# 返回平均损失和平均指标return loss, metricimport copy
# 定义一个函数,用于训练和验证模型
def train_val(model, params):# 获取训练参数num_epochs=params["num_epochs"]loss_func=params["loss_func"]opt=params["optimizer"]train_dl=params["train_dl"]val_dl=params["val_dl"]sanity_check=params["sanity_check"]lr_scheduler=params["lr_scheduler"]path2weights=params["path2weights"]# 初始化损失历史和指标历史loss_history={ "train": [],"val": [],}metric_history={ "train": [],"val": [],}    # 复制当前模型参数best_model_wts = copy.deepcopy(model.state_dict())# 初始化最佳损失best_loss=float('inf')    # 开始训练for epoch in range(num_epochs):# 获取当前学习率current_lr=get_lr(opt)# 打印当前训练轮数和学习率print('Epoch {}/{}, current lr={}'.format(epoch, num_epochs - 1, current_lr))    # 设置模型为训练模式model.train() # 计算训练损失和指标train_loss, train_metric=loss_epoch(model,loss_func,train_dl,sanity_check,opt) # 将训练损失和训练指标添加到历史记录中        loss_history["train"].append(train_loss) metric_history["train"].append(train_metric) # 设置模型为评估模式model.eval() with torch.no_grad(): # 不计算梯度# 计算验证损失和指标val_loss, val_metric=loss_epoch(model,loss_func,val_dl,sanity_check) # 将验证损失和验证指标添加到历史记录中loss_history["val"].append(val_loss) metric_history["val"].append(val_metric)  # 如果验证损失小于最佳损失,则更新最佳损失和最佳模型参数if val_loss < best_loss:best_loss = val_lossbest_model_wts = copy.deepcopy(model.state_dict())torch.save(model.state_dict(), path2weights) # 保存最佳模型参数print("Copied best model weights!") # 打印保存成功信息lr_scheduler.step(val_loss) # 更新学习率# 如果学习率发生变化,则加载最佳模型参数if current_lr != get_lr(opt):print("Loading best model weights!")model.load_state_dict(best_model_wts) print("train loss: %.6f, accuracy: %.2f" %(train_loss,100*train_metric)) # 打印训练损失和指标print("val loss: %.6f, accuracy: %.2f" %(val_loss,100*val_metric)) # 打印验证损失和指标print("-"*10) model.load_state_dict(best_model_wts) # 加载最佳模型参数return model, loss_history, metric_history # 返回模型、损失历史和指标历史
# 定义损失函数为SmoothL1Loss,reduction参数为sum
loss_func=nn.SmoothL1Loss(reduction="sum")
# 定义优化器为Adam,学习率为1e-4
opt = optim.Adam(model.parameters(), lr=1e-4)
# 定义学习率调度器,当验证集上的损失不再下降时,将学习率乘以0.5,等待20个epoch,并输出信息
lr_scheduler = ReduceLROnPlateau(opt, mode='min',factor=0.5, patience=20,verbose=1)# 定义模型保存路径
path2models= "./models/"
# 如果路径不存在,则创建路径
if not os.path.exists(path2models):os.mkdir(path2models)# 定义训练参数
params_train={"num_epochs": 100,  # 训练的epoch数"optimizer": opt,  # 优化器"loss_func": loss_func,  # 损失函数"train_dl": train_dl,  # 训练数据集"val_dl": val_dl,  # 验证数据集"sanity_check": False,  # 是否进行sanity check"lr_scheduler": lr_scheduler,  # 学习率调度器"path2weights": path2models+"weights_smoothl1.pt",  # 模型保存路径
}# 调用train_val函数进行训练和验证
model,loss_hist,metric_hist=train_val(model,params_train)

# 获取训练参数中的训练轮数
num_epochs=params_train["num_epochs"]# 绘制训练和验证损失曲线
plt.title("Train-Val Loss")
plt.plot(range(1,num_epochs+1),loss_hist["train"],label="train")
plt.plot(range(1,num_epochs+1),loss_hist["val"],label="val")
plt.ylabel("Loss")
plt.xlabel("Training Epochs")
plt.legend()
plt.show()

 

# 绘制训练和验证精度曲线
plt.title("Train-Val Accuracy")
plt.plot(range(1,num_epochs+1),metric_hist["train"],label="train")
plt.plot(range(1,num_epochs+1),metric_hist["val"],label="val")
plt.ylabel("Accuracy")
plt.xlabel("Training Epochs")
plt.legend()
plt.show()

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

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

相关文章

Unity:PC包直接查看Log日志

PC端会输出Log日志&#xff0c;位置在&#xff1a; C:\Users\用户名\AppData\LocalLow\公司名\项目名 在这里可以找到类似的文件&#xff1a; 打开便可以看到打印。

解决 elementUI 组件在 WebStorm 中显示为未知标签的问题

解决 elementUI 组件在 WebStorm 中显示为未知标签的问题 一、问题 自从转到 ts 之后&#xff0c;编辑器就一直提示用到的 elementUI 标签未知&#xff0c;一直显示一溜黄色警示&#xff0c;很烦&#xff1a; 二、解决 把它改成大写就可以了。 如下&#xff1a; 把整个项目…

springboot实战(十二)之通过注解的方式记录接口出入参log入库

前言 生产过程中&#xff0c;为了更好的辅助线上问题排查避免不了对接口出入参进行日志输出的时候&#xff0c;并且为了分析接口数据效果需要将每次请求接口的出入参进行落库方便后续的数据分析&#xff0c;这时总不能每个接口入参之后、出参之前都打印一遍日志吧&#xff1f;如…

51单片机嵌入式开发:16、STC89C52RC 嵌入式之 步进电机28BYJ48、四拍八拍操作

STC89C52RC 嵌入式之 步进电机28BYJ48、四拍八拍操作 STC89C52RC 之 步进电机28BYJ48操作1 概述1.1 步进电机概述1.2 28BYJ48概述 2 步进电机工作原理2.1 基本原理2.2 28BYJ48工作原理2.3 28BYJ48控制原理 3 电路及软件代码实现4 步进电机市场价值 STC89C52RC 之 步进电机28BYJ…

使用约束布局该如何设置哪个视图(UILabel)的内容优先被压缩?

引言 在实际项目开发中&#xff0c;约束布局给我们带来了很大的便利&#xff0c;可以帮助我们创建灵活且响应迅速的用户界面。通常情况下&#xff0c;它都能很好地工作&#xff0c;但在一些包含许多UILabel的场景中&#xff0c;比如会话列表的UI&#xff0c;哪个视图会被优先压…

《0基础》学习Python——第二十讲__网络爬虫/<3>

一、用post请求爬取网页 同样与上一节课的get强求的内容差不多&#xff0c;即将requests.get(url,headershead)代码更换成requests.post(url,headershead),其余的即打印获取的内容&#xff0c;如果content-typejson类型的&#xff0c;打印上述代码的请求&#xff0c;则用一个命…

2024论文精读:利用大语言模型(GPT)增强上下文学习去做关系抽取任务

文章目录 1. 前置知识2. 文章通过什么来引出他要解决的问题3. 作者通过什么提出RE任务存在上面所提出的那几个问题3.1 问题一&#xff1a;ICL检索到的**示范**中实体个关系的相关性很低。3.2 问题二&#xff1a;示范中缺乏解释输入-标签映射导致ICL效果不佳。 4. 作者为了解决上…

【Git】(基础篇七)—— IntelliJIDEA集成Git

InteliJ IDEA集成Git 现在有很多的集成工具帮助我们写代码&#xff0c;使用这些工具可以帮助我们加速写代码&#xff0c;很多工具也可以集成git&#xff0c;使用图形工具管理git&#xff0c;相信了解了底层运行逻辑的你能够很快地上手使用这些工具&#xff0c;本文以InteliJ I…

7 Vue3

相比 Vue2 1. 优点 vue3支持vue2的大多数特性&#xff0c;实现对vue2的兼容vue3对比vue2具有明显的性能提升 打包大小减少41%初次渲染快55%&#xff0c;更新快133%内存使用减少54% 更好的支持TypeScript使用Proxy代替defineProperty实现响应式数据 2. 性能提升的原因 静态标…

jmeter-beanshell学习11-从文件获取指定数据

参数文件里的参数可能过段时间就不能用了&#xff0c;需要用新的参数。如果有多个交易&#xff0c;读不同的参数文件&#xff0c;但是数据还是一套&#xff0c;就要改多个参数文件。或者只想执行参数文件的某一行数据&#xff0c;又不想调整参数文件顺序。 第一个问题目前想到…

Ant Design Vue中日期选择器快捷选择 presets 用法

ant写文档的纯懒狗 返回的是一个day.js对象 范围选择时可接受一个数组 具体参考 操作 Day.js 话不多说 直接上代码 <a-range-pickerv-model:value"formData.datePick"valueFormat"YYYY-MM-DD HH:mm:ss"showTime:presets"presets"change&quo…

(前缀和) LeetCode 238. 除自身以外数组的乘积

一. 题目描述 原题链接 给你一个整数数组 nums&#xff0c;返回 数组 answer &#xff0c;其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。 题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。 请 不要使用除法&…

WebRTC通话原理(SDP、STUN、 TURN、 信令服务器)

文章目录 1.媒体协商SDP简介 2.网络协商STUN的工作原理TURN工作原理 3.信令服务器信令服务器的主要功能信令服务器的实现方式 1.媒体协商 比如下面这个例子 A端与B端要想通信 A端视频采用VP8做解码&#xff0c;然后发送给B端&#xff0c;B端怎么解码&#xff1f; B端视频采用…

【论文共读】【翻译】ShuffleNet v1:一种用于移动设备的极其高效的卷积神经网络

[原文地址] https://arxiv.org/pdf/1707.01083 [翻译] 0. 摘要 我们介绍了一种计算效率极高的CNN架构&#xff0c;称为ShuffleNet&#xff0c;该架构专为计算能力非常有限的移动设备&#xff08;例如&#xff0c;10-150 MFLOPs&#xff09;而设计。新架构利用了两个新操作&am…

Ubuntu 22.04.4 LTS (linux) Tomcat 下载 安装配置详细教程

1 官网下载 下载链接 2 ubuntu 服务器安装 #下载 wget https://dlcdn.apache.org/tomcat/tomcat-9/v9.0.91/bin/apache-tomcat-9.0.91.tar.gz #解压 tar zxvf apache-tomcat-9.0.91.tar.gz sudo mv apache-tomcat-9.0.91/ /data/tomcat #配置环境变量 sudo vi /etc/profil…

WebGoC题解(13) 狐猬编程:GoC L4 结业测试 第4题 找木柴

题目描述 小明今天找了n跟木柴&#xff0c;但是木柴太多了&#xff0c;小明只能拿走m根木柴&#xff0c;小明希望拿走的木柴都是剩下的木柴中最长的&#xff0c;小明还画出以下图形 例如 输入 5 3 10 20 30 40 50 小明要拿走30 40 50 这3根木柴 从大到小画出以下图形 矩形的宽…

linux cpu 占用超100% 分析。

感谢: https://www.cnblogs.com/wolfstark/p/16450131.html 总结&#xff1a; 查看进程中各个线程占用百分比 top -H -p <pid> 某线程100%了 说明 任务处理不过来 会卡 但是永远不可能超100% 系统监视器里面看到的是 所有线程占用的 总和会超100%。 所以最好的情况是&…

MATLAB基础:字符串、元胞数组

今天我们继续学习MATLAB中的字符串、元胞和结构 字符串 由于MATLAB是面向矩阵的&#xff0c;所以字符串的处理可以用矩阵的形式实现 字符串的赋值与引用 假设变量a&#xff0c;将用单引号引起来的字符串赋值给它&#xff0c; a清心明目, b(a[4;-1;1]) 在这里&#xff0c;…

如何检查我的网站是否支持HTTPS

HTTPS是一种用于安全通信的协议&#xff0c;是HTTP的安全版本。HTTPS的主要作用在于为互联网上的数据传输提供安全性和隐私保护。通常是需要在网站安装部署SSL证书来实现网络数据加密传输&#xff0c;安全加密功能。 那么如果要检查你的网站是否支持HTTPS&#xff0c;可以看下…

云计算实训11——web服务器的搭建、nfs服务器的搭建、备份静态文件、基于linux和windows实现文件共享

一、搭建web服务器 1.关闭firewall和selinux 关闭防火墙 systemctl stop firewalld systemctl disable firewalld 停用selinux setenforce 0 配置文件中让sellinux不再启动 vim /etc/selinux/config SELINUXpermissive 2.编辑dns配置文件 vim /etc/resolv.conf nameserver 114.…