diff --git a/emer/layer.go b/emer/layer.go index e0d3b88..516af8d 100644 --- a/emer/layer.go +++ b/emer/layer.go @@ -56,13 +56,13 @@ type Layer interface { // not found. UnitVarIndex(varNm string) (int, error) - // UnitVal1D returns value of given variable index on given unit, + // UnitValue1D returns value of given variable index on given unit, // using 1-dimensional index, and a data parallel index di, // for networks capable of processing multiple input patterns // in parallel. Returns NaN on invalid index. // This is the core unit var access method used by other methods, // so it is the only one that needs to be updated for derived layer types. - UnitVal1D(varIndex int, idx, di int) float32 + UnitValue1D(varIndex int, idx, di int) float32 // VarRange returns the min / max values for given variable VarRange(varNm string) (min, max float32, err error) @@ -123,18 +123,8 @@ type Layer interface { // WriteWeightsJSON writes the weights from this layer from the // receiver-side perspective in a JSON text format. - // We build in the indentation logic to make it much faster and - // more efficient. WriteWeightsJSON(w io.Writer, depth int) - // ReadWeightsJSON reads the weights from this layer from the - // receiver-side perspective in a JSON text format. - // This is for a set of weights that were saved - // *for one layer only* and is not used for the - // network-level ReadWeightsJSON, which reads into a separate - // structure -- see SetWeights method. - ReadWeightsJSON(r io.Reader) error - // SetWeights sets the weights for this layer from weights.Layer // decoded values SetWeights(lw *weights.Layer) error @@ -212,6 +202,11 @@ type LayerBase struct { // provides a history of parameters applied to the layer ParamsHistory params.HistoryImpl `table:"-"` + + // optional metadata that is saved in network weights files, + // e.g., can indicate number of epochs that were trained, + // or any other information about this network that would be useful to save. + MetaData map[string]string } // InitLayer initializes the layer, setting the EmerLayer interface @@ -371,7 +366,7 @@ func (ly *LayerBase) UnitValues(vals *[]float32, varNm string, di int) error { return err } for lni := range nn { - (*vals)[lni] = ly.EmerLayer.UnitVal1D(vidx, lni, di) + (*vals)[lni] = ly.EmerLayer.UnitValue1D(vidx, lni, di) } return nil } @@ -400,7 +395,7 @@ func (ly *LayerBase) UnitValuesTensor(tsr tensor.Tensor, varNm string, di int) e return err } for lni := 0; lni < nn; lni++ { - v := ly.EmerLayer.UnitVal1D(vidx, lni, di) + v := ly.EmerLayer.UnitValue1D(vidx, lni, di) if math32.IsNaN(v) { tsr.SetFloat1D(lni, math.NaN()) } else { @@ -446,7 +441,7 @@ func (ly *LayerBase) UnitValuesSampleTensor(tsr tensor.Tensor, varNm string, di return err } for i, ui := range ly.SampleIndexes { - v := ly.EmerLayer.UnitVal1D(vidx, ui, di) + v := ly.EmerLayer.UnitValue1D(vidx, ui, di) if math32.IsNaN(v) { tsr.SetFloat1D(i, math.NaN()) } else { @@ -467,7 +462,7 @@ func (ly *LayerBase) UnitValue(varNm string, idx []int, di int) float32 { return math32.NaN() } fidx := ly.Shape.Offset(idx) - return ly.EmerLayer.UnitVal1D(vidx, fidx, di) + return ly.EmerLayer.UnitValue1D(vidx, fidx, di) } // CenterPoolIndexes returns the indexes for n x n center pools of given 4D layer. diff --git a/emer/path.go b/emer/path.go index 3c61e3f..66f3a03 100644 --- a/emer/path.go +++ b/emer/path.go @@ -96,17 +96,8 @@ type Path interface { // WriteWeightsJSON writes the weights from this pathway // from the receiver-side perspective in a JSON text format. - // We build in the indentation logic to make it much faster and - // more efficient. WriteWeightsJSON(w io.Writer, depth int) - // ReadWeightsJSON reads the weights from this pathway - // from the receiver-side perspective in a JSON text format. - // This is for a set of weights that were saved *for one path only* - // and is not used for the network-level ReadWeightsJSON, - // which reads into a separate structure -- see SetWeights method. - ReadWeightsJSON(r io.Reader) error - // SetWeights sets the weights for this pathway from weights.Path // decoded values SetWeights(pw *weights.Path) error diff --git a/emer/typegen.go b/emer/typegen.go index 6393d4b..2545b7a 100644 --- a/emer/typegen.go +++ b/emer/typegen.go @@ -6,7 +6,7 @@ import ( "cogentcore.org/core/types" ) -var _ = types.AddType(&types.Type{Name: "github.com/emer/emergent/v2/emer.Layer", IDName: "layer", Doc: "Layer defines the minimal interface for neural network layers,\nnecessary to support the visualization (NetView), I/O,\nand parameter setting functionality provided by emergent.\nMost of the standard expected functionality is defined in the\nLayerBase struct, and this interface only has methods that must be\nimplemented specifically for a given algorithmic implementation.", Methods: []types.Method{{Name: "AsEmer", Doc: "AsEmer returns the layer as an *emer.LayerBase,\nto access base functionality.", Returns: []string{"LayerBase"}}, {Name: "Label", Doc: "Label satisfies the core.Labeler interface for getting\nthe name of objects generically.", Returns: []string{"string"}}, {Name: "TypeName", Doc: "TypeName is the type or category of layer, defined\nby the algorithm (and usually set by an enum).", Returns: []string{"string"}}, {Name: "UnitVarIndex", Doc: "UnitVarIndex returns the index of given variable within\nthe Neuron, according to *this layer's* UnitVarNames() list\n(using a map to lookup index), or -1 and error message if\nnot found.", Args: []string{"varNm"}, Returns: []string{"int", "error"}}, {Name: "UnitVal1D", Doc: "UnitVal1D returns value of given variable index on given unit,\nusing 1-dimensional index, and a data parallel index di,\nfor networks capable of processing multiple input patterns\nin parallel. Returns NaN on invalid index.\nThis is the core unit var access method used by other methods,\nso it is the only one that needs to be updated for derived layer types.", Args: []string{"varIndex", "idx", "di"}, Returns: []string{"float32"}}, {Name: "VarRange", Doc: "VarRange returns the min / max values for given variable", Args: []string{"varNm"}, Returns: []string{"min", "max", "err"}}, {Name: "NumRecvPaths", Doc: "NumRecvPaths returns the number of receiving pathways.", Returns: []string{"int"}}, {Name: "RecvPath", Doc: "RecvPath returns a specific receiving pathway.", Args: []string{"idx"}, Returns: []string{"Path"}}, {Name: "NumSendPaths", Doc: "NumSendPaths returns the number of sending pathways.", Returns: []string{"int"}}, {Name: "SendPath", Doc: "SendPath returns a specific sending pathway.", Args: []string{"idx"}, Returns: []string{"Path"}}, {Name: "RecvPathValues", Doc: "RecvPathValues fills in values of given synapse variable name,\nfor pathway from given sending layer and neuron 1D index,\nfor all receiving neurons in this layer,\ninto given float32 slice (only resized if not big enough).\npathType is the string representation of the path type;\nused if non-empty, useful when there are multiple pathways\nbetween two layers.\nReturns error on invalid var name.\nIf the receiving neuron is not connected to the given sending\nlayer or neuron then the value is set to math32.NaN().\nReturns error on invalid var name or lack of recv path\n(vals always set to nan on path err).", Args: []string{"vals", "varNm", "sendLay", "sendIndex1D", "pathType"}, Returns: []string{"error"}}, {Name: "SendPathValues", Doc: "SendPathValues fills in values of given synapse variable name,\nfor pathway into given receiving layer and neuron 1D index,\nfor all sending neurons in this layer,\ninto given float32 slice (only resized if not big enough).\npathType is the string representation of the path type -- used if non-empty,\nuseful when there are multiple pathways between two layers.\nReturns error on invalid var name.\nIf the sending neuron is not connected to the given receiving layer or neuron\nthen the value is set to math32.NaN().\nReturns error on invalid var name or lack of recv path (vals always set to nan on path err).", Args: []string{"vals", "varNm", "recvLay", "recvIndex1D", "pathType"}, Returns: []string{"error"}}, {Name: "UpdateParams", Doc: "UpdateParams() updates parameter values for all Layer\nand recv pathway parameters,\nbased on any other params that might have changed."}, {Name: "ApplyParams", Doc: "ApplyParams applies given parameter style Sheet to this\nlayer and its recv pathways.\nCalls UpdateParams on anything set to ensure derived\nparameters are all updated.\nIf setMsg is true, then a message is printed to confirm\neach parameter that is set.\nit always prints a message if a parameter fails to be set.\nreturns true if any params were set, and error if\nthere were any errors.", Args: []string{"pars", "setMsg"}, Returns: []string{"bool", "error"}}, {Name: "SetParam", Doc: "SetParam sets parameter at given path to given value.\nreturns error if path not found or value cannot be set.", Args: []string{"path", "val"}, Returns: []string{"error"}}, {Name: "NonDefaultParams", Doc: "NonDefaultParams returns a listing of all parameters in the Layer that\nare not at their default values -- useful for setting param styles etc.", Returns: []string{"string"}}, {Name: "AllParams", Doc: "AllParams returns a listing of all parameters in the Layer", Returns: []string{"string"}}, {Name: "WriteWeightsJSON", Doc: "WriteWeightsJSON writes the weights from this layer from the\nreceiver-side perspective in a JSON text format.\nWe build in the indentation logic to make it much faster and\nmore efficient.", Args: []string{"w", "depth"}}, {Name: "ReadWeightsJSON", Doc: "ReadWeightsJSON reads the weights from this layer from the\nreceiver-side perspective in a JSON text format.\nThis is for a set of weights that were saved\n*for one layer only* and is not used for the\nnetwork-level ReadWeightsJSON, which reads into a separate\nstructure -- see SetWeights method.", Args: []string{"r"}, Returns: []string{"error"}}, {Name: "SetWeights", Doc: "SetWeights sets the weights for this layer from weights.Layer\ndecoded values", Args: []string{"lw"}, Returns: []string{"error"}}}}) +var _ = types.AddType(&types.Type{Name: "github.com/emer/emergent/v2/emer.Layer", IDName: "layer", Doc: "Layer defines the minimal interface for neural network layers,\nnecessary to support the visualization (NetView), I/O,\nand parameter setting functionality provided by emergent.\nMost of the standard expected functionality is defined in the\nLayerBase struct, and this interface only has methods that must be\nimplemented specifically for a given algorithmic implementation.", Methods: []types.Method{{Name: "AsEmer", Doc: "AsEmer returns the layer as an *emer.LayerBase,\nto access base functionality.", Returns: []string{"LayerBase"}}, {Name: "Label", Doc: "Label satisfies the core.Labeler interface for getting\nthe name of objects generically.", Returns: []string{"string"}}, {Name: "TypeName", Doc: "TypeName is the type or category of layer, defined\nby the algorithm (and usually set by an enum).", Returns: []string{"string"}}, {Name: "UnitVarIndex", Doc: "UnitVarIndex returns the index of given variable within\nthe Neuron, according to *this layer's* UnitVarNames() list\n(using a map to lookup index), or -1 and error message if\nnot found.", Args: []string{"varNm"}, Returns: []string{"int", "error"}}, {Name: "UnitValue1D", Doc: "UnitValue1D returns value of given variable index on given unit,\nusing 1-dimensional index, and a data parallel index di,\nfor networks capable of processing multiple input patterns\nin parallel. Returns NaN on invalid index.\nThis is the core unit var access method used by other methods,\nso it is the only one that needs to be updated for derived layer types.", Args: []string{"varIndex", "idx", "di"}, Returns: []string{"float32"}}, {Name: "VarRange", Doc: "VarRange returns the min / max values for given variable", Args: []string{"varNm"}, Returns: []string{"min", "max", "err"}}, {Name: "NumRecvPaths", Doc: "NumRecvPaths returns the number of receiving pathways.", Returns: []string{"int"}}, {Name: "RecvPath", Doc: "RecvPath returns a specific receiving pathway.", Args: []string{"idx"}, Returns: []string{"Path"}}, {Name: "NumSendPaths", Doc: "NumSendPaths returns the number of sending pathways.", Returns: []string{"int"}}, {Name: "SendPath", Doc: "SendPath returns a specific sending pathway.", Args: []string{"idx"}, Returns: []string{"Path"}}, {Name: "RecvPathValues", Doc: "RecvPathValues fills in values of given synapse variable name,\nfor pathway from given sending layer and neuron 1D index,\nfor all receiving neurons in this layer,\ninto given float32 slice (only resized if not big enough).\npathType is the string representation of the path type;\nused if non-empty, useful when there are multiple pathways\nbetween two layers.\nReturns error on invalid var name.\nIf the receiving neuron is not connected to the given sending\nlayer or neuron then the value is set to math32.NaN().\nReturns error on invalid var name or lack of recv path\n(vals always set to nan on path err).", Args: []string{"vals", "varNm", "sendLay", "sendIndex1D", "pathType"}, Returns: []string{"error"}}, {Name: "SendPathValues", Doc: "SendPathValues fills in values of given synapse variable name,\nfor pathway into given receiving layer and neuron 1D index,\nfor all sending neurons in this layer,\ninto given float32 slice (only resized if not big enough).\npathType is the string representation of the path type -- used if non-empty,\nuseful when there are multiple pathways between two layers.\nReturns error on invalid var name.\nIf the sending neuron is not connected to the given receiving layer or neuron\nthen the value is set to math32.NaN().\nReturns error on invalid var name or lack of recv path (vals always set to nan on path err).", Args: []string{"vals", "varNm", "recvLay", "recvIndex1D", "pathType"}, Returns: []string{"error"}}, {Name: "UpdateParams", Doc: "UpdateParams() updates parameter values for all Layer\nand recv pathway parameters,\nbased on any other params that might have changed."}, {Name: "ApplyParams", Doc: "ApplyParams applies given parameter style Sheet to this\nlayer and its recv pathways.\nCalls UpdateParams on anything set to ensure derived\nparameters are all updated.\nIf setMsg is true, then a message is printed to confirm\neach parameter that is set.\nit always prints a message if a parameter fails to be set.\nreturns true if any params were set, and error if\nthere were any errors.", Args: []string{"pars", "setMsg"}, Returns: []string{"bool", "error"}}, {Name: "SetParam", Doc: "SetParam sets parameter at given path to given value.\nreturns error if path not found or value cannot be set.", Args: []string{"path", "val"}, Returns: []string{"error"}}, {Name: "NonDefaultParams", Doc: "NonDefaultParams returns a listing of all parameters in the Layer that\nare not at their default values -- useful for setting param styles etc.", Returns: []string{"string"}}, {Name: "AllParams", Doc: "AllParams returns a listing of all parameters in the Layer", Returns: []string{"string"}}, {Name: "WriteWeightsJSON", Doc: "WriteWeightsJSON writes the weights from this layer from the\nreceiver-side perspective in a JSON text format.\nWe build in the indentation logic to make it much faster and\nmore efficient.", Args: []string{"w", "depth"}}, {Name: "ReadWeightsJSON", Doc: "ReadWeightsJSON reads the weights from this layer from the\nreceiver-side perspective in a JSON text format.\nThis is for a set of weights that were saved\n*for one layer only* and is not used for the\nnetwork-level ReadWeightsJSON, which reads into a separate\nstructure -- see SetWeights method.", Args: []string{"r"}, Returns: []string{"error"}}, {Name: "SetWeights", Doc: "SetWeights sets the weights for this layer from weights.Layer\ndecoded values", Args: []string{"lw"}, Returns: []string{"error"}}}}) var _ = types.AddType(&types.Type{Name: "github.com/emer/emergent/v2/emer.LayerBase", IDName: "layer-base", Doc: "LayerBase defines the basic shared data for neural network layers,\nused for managing the structural elements of a network,\nand for visualization, I/O, etc.\nNothing algorithm-specific is implemented here", Fields: []types.Field{{Name: "EmerLayer", Doc: "EmerLayer provides access to the emer.Layer interface\nmethods for functions defined in the LayerBase type.\nMust set this with a pointer to the actual instance\nwhen created, using InitLayer function."}, {Name: "Name", Doc: "Name of the layer, which must be unique within the network.\nLayers are typically accessed directly by name, via a map."}, {Name: "Class", Doc: "Class is for applying parameter styles across multiple layers\nthat all get the same parameters. This can be space separated\nwith multple classes."}, {Name: "Info", Doc: "Info contains descriptive information about the layer.\nThis is displayed in a tooltip in the network view."}, {Name: "Off", Doc: "Off turns off the layer, removing from all computations.\nThis provides a convenient way to dynamically test for\nthe contributions of the layer, for example."}, {Name: "Shape", Doc: "Shape of the layer, either 2D or 4D. Although spatial topology\nis not relevant to all algorithms, the 2D shape is important for\nefficiently visualizing large numbers of units / neurons.\n4D layers have 2D Pools of units embedded within a larger 2D\norganization of such pools. This is used for max-pooling or\npooled inhibition at a finer-grained level, and biologically\ncorresopnds to hypercolumns in the cortex for example.\nOrder is outer-to-inner (row major), so Y then X for 2D;\n4D: Y-X unit pools then Y-X neurons within pools."}, {Name: "Pos", Doc: "Pos specifies the relative spatial relationship to another\nlayer, which determines positioning. Every layer except one\n\"anchor\" layer should be positioned relative to another,\ne.g., RightOf, Above, etc. This provides robust positioning\nin the face of layer size changes etc.\nLayers are arranged in X-Y planes, stacked vertically along the Z axis."}, {Name: "Index", Doc: "Index is a 0..n-1 index of the position of the layer within\nthe list of layers in the network."}, {Name: "SampleIndexes", Doc: "SampleIndexes are the current set of \"sample\" unit indexes,\nwhich are a smaller subset of units that represent the behavior\nof the layer, for computationally intensive statistics and displays\n(e.g., PCA, ActRF, NetView rasters), when the layer is large.\nIf none have been set, then all units are used.\nSee utility function CenterPoolIndexes that returns indexes of\nunits in the central pools of a 4D layer."}, {Name: "SampleShape", Doc: "SampleShape is the shape to use for the subset of sample\nunit indexes, in terms of an array of dimensions.\nSee Shape for more info.\nLayers that set SampleIndexes should also set this,\notherwise a 1D array of len SampleIndexes will be used.\nSee utility function CenterPoolShape that returns shape of\nunits in the central pools of a 4D layer."}}}) diff --git a/emer/weights.go b/emer/weights.go index 2f9a341..819ad14 100644 --- a/emer/weights.go +++ b/emer/weights.go @@ -7,16 +7,18 @@ package emer import ( "bufio" "compress/gzip" - "errors" "fmt" "io" "log" "os" "path/filepath" + "sort" + "cogentcore.org/core/base/errors" "cogentcore.org/core/base/indent" "cogentcore.org/core/core" "github.com/emer/emergent/v2/weights" + "golang.org/x/exp/maps" ) // SaveWeightsJSON saves network weights (and any other state that adapts with learning) @@ -66,9 +68,8 @@ func (nt *NetworkBase) OpenWeightsJSON(filename core.Filename) error { //types:a // todo: proper error handling here! -// WriteWeightsJSON writes the weights from this layer from the receiver-side perspective -// in a JSON text format. We build in the indentation logic to make it much faster and -// more efficient. +// WriteWeightsJSON writes the weights from this network +// from the receiver-side perspective in a JSON text format. func (nt *NetworkBase) WriteWeightsJSON(w io.Writer) error { en := nt.EmerNetwork nlay := en.NumLayers() @@ -152,3 +153,119 @@ func (nt *NetworkBase) SetWeights(nw *weights.Network) error { } return errors.Join(errs...) } + +// WriteWeightsJSONBase writes the weights from this layer +// in a JSON text format. Any values in the layer MetaData +// will be written first, and unit-level variables in unitVars +// are saved as well. Then, all the receiving path data is saved. +func (ly *LayerBase) WriteWeightsJSONBase(w io.Writer, depth int, unitVars ...string) { + el := ly.EmerLayer + w.Write(indent.TabBytes(depth)) + w.Write([]byte("{\n")) + depth++ + w.Write(indent.TabBytes(depth)) + w.Write([]byte(fmt.Sprintf("\"Layer\": %q,\n", ly.Name))) + if len(ly.MetaData) > 0 { + w.Write(indent.TabBytes(depth)) + w.Write([]byte(fmt.Sprintf("\"MetaData\": {\n"))) + depth++ + kys := maps.Keys(ly.MetaData) + sort.StringSlice(kys).Sort() + for i, k := range kys { + w.Write(indent.TabBytes(depth)) + comma := "," + if i == len(kys)-1 { // note: last one has no comma + comma = "" + } + w.Write([]byte(fmt.Sprintf("%q: %q%s\n", k, ly.MetaData[k], comma))) + } + depth-- + w.Write(indent.TabBytes(depth)) + w.Write([]byte("},\n")) + } + if len(unitVars) > 0 { + w.Write(indent.TabBytes(depth)) + w.Write([]byte(fmt.Sprintf("\"Units\": {\n"))) + depth++ + for i, vname := range unitVars { + vidx, err := el.UnitVarIndex(vname) + if errors.Log(err) != nil { + continue + } + w.Write(indent.TabBytes(depth)) + w.Write([]byte(fmt.Sprintf("%q: [ ", vname))) + nu := ly.NumUnits() + for ni := range nu { + val := el.UnitValue1D(vidx, ni, 0) + w.Write([]byte(fmt.Sprintf("%g", val))) + if ni < nu-1 { + w.Write([]byte(", ")) + } + } + comma := "," + if i == len(unitVars)-1 { // note: last one has no comma + comma = "" + } + w.Write([]byte(fmt.Sprintf(" ]%s\n", comma))) + } + depth-- + w.Write(indent.TabBytes(depth)) + w.Write([]byte("},\n")) + } + w.Write(indent.TabBytes(depth)) + onps := make([]Path, 0, el.NumRecvPaths()) + for pi := range el.NumRecvPaths() { + pt := el.RecvPath(pi) + if !pt.AsEmer().Off { + onps = append(onps, pt) + } + } + np := len(onps) + if np == 0 { + w.Write([]byte(fmt.Sprintf("\"Paths\": null\n"))) + } else { + w.Write([]byte(fmt.Sprintf("\"Paths\": [\n"))) + depth++ + for pi := range el.NumRecvPaths() { + pt := el.RecvPath(pi) + pt.WriteWeightsJSON(w, depth) // this leaves path unterminated + if pi == np-1 { + w.Write([]byte("\n")) + } else { + w.Write([]byte(",\n")) + } + } + depth-- + w.Write(indent.TabBytes(depth)) + w.Write([]byte(" ]\n")) + } + depth-- + w.Write(indent.TabBytes(depth)) + w.Write([]byte("}")) // note: leave unterminated as outer loop needs to add , or just \n depending +} + +// ReadWeightsJSON reads the weights from this layer from the +// receiver-side perspective in a JSON text format. +// This is for a set of weights that were saved *for one layer only* +// and is not used for the network-level ReadWeightsJSON, +// which reads into a separate structure -- see SetWeights method. +func (ly *LayerBase) ReadWeightsJSON(r io.Reader) error { + lw, err := weights.LayReadJSON(r) + if err != nil { + return err // note: already logged + } + return ly.EmerLayer.SetWeights(lw) +} + +// ReadWeightsJSON reads the weights from this pathway from the +// receiver-side perspective in a JSON text format. +// This is for a set of weights that were saved *for one path only* +// and is not used for the network-level ReadWeightsJSON, +// which reads into a separate structure -- see SetWeights method. +func (pt *PathBase) ReadWeightsJSON(r io.Reader) error { + pw, err := weights.PathReadJSON(r) + if err != nil { + return err // note: already logged + } + return pt.EmerPath.SetWeights(pw) +}