Create your own Node
#
Subclassing the Node
to create a custom operator takes only a
few steps to be done and operational. Subclasses of Node
can
then be used as any other node instances.
Create a forward
function#
First, one needs to create the forward function that will be applied by the new node class:
In [1]: import numpy as np
In [2]: from reservoirpy import Node
In [3]: def forward(node: Node, x: np.ndarray) -> np.ndarray:
...: """Does something to the current state of the node, the input
...: data and some feedback."""
...: state = node.state() # get current node state
...: some_param = node.const1
...: some_other_param = node.const2
...: if node.has_feedback:
...: feedback = node.feedback() # call state of some distant node
...: return x + some_param * state + some_other_param * feedback
...: else:
...: return x + some_param * state
...:
This function must take as parameter a vector x of shape
(1, dim x)
(one timestep of data) and the node instance itself. You can
access any parameter stored in the node through this instance.
Create an initialize
function#
Then, one needs to create the initialize function that will be used at runtime to infer the input and output dimensions of the node, and optionally initialize some parameters (some neuronal weights, for instance):
In [4]: def initialize(node: Node, x: np.ndarray = None, y: np.ndarray = None):
...: """This function receives a data point x at runtime and uses it to
...: infer input and output dimensions.
...: """
...: if x is not None:
...: node.set_input_dim(x.shape[1])
...: node.set_output_dim(x.shape[1])
...: node.set_param("const1", 1)
...:
Initialize feedback connections#
Additionally, another function can be created to initialize feedback signal dimension, if the node requires feedback:
In [5]: def initialize_fb(node: Node, feedback=None):
...: """This function is called at runtime and
...: infer feedback dimensions.
...: """
...: if node.has_feedback:
...: if feedback is not None:
...: node.set_feedback_dim(feedback.shape[1])
...:
Finally, you can add some other functions to train the parameter of your node. See .. TODO: add link to train page for more information.
Instantiate a new Node
#
That’s it! You can now create a new Node
instance
parametrized with the functions you have just written:
In [6]: node = Node(
...: forward=forward,
...: initializer=initialize,
...: fb_initializer=initialize_fb,
...: params={"const1": None},
...: hypers={"const2": -1},
...: name="custom_node",
...: )
...:
Note
Do not forget to declare the mutable parameters params and immutable hyperparameters hypers as dictionaries. params should store all parameters that need to be initialized and that will evolve during the life cycle of the node (for example, neuronal weights whom value will change during training). hypers should store parameters used to define the architecture or the behavior of the node instance, and that will not change through learning mechanisms.
Subclassing Node
#
You can also create a new subclass of Node
in a similar way:
In [7]: class CustomNode(Node):
...: def __init__(self, const2=-1, name=None):
...: super().__init__(
...: forward=forward,
...: initializer=initialize,
...: fb_initializer=initialize_fb,
...: params={"const1": None},
...: hypers={"const2": const2},
...: name=name,
...: )
...:
In [8]: node = CustomNode(const2=-1, name="custom_node")
This allow more flexibility, as you can redefine the complete behavior of
the node in the subclass. Be careful to expose the name parameter in the
subclass __init__
, and to pass it to the base class as parameter.
It is a good practice to find meaningful names for your node instances.
Warning
All Node instances names must be unique! ReservoirPy will raise an exception if it is not the case. All node classes generate their own unique default names though.