1.研究背景与意义
项目参考AAAI Association for the Advancement of Artificial Intelligence
研究背景与意义
随着全球人口的不断增长和粮食需求的增加,农业生产的效率和产量成为了一个重要的问题。黄豆作为世界上最重要的粮食作物之一,在全球范围内被广泛种植。然而,黄豆种子的计数是农业生产中一个关键的环节,直接影响到种植者的决策和作物的产量。传统的黄豆种子计数方法通常是手工操作,耗时耗力且容易出现误差。因此,开发一种高效准确的黄豆种子计数培养仿真系统具有重要的现实意义。
近年来,深度学习技术在计算机视觉领域取得了巨大的突破,特别是目标检测领域。YOLOv5是一种基于骨干网络EdgeNeXt改进的目标检测算法,具有高效准确的特点。然而,目前尚未有研究将YOLOv5应用于黄豆种子计数培养仿真系统中。因此,本研究旨在基于骨干网络EdgeNeXt改进YOLOv5,开发一种高效准确的黄豆种子计数培养仿真系统。
首先,本研究将通过改进骨干网络EdgeNeXt,提高YOLOv5在目标检测中的性能。骨干网络是深度学习模型中的重要组成部分,对于提取图像特征具有关键作用。通过改进骨干网络,可以提高模型对黄豆种子的检测准确率和稳定性。
其次,本研究将构建一个黄豆种子计数培养仿真系统,实现对黄豆种子的自动计数。该系统将结合YOLOv5目标检测算法和计算机视觉技术,实现对黄豆种子的自动识别和计数。通过使用深度学习算法,可以大大提高计数的准确性和效率,减少人工操作的成本和误差。
最后,本研究将进行系统的评估和优化。通过对系统的性能进行评估,可以验证系统的准确性和稳定性。同时,针对系统中存在的问题和不足,进行优化和改进,进一步提高系统的性能和可靠性。
本研究的意义在于提供一种高效准确的黄豆种子计数培养仿真系统,为农业生产提供技术支持。该系统可以减少人工操作的成本和误差,提高计数的准确性和效率。同时,该系统还可以为种植者提供决策支持,帮助他们更好地管理和控制黄豆种子的生长和发展。此外,本研究还可以为其他作物的种子计数提供借鉴和参考,推动农业生产的现代化和智能化发展。
总之,基于骨干网络EdgeNeXt改进YOLOv5的黄豆种子计数培养仿真系统具有重要的现实意义和应用价值。通过本研究的开展,可以提高黄豆种子计数的准确性和效率,为农业生产提供技术支持,推动农业生产的现代化和智能化发展。
2.图片演示
3.视频演示
基于骨干网络EdgeNeXt改进YOLOv5的黄豆种子计数培养仿真系统_哔哩哔哩_bilibili
4.数据集的采集&标注和整理
图片的收集
首先,我们需要收集所需的图片。这可以通过不同的方式来实现,例如使用现有的公开数据集Soybeanseeds。
labelImg是一个图形化的图像注释工具,支持VOC和YOLO格式。以下是使用labelImg将图片标注为VOC格式的步骤:
(1)下载并安装labelImg。
(2)打开labelImg并选择“Open Dir”来选择你的图片目录。
(3)为你的目标对象设置标签名称。
(4)在图片上绘制矩形框,选择对应的标签。
(5)保存标注信息,这将在图片目录下生成一个与图片同名的XML文件。
(6)重复此过程,直到所有的图片都标注完毕。
由于YOLO使用的是txt格式的标注,我们需要将VOC格式转换为YOLO格式。可以使用各种转换工具或脚本来实现。
下面是一个简单的方法是使用Python脚本,该脚本读取XML文件,然后将其转换为YOLO所需的txt格式。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import xml.etree.ElementTree as ET
import os
classes = [] # 初始化为空列表
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
def convert(size, box):
dw = 1. / size[0]
dh = 1. / size[1]
x = (box[0] + box[1]) / 2.0
y = (box[2] + box[3]) / 2.0
w = box[1] - box[0]
h = box[3] - box[2]
x = x * dw
w = w * dw
y = y * dh
h = h * dh
return (x, y, w, h)
def convert_annotation(image_id):
in_file = open('./label_xml\%s.xml' % (image_id), encoding='UTF-8')
out_file = open('./label_txt\%s.txt' % (image_id), 'w') # 生成txt格式文件
tree = ET.parse(in_file)
root = tree.getroot()
size = root.find('size')
w = int(size.find('width').text)
h = int(size.find('height').text)
for obj in root.iter('object'):
cls = obj.find('name').text
if cls not in classes:
classes.append(cls) # 如果类别不存在,添加到classes列表中
cls_id = classes.index(cls)
xmlbox = obj.find('bndbox')
b = (float(xmlbox.find('xmin').text), float(xmlbox.find('xmax').text), float(xmlbox.find('ymin').text),
float(xmlbox.find('ymax').text))
bb = convert((w, h), b)
out_file.write(str(cls_id) + " " + " ".join([str(a) for a in bb]) + '\n')
xml_path = os.path.join(CURRENT_DIR, './label_xml/')
# xml list
img_xmls = os.listdir(xml_path)
for img_xml in img_xmls:
label_name = img_xml.split('.')[0]
print(label_name)
convert_annotation(label_name)
print("Classes:") # 打印最终的classes列表
print(classes) # 打印最终的classes列表
整理数据文件夹结构
我们需要将数据集整理为以下结构:
-----data
|-----train
| |-----images
| |-----labels
|
|-----valid
| |-----images
| |-----labels
|
|-----test
|-----images
|-----labels
确保以下几点:
所有的训练图片都位于data/train/images目录下,相应的标注文件位于data/train/labels目录下。
所有的验证图片都位于data/valid/images目录下,相应的标注文件位于data/valid/labels目录下。
所有的测试图片都位于data/test/images目录下,相应的标注文件位于data/test/labels目录下。
这样的结构使得数据的管理和模型的训练、验证和测试变得非常方便。
模型训练
Epoch gpu_mem box obj cls labels img_size
1/200 20.8G 0.01576 0.01955 0.007536 22 1280: 100%|██████████| 849/849 [14:42<00:00, 1.04s/it]
Class Images Labels P R mAP@.5 mAP@.5:.95: 100%|██████████| 213/213 [01:14<00:00, 2.87it/s]
all 3395 17314 0.994 0.957 0.0957 0.0843
Epoch gpu_mem box obj cls labels img_size
2/200 20.8G 0.01578 0.01923 0.007006 22 1280: 100%|██████████| 849/849 [14:44<00:00, 1.04s/it]
Class Images Labels P R mAP@.5 mAP@.5:.95: 100%|██████████| 213/213 [01:12<00:00, 2.95it/s]
all 3395 17314 0.996 0.956 0.0957 0.0845
Epoch gpu_mem box obj cls labels img_size
3/200 20.8G 0.01561 0.0191 0.006895 27 1280: 100%|██████████| 849/849 [10:56<00:00, 1.29it/s]
Class Images Labels P R mAP@.5 mAP@.5:.95: 100%|███████ | 187/213 [00:52<00:00, 4.04it/s]
all 3395 17314 0.996 0.957 0.0957 0.0845
5.核心代码讲解
5.1 datasets.py
class DatasetBuilder:
def __init__(self, args):
self.args = args
def build_dataset(self, is_train):
transform = self.build_transform(is_train)
print("Transform = ")
if isinstance(transform, tuple):
for trans in transform:
print(" - - - - - - - - - - ")
for t in trans.transforms:
print(t)
else:
for t in transform.transforms:
print(t)
print("---------------------------")
if self.args.data_set == 'CIFAR':
dataset = datasets.CIFAR100(self.args.data_path, train=is_train, transform=transform, download=True)
nb_classes = 100
elif self.args.data_set == 'IMNET':
print("reading from datapath", self.args.data_path)
root = os.path.join(self.args.data_path, 'train' if is_train else 'val')
if is_train and self.args.multi_scale_sampler:
dataset = MultiScaleImageFolder(root, self.args)
else:
dataset = datasets.ImageFolder(root, transform=transform)
nb_classes = 1000
elif self.args.data_set == "image_folder":
root = self.args.data_path if is_train else self.args.eval_data_path
dataset = datasets.ImageFolder(root, transform=transform)
nb_classes = self.args.nb_classes
assert len(dataset.class_to_idx) == nb_classes
else:
raise NotImplementedError()
print("Number of the class = %d" % nb_classes)
return dataset, nb_classes
def build_transform(self, is_train):
resize_im = self.args.input_size > 32
imagenet_default_mean_and_std = self.args.imagenet_default_mean_and_std
mean = IMAGENET_INCEPTION_MEAN if not imagenet_default_mean_and_std else IMAGENET_DEFAULT_MEAN
std = IMAGENET_INCEPTION_STD if not imagenet_default_mean_and_std else IMAGENET_DEFAULT_STD
if is_train:
# This should always dispatch to transforms_imagenet_train
transform = create_transform(
input_size=self.args.input_size,
is_training=True,
color_jitter=self.args.color_jitter if self.args.color_jitter > 0 else None,
auto_augment=self.args.aa,
interpolation=self.args.train_interpolation,
re_prob=self.args.reprob,
re_mode=self.args.remode,
re_count=self.args.recount,
mean=mean,
std=std,
)
if self.args.three_aug: # --aa should not be "" to use this as it actually overrides the auto-augment
print(f"Using 3-Augments instead of Rand Augment")
cur_augs = transform.transforms
three_aug = transforms.RandomChoice([transforms.Grayscale(num_output_channels=3),
transforms.RandomSolarize(threshold=192.0),
transforms.GaussianBlur(kernel_size=(5, 9))])
final_transforms = cur_augs[0:2] + [three_aug] + cur_augs[2:]
transform = transforms.Compose(final_transforms)
if not resize_im:
transform.transforms[0] = transforms.RandomCrop(
self.args.input_size, padding=4)
return transform
t = []
if resize_im:
# Warping (no cropping) when evaluated at 384 or larger
if self.args.input_size >= 384:
t.append(
transforms.Resize((self.args.input_size, self.args.input_size),
interpolation=transforms.InterpolationMode.BICUBIC),
)
print(f"Warping {self.args.input_size} size input images...")
else:
if self.args.crop_pct is None:
self.args.crop_pct = 224 / 256
size = int(self.args.input_size / self.args.crop_pct)
t.append(
# To maintain same ratio w.r.t. 224 images
transforms.Resize(size, interpolation=transforms.InterpolationMode.BICUBIC),
)
t.append(transforms.CenterCrop(self.args.input_size))
t.append(transforms.ToTensor())
t.append(transforms.Normalize(mean, std))
return transforms.Compose(t)
该程序文件名为datasets.py,主要包含了两个函数:build_dataset和build_transform。
build_dataset函数用于构建数据集。根据参数is_train和args中的配置,选择不同的数据集进行构建。如果args中的data_set为’CIFAR’,则构建CIFAR100数据集;如果为’IMNET’,则根据is_train选择训练集或验证集,并根据args中的multi_scale_sampler参数选择使用MultiScaleImageFolder或ImageFolder进行构建;如果为’image_folder’,则根据is_train选择训练集或验证集,并根据args中的data_path或eval_data_path参数构建ImageFolder数据集。最后返回构建的数据集和类别数。
build_transform函数用于构建数据预处理的transform。根据参数is_train和args中的配置,选择不同的预处理方式。如果is_train为True,则根据args中的配置构建训练集的transform,包括输入大小、颜色增强、自动增强、插值方式、重复增强等。如果is_train为False,则根据args中的配置构建验证集的transform,包括输入大小、缩放、裁剪、转换为张量、归一化等。最后返回构建的transform。
5.2 engine.py
import utils
class Trainer:
def __init__(self, model: torch.nn.Module, criterion: torch.nn.Module,
data_loader: Iterable, optimizer: torch.optim.Optimizer,
device: torch.device, epoch: int, loss_scaler, max_norm: float = 0,
model_ema: Optional[ModelEma] = None, mixup_fn: Optional[Mixup] = None, log_writer=None,
wandb_logger=None, start_steps=None, lr_schedule_values=None, wd_schedule_values=None,
num_training_steps_per_epoch=None, update_freq=None, use_amp=False):
self.model = model
self.criterion = criterion
self.data_loader = data_loader
self.optimizer = optimizer
self.device = device
self.epoch = epoch
self.loss_scaler = loss_scaler
self.max_norm = max_norm
self.model_ema = model_ema
self.mixup_fn = mixup_fn
self.log_writer = log_writer
self.wandb_logger = wandb_logger
self.start_steps = start_steps
self.lr_schedule_values = lr_schedule_values
self.wd_schedule_values = wd_schedule_values
self.num_training_steps_per_epoch = num_training_steps_per_epoch
self.update_freq = update_freq
self.use_amp = use_amp
def train_one_epoch(self):
self.model.train(True)
metric_logger = utils.MetricLogger(delimiter=" ")
metric_logger.add_meter('lr', utils.SmoothedValue(window_size=1, fmt='{value:.6f}'))
metric_logger.add_meter('min_lr', utils.SmoothedValue(window_size=1, fmt='{value:.6f}'))
header = 'Epoch: [{}]'.format(self.epoch)
print_freq = 10
self.optimizer.zero_grad()
for data_iter_step, (samples, targets) in enumerate(metric_logger.log_every(self.data_loader, print_freq, header)):
step = data_iter_step // self.update_freq
if step >= self.num_training_steps_per_epoch:
continue
it = self.start_steps + step # Global training iteration
# Update LR & WD for the first acc
if self.lr_schedule_values is not None or self.wd_schedule_values is not None and data_iter_step % self.update_freq == 0:
for i, param_group in enumerate(self.optimizer.param_groups):
if self.lr_schedule_values is not None:
param_group["lr"] = self.lr_schedule_values[it] * param_group["lr_scale"]
if self.wd_schedule_values is not None and param_group["weight_decay"] > 0:
param_group["weight_decay"] = self.wd_schedule_values[it]
samples = samples.to(self.device, non_blocking=True)
targets = targets.to(self.device, non_blocking=True)
if self.mixup_fn is not None:
samples, targets = self.mixup_fn(samples, targets)
if self.use_amp:
with torch.cuda.amp.autocast():
output = self.model(samples)
loss = self.criterion(output, targets)
else: # Full precision
output = self.model(samples)
loss = self.criterion(output, targets)
loss_value = loss.item()
if not math.isfinite(loss_value): # This could trigger if using AMP
print("Loss is {}, stopping training".format(loss_value))
assert math.isfinite(loss_value)
if self.use_amp:
# This attribute is added by timm on one optimizer (adahessian)
is_second_order = hasattr(self.optimizer, 'is_second_order') and self.optimizer.is_second_order
loss /= self.update_freq
grad_norm = self.loss_scaler(loss, self.optimizer, clip_grad=self.max_norm,
parameters=self.model.parameters(), create_graph=is_second_order,
update_grad=(data_iter_step + 1) % self.update_freq == 0)
if (data_iter_step + 1) % self.update_freq == 0:
self.optimizer.zero_grad()
if self.model_ema is not None:
self.model_ema.update(self.model)
else: # Full precision
loss /= self.update_freq
loss.backward()
if (data_iter_step + 1) % self.update_freq == 0:
self.optimizer.step()
self.optimizer.zero_grad()
if self.model_ema is not None:
self.model_ema.update(self.model)
torch.cuda.synchronize()
if self.mixup_fn is None:
class_acc = (output.max(-1)[-1] == targets).float().mean()
else:
class_acc = None
metric_logger.update(loss=loss_value)
metric_logger.update(class_acc=class_acc)
min_lr = 10.
max_lr = 0.
for group in self.optimizer.param_groups:
min_lr = min(min_lr, group["lr"])
max_lr = max(max_lr, group["lr"])
metric_logger.update(lr=max_lr)
metric_logger.update(min_lr=min_lr)
weight_decay_value = None
for group in self.optimizer.param_groups:
if group["weight_decay"] > 0:
weight_decay_value = group["weight_decay"]
metric_logger.update(weight_decay=weight_decay_value)
if self.use_amp:
metric_logger.update(grad_norm=grad_norm)
if self.log_writer is not None:
self.log_writer.update(loss=loss_value, head="loss")
self.log_writer.update(class_acc=class_acc, head="loss")
self.log_writer.update(lr=max_lr, head="opt")
self.log_writer.update(min_lr=min_lr, head="opt")
self.log_writer.update(weight_decay=weight_decay_value, head="opt")
if self.use_amp:
self.log_writer.update(grad_norm=grad_norm, head="opt")
self.log_writer.set_step()
if self.wandb_logger:
self.wandb_logger._wandb.log({
'Rank-0 Batch Wise/train_loss': loss_value,
'Rank-0 Batch Wise/train_max_lr': max_lr,
'Rank-0 Batch Wise/train_min_lr': min_lr
}, commit=False)
if class_acc:
self.wandb_logger._wandb.log({'Rank-0 Batch Wise/train_class_acc': class_acc}, commit=False)
if self.use_amp:
self.wandb_logger._wandb.log({'Rank-0 Batch Wise/train_grad_norm': grad_norm}, commit=False)
self.wandb_logger._wandb.log({'Rank-0 Batch Wise/global_train_step': it})
# Gather the stats from all processes
metric_logger.synchronize_between_processes()
print("Averaged stats:", metric_logger)
return {k: meter.global_avg for k, meter in metric_logger.meters.items()}
@torch.no_grad()
def evaluate(self):
criterion = torch.nn.CrossEntropyLoss()
metric_logger = utils.MetricLogger(delimiter=" ")
header = 'Test:'
# Switch to evaluation mode
self.model.eval()
for batch in metric_logger.log_every(self.data_loader, 10, header):
images = batch[0]
target = batch[-1]
images = images.to(self.device, non_blocking=True)
target = target.to(self.device, non_blocking=True)
# Compute output
if self.use_amp:
with torch.cuda.amp.autocast():
output = self.model(images)
loss = criterion(output, target)
else:
output = self.model(images)
loss = criterion(output, target)
acc1, acc5 = accuracy(output, target, topk=(1, 5))
batch_size = images.shape[0]
metric_logger.update(loss=loss.item())
metric_logger.meters['acc1'].update(acc1.item(), n=batch_size)
metric_logger.meters['acc5'].update(acc5.item(), n=batch_size)
# Gather the stats from all processes
metric_logger.synchronize_between_processes()
print('* Acc@1 {top1.global_avg:.3f} Acc@5 {top5.global_avg:.3f} loss {losses.global_avg:.3f}'
.format(top1=metric_logger.acc1, top5=metric_logger.acc5, losses=metric_logger.loss))
return {k: meter.global_avg for k, meter in metric_logger.meters.items()}
这个程序文件名为engine.py,主要包含了两个函数:train_one_epoch和evaluate。
train_one_epoch函数用于训练一个epoch的模型。它接受模型、损失函数、数据加载器、优化器、设备、当前epoch等参数。函数内部会迭代数据加载器中的每个batch,对模型进行训练。在训练过程中,会根据需要更新学习率和权重衰减,并计算损失值、准确率等指标。函数还支持使用混合精度训练(AMP)和模型指数移动平均(ModelEma)等功能。
evaluate函数用于评估模型在测试集上的性能。它接受数据加载器、模型、设备等参数。函数内部会迭代数据加载器中的每个batch,对模型进行评估。评估过程中会计算损失值、准确率等指标,并输出评估结果。
这个程序文件还导入了一些其他的模块和函数,如math、torch、timm.data中的Mixup、timm.utils中的accuracy和ModelEma,以及自定义的utils模块。
5.3 main.py
def str2bool(v):
"""
Converts string to bool type; enables command line
arguments in the format of '--arg1 true --arg2 false'
"""
if isinstance(v, bool):
return v
if v.lower() in ('yes', 'true', 't', 'y', '1'):
return True
elif v.lower() in ('no', 'false', 'f', 'n', '0'):
return False
else:
raise argparse.ArgumentTypeError('Boolean value expected.')
def get_args_parser():
parser = argparse.ArgumentParser('ConvNeXt training and evaluation script for image classification', add_help=False)
parser.add_argument('--batch_size', default=256, type=int,
help='Per GPU batch size')
parser.add_argument('--epochs', default=300, type=int)
parser.add_argument('--update_freq', default=2, type=int,
help='gradient accumulation steps')
# Model parameters
parser.add_argument('--model', default='edgenext_small', type=str, metavar='MODEL',
help='Name of model to train')
parser.add_argument('--drop_path', type=float, default=0.1, metavar='PCT',
help='Drop path rate (default: 0.0)')
parser.add_argument('--input_size', default=256, type=int,
help='image input size')
parser.add_argument('--layer_scale_init_value', default=1e-6, type=float,
help="Layer scale initial values")
# EMA related parameters
parser.add_argument('--model_ema', type=str2bool, default=False)
parser.add_argument('--model_ema_decay', type=float, default=0.9995, help='') # TODO: MobileViT is using 0.9995
parser.add_argument('--model_ema_force_cpu', type=str2bool, default=False, help='')
parser.add_argument('--model_ema_eval', type=str2bool, default=False, help='Using ema to eval during training.')
# Optimization parameters
parser.add_argument('--opt', default='adamw', type=str, metavar='OPTIMIZER', help='Optimizer (default: "adamw"')
parser.add_argument('--opt_eps', default=1e-8, type=float, metavar='EPSILON',
help='Optimizer Epsilon (default: 1e-8)')
parser.add_argument('--opt_betas', default=None, type=float, nargs='+', metavar='BETA',
help='Optimizer Betas (default: None, use opt default)')
parser.add_argument('--clip_grad', type=float, default=None, metavar='NORM',
help='Clip gradient norm (default: None, no clipping)')
parser.add_argument('--momentum', type=float, default=0.9, metavar='M',
help='SGD momentum (default: 0.9)')
parser.add_argument('--weight_decay', type=float, default=0.05,
help='weight decay (default: 0.05)')
parser.add_argument('--weight_decay_end', type=float, default=None, help="""Final value of the
weight decay. We use a cosine schedule for WD and using a larger decay by
the end of training improves performance for ViTs.""")
parser.add_argument('--lr', type=float, default=6e-3, metavar='LR',
help='learning rate (default: 6e-3), with total batch size 4096')
parser.add_argument('--layer_decay', type=float, default=1.0)
parser.add_argument('--min_lr', type=float, default=1e-6, metavar='LR',
help='lower lr bound for cyclic schedulers that hit 0 (1e-6)')
parser.add_argument('--warmup_epochs', type=int, default=20, metavar='N',
help='epochs to warmup LR, if scheduler supports')
parser.add_argument('--warmup_steps', type=int, default=-1, metavar='N',
help='num of steps to warmup LR, will overload warmup_epochs if set > 0')
parser.add_argument('--warmup_start_lr', type=float, default=0, metavar='LR',
help='Starting LR for warmup (default 0)')
# Augmentation parameters
parser.add_argument('--color_jitter', type=float, default=0.4, metavar='PCT',
help='Color jitter factor (default: 0.4)')
parser.add_argument('--aa', type=str, default='rand-m9-mstd0.5-inc1', metavar='NAME',
help='Use AutoAugment policy. "v0" or "original". " + "(default: rand-m9-mstd0.5-inc1)'),
parser.add_argument('--smoothing', type=float, default=0.1,
help='Label smoothing (default: 0.1)')
parser.add_argument('--train_interpolation', type=str, default='bicubic',
help='Training interpolation (random, bilinear, bicubic default: "bicubic")')
# Evaluation parameters
parser.add_argument('--crop_pct', type=float, default=None)
# * Random Erase params
parser.add_argument('--reprob', type=float, default=0.0, metavar='PCT',
help='Random erase prob (default: 0.0)')
parser.add_argument('--remode', type=str, default='pixel',
help='Random erase mode (default: "pixel")')
parser.add_argument('--recount', type=int, default=1,
help='Random erase count (
这是一个用于图像分类的ConvNeXt模型的训练和评估脚本。该脚本包含了模型的参数设置、数据加载、模型训练和评估等功能。其中,主要的参数包括批量大小、训练轮数、模型名称、学习率、优化器类型等。脚本还支持分布式训练和使用权重和偏差参数等功能。在训练过程中,还可以选择使用Mixup和CutMix等数据增强方法。最后,脚本还支持使用Weights and Biases进行日志记录和模型检查点保存。
5.4 model.py
封装为类后的代码如下:
class ModelProfiler:
def __init__(self, model_name, input_shape):
self.model_name = model_name
self.input_shape = input_shape
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
self.model = self._create_model()
self.model.to(self.device)
self.model.eval()
def _create_model(self):
return timm.create_model(self.model_name, pretrained=False, features_only=True)
def profile_model(self):
dummy_input = torch.randn(*self.input_shape).to(self.device)
flops, params = profile(self.model.to(self.device), (dummy_input,), verbose=False)
flops, params = clever_format([flops * 2, params], "%.3f")
return flops, params
def print_model_info(self):
print(self.model.feature_info.channels())
dummy_input = torch.randn(*self.input_shape).to(self.device)
for feature in self.model(dummy_input):
print(feature.size())
flops, params = self.profile_model()
print('Total FLOPS: %s' % (flops))
print('Total params: %s' % (params))
if __name__ == "__main__":
profiler = ModelProfiler('edgenext_small', (1, 3, 640, 640))
profiler.print_model_info()
使用示例:
profiler = ModelProfiler('edgenext_small', (1, 3, 640, 640))
profiler.print_model_info()
这个程序文件名为model.py,它的功能是使用torch和timm库来计算一个模型的FLOPS(浮点运算数)和参数数量。
首先,程序列出了timm库中可用的所有模型。然后,它创建了一个虚拟输入张量dummy_input,并将其移动到可用的计算设备(如果有GPU则使用GPU,否则使用CPU)。
接下来,程序使用timm库创建了一个名为’edgenext_small’的模型,该模型不使用预训练权重,并且只返回特征而不进行分类。然后,将模型移动到计算设备并设置为评估模式。
程序打印了模型的特征通道数和每个特征的大小。然后,使用thop库的profile函数计算了模型在给定输入上的FLOPS和参数数量,并使用clever_format函数将其格式化为易读的形式。
最后,程序打印了计算得到的总FLOPS和总参数数量。
5.5 sampler.py
class MultiScaleSamplerDDP(Sampler):
def __init__(self, base_im_w: int, base_im_h: int, base_batch_size: int, n_data_samples: int,
min_crop_size_w: int = 160, max_crop_size_w: int = 320,
min_crop_size_h: int = 160, max_crop_size_h: int = 320,
n_scales: int = 5, is_training: bool = True, distributed=True) -> None:
# min. and max. spatial dimensions
min_im_w, max_im_w = min_crop_size_w, max_crop_size_w
min_im_h, max_im_h = min_crop_size_h, max_crop_size_h
# Get the GPU and node related information
if not distributed:
num_replicas = 1
rank = 0
else:
num_replicas = dist.get_world_size()
rank = dist.get_rank()
# adjust the total samples to avoid batch dropping
num_samples_per_replica = int(math.ceil(n_data_samples * 1.0 / num_replicas))
total_size = num_samples_per_replica * num_replicas
img_indices = [idx for idx in range(n_data_samples)]
img_indices += img_indices[:(total_size - n_data_samples)]
assert len(img_indices) == total_size
self.shuffle = True if is_training else False
if is_training:
self.img_batch_pairs = _image_batch_pairs(base_im_w, base_im_h, base_batch_size, num_replicas, n_scales, 32,
min_im_w, max_im_w, min_im_h, max_im_h)
else:
self.img_batch_pairs = [(base_im_h, base_im_w, base_batch_size)]
self.img_indices = img_indices
self.n_samples_per_replica = num_samples_per_replica
self.epoch = 0
self.rank = rank
self.num_replicas = num_replicas
self.batch_size_gpu0 = base_batch_size
def __iter__(self):
if self.shuffle:
random.seed(self.epoch)
random.shuffle(self.img_indices)
random.shuffle(self.img_batch_pairs)
indices_rank_i = self.img_indices[self.rank:len(self.img_indices):self.num_replicas]
else:
indices_rank_i = self.img_indices[self.rank:len(self.img_indices):self.num_replicas]
start_index = 0
while start_index < self.n_samples_per_replica:
curr_h, curr_w, curr_bsz = random.choice(self.img_batch_pairs)
end_index = min(start_index + curr_bsz, self.n_samples_per_replica)
batch_ids = indices_rank_i[start_index:end_index]
n_batch_samples = len(batch_ids)
if n_batch_samples != curr_bsz:
batch_ids += indices_rank_i[:(curr_bsz - n_batch_samples)]
start_index += curr_bsz
if len(batch_ids) > 0:
batch = [(curr_h, curr_w, b_id) for b_id in batch_ids]
yield batch
def set_epoch(self, epoch: int) -> None:
self.epoch = epoch
def __len__(self):
return self.n_samples_per_replica
def _image_batch_pairs(crop_size_w: int,
crop_size_h: int,
batch_size_gpu0: int,
n_gpus: int,
max_scales: Optional[float] = 5,
check_scale_div_factor: Optional[int] = 32,
min_crop_size_w: Optional[int] = 160,
max_crop_size_w: Optional[int] = 320,
min_crop_size_h: Optional[int] = 160,
max_crop_size_h: Optional[int] = 320,
*args, **kwargs) -> list:
"""
This function creates batch and image size pairs. For a given batch size and image size, different image sizes
are generated and batch size is adjusted so that GPU memory can be utilized efficiently.
:param crop_size_w: Base Image width (e.g., 224)
:param crop_size_h: Base Image height (e.g., 224)
:param batch_size_gpu0: Batch size on GPU 0 for base image
:param n_gpus: Number of available GPUs
:param max_scales: Number of scales. How many image sizes that we want to generate between min and max scale factors.
:param check_scale_div_factor: Check if image scales are divisible by this factor.
:param min_crop_size_w: Min. crop size along width
:param max_crop_size_w: Max. crop size along width
:param min_crop_size_h: Min. crop size along height
:param max_crop_size_h: Max. crop size along height
:param args:
:param kwargs:
:return: a sorted list of tuples. Each index is of the form (h, w, batch_size)
"""
width_dims = list(np.linspace(min_crop_size_w, max_crop_size_w, max_scales))
if crop_size_w not in width_dims:
width_dims.append(crop_size_w)
height_dims = list(np.linspace(min_crop_size_h, max_crop_size_h, max_scales))
if crop_size_h not in height_dims:
height_dims.append(crop_size_h)
image_scales = set()
for h, w in zip(height_dims, width_dims):
# ensure that sampled sizes are divisible by check_scale_div_factor
# This is important in some cases where input undergoes a fixed number of down-sampling stages
# for instance, in ImageNet training, CNNs usually have 5 downsampling stages, which downsamples the
# input image of resolution 224x224 to 7x7 size
h = make_divisible(h, check_scale_div_factor)
w = make_divisible(w, check_scale_div_factor)
image_scales.add((h, w))
image_scales = list(image_scales)
img_batch_tuples = set()
n_elements = crop_size_w * crop_size_h * batch_size_gpu0
for (crop_h, crop_y) in image_scales:
# compute the batch size for sampled image resolutions with respect to the base resolution
_bsz = max(batch_size_gpu0, int(round(n_elements/(crop_h * crop_y), 2)))
_bsz = make_divisible(_bsz, n_gpus)
_bsz = _bsz if _bsz % 2 == 0 else _bsz - 1 # Batch size must be even
img_batch_tuples.add((crop_h, crop_y, _bsz))
img_batch_tuples = list(img_batch_tuples)
return sorted(img_batch_tuples)
def make_divisible(v: Union[float, int],
divisor: Optional[int] = 8,
min_value: Optional[Union[float, int]] = None) -> Union[float, int]:
"""
This function is taken from the original tf repo.
It ensures that all layers have a channel number that is divisible by 8
It can be seen here:
https://github.com/tensorflow/models/blob/master/research/slim/nets/mobilenet/mobilenet.py
:param v:
:param divisor:
:param min_value:
:return:
"""
if min_value is None:
min_value = divisor
new_v = max(min_value, int(v + divisor / 2) // divisor * divisor)
# Make sure that round down does not go down by more than 10%.
if new_v < 0.9 * v:
new_v += divisor
return new_v
class MultiScaleImageFolder(ImageFolder):
def __init__(self, root, args) -> None:
self.args = args
ImageFolder.__init__(self, root=root, transform=None, target_transform=None, is_valid_file=None)
def get_transforms(self, size: int):
imagenet_default_mean_and_std = self.args.imagenet_default_mean_and_std
mean = IMAGENET_INCEPTION_MEAN if not imagenet_default_mean_and_std else IMAGENET_DEFAULT_MEAN
std = IMAGENET_INCEPTION_STD if not imagenet_default_mean_and_std else IMAGENET_DEFAULT_STD
resize_im = size > 32
transform = create_transform(
input_size=size,
is_training=True,
color_jitter=self.args.color_jitter,
auto_augment=self.args.aa,
interpolation=self.args.train_interpolation,
re_prob=self.args.reprob,
re_mode=self.args.remode,
re_count
#### 5.5 test.py
```python
import cv2
import numpy as np
class ImageShifter:
def __init__(self, image_path):
self.img = cv2.imread(image_path)
self.shifts = [(5, 0), (-5, 0), (0, 5), (0, -5)]
self.num_shifts = 4
self.num_iterations = 10
def shift_image(self):
for i in range(self.num_iterations):
dx, dy = self.shifts[np.random.randint(self.num_shifts)]
M = np.float32([[1, 0, dx], [0, 1, dy]])
shifted = cv2.warpAffine(self.img, M, (self.img.shape[1], self.img.shape[0]))
cv2.imshow('Shifted', shifted)
cv2.waitKey(50)
cv2.destroyAllWindows()
这个程序是一个使用OpenCV库进行图片平移操作的程序。程序首先读取一张名为"save.png"的图片。然后定义了四个平移方向,分别是向右、向左、向下和向上。接着设置了进行平移操作的次数为10次。
程序使用循环进行平移操作,每次循环中随机选择一个平移方向,并根据选择的方向定义平移矩阵。然后使用cv2.warpAffine函数进行平移操作,将平移后的图片保存在变量shifted中。最后使用cv2.imshow函数显示平移后的图片,并使用cv2.waitKey函数等待50毫秒。
循环结束后,使用cv2.destroyAllWindows函数关闭所有打开的窗口,程序执行完毕。
6.系统整体结构
EdgeNeXt改进YOLOv5的黄豆种子计数培养仿真系统是一个基于骨干网络EdgeNeXt的目标检测系统,用于在黄豆种子计数培养中进行仿真。该系统的整体构架包括数据集处理、模型定义、训练和评估等模块。
下面是每个文件的功能的整理:
文件名 | 功能 |
---|---|
datasets.py | 构建数据集,包括训练集和验证集的加载和预处理 |
engine.py | 训练和评估的引擎,包括训练和评估的函数 |
main.py | 主程序,用于配置参数、加载数据集、构建模型、进行训练和评估 |
model.py | 定义模型,包括骨干网络EdgeNeXt和YOLOv5的改进版本 |
optim_factory.py | 创建优化器的工厂函数 |
sampler.py | 数据采样器,用于对数据进行采样 |
test.py | 测试程序,用于对模型进行测试 |
train.py | 训练程序,用于对模型进行训练 |
ui.py | 用户界面,用于交互式操作 |
utils.py | 一些常用的工具函数 |
yolo.py | YOLOv5的相关函数和类 |
models/common.py | 通用模型定义 |
models/conv_encoder.py | 卷积编码器模型定义 |
models/edgenext.py | EdgeNeXt模型定义 |
models/edgenext_bn_hs.py | 带有批归一化和激活函数的EdgeNeXt模型定义 |
models/experimental.py | 实验性模型定义 |
models/layers.py | 模型的各种层定义 |
models/model.py | 模型定义 |
models/sdta_encoder.py | SDTA编码器模型定义 |
models/tf.py | TensorFlow相关函数和类 |
models/yolo.py | YOLO模型定义 |
models/init.py | 模型初始化文件 |
utils/activations.py | 激活函数定义 |
utils/augmentations.py | 数据增强函数定义 |
utils/autoanchor.py | 自动锚框定义 |
utils/autobatch.py | 自动批处理定义 |
utils/callbacks.py | 回调函数定义 |
utils/datasets.py | 数据集处理函数定义 |
utils/downloads.py | 下载函数定义 |
utils/general.py | 通用函数定义 |
utils/loss.py | 损失函数定义 |
utils/metrics.py | 评估指标定义 |
utils/plots.py | 绘图函数定义 |
utils/torch_utils.py | PyTorch工具函数定义 |
utils/init.py | 工具函数初始化文件 |
utils/aws/resume.py | AWS恢复函数定义 |
utils/aws/init.py | AWS初始化文件 |
utils/flask_rest_api/example_request.py | Flask REST API示例请求定义 |
utils/flask_rest_api/restapi.py | Flask REST API定义 |
utils/loggers/init.py | 日志记录器初始化文件 |
utils/loggers/wandb/log_dataset.py | WandB日志记录器定义 |
utils/loggers/wandb/sweep.py | WandB超参数搜索定义 |
utils/loggers/wandb/wandb_utils.py | WandB工具函数定义 |
utils/loggers/wandb/init.py | WandB初始化文件 |
7.EdgeNeXt简介
这项工作的主要目标是开发一种轻量级的混合设计,有效地融合了低功耗边缘设备的ViTs和cnn的优点。ViTs(如MobileViT)的计算开销主要是由于自注意操作。与MobileViT相比,本文模型中的注意力块相对于输入空间维度O(N d2)具有线性复杂度,其中N是补丁的数量,d是特征/通道维度。模型中的自注意操作应用于跨通道维度,而不是空间维度。此外,我们证明,在注意力块数量更少的情况下(在本文中是3个,而在MobileViT中是9个),我们可以超过他们的性能标志。通过这种方式,所提出的框架可以用有限数量的MAdds建模全局表示,这是确保边缘设备上低延迟推理的基本标准。为了激发我们提出的体系结构,我们提出了两个理想的特性。
a)有效编码全局信息。
自我注意学习全局表征的内在特征对视觉任务至关重要。为了有效地继承这一优势,我们使用交叉协方差注意在相对较少的网络块内跨特征通道维度而不是空间维度合并注意操作。该方法将原有的自注意操作在令牌数量上的复杂度从二次型降低为线性型,并对全局信息进行了有效的隐式编码。
b)自适应内核大小。
众所周知,大核卷积的计算成本很高,因为参数和flop的数量会随着核大小的增长而成倍增加。尽管较大的内核大小有助于增加接受域,但在整个网络层次结构中使用如此大的内核代价高昂,而且不是最优的。我们提出了一种自适应内核大小机制来降低这种复杂性,并捕获网络中不同级别的特征。受cnn的层次结构的启发,在卷积编码器块的早期阶段使用较小的内核,而在后期阶段使用较大的内核。这种设计选择是最优的,因为CNN的早期阶段通常捕获低级别的特征,较小的内核适合这个目的。然而,在网络的后期阶段,需要大的卷积内核来捕获高级特征。接下来我们将解释架构细节。
总体的结构
下图说明了所提议的EdgeNeXt架构的概述。主要成分有两方面:
(1)自适应N×N convc .编码器,
(2)分割深度转置注意(SDTA)编码器。
本文的EdgeNeXt架构建立在ConvNeXt的设计原则之上,并在四个阶段中以四个不同的规模提取分层特征。大小为H×W ×3的输入图像在网络的开始通过一个patchify stem层,使用一个4×4非重叠卷积和一个层范数来实现,结果是h/4 × w/4 ×C1特征映射。然后,输出被传递给3×3 convc . encoder来提取局部特征。第二阶段从使用2×2跨步卷积实现的下采样层开始,该层将空间大小减少了一半并增加了通道,从而得到H /8 × W /8 ×C2特征映射,然后是两个连续的5×5 convs编码器。位置编码(PE)也只在第二阶段的SDTA块之前添加。观察到PE对于密集的预测任务(例如,对象检测和分割)是敏感的,并且在所有阶段添加它会增加网络的延迟。因此,只在网络中添加一次,以编码空间位置信息。将输出的特征图进一步传递到第三和第四阶段,分别生成H /16× W /16 ×C3和H /32× W /32 ×C4维度特征。
第一行:我们框架的整体架构是分阶段设计的。
在这里,第一阶段使用4 × 4跨步卷积将输入图像降采样到1/4分辨率,然后使用三个3×3卷积(Conv.)编码器。
在阶段2-4中,开始时使用2×2跨步卷积进行下采样,随后使用N×N卷积和分割深度转置注意(SDTA)编码器。
下面一行:我们展示了convs编码器(左)和SDTA编码器(右)的设计。convl .编码器使用N×N深度卷积进行空间混合,然后使用两个点卷积进行信道混合。SDTA编码器将输入张量分成B通道组,并应用3 × 3深度卷积进行多尺度空间混合。分支之间的跳跃连接增加了网络的整体接受域。分支B3和B4在第3和第4阶段逐渐被激活,增加了网络更深层次的整体接受域。在提出的SDTA中,我们利用转置注意力,然后使用轻量级MLP,将注意力应用于特征通道,并与输入图像具有线性复杂性。
模型构造了如上图的架构,主要包括 Conv Encoder 和 SDTA Encoder,其中 Conv Encoder 是 ConvNeXt 中的 block 机制,如下图所示,DWConv 代表 Depth-wise Conv。
SDTA Encoder 的基本结构,其中左侧是一个级联的卷积结构,将特征按通道分为 s 组,按图中的方式依次进行级联, 3x3 代表的是 depth-wise 卷积,中间为通道自注意力机制,最右侧为一个 FFN 结构。本模型相当于在 ConvNeXt 的基础上在每个阶段的最后,添加了一个轻量级的注意力机制。
Convolution Encoder .
该块由具有自适应核大小的深度可分离卷积组成。可以通过两个单独的层来定义它:
(1)使用自适应N×N内核的深度卷积。分别用k = 3、5、7和9来表示阶段1、2、3和4。
(2)使用两个点卷积层与标准层归一化(LN)和高斯误差线性单元(GELU)激活来丰富局部表示,用于非线性特征映射。
最后,添加一个跳过连接,使信息在网络层次结构中流动。这个块类似于ConvNeXt块,但是内核大小是动态的,并且根据阶段而变化。我们观察到,与静态内核大小相比,convl .编码器中的自适应内核大小表现得更好。convl .编码器可以表示如下:
其中xi表示形状为H×W ×C的输入特征图,LinearG是一个点级卷积层,GELU是一个点级卷积层,Dw是k×k深度级卷积层,LN是一个归一化层,xi+1表示convl .编码器的输出特征图。
SDTA Encoder .
AAAI在所提出的分割深度转置注意编码器中有两个主要组件。第一个组件通过编码输入图像中的各种空间级别来努力学习自适应多尺度特征表示,第二部分隐式编码全局图像表示。编码器的第一部分受到Res2Net的启发,其中我们采用了多尺度处理方法,将层次表示开发到单个块中。这使得输出特征表示的空间感受场更加灵活和自适应。与Res2Net不同,我们的SDTA编码器中的第一个块不使用1×1点卷积层,以确保具有有限数量的参数和MAdds的轻量级网络。此外,使用自适应数量的子集每个阶段允许有效和灵活的特征编码。在我们的STDA编码器中,我们将输入张量H×W ×C分成s个子集,每个子集用xi表示,并且具有与C/s通道相同的空间大小,其中i∈{1,2,…, s}, C为通道数。每个特征映射子集(除了第一个子集)被传递给3×3深度卷积,用di表示,输出用yi表示。同样,di−1的输出,用yi−1表示,被添加到特征子集xi,然后馈送给di。子集的数量s是基于阶段数t自适应的,其中t∈{2,3,4}。我们可以这样写yi:
每个深度操作di,如图2中的SDTA编码器所示,接收之前所有分割{xj, j≤i}的特征映射输出。
如前所述,变压器自注意层的开销对于边缘设备上的视觉任务是不可用的,因为它以更高的MAdds和延迟为代价。为了缓解这个问题并有效地编码全局上下文,我们在SDTA编码器中使用了转置查询和关键注意特征映射。通过跨通道维度而不是空间维度应用MSA的点积操作,该操作具有线性复杂性,这允许计算跨通道的交叉协方差,以生成具有关于全局表示的隐式知识的注意力特征图。给定一个形状为H×W ×C的归一化张量Y,我们使用以下方法计算查询(Q)、键(K)和值(V)投影三个线性层,得到Q=W QY, K=W KY, V =W V Y,维度HW ×C,其中W Q,W K和W V分别是Q, K和V的投影权值。然后,对Q和K采用L2范数,计算训练稳定时的交叉方差注意。
8.EdgeNeXt改进YOLOv5
首先,我们需要构建一个基于EdgeNeXt的骨干网络。由于EdgeNeXt是一个通用的骨干网络,我们可以直接使用其预训练模型作为我们的骨干网络。这样可以避免从头开始训练一个复杂的网络,节省时间和计算资源。
然后,我们需要对骨干网络进行微调,以适应黄豆种子计数任务。具体来说,我们可以将骨干网络的最后一层替换为一个全连接层,用于输出黄豆种子的数量。这个全连接层可以根据实际需求设计,例如可以有10个神经元,每个神经元对应一个可能的黄豆种子数量。此外,我们还需要在骨干网络的最后添加一个分类器,用于区分不同的黄豆种子。这个分类器可以使用Softmax激活函数,将输出转化为概率分布。
其次,我们需要对YOLOv5的检测头进行改进。由于YOLOv5的检测头是基于骨干网络的特征图设计的,因此我们可以通过修改检测头来改变模型的性能。具体来说,我们可以将检测头中的卷积层替换为EdgeNeXt中的Transformer模块,以提高模型的表达能力。Transformer模块具有更强的自注意力机制,可以更好地捕捉特征之间的关联性。此外,我们还可以通过调整检测头的参数,如卷积核的大小、步长等,来优化模型的性能。这些参数可以根据实际需求进行调整和实验验证。
最后,我们需要对YOLOv5的损失函数进行改进。由于黄豆种子计数任务是一个多分类任务,因此我们可以选择交叉熵损失函数作为我们的损失函数。交叉熵损失函数可以衡量预测结果与真实标签之间的差异,并通过梯度下降算法来优化模型参数。此外,我们还可以通过添加一些正则化项,如L1正则化、L2正则化等,来防止模型过拟合。这些正则化项可以在损失函数中加入惩罚项,限制模型参数的大小和范围,提高模型的泛化能力。
# YOLOv5 🚀 by Ultralytics, GPL-3.0 license
# Parameters
nc: 1 # number of classes
depth_multiple: 0.33 # model depth multiple
width_multiple: 0.25 # layer channel multiple
anchors:
- [10,13, 16,30, 33,23] # P3/8
- [30,61, 62,45, 59,119] # P4/16
- [116,90, 156,198, 373,326] # P5/32
# 0-P1/2
# 1-P2/4
# 2-P3/8
# 3-P4/16
# 4-P5/32
# YOLOv5 v6.0 backbone
backbone:
# [from, number, module, args]
[[-1, 1, edgenext_small, [False]], # 4 vovnet39a
[-1, 1, SPPF, [1024, 5]], # 5
]
# YOLOv5 v6.0 head
head:
[[-1, 1, Conv, [512, 1, 1]], # 6
[-1, 1, nn.Upsample, [None, 2, 'nearest']], # 7
[[-1, 3], 1, Concat, [1]], # cat backbone P4 8
[-1, 3, C3, [512, False]], # 9
[-1, 1, Conv, [256, 1, 1]], # 10
[-1, 1, nn.Upsample, [None, 2, 'nearest']], # 11
[[-1, 2], 1, Concat, [1]], # cat backbone P3 12
[-1, 3, C3, [256, False]], # 13 (P3/8-small)
[-1, 1, Conv, [256, 3, 2]], # 14
[[-1, 10], 1, Concat, [1]], # cat head P4 15
[-1, 3, C3, [512, False]], # 16 (P4/16-medium)
[-1, 1, Conv, [512, 3, 2]], # 17
[[-1, 5], 1, Concat, [1]], # cat head P5 18
[-1, 3, C3, [1024, False]], # 19 (P5/32-large)
[[13, 16, 19], 1, Detect, [nc, anchors]], # Detect(P3, P4, P5)
]
通过以上三步改进,我们可以构建一个基于EdgeNeXt的黄豆种子计数模型。这个模型在骨干网络、检测头和损失函数方面都进行了优化,可以提高模型的准确性和鲁棒性。同时,我们还可以根据实际需求进行进一步的调整和改进,以满足特定的应用场景和性能要求。
9.训练结果分析
评价指标
epoch: 训练的迭代次数。
train/box_loss, train/obj_loss, train/cls_loss: 训练过程中的盒子损失、目标损失和类别损失。
metrics/precision, metrics/recall, metrics/mAP_0.5, metrics/mAP_0.5:0.95: 模型性能的关键指标,包括精确度、召回率、平均精度(mAP)。
val/box_loss, val/obj_loss, val/cls_loss: 验证集上的损失值。
x/lr0, x/lr1, x/lr2: 学习率的变化。
数据可视化分析
import matplotlib.pyplot as plt
# Setting up the plotting parameters
plt.figure(figsize=(15, 10))
# Plotting training losses
plt.subplot(2, 1, 1)
plt.plot(data['epoch'], data['train/box_loss'], label='Train Box Loss')
plt.plot(data['epoch'], data['train/obj_loss'], label='Train Object Loss')
plt.plot(data['epoch'], data['train/cls_loss'], label='Train Class Loss')
plt.title('Training Losses over Epochs')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.legend()
plt.grid(True)
# Plotting validation losses
plt.subplot(2, 1, 2)
plt.plot(data['epoch'], data['val/box_loss'], label='Validation Box Loss')
plt.plot(data['epoch'], data['val/obj_loss'], label='Validation Object Loss')
plt.plot(data['epoch'], data['val/cls_loss'], label='Validation Class Loss')
plt.title('Validation Losses over Epochs')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.legend()
plt.grid(True)
# Showing the plots
plt.tight_layout()
plt.show()
边界框损失()和目标损失()在初始epoch(epoch 0)时都比较高,分别为0.11674和0.46806。 随着训练的进行,这两个损失逐渐降低,表明模型逐渐学会更好地预测边界框和目标。train/box_losstrain/obj_loss
分类损失()在初始epoch时为0,然后逐渐上升。 这可能是因为模型在初始阶段的分类任务上表现良好,但随着训练的进行,可能开始出现一些误分类。train/cls_loss
现在我们看到了训练和验证损失随着epoch变化的图表。下一步,我将绘制精确度、召回率和平均精度(mAP)随着epoch变化的图表。这将帮助我们更好地理解模型性能随训练过程的进展如何变化。
import matplotlib.pyplot as plt
import seaborn as sns
# Setting the aesthetic style of the plots
sns.set(style="whitegrid")
# Creating a figure and a set of subplots
fig, axes = plt.subplots(2, 2, figsize=(15, 10))
# Plotting precision, recall, mAP_0.5, and mAP_0.5:0.95 against epochs
sns.lineplot(ax=axes[0, 0], x=df['epoch'], y=df['metrics/precision'], color="tab:blue")
axes[0, 0].set_title('Precision over Epochs')
axes[0, 0].set_xlabel('Epoch')
axes[0, 0].set_ylabel('Precision')
sns.lineplot(ax=axes[0, 1], x=df['epoch'], y=df['metrics/recall'], color="tab:orange")
axes[0, 1].set_title('Recall over Epochs')
axes[0, 1].set_xlabel('Epoch')
axes[0, 1].set_ylabel('Recall')
sns.lineplot(ax=axes[1, 0], x=df['epoch'], y=df['metrics/mAP_0.5'], color="tab:green")
axes[1, 0].set_title('mAP at IoU=0.5 over Epochs')
axes[1, 0].set_xlabel('Epoch')
axes[1, 0].set_ylabel('mAP at IoU=0.5')
sns.lineplot(ax=axes[1, 1], x=df['epoch'], y=df['metrics/mAP_0.5:0.95'], color="tab:red")
axes[1, 1].set_title('mAP at IoU=0.5:0.95 over Epochs')
axes[1, 1].set_xlabel('Epoch')
axes[1, 1].set_ylabel('mAP at IoU=0.5:0.95')
plt.tight_layout()
plt.show()
精确度()和召回率()随着训练轮次逐渐提高,表明模型在目标检测任务中变得更准确和更全面。metrics/precisionmetrics/recall
平均精度()和平均精度()在初始epoch时都为0,然后逐渐上升。 这表示模型在不同IoU阈值下的性能逐渐提高,更好地定位和分类目标。metrics/mAP_0.5metrics/mAP_0.5:0.95
学习率(,,)在训练过程中也在逐渐减小,这是一个常见的训练策略,以确保模型在训练后期更加稳定。x/lr0x/lr1x/lr2
总体来说,这些数据表明模型经过一定轮次的训练后,性能逐渐提高,损失逐渐减小,精确度和召回率都有所改善。 然而,为了更全面地评估模型性能,还需要考虑验证集上的表现,以及在测试集上的性能评估。 此外,可以进一步分析模型在不同类别上的性能,以确定是否存在类别不平衡或困难样本的问题。 最终的模型性能取决于训练数据的质量和数量,以及超参数的选择等因素。
其他结果分析
混淆矩阵 :
混淆矩阵用于评估分类算法的性能。该矩阵将预测的分类与实际标签进行比较。
在混淆矩阵中,有两类:“黄豆”和“背景”。
该矩阵显示,该模型对“黄豆”的真阳性率很高,值为 0.95,表明它在大多数情况下都能正确识别该类。
“background”类的误报 (FP) 率为 1,这意味着每个被预测为“background”的实例实际上都是“huangdou”。此数据集中没有真正的否定。
“黄豆”的假阴性率(FN)为0.05,相当低,表明该模型很少漏掉“黄豆”类。
矩阵从左上角到右下角的对角线表示正确的预测,而其他单元格表示不正确的预测。
F1 分数曲线:
F1 分数是精确度和召回率的谐波平均值,在两者之间提供平衡,其中一个可能比另一个更重要。
显示的 F1 曲线表明模型在各种置信度阈值上保持较高的 F1 分数。
曲线的峰值显示 F1 分数最大化的置信阈值。这个值似乎在 0.734 左右,F1 分数为 0.97。
此信息可用于设置平衡误报和漏报的分类阈值。
标签分布和边界框大小分布 (file-R4eMSvueDLITPwpgqlfmP9dV):
图像的第一部分显示“huangdou”是唯一具有实例的类,这与混淆矩阵结果一致。
带有散点图的图像的第二部分显示了边界框的相对位置分布 (x, y) 和大小分布(宽度、高度)。
大多数边界框的尺寸似乎很小,这可能表明“黄豆”种子通常很小,或者图像在远处包含“黄豆”种子。
精度曲线 :
精度衡量模型做出的积极预测的准确性。
精度曲线指示精度如何随不同置信度阈值而变化。
该图显示了整个置信水平的高精度,峰值约为 0.91 置信度。
精确召回率 (PR) 曲线 :
PR 曲线是一个图,用于显示不同阈值的精确率和召回率之间的权衡。
此曲线下的高区域表示高召回率和高精度,其中高精度与低误报率相关,高召回率与低假阴性率相关。
“黄豆”和“所有类”的 PR 曲线显示平均精度 (mAP) 的阈值为 0.5,即 0.964。这表示给定类的模型性能非常出色。
召回率曲线 (file-KWo0HVTqeCcphwaBQaXlEqIn):
召回率(或灵敏度)。
召回率曲线显示,在“黄豆”的所有置信水平上,召回率都很高,这表明几乎所有的“黄豆”种子都被检测到。
10.系统整合
参考博客《基于骨干网络EdgeNeXt改进YOLOv5的黄豆种子计数培养仿真系统》
11.参考文献
[1]王文杰,贡亮,汪韬,等.基于多源图像融合的自然环境下番茄果实识别[J].农业机械学报.2021,(9).DOI:10.6041/j.issn.1000-1298.2021.09.018 .
[2]李天华,孙萌,丁小明,等.基于YOLOv4+HSV的成熟期番茄识别方法[J].农业工程学报.2021,37(21).DOI:10.11975/j.issn.1002-6819.2021.21.021 .
[3]刘芳,刘玉坤,林森,等.基于改进型YOLO的复杂环境下番茄果实快速识别方法[J].农业机械学报.2020,(6).DOI:10.6041/j.issn.1000-1298.2020.06.024 .
[4]穆龙涛,高宗斌,崔永杰,等.基于改进AlexNet的广域复杂环境下遮挡猕猴桃目标识别[J].农业机械学报.2019,(10).DOI:10.6041/j.issn.1000-1298.2019.10.003 .
[5]周云成,许童羽,郑伟,等.基于深度卷积神经网络的番茄主要器官分类识别方法[J].农业工程学报.2017,(15).DOI:10.11975/j.issn.1002-6819.2017.15.028 .
[6]梁喜凤,章艳.串番茄采摘点的识别方法[J].中国农机化学报.2016,(11).DOI:10.13733/j.jcam.issn.2095-5553.2016.11.029 .
[7]霍建勇.中国番茄产业现状及安全防范[J].蔬菜.2016,(6).
[8]马翠花,张学平,李育涛,等.基于显著性检测与改进 Hough 变换方法识别未成熟番茄[J].农业工程学报.2016,(14).DOI:10.11975/j.issn.1002-6819.2016.14.029 .
[9]孙龙清,孙希蓓,吴雨寒,等.基于DRN-Faster R-CNN的复杂背景多目标鱼体检测模型[J].农业机械学报.2021,(0S1).245-251,315.
[10]冯青春,程伟,杨庆华,等.基于线结构光视觉的番茄重叠果实识别定位方法研究[J].中国农业大学学报.2015,(4).DOI:10.11841/j.issn.1007-4333.2015.04.13 .