45. Aplicaciones en inteligencia artificial

La inteligencia artificial resuelve problemas que requieren aprendizaje, razonamiento y decisión. Las relaciones discretas modelan conocimiento, restricciones y preferencias. Desde búsqueda heurística hasta redes neuronales, la IA explota estructuras relacionales para simular inteligencia.

45.1 Introducción

La inteligencia artificial busca programar máquinas que resuelvan problemas complejos. Los enfoques fundamentales incluyen:

  • Búsqueda: Explorar espacios de estados para encontrar soluciones
  • Conocimiento: Modelar hechos y reglas como relaciones lógicas
  • Aprendizaje: Extraer patrones de datos (relaciones entre entrada y salida)
  • Razonamiento: Inferir nuevos hechos a partir de reglas conocidas

Todos estos enfoques tienen un denominador común: estructuras relacionales. Un estado es una tupla de atributos; una regla es una implicación lógica; el aprendizaje es encontrar una relación entre variables; el razonamiento es navegar una relación de inferencia.

45.2 Búsqueda A* con Heurísticas

A* es un algoritmo de búsqueda que combina costo real (distancia desde inicio) con heurística (estimación a meta). Explora el espacio de estados de forma inteligente.

Concepto formal: El espacio de estados es un grafo donde nodos son estados y aristas representan acciones. A* busca el camino de menor f(n) = g(n) + h(n), donde g(n) es costo real y h(n) es heurística.
// Búsqueda A* para navegación en malla 2D
class NodoA {
  constructor(x, y, padre = null, g = 0, h = 0) {
    this.x = x;
    this.y = y;
    this.padre = padre;
    this.g = g; // Costo desde inicio
    this.h = h; // Heurística a meta
  }

  f() {
    return this.g + this.h;
  }

  clave() {
    return `${this.x},${this.y}`;
  }
}

class BuscadorA {
  constructor(malla, meta) {
    this.malla = malla; // malla[y][x] = 0 libre, 1 obstáculo
    this.meta = meta;
  }

  heuristica(x, y) {
    // Distancia Manhattan a meta
    return Math.abs(x - this.meta[0]) + Math.abs(y - this.meta[1]);
  }

  vecinos(x, y) {
    const resultado = [];
    // 8 direcciones (incluidas diagonales)
    const direcciones = [
      [0, -1], [0, 1], [-1, 0], [1, 0],
      [-1, -1], [-1, 1], [1, -1], [1, 1]
    ];

    for (const [dx, dy] of direcciones) {
      const nx = x + dx, ny = y + dy;
      if (nx >= 0 && nx < this.malla[0].length &&
          ny >= 0 && ny < this.malla.length &&
          this.malla[ny][nx] === 0) {
        resultado.push([nx, ny]);
      }
    }
    return resultado;
  }

  buscarA(inicio) {
    const abierto = [new NodoA(inicio[0], inicio[1], null, 0, 
                               this.heuristica(inicio[0], inicio[1]))];
    const cerrado = new Set();
    const visitados = new Map();

    while (abierto.length > 0) {
      // Encontrar nodo con menor f(n)
      let minIdx = 0;
      for (let i = 1; i < abierto.length; i++) {
        if (abierto[i].f() < abierto[minIdx].f()) {
          minIdx = i;
        }
      }

      const actual = abierto.splice(minIdx, 1)[0];

      if (actual.x === this.meta[0] && actual.y === this.meta[1]) {
        // Reconstruir camino
        const camino = [];
        let nodo = actual;
        while (nodo) {
          camino.unshift([nodo.x, nodo.y]);
          nodo = nodo.padre;
        }
        return camino;
      }

      cerrado.add(actual.clave());

      for (const [nx, ny] of this.vecinos(actual.x, actual.y)) {
        const clave = `${nx},${ny}`;
        if (cerrado.has(clave)) continue;

        const g = actual.g + 1;
        const h = this.heuristica(nx, ny);
        const nuevoNodo = new NodoA(nx, ny, actual, g, h);

        // ¿Ya fue visitado con mejor g?
        if (visitados.has(clave) && visitados.get(clave) <= g) {
          continue;
        }

        visitados.set(clave, g);
        abierto.push(nuevoNodo);
      }
    }

    return null; // No hay camino
  }
}

