6

I'm predicting image classes using Keras. It works in Google Cloud ML (GCML), but for efficiency need change it to pass base64 strings instead of json array. Related Documentation

I can easily run python code to decode a base64 string into json array, but when using GCML I don't have the opportunity to run a preprocessing step (unless maybe use a Lambda layer in Keras, but I don't think that is the correct approach).

Another answer suggested adding tf.placeholder with type of tf.string, which makes sense, but how to incorporate that into the Keras model?

Here is complete code for training the model and saving the exported model for GCML...

import os
import numpy as np
import tensorflow as tf
import keras
from keras import backend as K
from keras.models import Sequential
from keras.layers import Dense, Dropout, Flatten
from keras.layers import Conv2D, MaxPooling2D
from keras.preprocessing import image
from tensorflow.python.platform import gfile

IMAGE_HEIGHT = 138
IMAGE_WIDTH = 106
NUM_CLASSES = 329

def preprocess(filename):
    # decode the image file starting from the filename
    # end up with pixel values that are in the -1, 1 range
    image_contents = tf.read_file(filename)
    image = tf.image.decode_png(image_contents, channels=1)
    image = tf.image.convert_image_dtype(image, dtype=tf.float32) # 0-1
    image = tf.expand_dims(image, 0) # resize_bilinear needs batches
    image = tf.image.resize_bilinear(image, [IMAGE_HEIGHT, IMAGE_WIDTH], align_corners=False)
    image = tf.subtract(image, 0.5)
    image = tf.multiply(image, 2.0) # -1 to 1
    image = tf.squeeze(image,[0])
    return image



filelist = gfile.ListDirectory("images")
sess = tf.Session()
with sess.as_default():
    x = np.array([np.array(     preprocess(os.path.join("images", filename)).eval()      ) for filename in filelist])

input_shape = (IMAGE_HEIGHT, IMAGE_WIDTH, 1)   # 1, because preprocessing made grayscale

# in our case the labels come from part of the filename
y = np.array([int(filename[filename.index('_')+1:-4]) for filename in filelist])
# convert class labels to numbers
y = keras.utils.to_categorical(y, NUM_CLASSES)

########## TODO: something here? ##########
image = K.placeholder(shape=(), dtype=tf.string)
decoded = tf.image.decode_jpeg(image, channels=3)
# scores = build_model(decoded)


model = Sequential()

# model.add(decoded)

