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, 4 de abril de 2025

El test FizzBuzz en C# y cómo abusar del lenguaje C# versión 8+

Rebuscando en mis archivos viejos me encontré una implementación del algoritmo FizzBuzz. Lo mencionan en esta entrada https://www.variablenotfound.com/2007/02/dnde-se-han-ido-los-programadores.html y también lo mencionan acá https://exponentis.es/el-test-de-fizz-buzz-para-contratar-programadores como uno de los tests básicos para contratar programadores.

Me divertí con este test hace casi veinte años, y por pura nostalgia pongo acá mi primera implementación de aquellos años y que es tan aburrida y genérica que hasta una AI podría hacerla:

for (int i = 1; i <= 100; i++)
{
    t = i % 3;
    c = i % 5;
    tc = i % 15;
    if (t == 0 || c == 0 || tc == 0)
    {
if (t == 0)
    Console.WriteLine("Fizz");
if (c == 0)
    Console.WriteLine("Buzz");
if (tc == 0)
    Console.WriteLine("Fizz Buzz");
    }
    else
Console.WriteLine(i);
}

Esta primera implementación tiene un detalle: para los múltiplos de 15 imprime
Fizz
Buzz
Fizz Buzz

Si sólo se quiere "Fizz Buzz" para los múltiplos de 15 debe ser:

int t = 0, c = 0, tc = 0;

for (int i = 1; i <= 100; i++)
{
    t = i % 3;
    c = i % 5;
    tc = i % 15;

    if (t == 0 || c == 0 || tc == 0)
    {
        if (tc == 0)
            Console.WriteLine("Fizz Buzz");
        else if (c == 0)
            Console.WriteLine("Buzz");
        else if (t == 0)
            Console.WriteLine("Fizz");
    }
    else
        Console.WriteLine(i);
}

Console.ReadLine();

La trampa en este test es que 15 es múltiplo de 3 y 5, así que se debe evaluar si es múltiplo de 15 primero si no se desea imprimir lo demás, de todos modos esto depende de si se considera correcto o no imprimir sólo "Fizz Buzz" con los múltiplos de 15 o si se acepta también que imprima "Fizz" y "Buzz" para este caso; todo depende de la salida que se desee, lo que se considera correcto en este contexto, más las pruebas unitarias... lo clásico del desarrollo de software.

Si se ignoran estos detalles y nos concentramos en el verdadero objetivo del test: saber si se es capaz de escribir un programita que haga algo y que implique un bucle con condicionales, sigue siendo mortalmente aburrido.


Para no aburrirnos tanto, acá hay una implementación que acabo de hacer, en una lambda recursiva porque le tengo manía a las lambdas recursivas:

Func<int, int> F2 = null;
F2 = i =>
{
    int t2, c2, tc2;
    if (i == 101)
        return 0;
    else
    {
        t2 = i % 3;
        c2 = i % 5;
        tc2 = i % 15;
        if (t2 == 0 || c2 == 0 || tc2 == 0)
        {
            if (tc2 == 0)
                Console.WriteLine("Fizz Buzz");
            else if (c2 == 0)
                Console.WriteLine("Buzz");
            else if (t2 == 0)
                Console.WriteLine("Fizz");
        }
        else
            Console.WriteLine(i);
        return i = F2(i + 1);
    }
};
Console.WriteLine("llamando a Función Lambda Recursiva");
F2(0);

Lo sigo viendo aburrido, es el mismo código pero metido en una función lambda. 

Por otro lado, estos días he estado revisando algunas novedades del lenguaje C# y encontré que hay otra forma de escribir la declaración switch, más info en estos enlaces:

https://www.csharp.com/article/c-sharp-12s-switch-expressions-a-more-powerful-alternative-to-traditional-switch-st/

https://stackoverflow.com/questions/44355630/how-to-use-c-sharp-tuple-value-types-in-a-switch-statement

Lo bonito es que también admite tuplas, y esos "ifs/elses" están que me piden ser convertidos en un switch al estilo C# a partir de la versión 8. Un detalle de los nuevos switch es que ya vienen con los breaks de forma implícita, de modo que si cumplen con una de las condiciones, el código no sigue cayendo en las demás. 
Luego de juguetear un poco, otro detalle que encontré es que las tuplas no necesitan ser de sólo dos valores, pueden tener más. En este caso tengo tres variables qué evaluar: tc2, c2 y tc2, además del contador. El resultado: una tupla de cuatro valores (yo la llamaría "cuadrupla").

El código es:

Func<int, int> F2 = null;
F2 = i =>
{
    int t2, c2, tc2;
    if (i == 101)
        return 0;
    else
    {
        t2 = i % 3;
        c2 = i % 5;
        tc2 = i % 15;
                           
        var resultado = (t2, c2, tc2, i);
        Console.WriteLine(resultado switch
        {
            (_, _, 0, > 14) => "Fizz Buzz",
            (_, 0, _, > 4) => "Buzz",
            (0, _, _, > 2) => "Fizz",
            _ => i,
        });
        return i = F2(i + 1);
    }
};
Console.WriteLine("llamando a Función Lambda Recursiva");
Console.WriteLine();
F2(0);

Se ve rarísimo y se puede poner peor, nos podemos deshacer de tanta variable y acortar el condicional:
Func<int, int> F2 = null;
F2 = i =>
{
    var resultado = (i % 3, i % 5, i % 15, i);
    Console.WriteLine(resultado switch
    {
        (_, _, 0, > 14) => "Fizz Buzz",
        (_, 0, _, > 4) => "Buzz",
        (0, _, _, > 2) => "Fizz",
        _ => i,
    });
    return i == 100 ? 0 : F2(i + 1);
};
Console.WriteLine("llamando a Función Lambda Recursiva");
Console.WriteLine();
F2(0);

Y puede ser aún peor, ya dejando de lado el switch podemos abusar de los IEnumerables de C# y terminar con esperpentos como el siguiente:

    Func<IEnumerable<string>, IEnumerable<string>> F3 =
    f =>
       f.Select(c => int.TryParse(c, out _) && int.Parse(c) % 15 == 0 ? c = "Fizz Buzz" : c)
       .Select(c => int.TryParse(c, out _) && int.Parse(c) % 3 == 0 ? c = "Fizz" : c)
       .Select(c => int.TryParse(c, out _) && int.Parse(c) % 5 == 0 ? c = "Buzz" : c);

Console.WriteLine("llamando a Función Lambda que ya no es Recursiva pero se ve más fea");
Console.WriteLine();

IEnumerable<string> f0 = F3(Enumerable.Range(1, 100).Select(c=> c.ToString()));

foreach (var item in f0)
{
    Console.WriteLine(item);
}

Console.ReadLine();

La función TryParse la uso sólo para evaluar si la variable c se puede convertir a entero, de otro modo arroja error de formato. Esa característica del C# de usar el guión largo para ignorar parámetros le da al código un aspecto medio esotérico que me encanta :)

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.