Current section

Files

Jump to
macula_tweann src plasticity_hebbian.erl
Raw

src/plasticity_hebbian.erl

%% @doc Basic Hebbian plasticity rule implementation.
%%
%% This module implements the classic Hebbian learning rule, often
%% summarized as "neurons that fire together wire together."
%%
%% == Theory ==
%%
%% Hebbian learning was proposed by Donald Hebb in 1949 as a model
%% for how neural connections are strengthened through experience.
%% The basic rule is:
%%
%% Δw_ij = η × pre_i × post_j
%%
%% Where:
%% - Δw_ij is the weight change from neuron i to j
%% - η (eta) is the learning rate
%% - pre_i is the presynaptic (input) activation
%% - post_j is the postsynaptic (output) activation
%%
%% == Variants Implemented ==
%%
%% 1. **Basic Hebbian** (default):
%% Δw = η × pre × post
%%
%% 2. **Bounded Hebbian** (with weight clamping):
%% Δw = η × pre × post, clamped to [-1, 1]
%%
%% 3. **Oja's Rule** (with normalization):
%% Δw = η × post × (pre - post × w)
%% This prevents unbounded weight growth.
%%
%% == Usage ==
%%
%% Weight = {0.5, 0.0, 0.01, []}, % Initial weight spec
%% PreActivity = 0.8,
%% PostActivity = 0.6,
%% Reward = 0.0, % Not used in basic Hebbian
%%
%% NewWeight = plasticity_hebbian:apply_rule(Weight, PreActivity, PostActivity, Reward).
%% %% => {0.5048, 0.0048, 0.01, []}
%%
%% == Configuration ==
%%
%% The learning rate is stored in the weight_spec tuple (3rd element).
%% Additional parameters can be stored in the parameter list (4th element):
%%
%% - `{bounded, Min, Max}` - Clamp weights to [Min, Max]
%% - `{oja, true}` - Use Oja's normalized rule
%% - `{decay, Rate}` - Apply weight decay: w' = w × (1 - decay)
%%
%% == References ==
%%
%% [1] Hebb, D.O. (1949). The Organization of Behavior. Wiley.
%% [2] Oja, E. (1982). "A simplified neuron model as a principal
%% component analyzer." Journal of Mathematical Biology.
%%
%% @copyright 2025 Macula.io
-module(plasticity_hebbian).
-behaviour(plasticity).
%% Behavior callbacks
-export([
apply_rule/4,
name/0,
description/0,
init/1,
reset/1
]).
%% Additional API
-export([
apply_bounded/5,
apply_oja/4,
apply_with_decay/5
]).
%% Types
-type weight_spec() :: plasticity:weight_spec().
%%==============================================================================
%% Behavior Callbacks
%%==============================================================================
%% @doc Return the rule name.
-spec name() -> atom().
name() -> hebbian.
%% @doc Return a description of this rule.
-spec description() -> binary().
description() ->
<<"Basic Hebbian learning: strengthens connections when pre and post fire together">>.
%% @doc Initialize any state for this rule.
%%
%% For basic Hebbian, no state is needed.
-spec init(map()) -> undefined.
init(_Params) ->
undefined.
%% @doc Reset the rule state.
-spec reset(term()) -> undefined.
reset(_State) ->
undefined.
%% @doc Apply the Hebbian learning rule to a weight.
%%
%% This is the main callback that updates a single weight based on
%% the activities of the pre and post neurons.
%%
%% @param Weight The weight specification tuple
%% @param PreActivity Presynaptic (input) activation
%% @param PostActivity Postsynaptic (output) activation
%% @param Reward Reward signal (unused in basic Hebbian)
%% @returns Updated weight specification
-spec apply_rule(weight_spec(), float(), float(), float()) -> weight_spec().
apply_rule({W, _DW, LR, Params}, PreActivity, PostActivity, _Reward) ->
%% Check for variant modes in parameters
case extract_variant(Params) of
oja ->
apply_oja_internal(W, LR, PreActivity, PostActivity, Params);
{bounded, Min, Max} ->
apply_bounded_internal(W, LR, PreActivity, PostActivity, Min, Max, Params);
basic ->
apply_basic(W, LR, PreActivity, PostActivity, Params)
end.
%%==============================================================================
%% Internal Implementation
%%==============================================================================
%% @private Apply basic Hebbian rule
apply_basic(W, LR, PreActivity, PostActivity, Params) ->
%% Δw = η × pre × post
Delta = LR * PreActivity * PostActivity,
%% Apply weight decay if configured
NewW = case proplists:get_value(decay, Params) of
undefined -> W + Delta;
DecayRate -> (W * (1.0 - DecayRate)) + Delta
end,
{NewW, Delta, LR, Params}.
%% @private Apply Oja's normalized Hebbian rule
apply_oja_internal(W, LR, PreActivity, PostActivity, Params) ->
%% Oja's rule: Δw = η × post × (pre - post × w)
%% This prevents unbounded weight growth by including a forgetting term
Delta = LR * PostActivity * (PreActivity - PostActivity * W),
NewW = W + Delta,
{NewW, Delta, LR, Params}.
%% @private Apply bounded Hebbian rule
apply_bounded_internal(W, LR, PreActivity, PostActivity, Min, Max, Params) ->
Delta = LR * PreActivity * PostActivity,
NewW = clamp(W + Delta, Min, Max),
{NewW, Delta, LR, Params}.
%% @private Extract variant configuration from params
extract_variant(Params) ->
case proplists:get_value(oja, Params) of
true -> oja;
_ ->
case proplists:get_value(bounded, Params) of
{Min, Max} -> {bounded, Min, Max};
_ -> basic
end
end.
%% @private Clamp value to range
clamp(X, Min, _Max) when X < Min -> Min;
clamp(X, _Min, Max) when X > Max -> Max;
clamp(X, _Min, _Max) -> X.
%%==============================================================================
%% Additional API Functions
%%==============================================================================
%% @doc Apply bounded Hebbian rule with explicit bounds.
%%
%% @param Weight Weight specification
%% @param PreActivity Presynaptic activity
%% @param PostActivity Postsynaptic activity
%% @param Min Minimum weight value
%% @param Max Maximum weight value
%% @returns Updated weight specification
-spec apply_bounded(weight_spec(), float(), float(), float(), float()) -> weight_spec().
apply_bounded({W, _DW, LR, Params}, PreActivity, PostActivity, Min, Max) ->
apply_bounded_internal(W, LR, PreActivity, PostActivity, Min, Max, Params).
%% @doc Apply Oja's normalized Hebbian rule.
%%
%% Oja's rule includes a "forgetting" term that prevents unbounded
%% weight growth, making it suitable for self-organizing maps and
%% principal component analysis.
%%
%% @param Weight Weight specification
%% @param PreActivity Presynaptic activity
%% @param PostActivity Postsynaptic activity
%% @returns Updated weight specification
-spec apply_oja(weight_spec(), float(), float(), float()) -> weight_spec().
apply_oja({W, _DW, LR, Params}, PreActivity, PostActivity, _Reward) ->
apply_oja_internal(W, LR, PreActivity, PostActivity, Params).
%% @doc Apply Hebbian with weight decay.
%%
%% Weight decay prevents weights from growing too large over time
%% by slightly reducing all weights each update.
%%
%% @param Weight Weight specification
%% @param PreActivity Presynaptic activity
%% @param PostActivity Postsynaptic activity
%% @param DecayRate Rate of weight decay (typically 0.001 to 0.01)
%% @returns Updated weight specification
-spec apply_with_decay(weight_spec(), float(), float(), float(), float()) -> weight_spec().
apply_with_decay({W, _DW, LR, Params}, PreActivity, PostActivity, _Reward, DecayRate) ->
Delta = LR * PreActivity * PostActivity,
NewW = (W * (1.0 - DecayRate)) + Delta,
{NewW, Delta, LR, Params}.