↓ Skip to main content

ESP-TFLite-Micro workshop - Assignment 2: Train and evaluate the model

·12 mins·
Table of Contents
WS004EN - This article is part of a series.
Part 7: This Article

Recap from Assignment 1
#

In the previous assignment, you defined a repeatable movement and recorded at least 25 samples with the BMI270 gyroscope. Each capture became one CSV row containing 600 values:

x0,y0,z0,x1,y1,z1,...,x199,y199,z199

The 600 values represent 200 time steps with three axes per step. You also kept the board grip and orientation consistent because changing its orientation changes the sensor axes and therefore the data seen by the model.

The original dataset from the gesture recognition example has three classes:

  • o.csv — counterclockwise circle
  • v.csv — V gesture
  • unknown.csv — movements that are neither circle nor V

Your newly collected CSV adds a fourth class. The training code below uses new_gesture.csv for the movement collected in Assignment 1. You can keep the gesture’s descriptive filename instead, but you must update the corresponding path and label name in the training script and, later, in the application label table.

The workshop code repository and the training script bundle provide the following training workspace:

1gesture_recognition/
2└── training/
3    ├── train_model.py
4    ├── requirements.txt
5    └── shuttle_dataset/
6        ├── o.csv
7        ├── v.csv
8        ├── unknown.csv
9        └── new_gesture.csv

What you will do
#

In this assignment, you will:

  1. Create an isolated Python environment.
  2. Load the four CSV files and assign one integer label to each gesture.
  3. Reshape every row from 600 values to a 200 × 3 tensor.
  4. Split the dataset into training and test sets.
  5. Train and evaluate a compact 1D convolutional neural network.
  6. Save the trained Keras model for TensorFlow Lite conversion.

Assignment steps
#

Step 1: Create the training workspace
#

Assignment 1 prepared training/shuttle_dataset and saved your collected data as new_gesture.csv. If you did not download the workshop project or training bundle, create the workspace now from the gesture_recognition project directory:

  1. Open the gesture_recognition folder in VS Code.
  2. In the Explorer, use New Folder to create training.
  3. Create shuttle_dataset inside training.
  4. Select File → Open Folder and open gesture_recognition/training before you create or run the files used in the following steps.

Opening the training folder makes relative paths such as ./shuttle_dataset/o.csv resolve correctly.

mkdir -p training/shuttle_dataset
cd training
mkdir -p training/shuttle_dataset
cd training

PowerShell:

New-Item -ItemType Directory -Force training\shuttle_dataset
Set-Location training

Download the original gesture recognition dataset and extract it:

1curl -fL \
2https://dl.espressif.com/AE/esp-iot-solution/gesture_recognition/shuttle_dataset.zip \
3-o shuttle_dataset.zip
4unzip shuttle_dataset.zip -d shuttle_dataset
5rm shuttle_dataset.zip
1curl -fL \
2https://dl.espressif.com/AE/esp-iot-solution/gesture_recognition/shuttle_dataset.zip \
3-o shuttle_dataset.zip
4unzip shuttle_dataset.zip -d shuttle_dataset
5rm shuttle_dataset.zip

PowerShell:

1Invoke-WebRequest `
2-Uri "https://dl.espressif.com/AE/esp-iot-solution/gesture_recognition/shuttle_dataset.zip" `
3-OutFile "shuttle_dataset.zip"
4Expand-Archive -Path "shuttle_dataset.zip" -DestinationPath "shuttle_dataset"
5Remove-Item "shuttle_dataset.zip"

Graphical alternative: Open the dataset link in a browser, save the ZIP archive, and extract it with Finder, File Explorer, or your desktop archive manager. Copy o.csv, v.csv, and unknown.csv into training/shuttle_dataset using your file manager or the VS Code Explorer.

The archive adds o.csv, v.csv, and unknown.csv alongside the new_gesture.csv file created in Assignment 1. If you saved the collected data elsewhere or used a descriptive filename, use Finder, File Explorer, or the VS Code Explorer to move it into training/shuttle_dataset and rename it to new_gesture.csv.

If you keep another filename, update the new_gesture references in train_model.py. Before training, confirm that each CSV row has exactly 600 values and that the new class contains at least 25 rows. Keeping class sizes reasonably balanced prevents a larger class from dominating training.

