Ce cas d'usage de reconnaissance d'activités humaines à partir des enregistrements d'un smartphone (gyroscope, accéléromètre) est traité pour illustrer les principales étapes d'exploration communes en science des données et appliquables à des signaux échantillonnés. Ce calepin propose de visualiser ces signaux puis de réaliser des ACP afin d'identifier les problèmes spécifiques soulevés par ce type de données.
Les données sont issues de la communauté qui vise la reconnaissance d'activités humaines (Human activity recognition, HAR) à partir d’enregistrements, par exemple du gyroscope et de l'accéléromètre d'un smartphone alors que le porteur produit une activité déterminée a priori pendnat un laps de temps défini.
Voir à ce propos l'article relatant un colloque de 2013.
Les données publiques disponibles et largement étudiées ont été acquises, décrites et analysées par Anguita et al. (2013). Elles sont accessibles sur le dépôt de l'University California Irvine (UCI) consacré à l'apprentissage machine ainsi que sur le site Kaggle.
L'archive contient les données brutes: accélérations échantillonnnées à 64 htz pendant 2s. Les accélérations en x, y, et z, chacune de 128 colonnes, celles en y soustrayant la gravité naturelle ainsi que les accélérations angulaires (gyroscope) en x, y, et z soit en tout 9 fichiers.
Ce sont ces données brutes qui sont analysées tandis qu'un autre calepin étudie des transformations expertes ou métier de celles-ci sous la forme de nouvelles variables ou features.
L'analyse des nouvelles variables issues de transformations des signaux (entropie, variance, corrélations, Fourier...) conduit à d'excellents résultats au moment de la reconnaisance de l'activité. Néanmoins, il est nécessaire de s'intéresser directement aux données brutes avec pour objectif d'économiser les calculs lourds et énergivores pour un système embarqué ou porté par un individu. Un [autre calepin] aborde cette question: obtenir la même qualité de reconnaissance que sur les variables transformées mais par un réseau de neurones (deep learning) qui, une fois implémenté, sera de faible consommation comme peuvent l'être les circuits de reconnaissance des visages sur un appareil photo.
Comme pour les données métier, l'étude commence par une exploration: visualisation des données, réduction de dimension par ACP.
Importation des principales librairies.
In [1]:
import pandas as pd
import numpy as np
import copy
import random
import itertools
%matplotlib inline
import matplotlib.pyplot as plt
import sklearn.decomposition as sdec
Les données sont celles originales du dépôt de l'UCI. Elle doivent être préalablement téléchargées en cliquant ici.
Elles contiennent deux jeux de dimensions différentes, chacun partagé en apprentissage et test. L'échantillon test n'est utilisé que pour dans les calepins d'apprentisage.
N.B. La structure des données est nettement plus complexe que celles couramment étudiées dans ce dépôt. Le code a été structuré en une séquence de fonctions afin d'en faciliter la compréhension. L'outil "calepin" atteint ici des limites pour la réalisation de codes complexes.
In [2]:
# Attention à la définition du chemin ci-dessous:
DATADIR_UCI = '/home-local/pbesse/Documents/2-Data_files/HAR/UCI HAR Dataset'
# Liste des fichiers afin d'automatiser la lecture.
SIGNALS = [ "body_acc_x", "body_acc_y", "body_acc_z", "body_gyro_x", "body_gyro_y", "body_gyro_z", "total_acc_x", "total_acc_y", "total_acc_z"]
# Fonctions permettant de lire la séquence des fichiers avant de restructurer les données
# dans le fortmat recherché.
def my_read_csv(filename):
return pd.read_csv(filename, delim_whitespace=True, header=None)
def load_signal(data_dir, subset, signal):
filename = data_dir+'/'+subset+'/Inertial Signals/'+signal+'_'+subset+'.txt'
x = my_read_csv(filename).as_matrix()
return x
def load_signals(data_dir, subset, flatten = False):
signals_data = []
for signal in SIGNALS:
signals_data.append(load_signal(data_dir, subset, signal))
if flatten :
X = np.hstack(signals_data)
else:
X = np.transpose(signals_data, (1, 2, 0))
return X
def load_y(data_dir, subset, dummies = False):
filename = data_dir+'/'+subset+'/y_'+subset+'.txt'
y = my_read_csv(filename)[0]
if dummies:
Y = pd.get_dummies(y).as_matrix()
else:
Y = y.as_matrix()
return Y
Lecture des données
In [3]:
#Multidimensional Data
X_train, X_test = load_signals(DATADIR_UCI, 'train'), load_signals(DATADIR_UCI, 'test')
# Flattened Data
X_train_flatten, X_test_flatten = load_signals(DATADIR_UCI, 'train', flatten=True), load_signals(DATADIR_UCI, 'test', flatten=True)
# Label Y
Y_train_label, Y_test_label = load_y(DATADIR_UCI, 'train', dummies = False), load_y(DATADIR_UCI, 'test', dummies = False)
#Dummies Y (For Keras)
Y_train_dummies, Y_test_dummies = load_y(DATADIR_UCI, 'train', dummies = True), load_y(DATADIR_UCI, 'test', dummies = True)
N_train = X_train.shape[0]
N_test = X_test.shape[0]
Vérification des dimensions afin de s'assurer de la bonne lecture des fichiers.
In [4]:
print("Dimension")
print("Données Multidimensionelles, : " + str(X_train.shape))
print("Données Unimensionelles, : " + str(X_train_flatten.shape))
print("Vecteur réponse (scikit-learn) : " + str(Y_train_label.shape))
print("Matrice réponse(Keras) : " + str(Y_train_dummies.shape))
In [5]:
# Liste des couleurs
CMAP = plt.get_cmap("Accent")
# Liste des types de signaux
SIGNALS = ["body_acc x", "body_acc y", "body_acc z",
"body_gyro x", "body_gyro y", "body_gyro z",
"total_acc x", "total_acc y", "total_acc z"]
# Dictionnaire en clair des activités expérimentées (contexte supervisé)
ACTIVITY_DIC = {1 : "WALKING",
2 : "WALKING UPSTAIRS",
3 : "WALKING DOWNSTAIRS",
4 : "SITTING",
5 : "STANDING",
6 : "LAYING"}
# Fonction pour le tracé d'un signal
def plot_one_axe(X, fig, ax, sample_to_plot, cmap):
for act,Xgb in X.groupby("Activity"):
Xgb_first_values = Xgb.values[:sample_to_plot,:-1]
x = Xgb_first_values[0]
ax.plot(x, linewidth=1, color=cmap(act-1), label = label_dic[act])
for x in Xgb_first_values[1:]:
ax.plot(x, linewidth=1, color=cmap(act-1))
def plot_one_axe_shuffle(X, fig, ax, sample_to_plot, cmap):
plot_data = []
for act,Xgb in X.groupby("Activity"):
Xgb_first_values = Xgb.values[:sample_to_plot,:-1]
x = Xgb_first_values[0]
ax.plot(x, linewidth=1, color=cmap(act-1), label = label_dic[act])
for x in Xgb_first_values[1:]:
plot_data.append([x,cmap(act-1)])
random.shuffle(plot_data)
for x,color in plot_data:
ax.plot(x, linewidth=1, color=color)
In [6]:
sample_to_plot = 50
index_per_act = [list(zip(np.repeat(act, sample_to_plot), np.where(Y_train_label==act)[0][:sample_to_plot])) for act in range(1,7)]
index_to_plot = list(itertools.chain.from_iterable(index_per_act))
random.shuffle(index_to_plot)
fig = plt.figure(figsize=(15,10))
for isignal in range(9):
ax = fig.add_subplot(3,3,isignal+1)
for act , i in index_to_plot:
ax.plot(range(128), X_train[i,:,isignal],color=CMAP(act-1), linewidth=1)
ax.set_title(SIGNALS[isignal])
plt.tight_layout()
In [7]:
sample_to_plot = 10
isignal = 1
index_per_act_dict = dict([(act, np.where(Y_train_label==act)[0][:sample_to_plot]) for act in range(1,7)])
fig = plt.figure(figsize=(15,8), num=SIGNALS[isignal])
for act , index in index_per_act_dict.items():
ax = fig.add_subplot(3,2,act)
for x in X_train[index]:
ax.plot(range(128), x[:,0],color=CMAP(act-1), linewidth=1)
ax.set_title(ACTIVITY_DIC[act])
plt.tight_layout()
Q Quelles est l'activité qui semble se distinguer facilement des autres?
Q Observer l'un des signaux, par exemple Walking upstairs
. Qu'est ce qui fait que, pour ces signaux ou courbes, une métrique euclidienne classique ($L_2$) est inopérante?
Q Corrélativement pourquoi est-il intéressant de décomposer un signal dans le domaine des fréquences?
Les ACP ne sont pas réduites car cette transformation n'a finalement pas d'effet sensible sur la discrimination des classes.
Fonction graphique pour la représentation d'un plan factoriel
In [8]:
def plot_pca(X_R, ytrain, fig, ax, nbc, nbc2, label_dic=ACTIVITY_DIC, cmaps = plt.get_cmap("Accent")
):
for i in range(6):
xs = X_R[ytrain==i+1,nbc-1]
ys = X_R[ytrain==i+1, nbc2-1]
label = label_dic[i+1]
color = cmaps(i)
ax.scatter(xs, ys, color=color, alpha=.8, s=10, label=label)
ax.set_xlabel("PC%d : %.2f %%" %(nbc,pca.explained_variance_ratio_[nbc-1]*100), fontsize=15)
ax.set_ylabel("PC%d : %.2f %%" %(nbc2,pca.explained_variance_ratio_[nbc2-1]*100), fontsize=15)
In [9]:
pca = sdec.PCA()
# CHoix du signal
isignal = 4
signal = SIGNALS[isignal]
print("ACP Sur signal : " +signal)
X_r = pca.fit_transform(X_train[:,:,isignal])
Représentation des parts de variances.
In [10]:
fig = plt.figure(figsize=(15,10))
ax = fig.add_subplot(1,2,1)
ax.bar(range(10), pca.explained_variance_ratio_[:10]*100, align='center',
color='grey', ecolor='black')
ax.set_xticks(range(10))
ax.set_ylabel("Variance")
ax.set_title("", fontsize=35)
ax.set_title(u"Pourcentage de variance expliquée \n par les premières composantes", fontsize=20)
ax = fig.add_subplot(1,2,2)
box=ax.boxplot(X_r[:,0:10],whis=100)
ax.set_title(u"Distribution des premières composantes", fontsize=20)
fig.suptitle(u"Résultat ACP sur Signal : " + signal, fontsize=25)
Out[10]:
Attention: les diagrammes boîtes sont très perturbés pas les distributions des composantes avec une très forte concentration autour de 0 et énormément de valeurs atypiques. D'où l'utilisation du paramètre whis=100
pour rallonger les moustaches.
Q Que osnt les graphes ci-dessus. Quelles interprétations ou absence d'interprétation en tirer?
Représentation du premier plan factoriel;
In [11]:
fig = plt.figure(figsize=(10,10), )
ax = fig.add_subplot(1,1,1)
plot_pca(X_r, Y_train_label,fig ,ax ,1 ,2)
In [12]:
fig = plt.figure(figsize=(10,10), )
ax = fig.add_subplot(1,1,1)
plot_pca(X_r, Y_train_label,fig ,ax ,1 ,3)
In [13]:
pca = sdec.PCA()
print("ACP Sur tous les signaux")
X_r = pca.fit_transform(X_train_flatten)
In [14]:
fig = plt.figure(figsize=(15,10))
ax = fig.add_subplot(1,2,1)
ax.bar(range(10), pca.explained_variance_ratio_[:10]*100, align='center',
color='grey', ecolor='black')
ax.set_xticks(range(10))
ax.set_ylabel("Variance")
ax.set_title("", fontsize=35)
ax.set_title(u"Pourcentage de variance expliqué \n des premières composantes", fontsize=20)
ax = fig.add_subplot(1,2,2)
box=ax.boxplot(X_r[:,0:10],whis=100)
ax.set_title(u"Distribution des premières composantes", fontsize=20)
fig.suptitle(u"Résultat ACP", fontsize=25)
Out[14]:
In [15]:
fig = plt.figure(figsize=(10,10), )
ax = fig.add_subplot(1,1,1)
plot_pca(X_r, Y_train_label,fig ,ax ,1 ,3)
Q Quelle est l'activité qui semble plus facile à identifier?