記事ディレクトリ
序文
最近、YOLOV5 のバックボーンを ShuffleNetv2 のような軽量なネットワークに変更してみようと思っています。yolov5s と比較してみたいです。特に言うことはありません。本文はここから始まります
1. 準備
1. コードの準備
YOLOV5 の最新コードを取得します。コードのリンクは次のとおりです: YOLOV5
2. データセットの準備
2.1 データセットのダウンロード
ここで VOC データセットを準備します。撤回してダウンロードしなくても問題ありません。トレーニング中に自動的にダウンロードされますが、事前に準備することをお勧めします。ダウンロード リンクは次のとおりです: VOC ,以下の図のボックス内の部分をダウンロードするだけです。
2.2 データセットの解凍と配置
(1) データセットを自分で手動でダウンロードする場合は、yolov5/datasets/VOC/images ディレクトリにアップロードする必要があります。そうでない場合は作成します。ディレクトリは不一致になる可能性がありますが、トレーニング中の変更を少なくするために、公式配置ディレクトリと同じです。一貫性を保ってください。(個人的には、独自のデータセットをトレーニングする場合は、yolov5 ディレクトリに配置しないことをお勧めします)
(2) YOLOv5 ディレクトリに、新しいフォルダー my_tools を作成し、ランダムなファイル名で新しい Python ファイルを作成します。このスクリプトはフォルダーを解凍し、YOLO 形式を生成するためのもので、コードは次のとおりです。
import xml.etree.ElementTree as ET
from tqdm import tqdm
from pathlib import Path
import os,sys,platform
FILE = Path(__file__).resolve()
ROOT = FILE.parents[1] # YOLOv5 root directory
print(ROOT)
if str(ROOT) not in sys.path:
sys.path.append(str(ROOT)) # add ROOT to PATH
if platform.system() != 'Windows':
ROOT = Path(os.path.relpath(ROOT, Path.cwd())) # relative
print(ROOT)
from utils.general import download
def convert_label(path, lb_path, year, image_id):
def convert_box(size, box):
dw, dh = 1. / size[0], 1. / size[1]
x, y, w, h = (box[0] + box[1]) / 2.0 - 1, (box[2] + box[3]) / 2.0 - 1, box[1] - box[0], box[3] - box[2]
return x * dw, y * dh, w * dw, h * dh
in_file = open(path / f'VOC{
year}/Annotations/{
image_id}.xml')
out_file = open(lb_path, 'w')
tree = ET.parse(in_file)
root = tree.getroot()
size = root.find('size')
w = int(size.find('width').text)
h = int(size.find('height').text)
names=["aeroplane","bicycle","bird","boat","bottle","bus","car",
"cat","chair","cow","diningtable","dog","horse","motorbike","person",
"pottedplant","sheep","sofa","train","tvmonitor"]
for obj in root.iter('object'):
cls = obj.find('name').text
if cls in names and int(obj.find('difficult').text) != 1:
xmlbox = obj.find('bndbox')
bb = convert_box((w, h), [float(xmlbox.find(x).text) for x in ('xmin', 'xmax', 'ymin', 'ymax')])
cls_id = names.index(cls) # class id
out_file.write(" ".join([str(a) for a in (cls_id, *bb)]) + '\n')
# Download
dir = Path("../datasets/VOC")
url = 'https://github.com/ultralytics/yolov5/releases/download/v1.0/'
urls = [f'{
url}VOCtrainval_06-Nov-2007.zip', # 446MB, 5012 images
f'{
url}VOCtest_06-Nov-2007.zip', # 438MB, 4953 images
f'{
url}VOCtrainval_11-May-2012.zip'] # 1.95GB, 17126 images
download(urls, dir=dir / 'images', delete=False, curl=True, threads=3)
# Convert
path = dir / 'images/VOCdevkit'
for year, image_set in ('2012', 'train'), ('2012', 'val'), ('2007', 'train'), ('2007', 'val'), ('2007', 'test'):
imgs_path = dir / 'images' / f'{
image_set}{
year}'
lbs_path = dir / 'labels' / f'{
image_set}{
year}'
imgs_path.mkdir(exist_ok=True, parents=True)
lbs_path.mkdir(exist_ok=True, parents=True)
with open(path / f'VOC{
year}/ImageSets/Main/{
image_set}.txt') as f:
image_ids = f.read().strip().split()
for id in tqdm(image_ids, desc=f'{
image_set}{
year}'):
f = path / f'VOC{
year}/JPEGImages/{
id}.jpg' # old img path
lb_path = (lbs_path / f.name).with_suffix('.txt') # new label path
f.rename(imgs_path / f.name) # move image
convert_label(path, lb_path, year, id) # convert labels to YOLO format
(3) yolov5/utils/general.py と入力し、主にダウンロード プロセスをコメント アウトし、解凍プロセスを直接実行するためにダウンロード関数を変更します。注釈付きのダウンロード関数は図に示されています: (4)、my_tools の下に yolov5 / と入力します
。 , test.py を実行するだけです。実行後、データセットは次のように配置されます。
2. shufflenet に構造を変更します。
1.シャッフルネットV2
ネットワーク構造は次のとおりです。
pytorch の正式な実装は次のとおりです。
import torch
import torch.nn as nn
__all__ = [
'ShuffleNetV2', 'shufflenet_v2_x0_5', 'shufflenet_v2_x1_0',
'shufflenet_v2_x1_5', 'shufflenet_v2_x2_0'
]
model_urls = {
'shufflenetv2_x0.5': 'https://download.pytorch.org/models/shufflenetv2_x0.5-f707e7126e.pth',
'shufflenetv2_x1.0': 'https://download.pytorch.org/models/shufflenetv2_x1-5666bf0f80.pth',
'shufflenetv2_x1.5': None,
'shufflenetv2_x2.0': None,
}
def channel_shuffle(x, groups):
batchsize, num_channels, height, width = x.data.size()
channels_per_group = num_channels // groups
# reshape
x = x.view(batchsize, groups,
channels_per_group, height, width)
x = torch.transpose(x, 1, 2).contiguous()
# flatten
x = x.view(batchsize, -1, height, width)
return x
class InvertedResidual(nn.Module):
def __init__(self, inp, oup, stride):
super(InvertedResidual, self).__init__()
if not (1 <= stride <= 3):
raise ValueError('illegal stride value')
self.stride = stride
branch_features = oup // 2
assert (self.stride != 1) or (inp == branch_features << 1)
if self.stride > 1:
self.branch1 = nn.Sequential(
self.depthwise_conv(inp, inp, kernel_size=3, stride=self.stride, padding=1),
nn.BatchNorm2d(inp),
nn.Conv2d(inp, branch_features, kernel_size=1, stride=1, padding=0, bias=False),
nn.BatchNorm2d(branch_features),
nn.ReLU(inplace=True),
)
self.branch2 = nn.Sequential(
nn.Conv2d(inp if (self.stride > 1) else branch_features,
branch_features, kernel_size=1, stride=1, padding=0, bias=False),
nn.BatchNorm2d(branch_features),
nn.ReLU(inplace=True),
self.depthwise_conv(branch_features, branch_features, kernel_size=3, stride=self.stride, padding=1),
nn.BatchNorm2d(branch_features),
nn.Conv2d(branch_features, branch_features, kernel_size=1, stride=1, padding=0, bias=False),
nn.BatchNorm2d(branch_features),
nn.ReLU(inplace=True),
)
@staticmethod
def depthwise_conv(i, o, kernel_size, stride=1, padding=0, bias=False):
return nn.Conv2d(i, o, kernel_size, stride, padding, bias=bias, groups=i)
def forward(self, x):
if self.stride == 1:
x1, x2 = x.chunk(2, dim=1)
out = torch.cat((x1, self.branch2(x2)), dim=1)
else:
out = torch.cat((self.branch1(x), self.branch2(x)), dim=1)
out = channel_shuffle(out, 2)
return out
class ShuffleNetV2(nn.Module):
def __init__(self, stages_repeats, stages_out_channels, num_classes=1000):
super(ShuffleNetV2, self).__init__()
if len(stages_repeats) != 3:
raise ValueError('expected stages_repeats as list of 3 positive ints')
if len(stages_out_channels) != 5:
raise ValueError('expected stages_out_channels as list of 5 positive ints')
self._stage_out_channels = stages_out_channels
input_channels = 3
output_channels = self._stage_out_channels[0]
self.conv1 = nn.Sequential(
nn.Conv2d(input_channels, output_channels, 3, 2, 1, bias=False),
nn.BatchNorm2d(output_channels),
nn.ReLU(inplace=True),
)
input_channels = output_channels
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
stage_names = ['stage{}'.format(i) for i in [2, 3, 4]]
for name, repeats, output_channels in zip(
stage_names, stages_repeats, self._stage_out_channels[1:]):
seq = [InvertedResidual(input_channels, output_channels, 2)]
for i in range(repeats - 1):
seq.append(InvertedResidual(output_channels, output_channels, 1))
setattr(self, name, nn.Sequential(*seq))
input_channels = output_channels
output_channels = self._stage_out_channels[-1]
self.conv5 = nn.Sequential(
nn.Conv2d(input_channels, output_channels, 1, 1, 0, bias=False),
nn.BatchNorm2d(output_channels),
nn.ReLU(inplace=True),
)
self.fc = nn.Linear(output_channels, num_classes)
def forward(self, x):
x = self.conv1(x)
x = self.maxpool(x)
x = self.stage2(x)
x = self.stage3(x)
x = self.stage4(x)
x = self.conv5(x)
x = x.mean([2, 3]) # globalpool
x = self.fc(x)
return x
def _shufflenetv2(arch, pretrained, progress, *args, **kwargs):
model = ShuffleNetV2(*args, **kwargs)
if pretrained:
model_url = model_urls[arch]
if model_url is None:
raise NotImplementedError('pretrained {} is not supported as of now'.format(arch))
else:
state_dict = load_state_dict_from_url(model_url, progress=progress)
model.load_state_dict(state_dict)
return model
def shufflenet_v2_x0_5(pretrained=False, progress=True, **kwargs):
"""
Constructs a ShuffleNetV2 with 0.5x output channels, as described in
`"ShuffleNet V2: Practical Guidelines for Efficient CNN Architecture Design"
<https://arxiv.org/abs/1807.11164>`_.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
return _shufflenetv2('shufflenetv2_x0.5', pretrained, progress,
[4, 8, 4], [24, 48, 96, 192, 1024], **kwargs)
def shufflenet_v2_x1_0(pretrained=False, progress=True, **kwargs):
"""
Constructs a ShuffleNetV2 with 1.0x output channels, as described in
`"ShuffleNet V2: Practical Guidelines for Efficient CNN Architecture Design"
<https://arxiv.org/abs/1807.11164>`_.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
return _shufflenetv2('shufflenetv2_x1.0', pretrained, progress,
[4, 8, 4], [24, 116, 232, 464, 1024], **kwargs)
def shufflenet_v2_x1_5(pretrained=False, progress=True, **kwargs):
"""
Constructs a ShuffleNetV2 with 1.5x output channels, as described in
`"ShuffleNet V2: Practical Guidelines for Efficient CNN Architecture Design"
<https://arxiv.org/abs/1807.11164>`_.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
return _shufflenetv2('shufflenetv2_x1.5', pretrained, progress,
[4, 8, 4], [24, 176, 352, 704, 1024], **kwargs)
def shufflenet_v2_x2_0(pretrained=False, progress=True, **kwargs):
"""
Constructs a ShuffleNetV2 with 2.0x output channels, as described in
`"ShuffleNet V2: Practical Guidelines for Efficient CNN Architecture Design"
<https://arxiv.org/abs/1807.11164>`_.
Args:
pretrained (bool): If True, returns a model pre-trained on ImageNet
progress (bool): If True, displays a progress bar of the download to stderr
"""
return _shufflenetv2('shufflenetv2_x2.0', pretrained, progress,
[4, 8, 4], [24, 244, 488, 976, 2048], **kwargs)
2. yaml ファイルのバックボーンを純粋な shufflenet に変更します
yolov5s.yaml ファイルをコピーし、yolov5_shufflenet.yaml という名前を付けます。ここでは、VOC データセットをトレーニングのベンチマークとして使用するため、次のように nc を 20 に変更する必要があります。
# Parameters
nc: 20 # number of classes
depth_multiple: 0.33 # model depth multiple
width_multiple: 0.50 # 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
# YOLOv5 v6.0 backbone
backbone:
# [from, number, module, args]
[
[-1, 1, CRM, [32]], # 0-P2/4
[-1, 1, InvertedResidual, [128,2]],
[-1, 3, InvertedResidual, [128,1]], # 2-P3/8
[-1, 1, InvertedResidual, [256,2]],
[-1, 7, InvertedResidual, [256,1]], # 4-P4/16
[-1, 1, InvertedResidual, [512,2]],
[-1, 3, InvertedResidual, [512,1]], # 6-P5/32
]
# YOLOv5 v6.0 head
head:
[[-1, 1, Conv, [256, 1, 1]],
[-1, 1, nn.Upsample, [None, 2, 'nearest']],
[[-1, 4], 1, Concat, [1]], # cat backbone P4
[-1, 3, C3, [256, False]], # 10
[-1, 1, Conv, [256, 1, 1]],
[-1, 1, nn.Upsample, [None, 2, 'nearest']],
[[-1, 2], 1, Concat, [1]], # cat backbone P3
[-1, 3, C3, [128, False]], # 14 (P3/8-small)
[-1, 1, Conv, [128, 3, 2]],
[[-1, 11], 1, Concat, [1]], # cat head P4
[-1, 3, C3, [256, False]], # 17 (P4/16-medium)
[-1, 1, Conv, [256, 3, 2]],
[[-1, 7], 1, Concat, [1]], # cat head P5
[-1, 3, C3, [512, False]], # 20 (P5/32-large)
[[14, 17, 20], 1, Detect, [nc, anchors]], # Detect(P3, P4, P5)
]
3. 公式コードの変更
ここでは、次のように、不足している関数を指定されたファイルに追加する必要があります。
(1) 次のように、yolov5/models/common.py に関数を追加します。
#Conv+Relu+MaxPool
class CRM(nn.Module):
def __init__(self,c1,c2,k=3,s=2):
super(CRM, self).__init__()
self.conv1=nn.Sequential(
nn.Conv2d(c1,c2,k,s,padding=1,bias=False),
nn.BatchNorm2d(c2),
nn.ReLU(inplace=True),
)
self.mp=nn.MaxPool2d(kernel_size=3,stride=2,padding=1)
def forward(self,x):
res=self.mp(self.conv1(x))
return res
#打乱通道
def channel_shuffle(x,groups):
#shuffleBlock
class InvertedResidual(nn.Module):
このうち、channel_shuffle 関数と InvertedResidual クラスは 2.1 からそのままコピーできます。
(2) yolov5/yolo.py の parse_model 関数を変更します。最新のコードでは、下図に示すように、2.3.(1) で追加した関数を 319 行目あたりに追加する必要があります。
次のように変更されます
weights:指定为空
cfg:指定路径为新增的yolov5_shufflenet.yaml路径
data:指定路径为yolov5/data下的VOC.yaml即可,前提是数据拜访要和上方一、2.2、(3)截图的部分相同
それからトレーニングする
3. 対照実験(バックボーンをステムブロック+シャッフルネットに変更)
1. ステムブロック構造
構造図は次のとおりです。
この構造は、yolov5-face の以前の論文で見つけた yolov5 の Focus を置き換えるものです。焦点はモデルの精度を向上させることではなく、ダウンサンプリングの目的を達成し、計算量を減らし、速度を向上させることですが、問題はダウンサンプリング時間が長すぎ、一部のデバイスにはあまり適していないことです。 。yolov5-face によって提案されたステムブロック構造では、最終的な出力サイズは入力の 1/4 になり、必要なダウンサンプリングは 1 回だけです。詳細な参照リンクは次のとおりです。
注目の参照リンク
StemBlock
コードの実装は次のとおりです。
#StemBlock结构
class StemBlock(nn.Module):
def __init__(self, c1, c2, k=3, s=2, p=None, g=1, d=1, act=True):
super(StemBlock, self).__init__()
self.stem_1=Conv(c1,c2,k,s,p,g,act)
self.stem_2a=Conv(c2,c2//2,1,1)
self.stem_2b=Conv(c2//2,c2,3,2)
self.stem_2c=nn.MaxPool2d(2,2,ceil_mode=True)
self.stem_3=Conv(c2*2,c2,1,1)
def forward(self,x):
res1=self.stem_1(x)
res2_a=self.stem_2a(res1)
res2_b=self.stem_2b(res2_a)
res2_c=self.stem_2c(res1)
cat_res=torch.cat((res2_b,res2_c),dim=1)
out=self.stem_3(cat_res)
return out
2. yaml ファイルを変更する
yolov5s.yaml ファイルを同じディレクトリにコピーし、名前を yolov5_stem_shufflenet.yaml に変更します。ここでは、VOC データセットもトレーニング セットとして使用されます。詳細は次のとおりです。
# Parameters
nc: 20 # number of classes
depth_multiple: 0.33 # model depth multiple
width_multiple: 0.50 # 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
# YOLOv5 v6.0 backbone
backbone:
# [from, number, module, args]
[[-1, 1, StemBlock, [64]], # 0-P1/2
[-1, 1, CRM, [128]], # 1-P2/4
[-1, 1, InvertedResidual, [256,2]],
[-1, 3, InvertedResidual, [256,1]], # 3-P3/8
[-1, 1, InvertedResidual, [512,2]],
[-1, 7, InvertedResidual, [512,1]], # 5-P4/16
[-1, 1, InvertedResidual, [1024,2]],
[-1, 3, InvertedResidual, [1024,1]], # 7-P5/32
]
# YOLOv5 v6.0 head
head:
[[-1, 1, Conv, [512, 1, 1]],
[-1, 1, nn.Upsample, [None, 2, 'nearest']],
[[-1, 5], 1, Concat, [1]], # cat backbone P4
[-1, 3, C3, [512, False]], # 11
[-1, 1, Conv, [256, 1, 1]],
[-1, 1, nn.Upsample, [None, 2, 'nearest']],
[[-1, 3], 1, Concat, [1]], # cat backbone P3
[-1, 3, C3, [256, False]], # 15 (P3/8-small)
[-1, 1, Conv, [256, 3, 2]],
[[-1, 12], 1, Concat, [1]], # cat head P4
[-1, 3, C3, [512, False]], # 18 (P4/16-medium)
[-1, 1, Conv, [512, 3, 2]],
[[-1, 8], 1, Concat, [1]], # cat head P5
[-1, 3, C3, [1024, False]], # 21 (P5/32-large)
[[15, 18, 21], 1, Detect, [nc, anchors]], # Detect(P3, P4, P5)
]
3. 関連するコードの変更
(1) 3.1 の StemBlock の実装コードを yolov5/models/common.py ファイルにコピーします。既に持っている場合は、CRM、channel_shuffle 関数、および 2.3.(1) の InvertedResidual クラスもコピーする必要があることに注意してください。 (2) と 2、3はスキップできます。
(2)、これらの関数を yolo.py の約 319 行で定義する必要があります
(3) train.py の変更は 2.3 と同じです。
4. 研修結果
これは 300 エポックのトレーニング後の結果です。オプティマイザー、学習率、その他の設定は同じです。
1. インデックスの比較
ネットワーク構造 | P | R | mAP_0.5 | mAP_0.5:0.95 | モデルサイズ |
---|---|---|---|---|---|
yolov5-シャッフルネット | 0.56 | 0.54 | 0.54 | 0.28 | 2.0M |
yolov5-stem_shufflenet | 0.72 | 0.56 | 0.62 | 0.37 | 6.7M |
トレーニング指標の観点からは後者の方がわずかに優れていますが、モデルのサイズの観点からは前者の方が優れています。次にテスト結果のグラフを見てみましょう
2. 画像テスト
次の写真は、左側がシャッフルネット、右側がステム+シャッフルネットです。
図から分かるように、ここではこれらの図のみを示しています。
・基本的に前者よりも後者の方が信頼度が高い
・どちらもそれぞれ誤検出はあるが、テスト時に閾値を調整することでどちらもフィルタリングできる など、
それぞれに利点があると言える。
参考リンク:マジックチェンジYOLOv5
要約する
以上がこの記事の全内容ですが、ご不明な点がございましたら、QQ グループ 995760755 に参加して一緒にコミュニケーションをとることもできます。