Working with Matrices¶
[1]:
import numpy as np
Matrices and liner combinations¶
Post-multiplication with vector¶
Matrix-vector multiplication is a linear combination of the columns of the matrix
We can stack the columns horizontally to get matrix multiplication.
[2]:
A = np.arange(1, 7).reshape((3,2))
x1 = np.array([2, 3]).reshape((2,1))
x2 = np.array([1,4]).reshape((2,1))
[3]:
A @ x1
[3]:
array([[ 8],
[18],
[28]])
[4]:
A @ x2
[4]:
array([[ 9],
[19],
[29]])
[5]:
np.c_[x1, x2]
[5]:
array([[2, 1],
[3, 4]])
[6]:
A @ np.c_[x1, x2]
[6]:
array([[ 8, 9],
[18, 19],
[28, 29]])
Pre-multiplication with vector¶
Vector-matrix multiplication is a linear combination of the rows of the matrix
We can stack the rows vertically to get matrix multiplication.
Matrix-matrix multiplication can be seen as the horizontal stacking of column operations or as the vertical stacking of row operations.
[7]:
y1 = np.array([1,2,3]).reshape((1,3))
y2 = np.array([4,5,6]).reshape((1,3))
[8]:
y1 @ A
[8]:
array([[22, 28]])
[9]:
y2 @ A
[9]:
array([[49, 64]])
[10]:
np.r_[y1, y2]
[10]:
array([[1, 2, 3],
[4, 5, 6]])
[11]:
np.r_[y1, y2] @ A
[11]:
array([[22, 28],
[49, 64]])
Extract columns of a matrix by post-multiplication with standard unit column vector¶
[12]:
A
[12]:
array([[1, 2],
[3, 4],
[5, 6]])
[13]:
e2 = np.array([0,1]).reshape((-1,1))
[14]:
A @ e2
[14]:
array([[2],
[4],
[6]])
Extract rows of a matrix by pre-multiplication with standard unit row vector¶
[15]:
e2 = np.array([0,1,0]).reshape((-1, 1))
[16]:
e2.T @ A
[16]:
array([[3, 4]])
Permutation matrices¶
From the column extraction by post-multiplication with a standard unit column vector, we generalize to permutation matrices (identity matrix with permuted columns). Post-multiplication of a matrix \(A\) with a permutation matrix \(P\) rearranges the columns of \(A\). To recover the original matrix, multiply with \(P^T\) - i.e. \(P^{-1} = P^T\) and the inverse of \(P\) is its inverse, \(P\) being our first example of an orthogonal matrix.
[17]:
A = np.arange(1, 17).reshape((4,4))
A
[17]:
array([[ 1, 2, 3, 4],
[ 5, 6, 7, 8],
[ 9, 10, 11, 12],
[13, 14, 15, 16]])
[18]:
I = np.eye(4, dtype='int')
I
[18]:
array([[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]])
[19]:
A @ I
[19]:
array([[ 1, 2, 3, 4],
[ 5, 6, 7, 8],
[ 9, 10, 11, 12],
[13, 14, 15, 16]])
[20]:
p = I[:, [2,1,3,0]]
p
[20]:
array([[0, 0, 0, 1],
[0, 1, 0, 0],
[1, 0, 0, 0],
[0, 0, 1, 0]])
[21]:
A @ p
[21]:
array([[ 3, 2, 4, 1],
[ 7, 6, 8, 5],
[11, 10, 12, 9],
[15, 14, 16, 13]])
[22]:
A @ p @ p.T
[22]:
array([[ 1, 2, 3, 4],
[ 5, 6, 7, 8],
[ 9, 10, 11, 12],
[13, 14, 15, 16]])
Matrix partitioning¶
We see above that matrix multiplication can be seen as separate operations on the row or column vectors. We can actually partition matrices into blocks (not just vectors) for matrix multiplication. Suppose we want to calculate \(AB\), where
We can consider (say) \(A\) and \(B\) as each being a \(2 \times 2\) matrix where each element is a \(2 \times 2\) sub-matrix (or block). This simplifies the computation since many blocks are the identity or null matrix.
and
In fact, we can see by inspection that the result will be
In general, any sub-block structure consistent with matrix multiplication (more formally, \(A\) and \(B\) are conformable for multiplication) is fine. In particular, the blocks do not have to be square.
[23]:
a11 = np.eye(2)
a12 = np.eye(2)
a21 = np.zeros((2,2))
a22 = np.diag((2,3))
b11 = np.array([
[1,2],
[5,6]
])
b12 = np.array([
[3,4],
[7,8]
])
b21 = np.zeros((2,2))
b22 = np.eye(2)
[24]:
A = np.block([
[a11, a12],
[a21, a22]
]).astype('int')
A
[24]:
array([[1, 0, 1, 0],
[0, 1, 0, 1],
[0, 0, 2, 0],
[0, 0, 0, 3]])
[25]:
B = np.block([
[b11, b12],
[b21, b22]
]).astype('int')
B
[25]:
array([[1, 2, 3, 4],
[5, 6, 7, 8],
[0, 0, 1, 0],
[0, 0, 0, 1]])
[26]:
A @ B
[26]:
array([[1, 2, 4, 4],
[5, 6, 7, 9],
[0, 0, 2, 0],
[0, 0, 0, 3]])
[27]:
np.block([
[a11@b11 + a12@b21, a11@b12 + a12@b22],
[a21@b11 + a22@b21, a21@b12 + a22@b22]
]).astype('int')
[27]:
array([[1, 2, 4, 4],
[5, 6, 7, 9],
[0, 0, 2, 0],
[0, 0, 0, 3]])