Pytorch CoreML:为ONNX创建自定义层

Pytorch CoreML:为ONNX创建自定义层,pytorch,coreml,onnx-coreml,Pytorch,Coreml,Onnx Coreml,我已经在PyTorch中培训了一个VAE,我需要将其转换为CoreML。通过这个线程,我能够导出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,

我已经在PyTorch中培训了一个VAE,我需要将其转换为CoreML。通过这个线程,我能够导出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会抱怨500类型的层“62”有0个输入,但至少需要1个。因此,我想知道如何为层指定一种虚拟输入,因为它实际上没有输入-它只是围绕torch.randn或更具体地说,onnx RandonNormal op的包装。我应该澄清,我确实需要整个VAE,而不仅仅是解码器,因为我实际上在使用整个过程来纠正我的输入错误,即。,编码器根据输入估计我的z向量,然后解码器生成输入的最接近的通用预测

非常感谢您的帮助

更新:好的,多亏@MattijsHollemans和他的书!我终于在Xcode中加载了一个版本!。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=在缺少自定义层信息后第二次重塑失败,因此失败。我发现非常奇怪和困惑的是,检查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:我刚刚在编译器报告中注意到错误:重要:新序列长度计算失败,返回到旧路径。您的编译是成功的,但请在Core ML |神经网络上提交雷达文件,并附上生成此消息的模型

这是原始的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

要向核心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")
在这里,您的_custom_layer是要添加虚拟输入的层的名称。在你的模型中,它看起来像是62。您可以查看图层字典以查看模型中所有图层的名称

注:

如果您的模型不是分类器,请使用nn=spec.neuralNetwork而不是neuralNetworkClassifier。 我使新的虚拟输入具有双重类型。这意味着您的自定义层将获得一个双倍值作为输入。 使用模型时,需要为此虚拟输入指定一个值。
Core ML中有一些层类型,例如load constant,它们没有输入,但我从未对自定义层尝试过这种方法。当您在Netron之类的工具中打开Core ML模型时,图形是什么样子的?我当然可以尝试加载常量层。我会调查的,谢谢。下面是Netron中模型的那部分:好吧,我试着制作一个自定义层来重新参数化,但它只会给我带来相同的错误,因为它实际上与ONNX的RandomNormal有关,而不是我的自定义层。我不明白为什么这是个问题;当然,生成随机张量并不是最不寻常的事情。。。奇怪。那么,目前的错误是什么?Core ML表示您的自定义层需要输入?如果是,请使用与自定义图层相同的名称向模型添加输入。您可以使用coremltools加载模型并更改spec对象。看起来图层的名称是62,因此您还需要添加一个名为62的输入。但是重命名图层更好。我会在下面的答案中添加答案,是的,它也在书中。非常感谢!这转换了我在Xcode中的错误:验证器错误:神经网络要求输入为图像或MLMultiArray。我可以在Netron中看到虚拟输入。这可能只是个问题吗
改变类型?我已将其标记为正确,因为这篇文章和Mattijs的书都引导我找到了答案。但是,您必须指定虚拟_输入的形状和数据类型,否则它将无法在我上次更新中详细介绍的Xcode中打开。是的,输入的数据类型和形状需要与自定义层所需的数据类型相匹配。好的,我得到:[espresso][espresso::handle_ex_plan]exception=在缺少自定义层信息后第二次重塑失败。我想知道我丢失了哪一个自定义图层信息?如果没有实际查看您的模型文件,就无法确定-