0


基于ResNet50实现垃圾分类

一、垃圾分类背景

在现代社会中,垃圾分类已经成为环保的重要措施之一。然而,手动分类垃圾既费时又容易出错。借助深度学习技术,特别是卷积神经网络(CNN),我们可以开发一种自动垃圾分类系统。本文将介绍如何基于ResNet50实现垃圾分类。

二、ResNet50简介

ResNet50是Residual Networks(残差网络)的一种变体,由Kaiming He等人在2015年提出。ResNet50包含50个深度层,通过引入残差模块,有效地解决了深层网络的退化问题。残差模块通过引入短连接(skip connections)使得网络在训练时更容易优化。下图在下文中称为主图

1、ResNet50整体架构图

首先需要声明,这张图的内容是ResNet的Backbone部分(即图中没有ResNet中的全局平均池化层和全连接层),输入

  1. INPUT

经过ResNet50的5个阶段(Stage 0Stage 1、……)得到输出

  1. OUTPUT

下面附上ResNet原文展示的ResNet结构,大家可以结合着看,看不懂也没关系,只看本文也可以无痛理解的。

上图描述了ResNet多个版本的具体结构,本文描述的“ResNet50”中的50指有50个层。和上图一样,本图描述的ResNet也分为5个阶段。

2、ResNet50各个部分的具体架构

1)Stage结构

  • (3,224,224)指输入INPUT的通道数(channel)、高(height)和宽(width),即(C,H,W)。现假设输入的高度和宽度相等,所以用(C,W,W)表示。
  • 该stage中第1层包括3个先后操作1. CONV``````CONV是卷积(Convolution)的缩写,7×7指卷积核大小,64指卷积核的数量(即该卷积层输出的通道数),/2指卷积核的步长为2。2. BN``````BN是Batch Normalization的缩写,即常说的BN层。3. RELU``````RELU指ReLU激活函数。
  • 该stage中第2层为MAXPOOL,即最大池化层,其kernel大小为3×3、步长为2
  • (64,56,56)是该stage输出的通道数(channel)、高(height)和宽(width),其中64等于该stage第1层卷积层中卷积核的数量,56等于224/2/2(步长为2会使输入尺寸减半)。

总体来讲,在Stage 0中,形状为

  1. (3,224,224)

的输入先后经过卷积层、BN层、ReLU激活函数、MaxPooling层得到了形状为

  1. (64,56,56)

的输出。

2)BINK1、BINK2的结构

BINK2(主图右侧部分):

BINK2有两个参数:C,W

C:代表输入通道数。W:代表输入尺寸。

BINK2左侧经过三个卷积快(包括BN,RELU),设其输出为F(x),将F和x相加再经过Relu激活函数得到BINK2的输出。至于为什么将F和x相加后再输出后面将会介绍。

BINK1:

BINK1有四个参数:C,W,C1,S。

S:代表卷积层中的步长,当S为1时,输入尺寸和输出尺寸相同,代表没有进行下采样。

C1:代表卷积层输出的特征图数目,即输出通道数。

C:代表输入通道数。C和C1相等说明左侧1×1的卷积层没有减少通道数,后三个stage中C=2*C1说明左侧1×1的卷积层减少了通道数。

W:代表输入尺寸,即长和宽。

BINK1相对于BINK2是输入通道和输出通道不一致的情况,BINK1右侧先经过一个卷积层,改变其输出通道数,设其输出为G(x),G函数起到了和左侧输出通道数匹配的作用,这样将F和G相加再经过Relu激活函数得到BINK1的输出。

3)简要分析

