matrices α and β have dimensions of nV x S and nU x S, respectively; the S parameters in each row are drawn from Normal distribution, giving rise to a different parameter for each species. The parameter array is of length T (number of years), and for each t, it has a length (S) with definition
.
# =============
# = Fit Model =
# =============
ebs_msom <- stan(
model_code=model_file,
data=staticData,
control=list(stepsize=0.05, adapt_delta=0.95),
chains=4, iter=500, refresh=1, seed=1337, cores=4
)The model looks OK, and if you've validated it with fake data, you should
be good to go in terms of what the model does. So what follows is just about
the comptutation.
Do you need to save all those transformed parameters? If
not, you can define them as local variables in the model block.
The bigger speedup gains would come from vectorizing more operations
and/or aggregating observation data.
I think it'd make sense to put Omega on the logit scale and give
it a hierarchical prior (you could do that on the (0,1) scale
with a hierarchical beta).
\\ in parameter block
Omega_mu ~ beta(2,2);
Omega_sd ~ cauchy(0, 1);
for (t in 1:nT) {
Omega_logit[t] ~ normal(logit(Omega_mu), Omega_sd);
}
By transposing alpha and making it an array of vectors, you will avoid
local copies (see the chapter in the manual on arrays vs. vectors)
and you should be able to get away with replacing
for(u in 1:nU){
alpha[u,s] ~ normal(alpha_mu[u], alpha_sd[u]);
}
with
alpha[s] ~ normal(alpha_mu, alpha_sd);
and same for the beta[v,s] loop.
for (t in 1:nT) {
for (j in 1:Jmax) {
int K;
K <- nK[t,j];
if (K>0) { // if K is not 0
// if the K matrix gets too large,
// it is in here that I should add
// a loop, wherein I subset to U[t,j,k], and
// vectorize over spp only
for (k in 1:K) {
logit_psi[t,j,k] <- U[t,j,k]*alpha;
logit_theta[t,j,k] <- V[t,j,k]*beta;
}
}
}
}
of U and V (currently declared as matrix[Kmax,nU] U[nT,Jmax], switch to matrix[nU,Kmax] U[nT,Jmax]), then switch toalpha * U[t,j. I'd have to check to make sure that'd work out.
Regardless, I remember thinking that I couldn't keep the current dimensions and vectorize across the column (in R, e.g., do a alpha[,1] <- statement), so I understand why the transposition is needed for this vectorization. I'll try it.
This is redundan
t:
if(tK>0){ // if >0 sampling events, proceed
for(k in 1:tK){// loop through sampling events
if tK <= 0, then the loop will have zero iterations and you save
a level of indentation, some clarity, and a conditional evaluation.
Oh, OK — so if I had for (k in 1:0) {, nothing would happen. That sounds familiar from something I read in the manual, because Stan doesn’t loop backwards. Gotcha. I hadn’t thought of the implications of the rule in this context, thank you.
This
1 ~ bernoulli(Omega[t])
is nested a couple levels deep. You could pull it out and then multiply
by the number of times its evaluated.
For multiplying out the probability increments, can I still use the ~ notation? Or would I just do something like 1 ~ bernoulli(Omega[t]) * N * sum(nK[t])? Note that nK[t] is a row_vector, and if that notation works, I could move this out of 3 for loops. Good call on moving this; however it’s done, it’ll help.
There's no point in defining a local here:
int K;
K <- nK[t,j];
if(K>0){ // if K is not 0
for(k in 1:K){
This should all be replaced with
for (k in 1:nK[t, j]) { ...
If it makes sense, tighter priors than cauchy(0, 2.5) can help,
either in tails (Say student-t with 4 dof or even normal), or
in reducing the scale from 2.5. I don't know where things liveh
ehre, but these look like logistic regression parameters.
Along the same lines as earlier examples, this loop can be
vectorized:
for(n in 1:N){// loop through observed species
1 ~ bernoulli(Omega[t]); // observed, so available
if(X[t,j,k,n] > 0){ // if present
// increment likelihood
increment_log_prob(
lp_obs(X[t,j,k,n], logit_psi[t,j,k,n], logit_theta[t,j,k,n])
The goal is to reduce the number of calls to the density functions
with vectorization, which in turn reduces the size of the expression graph for
autodiff.
I think I’m running into at least 2 potential problems vectorizing these pieces. First, the lp_obs function can only be applied to the first N elements of that fourth dimenion; i.e., to X[t,j,k, 1:N] … which is R syntax, and which I am unsure of how to do in Stan. On page 353 of the manual I see the vector and array slicing operations, but I didn’t understand them right away. Taking a second look, I see that maybe segment(X[t,j,k], 1, N) would give the desired result for vectorization. I’ve read elsewhere that in many cases the copy command induced by segment is a small loss in performance compared to the performance gained by vectorizing. So I see how this could be handy.
Second, this entire process could be greatly simplified if I could condense the lp_obs and lp_unobs functions into 1. These are your functions, which I simplified to my needs. After simplification, I didn’t see how to easily condense them into 1 function. It’s because of that log1m_inv_logit(logit_psi) term in the lp_unobs function —- I have to cancel it out if x is 1, and I can’t think of a way to do that, unless something like value * (x==0) can take advantage of TRUE being 1, and FALSE being 0, and then working for algebra (like in R). Any ideas?
- Bob
P.S. Some minor formatting.
I would get rid of these comments given that the
very next line of code says the same thing.
// ==========================
// = Transformed Parameters =
// ==========================
or things that are totally obvious from looking at the code like
// ============================================
// = Begin Looping down to Point Observations =
// ============================================
For a start, I wouldn't call a string "model_file". But I would
recommend actually pulling the model out and referring to it by path.
That lets you use line-number based diagnostics.
I really don't like 4 indents (see how out of hand the cut-and-pastes
got above) or the quirky R style of rendering loops as
"for(t in 1:nT){" because I find it hard to read, but consistency's
even more important. My main goal is to leave the code uncluttered,
and I find comments like this hurt more than they help:
}else{ // if absent
// increment likelihood
increment_log_prob(
lp_unobs(logit_psi[t,j,k,n], logit_theta[t,j,k,n])
);
You should assume people reading the program know the programming
language, so they know what "else" does and what "increment_log_prob"
does. The comments add clutter and make it hard to read the code
itself.
...
...
Update:
Doing the non-centered parameterization was a massive speed-up (~20-30 fold).
I noticed that I made an error in my most recent version of the model, after aggregating the presence process to the site level (still incrementing log_inv_logit(logit_psi) for each sample, instead of just 1 time per site).
For a similar reason, I think I also needed to change increment_log_prob(bernoulli_log(1, Omega[t]) * N * sum(nK[t])); to increment_log_prob(bernoulli_log(1, Omega[t]) * N * sum(nKJt]));; i.e., do 1~Omega[t] J*T times, not also incrementing for each sample K in each site J.
I’m currently working on correcting the above mistakes. I’m also going to aggregate the presence covariates, but then model them as ~normal(mean(covariate), sd(covariate)) so that I don’t lose all of the variability when I aggregate.
real lp_observed(int x, int K, real logit_psi, real logit_theta) {
return log_inv_logit(logit_psi) + binomial_logit_log(x, K, logit_theta);
}
real lp_unobserved(int K, real logit_psi, real logit_theta) {
return log_sum_exp(lp_observed(0, K, logit_psi, logit_theta), log1m_inv_logit(logit_psi));
}
real lp_never_observed(int J, int K, real logit_psi, real logit_theta, real Omega) {
real lp_unavailable;
real lp_available;
lp_unavailable <- bernoulli_log(0, Omega);
lp_available <- bernoulli_log(1, Omega) + J * lp_unobserved(K, logit_psi, logit_theta);
return log_sum_exp(lp_unavailable, lp_available);
}
lp_available <- bernoulli_log(1, Omega) + J * lp_unobserved(K, logit_psi, logit_theta); must be separated from the second, and the second has to be first be summed among all T,J for a species before being added to the first term. That's why I use the lp_available_pt1 vector in my current iterative form of the Never Observed species.Thanks Bob. The current model produces “reasonable” results when fed larger data sets (5 species isn’t very many!). I’ll work on adapting old methods for verifying these models.
If I have a statement like bernoulli_log(1, Omega)*D, is it faster/ correct to switch it to binomial_log(D, D, Omega) ? Similar question for increment_log_prob(bernoulli_log(1, Omega) * N).
I’ll report back once I’ve verified my current Stan model. Thank you so much.
One thing I've noticed during my testing is that I don't have to run a Stan model for very many iterations to get close to reasonable results. Honestly, with simulated data and this particular model, I'm finding that ~50 iterations gets be in the ballpark. In fact, I can usually figure out if the model is 'working' if I do ~20. That's orders of magnitude fewer iterations than I would do in JAGS for similar feel of accuracy.
Is there any standard expectation for the number of iterations I should run the Stan model? If I put in a paper that I ran my model for 200 iterations, a reviewer used to BUGS or JAGS would probably question the choice (if I didn't explain effective sample size, that I'm using Stan, etc). What should I consider to be in the ballpark of 'normal' for number of iterations? this point is germane to the performance of the occupancy model, because "performance" is really about "how long it takes for the model to finish", which obviously depends on # iterations. As Bob briefly alluded to, and is implied by extensive discussions here and elsewhere about "effective sample size", perhaps the easiest way to improve my definition of "performance" is to run the model for fewer iterations.
p.s. When I do 20-50 iterations, there's usually a couple parameters here and there that weren't even close to converging; these are typically some of the Psi parameters (or Theta), and there's one of those for each site-year-species (a lot). Just thought I'd point out that there are obvious problems with so few iterations. Nonetheless, most parameters are pretty close to true values by that time.
As far as I know, I used non-centered methods everywhere. I can repost the model with a message or as a .stan or .txt (for easier browser viewing) for convenience.