当前位置:   article > 正文

【PyTorch】单目标检测项目

【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

探索性数据分析

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

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

  1. import numpy as np
  2. from PIL import Image, ImageDraw
  3. import matplotlib.pylab as plt
  4. # 设置随机数生成器的种子,相同的种子将生成相同的随机数序列。有利于调试和重现结果。
  5. np.random.seed(2019)
  6. # 设置图形大小
  7. plt.rcParams['figure.figsize'] = (14, 14)
  8. # 调整子图之间的间距
  9. plt.subplots_adjust(wspace=0, hspace=0)
  10. # 设置子图的行数和列数
  11. nrows,ncols=4,4
  12. # 从labels_df中获取imgName列
  13. imgName=labels_df["imgName"]
  14. # 获取labels_df的索引
  15. ids=labels_df.index
  16. # 从ids中随机选择nrows*ncols个元素
  17. rndIds=np.random.choice(ids,nrows*ncols)
  18. print(rndIds)
  19. def load_img_label(labels_df,id_):
  20. # 获取图片名称
  21. imgName=labels_df["imgName"]
  22. # 判断图片名称是否以"A"开头
  23. if imgName[id_][0]=="A":
  24. # 如果是,则前缀为"AMD"
  25. prefix="AMD"
  26. else:
  27. # 否则,前缀为"Non-AMD"
  28. prefix="Non-AMD"
  29. # 拼接图片路径
  30. fullPath2img=os.path.join(path2data,"Training400",prefix,imgName[id_])
  31. # 打开图片
  32. img = Image.open(fullPath2img)
  33. # 获取图片中心点坐标
  34. x=labels_df["Fovea_X"][id_]
  35. y=labels_df["Fovea_Y"][id_]
  36. # 返回图片和中心点坐标
  37. label=(x,y)
  38. return img,label
  39. def show_img_label(img,label,w_h=(50,50),thickness=2):
  40. # 定义函数show_img_label,用于在图像上绘制矩形框,并显示图像
  41. w,h=w_h
  42. # 获取矩形框的宽度和高度
  43. cx,cy=label
  44. draw = ImageDraw.Draw(img)
  45. # 创建一个绘图对象
  46. draw.rectangle(((cx-w/2, cy-h/2), (cx+w/2, cy+h/2)),outline="green",width=thickness)
  47. # 在图像上绘制矩形框,颜色为绿色,线宽为thickness
  48. plt.imshow(np.asarray(img))
  49. # 显示图像
  50. # 遍历rndIds中的每个元素
  51. for i,id_ in enumerate(rndIds):
  52. # 加载图片和标签
  53. img,label=load_img_label(labels_df,id_)
  54. # 打印图片和标签的大小
  55. print(img.size,label)
  56. # 在子图中显示图片和标签
  57. plt.subplot(nrows, ncols, i+1)
  58. show_img_label(img,label,w_h=(150,150),thickness=20)
  59. # 设置子图的标题为imgName中对应id_的值
  60. plt.title(imgName[id_])

 

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

用于对象检测的数据转换

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

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

  1. import torchvision.transforms.functional as TF
  2. # 定义一个函数,用于调整图像和标签的大小
  3. def resize_img_label(image,label=(0.,0.),target_size=(256,256)):
  4. # 获取原始图像的宽度和高度
  5. w_orig,h_orig = image.size
  6. # 获取目标图像的宽度和高度
  7. w_target,h_target = target_size
  8. # 获取标签的坐标
  9. cx, cy= label
  10. # 调整图像大小
  11. image_new = TF.resize(image,target_size)
  12. # 调整标签大小
  13. label_new= cx/w_orig*w_target, cy/h_orig*h_target
  14. # 返回调整后的图像和标签
  15. return image_new,label_new
  16. # 加载图片和标签
  17. img, label=load_img_label(labels_df,1)
  18. # 打印图片和标签的大小
  19. print(img.size,label)
  20. # 调整图片和标签的大小
  21. img_r,label_r=resize_img_label(img,label)
  22. # 打印调整后图片和标签的大小
  23. print(img_r.size,label_r)
  24. # 在子图1中显示原始图片和标签
  25. plt.subplot(1,2,1)
  26. show_img_label(img,label,w_h=(150,150),thickness=20)
  27. # 在子图2中显示调整后图片和标签
  28. plt.subplot(1,2,2)
  29. show_img_label(img_r,label_r)

  1. # 定义一个函数,用于随机水平翻转图像和标签
  2. def random_hflip(image,label):
  3. # 获取图像的宽度和高度
  4. w,h=image.size
  5. # 获取标签的坐标
  6. x,y=label
  7. # 对图像进行水平翻转
  8. image = TF.hflip(image)
  9. # 更新标签的坐标
  10. label = w-x, y
  11. # 返回翻转后的图像和标签
  12. return image,label
  13. # 加载图片和标签
  14. img, label=load_img_label(labels_df,1)
  15. # 调整图片和标签的大小
  16. img_r,label_r=resize_img_label(img,label)
  17. # 随机水平翻转图片和标签
  18. img_fh,label_fh=random_hflip(img_r,label_r)
  19. # 显示原始图片和标签
  20. plt.subplot(1,2,1)
  21. show_img_label(img_r,label_r)
  22. # 显示翻转后的图片和标签
  23. plt.subplot(1,2,2)
  24. show_img_label(img_fh,label_fh)

  1. # 定义一个函数,用于随机垂直翻转图像和标签
  2. def random_vflip(image,label):
  3. # 获取图像的宽度和高度
  4. w,h=image.size
  5. # 获取标签的坐标
  6. x,y=label
  7. # 对图像进行垂直翻转
  8. image = TF.vflip(image)
  9. # 翻转标签的坐标
  10. label = x, h-y
  11. # 返回翻转后的图像和标签
  12. return image, label
  13. # 加载图片和标签
  14. img, label=load_img_label(labels_df,7)
  15. # 调整图片和标签的大小
  16. img_r,label_r=resize_img_label(img,label)
  17. # 随机翻转图片和标签
  18. img_fv,label_fv=random_vflip(img_r,label_r)
  19. # 显示原始图片和标签
  20. plt.subplot(1,2,1)
  21. show_img_label(img_r,label_r)
  22. # 显示翻转后的图片和标签
  23. plt.subplot(1,2,2)
  24. show_img_label(img_fv,label_fv)

  1. import numpy as np
  2. np.random.seed(1)
  3. # 定义一个函数,用于对图像和标签进行随机平移
  4. def random_shift(image,label,max_translate=(0.2,0.2)):
  5. # 获取图像的宽度和高度
  6. w,h=image.size
  7. # 获取最大平移量
  8. max_t_w, max_t_h=max_translate
  9. # 获取标签的坐标
  10. cx, cy=label
  11. # 生成一个随机数,范围在-11之间
  12. trans_coef=np.random.rand()*2-1
  13. # 计算平移量
  14. w_t = int(trans_coef*max_t_w*w)
  15. h_t = int(trans_coef*max_t_h*h)
  16. # 对图像进行平移
  17. image=TF.affine(image,translate=(w_t, h_t),shear=0,angle=0,scale=1)
  18. # 更新标签的坐标
  19. label = cx+w_t, cy+h_t
  20. # 返回平移后的图像和标签
  21. return image,label
  22. # 加载图片和标签
  23. img, label=load_img_label(labels_df,1)
  24. # 调整图片和标签的大小
  25. img_r,label_r=resize_img_label(img,label)
  26. # 随机平移图片和标签
  27. img_t,label_t=random_shift(img_r,label_r,max_translate=(.5,.5))
  28. # 显示原始图片和标签
  29. plt.subplot(1,2,1)
  30. show_img_label(img_r,label_r)
  31. # 显示平移后的图片和标签
  32. plt.subplot(1,2,2)
  33. show_img_label(img_t,label_t)

  1. # 定义一个transformer函数,用于对图像和标签进行变换
  2. def transformer(image, label, params):
  3. # 调用resize_img_label函数,对图像和标签进行resize操作
  4. image,label=resize_img_label(image,label,params["target_size"])
  5. # 随机生成一个0-1之间的随机数,如果小于p_hflip,则对图像和标签进行水平翻转
  6. if random.random() < params["p_hflip"]:
  7. image,label=random_hflip(image,label)
  8. # 随机生成一个0-1之间的随机数,如果小于p_vflip,则对图像和标签进行垂直翻转
  9. if random.random() < params["p_vflip"]:
  10. image,label=random_vflip(image,label)
  11. # 随机生成一个0-1之间的随机数,如果小于p_shift,则对图像和标签进行平移
  12. if random.random() < params["p_shift"]:
  13. image,label=random_shift(image,label, params["max_translate"])
  14. # 将图像转换为tensor类型
  15. image=TF.to_tensor(image)
  16. # 返回变换后的图像和标签
  17. return image, label
  18. import random
  19. # 设置随机数种子
  20. np.random.seed(0)
  21. random.seed(0)
  22. # 从数据集中加载一张图片和对应的标签
  23. img, label=load_img_label(labels_df,1)
  24. # 定义图像变换参数
  25. params={
  26. "target_size" : (256, 256), # 目标图像大小
  27. "p_hflip" : 1.0, # 水平翻转概率
  28. "p_vflip" : 1.0, # 垂直翻转概率
  29. "p_shift" : 1.0, # 平移概率
  30. "max_translate": (0.2, 0.2), # 最大平移距离
  31. }
  32. # 对图像进行变换
  33. img_t,label_t=transformer(img,label,params)
  34. # 创建一个12列的子图,并选择第一个子图
  35. plt.subplot(1,2,1)
  36. # 显示图片和标签,图片大小为150x150,线条粗细为20
  37. show_img_label(img,label,w_h=(150,150),thickness=20)
  38. # 创建一个12列的子图,并选择第二个子图
  39. plt.subplot(1,2,2)
  40. # 显示图片和标签,图片为img_t,标签为label_t
  41. show_img_label(TF.to_pil_image(img_t),label_t)

调整图片亮度

  1. # 加载图片和标签
  2. img, label=load_img_label(labels_df,1)
  3. # 调整图片和标签的大小
  4. img_r,label_r=resize_img_label(img,label)
  5. # 调整图片的亮度
  6. img_t=TF.adjust_brightness(img_r,brightness_factor=0.5)
  7. label_t=label_r
  8. # 显示原始图片和标签
  9. plt.subplot(1,2,1)
  10. show_img_label(img_r,label_r)
  11. # 显示调整亮度后的图片和标签
  12. plt.subplot(1,2,2)
  13. show_img_label(img_t,label_t)

 

  1. # 定义一个函数,用于将两个列表中的元素相除
  2. def scale_label(a,b):
  3. # 使用zip函数将两个列表中的元素一一对应
  4. div = [ai/bi for ai,bi in zip(a,b)]
  5. # 返回相除后的结果
  6. return div
  7. def transformer(image, label, params):
  8. # 调整图像和标签的大小
  9. image,label=resize_img_label(image,label,params["target_size"])
  10. # 随机水平翻转图像和标签
  11. if random.random() < params["p_hflip"]:
  12. image,label=random_hflip(image,label)
  13. # 随机垂直翻转图像和标签
  14. if random.random() < params["p_vflip"]:
  15. image,label=random_vflip(image,label)
  16. # 随机平移图像和标签
  17. if random.random() < params["p_shift"]:
  18. image,label=random_shift(image,label, params["max_translate"])
  19. # 随机调整图像的亮度
  20. if random.random() < params["p_brightness"]:
  21. brightness_factor=1+(np.random.rand()*2-1)*params["brightness_factor"]
  22. image=TF.adjust_brightness(image,brightness_factor)
  23. # 随机调整图像的对比度
  24. if random.random() < params["p_contrast"]:
  25. contrast_factor=1+(np.random.rand()*2-1)*params["contrast_factor"]
  26. image=TF.adjust_contrast(image,contrast_factor)
  27. # 随机调整图像的gamma值
  28. if random.random() < params["p_gamma"]:
  29. gamma=1+(np.random.rand()*2-1)*params["gamma"]
  30. image=TF.adjust_gamma(image,gamma)
  31. # 如果需要,调整标签的大小
  32. if params["scale_label"]:
  33. label=scale_label(label,params["target_size"])
  34. # 将图像转换为张量
  35. image=TF.to_tensor(image)
  36. return image, label
  37. # 设置随机种子,以保证结果的可重复性
  38. np.random.seed(0)
  39. random.seed(0)
  40. # 从labels_df中加载第1张图片和对应的标签
  41. img, label=load_img_label(labels_df,1)
  42. #设定参数
  43. params={
  44. "target_size" : (256, 256),
  45. "p_hflip" : 1.0,
  46. "p_vflip" : 1.0,
  47. "p_shift" : 1.0,
  48. "max_translate": (0.5, 0.5),
  49. "p_brightness": 1.0,
  50. "brightness_factor": 0.8,
  51. "p_contrast": 1.0,
  52. "contrast_factor": 0.8,
  53. "p_gamma": 1.0,
  54. "gamma": 0.4,
  55. "scale_label": False,
  56. }
  57. img_t,label_t=transformer(img,label,params)
  58. # 创建一个12列的子图,并选择第一个子图
  59. plt.subplot(1,2,1)
  60. # 显示图片和标签,图片大小为150x150,线条粗细为20
  61. show_img_label(img,label,w_h=(150,150),thickness=20)
  62. # 创建一个12列的子图,并选择第二个子图
  63. plt.subplot(1,2,2)
  64. # 显示图片和标签,图片为img_t,标签为label_t
  65. 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 类来创建数据加载器。使用数据加载器自动获取小批量数据并进行处理。

  1. from torch.utils.data import Dataset
  2. from PIL import Image
  3. class AMD_dataset(Dataset):
  4. def __init__(self, path2data, transform, trans_params):
  5. # 初始化函数,传入数据路径、转换函数和转换参数
  6. pass
  7. def __len__(self):
  8. # 返回数据集的大小
  9. return len(self.labels)
  10. def __getitem__(self, idx):
  11. # 根据索引获取数据集中的一个样本
  12. pass
  13. def __init__(self, path2data, transform, trans_params):
  14. # 获取标签文件的路径
  15. path2labels=os.path.join(path2data,"Training400","Fovea_location.xlsx")
  16. # 读取标签文件,并将ID列作为索引
  17. labels_df=pd.read_excel(path2labels,engine='openpyxl',index_col="ID")
  18. # 获取标签数据,包括Fovea_X和Fovea_Y两列
  19. self.labels = labels_df[["Fovea_X","Fovea_Y"]].values
  20. # 获取图片名称
  21. self.imgName=labels_df["imgName"]
  22. # 获取图片ID
  23. self.ids=labels_df.index
  24. # 获取图片的完整路径
  25. self.fullPath2img=[0]*len(self.ids)
  26. for id_ in self.ids:
  27. # 根据图片名称的前缀,确定图片所在的文件夹
  28. if self.imgName[id_][0]=="A":
  29. prefix="AMD"
  30. else:
  31. prefix="Non-AMD"
  32. # 获取图片的完整路径
  33. self.fullPath2img[id_-1]=os.path.join(path2data,"Training400",prefix,self.imgName[id_])
  34. # 获取数据转换函数和数据转换参数
  35. self.transform = transform
  36. self.trans_params=trans_params
  37. def __getitem__(self, idx):
  38. # 打开指定索引的图像
  39. image = Image.open(self.fullPath2img[idx])
  40. # 获取指定索引的标签
  41. label= self.labels[idx]
  42. # 对图像和标签进行变换
  43. image,label = self.transform(image,label,self.trans_params)
  44. # 返回变换后的图像和标签
  45. return image, label
  46. # 重写函数
  47. AMD_dataset.__init__=__init__
  48. AMD_dataset.__getitem__=__getitem__
  49. # 定义训练过程中的参数
  50. trans_params_train={
  51. "target_size" : (256, 256), # 目标图像大小
  52. "p_hflip" : 0.5, # 水平翻转概率
  53. "p_vflip" : 0.5, # 垂直翻转概率
  54. "p_shift" : 0.5, # 平移概率
  55. "max_translate": (0.2, 0.2), # 最大平移量
  56. "p_brightness": 0.5, # 亮度调整概率
  57. "brightness_factor": 0.2, # 亮度调整因子
  58. "p_contrast": 0.5, # 对比度调整概率
  59. "contrast_factor": 0.2, # 对比度调整因子
  60. "p_gamma": 0.5, # 伽马调整概率
  61. "gamma": 0.2, # 伽马调整因子
  62. "scale_label": True, # 是否调整标签大小
  63. }
  64. # 定义验证过程中的参数
  65. trans_params_val={
  66. "target_size" : (256, 256),
  67. "p_hflip" : 0.0,
  68. "p_vflip" : 0.0,
  69. "p_shift" : 0.0,
  70. "p_brightness": 0.0,
  71. "p_contrast": 0.0,
  72. "p_gamma": 0.0,
  73. "gamma": 0.0,
  74. "scale_label": True,
  75. }
  76. # 创建AMD_dataset类的实例,用于训练集
  77. amd_ds1=AMD_dataset(path2data,transformer,trans_params_train)
  78. # 创建AMD_dataset类的实例,用于验证集
  79. amd_ds2=AMD_dataset(path2data,transformer,trans_params_val)
  80. from sklearn.model_selection import ShuffleSplit
  81. # 创建ShuffleSplit对象,设置分割次数为1,测试集大小为0.2,随机种子为0
  82. sss = ShuffleSplit(n_splits=1, test_size=0.2, random_state=0)
  83. # 创建索引列表
  84. indices=range(len(amd_ds1))
  85. # 遍历ShuffleSplit对象,获取训练集和验证集的索引
  86. for train_index, val_index in sss.split(indices):
  87. # 打印训练集的长度
  88. print(len(train_index))
  89. # 打印分隔符
  90. print("-"*10)
  91. # 打印验证集的长度
  92. print(len(val_index))

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

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

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

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

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

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

搭建残差网络模型

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

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

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

 

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

 

  1. import torch.nn as nn
  2. import torch.nn.functional as F
  3. # 定义一个Net类,继承自nn.Module类
  4. class Net(nn.Module):
  5. # 初始化函数,接收一个参数params
  6. def __init__(self, params):
  7. # 调用父类的初始化函数
  8. super(Net, self).__init__()
  9. # 定义前向传播函数,接收一个参数x
  10. def forward(self, x):
  11. return x
  12. # 定义一个初始化函数,接收参数params
  13. def __init__(self, params):
  14. # 初始化父类Net
  15. super(Net, self).__init__()
  16. # 获取输入形状
  17. C_in,H_in,W_in=params["input_shape"]
  18. # 获取初始滤波器数量
  19. init_f=params["initial_filters"]
  20. # 获取输出数量
  21. num_outputs=params["num_outputs"]
  22. # 定义卷积层
  23. self.conv1 = nn.Conv2d(C_in, init_f, kernel_size=3,stride=2,padding=1)
  24. self.conv2 = nn.Conv2d(init_f+C_in, 2*init_f, kernel_size=3,stride=1,padding=1)
  25. self.conv3 = nn.Conv2d(3*init_f+C_in, 4*init_f, kernel_size=3,padding=1)
  26. self.conv4 = nn.Conv2d(7*init_f+C_in, 8*init_f, kernel_size=3,padding=1)
  27. self.conv5 = nn.Conv2d(15*init_f+C_in, 16*init_f, kernel_size=3,padding=1)
  28. # 定义全连接层1
  29. self.fc1 = nn.Linear(16*init_f, num_outputs)
  30. # 定义前向传播函数
  31. def forward(self, x):
  32. # 对输入进行平均池化,池化核大小为4,步长为4
  33. identity=F.avg_pool2d(x,4,4)
  34. # 对输入进行卷积操作,并使用ReLU激活函数
  35. x = F.relu(self.conv1(x))
  36. # 对输入进行最大池化,池化核大小为2,步长为2
  37. x = F.max_pool2d(x, 2, 2)
  38. # 将卷积后的结果与池化后的结果进行拼接
  39. x = torch.cat((x, identity), dim=1)
  40. # 对输入进行平均池化,池化核大小为2,步长为2
  41. identity=F.avg_pool2d(x,2,2)
  42. # 对输入进行卷积操作,并使用ReLU激活函数
  43. x = F.relu(self.conv2(x))
  44. # 对输入进行最大池化,池化核大小为2,步长为2
  45. x = F.max_pool2d(x, 2, 2)
  46. # 将卷积后的结果与池化后的结果进行拼接
  47. x = torch.cat((x, identity), dim=1)
  48. # 对输入进行平均池化,池化核大小为2,步长为2
  49. identity=F.avg_pool2d(x,2,2)
  50. # 对输入进行卷积操作,并使用ReLU激活函数
  51. x = F.relu(self.conv3(x))
  52. # 对输入进行最大池化,池化核大小为2,步长为2
  53. x = F.max_pool2d(x, 2, 2)
  54. # 将卷积后的结果与池化后的结果进行拼接
  55. x = torch.cat((x, identity), dim=1)
  56. # 对输入进行平均池化,池化核大小为2,步长为2
  57. identity=F.avg_pool2d(x,2,2)
  58. # 对输入进行卷积操作,并使用ReLU激活函数
  59. x = F.relu(self.conv4(x))
  60. # 对输入进行最大池化,池化核大小为2,步长为2
  61. x = F.max_pool2d(x, 2, 2)
  62. # 将卷积后的结果与池化后的结果进行拼接
  63. x = torch.cat((x, identity), dim=1)
  64. # 对输入进行卷积操作,并使用ReLU激活函数
  65. x = F.relu(self.conv5(x))
  66. # 对输入进行自适应平均池化,池化核大小为1
  67. x=F.adaptive_avg_pool2d(x,1)
  68. # 将输入展平成一维向量
  69. x = x.reshape(x.size(0), -1)
  70. # 对输入进行全连接操作
  71. x = self.fc1(x)
  72. return x
  73. # 重写
  74. Net.__init__=__init__
  75. Net.forward=forward
  76. # 定义模型参数
  77. params_model={
  78. "input_shape": (3,256,256),
  79. "initial_filters": 16,
  80. "num_outputs": 2,
  81. }
  82. # 创建模型
  83. model = Net(params_model)
  84. # 检查CUDA是否可用
  85. if torch.cuda.is_available():
  86. # 如果可用,将设备设置为CUDA
  87. device = torch.device("cuda")
  88. # 将模型移动到CUDA设备上
  89. model=model.to(device)
  90. 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)。

  1. # 定义损失函数,使用SmoothL1Loss,reduction参数设置为"sum",表示将所有样本的损失相加
  2. loss_func = nn.SmoothL1Loss(reduction="sum")
  3. # 定义变量n和c
  4. n,c=8,2
  5. # 定义变量y,形状为(n, c),值为0.5,requires_grad=True表示需要计算梯度
  6. y = 0.5 * torch.ones(n, c, requires_grad=True)
  7. # 打印y的形状
  8. print(y.shape)
  9. # 定义变量target,形状为(n, c),值为0,requires_grad=False表示不需要计算梯度
  10. target = torch.zeros(n, c, requires_grad=False)
  11. # 打印target的形状
  12. print(target.shape)
  13. # 计算损失函数,loss_func为自定义的损失函数
  14. loss = loss_func(y, target)
  15. # 打印损失函数的值
  16. print(loss.item())
  17. # 重新定义变量y,形状为(n, c),值为2,requires_grad=True表示需要计算梯度
  18. y = 2 * torch.ones(n, c, requires_grad=True)
  19. # 重新定义变量target,形状为(n, c),值为0,requires_grad=False表示不需要计算梯度
  20. target = torch.zeros(n, c, requires_grad=False)
  21. # 重新计算损失函数
  22. loss = loss_func(y, target)
  23. # 打印损失函数的值
  24. print(loss.item())

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

 

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

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

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

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