// Uso
const malla = [
  [0, 1, 0, 0, 0],
  [0, 1, 0, 1, 0],
  [0, 0, 0, 1, 0],
  [1, 1, 0, 1, 0],
  [0, 0, 0, 0, 0]
];

const buscador = new BuscadorA(malla, [4, 4]);
const camino = buscador.buscarA([0, 0]);
console.log("Camino A*:", camino);

45.3 Árboles de Decisión

Un árbol de decisión es una estructura jerárquica que particiona el espacio de entrada mediante preguntas (tests) sobre atributos. Cada hoja es una clasificación.

Relación binaria subyacente: La decisión "¿atributo X > valor?" define una relación de orden que particiona los datos.
// Árbol de decisión simple (clasificación binaria)
class NodoDecision {
  constructor(atributo = null, valor = null) {
    this.atributo = atributo;
    this.valor = valor;
    this.izquierda = null;
    this.derecha = null;
    this.clasificacion = null;
  }

  esHoja() {
    return this.clasificacion !== null;
  }
}

class ArbolDecision {
  constructor() {
    this.raiz = null;
  }

  // Calcular entropía (información de un conjunto)
  entropía(ejemplos) {
    const conteos = {};
    for (const ej of ejemplos) {
      const clase = ej.clase;
      conteos[clase] = (conteos[clase] || 0) + 1;
    }

    let entropia = 0;
    const total = ejemplos.length;
    for (const count of Object.values(conteos)) {
      const p = count / total;
      if (p > 0) entropia -= p * Math.log2(p);
    }
    return entropia;
  }

  // Ganancia de información al dividir por atributo
  gananciaInformacion(ejemplos, atributo, valor) {
    const izq = ejemplos.filter(e => e[atributo] < valor);
    const der = ejemplos.filter(e => e[atributo] >= valor);

    if (izq.length === 0 || der.length === 0) return 0;

    const total = ejemplos.length;
    const entropia_original = this.entropía(ejemplos);
    const entropia_nueva = 
      (izq.length / total) * this.entropía(izq) +
      (der.length / total) * this.entropía(der);

    return entropia_original - entropia_nueva;
  }

  // Entrenar árbol (ID3 simplificado)
  entrenar(ejemplos, atributos, nodo = null) {
    if (!nodo) nodo = new NodoDecision();

    // Caso base: todos los ejemplos tienen la misma clase
    const clases = new Set(ejemplos.map(e => e.clase));
    if (clases.size === 1) {
      nodo.clasificacion = ejemplos[0].clase;
      return nodo;
    }

    // Caso base: sin atributos
    if (atributos.length === 0) {
      const conteos = {};
      for (const ej of ejemplos) {
        conteos[ej.clase] = (conteos[ej.clase] || 0) + 1;
      }
      const masFrequente = Object.entries(conteos)
        .reduce((a, b) => a[1] > b[1] ? a : b)[0];
      nodo.clasificacion = masFrequente;
      return nodo;
    }

    // Seleccionar mejor atributo
    let mejorAtributo = null;
    let mejorGanancia = -1;
    let mejorValor = null;

    for (const atributo of atributos) {
      const valores = [...new Set(ejemplos.map(e => e[atributo]))];
      for (const valor of valores) {
        const ganancia = this.gananciaInformacion(ejemplos, atributo, valor);
        if (ganancia > mejorGanancia) {
          mejorGanancia = ganancia;
          mejorAtributo = atributo;
          mejorValor = valor;
        }
      }
    }

    if (mejorGanancia === 0) {
      const conteos = {};
      for (const ej of ejemplos) {
        conteos[ej.clase] = (conteos[ej.clase] || 0) + 1;
      }
      const masFrequente = Object.entries(conteos)
        .reduce((a, b) => a[1] > b[1] ? a : b)[0];
      nodo.clasificacion = masFrequente;
      return nodo;
    }

    nodo.atributo = mejorAtributo;
    nodo.valor = mejorValor;

    const ejemplosIzq = ejemplos.filter(e => e[mejorAtributo] < mejorValor);
    const ejemplosDer = ejemplos.filter(e => e[mejorAtributo] >= mejorValor);

    nodo.izquierda = this.entrenar(ejemplosIzq, atributos, new NodoDecision());
    nodo.derecha = this.entrenar(ejemplosDer, atributos, new NodoDecision());

    return nodo;
  }

  // Clasificar ejemplo
  clasificar(ejemplo, nodo = null) {
    if (!nodo) nodo = this.raiz;
    
    if (nodo.esHoja()) {
      return nodo.clasificacion;
    }

    if (ejemplo[nodo.atributo] < nodo.valor) {
      return this.clasificar(ejemplo, nodo.izquierda);
    } else {
      return this.clasificar(ejemplo, nodo.derecha);
    }
  }
}

// Uso
const arbol = new ArbolDecision();
const datos = [
  { edad: 25, ingresos: 30000, clase: "bajo" },
  { edad: 45, ingresos: 80000, clase: "alto" },
  { edad: 35, ingresos: 50000, clase: "medio" },
  { edad: 50, ingresos: 120000, clase: "alto" },
  { edad: 22, ingresos: 20000, clase: "bajo" }
];

arbol.raiz = arbol.entrenar(datos, ["edad", "ingresos"]);

console.log("Clasificar {edad: 30, ingresos: 40000}:", 
            arbol.clasificar({ edad: 30, ingresos: 40000 }));

45.4 K-Means: Clustering y Relaciones de Similitud

K-Means agrupa datos similares mediante una relación de distancia. Itera asignando puntos al centro más cercano y recalculando centros.

// K-Means clustering
class KMeans {
  constructor(k, maxIteraciones = 100) {
    this.k = k;
    this.maxIteraciones = maxIteraciones;
    this.centros = [];
    this.asignaciones = [];
  }

  distanciaEuclidea(p1, p2) {
    return Math.sqrt(p1.reduce((sum, val, i) => 
      sum + Math.pow(val - p2[i], 2), 0));
  }

  inicializarCentros(datos) {
    // Seleccionar k puntos aleatorios
    const indices = new Set();
    while (indices.size < this.k) {
      indices.add(Math.floor(Math.random() * datos.length));
    }
    this.centros = Array.from(indices).map(i => [...datos[i]]);
  }

  asignarPuntos(datos) {
    this.asignaciones = datos.map(punto => {
      let minDist = Infinity;
      let clusterAsignado = 0;

      for (let i = 0; i < this.centros.length; i++) {
        const dist = this.distanciaEuclidea(punto, this.centros[i]);
        if (dist < minDist) {
          minDist = dist;
          clusterAsignado = i;
        }
      }
      return clusterAsignado;
    });
  }

  actualizarCentros(datos) {
    const nuevosCentros = [];

    for (let i = 0; i < this.k; i++) {
      const puntosCluster = datos.filter((_, j) => this.asignaciones[j] === i);
      
      if (puntosCluster.length === 0) {
        nuevosCentros.push(this.centros[i]);
      } else {
        const nuevoCentro = [];
        for (let d = 0; d < datos[0].length; d++) {
          const media = puntosCluster.reduce((sum, p) => sum + p[d], 0) / puntosCluster.length;
          nuevoCentro.push(media);
        }
        nuevosCentros.push(nuevoCentro);
      }
    }

    return nuevosCentros;
  }

  convergio(nuevosCentros) {
    const umbral = 1e-6;
    for (let i = 0; i < this.k; i++) {
      const dist = this.distanciaEuclidea(this.centros[i], nuevosCentros[i]);
      if (dist > umbral) return false;
    }
    return true;
  }

  entrenar(datos) {
    this.inicializarCentros(datos);

    for (let iter = 0; iter < this.maxIteraciones; iter++) {
      this.asignarPuntos(datos);
      const nuevosCentros = this.actualizarCentros(datos);

      if (this.convergio(nuevosCentros)) {
        console.log(`K-Means convergió en ${iter + 1} iteraciones`);
        break;
      }

      this.centros = nuevosCentros;
    }
  }

  predecir(punto) {
    let minDist = Infinity;
    let cluster = 0;

    for (let i = 0; i < this.centros.length; i++) {
      const dist = this.distanciaEuclidea(punto, this.centros[i]);
      if (dist < minDist) {
        minDist = dist;
        cluster = i;
      }
    }
    return cluster;
  }
}

// Uso
const kmeans = new KMeans(2);
const datos = [
  [1, 1], [1, 2], [2, 1],
  [10, 10], [10, 11], [11, 10]
];

kmeans.entrenar(datos);
console.log("Cluster de [2, 2]:", kmeans.predecir([2, 2]));
console.log("Cluster de [11, 11]:", kmeans.predecir([11, 11]));

45.5 Minimax y Teoría de Juegos

Minimax es un algoritmo que encuentra la mejor estrategia en juegos de dos jugadores, asumiendo que ambos juegan óptimamente. La relación subyacente es de preferencia y utilidad.

Concepto: En un árbol de juego, maximizador y minimizador alternan. Minimax busca el nodo hoja que maximiza el mínimo que el oponente puede forzar.
// Minimax para Tic-Tac-Toe
class TicTacToe {
  constructor() {
    this.tablero = Array(9).fill(null);
    this.jugadorIA = "O";
    this.jugadorHumano = "X";
  }

  movimientosDisponibles() {
    return this.tablero
      .map((val, idx) => val === null ? idx : null)
      .filter(val => val !== null);
  }

  ganador() {
    const lineas = [
      [0, 1, 2], [3, 4, 5], [6, 7, 8],
      [0, 3, 6], [1, 4, 7], [2, 5, 8],
      [0, 4, 8], [2, 4, 6]
    ];

    for (const [a, b, c] of lineas) {
      if (this.tablero[a] && 
          this.tablero[a] === this.tablero[b] && 
          this.tablero[a] === this.tablero[c]) {
        return this.tablero[a];
      }
    }
    return null;
  }

  esTerminal() {
    return this.ganador() !== null || this.movimientosDisponibles().length === 0;
  }

  minimax(profundidad, esMaximizador) {
    const ganador = this.ganador();

    if (ganador === this.jugadorIA) return 10 - profundidad;
    if (ganador === this.jugadorHumano) return profundidad - 10;
    if (this.movimientosDisponibles().length === 0) return 0;

    if (esMaximizador) {
      let maxEval = -Infinity;
      for (const mov of this.movimientosDisponibles()) {
        this.tablero[mov] = this.jugadorIA;
        const eval_ = this.minimax(profundidad + 1, false);
        maxEval = Math.max(maxEval, eval_);
        this.tablero[mov] = null;
      }
      return maxEval;
    } else {
      let minEval = Infinity;
      for (const mov of this.movimientosDisponibles()) {
        this.tablero[mov] = this.jugadorHumano;
        const eval_ = this.minimax(profundidad + 1, true);
        minEval = Math.min(minEval, eval_);
        this.tablero[mov] = null;
      }
      return minEval;
    }
  }

  mejorMovimiento() {
    let mejorEval = -Infinity;
    let mejorMov = null;

    for (const mov of this.movimientosDisponibles()) {
      this.tablero[mov] = this.jugadorIA;
      const eval_ = this.minimax(0, false);
      this.tablero[mov] = null;

      if (eval_ > mejorEval) {
        mejorEval = eval_;
        mejorMov = mov;
      }
    }

    return mejorMov;
  }
}

// Uso
const juego = new TicTacToe();
console.log("Mejor movimiento inicial:", juego.mejorMovimiento()); // 4 (centro)

45.6 Redes Neuronales Simples

Una red neuronal es un grafo dirigido donde nodos son neuronas y aristas tienen pesos (relación funcional). El aprendizaje ajusta pesos mediante gradiente descendente.

