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,z199The 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 circlev.csv— V gestureunknown.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.csvWhat you will do#
In this assignment, you will:
- Create an isolated Python environment.
- Load the four CSV files and assign one integer label to each gesture.
- Reshape every row from 600 values to a
200 × 3tensor. - Split the dataset into training and test sets.
- Train and evaluate a compact 1D convolutional neural network.
- 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:
- Open the
gesture_recognitionfolder in VS Code. - In the Explorer, use New Folder to create
training. - Create
shuttle_datasetinsidetraining. - Select File → Open Folder and open
gesture_recognition/trainingbefore 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 trainingmkdir -p training/shuttle_dataset
cd trainingPowerShell:
New-Item -ItemType Directory -Force training\shuttle_dataset
Set-Location trainingDownload 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.zip1curl -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.zipPowerShell:
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.
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 --versionOn Ubuntu and Debian:
sudo apt update
sudo apt install python3.12 python3.12-venv
python3.12 --versionFor 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 --versionThe version command must report Python 3.12.x.
.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
5matplotlibCreate the virtual environment in the training directory with Python 3.12:
- Install the Microsoft Python extension.
- Open the Command Palette and run
Python: Create Environment. - Select Venv, choose Python 3.12, and select
requirements.txtwhen VS Code asks which dependencies to install. - Run
Python: Select Interpreterand confirm that the.venvenvironment in thetrainingdirectory 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 .venvActivate it:
source .venv/bin/activateCreate the environment:
python3.12 -m venv .venvActivate it:
source .venv/bin/activateCreate the environment:
py -3.12 -m venv .venvActivate it in PowerShell:
.\.venv\Scripts\Activate.ps1Command Prompt:
.venv\Scripts\activate.batIf you created the environment in a terminal, install the dependencies:
python -m pip install --upgrade pip
python -m pip install -r requirements.txtStep 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, yThe class-to-label mapping is:
| Label | Class |
|---|---|
0 | Counterclockwise circle |
1 | V gesture |
2 | Unknown movement |
3 | New 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_testThe 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 modelThe 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| Setting | Value | Purpose |
|---|---|---|
| Learning rate | 0.001 | Controls the Adam optimizer’s update size |
| Loss | Sparse categorical cross-entropy | Works with integer class labels |
| Epochs | 300 | Number of passes through the training set |
| Batch size | 64 | Samples processed before each parameter update |
| Shuffle | True | Changes 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:
- Open
train_model.py. - Confirm that the
.venvinterpreter appears in the VS Code status bar. - Click Run Python File in the editor toolbar.
Training progress and metrics appear in the VS Code Terminal panel.
python train_model.pyThe 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.h5If 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.h5is created and can be loaded again for conversion.
