AXTut TaskProgram

From Emergent

Jump to: navigation, search

(back to AX Tutorial)

Contents

Programming the Task Environment

The goal here is to write a Program that will automatically generate a set of input/output patterns to train the network. Because the task is so easy (at least to start), this will not represent a savings in time, but will hopefully generate understanding of how Programs work, and will also provide a basis for making more complex programs.

The major steps involved are:

  1. Create the new Program object
  2. Initialize "enums" based on unit names -- allows us to refer to units by name (an enum is geek-speak for an enumerated set of labeled values -- more later).
  3. Iterate over the input units and generate the appropriate output response.
  4. Write the appropriate information into the input data table.

Create the Program

In the context menu (right mouse or ctrl+mouse on mac) on the programs item in the left browser, select New -- default parameters are fine, so then hit OK. This should have created a .programs.Program_11 program, which you should now click on, and change the name to: AXTaskGen (the rest of the links here will assume this name, so do enter exactly that name).

It is good to get in the habit of entering descriptions of various objects in your simluation, especially programs, so enter something like "generates the simple A-X target detection task" in the desc field.

Note that there are three sub-tabs or sub-panels for a Program:

  • Program Ctrl: For the "end user" to control the running and key parameters of the program
  • Edit Program: For writing the program.
  • Properties: For setting overall parameters of the program object, including tags which help people find this program if it is uploaded to a common repository, and flags that determine various advanced properties.

Select Edit Program and we can get started doing that!

Overview of Programing Process

Programming in this system mostly consists of dragging program elements from the toolbar at the very left edge of the display into your program, and then configuring their properties (drag-and-drop and duplicate are also very handy here).

The Program object has several different locations for different types of program elements:

  • objs -- place to put misc objects that contain local data for the program (e.g., a local DataTable that might hold some intermediate processing data for the program).
  • types -- special user-defined types that define properties of corresponding variables (e.g., the enums we'll be using).
  • args -- (short for arguments) this is where you put variables that other programs will set when they run this program.
  • vars -- place for other non-argument variables that are used in the program.
  • functions -- you can define subroutines (functions) that can be called within a program to perform a given task that needs to be done repeatedly. These functions are only accessible from within this given program.
  • init_code -- actions to be performed when the user presses the Init button -- to initialize the program and other objects that it operates on (e.g., initializing the network weights, as the LeabraTrain process does).
  • prog_code -- finally, this is where the main code for your program goes! because it can depend on any of the preceding elements, it logically comes last (and it is typically the largest).

In the Toolbox, the program elements are organized into various sub-categories (Network, Ctrl, Var/Fun, etc). Take a look through these categories and use the mouse-over to see what kinds of things are available.

Initialize Unit Names and Enums

To begin your program, locate the Network category, and drag (click and hold and move the mouse) the init nm units element into the init_code section of your program.

This Init nm units is a very powerful program element, which does a lot of configuration when it is first dropped into place. You'll see various things being created in your project, and you should get an error message indicating that it could not find the input_data table. Just hit OK to the error message, and let's take stock of what just happened (and fix the error).

You should see that a variable named input_data was created in the args section, and unit_names was created in the vars section. These are both "pointer" variables that provide a local "handle" within the program to refer to objects that actually live outside of the program, in the data section of the overall project.

Click on the .programs.AXTaskGen.args.input_data object, and take some time to mouse over the various fields and read the tooltips. We want to set the object_val field to point to our StdInputData data table -- click on it and select it.

Now go down to the .programs.AXTaskGen.vars.unit_names object, and note that it is already set to point to the UnitNames data table, which was automatically created in the data/InputData subgroup section of the project. This new datatable will contain a single row of data, with labels for each of the units in the StdInputData data table. However, right now it is empty, because we hadn't set the input_data variable yet.

Entering UnitNames

Now that we have set input_data, we can go back to the InitNamedUnits guy in the init_code, and hit the [[.programs.AXTaskGen.init_code[0].InitNamedUnits()|Init Names Table]] button. This will pull up an informational dialog -- hit OK. Now go back up to the .data.gp.InputData.UnitNames data table, and you should see two columns: Input and Output, with a single row of data. Click on the Input matrix and enter text labels for each of the units, as follows:

XYZ
ABC

For the Output matrix, you can enter N and T (for non-target and target, respectively).

View Data Legend

Next, go back to the [[.programs.AXTaskGen.init_code[0]|InitNamedUnits]] object, and select [[.programs.AXTaskGen.init_code[0].ViewDataLegend()|View Data Legend]] -- this will configure a new view frame with the input data patterns, plus a legend from the UnitNames table showing what each of the unit names are. These same names can also be applied directly to the network to label the units -- we'll do that later. You might want to remove your other view frame for StdInputData (without the legend) -- do context menu and select Delete Frame.

Creating Enums

Next, we'll create those enums mentioned previously. Click on [[.programs.AXTaskGen.init_code[0].InitDynEnums()|Init Dyn Enums]]. You will see two new entries in the types section of your program -- Input and Output.

Under the Input type, you should see 6 items with names like I_A, I_B, etc. Under the Output type, you see O_N and O_T. The first letter is taken from the first letter of the layer (I = Input, O = Output), and the remainder after the underbar is the name entered in the UnitNames table.

The purpose of these enum types is to allow you to use a symbol to refer to a unit. If you want to activate the X input unit, you can use the I_X enum value to do that. It represents the index of the X unit within the input layer -- when you click on I_X, you can see that it has a value of 3. enums have both numeric and symbolic (name-like) properties, and can be converted to and from names and numbers. You'll understand more about why they are so useful as we go along.

We are done with the unit names for now, and can move on to writing our program.

Iterating over the Inputs

The core of our program will be to loop or iterate over each of the possible input units, and then generate an appropriate output for each. We can use a for loop for this purpose.

In the left Toolbox, click on the Ctrl category (for "control"), and drag the for element into your program code (prog_code).

You should now see a set of 3 main fields for the for loop object: init, test, iter -- the default values produce a loop that goes from 0 to 9:

  • Init is for initializing your looping variable (i = 0), where i is the integer variable that keeps track of where we are in the loop -- it was automatically created by the for loop element.
  • Test is for testing when to terminate the loop (i < 10) -- as long as the i variable remains less than 10, we continue looping.
  • iter is what to do on each iteration prior to the test (i++) -- i.e., increment the loop variable.

To see this in action, let's drag the print_var guy from the Print.. category of program elements into our loop_code of the for loop. This is where we put the program elements ("code") that we want to run during each iteration of the loop. Select the i variable for the print_var field.

Now we can Init and Run our simple program. You should see a sequence of "i=0, i=1...i=9" in the console window (the small black window that opens when you first open Emergent). It is a very good idea to keep that window visible during programming, as various informative messages may show up there.

Although perhaps fascinating for new programmers, this for loop is not exactly what we want. We want to iterate over the input units, not just over the numbers from 0-9. To do that, we need to click on the .programs.AXTaskGen.vars.i variable in the vars section of the program. Change the var_type to DynEnum instead of Int. Then, click on the </code>enum_type</code> field and select the Input type (which is what we created earlier). You can also change the name of this variable to something more expressive, like input_unit. Note that when you apply this name change, the for loop code automatically updates to use this new name, as does the print var guy.

Let's go back to that [[.programs.AXTaskGen.prog_code[0]|for]] loop guy, and change the test field to: input_unit <= I_Z. Note how you can just type in I_Z and this is automatically treated as a number -- this is what enums do. Init and Run that. Everything should be fine, up until the very end, when it tries to go beyond the I_Z case -- this will generate an error message (as it should -- one of the many advantages of using enums is that they provide built-in error-checking like this).

To get around this issue, we need to add a test inside of our loop that bails out when we get to I_Z, so that final ++ increment does not occur. In the Ctrl elements, there is an if.break guy that does just this -- drag it into the loop_code so it appears at the end (note that you need to drop it on the loop_code guy itself to put it at the end, or go just after the print var and you'll see a thin horizontal line -- dropping there should work too). In the cond field, enter input_unit == I_Z (note that you can do this without much typing by selecting lookup_var and lookup_enum to choose those guys off of a list). This will break the loop at the last item (I_Z). Init and Run to confirm.

Generating the Correct Output

Next, we need to generate the correct output for each input. To do this, we first need to create a variable to hold the output value. Goto Var/Fun in the toolbox, and drag the first Var item into your program vars (select "Copy Into", not "Add Var To", when you drop -- we'll explain later). Set the name to output_unit and change the type to DynEnum with enum_type selected as Output.

Now we just need to set this variable inside our for loop code, depending on the value of the input_unit variable. The Ctrl/if guy will do this for us -- drag it into the loop_code and drop on top of the PrintVar (it will become the first element in the program). In the cond condition expression, enter: input_unit == I_A || input_unit == I_X (again note that you can use the var and enum lookup to save some typing and make sure you've spelled them correctly). The == is the logical test for equality, and the || is the logical OR operator (this is standard C++ syntax -- any valid C++ expression can be entered here). This is the condition for a target.

We need to assign our output_unit variable to the target value when this "if condition" is true, and to the non-target case otherwise. To assign a variable value, drag var= from the Var/Fun category to the true_code section of your if guy. For the result_var, select output_unit, and for the expr expression, enter O_T (or choose from the enum_lookup). This sets output_unit to O_T (target). As a timesaver, drag this AssignExpr guy from true_code into false_code -- then you can just change O_T to O_N very quickly.

Finally, go to your PrintVar and select output_unit for print_var1 (or print_var2 if that is easier to see). Init and Run, and you should see the correct values for input_unit and output_unit being displayed in your console!

Writing the Data to the Input Data Table

Now that you have all the key logic of your task, you just need to write the results to the input data table. There are three main steps for this:

  1. Erase any existing data at the start
  2. In the for loop, add a new row, and write the data for each input/output pattern
  3. Tell the system that we're done writing to each row so it can update the view

The first step is achieved by dragging a reset_rows guy from the Data toolbox to the first line of the program code (drop right on top of the for loop). Then select the input_data variable for the data_var.

Next, drag the new_row guy into the loop_code, right before the final IfBreak guy (drop on top of it). Again select input_data as the data_var. This will add a new blank row to the data table. Then, drag set units var from the Network toolbox into the loop_code, again on top of IfBreak. This is a magic little program element that uses the name of the DynEnum type of a variable, plus its value, to determine which unit to activate in the input data table. All you have to do is select input_unit for unit1, and output_unit for unit2, and you're done -- it automatically located the input_data datatable (based on its name). Note that you want to make sure you don't put one of the vars as the offset variable, which allows you to have for example multiple slots of the same units repeated in an input layer -- the offset determines which slot to put things in.

Finally, go back to the Data toolbox, and drag the row_done guy on top of IfBreak -- this just lets the system know that you're done writing to the current row of data, and that it can update any relevant displays. You must set this to input_data too.

Congratulations -- you're done!!! Select the .T3Tab.StdInputData tab in the 3d view area, and then do Init and Run -- you should see the display update with the correct patterns freshly generated by your program!

You have now done a little bit of each of the critical main elements of simulating using this system. The next few steps in the tutorial take this simple starting point and go all the way to a more scientifically interesting model of an actual psychological task: the CPT-AX task: AXTut CPTAX_Program.

Personal tools