R velocità di data.table
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 rbind
di data.frame
e 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.frame
in 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.table
era più veloce di pandas
, quindi suppongo che il problema risieda nella mia scelta del contenitore.
Domande:
- il mio uso di un data.table è sbagliato e può essere corretto per migliorare e abbinare l'implementazione di Python?
- è possibile un altro progetto per evitare di dichiarare tutte le combinazioni stato/azioni che potrebbero essere un problema se le dimensioni diventano troppo grandi?
- 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 defaultdict
all'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:

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 env
o a collections
. (come suggerito qui: R ricerca rapida di un singolo elemento da list vs data.table vs hash )
CONCLUSIONE:
Ho sostituito il data.table
for a collections::dict
e il collo di bottiglia è completamente scomparso.
Risposte
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.
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 seq
richiede più tempo che chiamare il 1:nb
. inoltre l'utilizzo di un prodotto cartesiano rende il codice più veloce anche quando 1:nb
viene utilizzato