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)
TODO#
Explain fancy indexing (https://jakevdp.github.io/PythonDataScienceHandbook/02.07-fancy-indexing.html)