Acerca de:

Este blog contiene los códigos, ejemplos y bases de datos que he usado cuando aprendía acerca de algún tema específico. En lugar de borrarlos (una vez dominado ya el tema), he decidido publicarlos :)

viernes, 10 de enero de 2025

Implementando el algoritmo ADALINE

La idea fue convertir el algoritmo perceptron de esta estrada en el algoritmo ADALINE. La diferencia entre ambos algoritmos está explicada en https://pabloinsente.github.io/the-adaline y es casi mínima. En esta web fue donde conocí inicialmente el algoritmo ADALINE y donde el algoritmo perceptron está mejor explicado. La conversión de uno a otro parecía simple, y en realidad lo fue luego de superar las siguientes dificultades:

Primera dificultad: esta imagen es errónea:

La correcta es:
La forma correcta se deduce del código fuente en la web.

Segunda dificultad:
No especifica cómo debe actualizarse el parámetro bias b de la ecuación del error


e(w,x)=(y(b+wx))2

Mirando los códigos fuente en https://pabloinsente.github.io/the-adaline y en https://medium.com/@mr.sk12112002/understanding-adaline-da79ab8bbc5a  se ve que el bias es la suma de las derivadas de la función del error multiplicada por el ratio de aprendizaje, es decir:

bias +=sum(y(b+wx)) * lr

Tercera dificultad: los códigos de las webs que revisé son para un input X unidimensional o bidimensional con sólo dos valores por muestra, el mió tiene 30 valores por cada muestra.. Revisando la teoría y el código que modifiqué para el perceptron noté dónde debía hacer el cambio. 

El código resultante es:

from tkinter import Y
import numpy as np
np.set_printoptions(suppress=True)
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn import datasets

class Perceptron:
    
    def sign(self, a):
        if a > 0:
            return 1
        else:
            return 0
    

    def standard_scaler(self, X):
        mean = X.mean(0)
        sd = X.std(0)
        return (X - mean)/sd

    def fit(self, X, y, n_iter = 10**3, lr = 0.001, add_intercept = True, standardize = True):        
        # Add Info #
        if standardize:
            X = self.standard_scaler(X)
        if add_intercept:
            ones = np.ones(len(X)).reshape(-1, 1)
            
        self.X = X
        self.N, self.D = self.X.shape
        self.y = y
        self.n_iter = n_iter
        self.lr = lr
        self.converged = False
        self.accuracy = 0

        vsign = np.vectorize(self.sign)
        
        # Fit #
        beta = 0
        w = np.random.randn(self.D)/5
        yhat = []
        
        for i in range(int(self.n_iter)):
            yhat = np.dot(self.X, w) + beta
            yhat = vsign(yhat)

            # Check for convergence
            if np.all(yhat == self.y):
                self.converged = True
                self.iterations_until_convergence = i
                break
                
            # Otherwise, adjust
            for n in range(self.N):
                yhat_n = self.sign(np.dot(w, self.X[n]) + beta)
                delta = 2 * self.lr * (self.y[n] - yhat_n)
                w += delta * self.X[n]
                beta += np.cumsum(delta)

        # Return Values #
        self.beta = beta
        self.w = w
        self.yhat = yhat
        
        print(self.y[:50])
        print(self.yhat[:50])
        
        accuracy_true = self.y == self.yhat
        self.accuracy = np.count_nonzero(accuracy_true)/self.N * 100
        
        print("accuracy: ", self.accuracy, "%")
        
    def predict(self, X, y, N):
        accuracy_true = 0
        for n in range(N):
            yhat_n = self.sign(np.dot(self.w, self.X[n]) + self.beta)
        
            if (y[n] == yhat_n):
                accuracy_true += 1
                
        accuracy_true = accuracy_true/N * 100
        print("accuracy: ", accuracy_true, "%")
        
    
def main():
    # import data
    cancer = datasets.load_breast_cancer()
    X = cancer['data']
    y = cancer['target']
    perceptron = Perceptron()
    perceptron.fit(X[:-5], y[:-5], n_iter = 1.5e2, lr = 0.01)
    yy= np.atleast_1d(y[-5:])
    perceptron.predict(X[-5:], yy,  yy.size)
    
      
if __name__ == "__main__":
    main()


Al ejecutarlo resulta:


