Modificar la web embebida de ESP32-Cam CameraWebServer

ESP32-Cam incluye un ejemplo bastante completo de servidor web embebido dentro de la placa. Por si solo puede resultar útil y estaría bien poder modificar la página, personalizarla, quitar opciones o añadir librerias js externas para ampliar su funcionalidad o traducirla. Con solo cambiar la web que incluye el ejemplo tenemos una camada WiFi con un servidor web embebido que nos permite acceder a la misma y sus funcionalidades en remoto. Es una opción muy interesante pero no hay demasiada documentación sobre como hacerlo.

La web se encuentra «escondida» en el código fuente del ejemplo «CameraWebServer» dentro del fichero camera_index.h. La web esta comprimida, para incluirla en el código fuente se ha incluido en forma de un array de bytes (uint8_t) cuyos valores están en hexadecimal.

En le fichero camera_index.h hay dos variables que almacenan la web:

const uint8_t index_ov2640_html_gz[]

const uint8_t index_ov3660_html_gz[]

Que web se usa depende del sensor de la cámara, lo más fácil es probar a reemplazar uno a ver cual devuelve tu placa.

Decodificar la web:

Hay dos maneras de hacerlo, la sencilla y la complicada. La sencilla es tan simple como entrar en la web y click en el botón derecho y «Guardar como…». Tambien se puede usar herramientas como wget o curl.

La complicada requiere ir al código fuente, seleccionar el código hexadecimal del que se compone el array y con él:

  • Eliminar todos los espacios, comas y saltos de linea
  • Convertir cada bloque de texto «0x**» en un número hezadecimal
  • Descomprimir el resultado que esta comprimido unsado gzip

Para facilitar las cosas he usado CybeChef para crear una receta de para decodificar la web la teneis aquí: decodificador . Solo hay que copiar el código hexadecimal que hay en el array y pegarlo en la parte de «input» automáticamente se obtendrá el código dela web en el «output».

Podemos usar esta web como base para nuestros cambios.

Codificar la web:

Una vez programada la web de reemplazo hay que hacer los pasos contrarios:

  • Comprimir la web usando el formato gzip
  • Convertimos cada carácter a bytes en hexadecimal en formato «0X**»
  • Cada carácter hexadecimal tiene que estar separado por una coma, si ademas añadimos un espacio y cada cierto numero de bloques un salto de linea quedara visualmente más atractivo.

También he programado un codificador para facilitar la tarea.

Reiniciar Arduino por software

Hay veces que necesitamos reiniciar la placa Arduino desde el propio software. Por ejemplo cuando se produce un error que no podemos gestionar. Sin embargo Arduino no trae ninguna función «reset» para hacerlos. Si recordamos lo que dijimos del watchdog precisamente esa es su función, reiniciar la placa. Podemos aprovechar eso para hacer una función de reset, configuramos el watchdog con el tiempo más corto posible y entramos en un bucle infinito, obligándolo a que pasado el tiempo reinicie la placa. Basta con llamar a la función reset() y la placa se reiniciara sola.

#include <avr/wdt.h> //libreria del watchdog

void reset(){
  wdt_enable(WDTO_15MS);
  while(1){};
}

Puede ser que hayamos sobrescrito el ISR del watchdog o que no queramos incluir la librería del watchdog solo para hacer un reset. La solución es sencilla, imitar lo que hace el watchdog, llamar a la función en la posición 0 de la tabla de vectores de interrupción. Da igual si no sabes lo que es la traducción es que salta la ejecución del código a la posición 0 de memoria. Podemos usar un puntero a función que apunte a esa dirección.

void(* reset) (void) = 0;

Y luego llamar a esa función.

reset();

Es recomendable usar el primer método siempre que se pueda ya que reiniciar la placa es la función del watchdog y seguro que funciona en todas las placas Arduino, la segunda solución depende más del microcontrolador de la placa y aunque deberia de funcionar las placas con microntroladores AVR no es seguro que funcione con otros microcontroladores.

Este texto mejorado y ampliado forma parte de mi libro sobre como mejorar tus programas en Arduino. Puedes echarle un vistazo aquí.

Puedes ver la versión en vídeo en mi canal:

Haz click para ver el vídeo en mi canal de Youtube

Inicializar los servos en Arduino

Cuando se empieza con Arduino y se hace algún proyecto con servoa se llega el desesperante momento en que se reinicia la placa Arduino y los servos comienzan a dar bandazos hasta que llegan a la posición inicial que les marca el programa. ¿Como resolver esto?

Lo primero que hay que saber es que la mayoría de los ejemplos para aprender a usar la librería servo son incorrectos. No inicializan los servos y por se vuelvan locos. Veamoslo con un ejemplo sacado de la web de Arduino.

/* Sweep
 by BARRAGAN <http://barraganstudio.com>
 This example code is in the public domain.

 modified 8 Nov 2013
 by Scott Fitzgerald
 http://www.arduino.cc/en/Tutorial/Sweep
*/

#include <Servo.h>

Servo myservo; 

int pos = 0;

void setup() {
  myservo.attach(9);  
}

void loop() {
  for (pos = 0; pos <= 180; pos += 1) { 
    myservo.write(pos);
    delay(15);         
  }
  for (pos = 180; pos >= 0; pos -= 1) {
    myservo.write(pos);
    delay(15);  
  }
}

Lo primero es que el servo se vuelve «loco» porque toma los valores por defecto de la librería servo. Se pueden encontrar en el archivo Servo.h

#define MIN_PULSE_WIDTH       544     // the shortest pulse sent to a servo  
#define MAX_PULSE_WIDTH      2400     // the longest pulse sent to a servo 
#define DEFAULT_PULSE_WIDTH  1500     // default pulse width when servo is attached

Veamos lo que significan. Los servos ajustan su posición en base a la duración de un pulso que se les envía. En este caso se ajusta el pulso para que tenga una duración mínima de 544 microsegundos y una duración máxima de 2400 microsegundos. Si estáis acostumbrados a posicionar los servos usando grados de 0 a 180 el valor de 0 grados corresponde con un pulso de duración 544 y 180 con uno de 2400. Para los valores intermedios se interpola el valor, de tal forma que para G grados el pulso tiene que durar P microsegundos:

(G * (MAX_PULSE_WIDTH – MIN_PULSE_WIDTH) / 180 ) + MIN_PULSE_WIDTH = P

Sacando cuentas podemos ver que DEFAULT_PULSE_WIDTH corresponde más o menos con 90 grados.

Lo que ocurre es lo siguiente:

  1. Al ejecutar myservo.attach(9) el servo se mueve hasta los 90 grados
  2. Al ejecutar myservo.write(pos) el servo se mueve a la posición indicada, en este caso 0 grados.

No es que el servo se vuelva «loco» es que le decimos que de esos bandazos.

La solución es cambiar el setup para que antes de ejecutar myservo.attach(9) le ponga el valor con el que queremos inicializar el servo.

void setup() {
  myservo.write(pos); //inicializamos la posicion del servo
  myservo.attach(9);  
}

Conservar el valor anterior del servo

Hay casos en que no tenemos un valor por defecto para un servo, tiene que conservar el valor con el que se quedo antes del reinicio. Como no podemos leer el valor del servo (read/readMicroseconds no leen el valor del servo devuelve el último valor pasado a write/writeMicroseconds). ¿Qué podemos hacer? Usar la EEPROM para almacenar los valores de los servos. Podemos almacenar el valor del servo con EEPROM.put() y recuperarlos con EEPROM.get(). Sin embargo esto tiene un problema, la memoria EEPROM del Arduino tiene una vida de unas 100.000 escrituras, que pueden ser poco para algunos casos. Hay algunas posibles soluciones:

  • Crear una posición de apagado de tal manera que antes de apagarse los servos vuelvan a esa posición. No sirve para fallos/reinicios repentinos.
  • Guardar la posición de los servos cada cierto tiempo. En casos donde los servos cambian rápidamente de posición no sirve.
  • Ir cambiando la dirección de memoria EEPROM donde se almacena el estado de los servos. Si cada byte de EEPROM se puede escribir 100000 veces, si rotamos la dirección de byte a lo largo de toda la memoria alargamos esa vida.
  • Guardar solo la posición si cambia lo suficiente. Se compara el valor almacenado con el valor del servo si la diferencia es pequeña no se cambia.

Veamos un ejemplo del ultimo caso.

Mover un servo.

void moveServo(Servo servo, byte angle, int addressEEPROM) {
    byte savedAngle = EEPROM.read(addressEEPROM, angle);//recuperamos el valor 
    if(abs(angle - savedAngle) > 10){//si más de 10 grados de diferencia se guarda
        EEPROM.update(addressEEPROM, angle);
    }
    servo.write(angle);
}

Se incian los servos con el valor recuparado de memoria.

void initServo(Servo servo, int addressEEPROM, uint8_t pin) {
    int savedAngle;
    EEPROM.get(addressEEPROM, saveAngle);//recuperamos el valor guardado
    servo.write(savedAngle);
    servo.attach(pin);
}

Obtener el código en ensamblador de un sketch de Arduino

Una de las cosas que echaba de menos al programar un Arduino era la posibilidad de ver un desemsamblado del código generado. Hay veces que puede ayudarte a entender por qué falla algo o saciar tu curiosidad de cómo funciona. Aunque el IDE de Arduino no trae una opción cómoda para hacerlo es muy sencillo.

Hay que realizar el proceso en dos pasos:

Obtener el volcado binario del sketch de Arduino

Primero vamos a obtener un volcado binario del sketch. Para ello recurrimos a la opción «Exportar Binarios compilados» del menú «Programa».

Programa -> Exportar Binarios compilados

Una vez exportados podemos acceder a ellos con la opción «Mostrar Carpeta de Programa» del menú «Programa».

Como resultado de la exportación obtenemos dos archivos .hex.

Ficheros binarios obtenidos

Se corresponden al binario con el bootloader de Arduino o sin el bootloader. Podemos abrirlo con un editor hexadecimal y ver que contiene.

Primeras lineas de ejemplo.ino.with_bootloader.standard.hex

El fichero con el bootloader es el que se copia a la placa Arduino cuando «cargamos» el sketch

Convertir el volcado binario a ensamblador

Para realizar esta parte necesitaremos el programa avr-objdump el cual se puede encontrar en el directorio donde este instalado el IDE de Arduino en la ruta hardware\tools\avr\bin\avr-objdump. Una vez localizado podemos usar el siguiente comando para obtener el desensamblado del binario:

<ruta-avr-objdump>avr-objdump -j .sec1 -d -m avr5 fichero.hex > fichero.asm

Tras ejecutar ese comando la salida se vuelca en el fichero de nombre ejemplo.asm que podemos abrir con cualquier editor de textos.

Primeras lineas de ejemplo.asm

Como curiosidad las primeras lineas del fichero corresponde a la tabla de vectores de interrupción del microcontrolador, por eso son todo saltos al código que gestiona cada una de las interrupciones.

Este texto mejorado y ampliado forma parte de mi libro sobre como mejorar tus programas en Arduino. Puedes echarle un vistazo aquí.

Puedes ver todo este proceso en vídeo en mi canal de youtube:

Haz click para ver el vídeo en Youtube

Watchdog en Arduino

Un watchdog es un sistema de seguridad que usan muchos microcontroladores y sistemas embebidos. Su funcionamiento es simple, cada vez que se reinicia el watchdog empieza una cuenta atrás hasta cero, si antes de llegar a cero el watchdog se reinicia la cuenta atrás vuelve a empezar. Si no se reinicia y el contador alcanza el valor 0 el sistema se reinicia automáticamente. Así se evita que se quede colgado.

Con el reinicio se pierden los datos que están en la memoria SRAM por lo que todo dato que queramos conservar ha de guardarse en la EEPROM.

Para usar el watchdog hemos de incluir la librería del fabricante del microcontrolador:

#include <avr/wdt.h>

Para deshabilitar el watchdog se puede usar la función:

wdt_disable();

Empezamos aprendiendo a deshabilitarlo porque es lo primero que hay que hacer para evitar problemas durante el inicio de la placa o puede interferir en el proceso de grabación de un nuevo código en la placa. Cuando se vuelca un nuevo código hay que dar tiempo para que se pueda volcar un nuevo programa o se perderá la posibilidad de grabar nuevos programas. La causa de esto es que cuando se empieza a subir un nuevo código desde el IDE se paraliza la ejecución del programa, por lo que no se reinicia el contador del watchdog si este llega a cero antes de que se termine de subir el código la placa se reinicia y la grabación se corta.

Para activar el watchdog y fijar cuanto tiempo tiene que esperar el watchdog antes de reiniciar la placa se usa la función:

wdt_enable(WDTO_1S);

El parámetro que recibe la función indica el tiempo que esperará el watchdog antes de reiniciar el dispositivo. No puede tomar cualquier valor, tiene que ser una de los siguientes valores predefinidos:

  • WDTO_15MS
  • WDTO_30MS
  • WDTO_60MS
  • WDTO_120MS
  • WDTO_250MS
  • WDTO_500MS
  • WDTO_1S
  • WDTO_2S
  • WDTO_4S
  • WDTO_8S

Para evitar que se reinicie la placa hemos de llamar a la siguiente función ante de que pase el tiempo indicado:

wdt_reset();

Tras llamarla la cuenta atrás comienza otra vez.

Un ejemplo de esqueleto de función seria la siguiente:

#include <avr/wdt.h>

setup(){
  wdt_disable();
  //el resto del setup
  delay(3000); //para evitar que no nos da problemas al cargar nuevos programas
  wdt_enable(WDTO_4S); //se activa el watchdog 
}

loop(){
 wdt_reset(); //reiniciar contador del watchdog
 //código del programa
}

Este texto mejorado y ampliado forma parte de mi libro sobre como mejorar tus programas en Arduino. Puedes echarle un vistazo aquí.

También puedes verla versión en vídeo en mi canal de youtube:

Haz click para ver el vídeo en mi canal de youtube

Regresión lineal segmentada en Arduino

Vamos a seguir viendo «trucos» para aprovechar la regresión lineal. La principal limitación de la regresión lineal es precisamente que es «lineal». Ya hemos visto que se puede utilizar la regresión lineal para calcular otro tipo de regresiones. Ahora vamos a ver como aproximar formas mas complicadas.

Cualquier curva se puede aproximar usando segmentos de linea recta. A mayor número de segmentos mejor aproximación. Por ejemplo debajo podemos ver la aproximación a una curva con forma de campana.

Se puede consguir mejor resultado si cada segmento empieza en el mismo punto que termina el anterior, pero la imagen es más realista respecto al caso que vamos a ver: Usar la regresión lineal para calcular cada uno de esos segmentos de forma independiente.

La forma de trabajar es muy sencilla, dividimos el espacio en varias partes para cada una de las cuales calculamos una regresión lineal. Luego cuando queremos estimar un valor lo primero es ver a que segmento corresponde y usar esa regresión para calcular su valor.

Para elegir como dividir la regresión hay varias opciones:

  • Dividir en segmentos iguales, es la forma más sencilla y entendible. Tiene el problema de que te puedes encontrar huecos sin datos.
  • Dividir cuando haya datos suficientes para asegurarse la calidad del aprendizaje. Se toman segmentos de longitud variable, el unico requisito es que haya suficiente punto de aprendizaje en ese segmento para asegurarse de que hay datos, si no los hay se alarga el segmento hasta que los haya.
  • Se empieza con una solo regresión, luego se divide en dos y se compara el error cuadrático medio entre ambos, si es menor en la version de mas segmentos se repite la operación

Este último punto no muestra algo interesante. Si necesitas comparar que combinación de segmentos es la mejor opción se puede usar el error cuadrático medio. Aquí puedes ver como calcularlo.

Ejemplo

Vamos a usar la librería Regressino en concreto nos vamos a basar en uno de sus ejemplos.

Lo primero es incluir la librería correspondiente y declarar una regresión lineal para cada segmento que queremos crear (en este caso 3)

#include <LinearRegression.h>

LinearRegression lr1 = LinearRegression();
LinearRegression lr2 = LinearRegression();
LinearRegression lr3 = LinearRegression();

El primer segmento va de X = 1 a X = 10, el segundo de X = 11 a X = 20 y el tercero de X = 21 a X = 30.

    Serial.println("Start learn");
    //1-10
    lr1.learn(1,2);  
    lr1.learn(2,3);
    lr1.learn(3,4);
    lr1.learn(6,7);
    lr1.learn(8,9);

    //11-20
    lr2.learn(11,24);  
    lr2.learn(12,26);
    lr2.learn(13,28);
    lr2.learn(16,34);
    lr2.learn(18,38);

    //21-30
    lr3.learn(21,66);  
    lr3.learn(22,69);
    lr3.learn(23,72);
    lr3.learn(26,81);
    lr3.learn(28,87);
    Serial.println("End learn");

Para calcular la estimaciones hay que tener en cuenta esos límites para saber que regresión lineal usar:


    for(int i = 0; i < 31; i++){
      Serial.print("Result (");
      Serial.print(i);
      Serial.print("): ");
      if(i < 11){
        Serial.println(lr1.calculate(i));    
      } else if(i < 21){
        Serial.println(lr2.calculate(i));
      } else {
        Serial.println(lr3.calculate(i));
      }
    }

Como podemos ver es un proceso muy sencillo pero no todo son ventajas.

Problemas

  • Puede dar lugar discontinuidades y saltos bruscos en los puntos donde se produce un cambio de segmento
  • Una de las limitaciones es que para cada valor de X solo puede existir un valor Y. Las rectas calculadas en cada segmento no pueden solaparse en ningun punto.
  • Lo contrario si que puede producirse, que varios valores de X tengan el mismo valor de Y
  • Necesitas tener valores de muestra en todos los segmentos. Si divides los datos en cinco segmentos, te tienes que asegurar de que tienes muestras suficientes para que el algoritmo de regresión calcule una buena aproximación en cada uno de ellos.

Ideas para explorar

  • Hemos usado el algoritmo de regresión lineal para cada segmento, pero podria usarse algún otro de los que hemos visto, incluso distintos en cada segmento.
  • Si hay un hueco sin datos podemos estimar la ecuación del segmento Y = m*X + c usando el punto final de segmento anterior (X1, Y1) y el punto inicial del segmento siguiente (X2, Y2). Para ello m = (Y2-Y1)/(X2-X1) una vez calculada la m podemos calcular la c = Y1 – m*X1. No es una solución ideal y seguramente resulte en una mala aproximación
  • Otra opción pra calcularlo es realizar el aprendizaje con algunos de los puntos del final del segmento anterior y de algunos del inicio del segmento siguiente.
  • Esa misma idea se puede usar para tratar de mejorar el aprendizaje permitiendo que el inicio y final de cada segmento se solapen con los segmento anterior y siguiente.

Calcular la media aritmética, media geométrica, media armónica y media cuadrática en Arduino

Vamos a ver como implementar más funcione estadísticas en un entorno tan limitado como Arduino. Para ello necesitamos usar formas acumulativas de cálculo. En este caso acumulativas se refiere a que no tengan que calcularse de nuevo todos los valores cada vez que se añade uno nuevo, esto nos ahorra gran cantidad de cálculos y de espacio en memoria.

Media aritmética

Es o que normalmente llamamos «media». Corresponde con la suma de cada uno de los valores de muestra dividido entre el numero de valores:

\frac{1}{n} \sum_{} x

Ya la vimos como calcularla de forma acumulativa, vamos a recordarlo rápidamente:

mean = mean + (x-mean)/n);

Media geométrica

Es la raiz enesima del producto de cada uno de los valores:

\sqrt[n]{\prod_{} x}

Vamos a desarrollar nuestro cálculo acumulativo a partir del modelo acumulativo para calcularla que desarrollan en este articulo.

Resumiendo, calculamos la media de ln(x) usando la formula de la media acumulada vista antes:

meanLn = meanLn + ((log(x)-meanLn)/n);

Para calcula la media geométrica a partir de este valor vasta con elevar el numero e al valor calculado:

Necesitaras declarar el número e:

const double e=2.71828;

Media armónica

Se calcula dividiendo el numero de muestras entre el sumatorio de uno partido por el valor de cada muestra. (Si no te has enterado, tranquilo, no me he enterado ni yo y soy el que lo ha escrito). Se ve mejor con la fórmula:

\frac{n}{\sum_{} 1/x}

En lugar de usar la versión acumulativa vamos a optar por aprovecharnos de la relación entre las distintas medias:

harmonica = \frac{geometrica^2}{aritmetica}

En código:

harmonicMean =  pow(geometricMean(), 2)/mean();

Media Cuadrática

Es la raíz cuadra del sumatorio del cuadrado de los valores:

\sqrt{\frac{1}{n} \sum_{} x^2}

Para calcularlo usamos la misma formula que para la media aritmética pero aplicada al cuadrado del valor:

mean2 = mean2 + (((x*x)-mean2)/n);

Luego para obtener el valor final solo hemos de calcular la raíz cuadrada de la misma:

sqrt(mean2):

Puede encontrar el código de la implementación de todo esto en este proyecto de github.

Error medio absoluto y error cuadrático medio en Arduino

El error cuadrático y el R cuadrado se usan como medidas para evaluar el desempeño de un estimador. Es decir cual es el error que comete al estimar un valor. Un ejemplo de estimador seria, por ejemplo, una regresión, ahora si no interesa saber lo bien que estima esa función podemos hacerlo a partir de direrente estimadores.

Para calcularlos se usan dos valores, el valor real y el valor devuelto por nuestro estimador (representado por la Y con «sombrerito»). El error para cada caso es el valor absoluto de la resta de ambos valores:

Error = |\hat{Y_{i}}-Y_{i}|

Se puede entender intuitivamente de forma muy simple, es la diferencia entre el valor que obtenido del estimador y el valor real. Se usa el valor absoluto para que cuando se sumen varios errores no se «cancelen». Si al estimar un valor se equivoca en 3 y al estimar otro en -3 el error total es 6 no 0.

Su calculo en Arduino seria:

double error = abs(y - ey);

Error medio absoluto

No podemos valorar un estimador solo por el error en una estimación, habrá que usar varias y calcular la media del error a lo que se le llama «error medio absoluto«, la suma del error de cada medida partido por el número de muestras:

MAE = \frac{1}{n} * \sum _{i=1}^{n}|\hat{Y_{i}}-Y_{i}|

Para implementarlo en Arduino vamos a usar el mismo «truco» que usamos para calcular diferentes estadisticos en Arduino. Usando la versión acumulada del calculo de la media para no tener que guardar todos los valores:

meanError = meanError + ((error - meanError)/n);

Error cuadrático medio

Otro valor usado es la media del error al cuadrado:

MSE = \frac{1}{n} * \sum _{i=1}^{n}(\hat{Y_{i}}-Y_{i})^{2}

Para implmentarla en Arduino vamos a usar el mismo «truco» que antes para el error medio:

 meanError2 = meanError2 + (((error*error) - meanError2)/n);

El problema de elevar los valores al cuadrado es que cuantificar su diferencia resulta poco intuitivo. Para hacer el valor más entendible se puede usar la raiz cuadrada del error cuadrático medio:

RMSE = \sqrt{MSE}

En código para Arduino:

sqrt(meanError2);

Todo esto lo puedes ver implementado en los ficheros error.h y error.cpp de SimpleStatisticsArduino

De regresión lineal a regresión logística en Arduino

Ya hemos visto como calcular la regresión lineal en Arduino y como a partir de esta calcular diversos tipos de regresiones. Lo que vamos a ver aquí es usar un truco para convertir la regresión lineal en regresión logística basandonos en la función sigmoide.

La regresión lineal se usa como clasificador binario entre dos conjuntos. En el caso ideal de regresion logística para cualqueir valor de x devuelve un valor de y que es 0 o 1 dependiendo de a que clase pertenezca. Pero en la vida real rara vez suele ser un «caso ideal» y hay valores para los que devolverá un valor comprendido entre 0 y 1. Este resultado puede interpretarse como la probabilidad de que sea del grupo representado por el valor 1 o cuadno esto carezca de sentido simplemente tomar cualqueir valor mayor de 0,5 como del grupo del 1 y cualquie valor por debajo como del grupo del 0.

La función sigmoide se define como:

1 / 1 + e^{-y}

El valor de y lo podemos sacar de la regresión lineal:

y = mx +b

Juntandolo todo:

1 / 1 + e^{-(mx+b)}

Veamos las diferencias entre ambas fórmulas:

Regresión lineal (verde) comparada con regresión logística (naranja)

Regresión Lineal:

  • Su fórmula define una linea
  • No está acotada, no tiene un valor máximo ni mínimo
  • Se usa para estimar valores.
  • Devuelve un valor numérico

Regresión logística:

  • Su fórmula define una "S"
  • Esta acotada entre 1 y 0
  • Se usa para clasificar un valor en uno de dos grupos. Clasificador binario.
  • El resultado que devuelve se puede interpretar de dos maneras: como probabilidad de pertenecer a un grupo si se toma el valor directamente o como pertenencia absoluta a un grupo u otro si se considera que cuando el valor obtenido este por encima de 0,5 se pertenece a uno y por debajo al otro.

Forma de implementarlo

La forma de implementar esto en un Arduino es aprovechar la librería que ya tenemos de regresión lineal y que nos soluciona los problemas de memoria y tiempo de cálculo que tienen los cálculos estadísticos en Arduino. Simplemente una vez que nuestro sistema aprenda el modelo lineal basta con transformar el resultado que devuelve este modelo para convertir su respuesta a la de una regresión logística.

    double exp = linealRegression.calculate(x)*-1; 
    return 1/1+pow(e, exp);

La implementación del código se puede encontrar en la librería Regressino

Utilidad

¿Tiene sentido transformar una regresión lineal en un modelo de regresión logística?. Aunque esta conversión se puede realizar para cualquier regresión lineal no tiene sentido hacerlo. Solo tiene sentido usarlos cuando se quiera entrenar un clasificador binario y haya dos grupos de elementos claramente diferenciables. Entonces se puede calcular la recta de regresión y convertirla en una regresión logística que funcione como clasificador.

Tampoco va servir para calsificar cualquier grupo de elementos, han de ser linealmente separables. dicho de forma más intuitiva, tienen que poder separarse trazando una linea recta entre ellos.

En definitiva, sin ser una opción ideal, es suficiente buena y útil como para plantearse su uso.

Estadísticas básicas en Arduino

Como ya vimos en el post sobre regresión lineal en Arduino, el principal problema que plantea Arduino para realizar cálculos estadísticos es la escasa capacidad de memoria y cálculo que tiene. Para ello en lugar de guardar todos los datos vamos a usar formulas que permiten aproximar los valores estadísticos que vamos a utilizar sin gastar casi recursos, la idea es guardar solo una aproximación.

En el siguiente enlace puedes encontrar la librería SimpleStatisticsArduino de Arduino que implenta lo explicado en este texto.

Para la varianza y la media usaremos las siguientes formulas que tratan de aproximar

numeroDeMuestras++;
media += (nuevoValor – media)/numeroDeMuestras;
media2 += (value^2 – media2)/numeroDeMuestras;
varianza = media2 – media^2;

Con estos valores podemos aproximar la suma de todas los datos:

suma = media*numeroDeMuestras;

La desviación estándar :

desviacionEstandar = sqrt(varianza);

Otros dos valores que podemos almacenar de forma muy sencilla y casi sin costes es el valor mínimo y máximo. Cada nuevo valor se comprueba:

if(minimo > nuevoValor){
minimo = nuevoValor;
}
if(maximo < nuevoValor){
maximo = nuevoValor;
}

Ahora con estos dos valores podemos calcular el valor central, que no es lo mismo que la media:

centro = (maximo – minimo) / 2;

Con esta estrategia solo necesitamos 6 variables para almacenar los datos sobre los que se calcula la estadística.

Estadística con dos variables en Arduino

Tenemos dos variables X e Y, partiendo de los cálculos del apartado anterior para cada una ahora podemos calcular los valores conjuntos, para ello debemos de almacenar dos variables más necesarias para calcular la covarianza:

mediaXY += ((XY)-mediaXY)/numeroDeMuestras;
covarianza = mediaXY – (mediaXmediaY);

Ahora con la covarianza podemos calcular la correlación:

correlacion = covarianza / (desviacionEstandarX * desviacionEstandarY);

Con estos datos podemos calcular los parámetros de la regresión lineal:

m = covarianza / varianzaX;
b = mediaY – m*mediaX;

Y la propia regresión:

y = m*x + b;

Si buscas una implementación de la idea de este post pero optimizada exclusivamente para la regresión lineal puede mirar la librería Regressino.

Por último podemos calcular el centroide que no es nada mas que el centro de cada una de las variables X e Y.

centroide = [centroX, centroY]

De esta forma se pueden calcular bastantes valores sin consumir casi memoria o recursos