L25 서술 파트의 주장은 하나로 두 가지가 따라온다는 것이었다. 고윳값이 전부 실수이고, 고유벡터를 서로 직교하게 고를 수 있다.
이 노트북에서는 무작위 대칭행렬로 그 두 가지를 확인하고, 대칭성을 슬라이더로 조금씩 깨뜨리며 고유벡터의 사잇각이 90도에서 어떻게 벗어나는지 본다. 그리고 랭크 1 벽돌을 하나씩 쌓아 행렬이 복원되는 것을 확인한다.
| 서술 파트의 내용 | 여기서 확인하는 방법 |
|---|---|
| 고윳값이 실수 | 무작위 대칭행렬 500개의 허수부 |
| 고유벡터가 직교 | 가 인지 |
| 대칭이 아니면 깨진다 | 슬라이더로 사잇각이 90도에서 벗어난다 |
| 복원 오차와 | |
| 랭크 1 벽돌 | 하나씩 쌓아 가며 오차가 줄어드는 것 |
| 가 투영 | , , 랭크 1 |
| 완비성 | |
| 실베스터 관성 법칙 | 교환 없는 소거의 피벗 부호 |
| 행 교환의 함정 | scipy.linalg.lu 로는 틀린 답이 나온다 |
eig vs eigh | 무엇이 다른가 |
0. 준비¶
import numpy as np
import plotly.graph_objects as go
from linalg_viz import COLORS, layout2d, show_matrix, slider_figure
np.set_printoptions(precision=4, suppress=True)
rng = np.random.default_rng(25)
print("numpy", np.__version__)numpy 2.5.2
1. 두 선물을 확인한다¶
무작위 대칭행렬을 잔뜩 만들어 예외를 찾아보자. 대칭행렬은 아무 행렬 에 대해 로 만들면 된다.
def 무작위대칭(n, rng):
"""B + B^T 는 반드시 대칭이다."""
B = rng.normal(size=(n, n))
return B + B.T최대허수 = 0.0
최대비직교 = 0.0
for _ in range(500):
n = int(rng.integers(2, 9))
S = 무작위대칭(n, rng)
값 = np.linalg.eigvals(S) # 일부러 eig 계열로 (eigh 는 실수를 강제)
최대허수 = max(최대허수, float(np.abs(값.imag).max()))
_, Q = np.linalg.eigh(S)
최대비직교 = max(최대비직교, float(np.abs(Q.T @ Q - np.eye(n)).max()))
print(f"고윳값 허수부의 최댓값 : {최대허수:.3e} -> 전부 실수")
print(f"Q^T Q - I 의 최댓값 : {최대비직교:.3e} -> 전부 직교")고윳값 허수부의 최댓값 : 0.000e+00 -> 전부 실수
Q^T Q - I 의 최댓값 : 2.665e-15 -> 전부 직교
비대칭 행렬은 어떤가. 같은 실험을 대칭화하지 않은 행렬로 해 보자.
허수있음 = 0
사잇각들 = []
for _ in range(500):
B = rng.normal(size=(2, 2))
값, V = np.linalg.eig(B)
if np.abs(값.imag).max() > 1e-12:
허수있음 += 1
continue
v1, v2 = V[:, 0].real, V[:, 1].real
코사인 = abs(v1 @ v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))
사잇각들.append(np.degrees(np.arccos(np.clip(코사인, -1, 1))))
사잇각들 = np.array(사잇각들)
print(f"비대칭 2x2 500개 중 복소 고윳값 : {허수있음}개")
print(f"실수인 것들의 고유벡터 사잇각 : 최소 {사잇각들.min():.1f}도, "
f"최대 {사잇각들.max():.1f}도, 평균 {사잇각들.mean():.1f}도")
print(f" 90도에 가까운 것(오차 1도 이내) : {(np.abs(사잇각들-90) < 1).sum()}개")비대칭 2x2 500개 중 복소 고윳값 : 146개
실수인 것들의 고유벡터 사잇각 : 최소 8.8도, 최대 89.7도, 평균 64.3도
90도에 가까운 것(오차 1도 이내) : 9개
def 정보(t):
M = np.array([[2.0, 1.0 + t], [1.0, 2.0]])
값, V = np.linalg.eig(M)
if np.abs(값.imag).max() > 1e-9:
return M, 값, None
v1, v2 = V[:, 0].real, V[:, 1].real
코사인 = abs(v1 @ v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))
return M, 값, float(np.degrees(np.arccos(np.clip(코사인, -1, 1))))
print(f"{'t':>7}{'고윳값':>30}{'사잇각':>12}{'':>4}")
for t in (-3.0, -2.0, -1.0, -0.5, 0.0, 0.5, 1.0, 2.0):
M, 값, 각 = 정보(t)
보기 = ", ".join(f"{v.real:.2f}{v.imag:+.2f}i" if abs(v.imag) > 1e-9
else f"{v.real:.3f}" for v in 값)
각보기 = "복소라 없음" if 각 is None else f"{각:.2f}도"
별 = " <- 대칭" if abs(t) < 1e-12 else ""
print(f"{t:>7}{보기:>30}{각보기:>12}{별}") t 고윳값 사잇각
-3.0 2.00+1.41i, 2.00-1.41i 복소라 없음
-2.0 2.00+1.00i, 2.00-1.00i 복소라 없음
-1.0 2.000, 2.000 0.00도
-0.5 2.707, 1.293 70.53도
0.0 3.000, 1.000 90.00도 <- 대칭
0.5 3.225, 0.775 78.46도
1.0 3.414, 0.586 70.53도
2.0 3.732, 0.268 60.00도
에서만 정확히 90도이다. 이면 고윳값이 복소수가 되어 실수 고유벡터가 아예 없어지고, 은 그 경계로 고윳값이 겹치면서 고유벡터가 하나뿐인 결함 행렬이 된다.
M = np.array([[2.0, 0.0], [1.0, 2.0]]) # t = -1
print(show_matrix(M, "M(-1)"))
print("고윳값 :", np.linalg.eigvals(M))
print("M - 2I 의 랭크 :", np.linalg.matrix_rank(M - 2*np.eye(2)),
" -> 영공간이 1차원. 고유벡터가 하나뿐이다 (L21 의 결함 행렬)")M(-1)
[ 2 0 ]
[ 1 2 ]
고윳값 : [2.+0.j 2.+0.j]
M - 2I 의 랭크 : 1 -> 영공간이 1차원. 고유벡터가 하나뿐이다 (L21 의 결함 행렬)
t들 = np.round(np.linspace(-3.0, 2.0, 21), 2)
각도 = np.linspace(0, 2*np.pi, 300)
원 = np.vstack([np.cos(각도), np.sin(각도)])
프레임 = []
for t in t들:
M, 값, 각 = 정보(t)
묶음 = [
go.Scatter(x=원[0], y=원[1], mode="lines",
line=dict(color="#bbbbbb", width=1.5, dash="dot"),
name="단위원"),
go.Scatter(x=(M @ 원)[0], y=(M @ 원)[1], mode="lines",
line=dict(color=COLORS["output"], width=3), name="그 상"),
]
_, V = np.linalg.eig(M)
for j in range(2):
if 각 is None:
묶음.append(go.Scatter(x=[None], y=[None], mode="lines",
showlegend=False))
continue
v = V[:, j].real
v = v / np.linalg.norm(v) * 3.4
묶음.append(go.Scatter(x=[-v[0], v[0]], y=[-v[1], v[1]], mode="lines",
line=dict(color=COLORS["input"], width=2, dash="dash"),
showlegend=False))
말 = ("고윳값이 복소수 : 실수 고유벡터 없음" if 각 is None
else f"고유벡터 사잇각 {각:.2f}도" + (" (대칭!)" if abs(t) < 1e-9 else ""))
묶음.append(go.Scatter(x=[0], y=[-3.7], mode="text", text=[말],
textfont=dict(size=15), showlegend=False))
프레임.append(묶음)
배치 = layout2d("M(t) = [[2, 1+t], [1, 2]]", extent=4.1)
배치["height"] = 600
slider_figure(프레임, t들, 배치, prefix="t = ", initial=12)슬라이더를 에 맞추면 점선 둘이 정확히 직각을 이루고, 주황 타원의 축도 그 점선 위에 정확히 놓인다. 조금만 움직여도 둘 다 어긋난다.
3. 스펙트럼 정리¶
이번 강의의 앵커는 이다. 고윳값이 이고 고유벡터가 , , 이다.
S = np.array([[6.0, 0.0, -3.0],
[0.0, 6.0, -3.0],
[-3.0, -3.0, 9.0]])
print(show_matrix(S, "S"))
print("대칭인가 :", np.allclose(S, S.T))
for l, x in ((3, [1, 1, 1]), (6, [1, -1, 0]), (12, [1, 1, -2])):
x = np.array(x, float)
print(f" S{x} = {S @ x} = {l} x ->", np.allclose(S @ x, l * x))
print()
print("세 고유벡터의 내적 :",
[round(float(np.array(a, float) @ np.array(b, float)), 12)
for a, b in (([1,1,1],[1,-1,0]), ([1,1,1],[1,1,-2]), ([1,-1,0],[1,1,-2]))])S
[ 6 0 -3 ]
[ 0 6 -3 ]
[ -3 -3 9 ]
대칭인가 : True
S[1. 1. 1.] = [3. 3. 3.] = 3 x -> True
S[ 1. -1. 0.] = [ 6. -6. 0.] = 6 x -> True
S[ 1. 1. -2.] = [ 12. 12. -24.] = 12 x -> True
세 고유벡터의 내적 : [0.0, 0.0, 0.0]
값, Q = np.linalg.eigh(S) # 대칭행렬 전용. 오름차순 실수
print("고윳값 :", 값)
print(show_matrix(Q, "Q (열이 길이 1 인 고유벡터)"))
print("Q^T Q = I 인가 :", np.allclose(Q.T @ Q, np.eye(3)))
print("Q Q^T = I 인가 :", np.allclose(Q @ Q.T, np.eye(3)))
print()
print("Q^-1 과 Q^T 가 같은가 :", np.allclose(np.linalg.inv(Q), Q.T))
print(" 최대 차이 :", np.abs(np.linalg.inv(Q) - Q.T).max())
print()
print("Q Lambda Q^T 로 복원한 오차 :",
np.abs(Q @ np.diag(값) @ Q.T - S).max())고윳값 : [ 3. 6. 12.]
Q (열이 길이 1 인 고유벡터)
[ -0.577 -0.707 0.408 ]
[ -0.577 0.707 0.408 ]
[ -0.577 0 -0.816 ]
Q^T Q = I 인가 : True
Q Q^T = I 인가 : True
Q^-1 과 Q^T 가 같은가 : True
최대 차이 : 4.440892098500626e-16
Q Lambda Q^T 로 복원한 오차 : 2.220446049250313e-15
아무직교 = np.linalg.qr(rng.normal(size=(4, 4)))[0] # 아무 직교행렬
아무대각 = np.diag(rng.normal(size=4))
만든것 = 아무직교 @ 아무대각 @ 아무직교.T
print("Q D Q^T 가 대칭인가 :", np.allclose(만든것, 만든것.T))
print("그 고윳값 :", np.sort(np.linalg.eigvalsh(만든것)))
print("D 의 대각 :", np.sort(np.diag(아무대각)), " <- 같은 수들")Q D Q^T 가 대칭인가 : True
그 고윳값 : [-0.396 0.3251 0.4155 1.1532]
D 의 대각 : [-0.396 0.3251 0.4155 1.1532] <- 같은 수들
def 벽돌들(S):
"""대칭행렬을 lambda_i q_i q_i^T 들로 쪼갠다. 큰 lambda 부터."""
값, Q = np.linalg.eigh(S)
순서 = np.argsort(np.abs(값))[::-1]
조각 = []
for i in 순서:
P = np.outer(Q[:, i], Q[:, i])
조각.append((값[i], np.where(np.abs(P) < 1e-14, 0.0, P))) # -0 을 0 으로
return 조각조각 = 벽돌들(S)
쌓음 = np.zeros_like(S)
for k, (l, P) in enumerate(조각, start=1):
쌓음 = 쌓음 + l * P
print(f"벽돌 {k}개까지 쌓았을 때 (lambda = {l:>5.1f}) 복원 오차 "
f"{np.abs(쌓음 - S).max():.3e}")
print()
for k, (l, P) in enumerate(조각, start=1):
print(show_matrix(l * P, f"{l:.0f} q{k} q{k}^T"))벽돌 1개까지 쌓았을 때 (lambda = 12.0) 복원 오차 4.000e+00
벽돌 2개까지 쌓았을 때 (lambda = 6.0) 복원 오차 1.000e+00
벽돌 3개까지 쌓았을 때 (lambda = 3.0) 복원 오차 1.776e-15
12 q1 q1^T
[ 2 2 -4 ]
[ 2 2 -4 ]
[ -4 -4 8 ]
6 q2 q2^T
[ 3 -3 0 ]
[ -3 3 0 ]
[ 0 0 0 ]
3 q3 q3^T
[ 1 1 1 ]
[ 1 1 1 ]
[ 1 1 1 ]
서술 파트의 (32)에 적은 것과 같은 정수 벽돌이 나온다. 이제 각 벽돌이 정말 투영행렬인지 확인하자.
print(f"{'':>6}{'P^2 = P':>10}{'P^T = P':>10}{'랭크':>6}{'대각합':>10}")
for k, (l, P) in enumerate(조각, start=1):
print(f"{'P'+str(k):>6}{str(np.allclose(P@P, P)):>10}"
f"{str(np.allclose(P, P.T)):>10}"
f"{np.linalg.matrix_rank(P):>6}{np.trace(P):>10.4f}")
print()
print("투영행렬의 대각합은 랭크와 같다. 그래서 전부 1 이다.")
print()
print("모든 벽돌을 lambda 없이 더하면 :")
print(show_matrix(sum(P for _, P in 조각), "sum q q^T"))
print("I 인가 :", np.allclose(sum(P for _, P in 조각), np.eye(3))) P^2 = P P^T = P 랭크 대각합
P1 True True 1 1.0000
P2 True True 1 1.0000
P3 True True 1 1.0000
투영행렬의 대각합은 랭크와 같다. 그래서 전부 1 이다.
모든 벽돌을 lambda 없이 더하면 :
sum q q^T
[ 1 -1.11e-16 -5.55e-17 ]
[ -1.11e-16 1 -1.11e-16 ]
[ -5.55e-17 -1.11e-16 1 ]
I 인가 : True
투영이 서로 간섭하지 않는다¶
는 자기 방향은 그대로 두고 다른 고유방향은 0으로 보낸다. 그래서 이다 ().
print(f"{'P_i P_j':>10}" + "".join(f"{'P'+str(j+1):>8}" for j in range(3)))
for i in range(3):
칸들 = []
for j in range(3):
곱 = 조각[i][1] @ 조각[j][1]
기대 = 조각[i][1] if i == j else np.zeros((3, 3))
칸들.append(("P" + str(i+1) if i == j else "0")
if np.allclose(곱, 기대) else "?")
print(f"{'P'+str(i+1):>10}" + "".join(f"{c:>8}" for c in 칸들))
print()
print("대각만 자기 자신이고 나머지는 전부 0 이다. 서로 간섭하지 않는다.") P_i P_j P1 P2 P3
P1 P1 0 0
P2 0 P2 0
P3 0 0 P3
대각만 자기 자신이고 나머지는 전부 0 이다. 서로 간섭하지 않는다.
x = np.array([1.0, 2.0, -1.0])
_, Q = np.linalg.eigh(S)
값, _ = np.linalg.eigh(S)
좌표 = Q.T @ x
print("x =", x)
print("고유좌표 q_i^T x =", 좌표)
print("lambda 를 곱한 것 =", 값 * 좌표)
print("도로 더한 것 Q(...) =", Q @ (값 * 좌표))
print("S x =", S @ x)
print("같은가 :", np.allclose(Q @ (값 * 좌표), S @ x))
print()
print("좌표를 연립방정식으로 구하면 :", np.linalg.solve(Q, x))
print("내적으로 구하면 :", Q.T @ x, " <- 같은 답, 훨씬 싸다")x = [ 1. 2. -1.]
고유좌표 q_i^T x = [-1.1547 0.7071 2.0412]
lambda 를 곱한 것 = [-3.4641 4.2426 24.4949]
도로 더한 것 Q(...) = [ 9. 15. -18.]
S x = [ 9. 15. -18.]
같은가 : True
좌표를 연립방정식으로 구하면 : [-1.1547 0.7071 2.0412]
내적으로 구하면 : [-1.1547 0.7071 2.0412] <- 같은 답, 훨씬 싸다
5. 부호는 피벗이 알려 준다¶
실베스터 관성 법칙은 행 교환 없이 소거해서 얻은 피벗에 대한 것이다.
def 피벗들(S):
"""행 교환 없이 소거해서 피벗을 뽑는다. 대칭이 깨지지 않는다."""
U = np.array(S, dtype=float)
n = U.shape[0]
for k in range(n - 1):
if abs(U[k, k]) < 1e-14:
return None # 교환 없이는 진행할 수 없다
U[k+1:] -= np.outer(U[k+1:, k] / U[k, k], U[k])
return np.diag(U).copy()경우 = (
("S (앵커)", S),
("[[2,1],[1,2]]", np.array([[2.0, 1.0], [1.0, 2.0]])),
("[[1,2],[2,1]]", np.array([[1.0, 2.0], [2.0, 1.0]])),
("[[-2,1],[1,-2]]", np.array([[-2.0, 1.0], [1.0, -2.0]])),
("[[1,3,0],[3,1,0],[0,0,-2]]",
np.array([[1.0, 3.0, 0.0], [3.0, 1.0, 0.0], [0.0, 0.0, -2.0]])),
)
print(f"{'':>28}{'고윳값':>24}{'피벗':>24}{'양수 개수':>12}")
for 이름, M in 경우:
값m = np.linalg.eigvalsh(M)
p = 피벗들(M)
print(f"{이름:>28}{str(np.round(값m,2)):>24}{str(np.round(p,2)):>24}"
f"{f'{(값m>0).sum()} vs {(p>0).sum()}':>12}") 고윳값 피벗 양수 개수
S (앵커) [ 3. 6. 12.] [6. 6. 6.] 3 vs 3
[[2,1],[1,2]] [1. 3.] [2. 1.5] 2 vs 2
[[1,2],[2,1]] [-1. 3.] [ 1. -3.] 1 vs 1
[[-2,1],[1,-2]] [-3. -1.] [-2. -1.5] 0 vs 0
[[1,3,0],[3,1,0],[0,0,-2]] [-2. -2. 4.] [ 1. -8. -2.] 1 vs 1
무작위로도 확인하자¶
어긋남 = 시도 = 0
for _ in range(400):
n = int(rng.integers(2, 7))
M = 무작위대칭(n, rng)
p = 피벗들(M)
if p is None:
continue
시도 += 1
if int((np.linalg.eigvalsh(M) > 0).sum()) != int((p > 0).sum()):
어긋남 += 1
print(f"무작위 대칭행렬 {시도}개 중 부호 개수가 어긋난 것 : {어긋남}개")무작위 대칭행렬 400개 중 부호 개수가 어긋난 것 : 0개
함정 — 행을 바꾸면 틀린다¶
scipy.linalg.lu 는 수치 안정성을 위해 부분 피벗팅을 한다. 곧 행을 바꾼다.
그러면 실베스터 관성 법칙이 깨진다.
from scipy.linalg import lu
M = np.array([[1.0, 2.0], [2.0, 1.0]])
값m = np.linalg.eigvalsh(M)
_, _, U = lu(M)
print(show_matrix(M, "M"))
print("고윳값 :", 값m, " -> 양수", int((값m > 0).sum()), "개")
print("교환 없는 피벗 :", 피벗들(M), " -> 양수",
int((피벗들(M) > 0).sum()), "개 맞다")
print("scipy.linalg.lu :", np.diag(U), " -> 양수",
int((np.diag(U) > 0).sum()), "개 틀리다")M
[ 1 2 ]
[ 2 1 ]
고윳값 : [-1. 3.] -> 양수 1 개
교환 없는 피벗 : [ 1. -3.] -> 양수 1 개 맞다
scipy.linalg.lu : [2. 1.5] -> 양수 2 개 틀리다
대각합과 행렬식으로도 검산된다¶
p = 피벗들(S)
값, _ = np.linalg.eigh(S)
print("피벗 :", p, " 곱 =", np.prod(p))
print("고윳값 :", 값, " 곱 =", np.prod(값))
print("det S :", np.linalg.det(S))
print()
print("대각합 :", np.trace(S), " = 고윳값의 합", 값.sum())
print()
print("주의 : 피벗과 고윳값은 곱만 같지 하나하나는 다르다.")피벗 : [6. 6. 6.] 곱 = 216.0
고윳값 : [ 3. 6. 12.] 곱 = 215.99999999999994
det S : 216.00000000000006
대각합 : 21.0 = 고윳값의 합 21.0
주의 : 피벗과 고윳값은 곱만 같지 하나하나는 다르다.
6. eig 와 eigh 는 무엇이 다른가¶
대칭행렬에는 eigh 를 써야 한다.
값1, V1 = np.linalg.eig(S)
값2, V2 = np.linalg.eigh(S)
print("eig 의 고윳값 :", 값1, " 자료형", 값1.dtype)
print("eigh 의 고윳값 :", 값2, " 자료형", 값2.dtype)
print()
print("eig 의 Q^T Q - I 최대 :", np.abs(V1.T @ V1 - np.eye(3)).max())
print("eigh 의 Q^T Q - I 최대 :", np.abs(V2.T @ V2 - np.eye(3)).max())eig 의 고윳값 : [12.+0.j 6.+0.j 3.+0.j] 자료형 complex128
eigh 의 고윳값 : [ 3. 6. 12.] 자료형 float64
eig 의 Q^T Q - I 최대 : 2.869000362951402e-16
eigh 의 Q^T Q - I 최대 : 4.440892098500626e-16
세 가지가 다르다.
eigh는 실수를 돌려준다.eig는 대칭행렬에도 복소 자료형을 줄 수 있다.eigh는 고윳값을 오름차순으로 정렬해 준다.eigh는 대칭이라는 사실을 이용하므로 더 빠르고 더 정확하다.
고윳값이 겹칠 때 차이가 더 크다. 겹친 고유공간 안에서 eig 는 직교를 보장하지 않는다.
겹침 = 3.0 * np.eye(3) + np.ones((3, 3)) # 고윳값 6, 3, 3
print(show_matrix(겹침, "3I + J"))
print("고윳값 :", np.linalg.eigvalsh(겹침))
for 이름, 함수 in (("eig", np.linalg.eig), ("eigh", np.linalg.eigh)):
_, V = 함수(겹침)
V = np.real(V)
print(f" {이름:>5} 의 Q^T Q - I 최대 : {np.abs(V.T @ V - np.eye(3)).max():.2e}")3I + J
[ 4 1 1 ]
[ 1 4 1 ]
[ 1 1 4 ]
고윳값 : [3. 3. 6.]
eig 의 Q^T Q - I 최대 : 1.89e-01
eigh 의 Q^T Q - I 최대 : 4.44e-16
마치며...¶
| 서술 파트의 내용 | 이 노트북의 코드 |
|---|---|
| 고윳값이 실수 | 무작위 500개의 허수부가 10-15 수준 |
| 고유벡터가 직교 | 같은 500개에서 |
| 대칭이 아니면 | 비대칭 500개 중 90도인 것은 거의 없다 |
| 슬라이더 | 에서만 90도. 이면 복소, 은 결함 |
| 복원 오차 10-15, | |
| 거꾸로 | 는 늘 대칭 |
| 랭크 1 벽돌 | 하나씩 쌓으면 오차가 0으로 |
| 투영 성질 | , , 랭크 1, 대각합 1 |
| 간섭 없음 | |
| 완비성 | |
| 좌표 | 연립방정식 대신 내적 |
| 실베스터 | 무작위 400개에서 어긋난 것 0개 |
| 함정 | scipy.linalg.lu 는 행을 바꿔 부호 개수가 틀린다 |
더 해 볼 것¶
2절의 슬라이더에서 대신 를 써 보자. 이번에는 를 키우면 무엇이 먼저 깨지는가?
4절의 벽돌을 큰 부터 하나만 써서 를 근사해 보자. 오차가 얼마인가? 이것이 L29에서 다룰 저랭크 근사의 맛보기이다.
가 아무 행렬일 때 는 대칭인가? 코드로 확인하고 전치로 증명해 보자. 그 고윳값의 부호는 어떤가?
5절의
피벗들이None을 돌려주는 대칭행렬을 하나 찾아보자. 그런 경우에도 실베스터 관성 법칙은 성립하지만 소거 방식을 바꿔야 한다.
다음 강의에서는 잠시 실수의 세계를 벗어난다. 복소수 위에서 직교를 정의하는 일부터 시작해서, 직교기저의 챔피언인 푸리에 행렬을 만난다.