NumPy

Environment setup

import platform

print(f"Python version: {platform.python_version()}")
assert platform.python_version_tuple() >= ("3", "6")

import numpy as np

print(f"NumPy version: {np.__version__}")

Creating tensors

NumPy provides several useful functions for initializing tensors with particular values.

Filling a tensor with zeros

x = np.zeros(3)
print(x)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
[0. 0. 0.]
Dimensions: 1
Shape: (3,)
x = np.zeros((3,4))
print(x)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
[[0. 0. 0. 0.]
 [0. 0. 0. 0.]
 [0. 0. 0. 0.]]
Dimensions: 2
Shape: (3, 4)

Filling a tensor with random numbers

Values are sampled from a “normal” (Gaussian) distribution

x = np.random.randn(5,2)
print(x)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
[[ 0.50961507 -0.66325773]
 [-0.26615276 -0.61404259]
 [ 0.85148899 -1.00384867]
 [ 0.52699103  1.76971159]
 [-1.03823767 -0.36072364]]
Dimensions: 2
Shape: (5, 2)

Tensor shape management

x = np.array([12, 3, 6]) # x is a 3 dimensions vector (1D tensor)
print(x)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
[12  3  6]
Dimensions: 1
Shape: (3,)

Tensors with single-dimensional entries

x = np.array([[12, 3, 6, 14]]) # x is a one row matrix (2D tensor)
print(x)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
[[12  3  6 14]]
Dimensions: 2
Shape: (1, 4)
x = np.array([[12], [3], [6], [14]]) # x is a one column matrix (2D tensor)
print(x)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
[[12]
 [ 3]
 [ 6]
 [14]]
Dimensions: 2
Shape: (4, 1)

Removing single-dimensional entries from a tensor

x = np.array([[12, 3, 6, 14]])
x = np.squeeze(x) # x is now a vector (1D tensor)
print(x)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
[12  3  6 14]
Dimensions: 1
Shape: (4,)
x = np.array([[12], [3], [6], [14]])
x = np.squeeze(x) # x is now a vector (1D tensor)
print(x)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
[12  3  6 14]
Dimensions: 1
Shape: (4,)

Reshaping a tensor

# Reshape a 3D tensor into a matrix
x = np.array([[[5, 6],
               [7, 8]],
              [[9, 10],
               [11, 12]],
              [[13, 14],
               [15, 16]]])
print (f'Original dimensions: {x.ndim}')
print (f'Original shape: {x.shape}')
x = x.reshape(3, 2*2)
print(x)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
Original dimensions: 3
Original shape: (3, 2, 2)
[[ 5  6  7  8]
 [ 9 10 11 12]
 [13 14 15 16]]
Dimensions: 2
Shape: (3, 4)

Adding a dimension to a tensor

# Add a dimension to a vector, turning it into a row matrix
x = np.array([1, 2, 3])
print (f'Original dimensions: {x.ndim}')
print (f'Original shape: {x.shape}')
x = x[np.newaxis, :]
print(x)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
Original dimensions: 1
Original shape: (3,)
[[1 2 3]]
Dimensions: 2
Shape: (1, 3)
# Add a dimension to a vector, turning it into a column matrix
x = np.array([1, 2, 3])
print (f'Original dimensions: {x.ndim}')
print (f'Original shape: {x.shape}')
x = x[:, np.newaxis]
print(x)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
Original dimensions: 1
Original shape: (3,)
[[1]
 [2]
 [3]]
Dimensions: 2
Shape: (3, 1)

Transposing a tensor

# Transpose a vector (no effect)
x = np.array([12, 3, 6, 14])
x = x.T # alternative syntax: x = np.transpose(x)
print(x)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
[12  3  6 14]
Dimensions: 1
Shape: (4,)
# Transpose a matrix
x = np.array([[5, 78, 2, 34],
              [6, 79, 3, 35],
              [7, 80, 4, 36]])
x = x.T
print(x)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
[[ 5  6  7]
 [78 79 80]
 [ 2  3  4]
 [34 35 36]]
Dimensions: 2
Shape: (4, 3)

