1.研究背景与意义
项目参考AAAI Association for the Advancement of Artificial Intelligence
研究背景与意义:
随着计算机视觉和深度学习的快速发展,目标检测成为了计算机视觉领域的一个重要研究方向。目标检测的目标是在图像或视频中准确地识别和定位出物体的位置。在过去的几年中,YOLO(You Only Look Once)系列算法以其高效的实时目标检测能力而备受关注。YOLOv8是YOLO系列算法的最新版本,它在准确性和速度方面都有了显著的提升。
然而,尽管YOLOv8在目标检测任务中表现出色,但仍然存在一些潜在的问题和局限性。首先,YOLOv8在处理小目标时存在一定的困难,容易出现漏检和误检的情况。其次,YOLOv8在复杂背景下的目标检测效果也不够理想,容易受到背景干扰的影响。此外,YOLOv8对于目标形状的变化和姿态的变化也不够鲁棒,容易出现定位不准确的情况。
因此,改进YOLOv8的研究具有重要的理论和实际意义。首先,改进YOLOv8可以提高目标检测的准确性和鲁棒性,使其更适用于各种复杂场景和应用场景。其次,改进YOLOv8可以进一步提高目标检测的实时性能,使其能够更好地应用于实时视频监控、自动驾驶等领域。此外,改进YOLOv8还可以为其他相关研究提供有益的借鉴和参考,推动目标检测算法的发展。
为了改进YOLOv8,可以从多个方面进行研究。首先,可以探索更加有效的特征提取方法,以提高目标检测的准确性和鲁棒性。例如,可以引入更深的卷积神经网络结构或者使用注意力机制来提取更具有判别性的特征。其次,可以研究目标检测中的数据增强方法,以增加模型对于小目标、复杂背景和姿态变化的适应能力。此外,还可以探索目标检测中的多尺度处理方法,以提高模型对于不同尺度目标的检测能力。
总之,改进YOLOv8的研究具有重要的学术和实际意义。通过改进YOLOv8,可以提高目标检测的准确性、鲁棒性和实时性能,推动计算机视觉领域的发展。同时,改进YOLOv8还可以为其他相关研究提供有益的借鉴和参考,促进目标检测算法的进一步研究和应用。因此,改进YOLOv8的研究是一个具有挑战性和前景广阔的课题,值得深入探索和研究。
2.图片演示
3.视频演示
群山科技工作室的个人空间-群山科技工作室个人主页-哔哩哔哩视频 (bilibili.com)
4.数据集的采集&标注和整理
图片的收集
首先,我们需要收集所需的图片。这可以通过不同的方式来实现,例如使用现有的公开数据集PIGDatasets。
eiseg是一个图形化的图像注释工具,支持COCO和YOLO格式。以下是使用eiseg将图片标注为COCO格式的步骤:
(1)下载并安装eiseg。
(2)打开eiseg并选择“Open Dir”来选择你的图片目录。
(3)为你的目标对象设置标签名称。
(4)在图片上绘制矩形框,选择对应的标签。
(5)保存标注信息,这将在图片目录下生成一个与图片同名的JSON文件。
(6)重复此过程,直到所有的图片都标注完毕。
由于YOLO使用的是txt格式的标注,我们需要将VOC格式转换为YOLO格式。可以使用各种转换工具或脚本来实现。
下面是一个简单的方法是使用Python脚本,该脚本读取XML文件,然后将其转换为YOLO所需的txt格式。
import contextlib
import json
import cv2
import pandas as pd
from PIL import Image
from collections import defaultdict
from utils import *
# Convert INFOLKS JSON file into YOLO-format labels ----------------------------
def convert_infolks_json(name, files, img_path):
# Create folders
path = make_dirs()
# Import json
data = []
for file in glob.glob(files):
with open(file) as f:
jdata = json.load(f)
jdata['json_file'] = file
data.append(jdata)
# Write images and shapes
name = path + os.sep + name
file_id, file_name, wh, cat = [], [], [], []
for x in tqdm(data, desc='Files and Shapes'):
f = glob.glob(img_path + Path(x['json_file']).stem + '.*')[0]
file_name.append(f)
wh.append(exif_size(Image.open(f))) # (width, height)
cat.extend(a['classTitle'].lower() for a in x['output']['objects']) # categories
# filename
with open(name + '.txt', 'a') as file:
file.write('%s\n' % f)
# Write *.names file
names = sorted(np.unique(cat))
# names.pop(names.index('Missing product')) # remove
with open(name + '.names', 'a') as file:
[file.write('%s\n' % a) for a in names]
# Write labels file
for i, x in enumerate(tqdm(data, desc='Annotations')):
label_name = Path(file_name[i]).stem + '.txt'
with open(path + '/labels/' + label_name, 'a') as file:
for a in x['output']['objects']:
# if a['classTitle'] == 'Missing product':
# continue # skip
category_id = names.index(a['classTitle'].lower())
# The INFOLKS bounding box format is [x-min, y-min, x-max, y-max]
box = np.array(a['points']['exterior'], dtype=np.float32).ravel()
box[[0, 2]] /= wh[i][0] # normalize x by width
box[[1, 3]] /= wh[i][1] # normalize y by height
box = [box[[0, 2]].mean(), box[[1, 3]].mean(), box[2] - box[0], box[3] - box[1]] # xywh
if (box[2] > 0.) and (box[3] > 0.): # if w > 0 and h > 0
file.write('%g %.6f %.6f %.6f %.6f\n' % (category_id, *box))
# Split data into train, test, and validate files
split_files(name, file_name)
write_data_data(name + '.data', nc=len(names))
print(f'Done. Output saved to {os.getcwd() + os.sep + path}')
# Convert vott JSON file into YOLO-format labels -------------------------------
def convert_vott_json(name, files, img_path):
# Create folders
path = make_dirs()
name = path + os.sep + name
# Import json
data = []
for file in glob.glob(files):
with open(file) as f:
jdata = json.load(f)
jdata['json_file'] = file
data.append(jdata)
# Get all categories
file_name, wh, cat = [], [], []
for i, x in enumerate(tqdm(data, desc='Files and Shapes')):
with contextlib.suppress(Exception):
cat.extend(a['tags'][0] for a in x['regions']) # categories
# Write *.names file
names = sorted(pd.unique(cat))
with open(name + '.names', 'a') as file:
[file.write('%s\n' % a) for a in names]
# Write labels file
n1, n2 = 0, 0
missing_images = []
for i, x in enumerate(tqdm(data, desc='Annotations')):
f = glob.glob(img_path + x['asset']['name'] + '.jpg')
if len(f):
f = f[0]
file_name.append(f)
wh = exif_size(Image.open(f)) # (width, height)
n1 += 1
if (len(f) > 0) and (wh[0] > 0) and (wh[1] > 0):
n2 += 1
# append filename to list
with open(name + '.txt', 'a') as file:
file.write('%s\n' % f)
# write labelsfile
label_name = Path(f).stem + '.txt'
with open(path + '/labels/' + label_name, 'a') as file:
for a in x['regions']:
category_id = names.index(a['tags'][0])
# The INFOLKS bounding box format is [x-min, y-min, x-max, y-max]
box = a['boundingBox']
box = np.array([box['left'], box['top'], box['width'], box['height']]).ravel()
box[[0, 2]] /= wh[0] # normalize x by width
box[[1, 3]] /= wh[1] # normalize y by height
box = [box[0] + box[2] / 2, box[1] + box[3] / 2, box[2], box[3]] # xywh
if (box[2] > 0.) and (box[3] > 0.): # if w > 0 and h > 0
file.write('%g %.6f %.6f %.6f %.6f\n' % (category_id, *box))
else:
missing_images.append(x['asset']['name'])
print('Attempted %g json imports, found %g images, imported %g annotations successfully' % (i, n1, n2))
if len(missing_images):
print('WARNING, missing images:', missing_images)
# Split data into train, test, and validate files
split_files(name, file_name)
print(f'Done. Output saved to {os.getcwd() + os.sep + path}')
# Convert ath JSON file into YOLO-format labels --------------------------------
def convert_ath_json(json_dir): # dir contains json annotations and images
# Create folders
dir = make_dirs() # output directory
jsons = []
for dirpath, dirnames, filenames in os.walk(json_dir):
jsons.extend(
os.path.join(dirpath, filename)
for filename in [
f for f in filenames if f.lower().endswith('.json')
]
)
# Import json
n1, n2, n3 = 0, 0, 0
missing_images, file_name = [], []
for json_file in sorted(jsons):
with open(json_file) as f:
data = json.load(f)
# # Get classes
# try:
# classes = list(data['_via_attributes']['region']['class']['options'].values()) # classes
# except:
# classes = list(data['_via_attributes']['region']['Class']['options'].values()) # classes
# # Write *.names file
# names = pd.unique(classes) # preserves sort order
# with open(dir + 'data.names', 'w') as f:
# [f.write('%s\n' % a) for a in names]
# Write labels file
for x in tqdm(data['_via_img_metadata'].values(), desc=f'Processing {json_file}'):
image_file = str(Path(json_file).parent / x['filename'])
f = glob.glob(image_file) # image file
if len(f):
f = f[0]
file_name.append(f)
wh = exif_size(Image.open(f)) # (width, height)
n1 += 1 # all images
if len(f) > 0 and wh[0] > 0 and wh[1] > 0:
label_file = dir + 'labels/' + Path(f).stem + '.txt'
nlabels = 0
try:
with open(label_file, 'a') as file: # write labelsfile
# try:
# category_id = int(a['region_attributes']['class'])
# except:
# category_id = int(a['region_attributes']['Class'])
category_id = 0 # single-class
for a in x['regions']:
# bounding box format is [x-min, y-min, x-max, y-max]
box = a['shape_attributes']
box = np.array([box['x'], box['y'], box['width'], box['height']],
dtype=np.float32).ravel()
box[[0, 2]] /= wh[0] # normalize x by width
box[[1, 3]] /= wh[1] # normalize y by height
box = [box[0] + box[2] / 2, box[1] + box[3] / 2, box[2],
box[3]] # xywh (left-top to center x-y)
if box[2] > 0. and box[3] > 0.: # if w > 0 and h > 0
file.write('%g %.6f %.6f %.6f %.6f\n' % (category_id, *box))
n3 += 1
nlabels += 1
if nlabels == 0: # remove non-labelled images from dataset
os.system(f'rm {label_file}')
# print('no labels for %s' % f)
continue # next file
# write image
img_size = 4096 # resize to maximum
img = cv2.imread(f) # BGR
assert img is not None, 'Image Not Found ' + f
r = img_size / max(img.shape) # size ratio
if r < 1: # downsize if necessary
h, w, _ = img.shape
img = cv2.resize(img, (int(w * r), int(h * r)), interpolation=cv2.INTER_AREA)
ifile = dir + 'images/' + Path(f).name
if cv2.imwrite(ifile, img): # if success append image to list
with open(dir + 'data.txt', 'a') as file:
file.write('%s\n' % ifile)
n2 += 1 # correct images
except Exception:
os.system(f'rm {label_file}')
print(f'problem with {f}')
else:
missing_images.append(image_file)
nm = len(missing_images) # number missing
print('\nFound %g JSONs with %g labels over %g images. Found %g images, labelled %g images successfully' %
(len(jsons), n3, n1, n1 - nm, n2))
if len(missing_images):
print('WARNING, missing images:', missing_images)
# Write *.names file
names = ['knife'] # preserves sort order
with open(dir + 'data.names', 'w') as f:
[f.write('%s\n' % a) for a in names]
# Split data into train, test, and validate files
split_rows_simple(dir + 'data.txt')
write_data_data(dir + 'data.data', nc=1)
print(f'Done. Output saved to {Path(dir).absolute()}')
def convert_coco_json(json_dir='../coco/annotations/', use_segments=False, cls91to80=False):
save_dir = make_dirs() # output directory
coco80 = coco91_to_coco80_class()
# Import json
for json_file in sorted(Path(json_dir).resolve().glob('*.json')):
fn = Path(save_dir) / 'labels' / json_file.stem.replace('instances_', '') # folder name
fn.mkdir()
with open(json_file) as f:
data = json.load(f)
# Create image dict
images = {'%g' % x['id']: x for x in data['images']}
# Create image-annotations dict
imgToAnns = defaultdict(list)
for ann in data['annotations']:
imgToAnns[ann['image_id']].append(ann)
# Write labels file
for img_id, anns in tqdm(imgToAnns.items(), desc=f'Annotations {json_file}'):
img = images['%g' % img_id]
h, w, f = img['height'], img['width'], img['file_name']
bboxes = []
segments = []
for ann in anns:
if ann['iscrowd']:
continue
# The COCO box format is [top left x, top left y, width, height]
box = np.array(ann['bbox'], dtype=np.float64)
box[:2] += box[2:] / 2 # xy top-left corner to center
box[[0, 2]] /= w # normalize x
box[[1, 3]] /= h # normalize y
if box[2] <= 0 or box[3] <= 0: # if w <= 0 and h <= 0
continue
cls = coco80[ann['category_id'] - 1] if cls91to80 else ann['category_id'] - 1 # class
box = [cls] + box.tolist()
if box not in bboxes:
bboxes.append(box)
# Segments
if use_segments:
if len(ann['segmentation']) > 1:
s = merge_multi_segment(ann['segmentation'])
s = (np.concatenate(s, axis=0) / np.array([w, h])).reshape(-1).tolist()
else:
s = [j for i in ann['segmentation'] for j in i] # all segments concatenated
s = (np.array(s).reshape(-1, 2) / np.array([w, h])).reshape(-1).tolist()
s = [cls] + s
if s not in segments:
segments.append(s)
# Write
with open((fn / f).with_suffix('.txt'), 'a') as file:
for i in range(len(bboxes)):
line = *(segments[i] if use_segments else bboxes[i]), # cls, box or segments
file.write(('%g ' * len(line)).rstrip() % line + '\n')
def min_index(arr1, arr2):
"""Find a pair of indexes with the shortest distance.
Args:
arr1: (N, 2).
arr2: (M, 2).
Return:
a pair of indexes(tuple).
"""
dis = ((arr1[:, None, :] - arr2[None, :, :]) ** 2).sum(-1)
return np.unravel_index(np.argmin(dis, axis=None), dis.shape)
def merge_multi_segment(segments):
"""Merge multi segments to one list.
Find the coordinates with min distance between each segment,
then connect these coordinates with one thin line to merge all
segments into one.
Args:
segments(List(List)): original segmentations in coco's json file.
like [segmentation1, segmentation2,...],
each segmentation is a list of coordinates.
"""
s = []
segments = [np.array(i).reshape(-1, 2) for i in segments]
idx_list = [[] for _ in range(len(segments))]
# record the indexes with min distance between each segment
for i in range(1, len(segments)):
idx1, idx2 = min_index(segments[i - 1], segments[i])
idx_list[i - 1].append(idx1)
idx_list[i].append(idx2)
# use two round to connect all the segments
for k in range(2):
# forward connection
if k == 0:
for i, idx in enumerate(idx_list):
# middle segments have two indexes
# reverse the index of middle segments
if len(idx) == 2 and idx[0] > idx[1]:
idx = idx[::-1]
segments[i] = segments[i][::-1, :]
segments[i] = np.roll(segments[i], -idx[0], axis=0)
segments[i] = np.concatenate([segments[i], segments[i][:1]])
# deal with the first segment and the last one
if i in [0, len(idx_list) - 1]:
s.append(segments[i])
else:
idx = [0, idx[1] - idx[0]]
s.append(segments[i][idx[0]:idx[1] + 1])
else:
for i in range(len(idx_list) - 1, -1, -1):
if i not in [0, len(idx_list) - 1]:
idx = idx_list[i]
nidx = abs(idx[1] - idx[0])
s.append(segments[i][nidx:])
return s
def delete_dsstore(path='../datasets'):
# Delete apple .DS_store files
from pathlib import Path
files = list(Path(path).rglob('.DS_store'))
print(files)
for f in files:
f.unlink()
if __name__ == '__main__':
source = 'COCO'
if source == 'COCO':
convert_coco_json('./annotations', # directory with *.json
use_segments=True,
cls91to80=True)
elif source == 'infolks': # Infolks https://infolks.info/
convert_infolks_json(name='out',
files='../data/sm4/json/*.json',
img_path='../data/sm4/images/')
elif source == 'vott': # VoTT https://github.com/microsoft/VoTT
convert_vott_json(name='data',
files='../../Downloads/athena_day/20190715/*.json',
img_path='../../Downloads/athena_day/20190715/') # images folder
elif source == 'ath': # ath format
convert_ath_json(json_dir='../../Downloads/athena/') # images folder
# zip results
# os.system('zip -r ../coco.zip ../coco')
整理数据文件夹结构
我们需要将数据集整理为以下结构:
-----datasets
-----coco128-seg
|-----images
| |-----train
| |-----valid
| |-----test
|
|-----labels
| |-----train
| |-----valid
| |-----test
|
模型训练
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 predict.py
from ultralytics.engine.predictor import BasePredictor
from ultralytics.engine.results import Results
from ultralytics.utils import ops
class DetectionPredictor(BasePredictor):
def postprocess(self, preds, img, orig_imgs):
preds = ops.non_max_suppression(preds,
self.args.conf,
self.args.iou,
agnostic=self.args.agnostic_nms,
max_det=self.args.max_det,
classes=self.args.classes)
if not isinstance(orig_imgs, list):
orig_imgs = ops.convert_torch2numpy_batch(orig_imgs)
results = []
for i, pred in enumerate(preds):
orig_img = orig_imgs[i]
pred[:, :4] = ops.scale_boxes(img.shape[2:], pred[:, :4], orig_img.shape)
img_path = self.batch[0][i]
results.append(Results(orig_img, path=img_path, names=self.model.names, boxes=pred))
return results
这段代码定义了一个名为DetectionPredictor
的类,继承自BasePredictor
类。该类用于基于检测模型进行预测。其中,postprocess
方法用于对预测结果进行后处理,并返回一个Results
对象的列表。在postprocess
方法中,首先对预测结果进行非最大抑制处理,然后根据输入图片的形状对预测框进行缩放,最后将处理后的结果存储在Results
对象中,并添加到结果列表中。
该程序文件名为predict.py,主要包含一个名为DetectionPredictor的类,该类继承自BasePredictor类,用于基于检测模型进行预测。
该类中包含一个postprocess方法,用于对预测结果进行后处理,并返回一个Results对象的列表。在postprocess方法中,首先使用ops.non_max_suppression函数对预测结果进行非最大值抑制处理,根据设定的阈值和IOU阈值进行筛选。然后,根据输入的原始图像和预测结果,对预测框进行缩放和转换。最后,将原始图像、图像路径、类别名称和预测框作为参数,创建Results对象,并将其添加到结果列表中。
该文件还包含一个示例代码,用于演示如何使用DetectionPredictor类进行预测。首先导入所需的模块和资源,然后创建DetectionPredictor对象,并调用predict_cli方法进行预测。
总体而言,该程序文件实现了一个基于检测模型的预测器类,提供了预测和后处理的功能,并提供了一个示例代码供参考。
5.2 train.py
# Ultralytics YOLO 🚀, AGPL-3.0 license
from copy import copy
import numpy as np
from ultralytics.data import build_dataloader, build_yolo_dataset
from ultralytics.engine.trainer import BaseTrainer
from ultralytics.models import yolo
from ultralytics.nn.tasks import DetectionModel
from ultralytics.utils import LOGGER, RANK
from ultralytics.utils.torch_utils import de_parallel, torch_distributed_zero_first
class DetectionTrainer(BaseTrainer):
def build_dataset(self, img_path, mode='train', batch=None):
gs = max(int(de_parallel(self.model).stride.max() if self.model else 0), 32)
return build_yolo_dataset(self.args, img_path, batch, self.data, mode=mode, rect=mode == 'val', stride=gs)
def get_dataloader(self, dataset_path, batch_size=16, rank=0, mode='train'):
assert mode in ['train', 'val']
with torch_distributed_zero_first(rank):
dataset = self.build_dataset(dataset_path, mode, batch_size)
shuffle = mode == 'train'
if getattr(dataset, 'rect', False) and shuffle:
LOGGER.warning("WARNING ⚠️ 'rect=True' is incompatible with DataLoader shuffle, setting shuffle=False")
shuffle = False
workers = 0
return build_dataloader(dataset, batch_size, workers, shuffle, rank)
def preprocess_batch(self, batch):
batch['img'] = batch['img'].to(self.device, non_blocking=True).float() / 255
return batch
def set_model_attributes(self):
self.model.nc = self.data['nc']
self.model.names = self.data['names']
self.model.args = self.args
def get_model(self, cfg=None, weights=None, verbose=True):
model = DetectionModel(cfg, nc=self.data['nc'], verbose=verbose and RANK == -1)
if weights:
model.load(weights)
return model
def get_validator(self):
self.loss_names = 'box_loss', 'cls_loss', 'dfl_loss'
return yolo.detect.DetectionValidator(self.test_loader, save_dir=self.save_dir, args=copy(self.args))
def label_loss_items(self, loss_items=None, prefix='train'):
keys = [f'{prefix}/{x}' for x in self.loss_names]
if loss_items is not None:
loss_items = [round(float(x), 5) for x in loss_items]
return dict(zip(keys, loss_items))
else:
return keys
def progress_string(self):
return ('\n' + '%11s' *
(4 + len(self.loss_names))) % ('Epoch', 'GPU_mem', *self.loss_names, 'Instances', 'Size')
def plot_training_samples(self, batch, ni):
plot_images(images=batch['img'],
batch_idx=batch['batch_idx'],
cls=batch['cls'].squeeze(-1),
bboxes=batch['bboxes'],
paths=batch['im_file'],
fname=self.save_dir / f'train_batch{ni}.jpg',
on_plot=self.on_plot)
def plot_metrics(self):
plot_results(file=self.csv, on_plot=self.on_plot)
def plot_training_labels(self):
boxes = np.concatenate([lb['bboxes'] for lb in self.train_loader.dataset.labels], 0)
cls = np.concatenate([lb['cls'] for lb in self.train_loader.dataset.labels], 0)
plot_labels(boxes, cls.squeeze(), names=self.data['names'], save_dir=self.save_dir, on_plot=self.on_plot)
这是一个用于训练基于检测模型的程序文件train.py。它使用了Ultralytics YOLO库,主要包括以下几个部分:
- 导入所需的库和模块。
- 定义了一个名为DetectionTrainer的类,该类继承自BaseTrainer类,用于基于检测模型进行训练。
- 类中包含了一些方法,用于构建数据集、构建数据加载器、预处理批次数据、设置模型属性等。
- 类中还包含了一些用于可视化训练样本、绘制训练指标和绘制训练标签的方法。
- 在主函数中,创建了一个DetectionTrainer对象,并调用其train方法进行训练。
该程序文件使用了Ultralytics YOLO库提供的功能,可以用于训练和验证YOLO检测模型。
5.3 backbone\EfficientFormerV2.py
import torch
import torch.nn as nn
import math
from typing import Dict
import itertools
import numpy as np
from timm.models.layers import DropPath, trunc_normal_, to_2tuple
class Attention4D(torch.nn.Module):
def __init__(self, dim=384, key_dim=32, num_heads=8,
attn_ratio=4,
resolution=7,
act_layer=nn.ReLU,
stride=None):
super().__init__()
self.num_heads = num_heads
self.scale = key_dim ** -0.5
self.key_dim = key_dim
self.nh_kd = nh_kd = key_dim * num_heads
if stride is not None:
self.resolution = math.ceil(resolution / stride)
self.stride_conv = nn.Sequential(nn.Conv2d(dim, dim, kernel_size=3, stride=stride, padding=1, groups=dim),
nn.BatchNorm2d(dim), )
self.upsample = nn.Upsample(scale_factor=stride, mode='bilinear')
else:
self.resolution = resolution
self.stride_conv = None
self.upsample = None
self.N = self.resolution ** 2
self.N2 = self.N
self.d = int(attn_ratio * key_dim)
self.dh = int(attn_ratio * key_dim) * num_heads
self.attn_ratio = attn_ratio
h = self.dh + nh_kd * 2
self.q = nn.Sequential(nn.Conv2d(dim, self.num_heads * self.key_dim, 1),
nn.BatchNorm2d(self.num_heads * self.key_dim), )
self.k = nn.Sequential(nn.Conv2d(dim, self.num_heads * self.key_dim, 1),
nn.BatchNorm2d(self.num_heads * self.key_dim), )
self.v = nn.Sequential(nn.Conv2d(dim, self.num_heads * self.d, 1),
nn.BatchNorm2d(self.num_heads * self.d),
)
self.v_local = nn.Sequential(nn.Conv2d(self.num_heads * self.d, self.num_heads * self.d,
kernel_size=3, stride=1, padding=1, groups=self.num_heads * self.d),
nn.BatchNorm2d(self.num_heads * self.d), )
self.talking_head1 = nn.Conv2d(self.num_heads, self.num_heads, kernel_size=1, stride=1, padding=0)
self.talking_head2 = nn.Conv2d(self.num_heads, self.num_heads, kernel_size=1, stride=1, padding=0)
self.proj = nn.Sequential(act_layer(),
nn.Conv2d(self.dh, dim, 1),
nn.BatchNorm2d(dim), )
points = list(itertools.product(range(self.resolution), range(self.resolution)))
N = len(points)
attention_offsets = {}
idxs = []
......
EfficientFormerV2.py是一个用于图像分类的模型文件。该文件定义了EfficientFormerV2模型的结构和各个组件的实现。
EfficientFormerV2模型是一个基于Transformer的图像分类模型,它使用了多层的Attention机制来提取图像特征。模型的输入是一张图像,经过一系列的卷积和注意力操作后,输出图像的特征表示。最后,通过全局平均池化和全连接层将特征映射到类别空间,得到最终的分类结果。
EfficientFormerV2模型的主要组件包括:
- Attention4D:四维注意力模块,用于提取图像特征。
- stem:模型的初始卷积层,用于将输入图像转换为特征图。
- LGQuery:局部和全局查询模块,用于生成查询向量。
- Attention4DDownsample:下采样的四维注意力模块,用于降低特征图的分辨率。
- Embedding:特征嵌入模块,用于将输入图像转换为特征表示。
EfficientFormerV2模型提供了多个不同规模的预训练模型,包括efficientformerv2_s0、efficientformerv2_s1、efficientformerv2_s2和efficientformerv2_l。每个模型的输入图像尺寸和特征通道数不同,可以根据具体任务的需求选择合适的模型。
EfficientFormerV2模型的参数可以通过EfficientFormer_width和EfficientFormer_depth字典来配置,以控制模型的宽度和深度。模型的宽度和深度越大,模型的容量和表达能力越强,但计算和存储开销也会增加。
总的来说,EfficientFormerV2.py是一个用于图像分类的模型文件,定义了EfficientFormerV2模型的结构和各个组件的实现,提供了多个不同规模的预训练模型,可以根据具体任务的需求选择合适的模型。
5.4 backbone\efficientViT.py
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.utils.checkpoint as checkpoint
import itertools
from timm.models.layers import SqueezeExcite
import numpy as np
import itertools
class EfficientViT_M0(nn.Module):
def __init__(self, num_classes=1000, img_size=224, patch_size=16, in_chans=3, embed_dim=768, depth=12, num_heads=12, mlp_ratio=4.0, qkv_bias=False, drop_rate=0.0, attn_drop_rate=0.0, drop_path_rate=0.0):
super(EfficientViT_M0, self).__init__()
self.num_classes = num_classes
self.num_features = self.embed_dim = embed_dim
self.patch_embed = PatchEmbed(
img_size=img_size, patch_size=patch_size, in_chans=in_chans, embed_dim=embed_dim)
num_patches = self.patch_embed.num_patches
self.pos_embed = nn.Parameter(torch.zeros(1, num_patches + 1, embed_dim))
self.cls_token = nn.Parameter(torch.zeros(1, 1, embed_dim))
self.pos_drop = nn.Dropout(p=drop_rate)
dpr = [x.item() for x in torch.linspace(0, drop_path_rate, depth)]
self.blocks = nn.ModuleList([
Block(
dim=embed_dim, num_heads=num_heads, mlp_ratio=mlp_ratio, qkv_bias=qkv_bias, drop=drop_rate, attn_drop=attn_drop_rate, drop_path=dpr[i])
for i in range(depth)])
self.norm = nn.LayerNorm(embed_dim)
self.head = nn.Linear(embed_dim, num_classes) if num_classes > 0 else nn.Identity()
trunc_normal_(self.pos_embed, std=.02)
trunc_normal_(self.cls_token, std=.02)
self.apply(self._init_weights)
def _init_weights(self, m):
if isinstance(m, nn.Linear):
trunc_normal_(m.weight, std=.02)
if isinstance(m, nn.Linear) and m.bias is not None:
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.LayerNorm):
nn.init.constant_(m.bias, 0)
nn.init.constant_(m.weight, 1.0)
def forward_features(self, x):
B = x.shape[0]
x = self.patch_embed(x)
cls_tokens = self.cls_token.expand(B, -1, -1)
x = torch.cat((cls_tokens, x), dim=1)
x = x + self.pos_embed
x = self.pos_drop(x)
for blk in self.blocks:
x = blk(x)
x = self.norm(x)
return x[:, 0]
def forward(self, x):
x = self.forward_features(x)
x = self.head(x)
return x
该程序文件是一个EfficientViT模型的架构,用于下游任务。它定义了一系列的模块和函数,包括Conv2d_BN、replace_batchnorm、PatchMerging、Residual、FFN、CascadedGroupAttention和LocalWindowAttention等。
Conv2d_BN是一个包含卷积和批归一化的序列模块。
replace_batchnorm函数用于替换模型中的批归一化层。
PatchMerging模块用于将输入的图像分块合并。
Residual模块实现了残差连接。
FFN模块是一个前馈神经网络。
CascadedGroupAttention模块实现了级联的组注意力机制。
LocalWindowAttention模块实现了局部窗口注意力机制。
这些模块和函数组合在一起构成了EfficientViT模型的架构。
5.5 backbone\fasternet.py
import torch
import torch.nn as nn
from timm.models.layers import DropPath, to_2tuple, trunc_normal_
from functools import partial
from typing import List
from torch import Tensor
import copy
import os
import numpy as np
class Partial_conv3(nn.Module):
def __init__(self, dim, n_div, forward):
super().__init__()
self.dim_conv3 = dim // n_div
self.dim_untouched = dim - self.dim_conv3
self.partial_conv3 = nn.Conv2d(self.dim_conv3, self.dim_conv3, 3, 1, 1, bias=False)
if forward == 'slicing':
self.forward = self.forward_slicing
elif forward == 'split_cat':
self.forward = self.forward_split_cat
else:
raise NotImplementedError
def forward_slicing(self, x: Tensor) -> Tensor:
# only for inference
x = x.clone() # !!! Keep the original input intact for the residual connection later
x[:, :self.dim_conv3, :, :] = self.partial_conv3(x[:, :self.dim_conv3, :, :])
return x
def forward_split_cat(self, x: Tensor) -> Tensor:
# for training/inference
x1, x2 = torch.split(x, [self.dim_conv3, self.dim_untouched], dim=1)
x1 = self.partial_conv3(x1)
x = torch.cat((x1, x2), 1)
return x
class MLPBlock(nn.Module):
def __init__(self,
dim,
n_div,
mlp_ratio,
drop_path,
layer_scale_init_value,
act_layer,
norm_layer,
pconv_fw_type
):
super().__init__()
self.dim = dim
self.mlp_ratio = mlp_ratio
self.drop_path = DropPath(drop_path) if drop_path > 0. else nn.Identity()
self.n_div = n_div
mlp_hidden_dim = int(dim * mlp_ratio)
mlp_layer: List[nn.Module] = [
nn.Conv2d(dim, mlp_hidden_dim, 1, bias=False),
norm_layer(mlp_hidden_dim),
act_layer(),
nn.Conv2d(mlp_hidden_dim, dim, 1, bias=False)
]
self.mlp = nn.Sequential(*mlp_layer)
self.spatial_mixing = Partial_conv3(
dim,
n_div,
pconv_fw_type
)
if layer_scale_init_value > 0:
self.layer_scale = nn.Parameter(layer_scale_init_value * torch.ones((dim)), requires_grad=True)
self.forward = self.forward_layer_scale
else:
self.forward = self.forward
def forward(self, x: Tensor) -> Tensor:
shortcut = x
x = self.spatial_mixing(x)
x = shortcut + self.drop_path(self.mlp(x))
return x
def forward_layer_scale(self, x: Tensor) -> Tensor:
shortcut = x
x = self.spatial_mixing(x)
x = shortcut + self.drop_path(
self.layer_scale.unsqueeze(-1).unsqueeze(-1) * self.mlp(x))
return x
class BasicStage(nn.Module):
def __init__(self,
dim,
depth,
n_div,
mlp_ratio,
drop_path,
layer_scale_init_value,
norm_layer,
act_layer,
pconv_fw_type
):
super().__init__()
blocks_list = [
MLPBlock(
dim=dim,
n_div=n_div,
mlp_ratio=mlp_ratio,
drop_path=drop_path[i],
layer_scale_init_value=layer_scale_init_value,
norm_layer=norm_layer,
act_layer=act_layer,
pconv_fw_type=pconv_fw_type
)
for i in range(depth)
]
self.blocks = nn.Sequential(*blocks_list)
def forward(self, x: Tensor) -> Tensor:
x = self.blocks(x)
return x
class PatchEmbed(nn.Module):
def __init__(self, patch_size, patch_stride, in_chans, embed_dim, norm_layer):
super().__init__()
self.proj = nn.Conv2d(in_chans, embed_dim, kernel_size=patch_size, stride=patch_stride, bias=False)
if norm_layer is not None:
self.norm = norm_layer(embed_dim)
else:
self.norm = nn.Identity()
def forward(self, x: Tensor) -> Tensor:
x = self.norm(self.proj(x))
return x
class PatchMerging(nn.Module):
def __init__(self, patch_size2, patch_stride2, dim, norm_layer):
super().__init__()
self.reduction = nn.Conv2d(dim, 2 * dim, kernel_size=patch_size2, stride=patch_stride2, bias=False)
if norm_layer is not None:
self.norm = norm_layer(2 * dim)
else:
self.norm = nn.Identity()
def forward(self, x: Tensor) -> Tensor:
x = self.norm(self.reduction(x))
return x
class FasterNet(nn.Module):
def __init__(self,
in_chans=3,
num_classes=1000,
embed_dim=96,
depths=(1, 2, 8, 2),
mlp_ratio=2.,
n_div=4,
patch_size=4,
patch_stride=4,
patch_size2=2, # for subsequent layers
patch_stride2=2,
patch_norm=True,
feature_dim=1280,
drop_path_rate=0.1,
layer_scale_init_value=0,
norm_layer='BN',
act_layer='RELU',
init_cfg=None,
pretrained=None,
pconv_fw_type='split_cat',
**kwargs):
super().__init__()
if norm_layer == 'BN':
norm_layer = nn.BatchNorm2d
else:
raise NotImplementedError
if act_layer == 'GELU':
act_layer = nn.GELU
elif act_layer == 'RELU':
act_layer = partial(nn.ReLU, inplace=True)
else:
raise NotImplementedError
self.num_stages = len(depths)
self.embed_dim = embed_dim
该程序文件是一个名为fasternet.py
的Python程序文件,用于定义了一个名为FasterNet
的神经网络模型。该模型是基于PyTorch框架实现的,用于图像分类任务。
该程序文件中定义了多个类和函数,包括Partial_conv3
、MLPBlock
、BasicStage
、PatchEmbed
、PatchMerging
和FasterNet
等类,以及update_weight
、fasternet_t0
、fasternet_t1
、fasternet_t2
、fasternet_s
、fasternet_m
和fasternet_l
等函数。
FasterNet
类是该程序文件的核心部分,它继承自nn.Module
类,并定义了神经网络模型的结构和前向传播方法。该模型包括多个阶段(stage),每个阶段由多个MLP块(MLPBlock)组成。每个MLP块包括一个部分卷积层(Partial_conv3)和一个MLP层(MLP),用于提取特征和进行特征变换。模型还包括图像分割和特征融合的操作,以及一些其他辅助函数。
update_weight
函数用于更新模型的权重,将预训练的权重加载到模型中。
fasternet_t0
、fasternet_t1
、fasternet_t2
、fasternet_s
、fasternet_m
和fasternet_l
函数分别用于创建不同配置的FasterNet
模型,并加载预训练的权重。
在if __name__ == '__main__':
部分,程序创建了一个FasterNet
模型,并打印了模型的通道数。然后,程序创建了一个输入张量,并对模型进行前向传播,打印了每个阶段输出的张量大小。
总体来说,该程序文件定义了一个用于图像分类任务的FasterNet
神经网络模型,并提供了加载预训练权重的函数。
5.6 backbone\lsknet.py
import torch
import torch.nn as nn
from torch.nn.modules.utils import _pair as to_2tuple
from timm.layers import DropPath, to_2tuple
from functools import partial
import numpy as np
class Mlp(nn.Module):
def __init__(self, in_features, hidden_features=None, out_features=None, act_layer=nn.GELU, drop=0.):
super().__init__()
out_features = out_features or in_features
hidden_features = hidden_features or in_features
self.fc1 = nn.Conv2d(in_features, hidden_features, 1)
self.dwconv = DWConv(hidden_features)
self.act = act_layer()
self.fc2 = nn.Conv2d(hidden_features, out_features, 1)
self.drop = nn.Dropout(drop)
def forward(self, x):
x = self.fc1(x)
x = self.dwconv(x)
x = self.act(x)
x = self.drop(x)
x = self.fc2(x)
x = self.drop(x)
return x
class LSKblock(nn.Module):
def __init__(self, dim):
super().__init__()
self.conv0 = nn.Conv2d(dim, dim, 5, padding=2, groups=dim)
self.conv_spatial = nn.Conv2d(dim, dim, 7, stride=1, padding=9, groups=dim, dilation=3)
self.conv1 = nn.Conv2d(dim, dim//2, 1)
self.conv2 = nn.Conv2d(dim, dim//2, 1)
self.conv_squeeze = nn.Conv2d(2, 2, 7, padding=3)
self.conv = nn.Conv2d(dim//2, dim, 1)
def forward(self, x):
attn1 = self.conv0(x)
attn2 = self.conv_spatial(attn1)
attn1 = self.conv1(attn1)
attn2 = self.conv2(attn2)
attn = torch.cat([attn1, attn2], dim=1)
avg_attn = torch.mean(attn, dim=1, keepdim=True)
max_attn, _ = torch.max(attn, dim=1, keepdim=True)
agg = torch.cat([avg_attn, max_attn], dim=1)
sig = self.conv_squeeze(agg).sigmoid()
attn = attn1 * sig[:,0,:,:].unsqueeze(1) + attn2 * sig[:,1,:,:].unsqueeze(1)
attn = self.conv(attn)
return x * attn
class Attention(nn.Module):
def __init__(self, d_model):
super().__init__()
self.proj_1 = nn.Conv2d(d_model, d_model, 1)
self.activation = nn.GELU()
self.spatial_gating_unit = LSKblock(d_model)
self.proj_2 = nn.Conv2d(d_model, d_model, 1)
def forward(self, x):
shorcut = x.clone()
x = self.proj_1(x)
x = self.activation(x)
x = self.spatial_gating_unit(x)
x = self.proj_2(x)
x = x + shorcut
return x
class Block(nn.Module):
def __init__(self, dim, mlp_ratio=4., drop=0.,drop_path=0., act_layer=nn.GELU, norm_cfg=None):
super().__init__()
self.norm1 = nn.BatchNorm2d(dim)
self.norm2 = nn.BatchNorm2d(dim)
self.attn = Attention(dim)
self.drop_path = DropPath(drop_path) if drop_path > 0. else nn.Identity()
mlp_hidden_dim = int(dim * mlp_ratio)
self.mlp = Mlp(in_features=dim, hidden_features=mlp_hidden_dim, act_layer=act_layer, drop=drop)
layer_scale_init_value = 1e-2
self.layer_scale_1 = nn.Parameter(
layer_scale_init_value * torch.ones((dim)), requires_grad=True)
self.layer_scale_2 = nn.Parameter(
layer_scale_init_value * torch.ones((dim)), requires_grad=True)
def forward(self, x):
x = x + self.drop_path(self.layer_scale_1.unsqueeze(-1).unsqueeze(-1) * self.attn(self.norm1(x)))
x = x + self.drop_path(self.layer_scale_2.unsqueeze(-1).unsqueeze(-1) * self.mlp(self.norm2(x)))
return x
class OverlapPatchEmbed(nn.Module):
""" Image to Patch Embedding
"""
def __init__(self, img_size=224, patch_size=7, stride=4, in_chans=3, embed_dim=768, norm_cfg=None):
super().__init__()
patch_size = to_2tuple(patch_size)
self.proj = nn.Conv2d(in_chans, embed_dim, kernel_size=patch_size, stride=stride,
padding=(patch_size[0] // 2, patch_size[1] // 2))
self.norm = nn.BatchNorm2d(embed_dim)
def forward(self, x):
x = self.proj(x)
_, _, H, W = x.shape
x = self.norm(x)
return x, H, W
class LSKNet(nn.Module):
def __init__(self, img_size=224, in_chans=3, embed_dims=[64, 128, 256, 512],
mlp_ratios=[8, 8, 4, 4], drop_rate=0., drop_path_rate=0., norm_layer=partial(nn.LayerNorm, eps=1e-6),
depths=[3, 4, 6, 3], num_stages=4,
norm_cfg=None):
super().__init__()
self.depths = depths
self.num_stages = num_stages
dpr = [x.item() for x in torch.linspace(0, drop_path_rate, sum(depths))] # stochastic depth decay rule
cur = 0
for i in range(num_stages):
patch_embed = OverlapPatchEmbed(img_size=img_size if i == 0 else img_size // (2 ** (i + 1)),
patch_size=7 if i == 0 else 3,
stride=4 if i == 0 else 2,
in_chans=in_chans if i == 0 else embed_dims[i - 1],
embed_dim=embed_dims[i], norm_cfg=norm_cfg)
block = nn.ModuleList([Block(
dim=embed_dims[i], mlp_ratio=mlp_ratios[i], drop=drop_rate, drop_path=dpr[cur + j],norm_cfg=norm_cfg)
for j in range(depths[i])])
norm = norm_layer(embed_dims[i])
cur += depths[i]
setattr(self, f"patch_embed{i + 1}", patch_embed)
setattr(self, f"block{i + 1}", block)
setattr(self, f"norm{i + 1}", norm)
self.channel = [i.size(1) for i in self.forward(torch.randn(1, 3, 640, 640))]
def forward(self, x):
B = x.shape[0]
outs = []
for i in range(self.num_stages):
patch_embed = getattr(self, f"patch_embed{i + 1}")
block = getattr(self, f"block{i + 1}")
norm = getattr(self, f"norm{i + 1}")
x, H, W = patch_embed(x)
for blk in block:
x = blk(x)
x = x.flatten(2).transpose(1, 2)
x = norm(x)
x = x.reshape(B, H, W, -1).permute(0, 3, 1, 2).contiguous()
outs.append(x)
return outs
class DWConv(nn.Module):
def __init__(self, dim=768):
super(DWConv, self).__init__()
self.dwconv = nn.Conv2d(dim, dim, 3, 1, 1, bias=True, groups=dim)
def forward(self, x):
x = self.dwconv(x)
return x
该程序文件是一个实现LSKNet模型的Python代码。LSKNet是一个用于图像分类任务的卷积神经网络模型。该文件包含了LSKNet模型的定义以及一些辅助函数。
主要包含以下几个类和函数:
-
Mlp类:多层感知机模块,包含了两个卷积层和一个激活函数层。
-
LSKblock类:LSK模块,包含了多个卷积层和特殊的注意力机制。
-
Attention类:注意力机制模块,包含了两个卷积层和LSK模块。
-
Block类:LSKNet模型的基本模块,包含了注意力机制和多层感知机模块。
-
OverlapPatchEmbed类:将图像转换为补丁表示的模块。
-
LSKNet类:LSKNet模型的定义,包含了多个Block模块和PatchEmbed模块。
-
DWConv类:深度可分离卷积模块。
-
update_weight函数:用于加载预训练权重。
-
lsknet_t函数:创建一个LSKNet模型实例,用于训练。
-
lsknet_s函数:创建一个LSKNet模型实例,用于训练。
-
main函数:加载预训练权重并对输入数据进行前向传播。
LSKNet模型由多个Block模块组成,每个Block模块包含了注意力机制和多层感知机模块。PatchEmbed模块将输入图像转换为补丁表示,然后通过多个Block模块进行特征提取和分类。最后输出多个尺度的特征图。
LSKNet模型有两个版本,分别是lsknet_t和lsknet_s,它们的网络结构和参数设置略有不同。可以通过调用lsknet_t和lsknet_s函数来创建对应版本的LSKNet模型实例,并可以选择加载预训练权重。
6.系统整体结构
以下是每个文件的功能总结和整理:
文件路径 | 功能 |
---|---|
predict.py | 实现了预测器类和预测函数,用于模型的预测操作 |
train.py | 实现了训练器类和训练函数,用于模型的训练操作 |
backbone\EfficientFormerV2.py | 定义了EfficientFormerV2模型的结构和组件实现 |
backbone\efficientViT.py | 定义了efficientViT模型的结构和组件实现 |
backbone\fasternet.py | 定义了Fasternet模型的结构和组件实现 |
backbone\lsknet.py | 定义了LSKNet模型的结构和组件实现 |
backbone\repvit.py | 定义了RepVIT模型的结构和组件实现 |
backbone\revcol.py | 定义了RevCol模型的结构和组件实现 |
backbone\SwinTransformer.py | 定义了SwinTransformer模型的结构和组件实现 |
backbone\VanillaNet.py | 定义了VanillaNet模型的结构和组件实现 |
extra_modules\block.py | 定义了一些常用的模块和函数,如注意力机制、残差模块等 |
extra_modules\dynamic_snake_conv.py | 定义了动态蛇形卷积模块的实现 |
extra_modules\head.py | 定义了模型的头部结构,用于输出预测结果 |
extra_modules\kernel_warehouse.py | 定义了一些卷积核的存储和管理功能 |
extra_modules\orepa.py | 定义了一种特 |
7.YOLOv8简介
Backbone
借鉴了其他算法的这些设计思想
借鉴了VGG的思想,使用了较多的3×3卷积,在每一次池化操作后,将通道数翻倍;
借鉴了network in network的思想,使用全局平均池化(global average pooling)做预测,并把1×1的卷积核置于3×3的卷积核之间,用来压缩特征;(我没找到这一步体现在哪里)
使用了批归一化层稳定模型训练,加速收敛,并且起到正则化作用。
以上三点为Darknet19借鉴其他模型的点。Darknet53当然是在继承了Darknet19的这些优点的基础上再新增了下面这些优点的。因此列在了这里
借鉴了ResNet的思想,在网络中大量使用了残差连接,因此网络结构可以设计的很深,并且缓解了训练中梯度消失的问题,使得模型更容易收敛。
使用步长为2的卷积层代替池化层实现降采样。(这一点在经典的Darknet-53上是很明显的,output的长和宽从256降到128,再降低到64,一路降低到8,应该是通过步长为2的卷积层实现的;在YOLOv8的卷积层中也有体现,比如图中我标出的这些位置)
特征融合
模型架构图如下
Darknet-53的特点可以这样概括:(Conv卷积模块+Residual Block残差块)串行叠加4次
Conv卷积层+Residual Block残差网络就被称为一个stage
上面红色指出的那个,原始的Darknet-53里面有一层 卷积,在YOLOv8里面,把一层卷积移除了
为什么移除呢?
原始Darknet-53模型中间加的这个卷积层做了什么?滤波器(卷积核)的个数从 上一个卷积层的512个,先增加到1024个卷积核,然后下一层卷积的卷积核的个数又降低到512个
移除掉这一层以后,少了1024个卷积核,就可以少做1024次卷积运算,同时也少了1024个3×3的卷积核的参数,也就是少了9×1024个参数需要拟合。这样可以大大减少了模型的参数,(相当于做了轻量化吧)
移除掉这个卷积层,可能是因为作者发现移除掉这个卷积层以后,模型的score有所提升,所以才移除掉的。为什么移除掉以后,分数有所提高呢?可能是因为多了这些参数就容易,参数过多导致模型在训练集删过拟合,但是在测试集上表现很差,最终模型的分数比较低。你移除掉这个卷积层以后,参数减少了,过拟合现象不那么严重了,泛化能力增强了。当然这个是,拿着你做实验的结论,反过来再找补,再去强行解释这种现象的合理性。
过拟合
通过MMdetection官方绘制册这个图我们可以看到,进来的这张图片经过一个“Feature Pyramid Network(简称FPN)”,然后最后的P3、P4、P5传递给下一层的Neck和Head去做识别任务。 PAN(Path Aggregation Network)
“FPN是自顶向下,将高层的强语义特征传递下来。PAN就是在FPN的后面添加一个自底向上的金字塔,对FPN补充,将低层的强定位特征传递上去,
FPN是自顶(小尺寸,卷积次数多得到的结果,语义信息丰富)向下(大尺寸,卷积次数少得到的结果),将高层的强语义特征传递下来,对整个金字塔进行增强,不过只增强了语义信息,对定位信息没有传递。PAN就是针对这一点,在FPN的后面添加一个自底(卷积次数少,大尺寸)向上(卷积次数多,小尺寸,语义信息丰富)的金字塔,对FPN补充,将低层的强定位特征传递上去,又被称之为“双塔战术”。
FPN层自顶向下传达强语义特征,而特征金字塔则自底向上传达强定位特征,两两联手,从不同的主干层对不同的检测层进行参数聚合,这样的操作确实很皮。
自底向上增强
而 PAN(Path Aggregation Network)是对 FPN 的一种改进,它的设计理念是在 FPN 后面添加一个自底向上的金字塔。PAN 引入了路径聚合的方式,通过将浅层特征图(低分辨率但语义信息较弱)和深层特征图(高分辨率但语义信息丰富)进行聚合,并沿着特定的路径传递特征信息,将低层的强定位特征传递上去。这样的操作能够进一步增强多尺度特征的表达能力,使得 PAN 在目标检测任务中表现更加优秀。
8.可重参化EfficientRepBiPAN优化Neck
Repvgg-style
Repvgg-style的卷积层包含
卷积+ReLU结构,该结构能够有效地利用硬件资源。
在训练时,Repvgg-style的卷积层包含
卷积、
卷积、identity。(下图左图)
在推理时,通过重参数化(re-parameterization),上述的多分支结构可以转换为单分支的
卷积。(下图右图)
基于上述思想,作者设计了对GPU硬件友好的EfficientRep Backbone和Rep-PAN Neck,将它们用于YOLOv6中。
EfficientRep Backbone的结构图:
Rep-PAN Neck结构图:
Multi-path
只使用repvgg-style不能达到很好的精度-速度平衡,对于大模型,作者探索了多路径的网络结构。
参考该博客提出了Bep unit,其结构如下图所示:
CSP(Cross Stage Partial)-style计算量小,且有丰富的梯度融合信息,广泛应用于YOLO系列中,比如YOLOv5、PPYOLOE。
作者将Bep unit与CSP-style结合,设计了一种新的网络结构BepC3,如下图所示:
基于BepC3模块,作者设计了新的CSPBep Backbone和CSPRepPAN Neck,以达到很好的精度-速度平衡。
其他YOLO系列在使用CSP-stype结构时,partial ratio设置为1/2。为了达到更好的性能,在YOLOv6m中partial ratio的值为2/3,在YOLOv6l中partial ratio的值为1/2。
对于YOLOv6m,单纯使用Rep-style结构和使用BepC3结构的对比如下图所示:
BIFPN
BiFPN 全称 Bidirectional Feature Pyramid Network 加权双向(自顶向下 + 自低向上)特征金字塔网络。
相比较于PANet,BiFPN在设计上的改变:
总结下图:
图d 蓝色部分为自顶向下的通路,传递的是高层特征的语义信息;红色部分是自底向上的通路,传递的是低层特征的位置信息;紫色部分是上述第二点提到的同一层在输入节点和输入节点间新加的一条边。
我们删除那些只有一条输入边的节点。这么做的思路很简单:如果一个节点只有一条输入边而没有特征融合,那么它对旨在融合不同特征的特征网络的贡献就会很小。删除它对我们的网络影响不大,同时简化了双向网络;如上图d 的 P7右边第一个节点
如果原始输入节点和输出节点处于同一层,我们会在原始输入节点和输出节点之间添加一条额外的边。思路:以在不增加太多成本的情况下融合更多的特性;
与只有一个自顶向下和一个自底向上路径的PANet不同,我们处理每个双向路径(自顶向下和自底而上)路径作为一个特征网络层,并重复同一层多次,以实现更高层次的特征融合。如下图EfficientNet 的网络结构所示,我们对BiFPN是重复使用多次的。而这个使用次数也不是我们认为设定的,而是作为参数一起加入网络的设计当中,使用NAS技术算出来的。
Weighted Feature Fusion 带权特征融合:学习不同输入特征的重要性,对不同输入特征有区分的融合。
设计思路:传统的特征融合往往只是简单的 feature map 叠加/相加 (sum them up),比如使用concat或者shortcut连接,而不对同时加进来的 feature map 进行区分。然而,不同的输入 feature map 具有不同的分辨率,它们对融合输入 feature map 的贡献也是不同的,因此简单的对他们进行相加或叠加处理并不是最佳的操作。所以这里我们提出了一种简单而高效的加权特融合的机制。
常见的带权特征融合有三种方法,分别是:
这种方法比较简单,直接加一个可学习的权重。但是由于这个权重不受限制,所有可能引起训练的不稳定,所有并不推荐。
Softmax-based fusion: O = ∑ i e w i ∗ I i ϵ + ∑ j e w j O = \sum_{i} \frac{e^{w_i} * I_i}{ \epsilon+\sum_{j}e^{w_j}}O=∑
使用这种方法可以将范围放缩到 [ 0 , 1 ] [0, 1][0,1] 之间,训练稳定,但是训练很慢,所有也不推荐。
Fast normalized fusion: O = ∑ i w i ∗ I i ϵ + ∑ j w j O = \sum_{i} \frac{w_i * I_i}{ \epsilon+\sum_{j}w_j}O=∑
9.训练结果可视化分析
评价指标
Epochs:实验在多个 epoch 中进行,显示训练过程的进度。
训练损失指标:训练损失指标有多种,包括框损失(train/box_loss)、分段损失(train/seg_loss)、对象损失(train/obj_loss)和类损失(train/cls_loss)。
评估指标:数据包括品种 (B) 和肉类 (M) 在不同 IoU 阈值下的精度、召回率和平均精度 (mAP)。这些指标对于评估模型在猪品种和肉类类型分类和本地化方面的性能至关重要。
验证损失指标:与训练损失类似,也有框损失 (val/box_loss)、分段损失 (< a i=3>)、对象损失 () 和类损失 ()。val/seg_lossval/obj_lossval/cls_loss
学习率:数据集还跟踪三种不同的学习率(x/lr0、x/lr1、x/lr2),这可能是训练期间自适应学习率策略的一部分。
训练结果可视化
为了进行详细分析,我将可视化这些指标,以了解训练和验证损失趋势、模型准确性和学习率变化。让我们首先绘制各个时期的训练和验证损失趋势。
import matplotlib.pyplot as plt
# Plotting Training and Validation Losses
plt.figure(figsize=(15, 10))
# Training Losses
plt.subplot(2, 2, 1)
plt.plot(data['epoch'], data['train/box_loss'], label='Box Loss')
plt.plot(data['epoch'], data['train/seg_loss'], label='Segmentation Loss')
plt.plot(data['epoch'], data['train/obj_loss'], label='Object Loss')
plt.plot(data['epoch'], data['train/cls_loss'], label='Class Loss')
plt.title('Training Losses per Epoch')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.legend()
# Validation Losses
plt.subplot(2, 2, 2)
plt.plot(data['epoch'], data['val/box_loss'], label='Box Loss')
plt.plot(data['epoch'], data['val/seg_loss'], label='Segmentation Loss')
plt.plot(data['epoch'], data['val/obj_loss'], label='Object Loss')
plt.plot(data['epoch'], data['val/cls_loss'], label='Class Loss')
plt.title('Validation Losses per Epoch')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.legend()
# Show the plot
plt.tight_layout()
plt.show()
现在我们已经绘制了训练和验证损失的趋势图。接下来,我将对模型的精度和召回率进行可视化分析,这将有助于更深入地了解模型在分类和定位生猪品种和肉类方面的表现。然后,我还会分析学习率的变化情况。让我们继续进行这些分析。
通过上面的图表,我们可以看到生猪品种和肉类的精度、召回率和平均精度(mAP)随着训练周期的变化情况。
接下来,我将分析学习率的变化。这一点很重要,因为学习率的调整通常会影响模型的训练效率和最终性能。让我们绘制学习率随着训练周期的变化图。
学习率随训练周期的变化图已经展示出来了。从图中可以看出,学习率在整个训练过程中的调整情况。
现在,让我们从多个角度对这些数据进行详细分析:
训练和验证损失:
训练损失(包括盒损失、分割损失、对象损失和类别损失)随着训练周期的增加而逐渐下降,这表明模型正在有效学习。
验证损失也呈现类似的下降趋势,这表明模型在未见数据上的泛化能力良好。
精度和召回率:
(B)和(M)的精度和召回率都随着训练周期的增加而提高,说明模型在正确分类和定位方面表现越来越好。
特别是,精度(precision)指标表明模型在预测正类时的准确性很高,而召回率(recall)的提高表明模型越来越少地错过正类。
平均精度(mAP):
mAP指标在不同的交并比(IoU)阈值下的表现也提高,这进一步证实了模型在检测和分类方面的有效性。
mAP@0.5和mAP@0.5:0.95两个指标的提高表明模型在不同严格度的评价标准下都有良好的表现。
学习率:
学习率的变化显示了模型训练过程中学习率调整策略的实施情况。
学习率的适时调整对于避免过拟合、加快收敛速度和提高模型性能至关重要。
10.系统整合
下图完整源码&数据集&环境部署视频教程&自定义UI界面
参考博客《【改进YOLOv8】生猪胖瘦评价分级系统:可重参化EfficientRepBiPAN优化Neck》
11.参考文献
[1]李承明,张斌,夏雨阳,等.胶园植保硫磺粉颗粒离散元模型参数标定[J].江西农业大学学报.2022,44(3).DOI:10.13836/j.jjau.2022061 .
[2]贾乐心,江应星,孙佳,等.基于EDEM的圆盘弹齿组合式水田平地机圆盘转辊仿真与试验[J].江西农业大学学报.2021,43(6).DOI:10.13836/j.jjau.2021150 .
[3]韩树杰,戚江涛,坎杂,等.新疆果园深施散体厩肥离散元参数标定研究[J].农业机械学报.2021,(4).DOI:10.6041/j.issn.1000-1298.2021.04.010 .
[4]邢洁洁,张锐,吴鹏,等.海南热区砖红壤颗粒离散元仿真模型参数标定[J].农业工程学报.2020,(5).DOI:10.11975/j.issn.1002-6819.2020.05.018 .
[5]彭才望,许道军,贺喜,等.黑水虻处理的猪粪有机肥离散元仿真模型参数标定[J].农业工程学报.2020,(17).DOI:10.11975/j.issn.1002-6819.2020.17.025 .
[6]王黎明,范盛远,程红胜,等.基于EDEM的猪粪接触参数标定[J].农业工程学报.2020,(15).DOI:10.11975/j.issn.1002-6819.2020.15.012 .
[7]张珂,于天行,于文达,等.基于JKR黏结模型的混凝土离散元参数标定[J].混凝土.2020,(8).DOI:10.3969/j.issn.1002-3550.2020.08.011 .
[8]向伟,吴明亮,吕江南,等.基于堆积试验的黏壤土仿真物理参数标定[J].农业工程学报.2019,(12).DOI:10.11975/j.issn.1002-6819.2019.12.014 .
[9]林嘉聪,罗帅,袁巧霞,等.不同含水率蚯蚓粪颗粒物料流动性研究[J].农业工程学报.2019,(9).DOI:10.11975/j.issn.1002-6819.2019.09.027 .
[10]李永祥,李飞翔,徐雪萌,等.基于颗粒缩放的小麦粉离散元参数标定[J].农业工程学报.2019,(16).DOI:10.11975/j.issn.1002-6819.2019.16.035 .