¿Qué significan los parámetros de configuración de los LLM?

Cuando empiezas a trastear con los LLM descubres que se pueden configurar diversos parámetros, por desgracia su significado y utilidad muchas veces no esta muy claro, para solucionar eso voy a tratar de explicar los distintos parámetros que se pueden configurar habitualmente en los LLM. Mi intención no es dar una explicación detallada de como funciona cada uno, sino explicar de forma intuitiva su uso. He usado el nombre (o nombres) en inglés más habituales, aunque es posible que se llamen de forma un poco diferente según la librería que uses para ejecutar los modelos. Igualmente, cuales se pueden usar depende de la librería y del modelo de lenguaje que uses. Al final puedes encontrar un vídeo con una explicación más detallada de los parámetros más importantes. Empecemos:

Prompt Template: Establece la plantilla que se usara en la conversación con un cahtbot. En ella se indicara donde se inserta el prompt del usuario. Es importante usar la plantilla adecuada para el modelo de LLM que usamos. en algunos casos la plantilla esta dividida en prefix (prefijo) y (sufix) sufijo.

System Prompt: Es el prompt inicial que se le pasa al LLM en el se le indica como que tiene que hacer.

Context Size: indica el tamaño del contexto en tokens. Es la cantidad de tokens con la que el modelo puede trabajar.

Prompt Batch Size: Es una técnica de optimizan que divide el prompt en bloques del tamaño indicado para alimentar el LLM. Un tamaño demasiado pequeño puede afectar a la calidad del resultado

RoPE-scaling (rotary position embeddings scaling): se refiere a una técnica para aumentar el tamaño del contexto. Indica por cuánto se multiplica el tamaño del mismo.

Number of Tokens to Predict: Números de tokens a generar. ¡Ojo! Eso no quiere decir que la respuesta vaya a tener ese número exacto de tokens. Es posible que sea menor ya que existe un token End-of-Sequence (EOS) que indica que el texto termina ahí. Algunos modelos permiten ignorar este token, aunque no se garantiza que el texto que siga tenga demasiado sentido.

Temperature: Controla «lo aleatorio» que es el proceso para elegir la siguiente palabra. A mayor valor más «creativo» será el texto generado. Si su valor es 0 elegirá siempre el token más probable mientras que valores mayores hará que pueda elegir entre otros tokens menos probables. Se puede usar para equilibrar la coherencia y la creatividad.

Repeat penalty / Presence penalty / Frequency Penalty: se utiliza para evitar que el modelo repita las mismas palabras con demasiada frecuencia en el texto generado. Es un valor que se resta a la probabilidad de elegir un token cada vez que ocurre en el texto generado. Un valor elevado hará que el modelo sea menos propenso a repetir tokens. La diferencia entre presence y frequency es que el primero solo valora si el token está o no, frecuency acumula valor por cada vez que aparece el token
En algunos modelos se pueden usar número negativos para conseguir el efecto contrario.
Suele tener otro parámetro para indicar cuántos de los últimos tokens se tienen en cuenta.

Top K Sampling: es un método que selecciona el siguiente token de de un subconjunto formado por los k tokens más probables. A menor valor tenga más predecible será el texto generado.

Top P Sampling: similar a Top K, con la diferencia de que selecciona el siguiente token de un subconjunto de tokens que juntos tienen una probabilidad acumulada de al menos p.

Tail Free Sampling (TFS): La misma idea que Top P pero más refinada intenta no meter tokens con muy baja probabilidad dentro del subconjunto del tokens entre lo que se elige el siguiente. Su valor es entre 0 y 1. Si el valor es 1, TFS no influye en el resultado (podríamos decir que esta desactivado). Lo habitual son valores entre 0.9 y 0.95.

Classifier-Free Guidance (CFG): Es una técnica, que proviene de los mecanismos de difusión usados en la generación de imágenes, para ayudar a mantener el contenido generado próximo al prompt. En este caso se usa un contexto para guiar la generación, suele ser la última palabra del prompt o el System promtp. También permite prompts negativos con los LLMs

Logit Bias: Permite aumentar/disminuir la probabilidad de ciertos tokens.

RNG Seed / Seed: Establece la semilla con la que se inicializa el generador de números aleatorios. Permite obtener resultados repetibles usando la misma semilla.

Puede ver esto mismo explicado en vídeo haciendo click en la imagen:

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

Clonar tu voz en otro idioma usando bark

Investigando sobre Bark, proyecto del que ya hemos hablado en este blog. Descubrí que hay un proyecto que permite clonar voces para usarlas en Bark.

La idea me entusiasmo, sin embargo surgió un problema, no había un modelo en español sobre el que clonar mi voz. Resumiendo mucho, cuando «clonas» una voz a partir de un breve audio lo que hace el programa es extraer las características de tu voz y aplicarlas a un modelo ya existente. Por eso el resultado de «clonar» es tan dispar según la voz que se trate de replicar. Al no tener un modelo es español tuve que usar uno en inglés, el resultado es una voz inglesa pero «que suena» como la mía.

El proyecto original tiene una notebook en python para ejecutar: clone_voice.ipynb. Está pensado para ejecutarlo en local. Más adelante explico que pasos hacer para ejecutar ese notebook en VSC. Pero he creado una versión que se ejecuta en Google Colab, lo puedes encontrar aqui.

Consejos para grabar tu voz

