Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Lecture 25. 대칭행렬과 스펙트럼 정리 — 파이썬 실습

Symmetric Matrices and the Spectral Theorem — 실습

L25 서술 파트의 주장은 S=STS = S^{\mathsf T} 하나로 두 가지가 따라온다는 것이었다. 고윳값이 전부 실수이고, 고유벡터를 서로 직교하게 고를 수 있다.

이 노트북에서는 무작위 대칭행렬로 그 두 가지를 확인하고, 대칭성을 슬라이더로 조금씩 깨뜨리며 고유벡터의 사잇각이 90도에서 어떻게 벗어나는지 본다. 그리고 랭크 1 벽돌을 하나씩 쌓아 행렬이 복원되는 것을 확인한다.

서술 파트의 내용여기서 확인하는 방법
고윳값이 실수무작위 대칭행렬 500개의 허수부
고유벡터가 직교QTQQ^{\mathsf T}QII 인지
대칭이 아니면 깨진다슬라이더로 사잇각이 90도에서 벗어난다
S=QΛQTS = Q\Lambda Q^{\mathsf T}복원 오차와 Q1=QTQ^{-1} = Q^{\mathsf T}
랭크 1 벽돌하나씩 쌓아 가며 오차가 줄어드는 것
qqT\vv{q}\vv{q}^{\mathsf T} 가 투영P2=PP^2 = P, PT=PP^{\mathsf T} = P, 랭크 1
qiqiT=I\sum\vv{q}_i\vv{q}_i^{\mathsf T} = I완비성
실베스터 관성 법칙교환 없는 소거의 피벗 부호
행 교환의 함정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. 두 선물을 확인한다

무작위 대칭행렬을 잔뜩 만들어 예외를 찾아보자. 대칭행렬은 아무 행렬 BB 에 대해 B+BTB + B^{\mathsf T} 로 만들면 된다.

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개

2. 대칭성을 조금씩 깨뜨려 보자

M(t)=[21+t12]M(t) = \begin{bmatrix} 2 & 1 + t \\ 1 & 2 \end{bmatrix}

t=0t = 0 이면 대칭이다. tt 를 움직이면 무슨 일이 벌어지는지 슬라이더로 보자.

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도

t=0t = 0 에서만 정확히 90도이다. t<1t < -1 이면 고윳값이 복소수가 되어 실수 고유벡터가 아예 없어지고, t=1t = -1 은 그 경계로 고윳값이 겹치면서 고유벡터가 하나뿐인 결함 행렬이 된다.

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)
Loading...

슬라이더를 t=0t = 0 에 맞추면 점선 둘이 정확히 직각을 이루고, 주황 타원의 축도 그 점선 위에 정확히 놓인다. 조금만 움직여도 둘 다 어긋난다.

3. 스펙트럼 정리

이번 강의의 앵커는 3×33 \times 3 이다. 고윳값이 3,6,123, 6, 12 이고 고유벡터가 (1,1,1)(1,1,1), (1,1,0)(1,-1,0), (1,1,2)(1,1,-2) 이다.

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

Q1Q^{-1} 을 구하는 데 드는 일과 QTQ^{\mathsf T} 를 구하는 데 드는 일을 비교하면 차이가 분명하다. 전치는 곱셈이 한 번도 없다.

곱셈·나눗셈 횟수
Q1Q^{-1} (소거)n3n^3
QTQ^{\mathsf T}0

거꾸로도 성립한다

아무직교 = 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]   <- 같은 수들

4. 랭크 1 벽돌 — 이 강의의 심장

S=λ1q1q1T+λ2q2q2T++λnqnqnTS = \lambda_1\vv{q}_1\vv{q}_1^{\mathsf T} + \lambda_2\vv{q}_2\vv{q}_2^{\mathsf T} + \cdots + \lambda_n\vv{q}_n\vv{q}_n^{\mathsf T}
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

투영이 서로 간섭하지 않는다

PiP_i 는 자기 방향은 그대로 두고 다른 고유방향은 0으로 보낸다. 그래서 PiPj=0P_iP_j = 0 이다 (iji \neq j).

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 이다. 서로 간섭하지 않는다.

SxS\vv{x} 를 한 줄로 읽는다

Sx=iλi(qiTx)qiS\vv{x} = \sum_i \lambda_i\left(\vv{q}_i^{\mathsf T}\vv{x}\right)\vv{q}_i

쪼개고, 각각에 λ\lambda 를 곱하고, 도로 더한다.

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. eigeigh 는 무엇이 다른가

대칭행렬에는 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

세 가지가 다르다.

  1. eigh실수를 돌려준다. eig 는 대칭행렬에도 복소 자료형을 줄 수 있다.

  2. eigh 는 고윳값을 오름차순으로 정렬해 준다.

  3. 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개에서 QTQ=IQ^{\mathsf T}Q = I
대칭이 아니면비대칭 500개 중 90도인 것은 거의 없다
슬라이더t=0t=0 에서만 90도. t<1t<-1 이면 복소, t=1t=-1 은 결함
S=QΛQTS = Q\Lambda Q^{\mathsf T}복원 오차 10-15, Q1=QTQ^{-1} = Q^{\mathsf T}
거꾸로QDQTQDQ^{\mathsf T} 는 늘 대칭
랭크 1 벽돌하나씩 쌓으면 오차가 0으로
투영 성질P2=PP^2 = P, PT=PP^{\mathsf T} = P, 랭크 1, 대각합 1
간섭 없음PiPj=0P_iP_j = 0
완비성Pi=I\sum P_i = I
좌표연립방정식 대신 내적
실베스터무작위 400개에서 어긋난 것 0개
함정scipy.linalg.lu 는 행을 바꿔 부호 개수가 틀린다

더 해 볼 것

  1. 2절의 슬라이더에서 M(t)M(t) 대신 [21+t1t2]\begin{bmatrix} 2 & 1+t \\ 1-t & 2\end{bmatrix} 를 써 보자. 이번에는 tt 를 키우면 무엇이 먼저 깨지는가?

  2. 4절의 벽돌을 λ\lambda 부터 하나만 써서 SS 를 근사해 보자. 오차가 얼마인가? 이것이 L29에서 다룰 저랭크 근사의 맛보기이다.

  3. AA 가 아무 m×nm \times n 행렬일 때 ATAA^{\mathsf T}A 는 대칭인가? 코드로 확인하고 전치로 증명해 보자. 그 고윳값의 부호는 어떤가?

  4. 5절의 피벗들None 을 돌려주는 대칭행렬을 하나 찾아보자. 그런 경우에도 실베스터 관성 법칙은 성립하지만 소거 방식을 바꿔야 한다.

다음 강의에서는 잠시 실수의 세계를 벗어난다. 복소수 위에서 직교를 정의하는 일부터 시작해서, 직교기저의 챔피언인 푸리에 행렬을 만난다.