Moved back prop. to neuron

This commit is contained in:
Pascal Serrarens 2026-06-29 15:53:58 +02:00
parent 0dfc3bec97
commit 9258655c0b
2 changed files with 143 additions and 66 deletions

View File

@ -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);
// }
// }
}
}

View File

@ -6,7 +6,8 @@ using Unity.Mathematics;
using static Unity.Mathematics.math;
#endif
namespace NanoBrain {
namespace NanoBrain
{
/// <summary>
/// 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
{
/// <summary>
/// Create a new Neuron in a Cluster instance
/// </summary>
/// <param name="parent">The parent cluster in which the new Neuron should be created</param>
/// <param name="name">The name of the new Neuron</param>
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 {
/// <param name="weight">The weight applied to the input. Default value = 1</param>
/// <returns>The created Synapse</returns>
/// 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 {
/// </summary>
/// <param name="sender">The sender of the input to the Synapse</param>
/// <returns>The found Synapse or null when the sender has no synapse to this nucleus.</returns>
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
/// </summary>
/// <param name="sendingNucleus">Remote the synapse connecting to this Nucleus</param>
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
/// </summary>
/// <param name="inputValue"></param>
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
/// </summary>
/// 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 {
/// <summary>
/// The type of
/// </summary>
public enum ActivationType {
public enum ActivationType
{
Linear,
Power,
Sqrt,
@ -139,9 +149,11 @@ namespace NanoBrain {
/// <summary>
/// The activation funtion
/// </summary>
public ActivationType activator {
public ActivationType activator
{
get { return _activator; }
set {
set
{
_activator = value;
//this.curve = GenerateCurve();
}
@ -159,9 +171,11 @@ namespace NanoBrain {
/// <summary>
/// The output value of the neuron
/// </summary>
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.
/// </summary>
/// 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
/// </summary>
/// <param name="clone"></param>
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
/// </summary>
/// <param name="nucleus">The neuron to delete</param>
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 {
/// <param name="bias">The bias of the neuron</param>
/// <param name="synapses">The synapses of the neuron</param>
/// <returns></returns>
protected float3 Combinator(float3 bias, List<Synapse> synapses) {
switch (combinator) {
protected float3 Combinator(float3 bias, List<Synapse> synapses)
{
switch (combinator)
{
case CombinatorType.Sum:
return CombinatorSum(bias, synapses);
case CombinatorType.Product:
@ -354,9 +389,11 @@ namespace NanoBrain {
/// <param name="bias">The bias of the neuron</param>
/// <param name="synapses">The synapses of the neuron</param>
/// <returns></returns>
public static float3 CombinatorSum(float3 bias, List<Synapse> synapses) {
public static float3 CombinatorSum(float3 bias, List<Synapse> 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 {
/// <param name="bias">The bias of the neuron</param>
/// <param name="synapses">The synapses of the neuron</param>
/// <returns>The result of the multiplication</returns>
public static float3 CombinatorProduct(float3 bias, List<Synapse> synapses) {
public static float3 CombinatorProduct(float3 bias, List<Synapse> 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 {
/// <param name="inputValue"></param>
/// <returns>The result of applying the activation function</returns>
// 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 {
/// </summary>
/// <param name="input">Input value</param>
/// <returns>The unchanged value</returns>
protected float3 ActivatorLinear(float3 input) {
protected float3 ActivatorLinear(float3 input)
{
return input;
}
@ -472,7 +514,8 @@ namespace NanoBrain {
/// </summary>
/// <param name="input">Input value</param>
/// <returns>The square root of the input</returns>
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 {
/// </summary>
/// <param name="input">Input value</param>
/// <returns>The input to the power of 2</returns>
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 {
/// </summary>
/// <param name="input">Input value</param>
/// <returns>1/input value</returns>
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 {
/// </summary>
/// <param name="input">Input value</param>
/// <returns>Tanh(input value)</returns>
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 {
/// </summary>
/// <param name="input">Input value</param>
/// <returns>An uniform vector with magnitude between 0 and 1</returns>
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 {
/// </summary>
/// <param name="input">Input value</param>
/// <returns>The normalized vector</returns>
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 {
/// <summary>
/// The nuclei which have a synapse to this neuron
/// </summary>
public virtual List<Nucleus> receivers {
public virtual List<Nucleus> receivers
{
get { return _receivers; }
set { _receivers = value; }
}
@ -623,7 +672,8 @@ namespace NanoBrain {
/// </summary>
/// <param name="receiverToAdd">The receiver to add</param>
/// <param name="weight">The weight to use for the synapse to his neuron</param>
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
/// </summary>
/// <param name="receiverToRemove">The receiver to remove</param>
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
/// <summary>
/// Process an external stimulus
/// </summary>
/// <param name="inputValue">The value of the stimulus</param>
public virtual void ProcessStimulus(Vector3 inputValue) {
public virtual void ProcessStimulus(Vector3 inputValue)
{
this.lastUpdate = Time.time;
this.bias = inputValue;
this.parent?.UpdateFromNucleus(this);