CoreML:为 ONNX RandomNormal 创建自定义层

Posted

技术标签:

【中文标题】CoreML:为 ONNX RandomNormal 创建自定义层【英文标题】:CoreML: creating a custom layer for ONNX RandomNormal 【发布时间】:2019-07-10 03:33:04 【问题描述】:

我已经在 PyTorch 中训练了一个 VAE,我需要将其转换为 CoreML。从这个线程PyTorch VAE fails conversion to onnx 我能够导出 ONNX 模型,但是,这只是将问题进一步推到了 ONNX-CoreML 阶段。

包含torch.randn()调用的原始函数是reparametrize func:

def reparametrize(self, mu, logvar):
    std = logvar.mul(0.5).exp_()
    if self.have_cuda:
        eps = torch.randn(self.bs, self.nz, device='cuda')
    else:
        eps = torch.randn(self.bs, self.nz)
    return eps.mul(std).add_(mu)

当然,解决方案是创建一个自定义层,但我在创建一个没有输入的层时遇到了问题(即,它只是一个 randn() 调用)。

我可以用这个 def 完成 CoreML 转换:

def convert_randn(node):
    params = NeuralNetwork_pb2.CustomLayerParams()
    params.className = "RandomNormal"
    params.description = "Random normal distribution generator"
    params.parameters["dtype"].intValue = node.attrs.get('dtype', 1)
    params.parameters["bs"].intValue = node.attrs.get("shape")[0]
    params.parameters["nz"].intValue = node.attrs.get("shape")[1]
    return params

我使用以下方法进行转换:

coreml_model = convert(onnx_model, add_custom_layers=True, 
    image_input_names = ['input'], 
    custom_conversion_functions="RandomNormal": convert_randn)

我还应该注意,在完成mlmodel 导出时,会打印以下内容:

Custom layers have been added to the CoreML model corresponding to the 
following ops in the onnx model: 
1/1: op type: RandomNormal, op input names and shapes: [], op output     
names and shapes: [('62', 'Shape not available')]

.mlmodel 带入Xcode 抱怨Layer '62' of type 500 has 0 inputs but expects at least 1. 所以我想知道如何为层指定一种“虚拟”输入,因为它实际上没有输入——它只是一个包装器torch.randn()(或者,更具体地说,onnx RandonNormal 操作)。我应该澄清一下,我确实需要整个 VAE,而不仅仅是解码器,因为我实际上是在使用整个过程来“纠正”我的输入(即编码器根据输入估计我的 z 向量,然后解码器生成输入的最接近的可泛化预测)。

非常感谢任何帮助。

更新:好的,我终于得到了一个可以在 Xcode 中加载的版本(感谢@MattijsHollemans 和他的书!)。 originalConversion.mlmodel 是将我的模型从 ONNX 转换为 CoreML 的初始输出。为此,我必须手动插入RandomNormal 层的输入。我无缘无故地做到了 (64, 28, 28) - 我知道我的批量大小是 64,我的输入是 28 x 28(但大概它也可能是 (1, 1, 1),因为它是一个“虚拟"):

spec = coremltools.utils.load_spec('originalConversion.mlmodel')
nn = spec.neuralNetwork
layers = l.name:i for i,l in enumerate(nn.layers)
layer_idx = layers["62"] # '62' is the name of the layer -- see above
layer = nn.layers[layer_idx]
layer.input.extend(["dummy_input"])

inp = spec.description.input.add()
inp.name = "dummy_input"
inp.type.multiArrayType.SetInParent()
spec.description.input[1].type.multiArrayType.shape.append(64)
spec.description.input[1].type.multiArrayType.shape.append(28)
spec.description.input[1].type.multiArrayType.shape.append(28)
spec.description.input[1].type.multiArrayType.dataType = ft.ArrayFeatureType.DOUBLE

coremltools.utils.save_spec(spec, "modelWithInsertedInput.mlmodel") 

这会在 Xcode 中加载,但我还没有在我的应用程序中测试模型的功能。由于附加层很简单,而且输入实际上是一个虚假的、非功能性输入(只是为了让 Xcode 开心),我不认为这会是一个问题,但如果它确实会再次发布'不正常运行。

更新 2:不幸的是,模型不会在运行时加载。它以[espresso] [Espresso::handle_ex_plan] exception=Failed in 2nd reshape after missing custom layer info. 失败我发现非常 奇怪和令人困惑的是,检查model.espresso.shape,我发现几乎每个节点都有如下形状:

"62" : 
  "k" : 0,
  "w" : 0,
  "n" : 0,
  "seq" : 0,
  "h" : 0

我有两个问题/担忧:1) 最明显的是,为什么所有值都为零(除了输入节点之外的所有值都是这种情况),以及 2) 为什么它看起来是一个顺序模型,而它只是一个相当传统的VAE?在同一个应用程序中打开 model.espresso.shape 以获得功能齐全的 GAN,我看到节点的格式为:

"54" : 
  "k" : 256,
  "w" : 16,
  "n" : 1,
  "h" : 16

也就是说,它们包含合理的形状信息,并且它们没有具有seq 字段。

非常非常困惑……

更新 3: 我也刚刚注意到编译器报告错误:IMPORTANT: new sequence length computation failed, falling back to old path. Your compilation was sucessful, but please file a radar on Core ML | Neural Networks and attach the model that generated this message.

这是原始的 PyTorch 模型:

class VAE(nn.Module):
def __init__(self, bs, nz):
    super(VAE, self).__init__()

    self.nz = nz
    self.bs = bs

    self.encoder = nn.Sequential(
        # input is (nc) x 28 x 28
        nn.Conv2d(nc, ndf, 4, 2, 1, bias=False),
        nn.LeakyReLU(0.2, inplace=True),
        # size = (ndf) x 14 x 14
        nn.Conv2d(ndf, ndf * 2, 4, 2, 1, bias=False),
        nn.BatchNorm2d(ndf * 2),
        nn.LeakyReLU(0.2, inplace=True),
        # size = (ndf*2) x 7 x 7
        nn.Conv2d(ndf * 2, ndf * 4, 3, 2, 1, bias=False),
        nn.BatchNorm2d(ndf * 4),
        nn.LeakyReLU(0.2, inplace=True),
        # size = (ndf*4) x 4 x 4
        nn.Conv2d(ndf * 4, 1024, 4, 1, 0, bias=False),
        nn.LeakyReLU(0.2, inplace=True),
    )

    self.decoder = nn.Sequential(
        # input is Z, going into a convolution
        nn.ConvTranspose2d(     1024, ngf * 8, 4, 1, 0, bias=False),
        nn.BatchNorm2d(ngf * 8),
        nn.ReLU(True),
        # size = (ngf*8) x 4 x 4
        nn.ConvTranspose2d(ngf * 8, ngf * 4, 3, 2, 1, bias=False),
        nn.BatchNorm2d(ngf * 4),
        nn.ReLU(True),
        # size = (ngf*4) x 8 x 8
        nn.ConvTranspose2d(ngf * 4, ngf * 2, 4, 2, 1, bias=False),
        nn.BatchNorm2d(ngf * 2),
        nn.ReLU(True),
        # size = (ngf*2) x 16 x 16
        nn.ConvTranspose2d(ngf * 2,     nc, 4, 2, 1, bias=False),
        nn.Sigmoid()
    )

    self.fc1 = nn.Linear(1024, 512)
    self.fc21 = nn.Linear(512, nz)
    self.fc22 = nn.Linear(512, nz)

    self.fc3 = nn.Linear(nz, 512)
    self.fc4 = nn.Linear(512, 1024)

    self.lrelu = nn.LeakyReLU()
    self.relu = nn.ReLU()

def encode(self, x):
    conv = self.encoder(x);
    h1 = self.fc1(conv.view(-1, 1024))
    return self.fc21(h1), self.fc22(h1)

def decode(self, z):
    h3 = self.relu(self.fc3(z))
    deconv_input = self.fc4(h3)
    deconv_input = deconv_input.view(-1,1024,1,1)
    return self.decoder(deconv_input)

def reparametrize(self, mu, logvar):
    std = logvar.mul(0.5).exp_()
    eps = torch.randn(self.bs, self.nz, device='cuda') # needs custom layer!
    return eps.mul(std).add_(mu)

def forward(self, x):
    # print("x", x.size())
    mu, logvar = self.encode(x)
    z = self.reparametrize(mu, logvar)
    decoded = self.decode(z)
    return decoded, mu, logvar

【问题讨论】:

Core ML 中有一些层类型,例如load constant,它们没有输入,但我从未尝试过自定义层。当您在 Netron 等工具中打开 Core ML 模型时,图表会是什么样子? 我当然可以尝试load constant 层。我研究一下,谢谢。这是 Netron 中模型的那一部分:dropbox.com/s/p6ash42i4acqtnd/custom_layer_netron.png?dl=0 好的,我尝试为reparametrize 制作一个自定义层,但它只是给我带来了同样的错误,因为它确实与 ONNX 的 RandomNormal 有关,而不是 my i> 自定义层。我不明白为什么会出现这样的问题;当然生成随机张量并不是最不寻常的事情......奇怪。 那么当前的错误是什么? Core ML 说您的自定义层需要输入?如果是这样,请使用与自定义层相同的名称向模型添加一个输入。您可以使用 coremltools 加载模型并更改“spec”对象。 看起来图层的名称是62,因此您还需要添加一个名为62 的输入。但是重命名图层会更好。我将在下面的答案中添加答案(是的,它也在书中)。 【参考方案1】:

要向您的 Core ML 模型添加输入,您可以从 Python 执行以下操作:

import coremltools
spec = coremltools.utils.load_spec("YourModel.mlmodel")

nn = spec.neuralNetworkClassifier  # or just spec.neuralNetwork

layers = l.name:i for i,l in enumerate(nn.layers)
layer_idx = layers["your_custom_layer"]
layer = nn.layers[layer_idx]
layer.input.extend(["dummy_input"])

inp = spec.description.input.add()
inp.name = "dummy_input"
inp.type.doubleType.SetInParent()

coremltools.utils.save_spec(spec, "NewModel.mlmodel")

这里,"your_custom_layer" 是您要添加虚拟输入的层的名称。在您的模型中,它看起来被称为62。您可以查看layers 字典以查看模型中所有层的名称。

注意事项:

如果您的模型不是分类器,请使用 nn = spec.neuralNetwork 而不是 neuralNetworkClassifier。 我将新的虚拟输入设为“双”类型。这意味着您的自定义层会获得一个双精度值作为输入。 您需要在使用模型时为此虚拟输入指定一个值。

【讨论】:

非常感谢!这转换了我在 Xcode 中的错误:validator error: Neural Networks require inputs to be images or MLMultiArray. 我可以在 Netron 中看到“dummy_input”。可能只是改变类型的问题吗? 我已将此标记为正确,因为这篇文章和 Mattijs 的书都让我找到了答案。但是,您必须指定 dummy_input 的形状和数据类型,否则它不会在 Xcode 中打开(在我上次的更新中有详细说明)。 是的,输入的数据类型和形状需要与您的自定义层期望的数据类型相匹配。 好的,我收到了:[espresso] [Espresso::handle_ex_plan] exception=Failed in 2nd reshape after missing custom layer info. 我想知道我错过了哪个custom layer info 不可能在没有实际看到您的模型文件的情况下说。 ;-)

以上是关于CoreML:为 ONNX RandomNormal 创建自定义层的主要内容,如果未能解决你的问题,请参考以下文章

没有先验层和 softmax 层的 YOLACT onnx 的 xrunc coreml 模型错误

使用 MLMultiArray 作为 CoreML 模型的输出

将 PyTorch 模型与 CoreML 一起使用时输入尺寸重塑

CoreML 模型在 coremltools 和 Xcode 之间产生不同的结果

caffe 到 onnx 的 libcaffeconverter 导入错误

从 pytorch 模型转换而来的 coreML 模型给出了错误的预测概率