GitHub

@@ -17,21 +17,23 @@ kernelspec:

17171818

This lecture describes circulant matrices and some of their properties.

191920-

Circulant matrices have a special structure that connects them to useful concepts

21-

including

20+

Circulant matrices are useful because multiplying by them is closely connected to convolution, and their eigenvectors can be constructed using the Discrete Fourier Transform.

21+22+

We use circulant matrices to connect several useful concepts, including

22232324

* convolution

2425

* Fourier transforms

2526

* permutation matrices

262727-

Because of these connections, circulant matrices are widely used in machine learning, for example, in image processing.

28+

For background on eigenvalues and eigenvectors, see {doc}`linear_algebra`; for another use of Fourier transforms and convolution, see {doc}`hoist_failure`.

29+30+

Circulant matrices are also widely used in machine learning, for example, in image processing.

283129323033

We begin by importing some Python packages

31343235

```{code-cell} ipython3

3336

import numpy as np

34-

from numba import jit

3537

import matplotlib.pyplot as plt

3638

```

3739

@@ -60,14 +62,28 @@ c_{1} & c_{2} & c_{3} & c_{4} & c_{5} & \cdots & c_{0}

6062

\end{array}\right]

6163

$$ (eqn:circulant)

626465+

This pattern can be formalized as follows.

66+67+

```{prf:definition} Circulant matrix

68+

:label: def-circulant-matrix

69+70+

An $N \times N$ matrix $C$ is **circulant** if there are numbers $c_0, \ldots, c_{N-1}$ such that

71+72+

$$

73+

C_{ij} = c_{(j-i) \bmod N},

74+

\qquad 0 \leq i,j \leq N-1.

75+

$$

76+77+

Equivalently, each row is obtained from the previous row by shifting entries one step to the right.

78+

```

79+6380

It is also possible to construct a circulant matrix by creating the transpose of the above matrix, in which case only the

6481

first column needs to be specified.

65826683

Let's write some Python code to generate a circulant matrix.

67846885

```{code-cell} ipython3

69-

@jit

70-

def construct_cirlulant(row):

86+

def construct_circulant(row):

71877288

N = row.size

7389

@@ -83,14 +99,16 @@ def construct_cirlulant(row):

839984100

```{code-cell} ipython3

85101

# a simple case when N = 3

86-

construct_cirlulant(np.array([1., 2., 3.]))

102+

construct_circulant(np.array([1., 2., 3.]))

87103

```

8810489105

### Some Properties of Circulant Matrices

9010691107

Here are some useful properties:

9210893-

Suppose that $A$ and $B$ are both circulant matrices. Then it can be verified that

109+

Suppose that $A$ and $B$ are both circulant matrices of the same order and constructed using the same cyclic shift convention.

110+111+

Then it can be verified that

9411295113

* The transpose of a circulant matrix is a circulant matrix.

96114

@@ -111,16 +129,18 @@ Now consider a circulant matrix with first row

111129

The **convolution** of vectors $c$ and $a$ is defined as the vector $b = c * a $ with components

112130113131

$$

114-

b_k = \sum_{i=0}^{n-1} c_{k-i} a_i

132+

b_k = \sum_{i=0}^{N-1} c_{k-i} a_i

115133

$$ (eqn:conv)

116134135+

Here and below, indices such as $k-i$ are interpreted modulo $N$.

136+117137

We use $*$ to denote **convolution** via the calculation described in equation {eq}`eqn:conv`.

118138119139

It can be verified that the vector $b$ satisfies

120140121-

$$ b = C^T a $$

141+

$$ b = C^\top a $$

122142123-

where $C^T$ is the transpose of the circulant matrix defined in equation {eq}`eqn:circulant`.

143+

where $C^\top$ is the transpose of the circulant matrix defined in equation {eq}`eqn:circulant`.

124144125145126146

@@ -176,7 +196,7 @@ $$

176196

and solving

177197178198

$$

179-

\textrm{det}(P - \lambda I) = (-1)^N \lambda^{N}-1=0

199+

\textrm{det}(P - \lambda I) = (-1)^N(\lambda^N - 1)=0

180200

$$

181201182202

@@ -189,7 +209,7 @@ Thus, **singular values** of the permutation matrix $P$ defined in equation {eq

189209

It can be verified that permutation matrices are orthogonal matrices:

190210191211

$$

192-

P P' = I

212+

P P^\top = I

193213

$$

194214195215

@@ -200,8 +220,7 @@ $$

200220

Let's write some Python code to illustrate these ideas.

201221202222

```{code-cell} ipython3

