L13에서 행렬해부 를 만들었다. 소거 한 번으로 랭크, 피벗, 네 부분공간, 해의 존재와
유일성을 한꺼번에 뽑아 주는 함수였다.
이번에는 그것을 후반부 내용까지 담은 행렬진단 으로 확장한다. 고윳값, 대각화
가능성, 대칭성과 정부호성, 특이값, 조건수, 정규성까지 한 번에 찍어 주는 도구다.
그리고 그 도구로 행렬 동물원을 통째로 통과시켜 서술 파트의 흐름도가 실제로
작동하는지 확인한다.
| 서술 파트의 내용 | 여기서 확인하는 방법 |
|---|---|
| 진단 흐름도 | 행렬진단 이 동물원 열두 마리를 분류 |
| 실수 vs 복소 대각화 | 회전행렬은 복소에서만 |
| 조건수의 뜻 | 오차 증폭을 실제로 재기 |
| 조건수는 하나가 아니다 | 는 평평, 는 터짐 |
| 고윳값 vs 특이값 | 정규행렬일 때만 일치 |
| 유사 vs 합동 | 무작위 500개로 |
| 직교 vs 정규직교 | 가 무엇이 되는가 |
| 대칭 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
조건수가 무한대인 것들¶
랭크가 모자라면 이라 조건수가 무한대다. 이것은 "위험하다"가 아니라 역행렬이 아예 없다는 뜻이다.
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 를 푸는 데는 아무 문제가 없다. 고유벡터를 구하는 것이 문제다.
'이 행렬은 상태가 좋은가' 는 무엇을 물을지 정해야 답할 수 있는 질문이다.
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 복소 대각화 | 회전은 “복소수에서만 가능” |
| 조건수의 뜻 | 실제 증폭이 늘 조건수 이하 |
| 조건수는 하나가 아니다 | 는 1.39, 는 106 |
| 고윳값 vs 특이값 | 500개 중 500개가 “정규 = 일치” |
| 대각화 vs SVD | 200개 중 SVD만 전부 통과 |
| 유사 vs 합동 | 500개 중 고윳값 500, 관성 500 |
| 이 직교면 겹친다 | 스펙트럼 정리가 특별한 이유 |
| 직교 vs 정규직교 | 가 대각 vs |
| 대칭 vs 에르미트 | A.T 를 쓰면 대각이 복소수 |
| 영공간 vs 좌영공간 | 벡터의 길이부터 다르다 |
| 함정 열넷 | 전부 코드로 재현 |
| 판단 문제 열 개 | 10/10 일치 |
더 해 볼 것¶
행렬진단에 양정치일 때 콜레스키 분해를 붙여 보자. 실패하면 정말 양정치가 아닌가? L27의 다섯 조건 중 어느 것이 가장 빠른 판정인가?동물원에 여러분이 만든 행렬을 추가해 보자. 흐름도가 예상대로 보내는가? 예상과 다르게 가는 행렬을 만들 수 있는가?
3절의 오차 증폭을 무작위 방향 대신 최악의 방향으로 흔들어 보자. 를 방향으로 주면 증폭이 정확히 조건수에 붙는가?
행렬진단이 복소 행렬도 받도록 고쳐 보자. 무엇을 바꿔야 하는가? (A.T를 전부 찾아내는 것이 첫 걸음이다.)4절 ③의 실험에서 을 거의 특이한 행렬로 만들어 보자. 관성이 여전히 보존되는가? 이면 어떻게 되는가?
다음 강의에서는 미뤄 두었던 마지막 질문에 답한다. 역행렬이 없는 행렬을 어떻게 되돌릴 것인가.