How to think in Aesara#
Adding two Scalars#
To get us started with Aesara and get a feel of what we’re working with, let’s make a simple function: add two numbers together. Here is how you do it:
>>> import numpy
>>> import aesara.tensor as at
>>> from aesara import function
>>> x = at.dscalar('x')
>>> y = at.dscalar('y')
>>> z = x + y
>>> f = function([x, y], z)
And now that we’ve created our function we can use it:
>>> f(2, 3)
array(5.0)
>>> numpy.allclose(f(16.3, 12.1), 28.4)
True
Let’s break this down into several steps. The first step is to define
two symbols (Variables) representing the quantities that you want
to add. Note that from now on, we will use the term
Variable to mean “symbol” (in other words,
x, y, z are all Variable objects). The output of the function
f is a numpy.ndarray
with zero dimensions.
If you are following along and typing into an interpreter, you may have
noticed that there was a slight delay in executing the function
instruction. Behind the scene, f was being compiled into C code.
Step 1
>>> x = at.dscalar('x')
>>> y = at.dscalar('y')
In Aesara, all symbols must be typed. In particular, at.dscalar
is the type we assign to “0-dimensional arrays (scalar
) of doubles
(d
)”. It is an Aesara Type.
dscalar
is not a class. Therefore, neither x nor y
are actually instances of dscalar
. They are instances of
TensorVariable
. x and y
are, however, assigned the aesara Type dscalar
in their type
field, as you can see here:
>>> type(x)
<class 'aesara.tensor.var.TensorVariable'>
>>> x.type
TensorType(float64, ())
>>> at.dscalar
TensorType(float64, ())
>>> x.type is at.dscalar
True
By calling at.dscalar
with a string argument, you create a
Variable representing a floating-point scalar quantity with the
given name. If you provide no argument, the symbol will be unnamed. Names
are not required, but they can help debugging.
More will be said in a moment regarding Aesara’s inner structure. You could also learn more by looking into Graph Structures.
Step 2
The second step is to combine x and y into their sum z:
>>> z = x + y
z is yet another Variable which represents the addition of x and y. You can use the pp function to pretty-print out the computation associated to z.
>>> from aesara import pp
>>> print(pp(z))
(x + y)
Step 3
The last step is to create a function taking x and y as inputs and giving z as output:
>>> f = function([x, y], z)
The first argument to function
is a list of Variables
that will be provided as inputs to the function. The second argument
is a single Variable or a list of Variables. For either case, the second
argument is what we want to see as output when we apply the function. f may
then be used like a normal Python function.
Note
As a shortcut, you can skip step 3, and just use a variable’s
eval
method.
The eval()
method is not as flexible
as function()
but it can do everything we’ve covered in
the tutorial so far. It has the added benefit of not requiring
you to import function()
. Here is how eval()
works:
>>> import numpy
>>> import aesara.tensor as at
>>> x = at.dscalar('x')
>>> y = at.dscalar('y')
>>> z = x + y
>>> numpy.allclose(z.eval({x : 16.3, y : 12.1}), 28.4)
True
We passed eval()
a dictionary mapping symbolic aesara
variables to the values to substitute for them, and it returned
the numerical value of the expression.
eval()
will be slow the first time you call it on a variable –
it needs to call function()
to compile the expression behind
the scenes. Subsequent calls to eval()
on that same variable
will be fast, because the variable caches the compiled function.
Adding two Matrices#
You might already have guessed how to do this. Indeed, the only change from the previous example is that you need to instantiate x and y using the matrix Types:
>>> x = at.dmatrix('x')
>>> y = at.dmatrix('y')
>>> z = x + y
>>> f = function([x, y], z)
dmatrix
is the Type for matrices of doubles. Then we can use
our new function on 2D arrays:
>>> f([[1, 2], [3, 4]], [[10, 20], [30, 40]])
array([[ 11., 22.],
[ 33., 44.]])
The variable is a NumPy array. We can also use NumPy arrays directly as inputs:
>>> import numpy
>>> f(numpy.array([[1, 2], [3, 4]]), numpy.array([[10, 20], [30, 40]]))
array([[ 11., 22.],
[ 33., 44.]])
It is possible to add scalars to matrices, vectors to matrices, scalars to vectors, etc. The behavior of these operations is defined by broadcasting rules.
The following types are available:
byte:
bscalar, bvector, bmatrix, brow, bcol, btensor3, btensor4, btensor5, btensor6, btensor7
16-bit integers:
wscalar, wvector, wmatrix, wrow, wcol, wtensor3, wtensor4, wtensor5, wtensor6, wtensor7
32-bit integers:
iscalar, ivector, imatrix, irow, icol, itensor3, itensor4, itensor5, itensor6, itensor7
64-bit integers:
lscalar, lvector, lmatrix, lrow, lcol, ltensor3, ltensor4, ltensor5, ltensor6, ltensor7
float:
fscalar, fvector, fmatrix, frow, fcol, ftensor3, ftensor4, ftensor5, ftensor6, ftensor7
double:
dscalar, dvector, dmatrix, drow, dcol, dtensor3, dtensor4, dtensor5, dtensor6, dtensor7
complex:
cscalar, cvector, cmatrix, crow, ccol, ctensor3, ctensor4, ctensor5, ctensor6, ctensor7
The previous list is not exhaustive and a guide to all types compatible with NumPy arrays may be found here: tensor creation.
Note
You, the user—not the system architecture—have to choose whether your
program will use 32- or 64-bit integers (i
prefix vs. the l
prefix)
and floats (f
prefix vs. the d
prefix).