摘要:本文旨在分享 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_val
、top_k
、axis
三个参数:
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 中无间接对应的层实现,解决思路次要有两个:
-
在 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 模型
-
在 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 操作,解决思路为:- 如果不对称 pad 的层不存在后续的维度不匹配的问题,能够先判断一下 pad 对后果的影响,一些工作受 pad 的影响很小,那么就不须要批改。
- 如果存在维度不匹配的问题,能够思考依照较大的参数充沛 pad 之后进行 Crop,或是将前后两个
(0, 0, 1, 1)
与(1, 1, 0, 0)
的 pad 合为一个(1, 1, 1, 1)
,这要看具体的网络结构确定。 -
如果是 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 模型转换》,原文作者:杜甫盖房子。
点击关注,第一工夫理解华为云陈腐技术~