附上代码
https://github.com/qqsuhao/YOLOv3-YOLOv3-tiny-yolo-fastest-xl–pytorch
本篇博客我们重点讲解如何将“YOLOv3 从入门到部署(二)”中构建的模型转换为onnx。关于onnx的介绍,读者可以查询其他资料。本章节讲述的内容需要大量参考YOLOv3 从入门到部署(二)。
DNN模块是opencv的一个深度学习推理模块,我们使用的是opencv 4.5.1。我们最终要使用DNN模块加载我们导出的onnx。要完成这件事我们必须要确保两件事:onnx的导出过程是成功的;onnx可以被DNN正确加载。为什么这么说呢?因为DNN模块尚不完整,有很多onnx中的网络层DNN是不支持的,这会导致即使onnx导出成功,也无法被DNN正确加载。
pytorch导出onnx有很多资料,但是我也相信很多人在将自己的模型导出为onnx的时候总是会遇到各种各样的问题。我把自己的一些经验和解决办法总结如下(有些经验不只是为了成功导出onnx,也是为了让DNN正确加载onnx):
self.stride = torch.floor_divide(self.img_dim, self.grid_size)
,正常的我们可能会直接使用self.img_dim/self.grid_size然后取整,但是如果要转为onnx,这么写就会报出warning。这是因为在我们的程序中,self.img_dim/是python的int类型,而self.grid_size=inputs.size(2)
是一个tensor。这样的话再运算过程中,tensor会被隐式地转换为python的数据类型,转为onnx的时候会被警告有tensor被转换为其它数据类型。X = x.data + self.grid_x
其中x是一个有着4个维度的tensor,而self.grid_x只是一个二维的tensor,在做加法的时候pytorch会默认使用广播机制,给x的每个元素加上self.grid_x,但是这样的操作在DNN中不被支持。因此我们将其改为
FloatTensor = torch.cuda.FloatTensor if inputs.is_cuda else torch.FloatTensor
X = FloatTensor() # x 和 self.grid_x维度并不完全相同,为了转onnx成功,需要写成这样
for i in range(self.num_anchors):
X = torch.cat( (X, torch.add(x[:, i:i+1, :, :], self.grid_x)), 1)
我们使用for循环逐次和x的元素相加;并且使用torch.add(),而不是普通的加号。
self.grid_x = torch.arange(g).repeat(g, 1).view([1, 1, g, g]).type(FloatTensor)
创建self.grid_x;为了避免使用arange,我们将代码改为如下:
self.grid_x = FloatTensor([i for j in range(self.grid_size) for i in range(self.grid_size)])\
.view([1, 1, self.grid_size, self.grid_size])
不要对切片进行赋值操作:onnx不支持对切片进行赋值操作,因此要避免使用。
onnx支持多输入多输出:onnx支持模型有多个输入和输出。在我们的模型中,输入只有一个,但是输出有两个。尽管我们将两个yolo层的输出结果保存在一个列表中,但是模型输出的tensor是有两个,因此相当于两个输出。
conifg_path = "./configs/yolo-fastest-xl.cfg"
weights_path = "./weights/yolo-fastest-xl.weights"
save_path = "./weights/yolo-fastest-xl.onnx"
net = YOLOv3(conifg_path)
# If specified we start from checkpoint
if weights_path:
if weights_path.endswith(".pth"):
net.load_state_dict(torch.load(weights_path)) # 加载pytorch格式的权重文件
print("load_state_dict")
else:
net.load_darknet_weights(weights_path) # 加载darknet格式的权重文件。以.weight为后缀
print("load_darknet_weights")
net.eval()
inputs = torch.rand(1, 3, 320, 320)
torch.onnx.export(net, inputs, save_path, input_names=["input"], output_names=["outputs0", "outputs1"],
verbose=True, opset_version=11)
model = onnx.load(save_path)
onnx.checker.check_model(model)
我们前面说过需要自行定义DNN中的exp模块,具体方法如下
import
# 添加 ExpLayer
class ExpLayer(object):
def __init__(self, params, blobs):
super(ExpLayer, self).__init__()
def getMemoryShapes(self, inputs):
return inputs
def forward(self, inputs):
return [np.exp(inputs[0])]
cv2.dnn_registerLayer('Exp', ExpLayer)
# opencv dnn加载
net = cv2.dnn.readNetFromONNX(save_path)
img = inputs.numpy() * 255
img = img[0]
img = img.transpose((1, 2, 0))
img = img.astype('uint8')
blob = cv2.dnn.blobFromImage(img, size=(320, 320)) # img 必须是uint8
print(blob.shape)
net.setInput(blob)
out_blob = net.forward(net.getUnconnectedOutLayersNames())
print(out_blob[1].shape)
out = cv2.dnn.imagesFromBlob(out_blob[1])
print(out[0].shape)
目录