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 27. 양의 정부호 행렬과 최소값 — 파이썬 실습

Positive Definite Matrices and Minima — 실습

L27 서술 파트의 주장은 다섯 개의 조건이 전부 동치라는 것이었다. 고윳값, 선행 주소행렬식, 피벗, 에너지, ATAA^{\mathsf T}A 가 하나로 묶인다.

이 노트북에서는 다섯 조건을 각각 함수로 만들어 무작위 행렬 수백 개에서 한 번도 어긋나지 않는지 확인하고, 슬라이더로 그릇을 안장으로 바꿔 보며, 소거와 제곱완성이 같은 계산임을 숫자로 맞춰 본다. 마지막으로 조건수가 커질수록 경사하강이 몇 걸음 더 걸리는지 재 본다.

서술 파트의 내용여기서 확인하는 방법
다섯 조건이 동치무작위 600개에서 어긋난 횟수
앵커 [[5,4],[4,5]][[5,4],[4,5]]고윳값 1·9, 피벗 5·1.8, 주소행렬식 5·9
에너지는 대칭 부분만비대칭 행렬로 확인
그릇 / 안장 / 골짜기슬라이더로 s12s_{12} 를 움직인다
제곱완성 == 소거계수가 곧 피벗
S=LDLTS = LDL^{\mathsf T}직접 만들어 복원
타원의 축과 반지름고유벡터 방향, 1/λ1/\sqrt\lambda
ATAA^{\mathsf T}AAx2\lVert A\vv{x}\rVert^2, 열 독립이면 양정치
조건수경사하강 걸음 수
촐레스키실패하면 즉시 멈춘다

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(27)
print("numpy", np.__version__)
numpy 2.5.2

1. 다섯 조건을 함수로 만든다

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()


def 선행주소행렬식(S):
    """왼쪽 위 모서리부터 잘라 낸 부분행렬들의 행렬식."""
    S = np.asarray(S, dtype=float)
    return np.array([np.linalg.det(S[:k + 1, :k + 1]) for k in range(S.shape[0])])
def 조건1_고윳값(S, 눈감아=1e-10):
    return bool((np.linalg.eigvalsh(S) > 눈감아).all())


def 조건2_주소행렬식(S, 눈감아=1e-10):
    return bool((선행주소행렬식(S) > 눈감아).all())


def 조건3_피벗(S, 눈감아=1e-10):
    p = 피벗들(S)
    return bool(p is not None and (p > 눈감아).all())


def 조건4_에너지(S, rng, 표본=2000):
    """아무 x 나 넣어 봐서 음수가 나오는지 본다. 반례 찾기용이지 증명은 아니다."""
    x = rng.normal(size=(np.shape(S)[0], 표본))
    return bool((np.einsum("ik,ij,jk->k", x, np.asarray(S, float), x) > 0).all())


def 조건5_촐레스키(S):
    """S = R R^T 를 찾을 수 있으면 양정치. 실패하면 그 자리에서 끝난다."""
    try:
        np.linalg.cholesky(S)
        return True
    except np.linalg.LinAlgError:
        return False
S = np.array([[5.0, 4.0],
              [4.0, 5.0]])
print(show_matrix(S, "앵커 S"))
print("고윳값           :", np.linalg.eigvalsh(S), "  (기대 1, 9)")
print("선행 주소행렬식  :", 선행주소행렬식(S), "  (기대 5, 9)")
print("피벗             :", 피벗들(S), "  (기대 5, 1.8)")
print("피벗의 곱        :", np.prod(피벗들(S)), " = det =", np.linalg.det(S))
print()
for 이름, 판정 in (("① 고윳값", 조건1_고윳값), ("② 주소행렬식", 조건2_주소행렬식),
                  ("③ 피벗", 조건3_피벗), ("⑤ 촐레스키", 조건5_촐레스키)):
    print(f"  {이름:>14} : {판정(S)}")
print(f"  {'④ 에너지':>14} : {조건4_에너지(S, rng)}")
앵커 S
[  5   4 ]
[  4   5 ]
고윳값           : [1. 9.]   (기대 1, 9)
선행 주소행렬식  : [5. 9.]   (기대 5, 9)
피벗             : [5.  1.8]   (기대 5, 1.8)
피벗의 곱        : 9.0  = det = 8.999999999999998

           ① 고윳값 : True
         ② 주소행렬식 : True
            ③ 피벗 : True
          ⑤ 촐레스키 : True
           ④ 에너지 : True

3×33 \times 3 도 보자. 선행 주소행렬식이 2,3,42, 3, 4 이고 피벗이 2,32,432, \tfrac32, \tfrac43 인 행렬이다.

T = np.array([[2.0, -1.0, 0.0],
              [-1.0, 2.0, -1.0],
              [0.0, -1.0, 2.0]])
print("고윳값          :", np.linalg.eigvalsh(T))
print("  = 2-sqrt2, 2, 2+sqrt2 인가 :",
      np.allclose(np.linalg.eigvalsh(T), sorted([2-np.sqrt(2), 2, 2+np.sqrt(2)])))
print("선행 주소행렬식 :", 선행주소행렬식(T), "  (기대 2, 3, 4)")
print("피벗            :", 피벗들(T), "  (기대 2, 1.5, 1.3333)")
print("피벗의 곱       :", np.prod(피벗들(T)), " = det =", np.linalg.det(T))
고윳값          : [0.5858 2.     3.4142]
  = 2-sqrt2, 2, 2+sqrt2 인가 : True
선행 주소행렬식 : [2. 3. 4.]   (기대 2, 3, 4)
피벗            : [2.     1.5    1.3333]   (기대 2, 1.5, 1.3333)
피벗의 곱       : 4.0  = det = 4.0

2. 정말 동치인가

무작위 대칭행렬을 잔뜩 만들어 판정이 한 번이라도 갈리는지 본다. 먼저 계산으로 정확히 답이 나오는 넷(고윳값·주소행렬식·피벗·촐레스키)이다.

표본 = 600
정확어긋 = []
양, 음 = 0, 0
표본들 = []
for _ in range(표본):
    n = int(rng.integers(2, 6))
    B = rng.normal(size=(n, n))
    M = B + B.T
    if rng.random() < 0.5:
        M = M + n * np.eye(n)                  # 절반쯤은 양정치가 되게 밀어 준다
    표본들.append(M)
    판정 = (조건1_고윳값(M), 조건2_주소행렬식(M), 조건3_피벗(M), 조건5_촐레스키(M))
    if len(set(판정)) != 1:
        정확어긋.append((M, 판정))
    if 판정[0]:
        양 += 1
    else:
        음 += 1

print(f"대칭행렬 {표본}개 : 양정치 {양}개, 아닌 것 {음}개  -> 양쪽이 다 나온다")
print(f"네 판정이 갈린 경우 : {len(정확어긋)}건")
대칭행렬 600개 : 양정치 187개, 아닌 것 413개  -> 양쪽이 다 나온다
네 판정이 갈린 경우 : 0건

한 번도 갈리지 않는다. 서로 다른 강의에서 배운 넷이 정말 같은 것을 말한다.

이제 에너지 조건을 보자. 그런데 이것은 성격이 다르다. "모든 x\vv{x} 에 대해"라는 조건이라 컴퓨터로 확인할 수가 없다. x\vv{x} 를 아무리 많이 뽑아 봐도 안 뽑아 본 방향이 남는다. 그래도 해 보자.

에너지어긋 = []
for M in 표본들:
    if 조건1_고윳값(M) != 조건4_에너지(M, rng):
        에너지어긋.append(M)

print(f"표본으로 판정한 에너지가 실제와 어긋난 경우 : {len(에너지어긋)}건 / {표본}")
print()
for M in 에너지어긋[:3]:
    값 = np.linalg.eigvalsh(M)
    print(f"  n={len(M)}, 고윳값 {np.round(값, 4)}")
    print(f"     음수인 고윳값의 크기 / 가장 큰 고윳값 = "
          f"{abs(값).min()/abs(값).max():.1e}   <- 아주 얇은 방향 하나뿐")
표본으로 판정한 에너지가 실제와 어긋난 경우 : 5건 / 600

  n=4, 고윳값 [-0.0324  2.8445  4.754  11.1325]
     음수인 고윳값의 크기 / 가장 큰 고윳값 = 2.9e-03   <- 아주 얇은 방향 하나뿐
  n=3, 고윳값 [-0.0009  2.3857  3.2305]
     음수인 고윳값의 크기 / 가장 큰 고윳값 = 2.9e-04   <- 아주 얇은 방향 하나뿐
  n=5, 고윳값 [-0.0685  2.1032  4.757   6.684  11.4574]
     음수인 고윳값의 크기 / 가장 큰 고윳값 = 6.0e-03   <- 아주 얇은 방향 하나뿐

어긋난 것들의 정체가 분명하다. 음수인 고윳값이 딱 하나이고 그것도 아주 작다. 그런 행렬은 거의 모든 방향에서 에너지가 양수이고, 오직 한 방향의 아주 좁은 원뿔 안에서만 음수이다. 무작위로 뽑은 x\vv{x} 가 거기에 걸릴 확률이 낮다.

찾는 법은 안다. 가장 작은 고윳값의 고유벡터를 넣어 보면 된다. 그 방향의 에너지가 정확히 λmin\lambda_{\min} 이기 때문이다.

def 조건4_정확(S, 눈감아=1e-10):
    """최소 고유벡터 방향만 보면 된다. 거기가 에너지가 가장 작은 곳이다."""
    값, V = np.linalg.eigh(S)
    v = V[:, 0]
    return bool(v @ np.asarray(S, float) @ v > 눈감아)


어긋 = sum(1 for M in 표본들 if 조건1_고윳값(M) != 조건4_정확(M))
print(f"최소 고유벡터로 판정하면 어긋난 경우 : {어긋}건 / {표본}")
print()
for M in 에너지어긋[:3]:
    값, V = np.linalg.eigh(M)
    v = V[:, 0]
    print(f"  최소 고유벡터 방향의 에너지 : {v @ M @ v:>11.4e}"
          f"   = 최소 고윳값 {값[0]:>11.4e}")
최소 고유벡터로 판정하면 어긋난 경우 : 0건 / 600

  최소 고유벡터 방향의 에너지 : -3.2439e-02   = 최소 고윳값 -3.2439e-02
  최소 고유벡터 방향의 에너지 : -9.4755e-04   = 최소 고윳값 -9.4755e-04
  최소 고유벡터 방향의 에너지 : -6.8513e-02   = 최소 고윳값 -6.8513e-02

3. 에너지는 대칭 부분만 본다

xTAx=xTA+AT2x\vv{x}^{\mathsf T}A\vv{x} = \vv{x}^{\mathsf T}\frac{A + A^{\mathsf T}}{2}\vv{x} 였다.

A = np.array([[3.0, 7.0],
              [-1.0, 2.0]])                      # 대칭이 아니다
대칭부 = (A + A.T) / 2
비대칭부 = (A - A.T) / 2
print(show_matrix(A, "A  (비대칭)"))
print(show_matrix(대칭부, "대칭 부분"))
print(show_matrix(비대칭부, "비대칭 부분"))

x = rng.normal(size=(2, 5))
왼 = np.array([xx @ A @ xx for xx in x.T])
오 = np.array([xx @ 대칭부 @ xx for xx in x.T])
비 = np.array([xx @ 비대칭부 @ xx for xx in x.T])
print()
print("x^T A x        :", np.round(왼, 6))
print("x^T (대칭부) x :", np.round(오, 6), "  <- 같다")
print("x^T (비대칭부) x:", np.round(비, 12), "  <- 언제나 0")
A  (비대칭)
[   3    7 ]
[  -1    2 ]
대칭 부분
[  3   3 ]
[  3   2 ]
비대칭 부분
[   0    4 ]
[  -4    0 ]

x^T A x        : [ 0.3649 -0.5454 -0.1088 -0.0042  6.6172]
x^T (대칭부) x : [ 0.3649 -0.5454 -0.1088 -0.0042  6.6172]   <- 같다
x^T (비대칭부) x: [0. 0. 0. 0. 0.]   <- 언제나 0

4. 그릇에서 안장으로

S(t)=[2tt2]S(t) = \begin{bmatrix} 2 & t \\ t & 2 \end{bmatrix}

tt 를 키우면 언제 그릇이 안장으로 바뀌는가. 고윳값이 2±t2 \pm t 이므로 t=2t = 2 가 경계이다.

print(f"{'t':>6}{'고윳값':>20}{'det':>10}{'판정':>28}")
for t in (0.0, 1.0, 1.9, 2.0, 2.5, 4.0):
    M = np.array([[2.0, t], [t, 2.0]])
    값 = np.linalg.eigvalsh(M)
    if (값 > 1e-9).all():
        판정 = "양의 정부호 — 그릇"
    elif (값 > -1e-9).all():
        판정 = "준정부호 — 골짜기"
    else:
        판정 = "부정부호 — 안장"
    print(f"{t:>6}{str(np.round(값,3)):>20}{np.linalg.det(M):>10.2f}{판정:>28}")
     t                 고윳값       det                          판정
   0.0             [2. 2.]      4.00                 양의 정부호 — 그릇
   1.0             [1. 3.]      3.00                 양의 정부호 — 그릇
   1.9           [0.1 3.9]      0.39                 양의 정부호 — 그릇
   2.0             [0. 4.]      0.00                  준정부호 — 골짜기
   2.5         [-0.5  4.5]     -2.25                   부정부호 — 안장
   4.0           [-2.  6.]    -12.00                   부정부호 — 안장
격자 = np.linspace(-2.2, 2.2, 160)
X, Y = np.meshgrid(격자, 격자)
t들 = np.round(np.linspace(0.0, 4.0, 21), 2)

프레임 = []
for t in t들:
    M = np.array([[2.0, t], [t, 2.0]])
    Z = 2 * X**2 + 2 * t * X * Y + 2 * Y**2
    값 = np.linalg.eigvalsh(M)
    if (값 > 1e-9).all():
        말, 색 = "양의 정부호 — 그릇", "#2ca02c"
    elif (값 > -1e-9).all():
        말, 색 = "준정부호 — 골짜기", "#7f7f7f"
    else:
        말, 색 = "부정부호 — 안장", "#d62728"
    프레임.append([
        go.Contour(x=격자, y=격자, z=Z, contours=dict(coloring="lines",
                   start=-16, end=32, size=3), line=dict(width=2),
                   colorscale="RdBu", reversescale=True, showscale=False),
        go.Scatter(x=[0], y=[-2.75], mode="text",
                   text=[f"고윳값 {np.round(값,2)}   {말}"],
                   textfont=dict(size=16, color=색), showlegend=False),
    ])

배치 = layout2d("S(t) = [[2, t], [t, 2]] 의 등고선", extent=3.0)
배치["height"] = 600
slider_figure(프레임, t들, 배치, prefix="t = ", initial=5)
Loading...

t<2t < 2 이면 등고선이 닫힌 타원이다. t=2t = 2 에서 타원이 무한히 길어져 평행선이 되고, t>2t > 2 를 넘으면 쌍곡선으로 바뀐다. 안장이 된 것이다.

5. 소거가 곧 제곱완성

xTSx=5(x1+0.8x2)2+1.8x22\vv{x}^{\mathsf T}S\vv{x} = 5(x_1 + 0.8x_2)^2 + 1.8\,x_2^2

계수 51.8 이 피벗이고, 0.8 이 소거의 배수였다.

x = rng.normal(size=(2, 6))
왼 = np.array([xx @ S @ xx for xx in x.T])
오 = 5 * (x[0] + 0.8 * x[1]) ** 2 + 1.8 * x[1] ** 2
print("x^T S x      :", np.round(왼, 6))
print("제곱완성 결과 :", np.round(오, 6))
print("최대 차이 :", np.abs(왼 - 오).max())
x^T S x      : [ 5.0354 13.1014  1.6169  6.4423 22.382   1.5392]
제곱완성 결과 : [ 5.0354 13.1014  1.6169  6.4423 22.382   1.5392]
최대 차이 : 3.552713678800501e-15
def LDL(S):
    """행 교환 없이 소거하며 배수를 L 에, 피벗을 D 에 모은다."""
    U = np.array(S, dtype=float)
    n = U.shape[0]
    L = np.eye(n)
    for k in range(n - 1):
        for i in range(k + 1, n):
            배수 = U[i, k] / U[k, k]
            L[i, k] = 배수
            U[i] -= 배수 * U[k]
    return L, np.diag(np.diag(U))
L, D = LDL(S)
print(show_matrix(L, "L  (배수)"))
print(show_matrix(D, "D  (피벗)"))
print("L D L^T = S 인가 :", np.allclose(L @ D @ L.T, S))
print()
L3, D3 = LDL(T)
print(show_matrix(L3, "3x3 의 L"))
print(show_matrix(D3, "3x3 의 D"))
print("복원되는가 :", np.allclose(L3 @ D3 @ L3.T, T))
print("D 의 대각이 피벗과 같은가 :", np.allclose(np.diag(D3), 피벗들(T)))
L  (배수)
[    1     0 ]
[  0.8     1 ]
D  (피벗)
[    5     0 ]
[    0   1.8 ]
L D L^T = S 인가 : True

3x3 의 L
[       1        0        0 ]
[    -0.5        1        0 ]
[       0   -0.667        1 ]
3x3 의 D
[     2      0      0 ]
[     0    1.5      0 ]
[     0      0   1.33 ]
복원되는가 : True
D 의 대각이 피벗과 같은가 : True

y=LTx\vv{y} = L^{\mathsf T}\vv{x} 로 두면 에너지가 diyi2\sum d_iy_i^2 이 된다.

x3 = rng.normal(size=(3, 5))
y3 = L3.T @ x3
왼 = np.array([xx @ T @ xx for xx in x3.T])
오 = (np.diag(D3)[:, None] * y3 ** 2).sum(axis=0)
print("x^T T x    :", np.round(왼, 6))
print("sum d_i y_i^2:", np.round(오, 6))
print("최대 차이 :", np.abs(왼 - 오).max())
x^T T x    : [ 4.6976 14.1237  3.2288  1.9082  0.0535]
sum d_i y_i^2: [ 4.6976 14.1237  3.2288  1.9082  0.0535]
최대 차이 : 3.552713678800501e-15

6. 타원의 축과 반지름

xTSx=1\vv{x}^{\mathsf T}S\vv{x} = 1 의 축은 고유벡터 방향이고 반지름은 1/λ1/\sqrt\lambda 였다.

값, Q = np.linalg.eigh(S)
print("고윳값 :", 값)
print("반지름 1/sqrt(lambda) :", 1 / np.sqrt(값), "  (기대 1, 1/3)")
print()
for j in range(2):
    끝 = Q[:, j] / np.sqrt(값[j])                # 축의 끝점
    print(f"  lambda={값[j]:.0f} 축 끝 {np.round(끝,4)},  "
          f"거기서 x^T S x = {끝 @ S @ 끝:.6f}  <- 1 이어야 한다")
고윳값 : [1. 9.]
반지름 1/sqrt(lambda) : [1.     0.3333]   (기대 1, 1/3)

  lambda=1 축 끝 [-0.7071  0.7071],  거기서 x^T S x = 1.000000  <- 1 이어야 한다
  lambda=9 축 끝 [0.2357 0.2357],  거기서 x^T S x = 1.000000  <- 1 이어야 한다
t = np.linspace(0, 2 * np.pi, 400)
타원 = Q @ np.diag(1 / np.sqrt(값)) @ np.vstack([np.cos(t), np.sin(t)])
자료 = [go.Scatter(x=타원[0], y=타원[1], mode="lines", name="x^T S x = 1",
                  line=dict(color=COLORS["output"], width=4))]
for j, 색 in enumerate(("#2ca02c", "#d62728")):
    끝 = Q[:, j] / np.sqrt(값[j])
    자료.append(go.Scatter(x=[-끝[0], 끝[0]], y=[-끝[1], 끝[1]], mode="lines",
                          line=dict(color=색, width=3),
                          name=f"q{j+1} 축,  lambda={값[j]:.0f},  "
                               f"반지름 {1/np.sqrt(값[j]):.3f}"))
go.Figure(data=자료, layout=layout2d("축은 고유벡터, 반지름은 1/sqrt(lambda)",
                                    extent=1.3))
Loading...

7. ATAA^{\mathsf T}A — L16의 빚

xTATAx=Ax2\vv{x}^{\mathsf T}A^{\mathsf T}A\vv{x} = \lVert A\vv{x}\rVert^2 이므로 늘 준정부호 이상이고, 열이 독립이면 양정치이다.

print(f"{'A 의 크기':>12}{'열 독립':>10}{'최소 고윳값':>16}{'양정치':>10}{'가역':>8}")
for m, n in ((5, 3), (4, 4), (3, 5), (6, 2)):
    A = rng.normal(size=(m, n))
    G = A.T @ A
    독립 = np.linalg.matrix_rank(A) == n
    최소 = np.linalg.eigvalsh(G).min()
    print(f"{f'{m}x{n}':>12}{str(독립):>10}{최소:>16.3e}"
          f"{str(조건1_고윳값(G)):>10}{str(abs(np.linalg.det(G)) > 1e-12):>8}")
      A 의 크기      열 독립          최소 고윳값       양정치      가역
         5x3      True       1.350e+00      True    True
         4x4      True       1.644e-01      True    True
         3x5     False      -3.059e-16     False   False
         6x2      True       1.689e+00      True    True
# 에너지가 정말 |Ax|^2 인가
A = rng.normal(size=(5, 3))
x = rng.normal(size=(3, 4))
왼 = np.array([xx @ (A.T @ A) @ xx for xx in x.T])
오 = (np.linalg.norm(A @ x, axis=0)) ** 2
print("x^T A^T A x :", np.round(왼, 8))
print("|A x|^2     :", np.round(오, 8))
print("최대 차이 :", np.abs(왼 - 오).max())
print()
print("A^T A 가 대칭인가 :", np.allclose(A.T @ A, (A.T @ A).T))
x^T A^T A x : [ 2.7576  2.395  12.0071  6.2829]
|A x|^2     : [ 2.7576  2.395  12.0071  6.2829]
최대 차이 : 1.7763568394002505e-15

A^T A 가 대칭인가 : True

열이 종속이면 어떻게 되는가. 골짜기가 생긴다.

A = rng.normal(size=(5, 3))
A[:, 2] = A[:, 0] + 2 * A[:, 1]                  # 3열을 종속으로
G = A.T @ A
print("A 의 랭크 :", np.linalg.matrix_rank(A))
print("A^T A 의 고윳값 :", np.linalg.eigvalsh(G))
print("0 인 고윳값의 개수 :", int((np.abs(np.linalg.eigvalsh(G)) < 1e-9).sum()))
print("  = 3 - 랭크 =", 3 - np.linalg.matrix_rank(A))
print()
_, V = np.linalg.eigh(G)
평평 = V[:, 0]
print("평평한 방향 :", np.round(평평 / abs(평평).max(), 4), "  (1, 2, -1 의 배수)")
print("  그 방향의 에너지 :", 평평 @ G @ 평평)
print("  A 를 곱하면 :", np.round(A @ 평평, 12))
A 의 랭크 : 2
A^T A 의 고윳값 : [-0.      2.0285 46.1201]
0 인 고윳값의 개수 : 1
  = 3 - 랭크 = 1

평평한 방향 : [-0.5 -1.   0.5]   (1, 2, -1 의 배수)
  그 방향의 에너지 : 3.625973214694835e-16
  A 를 곱하면 : [-0. -0.  0.  0.  0.]

8. 조건수가 크면 헤맨다

