Me gustaría empezar este artículo explicando un poco sobre los perceptrones y su importancia en las redes neuronales artificiales. Un perceptrón es como una neurona biológica, posee unas entradas y salidas, cada perceptrón tiene una función de activación la cual nos permite resolver un determinado problema a través de procesos matemáticos. Tal vez en este punto ya te hallas dado cuenta de que las redes de neuronas artificiales están compuestas por enormes cantidades de conexiones entre perceptrones, y es aquí donde recide la importancia de comprender como funcionan para poder crear procesos mucho más complejos. Así que, en el transcurso del siguiente artículo te mostraré un proyecto el cual consiste en el desarrollo e implementación de un perceptrón capaz de resolver la función lógica AND.

Introducción

Contexto

En la actualidad (fecha de publicación de este artículo) el avance de los modelos de inteligencia artificial han sido exponenciales, teniendo estos la capacidad de recrear distintas capacidades humanas como crear arte, redactar artículos, resolver problemas matemáticos… lo que llevo a interesarme en este mundo y a formularme la siguiente pregunta, ¿cómo aprenden estos modelos de inteligencia artificial?, y en una exhaustiva búsqueda sobre su funcionamiento encontré un punto de partida, los perceptrones.

Propósito

El propósito de este artículo es dar una respuesta introductoria a la forma en que aprenden los modelos de inteligencia artificial, puesto que estos tienen diferentes mecanismos de aprendizaje. Por ello decidí empezar desde lo más básico que son los perceptrones, los cuales son la base de sistemas más complejos de inteligencia artificial. Así que, para lograr comprender mejor lo que es un perceptrón me he propuesto desarrollar uno desde cero.

Métodos

Descripción del modelo

Como comenté en el comienzo de este artículo un perceptrón busca similar el comportamiento de una neurona biológica, estando esta compuesta por entradas y salidas, el perceptrón también cuenta con esto, pero adicionalmente posee algo denominado pesos los cuales permiten modificar la salida a partir de las entradas recibidas. El objetivo principal de los pesos es que nos permitan encontrar la salida esperada a través de la modificación de estos si la salida generada por ellos no es la correcta. El proceso para generar una salida (en este caso de tipo binaria) se realiza a través de una función de activación la cual realiza un simple proceso de comparación y retorno según el resultado de esta comparación, si este resultado es diferente al esperado entonces modificamos los pesos usando la función de error la cual configura nuevos a través de algunos parámetros, repitiendo estos procesos hasta obtener el resultado que deseamos.

Es posible que todo esto suene muy complicado, así que trataré de explicarlo con un ejemplo, imaginemos por un momento que estamos aprendiendo a preparar un arroz, para ello necesitaremos ingredientes como: arroz, agua y sal (estas serían las entradas de nuestro perceptrón, los ingredientes); pero antes de revolver los ingredientes debemos medir la cantidad de cada uno que agregaremos a la olla (estas cantidades iniciales serían los pesos correspondientes a cada entrada de nuestro perceptrón, como vemos no sabemos realmente que cantidad es necesaria para obtener el arroz que deseamos). Una vez tengamos todos los ingredientes listos y sus cantidades vamos a necesitar una olla y una cuchara para revolver los ingredientes (este proceso de mezclar los ingredientes es el que realizamos en el perceptrón llamado sumatoria, el cual consiste en sumar las entradas y los pesos del perceptrón). Después de dejar a nuestro arroz cocinarse obtendremos nuestro primer plato de arroz (este resultado es generado en el perceptrón a través de la función de activación, que recibe el valor de la sumatoria de los ingredientes con los pesos, para retornar el resultado generado), lo probamos y si no es como lo esperábamos tendremos que modificar las cantidades que agregamos de cada ingrediente (este proceso de realiza a través de la función de error, la cual nos permite modificar los pesos de las entradas) y repetir este proceso hasta obtener el resultado que esperamos.

Espero que con este ejemplo hallas podido comprender un poco mejor el funcionamiento de un perceptrón ya que ahora pasaremos a la parte más técnica.

La sumatoria para nuestro perceptrón esta distribuida de la siguiente manera:

(wi * xi) + b

w = pesos, x = entradas, b = bia

Nota. La bia es otro peso adicional al de las entradas.

Para este caso, en donde nuestro perceptrón tendrá que resolver la función AND, usaremos la función de activación escalonada, la cual sigue la siguiente premisa:

Si el valor de entrada es menor que 0, el perceptrón va a retornar un 0, si el valor de entrada es mayor o igual a 0 va a retornar un 1.

De igual manera la notación de nuestra función de error se resumen en la siguiente formula:

wi = wi + (a * error * xi)

a = factor de aprendizaje

error = peso erróneo

Nota. El factor de aprendizaje lo podemos establecer según nos parezca más adecuado, preferiblemente entre un rango de 0 a 1.

Una vez se tenga claro esto vamos a proceder a implementar estas formulas en nuestro programa, pero antes de esto debemos organizar los datos de entrada para nuestro perceptrón.

Datos

Los datos usados para entrenar a nuestro perceptrón son los que encontramos en la tabla de función AND, debido a que los datos que se encuentran allí son correctos no es necesario realizar ningún procedimiento adicional.

pqp^q
000
010
100
111

Implementación

Una vez con los datos organizados y con mayor claridad sobre el funcionamiento del perceptrón procedí entonces a desarrollar el programa usando el lenguaje de programación C++, decidí usarlo debido a que es más rápido y quería ver que tan factible es utilizar este lenguaje para crear modelos de inteligencia artificial.

Lo primero que realicé fue importar algunas librerías que necesitaría como vector, la cual use para el manejo de vectores y random para generar números aleatorios. Una vez importadas las librerías definí el factor de aprendizaje en 0.5 (escogí este número porque se encuentre en el rango dentro de 0.0 y 1.0) para implementarlo posteriormente en la función de error.

#include <iostream>
#include <vector>
#include <random>

#define LEARNING_FACTOR 0.5

Luego cree una clase llamada Perceptron, allí definí variables y métodos privados, de igual manera que cree el constructor, el destructor y otro método de tipos públicos para que se pudieran acceder a ellos desde fuera de la clase.

class Perceptron {
private:
	std::vector<double>* weights;
	std::vector<double>* inputs;

	int unsigned epoch;

	//Methods
	double activationFunction() {
		double summation = 0.0;
		//Summation
		for (int i = 0; i < weights->size(); i++) {
			if (i + 1 == weights->size()) {
				summation += ((*weights)[i] * 1);
			}
			else {
				summation += ((*weights)[i] * (*inputs)[i]);
			}
		}

		if (summation >= 0.0) {
			return 1.0;
		}
		else {
			return -1.0;
		}
	}

	bool errorFunction() {
		double output = activationFunction();
		double expectedOutput = (*inputs)[2];

		double error = expectedOutput - output;

		if (error != 0.0) {
			for (int i = 0; i < weights->size(); i++) {
				if (i + 1 == weights->size()) {
					(*weights)[i] += LEARNING_FACTOR * error * 1;
				}
				else {
					(*weights)[i] += (LEARNING_FACTOR * error * (*inputs)[i]);
				}
			}
			return true;
		}
		else {
			return false;
		}
	}

	void convertInputsToNegative() {
		for (int i = 0; i < inputs->size(); i++) {
			if ((*inputs)[i] == 0.0) {
				(*inputs)[i] = -1.0;
			}
		}
	}

public:
	//Constructor
	Perceptron(){
		epoch = 0;
	}

	//Setters
	void setWeights(std::vector<double>* weights) {
		this->weights = weights;
	}

	void setInputs(std::vector<double>* inputs) {
		this->inputs = inputs;

		convertInputsToNegative();
	}
	//Getters
	std::vector<double>* getWeights() {
		return weights;
	}

	int getEpochs() {
		return epoch;
	}
	//Method
	bool trainingFunction() {
		int unsigned attempts = 0;

		while (errorFunction()) {
			attempts++;
			epoch++;
		}

		if (attempts > 0) {
			return true;
		}
		else {
			return false;
		}
	}
	//Destructor
	~Perceptron(){}
};

Voy a explicar un poco mejor algunos métodos que se encuentran dentro de la clase, es probable que tes estés preguntando por qué decidí convertir las entradas con valores de 0.0 a -1.0, esto es debido al proceso de la sumatoria ya que es muy probable de que obtuviera errores si multiplicaba algún valor por 0.0 dentro de la operación. La función de entrenamiento ejecuta un bucle hasta que los pesos generados permitan obtener el valor esperado, almacenando en una variable las eras que transcurren hasta encontrarlo. También podemos ver las funciones de activación y de error que ejecutan respectivamente las formulas anteriormente mostradas en este artículo.

Después de haber construido la clase podemos encontrar el método para generar número aleatorios, que en este caso usaremos para generar los pesos iniciales de las entradas.

