Volver al blog

TensorFlow.js: Machine Learning Directamente en el Browser Con JavaScript

Hola HaWkers, ¿y si pudieras ejecutar modelos de machine learning directamente en el navegador del usuario, sin precisar servidor? En 2025, esto no es más futuro - es realidad, y TensorFlow.js es la herramienta que torna esto posible.

En esta guía, vamos a explorar cómo usar TensorFlow.js para crear aplicaciones inteligentes que corren 100% en el client-side.

Por Qué Machine Learning en el Browser

Ejecutar ML en el browser ofrece ventajas únicas que muchos desarrolladores aún no explotan.

Beneficios

Privacidad:

  • Datos nunca salen del dispositivo del usuario
  • Sin preocupaciones con LGPD/GDPR para procesamiento
  • Ideal para aplicaciones sensibles

Performance:

  • Sin latencia de red para inferencia
  • Usa GPU del dispositivo via WebGL
  • Respuestas en tiempo real

Costo:

  • Cero costo de servidor para inferencia
  • Escala ilimitada sin aumentar infraestructura
  • Democratiza acceso a ML

Experiencia:

  • Funciona offline
  • Interactividad en tiempo real
  • No depende de conexión

🧠 Contexto: TensorFlow.js es usado por millones de desarrolladores y empresas como Google, Spotify y Airbnb para ML en el browser.

Primeros Pasos Con TensorFlow.js

Vamos a configurar un proyecto y entender los conceptos básicos.

Instalación

# Via NPM
npm install @tensorflow/tfjs

# O via CDN en el HTML
# <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs"></script>

Conceptos Fundamentales

import * as tf from '@tensorflow/tfjs';

// Tensors: Arrays multidimensionales
const tensor1D = tf.tensor([1, 2, 3, 4]);
const tensor2D = tf.tensor([[1, 2], [3, 4]]);

// Shapes: Dimensiones del tensor
console.log(tensor2D.shape); // [2, 2]

// Operaciones básicas
const a = tf.tensor([1, 2, 3]);
const b = tf.tensor([4, 5, 6]);

const sum = a.add(b);      // [5, 7, 9]
const product = a.mul(b);  // [4, 10, 18]
const mean = a.mean();     // 2

// IMPORTANTE: Limpiar memoria
tensor1D.dispose();
tensor2D.dispose();
// O usar tf.tidy() para limpieza automática

Gerenciamiento de Memoria

// tf.tidy() limpia tensors intermediarios automáticamente
const result = tf.tidy(() => {
  const a = tf.tensor([1, 2, 3]);
  const b = tf.tensor([4, 5, 6]);
  const c = a.add(b);
  const d = c.mul(tf.scalar(2));
  return d; // Apenas d es mantenido
});

// Verificar uso de memoria
console.log(tf.memory());
// { numTensors: X, numDataBuffers: Y, numBytes: Z }

Usando Modelos Pre-Entrenados

La forma más rápida de comenzar es usar modelos ya entrenados.

Clasificación de Imágenes Con MobileNet

import * as tf from '@tensorflow/tfjs';
import * as mobilenet from '@tensorflow-models/mobilenet';

async function classifyImage() {
  // Carga el modelo (primera vez puede demorar)
  const model = await mobilenet.load();

  // Elemento de imagen del DOM
  const img = document.getElementById('myImage');

  // Clasifica la imagen
  const predictions = await model.classify(img);

  console.log(predictions);
  // [
  //   { className: 'golden retriever', probability: 0.89 },
  //   { className: 'Labrador retriever', probability: 0.08 },
  //   { className: 'cocker spaniel', probability: 0.02 }
  // ]
}

Detección de Objetos Con COCO-SSD

import * as cocoSsd from '@tensorflow-models/coco-ssd';

async function detectObjects() {
  const model = await cocoSsd.load();
  const img = document.getElementById('myImage');

  const predictions = await model.detect(img);

  predictions.forEach(prediction => {
    console.log(prediction);
    // {
    //   bbox: [x, y, width, height],
    //   class: 'person',
    //   score: 0.95
    // }
  });

  // Dibujar bounding boxes
  const canvas = document.getElementById('canvas');
  const ctx = canvas.getContext('2d');

  predictions.forEach(pred => {
    ctx.strokeStyle = '#00FF00';
    ctx.lineWidth = 2;
    ctx.strokeRect(...pred.bbox);
    ctx.fillStyle = '#00FF00';
    ctx.fillText(
      `${pred.class} (${Math.round(pred.score * 100)}%)`,
      pred.bbox[0],
      pred.bbox[1] - 5
    );
  });
}

