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 32. 중간 정리 — 행렬을 진단하는 법 — 파이썬 실습

A Diagnostic Procedure for Matrices — 실습

L13에서 행렬해부 를 만들었다. 소거 한 번으로 랭크, 피벗, 네 부분공간, 해의 존재와 유일성을 한꺼번에 뽑아 주는 함수였다.

이번에는 그것을 후반부 내용까지 담은 행렬진단 으로 확장한다. 고윳값, 대각화 가능성, 대칭성과 정부호성, 특이값, 조건수, 정규성까지 한 번에 찍어 주는 도구다. 그리고 그 도구로 행렬 동물원을 통째로 통과시켜 서술 파트의 흐름도가 실제로 작동하는지 확인한다.

서술 파트의 내용여기서 확인하는 방법
진단 흐름도행렬진단 이 동물원 열두 마리를 분류
실수 vs 복소 대각화회전행렬은 복소에서만
조건수의 뜻오차 증폭을 실제로 재기
조건수는 하나가 아니다κ(A)\kappa(A) 는 평평, κ(S)\kappa(S) 는 터짐
고윳값 vs 특이값정규행렬일 때만 일치
유사 vs 합동무작위 500개로
직교 vs 정규직교QTQQ^{\mathsf T}Q 가 무엇이 되는가
대칭 vs 에르미트A.T 함정
영공간 vs 좌영공간사는 공간이 다르다
함정 열넷코드로 하나씩 재확인
판단 문제 열 개코드로 채점

0. 준비

import numpy as np
import plotly.graph_objects as go
from scipy.linalg import null_space, expm

from linalg_viz import COLORS, show_matrix

np.set_printoptions(precision=4, suppress=True)
rng = np.random.default_rng(32)
print("numpy", np.__version__)
numpy 2.5.2

1. 행렬진단 조립

서술 파트의 흐름도를 그대로 코드로 옮긴다. 묻는 순서가 곧 함수의 구조다.

def 행렬진단(A, 눈감아=1e-9):
    """행렬 하나를 L1~L31 의 도구로 전부 재서 사전으로 돌려준다.

    L13 의 행렬해부() 가 소거로 알 수 있는 것을 담았다면,
    이쪽은 고윳값·특이값까지 포함한 확장판이다.
    """
    A = np.asarray(A, dtype=float)
    m, n = A.shape
    U, s, Vt = np.linalg.svd(A, full_matrices=False)
    r = int((s > 눈감아 * max(m, n) * (s[0] if s.size else 1.0)).sum())

    보고 = {
        "크기": (m, n), "랭크": r, "특이값": s,
        "차원": {"열공간": r, "좌영공간": m - r, "행공간": r, "영공간": n - r},
        "조건수": float(s[0] / s[-1]) if s.size and s[-1] > 1e-14 else np.inf,
        "정방": m == n,
    }
    if m != n:
        보고["길"] = "직사각 -> SVD"
        return 보고

    보고["대칭"] = bool(np.allclose(A, A.T))
    보고["정규"] = bool(np.allclose(A @ A.T, A.T @ A))
    보고["행렬식"] = float(np.linalg.det(A))
    보고["대각합"] = float(np.trace(A))

    if 보고["대칭"]:
        고 = np.linalg.eigvalsh(A)
        보고["고윳값"] = 고
        보고["고윳값실수"] = True
        보고["대각화"] = "직교기저로 가능 (스펙트럼 정리)"
        보고["정부호"] = ("양의 정부호" if (고 > 눈감아).all() else
                       "양의 준정부호" if (고 >= -눈감아).all() else
                       "음의 정부호" if (고 < -눈감아).all() else
                       "음의 준정부호" if (고 <= 눈감아).all() else "부정부호")
        보고["관성"] = (int((고 > 눈감아).sum()), int((np.abs(고) <= 눈감아).sum()),
                     int((고 < -눈감아).sum()))
        보고["길"] = f"대칭 -> {보고['정부호']}"
        보고["고유벡터조건수"] = 1.0            # 직교라서 언제나 1
        return 보고

    고, V = np.linalg.eig(A)
    보고["고윳값"] = 고
    보고["고윳값실수"] = bool(np.allclose(고.imag, 0))
    가능 = np.linalg.matrix_rank(V, tol=1e-8) == n
    보고["고유벡터조건수"] = float(np.linalg.cond(V)) if 가능 else np.inf
    if not 가능:
        보고["대각화"] = "불가 (결함 행렬)"
        보고["길"] = "결함 -> 조르당은 포기, SVD 로"
    elif 보고["고윳값실수"]:
        보고["대각화"] = "실수에서 가능"
        보고["길"] = "비대칭 대각화 가능"
    else:
        보고["대각화"] = "복소수에서만 가능"
        보고["길"] = "비대칭, 복소 고윳값"
    return 보고
def 진단출력(A, 이름="A"):
    """행렬진단 의 결과를 사람이 읽기 좋게 찍는다."""
    보 = 행렬진단(A)
    m, n = 보["크기"]
    print(f"[{이름}]  {m} x {n},  랭크 {보['랭크']}")
    d = 보["차원"]
    print(f"  차원   행공간 {d['행공간']} | 영공간 {d['영공간']} | "
          f"열공간 {d['열공간']} | 좌영공간 {d['좌영공간']}")
    print(f"  특이값 {np.round(보['특이값'], 4)}")
    print(f"  조건수 {보['조건수']:.4g}")
    if not 보["정방"]:
        print(f"  -> {보['길']}")
        return 보
    print(f"  det {보['행렬식']:.4f}   trace {보['대각합']:.4f}")
    print(f"  대칭 {보['대칭']}   정규 {보['정규']}")
    print(f"  고윳값 {np.round(보['고윳값'], 4)}   (실수: {보['고윳값실수']})")
    print(f"  대각화 : {보['대각화']}")
    if "정부호" in 보:
        print(f"  정부호 : {보['정부호']}   관성(양,0,음) {보['관성']}")
    print(f"  고유벡터 행렬의 조건수 : {보['고유벡터조건수']:.4g}")
    print(f"  -> {보['길']}")
    return 보
