top of page

COSAS VEREDES

  • fjroar
  • 25 ene
  • 4 Min. de lectura

Investigando y repasando el mundo de los fractales, me viene a la mente algunas ideas donde se mezcla por un lado la frialdad del mundo de la aleatoriedad y por otro la belleza de la geometría de Mandelbrot.


En este sentido, acabando el libro de Fractals for de Classroom, me encuentro en su capítulo 6 observaciones sobre el cuidado que hay que poner a la hora de elegir un generador de números aleatorios, elemento éste muy utilizado en aplicaciones reales cuando se quieren realizar, por ejemplo, simulaciones tipo Montecarlo en riesgo de mercado.


En este sentido, no sé hasta donde se tiene idea de cómo se generan los números aleatorios en un ordenador, pero desde luego son todo menos aleatorios y se suelen denominar más propiamente como “pseudo-aleatorios”.


Así pues, una prueba geométrica para ver si un sistema cumple con esa condición de aleatoriedad, es ver si es capaz de generar o no un triángulo de Sierpinski tal y como se muestra a continuación:

ree

La idea geométrica para realizar se basa en dar los siguientes pasos:


-        Se fijan 3 vértices de un triángulo que en este caso son el (0; 0), el (4; 0) y el (2; 3’46)

-        Posteriormente se elije un punto inicial como pueda ser el (2; 1)

-        Y se generan el resto de los puntos siguiendo la regla:

 

ree

Donde V es uno de los anteriores 3 vértice que se escoge aleatoriamente en cada

iteración con n suficientemente grande, aquí se supone que n va desde 1 a 100.000

 

Pues bien, si os entretenéis en ejecutar el siguiente programa se obtiene la anterior figura:

 

set.seed(123)

n_points <- 100000

vertices <- matrix(c(0, 0,

                     4, 0, 

                     2, 3.46),

                   ncol = 2, byrow = TRUE)

current_point <- c(2, 1)

sierpinski_points <- matrix(NA, nrow = n_points, ncol = 2)

guardaVerticesAleatorio <- numeric(n_points)

 

for (i in 1:n_points) { 

  guardaVerticesAleatorio[i] <- sample(1:3, 1)

  chosen_vertex <- vertices[guardaVerticesAleatorio[i], ] 

  current_point <- (current_point + chosen_vertex) / 2

  sierpinski_points[i, ] <- current_point

}

plot(sierpinski_points, pch = ".", col = "blue", xlab = "", ylab = "", asp = 1,

     main = "Triángulo de Sierpinski")

 

La ejecución del anterior código sólo requiere Rbase y ante esto uno puede pensar que lo anterior es sencillo y puede que hasta haya escuchado hablar de generadores de números pseudoaleatorios basados en sucesiones con operaciones modulares del tipo:

                                                                                                                                                       

ree

 

O incluso algunas de tipo más sofisticado como las de tipo Fibonacci:


ree

Pensando en un m suficientemente grande como 2^18 para evitar ciclos sistemáticos.

 

Entonces ¿Qué sucedería si tomamos uno de tipo Fibonacci tal y como se generaría a continuación?:

 

# Generador de Fibonnacci

m <- 2^18         

r <- numeric(n_points)   

r[1] <- 5

r[2] <- 7

for(k in c(3:100000)){

  r[k] <- (r[k-2] + r[k-1]) %% m

}

# Transformar los valores al rango deseado {1, 2, 3}

r_mod <- (r %% 3) + 1

 

Pues bien, lo que sucedería es que a pesar de que esta secuencia tendría este aspecto:

ree

Y la generada por R tendría este otro aspecto:

ree

No haciendo pensar aparentemente en ninguna diferencia en cuanto a la “independencia” de estas generaciones.


Sin embargo, si se ejecuta el programa completo basado en el generador de Fibonacci (ver anexo), el resultado es el siguiente (nótese la diferencia en la altura que en este gráfico llega a 3):


ree

Es decir, esta serie de números no es capaz de rellenar el triángulo de Sierpinski tal y como se indica el mencionado libro.


Por tanto resulta curioso cómo cuando se consigue estar cerca de la aleatoriedad con un algoritmo suficientemente fiable se es capaz de tener una figura de cierta estética, mientras que cuando nos apartamos del “caos” el resultado aunque ofrece cierta regularidad, no es capaz de aportar completitud, por lo que resulta importante conocer si los algoritmos que sustentan la generación de números pseudo-aleatorios son suficientemente fiables, en el caso de R el módulo del algoritmo (por defecto se usa el Mersenne – Twister) es igual a 2^19937 – 1 dando una idea de la complejidad del problema frente al anterior basado en Fibonacci.

 

Anexo: Segunda parte del código R al completo:


vertices <- matrix(c(0, 0,  # Vértice 1

                     4, 0,  # Vértice 2

                     2, 3.46), # Vértice 3 (altura de un triángulo equilátero)

                   ncol = 2, byrow = TRUE)

current_point <- c(2, 1)

sierpinski_points <- matrix(NA, nrow = n_points, ncol = 2)

r = 0

m <- 2^18         

r <- numeric(n_points)

r[1] <- 5

r[2] <- 7

for(k in c(3:100000)){

  r[k] <- (r[k-2] + r[k-1]) %% m 

}

r_mod <- (r %% 3) + 1

 

for (i in 1:n_points) {

  chosen_vertex <- vertices[r_mod[i], ]

  current_point <- (current_point + chosen_vertex) / 2

 

  sierpinski_points[i, ] <- current_point

}

plot(sierpinski_points, pch = ".", col = "blue", xlab = "", ylab = "", asp = 1,

     main = "Triángulo de Sierpinski")

 
 
 

Aprovechando que estoy investigando algoritmos que tienen que ver con la Luna, me encuentro con uno en particular, muy sencillo y que es la base de la mayoría de los algoritmos astronómicos como es el cálculo del día juliano.


Aunque no me voy a poner describir este algoritmo en este momento, lo que me llama la atención es el gran uso que hace de una función que se enseña en lo que ahora es segundo o tercero de la ESO (o al menos eso creo y espero) y antes era séptimo u octavo y que me parece incluso hasta más interesante que la fórmula de I = Crt/100 que para los preocupados en que se enseñe como pedir créditos a los chavales (véase algún comentario de linkedin anterior que hice), también se enseña por ese tramo de edad.

Pues bien, la gráfica de la función resulta ser la siguiente:


Imagen generada en R con plotly usando la función floor()
Imagen generada en R con plotly usando la función floor()

Así pues, como la mayoría de la gente sabe, por ejemplo, la parte entera de -1.65 es -2 y si se quiere mayor detalle se puede consultar en https://eliatron.blogspot.com/2012/05/cuanto-vale-la-parte-entera-de-165.html

Sin embargo, el problema que se tiene en Astronomía es que el grado de precisión importa y por ejemplo, si hago la pregunta de ¿Cuál es la parte entera de -2.001? Estoy seguro que casi todo el mundo acierta y me dice -3, de hecho, si en R hago eso, observo lo mismo con tan sólo teclear lo siguiente:

                                                           floor(-2.001)


O al que le guste Python (que también los hay y tendrán que llamar a una librería):


                                                           import math

math.floor(-2.01)

 

Sin embargo ¿Cuál sería el resultado de lo anterior con:


                                                           floor(-2.0000000000000001)

O con:

                                           import math

math.floor(-2.0000000000000001)

 

La respuesta sorprendente, al menos en mi máquina de de 64bit, hp de 32gb de ram es que el resultado es -2 y sólo hay 16 decimales tras la coma, lo cual en algoritmos de alta precisión os podéis imaginar qué sucede.


Lo anterior, me obligó en la función que tengo par el cálculo de días julianos en mi librería RMoon y que podéis encontrar en https://github.com/FJROAR/RMoon/blob/main/R/JulianDayMeeus.R  a definir una función especial (por recomendación del libro de Jean Meeus y del responsable del Grupo de Observación Lunar de la Agrupación Astronómica de Madrid https://www.aam.org.es/, el gran Alberto Martos a programar algo específico para evitar errores que diese problemas en el cálculo del día juliano y por tanto para ser preciso en el algoritmo encontráis la función goodInteger():

   

goodInteger <- function(number){

for (i in c(1: length(number))){

  number[i] <- ifelse(number[i] < 0, as.integer(number[i]) - 1, as.integer(number[i]))

}


return(number)


}

 

Que soluciona este problema y ofrece el valor correcto pero ¿Por qué es tan importante calcular bien la función parte entera de un número? Pues bien, en el algoritmo del cálculo del día juliano, cuyo concepto parte de Justus Scaliger (1540 – 1609) en 1583, existen algunas piezas claves, (y que incorporo en RMoon) siendo una de las importantes la siguiente:

 

JD <- goodInteger(365.25 (Y + 4716)) + goodInteger(30.6001 (M + 1)) + D + B - 1524.5

 

No voy a hablar del algoritmo en sí y de todo lo que significa la anterior línea, pero sí me voy a detener en el primer sumando y en la última resta, es decir, en el último número que se descompone como 1524.5 = 1461 + 62 + 1.5. Esta línea se puede re-escribir del siguiente modo para separar los efectos en 2 partes distintas:

 

JD1 <- goodInteger(365.25 * (Y + 4716)) – (1461 + 1.5)

JD2 <- goodInteger(30.6001 *(M + 1)) + D + B – 62

 

Fijémonos en JD1, ya que JD2 sirve para otros temas que tienen que ver con la reforma de Gregorio XIII de 1582.


Partiendo de la base de que Y es un año cualquiera. ¿Qué sucede cuando por ejemplo es el año 2022? Pues en ese caso el valor que toma JD1 sería:


[365.25 * (2022+4716)] – 1462.5 = [2461054.5] – 1462.5 = 2461054 – 1462.5 = 2459591.5


Es decir, al no ser 2022 bisiesto el decimal 0.5 no tiene efecto y se anula y no se cuenta y se acaban contando 365 días que se resta a una cantidad fija, pero es que si se considera por ejemplo el año 2023, el resultado sería:


[365.25 * (2023+4716)] – 1462.5 = [2461419.75] – 1462.5 = 2460689 – 1462.5 = 2459956.5


Y ahora, que el redondeo natural sería una cifra más por ser 0.75 lo que hay dentro de la función parte entera, también desaparece y se sigue contando esos 365 día, pero fijémonos que cuando se alcanza el año 2024, sucede lo siguiente:


[365.25 * (2024+4716)] – 1462.5 = [2461785] – 1462.5 = 2461785 – 1462.5 = 2460322.5


¡Ahora no se eliminan decimales! Porque cada 4 años, la parte 0.25 de 365.25 aumenta un día y ese día se mantiene inalterado mientras que, en el resto de los casos, ese 0.25 no tiene efecto y así con una simple línea donde la función parte entera es esencial se van contando los años (¡Ojo! Que como he mencionado hay más cosas en el algoritmo pero quería mostrar la ingeniosidad de la esta función).


Además, se puede comprobar que como no podía ser de otro modo que la diferencia de días entre los años se mantendría del siguiente modo:


                                             2460322.5 – 2459956.5 = 366

                                             2459956.5 – 2459336.5 = 365


Por tanto la función parte entera es muy útil para la configuración de modo eficiente e ingeniosa de algoritmos como el aquí presentado, pero que nos advierte de que hay que tener cuidado con las funciones que vienen de los lenguajes a nivel base o en librerías estándar y que todo el mundo asume que hace bien los cálculos pero que no alcanza el grado de exactitud que se requiere sobre todo en los puntos límite y vuelvo a recordar que en la época actual con el hype de la Inteligencia Artificial donde las funciones ReLu que componen muchos de nuestros algoritmos y que usan funciones similares a la parte entera.


Miedo me da, cuando hay miles de estas funciones entremezcladas donde no se ha hecho un estudio adecuado de si en casos límite, la cosa va o no, después vienen las “alucinaciones”, los errores de predicción inexplicables, … pero si fallamos en la base, después es difícil encontrar por qué todo fue mal.


Finalmente quisiera acabar con que hay que investigar y aprender y esto no va sólo apostar y arriesgar patrimonios y si no lo haces eres un looser, admiro a quien lo hace, pero también admiro al que estudia e indaga y no se queda con la primera respuesta, le da vueltas a las cosas y también se equivoca, este último es muy posible que no llegue a ser millonario, pero tiene altas probabilidades de disfrutar con lo que hace, e investigar el día juliano te lleva a analizar cómo funciona el tiempo, los calendarios, cómo se establece la hora civil, … y eso te llena el alma, aunque no la cuenta bancaria.

 
 
 

Una de mis aficiones predilectas son los fractales, desde que los vería en la carrera allá por el año 97 – 98 siempre me han fascinado y ahora estoy retomando su estudio de mano de un libro que se publicó a principios de los 90 y que lleva por título Fractals for the classroom. Ya en esta época estaba bastante sólida la Teoría de los Fractales introducida por Benoit Mandelbrot en 1975 y que en 1982 publicó su famoso libro Fractal Geometry of Nature (que por cierto lo hojeaba bastante en aquellos años de carrera en la biblioteca y ahora se lo he pedido a los reyes).

ree

Figura 1 Fotografía de la página 59 de la edición de 1992 del libro Fracta for the classroom

 

Pues bien, el primer libro que menciono bastante recomendable, comienza con un primer capítulo bastante sencillo de seguir y que recoge varios experimentos hechos con aparatos de finales de los 80 seguramente en los que se relaciona el tema fractal con la teoría del caos pero de lo que voy a tratar aquí es de un experimento muy sencillo que espero que todo el lector interesado pueda entender aunque no tenga formación matemática y que espero que le sorprenda.