Open a new terminal session for the Python steps. Do not create or activate the training environment in the terminal session where you activated ESP-IDF. ESP-IDF uses its own Python environment, which can conflict with the TensorFlow dependencies. In the new terminal, navigate to gesture_recognition/training before continuing.

Step 2: Create the Python environment
#

This workshop uses Python 3.12. TensorFlow 2.20 does not provide packages for every newer Python release; for example, installation fails with No matching distribution found for tensorflow when the environment uses Python 3.14.

Install Python 3.12 if it is not already available:

Install it with Homebrew:

brew install python@3.12
python3.12 --version

On Ubuntu and Debian:

sudo apt update
sudo apt install python3.12 python3.12-venv
python3.12 --version

For other distributions, install Python 3.12 and its venv module using the distribution’s package manager.

Download and run the Python 3.12.10 installer. Then open PowerShell and verify it:

py -3.12 --version

The version command must report Python 3.12.x.

If .venv was previously created with another Python version, deactivate it and delete the .venv folder before continuing. A virtual environment keeps the Python version with which it was created; selecting another interpreter does not upgrade it.

Create requirements.txt in the training directory with the following content:

1numpy
2pandas
3scikit-learn
4tensorflow==2.20.0
5matplotlib

Create the virtual environment in the training directory with Python 3.12:

  1. Install the Microsoft Python extension.
  2. Open the Command Palette and run Python: Create Environment.
  3. Select Venv, choose Python 3.12, and select requirements.txt when VS Code asks which dependencies to install.
  4. Run Python: Select Interpreter and confirm that the .venv environment in the training directory is selected.

VS Code creates the environment and installs the dependencies, so you can skip the pip commands below.

Create the environment:

python3.12 -m venv .venv

Activate it:

source .venv/bin/activate

Create the environment:

python3.12 -m venv .venv

Activate it:

source .venv/bin/activate

Create the environment:

py -3.12 -m venv .venv

Activate it in PowerShell:

.\.venv\Scripts\Activate.ps1

Command Prompt:

.venv\Scripts\activate.bat

If you created the environment in a terminal, install the dependencies:

python -m pip install --upgrade pip
python -m pip install -r requirements.txt

Step 3: Load and label the CSV files
#

Open the supplied train_model.py in the training directory. The following sections explain how the script works, beginning with the imports and the extract_data() function that loads one file per class. If you are following the exercise without the downloadable assets, create the file with this code:

 1import matplotlib.pyplot as plt
 2import numpy as np
 3import pandas as pd
 4from sklearn.model_selection import train_test_split
 5from tensorflow import keras
 6
 7
 8def extract_data():
 9    o_data = pd.read_csv("./shuttle_dataset/o.csv", sep=",", header=None)
10    v_data = pd.read_csv("./shuttle_dataset/v.csv", sep=",", header=None)
11    unknown_data = pd.read_csv("./shuttle_dataset/unknown.csv", sep=",", header=None)
12    new_gesture_data = pd.read_csv(
13        "./shuttle_dataset/new_gesture.csv",
14        sep=",",
15        header=None,
16    )
17
18    o_label = np.zeros(o_data.shape[0], dtype=int)
19    v_label = np.ones(v_data.shape[0], dtype=int)
20    unknown_label = np.full(unknown_data.shape[0], 2, dtype=int)
21    new_gesture_label = np.full(new_gesture_data.shape[0], 3, dtype=int)
22
23    X_raw = np.vstack([
24        o_data.values,
25        v_data.values,
26        unknown_data.values,
27        new_gesture_data.values,
28    ])
29    y = np.concatenate([
30        o_label,
31        v_label,
32        unknown_label,
33        new_gesture_label,
34    ])
35
36    num_samples = X_raw.shape[0]
37    X = X_raw.reshape(num_samples, 200, 3)
38
39    return X, y

The class-to-label mapping is:

LabelClass
0Counterclockwise circle
1V gesture
2Unknown movement
3New gesture collected in Assignment 1

X has shape (sample_count, 200, 3), while y is a one-dimensional array containing the class number for each sample.

Step 4: Prepare the training and test sets
#

