乐趣区

关于网络:一文带你熟悉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 模型转换》,原文作者:杜甫盖房子。

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

退出移动版