我们在训练好自己的yolov5模型后,需要对模型进行部署,大多是将torch转为onnx格式进行使用.但在部署之前需要对转的onnx模型进行精度上的测试,看看和torch下的精度是否一致,如果不一致或差距较大还需要进一步的调整.
导出为onnx格式在yolov5/export.py就有实现,输入命令:
python export.py --data data/mydata.yaml --weights [你的torch权重路径]
将会在你的项目中生成对应的onnx模型.可以通过Netron对onnx模型进行可视化.如果你不想下载Netron,可以访问网页版的Netron.
我这里将提供三种方式来测试转化后的onnx精度.
这种方式最为简单粗暴,可以直接对比onnx和torch的输出,看两者的输出是否一致即可,不过该方法有一定的局限性,测试不够全面,但可以用来做参考.下面附上代码:
- import onnxruntime
- import torch
- from models.experimental import attempt_load
- import numpy as np
- def to_numpy(tensor):
- return tensor.detach().cpu().numpy() if tensor.requires_grad else tensor.cpu().numpy()
-
- onnx_weights = '' # onnx权重路径
- torch_weights = '' # torch权重路径
- session = onnxruntime.InferenceSession(onnx_weights,providers=['CUDAExecutionProvider', 'CPUExecutionProvider'])
- model = attempt_load(torch_weights)
-
- input1 = torch.randn(1, 3, 640, 640) # tensor
- img = input1.numpy().astype(np.float32) # array
- model.eval()
- with torch.no_grad():
- torch_output = model(input1)[0]
- # get yolov5 onnx ouput
- onnx_output = torch.tensor(session.run([session.get_outputs()[0].name], {session.get_inputs()[0].name: img}))
- # 判断输出结果是否一致,小数点后3位一致即可
- print(np.testing.assert_almost_equal(to_numpy(torch_output), onnx_output[0], decimal=3))
这里使用随机输入input1进行测试,看torch下的模型和onnx模型输出是否一致.判断方法为采用np.testing.assert_almost_equal进行测试,判断输出的小数点后三位,如果一致,输出结果为None.
方法1的精度测试可以直接看onnx和torch的输出是否一致,但这方法有一定的局限性,因此我们可以用onnx来测试mAP.[用onnx测试mAP在yolov5 6.0中是没有该功能能,因此我们需要修改代码]
在yolo中的后处理其实有很多,比如输出后通过置信度和NMS的滤除就输出后处理.但这里的后处理并不是指NMS的后处理,而是指对三个特征图解码的部分.也就是models/yolo.py中的Detect中的下面这部分对应的代码:
- if not self.training: # inference 解码,即有后处理
- if self.grid[i].shape[2:4] != x[i].shape[2:4] or self.onnx_dynamic:
- self.grid[i], self.anchor_grid[i] = self._make_grid(nx, ny, i)
-
- y = x[i].sigmoid()
- if self.inplace:
- y[..., 0:2] = (y[..., 0:2] * 2. - 0.5 + self.grid[i]) * self.stride[i] # xy
- y[..., 2:4] = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i] # wh
- else: # for YOLOv5 on AWS Inferentia https://github.com/ultralytics/yolov5/pull/2953
- xy = (y[..., 0:2] * 2. - 0.5 + self.grid[i]) * self.stride[i] # xy
- wh = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i] # wh
- y = torch.cat((xy, wh, y[..., 4:]), -1)
- z.append(y.view(bs, -1, self.no))
- return x if self.training else (torch.cat(z, 1), x)
通过export.py导出onnx模型后修改val.py,我们将用onnx模型来测试mAP.
修改1:加载模型
- # Load model
- if onnx:
- check_requirements(('onnx', 'onnxruntime'))
- import onnxruntime
- session = onnxruntime.InferenceSession(str(weights), None)
-
- gs = 64
修改2:图像预处理
- if onnx:
- img = img.numpy().astype(np.float32)
- names = {k: v for k, v in enumerate(data['names'])}
- else: # torch img:torch.uint8
- img = img.to(device, non_blocking=True)
- img = img.half() if half else img.float() # uint8 to fp16/32
- img /= 255.0 # 0 - 255 to 0.0 - 1.0 图像的归一化操作
- targets = targets.to(device) # get标签
- nb, _, height, width = img.shape # batch size, channels, height, width
- t2 = time_sync()
- dt[0] += t2 - t1 # 图像处理时间
修改3:获得模型的输出
这里的Post_proc是用来判断是否加入后处理的,加入后处理的我将会在方法3中写出来.
- if pt:
- out, train_out = model(img, augment=augment) # inference and training outputs
- elif onnx:
- if not Post_proc:
- out = torch.tensor(session.run([session.get_outputs()[0].name], {session.get_inputs()[0].name: img}))
这里可以看到session就是我们前面定义好的onnx模型,可以通过get_outputs()和get_inputs()获取输入和输出,因为在yolov5中的输出其实是有两个,一个是通过cat后的拼接输出,一个是列表形式[包含了三个head].
我们需要获得是cat后的输出结果,因此这里是get_outputs()[0],而输入只有1个,也就是在export中定义的名字为'images'的输入.[这个名字翻看export.py中就可以看到]
最后将上面的out送入NMS计算即可:
out = non_max_suppression(out, conf_thres, iou_thres, labels=lb, multi_label=True, agnostic=single_cls)
我们即可获得onnx模型下的mAP结果.然后用该结果和torch下的mAP进行对比.
方法2是对三个head解码后处理的操作进行mAP的测试,在方法3中是不进行后处理进行精度的测试.可能会想为什么要进行这个操作呢?有方法2不就可以了吗?是这样的,如果你方法2测出的精度和torch是一样,其实大可不比做方法3,但假如你精度不对,那怎么排查呢,我们就可以通过从输出往前一步一步的排查,看看那里不一样,因此我们可以通过方法3看看网络的输出的精度是否一致.
方法也很简单,修改models/yolo.py中的后处理部分代码,如下:
我们只需要获得三个head的sigmoid输出即可.
- def forward(self, x): # x是list
- z = [] # inference output
- for i in range(self.nl):
- x[i] = self.m[i](x[i]) # conv
- bs, _, ny, nx = x[i].shape # x(bs,255,20,20) to x(bs,3,20,20,85)
- x[i] = x[i].view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2).contiguous()
- if not self.training:
- x[i] = x[i].sigmoid()
- return x
- # if not self.training: # inference 解码,即有后处理
- # if self.grid[i].shape[2:4] != x[i].shape[2:4] or self.onnx_dynamic:
- # self.grid[i], self.anchor_grid[i] = self._make_grid(nx, ny, i)
- #
- # y = x[i].sigmoid()
- # if self.inplace:
- # y[..., 0:2] = (y[..., 0:2] * 2. - 0.5 + self.grid[i]) * self.stride[i] # xy
- # y[..., 2:4] = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i] # wh
- # else: # for YOLOv5 on AWS Inferentia https://github.com/ultralytics/yolov5/pull/2953
- # xy = (y[..., 0:2] * 2. - 0.5 + self.grid[i]) * self.stride[i] # xy
- # wh = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i] # wh
- # y = torch.cat((xy, wh, y[..., 4:]), -1)
- # z.append(y.view(bs, -1, self.no))
- # return x if self.training else (torch.cat(z, 1), x)
然后在val.py中修改如下代码,获取onnx模型的输出:
- elif onnx:
- if not Post_proc:
- out = torch.tensor(session.run([session.get_outputs()[0].name], {session.get_inputs()[0].name: img}))
- else:
- from tools.make_grid import _make_grid
- # ---------------------------------不加后处理---------------------------------------------------
- out1 = torch.tensor(session.run([session.get_outputs()[0].name], {session.get_inputs()[0].name: img}))
- out2 = torch.tensor(session.run([session.get_outputs()[1].name], {session.get_inputs()[0].name: img}))
- out3 = torch.tensor(session.run([session.get_outputs()[2].name], {session.get_inputs()[0].name: img}))
- out = (out1,out2,out3)
-
- grid = [torch.zeros(1)] * 3 # init grid
- anchor_grid = [torch.zeros(1)] * 3
- stride = torch.tensor([8.,16.,32.])
- z = []
- for i in range(3):
- _, bs, _, ny, nx, no = out[i].shape
- if grid[i].shape[2:4] != out[i][0].shape[2:4]:
- grid[i], anchor_grid[i] = _make_grid(stride, nx, ny, i)
- y = out[i][0]
- y[..., 0:2] = (y[..., 0:2] * 2. - 0.5 + grid[i]) * stride[i] # xy
- y[..., 2:4] = (y[..., 2:4] * 2) ** 2 * anchor_grid[i] # wh
- z.append(y.view(bs, -1, no))
- out = torch.cat(z, 1)
- # --------------------------------------------------------------------------------------------
- dt[1] += time_sync() - t2 # 模型推理时间
以上三种精度测试均可以看onnx和torch下的精度是否一致,第一种是看两者输出是否一致[但这个不够全面],后两种是通过mAP判断精度是否一致,需要看具体需求.