Train a model for detecting the fist activity using Naive Bayes Special Contributors:
Hui Liu
✉ hui.liu@uni-bremen.de
Lorenz Diener
✉ lorenz.diener@uni-bremen.de
Difficulty Level:
Tags train_and_classify☁emg☁naive bayes

"Mathematics is everywhere!" 😏

Despite not being the most original sentence, this idea is almost a universal truth, something that makes it the ideal candidate to start our introductory text.

Probability and Statistics is one prominent branch of (Applied) Mathematics identifiable in a wide range of segments of our society, ranging from the daily evaluation of future meteorological conditions to epidemiological studies while evaluating the risk that a subject presents of contracting a disease.

With such a diversified set of possibilities and exciting opportunities, Probability and Statistics also provide extremely important tools to computational sciences, contributing for the creation of a notable group/family of Machine-learning algorithms based on Bayesian/probabilistic inference .

One of the simpler members of this family is the Naive Bayes classifier, which belongs to the group of supervised machine-learning algorithms supported by the Bayes Theorem , i.e., in the "assumption of conditional independence between every pair of features given the value of the class variable" ( further details in scikit-learn official page ).

The "naive" term suits extremely well, considering that the Bayesian assumption, regarding the conditional independence between features, is commonly not true.

This Jupyter Notebook will be dedicated to present a practical application of Bayesian/probabilistic inference through the training and evaluation of a Naive Bayes classifier focused on the detection of Fist activity .

As demonstrated in other Jupyter Notebooks belonging to Train and Classify category ( Signal Classifier - Distinguish between EMG and ECG and Rock, Paper or Scissor Game - Train and Classify ), machine-learning algorithms can be used to distinguish signals or identify movements, now it will be explored what they can offer while facing an event detection challenge.


1 - Import relevant packages and functions

In [1]:
# Package that provides useful matrix operation methods.
import numpy as np

# Machine-learning toolbox.
from sklearn.naive_bayes import GaussianNB # For Naive Bayes modelling

# Provides useful tools to organize data into easily readable structures.
from pandas import DataFrame # For preparing the confusion matrix in text form

# Graphical package used for drawing a confusion matrix.
from seaborn import heatmap
from matplotlib.pyplot import figure, xlabel, ylabel
In [2]:
%matplotlib inline

# biosignalsnotebooks Python package containing auxiliary functions.
import biosignalsnotebooks as bsnb

2 - Load and visualize the data for training and testing

The EMG data which is used here was recorded by the biosignalsplux ( product kits homepage ) research kit. You could use other devices (e.g. BITalino - product homepage ), other sensors, or even multiple devices + sensors in advanced tasks to collect your own data.

Note: Paths given as input of the NumPy | load function are relative to the biosignalsnotebooks project. You should adapt them in accordance to your needs! 😉

In [3]:
# Load data from npy file (a good format to read data in a fast way)
emg_train = np.load("../../images/train_and_classify/emg_fist_classifier/emg_train.npy")
emg_test = np.load("../../images/train_and_classify/emg_fist_classifier/emg_test.npy")
In [4]:
# Generation of time-axes.
time_train = np.linspace(0, (len(emg_train) - 1) / 1000, len(emg_train))
time_test = np.linspace(0, (len(emg_test) - 1) / 1000, len(emg_test))

# Visualize the dataset
bsnb.plot([time_train, time_test], [emg_train, emg_test], y_axis_label="Amplified EMG value (RAW)", legend_label=["EMG Train", "EMG Test"], title=["EMG signal in waveform (for training)", "EMG signal in waveform (for test)"], grid_plot=True, grid_columns=1, grid_lines=2)

3 - Define the function for calculation of features based on multiple windows along the time axis

The feature used in this exploratory stage was the bin of 120Hz from the whole spectral as a simple (and the only one) feature of a window.

This feature is from the spectral domain (i. e. frequency domain). Note that you could try other bins, other feature types from spectral domain (e. g. spectral kurtosis), features from time domain or statistical domain, or even more dimension of features - thus you will have Feature Vectors of each window.

For the specific purpose of this Jupyter Notebook it was used a window length of 1000 samples (i. e. 1 second), and an overlap of 1/4 window length (i.e. 250 samples) for feature extraction.

Different configurations could be tested for window lengths or/and overlaps to see which combination may work better.

In [5]:
def calculate_features(emg_signal, window_length = 1000, window_overlap = 1/4):
    feature_sequence = [] # Initialise the feature sequence
    window_shift = int(window_length * window_overlap)
    window_type = np.blackman(window_length) # Use blackman window for windowing the signal. You can also choose another window types

    for window_start in range(0, len(emg_signal) - window_length, window_shift): # Calculate features for each window along time axis
        windowed_signal = window_type * np.array(emg_signal[window_start:window_start + window_length]) # Get windowed data
        spectrogram = np.abs(np.fft.rfft(windowed_signal)) # Calculate magnitude Spectrogram
        feature_sequence.append([spectrogram[120]]) # Put the new calculated feature in the feature sequence.
    return(np.array(feature_sequence))

4 - Generate the feature sequences of training and test data through the calculate_features method created before.

In [6]:
# Pre-processing: signal (raw data) --> feature sequence
train_features = calculate_features(emg_train)
test_features = calculate_features(emg_test)

5 - Being Naive Bayes a supervised machine-learning classifier, it is now time to define the labels for the training examples.

Definition of the labels could be done in different ways, here the labels are stored in a .npy file and we simply need to load them.

Note: Paths given as input of the NumPy | load function are relative to the biosignalsnotebooks project. You should adapt them in accordance to your needs! 😉

In [7]:
# Prepare the labels for training and evaluation
train_labels = np.load("../../images/train_and_classify/emg_fist_classifier/emg_train_labels.npy")
train_labels = train_labels[2:-2] # Alignment
test_labels = np.load("../../images/train_and_classify/emg_fist_classifier/emg_test_labels.npy")
test_labels = test_labels[2:-2] # Alignment
In [8]:
print("\033[1mTrain Labels:\033[0m\n " + str(train_labels))
print("\033[1mTest Labels:\033[0m\n " + str(test_labels))
Train Labels:
 [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
 0 0 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 0 0 0 0 0 0 0 0 0 0 0 0 0
 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0]
Test Labels:
 [0 0 0 0 0 0 0 0 0 0 0 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 0 0
 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]

6 - Graphically visualize the feature sequences as well as the labels (references) for observation and confirmation

In [9]:
from bokeh.layouts import gridplot
from bokeh.plotting import show

# Populating the plots.
sample_axis_train = np.linspace(0, len(train_features), len(train_features))
sample_axis_test = np.linspace(0, len(test_features), len(test_features))
figure_list = bsnb.plot([sample_axis_train, sample_axis_test], [np.concatenate(train_features), np.concatenate(test_features)], grid_plot=True, grid_columns=1, grid_lines=2,
                        y_axis_label="Magnitude (RAW)", x_axis_label="Frame Number", legend_label=["Feature Sequence", "Feature Sequence"], 
                        title=["Feature sequence of training data: bin of 120Hz in Spectral magnitude", "Feature sequence of test data: bin of 120Hz in Spectral magnitude"],
                        show_plot=False, get_fig_list=True)

# Adding the labels time-series.
figure_list[0].line(sample_axis_train, train_labels * 700, legend_label="Labels of fist activity", **bsnb.opensignals_kwargs("line")) # TRAINING
figure_list[1].line(sample_axis_test, test_labels * 700, legend_label="Labels of fist activity", **bsnb.opensignals_kwargs("line")) # TEST

# Visualize the feature sequences
grid_plot = gridplot([[figure_list[0]], [figure_list[1]]], **bsnb.opensignals_kwargs("gridplot"))
show(grid_plot)

7 - Train a basic Naive Bayes model using the feature sequence of the training data

In [10]:
basic_gaussian_classifier = GaussianNB()
basic_gaussian_classifier.fit(train_features, train_labels)
Out[10]:
GaussianNB()

8 - Define the function to draw confusion matrix in order to evaluate the performance of the trained classifier

In [11]:
def draw_confusion_matrix(prediction, references):
    # Prepare statistics of confusion matrix
    confusion_matrix = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
    for i in range(0, len(prediction)):
        confusion_matrix[prediction[i]][references[i]] += 1
    
    # Draw confusion matrix
    dataframe = DataFrame(confusion_matrix, index = [0, 1, 2], columns = [0, 1, 2])
    print ("\n\033[1mConfusion Matrix (text form):\033[0m\n")
    print (dataframe)
    print ("\n\033[1mConfusion Matrix (figure):\033[0m")
    figure(figsize = (5, 4.5))
    heatmap(dataframe, annot=True)
    ylabel("Prediction")
    xlabel("Reference")

9 - Use the trained Naive Bayes model to decode the test data and evaluate its performance

In [12]:
# Show the accuracy of the classifier considering its score property.
print("\nAccuracy: " + str(basic_gaussian_classifier.score(test_features, test_labels)))

# Request classifier a prediction regarding the areas of fist activity in the test data.
prediction = basic_gaussian_classifier.predict(test_features)

# Draw a confusion matrix to compare the prediction with the expected fist activity envelope.
draw_confusion_matrix(prediction, test_labels)
Accuracy: 0.8981481481481481

Confusion Matrix (text form):

    0   1   2
0  61   1   0
1   1  17   5
2   0   4  19

Confusion Matrix (figure):

The accuracy/score of the classifier is very reasonable (89.81%) and the confusion matrix demonstrates that the great majority of the obtained prediction meet the expected result (considering the concentration of values in the top-left to bottom-right diagonal).

10 - Upgrade the algorithm: use stacking

Through the stacking technique a meta-classifier is created, combining multiple classification models or information sources to improve the prediction capabilities of the original model (for more information regarding stacking and ensemble learning, please, visit: https://blog.statsbot.co/ensemble-learning-d1dcd548e936 ).

For our specific case, features extracted from multiple windows are combined in a single feature vector (stacked features).

Here it is used a context length of 2, i.e for each window a 5-dimensional feature vector is generated:

\begin{equation} [120Hz\; Bin(Window[i-2]), 120Hz\; Bin(Window[i-1]), 120Hz\; Bin(Window[i]), 120Hz\; Bin(Window[i+1]), 120Hz\; Bin(Window[i+2])] \end{equation}

It should begin with $Window[2]$.

In [13]:
# You could test different combinations
context_length = 2

def stack_features(features, labels, context_length):
    stacked_features = []
    aligned_labels = []
    for window_start in range(0, len(features) - (context_length * 2 + 1)):
        stacked_feature_vector = features[window_start:window_start + context_length * 2 + 1, :]
        stacked_features.append(stacked_feature_vector.flatten())
        aligned_labels.append(labels[window_start + context_length])
    return(np.array(stacked_features), np.array(aligned_labels))

stacked_train_features, aligned_train_labels = stack_features(train_features, train_labels, context_length)
stacked_test_features, aligned_test_labels = stack_features(test_features, test_labels, context_length)

11 - Train a Naive Bayes model using the feature vector sequence of the stacked features

In [14]:
upgraded_gaussian_classifier = GaussianNB()
upgraded_gaussian_classifier.fit(stacked_train_features, aligned_train_labels)
Out[14]:
GaussianNB()

12 - Use the new trained Naive Bayes model to decode the test data and evaluate

In [15]:
# Show the accuracy of the classifier considering its score property.
print("Accuracy: " + str(upgraded_gaussian_classifier.score(stacked_test_features, aligned_test_labels)))

# Request classifier a prediction regarding the areas of fist activity in the test data.
prediction = upgraded_gaussian_classifier.predict(stacked_test_features)

# Draw a confusion matrix to compare the prediction with the expected fist activity envelope.
draw_confusion_matrix(prediction, aligned_test_labels)
Accuracy: 0.9611650485436893

Confusion Matrix (text form):

    0   1   2
0  53   0   0
1   2  22   0
2   2   0  24

Confusion Matrix (figure):

As demonstrated by the previous evaluation (accuracy estimate and confusion matrix generation) the performance of the "stacked" classifier greatly improves, reaching the 96.12% of accuracy!

After a diversified set of challenges, our journey through Naive Bayes classifiers reaches the conclusive point.

In this Jupyter Notebook you had the opportunity to explore a new classification algorithm and also how to improve the performance of the trained model through stacking.

We hope that you have enjoyed this guide. biosignalsnotebooks is an environment in continuous expansion, so don"t stop your journey and learn more with the remaining Notebooks !

In [16]:
from biosignalsnotebooks.__notebook_support__ import css_style_apply
css_style_apply()
.................... CSS Style Applied to Jupyter Notebook .........................
Out[16]: