# Matrices

A matrix is a $2$-dimensional (2D) array of numbers. This means that every element in the matrix is identified by two indices, commonly $i$ representing the row-index and $j$ representing the column-index.

Matrices are usually given uppercase variable names in bold, such as $\mathbf{A}$.

$\mathbf{A} = \begin{bmatrix} A_{1,1} & A_{1,2} \\ A_{2,1} & A_{2,2} \end{bmatrix}$

We use a colon “:” to represent all the elements across an axis. So, $\mathbf{A}_{i,:}$ identifies all the elements in the $i$th row and $\mathbf{A}_{:,j}$ identifies all the elements in the $j$th column. If a real-valued matrix (i.e. its elements are all real numbers) has $m$ rows and $n$ columns, we show the matrix dimensionality as $\mathbf{A} \in \mathbb{R}^{m \times n}$.

In Python, we can represent a matrix as a list of lists, however, it is more convenient to represent matrices and higher-order tensors in packages designed to handle them such as NumPy, TensorFlow or PyTorch.

A = np.array([[7, 4, 3, 2], [0, 8, 9, 5]])

# Python is zero-indexed so A[1,:] will give the second row in the matrix
print("First element of matrix A is {}".format(A[0,0]))
print("Second row of matrix A is {}".format(A[1,:]))
print("A is of type {}".format(type(A)))
print("A has shape {}".format(A.shape))  # 2 rows, 4 columns

First element of matrix A is 7
Second row of matrix A is [0 8 9 5]
A is of type <class 'numpy.ndarray'>
A has shape (2, 4)


NumPy allows us to do various matrix operations such as element-wise multiplication, matrix multiplication, dimensionality manipulation, broadcasting and more. Some examples can be found below.

# Create a 2x2 identitity matrix
B = np.identity(2)
print(B)

[[1. 0.]
[0. 1.]]

# Create a matrix of ones
B = np.ones(shape=(2, 4))
print(B)

[[1. 1. 1. 1.]
[1. 1. 1. 1.]]

# Element-wise multiplication by a scalar
B = 3*B
print(B)

[[3. 3. 3. 3.]
[3. 3. 3. 3.]]

# Multiplication of a vector and a matrix through broadcasting
x = np.array([7, 4, 3, 2])
B = x*B
print(B)

[[21. 12.  9.  6.]
[21. 12.  9.  6.]]

# Element-wise multiplication between two matrices of the same shape
C = A*B
print(C)

[[147.  48.  27.  12.]
[  0.  96.  81.  30.]]

# Matrix multiplication between two matrices
C = A.dot(B.T)  # We transpose B to get a multiplication between matrices of shapes (2, 4) x (4, 2) = (2, 2)
print(C)

[[234. 234.]
[207. 207.]]

# Modify a matrix element in-place
C[1,1] -= 5
print(C)

[[234. 234.]
[207. 202.]]

# We can also invert a square, non-singular matrix
C_inv = np.linalg.inv(C)
print(C_inv)

[[-0.17264957  0.2       ]
[ 0.17692308 -0.2       ]]

# Another way to do matrix inversion would be through the NumPy matrix object instead of using arrays
D = np.matrix([[234, 234], [207, 202]])
D_inv = D.I
print(D_inv)

[[-0.17264957  0.2       ]
[ 0.17692308 -0.2       ]]

# The multiplication of a matrix with its inverse should therefore give
# the Identity matrix (subject to floating-point inaccuracies)
E = D.dot(D_inv)
assert np.array_equal(E, np.identity(E.shape)), "E is not equal to the identity matrix of the same shape!"
print(E)

[[1. 0.]
[0. 1.]]

# Expand a matrix dimensions
print("Original matrix shape is", E.shape)
E = np.expand_dims(E, axis=1)
print("Matrix shape after expanding dimensions on axis 1 is", E.shape)

Original matrix shape is (2, 2)
Matrix shape after expanding dimensions on axis 1 is (2, 1, 2)


For more examples, see https://scipy-lectures.org/intro/numpy/operations.html