残差神经网络(ResNet)是由微软研究院的何恺明、张祥雨、任少卿、孙剑等人提出,resnet在2015 年的ILSVRC(ImageNet Large Scale Visual Recognition Challenge)中取得了冠军,其与传统的卷积神经网络相比主要有两个亮点:(1)提出residual结构(残差结构),并搭建超深的网络结构(突破1000层);(2)使用Batch Normalization加速训练(丢弃dropout)。
今天给大家分享一下如何利用resnet152实现水稻病害识别,代码方面我会先按照自己的理解进行拆分解释,而后再汇总放在后面。
1、获取并划分数据集
此分类任务我使用的是4类水稻叶病害数据集:Bacterialblight(白枯病),Blast(稻瘟病),Brownspot(褐斑病),Tungro(钨腐病),数据集百度网盘链接放在下面,里面有近5000张数据图片并划分好了类别,大家可自行下载:
链接:4种水稻叶病害数据集
2、代码架构
(1)数据准备与数据增强处理
# (1)、数据增强
data_transform = {
"train": transforms.Compose([transforms.RandomResizedCrop(224),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])]),
"test": transforms.Compose([transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])])}
# (2)、加载数据集
train_dataset = torchvision.datasets.ImageFolder(root=r"D:\Dataset\Rice Leaf Disease Images\train",
transform=data_transform["train"])
print(train_dataset.class_to_idx)
test_dataset = torchvision.datasets.ImageFolder(root=r"D:\Dataset\Rice Leaf Disease Images\test",
transform=data_transform["test"])
print(test_dataset.class_to_idx)
# (3)、导入数据集
train_data = DataLoader(dataset=train_dataset, batch_size=20,
shuffle=True, num_workers=0)
test_data = DataLoader(dataset=test_dataset, batch_size=10,
shuffle=True, num_workers=0)
# train_size = len(train_dataset) # 训练集的长度
# test_size = len(test_dataset) # 测试集的长度
# print(train_size) # 输出训练集长度看一下,相当于看看有几张图片
# print(test_size) # 输出测试集长度看一下,相当于看看有几张图片
这里我们首先进行数据增强处理(data_transform),数据增强的作用主要有两点:一是增加训练的数据量,提高模型的泛化能力;一是增加噪声数据,提升模型的鲁棒性。这里注意进行数据增强时,我们一般都是对训练集数据进行处理,而测试集与验证集数据几乎保持不变,这是为了保证测试集与验证集数据的真实性。此处的数据增强主要是对图片进行了裁剪,翻转,归一化处理。
接下来我们加载自己的数据集并将其放入到数据加载器中,加载数据集时大家填入自己的数据集保存路径就可,同时在数据加载器中设置好训练集和测试集的batch_size大小,batch_size通俗来说就是一次训练或测试时所抓取的数据样本数量。
(2)网络模型搭建
class BasicBlock(nn.Module):
expansion = 1 # 对应残差结构主分支结构当中,同一卷积层 每层卷积核的个数是否发生改变
# 初始化函数,各参数依次为:输入特征矩阵深度、输出特征矩阵深度(对应主分支上卷积核的个数)
# down_sample下采样参数(对应虚线的残差结构)
def __init__(self, in_channel, out_channel, stride=1, down_sample=None):
super(BasicBlock, self).__init__()
# stride默认=1(表示实线残差结构)
# output=(input-3+2*1)/1+1=input输出特征矩阵的高和宽未改变
# stride默认=2(表示虚线残差结构)
# output=(input-3+2*1)/2+1=input/2+0.5=input/2(向下取整)。输出特征矩阵的高和宽缩减为原来的一半
self.conv1 = nn.Conv2d(in_channels=in_channel, out_channels=out_channel,
kernel_size=3, stride=stride, padding=1, bias=False)
# 使用BN时,将bias=False
# 将BN层放在卷积层conv和激活层relu之间
self.bn1 = nn.BatchNorm2d(out_channel)
self.relu = nn.ReLU()
# 接下来开始第二层卷积层,stride都=1
self.conv2 = nn.Conv2d(in_channels=out_channel, out_channels=out_channel,
kernel_size=3, stride=1, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(out_channel)
self.down_sample = down_sample # 定义下采样方法=传入的下采样参数
def forward(self, x): # 正向传播过程,x为输入的特征矩阵
identity = x # 将x赋值给分支identity
if self.downsample is not None: # =none没有输入下采样函数,对应实线残差结构,跳过此部分
# is not None输入了下采样函数,对应虚线残差结构,将输入特征矩阵输入下采样中,得到捷径分支identity的输出
identity = self.downsample(x)
# 主分支
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
# 主分支的输出+捷径分支的输出,再使用激活函数
out += identity
out = self.relu(out)
# 返回残差结构的最终输出
return out
class Bottleneck(nn.Module): # 针对更深层次的残差结构
# 以50层conv2_x为例,卷积层1、2的卷积核个数=64,而第三层卷积核个数=64*4=256,故expansion = 4
expansion = 4
def __init__(self, in_channel, out_channel, stride=1, down_sample=None):
super(Bottleneck, self).__init__()
# 对于第一层卷积层,无论是实线残差结构还是虚线,stride都=1
self.conv1 = nn.Conv2d(in_channels=in_channel, out_channels=out_channel,
kernel_size=1, stride=1, bias=False) # squeeze channels
self.bn1 = nn.BatchNorm2d(out_channel)
# 对于第二层卷积层,实线残差结构和虚线的stride是不同的,stride采用传入的方式
self.conv2 = nn.Conv2d(in_channels=out_channel, out_channels=out_channel,
kernel_size=3, stride=stride, bias=False, padding=1)
self.bn2 = nn.BatchNorm2d(out_channel)
self.conv3 = nn.Conv2d(in_channels=out_channel, out_channels=out_channel * self.expansion,
kernel_size=1, stride=1, bias=False)
self.bn3 = nn.BatchNorm2d(out_channel * self.expansion)
self.relu = nn.ReLU(inplace=True)
self.down_sample = down_sample
# 正向传播过程
def forward(self, x):
identity = x
# self.down_sample=none对应实线残差结构,否则为虚线残差结构
if self.down_sample is not None:
identity = self.down_sample(x)
out = self.conv1(x) # 卷积层
out = self.bn1(out) # BN层
out = self.relu(out) # 激活层
out = self.conv2(out)
out = self.bn2(out)
out = self.relu(out)
out = self.conv3(out)
out = self.bn3(out)
out += identity
out = self.relu(out)
return out
class ResNet(nn.Module):
# 若选择浅层网络结构block=BasicBlock,否则=Bottleneck
# blocks_num所使用的残差结构的数目(是一个列表),若选择34层网络结构,blocks_num=[3,4,6,3]
# num_classes训练集的分类个数
# include_top参数便于在ResNet网络基础上搭建更复杂的网络
def __init__(self, block, blocks_num, num_classes=1000, include_top=True):
super(ResNet, self).__init__()
self.include_top = include_top
self.in_channel = 64 # 输入特征矩阵的深度(经过最大下采样层之后的)
# 第一个卷积层,对应表格中7*7的卷积层,输入特征矩阵的深度RGB图像,故第一个参数=3
self.conv1 = nn.Conv2d(3, self.in_channel, kernel_size=7, stride=2,
padding=3, bias=False)
self.bn1 = nn.BatchNorm2d(self.in_channel)
self.relu = nn.ReLU(inplace=True)
self.max_pool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1) # 对应3*3那个maxpooling
# conv2_x对应的残差结构,是通过_make_layer函数生成的
self.layer1 = self._make_layer(block, 64, blocks_num[0])
# conv3_x
self.layer2 = self._make_layer(block, 128, blocks_num[1], stride=2)
# conv4_x
self.layer3 = self._make_layer(block, 256, blocks_num[2], stride=2)
# conv5_x
self.layer4 = self._make_layer(block, 512, blocks_num[3], stride=2)
if self.include_top:
# 平均池化下采样层,AdaptiveAvgPool2d自适应的平均池化下采样操作,所得到特征矩阵的高和宽都是(1,1)
self.avg_pool = nn.AdaptiveAvgPool2d((1, 1)) # output size = (1, 1)
# 全连接层(输出节点层)
self.fc = nn.Linear(512 * block.expansion, num_classes)
# 卷积层初始化操作
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
# block为BasicBlock或Bottleneck
# channel残差结构中所对应的第一层的卷积核的个数(值为64/128/256/512)
# block_num对应残差结构中每一个conv*_x卷积层的个数(该层一共包含了多少个残差结构)例:34层的conv2_x:block_num取值为3
def _make_layer(self, block, channel, block_num, stride=1):
down_sample = None # 下采样赋值为none
# 对于18层、34层conv2_x不满足此if语句(不执行)
# 而50层、101层、152层网络结构的conv2_x的第一层也是虚线残差结构,需要调整特征矩阵的深度而高度和宽度不需要改变
# 但对于conv3_x、conv4_x、conv5_x不论ResNet为多少层,特征矩阵的高度、宽度、深度都需要调整(高和宽缩减为原来的一半)
if stride != 1 or self.in_channel != channel * block.expansion:
# 生成下采样函数
down_sample = nn.Sequential(
nn.Conv2d(self.in_channel, channel * block.expansion, kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(channel * block.expansion))
layers = [] # 定义一个空列表
# 参数依次为输入特征矩阵的深度,残差结构所对应主分支上第一个卷积层的卷积核个数
# 18层34层的conv2_x的layer1没有经过下采样函数那个if语句down_sample=none
# conv2_x对应的残差结构,通过此函数_make_layer生成的时,没有传入stride参数,stride默认=1
layers.append(block(self.in_channel, channel, down_sample=down_sample, stride=stride))
self.in_channel = channel * block.expansion
# conv3_x、conv4_x、conv5_x的第一层都是虚线残差结构,
# 而从第二层开始都是实线残差结构了,直接压入统一处理
for _ in range(1, block_num): # 由于第一层已经搭建好,从1开始
# self.in_channel:输入特征矩阵的深度,channel:残差结构主分支第一层卷积的卷积核个数
layers.append(block(self.in_channel, channel))
# 通过非关键字参数的形式传入到nn.Sequential,nn.Sequential将所定义的一系列层结构组合在一起并返回
return nn.Sequential(*layers)
# 正向传播
def forward(self, x):
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
x = self.max_pool(x)
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
if self.include_top:
x = self.avg_pool(x) # 平均池化下采样
x = torch.flatten(x, 1) # 展平处理
x = self.fc(x) # 全连接
return x
def resnet18(num_classes=1000, include_top=True):
return ResNet(BasicBlock, [2, 2, 2, 2], num_classes=num_classes, include_top=include_top)
def resnet34(num_classes=1000, include_top=True):
return ResNet(BasicBlock, [3, 4, 6, 3], num_classes=num_classes, include_top=include_top)
def resnet50(num_classes=1000, include_top=True):
return ResNet(Bottleneck, [3, 4, 6, 3], num_classes=num_classes, include_top=include_top)
def resnet101(num_classes=1000, include_top=True):
return ResNet(Bottleneck, [3, 4, 23, 3], num_classes=num_classes, include_top=include_top)
def resnet152(num_classes=1000, include_top=True):
return ResNet(Bottleneck, [3, 8, 36, 3], num_classes=num_classes, include_top=include_top)
这一部分代码量太大,大家根据注释来理解吧(emmmm...因为博主我也不一定能完全说的明白,见谅)。
(3)定义相关变量,函数与容器
net = resnet152()
model_weight_path = "./resnet152-b121ed2d.pth"
assert os.path.exists(model_weight_path), "file {} does not exist.".format(model_weight_path)
missing_keys, unexpected_keys = net.load_state_dict(torch.load(model_weight_path), strict=False)
# 获取GPU设备
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
net.to(device)
# print(model.to(device)) # 输出模型结构
# 定义训练次数,学习率,损失函数与优化器
epoch = 10 # 设置迭代训练次数
learning_rate = 0.001 # 定义学习率
Loss_func = nn.CrossEntropyLoss() # 定义损失函数
optimizer = torch.optim.Adam(net.parameters(), lr=learning_rate) # 定义优化器
这部分我们首先加载resnet152预训练模型(resnet152模型的下载可以参考此文章:ResNet 预训练模型下载_resnet18预训练模型下载-CSDN博客),并加入了一个assert断言语句来捕获异常:assert可以在不满足相关程序运行条件时直接返回错误,由此避免程序运行后出现崩溃的情况。
接下来我们定义了一个变量device = torch.device("cuda" if torch.cuda.is_available() else "cpu"),其作用为根据当前系统是不是支持CUDA来选择GPU或CPU进行计算,net.to(device)则是将tensor数据分配到相应的GPU或CPU上。
最后我们定义相关变量:epoch(训练次数),learing_rate(学习率),Loss_func(损失函数),optimizer(优化器),其中epoch和learing_rate大家要选取一个合适的值,不能过大也不能过小,否则可能会导致模型训练效果欠佳或过拟合等情况的发生;Loss_func这里选用了交叉熵损失函数CrossEntropyLoss(),其结合了LogSoftmax()和NLLLoss()两个函数,对于处理人工智能中的分类问题十分常见与有用。
(4)模型的训练与测试
# 训练模型
for epoch in range(epoch):
train_num = 0 # 训练集样本总数初始设为0
net.train() # 设置模型为训练模式,保证 BN层 能够用到每一批数据的均值和方差
for step, (data, target) in enumerate(train_data):
data, target = data.to(device), target.to(device)
optimizer.zero_grad()
output = net(data)
loss_value = Loss_func(output, target)
loss_value.backward()
optimizer.step()
train_num += data.size(0)
# 输出每次循环时的进度与每次循环中的loss值
print(f'当前为第{epoch + 1}次循环 '
f'[此次训练已进行:{step * len(data)}/{len(train_data.dataset)} '
f'{100 * step / len(train_data):.0f}%)]\tLoss is {loss_value.item():.4f}')
print()
# 测试模型
test_loss = 0.0
test_acc = 0.0
test_num = 0
accuracy = 0.0
net.eval()
with torch.no_grad(): # 清空历史梯度
for data, target in test_data:
data, target = data.to(device), target.to(device)
output = net(data)
loss_value2 = Loss_func(output, target)
_, predicted = torch.max(output.data, 1)
test_loss += loss_value2.item()
test_num += data.size(0)
accuracy += (predicted == target).sum().item()
test_acc = 100 * accuracy / test_num
print(f'第{epoch+1}次循环测试:'
f'模型损失值为:{test_loss / test_num:.4f} \t 模型准确率为:{test_acc:.4f}%')
print()
# 保存模型
# torch.save(net,'./shudiao.pth')
这部分我们完成对模型的训练与测试,同时将训练测试后得到的模型进行保存,以此在后面的验证中进行调用。
首先是训练模型,我们对模型进行epoch次迭代训练,训练时每次从数据加载器中取出batch_size大小数量的图片。for step, (data, target) in enumerate(train_data):这句代码中的step表示整个数据集进行多少次batch_size的训练和迭代,(data, target) in ... (train_data):意为将训练集数据分为data图片与target类别标签。在训练的过程中我们加入了optimizer.zero_grad()这句代码来清空每次循环训练的历史梯度,防止梯度累加而造成结果不收敛,output = net(data)这句代码表示将图片数据放入到模型中获取预测值,loss_value = Loss_func(output, target)则是通过对比output(预测值)和target(目标值)之间的差异来计算得到损失值。
接下来是测试模型,此处的测试模型代码是嵌套在模型训练中的,即每一次训练完成后都对模型进行一次测试,从而可以很直观的发现模型精度的变化。测试模型时首先加入代码with torch.no_grad(): 来清空历史梯度,防止梯度累加。测试过程大致可以理解为先获取模型预测值,而后将这个预测值与目标值进行比较,当两者相同时则认为该样本预测正确,统计预测正确的样本数量和,再除以总样本个数,即可得到模型预测正确率。
最后,当epoch次训练完成后我们通过torch.save(net,'./shudiao.pth')这句代码来保存训练得到的模型。
(5)验证模型
# 创建一个与训练时类名顺序一致的列表
class_names = ['Bacterialblight——白枯病', 'Blast——稻瘟病', 'Brownspot——褐斑病', 'Tungro——钨腐病']
# 载入模型:
model = torch.load('shuidao.pth')
model.to(device)
image = Image.open(r'D:\Dataset\Rice Leaf Disease Images\train\Blast\BLAST1_121.JPG')
trans = transforms.Compose([transforms.Resize((224, 224)), transforms.ToTensor()])
image = image.convert("RGB")
image = trans(image)
image = torch.unsqueeze(image, dim=0)
# 开始验证
model.eval() # 关闭梯度,将模型调整为测试模式
with torch.no_grad(): # 梯度清零
outputs = model(image.to(device))
# ans = torch.tensor(outputs.argmax(1)).item()
ans = outputs.argmax(1).clone().detach().item() # 最大的值即为预测结果,找出最大值在数组中的序号
print(class_names[ans]) # 输出的是那种即为预测结果
验证模型即通过验证数据集来验证模型的准确度,我们首先创建一个与训练时类名顺序一致的类名列表,而后载入我们训练得到的模型,并导入一张验证数据集中的图片,再通过trans = transforms.Compose([transforms.Resize((224, 224)), transforms.ToTensor()])——>image = trans(image)对图片进行裁剪和转换为张量数据,最后我们将图片数据放入训练得到的神经网络模型中进行验证。
验证得到的结果如下所示,可以看到结果正确。
这里训练得到的模型文件就不给大家了,此模型当初我只训练了10轮,准确率好像就达到了95%以上(不过实际测试时效果一般般),虽然选用resnet152网络来实现分类任务可能达不到高实时性要求,但此类问题更注重的是检测结果的准确率,因此选用resnet152实现水稻病害的识别是合理的。
3、全部代码
这里将使用resnet152网络实现水稻病害识别的全部代码放在下方,为了助于大家理解,代码的注释做了一定的完善,若有疑问或错误大家可以在评论区中提出。
# 1、导入相关库
import os
import torch
import torchvision
import torch.nn as nn
from PIL import Image
from torch.utils.data import DataLoader
import torchvision.transforms as transforms
# 2、数据准备与数据增强处理
# (1)、数据增强
data_transform = {
"train": transforms.Compose([transforms.RandomResizedCrop(224), # 将数据集图片放缩成224*224大小
transforms.RandomHorizontalFlip(), # 图片有默认0.5的概率进行垂直旋转
# 归一化处理,所有图片元素除以255,得到的元素值都为[0,1])的Tensor
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])]),
# 进行image = (image-mean)/std 的操作,前一组数据表示mean(均值),即图片Tensor的三个维度的均值为[0.485, 0.456, 0.406],
# 后面一组数据表示std(方差),及图片Tensor的三个维度的方差为[0.229, 0.224, 0.225]
# 最终我们得到的图片Tensor由toTensor操作后再调整到[-1,1]
# 验证数据集需要保持数据真实性,因此无需过多处理
"test": transforms.Compose([transforms.Resize((224, 224)), # 调整图片大小为224*224
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])])}
# (2)、加载数据集
train_dataset = torchvision.datasets.ImageFolder(root=r"D:\Dataset\Rice Leaf Disease Images\train",
transform=data_transform["train"])
print(train_dataset.class_to_idx) # 获取训练集中不同类别对应的索引序号
test_dataset = torchvision.datasets.ImageFolder(root=r"D:\Dataset\Rice Leaf Disease Images\test",
transform=data_transform["test"])
print(test_dataset.class_to_idx) # 获取测试集中不同类别对应的索引序号
# (3)、导入数据集
train_data = DataLoader(dataset=train_dataset, batch_size=20, # 将训练数据以每次20张图片的形式抽出进行训练
shuffle=True, num_workers=0)
test_data = DataLoader(dataset=test_dataset, batch_size=10, # 将测试数据以每次10张图片的形式抽出进行测试
shuffle=True, num_workers=0)
# train_size = len(train_dataset) # 训练集的长度
# test_size = len(test_dataset) # 测试集的长度
# print(train_size) # 输出训练集长度看一下,相当于看看有几张图片
# print(test_size) # 输出测试集长度看一下,相当于看看有几张图片
# 3、网络模型搭建
class BasicBlock(nn.Module):
expansion = 1 # 对应残差结构主分支结构当中,同一卷积层 每层卷积核的个数是否发生改变
# 初始化函数,各参数依次为:输入特征矩阵深度、输出特征矩阵深度(对应主分支上卷积核的个数)
# down_sample下采样参数(对应虚线的残差结构)
def __init__(self, in_channel, out_channel, stride=1, down_sample=None):
super(BasicBlock, self).__init__()
# stride默认=1(表示实线残差结构)
# output=(input-3+2*1)/1+1=input输出特征矩阵的高和宽未改变
# stride默认=2(表示虚线残差结构)
# output=(input-3+2*1)/2+1=input/2+0.5=input/2(向下取整)。输出特征矩阵的高和宽缩减为原来的一半
self.conv1 = nn.Conv2d(in_channels=in_channel, out_channels=out_channel,
kernel_size=3, stride=stride, padding=1, bias=False)
# 使用BN时,将bias=False
# 将BN层放在卷积层conv和激活层relu之间
self.bn1 = nn.BatchNorm2d(out_channel)
self.relu = nn.ReLU()
# 接下来开始第二层卷积层,stride都=1
self.conv2 = nn.Conv2d(in_channels=out_channel, out_channels=out_channel,
kernel_size=3, stride=1, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(out_channel)
self.down_sample = down_sample # 定义下采样方法=传入的下采样参数
def forward(self, x): # 正向传播过程,x为输入的特征矩阵
identity = x # 将x赋值给分支identity
if self.downsample is not None: # =none没有输入下采样函数,对应实线残差结构,跳过此部分
# is not None输入了下采样函数,对应虚线残差结构,将输入特征矩阵输入下采样中,得到捷径分支identity的输出
identity = self.downsample(x)
# 主分支
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
# 主分支的输出+捷径分支的输出,再使用激活函数
out += identity
out = self.relu(out)
# 返回残差结构的最终输出
return out
class Bottleneck(nn.Module): # 针对更深层次的残差结构
# 以50层conv2_x为例,卷积层1、2的卷积核个数=64,而第三层卷积核个数=64*4=256,故expansion = 4
expansion = 4
def __init__(self, in_channel, out_channel, stride=1, down_sample=None):
super(Bottleneck, self).__init__()
# 对于第一层卷积层,无论是实线残差结构还是虚线,stride都=1
self.conv1 = nn.Conv2d(in_channels=in_channel, out_channels=out_channel,
kernel_size=1, stride=1, bias=False) # squeeze channels
self.bn1 = nn.BatchNorm2d(out_channel)
# 对于第二层卷积层,实线残差结构和虚线的stride是不同的,stride采用传入的方式
self.conv2 = nn.Conv2d(in_channels=out_channel, out_channels=out_channel,
kernel_size=3, stride=stride, bias=False, padding=1)
self.bn2 = nn.BatchNorm2d(out_channel)
self.conv3 = nn.Conv2d(in_channels=out_channel, out_channels=out_channel * self.expansion,
kernel_size=1, stride=1, bias=False)
self.bn3 = nn.BatchNorm2d(out_channel * self.expansion)
self.relu = nn.ReLU(inplace=True)
self.down_sample = down_sample
# 正向传播过程
def forward(self, x):
identity = x
# self.down_sample=none对应实线残差结构,否则为虚线残差结构
if self.down_sample is not None:
identity = self.down_sample(x)
out = self.conv1(x) # 卷积层
out = self.bn1(out) # BN层
out = self.relu(out) # 激活层
out = self.conv2(out)
out = self.bn2(out)
out = self.relu(out)
out = self.conv3(out)
out = self.bn3(out)
out += identity
out = self.relu(out)
return out
class ResNet(nn.Module):
# 若选择浅层网络结构block=BasicBlock,否则=Bottleneck
# blocks_num所使用的残差结构的数目(是一个列表),若选择34层网络结构,blocks_num=[3,4,6,3]
# num_classes训练集的分类个数
# include_top参数便于在ResNet网络基础上搭建更复杂的网络
def __init__(self, block, blocks_num, num_classes=1000, include_top=True):
super(ResNet, self).__init__()
self.include_top = include_top
self.in_channel = 64 # 输入特征矩阵的深度(经过最大下采样层之后的)
# 第一个卷积层,对应表格中7*7的卷积层,输入特征矩阵的深度RGB图像,故第一个参数=3
self.conv1 = nn.Conv2d(3, self.in_channel, kernel_size=7, stride=2,
padding=3, bias=False)
self.bn1 = nn.BatchNorm2d(self.in_channel)
self.relu = nn.ReLU(inplace=True)
self.max_pool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1) # 对应3*3那个maxpooling
# conv2_x对应的残差结构,是通过_make_layer函数生成的
self.layer1 = self._make_layer(block, 64, blocks_num[0])
# conv3_x
self.layer2 = self._make_layer(block, 128, blocks_num[1], stride=2)
# conv4_x
self.layer3 = self._make_layer(block, 256, blocks_num[2], stride=2)
# conv5_x
self.layer4 = self._make_layer(block, 512, blocks_num[3], stride=2)
if self.include_top:
# 平均池化下采样层,AdaptiveAvgPool2d自适应的平均池化下采样操作,所得到特征矩阵的高和宽都是(1,1)
self.avg_pool = nn.AdaptiveAvgPool2d((1, 1)) # output size = (1, 1)
# 全连接层(输出节点层)
self.fc = nn.Linear(512 * block.expansion, num_classes)
# 卷积层初始化操作
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
# block为BasicBlock或Bottleneck
# channel残差结构中所对应的第一层的卷积核的个数(值为64/128/256/512)
# block_num对应残差结构中每一个conv*_x卷积层的个数(该层一共包含了多少个残差结构)例:34层的conv2_x:block_num取值为3
def _make_layer(self, block, channel, block_num, stride=1):
down_sample = None # 下采样赋值为none
# 对于18层、34层conv2_x不满足此if语句(不执行)
# 而50层、101层、152层网络结构的conv2_x的第一层也是虚线残差结构,需要调整特征矩阵的深度而高度和宽度不需要改变
# 但对于conv3_x、conv4_x、conv5_x不论ResNet为多少层,特征矩阵的高度、宽度、深度都需要调整(高和宽缩减为原来的一半)
if stride != 1 or self.in_channel != channel * block.expansion:
# 生成下采样函数
down_sample = nn.Sequential(
nn.Conv2d(self.in_channel, channel * block.expansion, kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(channel * block.expansion))
layers = [] # 定义一个空列表
# 参数依次为输入特征矩阵的深度,残差结构所对应主分支上第一个卷积层的卷积核个数
# 18层34层的conv2_x的layer1没有经过下采样函数那个if语句down_sample=none
# conv2_x对应的残差结构,通过此函数_make_layer生成的时,没有传入stride参数,stride默认=1
layers.append(block(self.in_channel, channel, down_sample=down_sample, stride=stride))
self.in_channel = channel * block.expansion
# conv3_x、conv4_x、conv5_x的第一层都是虚线残差结构,
# 而从第二层开始都是实线残差结构了,直接压入统一处理
for _ in range(1, block_num): # 由于第一层已经搭建好,从1开始
# self.in_channel:输入特征矩阵的深度,channel:残差结构主分支第一层卷积的卷积核个数
layers.append(block(self.in_channel, channel))
# 通过非关键字参数的形式传入到nn.Sequential,nn.Sequential将所定义的一系列层结构组合在一起并返回
return nn.Sequential(*layers)
# 正向传播
def forward(self, x):
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
x = self.max_pool(x)
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
if self.include_top:
x = self.avg_pool(x) # 平均池化下采样
x = torch.flatten(x, 1) # 展平处理
x = self.fc(x) # 全连接
return x
def resnet18(num_classes=1000, include_top=True):
return ResNet(BasicBlock, [2, 2, 2, 2], num_classes=num_classes, include_top=include_top)
def resnet34(num_classes=1000, include_top=True):
return ResNet(BasicBlock, [3, 4, 6, 3], num_classes=num_classes, include_top=include_top)
def resnet50(num_classes=1000, include_top=True):
return ResNet(Bottleneck, [3, 4, 6, 3], num_classes=num_classes, include_top=include_top)
def resnet101(num_classes=1000, include_top=True):
return ResNet(Bottleneck, [3, 4, 23, 3], num_classes=num_classes, include_top=include_top)
def resnet152(num_classes=1000, include_top=True):
return ResNet(Bottleneck, [3, 8, 36, 3], num_classes=num_classes, include_top=include_top)
# 4、定义相关变量,函数与容器
net = resnet152()
model_weight_path = "./resnet152-b121ed2d.pth" # 加载resnet的预训练模型
assert os.path.exists(model_weight_path), "file {} does not exist.".format(model_weight_path)
missing_keys, unexpected_keys = net.load_state_dict(torch.load(model_weight_path), strict=False)
# 获取GPU设备
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
net.to(device) # 将网络模型放置在可利用的GPU上
# print(model.to(device)) # 输出模型结构
# 定义训练次数,学习率,损失函数与优化器
epoch = 1 # 设置迭代训练次数
learning_rate = 0.001 # 定义学习率
Loss_func = nn.CrossEntropyLoss() # 定义损失函数
optimizer = torch.optim.Adam(net.parameters(), lr=learning_rate) # 定义优化器
# 5、训练模型
for epoch in range(epoch):
train_num = 0 # 训练集样本总数初始设为0
net.train() # 设置模型为训练模式,保证 BN层 能够用到每一批数据的均值和方差
# 每次从数据加载器中取出一批数据,每批次数目为batch_size=20个(step表示整个数据集进行多少次batch_size的训练和迭代)
# (data, target) in ... (train_data):意为将训练集数据分为data图片与target标签
for step, (data, target) in enumerate(train_data):
data, target = data.to(device), target.to(device) # 将取出的数据加载到可利用的GPU上
optimizer.zero_grad() # 清空每次循环训练的历史梯度,防止梯度累加而造成结果不收敛
output = net(data) # 将数据传入模型,通过前向传播获得预测值:
loss_value = Loss_func(output, target) # 获取损失值(即计算神经网络的输出结果output与图片真实标签target的差别)
loss_value.backward() # 进行反向传播:
optimizer.step() # 梯度下降更新参数:
train_num += data.size(0) # 统计训练集样本总数
# 输出训练信息
# 输出每次循环时的进度与每次循环中的loss值
print(f'当前为第{epoch + 1}次循环 '
f'[此次训练已进行:{step * len(data)}/{len(train_data.dataset)} '
f'{100 * step / len(train_data):.0f}%)]\tLoss is {loss_value.item():.4f}')
print()
# 6、测试模型
test_loss = 0.0 # 测试集的损失值初始设为0
test_acc = 0.0 # 测试集的准确率初始设为0
test_num = 0 # 测试集样本总数初始设为0
accuracy = 0.0
net.eval() # 将模型调整为测试模型
with torch.no_grad(): # 清空历史梯度
for data, target in test_data:
data, target = data.to(device), target.to(device)
output = net(data)
loss_value2 = Loss_func(output, target)
_, predicted = torch.max(output.data, 1) # 得到每行最大值(dim=1,按行进行计算)与每个最大值对应的索引值:
test_loss += loss_value2.item() # 每次测试得到的损失值相加
test_num += data.size(0) # 统计样本总数
accuracy += (predicted == target).sum().item() # 统计经过模型测试正确的样本个数:
test_acc = 100 * accuracy / test_num # 计算测试集的本次循环测试后的总准确率
print(f'第{epoch+1}次循环测试:'
f'模型损失值为:{test_loss / test_num:.4f} \t 模型准确率为:{test_acc:.4f}%')
print()
# 保存模型
# torch.save(net,'./shuidao.pth')
# 7、验证模型
# 创建一个与训练时类名顺序一致的列表
class_names = ['Bacterialblight——白枯病', 'Blast——稻瘟病', 'Brownspot——褐斑病', 'Tungro——钨腐病']
# 载入模型:
model = torch.load('shuidao.pth')
model.to(device)
# 载入需要验证识别的数据集图片
image = Image.open(r'D:\Dataset\Rice Leaf Disease Images\train\Blast\BLAST1_121.JPG')
# 将图片缩放为跟训练集图片的大小一样 方便预测,且将图片转换为张量
trans = transforms.Compose([transforms.Resize((224, 224)), transforms.ToTensor()])
# 将图片切换为RGB格式与进行转换操作
image = image.convert("RGB")
image = trans(image)
# 将图片维度扩展一维,得到4维图片
image = torch.unsqueeze(image, dim=0)
# 开始验证
model.eval() # 关闭梯度,将模型调整为测试模式
with torch.no_grad(): # 梯度清零
outputs = model(image.to(device)) # 将图片放入入神经网络进行验证
ans = outputs.argmax(1).clone().detach().item()
# ans = torch.tensor(outputs.argmax(1)).item() # 最大的值即为预测结果,找出最大值在数组中的序号,
print(class_names[ans]) # 输出的是那种即为预测结果
4、常见错误
这里给大家列举一个非常常见的报错问题,问题如下图:
其实这个报错的主要原因就是你的计算机显存不足以支撑这个模型的训练,简单的解决办法就是减小batch_size大小或者减小训练数据集的图片大小(即数据增强部分对图片的裁剪设置的更小一些),麻烦一点的解决办法则是在网上租一个GPU进行训练。具体的解决方法大家可自行查阅一下,这里就不做扩展了。
如果大家想了解模型训练与测试后的准确率和损失值曲线的绘制,可以参考我的这篇文章:(Python可视化)使用matplotlib.pyplot绘制神经网络模型的acc(准确率)和loss(损失值)曲线图-CSDN博客
OK,以上就是本次文章的全部内容,其实对于这种相似的分类问题,代码框架都是相差不大的,大家可以把代码中一些必要的部分进行修改,从而灵活应用到其它分类任务上。