Tensors in pyAgrum
Posted on Fri 14 March 2025 in notebooks
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, aPotential
was 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 $$h(y,x,t,z)=f(x,y,z)+g(z,t,x)$$
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 dimensions
x_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 values
npf = 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 axis
npf_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 axis
npg_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 tensors
nph = 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 variables
x,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 values
gumf.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 time
gumh=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 order
gumh=gumh.reorganize("yxtz")
print(f"order of variable (after reorganization) : {gumh.names}")
Inspecting the results¶
The difference is obvious when inspecting the objets
print("==== numpy version")
print(nph)
print("=== pyAgrum version")
print(gumh)
From pyAgrum to numpy to pyAgrum¶
npf
# fill a pyagrum.Tensor with a numpy array
gumf[:]=npf
gumf
# from pyagrum.Tensor to numpy array
gumf[:]