En este libro se hace referencia al modelo de crecimiento población de Verhulst y que propone que, el crecimiento de la población en t debería ser proporcional a la diferencia entre el conteo de la población hasta el momento y el máximo tamaño alcanzable por dicha población, por tanto propone la siguiente fórmula de crecimiento:

              

ree

 

Fuera de los usos de este modelo que también recibe el nombre de modelo logístico. Lo que me resultó sorprendente es lo que se observa en la figura 1 y es cómo una fórmula iterativa como la anterior que no parece muy complicada con valor inicial 0.01  y r = 3, diese con distintos aparatos (cuya única diferencia era que la Casio tenía hasta 10 cifras decimales y HP llegaba a 12) unas diferencias tan brutales con tan sólo 50 iteraciones, pero no sólo pasaba eso, sino que si se reorganizaban los factores de la anterior fórmula del siguiente modo equivalente:

                        

ree

 

Sucedía esto bajo una misma máquina Casio de la época:

ree

Figura 2 Fotografía de la página 61 de la edición de 1992 del libro Fracta for the classroom


Esto me parecía aún más terrible porque ya las iteraciones 45 se parecen como un huevo a una castaña y visto esto pensé que en los 80, las máquinas eran lo que eran, pero que ahora ya, seguro, que estas cosas no pasan y mucho menos con mi querido R y por tanto me puse con mi moderno HP Notebook de 32 GB a ejecutar el siguiente código en R replicando esta última figura 2:


options(digits=17)

options(scipen=999)

r <- 3

p1 <- 0.01

p2 <- 0.01

 

for (i in c(1:100)){

 

                              p1 <- p1 + r p1 (1 - p1)

                              p2 <- (1 + r) p2 - r p2**2

}

 

print(p1)

print(p2)

 

Pues bien, si ejecuto lo anterior obtengo el siguiente resultado:


ree

Si bien es cierto, que he llegado hasta la iteración 100, ya en la 50 la diferencia no era tan brutal y sólo variaba la segunda cifra decimal, pero a pesar de la potencia de la versión 64 bits de R, el panorama está un poco mal.


Claro, en este momento me pongo a pensar en la risa que les tiene que dar esto a los tontacos de python que odian de forma natural a los que programamos en R (¡Ojo! Hay gente que programa en python muy bien y no son nada tontaco y no odian a lo que programamos en R, a esos los aprecio) y que seguro piensan: “Esto en python no pasa” y que me debería pasar a su lado. Pues, la verdad es que se equivocan, a mí que también me gusta bastante python y para algunas cosas está mejor que R y para otras no, precisamente en este tema, no hay ningún tipo de mejora, y es que tanto R como Python están hechos sobre la misma base de lenguaje C y por tanto “chorpresa!!!”:


ree

Tiene lugar el mismo resultado con la misma disparidad y los mismos decimales. Por tanto, Python no nos va a solucionar la papeleta en este sentido tampoco.


Sobre el ejemplo de la Figura 1 no tenía a mano ninguna calculadora programable (y me tendría que poner a recordar cómo se hacía), pero lo que sí puedo hacer es repetir, por ejemplo en R lo anterior con redondeos en un caso hasta la 10 cifra decimal y en el otro hasta la cifra 12 y observo que pasa lo siguiente:


ree

Con lo que queda más que comprobado que la precisión de la máquina influye en el resultado final.


Así pues, habiendo visto lo anteior y dado todo el hype que veo a mi alrededor de diversos entendidos que nos hacen creer que estamos en una época donde la inteligencia artificial poco le falta para superar al humano, creo que no en todos los casos la convergencia de los algoritmos está suficientemente garantizada, además si lo anterior ocurre con modelos tan sencillos ¿Qué no ocurrirá con los actuales LLM, sin olvidar los modelos de analitica más avanzada tipo xgb y otros? Máxime si se hace uso por lo general de espacios multidimensionales y por tanto no es que las condicioines iniciales sean importantes, sino que como hemos visto, en estos casos la característica de la máquina también lo son.


Finalmente y para no extenderme más, el anterior modelo no es estable claramente con ese parámetro r, si se cambia, dicha inestabilidad a veces se soluciona y otras veces no, así pues, pruébese con r = 2 y todo lo anterior no habrá sido más que un mal sueño …     

 
 
 

© 2021 by Francisco J. Rodríguez Aragón. Proudly created with Wix.com

bottom of page