Regresión lineal con incertidumbre en Arduino

Vamos a empezar este texto develando el truco que usaremos para representar incertidumbre con la regresión lineal para Arduino y que se basa en emplear la versión con pesos del algoritmo de regresión lineal. La incertidumbre estará representada como valores con una variación de pesos según la certeza que tengamos de su valor. Usaremos el peso como porcentaje de certeza de ese dato

Esta no es la mejor ni la única manera de hacerlo. No hay que olvidar que aquí se trata de hacerlo en algo tan limitado en memoria y potencia como pude ser un Arduino UNO.

Usaremos la librería regressino, en concreto su librería para regresión lineal:

#include <LinearRegression.h>

LinearRegression lr = LinearRegression();

Una forma de incertidumbre es cuando directamente tenemos valores de los que “nos fiamos” menos que de otros. Por ejemplo, porque vienen de dos fuentes distintas. En este caso los datos menos fiables tendrán que tener un peso más bajo que los más fiables para que su influencia sobre el resultado final sea menor.

//datos fuente no fiable
lr.learn(1, 3, 0.5);
lr.learn(2, 5, 0.5);
lr.learn(3, 6, 0.5);

//datos fuente fiable
lr.learn(2, 4, 1);
lr.learn(4, 5, 1);
lr.learn(5, 6, 1);

Pero hay otro caso de incertidumbre, cuando no conocemos el valor del dato con seguridad, lo que conocemos son los valores entre los que está comprendido. Generalmente tenemos dos valores, un mínimo y un máximo o tres valores con uno más probable y un mínimo y un máximo (a veces representados como errores) entre los que ese valor puede variar. En este caso tenemos que definir cómo se distribuye el peso (probabilidad) entre estos valores. Hay que recordar que la suma total de los pesos tiene que ser igual a 1.

Una vez definida la forma en que se distribuye la probabilidad hay que descuartizarla en puntos. La idea es que esto funcione en un Arduino UNO y no podemos trabajar directamente con funciones de probabilidad.

Supongamos que para x = 10 sabemos que el valor de y está comprendido entre 2 y 3. Ahora hay que saber cómo está distribuida la probabilidad entre esos dos valores. Veamos algunas posibilidades:

  • Toda la probabilidad se concentra en cada uno de esos valores por lo tanto el 2 tendría un 50% de certeza y el 3 otro 50%. O lo que es lo mismo un peso de 0.5
lr.learn(10, 2, 0.5);
lr.learn(10, 3, 0.5);
  • La probabilidad se distribuye de forma uniforme por todo el espacio entre esos dos puntos. Para representarlo tómanos varios puntos entre 2 y 3 y les asignamos a todos la misma probabilidad.
lr.learn(10, 2, 0.2);
lr.learn(10, 2.25, 0.2);
lr.learn(10, 2.5, 0.2);
lr.learn(10, 2.75, 0.2);
lr.learn(10, 3, 0.2);
  • El punto central es mucho más probable que los extremos. Un ejemplo de como podemos hacerlo.
lr.learn(10, 2, 0.25);
lr.learn(10, 2.5, 0.5);
lr.learn(10, 3, 0.25);
  • Si por ejemplo queremos simular una distribución en campana de media u y con desviación estándar s
lr.learn(10, u, 0.682);
lr.learn(10, u-s, 0.136);
lr.learn(10, u-(2*s), 0.023);
lr.learn(10, u+s, 0.136);
lr.learn(10, u+(2*s), 0.023);

Regresión lineal con pesos en Arduino

Ya hemos visto varias formas de extender las capacidades de la regresión lineal en Arduino. En este caso vamos a asociar pesos a los valores para que no todos los casos aporten los mismo al resultado final. Pero qué significa “aportar” más al resultado, de forma gráfica podríamos imaginarnos que los puntos de mayor peso atraen más a la recta de la regresión lineal por lo que esta tiende a acercarse más a estos. La utilidad de este sistema es cuando tenemos resultados que por algún motivo valoramos más que otros.

Vamos a modelar el peso como un valor entre 0 y 1. De tal forma que el peso máximo corresponda con 1 y el mínimo con 0. A mayor peso más “aporta” ese valor al resultado. Entre dos valores uno con peso 1 y otro con peso 0.5 el primer cuenta el doble que el segundo. Cuando el peso es 1 no hay diferencia con la regresión lineal sin pesos. Y cuando el peso es cero el valor no va a aportar nada al resultado.

Partimos de la función de regresión lineal que ya vimos en otro post:

void LinearRegression::learn(double x, double y){
    n++;
    meanX = meanX + ((x-meanX)/n);
    meanX2 = meanX2 + (((x*x)-meanX2)/n);
    varX = meanX2 - (meanX*meanX);

    meanY = meanY + ((y-meanY)/n);
    meanY2 = meanY2 + (((y*y)-meanY2)/n);
    varY = meanY2 - (meanY*meanY);

    meanXY = meanXY + (((x*y)-meanXY)/n);

    covarXY = meanXY - (meanX*meanY);

    m = covarXY / varX;
    b = meanY-(m*meanX);
}

Como ya hemos visto en otro post vamos a usar “el truco” que consiste en transformar el valor de x e y que se le pasa a la función que calcula la regresión lineal (learn). Podríamos verlo como que el valor de x se usan para modificar el valor de meanX y meanX2 y el de y para meanY y meanY2. Entonces si w es el peso:

  • Si el peso es 1 x e y no cambian su valor
  • Si es 0 x e y no tienen que afectar a los valores de meanX y meanY para que pase eso el valor de x ha de ser igual que meanX y el de y igual a meanY.
  • Si el peso este entre 0 y 1 el valor ha de componerse con el valor de x e y y el valor de las medias de cada uno.