진단출력(np.array([[5.0, 4.0], [4.0, 5.0]]), "대칭 양정치")
print()
진단출력(np.array([[3.0, 1.0], [0.0, 3.0]]), "결함 행렬")
print()
_ = 진단출력(np.array([[1.0, 1.0], [1.0, 0.0], [0.0, 1.0]]), "직사각 3x2")
[대칭 양정치]  2 x 2,  랭크 2
  차원   행공간 2 | 영공간 0 | 열공간 2 | 좌영공간 0
  특이값 [9. 1.]
  조건수 9
  det 9.0000   trace 10.0000
  대칭 True   정규 True
  고윳값 [1. 9.]   (실수: True)
  대각화 : 직교기저로 가능 (스펙트럼 정리)
  정부호 : 양의 정부호   관성(양,0,음) (2, 0, 0)
  고유벡터 행렬의 조건수 : 1
  -> 대칭 -> 양의 정부호

[결함 행렬]  2 x 2,  랭크 2
  차원   행공간 2 | 영공간 0 | 열공간 2 | 좌영공간 0
  특이값 [3.5414 2.5414]
  조건수 1.393
  det 9.0000   trace 6.0000
  대칭 False   정규 False
  고윳값 [3.+0.j 3.+0.j]   (실수: True)
  대각화 : 불가 (결함 행렬)
  고유벡터 행렬의 조건수 : inf
  -> 결함 -> 조르당은 포기, SVD 로

[직사각 3x2]  3 x 2,  랭크 2
  차원   행공간 2 | 영공간 0 | 열공간 2 | 좌영공간 1
  특이값 [1.7321 1.    ]
  조건수 1.732
  -> 직사각 -> SVD

2. 행렬 동물원을 통과시킨다

흐름도가 정말 갈래를 제대로 나누는지, 성질이 다른 열두 마리를 한꺼번에 넣어 본다.

동물원 = (
    ("영행렬 3x3", np.zeros((3, 3))),
    ("단위행렬 I3", np.eye(3)),
    ("대칭 양정치 [[5,4],[4,5]]", np.array([[5.0, 4.0], [4.0, 5.0]])),
    ("대칭 부정부호 [[1,2],[2,1]]", np.array([[1.0, 2.0], [2.0, 1.0]])),
    ("대칭 준정부호 [[1,1],[1,1]]", np.array([[1.0, 1.0], [1.0, 1.0]])),
    ("비대칭 대각화가능 [[4,1],[2,3]]", np.array([[4.0, 1.0], [2.0, 3.0]])),
    ("결함 [[3,1],[0,3]]", np.array([[3.0, 1.0], [0.0, 3.0]])),
    ("회전 90도", np.array([[0.0, -1.0], [1.0, 0.0]])),
    ("마코브 [[.9,.2],[.1,.8]]", np.array([[0.9, 0.2], [0.1, 0.8]])),
    ("투영 (y=x 로)", np.array([[0.5, 0.5], [0.5, 0.5]])),
    ("힐베르트 5x5", np.array([[1/(i+j+1) for j in range(5)] for i in range(5)])),
    ("직사각 3x2", np.array([[1.0, 1.0], [1.0, 0.0], [0.0, 1.0]])),
)
print(f"{'':>32}{'랭크':>5}{'조건수':>12}{'고유벡터 조건수':>16}{'진단':>26}")
for 이름, A in 동물원:
    보 = 행렬진단(A)
    cv = 보.get("고유벡터조건수", float("nan"))
    cv글 = "-" if 보["크기"][0] != 보["크기"][1] else f"{cv:.4g}"
    print(f"{이름:>32}{보['랭크']:>5}{보['조건수']:>12.4g}{cv글:>16}{보['길']:>26}")
                                   랭크         조건수        고유벡터 조건수                        진단
                         영행렬 3x3    0         inf               1             대칭 -> 양의 준정부호
                         단위행렬 I3    3           1               1              대칭 -> 양의 정부호
            대칭 양정치 [[5,4],[4,5]]    2           9               1              대칭 -> 양의 정부호
           대칭 부정부호 [[1,2],[2,1]]    2           3               1                대칭 -> 부정부호
           대칭 준정부호 [[1,1],[1,1]]    1         inf               1             대칭 -> 양의 준정부호
         비대칭 대각화가능 [[4,1],[2,3]]    2       2.618           1.387                비대칭 대각화 가능
                결함 [[3,1],[0,3]]    2       1.393             inf      결함 -> 조르당은 포기, SVD 로
                          회전 90도    2           1               1               비대칭, 복소 고윳값
           마코브 [[.9,.2],[.1,.8]]    2       1.456           1.387                비대칭 대각화 가능
                      투영 (y=x 로)    1         inf               1             대칭 -> 양의 준정부호
                        힐베르트 5x5    5   4.766e+05               1              대칭 -> 양의 정부호
                         직사각 3x2    2       1.732               -                직사각 -> SVD

조건수가 무한대인 것들

랭크가 모자라면 σn=0\sigma_n = 0 이라 조건수가 무한대다. 이것은 "위험하다"가 아니라 역행렬이 아예 없다는 뜻이다.

