• 【深度学习实验】卷积神经网络(八):使用深度残差神经网络ResNet完成图片多分类任务


    目录

    一、实验介绍

    二、实验环境

    1. 配置虚拟环境

    2. 库版本介绍

    三、实验内容

    0. 导入必要的工具包

    1. 构建数据集(CIFAR10Dataset)

     a. read_csv_labels()

    b. CIFAR10Dataset

     2. 构建模型(FeedForward)

    3.整合训练、评估、预测过程(Runner)

    4. __main__

    5. 代码整合


    一、实验介绍

            本实验实现了实现深度残差神经网络ResNet,并基于此完成图像分类任务。

            残差网络(ResNet)是一种深度神经网络架构,用于解决深层网络训练过程中的梯度消失和梯度爆炸问题。通过引入残差连接(residual connection)来构建网络层与层之间的跳跃连接,使得网络可以更好地优化深层结构。

            残差网络的一个重要应用是在图像识别任务中,特别是在深度卷积神经网络(CNN)中。通过使用残差模块,可以构建非常深的网络,例如ResNet,其在ILSVRC 2015图像分类挑战赛中取得了非常出色的成绩。

            在ResNet中,每个残差块由一个或多个卷积层组成,其中包含了跳跃连接。跳跃连接将输入直接添加到残差块的输出中,从而使得网络可以学习残差函数,即残差块只需学习将输入的变化部分映射到输出,而不需要学习完整的映射关系。这种设计有助于减轻梯度消失问题,使得网络可以更深地进行训练。

    二、实验环境

            本系列实验使用了PyTorch深度学习框架,相关操作如下:

    1. 配置虚拟环境

    conda create -n DL python=3.7 
    conda activate DL
    pip install torch==1.8.1+cu102 torchvision==0.9.1+cu102 torchaudio==0.8.1 -f https://download.pytorch.org/whl/torch_stable.html
    
    conda install matplotlib
     conda install scikit-learn

    2. 库版本介绍

    软件包本实验版本目前最新版
    matplotlib3.5.33.8.0
    numpy1.21.61.26.0
    python3.7.16
    scikit-learn0.22.11.3.0
    torch1.8.1+cu1022.0.1
    torchaudio0.8.12.0.2
    torchvision0.9.1+cu1020.15.2

    三、实验内容

    0. 导入必要的工具包

    1. import torch
    2. from torch import nn
    3. import torch.nn.functional as F
    4. from torch.utils.data import Dataset, DataLoader
    5. from torchvision.io import read_image

    1. 构建数据集(CIFAR10Dataset)

            CIFAR10数据集共有60000个样本,每个样本都是一张32*32像素的RGB图像(彩色图像),每个RGB图像又必定分为3个通道(R通道、G通道、B通道)。CIFAR10中有10类物体,标签值分别按照0~9来区分,他们分别是飞机( airplane )、汽车( automobile )、鸟( bird )、猫( cat )、鹿( deer )、狗( dog )、青蛙( frog )、马( horse )、船( ship )和卡车( truck )。为减小运行时间,本实验选取其中1000张作为训练集。

    数据集链接:

    CIFAR-10 and CIFAR-100 datasets (toronto.edu)icon-default.png?t=N7T8http://www.cs.toronto.edu/~kriz/cifar.html

     a. read_csv_labels()

            从CSV文件中读取标签信息并返回一个标签字典。

    1. def read_csv_labels(fname):
    2. """读取fname来给标签字典返回一个文件名"""
    3. with open(fname, 'r') as f:
    4. # 跳过文件头行(列名)
    5. lines = f.readlines()[1:]
    6. tokens = [l.rstrip().split(',') for l in lines]
    7. return dict(((name, label) for name, label in tokens))
    •  使用open函数打开指定文件名的CSV文件,并将文件对象赋值给变量f。这里使用'r'参数以只读模式打开文件。

    • 使用文件对象的readlines()方法读取文件的所有行,并将结果存储在名为lines的列表中。通过切片操作[1:],跳过了文件的第一行(列名),将剩余的行存储在lines列表中。

    • 列表推导式(list comprehension):对lines列表中的每一行进行处理。对于每一行,使用rstrip()方法去除行末尾的换行符,并使用split(',')方法将行按逗号分割为多个标记。最终,将所有行的标记组成的子列表存储在tokens列表中。

    • 使用字典推导式(dictionary comprehension)将tokens列表中的子列表转换为字典。对于tokens中的每个子列表,将子列表的第一个元素作为键(name),第二个元素作为值(label),最终返回一个包含这些键值对的字典。

    b. CIFAR10Dataset

    1. class CIFAR10Dataset(Dataset):
    2. def __init__(self, folder_path, fname):
    3. self.labels = read_csv_labels(os.path.join(folder_path, fname))
    4. self.folder_path = os.path.join(folder_path, 'train')
    5. def __len__(self):
    6. return len(self.labels)
    7. def __getitem__(self, idx):
    8. img = read_image(self.folder_path + '/' + str(idx + 1) + '.png')
    9. label = self.labels[str(idx + 1)]
    10. return img, torch.tensor(int(label))
    • 构造函数:

      • 接受两个参数

        • folder_path表示数据集所在的文件夹路径

        • fname表示包含标签信息的文件名。

      • 调用read_csv_labels函数,传递folder_pathfname作为参数,以读取CSV文件中的标签信息,并将返回的标签字典存储在self.labels变量中。

      • 通过拼接folder_path和字符串'train'来构建数据集的文件夹路径,将结果存储在self.folder_path变量中。

    • def __len__(self)

      • 这是CIFAR10Dataset类的方法,用于返回数据集的长度,即样本的数量。

    • def __getitem__(self, idx): 这是CIFAR10Dataset类的方法,用于根据给定的索引idx获取数据集中的一个样本。它首先根据索引idx构建图像文件的路径,并调用read_image函数来读取图像数据,将结果存储在img变量中。然后,它通过将索引转换为字符串,并使用该字符串作为键来从self.labels字典中获取相应的标签,将结果存储在label变量中。最后,它返回一个元组,包含图像数据和经过torch.tensor转换的标签。

     2. 构建模型(FeedForward)

            参考前文:

    【深度学习实验】卷积神经网络(七):实现深度残差神经网络ResNet-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/m0_63834988/article/details/133705834

    3.整合训练、评估、预测过程(Runner)

            参考前文:

    【深度学习实验】前馈神经网络(九):整合训练、评估、预测过程(Runner)_QomolangmaH的博客-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/m0_63834988/article/details/133219448?spm=1001.2014.3001.5501

    4. __main__

    1. if __name__ == '__main__':
    2. batch_size = 20
    3. # 构建训练集
    4. train_data = CIFAR10Dataset('cifar10_tiny', 'trainLabels.csv')
    5. train_iter = DataLoader(train_data, batch_size=batch_size)
    6. # 构建测试集
    7. test_data = CIFAR10Dataset('cifar10_tiny', 'trainLabels.csv')
    8. test_iter = DataLoader(test_data, batch_size=batch_size)
    9. # 模型训练
    10. num_classes = 10
    11. # 定义模型
    12. model = ResNet(num_classes)
    13. # 定义损失函数
    14. loss_fn = F.cross_entropy
    15. # 定义优化器
    16. optimizer = torch.optim.SGD(model.parameters(), lr=0.1)
    17. runner = Runner(model, optimizer, loss_fn, metric=None)
    18. runner.train(train_iter, num_epochs=10, save_path='chapter_5')
    19. # 模型预测
    20. runner.load_model('chapter_5.pth')
    21. x, label = next(iter(test_iter))
    22. predict = torch.argmax(runner.predict(x.float()), dim=1)
    23. print('predict:', predict)
    24. print(' label:', label)

    5. 代码整合

    1. # 导入必要的工具包
    2. import torch
    3. from torch import nn
    4. import torch.nn.functional as F
    5. from torch.utils.data import Dataset, DataLoader
    6. from torchvision.io import read_image
    7. # 残差连接, 输入和输出的维度有时是相同的, 有时是不同的, 所以需要 use_1x1conv来判断是否需要
    8. class Residual(nn.Module):
    9. def __init__(self, input_channels, num_channels, use_1x1conv=False, strides=1):
    10. super().__init__()
    11. self.conv1 = nn.Conv2d(input_channels, num_channels, kernel_size=3, padding=1, stride=strides)
    12. self.conv2 = nn.Conv2d(num_channels, num_channels, kernel_size=3, padding=1)
    13. if use_1x1conv:
    14. self.conv3 = nn.Conv2d(input_channels, num_channels, kernel_size=1, stride=strides)
    15. else:
    16. self.conv3 = None
    17. # 批量归一化层,将会在第7章讲到
    18. self.bn1 = nn.BatchNorm2d(num_channels)
    19. self.bn2 = nn.BatchNorm2d(num_channels)
    20. def forward(self, X):
    21. Y = F.relu(self.bn1(self.conv1(X)))
    22. Y = self.bn2(self.conv2(Y))
    23. if self.conv3:
    24. X = self.conv3(X)
    25. Y += X
    26. return F.relu(Y)
    27. # 残差网络是由几个不同的残差块组成的
    28. def resnet_block(input_channels, num_channels, num_residuals, first_block=False):
    29. blk = []
    30. for i in range(num_residuals):
    31. if i == 0 and not first_block:
    32. blk.append(Residual(input_channels, num_channels,
    33. use_1x1conv=True, strides=2))
    34. else:
    35. blk.append(Residual(num_channels, num_channels))
    36. return blk
    37. class ResNet(nn.Module):
    38. def __init__(self, num_classes):
    39. super().__init__()
    40. self.b1 = nn.Sequential(nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3),
    41. nn.BatchNorm2d(64), nn.ReLU(),
    42. nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
    43. self.b2 = nn.Sequential(*resnet_block(64, 64, 2, first_block=True))
    44. self.b3 = nn.Sequential(*resnet_block(64, 128, 2))
    45. self.b4 = nn.Sequential(*resnet_block(128, 256, 2))
    46. self.b5 = nn.Sequential(*resnet_block(256, 512, 2))
    47. self.head = nn.Sequential(nn.AdaptiveAvgPool2d((1, 1)), nn.Flatten(), nn.Linear(512, num_classes))
    48. def forward(self, x):
    49. net = nn.Sequential(self.b1, self.b2, self.b3, self.b4, self.b5, self.head)
    50. return net(x)
    51. import os
    52. def read_csv_labels(fname):
    53. """读取fname来给标签字典返回一个文件名"""
    54. with open(fname, 'r') as f:
    55. # 跳过文件头行(列名)
    56. lines = f.readlines()[1:]
    57. tokens = [l.rstrip().split(',') for l in lines]
    58. return dict(((name, label) for name, label in tokens))
    59. class CIFAR10Dataset(Dataset):
    60. def __init__(self, folder_path, fname):
    61. self.labels = read_csv_labels(os.path.join(folder_path, fname))
    62. self.folder_path = os.path.join(folder_path, 'train')
    63. def __len__(self):
    64. return len(self.labels)
    65. def __getitem__(self, idx):
    66. img = read_image(self.folder_path + '/' + str(idx + 1) + '.png')
    67. label = self.labels[str(idx + 1)]
    68. return img, torch.tensor(int(label))
    69. class Runner(object):
    70. def __init__(self, model, optimizer, loss_fn, metric=None):
    71. self.model = model
    72. self.optimizer = optimizer
    73. self.loss_fn = loss_fn
    74. # 用于计算评价指标
    75. self.metric = metric
    76. # 记录训练过程中的评价指标变化
    77. self.dev_scores = []
    78. # 记录训练过程中的损失变化
    79. self.train_epoch_losses = []
    80. self.dev_losses = []
    81. # 记录全局最优评价指标
    82. self.best_score = 0
    83. # 模型训练阶段
    84. def train(self, train_loader, dev_loader=None, **kwargs):
    85. # 将模型设置为训练模式,此时模型的参数会被更新
    86. self.model.train()
    87. num_epochs = kwargs.get('num_epochs', 0)
    88. log_steps = kwargs.get('log_steps', 100)
    89. save_path = kwargs.get('save_path', 'best_model.pth')
    90. eval_steps = kwargs.get('eval_steps', 0)
    91. # 运行的step数,不等于epoch数
    92. global_step = 0
    93. if eval_steps:
    94. if dev_loader is None:
    95. raise RuntimeError('Error: dev_loader can not be None!')
    96. if self.metric is None:
    97. raise RuntimeError('Error: Metric can not be None')
    98. # 遍历训练的轮数
    99. for epoch in range(num_epochs):
    100. total_loss = 0
    101. # 遍历数据集
    102. for step, data in enumerate(train_loader):
    103. x, y = data
    104. logits = self.model(x.float())
    105. loss = self.loss_fn(logits, y.long())
    106. total_loss += loss
    107. if step % log_steps == 0:
    108. print(f'loss:{loss.item():.5f}')
    109. loss.backward()
    110. self.optimizer.step()
    111. self.optimizer.zero_grad()
    112. # 每隔一定轮次进行一次验证,由eval_steps参数控制,可以采用不同的验证判断条件
    113. if eval_steps != 0:
    114. if (epoch + 1) % eval_steps == 0:
    115. dev_score, dev_loss = self.evaluate(dev_loader, global_step=global_step)
    116. print(f'[Evalute] dev score:{dev_score:.5f}, dev loss:{dev_loss:.5f}')
    117. if dev_score > self.best_score:
    118. self.save_model(f'model_{epoch + 1}.pth')
    119. print(
    120. f'[Evaluate]best accuracy performance has been updated: {self.best_score:.5f}-->{dev_score:.5f}')
    121. self.best_score = dev_score
    122. # 验证过程结束后,请记住将模型调回训练模式
    123. self.model.train()
    124. global_step += 1
    125. # 保存当前轮次训练损失的累计值
    126. train_loss = (total_loss / len(train_loader)).item()
    127. self.train_epoch_losses.append((global_step, train_loss))
    128. self.save_model(f'{save_path}.pth')
    129. print('[Train] Train done')
    130. # 模型评价阶段
    131. def evaluate(self, dev_loader, **kwargs):
    132. assert self.metric is not None
    133. # 将模型设置为验证模式,此模式下,模型的参数不会更新
    134. self.model.eval()
    135. global_step = kwargs.get('global_step', -1)
    136. total_loss = 0
    137. self.metric.reset()
    138. for batch_id, data in enumerate(dev_loader):
    139. x, y = data
    140. logits = self.model(x.float())
    141. loss = self.loss_fn(logits, y.long()).item()
    142. total_loss += loss
    143. self.metric.update(logits, y)
    144. dev_loss = (total_loss / len(dev_loader))
    145. self.dev_losses.append((global_step, dev_loss))
    146. dev_score = self.metric.accumulate()
    147. self.dev_scores.append(dev_score)
    148. return dev_score, dev_loss
    149. # 模型预测阶段,
    150. def predict(self, x, **kwargs):
    151. self.model.eval()
    152. logits = self.model(x)
    153. return logits
    154. # 保存模型的参数
    155. def save_model(self, save_path):
    156. torch.save(self.model.state_dict(), save_path)
    157. # 读取模型的参数
    158. def load_model(self, model_path):
    159. self.model.load_state_dict(torch.load(model_path, map_location=torch.device('cpu')))
    160. if __name__ == '__main__':
    161. batch_size = 20
    162. # 构建训练集
    163. train_data = CIFAR10Dataset('cifar10_tiny', 'trainLabels.csv')
    164. train_iter = DataLoader(train_data, batch_size=batch_size)
    165. # 构建测试集
    166. test_data = CIFAR10Dataset('cifar10_tiny', 'trainLabels.csv')
    167. test_iter = DataLoader(test_data, batch_size=batch_size)
    168. # 模型训练
    169. num_classes = 10
    170. # 定义模型
    171. model = ResNet(num_classes)
    172. # 定义损失函数
    173. loss_fn = F.cross_entropy
    174. # 定义优化器
    175. optimizer = torch.optim.SGD(model.parameters(), lr=0.1)
    176. runner = Runner(model, optimizer, loss_fn, metric=None)
    177. runner.train(train_iter, num_epochs=15, save_path='chapter_5')
    178. # 模型预测
    179. runner.load_model('chapter_5.pth')
    180. x, label = next(iter(test_iter))
    181. predict = torch.argmax(runner.predict(x.float()), dim=1)
    182. print('predict:', predict)
    183. print(' label:', label)

  • 相关阅读:
    HTML标签详解 HTML5+CSS3+移动web 前端开发入门笔记(四)
    dialogx,给大家推荐一个开源安卓弹窗组件。
    从阿里云迁移Redis到AWS的规划和前期准备
    如果想搭建在线客服,应该如何建、
    Linux便捷操作
    Java基于基于Springboot+vue的药品销售商城网站 在线购药 elementui毕业设计
    Java开发-应届生面试常常涉及到的问题
    Qt编程-QTableView冻结行或冻结列或冻结局部单元格
    网络框架重构之路plain2.0(c++23 without module) 综述
    【35kJava开发岗:基础篇】
  • 原文地址:https://blog.csdn.net/m0_63834988/article/details/133716752