R vitesse de data.table

Aug 19 2020

J'ai un problème de performances spécifique, que je souhaite étendre plus généralement si possible.

Le contexte:

J'ai joué sur google colab avec un exemple de code python pour un agent Q-Learning, qui associe un état et une action à une valeur à l'aide d'un defaultdict :

self._qvalues = defaultdict(lambda: defaultdict(lambda: 0))
return self._qvalues[state][action]

Pas un expert mais ma compréhension est qu'il renvoie la valeur ou l'ajoute et renvoie 0 si la clé n'est pas trouvée.

J'adapte une partie de cela dans R.
Le problème est que je ne sais pas combien de combinaisons état/valeurs j'ai, et techniquement, je ne devrais pas savoir combien d'états je suppose.
Au début j'allais dans le mauvais sens, avec le s rbindde data.frames et c'était très lent.
J'ai ensuite remplacé mon objet R par un data.frame(state, action, value = NA_real). ça marche mais c'est encore très lent. un autre problème est que mon objet data.frame a la taille maximale qui pourrait être problématique à l'avenir.
puis j'ai changé mon data.frameen a data.table, ce qui m'a donné les pires performances, puis je l'ai finalement indexé par (état, action).

qvalues <- data.table(qstate = rep(seq(nbstates), each = nbactions),
                        qaction = rep(seq(nbactions), times = nbstates),
                        qvalue = NA_real_,
                        stringsAsFactors =  FALSE)
setkey(qvalues, "qstate", "qaction")

Problème:

En comparant googlecolab/python à mon implémentation R locale, Google effectue un accès 1000x10e4 à l'objet en disons 15s, tandis que mon code effectue un accès 100x100 en 28s. J'ai eu 2s améliorations par byte en compilant mais c'est quand même dommage.

En utilisant profvis, je vois que la plupart du temps est consacré à l'accès à data.table sur ces deux appels :

qval <- self$qvalues[J(state, action), nomatch = NA_real_]$qvalue
self$qvalues[J(state, action)]$qvalue <- value

Je ne sais pas vraiment ce que Google a, mais mon bureau est une bête. J'ai également vu des points de repère indiquant que data.tablec'était plus rapide que pandas, donc je suppose que le problème réside dans mon choix de conteneur.

Des questions:

  1. mon utilisation d'un data.table est-elle erronée et peut-elle être corrigée pour améliorer et correspondre à l'implémentation de python?
  2. une autre conception est-elle possible pour éviter de déclarer toutes les combinaisons état/actions qui pourraient poser problème si les dimensions devenaient trop grandes ?
  3. J'ai vu le paquet de hachage, est-ce la voie à suivre ?

Merci beaucoup pour tout pointeur !

METTRE À JOUR:

Merci pour toute la contribution. Donc, ce que j'ai fait, c'est de remplacer 3 accès à mon data.table en utilisant vos suggestions :

#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]

cela a fait passer le temps d'exécution de 33 à 21 secondes, ce qui est une amélioration considérable, mais cela reste extrêmement lent par rapport à l' defaultdictimplémentation de python.

J'ai noté ceci :
travail en batch : je ne pense pas pouvoir le faire car l'appel à la fonction dépend de l'appel précédent.
peudospin> Je vois que vous êtes surpris que l'obtention prenne du temps. moi aussi mais c'est ce que dit profvis :

et voici le code de la fonction comme référence :

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)
})

À ce stade, s'il n'y a plus de suggestion, je conclurai que data.table est tout simplement trop lent pour ce type de tâche, et je devrais envisager d'utiliser un envou un collections. (comme suggéré ici: R recherche rapide d'un seul élément à partir de la liste vs data.table vs hash )

CONCLUSION:

J'ai remplacé le data.tablepour un collections::dictet le goulot d'étranglement a complètement disparu.

Réponses

2 pseudospin Aug 19 2020 at 02:45

data.tableest rapide pour effectuer des recherches et des manipulations dans de très grandes tables de données, mais il ne sera pas rapide pour ajouter des lignes une par une comme les dictionnaires python. Je m'attendrais à ce qu'il copie l'intégralité du tableau chaque fois que vous ajoutez une ligne qui n'est clairement pas ce que vous voulez.

Vous pouvez soit essayer d'utiliser des environnements (qui ressemblent à un hashmap), soit si vous voulez vraiment le faire dans R, vous aurez peut-être besoin d'un package spécialisé, voici un lien vers une réponse avec quelques options.

AbdessabourMtk Aug 19 2020 at 04:53

Référence

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)

il est clair que l'utilisation prend seqplus de temps que d'appeler le 1:nb. de plus, l'utilisation d'un produit cartésien rend le code plus rapide même lorsqu'il 1:nbest utilisé