« SE5 ECEAI/eceai 2023/2024/jcie » : différence entre les versions

De wiki-se.plil.fr
Aller à la navigation Aller à la recherche
Ligne 406 : Ligne 406 :
</pre>
</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 et Sobriété Energétique</div>==
==<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 : ===
=== Performance de NanoEdge : ===

Version du 28 janvier 2024 à 16:19

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
Creation VM.png

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

Intdot11.png

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.

Dot11config.png

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

Dhcp.png

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.

JCIE RPI Imager.png

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 :

Screen WiFi.png

Nous verifions ensuite que nous avons une adresse IP :

Screenshot from 2023-12-04 17-17-05.png

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 :

ChifoumIA Print Result.png


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).

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.

     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 :

Image 2024-01-28 171138780.png


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 :