-
Notifications
You must be signed in to change notification settings - Fork 25
Tutorial: Building Projects
- Steps in Building a Model 2
- Sections of Code That You Are Most Likely to Want to Modify 3
- Comments at the Beginning of the File 3
- Parameters for Network, Layers, and Projections 4
- ParamSets 4
- Variables Listed for the Sim struct 8
- Configuring the Network 9
- Configuring a One to One Projection 10
- Definition of one to one projection 10
- Setting up one to one projection in ConfigNet 10
- Setting up Training and Testing Environment 11
- Defining/Opening Training, Testing Data files 12
- Setting Up ApplyInputs to Ensure Correct Layers are Referenced 14
- Modifying or Configuring Logs 15
- Example: TrainEpcLog 15
- Configuring a Log 16
- Configuring the GUI 17
- Creating a Program That Can Run Without the GUI 18
- Configuring Input DataTables 20
- Creating a Scalar Value Layer 21
- Encoding a Single Scalar Value into a Distributed Representation and Applying It to a Layer 21
- Decoding from Scalar Value Layer to Single Scalar Value and Saving to Data File 23
- Lesioning Layers and Neurons (from Dyslexia program)
- Creating Drop Down Menus
-
Decide what you are going to model
-
Design the structure of the Network
-
What Layers do you need?
-
How many layers?
-
What are their names? Remember, you will use these names to define layers and in several other places to refer to those layers.
- You can pick the name you want, you do not need to use Input, Output, or Hidden, if you want to use more descriptive names.
-
How many nodes in each layer and their shape.
- Number of nodes in Hidden layers and their level of inhibition will influence what they can represent.
-
What kind of layer are they: Input, Hidden, Target (for Training) or Compare (for Testing, learning off).
-
Update and add layer names in relevant places in program(described in following pages)
-
How are the layers connected to other layers?
-
Uni directional or bi-directional?
-
If uni directional, which direction?
-
Can you use Full projection or do you need specialized ones, such as one to one or receptive field ones?
-
-
-
Can you use the default parameters or do you need to tweak some of them? Some of the most important are:
-
Inhibition in a layer
- Need to reduce if you have small number of nodes.
-
Learning rate on projections?
-
Should the learning rate for the whole model be different from default?
-
Or should just some of the projections be different from default?
-
Should learning be on or off?
-
-
Gain and threshold in the activation function for nodes or units.
-
Conductances (excitatory and inhibitory) of nodes (function as gain)
-
Weight scaling (absolute and relative) on the weights.
-
Number of epochs.
-
Number of runs.
-
NZeroStop
-
-
Design training and testing data.
-
Create Training and Testing Data Tables to match the structure of your network
-
Manual
- Copy an already created tab-delimited text file (tsv file), such as the one in the ra25 program and then manually edit Headers and entries.
-
Automatic
- Run (or edit and then run) the ConfigPats function in ra25 example or described near end of this tutorial, that should create a tab-delimited text file (tsv) which you can then save and enter data into. Can extend to more layers. Currently what it does is it generate 25 rows of randomly generated data, with 5 X 5 Layers for both Input and Output. This can be modified as necessary.
-
-
Modify Output Logs, if necessary, to add new layers or new variables that are not tracked in the example Tables.
-
Modify GUI, if needed.
-
Add Tabs for commands.
-
Add Tabs for Plots
-
Text editors and IDEs. Text editors, such as BBedit (Mac only), and Atom and Sublime Text (Mac, Windows and Linux), have the ability to easily find functions, as well as doing syntax coloring (as do the Full featured IDEs). Full featured IDEs such as Xcode, GoLand, Visual Studio Code and Gide (developed by O'Reilly) can be used to develop in Go, and Visual Studio Code, PyCharm and Gide can be used for developing in python. Gide was developed by Randy O’Reilly and was written in Go. You can get the code at https://github.com/goki/gide/ and compile it like any Go program. The other programs are commercial but most, if not all, have free versions.
Warning: Reading data and weight files. There are two ways you can configure your program to read data and weight files into an emergent project.
-
You can read the data and weights in from an external file that is properly set up. See the Wiki for descriptions of how Training and Testing files should be formatted. There are separate instructions for how to format weight files. The description for how to create code for reading in data can be found in the Defining/Opening Training, Testing Data files section of this tutorial
-
You can "embed" the data and weight files into the executable. The description of how to do that can be found in the Wiki at:
https://github.com/emer/emergent/wiki/Embed
Note: As of go version 1.16, embed functionality is built into the language definition and the technique in the Wiki is no longer required, although it should still work.
It is important to be aware that the ra25 example reads in data from an external file, whereas most, if not all, of the sims for the textbook embed the data and weights in the executable. This difference has critical implications for how you execute a project once it is compiled.
Executing a file with these different configurations for reading data and weight files
-
Read from external file: If you read data from external data files, as in the ra25 project, then when you execute the executable you need to type
./projectname
at the command line in the Terminal window when you are in the relevant folder, so it knows to look for the data files in the current folder. Double clicking the executable will start the program, but it will not read in the data files, and typically when you Init the program it will crash. -
Read from embedded data: If you have embedded the data in the executable, then you do not need the ./ . You can either type the projectname at the command line or double click on the binary/executable.
One other reminder: When you type go build filename.go
at the command line when you are in the relevant folder it will build only that file. But many of the projects rely on multiple go files that need to be compiled into a single executable. If you compile only the single file, the build process will typically give you an error. To compile all the files in a folder into a single executable you should type simply go build
which will compile everything into a single executable with the name of the folder.
Use comments at the beginning of the file to document what the program is doing.
How to set the different parameters for nodes, layers, projections, and the Network
ParamSets is the default set of parameters -- Base is always applied, and others can be optionally selected to apply on top of that
Although this is the first major part of the program, this is not where I would typically start. I would first set up the simulation and configure the Network.
var ParamSets = params.Sets{
Below are all the defaults. (There is a button on NetView in compiled projects that shows both Default and nondefault Params for a program).
Following the defaults is the description of how to define parameters.
Regardless of whether you use type, class or specific name in referring to a layer or projection, you use the same statement to set parameters. See examples below, or in ra25 or various sims for the textbook.
Can use type, Class or specific name when defining parameters.
We adopt the CSS (cascading-style-sheets) standard where parameters can be specified in terms of the
-
Name of an object (e.g., #Hidden),
-
the Class of an object (e.g., .TopDown -- where the class name TopDown is manually assigned to relevant elements), and
-
the Type of an object (e.g., Layer applies to all layers).
The Layer Names are defined as part of creating the layers in your network in the ConfigNet part of the program. Names of individual layers are always defined when you configure the network.
Projection Names are automatically named by the program by concatenating "SendingLayerName" + To + "ReceivingLayerName" (e.g., InputToHidden). Names of a Projection can also be explicitly defined in the ConfigNet part of the program.
The Class of an object (either Layer or Projection) can be defined in the ConfigNet part of the program. Note that you do not have to define a Class unless you need to refer to that Class later, such as in defining parameters.
The following code sets the result of net.ConnectLayers to a variable pj and then uses SetClass to set different projections to the same class. Can then set unique parameters for all projections that are in that class with a single reference, instead of having to set the parameters for each projection independently.
pj := net.ConnectLayersPrjn(inp, mtxGo, onetoone, emer.Forward, &pbwm.DaHebbPrjn{})
pj.SetClass("MatrixPrjn")
pj = net.ConnectLayersPrjn(inp, mtxNoGo, onetoone, emer.Forward, &pbwm.DaHebbPrjn{})
pj.SetClass("MatrixPrjn")
pj = net.ConnectLayers(inp, pfcOut, onetoone, emer.Forward)
pj.SetClass("PFCFixed")
first set the result from net.AddLayer. to a variable, such as inp, ob1, etc. that has the value of the Layer.
inp := net.AddLayer4D("Input", 1, 7, 2, 1, emer.Input)
v1 := net.AddLayer4D("V1", 1, 7, 2, 1, emer.Hidden)
sp1 := net.AddLayer4D("Spat1", 1, 5, 2, 1, emer.Hidden)
sp2 := net.AddLayer4D("Spat2", 1, 3, 2, 1, emer.Hidden)
ob1 := net.AddLayer4D("Obj1", 1, 5, 2, 1, emer.Hidden)
out := net.AddLayer2D("Output", 2, 1, emer.Compare)
ob2 := net.AddLayer4D("Obj2", 1, 3, 2, 1, emer.Hidden)
Then use SetClass to set the layers, such as ob1 and ob2 to be members of the same class.
Can then use this to set unique parameters that only apply to these Layers that are members of this class.
ob1.SetClass("Object")
ob2.SetClass("Object")
sp1.SetClass("Spatial")
sp2.SetClass("Spatial")
The first variable in the assignment statement below, spob1, is assigned the value of the projection from sp1 to ob1 and the second variable is assigned to the value of the projection from ob1 to sp1. Then you can set each projection to a different class.*
Between pathways
p1to1 := prjn.NewPoolOneToOne()
spob1, obsp1 := net.BidirConnectLayers(sp1, ob1, p1to1)
spob2, obsp2 := net.BidirConnectLayers(sp2, ob2, p1to1)
spob1.SetClass("SpatToObj")
spob2.SetClass("SpatToObj")
obsp1.SetClass("ObjToSpat")
obsp2.SetClass("ObjToSpat")
Projection names are automatically generated by the program by concatenating "SendingLayerName" + To + "ReceivingLayerName" (e.g., InputToHidden). Names of a Projection can also be explicitly defined in the ConfigNet part of the program.
.Back, .Forward and .Lateral are already defined Classes of projections, that are applied to a particular projection when the ConnectLayer command is used.
These specify parameters for Layers and for the nodes within a Layer.
Layer.Act.
Layer.Inhib.
Layer.Learn.
These specify parameters for Projections.
Prjn.WtInit.
Prjn.WtScale.
Prjn.Learn.
Following lists all the default parameters for the layers and projections in that model. In the following example, have one example of a Layer and one of a Projection to simplify description.
Italicized options are ones that are probably of most interest to people.
/////////////////////////////////////////////////
Layer: Input
Act: {
XX1: { Thr: 0.5 Gain: 100 NVar: 0.005 VmActThr: 0.01 } // parameters of activation function
OptThresh: { Send: 0.1 Delta: 0.005 }
Init: { Decay: 1 Vm: 0.4 Act: 0 Ge: 0 } // Decay indicates proportion of activation that decays trial to trial
Dt: { Integ: 1 VmTau: 3.3 GTau: 1.4 AvgTau: 200 }
Gbar: { E: 1 L: 0.1 I: 1 K: 1 } // conductances, especially important are excitatory and inhibitory
Erev: { E: 1 L: 0.3 I: 0.25 K: 0.25 }
Clamp: { Hard: true Range: { Min: 0 Max: 0.95 } Gain: 0.2 Avg: false AvgGain: 0.2 }
Noise: { Dist: Uniform Mean: 0 Var: 0 Par: 0 Type: NoNoise Fixed: true }
**Dist** Different distributions
Uniform: Mean, Var Gaussian: Mean, Var Binomial: Mean, Par, Var
Poisson: Mean, Var. Gamma: Mean, Var, Par. Beta: Mean, Var, Par
**Noise**. Different types of noise
NoNoise // NoNoise means no noise added
VmNoise // VmNoise means noise is added to the membrane potential.IMPORTANT: this should NOT be used for rate-code
(NXX1) activations,because they do not depend directly on the vm -- this then has no effect
GeNoise // GeNoise means noise is added to the excitatory conductance (Ge). This should be used for rate coded
activations (NXX1
ActNoise // ActNoise means noise is added to the final rate code activation
GeMultNoise // GeMultNoise means that noise is multiplicative on the Ge excitatory conductance values
VmRange: { Min: 0 Max: 2 }
KNa: { On: false Rate: 0.8 Fast: { On: true Rise: 0.05 Max: 0.1 Tau: 50}
Dt: 0.02 } Med: { On: true Rise: 0.02 Max: 0.1 Tau: 200 Dt: 0.005 }
Slow: { On: true Rise: 0.001 Max: 1 Tau: 1000 Dt: 0.001} } }
Inhib: {
Layer: { On: true Gi: 1.8 FF: 1 FB: 1 FBTau: 1.4 MaxVsAvg: 0 FF0: 0.1 } // whether inhibition is on in a layer, and the amount of inhibition.
Pool: { On: false Gi: 1.8 FF: 1 FB: 1 FBTau: 1.4 MaxVsAvg: 0 FF0: 0.1 } // same parameters for pools of units
Self: { On: false Gi: 0.4 Tau: 1.4 }
ActAvg: { Init: 0.15 Fixed: false UseExtAct: false UseFirst: true Tau: 100 Adjust: 1 }
}
Learn: {
ActAvg: { SSTau: 2 STau: 2 MTau: 10 LrnM: 0.1 Init: 0.15 }
AvgL: { Init: 0.4 Gain: 1.5 Min: 0.2 Tau: 10 LrnMax: 0.5 LrnMin: 0.0001 ErrMod: true ModMin: 0.01 }
CosDiff: { Tau: 100 }
}
///////////////////////////////////////////////////
Prjn: InputToHidden
WtInit: { Dist: Uniform Mean: 0.5 Var: 0.25 Par: 0 Sym: true}
// randomly initialized starting weights, shape of distribution, mean of distribution and variance of distribution.
WtScale: { Abs: 1 Rel: 1}
// whether weights are rescaled, Abs is absolute scaling of a weight, Rel is relative to other projections.
Learn: {
Learn: true Lrate: 0.04 LrateInit: 0.04}* // whether Learning ison, learning rate, and starting value if changing learning rate over training.
XCal: { MLrn: 1 SetLLrn: true LLrn: 0 DRev: 0.1 DThr: 0.0001 LrnThr: 0.01 } // whether both Hebbian and error correcting learning are used. See the Paramset example below for how to set parameters for Hebbian, error-correcting learning and combined.
WtSig: { Gain: 6 Off: 1 SoftBound: true }
Norm: { On: true DecayTau: 1000 NormMin: 0.001 LrComp: 0.15 Stats: false }
Momentum: { On: true MTau: 10 LrComp: 0.1 }
WtBal: { On: false AvgThr: 0.25 HiThr: 0.4 HiGain: 4 LoThr: 0.4 LoGain: 6 }
}
// ParamSets is the default set of parameters -- Base is always applied, and others can be optionally
// selected to apply on top of that
var ParamSets = params.Sets{
{Name: "Base", Desc: "these are the best params", Sheets:params.Sheets{
"Network": ¶ms.Sheet{
{Sel: "Prjn", Desc: "no extra learning factors",
Params: params.Params{
"Prjn.Learn.Norm.On": "false",
"Prjn.Learn.Momentum.On": "false",
"Prjn.Learn.WtBal.On": "false",
}},
{Sel: "Layer", Desc: "needs some special inhibition and learning params",
Params: params.Params{
"Layer.Learn.AvgL.Gain": "1.5", // this is critical! 2.5 def doesn't work
"Layer.Inhib.Layer.Gi": "1.3",
"Layer.Inhib.ActAvg.Init": "0.5",
"Layer.Inhib.ActAvg.Fixed": "true",
"Layer.Act.Gbar.L": "0.1",
}},
{Sel: ".Back", Desc: "top-down back-projections MUST have lower relative weight scale, otherwise network hallucinates",
Params: params.Params{
"Prjn.WtScale.Rel": "0.3",
}},
},
}},
{Name: "Hebbian", Desc: "Hebbian-only learning params", Sheets: params.Sheets{
"Network": ¶ms.Sheet{
{Sel: "Prjn", Desc: "",
Params: params.Params{
"Prjn.Learn.XCal.MLrn": "0",
"Prjn.Learn.XCal.SetLLrn": "true",
"Prjn.Learn.XCal.LLrn": "1",
}},
},
}},
{Name: "ErrorDriven", Desc: "Error-driven-only learning params", Sheets: params.Sheets{
"Network": ¶ms.Sheet{
{Sel: "Prjn", Desc: "",
Params: params.Params{
"Prjn.Learn.XCal.MLrn": "1",
"Prjn.Learn.XCal.SetLLrn": "true",
"Prjn.Learn.XCal.LLrn": "0",
}},
},
}},
{Name: "Hebb+ErrorDriven", Desc: "Mixed Hebbian and error-correcting learning params", Sheets: params.Sheets{
"Network": ¶ms.Sheet{
{Sel: "Prjn", Desc: "",
Params: params.Params{
"Prjn.Learn.XCal.MLrn": "1",
"Prjn.Learn.XCal.SetLLrn": "false",
"Prjn.Learn.XCal.LLrn": "1",
type Sim struct {
Sim encapsulates the entire simulation model, defining which variables are part of the simulation data structure (struct). All functionality is defined as methods on this struct. This structure keeps all relevant state information organized and available without having to pass everything around as arguments to methods.
It also defines which variables show up in the control panel in NetView. Many of the variables listed here can be accessed or modified in the left hand panel in NetView, depending on settings. (note the view tags for the fields which provide hints to how things should be displayed). Definitions of view tags can be found at https://github.com/goki/gi and in the associated Wiki
If you modify an existing project, you probably don't have to do much here or with New, except for making sure you add any new layers you create or edit any layer name changes.
Look at this definition in various projects to see how visibility and ability to change a value from the GUI are indicated.
Need to define LayStatsNm here for the layers on which you record detailed statistics
// New creates new blank elements and initializes defaults
func (ss *Sim) New() {
Need to give specific LayStatsNm here for layers on which you record detailed statistics
func (ss *Sim) ConfigNet(net *leabra.Network) {
net.InitName(net, "PatAssoc")
This function has all the statements for setting up the layers and connections among layers.
Also provides positioning information for all the layers.
The following lines define the name of a Layer, its shape, its dimensions, and what type of Layer it is.
The result is assigned to the variables: inp, hid, and out, which can be used in the commands to connect the layers. Note that the variable names, such as inp, hid, and out are arbitrary. You can use other names. And you can have additional layers, with appropriate command.
inp := net.AddLayer2D("Input", 1, 4, emer.Input)
hid := net.AddLayer2D("Hidden", 1, 4, emer.Hidden)
out := net.AddLayer2D("Output", 1, 2, emer.Target)
The line below simply assigns the function for creating a new Full projection (fully connects the connected layers) to the variable full, so that the entire function name does not have to be repeatedly typed out. Could do same thing for other kinds of Prjn types, such as OneToOne.
full := prjn.NewFull()
First command below connects the input layer to the hidden layer, unidirectionally. Second command connects hidden and output layers bi-directionally.
net.ConnectLayers(inp, hid, full, emer.Forward)
net.BidirConnectLayers(hid, out, full)
If you want to create an inhibitory connection between two layers, then you would do it by using emer.Inhib in the statement for connecting two layers as follows:
net.ConnectLayers(inp, hid, full, emer.Inhib)
The following is an example of a command that helps position the layers in the 3D Network view. Documentation on Github gives more detailed information on these commands. More detailed instructions can be found in the emergent/relpos/rel.go file in Github. Provides all the relevant options.
hid.SetRelPos(relpos.Rel{Rel: relpos.Above, Other: "Input", YAlign:
relpos.Front, XAlign: relpos.Left, YOffset: 1})
these commands build the network and initialize the weights to random starting values.
net.Defaults()
ss.SetParams("Network", false) // only set Network params
err := net.Build()
if err != nil {
log.Println(err)
return
}
net.InitWts()
}
If you want to put Unit names on nodes, see Dogs and Cats project
The Full projection does not need to be configured as it automatically connects each node in the sending layer to each node in the receiving layer. However, other projection types, such as OneToOne do need be configured. For example, for OneToOne you need to specify the number of connections, the starting sending node, and the starting receiving sending node.
Other types of projections can be found in the prjn folder in:
https://github.com/emer/emergent
This is the definition of the parameters for a OneToOne projection taken from the emergent code found in the folder specified above: onetoone.go
OneToOne implements point-to-point one-to-one pattern of connectivity between two layers
type OneToOne struct {
NCons int `desc:"number of recv connections to make (0 for entire size of recv layer)"`
SendStart int `desc:"starting unit index for sending connections"`
RecvStart int `desc:"starting unit index for recv connections"`
}
Remember emergent indices always start at 0.
If needed, these three above parameters are defined in a program after one creates a NewOneToOne as in the example below
First we define the layers for the network. Then we define the projections that are going to be used. Finally we connect the layers.
func (ss *Sim) ConfigNet(net *leabra.Network) {
net.InitName(net, "PatAssoc")
inp := net.AddLayer2D("Input", 2, 11, emer.Input)
hid := net.AddLayer2D("Hidden", 6, 6, emer.Hidden)
pos := net.AddLayer2D("PosEval", 1, 1, emer.Hidden)
neg := net.AddLayer2D("NegEval", 1, 1, emer.Hidden)
out := net.AddLayer2D("Output", 1, 2, emer.Target)
full := prjn.NewFull()
Below we create a new onetoone projection for the connection between a NegEval layer that consists of just one node, and an Output layer that consists of two nodes.
Because the single unit in NegEval is connected to just the first unit in the Output layer, one can just assume defaults. Also, because the first unit in NegEval is connected only to the first unit in Output (and vice versa) we can just connect with a BidirConnectLayers as we do in the following section where we connect the layers.
neg2out := prjn.NewOneToOne()
Here we create a new onetoone projection for the connection between the PosEval layer and the Output layer. Because the single unit in PosEval is connected to the second unit in the Output layer, we cannot assume the defaults. So we use RecvStart to define the starting Receiving index (which is 1, the second unit).
Because the PosEval layer only has one node and the Output Layer has two and because the PosEval layer is connected to the second Output node, we need to define separate projections for the Forward and Backward direction, and use the ConnectLayers command for each direction, rather than using a BidirConnectLayers.
pos2out := prjn.NewOneToOne()
pos2out.RecvStart = 1
out2pos := prjn.NewOneToOne()
out2pos.SendStart = 1
// Here we connect all the layers.
net.ConnectLayers(inp, hid, full, emer.Forward)
net.BidirConnectLayers(hid, pos, full)
net.BidirConnectLayers(hid, neg, full)
net.ConnectLayers(pos, out, pos2out, emer.Forward)
net.ConnectLayers(out, pos, out2pos, emer.Forward)
net.BidirConnectLayers(neg, out, neg2out)
func (ss *Sim) ConfigEnv() {
This is where you would config the training (and testing environment), giving information about training and testing tables.
Configure Training and Testing Trials
func (ss *Sim) ConfigEnv() {
if ss.MaxRuns == 0 { // allow user override
ss.MaxRuns = 10
}
if ss.MaxEpcs == 0 { // allow user override
ss.MaxEpcs = 50
ss.NZeroStop = 5
}
// sets Name, Description and the data file that the Training environment uses
ss.TrainEnv.Nm = "TrainEnv"
ss.TrainEnv.Dsc = "training params and state"
ss.TrainEnv.Table = etable.NewIdxView(ss.Pats)
// ss.TrainEnv.Sequential = true // Training is normally random, if you
// uncommented, this would have training go sequentially through the
// Training data, as in the srn.
ss.TrainEnv.Validate()
ss.TrainEnv.Run.Max = ss.MaxRuns // note: we are not setting epoch max -- do that manually
See ss project for example of how you would set up different testing patterns that are different from the training patterns.
// sets Name, Description and the data file that the Testing environment uses
ss.TestEnv.Nm = "TestEnv"
ss.TestEnv.Dsc = "testing params and state"
ss.TestEnv.Table = etable.NewIdxView(ss.Pats)
ss.TestEnv.Sequential = true // insures testing goes sequentially through the Testing Data
ss.TestEnv.Validate()
// note: to create a train / test split of pats, do this:
// all := etable.NewIdxView(ss.Pats)
// splits, _ := split.Permuted(all, []float64{.8, .2}, []string{"Train", "Test"})
// ss.TrainEnv.Table = splits.Splits[0]
// ss.TestEnv.Table = splits.Splits[1]
ss.TrainEnv.Init(0)
ss.TestEnv.Init(0)
}
The following is modified to read external files and not stored Assets. This is taken from the err_hidden program and reads in three different data files. Uncommented code will read data from external tab-delimited text files. The OpenCSV commands are used to read data from an external tab delimited text file.
If you read data from external data files, then when you execute the executable you need to type ./projectname at the command line in the Terminal window when you are in the relevant folder, so it knows to look for the data files in the current folder. Double clicking the executable will start the program, but it will not read in the data files
NOTE: The commented code could be uncommented and used to read datafiles that are stored as Assets in the executable. If you used the OpenPatAsset commands you would comment out the OpenCSV commands.
If you embed the data into the executable, you would use the OpenPatAsset command to read the data from Assets stored in the executable. If you want to embed the data into the executable, there is a block of code that you can include in the folder from which you will create your executable. (called bindata.go and a version is found in the source code file for most of the projects). Instructions for how to do this can be found on the GitHub page at:
https://github.com/emer/emergent/wiki/Embed
func (ss *Sim) OpenPats() {
// ss.OpenPatAsset(ss.Easy, "easy.tsv", "Easy", "Easy Training patterns")
// ss.OpenPatAsset(ss.Hard, "hard.tsv", "Hard", "Hard Training patterns")
// ss.OpenPatAsset(ss.Impossible, "impossible.tsv", "Impossible", "Impossible Training patterns")
ss.Easy.OpenCSV("easy.tsv", etable.Tab)
ss.Hard.OpenCSV("hard.tsv", etable.Tab)
ss.Impossible.OpenCSV("impossible.tsv", etable.Tab)
}
Need to make sure you give the proper layer names for training and testing when you create a new network. This goes in the string beginning with "lays"
// ApplyInputs applies input patterns from given environment.
// It is good practice to have this be a separate method with appropriate
// args so that it can be used for various different contexts (training, testing, etc).
func (ss *Sim) ApplyInputs(en env.Env) {
ss.Net.InitExt()
// clear any existing inputs -- not strictly necessary if always
// going to the same layers, but good practice and cheap anyway
// the for loop goes through the list of layers[lays] and takes the
// relevant data for that layer from the training or testing tables and
// applies it to the network before the program starts to let activation
// spread through the network.
lays := []string{"Input", "Output"}
for _, lnm := range lays {
ly := ss.Net.LayerByName(lnm).(leabra.LeabraLayer).AsLeabra()
pats := en.State(ly.Nm)
if pats != nil {
ly.ApplyExt(pats)
}
}
}
For all logs there is a configuration function, such as ConfigTrnEpcLog
, that defines the structure of
the table (Schema) and what variables will be recorded. Then there is a
separate function that actually runs data collection to put data in the
logs.
NOTE: The configuration function for a log must be called in the Config
function. If you are just modifying a log, this should already be in the Config
function. However, if you are defining a new log, such as one that records at the Cycle level, this must be added to the Config
function
In the following we first describe the function that writes data to a log table and we then describe the function that configures the log so that it has the proper structure for the intended data. We use the example of the TrainEpcLog, but the basic ideas generalize to the other logs.
This function first calculates a number of variables, such as the SSE for the current Epoch (EpcSSE), and then writes the relevant data to the TrainEpcLog.
// LogTrnEpc adds data from current epoch to the TrnEpcLog table.
// computes epoch averages prior to logging.
func (ss *Sim) LogTrnEpc(dt *etable.Table) {
row := dt.Rows
dt.SetNumRows(row + 1)
epc := ss.TrainEnv.Epoch.Prv // this is triggered by increment so use previous value
nt := float64(ss.TrainEnv.Table.Len()) // number of trials in view
ss.EpcSSE = ss.SumSSE / nt
ss.SumSSE = 0
ss.EpcAvgSSE = ss.SumAvgSSE / nt
ss.SumAvgSSE = 0
ss.EpcPctErr = float64(ss.SumErr) / nt
ss.SumErr = 0
ss.EpcPctCor = 1 - ss.EpcPctErr
ss.EpcCosDiff = ss.SumCosDiff / nt
ss.SumCosDiff = 0
if ss.FirstZero < 0 && ss.EpcPctErr == 0 {
ss.FirstZero = epc
}
if ss.EpcPctErr == 0 {
ss.NZero++
} else {
ss.NZero = 0
}
For each column that has been specified in the Log Schema in the Config function that follows this example, this block of code writes the value to the cell corresponding to row for the current Epoch in the named Column (Run, Epoch, etc.). The code first goes through the list of variables (Run, Epoch, etc.). Then the following "for" loop loops through the list of Layers being recorded and writes the ActAvg value for that current layer. If you want to write other variables to this log you need to define the appropriate structure in the corresponding Config function following.
dt.SetCellFloat("Run", row, float64(ss.TrainEnv.Run.Cur))
dt.SetCellFloat("Epoch", row, float64(epc))
dt.SetCellFloat("SSE", row, ss.EpcSSE)
dt.SetCellFloat("AvgSSE", row, ss.EpcAvgSSE)
dt.SetCellFloat("PctErr", row, ss.EpcPctErr)
dt.SetCellFloat("PctCor", row, ss.EpcPctCor)
dt.SetCellFloat("CosDiff", row, ss.EpcCosDiff)
for _, lnm := range ss.LayStatNms {
ly := ss.Net.LayerByName(lnm).(leabra.LeabraLayer).AsLeabra()
dt.SetCellFloat(ly.Nm+" ActAvg", row, float64(ly.Pools[0].ActAvg.ActPAvgEff))
}
// note: essential to use Go version of update when called from another goroutine
ss.TrnEpcPlot.GoUpdate()
if ss.TrnEpcFile != nil {
if ss.TrainEnv.Run.Cur == 0 && epc == 0 {
dt.WriteCSVHeaders(ss.TrnEpcFile, etable.Tab)
}
dt.WriteCSVRow(ss.TrnEpcFile, row, etable.Tab)
}
}
This function configures the Train Epoch Log. If you want to record variables that are not in the example or project from which you are building this, or you want to delete variables, you need to edit this configuration function. It first gives all the meta data such as the table name and then defines the etable Schema, which defines the structure of the table. "sch" holds the table structure and gives the name of the column, and the data type that will be stored in it, such as INT64 or FLOAT64. The first part defines a column for each individual variable. It is then followed by a "for" loop that loops through the list of Layers in the network that you are recording (LayStatNms) and creates a variable for ActAvg for each layer (concatenates Layer name with Variable name), creates a column in the Log, specifies the data type, and appends this to the "sch" schema structure. The last command sets the Schema (structure) of the table from "sch".
The preceding function, LogTrnEpc, then records Epoch level data to this etable.
func (ss *Sim) ConfigTrnEpcLog(dt *etable.Table) {
dt.SetMetaData("name", "TrnEpcLog")
dt.SetMetaData("desc", "Record of performance over epochs of training")
dt.SetMetaData("read-only", "true")
dt.SetMetaData("precision", strconv.Itoa(LogPrec))
sch := etable.Schema{
{"Run", etensor.INT64, nil, nil},
{"Epoch", etensor.INT64, nil, nil},
{"SSE", etensor.FLOAT64, nil, nil},
{"AvgSSE", etensor.FLOAT64, nil, nil},
{"PctErr", etensor.FLOAT64, nil, nil},
{"PctCor", etensor.FLOAT64, nil, nil},
{"CosDiff", etensor.FLOAT64, nil, nil},
}
for _, lnm := range ss.LayStatNms {
sch = append(sch, etable.Column{lnm + " ActAvg", etensor.FLOAT64, nil, nil})
}
dt.SetFromSchema(sch, 0)
}
This is found in the ConfigGui function. You can probably just take the default for most of this, but if you are creating a new program, you will want to use this section to set (See below):
1) App Name in gi.SetAppName
2) Brief description of the project in gi.SetAppAbout
3) New Main Window and name in gi.NewMainWindow
This is also where you can add a bunch of commands to the toolbar, if you wish.
////////////////////////////////////////////////////////////////////////////////////////////
// Gui
// ConfigGui configures the GoGi gui interface for this simulation,
func (ss *Sim) ConfigGui() *gi.Window {
width := 1600
height := 1200
// gi.WinEventTrace = true
gi.SetAppName("ra25")
gi.SetAppAbout(`This demonstrates a basic Leabra model. See <a
href="https://github.com/emer/emergent">emergent on
GitHub</a>.</p>`)
win := gi.NewMainWindow("ra25", "Leabra Random Associator", width,
height)
If you wish to create a program that can run without the gui, you need to do the following things.
First, set up your main run function as essentially two separate functions, as below. The first function creates and configures the sim and then tests to see if there are commands on the command line, in addition to the command to start the project. If there are, it assumes that you want to run without the gui and it then calls the CmdArgs function, which will read and execute the commands. But if there are no additional commands, it calls the guirun function, defined below, which starts the gui. Second, you need to define the CmdArgs function, given below, which will be called for nogui mode.
func main() {
TheSim.New()
TheSim.Config()
if len(os.Args) 1 {
TheSim.CmdArgs() // simple assumption is that any args = no gui -- could
add explicit arg if you want
} else {
gimain.Main(func() { // this starts gui -- requires valid OpenGL display connection (e.g., X11)
guirun()
})
}
}
This is the function that will be called if you run the gui.
func guirun() {
TheSim.Init()
win := TheSim.ConfigGui()
win.StartEventLoop()
}
This is the function that will be called if you run at the command line.
func (ss *Sim) CmdArgs() {
ss.NoGui = true
var nogui bool
var saveEpcLog bool
var saveRunLog bool
var note string
flag.StringVar(&ss.ParamSet, "params", "", "ParamSet name to use --
must be valid name as listed in compiled-in params or loaded params")
flag.StringVar(&ss.Tag, "tag", "", "extra tag to add to file names
saved from this run")
flag.StringVar(¬e, "note", "", "user note -- describe the run params
etc")
flag.IntVar(&ss.MaxRuns, "runs", 10, "number of runs to do (note that
MaxEpcs is in paramset)")
flag.BoolVar(&ss.LogSetParams, "setparams", false, "if true, print a
record of each parameter that is set")
flag.BoolVar(&ss.SaveWts, "wts", false, "if true, save final weights
after each run")
flag.BoolVar(&saveEpcLog, "epclog", true, "if true, save train epoch
log to file")
flag.BoolVar(&saveRunLog, "runlog", true, "if true, save run epoch log
to file")
flag.BoolVar(&nogui, "nogui", true, "if not passing any other args and
want to run nogui, use nogui")
flag.Parse()
ss.Init()
if note != "" {
fmt.Printf("note: %sn", note)
}
if ss.ParamSet != "" {
fmt.Printf("Using ParamSet: %s \n", ss.ParamSet)
}
if saveEpcLog {
var err error
fnm := ss.LogFileName("epc")
ss.TrnEpcFile, err = os.Create(fnm)
if err != nil {
log.Println(err)
ss.TrnEpcFile = nil
} else {
fmt.Printf("Saving epoch log to: %v \n", fnm)
defer ss.TrnEpcFile.Close()
}
}
if saveRunLog {
var err error
fnm := ss.LogFileName("run")
ss.RunFile, err = os.Create(fnm)
if err != nil {
log.Println(err)
ss.RunFile = nil
} else {
fmt.Printf("Saving run log to: %v \n", fnm)
defer ss.RunFile.Close()
}
}
if ss.SaveWts {
fmt.Printf("Saving final weights per run \n")
}
fmt.Printf("Running %d Runs \n", ss.MaxRuns)
ss.Train()
}
Unlike the previous version of emergent, there is no wizard that can be used to create the various Training and Testing DataTables that the program uses, with the appropriate headers and structure. There are two possibilities for doing so.
-
One possibility is to explicitly create the structure and column names for a datatable in a .tsv (tab delimited text file) in a program like Excel or modify an existing datatable. See examples of the sim programs, such as err_hidden, or ra25 for examples of the structure of a Training and Testing file, with appropriate headers that define the cell structure of the tables.
-
Use a ConfigPats program, such as the one below, to define the structure of a datatable and the variables you wish to record. Also see example in ra25. In the ra25 program, the ConfigPats function is commented out under the Config function, so the current input data file in the folder will not change. However, if you uncommented the ConfigPats function, built the program again, and started the program, then it would create a new version of the input data file. So one could follow the example in the ra25 program and in your own program edit ConfigPats to build the structure of an initial data file to your specification, uncomment ConfigPats, build the program and then run the program, which should create a data table with the specified structure. Then one could add relevant Training and Testing data to this data table. Once you have created the data table, you would then comment out ConfigPats, build the program again, and when you run the program it should read in your new data.
func (ss *Sim) ConfigPats() {
dt := ss.Pats
dt.SetMetaData("name", "TrainPats")
dt.SetMetaData("desc", "Training patterns")
// define name of Columns/Layers (Column in the datatable and corresponding Layer in the Network
// need to have identical names), dimensions and shapes.
// This piece of the Input and Output definitions: []int{5, 5} defines the dimensions of the layers
// Here it is defining layers that are 5 X 5, but you can make layers any shape that makes sense.
// Last element in this section, the 25 defines the number of rows to be generated in the DataTable.
// Can add additional Columns/Layers here
dt.SetFromSchema(etable.Schema{
{"Name", etensor.STRING, nil, nil},
{"Input", etensor.FLOAT32, []int{5, 5}, []string{"Y", "X"}},
{"Output", etensor.FLOAT32, []int{5, 5}, []string{"Y", "X"}},
}, 25)
// following generates permuted binary rows and puts them in columns
// indicated by dt.Cols[n] (does not include Name Column). First number
// (here 6, but could be something else ) indicates number of entries
// that are on, second number indicates on value, and third number
// indicates off value.
// Note that this is merely a way to create a datatable with the
// correct structure that can be edited in Excel or a similar program.
// Could make other choices.
patgen.PermutedBinaryRows(dt.Cols[1], 6, 1, 0)
patgen.PermutedBinaryRows(dt.Cols[2], 6, 1, 0)
// this saves the resulting data table.
dt.SaveCSV("random_5x5_25_gen.csv", etable.Tab, etable.Headers)
}
Creating a Scalar Value Layer, using popcode: turns single scalar value into distributed representation.
[Based on John Rohrlich's code for encoding scalar value layers (popcode)] This is a description for how to create a oneD scalar layer, e.g, 1 X N or N X 1. Other function for TwoD.
- Define Tensors for popcode layers in the Sim struct. Note that these are defined below code as Input Tensors. No reason they cannot be used to define a Target Tensor, with a different name. Also, don't have to define 2, can define as few or as many as you want.
InOneTsr *etensor.Float32 `view:"-" desc:"for holding layer values"`
InTwoTsr *etensor.Float32 `view:"-" desc:"for holding layer values"`
- Include Function to construct the Tensors, if they don't already exist. Define the desired dimension. Typical is 11 X 1, but could create larger layer, if desired. Only need to construct as many as you need. Note that InputTsrs is arbitrary name. Could use alternative name, depending on what the function is of the Layers you are using (e.g., ConfigEvalTsrs if you were using evaluation layers)
func (ss *Sim) ConfigInputTsrs() {
if ss.InOneTsr == nil {
ss.InOneTsr = etensor.NewFloat32([]int{11, 1}, nil, nil)
}
if ss.InTwoTsr == nil {
ss.InTwoTsr = etensor.NewFloat32([]int{11, 1}, nil, nil)
}
}
-
Make sure the call to construct the tensors (e.g., ConfigInputTsrs() function) is called in the Config function.
-
Define layer and dimensions in ConfigNet as you do for all other Layers in the Network. Typical dimensions are 11 X 1, but could be different. But need to match the dimension of the tensors you create. Use standard inhibition for Target layer. Layer name can be whatever you would like and typically different from name of tensor. This name is how you refer to Layer.
-
0 to 1 values that are the input data for the layer are in a Named column (Named with Layer name) in the training etable, in the first element of a two dimensional cell with the same size as the network layer. That is, if the network layer is 11 X 1, then the cell should have a 11 X 1 size
-
Apply values to network, as in the modified ApplyInputs function below
-
Read the 0 to 1 layer value from the etable.
-
Encode the layer value as an n value tensor, the size of the layer.
-
Apply tensor to the appropriate layer
-
Notes about code below
pc set to popcode.OneD{}.
pc.Defaults sets defaults for popcode functions; can be found in popcode.go file.
v := float32(pats.FloatVal1D(0))
[reads the first element in the Tensor in pats into float32 single value]
// Encode generates a pattern of activation of given size to encode given value.
// n must be 2 or more. pat slice will be constructed if len != n.
// If add == false (use Set const for clarity), values are set to pattern
// else if add == true (Add), then values are added to any existing,
// for encoding additional values in same pattern.
pc.Encode(&ss.InOneTsr.Values, v, 11, add)
Encodes float32 value in v into an 11 element tensor (depends on size of layer) which is then applied to the relevant network. add is a boolean, which should be replaced with 'false' if values are to be set to the pattern, and replaced with 'true', if values are added to existing values. Would typically set add to 'false'. Encode needs the & prepended to indicate that this is a pointer to an underlying value.
Note that ly.ApplyExt uses a Tensor name without the & prepended as the value is not being modified.
Note that ly refers to the Layer
func (ss *Sim) ApplyInputs(en env.Env) {
ss.Net.InitExt() // clear any existing inputs -- not strictly
necessary if always
// going to the same layers, but good practice and cheap anyway
pc := popcode.OneD{}
pc.Defaults()
lays := []string{"InputOne", "InputTwo", "Output"}
for _, lnm := range lays {
ly := ss.Net.LayerByName(lnm).(leabra.LeabraLayer).AsLeabra()
pats := en.State(ly.Nm)
if pats != nil {
if ly.Nm == "InputOne" {
v := float32(pats.FloatVal1D(0))
pc.Encode(&ss.InOneTsr.Values, v, 11)
ly.ApplyExt(ss.InOneTsr)
}
if ly.Nm == "InputTwo" {
v := float32(pats.FloatVal1D(0))
pc.Encode(&ss.InTwoTsr.Values, v, 11)
ly.ApplyExt(ss.InTwoTsr)
}
if ly.Nm == "Output" {
ly.ApplyExt(pats)
}
}
}
}
After training or testing, you then need to turn layer representation back into a single scalar value and save to a data file.
-
When saving to data file need to get value of layer from the Network and have it turned into a FLOAT64 single number that will be written to a log file.
-
popcode.Decode returns a FLOAT32, so need to convert to FLOAT64, as in following code.
The example below shows how to do this for a TstTrlLog.
Note that because this is a single FLOAT64 value, you will use a SetCellFloat command, not a SetCellTensor command to save the value to the Table.
Notes: When you define this in the DataTable schema in the Config function configuring the TstTrlLog, result should be set to an etensor.FLOAT64, with a Shape as nil.
To set up Decode need to do the following:
In the Sim struct, need to define a TmpVals to hold a slice of float32 ([ ]float32) as below (this is a temporary location).
- This TmpVals will hold the result of reading the values from a scalar value layer so that it can then be turned into a single scalar value before it is written to an etable.
Type Sim struct {
…
TmpVals []float32
…
}
Assuming that the sim is referred to as ss, then
ss.TmpVals is a slice of float32’s and the '&' in front of it provides UnitVals (see below) with a pointer (i.e. the address of the slice). UnitVals takes the specified value of the layer and writes it to ss.TmpVals. UnitVals needs the address so it can modify the values of the slice. Decode doesn’t need the address (the preceding &) because it isn’t going to change the values, it decodes it into another value. So for Decode you would just pass ss.TmpVals.
Code that would need to be in the function to write results to a Log, as in example below.
inp1 := ss.Net.LayerByName("InputOne").(leabra.LeabraLayer).AsLeabra()
// assigns this layer to the variable inp1, so it can be referenced below
……
pc := popcode.OneD{}
this simply allows you to refer to popcode.OneD{} by a shorter name, by assigning that function to the variable pc.
……
inp1.UnitVals(&ss.TmpVals, "ActM") // this writes Act value from the layer
// referenced by inp1 into the slice of floats.
iva1 := pc.Decode(ss.TmpVals) // this Decodes the slice into a single float32
dt.SetCellFloat("InAct1M", row, float64(iva1)) // this changes the float32 to float64 and writes to column in Log.
This block can be duplicated as necessary.
Here is a LogTstTrl and ConfigTstTrlLog pair that Configs the Log and then writes to the Log using Decode from the popcode (ScalarValueLayer) code.
// LogTstTrl adds data from current trial to the TstTrlLog table.
// log always contains number of testing items
func (ss *Sim) LogTstTrl(dt *etable.Table) {
epc := ss.TrainEnv.Epoch.Prv // this is triggered by increment so use previous value
inp1 := ss.Net.LayerByName("InputOne").(leabra.LeabraLayer).AsLeabra()
inp2 := ss.Net.LayerByName("InputTwo").(leabra.LeabraLayer).AsLeabra()
inp3 := ss.Net.LayerByName("InputThree").(leabra.LeabraLayer).AsLeabra()
inp4 := ss.Net.LayerByName("InputFour").(leabra.LeabraLayer).AsLeabra()
out := ss.Net.LayerByName("Output").(leabra.LeabraLayer).AsLeabra()
pc := popcode.OneD{}
trl := ss.TestEnv.Trial.Cur
row := trl
if dt.Rows <= row {
dt.SetNumRows(row + 1)
}
dt.SetCellFloat("Run", row, float64(ss.TrainEnv.Run.Cur))
dt.SetCellFloat("Epoch", row, float64(epc))
dt.SetCellFloat("Trial", row, float64(trl))
dt.SetCellString("TrialName", row, ss.TestEnv.TrialName.Cur)
dt.SetCellFloat("Err", row, ss.TrlErr)
dt.SetCellFloat("SSE", row, ss.TrlSSE)
dt.SetCellFloat("AvgSSE", row, ss.TrlAvgSSE)
dt.SetCellFloat("CosDiff", row, ss.TrlCosDiff)
for _, lnm := range ss.LayStatNms {
ly := ss.Net.LayerByName(lnm).(leabra.LeabraLayer).AsLeabra()
dt.SetCellFloat(ly.Nm+" ActM.Avg", row, float64(ly.Pools[0].ActM.Avg))
}
ovt := ss.ValsTsr("Output")
// Decodes the scalar value layers into a float32 scalar value and then changes to
// float64 and writes to the cell in the named column in the appropriate row.
inp1.UnitVals(&ss.TmpVals, "ActM")
iva1 := pc.Decode(ss.TmpVals)
dt.SetCellFloat("InAct1", row, float64(iva1))
inp2.UnitVals(&ss.TmpVals, "ActM")
iva2 := pc.Decode(ss.TmpVals)
dt.SetCellFloat("InAct2", row, float64(iva2))
inp3.UnitVals(&ss.TmpVals, "ActM")
iva3 := pc.Decode(ss.TmpVals)
dt.SetCellFloat("InAct3", row, float64(iva3))
inp4.UnitVals(&ss.TmpVals, "ActM")
iva4 := pc.Decode(ss.TmpVals)
dt.SetCellFloat("InAct4", row, float64(iva4))
out.UnitValsTensor(ovt, "ActM")
dt.SetCellTensor("OutActM", row, ovt)
out.UnitValsTensor(ovt, "Targ")
dt.SetCellTensor("OutTarg", row, ovt)
// note: essential to use Go version of update when called from another goroutine
ss.TstTrlPlot.GoUpdate()
}
This configures the TstTrlLog so that it has the correct columns and the correct types for the Cells in the Log.
func (ss *Sim) ConfigTstTrlLog(dt *etable.Table) {
out := ss.Net.LayerByName("Output").(leabra.LeabraLayer).AsLeabra()
dt.SetMetaData("name", "TstTrlLog")
dt.SetMetaData("desc", "Record of testing per input pattern")
dt.SetMetaData("read-only", "true")
dt.SetMetaData("precision", strconv.Itoa(LogPrec))
nt := ss.TestEnv.Table.Len() // number in view
sch := etable.Schema{
{"Run", etensor.INT64, nil, nil},
{"Epoch", etensor.INT64, nil, nil},
{"Trial", etensor.INT64, nil, nil},
{"TrialName", etensor.STRING, nil, nil},
{"Err", etensor.FLOAT64, nil, nil},
{"SSE", etensor.FLOAT64, nil, nil},
{"AvgSSE", etensor.FLOAT64, nil, nil},
{"CosDiff", etensor.FLOAT64, nil, nil},
}
for _, lnm := range ss.LayStatNms {
sch = append(sch, etable.Column{lnm + " ActM.Avg", etensor.FLOAT64, nil,
nil})
}
sch = append(sch, etable.Schema{
{"InAct1", etensor.FLOAT64, nil, nil}, // defines columns in etable for
{"InAct2", etensor.FLOAT64, nil, nil}, // the scalar values decoded from
{"InAct3", etensor.FLOAT64, nil, nil}, // the layers and adds to Schema
{"InAct4", etensor.FLOAT64, nil, nil},
{"OutActM", etensor.FLOAT64, out.Shp.Shp, nil},
{"OutTarg", etensor.FLOAT64, out.Shp.Shp, nil},
}...)
dt.SetFromSchema(sch, nt)
}
// Lesion proportion of the Neurons in a layer
net.LayerByName("LayerName").(leabra.LeabraLayer).AsLeabra().LesionNeurons(prop)
prop – proportion of neurons to lesion, ranges from 0 to 1
// Lesion entire Layer by Name
net.LayerByName("LayerName").SetOff(true)
// UnLesion entire Layer by Name
net.LayerByName("LayerName").SetOff(false)
// Unlesion all Layers in a Network
net.LayersSetOff(false)
// Unlesion all Neurons in a Network
net.UnLesionNeurons()
// Function that would lesion specified units in a Layer. LesionUnit lesions given unit number in given layer by setting all weights to 0
func (ss *Sim) LesionUnit(lay *leabra.Layer, unx, uny int) {
ui := etensor.Prjn2DIdx(&lay.Shp, false, uny, unx)
rpj := lay.RecvPrjns()
for _, pji := range *rpj {
pj := pji.(*leabra.Prjn)
nc := int(pj.RConN[ui])
st := int(pj.RConIdxSt[ui])
for ci := 0; ci < nc; ci++ {
rsi := pj.RSynIdx[st+ci]
sy := &pj.Syns[rsi]
sy.Wt = 0
pj.Learn.LWtFmWt(sy)
}
}
}
Taken from the dyslexia sim, where one can choose which layers to network and what proportion of neurons to lesion
Function that insures that all Layers and neurons are not lesioned. Can use to insure that you are always starting with a fully intact network before you do any lesioning.
func (ss *Sim) UnLesionNet(net *leabra.Network) {
net.LayersSetOff(false)
net.UnLesionNeurons()
net.InitActs()
}
LesionNet does lesion of network with given proportion of neurons damaged 0 < prop < 1.
func (ss *Sim) LesionNet(les LesionTypes, prop float32) {
net := ss.Net
lesStep := float32(0.1)
if les == AllPartial {
for ls := OShidden; ls < AllPartial; ls++ {
for prp := lesStep; prp < 1; prp += lesStep {
ss.UnLesionNet(net)
ss.LesionNetImpl(net, ls, prp)
ss.TestAll()
}
}
} else {
ss.UnLesionNet(net)
ss.LesionNetImpl(net, les, prop)
}
}
Function that uses a switch statement to decide which Layers to lesion, or to lesion a proportion of the neurons in a layer.
func (ss *Sim) LesionNetImpl(net *leabra.Network, les LesionTypes, prop float32) {
ss.Lesion = les
ss.LesionProp = prop
switch les {
case NoLesion:
case SemanticsFull:
net.LayerByName("OShidden").SetOff(true)
net.LayerByName("Semantics").SetOff(true)
net.LayerByName("SPhidden").SetOff(true)
case DirectFull:
net.LayerByName("OPhidden").SetOff(true)
case OShidden:
net.LayerByName("OShidden").(leabra.LeabraLayer).AsLeabra().LesionNeurons(prop)
case SPhidden:
net.LayerByName("SPhidden").(leabra.LeabraLayer).AsLeabra().LesionNeurons(prop)
case OPhidden:
net.LayerByName("OPhidden").(leabra.LeabraLayer).AsLeabra().LesionNeurons(prop)
case OShidDirectFull:
net.LayerByName("OPhidden").SetOff(true)
net.LayerByName("OShidden").(leabra.LeabraLayer).AsLeabra().LesionNeurons(prop)
case SPhidDirectFull:
net.LayerByName("OPhidden").SetOff(true)
net.LayerByName("SPhidden").(leabra.LeabraLayer).AsLeabra().LesionNeurons(prop)
case OPhidSemanticsFull:
net.LayerByName("OShidden").SetOff(true)
net.LayerByName("Semantics").SetOff(true)
net.LayerByName("SPhidden").SetOff(true)
net.LayerByName("OPhidden").(leabra.LeabraLayer).AsLeabra().LesionNeurons(prop)
}
}
Use the stringer program to create a set of Enums that can be used in switch statements that can be linked to drop-down menus in the gui.
The following example shows how to create drop down menus to select different datasets for Training and Testing, and to select different paramsets to specify different types of learning: Hebbian, error-correcting and combined. However, similar logic can be used to create Enums for switch statements to do other things.
This code is taken from the error_driven_combo sim.
First, make sure that you have followed the instructions on the GoGi install page to install the stringer function. The following instruction should work
go install GitHub.com/goki/stringer@latest
PatsType is the type of training patterns. Refers to different data files
type PatsType int32
Following is a command that will call the stringer program, with a type flag set. In go, you can use the go:generate command in a comment to run programs like stringer.
However, if you find that go:generate, as described below, does not seem to be working, you can call the stringer program manually by cd'ing into the folder containing the program you want to build drop down menus for and then execute the following command, where you should replace PatsType with whatever is the type you want to create.
stringer -type=PatsType
this should create a go file called something like: patstype_string.go when you build the program, this code will be part of the executable.
Here is how to use go:generate within the main package. See above if this does not seem to work.
//go:generate stringer -type=PatsType
var KiT_PatsType = kit.Enums.AddEnum(PatsTypeN, kit.NotBitFlag, nil)
func (ev PatsType) MarshalJSON() ([]byte, error) { return kit.EnumMarshalJSON(ev) }
func (ev *PatsType) UnmarshalJSON(b []byte) error { return kit.EnumUnmarshalJSON(ev, b) }
const (
// Easy patterns can be learned by Hebbian learning
Easy PatsType = iota
// Hard patterns can only be learned with error-driven learning
Hard
// Impossible patterns require error-driven + a hidden layer
Impossible
// Lines patterns
Lines2
PatsTypeN
)
LearnType is the type of learning to use. Applies different parameters.
type LearnType int32
//go:generate stringer -type=LearnType
var KiT_LearnType = kit.Enums.AddEnum(LearnTypeN, kit.NotBitFlag, nil)
func (ev LearnType) MarshalJSON() ([]byte, error) { return kit.EnumMarshalJSON(ev) }
func (ev *LearnType) UnmarshalJSON(b []byte) error { return kit.EnumUnmarshalJSON(ev, b) }
const (
Hebbian LearnType = iota
ErrorDriven
ErrorHebbIn
LearnTypeN
)
See error_driven_combo for an example.
Example ParamSet from error_driven_combo is below. Make sure that you define the Learn and Pats variable in the Sim struct and also define the different datatables you are going to select with the PatsType drop down menu.
type Sim struct {
Net *leabra.Network `view:"no-inline" desc:"the network -- click to view / edit parameters for layers, prjns, etc"`
Learn LearnType `desc:"select which type of learning to use"`
Pats PatsType `desc:"select which type of patterns to use"`
Easy *etable.Table `view:"no-inline" desc:"easy training patterns -- can be learned with Hebbian"`
Hard *etable.Table `view:"no-inline" desc:"hard training patterns -- require error-driven"`
Impossible *etable.Table `view:"no-inline" desc:"impossible training patterns -- require error-driven + hidden layer"`
Lines2 *etable.Table `view:"no-inline" desc:"lines training patterns"`
........
Make sure the variables corresponding to the different types are given a default value in the New function for creating a new sim struct.
New creates new blank elements and initializes defaults
func (ss *Sim) New() {
ss.Net = &leabra.Network{}
ss.Learn = ErrorDriven
ss.Pats = Lines2
ss.Easy = &etable.Table{}
ss.Hard = &etable.Table{}
ss.Impossible = &etable.Table{}
ss.Lines2 = &etable.Table{}
......
This insures that after you have used the dropdown menu to specify new datatables, that the TraineEnv and TestEnv are properly set, once you have hit the Init button to initialize the network or have hit the Run button to start a new run.
func (ss *Sim) UpdateEnv() {
switch ss.Pats {
case Easy:
ss.TrainEnv.Table = etable.NewIdxView(ss.Easy)
ss.TestEnv.Table = etable.NewIdxView(ss.Easy)
case Hard:
ss.TrainEnv.Table = etable.NewIdxView(ss.Hard)
ss.TestEnv.Table = etable.NewIdxView(ss.Hard)
case Impossible:
ss.TrainEnv.Table = etable.NewIdxView(ss.Impossible)
ss.TestEnv.Table = etable.NewIdxView(ss.Impossible)
case Lines2:
all := etable.NewIdxView(ss.Lines2)
splits, _ := split.Permuted(all, []float64{.9, .1}, []string{"Train", "Test"})
ss.TrainEnv.Table = splits.Splits[0]
ss.TestEnv.Table = splits.Splits[1]
// ss.TrainEnv.Table = splits.Splits[0]//etable.NewIdxView(ss.Lines2)
// ss.TestEnv.Table = splits.Splits[1]//etable.NewIdxView(ss.Lines2)
}
}
Modify the SetParams function to include the switch statement that sets different paramsets for the different learning types.
SetParams sets the params for "Base" and then current ParamSet.
If sheet is empty, then it applies all avail sheets (e.g., Network, Sim) otherwise just the named sheet
If setMsg = true then we output a message for each param that was set.
These different parameter sets are defined at the beginning of the program in the ParamSet.
See after this function for example in error_driven_hidden.
func (ss *Sim) SetParams(sheet string, setMsg bool) error {
if sheet == "" {
// this is important for catching typos and ensuring that all sheets can be used
ss.Params.ValidateSheets([]string{"Network", "Sim"})
}
err := ss.SetParamsSet("Base", sheet, setMsg)
if ss.ParamSet != "" && ss.ParamSet != "Base" {
sps := strings.Fields(ss.ParamSet)
for _, ps := range sps {
err = ss.SetParamsSet(ps, sheet, setMsg)
}
}
switch ss.Learn {
case Hebbian:
ss.SetParamsSet("Hebbian", sheet, setMsg)
case ErrorDriven:
ss.SetParamsSet("ErrorDriven", sheet, setMsg)
case ErrorHebbIn:
ss.SetParamsSet("ErrorHebbIn", sheet, setMsg)
}
return err
}
ParamSets is the default set of parameters -- Base is always applied, and others can be optionally selected to apply on top of that
var ParamSets = params.Sets{
{Name: "Base", Desc: "these are the best params", Sheets: params.Sheets{
"Network": ¶ms.Sheet{
{Sel: "Prjn", Desc: "no extra learning factors",
Params: params.Params{
"Prjn.Learn.Norm.On": "false",
"Prjn.Learn.Momentum.On": "false",
"Prjn.Learn.WtBal.On": "false",
}},
{Sel: "Layer", Desc: "needs some special inhibition and learning params",
Params: params.Params{
"Layer.Learn.AvgL.Gain": "1.5", // this is critical! 2.5 def doesn't work
"Layer.Inhib.Layer.Gi": "1.3",
"Layer.Inhib.ActAvg.Init": "0.5",
"Layer.Inhib.ActAvg.Fixed": "true",
"Layer.Act.Gbar.L": "0.1",
}},
{Sel: ".Back", Desc: "top-down back-projections MUST have lower relative weight scale, otherwise network hallucinates",
Params: params.Params{
"Prjn.WtScale.Rel": "0.3",
}},
},
}},
{Name: "Hebbian", Desc: "Hebbian-only learning params", Sheets: params.Sheets{
"Network": ¶ms.Sheet{
{Sel: "Prjn", Desc: "",
Params: params.Params{
"Prjn.Learn.XCal.MLrn": "0",
"Prjn.Learn.XCal.SetLLrn": "true",
"Prjn.Learn.XCal.LLrn": "1",
}},
},
}},
{Name: "ErrorDriven", Desc: "Error-driven-only learning params", Sheets: params.Sheets{
"Network": ¶ms.Sheet{
{Sel: "Prjn", Desc: "",
Params: params.Params{
"Prjn.Learn.XCal.MLrn": "1",
"Prjn.Learn.XCal.SetLLrn": "true",
"Prjn.Learn.XCal.LLrn": "0",
}},
},
}},
}
Make sure that OpenPats or OpenPatAsset is set to read in all the datatables that you need for the different variables you have defined.
Note that in the following it uses the OpenPatAsset command which reads data from the compiled code. The code for reading from an external data file is commented out.
func (ss *Sim) OpenPats() {
// patgen.ReshapeCppFile(ss.Easy, "easy.dat", "easy.tsv") // one-time reshape
// patgen.ReshapeCppFile(ss.Hard, "hard.dat", "hard.tsv") // one-time reshape
// patgen.ReshapeCppFile(ss.Impossible, "impossible.dat", "impossible.tsv") // one-time reshape
ss.OpenPatAsset(ss.Easy, "easy.tsv", "Easy", "Easy Training patterns")
// err := ss.Easy.OpenCSV("easy.tsv", etable.Tab)
ss.OpenPatAsset(ss.Hard, "hard.tsv", "Hard", "Hard Training patterns")
// err = ss.Hard.OpenCSV("hard.tsv", etable.Tab)
ss.OpenPatAsset(ss.Impossible, "impossible.tsv", "Impossible", "Impossible Training patterns")
// err = ss.Impossible.OpenCSV("impossible.tsv", etable.Tab)
ss.OpenPatAsset(ss.Lines2, "lines2out1.tsv", "Lines2", "Lines2 Training patterns")
// err = ss.Lines2.OpenCSV("lines2out1.tsv", etable.Tab)
}
test