BDS 754: Python for Data Science


drawing

Topic 14: Array Programming and Numpy

This topic:¶

  1. Vectors and Matrices in Python
  2. Array Programming
  3. Numpy

Readings:

  • data science tutor GPT: https://chatgpt.com/g/g-SSBhmwHol-introductory-data-science-tutor
  • some python and linear algebra review material: https://www.keithdillon.com/index.php/bootcamp/

Numpy/Scipy References

  • "Array programming with NumPy", https://www.nature.com/articles/s41586-020-2649-2
  • http://www.scipy-lectures.org/intro/numpy/numpy.html.
  • http://docs.scipy.org/

Vector Addition $\mathbf x + \mathbf y = \mathbf z$¶

$$ \textbf{x}+\textbf{y} = \textbf{z} \;\;\; \leftrightarrow \;\;\; \begin{bmatrix} x_1 \\x_2 \\ \vdots \\ x_k \end{bmatrix} + \begin{bmatrix} y_1 \\y_2 \\ \vdots \\ y_k \end{bmatrix} = \begin{bmatrix} x_1 + y_1 \\x_y + y_2 \\ \vdots \\ x_k+y_k \end{bmatrix} = \begin{bmatrix} z_1 \\z_2 \\ \vdots \\ z_k \end{bmatrix}, \text{ where $z_j = x_j+y_j$} $$



drawing

Can you add a scalar to a vector? Explain why or why not.

Scalar-Vector multiplication $\alpha \mathbf x = \mathbf y$¶

$$ \alpha \textbf{x} = \textbf{y} \;\;\; \leftrightarrow \;\;\; \alpha \begin{bmatrix} x_1 \\x_2 \\ \vdots \\ x_k \end{bmatrix} = \begin{bmatrix} \alpha x_1 \\\alpha x_2 \\ \vdots \\ \alpha x_k \end{bmatrix} = \begin{bmatrix} y_1 \\y_2 \\ \vdots \\ y_k \end{bmatrix}, \text{ where $y_j = \alpha x_j$} $$

Code these operations in Python¶

Consider vectors describing distance travelled in 2D.

Make two functions:

  • multiply a vector of data by a scalar (i.e. "scale it")
  • add two vectors.

  • Make it work for any number of dimensions

The Dot Product $c = \mathbf x \cdot \mathbf y$¶

If we have two vectors: ${\bf{x}} = \begin{bmatrix} x_1 \\x_2 \\ \vdots \\ x_k \end{bmatrix}$ and ${\bf{y}} = \begin{bmatrix} y_1 \\y_2 \\ \vdots \\ y_k \end{bmatrix}$

The dot product is written: ${\bf{x}} \cdot {\bf{y}} = x_{1}y_{1}+x_{2}y_{2}+\cdots+x_{k}y_{k}$

If $\mathbf{x} \cdot \mathbf{y} = 0$ then $x$ and $y$ are orthogonal

What is $\mathbf{x} \cdot \mathbf{x}$?

Code the dot product in Python¶

  • Choose appropriate data structures for your vectors

  • Make it work for any number of dimensions

Applications¶

You may have heard of this structure:

drawing

Use your dot product function to make an artificial neuron function $f(\mathbf x) = y$

Use the signum function for the activation function.

You may heard modern AI is based on matrix multiplication... this is because matrix multiplication consists of multiple dot products in parallel

Python Lab: DIY Array Programming¶

Make a class for "dense" vectors that contains all your functions thus far.

  1. Vector addition
  2. Scalar-vector multiplication
  3. Dot product

Add dunder support for addition, scalar multiplication. What other possibilities might there be?

Put your class in a separate file in the path so you can import it and use the class.

You have created a starting point for your own homwmade numpy module.

Numpy¶

View numpy as library of efficient vector functions.


drawing

All modern AI tools and much of Data Science tools is based on "drop-in replacements" of numpy. Basically the same basic programming syntax, but coded to run efficiently on GPUs or large server arrays.

https://www.nature.com/articles/s41586-020-2649-2

https://numpy.org/

drawing

Harris, Charles R., et al. "Array programming with NumPy." nature 585.7825 (2020): 357-362.

Dask = python parallel computing library for cluster computing (e.g., at Google etc).

Many popular packages (include popular AI frameworks) are built upon Numpy or "swappable" replacements.

drawing

Harris, Charles R., et al. "Array programming with NumPy." nature 585.7825 (2020): 357-362.

Note that numpy didn't invent everything. It primarily imitated matlab syntax. For the fast algorithm implementations it utilized the same (national lab created) open-source linear algebra libraries that were the basis for matlab.

drawing

NumPy Arrays for Vectors¶

View as wrappers for python lists, similar to our vector class.

In [29]:
import numpy as np

lst = [2,0,1,8]
x = np.array(lst)
x
Out[29]:
array([2, 0, 1, 8])
In [30]:
type(x)
Out[30]:
numpy.ndarray
In [31]:
#help(np.array)

Importing numpy¶

It is very common to import numpy with alias 'np', then call functions using 'np.' prefix. It typically gives access to everything in the module and its submodules (np.random, np.linalg), but may not if they are large and not included by default.

import numpy as np
arr = np.array([1,2,3,4])

You can also import the classes individually

from numpy import array
arr = array([1,2,3,4])
lst = list([1,2,3,4])

You can also import everything, though this risks overwriting names you use. Ultimately they are just more classes like the vector class we created.

from numpy import *

Numpy arrays behave like vectors¶

In [9]:
np.array([2,0,1,8]) + np.array([1,1,1,1])                   
Out[9]:
array([3, 1, 2, 9])
In [8]:
list([2,0,1,8]) + list([1,1,1,1])                   
Out[8]:
[2, 0, 1, 8, 1, 1, 1, 1]
In [13]:
# "Array programming"

x = np.array([2,0,1,8])
y = np.array([1,1,1,1])

z = x + y
print(z)
[3 1 2 9]
In [25]:
v = np.array([1,2,3])
w = np.array([3,4,5])


print(5*v+3*w)
print(5*(v + 3*w) + w + 1) # note scalar addition also
[14 22 30]
[54 75 96]

Scalar-vector multiplication¶

In [11]:
3 * np.array([2,0,1,8])
Out[11]:
array([ 6,  0,  3, 24])
In [10]:
3 * list([2,0,1,8])
Out[10]:
[2, 0, 1, 8, 2, 0, 1, 8, 2, 0, 1, 8]

Dot product¶

This can be defined a number of ways in linear algebra (e.g. $\mathbf x^T \mathbf y$). Will return to this later.

In [13]:
w = np.array([1, 2])
v = np.array([3, 4])
np.dot(w,v)
Out[13]:
11
In [14]:
w = np.array([0, 1])
v = np.array([1, 0])
np.dot(w,v)
Out[14]:
0
In [15]:
w = np.array([1, 2])
v = np.array([-2, 1])
np.dot(w,v)
Out[15]:
0

NumPy arrays are zero-based like Python lists

In [23]:
x = np.array([2,0,1,8])

print(x)
print(x[0],x[3])
[2 0 1 8]
2 8

Also slicing is similar

In [22]:
print(x[0:3])
[2 0 1]

Container type versus data type (dtype)¶

Numpy arrays are classes with their own type

The data can be of various types.

In [33]:
type(x), type(x[0])
Out[33]:
(numpy.ndarray, numpy.int32)
In [37]:
x = np.array([2,0,1,8], dtype=np.int64)
type(x), type(x[0])
Out[37]:
(numpy.ndarray, numpy.int64)

Commonly used number types¶

Various combinations of short vs long (or single versus double) int versus float, signed versus unsigned (for ints).

Details depends on platform (Linux vs Windows vs Mac vs Andriod..) and has evolved over time.

Common names bits Python name Numpy name(s)
boolean (bool) 8 numpy.bool
character (char) 8 numpy.char
integers (int) 32 int numpy.intc, numpy.int32
unsigned integers (uint) 32 numpy.uint32
floating-pointing numbers (float, single) 32,64 float numpy.float32, numpy.float64
double precision floating point numbers (double) 64 numpy.double
short integers (short) 16 numpy.short
long integers (long) 32,64 numpy.long
double long integers (longlong) 64,128? numpy.longlong
unsigned long integers (ulong) 32,64 numpy.ulong

https://en.cppreference.com/w/cpp/language/types </br> https://numpy.org/devdocs/user/basics.types.html

Numpy inf and nan¶

In [138]:
import numpy

-1.0*numpy.float32(0.0)
Out[138]:
-0.0
In [83]:
import numpy

-1./numpy.float32(0.)
C:\Users\micro\AppData\Local\Temp\ipykernel_29412\4148764609.py:3: RuntimeWarning: divide by zero encountered in true_divide
  -1./numpy.float32(0.)
Out[83]:
-inf
In [25]:
0.0/numpy.float32(0.0)
C:\Users\micro\AppData\Local\Temp\ipykernel_29412\3555820988.py:1: RuntimeWarning: invalid value encountered in true_divide
  0.0/numpy.float32(0.0)
Out[25]:
nan
In [26]:
0*numpy.nan
Out[26]:
nan
In [27]:
numpy.inf*numpy.nan
Out[27]:
nan

Checking for nan and inf directly¶

In [139]:
numpy.inf == 1/numpy.float32(0.0)
C:\Users\micro\AppData\Local\Temp\ipykernel_29412\2350908150.py:1: RuntimeWarning: divide by zero encountered in true_divide
  numpy.inf == 1/numpy.float32(0.0)
Out[139]:
True
In [140]:
numpy.nan == numpy.nan # !!
Out[140]:
False
In [141]:
numpy.isnan(numpy.float32(0)/numpy.float32(0))
C:\Users\micro\AppData\Local\Temp\ipykernel_29412\4269917502.py:1: RuntimeWarning: invalid value encountered in float_scalars
  numpy.isnan(numpy.float32(0)/numpy.float32(0))
Out[141]:
True
In [142]:
numpy.isinf(1.0/numpy.float32(0))
C:\Users\micro\AppData\Local\Temp\ipykernel_29412\468334143.py:1: RuntimeWarning: divide by zero encountered in true_divide
  numpy.isinf(1.0/numpy.float32(0))
Out[142]:
True

"Vectorization"¶

Converting loops into linear algebra functions where loop is performed at lower level

E.g. instead of looping over lists, use preprogrammed fast operations on arrays

In [40]:
x = range(0,1000)
alpha = 5

%timeit ax = list([alpha*x_k for x_k in x])
99.1 µs ± 10.5 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
In [41]:
import numpy as np
x = np.arange(0,1000) # <-- conceptually same as np.array(range(0,1000)) 
alpha = 5

%timeit ax = alpha*x
1.75 µs ± 433 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

Universal Functions (ufuncs) = fast elementwise operations¶

https://numpy.org/doc/stable/reference/ufuncs.html

Many common operations are implemented efficiently in numpy on elementwise basis

In [61]:
type(np.cos)
Out[61]:
numpy.ufunc
In [62]:
v = np.array([1,2,3,4])
np.cos(np.array(v))
Out[62]:
array([ 0.54030231, -0.41614684, -0.9899925 , -0.65364362])
In [63]:
np.log(np.array(v))
Out[63]:
array([0.        , 0.69314718, 1.09861229, 1.38629436])
In [64]:
np.exp([1,2,3,4]) # note can pass in lists
Out[64]:
array([ 2.71828183,  7.3890561 , 20.08553692, 54.59815003])
In [65]:
np.add((1,2,3,4),(5,6,7,8)) # or tuples
Out[65]:
array([ 6,  8, 10, 12])

Many are also supported via dunders for array programming

In [54]:
v = [1,2,3,4,5]

np.power(v,2), np.array(v)**2, np.array(v).__pow__(2)
Out[54]:
(array([ 1,  4,  9, 16, 25], dtype=int32),
 array([ 1,  4,  9, 16, 25], dtype=int32),
 array([ 1,  4,  9, 16, 25], dtype=int32))
In [14]:
v = range(0,1000)

%timeit v2 = list([x**2 for x in v])
266 µs ± 7.21 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
In [4]:
import numpy as np
v = np.arange(0,1000)

%timeit v2 = v**2
1.33 µs ± 18.4 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

Custom elementwise ops: vectorize¶

https://numpy.org/doc/stable/reference/generated/numpy.vectorize.html

f_vectorized = np.vectorize(f)
In [44]:
import numpy as np
import math

def myfun(x):
    if x>0:
        return math.sqrt(x)
    else:
        return 0

myvfun = np.vectorize(myfun)

myvfun([1,2,3,4])
Out[44]:
array([1.        , 1.41421356, 1.73205081, 2.        ])

Matrices¶

A matrix $\mathbf A$ is a rectangular array of numbers, of size $m \times n$ as follows:

$\mathbf A = \begin{bmatrix} A_{1,1} & A_{1,2} & A_{1,3} & \dots & A_{1,n} \\ A_{2,1} & A_{2,2} & A_{2,3} & \dots & A_{2,n} \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ A_{m,1} & A_{m,2} & A_{m,3} & \dots & A_{m,n} \end{bmatrix}$

Where the numbers $A_{ij}$ are called the elements of the matrix. We describe matrices as wide if $n > m$ and tall if $n < m$. They are square iff $n = m$.

NOTE: naming convention for scalars vs. vectors vs. matrices.

Matrix representations¶

Modern computer memory is one-dimensional, so must define meaning of rows vs columns

List of lists (but is it a list of rows or of columns?)

In [56]:
A = [[1,2,3],[4,5,6],[7,8,9]]

How do you select the $(i,j)$th element?

What if we simply stored the matrix $A$ as a 1d Array? Now how do you select the $(i,j)$th element?

Can convert that list of lists into a numpy matrix

In [5]:
A = [[1,2,3],[4,5,6],[7,8,9]]

import numpy as np
np.array(A)
Out[5]:
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])

Aside: Numpy matrix class¶

In [9]:
import numpy as np

np.matrix([[1,2,3],[4,5,6],[7,8,9]]) 
Out[9]:
matrix([[1, 2, 3],
        [4, 5, 6],
        [7, 8, 9]])

https://numpy.org/doc/stable/reference/generated/numpy.matrix.html

"Note: It is no longer recommended to use this class, even for linear algebra. Instead use regular arrays. The class may be removed in the future."

https://stackoverflow.com/questions/53254738/deprecation-status-of-the-numpy-matrix-class

Numpy n-dimensional arrays¶

In [58]:
import numpy as np
np.array([[1,2,3],[4,5,6],[7,8,9]]) 
Out[58]:
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])
In [60]:
np.array([[[1,2,3],[4,5,6],[7,8,9]],[[0,2,0],[4,0,6],[0,8,9]]])
Out[60]:
array([[[1, 2, 3],
        [4, 5, 6],
        [7, 8, 9]],

       [[0, 2, 0],
        [4, 0, 6],
        [0, 8, 9]]])

ndarray size¶

the length of the "raw data"

In [191]:
np.array([2,0,1,8]).size
Out[191]:
4
In [193]:
np.array([[[2,0,1,8]]]).size
Out[193]:
4
In [194]:
np.array([[[1,2,3],[4,5,6],[7,8,9]],[[0,2,0],[4,0,6],[0,8,9]]]).size
Out[194]:
18

ndim¶

the number of dims. Think of as number of nested lists

In [195]:
np.array([2,0,1,8]).ndim
Out[195]:
1
In [196]:
np.array([[[1,2,3],[4,5,6],[7,8,9]],[[0,2,0],[4,0,6],[0,8,9]]]).ndim
Out[196]:
3
In [197]:
np.array([[2,0,1,8]]).ndim
Out[197]:
2
In [198]:
np.array([[[2,0,1,8]]]).ndim
Out[198]:
3

ndarray shapes¶

If view $A$ as a $m \times n$ matrix, A.shape returns a tuple $(m,n)$

In [67]:
A = np.array([[1,2],[4,5],[7,8]]) 

print(A)
print('')
print(A.shape)
[[1 2]
 [4 5]
 [7 8]]

(3, 2)

Note if view as list of lists, shape elements are outermost list size first

In [10]:
T = np.array([[[1,2],[3,4],[5,6],[1,2],[3,4],[5,6]],[[1,2],[3,4],[5,6],[1,2],[3,4],[5,6]]])
print(T)
print('')
print(T.shape)
[[[1 2]
  [3 4]
  [5 6]
  [1 2]
  [3 4]
  [5 6]]

 [[1 2]
  [3 4]
  [5 6]
  [1 2]
  [3 4]
  [5 6]]]

(2, 6, 2)

Shape for "flat" arrays¶

In [68]:
np.array([1,2,3,4]).shape
Out[68]:
(4,)
In [89]:
tuple([4])
Out[89]:
(4,)

row/column vectors as matrices¶

In [73]:
np.array([[1,2,3,4]]).shape
Out[73]:
(1, 4)
In [74]:
np.array([[1],[2],[3],[4]]).shape
Out[74]:
(4, 1)
In [78]:
print(np.array([[1,2,3,4]]))
print(np.array([[1],[2],[3],[4]]))
[[1 2 3 4]]
[[1]
 [2]
 [3]
 [4]]

Numpy slicing¶

Similar to lists. Supports index tuples also

In [13]:
A = np.array([[1,2,3],[4,5,6],[7,8,9]])
print(A)
[[1 2 3]
 [4 5 6]
 [7 8 9]]
In [14]:
print(A[1][2]) # list-of-list style (zero-based of course)
6
In [15]:
print(A[1,2]) # tuple style (also matlab)
6
In [16]:
print(A[(1,2)]) # explicit tuple
6

tuple-style supports slicing across rows as well as cols¶

In [17]:
print(A)
print('')
print(A[:,0]) # slice all rows, 3th column ~ A[0:3,0]
[[1 2 3]
 [4 5 6]
 [7 8 9]]

[1 4 7]
In [18]:
print(A[1,:])
[4 5 6]

list-of-lists does not support slicing across cols properly¶

In [19]:
print(A)
print('')

print(A[1][:]) # ok
[[1 2 3]
 [4 5 6]
 [7 8 9]]

[4 5 6]
In [20]:
print(A[:][0]) # wrong! 
[1 2 3]

what did this do? hint: view as (A[:])[0]

general submatrix slicing¶

In [21]:
print(A)
print('')

print(A[1:,0:2])
[[1 2 3]
 [4 5 6]
 [7 8 9]]

[[4 5]
 [7 8]]
In [22]:
print(A[:2,:2])
[[1 2]
 [4 5]]

rows or cols as submatrices¶

In [25]:
print(A)
print('')
print(A[:,0:1])
[[1 2 3]
 [4 5 6]
 [7 8 9]]

[[1]
 [4]
 [7]]
In [26]:
print(A[:,0])
[1 4 7]

NumPy for Matlab Users¶

drawing

Slicing using a mask¶

In [130]:
mask = np.array([[True, True, False],[True,True,False],[False,False,False]])
print(mask)
[[ True  True False]
 [ True  True False]
 [False False False]]
In [131]:
print(A)
print('')
A[mask]
[[1 2 3]
 [4 5 6]
 [7 8 9]]

Out[131]:
array([1, 2, 4, 5])

Notice it comes out flat

handy when combined with logic

In [127]:
v = np.arange(10)
print(v)
print(v>=5)
print(v[v>=5])
[0 1 2 3 4 5 6 7 8 9]
[False False False False False  True  True  True  True  True]
[5 6 7 8 9]

Converting matrices (or beyond) into 1D arrays¶

A.flatten() - returns flattened copy

A.ravel() - returns reference of original, but as if flat

In [27]:
A = np.array([[1,2,3],[4,5,6]])

print(A)

A.flatten(), A.shape, A.flatten().shape # note returned, not in-place
[[1 2 3]
 [4 5 6]]
Out[27]:
(array([1, 2, 3, 4, 5, 6]), (2, 3), (6,))
In [28]:
A.ravel(), A.shape, A.ravel().shape # note returned, not in-place
Out[28]:
(array([1, 2, 3, 4, 5, 6]), (2, 3), (6,))
In [30]:
A.flatten()[4], A.ravel()[4]
Out[30]:
(5, 5)
In [138]:
aflat = A.flatten()

aflat[0] = -1
print(A)
print(aflat)
[[1 2 3]
 [4 5 6]]
[-1  2  3  4  5  6]
In [139]:
arav = A.ravel()

arav[0] = -1
print(A)
print(arav)
[[-1  2  3]
 [ 4  5  6]]
[-1  2  3  4  5  6]

This kind of reference to same memory but treating as if different shape is called a "view".

Reshaping vectors into matrices and whatever else you want¶

A.reshape(N,M,...)

In [63]:
A = np.array([[1,2,3],[4,5,6]])
print(A)
[[1 2 3]
 [4 5 6]]
In [62]:
x = np.array([1,2,3,4])
x = x.reshape(4,1) # note returned, not in-place
print(x)
[[1]
 [2]
 [3]
 [4]]
In [31]:
x = np.array([1,2,3,4])
x = x.reshape(4,) # note returned, not in-place
print(x)
[1 2 3 4]
In [65]:
print(x.reshape(2,2))
[[1 2]
 [3 4]]
In [140]:
print(x.reshape(1,1,2,2))
[[[[1 2]
   [3 4]]]]

Matrix Transpose¶

The transpose of a matrix $\textit{A}$ is formed by interchanging the rows and columns of $\textit{A}$. That is

$a_{ij}^T = a_{ji}$

Example 1:¶

$\textit{A} = \begin{bmatrix} 1 & 2 \\ 0 & 1 \\ \end{bmatrix}$

$\textit{A}^{T} = ?$

Transpose in numpy¶

Three ways: A.T, A.transpose(), np.transpose(A)

In [42]:
x = np.array([[1,2,3]])
print(x)
print(x.T)
print(x.transpose())
print(np.transpose(x))
[[1 2 3]]
[[1]
 [2]
 [3]]
[[1]
 [2]
 [3]]
[[1]
 [2]
 [3]]
In [38]:
x = np.array([[1,2,3]])
print(x)
print(x.T)
print (x.shape, x.T.shape)
[[1 2 3]]
[[1]
 [2]
 [3]]
(1, 3) (3, 1)
In [39]:
x = np.array([1,2,3,4])
print(x)
print(x.T)
print(x.shape, x.T.shape)
[1 2 3 4]
[1 2 3 4]
(4,) (4,)

Elementwise Matrix Ops¶

ufuncs again

sizes must agree.

Scalar Multiplication: $\textit{B} = \alpha\ \textit{A}$ or $\textit{B} = \textit{A} \alpha$ with components $b_{ij} = \alpha a_{ij}$.

Matrix Addition: $\textit{C} = \textit{A} + \textit{B}$ with components $c_{ij} = a_{ij}+b_{ij}$.

Hadamard product (aka elementwise product): $\textit{C} = \textit{A} \odot \textit{B}$ with components $c_{ij} = a_{ij}b_{ij}$.

Can easily implement these manually on a list of lists by looping over elements

use numpy array programming to demonstrate an example for each of these

Extending elementwise ops with Broadcasting¶

Recall scalar-vector addition, it seems natural to do the following

$$ \alpha + \textbf{x} \;\;\; \rightarrow \;\;\; \alpha + \begin{bmatrix} x_1 \\x_2 \\ \vdots \\ x_k \end{bmatrix} \;\;\; \leftrightarrow \;\;\; \begin{bmatrix} \alpha + x_1 \\\alpha + x_2 \\ \vdots \\ \alpha + x_k \end{bmatrix} $$

view as combination of broadcasting of scalar to vector, following by vector-vector addition (i.e., elementwise addition)

$$ \alpha + \begin{bmatrix} x_1 \\x_2 \\ \vdots \\ x_k \end{bmatrix} \;\;\; \rightarrow \;\;\; \begin{bmatrix} \alpha \\\alpha \\ \vdots \\ \alpha \end{bmatrix} + \begin{bmatrix} x_1 \\x_2 \\ \vdots \\ x_k \end{bmatrix} = \begin{bmatrix} \alpha + x_1 \\\alpha + x_2 \\ \vdots \\ \alpha + x_k \end{bmatrix} $$

Broadcasting generalized¶

Add (or Multiply) matrices or vectrors in an element-wise manner by repeating "singular" dimensions.

drawing

Matlab and Numpy automatically do this now.

Also (and previously) in matlab, e.g.: "brcm(@plus, A, x). numpy.broadcast

In [58]:
w = np.array([[2,-6],[-1, 4]])
v = np.array([1,0])

print(w)
print('')
print(w+v)
[[ 2 -6]
 [-1  4]]

[[ 3 -6]
 [ 0  4]]
In [59]:
print(w*v)
[[ 2  0]
 [-1  0]]
In [60]:
print(np.multiply(w,v)) # ufunc version. supports broadcasting
[[ 2  0]
 [-1  0]]

Matrix-Vector Multiplication¶

If $A$ is an $m \times n$ matrix and $\mathbf{v}$ is a vector of size $n$:

$$\mathbf{b} = A \mathbf{v}$$$$b_i = (A \mathbf{v})_i = \sum_{j=1}^{n} a_{ij} v_j $$

Two perspectives¶

  1. Linear combination of columns

  2. Dot product of vector with rows of matrix

$\begin{bmatrix} 2 & -6 \\ -1 & 4\\ \end{bmatrix} \begin{bmatrix} 2 \\ -1 \\ \end{bmatrix} = ?$

Test both ways out.

You can easily implement both approaches with your vector class

Matrix equations $\mathbf A \mathbf x = \mathbf b$¶

Can view a matrix as list of rows or list of columns

Hence can view this system two different ways.

Perspective 1: Dot products ("System of Linear equations")¶

$$ \mathbf a^{(1)} \cdot \mathbf x = b_1 \\ \mathbf a^{(2)} \cdot \mathbf x = b_2 \\ \vdots \\ \mathbf a^{(m)} \cdot \mathbf x = b_m \\ $$

where $\mathbf a^{(i)}$ are rows of $\mathbf A$

Perspective 2: Linear combination of columns¶

$$ x_1 \mathbf a_1 + x_2 \mathbf a_2 + ... + x_n \mathbf a_n = \mathbf b $$

where $\mathbf a_1$ are columns of $\mathbf A$.

vector-matrix multiplication in numpy¶

A special case of matrix-matrix multiplication (coming up)

In [149]:
A = np.array([[1,2,3],[4,5,6],[7,8,9]])

v = np.array([1,0,0])

A@v # array programming approach
Out[149]:
array([1, 4, 7])
In [147]:
np.matmul(A,v)
Out[147]:
array([1, 4, 7])
In [148]:
np.dot(A,v)
Out[148]:
array([1, 4, 7])

what is the rule for shape matching in these products?

vector-matrix versus matrix-vector products¶

In [157]:
A = np.array([[1,2,3],[4,5,6],[7,8,9]])
x = np.array([1,0,0])
print(A,'\n')
print(x,'\n')
print(A@x,'\n') # right multiply (implies column vector)
print(x@A) # left multiply (implies row vector)
[[1 2 3]
 [4 5 6]
 [7 8 9]] 

[1 0 0] 

[1 4 7] 

[1 2 3]

Matrix Multiplication¶

Multiplication of an $m \times n$ matrix $\textit{A}$ and a $n \times k$ matrix $\textit{B}$ is a $m \times k$ matrix $\textit{C}$, written $\textit{C} = \textit{A}\textit{B}$, whose elements $C_{ij}$ are

$$ C_{i,j} = \sum_{l=1}^n A_{i,l}B_{l,j} $$
  • $C_{ij}$ as dot (inner) products of $i$th row of $A$ with $j$th column of $B$
  • Each column of $C$ as linear combination of columns of $A$, weighted by elements of corresponding column of $B$
  • Each row of $C$ as linear combination of rows of $B$, weighted by elements of corresponding row of $A$
  • $C$ as sum over outer products of $l$th column of $A$ with $l$th row of $B$

Note that $\textit{B}\textit{A} \neq \textit{A}\textit{B}$ in general.

Example 1:¶

$\textit{C} = \textit{A}\textit{B} = \begin{bmatrix} 1 & 2 \\ 0 & 1 \\ \end{bmatrix} \begin{bmatrix} 2 & 0 \\ 1 & 4 \\ \end{bmatrix} = \begin{bmatrix} 4 & 8 \\ 1 & 4 \\ \end{bmatrix}$

Example 2:¶

$\begin{bmatrix} 1 & 2 \\ 0 & -3 \\ 3 & 1 \\ \end{bmatrix} \begin{bmatrix} 2 & 6 & -3 \\ 1 & 4 & 0 \\ \end{bmatrix} = ?$

numpy matrix multiplication¶

In [161]:
A = np.array([[1,2],[3, 4]])
B = np.array([[1,0],[2, 1]])
print(A@B)
[[ 5  2]
 [11  4]]
In [162]:
print(np.matmul(A,B))
[[ 5  2]
 [11  4]]
In [163]:
print(np.dot(A,B))
[[ 5  2]
 [11  4]]

vector matrix products as special case of matrix-matrix multiplication¶

In [173]:
A = np.array([[ 2, -6],
              [-1,  4]])

x = np.array([[+1], 
              [-1]])
In [171]:
A@x
Out[171]:
array([[ 8],
       [-5]])
In [172]:
x@A
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
~\AppData\Local\Temp\ipykernel_7112\299605771.py in <module>
----> 1 x@A

ValueError: matmul: Input operand 1 has a mismatch in its core dimension 0, with gufunc signature (n?,k),(k,m?)->(n?,m?) (size 2 is different from 1)

inner and outer products¶

In [174]:
vrow = np.array([[1,2,3,4]])
print(vrow)
vcol = np.array([[1],[2],[3],[4]])
print(vcol)
[[1 2 3 4]]
[[1]
 [2]
 [3]
 [4]]
In [175]:
vrow@vcol
Out[175]:
array([[30]])
In [176]:
vcol@vrow
Out[176]:
array([[ 1,  2,  3,  4],
       [ 2,  4,  6,  8],
       [ 3,  6,  9, 12],
       [ 4,  8, 12, 16]])
In [178]:
np.array([1,2,3,4])@np.array([1,2,3,4]) # 1D arrays - treated as inner product
Out[178]:
30

Famous matrices in NumPy¶

In [20]:
np.zeros((3,4))
Out[20]:
array([[0., 0., 0., 0.],
       [0., 0., 0., 0.],
       [0., 0., 0., 0.]])
In [21]:
np.ones((3,4))
Out[21]:
array([[1., 1., 1., 1.],
       [1., 1., 1., 1.],
       [1., 1., 1., 1.]])
In [17]:
D = np.diag([1,2,3])
print(D)
[[1 0 0]
 [0 2 0]
 [0 0 3]]
In [18]:
np.diag(D)
Out[18]:
array([1, 2, 3])

Identity Matrix¶

$I_3 = \begin{bmatrix} 1 & 0 & 0\\ 0 & 1 & 0 \\ 0 & 0 & 1\\ \end{bmatrix}$

Here we can write $\textit{I}\textit{B} = \textit{B}\textit{I} = \textit{B}$ or $\textit{I}\textit{I} = \textit{I}$

Again, it is important to reiterate that matrices are not in general commutative with respect to multiplication. That is to say that the left and right products of matrices are, in general different.

$AB \neq BA$

In [16]:
import numpy as np
np.eye(3)
Out[16]:
array([[1., 0., 0.],
       [0., 1., 0.],
       [0., 0., 1.]])

random matrices¶

In [22]:
# rand = uniform random numbers in [0,1]

np.random.rand(3,4) # note two inputs, not tuple
Out[22]:
array([[0.99147344, 0.01075122, 0.81974321, 0.10942128],
       [0.94041317, 0.50408574, 0.71896232, 0.12113072],
       [0.77483078, 0.32627551, 0.44489225, 0.13208849]])
In [23]:
# randn ~ N(0,1)

np.random.randn(3,4)
Out[23]:
array([[ 1.09642959,  1.40715723, -0.30932909,  0.66846735],
       [ 0.22536484, -0.08283707, -0.60713286, -0.16836462],
       [-1.20191307,  1.53390883, -0.48587343, -0.84379159]])

Question:¶

What is the product of $\begin{bmatrix} 2 & -6 \\ -1 & 4\\ \end{bmatrix}$ and $\begin{bmatrix} -1 & 1 \\ \end{bmatrix}$ in python?

If question seems vague, list all possible ways to address this question.

Lab: Numpy practice¶

Implement the "linear algebra with Python" Exercises from the bootcamp using numpy:

https://www.keithdillon.com/classes/UMMC/Bootcamp/__python_exercises_01.html

Recap¶

  • array programming is intuitive and fast with numpy, ufuncs + linear algebra
  • zero-based indexing, list-style slicing, now with tuples for nested lists
  • "row major" is normal presumed orientation (based on indexing, repr printout)
  • "1D" versus ndarrays (e.g., vector as 1D array versus vector as N-by-1 matrix)
  • multiplication can be tricky, broadcasting versus matrix product. be careful of shapes

Many popular packages (include popular AI frameworks) are built upon Numpy or "swappable" replacements.

drawing

Harris, Charles R., et al. "Array programming with NumPy." nature 585.7825 (2020): 357-362.

drawing

Harris, Charles R., et al. "Array programming with NumPy." nature 585.7825 (2020): 357-362.

Dask = python parallel computing library for cluster computing (e.g., at Google etc).

$\verb|numpy.linalg|$¶

  • Use BLAS and LAPACK to provide efficient low level implementations of standard linear algebra algorithms.
  • Those libraries may be provided by NumPy itself using C versions of a subset of their reference implementations
  • When possible, highly optimized libraries that take advantage of specialized processor functionality are preferred.
  • Examples of such libraries are OpenBLAS, MKL (TM), and ATLAS.

https://docs.scipy.org/doc/numpy/reference/routines.linalg.html

Matrix and vector products¶

  • dot(a, b[, out]) Dot product of two arrays.
  • linalg.multi_dot(arrays) Compute the dot product of two or more arrays in a single function call, while automatically selecting the fastest evaluation order.
  • vdot(a, b) Return the dot product of two vectors.
  • inner(a, b) Inner product of two arrays.
  • outer(a, b[, out]) Compute the outer product of two vectors.
  • matmul(x1, x2, /[, out, casting, order, …]) Matrix product of two arrays.
  • tensordot(a, b[, axes]) Compute tensor dot product along specified axes.
  • einsum(subscripts, *operands[, out, dtype, …]) Evaluates the Einstein summation convention on the operands.
  • einsum_path(subscripts, *operands[, optimize]) Evaluates the lowest cost contraction order for an einsum expression by considering the creation of intermediate arrays.
  • linalg.matrix_power(a, n) Raise a square matrix to the (integer) power n.
  • kron(a, b) Kronecker product of two arrays.

Decompositions¶

  • linalg.cholesky(a) Cholesky decomposition.
  • linalg.qr(a[, mode]) Compute the qr factorization of a matrix.
  • linalg.svd(a[, full_matrices, compute_uv, …]) Singular Value Decomposition.

Matrix eigenvalues¶

  • linalg.eig(a) Compute the eigenvalues and right eigenvectors of a square array.
  • linalg.eigh(a[, UPLO]) Return the eigenvalues and eigenvectors of a complex Hermitian (conjugate symmetric) or a real symmetric matrix.
  • linalg.eigvals(a) Compute the eigenvalues of a general matrix.
  • linalg.eigvalsh(a[, UPLO]) Compute the eigenvalues of a complex Hermitian or real symmetric matrix.

Norms and other numbers¶

  • linalg.norm(x[, ord, axis, keepdims]) Matrix or vector norm.
  • linalg.cond(x[, p]) Compute the condition number of a matrix.
  • linalg.det(a) Compute the determinant of an array.
  • linalg.matrix_rank(M[, tol, hermitian]) Return matrix rank of array using SVD method
  • linalg.slogdet(a) Compute the sign and (natural) logarithm of the determinant of an array.
  • trace(a[, offset, axis1, axis2, dtype, out]) Return the sum along diagonals of the array.

Solving equations and inverting matrices¶

  • linalg.solve(a, b) Solve a linear matrix equation, or system of linear scalar equations.
  • linalg.tensorsolve(a, b[, axes]) Solve the tensor equation a x = b for x.
  • linalg.lstsq(a, b[, rcond]) Return the least-squares solution to a linear matrix equation.
  • linalg.inv(a) Compute the (multiplicative) inverse of a matrix.
  • linalg.pinv(a[, rcond, hermitian]) Compute the (Moore-Penrose) pseudo-inverse of a matrix.
  • linalg.tensorinv(a[, ind]) Compute the ‘inverse’ of an N-dimensional array.