More Matrix Math in Python

Beginning

This is another lab from Coursera's NLP Specialization. This time it's about using numpy to perform vector operations.

Imports

# python
from argparse import Namespace
from functools import partial

import math

# from pypi
import hvplot.pandas
import numpy
import pandas

# my stuff
from graeae import EmbedHoloviews

Set Up

Plotting

SLUG = "more-matrix-math-in-python"
Embed = partial(EmbedHoloviews, folder_path=f"files/posts/nlp/{SLUG}")
Plot = Namespace(
    width=990,
    height=780,
    fontscale=2,
    tan="#ddb377",
    blue="#4687b7",
    red="#ce7b6d",
 )

Middle

Let's start with a simple matrix. We'll call it R because when we do our machine translation we'll need a rotation matrix which is named R.

R = numpy.array([[2, 0],
                 [0, -2]])

Now we'll create another matrix.

x = numpy.array([[1, 1]])
print(x.shape)
(1, 2)

Note the nested square brackets, this makes it a matrix and not a vector.

The Dot Product

y = numpy.dot(x, R)
print(y)
[[ 2 -2]]

The rotation matrix (R) rotates and scales the matrix x. To see the effect we can plot the original vector x and the rotated version y.

X = pandas.DataFrame(dict(X=[0, x[0][0]], Y=[0, x[0][1]]))
Y = pandas.DataFrame(dict(X=[0, y[0][0]], Y=[0, y[0][1]]))

x_plot = X.hvplot(x="X", y="Y", color=Plot.blue)
y_plot = Y.hvplot(x="X", y="Y", color=Plot.red)

plot = (x_plot * y_plot).opts(
    title="Original and Rotated Vectors",
    width=Plot.width,
    height=Plot.height,
    fontscale=Plot.fontscale,
    xlim=(-2, 2),
    ylim=(-2, 2)
)

outcome = Embed(plot=plot, file_name="original_and_rotate_vectors")()
print(outcome)

Figure Missing

The blue segment is the original vector and the red is the rotated and scaled vector.

More Rotations

In the previous section we rotated the vector using integer values, but if we wanted to rotate the vector a specific number of degrees then the way to do that is to use a rotation matrix.

\[ Ro = \begin{bmatrix} cos \theta & -sin \theta \\ sin \theta & cos \theta \end{bmatrix} \]

Let's start with a vector and rotate it \(100^o\).

theta = math.radians(100)
Ro = pandas.DataFrame([[numpy.cos(theta), -numpy.sin(theta)],
                  [numpy.sin(theta), numpy.cos(theta)]])

x_2 = pandas.Series([2, 2])
y_2 = x_2.dot(Ro)
print("The Rotation Matrix")
print(Ro)
print("\nThe Rotated Vector")
print(y_2)

print(f'\n x2 norm {numpy.linalg.norm(x_2)}')
print(f'\n y2 norm {numpy.linalg.norm(y_2)}')
print(f'\n Rotation matrix norm {numpy.linalg.norm(Ro)}')
print(f" Square Root of 2: {2**0.5}")
The Rotation Matrix
          0         1
0 -0.173648 -0.984808
1  0.984808 -0.173648

The Rotated Vector
0    1.622319
1   -2.316912
dtype: float64

 x2 norm 2.8284271247461903

 y2 norm 2.82842712474619

 Rotation matrix norm 1.414213562373095
 Square Root of 2: 1.4142135623730951

You can see that in this case our transformed vector (y2) didn't change in length the way it did in the previous example. Let's plot it and see what it looks like.

origin = pandas.DataFrame([[0, 0]])
X = origin.append(x_2, ignore_index=True)
Y = origin.append(y_2, ignore_index=True)
COLUMNS = "X Y".split()

X.columns = COLUMNS
Y.columns = COLUMNS

x_plot = X.hvplot(x="X", y="Y", color=Plot.blue)
y_plot = Y.hvplot(x="X", y="Y", color=Plot.red)

plot = (x_plot * y_plot).opts(
    title="100 Degree rotation",
    width=Plot.width,
    height=Plot.height,
    fontscale=Plot.fontscale,
    xlim=(-3, 3),
    ylim=(-3, 3)
)

outcome = Embed(plot=plot, file_name="one_hundred_degree_rotation")()
print(outcome)

Figure Missing

Rotation matrices rotate anti-clockwise, which makes that look like more than a 100 degree rotation. I'm going to have to figure that out.

The Frobenius Norm

\[ \| \vec a \| = \sqrt {{\vec a} \cdot {\vec a}} \]

For an \(R_2\) matrix, the Frobenius Norm looks like this:

\[ \| \mathrm{A} \|_{F} \equiv \sqrt{\sum_{i=1}^{m} \sum_{j=1}^{n}\left|a_{i j}\right|^{2}} \]

We can translate the second equation directly to numpy.

some_array = numpy.array([[2, 2],
                          [2, 2]])
frobenius_norm = numpy.sqrt(numpy.sum(numpy.square(some_array)))

print(f"The Frobenius Norm = {frobenius_norm}")
The Frobenius Norm = 4.0

So, you might be thinking, we've been using numpy.linalg.norm all this time, what's the difference?

old_norm = numpy.linalg.norm(some_array)
print(old_norm)
assert old_norm == frobenius_norm
4.0

It turns out that the default for norm is the Frobenius Norm so you can calculate it either way.