Training and Predicting with instance keys
Asked Answered
J

2

15

I am able to train my model and use ML Engine for prediction but my results don't include any identifying information. This works fine when submitting one row at a time for prediction but when submitting multiple rows I have no way of connecting the prediction back to the original input data. The GCP documentation discusses using instance keys but I can't find any example code that trains and predicts using an instance key. Taking the GCP census example how would I update the input functions to pass a unique ID through the graph and ignore it during training yet return the unique ID with predictions? Or alternatively if anyone knows of a different example already using keys that would help as well.

From Census Estimator Sample

def serving_input_fn():
    feature_placeholders = {
      column.name: tf.placeholder(column.dtype, [None])
      for column in INPUT_COLUMNS
    }

    features = {
      key: tf.expand_dims(tensor, -1)
      for key, tensor in feature_placeholders.items()
    }

    return input_fn_utils.InputFnOps(
      features,
      None,
      feature_placeholders
    )


def generate_input_fn(filenames,
                  num_epochs=None,
                  shuffle=True,
                  skip_header_lines=0,
                  batch_size=40):

    def _input_fn():
        files = tf.concat([
          tf.train.match_filenames_once(filename)
          for filename in filenames
        ], axis=0)

        filename_queue = tf.train.string_input_producer(
          files, num_epochs=num_epochs, shuffle=shuffle)
        reader = tf.TextLineReader(skip_header_lines=skip_header_lines)

        _, rows = reader.read_up_to(filename_queue, num_records=batch_size)

        row_columns = tf.expand_dims(rows, -1)
        columns = tf.decode_csv(row_columns, record_defaults=CSV_COLUMN_DEFAULTS)
        features = dict(zip(CSV_COLUMNS, columns))

        # Remove unused columns
        for col in UNUSED_COLUMNS:
          features.pop(col)

        if shuffle:
           features = tf.train.shuffle_batch(
             features,
             batch_size,
             capacity=batch_size * 10,
             min_after_dequeue=batch_size*2 + 1,
             num_threads=multiprocessing.cpu_count(),
             enqueue_many=True,
             allow_smaller_final_batch=True
           )
        label_tensor = parse_label_column(features.pop(LABEL_COLUMN))
        return features, label_tensor

    return _input_fn

Update: I was able to use the suggested code from this answer below I just needed to alter it slightly to update the output alternatives in the model_fn_ops instead of just the prediction dict. However, this only works if my serving input function is coded for json inputs similar to this. My serving input function was previously modeled after the CSV serving input function in the Census Core Sample.

I think my problem is coming from the build_standardized_signature_def function and even more so the is_classification_problem function that it calls. The input dict length using the csv serving function is 1 so this logic ends up using the classification_signature_def which only ends up displaying the scores (which turns out are actually the probabilities) whereas the input dict length is greater than 1 with the json serving input function and instead the predict_signature_def is used which includes all of the outputs.

Jordanna answered 6/6, 2017 at 5:20 Comment(1)
This is a known problem with the classification tag in ModelServer (which CMLE uses for inference). In 1.2 The EstimatorSpec lets you choose your own export method, so hopefully that should fix things for you, however it will require a rewrite to use tf.estimator.Estimator rather than tf.contrib.learn.Estimator.Granite
G
8

UPDATE: In version 1.3 the contrib estimators (tf.contrib.learn.DNNClassifier for example), were changed to inherit from the core estimator class tf.estimator.Estimator which unlike it's predecessor, hides the model function as a private class member, so you'll need to replace estimator.model_fn in the solution below with estimator._model_fn.

Josh's answer points you to the Flowers example, which is a good solution if you want to use a custom estimator. If you want to stick with a canned estimator, (e.g. the tf.contrib.learn.DNNClassifiers) you can wrap it in a custom estimator that adds support for keys. (Note: I think it's likely canned estimators will gain key support when they move into core).

KEY = 'key'
def key_model_fn_gen(estimator):
    def _model_fn(features, labels, mode, params):
        key = features.pop(KEY, None)
        model_fn_ops = estimator.model_fn(
           features=features, labels=labels, mode=mode, params=params)
        if key:
            model_fn_ops.predictions[KEY] = key
            # This line makes it so the exported SavedModel will also require a key
            model_fn_ops.output_alternatives[None][1][KEY] = key
        return model_fn_ops
    return _model_fn

my_key_estimator = tf.contrib.learn.Estimator(
    model_fn=key_model_fn_gen(
        tf.contrib.learn.DNNClassifier(model_dir=model_dir...)
    ),
    model_dir=model_dir
)

my_key_estimator can then be used exactly like your DNNClassifier would be used, except it will expect a feature with the name 'key' from input_fns (prediction, evaluation and training).

EDIT2: You will also need to add the corresponding input tensor to the prediction input function of your choice. For example, a new JSON serving input fn would look like:

def json_serving_input_fn():
  inputs = # ... input_dict as before
  inputs[KEY] = tf.placeholder([None], dtype=tf.int64)
  features = # .. feature dict made from input_dict as before
  tf.contrib.learn.InputFnOps(features, None, inputs)

(slightly different between 1.2 and 1.3, as tf.contrib.learn.InputFnOps is replaced with tf.estimator.export.ServingInputReceiver, and padding tensors to rank 2 is no longer necessary in 1.3)

Then ML Engine will send a tensor named "key" with your prediction request, which will be passed to your model, and through with your predictions.

EDIT3: Modified key_model_fn_gen to support ignoring missing key values. EDIT4: Added key for prediction

Granite answered 8/6, 2017 at 18:44 Comment(7)
I've tried implementing this and the code succeeds but when I submit my prediction request through MLE I am only getting back the predicted Scores-which isn't even in the prediction dict? Looking at the estimator class predict() method it seems it should be returning everything in the model_fn_ops prediction dictionary when no output is specified: github.com/tensorflow/tensorflow/blob/master/tensorflow/contrib/… Do you know of any additional changes I would need to make for this to work?Jordanna
I'm assuming you're using Experiment to train? If so what export_strategy are you using?Granite
I am using the experiment. I've added more details about the export strategy to my original posted question.Jordanna
i've got the latest census example working for my problem, have deployed the model and able to get predictions via gcloud in batch and online. But i can't for the life of me figure out how to implement the key's here. Does anyone know of an version of this census example using the estimator api that implemented instance keys?Propagandize
@EliBixby I don't think this solution works anymore. It seems like we can't use estimator.model_fn() anymore (doesn't exist even in TF 1.2) neither estimator._model_fn() (breaks because of empty params). It seems to me that we need to switch to estimator._call_model_fn(), but I can't get the parameters right.Kado
I tried this on tf 1.3 but am getting "AttributeError: 'DNNClassifier' object has no attribute 'model_fn'". Similar to @KadoPropagandize
I've created a separate question for the case i am trying to solve - any help much appreciated. #48543799Propagandize
G
4

Great question. The Cloud ML Engine flowers sample does this, by using the tf.identity operation to pass a string straight through from input to output. Here are the relevant lines during graph construction.

keys_placeholder = tf.placeholder(tf.string, shape=[None])
inputs = {
    'key': keys_placeholder,
    'image_bytes': tensors.input_jpeg
}

# To extract the id, we need to add the identity function.
keys = tf.identity(keys_placeholder)
outputs = {
   'key': keys,
   'prediction': tensors.predictions[0],
   'scores': tensors.predictions[1]
}

For batch prediction you need to insert "key": "some_key_value" into your instance records. For online prediction you would query the above graph with a JSON request like:

{'instances' : [
    {'key': 'first_key', 'image_bytes' : {'b64': ...}}, 
    {'key': 'second_key', 'image_bytes': {'b64': ...}}
    ]
}
Giacopo answered 6/6, 2017 at 15:2 Comment(1)
Thank you! I have been reviewing the flower code and the key seems to be to specify the input and output format and creating a signature. If you are using the pre-canned estimators and the experiment class, is there still a way to get this kind of customization or will it only work if I'm building out the graph myself?Is the export strategies in the experiment the place to do this? I also noticed the Iris example doing something similar within the train monitors argument. Code LinkJordanna

© 2022 - 2024 — McMap. All rights reserved.