double generateRandomNumbers() {
	std::random_device rd;
	std::mt19937 gen(rd());

	std::uniform_real_distribution<> dis(0.0, 1.0);

	return dis(gen);

}

Finalmente tenemos nuestra función principal, en donde tenemos nuestro dataset de la tabla AND, y los pesos iniciales generados desde nuestro función para generar números aleatorios. Aquí, nosotros creamos nuestro objeto de tipo perceptrón y le asignamos los pesos iniciales y las entradas de cada fila de nuestro dataset. Una vez ha recorrido todas las filas mostramos los pesos con los cuales han sido entrenado nuestro perceptrón para resolver la función AND.

int main() {
	std::vector<std::vector<double>> dataset = {
		{0.0, 0.0, 0.0},
		{0.0, 1.0, 0.0},
		{1.0, 0.0, 0.0},
		{1.0, 1.0, 1.0},
	};

	std::vector<double> weights = {
		generateRandomNumbers(),
		generateRandomNumbers(),
		generateRandomNumbers(),
	};
	//Creating the object type Perceptron
	Perceptron perceptronSimple = Perceptron();
	//Setting the initial weights
	perceptronSimple.setWeights(&weights);
	//Training the perceptron
	for (int i = 0; i < dataset.size(); i++) {
		perceptronSimple.setInputs(&dataset[i]);
		if (perceptronSimple.trainingFunction()) {
			i = -1;
		}
	}
	//Saving the weights trained and the epochs
	std::vector<double>* weightsTrained = perceptronSimple.getWeights();
	int epochs = perceptronSimple.getEpochs();
	//Showing the weights and the epochs
	std::cout << "\n[----- PERCEPTRON TRAINED -----]\n" << std::endl;
	std::cout << "Epochs: " << epochs << std::endl;
	for (int i = 0; i < weightsTrained->size(); i++) {
		std::cout << "Weight [" << i << "]: " << (*weightsTrained)[i] << std::endl;
	}

	return 0;
}

Resultados

Los resultados obtenidos luego de haber desarrollado el perceptrón usando el lenguaje de programación C++ fue todo un éxito, porque se cumplieron los propósitos por los cuales fue desarrollado este proyecto, lo primero que se logro obtener una mayor comprensión de como aprender los sistemas de inteligencia artificial y lo segundo que se logro implementar un perceptrón capaz de resolver la función lógica AND.

Una vez ejecutado el perceptrón so obtuvieron los siguientes valores:

Epochs: 5
weightOne: 1.75095
weightTwo: 1.43749
weightThree: -2.30461

Que al momento de pasarlos a un script en Python que me ayudará a verificar rápidamente si los datos generados por nuestro perceptrón eran correctos obtuve un resultado satisfactorio:

[x1] [x2] [ yD ] [y] 
[0.0, 0.0, 0.0, 0.0]
[0.0, 1.0, 0.0, 0.0]
[1.0, 0.0, 0.0, 0.0]
[1.0, 1.0, 1.0, 1.0]

x = entradas
yD = Salida Esperada
y = Salida Generada

Como se puede apreciar la salida esperada y salida generada son las mismas, por lo que este proyecto fue un éxito.

Conclusiones

La forma en que los sistemas de inteligencia artificial logran comprender suelen ser algunas veces bastantes complejos, es por ello que es importante tener buenas bases en matemáticas y programación si lo que deseeamos es crearlos desde cero. Aún así, no es imposible, pues como hemos visto pudimos crear un perceptrón y comprender como funciona, lo que nos ha permitido dar ese primer paso en este campo de la tecnología.

Referencias

Apéndices

Código del Perceptrón

#include <iostream>
#include <vector>
#include <random>

#define LEARNING_FACTOR 0.5

class Perceptron {
private:
	std::vector<double>* weights;
	std::vector<double>* inputs;

	int unsigned epoch;

	//Methods
	double activationFunction() {
		double summation = 0.0;
		//Summation
		for (int i = 0; i < weights->size(); i++) {
			if (i + 1 == weights->size()) {
				summation += ((*weights)[i] * 1);
			}
			else {
				summation += ((*weights)[i] * (*inputs)[i]);
			}
		}

		if (summation >= 0.0) {
			return 1.0;
		}
		else {
			return -1.0;
		}
	}

	bool errorFunction() {
		double output = activationFunction();
		double expectedOutput = (*inputs)[2];

		double error = expectedOutput - output;

		if (error != 0.0) {
			for (int i = 0; i < weights->size(); i++) {
				if (i + 1 == weights->size()) {
					(*weights)[i] += LEARNING_FACTOR * error * 1;
				}
				else {
					(*weights)[i] += (LEARNING_FACTOR * error * (*inputs)[i]);
				}
			}
			return true;
		}
		else {
			return false;
		}
	}

	void convertInputsToNegative() {
		for (int i = 0; i < inputs->size(); i++) {
			if ((*inputs)[i] == 0.0) {
				(*inputs)[i] = -1.0;
			}
		}
	}

public:
	//Constructor
	Perceptron(){
		epoch = 0;
	}

	//Setters
	void setWeights(std::vector<double>* weights) {
		this->weights = weights;
	}

	void setInputs(std::vector<double>* inputs) {
		this->inputs = inputs;

		convertInputsToNegative();
	}
	//Getters
	std::vector<double>* getWeights() {
		return weights;
	}

	int getEpochs() {
		return epoch;
	}
	//Method
	bool trainingFunction() {
		int unsigned attempts = 0;

		while (errorFunction()) {
			attempts++;
			epoch++;
		}

		if (attempts > 0) {
			return true;
		}
		else {
			return false;
		}
	}
	//Destructor
	~Perceptron(){}
};

double generateRandomNumbers() {
	std::random_device rd;
	std::mt19937 gen(rd());

	std::uniform_real_distribution<> dis(0.0, 1.0);

	return dis(gen);

}

int main() {
	std::vector<std::vector<double>> dataset = {
		{0.0, 0.0, 0.0},
		{0.0, 1.0, 0.0},
		{1.0, 0.0, 0.0},
		{1.0, 1.0, 1.0},
	};

	std::vector<double> weights = {
		generateRandomNumbers(),
		generateRandomNumbers(),
		generateRandomNumbers(),
	};
	//Creating the object type Perceptron
	Perceptron perceptronSimple = Perceptron();
	//Setting the initial weights
	perceptronSimple.setWeights(&weights);
	//Training the perceptron
	for (int i = 0; i < dataset.size(); i++) {
		perceptronSimple.setInputs(&dataset[i]);
		if (perceptronSimple.trainingFunction()) {
			i = -1;
		}
	}
	//Saving the weights trained and the epochs
	std::vector<double>* weightsTrained = perceptronSimple.getWeights();
	int epochs = perceptronSimple.getEpochs();
	//Showing the weights and the epochs
	std::cout << "\n[----- PERCEPTRON TRAINED -----]\n" << std::endl;
	std::cout << "Epochs: " << epochs << std::endl;
	for (int i = 0; i < weightsTrained->size(); i++) {
		std::cout << "Weight [" << i << "]: " << (*weightsTrained)[i] << std::endl;
	}

	return 0;
}

Script en Python

import argparse

class Perceptron:
    def __init__(self, w1: float, w2: float, b: float) -> None:
        self.__table = [
        [0.0, 0.0, 0.0],
        [0.0, 1.0, 0.0],
        [1.0, 0.0, 0.0],
        [1.0, 1.0, 1.0],
        ]

        self.__weights = [w1, w2, b]
        self.__results = []

    def activation_function(self) -> None:
        self.__temp_input = []

        for index in range(len(self.__table)):
            row = self.__table[index]
            for value in row:
                if value == 0.0:
                    self.__temp_input.append(-1.0)
                else:
                    self.__temp_input.append(value)
            try:

                self.__summation = (self.__temp_input[0] * self.__weights[0]) + (self.__temp_input[1] * self.__weights[1]) + self.__weights[2]

                if self.__summation >= 0.0: self.__table[index].append(1.0)
                else: self.__table[index].append(0.0)

                self.__temp_input.clear()
            except:
                print("[-]The weights have not been introduced")
                exit(1)

    def show_result(self) -> None:
        self.__index = 0

        print("_______________________")
        print("[x1] [x2] [ yD ] [y]")

        for row in self.__table:
            print(row)
        

def main() -> None:
    parser = argparse.ArgumentParser()

    parser.add_argument("--weight_one", dest="w1", type=float)
    parser.add_argument("--weight_two", dest="w2", type=float)
    parser.add_argument("--bia", dest="b", type=float)

    args = parser.parse_args()

    perceptron: Perceptron = Perceptron(w1=args.w1, w2=args.w2, b=args.b)
    perceptron.activation_function()
    perceptron.show_result()

if __name__ == "__main__":
    main()

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *