Perceptrón multicapas en Mojo
Probablemente ta has oído hablar que la IA que puede hacer cosas realmente geniales e interesantes, como reconocer objetos en una imagen, escribir historias o jugar juegos de ordenador y probablemente te estés preguntando cómo los científicos lograron que los ordenadores pensaran de la manera en que lo hacemos nosotros, uno de los conceptos principales detrás de lograr que la IA piense de la manera en que lo hacemos nosotros es el perceptrón multicapa, ya hemos comentado este tema aquí y con algún nivel de detalle en primer lugar, el perceptrón está fuertemente inspirado en la unidad de pensamiento más básica de nuestro cerebro, que es la neurona, como ya lo sabréis, la neurona tiene un núcleo recibe entradas de múltiples neuronas y da salida a otras neuronas.
A las neuronas no les gusta estar solas y les gusta estar densamente conectadas en grandes grupos las neuronas que son responsables de tus ojos y de tu capacidad para reconocer colores, objetos e imágenes en profundidad, son una red neuronal que está formada por alrededor de 140 millones de neuronas, todas trabajando juntas para brindarte las imágenes de las cosas que ves, de la misma manera, un perceptrón está formado por tres componentes básicos.
Hay una función que es la parte pensante del perceptrón hay entradas que vienen de otros perceptrones, y hay salidas que irán a otros perceptrones. No podrás encontrar un perceptrón es solo un concepto, pero la forma en que está organizado el perceptrón también es muy similar a la forma en que nuestras neuronas cerebrales están organizadas físicamente.
Los perceptrones están organizados en capas así que todas estas neuronas están conectadas y se alimentan de las entradas y salidas de las demás, este es solo el concepto básico detrás del perceptrón multicapa.
Probablemente te estés preguntando cómo hacemos que los ordenadores piensen y cómo hacemos que el perceptrón multicapa aprenda, bueno, hay tres partes básicas del aprendizaje.
- haces una suposición fundamentada, por ejemplo, cuando estabas aprendiendo sobre animales de cuatro patas, habrías visto una cerveza y podrías haberla llamado perro ¿Por qué lo llamarías perro?, bueno, los perros tienen cuatro patas y una cola, y esta cerveza en particular tiene cuatro patas y una cola, watt ?? chaval, estabas equivocado.
- Lo que tienes que hacer ahora es el segundo paso del aprendizaje, que es cambiar. Así que el segundo paso es… ¿Cambiaste de opinión sobre cuál es la diferencia entre un perro y una cerveza? Pero ¿qué pasa la próxima vez que haya un caballo?, ninguna de esas respuestas funciona realmente.
- Entonces, lo que debe suceder es que debe repetir este proceso, el tercer paso será repetir. Básicamente, esta es la misma forma en que los científicos pueden entrenar perceptrones multicapa.
- Por tanto, el perceptrón multicapa proporciona un resultado. Muy a menudo, ese resultado es incorrecto en algunas ocasiones y, en otras, puede ser correcto. Pero en función de esa retroalimentación, debe cambiar.
- El proceso de cambio es algo llamado propagación hacia atrás. Simplemente significa que el perceptrón multicapa tiene que volver a pasar por sus capas y mejorarse a sí mismo hasta llegar a la entrada con lo cual, tiene que volver de esta manera y aprender los pesos y sesgos correctos y luego corregirse a sí mismo para que el siguiente resultado sea mejor.
- Hablando del siguiente resultado, el proceso de repetición se llama época cada época por la que pasa un perceptrón multicapa lo acerca al resultado perfecto.
Hemos analizado el concepto básico del perceptrón multicapa y me gustaría presentar una forma sencilla de escribir desde cero un perceptrón multicapa básico, junto con la propagación hacia adelante y la propagación hacia atrás del error mediante el descenso de gradiente estocástico.
Digamos que tenemos múltiples entradas que llegan a la neurona x1,x2 hasta xn y esta es la neurona por lo tanto, esta neurona será igual a la suma de los pesos multiplicados por las entradas más el sesgo, cada entrada tendrá un peso(W) correspondiente, por lo tanto, esta neurona es la suma ponderada de las entradas y sus pesos correspondientes, que luego es el argumento de una función de activación. Entonces, este será un argumento para una función de activación luego nos dará una salida (output).
Las neuronas individuales pueden interconectarse para formar redes neuronales. A menudo, se agrega un denominado sesgo a las neuronas, que simplemente se puede decir que es el peso de las neuronas, para que cada neurona y su correlación tengan algún significado.
Ahora, vamos a dibujar otro diagrama
Y este es el que usaremos como práctica para construir el proyecto de perceptrones multicapa. Entonces, x0 será la primera entrada y x1 será la segunda entrada.
Y tendremos dos neuronas, Oculto 0 y Oculto 1, ambas entradas estarán conectadas a ambas neuronas, por lo que esto estará conectado de manera similar, tendrá pesos correspondientes. Entonces, x0 tendrá un peso cero, y de manera similar, x1 tendrá un peso w1.
Ambas neuronas estarán conectadas a la capa de salida.
El objetivo de este proyecto es precisamente enseñarle a la red neuronal cómo calcular puertas aritméticas, e implementaremos específicamente la puerta final.
Lo primero que haremos es implementar las estructuras de datos básicas, y luego inicializaremos los pesos de la red neuronal y en el tercer paso calcularemos el valor de la red para los datos de entrada, en el cuarto paso usaremos la propagación hacia atrás, calcularemos el error de nuestra red neuronal y actualizaremos los pesos según este error y utilizaremos el descenso de gradiente estocástico en la retropropagación el descenso de gradiente estocástico es un método de optimización utilizado para actualizar los pesos en una red neuronal a diferencia del descenso de gradiente tradicional, que utiliza datos antiguos para actualizar los pesos después de cada época, funcionará de esta manera.
El descenso de gradiente estocástico funcionará de esta manera.
- La selección de la muestra, por lo tanto, seleccionaremos aleatoriamente un pequeño conjunto de minibatch de datos de todo el conjunto de entrenamiento.
- Calcularemos el gradiente de la función de costo para el lote seleccionado, luego actualizaremos los pesos actualizaremos los pesos en la dirección opuesta al gradiente., aplicando un pequeño paso llamado tasa de aprendizaje y este será el perceptrón multicapa que construiremos en Mojo.
Comenzaremos a implementar el perceptrón multicapa, para ello en Visual Studio crearemos un nuevo fichero mlpercepotron.mojo, antes de comenzar a trabajar en el perceptrón, necesitaremos la estructura de datos básica por lo tanto, crearé las estructuras de datos para almacenar los valores de nuestras muestras y valores de salida, implementaré la matriz unidimensional y luego aprenderás a crear una matriz bidimensional, vamos ello.
Sin perder tiempo, declararé una estructura llamada Array para representar una matriz de números de punto flotante de 64 bits, dentro de esta estructura, tendremos dos campos el primero será data del tipo Pointer igual a Float64.
struct Array:
var data: Pointer[Float64]
var size: Int
Esta variable data almacenará un puntero a la matriz real data que se asigna dinámicamente y el segundo campo para esta estructura será el tamaño del tipo Int, esta variable contendrá el tamaño de la matriz y la cantidad de elementos en la matriz, después de eso, crearemos el constructor que creará una matriz vacía del tamaño especificado.
fn __init__(inout self, size: Int):
self.size = size
self.data = Pointer[Float64].alloc(self.size)
fn __init__ dentro de los parámetros especificaré self y el tamaño de la matriz, que será de tipo entero, dentro de esta función, inicializaré la variable size, self.size será igual a size y self.data será igual a la asignación de la memoria para este tamaño específico para asignar la memoria, usaremos la función alloc(), y dentro de esta función especificaremos el tamaño, para ello especificamos self.size. Esta función iniciará una matriz vacía en la memoria.
Entonces, crearemos otro constructor que inicializará todos los elementos de esta matriz con el valor predeterminado usando un bucle.
fn __init__(inout self, size: Int, default_value:Float64):
self.size = size
self.data = Pointer[Float64].alloc(self.size)
for i in range(self.size):
self.data.store(i, default_value)
Entonces, crearé otro constructor sobrecargado, dentro de los parámetros en primer lugar, especificaré inout self y el tamaño del tipo entero y luego el valor predeterminado del tipo Float64 para las dos primeras líneas del constructor vacío será el mismo en este caso, copiaré y pegaré las líneas y luego usaré un bucle para i in range(self.size), inicializaremos esta matriz con el valor predeterminado, para ello dentro del bucle tratamos la store, usaremos la función store() para colocar algunos valores en esta matriz, nuestros dos constructores están listos.
struct Array:
var data: Pointer[Float64]
var size: Int
fn __init__(inout self, size: Int):
self.size = size
self.data = Pointer[Float64].alloc(self.size)
fn __init__(inout self, size: Int, default_value:Float64):
self.size = size
self.data = Pointer[Float64].alloc(self.size)
for i in range(self.size):
self.data.store(i, default_value)
Ahora crearé un constructor de copia que creará una copia profunda de esta matriz, este nombre de función será __copyinit__ de manera similar, tendremos inout self y luego, tendremos otra variable de tipo matriz.
fn __copyinit__(inout self, copy: Array):
self.size = copy.size
self.data = Pointer[Float64].alloc(self.size)
for i in range(self.size):
self.data.store(i, copy[i])
En primer lugar, inicializaremos el tamaño, será igual al punto de copia y los datos serán iguales al Puntero de tipo Float64, luego asignaremos la memoria de tamaño específico con la función alloc(), después de eso usaremos un bucle for para copiar los datos, para i in range(self.size). Almacenamiento del índice i y obtendremos el valor específico de la copia de i. Entonces, esto nos dará dando un error básicamente que debemos implementar los metodos getter y setter, vamos a ello.
fn __getitem(self, i: Int) -> Float64:
return self.data.load(i)
Para obtener un elemento de un índice específico, se le pasa un parametro en el metodo del tipo Int, y luego devolverá el valor de un índice específico en esta matriz, por lo que devolveremos datos del punto propio y para eso usamos la función load(). Para obtener un elemento específico de la matriz de índice i, el tipo de retorno de la función será Float64, vamos a por la setter.
fn __setitem(self, i: Int, value: Float64):
self.data.store(i, value)
El primer argumento será el índice de tipo entero y luego el valor que queremos establecer en este índice específico, el valor será de tipo Float64, luego usaremos la función self.data.store, establecermos un valor en el índice y el valor provendrá del parámetro del método, después de implementaremos un destructor para liberar nuestra memoria.
fn __del__(owned self):
self.data.free()
Luego liberaremos la memoria usando el método self.data.free().
Después de implementar el destructor, crearemos otro método para implementar el método Len.
fn len(self) -> Int:
return self.size
Que devolverá la longitud de esta matriz y el tipo de retorno será entero.
struct Array:
var data: Pointer[Float64]
var size: Int
fn __init__(inout self, size: Int):
self.size = size
self.data = Pointer[Float64].alloc(self.size)
fn __init__(inout self, size: Int, default_value:Float64):
self.size = size
self.data = Pointer[Float64].alloc(self.size)
for i in range(self.size):
self.data.store(i, default_value)
fn __copyinit__(inout self, copy: Array):
self.size = copy.size
self.data = Pointer[Float64].alloc(self.size)
for i in range(self.size):
self.data.store(i, copy[i])
fn __getitem(self, i: Int) -> Float64:
return self.data.load(i)
fn __setitem(self, i: Int, value: Float64):
self.data.store(i, value)
fn __del__(owned self):
self.data.free()
fn len(self) -> Int:
return self.size
Así que nuestra matriz unidimensional está lista.
En el próximo artículo, crearemos una matriz bidimensional y estas estructuras de datos se utilizarán para almacenar las características de nuestros valores de entrada y para almacenar nuestros valores de salida.