Neuromodulatory Signals as Dynamic Hyperparameters
Dopamine, norepinephrine, serotonin, and acetylcholine implemented as runtime scalars that change network behaviour without retraining.
Hyperparameters like learning rate and dropout rate are fixed at training time. But the brain adjusts analogous parameters in real time based on context: arousal, reward, uncertainty, and fatigue all shift how information is processed. Implementing neuromodulatory signals as runtime scalars lets you do the same.
The Four Signals
In Neural Nexus, a singleton NeuromodulatorBus holds four named float signals:
class NeuromodulatorBus:
def __init__(self):
self._signals: dict[str, float] = {
"dopamine": 0.0,
"norepinephrine": 0.0,
"serotonin": 0.0,
"acetylcholine": 0.0,
}
def set(self, name: str, value: float):
self._signals[name] = float(np.clip(value, -1.0, 1.0))
def get(self, name: str) -> float:
return self._signals.get(name, 0.0)
NEURO_BUS = NeuromodulatorBus()All values are in [-1, 1]. Zero is neutral. Positive values amplify the signal's effect; negative values suppress it.
Dopamine: Gating Memory Writes
Dopamine in neuroscience signals reward prediction and modulates whether experiences are worth encoding. In implementation, it gates how aggressively EpisodicStore writes new memories:
class EpisodicStore:
def write(self, key, value):
da = NEURO_BUS.get("dopamine")
# Gate: high dopamine → more likely to write
write_prob = sigmoid(3.0 + da * 4.0)
if np.random.rand() < write_prob:
slot = self.ptr % self.num_slots
self.keys[slot] = key
self.values[slot] = value
self.ptr += 1At dopamine = 0.0, write probability ≈ 0.95 (almost always writes). At dopamine = -1.0, probability ≈ 0.27 (filters to only strong experiences). At dopamine = 1.0, always writes.
Norepinephrine: Attention Focus
Norepinephrine corresponds to arousal and signal-to-noise ratio. In WorkingMemory, it sharpens or softens the attention distribution:
def read(self, query: np.ndarray) -> np.ndarray:
ne = NEURO_BUS.get("norepinephrine")
q = query @ self.W_q
k = self.buffer @ self.W_k
# NE scales the temperature of attention
temperature = 1.0 / (1.0 + ne * 2.0)
scores = (q @ k.T) * temperature / np.sqrt(q.shape[-1])
weights = softmax(scores)
return weights @ self.bufferHigh norepinephrine (high arousal) → sharper attention → the system focuses more narrowly on the most relevant memory slot. Low norepinephrine → softer attention → more diffuse retrieval.
Serotonin: Modulating Reconsolidation and Risk
Serotonin affects EpisodicStore reconsolidation rate and ProceduralMemory routing temperature:
# In EpisodicStore.read():
def _reconsolidate(self, attended_idx, query):
ser = NEURO_BUS.get("serotonin")
# High serotonin → slower reconsolidation (more stable memories)
rate = self.recon_rate * (1.0 - ser * 0.5)
self.keys[attended_idx] += rate * (query - self.keys[attended_idx])
# In ProceduralMemory.forward():
def forward(self, x):
ser = NEURO_BUS.get("serotonin")
temp = 1.0 + ser * 2.0 # high serotonin → softer routing → more exploration
logits = x @ self.W_router / temp
return softmax(logits) @ self.programsAcetylcholine: Learning Gate
Acetylcholine modulates whether the network is in a "learning mode" or "performance mode":
def write(self, key, value):
ach = NEURO_BUS.get("acetylcholine")
# High ACh → lower threshold → more discriminative encoding
threshold = 0.3 - ach * 0.2
if write_score > threshold:
self._store(key, value)High acetylcholine corresponds to focused attention and active encoding — the equivalent of being alert and trying to learn something. Low acetylcholine is the default "habit execution" mode.
Updating Signals at Runtime
In CognitiveBridge, signals update via EMA after each turn based on computed cognitive state:
def _update_neuromodulators(self, state: CognitiveState):
da = NEURO_BUS.get("dopamine")
ne = NEURO_BUS.get("norepinephrine")
ser = NEURO_BUS.get("serotonin")
ach = NEURO_BUS.get("acetylcholine")
NEURO_BUS.set("dopamine", 0.9 * da + 0.1 * (1.0 - state.conflict))
NEURO_BUS.set("norepinephrine", 0.9 * ne + 0.1 * state.surprise)
NEURO_BUS.set("serotonin", 0.9 * ser + 0.1 * (1.0 - state.surprise))
NEURO_BUS.set("acetylcholine", 0.9 * ach + 0.1 * state.arousal)High conflict → lower dopamine (fewer memory writes). High surprise → higher norepinephrine (sharper focus), lower serotonin (faster reconsolidation, more routing variance). High arousal → higher acetylcholine (active learning mode).
Why This Is Useful
The big advantage of the neuromodulatory bus is that you can override signals externally for specific use cases:
# Before exposing to a high-stakes query: increase focus and learning
NEURO_BUS.set("norepinephrine", 0.8)
NEURO_BUS.set("acetylcholine", 0.7)
# After a "familiar" situation: reduce reconsolidation churn
NEURO_BUS.set("serotonin", 0.6)
# When the system is "bored": reduce memory gate threshold
NEURO_BUS.set("dopamine", -0.3)And in the dashboard's Neuro tab, you can manually fire any signal and immediately observe its effect on the next forward pass — which is the most direct way to develop intuition for what each signal actually does.