Módulo 2 - Resolviendo XOR
Introduccion
En el módulo anterior terminamos con este código que describe el comportamiento de un perceptrón booleano:
/*========BIBLIOTECAS========*/
#include <stdio.h>
/*=========VARIABLES=========*/
int Entrada[2];
int Salida;
float Peso[]={ 0 , 0 };
float Sesgo= 0;
/*=========FUNCIONES=========*/
int activacion_booleana( float x ){
return x >= 0;
}
int evaluar_neurona(){
float ponderacion= Sesgo;
for( int i= 0 ; i < 2 ; i++ )
ponderacion+= Entrada[i] * Peso[i];
return activacion_booleana( ponderacion );
}
/*=========PRINCIPAL=========*/
int main(){
/* ENTRADAS */
int tabla[4][2]={
{ 0 , 0 },
{ 0 , 1 },
{ 1 , 0 },
{ 1 , 1 }
};
/* ENTRENAMIENTO */
int muestra[]={ 0 , 0 , 0 , 0 };
float tasa_aprendizaje= 0.1;
int epoca= 0;
int error_total;
do{
error_total= 0;
for( int i= 0 ; i < 4 ; i++ ){
for( int j= 0 ; j < 2 ; j++ )
Entrada[j]= tabla[i][j];
Salida= evaluar_neurona();
int error= muestra[i] - Salida;
Sesgo+= error * tasa_aprendizaje;
for( int j= 0 ; j < 2 ; j++ )
Peso[j]+= error * tasa_aprendizaje * Entrada[j];
error_total+= !!error;
}
} while( ++epoca < 1000 && error_total );
printf( "\nIntentos: %i\n" , epoca );
for( int i= 0 ; i < 2 ; i++ )
printf( "Peso %i: %.2f\t" , i + 1 , Peso[i] );
printf( "Sesgo: %.2f" , Sesgo );
/* CALCULAR E IMPRIMIR */
printf( "\n\n| A | B | S |" );
for( int i= 0 ; i < 4 ; i++ ){
for( int j= 0 ; j < 2 ; j++ )
Entrada[j]= tabla[i][j];
Salida= evaluar_neurona();
printf( "\n| %i | %i | %i |" , Entrada[0] , Entrada[1] , Salida );
}
printf( "\n" );
/* TERMINAR PROGRAMA */
return 0;
}
Este código resume tanto la estructura como el comportamiento de un perceptrón, además de mostrar el algoritmo de entrenamiento en acción. Antes de avanzar con este nuevo módulo, repasemos rápidamente los puntos clave:
- Para trabajar con el perceptrón necesitamos ingresar datos a través de sus entradas y luego observar la salida que genera.
- El resultado de esa salida depende de los valores de los pesos, del sesgo y de la función de activación.
- El entrenamiento requiere una lista de ejemplos que el perceptrón debe aprender a imitar.
- Para lograrlo, calcula el error entre la salida que genera y la que debería haber generado según el ejemplo.
- Luego ajusta los pesos y el sesgo en función de ese error y de los valores que tenían en ese momento, acercándose progresivamente a la solución correcta.
En el módulo anterior se mencionó que hay ciertos casos que un perceptrón no puede resolver, como la operación lógica XOR (¿Las entradas son distintas?) o XNOR (¿Son iguales?).
A XOR B
A | B | Salida |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
A XNOR B
A | B | Salida |
---|---|---|
0 | 0 | 1 |
0 | 1 | 0 |
1 | 0 | 0 |
1 | 1 | 1 |
Si no lograste encontrar el motivo al final del módulo anterior, ahora te lo explico.
Ya que el comportamiento del perceptrón está determinado por los valores de sus pesos y su sesgo, podemos analizar qué condiciones hacen posible representar comportamientos lógicos como AND y OR.
AND
- El sesgo debe comenzar con un valor negativo. Si ambas entradas son 0, el único valor que afecta el cálculo es el sesgo, por lo tanto la activación devuelve 0.
- Cada peso debe ser positivo y menor que el valor absoluto del sesgo. Así, una sola entrada no basta para superar el umbral, y la salida sigue siendo 0.
- Solo cuando ambas entradas valen 1, la suma puede superar el umbral y la activación responde con un 1.
OR
- Al igual que con AND, el sesgo comienza negativo.
- Pero ahora, cada peso debe ser igual o mayor al valor absoluto del sesgo. De esa forma, basta con que una sola entrada valga 1 para que el resultado supere el umbral y se active la salida.
Siguiendo esta lógica, no hay ninguna combinación posible de pesos y sesgo que permita que el perceptrón tome decisiones basadas en si las entradas son iguales o diferentes. Eso ya no depende solo de los valores, sino de la relación entre ellos. Y una sola neurona no tiene forma de comparar.
En este módulo vas a descubrir cómo una red de neuronas puede lograr lo que una sola no. Vamos a romper el límite del perceptrón individual y a construir algo más poderoso.
Creando tu caja de herramientas
Antes de continuar, conviene hacer un poco de organización en nuestro código para no terminar atrapados en un galimatías de instrucciones. Vamos a trabajar con varias neuronas, entrenarlas por separado, conectarlas entre sí y automatizar el cálculo en conjunto para obtener un resultado final. Suena a mucho, pero con algo de orden verás que no es tan complicado.
El primer paso será crear un archivo nuevo, distinto al que venías usando como ejemplo. En este nuevo archivo vas a comenzar a construir tu propia biblioteca, algo así como una versión simplificada de stdio.h. Lo vas a llamar NeuroTIC.h, y ahí irás agrupando funciones que podrás reutilizar en distintos momentos del programa.
Por ahora, vas a mover ahí las funciones que ya tienes: activación_booleana y evaluar_neurona. El contenido del archivo debe quedarte así:
NeuroTIC.h
/*=========CALCULAR==========*/
int activacion_booleana( float x ){
return x >= 0;
}
int evaluar_neurona(){
float ponderacion= Sesgo;
for( int i= 0 ; i < 2 ; i++ )
ponderacion+= Entrada[i] * Peso[i];
return activacion_booleana( ponderacion );
}
Luego vas a eliminar esas funciones del archivo principal y, en su lugar, vas a incluir tu nueva biblioteca:
NeuroTIC.c
/*========BIBLIOTECAS========*/
#include <stdio.h>
#include "NeuroTIC.h"
/*=========VARIABLES=========*/
/*=========PRINCIPAL=========*/
Seguro notaste una diferencia: stdio.h se incluye con los símbolos < >, mientras que NeuroTIC.h se incluye entre comillas " ". Sin entrar en demasiados detalles técnicos, esto se debe a que tu biblioteca no forma parte del índice estándar del compilador. Las comillas indican que el archivo debe buscarse en una ruta específica que tú le indiques: puede ser absoluta o relativa. En este caso, como el archivo NeuroTIC.h está en la misma carpeta que el archivo principal, basta con escribir su nombre.
Intenta compilar el programa.
Verás que aparece un error que dice que algunas variables no están declaradas dentro de las funciones de la biblioteca. ¿Por qué sucede esto? Porque esas funciones están intentando usar variables como Entrada, Peso y Sesgo, pero esas variables no existen aún en el momento en que el compilador intenta crear las funciones, ya que fueron declaradas después de la inclusión de la biblioteca.
Podrías resolver esto de forma rápida (pero poco elegante) moviendo la línea de #include "NeuroTIC.h" más abajo, después de las variables. Pero eso dejaría un código más rígido y difícil de mantener. La mejor solución es hacer que las funciones no dependan de variables externas, sino que reciban lo que necesitan como argumentos.
Vamos a modificar la función evaluar_neurona para que use variables internas, y reciba sus valores desde afuera. Así podrás llamarla en cualquier parte del código sin importar en qué orden esté escrita.
NeuroTIC.h
/*=========CALCULAR==========*/
int activacion_booleana( float x ){
return x >= 0;
}
int evaluar_neurona( int Entrada[2] , float Peso[2], float Sesgo ){
float ponderacion= Sesgo;
for( int i= 0 ; i < 2 ; i++ )
ponderacion+= Entrada[i] * Peso[i];
return activacion_booleana( ponderacion );
}
Y luego actualiza las dos instrucciones donde llamas a esa función en el archivo principal para incluir los argumentos necesarios:
NeuroTIC.c
Salida= evaluar_neurona( Entrada , Peso , Sesgo )
Asegúrate de hacer ese cambio tanto en el bloque de entrenamiento como en la sección donde se imprime la tabla.
Compila de nuevo y ejecuta el programa. Ahora todo debería funcionar sin errores. La función evaluar_neurona ya no depende de variables externas: recibe los datos que necesita, los procesa internamente y devuelve el resultado.
Con tu archivo NeuroTIC.h, ahora tienes un espacio donde podrás guardar las herramientas que vayas creando, de manera ordenada y reutilizable.
Armando la red para resolver XOR
Ahora sí podemos detenernos a analizar cómo es que una red puede resolver la operación XOR. Piensa en lo que ya tienes: una neurona que puede ser entrenada para comportarse como AND, OR, NAND, NOR, implicación o conjunción. Si necesitas refrescar esas operaciones, puedes consultar sus tablas de verdad en el Módulo 0.
Entonces, la pregunta clave es: ¿cómo combinamos esas funciones para que al final el resultado se comporte como una XOR?
Hay varias maneras de lograrlo, y cada una implica una forma distinta de conectar las neuronas. En este caso, vamos a construir una red neuronal multicapa muy simple que puede resolver la operación XOR combinando tres funciones lógicas que ya conoces: NAND, OR y AND. La idea es que las entradas A y B pasen primero por dos neuronas independientes, una que calcule NAND(A, B) y otra que calcule OR(A, B), y luego sus salidas se conecten a una tercera neurona que calcule el AND de esos dos resultados. En resumen:
XOR(A , B) = AND( NAND(A , B) , OR(A , B))
Para visualizarlo mejor, mira este pequeño diagrama:
A———
|———NAND———
B——— |
|———AND———Salida
A——— |
|————OR————
B———
Ahora analicemos cómo se comporta esta red con cada combinación de entradas:
Entradas
A | B |
---|---|
0 | 0 |
0 | 1 |
1 | 0 |
1 | 1 |
XOR
NAND | OR | Salida |
---|---|---|
1 | 0 | 0 |
1 | 1 | 1 |
1 | 1 | 1 |
0 | 1 | 0 |
Ya que entendemos la lógica detrás de esta red, es momento de traducirla a código C. Para eso, necesitamos crear tres neuronas distintas, una para cada función lógica. Como cada neurona tendrá sus propias entradas, pesos, sesgo y salida, lo primero será renombrar las variables del ejemplo original para que podamos distinguirlas claramente.
/*========BIBLIOTECAS========*/
int Entrada[2];
int Salida;
int Entrada_A[2];
float Peso_A[]= { 0 , 0 };
float Sesgo_A= 0;
int Salida_A;
int Entrada_B[2];
float Peso_B[]= { 0 , 0 };
float Sesgo_B= 0;
int Salida_B;
int Entrada_C[2];
float Peso_C[]= { 0 , 0 };
float Sesgo_C= 0;
int Salida_C;
/*=========VARIABLES=========*/
/*=========PRINCIPAL=========*/
Con las neuronas ya declaradas, el siguiente paso es entrenarlas por separado. Cada una aprenderá una función lógica distinta: A aprenderá NAND, B aprenderá OR y C aprenderá AND. Para no duplicar innecesariamente el mismo código, podemos extraer las variables comunes y luego duplicar el bloque de entrenamiento, cambiando solo las variables y la tabla de resultados objetivo.
/*========BIBLIOTECAS========*/
/*=========VARIABLES=========*/
/*=========PRINCIPAL=========*/
int main(){
/* ENTRADAS */
/* ENTRENAMIENTO */
int error;
int error_total;
float tasa_aprendizaje= 0.1;
int epoca;
int A[]={ 1 , 1 , 1 , 0 };
epoca=0;
do{
error_total= 0;
for( int i= 0 ; i < 4 ; i++ ){
for( int j= 0 ; j < 2 ; j++ )
Entrada_A[j]= tabla[i][j];
Salida_A= evaluar_neurona( Entrada_A, Peso_A, Sesgo_A );
error= A[i] - Salida_A;
Sesgo_A+= error * tasa_aprendizaje;
for( int j= 0 ; j < 2 ; j++ )
Peso_A[j]+= error * tasa_aprendizaje * Entrada_A[j];
error_total+= !!error;
}
} while( ++epoca < 1000 && error_total );
printf( "\nEntrenamiento A Intentos: %i\n" , epoca );
for( int i= 0 ; i < 2 ; i++ )
printf( "Peso %i: %.2f\t" , i + 1 , Peso_A[i] );
printf( "Sesgo: %.2f" , Sesgo_A );
int B[]={ 0 , 1 , 1 , 1 };
epoca=0;
do{
error_total= 0;
for( int i= 0 ; i < 4 ; i++ ){
for( int j= 0 ; j < 2 ; j++ )
Entrada_B[j]= tabla[i][j];
Salida_B= evaluar_neurona( Entrada_B, Peso_B, Sesgo_B );
error= B[i] - Salida_B;
Sesgo_B+= error * tasa_aprendizaje;
for( int j= 0 ; j < 2 ; j++ )
Peso_B[j]+= error * tasa_aprendizaje * Entrada_B[j];
error_total+= !!error;
}
} while( ++epoca < 1000 && error_total );
printf( "\nEntrenamiento B Intentos: %i\n" , epoca );
for( int i= 0 ; i < 2 ; i++ )
printf( "Peso %i: %.2f\t" , i + 1 , Peso_B[i] );
printf( "Sesgo: %.2f" , Sesgo_B );
int C[]={ 0 , 0 , 0 , 1 };
epoca=0;
do{
error_total= 0;
for( int i= 0 ; i < 4 ; i++ ){
for( int j= 0 ; j < 2 ; j++ )
Entrada_C[j]= tabla[i][j];
Salida_C= evaluar_neurona( Entrada_C, Peso_C, Sesgo_C );
error= C[i] - Salida_C;
Sesgo_C+= error * tasa_aprendizaje;
for( int j= 0 ; j < 2 ; j++ )
Peso_C[j]+= error * tasa_aprendizaje * Entrada_C[j];
error_total+= !!error;
}
} while( ++epoca < 1000 && error_total );
printf( "\nEntrenamiento C Intentos: %i\n" , epoca );
for( int i= 0 ; i < 2 ; i++ )
printf( "Peso %i: %.2f\t" , i + 1 , Peso_C[i] );
printf( "Sesgo: %.2f" , Sesgo_C );
/* CALCULAR E IMPRIMIR */
/* TERMINAR PROGRAMA */
}
Solo recuerda cambiar el arreglo A[] por B[] y C[] según la tabla de verdad que corresponda. Asegúrate también de usar las variables correctas (Entrada_B, Peso_B, Sesgo_B, etc.) para cada neurona.
Una vez entrenadas todas las neuronas, ya podemos conectarlas y calcular el resultado conjunto de la red. Eso lo hacemos en la sección de impresión final, donde simulamos el paso de señales entre neuronas:
/*========BIBLIOTECAS========*/
/*=========VARIABLES=========*/
/*=========PRINCIPAL=========*/
int main(){
/* ENTRADAS */
/* ENTRENAMIENTO */
/* CALCULAR E IMPRIMIR */
printf( "\n\n| A | B | S |\n" );
for( int i= 0 ; i < 4 ; i++ ){
for( int j= 0 ; j < 2 ; j++ )
Entrada_A[j]= Entrada_B[j]= Entrada[j]= tabla[i][j];
Entrada_C[0]= Salida_A= evaluar_neurona( Entrada_A , Peso_A , Sesgo_A );
Entrada_C[1]= Salida_B= evaluar_neurona( Entrada_B , Peso_B , Sesgo_B );
Salida= Salida_C= evaluar_neurona( Entrada_C , Peso_C , Sesgo_C );
printf( "| %i | %i | %i |\n" , Entrada[0] , Entrada[1] , Salida );
}
printf( "\n" );
/* TERMINAR PROGRAMA */
}
Para conectar las neuronas se ha usado un pequeño truco conocido como cadena de asignaciones. Aunque quizás no lo habías notado explícitamente, ya vienes usándolo desde el inicio del taller: cada vez que usas el operador =, primero se evalúa lo que está a la derecha y luego se asigna el resultado a la izquierda. Por ejemplo, en algo como variable= 5 + 3, primero se realiza la suma (que está a la derecha) y luego se guarda el resultado en variable.
Con eso en mente, veamos esta línea:
for( int j= 0 ; j < 2 ; j++ )
Entrada_A[j]= Entrada_B[j]= Entrada[j]= tabla[i][j];
Según el diagrama que viste antes, las neuronas A y B comparten las mismas entradas. Así que en lugar de escribir tres líneas separadas para copiar el valor desde tabla[i][j] a Entrada[j], y de ahí a Entrada_B[j] y luego a Entrada_A[j], usamos una sola instrucción donde los valores se van encadenando.
Esta cadena de asignaciones se ejecuta de derecha a izquierda:
- Se toma el valor de tabla[i][j]
- Ese valor se asigna a Entrada[j]
- Luego, Entrada_B[j] toma el valor de Entrada[j]
- Y finalmente, Entrada_A[j] toma el valor de Entrada_B[j]
Con esto nos aseguramos de que todas las entradas están sincronizadas, como si estuvieran conectadas entre sí. Además, evitamos errores por copiar mal un valor o repetir instrucciones innecesarias.
Una vez que completes todas las conexiones y ajustes el código, ya puedes compilar y ejecutar el programa. Si todo está en orden, verás que se entrena cada neurona por separado y luego se imprime la tabla de verdad correspondiente a la operación XOR. El resultado en pantalla debería ser algo como esto:
Entrenamiento A Intentos: 6
Peso 1: -0.20 Peso 2: -0.10 Sesgo: 0.20
Entrenamiento B Intentos: 4
Peso 1: 0.10 Peso 2: 0.10 Sesgo: -0.10
Entrenamiento C Intentos: 4
Peso 1: 0.20 Peso 2: 0.10 Sesgo: -0.20
| A | B | S |
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
¡Felicidades! Acabas de construir tu primera red neuronal funcional. Usaste neuronas entrenadas por separado, las conectaste entre sí y lograste que trabajaran en conjunto para resolver una operación que una sola no podía manejar. Ahora entiendes cómo se puede escalar el comportamiento de una neurona simple para resolver problemas más complejos. Nada mal, ¿eh?
Poniendo orden con punteros
Hasta ahora, cada vez que querías entrenar una neurona, tuviste que escribir todo el bloque de instrucciones desde cero: declarar las épocas, calcular errores, ajustar los pesos y el sesgo… y repetir lo mismo tres veces, solo cambiando el nombre de las variables.
Seguramente ya se te cruzó esta pregunta por la cabeza:
¿Por qué no hacer una función para entrenar cualquier neurona, como hiciste con evaluar_neurona?
La respuesta corta es que aún no te he presentado los punteros. Y para entenderlos, necesitamos adentrarnos —solo un poco— en cómo la computadora maneja las variables cuando ejecutamos un programa.
Cuando declaras una variable, puedes imaginar que estás creando una caja que va a guardar un número. En tu código le das un nombre para poder identificarla, pero para la computadora esos nombres no existen. En lugar de eso, la computadora marca cada caja con un número único por fuera para identificarla. Ese número externo es lo que se llama dirección de memoria. Literalmente, indica un lugar físico dentro de la memoria del sistema. Es muy parecido a cómo las casas tienen un número en la puerta para que les llegue el correo.
El número que guardas dentro de la caja se llama dato, y lo puedes cambiar tantas veces como quieras. Podemos visualizar esta idea con una tabla como la siguiente:
MEMORIA
Direccion | Dato |
---|---|
4 | 58 |
5 | 2 |
6 | 23 |
7 | 908 |
... | ... |
Entendiendo esto, cuando ejecutas una instrucción como:
int original= 5;
int copia= original;
le estás diciendo a la computadora que busque la dirección de memoria correspondiente a la variable original, copie el dato que contiene y lo guarde en la dirección reservada para copia.
Esto significa que si más adelante haces algo como:
copia+= 5;
la computadora buscará el dato almacenado en la dirección de copia, le sumará 5 y luego volverá a guardar el nuevo valor en el mismo lugar. Como puedes ver, en ningún momento se modifica el contenido de original.
Cuando pasas argumentos a una función, sucede algo muy similar. Por ejemplo, en la función que escribiste antes:
int activacion_booleana( float x ){
return x >= 0;
}
y su uso dentro de evaluar_neurona:
int evaluar_neurona( ... ){
float ponderacion= ...;
return activacion_booleana( ponderacion );
}
lo que estás indicando es que se cree una copia del valor de ponderacion y se guarde en la variable x dentro de la función activacion_booleana. Esa variable x es completamente independiente, así que cualquier cambio que hagas sobre ella no afectará el valor original de ponderacion.
Sin embargo, si haces memoria, ya usaste una función que sí puede modificar una variable. ¿Recuerdas scanf en el módulo 1? ¿Recuerdas ese operador & que tenías que poner delante de la variable? Estabas usando un puntero.
Un puntero no es más que un tipo de variable especial, en la cual, el numero que guarda como dato, es otra dirección de memoria, con esto, puedes crear una variable tipo puntero dentro de una funcion, y en lugar de pasarle como argumento el dato de otra variable, le pasas la direccion de esa otra variable, así, cuando trabajas con ella dentro dentro de la función, estás indicando dónde quieres que se hagan los cambios, en lugar de hacerlos en una variable interna de la función.
Como ves, el concepto es sencillo, es cierto que se requiere práctica para dominarlos (y obtener el poder de Grayskull), pero para lo que nos atañe, que es crear una función de entrenamiento para las neuronas, con algunos ejemplos nos basta.
Para crear un puntero la cosa es sencilla, se declara como una variable normal, solo que se le agrega un * entre el tipo de dato y el nombre de la variable:
int variable_normal;
int *variable_puntero;
Si intentaras hacer algo como
variable_puntero= variable_normal;
El compilador lanzaría un error, ya que estarías intentando meter un tipo de dato numero en una variable que espera recibir una dirección, entoces para obtener la dirección de la variable normal es que se usa el operador &:
variable_puntero= &variable_normal;
Con el operador & lo que indica es que no interesa el valor que tenga variable_normal, nos interesa su dirección, y esta es la que se estaría guardado en variable puntero.
Por restricciones del sistema (y por seguridad), no se puede operar directamente sobre el valor de un puntero, ya que podría intentar acceder a secciones de memoria que podrian estropear la computadora, por lo que si despues quisieras hacer algo como:
variable_puntero++
obtendrias un error muy conocido por muchos programadores "Segmentation fault", que indica que se intenta acceder a un lugar de la memoria restringida. Pero entoces… ¿para qué sirve guardar la dirección de memoria de otra variable?
Bien, aquí volvemos a usar el operador *, pero en este caso tiene un significado distinto, una vez que se ha declarado un puntero, si se usa sobre este, se está dando la instrucción de que quieres acceder al dato que esta guardado en la dirección de memoria que tiene el puntero.
Si retomamos los ejemplos anteriores, podrías hacer algo como esto:
int variable_normal= 5;
int *variable_puntero= &variable_normal
*variable_puntero= 10;
printf( "%i" , variable_normal );
El resultado del ejemplo anterior sería la impresion en pantalla del número 10, como ves, se ha modificado el valor de variable_normal sin tocarla directamente, y esta misma técnica se puede utilizar con las funciones, por ejemplo, cuando haces:
scanf( "%i", &variable );
El argumento que se le esta pasando a la función, no es el dato de la variable, sino su dirección a un puntero interno de scanf, Luego, un puntero dentro de scanf accede a esa dirección y guarda allí el valor que ingresaste por teclado.
Hay muchos matices con respecto al uso de los punteros, pero con esto ya te puedes estar haciendo una idea de lo que se requiere para crear la función de entrenamiento, asi que vamos a empezar a construirla y guradarla en tu caja de herramientas NeuroTIC.h
NeuroTIC.h
/*=========CALCULAR==========*/
/*=========ENTRENAR==========*/
int entrenar( int entradas[4][2] , int resultados[4] , float *Peso , float *Sesgo ){
int error;
int error_total;
float tasa_aprendizaje= 0.1;
int epoca= 0;
do{
error_total= 0;
for( int i= 0 ; i < 4 ; i++ ){
error= resultados[i] - evaluar_neurona( entradas[i] , Peso , *Sesgo );
*Sesgo+= error * tasa_aprendizaje;
for( int j= 0 ; j < 2 ; j++ )
Peso[j]+= error * tasa_aprendizaje * entradas[i][j];
error_total+= !!error;
}
} while( ++epoca < 1000 && error_total );
return epoca;
}
Y tu función esta lista para implementarse en el código principal:
NeuroTIC.c
/*========BIBLIOTECAS========*/
/*=========VARIABLES=========*/
/*=========PRINCIPAL=========*/
int main(){
/* ENTRADAS */
/* ENTRENAMIENTO */
int A[]={ 1 , 1 , 1 , 0 };
printf( "\nEntrenamiento A Intentos: %i\n" , entrenar( tabla , A , Peso_A , &Sesgo_A ) );
for( int i= 0 ; i < 2 ; i++ )
printf( "Peso %i: %.2f\t" , i + 1 , Peso_A[i] );
printf( "Sesgo: %.2f" , Sesgo_A );
int B[]={ 0 , 1 , 1 , 1 };
printf( "\nEntrenamiento B Intentos: %i\n" , entrenar( tabla , B , Peso_B , &Sesgo_B ) );
for( int i= 0 ; i < 2 ; i++ )
printf( "Peso %i: %.2f\t" , i + 1 , Peso_B[i] );
printf( "Sesgo: %.2f" , Sesgo_B );
int C[]={ 0 , 0 , 0 , 1 };
printf( "\nEntrenamiento C Intentos: %i\n" , entrenar( tabla , C , Peso_C , &Sesgo_C ) );
for( int i= 0 ; i < 2 ; i++ )
printf( "Peso %i: %.2f\t" , i + 1 , Peso_C[i] );
printf( "Sesgo: %.2f" , Sesgo_C );
/* CALCULAR E IMPRIMIR */
/* TERMINAR PROGRAMA */
}
Tal vez notaste algo curioso… o tal vez no. Mientras que la variable Sesgo siguió todas las reglas que te conté sobre punteros —desde pasarla como argumento usando &, hasta manipularla dentro de la función con *— con el arreglo Peso no pasó lo mismo. Es como si ya fuera un puntero.
Y en realidad, lo es. Un arreglo en C se comporta como un puntero especial: no puedes cambiar su dirección, pero sí acceder a sus elementos como si fuera un puntero que apunta al primero. A diferencia de un puntero convencional (declarado con *), que puede apuntar a cualquier variable, un arreglo está confinado a un bloque de direcciones consecutivas, y su índice representa un desplazamiento desde la posición inicial.
Por eso, podemos pasar el nombre del arreglo directamente como argumento sin ningún símbolo adicional, y dentro de la función acceder a sus elementos como siempre, con Peso[j].
Notas finales
Si llegaste hasta aquí, mereces una felicitación. Este módulo no solo fue largo, también estuvo lleno de ideas nuevas, conexiones y reorganización del código. Pero ahora, mirando hacia atrás, hay bastante por celebrar.
Empezaste con el código final del módulo anterior: una sola neurona booleana capaz de entrenarse. A partir de ahí, diste el primer paso hacia una estructura más ordenada, creando tu propia biblioteca básica al separar las funciones activacion_booleana y evaluar_neurona en un archivo externo. Aprendiste a incluir esa biblioteca en tu código principal… y también descubriste por qué eso no funcionó de inmediato.
Al ver que la función evaluar_neurona dependía de variables definidas en otro archivo, modificaste su estructura para que pudiera recibir todo lo necesario desde fuera, y así convertirla en una función verdaderamente independiente.
Después diste un salto conceptual: aprendiste a resolver XOR, una operación lógica que no puede ser resuelta por una sola neurona, combinando varias funciones que ya conocías. Para lograrlo, construiste una red simple con tres neuronas conectadas entre sí. Aprendiste a declarar sus variables, a entrenarlas por separado y a conectarlas usando un truco muy útil: las cadenas de asignación.
Finalmente, tuviste tu primer acercamiento al mundo de los punteros. Conociste, de forma sencilla, cómo se organiza la memoria de la computadora, cómo funcionan las direcciones, qué significa pasar una variable por valor o por referencia, y por qué eso importa al momento de modificar datos dentro de una función. Gracias a ese conocimiento, creaste una nueva función de entrenamiento reutilizable y la agregaste a tu biblioteca. Y también descubriste algo que probablemente no te habías cuestionado hasta ahora: los arreglos que vienes usando desde el Módulo 1 son, en realidad, un tipo especial de puntero.
Todo lo que construiste en este módulo te prepara para un nuevo nivel: ahora tu código es más limpio, tus funciones son reutilizables y tus neuronas pueden trabajar en equipo. Las de tu cabeza también.
Ahora tienes las herramientas para experimentar con otras combinaciones que resuelvan XOR… o para intentar con XNOR. Diviértete con eso. Esa es tu tarea.
En el próximo módulo el objetivo será empaquetar todos los elementos que conforman una neurona para tratarlos como una unidad. Una vez hecho esto, podrás crear arreglos de neuronas, entrenarlas en conjunto y dejar atrás la necesidad de definir cada una por separado.
Recuerda seguir haciendo sinapsis.