print(f"{'':>32}{'랭크':>5}{'열 개수':>8}{'sigma_min':>12}{'뜻':>22}")
for 이름, A in 동물원:
    보 = 행렬진단(A)
    m, n = 보["크기"]
    s최소 = 보["특이값"][-1] if 보["특이값"].size else 0.0
    뜻 = "가역" if (m == n and 보["랭크"] == n) else "역행렬 없음"
    print(f"{이름:>32}{보['랭크']:>5}{n:>8}{s최소:>12.4g}{뜻:>22}")
                                   랭크    열 개수   sigma_min                     뜻
                         영행렬 3x3    0       3           0                역행렬 없음
                         단위행렬 I3    3       3           1                    가역
            대칭 양정치 [[5,4],[4,5]]    2       2           1                    가역
           대칭 부정부호 [[1,2],[2,1]]    2       2           1                    가역
           대칭 준정부호 [[1,1],[1,1]]    1       2           0                역행렬 없음
         비대칭 대각화가능 [[4,1],[2,3]]    2       2       1.954                    가역
                결함 [[3,1],[0,3]]    2       2       2.541                    가역
                          회전 90도    2       2           1                    가역
           마코브 [[.9,.2],[.1,.8]]    2       2      0.6934                    가역
                      투영 (y=x 로)    1       2           0                역행렬 없음
                        힐베르트 5x5    5       5   3.288e-06                    가역
                         직사각 3x2    2       2           1                역행렬 없음

3. 조건수가 정말 오차를 그만큼 키우는가

서술 파트의 부등식을 직접 재 본다.

def 오차증폭(A, 횟수=4000, 세기=1e-10):
    """b 를 아주 조금 흔들었을 때 x 가 얼마나 흔들리는지 잰다."""
    n = A.shape[0]
    최대 = 0.0
    for _ in range(횟수):
        x = rng.normal(size=n)
        b = A @ x
        db = rng.normal(size=n)
        db = db / np.linalg.norm(db) * 세기 * np.linalg.norm(b)
        dx = np.linalg.solve(A, db)
        최대 = max(최대, (np.linalg.norm(dx)/np.linalg.norm(x))
                       / (np.linalg.norm(db)/np.linalg.norm(b)))
    return 최대

print(f"{'':>26}{'조건수':>12}{'실제로 잰 최대 증폭':>22}{'한계 안인가':>12}")
for 이름, A in (("[[5,4],[4,5]]", np.array([[5.0, 4.0], [4.0, 5.0]])),
                ("diag(1e4, 1e-4)", np.diag([1e4, 1e-4])),
                ("힐베르트 5x5",
                 np.array([[1/(i+j+1) for j in range(5)] for i in range(5)])),
                ("[[1,1],[1,1.0001]]", np.array([[1.0, 1.0], [1.0, 1.0001]]))):
    k = np.linalg.cond(A)
    잰것 = 오차증폭(A)
    print(f"{이름:>26}{k:>12.4g}{잰것:>22.4g}{str(잰것 <= k * 1.001):>12}")
print()
print("-> 조건수는 '최대 이만큼' 이라는 한계다. 실제로는 그보다 작게 나올 수 있고,")
print("   방향을 잘 맞추면 한계에 붙는다.")
                                   조건수           실제로 잰 최대 증폭      한계 안인가
             [[5,4],[4,5]]           9                  8.99        True
           diag(1e4, 1e-4)       1e+08                 1e+08        True
                  힐베르트 5x5   4.766e+05             4.365e+05        True
        [[1,1],[1,1.0001]]       4e+04                 4e+04        True

-> 조건수는 '최대 이만큼' 이라는 한계다. 실제로는 그보다 작게 나올 수 있고,
   방향을 잘 맞추면 한계에 붙는다.

조건수는 하나가 아니다

print("A = [[3, 1], [eps, 3]]  (L28 의 그 족)")
print(f"{'eps':>10}{'cond(A)':>10}{'cond(S)':>12}{'1/sqrt(eps)':>14}{'고윳값 간격':>14}")
for e in (1e-12, 1e-10, 1e-8, 1e-6, 1e-4, 1e-2, 1.0):
    A = np.array([[3.0, 1.0], [e, 3.0]])
    w, V = np.linalg.eig(A)
    print(f"{e:>10.0e}{np.linalg.cond(A):>10.4f}{np.linalg.cond(V):>12.4g}"
          f"{1/np.sqrt(e):>14.4g}{abs(w[0]-w[1]):>14.4g}")
print()
print("-> Ax=b 를 푸는 데는 아무 문제가 없다. 고유벡터를 구하는 것이 문제다.")
print("   '이 행렬은 상태가 좋은가' 는 무엇을 물을지 정해야 답할 수 있는 질문이다.")
A = [[3, 1], [eps, 3]]  (L28 의 그 족)
       eps   cond(A)     cond(S)   1/sqrt(eps)        고윳값 간격
     1e-12    1.3935       1e+06         1e+06         2e-06
     1e-10    1.3935       1e+05         1e+05         2e-05
     1e-08    1.3935       1e+04         1e+04        0.0002
     1e-06    1.3935        1000          1000         0.002
     1e-04    1.3935         100           100          0.02
     1e-02    1.3983          10            10           0.2
     1e+00    2.0000           1             1             2

-> Ax=b 를 푸는 데는 아무 문제가 없다. 고유벡터를 구하는 것이 문제다.
   '이 행렬은 상태가 좋은가' 는 무엇을 물을지 정해야 답할 수 있는 질문이다.

4. 헷갈리는 여섯 쌍을 반례로

① 고윳값 vs 특이값

print(f"{'':>26}{'정규':>7}{'|고윳값|':>20}{'특이값':>20}{'일치':>7}")
for 이름, A in (("대칭 [[5,4],[4,5]]", np.array([[5.0, 4.0], [4.0, 5.0]])),
                ("회전 90도", np.array([[0.0, -1.0], [1.0, 0.0]])),
                ("결함 [[3,1],[0,3]]", np.array([[3.0, 1.0], [0.0, 3.0]])),
                ("[[4,1],[2,3]]", np.array([[4.0, 1.0], [2.0, 3.0]]))):
    고 = np.sort(np.abs(np.linalg.eigvals(A)))[::-1]
    특 = np.linalg.svd(A, compute_uv=False)
    print(f"{이름:>26}{str(np.allclose(A@A.T, A.T@A)):>7}"
          f"{str(np.round(고,4)):>20}{str(np.round(특,4)):>20}"
          f"{str(np.allclose(고, 특)):>7}")
