« SE5 ECEAI/eceai 2023/2024/jcie » : différence entre les versions
(96 versions intermédiaires par 2 utilisateurs non affichées) | |||
Ligne 1 : | Ligne 1 : | ||
== Introduction Projet IE Delannoy - Lemaire == | ==<div class="mcwiki-header" style="border-radius: 15px; padding: 15px; font-weight: bold; color: #FFFFFF; text-align: center; font-size: 80%; background: #42acc9; vertical-align: top; width: 96%;"> Introduction Projet IE Delannoy - Lemaire </div>== | ||
Dans le cadre la matière '''"Intelligence Embarquée"''' de l'UE "Module de Spécialité" des SE5 - Option Systèmes Communicants, nous avons dû concevoir un projet permettant de '''distribuer le calcul et le processus d'entrainement''' et d'utilisation d'un Intelligence Artificielle. Nous devrons distribuer le calcul à différents endroits du réseau afin d'être en mesure de proposer des pistes d'optimisation que ce soit en terme de performance ou en terme de sobriété énergétique. Pour cela, nous allons mettre en oeuvre un réseau d'objets connectés à des passerelles et à un serveur. '''Les objets collecteront des données''' de l'environnemet et suivants les scénarios, traiteront les données ou les enverront de façon brute à la passerelle ou au serveur. Nous aurons à disposition : | |||
* Une machine virtuelle sur le serveur '''Chassiron''' en ''E304/306'' | |||
* Une '''Raspberry PI 4''' | |||
* Un carte programmable '''STM32F401RE''' | |||
* Un capteur à ultrason '''Nucleo-53L5A1''' | |||
Ce wiki est le compte-rendu du projet de '''Jason DELANNOY et Chloé LEMAIRE'''. Nous avons décider de développer une intelligence artificielle permettant de '''déterminer''' '''l'action faite par un joueur de Pierre-Feuille-Ciseau''' à l'aide du capteur à ultrason. | |||
Pour ce faire, nous allons développer notre IA de deux façons : | |||
* En utilisant '''NanoEdge Studio''' : Dans ce cas, le calcul serait fait par la carte programmable et envoyé au serveur via la Raspberry | |||
* En utilisant '''un algorithme de machine learning ''': Ici, c'est le serveur qui fera le calcul et la Raspberry jouera le rôle de passerelle entre la carte programmable et le serveu | |||
==<div class="mcwiki-header" style="border-radius: 15px; padding: 15px; font-weight: bold; color: #FFFFFF; text-align: center; font-size: 80%; background: #42acc9; vertical-align: top; width: 96%;"> Mise en oeuvre du réseau </div>== | |||
=== Création VM XEN === | |||
Nous avons commencé par créer une machine virtuelle sur Chassiron avec la commande : | Nous avons commencé par créer une machine virtuelle sur Chassiron avec la commande : | ||
<pre> | <pre> | ||
xen-create-image --hostname jcie --force --dist bookworm --size 10G --memory 1G --dir /usr/local/xen --password glopglop --dhcp --bridge bridgeStudents</pre> | xen-create-image --hostname jcie --force --dist bookworm --size 10G --memory 1G --dir /usr/local/xen --password glopglop --dhcp --bridge bridgeStudents</pre> | ||
[[Fichier:Creation VM.png|centré|vignette|600x600px]]Notre VM est accessible en ssh à partir de son IPv6 : 2001:660:4401:6050:216:3eff:fe68:8176 | |||
=== Configuration de la VM === | |||
===== Configuration de l'interface enX0 en ipv6: ===== | |||
auto enX0 | |||
iface enX0 inet6 auto | |||
===== Modification du fichier resolv.conf: ===== | |||
Ce fichier permet de stocker les adresses des serveurs DNS du système. Dans notre cas, nous allons interroger le serveur DNS de l'école: plil.info. | |||
domain plil.info | |||
search plil.info | |||
nameserver 2a01:c916:2047:c800:216:3eff:fe82:8a5c | |||
===== Modification du fichier sources.list: ===== | |||
Ce fichier se situe dans le répertoire "/etc/apt/". Il référence l'ensemble des sources utilisées par l'utilitaire APT pour télécharger les paquets. | |||
Nous avons ajouté deux nouvelles sources : | |||
deb http://deb.debian.org/debian bookworm main contrib non-free non-free-firmware | |||
deb-src http://deb.debian.org/debian bookworm main contrib non-free non-free-firmware | |||
=== Configuration de la borne WIFI Cisco === | |||
La connexion avec la borne Wifi CISCO se fait via minicom sur le port ttyS0 avec un baudrate de 9600. | |||
Pour réaliser notre point d'accès, nous avons configurer l'interface dot11Radio1 | |||
[[Fichier:Intdot11.png|centré|vignette|500x500px]] | |||
Nous avons nommé notre point d'accès Wifi: WiFi_IE_1 | |||
Pour la connexion à celui-ci nous devons utiliser le mot de passe glopglop. | |||
[[Fichier:Dot11config.png|centré|vignette|500x500px]] | |||
Pour l'attribution de l'adresse, on utilise le protocole DHCP. Dans la définition de notre protocole, on exclut les intervalles d'adresses suivantes : 172.26.145.1 - 172.26.145.100 et 172.26.145.139 - 172.26.145.254 | |||
[[Fichier:Dhcp.png|centré|vignette|500x500px]]Le point d'accès a été modifié par Monsieur Redon pour les projets PEIP. On a maintenant : | |||
Dans le protocole DHCP: | |||
- default-router 172.26.145.251 (changement car besoin pour les PEIP) | |||
- dns-server: 193.48.57.33 (changement car problème avec le serveur DNS initial) | |||
==<div class="mcwiki-header" style="border-radius: 15px; padding: 15px; font-weight: bold; color: #FFFFFF; text-align: center; font-size: 80%; background: #42acc9; vertical-align: top; width: 96%;"> Configuration de la Raspberry </div>== | |||
Dans le cadra du projet, nous avons à disposition une Raspberry PI 4 model B que nous allons configurer. Pour ce faire, nous utilisons le logiciel "Raspberry PI Imager" pour choisir notre OS. | |||
[[Fichier:JCIE RPI Imager.png|centré|vignette|400x400px]]Maintenant que nous avons une carte SD prête pour la Raspberry, nous la configurons via sa liaison série en passant par minicom : | |||
root@zabeth18: / $ minicom -D /dev/ttyACM0 -b 9600 | |||
De plus, nous modifions le fichier /etc/network/interfaces pour pouvoir connecter notre carte au réseau wifi créé plus haut : | |||
[[Fichier:Screen WiFi.png|centré|vignette|441x441px]] | |||
Nous verifions ensuite que nous avons une adresse IP : | |||
[[Fichier:Screenshot from 2023-12-04 17-17-05.png|centré|vignette|500x500px]] | |||
==<div class="mcwiki-header" style="border-radius: 15px; padding: 15px; font-weight: bold; color: #FFFFFF; text-align: center; font-size: 80%; background: #42acc9; vertical-align: top; width: 96%;"> Capteur NUCLEO - 53L5A1 </div>== | |||
Pour ce projet, nous disposons d'une STM32 Nucléo F401RE muni d'un capteur à ultrason X-Nucleo-53L5A1. | |||
Pour pouvoir entrainer notre IA, nous devons faire en sorte d'afficher et de ressortir sur la liaison série les données du capteur. Pour ce faire, nous utilisons le fichier example "'''''53L5A1_ThresholdDetection'''''". Nous modifions la fonction print_result pour afficher 16 valeurs à la suite : | |||
[[Fichier:ChifoumIA Print Result.png|centré|vignette|500x500px]] | |||
Après avoir téléversé, nous pouvons vérifier le bon fonctionnement du programme en nous connectant à la carte via minicom : | |||
root@raspberrypi:/dev$ minicom -D ttyACM0 -b 460800 | |||
==<div class="mcwiki-header" style="border-radius: 15px; padding: 15px; font-weight: bold; color: #FFFFFF; text-align: center; font-size: 80%; background: #42acc9; vertical-align: top; width: 96%;"> Communication VM-Raspberry </div>== | |||
=== Envoie de donnée : === | |||
Dans notre projet, nous avons besoin de remonter les données de notre capteur branché en liaison série à la Raspberry sur le port "ttyACM0". Nous allons donc passer par des requêtes API entre le serveur sur chassiron et notre Raspberry. | |||
==== Programmation côté Raspberry : ==== | |||
Du côté Raspberry, nous programmons un script en python qui lit sur la liaison série les valeurs du capteurs et les envoie via une requête POST vers le serveur | |||
<pre> | |||
#Import Librairies | |||
import requests | |||
import json | |||
import serial | |||
<nowiki>#</nowiki>Variable | |||
STM32_port = "/dev/ttyACM0" | |||
baudrate = 460800 | |||
Serial_link = serial.Serial(STM32_port,baudrate,timeout=1) | |||
print("Serial link done") | |||
liste_mesures=[] | |||
for i in range(101): | |||
print("Mesure",i) | |||
mesure = Serial_link.readline().decode('utf-8') | |||
liste_mesures.append(mesure) | |||
data_to_send = json.dumps(liste_mesures) | |||
url_request = "http://[2001:660:4401:6050:216:3eff:fe68:8176]:8080" | |||
headers = { | |||
'Content-type':'application/json', | |||
'Accept':'application/json' | |||
} | |||
r = requests.post( | |||
url_request, | |||
json=data_to_send, | |||
headers=headers | |||
) | |||
print(f"Status code : {r.status_code}") | |||
</pre> | |||
==== Programmation côté Serveur : ==== | |||
La communication entre notre VM et la Raspberry est possible. Nous arrivons à effectuer un ping de l'une sur l'autre en passant par l'addresse IPv6. | |||
IPv6 VM : 2001:660:4401:6050:216:3eff:fe68:8176 | |||
IPv6 Raspberry : 2001:660:4401:6050:da3a:ddff:fe59:4cb5 | |||
Nous programmons ensuite une API sur notre VM en utilisant la libraire Flask. | |||
<pre> | |||
#Import Librairies | |||
import json | |||
from flask import Flask | |||
from flask import request | |||
from waitress import serve | |||
from flask import Response | |||
from flask_cors import CORS | |||
from flask_cors import cross_origin | |||
app = Flask(__name__) | |||
cors = CORS(app, ressourceas={r"/api/*":{"origins":"*"}}) | |||
def compute_data(data): | |||
filename = input("Nom du fichier :") | |||
to_write = data.split("\"")[1:-1] | |||
with open(filename, "w") as f: | |||
i = 0 | |||
for elem in to_write: | |||
if i == 0: | |||
print("We skip") | |||
else: | |||
if (i%2) == 0: | |||
print("Data writed :",elem[:-4]) | |||
f.write(elem[:-4]) | |||
f.write("\n") | |||
i = i + 1 | |||
#Main | |||
@app.route('/',methods=["GET","POST"]) | |||
@cross_origin() | |||
def hello(): | |||
data = request.json | |||
compute_data(data) | |||
return data | |||
if __name__ == '__main__': | |||
print("Listening") | |||
serve(app,host="::",port="8080") | |||
</pre> | |||
=== Implémentation de l'IA : === | |||
Maintenant qu'on a une communication pour envoyer un fichier à notre serveur depuis la raspberry grâce à notre API, nous la modifions pour que la Raspberry puisse lire la valeur du capteur à ultrason, l'envoyer à notre serveur et recevoir en réponse, la prédiction de l'IA. Nous créons donc une route '''''/predict''''' sur notre API qui récupère les valeurs du capteurs données en paramètre et renvoie la prédiction de l'IA : | |||
<pre> | |||
@app.route('/predict',methods=["GET","POST"]) | |||
def predict(): | |||
data = request.json | |||
print(data) | |||
features = data['Features'] | |||
print(features) | |||
features_splitted = features[0].split(",") | |||
features_int = [int(elem) for elem in features_splitted] | |||
loaded_model = joblib.load('model_entraine.pkl') | |||
predictios = loaded_model.predict(np.array(features_int).reshape(1,-1)) | |||
return jsonify(predictions.tolist()) | |||
</pre>Nous envoyons ensuite les données à analyser via des requêtes API : | |||
<pre> | |||
#Import Librairies | |||
import requests | |||
import json | |||
import serial | |||
from flask import jsonify | |||
#Variable | |||
STM32_port = "/dev/ttyACM0" | |||
baudrate = 460800 | |||
Serial_link = serial.Serial(STM32_port, baudrate, timeout=1) | |||
print("Serial link done") | |||
tab_to_send = [] | |||
while True: | |||
Serial_link.readline().decode('utf-8') | |||
tab_to_send.append(Serial_link.readline().decode('utf-8') | |||
if (tab_to_send != [""]): | |||
print("Data sended :",tab_to_send) | |||
url_request = "http://[2001:660:4401:6050:216:3eff:fe68:8176]:8080/predict" | |||
headers = { | |||
'Content-type':'application/json', | |||
'Accept':'application/json' | |||
} | |||
r = requests.post( | |||
url_request, | |||
json = { | |||
"Features": tab_to_send | |||
}, | |||
headers=headers | |||
) | |||
print(f"Status code : {r.status_code}") | |||
print("Response :",r.json()) | |||
else: | |||
print("Data skip:",tab_to_send) | |||
tab_to_send.clear() | |||
</pre> | |||
== <div class="mcwiki-header" style="border-radius: 15px; padding: 15px; font-weight: bold; color: #FFFFFF; text-align: center; font-size: 80%; background: #42acc9; vertical-align: top; width: 96%;"> Entrainement de l'IA </div> == | |||
=== Entrainement avec NanoEdge AI Studio : === | |||
Pour le projet, nous avons d'abord développé et entrainé notre IA sur la plateforme NanoEdge AI Studio. Pour ce faire, nous devons d'abord définir un projet de classification de n-class ("pierre","feuille","ciseau") en utilisant un capteur générique à 16 axes. Pour ce faire, nous modifions le code de la STM32 pour qu'elle renvoie une ligne contenant 16*16 mesure en continue. Une fois le projet créé, nous ajoutons 3 signaux correspondant à nos dataset de "Pierre", "Feuille" et "Ciseau". Pour rajouter des valeurs dans notre dataset, nous utilisons la liaison série pour lire les valeurs de notre capteur. | |||
=== Entrainement avec Scikit === | |||
Pour la suite de notre projet, nous avons developpé et entrainé une IA avec Scikit. | |||
==== Construction du fichier d'entrainement ==== | |||
Pour commencer, nous avons du construire un fichier csv reprenant nos mesures sur un format de 16 valeurs. Pour chaque mesure, on ajoute en amont de la mesure la catégorie (feuille, ciseau ou pierre). | |||
[[Fichier:Capture d’écran 2024-01-18 à 16.18.19.png|centré|vignette|500x500px|Fichier chifoumIA.csv]] | |||
Pour construire ce fichier, nous avons assemblé trois fichiers csv de test que nous avons généré grâce à notre capteur : un fichier d'entrainement Pierre.csv, un fichier d'entrainement Ciseau.csv et un fichier d'entrainement Feuille.csv . | |||
Pour faire cette concaténation nous avons développer un programme python prenant en entrée nos fichiers d'entrainement et fournissant en sortie le fichier csv chifoumIA. | |||
<pre> | |||
line_feuille = [] | |||
line_pierre = [] | |||
line_ciseau = [] | |||
with open("Feuille.csv","r") as f: | |||
line_feuille = f.readlines() | |||
with open("Pierre.csv","r") as f: | |||
line_pierre = f.readlines() | |||
with open("Ciseau.csv","r") as f: | |||
line_ciseau = f.readlines() | |||
== | with open("chifoumIA.csv","w") as f: | ||
for elem in line_feuille: | |||
if(elem != ""): | |||
f.write("feuille,") | |||
f.write(elem) | |||
for elem in line_pierre: | |||
if(elem != ""): | |||
f.write("pierre,") | |||
f.write(elem) | |||
for elem in line_ciseau: | |||
if(elem != ""): | |||
f.write("ciseau,") | |||
f.write(elem) | |||
</pre> | |||
==== Implémentation et Entrainement de notre IA ==== | |||
Maintenant que nous avons notre fichier d'entrainement, nous implémentons notre IA. | |||
Nous avons commencé par créer un tableau de données et un tableau de classes. | |||
Pour remplir ces tableaux, on va lire ligne par ligne le fichier chifoumIA.csv. Le premier élément de la ligne est stockée dans le tableau de classes et le reste de la ligne est stockée dans le tableau de données. | |||
chifoumi_data=[] | |||
chifoumi_classe=[] | |||
with open('chifoumIA.csv') as csv_file: | |||
csv_reader = csv.reader(csv_file,delimiter=',') | |||
for row in csv_reader: | |||
chifoumi_data.append([int(row[1]),int(row[2]),int(row[3]),int(row[4]),int(row[5]),int(row[6]),int(row[7]),int(row[8]), | |||
int(row[9]),int(row[10]),int(row[11]),int(row[12]),int(row[13]),int(row[14]),int(row[15]),int(row[16])]) | |||
chifoumi_classe.append(row[0]) | |||
A partir de ces deux tableaux, on construit un ensemble de données d'entraînement et un ensemble de données de tests. | |||
<pre> | On choisi d'utiliser un réseau de neurone du type MLPClassifer. | ||
Le MLPClassifer est un type de réseau de neurones artificiels composés d'au minimum trois couches de neurones: une couche d'entrée, une ou plusieurs couches cachées et une couche de sortie. | |||
Dans ce type de réseau, chaque neurone est connecté à l'ensemble des neurones de la couche suivante par un lien auquel est associé un poids. | |||
La classe MLPClassifer implémentée dans la bibliothèque Scikit-Learn dispose de nombreux paramètres. Pour notre projet, nous avons utilisé cette classe de façon très primaire. Les paramètres que nous avons utilisés sont : | |||
- '''hidden_layer_sizes :''' ce paramètre est un tuple spécifiant le nombre de neurones dans chaque couche cachée. | |||
- '''max_iter :''' ce paramètre représente le nombre maximal d'époque. Il permet de contrôler également le temps d'entrainement. | |||
<pre> | |||
model=MLPClassifier(hidden_layer_sizes=(50,25),max_iter=100,solver='adam') | |||
model.fit(X_train,y_train) | |||
<nowiki>#</nowiki>predictions = model.predict(X_test) | |||
<nowiki>#</nowiki>accuracy = accuracy_score(y_test,predictions) | |||
<nowiki>#</nowiki>print(f'Accuracy:{accuracy}') | |||
</pre> | |||
==== Déploiement de notre IA ==== | |||
Pour pouvoir déployer notre IA, nous avons enregistré notre model d'entrainement et intégré un service web à notre code existant avec deux endpoint : | |||
- predict : pour effectuer des prédictions sur le model. | |||
- evaluate : pour évaluer les performances du model sur l'ensemble de tests. | |||
Malheureusement nous n'avons pas eu le temps d'entrainer suffisamment cette IA. Ces performances sont donc très faible un peu moins de 50% de fiabilité. | |||
==== Code complet ==== | |||
<pre> | |||
import joblib | |||
import pandas as pd | |||
import csv | |||
from flask import Flask, request,jsonify | |||
from sklearn.model_selection import train_test_split | |||
from sklearn.ensemble import RandomForestClassifier | |||
from sklearn.metrics import accuracy_score,precision_score,recall_score,f1_score | |||
from sklearn.neural_network import MLPClassifier | |||
from waitress import serve | |||
from flask import Response | |||
from flask_cors import CORS | |||
from flask_cors import cross_origin | |||
import numpy as np | |||
chifoumi_data=[] | |||
chifoumi_classe=[] | |||
with open('chifoumIA.csv') as csv_file: | |||
csv_reader = csv.reader(csv_file,delimiter=',') | |||
for row in csv_reader: | |||
chifoumi_data.append([int(row[1]),int(row[2]),int(row[3]),int(row[4]),int(row[5]),int(row[6]),int(row[7]),int(row[8]),int(row[9]),int(row[10]),int(row[11]),int(row[12]),int(row[13]),int(row[14]),int(row[15]),int(row[16])]) | |||
chifoumi_classe.append(row[0]) | |||
X_train,X_test,y_train,y_test= train_test_split(chifoumi_data,chifoumi_classe,test_size=0.2,random_state=42) | |||
model=MLPClassifier(hidden_layer_sizes=(50,25),max_iter=100,solver='adam') | |||
model.fit(X_train,y_train) | |||
#predictions = model.predict(X_test) | |||
#accuracy = accuracy_score(y_test,predictions) | |||
#print(f'Accuracy:{accuracy}') | |||
joblib.dump(model,'model_entraine.pkl') | |||
app = Flask(__name__) | |||
cors = CORS(app,ressources={r"/api/*":{"origins": "*"}}) | |||
#Main | |||
@app.route('/predict',methods=["GET","POST"]) | |||
#@cross_origin() | |||
def predict(): | |||
data = request.json | |||
print(data) | |||
features = data['Features'] | |||
print(features) | |||
features_splitted = features[0].split(",") | |||
features_int = [int(elem) for elem in features_splitted] | |||
loaded_model=joblib.load('model_entraine.pkl') | |||
predictions = loaded_model.predict(np.array(features_int).reshape(1,-1)) | |||
return jsonify(predictions.tolist()) | |||
@app.route('/evaluate',methods=["GET","POST"]) | |||
def evaluate(): | |||
loaded_model = joblib.load('model_entraine.pkl') | |||
predictions = loaded_model.predict(X_test) | |||
accuracy = accuracy_score(y_test,predictions) | |||
precision = precision_score(y_test,predictions) | |||
recall = recall_score(y_test,predictions) | |||
f1=f1_score(y_test,predictions) | |||
return jsonify({ 'Accuracy': accuracy,'Precision': precision,'Recall': recall,'F1 Score':f1}) | |||
if __name__ == '__main__': | |||
print("listening") | |||
serve(app,host="::", port="8080") | |||
</pre> | |||
==<div class="mcwiki-header" style="border-radius: 15px; padding: 15px; font-weight: bold; color: #FFFFFF; text-align: center; font-size: 80%; background: #42acc9; vertical-align: top; width: 96%;"> Performance</div>== | |||
=== Performance de NanoEdge : === | |||
Après avoir paramétré NanoEdge et lancé l'acquisition de valeur depuis la STM32, nous pouvons lancer un Benchmark qui va pouvoir entrainer notre IA. Voici les résultats : | |||
[[Fichier:Image 2024-01-28 171138780.png|centré|vignette|400x400px]] | |||
Nous obtenons donc un score de 73,65 % dont une balanced accuracy de 78,27 %. Cependant, après analyse des résultats, nous remarquons que l'IA a énormément de mal à distinguer les ciseaux, les confondant souvent avec des feuilles et des pierres. Ce résultat était prévisible dû à la resemblance des gestes et à la précision du capteur. Voici un test en vidéo de l'IA : | |||
[[Fichier:VID 20240128 170828.mp4|centré|vignette]] | |||
On remarque ici que le premier essai devine la pierre comme prévu mais lorsque qu'on essaie la feuille, il ressort pierre à égalité avec feuille (49% chacun). Ensuite, tel prévu, il ne reconnait pas du tout le ciseau. | |||
On peut aussi remarqué que l'acquisition des valeur est très lente mais l'IA fournit un résultat convaicant pour le projet | |||
=== Performance Serveur : === | |||
De ce point là, et après le cours, la Raspberry a cessé de vouloir envoyer des requêtes API vers notre serveur sans qu'on ne sache pourquoi. Pour avoir essayé en cours, l'acquisition des données était plutôt rapide mais cependant, et comme expliqué plus haut, le résultat n'était pas du tout convaincant (avec moins de 50% de réussite) |
Version actuelle datée du 28 janvier 2024 à 16:36
Introduction Projet IE Delannoy - Lemaire
Dans le cadre la matière "Intelligence Embarquée" de l'UE "Module de Spécialité" des SE5 - Option Systèmes Communicants, nous avons dû concevoir un projet permettant de distribuer le calcul et le processus d'entrainement et d'utilisation d'un Intelligence Artificielle. Nous devrons distribuer le calcul à différents endroits du réseau afin d'être en mesure de proposer des pistes d'optimisation que ce soit en terme de performance ou en terme de sobriété énergétique. Pour cela, nous allons mettre en oeuvre un réseau d'objets connectés à des passerelles et à un serveur. Les objets collecteront des données de l'environnemet et suivants les scénarios, traiteront les données ou les enverront de façon brute à la passerelle ou au serveur. Nous aurons à disposition :
- Une machine virtuelle sur le serveur Chassiron en E304/306
- Une Raspberry PI 4
- Un carte programmable STM32F401RE
- Un capteur à ultrason Nucleo-53L5A1
Ce wiki est le compte-rendu du projet de Jason DELANNOY et Chloé LEMAIRE. Nous avons décider de développer une intelligence artificielle permettant de déterminer l'action faite par un joueur de Pierre-Feuille-Ciseau à l'aide du capteur à ultrason.
Pour ce faire, nous allons développer notre IA de deux façons :
- En utilisant NanoEdge Studio : Dans ce cas, le calcul serait fait par la carte programmable et envoyé au serveur via la Raspberry
- En utilisant un algorithme de machine learning : Ici, c'est le serveur qui fera le calcul et la Raspberry jouera le rôle de passerelle entre la carte programmable et le serveu
Mise en oeuvre du réseau
Création VM XEN
Nous avons commencé par créer une machine virtuelle sur Chassiron avec la commande :
xen-create-image --hostname jcie --force --dist bookworm --size 10G --memory 1G --dir /usr/local/xen --password glopglop --dhcp --bridge bridgeStudents
Notre VM est accessible en ssh à partir de son IPv6 : 2001:660:4401:6050:216:3eff:fe68:8176
Configuration de la VM
Configuration de l'interface enX0 en ipv6:
auto enX0 iface enX0 inet6 auto
Modification du fichier resolv.conf:
Ce fichier permet de stocker les adresses des serveurs DNS du système. Dans notre cas, nous allons interroger le serveur DNS de l'école: plil.info.
domain plil.info search plil.info nameserver 2a01:c916:2047:c800:216:3eff:fe82:8a5c
Modification du fichier sources.list:
Ce fichier se situe dans le répertoire "/etc/apt/". Il référence l'ensemble des sources utilisées par l'utilitaire APT pour télécharger les paquets.
Nous avons ajouté deux nouvelles sources :
deb http://deb.debian.org/debian bookworm main contrib non-free non-free-firmware
deb-src http://deb.debian.org/debian bookworm main contrib non-free non-free-firmware
Configuration de la borne WIFI Cisco
La connexion avec la borne Wifi CISCO se fait via minicom sur le port ttyS0 avec un baudrate de 9600.
Pour réaliser notre point d'accès, nous avons configurer l'interface dot11Radio1
Nous avons nommé notre point d'accès Wifi: WiFi_IE_1
Pour la connexion à celui-ci nous devons utiliser le mot de passe glopglop.
Pour l'attribution de l'adresse, on utilise le protocole DHCP. Dans la définition de notre protocole, on exclut les intervalles d'adresses suivantes : 172.26.145.1 - 172.26.145.100 et 172.26.145.139 - 172.26.145.254
Le point d'accès a été modifié par Monsieur Redon pour les projets PEIP. On a maintenant :
Dans le protocole DHCP:
- default-router 172.26.145.251 (changement car besoin pour les PEIP)
- dns-server: 193.48.57.33 (changement car problème avec le serveur DNS initial)
Configuration de la Raspberry
Dans le cadra du projet, nous avons à disposition une Raspberry PI 4 model B que nous allons configurer. Pour ce faire, nous utilisons le logiciel "Raspberry PI Imager" pour choisir notre OS.
Maintenant que nous avons une carte SD prête pour la Raspberry, nous la configurons via sa liaison série en passant par minicom :
root@zabeth18: / $ minicom -D /dev/ttyACM0 -b 9600
De plus, nous modifions le fichier /etc/network/interfaces pour pouvoir connecter notre carte au réseau wifi créé plus haut :
Nous verifions ensuite que nous avons une adresse IP :
Capteur NUCLEO - 53L5A1
Pour ce projet, nous disposons d'une STM32 Nucléo F401RE muni d'un capteur à ultrason X-Nucleo-53L5A1.
Pour pouvoir entrainer notre IA, nous devons faire en sorte d'afficher et de ressortir sur la liaison série les données du capteur. Pour ce faire, nous utilisons le fichier example "53L5A1_ThresholdDetection". Nous modifions la fonction print_result pour afficher 16 valeurs à la suite :
Après avoir téléversé, nous pouvons vérifier le bon fonctionnement du programme en nous connectant à la carte via minicom :
root@raspberrypi:/dev$ minicom -D ttyACM0 -b 460800
Communication VM-Raspberry
Envoie de donnée :
Dans notre projet, nous avons besoin de remonter les données de notre capteur branché en liaison série à la Raspberry sur le port "ttyACM0". Nous allons donc passer par des requêtes API entre le serveur sur chassiron et notre Raspberry.
Programmation côté Raspberry :
Du côté Raspberry, nous programmons un script en python qui lit sur la liaison série les valeurs du capteurs et les envoie via une requête POST vers le serveur
#Import Librairies import requests import json import serial #Variable STM32_port = "/dev/ttyACM0" baudrate = 460800 Serial_link = serial.Serial(STM32_port,baudrate,timeout=1) print("Serial link done") liste_mesures=[] for i in range(101): print("Mesure",i) mesure = Serial_link.readline().decode('utf-8') liste_mesures.append(mesure) data_to_send = json.dumps(liste_mesures) url_request = "http://[2001:660:4401:6050:216:3eff:fe68:8176]:8080" headers = { 'Content-type':'application/json', 'Accept':'application/json' } r = requests.post( url_request, json=data_to_send, headers=headers ) print(f"Status code : {r.status_code}")
Programmation côté Serveur :
La communication entre notre VM et la Raspberry est possible. Nous arrivons à effectuer un ping de l'une sur l'autre en passant par l'addresse IPv6.
IPv6 VM : 2001:660:4401:6050:216:3eff:fe68:8176
IPv6 Raspberry : 2001:660:4401:6050:da3a:ddff:fe59:4cb5
Nous programmons ensuite une API sur notre VM en utilisant la libraire Flask.
#Import Librairies import json from flask import Flask from flask import request from waitress import serve from flask import Response from flask_cors import CORS from flask_cors import cross_origin app = Flask(__name__) cors = CORS(app, ressourceas={r"/api/*":{"origins":"*"}}) def compute_data(data): filename = input("Nom du fichier :") to_write = data.split("\"")[1:-1] with open(filename, "w") as f: i = 0 for elem in to_write: if i == 0: print("We skip") else: if (i%2) == 0: print("Data writed :",elem[:-4]) f.write(elem[:-4]) f.write("\n") i = i + 1 #Main @app.route('/',methods=["GET","POST"]) @cross_origin() def hello(): data = request.json compute_data(data) return data if __name__ == '__main__': print("Listening") serve(app,host="::",port="8080")
Implémentation de l'IA :
Maintenant qu'on a une communication pour envoyer un fichier à notre serveur depuis la raspberry grâce à notre API, nous la modifions pour que la Raspberry puisse lire la valeur du capteur à ultrason, l'envoyer à notre serveur et recevoir en réponse, la prédiction de l'IA. Nous créons donc une route /predict sur notre API qui récupère les valeurs du capteurs données en paramètre et renvoie la prédiction de l'IA :
@app.route('/predict',methods=["GET","POST"]) def predict(): data = request.json print(data) features = data['Features'] print(features) features_splitted = features[0].split(",") features_int = [int(elem) for elem in features_splitted] loaded_model = joblib.load('model_entraine.pkl') predictios = loaded_model.predict(np.array(features_int).reshape(1,-1)) return jsonify(predictions.tolist())
Nous envoyons ensuite les données à analyser via des requêtes API :
#Import Librairies import requests import json import serial from flask import jsonify #Variable STM32_port = "/dev/ttyACM0" baudrate = 460800 Serial_link = serial.Serial(STM32_port, baudrate, timeout=1) print("Serial link done") tab_to_send = [] while True: Serial_link.readline().decode('utf-8') tab_to_send.append(Serial_link.readline().decode('utf-8') if (tab_to_send != [""]): print("Data sended :",tab_to_send) url_request = "http://[2001:660:4401:6050:216:3eff:fe68:8176]:8080/predict" headers = { 'Content-type':'application/json', 'Accept':'application/json' } r = requests.post( url_request, json = { "Features": tab_to_send }, headers=headers ) print(f"Status code : {r.status_code}") print("Response :",r.json()) else: print("Data skip:",tab_to_send) tab_to_send.clear()
Entrainement de l'IA
Entrainement avec NanoEdge AI Studio :
Pour le projet, nous avons d'abord développé et entrainé notre IA sur la plateforme NanoEdge AI Studio. Pour ce faire, nous devons d'abord définir un projet de classification de n-class ("pierre","feuille","ciseau") en utilisant un capteur générique à 16 axes. Pour ce faire, nous modifions le code de la STM32 pour qu'elle renvoie une ligne contenant 16*16 mesure en continue. Une fois le projet créé, nous ajoutons 3 signaux correspondant à nos dataset de "Pierre", "Feuille" et "Ciseau". Pour rajouter des valeurs dans notre dataset, nous utilisons la liaison série pour lire les valeurs de notre capteur.
Entrainement avec Scikit
Pour la suite de notre projet, nous avons developpé et entrainé une IA avec Scikit.
Construction du fichier d'entrainement
Pour commencer, nous avons du construire un fichier csv reprenant nos mesures sur un format de 16 valeurs. Pour chaque mesure, on ajoute en amont de la mesure la catégorie (feuille, ciseau ou pierre).
Pour construire ce fichier, nous avons assemblé trois fichiers csv de test que nous avons généré grâce à notre capteur : un fichier d'entrainement Pierre.csv, un fichier d'entrainement Ciseau.csv et un fichier d'entrainement Feuille.csv .
Pour faire cette concaténation nous avons développer un programme python prenant en entrée nos fichiers d'entrainement et fournissant en sortie le fichier csv chifoumIA.
line_feuille = [] line_pierre = [] line_ciseau = [] with open("Feuille.csv","r") as f: line_feuille = f.readlines() with open("Pierre.csv","r") as f: line_pierre = f.readlines() with open("Ciseau.csv","r") as f: line_ciseau = f.readlines() with open("chifoumIA.csv","w") as f: for elem in line_feuille: if(elem != ""): f.write("feuille,") f.write(elem) for elem in line_pierre: if(elem != ""): f.write("pierre,") f.write(elem) for elem in line_ciseau: if(elem != ""): f.write("ciseau,") f.write(elem)
Implémentation et Entrainement de notre IA
Maintenant que nous avons notre fichier d'entrainement, nous implémentons notre IA.
Nous avons commencé par créer un tableau de données et un tableau de classes.
Pour remplir ces tableaux, on va lire ligne par ligne le fichier chifoumIA.csv. Le premier élément de la ligne est stockée dans le tableau de classes et le reste de la ligne est stockée dans le tableau de données.
chifoumi_data=[] chifoumi_classe=[] with open('chifoumIA.csv') as csv_file: csv_reader = csv.reader(csv_file,delimiter=',') for row in csv_reader: chifoumi_data.append([int(row[1]),int(row[2]),int(row[3]),int(row[4]),int(row[5]),int(row[6]),int(row[7]),int(row[8]), int(row[9]),int(row[10]),int(row[11]),int(row[12]),int(row[13]),int(row[14]),int(row[15]),int(row[16])]) chifoumi_classe.append(row[0])
A partir de ces deux tableaux, on construit un ensemble de données d'entraînement et un ensemble de données de tests.
On choisi d'utiliser un réseau de neurone du type MLPClassifer.
Le MLPClassifer est un type de réseau de neurones artificiels composés d'au minimum trois couches de neurones: une couche d'entrée, une ou plusieurs couches cachées et une couche de sortie.
Dans ce type de réseau, chaque neurone est connecté à l'ensemble des neurones de la couche suivante par un lien auquel est associé un poids.
La classe MLPClassifer implémentée dans la bibliothèque Scikit-Learn dispose de nombreux paramètres. Pour notre projet, nous avons utilisé cette classe de façon très primaire. Les paramètres que nous avons utilisés sont :
- hidden_layer_sizes : ce paramètre est un tuple spécifiant le nombre de neurones dans chaque couche cachée.
- max_iter : ce paramètre représente le nombre maximal d'époque. Il permet de contrôler également le temps d'entrainement.
model=MLPClassifier(hidden_layer_sizes=(50,25),max_iter=100,solver='adam') model.fit(X_train,y_train) #predictions = model.predict(X_test) #accuracy = accuracy_score(y_test,predictions) #print(f'Accuracy:{accuracy}')
Déploiement de notre IA
Pour pouvoir déployer notre IA, nous avons enregistré notre model d'entrainement et intégré un service web à notre code existant avec deux endpoint :
- predict : pour effectuer des prédictions sur le model.
- evaluate : pour évaluer les performances du model sur l'ensemble de tests.
Malheureusement nous n'avons pas eu le temps d'entrainer suffisamment cette IA. Ces performances sont donc très faible un peu moins de 50% de fiabilité.
Code complet
import joblib import pandas as pd import csv from flask import Flask, request,jsonify from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import accuracy_score,precision_score,recall_score,f1_score from sklearn.neural_network import MLPClassifier from waitress import serve from flask import Response from flask_cors import CORS from flask_cors import cross_origin import numpy as np chifoumi_data=[] chifoumi_classe=[] with open('chifoumIA.csv') as csv_file: csv_reader = csv.reader(csv_file,delimiter=',') for row in csv_reader: chifoumi_data.append([int(row[1]),int(row[2]),int(row[3]),int(row[4]),int(row[5]),int(row[6]),int(row[7]),int(row[8]),int(row[9]),int(row[10]),int(row[11]),int(row[12]),int(row[13]),int(row[14]),int(row[15]),int(row[16])]) chifoumi_classe.append(row[0]) X_train,X_test,y_train,y_test= train_test_split(chifoumi_data,chifoumi_classe,test_size=0.2,random_state=42) model=MLPClassifier(hidden_layer_sizes=(50,25),max_iter=100,solver='adam') model.fit(X_train,y_train) #predictions = model.predict(X_test) #accuracy = accuracy_score(y_test,predictions) #print(f'Accuracy:{accuracy}') joblib.dump(model,'model_entraine.pkl') app = Flask(__name__) cors = CORS(app,ressources={r"/api/*":{"origins": "*"}}) #Main @app.route('/predict',methods=["GET","POST"]) #@cross_origin() def predict(): data = request.json print(data) features = data['Features'] print(features) features_splitted = features[0].split(",") features_int = [int(elem) for elem in features_splitted] loaded_model=joblib.load('model_entraine.pkl') predictions = loaded_model.predict(np.array(features_int).reshape(1,-1)) return jsonify(predictions.tolist()) @app.route('/evaluate',methods=["GET","POST"]) def evaluate(): loaded_model = joblib.load('model_entraine.pkl') predictions = loaded_model.predict(X_test) accuracy = accuracy_score(y_test,predictions) precision = precision_score(y_test,predictions) recall = recall_score(y_test,predictions) f1=f1_score(y_test,predictions) return jsonify({ 'Accuracy': accuracy,'Precision': precision,'Recall': recall,'F1 Score':f1}) if __name__ == '__main__': print("listening") serve(app,host="::", port="8080")
Performance
Performance de NanoEdge :
Après avoir paramétré NanoEdge et lancé l'acquisition de valeur depuis la STM32, nous pouvons lancer un Benchmark qui va pouvoir entrainer notre IA. Voici les résultats :
Nous obtenons donc un score de 73,65 % dont une balanced accuracy de 78,27 %. Cependant, après analyse des résultats, nous remarquons que l'IA a énormément de mal à distinguer les ciseaux, les confondant souvent avec des feuilles et des pierres. Ce résultat était prévisible dû à la resemblance des gestes et à la précision du capteur. Voici un test en vidéo de l'IA :
On remarque ici que le premier essai devine la pierre comme prévu mais lorsque qu'on essaie la feuille, il ressort pierre à égalité avec feuille (49% chacun). Ensuite, tel prévu, il ne reconnait pas du tout le ciseau. On peut aussi remarqué que l'acquisition des valeur est très lente mais l'IA fournit un résultat convaicant pour le projet
Performance Serveur :
De ce point là, et après le cours, la Raspberry a cessé de vouloir envoyer des requêtes API vers notre serveur sans qu'on ne sache pourquoi. Pour avoir essayé en cours, l'acquisition des données était plutôt rapide mais cependant, et comme expliqué plus haut, le résultat n'était pas du tout convaincant (avec moins de 50% de réussite)