Back to Blog
Dec 8, 20254 min readOnuzulike Anthony

Neuromodulatory Signals as Dynamic Hyperparameters

Dopamine, norepinephrine, serotonin, and acetylcholine implemented as runtime scalars that change network behaviour without retraining.

AI/MLNeuromodulationNeural NetworksCognitive ArchitectureAI/ML

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:

python
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:

python
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 += 1

At 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:

python
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.buffer

High 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:

python
# 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.programs

Acetylcholine: Learning Gate

Acetylcholine modulates whether the network is in a "learning mode" or "performance mode":

python
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:

python
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:

python
# 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.