lapply vs pour boucle-Performance R

on dit souvent qu'il faut préférer lapply aux for boucles. Il y a quelques exceptions comme par exemple Hadley Wickham souligne dans son Advance R book.

( http://adv-r.had.co.nz/Functionals.html ) (modification en place, récursion, etc.). Le suivant est un de ces cas.

juste pour apprendre, j'ai essayé de réécrire un algorithme perceptron sous une forme fonctionnelle afin de comparer performance relative. source ( https://rpubs.com/FaiHas/197581 ).

voici le code.

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

Je ne m'attendais pas à une amélioration constante en raison de ce qui précède question. Mais néanmoins, j'ai été vraiment surpris quand j'ai vu la forte détérioration en utilisant lapply et replicate .

j'ai obtenu ce résultat en utilisant la fonction microbenchmark de microbenchmark bibliothèque

Quelles pourraient être les raisons? Pourrait-il être certains de fuite de mémoire?

                                                      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 première fonction est la lapply / replicate fonction

la seconde est la fonction avec for boucles

la troisième est la même fonction dans C++ en utilisant Rcpp

ici selon Roland le profilage de la fonction. Je ne suis pas sûr que je peux l'interpréter dans le droit chemin. Il ressemble pour moi la plupart du temps est passé en. profilage de fonction

12
demandé sur d.b 2017-02-22 17:02:23

2 réponses

tout d'Abord, c'est une déjà longue démystifié le mythe que for boucles sont plus lentement que lapply . Les boucles for en R ont été rendues beaucoup plus performantes et sont actuellement au moins aussi rapides que lapply .

cela dit, vous devez repenser votre utilisation de lapply ici. Votre mise en œuvre exige l'affectation à l'environnement global, parce que votre code exige que vous mettiez à jour le poids pendant la boucle. Et c'est une raison valable ne pas considérer lapply .

lapply est une fonction que vous devez utiliser pour ses effets secondaires (ou l'absence d'effets secondaires). La fonction lapply combine les résultats dans une liste automatiquement et ne perturbe pas l'environnement dans lequel vous travaillez, contrairement à une boucle for . Il en va de même pour replicate . Voir aussi cette question:

est-ce que les R's appliquent la famille plus que le sucre syntaxique?

la raison pour laquelle votre solution lapply est beaucoup plus lente, est que votre façon de l'utiliser crée beaucoup plus de frais généraux.

  • replicate n'est rien d'autre que sapply en interne, donc vous combinez en fait sapply et lapply pour implémenter votre double boucle. sapply crée des frais généraux supplémentaires parce qu'il doit tester si oui ou non le résultat peut être simplifié. Donc une boucle for sera en fait plus rapide qu'en utilisant replicate .
  • dans votre fonction lapply anonyme, vous devez accéder à la base de données pour x et y pour chaque observation. Cela signifie que -contrairement à dans votre boucle for - par exemple la fonction $ doit être appelée à chaque fois.
  • parce que vous utilisez ces fonctions haut de gamme, votre solution" lapply "appelle 49 fonctions, par rapport à votre solution for qui n'appelle que 26. Ces fonctions supplémentaires pour la solution lapply inclure des appels à des fonctions comme match , structure , [[ , names , %in% , sys.call , duplicated , ... Toutes les fonctions qui ne sont pas nécessaires à votre boucle for car celle-ci ne fait aucune de ces vérifications.

si vous voulez voir d'où vient ce surplus, regardez le code interne de replicate , unlist , sapply et simplify2array .

vous pouvez utiliser ce qui suit code pour avoir une meilleure idée de l'endroit où vous perdez votre performance avec le lapply . Exécutez cette ligne par ligne!

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),]
28
répondu Joris Meys 2017-05-23 12:10:27

en fait,

j'ai testé la différence avec un problème que A résoudre récemment.

essayez vous-même.

dans ma conclusion, n'ont aucune différence, mais pour boucle à mon cas ont été insignificamment plus rapide que lapply.

Ps: j'essaie surtout de garder la même logique en usage.

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

comme vous pouvez le voir, j'ai fait une routine simple pour construire un named_list basé sur une base de données, la fonction func fait les valeurs de colonne extraites, la f1 utilise une boucle for pour itérer à travers la dataframe et la f2 utilise une fonction lapply.

dans mon ordinateur je reçois ce résultat:

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
0
répondu Aureliano Guedes 2018-06-19 19:38:24