R velocidad de data.table
Tengo un problema de rendimiento específico, que deseo extender de manera más general si es posible.
Contexto:
He estado jugando en google colab con una muestra de código de python para un agente de Q-Learning, que asocia un estado y una acción a un valor usando un dictado predeterminado:
self._qvalues = defaultdict(lambda: defaultdict(lambda: 0))
return self._qvalues[state][action]
No soy un experto, pero tengo entendido que devuelve el valor o agrega y devuelve 0 si no se encuentra la clave.
Estoy adaptando parte de esto en R.
El problema es que no sé cuántas combinaciones de estado/valor tengo, y técnicamente no debería saber cuántos estados supongo.
Al principio me fui por el camino equivocado, con la rbind
de data.frame
s y eso fue muy lento.
Luego reemplacé mi objeto R con un data.frame(state, action, value = NA_real)
. funciona pero sigue siendo muy lento. otro problema es que mi objeto data.frame tiene el tamaño máximo, lo que podría ser problemático en el futuro.
luego cambié my data.frame
a a data.table
, lo que me dio el peor rendimiento, luego finalmente lo indexé por (estado, acción).
qvalues <- data.table(qstate = rep(seq(nbstates), each = nbactions),
qaction = rep(seq(nbactions), times = nbstates),
qvalue = NA_real_,
stringsAsFactors = FALSE)
setkey(qvalues, "qstate", "qaction")
Problema:
Comparando googlecolab/python con mi implementación local de R, Google realiza un acceso de 1000x10e4 al objeto en, digamos, 15 s, mientras que mi código realiza un acceso de 100x100 en 28 s. Obtuve mejoras de 2s por compilación de bytes, pero eso sigue siendo una lástima.
Usando profvis
, veo que la mayor parte del tiempo se dedica a acceder a data.table en estas dos llamadas:
qval <- self$qvalues[J(state, action), nomatch = NA_real_]$qvalue
self$qvalues[J(state, action)]$qvalue <- value
Realmente no sé qué tiene Google, pero mi escritorio es una bestia. También vi algunos puntos de referencia que indicaban que data.table
era más rápido que pandas
, así que supongo que el problema radica en mi elección de contenedor.
Preguntas:
- ¿Mi uso de una tabla de datos es incorrecto y puede corregirse para mejorar y coincidir con la implementación de python?
- ¿Es posible otro diseño para evitar declarar todas las combinaciones de estado/acciones que podrían ser un problema si las dimensiones se vuelven demasiado grandes?
- He visto sobre el paquete hash, ¿es el camino a seguir?
¡Muchas gracias por cualquier indicación!
ACTUALIZAR:
gracias por todos los aportes Entonces, lo que hice fue reemplazar 3 accesos a mi data.table usando sus sugerencias:
#self$qvalues[J(state, action)]$qvalue <- value
self$qvalues[J(state, action), qvalue := value]
#self$qvalues[J(state, action),]$qvalue <- 0
self$qvalues[J(state, action), qvalue := 0]
#qval <- self$qvalues[J(state, action), nomatch = NA_real_]$qvalue
qval <- self$qvalues[J(state, action), nomatch = NA_real_, qvalue]
esto redujo el tiempo de ejecución de 33 s a 21 s, lo que es una gran mejora, pero sigue siendo extremadamente lento en comparación con la defaultdict
implementación de python.
Noté lo siguiente:
trabajando en lote: no creo que pueda hacerlo ya que la llamada a la función depende de la llamada anterior.
peudospin> Veo que está sorprendido de que la obtención lleve mucho tiempo. yo también, pero eso es lo que dice profvis:

QAgent$set("public", "get_qvalue", function( state, action) {
#qval <- self$qvalues[J(state, action), nomatch = NA_real_]$qvalue
qval <- self$qvalues[J(state, action), nomatch = NA_real_, qvalue]
if (is.na(qval)) {
#self$qvalues[self$qvalues$qstate == state & self$qvalues$qaction == action,]$qvalue <- 0
#self$qvalues[J(state, action),]$qvalue <- 0
self$qvalues[J(state, action), qvalue := 0]
return(0)
}
return(qval)
})
En este punto, si no hay más sugerencias, concluiré que data.table es demasiado lento para este tipo de tarea, y debería considerar usar un env
o un collections
. (como se sugiere allí: R búsqueda rápida de un solo elemento de la lista vs data.table vs hash )
CONCLUSIÓN:
Reemplacé el data.table
por a collections::dict
y el cuello de botella desapareció por completo.
Respuestas
data.table
es rápido para realizar búsquedas y manipulaciones en tablas de datos muy grandes, pero no será rápido para agregar filas una por una como los diccionarios de Python. Espero que copie toda la tabla cada vez que agregue una fila que claramente no es lo que desea.
Puede intentar usar entornos (que son algo así como un hashmap), o si realmente desea hacer esto en R, es posible que necesite un paquete especializado, aquí hay un enlace a una respuesta con algunas opciones.
Punto de referencia
library(data.table)
Sys.setenv('R_MAX_VSIZE'=32000000000) # add to the ram limit
setDTthreads(threads=0) # use maximum threads possible
nbstates <- 1e3
nbactions <- 1e5
cartesian <- function(nbstates,nbactions){
x= data.table(qstate=1:nbactions)
y= data.table(qaction=1:nbstates)
k = NULL
x = x[, c(k=1, .SD)]
setkey(x, k)
y = y[, c(k=1, .SD)]
setkey(y, NULL)
x[y, allow.cartesian=TRUE][, c("k", "qvalue") := list(NULL, NA_real_)][]
}
#comparing seq with `:`
(bench = microbenchmark::microbenchmark(
1:1e9,
seq(1e9),
times=1000L
))
#> Unit: nanoseconds
#> expr min lq mean median uq max neval
#> 1:1e+09 120 143 176.264 156.0 201 5097 1000
#> seq(1e+09) 3039 3165 3333.339 3242.5 3371 21648 1000
ggplot2::autoplot(bench)
(bench = microbenchmark::microbenchmark(
"Cartesian product" = cartesian(nbstates,nbactions),
"data.table assignement"=qvalues <- data.table(qstate = rep(seq(nbstates), each = nbactions),
qaction = rep(seq(nbactions), times = nbstates),
qvalue = NA_real_,
stringsAsFactors = FALSE),
times=100L))
#> Unit: seconds
#> expr min lq mean median uq max neval
#> Cartesian product 3.181805 3.690535 4.093756 3.992223 4.306766 7.662306 100
#> data.table assignement 5.207858 5.554164 5.965930 5.895183 6.279175 7.670521 100
#> data.table (1:nb) 5.006773 5.609738 5.828659 5.80034 5.979303 6.727074 100
#>
#>
ggplot2::autoplot(bench)
está claro que el uso seq
consume más tiempo que llamar al 1:nb
. además, usar un producto cartesiano hace que el código sea más rápido incluso cuando 1:nb
se usa