关于网络:一文带你熟悉PytorchCaffeom模型转换流程

摘要:本文旨在分享Pytorch->Caffe->om模型转换流程。

规范网络

Baseline:PytorchToCaffe

次要性能代码在:

PytorchToCaffe
+-- Caffe
|   +-- caffe.proto
|   +-- layer_param.py
+-- example
|   +-- resnet_pytorch_2_caffe.py
+-- pytorch_to_caffe.py

间接应用能够参考resnet_pytorch_2_caffe.py,如果网络中的操作Baseline中都曾经实现,则能够间接转换到Caffe模型。

增加自定义操作

如果遇到没有实现的操作,则要分为两种状况来思考。

Caffe中有对应操作

以arg_max为例分享一下增加操作的形式。

首先要查看Caffe中对应层的参数:caffe.proto为对应版本caffe层与参数的定义,能够看到ArgMax定义了out_max_valtop_kaxis三个参数:

message ArgMaxParameter {
  // If true produce pairs (argmax, maxval)
  optional bool out_max_val = 1 [default = false];
  optional uint32 top_k = 2 [default = 1];
  // The axis along which to maximise -- may be negative to index from the
  // end (e.g., -1 for the last axis).
  // By default ArgMaxLayer maximizes over the flattened trailing dimensions
  // for each index of the first / num dimension.
  optional int32 axis = 3;
}

与Caffe算子边界中的参数是统一的。

layer_param.py构建了具体转换时参数类的实例,实现了操作参数从Pytorch到Caffe的传递:

def argmax_param(self, out_max_val=None, top_k=None, dim=1):
    argmax_param = pb.ArgMaxParameter()
    if out_max_val is not None:
        argmax_param.out_max_val = out_max_val
    if top_k is not None:
        argmax_param.top_k = top_k
    if dim is not None:
        argmax_param.axis = dim
    self.param.argmax_param.CopyFrom(argmax_param)

pytorch_to_caffe.py中定义了Rp类,用来实现Pytorch操作到Caffe操作的变换:

class Rp(object):
    def __init__(self, raw, replace, **kwargs):
        self.obj = replace
        self.raw = raw
​
    def __call__(self, *args, **kwargs):
        if not NET_INITTED:
            return self.raw(*args, **kwargs)
        for stack in traceback.walk_stack(None):
            if 'self' in stack[0].f_locals:
                layer = stack[0].f_locals['self']
                if layer in layer_names:
                    log.pytorch_layer_name = layer_names[layer]
                    print('984', layer_names[layer])
                    break
        out = self.obj(self.raw, *args, **kwargs)
        return out

在增加操作时,要应用Rp类替换操作:

torch.argmax = Rp(torch.argmax, torch_argmax)

接下来,要具体实现该操作:

def torch_argmax(raw, input, dim=1):
    x = raw(input, dim=dim)
    layer_name = log.add_layer(name='argmax')
    top_blobs = log.add_blobs([x], name='argmax_blob'.format(type))
    layer = caffe_net.Layer_param(name=layer_name, type='ArgMax',
                                  bottom=[log.blobs(input)], top=top_blobs)
    layer.argmax_param(dim=dim)
    log.cnet.add_layer(layer)
    return x

即实现了argmax操作Pytorch到Caffe的转换。

Caffe中无间接对应操作

如果要转换的操作在Caffe中无间接对应的层实现,解决思路次要有两个:

  1. 在Pytorch中将不反对的操作合成为反对的操作:

    nn.InstanceNorm2d,实例归一化在转换时是用BatchNorm做的,不反对 affine=True 或者track_running_stats=True,默认use_global_stats:false,但om转换时use_global_stats必须为true,所以能够转到Caffe,但再转om不敌对。

    InstanceNorm是在featuremap的每个Channel上进行归一化操作,因而,能够实现nn.InstanceNorm2d为:

    class InstanceNormalization(nn.Module):
        def __init__(self, dim, eps=1e-5):
            super(InstanceNormalization, self).__init__()
            self.gamma = nn.Parameter(torch.FloatTensor(dim))
            self.beta = nn.Parameter(torch.FloatTensor(dim))
            self.eps = eps
            self._reset_parameters()
    ​
        def _reset_parameters(self):
            self.gamma.data.uniform_()
            self.beta.data.zero_()
    ​
        def __call__(self, x):
            n = x.size(2) * x.size(3)
            t = x.view(x.size(0), x.size(1), n)
            mean = torch.mean(t, 2).unsqueeze(2).unsqueeze(3).expand_as(x)
            var = torch.var(t, 2).unsqueeze(2).unsqueeze(3).expand_as(x)
            gamma_broadcast = self.gamma.unsqueeze(1).unsqueeze(1).unsqueeze(0).expand_as(x)
            beta_broadcast = self.beta.unsqueeze(1).unsqueeze(1).unsqueeze(0).expand_as(x)
            out = (x - mean) / torch.sqrt(var + self.eps)
            out = out * gamma_broadcast + beta_broadcast
            return out

    但在验证HiLens Caffe算子边界中发现,om模型转换不反对Channle维度之外的求和或求均值操作,为了躲避这个操作,咱们能够通过反对的算子从新实现nn.InstanceNorm2d

    class InstanceNormalization(nn.Module):
        def __init__(self, dim, eps=1e-5):
            super(InstanceNormalization, self).__init__()
            self.gamma = torch.FloatTensor(dim)
            self.beta = torch.FloatTensor(dim)
            self.eps = eps
            self.adavg = nn.AdaptiveAvgPool2d(1)
    ​
        def forward(self, x):
            n, c, h, w = x.shape
            mean = nn.Upsample(scale_factor=h)(self.adavg(x))
            var = nn.Upsample(scale_factor=h)(self.adavg((x - mean).pow(2)))
            gamma_broadcast = self.gamma.unsqueeze(1).unsqueeze(1).unsqueeze(0).expand_as(x)
            beta_broadcast = self.beta.unsqueeze(1).unsqueeze(1).unsqueeze(0).expand_as(x)
            out = (x - mean) / torch.sqrt(var + self.eps)
            out = out * gamma_broadcast + beta_broadcast
            return out

    通过验证,与原操作等价,能够转为Caffe模型

  2. 在Caffe中通过利用现有操作实现:

    在Pytorch转Caffe的过程中发现,如果存在featuremap + 6这种波及到常数的操作,转换过程中会呈现找不到blob的问题。咱们首先查看pytorch_to_caffe.py中add操作的具体转换方法:

    def _add(input, *args):
        x = raw__add__(input, *args)
        if not NET_INITTED:
            return x
        layer_name = log.add_layer(name='add')
        top_blobs = log.add_blobs([x], name='add_blob')
        if log.blobs(args[0]) == None:
            log.add_blobs([args[0]], name='extra_blob')
        else:
            layer = caffe_net.Layer_param(name=layer_name, type='Eltwise',
                                          bottom=[log.blobs(input),log.blobs(args[0])], top=top_blobs)
            layer.param.eltwise_param.operation = 1 # sum is 1
            log.cnet.add_layer(layer)
        return x

    能够看到对于blob不存在的状况进行了判断,咱们只须要在log.blobs(args[0]) == None条件下进行批改,一个天然的想法是利用Scale层实现add操作:

    def _add(input, *args):
        x = raw__add__(input, *args)
        if not NET_INITTED:
            return x
        layer_name = log.add_layer(name='add')
        top_blobs = log.add_blobs([x], name='add_blob')
        if log.blobs(args[0]) == None:
            layer = caffe_net.Layer_param(name=layer_name, type='Scale',
                                           bottom=[log.blobs(input)], top=top_blobs)
            layer.param.scale_param.bias_term = True
            weight = torch.ones((input.shape[1]))
            bias = torch.tensor(args[0]).squeeze().expand_as(weight)
            layer.add_data(weight.cpu().data.numpy(), bias.cpu().data.numpy())
            log.cnet.add_layer(layer)
        else:
            layer = caffe_net.Layer_param(name=layer_name, type='Eltwise',
                                          bottom=[log.blobs(input), log.blobs(args[0])], top=top_blobs)
            layer.param.eltwise_param.operation = 1  # sum is 1
            log.cnet.add_layer(layer)
        return x

    相似的,featuremap * 6这种简略乘法也能够通过同样的办法实现。

踩过的坑

  • Pooling:Pytorch默认 ceil_mode=false,Caffe默认 ceil_mode=true,可能会导致维度变动,如果呈现尺寸不匹配的问题能够检查一下Pooling参数是否正确。另外,尽管文档上没有看到,然而 kernel_size > 32 后模型尽管能够转换,但推理会报错,这时能够分两层进行Pooling操作。
  • Upsample :om边界算子中的Upsample 层scale_factor参数必须是int,不能是size。如果已有模型参数为size也会失常跑完Pytorch转Caffe的流程,但此时Upsample参数是空的。参数为size的状况能够思考转为scale_factor或用Deconvolution来实现。
  • Transpose2d:Pytorch中 output_padding 参数会加在输入的大小上,但Caffe不会,输入特色图绝对会变小,此时反卷积之后的featuremap会变大一点,能够通过Crop层进行裁剪,使其大小与Pytorch对应层统一。另外,om中反卷积推理速度较慢,最好是不要应用,能够用Upsample+Convolution代替。
  • Pad:Pytorch中Pad操作很多样,但Caffe中只能进行H与W维度上的对称pad,如果Pytorch网络中有h = F.pad(x, (1, 2, 1, 2), "constant", 0)这种不对称的pad操作,解决思路为:

    1. 如果不对称pad的层不存在后续的维度不匹配的问题,能够先判断一下pad对后果的影响,一些工作受pad的影响很小,那么就不须要批改。
    2. 如果存在维度不匹配的问题,能够思考依照较大的参数充沛pad之后进行Crop,或是将前后两个(0, 0, 1, 1)(1, 1, 0, 0)的pad合为一个(1, 1, 1, 1),这要看具体的网络结构确定。
    3. 如果是Channel维度上的pad如F.pad(x, (0, 0, 0, 0, 0, channel_pad), "constant", 0),能够思考零卷积后cat到featuremap上:

      zero = nn.Conv2d(in_channels, self.channel_pad, kernel_size=3, padding=1, bias=False)
      nn.init.constant(self.zero.weight, 0)
      pad_tensor = zero(x)
      x = torch.cat([x, pad_tensor], dim=1)
  • 一些操作能够转到Caffe,但om并不反对规范Caffe的所有操作,如果要再转到om要对照文档确认好边界算子。

本文分享自华为云社区《Pytorch->Caffe模型转换》,原文作者:杜甫盖房子 。

点击关注,第一工夫理解华为云陈腐技术~

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理