This post is a step by step tutorial on how to train, export and run a Tensorflow Neural Network on an Arduino-compatible microcontroller for the task of classification: in particular, we will classify the Wine dataset.

TinyML Arduino

Many new comers to TinyML and Tensorflow for Microcontrollers still struggles to get started with neural networks for classification tasks, so I decided to write this post as a step by step guide to fill this gap.

This tutorial will cover:

  1. train a Tensorflow Neural network in Python to classify the Iris dataset
  2. export the Neural Network to Arduino-compatible C++
  3. run the Neural network on a microcontroller

I'll cover all the steps, so you can use this post as a reference for further projects that need to classify any dataset you may have: most (if not all) the steps will be the same.

Step 1) Train a Nerual Network in Python

First of all, we need to create and train a neural network in Python using the Tensorflow framework. I will use the Keras API because it's very easy to use in my opinion.

import numpy as np
from tensorflow.keras import Sequential, layers
from tensorflow.keras.utils import to_categorical
from sklearn.datasets import load_wine
from sklearn.model_selection import train_test_split

# load and split dataset into train, validation, test
X, y = load_wine(return_X_y=True)
y = to_categorical(y)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
X_train, X_valid, y_train, y_valid = train_test_split(X_train, y_train, test_size=0.3)

input_dim = X_train.shape[1:]
output_dim = y.shape[1]

print('input_dim', input_dim)
print('output_dim', output_dim)

# create and train network
# you can customize the layers as you prefer
nn = Sequential()
nn.add(layers.Dense(units=50, activation='relu', input_shape=input_dim))
nn.add(layers.Dense(units=50, activation='relu'))
nn.add(layers.Dense(output_dim, activation='softmax'))

# use categorical_crossentropy for multi-class classification
nn.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
nn.fit(X_train, y_train, validation_data=(X_valid, y_valid), epochs=100, verbose=0)

print('Accuracy: %.1f' % nn.evaluate(X_test, y_test)[1])

I got a best accuracy of 89% with this configuration: depending on your dataset and layers the results can vary.

Step 2) Export to C++

We will use the tinymlgen package to export the network to a C data array.

from tinymlgen import port

print(port(nn, variable_name='wine_model', pretty_print=True, optimize=False))

You will get something like the following.

#ifdef __has_attribute
#define HAVE_ATTRIBUTE(x) __has_attribute(x)
#else
#define HAVE_ATTRIBUTE(x) 0
#endif
#if HAVE_ATTRIBUTE(aligned) || (defined(__GNUC__) && !defined(__clang__))
#define DATA_ALIGN_ATTRIBUTE __attribute__((aligned(4)))
#else
#define DATA_ALIGN_ATTRIBUTE
#endif

const unsigned char wine_model[] DATA_ALIGN_ATTRIBUTE = {
    0x1c, 0x00, 0x00, 0x00, 0x54, 0x46, 0x4c, 0x33, 0x00, 0x00, 0x12, 0x00, 
    0x1c, 0x00, 0x04, 0x00, 0x08, 0x00, 0x0c, 0x00, 0x10, 0x00, 0x14, 0x00,
    .......
};
const int iris_model_len = 15228;

Copy-paste this text into a file named wine_model.h inside your Arduino project.

Step 3) Run the Neural Network on your microcontroller

We will make use of the EloquentTinyML library to easily run the neural network on our microcontroller. You can easily install it via the Arduino IDE library manager.

#include <EloquentTinyML.h>
#include <eloquent_tinyml/tensorflow.h>
#include "wine_model.h"

#define N_INPUTS 13
#define N_OUTPUTS 3
#define TENSOR_ARENA_SIZE 16*1024

Eloquent::TinyML::TfLite<N_INPUTS, N_OUTPUTS, TENSOR_ARENA_SIZE> tf;

