R Geschwindigkeit von data.table
Ich habe ein spezifisches Leistungsproblem, das ich nach Möglichkeit allgemeiner ausweiten möchte.
Kontext:
Ich habe auf Google Colab mit einem Python-Codebeispiel für einen Q-Learning-Agenten herumgespielt, der einen Zustand und eine Aktion mit einem Standardwert einem Wert zuordnet:
self._qvalues = defaultdict(lambda: defaultdict(lambda: 0))
return self._qvalues[state][action]
Kein Experte, aber ich verstehe, dass es den Wert zurückgibt oder addiert und 0 zurückgibt, wenn der Schlüssel nicht gefunden wird.
Ich passe einen Teil davon in R an.
Das Problem ist, dass ich nicht weiß, wie viele Zustands- / Wertekombinationen ich habe, und technisch gesehen sollte ich nicht wissen, wie viele Zustände ich errate.
Zuerst bin ich in die falsche Richtung gegangen, mit dem rbindvon data.frames und das war sehr langsam.
Ich habe dann mein R-Objekt durch eine data.frame(state, action, value = NA_real). es funktioniert, aber es ist immer noch sehr langsam. Ein weiteres Problem ist, dass mein data.frame-Objekt die maximale Größe hat, die in Zukunft problematisch sein könnte.
dann habe ich mein data.framezu a geändert data.table, was mir die schlechteste Leistung gab, dann habe ich es schließlich mit (state, action) indiziert.
qvalues <- data.table(qstate = rep(seq(nbstates), each = nbactions),
qaction = rep(seq(nbactions), times = nbstates),
qvalue = NA_real_,
stringsAsFactors = FALSE)
setkey(qvalues, "qstate", "qaction")
Problem:
Beim Vergleich von googlecolab/python mit meiner lokalen R-Implementierung führt Google einen 1000x10e4-Zugriff auf das Objekt in sagen wir 15 Sekunden aus, während mein Code einen 100x100-Zugriff in 28 Sekunden durchführt. Ich habe 2s-Verbesserungen durch Byte-Kompilierung erhalten, aber das ist immer noch schade.
Mit profvissehe ich, dass die meiste Zeit für den Zugriff auf die data.table bei diesen beiden Aufrufen aufgewendet wird:
qval <- self$qvalues[J(state, action), nomatch = NA_real_]$qvalue
self$qvalues[J(state, action)]$qvalue <- value
Ich weiß nicht wirklich, was Google hat, aber mein Desktop ist ein Biest. Außerdem habe ich einige Benchmarks gesehen, die besagen , dass data.tablewar schneller als pandas, also nehme ich an, dass das Problem in meiner Wahl des Containers liegt.
Fragen:
- ist meine Verwendung einer data.table falsch und kann behoben werden, um die Python-Implementierung zu verbessern und anzupassen?
- Ist ein anderes Design möglich, um zu vermeiden, dass alle Zustands-/Aktionskombinationen deklariert werden, was ein Problem sein könnte, wenn die Dimensionen zu groß werden?
- Ich habe über das Hash-Paket gesehen, ist es der richtige Weg?
Vielen Dank für jeden Hinweis!
AKTUALISIEREN:
danke für all die beiträge. Also habe ich 3 Zugriffe auf meine data.table mit Ihren Vorschlägen ersetzt:
#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]
Dadurch wurde die Laufzeit von 33 s auf 21 s gesenkt, was eine massive Verbesserung darstellt, aber im Vergleich zur Python- defaultdictImplementierung immer noch extrem langsam ist.
Ich habe Folgendes festgestellt:
Arbeiten im Batch: Ich glaube nicht, dass ich das tun kann, da der Aufruf der Funktion vom vorherigen Aufruf abhängt.
peudospin> Ich sehe, Sie sind überrascht, dass das Abrufen zeitaufwändig ist. ich auch, aber das sagt 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)
})
Wenn an dieser Stelle kein weiterer Vorschlag vorliegt, werde ich zu dem Schluss kommen, dass data.table für diese Art von Aufgabe einfach zu langsam ist, und ich sollte die Verwendung von an envoder a prüfen collections. (wie dort vorgeschlagen: R Fast Single Item Lookup from list vs data.table vs hash )
FAZIT:
Ich ersetzte das data.tabledurch a collections::dictund der Engpass verschwand vollständig.
Antworten
data.tableist schnell für Suchvorgänge und Manipulationen in sehr großen Datentabellen, aber es wird nicht schnell sein, Zeilen nacheinander wie Python-Wörterbücher hinzuzufügen. Ich würde erwarten, dass jedes Mal, wenn Sie eine Zeile hinzufügen, die gesamte Tabelle kopiert wird, was eindeutig nicht das ist, was Sie wollen.
Sie können entweder versuchen, Umgebungen zu verwenden (die so etwas wie eine Hashmap sind), oder wenn Sie dies wirklich in R tun möchten, benötigen Sie möglicherweise ein spezielles Paket. Hier ist ein Link zu einer Antwort mit einigen Optionen.
Benchmark
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)
Es ist klar, dass die Verwendung seqmehr Zeit in Anspruch nimmt als das Aufrufen der 1:nb. Außerdem macht die Verwendung eines kartesischen Produkts den Code schneller, selbst wenn 1:nber verwendet wird