基于深度学习算法的动物检测系统(含PyQt+代码+训练数据集)
基于深度学习算法的动物检测系统(含PyQt+代码+训练数据集)
- 前言
- 一、数据集
- 1.1 数据集介绍
- 1.2 数据预处理
- 二、模型搭建
- 三、训练与测试
- 3.1 模型训练
- 3.2 模型测试
- 四、PyQt界面实现
- 五、讨论
- 5.1 模型优缺点分析
- 5.2 实验意义
- 参考资料
前言
本项目是基于MobileNet深度学习网络模型的动物检测系统,目前能够检测大象、狗、蝴蝶、坤坤、牛、马等10种动物,可以自己添加动物种类进行训练。本文将详述数据集处理、模型构建、训练代码、以及基于PyQt5的应用界面设计。在应用中可以对动物的图片进行识别,输出动物的类别、模型对其预测结果的置信度以及关于该动物的详细描述。本文附带了完整的应用界面设计、深度学习模型代码和训练数据集的下载链接。
完整资源下载链接:
- 基于深度学习的动物分类检测识别系统(PyQT+代码+训练数据集+UI界面版本1)
- 基于深度学习的动物分类检测识别系统(PyQT+代码+训练数据集+UI界面版本2)
项目演示视频:
一、数据集
1.1 数据集介绍
本项目使用的数据集在all_data
文件夹下。它包含了来自10种不同的动物类别的图像(来自网络采集),每个类别有1000张以上的图像,共计26179张图像。这些动物类别包括:大象、狗、蝴蝶、坤坤、牛、马等。
该文件夹下包含了10个子文件夹,每个子文件夹都存储了一种类别的动物图像,子文件夹的名称就是动物类别的名称,同时为了保证数据的多样性和代表性,每类动物包括多个个体和不同的姿态。动物数据集,如下图所示:
1.2 数据预处理
首先使用MyDataSet
类在 PyTorch 中加载图像数据并将其与相应的类别标签配对,完成自定义数据集的生成。它包含初始化方法__init__
来接收图像文件路径列表和对应的类别标签列表,并提供了__getitem__
方法来获取图像及其标签,同时还可以使用collate_fn
将多个样本进行批处理。
class MyDataSet(Dataset):
"""自定义数据集"""
def __init__(self, images_path: list, images_class: list, transform=None):
self.images_path = images_path
self.images_class = images_class
self.transform = transform
def __len__(self):
return len(self.images_path)
def __getitem__(self, item):
img = Image.open(self.images_path[item])
# RGB为彩色图片,L为灰度图片
if img.mode != 'RGB':
raise ValueError("image: {} isn't RGB mode.".format(self.images_path[item]))
label = self.images_class[item]
if self.transform is not None:
img = self.transform(img)
return img, label
@staticmethod
def collate_fn(batch):
images, labels = tuple(zip(*batch))
images = torch.stack(images, dim=0)
labels = torch.as_tensor(labels)
return images, labels
然后,我们将所有图像的尺寸调整为224x224像素,以适应模型输入层的要求。最后,应用了一系列数据增强技术来提高模型的泛化能力,包括随机裁剪、水平翻转等。此外,对所有图像进行了标准化处理,使用了ImageNet的预训练均值和标准差,即均值[0.485, 0.456, 0.406],标准差[0.229, 0.224, 0.225]。这些预处理步骤有助于减轻模型在训练过程中因图像大小、颜色变化等因素带来的偏差。
img_size = 224
data_transform = transforms.Compose(
[transforms.Resize(int(img_size * 1.143)),
transforms.CenterCrop(img_size),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])])
assert os.path.exists(img_path), "file: '{}' dose not exist.".format(img_path)
img = Image.open(img_path)
plt.imshow(img)
# [N, C, H, W]
img = data_transform(img)
# expand batch dimension
img = torch.unsqueeze(img, dim=0)
二、模型搭建
选择MobileNet
作为主要的模型结构。MobileNet
是一种轻量级卷积神经网络,特别适合于资源受限的设备,如移动设备和嵌入式系统。MobileNet
通过引入深度可分离卷积(Depthwise Separable Convolution),显著降低了模型的计算复杂度和参数量,同时保持了较高的分类性能。传统的卷积操作在进行特征提取时需要大量的计算资源。MobileNet
通过将标准卷积分解为深度卷积(Depthwise Convolution)和逐点卷积(Pointwise Convolution),大幅减少了计算量。具体而言,深度卷积是指对每个输入通道单独应用一个卷积核,而逐点卷积则是在深度卷积的输出上应用1x1
卷积核,用于组合这些输出通道。通过这种分解,MobileNet
的计算量和参数量分别减少了约8-9
倍,而模型的分类性能则得到了有效的保持。
MobileNet
的网络架构主要由若干个深度可分离卷积模块组成,每个模块包括一个深度卷积层和一个逐点卷积层。
以下是MobileNet的主要结构特点:
- 输入层:输入层接受224x224的RGB图像,并通过标准卷积层提取初步特征。
- 深度可分离卷积模块:多个深度可分离卷积模块逐层提取和组合特征。每个模块通常伴随着批归一化(Batch Normalization)和ReLU激活函数,以加速收敛和提高非线性表达能力。
- 全局平均池化层:在特征提取完成后,使用全局平均池化层将特征映射缩减为一个向量,减少参数量的同时保持重要信息。
- 全连接层:最终的全连接层用于输出每个类别的概率分布,模型通过Softmax函数生成最终的分类结果。
MobileNet
的网络架构代码如下,
"""mobilenet in pytorch
[1] Andrew G. Howard, Menglong Zhu, Bo Chen, Dmitry Kalenichenko, Weijun Wang, Tobias Weyand, Marco Andreetto, Hartwig Adam
MobileNets: Efficient Convolutional Neural Networks for Mobile Vision Applications
https://arxiv.org/abs/1704.04861
"""
import torch
import torch.nn as nn
class DepthSeperabelConv2d(nn.Module):
def __init__(self, input_channels, output_channels, kernel_size, **kwargs):
super().__init__()
self.depthwise = nn.Sequential(
nn.Conv2d(
input_channels,
input_channels,
kernel_size,
groups=input_channels,
**kwargs),
nn.BatchNorm2d(input_channels),
nn.ReLU(inplace=True)
)
self.pointwise = nn.Sequential(
nn.Conv2d(input_channels, output_channels, 1),
nn.BatchNorm2d(output_channels),
nn.ReLU(inplace=True)
)
def forward(self, x):
x = self.depthwise(x)
x = self.pointwise(x)
return x
class BasicConv2d(nn.Module):
def __init__(self, input_channels, output_channels, kernel_size, **kwargs):
super().__init__()
self.conv = nn.Conv2d(
input_channels, output_channels, kernel_size, **kwargs)
self.bn = nn.BatchNorm2d(output_channels)
self.relu = nn.ReLU(inplace=True)
def forward(self, x):
x = self.conv(x)
x = self.bn(x)
x = self.relu(x)
return x
class MobileNet(nn.Module):
"""
Args:
width multipler: The role of the width multiplier α is to thin
a network uniformly at each layer. For a given
layer and width multiplier α, the number of
input channels M becomes αM and the number of
output channels N becomes αN.
"""
def __init__(self, width_multiplier=1, class_num=100):
super().__init__()
alpha = width_multiplier
self.stem = nn.Sequential(
BasicConv2d(3, int(32 * alpha), 3, padding=1, bias=False),
DepthSeperabelConv2d(
int(32 * alpha),
int(64 * alpha),
3,
padding=1,
bias=False
)
)
#downsample
self.conv1 = nn.Sequential(
DepthSeperabelConv2d(
int(64 * alpha),
int(128 * alpha),
3,
stride=2,
padding=1,
bias=False
),
DepthSeperabelConv2d(
int(128 * alpha),
int(128 * alpha),
3,
padding=1,
bias=False
)
)
#downsample
self.conv2 = nn.Sequential(
DepthSeperabelConv2d(
int(128 * alpha),
int(256 * alpha),
3,
stride=2,
padding=1,
bias=False
),
DepthSeperabelConv2d(
int(256 * alpha),
int(256 * alpha),
3,
padding=1,
bias=False
)
)
#downsample
self.conv3 = nn.Sequential(
DepthSeperabelConv2d(
int(256 * alpha),
int(512 * alpha),
3,
stride=2,
padding=1,
bias=False
),
DepthSeperabelConv2d(
int(512 * alpha),
int(512 * alpha),
3,
padding=1,
bias=False
),
DepthSeperabelConv2d(
int(512 * alpha),
int(512 * alpha),
3,
padding=1,
bias=False
),
DepthSeperabelConv2d(
int(512 * alpha),
int(512 * alpha),
3,
padding=1,
bias=False
),
DepthSeperabelConv2d(
int(512 * alpha),
int(512 * alpha),
3,
padding=1,
bias=False
),
DepthSeperabelConv2d(
int(512 * alpha),
int(512 * alpha),
3,
padding=1,
bias=False
)
)
#downsample
self.conv4 = nn.Sequential(
DepthSeperabelConv2d(
int(512 * alpha),
int(1024 * alpha),
3,
stride=2,
padding=1,
bias=False
),
DepthSeperabelConv2d(
int(1024 * alpha),
int(1024 * alpha),
3,
padding=1,
bias=False
)
)
self.fc = nn.Linear(int(1024 * alpha), class_num)
self.avg = nn.AdaptiveAvgPool2d(1)
def forward(self, x):
x = self.stem(x)
x = self.conv1(x)
x = self.conv2(x)
x = self.conv3(x)
x = self.conv4(x)
x = self.avg(x)
x = x.view(x.size(0), -1)
x = self.fc(x)
return x
def mobilenet(alpha=1, class_num=10):
return MobileNet(alpha, class_num)
为了提高MobileNet
模型的识别精度,训练过程中采用了随机梯度下降(SGD)优化器,学习率初始设置为0.0001
,并采用了动态学习率调整策略,根据验证集的表现进行适当的学习率衰减。
三、训练与测试
3.1 模型训练
为了确保模型能够在多样化的数据上进行训练,数据集被划分为训练集(80%)和验证集(20%),并在独立的验证集上进行性能评估。在训练过程中,模型训练了50个Epoch,每个Epoch包含多个Batch处理,每个Batch的大小为16。通过小批量随机梯度下降法(Mini-batch SGD)进行优化,Batch size设置为16,能够在内存和计算效率之间取得平衡。
parser = argparse.ArgumentParser()
parser.add_argument('--num_classes', type=int, default=10)
parser.add_argument('--epochs', type=int, default=100)
parser.add_argument('--batch-size', type=int, default=2)
parser.add_argument('--lr', type=float, default=0.0001)
# 数据集所在根目录
parser.add_argument('--data-path', type=str,
default="all_data")
# 预训练权重路径,如果不想载入就设置为空字符
parser.add_argument('--weights', type=str, default='',
help='initial weights path')
# 是否冻结权重
parser.add_argument('--freeze-layers', type=bool, default=False)
parser.add_argument('--device', default='cuda:0', help='device id (i.e. 0 or 0,1 or cpu)')
opt = parser.parse_args()
train_loss_list = []
train_acc_list = []
val_loss_list = []
val_acc_list = []
main(opt)
draw(train_acc_list, val_acc_list, 'acc')
draw(train_loss_list, val_loss_list, 'loss')
然后通过下面代码,设置模型训练设备和文件夹路径。接着对数据进行预处理并创建数据集和数据加载器。并根据命令行参数配置模型并加载预训练权重,可选择性地冻结部分模型参数。最后,使用SGD
优化器进行训练,并在每个epoch结束时保存模型权重。整个训练过程可以记录损失、准确率等指标,并将其写入TensorBoard。
def main(args):
device = torch.device(args.device if torch.cuda.is_available() else "cpu")
if os.path.exists("./weights") is False:
os.makedirs("./weights")
tb_writer = SummaryWriter()
train_images_path, train_images_label, val_images_path, val_images_label = read_split_data(args.data_path)
img_size = 224
data_transform = {
"train": transforms.Compose([transforms.RandomResizedCrop(img_size),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])]),
"val": transforms.Compose([transforms.Resize(int(img_size * 1.143)),
transforms.CenterCrop(img_size),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])])}
# 实例化训练数据集
train_dataset = MyDataSet(images_path=train_images_path,
images_class=train_images_label,
transform=data_transform["train"])
# 实例化验证数据集
val_dataset = MyDataSet(images_path=val_images_path,
images_class=val_images_label,
transform=data_transform["val"])
batch_size = args.batch_size
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,
pin_memory=True,
num_workers=nw,
collate_fn=train_dataset.collate_fn)
val_loader = torch.utils.data.DataLoader(val_dataset,
batch_size=batch_size,
shuffle=False,
pin_memory=True,
num_workers=nw,
collate_fn=val_dataset.collate_fn)
model = create_model(class_num=args.num_classes).to(device)
if args.weights != "":
assert os.path.exists(args.weights), "weights file: '{}' not exist.".format(args.weights)
model.load_state_dict(torch.load(args.weights, map_location=device))
if args.freeze_layers:
for name, para in model.named_parameters():
# 除head外,其他权重全部冻结
if "head" not in name:
para.requires_grad_(False)
else:
print("training {}".format(name))
pg = [p for p in model.parameters() if p.requires_grad]
optimizer = optim.AdamW(pg, lr=args.lr, weight_decay=5E-2)
for epoch in range(args.epochs):
# train
train_loss, train_acc = train_one_epoch(model=model,
optimizer=optimizer,
data_loader=train_loader,
device=device,
epoch=epoch)
# validate
val_loss, val_acc = evaluate(model=model,
data_loader=val_loader,
device=device,
epoch=epoch)
train_acc_list.append(train_acc)
train_loss_list.append(train_loss)
val_acc_list.append(val_acc)
val_loss_list.append(val_loss)
tags = ["train_loss", "train_acc", "val_loss", "val_acc", "learning_rate"]
tb_writer.add_scalar(tags[0], train_loss, epoch)
tb_writer.add_scalar(tags[1], train_acc, epoch)
tb_writer.add_scalar(tags[2], val_loss, epoch)
tb_writer.add_scalar(tags[3], val_acc, epoch)
tb_writer.add_scalar(tags[4], optimizer.param_groups[0]["lr"], epoch)
if train_loss == min(train_loss_list):
print('save-best-epoch:{}'.format(epoch))
torch.save(model.state_dict(), "./weights/best-epoch.pth")
整个训练、测试过程可以记录损失、准确率、混淆矩阵等性能评价指标
1. 损失指标
损失值是衡量模型在给定数据上预测误差的重要指标,随着训练的进行,损失值应逐渐降低,直到达到某一稳定值。
根据实验数据,训练集的损失值从初始的1.87
逐步下降到0.448
,验证集的损失值则从1.75
下降到0.309
(如图上图所示)。验证集的损失值明显低于训练集,表明模型在验证数据上的表现优于训练数据,这可能与验证数据的样本分布更加均匀有关。同时,损失值的收敛趋势表明,经过50
个Epoch的训练,模型已经达到了较好的拟合效果,并且没有出现过拟合现象。
2. 准确率
模型的分类准确率是评估其性能的关键指标之一。
实验结果显示,训练集的准确率从初始的约0.1
逐渐提升到0.852
,而验证集的准确率则从0.12
提升至0.914
(如图上图所示)。验证集的准确率在模型训练后期持续高于训练集,这进一步验证了模型在验证数据上的良好表现。这表明模型不仅能够有效地学习训练数据中的模式,而且能够很好地泛化到未见过的验证数据。最高的验证准确率为91.4%
,表明本系统在动物分类任务中具有较高的识别精度。
- 混淆矩阵分析
混淆矩阵提供了更为详细的分类结果信息,显示了模型在每一类上的预测准确性。
根据混淆矩阵的结果,模型对大部分类别的识别准确率都非常高,平均准确率达到了95%
。对于部分类别,如狗和绵羊,尽管存在少量的误分类情况,但整体表现依然令人满意。这些误分类现象可能与这些类别之间存在较高的相似性或样本的多样性有关。进一步的误分类分析可以帮助优化数据集,或者调整模型的超参数设置。
3.2 模型测试
可以分别使用predict.py
对单张动物图片进行检测。
# predict.py
def main(img_path):
import os
os.environ['KMP_DUPLICATE_LIB_OK'] = 'TRUE'
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
img_size = 224
data_transform = transforms.Compose(
[transforms.Resize(int(img_size * 1.143)),
transforms.CenterCrop(img_size),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])])
assert os.path.exists(img_path), "file: '{}' dose not exist.".format(img_path)
img = Image.open(img_path)
plt.imshow(img)
# [N, C, H, W]
img = data_transform(img)
# expand batch dimension
img = torch.unsqueeze(img, dim=0)
# read class_indict
json_path = './class_indices.json'
assert os.path.exists(json_path), "file: '{}' dose not exist.".format(json_path)
json_file = open(json_path, "r")
class_indict = json.load(json_file)
# create model 创建模型网络
model = create_model(class_num=10).to(device)
# load model weights 加载模型
model_weight_path = "weights/best-epoch.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.to(device))).cpu()
predict = torch.softmax(output, dim=0)
predict_cla = torch.argmax(predict).numpy()
for i in range(len(predict)):
print("class: {:10} prob: {:.3}".format(class_indict[str(i)],
predict[i].numpy()))
# 返回检测结果和准确率
res = class_indict[str(list(predict.numpy()).index(max(predict.numpy())))]
num= "%.2f" % (max(predict.numpy()) * 100) + "%"
print(res,num)
return res,num
测试结果:
四、PyQt界面实现
当整个项目构建完成后,使用PyQt5编写可视化界面,可以支持植物/中草药的检测。运行ui.py
,然后点击文件夹图片传入待检测的动物图像即可。经过动物识别系统识别后,会输出相应的类别和置信度,以及相应类别的详细介绍。
系统界面UI基础版,只可以选择单张图形进行识别检测。
系统界面UI升级版,可以批量地对动物图像进行识别检测。
五、讨论
5.1 模型优缺点分析
1、模型优点
本研究选择了MobileNet
作为核心模型,这一选择带来了多方面的优势。首先,MobileNet
通过引入深度可分离卷积,有效地降低了模型的计算复杂度和参数量,这使得模型在资源受限的环境下也能够运行良好。具体而言,MobileNet
的参数量仅为传统卷积神经网络的1/8至1/9,这不仅减少了计算成本,也降低了内存占用,使得模型可以在移动设备或嵌入式系统中部署。
其次,实验结果表明,尽管MobileNet
是一个轻量级模型,但它在分类准确率上表现出色。验证集的最高准确率达到了91.4%,平均准确率达到了96%,这表明MobileNet
能够在保证高识别精度的同时,兼顾模型的效率和轻量化需求。此外,模型在多种不同类别的动物识别任务中表现稳定,没有明显的类别偏差,说明其具有良好的泛化能力。
2、模型缺点
尽管MobileNet
在本研究中表现良好,但它也存在一些局限性。首先,轻量级的设计虽然降低了计算复杂度,但在处理非常复杂的图像特征时,MobileNet
可能无法与更深层次的网络(如ResNet或DenseNet)相比。这一点在处理高相似度的动物类别时尤为明显,虽然整体准确率较高,但在某些细分类别上,误分类的现象仍然存在。
此外,MobileNet
在应对极端情况下(如低光照、遮挡或严重噪声干扰)时的表现还有待提升。虽然本研究通过数据增强技术部分缓解了这些问题,但仍需进一步研究如何提高模型在复杂环境下的鲁棒性。
5.2 实验意义
本研究的实验结果对动物识别技术的发展具有重要意义。首先,我们验证了MobileNet
作为轻量级模型在实际应用中的可行性和有效性。这表明在资源有限的场景中,如移动设备或嵌入式系统,轻量级的深度学习模型仍能提供高效且准确的动物识别服务。
其次,实验结果也显示了深度学习在处理多样化数据时的强大能力。通过对26179
张多样化动物图像的训练,模型成功学会了捕捉各类动物的关键特征,这不仅证明了深度学习的强大学习能力,也为进一步的应用奠定了基础。
参考资料
- 论文:https://arxiv.org/pdf/2103.14030.pdf
MobileNet
网络模型详解资料:详解MobileNet- 界面设计