Detección de Pose Con PoseNet/MoveNet

Uno de los casos de uso más populares es la detección de pose corporal.

Implementación Básica

import * as poseDetection from '@tensorflow-models/pose-detection';

async function detectPose() {
  // Configura el detector
  const detectorConfig = {
    modelType: poseDetection.movenet.modelType.SINGLEPOSE_LIGHTNING
  };

  const detector = await poseDetection.createDetector(
    poseDetection.SupportedModels.MoveNet,
    detectorConfig
  );

  // Usa video de la webcam
  const video = document.getElementById('webcam');

  async function detect() {
    const poses = await detector.estimatePoses(video);

    if (poses.length > 0) {
      const pose = poses[0];

      // Keypoints del cuerpo
      pose.keypoints.forEach(keypoint => {
        if (keypoint.score > 0.5) {
          console.log(`${keypoint.name}: (${keypoint.x}, ${keypoint.y})`);
          // nose: (320, 180)
          // left_eye: (310, 170)
          // right_shoulder: (280, 250)
          // etc.
        }
      });
    }

    requestAnimationFrame(detect);
  }

  detect();
}

Aplicación Práctica: Contador de Ejercicios

class ExerciseCounter {
  constructor() {
    this.squatCount = 0;
    this.isDown = false;
  }

  detectSquat(pose) {
    const leftHip = pose.keypoints.find(k => k.name === 'left_hip');
    const leftKnee = pose.keypoints.find(k => k.name === 'left_knee');
    const leftAnkle = pose.keypoints.find(k => k.name === 'left_ankle');

    if (!leftHip || !leftKnee || !leftAnkle) return;

    // Calcula ángulo de la rodilla
    const angle = this.calculateAngle(
      leftHip,
      leftKnee,
      leftAnkle
    );

    // Lógica de conteo
    if (angle < 90 && !this.isDown) {
      this.isDown = true;
    } else if (angle > 160 && this.isDown) {
      this.isDown = false;
      this.squatCount++;
      this.onSquatComplete(this.squatCount);
    }
  }

  calculateAngle(a, b, c) {
    const radians = Math.atan2(c.y - b.y, c.x - b.x) -
                    Math.atan2(a.y - b.y, a.x - b.x);
    let angle = Math.abs(radians * 180 / Math.PI);
    if (angle > 180) angle = 360 - angle;
    return angle;
  }

  onSquatComplete(count) {
    console.log(`¡Sentadilla #${count} completa!`);
  }
}

Reconocimiento de Gestos de Mano

Otro caso de uso poderoso es detectar gestos de las manos.

import * as handPoseDetection from '@tensorflow-models/hand-pose-detection';

async function detectHands() {
  const model = handPoseDetection.SupportedModels.MediaPipeHands;
  const detectorConfig = {
    runtime: 'tfjs',
    maxHands: 2
  };

  const detector = await handPoseDetection.createDetector(
    model,
    detectorConfig
  );

  const video = document.getElementById('webcam');

  async function detect() {
    const hands = await detector.estimateHands(video);

    hands.forEach(hand => {
      // 21 keypoints por mano
      const landmarks = hand.keypoints;

      // Detectar gesto de "pulgar arriba"
      if (isThumbsUp(landmarks)) {
        console.log('👍 ¡Pulgar arriba detectado!');
      }

      // Detectar gesto de "paz"
      if (isPeaceSign(landmarks)) {
        console.log('✌️ ¡Paz detectado!');
      }
    });

    requestAnimationFrame(detect);
  }

  detect();
}

function isThumbsUp(landmarks) {
  const thumbTip = landmarks[4];
  const thumbBase = landmarks[2];
  const indexTip = landmarks[8];

  // Pulgar para arriba y otros dedos cerrados
  const thumbUp = thumbTip.y < thumbBase.y;
  const indexDown = indexTip.y > landmarks[5].y;

  return thumbUp && indexDown;
}

function isPeaceSign(landmarks) {
  const indexTip = landmarks[8];
  const middleTip = landmarks[12];
  const ringTip = landmarks[16];

  // Índice y medio estirados, otros cerrados
  const indexUp = indexTip.y < landmarks[5].y;
  const middleUp = middleTip.y < landmarks[9].y;
  const ringDown = ringTip.y > landmarks[13].y;

  return indexUp && middleUp && ringDown;
}

Clasificación de Texto Con Universal Sentence Encoder

TensorFlow.js no es solo para imágenes - funciona óptimo con texto.

import * as use from '@tensorflow-models/universal-sentence-encoder';

async function analyzeText() {
  const model = await use.load();

  // Embeddings de texto
  const sentences = [
    '¡Adoré este producto, muy bueno!',
    'Pésimo, no recomiendo.',
    'El servicio fue ok, nada excepcional.'
  ];

  const embeddings = await model.embed(sentences);
  console.log(embeddings.shape); // [3, 512]

  // Clasificación de sentimiento simple
  const positiveRef = await model.embed(['Excelente, maravilloso, perfecto']);
  const negativeRef = await model.embed(['Terrible, horrible, pésimo']);

  sentences.forEach(async (sentence, i) => {
    const sentenceEmbed = embeddings.slice([i, 0], [1, 512]);

    const positiveSim = cosineSimilarity(sentenceEmbed, positiveRef);
    const negativeSim = cosineSimilarity(sentenceEmbed, negativeRef);

    const sentiment = positiveSim > negativeSim ? 'Positivo' : 'Negativo';
    console.log(`"${sentence}" -> ${sentiment}`);
  });
}

function cosineSimilarity(a, b) {
  const dotProduct = tf.sum(tf.mul(a, b));
  const normA = tf.norm(a);
  const normB = tf.norm(b);
  return dotProduct.div(normA.mul(normB)).dataSync()[0];
}

Entrenando Tu Propio Modelo

Además de usar modelos pre-entrenados, puedes crear los tuyos.

Modelo Simple de Clasificación

import * as tf from '@tensorflow/tfjs';

async function trainModel() {
  // Datos de ejemplo: XOR problem
  const xs = tf.tensor2d([[0, 0], [0, 1], [1, 0], [1, 1]]);
  const ys = tf.tensor2d([[0], [1], [1], [0]]);

  // Arquitectura del modelo
  const model = tf.sequential({
    layers: [
      tf.layers.dense({
        inputShape: [2],
        units: 8,
        activation: 'relu'
      }),
      tf.layers.dense({
        units: 4,
        activation: 'relu'
      }),
      tf.layers.dense({
        units: 1,
        activation: 'sigmoid'
      })
    ]
  });

  // Compilar
  model.compile({
    optimizer: tf.train.adam(0.1),
    loss: 'binaryCrossentropy',
    metrics: ['accuracy']
  });

  // Entrenar
  await model.fit(xs, ys, {
    epochs: 100,
    callbacks: {
      onEpochEnd: (epoch, logs) => {
        console.log(`Epoch ${epoch}: loss = ${logs.loss.toFixed(4)}`);
      }
    }
  });

  // Testar
  const prediction = model.predict(tf.tensor2d([[0, 1]]));
  prediction.print(); // ~0.95 (próximo a 1)

  // Guardar modelo
  await model.save('localstorage://mi-modelo');
}

// Cargar modelo guardado
async function loadModel() {
  const model = await tf.loadLayersModel('localstorage://mi-modelo');
  return model;
}

Transfer Learning Con MobileNet

async function customImageClassifier() {
  // Carga MobileNet sin la capa final
  const mobilenet = await tf.loadLayersModel(
    'https://tfhub.dev/google/tfjs-model/mobilenet_v2_100_224/feature_vector/3/default/1',
    { fromTFHub: true }
  );

  // Congela pesos de MobileNet
  mobilenet.trainable = false;

  // Agrega capas customizadas
  const model = tf.sequential();
  model.add(mobilenet);
  model.add(tf.layers.dense({
    units: 128,
    activation: 'relu'
  }));
  model.add(tf.layers.dropout({ rate: 0.5 }));
  model.add(tf.layers.dense({
    units: 3, // Número de clases
    activation: 'softmax'
  }));

  model.compile({
    optimizer: tf.train.adam(0.0001),
    loss: 'categoricalCrossentropy',
    metrics: ['accuracy']
  });

  return model;
}

