Semillas y estados en generación de números aleatorios
18 May 2023 - Memo y Nepo
Cuando estuvimos trabajando en la estimación del tamaño poblacional de petrel cenizo no logramos independencia en las pruebas.
Con independencia nos referimos a que podemos correr las pruebas en cualquier orden o solo subconjuntos de ellas y el resultado no cambia.
Al agregar un describe
o un it
cambiaban algunos resultados en las pruebas.
También notamos que los resultados eran diferentes dependiendo de cómo corremos las pruebas: solo un archivo, en la instalación o todas las pruebas.
Resolvimos parcialmente el problema agregando semillas.
Agregamos semillas a las pruebas: en el inicio de los archivos de pruebas, al inicio de los describe
y hasta en cada uno de los it
.
Intentamos fijar las semillas antes de cada prueba con set.seed()
.
Y al final de cada prueba tratamos de restablecer el .Random.seed
con set.seed(NULL)
.
Este procedimiento lo repetimos en el código de producción.
Primero como argumento opcional y después las fijamos desde la definición de las funciones.
Nada nos regresaba la independencia de las pruebas.
Xie nos señaló que había diferentes ambientes y por eso mismo diferentes .Random.seed
.
La manera correcta de restablecer esa variable era con tres funciones.
Con la primera obtenemos el estado global del .Random.seed
.
get_rand_state <- function() {
get0(".Random.seed", envir = .GlobalEnv, inherits = FALSE)
}
Con la segunda función podemos regresar al estado de .Random.seed
que guardamos:
set_rand_state <- function(state) {
if (!is.null(state)) {
assign(".Random.seed", state, envir = .GlobalEnv, inherits = FALSE)
}
}
Debemos usar la función on.exit()
para asegurar que fijemos el estado de la variable .Random.seed
al salir de alguna función.
Por ejemplo:
my_f <- function() {
old_state <- get_rand_state()
on.exit(set_rand_state(old_state))
}
Tampoco logramos la independencia de las pruebas con la recomendación de Xie.
Parecía que alguna función modificaba la secuencia de números aleatorios. Pensábamos que los resultados no eran los esperados porque generamos números aleatorios anidados. Con números aleatorios anidados nos referimos a funciones aleatorias que usan funciones aleatorias. Entonces intentamos evitar los números aleatorios anidados.
Por esto decidimos quitar el antipatrón trenecito. Trenecito es el patrón en las pruebas donde utilizamos otras funciones para generar las variables de entrada de las funciones que probaremos. En este caso, usábamos funciones nuestras y de terceros para generar las matrices de historia de capturas. En lugar de usar esas funciones, consignamos esas matrices en archivos. Así, evitamos el patrón de trenecito. Pero tampoco nos regreso la independencia de las pruebas.
Con todo lo anterior, aseguramos que podíamos obtener el mismo valor de .Random.seed
.
A pesar de eso, los resultados de las pruebas seguían dependiendo de cómo las corríamos.
Eso nos ayudó a darnos cuenta que el generador de números aleatorios cambiaba.
Creemos que ese cambio era en el RNGkind
.
Intentamos fijar el estado de esa variable y de todas formas los resultados cambian.
Al final nos quedamos con la impresión de que este comportamiento era debido a las estimaciones de población abierta.
Como la función markCJS
que utilizamos en run_open_scenario
.
Conclusiones:
- Aunque el trenecito no era el problema, ese patrón nos complicó la búsqueda.
- Sabemos que al fijar una semilla tenemos el mismo valor de
.Random.seed
, aunque no es el único participante en la generación de números aleatorios. También está elRNGkind
. - El problema aún lo tenemos. Las pruebas tienen dependencia entre cómo las corremos y hasta en el orden en el que las corremos.
- Nos falta buscar aquí el generador que usa la función
parallel::clusterSetRNGStream
.