Papelera Inteligente con Machine Learning

Alumnos

Alejandro Delgado García 

Marco Zucchi Mesia

Joaquín Ferrer Núñez 

Introducción y objetivo de la práctica

En primer lugar, esta práctica consistía en realizar un proyecto en el que se use lo que se ha aprendido en clase con Arduino de manera práctica. De esta manera, se adquiriría mayor soltura con los componentes electrónicos en proyecto real. Nuestro grupo decidió crear una papelera que detecte distintos tipos de residuos, y según el tipo de residuo lo dirija a un contenedor u otro. Para ello, se ha hecho uso del kit de Machine Learning de Arduino (creado por Edge Impulse®), con el fin de poder identificar los objetos que se deseen tirar a la basura. La idea de este proyecto es poder reciclar abstrayendo al usuario de a dónde tirar cada residuo.

Video de muestra:

Materiales utilizados y precios

ObjetoPrecio
Caja de cartón 5,49€ 
Spray pintura verde 3,89€ 
Servomotor (2) 2,52€ (5,04€) 
Kit Machine Learning Proporcionad por la universidad 
Kit Arduino Uno R3 Proporcionad por la universidad 
Total: 14,42€ 

Montaje de la caja

El montaje consiste en dos rampas, una más grande (la inferior), que se mueve por un servomotor hacia delante y hacia detrás, y una más pequeña (la superior), que gira hacia la derecha o hacia la izquierda gracias a otro servomotor.

En la parte inferior de la caja (aunque no se puede apreciar bien en las imágenes) se encuentran unas separaciones en forma de cruz para formar los cuatro cubículos que hacen de contenedores. En la parte exterior de la caja se encuentran cuatro puertas que van a dichos cubículos que indican que contiene dicho cubículo y a que numero del mando pertenece.

En la segunda imagen, en la parte que se ha coloreado con un círculo rojo, es donde se esperaba que se posicionase el módulo de Machine Learning con la cámara, para que identificase los objetos que se colocasen en la rampa superior y proceder a tirarlo a su contenedor correspondiente. Como al final no se pudo implementar el módulo de Machine Learning por razones que se explicarán más adelante, se situó el circuito como se ve en la primera imagen. Además, en la mini-protoboard se encuentra el sensor de infrarrojo para el mando.

Circuito

Ahora a continuación se procederá a explicar el circuito. El circuito cuenta con dos servomotores conectados a los pines PWM 11 y 9. Estos, además, también están conectados a los 5V de la placa y al GND. Por otra parte, se encuentra el sensor infrarrojo posicionado en la protoboard. Al igual que los servos, está conectado a la alimentación de 5V, al GND, y a un pin PWM, que en su caso es el 3.

En caso de usar el módulo de machine learning, los dos servos irían conectados a los pines digitales D12 y D11 del shield.

Machine Learning

En cuanto a la parte de Machine Learning, se trataba del grueso del proyecto y una de las partes que nos resultaron más difíciles de comprender. Para ello contábamos con varios componentes y frameworks de desarrollo, que facilitaron la obtención de imágenes de prueba, el entrenamiento y por último, la compresión del modelo en el sistema embebido de Arduino Nano, que venía en el kit de Machine Learning de Edge Impulse®:

  • JMD Imagescraper (repositorio)
  • Tensorflow Lite y Tensorflow Lite Micro
  • Arduino IDE

El primer repositorio proporcionaba una serie de scripts que permitían de manera más o menos sencilla obtener gran cantidad de imágenes etiquetadas por directorio para ciertas búsquedas. Esto se llevaba a cabo mediante búsquedas en el buscador Duckduckgo. En segundo lugar, tenemos el framework open-source para entrenamiento de modelos de red neuronal Tensorflow Lite y Tensorflow Lite Micro (versión para microcontroladores, con limitaciones de memoria y capacidades de procesamiento muy limitadas). Por último, el Arduino IDE se trata de un entorno de desarrollo que nos permite interactuar con los dispositivos de Arduino, mediante la programación del bootloader en un lenguaje parecido a C.

Por último en esta introducción, cabe destacar que muchos de los conocimientos de Machine Learning, así como parte del despliegue en la plataforma de Arduino, se deben a los tres cursos de Harvard con el kit de Machine Learning TinyMLx :

Ahora a continuación se procederá a explicar las distintas fases dentro del Machine Learning:

  1. Obtención de imágenes de prueba (para entrenar, validar y evaluar el modelo)
  2. propio del modelo
  3. Compresión del modelo para que quepa en las limitaciones físicas del Arduino Nano

