
Chismes del Lince 40: El Lince aprende tus gestos
El barranco olía a tierra mojada y a romero. El Lince de la Noche estaba agachado junto a un charco, con el hocico casi pegado al barro, cuando la Gata Montesa llegó dando saltitos por las piedras.
—¿Otra vez hablando con las huellas? —dijo.
—Shhh. Esta es de cabra montés. Y esta, un poco más ancha, de jabalí. Y esta otra… —el Lince alzó una ceja— no me suena de nada.
—¿Y cómo lo sabes?
—Porque soy un lince —contestó, muy digno—. Y porque llevo toda la vida mirando huellas.
La Gata se sentó a su lado y sonrió, de esa manera que el Lince ya conocía.
—Eso que acabas de decir es lo que hace una inteligencia artificial. Ni más ni menos.
—No empieces. La IA son redes neuronales, millones de números, cajas negras… magia de ordenador.
—A veces. Pero tú no tienes ninguna red neuronal en la cabeza para saber qué huella es esa. Tienes una memoria llena de huellas que ya has visto, y comparas la nueva con las que recuerdas.
El Lince se quedó callado. Después miró la huella desconocida otra vez, con otros ojos.
—¿Y eso se puede construir? ¿Una IA que aprenda así?
—Con una placa, un móvil y tres gestos —dijo la Gata—. Pero primero tendrás que enseñarle a distinguir un vaivén de un martillazo.
Introducción
En este chisme vamos a reconocer gestos con IA. Agitas el móvil de tres maneras distintas, y el móvil adivina cuál has hecho.
La Heltec aprovecha para enseñar el resultado en su pantalla OLED, con el nombre del gesto, un porcentaje de seguridad y un contador de cuántas veces ha salido cada uno.
Los tres gestos son el vaivén (el móvil adelante y atrás, muy rápido), el giro (de tumbado a de pie, despacio) y el martillo (arriba y abajo de golpe, y quieto).
Lo importante es cómo aprende. Nadie escribe una regla del tipo «si se mueve más de tanto, es un vaivén». Le enseñas ejemplos y la IA saca sus propias conclusiones.
Y aquí está la gracia: no usa una red neuronal. Son unas 60 líneas de JavaScript que puedes leer enteras, y por eso es la mejor forma de ver que una IA no es magia.
Por qué este chisme
Hoy se habla de IA en todas partes, pero casi siempre como algo que ocurre dentro de una caja cerrada. Un alumno puede usarla sin saber qué hace por dentro.
Aquí la caja está abierta. Los alumnos graban sus propios gestos, ven los números en los que se convierten, y comprueban cuándo la IA acierta y cuándo se equivoca. Y no es un juguete: la misma idea se usa de verdad en sistemas que clasifican datos con pocos ejemplos.
Es el chisme de la serie en el que la IA es la protagonista y no un ingrediente más. Si has hecho el 18 con Teachable Machine, aquí haces lo contrario: no entrenas una red con una herramienta ya hecha, sino que construyes y lees tu propio clasificador.
Encaja con el bloque de IA de 3º de ESO (clasificación y entrenamiento de modelos sencillos) y con el análisis de datos de 1º de Bachillerato, y con la idea del marco europeo de alfabetización en IA de entender cómo realiza una tarea un sistema de IA.
Qué es la IA en este chisme: aprender de ejemplos
Un programa «normal» sigue reglas que escribe una persona: si pasa esto, haz aquello. En una IA de aprendizaje automático, las reglas no las escribimos nosotros. Le damos ejemplos y el programa saca de ellos su manera de decidir.
Prueba de que aprende: si le enseñas otros ejemplos, se comporta de otra manera sin que cambies ni una línea de código.
El símil: las huellas del Lince
Piensa en cómo reconoce el Lince una huella en el barro. No calcula nada: tiene en la memoria cientos de huellas que ya ha visto, cada una con su etiqueta (cabra, jabalí, zorro).
Cuando aparece una huella nueva, la compara con todas las que recuerda y se fija en las más parecidas. Si las tres más parecidas son de cabra, de cabra y de jabalí, concluye «cabra»: votan los vecinos y gana la mayoría.
Y si la huella no se parece a ninguna de las que conoce, no se inventa nada: dice «esta no la he visto nunca».
Nuestro programa hace exactamente eso, pero con gestos. Cada gesto grabado es una «huella» con su etiqueta (vaivén, giro o martillo). Cuando haces un gesto nuevo, el móvil busca los ejemplos más parecidos y ellos votan. Esta técnica se llama k vecinos más cercanos (k-NN en inglés, de k-nearest neighbours): la k es cuántos vecinos votan.
¿Y una red neuronal?
No, esto no es una red neuronal. Pero es IA de verdad: pertenece a la misma familia, el aprendizaje automático supervisado (aprender de ejemplos que ya tienen su etiqueta).
La diferencia se entiende con un examen. La red neuronal es como estudiar mucho y hacer el examen de memoria, sin libros: durante el entrenamiento ajusta miles o millones de números internos y después ya no necesita los ejemplos. Los vecinos más cercanos son como hacer el examen con todos los apuntes abiertos: guarda todos los ejemplos y, ante cada pregunta, busca el más parecido.
| Red neuronal | Vecinos más cercanos (este chisme) | |
|---|---|---|
| Qué guarda | Muchos números internos (pesos) | Todos los ejemplos |
| Cuándo trabaja | Mucho al entrenar; poco al usarla | Nada al entrenar; mucho al usarla |
| Entrenar | Ajustar los pesos poco a poco | Guardar el ejemplo |
| Se puede leer entera | No, son demasiados números | Sí, cabe en una pantalla |
| Ejemplos que necesita | Muchos | Pocos pueden bastar |
La red neuronal gana cuando hay muchos datos y problemas difíciles, como reconocer fotos. Los vecinos más cercanos ganan cuando quieres ver y entender lo que ocurre, que es lo que buscamos en el aula.
Dónde se ejecuta y qué datos salen
Todo ocurre en el móvil. Los gestos grabados se quedan en la memoria de la página y desaparecen al cerrarla. No hay servidor, no hay internet y no se guarda nada, salvo que tú pulses Descargar CSV. A la placa solo le llega el resultado (por ejemplo, «G1,87»).
Cómo funciona, paso a paso
Para que el móvil distinga gestos, primero tiene que «sentir» cómo se mueve. Para eso usa el acelerómetro, un sensor diminuto que mide la aceleración en tres ejes (x, y, z), en metros por segundo al cuadrado (m/s²).
Un detalle importante: el sensor también nota la gravedad. Con el móvil quieto sobre la mesa, mide unos 9,8 m/s² hacia abajo. Por eso, al girar el móvil, aunque no lo aceleres, los tres números cambian: la gravedad pasa de un eje a otro.
El proceso tiene seis pasos:
- Grabar. Al pulsar el botón, la página guarda durante 1,5 segundos las lecturas del acelerómetro: unas 90 filas de tres números.
- Resumir en 6 rasgos. Comparar 90 filas con otras 90 filas sería complicadísimo. Así que cada gesto se resume en 6 números que describen cómo es (la tabla de abajo).
- Igualar las escalas. Un rasgo puede valer 20 y otro 1. Si no los igualamos, el grande «manda» sin motivo. Dividimos cada rasgo por su variación habitual.
- Medir parecidos. Con los 6 números, cada gesto es un punto en un espacio de 6 dimensiones. Dos gestos parecidos son dos puntos cercanos. La distancia entre puntos es la misma de siempre: Pitágoras, pero con 6 números en lugar de 2.
- Votar. Se miran los k ejemplos más cercanos y cada uno vota por su gesto; los más cercanos pesan más. El porcentaje que ves es la parte de los votos que se lleva el ganador.
- Decir «no sé». Si el ejemplo más cercano está muy lejos (el triple de lo normal entre ejemplos del mismo gesto), la IA no adivina: dice que no sabe.
Los 6 rasgos de cada gesto son:
| Rasgo | Qué mide | Vaivén | Giro | Martillo |
|---|---|---|---|---|
| sd_mayor, sd_medio, sd_menor | Cuánto varía cada eje (de mayor a menor) | Uno muy alto | Medio | Bajo |
| inclinación | Cuánto cambia la gravedad entre el principio y el final | Casi nada | Mucho | Casi nada |
| pico | El mayor tirón respecto a la fuerza media | Alto | Casi nada | El más alto |
| golpes | Cuántas veces sube el tirón de un umbral | Muchos | Ninguno | Uno o dos |
Fíjate en que ningún rasgo reconoce un gesto por sí solo. El vaivén y el martillo comparten un pico alto; lo que los separa es cuántos golpes hay. Es la combinación de los seis números la que distingue los gestos.
Un truco: los tres primeros rasgos se ordenan de mayor a menor. Así no importa en qué posición agarres el móvil (vertical, horizontal…): lo que cuenta es cuánto se mueve, no por qué eje.
La distancia entre dos gestos a y b, con rasgos normalizados, es:
d(a,b) = \sqrt{(a_1-b_1)^2 + (a_2-b_2)^2 + \dots + (a_6-b_6)^2}
Qué necesitamos
- Una placa Heltec WiFi LoRa 32 (o TTGO LoRa32), con su pantalla OLED.
- Un móvil Android con Chrome, que lleva acelerómetro y Bluetooth. En iPhone, Chrome no permite Web Bluetooth.
- El Arduino IDE con la placa «ESP32 Dev Module» (paquete de placas ESP32 de Espressif, versión 2.0.x) y las bibliotecas Adafruit SSD1306 y Adafruit GFX.
- Un sitio con https donde poner la página (Web Bluetooth lo exige), o la página de MonteSteam.
No hay que conectar ningún cable ni ningún sensor a la placa: el acelerómetro es el del móvil. Y si no tienes la placa a mano, la página funciona igual con la opción «Probar sin móvil», que inventa los gestos.
Construcción
Este es el chisme más corto de montar de la serie, porque no hay nada que conectar.
- Abre el Arduino IDE y pega el programa de la placa (el siguiente apartado).
- Elige la placa «ESP32 Dev Module» y el puerto USB de la Heltec.
- Pulsa Subir. Si dice «Sketch too big», ve a Herramientas → Partition Scheme → «Huge APP (3MB No OTA/1MB SPIFFS)» y vuelve a subir.
- La pantalla debe mostrar «GESTOS – esperando».
- Guarda la página del móvil como un archivo .html y ábrela desde una dirección https, con Chrome en el móvil.
- Pulsa «Conectar Heltec» y elige «Heltec-Gestos». La pantalla pasa a «GESTOS – conectado».
Si la pantalla de la placa sale en blanco, comprueba los pines de la OLED: en este programa son SDA 21, SCL 22 y RST 16, que son los de las placas TTGO. En una Heltec V2 oficial pueden ser 4, 15 y 16.





El código de la placa
La placa no tiene nada de IA: solo recibe el resultado y lo enseña. Antes de pegarlo, responde en tu cuaderno sin ejecutarlo:
- Si llega el mensaje «G2,87», ¿qué pone la pantalla y cuánto vale el contador del martillo?
- Si llega «G7,50», ¿qué pasa? ¿Y si llega «G?»?
- ¿En qué línea de este programa está la IA que reconoce los gestos?
PARTE 1 · lo importante. Se explica qué mensajes llegan y qué se hace con cada uno. PARTE 2 (pantalla) y PARTE 3 (Bluetooth) no hay que tocarlas.
Cambios guiados: cambia los nombres de los gestos en NOMBRES (máximo 8 letras para que quepan en grande) y mira la pantalla. Después, en la página, cambia también el texto de los botones para que coincida.
/*
Chisme 40 · El Lince aprende tus gestos
La IA vive en el MOVIL: ahi se graban los gestos, se comparan y se adivina cual
has hecho. La placa NO sabe nada de IA: solo recibe el resultado por Bluetooth y
lo ensena en su pantalla, con la seguridad (%) y un contador de cada gesto.
Este programa tiene 3 partes. Solo la PARTE 1 es el chisme:
PARTE 1 · RECIBIR EL RESULTADO <- LEE ESTA. Que mensaje llega y que hacemos con el.
PARTE 2 · LA PANTALLA <- NO ES NECESARIA. Solo escribe el resultado en la OLED.
PARTE 3 · EL BLUETOOTH <- Como en el chisme 38. No hace falta tocarla.
(y al final, setup() y loop())
NO HAY QUE CONECTAR NADA: solo la placa y el movil.
PUERTO SERIE: Herramientas -> Monitor Serie, a 115200 baudios.
Librerias: Adafruit SSD1306 y Adafruit GFX.
Placa en el Arduino IDE: "ESP32 Dev Module".
Pensado para el paquete de placas ESP32 de Espressif version 2.0.x.
Si al compilar dice "Sketch too big":
Herramientas -> Partition Scheme -> "Huge APP (3MB No OTA/1MB SPIFFS)".
*/
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
/* ============================================================
PARTE 1 · RECIBIR EL RESULTADO (lo importante)
El movil ya ha hecho todo el trabajo de "inteligencia". A la placa le manda
mensajes muy cortos:
"G1,87" = he visto el gesto numero 1 (GIRO) con una seguridad del 87 %
"G?" = no se que gesto ha sido (no se parece a nada de lo que aprendi)
"R" = pon los contadores a cero
La placa guarda cuantas veces ha visto cada gesto.
============================================================ */
const int NUM_GESTOS = 3;
const char *NOMBRES[NUM_GESTOS] = {"VAIVEN", "GIRO", "MARTILLO"}; // el orden es el mismo que en la pagina
int cuenta[NUM_GESTOS] = {0, 0, 0}; // cuantas veces ha salido cada gesto
int desconocidos = 0; // cuantas veces la IA ha dicho "no se"
int ultimo = -2; // -2 = aun nada, -1 = "no se", 0..2 = el gesto
int seguridad = 0; // 0 a 100
void atenderMensaje(String m) {
m.trim();
if (m.length() == 0) return;
if (m[0] == 'R') { // R = vuelta a cero
for (int i = 0; i < NUM_GESTOS; i++) cuenta[i] = 0;
desconocidos = 0;
ultimo = -2;
return;
}
if (m[0] != 'G') return; // lo demas no nos interesa
if (m.length() > 1 && m[1] == '?') { // "G?"
ultimo = -1;
seguridad = 0;
desconocidos++;
} else { // "G1,87"
int coma = m.indexOf(',');
int gesto = m.substring(1, coma > 0 ? coma : m.length()).toInt();
int pct = coma > 0 ? m.substring(coma + 1).toInt() : 0;
if (gesto < 0 || gesto >= NUM_GESTOS) return; // un numero que no existe: lo ignoramos
ultimo = gesto;
seguridad = constrain(pct, 0, 100);
cuenta[gesto]++;
}
Serial.println(String("Gesto: ") + (ultimo >= 0 ? NOMBRES[ultimo] : "no se") + " seguridad " + seguridad + " %");
}
/* ============================================================
PARTE 2 · LA PANTALLA (NO ES NECESARIA: lo importante llega por Bluetooth)
Escribe el gesto en grande, la seguridad y los contadores.
============================================================ */
#define OLED_SDA 21
#define OLED_SCL 22
#define OLED_RST 16
#define OLED_ADDR 0x3C
Adafruit_SSD1306 display(128, 64, &Wire, OLED_RST);
volatile bool conectado = false; // ¿hay un movil conectado?
void pintarPantalla() {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 0);
display.print(conectado ? "GESTOS - conectado" : "GESTOS - esperando");
display.setTextSize(2);
display.setCursor(0, 16);
if (ultimo >= 0) display.print(NOMBRES[ultimo]);
else if (ultimo == -1) display.print("NO SE");
else display.print("...");
display.setTextSize(1);
display.setCursor(0, 36);
if (ultimo >= 0) display.print(String("Seguridad ") + seguridad + " %");
display.setCursor(0, 54);
display.print(String("V") + cuenta[0] + " G" + cuenta[1] + " M" + cuenta[2] + " ?" + desconocidos);
display.display();
}
void arrancarPantalla() {
Wire.begin(OLED_SDA, OLED_SCL);
pinMode(OLED_RST, OUTPUT);
digitalWrite(OLED_RST, LOW); delay(20); digitalWrite(OLED_RST, HIGH);
display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR);
}
/* ============================================================
PARTE 3 · EL BLUETOOTH (como en el chisme 38)
La placa se anuncia como "Heltec-Gestos". Solo tiene un "buzon":
RX: por aqui LLEGAN los mensajes de la pagina ("G1,87", "G?", "R").
============================================================ */
#define SERVICE_UUID "6e400001-b5a3-f393-e0a9-e50e24dcca9e"
#define UUID_RX "6e400002-b5a3-f393-e0a9-e50e24dcca9e"
String mensajePendiente = ""; // lo llegado del movil (lo atiende loop())
volatile bool hayMensaje = false;
class AlConectar : public BLEServerCallbacks {
void onConnect(BLEServer *s) { conectado = true; }
void onDisconnect(BLEServer *s) { conectado = false; BLEDevice::startAdvertising(); }
};
class AlEscribir : public BLECharacteristicCallbacks { // llega algo desde la pagina
void onWrite(BLECharacteristic *c) {
mensajePendiente = String(c->getValue().c_str()); // lo guardamos; loop() lo atendera
hayMensaje = true;
}
};
void arrancarBluetooth() {
BLEDevice::init("Heltec-Gestos");
BLEServer *servidor = BLEDevice::createServer();
servidor->setCallbacks(new AlConectar());
BLEService *svc = servidor->createService(SERVICE_UUID);
BLECharacteristic *rx = svc->createCharacteristic(UUID_RX,
BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_WRITE_NR);
rx->setCallbacks(new AlEscribir());
svc->start();
BLEAdvertising *pub = BLEDevice::getAdvertising();
pub->addServiceUUID(SERVICE_UUID);
pub->setScanResponse(true);
BLEDevice::startAdvertising();
}
/* ============================================================
setup() se ejecuta UNA vez al encender. loop() se repite sin parar.
============================================================ */
void setup() {
Serial.begin(115200);
arrancarPantalla();
arrancarBluetooth();
pintarPantalla();
Serial.println("Listo. Conecta el movil desde la pagina.");
}
unsigned long ultimaPantalla = 0;
void loop() {
if (hayMensaje) { // ¿ha llegado algo del movil?
hayMensaje = false;
atenderMensaje(mensajePendiente);
pintarPantalla(); // y lo ensenamos enseguida
ultimaPantalla = millis();
}
if (millis() - ultimaPantalla >= 500) { // si no, refrescamos cada medio segundo
ultimaPantalla = millis();
pintarPantalla();
}
delay(5);
}
La página del móvil
Aquí vive la IA. La página graba los gestos, los resume, los compara y decide. Antes de pegarla, responde en tu cuaderno:
- Con 10 ejemplos de cada gesto y k = 3, ¿cuántos ejemplos compara la IA con el gesto nuevo? ¿Y cuántos votan?
- Si en la autoevaluación el vaivén se confunde con el martillo, ¿qué rasgo crees que les parece igual a la IA?
- ¿Qué crees que pasa con k = 1? ¿Y con k = 5?
PARTE 1 · la IA. Es la que hay que leer: los 6 rasgos, la distancia, la votación y el «no sé». PARTE 2 graba el movimiento. PARTE 3 (pantalla y botones) y PARTE 4 (Bluetooth) no hay que tocarlas.
Cambios guiados: en umbralDesconocido() cambia el 3 por un 1,5 y mira cuántas veces dice «no sé». Después prueba con un 6. ¿Cuál es el valor más útil?
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Chisme 40 · El Lince aprende tus gestos</title>
<style>
/* Los colores, en un solo sitio: cámbialos y cambia toda la página */
:root { --fondo:#eef2ef; --texto:#10201c; --suave:#4d625b; --verde:#1f5b4a; --linea:#7b3fa0;
--aviso:#ffd23f; --tarjeta:#ffffff; --borde:#c5d2cc; --ejeX:#d9480f; --ejeY:#1971c2; --ejeZ:#2b8a3e; }
@media (prefers-color-scheme: dark) {
:root { --fondo:#0f1a17; --texto:#e8f1ed; --suave:#9db3aa; --verde:#5fc4a2; --linea:#c79bff;
--tarjeta:#16251f; --borde:#2d443b; --ejeX:#ff8c5a; --ejeY:#5aa9ff; --ejeZ:#5fd37a; }
}
body { margin:0; padding:1.5rem 1rem; background:var(--fondo); color:var(--texto);
font:18px/1.45 system-ui, sans-serif; text-align:center; }
h1 { font-size:1.5rem; margin:0 0 .3rem; }
h2 { font-size:1.15rem; margin:1.6rem 0 .4rem; }
p { color:var(--suave); margin:.3rem 0; }
button { font:inherit; font-weight:700; padding:.7rem 1.1rem; margin:.3rem;
border:2px solid var(--verde); background:var(--verde); color:var(--fondo);
border-radius:.9rem; cursor:pointer; touch-action:manipulation; }
button.claro { background:transparent; color:var(--verde); }
button:disabled { opacity:.45; cursor:not-allowed; }
select { font:inherit; padding:.3rem; border-radius:.5rem; }
.tarjetas { display:flex; flex-wrap:wrap; gap:.7rem; justify-content:center; max-width:40rem; margin:.6rem auto; }
.tarjeta { background:var(--tarjeta); border:2px solid var(--borde); border-radius:1rem; padding:.7rem; width:11rem; }
.tarjeta b { font-size:1.1rem; }
.tarjeta p { font-size:.9rem; min-height:3.6rem; }
.contador { font-weight:700; color:var(--linea); }
#simulado { display:none; background:var(--aviso); color:#10201c; font-weight:700; padding:.6rem 1rem;
border-radius:.9rem; max-width:34rem; margin:.8rem auto; }
#simulado.visible { display:block; }
#mensaje { font-weight:700; min-height:1.6rem; color:var(--texto); }
#resultado { font-size:3rem; font-weight:700; color:var(--linea); min-height:4rem; margin-top:.6rem; }
#resultado.callado { color:var(--suave); font-size:1.6rem; padding-top:1rem; }
#seguridad { min-height:1.6rem; }
canvas { width:100%; max-width:34rem; height:9rem; display:block; margin:.6rem auto; border-bottom:2px solid var(--suave); }
table { margin:.5rem auto; border-collapse:collapse; font-size:.95rem; }
th, td { border:1px solid var(--borde); padding:.25rem .6rem; }
td.diagonal { font-weight:700; color:var(--verde); }
.fila { max-width:34rem; margin:.6rem auto; display:flex; align-items:center; gap:.6rem;
justify-content:center; flex-wrap:wrap; }
.fila label { color:var(--suave); }
#rasgos { font-family:ui-monospace, monospace; font-size:.9rem; color:var(--suave); }
.oculto { display:none; }
.leyenda span { font-weight:700; margin:0 .4rem; }
</style>
</head>
<body>
<h1>El Lince aprende tus gestos</h1>
<p>Enséñale tres gestos con el móvil en la mano. Después hará otro gesto y la IA dirá cuál ha sido.</p>
<p id="sensor">Sensor de movimiento: comprobando…</p>
<button id="botonSimular" class="claro">Probar sin móvil (datos simulados)</button>
<div id="simulado">DATOS SIMULADOS: los gestos los inventa la página. Sirve para probar en un ordenador.</div>
<div class="fila oculto" id="filaSimulada">
<label for="gestoSimulado">Gesto simulado al probar</label>
<select id="gestoSimulado">
<option value="0">Vaivén</option><option value="1">Giro</option><option value="2">Martillo</option>
</select>
</div>
<h2>1 · Enséñale</h2>
<p>Pulsa «Grabar», espera el pitido agudo y haz el gesto durante un segundo y medio. Graba al menos 3 de cada uno (mejor 8 o 10).</p>
<div class="tarjetas">
<div class="tarjeta"><b>Vaivén</b><p>Móvil adelante y atrás, muy rápido, varias veces.</p>
<div class="contador" id="cuenta0">0 ejemplos</div><button data-grabar="0">Grabar</button></div>
<div class="tarjeta"><b>Giro</b><p>Móvil tumbado, lo giras despacio hasta ponerlo de pie.</p>
<div class="contador" id="cuenta1">0 ejemplos</div><button data-grabar="1">Grabar</button></div>
<div class="tarjeta"><b>Martillo</b><p>Subes el móvil y lo bajas de golpe, como clavando; y lo dejas quieto.</p>
<div class="contador" id="cuenta2">0 ejemplos</div><button data-grabar="2">Grabar</button></div>
</div>
<p id="mensaje"></p>
<p id="faltan"></p>
<canvas id="grafica" width="680" height="180" aria-label="Gráfica de la última grabación: tres líneas, una por eje"></canvas>
<p class="leyenda">Última grabación: <span style="color:var(--ejeX)">x</span><span style="color:var(--ejeY)">y</span><span style="color:var(--ejeZ)">z</span></p>
<p id="rasgos">Los números que ve la IA aparecerán aquí.</p>
<h2>2 · Pruébala</h2>
<button id="botonProbar" disabled>Probar un gesto</button>
<div id="resultado" class="callado" aria-live="polite">Esperando…</div>
<div id="seguridad"></div>
<div id="preguntaReal" class="oculto">
<p>¿Qué gesto hiciste de verdad?</p>
<button data-real="0" class="claro">Vaivén</button>
<button data-real="1" class="claro">Giro</button>
<button data-real="2" class="claro">Martillo</button>
</div>
<h2>3 · ¿Acierta?</h2>
<p><b>Autoevaluación</b>: la IA adivina cada ejemplo que grabaste usando solo los demás.</p>
<div id="tablaAuto"></div>
<p><b>Pruebas de verdad</b>: lo que dijo la IA en tus pruebas, frente a lo que tú dijiste que habías hecho.</p>
<div id="tablaPruebas"></div>
<div class="fila">
<label for="k">Vecinos que votan (k)</label>
<select id="k"><option>1</option><option selected>3</option><option>5</option></select>
</div>
<button id="botonBorrar" class="claro">Borrar todos los ejemplos</button>
<button id="botonCSV" class="claro" disabled>Descargar CSV</button>
<h2>Heltec (opcional)</h2>
<p>La placa enseña en su pantalla lo que dice la IA. La IA sigue estando en el móvil.</p>
<p id="estado">Sin conectar.</p>
<button id="botonConectar">Conectar Heltec</button>
<script>
/* ============================================================
PARTE 1 · LA IA (léela: son unas 60 líneas, no hay magia)
Cada gesto dura 1,5 s y son ~90 lecturas del acelerómetro: [x, y, z] en m/s².
Eso es demasiado para comparar, así que lo RESUMIMOS en 6 números (los "rasgos").
Después, para adivinar un gesto nuevo, buscamos cuáles de los ejemplos guardados
tienen los rasgos más parecidos (los "vecinos más cercanos") y ELLOS votan.
============================================================ */
const NOMBRES = ["Vaivén", "Giro", "Martillo"];
const RASGOS = ["sd_mayor", "sd_medio", "sd_menor", "inclinacion", "pico", "golpes"];
const MINIMO = 3; // ejemplos mínimos de cada gesto para poder probar
let ejemplos = []; // cada uno: { g: gesto (0..2), r: [6 rasgos] }
const media = a => a.reduce((s, v) => s + v, 0) / a.length;
const desviacion = a => { const m = media(a); return Math.sqrt(media(a.map(v => (v - m) ** 2))); };
// De ~90 lecturas a 6 números. d = [[ms, x, y, z], ...]
function rasgos(d) {
const ejes = [1, 2, 3].map(i => d.map(f => f[i]));
// 1-3. ¿Cuánto se mueve cada eje? Los ordenamos de mayor a menor para que
// no importe en qué posición agarres el móvil.
const sd = ejes.map(desviacion).sort((a, b) => b - a);
// 4. Cambio de inclinación: cuánto se mueve la gravedad entre el principio y el final.
const k = Math.max(1, Math.round(d.length * 0.15));
const ini = ejes.map(e => media(e.slice(0, k))), fin = ejes.map(e => media(e.slice(-k)));
const inclinacion = Math.hypot(fin[0] - ini[0], fin[1] - ini[1], fin[2] - ini[2]);
// 5. Pico: el mayor "tirón" respecto a la fuerza media (sin gravedad, los tirones son lo que sobra).
const modulo = d.map(f => Math.hypot(f[1], f[2], f[3]));
const mm = media(modulo);
const tiron = modulo.map(v => Math.abs(v - mm));
const pico = Math.max(...tiron);
// 6. Golpes: cuántas veces el tirón sube por encima de un umbral (con "histéresis" para no contar de más).
const umbral = Math.max(2, 0.4 * pico);
let golpes = 0, arriba = false;
for (const v of tiron) {
if (!arriba && v > umbral) { arriba = true; golpes++; }
else if (arriba && v < umbral * 0.5) arriba = false;
}
return [...sd, inclinacion, pico, golpes];
}
// Cada rasgo tiene unidades y tamaños distintos: dividimos por su desviación para que ninguno "mande" más.
function escalas(lista) {
return RASGOS.map((_, i) => Math.max(desviacion(lista.map(e => e.r[i])), 0.1));
}
function distancia(a, b, esc) {
let s = 0;
for (let i = 0; i < a.length; i++) { const t = (a[i] - b[i]) / esc[i]; s += t * t; }
return Math.sqrt(s);
}
// Adivina un gesto: los k ejemplos más parecidos votan (los más cercanos pesan más).
function clasificar(r, lista, k, esc) {
const orden = lista.map(e => ({ g: e.g, d: distancia(r, e.r, esc) })).sort((a, b) => a.d - b.d);
const vecinos = orden.slice(0, Math.min(k, orden.length));
const votos = [0, 0, 0];
vecinos.forEach(v => { votos[v.g] += 1 / (v.d + 0.05); });
const total = votos[0] + votos[1] + votos[2];
const g = votos.indexOf(Math.max(...votos));
return { g: g, pct: Math.round(100 * votos[g] / total), dmin: orden[0].d };
}
// ¿A qué distancia deja de parecerse a algo conocido? 3 veces lo normal entre ejemplos del mismo gesto.
function umbralDesconocido(lista, esc) {
const cercanas = [];
lista.forEach((e, i) => {
let mejor = Infinity;
lista.forEach((o, j) => { if (i !== j && o.g === e.g) mejor = Math.min(mejor, distancia(e.r, o.r, esc)); });
if (mejor < Infinity) cercanas.push(mejor);
});
return cercanas.length ? 3 * media(cercanas) : Infinity;
}
// Resultado completo: un gesto, o 3 = "no sé".
function adivinar(r) {
const esc = escalas(ejemplos);
const c = clasificar(r, ejemplos, kActual(), esc);
if (c.dmin > umbralDesconocido(ejemplos, esc)) return { g: 3, pct: 0 };
return c;
}
/* ============================================================
PARTE 2 · LEER Y GRABAR EL MOVIMIENTO
El navegador nos da la aceleración del móvil (con la gravedad incluida) unas 60 veces
por segundo. Mientras grabamos, las lecturas se van guardando en una lista.
============================================================ */
const DURACION = 1500; // ms que dura cada gesto
let capturando = null, hayMovimiento = false, ocupado = false;
window.addEventListener("devicemotion", function (e) {
const a = e.accelerationIncludingGravity;
if (!a || a.x === null || a.x === undefined) return;
hayMovimiento = true;
if (capturando) capturando.datos.push([performance.now() - capturando.t0, a.x, a.y, a.z]);
});
async function permisoMovimiento() { // iPhone: hay que pedir permiso al pulsar un botón
if (typeof DeviceMotionEvent !== "undefined" && typeof DeviceMotionEvent.requestPermission === "function") {
const r = await DeviceMotionEvent.requestPermission();
if (r !== "granted") throw new Error("Sin permiso para leer el movimiento.");
}
}
async function capturar(gestoSimulado) {
prepararAudio();
await permisoMovimiento();
decir("Prepárate: coloca el móvil…");
await espera(1500);
pitar(880, 0.12); // pitido agudo: ¡ya!
decir("¡YA! Haz el gesto");
let datos;
if (simulando) { await espera(DURACION); datos = simular(gestoSimulado); }
else {
capturando = { t0: performance.now(), datos: [] };
await espera(DURACION);
datos = capturando.datos; capturando = null;
}
pitar(440, 0.2); // pitido grave: se acabó
if (datos.length < 15) throw new Error("El móvil no ha dado datos de movimiento. ¿Usas Chrome en un móvil? En un ordenador, prueba «Probar sin móvil».");
return datos;
}
/* ============================================================
PARTE 3 · LA PANTALLA Y LOS BOTONES (NO ES NECESARIA para que la IA funcione)
============================================================ */
const espera = ms => new Promise(r => setTimeout(r, ms));
const kActual = () => parseInt(document.getElementById("k").value, 10);
const decir = t => document.getElementById("mensaje").textContent = t;
let ultimoRasgos = null, ultimaPrediccion = null;
let pruebas = [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]; // filas: lo que hiciste; columnas: lo que dijo la IA (3 = "no sé")
// Un pitido corto (Web Audio): así sabes cuándo empezar sin mirar la pantalla.
let audio = null;
function prepararAudio() {
if (!audio && window.AudioContext) audio = new AudioContext();
if (audio && audio.state === "suspended") audio.resume();
}
function pitar(hz, seg) {
if (!audio) return;
const o = audio.createOscillator(), g = audio.createGain();
o.frequency.value = hz; g.gain.value = 0.2;
o.connect(g); g.connect(audio.destination);
o.start(); o.stop(audio.currentTime + seg);
}
function dibujar(d) {
const lienzo = document.getElementById("grafica"), c = lienzo.getContext("2d");
const estilo = getComputedStyle(document.body);
c.clearRect(0, 0, lienzo.width, lienzo.height);
if (!d) return;
const max = Math.max(15, ...d.map(f => Math.max(Math.abs(f[1]), Math.abs(f[2]), Math.abs(f[3])))) * 1.1;
const tFin = d[d.length - 1][0] || 1;
[["--ejeX", 1], ["--ejeY", 2], ["--ejeZ", 3]].forEach(function (par) {
c.beginPath(); c.strokeStyle = estilo.getPropertyValue(par[0]); c.lineWidth = 2.5;
d.forEach((f, i) => {
const x = f[0] / tFin * lienzo.width, y = lienzo.height / 2 - f[par[1]] / max * lienzo.height / 2;
if (i === 0) c.moveTo(x, y); else c.lineTo(x, y);
});
c.stroke();
});
}
function pintarTabla(id, m, titulo) {
const cab = ["Vaivén", "Giro", "Martillo", "No sé"];
let html = "<table><tr><th>" + titulo + "</th>" + cab.map(n => "<th>" + n + "</th>").join("") + "</tr>";
let bien = 0, total = 0;
m.forEach(function (fila, i) {
html += "<tr><th>" + NOMBRES[i] + "</th>" + fila.map((v, j) => "<td" + (i === j ? ' class="diagonal"' : "") + ">" + v + "</td>").join("") + "</tr>";
fila.forEach((v, j) => { total += v; if (i === j) bien += v; });
});
html += "</table><p>" + (total ? "Aciertos: " + bien + " de " + total + " (" + Math.round(100 * bien / total) + " %)" : "Todavía sin datos.") + "</p>";
document.getElementById(id).innerHTML = html;
}
function autoevaluar() { // "dejar uno fuera": cada ejemplo se adivina con los demás
const m = [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]];
if (ejemplos.length >= 6) {
ejemplos.forEach(function (e, i) {
const resto = ejemplos.filter((_, j) => j !== i);
const esc = escalas(resto);
const c = clasificar(e.r, resto, kActual(), esc);
m[e.g][c.dmin > umbralDesconocido(resto, esc) ? 3 : c.g]++;
});
}
pintarTabla("tablaAuto", m, "Hice ↓ · IA dijo →");
}
function actualizar() {
const cuentas = [0, 1, 2].map(g => ejemplos.filter(e => e.g === g).length);
cuentas.forEach((n, g) => document.getElementById("cuenta" + g).textContent = n + (n === 1 ? " ejemplo" : " ejemplos"));
const listo = cuentas.every(n => n >= MINIMO);
document.getElementById("botonProbar").disabled = !listo;
document.getElementById("botonCSV").disabled = ejemplos.length === 0;
document.getElementById("faltan").textContent = listo ? "" : "Faltan ejemplos: necesito al menos " + MINIMO + " de cada gesto.";
autoevaluar();
pintarTabla("tablaPruebas", pruebas, "Hice ↓ · IA dijo →");
}
function mostrarRasgos(r) {
document.getElementById("rasgos").textContent = RASGOS.map((n, i) => n + " " + r[i].toFixed(1)).join(" · ");
}
document.querySelectorAll("[data-grabar]").forEach(function (b) {
b.onclick = async function () {
if (ocupado) return;
ocupado = true;
const g = parseInt(b.dataset.grabar, 10);
try {
const datos = await capturar(g);
const r = rasgos(datos);
ejemplos.push({ g: g, r: r });
dibujar(datos); mostrarRasgos(r);
decir("Guardado un ejemplo de «" + NOMBRES[g] + "».");
} catch (e) { decir(e.message); }
ocupado = false;
actualizar();
};
});
document.getElementById("botonProbar").onclick = async function () {
if (ocupado) return;
ocupado = true;
document.getElementById("preguntaReal").classList.add("oculto");
try {
const datos = await capturar(parseInt(document.getElementById("gestoSimulado").value, 10));
const r = rasgos(datos);
dibujar(datos); mostrarRasgos(r);
const res = adivinar(r);
ultimaPrediccion = res.g;
const cuadro = document.getElementById("resultado");
cuadro.classList.remove("callado");
cuadro.textContent = res.g === 3 ? "No sé qué ha sido" : NOMBRES[res.g];
document.getElementById("seguridad").textContent = res.g === 3 ? "No se parece a nada de lo que le enseñaste." : "Seguridad: " + res.pct + " %";
enviar(res.g === 3 ? "G?" : "G" + res.g + "," + res.pct); // y se lo contamos a la placa
decir("");
document.getElementById("preguntaReal").classList.remove("oculto");
} catch (e) { decir(e.message); }
ocupado = false;
};
document.querySelectorAll("[data-real]").forEach(function (b) {
b.onclick = function () { // tú le dices lo que hiciste de verdad: así medimos si acierta
if (ultimaPrediccion === null) return;
pruebas[parseInt(b.dataset.real, 10)][ultimaPrediccion]++;
ultimaPrediccion = null;
document.getElementById("preguntaReal").classList.add("oculto");
actualizar();
};
});
document.getElementById("k").onchange = actualizar;
document.getElementById("botonBorrar").onclick = function () {
ejemplos = []; pruebas = [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]; ultimaPrediccion = null;
dibujar(null);
document.getElementById("resultado").textContent = "Esperando…";
document.getElementById("resultado").classList.add("callado");
document.getElementById("seguridad").textContent = "";
document.getElementById("rasgos").textContent = "Los números que ve la IA aparecerán aquí.";
document.getElementById("preguntaReal").classList.add("oculto");
enviar("R");
actualizar();
};
document.getElementById("botonCSV").onclick = function () { // se descarga EN TU móvil; no sale a ningún servidor
const texto = ["gesto," + RASGOS.join(",")].concat(ejemplos.map(e => NOMBRES[e.g] + "," + e.r.map(v => v.toFixed(3)).join(","))).join("\n");
const enlace = document.createElement("a");
enlace.href = URL.createObjectURL(new Blob([texto], { type: "text/csv" }));
enlace.download = "gestos.csv";
enlace.click();
};
setTimeout(function () {
document.getElementById("sensor").textContent = hayMovimiento
? "Sensor de movimiento: ✔ recibiendo datos."
: "Sensor de movimiento: no detecto datos (normal en un ordenador). Usa «Probar sin móvil».";
}, 1500);
// Modo SIN MÓVIL: inventamos gestos con un poco de ruido y orientación al azar.
let simulando = false;
const ruido = s => s * Math.sqrt(-2 * Math.log(1 - Math.random())) * Math.cos(2 * Math.PI * Math.random());
const entre = (a, b) => a + Math.random() * (b - a);
function simular(g) {
const G = 9.81, datos = [], dt = 1000 / 60;
const inc = entre(-0.35, 0.35), lado = entre(-0.35, 0.35); // cómo agarra el móvil esta vez
const A = entre(12, 25), f = entre(3, 4.5), fase = entre(0, 6.28); // vaivén
const t0 = entre(0.2, 0.5), dur = entre(0.6, 0.9); // giro
const tm = entre(0.4, 0.8), P = entre(20, 35); // martillo
for (let t = 0; t <= DURACION / 1000; t += dt / 1000) {
let ang = inc, extra = 0;
if (g === 1) { const u = Math.min(1, Math.max(0, (t - t0) / dur)); ang = inc + (u * u * (3 - 2 * u)) * Math.PI / 2; }
let x = G * Math.sin(lado), y = G * Math.sin(ang), z = G * Math.cos(ang);
if (g === 0) y += A * Math.sin(2 * Math.PI * f * t + fase);
if (g === 2) z += P * Math.exp(-(((t - tm) / 0.06) ** 2)) - 0.5 * P * Math.exp(-(((t - tm - 0.12) / 0.06) ** 2));
datos.push([t * 1000, x + ruido(0.3), y + ruido(0.3), z + ruido(0.3)]);
}
return datos;
}
document.getElementById("botonSimular").onclick = function () {
simulando = !simulando;
document.getElementById("simulado").classList.toggle("visible", simulando);
document.getElementById("filaSimulada").classList.toggle("oculto", !simulando);
this.textContent = simulando ? "Dejar de simular" : "Probar sin móvil (datos simulados)";
};
/* ============================================================
PARTE 4 · EL BLUETOOTH (casi igual que en el chisme 38; no hace falta tocarlo)
Solo ENVIAMOS: "G1,87" (gesto y seguridad), "G?" (no sé) y "R" (contadores a cero).
============================================================ */
const SERVICIO = "6e400001-b5a3-f393-e0a9-e50e24dcca9e";
const UUID_RX = "6e400002-b5a3-f393-e0a9-e50e24dcca9e"; // aquí ESCRIBIMOS
let dispositivo, canalOrdenes;
const estado = document.getElementById("estado");
async function enviar(texto) {
if (!canalOrdenes) return; // sin placa conectada, no pasa nada
const datos = new TextEncoder().encode(texto);
try {
if (canalOrdenes.writeValueWithoutResponse) await canalOrdenes.writeValueWithoutResponse(datos);
else await canalOrdenes.writeValue(datos);
} catch (e) { estado.textContent = "No se pudo enviar: " + e.message; }
}
async function abrirCanales() {
for (let intento = 1; intento <= 3; intento++) { // a veces falla la primera vez: reintentamos
try {
const servidor = await dispositivo.gatt.connect();
await espera(600);
const servicio = await servidor.getPrimaryService(SERVICIO);
canalOrdenes = await servicio.getCharacteristic(UUID_RX);
estado.textContent = "Conectada a " + dispositivo.name + ".";
return;
} catch (e) {
if (intento === 3) throw e;
await espera(800);
}
}
}
document.getElementById("botonConectar").onclick = async function () {
if (!navigator.bluetooth) { estado.textContent = "Este navegador no permite Bluetooth. Usa Chrome."; return; }
try {
estado.textContent = "Buscando la placa...";
dispositivo = await navigator.bluetooth.requestDevice({ acceptAllDevices: true, optionalServices: [SERVICIO] });
dispositivo.addEventListener("gattserverdisconnected", () => {
estado.textContent = "Conexión perdida. Pulsa «Conectar Heltec».";
canalOrdenes = null;
});
await abrirCanales();
} catch (e) {
estado.textContent = "Error: " + e.message + " (elige «Heltec-Gestos» en la lista).";
}
};
actualizar();
</script>
</body>
</html>
Cómo probarlo
Primero, la IA sola. Pulsa «Probar sin móvil», graba 8 ejemplos de cada gesto (la página inventa los datos) y pulsa «Probar un gesto» varias veces. Así ves cómo funciona sin mover nada.
Después, con tus gestos de verdad:
- Deja de simular y pulsa «Grabar» en el vaivén. Suena un pitido agudo: haz el gesto durante un segundo y medio. Repite 8 veces.
- Haz lo mismo con el giro y con el martillo.
- Mira la gráfica de cada grabación: el vaivén son olas, el giro es un escalón suave y el martillo, un solo pico.
- Pulsa «Probar un gesto» y haz uno de los tres. La IA dirá cuál cree que es y con qué seguridad.
- Pulsa debajo el gesto que hiciste de verdad. Así la tabla de pruebas va midiendo si acierta.
- Conecta la Heltec y repite: el resultado también sale en su pantalla.
Si algo falla:
- «No detecto datos» de movimiento. Usa Chrome en el móvil y una dirección https. En un ordenador, usa el modo simulado.
- No suena el pitido. Sube el volumen del móvil. El navegador solo deja sonar después de que pulses un botón.
- Dice «no sé» muchas veces. Graba más ejemplos o hazlos más parecidos entre sí. Si grabas un gesto de maneras muy distintas, la IA no sabe qué es «normal».
- Confunde vaivén y martillo. Es esperable. Haz el martillo más brusco y deja el móvil quieto al final, y graba más ejemplos de los dos.
- La placa no cambia. Mira si pone «conectado» en su pantalla. Si no, vuelve a pulsar «Conectar Heltec».
Qué estamos midiendo realmente
La IA no ve tu gesto. Solo ve 6 números que resumen unas 90 lecturas del acelerómetro. En ese resumen se pierde información: dos gestos distintos podrían dar los mismos 6 números, y entonces la IA no puede distinguirlos. Elegir bien qué números usar es la parte más difícil de cualquier clasificador.
El acelerómetro mide aceleración en m/s², con ruido y con diferencias entre móviles. Es un dato, no una verdad exacta. Por eso el mismo gesto, grabado dos veces, nunca da los mismos 6 números: la IA funciona porque los resultados se parecen, no porque sean iguales.
Entrenar no es evaluar
La tabla de autoevaluación pone a prueba a la IA con los mismos ejemplos con que se entrenó (aunque cada uno se adivina usando solo los demás). Suele dar notas altas, porque los ejemplos los hizo la misma persona, el mismo día y de la misma forma.
La tabla de pruebas de verdad es más exigente: gestos nuevos, y tú decides cuál era. Si entre las dos hay mucha diferencia, la IA aprendió tus manías y no los gestos.
Los datos mandan
Si entrenas la IA con los gestos de una persona y la pruebas con otra, empezará a fallar. No porque sea tonta, sino porque no ha visto ejemplos de esa persona. Es el mismo problema que tiene cualquier IA real entrenada con datos poco variados: funciona bien con quien se parece a sus ejemplos y peor con el resto.
Qué significa el porcentaje
La «seguridad» es solo el reparto de votos entre los vecinos. Un 100 % no quiere decir que acierte siempre: quiere decir que todos los vecinos votaron lo mismo. Si hay pocos ejemplos o están mal grabados, la IA puede estar muy segura y equivocarse.
Cuántos vecinos votan
Con k = 1 manda un solo ejemplo, y si es un ejemplo raro arrastra al resultado. Con k más grande el voto es más estable, pero si hay pocos ejemplos de un gesto, los de otro pueden ganar por mayoría. No hay un k perfecto: se elige probando.
Lo que sí y lo que no
Podemos concluir que, con tus ejemplos, la IA distingue estos tres gestos con un porcentaje de aciertos que ves en la tabla. No podemos concluir que reconozca cualquier gesto, a cualquier persona, con cualquier móvil. Es un prototipo educativo.
Para el aula
La clave es usar las dos tablas de aciertos como un cuaderno de laboratorio. Por parejas, con un móvil por pareja.
Actividad 1 · Antes de entrenar. Cada pareja predice, sin tocar el móvil: ¿qué gesto se confundirá más con cuál? ¿Qué rasgo creen que los separa? Después se comprueba en la tabla de autoevaluación.
Actividad 2 · Pocos ejemplos, muchos ejemplos. Graba 3 ejemplos de cada gesto y apunta el porcentaje de aciertos. Sigue hasta 10 y apunta de nuevo. Haz la gráfica: número de ejemplos frente a aciertos. ¿Cuándo deja de mejorar?
Actividad 3 · Cambio de persona. Una pareja entrena la IA. Otra pareja, sin ver las grabaciones, hace los gestos para probarla. Anotan los aciertos en la tabla de pruebas. ¿Son mejores o peores que en la autoevaluación? ¿Por qué?
Actividad 4 · Datos malos a propósito. Entrena un gesto haciendo cada ejemplo muy distinto, o mete un ejemplo de otro gesto etiquetado mal. Mide cuánto cae la tasa de aciertos. Así se ve qué es un dato sucio.
Actividad 5 · El valor de k. Compara aciertos con k = 1, 3 y 5 usando los mismos ejemplos. ¿Cuál funciona mejor? ¿Cambia si tienes pocos ejemplos?
Actividad 6 · La IA que dice «no sé». Haz un gesto que no sea ninguno de los tres (un círculo, por ejemplo). ¿Qué contesta la IA? ¿Preferirías que adivine siempre o que a veces diga que no sabe? ¿En qué casos de la vida real importa esa diferencia?
Actividad 7 · Mirar el CSV. Descarga el CSV y ábrelo en una hoja de cálculo. Calcula la media de cada rasgo para cada gesto. ¿Qué gestos tienen medias parecidas en qué rasgos?
Para discutir: si la IA falla, ¿la culpa es del programa, de los datos o de quien hace el gesto? ¿Cómo lo averiguaríamos?
Retos
Reto 1 · Haz que funcione. Graba 8 ejemplos de cada gesto y consigue más de un 80 % de aciertos en la tabla de pruebas de verdad.
Reto 2 · Modifica algo. Cambia el nombre de los gestos y sus descripciones (en la página y en el programa de la placa). Por ejemplo: «agitar», «volcar» y «golpe».
Reto 3 · Añade un gesto. Un cuarto gesto, como el círculo en el aire. Hay que tocar NOMBRES, las tarjetas, las tablas y el vector votos de la función clasificar(). ¿Cuánto cae el porcentaje de aciertos?
Reto 4 · Un rasgo nuevo. Inventa un séptimo rasgo (por ejemplo, el tiempo que tarda en aparecer el pico) y añádelo a rasgos(). ¿Mejora la autoevaluación?
Reto 5 · Guarda y compara los datos. Descarga el CSV de dos compañeros y compáralos en una hoja de cálculo. ¿Se parecen sus gestos?
Reto 6 · Representa los datos. Dibuja los ejemplos en una gráfica con dos rasgos (por ejemplo, pico frente a golpes). ¿Se forman tres nubes de puntos? ¿Dónde se mezclan?
Reto 7 · Quita la placa de la ecuación. Haz que el móvil pite de forma distinta según el gesto reconocido, sin usar la Heltec. Después, vuelve a añadirla y decide qué aporta cada dispositivo.
Reto 8 · Conviértelo en una pequeña investigación. Plantea una pregunta (¿importa en qué mano llevas el móvil?), diseña el experimento con al menos 5 personas, y escribe una conclusión que diga qué puedes afirmar y qué no.
Qué hemos aprendido
Hemos aprendido que una IA puede ser un programa que cabe en una pantalla y que se puede leer. Aprende de ejemplos: guarda gestos con su etiqueta, compara el nuevo con los que conoce y deja que los más parecidos voten. Hemos visto que tiene que resumir los datos en números, que esos números se igualan para que ninguno mande más que otro, y que dice «no sé» cuando nada se parece.
Y hemos visto que acertar en el laboratorio no es lo mismo que acertar en la vida real: la autoevaluación casi siempre mejora lo que pasa cuando prueba otra persona.
El Lince había grabado ocho martillazos al aire y se estaba quedando afónico de tanto decir «otra vez». En la pantalla de la Heltec, la palabra MARTILLO aparecía una y otra vez, con su porcentaje.
—¿Y esto es una red neuronal? —preguntó, con un poco de decepción.
—No —dijo la Gata—. Es algo mejor para aprender: la puedes leer entera.
—Pero entonces… no es tan misteriosa.
—Ninguna lo es, Lince. Las grandes también aprenden de ejemplos. Solo que tienen muchos más, y no caben en una pantalla.
El Lince miró la placa, luego la huella del barro, que ya se estaba secando, y luego otra vez la placa.
—Una cosa te digo —murmuró—. La próxima vez que alguien me diga que su IA «lo sabe todo», le voy a preguntar con qué ejemplos la entrenó.
—Por fin —sonrió la Gata— una pregunta de científico.

Para seguir investigando
Si quieres entender mejor lo que has usado, estos enlaces te llevan a la fuente. Los dos primeros son la documentación oficial en inglés:
- Acelerómetro del navegador, la propiedad que lee la página (MDN):
- Vecinos más cercanos, la técnica de este chisme, en la biblioteca profesional de Python scikit-learn:
Y otros chismes de la serie relacionados:
- Todos los Chismes del Lince:
- Chismes del Lince 25, El letrero del Lince (la placa y el móvil por Bluetooth):
El siguiente paso natural: la misma idea con otros datos. Si un clasificador de vecinos más cercanos distingue gestos, también podría distinguir el ruido de una cabra frente al de un jabalí, o el movimiento de un collar. Eso lo dejamos para otro chisme.


