Stan coding difficulty in lognormal race model

Hi, I’m not sure what’s wrong with this stan model, and would appreciate some pointers. I’m trying to modify the activation-based lognormal race model implemented by Nicenboim et al. (2017) (@bnicenboim) , and detailed here. I can tell there’s something off with the types / dimensions, but I haven’t been able to figure out exactly where to change things to make them match up.

I’d like to add a predictor to the item-specific evidence accumulation parameter, alpha, and so I followed (as best I could) the way alpha was modified to add by-subject and by-item offsets in the hierarchical version detailed in the link above. However, the error I’m getting is:

SYNTAX ERROR, MESSAGE(S) FROM PARSER:
Dimension mismatch in assignment; variable name = alpha, type = vector; right-hand side type = real.
Illegal statement beginning with non-void expression parsed as
  alpha
Not a legal assignment, sampling, or function statement.  Note that
  * Assignment statements only allow variables (with optional indexes) on the left;
  * Sampling statements allow arbitrary value-denoting expressions on the left.
  * Functions used as statements must be declared to have void returns

 error in 'model6d5c72471037_EnrichedBasicRace' at line 44, column 4
  -------------------------------------------------
    42:   for (n in 1:N_obs) {
    43:     vector[N_choices] alpha; 
    44:     alpha = b_intercept + b_target_syll_heavy * TargetSyllHeavy[n];
           ^
    45:     log_lik[n] = race(winner[n], RT[n], alpha, b, sigma, psi);
  -------------------------------------------------

PARSER EXPECTED: "}"
Error in stanc(filename, allow_undefined = TRUE) : 
  failed to parse Stan model 'EnrichedBasicRace' due to the above error.
> 

The model code is here:

functions {
    real race(int winner, real RT, vector alpha, real b, real sigma, real psi){
    real log_lik;
    int N_choices;
    N_choices = num_elements(alpha);
    log_lik = 0;
    for(c in 1:N_choices)
        if(c == winner)
          log_lik = log_lik + lognormal_lpdf(RT - psi|b - alpha[c], sigma);
        else 
          log_lik = log_lik + lognormal_lccdf(RT - psi|b - alpha[c], sigma); 
    return(log_lik);
    }
}
data { 
  int<lower = 0> N_obs; 
  int<lower = 1> N_choices; 
  int<lower = 1, upper = N_choices> winner[N_obs];
  vector<lower = 0>[N_obs] RT;
  vector[N_obs] TargetSyllHeavy;
}
transformed data {
  real  b; //arbitrary threshold
  real min_RT;
  b = 10;
  min_RT = min(RT);
}
parameters{
  
  real<lower=0> sigma;
  real<lower=0,upper=min_RT> psi;
  real b_intercept;
  real b_target_syll_heavy;
}
model {
  vector[N_obs] loglik;
  //alpha ~ normal(0,10);
  b_intercept ~ normal(0,50);
  b_target_syll_heavy ~ normal(0,10);
  sigma ~ normal(0,2);
  psi ~ normal(0,300); 
  for (n in 1:N_obs) {
    vector[N_choices] alpha; 
    alpha = b_intercept + b_target_syll_heavy * TargetSyllHeavy[n];
    log_lik[n] = race(winner[n], RT[n], alpha, b, sigma, psi);
    }
  target += loglik;
}

The regression parameter I’m trying to add is b_target_syll_heavy; the relevant data can be found forums_out_data.csv (50.1 KB) .

Thanks very much for your assistance with this, I appreciate it a lot!

2 Likes

The model compiles if you change either b_intercept or b_target_syll_heavy to a vector.

parameters{
  real<lower=0> sigma;
  real<lower=0,upper=min_RT> psi;
  vector[N_choices] b_intercept;
  vector[N_choices] b_target_syll_heavy;
}

Does that make sense?

3 Likes

Yes it does - thank you very much!

Hi @nhuurre , thanks again for your kind help before. I’m running into another type mismatch issue in an extended version of this model and I was wondering if you would mind pointing out where I’m going wrong. What I want is to have b_mix_into_good and b_mix_into_bad be 0 or 1 on each of n trials, determined by the parameter theta_mix_into_good and theta_mix_into_bad respectively. However, no matter where I seem to put the relevant statements and declarations, I still get one error or another. Any help would be very appreciated - my gut says this is a trivial problem, but I can’t quite figure it out, because it seems like what you need is a vector of ints, but that’s not declarable I guess…

functions {
    real race(int winner, real RT, vector alpha, real b, real sigma, real psi){
    real log_lik;
    int N_choices;
    N_choices = num_elements(alpha);
    log_lik = 0;
    for(c in 1:N_choices)
        if(c == winner)
          log_lik = log_lik + lognormal_lpdf(RT - psi|b - alpha[c], sigma);
        else
          log_lik = log_lik + lognormal_lccdf(RT - psi|b - alpha[c], sigma);
    return(log_lik);
    }
    vector race_rng(vector alpha, real b, real sigma,
                   real psi) {
    int N_choices;
    real curr_accum_RT;
    real fastest_accum_RT;
    vector[2] gen;
    N_choices = num_elements(alpha);
    fastest_accum_RT = positive_infinity();
    for (c in 1:N_choices) {
      curr_accum_RT = psi + lognormal_rng(b - alpha[c], sigma);
      if (curr_accum_RT < fastest_accum_RT){ //this accumulator is faster
        fastest_accum_RT = curr_accum_RT;
        gen[1] = c;
        gen[2] = curr_accum_RT;
      }
    }
    return(gen);
    }
}
data {
  int<lower = 0> N_obs;
  int<lower = 1> N_choices;
  int<lower = 1, upper = N_choices> winner[N_obs];
  vector<lower = 0>[N_obs] RT;
  vector[N_obs] TargetSyllHeavy;
  vector[N_obs] TargetSyllSecondaryStress;
  vector[N_obs] LocalBaseFreq;
  vector[N_obs] RemoteBaseFreq;
  vector[N_obs] HasRemoteBase;
  vector[N_obs] Primed;
}
transformed data {

  real  b; //arbitrary threshold
  real min_RT;
  b = 10;
  min_RT = min(RT);

}


parameters{

  real<lower=0> sigma;
  real<lower=0,upper=min_RT> psi;
  real b_violate_weight_to_stress;
  real b_lapse;
  real b_faith;
  real b_violate_stress_secondary_stressed;
  real <lower=0>b_local_base_freq;
  real <lower=0>b_remote_base_freq;
  real b_intercept;

  real <lower=0, upper=1>theta_mix_into_good;
  real <lower=0, upper=1>theta_mix_into_bad;
  real<lower=0> b_primed;


}



model {
  vector[N_obs] loglik;
  vector[N_obs] b_mix_into_bad;
  vector[N_obs] b_mix_into_good;

  b_faith ~ normal(0,1);
  b_lapse ~ normal(0,1);
  b_violate_stress_secondary_stressed ~ normal(0,1);
  b_local_base_freq ~ normal(0,1)T[0,];
  b_remote_base_freq ~ normal(0,1)T[0,];
  theta_mix_into_bad ~ beta(1,1);
  theta_mix_into_good ~ beta(1,1);


  b_violate_weight_to_stress ~ normal(0,1);
  b_intercept ~ normal(0,1);
  b_primed ~ normal(0,1);
  sigma ~ normal(0,2);
  psi ~ normal(0,300);
  for (n in 1:N_obs) {
    vector[N_choices] alpha;
    //int b_mix_into_bad;
    //int b_mix_into_good;
    b_mix_into_bad[n] ~ bernoulli(theta_mix_into_bad);
    b_mix_into_good[n] ~ bernoulli(theta_mix_into_good);
    for (c in 1:N_choices){
      if (HasRemoteBase[n] == 1) // if remote base, use mixture model
        if (winner[n] == 1)// Yes BR, no shift
          alpha[c] = (b_mix_into_bad[n] *(b_intercept+ b_lapse + b_local_base_freq * LocalBaseFreq[n] + b_violate_weight_to_stress * TargetSyllHeavy[n] + b_violate_stress_secondary_stressed * TargetSyllSecondaryStress[n])+ // faithful to loca base
                      (1-b_mix_into_bad[n])*(b_intercept+b_lapse + b_faith + b_remote_base_freq * RemoteBaseFreq[n]+ b_violate_weight_to_stress * TargetSyllHeavy[n] + b_primed * Primed[n] + b_violate_stress_secondary_stressed * TargetSyllSecondaryStress[n])) ; // unfaithful to remote base
        else // Yes BR, yes shift
          alpha[c] =  (b_mix_into_good[n] * ( b_intercept+b_faith + b_local_base_freq * LocalBaseFreq[n]) + // unfaithful to local  base
                      (1-b_mix_into_good[n]) * (b_intercept+b_remote_base_freq * RemoteBaseFreq[n] + b_primed * Primed[n])); // faithful to remote base
      else // else just use half of it

        if (winner[n] == 1) // No BR, no shift
          alpha[c] = b_intercept+ b_lapse  + b_local_base_freq * LocalBaseFreq[n]+b_violate_weight_to_stress * TargetSyllHeavy[n] + b_violate_stress_secondary_stressed * TargetSyllSecondaryStress[n];
        else // No BR, yes shift
          alpha[c] = b_intercept+ b_faith + b_local_base_freq * LocalBaseFreq[n];
    }
    loglik[n] = race(winner[n], RT[n], alpha, b, sigma, psi);
    }
  target += loglik;
}

generated quantities {
  vector[N_obs] gen_RT;
  vector[N_obs] gen_winner;
  vector[N_obs] log_lik;

  for (n in 1:N_obs) {
    vector[N_choices] alpha;
    vector[2] gen;
    for (c in 1:N_choices){
      if (HasRemoteBase[n] == 1) // if remote base, use mixture model
        if (winner[n] == 1)// Yes BR, no shift
          alpha[c] = (b_mix_into_bad *(b_intercept+ b_lapse + b_local_base_freq * LocalBaseFreq[n] + b_violate_weight_to_stress * TargetSyllHeavy[n] + b_violate_stress_secondary_stressed * TargetSyllSecondaryStress[n])+ // faithful to loca base
                      (1-b_mix_into_bad)*(b_intercept+b_lapse + b_faith + b_remote_base_freq * RemoteBaseFreq[n]+ b_violate_weight_to_stress * TargetSyllHeavy[n] + b_primed * Primed[n] + b_violate_stress_secondary_stressed * TargetSyllSecondaryStress[n])) ; // unfaithful to remote base
        else // Yes BR, yes shift
          alpha[c] =  (b_mix_into_good * (b_intercept+ b_faith + b_local_base_freq * LocalBaseFreq[n]) + // unfaithful to local  base
                      (1-b_mix_into_good) * (b_intercept+b_remote_base_freq * RemoteBaseFreq[n] + b_primed * Primed[n])); // faithful to remote base
      else // else just use half of it

        if (winner[n] == 1) // No BR, no shift
          alpha[c] = b_intercept+ b_lapse  + b_local_base_freq * LocalBaseFreq[n]+b_violate_weight_to_stress * TargetSyllHeavy[n] + b_violate_stress_secondary_stressed * TargetSyllSecondaryStress[n];
        else // No BR, yes shift
          alpha[c] =b_intercept+  b_faith + b_local_base_freq * LocalBaseFreq[n];
    }
    gen = race_rng(alpha, b, sigma, psi);
    gen_winner[n] = gen[1];
    gen_RT[n] = gen[2];
    log_lik[n] = race(winner[n], RT[n], alpha, b, sigma, psi);
    }
}


That’s a latent discrete parameter model. (There’s some more examples in the finite mixtures chapter, too)

You have to marginalize out the discrete parameter by looping over all possible values and then combining the probabilities using log_sum_exp(). Here’s a quick attempt at your model

for (n in 1:N_obs) {
  matrix[2,2] ll;
  for (b_mix_into_good in 0:1) {
    for (b_mix_into_bad in 0:1) {
      vector[N_choices] alpha;
      for (c in 1:N_choices){
        if (HasRemoteBase[n] == 1) // if remote base, use mixture model
          if (winner[n] == 1)// Yes BR, no shift
            alpha[c] = (b_mix_into_bad *(b_intercept+ b_lapse + b_local_base_freq * LocalBaseFreq[n] + b_violate_weight_to_stress * TargetSyllHeavy[n] + b_violate_stress_secondary_stressed * TargetSyllSecondaryStress[n])+ // faithful to loca base
                        (1-b_mix_into_bad)*(b_intercept+b_lapse + b_faith + b_remote_base_freq * RemoteBaseFreq[n]+ b_violate_weight_to_stress * TargetSyllHeavy[n] + b_primed * Primed[n] + b_violate_stress_secondary_stressed * TargetSyllSecondaryStress[n])) ; // unfaithful to remote base
          else // Yes BR, yes shift
            alpha[c] =  (b_mix_into_good * ( b_intercept+b_faith + b_local_base_freq * LocalBaseFreq[n]) + // unfaithful to local  base
                        (1-b_mix_into_good) * (b_intercept+b_remote_base_freq * RemoteBaseFreq[n] + b_primed * Primed[n])); // faithful to remote base
        else // else just use half of it
  
          if (winner[n] == 1) // No BR, no shift
            alpha[c] = b_intercept+ b_lapse  + b_local_base_freq * LocalBaseFreq[n]+b_violate_weight_to_stress * TargetSyllHeavy[n] + b_violate_stress_secondary_stressed * TargetSyllSecondaryStress[n];
          else // No BR, yes shift
            alpha[c] = b_intercept+ b_faith + b_local_base_freq * LocalBaseFreq[n];
      }
      ll[1+b_mix_into_good, 1+b_mix_into_bad] =
            bernoulli_lpmf(b_mix_into_good| theta_mix_into_good)
          + bernoulli_lpmf(b_mix_into_bad| theta_mix_into_bad)
          + race(winner[n], RT[n], alpha, b, sigma, psi);
    }
  }
  loglik[n] = log_sum_exp(ll); 
}
target += loglik;
4 Likes

Hi @nhuurre, yes this makes sense - thanks very much again (I even knew that stan didn’t do discrete latent variables, but somehow the two facts didn’t connect for me :) ) I have one further question - the model now is quite slow, and I was wondering whether it was necessary to keep the loglik calculation in the generated quantities block if I’m not planning on using it for bayes factor comparison; is it safe to get rid of this? Thanks.

1 Like

Yes, you don’t need loglik. Even in the model block you could replace that loglik[n] = ... with target += ... as loglik goes into target anyway.

I think you can get a significant speedup if you write explicit control flow instead of multiply-by-zero, ie. instead of

alpha[c] = (b_mix_into_bad *(b_intercept+ b_lapse + b_local_base_freq * LocalBaseFreq[n] + b_violate_weight_to_stress * TargetSyllHeavy[n] + b_violate_stress_secondary_stressed * TargetSyllSecondaryStress[n])+ // faithful to loca base
           (1-b_mix_into_bad)*(b_intercept+b_lapse + b_faith + b_remote_base_freq * RemoteBaseFreq[n]+ b_violate_weight_to_stress * TargetSyllHeavy[n] + b_primed * Primed[n] + b_violate_stress_secondary_stressed * TargetSyllSecondaryStress[n])) ; // unfaithful to remote base

just write

if (b_mix_into_bad) {
  // faithful to loca base
  alpha[c] = b_intercept + b_lapse + b_local_base_freq * LocalBaseFreq[n]
             + b_violate_weight_to_stress * TargetSyllHeavy[n]
             + b_violate_stress_secondary_stressed * TargetSyllSecondaryStress[n];
} else {
  // unfaithful to remote base
  alpha[c] = b_intercept + b_lapse + b_faith
             + b_remote_base_freq * RemoteBaseFreq[n]
             + b_violate_weight_to_stress * TargetSyllHeavy[n]
             + b_primed * Primed[n]
             + b_violate_stress_secondary_stressed * TargetSyllSecondaryStress[n];
}

Furthermore, it seems that you only need either b_mix_into_bad or b_mix_into_good depending on winner[n]. If so the loops don’t need to be nested…although now that I look into it carefully it seems that for (c in 1:N_choices) loop doesn’t use c for anything except alpha[c] and that can’t be right?

2 Likes