原文可知,ResNet后4个stage中都有

  1. BTNK1

  1. BTNK2

  • 4个stage中BTNK2参数规律相同4个stage中BTNK2的参数全都是1个模式和规律,只是输入的形状(C,W,W)不同。
  • Stage 1BTNK1参数的规律与后3个stage不同然而,4个stage中BTNK1的参数的模式并非全都一样。具体来讲,后3个stage中BTNK1的参数模式一致,Stage 1BTNK1的模式与后3个stage的不一样,这表现在以下2个方面:1. 参数SBTNK1左右两个1×1卷积层是否下采样Stage 1中的BTNK1:步长S为1,没有进行下采样,输入尺寸和输出尺寸相等。后3个stage的BTNK1:步长S为2,进行了下采样,输入尺寸是输出尺寸的2倍。2. 参数CC1BTNK1左侧第一个1×1卷积层是否减少通道数Stage 1中的BTNK1:输入通道数C和左侧1×1卷积层通道数C1相等(C=C1=64),即左侧1×1卷积层没有减少通道数。后3个stage的BTNK1:输入通道数C和左侧1×1卷积层通道数C1不相等(C=2*C1),左侧1×1卷积层有减少通道数。
  • 为什么Stage 1BTNK1参数的规律与后3个stage不同?(个人观点)- 关于BTNK1左右两个1×1卷积层是否下采样因为Stage 0中刚刚对网络输入进行了卷积和最大池化,还没有进行残差学习,此时直接下采样会损失大量信息;而后3个stage直接进行下采样时,前面的网络已经进行过残差学习了,所以可以直接进行下采样。- 关于BTNK1左侧第一个1×1卷积层是否减少通道数根据ResNet原文可知,Bottleneck左侧两个1×1卷积层的主要作用分别是减少通道数和恢复通道数,这样就可以使它们中间的3×3卷积层的输入和输出的通道数都较小,因此效率更高。Stage 1BTNK1的输入通道数C为64,它本来就比较小,因此没有必要通过左侧第一个1×1卷积层减少通道数。

4)残差结构

传统的卷积神经网络(CNN)在训练过程中,当网络深度增加时,梯度消失和退化问题变得更加明显。为了解决这些问题,残差结构引入了短连接,通过直接将输入跳跃连接到输出,形成了所谓的“残差”连接。具体来说,残差结构通过以下公式来表达:

y=F(x,{Wi})+x\mathbf{y} = \mathcal{F}(\mathbf{x}, {W_i}) + \mathbf{x}y=F(x,{Wi​})+x

其中:

  • x\mathbf{x}x 是输入。
  • F(x,{Wi})\mathcal{F}(\mathbf{x}, {W_i})F(x,{Wi​}) 表示学习到的残差函数,即输入经过若干卷积层后的输出。
  • y\mathbf{y}y 是残差结构的最终输出。
  • {Wi}{W_i}{Wi​} 表示卷积层的权重。

通过这种形式,残差结构能够确保输入信息在每一层都能得到保留和传递,从而缓解了深层网络中的梯度消失问题。

三、ResNet50实现垃圾分类

1、数据集准备

1)首先,我们需要一组垃圾分类的数据集。

常用的数据集有:

  • Garbage Classification Dataset
  • TrashNet

我们以垃圾分类数据集为例,该数据集包含多个类别的垃圾图像,如纸张、塑料、金属等。

本文采用自定义的158类别数据集。

2)数据集的处理

对于文章的数据,我们定义一个split_data.py文件用于生成训练集,验证集和测试集。

  1. import os
  2. from shutil import copy, rmtree
  3. import random
  4. def mk_file(file_path: str):
  5. if os.path.exists(file_path):
  6. # 如果文件夹存在,则先删除原文件夹在重新创建
  7. rmtree(file_path)
  8. os.makedirs(file_path)
  9. def main():
  10. # 保证随机可复现
  11. random.seed(0)
  12. # 将数据集中10%的数据划分到验证集中
  13. split_rate = 0.1
  14. # 将数据集中20%的数据划分到测试集中
  15. split_rate_2 = 0.2
  16. #因此训练集、验证集、测试集比例为 7:1:2
  17. # 指向你解压后的garbage_photos文件夹
  18. cwd = os.getcwd()
  19. data_root_pro = os.path.join(cwd, "data_set")
  20. data_root = os.path.join(data_root_pro, "garbage_data") # data_root = /data_set/garbage
  21. assert os.path.exists(data_root), "path '{}' does not exist.".format(data_root)
  22. # flower_class = [cla for cla in os.listdir(data_root)
  23. # if os.path.isdir(os.path.join(data_root, cla))]
  24. garbage_class = [cla for cla in os.listdir(data_root)
  25. if os.path.isdir(os.path.join(data_root, cla))]
  26. # 建立保存训练集的文件夹
  27. train_root = os.path.join(data_root, "train_garbage")
  28. mk_file(train_root)
  29. for cla in garbage_class:
  30. # 建立每个类别对应的文件夹
  31. mk_file(os.path.join(train_root, cla))
  32. # 建立保存验证集的文件夹
  33. val_root = os.path.join(data_root, "val_garbage")
  34. mk_file(val_root)
  35. for cla in garbage_class:
  36. # 建立每个类别对应的文件夹
  37. mk_file(os.path.join(val_root, cla))
  38. # 建立保存测试集的文件夹
  39. test_root = os.path.join(data_root, "test_garbage") #对于测试集无需进行分类保存图片
  40. mk_file(test_root)
  41. for cla in garbage_class:
  42. cla_path = os.path.join(data_root, cla) # /data_set/garbage/0/
  43. images = os.listdir(cla_path)
  44. num = len(images)
  45. # 随机采样验证集和测试集的索引
  46. eval_index = random.sample(images, k=int(num*split_rate))
  47. test_index = random.sample(images, k=int(num*split_rate_2))
  48. for index, image in enumerate(images):
  49. if image in eval_index:
  50. # 将分配至验证集中的文件复制到相应目录
  51. image_path = os.path.join(cla_path, image)
  52. new_path = os.path.join(val_root, cla)
  53. copy(image_path, new_path)
  54. elif image in test_index:
  55. # 将分配至测试集中的文件复制到相应目录
  56. image_path = os.path.join(cla_path, image)
  57. #new_path = os.path.join(test_root, cla)
  58. copy(image_path, test_root)
  59. else:
  60. # 将分配至训练集中的文件复制到相应目录
  61. image_path = os.path.join(cla_path, image)
  62. new_path = os.path.join(train_root, cla)
  63. copy(image_path, new_path)
  64. print("\r[{}] processing [{}/{}]".format(cla, index+1, num), end="") # processing bar
  65. print()
  66. print("processing done!")
  67. if __name__ == '__main__':
  68. main()

运行split_data.py文件之后,目录将生成下图所示三个文件夹,其中train_garbage和val_garbage包含158个子文件夹,每个文件夹存在对应的图像。

