diff --git a/Editor/Cluster_Drawer.cs b/Editor/Cluster_Drawer.cs index acc8eb3..d1e3778 100644 --- a/Editor/Cluster_Drawer.cs +++ b/Editor/Cluster_Drawer.cs @@ -150,20 +150,20 @@ namespace NanoBrain.Unity { else if (selectedTarget is GameObject g) gameObject = g; - Handles.color = Color.yellow; - if (Cluster_Drawer.currentClusterView.selectedSynapseNeuron != null) { - foreach (Cluster sibling in Cluster_Drawer.currentClusterView.selectedSynapseNeuron.parent.instances) { - Neuron siblingNeuron = sibling.GetNeuron(Cluster_Drawer.currentClusterView.selectedSynapseNeuron.name); - Vector3 worldVector = gameObject.transform.TransformVector(siblingNeuron.outputValue); - Handles.DrawLine(gameObject.transform.position, gameObject.transform.position + worldVector); - } - } - else { - if (Cluster_Drawer.currentClusterView.currentNucleus is Neuron currentNeuron) { - Vector3 worldVector = gameObject.transform.TransformVector(currentNeuron.outputValue); - Handles.DrawLine(gameObject.transform.position, gameObject.transform.position + worldVector); - } - } + // Handles.color = Color.yellow; + // if (Cluster_Drawer.currentClusterView.selectedSynapseNeuron != null) { + // foreach (Cluster sibling in Cluster_Drawer.currentClusterView.selectedSynapseNeuron.parent.instances) { + // Neuron siblingNeuron = sibling.GetNeuron(Cluster_Drawer.currentClusterView.selectedSynapseNeuron.name); + // Vector3 worldVector = gameObject.transform.TransformVector(siblingNeuron.outputValue); + // Handles.DrawLine(gameObject.transform.position, gameObject.transform.position + worldVector); + // } + // } + // else { + // if (Cluster_Drawer.currentClusterView.currentNucleus is Neuron currentNeuron) { + // Vector3 worldVector = gameObject.transform.TransformVector(currentNeuron.outputValue); + // Handles.DrawLine(gameObject.transform.position, gameObject.transform.position + worldVector); + // } + // } } } diff --git a/Runtime/Scripts/Core/Neuron.cs b/Runtime/Scripts/Core/Neuron.cs index 82b3141..c8a74e3 100644 --- a/Runtime/Scripts/Core/Neuron.cs +++ b/Runtime/Scripts/Core/Neuron.cs @@ -6,7 +6,8 @@ using Unity.Mathematics; using static Unity.Mathematics.math; #endif -namespace NanoBrain { +namespace NanoBrain +{ /// /// A neuron is a basic Nucleus @@ -21,17 +22,20 @@ namespace NanoBrain { /// Each connection has a weight which is used to multiply the output of that other neuron /// before it is used by the combinator. [Serializable] - public class Neuron : Nucleus { + public class Neuron : Nucleus + { /// /// Create a new Neuron in a Cluster instance /// /// The parent cluster in which the new Neuron should be created /// The name of the new Neuron - public Neuron(Cluster parent, string name) { + public Neuron(Cluster parent, string name) + { this.parent = parent; this.name = name; - if (this.parent != null) { + if (this.parent != null) + { this.parent.nuclei ??= new(); this.parent.nuclei.Add(this); } @@ -63,7 +67,8 @@ namespace NanoBrain { /// The weight applied to the input. Default value = 1 /// The created Synapse /// This will add a new input to this nucleus with the given weight. - public Synapse AddSynapse(Neuron sendingNucleus, float weight = 1) { + public Synapse AddSynapse(Neuron sendingNucleus, float weight = 1) + { Synapse synapse = new(sendingNucleus, weight); this.synapses.Add(synapse); return synapse; @@ -74,7 +79,8 @@ namespace NanoBrain { /// /// The sender of the input to the Synapse /// The found Synapse or null when the sender has no synapse to this nucleus. - public Synapse GetSynapse(Nucleus sender) { + public Synapse GetSynapse(Nucleus sender) + { foreach (Synapse synapse in this.synapses) if (synapse.neuron == sender) return synapse; @@ -85,7 +91,8 @@ namespace NanoBrain { /// Remove a synapse from a Nucleus /// /// Remote the synapse connecting to this Nucleus - public void RemoveSynapse(Nucleus sendingNucleus) { + public void RemoveSynapse(Nucleus sendingNucleus) + { this.synapses.RemoveAll(synapse => synapse.neuron == sendingNucleus); } @@ -95,7 +102,8 @@ namespace NanoBrain { /// Set the bias, recalculate the output and update all Nuclei receiving from this Nucleus /// /// - public virtual void SetBias(Vector3 inputValue) { + public virtual void SetBias(Vector3 inputValue) + { this.bias = inputValue; this.lastUpdate = Time.time; this.parent?.UpdateFromNucleus(this); @@ -105,7 +113,8 @@ namespace NanoBrain { /// The type of combinators /// /// A combinator combines the weighted values of the synapses to a single value - public enum CombinatorType { + public enum CombinatorType + { /// Add the weighted values together Sum, /// Multiply the weighted values @@ -120,7 +129,8 @@ namespace NanoBrain { /// /// The type of /// - public enum ActivationType { + public enum ActivationType + { Linear, Power, Sqrt, @@ -139,9 +149,11 @@ namespace NanoBrain { /// /// The activation funtion /// - public ActivationType activator { + public ActivationType activator + { get { return _activator; } - set { + set + { _activator = value; //this.curve = GenerateCurve(); } @@ -159,9 +171,11 @@ namespace NanoBrain { /// /// The output value of the neuron /// - public virtual float3 outputValue { + public virtual float3 outputValue + { get { return _outputValue; } - set { + set + { _outputValue = value; if (this.isFiring) WhenFiring?.Invoke(); @@ -224,8 +238,10 @@ namespace NanoBrain { /// Check if the neuron is sleeping. /// /// This will reset the output value if it is sleeping - public void SleepCheck() { - if (this.isSleeping && this.outputSqrMagnitude > 0) { + public void SleepCheck() + { + if (this.isSleeping && this.outputSqrMagnitude > 0) + { #if UNITY_MATHEMATICS this._outputValue = new float3(0, 0, 0); #else @@ -251,8 +267,10 @@ namespace NanoBrain { public bool breakOnUpdate = false; /// \copydoc NanoBrain::Nucleus::ShallowCloneTo - public override Nucleus ShallowCloneTo(Cluster parent) { - Neuron clone = new(parent, this.name) { + public override Nucleus ShallowCloneTo(Cluster parent) + { + Neuron clone = new(parent, this.name) + { // prefabNucleus = this }; CloneFields(clone); @@ -263,7 +281,8 @@ namespace NanoBrain { /// Copy relevant fields of this neuron to the given neuron /// /// - protected virtual void CloneFields(Neuron clone) { + protected virtual void CloneFields(Neuron clone) + { clone.bias = this.bias; clone.persistOutput = this.persistOutput; clone.combinator = this.combinator; @@ -275,34 +294,45 @@ namespace NanoBrain { /// Delete the give neuron /// /// The neuron to delete - public static void Delete(Nucleus nucleus) { + public static void Delete(Nucleus nucleus) + { if (nucleus == null) return; - if (nucleus is Neuron neuron) { - foreach (Synapse synapse in neuron.synapses) { - if (synapse.neuron is Neuron synapse_nucleus) { - if (synapse_nucleus.receivers.Count > 1) { + if (nucleus is Neuron neuron) + { + foreach (Synapse synapse in neuron.synapses) + { + if (synapse.neuron is Neuron synapse_nucleus) + { + if (synapse_nucleus.receivers.Count > 1) + { // there is another nucleus feeding into this input nucleus synapse_nucleus.receivers.RemoveAll(r => r == nucleus); } - else { + else + { // No other links, delete it. Neuron.Delete(synapse_nucleus); } } } - foreach (Nucleus receiver in neuron.receivers) { + foreach (Nucleus receiver in neuron.receivers) + { if (receiver is not Neuron receiverNeuron) continue; if (receiver != null && receiverNeuron.synapses != null) receiverNeuron.synapses.RemoveAll(s => s.neuron == nucleus); } } - else if (nucleus is Cluster cluster) { + else if (nucleus is Cluster cluster) + { // remove all receivers for this cluster - foreach (Nucleus clusterNucleus in cluster.nuclei) { - if (clusterNucleus is Neuron output) { - foreach (Nucleus receiver in output.receivers) { + foreach (Nucleus clusterNucleus in cluster.nuclei) + { + if (clusterNucleus is Neuron output) + { + foreach (Nucleus receiver in output.receivers) + { if (receiver is not Neuron receiverNeuron) continue; receiverNeuron.synapses.RemoveAll(s => s.neuron == output); @@ -311,15 +341,18 @@ namespace NanoBrain { } } - if (nucleus.parent.prefab != null) { + if (nucleus.parent.prefab != null) + { nucleus.parent.nuclei.RemoveAll(n => n == nucleus); nucleus.parent.RefreshOutputs(); } } /// \copydoc NanoBrain::Nucleus::UpdateStateIsolated - public override void UpdateStateIsolated() { - if (breakOnUpdate) { + public override void UpdateStateIsolated() + { + if (breakOnUpdate) + { Debug.Break(); } var combination = Combinator(this.bias, this.synapses); @@ -337,8 +370,10 @@ namespace NanoBrain { /// The bias of the neuron /// The synapses of the neuron /// - protected float3 Combinator(float3 bias, List synapses) { - switch (combinator) { + protected float3 Combinator(float3 bias, List synapses) + { + switch (combinator) + { case CombinatorType.Sum: return CombinatorSum(bias, synapses); case CombinatorType.Product: @@ -354,9 +389,11 @@ namespace NanoBrain { /// The bias of the neuron /// The synapses of the neuron /// - public static float3 CombinatorSum(float3 bias, List synapses) { + public static float3 CombinatorSum(float3 bias, List synapses) + { float3 sum = bias; - foreach (Synapse synapse in synapses) { + foreach (Synapse synapse in synapses) + { synapse.neuron.SleepCheck(); sum += synapse.weight * synapse.neuron.outputValue; } @@ -369,9 +406,11 @@ namespace NanoBrain { /// The bias of the neuron /// The synapses of the neuron /// The result of the multiplication - public static float3 CombinatorProduct(float3 bias, List synapses) { + public static float3 CombinatorProduct(float3 bias, List synapses) + { float3 product = bias; - foreach (Synapse synapse in synapses) { + foreach (Synapse synapse in synapses) + { synapse.neuron.SleepCheck(); product *= synapse.weight * synapse.neuron.outputValue; } @@ -437,8 +476,10 @@ namespace NanoBrain { /// /// The result of applying the activation function // This does not allocate memory and seems faster than a switch expression - protected float3 Activator(float3 inputValue) { - switch (activator) { + protected float3 Activator(float3 inputValue) + { + switch (activator) + { case ActivationType.Linear: return ActivatorLinear(inputValue); case ActivationType.Sqrt: @@ -463,7 +504,8 @@ namespace NanoBrain { /// /// Input value /// The unchanged value - protected float3 ActivatorLinear(float3 input) { + protected float3 ActivatorLinear(float3 input) + { return input; } @@ -472,7 +514,8 @@ namespace NanoBrain { /// /// Input value /// The square root of the input - protected float3 ActivatorSqrt(float3 input) { + protected float3 ActivatorSqrt(float3 input) + { float3 result = normalize(input) * MathF.Sqrt(length(input)); return result; } @@ -482,7 +525,8 @@ namespace NanoBrain { /// /// Input value /// The input to the power of 2 - protected float3 ActivatorPower(float3 input) { + protected float3 ActivatorPower(float3 input) + { float3 result = normalize(input) * MathF.Pow(length(input), 2); return result; } @@ -492,7 +536,8 @@ namespace NanoBrain { /// /// Input value /// 1/input value - protected float3 ActivatorReciprocal(float3 input) { + protected float3 ActivatorReciprocal(float3 input) + { float magnitude = length(input); if (magnitude == 0) return new float3(0, 0, 0); @@ -506,7 +551,8 @@ namespace NanoBrain { /// /// Input value /// Tanh(input value) - protected float3 ActivatorTanh(float3 input) { + protected float3 ActivatorTanh(float3 input) + { float magnitude = length(input); float3 result = normalize(input) * MathF.Tanh(magnitude); return result; @@ -516,7 +562,8 @@ namespace NanoBrain { /// /// Input value /// An uniform vector with magnitude between 0 and 1 - protected float3 ActivatorBinary(float3 input) { + protected float3 ActivatorBinary(float3 input) + { float magnitude = length(input); float value = Mathf.Clamp01(magnitude); return float3(value, value, value); @@ -527,7 +574,8 @@ namespace NanoBrain { /// /// Input value /// The normalized vector - protected float3 ActivatorNormalized(float3 input) { + protected float3 ActivatorNormalized(float3 input) + { if (lengthsq(input) == 0) return input; float3 result = normalize(input); @@ -613,7 +661,8 @@ namespace NanoBrain { /// /// The nuclei which have a synapse to this neuron /// - public virtual List receivers { + public virtual List receivers + { get { return _receivers; } set { _receivers = value; } } @@ -623,7 +672,8 @@ namespace NanoBrain { /// /// The receiver to add /// The weight to use for the synapse to his neuron - public virtual void AddReceiver(Nucleus receiverToAdd, float weight = 1) { + public virtual void AddReceiver(Nucleus receiverToAdd, float weight = 1) + { if (receiverToAdd is not Neuron receiverNeuron) return; this._receivers.Add(receiverNeuron); @@ -636,7 +686,8 @@ namespace NanoBrain { /// Remove a receiver to this neuron /// /// The receiver to remove - public virtual void RemoveReceiver(Nucleus receiverToRemove) { + public virtual void RemoveReceiver(Nucleus receiverToRemove) + { if (receiverToRemove is not Neuron receiverNeuron) return; this._receivers.RemoveAll(receiver => receiver == receiverNeuron); @@ -652,11 +703,37 @@ namespace NanoBrain { #endregion Receivers + #region Back propagation + + public void BackPropagation(Synapse synapse, Vector3 error, float learningRate) + { + // Loss function: + // Mean Squared Error (MSE) 1/n * sum(errors^2) + // We use simplified here 1/2 * (error^2) + // For vectors, we need to use MSE component wise. + Vector3 loss = 0.5f * Vector3.Scale(error, error); + + // loss is a derivative of error + // Backpropagation = loss * d(combinator) + + // Assuming linear activation function. + // Derivative of this (f'()) would be 1. + Vector3 delta2 = loss * 1; + + Vector3 scaledOutput = Vector3.Scale(delta2, synapse.neuron.outputValue); + float deltaWeight = Mathf.Abs(scaledOutput.x) + Mathf.Abs(scaledOutput.y) + Mathf.Abs(scaledOutput.z); + synapse.weight += learningRate * deltaWeight; + Debug.Log($"Updated weight: {error.magnitude} {error} {scaledOutput} {synapse.weight}"); + } + + #endregion Back propagation + /// /// Process an external stimulus /// /// The value of the stimulus - public virtual void ProcessStimulus(Vector3 inputValue) { + public virtual void ProcessStimulus(Vector3 inputValue) + { this.lastUpdate = Time.time; this.bias = inputValue; this.parent?.UpdateFromNucleus(this);