Parar la ejecución o finalizar un programa en Arduino

Puede ser que sea lleguemos a un punto en que por seguridad sea necesario parar la ejecución del programa o que simplemente deseemos “apagar” la placa. Para ello se puede llamar a las funciones abort() o exit(0). Que es prácticamente lo mismo ya que abort lo único que hace es llamar a exit(1). Esto tiene sentido en aplicaciones de ordenador donde exit(0) indica la finalización del programa correctamente mientras que cualquier parámetro distinto de 0 indica que ocurrió un error. En Arduino ese valor que se pasa es ignorado, aunque tenemos que añadirlo si no queremos errores de compilación.

Puesta en forma de código la función exit seria similar a esta:

void exit(int ignored){
  cli(); //desactiva las interrupciones
  while(1); //bucle infinito
}

Básicamente suspende la interrupciones y luego entra en un bucle infinito. Si por algún motivo queremos hacer nuestra propia función de stop es fácil imitarla:

void stop(){
  noInterrupts();
  while(1);
}

En caso de que se use el watchdog es necesario deshabilitarlo o reiniciará la placa.

void stop(){
  wdt_disable(); //solo si esta activado el watchdog
  noInterrupts();
  while(1);
}

Las funciones anteriores son recomendables para casos en que la parada se produce por un error grave y donde menos acciones se realicen mejor. Sin embargo si queremos “apagar” la placa podemos dormirla profundamente, no llegara al consumo cero pero si al mínimo.

#include <avr/sleep.h>

void stop(){
  set_sleep_mode( SLEEP_MODE_PWR_DOWN );
  sleep_enable();
  noInterrupts(); //nunca será despertada
  sleep_cpu();
}

Por supuesto en todos estos casos basta con dejar la placa sin alimentación para poder volver a usarla.

Usar el led de las placa Arduino. LED_BUILTIN

Muchas placas de Arduino tienen un pin digital conectado a un led de la propia placa de tal forma que se puede controlar ese pin desde el código. En la placa el led suele ir etiquetado con una “L”. En Arduino UNO es el pin digital 13. Es una forma cómoda de tener un led de señalización sin tener que montar nada. No es necesario saber que pin es, para ello se puede usar la constante LED_BUILTIN que hace referencia a ese pin.

Para poder usarlo lo primero es estar seguro que no se usa ese pin para otra cosa. Es necesario configurarlo como OUTPUT. Basta con poner el pin a HIGH para encenderlo y a LOW para apagarlo.

Aunque es algo muy rudimentario se puede usar para indicar estados del software o errores. Y puede ser útil cuando no podemos tener el ordenador con el monitor serie conectado todo el tiempo al Arduino.

Su uso puede ser tan simple como encenderlo o llegar a cosas más complicadas como usar parpadeos para indicar distintos errores.

En este ejemplo la función blink hace parpadear el led las veces que se le pasa como parámetro

void setup(){
   pinMode(LED_BUILTIN, OUTPUT);
}

void loop(){
  blink(1);
  delay(2000);
  blink(2);
  delay(2000);
  blink(3);
  delay(2000);
}

void blink(byte n){
  for(; n > 0; n--){
    digitalWrite(LED_BUILTIN, HIGH);//Encender led
    delay(500);
    digitalWrite(LED_BUILTIN, LOW);//Apagar led
    delay(500);
  } 
}

Normalizar un valor entre 0 y 1en Arduino.

Cuando quieres comparar distintos datos en distintos rangos es habitual el tener que normalizarlos entre 0 y 1, es decir, convertir una variable de un rango de valores [in_min, in_max] a otro que va de 0 a 1. Para ello podemos usar la función map.

map(x, in_min, in_max, 0.0, 1.0);

En esta otra entrada de blog hay más información sobre la función map y como optimizarla, vamos a usar esos trucos para crear funciones de normalización más optimas.

Lo primero es que como los valores sobre los que se “mapea” son siempre los mismos 0 y 1 podemos crear nuestra propia función a partir del código de la función map.

long map(long x, long in_min, long in_max, long out_min, long out_max) {
    return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}

Se reemplaza out_min por 0 y out_max por 1 y se simplifican los cálculos:

long map(long x, long in_min, long in_max, long out_min, long out_max) {
    return (x - in_min) * (1 - 0) / (in_max - in_min) + 0;
}

Obteniendo:

long normalize(long x, long in_min, long in_max) {
    return (x - in_min) / (in_max - in_min);
}

Vamos a crear también funciones para los float y los double:

float floatNormalize(float x, float in_min, float in_max) {
    return (x - in_min) / (in_max - in_min);
}

double doubleNormalize(double x, double in_min, double in_max) {
    return (x - in_min) / (in_max - in_min);
}

Genial, pero aun podemos hacer otra mejora, la función anterior es genérica, si vamos a aplicar la normalización siempre a valores en el mismo rango podemos crear una nueva función donde remplacemos in_min e in_max por los valores de este rango. Por ejemplo en un rango de 0 a 1023:

//in: 0..1023
//in_min = 0
//in_max = 1023

long customNormalize(long x){
    return (x - 0) / (1023 - 0);
}

Simplificando:

long customNormalize(long x){
    return x / 1023;
}

Hemos reducido el coste de la normalización a una solo división, en el peor de los casos (cuando el in_min no sea 0) será una resta y una división.

Transformar un valor de una escala a otra en Arduino. Map

Hay veces que tenemos un valor expresado en una escala y queremos pasarla a otra. Por ejemplo uno de los casos más habituales es el calcular un porcentaje. Que pasamos un número expresado en una escala a otra que va de 0 a 100. En Arduino para pasar un valor de un rango o escala [in_min, in_max] a otro [out_min, out_max] podemos usar la función map.

long map(long x, long in_min, long in_max, long out_min, long out_max)

La función map toma 5 parámetros:

  • x: Valor a transformar
  • in_min: Valor más bajo de la escala origen
  • in_max: Valor más alto de la escala origen
  • out_min: Valor más bajo de la escala destino
  • out_max: Valor más alto de la escala destino.

La función tiene la siguiente implementación interna:

long map(long x, long in_min, long in_max, long out_min, long out_max) { 
    return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; 
}

Os habréis dado cuenta de que trabajo con long ¿Qué pasa si queremos otro tipo de datos?. Muy fácil, nos hacemos nosotros mismos la función:

float floatMap(float x, float in_min, float in_max, float out_min, float out_max) { 
    return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; 
}


double doubleMap(double x, double in_min, double in_max, double out_min, double out_max) { 
    return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; 
}

Optimizar la función map

Ya que nos hemos puesto a crear nuestras funciones veamos como podemos optimizar los cálculos, si siempre tenemos que usar la función map con los mismos rango de números podemos crear nuestra versión personalizarla para que se ejecute más rápido. Es tan sencillo como sustituir los valores min y max de cada rango y simplificar. Veamoslo con un ejemplo, vamos a crearnos nuestro propio map que transforme de la escala de 0 a 1023 a otra escala de 0 a 255.

in: 0..1023
out: 0..255
int_min = 0
int_max = 1023
out_min = 0
out_max = 255

Reemplazamos:

long customMap(long x) { 
    return ((x - 0) * (255 - 0) / (1023 - 0)) + 0; 
}

Simplificamos donde podamos:

long customMap(long x) { 
    return (x * 255) / 1023; 
}

Ahora tenemos nuestra función personalizada que realiza solo dos operaciones. Como no es necesario pesarle los rangos usa solo un parámetro.

Lanzar tareas en una web cuando el navegador está desocupado. RequestIdleCallback

Un problema típico cuando desarrollas webs con mucho javascript es tener que ejecutar ciertas funciones (precarga, envío de estadísticas, actualizar los datos almacenados en local,…) pero que esta ejecución ocurra cuando la carga de la web haya terminado. Y con carga no me refiero únicamente a que haya terminado de cargar el documentos, si no de inicia izar todas las librerías para no afectar al rendimiento de la web. Para ello existe una solución en los navegadores: RequestIdleCallback

RequestIdleCallback lo que hace es encolar las funciones que se le pasan y llamarlas cuando el el navegador tenga poca carga de trabajo, así se mejora el rendimiento de la web y la experiencia de usuario. Su uso es muy sencillo:

requestIdleCallback(nonEssentialFunction);

Donde nonEssentialFunction es la función a la que se llamará cuando el navegador quede inactivo. La función recibe un parámetro un objeto deadline que tiene dos propiedades que podemos usar para planificar la ejecución de nuestro código:

  • timeRemaining(), una función que devuelve cuanto tiempo tenemos para ejecutar nuestro código.
  • didTimeout, que indica si el tiempo fijado como limite para ejecutar nuestra función (más adelante lo veremos) ha expirado. Si su valor es true, timeRemaining() devolverá 0.

Es decir que consultando deadline.timeRemaining() podemos saber si tenemos tiempo suficiente para ejecutar nuestro código. Hay que decir que es un valor orientativo y que si realmente nuestro código tarda más tiempo en terminar no pasara nada.

Veamos el ejemplo más simple, ejecutar una solo función:

function nonEssentialFunction(deadline) {
  task();
}

Si tenemos que realizar varias tareas podemos ir haciéndolas de una en una hasta que se termine el tiempo. na vez terminado el tiempo no se han acabado todas las tareas se puede volver a llamar a requestIdleCallback:

function nonEssentialFunction(deadline) {
    while (deadline.timeRemaining() > 0){ //mientras quede tiempo
        task(n); //ejecutar tarea n
        n--;
    }

    if(n > 0) { //si aun quedan tareas se encola la función otra vez
        requestIdleCallback(nonEssentialFunction);
    }
}

Veamos un ejemplo completo:

<html>
<script>
    function nonEssentialFunction(deadline) {
        while (deadline.timeRemaining() > 0)
            console.log(deadline.timeRemaining());
    }

    //Se prepara la función cuando este libre el procesador
    requestIdleCallback(nonEssentialFunction);
</script>

<body>
</body>

<script>
    //esto se ejecuta antes que nonEssentialFunction
    for(let i = 0; i < 100; i++){
        console.log(i);
    }    
</script>
</html>

Poner un limite de tiempo

Puede ser que no queramos que la función que queremos llamar se retrase demasiado, para ellos podemos pasar otro parámetro que establece el tiempo de expiación, pasado ese tiempo se lanza la ejecución de la función:

requestIdleCallback(nonEssentialFunction, { timeout: 2000 });

El tiempo viene expresado en milisegundos.

Cuando una función es ejecuta porque expira el tiempo fijado en timeout se puede ver en que deadline.didTimeout es true.

function nonEssentialFunction(deadline) {
    while (deadline.timeRemaining() > 0 || deadline.didTimeout )
        console.log(deadline.timeRemaining());
}


requestIdleCallback(nonEssentialFunction, {timeout, 3000});

Como saber si un elemento HTML se ve en pantalla en una web con scroll. IntersectionObserver

Todos los que llevamos un tiempo programando webs nos hemos tenido que enfrentar al scroll. ¿Qué vel el usuario en su pantalla? ¿Por que punto del div (párrafo, imagen,…) está?. Muchas veces la solución es un enrevesado conjunto de cálculos. Por suerte los navegadores han implementado un solución en el javascripr que permite hacerlo fácilmente: IntersectionObserver

Explicado de forma muy resumida, IntersectionObserver invoca a una función cuando el elemento html que le indiquemos se muestre en la pantalla en distintos porcentajes.

Primero veamos como se crea un IntersectionObserver:

let observer = new IntersectionObserver(handleIntersect, options);
observer.observe(element);

handleIntersect es la función a la que llamara el observer. Se le pasa un objeto entries que es un array de objetos entry correspondiente a vrios eventos de intersección asociados (por lo general es solo uno). Cada uno de los objetos entry tiene las siguientes propiedades:

entry.boundingClientRect
entry.intersectionRatio
entry.intersectionRect
entry.isIntersecting
entry.rootBounds
entry.target
entry.time

option es un objeto con tres posibles campos:

  • root: Indica que elemento que es usado como viewport para comprobar la visibilidad de nuestro elemento. Debe ser ancestro del nuestro. Por defecto es el viewport del navegador.
  • rootMargin: Delimita los margenes del elemento root que se ha de tener en cuanta al calcular la intersecciones. Por defecto son cero.
  • threshold: Es un número o un array que indican a que porcentaje de visibilidad se llama a la función asociada al observer. Su valor debe estar definido entre 0 (0%) y 1 (100%)

element es el elemento que el observer “estará vigilando”

Veamos un ejemplo completo:

<html>
<style>
    p {
        font-size: 120px;
        border: 2px solid black;    
    }
</style>

<body>
    <p id="p0">0</p>
    <p id="p1">1</p>
    <p id="p2">2</p>
    <p id="p3">3</p>
    <p id="p4">4</p>
    <p id="p5">5</p>
    <p id="p6">6</p>
    <p id="p7">7</p>
    <p id="p8">8</p>
    <p id="p9">9</p>    
</body>

<script>
    let element = document.getElementById("p5");

    let options = {
        root: null, //document.getElementById()
        rootMargin: '0px',
        threshold: [0, 0.5, 1.0]
    }

    let observer = new IntersectionObserver(handleIntersect, options);
    observer.observe(element);

    function handleIntersect(entries){
        entries.forEach(entry => {
            console.log("Elemento con id "+entry.target.id+"se muestra en pantalla en un porcentaje de: "+(entry.intersectionRatio*100));
        });
    }
</script>
</html>

Primeros pasos SDR. ¿Qué puedo hacer?

Acabas de comprar tu primer equipo de SDR que posiblemente sea un decodificador de TDT (DVB-T) con un chip RTL2832 o similar conectado por USB. ¿Ahora que?

Vamos a revisar rápidamente que herramientas hay que aprender a usar con tu equipo SDR.

Comprobar que todo funciona

Lo primero es comprobar si esta correctamente conectado y las capacidades de tu dispositivo. Para ello podemos usar:

rtl_test

La salida de la aplicación te muestra los datos de los sintonizadores encontrados.

Explorar el espectro de radio

Para ello puedes usar la aplicación gqrx que te permite explorar todo el espectro de radio.

Lo primero que puedes hacer es buscar una lista de emisoras de radio, sintonizar su frecuencia y probar a escucharlas. Puedes ver cómo afecta cambiar el ancho de banda o el modo de decodificación. Luego con cierta práctica puedes ir recorriendo el espectro a ver qué “escuchas”. Por ejemplo puedes buscar en que frecuencia trabajan algunos equipos inalámbricos y analógicos para ver si alguien cerca usa alguno.

Cuando tengas más soltura y conocimiento del tema gqrx puede quedarse corto para ti y puede usar algun software con más opciones como SDR++

Otra herramienta imprescindible que hay que saber usar es rtl_sdr que te permite capturar la señal y guardarla en un fichero. Muchas herramientas trabajan con estas capturas.

rtl_sdr -f frecuencia -g ganancia -s samplerate -n numero_de_muestras archivo

También hay que saber usar rt_power que captura el espectro de la señal.

Vamos a “cazar” aviones. ADS-B

Otra cosa que podemos hacer es “espiar” que aviones pasan por encima nuestro. Para ellos podemos escuchar la señales del sistema ADS-B que los aviones emiten. Para leer estos datos puedes usar el comando:

rtl_adsb

Así lo único que consigues es montón de caracteres alfanuméricos con el parámetro -V la información se muestra de una forma más amigable

rtl_adsb -V

Hay que decir que no solo los aviones emiten este tipo de señales,

El resultado es algo más entendible aunque es difícil saber que significa. Aquí puedes tener una pista de que significa cada cosa. Haciendo un pequeño resumen.

El campo Type Code indica el tipo de mensaje:

Type CodeTipo de mensjae
1-4Identificación de la aeronave
5-8Posición
9-18Altitud (altimetro)
19Velocidad
20-22Altitud (GNSS)
23-27Reservado
28Estado de la aeronave
29Información de estado y estado de destino
31Estado operacional

El campo ICAO Address puede ayudarnos a identificar el avión y por tanto el vuelo. Para ello s epeuden suar webs como flightradar24.

Si lo que quiere es tener la información más clara puedes usar dump1090 (o dump109-mutability en el caso de mi distribución de Linux). Basta con lanzar lo con la opción –interactive y procesara esos datos por ti y te los mostrará de un forma más sencilla de entender.

dump1090 --interactive
dump1090-mutability --interactive

Es posible que te ocurra que rtl_adsb muestre un montón de resultados pero dump1090 no muestre ningún avión, se puede deber a estaciones emisoras en tierra. A menos que vivas cerca de un aeropuerto de una gran ciudad hay que tener paciencia para captar los datos de un avión.

Internet of Thing

Pasemos a capturar y decodificar mensajes de IoT. Para ellos puedes probar con el programa rtl_433.

rtl_433

Los datos son devueltos de una manera muy intuitiva donde cada paquete es decodificado y mostrado en pantalla.

Por defecto escucha la frecuencia 433 MHz pero usando el parámetro -f puedes probar con diferentes frecuencias a ver qué detectas, para probar puedes empezar con 315 MHz, 345 MHz, 868 MHz o 915 MHz. Por ejemplo:

rtl_433 -F F868M

Si deseas volcar los datos a un fichero con el parámeto -F puedes configurar que formato usar de la lista: kv|, json, csv, mqtt, |influx, syslog, |null.

Microcontroladores e inteligencia artificial embebida

En los últimos años hay dos áreas que han avanzado rápidamente. La inteligencia artificial y todo lo que rodea los microcontraladores y sensores. Sin embargo los avances de una y otra parece separarlas. La I.A. se mueve hacia grandes redes neuronales que requieren grandes máquinas con mucha potencia y memoria. Si bien los microcontroladores están bajando de precio y aumentando sus capacidades quedan muy atrás de lo que necesitan las I.A. actuales.

Ambos están condenados a entenderse. Los sensores y actuadores  son la forma en que la I.A. interactúa con el mundo físico. Muchas aplicaciones de la I.A. viven cómodamente en mundos virtuales procesando y generando datos que no provienen directamente del mundo físico. Pero en muchos otros casos las grandes inteligencias en la nube y los pequeños microcontroladores están condenados a entenderse.

Muchas veces ese problema se soluciona añadiendo conectividad a los microcontroladores y que se comuniquen con la nube. Eso da lugar a dispositivos extremadamente tontos que necesitan conexión a un servidor a miles de kilómetros para encender o apagar una luz o poner la calefacción.

No hay nada malo en que los dispositivos estén conectados. Permite su control y monitorización de forma remota. Lo que no tiene sentido es que el dispositivo no pueda actuar sin conexión a internet. Internet debe de ser una fuente de datos más para el dispositivo. Un termostato puede mirar en internet la previsión del tiempo o el precio de la luz y usarlos como complemento para ajustar la temperatura de la casa pero sin ellos debes de seguir funcionando.

La inteligencia artificial es un área que se avergüenza de sus pasado. Tanto como para dejar de llamar “inteligencia artificial” a sus éxitos anteriores en cuanto aparece un nuevo enfoque. Por ejemplo la visión por computador de hace unos años está llena de algoritmos para reconocer formas, seguir objetos, calcular bordes….llegaron las redes neuronales convolucionales y todo eso quedó en un segundo plano.

Son algoritmos válidos y eficaces, no tan flexibles como el deep learning pero lo poco que hacen lo hacen suficientemente bien. Muchos de esos algoritmos se usan en entornos industriales. Lo mejor de todo es que los microcontroladores actuales pueden ejecutarlos. Podemos tener chips diminutos, de bajo consumo y barato ejecutando algoritmos que llevan décadas siendo usados (y por tanto mejorados) para realizar tareas que requieran cierta cantidad de inteligencia o de aprendizaje.

Si los microcontroladores no son suficientemente potentes los ordenadores también han bajado de precio y hay SOC que pueden actuar de cerebro cuando se requiera más potencia de calculo sin necesidad de abandonar tu red local. Y aún si no fuera suficiente, por ejemplo para reconocimiento de voz, se puede consumir como si fuera un servicio.

Lo ideal seria una arquitectura en la que los micontroladores fueran capaces de realizar las tareas básicas sin ayuda externa, por encima de ellos un hub que conecta y coordina los dispositivos y si aun asi se requiriera alguna tarea demasiado pesada para el hub se puede consumir como servicio. La idea de este modelo es que se “quede en casa” tanta parte del sistema como sea posible.

Ahora que tenemos una idea de la arquitectura ideal centrémonos en la inteligencia embebida dentro los microntroladores, se enfrenta a la siguientes limitaciones:

  • Memoria RAM, suele ser poca y hay que gestionarla bien.
  • Potencia de cálculo, los microcontroladores no están diseñados para grandes cálculos por lo que tienen importantes limitaciones.
  • Tiempo real, por lo general no pueden esperar demasiado a dar respuesta. Un sistema de seguridad no puede tardar un cuarto de hora en decidir si da la alarma.
  • Bateria, muchos dispositivos funcionan con baterías lo cual complica todo un poco más y que obliga a aplicar medidas para reducir el consumo y alargar su duración.
  • Poco espacio para modelos, los microcontroladores no tienen “un disco duro” donde almacenar grandes cantidades de datos. Eso limita el tamaño de los modelos y bases de datos a usar.

En resumen los algoritmos de inteligencia artificial que se llevan usando décadas pueden seguir siendo útiles en dispositivos de domótica en lugar de depender de sistemas en la nube.

Comprobar la aleatoriedad. Test de rachas.

Uno de los problemas de tener un generador de números que parecen aleatorios es estar seguro de si lo son. Hay que tener en cuenta que hablamos de números aleatorios justos, donde todos sean igual de probables, en este caso de números binarios que con un 50% salga 0 o 1 (existen formas de asegurarse de que una fuente aleatoria es justa). Vamos a ver el test de rachas para validar la aleatoriedad.

Test de rachas

En el caso de números binarios este test es muy fácil de entender, contamos el número de rachas que hay en los datos. ¿Qués un racha? Cada vez que un valor es distinto que el valor anterior. Por ejemplo:

00110101100111100

Si lo separamos en rachas:

00 – 11 – 0 – 1 – 0 – 11 – 00 – 1111 – 00

Ahora contamos las rachas, los ceros y los unos. En este caso hay 9 rachas, 8 ceros y 9 unos.

Si el número de rachas es R, el número de ceros es n0, el de uno es n1 y n = n0+n1.

media => u = (2*n0 *n1 / n) + 1

varianza => var = 2*n0*n1*(2*n0*n1-n) / n² * (n-1)

desviación típica => des = sqrt(var)

Z = R + c – u / des

Si R > u → c = -0,5

Si R < u → c = 0,5

El resultado es el Z score. Podemos usar tablas para saber su valor pero si queremos tener una buena aproximación con una seguridad del 95% de que es una distribución aleatoria no puede ser mayor de 1.6 ni menor de -1.6.

R = 9

n0 = 8

n1 = 9

n = 17

u = (298 / 17) +1 = 9,47

var = 298 * (298-17) / 17² * (17-1) = 18,10

des = 4,25

Z = 9 – 0,5 – 9,47 / 4,25 = -0,22

-0,22 esta entre 1.6 y -1.6 con lo cual lo consideraríamos como aleatorio.

Creatividad artificial. CLIP+GA. Pinturas minimalistas y conceptuales con CLIP y algoritmos genéticos

Tras ver lo bien que funcionaba VQGAN+CLIP para generar imágenes yo también quería hacer un programa que creara arte a partir de un texto pasado como referencia. Por supuesto el resultado ha sido un fracaso, sin embargo se puede aprender de los fracasos.

La idea

La idea es usar solo CLIP con algoritmos genéticos. CLIP permite calcular (más o menos) la diferencia entre una imagen y un texto. Si le paso el texto “pato” y una foto me dice como de parecido es el contenido de esa foto al texto, en este caso a un pato. La idea es usar CLIP como función fitness. Para ello calculamos el vector que CLIP asocia a la imagen generada por nuestro algoritmo genético (luego vemos esto) y el vector que asocia a nuestro texto de referencia y calculamos la similitud coseno . La similitud coseno devuelve un valor entre -1 y 1:

  • 1 significa que es idéntico
  • 0 significa que no tiene nada que ver (es ortogonal)
  • -1 que es opuesto

Por lo tanto nuestra función fitness seria:

fitness = 1 - simCos(vectorTexto, vectorImagen)

Vamos a ver un poco de código.

Vector de características de texto:

#vector de carateristicas de texto
text_tokenize = clip.tokenize(TEXT).to(device)
with torch.no_grad():
  text_features = model.encode_text(text_tokenize)
text_features /= text_features.norm(dim=-1, keepdim=True)

Vector de características de la imagen:

#vector de carateristicas de la imagen
image = preprocess(im).unsqueeze(0).to(device)
with torch.no_grad():
  image_features = model.encode_image(image)
image_features /= image_features.norm(dim=-1, keepdim=True)  

Similitud coseno:

#similitud coseno
sim = torch.cosine_similarity(text_features, image_features, dim=1) 

Fitness:

#fitness
return 1 - sim[0].item()

Teniendo una función fitness ya puedo usar cualquier metaheurística. En este caso se trata de optimizar una imagen para que se acerque cada vez más a lo que CLIP considera similar al texto.

Para este caso vamos a usar algoritmos genéticos. Cada individuo es una imagen. Por lo tanto la población es un conjunto de imágenes que irán evolucionando y cruzándose compitiendo por reducir el valor de la función fitness.

Individuos, cruces y mutaciones

En un primer momento intente que el genotipo fueran los pixeles de la imagen y que las mutaciones y cruces fueran cambios aleatorios en los mismos. Manejar 16 millones de colores por cada pixel me parecían un espacio de soluciones muy amplio y decidí hacerlo en escala de grises por lo que solo tendría 256 posibilidades por pixel. Sin embargo el resultado fue un desastre los pixeles sueltos eran tan “pequeños” que creaba un “ruido” que no era capaz de evolucionar. Como solución intenté crear “píxels” más gordos. Para ello use cuadrados. Tampoco funcionó. Añadir colores dio como resultado cosas que podían tener sentido pro dentro de un ruido de colorines.

Siguiente plan, separar el genotipo y el fenotipo. El genotipo serían las instrucciones para construir la imagen mientras que el fenotipo sería la propia imagen. Como pasos para construir la imagen decidí usar pinceladas. En este caso para simularlas use líneas rectas que van de un punto aleatorio a otro, con color y anchura también aleatorios.

Una vez tenemos definidos los individuos hay que definir sus operaciones de cruce, de mutación y de reemplazo.

Operadores de cruce:

  • Intercambiar un bloque de trazos
  • Mezclar todos los trazos
  • No cruzar

Operadores de mutación:

  • Mover un trazo
  • Cambiar de color un trazo
  • Cambiar de orden un trazo por otro
  • Añadir un trazo

Como operador de reemplazo se reemplaza al peor individuo de la población (fitness más alto) por aquel hijo que tenga menos fitness que él.

Como el resultado era caótico, lleno de lineas de colores que no aportaban nada a CLIP pero dificultaba que un humano reconociera el dibujo decidí minimizar el número de trazos, para ello cada cierto tiempo se ejecuta una función de optimización local que prueba a eliminar cada uno de los trazos y si no altera o reduce la función fitness lo descarta. 

Como último añadido y para dar más variedad a los trazos incluí que un trazo pueda ser una linea, un rectángulo o una elipse. Y una función mutación que cambia el tipo de trazo.

El resultado es un artista minimista que trata de trazar el menor número de líneas para dibujar un concepto.

Una Inteligencia artificial conceptual y minimalista

Hemos logrado un artista que crea con el menor número de trazos posible. Conceptual y minimalista…solo hay un problema que es un autor conceptual y minimalista desde el punto de vista de una IA. Hay imágenes en la que coincidimos, por ejemplo con las rosas:

Rosa

Otras imágenes sin embargo son incomprensibles, por ejemplo cuando le pido que me dibuje un pato le basta con dibujar una linea naranja rodeada de lineas verdosas, me lo hizo varias veces.

Pato ¿?

¿Por qué esa linea es un pato? Seguramente porque lo identifica con el pico y no necesita más para identificar un pato. Así que como nuestro autor minimalista reconoce patos por el pico (su rasgo más distintivo) es difícil que surja una mutación que mejore ese resultado.

Algo parecido me pasa con los dragones que no me pinta el cuerpo solo la cabeza, generalmente con algún tipo de “cresta” y a veces las patas, pero nada de cuerpo. Lo cual tiene sentido ya que su cuerpo se parece al de otros reptiles.

¿El dragón sin cuerpo?

Como herramienta puede ayudarnos a entender que “ve” una IA en una imagen.

Para probarlo dejo este enlace al codebook de Google donde probarlo