Para conseguir eso vamos a ponderar el valor de la x y el de meanX según w

x = x*w + meanX*(1-w);
y = y*w + meanY*(1-w);

Solo queda añadir un par de comprobaciones para evitar que el valor pueda ser mayor de 1 o menor de 0:

void LinearRegression::learn(double x, double y, double w){
    if(w >= 1) { 
        learn(double x, double y);
    } else if(w <= 0) {
        return;
    } 

    x = x*w + meanX*(1-w);
    y = y*w + meanY*(1-w);
    learn(double x, double y);
}

Todo esto se puede ver en la librería Regressino.

Por último comentar que hay dos posible optimizaciones que se podrían aplicar si fuera necesario y que nos permiten ahorrar algunos cálculos si hay muchos pesos cercanos a 0 o a 1:

  • Los pesos muy cercanos a 0 se ignoran
  • Los pesos muy cercanos a 1 se tratan como si no tuvieran peso


Hay una pequeña pérdida de precisión pero en el caso de tener muchos datos puede compensar al reducir el tiempo de cálculo.

Excepciones y control de errores en Arduino

Uno de los puntos débiles de las placas más básicas de Arduino es la gestión de errores, siguen la política de “nunca me equivoco” de tal forma que acciones o código que en otros sistemas lanzarían error no producen ninguno en Arduino.

Un excepción es un mecanismo en el software que “salta” cuando se produce un error y permite que el software ejecute una rutina de gestión de errores que se ocupa de gestionarlo para después recuperar su funcionamiento normal, si es posible. Arduino no permite usar el mecanismo habitual de C++, las instrucciones try y catch. Es posible usarlas en el código sin que den error al compilar pero durante el proceso de compilación se desactiva su funcionamiento. No es algo que se haga de forma arbitraria, simplemente no funcionan en Arduino. Si embargo vamos a tratar de conseguir un mecanismo similar que nos permite una gestión de errores muy básica . Para ello emplearemos dos funciones (setjmp y longjmp) y un tipo de variable (jmp_buf). esta funciones permiten establecer un punto de salto dentro de una función al que se puede ir desde cualquier parte del programa:

  • setjmp – establece el punto donde se va recuperar la ejecución del programa hay que pasarle como parámetro una variable de tipo jmp_buf.
  • jmp_buf – es un tipo de variable que almacena la información necesaria para restaurar la ejecución del código en el punto indicado por setjmp.
  • longjmp – salta al punto establecido por setjmp. Requiere dos parámetros uno de tipo jmp_buf que indica a que setjmp va a saltar y otro que indica el código de error que va devolver setjmp tras el salto. Este código nunca puede ser 0 si se usa 0 devolverá 1.

Ahora veamos como se usa, para entenderlo bien el código hay que saber los siguientes detalles:

  • Es necesario incluir la librería setjmp.h
  • setjmp la primera vez que se llama se usa para fijar el punto de salto y devuelve el valor cero
  • Cuando se llama a longjmp es como si el programa continuara desde donde se llamo a setjmp pero en lugar de devolver 0 devuelve otro valor
  • Para que esto funcione correctamente setjmp tiene que estar dentro de un if o un switch.
  • Para que todo funcione correctamente setjmp ha de fijar el punto de salto dentro del loop, si lleva a otras funciones puede fallar al llegar al final de la función donde ha saltado

En el caso más simple solo tenemos un tipo de errores y por tanto cada vez que se produce un error la forma de gestionarlo es siempre la misma. Para ello basta con usar un if.

#include <setjmp.h>

jmp_buf exception_mng;
int a = 5;
int b = 1;
int c;

void setup() {
  Serial.begin(9600);
}

void loop() { 
  if (setjmp(exception_mng)) { //si se produce error
		Serial.println("EXCEPTION");
  }
  a--;
  divide();
  delay(1000);    
}

void divide(){
  if(a == 0){
    longjmp(exception_mng, 1);
  }
  c = b/a;  
  Serial.print(b);
  Serial.print("/");
  Serial.print(a);
  Serial.print(" = ");
  Serial.println(c);
}

Una versión más avanzada permite gestionar distintos tipos de incidencias según el valor que se le pase a longjmp como segundo parámetro

#include <setjmp.h>

jmp_buf exception_mng;
int a = 5;
int b = 1;
int c;

void setup() {
  Serial.begin(9600);
}

void loop() { 
  switch (setjmp(exception_mng)) {
	case 0: //sin errores
		break;
  case 1: //division por cero
		Serial.println("EXCEPTION DIVISION BY 0");
    break;
  case 2: //divisor negativo
		Serial.println("EXCEPTION DIVISION BY NEGATIVE NUMBER");
    break;
  default: //se ejecuta cuando no se cumple ninguno de los casos anteriores
		Serial.println("GENERIC EXCEPTION");
    break;

  }
  a--;
  divide();
  delay(1000);    
}

void divide(){
  if(a == 0){
    longjmp(exception_mng, 1);
  }
  if(a < 0){
    longjmp(exception_mng, 2);
  }
  c = b/a;  
  Serial.print(b);
  Serial.print("/");
  Serial.print(a);
  Serial.print(" = ");
  Serial.println(c);
}

Este sistema nos dota de un mecanismo básico de gestión de errores aunque nos obliga a nosotros a realizar la comprobación para lanzar la “excepción”.

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.

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.

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

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
}

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.