Tensors in pyAgrum
Major Change in aGrUM/pyAgrum 2.0.0: Renaming Potential to Tensor
One of the significant changes in aGrUM/pyAgrum 2.0.0 is the renaming of the class Potential to Tensor. This change reflects a more accurate and mathematically precise naming convention. Here’s why this change is important:
Why the Change?
-
Potential: In pyAgrum, aPotentialwas traditionally used to represent a factor that contributes to a joint probability distribution. While this term was functional, it was specific to probabilistic graphical models and did not fully capture the mathematical nature of the object. -
Tensor: A tensor is a well-defined mathematical object that generalizes scalars, vectors, and matrices to higher dimensions. By renaming the class toTensor, the library now aligns with the standard mathematical terminology, making it more intuitive for users familiar with linear algebra and tensor operations.
This change emphasizes that the object is not limited to probabilistic applications but is a general-purpose mathematical tool.
Efficiency of Tensors in aGrUM/pyAgrum
Tensors in aGrUM/pyAgrum are highly efficient because they avoid ambiguity in their dimensions. Unlike generic tensor libraries (e.g., NumPy), where dimensions are often implicit and can lead to confusion, aGrUM/pyAgrum enforces clear and explicit dimension handling.
Example: Tensor Operations in NumPy
Consider the tensorial addition
In NumPy, performing tensor operations requires manual handling of dimensions, such as expanding and rearranging axes to ensure compatibility. This process can be error-prone and requires careful attention to the order of dimensions.
import numpy as np
# initialization# Define the dimensionsx_dim, y_dim, z_dim, t_dim = 3, 2, 3, 4
# Create tensors f(x, y, z) and g(z, t, x) and add random valuesnpf = np.random.uniform(0,1, size=(x_dim, y_dim, z_dim)) # Shape: (3, 3, 3)npg = np.random.uniform(0,1, size=(z_dim, t_dim, x_dim)) # Shape: (3, 3, 3)# the operation itself
# we have to explicitely fix the dimension and the order of variables in the tensor
# To compute h(y, x, t, z) = f(x, y, z) + g(z, t, x), we need to align the dimensions:# 1. Expand f to include the t dimension (since f doesn't have t)# 2. Expand g to include the y dimension (since g doesn't have y)# 3. Rearrange the axes to match the desired output shape (y, x, t, z)
# Step 1: Expand f along the t axisnpf_expanded = npf[:, :, :, np.newaxis] # Add a new axis for t, shape: (3, 3, 3, 1)npf_expanded = np.broadcast_to(npf_expanded, (x_dim, y_dim, z_dim, t_dim)) # Shape: (3, 3, 3, 3)
# Step 2: Expand g along the y axisnpg_expanded = npg[:, :, :, np.newaxis] # Add a new axis for y, shape: (3, 3, 3, 1)npg_expanded = np.broadcast_to(npg_expanded, (z_dim, t_dim, x_dim, y_dim)) # Shape: (3, 3, 3, 3)
# Step 3: Rearrange the axes of f and g to match the desired output shape (y, x, t, z)# For f: (x, y, z, t) -> (y, x, t, z)npf_rearranged = np.transpose(npf_expanded, (1, 0, 3, 2)) # Shape: (3, 3, 3, 3)
# For g: (z, t, x, y) -> (y, x, t, z)npg_rearranged = np.transpose(npg_expanded, (3, 2, 1, 0)) # Shape: (3, 3, 3, 3)
# Step 4: Add the two tensorsnph = npf_rearranged + npg_rearranged # Shape: (3, 3, 3, 3)which makes it difficult to write a generic library that can manipulate any type of tensors and implies copies, wasted memory, etc.
Why aGrUM/pyAgrum Tensors are specific
In aGrUM/pyAgrum, tensors are designed to avoid such ambiguity:
- Explicit Dimensions: Each tensor has clearly defined dimensions, eliminating the need for manual reshaping or broadcasting.
- Efficient Operations: Tensor operations can then be optimized (in space and in time), ensuring both clarity and performance.
import pyagrum as gum
# initialization# Define the variablesx,y,z,t=[gum.RangeVariable(name,"",0,dim-1) for name,dim in [("x",3),("y",2),("z",3),("t",4)]]
# Create tensors f(x, y, z) and g(z, t, x)gumf=gum.Tensor().add(x).add(y).add(z)gumg=gum.Tensor().add(z).add(t).add(x)
# random valuesgumf.random()gumg.random();# the operation itself (to compare with cell 2 !)
# everything is implicitely known. So it is just a non-ambiguous addition,# optimized in space and timegumh=gumf+gumg# Note that we do not know the order of variables in h, which is not important for gum.Tensor.print(f"order of variable (decided by pyAgrum) : {gumh.names}")# But if you need to specify this ordergumh=gumh.reorganize("yxtz")print(f"order of variable (after reorganization) : {gumh.names}")order of variable (decided by pyAgrum) : ('x', 'z', 't', 'y')order of variable (after reorganization) : ('y', 'x', 't', 'z')Inspecting the results
The difference is obvious when inspecting the objets
print("==== numpy version")print(nph)==== numpy version[[[[1.00279199 1.40211225 1.58851754] [1.08726371 1.3410301 1.02025983] [1.46783515 1.53827553 1.51562036] [1.53723151 1.77103416 1.10407083]]
[[1.15403673 1.67844666 1.00992697] [1.00770316 1.19500721 0.56922697] [1.55519353 1.22210327 1.08300224] [1.33155445 1.41210413 1.204056 ]]
[[1.26873044 0.95620388 1.00910535] [0.67311225 1.61212158 1.14250785] [1.11994453 1.42231681 0.35263205] [1.16701443 1.0389048 1.14311401]]]
[[[1.23413197 1.21502322 1.12590436] [1.3186037 1.15394107 0.55764665] [1.69917513 1.3511865 1.05300718] [1.76857149 1.58394513 0.64145765]]
[[0.73306339 1.89873308 0.72321355] [0.58672982 1.41529363 0.28251355] [1.13422019 1.44238969 0.79628882] [0.9105811 1.63239054 0.91734259]]
[[1.74315964 0.79601605 1.61623904] [1.14754145 1.45193375 1.74964154] [1.59437373 1.26212898 0.95976574] [1.64144363 0.87871697 1.7502477 ]]]]print("=== pyAgrum version")print(gumh)=== pyAgrum version
|| y |x |t |z ||0 |1 |------|------|------||---------|---------|0 |0 |0 || 1.1656 | 0.6120 |1 |0 |0 || 0.4794 | 0.9638 |2 |0 |0 || 0.2967 | 1.0005 |0 |1 |0 || 1.7637 | 1.2102 |1 |1 |0 || 1.3657 | 1.8501 |2 |1 |0 || 0.8423 | 1.5462 |[...24 more line(s) ...]0 |2 |2 || 1.1165 | 1.1909 |1 |2 |2 || 1.0813 | 0.8496 |2 |2 |2 || 1.5744 | 1.4700 |0 |3 |2 || 0.8403 | 0.9148 |1 |3 |2 || 1.2494 | 1.0177 |2 |3 |2 || 1.4238 | 1.3194 |From pyAgrum to numpy to pyAgrum
npfarray([[[0.61588432, 0.88034016, 0.65997397], [0.84722431, 0.69325114, 0.19736079]],
[[0.92828417, 0.687847 , 0.51705189], [0.50731083, 0.90813342, 0.23033847]],
[[0.4687125 , 0.71961079, 0.24134499], [0.9431417 , 0.55942296, 0.84847868]]])# fill a pyagrum.Tensor with a numpy arraygumf[:]=npfgumf|
|
|
| ||
|---|---|---|---|---|
|
| 0.6159 | 0.8803 | 0.6600 | |
| 0.8472 | 0.6933 | 0.1974 | ||
|
| 0.9283 | 0.6878 | 0.5171 | |
| 0.5073 | 0.9081 | 0.2303 | ||
|
| 0.4687 | 0.7196 | 0.2413 | |
| 0.9431 | 0.5594 | 0.8485 | ||
# from pyagrum.Tensor to numpy arraygumf[:]array([[[0.61588432, 0.88034016, 0.65997397], [0.84722431, 0.69325114, 0.19736079]],
[[0.92828417, 0.687847 , 0.51705189], [0.50731083, 0.90813342, 0.23033847]],
[[0.4687125 , 0.71961079, 0.24134499], [0.9431417 , 0.55942296, 0.84847868]]])