Shapes in ΦML¶

Colab   •   🌐 ΦML   •   📖 Documentation   •   🔗 API   •   ▶ Videos   •   Examples

In [1]:
# !pip install phiml
from phiml import math

Dimension Types¶

The largest difference between ΦML and its backend libraries like PyTorch or Jax lies in the tensor shapes. When using ΦML's tensors, all dimensions must be assigned a name and type flag. To learn why this is useful, see here.

The following dimension types are available:

  • batch dimensions can be added to any code in order to parallelize it. This is their only function. The code should always give the exact same result as if it was called sequentially on all slices and the results were stacked along the batch dimension.
  • channel dimensions list components of one object, such as a pixel, grid cell or particle. Typical examples include color channels or (x,y,z) components of a vector.
  • spatial dimensions denote grid dimensions. Typically, elements are equally-spaced along spatial dimensions, enabling operations such as convolutions or FFTs. The resolution of an image or lattice is typically expressed via spatial dimensions.
  • instance dimensions enumerate objects that are not regularly ordered, such as moving particles or finite elements.
  • dual dimensions represent function inputs and are typically used to denote the columns of matrices. See the matrix documentation for more.
In [2]:
from phiml.math import batch, channel, spatial, instance, dual
BATCH = batch(examples=100)
BATCH
Out[2]:
(examplesᵇ=100)

Here, we have created a Shape containing a single batch dimension with name examples. Note the superscript b to indicate that this is a batch dimension. Naturally the other superscripts are c for channel, s for spatial, i for instance and d for dual.

We can now use this shape to construct tensors:

In [3]:
x = math.zeros(BATCH)
x
Out[3]:
(examplesᵇ=100) const 0.0

Let's create a tensor with this batch and multiple spatial dimensions! We can pass multiple shapes to tensor constructors and can construct multiple dimensions of the same type in one call.

In [4]:
x = math.ones(BATCH, spatial(x=28, y=28))
x
Out[4]:
(examplesᵇ=100, xˢ=28, yˢ=28) const 1.0

We can retrieve the Shape of x using either x.shape or math.shape(x) which also works on primitive types.

In [5]:
x.shape
Out[5]:
(examplesᵇ=100, xˢ=28, yˢ=28)

The dimension constructors, such as math.spatial, can also be used to filter for only these dimensions off an object.

In [6]:
spatial(x)
Out[6]:
(xˢ=28, yˢ=28)

There are additional filter function, such as non_*** as well as primal to exclude batch and dual dimensions.

This way, we can easily construct a tensor without the batch dimension.

In [7]:
from phiml.math import non_batch, non_channel, non_spatial, non_instance, non_dual, primal, non_primal
math.random_uniform(non_batch(x))
Out[7]:
(xˢ=28, yˢ=28) 0.484 ± 0.290 (2e-03...1e+00)

Automatic Reshaping¶

One major advantage of naming all dimensions is that reshaping operations can be performed under-the-hood. Assuming we have a tensor with dimensions a,b and another with the reverse dimension order.

In [8]:
t1 = math.random_normal(channel(a=2, b=3))
t2 = math.random_normal(channel(b=3, a=2))

When combining them in a tensor operation, ΦML automatically transposes the tensors to match.

In [9]:
t1 + t2
Out[9]:
(-1.938, -1.547, 1.655, -0.457, 0.587, -1.382) (aᶜ=2, bᶜ=3)

The resulting dimension order is generally undefined. However, this is of no consequence, because dimensions are never referenced by their index in the shape.

When one of the tensors is missing a dimension, it will be added automatically. In these cases, you can think of the value being constant along the missing dimension (like with singleton dimensions in NumPy).

In [10]:
t1 = math.random_normal(channel(a=2))
t2 = math.random_normal(channel(b=3))
t1 + t2
Out[10]:
(-2.514, -4.295, -0.932, -1.224, -3.005, 0.357) (aᶜ=2, bᶜ=3)

Here, we created a 2D tensor from two 1D tensors. No manual reshaping required.

Selecting and Combining Dimensions¶

All tensor creation functions accept a variable number of Shape objects as input and concatenate the dimensions internally. This can also be done explicitly using concat_shapes().

In [11]:
b = batch(examples=16)
s = spatial(x=28, y=28)
c = channel(channels='red,green,blue')
math.concat_shapes(s, c, b)
Out[11]:
(xˢ=28, yˢ=28, channelsᶜ=3:red..., examplesᵇ=16)

This preserves the dimension order and fails if multiple dimensions with the same name are given. Alternatively, merge_shapes() can be used, which groups dimensions by type and allows for the same dimensions to be present on multiple inputs.

In [12]:
s = math.merge_shapes(s, c, b)
s
Out[12]:
(examplesᵇ=16, xˢ=28, yˢ=28, channelsᶜ=3:red...)

This can also be done using the & operator. Notice how the batch dimension is moved to the first place.

In [13]:
s & c & b
Out[13]:
(examplesᵇ=16, xˢ=28, yˢ=28, channelsᶜ=3:red...)

Filtering shapes for specific dimensions can be done using Shape[name], Shape.only() and Shape.without().

In [14]:
s['x']
Out[14]:
(xˢ=28)
In [15]:
s.only('x,y')
Out[15]:
(xˢ=28, yˢ=28)
In [16]:
s.without('x,y')
Out[16]:
(examplesᵇ=16, channelsᶜ=3:red...)
In [17]:
s.only(spatial)
Out[17]:
(xˢ=28, yˢ=28)

Selecting only one type of dimension can also be done using the construction function or the corresponding Shape member variable.

In [18]:
s.spatial
Out[18]:
(xˢ=28, yˢ=28)
In [19]:
spatial(s)
Out[19]:
(xˢ=28, yˢ=28)
In [20]:
s.non_spatial
Out[20]:
(examplesᵇ=16, channelsᶜ=3:red...)
In [21]:
non_spatial(s)
Out[21]:
(examplesᵇ=16, channelsᶜ=3:red...)

Properties of Shapes¶

Shape objects are immutable. Do not attempt to change any property of a Shape directly. The sizes of all dimensions can be retrieved as a tuple using Shape.sizes´. The result is equal to what NumPy or any of the other backends would return fortensor.shape`.

In [22]:
s.sizes
Out[22]:
(16, 28, 28, 3)

Likewise, the names of the dimensions can be read using Shape.names.

In [23]:
s.names
Out[23]:
('examples', 'x', 'y', 'channels')

For single-dimension shapes, the properties name and size return the value directly. You can select To get the size of a specific dimension, you can use one of the following methods:

In [24]:
s['x'].size
Out[24]:
28
In [25]:
for dim in s:
    print(dim.name, dim.size, dim.dim_type.__name__)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[25], line 2
      1 for dim in s:
----> 2     print(dim.name, dim.size, dim.dim_type.__name__)

AttributeError: 'str' object has no attribute '__name__'

The number of dimensions and total elements can be retrieved using len(Shape) and Shape.volume, respectively.

In [26]:
len(s)
Out[26]:
4
In [27]:
s.non_batch.volume
Out[27]:
2352

Changing Dimensions¶

The names and types of dimensions can be changed, but this always returns a new object, leaving the original unaltered. Assume, we want to rename the channels dimension from above to color.

In [28]:
math.rename_dims(s, 'channels', 'color')
Out[28]:
(examplesᵇ=16, xˢ=28, yˢ=28, colorᶜ=3:red...)

The same can be done for tensors.

In [29]:
math.rename_dims(math.zeros(s), 'channels', 'color')
Out[29]:
(examplesᵇ=16, xˢ=28, yˢ=28, colorᶜ=3:red...) const 0.0

To change the type, you may use replace_dims(), which is an alias for rename_dims() but clarifies the intended use.

In [30]:
math.replace_dims(s, 'channels', batch('channels'))
Out[30]:
(examplesᵇ=16, xˢ=28, yˢ=28, channelsᵇ=3:red...)

Response to Dimension Types by Function¶

The dimension types serve an important role in indicating what role a dimension plays. Many math functions behave differently, depending on the given dimension types.

Vector operations like vec_length or rotate_vector require the input to have a channel dimension to list the vector components.

Spatial operations like fft or convolve, as well as finite differences spatial_gradient, laplace, fourier_laplace, fourier_poisson, and resampling operations like downsample2x, upsample2x, grid_sample act only on spatial dimensions. Their dimensionality (1D/2D/3D/etc.) depends on the number of spatial dimensions of the input.

Dual dimensions are ignored (treated as batch dimensions) by almost all functions, except for matrix multiplications, matrix @ vector, which reduces the dual dimensions of the matrix against the corresponding primal dimensions of the vector. Dual dimensions are created by certain operations like pairwise_distances.

All functions ignore batch dimensions. This also applies to functions that would usually reduce all dimensions by default, such as sum, mean, std, any, all, max, min and many more, as well as loss functions like the l2_loss.

The elementary functions gather and scatter act on spatial or instance dimensions of the grid. The indices are listed along instance dimensions and the index components along a singular channel dimension.

Further Reading¶

See Advantages of Dimension Names and Types for additional examples with comparisons to other computing libraries.

Dimension names play an important role in slicing tensors. To make your code more readable, you can also name slices along dimensions.

The number of spatial dimensions dictates what dimensionality (1D, 2D, 3D) your code works in. You can therefore write code that works in 1D, 2D, 3D and beyond.

Dual dimensions are used to represent columns of matrices.

Stacking tensors with the same dimension names but different sizes results in non-uniform shapes.

🌐 ΦML   •   📖 Documentation   •   🔗 API   •   ▶ Videos   •   Examples