Add an Existing Script to the MLTK¶
The MLTK is a collection of tools to aid the development of machine learning models for Silicon Labs embedded devices. As part of its offering, the MLTK features an optional Python API to simplify ML model development using the Tensorflow framework. The MLTK Python API combines and simplifies several different ML model development tasks including:
The MLTK Python API is effectively a light wrapper around the Tensorflow API. As such, very little needs to be done to convert an existing Tensorflow model training script to support the MLTK, and, thus, gain the additional MLTK features (quantization, evaluation, profiling).
NOTE: Converting your existing model script to the MLTK is not required. The only hard requirement to use your model on a Silicon Labs embedded device is that your model be converted to the .tflite format with int8
quantization.
Setup¶
Before continuing with this tutorial, ensure you have the MLTK Python package installed and that the virtual environment is activated (if applicable).
Existing Script¶
For this tutorial, we use the Keras example: Simple MNIST convnet
The source code for the model training script may be found on GitHub.
It has also been copy & pasted as follows:
Original mnist_convnet.py¶
import numpy as np
from tensorflow import keras
from tensorflow.keras import layers
"""
## Prepare the data
"""
# Model / data parameters
num_classes = 10
input_shape = (28, 28, 1)
# Load the data and split it between train and test sets
(x_train, y_train), (x_test, y_test) = keras.datasets.mnist.load_data()
# Scale images to the [0, 1] range
x_train = x_train.astype("float32") / 255
x_test = x_test.astype("float32") / 255
# Make sure images have shape (28, 28, 1)
x_train = np.expand_dims(x_train, -1)
x_test = np.expand_dims(x_test, -1)
print("x_train shape:", x_train.shape)
print(x_train.shape[0], "train samples")
print(x_test.shape[0], "test samples")
# convert class vectors to binary class matrices
y_train = keras.utils.to_categorical(y_train, num_classes)
y_test = keras.utils.to_categorical(y_test, num_classes)
"""
## Build the model
"""
model = keras.Sequential(
[
keras.Input(shape=input_shape),
layers.Conv2D(32, kernel_size=(3, 3), activation="relu"),
layers.MaxPooling2D(pool_size=(2, 2)),
layers.Conv2D(64, kernel_size=(3, 3), activation="relu"),
layers.MaxPooling2D(pool_size=(2, 2)),
layers.Flatten(),
layers.Dropout(0.5),
layers.Dense(num_classes, activation="softmax"),
]
)
model.summary()
"""
## Train the model
"""
batch_size = 128
epochs = 15
model.compile(loss="categorical_crossentropy", optimizer="adam", metrics=["accuracy"])
model.fit(x_train, y_train, batch_size=batch_size, epochs=epochs, validation_split=0.1)
"""
## Evaluate the trained model
"""
score = model.evaluate(x_test, y_test, verbose=0)
print("Test loss:", score[0])
print("Test accuracy:", score[1])
MLTK Modifications¶
Download the mnist_convnet.py script to your local PC, and add the following modifications to it so that it we can execute it in the MLTK.
All modifications are prefixed with the comment: # Modified for the MLTK
Copy & paste the following into the mnist_convnet.py
script on your local PC:
Modified mnist_convnet.py¶
import numpy as np
from tensorflow import keras
from tensorflow.keras import layers
"""
## Prepare the data
"""
# Model / data parameters
num_classes = 10
input_shape = (28, 28, 1)
# Load the data and split it between train and test sets
(x_train, y_train), (x_test, y_test) = keras.datasets.mnist.load_data()
# Scale images to the [0, 1] range
x_train = x_train.astype("float32") / 255
x_test = x_test.astype("float32") / 255
# Make sure images have shape (28, 28, 1)
x_train = np.expand_dims(x_train, -1)
x_test = np.expand_dims(x_test, -1)
print("x_train shape:", x_train.shape)
print(x_train.shape[0], "train samples")
print(x_test.shape[0], "test samples")
# convert class vectors to binary class matrices
y_train = keras.utils.to_categorical(y_train, num_classes)
y_test = keras.utils.to_categorical(y_test, num_classes)
"""
## Build the model
"""
model = keras.Sequential(
[
keras.Input(shape=input_shape),
layers.Conv2D(32, kernel_size=(3, 3), activation="relu"),
layers.MaxPooling2D(pool_size=(2, 2)),
layers.Conv2D(64, kernel_size=(3, 3), activation="relu"),
layers.MaxPooling2D(pool_size=(2, 2)),
layers.Flatten(),
layers.Dropout(0.5),
layers.Dense(num_classes, activation="softmax"),
]
)
model.summary()
"""
## Train the model
"""
batch_size = 128
epochs = 15
model.compile(loss="categorical_crossentropy", optimizer="adam", metrics=["accuracy"])
# Modified for the MLTK
# We only want to train when calling the train_model() API
#model.fit(x_train, y_train, batch_size=batch_size, epochs=epochs, validation_split=0.1)
"""
## Evaluate the trained model
"""
# Modified for the MLTK
# We only want to evaluate when calling teh evaluate_model() API
#score = model.evaluate(x_test, y_test, verbose=0)
#print("Test loss:", score[0])
#print("Test accuracy:", score[1])
# Modified for the MLTK
# The follow allows for running this existing model training script in the MLTK
# Import the MLTK Python package
import mltk.core as mltk_core
import tensorflow as tf
# Instantiate the MltkModel object
class MyModel(
mltk_core.MltkModel,
mltk_core.TrainMixin,
mltk_core.DatasetMixin,
mltk_core.EvaluateClassifierMixin,
):
pass
my_model = MyModel()
# This simply returns the previously built KerasModel
def my_model_builder(mltk_model):
return model
# This callback allows for loading the training or testing dataset
def my_dataset_loader(
subset:str,
test:bool,
**kwargs
):
if subset == 'training':
return x_train, y_train
else:
return x_test, y_test
# This is used by the TfliteConverter when generating the quantized .tflite
def my_representative_dataset_generator():
for input_value in tf.data.Dataset.from_tensor_slices(my_model.x).batch(1).take(100):
yield [input_value]
# Populate the various MltkModel properties
my_model.build_model_function = my_model_builder
my_model.dataset = my_dataset_loader
my_model.epochs = epochs
my_model.batch_size = batch_size
my_model.validation_split = 0.1
my_model.classes = ['0','1','2','3','4','5','6','7','8','9']
my_model.tflite_converter['representative_dataset'] = my_representative_dataset_generator
# If this script is being invoked from the command-line then execute it now, e.g.:
# python mnist_convnet.py
if __name__ == '__main__':
from mltk import cli
cli.get_logger(verbose=False)
train_results = mltk_core.train_model(my_model, clean=True)
print(train_results)
tflite_eval_results = mltk_core.evaluate_model(my_model, verbose=True)
print(tflite_eval_results)
profiling_results = mltk_core.profile_model(my_model)
print(profiling_results)
Run the modified script¶
If you invoke the modified script with the command:
python mnist_convnet.py
Then, instead of just using Tensorflow for model training like the original script used, the modified script will use the MLTK and perform the following:
Train the model using Tensorflow
Generate a quantized
.tflite
model fileEvaluate the quantized
.tflite
modelProfile the
.tflite
model in the Tensorflow-Lite Micro simulator
Run from command-line¶
Additionally, the model script can either directly invoke the model script from Python (e.g. python mnist_convnet.py
) or use the mltk
command, e.g.:
# Train the model using the "mltk train" command
mltk train mnist_convnet.py
# Evaluate the model using the "mltk evaluate" command
mltk evaluate mnist_convnet.py
# Profile the model using the "mltk profile" command
mltk profile mnist_convnet.py
Recommended Modifications¶
While the above script works with minimal changes to the original script, it does have some deficiencies.
For instance, the dataset is always loaded each time the model script is loaded. Ideally, the dataset would only be loaded when it is actually needed before training or evaluation. Similarly, the Keras model is always built each time the model script is loaded. Ideally, the model would only be built before training is invoked.
To account for these issues, the MLTK features the callbacks:
The MLTK will invoke these callbacks at the necessary time for the given model task.
These changes may be found in the basic_example.py reference model. This model is based on mnist_convnet.py but has been refactored to better align with the MLTK development.
basic_example.py¶
from typing import Tuple
import numpy as np
import tensorflow as tf
from mltk import core as mltk_core
##########################################################################
# Prepare the model parameters
classes = ['0','1','2','3','4','5','6','7','8','9']
num_classes = len(classes)
input_shape = (28, 28, 1)
batch_size = 128
epochs = 15
validation_split = 0.1
##########################################################################
# Prepare the dataset
def my_dataset_loader(
subset:str,
test:bool,
**kwargs
) -> Tuple[np.ndarray, np.ndarray]:
"""Load the dataset subset
This is called automatically by the MLTK before training
or evaluation.
Args:
subset: The dataset subset to return: 'training' or 'evaluation'
test: This is optional, it is used when invoking a training "dryrun", e.g.: mltk train basic_example-test
If this is true, then only return a small portion of the dataset for testing purposes
Return:
A tuple (x,y) for the subset
"""
# Load the data and split it between train and test sets
(x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data()
# Scale images to the [0, 1] range
x_train = x_train.astype("float32") / 255
x_test = x_test.astype("float32") / 255
# Make sure images have shape (28, 28, 1)
x_train = np.expand_dims(x_train, -1)
x_test = np.expand_dims(x_test, -1)
# If we're just testing, then truncate the dataset
if test:
x_test = x_test[:64]
x_train = x_train[:64]
y_train = y_train[:64]
y_test = y_test[:64]
# This is optional, but useful for automatically generating a summary of the dataset
if subset == 'training':
my_model.class_counts['training'] = { my_model.classes[class_id]: count for (class_id, count) in enumerate(np.bincount(y_train)) }
else:
my_model.class_counts['evaluation'] = { my_model.classes[class_id]: count for (class_id, count) in enumerate(np.bincount(y_test)) }
# Convert class vectors to binary class matrices
y_train = tf.keras.utils.to_categorical(y_train, num_classes)
y_test = tf.keras.utils.to_categorical(y_test, num_classes)
if subset == 'training':
return x_train, y_train
else:
return x_test, y_test
##########################################################################
# Build the model
def my_model_builder(my_model: mltk_core.MltkModel) -> tf.keras.Model:
"""Build the Keras model
This is called by the MLTK just before training starts.
Arguments:
my_model: The MltkModel instance
Returns:
Compiled Keras model instance
"""
model = tf.keras.Sequential([
tf.keras.Input(shape=input_shape),
tf.keras.layers.Conv2D(32, kernel_size=(3, 3), activation="relu"),
tf.keras.layers.MaxPooling2D(pool_size=(2, 2)),
tf.keras.layers.Conv2D(64, kernel_size=(3, 3), activation="relu"),
tf.keras.layers.MaxPooling2D(pool_size=(2, 2)),
tf.keras.layers.Flatten(),
tf.keras.layers.Dropout(0.5),
tf.keras.layers.Dense(num_classes, activation="softmax"),
])
model.compile(
loss="categorical_crossentropy",
optimizer="adam",
metrics=["accuracy"]
)
return model
##########################################################################
# Create the MltkModel instance
# and set the various properties
# @mltk_model
class MyModel(
mltk_core.MltkModel, # We must inherit the MltkModel class
mltk_core.TrainMixin, # We also inherit the TrainMixin since we want to train this model
mltk_core.DatasetMixin, # We also need the DatasetMixin mixin to provide the relevant dataset properties
mltk_core.EvaluateClassifierMixin, # While not required, also inherit EvaluateClassifierMixin to help will generating evaluation for our classification model
):
pass
my_model = MyModel()
# These properties are optional
# but a useful for tracking the generated .tflite
my_model.version = 1
my_model.description = 'Basic model specification example'
my_model.classes = classes
my_model.class_weights = 'balanced' # Automatically generate balanced class weights for training
# Required: Set the model build function
my_model.build_model_function = my_model_builder
# Set the other model properties
# These values are passed directly to the model.fit() API
# https://www.tensorflow.org/api_docs/python/tf/keras/Model#fit
my_model.batch_size = batch_size
my_model.epochs = epochs
my_model.validation_split = validation_split
# NOTE: All the other fit() arguments may also be set in the model, e.g.:
# my_model.x = x_train
# my_model.y = y_train
# my_model.step_per_epoch = 60
# Set the dataset
my_model.dataset = my_dataset_loader
# NOTE: Since my_dataset_loader() returns the x,y
# We do not need to manually set the my_model.x, my_model.y properties.
# NOTE: You can also add the various KerasCallbacks
# https://www.tensorflow.org/api_docs/python/tf/keras/callbacks/
my_model.train_callbacks = [
tf.keras.callbacks.TerminateOnNaN()
]
##########################################################################
# Specify the .tflite conversion parameters
# This is used to convert the float32 model to int8 model
# that can run on the embedded device.
def my_representative_dataset_generator():
"""A representative dataset is required generate the .tflite
In this example we just take the first 100 samples from the validation set.
For more details, see:
https://www.tensorflow.org/lite/performance/post_training_integer_quant#convert_using_integer-only_quantization
NOTE: Quantization is automatically done at the end of training.
It may also be invoked with:
mltk train basic_example
"""
for input_value in tf.data.Dataset.from_tensor_slices(my_model.x).batch(1).take(100):
yield [input_value]
my_model.tflite_converter['inference_input_type'] = tf.float32
my_model.tflite_converter['inference_output_type'] = tf.float32
my_model.tflite_converter['representative_dataset'] = my_representative_dataset_generator
##########################################################################################
# (Optional) Configure model parameters
#
# While not required, user-defined parameters may be embedded into the .tflite model file.
# These parameters may then be read by the embedded device at runtime.
#
# This is useful for syncing data preprocessing parameters between the model training
# script and embedded device.
# In my_dataset_loader() we scaled the image data by 1/255.
# This same scaling must also happen on the embedded device.
# Here, we're embedding the scaling value as "metadata" into the generated .tflite.
# At runtime, the embedded device should read this value from the .tflite
# and use it accordingly.
my_model.model_parameters['samplewise_norm.rescale'] = 1/255.
# Most standard Python data types may be embedded
# See: https://siliconlabs.github.io/mltk/docs/guides/model_parameters.html
my_model.model_parameters['my_boolean'] = True
my_model.model_parameters['my_string'] = 'This string will be embedded into the .tflite'
my_model.model_parameters['my_bytes'] = b'This byte string will be embedded also'
my_model.model_parameters['my_float_list'] = [4.5, 2., 3.14]
##########################################################################################
# (Optional) The following allows for running this model training script directly, e.g.:
# python basic_example.py
#
# Note that this has the similar functionality to:
# mltk train basic_example
#
if __name__ == '__main__':
from mltk import cli
# Setup the CLI logger
cli.get_logger(verbose=False)
# If this is true then this will do a "dry run" of the model testing
# If this is false, then the model will be fully trained
test_mode_enabled = True
# Train the model
# This does the same as issuing the command: mltk train basic_example-test --clean
train_results = mltk_core.train_model(my_model, clean=True, test=test_mode_enabled)
print(train_results)
# Evaluate the model against the quantized .h5 (i.e. float32) model
# This does the same as issuing the command: mltk evaluate basic_example-test
tflite_eval_results = mltk_core.evaluate_model(my_model, verbose=True, test=test_mode_enabled)
print(tflite_eval_results)
# Profile the model in the simulator
# This does the same as issuing the command: mltk profile basic_example-test
profiling_results = mltk_core.profile_model(my_model, test=test_mode_enabled)
print(profiling_results)
Next Steps¶
This tutorial demonstrates how minimal modifications can be added to an existing model training script so that it can run in the MLTK.
For complete, non-trivial models, please see the Reference Models that come with the MLTK.