model.add(Conv2D(32, kernel_size=(2, 2), activation='relu', input_shape=input_shape))
model.add(Conv2D(64, (3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(64, activation='relu'))
model.add(Dropout(0.25))
model.add(Dense(num_classes, activation='softmax'))

model.compile(loss=keras.losses.categorical_crossentropy,
            optimizer=keras.optimizers.Adadelta(),
            metrics=['accuracy'])

model.fit(
    x,
    y,
    batch_size=64,
    epochs=20,
    verbose=1,
    validation_split=0.2,
    shuffle=False
    )

predict_signature = tf.saved_model.signature_def_utils.build_signature_def(
    inputs={'input_bytes':tf.saved_model.utils.build_tensor_info(model.input)},
    ########## TODO: something here? ##########
    # inputs={'input': image },    # input name must have "_bytes" suffix to use base64.
    outputs={'formId': tf.saved_model.utils.build_tensor_info(model.output)},
    method_name=tf.saved_model.signature_constants.PREDICT_METHOD_NAME
)

builder = tf.saved_model.builder.SavedModelBuilder("exported_model")

builder.add_meta_graph_and_variables(
    sess=K.get_session(),
    tags=[tf.saved_model.tag_constants.SERVING],
    signature_def_map={
        tf.saved_model.signature_constants.DEFAULT_SERVING_SIGNATURE_DEF_KEY: predict_signature
    },
    legacy_init_op=tf.group(tf.tables_initializer(), name='legacy_init_op')
)

builder.save()

This is related to my previous question.

Update:

The heart of the question is how to incorporate the placeholder that calls decode into the Keras model. In other words, after creating the placeholder that decodes the base64 string to a tensor, how to incorporate that into what Keras runs? I assume it needs to be a layer.

image = K.placeholder(shape=(), dtype=tf.string)
decoded = tf.image.decode_jpeg(image, channels=3)
model = Sequential()

# Something like this, but this fails because it is a tensor, not a Keras layer.  Possibly this is where a Lambda layer comes in?
model.add(decoded)
model.add(Conv2D(32, kernel_size=(2, 2), activation='relu', input_shape=input_shape))
...

Update 2:

Trying to use a lambda layer to accomplish this...

import keras
from keras.models import Sequential
from keras.layers import Lambda
from keras import backend as K
import tensorflow as tf

image = K.placeholder(shape=(), dtype=tf.string)
model = Sequential()
model.add(Lambda(lambda image: tf.image.decode_jpeg(image, channels=3), input_shape=() ))

Gives the error: TypeError: Input 'contents' of 'DecodeJpeg' Op has type float32 that does not match expected type of string.

user3567174
  • 1,898
  • 2
  • 15
  • 18
  • as per your second update, that shows a mismatch between types. Check if your decode function is returning the data type you expect, or well try changing the placeholder dtype to float. – DarkCygnus Jan 11 '18 at 23:03
  • did you figure out a solution to this? – Pavan K May 11 '18 at 13:22
  • I added the dtype however I get this error Shape must be rank 0 but is rank 2 for 'lambda_4/DecodePng' (op: 'DecodePng') with input shapes: [?,?]. – Pavan K May 11 '18 at 13:28

2 Answers2

9

first of all I use tf.keras but this should not be a big problem. So here is an example of how you can read a base64 decoded jpeg:

def preprocess_and_decode(img_str, new_shape=[299,299]):
    img = tf.io.decode_base64(img_str)
    img = tf.image.decode_jpeg(img, channels=3)
    img = tf.image.resize_images(img, new_shape, method=tf.image.ResizeMethod.BILINEAR, align_corners=False)
    # if you need to squeeze your input range to [0,1] or [-1,1] do it here
    return img
InputLayer = Input(shape = (1,),dtype="string")
OutputLayer = Lambda(lambda img : tf.map_fn(lambda im : preprocess_and_decode(im[0]), img, dtype="float32"))(InputLayer)
base64_model = tf.keras.Model(InputLayer,OutputLayer)   

The code above creates a model that takes a jpeg of any size, resizes it to 299x299 and returns as 299x299x3 tensor. This model can be exported directly to saved_model and used for Cloud ML Engine serving. It is a little bit stupid, since the only thing it does is the convertion of base64 to tensor.

If you need to redirect the output of this model to the input of an existing trained and compiled model (e.g inception_v3) you have to do the following:

base64_input = base64_model.input
final_output = inception_v3(base64_model.output)
new_model = tf.keras.Model(base64_input,final_output)

This new_model can be saved. It takes base64 jpeg and returns classes identified by the inception_v3 part.

Fedor Petrov
  • 990
  • 9
  • 19
1

Another answer suggested adding tf.placeholder with type of tf.string, which makes sense, but how to incorporate that into the Keras model?

In Keras you can access your selected Backend (in this case Tensorflow) by doing:

from keras import backend as K

This you already seem to import on your code. That will enable you to access some native methods and resources available on the backend of your choice. It is the case that Keras backend includes a method for creating placeholders, among other utilities. Regarding placeholders, we can see what the Keras docs indicates about them:

placeholder

keras.backend.placeholder(shape=None, ndim=None, dtype=None, sparse=False, name=None)

Instantiates a placeholder tensor and returns it.

It also gives some example on its use:

>>> from keras import backend as K
>>> input_ph = K.placeholder(shape=(2, 4, 5))
>>> input_ph._keras_shape
(2, 4, 5)
>>> input_ph
<tf.Tensor 'Placeholder_4:0' shape=(2, 4, 5) dtype=float32>

As you can see, this is returning a Tensorflow tensor, with shape (2,4,5) and of dtype float. If you had another backend while doing the example you would get another tensor object (a Theano one surely). You can therefore use this placeholder() to adapt the solution you got on your previous question.

In conclusion, you can use your backend imported as K (or whatever you want) to do calls on the methods and objects available on the backend of your choice, by doing K.foo.bar() on the desired method. I suggest you give a read to what the Keras Backend to explore more things that can be useful for you on future situations.

Update: As per your edit. Yes, this placeholder should be a layer in your model. Specifically, it should be the Input Layer of your model, as it holds your decoded image (as Keras needs it that way) to classify.

Community
  • 1
  • 1
DarkCygnus
  • 7,420
  • 4
  • 36
  • 59
  • Thanks for your response. I understand K.placeholder provides access to create a tf.placeholder. However adapting the code (simply replacing tf.placeholder with K.placeholder) has problems. First, tf.image.decode_jpeg expects image to be rank 0, but it is defined as rank 1 (because shape=[None]). Also confused by how the answerer used the 'scores' variable to hold the result of the model build, as well as the output on the signature of the model. Could you include specific code? – user3567174 Jan 10 '18 at 03:05
  • @user3567174 surely will do tomorrow morning when I get to my pc, as I am currently mobile and can't browse and try much – DarkCygnus Jan 10 '18 at 03:35
  • @user3567174 Have you tried what I suggested? I wonder why you accepted that answer if there were things there you did not understand - "... expects image to be rank 0 but it is defined as rank 1 (because shape=[None])." - in that case don't pass `[None]` (as you don't want batches) and pass the corresponding shape instead. Have you tried `shape=()` instead to indicate rank 0?... I don't quite get what that user answered to you there, but seems that `scores` stores the classification obtained from the rest of your model (which I understand the answerer summarized with `build_model`). – DarkCygnus Jan 10 '18 at 22:42
  • I didn't realize you could create a tensor of rank 0; seems like an oxymoron. I accepted your answer to the other question before I tried implementing it, because it seemed to address what I was asking. I've updated this question with additional detail. Let me know if that doesn't clarify what I am asking. – user3567174 Jan 11 '18 at 15:07
  • @user3567174 as per your update, yes it should be a layer. It is your input layer, as it carries your decoded image to classify. Updating answer to reflect this. As a side comment, I usually prefer to *not* use the Sequential model and go for the [Functional API](https://keras.io/models/model/), as I have found it enables you more customization, so maybe you can check it out also to see if you like it better :) – DarkCygnus Jan 11 '18 at 15:14
  • Good to know a layer is the right approach. I assume it needs to be a lambda layer. I updated to show the latest attempt. Working sample code would be helpful if you've done this before. – user3567174 Jan 11 '18 at 22:09
  • @user3567174 sorry, no working sample code for this application, have done several others but not like this (cloud etc). I added a comment on your post regarding your new update – DarkCygnus Jan 11 '18 at 23:19
  • Haven't gotten the code working yet, but appreciate your help towards the goal, so awarding bounty. Thanks. – user3567174 Jan 12 '18 at 13:44