def 경사하강(M, 시작=(1.1, 0.35), 눈감아=1e-6, 최대=200_000):
    """f = x^T M x 를 최적 고정 걸음으로 내려간다. 걸음 수와 경로를 돌려준다."""
    값 = np.linalg.eigvalsh(M)
    보폭 = 1.0 / (값.max() + 값.min())            # 표준 최적 걸음
    v = np.array(시작, dtype=float)
    경로 = [v.copy()]
    while np.linalg.norm(v) > 눈감아 and len(경로) <= 최대:
        v = v - 보폭 * (2 * M @ v)                # 기울기는 2 M v
        경로.append(v.copy())
    return len(경로) - 1, np.array(경로)
print(f"{'':>22}{'조건수':>10}{'(k-1)/(k+1)':>14}{'걸음':>8}{'이론':>8}")
for 이름, M in (("[[1,0],[0,1.2]]", np.array([[1.0, 0.0], [0.0, 1.2]])),
                ("앵커 [[5,4],[4,5]]", S),
                ("[[1,0],[0,30]]", np.array([[1.0, 0.0], [0.0, 30.0]])),
                ("[[1,0],[0,300]]", np.array([[1.0, 0.0], [0.0, 300.0]]))):
    값 = np.linalg.eigvalsh(M)
    k = 값.max() / 값.min()
    r = (k - 1) / (k + 1)
    걸음, _ = 경사하강(M)
    이론 = np.log(1e-6 / np.linalg.norm([1.1, 0.35])) / np.log(r)
    print(f"{이름:>22}{k:>10.1f}{r:>14.4f}{걸음:>8}{이론:>8.0f}")
                             조건수   (k-1)/(k+1)      걸음      이론
       [[1,0],[0,1.2]]       1.2        0.0909       6       6
      앵커 [[5,4],[4,5]]       9.0        0.8000      63      63
        [[1,0],[0,30]]      30.0        0.9355     210     209
       [[1,0],[0,300]]     300.0        0.9934    2094    2094

걸음 수가 이론값과 거의 정확히 맞는다. 조건수가 열 배가 되면 걸음도 대략 열 배가 된다.

자료 = []
for 이름, M, 색 in (("조건수 1.2", np.array([[1.0, 0.0], [0.0, 1.2]]), "#2ca02c"),
                   ("조건수 9 (앵커)", S, COLORS["input"]),
                   ("조건수 30", np.array([[1.0, 0.0], [0.0, 30.0]]), "#d62728")):
    _, 경로 = 경사하강(M)
    보일 = 경로[:40]
    자료.append(go.Scatter(x=보일[:, 0], y=보일[:, 1], mode="lines+markers",
                          name=이름, line=dict(color=색, width=2),
                          marker=dict(size=5)))
go.Figure(data=자료, layout=layout2d("같은 자리에서 출발한 세 궤적 (앞 40걸음)",
                                    extent=1.3))
Loading...

9. 촐레스키 — 실패하면 즉시 멈춘다

R = np.linalg.cholesky(T)
print(show_matrix(R, "R  (하삼각)"))
print("R R^T = T 인가 :", np.allclose(R @ R.T, T))
print("R 의 대각 :", np.round(np.diag(R), 6))
print("그 제곱   :", np.round(np.diag(R) ** 2, 6), " = 피벗", np.round(피벗들(T), 6))
print()
print("A = R^T 로 두면 A^T A = T 인가 :", np.allclose(R @ R.T, T),
      " <- 조건 ⑤ 가 구성적으로 확인된다")
R  (하삼각)
[    1.41        0        0 ]
[  -0.707     1.22        0 ]
[       0   -0.816     1.15 ]
R R^T = T 인가 : True
R 의 대각 : [1.4142 1.2247 1.1547]
그 제곱   : [2.     1.5    1.3333]  = 피벗 [2.     1.5    1.3333]

A = R^T 로 두면 A^T A = T 인가 : True  <- 조건 ⑤ 가 구성적으로 확인된다
나쁨 = np.array([[1.0, 10.0], [10.0, 1.0]])
print(show_matrix(나쁨, "[[1,10],[10,1]]  — 대각은 양수인데"))
print("고윳값          :", np.linalg.eigvalsh(나쁨), "  <- 하나가 음수")
print("선행 주소행렬식 :", 선행주소행렬식(나쁨), "  <- 두 번째에서 걸린다")
print("피벗            :", 피벗들(나쁨))
print("촐레스키        :", 조건5_촐레스키(나쁨))
print()
x = np.array([1.0, -1.0])
print("x = (1,-1) 의 에너지 :", x @ 나쁨 @ x, "  <- 음수. 반례를 손에 쥐었다")
[[1,10],[10,1]]  — 대각은 양수인데
[   1   10 ]
[  10    1 ]
고윳값          : [-9. 11.]   <- 하나가 음수
선행 주소행렬식 : [  1. -99.]   <- 두 번째에서 걸린다
피벗            : [  1. -99.]
촐레스키        : False

x = (1,-1) 의 에너지 : -18.0   <- 음수. 반례를 손에 쥐었다

대각만 보고 판정하면 안 된다. 대각 밖의 성분이 크면 언제든 무너진다.

print(f"{'c':>6}{'[[1,c],[c,1]] 의 고윳값':>26}{'양정치':>10}")
for c in (0.0, 0.5, 0.9, 1.0, 1.5, 10.0):
    M = np.array([[1.0, c], [c, 1.0]])
    print(f"{c:>6}{str(np.round(np.linalg.eigvalsh(M), 3)):>26}"
          f"{str(조건1_고윳값(M)):>10}")
print()
print("고윳값이 1 ± c 이므로 |c| < 1 일 때만 양정치이다.")
     c       [[1,c],[c,1]] 의 고윳값       양정치
   0.0                   [1. 1.]      True
   0.5                 [0.5 1.5]      True
   0.9                 [0.1 1.9]      True
   1.0                   [0. 2.]     False
   1.5               [-0.5  2.5]     False
  10.0                 [-9. 11.]     False

고윳값이 1 ± c 이므로 |c| < 1 일 때만 양정치이다.

마치며...

서술 파트의 내용이 노트북의 코드
다섯 조건각각 함수로. 무작위 600개에서 어긋난 것 0건
앵커고윳값 1·9, 주소행렬식 5·9, 피벗 5·1.8
피벗의 곱 == 행렬식5×1.8=95 \times 1.8 = 9
에너지는 대칭 부분만비대칭 부분의 에너지가 언제나 0
그릇에서 안장으로슬라이더. t=2t = 2 에서 타원이 쌍곡선으로
제곱완성 == 소거5(x1+0.8x2)2+1.8x225(x_1+0.8x_2)^2 + 1.8x_2^2 가 정확히 일치
LDLTLDL^{\mathsf T}직접 만들어 복원, diyi2\sum d_iy_i^2 확인
타원축 끝에서 에너지가 정확히 1
ATAA^{\mathsf T}A에너지 =Ax2= \lVert A\vv{x}\rVert^2, 종속이면 0 고윳값
조건수걸음 수가 이론값 (κ1)/(κ+1)(\kappa-1)/(\kappa+1) 과 일치
촐레스키대각의 제곱이 피벗. 반례에서 실패

더 해 볼 것

  1. 4절의 슬라이더에서 대각을 2 가 아니라 28 로 다르게 해 보자. 경계가 되는 tt 는 얼마인가? det=0\det = 0 과 관계가 있는가?

  2. 8절의 경사하강 에서 보폭을 1/λmax1/\lambda_{\max} 로 바꿔 보자. 수렴하는가? 왜 그런지 12αλ1 - 2\alpha\lambda 로 설명해 보자.

  3. 3×33 \times 3 이상에서 선행 주소행렬식은 전부 양수인데 다른 주소행렬식이 음수인 대칭행렬을 찾을 수 있는가? 없다면 왜인가?

  4. 무작위 AAATAA^{\mathsf T}A 를 만들고 그 조건수를 재 보자. AA 의 조건수와 어떤 관계인가? (제곱이 되는지 확인해 보자. L33의 예고편이다.)

다음 강의에서는 오랫동안 미뤄 둔 물음에 답한다. 고유벡터가 모자란 행렬은 대체 어떻게 해야 하는가.