Skip to article content

Neural Networks

Back to Article
Visualizing eigen vectors
Download Notebook

Visualizing eigen vectors

Helper functions

import numpy as np
def random_symmetric_matrix(n):
    A = np.random.rand(n, n)
    return A + A.T

def random_positive_definite_matrix(n):
    A = np.random.rand(n, n)
    return A.T @ A
import matplotlib.pyplot as plt
def plot_contour(ax, func):
    x, y = np.mgrid[-2:2:21j,
                    -2:2:21j]
    bfx = np.concatenate([x[..., None], y[..., None]], axis=-1)
    f = func(bfx)
    ctr = ax.contour(x, y, f, 20, cmap='Blues_r')
    plt.clabel(ctr, ctr.levels, inline=True, fontsize=6)
    ax.set_xlabel('$x$')
    ax.set_ylabel('$y$')
    return ax


def draw_arrows(ax, xs):
    ax.arrow(0, 0, xs[0, 0], xs[0, 1], color='r', head_width=0.05)
    ax.arrow(0, 0, xs[1, 0], xs[1, 1], color='y', head_width=0.05)
    ax.axis('equal')
    return ax
def f(x, A=np.array([[2, -1], [-1, 3]])):
    # x is m x m x n tensor
    x_row_vec = x[..., None, :] # m x m x 1 x n
    x_col_vec = x[..., :, None] # m x m x n x 1
    res = x_row_vec @ A @ x_col_vec # m x m x 1 x 1
    return res[..., 0, 0] # m x m

fig, ax = plt.subplots()
plot_contour(ax, f)
ax
<Axes: xlabel='$x$', ylabel='$y$'>
<Figure size 640x480 with 1 Axes>

Eigeven value decomposition

Recall that eigen values λR\lambda \in \bbR and eigen vectors vRn\bfv \in \bbR^n are the solutions to the equation,

Av=λvA \bfv = \lambda \bfv

There are nn such solutions let λi\lambda_i vi\bfv_i for i{1,,n}i \in \{ 1, \dots, n\} be such solutions.

Av1=λ1v1Av2=λ2v2Avn=λnvn\begin{align} A\bfv_1 &= \lambda_1 \bfv_1 \\ A\bfv_2 &= \lambda_2 \bfv_2 \\ \vdots &\\ A\bfv_n &= \lambda_n \bfv_n \end{align}

You can arrange these equations in a matrix $$

[Av1Av2Avn]\begin{bmatrix} A\bfv_1 & A\bfv_2 & \dots & A\bfv_n \end{bmatrix}

=

[λ1v1λ2v2λnvn]\begin{bmatrix} \lambda_1\bfv_1 & \lambda_2\bfv_2 & \dots & \lambda_n\bfv_n \end{bmatrix}

$$

You can take AA common from the left hand side. On the right hand side, you will have to construct a diagonal matrix of λi\lambda_i for factorizing eigen vectors and eigen values.

A[v1v2vn]V=[v1v2vn]V[λ1000λ2000λn]ΛA \underbrace{ \begin{bmatrix} \bfv_1 & \bfv_2 & \dots & \bfv_n \end{bmatrix}}_{V} = \underbrace{ \begin{bmatrix} \bfv_1 & \bfv_2 & \dots & \bfv_n \end{bmatrix}}_{V} \underbrace{ \begin{bmatrix} \lambda_1 & 0 & \dots & 0\\ 0 & \lambda_2 & \dots & 0\\ \vdots & \vdots & \ddots & \vdots \\ 0 & 0 & \dots & \lambda_n \end{bmatrix}}_{\Lambda}

Homework (VisEigenVectors): Problem 1

(10 marks)

Prove that

[λ1v1λ2v2λnvn]=[v1v2vn][λ1000λ2000λn]\begin{bmatrix} \lambda_1\bfv_1 & \lambda_2\bfv_2 & \dots & \lambda_n\bfv_n \end{bmatrix} = \begin{bmatrix} \bfv_1 & \bfv_2 & \dots & \bfv_n \end{bmatrix} \begin{bmatrix} \lambda_1 & 0 & \dots & 0\\ 0 & \lambda_2 & \dots & 0\\ \vdots & \vdots & \ddots & \vdots \\ 0 & 0 & \dots & \lambda_n \end{bmatrix}

Verify that the above matrix multiplication gives the same results.

AV=VΛA V = V\Lambda

VV is a matrix of eigen vectors arranged as columns. And Λ\Lambda is a diagonal matrix of eigen values.

If the matrix VV is invertible, then we can right multiply V1V^{-1} to both sides,

A=VΛV1A = V \Lambda V^{-1}

We have found a way to factorize (or decompose) a square matrix AA. This particular decomposition is called Eigen value decomposition. When such a decomposition exists, the matrix AA is called diagonalizable. A generalized form of such decomposition also exists, which is called generalized eigen value decomposition, where the middle matrix is a block diagonal matrix, not diagonal matrix.

Theorem: Symmeteric matrices have orthonormal Eigen vectors

When a square matrix ARn×nA \in \bbR^{n \times n} is real and symmetric, then its Eigen value decomposition exists, AA is diagonalizable, and the eigen matrix VV is an orthogonal matrix.

If the matrix AA is symmetric,

A=VΛVA = V\Lambda V^\top

where V1=VV^{-1} = V^\top.

Proof:

Proof will take too much time out of the class. Interested readers are encouraged to read about Gram-Schmidt orthogonalization, Schur’s lemma and Spectral theorem.

Definition (Orthonormal)

A matrix VRn×nV \in \bbR^{n \times n} is called orthonormal or orthogonal if VV=InV^\top V = I_n equivalently, V1=VV^{-1} = V^\top.

Example

Consider the quadratic form of a symmetric matrix ARn×nA \in \bbR^{n \times n},

f(x)=xAxf(\bfx) = \bfx^\top A \bfx

Because the matrix AA is symmetric, we can write

f(x)=xVΛVx=(Vx)Λ(Vx)f(\bfx) = \bfx^\top V\Lambda V^\top \bfx = (V\bfx)^\top \Lambda (V\bfx)

for eigen value decomposition of A=VΛVA = V\Lambda V^\top

A = np.array([[2, -1],
             [-1, 3]])
A
array([[ 2, -1], [-1, 3]])

np.linalg.eigh finds Eigen values of a Hermitian matrix.

lambdas, V = np.linalg.eigh(A)
V
array([[-0.85065081, -0.52573111], [-0.52573111, 0.85065081]])

Definition (Conjugate transpose)

Conjugate transpose of a complex square matrix ACn×nA \in \bbC^{n \times n} is defined as

AH=Re(A)ιIm(A),A^H = Re(A)^\top - \iota Im(A)^\top,

where Re(A)Re(A) is the real part of the matrix AA and Im(A)Im(A) is the imaginary part of matrix A=Re(A)+ιIm(A)A = Re(A) + \iota Im(A) and ι=1\iota = \sqrt{-1}.

Conjugate transpose is the generalization of transpose to complex matrices.

Definition (Hermitian matrix)

A complex matrix ACn×nA \in \bbC^{n \times n} is said to be Hermitian matrix if

AH=AA^H = A

Hermitian matrix is a generalization of symmetric matrices to complex matrices.

Theorem: Eigen values of an Hermitian matrices are real

For a complex vector xCn\bfx \in \bbC^n, the dot product xHxR\bfx^H \bfx \in \bbR is real.

xHx=xˉ1x1+xˉ2x2++xˉnxn,\bfx^H \bfx = \bar{x}_1 x_1 + \bar{x}_2 x_2 + \dots + \bar{x}_n x_n,

where xˉi\bar{x}_i denotes the complex conjugate of xix_i. If xi=ai+ιbix_i = a_i + \iota b_i , then xˉi=aiιbi\bar{x}_i = a_i - \iota b_i. Note that xˉixi=ai2+bi2\bar{x}_i x_i = a^2_i + b^2_i is real. Because each element is real, hence their sum xHx\bfx^H \bfx is real.

Let ACn×nA\in\bbC^{n \times n} be Hermitian then, its eigen vectors viCn\bfv_i \in \bbC^n and eigen values λC\lambda \in \bbC satisify

Avi=λiviA \bfv_i = \lambda_i \bfv_i

Multiply both sides on the left by viH\bfv_i^H

viHAvi=λivHvi\bfv_i^H A \bfv_i = \lambda_i \bfv^H \bfv_i

or

λi=viHAvivHvi\lambda_i = \frac{\bfv_i^H A \bfv_i}{\bfv^H \bfv_i}

Take complex conjugate on both sides,

λˉi=viHAviviHvi\bar{\lambda}_i = \frac{\overline{\bfv_i^H A \bfv_i}}{\overline{\bfv_i^H \bfv_i}}

We know the denominator viHvi\bfv_i^H\bfv_i is real. Numerator can be computed by taking the Hermitian transpose of the product,

λˉi=viHAHviviHvi\bar{\lambda}_i = \frac{\bfv_i^H A^H \bfv_i}{\bfv_i^H \bfv_i}

Because AHA^H is Hermitian, AH=AA^H = A, which implies,

λˉi=λi.\bar{\lambda}_i = \lambda_i.

This implies that λiR\lambda_i\in \bbR is real.

When λi\lambda_i is real

Theorem: Eigen vectors of a real symmetric matrices are real

Because real symmetric matrices are also hermitian matrices, their eigen values are real. If their eigen values are real, then eigen vectors that are non-zero solutions to the equation

(AλIn)v=0(A - \lambda I_n) \bfv = \mathbf{0}

are also real.

np.linalg.eigh finds Eigen values of a Hermitian matrix.

lambdas, V = np.linalg.eigh(A)
lambdas
array([1.38196601, 3.61803399])
V
array([[-0.85065081, -0.52573111], [-0.52573111, 0.85065081]])

You can verify that the eigen values and vectors are correct by checking if

A=VΛVA = V \Lambda V^\top
V @ np.diag(lambdas) @ V.T
array([[ 2., -1.], [-1., 3.]])
np.allclose(A, V @ np.diag(lambdas) @ V.T)
True

Recall that we were analyzing the function,

f(x)=xAx=xVΛVx=(Vx)Λ(Vx)f(\bfx) = \bfx^\top A \bfx = \bfx^\top V \Lambda V^\top \bfx = (V^\top\bfx)^\top \Lambda (V^\top \bfx)

With this we can break down the computation of function f(x)f(\bfx) in two steps,

y=Vx    x=Vy,\bfy = V^\top \bfx \iff \bfx = V \bfy,

followed by scaling,

f(x)=yΛyf(\bfx) = \bfy^\top \Lambda \bfy

If all eigen values are positive (AA is positive definite), we can break down eigen values into their square roots and then

z=Λy    y=Λ1\bfz = \sqrt{\Lambda} \bfy \iff \bfy = \sqrt{\Lambda}^{-1}

can be understood as element wise scaling.

f(x)=zzf(\bfx) = \bfz^\top \bfz

Note that zz=z12+z22++zn2=f1\bfz^\top \bfz = z_1^2 + z_2^2 + \dots + z_n^2 = f_1 is equation of a circle in terms of z\bfz for a contour where f(z)=f1f(\bfz) = f_1. We can also start from a circle plot for z\bfz .

and then compute and plot

y=Λ1z.\bfy = \sqrt{\Lambda}^{-1} \bfz.

Note that the principle axes of the circle are scaled by eigen values.

Next we compute and plot x\bfx from y\bfy

x=Vy\bfx = V\bfy
import matplotlib as mpl
from functools import partial

def f_wrt_y(lambdas, y):
    # lambdas is of shape 2
    # y is of shape m x m x n
    return (y * lambdas * y).sum(axis=-1)

def f_wrt_z(z):
    # z is m x m x n
    return (z * z).sum(axis=-1)

zs = np.array([
    [1, 0],
    [0, 1]])
fig, axes = plt.subplots(1, 3, figsize=(18, 6))

ax = axes[0]
plot_contour(ax, f_wrt_z)
ax.set_title(r'$\mathbf{z}$')
ax.set_xlabel(r'$z_1$')
ax.set_ylabel(r'$z_2$')
draw_arrows(ax, zs)

ax = axes[1]
plot_contour(ax, partial(f_wrt_y, lambdas))
ax.set_title(r'$\mathbf{y} = \sqrt{\Lambda}^{-1} \mathbf{z}$')
ax.set_xlabel(r'$y_1$')
ax.set_ylabel(r'$y_2$')
ys =  zs / np.sqrt(lambdas)
draw_arrows(ax, ys)

ax = axes[2]
plot_contour(ax, partial(f, A=A))
ax.set_title(r'$\mathbf{x} = V \sqrt{\Lambda}^{-1} \mathbf{z}$')
ax.set_xlabel(r'$x_1$')
ax.set_ylabel(r'$x_2$')
xs = (V @ ys.T).T
draw_arrows(ax, xs)
<Axes: title={'center': '$\\mathbf{x} = V \\sqrt{\\Lambda}^{-1} \\mathbf{z}$'}, xlabel='$x_1$', ylabel='$x_2$'>
<Figure size 1800x600 with 3 Axes>

Note that countours of f(x)f(\bfx) are exactly alinged with the transformed circle from z\bfz to x\bfx

Eigen values of saddle point (Indefinite matrix)

A = np.array([[2, -1],
             [-1, -3]])
A
array([[ 2, -1], [-1, -3]])
lambdas, V = np.linalg.eigh(A)
lambdas
array([-3.1925824, 2.1925824])

Note that the eigen values are neither all positive nor all negative. We cannot take the square root directly and we have to be respectful of the sign of the lambdas.

fig, axes = plt.subplots(1, 3, figsize=(18, 6))

ax = axes[0]
plot_contour(ax, f_wrt_z)
ax.set_title(r'$\mathbf{z}$')
ax.set_xlabel(r'$z_1$')
ax.set_ylabel(r'$z_2$')
draw_arrows(ax, zs)

ax = axes[1]
plot_contour(ax, partial(f_wrt_y, lambdas))
ax.set_title(r'$\mathbf{y} = \text{sign}(\Lambda)\sqrt{|\Lambda|}^{-1} \mathbf{z}$')
ax.set_xlabel(r'$y_1$')
ax.set_ylabel(r'$y_2$')
ys =  zs / np.sign(lambdas) * np.sqrt(np.abs(lambdas))
draw_arrows(ax, ys)

ax = axes[2]
plot_contour(ax, partial(f, A=A))
ax.set_title(r'$\mathbf{x} = V \text{sign}(\Lambda)\sqrt{|\Lambda|}^{-1} \mathbf{z}$')
ax.set_xlabel(r'$x_1$')
ax.set_ylabel(r'$x_2$')
xs = (V @ ys.T).T
draw_arrows(ax, xs)
<Axes: title={'center': '$\\mathbf{x} = V \\text{sign}(\\Lambda)\\sqrt{|\\Lambda|}^{-1} \\mathbf{z}$'}, xlabel='$x_1$', ylabel='$x_2$'>
<Figure size 1800x600 with 3 Axes>