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

7 ene. 2016

El gradiente descendente y la regresión lineal - Introducción

El objetivo de esta entrada es introducir al lector en algunos aspectos teóricos el método numérico del gradiente descendente, la regresión lineal y presentar una breve implementación en Python.
Para comenzar hablaremos del algoritmo del gradiente descendente (AGD) y luego mostraremos un ejemplo con el método de la regresión lineal. El AGD busca los puntos $p\in \Omega$ donde funciones del tipo $f:\Omega\subseteq \mathbb{R}^{m}\to \mathbb{R}$ alcanzan su mínimo. La idea de este método surge de la siguiente situación: Si $f$ es una función diferenciable en todo su dominio $\Omega$, entonces la derivada de $f$ en un punto $p\in \Omega$ en dirección de un vector unitario $v\in \mathbb{R}^{m}$ se define como: \begin{equation}\label{eq:01}df_{p}(v)=\nabla f(p)\cdot v.\end{equation} Observe que la magnitud de la ecuación $\eqref{eq:01}$ es: $$|df_{p}(v)|=||\nabla f(p)||||v||\cos \theta = ||\nabla f(p)||\cos \theta$$ Dicha magnitud es máxima cuando $\theta = 2n \pi, n\in \mathbb{Z}$. Es decir, para que $|df_{p}(v)|$ sea máxima los vectores $\nabla f(p)$ y $v$ deben ser paralelos. De esta manera la función $f$ crece más rápidamente en la dirección del vector $\nabla f(p)$ y decrece más rápidamente en la dirección del vector $-\nabla f(p)$. Nótese además que cualquier dirección $v\in \mathbb{R}^m$ ortogonal a $\nabla f(p)$ es una dirección de cambio nulo. Esto sugiere que la dirección negativa del gradiente $-\nabla f(p)$ es una buena dirección de búsqueda para encontrar el minimizador de la función $f$. Es importante recordar que dada una hipersuperfice de nivel $S_{k}=\{p\in \Omega: f(p)=k\}$, si $p\in S_{k}$ entonces $\nabla f(p)\perp t_{p}$, donde $t_{p}$ es cualquier vector tangente a $S_{k}$ en $p$. Por lo tanto surge de manera natural el siguiente algoritmo:

Algoritmo del gradiente descendente

Sea $f:\Omega \subseteq \mathbb{R}^{m}\to \mathbb{R}$, si $f$ tiene un mínimo en $p$, para encontrar a $p$ se construye una sucesión de puntos $\{p_t\}$ tal que $p_{t}$ converge a $p$. Para resolver esto, comenzamos en $p_{t}$ y nos desplazamos por una cantidad $-\lambda_{t}\nabla f(p_{t})$ para encontrar el punto $p_{t+1}$ más cercano a $p$. Es decir: \begin{equation}\label{eq:02} p_{t+1}=p_{t}-\lambda_{t}\nabla f(p_{t})\end{equation} donde el parámetro $\lambda_{t}$ se selecciona de tal manera que $p_{t+1}\in \Omega$ y $f(p_{t})\geq f(p_{t+1})$.
El parámetro $\lambda_{t}$ se selecciona para maximizar la cantidad a la que decrece la función $f$ en cada paso. Es decir, $\lambda_{t}$ se escoge para minimizar la aplicación: $$\phi_{t}(\lambda)=f(p_{t}-\lambda \nabla f(p_{t}))$$ Esto es: $$\lambda_{t}=\arg \min_{\lambda \geq 0} f(p_{t}-\lambda \nabla f(p_{t})). $$
Al momento de realizar una implementación computacional de este método, es necesario establecer un criterio de parada para cada iteración del algoritmo. Para esto, el lector puede definir un umbral $\epsilon>0$ y detener el algoritmo cuando los errores relativos en los valores de $f(p)$ cumplan: \begin{equation}\label{eq:03}\frac{|f(p_{t+1})-f(p_{t})|}{\max\{1, |f(p_{t})|\}}<\epsilon.\end{equation}
El criterio de parada se selecciona de acuerdo a las necesidades y creencias del lector.

Ejemplo del AGD aplicado al método de regresión lineal

La técnica de la regresión lineal se clasifica en la categoría del aprendizaje mecánico supervisado y dentro de esta es de tipo aprendizaje mecánico por regresión. El objetivo de la regresión lineal es ajustar una aplicación afín a un conjunto de datos $M=\{(x_{i},y_{i})\}_{1\leq i\leq k}$ con $(x_{i},y_{i})\in \mathbb{R}^m\times\mathbb{R}^n$. Es decir, se busca una aplicación $h_{\theta}:\mathbb{R}^{m}\to \mathbb{R}^{n}$ de la forma: \begin{equation}\label{eq:04}h_{\theta}(v)=v\cdot\beta + \alpha\end{equation} donde $v\in \mathbb{R}^{m}$, $\alpha\in \mathbb{R}^{n}$ son vectores filas, $\beta\in M_{m\times n}(\mathbb{R})$ es una matriz de orden $m\times n$ y $\theta^{\top}=(\alpha^\top, \beta^\top)\in M_{n\times(m+1)}(\mathbb{R})$, de tal forma que la suma de las distancias entre los puntos $h_{\theta}(x_{i})$ e $y_{i}$ sea mínima. Se desea encontrar un parámetro $\theta$ tal que: \begin{equation}\label{eq:05}\theta = \arg\min_{\theta}\frac{1}{2k}\sum_{i=1}^{k}||y_{i}-h_{\theta}(x_{i})||^{2} \end{equation}
Por lo tanto para encontrar los parámetros $\alpha$ y $\beta$ (de tal forma que se cumpla la condición $\eqref{eq:05}$), utilizamos el AGD empleando la ecuación $\eqref{eq:02}$. Para cuando $m=n=1$ las iteraciones que se deben realizar son: $$\alpha_{t+1}=\alpha_{t}+\frac{\lambda_{t}}{k}\sum_{i=1}^{k}(y_{i}-h_{\theta_{t}}(x_{i})),$$ $$\beta_{t+1}=\beta_{t}+\frac{\lambda_{t}}{k}\sum_{i=1}^{k}(y_{i}-h_{\theta_{t}}(x_{i}))x_{i}.$$ Implementando lo anterior en Python se tiene algo así:
#! /usr/bin/env python
# -*- coding: UTF-8 -*-

from __future__ import division, print_function
import numpy as np
import random
import sklearn
from sklearn.datasets.samples_generator import make_regression
import pylab
from scipy import stats

def gradientDescent(Lambda, x, y, epsilon, max_iter = 10000):
    """
    Esta función permite calcular el parametro «theta» para la aplicación afín que mejor se ajusta
    a los datos (x, y).
    :param Lambda: Tasa de aprendizaje.
    :param x: Valores independientes de entrada.
    :param y: Valores dependientes de salida.
    :param epsilon: Umbral de parada.
    :param max_iter: Numero de veces que se repite la iteración. 
    """
    
    converged = False
    iter = 0
    k = x.shape[0] # tamaño de la muestra.
    
    # Iniciamos el parametro theta.
    alpha = np.random.random(x.shape[1])
    beta = np.random.random(x.shape[1])
    
    # Error total, e(theta_t)
    temp_et = sum([(y[i]-alpha - beta*x[i])**2 for i in range(k)])
    
    #Ciclo de iteraciones
    while not converged:
        #Para el conjunto muestral se calcula el gradiente de la funcion e(theta)
        grad0 = 1.0/k * sum([(y[i]-alpha - beta*x[i]) for i in range(k)])
        grad1 = 1.0/k * sum([(y[i]-alpha - beta*x[i])*x[i] for i in range(k)])
    
        #Guardamos temporamente el parametro theta
        temp_alpha = alpha + Lambda*grad0
        temp_beta = beta + Lambda*grad1
        
        #Actualizamos el parametro theta
        alpha = temp_alpha
        beta = temp_beta
        
        # Error cuadrático medio.
        et = sum([(y[i]-alpha - beta*x[i])**2 for i in range(k)])
        
        if abs(temp_et-et) <= epsilon:
            print('La iteración converge: ', iter)
            converged = True
            
        temp_et = et #Actualización del error
        iter += 1 #Actualización del numero de iteraciones
        
        if iter == max_iter:
            print('¡Máximo de iteraciones excedido!')
            converged = True
            
    return alpha, beta       
    pass

if __name__ == '__main__':
    x, y = make_regression(n_samples = 200, n_features=1, n_informative=1, random_state=0, noise=35)
    print('Tamaño del conjunto de prueba: ', x.shape, y.shape)
    
    Lambda = 0.001 # Tasa de aprendizaje
    epsilon = 0.01 # Umbra de convergencia
    
    # Llamamos la función gradientDescent para obtener el parametro theta.
    alpha, beta = gradientDescent(Lambda, x, y, epsilon, max_iter=10000)
    print('alpha = ', alpha, ', beta = ', beta)
    
    #Comprobando los resultados de nuestro algoritmo con scipy linear regression
    slope, intercept, r_value, p_value, slope_std_error = stats.linregress(x[:,0], y)
    print('Intercepto = ', intercept, ', Pendiente = ', slope) 
 
    # plot
    for i in range(x.shape[0]):
        y_predict = alpha + beta*x 
 
    pylab.plot(x, y, 'o')
    pylab.plot(x, y_predict, 'k-')
    pylab.show()
    print('¡Listo!')
El lector puede notar que en la líneas 53-55 se estableció el criterio de parada $|e(\theta_t)-e(\theta_{t+1})|<\epsilon$ como $\verb|abs(temp_et-et) <= epsilon|$ donde $\verb|temp_et|=e(\theta_t)$ y $\verb|et|=e(\theta_{t+1})$. Esta condición es equivalente a la condición de parada dada por los errores relativos en la ecuación $\eqref{eq:03}$.
Para el caso más general de $m$ y $n$, conviene escribir la ecuación $\eqref{eq:04}$ como: \begin{equation} h_{\theta}(v)=\xi\cdot \theta \end{equation} donde $\xi=(1, v)$. Definimos además las matrices $X^\top = (\xi_{1}^\top,\dots\xi_{k}^\top)$ e $Y^\top = (y_{1}^\top,\dots y_{k}^\top)$. Esto nos permite escribir la condición $\eqref{eq:05}$ de forma equivalente como: \begin{equation} \theta = \arg \min_{\theta} \frac{1}{2m}||X\theta -Y||^2. \end{equation} Donde la función $E(\theta)=||X\theta -Y||^2$ es conocida como la función de costo o pérdida y la función $h_{\theta}(x)=\xi\cdot\theta$ conocida como la función hipótesis. Por lo tanto se debe aplicar el AGD a la función $E$. Para esto tenemos que el gradiente de $E$ es: \begin{eqnarray*} \nabla_{\theta}E(\theta)&=&\frac{1}{2m}\nabla_{\theta}[(X\theta -Y)^\top (X\theta -Y)]\\ &=&-\frac{1}{2m}\nabla_{\theta}(Y^\top X\theta)-\frac{1}{2m}\nabla_{\theta}(\theta^\top X^\top Y)+\frac{1}{2m}\nabla_{\theta}(\theta^\top X^\top X\theta)\\ &=& \frac{1}{m} X^\top(X\theta-Y) \end{eqnarray*} Luego se debe realizar la siguiente iteración: \begin{equation} \theta_{t+1}=\theta_{t}-\frac{\lambda_{t}}{m}X^\top(X\theta_{t}-Y) \end{equation} Implementando esto en Python sería algo así:
#! /usr/bin/env python
# -*- coding: UTF-8 -*-

from __future__ import division, print_function
import numpy as np
import random
import sklearn
from sklearn.datasets.samples_generator import make_regression
import pylab
from scipy import stats

def gradientDescent2(Lambda, x, y, max_iter = 10000):
    """
    Esta función permite calcular el parametro «theta» para la aplicación afín que mejor se ajusta
    a los datos (x, y).
    :param Lambda: Tasa de aprendizaje.
    :param x: Valores independientes de entrada.
    :param y: Valores dependientes de salida.
    :param epsilon: Umbral de parada.
    :param
    """
    k = x.shape[0] # número de muestras.
    theta = np.ones(x.shape[1]+1)    
    X = np.c_[ np.ones(x.shape[0]), x]
    Y = y
    for iter in range(0, max_iter):
        hypothesis = np.dot(X, theta)
        loss = hypothesis - Y
        gradient = np.dot(X.T, loss) / k         
        theta = theta - alpha * gradient  # update
    return theta   
    pass

if __name__ == '__main__':
    x, y = make_regression(n_samples = 200, n_features = 1, n_informative = 1, random_state = 0, noise = 35) 
    m, n = np.shape(x)
    alpha = 0.001 # learning rate
    theta = gradientDescent2(alpha, x, y, 10000)
    # plot
    for i in range(x.shape[1]):
        y_predict = theta[0] + theta[1]*x 
    pylab.plot(x, y, 'o')
    pylab.plot(x, y_predict,'k-')
    pylab.show()
    print(theta)
    print('¡Listo!')
Este último algoritmo es más eficiente que el anterior, además el lector, si así lo desea, puede modificarlo para establecer su propio criterio de parada. Para ir finalizando, merece mencionar que el problema de la regresión lineal se puede resolver de una manera más eficiente que los dos métodos anteriores, siempre y cuando en la ecuación $\nabla_{\theta}E(\theta)=\frac{1}{m} X^\top(X\theta-Y)$ la matriz $ X^\top X$ sea invertible, podemos optimizar $E$ empleando el criterio de los puntos críticos de la primera y segunda derivada. En este caso nosotros encontramos que el parámetro $\theta$ que deseamos es: \begin{equation} \theta = (X^\top X)^{-1}X^\top Y \end{equation} Esta última expresión se puede implementar fácilmente en Python así que se sugiere como ejercicio para el lector. Finalmente, espero compartir pronto con ustedes otra agradable entrada de aplicaciones de las matemáticas y la programación.

Referencias

28 oct. 2012

MathJaX

¿Cómo publicar ecuaciones con código TeX en Blogger? La respuesta es MathJax. Como se encuentra en Wikipedia; MathJax es una biblioteca javascript que permite visualizar fórmulas matemáticas en navergadores web, utilizando los lenguajes de marcado LaTeX o MathML. MathJax tiene licencia libre y soporta múltiples navegadores. El proyecto MathJax nació en 2009 como sucesor de una biblioteca de JavaScript anterior llamada jsMath. Está liderado por Design Science y patrocinado por American Mathematical Society, Design Science, y Society for Industrial An Applied Mathematics. Entre los sitios web que usan MathJax se encuentran MathSciNet, GitHub Project Euclid journals y el portal All-Russian Mathematical.

¿Cómo se utiliza MathJax en Blogger? Sólo hay que ir a tu cuenta de Blogger, buscar \Diseño \Plantilla \Editar HTML, y luego pegar después entre <head> y <\head> el siguiente código:

<script 
    src="http://cdn.mathjax.org/mathjax/latest/MathJax.js" 
    type="text/javascript"> 
    MathJax.Hub.Config({ HTML: ["input/TeX","output/HTML-CSS"],  
    TeX: { extensions: ["AMSmath.js","AMSsymbols.js"], 
    equationNumbers: { autoNumber: "AMS" } }, 
    extensions: ["tex2jax.js"], 
    jax: ["input/TeX","output/HTML-CSS"],  
    tex2jax: { inlineMath: [ ['\$','\$'], ["\\(","\\)"] ], 
    displayMath: [ ['\$\$','\$\$'], ["\\[","\\]"] ], 
    processEscapes: true }, 
    "HTML-CSS": { availableFonts: ["TeX"], 
    linebreaks: { automatic: true } } }); 
</script>

Una vez se ha pegado este código en la plantilla de nuestro blog, se puede utilizar inmediatamente desde nuestro editor de entradas. Por ejemplo: si deseamos escribir una ecuación centrada como;$$F(x)=\int_{a}^{b}f(x)dx$$ entonces debemos escribirp; el comando \$\$F(x)=\int_{a}^{b}f(x)dx\$\$. Si deseamos escribir un ecuación en una línea de texto como $x^{2}$ tan sólo tenemos que escribir lo siguiente \$x^{2}\$.

Espero que disfrute de está herramienta. Nos vemos pronto.

9 oct. 2010

TpX Project

TpX es un sencillo editor gráfico para Windows que permite incluir gráficos en  TeX. También se puede utilizar como un editor independiente para gráficos vectoriales, además es un programa Open Source, es decir con licencia de software libre. El código fuente de este software está libre de cargo, y cualquier personas puede realizar cambios para satisfacer mejor sus necesidades. Los términos exactos de la licencia utilizada de este proyecto está en su página http://tpx.sourceforge.net. Para unirse al proyecto, se pueden poner en contacto con los administradores del proyecto, como se muestra en la página. Además en el fichero de Sourceforge pueden encontrar el código fuente y los resultados finales de la aplicación.

Algunas características de la última versión:

  • Permite manejar grupo de objetos y desagrupar operaciones.
  • Es posible usar el tamaño de fuente por defecto del documento principal de LaTeX estableciendo la propiedad FontSizeInTeX. (Ver)
  • Más propiedades para los objetos gráficos, ya se puede cambiar sus caracteística usando la barra de herramientas (para todos los objetos seleccionados a la vez): puntas de flecha, las etiquetas de texto, las estrellas.
  • TPX ahora añade los paquetes que necesita de forma automática y los paquetes que no sean necesarios no se agregan al documento preliminar de LaTeX.

Bueno si desean más información visiten la página: http://tpx.sourceforge.net.