Creating NN graphs from scratch using Aidge#
A Neural Network is in fact a special type of computation graph, with associated data, forward and backward operators per node. In Aidge, it is very easy to create your own graph from scratch. Let’s understand it step by step in this tutorial.
Install requirements#
[1]:
#!pip install aidge-core
Introduction#
Aidge is a collaborative open source deep learning library optimized for exporting and processing deep learning algorithms on embedded devices. With Aidge, one can create a computational graph representing the neural network, apply modifications to its structure, train it, and finally run it using different backends or export it to different targets and formats.
Import Aidge and other modules#
Aidge is built around a core library that interfaces with multiple modules bound to Python libraries. For this tutorial, we only need:
Necessary Aidge modules:
aidge_core– the core library that offers all the basic functionalities for creating and manipulating the internal computational graph representation.
Other necessary modules:
numpyto manipulate data.
[2]:
import numpy as np
import aidge_core as ai
My very first sequential graph#
Suppose the constants a and b, and the input x. You want to find the answer y for the equation:
y = max(0, (x + a) * b)
Decomposing the functional definition we have:
s = (x + a)
m = (s * b)
r = max(0, m)
y = r
Note that this max operation corresponds to the ReLu function.
Let’s create this linear computation graph:
x -> Add -> Mul -> ReLU -> y
[3]:
# create the nodes
s = ai.Add("s")
m = ai.Mul("m")
r = ai.ReLU("r")
# create the graph linking the nodes with special operators
graph = s >> m >> r
graph.set_name("my_graph")
# show the graph
graph.print()
r (ReLU)
└ m (Mul)
├ s (Add)
| ├ <input>
| └ <input>
└ <input>
Let’s see the classes of each object, inputs and outputs of the graph
[4]:
print("- graph:", graph)
print("- s:", s)
print("- m:", m)
print("- r:", r)
print(
"- graph inputs:",
[(node.name(), idx) for (node, idx) in graph.get_ordered_inputs()],
)
print(
"- graph outputs:",
[(node.name(), idx) for (node, idx) in graph.get_ordered_outputs()],
)
- graph: GraphView(name='my_graph', Nodes: 3 (inputs: 3, outputs: 1))
- s: Node(name='s', optype='Add', parents: [0, 0], children: [[1]])
- m: Node(name='m', optype='Mul', parents: [1, 0], children: [[1]])
- r: Node(name='r', optype='ReLU', parents: [1], children: [[]])
- graph inputs: [('s', 0), ('s', 1), ('m', 1)]
- graph outputs: [('r', 0)]
Ok, the graph seems correct, but the constants are lacking.
Let’s create the constants as producers, in link them to the graph.
[5]:
a_value = 3.0
b_value = 7.0
a = ai.Producer(ai.Tensor(np.array([a_value])), "a")
b = ai.Producer(ai.Tensor(np.array([b_value])), "b")
# this operations link the constants (producer nodes) to the first free (non-connected) input ports of the indicated following nodes,
a >> s
b >> m
# include the precedent nodes into the graph
graph.push(a, b)
print(graph)
graph.print()
GraphView(name='my_graph', Nodes: 5 (inputs: 1, outputs: 1))
r (ReLU)
└ m (Mul)
├ s (Add)
| ├ a (Producer)
| └ <input>
└ b (Producer)
[6]:
print("- graph:", graph)
print()
print("- a:", a)
print("- b:", b)
print("- s:", s)
print("- m:", m)
print("- r:", r)
print()
print(
"- graph inputs:",
[(node.name(), idx) for (node, idx) in graph.get_ordered_inputs()],
)
print(
"- graph outputs:",
[(node.name(), idx) for (node, idx) in graph.get_ordered_outputs()],
)
- graph: GraphView(name='my_graph', Nodes: 5 (inputs: 1, outputs: 1))
- a: Node(name='a', optype='Producer', children: [[1]])
- b: Node(name='b', optype='Producer', children: [[1]])
- s: Node(name='s', optype='Add', parents: [1, 0], children: [[1]])
- m: Node(name='m', optype='Mul', parents: [1, 1], children: [[1]])
- r: Node(name='r', optype='ReLU', parents: [1], children: [[]])
- graph inputs: [('s', 1)]
- graph outputs: [('r', 0)]
Let’s execute the graph using backend cpu#
[7]:
import aidge_backend_cpu as aibkcpu
x_value = 5.0
x_np = np.array(x_value, dtype=np.float32)
x_ai = ai.Tensor(x_np)
graph.set_backend("cpu")
graph.forward_dims([[1]])
graph.set_datatype(ai.dtype.float32)
# Create scheduler
scheduler = ai.SequentialScheduler(graph)
# Run inference!
scheduler.forward(data=[x_ai])
[7]:
[Tensor([ 56.00000], dims = [1], dtype = float32)]
[8]:
# verify result:
a_value = 3.0
b_value = 7.0
print(max(0, (x_value + a_value) * b_value))
56.0
There are different manners to create your graph.#
Let’s do it again, in a single script.
1a) using sequential >> and parallel | operators:#
[9]:
# create the nodes
a = ai.Producer(ai.Tensor(np.array([3.0])), "a")
b = ai.Producer(ai.Tensor(np.array([7.0])), "b")
s = ai.Add("s")
m = ai.Mul("m")
r = ai.ReLU("r")
# create the graph linking the nodes with special operators
graph = ((a >> s) | b) >> m >> r
graph.set_name("my_graph")
# show the graph
graph.print()
r (ReLU)
└ m (Mul)
├ s (Add)
| ├ a (Producer)
| └ <input>
└ b (Producer)
1b) using sequential and parallel facilities#
[10]:
# create nodes
a = ai.Producer(ai.Tensor(np.array([3.0])), "a")
b = ai.Producer(ai.Tensor(np.array([7.0])), "b")
s = ai.Add("s")
m = ai.Mul("m")
r = ai.ReLU("r")
# create the graph
g = ai.sequential([ai.parallel([ai.sequential([a, s]), b]), m, r])
print(g)
g.print()
GraphView(name='', Nodes: 5 (inputs: 1, outputs: 1))
r (ReLU)
└ m (Mul)
├ s (Add)
| ├ a (Producer)
| └ <input>
└ b (Producer)
2a) using GraphView.add and Node.add_child#
[11]:
# create nodes
c = ai.Producer(ai.Tensor(np.array([3.0])), "c")
s = ai.Add("s")
z = ai.ReLU("z")
# connect nodes
c.add_child(s, 0, 1) # must indicate the output index for s, and the input index for c
s.add_child(z) # default is using the first free inputs/outputs
# create graph
g = ai.GraphView()
g.push(c, s, z)
print(g)
g.print()
GraphView(name='', Nodes: 3 (inputs: 1, outputs: 1))
z (ReLU)
└ s (Add)
├ <input>
└ c (Producer)
2b) using get_connected_graph_view facility#
[12]:
# create nodes
c = ai.Producer(ai.Tensor(np.array([3.0])), "c")
s = ai.Add("s")
z = ai.ReLU("z")
# connect nodes
c.add_child(s, 0, 1) # must indicate the output index for s, and the input index for c
s.add_child(z) # default is using the first free inputs/outputs
# create graph
g = ai.get_connected_graph_view(c) # or z or s
print(g)
g.print()
GraphView(name='', Nodes: 3 (inputs: 1, outputs: 1))
z (ReLU)
└ s (Add)
├ <input>
└ c (Producer)
3) using chained link_to facility#
[13]:
# create nodes
c = ai.Producer(ai.Tensor(np.array([3.0])), "c")
s = ai.Add("s")
z = ai.ReLU("z")
g = ai.get_connected_graph_view(c.link_to(s, 0, 1).link_to(z))
print(g)
g.print()
GraphView(name='', Nodes: 3 (inputs: 1, outputs: 1))
z (ReLU)
└ s (Add)
├ <input>
└ c (Producer)
In the future, using Input and Output special Nodes.
[14]:
# create nodes
x = ai.Input("x")
c = ai.Producer(ai.Tensor(3.0), "c")
s = ai.Add("s")
z = ai.ReLU("z")
y = ai.Output("y")
# create graph
g = (x | c) >> s >> z >> y
print(g)
g.print()
GraphView(name='', Nodes: 5 (inputs: 0, outputs: 0))
[15]:
# #create and connect nodes
# x = ai.Input('x')
# c = ai.Producer(t, 'c')
#
# s = ai.Add([x, c], 's') #or s = ai.Add(a=x, b=c, name=‘s’) #or s = ai.Add({‘a’:x, ‘b’:c}, ‘s’)
# z = ai.ReLU(s, 'z')
#
# #create graph
# g = ai.get_connected_graph_view(s) #or z or s
My first multilayer perceptron (MLP)#
The MLP is a basic feedforward neural network composed by a sequence of fully-connected (FC) + activation (e.g.: ReLU) layers.
Let’s create this one:
x -> FC -> ReLU -> FC -> y
[16]:
# when using the FC factory, Aidge will automatically create the producers
# however, you need to inform the number of inputs and outputs
# create the nodes
fc1 = ai.FC(in_channels=10, out_channels=10, no_bias=False, name="fc1")
act1 = ai.ReLU("act1")
fc2 = ai.FC(in_channels=10, out_channels=2, no_bias=False, name="fc2")
# create the graph linking the nodes with special operators
mlp = fc1 >> act1 >> fc2
mlp.set_name("mlp")
# show the graph
print(mlp)
mlp.print()
print(
"- graph inputs:", [(node.name(), idx) for (node, idx) in mlp.get_ordered_inputs()]
)
print(
"- graph outputs:",
[(node.name(), idx) for (node, idx) in mlp.get_ordered_outputs()],
)
GraphView(name='mlp', Nodes: 7 (inputs: 1, outputs: 1))
fc2 (FC)
├ act1 (ReLU)
| └ fc1 (FC)
| ├ <input>
| ├ fc1_w (Producer)
| └ fc1_b (Producer)
├ fc2_w (Producer)
└ fc2_b (Producer)
- graph inputs: [('fc1', 0)]
- graph outputs: [('fc2', 0)]
[17]:
mlp.compile(backend="cpu", datatype=ai.dtype.float32, dims=[[10]])
The graph can also be visualized using the visualize method from aidge_model_explorer. This package extends the ai_edge_model_explorer project by Google, and has been adapted for Aidge’s GraphViews.
[18]:
import aidge_model_explorer as aime
aime.visualize(mlp, "my_model", embed=True)
Loading extensions...
! Failed to load extension module ".builtin_tflite_flatbuffer_adapter":
/home/fperotto/miniconda/lib/python3.13/site-packages/zmq/backend/cython/../../../../.././libstdc++.so.6: version `GLIBCXX_3.4.30' not found (required by /home/fperotto/miniconda/lib/python3.13/site-packages/ai_edge_model_explorer_adapter/_pywrap_convert_wrapper.so)
! Failed to load extension module ".builtin_tflite_mlir_adapter":
/home/fperotto/miniconda/lib/python3.13/site-packages/zmq/backend/cython/../../../../.././libstdc++.so.6: version `GLIBCXX_3.4.30' not found (required by /home/fperotto/miniconda/lib/python3.13/site-packages/ai_edge_model_explorer_adapter/_pywrap_convert_wrapper.so)
! Failed to load extension module ".builtin_tf_mlir_adapter":
/home/fperotto/miniconda/lib/python3.13/site-packages/zmq/backend/cython/../../../../.././libstdc++.so.6: version `GLIBCXX_3.4.30' not found (required by /home/fperotto/miniconda/lib/python3.13/site-packages/ai_edge_model_explorer_adapter/_pywrap_convert_wrapper.so)
! Failed to load extension module ".builtin_tf_direct_adapter":
/home/fperotto/miniconda/lib/python3.13/site-packages/zmq/backend/cython/../../../../.././libstdc++.so.6: version `GLIBCXX_3.4.30' not found (required by /home/fperotto/miniconda/lib/python3.13/site-packages/ai_edge_model_explorer_adapter/_pywrap_convert_wrapper.so)
! Failed to load extension module ".builtin_graphdef_adapter":
/home/fperotto/miniconda/lib/python3.13/site-packages/zmq/backend/cython/../../../../.././libstdc++.so.6: version `GLIBCXX_3.4.30' not found (required by /home/fperotto/miniconda/lib/python3.13/site-packages/ai_edge_model_explorer_adapter/_pywrap_convert_wrapper.so)
! Failed to load extension module ".builtin_mlir_adapter":
/home/fperotto/miniconda/lib/python3.13/site-packages/zmq/backend/cython/../../../../.././libstdc++.so.6: version `GLIBCXX_3.4.30' not found (required by /home/fperotto/miniconda/lib/python3.13/site-packages/ai_edge_model_explorer_adapter/_pywrap_convert_wrapper.so)
Loaded 3 adapters:
- Aidge adapter
- Pytorch adapter (exported program)
- JSON adapter
Starting Model Explorer server at:
http://localhost:8090/?data=%7B%22models%22%3A%20%5B%7B%22url%22%3A%20%22graphs%3A//my_model/0%22%2C%20%22adapterId%22%3A%20%22aidge_model_explorer%22%7D%5D%7D
Press Ctrl+C to stop.
Create a scheduler and run inference#
The graph is now ready for execution. To schedule the execution, a Scheduler object is created, which takes the graph and generates an optimized schedule using a consumer-producer heuristic.
[19]:
# Create scheduler
scheduler = ai.SequentialScheduler(mlp)
input_tensor = ai.Tensor(np.ones(shape=(10,), dtype=np.float32))
# Run inference!
scheduler.forward(data=[input_tensor])
[19]:
[Tensor([[ 0.00000, 0.00000]], dims = [1, 2], dtype = float32)]
Conclusion#
We discovered how to create your neural networks defining a computation graph. That is great!
Now, you are ready to the next step: training your neural networks! (See next tutorial)
[ ]: