Chapter 3: Building a Weather Station with TensorFlow Lite for Microcontrollers

Nowadays, it is straightforward to get the weather forecast with our smartphones, laptops, and tablets, thanks to internet connectivity. However, have you ever thought of what you would do if you had to track the weather in a remote region with no internet access?

This chapter will teach us how to implement a weather station with machine learning (ML) using the temperature and humidity of the last three hours.

In this chapter, we will focus on dataset preparation and show how to acquire historical weather data from WorldWeatherOnline. After that, we will explain how to train and test a model with TensorFlow (TF). In the last part, we will deploy the model on an Arduino Nano and a Raspberry Pi Pico with TensorFlow Lite for Microcontrollers (TFLu) and build an application to predict whether it will snow.

The goal of this chapter is to guide you through all the development stages of a TF-based application for microcontrollers and explain how to acquire temperature and humidity sensor data.

In this chapter, we're going to implement the following recipes:

  • Importing weather data from WorldWeatherOnline
  • Preparing the dataset
  • Training the model with TF
  • Evaluating the model's effectiveness
  • Quantizing the model with TFLite converter
  • Using the built-in temperature and humidity sensor on an Arduino Nano
  • Using the DHT22 sensor with a Raspberry Pi Pico
  • Preparing the input features for the model inference
  • On-device inference with TFLu

Technical requirements

To complete all the practical recipes of this chapter, we will need the following:

  • An Arduino Nano 33 Sense board
  • A Raspberry Pi Pico board
  • A micro-USB cable
  • 1 x half-size solderless breadboard (Raspberry Pi Pico only)
  • 1 x AM2302 module with the DHT22 sensor (Raspberry Pi Pico only)
  • 5 x jumper wires (Raspberry Pi Pico only)
  • Laptop/PC with either Ubuntu 18.04+ or Windows 10 on x86-64

The source code and additional material are available in the Chapter03 folder of the repository for this book (https://github.com/PacktPublishing/TinyML-Cookbook/tree/main/Chapter03).

Importing weather data from WorldWeatherOnline

The effectiveness of ML algorithms depends heavily on the data used for training. Hence, as we commonly say, the ML model is only good as the dataset. The essential requirement for a good dataset is that the input data must represent the problem we want to solve. Considering our context, we know from physics that temperature and humidity affect snow formation.

Hence, in this recipe, we will show how to gather historical hourly temperature, humidity, and snowfall data to build a dataset for forecasting snow.

The following Colab file (see the Importing weather data from WorldWeatherOnline section in the following repository) contains the code referred to in this recipe:

  • preparing_model.ipynb:

https://github.com/PacktPublishing/TinyML-Cookbook/blob/main/Chapter03/ColabNotebooks/preparing_model.ipynb

Getting ready

On the internet, there are various sources from which we can gather hourly weather data, but most of them are not free or have limited usage.

For this recipe, WorldWeatherOnline (https://www.worldweatheronline.com/developer/) has been our choice, which has a free trial period for 30 days and provides the following:

  • Simple API through HTTP requests to acquire the data
  • Historical worldwide weather data
  • 250 weather data requests per day

    Important Note

    The limit on the weather data requests per day has no impact on this recipe.

You only need to sign up on the website to start fetching the data.

WorldWeatherOnline has an API called the Past Historical Weather API (https://www.worldweatheronline.com/developer/premium-api-explorer.aspx) that allows us to gather historical weather conditions from July 1, 2008.

However, we will not directly deal with its native API but use the Python package wwo-hist (https://github.com/ekapope/WorldWeatherOnline) to export the data directly to a pandas DataFrame.

How to do it…

Open Colab and create a new notebook. In the coding area, do the following:

  1. Install the wwo-hist package:

    !pip install wwo-hist

  2. Import the retrieve_hist_data function from wwo-hist:

    from wwo_hist import retrieve_hist_data

retrieve_hist_data is the only function required to acquire data from WorldWeatherOnline and can export to either pandas DataFrames or CSV files.

  1. Acquire data for ten years (01-JAN-2011 to 31-DEC-2020) with an hourly frequency from Canazei:

    frequency=1

    api_key = 'YOUR_API_KEY'

    location_list = [canazei]

    df_weather = retrieve_hist_data(api_key,

                                    location_list,

                                    '01-JAN-2011',

                                    '31-DEC-2020',

                                    frequency,

                                    location_label = False,

                                    export_csv = False,

                                    store_df = True)

www-hist will export the data to df_weather, a list of pandas DataFrames.

In this step, we set the input arguments for retrieve_hist_data. Let's unpack all of them:

  • API key: The API key is reported in the WorldWeatherOnline subscription dashboard, and it should replace the YOUR_API_KEY string.
  • Location: This is the list of locations from which to acquire the weather data. Since we are building a dataset to forecast the snow, we should consider places where it snows periodically. For example, you can consider Canazei (https://en.wikipedia.org/wiki/Canazei), located in the north of Italy, where snowfall can occur at any point between December and March. We could also add other locations to make the ML model more generic.
  • Start date/End date: The start and end dates define the temporal interval in which to gather the data. The date format is dd-mmm-yyyy. Since we want a large representative dataset, we query 10 years of weather data. Therefore, the interval time is set to 01-JAN-201131-DEC-2020.
  • Frequency: This defines the hourly frequency. For example, 1 stands for every hour, 3 for every three hours, 6 for every six hours, and so on. We opt for an hourly frequency since we need the temperature and humidity of the last three hours to forecast snow.
  • Location label: Since we might need to acquire data from different locations, this flag binds the acquired weather data to the place. We set this option to False because we are only using a single location.
  • export_csv: This is the flag to export the weather data to a CSV file. We set it to False because we do not need to export the data to a CSV file.
  • store_df: This is the flag to export the weather data to a pandas DataFrame. We set it to True.

Once the weather data is retrieved, the console output will report export to canazei completed!.

  1. Export temperature, humidity, and output snowfall to lists:

    t_list = df_weather[0].tempC.astype(float).to_list()

    h_list = df_weather[0].humidity.astype(float).to_list()

    s_list = df_weather[0].totalSnow_cm.astype(float).to_         list()

The generated df_weather[] dataset includes several weather conditions for each requested date and time. For example, we can find the pressure in millibars, cloud coverage in percentage, visibility in kilometers, and, of course, the physical quantities that we're interested in:

  • tempC: The temperature in degrees Celsius (°C)
  • humidity: The relative air humidity in percentage (%)
  • totalSnow_cm: Total snowfall in centimeters (cm)

In this final step, we export the hourly temperature, humidity, and snowfall in cm to three lists using the to_list() method.

Now, we have all we need to prepare the dataset for forecasting the snow.

Preparing the dataset

Preparing a dataset is a crucial phase in any ML project because it has implications for the effectiveness of the trained model.

In this recipe, we will put into action two techniques to make the dataset more suitable to get a more accurate model. These two techniques will balance the dataset with standardization and bring the input features into the same numerical range.

The following Colab file (see the Preparing the dataset section in the following repository) contains the code referred to in this recipe:

  • preparing_model.ipynb:

https://github.com/PacktPublishing/TinyML-Cookbook/blob/main/Chapter03/ColabNotebooks/preparing_model.ipynb

Getting ready

The temperature and humidity of the last three hours are our input features. If you wonder why we use the last three hours' weather conditions, it is just so we have more input features and Increase the chance of higher classification accuracy.

To get ready for the dataset preparation, we need to know why the dataset needs to be balanced and why the raw input features should not be used for training. These two aspects will be examined in the following subsections.

Preparing a balanced dataset

An unbalanced dataset is a dataset where one of the classes has considerably more samples than the others. Training with an unbalanced dataset could produce a model with high accuracy but that's incapable of solving our problem. For example, consider a dataset where one of the two classes has 99% of the samples. If the network miss-classified the minority class, we would still have 99% accuracy, but the model would be ineffective.

Therefore, we require a balanced dataset with roughly the same input samples for each output category.

Balancing a dataset can be done with the following techniques:

  • Acquiring more input samples for the minority class: This should be the first thing we do to ensure we have correctly generated the dataset. However, it is not always possible to collect more data, particularly when dealing with infrequent events.
  • Oversampling the minority class: We could randomly duplicate samples from the under-represented class. However, this approach may increase the risk of overfitting the minority class if we duplicate many instances.
  • Undersampling the majority class: We could randomly delete samples from the over-represented class. Since this approach reduces the dataset's size, we could lose valuable training information.
  • Generating synthetic samples for the minority class: We could develop artificially manufactured samples. The most common algorithm for this is Synthetic Minority Over-sampling Technique (SMOTE). SMOTE is an oversampling technique that creates new samples instead of duplicating under-represented instances. Although this technique reduces the risk of overfitting caused by oversampling, the generated synthetic samples could be incorrect near the class separation border, adding undesirable noise to the dataset.

As we can see, despite the variety of techniques, there is not an overall best solution to fix an unbalanced dataset. The method or methods to adopt will depend on the problem to solve.

Feature scaling with Z-score

Our input features exist in different numerical ranges. For example, humidity is always between 0 and 100, while the temperature on the Celsius scale can be negative and has a smaller positive numerical range than humidity.

This is a typical scenario when dealing with various physical quantities and could impact the effectiveness of the training.

Generally, if the input features have different numerical ranges, the ML model may not generalize properly because it will be influenced more by the features with more significant values. Therefore, the input features need to be rescaled to ensure that each input feature contributes equally during training. Furthermore, another benefit of feature scaling in neural networks is that it helps converge the gradient descent faster toward the minima.

Z-score is a common scaling technique adopted in neural networks, and it is defined with the following formula:

Let's break down this formula:

  • : the mean of the input features
  • : the standard deviation of the input features

Z-score can bring the input features to a similar numerical range, but not necessarily between zero and one.

How to do it…

Continue working on the Colab file and follow the following steps to discover how to balance the dataset and rescale the input features with Z-score:

  1. Visualize the extracted physical measurements (temperature, humidity, and snow) in a 2D scatter chart. To do so, consider the snow formation only when the snowfall (totalSnow_cm) is above 0.5 cm:

    def binarize(snow, threshold):

      if snow > threshold:

        return 1

      else:

        return 0

    s_bin_list = [binarize(snow, 0.5) for snow in s_list]

    cm = plt.cm.get_cmap('gray_r')

    sc = plt.scatter(t_list, h_list, c=s_bin_list, cmap=cm, label="Snow")

    plt.figure(dpi=150)

    plt.colorbar(sc)

    plt.legend()

    plt.grid(True)

    plt.title("Snow(T, H)")

    plt.xlabel("Temperature - °C")

    plt.ylabel("Humidity - %")

    plt.show()

The preceding code generates the following scatter plot:

Figure 3.1 – Visualization of the temperature, humidity, and snow in a 2D chart. 
Data provided by WorldWeatherOnline.com

Figure 3.1 – Visualization of the temperature, humidity, and snow in a 2D chart. Data provided by WorldWeatherOnline.com

In the preceding chart, the x-axis is the temperature, the y-axis is the humidity, and the black dot is the snow formation.

As you can observe from the distribution of the black dots, there are cases where the snow formation is reported for temperatures well above 0°C.

To simplify the recipe, we can ignore these cases and consider 2° C as the maximum temperature for the snow formation.

  1. Generate the output labels (Yes and No):

    def gen_label(snow, temperature):

      if snow > 0.5 and temperature < 2:

        return "Yes"

      else:

        return "No"

    snow_labels = [gen_label(snow, temp) for snow, temp in zip(s_list, t_list)]

Since we are only forecasting snow, only two classes are needed: Yes, it snows, or No, it does not snow. At this scope, we convert totalSnow_cm to the corresponding class (Yes or No) through the gen_label() function. The mapping function assigns Yes when totalSnow_cm exceeds 0.5 cm and the temperature is below 2° C.

  1. Build the dataset:

    csv_header = ["Temp0", "Temp1", "Temp2", "Humi0", "Humi1", "Humi2", "Snow"]

    df_dataset = pd.DataFrame(list(zip(t_list[:-2], t_list[1:-1], t_list[2:], h_list[:-2], h_list[1:-1], h_list[2:], snow_labels[2:])), columns = csv_header)

If t0 is the current time, the values stored in the dataset are as follows:

  • Temp0/Humi0: Temperature and humidity at time t = t0 - 2
  • Temp1/Humi1: Temperature and humidity at time t = t0 - 1
  • Temp2/Humi2: Temperature and humidity at time t = t0
  • Snow: Label reporting whether it will snow at time t = t0

Therefore, we just need a zip and a few indices calculations to build the dataset.

  1. Balance the dataset by undersampling the majority class:

    df0 = df_dataset[df_dataset['Snow'] == "No"]

    df1 = df_dataset[df_dataset['Snow'] == "Yes"]

    if len(df1.index) < len(df0.index):

      df0_sub = df0.sample(len(df1.index))

      df_dataset = pd.concat([df0_sub, df1])

    else:

      df1_sub = df1.sample(len(df0.index))

      df_dataset = pd.concat([df1_sub, df0])

The original dataset is unbalanced because, in the selected location, it typically snows during the winter season, which lasts from December to March. The following bar chart shows that the No class represents 87% of all cases, so we need to apply one of the techniques shown in the Getting ready section to balance the dataset.

Figure 3.2 – Distribution of the dataset samples

Figure 3.2 – Distribution of the dataset samples

Since the minority class has many samples (~5000), we can randomly undersample the majority class so the two categories have the same number of observations.

  1. Scale the input features with Z-score independently. To do so, extract all the temperature and humidity values:

    t_list = df_dataset['Temp0'].tolist()

    h_list = df_dataset['Humi0'].tolist()

    t_list = t_list + df_dataset['Temp2'].tail(2).tolist()

    h_list = h_list + df_dataset['Humi2'].tail(2).tolist()

You can get all the temperature (or humidity) values from the Temp0 (or Humi0) column and the last two records of the Temp2 (or Humi2) column.

Next, calculate the mean and standard deviation of the temperature and humidity input features:

t_avg = mean(t_list)

h_avg = mean(h_list)

t_std = std(t_list)

h_std = std(h_list)

print("COPY ME!")

print("Temperature - [MEAN, STD]  ", round(t_avg, 5), round(t_std, 5))

print("Humidity - [MEAN, STD]     ", round(h_avg, 5), round(h_std, 5))

The expected output is as follows:

Figure 3.3 – Expected mean and standard deviation values

Figure 3.3 – Expected mean and standard deviation values

Copy the mean and standard deviation values printed in the output log because they will be required when deploying the application on the Arduino Nano and Raspberry Pi Pico.

Finally, scale the input features with Z-score:

def scaling(val, avg, std):

  return (val - avg) / (std)

df_dataset['Temp0']=df_dataset['Temp0'].apply(lambda x: scaling(x, t_avg, t_std))

df_dataset['Temp1']=df_dataset['Temp1'].apply(lambda x: scaling(x, t_avg, t_std))

df_dataset['Temp2']=df_dataset['Temp2'].apply(lambda x: scaling(x, t_avg, t_std))

df_dataset['Humi0']=df_dataset['Humi0'].apply(lambda x: scaling(x, h_avg, h_std))

df_dataset['Humi1']=df_dataset['Humi1'].apply(lambda x: scaling(x, h_avg, h_std))

df_dataset['Humi2']=df_dataset['Humi2'].apply(lambda x: scaling(x, h_avg, h_std))

The following charts compare the raw and scaled input feature distributions:

Figure 3.4 – Raw (left charts) and scaled (right charts) input feature distributions

Figure 3.4 – Raw (left charts) and scaled (right charts) input feature distributions

As you can observe from the charts, Z-score provides roughly the same value range (the x axis) for both features.

Now, the dataset is ready to be used for training our snow forecast model!

Training the ML model with TF

The model designed for forecasting the snow is a binary classifier, and it is illustrated in the following diagram:

Figure 3.5 – Neural network model for forecasting the snow

Figure 3.5 – Neural network model for forecasting the snow

The network consists of the following layers:

  • 1 x fully connected layers with 12 neurons and followed by a ReLU activation function
  • 1 x dropout layer with a 20% rate (0.2) to prevent overfitting
  • 1 x fully connected layer with one output neuron and followed by a sigmoid activation function

In this recipe, we will train the preceding model with TF.

The following Colab file (see the Training the ML model with TF section in the following repository) contains the code referred to in this recipe:

  • preparing_model.ipynb:

https://github.com/PacktPublishing/TinyML-Cookbook/blob/main/Chapter03/ColabNotebooks/preparing_model.ipynb

Getting ready

The model designed in this recipe has one input and output node. The input node provides the six input features to the network: the temperature and humidity for each of the last three hours.

The model consumes the input features and returns the probability of the class in the output node. Since the sigmoid function produces the output, the result is between zero and one and considered No when it is below 0.5; otherwise, it's Yes.

In general, we consider the following four sequential steps when training a neural network:

  1. Encoding the output labels
  2. Splitting the dataset into training, test, and validation datasets
  3. Creating the model
  4. Training the model

In this recipe, we will use TF and scikit-learn to implement them.

Scikit-Learn (https://scikit-learn.org/stable/) is a higher-level Python library for implementing generic ML algorithms, such as SVMs, random forests, and logistic regression. It is not a DNN-specific framework but rather a software library for a wide range of ML algorithms.

How to do it…

The following steps show how to train the model presented in the Getting ready section with TF:

  1. Extract the input features (x) and output labels (y) from the df_dataset pandas DataFrame:

    f_names = df_dataset.columns.values[0:6]

    l_name  = df_dataset.columns.values[6:7]

    x = df_dataset[f_names]

    y = df_dataset[l_name]

  2. Encode the labels to numerical values:

    labelencoder = LabelEncoder()

    labelencoder.fit(y.Snow)

    y_encoded = labelencoder.transform(y.Snow)

This step converts the output labels (Yes and No) to numerical values since neural networks can only deal with numbers. We use scikit-learn to transform the target labels to integer values (zero and one). The conversion requires calling the following three functions:

  1. LabelEncoder() to initialize the LabelEncoder module
  2. fit() to identify the target integer values by parsing the output labels
  3. transform() to translate the output labels to numerical values

After transform(), the encoded labels are available in y_encoded.

  1. Split the dataset into train, validation, and test datasets:

    # Split 1 (85% vs 15%)

    x_train, x_validate_test, y_train, y_validate_test = train_test_split(x, y_encoded, test_size=0.15, random_state = 1)

    # Split 2 (50% vs 50%)

    x_test, x_validate, y_test, y_validate = train_test_split(x_validate_test, y_validate_test, test_size=0.50, random_state = 3)

The following diagram shows how we split the train, validation, and test datasets:

Figure 3.6 – The dataset is split into the train, validation, and test datasets

Figure 3.6 – The dataset is split into the train, validation, and test datasets

These three datasets are as follows:

  • Training dataset: This dataset contains the samples to train the model. The weights and biases are learned with these data.
  • Validation dataset: This dataset contains the samples to evaluate the model's accuracy on unseen data. The dataset is used during the training process to indicate how well the model generalizes because it includes instances not included in the training dataset. However, since this dataset is still used during training, we could indirectly influence the output model by fine-tuning some training hyperparameters.
  • Test dataset: This dataset contains the samples for testing the model after training. Since the test dataset is not employed during training, it evaluates the final model without bias.

From the original dataset, we assign 85% to the training dataset, 7.5% to the validation dataset, and 7.5% to the test dataset. With this split, the validation and test dataset will have roughly 1,000 samples each, enough to see if the model works properly.

The dataset splitting is done with the train_test_split() function from scikit-learn which splits the dataset into training and test datasets. The split proportion is defined with the test_size (or train_size) input argument, representing the input dataset's percentage to include in the test (or train) split.

We call this function twice to generate the three different datasets. The first split generates the 85% training dataset by providing test_size=0.15. The second split produces the validation and test datasets by halving the 15% dataset from the first split.

  1. Create the model with the Keras API:

    model = tf.keras.Sequential()

    model.add(layers.Dense(12, activation='relu', input_shape=(len(f_names),)))

    model.add(layers.Dropout(0.2))

    model.add(layers.Dense(1, activation='sigmoid'))

    model.summary()

The preceding code generates the following output:

Figure 3.7 – Model summary returned by model.summary()

Figure 3.7 – Model summary returned by model.summary()

The summary reports useful architecture information about the neural network model, such as the layer types, the output shapes, and the number of trainable weights required.

Important Note

In TinyML, it is important to keep an eye on the number of weights because it is related to the program's memory utilization.

  1. Compile the model:

    model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])

In this step, we initialize the training parameters, such as the following:

  • Loss function: Training aims to find weights and biases to minimize a loss function. The loss indicates how far the predicted output is from the expected result, so the lower the loss, the better the model. Cross-entropy is the standard loss function for classification problems because it produces faster training with a better model generalization. For a binary classifier, we should use binary_crossentropy.
  • Performance metrics: Performance metrics evaluate how well the model predicts the output classes. We use accuracy, defined as the ratio between the number of correct predictions and the total number of tests:
  • Optimizer: The optimizer is the algorithm used to update the weights of the network during training. The optimizer mainly affects the training time. In our example, we use the widely adopted Adam optimizer.

Once we have initialized the training parameters, we can train the model.

  1. Train the model:

    NUM_EPOCHS=20

    BATCH_SIZE=64

    history = model.fit(x_train, y_train, epochs=NUM_EPOCHS, batch_size=BATCH_SIZE, validation_data=(x_validate, y_validate))

During training, TF reports the loss and accuracy after each epoch on both the train and validation datasets, as shown in the following screenshot:

Figure 3.8 – Accuracy and loss are reported on both the train and validation datasets

Figure 3.8 – Accuracy and loss are reported on both the train and validation datasets

accuracy and loss are the accuracy and loss on the train data, while val_accuracy and val_loss are the accuracy and loss on the validation data.

It is best to rely on the accuracy and loss of the validation data to prevent overfitting and to see how the model behaves on unseen data.

  1. Plot the accuracy and loss over training epochs:

    loss_train = history.history['loss']

    loss_val   = history.history['val_loss']

    acc_train  = history.history['accuracy']

    acc_val    = history.history['val_accuracy']

    epochs     = range(1, NUM_EPOCHS + 1)

    def plot_train_val_history(x, y_train, y_val, type_txt):

      plt.figure(figsize = (10,7))

      plt.plot(x, y_train, 'g', label='Training'+type_txt)

      plt.plot(x, y_val, 'b', label='Validation'+type_txt)

      plt.title('Training and Validation'+type_txt)

      plt.xlabel('Epochs')

      plt.ylabel(type_txt)

      plt.legend()

      plt.show()

    plot_train_val_history(epochs, loss_train, loss_val, "Loss")

    plot_train_val_history(epochs, acc_train, acc_val, "Accuracy")

The preceding code plots the following two charts:

Figure 3.9 – Plot of the accuracy (left chart) and loss (right chart) over training epochs

Figure 3.9 – Plot of the accuracy (left chart) and loss (right chart) over training epochs

From the plots of the accuracy and loss during training, we can see the trend of the model's performance. The trend tells us whether we should train less to avoid overfitting or more to prevent underfitting. The validation accuracy and loss are at their best around ten epochs in our case. Therefore, we should consider terminating the training earlier to prevent overfitting. To do so, you can either re-train the network for ten epochs or use the EarlyStopping Keras function to stop training when a monitored performance metric has stopped improving. You can discover more about EarlyStopping at the following link: https://www.tensorflow.org/api_docs/python/tf/keras/callbacks/EarlyStopping.

  1. Save the entire TF model as a SavedModel:

    model.save("snow_forecast")

SavedModel is a directory containing the following:

Therefore, the preceding command creates the snow_forecast folder, which you can explore using the file explorer pane on the left of Colab.

We have finally in our hands a model to forecast the snow!

Evaluating the model's effectiveness

Accuracy and loss are not enough to judge the model's effectiveness. In general, accuracy is a good performance indicator if the dataset is balanced, but it does not tell us the strengths and weaknesses of our model. For instance, what classes do we recognize with high confidence? What frequent mistakes does the model make?

This recipe will judge the model's effectiveness by visualizing the confusion matrix and evaluating the recall, precision, and F1-score performance metrics.

The following Colab file (see the Evaluating the model's effectiveness section in the following repository) contains the code referred to in this recipe:

  • preparing_model.ipynb:

https://github.com/PacktPublishing/TinyML-Cookbook/blob/main/Chapter03/ColabNotebooks/preparing_model.ipynb

Getting ready

To complete this recipe, we need to know what a confusion matrix is and which performance metrics we can use to understand whether the model works fine.

The following subsections will examine these performance indicators.

Visualizing the performance with the confusion matrix

A confusion matrix is an NxN matrix reporting the number of correct and incorrect predictions on the test dataset.

For our binary classification model, we have a 2x2 matrix like the one in the following diagram:

Figure 3.10 – Confusion matrix

Figure 3.10 – Confusion matrix

The four values reported in the previous confusion matrix are as follows:

  • True positive (TP): The number of predicted positive results that are actually positive
  • True negative (TN): The number of predicted negative results that are actually negative
  • False positive (FP): The number of predicted positive results that are actually negative
  • False negative (FN): The number of predicted negative results that are actually positive

Ideally, we would like to have 100% accuracy, therefore, zero in the gray cells (FN and FP) of the confusion matrix reported in Figure 3.10. In fact, from the confusion matrix, we can calculate the accuracy using the following formula:

However, as previously mentioned, we are more interested in alternative performance metrics. These performance indicators are described in the following subsection.

Evaluating recall, precision, and F-score

The first performance metric evaluated is recall, defined as follows:

This metric tells us how many of all positive ("Yes") samples we predicted correctly. Recall should be as high as possible.

However, this metric does not consider the misclassification of negative samples. In short, the model could be excellent at classifying positive samples but incapable of classifying negative ones.

For this reason, there is another metric that takes into consideration FPs. It is precision, defined as follows:

This metric tells us how many predicted positive classes ("yes") were actually positive. Precision should be as high as possible.

Another key performance metric combines recall and precision with a single formula. It is F-score, defined as follows:

This formula helps us to evaluate the recall and precision metrics at the same time. Also, a high F-score implies a good model performance.

How to do it…

The following steps will teach us how to visualize the confusion matrix and calculate the recall, precision, and F-score metrics:

  1. Visualize the confusion matrix:

    y_test_pred = model.predict(x_test)

    y_test_pred = (y_test_pred > 0.5).astype("int32")

    cm = sklearn.metrics.confusion_matrix(y_test, y_test_pred)

    index_names  = ["Actual No Snow", "Actual Snow"]

    column_names = ["Predicted No Snow", "Predicted Snow"]

    df_cm = pd.DataFrame(cm, index = index_names, columns = column_names)

    plt.figure(figsize = (10,7))

    sns.heatmap(df_cm, annot=True, fmt='d', cmap="Blues")

    plt.figure(figsize = (10,7))

The previous code produces the following output:

Figure 3.11 – Confusion matrix for the snow forecast model

Figure 3.11 – Confusion matrix for the snow forecast model

The confusion matrix is obtained with the following two steps:

  1. Predict the labels on the test dataset using model.predict() and threshold the output result at 0.5. The thresholding is required because model.predict() returns the output of the sigmoid function, which is a value between zero and one.
  2. Use the confusion_matrix() function from the scikit-learn library to calculate the confusion matrix (cm).

From Figure 3.11, we can see that the samples are mainly distributed in the leading diagonal, and there are more FPs than FNs. Therefore, although the network is suitable for detecting snow, we should expect some false detections.

  1. Calculate the recall, precision, and F-score performance metrics:

    TN = cm[0][0]

    TP = cm[1][1]

    FN = cm[1][0]

    FP = cm[0][1]

    precision = TP / (TP + FP)

    recall = TP / (TP + FN)

    f_score = (2 * recall * precision) / (recall + precision)

    print("Recall:    ", round(recall, 3))

    print("Precision: ", round(precision, 3))

    print("F-score:     ", round(f_score, 3))

The preceding code prints the following information on the output console:

Figure 3.12 – Expected results for precision, recall, and F-score

Figure 3.12 – Expected results for precision, recall, and F-score

As we can see from the expected results, Recall equals 0.983, so our model can forecast the snow with high confidence. However, the Precision is lower, 0.808. This metric shows that we should expect some false alarms from our model. Finally, the value of 0.887 obtained for the F-score tells us that Recall and Precision are balanced. Therefore, we have a good ML model in our hands capable of forecasting the snow with the input features provided.

The model is now trained and validated. Hence, it is time to make it suitable for microcontroller deployment.

Quantizing the model with the TFLite converter

Exporting the trained network as SavedModel saves the training graphs such as the network architecture, weights, training variables, and checkpoints. Therefore, the generated TF model is perfect for sharing or resuming a training session but not suitable for microcontroller deployment for the following reasons:

  • The weights are stored in floating-point format.
  • The model keeps information that's not required for the inference.

Since our target device has computational and memory constraints, it is crucial to transform the trained model into something compact.

This recipe will teach how to quantize and convert the trained model into a lightweight, memory-efficient, and easy-to-parse exporting format with TensorFlow Lite (TFLite). The generated model will then be converted to a C-byte array, suitable for microcontroller deployments.

The following Colab file (see the Quantizing the model with TFLite converter section in the following recipe) contains the code referred to in this recipe:

  • preparing_model.ipynb:

https://github.com/PacktPublishing/TinyML-Cookbook/blob/main/Chapter03/ColabNotebooks/preparing_model.ipynb

Getting ready

The main ingredients used in this recipe are the TFLite converter and quantization.

TFLite (https://www.tensorflow.org/lite) is a deep learning framework specifically for inference on edge devices such as smartphones or embedded platforms.

As reported in the following diagram, TFLite provides a set of tools for the following:

  • Converting the TF model into a lightweight representation
  • Running the model efficiently on the target device
Figure 3.13 – TFLite components

Figure 3.13 – TFLite components

The lightweight model representation used by TFLite is identified with the .tflite extension, and it is internally represented as FlatBuffers (https://google.github.io/flatbuffers/). The FlatBuffers format offers a flexible, easy-to-parse, and memory-efficient structure. The TFLite converter is responsible for converting the TF model to FlatBuffers and applying optimizations based on 8-bit integer quantization to reduce the model size and improve latency.

Quantizing the input model

An indispensable technique to make the model suitable for microcontrollers is quantization.

Model quantization, or simply quantization, has three significant advantages:

  • It reduces the model size by converting all the weights to lower bit precision.
  • It reduces the power consumption by reducing the memory bandwidth.
  • It improves inference performance by employing integer arithmetic for all the operations.

This widely adopted technique applies the quantization after training and converts the 32-bit floating-point weights to 8-bit integer values. To understand how quantization works, consider the following C-like function that approximates a 32-bit floating-point value using an 8-bit value:

float dequantize(int8 x, float zero_point, float scale) {

  return ((float)x - zero_point) * scale;

}

In the proceeding code, x is the quantized value represented as an 8-bit signed integer value, while scale and zero_point are the quantization parameters. The scale parameter is used to map our quantized value to the floating-point domain and vice versa. zero_point is the offset to consider for the quantized range.

To understand why the zero_point could not be zero, consider the following floating-point input distribution that we want to scale to the 8-bit range:

Figure 3.14 – Example where the distribution of the values is shifted toward the negative range

Figure 3.14 – Example where the distribution of the values is shifted toward the negative range

The proceeding figure shows that the input floating-point distribution is not zero-centered but shifted toward the negative range. Therefore, if we simply scaled the floating-point values to 8-bit, we could have the following:

  • Multiple negative input values with the same 8-bit counterpart
  • Many positive 8-bit values unused

Therefore, it would be inefficient to assign zero to zero_point since we could dedicate a larger range to the negative values to reduce their quantization error, defined as follows:

When zero_point is not zero, we commonly call the quantization asymmetric because we assign a different range of values for the positive and negative sides, as shown in the following diagram:

Figure 3.15 – Asymmetric quantization

Figure 3.15 – Asymmetric quantization

When zero_point is zero, we commonly call the quantization symmetric because it is symmetric about zero, as we can see in the following diagram:

Figure 3.16 – Symmetric quantization

Figure 3.16 – Symmetric quantization

Commonly, we apply symmetric quantization to the model's weights and asymmetric quantization to the input and output of the layers.

The scale and zero_point values are the only parameters required for quantization and are commonly provided in the following ways:

  • Per-tensor: The quantization parameters are the same for all tensor elements.
  • Per-channel: The quantization parameters are different for each feature map of the tensor.

The following diagram visually describes per-tensor and per-channel quantization:

Figure 3.17 – Per-tensor versus per-channel quantization

Figure 3.17 – Per-tensor versus per-channel quantization

Commonly, we adopt the per-tensor approach except for the weights and biases of the convolution and depth-wise convolution layers.

How to do it…

The following steps show how to use the TFLite converter to quantize and produce a suitable model for microcontrollers:

  1. Select a few hundred samples randomly from the test dataset to calibrate the quantization:

    def representative_data_gen():

      for i_value in tf.data.Dataset.from_tensor_slices(x_test).batch(1).take(100):

        i_value_f32 = tf.dtypes.cast(i_value, tf.float32)

        yield [i_value_f32]

This step is commonly called generating a representative dataset, and it is essential to reduce the risk of an accuracy drop in the quantization. In fact, the converter uses this set of samples to find out the range of the input values and then estimate the quantization parameters. Typically, a hundred samples is enough and can be taken from the test or training dataset. In our case, we used the test dataset.

  1. Import the TF SavedModel directory into TFLite converter:

    converter = tf.lite.TFLiteConverter.from_saved_model("snow_forecast")

  2. Initialize the TFLite converter for the 8-bit quantization:

    # Representative dataset

    converter.representative_dataset = tf.lite.RepresentativeDataset(representative_data_gen)

    # Optimizations

    converter.optimizations = [tf.lite.Optimize.DEFAULT]

    # Supported ops

    converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]

    # Inference input/output type

    converter.inference_input_type = tf.int8

    converter.inference_output_type = tf.int8

In this step, we configure the TFLite converter to apply the 8-bit quantization. The input arguments passed to the tool are as follows:

  • Representative dataset: This is the representative dataset generated in the first step.
  • Optimizations: This defines the optimization strategy to adopt. At the moment, only DEFAULT optimization is supported, which tries to optimize for both size and latency, minimizing the accuracy drop.
  • Supported ops: This forces the adoption of only integer 8-bit operators during the conversion. If our model has unsupported kernels, the conversion will not succeed.
  • Inference input/output type: This adopts the 8-bit quantization format for the network's input and output. Therefore, we will need to feed the ML model with the quantized input features to run the inference correctly.

Once we have initialized the TFLite converter, we can execute the conversion:

tflite_model_quant = converter.convert()

  1. Save the converted model as .tflite:

    open("snow_forecast_model.tflite", "wb").write(tflite_model_quant)

  2. Convert the TFLite model to a C-byte array with xxd:

    !apt-get update && apt-get -qq install xxd

    !xxd -i snow_forecast_model.tflite > model.h

The previous command outputs a C header file (the -i option) containing the TFLite model as an unsigned char array with many hexadecimal numbers. However, in the Getting ready section, we mentioned that the model is a file with a .tflite extension. Therefore, why do we need this extra conversion? The conversion to a C-byte array is crucial for deploying the model on microcontrollers because the .tflite format requires an additional software library into our application to load the file from memory. We need to remember that most microcontrollers do not have OS and native filesystem support. Therefore, the C-byte array format allows us to integrate the model directly into the application. The other important reason for this conversion is that the .tflite file does not allow keeping the weights in program memory. Since every byte matters and the SRAM has a limited capacity, keeping the model in program memory is generally more memory efficient when the weights are constant.

Now, you can download the generated model.h file from Colab's left pane. The TFLite model is stored in the snow_forecast_model_tflite array.

Using the built-in temperature and humidity sensor on Arduino Nano

As we know, the Arduino Nano and Raspberry Pi Pico have unique hardware features that make them ideal for tackling different development scenarios. For example, the Arduino Nano have a built-in temperature and humidity sensor so that we do not need external components for our project with this board.

In this recipe, we will show how to read the temperature and humidity sensor data on an Arduino Nano.

The following Arduino sketch contains the code referred to in this recipe:

  • 06_sensor_arduino_nano.ino:

https://github.com/PacktPublishing/TinyML-Cookbook/blob/main/Chapter03/ArduinoSketches/06_sensor_arduino_nano.ino

Getting ready

There are no particular new things to know to accomplish this task. Therefore, this Getting ready section will give just an overview of the main characteristics of the built-in temperature and humidity sensor on the Arduino Nano.

The Arduino Nano board integrates the HTS221 (https://www.st.com/resource/en/datasheet/HTS221.pdf) sensor from ST (https://www.st.com/content/st_com/en.html) for relative humidity and temperature measurements.

The sensor is ultra-compact (2x2mm) and provides the measurements through two digital serial interfaces. The following table reports the main characteristics of this sensing element:

Figure 3.18 – Key characteristics of the HTS221 temperature and humidity sensor

Figure 3.18 – Key characteristics of the HTS221 temperature and humidity sensor

As we can see from the table, the sensor is extremely low-power since it has a current power consumption in the range of µA.

How to do it…

Create a new sketch on the Arduino IDE and follow the following steps to initialize and test the temperature and humidity sensor on an Arduino Nano:

  1. Include the Arduino_HTS221.h C header file in the sketch:

    #include <Arduino_HTS221.h>

  2. Create function-like macros for reading the temperature and humidity:

    #define READ_TEMPERATURE() HTS.readTemperature()

    #define READ_HUMIDITY() HTS.readHumidity()

The reason for defining the preceding two C macros is because the Raspberry Pi Pico will use different functions to read the temperature and humidity from the sensor. Therefore, it is more practical to have a common interface so that our Arduino Nano and Raspberry Pi Pico applications can share most of their code.

  1. Initialize both the serial peripheral and the HTS221 sensor in the setup() function:

    void setup() {

      Serial.begin(9600);

      while (!Serial);

      if (!HTS.begin()) {

        Serial.println("Failed initialization of HTS221!");

        while (1);

      }

    }

The serial peripheral will be used to return the classification result.

Important Note

As reported in the FAQ of the Arduino Nano 33 BLE Sense Board, due to self-heating, when the board is powered by USB, the HTS221 becomes unreliable and shows an offset in each reading that changes with the external temperature.

We recommend disconnecting the USB cable and powering the board with batteries through the VIN pin to obtain reliable measurements. Refer to Chapter 2, Prototyping with Microcontrollers, to discover how to power an Arduino Nano with batteries.

Using the DHT22 sensor with the Raspberry Pi Pico

In contrast to the Arduino Nano, the Raspberry Pi Pico requires an external sensor module and an additional software library to measure the temperature and humidity.

In this recipe, we will show how to use the DHT22 sensor with a Raspberry Pico to get temperature and humidity measurements.

The following Arduino sketch contains the code referred to in this recipe:

  • 07_sensor_rasp_pico.ino:

https://github.com/PacktPublishing/TinyML-Cookbook/blob/main/Chapter03/ArduinoSketches/07_sensor_rasp_pico.ino

Getting ready

The temperature and humidity sensor module considered for the Raspberry Pi Pic is the low-cost AM2302 that you can get either from Adafruit (https://www.adafruit.com/product/393) or Amazon.

As shown in the following diagram, the AM2302 module is a through-hole component with three pins that integrates the DHT22 temperature and humidity sensor:

Figure 3.19 – The AM2302 module with the DHT22 sensor

Figure 3.19 – The AM2302 module with the DHT22 sensor

The following table summarizes the key characteristics of the DHT22 sensor:

Figure 3.20 – Key characteristics of the DHT22 temperature and humidity sensor

Figure 3.20 – Key characteristics of the DHT22 temperature and humidity sensor

Note

DHT11 is another popular temperature and humidity sensor from the DHT family. However, we cannot use it in our recipe because it has a good temperature accuracy only between 0 °C and 50 °C.

In contrast to the HTS221 sensor on the Arduino Nano, the DHT22 has a digital protocol to read the temperature and humidity values. The protocol must be implemented through the GPIO peripheral and requires precise timing to read the data. Luckily, Adafruit developed a software library (https://github.com/adafruit/DHT-sensor-library) for the DHT sensors, so we do not have to worry about it. The library will deal with the low-level software details and provide an API to read the temperature and humidity.

How to do it…

Create a new sketch on the Arduino IDE and follow these steps to use the DHT22 sensor with a Raspberry Pi Pico:

  1. Connect the DHT22 sensor to the Raspberry Pi Pico. Use the G10 (row 14) GPIO on the Raspberry Pi Pico for the DHT22 data terminal:
Figure 3.21 – Complete circuit with the Raspberry Pi Pico and the AM2302 sensor module

Figure 3.21 – Complete circuit with the Raspberry Pi Pico and the AM2302 sensor module

  1. Download the latest release of the DHT sensor software library from https://www.arduino.cc/reference/en/libraries/dht-sensor-library/. In the Arduino IDE, import the ZIP file by clicking on the Libraries tab on the left pane and Import, as shown in the following screenshot:
Figure 3.22 – Import the DHT sensor library in Arduino Web Editor

Figure 3.22 – Import the DHT sensor library in Arduino Web Editor

A pop-up window will tell us that the library has been successfully imported.

  1. Include the DHT.h C header file in the sketch:

    #include <DHT.h>

  2. Define a global DHT object to interface with the DHT22 sensor:

    const int gpio_pin_dht_pin = 10;

    DHT dht(gpio_pin_dht_pin, DHT22);

The DHT object is initialized with the GPIO pin used by the DHT22 data terminal (G10) and the type of DHT sensor (DHT22).

  1. Create function-like macros for reading the temperature and humidity:

    #define READ_TEMPERATURE() dht.readTemperature()

    #define READ_HUMIDITY() dht.readHumidity()

The function's name must be the same as the ones of the previous recipe. This step ensures a common function interface to measure the temperature and humidity on an Arduino Nano and a Raspberry Pi Pico.

  1. Initialize the serial peripheral and the DHT22 sensor in the setup() function:

    void setup() {

      Serial.begin(9600);

      while(!Serial);

      dht.begin();

      delay(2000);

    }

The DHT22 can only return new data after two seconds. For this reason, we use delay(2000) to wait for the peripheral to be ready.

Now, the Raspberry Pi Pico can read temperature and humidity sensor data.

Preparing the input features for the model inference

As we know, the model's input features are the scaled and quantized temperature and humidity of the last three hours. Using this data, the ML model can forecast whether it will snow.

In this recipe, we will see how to prepare the input data to feed into our ML model. In particular, this recipe will teach us how to acquire, scale, and quantize the sensor measurements and keep them in temporal order using a circular buffer.

The following Arduino sketch contains the code referred to in this recipe:

  • 08_input_features.ino:

https://github.com/PacktPublishing/TinyML-Cookbook/blob/main/Chapter03/ArduinoSketches/08_input_features.ino

Getting ready

Our application will acquire the temperature and humidity every hour to get the necessary input features for the model. However, how can we keep the last three measurements in temporal order to feed the network the correct input?

In this recipe, we will use a circular buffer, a fixed-sized data structure that implements a First-In-First-Out (FIFO) buffer.

This data structure is well-suited to buffering data streams and can be implemented with an array and a pointer that tells where to store the element in memory. The following diagram shows how a circular buffer with three elements works:

Figure 3.23 – Circular buffer with three elements

Figure 3.23 – Circular buffer with three elements

As you can see from the preceding diagram, this data structure simulates a ring since the pointer (Ptr) is incremented after each data insertion and wraps around when it reaches the end.

How to do it…

The instructions provided in this section apply to both the Arduino Nano and the Raspberry Pi Pico. Follow these steps to see how to create a circular buffer and prepare the input for the model inference:

  1. Define two global int8_t arrays of size three and an integer variable to implement the circular buffer data structure:

    #define NUM_HOURS 3

    int8_t t_vals [NUM_HOURS] = {0};

    int8_t h_vals [NUM_HOURS] = {0};

    int cur_idx = 0;

These two arrays will be used to keep the scaled and quantized temperature and humidity measurements in temporal order.

  1. Define two variables for the scale (float) and zero point (int32_t) quantization parameters of the input features:

    float   tflu_i_scale      = 0.0f;

    int32_t tflu_i_zero_point = 0;

The following recipe will extract these quantization parameters from the TF model. Please note that scale (tflu_i_scale) is a floating-point number, while zero point (tflu_i_zero_point) is a 32-bit integer.

  1. Take the average of three temperature and humidity samples, captured every three seconds in the loop() function:

    constexpr int num_reads = 3;

    void loop() {  

      float t = 0.0f;

      float h = 0.0f;

      for(int i = 0; i < num_reads; ++i) {

        t += READ_TEMPERATURE();

        h += READ_HUMIDITY();

        delay(3000);

      }

      t /= (float)num_reads;

      h /= (float)num_reads;

Capturing more than one sample is, in general, a good way to have a robust measurement.

  1. Scale the temperature and humidity data with Z-score in the loop() function:

    constexpr float t_mean  = 2.05179f;

    constexpr float h_mean  = 82.30551f;

    constexpr float t_std   = 7.33084f;

    constexpr float h_std   = 14.55707f;

    t = (t – t_mean) / t_std;

    h = (h – h_mean) / h_std;

Z-score requires the mean and standard deviation, which we calculated in the second recipe of this chapter.

  1. Quantize the input features in the loop() function:

    t_vals[cur_idx] = (t / tflu_i_scale) + tflu_i_zero_point;

    h_vals[cur_idx] = (h / tflu_i_scale) + tflu_i_zero_point;

The samples are quantized using the tflu_i_scale and tflu_i_zero_point input quantization parameters. Remember that the model's input uses the per-tensor quantization schema, so all input features need to be quantized with the same scale and zero-point.

  1. Store the temperature and humidity sensor in the circular array:

    t_vals[cur_idx] = t;

    h_vals[cur_idx] = h;

    cur_idx = (cur_idx + 1) % NUM_HOURS;

    delay(2000);

The pointer of the circular buffer (cur_index) is updated after each data insertion with the following formula:

In the preceding formula, is the size of the circular buffer, while and are the pointer's values before and after the data insertion.

Important Note

At the end of the code, we have a delay of two seconds, but it should be one hour in the actual application. The pause of two seconds is used to avoid waiting too long in our experiments.

On-device inference with TFLu

Here we are, with our first ML application on microcontrollers.

In this recipe, we will finally discover how to use TensorFlow Lite for Microcontrollers (TFLu) to run the TFLite model on an Arduino Nano and a Raspberry Pi Pico.

The following Arduino sketch contains the code referred to in this recipe:

  • 09_classification.ino:

https://github.com/PacktPublishing/TinyML-Cookbook/blob/main/Chapter03/ArduinoSketches/09_classification.ino

Getting ready

To get ready with this last recipe, we need to know how inference with TFLu works.

TFLu was introduced in Chapter 1, Getting Started with TinyML, and is the software component that runs TFLite models on microcontrollers.

Inference with TFLu typically consists of the following:

  1. Loading and parsing the model: TFLu parses the weights and network architecture stored in the C-byte array.
  2. Transforming the input data: The input data acquired from the sensor is converted to the expected format required by the model.
  3. Executing the model: TFLu executes the model using optimized DNN functions.

When dealing with microcontrollers, it is necessary to optimize every line of our code to keep the memory footprint at the minimum and maximize performance.

For this reason, TFLu also integrates software libraries to get the best performance from various target processors. For example, TFLu supports CMSIS-NN (https://www.keil.com/pack/doc/CMSIS/NN/html/index.html), a free and open source software library developed by Arm for optimized DNN operators on Arm Cortex-M architectures. These optimizations are relevant to the critical DNN primitives such as convolution, depth-wise convolution, and the fully connected layer, and are compatible with the Arm processors in the Arduino Nano and Raspberry Pi Pico.

At this point, you might have one question in mind: How can we use TFLu with CMSIS-NN?

We do not need to install additional libraries because TFLu for Arduino comes with CMSIS-NN. Therefore, Arduino will automatically include CMSIS-NN to run the inference faster when using TFLu.

How to do it…

The instructions in this section are applicable to both the Arduino Nano and the Raspberry Pi Pico. The following steps will show how to use TFLu to run the snow forecast TFLite model on our boards:

  1. Import the model.h file into the Arduino project. As shown in the following screenshot, click on the tab button with the upside-down triangle and click on Import File into Sketch.
Figure 3.24 – Importing the model.h file into the Arduino project

Figure 3.24 – Importing the model.h file into the Arduino project

A folder window will appear from which you can drag and drop the TFLu model's file.

Once the file has been imported, include the C header in the sketch:

#include "model.h"

  1. Include the header files required by TFLu:

    #include <TensorFlowLite.h>

    #include <tensorflow/lite/micro/all_ops_resolver.h>

    #include <tensorflow/lite/micro/micro_error_reporter.h>

    #include <tensorflow/lite/micro/micro_interpreter.h>

    #include <tensorflow/lite/schema/schema_generated.h>

    #include <tensorflow/lite/version.h>

The main header files are as follows:

  • all_ops_resolver.h: To load the DNN operators required for running the ML model
  • micro_error_reporter.h: To output the debug information returned by the TFLu runtime
  • micro_interpreter.h: To load and execute the ML model
  • schema_generated.h: For the schema of the TFLite FlatBuffer format
  • version.h: For the versioning of the TFLite schema

For more information about the header files, we recommend reading the Get started with microcontroller guide in the TF documentation (https://www.tensorflow.org/lite/microcontrollers/get_started_low_level).

  1. Declare the variables required by TFLu:

    const tflite::Model* tflu_model            = nullptr;

    tflite::MicroInterpreter* tflu_interpreter = nullptr;

    TfLiteTensor* tflu_i_tensor                = nullptr;

    TfLiteTensor* tflu_o_tensor                = nullptr;

    tflite::MicroErrorReporter tflu_error;

    constexpr int tensor_arena_size = 4 * 1024;

    byte tensor_arena[tensor_arena_size] __attribute__((aligned(16)));

The global variables declared in this step are as follows:

  • tflu_model: The model parsed by the TFLu parser.
  • tflu_interpreter: The pointer to TFLu interpreter.
  • tflu_i_tensor: The pointer to the model's input tensor.
  • tflu_o_tensor: The pointer to the model's output tensor.
  • tensor_arena: The memory required by the TFLu interpreter. TFLu does not use dynamic allocation. Therefore, we should provide a fixed amount of memory for the input, output, and intermediate tensors. The arena's size depends on the model and is only determined by experiments. In our case, 4,096 is more than enough.

The preceding variables are generally required in all TFLu-based applications.

  1. Load the TFLite model from the C-byte snow_forecast_model_tflite array in the setup() function:

    tflu_model = tflite::GetModel(snow_forecast_model_tflite);

  2. Define a tflite::AllOpsResolver object in the setup() function:

    tflite::AllOpsResolver tflu_ops_resolver;

The TFLu interpreter will use this interface to find the function pointers for each DNN operator.

  1. Create the TFLu interpreter in the setup() function:

    tflu_interpreter = new tflite::MicroInterpreter(tflu_model, tflu_ops_resolver, tensor_arena, tensor_arena_size, &tflu_error);

  2. Allocate the memory required for the model and get the memory pointer of the input and output tensors in the setup() function:

    tflu_interpreter->AllocateTensors();

    tflu_i_tensor = tflu_interpreter->input(0);

    tflu_o_tensor = tflu_interpreter->output(0);

  3. Get the quantization parameters for the input and output tensors in the setup() function:

    const auto* i_quantization = reinterpret_cast<TfLiteAffineQuantization*>(tflu_i_tensor->quantization.params);

    onst auto* o_quantization = reinterpret_cast<TfLiteAffineQuantization*>(tflu_o_tensor->quantization.params);

    tflu_i_scale      = i_quantization->scale->data[0];

    tflu_i_zero_point = i_quantization->zero_point->data[0];

    tflu_o_scale      = o_quantization->scale->data[0];

    tflu_o_zero_point = o_quantization->zero_point->data[0];

The quantization parameters are returned in the TfLiteAffineQuantization object, containing two arrays for the scale and zero point parameters. Since both input and output tensors adopt a per-tensor quantization, each array stores a single value.

  1. Initialize the input tensor with the quantized input features in the loop() function:

    const int idx0 = cur_idx;

    const int idx1 = (cur_idx - 1 + NUM_HOURS) % NUM_HOURS;

    const int idx2 = (cur_idx - 2 + NUM_HOURS) % NUM_HOURS;

    tflu_i_tensor->data.int8[0] = t_vals[idx2];

    tflu_i_tensor->data.int8[1] = t_vals[idx1];

    tflu_i_tensor->data.int8[2] = t_vals[idx0];

    tflu_i_tensor->data.int8[3] = h_vals[idx2];

    tflu_i_tensor->data.int8[4] = h_vals[idx1];

    tflu_i_tensor->data.int8[5] = h_vals[idx0];

Since we need the last three samples, we use the following formula to read the elements from the circular buffer:

In the preceding formula, N is the sampling instant and is the corresponding circular buffer's pointer. For example, if t0 is the current instant, N = 0 means the sample at time t = t0, N = 1 the sample at time t = t0 – 1, and N = 2 the sample at time t = t0 – 2.

  1. Run the inference in the loop() function:

    tflu_interpreter->Invoke();

  2. Dequantize the output tensor and forecast the weather condition in the loop() function:

    int8_t out_int8 = tflu_o_tensor->data.int8[0];

    float out_f = (out_int8 - tflu_o_zero_point) * tflu_o_scale;

    if (out_f > 0.5) {

      Serial.println("Yes, it snows");

    }

    else {

      Serial.println("No, it does not snow");

    }

The dequantization of the output is done with the tflu_o_scale and tflu_o_zero_point quantization parameters retrieved in the setup() function. Once we have the floating-point representation, the output is considered No when it is below 0.5; otherwise, it's Yes.

Now, compile and upload the program on the microcontroller board. The serial terminal in the Arduino IDE will report Yes, it snows or No, it does not snow, depending on whether snow is forecast.

To check if the application can forecast snow, you can simply force the temperature to -10 and the humidity to 100. The model should return Yes, it snows on the serial terminal.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
44.200.94.150