Rcpp ile R objektif fonksiyonunu optimize etmek daha yavaş, neden?


16

Şu anda, yineleme başına çok terimli bir logit modelinin optimizasyonunun birden fazla adımını gerektiren bir Bayes yöntemi üzerinde çalışıyorum. Bu optimizasyonları yapmak için optim () kullanıyorum ve R'de yazılan objektif bir fonksiyon Profilleme, optim () 'in ana darboğaz olduğunu ortaya çıkardı.

Etrafa baktıktan sonra , nesnel fonksiyonun yeniden kodlanmasının süreci hızlandırabileceğini önerdikleri bu soruyu buldum Rcpp. Öneriyi takip ettim ve objektif fonksiyonumu yeniden kodladım Rcpp, ama yavaşladı (yaklaşık iki kat daha yavaş!).

Bu benim Rcpp(veya C ++ ile ilgili bir şey) ile ilk kez oldu ve ben kod vektörleştirme bir yol bulamadı. Nasıl daha hızlı yapacağınız hakkında bir fikriniz var mı?

Tl; dr: Rcpp'deki fonksiyonun mevcut uygulaması vektörize R kadar hızlı değildir; nasıl daha hızlı yapılır?

Tekrarlanabilir bir örnek :

1) Objektif fonksiyonların tanımlanması Rve Rcpp: sadece çok-modelli bir kesişim modelinin log-olasılık

library(Rcpp)
library(microbenchmark)

llmnl_int <- function(beta, Obs, n_cat) {
  n_Obs     <- length(Obs)
  Xint      <- matrix(c(0, beta), byrow = T, ncol = n_cat, nrow = n_Obs)
  ind       <- cbind(c(1:n_Obs), Obs)
  Xby       <- Xint[ind]
  Xint      <- exp(Xint)
  iota      <- c(rep(1, (n_cat)))
  denom     <- log(Xint %*% iota)
  return(sum(Xby - denom))
}

cppFunction('double llmnl_int_C(NumericVector beta, NumericVector Obs, int n_cat) {

    int n_Obs = Obs.size();

    NumericVector betas = (beta.size()+1);
    for (int i = 1; i < n_cat; i++) {
        betas[i] = beta[i-1];
    };

    NumericVector Xby = (n_Obs);
    NumericMatrix Xint(n_Obs, n_cat);
    NumericVector denom = (n_Obs);
    for (int i = 0; i < Xby.size(); i++) {
        Xint(i,_) = betas;
        Xby[i] = Xint(i,Obs[i]-1.0);
        Xint(i,_) = exp(Xint(i,_));
        denom[i] = log(sum(Xint(i,_)));
    };

    return sum(Xby - denom);
}')

2) Verimliliklerini karşılaştırın:

## Draw sample from a multinomial distribution
set.seed(2020)
mnl_sample <- t(rmultinom(n = 1000,size = 1,prob = c(0.3, 0.4, 0.2, 0.1)))
mnl_sample <- apply(mnl_sample,1,function(r) which(r == 1))

## Benchmarking
microbenchmark("llmml_int" = llmnl_int(beta = c(4,2,1), Obs = mnl_sample, n_cat = 4),
               "llmml_int_C" = llmnl_int_C(beta = c(4,2,1), Obs = mnl_sample, n_cat = 4),
               times = 100)
## Results
# Unit: microseconds
#         expr     min       lq     mean   median       uq     max neval
#    llmnl_int  76.809  78.6615  81.9677  79.7485  82.8495 124.295   100
#  llmnl_int_C 155.405 157.7790 161.7677 159.2200 161.5805 201.655   100

3) Şimdi onları şu numaraya çağırıyorum optim:

## Benchmarking with optim
microbenchmark("llmnl_int" = optim(c(4,2,1), llmnl_int, Obs = mnl_sample, n_cat = 4, method = "BFGS", hessian = T, control = list(fnscale = -1)),
               "llmnl_int_C" = optim(c(4,2,1), llmnl_int_C, Obs = mnl_sample, n_cat = 4, method = "BFGS", hessian = T, control = list(fnscale = -1)),
               times = 100)
## Results
# Unit: milliseconds
#         expr      min       lq     mean   median       uq      max neval
#    llmnl_int 12.49163 13.26338 15.74517 14.12413 18.35461 26.58235   100
#  llmnl_int_C 25.57419 25.97413 28.05984 26.34231 30.44012 37.13442   100

R'deki vektörize uygulamanın daha hızlı olması beni biraz şaşırttı. Rcpp'de daha verimli bir sürüm uygulamak (örneğin RcppArmadillo ile?) Herhangi bir kazanç sağlayabilir mi? Bir C ++ optimiser kullanarak Rcpp her şeyi yeniden kodlamak daha iyi bir fikir mi?

Not: Stackoverflow'da ilk kez gönderme!

Yanıtlar:


9

Genel olarak vectorized fonksiyonları kullanabiliyorsanız, kodunuzu doğrudan Rcpp'de çalıştırmak kadar (neredeyse) kadar hızlı bulacaksınız. Bunun nedeni, R'deki birçok vektörize fonksiyonun (Base R'deki hemen hemen tüm vektörize edilmiş fonksiyonlar) C, Cpp veya Fortran'da yazılması ve bu nedenle kazanılacak çok az şeyin olmasıdır.

Bu senin hem kazanmak için iyileştirmeler vardır, dedi Rve Rcppkodu. Optimizasyon, kodu dikkatle incelemek ve gereksiz adımları (bellek ataması, toplamlar vb.) Kaldırmaktan gelir.

RcppKod optimizasyonu ile başlayalım .

Sizin durumunuzda ana optimizasyon, gereksiz matris ve vektör hesaplamalarını kaldırmaktır. Kod özünde

  1. Shift beta
  2. exp (shift beta) toplamının günlüğünü hesapla [log-sum-exp]
  3. kaydırılmış beta için bir dizin olarak Obs kullanın ve tüm olasılıkları toplayın
  4. log-sum-exp özetini

Bu gözlemi kullanarak kodunuzu 2 döngüye indirebiliriz. Not sumbasitçe başka For döngüsü (az ya da çok: for(i = 0; i < max; i++){ sum += x }) toplamları kaçınarak daha da olanları kodunu hızlandırabilir böylece (çoğu durumda bu gereksiz optimizasyon!). Ayrıca girdiniz Obsbir tamsayı vektörüdür ve elemanları değerlere dökmekten IntegerVectorkaçınmak için türü kullanarak kodu daha da optimize edebiliriz (Ralf Stubner'ın cevabına kredi).doubleinteger

cppFunction('double llmnl_int_C_v2(NumericVector beta, IntegerVector Obs, int n_cat)
 {

    int n_Obs = Obs.size();

    NumericVector betas = (beta.size()+1);
    //1: shift beta
    for (int i = 1; i < n_cat; i++) {
        betas[i] = beta[i-1];
    };
    //2: Calculate log sum only once:
    double expBetas_log_sum = log(sum(exp(betas)));
    // pre allocate sum
    double ll_sum = 0;

    //3: Use n_Obs, to avoid calling Xby.size() every time 
    for (int i = 0; i < n_Obs; i++) {
        ll_sum += betas(Obs[i] - 1.0) ;
    };
    //4: Use that we know denom is the same for all I:
    ll_sum = ll_sum - expBetas_log_sum * n_Obs;
    return ll_sum;
}')

Oldukça az bellek ayırma ve for-döngüsü gereksiz hesaplamaları kaldırdık unutmayın. Ayrıca denomtüm iterasyonlar için aynı ve sadece nihai sonuç için çarpılmış kullandım.

R-kodunuzda, aşağıdaki işlevle sonuçlanan benzer optimizasyonlar gerçekleştirebiliriz:

llmnl_int_R_v2 <- function(beta, Obs, n_cat) {
    n_Obs <- length(Obs)
    betas <- c(0, beta)
    #note: denom = log(sum(exp(betas)))
    sum(betas[Obs]) - log(sum(exp(betas))) * n_Obs
}

İşlevin karmaşıklığının büyük ölçüde azaldığını ve başkalarının okumasını kolaylaştırdığını unutmayın. Sadece bir yerde kodu berbat değil emin olmak için onlar aynı sonuçları döndürmek kontrol edelim:

set.seed(2020)
mnl_sample <- t(rmultinom(n = 1000,size = 1,prob = c(0.3, 0.4, 0.2, 0.1)))
mnl_sample <- apply(mnl_sample,1,function(r) which(r == 1))

beta = c(4,2,1)
Obs = mnl_sample 
n_cat = 4
xr <- llmnl_int(beta = beta, Obs = mnl_sample, n_cat = n_cat)
xr2 <- llmnl_int_R_v2(beta = beta, Obs = mnl_sample, n_cat = n_cat)
xc <- llmnl_int_C(beta = beta, Obs = mnl_sample, n_cat = n_cat)
xc2 <- llmnl_int_C_v2(beta = beta, Obs = mnl_sample, n_cat = n_cat)
all.equal(c(xr, xr2), c(xc, xc2))
TRUE

iyi bir rahatlama.

Verim:

Performansı göstermek için mikrobenmark kullanacağım. Optimize edilmiş işlevler hızlıdır, bu nedenle 1e5çöp toplayıcısının etkisini azaltmak için işlev sürelerini çalıştıracağım

microbenchmark("llmml_int_R" = llmnl_int(beta = beta, Obs = mnl_sample, n_cat = n_cat),
               "llmml_int_C" = llmnl_int_C(beta = beta, Obs = mnl_sample, n_cat = n_cat),
               "llmnl_int_R_v2" = llmnl_int_R_v2(beta = beta, Obs = mnl_sample, n_cat = n_cat),
               "llmml_int_C_v2" = llmnl_int_C_v2(beta = beta, Obs = mnl_sample, n_cat = n_cat),
               times = 1e5)
#Output:
#Unit: microseconds
#           expr     min      lq       mean  median      uq        max neval
#    llmml_int_R 202.701 206.801 288.219673 227.601 334.301  57368.902 1e+05
#    llmml_int_C 250.101 252.802 342.190342 272.001 399.251 112459.601 1e+05
# llmnl_int_R_v2   4.800   5.601   8.930027   6.401   9.702   5232.001 1e+05
# llmml_int_C_v2   5.100   5.801   8.834646   6.700  10.101   7154.901 1e+05

Burada önceki ile aynı sonucu görüyoruz. Şimdi yeni fonksiyonlar ilk karşı parçalarına kıyasla yaklaşık 35 kat daha hızlı (R) ve 40 kat daha hızlı (Cpp). İlginçtir ki, optimize edilmiş Rfonksiyon benim optimize edilmiş fonksiyonumdan hala çok az (0,3 ms veya% 4) daha hızlıdır Cpp. Buradaki en iyi bahisim, Rcpppaketten biraz ek yük olması ve bu kaldırılırsa ikisi aynı ya da R olurdu.

Benzer şekilde Optim'i kullanarak performansı kontrol edebiliriz.

microbenchmark("llmnl_int" = optim(beta, llmnl_int, Obs = mnl_sample, 
                                   n_cat = n_cat, method = "BFGS", hessian = F, 
                                   control = list(fnscale = -1)),
               "llmnl_int_C" = optim(beta, llmnl_int_C, Obs = mnl_sample, 
                                     n_cat = n_cat, method = "BFGS", hessian = F, 
                                     control = list(fnscale = -1)),
               "llmnl_int_R_v2" = optim(beta, llmnl_int_R_v2, Obs = mnl_sample, 
                                     n_cat = n_cat, method = "BFGS", hessian = F, 
                                     control = list(fnscale = -1)),
               "llmnl_int_C_v2" = optim(beta, llmnl_int_C_v2, Obs = mnl_sample, 
                                     n_cat = n_cat, method = "BFGS", hessian = F, 
                                     control = list(fnscale = -1)),
               times = 1e3)
#Output:
#Unit: microseconds
#           expr       min        lq      mean    median         uq      max neval
#      llmnl_int 29541.301 53156.801 70304.446 76753.851  83528.101 196415.5  1000
#    llmnl_int_C 36879.501 59981.901 83134.218 92419.551 100208.451 190099.1  1000
# llmnl_int_R_v2   667.802  1253.452  1962.875  1585.101   1984.151  22718.3  1000
# llmnl_int_C_v2   704.401  1248.200  1983.247  1671.151   2033.401  11540.3  1000

Bir kez daha sonuç aynı.

Sonuç:

Kısa bir sonuç olarak, kodunuzu Rcpp'e dönüştürmenin gerçekten sorun yapmaya değmediği bir örnek olduğunu belirtmek gerekir. Bu her zaman böyle değildir, ancak kodunuzun gereksiz hesaplamaların yapıldığı alanları olup olmadığını görmek için genellikle işlevinize ikinci bir bakış atmaya değer. Özellikle yapının vektörize fonksiyonlar kullandığı durumlarda, kodu Rcpp'e dönüştürmek için zaman ayırmaya değmez. for-loopsFor-loop'u kaldırmak için kolayca vektörleştirilemeyen kodla kullanılırsa , çoğu zaman büyük gelişmeler görülebilir.


1
Sen tedavi edebilir Obsbir şekilde IntegerVectorbazı yayınları kaldırarak.
Ralf Stubner

Sadece cevabınızda bunu fark ettiğiniz için teşekkür etmeden önce ekliyordu. Sadece benim tarafımdan geçti. @RalfStubner cevabımda size bunun için kredi verdim. :-)
Oliver

2
Bu oyuncak örneğinde (sadece kesişen mnl modeli) fark ettiğiniz gibi, doğrusal öngörücüler ( beta) gözlemler üzerinde sabit kalır Obs. Zamanla değişen öngörücülerimiz olsaydı, bir tasarım matrisinin değerine bağlı olarak denomher biri için örtük bir hesaplama Obsgerekli olurdu X. Olduğu söyleniyor, zaten kodumu geri kalanı üzerinde bazı güzel kazançlar ile önerilerinizi uyguluyorum :). @RalfStubner, @Oliver ve @thc'ye çok anlayışlı yanıtlarınız için teşekkür ederiz! Şimdi bir sonraki darboğuma geçiyorum!
smildiner

1
Yardımcı olabileceğimiz için mutluyum. Daha genel bir durumda, saniyenin her adımında for-loopsize en büyük kazancı verecek olan çıkarma payını hesaplamak . Ayrıca daha genel durumda model.matrix(...), fonksiyonlarınıza giriş için matrisinizi oluşturmak için kullanmanızı öneririm .
Oliver

9

C ++ işleviniz aşağıdaki gözlemler kullanılarak daha hızlı yapılabilir. En azından birincisi R işlevinizle de kullanılabilir:

  • Hesaplama şekliniz denom[i]herkes için aynıdır i. Bu nedenle a'yı kullanmak double denomve bu hesaplamayı yalnızca bir kez yapmak mantıklıdır . Sonunda bu ortak terimi çıkarmayı da etkisiz hale getiriyorum.

  • Gözlemleriniz aslında R tarafında bir tamsayı vektörü ve C ++ 'da da tamsayı olarak kullanıyorsunuz. Bir IntegerVectorile başlamak, dökümün çoğunu gereksiz kılar.

  • C ++ 'da bir NumericVectorkullanarak bir dizin oluşturabilirsiniz IntegerVector. Bu performans yardımcı olur emin değilim, ama kodu biraz daha kısa yapar.

  • Performanstan ziyade stil ile ilgili bazı değişiklikler.

Sonuç:

double llmnl_int_C(NumericVector beta, IntegerVector Obs, int n_cat) {

    int n_Obs = Obs.size();

    NumericVector betas(beta.size()+1);
    for (int i = 1; i < n_cat; ++i) {
        betas[i] = beta[i-1];
    };

    double denom = log(sum(exp(betas)));
    NumericVector Xby = betas[Obs - 1];

    return sum(Xby) - n_Obs * denom;
}

Benim için bu işlev, R işlevinizden yaklaşık on kat daha hızlıdır.


Cevabınız için teşekkürler Ralph, giriş türünü tespit etmedi. Bunu cevabıma dahil ettim ve size kredi veriyorum. :-)
Oliver

7

Ralf ve Olivers cevapları üzerinde dört potansiyel optimizasyon düşünebilirim .

(Cevaplarını kabul etmelisin, ama sadece 2 sentimi eklemek istedim).

1) // [[Rcpp::export(rng = false)]]Ayrı bir C ++ dosyasındaki işleve yorum üstbilgisi olarak kullanın . Bu, makinemde ~% 80 hızlanmaya neden olur. (Bu 4 içinden en önemli öneri).

2) cmathMümkün olduğunda tercih edin . (Bu durumda, bir fark yaratmıyor gibi görünüyor).

3) Mümkün olduğunca tahsis betaetmekten kaçının, örneğin yeni bir vektöre geçmeyin .

4) Germe hedefi: SEXPRcpp vektörleri yerine parametreleri kullanın . (Okuyucuya bir egzersiz olarak bırakılmıştır). Rcpp vektörleri çok ince sarmalayıcılardır, ancak yine de sarmalayıcılardır ve küçük bir ek yük vardır.

Bu öneriler, işlevi sıkı bir döngü içinde çağırdığınız için olmasa bile önemli olmayacaktır optim. Bu yüzden herhangi bir ek yük çok önemlidir.

Bank:

microbenchmark("llmnl_int_R_v1" = optim(beta, llmnl_int, Obs = mnl_sample, 
                                      n_cat = n_cat, method = "BFGS", hessian = F, 
                                      control = list(fnscale = -1)),
             "llmnl_int_R_v2" = optim(beta, llmnl_int_R_v2, Obs = mnl_sample, 
                                      n_cat = n_cat, method = "BFGS", hessian = F, 
                                      control = list(fnscale = -1)),
             "llmnl_int_C_v2" = optim(beta, llmnl_int_C_v2, Obs = mnl_sample, 
                                      n_cat = n_cat, method = "BFGS", hessian = F, 
                                      control = list(fnscale = -1)),
             "llmnl_int_C_v3" = optim(beta, llmnl_int_C_v3, Obs = mnl_sample, 
                                      n_cat = n_cat, method = "BFGS", hessian = F, 
                                      control = list(fnscale = -1)),
             "llmnl_int_C_v4" = optim(beta, llmnl_int_C_v4, Obs = mnl_sample, 
                                      n_cat = n_cat, method = "BFGS", hessian = F, 
                                      control = list(fnscale = -1)),
             times = 1000)


Unit: microseconds
expr      min         lq       mean     median         uq        max neval cld
llmnl_int_R_v1 9480.780 10662.3530 14126.6399 11359.8460 18505.6280 146823.430  1000   c
llmnl_int_R_v2  697.276   735.7735  1015.8217   768.5735   810.6235  11095.924  1000  b 
llmnl_int_C_v2  997.828  1021.4720  1106.0968  1031.7905  1078.2835  11222.803  1000  b 
llmnl_int_C_v3  284.519   295.7825   328.5890   304.0325   328.2015   9647.417  1000 a  
llmnl_int_C_v4  245.650   256.9760   283.9071   266.3985   299.2090   1156.448  1000 a 

v3 Oliver'ın cevabı rng=false. v4, Öneriler # 2 ve # 3 dahil edilmiştir.

İşlev:

#include <Rcpp.h>
#include <cmath>
using namespace Rcpp;

// [[Rcpp::export(rng = false)]]
double llmnl_int_C_v4(NumericVector beta, IntegerVector Obs, int n_cat) {

  int n_Obs = Obs.size();
  //2: Calculate log sum only once:
  // double expBetas_log_sum = log(sum(exp(betas)));
  double expBetas_log_sum = 1.0; // std::exp(0)
  for (int i = 1; i < n_cat; i++) {
    expBetas_log_sum += std::exp(beta[i-1]);
  };
  expBetas_log_sum = std::log(expBetas_log_sum);

  double ll_sum = 0;
  //3: Use n_Obs, to avoid calling Xby.size() every time 
  for (int i = 0; i < n_Obs; i++) {
    if(Obs[i] == 1L) continue;
    ll_sum += beta[Obs[i]-2L];
  };
  //4: Use that we know denom is the same for all I:
  ll_sum = ll_sum - expBetas_log_sum * n_Obs;
  return ll_sum;
}
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.