Atualmente, estou trabalhando em um método bayesiano que requer várias etapas de otimização de um modelo de logit multinomial por iteração. Estou usando o optim () para realizar essas otimizações e uma função objetiva escrita em R. Uma criação de perfil revelou que o optim () é o principal gargalo.
Depois de pesquisar, encontrei esta pergunta na qual eles sugerem que a recodificação da função objetivo Rcpp
poderia acelerar o processo. Eu segui a sugestão e recodifiquei minha função objetiva Rcpp
, mas acabou sendo mais lenta (cerca de duas vezes mais lenta!).
Esta foi a minha primeira vez com Rcpp
(ou qualquer coisa relacionada ao C ++) e não consegui encontrar uma maneira de vetorizar o código. Alguma idéia de como torná-lo mais rápido?
Tl; dr: A implementação atual da função no Rcpp não é tão rápida quanto o R vetorizado; como torná-lo mais rápido?
Um exemplo reproduzível :
1) Definir funções objetivas R
e Rcpp
: probabilidade logarítmica de um modelo multinomial de interceptação apenas
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) Compare sua eficiência:
## 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) Agora, chamando-os 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
Fiquei um pouco surpreso que a implementação vetorizada em R fosse mais rápida. Implementar uma versão mais eficiente no Rcpp (digamos, com o RcppArmadillo?) Pode gerar ganhos? É uma idéia melhor recodificar tudo no Rcpp usando um otimizador de C ++?
PS: primeira postagem no Stackoverflow!
fonte
Obs
comoIntegerVector
remover alguns elencos.beta
) permanecem constantes sobre as observaçõesObs
. Se tivéssemos tempo variando de preditores, um cálculo implícito dedenom
cada umObs
seria necessário, com base no valor de uma matriz de designX
. Dito isto, já estou implementando suas sugestões no restante do meu código com alguns ganhos muito bons :). Obrigado @ RalfStubner, @ Oliver e @ thc por suas respostas muito perspicazes! Agora vamos para o meu próximo gargalo!for-loop
que lhe dará o maior ganho. Também no caso mais geral, eu sugiro usarmodel.matrix(...)
para criar sua matriz para entrada em suas funções.Sua função C ++ pode ser mais rápida usando as seguintes observações. Pelo menos o primeiro também pode ser usado com sua função R:
A maneira como você calcula
denom[i]
é a mesma para todosi
. Portanto, faz sentido usar adouble denom
e fazer esse cálculo apenas uma vez. Eu também fatorei a subtração desse termo comum no final.Suas observações são na verdade um vetor inteiro no lado R e você as está usando também como números inteiros em C ++. Usar um
IntegerVector
para começar faz com que muitas transmissões sejam desnecessárias.Você pode indexar um
NumericVector
usando umIntegerVector
em C ++. Não tenho certeza se isso ajuda no desempenho, mas torna o código um pouco mais curto.Mais algumas mudanças relacionadas ao estilo do que ao desempenho.
Resultado:
Para mim, essa função é aproximadamente dez vezes mais rápida que a sua função R.
fonte
Eu posso pensar em quatro potenciais otimizações sobre as respostas de Ralf e Olivers.
(Você deve aceitar as respostas deles, mas eu só queria adicionar meus 2 centavos).
1) Use
// [[Rcpp::export(rng = false)]]
como cabeçalho de comentário para a função em um arquivo C ++ separado. Isso leva a uma velocidade de ~ 80% na minha máquina. (Esta é a sugestão mais importante das 4).2) Preferir
cmath
quando possível. (Nesse caso, não parece fazer a diferença).3) Evite alocação sempre que possível, por exemplo, não mude
beta
para um novo vetor.4) meta de alongamento: use
SEXP
parâmetros em vez de vetores Rcpp. (Deixado como um exercício para o leitor). Os vetores Rcpp são invólucros muito finos, mas ainda são invólucros e há uma pequena sobrecarga.Essas sugestões não seriam importantes, se não fosse pelo fato de você estar chamando a função em um loop restrito
optim
. Portanto, qualquer sobrecarga é muito importante.Banco:
v3 é a resposta de Oliver com
rng=false
. v4 está com as sugestões 2 e 3 incluídas.A função:
fonte