print()
print("무작위 500개로 : 정규행렬일 때만 일치하는가")
일치, 정규 = 0, 0
for _ in range(500):
    B = rng.normal(size=(3, 3))
    if rng.random() < 0.5:
        B = B + B.T                                   # 절반은 대칭으로
    같음 = np.allclose(np.sort(np.abs(np.linalg.eigvals(B)))[::-1],
                     np.linalg.svd(B, compute_uv=False))
    정규여부 = np.allclose(B @ B.T, B.T @ B)
    일치 += (같음 == 정규여부)
    정규 += 정규여부
print(f"  '일치한다' 와 '정규행렬이다' 가 같은 답을 준 횟수 : {일치}/500")
print(f"  (그중 정규행렬이었던 것 {정규}개)")
                               정규               |고윳값|                 특이값     일치
          대칭 [[5,4],[4,5]]   True             [9. 1.]             [9. 1.]   True
                    회전 90도   True             [1. 1.]             [1. 1.]   True
          결함 [[3,1],[0,3]]  False             [3. 3.]     [3.5414 2.5414]  False
             [[4,1],[2,3]]  False             [5. 2.]     [5.1167 1.9544]  False

무작위 500개로 : 정규행렬일 때만 일치하는가
  '일치한다' 와 '정규행렬이다' 가 같은 답을 준 횟수 : 500/500
  (그중 정규행렬이었던 것 248개)

② 대각화 vs SVD — 존재 조건

시도 = 200
대각화됨 = SVD됨 = 정방개수 = 0
for _ in range(시도):
    꼴 = rng.integers(0, 4)
    if 꼴 == 0:
        B = rng.normal(size=(3, 4))                   # 직사각
    elif 꼴 == 1:
        B = np.array([[3.0, 1.0], [0.0, 3.0]])        # 결함
    elif 꼴 == 2:
        B = rng.normal(size=(3, 3))
        B[:, 2] = B[:, 0]                             # 랭크 부족
    else:
        B = rng.normal(size=(3, 3))
    m, n = B.shape
    if m == n:
        정방개수 += 1
        V = np.linalg.eig(B)[1]
        대각화됨 += np.linalg.matrix_rank(V, tol=1e-8) == n
    try:
        np.linalg.svd(B)
        SVD됨 += 1
    except np.linalg.LinAlgError:
        pass
print(f"무작위 {시도}개 (직사각·결함·랭크부족을 섞어서)")
print(f"  그중 정방행렬 : {정방개수}개")
print(f"  대각화된 것   : {대각화됨} / {정방개수}   (직사각은 시도조차 못 한다)")
print(f"  SVD 된 것     : {SVD됨} / {시도}   <- 전부")
무작위 200개 (직사각·결함·랭크부족을 섞어서)
  그중 정방행렬 : 155개
  대각화된 것   : 100 / 155   (직사각은 시도조차 못 한다)
  SVD 된 것     : 200 / 200   <- 전부

③ 유사 vs 합동 — 가장 위험한 쌍

def 관성(X, 눈=1e-9):
    """대칭행렬의 (양수 개수, 0 개수, 음수 개수)."""
    l = np.linalg.eigvalsh(X)
    return (int((l > 눈).sum()), int((np.abs(l) <= 눈).sum()),
            int((l < -눈).sum()))
S = np.array([[3.0, 1.0, 0.0], [1.0, 3.0, 0.0], [0.0, 0.0, -2.0]])
print("S 의 고윳값 :", np.sort(np.linalg.eigvalsh(S)), "  관성", 관성(S))
print()
M = np.array([[1.0, 2.0, 0.0], [0.0, 1.0, 1.0], [1.0, 0.0, 2.0]])
유사 = np.linalg.inv(M) @ S @ M
합동 = M.T @ S @ M
print("유사 M^-1 S M :")
print("  고윳값 :", np.sort(np.linalg.eigvals(유사).real), "  <- 그대로")
print("  대칭인가 :", np.allclose(유사, 유사.T), "  <- 깨졌다")
print("합동 M^T S M :")
print("  고윳값 :", np.sort(np.linalg.eigvalsh(합동)), "  <- 달라졌다")
print("  대칭인가 :", np.allclose(합동, 합동.T), "  관성", 관성(합동), " <- 그대로")
S 의 고윳값 : [-2.  2.  4.]   관성 (2, 0, 1)

유사 M^-1 S M :
  고윳값 : [-2.  2.  4.]   <- 그대로
  대칭인가 : False   <- 깨졌다
합동 M^T S M :
  고윳값 : [-8.3411  1.3987 21.9424]   <- 달라졌다
  대칭인가 : True   관성 (2, 0, 1)  <- 그대로
고윳값보존 = 관성보존 = 대칭보존 = 0
for _ in range(500):
    Mr = rng.normal(size=(3, 3))
    if abs(np.linalg.det(Mr)) < 1e-8:
        continue
    고윳값보존 += np.allclose(np.sort(np.linalg.eigvals(np.linalg.inv(Mr)@S@Mr).real),
                          np.sort(np.linalg.eigvalsh(S)))
    합 = Mr.T @ S @ Mr
    관성보존 += (관성(합) == 관성(S))
    대칭보존 += np.allclose(합, 합.T)
print(f"무작위 M 500개")
print(f"  유사가 고윳값을 보존한 횟수  : {고윳값보존}")
print(f"  합동이 관성을 보존한 횟수    : {관성보존}")
print(f"  합동이 대칭을 보존한 횟수    : {대칭보존}")
print()
Q, _ = np.linalg.qr(rng.normal(size=(3, 3)))
print("M 이 직교행렬이면 둘이 같아지는가 :",
      np.allclose(np.linalg.inv(Q) @ S @ Q, Q.T @ S @ Q))
print("-> 스펙트럼 정리가 특별한 이유. 유사이면서 동시에 합동이다.")
무작위 M 500개
  유사가 고윳값을 보존한 횟수  : 500
  합동이 관성을 보존한 횟수    : 500
  합동이 대칭을 보존한 횟수    : 500

M 이 직교행렬이면 둘이 같아지는가 : True
-> 스펙트럼 정리가 특별한 이유. 유사이면서 동시에 합동이다.

④ 직교 vs 정규직교

Q0, _ = np.linalg.qr(rng.normal(size=(4, 4)))
직교만 = Q0 * np.array([1.0, 2.5, 0.4, 3.0])       # 길이만 흐트러뜨린다
print("열끼리 직교인가 :",
      np.allclose(직교만.T @ 직교만, np.diag(np.diag(직교만.T @ 직교만))))
print(show_matrix(직교만.T @ 직교만, "Q^T Q  (직교이기만 할 때)"))
print(show_matrix(Q0.T @ Q0, "Q^T Q  (정규직교일 때)"))
print()
print("전치가 역행렬인가 :", np.allclose(직교만.T @ 직교만, np.eye(4)),
      "/", np.allclose(Q0.T @ Q0, np.eye(4)))
print("길이를 보존하는가 :")
v = rng.normal(size=4)
print(f"  |v| = {np.linalg.norm(v):.6f}")
print(f"  |Q0 v|    = {np.linalg.norm(Q0 @ v):.6f}   (정규직교)")
print(f"  |직교만 v| = {np.linalg.norm(직교만 @ v):.6f}   (직교이기만)")
열끼리 직교인가 : True
Q^T Q  (직교이기만 할 때)
[          1    8.44e-17   -3.56e-17    1.95e-17 ]
[   8.44e-17        6.25   -3.17e-17   -1.82e-16 ]
[  -3.56e-17   -3.17e-17        0.16   -1.15e-16 ]
[   1.95e-17   -1.82e-16   -1.15e-16           9 ]
Q^T Q  (정규직교일 때)
[          1    6.71e-17   -1.95e-17   -3.28e-17 ]
[   6.71e-17           1   -8.72e-17   -4.32e-17 ]
[  -1.95e-17   -8.72e-17           1   -5.34e-17 ]
[  -3.28e-17   -4.32e-17   -5.34e-17           1 ]

전치가 역행렬인가 : False / True
길이를 보존하는가 :
  |v| = 0.848872
  |Q0 v|    = 0.848872   (정규직교)
  |직교만 v| = 2.306007   (직교이기만)

⑤ 대칭 vs 에르미트 — A.T 함정

H = np.array([[2.0+0j, 1.0-3.0j], [1.0+3.0j, 5.0+0j]])
print("H =\n", H)
print("H 는 에르미트인가 (H = H^H) :", np.allclose(H, H.conj().T))
print("H 는 대칭인가 (H = H^T)     :", np.allclose(H, H.T), "  <- 아니다")
print()
print("고윳값 :", np.linalg.eigvals(H), "  <- 실수다")
print("대각 성분 :", np.diag(H), "  <- 실수여야 한다")
print()
print("함정: A.T 를 쓰면 조용히 틀린 답이 나온다")
print("  H.T @ H   의 대각 :", np.round(np.diag(H.T @ H), 4))
print("  H^H @ H   의 대각 :", np.round(np.diag(H.conj().T @ H), 4))
print("  앞엣것은 복소수가 섞여 있다. '길이의 제곱' 이 복소수일 수는 없다.")
H =
 [[2.+0.j 1.-3.j]
 [1.+3.j 5.+0.j]]
H 는 에르미트인가 (H = H^H) : True
H 는 대칭인가 (H = H^T)     : False   <- 아니다

고윳값 : [0.+0.j 7.-0.j]   <- 실수다
대각 성분 : [2.+0.j 5.+0.j]   <- 실수여야 한다

함정: A.T 를 쓰면 조용히 틀린 답이 나온다
  H.T @ H   의 대각 : [-4.+6.j 17.-6.j]
  H^H @ H   의 대각 : [14.+0.j 35.+0.j]
  앞엣것은 복소수가 섞여 있다. '길이의 제곱' 이 복소수일 수는 없다.

⑥ 영공간 vs 좌영공간

D = np.array([[1.0, 2.0, 3.0],
              [2.0, 4.0, 6.0],
              [1.0, 1.0, 1.0],
              [0.0, 1.0, 2.0]])
r = np.linalg.matrix_rank(D)
print(show_matrix(D, "D  (4x3)"), f"\n랭크 {r}")
영 = null_space(D)
좌영 = null_space(D.T)
print(f"\n영공간   : R^{D.shape[1]} 안에 사는 {영.shape[1]} 차원  (n - r = {D.shape[1]-r})")
print(f"좌영공간 : R^{D.shape[0]} 안에 사는 {좌영.shape[1]} 차원  (m - r = {D.shape[0]-r})")
print()
print("영공간 원소에 D 를 곱하면    :", np.round(D @ 영[:, 0], 12))
print("좌영공간 원소에 D^T 를 곱하면 :", np.round(D.T @ 좌영[:, 0], 12))
print()
print("-> 벡터의 길이부터 다르다. 섞어 쓸 수가 없다.")
D  (4x3)
[  1   2   3 ]
[  2   4   6 ]
[  1   1   1 ]
[  0   1   2 ] 
랭크 2

영공간   : R^3 안에 사는 1 차원  (n - r = 1)
좌영공간 : R^4 안에 사는 2 차원  (m - r = 2)

영공간 원소에 D 를 곱하면    : [-0. -0. -0.  0.]
좌영공간 원소에 D^T 를 곱하면 : [-0.  0.  0.]

-> 벡터의 길이부터 다르다. 섞어 쓸 수가 없다.

5. 후반부의 함정 열넷을 코드로

서술 파트의 표를 하나씩 실제로 확인한다.

A = np.array([[2.0, 1.0], [1.0, 3.0]])
B = np.array([[1.0, 0.0], [2.0, 1.0]])
print("L18  det(A+B) 와 det A + det B")
print(f"  det(A+B) = {np.linalg.det(A+B):.4f},  "
      f"det A + det B = {np.linalg.det(A)+np.linalg.det(B):.4f}")
print(f"  det(AB) = {np.linalg.det(A@B):.4f},  "
      f"det A * det B = {np.linalg.det(A)*np.linalg.det(B):.4f}   <- 곱은 된다")
print()
print("L20  크래머 공식의 비용")
import math
for n in (5, 10, 15, 20):
    print(f"  n={n:>3} : 소거 ~{n**3//3:>10,} 연산,  크래머 ~{math.factorial(n)*n:>22,} 연산")
L18  det(A+B) 와 det A + det B
  det(A+B) = 9.0000,  det A + det B = 6.0000
  det(AB) = 5.0000,  det A * det B = 5.0000   <- 곱은 된다

L20  크래머 공식의 비용
  n=  5 : 소거 ~        41 연산,  크래머 ~                   600 연산
  n= 10 : 소거 ~       333 연산,  크래머 ~            36,288,000 연산
  n= 15 : 소거 ~     1,125 연산,  크래머 ~    19,615,115,520,000 연산
  n= 20 : 소거 ~     2,666 연산,  크래머 ~48,658,040,163,532,800,000 연산
print("L21  소거는 고윳값을 보존하지 않는다")
A = np.array([[2.0, 1.0], [1.0, 2.0]])
U = np.array([[2.0, 1.0], [0.0, 1.5]])          # A 를 한 번 소거한 것
print(f"  A 의 고윳값 : {np.sort(np.linalg.eigvalsh(A))}")
print(f"  U 의 고윳값 : {np.sort(np.linalg.eigvals(U).real)}   <- 달라졌다")
print(f"  det 는 : {np.linalg.det(A):.4f} vs {np.linalg.det(U):.4f}   <- 보존된다")
print()
print("L22  성분의 크기가 아니라 고윳값이 정한다")
큰성분 = np.array([[0.5, 20.0], [0.0, 0.5]])
print(f"  [[0.5, 20],[0, 0.5]] 의 고윳값 : {np.linalg.eigvals(큰성분)}")
for k in (1, 10, 50, 200):
    print(f"    k={k:>4} : |A^k| 최댓값 = {np.abs(np.linalg.matrix_power(큰성분,k)).max():.4g}")
L21  소거는 고윳값을 보존하지 않는다
  A 의 고윳값 : [1. 3.]
  U 의 고윳값 : [1.5 2. ]   <- 달라졌다
  det 는 : 3.0000 vs 3.0000   <- 보존된다

L22  성분의 크기가 아니라 고윳값이 정한다
  [[0.5, 20],[0, 0.5]] 의 고윳값 : [0.5+0.j 0.5+0.j]
    k=   1 : |A^k| 최댓값 = 20
    k=  10 : |A^k| 최댓값 = 0.3906
    k=  50 : |A^k| 최댓값 = 1.776e-12
    k= 200 : |A^k| 최댓값 = 4.978e-57
print("L23  e^(At) 는 성분마다 지수함수가 아니다")
A = np.array([[-1.0, 2.0], [1.0, -2.0]])
print(show_matrix(expm(A), "expm(A)  (진짜)"))
print(show_matrix(np.exp(A), "np.exp(A)  (성분마다 - 틀렸다)"))
print("  같은가 :", np.allclose(expm(A), np.exp(A)))
print()
print("L24  정상상태는 (1,1,...,1) 이 아니다")
Mk = np.array([[0.9, 0.2], [0.1, 0.8]])
w, V = np.linalg.eig(Mk)
정상 = V[:, np.argmin(np.abs(w - 1))].real
정상 = 정상 / 정상.sum()
print(f"  A 의 고유벡터 (lambda=1) : {정상}   <- 정상상태")
print(f"  A^T 의 고유벡터 (lambda=1) : {np.array([1.0,1.0])/2}   <- 이것과 헷갈리기 쉽다")
print(f"  A @ 정상 = {Mk @ 정상}   (그대로인가: {np.allclose(Mk@정상, 정상)})")
L23  e^(At) 는 성분마다 지수함수가 아니다
expm(A)  (진짜)
[  0.683   0.633 ]
[  0.317   0.367 ]
np.exp(A)  (성분마다 - 틀렸다)
[  0.368    7.39 ]
[   2.72   0.135 ]
  같은가 : False

L24  정상상태는 (1,1,...,1) 이 아니다
  A 의 고유벡터 (lambda=1) : [0.6667 0.3333]   <- 정상상태
  A^T 의 고유벡터 (lambda=1) : [0.5 0.5]   <- 이것과 헷갈리기 쉽다
  A @ 정상 = [0.6667 0.3333]   (그대로인가: True)
print("L25  피벗을 구할 때 행 교환을 하면 안 된다")
from scipy.linalg import lu
S = np.array([[1.0, 2.0], [2.0, 1.0]])          # 고윳값 -1, 3  -> 양수 1개
_, _, Upiv = lu(S)
print(f"  S 의 고윳값 : {np.sort(np.linalg.eigvalsh(S))}   양수 {int((np.linalg.eigvalsh(S)>0).sum())}개")
print(f"  scipy.linalg.lu 의 피벗 : {np.diag(Upiv)}   양수 "
      f"{int((np.diag(Upiv)>0).sum())}개   <- 어긋난다 (행을 바꿨다)")
직접 = S.copy()
직접[1] = 직접[1] - 직접[1,0]/직접[0,0] * 직접[0]
print(f"  행 교환 없이 직접 소거한 피벗 : {np.diag(직접)}   양수 "
      f"{int((np.diag(직접)>0).sum())}개   <- 맞다")
