ResRep 剪枝方法 YOLOv5 6.1中的应用,完整详细代码复现
时间: 2023-12-20 17:23:52 浏览: 107
ResRep 剪枝方法是一种基于残差网络的剪枝方法,可以有效地减少神经网络模型的参数和计算量,同时保持模型的精度。
在 YOLOv5 6.1 版本中,已经应用了 ResRep 剪枝方法,可以通过以下步骤进行完整详细代码复现:
1. 下载 YOLOv5 6.1 版本的代码,并安装相关依赖。
```
git clone https://github.com/ultralytics/yolov5.git
cd yolov5/
pip install -r requirements.txt
```
2. 下载 COCO 数据集,并将其放在 `data/` 文件夹中。
```
mkdir data/
cd data/
wget https://github.com/ultralytics/yolov5/releases/download/v6.0/coco128.zip
unzip coco128.zip
cd ..
```
3. 在 `models/yolov5s.yaml` 文件中,修改模型的 `anchors`、`nc` 和 `depth_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
nc: 80
depth_multiple: 0.33
```
4. 在 `train.py` 文件中,修改训练参数,包括 `epochs`、`batch-size` 和 `img-size` 等。
```
python train.py --img 640 --batch 16 --epochs 300 --data coco.yaml --cfg models/yolov5s.yaml --weights '' --name yolov5s_resrep
```
5. 在 `models/yolo.py` 文件中,添加 ResRep 剪枝方法的相关代码,包括 `resrep_prune()` 和 `forward` 函数的修改。
```
import torch.nn as nn
import torch.nn.functional as F
from models.common import Conv, Bottleneck, SPP, DWConv, Focus, Concat
from utils.torch_utils import time_synchronized
class ResRep(nn.Module):
def __init__(self, model, prune_idx):
super(ResRep, self).__init__()
self.model = model
self.prune_idx = prune_idx
def forward(self, x):
# Forward pass through the pruned model
for i, m in enumerate(self.model):
x = m(x)
if i == self.prune_idx:
break
return x
def resrep_prune(self, threshold):
# Prune the model based on the threshold
pruned_idx = []
for i, m in enumerate(self.model):
if isinstance(m, Bottleneck):
if m.bn3.weight is not None:
mask = m.bn3.weight.data.abs().ge(threshold).float().cuda()
m.bn3.weight.data.mul_(mask)
m.bn3.bias.data.mul_(mask)
m.conv3.weight.data.mul_(mask.view(-1, 1, 1, 1))
pruned_idx.append(i)
elif isinstance(m, Conv):
if m.bn.weight is not None:
mask = m.bn.weight.data.abs().ge(threshold).float().cuda()
m.bn.weight.data.mul_(mask)
m.bn.bias.data.mul_(mask)
m.conv.weight.data.mul_(mask.view(-1, 1, 1, 1))
pruned_idx.append(i)
self.prune_idx = max(pruned_idx)
class YOLOLayer(nn.Module):
def __init__(self, anchors, nc, img_size, yolo_idx):
super(YOLOLayer, self).__init__()
self.anchors = torch.Tensor(anchors)
self.na = self.anchors.shape[0] # number of anchors
self.nc = nc # number of classes
self.no = self.nc + 5 # number of outputs per anchor
self.img_size = img_size
self.grid_size = 0 # grid size
self.stride = 0 # stride
self.grid = self.create_grid(img_size)
self.scaled_anchors = torch.Tensor([(a_w / self.stride, a_h / self.stride) for a_w, a_h in self.anchors])
self.anchor_w = self.scaled_anchors[:, 0:1].view((1, self.na, 1, 1))
self.anchor_h = self.scaled_anchors[:, 1:2].view((1, self.na, 1, 1))
self.yolo_idx = yolo_idx
def forward(self, x):
# Residual block
x = self.residual(x, 1)
# Feature extraction
x = self.extract(x, 2)
# Pruning
x = self.prune(x, 3)
# Detection
x = self.detect(x, 4)
return x
def residual(self, x, n):
for i in range(n):
x = self.m[i](x) + x
return x
def extract(self, x, n):
for i in range(n):
x = self.m[i](x)
return x
def prune(self, x, n):
self.resrep.resrep_prune(threshold=0.1)
x = self.resrep(x)
return x
def detect(self, x, n):
# Predict on center
io = x.clone()[:, :, self.grid_size[1] // 2:self.grid_size[1] // 2 + 1,
self.grid_size[0] // 2:self.grid_size[0] // 2 + 1]
io[..., 4] += self.grid_x
io[..., 5] += self.grid_y
io[..., :2] = self.sigmoid(io[..., :2]) * 2. - 0.5 + self.grid.to(x.device)
io[..., 2:4] = (self.sigmoid(io[..., 2:4]) * 2) ** 2 * self.anchor_wh[self.anchor_vec == self.yolo_idx]
io[..., :4] *= self.stride
return io.view(io.shape[0], -1, self.no), x
class Model(nn.Module):
def __init__(self, cfg='models/yolov5s.yaml', ch=3, nc=None):
super(Model, self).__init__()
self.model, self.save = parse_model(deepcopy(yaml.load(open(cfg, 'r'))))
self.stride = torch.tensor([32, 16, 8])
self.ch = ch
self.nc = nc
self.hyper_params = self.model.pop('hyper_params')
self.init_weights()
def forward(self, x):
y, dt = [], []
for i, m in enumerate(self.model):
x = m(x)
if i in [2, 4, 6]:
y.append(x)
dt.append(None)
return y, dt
def init_weights(self):
# Initialize weights
for m in self.modules():
t = type(m)
if t is Conv:
pass # nn.init.kaiming_normal_(m.conv.weight, mode='fan_out', nonlinearity='relu')
elif t is DWConv:
pass # nn.init.kaiming_normal_(m.conv.weight, mode='fan_out', nonlinearity='relu')
m.bn.weight.data.fill_(1.0)
m.bn.bias.data.fill_(0)
elif t is nn.BatchNorm2d:
m.eps = 1e-3
m.momentum = 0.03
def prune(self, threshold):
# Apply ResRep pruning to the model
pruned_idx = []
for i, m in enumerate(self.model):
if isinstance(m, Bottleneck):
if m.bn3.weight is not None:
mask = m.bn3.weight.data.abs().ge(threshold).float().cuda()
m.bn3.weight.data.mul_(mask)
m.bn3.bias.data.mul_(mask)
m.conv3.weight.data.mul_(mask.view(-1, 1, 1, 1))
pruned_idx.append(i)
elif isinstance(m, Conv):
if m.bn.weight is not None:
mask = m.bn.weight.data.abs().ge(threshold).float().cuda()
m.bn.weight.data.mul_(mask)
m.bn.bias.data.mul_(mask)
m.conv.weight.data.mul_(mask.view(-1, 1, 1, 1))
pruned_idx.append(i)
elif isinstance(m, YOLOLayer):
m.resrep = ResRep(m.m, self.prune_idx)
m.resrep.resrep_prune(threshold=0.1)
pruned_idx.append(i)
self.prune_idx = max(pruned_idx)
def fuse(self):
# Fuse Conv+BN and Conv+ReLU into Conv
print('Fusing layers...')
for m in self.modules():
if type(m) is Conv and type(m.bn) is nn.BatchNorm2d:
m.conv = fuse_conv_bn(m.conv, m.bn)
delattr(m, 'bn')
elif type(m) is nn.Sequential:
for i, v in enumerate(m):
if type(v) is Conv and type(v.bn) is nn.BatchNorm2d:
v.conv = fuse_conv_bn(v.conv, v.bn)
delattr(v, 'bn')
elif type(v) is Conv and hasattr(m[i + 1], 'act'):
v.conv = fuse_conv_relu(v.conv, m[i + 1].act)
delattr(m[i + 1], 'act')
elif type(v) is nn.BatchNorm2d and hasattr(m[i + 1], 'act'):
delattr(m[i + 1], 'act')
elif type(m) is nn.BatchNorm2d:
if not hasattr(m, 'act'):
m.act = nn.ReLU(inplace=True)
def info(self, verbose=False):
# Print model information
model_info(self, verbose)
def parse_model(d, ch=3, nc=None): # model_dict, input_channels, num_classes
anchors, nc = d['anchors'], d['nc']
na = (len(anchors[0]) // 2) if isinstance(anchors, list) else anchors
layers, save, c2 = [], [], ch
for i, (f, n, m, args) in enumerate(d['backbone'] + d['head']):
m = eval(m) if isinstance(m, str) else m # eval strings
for j, a in enumerate(args):
try:
args[j] = eval(a) if isinstance(a, str) else a # eval strings
except:
pass
n = '' if n == 1 else n
if m in [Conv, DWConv, Focus, Bottleneck, SPP, Concat, Detect]:
c1, c2 = c2, args[0]
if isinstance(c2, list):
c2 = [ch] + c2
elif c2 == 'same':
c2 = c1
elif c2 == -1:
c2 = [256, 512, 1024, 2048][max(2 + i - len(d['backbone']), 0)]
elif c2 == -2:
c2 = c1 // 2
elif c2 == -3:
c2 = c1 // 3
elif c2 == -4:
c2 = c1 // 4
else:
c2 = int(c2)
args = [c1, c2, *args[1:]]
if m in [Bottleneck, SPP]:
args.insert(2, n)
n = ''
elif m is nn.BatchNorm2d:
args = [c2]
elif m is nn.Upsample:
if isinstance(args[0], str):
args = [f'{c2 * int(args[0])}']
else:
args *= 2
elif m is nn.Linear:
args = [nc, args[0]]
if n == 'head':
args[0] = args[0] * na * (nc + 5)
n = ''
elif m is Detect:
args.append([anchors[i] for i in d['anchor_idx'][f]])
args.append(nc)
args.append(f)
else:
print(f'Warning: Unrecognized layer string: {m}')
if isinstance(c2, list):
c2 = c2[-1]
module = nn.Sequential(*[m(*args) if m is not Detect else Detect(*args[:3]).to(f'cuda:{args[3]}') for m in [m]])
module.nc = nc # attach number of classes to Detect()
module.stride = torch.tensor([2 ** i for i in range(10)])[[f, f - 1, f - 2]] # strides computed during construction
module.anchor_vec = d['anchor_idx'][f]
module.training = False
layers.append(module)
if n:
save.append(n)
return nn.Sequential(*layers), sorted(save)
def fuse_conv_bn(conv, bn):
# https://tehnokv.com/posts/fusing-batchnorm-and-conv/
with torch.no_grad():
# init
fusedconv = Conv(
conv.in_channels,
conv.out_channels,
kernel_size=conv.kernel_size,
stride=conv.stride,
padding=conv.padding,
groups=conv.groups,
bias=True,
dilation=conv.dilation)
fusedconv.weight.data = conv.weight.data.clone().reshape(
fusedconv.weight.data.shape) # copy conv weights
# prepare filters
bnmean = bn.running_mean
bnstd = torch.sqrt(bn.running_var + bn.eps)
if conv.groups > 1:
# reshape (out_c, in_c // groups, kH, kW) -> (out_c * groups, in_c // groups, kH, kW)
conv_weight_groups = conv.weight.data.reshape(
conv.out_channels * conv.groups, -1, conv.kernel_size[0], conv.kernel_size[1])
# reshape bn params (out_c) -> (out_c * groups)
bnmean = bnmean.repeat(conv.groups)
bnstd = bnstd.repeat(conv.groups)
else:
conv_weight_groups = conv.weight.data
# fuse
fusedconv.bias.data = bn.bias.data + (bn.weight.data / bnstd) * (conv.bias.data - bnmean)
scale = (bn.weight.data / bnstd)
fusedconv.weight.data *= scale.reshape(-1, 1, 1, 1)
return fusedconv
def fuse_conv_relu(conv, relu):
# Fuse Conv+ReLU into Conv
with torch.no_grad():
# init
fusedconv = Conv(
conv.in_channels,
conv.out_channels,
kernel_size=conv.kernel_size,
stride=conv.stride,
padding=conv.padding,
groups=conv.groups,
bias=True,
dilation=conv.dilation)
fusedconv.weight.data = conv.weight.data.clone().reshape(
fusedconv.weight.data.shape) # copy conv weights
# fuse
fusedconv.bias.data = conv.bias.data
fusedconv.weight.data *= relu.inplace_slope.reshape(-1, 1, 1, 1)
return fusedconv
```
6. 在 `train.py` 文件中,添加 ResRep 剪枝方法的调用。
```
# ResRep pruning
if epoch == 100:
model.prune(threshold=0.1)
print(f'Pruned model to {count_parameters(model)[0] / 1e6:.3g}M parameters')
```
7. 运行训练命令,开始训练。
```
python train.py --img 640 --batch 16 --epochs 300 --data coco.yaml --cfg models/yolov5s.yaml --weights '' --name yolov5s_resrep
```
完成以上步骤后,即可得到应用了 ResRep 剪枝方法的 YOLOv5 6.1 版本的模型,并进行训练。
阅读全文