Building a Keras API with MLChain¶
In this tutorial, we will build a Keras model on the Fashion-MNIST dataset and deploy it using ML-Chain. If you are already familiar with Keras, you can simply download the Keras model here and skip to section 2 in this tutorial.
Otherwise, let's get started.
1. Building a Fashion-MNIST classifier using Keras¶
In this section, we will build a simple Keras model using Jupyter notebook. After saving the model, we will use it and deploy an API using MLChain.
First, start your Jupyter Notebook and import the necessary libraries. For this version, I'm using tensorflow 2.2.0 for our keras model.
# TensorFlow and tf.keras import tensorflow as tf from tensorflow import keras # Helper libraries import numpy as np import matplotlib.pyplot as plt
Next, let's load our dataset into our notebook. In this step, we first download the fashion_mnist data from the keras.datasets library. Next, we load them with load_data(), and finally assign the class names.
# get the FASHION-MNIST data fashion_mnist = keras.datasets.fashion_mnist # load train (train_images, train_labels), (test_images, test_labels) = fashion_mnist.load_data() # list the classes class_names = ['T-shirt/top', 'Trouser', 'Pullover', 'Dress', 'Coat', 'Sandal', 'Shirt', 'Sneaker', 'Bag', 'Ankle boot']
Next, let's display some images:
plt.figure() plt.imshow(train_images[3]) plt.colorbar() plt.grid(False) plt.show()

To improve our model's performance, we should also convert our data to a scale from 0 - 1 as input. This is because pixel data are represented for a scale from 0 to 255. Since these number are quite large, the model will find more difficulty converging. Normalizing them to a scale between 0 and 1 can lead to faster convergence.
train_images = train_images / 255.0 test_images = test_images / 255.0
In this tutorial, we will be adding a convolution layer in our dataset. For this reason, we will have to reshape our input data shape. Convolution layers take in input images with 4 dimensions (batch_size, height, width, no. of channels), and perform subsequent operations on them. Since we have batches of images of size 28x28, with only 1 channel, we reshape them into the following dimension: (batch_size, 28, 28, 1).
The following code complete this task:
train_images = train_images.reshape(train_images.shape[0], train_images.shape[1], train_images.shape[2], 1) test_images = test_images.reshape(test_images.shape[0], test_images.shape[1], test_images.shape[2], 1)
Next, we define our model structure. This includes first a convolution layer with a 32 output layers, with a kernel size of 3, followed by a relu activation funtion. After that, our model is flattened, then and applied a dropout layer with 25% probability. This dropout layer will turn some hidden layer's values to 0, and reduce overfitting in our model. Next, a fully connected layer with 128 output nodes is applied, followed by the relu activation function. A final dropout layer of probability 25% is applied, and a fully connected layer of 10 output is applied, corresponding to our 10 classes.
model = keras.Sequential([ keras.layers.Conv2D(32, kernel_size=3, activation='relu', input_shape=(28,28,1)), keras.layers.Flatten(), keras.layers.Dropout(0.25), keras.layers.Dense(128, activation='relu'), keras.layers.Dropout(0.25), keras.layers.Dense(10) ])
Lastly, let's define our optimizer, loss function, and evaluation metrics. We will use the Adam optimizer, Cross-Entropy loss, and accuracy as our main metric.
model.compile(optimizer='adam', loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True), metrics=['accuracy'])
Let's run our model:
model.fit(train_images, train_labels, epochs=15)
Check the log for training progress. We should see our loss steadily decreasing and our accuracy steadily increasing.
Train on 60000 samples Epoch 1/15 60000/60000 [==============================] - 7s 122us/sample - loss: 0.4203 - accuracy: 0.8491 Epoch 2/15 60000/60000 [==============================] - 6s 99us/sample - loss: 0.2892 - accuracy: 0.8945 Epoch 3/15 60000/60000 [==============================] - 6s 97us/sample - loss: 0.2437 - accuracy: 0.9095 Epoch 4/15 60000/60000 [==============================] - 6s 97us/sample - loss: 0.2098 - accuracy: 0.9216 Epoch 5/15 60000/60000 [==============================] - 6s 100us/sample - loss: 0.1834 - accuracy: 0.9307 Epoch 6/15 60000/60000 [==============================] - 6s 100us/sample - loss: 0.1637 - accuracy: 0.9390 Epoch 7/15 60000/60000 [==============================] - 6s 99us/sample - loss: 0.1477 - accuracy: 0.9443 Epoch 8/15 60000/60000 [==============================] - 6s 100us/sample - loss: 0.1319 - accuracy: 0.9501 Epoch 9/15 60000/60000 [==============================] - 6s 99us/sample - loss: 0.1191 - accuracy: 0.9550 Epoch 10/15 60000/60000 [==============================] - 6s 100us/sample - loss: 0.1092 - accuracy: 0.9584 Epoch 11/15 60000/60000 [==============================] - 6s 100us/sample - loss: 0.1013 - accuracy: 0.9616 Epoch 12/15 60000/60000 [==============================] - 6s 101us/sample - loss: 0.0902 - accuracy: 0.9647 Epoch 13/15 60000/60000 [==============================] - 6s 100us/sample - loss: 0.0863 - accuracy: 0.9671 Epoch 14/15 60000/60000 [==============================] - 6s 99us/sample - loss: 0.0805 - accuracy: 0.9692 Epoch 15/15 60000/60000 [==============================] - 6s 100us/sample - loss: 0.0776 - accuracy: 0.9712
Our model return a loss of 0.0776 on the training set, with an accuracy of upto 97.12%. We will need to test on the test set to determine its actual ability in generalizing to new dataset.
test_loss, test_acc = model.evaluate(test_images, test_labels, verbose=2) print('Test loss: ', test_loss) print('\nTest accuracy: ', test_acc)
Since our model is testing on an entirely different dataset, it indeed performs somewhat more poorly in comparision with the training dataset. The loss is at 0.37, while the accuracy is at 90%. This is an example of overfitting.
10000/1 - 1s - loss: 0.4749 - accuracy: 0.9095 Test loss: 0.37909149531126024 Test accuracy: 0.9095
We could have employed some strategies, such as early stopping and training the training set on a smaller dataset. However, now that we're trying to use mlchain to deploy our API, let's save the model and deploy it only for actual use cases.
# save model model_json = model.to_json() with open("model.json", "w") as json_file: json_file.write(model_json) # serialize weights to HDF5 model.save_weights("model.h5") print("Saved model to disk")
2. Using MLChain to deploy our model¶
Regardless of whether you decided to follow step 1 or not, you should now have a model.h5 and a model.json file that allows you to deploy your Keras model. If you haven't downloaded these files yet, get them here: https://drive.google.com/file/d/1z-AK8Ld3krtxYh9FPWDQb2x1ZXdZyow2/view?usp=sharing
In the same folder, create a main.py file. This will be where we deploy our model. Import the necessary libraries
# helper libraries import tensorflow as tf from tensorflow import keras import numpy as np from tensorflow.keras.models import model_from_json import cv2 # mlchain libraries from mlchain.base import ServeModel
Next, we load our model into the program. To use mlchain, we will have to create a class serving this particular purpose. Create the following class with the init function.
class Model(): # define and load our prior model def __init__(self): # load model # load json and create model json_file = open('../model.json', 'r') loaded_model_json = json_file.read() json_file.close() self.model = model_from_json(loaded_model_json) # load weights into new model self.model.load_weights("../model.h5") # list of class names self.class_names = ['T-shirt/top', 'Trouser', 'Pullover', 'Dress', 'Coat', 'Sandal', 'Shirt', 'Sneaker', 'Bag', 'Ankle boot']
In the above code, we load our model from the json file and subsequently the weights from the h5 file. In addition, we define the class_names that will allow us to distinguish between different class output.
Our model will serve one specific purpose: predicting . With any image put through the input, we will try to predict the corresponding digit to that image. Let's now define our predict function.
# define function for predicting images def predict(self, img:np.ndarray): r""" Predict classes that image is based in Args: img(numpy array): Return an image used for prediction Note: You don't have to worry about the input for this function. Most of the time the input img:np.ndarray would be sufficient. It's important how you work with that input. """ # convert color img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # predict predict = self.model.predict(img.reshape(1, 28, 28, 1)) # get class num predict_index = np.argmax(predict) # return class name return self.class_names[predict_index]
We also might want to predict multiple images, rather than just one. Create another function which take a list of images, and make predictions based on those images.
def batch_predict(self, imgs:List(np.ndarray)): ans = [] for img in imgs: ans.append(self.predict(img)) return ans
Lastly, we deploy our model with 2 lines of codes:
# define model model = Model() # serve model serve_model = ServeModel(model)
To deploy our model, we first run mlchain init. This should result in a yaml file.
We modify the parameters (name, entry_file, and port) to get the following file:
name: Fashion-MNIST classifier # name of service entry_file: main.py # python file contains object ServeModel host: localhost # host service port: 5000 # port service server: flask # option flask or quart or grpc wrapper: None # option None or gunicorn or hypercorn cors: true dump_request: None # None or path folder log request version: '1.0.0' api_keys: None # - key1 # - key2 gunicorn: # config apm-server if uses gunicorn wrapper timeout: 60 keepalive: 60 max_requests: 0 threads: 10 worker_class: 'gthread' umask: '0' hypercorn: # config apm-server if uses hypercorn wrapper keep_alive_timeout: 60 worker_class: 'asyncio' umask: 0 mode: default: dev # running mode env: # set of mode env default: {} # environ default dev: {} # environ by mode prod: {} # environ by mode
Next, run mlchain run and go to http://localhost:5000 to test our your API. Alternatively, you
can also test out our request with Postman.
The following POST request to http://localhost:5000/call/predict with a img param get us the subsequent response.

Similarly, a similar request to http://localhost:5000/call/batch_predict with imgs param including multiple images returns the following:

3. Communicating with MLChain¶
Now that we have had our model deployed, we can use MLChain Client to communicate between multiple programs.
In a separate folder, create a client.py file. Import the necessary libraries:
from mlchain.client import Client import cv2
Next, we will "import" our model that is already deployed on the web.
model = Client(api_address='127.0.0.1:5000', serializer='msgpack').model(check_status=False)
In the above code, we are using the model that is deployed on 127.0.0.1:5000. We also identified the serializer, in which our model will return data in message pack.
At this point, it takes 3 lines to get our model working. Simply by identifying the files, we run batch predict to get the response of all the files.
# list the file to run classifier files = ['data/1.png', 'data/2.png', 'data/3.png'] # TODO: list of files you want to process imgs = [cv2.imread(i) for i in files] # get our result print(model.batch_predict(imgs))
We get the following as our response:
['T-shirt/top', 'T-shirt/top', 'Dress']