// Red neuronal simple (perceptrón multicapa)
class Red {
  constructor(capas) {
    this.capas = [];
    
    for (let i = 0; i < capas.length - 1; i++) {
      const capa = {
        pesos: Array(capas[i]).fill(null).map(() =>
          Array(capas[i + 1]).fill(null).map(() => Math.random() * 2 - 1)
        ),
        sesgos: Array(capas[i + 1]).fill(null).map(() => Math.random() * 2 - 1)
      };
      this.capas.push(capa);
    }
  }

  sigmoid(x) {
    return 1 / (1 + Math.exp(-Math.max(-500, Math.min(500, x))));
  }

  sigmoidDerivada(x) {
    return x * (1 - x);
  }

  propagarAdelante(entrada) {
    let activacion = [...entrada];
    this.activaciones = [activacion];

    for (const capa of this.capas) {
      const nuevaActivacion = Array(capa.sesgos.length).fill(0);

      for (let i = 0; i < activacion.length; i++) {
        for (let j = 0; j < capa.pesos[i].length; j++) {
          nuevaActivacion[j] += activacion[i] * capa.pesos[i][j];
        }
      }

      for (let j = 0; j < nuevaActivacion.length; j++) {
        nuevaActivacion[j] = this.sigmoid(nuevaActivacion[j] + capa.sesgos[j]);
      }

      this.activaciones.push(nuevaActivacion);
      activacion = nuevaActivacion;
    }

    return activacion;
  }

  propagarAtras(salida, tasaAprendizaje) {
    const L = this.capas.length;
    let delta = salida.map((out, i) => 
      (out - this.activaciones[L][i]) * this.sigmoidDerivada(this.activaciones[L][i])
    );

    for (let l = L - 1; l >= 0; l--) {
      const nuevoDelta = Array(this.activaciones[l].length).fill(0);

      for (let i = 0; i < this.activaciones[l].length; i++) {
        for (let j = 0; j < this.capas[l].pesos[i].length; j++) {
          this.capas[l].pesos[i][j] -= tasaAprendizaje * delta[j] * this.activaciones[l][i];
          nuevoDelta[i] += delta[j] * this.capas[l].pesos[i][j];
        }
      }

      for (let j = 0; j < this.capas[l].sesgos.length; j++) {
        this.capas[l].sesgos[j] -= tasaAprendizaje * delta[j];
      }

      if (l > 0) {
        delta = nuevoDelta.map((d, i) => d * this.sigmoidDerivada(this.activaciones[l][i]));
      }
    }
  }

  entrenar(datos, epocas = 100, tasaAprendizaje = 0.5) {
    for (let e = 0; e < epocas; e++) {
      for (const [entrada, salida] of datos) {
        this.propagarAdelante(entrada);
        this.propagarAtras(salida, tasaAprendizaje);
      }
    }
  }

  predecir(entrada) {
    return this.propagarAdelante(entrada);
  }
}

// Uso: XOR
const red = new Red([2, 4, 1]);
const datosXOR = [
  [[0, 0], [0]],
  [[0, 1], [1]],
  [[1, 0], [1]],
  [[1, 1], [0]]
];

red.entrenar(datosXOR, 1000, 1);

console.log("XOR(0,0):", red.predecir([0, 0])[0].toFixed(2)); // ~0.0
console.log("XOR(0,1):", red.predecir([0, 1])[0].toFixed(2)); // ~1.0
console.log("XOR(1,1):", red.predecir([1, 1])[0].toFixed(2)); // ~0.0

45.7 Sistemas de Reglas y Lógica

Un sistema de reglas modelan conocimiento como implicaciones lógicas: Si (condición) entonces (conclusión). Las reglas forman una relación de inferencia.

// Sistema simple de reglas (motor de inferencia)
class SistemaReglas {
  constructor() {
    this.reglas = [];
    this.hechos = new Set();
  }

  agregarRegla(condiciones, conclusiones) {
    this.reglas.push({ condiciones, conclusiones });
  }

  agregarHecho(hecho) {
    this.hechos.add(hecho);
  }

  hechosCoinciden(condiciones) {
    return condiciones.every(cond => this.hechos.has(cond));
  }

  ejecutarHastaFijo() {
    let cambios = true;

    while (cambios) {
      cambios = false;
      const nuevosHechos = new Set(this.hechos);

      for (const { condiciones, conclusiones } of this.reglas) {
        if (this.hechosCoinciden(condiciones)) {
          for (const conclusion of conclusiones) {
            if (!this.hechos.has(conclusion)) {
              nuevosHechos.add(conclusion);
              cambios = true;
            }
          }
        }
      }

      this.hechos = nuevosHechos;
    }
  }

  consultarHecho(hecho) {
    return this.hechos.has(hecho);
  }
}

// Uso: Diagnosis de enfermedad
const sistema = new SistemaReglas();

// Reglas médicas
sistema.agregarRegla(["fiebre", "tos"], ["posible_gripe"]);
sistema.agregarRegla(["posible_gripe", "dolor_cabeza"], ["probable_gripe"]);
sistema.agregarRegla(["fiebre", "dificultad_respiratoria"], ["posible_neumonía"]);
sistema.agregarRegla(["probable_gripe"], ["necesita_descanso"]);

// Hechos observados
sistema.agregarHecho("fiebre");
sistema.agregarHecho("tos");
sistema.agregarHecho("dolor_cabeza");

sistema.ejecutarHastaFijo();

console.log("¿Probable gripe?", sistema.consultarHecho("probable_gripe")); // true
console.log("¿Necesita descanso?", sistema.consultarHecho("necesita_descanso")); // true

45.8 Resumen: IA y Relaciones Discretas

Técnicas Fundamentales
  • Búsqueda A*: Explora grafos de estados con heurística
  • Árboles de Decisión: Particiona espacio mediante orden de atributos
  • K-Means: Agrupa según relación de distancia/similitud
  • Minimax: Encuentra estrategia óptima en juegos (teoría de preferencias)
  • Redes Neuronales: Aprenden relaciones funcionales entrada-salida
  • Sistemas de Reglas: Modelan conocimiento como implicaciones lógicas
  • Todas explotan estructuras relacionales: Orden, distancia, preferencia, causalidad

Qué debes recordar de este tema

Puntos Clave
  • La IA resuelve problemas buscando estructuras relacionales en datos y espacios de estados
  • A* explota heurísticas (funciones que estiman distancia a meta) para guiar búsqueda
  • Los árboles de decisión explotan relaciones de orden en atributos para clasificar
  • K-Means agrupa según la relación de similitud (distancia euclidea)
  • Minimax navega un árbol de juego explotando la relación de preferencia entre estados
  • Las redes neuronales aprenden relaciones funcionales (pesos son relaciones entre capas)
  • Los sistemas de reglas modelan conocimiento como implicaciones lógicas (relaciones de causalidad)
  • La "magia" de la IA es descubrir la estructura relacional correcta en un problema

Conclusión

La inteligencia artificial no es magia, sino el arte de encontrar la estructura relacional correcta en un problema y aplicar el algoritmo adecuado para explotarla.

Cada técnica de IA que hemos explorado—desde búsqueda A* hasta redes neuronales—se basa en una observación relacional fundamental:

  • A* explota que la heurística es una relación de aproximación a la distancia real
  • Árboles de decisión explotan que los atributos tienen órdenes naturales que particionan bien
  • K-Means explota que la distancia euclidea es una relación transitiva y simétrica
  • Minimax explota que los estados de juego tienen una relación de preferencia total
  • Redes neuronales explotan que existe una relación funcional aprendible entre entrada y salida
  • Sistemas de reglas explotan que el conocimiento puede modelarse como implicaciones lógicas

Lo más profundo de la IA es esto: no es que las máquinas piensen, es que hemos descubierto cómo representar problemas como relaciones discretas y automáticamente buscar sus soluciones.

El futuro de la IA está en nuevas formas de descubrir estas estructuras relacionales. La teoría de relaciones discretas es la matemática subyacente de toda la inteligencia artificial moderna.