La exactitud es mejor que con el algoritmo perceptron tal y como indica la teoría, pues el algoritmo ADALINE ajusta sus parámetros en cada iteración, mientras que el perceptron sólo los ajusta cuando obtiene un resultado erróneo.

lunes, 30 de diciembre de 2024

Tutorial MUY viejito: Usando el Puerto Serial Con Visual Basic 6 y Visual C Sharp 2005

Motivo de poner algo tan viejo acá:: lo tenía tirado en mi disco duro, es todo un documento histórico. 

Antes de empezar con este tutorial recomiendo leer: http://www.lammertbies.nl/comm/info/RS-232_null_modem.html porque puede pasar que no se sepa de qué estoy hablando (que es lo más seguro, este tutorial ya tiene veinte años).



Manejo del Puerto Serie: Visual Basic 6


Manejar el puerto serie en VB6 es bastante sencillo con el control MSCOMM. 
Es posible controlar por separado las líneas RTS y DTR por si se las desea manipular independientemente al envío y recepción de datos. Para ello la propiedad de Handshaking debe estar en "none".
En este link: 
https://www.frro.utn.edu.ar/repositorio/catedras/electrica/2_anio/fundamentos_informatica/apuntes/visual_basic/Manejo%20Puertos.pdf (warning: tecnología antigua) da toda la teoría necesaria para tener un control total del puerto serie con VB6, especialmente el manejo de eventos. Los eventos le dicen al MSCOMM si ha habido algún cambio en el puerto; el tipo de evento es recibido por la propiedad CommEvent y según su valor se puede saber qué ha ocurrido (qué pin ha cambiado su estado). Para esto, la evaluación de CommEvent se hace dentro de OnComm del control MSCOMM (en realidad es el único evento que tiene este control).

Para leer caracteres correctamente en una aplicación tipo chat, es mejor dejar la propiedad RThreshold en 1. Así el evento de lectura se disparará cada vez que se reciba un caracter.

Y aquí http://uttinfor.tripod.com/index/id4.html explican las propiedades del MSCOMM.

Aquí se puede descargar mi programa y el código fuente de ejemplo que envía y recibe datos de texto, activa y desactiva las señales RTS y DTR (manipulando las propiedades RTSEnable y DTREnable respectivamente), y evalúa el estado de las líneas CD, DSR y CTS, así como si el puerto puede o no abrirse (en el caso que ya esté abierto por otro programa).

 



Manejo del Puerto Serie: C Sharp 2005


Acá las cosas son un poco más complicadas. Puede optarse por usar el ocx de Visual Basic MSCOMM eligiendo el respectivo componente .com al hacer click sobre el cuadro de herramientas->Elegir Elementos. Pero entonces no tendría sentido hacerlo desde C Sharp en lugar del mismo Visual Basic 6. En este ejemplo he querido usar la plataforma .Net para controlar el puerto serial.

Primero se debe añadir la línea:
using System.IO.Ports; 


Luego, desde en el cuadro de herramientas buscar "serialPort" y jalar este componente a nuestra aplicación (que será del tipo WindowsApplication).

El problema con C Sharp es que la recepción de datos no puede hacerse de manera tan directa como con Visual Basic 6. El evento que detecta que se ha recibido data no puede tratarse igual. 
Debe hacerse tal y como lo muestran en la página 176 de este documento: http://www.slideshare.net/Metaconta/pic-rs232-puerto-serie-con-pic16f84a-presentation 

Yo lo hice exactamente como se indica ahí y me salió :) 


Otro problema que tuve fue el no poder cambiar las propiedades de los controles (como Labels o PictureBox) tan libremente como en VB6. El C Sharp dispara una excepción cada vez que se intenta cambiar las propiedades de los controles en tiempo de ejecución como protección en un entorno multitarea. Para desactivar esta excepción puse en Form1_Load (de la documentación de Microsoft):
CheckForIllegalCrossThreadCalls = false;

Así ya se puede cambiar ciertas cositas en los controles que son necesarias, como se verá más adelante.

Para enviar data del tipo texto, basta la instrucción:
serialPort1.Write(txtEnviar.Text);

Donde txtEnviar es un RichTextBox que contiene el texto a enviar por el puerto.

Para recibir la data se llama a la propiedad: serialPort1.ReadExisting(); que lee todo el buffer de lectura del puerto.

Tuve algunos problemas con el salto de línea a la hora de recibir los datos. Para corregirlos, y hacer que después de enviar el texto salte a la siguiente línea de forma automática en el RichText que recibe los datos, tuve que añadir el caracter 13 (el salto) al texto en txtEnviar y recién enviar la data. Luego restablecía el texto original en txtEnviar sin el caracter 13 al final. Para esto usé la variable data_temp.

La diferencia con Visual Basic, es que C Sharp, a la hora de abrir el puerto, manda las señales CD, DSR y CTS a "true" ó 1. Si se desea usar estas líneas para recibir señales y disparar el evento que detecta cambios en ellas se debe trabajar con la lógica invertida. Es decir que el dispositivo que nos envía esas señales debe usar lógica invertida también.

En cambio si se desea enviar las señales de control RTS y DTR, la lógica de trabajo dependerá de cómo las interprete el dispositivo que las recibe (esto es válido también para VB6). Para este caso, también consideré lógica invertida para las señales RTS y DTR. 1 es desactivado, 0 es activado.
Así, en el control serialPort se colocan sus propiedades RtsEnable y DtrEnable como "true" para que tengan estos valores al iniciar la aplicación. Si se desea activar, su estado pasará a "false".

Para controlar los eventos, o los cambios en las líneas de entrada del puerto serial, me basé en esté código:

Imports System.IO.Ports
Public Class Form1
    Private WithEvents mPort As SerialPort

    Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        mPort = New SerialPort("COM1", 9600)
        mPort.Open()
    End Sub

Private Sub mPort_PinChanged(ByVal sender As Object, ByVal e As System.IO.Ports.SerialPinChangedEventArgs) Handles mPort.PinChanged
        If e.EventType = SerialPinChange.CDChanged Then
            If mPort.CDHolding Then
                Debug.Print("Carrier detect on")
            Else
                Debug.Print("Carrier detect off")
            End If
        End If
    End Sub
End Class

Fuente: http://social.msdn.microsoft.com/Forums/en-US/netfxbcl/thread/ffbb63a1-5a04-46c9-a71a-f52b6f7ada81/ (el enlace ya no existe)


Está hecho para Visual Basic 2005 así que tuve que "traducirlo" a C Sharp y adaptarlo para que reconociera los eventos que me interesaban. La lógica es la misma que con VB6: con un "switch" evaluar qué evento ha ocurrido según el valor de la propiedad que los recibe. En C Sharp es e.EventType y se evalúa versus las propiedades de serialPort CDChanged, CtsChanged y DsrChanged. Estas propiedades sólo saben si hubo un cambio en las señales CD, CTS y DSR, no conocen sus estados (para saber los estados se usan otras propiedades que se explican más abajo).

Ambos programas, el de VB6 y C Sharp funcionan igual y hacen lo mismo. La única diferencia es que el de C Sharp emplea lógica invertida. 

El código fuente y el programa en C Sharp se puede bajar aquí.

Si los "leds" que indican el estado de las señales CD, DSR y CTS en VB6 son controles tipo "Shape", en C Sharp usé controles Picturebox. Dibujar un círculo y un cuadrado en C Sharp es muy engorroso, llegué a crear una clase para ello, pero cuando quería dibujar dos círculos, al final el programa me los dibujaba uno encima de otro sin importar lo que hiciera para evitarlo. Creé dos propiedades para pasarle el valor de las coordenadas, pero igual no funcionó. Haciendo puntos de quiebre descubrí que el C Sharp primero asigna los valores a las variables con los valores de las coordenadas y luego dibuja todo lo que se le pide, usando sólo los valores finales de estas variables. Después de mucho tratar decidí no dibujar nada. Para quienes quieran intentar hallarle una solución, les dejo la clase:

using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;

namespace WindowsApplication1
{
    /* en el programa principal crear un objeto.
    Shapes cuad = new Shapes();
    Estado.Paint += new PaintEventHandler(cuad.Shapes_Fill);
    Estado.Paint += new PaintEventHandler(cuad.Shapes_Paint); 
    */


    class Shapes
    {
        private Graphics g;
        private int _coorX;
        private int _coorY;

        public Shapes()
        {
            _coorX = 30; 
            _coorY = 30; //sólo para inicializarlas
        }

        public int coorX
        {
            get { return _coorX; }
            set { _coorX = value; }
        }

        public int coorY
        {
            get { return _coorY; }
            set { _coorY = value; }
        }

        public void Shapes_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
        
            g = e.Graphics;
            Pen p = new Pen(Color.Black,2) ;
            g.DrawEllipse(p, 30, 30 , 30, 30); 
        }

        public void Shapes_Fill(object sender, System.Windows.Forms.PaintEventArgs e) 
        
            g = e.Graphics;
            Pen p = new Pen(Color.Black, 2);
            //g.DrawEllipse(p, _coorX , _coorY, 30, 30); 
            g.FillEllipse(Brushes.Firebrick, 30, 80, 30, 30); 
        }
    }
}


Al final preferí usar tres picturebox para los "leds" de estado de CD, DRS y CTS, así simplemente cambiaba su color según el estado de estas líneas (para poder hacer esto tuve que poner a "false" la propiedad CheckForIllegalCrossThreadCalls mencionada más arriba). Estos estados se obtienen evaluando las propiedades de serialPort CDHolding, DsrHolding y CtsHolding respectivamente (estas propiedades son las mismas que en VB6).

Otra diferencia que hallé es que si se deja la línea CD sin conexión, VB6 no le hace nada, pero C Sharp la manda a cero al enviar data. Como está sin conectar no hay forma de ponerla a uno nuevamente.

Ambos programas los probé con el test de Loop Back puenteando los puntos 7-8-1, 4-6 y 2-3. Cualquiera de los dos ejecutables pueden usarse como programas independientes para probar el puerto serie. En el caso del C Sharp, el ejecutable está en \WindowsApplication1\

sábado, 26 de octubre de 2024

Tutorial Viejito: Creando array de Controles con sus Eventos en C#

Para una aplicación necesité tener siete CheckBoxes en un formulario indicando los días de la semana. También tengo un array de variables booleanas llamado "dias" cuyos índices (0,1,2,3,4,5,6) representan los días de la semana (L,M,M,J,V,S,D) respectivamente (en C# los índices de los arrays empiezan siempre desde cero). Si selecciono el CkeckBox de los días lunes y miércoles solamente, las variables dias[0] y dias[2] toman el valor de true, el resto de variables "dias" serán false (en la práctica necesitaba el array "dias" para pasárselo a otra clase y luego usarlo para modificar unos campos en una BD...)

Esta vez, en lugar de crear cada CheckBox con el diseñador del Visual Studio quise crearlos como elementos de un array de CheckBoxes. Para este tutorial también voy a crear un array de TextBoxes y otro de Buttons.

La declaración para crear los arrays es como sigue:


Mirando cómo el diseñador del Visual Studio declara y asigna las propiedades a los controles, hice lo mismo para mis arrays de controles, pero dentro de un bucle. Aquí muestro la función que crea los CheckBoxes:

Dentro del bucle asigno los delegados para los eventos respectivos de cada control. Para saber cuál control del array es el que lanza el evento debo examinar el objeto "sender" que es el control quien "envía" o lanza el evento. Para saber el índice del control dentro del array lo almaceno en una propiedad del control: puede ser el nombre, el TabIndex, o el Tag. En realidad puede ser cualquier propiedad cuyo valor no vamos a modificar a lo largo de la aplicación, ya que desde la cual vamos a saber el índice del control dentro del array.

Un dato interesante acerca de los eventos es que estos no se ejecutan al declararlos dentro del bucle que crea los controles. Aquí sólo declaramos el evento y lo asignamos a su respectivo control, el evento recién se ejecuta cuando el control lo "lanza" (es decir: cuando se ejecuta la acción que llama al evento. Esto me ha recordado un poco a la "Evaluación Perezosa" de la programación funcional, no sé si será exactamente lo mismo, pero se le parece mucho).

Las dos líneas con el comentario "poned un breakpoint acá" hacen exactamente lo mismo: Si deseo leer el índice guardado en el nombre del control que lanza el evento, utilizo la variable "n", o más directamente puedo leer el índice usando la propiedad "Tag". Ambas líneas de código cambian el valor del elemento con índice "n" ó índice "Tag" (ambos tienen el mismo valor) del array "dias" a verdadero ó falso, según sea el valor de la propiedad "Checked" del CheckBox con índice "n" ó "Tag" dentro del array de ChackBoxes.

Y esta es la función de los TextBoxes:


En el caso de los TextBoxes, mandarán un saludo al texto de su formulario (el cual se llama "Form1") al recibir el cursor (el índice del respectivo TextBox se lee desde la propiedad "Tag"):



Si se pone un punto de quiebre en las líneas donde dice "poned un breakpoint acá" se podrá ver cómo la ejecución del programa "salta" a la declaración del evento para cambiar los valores a los elementos del array "dias" cuando se pone o quita el "check" de los CheckBoxes.

¿Y qué sucede si deseo enviar mensajes a otros formularios? Debo crear un delegado y su respectivo evento:

También debo crear el otro formulario que va a recibir los mensajes. Lo he llamado Form2, sólo posee un label llamado "label1" y éste es su código:

Ahora voy a crear un array de botones en Form1 con los que le mandaré mensajes a Form2:

Debo tener cuidado de evaluar que mi evento "Enviar" no sea nulo o lanzará una excepción. Esto ocurre si Form2 no se ha cargado. Luego añado un botón desde la barra de herramientas del Visual Studio con el que abriré Form2:


El código del nuevo botón (llamado btnForm2) es::

 Si ejecuto la aplicación, y abro también Form2, se verá cómo recibe los mensajes desde los botones creados en el array:

 

Esta aplicación está compilada con Visual Studio Express 2008 y se puede descargar de aquí.

jueves, 22 de agosto de 2024

Probando el Algoritmo Perceptron de la web Machine Learning from Scratch

Continuación de https://programacionamartillazos.blogspot.com/2024/07/correccion-del-algoritmo-perceptron-de.html

Ahora toca probar los algoritmos. Para esto hice una pequeña modificación en el código de la web Machine Learning from Scratch y al que yo modifiqué:

Código de Machine Learning from Scratch:

from tkinter import Y
import numpy as np
np.set_printoptions(suppress=True)
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn import datasets

class Perceptron:
    
    def sign(self, a):
        return (-1)**(a < 0)

    def to_binary(self, y):
            return y > 0

    def standard_scaler(self, X):
        mean = X.mean(0)
        sd = X.std(0)
        return (X - mean)/sd

    def fit(self, X, y, n_iter = 10**3, lr = 0.001, add_intercept = True, standardize = True):
        
        # Add Info #
        if standardize:
            X = self.standard_scaler(X)
        if add_intercept:
            ones = np.ones(len(X)).reshape(-1, 1)
        self.X = X
        self.N, self.D = self.X.shape
        self.y = y
        self.n_iter = n_iter
        self.lr = lr
        self.converged = False
        
        # Fit #
        beta = np.random.randn(self.D)/5
        for i in range(int(self.n_iter)):
            
            # Form predictions
            yhat = self.to_binary(self.sign(np.dot(self.X, beta)))
            
            # Check for convergence
            if np.all(yhat == self.sign(self.y)):
                self.converged = True
                self.iterations_until_convergence = i
                break
                
            # Otherwise, adjust
            for n in range(self.N):
                yhat_n = self.sign(np.dot(beta, self.X[n]))
                if (self.y[n]*yhat_n == -1):
                    beta += self.lr * self.y[n]*self.X[n]

        # Return Values #
        self.beta = beta
        self.yhat = self.to_binary(self.sign(np.dot(self.X, self.beta)))
        
        print(self.y[:50])
        print(self.yhat[:50])
        
        accuracy_true = self.y == self.yhat
        self.accuracy = np.count_nonzero(accuracy_true)/self.N * 100
        
        print("accuracy: ", self.accuracy, "%")
        
    def predict(self, X, y, N):
        accuracy_true = 0
        for n in range(N):
            yhat_n = self.to_binary(self.sign(np.dot(self.beta, X[n])))
        
            if (y[n] == yhat_n):
                accuracy_true += 1
                
        accuracy_true = accuracy_true/N * 100
        print("accuracy: ", accuracy_true, "%")
        
    
def main():
    # import data
    cancer = datasets.load_breast_cancer()
    X = cancer['data']
    y = cancer['target']
    perceptron = Perceptron()
    perceptron.fit(X[:-10], y[:-10], n_iter = 2e2, lr = 0.01)
    yy= np.atleast_1d(y[-10:])
    perceptron.predict(X[-10:], yy,  yy.size)
    
      
if __name__ == "__main__":
    main()


Código que modifiqué:

from tkinter import Y
import numpy as np
np.set_printoptions(suppress=True)
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn import datasets

class Perceptron:
    
    def sign(self, a):
        if a > 0:
            return 1
        else:
            return 0
    

    def standard_scaler(self, X):
        mean = X.mean(0)
        sd = X.std(0)
        return (X - mean)/sd

    def fit(self, X, y, n_iter = 10**3, lr = 0.001, add_intercept = True, standardize = True):
        
        # Add Info #
        if standardize:
            X = self.standard_scaler(X)
        if add_intercept:
            ones = np.ones(len(X)).reshape(-1, 1)
            
        self.X = X
        self.N, self.D = self.X.shape
        self.y = y
        self.n_iter = n_iter
        self.lr = lr
        self.converged = False
        self.accuracy = 0

        vsign = np.vectorize(self.sign)
        
        # Fit #
        beta = np.random.randn(self.D)/5
        
        for i in range(int(self.n_iter)):
            # Form predictions
            
            yhat = np.dot(self.X, beta)
            yhat = vsign(yhat)

            # Check for convergence
            if np.all(yhat == self.y):
                self.converged = True
                self.iterations_until_convergence = i
                break
                
            # Otherwise, adjust
            for n in range(self.N):
                yhat_n = self.sign(np.dot(beta, self.X[n]))
        
                if (self.y[n] != yhat_n):
                    beta += self.lr * self.y[n]*self.X[n]

        # Return Values #
        self.beta = beta
        self.yhat = yhat
        
        print(self.y[:50])
        print(self.yhat[:50])
        
        accuracy_true = self.y == self.yhat
        self.accuracy = np.count_nonzero(accuracy_true)/self.N * 100
        
        print("accuracy: ", self.accuracy, "%")
        
    def predict(self, X, y, N):
        accuracy_true = 0
        for n in range(N):
            yhat_n = self.sign(np.dot(self.beta, X[n]))
        
            if (y[n] == yhat_n):
                accuracy_true += 1
                
        accuracy_true = accuracy_true/N * 100
        print("accuracy: ", accuracy_true, "%")
        
    
def main():
    # import data
    cancer = datasets.load_breast_cancer()
    X = cancer['data']
    y = cancer['target']
    perceptron = Perceptron()
    perceptron.fit(X[:-10], y[:-10], n_iter = 2e2, lr = 0.01)
    yy= np.atleast_1d(y[-10:])
    perceptron.predict(X[-10:], yy,  yy.size)
    
      
if __name__ == "__main__":
    main()



El cálculo de la exactitud se hace en dos momentos, con la data de entrenamiento (dentro de la función fit), y con la data de validación (también de prueba en este caso) dentro de la función predict.

Dentro de la función main, lo que se hace es partir la data en dos pedazos: la data de entrenamiento que no incluye los 10 últimos elementos que se pasan a la función fit, y luego los diez últimos elementos que serán la data de evaluación o prueba, que se pasarán como parámetro a la función predict.

En el algoritmo perceptrón el único parámetro que se debe optimizar es beta (es un vector de valores en realidad), el cual luego se usará en la predicción. Los resultados son:

Código de Machine Learning from Scratch:


 

 

 

 

 

 

Código que modifiqué:


 

 

 

 

El primer valor "accuracy" es el calculado para la data de entrenamiento, el segundo valor es el resultado con la data de testeo.

Ahora otra prueba pero con menos data de entrenamiento, para esto se cambia en la función main donde dice 10 por cualquier otro valor (debe ser el mismo). Para este ejemplo, voy a coger 100 elementos para que sean los de testeo:


 

 

 

 

 

El resultado es:

Código de Machine Learning from Scratch:


Código Modificado:


Ambos códigos son equivalentes, la diferencia entre ambos es menor al 1%, siendo el código original el que calcula el parámetro beta un poquito mejor. 

Por otro lado, esto demuestra que el resultado depende de la cantidad de data que tengamos para entrenar el algoritmo. En este ejemplo es muy poca data. Una forma de compensarlo es aumentando el número de iteraciones; pero poner demasiadas, con apenas unos pocos cientos de elementos, se corre el riesgo de "overfitting": es decir sobreentrenar nuestro algoritmo de forma que sólo tenga la precisión necesaria con la data de entrenamiento.

viernes, 19 de julio de 2024

Corrección del Algoritmo Perceptrón de la web Machine Learning from Scratch

 Revisaba el algoritmo percentrón, como se explica en los tutoriales que hay por internet, es un algoritmo que recibe una entrada binaria (1 o 0), que suele ser un vector, donde cada elemento del vector está asociado a un peso. Además cada elemento del vector tiene asociado una etiqueta binaria. Lo que se desea con el algoritmo perceptrón es hallar parámetros (también llamados "pesos") que predigan correctamente la etiqueta si se ingresa un nuevo vector.

El perceptrón sólo actualiza sus pesos si predice mal una etiqueta. No siempre es necesario un condicional (un if... else...), si una etiqueta ha sido predicha correctamente, el error será cero y los pesos mantendrán su valor. Esta idea se usa en How To Implement The Perceptron Algorithm From Scratch In Python - MachineLearningMastery.com

El algoritmo implementado en esta web https://dafriedman97.github.io/mlbook/content/c3/s2/perceptron.html es uno de los más simples que he encontrado, pero hay un error. Teóricamente el algoritmo Perceptrón debería aproximarse bastante a una clasificación correcta, pero casi nunca del 100%. En esta web siempre resulta que el algoritmo no converge.

Hallé dos errores. una es en la función sign. No es necesario que devuelva {-1, +1}. Las etiquetas ya están clasificadas como {0, 1} así que las predicciones del perceptrón pueden dejarse con esos valores. Al cambiar la función sign también tuve que cambiar estas líneas:

 En mi caso basta evaluar cada elemento del vector de etiquetas:

 

El otro error es al evaluar si el algoritmo converge. Pide una exactitud del 100% para la convergencia:

Esto casi nunca se consigue. No eliminé estas líneas porque en casos muy raros sí puede cumplirse que las etiquetas predichas y las reales sean todas iguales.  

El código corregido es:

from tkinter import Y
import numpy as np
np.set_printoptions(suppress=True)
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn import datasets

class Perceptron:
    
    def sign(self, a):
        if a > 0:
            return 1
        else:
            return 0
    

    def standard_scaler(self, X):
        mean = X.mean(0)
        sd = X.std(0)
        return (X - mean)/sd

    def fit(self, X, y, n_iter = 10**3, lr = 0.001, add_intercept = True, standardize = True):
        
        # Add Info #
        if standardize:
            X = self.standard_scaler(X)
        if add_intercept:
            ones = np.ones(len(X)).reshape(-1, 1)
            
        self.X = X
        self.N, self.D = self.X.shape
        self.y = y
        self.n_iter = n_iter
        self.lr = lr
        self.converged = False
        self.accuracy = 0

        vsign = np.vectorize(self.sign)
        
        # Fit #
        beta = np.random.randn(self.D)/5
        
        for i in range(int(self.n_iter)):
            # Form predictions
            
            yhat = np.dot(self.X, beta)
            yhat = vsign(yhat)

            # Check for convergence
            if np.all(yhat == self.y):
                self.converged = True
                self.iterations_until_convergence = i
                break
                
            # Otherwise, adjust
            for n in range(self.N):
                yhat_n = self.sign(np.dot(beta, self.X[n]))
        
                if (self.y[n] != yhat_n):
                    beta += self.lr * self.y[n]*self.X[n]

        # Return Values #
        self.beta = beta
        self.yhat = yhat
        
        print(self.y[:50])
        print(self.yhat[:50])
        
        accuracy_true = self.y == self.yhat
        self.accuracy = np.count_nonzero(accuracy_true)/self.N * 100
        
        print("accuracy: ", self.accuracy, "%")
        
    
def main():
    # import data
    cancer = datasets.load_breast_cancer()
    X = cancer['data']
    y = cancer['target']
    perceptron = Perceptron()
    perceptron.fit(X, y, n_iter = 1e2, lr = 0.01)
      
if __name__ == "__main__":
    main() 


El resultado es: