Genetic Algorithm Read me! By Tor Wager, 7/2/02 Wager, T. D. & Nichols, T. E. (2003) Optimization of Experimental Design in fMRI: A General Framework Using a Genetic Algorithm. Neuroimage, 18, 293-309. The idea behind the genetic algorithm is that it is an efficient way to search through a large and complicated space. In this case, the complicated space is the space of possible fMRI designs. In order to use a GA, you have to 1) be able to parameterize a design as a list of numbers, and 2) evaluate the fitness of a design using some kind of metric testable in computer simulation. This set of Matlab routines will help you to run a GA to optimize experimental designs (hopefully) a little easier than if you didn’t have them. In the best case, it’s “plug and play” – just create (or modify an example script) a script with the experiment parameters you want and run it. Text script vs. GUI This set of functions is called from a single script, which is unique to each experiment. I chose to implement it this way instead of creating a graphical user interface because the text version has several advantages. First, in the text version, you can see and save all the input parameters, and you can easily save each script you run for each experiment as a record. Second, text is easier to use (since there are a number of input parameters that must be specified), and easier to transfer across platforms. Resources The principal resources you can use to help you are 1) This readme file 2) The paper (currently under review) describing the GA 3) The code When you should use the GA (and when you shouldn’t) Using the GA is good if you have to optimize a randomized event-related design, and you want to detect one or more contrasts across different event types. (A contrast is a linear combination of signal estimates for each event type; for example, 2 different “visual” events versus two different “auditory” events.) Or, it could be good if you want to recover hemodynamic response estimates (HRF shapes) for each event type. Or, it could be good if you want to counterbalance the order of event types up to any number of time steps back (i.e., each trial follows each other one equally often). If you really want to maximize detection power for a single contrast, consider using a blocked design. That will give you the best detection power. If you can’t use a blocked design because you want to isolate particular events (and avoid confounds), the task can’t be done in a blocked fashion, or for any other reason, the GA may be able to help. Some block designs are better than others, especially with more than two conditions, but the problem can be solved analytically—the strong suit of the GA is in cases where the solution is too hard to predict a priori. ER designs with multiple contrasts generally fall under this category. If you optimize for contrast detection, what the GA will try to do with your event-related design is to create mini-blocks, or clumps of similar event types. This may make the design somewhat predictable to subjects—after all, it’s not truly random once you start selecting specific trial orders. You can counteract this by specifying maximum numbers of the same trial types that can occur in a row (hard constraint) or optimize for HRF shape estimation and/or counterbalancing in addition to contrast detection. The original GA was designed to work with single events as models, not sustained epochs. If you have an epoch-related design, there is an alternative GA function that has worked for me in the past, but hasn’t been extensively tested. The normal event-related GA function (that does most of the work) is called optimizeGA.m. The epoch-design GA function is called optimizeGA_epochs.m. Using this second function might be frustrating at this point. What to do to get started Here are some simple steps to get you going. 1) Download the zipped tar file from the website. Unzip it using: tar zxvf OptimizeDesign10.tar.gz 2) The resulting folders should be included in your matlab path. Some ways to do this are with the addpath command (try help addpath), pathtool, or specifying the GA folders in the matlab path in your .cshrc file. 3) Choose one of the example scripts stored in the example_scripts subfolder (ga_example_script.m) and open it in a text editor. The script has commenting with each line you should edit for your experiment that explains some of the options. Type the script name to run it. Here are some of the example scripts: ga_example_script.m A basic 2-condition design with probe periods, nonlinearity modeling, high-pass filtering, and autocorrelation modeling. ga_example_script_basic.m Stripped-down 2-condition difference detection ga_example_script_factorial.m ga_text_script_epochs.m ga_text_script_hox.m A 2-factor design optimizing main effects and the interaction An uncommented example script for an epoch design with 3 epochs per trial (long trials). Use of 3 epochs/trial is mandatory with this version. An uncommented example script for a GA that uses ISI as a variable parameter, and attempts to optimize ISI as well as trial order. 4) If your script terminates successfully, a variable called M (for “Model”) will be created in the workspace. M is a structure that contains information about your experiment. Refer to the fields of M with the . operator; e.g., typing M.stimlist will show the optimized stimulus presentation sequence. You can use this to program your experiment. Try typing: modelDiagnostics2(M,'plot') M.ga is a nested structure contains all the input parameters to the GA, and some measures of performance across generations. More explanation on the fields can be obtained by typing help modelDiagnostics2. modelDiagnostics2.m is the function that creates the M structure given a stimulus list and a set of filtering and other experiment parameters. a-optimality vs. d-optimality A-optimality is currently the default option in the GA. It minimizes the standard error of each contrast or predictor you’re trying to estimate. This is good for testing an a prior hypothesis – e.g., maximizing power to look for an A – B effect. However, it doesn’t take into account the correlations between estimated coefficients, and so isn’t as good if you want to know WHICH of several trial types are producing a change in a particular brain area. For these types of questions, d-optimality may be more appropriate. Doptimality maximizes the determinant of the Fisher Information Matrix X’X, and imposes a more severe penalty for multicolinearity than a-optimality. I recommend using this option for estimating HRFs. To use it, make sure GA.dflag = 1 in your script. Optimizing the spacing of events within multi-part trials The GA optimizes the order of discrete events or trial types in a sequence. However, many experiments have trials with multiple parts (e.g., encoding, delay, probe, and feedback periods), and we’d like to separate responses to each part. The way to do that is to vary the intervals between each part of the trial. A way to optimize the design by selecting the exact placement of events relative to one another is provided in ga2.m, in the ga2 subfolder. Rather than optimizing the order, GA2 optimizes the timing parameters, including presentation length and jitter. It may be desirable to optimize both the order and timing in sequences with dependencies, but that’s not quite implemented yet. GA2 has its own help files, located in the GA2 subfolder. “Jitter” in designs There are two easy ways to introduce random jitter into GA designs. The first is by setting the field GA.freqConditions to be a vector that sums to less than 1 (this is set in the GA script that you run, e.g., ga_example_script.m. freqConditions specifies the proportion of the total number of events for each trial type. For example, if you have two conditions, then [.5 .5] means 50% event type 1 and 50% event type 2. [.4 .6] signifies 40% event type 1 and 60% type 2. [.4 .4] signifies 40% for type 1 and 40% for type 2; the remaining 20% will be blank rest intervals. The second way is to create an extra condition that will be the “baseline” condition. If you have two event types plus rest, you might set GA.conditions = [1 2 3] to specify three trial types: type 1, type 2, and rest. GA.freqConditions = [.4 .4 .2] signifies 40% for type 1, 40% for type 2, and 20% rest (condition 3), and is equivalent to the example above. How designs are coded Designs are coded as a column vector of integers. Each integer represents a different condition, or “event type,” and the elements represent the position in time. Zeros represent rests. So the vector represents the stimulus presentation state at each time interval throughout the run, where the intervals are defined at an arbitrary resolution (stored in GA.ISI). ISI stands for inter-stimulus interval; in practice, that’s a pretty good time resolution to use, as design vectors too fine-grained take a long time to process. Another way of saying this is that you must specify some stimulus-presentation state at some regular interval (ISI). Zeros indicate rest intervals, and they are not modeled in the model matrix or considered in counterbalancing or other fitness measures. Much of the GA script and the main GA function (optimizeGA.m) are for the purpose of constructing a number of randomized lists of event codes according to your specifications. These vectors, concatenated into a matrix of column vectors stored in the variable listMatrix, forms the starting point for the GA. The rest of optimizeGA.m Generate stimulus lists #1 #2 #3 1 2 1 2 3 4 4 2 3 2 1 4 1 4 3 1 2 4 3 1 2 Build design matrices Test fitness of designs Select designs Interbreed stimulus lists iterate over generations #1 #2 1 2 4 3 3 2 4 1 1 4 1 2 3 1 #3 1 2 4 2 3 4 2 creates model matrices (the X matrix in a statistical GLM analysis) from each vector, tests them with fitness (or “objective”) functions, and performs crossover on the best half of each generation. This is illustrated graphically above. GA timeline: Sequence of operation Here’s a slightly more detailed look at what the GA does at what point during its operation. Your_script.m creates GA structure variable with all necessary input values For 1 to nmodels (all these names refer to variables defined in the script), runs the function optimizeGA.m OptimizeGA.m the main GA function; can also substitute optimizeGA_epochs.m (discretion is advised). This function does the following: 1) SETUP: set up contrasts, nonlinear saturation, periodic rest intervals, filtering and intrinsic autocorrelation matrices, task switching (trial order effects) and mixed block/ER setup (if specified), create initial population of randomized designs (the “dna”), hard-constraint criteria setup, approximately in that order. Prints output so you can check it. 2) Loop through generations; for each generation, and each list in each generation runs these functions in order (if necessary given your input specifications): a. dna2rna.m: Create intermediate design vector (“rna”) from the original event list (“dna”) b. getCounterBal.m: Counterbalancing fitness calculation c. calcFreqDev.m: Calculate deviation from input frequencies for each event-type (fitness measure) d. getMaxInARow.m: Calculate maximum repeats for the list (hard-constraint measure) e. designvector2model.m: Contrast detection model matrix builder. f. calcEfficiency.m: Compute contrast detection efficiency. g. tor_make_deconv_mtx2.m: Construct model matrix for HRF shape estimation efficiency. h. CalcEfficiency.m: Run again for HRF shape. 3) After each generation a. GetOverallFitness.m: calculates overall fitness score given results of all fitness tests. b. Save the best of this generation. c. Breedorgs.m: Take the best half (or roughly so, depending on value of “alpha” input parameter) of designs (“dna”) and crossover, exchanging upper halves. Returns a new population of design vectors. d. Print output for the generation. e. Every 10 generations, save intermediate results in listMat_working. If the GA quits partway through, you can use the listMatrix (the population list) variable in this file to start again where the GA left off. 4) After all generations a. Create M structure using the best overall design, with all diagnostic measures. b. modelDiagnostics2.m: adds resampled and filtered design matrices to M structure, as well as efficiency, variance inflation, condition number, predictor correlation matrix, average time between repeats, and other design fitness/evaluation measures. Also plots predictors and contrasts graphically, if specified. c. M structure is saved in model#_<date>.mat, where # is the #’th iteration of the GA (out of nmodels). Additional functions not further explained here A number of parameters coded in the scripts and functions are “add-ons” designed to perform specific functions. If you generally leave these at their default values when getting started, you should be OK. See the simple ga_example_script.m for defaults. Some of these functions (and their defaults) are: GA.doepochs = 0; Build epochs instead of events from design vector Use with optimizeGA_epochs.m GA.numhox = 0; “Hox” genes (by analogy) allow you to optimize things like ISI directly with the GA. This is experimental. It’s worked for me, but it hasn’t been widely tested, and probably only works under specific conditions. The idea is simple; include ISI, or any other input parameter, in the GA with a random value between two limits. Then optimize that parameter value with the GA. In practice, some input parameters interact with how model matrices are constructed, and some are very computationally intensive to implement. The current ones are hard-coded to represent [ISI TR cuelen cuerest stimlen stimrest resplen resprest (in s)] in that order. ISI should work, TR does not work (specify and leave at whatever your TR is), and the rest code for epoch lengths for a long singletrial epoch design with multiple periods (cue, stimulation, response) within the trial. GA.hoxrange = []; Starting ranges for each Hox parameter to optimize. GA.alph = 2.1; "selection pressure": higher is more extreme selection. GA.lowerLimit = []; From an older version, not used; don’t worry about it. GA.restevery = []; GA.restlength = []; These two parameters insert rest intervals (0’s) of length restlength (in units of the ISI) every restevery ISIs. For example, restevery = 5, restlength = 3 would insert 3 zeros into each design vector (3 ISIs) every 5th element. This is a convenient way to model instruction and probe periods, which occur periodically and regularly, but are not of interest in the study. These periods do affect the design efficiency, so including them makes the simulation a bit more realistic. GA.trans2switch = 0; This parameter is set to 1 when you have a history-dependent design. For example, if your predictors of interest are switches between two event types [1 2], and you set trans2switch to 1, conditions 1 and 2 will represent events A and B, where A is preceded by A and B is preceded by B (“no switch”). Conditions 3 and 4 will be created automatically, and they will represent A and B, where A is preceded by B and B by A (“switch”) condition. Contrasts will then have 4 conditions, potentially representing main effects of A vs B, main effects of switching, and interactions. This special feature works only when GA.conditions = [1 2]. GA.trans2block = 0; This parameter is for mixed blocked-event related designs. It takes the event-related conditions you specify, and doubles the number of conditions. If you specify [1 2 3 4] as the conditions, it will create [5 6 7 8] as well, and alternate between blocks of 1:4 and 5:8 in an ABAB fashion. Contrast lengths should double, so that if you were interested in a [1 0 0 –1] contrast, you would specify that as [1 0 0 –1 1 0 0 –1]. The contrast for block A vs. block B would be [1 1 1 1 –1 –1 –1 –1]. The blocks alternate between A and B every # elements, where # is specified by GA.restevery. If you want to use a mixed blocked/ER design but don’t want to include rest intervals, set restevery to the number of ISIs in a block, and restlength to 0. If you use both trans2switch and trans2block, you would start with [1 2] in the GA.condition vector, and specify eight values in the contrasts. [1 1 –1 –1 1 1 –1 –1] would be a main effect of switching, and [1 –1 1 –1 1 –1 1 –1] would be a main effect of event 1 vs. event 2. GA.dofirst = 0; This was included because in designing mixed block/ER switching experiments, it may be desirable to code the first trial of every block as a special trial type, neither a switch or a non-switch. Use this when using trans2switch and trans2block, but do not lengthen the contrast vectors to account for this; this special “first trial” predictor is automatically added to the end of the contrast with a weight of 0. What usually goes wrong Most of the time, an error will be caused by one of several common problems relating to the specification of input parameters. 1) The contrasts are the wrong length. There should be one column in the contrast matrix for each condition (not including the intercept). If you’re using the trans2switch option (history-dependent designs) or the trans2block option (mixed block/ER design), the number of columns in the contrasts should double—4 if your conditions are [1 2], 8 if they’re [1 2 3 4]. If you’re using both options, they’ll double twice. The intercept shouldn’t be included in the contrast weights. If you’re using the dofirst option, the contrast should not reflect this; an extra 0 is added onto the end. 2) The contrastweights vector is not the same length as the number of contrasts (rows in the contrast matrix, GA.contrasts) you specified. 3) You’re using trans2block, trans2switch, or dofirst when you didn’t mean to. A 1 indicates “yes, use this” and a 0 is “off.” Restarting the GA with a partially completed simulation If, for some reason, the GA completes a number of generations, and then the simulation is stopped partway through, you can re-start it approximately where it left off. Every 10 generations, the workspace of the main GA function is saved to listMat_working.mat. To restart, 1) Load that mat file 2) Run the first part of your script and break partway through, or paste the GA.xxxx definitions from your script into the workspace. 3) Type GA.listMatrix = listMatrix; 4) Run the bottom part of the script (paste into the workspace) after “End User Input.” The function optimizeGA will see that there is an existing listMatrix to start with (the one from your old run), so it will use that instead of creating a new randomized one. Nonlinearities The “truth” of the simulations depends on the validity of the assumption that the fMRI response is a linear function of the input. One of the things that means is that the response to each event should be the same as to all other ones of the same type, regardless of the history of past events or the identities of the proximal events. One of the best-known violations of this assumption is that the fMRI signal saturates when stimuli of the same type are presented close together in time (less than about 2 s): After the first stimulus, subsequent repeated events within the next few seconds (at least) produce less additional activation. One simple way to account for this effect in the design is to model the saturation effect as an activation “ceiling.” The response is allowed to increase linearly up to some multiple of the unit response; after that, it is “capped” at that value and not allowed to increase. This type of model is incorporated into the GA under the “nonlinthreshold” parameter. The value entered by the user is the multiple of the unit response at which the predicted signal is capped. This nonlinearity model effectively implements a penalty for designs that place stimuli too close together. Predictors that are “capped” suffer from restricted range and possibly higher collinearity with other predictors. Without this model, efficiency would increase without bound as events are spaced closer together, particularly for difference contrasts. This model is not as accurate as more complex models of nonlinearity in BOLD signal. Its main advantage in this context is that it’s very computationally efficient to implement, so it’s feasible to test the hundreds of thousands of designs necessary for GA operation with this model. Other functions included in the package (e.g., parameter mapping) There are one or two other functions that are not documented. rundesignsimMAP2.m …a function for creating surface maps of exhaustive search over parameters, particularly ISI and jitter (created by inserting 0’s into the design vector). testMvsOther.m …generates a population of random designs and tests your design (in the M structure), both in contrast efficiency and estimated t-scores, against this random population. Graphical output. …and a couple of other ones in there, too.