2、 模型的构建

  1. import torch.nn as nn
  2. import torch
  3. class BasicBlock(nn.Module):
  4. expansion = 1
  5. # 适用于ResNet18和ResNet34的基本残差块
  6. def __init__(self, in_channel, out_channel, stride=1, downsample=None, **kwargs):
  7. super(BasicBlock, self).__init__()
  8. # 第一个3x3卷积层
  9. self.conv1 = nn.Conv2d(in_channels=in_channel, out_channels=out_channel,
  10. kernel_size=3, stride=stride, padding=1, bias=False)
  11. self.bn1 = nn.BatchNorm2d(out_channel) # 批量归一化层
  12. self.relu = nn.ReLU() # 激活函数
  13. # 第二个3x3卷积层
  14. self.conv2 = nn.Conv2d(in_channels=out_channel, out_channels=out_channel,
  15. kernel_size=3, stride=1, padding=1, bias=False)
  16. self.bn2 = nn.BatchNorm2d(out_channel) # 批量归一化层
  17. self.downsample = downsample # 下采样层(用于调整输入尺寸和通道数)
  18. def forward(self, x):
  19. identity = x # 保存输入值
  20. if self.downsample is not None: # 如果需要下采样
  21. identity = self.downsample(x)
  22. out = self.conv1(x) # 第一个卷积层
  23. out = self.bn1(out) # 批量归一化
  24. out = self.relu(out) # 激活
  25. out = self.conv2(out) # 第二个卷积层
  26. out = self.bn2(out) # 批量归一化
  27. out += identity # 残差连接
  28. out = self.relu(out) # 激活
  29. return out
  30. class Bottleneck(nn.Module):
  31. expansion = 4
  32. # 适用于ResNet50、ResNet101和ResNet152的瓶颈残差块
  33. def __init__(self, in_channel, out_channel, stride=1, downsample=None,
  34. groups=1, width_per_group=64):
  35. super(Bottleneck, self).__init__()
  36. width = int(out_channel * (width_per_group / 64.)) * groups # 计算组卷积的宽度
  37. # 第一个1x1卷积层(压缩通道数)
  38. self.conv1 = nn.Conv2d(in_channels=in_channel, out_channels=width,
  39. kernel_size=1, stride=1, bias=False)
  40. self.bn1 = nn.BatchNorm2d(width) # 批量归一化层
  41. # 第二个3x3卷积层
  42. self.conv2 = nn.Conv2d(in_channels=width, out_channels=width, groups=groups,
  43. kernel_size=3, stride=stride, bias=False, padding=1)
  44. self.bn2 = nn.BatchNorm2d(width) # 批量归一化层
  45. # 第三个1x1卷积层(扩展通道数)
  46. self.conv3 = nn.Conv2d(in_channels=width, out_channels=out_channel*self.expansion,
  47. kernel_size=1, stride=1, bias=False)
  48. self.bn3 = nn.BatchNorm2d(out_channel*self.expansion) # 批量归一化层
  49. self.relu = nn.ReLU(inplace=True) # 激活函数
  50. self.downsample = downsample # 下采样层
  51. def forward(self, x):
  52. identity = x # 保存输入值
  53. if self.downsample is not None: # 如果需要下采样
  54. identity = self.downsample(x)
  55. out = self.conv1(x) # 第一个卷积层
  56. out = self.bn1(out) # 批量归一化
  57. out = self.relu(out) # 激活
  58. out = self.conv2(out) # 第二个卷积层
  59. out = self.bn2(out) # 批量归一化
  60. out = self.relu(out) # 激活
  61. out = self.conv3(out) # 第三个卷积层
  62. out = self.bn3(out) # 批量归一化
  63. out += identity # 残差连接
  64. out = self.relu(out) # 激活
  65. return out
  66. class ResNet(nn.Module):
  67. def __init__(self,
  68. block,
  69. blocks_num, # 每个阶段的残差块数量
  70. num_classes=1000,
  71. include_top=True,
  72. groups=1,
  73. width_per_group=64):
  74. super(ResNet, self).__init__()
  75. self.include_top = include_top # 是否包含全连接层
  76. self.in_channel = 64 # 初始通道数
  77. self.groups = groups # 组卷积数量
  78. self.width_per_group = width_per_group # 每组的宽度
  79. # 初始卷积层
  80. self.conv1 = nn.Conv2d(3, self.in_channel, kernel_size=7, stride=2,
  81. padding=3, bias=False)
  82. self.bn1 = nn.BatchNorm2d(self.in_channel) # 批量归一化层
  83. self.relu = nn.ReLU(inplace=True) # 激活函数
  84. # 最大池化层
  85. self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
  86. # 各个阶段的残差块
  87. self.layer1 = self._make_layer(block, 64, blocks_num[0])
  88. self.layer2 = self._make_layer(block, 128, blocks_num[1], stride=2)
  89. self.layer3 = self._make_layer(block, 256, blocks_num[2], stride=2)
  90. self.layer4 = self._make_layer(block, 512, blocks_num[3], stride=2)
  91. # 全局平均池化层和全连接层
  92. if self.include_top:
  93. self.avgpool = nn.AdaptiveAvgPool2d((1, 1)) # 自适应平均池化层
  94. self.fc = nn.Linear(512 * block.expansion, num_classes) # 全连接层
  95. # 初始化卷积层权重
  96. for m in self.modules():
  97. if isinstance(m, nn.Conv2d):
  98. nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
  99. def _make_layer(self, block, channel, block_num, stride=1):
  100. downsample = None # 对于ResNet18和ResNet34,下采样默认为None,其他层为下面函数
  101. if stride != 1 or self.in_channel != channel * block.expansion: # 如果步长不为1或通道数不匹配
  102. downsample = nn.Sequential(
  103. nn.Conv2d(self.in_channel, channel * block.expansion, kernel_size=1, stride=stride, bias=False),
  104. nn.BatchNorm2d(channel * block.expansion)) # 下采样层
  105. layers = []
  106. layers.append(block(self.in_channel,
  107. channel, # 主分支上第一个卷积核(通道)的个数
  108. downsample=downsample, # 下采样层
  109. stride=stride,
  110. groups=self.groups,
  111. width_per_group=self.width_per_group))
  112. self.in_channel = channel * block.expansion # 更新输入通道数
  113. for _ in range(1, block_num): # 添加剩余的残差块
  114. layers.append(block(self.in_channel,
  115. channel,
  116. groups=self.groups,
  117. width_per_group=self.width_per_group))
  118. return nn.Sequential(*layers) # 将layers列表转化为nn.Sequential
  119. def forward(self, x):
  120. x = self.conv1(x) # 初始卷积层
  121. x = self.bn1(x) # 批量归一化
  122. x = self.relu(x) # 激活
  123. x = self.maxpool(x) # 最大池化层
  124. x = self.layer1(x) # 第一阶段
  125. x = self.layer2(x) # 第二阶段
  126. x = self.layer3(x) # 第三阶段
  127. x = self.layer4(x) # 第四阶段
  128. if self.include_top: # 如果包含全连接层
  129. x = self.avgpool(x) # 全局平均池化层
  130. x = torch.flatten(x, 1) # 展平
  131. x = self.fc(x) # 全连接层
  132. return x
  133. def resnet50(model_name="resnet50", num_classes=1000, init_weights=False, **kwargs):
  134. # 创建ResNet-50模型
  135. model = ResNet(Bottleneck, [3, 4, 6, 3], num_classes=num_classes, include_top=True)
  136. # 替换最后的全连接层以适应新的类别数
  137. model.fc = nn.Linear(model.fc.in_features, num_classes)
  138. # 初始化权重(如果需要)
  139. if init_weights:
  140. def init_weights(m):
  141. if isinstance(m, nn.Conv2d) or isinstance(m, nn.Linear):
  142. nn.init.kaiming_normal_(m.weight)
  143. if m.bias is not None:
  144. nn.init.constant_(m.bias,

3、训练模型

  1. # -*- coding: utf-8 -*-
  2. import os
  3. import sys
  4. import json
  5. import torch
  6. import torch.nn as nn
  7. import torch.optim as optim
  8. from torchvision import transforms, datasets
  9. from tqdm import tqdm
  10. from model import resnet50 # 导入定义的ResNet50模型
  11. def main():
  12. device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") # 设置运行设备
  13. print("using {} device.".format(device))
  14. # 数据预处理
  15. data_transform = {
  16. "train_garbage": transforms.Compose([transforms.RandomResizedCrop(224), # 随机裁剪并调整大小
  17. transforms.RandomHorizontalFlip(), # 随机水平翻转
  18. transforms.ToTensor(), # 转换为张量
  19. transforms.Normalize([0.485, 0.456, 0.406],
  20. [0.229, 0.224, 0.225])]), # 归一化
  21. "val_garbage": transforms.Compose([transforms.Resize(256), # 调整大小
  22. transforms.CenterCrop(224), # 中心裁剪
  23. transforms.ToTensor(), # 转换为张量
  24. transforms.Normalize([0.485, 0.456, 0.406],
  25. [0.229, 0.224, 0.225])])} # 归一化
  26. data_root = os.path.abspath(os.path.join(os.getcwd(), "./")) # 获取数据根路径
  27. print(data_root)
  28. image_path = os.path.join(data_root, "data_set", "garbage_data") # 垃圾数据集路径
  29. assert os.path.exists(image_path), "{} path does not exist.".format(image_path)
  30. train_dataset = datasets.ImageFolder(root=os.path.join(image_path, "train_garbage"),
  31. transform=data_transform["train_garbage"]) # 加载训练数据集
  32. train_num = len(train_dataset)
  33. # 获取类别映射字典并写入json文件
  34. garbage_list = train_dataset.class_to_idx
  35. cla_dict = dict((val, key) for key, val in garbage_list.items())
  36. json_str = json.dumps(cla_dict, indent=4)
  37. with open('/home/dell/CV408/hb/data_set/garbage_data/garbage_classification.json', 'w') as json_file:
  38. json_file.write(json_str)
  39. batch_size = 32
  40. nw = min([os.cpu_count(), batch_size if batch_size > 1 else 0, 8]) # 设置dataloader的worker数量
  41. print('Using {} dataloader workers every process'.format(nw))
  42. train_loader = torch.utils.data.DataLoader(train_dataset,
  43. batch_size=batch_size, shuffle=True,
  44. num_workers=nw) # 加载训练数据集
  45. validate_dataset = datasets.ImageFolder(root=os.path.join(image_path, "val_garbage"),
  46. transform=data_transform["val_garbage"]) # 加载验证数据集
  47. val_num = len(validate_dataset)
  48. validate_loader = torch.utils.data.DataLoader(validate_dataset,
  49. batch_size=batch_size, shuffle=False,
  50. num_workers=nw) # 加载验证数据集
  51. print("using {} images for training, {} images for validation.".format(train_num,
  52. val_num))
  53. # 加载ResNet50模型
  54. model_name = "resnet50"
  55. net = resnet50(model_name=model_name, num_classes=158, init_weights=True)
  56. net.to(device) # 将模型移至指定设备
  57. # 定义损失函数
  58. loss_function = nn.CrossEntropyLoss()
  59. # 定义优化器
  60. params = [p for p in net.parameters() if p.requires_grad]
  61. optimizer = optim.Adam(params, lr=0.0001)
  62. epochs = 30
  63. best_acc = 0.0
  64. save_path = './Test5_resnet{}.pth'.format(model_name) # 保存当前最好的权重路径
  65. train_steps = len(train_loader)
  66. for epoch in range(epochs):
  67. # 训练过程
  68. net.train()
  69. running_loss = 0.0
  70. train_bar = tqdm(train_loader, file=sys.stdout)
  71. for step, data in enumerate(train_bar):
  72. images, labels = data
  73. optimizer.zero_grad()
  74. logits = net(images.to(device))
  75. loss = loss_function(logits, labels.to(device))
  76. loss.backward()
  77. optimizer.step()
  78. # 打印损失
  79. running_loss += loss.item()
  80. train_bar.desc = "train epoch[{}/{}] loss:{:.3f}".format(epoch + 1,
  81. epochs,
  82. loss)
  83. # 验证过程
  84. net.eval()
  85. acc = 0.0 # 累积准确数
  86. with torch.no_grad():
  87. val_bar = tqdm(validate_loader, file=sys.stdout)
  88. for val_data in val_bar:
  89. val_images, val_labels = val_data
  90. outputs = net(val_images.to(device))
  91. predict_y = torch.max(outputs, dim=1)[1]
  92. acc += torch.eq(predict_y, val_labels.to(device)).sum().item()
  93. val_bar.desc = "valid epoch[{}/{}]".format(epoch + 1,
  94. epochs)
  95. val_accurate = acc / val_num
  96. print('[epoch %d] train_loss: %.3f val_accuracy: %.3f' %
  97. (epoch + 1, running_loss / train_steps, val_accurate))
  98. if val_accurate > best_acc: # 更新最好权重
  99. best_acc = val_accurate
  100. torch.save(net.state_dict(), save_path)
  101. print('Finished Training')
  102. if __name__ == '__main__':
  103. main()

对模型进行预训练之后,将得到一个最好的权重文件 。

4、测试模型

1)单图片测试

  1. import os
  2. import json
  3. import torch
  4. from PIL import Image
  5. from torchvision import transforms
  6. import matplotlib.pyplot as plt
  7. from model import resnet50 # 导入定义的ResNet50模型
  8. def main():
  9. device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") # 设置运行设备
  10. # 定义数据预处理步骤
  11. data_transform = transforms.Compose(
  12. [transforms.Resize(256), # 调整大小到256x256
  13. transforms.CenterCrop(224), # 中心裁剪到224x224
  14. transforms.ToTensor(), # 转换为张量
  15. transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])]) # 归一化
  16. # 加载图像
  17. img_path = "/home/dell/CV408/hb/data_set/garbage_data/test_garbage/8/1171.jpg" # 图像路径
  18. assert os.path.exists(img_path), "file: '{}' dose not exist.".format(img_path)
  19. img = Image.open(img_path) # 打开图像
  20. plt.imshow(img) # 显示图像
  21. img = data_transform(img) # 对图像进行预处理
  22. img = torch.unsqueeze(img, dim=0) # 扩展批次维度
  23. # 读取类别字典
  24. json_path = '/home/dell/CV408/hb/data_set/garbage_data/garbage_classification.json'
  25. assert os.path.exists(json_path), "file: '{}' dose not exist.".format(json_path)
  26. with open(json_path, "r") as f:
  27. class_indict = json.load(f)
  28. # 创建模型
  29. model = resnet50(num_classes=158).to(device) # 初始化ResNet50模型并设置类别数为158
  30. # 加载模型权重
  31. weights_path = "/home/dell/CV408/hb/resnet50.pth" # 模型权重路径
  32. assert os.path.exists(weights_path), "file: '{}' dose not exist.".format(weights_path)
  33. model.load_state_dict(torch.load(weights_path, map_location=device)) # 加载权重
  34. # 创建保存预测结果的目录
  35. save_dir = "/home/dell/CV408/hb/data_set/garbage_data/"
  36. os.makedirs(save_dir, exist_ok=True)
  37. # 预测过程
  38. model.eval() # 设置模型为评估模式
  39. with torch.no_grad(): # 禁用梯度计算
  40. output = torch.squeeze(model(img.to(device))).cpu() # 模型预测并去除批次维度
  41. predict = torch.softmax(output, dim=0) # 应用softmax获取概率
  42. predict_cla = torch.argmax(predict).numpy() # 获取预测类别
  43. # 打印预测结果
  44. print_res = "class: {} prob: {:.3}".format(class_indict[str(predict_cla)],
  45. predict[predict_cla].numpy())
  46. plt.title(print_res) # 在图像上显示预测结果
  47. for i in range(len(predict)): # 打印所有类别的概率
  48. print("class: {:10} prob: {:.3}".format(class_indict[str(i)],
  49. predict[i].numpy()))
  50. plt.savefig(os.path.join(save_dir, "predicted_image_garbage.png")) # 保存带有预测结果的图像
  51. plt.show() # 显示图像
  52. if __name__ == '__main__':
  53. main() # 运行main函数

测试结果如下图(类别144为:火龙果)

2)批量测试

  1. import os
  2. import json
  3. import matplotlib.pyplot as plt
  4. import torch
  5. from PIL import Image
  6. from torchvision import transforms
  7. from model import resnet50
  8. def main():
  9. # 设置设备为GPU(如果可用)或CPU
  10. device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
  11. # 定义数据预处理操作,包括调整大小、中心裁剪、转换为张量和归一化
  12. data_transform = transforms.Compose([
  13. transforms.Resize(256),
  14. transforms.CenterCrop(224),
  15. transforms.ToTensor(),
  16. transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
  17. ])
  18. # 指定需要遍历预测的图像文件夹
  19. imgs_root = "/home/dell/CV408/hb/data_set/garbage_data/test_garbage"
  20. assert os.path.exists(imgs_root), f"file: '{imgs_root}' does not exist."
  21. # 读取指定文件夹下所有jpg图像路径
  22. img_path_list = [os.path.join(imgs_root, i) for i in os.listdir(imgs_root) if i.endswith(".jpg")]
  23. # 读取类别映射文件
  24. json_path = '/home/dell/CV408/hb/data_set/garbage_data/garbage_classification.json'
  25. assert os.path.exists(json_path), f"file: '{json_path}' does not exist."
  26. with open(json_path, "r") as json_file:
  27. class_indict = json.load(json_file)
  28. # 创建ResNet50模型并加载预训练权重
  29. model = resnet50(num_classes=158).to(device)
  30. weights_path = "/home/dell/CV408/hb/Test5_resnetresnet50.pth"
  31. assert os.path.exists(weights_path), f"file: '{weights_path}' does not exist."
  32. model.load_state_dict(torch.load(weights_path, map_location=device))
  33. # 设置模型为评估模式
  34. model.eval()
  35. batch_size = 8 # 每次预测时处理的图像数量
  36. save_dir = "/home/dell/CV408/hb/data_set/garbage_data/predictions/"
  37. os.makedirs(save_dir, exist_ok=True) # 创建保存预测结果的目录
  38. with torch.no_grad():
  39. # 按批次处理图像
  40. for ids in range(0, len(img_path_list) // batch_size):
  41. img_list = []
  42. img_paths = img_path_list[ids * batch_size: (ids + 1) * batch_size]
  43. for img_path in img_paths:
  44. assert os.path.exists(img_path), f"file: '{img_path}' does not exist."
  45. img = Image.open(img_path)
  46. img = data_transform(img)
  47. img_list.append(img)
  48. # 将图像列表打包成一个批次
  49. batch_img = torch.stack(img_list, dim=0)
  50. # 预测类别
  51. output = model(batch_img.to(device)).cpu()
  52. predict = torch.softmax(output, dim=1)
  53. probs, classes = torch.max(predict, dim=1)
  54. # 可视化并保存每张图像的预测结果
  55. for idx, (pro, cla) in enumerate(zip(probs, classes)):
  56. img_path = img_paths[idx]
  57. img = Image.open(img_path)
  58. plt.imshow(img)
  59. plt.title(f"Class: {class_indict[str(cla.numpy())]} Prob: {pro.numpy():.3f}")
  60. save_path = os.path.join(save_dir, f"pred_{os.path.basename(img_path)}")
  61. plt.savefig(save_path)
  62. plt.close()
  63. print(f"image: {img_path} class: {class_indict[str(cla.numpy())]} prob: {pro.numpy():.3f}")
  64. if __name__ == '__main__':
  65. main()

生成一个文件夹保存着测试集图像对应的预测结果。

五、总结

模型的精确率达到了69.7%左右,并且对一些样本少的类别和图像质量较差的预测结果不理想,可能需要对图像的预处理方法进行改进,比如图像增强等等


本文转载自: https://blog.csdn.net/qq_52191127/article/details/140814422
版权归原作者 只爱喝水 所有, 如有侵权,请联系我们删除。

“基于ResNet50实现垃圾分类”的评论:

还没有评论