极简yolov5转torchscript

极简yolov5转torchscript–以yolov5s为例

最近在使用yolov5转torchscript模型做推理时遇到了这样一个问题:
RuntimeError: Expected all tensors to be on the same device, but found at least two devices, cuda:0 and cpu!
不是所有tensor都在同个设备上,但是检查了model的weights和input都是在一个设备上,就很疑惑,后来发现是yolov5推理时的detect
层的融框操作导致。

首先我们来看一般的torchscript模型转换操作

import torch
from models.experimental import attempt_load

weight = 'weights/yolov5s.pt'
jit_weight = weight.replace('.pt', '.jit')

x = torch.rand([1, 3, 640, 640])
device = torch.device('cuda:0')

torch_model = attempt_load(weight, map_location=device)
x = x.to(device)

torch.jit.trace(torch_model, x).save(jit_weight)

这种一般的转法,yolov5只能在你转模型时设置的device上运行(其他不复杂的模型不会出现这个问题),比如这次设置的是’cuda:0’,那模型就只能在0号gpu上运行,如果device=torch.device(‘cpu’),那就只能在cpu上运行,之所以出现这种情况,是因为yolov5推理和训练时的输出不一样,详见models/yolo.py中的Detect类中的forword方法

class Detect(nn.Module):
    stride = None  # strides computed during build
    onnx_dynamic = False  # ONNX export parameter

    def __init__(self, nc=80, anchors=(), ch=(), inplace=True):  # detection layer
        super(Detect, self).__init__()

        self.nc = nc  # number of classes
        self.no = nc + 5  # number of outputs per anchor
        self.nl = len(anchors)  # number of detection layers
        self.na = len(anchors[0]) // 2  # number of anchors
        self.grid = [torch.zeros(1)] * self.nl  # init grid
        a = torch.tensor(anchors).float().view(self.nl, -1, 2)
        self.register_buffer('anchors', a)  # shape(nl,na,2)
        self.register_buffer('anchor_grid', a.clone().view(self.nl, 1, -1, 1, 1, 2))  # shape(nl,1,na,1,1,2)
        self.m = nn.ModuleList(nn.Conv2d(x, self.no * self.na, 1) for x in ch)  # output conv
        self.inplace = inplace  # use in-place ops (e.g. slice assignment)

    def forward(self, x):
        # x = x.copy()  # for profiling
        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:  # inference
                if self.grid[i].shape[2:4] != x[i].shape[2:4] or self.onnx_dynamic:
                    self.grid[i] = self._make_grid(nx, ny).to(x[i].device)

                y = x[i].sigmoid()
                if self.inplace:
                # if True:
                    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].view(1, self.na, 1, 1, 2)  # wh
                    y = torch.cat((xy, wh, y[..., 4:]), -1)
                z.append(y.view(bs, -1, self.no))

        '''
        训练时输出为x,其为一个list,list中有三个tensor(这里以yolov5为例)
        x[0].shape = (bs,3,80,80,nc+5)
        x[1].shape = (bs,3,40,40,nc+5)
        x[2].shape = (bs,3,20,20,nc+5)
        bs为batch_size
        nc为类别数

        推理时输出为(torch.cat(z, 1), x)
        其中x与训练时的x相同
        torch.cat(z,1)为融合后的预测框
        '''
        return x if self.training else (torch.cat(z, 1), x)

    @staticmethod
    def _make_grid(nx=20, ny=20):
        yv, xv = torch.meshgrid([torch.arange(ny), torch.arange(nx)])
        return torch.stack((xv, yv), 2).view((1, 1, ny, nx, 2)).float()

forward方法中有个self.training变量,用来控制训练和推理时是否执行if not self.training:中的融框操作,
if not self.training:中的self.anchor_grid,self.grid不在模型结构中,所以在你转化时其所处的设备位置不会变化,
即你在使用torchscript模型时model.to(device)操作不会改变self.anchor_grid,self.grid的device,
它们的device仍然是模型转换时所设置的device

比如

import torch
from models.experimental import attempt_load

weight = 'weights/yolov5s.pt'
jit_weight = weight.replace('.pt', '.jit')

x = torch.rand([1, 3, 640, 640])
pre_device = torch.device('cuda:0')

torch_model = attempt_load(weight, map_location=pre_device)
x = x.to(pre_device)

torch.jit.trace(torch_model, x).save(jit_weight)

model = torch.jit.load(jit_weights)

device = torch.device('cuda:0')

model = model.to(device)

此时,model中的参数在device='cpu’中,而self.anchor_grid和self.grid都在pre_device='cuda:0’中,所以会报错:RuntimeError: Expected all tensors to be on the same device, but found at least two devices, cuda:0 and cpu!

解决方法

在转换模型前将self.training设置为True

self.training = True
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._make_grid(nx, ny).to(x[i].device)

然后用numpy重写推理时的融合大中小框的操作,这里仍然以yolov5s为例,anchor_grid为先验框,在models/yolov5s.yaml,
这里的80,40,20为最后大中小不同物体的检验格子的大小,这里以输入图片的尺度为6406403为例,不同的输入尺度可能不同


def numpy_detect(x,nc=None):
    # anchor_grid为先验眶
    anchor_grid = [10.0, 13.0, 16.0, 30.0, 33.0, 23.0, 30.0, 61.0, 62.0, 45.0, 59.0, 119.0, 116.0, 90.0, 156.0, 198.0, 373.0, 326.0]
    anchor_grid = np.array(anchor_grid).reshape(3,1,-1,1,1,2)
    stride = np.array([8, 16, 32])
    grid = [make_grid(80,80), make_grid(40,40), make_grid(20,20)]
    
    z = []
    
    for i in range(3):
        y = numpy_sigmoid(x[i])

        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.reshape(1, -1, nc + 5))
    
    res = np.concatenate(z, 1)

    return res

def numpy_sigmoid(x):
    return 1/(1+np.exp(-x))

def make_grid(nx=20,ny=20):
    xv,yv = np.meshgrid(np.arange(nx), np.arange(ny))
    res = np.stack((xv,yv), 2).reshape(1,1,nx,ny,2).astype(np.float32)
    return res

def img_pad(img, size, pad_value=[114,114,114]):
    H,W,_ = img.shape
    r = max(H/size[0], W/size[1])
    img_r = cv2.resize(img, (int(W/r), int(H/r)))
    tb = size[0] - img_r.shape[0]
    lr = size[1] - img_r.shape[1]
    top = int(tb/2)
    bottom = tb - top
    left = int(lr/2)
    right = lr - left
    pad_image = cv2.copyMakeBorder(img_r, top, bottom, left, right, cv2.BORDER_CONSTANT,value=pad_value)
    return pad_image

实例如下:

jit_weights = 'weights/yolov5s.jit'
model = torch.jit.load(jit_weights)

imgpath = 'test.jpg
im0 = cv2.imread(imgpath)

img = img_pad(im0, size=self.input_hw)[0]
img = img[:, :, ::-1].transpose(2, 0, 1)
img = np.ascontiguousarray(img, dtype=np.float32)

img = torch.from_numpy(img).to(self.device)
img /= 255.0
if img.ndimension() == 3:
    img = img.unsqueeze(0)

pred = model(img)

pred = [x.data.cpu().numpy() for x in pred]
pred = numpy_detect(pred, self.nc)
pred = torch.tensor(pred).to(self.device)

pred = non_max_suppression(pred, self.conf_thres, self.nms_thres)

  • 0
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要查看YOLOv5的训练过程,可以按照以下步骤进行操作。首先,将训练所用的数据分别放入`Annotations`和`images`文件夹中。然后,删除一些文件,但请注意只删除文件而不要删除文件夹。接下来,运行`split_train_val.py`和`voc_label.py`文件重新生成之前删除的文件。确认`train.py`文件中的训练配置,并运行该文件开始训练。训练过程完成后,可以记录训练结果。\[1\] 如果在训练过程中有任何变动,比如数据集发生变化或文件夹重命名,需要重新生成一些文件。可以依次运行`split_train_val.py`和`voc_label.py`文件重新生成之前删除的文件。然后,确认`train.py`文件中的训练配置,并再次运行该文件进行训练。\[2\] 请注意,以上步骤是基于YOLOv5的一般流程,具体操作可能会因个人需求和环境而有所不同。希望这些步骤对您有所帮助。\[3\] #### 引用[.reference_title] - *1* *2* [YOLOv5训练&检测流程](https://blog.csdn.net/qq_38251616/article/details/124165074)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [极简YOLOv5训练以及检测流程(ubuntu)](https://blog.csdn.net/qq_52135902/article/details/130485789)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值