Convert the arrays to the data types expected by TensorFlow and reserve 30% of the samples for evaluation:

 1def prepare_data():
 2    X, y = extract_data()
 3
 4    X = np.array(X, dtype=np.float32)
 5    y = np.array(y, dtype=np.int32)
 6
 7    X_train, X_test, y_train, y_test = train_test_split(
 8        X,
 9        y,
10        test_size=0.3,
11        random_state=42,
12    )
13
14    print(f"Training set: {X_train.shape[0]} samples")
15    print(f"Test set: {X_test.shape[0]} samples")
16
17    return X_train, X_test, y_train, y_test

The fixed random_state makes the split repeatable. For a production dataset collected by several people or in several sessions, split by participant or session instead of randomly mixing related samples. This provides a more realistic test of how the model handles unseen users.

Step 5: Define the 1D CNN
#

The reference architecture applies convolution along the time axis:

 1def create_1d_cnn_model():
 2    model = keras.Sequential([
 3        keras.layers.Conv1D(
 4            filters=8,
 5            kernel_size=5,
 6            padding="same",
 7            activation="relu",
 8            input_shape=(200, 3),
 9        ),
10        keras.layers.MaxPooling1D(pool_size=4),
11
12        keras.layers.Conv1D(
13            filters=16,
14            kernel_size=5,
15            padding="same",
16            activation="relu",
17        ),
18        keras.layers.MaxPooling1D(pool_size=4),
19
20        keras.layers.GlobalAveragePooling1D(),
21        keras.layers.Dense(32, activation="relu"),
22        keras.layers.Dropout(0.2),
23        keras.layers.Dense(4, activation="softmax"),
24    ])
25
26    return model

The input shape is (200, 3): 200 time steps and three gyroscope axes. The final Dense(4) layer produces one score for each of the four classes. If you add or remove classes, change this number and keep the labels in the ESP-IDF application synchronized.

Step 6: Train the model
#

Compile and train the model using the settings from the reference script:

 1def train_model(model, X_train, y_train, X_test, y_test, epochs=300):
 2    model.compile(
 3        optimizer=keras.optimizers.Adam(learning_rate=0.001),
 4        loss="sparse_categorical_crossentropy",
 5        metrics=["accuracy"],
 6    )
 7
 8    model.summary()
 9
10    history = model.fit(
11        X_train,
12        y_train,
13        validation_data=(X_test, y_test),
14        epochs=epochs,
15        batch_size=64,
16        verbose=1,
17        shuffle=True,
18    )
19
20    return history
SettingValuePurpose
Learning rate0.001Controls the Adam optimizer’s update size
LossSparse categorical cross-entropyWorks with integer class labels
Epochs300Number of passes through the training set
Batch size64Samples processed before each parameter update
ShuffleTrueChanges sample order between epochs

Step 7: Evaluate the result
#

Print the held-out test metrics after training:

1test_loss, test_accuracy = model.evaluate(X_test, y_test, verbose=0)
2
3print(f"Test loss: {test_loss:.4f}")
4print(f"Test accuracy: {test_accuracy:.4f}")

Optional: Plot the training curves
#

Plotting is not required to train, save, convert, or deploy the model. It can help you identify overfitting by comparing training and validation loss and accuracy.

Add this function if you want to generate training_curves.png:

 1def plot_training_curves(history, save_path="training_curves.png"):
 2    fig, axes = plt.subplots(1, 2, figsize=(12, 4))
 3    epochs = range(1, len(history.history["loss"]) + 1)
 4
 5    axes[0].plot(epochs, history.history["loss"], label="Train Loss")
 6    axes[0].plot(epochs, history.history["val_loss"], label="Validation Loss")
 7    axes[0].set_xlabel("Epoch")
 8    axes[0].set_ylabel("Loss")
 9    axes[0].set_title("Training and Validation Loss")
10    axes[0].legend()
11    axes[0].grid(True, alpha=0.3)
12
13    axes[1].plot(epochs, history.history["accuracy"], label="Train Accuracy")
14    axes[1].plot(
15        epochs,
16        history.history["val_accuracy"],
17        label="Validation Accuracy",
18    )
19    axes[1].set_xlabel("Epoch")
20    axes[1].set_ylabel("Accuracy")
21    axes[1].set_title("Training and Validation Accuracy")
22    axes[1].legend()
23    axes[1].grid(True, alpha=0.3)
24
25    plt.tight_layout()
26    plt.savefig(save_path, dpi=150)
27    plt.show()

Call it after training:

plot_training_curves(history)

Optionally, inspect mistakes per class:

1from sklearn.metrics import classification_report, confusion_matrix
2
3y_probability = model.predict(X_test)
4y_prediction = np.argmax(y_probability, axis=1)
5
6print(confusion_matrix(y_test, y_prediction))
7print(classification_report(y_test, y_prediction))

Do not judge the model only by training accuracy. Check whether the held-out samples distinguish intentional gestures from unknown movements. If you generate the curves, a large gap between training and validation performance usually indicates overfitting.

Step 8: Run the training and export the model
#

Add the save function and main program at the end of train_model.py:

 1def save_model_h5(model, filepath):
 2    model.save(filepath)
 3    print(f"Model saved to {filepath}")
 4
 5
 6if __name__ == "__main__":
 7    keras.utils.set_random_seed(42)
 8
 9    X_train, X_test, y_train, y_test = prepare_data()
10    model = create_1d_cnn_model()
11    history = train_model(model, X_train, y_train, X_test, y_test)
12
13    test_loss, test_accuracy = model.evaluate(X_test, y_test, verbose=0)
14    print(f"Test loss: {test_loss:.4f}")
15    print(f"Test accuracy: {test_accuracy:.4f}")
16
17    # Optional:
18    # plot_training_curves(history)
19
20    save_model_h5(model, "simple_1dcnn_model.h5")

Run the completed script:

  1. Open train_model.py.
  2. Confirm that the .venv interpreter appears in the VS Code status bar.
  3. Click Run Python File in the editor toolbar.

Training progress and metrics appear in the VS Code Terminal panel.

python train_model.py

The beginning of the output should show the number of samples per class, the train/test split, and the model summary before training starts.

After the script finishes, the training directory should contain:

simple_1dcnn_model.h5

If you enable plotting, it also creates training_curves.png.

Keep simple_1dcnn_model.h5; Assignment 3 converts this file to TensorFlow Lite.

Complete training script
#

Show complete train_model.py
  1import matplotlib.pyplot as plt
  2import numpy as np
  3import pandas as pd
  4from sklearn.model_selection import train_test_split
  5from tensorflow import keras
  6
  7
  8def extract_data():
  9    o_data = pd.read_csv("./shuttle_dataset/o.csv", header=None)
 10    v_data = pd.read_csv("./shuttle_dataset/v.csv", header=None)
 11    unknown_data = pd.read_csv("./shuttle_dataset/unknown.csv", header=None)
 12    new_gesture_data = pd.read_csv(
 13        "./shuttle_dataset/new_gesture.csv",
 14        header=None,
 15    )
 16
 17    o_label = np.zeros(o_data.shape[0], dtype=int)
 18    v_label = np.ones(v_data.shape[0], dtype=int)
 19    unknown_label = np.full(unknown_data.shape[0], 2, dtype=int)
 20    new_gesture_label = np.full(new_gesture_data.shape[0], 3, dtype=int)
 21
 22    X_raw = np.vstack([
 23        o_data.values,
 24        v_data.values,
 25        unknown_data.values,
 26        new_gesture_data.values,
 27    ])
 28    y = np.concatenate([
 29        o_label,
 30        v_label,
 31        unknown_label,
 32        new_gesture_label,
 33    ])
 34
 35    print(f"Counterclockwise circle samples: {o_label.shape[0]}")
 36    print(f"V gesture samples: {v_label.shape[0]}")
 37    print(f"Unknown samples: {unknown_label.shape[0]}")
 38    print(f"New gesture samples: {new_gesture_label.shape[0]}")
 39    print(f"Total samples: {len(y)}")
 40
 41    X = X_raw.reshape(X_raw.shape[0], 200, 3)
 42    return X, y
 43
 44
 45def prepare_data():
 46    X, y = extract_data()
 47
 48    X = np.array(X, dtype=np.float32)
 49    y = np.array(y, dtype=np.int32)
 50
 51    X_train, X_test, y_train, y_test = train_test_split(
 52        X,
 53        y,
 54        test_size=0.3,
 55        random_state=42,
 56    )
 57
 58    print(f"Training set: {X_train.shape[0]} samples")
 59    print(f"Test set: {X_test.shape[0]} samples")
 60
 61    return X_train, X_test, y_train, y_test
 62
 63
 64def create_1d_cnn_model():
 65    return keras.Sequential([
 66        keras.layers.Conv1D(
 67            filters=8,
 68            kernel_size=5,
 69            padding="same",
 70            activation="relu",
 71            input_shape=(200, 3),
 72        ),
 73        keras.layers.MaxPooling1D(pool_size=4),
 74        keras.layers.Conv1D(
 75            filters=16,
 76            kernel_size=5,
 77            padding="same",
 78            activation="relu",
 79        ),
 80        keras.layers.MaxPooling1D(pool_size=4),
 81        keras.layers.GlobalAveragePooling1D(),
 82        keras.layers.Dense(32, activation="relu"),
 83        keras.layers.Dropout(0.2),
 84        keras.layers.Dense(4, activation="softmax"),
 85    ])
 86
 87
 88def train_model(model, X_train, y_train, X_test, y_test, epochs=300):
 89    model.compile(
 90        optimizer=keras.optimizers.Adam(learning_rate=0.001),
 91        loss="sparse_categorical_crossentropy",
 92        metrics=["accuracy"],
 93    )
 94
 95    model.summary()
 96
 97    return model.fit(
 98        X_train,
 99        y_train,
100        validation_data=(X_test, y_test),
101        epochs=epochs,
102        batch_size=64,
103        verbose=1,
104        shuffle=True,
105    )
106
107
108def plot_training_curves(history, save_path="training_curves.png"):
109    fig, axes = plt.subplots(1, 2, figsize=(12, 4))
110    epochs = range(1, len(history.history["loss"]) + 1)
111
112    axes[0].plot(epochs, history.history["loss"], label="Train Loss")
113    axes[0].plot(
114        epochs,
115        history.history["val_loss"],
116        label="Validation Loss",
117    )
118    axes[0].set_xlabel("Epoch")
119    axes[0].set_ylabel("Loss")
120    axes[0].set_title("Training and Validation Loss")
121    axes[0].legend()
122    axes[0].grid(True, alpha=0.3)
123
124    axes[1].plot(
125        epochs,
126        history.history["accuracy"],
127        label="Train Accuracy",
128    )
129    axes[1].plot(
130        epochs,
131        history.history["val_accuracy"],
132        label="Validation Accuracy",
133    )
134    axes[1].set_xlabel("Epoch")
135    axes[1].set_ylabel("Accuracy")
136    axes[1].set_title("Training and Validation Accuracy")
137    axes[1].legend()
138    axes[1].grid(True, alpha=0.3)
139
140    plt.tight_layout()
141    plt.savefig(save_path, dpi=150)
142    plt.show()
143
144
145def save_model_h5(model, filepath):
146    model.save(filepath)
147    print(f"Model saved to {filepath}")
148
149
150if __name__ == "__main__":
151    keras.utils.set_random_seed(42)
152
153    X_train, X_test, y_train, y_test = prepare_data()
154    model = create_1d_cnn_model()
155    history = train_model(model, X_train, y_train, X_test, y_test)
156
157    test_loss, test_accuracy = model.evaluate(X_test, y_test, verbose=0)
158    print(f"Test loss: {test_loss:.4f}")
159    print(f"Test accuracy: {test_accuracy:.4f}")
160
161    # Optional:
162    # plot_training_curves(history)
163
164    save_model_h5(model, "simple_1dcnn_model.h5")

Acceptance criteria
#

  • Training completes without invalid values or tensor-shape errors.
  • The model input shape is (200, 3).
  • The model output contains four class scores.
  • Test-set metrics are recorded.
  • simple_1dcnn_model.h5 is created and can be loaded again for conversion.

Next step
#

Assignment 3: Convert and embed the model

Back to workshop home

WS004EN - This article is part of a series.
Part 7: This Article

Related