项目目的:通过CNN(卷积神经网络)来对植物叶片进行植物病害识别。
相应的模型有很多,考虑到识别速度和准确率以及部署难易程度的要求,我选择了比较成熟的resnet系列模型。
众所周知,卷积神经网络的深度很大程度决定了识别的准确度,但是较大的深度又会导致较长的训练以及识别时间,由于我不打算做成实时识别的项目,而是逐个输入图片进行判断,所以我对准确率的要求是大于运行时间的。若要做成人脸识别或者手写数字识别等实时识别的项目则,需要考虑运行时间的问题,在精度有保证的情况下使用18、34、50等深度较浅的网络,识别速度较快。
我的数据集有54000张,分为38类。故我这里使用resnet152进行迁移学习。
首先进行数据集的处理,本人使用的植物病害原始数据集格式是这样的:
datasets
-"Apple___Apple_scab"
-"Apple___Black_rot"
-"Apple___Cedar_apple_rust"
-.....................
-"Tomato___Tomato_mosaic_virus"
-"Tomato___healthy"
合计38种 图片总数54000左右
plant_disease_dataset.zip-深度学习文档类资源-CSDN下载
这个是用于数据集增强的代码,会把文件夹下的每张原图片进行随机处理产生新的图片,如果你的数据集数量比较少可以尝试使用。
'''
本代码共采用了四种数据增强,如采用其他数据增强方式,可以参考本代码,随意替换。
imageDir 为原数据集的存放位置
saveDir 为数据增强后数据的存放位置
'''
def flip(root_path,img_name): #翻转图像
img = Image.open(os.path.join(root_path, img_name))
filp_img = img.transpose(Image.FLIP_LEFT_RIGHT)
# filp_img.save(os.path.join(root_path,img_name.split('.')[0] + '_flip.jpg'))
return filp_img
def rotation(root_path, img_name):
img = Image.open(os.path.join(root_path, img_name))
rotation_img = img.rotate(20) #旋转角度
# rotation_img.save(os.path.join(root_path,img_name.split('.')[0] + '_rotation.jpg'))
return rotation_img
def randomColor(root_path, img_name): #随机颜色
"""
对图像进行颜色抖动
:param image: PIL的图像image
:return: 有颜色色差的图像image
"""
image = Image.open(os.path.join(root_path, img_name))
random_factor = np.random.randint(0, 31) / 10. # 随机因子
color_image = ImageEnhance.Color(image).enhance(random_factor) # 调整图像的饱和度
random_factor = np.random.randint(10, 21) / 10. # 随机因子
brightness_image = ImageEnhance.Brightness(color_image).enhance(random_factor) # 调整图像的亮度
random_factor = np.random.randint(10, 21) / 10. # 随机因子
contrast_image = ImageEnhance.Contrast(brightness_image).enhance(random_factor) # 调整图像对比度
random_factor = np.random.randint(0, 31) / 10. # 随机因子
return ImageEnhance.Sharpness(contrast_image).enhance(random_factor) # 调整图像锐度
def contrastEnhancement(root_path, img_name): # 对比度增强
image = Image.open(os.path.join(root_path, img_name))
enh_con = ImageEnhance.Contrast(image)
contrast = 1.5
image_contrasted = enh_con.enhance(contrast)
return image_contrasted
def brightnessEnhancement(root_path,img_name):#亮度增强
image = Image.open(os.path.join(root_path, img_name))
enh_bri = ImageEnhance.Brightness(image)
brightness = 1.5
image_brightened = enh_bri.enhance(brightness)
return image_brightened
def colorEnhancement(root_path,img_name):#颜色增强
image = Image.open(os.path.join(root_path, img_name))
enh_col = ImageEnhance.Color(image)
color = 1.5
image_colored = enh_col.enhance(color)
return image_colored
from PIL import Image
from PIL import ImageEnhance
import os
import cv2
import numpy as np
imageDir="./样本/正常/" #要改变的图片的路径文件夹
saveDir="./样本/正常_增强/" #要保存的图片的路径文件夹
for name in os.listdir(imageDir):
saveName= name[:-4]+"id.jpg"
image = Image.open(os.path.join(imageDir, name))
image.save(os.path.join(saveDir,saveName))
saveName= name[:-4]+"be.jpg"
saveImage=brightnessEnhancement(imageDir,name)
saveImage.save(os.path.join(saveDir,saveName))
saveName= name[:-4]+"fl.jpg"
saveImage=flip(imageDir,name)
saveImage.save(os.path.join(saveDir,saveName))
saveName= name[:-4]+"ro.jpg"
saveImage=rotation(imageDir,name)
saveImage.save(os.path.join(saveDir,saveName))
用于将数据集按 train : val :test = 8 : 1 :1 复制分配好,完成后备份好原数据集即可。
# 工具类
import os
import random
import shutil
from shutil import copy2
"""
数据集默认的比例是--训练集:验证集:测试集=8:1:1
"""
def data_set_split(src_data_folder, target_data_folder, train_scale=0.8, val_scale=0.1, test_scale=0.1):
'''
读取源数据文件夹,生成划分好的文件夹,分为trian、val、test三个文件夹进行
:param src_data_folder: 源文件夹
:param target_data_folder: 目标文件夹
:param train_scale: 训练集比例
:param val_scale: 验证集比例
:param test_scale: 测试集比例
:return:
'''
print("开始数据集划分")
class_names = os.listdir(src_data_folder)
# 在目标目录下创建文件夹
split_names = ['train', 'val', 'test']
for split_name in split_names:
split_path = os.path.join(target_data_folder, split_name)
if os.path.isdir(split_path):
pass
else:
os.mkdir(split_path)
# 然后在split_path的目录下创建类别文件夹
for class_name in class_names:
class_split_path = os.path.join(split_path, class_name)
if os.path.isdir(class_split_path):
pass
else:
os.mkdir(class_split_path)
# 按照比例划分数据集,并进行数据图片的复制
# 首先进行分类遍历
for class_name in class_names:
current_class_data_path = os.path.join(src_data_folder, class_name)
current_all_data = os.listdir(current_class_data_path)
current_data_length = len(current_all_data)
current_data_index_list = list(range(current_data_length))
random.shuffle(current_data_index_list)
train_folder = os.path.join(os.path.join(target_data_folder, 'train'), class_name)
val_folder = os.path.join(os.path.join(target_data_folder, 'val'), class_name)
test_folder = os.path.join(os.path.join(target_data_folder, 'test'), class_name)
train_stop_flag = current_data_length * train_scale
val_stop_flag = current_data_length * (train_scale + val_scale)
current_idx = 0
train_num = 0
val_num = 0
test_num = 0
for i in current_data_index_list:
src_img_path = os.path.join(current_class_data_path, current_all_data[i])
if current_idx <= train_stop_flag:
copy2(src_img_path, train_folder)
# print("{}复制到了{}".format(src_img_path, train_folder))
train_num = train_num + 1
elif (current_idx > train_stop_flag) and (current_idx <= val_stop_flag):
copy2(src_img_path, val_folder)
# print("{}复制到了{}".format(src_img_path, val_folder))
val_num = val_num + 1
else:
copy2(src_img_path, test_folder)
# print("{}复制到了{}".format(src_img_path, test_folder))
test_num = test_num + 1
current_idx = current_idx + 1
print("*********************************{}*************************************".format(class_name))
print(
"{}类按照{}:{}:{}的比例划分完成,一共{}张图片".format(class_name, train_scale, val_scale, test_scale, current_data_length))
print("训练集{}:{}张".format(train_folder, train_num))
print("验证集{}:{}张".format(val_folder, val_num))
print("测试集{}:{}张".format(test_folder, test_num))
if __name__ == '__main__':
src_data_folder = r".color" #划分前的数据集的位置
target_data_folder = r".dataset" #划分后的数据集的位置
data_set_split(src_data_folder, target_data_folder)
resnet各类模型的代码实现(18、34、50、101、152等)
import torch.nn as nn
import torch
class BasicBlock(nn.Module):
expansion = 1 # 对应残差结构主分支结构当中,同一卷积层 每层卷积核的个数是否发生改变
# 初始化函数,各参数依次为:输入特征矩阵深度、输出特征矩阵深度(对应主分支上卷积核的个数)
# downsample下采样参数(对应虚线的残差结构)
def __init__(self, in_channel, out_channel, stride=1, downsample=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.downsample = downsample # 定义下采样方法=传入的下采样参数
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, downsample=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) # unsqueeze channels
self.bn3 = nn.BatchNorm2d(out_channel * self.expansion)
self.relu = nn.ReLU(inplace=True)
self.downsample = downsample
# 正向传播过程
def forward(self, x):
identity = x
# self.downsample=none对应实线残差结构,否则为虚线残差结构
if self.downsample is not None:
identity = self.downsample(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.maxpool = 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.avgpool = 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):
downsample = 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:
# 生成下采样函数
downsample = 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语句downsample=none
# conv2_x对应的残差结构,通过此函数_make_layer生成的时,没有传入stride参数,stride默认=1
layers.append(block(self.in_channel, channel, downsample=downsample, 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.maxpool(x)
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
if self.include_top:
x = self.avgpool(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)
预训练模型下载地址,我在这里使用的是 resnet152 模型
'resnet18': 'https:
'resnet34': 'https:
'resnet50': 'https:
'resnet101': 'https:
'resnet152': 'https:
'resnext50_32x4d': 'https:
'resnext101_32x8d': 'https:
'wide_resnet50_2': 'https:
'wide_resnet101_2': 'https:
对数据集进行归一化处理并导入预训练模型进行迁移学习训练
import torch
import torch.nn as nn
from torchvision import transforms, datasets
import json
import matplotlib.pyplot as plt
import os
import torch.optim as optim
from model import resnet152
import torchvision.models.resnet
def main():
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print("using {} device.".format(device))
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])]),
"val": transforms.Compose([transforms.Resize(256), # 长宽比例不变,将最小边缩放到256
transforms.CenterCrop(224), # 再中心裁减一个224*224大小的图片
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])])}
data_root = os.path.abspath(os.path.join(os.getcwd(), "../..")) # get data root path
image_path = data_root + "/pyCharmdata/Resnet151_plant/dataset/" # data set path
#更改成你的数据集的位置
assert os.path.exists(image_path), "{} path does not exist.".format(image_path)
train_dataset = datasets.ImageFolder(root=os.path.join(image_path, "train"),
transform=data_transform["train"])
train_num = len(train_dataset)
flower_list = train_dataset.class_to_idx
cla_dict = dict((val, key) for key, val in flower_list.items())
# write dict into json file
json_str = json.dumps(cla_dict, indent=4)
with open('class_indices.json', 'w') as json_file:
json_file.write(json_str)
batch_size = 16 #
nw = min([os.cpu_count(), batch_size if batch_size > 1 else 0, 8]) # number of workers
print('Using {} dataloader workers every process'.format(nw))
train_loader = torch.utils.data.DataLoader(train_dataset,
batch_size=batch_size, shuffle=True,
num_workers=0)#nw 单线程编译
validate_dataset = datasets.ImageFolder(root=os.path.join(image_path, "val"),
transform=data_transform["val"])
val_num = len(validate_dataset)
validate_loader = torch.utils.data.DataLoader(validate_dataset,
batch_size=batch_size, shuffle=False,
num_workers=0)#nw 单线程编译
print("using {} images for training, {} images for validation.".format(train_num,
val_num))#
# 若不使用迁移学习的方法,注释掉61-69行,并net = resnet152(num_calsses参数)
net = resnet152() # 未传入参数,最后一个全连接层有1000个结点 这里也可以使用resnet101、50、34、18等网络,不过得提前下载
# load pretrain weights
# 模型下载地址在dowmload.txt可见
model_weight_path = "./resnet152-b121ed2d.pth"
assert os.path.exists(model_weight_path), "file {} does not exist.".format(model_weight_path)
# net.load_state_dict载入模型权重。torch.load(model_weight_path)载入到内存当中还未载入到模型当中
missing_keys, unexpected_keys = net.load_state_dict(torch.load(model_weight_path), strict=False)
# for param in net.parameters():
# param.requires_grad = False
# change fc layer structure
in_channel = net.fc.in_features # 输入特征矩阵的深度。net.fc是所定义网络的全连接层
# 类别个数
net.fc = nn.Linear(in_channel, 38) # 类别个数 38
net.to(device)
loss_function = nn.CrossEntropyLoss()
optimizer = optim.Adam(net.parameters(), lr=0.0001)
best_acc = 0.0
save_path = './resNet152.pth' #可以自己命名,后续预测时得对应得上
for epoch in range(30):#训练次数
# train
net.train()
running_loss = 0.0
for step, data in enumerate(train_loader, start=0):
images, labels = data
optimizer.zero_grad()
logits = net(images.to(device))
loss = loss_function(logits, labels.to(device))
loss.backward()
optimizer.step()
# print statistics
running_loss += loss.item()
# print train process
rate = (step+1)/len(train_loader)
a = "*" * int(rate * 50)
b = "." * int((1 - rate) * 50)
print("rtrain loss: {:^3.0f}%[{}->{}]{:.4f}".format(int(rate*100), a, b, loss), end="")
print()
# validate
net.eval() # 控制训练过程中的Batch normalization
acc = 0.0 # accumulate accurate number / epoch
with torch.no_grad():
for val_data in validate_loader:
val_images, val_labels = val_data
outputs = net(val_images.to(device)) # eval model only have last output layer
# loss = loss_function(outputs, test_labels)
predict_y = torch.max(outputs, dim=1)[1]
acc += (predict_y == val_labels.to(device)).sum().item()
val_accurate = acc / val_num
if val_accurate > best_acc:
best_acc = val_accurate
torch.save(net.state_dict(), save_path)
print('[epoch %d] train_loss: %.3f test_accuracy: %.3f' %
(epoch + 1, running_loss / step, val_accurate))
print('Finished Training')
if __name__ == '__main__':
main()
我在简单的训练了30轮后,最高的准确率已达99.6%,若增加训练次数,效果估计可以更好。
导入训练好的模型和待预测的图片,返回判断的种类以及概率。
import torch
from model import resnet152
from PIL import Image
from torchvision import transforms
import matplotlib.pyplot as plt
import json
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
data_transform = transforms.Compose(
[transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])]) # 预处理
# load image
img = Image.open("./huanglongbing.JPG")#导入需要检测的图片
plt.imshow(img)
# [N, C, H, W]
img = data_transform(img)
# expand batch dimension
img = torch.unsqueeze(img, dim=0)
# read class_indict
try:
json_file = open('./class_indices.json', 'r')
class_indict = json.load(json_file)
except Exception as e:
print(e)
exit(-1)
# create model
model = resnet152(num_classes=38)#修改为你训练时一共的种类数
# load model weights
model_weight_path = "./resNet152.pth"#导入训练好的模型
model.load_state_dict(torch.load(model_weight_path, map_location=device))
model.eval()
with torch.no_grad(): # 不对损失梯度进行跟踪
# predict class
output = torch.squeeze(model(img)) # 压缩batch维度
predict = torch.softmax(output, dim=0) # 得到概率分布
predict_cla = torch.argmax(predict).numpy() # argmax寻找最大值对应的索引
print(class_indict[str(predict_cla)], predict[predict_cla].numpy())
plt.show()
这里给出我的项目成品,识别38种植物病害,准确率为99.6%。内含训练好的模型和test数据集:
resnet152_plant.zip-深度学习文档类资源-CSDN下载
相关知识
基于深度学习和迁移学习的识花实践
Pytorch实现鲜花分类(102 Category Flower Dataset)
植物病害检测系统:利用深度学习守护农田的科技先锋
基于残差网络迁移学习的花卉识别系统
基于深度迁移学习模型的花卉种类识别
基于迁移学习的花卉识别系统
【深度学习】 图像识别实战 102鲜花分类(flower 102)实战案例
基于半监督主动学习的菊花表型分类研究
图像识别算法有哪些
基于 CNN 和迁移学习的农作物病害识别方法研究
网址: 【pytorch】resNet152迁移学习实现植物病害图像识别分类 https://m.huajiangbk.com/newsview130976.html
上一篇: 基于深度学习神经网络的农业病虫害 |
下一篇: 自动叶片感染识别系统:智能检测植 |