lapply vs for loop – Performance R

Si dice spesso che si dovrebbe preferire lapply over for loop. Ci sono alcune eccezioni, come ad esempio Hadley Wickham sottolinea nel suo libro Advance R.

( http://adv-r.had.co.nz/Functionals.html ) (Modifica in atto, ricorsione ecc.). Quello che segue è uno di questi casi.

Solo per motivi di apprendimento, ho provato a riscrivere un algoritmo perceptron in una forma funzionale al fine di valutare le prestazioni relative. fonte ( https://rpubs.com/FaiHas/197581 ).

Ecco il codice.

 # prepare input data(iris) irissubdf <- iris[1:100, c(1, 3, 5)] names(irissubdf) <- c("sepal", "petal", "species") head(irissubdf) irissubdf$y <- 1 irissubdf[irissubdf[, 3] == "setosa", 4] <- -1 x <- irissubdf[, c(1, 2)] y <- irissubdf[, 4] # perceptron function with for perceptron <- function(x, y, eta, niter) { # initialize weight vector weight <- rep(0, dim(x)[2] + 1) errors <- rep(0, niter) # loop over number of epochs niter for (jj in 1:niter) { # loop through training data set for (ii in 1:length(y)) { # Predict binary label using Heaviside activation # function z <- sum(weight[2:length(weight)] * as.numeric(x[ii, ])) + weight[1] if (z < 0) { ypred <- -1 } else { ypred <- 1 } # Change weight - the formula doesn't do anything # if the predicted value is correct weightdiff <- eta * (y[ii] - ypred) * c(1, as.numeric(x[ii, ])) weight <- weight + weightdiff # Update error function if ((y[ii] - ypred) != 0) { errors[jj] <- errors[jj] + 1 } } } # weight to decide between the two species return(errors) } err <- perceptron(x, y, 1, 10) ### my rewriting in functional form auxiliary ### function faux <- function(x, weight, y, eta) { err <- 0 z <- sum(weight[2:length(weight)] * as.numeric(x)) + weight[1] if (z < 0) { ypred <- -1 } else { ypred <- 1 } # Change weight - the formula doesn't do anything # if the predicted value is correct weightdiff <- eta * (y - ypred) * c(1, as.numeric(x)) weight <<- weight + weightdiff # Update error function if ((y - ypred) != 0) { err <- 1 } err } weight <- rep(0, 3) weightdiff <- rep(0, 3) f <- function() { t <- replicate(10, sum(unlist(lapply(seq_along(irissubdf$y), function(i) { faux(irissubdf[i, 1:2], weight, irissubdf$y[i], 1) })))) weight <<- rep(0, 3) t } 

Non mi aspettavo alcun miglioramento coerente a causa dei problemi sopra citati. Tuttavia, sono rimasto davvero sorpreso quando ho visto il brusco peggioramento usando lapply e replicate .

Ho ottenuto questi risultati utilizzando la funzione microbenchmark dalla libreria microbenchmark

Quali potrebbero essere le ragioni? Potrebbe esserci qualche perdita di memoria?

  expr min lq mean median uq f() 48670.878 50600.7200 52767.6871 51746.2530 53541.2440 perceptron(as.matrix(irissubdf[1:2]), irissubdf$y, 1, 10) 4184.131 4437.2990 4686.7506 4532.6655 4751.4795 perceptronC(as.matrix(irissubdf[1:2]), irissubdf$y, 1, 10) 95.793 104.2045 123.7735 116.6065 140.5545 max neval 109715.673 100 6513.684 100 264.858 100 

La prima funzione è la funzione lapply / replicate

Il secondo è la funzione con cicli for

Il terzo è la stessa funzione in C++ usando Rcpp

Qui Secondo Roland la profilazione della funzione. Non sono sicuro di poterlo interpretare nel modo giusto. A me sembra che la maggior parte del tempo venga speso nella sottomissione del profilo di funzione

Prima di tutto, è già un lungo mito sfatato che for loop è più lento di lapply . I loop for in R sono stati resi molto più performanti e sono attualmente almeno veloci quanto lapply .

Detto questo, devi ripensare al tuo uso di lapply qui. L’implementazione richiede l’assegnazione all’ambiente globale, poiché il codice richiede l’aggiornamento del peso durante il ciclo. E questa è una ragione valida per non considerare lapply .

lapply è una funzione che dovresti usare per i suoi effetti collaterali (o mancanza di effetti collaterali). La funzione lapply combina automaticamente i risultati in una lista e non lapply con l’ambiente in cui lavori, al contrario di un ciclo for . Lo stesso vale per la replicate . Vedi anche questa domanda:

La famiglia di R sta applicando più dello zucchero sintattico?

La ragione per cui la tua soluzione lapply è molto più lenta, è perché il tuo modo di usarlo crea un sovraccarico in più.

  • replicate non è nient’altro che internamente, quindi in realtà si combinano in modo sapply e lapply per implementare il doppio ciclo. sapply crea overhead in più perché deve verificare se il risultato può essere semplificato o meno. Quindi un ciclo for sarà effettivamente più veloce rispetto all’utilizzo di replicate .
  • all’interno della tua funzione anonimo lapply , devi accedere al dataframe per xey per ogni osservazione. Ciò significa che -contrario nel tuo ciclo for, ad esempio la funzione $ deve essere chiamata ogni volta.
  • Poiché utilizzi queste funzioni di fascia alta, la tua soluzione ‘lapply’ chiama 49 funzioni, rispetto alla soluzione for che chiama solo 26. Queste funzioni extra per la soluzione lapply includono chiamate a funzioni come match , structure , [[ , names , %in% , sys.call , duplicated , … Tutte le funzioni non necessarie per il ciclo for quanto non eseguono nessuno di questi controlli.

Se vuoi vedere da dove proviene questo overhead, guarda il codice interno di replicate , sapply , sapply e simplify2array .

È ansible utilizzare il seguente codice per avere un’idea migliore di dove si perde la performance con lapply . Esegui questa linea per linea!

 Rprof(interval = 0.0001) f() Rprof(NULL) fprof <- summaryRprof()$by.self Rprof(interval = 0.0001) perceptron(as.matrix(irissubdf[1:2]), irissubdf$y, 1, 10) Rprof(NULL) perprof <- summaryRprof()$by.self fprof$Fun <- rownames(fprof) perprof$Fun <- rownames(perprof) Selftime <- merge(fprof, perprof, all = TRUE, by = 'Fun', suffixes = c(".lapply",".for")) sum(!is.na(Selftime$self.time.lapply)) sum(!is.na(Selftime$self.time.for)) Selftime[order(Selftime$self.time.lapply, decreasing = TRUE), c("Fun","self.time.lapply","self.time.for")] Selftime[is.na(Selftime$self.time.for),] 

In realtà,

Ho provato la differenza con un problema che risolve di recente.

Provaci.

Nella mia conclusione, non ho alcuna differenza, ma per il mio caso il ciclo era insignificante più veloce di lapply.

Ps: provo soprattutto a mantenere la stessa logica in uso.

 ds <- data.frame(matrix(rnorm(1000000), ncol = 8)) n <- c('a','b','c','d','e','f','g','h') func <- function(ds, target_col, query_col, value){ return (unique(as.vector(ds[ds[query_col] == value, target_col]))) } f1 <- function(x, y){ named_list <- list() for (i in y){ named_list[[i]] <- func(x, 'a', 'b', i) } return (named_list) } f2 <- function(x, y){ list2 <- lapply(setNames(nm = y), func, ds = x, target_col = "a", query_col = "b") return(list2) } benchmark(f1(ds2, n )) benchmark(f2(ds2, n )) 

Come si può vedere, ho fatto una semplice routine per creare una named_list basata su un dataframe, la funzione func fa estrarre i valori della colonna, f1 usa un ciclo for per scorrere il dataframe e f2 usa una funzione lapply.

Nel mio computer ottengo questi risultati:

 test replications elapsed relative user.self sys.self user.child 1 f1(ds2, n) 100 110.24 1 110.112 0 0 sys.child 1 0 

&&

  test replications elapsed relative user.self sys.self user.child 1 f1(ds2, n) 100 110.24 1 110.112 0 0 sys.child 1 0