Cannot find input layer of keras model when loading

Viewed 796

I've trained a Sequential model in TensorFlow 2.3.1 with Keras in Python resulting in a saved_model.pb file, but when I try to make use of the model in Go, I don't see the input or output layers I specified, only the Bias and Kernel ones.

Python code which trains the model:

import numpy as np
import pandas as pd

import tensorflow as tf

from tensorflow import feature_column
from tensorflow.keras import layers
from sklearn.model_selection import train_test_split
from sklearn.utils import shuffle
from sklearn import preprocessing
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Input, Dense

# A utility method to create a tf.data dataset from a Pandas Dataframe
def dataframe_to_dataset(dataframe, shuffle=True, batch_size=32):
    dataframe = dataframe.copy()
    labels = dataframe.pop("URLType")
    ds = tf.data.Dataset.from_tensor_slices((dict(dataframe), labels))
    if shuffle:
        ds = ds.shuffle(buffer_size=len(dataframe), seed=0)

    ds = ds.batch(batch_size)

    return ds, labels

# load full data list from csv file
csvList = pd.read_csv("./training-data.csv", sep='\t')

# split the list into training and test data lists
trainingList, _ = train_test_split(csvList, test_size=0.2, random_state=0)
# split the training data list into validation and test data lists
trainingList, validationList = train_test_split(
    trainingList, test_size=0.2, random_state=0
)

# print the length of the individual lists
print("training: %d\nvalidation: %d" % (len(trainingList), len(validationList)))

# set up empty list for feature columns
feature_columns = []

# add numeric columns to feature columns
for header in ["ExcessLength", "PathTokens", "NumericPathTokens", "QueryTokens", "NumericQueryTokens", "LongestNumber"]:
    feature_columns.append(feature_column.numeric_column(header))
    
# create datasets of the training and validation dataframes
batch_size = 32

trainDataset, _ = dataframe_to_dataset(
    trainingList, shuffle=False, batch_size=batch_size
)

validationDataset, _ = dataframe_to_dataset(
    validationList, shuffle=False, batch_size=batch_size
)

# set up the model layers
model = tf.keras.Sequential(
    [
        layers.DenseFeatures(feature_columns, name="layer_input"),
        layers.Dense(128, activation="elu", name="layer_1"),
        layers.Dense(128, activation="elu", name="layer_2"),
        layers.Dense(128, activation="elu", name="layer_3"),
        layers.Dense(1, name="layer_label"),
    ]
)

# compile the model
model.compile(
    optimizer="adam",
    loss=tf.keras.losses.Hinge(),
    metrics=["accuracy"],
)

# callback to stop training early if the loss starts to increase
earlyStopCallback = tf.keras.callbacks.EarlyStopping(monitor='loss', patience=3)

# train the model
model.fit(trainDataset, validation_data=validationDataset, epochs=5, shuffle=False, callbacks=[earlyStopCallback])

# save the model
model.save("./url_recognizer")
tf.keras.models.save_model(
    model=model,
    filepath='./url_recognizer',
    overwrite=True,
    include_optimizer=True,
    save_format='tf')

Then when I load the model in Go, and list all the operations, there is no trace of the input layer.


import (
    "fmt"

    tf "github.com/tensorflow/tensorflow/tensorflow/go"
)

func main() {

    model, err := tf.LoadSavedModel("url_recognizer", []string{"serve"}, nil)

    if err != nil {
        fmt.Printf("Error loading saved model: %s\n", err.Error())
        return
    }

    defer model.Session.Close()

    ops := model.Graph.Operations()

    for _, op := range ops {
        fmt.Println(op.Name())
    }
}

Results in:

sequential/layer_1/kernel
sequential/layer_1/kernel/Read/ReadVariableOp
sequential/layer_1/bias
sequential/layer_1/bias/Read/ReadVariableOp
sequential/layer_2/kernel
sequential/layer_2/kernel/Read/ReadVariableOp
sequential/layer_2/bias
sequential/layer_2/bias/Read/ReadVariableOp
sequential/layer_3/kernel
sequential/layer_3/kernel/Read/ReadVariableOp
sequential/layer_3/bias
sequential/layer_3/bias/Read/ReadVariableOp
sequential/layer_label/kernel
sequential/layer_label/kernel/Read/ReadVariableOp
sequential/layer_label/bias
sequential/layer_label/bias/Read/ReadVariableOp
Adam/iter
Adam/iter/Read/ReadVariableOp
Adam/beta_1
Adam/beta_1/Read/ReadVariableOp
Adam/beta_2
Adam/beta_2/Read/ReadVariableOp
Adam/decay
Adam/decay/Read/ReadVariableOp
Adam/learning_rate
Adam/learning_rate/Read/ReadVariableOp
total
total/Read/ReadVariableOp
count
count/Read/ReadVariableOp
total_1
total_1/Read/ReadVariableOp
count_1
count_1/Read/ReadVariableOp
Adam/sequential/layer_1/kernel/m
Adam/sequential/layer_1/kernel/m/Read/ReadVariableOp
Adam/sequential/layer_1/bias/m
Adam/sequential/layer_1/bias/m/Read/ReadVariableOp
Adam/sequential/layer_2/kernel/m
Adam/sequential/layer_2/kernel/m/Read/ReadVariableOp
Adam/sequential/layer_2/bias/m
Adam/sequential/layer_2/bias/m/Read/ReadVariableOp
Adam/sequential/layer_3/kernel/m
Adam/sequential/layer_3/kernel/m/Read/ReadVariableOp
Adam/sequential/layer_3/bias/m
Adam/sequential/layer_3/bias/m/Read/ReadVariableOp
Adam/sequential/layer_label/kernel/m
Adam/sequential/layer_label/kernel/m/Read/ReadVariableOp
Adam/sequential/layer_label/bias/m
Adam/sequential/layer_label/bias/m/Read/ReadVariableOp
Adam/sequential/layer_1/kernel/v
Adam/sequential/layer_1/kernel/v/Read/ReadVariableOp
Adam/sequential/layer_1/bias/v
Adam/sequential/layer_1/bias/v/Read/ReadVariableOp
Adam/sequential/layer_2/kernel/v
Adam/sequential/layer_2/kernel/v/Read/ReadVariableOp
Adam/sequential/layer_2/bias/v
Adam/sequential/layer_2/bias/v/Read/ReadVariableOp
Adam/sequential/layer_3/kernel/v
Adam/sequential/layer_3/kernel/v/Read/ReadVariableOp
Adam/sequential/layer_3/bias/v
Adam/sequential/layer_3/bias/v/Read/ReadVariableOp
Adam/sequential/layer_label/kernel/v
Adam/sequential/layer_label/kernel/v/Read/ReadVariableOp
Adam/sequential/layer_label/bias/v
Adam/sequential/layer_label/bias/v/Read/ReadVariableOp
NoOp
Const
serving_default_Ending
serving_default_ExcessLength
serving_default_HasArticleToken
serving_default_HasDate
serving_default_IsFile
serving_default_LongestNumber
serving_default_NumericPathTokens
serving_default_NumericQueryTokens
serving_default_OriginalURL
serving_default_PathTokens
serving_default_QueryTokens
StatefulPartitionedCall
saver_filename
StatefulPartitionedCall_1
StatefulPartitionedCall_2

All the sample codes I found online show that I should be able to see an operation with the name layer_input or layer_input_input, but I see none of this.

I suspect the issue might be on the Python side, as loading other pre-built models with this method in Go I see the appropriate input layers among the Operations when I list them.

I'm using the following versions of Python and libraries:

  • Python 3.8.5
  • TensorFlow 2.3.1
  • pandas 1.1.2
  • numpy 1.18.5
1 Answers

Finding out the Input Signature

The Input layers of keras are a bit different from the others. In your case, your DenseFeatures layer is used to define the default serving signature of the model, i.e the input that you should provide to it.

If we look at the output of your go sample program, we see those lines:

serving_default_Ending
serving_default_ExcessLength
serving_default_HasArticleToken
serving_default_HasDate
serving_default_IsFile
serving_default_LongestNumber
serving_default_NumericPathTokens
serving_default_NumericQueryTokens
serving_default_OriginalURL
serving_default_PathTokens
serving_default_QueryTokens

Those operations are actually the input of your model. They are inferred automatically when using a tf.keras.Model as stated in the documentation :

Tools like TensorFlow Serving and saved_model_cli can interact with SavedModels. To help these tools determine which ConcreteFunctions to use, you need to specify serving signatures. tf.keras.Models automatically specify serving signatures, but you'll have to explicitly declare a serving signature for our custom modules.

When saving your model, the signature of your model (the inputs that you have to provide in order to make a prediction) was automatically inferred from the DenseFeatures.

Finding out the output signature

You will also need to provide the output Tensor to be able to fetch the operation through the Graph. It was surprisingly difficult to find the name of the operation to run. A good trick is to use the saved_model_cli utilities from tensorflow serving.

Running the utility :

$ saved_model_cli show --dir ./url_recognizer --tag_set serve --signature_def serving_default
The given SavedModel SignatureDef contains the following input(s):
  inputs['ExcessLength'] tensor_info:
      dtype: DT_INT32
      shape: (-1, 1)
      name: serving_default_ExcessLength:0

  [...]

The given SavedModel SignatureDef contains the following output(s):
  outputs['output_1'] tensor_info:
      dtype: DT_FLOAT
      shape: (-1, 1)
      name: StatefulPartitionedCall:0
Method name is: tensorflow/serving/predict

We see that the name of the OP that give the output of the model is StatefulPartitionedCall.

Running a prediction

If you want to run a prediction, you will have to provide your input using those "serving_default" keys, and the Output Tensor using the output key. Then a call to model.Session.Run()

So we managed to gather the following informations :

  • The inputs are named serving_default_<Name of the feature column>
  • The inputs have a batch size (shape (-1,1)). It means that the model will expect an array for each input.
  • The output is StatefulPartitionedCall

With those, we can build a running example: (partial only: I omitted the other inputs to avoid repetition, but the pattern is pretty easy to replicate).

// defining inputs
excessLength, err := tf.NewTensor([1]int32{0})
// otherInput, err := tf.NewTensor([1]int32{0})

// Calling session Run
output, err := model.Session.Run(
    map[tf.Output]*tf.Tensor{
        model.Graph.Operation("serving_default_ExcessLength").Output(0): excessLength,
        // model.Graph.Operation("serving_default_<OtherInput>").Output(0): otherInput,
    },
    []tf.Output{
        model.Graph.Operation("StatefulPartitionedCall").Output(0),
    },
    nil)
if err != nil {
    fmt.Printf("Error using the model: %s\n", err.Error())
    return
}

// Getting the result of the run throigh the graph
val := output[0].Value()   
fmt.Printf("Output : %f\n", val)


Note that the behavior is similar when using an Input layer instead.

 # set up the model layers
slp = tf.keras.Sequential(
    [
        # layers.DenseFeatures(feature_columns, name="layer_input"),
        layers.Input(shape=(1), name='layer_input'),
        layers.Dense(1, name="layer_label", activation='sigmoid'),
    ]
)

# compile the model
slp.compile(
    optimizer="adam", loss='mse', metrics=["accuracy"],
)
slp.save('./url_recognizer')

And your go program outputs:

layer_label/kernel
layer_label/kernel/Read/ReadVariableOp
layer_label/bias
layer_label/bias/Read/ReadVariableOp
NoOp
Const
serving_default_layer_input
StatefulPartitionedCall
saver_filename
StatefulPartitionedCall_1

Notice the serving_default_layer_input line.

Related