float X_test[20][13] = {
    {1.340e+01, 4.600e+00, 2.860e+00, 2.500e+01, 1.120e+02, 1.980e+00,
    9.600e-01, 2.700e-01, 1.110e+00, 8.500e+00, 6.700e-01, 1.920e+00, 6.300e+02},
    {1.285e+01, 3.270e+00, 2.580e+00, 2.200e+01, 1.060e+02, 1.650e+00,
    6.000e-01, 6.000e-01, 9.600e-01, 5.580e+00, 8.700e-01, 2.110e+00, 5.700e+02},
    {1.334e+01, 9.400e-01, 2.360e+00, 1.700e+01, 1.100e+02, 2.530e+00,
    1.300e+00, 5.500e-01, 4.200e-01, 3.170e+00, 1.020e+00, 1.930e+00, 7.500e+02},
    {1.423e+01, 1.710e+00, 2.430e+00, 1.560e+01, 1.270e+02, 2.800e+00,
    3.060e+00, 2.800e-01, 2.290e+00, 5.640e+00, 1.040e+00, 3.920e+00, 1.065e+03},
    {1.483e+01, 1.640e+00, 2.170e+00, 1.400e+01, 9.700e+01, 2.800e+00,
    2.980e+00, 2.900e-01, 1.980e+00, 5.200e+00, 1.080e+00, 2.850e+00, 1.045e+03},
    {1.245e+01, 3.030e+00, 2.640e+00, 2.700e+01, 9.700e+01, 1.900e+00,
    5.800e-01, 6.300e-01, 1.140e+00, 7.500e+00, 6.700e-01, 1.730e+00, 8.800e+02},
    {1.430e+01, 1.920e+00, 2.720e+00, 2.000e+01, 1.200e+02, 2.800e+00,
    3.140e+00, 3.300e-01, 1.970e+00, 6.200e+00, 1.070e+00, 2.650e+00, 1.280e+03},
    {1.390e+01, 1.680e+00, 2.120e+00, 1.600e+01, 1.010e+02, 3.100e+00,
    3.390e+00, 2.100e-01, 2.140e+00, 6.100e+00, 9.100e-01, 3.330e+00, 9.850e+02},
    {1.165e+01, 1.670e+00, 2.620e+00, 2.600e+01, 8.800e+01, 1.920e+00,
    1.610e+00, 4.000e-01, 1.340e+00, 2.600e+00, 1.360e+00, 3.210e+00, 5.620e+02},
    {1.386e+01, 1.510e+00, 2.670e+00, 2.500e+01, 8.600e+01, 2.950e+00,
    2.860e+00, 2.100e-01, 1.870e+00, 3.380e+00, 1.360e+00, 3.160e+00, 4.100e+02},
    {1.377e+01, 1.900e+00, 2.680e+00, 1.710e+01, 1.150e+02, 3.000e+00,
    2.790e+00, 3.900e-01, 1.680e+00, 6.300e+00, 1.130e+00, 2.930e+00, 1.375e+03},
    {1.296e+01, 3.450e+00, 2.350e+00, 1.850e+01, 1.060e+02, 1.390e+00,
    7.000e-01, 4.000e-01, 9.400e-01, 5.280e+00, 6.800e-01, 1.750e+00, 6.750e+02},
    {1.305e+01, 5.800e+00, 2.130e+00, 2.150e+01, 8.600e+01, 2.620e+00,
    2.650e+00, 3.000e-01, 2.010e+00, 2.600e+00, 7.300e-01, 3.100e+00, 3.800e+02},
    {1.182e+01, 1.470e+00, 1.990e+00, 2.080e+01, 8.600e+01, 1.980e+00,
    1.600e+00, 3.000e-01, 1.530e+00, 1.950e+00, 9.500e-01, 3.330e+00, 4.950e+02},
    {1.164e+01, 2.060e+00, 2.460e+00, 2.160e+01, 8.400e+01, 1.950e+00,
    1.690e+00, 4.800e-01, 1.350e+00, 2.800e+00, 1.000e+00, 2.750e+00, 6.800e+02},
    {1.303e+01, 9.000e-01, 1.710e+00, 1.600e+01, 8.600e+01, 1.950e+00,
    2.030e+00, 2.400e-01, 1.460e+00, 4.600e+00, 1.190e+00, 2.480e+00, 3.920e+02},
    {1.176e+01, 2.680e+00, 2.920e+00, 2.000e+01, 1.030e+02, 1.750e+00,
    2.030e+00, 6.000e-01, 1.050e+00, 3.800e+00, 1.230e+00, 2.500e+00, 6.070e+02},
    {1.439e+01, 1.870e+00, 2.450e+00, 1.460e+01, 9.600e+01, 2.500e+00,
    2.520e+00, 3.000e-01, 1.980e+00, 5.250e+00, 1.020e+00, 3.580e+00, 1.290e+03},
    {1.420e+01, 1.760e+00, 2.450e+00, 1.520e+01, 1.120e+02, 3.270e+00,
    3.390e+00, 3.400e-01, 1.970e+00, 6.750e+00, 1.050e+00, 2.850e+00, 1.450e+03},
    {1.368e+01, 1.830e+00, 2.360e+00, 1.720e+01, 1.040e+02, 2.420e+00,
    2.690e+00, 4.200e-01, 1.970e+00, 3.840e+00, 1.230e+00, 2.870e+00, 9.900e+02}
};

uint8_t y_test[20] = {2, 2, 1, 0, 0, 2, 0, 0, 1, 1, 0, 2, 1, 1, 1, 1, 1, 0, 0, 0};

void setup() {
    Serial.begin(115200);
    tf.begin(wine_model);
}

void loop() {
    for (uint8_t i = 0; i < 10; i++) {
        Serial.print("Sample #");
        Serial.print(i + 1);
        Serial.print(": ");
        Serial.print("predicted ");
        Serial.print(tf.predictClass(X_test[i]));
        Serial.print(" vs ");
        Serial.print(y_test[i]);
        Serial.println(" actual");
    }

    delay(10000);
}

That's it!

Compile, upload the sketch and open the serial monitor: you should see the predicted classes matching the target ones.

Final considerations

Was it easy to deploy your neural network to your microcontroller?

As you can see, you don't need anything too fancy: just 3 easy steps and you're up and running.

The TensorFlow class from the EloquentTinyML library implements a couple methods you can use to run predictions when dealing with classification tasks:

  • predict(input, output) will save the predicted values for each class into the output array and you can use it later (for example to predict class probabilities)
  • predictClass(input) will only return the class with the highest probability

Check the full project code on Github and remember to star!

Help the blow grow