Obtención de imágenes de prueba

Para que la red neuronal pueda aprender, necesita un dataset etiquetado y dividido en las diferentes clases que queremos clasificar. Para ello, como se ha mencionado anteriormente, se usó el script de JMD imagescraper, que permite obtener imágenes de DuckDuckGo, usando palabras claves. La estructura de directorios resultante es la siguiente:

En cada una de las subcarpetas, se encontrarán las diferentes imágenes que se pasarán al modelo en las fases de entrenamiento, validación o evaluación.

El script resultante para descargar las imágenes es el siguiente:

Entrenamiento del modelo

El entrenamiento del modelo se ha hecho con Tensorflow en Python, ya que se tiene más capacidad de cómputo para entrenar el modelo.

En primer lugar, se ha cargado el dataset de la carpeta, con parámetros para que se dividiese en dos, dataset de entranamiento y de validación:

Luego, se construyó la arquitectura de la red neuronal, con cuatro capas a alto nivel: la capa de aumentación de datos (transformaciones al dataset original), modelo base pre-entrenado, capas adicionales para clasificación específica a nuestro caso de uso y capa de predicción:

Luego, se construyó la arquitectura de la red neuronal, con cuatro capas a alto nivel: la capa de aumentación de datos (transformaciones al dataset original), modelo base pre-entrenado, capas adicionales para clasificación específica a nuestro caso de uso y capa de predicción:

A continuación, se muestran, cada una de las capas a alto nivel individualmente:

En último lugar, se muestra cómo se han entrenado los modelos y guardado en archivos:

Compresión del modelo

En esta fase, se ha comprimido el modelo con ciertas optimizaciones para poder reducir el tamaño que ocupa (dado que en el Arduino Nano tenemos solo 1MB de memoria flash y 256KB de RAM). Para ello, se ha convertido, primero en un formato .tflite y segundo, en un array estilo-C para poder ponerlo dentro del Arduino Nano. Se muestra el código a continuación:

Arduino

El código de Arduino es muy largo, pero en cuanto a compresión del modelo, existen diferentes técnicas que permiten reducir incluso más el tamaño del modelo. Estas técnicas se encuentran dentro del marco de Tensorflow Lite Micro (“para microcontroladores”) y permiten añadir, solo las operaciones de la librería de Tensorflow que se necesitan en el modelo.

Para determinar las operaciones que necesita el modelo se puede usar una página visualizadora de capas como Netron. A continuación, se muestran algunas de las capas de nuestro modelo, junto con una captura que muestra cómo se añaden dichas capas en fichero .ino

En el código anterior, también se muestra el intérpretede Tensorflow Lite Micro que permite modificar instrucciones del modelo sin tener que recompilar el código y solo añadiendo una ligera latencia (despreciable debido a las numerosas iteraciones/epochs del modelo).

Cámara OV7675

Un inciso sobre la cámara OV7675. Nos hemos guiado de ejemplos de Arduino en Github, que permitían obtener una imagen, mediante el siguiente código:

Código de Arduino

En cuanto al código, se ha decidido dividirlo en distintos archivos, debido a que entre todos los archivos es muy pesado (el modelo tiene más de 10.000 líneas de código). El código se puede encontrar completo en el siguiente enlace: https://gist.github.com/marcozucchi/24256f1e4142c39c3fad6f1333f7441e

El código del archivo principal es el siguiente:

#include <TensorFlowLite.h>

//librerias de machine learning
#include "tensorflow/lite/micro/micro_interpreter.h"
#include "tensorflow/lite/micro/micro_mutable_op_resolver.h"
#include "tensorflow/lite/schema/schema_generated.h"
#include "model.h"
#include "model_settings.h"
#include "detection_responder.h"
#include "tensorflow/lite/micro/system_setup.h"
#include "tensorflow/lite/micro/micro_log.h"

//librerias de servomotores y sensor infrarrojo
#include <Servo.h>
#include <IRremote.h>

//teclas del mando
#define TECLA1 0xF30CFF00
#define TECLA2 0xE718FF00
#define TECLA3 0xA15EFF00
#define TECLA4 0xF708FF00
 
Servo servomotor1;
Servo servomotor2;
int IR =12;

//creacion de modelo, interprete, error reporter y entradas a null, así como el tamaño de memoria del modelo.
namespace {
  tflite::ErrorReporter* error_reporter = nullptr;
  const tflite::Model* model = nullptr;
  tflite::MicroInterpreter* interpreter = nullptr;
  TfLiteTensor* input = nullptr;

  constexpr int kTensorArenaSize = 90*1024;
  uint8_t tensor_arena[kTensorArenaSize];

}

//funciones auxiliares para el movimiento de los servomotores
void papel(int servo1, int servo2)
{
       
    while(servo1 < 160 || servo2 < 160)
    {
      servomotor1.write(servo1);
      servomotor2.write(servo2);
      delay(25);
      servo1++;
      servo2++;
    } 
    Serial.println(servo1);
    Serial.println(servo2);
    delay(100);
    while(servo1 > 90 || servo2 > 90)
    {
      servomotor1.write(servo1);
      servomotor2.write(servo2);
      delay(25);
      servo1--;
      servo2--;
    }
    Serial.println(servo1);
    Serial.println(servo2);

}
void plastico(int servo1, int servo2)
{
   
     while(servo1 > 20 || servo2 > 20)
    {
      servomotor1.write(servo1);
      servomotor2.write(servo2);
      delay(25);
      servo1--;
      servo2--;
    } 
    delay(100);
    
    while(servo1 < 90 || servo2< 90)
    {
      servomotor1.write(servo1);
      servomotor2.write(servo2);
      delay(25);
      servo1++;
      servo2++;
    }   
}
void organico(int servo1, int servo2)
{
    while(servo1 < 160 || servo2 > 20)
    {
      servomotor1.write(servo1);
      servomotor2.write(servo2);
      delay(25);
      servo1++;
      servo2--;
    } 
     delay(100);
    while(servo1 > 90 && servo2 < 90)
    {
      servomotor1.write(servo1);
      servomotor2.write(servo2);
      delay(25);
      servo1--;
      servo2++;
    }
  
}
void pilas(int servo1, int servo2)
{
    while(servo1 < 20 || servo2 < 160)
    {
      servomotor1.write(servo1);
      servomotor2.write(servo2);
      delay(25);
      servo1--;
      servo2++;
    } 
    
    while(servo1 < 90 && servo2 > 90)
    {
      servomotor1.write(servo1);
      servomotor2.write(servo2);
      delay(25);
      servo1++;
      servo2--;
    }  
}

//inicializacion de variables
void setup() {
  tflite::InitializeTarget();
  Serial.begin(9600);
  //ponemos los pines de los servomotores y del modulo de infrarrojos
  servomotor1.attach(13);
  IrReceiver.begin(IR, DISABLE_LED_FEEDBACK); 
  servomotor2.attach(11);
  
  //escribimos la posicion neutra
  servomotor1.write(90);
  servomotor2.write(90);


  //inicializacion del modelo y comprobacion de que ha cargado correctamente
  model = tflite::GetModel(g_model);
  if (model->version() != TFLITE_SCHEMA_VERSION) {
    MicroPrintf(
        "Model provided is schema version %d not equal "
        "to supported version %d.",
        model->version(), TFLITE_SCHEMA_VERSION);
    return;
  }

  static tflite::MicroMutableOpResolver<7> micro_op_resolver;
  micro_op_resolver.AddResizeBilinear();
  micro_op_resolver.AddAveragePool2D();
  micro_op_resolver.AddConv2D();
  micro_op_resolver.AddDepthwiseConv2D();
  micro_op_resolver.AddReshape();
  micro_op_resolver.AddSoftmax();
  micro_op_resolver.AddResizeNearestNeighbor();


  //creacion del interprete
  static tflite::MicroInterpreter static_interpreter(
    model, micro_op_resolver, tensor_arena, kTensorArenaSize
  );
  interpreter = &static_interpreter;

//comprueba los tensores y escribe errores
  TfLiteStatus allocate_status = interpreter->AllocateTensors();
 if (allocate_status != kTfLiteOk) {
    MicroPrintf("AllocateTensors() failed");
    return;
  }

  //guarda la entrada
  input = interpreter->input(0);


  if ((input->dims->size != 4) || (input->dims->data[0] != 1) ||
      (input->dims->data[1] != kNumRows) ||
      (input->dims->data[2] != kNumCols) ||
      (input->dims->data[3] != kNumChannels) || (input->type != kTfLiteInt8)) {
    MicroPrintf("Bad input tensor parameters in model");
    return;
  }
}

void loop() {

//recoge la imagen y comprueba que no ha habido ningun error
if (kTfLiteOk != GetImage(input)) {
    MicroPrintf("Image capture failed.");
  }

  // Inferencia
  if (kTfLiteOk != interpreter->Invoke()) {
    MicroPrintf("Invoke failed.");
  }


  TfLiteTensor* output = interpreter->output(0);

  // Post-processing
  
  //recogemos los valores de los papeles, plastico, organico y bateria del modelo
  int8_t paper_score = output-> data.uint8[kPaperIndex];
  int8_t plastic_score = output->data.uint8[kPlasticIndex];
  int8_t organic_score = output->data.uint8[kOrganicIndex];
  int8_t battery_score = output->data.uint8[kBatteryIndex];

  //comprobamos cual de todos es mayor y lo ejecutamos
  if(paper_score > plastic_score && paper_score > organic_score && paper_score > battery_score && paper_score > 50)
  papel(90,90);
  else if(plastic_score > paper_score && plastic_score > organic_score && plastic_score > battery_score && plastic_score > 50)
  plastico(90, 90);
  else if(organic_score > paper_score &&organic_score > plastic_score && organic_score > battery_score && organic_score > 50)
  organico(90, 90);
  else if(battery_score > paper_score && battery_score > plastic_score && battery_score > organic_score && battery_score > 50)
  pilas(90, 90);
  else if (IrReceiver.decode()) 
   {   
     Serial.println(IrReceiver.decodedIRData.decodedRawData, HEX);       
    IrReceiver.resume();     
    if(IrReceiver.decodedIRData.decodedRawData == TECLA1)
    {   
      papel(90,90);
    }
    if(IrReceiver.decodedIRData.decodedRawData == TECLA2)
    {
      plastico(90,90);
     
    }
    if(IrReceiver.decodedIRData.decodedRawData == TECLA3)
    {
      organico(90, 90);
    }
    if(IrReceiver.decodedIRData.decodedRawData == TECLA4)
    {
      pilas(90, 90);      
    }
  delay (50);       
  }
}

Problemas y soluciones encontradas

En cuanto a las dificultades, se han encontrado bastantes. En primer lugar, se encontraron dificultades a la hora de plantear el montaje del proyecto. Se pensaron en distintas estructuras, como puede ser una estructura que se fuese moviendo con una plataforma y que debajo de esa plataforma tuviese un determinado objeto pesado y se fuese moviendo alrededor de esa plataforma, pero se concluyó que podría dar diferentes problemas más adelante.

Es por ello que se decidió que el montaje fuese dos plataformas que girasen a la izquierda o derecha (dependiendo del material del objeto) según los cuatro compartimentos.

Tras esta decisión se produjo otro problema, y es que determinados objetos podían llegar a rebotar en la segunda plataforma por sus dimensiones, y podría entrar en una papelera que no fuera la suya. La solución que se encontró a este problema fue que ambas rampas girasen a la vez, de tal forma que es muy difícil que los objetos reboten.

Una vez se decidió el tipo de proyecto, se tuvo que pensar que las rampas no debían de chocar entre ellas, por lo que solo deberían de rotar un cierto grado de ángulo. Se encontró que el servomotor no rotaba correctamente debido al apoyo que tenía en el otro lado, por lo que se reforzó con un tornillo para ayudar a los servomotores a girar, sin que ocasionasen ningún problema. Se creó un agujero detrás de cada servomotor para poder pasar los cables sin ningún problema.

Por otro lado, hubo algunas dificultades con Machine learning. En un principio se utilizó la librería del kit de Machine Learning asociada a un curso de Harvard. Esta librería causaría bastantes problemas al final, en primer lugar, porque la versión de la librería se trataría versión alfa antiguas, por lo que muchas funcionalidades no las podía cumplir. Otro de los problemas que daría es que no estaba actualizada. Dicha librería estaba basada en la librería oficial de TensorFlowLite Micro pero no actualizaba funcionalidad desde 2019. La solución a este problema vino dada una que estaba en el administrador de librerías, sino que estaba en GitHub, por lo que se procedió a descargarla aquí.

Conclusiones y mejoras de cara al futuro

Como conclusiones de esta práctica, podemos destacar el aprendizaje desde 0 de un modelo de machine Learning, así como su despliegue en un sistema empotrado como es el Arduino Nano. Este proyecto se podría ampliar y mejorar a un nivel comercial haciendo la detección de cámara más precisa, usando servomotores con un mayor torque, ampliando los contenedores para que puedan almacenar más residuos, así como mejorando la precisión del modelo empleado.

También te podría gustar...

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *