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 34. 전체 정리 — 다섯 개의 분해 — 파이썬 실습

Five Decompositions, and the Map They Draw — 실습

L34 서술 파트의 결론은 둘이었다. 다섯 조건은 사슬이 아니다. 그리고 네 부분공간의 그림 한 장에 전부가 있다.

이 노트북에서는 다섯분해 함수 하나를 만들어 같은 행렬에 다섯을 전부 걸어 보고, 어느 것이 걸리고 어느 것이 통과하는지 표로 확인한다. 그다음 네 부분공간 최종본을 회전 가능한 그림으로 그리고, 마지막으로 이 책에 나온 행렬을 전부 모아 한 번씩 통과시킨다.

서술 파트의 내용여기서 확인하는 방법
다섯 개의 분해다섯분해(A) 한 줄
TT 는 다섯을 다 통과한다아래 세 줄의 숫자가 같아진다
조건은 사슬이 아니다반례 셋을 직접 만든다
인구조사 80 / 80 / 20 / 60 / 100무작위 행렬 2000개
σ=3,1,0\sigma = 3, 1, 0손으로 세운 값과 svd 대조
u\vv{u}Av/σA\vv{v}/\sigma 로 얻는다따로 구하면 부호가 깨진다
네 부분공간 최종본회전 가능한 3차원 그림 두 장
A+A=Iv3v3TA^{+}A = I - \vv{v}_3\vv{v}_3^{\mathsf T}투영행렬 성질
옳은 공식과 쓰는 공식연산 횟수와 조건수
좋은 기저FFT 와 대각화
조건이 없는 도구복소 고윳값 88%
행렬 동물원마지막으로 한 번씩

0. 준비

import numpy as np
import plotly.graph_objects as go
from scipy.linalg import lu

from linalg_viz import COLORS, arrow, layout3d, line, plane, show_matrix, slider_figure

np.set_printoptions(precision=4, suppress=True)
rng = np.random.default_rng(34)
print("numpy", np.__version__)
numpy 2.5.2
# 이 강의에서 계속 쓸 두 행렬
T = np.array([[2.0, -1.0, 0.0],
              [-1.0, 2.0, -1.0],
              [0.0, -1.0, 2.0]])          # L27 의 삼중대각행렬
A = np.array([[1.0, 1.0, 2.0],
              [1.0, 0.0, 1.0],
              [0.0, 1.0, 1.0]])           # L29 의 C 에 열 하나를 더한 것
print(show_matrix(T, "T   다섯을 전부 통과한다"))
print(show_matrix(A, "A   마지막 그림의 주인공. 3열 = 1열 + 2열"))
print("A 의 앞 두 열이 L29 의 앵커 C 인가 :",
      np.allclose(A[:, :2], np.array([[1.0, 1], [1, 0], [0, 1]])))
T   다섯을 전부 통과한다
[   2   -1    0 ]
[  -1    2   -1 ]
[   0   -1    2 ]
A   마지막 그림의 주인공. 3열 = 1열 + 2열
[  1   1   2 ]
[  1   0   1 ]
[  0   1   1 ]
A 의 앞 두 열이 L29 의 앵커 C 인가 : True

1. 다섯분해 — 한 행렬에 다섯을 전부 건다

각 줄이 무엇을 요구하고 무엇을 돌려주는지를 그대로 코드로 옮긴다. 요구가 맞지 않으면 왜 맞지 않는지도 함께 적는다.

def 다섯분해(M, 이름="A", 출력=True):
    """같은 행렬을 다섯 가지로 쪼개고, 각각이 무엇을 알려주는지 한눈에 본다.

    돌려주는 것은 {분해이름: (가능한가, 설명, 조각들)} 형태의 사전이다.
    """
    M = np.asarray(M, dtype=float)
    m, n = M.shape
    r = np.linalg.matrix_rank(M)
    정방 = m == n
    보고 = {}

    # 1) A = LU  —  정방이고 행 교환 없이 소거가 되어야 한다.
    #    선행 주소행렬식으로 재면 안 된다. 그것은 피벗이 n 개 다 잡히는 것까지
    #    요구해서 [[1,2],[2,4]] 같은 특이행렬을 떨어뜨린다. 그 행렬은 교환 없이
    #    소거되고 L @ U 가 정확히 원래 행렬이다. 그래서 직접 소거해 본다.
    if not 정방:
        보고["A = LU"] = (False, "정방이 아니다", ())
    else:
        허용 = 1e-11 * max(1.0, float(np.abs(M).max()))
        Ue, Le, 걸린곳 = M.copy(), np.eye(n), 0
        for k in range(n):
            if abs(Ue[k, k]) <= 허용:
                if np.all(np.abs(Ue[k + 1:, k]) <= 허용):
                    continue                  # 바꿀 행이 없다. 자유열이니 넘어간다
                걸린곳 = k + 1
                break
            곱수 = Ue[k + 1:, k] / Ue[k, k]
            Le[k + 1:, k] = 곱수
            Ue[k + 1:, k:] -= np.outer(곱수, Ue[k, k:])
        피벗 = np.diag(Ue)
        곱 = float(np.prod(피벗))
        곱 = 0.0 if 곱 == 0.0 else 곱          # -0.0 이 찍히는 것을 막는다
        if 걸린곳:
            보고["A = LU"] = (False, f"{걸린곳}번째 피벗 자리가 0이라 행을 바꿔야 한다",
                              lu(M))
        else:
            꼬리 = ("" if float(np.abs(피벗).min()) > 허용
                    else "  (0 인 피벗이 있어 후진 대입은 못 한다)")
            보고["A = LU"] = (True,
                              f"피벗 {np.round(피벗, 4)}, 곱 {곱:.4f}{꼬리}",
                              (np.eye(n), Le, Ue))

    # 2) A = QR  —  열이 독립이어야 한다
    Q, R = np.linalg.qr(M)
    if r == n:
        보고["A = QR"] = (True, f"|det R| = {abs(np.prod(np.diag(R))):.4f}", (Q, R))
    else:
        보고["A = QR"] = (False, f"열이 종속 (랭크 {r} < 열 {n})", (Q, R))

    # 3) S = Q L Q^T  —  대칭이어야 한다
    if 정방 and np.allclose(M, M.T):
        w, V = np.linalg.eigh(M)
        순 = np.argsort(w)[::-1]
        부호 = "전부 양수" if w.min() > 1e-12 else ("전부 음이 아님" if w.min() > -1e-12
                                                  else "부호가 섞임")
        보고["S = Q L Q^T"] = (True, f"lambda {np.round(w[순], 4)}, {부호}",
                               (V[:, 순], w[순]))
    else:
        보고["S = Q L Q^T"] = (False, "대칭이 아니다" if 정방 else "정방이 아니다", ())

    # 4) A = S L S^-1  —  독립인 고유벡터가 n 개여야 한다
    if 정방:
        w2, S = np.linalg.eig(M)
        cond = np.linalg.cond(S)
        복소 = np.max(np.abs(w2.imag)) > 1e-9
        if cond < 1e8:
            꼬리 = " (복소수에서만)" if 복소 else ""
            보고["A = S L S^-1"] = (True, f"cond(S) = {cond:.4g}{꼬리}", (S, w2))
        else:
            보고["A = S L S^-1"] = (False, f"고유벡터가 모자란다 (cond(S) = {cond:.2e})",
                                    (S, w2))
    else:
        보고["A = S L S^-1"] = (False, "정방이 아니다", ())

    # 5) A = U S V^T  —  요구하는 것이 없다
    U_, s, Vt = np.linalg.svd(M)
    조건수 = s[0] / s[-1] if s[-1] > 1e-14 else np.inf
    보고["A = U S V^T"] = (True, f"sigma {np.round(s, 4)}, cond {조건수:.4g}",
                           (U_, s, Vt))

    if 출력:
        print(f"{이름}   ({m}x{n}, 랭크 {r}, "
              f"{'대칭' if 정방 and np.allclose(M, M.T) else '비대칭'})")
        print("-" * 78)
        for 줄, (됨, 설명, _) in 보고.items():
            print(f"  {줄:<14}{'O' if 됨 else 'X'}   {설명}")
        통과 = sum(1 for 됨, _, _ in 보고.values() if 됨)
        print(f"  {'':<14}     -> 다섯 중 {통과}개 통과")
    return 보고
보T = 다섯분해(T, "T = tridiag(-1, 2, -1)")
T = tridiag(-1, 2, -1)   (3x3, 랭크 3, 대칭)
------------------------------------------------------------------------------
  A = LU        O   피벗 [2.     1.5    1.3333], 곱 4.0000
  A = QR        O   |det R| = 4.0000
  S = Q L Q^T   O   lambda [3.4142 2.     0.5858], 전부 양수
  A = S L S^-1  O   cond(S) = 1
  A = U S V^T   O   sigma [3.4142 2.     0.5858], cond 5.828
                     -> 다섯 중 5개 통과
보A = 다섯분해(A, "A = [[1,1,2],[1,0,1],[0,1,1]]")
A = [[1,1,2],[1,0,1],[0,1,1]]   (3x3, 랭크 2, 비대칭)
------------------------------------------------------------------------------
  A = LU        O   피벗 [ 1. -1.  0.], 곱 0.0000  (0 인 피벗이 있어 후진 대입은 못 한다)
  A = QR        X   열이 종속 (랭크 2 < 열 3)
  S = Q L Q^T   X   대칭이 아니다
  A = S L S^-1  O   cond(S) = 6.029
  A = U S V^T   O   sigma [3. 1. 0.], cond inf
                     -> 다섯 중 3개 통과

서술 파트의 숫자와 대조

P, L, U = 보T["A = LU"][2]
print("T 의 피벗 :", np.diag(U), "  서술의 2, 1.5, 4/3 인가 :",
      np.allclose(np.diag(U), [2.0, 1.5, 4/3]))
print("행 교환이 있었는가 :", not np.allclose(P, np.eye(3)),
      "  L @ U 가 T 인가 :", np.allclose(L @ U, T))
print("피벗의 곱 :", np.prod(np.diag(U)), "  det T :", np.linalg.det(T))
print()
Q, R = 보T["A = QR"][2]
print("|det R| :", abs(np.prod(np.diag(R))), "  <- 직교행렬은 부피를 안 바꾼다")
print()
_, s, _ = 보T["A = U S V^T"][2]
print("T 의 특이값 :", s)
print("2+sqrt2, 2, 2-sqrt2 :", [2 + np.sqrt(2), 2.0, 2 - np.sqrt(2)])
print("cond(T) :", np.linalg.cond(T), "  3 + 2 sqrt2 :", 3 + 2*np.sqrt(2))
print("sigma 의 곱 :", np.prod(s), "  |det T| :", abs(np.linalg.det(T)))
T 의 피벗 : [2.     1.5    1.3333]   서술의 2, 1.5, 4/3 인가 : True
행 교환이 있었는가 : False   L @ U 가 T 인가 : True
피벗의 곱 : 4.0   det T : 4.0

|det R| : 4.0   <- 직교행렬은 부피를 안 바꾼다

T 의 특이값 : [3.4142 2.     0.5858]
2+sqrt2, 2, 2-sqrt2 : [np.float64(3.414213562373095), 2.0, np.float64(0.5857864376269049)]
cond(T) : 5.8284271247461925   3 + 2 sqrt2 : 5.82842712474619
sigma 의 곱 : 3.9999999999999973   |det T| : 4.0

아래 세 줄은 정말 같은 것인가

부호는 관례일 뿐이므로, 열마다 0이 아닌 첫 성분을 양수로 맞춘 뒤에 견준다.

def 부호맞춤(M):
    """열마다 0이 아닌 첫 성분이 양수가 되도록 부호를 고른다."""
    M = np.array(M, dtype=float)
    for j in range(M.shape[1]):
        큰것 = np.flatnonzero(np.abs(M[:, j]) > 1e-8)
        if 큰것.size and M[큰것[0], j] < 0:
            M[:, j] *= -1
    return M
Qs, 람 = 보T["S = Q L Q^T"][2]
S2, 람2 = 보T["A = S L S^-1"][2]
순2 = np.argsort(람2.real)[::-1]
Uu, 시그, Vt = 보T["A = U S V^T"][2]

Qs, S2, Uu, Vv = (부호맞춤(Qs), 부호맞춤(S2[:, 순2].real),
                  부호맞춤(Uu), 부호맞춤(Vt.T))
print(show_matrix(Qs, "Q   (eigh)"))
print(show_matrix(S2, "S   (eig)"))
print(show_matrix(Uu, "U   (svd)"))
print(show_matrix(Vv, "V   (svd)"))
print("Q == S :", np.allclose(Qs, S2))
print("Q == U :", np.allclose(Qs, Uu))
print("U == V :", np.allclose(Uu, Vv))
print("lambda == sigma :", np.allclose(np.sort(람)[::-1], 시그))
print()
print("-> 대칭 양정치라서 세 분해가 하나로 무너진다.")
Q   (eigh)
[        0.5       0.707         0.5 ]
[     -0.707   -3.12e-16       0.707 ]
[        0.5      -0.707         0.5 ]
S   (eig)
[        0.5       0.707         0.5 ]
[     -0.707   -4.05e-16       0.707 ]
[        0.5      -0.707         0.5 ]
U   (svd)
[       0.5      0.707        0.5 ]
[    -0.707   3.89e-16      0.707 ]
[       0.5     -0.707        0.5 ]
V   (svd)
[        0.5       0.707         0.5 ]
[     -0.707   -1.11e-16       0.707 ]
[        0.5      -0.707         0.5 ]
Q == S : True
Q == U : True
U == V : True
lambda == sigma : True

-> 대칭 양정치라서 세 분해가 하나로 무너진다.

그런데 AA 에서는 둘이 걸린다

for 줄 in ("A = LU", "A = QR", "S = Q L Q^T", "A = S L S^-1", "A = U S V^T"):
    됨, 설명, _ = 보A[줄]
    print(f"{줄:<14}{'통과' if 됨 else '실패':<6}{설명}")
print()
print("A 의 고윳값 :", np.sort(np.linalg.eigvals(A).real))
print("0, 1-sqrt2, 1+sqrt2 :", sorted([0.0, 1 - np.sqrt(2), 1 + np.sqrt(2)]))
print()
print("-> 랭크가 모자라 QR 은 안 되는데, 고윳값이 서로 달라 대각화는 된다.")
print("   LU 는 통과한다. 교환 없이 소거가 끝나고 마지막 피벗만 0 이다.")
A = LU        통과    피벗 [ 1. -1.  0.], 곱 0.0000  (0 인 피벗이 있어 후진 대입은 못 한다)
A = QR        실패    열이 종속 (랭크 2 < 열 3)
S = Q L Q^T   실패    대칭이 아니다
A = S L S^-1  통과    cond(S) = 6.029
A = U S V^T   통과    sigma [3. 1. 0.], cond inf

A 의 고윳값 : [-0.4142 -0.      2.4142]
0, 1-sqrt2, 1+sqrt2 : [np.float64(-0.41421356237309515), 0.0, np.float64(2.414213562373095)]

-> 랭크가 모자라 QR 은 안 되는데, 고윳값이 서로 달라 대각화는 된다.
   LU 는 통과한다. 교환 없이 소거가 끝나고 마지막 피벗만 0 이다.

2. 조건은 사슬이 아니다

서술 파트의 반례 셋을 코드로 확인한다.

반례 = (
    ("대칭인데 LU 가 없다", np.array([[0.0, 1.0], [1.0, 0.0]]), "S = Q L Q^T", "A = LU"),
    ("LU 는 되는데 대각화가 안 된다", np.array([[3.0, 1.0], [0.0, 3.0]]),
     "A = LU", "A = S L S^-1"),
    ("대각화는 되는데 QR 이 없다", A, "A = S L S^-1", "A = QR"),
)
for 제목, M, 되는것, 안되는것 in 반례:
    보 = 다섯분해(M, 제목, 출력=False)
    print(f"{제목}")
    print(f"   {되는것:<14}{'O' if 보[되는것][0] else 'X'}   {보[되는것][1]}")
    print(f"   {안되는것:<14}{'O' if 보[안되는것][0] else 'X'}   {보[안되는것][1]}")
    print()
print("-> 어느 조건도 다른 조건을 품지 않는다.")
대칭인데 LU 가 없다
   S = Q L Q^T   O   lambda [ 1. -1.], 부호가 섞임
   A = LU        X   1번째 피벗 자리가 0이라 행을 바꿔야 한다

LU 는 되는데 대각화가 안 된다
   A = LU        O   피벗 [3. 3.], 곱 9.0000
   A = S L S^-1  X   고유벡터가 모자란다 (cond(S) = 3.00e+15)

대각화는 되는데 QR 이 없다
   A = S L S^-1  O   cond(S) = 6.029
   A = QR        X   열이 종속 (랭크 2 < 열 3)

-> 어느 조건도 다른 조건을 품지 않는다.

참인 포함은 하나뿐이다

대칭이면 반드시 대각화된다. 그 역은 거짓이다.

print("대칭 행렬 500개를 만들어 전부 대각화되는지 본다")
깨진것 = 0
for _ in range(500):
    n = int(rng.integers(2, 6))
    B = rng.normal(size=(n, n))
    Sym = B + B.T
    _, V = np.linalg.eigh(Sym)
    if not np.allclose(V @ V.T, np.eye(n)):
        깨진것 += 1
print("  고유벡터가 정규직교가 아닌 경우 :", 깨진것, "/ 500")
print()
print("역은 거짓인가 :  M = [[2,1],[0,3]]")
M = np.array([[2.0, 1.0], [0.0, 3.0]])
w, V = np.linalg.eig(M)
print("  고윳값", w, " cond(V)", f"{np.linalg.cond(V):.4f}", " -> 대각화 O")
print("  대칭인가 :", np.allclose(M, M.T), " -> 대칭 X")
대칭 행렬 500개를 만들어 전부 대각화되는지 본다
  고유벡터가 정규직교가 아닌 경우 : 0 / 500

역은 거짓인가 :  M = [[2,1],[0,3]]
  고윳값 [2.+0.j 3.+0.j]  cond(V) 2.4142  -> 대각화 O
  대칭인가 : False  -> 대칭 X

인구조사 — 무작위 행렬 2000개

대각화 가능 여부는 만들 때 정해 둔 사실을 쓴다. numpy 로 판정하면 결함 행렬을 대각화 가능이라고 잘못 보고하기 때문이다(L28). 그 오보율도 함께 잰다.

def 무작위행렬(종류, rng):
    """다섯 족(族)에서 하나씩 뽑는다. (행렬, 대각화 가능한가) 를 돌려준다."""
    n = int(rng.integers(2, 6))
    if 종류 == "일반 정방":
        return rng.normal(size=(n, n)), True
    if 종류 == "대칭":
        B = rng.normal(size=(n, n))
        return B + B.T, True
    if 종류 == "결함":                      # 조르당 블록을 닮음변환으로 감춘다
        J = np.eye(n) * rng.normal()
        for i in range(n - 1):
            J[i, i + 1] = 1.0
        Mv = rng.normal(size=(n, n))
        while abs(np.linalg.det(Mv)) < 1e-2:
            Mv = rng.normal(size=(n, n))
        return Mv @ J @ np.linalg.inv(Mv), False
    if 종류 == "랭크부족":
        r = max(1, n - 1)
        return rng.normal(size=(n, r)) @ rng.normal(size=(r, n)), None
    if 종류 == "직사각":
        m = n + int(rng.integers(1, 4))
        return rng.normal(size=(m, n)), False
    raise ValueError(종류)
종류들 = ["일반 정방", "대칭", "결함", "랭크부족", "직사각"]
줄이름 = ["A = LU", "A = QR", "S = Q L Q^T", "A = S L S^-1", "A = U S V^T"]
N = 400
속은횟수, 조건수들 = 0, []
집계 = {}
rng2 = np.random.default_rng(34)
for 종류 in 종류들:
    합 = np.zeros(5)
    for _ in range(N):
        M, 참 = 무작위행렬(종류, rng2)
        보 = 다섯분해(M, 출력=False)
        표 = [보[k][0] for k in 줄이름]
        if 참 is not None:                       # 대각화는 만들 때의 사실을 쓴다
            표[3] = bool(참) and M.shape[0] == M.shape[1]
        합 += np.array(표, dtype=float)
        if 종류 == "결함":
            _, V = np.linalg.eig(M)
            조건수들.append(np.linalg.cond(V))
            속은횟수 += int(np.linalg.matrix_rank(V) == M.shape[0])
    집계[종류] = 합 / N * 100

print(f"{'':>10}" + "".join(f"{k:>14}" for k in 줄이름))
for 종류 in 종류들:
    print(f"{종류:>10}" + "".join(f"{v:>13.1f}%" for v in 집계[종류]))
전체 = np.mean([집계[k] for k in 종류들], axis=0)
print(f"{'전체':>10}" + "".join(f"{v:>13.1f}%" for v in 전체))
print()
print("서술 파트의 80 / 80 / 20 / 60 / 100 과 맞는가 :", np.round(전체, 1))
print("LU 와 QR 이 똑같이 80% 인데, LU 는 직사각에서 QR 은 랭크부족에서 걸린다.")
                  A = LU        A = QR   S = Q L Q^T  A = S L S^-1   A = U S V^T
     일반 정방        100.0%        100.0%          0.0%        100.0%        100.0%
        대칭        100.0%        100.0%        100.0%        100.0%        100.0%
        결함        100.0%        100.0%          0.0%          0.0%        100.0%
      랭크부족        100.0%          0.0%          0.0%        100.0%        100.0%
       직사각          0.0%        100.0%          0.0%          0.0%        100.0%
        전체         80.0%         80.0%         20.0%         60.0%        100.0%

서술 파트의 80 / 80 / 20 / 60 / 100 과 맞는가 : [ 80.  80.  20.  60. 100.]
LU 와 QR 이 똑같이 80% 인데, LU 는 직사각에서 QR 은 랭크부족에서 걸린다.
print(f"결함 행렬 {N}개에 numpy 를 그대로 믿으면")
print(f"  matrix_rank(V) 가 '꽉 찼다'고 본 비율 : {속은횟수/N*100:.1f}%")
print(f"  고유벡터 행렬 조건수 중앙값 : {np.median(조건수들):.3e}")
print(f"  그중 가장 작은 것          : {min(조건수들):.3e}")
print()
print("-> 조르당 형은 수치적으로 존재하지 않는다는 L28 의 결론 그대로다.")
결함 행렬 400개에 numpy 를 그대로 믿으면
  matrix_rank(V) 가 '꽉 찼다'고 본 비율 : 95.0%
  고유벡터 행렬 조건수 중앙값 : 2.398e+11
  그중 가장 작은 것          : 3.606e+07

-> 조르당 형은 수치적으로 존재하지 않는다는 L28 의 결론 그대로다.

3. 네 부분공간 최종본

이 강의의 결론이다. AA 의 SVD 를 손으로 세운 값과 대조하고, 네 칸을 채우고, AAA+A^{+} 의 화살표를 양쪽으로 그린다.

print(show_matrix(A.T @ A, "A^T A"))
print("눈으로 찾은 고유벡터로 확인")
for v, 배 in (([1.0, 1, 2], 9), ([1.0, -1, 0], 1), ([1.0, 1, -1], 0)):
    v = np.array(v)
    print(f"  A^T A {str(v):<12} = {str(A.T @ A @ v):<16} = {배} x {v}   "
          f"{np.allclose(A.T @ A @ v, 배 * v)}")
print()
print("특이값 :", np.linalg.svd(A, compute_uv=False))
print("sqrt(9), sqrt(1), sqrt(0) :", [3.0, 1.0, 0.0])
A^T A
[  2   1   3 ]
[  1   2   3 ]
[  3   3   6 ]
눈으로 찾은 고유벡터로 확인
  A^T A [1. 1. 2.]   = [ 9.  9. 18.]    = 9 x [1. 1. 2.]   True
  A^T A [ 1. -1.  0.] = [ 1. -1.  0.]    = 1 x [ 1. -1.  0.]   True
  A^T A [ 1.  1. -1.] = [0. 0. 0.]       = 0 x [ 1.  1. -1.]   True

특이값 : [3. 1. 0.]
sqrt(9), sqrt(1), sqrt(0) : [3.0, 1.0, 0.0]
v1 = np.array([1.0, 1, 2]) / np.sqrt(6)
v2 = np.array([1.0, -1, 0]) / np.sqrt(2)
v3 = np.array([1.0, 1, -1]) / np.sqrt(3)
시그마 = np.array([3.0, 1.0, 0.0])

u1 = A @ v1 / 시그마[0]              # u 는 반드시 A v / sigma 로 얻는다
u2 = A @ v2 / 시그마[1]
u3 = np.array([-1.0, 1, 1]) / np.sqrt(3)

V손 = np.column_stack([v1, v2, v3])
U손 = np.column_stack([u1, u2, u3])
print("u1 =", u1, "  (2,1,1)/sqrt6 =", np.array([2.0, 1, 1])/np.sqrt(6))
print("u2 =", u2, "  (0,1,-1)/sqrt2 =", np.array([0.0, 1, -1])/np.sqrt(2))
print()
print("V 가 직교인가 :", np.allclose(V손.T @ V손, np.eye(3)))
print("U 가 직교인가 :", np.allclose(U손.T @ U손, np.eye(3)))
print("U S V^T 로 A 가 복원되는가 :",
      np.allclose(U손 @ np.diag(시그마) @ V손.T, A),
      "  오차", f"{np.linalg.norm(U손 @ np.diag(시그마) @ V손.T - A):.2e}")
print()
print("A v3     =", np.round(A @ v3, 12), " (영공간)")
print("A^T u3   =", np.round(A.T @ u3, 12), " (좌영공간)")
u1 = [0.8165 0.4082 0.4082]   (2,1,1)/sqrt6 = [0.8165 0.4082 0.4082]
u2 = [ 0.      0.7071 -0.7071]   (0,1,-1)/sqrt2 = [ 0.      0.7071 -0.7071]

V 가 직교인가 : True
U 가 직교인가 : True
U S V^T 로 A 가 복원되는가 : True   오차 6.89e-16

A v3     = [0. 0. 0.]  (영공간)
A^T u3   = [0. 0. 0.]  (좌영공간)

u\vv{u} 를 따로 구하면 어떻게 되는가

L29에서 경고한 그 함정이다. AATAA^{\mathsf T} 에서 u\vv{u} 를 따로 뽑으면 부호가 v\vv{v} 와 맞는다는 보장이 없다.

w, U따로 = np.linalg.eigh(A @ A.T)
순 = np.argsort(w)[::-1]
U따로 = U따로[:, 순]
print(show_matrix(U따로, "A A^T 의 고유벡터를 따로 뽑은 것"))
print(show_matrix(U손, "A v / sigma 로 얻은 것"))
print()
복원 = U따로 @ np.diag(시그마) @ V손.T
print("따로 뽑은 U 로 복원하면 A 가 되는가 :", np.allclose(복원, A))
print("복원 오차 :", f"{np.linalg.norm(복원 - A):.4f}")
print("어느 열의 부호가 뒤집혔는가 :",
      [j for j in range(3) if not np.allclose(U따로[:, j], U손[:, j])])
A A^T 의 고유벡터를 따로 뽑은 것
[  -0.816        0   -0.577 ]
[  -0.408   -0.707    0.577 ]
[  -0.408    0.707    0.577 ]
A v / sigma 로 얻은 것
[   0.816        0   -0.577 ]
[   0.408    0.707    0.577 ]
[   0.408   -0.707    0.577 ]

따로 뽑은 U 로 복원하면 A 가 되는가 : False
복원 오차 : 6.3246
어느 열의 부호가 뒤집혔는가 : [0, 1]

화살표를 양쪽으로

A플러스 = np.linalg.pinv(A)
print(show_matrix(A플러스 * 9, "9 A^+   <- 서술의 [[1,5,-4],[1,-4,5],[2,1,1]] 인가"))
print("정수 행렬과 같은가 :",
      np.allclose(A플러스 * 9, [[1, 5, -4], [1, -4, 5], [2, 1, 1]]))
print()
print(f"{'':>6}{'A v = sigma u':>34}{'A^+ u = v / sigma':>34}")
for i, (v, u, s) in enumerate(zip([v1, v2, v3], [u1, u2, u3], 시그마), 1):
    앞 = np.allclose(A @ v, s * u)
    뒤 = np.allclose(A플러스 @ u, v / s) if s > 0 else np.allclose(A플러스 @ u, 0)
    print(f"  i={i} {str(np.round(A @ v, 4)):>28}{str(np.round(A플러스 @ u, 4)):>34}"
          f"   {앞 and 뒤}")
print()
print("i=3 은 양쪽 다 0 이다. 그것이 편도라는 뜻이다.")
9 A^+   <- 서술의 [[1,5,-4],[1,-4,5],[2,1,1]] 인가
[   1    5   -4 ]
[   1   -4    5 ]
[   2    1    1 ]
정수 행렬과 같은가 : True

                           A v = sigma u                 A^+ u = v / sigma
  i=1       [2.4495 1.2247 1.2247]            [0.1361 0.1361 0.2722]   True
  i=2    [ 0.      0.7071 -0.7071]         [ 0.7071 -0.7071 -0.    ]   True
  i=3                   [0. 0. 0.]                     [-0.  0.  0.]   True

i=3 은 양쪽 다 0 이다. 그것이 편도라는 뜻이다.
왼투영, 오른투영 = A플러스 @ A, A @ A플러스
print(show_matrix(왼투영, "A^+ A   행공간으로의 투영"))
print(show_matrix(오른투영, "A A^+   열공간으로의 투영"))
print("I - v3 v3^T 와 같은가 :", np.allclose(왼투영, np.eye(3) - np.outer(v3, v3)))
print("I - u3 u3^T 와 같은가 :", np.allclose(오른투영, np.eye(3) - np.outer(u3, u3)))
print()
for 이름, P in (("A^+A", 왼투영), ("AA^+", 오른투영)):
    print(f"  {이름} : P^2=P {np.allclose(P@P, P)}, P^T=P {np.allclose(P.T, P)}, "
          f"고윳값 {np.round(np.linalg.eigvalsh(P), 10)}")
print()
print("-> 살렸다, 살렸다, 죽였다.")
A^+ A   행공간으로의 투영
[   0.667   -0.333    0.333 ]
[  -0.333    0.667    0.333 ]
[   0.333    0.333    0.667 ]
A A^+   열공간으로의 투영
[   0.667    0.333    0.333 ]
[   0.333    0.667   -0.333 ]
[   0.333   -0.333    0.667 ]
I - v3 v3^T 와 같은가 : True
I - u3 u3^T 와 같은가 : True

  A^+A : P^2=P True, P^T=P True, 고윳값 [0. 1. 1.]
  AA^+ : P^2=P True, P^T=P True, 고윳값 [0. 1. 1.]

-> 살렸다, 살렸다, 죽였다.

회전 가능한 그림 두 장

정의역 쪽 R3\R^3 과 치역 쪽 R3\R^3 을 따로 그린다. 끌어서 돌려 보면 평면과 직선이 정말 수직인지 보인다.

traces = [plane(spans=[v1, v2], extent=2.4, color=COLORS["colspace"],
                opacity=0.30, name="행공간 (v1, v2 의 평면)"),
          line(v3, extent=2.8, color=COLORS["nullspace"], name="영공간 (v3 방향)")]
traces += arrow(np.zeros(3), 1.4 * v1, color=COLORS["input"], name="v1")
traces += arrow(np.zeros(3), 1.4 * v2, color=COLORS["second"], name="v2")
traces += arrow(np.zeros(3), 1.4 * v3, color=COLORS["nullspace"], name="v3")
go.Figure(traces, layout=layout3d("정의역 R^3 — 행공간(평면) + 영공간(직선)",
                                  extent=2.6, axis_names=("x1", "x2", "x3")))
Loading...
traces = [plane(spans=[u1, u2], extent=2.4, color=COLORS["output"],
                opacity=0.25, name="열공간 (u1, u2 의 평면)"),
          line(u3, extent=2.8, color=COLORS["nullspace"], name="좌영공간 (u3 방향)")]
traces += arrow(np.zeros(3), 3.0 * u1, color=COLORS["output"], name="A v1 = 3 u1")
traces += arrow(np.zeros(3), 1.0 * u2, color=COLORS["second"], name="A v2 = 1 u2")
traces += arrow(np.zeros(3), 1.4 * u3, color=COLORS["nullspace"], name="u3 (아무것도 안 온다)")
go.Figure(traces, layout=layout3d("치역 R^3 — 열공간(평면) + 좌영공간(직선)",
                                  extent=2.6, axis_names=("y1", "y2", "y3")))
Loading...

공간이 눌리는 것을 슬라이더로

구면 위의 점을 x(1t)x+tAx\vv{x} \mapsto (1-t)\vv{x} + tA\vv{x} 로 옮긴다. t=1t = 1 에서 전부 열공간의 평면 위에 앉는다.

표본 = rng.normal(size=(600, 3))
표본 /= np.linalg.norm(표본, axis=1, keepdims=True)
프레임, 이름표 = [], []
for t in np.linspace(0.0, 1.0, 11):
    Y = (1 - t) * 표본 + t * (표본 @ A.T)
    프레임.append([
        plane(spans=[u1, u2], extent=2.6, color=COLORS["output"], opacity=0.22,
              grid=6, name="열공간"),          # 평평한 면이라 격자는 성기게
        go.Scatter3d(x=Y[:, 0], y=Y[:, 1], z=Y[:, 2], mode="markers",
                     marker=dict(size=2.4, color=COLORS["input"]),
                     name=f"점 600개 (t = {t:.1f})"),
    ])
    이름표.append(f"{t:.1f}")
배치 = layout3d("구면이 열공간의 평면으로 눌린다", extent=3.2,
                axis_names=("y1", "y2", "y3"))
배치["height"] = 600
slider_figure(프레임, 이름표, 배치, prefix="t = ", initial=0)
Loading...
Y = 표본 @ A.T
거리 = np.abs(Y @ u3)                       # 열공간까지의 거리
print("t = 1 에서 모든 점이 열공간 위에 있는가")
print("  u3 방향 성분의 최댓값 :", f"{거리.max():.3e}")
print("  (0 이면 평면 위에 정확히 앉았다는 뜻이다)")
print()
print("부피는 얼마나 남았는가 : |det A| =", f"{abs(np.linalg.det(A)):.3e}")
print("  0 이다. 3차원 덩어리가 2차원 평면이 되었으니 부피가 남을 리 없다.")
t = 1 에서 모든 점이 열공간 위에 있는가
  u3 방향 성분의 최댓값 : 2.505e-16
  (0 이면 평면 위에 정확히 앉았다는 뜻이다)

부피는 얼마나 남았는가 : |det A| = 0.000e+00
  0 이다. 3차원 덩어리가 2차원 평면이 되었으니 부피가 남을 리 없다.

4. 세 개의 태도

옳은 공식과 쓰는 공식은 다르다

import math
print(f"{'n':>4}{'크래머 (n+1)n!':>18}{'여인수 n!':>16}{'소거 n^3/3':>14}{'배수':>12}")
for n in (5, 10, 15, 20, 25):
    크 = float(n + 1) * math.factorial(n)
    여 = float(math.factorial(n))
    소 = n ** 3 / 3
    print(f"{n:>4}{크:>18.3e}{여:>16.3e}{소:>14.1f}{크/소:>12.2e}")
print()
print("-> n = 20 에서 5.1e19 대 2667 이다. 아름다움에는 값이 붙는다.")
   n       크래머 (n+1)n!          여인수 n!      소거 n^3/3          배수
   5         7.200e+02       1.200e+02          41.7    1.73e+01
  10         3.992e+07       3.629e+06         333.3    1.20e+05
  15         2.092e+13       1.308e+12        1125.0    1.86e+10
  20         5.109e+19       2.433e+18        2666.7    1.92e+16
  25         4.033e+26       1.551e+25        5208.3    7.74e+22

-> n = 20 에서 5.1e19 대 2667 이다. 아름다움에는 값이 붙는다.
print("조르당 형 : eps 를 흔들면 고유벡터 행렬이 얼마나 나빠지는가")
print(f"{'eps':>10}{'고윳값':>28}{'cond(S)':>14}{'1/sqrt(eps)':>14}")
for e in (0.0, 1e-12, 1e-10, 1e-8, 1e-6, 1e-4):
    J = np.array([[3.0, 1.0], [e, 3.0]])
    w, V = np.linalg.eig(J)
    이론 = np.inf if e == 0 else 1 / np.sqrt(e)
    갈림 = f"3 +- {abs(w[0] - w[1]) / 2:.3e}" if abs(w[0] - w[1]) > 0 else "3, 3 (구별 못 함)"
    print(f"{e:>10.0e}{갈림:>28}{np.linalg.cond(V):>14.3e}{이론:>14.3e}")
print()
print("-> cond(S) 가 1/sqrt(eps) 를 따라간다. L28 의 그 법칙이다.")
조르당 형 : eps 를 흔들면 고유벡터 행렬이 얼마나 나빠지는가
       eps                         고윳값       cond(S)   1/sqrt(eps)
     0e+00               3, 3 (구별 못 함)     3.002e+15           inf
     1e-12              3 +- 1.000e-06     1.000e+06     1.000e+06
     1e-10              3 +- 1.000e-05     1.000e+05     1.000e+05
     1e-08              3 +- 1.000e-04     1.000e+04     1.000e+04
     1e-06              3 +- 1.000e-03     1.000e+03     1.000e+03
     1e-04              3 +- 1.000e-02     1.000e+02     1.000e+02

-> cond(S) 가 1/sqrt(eps) 를 따라간다. L28 의 그 법칙이다.
print("정규방정식 : 조건수가 정말 제곱되는가")
print(f"{'cond(A)':>12}{'cond(A^T A)':>16}{'cond(A)^2':>16}{'비':>10}")
for k in (1e2, 1e4, 1e6, 1e8):
    m, n = 40, 6
    Uq, _ = np.linalg.qr(rng.normal(size=(m, m)))
    Vq, _ = np.linalg.qr(rng.normal(size=(n, n)))
    X = Uq[:, :n] @ np.diag(np.logspace(0, -np.log10(k), n)) @ Vq.T
    k1, k2 = np.linalg.cond(X), np.linalg.cond(X.T @ X)
    print(f"{k1:>12.4e}{k2:>16.4e}{k1**2:>16.4e}{k2/k1**2:>10.4f}")
print()
print("-> 마지막 칸이 1 근처다. inv(A.T @ A) @ A.T 를 손으로 쓰지 마라.")
정규방정식 : 조건수가 정말 제곱되는가
     cond(A)     cond(A^T A)       cond(A)^2         비
  1.0000e+02      1.0000e+04      1.0000e+04    1.0000
  1.0000e+04      1.0000e+08      1.0000e+08    1.0000
  1.0000e+06      1.0000e+12      1.0000e+12    1.0000
  1.0000e+08      1.0621e+16      1.0000e+16    1.0621

-> 마지막 칸이 1 근처다. inv(A.T @ A) @ A.T 를 손으로 쓰지 마라.

좋은 기저를 고르면 어려운 문제가 쉬워진다

print("같은 일을 어느 기저에서 하느냐")
print(f"{'N':>10}{'직접 DFT N^2':>16}{'FFT N log2 N':>16}{'배수':>12}")
for N in (2**10, 2**12, 2**16, 2**20):
    직 = float(N) ** 2
    빠 = N * np.log2(N)
    print(f"{N:>10}{직:>16.3e}{빠:>16.3e}{직/빠:>12.1f}")
print()
print("대각화 : A^100 을 어떻게 구하는가")
B = np.array([[0.9, 0.2], [0.1, 0.8]])          # 마코브 행렬 (L24)
w, S = np.linalg.eig(B)
직접 = np.linalg.matrix_power(B, 100)
기저 = (S @ np.diag(w ** 100) @ np.linalg.inv(S)).real
print("  행렬곱 99번 :", np.round(직접, 8).tolist())
print("  스칼라 100제곱 2번 :", np.round(기저, 8).tolist())
print("  같은가 :", np.allclose(직접, 기저))
print("  고윳값 :", w, " -> |lambda|<1 인 쪽이 사라지고 1 인 쪽만 남는다")
같은 일을 어느 기저에서 하느냐
         N      직접 DFT N^2    FFT N log2 N          배수
      1024       1.049e+06       1.024e+04       102.4
      4096       1.678e+07       4.915e+04       341.3
     65536       4.295e+09       1.049e+06      4096.0
   1048576       1.100e+12       2.097e+07     52428.8

대각화 : A^100 을 어떻게 구하는가
  행렬곱 99번 : [[0.66666667, 0.66666667], [0.33333333, 0.33333333]]
  스칼라 100제곱 2번 : [[0.66666667, 0.66666667], [0.33333333, 0.33333333]]
  같은가 : True
  고윳값 : [1. +0.j 0.7+0.j]  -> |lambda|<1 인 쪽이 사라지고 1 인 쪽만 남는다

조건이 없는 도구가 가장 강하다

개수, 복소인것 = 400, 0
시그마모음 = []
rng34 = np.random.default_rng(34)      # 그림 4와 같은 표본을 쓴다
for _ in range(개수):
    B = rng34.normal(size=(4, 4))
    w = np.linalg.eigvals(B)
    시그마모음.extend(np.linalg.svd(B, compute_uv=False))
    복소인것 += int(np.max(np.abs(w.imag)) > 1e-9)
print(f"무작위 4x4 실행렬 {개수}개 중 복소 고윳값을 가진 것 : "
      f"{복소인것} ({복소인것/개수*100:.0f}%)")
print("특이값 중 음수이거나 복소인 것 :",
      int(np.sum(np.array(시그마모음) < 0)), "개")
print()
X = rng34.normal(size=(4, 3))
print("4x3 행렬의 고윳값을 구하려 하면")
try:
    np.linalg.eigvals(X)
except np.linalg.LinAlgError as 오류:
    print("  LinAlgError :", 오류)
print("  특이값은 :", np.linalg.svd(X, compute_uv=False))
무작위 4x4 실행렬 400개 중 복소 고윳값을 가진 것 : 351 (88%)
특이값 중 음수이거나 복소인 것 : 0 개

4x3 행렬의 고윳값을 구하려 하면
  LinAlgError : Last 2 dimensions of the array must be square
  특이값은 : [1.9209 1.0529 0.6578]
print("랭크가 떨어지는 순간 무엇이 무너지는가")
print(f"{'t':>10}{'세 번째 피벗':>16}{'|R33|':>14}{'sigma_3':>14}{'sigma_3/t':>12}")
for t in (1e-1, 1e-3, 1e-5, 1e-7, 1e-9, 0.0):
    M = A.copy()
    M[2, 2] += t
    _, _, Um = lu(M)
    _, Rm = np.linalg.qr(M)
    s3 = np.linalg.svd(M, compute_uv=False)[-1]
    비 = s3 / t if t else float("nan")
    print(f"{t:>10.0e}{abs(Um[2,2]):>16.3e}{abs(Rm[2,2]):>14.3e}{s3:>14.3e}"
          f"{비:>12.4f}")
print()
print("-> 셋 다 0 으로 간다. 다만 LU 와 QR 은 '나눌 수 없다'로 끝나고,")
print("   sigma_3 은 '얼마나 가까운가'를 숫자로 남긴다.")
랭크가 떨어지는 순간 무엇이 무너지는가
         t         세 번째 피벗         |R33|       sigma_3   sigma_3/t
     1e-01       1.000e-01     5.774e-02     3.294e-02      0.3294
     1e-03       1.000e-03     5.774e-04     3.333e-04      0.3333
     1e-05       1.000e-05     5.774e-06     3.333e-06      0.3333
     1e-07       1.000e-07     5.774e-08     3.333e-08      0.3333
     1e-09       1.000e-09     5.773e-10     3.333e-10      0.3333
     0e+00       0.000e+00     0.000e+00     1.622e-16         nan

-> 셋 다 0 으로 간다. 다만 LU 와 QR 은 '나눌 수 없다'로 끝나고,
   sigma_3 은 '얼마나 가까운가'를 숫자로 남긴다.

5. 행렬 동물원, 마지막으로 한 번

이 책에 나온 행렬을 전부 모아 다섯 분해에 통과시킨다.

동물원 = [
    ("L2  치환 [[0,1],[1,0]]", np.array([[0.0, 1.0], [1.0, 0.0]])),
    ("L11 랭크 1 [[1,2],[2,4]]", np.array([[1.0, 2.0], [2.0, 4.0]])),
    ("L16 최소제곱 3x2", np.array([[1.0, 1], [1, 2], [1, 3]])),
    ("L20 [[3,0],[4,5]]", np.array([[3.0, 0.0], [4.0, 5.0]])),
    ("L21 90도 회전", np.array([[0.0, -1.0], [1.0, 0.0]])),
    ("L24 마코브 [[.9,.2],[.1,.8]]", np.array([[0.9, 0.2], [0.1, 0.8]])),
    ("L27 S = [[5,4],[4,5]]", np.array([[5.0, 4.0], [4.0, 5.0]])),
    ("L27 T = tridiag", T),
    ("L28 결함 [[3,1],[0,3]]", np.array([[3.0, 1.0], [0.0, 3.0]])),
    ("L29 C = 3x2", np.array([[1.0, 1], [1, 0], [0, 1]])),
    ("L34 A = 3x3 랭크 2", A),
]
줄이름 = ["A = LU", "A = QR", "S = Q L Q^T", "A = S L S^-1", "A = U S V^T"]
머리 = ["LU", "QR", "QLQ^T", "SLS^-1", "USV^T"]
print(f"{'':>30}" + "".join(f"{h:>9}" for h in 머리) + f"{'통과':>7}")
합계 = np.zeros(5)
for 이름, M in 동물원:
    보 = 다섯분해(M, 출력=False)
    표 = np.array([보[k][0] for k in 줄이름], dtype=float)
    합계 += 표
    print(f"{이름:>30}" + "".join(f"{'O' if v else 'X':>9}" for v in 표)
          + f"{int(표.sum()):>7}")
print(f"{'합계':>30}" + "".join(f"{int(v):>9}" for v in 합계)
      + f"{'/ ' + str(len(동물원)):>7}")
print()
print("-> 마지막 열만 꽉 찼다. 이것이 34강의 결론이다.")
print("   (SLS^-1 열은 복소수까지 허용해 센 것이다. 90도 회전이 그런 경우다.)")
                                     LU       QR    QLQ^T   SLS^-1    USV^T     통과
          L2  치환 [[0,1],[1,0]]        X        O        O        O        O      4
        L11 랭크 1 [[1,2],[2,4]]        O        X        O        O        O      4
                  L16 최소제곱 3x2        X        O        X        X        O      2
             L20 [[3,0],[4,5]]        O        O        X        O        O      4
                    L21 90도 회전        X        O        X        O        O      3
     L24 마코브 [[.9,.2],[.1,.8]]        O        O        X        O        O      4
         L27 S = [[5,4],[4,5]]        O        O        O        O        O      5
               L27 T = tridiag        O        O        O        O        O      5
          L28 결함 [[3,1],[0,3]]        O        O        X        X        O      3
                   L29 C = 3x2        X        O        X        X        O      2
              L34 A = 3x3 랭크 2        O        X        X        O        O      3
                            합계        7        9        4        8       11   / 11

-> 마지막 열만 꽉 찼다. 이것이 34강의 결론이다.
   (SLS^-1 열은 복소수까지 허용해 센 것이다. 90도 회전이 그런 경우다.)
print("마지막으로, 이 책의 행렬 전부에 대해 네 부분공간의 차원을 세어 본다")
print(f"{'':>30}{'m':>4}{'n':>4}{'r':>4}{'행공간':>8}{'영공간':>8}"
      f"{'열공간':>8}{'좌영공간':>10}")
for 이름, M in 동물원:
    m, n = M.shape
    r = np.linalg.matrix_rank(M)
    print(f"{이름:>30}{m:>4}{n:>4}{r:>4}{r:>8}{n-r:>8}{r:>8}{m-r:>10}")
print()
print("네 칸의 차원이 전부 (m, n, r) 하나로 정해진다. L10 의 차원의 기본정리다.")
마지막으로, 이 책의 행렬 전부에 대해 네 부분공간의 차원을 세어 본다
                                 m   n   r     행공간     영공간     열공간      좌영공간
          L2  치환 [[0,1],[1,0]]   2   2   2       2       0       2         0
        L11 랭크 1 [[1,2],[2,4]]   2   2   1       1       1       1         1
                  L16 최소제곱 3x2   3   2   2       2       0       2         1
             L20 [[3,0],[4,5]]   2   2   2       2       0       2         0
                    L21 90도 회전   2   2   2       2       0       2         0
     L24 마코브 [[.9,.2],[.1,.8]]   2   2   2       2       0       2         0
         L27 S = [[5,4],[4,5]]   2   2   2       2       0       2         0
               L27 T = tridiag   3   3   3       3       0       3         0
          L28 결함 [[3,1],[0,3]]   2   2   2       2       0       2         0
                   L29 C = 3x2   3   2   2       2       0       2         1
              L34 A = 3x3 랭크 2   3   3   2       2       1       2         1

네 칸의 차원이 전부 (m, n, r) 하나로 정해진다. L10 의 차원의 기본정리다.

마치며...

서술 파트의 내용이 노트북의 코드
다섯 개의 분해다섯분해(A) 가 요구와 결과를 한 줄씩 적는다
TT 의 피벗 2,1.5,4/32, 1.5, 4/3곱이 4, 그리고 detT=4\det T = 4
λ=2±2, 2\lambda = 2 \pm \sqrt2,\ 2전부 실수, 전부 양수
아래 세 줄이 무너진다부호를 맞추면 Q=S=U=VQ = S = U = V
AA 는 대각화되는데 QRQR 이 없다고윳값 0, 1±20,\ 1\pm\sqrt2 가 서로 다르다
조건은 사슬이 아니다반례 셋이 전부 재현된다
인구조사80/80/20/60/10080 / 80 / 20 / 60 / 100
조르당은 수치적으로 없다numpy 가 결함 행렬의 95%를 놓친다
σ=3, 1, 0\sigma = 3,\ 1,\ 0ATAA^{\mathsf T}A 의 고윳값 9,1,09, 1, 0
u=Av/σ\vv{u} = A\vv{v}/\sigma따로 뽑으면 두 열의 부호가 뒤집혀 복원이 깨진다
9A+9A^{+} 는 정수 행렬[[1,5,4],[1,4,5],[2,1,1]][[1,5,-4],[1,-4,5],[2,1,1]]
A+A=Iv3v3TA^{+}A = I - \vv{v}_3\vv{v}_3^{\mathsf T}고윳값 1,1,01, 1, 0
위쪽은 왕복, 아래쪽은 편도i=3i = 3 에서 양쪽 다 0\vv{0}
구면이 평면으로 눌린다u3\vv{u}_3 방향 성분이 10-16
옳은 공식과 쓰는 공식n=20n=20 에서 5.1×10195.1\times10^{19}2667
좋은 기저N=220N = 2^{20} 에서 52429
조건이 없는 도구복소 고윳값 88%, 특이값은 언제나 실수
마지막 열만 꽉 찬다동물원 11개 전부 통과

더 해 볼 것

  1. 다섯분해여섯째 줄을 붙여 보자. 촐레스키 S=LLTS = LL^{\mathsf T} (L27) 를 추가하면 요구하는 것은 무엇이고, 동물원에서 몇 개가 통과하는가?

  2. 인구조사의 다섯 족에 직교행렬을 하나 더해 보자. 다섯 줄 중 몇 개를 통과하는가? 특이값을 계산하지 않고 미리 말할 수 있는가?

  3. 3절의 앵커에서 AA 의 셋째 열을 a1+a2+εe3\vv{a}_1 + \vv{a}_2 + \varepsilon\vv{e}_3 으로 바꿔 보자. ε\varepsilon 이 줄어들 때 v3\vv{v}_3u3\vv{u}_3 은 어디로 가는가? 네 부분공간의 차원은 언제 바뀌는가?

  4. 구면이 눌리는 그림에서 AA 대신 A+A^{+} 를 넣어 보자. 어느 평면으로 눌리는가? 그다음 A+AA^{+}A 를 넣으면 무엇이 보이는가?

  5. 동물원에 자기 행렬을 하나 넣어 보자. 손에 있는 데이터 행렬이면 더 좋다. 다섯 줄 중 몇 개가 통과하는가? 조건수는 얼마인가?

여기까지다. 서른네 편에 걸쳐 만든 도구가 전부 이 노트북 한 장 안에서 돌아간다.

긴 여정을 함께해 주어 고맙다.