Hopfield network
A Hopfield network that stores patterns with Hebb's rule and recalls the first from a corrupted copy.
- Name:
hopfield - Field:
computer_science/neural_networks - Formalism: agent-based model
- References: Hopfield 1982; Trzaska 2023
Factory
@register_model(
field='computer_science/neural_networks',
tags=('neural network', 'associative memory', 'agents', 'attractor'),
references=('Hopfield 1982', 'Trzaska 2023'),
defaults={'until': 10},
)
def hopfield(
patterns: npt.ArrayLike | None = None,
*,
n_neurons: int = 64,
n_patterns: int = 3,
noise: float = 0.2,
random_state: int | np.random.Generator | None = 0,
) -> System:
"""Return a Hopfield network that stores patterns with Hebb's rule and recalls the first from a corrupted copy.
Neurons update one at a time in random order, so the energy `-Σ w_ij s_i s_j / 2` never increases and the network
settles in a stored pattern when few patterns are stored, about fewer than `0.14 n` for `n` neurons.
Args:
patterns:
The stored patterns of -1 and 1, one row per pattern, or `None` for random patterns.
n_neurons:
The number of neurons of random patterns.
n_patterns:
The number of random patterns.
noise:
The probability that a neuron starts flipped from the first pattern.
random_state:
The seed or generator of the random patterns and of the corruption.
Returns:
system:
The model, with the group `neurons` of states `spin` and `weights` on a complete graph.
Examples:
>>> from skcomplex.models.computer_science.neural_networks import hopfield
>>> hopfield(n_neurons=16, n_patterns=2).elements[0].check().prepare().size_
16
"""
rng = np.random.default_rng(random_state)
stored: Array = np.asarray(
rng.choice([-1.0, 1.0], size=(n_patterns, n_neurons)) if patterns is None else patterns,
dtype=np.float64,
)
size = stored.shape[1]
spins = np.where(rng.random(size) < noise, -stored[0], stored[0])
neurons = GroupElement(
'neurons',
element=NeuronElement,
size=size,
values={'spin': spins, 'weights': _hebbian_weights(stored)},
)
return System(
elements=[neurons],
interactions=[HopfieldUpdateInteraction('update', target='neurons')],
space=NetworkSpace(nx.complete_graph(size)),
)
Components
class NeuronElement(Element):
"""Binary neuron of a Hopfield network, which the update aligns with the sign of its weighted input.
The neuron sits on its node of a complete graph and holds the weights of its incoming links, one per node.
Args:
label:
The label of the agent or group.
visual:
How the element is drawn, or `None` for `default_visual_`.
spin:
The initial state, -1 or 1.
weights:
The weights of the links from every node, an array with one value per node.
Examples:
>>> from skcomplex.models.computer_science.neural_networks import NeuronElement
>>> group = GroupElement('neurons', element=NeuronElement, size=4, values={'spin': 1.0})
>>> group.label, group.element.__name__, group.size
('neurons', 'NeuronElement', 4)
"""
def __init__(
self,
label: str,
visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'spin', 'color_range': (-1.0, 1.0)}),
*,
spin: Annotated[float, StateSpec(domain=ZZ)] = 1.0,
weights: Annotated[object, StateSpec()] = 0.0,
) -> None:
super().__init__(label, visual=visual)
self.spin = spin
self.weights = weights
@classmethod
def _get_view(cls) -> type[NeuronView]:
"""Return the view of the neurons, which types their states."""
return NeuronView
class HopfieldUpdateInteraction(MapInteraction):
"""Asynchronous update of a Hopfield network, where every neuron in turn takes the sign of its weighted input.
The neurons update one after the other in a new random order every step, each reading the states the neurons before
it set, so the energy `-sum w_ij s_i s_j / 2` never increases. A neuron whose input is zero keeps its state.
Args:
label:
The label of the interaction.
target:
The path of the group of neurons.
Examples:
>>> from skcomplex.models.computer_science.neural_networks import HopfieldUpdateInteraction
>>> HopfieldUpdateInteraction('update', target='neurons').check().schedule
'random-sequential'
"""
def __init__(self, label: str, *, target: Annotated[str, RoleSpec()]) -> None:
super().__init__(
label,
target=target,
state='spin',
function=self._get_spin,
schedule='random-sequential',
form='agent',
)
def _get_spin(self, agent: NeuronView, model: ModelViewProtocol) -> float:
"""Return the sign of the weighted input of a neuron from the other neurons, or its state for a zero input."""
del model
weights = np.asarray(agent.weights)
neighbors = [other for other in agent.neighbors() if isinstance(other, NeuronView)]
field = sum(weights[int(np.asarray(other.location))] * other.spin for other in neighbors)
return float(np.sign(field)) if field != 0 else float(agent.spin)