203-

@jit

204-

def construct_P(N):

223+

def construct_cyclic_shift_matrix(N):

205224206225

P = np.zeros((N, N))

207226

@@ -213,7 +232,7 @@ def construct_P(N):

213232

```

214233215234

```{code-cell} ipython3

216-

P4 = construct_P(4)

235+

P4 = construct_cyclic_shift_matrix(4)

217236

P4

218237

```

219238

@@ -224,7 +243,7 @@ P4

224243225244

```{code-cell} ipython3

226245

for i in range(4):

227-

print(f'𝜆{i} = {𝜆[i]:.1f} \nvec{i} = {Q[i, :]}\n')

246+

print(f'𝜆{i} = {𝜆[i]:.1f} \nvec{i} = {Q[:, i]}\n')

228247

```

229248230249

In graphs below, we shall portray eigenvalues of a shift permutation matrix in the complex plane.

@@ -249,7 +268,7 @@ for i, N in enumerate([3, 4, 6, 8]):

249268

row_i = i // 2

250269

col_i = i % 2

251270252-

P = construct_P(N)

271+

P = construct_cyclic_shift_matrix(N)

253272

𝜆, Q = np.linalg.eig(P)

254273255274

circ = plt.Circle((0, 0), radius=1, edgecolor='b', facecolor='None')

@@ -287,7 +306,7 @@ F_{8}=\left[\begin{array}{ccccc}

287306

\end{array}\right]

288307

$$

289308290-

The matrix $F_8$ defines a [Discete Fourier Transform](https://en.wikipedia.org/wiki/Discrete_Fourier_transform).

309+

The matrix $F_8$ defines a [Discrete Fourier Transform](https://en.wikipedia.org/wiki/Discrete_Fourier_transform).

291310292311

To convert it into an orthogonal eigenvector matrix, we can simply normalize it by dividing every entry by $\sqrt{8}$.

293312

@@ -332,7 +351,7 @@ Q8 @ np.conjugate(Q8)

332351

Let's verify that $k$th column of $Q_{8}$ is an eigenvector of $P_{8}$ with an eigenvalue $w^{k}$.

333352334353

```{code-cell} ipython3

335-

P8 = construct_P(8)

354+

P8 = construct_cyclic_shift_matrix(8)

336355

```

337356338357

```{code-cell} ipython3

@@ -353,15 +372,11 @@ Next, we execute calculations to verify that the circulant matrix $C$ defined i

353372354373355374

$$

356-

C = c_{0} I + c_{1} P + \cdots + c_{n-1} P^{n-1}

375+

C = c_{0} I + c_{1} P + \cdots + c_{N-1} P^{N-1}

357376

$$

358377359378

and that every eigenvector of $P$ is also an eigenvector of $C$.

360379361-

```{code-cell} ipython3

362-363-

```

364-365380

We illustrate this for $N=8$ case.

366381367382

```{code-cell} ipython3

@@ -373,10 +388,10 @@ c

373388

```

374389375390

```{code-cell} ipython3

376-

C8 = construct_cirlulant(c)

391+

C8 = construct_circulant(c)

377392

```

378393379-

Compute $c_{0} I + c_{1} P + \cdots + c_{n-1} P^{n-1}$.

394+

Compute $c_{0} I + c_{1} P + \cdots + c_{N-1} P^{N-1}$.

380395381396

```{code-cell} ipython3

382397

N = 8

@@ -403,7 +418,7 @@ Now let's compute the difference between two circulant matrices that we have co

403418

np.abs(C - C8).max()

404419

```

405420406-

The $k$th column of $P_{8}$ associated with eigenvalue $w^{k-1}$ is an eigenvector of $C_{8}$ associated with an eigenvalue $\sum_{h=0}^{7} c_{j} w^{h k}$.

421+

The $j$th column of $Q_{8}$ is an eigenvector of $C_{8}$ associated with eigenvalue $\sum_{k=0}^{7} c_k w^{j k}$.

407422408423

```{code-cell} ipython3

409424

𝜆_C8 = np.zeros(8, dtype=complex)

@@ -430,7 +445,7 @@ for j in range(8):

430445431446

The **Discrete Fourier Transform** (DFT) allows us to represent a discrete time sequence as a weighted sum of complex sinusoids.

432447433-

Consider a sequence of $N$ real number $\{x_j\}_{j=0}^{N-1}$.

448+

Consider a sequence of $N$ real numbers $\{x_j\}_{j=0}^{N-1}$.

434449435450

The **Discrete Fourier Transform** maps $\{x_j\}_{j=0}^{N-1}$ into a sequence of complex numbers $\{X_k\}_{k=0}^{N-1}$

436451

@@ -498,7 +513,7 @@ def plot_magnitude(x=None, X=None):

498513

if (X is not None):

499514

data.append(X)

500515

names.append('X')

501-

xs.append('j')

516+

xs.append('k')

502517503518

num = len(data)

504519

for i in range(num):

@@ -526,7 +541,7 @@ x_{n} = \sum_{k=0}^{N-1} \frac{1}{N} X_{k} e^{2\pi\left(\frac{kn}{N}\right)i}, \

526541

$$

527542528543

```{code-cell} ipython3

529-

def inverse_transform(X):

544+

def inverse_DFT(X):

530545531546

N = len(X)

532547

w = np.e ** (complex(0, 2*np.pi/N))

@@ -540,7 +555,7 @@ def inverse_transform(X):

540555

```

541556542557

```{code-cell} ipython3

543-

inverse_transform(X)

558+

inverse_DFT(X)

544559

```

545560546561

Another example is

@@ -551,7 +566,7 @@ $$

551566552567

Since $N=20$, we cannot use an integer multiple of $\frac{1}{20}$ to represent a frequency $\frac{11}{40}$.

553568554-

To handle this, we shall end up using all $N$ of the availble frequencies in the DFT.

569+

To handle this, we shall end up using all $N$ of the available frequencies in the DFT.

555570556571

Since $\frac{11}{40}$ is in between $\frac{10}{40}$ and $\frac{12}{40}$ (each of which is an integer multiple of $\frac{1}{20}$), the complex coefficients in the DFT have their largest magnitudes at $k=5,6,15,16$, not just at a single frequency.

557572

@@ -614,7 +629,7 @@ X = DFT(x)

614629

X

615630

```

616631617-

Now let's evaluate the outcome of postmultiplying the eigenvector matrix $F_{20}$ by the vector $x$, a product that we claim should equal the Fourier tranform of the sequence $\{x_n\}_{n=0}^{N-1}$.

632+

Now let's evaluate the outcome of postmultiplying the eigenvector matrix $F_{20}$ by the vector $x$, a product that we claim should equal the Fourier transform of the sequence $\{x_n\}_{n=0}^{N-1}$.

618633619634

```{code-cell} ipython3

620635

F20, _ = construct_F(20)

@@ -624,13 +639,9 @@ F20, _ = construct_F(20)

624639

F20 @ x

625640

```

626641627-

Similarly, the inverse DFT can be expressed as a inverse DFT matrix $F^{-1}_{20}$.

642+

Similarly, the inverse DFT can be expressed as an inverse DFT matrix $F^{-1}_{20}$.

628643629644

```{code-cell} ipython3

630645

F20_inv = np.linalg.inv(F20)

631646

F20_inv @ X

632647

```

633-634-

```{code-cell} ipython3

635-636-

```

Read the original on github.com ↗