Hi there,
I am very new to amortised bayesian inference, so apologies in advance if my questions seem uninformed or contain some misconceptions!
Anyway, I am hoping to create and fit a decision-making model, not quite like the DDM but one that also generates response times and choice variables, and which consists of 6 parameters. So far so good - my task is a two-alternative social decision task though, where the decision difficulty and thus the choice/RT varies based on the current option pair (i.e., in addition to the 6 parameters, it is also determined by the value of the “selfish” option/self_option, the value of the “altruistic” option/alt_option, and a participant-level variable called the “preferred allocation”/PA). Based on my understanding, it would make sense to train the neural network over different trials (i.e., different combinations of self_option, alt_option and PA), to make sure the network learns “proper” relationships between the parameters and the choices/RTs conditioned over the context. To make matters slightly more complex, the exact combination of self_opt, alt_opt and PA differ across participants (meaning that participants do not share the same decision option set/context), so I really need to train the network on all sorts of combinations of these variables. My question is, what is the proper way to do this? It seems to me that these variables (self_opt, alt_opt, PA) should be simulated in a separate function and then fed to the simulator object as a context variable. To that end, I have created the following function which creates an (experimentally valid) combination of self_opt, alt_opt and PA:
def make_optionpairs(batch_size=1):
step = 0.05
max_int = 19
# For batch sampling
alt_ints = np.random.randint(0, max_int, batch_size)
self_ints = alt_ints + 1 + np.random.randint(0, max_int - alt_ints)
alt_option = alt_ints * step
self_option = self_ints * step
PA = np.random.random(batch_size)
# Best: always return arrays of shape (batch_size,)
return {
"alt_option": alt_option, # shape (batch_size,)
"self_option": self_option,
"PA": PA
}
Then I add my prior-generating function and the trial-generating function (for the time being, I set it to generate and put out one choice/RT combination at a time):
def prior_sampler():
# You can change these to realistic ranges
n_sims = int(np.round(np.random.uniform(1,11)))
drift_scalings = np.random.uniform(0.1, 10)
thresholds = np.random.uniform(0.7, 3.0)
ndts = np.random.uniform(0,0.3)
rel_sps = np.random.uniform(0.3,0.7)
rt_multipliers = np.random.uniform(0.1,100)
return {"n_sims": n_sims, "drift_scaling": drift_scalings, "threshold": thresholds, "ndt": ndts, "rel_sp": rel_sps, "rt_multiplier": rt_multipliers}
def random_dm(alt_option, self_option, PA,n_sims, drift_scaling, threshold, ndt, rel_sp, rt_multiplier, noise_constant=1, dt=0.001, max_rt=10):
v = (drift_scaling *
((1 - (alt_option - PA)**2) -
(1 - (self_option - PA)**2))
)
drift_rates = v
drifts = np.broadcast_to(drift_rates, (n_sims, 1)).T
n_trials = drifts.shape[0]
acc = np.full(drifts.shape, np.nan)
rt = np.full(drifts.shape, np.nan)
max_tsteps = int(max_rt / dt)
x = np.ones(drifts.shape) * rel_sp * threshold # evidence at t=0
ongoing = np.full(drifts.shape, True)
tstep = 0
while np.any(ongoing) and tstep < max_tsteps:
# apply drift and noise only to ongoing trials
moving_indices = np.where(ongoing)
x[moving_indices] += (
drifts[moving_indices] * dt +
np.random.normal(loc=0, scale=noise_constant * np.sqrt(dt), size=len(moving_indices[0]))
)
tstep += 1
# handle threshold crossing
ended_correct = (x >= threshold)
ended_incorrect = (x <= 0)
# For correct responses
mask = (ended_correct & ongoing)
if np.any(mask):
acc[mask] = 1
rt[mask] = dt * tstep
ongoing[mask] = False
# For incorrect responses
mask = (ended_incorrect & ongoing)
if np.any(mask):
acc[mask] = -1
rt[mask] = dt * tstep
ongoing[mask] = False
rt = rt / rt_multiplier
# Aggregate "majority" choice per trial (as in rowSums)
row_acc = np.nansum(acc, axis=1)
acc_averaged = np.zeros(n_trials, dtype=int)
acc_averaged[row_acc > 0] = 1
acc_averaged[row_acc < 0] = 0
undecided = (row_acc == 0)
acc_averaged[undecided] = np.random.choice([1, 0], size=np.sum(undecided))
rt_averaged = np.nansum(rt, axis=1) + ndt
return dict(accuracy=acc_averaged, rt=rt_averaged)
And finally combine it into one simulator
simulator = bf.make_simulator([prior_sampler, random_dm], meta_fn=make_optionpairs)
Here are my questions:
- What I am unsure about is that, when I feed this all into the adapter like so:
adapter = (
bf.Adapter()
.to_array()
#.constrain(["n_sims","drift_scaling", "threshold", "ndt", "rel_sp", "rt_multiplier"],lower=np.array([0,0,0,0,0,0]),upper=np.array([100,10,10,1.5,1,5]))
.as_set(["accuracy", "rt"])
.convert_dtype("float64", "float32")
.concatenate(["accuracy","rt"], into="summary_variables")
.concatenate(["n_sims","drift_scaling", "threshold", "ndt", "rel_sp", "rt_multiplier"],into="inference_variables")
.concatenate(["alt_option","self_option","PA"], into="inference_conditions")
)
adapt_sims = adapter(simulator.sample(5))
adapt_sims ends up being a dictionary with the array “inference_conditions” of size 5 * 3 (nbatches x n_contextvariables), “inference_variables” of size 5 * 6 (nbatches x nparameters) and finally, “summary_variables” of size 5*1* 2 (nbatches x ntrials x nvariables). I am wondering whether this is right, whether having all three context variables together is “correct” and whether they should not be split into their own dimension each (similar to how the “summary_variables” operates)? And is this even the appropriate way of incorporating context variables into the workflow in the first place, or am I completely off the mark?
- Another concern I have is, how best to scale up the functions so that they accommodate multiple decision trials at once? Right now one batch represents a single decision trial; however, my guesstimate is that it would be more computationally efficient to simulate responses for a whole dataset (say, 96 trials) at once, with each of the 3 context variables also being generated 96 times, such that a single batch would represent a whole experiment. I can easily extend my random_dm function to generate multiple trials - what I’m wondering is, how should I format the choice and RT variables, and especially the 3 context variables, in the adapter such that the neural network correctly interprets the relationship between a particular set of context variables (“inference conditions”) and a particular set of response variables (“summary variables”)?
Many thanks in advance!