L25  피벗을 구할 때 행 교환을 하면 안 된다
  S 의 고윳값 : [-1.  3.]   양수 1개
  scipy.linalg.lu 의 피벗 : [2.  1.5]   양수 2개   <- 어긋난다 (행을 바꿨다)
  행 교환 없이 직접 소거한 피벗 : [ 1. -3.]   양수 1개   <- 맞다
print("L27  무작위로 찔러 보면 양정치를 놓친다")
놓침 = 0
for _ in range(600):
    B = rng.normal(size=(3, 3))
    Ssym = B.T @ B - 0.02 * np.eye(3)           # 아슬아슬하게 만든다
    진짜 = bool((np.linalg.eigvalsh(Ssym) > 0).all())
    표본 = all(float(x @ Ssym @ x) > 0
              for x in rng.normal(size=(200, 3)))
    놓침 += (표본 != 진짜)
print(f"  아슬아슬한 대칭행렬 600개 중 표본 판정이 틀린 것 : {놓침}개")
print()
print("L29  U 를 따로 구하면 깨진다")
def 따로SVD(A):
    lv, V = np.linalg.eigh(A.T @ A)
    lu_, U = np.linalg.eigh(A @ A.T)
    V, U = V[:, np.argsort(lv)[::-1]], U[:, np.argsort(lu_)[::-1]]
    return U, np.sqrt(np.maximum(np.sort(lv)[::-1], 0)), V
깨짐 = sum(np.abs(np.linalg.multi_dot([따로SVD(X)[0], np.diag(따로SVD(X)[1]),
                                      따로SVD(X)[2].T]) - X).max() > 1e-8
          for X in (rng.normal(size=(4, 4)) for _ in range(500)))
print(f"  무작위 4x4 500개 중 복원이 깨진 것 : {깨짐}개")
L27  무작위로 찔러 보면 양정치를 놓친다
  아슬아슬한 대칭행렬 600개 중 표본 판정이 틀린 것 : 77개

L29  U 를 따로 구하면 깨진다
  무작위 4x4 500개 중 복원이 깨진 것 : 461개
print("L30  평행이동은 선형변환이 아니다")
평행 = lambda v: v + np.array([1.0, 2.0])
v, w = np.array([3.0, 0.0]), np.array([0.0, 4.0])
print(f"  T(0) = {평행(np.zeros(2))}   <- 0 이 아니다")
print(f"  T(v+w) = {평행(v+w)},  T(v)+T(w) = {평행(v)+평행(w)}")
print()
print("L31  '큰 것부터 버리기' 는 정규직교에서만 최선이다")
from scipy.fft import dct, idct
x = np.array([7.0, 7.5, 8.0, 8.2, 8.0, 7.0, 5.5, 4.0])
W = idct(np.eye(8), norm="ortho", axis=0)
c = W.T @ x
남 = np.zeros(8); 큰 = np.argsort(-np.abs(c))[:3]; 남[큰] = c[큰]
print(f"  정규직교 : 버린 계수 크기 {np.linalg.norm(c-남):.6f}"
      f"  = 실제 오차 {np.linalg.norm(x - W@남):.6f}")
비 = W + 0.35 * rng.normal(size=(8, 8))
비 = 비 / np.linalg.norm(비, axis=0)
c비 = np.linalg.solve(비, x)
남비 = np.zeros(8); 큰비 = np.argsort(-np.abs(c비))[:3]; 남비[큰비] = c비[큰비]
print(f"  비스듬   : 버린 계수 크기 {np.linalg.norm(c비-남비):.6f}"
      f"  != 실제 오차 {np.linalg.norm(x - 비@남비):.6f}")
L30  평행이동은 선형변환이 아니다
  T(0) = [1. 2.]   <- 0 이 아니다
  T(v+w) = [4. 6.],  T(v)+T(w) = [5. 8.]

L31  '큰 것부터 버리기' 는 정규직교에서만 최선이다
  정규직교 : 버린 계수 크기 0.587672  = 실제 오차 0.587672
  비스듬   : 버린 계수 크기 22.506099  != 실제 오차 17.055422

6. 판단 문제 열 개를 코드로 채점

서술 파트에서 답을 읽었다면, 여기서는 코드가 같은 답을 내는지 본다.

문제 = []

A1 = np.array([[2.0, 5.0], [0.0, 2.0]])
문제.append(("1. [[2,5],[0,2]] 는 대각화 가능한가",
             2 - np.linalg.matrix_rank(A1 - 2*np.eye(2)) == 2, False))

A2 = rng.normal(size=(5, 3))
문제.append(("2. 5x3 랭크3 이면 A^T A 가 양의 정부호인가",
             bool((np.linalg.eigvalsh(A2.T @ A2) > 0).all()), True))

Q4, _ = np.linalg.qr(rng.normal(size=(4, 4)))
문제.append(("3. 직교행렬의 특이값이 전부 1 인가",
             bool(np.allclose(np.linalg.svd(Q4, compute_uv=False), 1.0)), True))

작은det = 1e-4 * np.eye(3)
문제.append(("4. det 이 1e-12 면 반드시 위험한가",
             bool(np.linalg.cond(작은det) > 1e3), False))

S5 = np.array([[3.0, 1.0, 0.0], [1.0, 3.0, 0.0], [0.0, 0.0, -2.0]])
M5 = rng.normal(size=(3, 3))
문제.append(("5. M^T A M 의 고윳값이 A 와 같은가",
             bool(np.allclose(np.sort(np.linalg.eigvalsh(M5.T@S5@M5)),
                              np.sort(np.linalg.eigvalsh(S5)))), False))

B6 = rng.normal(size=(4, 4))
문제.append(("6. A 와 A^T 의 특이값이 같은가",
             bool(np.allclose(np.linalg.svd(B6, compute_uv=False),
                              np.linalg.svd(B6.T, compute_uv=False))), True))

Mk7 = np.array([[0.9, 0.2], [0.1, 0.8]])
문제.append(("7. 마코브의 최대 고윳값이 1 인가",
             bool(np.isclose(max(abs(np.linalg.eigvals(Mk7))), 1.0)), True))

S8 = rng.normal(size=(5, 5)); S8 = S8 + S8.T
문제.append(("8. 실수 대칭이 복소 고윳값을 가질 수 있는가",
             bool(not np.allclose(np.linalg.eigvals(S8).imag, 0)), False))

X9, Y9 = rng.normal(size=(5, 2)), rng.normal(size=(2, 4))
문제.append(("9. rank(XY) <= min(rank X, rank Y) 인가",
             np.linalg.matrix_rank(X9@Y9) <= min(np.linalg.matrix_rank(X9),
                                                 np.linalg.matrix_rank(Y9)), True))

C10 = np.array([[3.0, 0.0], [4.0, 5.0]])
문제.append(("10. |A|_2 가 max|lambda| 와 같은가",
             bool(np.isclose(np.linalg.norm(C10, 2),
                             max(abs(np.linalg.eigvals(C10))))), False))

맞음 = 0
for 글, 코드답, 정답 in 문제:
    맞음 += (코드답 == 정답)
    print(f"{글:<44} 코드 {str(코드답):>5}   서술 파트 {str(정답):>5}   "
          f"{'일치' if 코드답 == 정답 else '어긋남'}")
print(f"\n{맞음}/{len(문제)} 일치")
1. [[2,5],[0,2]] 는 대각화 가능한가                  코드 False   서술 파트 False   일치
2. 5x3 랭크3 이면 A^T A 가 양의 정부호인가               코드  True   서술 파트  True   일치
3. 직교행렬의 특이값이 전부 1 인가                        코드  True   서술 파트  True   일치
4. det 이 1e-12 면 반드시 위험한가                    코드 False   서술 파트 False   일치
5. M^T A M 의 고윳값이 A 와 같은가                    코드 False   서술 파트 False   일치
6. A 와 A^T 의 특이값이 같은가                        코드  True   서술 파트  True   일치
7. 마코브의 최대 고윳값이 1 인가                         코드  True   서술 파트  True   일치
8. 실수 대칭이 복소 고윳값을 가질 수 있는가                   코드 False   서술 파트 False   일치
9. rank(XY) <= min(rank X, rank Y) 인가        코드  True   서술 파트  True   일치
10. |A|_2 가 max|lambda| 와 같은가                코드 False   서술 파트 False   일치

10/10 일치
print("10번을 조금 더 : max|lambda| 와 |A|_2 의 차이")
print(f"{'':>26}{'max|lambda|':>14}{'|A|_2':>10}{'정규':>7}")
for 이름, A in (("[[3,0],[4,5]]", C10),
                ("[[5,4],[4,5]] 대칭", np.array([[5.0, 4.0], [4.0, 5.0]])),
                ("회전 90도", np.array([[0.0, -1.0], [1.0, 0.0]]))):
    print(f"{이름:>26}{max(abs(np.linalg.eigvals(A))):>14.4f}"
          f"{np.linalg.norm(A, 2):>10.4f}"
          f"{str(np.allclose(A@A.T, A.T@A)):>7}")
print()
print("-> 정규행렬일 때만 같다. 그래서 |A|_2 는 늘 sigma1 로 구해야 한다.")
10번을 조금 더 : max|lambda| 와 |A|_2 의 차이
                             max|lambda|     |A|_2     정규
             [[3,0],[4,5]]        5.0000    6.7082  False
          [[5,4],[4,5]] 대칭        9.0000    9.0000   True
                    회전 90도        1.0000    1.0000   True

-> 정규행렬일 때만 같다. 그래서 |A|_2 는 늘 sigma1 로 구해야 한다.

마치며...

서술 파트의 내용이 노트북의 코드
진단 흐름도행렬진단 이 동물원 열두 마리를 갈랐다
실수 vs 복소 대각화회전은 “복소수에서만 가능”
조건수의 뜻실제 증폭이 늘 조건수 이하
조건수는 하나가 아니다κ(A)\kappa(A) 는 1.39, κ(S)\kappa(S)106
고윳값 vs 특이값500개 중 500개가 “정규 = 일치”
대각화 vs SVD200개 중 SVD만 전부 통과
유사 vs 합동500개 중 고윳값 500, 관성 500
MM 이 직교면 겹친다스펙트럼 정리가 특별한 이유
직교 vs 정규직교QTQQ^{\mathsf T}Q 가 대각 vs II
대칭 vs 에르미트A.T 를 쓰면 대각이 복소수
영공간 vs 좌영공간벡터의 길이부터 다르다
함정 열넷전부 코드로 재현
판단 문제 열 개10/10 일치

더 해 볼 것

  1. 행렬진단양정치일 때 콜레스키 분해를 붙여 보자. 실패하면 정말 양정치가 아닌가? L27의 다섯 조건 중 어느 것이 가장 빠른 판정인가?

  2. 동물원에 여러분이 만든 행렬을 추가해 보자. 흐름도가 예상대로 보내는가? 예상과 다르게 가는 행렬을 만들 수 있는가?

  3. 3절의 오차 증폭을 무작위 방향 대신 최악의 방향으로 흔들어 보자. δb\delta\vv{b}un\vv{u}_n 방향으로 주면 증폭이 정확히 조건수에 붙는가?

  4. 행렬진단복소 행렬도 받도록 고쳐 보자. 무엇을 바꿔야 하는가? (A.T 를 전부 찾아내는 것이 첫 걸음이다.)

  5. 4절 ③의 실험에서 MM거의 특이한 행렬로 만들어 보자. 관성이 여전히 보존되는가? detM=0\det M = 0 이면 어떻게 되는가?

다음 강의에서는 미뤄 두었던 마지막 질문에 답한다. 역행렬이 없는 행렬을 어떻게 되돌릴 것인가.