R velocità di data.table

Aug 19 2020

Ho un problema di prestazioni specifico, che desidero estendere più in generale, se possibile.

Contesto:

Ho giocato su google colab con un esempio di codice python per un agente Q-Learning, che associa uno stato e un'azione a un valore utilizzando un defaultdict:

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

Non sono un esperto, ma la mia comprensione è che restituisce il valore o aggiunge e restituisce 0 se la chiave non viene trovata.

sto adattando parte di questo in R.
il problema è che non so quante combinazioni stato/valori ho, e tecnicamente non dovrei sapere quanti stati immagino.
All'inizio ho sbagliato strada, con la rbinddi data.framee quella era molto lenta.
Ho quindi sostituito il mio oggetto R con un file data.frame(state, action, value = NA_real). funziona ma è ancora molto lento. un altro problema è che il mio oggetto data.frame ha la dimensione massima che potrebbe essere problematica in futuro.
poi ho cambiato il mio data.framein a data.table, che mi ha dato le prestazioni peggiori, quindi l'ho finalmente indicizzato per (stato, azione).

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:

Confrontando googlecolab/python con la mia implementazione R locale, google esegue l'accesso 1000x10e4 all'oggetto diciamo in 15 secondi, mentre il mio codice esegue l'accesso 100x100 in 28 secondi. Ho ottenuto miglioramenti di 2 secondi mediante la compilazione di byte, ma è ancora un peccato.

Usando profvis, vedo che la maggior parte del tempo viene speso per accedere a data.table su queste due chiamate:

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

Non so davvero cosa abbia Google, ma il mio desktop è una bestia. Inoltre ho visto alcuni benchmark che affermavano che data.tableera più veloce di pandas, quindi suppongo che il problema risieda nella mia scelta del contenitore.

Domande:

  1. il mio uso di un data.table è sbagliato e può essere corretto per migliorare e abbinare l'implementazione di Python?
  2. è possibile un altro progetto per evitare di dichiarare tutte le combinazioni stato/azioni che potrebbero essere un problema se le dimensioni diventano troppo grandi?
  3. ho visto del pacchetto hash, è la strada da percorrere?

Grazie mille per qualsiasi suggerimento!

AGGIORNARE:

Grazie per tutti gli input. Quindi quello che ho fatto è stato sostituire 3 accessi al mio data.table usando i tuoi suggerimenti:

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

questo ha ridotto il tempo di esecuzione da 33 a 21 secondi, il che rappresenta un enorme miglioramento, ma è ancora estremamente lento rispetto defaultdictall'implementazione di Python.

Ho notato quanto segue:
lavorare in batch: non credo di poterlo fare poiché la chiamata alla funzione dipende dalla chiamata precedente.
peudospin> Vedo che sei sorpreso che ottenere richieda molto tempo. lo sono anch'io ma è quello che afferma profvis:

e qui il codice della funzione come riferimento:

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

A questo punto, se non ci sono più suggerimenti, concluderò che data.table è semplicemente troppo lento per questo tipo di attività e dovrei esaminare l'utilizzo di an envo a collections. (come suggerito qui: R ricerca rapida di un singolo elemento da list vs data.table vs hash )

CONCLUSIONE:

Ho sostituito il data.tablefor a collections::dicte il collo di bottiglia è completamente scomparso.

Risposte

2 pseudospin Aug 19 2020 at 02:45

data.tableè veloce per eseguire ricerche e manipolazioni in tabelle di dati molto grandi, ma non sarà veloce nell'aggiungere righe una per una come i dizionari Python. Mi aspetto che copi l'intera tabella ogni volta che aggiungi una riga che chiaramente non è quella che desideri.

Puoi provare a utilizzare ambienti (che sono qualcosa come una hashmap), oppure se vuoi davvero farlo in R potresti aver bisogno di un pacchetto specializzato, ecco un collegamento a una risposta con alcune opzioni.

AbdessabourMtk Aug 19 2020 at 04:53

Prova delle prestazioni

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)

è chiaro che l'utilizzo seqrichiede più tempo che chiamare il 1:nb. inoltre l'utilizzo di un prodotto cartesiano rende il codice più veloce anche quando 1:nbviene utilizzato