13 ene. 2017

Redes neuronales artificiales y el perceptrón digital

En esta ocasión se introducirá de forma muy resumida aquellos aspectos más relevantes de un sistema neuronal artificial, tales como el concepto de neurona artificial, dendrita artificial, axón artificial, función de potencial, función de activación y función de señal emitida, para finalmente explicar como construir un un perceptrón digital simple utilizando python.

¿Qué es un sistema neuronal artificial?

Los sistemas neuronales artificiales fueron inspirados por los sistemas neuronales biológicos. De acuerdo con Ramón y Cajal (1888), un sistema neuronal biológico está compuesto por una red de células individuales, ampliamente interconectadas entre sí. Estas células son denominadas neuronas y son como pequeños procesadores de información. A continuación se señala de manera muy general y tal vez atrevida sus principales componentes:

  1. Las dendritas, constituyen el canal receptor de la información.
  2. El soma, es el órgano encargado de procesar la información.
  3. El axón, es el canal de emisión de información a otras neuronas.

En el caso del cerebro humano tiene cerca 90.000.000.000 neuronas, además cada neurona recibe información de aproximadamente 10.000 neuronas y envía impulsos a cientos de ellas. También hay neuronas que reciben información directamente de exterior. Es importante observar que el cerebro se modela durante el desarrollo de un ser vivo, por lo tanto, algunas cualidades no son innatas, sino adquiridas por la influencia de la información que del medio externo recibe.

El modelo estándar de una neurona artificial

A continuación introduciremos el denominado modelo estándar de una neurona artificial según los principios descritos por Rumelhart y McClelland (1986). Una neurona artificial tiene los siguientes componentes:

  1. Las dendritas artificiales. Las dentritas artificiales son un conjunto de parámetros $x_{_i}(t)$ con los cuales se codifica la información de un problema que se quiere resolver. Las variables de entrada y salida pueden ser binarias (digitales) o continuas (analógicas), dependiendo del modelo y la aplicación. Por ejemplo en un perceptrón multicapa (MLP), por lo general las salidas son señales digitales representadas por $1$ y $-1$, en el caso de las salidas analógicas, la señal se dan en un cierto intervalo.
  2. Los pesos sinápticos $w_{_{ij}}(t)$ de la neurona $i$ son variables relacionadas a la sinapsis o conexión entre neuronas, los cuales representan la intensidad de interacción entre la neurona presináptica $j$ y la postsináptica $i$. Dada una entrada positiva (puede ser una señal proveniente de una neurona), si el peso es positivo tenderá a excitar a la neurona postsináptica, si el peso es negativo tenderá a inhibirla.
  3. La función de potencial, permite obtener a partir de las entradas (dendritas) y los pesos sinápticos, el valor de potencial postsináptico $h_{_i}$ de la neurona $i$ en función de los pesos y entradas \begin{equation} h_{_i}(t)=\sigma_{_i}(w_{_{ij}}(t), x_{_j}(t). \end{equation} La función más habitual es de tipo lineal, y se basa en la suma ponderada de las entradas con los pesos sinápticos, es decir, \begin{equation} h_{_i}(t) = \sum_{j}w_{_{ij}}(t)x_{_j}(t). \end{equation} Habitualmente se agrega al conjunto de pesos de la neurona un parámetro adicional $\theta_{_i}$, que se denomina umbral de excitación, el cual se acostumbra a restar al potencial postsináptico. Es decir: \begin{equation} h_{_i}(t) = \sum_{j}w_{_{ij}}(t)x_{_j}(t)-\theta_{_i}. \end{equation} Si se tiene un número finito de dendritas y hacemos que los índices $i$ y $j$ comiencen en cero, y denotamos por $w_{_{i0}}=\theta_{_i}$ y $x_{_0}=-1$, la función de potencial lineal se puede expresar como \begin{equation} h_{_i}(t) = \sum_{j=0}^{n}w_{_{ij}}(t)x_{_j}(t)=\pmb{w}^{\top}(t)\cdot \pmb{x}(t), \end{equation} con $\pmb{w}(t) = (w_{_0}(t),\dots,w_{_n}(t))$ y $\pmb{x}(t)=(x_{_0}(t),\dots,x_{_n}(t))$.
  4. La función de activación $f_{_i}$ de la neurona $i$ proporciona el estado de activación actual $a_{_i}(t)$ a partir del potencial postsináptico $h_{_i}(t)$ y del propio estado de activación anterior, $a_{_{i}}(t-1)$, es decir, \begin{equation} a_{_i}(t)=f_{_i}(a_{_i}(t-1), h_{_i}(t)). \end{equation} Sin embargo, en muchos modelos de redes artificiales se considera que el estado actual de la neurona no depende de su estado anterior, sino unicamente del actual, por lo tanto, \begin{equation} a_{_i}(t)=f_{_i}(h_{_i}(t)). \end{equation}
  5. La función de emisión. Esta función proporciona la salida global $y_{_i}(t)$ y es el componente principal del axón artificial de la neurona $i$ en función de su estado de activación actual. Muy frecuentemente la función de emisión es simplemente la identidad $F(x) = x$ de tal modo que el estado de activación de la neurona se considera como la propia señal de la neurona.

Gráficamente, una neuronal artificial se puede representar de la siguiente forma:




Figura 1. Esquema de una neurona artificial.

Los pesos sinápticos, la función de potencial y la función de activación son los componentes que definen el soma artificial de la neurona.

¿Qué es un perceptrón?

En los 50's Frank Rosenblatt propuso una red neuronal denominada Perceptrón digital simple. Éste consiste de una o varias neuronas, donde la función de activación para cada neurona es \begin{equation} y = F\left(\sum_{i=1}^{n}w_{_{i}}x_{_i}+\theta(t)\right). \end{equation} Generalmente la función de activación $F$ puede ser linea, y se dice por lo tanto que la conexión es de lineal, o puede ser no lineal. Para los objetivos ilustrativos, vamos a considerar la siguiente función: \begin{equation} F(s) = \left\{\begin{array}{cc} 1 & \mbox{ si } s > 0 \\ -1 & \mbox{ si } s \leq 0 \end{array}\right. \end{equation} Para simplificar la exposición de las ideas, supondremos que las señal emitida por la neurona sera $1$ o $-1$, aunque puede ser cualquier otra cosa. Este tipo de neurona se puede usar para tareas de clasificación, es decir, se puede usara para decir si una patrón de entrada pertenece a alguna de las clases definidas por los valores $1$ o $-1$. Si el potencial de entrada es positivo, entonces el patrón se le asignará la etiqueta $1$, sino por el contrario es cero o menor que cero se le asignará la etiqueta de $-1$. Si el espacio de los patrones de entrada se pueden identificar con algún $\mathbb{R}^{n}$, entonces está neuronal separará a $\mathbb{R}^{n}$ en dos clases mediante un hiperplano, dado por la ecuación \begin{equation} x_{_n} = -\frac{w_{_1}}{w_{_n}}x_{_1}-\frac{w_{_2}}{w_{_n}}x_{_1}-\cdots-\frac{w_{_{n-1}}}{w_{_n}}x_{_1}+\frac{\theta}{w_{_n}}. \end{equation} La anterior función se denomina, función discriminante.

En el caso en que el espacio de patrones de entrada se pueda identificar con $\mathbb{R}^{2}$, la situación se puede representar gráficamente, en este caso el hiperplano que define las dos clases es la linea recta dada por \begin{equation} w_{_1}x_{_1}+w_{_2}x_{_2}-\theta = 0, \end{equation} la cual podemos escribir como \begin{equation} x_{_2}=\frac{w_{_1}}{w_{_2}}x_{_1}+\frac{\theta}{w_{_2}} = 0, \end{equation} observe que el cociente $\frac{w_{_1}}{w_{_2}}$ determina la pendiente de la recta y $\frac{\theta}{w_{_2}}$ su bias. Note también que el vector $(w_{_1}, w_{_2})$ es siempre perpendicular a recta.


Figura 2. Función discriminante de un perceptrón simple.
Suponga ahora que se tiene un conjunto de datos $S\subset\mathbb{R}^{2}$, y para un vector $\pmb{x}\in S$ se desea obtener la señal $\hat{y}(x)$. Con se ha dicho anteriormente $\hat{y}(x)$ es usualmente $+1$ o $-1$. ¿Cómo aprende el perceptrón a clasificar adecuadamente? Para eso el perceptrón sigue la siguiente rutina de aprendizaje:

  1. Iniciar con un conjunto aleatorio de pesos sinápticos.
  2. Seleccionar un patrón de entrada $x\in S$.
  3. Si $y(x) \neq \hat{y}(x)$, entonces los pesos se modifican de acuerdo a la regla: \begin{equation} \Delta w_{_i}= \hat{y}(x)x_{_i}; \end{equation}.
  4. Volver al paso 2.

El lector podrá verificar que este procedimiento es muy similar a las regla de aprendizaje de Hebb, la única diferencia es que cuando la neurona responde correctamente, los pesos sinápticos no son modificados. Por otro otro lado, $\theta$ como es considerado es el peso sináptico $w_{_0}$ que siempre recibe por la dendrita $x_{0}$ el valor de $-1$. Para el caso de $theta$, la regla de aprendizaje viene dada por: \begin{equation} \Delta \theta = \left\{\begin{array}{cc} 0 & \mbox{ si el perceptron responde correctamente} \\ \hat{y}(x) & \mbox{ si el perceptron responde incorrectamente} \end{array}\right. \end{equation} Por ahora no entraremos en más detalles teóricos y vamos a ver como tener nuestro propio perceptrón con Python.

¿Cómo construir un perceptrón con Python?

El perceptron que vamos a construir consiste de una capa de $n$ neuronas artificiales, cada una para reconocer un único patrón. El objetivo de la operación del perceptrón es aprender una tranformación dada de la forma $\hat{y}:\{1,-1\}^{m}\to \{1,-1\}^{m}$ usando un conjunto de muestras donde cada elemtento es de la forma $(\pmb{x}, \pmb{y})$ donde $\pmb{x}\in \{1,-1\}^{m}$ y además vamos a pedirle que $\pmb{x}, \pmb{y}$, para esto vamos a usar la función $tanh\,\theta$, de la siguiente forma: \begin{equation} f(t)=tanh(\pmb{w}\cdot \pmb{x}), \end{equation}

Tomaremos como bias para el criterio de desición es el valor $\theta = 0.9999$ y además vamos a considerar que $\pmb{w}=\pmb{y}$, la razón de esta consideración es debido a que la función $tanh\,\theta$ nos permite decir que tan diferentes son dos vectores, así, si cuando se da un valor de entrada $\pmb{x}$ y la neurona artificial nos entrega el valor correcto de $\pmb{y}$ es porque el patrón $\pmb{x}$ está muy cercado al vector $w$, es decir, que el valor de $tanh,\theta$ es muy cercano a uno. En otra ocasión explicaré como hacer el entrenamiento de la neurona. Por ahora solo veremos un ejemplo completamente funcional.

El código que presentaré es una versión mejorada del trabajo presentado por RokerLauncher96 en el siguiente video (ver).

Lo primero que haremos en importar las librerías necesarias para nuestro algoritmo.

#!/usr/bin/env python
# -*- coding: UTF-8 -*-
import numpy as np
import math
import cPickle
from Tkinter import *
from tkMessageBox import *

Luego establecemos una clase que llamaremos Neurona:

class Neurona():
 
    def __init__(self, signal):
        """
        Función para implementar los valores iniciales de la clase. 
        """
        # Se inicia por defecto los pesos sinápticos de la neurona.
        self.weight_sipnatics = np.ones(len(signal))
        # Se establece la memoria de la neurona. 
        cPickle.dump([],open("memoria.mem","wb"))
        self.memoria = cPickle.load(open("memoria.mem","rb"))
  
    def soma(self, signal, potential_function = np.dot, active_function = math.tanh):
        """
        Esto es el centro de computo de la neurona.
        Parameter signal: np.array.
        Parameter weight_sipnactis: np.array.
        Parameter potential_function: función de dos variables.
        Parameter active_function: función de una variable. 
        Return answer: float.
        """
        return active_function(potential_function(signal, self.weight_sipnatics))
    
 
    def learn(self, signal):
        """
        Función para aprender la señal de referencia.
        """
        self.memoria.append(signal)
        self.memoria.reverse()
        print 'Señal aprendida'  
        pass
 
    def forget(self):
        """
        Función para olvidar lo aprendido.
        """
        self.memoria = []
        cPickle.dump(self.memoria, open('memoria.men', 'wb'))
        print('Memoria borrada')
        pass
 
    def analysis(self, signal):
        """
        Función para emitar la señal del Axón.
        """
        candidates = {}
        y = 0
        z = 0
        for i in self.memoria:
            self.weight_sipnatics = self.memoria[z]
            weight = self.soma(signal)
            if weight <= 0.9999:
                pass
            else:
                candidates[weight] = self.memoria[z]
                z += 1
  
        for i in candidates:
            if i > y:
                y = i
            else:
                pass
            if candidates == {}:
                return candidates
            else:
                return candidates[y]

Lo programos la interfaz gráfica del percetrón:

class Perceptron():
    def __init__(self):
        """
        Se dan los valores por defecto de la señal.
        """
        self.signal = np.array([-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1])
        self.neurona = Neurona(self.signal)
  
        # Interfaz gráfica del perceptrón.
        main_window = Tk()  
        main_window.title('Perceptrón')
        main_window.config(bg = '#ECFFFF')
        main_window.geometry('300x300+100+100')

        # Se instancian 25 botones. 
        self.button25 = Button(main_window, text = '25', command = self.veinticinco, bg = '#FFFFFF')
        self.button25.place(relx = 0.73, rely = 0.77, relwidth = 0.13, relheight = 0.15)
        self.button24 = Button(main_window, text = '24', command = self.veinticuatro, bg = '#FFFFFF')
        self.button24.place(relx = 0.58, rely=0.77, relwidth = 0.13, relheight = 0.15)
        self.button23 = Button(main_window, text = '23',command = self.veintitres, bg = '#FFFFFF')
        self.button23.place(relx = 0.43, rely = 0.77, relwidth = 0.13, relheight = 0.15)
        self.button22 = Button(main_window, text = '22', command = self.veintidos, bg = '#FFFFFF')
        self.button22.place(relx = 0.28, rely = 0.77, relwidth = 0.13, relheight = 0.15)
        self.button21 = Button(main_window, text = '21', command = self.veintiuno, bg = '#FFFFFF')
        self.button21.place(relx = 0.12, rely = 0.77, relwidth = 0.13, relheight = 0.15)  
        self.button20 = Button(main_window, text = '20', command = self.veinte, bg = '#FFFFFF')
        self.button20.place(relx = 0.73, rely = 0.60, relwidth = 0.13, relheight = 0.15)
        self.button19 = Button(main_window, text = '19', command = self.diesinueve, bg = '#FFFFFF')
        self.button19.place(relx = 0.58, rely = 0.60, relwidth = 0.13, relheight = 0.15)
        self.button18=Button(main_window, text = '18', command = self.diesiocho, bg = '#FFFFFF')
        self.button18.place(relx = 0.43, rely = 0.60, relwidth = 0.13, relheight = 0.15)
        self.button17 = Button(main_window, text = '17', command = self.diesisiete, bg = '#FFFFFF')
        self.button17.place(relx = 0.28, rely = 0.60, relwidth = 0.13, relheight = 0.15)
        self.button16 = Button(main_window, text = '16',command = self.diesiseis, bg = '#FFFFFF')
        self.button16.place(relx = 0.12, rely = 0.60, relwidth = 0.13, relheight = 0.15)
        self.button15 = Button(main_window, text = '15', command = self.quince, bg = '#FFFFFF')
        self.button15.place(relx = 0.73, rely = 0.43, relwidth = 0.13, relheight = 0.15)
        self.button14 = Button(main_window, text = '14', command = self.catorce, bg = '#FFFFFF')
        self.button14.place(relx = 0.58, rely = 0.43, relwidth = 0.13, relheight = 0.15)
        self.button13 = Button(main_window, text = '13', command = self.trece, bg = '#FFFFFF')
        self.button13.place(relx = 0.43, rely = 0.43, relwidth = 0.13, relheight = 0.15)
        self.button12 = Button(main_window, text = '12', command = self.doce, bg = '#FFFFFF')
        self.button12.place(relx = 0.28, rely = 0.43, relwidth = 0.13, relheight = 0.15)
        self.button11 = Button(main_window, text = '11', command = self.once, bg = '#FFFFFF')
        self.button11.place(relx = 0.12, rely = 0.43, relwidth = 0.13, relheight = 0.15)
        self.button10 = Button(main_window, text = '10', command = self.diez, bg = '#FFFFFF')
        self.button10.place(relx = 0.73, rely = 0.26, relwidth = 0.13, relheight = 0.15)
        self.button9 = Button(main_window, text = '9', command = self.nueve, bg = '#FFFFFF')
        self.button9.place(relx = 0.58, rely = 0.26, relwidth = 0.13, relheight = 0.15)
        self.button8 = Button(main_window, text = '8', command = self.ocho, bg = '#FFFFFF')
        self.button8.place(relx = 0.43, rely = 0.26, relwidth = 0.13, relheight = 0.15)
        self.button7 = Button(main_window, text = '7', command = self.siete, bg = '#FFFFFF')
        self.button7.place(relx = 0.28, rely = 0.26, relwidth = 0.13, relheight = 0.15)
        self.button6 = Button(main_window, text = '6', command = self.seis, bg = '#FFFFFF')
        self.button6.place(relx = 0.12, rely = 0.26, relwidth = 0.13, relheight = 0.15)  
        self.button5 = Button(main_window, text = '5', command = self.cinco, bg = '#FFFFFF')
        self.button5.place(relx = 0.73, rely = 0.09, relwidth = 0.13, relheight = 0.15)
        self.button4 = Button(main_window, text = '4', command = self.cuatro, bg = '#FFFFFF')
        self.button4.place(relx = 0.58, rely = 0.09, relwidth = 0.13, relheight = 0.15)
        self.button3 = Button(main_window, text = '3', command = self.tres, bg = '#FFFFFF')
        self.button3.place(relx = 0.43, rely = 0.09, relwidth = 0.13, relheight = 0.15)
        self.button2 = Button(main_window, text = '2',command = self.dos, bg = '#FFFFFF')
        self.button2.place(relx = 0.28, rely = 0.09, relwidth = 0.13, relheight = 0.15)
        self.button1 = Button(main_window, text = '1', command = self.uno, bg = '#FFFFFF')
        self.button1.place(relx = 0.12, rely = 0.09, relwidth = 0.13, relheight = 0.15)
        self.botones = [self.button1, self.button2, self.button3, self.button4, self.button5,
                        self.button6, self.button7, self.button8, self.button9, self.button10,
                        self.button11, self.button12, self.button13, self.button14, self.button15,
                        self.button16, self.button17, self.button18, self.button19, self.button20,
                        self.button21, self.button22, self.button23, self.button24, self.button25]

        second_window = Tk()
        second_window.title('control')
        second_window.geometry("115x115+450+350")

        Button(second_window, text = 'Aprender', command = self.memorizar, width = 455).pack()
        Button(second_window, text = 'Analizar', command = self.id_signal, width = 455).pack()
        Button(second_window, text = 'Resetear Tabla', command = self.table_reset, width = 455).pack()
        Button(second_window, text = 'Borrar Memoria', command = self.neurona.forget, width = 455).pack()

        second_window.mainloop() 
        main_window.mainloop()

    # A continuación se programa el funcionamiento de cada uno de los botones.
    def uno(self):
        if self.signal[0] == -1:
            self.signal[0] = 1
            self.button1.config(bg = '#5BADFF')
        else:
            self.signal[0] = -1
            self.button1.config(bg = '#FFFFFF')
            pass


    def dos(self):
        if self.signal[1] == -1:
            self.signal[1] = 1
            self.button2.config(bg = '#5BADFF')
        else:
            self.signal[1] = -1
            self.button2.config(bg = '#FFFFFF')
            pass

    def tres(self):
        if self.signal[2] == -1:
            self.signal[2] = 1
            self.button3.config(bg = '#5BADFF')
        else:
            self.signal[2] = -1
            self.button3.config(bg = '#FFFFFF')
            pass
    
    def cuatro(self):
        if self.signal[3] == -1:
            self.signal[3] = 1
            self.button4.config(bg = '#5BADFF')
        else:
            self.signal[3] = -1
            self.button4.config(bg = '#FFFFFF')
            pass
 
    def cinco(self):
        if self.signal[4] == -1:
            self.signal[4] = 1
            self.button5.config(bg = '#5BADFF')
        else:
            self.signal[4] = -1
            self.button5.config(bg = '#FFFFFF')
            pass

    def seis(self):
        if self.signal[5] == -1:
            self.signal[5] = 1
            self.button6.config(bg = '#5BADFF')
        else:
            self.signal[5] = -1
            self.button6.config(bg = '#FFFFFF')
            pass
 
    def siete(self):
        if self.signal[6] == -1:
            self.signal[6] = 1
            self.button7.config(bg = '#5BADFF')
        else:
            self.signal[6] = -1
            self.button7.config(bg = '#FFFFFF')
        pass

    def ocho(self):
        if self.signal[7] == -1:
            self.signal[7] = 1
            self.button8.config(bg = '#5BADFF')
        else:
            self.signal[7] = -1
            self.button8.config(bg = '#FFFFFF')
            pass
 
    def nueve(self):
        if self.signal[8] == -1:
            self.signal[8] = 1
            self.button9.config(bg = '#5BADFF')
        else:
            self.signal[8] = -1
            self.button9.config(bg = '#FFFFFF')
            pass

    def diez(self):
        if self.signal[9] == -1:
            self.signal[9] = 1
            self.button10.config(bg = '#5BADFF')
        else:
            self.signal[9] = -1
            self.button10.config(bg = '#FFFFFF')
            pass
 
    def once(self):
        if self.signal[10] == -1:
            self.signal[10] = 1
            self.button11.config(bg = '#5BADFF')
        else:
            self.signal[10] = -1
            self.button11.config(bg = '#FFFFFF')
            pass
 
    def doce(self):
        if self.signal[11] == -1:
            self.signal[11] = 1
            self.button12.config(bg = '#5BADFF')
        else:
            self.signal[11] = -1
            self.button12.config(bg = '#FFFFFF')
            pass
 
    def trece(self):
        if self.signal[12] == -1:
            self.signal[12] = 1
            self.button13.config(bg = '#5BADFF')
        else:
            self.signal[12] = -1
            self.button13.config(bg = '#FFFFFF')
            pass
 
    def catorce(self):
        if self.signal[13] == -1:
            self.signal[13] = 1
            self.button14.config(bg = '#5BADFF')
        else:
            self.signal[13] = -1
            self.button14.config(bg = '#FFFFFF')
            pass

    def quince(self):
        if self.signal[14] == -1:
            self.signal[14] = 1
            self.button15.config(bg = '#5BADFF')
        else:
            self.signal[14] = -1
            self.button15.config(bg = '#FFFFFF')
            pass

    def diesiseis(self):
        if self.signal[15] == -1:
            self.signal[15] = 1
            self.button16.config(bg = '#5BADFF')
        else:
            self.signal[15] = -1
            self.button16.config(bg = '#FFFFFF')
            pass

    def diesisiete(self):
        if self.signal[16] == -1:
            self.signal[16] = 1
            self.button17.config(bg = '#5BADFF')
        else:  
            self.signal[16] = -1
            self.button17.config(bg = '#FFFFFF')
            pass

    def diesiocho(self):
        if self.signal[17] == -1:
            self.signal[17] = 1
            self.button18.config(bg = '#5BADFF')
        else:
            self.signal[17] = -1
            self.button18.config(bg = '#FFFFFF')
            pass

    def diesinueve(self):
        if self.signal[18] == -1:
            self.signal[18] = 1
            self.button19.config(bg = '#5BADFF')
        else:
            self.signal[18] = -1
            self.button19.config(bg = '#FFFFFF')
            pass

    def veinte(self):
        if self.signal[19] == -1:
            self.signal[19] = 1
            self.button20.config(bg = '#5BADFF')
        else:
            self.signal[19] = -1
            self.button20.config(bg = '#FFFFFF')
            pass

    def veintiuno(self):
        if self.signal[20] == -1:
            self.signal[20] = 1
            self.button21.config(bg = '#5BADFF')
        else:
            self.signal[20] = -1
            self.button21.config(bg = '#FFFFFF')
            pass

    def veintidos(self):
        if self.signal[21] == -1:
            self.signal[21] = 1
            self.button22.config(bg = '#5BADFF')
        else:
            self.signal[21] = -1
            self.button22.config(bg = '#FFFFFF')
            pass

    def veintitres(self):
        if self.signal[22] == -1:
            self.signal[22] = 1
            self.button23.config(bg = '#5BADFF')
        else:
            self.signal[22] = -1
            self.button23.config(bg = '#FFFFFF')
            pass

    def veinticuatro(self):
        if self.signal[23] == -1:
            self.signal[23] = 1
            self.button24.config(bg = '#5BADFF')
        else:
            self.signal[23] = -1
            self.button24.config(bg = '#FFFFFF')
            pass

    def veinticinco(self):
        if self.signal[24] == -1:
            self.signal[24] = 1
            self.button25.config(bg = '#5BADFF')
        else:
            self.signal[24] = -1
            self.button25.config(bg = '#FFFFFF')
            pass

    def id_signal(self):
        if self.neurona.analysis(self.signal) == None:
            print('No sé qué es')
        else:
            c = 0
            for i in self.neurona.analysis(self.signal):
                if c == len(self.botones):
                    break
                if i == 1:
                    self.botones[c].config(bg = '#01D826')
                if i == -1:
                    pass
                c += 1
            pass

    def table_reset(self):
        self.signal = -1 * np.ones(len(self.signal))
        for t in self.botones:
            t.config(bg = '#FFFFFF')

    def memorizar(self):
        self.neurona.learn(self.signal)
        self.table_reset()  

Finalmente instanciamos la clase del perceptrón.

Perceptron()

El lector debe notar, cuando tenga funcionando el perceptrón, que cada que ingresa un nuevo patrón de aprendizaje, se está entrenando una nueva neurona. En otras palabras, para cada patrón se tiene un vector de pesos que permite caracterizar cada nueva neurona entrenada. Estos pesos, se almacenan en memoria.mem. Así, que el lector, puede imaginar el funcionamiento del perceptrón como se muestra en la siguiente figura:



Figura 3. Estructura global del perceptrón.

Otra observación importante, es que el perceptron aprende adecuadamente los pesos sinápticos en un tipo finito. Teóricamente, veamos el siguiente resultado:

Teorema. Se tiene un perceptrón con un conjunto adecuado de pesos sinápticos $w^*$ para el resultado $\hat{y}(x) = y$. Entonces el perceptrón converge en un tiempo finito, sin importar quién sea $w^*$ inicial.
En efecto, si consideramos que $w^*$ es una solución adecuada, entonces $||w^*|| = 1$ (esto si se considera como criterio de desición a la función $sgn$, hacer esta consideración no representa ninguna perdida de generalidad). Ahora si calculamos $|w^*\cdot x|$, entonces tenemos dos posibilidades o que el resultado sea cero, o que existe un $\delta > 0$ tal que $|w^*\cdot x|>0$ para la entrada $x$. Si ahora se considera \begin{equation} \cos \alpha = \frac{w\cdot w^*}{||w||}, \end{equation} entonces de acuerdo a las reglas de aprendizaje del perceptrón se tiene que $\Delta w = \hat{y}x, y por lo tanto la modificación a los pesos sería $w' = w +\Delta w. De esto se sigue que: $$w'\cdot w^* = w\cdot w^*+\hat{y}w^*\cdot x = w\cdot w^*+sgn(w^*\cdot x)w^*\cdot x > w\cdot w^* +\delta$$ por otro lado se tiene: $$||w'||^2=w^2+2\hat{y}w\cdot x + x^2 < w^2 + x^2 = w^2+ M$$ dado que $\hat{y}=-sgn(w\cdot x)$. Después de estás modificaciones, entonces es puede concluir que: $$\cos\alpha > \frac{w^*\cdot w + \delta}{\sqrt{w^2+tM}}.$$ De esta última expresión se concluye que el tiempo de convergencia debe ser finito, dado que $cos \alpha \leq 1$. Con algunas modificaciones, se puede considerar como el tiempo máximo a $t_{máx}=\frac{M}{\delta^2}$.

Ya me he extendido mucho, espero verlos pronto.

Referencias