Tensor indexing and slicing

# Slice a vector
x = np.array([1, 2, 3, 4, 5, 6, 7])
print(x[:3])
print(x[3:])
[1 2 3]
[4 5 6 7]
# Slice a matrix
x = np.array([[5, 78, 2, 34],
              [6, 79, 3, 35],
              [7, 80, 4, 36]])
print(x[:2, :])
print(x[2:, :])
print(x[:, :2])
print(x[:, 2:])
[[ 5 78  2 34]
 [ 6 79  3 35]]
[[ 7 80  4 36]]
[[ 5 78]
 [ 6 79]
 [ 7 80]]
[[ 2 34]
 [ 3 35]
 [ 4 36]]

Operations between tensors

Element-wise operations are applied independently to each entry in the tensors being considered.

Other operations, like dot product, combine entries in the input tensors to produce a differently shaped result.

Element-wise addition

# Element-wise addition between two vectors
x = np.array([2, 5, 4])
y = np.array([1, -1, 4])
z = x + y
print(z)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
[3 4 8]
Dimensions: 1
Shape: (3,)

Element-wise product

# Element-wise product between two matrices (shapes must be identical)
x = np.array([[1, 2, 3], 
              [3, 2, 1]])
y = np.array([[3, 0, 2], 
              [1, 4, -2]])
z = x * y
print(z)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
[[ 3  0  6]
 [ 3  8 -2]]
Dimensions: 2
Shape: (2, 3)

Dot product

# Dot product between two matrices (shapes must be compatible)
x = np.array([[1, 2, 3], 
              [3, 2, 1]]) # x has shape (2, 3)
y = np.array([[3, 0], 
              [2, 1], 
              [4, -2]]) # y has shape (3, 2)
z = np.dot(x, y) # alternative syntax: z = x.dot(y)
print(z)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
[[19 -4]
 [17  0]]
Dimensions: 2
Shape: (2, 3)

Broadcasting

Broadcasting is a powerful NumPy functionality.

If there is no ambiguity, the smaller tensor can be “broadcasted” implicitly to match the larger tensor’s shape before an operation is applied to them.

Broadcasting between a vector and a scalar

x = np.array([12, 3, 6, 14])
x = x + 3
print(x)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
[15  6  9 17]
Dimensions: 1
Shape: (4,)

Broadcasting between a matrix and a scalar

x = np.array([[0, 1, 2], 
              [-2, 5, 3]])
x = x - 1
print(x)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
[[-1  0  1]
 [-3  4  2]]
Dimensions: 2
Shape: (2, 3)

Broadcasting between a matrix and a vector

x = np.array([[0, 1, 2], 
              [-2, 5, 3]])
y = np.array([1, 2, 3])
z = x + y
print(z)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
[[ 1  3  5]
 [-1  7  6]]
Dimensions: 2
Shape: (2, 3)

Summing tensors

Summing on all axes

x = np.array([[0, 1, 2], 
              [-2, 5, 3]])
x = np.sum(x) # x is now a scalar (0D tensor)
print(x)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
9
Dimensions: 0
Shape: ()

Summing on a specific axis

# Sums a matrix on its first axis (rows)
x = np.array([[0, 1, 2], 
              [-2, 5, 3]])
x = np.sum(x, axis=0) # x is now a vector (1D tensor)
print(x)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
[-2  6  5]
Dimensions: 1
Shape: (3,)
# Sums a matrix on its second axis (columns)
x = np.array([[0, 1, 2], 
              [-2, 5, 3]])
x = np.sum(x, axis=1) # x is now a vector (1D tensor)
print(x)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
[3 6]
Dimensions: 1
Shape: (2,)

Keeping tensor dimensions while summing

# Sums a matrix on its second axis (columns), keeping the same dimensions
x = np.array([[0, 1, 2], 
              [-2, 5, 3]])
x = np.sum(x, axis=0, keepdims=True) # x is still a matrix (2D tensor)
print(x)
print(f'Dimensions: {x.ndim}')
print(f'Shape: {x.shape}')
[[-2  6  5]]
Dimensions: 2
Shape: (1, 3)