模型训练与评估

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

  1. # 定义一个函数,用于计算模型在数据集上的损失和指标
  2. def loss_epoch(model,loss_func,dataset_dl,sanity_check=False,opt=None):
  3. # 初始化运行损失和运行指标
  4. running_loss=0.0
  5. running_metric=0.0
  6. # 获取数据集的长度
  7. len_data=len(dataset_dl.dataset)
  8. # 遍历数据集
  9. for xb, yb in dataset_dl:
  10. # 将标签堆叠成一维张量
  11. yb=torch.stack(yb,1)
  12. # 将标签转换为浮点型,并移动到设备上
  13. yb=yb.type(torch.float32).to(device)
  14. # 将输入数据移动到设备上,并获取模型输出
  15. output=model(xb.to(device))
  16. # 计算当前批次的损失和指标
  17. loss_b,metric_b=loss_batch(loss_func, output, yb, opt)
  18. # 累加损失
  19. running_loss+=loss_b
  20. # 如果指标不为空,则累加指标
  21. if metric_b is not None:
  22. running_metric+=metric_b
  23. # 计算平均损失
  24. loss=running_loss/float(len_data)
  25. # 计算平均指标
  26. metric=running_metric/float(len_data)
  27. # 返回平均损失和平均指标
  28. return loss, metric
  29. import copy
  30. # 定义一个函数,用于训练和验证模型
  31. def train_val(model, params):
  32. # 获取训练参数
  33. num_epochs=params["num_epochs"]
  34. loss_func=params["loss_func"]
  35. opt=params["optimizer"]
  36. train_dl=params["train_dl"]
  37. val_dl=params["val_dl"]
  38. sanity_check=params["sanity_check"]
  39. lr_scheduler=params["lr_scheduler"]
  40. path2weights=params["path2weights"]
  41. # 初始化损失历史和指标历史
  42. loss_history={
  43. "train": [],
  44. "val": [],
  45. }
  46. metric_history={
  47. "train": [],
  48. "val": [],
  49. }
  50. # 复制当前模型参数
  51. best_model_wts = copy.deepcopy(model.state_dict())
  52. # 初始化最佳损失
  53. best_loss=float('inf')
  54. # 开始训练
  55. for epoch in range(num_epochs):
  56. # 获取当前学习率
  57. current_lr=get_lr(opt)
  58. # 打印当前训练轮数和学习率
  59. print('Epoch {}/{}, current lr={}'.format(epoch, num_epochs - 1, current_lr))
  60. # 设置模型为训练模式
  61. model.train()
  62. # 计算训练损失和指标
  63. train_loss, train_metric=loss_epoch(model,loss_func,train_dl,sanity_check,opt)
  64. # 将训练损失和训练指标添加到历史记录中
  65. loss_history["train"].append(train_loss)
  66. metric_history["train"].append(train_metric)
  67. # 设置模型为评估模式
  68. model.eval()
  69. with torch.no_grad(): # 不计算梯度
  70. # 计算验证损失和指标
  71. val_loss, val_metric=loss_epoch(model,loss_func,val_dl,sanity_check)
  72. # 将验证损失和验证指标添加到历史记录中
  73. loss_history["val"].append(val_loss)
  74. metric_history["val"].append(val_metric)
  75. # 如果验证损失小于最佳损失,则更新最佳损失和最佳模型参数
  76. if val_loss < best_loss:
  77. best_loss = val_loss
  78. best_model_wts = copy.deepcopy(model.state_dict())
  79. torch.save(model.state_dict(), path2weights) # 保存最佳模型参数
  80. print("Copied best model weights!") # 打印保存成功信息
  81. lr_scheduler.step(val_loss) # 更新学习率
  82. # 如果学习率发生变化,则加载最佳模型参数
  83. if current_lr != get_lr(opt):
  84. print("Loading best model weights!")
  85. model.load_state_dict(best_model_wts)
  86. print("train loss: %.6f, accuracy: %.2f" %(train_loss,100*train_metric)) # 打印训练损失和指标
  87. print("val loss: %.6f, accuracy: %.2f" %(val_loss,100*val_metric)) # 打印验证损失和指标
  88. print("-"*10)
  89. model.load_state_dict(best_model_wts) # 加载最佳模型参数
  90. return model, loss_history, metric_history # 返回模型、损失历史和指标历史
  1. # 定义损失函数为SmoothL1Loss,reduction参数为sum
  2. loss_func=nn.SmoothL1Loss(reduction="sum")
  3. # 定义优化器为Adam,学习率为1e-4
  4. opt = optim.Adam(model.parameters(), lr=1e-4)
  5. # 定义学习率调度器,当验证集上的损失不再下降时,将学习率乘以0.5,等待20个epoch,并输出信息
  6. lr_scheduler = ReduceLROnPlateau(opt, mode='min',factor=0.5, patience=20,verbose=1)
  7. # 定义模型保存路径
  8. path2models= "./models/"
  9. # 如果路径不存在,则创建路径
  10. if not os.path.exists(path2models):
  11. os.mkdir(path2models)
  12. # 定义训练参数
  13. params_train={
  14. "num_epochs": 100, # 训练的epoch数
  15. "optimizer": opt, # 优化器
  16. "loss_func": loss_func, # 损失函数
  17. "train_dl": train_dl, # 训练数据集
  18. "val_dl": val_dl, # 验证数据集
  19. "sanity_check": False, # 是否进行sanity check
  20. "lr_scheduler": lr_scheduler, # 学习率调度器
  21. "path2weights": path2models+"weights_smoothl1.pt", # 模型保存路径
  22. }
  23. # 调用train_val函数进行训练和验证
  24. model,loss_hist,metric_hist=train_val(model,params_train)

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

 

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

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop】
推荐阅读
相关标签
  

闽ICP备14008679号