Es necesario crear un pequeño fichero audio.wav del que poder clonar la voz.

  • Audios de no más de 13 segundos
  • Formato .wav PCM 16 bits (a mí me ha funcionado, es posible que acepte más)
  • Lo recomendado son 9 – 10 segundos
  • Habla claro, tranquilo y vocaliza bien
  • Reduce, en lo posible, los ruidos de fondo
  • Que no haya más de una voz en el audio

Puedes ver un ejemplo de como usar el Google Colab en el siguiente vídeo de mi canal de Youtube:

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

Instalar y ejecutar en local

Veamos los pasos para instalarlo en local

git clone https://github.com/serp-ai/bark-with-voice-clone.git
cd bark-with-voice-clone
pip install git+https://github.com/suno-ai/bark.git
git clone https://github.com/gitmylo/bark-voice-cloning-HuBERT-quantizer/  
cd bark-voice-cloning-HuBERT-quantizer
pip install -r requirements.txt
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu117  
cd ..

Para clonar una voz debemos usar el cuaderno de Python: clone_voice.ipynb Que se encuentra en el directorio raíz del proyecto.

Si no sabes como ejecutarlo puedes usar Visual Studio Code, el propio IDE te sugerirá que plugins necesitas instalar para ejecutarlo.

Si tienes una tarjeta gráfica que no sea de NVIDIA, que no soporte CUDA o que tenga menos de 12Gb de VRAM puedes ejecutarlo usando exclusivamente CPU (debes disponer de 12 Gb de memoria RAM libres).  Para que todo funcione usando la CPU debes cambiar la siguiente línea:

#device = 'cuda' # or 'cpu'
device = 'cpu'

Debo avisar que ejecutarlos sobre CPU hace que tarde unas dos horas (portátil con AMD Ryzen 7 – 50xx) durante las cuales el ventilador hace ruido de que quiere despegar.

Crea un asistente para consultarle dudas de tus proyectos de software

Uno de los problemas que tengo con copilot y otros asistentes para escribir código es lo mal que se les da crear código para proyectos  grandes. Si quieres hacer un proyecto pequeño o tienes un problema con un algoritmo aislado van genial. Pero cuando tienes un proyecto grande con librerias y métodos ya creados, tiende a ignorarlos y usar otras librerías. Vamos a crearnos un asistente, que no va a generar código pero va a ayudarnos a programar proyectos ya existentes. Le preguntaremos nuestras dudas y el nos responderá.

Ya vimos la arquitectura Retrieval Augmented Generation (RAG) en otro post. Vamos a partir de ese punto. Recapitulo rápidamente. Tenemos un listado de documentos que usaremos para complementar el prompt que crea el usuario. Para ello se usa alguna técnica para extraer información de los documentos a partir del prompt de usuario. Con esto se da un «contexto» que permite al LLM dar una mejor respuesta.

Con la arquitectura anterior ya hecha puede parecer muy sencillo hacer esto . Ponemos el código como documentos y listo. El problema de esto es que es difícil que añadiendo solo el código sea capaz de responderte a tus dudas. Pensar que realmente el LLM no tiene acceso a todo tu código. Solo a una parte, la que se le pasa como contexto.

La forma de mejorar esto es meter otro LLM y un prompt. En mi caso la idea es pedirle que comenté el código. Y el resultado usarlo como documentos sobre lo que usar RAG. Al estar el código comentado en lenguaje escrito es más fácil que responda a las preguntas.

Arquitectura del sistema

De la idea a la práctica

Ya hemos visto la idea, ahora toca ver cómo la convertimos en realidad.

Mi idea era usar Stable Vicuna 13B en mi máquina local como LLM para ambas tareas. Pero su rendimiento es terrible en mi máquina (lo ejecuto sin CPU). Así que para comentar el código use ChatGPT 3.5.

Por desgracia un fichero entero de código rara vez cabe. Para ello dividí el código en trozos. Hay que asegurarse de que no se parte una función por la mitad (lo correcto sería usar alguna librería para ello, yo lo he hecho a mano).

Para ello le pasó el siguiente prompt:

Te voy a pasar fragmentos de código de un mismo proyecto, 
añade comentarios en español explicando su funcionamiento. 
Sigue las siguientes reglas:
1. Comenta las lineas que sean interesantes
2. Comentas la función de cada variable
3. Comenta antes de cada función que es lo que hace
4. Se breve
Por ejemplo:
var f = 0; //variable que indica hasta que numero de fibonacci se calcula
/*Toma un número n como argumento y devuelve un array 
con la secuencia de Fibonacci de longitud n. 
Si n es igual o menor que 0, devuelve un array vacío.*/
function fibonacci(n) {
    if (n <= 0) {
        return [];
    } else if (n === 1) {
        return [0];
    } else if (n === 2) {
        return [0, 1];
    } else {
        // Inicializa el array con los dos primeros elementos de la secuencia  
        var sequence = [0, 1];
        // Itera desde el tercer elemento hasta el n-ésimo elemento de la secuencia
        for (var i = 2; i < n; i++) {
            // Calcula el siguiente número de Fibonacci sumando los dos números anteriores
            var nextNumber = sequence[i - 1] + sequence[i - 2];
            // Agrega el siguiente número a la secuencia
            sequence.push(nextNumber);
        }
        // Devuelve la secuencia completa de Fibonacci
        return sequence;
    }
}
//Calcula el número de fibbonaci f
fibonacci(f);

Ignoró la respuesta y paso luego cada bloque de código. Voy juntando las respuestas en un fichero con el nombre del original pero terminado en .txt. Por ejemplo, si proceso el fichero HolaMundo.cpp obtendré el fichero HolaMundo.cpp.txt. Esto lo hago para poder saber de qué fichero vienen las citas elegidas (es una pista de por donde buscar si la respuesta ni me satisface del todo). Es posible que el fichero obtenido este lleno de errores (comentarios mal cerrados, faltan llaves, cosas así) da igual, lo queremos solo como documentación.

También se puede añadir cualquier documentación. Si está en otro idioma se podría usar ChatGPT para traducirla. Recordar que el algoritmo que elige que fragmentos se añaden al contexto no es tan «listo» como un LLM y hay que ponerle las cosas fáciles.

Con todos los documentos preparados ya podemos añadirlos a nuestro RAG. En mi caso uso GPT4all como ya expliqué en este otro post. Ahora tengo un chatbot al que puedo preguntar sobre mi código.

Resultados

Suficiente, es una herramienta útil, no es perfecta. Unas veces da respuestas sorprendentemente ingeniosas, otras sorprendentemente estúpidas. Lo normal es que sirva de ayuda y si no te da la respuesta te da una buena pista. Si te enfrentas a un proyecto mal documentado (o demasiado grande para estudiarlo en profundidad) puede ayudarte.

Una sorpresa que me he encontrado es lo útil que resultan las citas que incluyen las respuestas. Al saber de qué fichero provienen las citas, te puedes ahorrar mucho tiempo de buscar por el código (por eso mantengo el nombre original del archivo y añado la extensión «.txt»)

Puedes ver un vídeo sobre este proceso en mi canal de Youtube:

Haz click para ver el vídeo en Youtube

Hacer un robot que reaccione a los gestos de la cara con visión por computador y Arduino

En otros posts ya hemos visto como controlar un robot (o un brazo robótico en nuestro caso) con Arduino desde el navegador usando distintos medios de entrada como voz o gestos. La intención de este post es controlarlo con los gestos de la cara. Que reaccione a nosotros de diversas formas. Para ello, como en casos anteriores, usaremos Processing, en concreto su versión en Javascript, P5.js con la librería ML5.js que integra varios modelos de Tensorflow.js.

Para ver cómo funciona la parte de Arduino podéis ver este post y para el control desde el navegador este otro.

La idea

Para este proyecto vamos a usar facemesh, esta red neuronal estima la posición de distintos puntos clave de la cara (468 puntos) en tres dimensiones a partir de una imagen en dos dimensiones. Estos puntos forman una malla sobre la cara. Los puntos se concentran en zonas representativas de la cara. Delimitan zonas como las cejas, nariz, labios, ojos, …. Calculando las posiciones relativas de estos puntos uno respecto al otro
deducimos que expresión facial tienes, una vez veamos la expresión lo que haremos será que el robot reaccione a la misma.

El primer gesto es que el robot te siga con la mirada. ¿Mirada? ¿No era un brazo? Si, y como podéis ver en la foto inferior le he puesto ojitos de cartón. Para ello tomara como referencia el punto medio entre los dos ojos midwayBetweenEyes y según se desplace por la pantalla girara la base del brazo en una dirección u otra. Para hacerlo más sencillo la cámara va a estar justo encima del brazo por lo que tomara  el punto central de la pantalla como posición inicial, a partir de ahí girará a derecha o izquierda. La conversación de grados a pixels habrá que ajustarla para cada cámara. Ya que dependerá del ángulo de visión de la misma. Trabajaremos en bloques de 20 pixeles. Así evitaremos temblores en el movimiento. Si intentamos ajustar «al pixel» se producirán temblores ya que debido al ruido el punto dónde lo detecta puede variar.

  [centroOjosX, centroOjosY] = predictions[i].annotations.midwayBetweenEyes[0];

...

function gestoCentroCara(){
  //ang.del brazo con la cara a la izquerda
  let minAng = 30;
  //ang.del brazo con la cara a la derecha
  let maxAng = 150; 
  //pixeles por parte
  let pixelsParte = 20;  
  //partes en laimagen
  let partes = 640 / pixelsParte;  
  //En que parte esta la cara
  let parteCentroOjos = Math.floor(centroOjosX/pixelsParte);  
  //Grados que mueve el brazo por parte
  let gradosParte = (maxAng-minAng) / partes;   
  //Grados que hay que mover el brazo
  anguloBase = maxAng  - Math.floor(gradosParte * parteCentroOjos);
}

El siguiente gesto es acercar «la cabeza» cuando te acerques, ¿cómo haremos esto? Con el tamaño de la cabeza (cuidado con los cabezones), lo que haremos será medir el ancho del cuadrado que contiene la cabeza y según la misma calcularemos aproximadamente la distancia a la que está. De tal forma que si el cuadrado se incrementa el robot «sentirá» que te estás acercando.

  [topLeftX, topLeftY] = predictions[i].boundingBox.topLeft[0];
  [bottomRightX, bottomRightY] = predictions[i].boundingBox.bottomRight[0];
  faceWidth = bottomRightX - topLeftX;
  faceHeight = bottomRightY - topLeftY;

...

function gestoDistanciaCabeza(){
  if(faceWidth > 320){
    anguloHombro = 140;
  } else if(faceWidth > 200){
    anguloHombro = 110;
  } else {
    anguloHombro = 90;
  }
}

El último gesto es imitar los movimientos de la boca, de tal manera que cuando abres la boca abre la pinza y cuando cierras la boca cierra la pinza. Para ello miraremos los puntos que hay inferior y superior de los labios y calcularemos su distancia, pasado cierto límite se considera abierta.

  [labioArribaX, labioArribaY] = predictions[i].annotations.lipsUpperInner[5];
  [labioAbajoX, labioAbajoY] = predictions[i].annotations.lipsLowerInner[5];

...

function gestoBoca(){
  if(labioAbajoY - labioArribaY > 15){
    anguloMano = 10; //abierta
  } else {
    anguloMano = 35; //cerrada
  }    
}

Ejemplo de detección

Puedes ver cómo funciona todo esto en el siguiente video de mi canal de Youtube:

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

El código

La estructura de datos que nos devuelve faceMesh ante cada detección (puede detectar más de una cara pero nosotros leeremos solo la primera) es la siguiente:

faceInViewConfidence: 1 //confianza en el resultado
boundingBox: Object //esquinas del cuadrado de la cara
mesh: Array(468) //malla
scaledMesh: Array(468) //mala normalizada
annotations: Object //datos estructurados

Nosotros vamos a usar dos campos annotations, que nos devuelve las coordenadas de cada parte de la cara y boundingBox que nos define un cuadrado donde se encuentra la cara.

boundingBox: Object
    topLeft: Array(1)
    bottomRight: Array(1)
annotations: 
    silhouette: Array(36)
    lipsUpperOuter: Array(11)
    lipsLowerOuter: Array(10)
    lipsUpperInner: Array(11)
    lipsLowerInner: Array(11)
    rightEyeUpper0: Array(7)
    rightEyeLower0: Array(9)
    rightEyeUpper1: Array(7)
    rightEyeLower1: Array(9)
    rightEyeUpper2: Array(7)
    rightEyeLower2: Array(9)
    rightEyeLower3: Array(9)
    rightEyebrowUpper: Array(8)
    rightEyebrowLower: Array(6)
    leftEyeUpper0: Array(7)
    leftEyeLower0: Array(9)
    leftEyeUpper1: Array(7)
    leftEyeLower1: Array(9)
    leftEyeUpper2: Array(7)
    leftEyeLower2: Array(9)
    leftEyeLower3: Array(9)
    leftEyebrowUpper: Array(8)
    leftEyebrowLower: Array(6)
    midwayBetweenEyes: Array(1)
    noseTip: Array(1)
    noseBottom: Array(1)
    noseRightCorner: Array(1)
    noseLeftCorner: Array(1)
    rightCheek: Array(1)
    leftCheek: Array(1)

Para calcular la distancia solo tomaremos los puntos X eY, ignoraremos el Z. Si OS preguntáis porqué no usamos esa Z, es porque en mis pruebas no da la profundidad respecto a la cámara, sino respecto al resto de puntos de la cara.

Código HTML:

<html>
  <head>
    <meta charset="UTF-8" />
    <title></title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/p5.min.js"></script>  
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/addons/p5.dom.min.js"></script>  
    <script src="https://unpkg.com/p5-webserial@0.1.1/build/p5.webserial.js"></script>  
    <script src="https://unpkg.com/ml5@latest/dist/ml5.min.js"></script>  
    <style></style>
  </head>
  <script src="sketch.js"></script>  
  <body>
    <h1>Control usando gestos</h1>
  </body>
</html>

Código JS:

let facemesh;
let video;
let predictions = [];
let labioArribaX, labioArribaY;
let labioAbajoX, labioAbajoY;
let centroOjosX, centroOjosY;
let faceWidth, faceHeight;

let anguloBase = 90;
let anguloHombro = 90;
let anguloMano = 35;
let terminado = false;
let hayPrediccion = false;

//puerto serie
const serial = new p5.WebSerial();

function setup() {
  createCanvas(640, 480);
  portButton = createButton("Elegir puerto");
  portButton.position(5, 5);
  portButton.mousePressed(choosePort);
  let botonApagar = createButton("Terminar");
  botonApagar.position(300, 5);
  botonApagar.mousePressed(() => {    
    terminado = true;
    anguloBase = 90;
    anguloHombro = 90;
    anguloMano = 35;    
    send("Q");   
    console.log("Brazo en posisicion incial");
  });
  video = createCapture(VIDEO);  
  video.size(width, height);
  inicializarSerial();
  facemesh = ml5.facemesh(video, modelReady);
  //si hay datos de una cara actualizar predictions 
  facemesh.on("predict", results => {
    predictions = results;
  });  
  video.hide();
  //enviar datos al brazo cada 250ml
  setInterval(actualizarAngulos, 250);
}

function modelReady() {
  console.log("Model ready!");
}

function draw() {
  image(video, 0, 0, width, height);
  dibujarPuntos()
  if(hayPrediccion){
    gestoCentroCara()
    gestoDistanciaCabeza()
    gestoBoca()
    hayPrediccion = false
  }
}

function dibujarPuntos() {  
   if(predictions.length > 0) {
    let i = 0;//solo la primera prediccion
    hayPrediccion = true;
    [labioArribaX, labioArribaY] = predictions[i].annotations.lipsUpperInner[5];
    [labioAbajoX, labioAbajoY] = predictions[i].annotations.lipsLowerInner[5];
    [centroOjosX, centroOjosY] = predictions[i].annotations.midwayBetweenEyes[0];
    [topLeftX, topLeftY] = predictions[i].boundingBox.topLeft[0];
    [bottomRightX, bottomRightY] = predictions[i].boundingBox.bottomRight[0];
    faceWidth = bottomRightX - topLeftX;
    faceHeight = bottomRightY - topLeftY;
    
    fill(0, 255, 0);
    ellipse(labioArribaX, labioArribaY, 5, 5);
    ellipse(labioAbajoX, labioAbajoY, 5, 5);
    ellipse(centroOjosX, centroOjosY, 5, 5);
    ellipse(centroOjosX, centroOjosY, 5, 5);
    noFill();
    rect(topLeftX, topLeftY, faceWidth, faceHeight);
    
  }
}

function gestoCentroCara(){
  //ang.del brazo con la cara a la izquerda
  let minAng = 30;
  //ang.del brazo con la cara a la derecha
  let maxAng = 150; 
  //pixeles por parte
  let pixelsParte = 20;  
  //partes en laimagen
  let partes = 640 / pixelsParte;  
  //En que parte esta la cara
  let parteCentroOjos = Math.floor(centroOjosX/pixelsParte);  
  //Grados que mueve el brazo por parte
  let gradosParte = (maxAng-minAng) / partes;   
  //Grados que hay que mover el brazo
  anguloBase = maxAng  - Math.floor(gradosParte * parteCentroOjos);
}

function gestoDistanciaCabeza(){
  if(faceWidth > 320){
    anguloHombro = 140; //cerca
  } else if(faceWidth > 200){
    anguloHombro = 110; //medio
  } else {
    anguloHombro = 90; //lejos
  }
}

function gestoBoca(){
  if(labioAbajoY - labioArribaY > 15){
    anguloMano = 10; //abierta
  } else {
    anguloMano = 35; //cerrada
  }    
}

function actualizarAngulos(){
  if(!terminado){
    send("S1:"+anguloBase);
    send("S3:"+anguloHombro);
    send("S4:"+anguloMano);   
    console.log(anguloBase, anguloHombro, anguloMano);
  }
}

//-----PUERTO SERIE-------

//enviar datos al puerto serie
function send(cmd) {    
    serial.write(cmd+"\n");
}
 
//leer datos del puerto serie
function serialEvent() {
    let readSerialStr = serial.readLine();
    trim(readSerialStr);
    if (readSerialStr) {
        console.log(readSerialStr);
    }
}
 
//incializar la conexion serie
function inicializarSerial() {
    if (!navigator.serial) {
        alert("WebSerial no sorportado. Prueba Chrome o Edge.");
    }
    serial.getPorts();
    serial.on("noport", showPortButton);
    serial.on("portavailable", openPort);
    serial.on("requesterror", portError);
    serial.on("data", serialEvent);
    serial.on("close", closePort);
    navigator.serial.addEventListener("connect", portConnect);
    navigator.serial.addEventListener("disconnect", portDisconnect);
    let statusText = "Puerto serie incializado<br>"
    select('#status').html(statusText);
}
 
// Muestra la ventana de seleccion de puerto
function choosePort() {
    console.log("ChoosePort");
    showPortButton();
    serial.requestPort();
}
 
//abrir conexion con puerto serie
function openPort() {
    console.log("Abriendo puerto serie");
    serial.open().then(initiateSerial);
    function initiateSerial() {
        console.log("Puerto serie abierto");
    }
    hidePortButton();
}
 
//Cerrar conexion con puerto serie
function closePort() {
    console.log("Puerto serie cerrado");
    serial.close();
    showPortButton();
}
 
//Error con el puerto serie
function portError(err) {
    alert("Serial port error: " + err);
    showPortButton();
}
 
//Evento puerto serie conectado
function portConnect() {
    console.log("Puerto serie conectado");
    serial.getPorts();
    hidePortButton()
}
 
//Evento puerto serie desconectado
function portDisconnect() {
    serial.close();
    console.log("Puerto serie desconectado");
    showPortButton();
}
 
function showPortButton() {
    portButton.show();
}
 
function hidePortButton() {
    portButton.hide();
}

Whisper + ChatGPT = ¿Auto-Tune para escritores?

El siguiente texto ha sido generado usando ChatGPT a partir de un audio. Para entender de que va esto puedes ver el vídeo que hay tras el parrafo. Para los prompt lovers el prompt usado es: El texto que viene tras los dos puntos es una transcripción, corrige los errores y reescribe el texto para un blog de tecnología, usa un estilo profesional pero informal:

Soy de esas personas que creen que lo más difícil de escribir es, precisamente, escribir. No me malinterpreten, no digo que no disfrute escribiendo. De hecho, tengo este blog y he publicado un libro. Sin embargo, sentarme frente al teclado y plasmar mis pensamientos en palabras, ya sea en papel o en una pantalla, me resulta bastante desafiante.

De hecho, es mucho más fácil hablar, porque la estructura no es tan rígida y las ideas pueden fluir de manera más desordenada. El resultado final es más sencillo y libre. Sería genial poder convertir un audio, como este que estás grabando tranquilamente, en un texto con el estilo que uno desee utilizando la Inteligencia Artificial.

Para lograrlo, existen cuatro pasos que podrías seguir.

El primero consiste en grabar este audio y utilizar una IA como, por ejemplo, Whisper, para convertirlo en texto.

El segundo paso implica preparar indicaciones sobre cómo deseas que ese texto se presente. Puedes especificar si deseas un tono didáctico, formal o informal, o incluso si quieres que se asemeje al estilo de un autor en particular. Es importante tener en cuenta estas indicaciones.

El tercer paso consiste en combinar el resultado obtenido en el primer paso con las indicaciones del segundo. Tomar el texto generado por Whisper a partir de tu audio y unirlo con las instrucciones que le proporcionas. Puedes utilizar una inteligencia conversacional, como ChatGPT u Open Assistance, para lograrlo.

Por último, el cuarto paso sería revisar el resultado, ya que aún no hemos alcanzado el punto en el que puedes dejar que una IA actúe por sí sola, con total libertad, para escribir un blog que puedas publicar directamente.

Con estas herramientas y pasos, es posible aprovechar la IA para simplificar el proceso de escritura y obtener textos adaptados a tus preferencias. Aunque todavía es necesario revisar y perfeccionar los resultados, el potencial de esta tecnología es prometedor y podría brindar nuevas oportunidades a los escritores y creadores de contenido.

Haz click en la siguiente imagen para ver en Youtube el vídeo donde se explica como se ha creado esta entrada:

Haz click para ver el vídeo en Youtube

Crear subtitulos automáticamente con Whisper

Aprovechando que ya vimos cómo instalar whisper.cpp y usarlo para convertir de audio a texto, veremos otra de las utilidades de whispper.cpp, generar subtítulos. No me refiero a solo generar el fichero con la transcripción del audio a texto. Además tiene que tener las marcas de tiempo de cuando es cada texto y el formato adecuado para que pueda cargarlo el reproductor (Usaremos VLC).

Lo primero es convertir el vídeo, en nuestro ejemplo whispercpp.webm, a un fichero de audio .wav con el formato adecuado. Para ello usaremos ffmpeg.

ffmpeg -i samples/whispercpp.webm -ar 16000 -ac 1 -c:a pcm_s16le samples/whispercpp.wav

Parar generar los subtitulo usamos el parametro -osrt, si no queremos o podemos especificar la lengua del vídeo podemos usar -l auto

/main -m models/ggml-large.bin -f samples/whispercpp.wav -l auto -osrt

Aún hay otra posibilidad más: traducir al inglés. Si eres capaz de entender el inglés leído, está opción te permite añadir subtitulos en inglés a múltiples idiomas.

Es tan sencillo como añadir el parámetro -tr

/main -m models/ggml-large.bin -f samples/whispercpp.wav -l auto -osrt -tr

Este parámetro combinado con -l auto para que autodetecte el lenguaje lo convierte en un traductor universal…para unos cuantos lenguajes de la Tierra, siempre y cuando sepas inglés. Aunque en este caso el resultado ha sido muy bueno al principio del audio y bastante desastre al final.

Si quieres, puedes ver los explicado en este post en acción, haz click en la siguiente imagen para ver un vídeo de mi canal de Youtube:

Haz click en la imagen para ver el vídeo en Youtube

Instala Whisper en local, sin necesidad de GPU

Whisper es, posiblemente, uno de los sistemas más avanzados para convertir audio a texto. Ha sido desarrollado por OpenAI y tiene la gran ventaja de ser código libre sin embargo surge el problema de usarlo en máquinas que no tengan carismas tarjetas con potentes GPUs y grandes cantidades de RAM dedicadas. Por suerte tenemos proyectos que adaptan su uso a CPU como Whisper,cpp con lo que podemos usarlo en un ordenador medianamente potente.

Los pasos para instalarlos son muy sencillos

Clonar el proyecto de Github (necesitaras tener git instalado).

git clone https://github.com/ggerganov/whisper.cpp

Vamos al directorio del proyecto y construimos el main

cd whisper.cpp
make main

Descargamos el modelo que deseemos usar. En la siguiente tablas esta el listado de modelos con su tamaño en disco y el consumo de RAM. Los terminados en «.en» significan que solo es en inglés:

ModeloTamaño en discoTamaño en memoria (RAM)
tiny75 MB~390 MB
tiny.en75 MB~390 MB
base142 MB~500 MB
base.en142 MB~500 MB
small466 MB~1.0 GB
small.en466 MB~1.0 GB
medium1.5 GB~2.6 GB
medium.en1.5 GB~2.6 GB
large-v12.9 GB~4.7 GB
large2.9 GB~4.7 GB

Una vez sepamos que modelos queremos lo descargamos:

bash ./models/download-ggml-model.sh base

Podemos descargar cuantos modelos como queramos.

Para probar que todo va bien usaremos uno de los ejemplos que vienen con el código:

./main -m models/ggml-base.bin -f samples/jfk.wav -l auto 

Alguno parámetros útiles:

-m indica el modelo a usar

-f el archivo de sonido a transcribir

-l el lenguaje en que esta el audio, con auto lo detecta el propio whisper.

-otxt fichero de texto donde se almacena la salida

-pc usa colores para indicar el nivel de confianza en cada palabra

-h muestra una lista de parámetros y su explicación

¿Y para usar nuestros propios audios?. Podemos usar ffmpeg para convertirlos.

Por ejemplo de test.mp3 a test.wav:

ffmpeg -i test.mp3 -ar 16000 -ac 1 -c:a pcm_s16le test.wav

Puedes ver el proceso en vídeo en mi canal de Youtube haciendo click en la siguiente imagen:

Haz click para ver el vídeo en Youtube

Un símil para entender como «razona» ChatGPT

Vamos a intentar entender como «razona» (aceptando «razonar» en un sentido muy amplio) ChatGPT sin usar matemáticas ni términos técnicos.

Para ello vamos a usar un pequeño problema de programación, usaremos el siguiente código en javascript para calcular 5000 tiradas de dados y luego calcular la media y el histograma (número de veces que ha salido cada cara):

var a = []; 
for(var i = 0; i < 5000; i++){ 
    a.push(Math.trunc(Math.random()*6)+1); 
} 

let media=0;
let histograma=[0,0,0,0,0,0];
a.forEach(function(n){
    media += n; 
    histograma[n-1]++;
    });
media /= a.length;
console.log(a.length);
console.log(media);
console.log(histograma);

Si le preguntáramos a un humano que nos dijera que resultado va dar este código tiene dos formas de enfrentarse a este problema:

  • Mirar paso a paso que hace cada linea de código. Apunetnado en un papael el resultado de la ejecución de cada una tratando de imitar a como le ejecutaria un ordenador
  • Tomar atajos mentales, ver por encima que es lo que hace cada parte del código y junto con su experiencia intuir como funciona.

En este caso el segundo método» seria algo así como:

«Un bucle que se repite 500 veces»

«Dentro se calcula un numero del 1 al 6 ambos incluidos»

«Luego hay otro bucle y una variable llamada media y otra llamada histograma»

«Calcula la media y el histograma de 5000 tiradas»

«Por lo que se de probabilidad, todas las caras han de salir aproximadamente el mismo número de veces. en este caso 5000/6»

«La media probabilidad de tirar un dado (caso similar y muy conocido) sera de alrededor de 3.5»

Pues bien esa es la forma que usa ChatGPT para dar una respuesta. Usa su «intuición» a partir de la cantidad de ejemplos con los que ha sido entrenada. Con una diferencia. Nuestro programador humano se ha saltado partes de código al ver el nombre de las variables. ChatGPT no hace eso, procesa todas las lineas de código por lo que sus «intuiciones» son mejores.

Vamos a pedirle a ChatGPT que resuelva este mismo problema.

Actúa como si fueras una consola de Javascript y muestra únicamente (sin ningún comentario tuyo) los resultados de simular la ejecución de los distintos códigos que voy a pasarte: 

var a = []; 
for(var i = 0; i < 5000; i++){ 
    a.push(Math.trunc(Math.random()*6)+1); 
} 

let media=0;
let histograma=[0,0,0,0,0,0];
a.forEach(function(n){
    media += n; 
    histograma[n-1]++;
    });
media /= a.length;
console.log(a.length);
console.log(media);
console.log(histograma);

El resultado que debería mostrar es el numero de elementos calculados, la media y el histograma:

5000
3.5194
[ 807, 855, 865, 838, 849, 796 ]

Es un buen resultado, pero le falla que si sumas todos los valores del histograma sale 5010 en lugar de 5000. Sin embargo es imposible que haya calculado 5000 números aleatorios ChatGPT no tiene capacidad para hacer eso. Ha tenido que usar unaforma muy parecida a la nuestro programador.

Esto explica un curioso fenomeno que se produce con ChartGPT, que a veces, cuando es incapaz de resolver un problema, si le pides que lo resuelva paso a paso es capaz de llegar a la respuesta correcta.

Todo esto me recuerda al libro «Pensar rápido, pensar despacio» de Daniel Kahneman donde presenta una perspectiva interesante sobre el funcionamiento de la mente humana, sobre la forma en que tomamos decisiones. Describe dos sistemas cognitivos que trabajan en conjunto: el Sistema 1 y el Sistema 2.

El Sistema 1 es el que toma decisiones rápidas e inconscientes. Es automático, frecuente, emocional, estereotipado y subconsciente. Este sistema se encarga de procesar información de forma intuitiva y generar rápidamente juicios y decisiones. Aunque este sistema suele ser muy útil para tomar decisiones cotidianas, no siempre es confiable ya que a veces puede ser influenciado por prejuicios o sesgos cognitivos.

El Sistema 2, por otro lado, es el que toma decisiones más conscientes y racionales. Es lento, requiere esfuerzo, poco frecuente, lógico, calculador y consciente. Este sistema se encarga de analizar la información de forma crítica, evaluar alternativas y tomar decisiones basadas en la razón. Sin embargo, también puede ser susceptible a errores si se utiliza de manera inadecuada o si no se tiene la información suficiente.

Es importante destacar que ambos sistemas trabajan juntos y son necesarios para tomar decisiones efectivas. El Sistema 1 proporciona intuiciones y respuestas rápidas, mientras que el Sistema 2 se encarga de validarlas y tomar decisiones más conscientes y racionales. A veces, los juicios del Sistema 1 pueden ser engañosos o incompletos, y es entonces cuando el Sistema 2 entra en acción para corregirlos. Comprender la forma en que estos sistemas trabajan juntos nos puede ayudar a tomar decisiones más efectivas y evitar errores cognitivos comunes.

ChatGPT usaría por defecto el Sistema 1 (curiosamente tachado de emocional). Al pedirle que resuelva el problema paso a paso estaría cambiando al Sistema 2.

El problema de usar el Sistema 1 es que está sujeto a errores ya que en lugar de reflexionar sobre el problema elemento por elemento se usan heurísticas y suposiciones aprendidas de nuestra experiencia. Lo cual puede llevar a errores.

Hay que entender todo como una metáfora, no como si ChatGPT de verdad razonara usando uno de estos sistemas. Pero creo que como intuición puede servir para hacernos una idea «no técnica» de como funciona.

Puedes ver un vídeo sobre este tema en mi canal de Youtube:

Haz click para ver el vídeo en Youtube

Usar ChatGPT para que simule ser una aventura conversacional

Una de las múltiples opciones que nos brinda ChatGPT es simular se una aventura conversacional. Este sistema tiene muchas ventajas respecto a las clásicas que pecaban de ser demasiado estrictas y a veces tenias que jugar a prueba y error para encontrar las palabras exactas para realizar la acción deseada. Si bien no es un sistema perfecto y tiene otros problemas.

El prompt para lograr esto es muy sencillo:

¡Vamos a jugar! Tu simulas ser una aventura conversacional, me describes la situación en la que esta el personaje yo te respondo como reacciona.[Temática de la aventura] 

Por ejemplo:

¡Vamos a jugar! Tu simulas ser una aventura conversacional, me describes la situación en la que esta el personaje yo te respondo como reacciona. Debe de ser una aventura de ciencia ficción con viajes en el tiempo, acelgas mutantes y ornitorrincos espaciales

Y aunque parezca una locura funciona perfectamente.

Para profundizar más en el tema puedes ver el siguiente vídeo en mi canal de Youtube:

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

¿Puede ChatGPT ofuscar código?

Ya hemos probado a usar ChatGPT para desofuscar código, ahora lo vamos a probar para lo contrario, ofuscarlo.

Puedes ver el resultado en el siguiente vídeo de mi canal de Youtube:

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

Prueba 1: Hola mundo en C

#include <stdio.h>
int main() {
   // printf() displays the string inside quotation
   printf("Hello, World!");
   return 0;
}

Resultados:

Como ofuscación es bastante pobre:

#include <stdio.h>
#define p printf
#define r return
int main() {
   p("H");
   p("e");
   p("l");
   p("l");
   p("o");
   p(",");
   p(" ");
   p("W");
   p("o");
   p("r");
   p("l");
   p("d");
   p("!");
   r 0;
}

Otro intento con mejor pinta aunque sigue sin ser perfecto y la cadena de texto sigue expuesta:

#include <stdio.h>
#include <string.h>

char *encode(char *str) {
    static char buffer[256];
    int i;
    for (i = 0; str[i] != '\0'; i++) {
        buffer[i] = str[i] + 1; // se suma 1 al código ASCII de cada caracter
    }
    buffer[i] = '\0';
    return buffer;
}

char *decode(char *str) {
    static char buffer[256];
    int i;
    for (i = 0; str[i] != '\0'; i++) {
        buffer[i] = str[i] - 1; // se resta 1 al código ASCII de cada caracter
    }
    buffer[i] = '\0';
    return buffer;
}

int main() {
    char *message = "Hello, World!";
    char *encoded_message = encode(message);
    char *decoded_message = decode(encoded_message);
    printf("%s", decoded_message);
    return 0;
}

El último intento tiene mejor pinta pero un ser humano seria capaz de entenderlo fácilmente ya que solo trata de añadir un

#include <stdio.h>

#define a(e)(char)(e-1)
#define b(e)(char)(e+1)

int main() {
    char* m = b('G') + a('F') + b('m') + a('n') + b('m') + a('k') + b('`') + b('V') + a('m') + a('o') + b('y') + b('q') + b('t') + b('u') + b('k') + b('w') + a('t');
    char* c = m;
    while(*c) {
        putchar(*c);
        c++;
    }
    return 0;
}

Para ver si obtiene mejor resultado le especifico desde el principio que no se pueda leer «Hello, world» y ChatGPT cumple….a su manera:

#include <stdio.h>

#define H e##l##l##o
#define W o##r##l##d
#define S(x) #x

int main() {
   printf("%s, %s!", S(H), S(W));
   return 0;
}

Prueba 2: Fibonacci en C

Para la segunda prueba le pediremos que ofusque el siguiente código en C que calcula la serie de Fibonacci y veremos como lo ofusca:

El primer resultado no es malo (omitiendo las cadenas que ya hemos visto que no entiende que hay que «ocultarlas»):

#include <stdio.h>
#include <stdlib.h>

unsigned long long f(int n){
    unsigned long long a,b,c;
    if (n<=0) return 0;
    if (n==1) return 1;
    a=0,b=1;
    for(int i=2;i<=n;i++)c=a+b,a=b,b=c;
    return c;
}

int main(int a,char **b){
    printf("fibo[%s] = %llu\n",b[1],f(atoi(b[1])));
    return 0;
}

Pidiéndole que lo ofusque aun más el resultado es bueno aunque falta añadir una linea con F(x) para que funcione:/s

#include <stdio.h>
#include <stdlib.h>
#define O unsigned long long
#define F(x) O f(x){O a,b,c;if(x<=0)return 0;if(x==1)return 1;a=0,b=1;for(int i=2;i<=x;i++)c=a+b,a=b,b=c;return c;}int main(int a,char**b){printf("fibo[%s] = %llu\n",b[1],f(atoi(b[1])));return 0;}

Prueba 3: Fibonacci en JS

fibonacci(n) {
  if (n <= 1) {
    return n;
  } else {
    return fibonacci(n - 1) + fibonacci(n - 2);
  }
}

Resultados:

function f(n){if(n<=1){return n}else{return f(n-1)+f(n-2)}}

Y:

var f=function(g){if(g<=1){return g;}else{return f(g-1)+f(g-2);}};