Optimización de Performance

Para ML en el browser funcionar bien, optimización es crucial.

Backend WebGL vs CPU

// Verificar backend actual
console.log(tf.getBackend()); // 'webgl' o 'cpu'

// Forzar WebGL (más rápido)
await tf.setBackend('webgl');

// O usar WebGPU (más rápido aún, si disponible)
if (navigator.gpu) {
  await tf.setBackend('webgpu');
}

Cuantización de Modelos

// Al convertir modelo Python para TensorFlow.js
// Usa cuantización para reducir tamaño

// tensorflowjs_converter \
//   --input_format=tf_saved_model \
//   --output_format=tfjs_graph_model \
//   --quantize_float16=* \
//   ./saved_model \
//   ./tfjs_model

// Resultado: modelo 50% menor, performance similar

Warm-up del Modelo

async function warmupModel(model) {
  // Primera inferencia es más lenta
  // Haz "warm-up" antes de uso real

  const dummyInput = tf.zeros([1, 224, 224, 3]);
  const warmupResult = model.predict(dummyInput);
  warmupResult.dispose();
  dummyInput.dispose();

  console.log('¡Modelo calentado y listo!');
}

Aplicación Completa: Clasificador en Tiempo Real

Vamos a juntar todo en una aplicación funcional.

import * as tf from '@tensorflow/tfjs';
import * as mobilenet from '@tensorflow-models/mobilenet';

class RealTimeClassifier {
  constructor(videoElement, resultsElement) {
    this.video = videoElement;
    this.results = resultsElement;
    this.model = null;
    this.isRunning = false;
  }

  async init() {
    // Configura WebGL
    await tf.setBackend('webgl');

    // Carga modelo
    console.log('Cargando modelo...');
    this.model = await mobilenet.load({
      version: 2,
      alpha: 1.0
    });

    // Calienta modelo
    await this.warmup();

    // Inicia webcam
    await this.setupCamera();

    console.log('¡Listo!');
  }

  async setupCamera() {
    const stream = await navigator.mediaDevices.getUserMedia({
      video: { facingMode: 'environment' }
    });
    this.video.srcObject = stream;
    await new Promise(resolve => {
      this.video.onloadedmetadata = resolve;
    });
    this.video.play();
  }

  async warmup() {
    const dummyInput = tf.zeros([1, 224, 224, 3]);
    const result = this.model.infer(dummyInput, 'conv_preds');
    result.dispose();
    dummyInput.dispose();
  }

  start() {
    this.isRunning = true;
    this.detect();
  }

  stop() {
    this.isRunning = false;
  }

  async detect() {
    if (!this.isRunning) return;

    const predictions = await this.model.classify(this.video, 3);

    this.displayResults(predictions);

    requestAnimationFrame(() => this.detect());
  }

  displayResults(predictions) {
    this.results.innerHTML = predictions
      .map(p => `
        <div class="prediction">
          <span class="label">${p.className}</span>
          <span class="score">${(p.probability * 100).toFixed(1)}%</span>
          <div class="bar" style="width: ${p.probability * 100}%"></div>
        </div>
      `)
      .join('');
  }
}

// Uso
const classifier = new RealTimeClassifier(
  document.getElementById('video'),
  document.getElementById('results')
);

document.getElementById('startBtn').onclick = async () => {
  await classifier.init();
  classifier.start();
};

Conclusión

TensorFlow.js democratiza el acceso a machine learning, permitiendo que cualquier desarrollador JavaScript cree aplicaciones inteligentes sin precisar infraestructura de servidor o conocimiento profundo de Python.

En 2025, con GPUs más poderosas en los dispositivos y modelos cada vez más optimizados, el browser se torna una plataforma legítima para ML - ofreciendo privacidad, performance y escala sin costo de infraestructura.

Si quieres explorar otras formas de mejorar performance en el browser, te recomiendo echar un vistazo al artículo WebAssembly y JavaScript: Performance Extrema donde exploramos cómo combinar estas tecnologías.

¡Vamos a por ello! 🦅

Comentarios (0)

Este artículo aún no tiene comentarios 😢. ¡Sé el primero! 🚀🦅

Añadir comentarios