Tensor operations and primitives (with Tensorflow)

August 3, 2020 by Stefan Huber

For fluently reading and writing custom TensorFlow code (e.g., custom loss functions, custom layers, …) a good understanding of basic tensor operations is necessary. This article gives an overview of operations, which are commonly found in the wild.

Terminology

A tensor is a multi-dimensional data structure (basically another name for a multi-dimensional array). Tensors (tf.Tensor) have a fixed shape, which defines the number of dimensions (aka axis) and the number of items within each dimension. The total number of dimensions is defined as the rank of a tensor. The total number of items within the data structure states the size of the tensor. Furthermore the tensor has a defined datatype (e.g., tf.int32, tf.float32) and all items must be of this datatype.

The following code snippet creates a tensor with the size of 9 and the rank of 2. The shape is (3, 3) and the datatype is implicitly set to tf.int32.

tensor = tf.constant([
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
])

print('shape: {}'.format(tensor.shape))
print('datatype: {}'.format(tensor.dtype))
print('rank: {}'.format(tf.rank(tensor)))
print('size: {}'.format(tf.size(tensor)))

Output:

shape: (3, 3)
datatype: <dtype: 'int32'>
rank: 2
size: 9

Based on the rank different mathematical names are defined for the tensor data structure. That is, a tensor with rank 0 is called a scalar, a tensor with rank 1 is called a vector, a tensor with rank 2 is called a matrix. Everything which has a rank higher than 2 is a tensor.

Creating tensors

tf.constant creates a immutable tensor from a tensor like data structure (e.g., a python list):

tensor = tf.constant([
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
])
print(tensor)

Output:

tf.Tensor(
[[1 2 3]
 [4 5 6]
 [7 8 9]], shape=(3, 3), dtype=int32)

tf.range creates a tensor similarly to a python range definition, with a start, end (optional) and delta (optional, default=1) value.

tensor = tf.range(1, 8, 2)
print(tensor)

Output:

tf.Tensor([1 3 5 7], shape=(4,), dtype=int32)

tf.fill creates a tensor based on a specified shape and sets a given value for each item. tf.ones and tf.zeros are similar to tf.fill, however instead of an arbitrary value 1 or 0 is used. Furthermore tf.ones_like and tf.zeros_like create a tensor with 1 or 0 based on the shape of an existing tensor.

tensor = tf.fill([3, 2], 5)
print(tensor)

Output:

tf.Tensor(
[[5 5]
 [5 5]
 [5 5]], shape=(3, 2), dtype=int32)

Indexing and slicing

Similar to numpy or python data structures certain indices can be accessed or parts of a tensor can be selected by slicing.

Important syntax for indexing and slicing:

  • negative indices count backward from the end of the tensor dimension
  • slicing can be used with :
  • ... includes all dimension, which is a shorthand for using : repeatingly
tensor = tf.constant([
    [
        [1, 2, 3],
        [4, 5, 6]
    ],
    [
        [3, 2, 1],
        [6, 5, 4]
    ]
])
print(tensor[:, :, -1], tensor[..., -1])

Output:

tf.Tensor(
[[3 6]
 [1 4]], shape=(2, 2), dtype=int32)
tf.Tensor(
[[3 6]
 [1 4]], shape=(2, 2), dtype=int32)

Mathematical/Arithmetic operations

There are many mathematical operations implemented for tensors. Some are also overloaded with respective operators.

In the following code snippet several element-wise operations are demonstrated. Generally, for element-wise operations the tensors require the same shape. Broadcasting, which is explained in the next section allows for element-wise operations which don’t require the same shape.

a = tf.constant([1, 2, 3])
b = tf.constant([4, 5, 6])

print(a/b, tf.truediv(a, b))
print(a+b, tf.add(a, b))
print(a-b, tf.subtract(a, b))
print(a*b, tf.multiply(a, b))

Output:

tf.Tensor([0.25 0.4  0.5 ], shape=(3,), dtype=float64) tf.Tensor([0.25 0.4  0.5 ], shape=(3,), dtype=float64)
tf.Tensor([5 7 9], shape=(3,), dtype=int32) tf.Tensor([5 7 9], shape=(3,), dtype=int32)
tf.Tensor([-3 -3 -3], shape=(3,), dtype=int32) tf.Tensor([-3 -3 -3], shape=(3,), dtype=int32)
tf.Tensor([ 4 10 18], shape=(3,), dtype=int32) tf.Tensor([ 4 10 18], shape=(3,), dtype=int32)

Also the matrix multiplication is implemented:

a = tf.constant([
    [1, 2, 3],
    [4, 5, 6]
])
b = tf.constant([
    [4],
    [6],
    [8]
])
print(a @ b, tf.matmul(a, b))

Output:

tf.Tensor(
[[40]
 [94]], shape=(2, 1), dtype=int32)

tf.Tensor(
[[40]
 [94]], shape=(2, 1), dtype=int32)

Broadcasting

Broadcasting enables element-wise operations of tensors with different shapes. Thereby the tensor with the smaller shape for a certain dimension is “streched”, in order to execute the operation.

a = tf.constant([1, 2, 3, 4])
b = tf.constant([5])
print(a + b)
print(a * b)

Output:

tf.Tensor([6 7 8 9], shape=(4,), dtype=int32)
tf.Tensor([ 5 10 15 20], shape=(4,), dtype=int32)

In the example above the tensor b is broadcast to fit the shape of tensor a. The tensor would be broadcast into the following structure: [5, 5, 5, 5].

Reshaping tensors

The memory layout of a tf.Tensor is organized in row-major ordering (C-Style). When reshaping a tensor the actual data in the memory is not changed, only the index is reorganized. Therefore the input and output tensors of a reshaping operation must have the same size.

The tf.reshape operation is taking a tensor as input and additionally a shape definition:

tensor = tf.constant([
    [
        [1., 6., 4.],
        [2., 5., 1.]
    ],
    [
        [3., 8., 3.],
        [4., 9., 8.]
    ],
])

# reshape: (2, 2, 3) >>> (12, )

result1 = tf.reshape(tensor, [12])
print(result1)

# reshape: (2, 2, 3) >>> (4, 1, 1, 3)

result2 = tf.reshape(tensor, [4, 1, 1, 3])
print(result2)

Output:

tf.Tensor([1. 6. 4. 2. 5. 1. 3. 8. 3. 4. 9. 8.], shape=(12,), dtype=float32)
tf.Tensor(
[[[[1. 6. 4.]]]
 [[[2. 5. 1.]]]
 [[[3. 8. 3.]]]
 [[[4. 9. 8.]]]], shape=(4, 1, 1, 3), dtype=float32)

The tf.expand_dims and the tf.squeeze operations add or remove dimensions of length 1.

Tiling

Create an output tensor by tiling an input tensor according to a given specification. The tf.tile operation requires an input tensor and a multiples specification. The multiples specification is a one-dimensional tensor and the length of the dimension must be the same as the rank of the input tensor.

tensor = tf.constant([
    [
        [1, 2, 3],
        [4, 5, 6]
    ]
])
print(tf.tile(tensor, tf.constant([1, 3, 2])))

Output:

tf.Tensor(
[[[1 2 3 1 2 3]
  [4 5 6 4 5 6]
  [1 2 3 1 2 3]
  [4 5 6 4 5 6]
  [1 2 3 1 2 3]
  [4 5 6 4 5 6]]], shape=(1, 6, 6), dtype=int32)

The tf.tile operation replicates the given tensor of shape (1, 2, 3) according to the multiples specification [1, 3, 2]. That is, axis 0 is replicated once, axis 1 is replicated three times and axis 2 is replicated twice.

Repeat

The tf.repeat operation repeats tensor items on a specified axis according to a specified pattern. The length of the repeat pattern must be of the same length as the selected axis in the input tensor shape.

tensor = tf.constant([
    [1, 2, 3],
    [3, 4, 5]
])
print(tf.repeat(tensor, [2, 3], 0))
print(tf.repeat(tensor, [2, 4, 1], 1))

Output:

tf.Tensor(
[[1 2 3]
 [1 2 3]
 [3 4 5]
 [3 4 5]
 [3 4 5]], shape=(5, 3), dtype=int32)

tf.Tensor(
[[1 1 2 2 2 2 3]
 [3 3 4 4 4 4 5]], shape=(2, 7), dtype=int32)

Concat

The tf.concat operation “merges” tensors along a specified axis. The axis could also be specified with a negative number (similar to python when using a negative index for a list).

t1 = [[1, 2, 3], [4, 5, 6]]
t2 = [[7, 8, 9], [10, 11, 12]]

# concat along the 0-axis

print(tf.concat([t1, t2], 0))

# concat along the 1-axis

print(tf.concat([t1, t2], 1))

Output:

tf.Tensor(
[[ 1  2  3]
 [ 4  5  6]
 [ 7  8  9]
 [10 11 12]], shape=(4, 3), dtype=int32)

tf.Tensor(
[[ 1  2  3  7  8  9]
 [ 4  5  6 10 11 12]], shape=(2, 6), dtype=int32)

Transpose

In the case of a 2-D tensor the tf.transpose operation performs a classic matrix transposition.

tensor = tf.constant([
    [1, 2, 3],
    [4, 5, 6]
])
print(tf.transpose(tensor))

Output:

tf.Tensor(
[[1 4]
 [2 5]
 [3 6]], shape=(3, 2), dtype=int32)

For tensors of a rank greater than 2 a permutation specification can be defined. This specification states the “position” of every index in the resulting tensor.

In the example a tensor with the shape (2, 2, 3) is transposed according to a permutation specification [0, 2, 1]. The original order of the axes is [0, 1, 2], thus the transposition “swaps” the second with the third axis.

tensor = tf.constant([[[1,  2,  3],
                       [4,  5,  6]],
                      [[7,  8,  9],
                       [10, 11, 12]]])
print(tf.transpose(tensor, [0, 2, 1]))

Output:

tf.Tensor(
[[[ 1  4]
  [ 2  5]
  [ 3  6]]

 [[ 7 10]
  [ 8 11]
  [ 9 12]]], shape=(2, 3, 2), dtype=int32)

to be continued …

© 2020, Stefan Huber