0

I've trainined a VAE that in PyTorch that I need to convert to CoreML. From this thread PyTorch VAE fails conversion to onnx I was able to get the ONNX model to export, however, this just pushed the problem one step further to the ONNX-CoreML stage.

The original function that contains the torch.randn() call is the 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)

The solution is, of course, to create a custom layer, but I'm having problems creating a layer with no inputs (i.e., it's just a randn() call).

I can get the CoreML conversion to complete with this def:

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

I do the conversion with:

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

I should also note that, at the completion of the mlmodel export, the following is printed:

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')]

Bringing the .mlmodel into Xcode complains that Layer '62' of type 500 has 0 inputs but expects at least 1. So I'm wondering how to specify a kind of "dummy" input to the layer, since it doesn't actually have an input -- it's just a wrapper around torch.randn() (or, more specifically, the onnx RandonNormal op). I should clarify that I do need the whole VAE, not just the decoder, as I'm actually using the entire process to "error correct" my inputs (i.e., the encoder estimates my z vector, based on an input, then the decoder generates the closest generalizable prediction of the input).

Any help greatly appreciated.

UPDATE: Okay, I finally got a version to load in Xcode (thanks to @MattijsHollemans and his book!). The originalConversion.mlmodel is the initial output of converting my model from ONNX to CoreML. To this, I had to manually insert the input for the RandomNormal layer. I made it (64, 28, 28) for no great reason — I know my batch size is 64, and my inputs are 28 x 28 (but presumably it could also be (1, 1, 1), since it's a "dummy"):

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") 

This loads in Xcode, but I have yet to test the functioning of the model in my app. Since the additional layer is simple, and the input is literally a bogus, non-functional input (just to keep Xcode happy), I don't imagine it will be a problem, but I'll post again if it doesn't run properly.

UPDATE 2: Unfortunately, the model doesn't load at runtime. It fails with [espresso] [Espresso::handle_ex_plan] exception=Failed in 2nd reshape after missing custom layer info. What I find very strange and confusing is that, inspecting model.espresso.shape, I see that almost every node has a shape like:

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

I have two question/concerns: 1) Most obviously, why are all the values zero (this is the case with all but the input nodes), and 2) Why does it appear to be a sequential model, when it's just a fairly conventional VAE? Opening model.espresso.shape for a fully-functioning GAN in the same app, I see that the nodes are of the format:

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

That is, they contain reasonable shape info, and they don't have seq fields.

Very, very confused...

UPDATE 3: I've also just noticed in the compiler report the error: 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.

Here's the original PyTorch model:

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
jbm
  • 1,248
  • 10
  • 22
  • There are layer types in Core ML, such as `load constant` that do not have inputs but I've never tried this for a custom layer. What does the graph look like when you open the Core ML model in a tool such as Netron? – Matthijs Hollemans Feb 16 '19 at 11:30
  • I could certainly try a `load constant` layer. I'll look into it, thanks. Here's that portion of the model in Netron: https://www.dropbox.com/s/p6ash42i4acqtnd/custom_layer_netron.png?dl=0 – jbm Feb 16 '19 at 14:22
  • Okay, I tried making a custom layer for `reparametrize`, but it just brings me to the same error, since it really has to do with ONNX's `RandomNormal`, not _my_ custom layer. I don't understand why this is such a problem; surely generating random tensors isn't the most unusual thing to do... Bizarre. – jbm Feb 16 '19 at 15:20
  • So what is the current error? Core ML saying that your custom layer requires an input? If so, add an input to the model using the same name as the custom layer. You can load the model using coremltools and change the "spec" object. – Matthijs Hollemans Feb 16 '19 at 16:33
  • The error is in Xcode: `validator error: Layer '62' of type 500 has 0 inputs but expects at least 1.` And from the coreml conversion I can see that '62' is the RandomNormal custom layer. By "the same name", do you mean my (e.g.) `params.className = "RandomNormal"`, or the `custom_conversion_function`(i.e., `convert_randn`) – jbm Feb 16 '19 at 16:37
  • "You can load the model using coremltools and change the "spec" object." I'm sorry, but I'm really not sure what that means. Is this covered in your new/recent book? (I'm planning to pick it up!) – jbm Feb 16 '19 at 16:40
  • 1
    It looks like the name of the layer is `62` so you'd also need to add an input named `62`. But it's nicer to rename the layer. I'll add the answer in an answer below (and yes, it's in the book too). – Matthijs Hollemans Feb 16 '19 at 16:41
  • Much appreciated! I do have a general question, though: If I'm going from PyTorch to ONNX to CoreML, does coremltools think of the model as basically keras? (Strange question, I guess, but it doesn't seem obvious to me.) – jbm Feb 16 '19 at 16:59
  • Hmm... is it possible that the dummy_input is being misinterpreted as a sequential input? – jbm Feb 17 '19 at 19:41

1 Answers1

1

To add an input to your Core ML model, you can do the following from 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")

Here, "your_custom_layer" is the name of the layer you want to add the dummy input to. In your model it looks like it's called 62. You can look at the layers dictionary to see the names of all the layers in the model.

Notes:

  • If your model is not a classifier, use nn = spec.neuralNetwork instead of neuralNetworkClassifier.
  • I made the new dummy input have the type "double". That means your custom layer gets a double value as input.
  • You need to specify a value for this dummy input when using the model.
Matthijs Hollemans
  • 7,706
  • 2
  • 16
  • 23
  • Thanks so much! This converted my error in Xcode: `validator error: Neural Networks require inputs to be images or MLMultiArray.` I can see the "dummy_input" in Netron. Is it perhaps just a matter of changing the type? – jbm Feb 16 '19 at 17:25
  • I've marked this as correct because both this post, and Mattijs' book, led me to the answer. However, you do have to specify the shape and dataType of the dummy_input, or it won't open in Xcode (detailed in my last UPDATE). – jbm Feb 16 '19 at 20:49
  • Yes, the datatype and shape of the input needs to match the kind of data your custom layer expects. – Matthijs Hollemans Feb 17 '19 at 12:07
  • Okay, I'm getting: `[espresso] [Espresso::handle_ex_plan] exception=Failed in 2nd reshape after missing custom layer info.` I wonder which `custom layer info` I'm missing? – jbm Feb 17 '19 at 16:05
  • Impossible to say without actually seeing your model file. ;-) – Matthijs Hollemans Feb 17 '19 at 18:50