McCulloch y Pitts: La pareja que encendió la chispa neuronal

0
357
Ilustración generada por inteligencia artificial donde aparecen dos hombres estrechando sus manos; de ellas aparece una especie de neurona que parece proyectarse en el cielo. Los hombres aparecen con cara de sorpresa.

En el año 1943, McCulloch y Pitts presentaron un artículo que sentaría las bases para la evolución de la inteligencia artificial. Se trataba de la primera neurona artificial de la historia.

En este artículo ahondaremos en la base de la primera neurona de McCulloch y Pitts desde un punto de vista pedagógico (al menos, todo lo que pueda), veremos el potencial y las limitaciones de esta neurona y programaremos nuestra primera neurona.

Una vez conseguido estos objetivos, usaremos una versión modificada de dicha neurona para realizar una predicción real utilizando un conjunto de datos pre-etiquetados.

0. Índice de contenidos


  1. Características de la neurona de McCulloch y Pitts
  2. Limitaciones de la neurona
  3. Keep it real
  4. Conclusiones
  5. Referencias

1. Características de la neurona M-P


La primera neurona que definen McCulloch y Pitts está compuesta por las siguientes partes:

  • Datos binarios de entrada
  • Threshold
  • Función de adición
  • Función de activación
  • Inhibidores y excitadores

1.1 Datos binarios de entrada

La neurona M-P es una neurona que, acepta datos binarios de entrada y entrega un dato binario de salida

Los datos binarios de entrada de la neurona representan las características que se han extraído de la pregunta que queremos responder. Para nuestro ejemplo, vamos a definir como pregunta a resolver: ¿Voy a ir al parque de atracciones?

Escogeremos 3 preguntas con respuesta binaria para intentar predecir la respuesta a la pregunta. Nuestros datos de entrada serán:

  • ¿Está cerrado?
  • ¿Es fin de semana?
  • ¿Está despejado?

Como vemos, estas preguntas podemos responderlas con un sí o un no; por tanto, se tratan de datos de entrada binarios.

1.2 Threshold

El threshold en la neurona de McCulloch y Pitts es un valor umbral predefinido que determina si la neurona se activa o permanece inactiva. Al comparar la suma ponderada de las entradas con este umbral, se toma la decisión de activación. El valor del threshold influye en la sensibilidad y capacidad de procesamiento de la neurona.

En la neurona original de McCulloch y Pitts, el threshold se establecía de forma estática y no se modificaba durante el procesamiento. Es decir, la neurona M-P no tenía un proceso de entrenamiento.

1.3 Función de adición

Cuando los datos de entrada llegan a la neurona, lo primero que hace la neurona con ellos es una adición. La función z recibirá los parámetros de entrada como un vector x, donde cada componente del vector será una de las entradas de la neurona y realizará la suma de las componentes del vector x:

z(x) ≡ ∑ x_i

1.4 Función de activación

Una vez ha computado la adición de los datos, se pasará por una función de activación que devolverá 1 si se alcanza cierto umbral, o 0 si no es alcanzado (función heaviside step):

a(z(x)) ≡ 1 si z(x) ≥ θ; 0 si z(x) < θ

El valor de salida será igual al valor proporcionado por la función de activación.

Imagen donde aparece un dibujo de un una neurona a la que se conectan tres entradas y un umbral. La neurona se representa con un círculo que contiene dos letras representando las funciones de adición y activación. Aparece una leyenda indicando qué son cada una de las entradas. Aparecen las funciones matemáticas de adición y activación. La neurona tiene una salida representando el resultado del procesamiento de la neurona
Neurona de McCulloch y Pitts

1.5 Inhibidores y excitadores

Seguramente hayas notado que, si bien algunas entradas de la neurona pueden dar más o menos peso a la respuesta final, hay alguna (en nuestro caso ¿Está cerrado?) que hacen que la respuesta sea automáticamente 0. Es decir, si el parque de atracciones está cerrado, no voy a poder ir.

Para solventar este tipo de casuísticas, McCulloch y Pitts definen 2 características de entrada que denominaremos inhibidores y excitadores.

  1. Excitadores: Los excitadores son las entradas que activan o estimulan la neurona. En el modelo de McCulloch y Pitts, estas entradas se representan como valores binarios, generalmente 1 o 0, lo que indica si el excitador está presente o no. Si todos los excitadores necesarios para activar la neurona están presentes, se generará una respuesta de salida.
  2. Inhibidores: Los inhibidores, por otro lado, son las entradas que desactivan o inhiben la neurona. Al igual que los excitadores, se representan como valores binarios. La presencia de un inhibidor puede contrarrestar o anular el efecto de los excitadores, lo que impide que la neurona se active o genere una respuesta de salida.

2. Limitaciones de la neurona M-P


Con la neurona de McCulloch y Pitts podemos realizar algunas predicciones sencillas. Por ejemplo, nos van a permitir representar puertas lógicas sencillas como son las puertas AND, OR y NOT.

Básicamente, este tipo de neurona nos va a permitir solventar problemas que sea linealmente separables.

Como resumen de las limitaciones de este tipo de neurona, tenemos:

  • Solo aceptan valores binarios como datos de entrada
  • Es necesario definir el threshold de forma manual
  • Las entradas no tienen pesos asociados que hagan ciertas entradas más importantes que otras
  • Solo pueden resolver problemas linealmente separables
Imagen de una gráfica donde se ilustra la separación de datos de forma lineal. Aparecen una serie de puntos en color naranja y morado; estos son fácilmente separables por una línea.
Ilustración de separación lineal de datos

Implementación de neurona de McCulloch y Pitts sin excitadores ni inhibidores en typescript:

class MPNeuron {
  private threshold: number;

  constructor(threshold: number) {
    this.threshold = threshold;
  }

  public predict(inputs: number[]): number {
    const sum = this.sum(inputs);
    return this.activationFunction(sum);
  }

  private sum(inputs: number[]): number {
    let sum = 0;
    for (let i = 0; i < inputs.length; i++) {
      sum += inputs[i];
    }
    return sum;
  }

  private activationFunction(sum: number): number {
    return sum >= this.threshold ? 1 : 0;
  }
}

3. Keep it real


Para comprobar el poder que puede tener este tipo de neurona, vamos a diseñar un modelo de aprendizaje basado en la neurona de McCulloch y Pitts. Será una neurona modificada con un método de entrenamiento fit que elegirá un threshold adecuado para nuestro modelo. Debido a que suele asociarse la IA con el lenguaje de programación Python, vamos a aventurarnos a diseñar nuestra neurona con el entorno de Nodejs, usando Typescript como lenguaje de programación. Se ha usado para este tutorial el siguiente entorno de desarrollo:

  • Nodejs v18.16.0
  • Typescript v5.0.4

Puedes ver el código al completo en el siguiente repositorio

3.1 Datos pre-etiquetados «breast_cancer»

Para dicho ejemplo, vamos a utilizar un conjunto de datos extraídos de la web huggingface (te recomiendo mucho el artículo de Luis sobre huggingface). Este conjunto de datos se basa en el estudio de datos sobre posibles tumores de mama. Estos pueden ser malignos o benignos.El conjunto de datos consta de 700 casos pre-etiquetados. Los datos de entrada serán los siguientes:

  • clump_thickness: Grosor del cúmulo
  • uniformity_of_cell_size: Uniformidad del tamaño celular
  • uniformity_of_cell_shape: Uniformidad de la forma celular
  • marginal_adhesion: Adhesión marginal
  • single_epithelial_cell_size: Tamaño de células epiteliales individuales
  • bare_nuclei: Núcleos desnudos
  • bland_chromatin: Cromatina suave
  • normal_nucleoli: Nucleolos normales
  • mitoses: Mitosis

Veamos los pasos que vamos a dar para entrenar nuestra neurona:

3.2 Extraer los valores de entrada del valor objetivo

Los datos nos vienen en un objeto donde la clave es el nombre del valor de entrada y el valor es el valor que este tiene (número no binario). En este objeto también nos viene una clave is_cancer que debemos separar de los datos de entrada, ya que este es el valor de salida.

Para hacerlo, primero convertiremos el objeto a una lista de números y extraeremos el último valor, que hará referencia al valor de is_cancer.

// path: src/getBreastCancerData.ts

import axios from "axios";

interface Data {
  clump_thickness: number;
  uniformity_of_cell_size: number;
  uniformity_of_cell_shape: number;
  marginal_adhesion: number;
  single_epithelial_cell_size: number;
  bare_nuclei: number;
  bland_chromatin: number;
  normal_nucleoli: number;
  mitoses: number;
  is_cancer: 0 | 1;
}

interface Row {
  row: Data;
}

interface BreastCancerData {
  rows: Row[];
}

const huggingFaceDataUrl = (page: number) =>
  `https://datasets-server.huggingface.co/rows?dataset=mstz%2Fbreast&config=cancer&split=train&offset=${
    (page + 1) * 100
  }&limit=100`;

export const getBreastCancerData = async (): Promise<{
  train: number[][];
  target: number[];
}> => {
  const pages = 7;
  const rows = await Promise.all(
    new Array(pages)
      .fill(0)
      .map(
        async (_, index) =>
          await axios
            .get<BreastCancerData>(huggingFaceDataUrl(index))
            .then((r) => r.data)
      )
  ).then((responses) =>
    responses.reduce((acc: Row[], response) => [...acc, ...response.rows], [])
  );

  return getTrainingData(rows);
};

const getTrainingData = (
  rows: Row[]
): { train: Array<Array<number>>; target: Array<number> } => {
  const train: Array<Array<number>> = [];
  const target: Array<number> = [];

  rows.forEach((row) => {
    const data = Object.values(row.row);
    const targetValue = data.pop();

    if (targetValue === undefined) {
      throw new Error("Target value is undefined");
    }

    train.push(data);
    target.push(targetValue);
  });

  return { train, target };
};

3.3 Transformar datos de entrada en datos binarios

Una vez hemos extraído los datos, necesitamos transformarlos a binario. Recuerda que la neurona de McCulloch y Pitts solo admite valores binarios de entrada.Para hacerlo, hemos elegido un algoritmo que obtiene una media aritmética sobre todos los valores de entrada posibles. Posteriormente, devuelve 1 si el valor es mayor o igual a la media aritmética o 0 si es menor

// path: src/transformToBinary.ts

export const transformToBinary = (array: number[][]): number[][] => {
  const flatArray = array.flat();
  const arithmeticMean = flatArray.reduce((a, b) => a + b) / flatArray.length;
  const binaryArrays = array.map((a) =>
    a.map((b) => (b >= arithmeticMean ? 1 : 0))
  );

  return binaryArrays;
};

3.4 Seleccionar datos para entrenamiento y prueba

Una vez hemos transformado los datos de entrada a datos binarios, es momento de partir nuestro conjunto de datos en un conjunto de entrenamiento y otro de pruebas. Para ello, hemos creado una función que recibe la lista de datos de entrada, la lista de valores y el porcentaje de partición

// Path: src/cutArray.ts

interface CutArray {
  data: number[][];
  target: number[];
}

interface CutArrayResult {
  train: CutArray;
  test: CutArray;
}

export const cutArray = (
  array: number[][],
  target: number[],
  percentageForTest: number
): CutArrayResult => {
  if (percentageForTest < 0 || percentageForTest > 1) {
    throw new Error("Percentage must be between 0 and 1");
  }

  const testLength = Math.round(array.length * percentageForTest);
  const trainLength = array.length - testLength;

  const trainData = array.slice(0, trainLength);
  const testData = array.slice(trainLength);

  const trainTarget = target.slice(0, trainLength);
  const testTarget = target.slice(trainLength);

  return {
    train: {
      data: trainData,
      target: trainTarget,
    },
    test: {
      data: testData,
      target: testTarget,
    },
  };
};

3.5 Programando neurona McCulloch y Pitts con entrenamiento

Una vez hemos hecho estos procesos, es momento de crear nuestra neurona. Para ello, vamos a ver el código de la neurona de M-P con método de entrenamiento:

// Path: src/MPNeuron.ts

import { accuracyScore } from "./accuracyScore";

export class MPNeuron {
  threshold: number;

