R velocidad de data.table

Aug 19 2020

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 rbindde data.frames 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.framea 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.tableera más rápido que pandas, así que supongo que el problema radica en mi elección de contenedor.

Preguntas:

  1. ¿Mi uso de una tabla de datos es incorrecto y puede corregirse para mejorar y coincidir con la implementación de python?
  2. ¿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?
  3. 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 defaultdictimplementació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:

y aquí el código de la función como referencia:

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 envo 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.tablepor a collections::dicty el cuello de botella desapareció por completo.

Respuestas

2 pseudospin Aug 19 2020 at 02:45

data.tablees 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.

AbdessabourMtk Aug 19 2020 at 04:53

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 seqconsume 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:nbse usa