R prędkość danych. Tabela
Mam konkretny problem z wydajnością, który chciałbym omówić bardziej ogólnie, jeśli to możliwe.
Kontekst:
Bawiłem się w Google Colab z przykładowym kodem Pythona dla agenta Q-Learning, który kojarzy stan i akcję z wartością za pomocą defaultdict:
self._qvalues = defaultdict(lambda: defaultdict(lambda: 0))
return self._qvalues[state][action]
Nie jest ekspertem, ale rozumiem, że zwraca wartość lub dodaje i zwraca 0, jeśli klucz nie zostanie znaleziony.
adaptuję część tego w R.,
problem polega na tym, że nie wiem, ile mam kombinacji stanów / wartości, a technicznie rzecz biorąc, nie powinienem wiedzieć, ile stanów wydaje mi się.
Na pierwszy pojechałem w niewłaściwy sposób, przy czym rbindod data.frames, a że był bardzo powolny.
Następnie zamieniłem mój obiekt R na data.frame(state, action, value = NA_real). działa, ale nadal jest bardzo powolny. innym problemem jest to, że mój obiekt data.frame ma maksymalny rozmiar, który może być problematyczny w przyszłości.
potem zmieniłem mój data.framena a data.table, który dał mi najgorsze wyniki, a potem w końcu zindeksowałem go według (stan, akcja).
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:
Porównując googlecolab / python z moją lokalną implementacją R, Google wykonuje dostęp 1000x10e4 do obiektu w powiedzmy 15 sekund, podczas gdy mój kod wykonuje dostęp 100x100 w 28 sekund. Uzyskałem 2-sekundowe ulepszenia dzięki kompilacji bajtów, ale to wciąż szkoda.
Używając profvis, widzę, że większość czasu spędza na uzyskiwaniu dostępu do data.table na tych dwóch wywołaniach:
qval <- self$qvalues[J(state, action), nomatch = NA_real_]$qvalue
self$qvalues[J(state, action)]$qvalue <- value
Naprawdę nie wiem, co ma Google, ale mój komputer to bestia. Widziałem również, że niektóre testy porównawcze data.tablebyły szybsze niż pandas, więc przypuszczam, że problem leży w moim wyborze kontenera.
Pytania:
- czy moje użycie data.table jest niewłaściwe i czy można to naprawić, aby ulepszyć i dopasować implementację języka Python?
- Czy jest możliwy inny projekt, aby uniknąć deklarowania wszystkich kombinacji stanu / działań, które mogłyby stanowić problem, gdyby wymiary stały się zbyt duże?
- widziałem pakiet hash, czy to jest droga?
Wielkie dzięki za każdą wskazówkę!
AKTUALIZACJA:
dzięki za cały wkład. Więc to, co zrobiłem, to zastąpienie 3 dostępu do moich danych.table, korzystając z twoich sugestii:
#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]
skróciło to czas działania z 33 do 21 sekund, co jest ogromną poprawą, ale nadal jest bardzo powolne w porównaniu z defaultdictimplementacją Pythona .
Zauważyłem, co następuje:
praca wsadowa: nie sądzę, żebym mógł to zrobić, ponieważ wywołanie funkcji zależy od poprzedniego wywołania.
peudospin> Widzę, że jesteś zaskoczony, że uzyskanie jest czasochłonne. ja też, ale to właśnie stwierdza 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)
})
W tym momencie, jeśli nie będzie już sugestii, stwierdzę, że data.table jest po prostu zbyt wolna dla tego rodzaju zadań i powinienem przyjrzeć się użyciu pliku envlub a collections. (zgodnie z sugestią: R szybkie wyszukiwanie pojedynczego elementu z listy vs data.table vs hash )
WNIOSEK:
Wymieniłem data.tablena a collections::dicti wąskie gardło całkowicie zniknęło.
Odpowiedzi
data.tablejest szybki do wyszukiwania i manipulacji w bardzo dużych tabelach danych, ale nie będzie szybki w dodawaniu wierszy jeden po drugim, jak w słownikach Pythona. Spodziewałbym się, że będzie to kopiowanie całej tabeli za każdym razem, gdy dodasz wiersz, który najwyraźniej nie jest tym, czego chcesz.
Możesz spróbować użyć środowisk (które są czymś w rodzaju hashmap) lub jeśli naprawdę chcesz to zrobić w R, możesz potrzebować specjalistycznego pakietu, tutaj jest link do odpowiedzi z kilkoma opcjami.
Reper
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)
jasne jest, że użycie seqzajmuje więcej czasu niż wywołanie 1:nb. plus użycie produktu kartezjańskiego sprawia, że kod jest szybszy, nawet gdy 1:nbjest używany