  constructor(threshold: number) {
    this.threshold = threshold;
  }

  public predict(inputs: number[]): number {
    const sum = this.sum(inputs);
    return this.activationFunction(sum);
  }

  public fit(inputs: number[][], targets: number[]): void {
    let accuracy: number[] = [];
    inputs.forEach((_, index) => {
      this.threshold = index;
      const prediction = inputs.map((input) => this.predict(input));
      accuracy[index] = accuracyScore(targets, prediction);
    });
    const maxAccuracy = Math.max(...accuracy);
    this.threshold = accuracy.indexOf(maxAccuracy);
  }

  private sum(inputs: number[]): number {
    let sum = 0;
    for (let i = 0; i < inputs.length; i++) {
      sum += inputs[i];
    }
    return sum;
  }

  private activationFunction(sum: number): number {
    return sum >= this.threshold ? 1 : 0;
  }
}

Como vemos, esta neurona tendrá un método fit encargado de entrenar el modelo (ajustar el threshold). Para realizar el ajuste del modelo, se utiliza la función accuracyScore:

// Path: src/accuracyScore.ts

export const accuracyScore = (y_target: number[], y_pred: number[]) => {
  let correct = 0;
  for (let i = 0; i < y_target.length; i++) {
    if (y_target[i] === y_pred[i]) {
      correct++;
    }
  }
  return correct / y_target.length;
};

Esta función comprueba la predicción de cada resultado; si la predicción es correcta añade 1 a un contador correct. Una vez finaliza, devuelve la media entre el número de valores correcto y el numero de valores totales.

En base a esto, la neurona ajusta su threshold obteniendo el índice del mayor accuracy obtenido.

3.6 Entrenamiento y predicción de neurona de McCulloch y Pitts

Una vez hemos entrenado la neurona, es momento de comprobar que tan buena es prediciendo valores que nunca “ha visto”. Para ello, ejecutamos todo lo mencionado anteriormente junto:

// Path: src/index.ts

import { MPNeuron } from "./MPNeuron";
import { accuracyScore } from "./accuracyScore";
import { cutArray } from "./cutArray";
import { getBreastCancerData } from "./getBreastCancerData";
import { transformToBinary } from "./transformToBinary";

const init = async () => {
  const data = await getBreastCancerData();
  const binaryData = transformToBinary(data.train);

  const { train, test } = cutArray(binaryData, data.target, 0.7);

  const neuron = new MPNeuron(0);
  neuron.fit(train.data, train.target);

  const result = test.data.map((item) => neuron.predict(item));
  const accuracy = accuracyScore(test.target, result);

  console.log({
    accuracy,
    threshold: neuron.threshold,
  });
};

init();

Aquí podemos “jugar” con el porcentaje pasado a la función cutArray para ver que nivel de precisión obtiene nuestra neurona.

Para poner algunos ejemplos:

  • porcentaje para test = 0.9: { accuracy: 0.939047619047619, threshold: 2 }
  • porcentaje para test = 0.7: { accuracy: 0.9681372549019608, threshold: 3 }

Es decir, si partimos los datos dejando un 90% para pruebas y un 10% para entrenamiento, nuestra neurona es capaz de predecir los valores con un 94% de precisión.

Si esa partición la hacemos al 70% para tests y 30% para entrenamiento, nuestra neurona será capaz de predecir correctamente el 97% de los datos de prueba. ¡Asombroso!

Conclusiones


En resumen, la neurona de McCulloch y Pitts ha sido un punto de referencia en la historia de las redes neuronales, estableciendo los cimientos para modelos más complejos. Aunque presenta limitaciones, su estructura intuitiva y simplificada sigue siendo valiosa para comprender los conceptos fundamentales de las redes neuronales y sentar las bases para futuros avances en este campo emocionante.

En próximas entradas hablaremos de la evolución lógica de la neurona de McCulloch y Pitts: el perceptrón y de su hermano mayor: el perceptrón multicapa. Veremos como “descender una colina” y algunos algoritmos que hacen más “inteligentes” a estas redes artificiales.

Hasta entonces, ¡Buen provecho!

Referencias

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad