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 24. 마코브 행렬 — 파이썬 실습

Markov Matrices — 실습

L24 서술 파트의 결론은 조건 두 개가 결론 두 개를 낳는다는 것이었다. 성분이 음이 아니고 열의 합이 1이면, 고윳값 1이 반드시 있고 나머지는 단위원을 벗어나지 못한다.

이 노트북에서는 무작위 마코브 행렬을 잔뜩 만들어 그 주장이 정말 예외 없이 성립하는지 확인하고, (1,1,,1)(1,1,\dots,1) 을 정상상태로 착각하는 함정을 코드로 확인하며, 세 상태의 확률분포 구름이 삼각형 위에서 한 점으로 수축하는 것을 본다. 마지막으로 작은 웹을 만들어 페이지랭크를 직접 계산한다.

서술 파트의 내용여기서 확인하는 방법
확률벡터를 확률벡터로성분과 합을 찍어 본다
고윳값 1이 반드시 있다무작위 행렬 500개에서 예외를 찾아본다
AAATA^{\mathsf T} 의 고윳값이 같다특성다항식 계수를 대조
고유벡터는 다르다A(1,1)(1,1)A(1,1) \neq (1,1)
λ1\lvert\lambda\rvert \le 1최대 절댓값을 모아 본다
다른 고유벡터는 성분 합 01Tx\vv{1}^{\mathsf T}\vv{x} 를 찍는다
c1=1c_1 = 1초기 상태를 바꿔도 그대로
어디서 출발하든삼각형 위의 구름이 한 점으로
수렴하지 않는 경우주기적 사슬과 II
페이지랭크작은 웹, 감쇠 전후, λ2α\lvert\lambda_2\rvert \le \alpha
I+hAcI + hA_cL23의 행렬에서 이번 행렬 만들기

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

1. 두 조건이 하는 일

마코브 행렬의 존재 이유는 확률벡터를 확률벡터로 보내는 것이다. 두 조건이 정확히 그것을 보장하는지 확인하자.

A = np.array([[0.9, 0.2],
              [0.1, 0.8]])


def 마코브인가(M, 눈감아=1e-12):
    """성분이 음이 아니고 열의 합이 1인지 본다."""
    M = np.asarray(M, dtype=float)
    return bool((M >= -눈감아).all() and np.allclose(M.sum(axis=0), 1.0))


def 확률벡터인가(p, 눈감아=1e-12):
    """성분이 음이 아니고 합이 1인지 본다."""
    p = np.asarray(p, dtype=float)
    return bool((p >= -눈감아).all() and np.isclose(p.sum(), 1.0))
print(show_matrix(A, "A"))
print("열의 합 :", A.sum(axis=0))
print("마코브 행렬인가 :", 마코브인가(A))
print()
p = np.array([0.3, 0.7])
for k in range(4):
    print(f"k={k} :  p = {p},  합 = {p.sum():.12f},  확률벡터 {확률벡터인가(p)}")
    p = A @ p
A
[  0.9   0.2 ]
[  0.1   0.8 ]
열의 합 : [1. 1.]
마코브 행렬인가 : True

k=0 :  p = [0.3 0.7],  합 = 1.000000000000,  확률벡터 True
k=1 :  p = [0.41 0.59],  합 = 1.000000000000,  확률벡터 True
k=2 :  p = [0.487 0.513],  합 = 1.000000000000,  확률벡터 True
k=3 :  p = [0.5409 0.4591],  합 = 1.000000000000,  확률벡터 True

서술 파트의 핵심은 괄호를 옮기는 한 줄이었다.

1T(Ap)=(1TA)p=1Tp\vv{1}^{\mathsf T}(A\vv{p}) = (\vv{1}^{\mathsf T}A)\vv{p} = \vv{1}^{\mathsf T}\vv{p}

가운데 1TA\vv{1}^{\mathsf T}A 가 곧 열의 합이다.

하나 = np.ones(2)
print("1^T A =", 하나 @ A, "  -> 1^T 인가 :", np.allclose(하나 @ A, 하나))
p = np.array([0.3, 0.7])
print("1^T (A p) =", 하나 @ (A @ p))
print("(1^T A) p =", (하나 @ A) @ p, "  <- 같은 수")
1^T A = [1. 1.]   -> 1^T 인가 : True
1^T (A p) = 1.0
(1^T A) p = 1.0   <- 같은 수

2. 고윳값 1은 우연이 아니다

무작위로 마코브 행렬을 잔뜩 만들어 예외를 찾아보자. 하나라도 있으면 서술 파트가 틀린 것이다.

def 무작위마코브(n, rng):
    """성분을 아무렇게나 뽑고 열마다 합으로 나눈다."""
    M = rng.random((n, n))
    return M / M.sum(axis=0)
최악 = 0.0
표본 = 500
for _ in range(표본):
    n = int(rng.integers(2, 9))
    M = 무작위마코브(n, rng)
    값 = np.linalg.eigvals(M)
    최악 = max(최악, float(np.abs(값 - 1.0).min()))     # 1 에 가장 가까운 고윳값과의 거리
print(f"마코브 행렬 {표본}개에서 '1 에 가장 가까운 고윳값' 과 1 의 거리")
print(f"  그중 최댓값 : {최악:.3e}   -> 전부 정확히 1 을 가지고 있다")
마코브 행렬 500개에서 '1 에 가장 가까운 고윳값' 과 1 의 거리
  그중 최댓값 : 2.442e-15   -> 전부 정확히 1 을 가지고 있다

왜 그런가 — 특성다항식이 같기 때문

(AλI)T=ATλI\left(A - \lambda I\right)^{\mathsf T} = A^{\mathsf T} - \lambda I 이고 detMT=detM\det M^{\mathsf T} = \det M 이므로 두 특성다항식이 글자 그대로 같다.

M = 무작위마코브(5, rng)
print("A   의 특성다항식 계수 :", np.round(np.poly(M), 10))
print("A^T 의 특성다항식 계수 :", np.round(np.poly(M.T), 10))
print("같은가 :", np.allclose(np.poly(M), np.poly(M.T)))
print()
print("A^T 1 =", M.T @ np.ones(5), "  <- 행의 합이 전부 1")
A   의 특성다항식 계수 : [ 1.     -1.1946  0.1826  0.0142 -0.0022  0.0001]
A^T 의 특성다항식 계수 : [ 1.     -1.1946  0.1826  0.0142 -0.0022  0.0001]
같은가 : True

A^T 1 = [1. 1. 1. 1. 1.]   <- 행의 합이 전부 1

그러나 고유벡터는 다르다

하나 = np.ones(2)
print("A^T (1,1) =", A.T @ 하나, "  -> (1,1) 그대로.  A^T 의 고유벡터")
print("A   (1,1) =", A @ 하나, "  -> 달라졌다.  A 의 고유벡터가 아니다")
print()
값, V = np.linalg.eig(A)
i = int(np.argmin(np.abs(값 - 1)))
정상 = V[:, i].real
print("np.linalg.eig 가 준 고유벡터 :", 정상, "  <- 길이가 1 로 맞춰져 있다")
정상 = 정상 / 정상.sum()
print("성분의 합으로 나눈 것        :", 정상, "  = (2/3, 1/3)")
print("확률벡터인가 :", 확률벡터인가(정상))
A^T (1,1) = [1. 1.]   -> (1,1) 그대로.  A^T 의 고유벡터
A   (1,1) = [1.1 0.9]   -> 달라졌다.  A 의 고유벡터가 아니다

np.linalg.eig 가 준 고유벡터 : [0.8944 0.4472]   <- 길이가 1 로 맞춰져 있다
성분의 합으로 나눈 것        : [0.6667 0.3333]   = (2/3, 1/3)
확률벡터인가 : True

3. 나머지 고윳값과 성분의 합

두 가지를 한꺼번에 확인하자. 모든 고윳값이 λ1|\lambda| \le 1 이고, λ1\lambda \neq 1 인 고유벡터는 성분의 합이 0이다.

최대크기 = 0.0
최대합 = 0.0
for _ in range(300):
    n = int(rng.integers(2, 8))
    M = 무작위마코브(n, rng)
    값, V = np.linalg.eig(M)
    최대크기 = max(최대크기, float(np.abs(값).max()))
    for j in range(n):
        if abs(값[j] - 1) > 1e-8:                        # lambda != 1 인 것만
            최대합 = max(최대합, float(abs(V[:, j].sum())))
print(f"모든 고윳값의 절댓값 중 최댓값 : {최대크기:.12f}   -> 1 을 넘지 않는다")
print(f"lambda != 1 인 고유벡터의 성분 합의 절댓값 중 최댓값 : {최대합:.3e}   -> 0 이다")
모든 고윳값의 절댓값 중 최댓값 : 1.000000000000   -> 1 을 넘지 않는다
lambda != 1 인 고유벡터의 성분 합의 절댓값 중 최댓값 : 2.571e-15   -> 0 이다

앵커에서 눈으로 확인된다. x2=(1,1)\vv{x}_2 = (1, -1) 의 성분 합이 0이다.

값, V = np.linalg.eig(A)
for j in range(2):
    x = V[:, j].real
    한마디 = "0 이 아니다" if abs(값[j].real - 1) < 1e-9 else "0 이다"
    print(f"lambda = {값[j].real:.4f} :  고유벡터 {np.round(x/abs(x).max(), 4)},"
          f"   성분의 합 {x.sum():>10.6f}   <- {한마디}")
lambda = 1.0000 :  고유벡터 [1.  0.5],   성분의 합   1.341641   <- 0 이 아니다
lambda = 0.7000 :  고유벡터 [-1.  1.],   성분의 합   0.000000   <- 0 이다

4. 정상상태 — c1c_1 은 언제나 1이다

u0\vv{u}_0 이 확률벡터이기만 하면, 정상상태 방향의 계수가 초기 상태와 무관하게 1이다.

S = np.array([[2/3, 1.0],                              # 열1 = 정규화한 정상상태
              [1/3, -1.0]])
print(f"{'초기 u0':>18}{'c1':>10}{'c2':>10}")
for u0 in ([1.0, 0.0], [0.0, 1.0], [0.5, 0.5], [0.13, 0.87]):
    c = np.linalg.solve(S, np.array(u0))
    print(f"{str(u0):>18}{c[0]:>10.6f}{c[1]:>10.6f}")
print()
print("c1 은 사실 초기벡터의 성분 합이다. 확률벡터가 아닌 것을 넣어 보자.")
for u0 in ([2.0, 1.0], [5.0, 0.0], [0.4, 0.4]):
    c1 = np.linalg.solve(S, np.array(u0))[0]
    print(f"  u0 = {str(u0):>14} :  성분 합 {sum(u0):>5.1f},  c1 = {c1:>7.4f}")
             초기 u0        c1        c2
        [1.0, 0.0]  1.000000  0.333333
        [0.0, 1.0]  1.000000 -0.666667
        [0.5, 0.5]  1.000000 -0.166667
      [0.13, 0.87]  1.000000 -0.536667

c1 은 사실 초기벡터의 성분 합이다. 확률벡터가 아닌 것을 넣어 보자.
  u0 =     [2.0, 1.0] :  성분 합   3.0,  c1 =  3.0000
  u0 =     [5.0, 0.0] :  성분 합   5.0,  c1 =  5.0000
  u0 =     [0.4, 0.4] :  성분 합   0.8,  c1 =  0.8000
정상 = np.array([2/3, 1/3])
u = np.array([1.0, 0.0])
print(f"{'k':>4}{'u_k':>24}{'공식':>24}{'합':>6}")
for k in range(6):
    공식 = 정상 + (1/3) * (0.7 ** k) * np.array([1.0, -1.0])
    print(f"{k:>4}{str(np.round(u, 4)):>24}{str(np.round(공식, 4)):>24}{u.sum():>6.1f}")
    u = A @ u
print()
print("A^50 =")
print(show_matrix(np.linalg.matrix_power(A, 50), "  랭크 1, 두 열이 모두 정상상태"))
   k                     u_k                      공식     합
   0                 [1. 0.]                 [1. 0.]   1.0
   1               [0.9 0.1]               [0.9 0.1]   1.0
   2             [0.83 0.17]             [0.83 0.17]   1.0
   3           [0.781 0.219]           [0.781 0.219]   1.0
   4         [0.7467 0.2533]         [0.7467 0.2533]   1.0
   5         [0.7227 0.2773]         [0.7227 0.2773]   1.0

A^50 =
  랭크 1, 두 열이 모두 정상상태
[  0.667   0.667 ]
[  0.333   0.333 ]

5. 삼각형 위의 구름이 한 점으로

상태가 셋이면 확률벡터는 삼각형(심플렉스) 안의 점이다. 무작위로 뿌린 점 200개가 걸음마다 어떻게 움직이는지 슬라이더로 보자.

P = np.array([[0.8, 0.1, 0.1],
              [0.1, 0.7, 0.3],
              [0.1, 0.2, 0.6]])
print("열의 합 :", P.sum(axis=0), "  마코브인가 :", 마코브인가(P))
값 = np.linalg.eigvals(P)
print("고윳값 :", np.round(np.sort(값.real)[::-1], 6))
print("두 번째 크기 |lambda2| :", np.sort(np.abs(값))[::-1][1])

_, V = np.linalg.eig(P)
s = V[:, int(np.argmin(np.abs(값 - 1)))].real
s = s / s.sum()
print("정상상태 :", np.round(s, 6), "  = (1/3, 7/18, 5/18) 인가 :",
      np.allclose(s, [1/3, 7/18, 5/18]))
열의 합 : [1. 1. 1.]   마코브인가 : True
고윳값 : [1.  0.7 0.4]
두 번째 크기 |lambda2| : 0.7000000000000002
정상상태 : [0.3333 0.3889 0.2778]   = (1/3, 7/18, 5/18) 인가 : True
꼭짓 = np.array([[0.0, 0.0], [1.0, 0.0], [0.5, np.sqrt(3)/2]])
구름 = rng.dirichlet(np.ones(3), size=200)

테두리 = np.vstack([꼭짓, 꼭짓[:1]])
끝점 = s @ 꼭짓

프레임 = []
걸음들 = list(range(0, 16))
점 = 구름.copy()
for k in 걸음들:
    좌표 = 점 @ 꼭짓
    프레임.append([
        go.Scatter(x=테두리[:, 0], y=테두리[:, 1], mode="lines",
                   line=dict(color="#bbbbbb", width=2), showlegend=False),
        go.Scatter(x=좌표[:, 0], y=좌표[:, 1], mode="markers",
                   marker=dict(color=COLORS["input"], size=6, opacity=0.65),
                   name="분포 200개"),
        go.Scatter(x=[끝점[0]], y=[끝점[1]], mode="markers",
                   marker=dict(color="#d62728", size=14), name="정상상태"),
    ])
    점 = 점 @ P.T                                      # 행마다 P p 를 한꺼번에

배치 = layout2d("걸음을 옮기면", extent=1.0)
배치["height"] = 560
배치["xaxis"] = dict(visible=False, range=[-0.1, 1.1], scaleanchor="y")
배치["yaxis"] = dict(visible=False, range=[-0.1, 0.98])
slider_figure(프레임, 걸음들, 배치, prefix="k = ")
Loading...

구름이 삼각형 밖으로 나가는 일은 없다. 1절에서 확인한 대로 마코브 행렬이 확률벡터를 확률벡터로 보내기 때문이다. 그리고 λ2=0.7|\lambda_2| = 0.7 이라 걸음마다 지름이 0.7배씩 준다.

점 = 구름.copy()
print(f"{'k':>4}{'구름의 지름':>14}{'0.7^k':>10}")
초기지름 = None
for k in range(0, 16, 3):
    좌표 = np.linalg.matrix_power(P, k) @ 점.T
    지름 = float(np.abs(좌표 - (s[:, None])).max())
    if 초기지름 is None:
        초기지름 = 지름
    print(f"{k:>4}{지름:>14.6f}{초기지름 * 0.7 ** k:>10.6f}")
   k        구름의 지름     0.7^k
   0      0.666202  0.666202
   3      0.213761  0.228507
   6      0.073320  0.078378
   9      0.025149  0.026884
  12      0.008626  0.009221
  15      0.002959  0.003163

6. 수렴하지 않는 경우

λ1|\lambda| \le 1 은 보장되지만 <1< 1 은 보장되지 않는다. 두 반례를 보자.

B = np.array([[0.0, 1.0], [1.0, 0.0]])              # 반드시 번갈아 오간다
print("B 는 마코브인가 :", 마코브인가(B), "  고윳값 :", np.linalg.eigvals(B).real)
u = np.array([1.0, 0.0])
궤적 = [np.linalg.matrix_power(B, k) @ u for k in range(7)]
print("궤적 :", " -> ".join(f"({v[0]:.0f},{v[1]:.0f})" for v in 궤적))
print("-> 정상상태 (0.5, 0.5) 는 있지만 (1,0) 에서 출발하면 닿지 못한다")
print("   B (0.5,0.5) =", B @ np.array([0.5, 0.5]), " <- 정상상태 자체는 맞다")
print()
I = np.eye(3)
print("I 는 마코브인가 :", 마코브인가(I), "  고윳값 :", np.linalg.eigvals(I).real)
print("-> 모든 확률벡터가 정상상태. 유일하지 않다")
print("   아무 것이나 :", I @ np.array([0.2, 0.3, 0.5]))
B 는 마코브인가 : True   고윳값 : [ 1. -1.]
궤적 : (1,0) -> (0,1) -> (1,0) -> (0,1) -> (1,0) -> (0,1) -> (1,0)
-> 정상상태 (0.5, 0.5) 는 있지만 (1,0) 에서 출발하면 닿지 못한다
   B (0.5,0.5) = [0.5 0.5]  <- 정상상태 자체는 맞다

I 는 마코브인가 : True   고윳값 : [1. 1. 1.]
-> 모든 확률벡터가 정상상태. 유일하지 않다
   아무 것이나 : [0.2 0.3 0.5]

두 반례 모두 0인 성분이 있다. 성분이 전부 양수이면 페론-프로베니우스에 의해 이런 일이 없다. 무작위 행렬로 확인하자.

최대둘째 = 0.0
for _ in range(300):
    n = int(rng.integers(2, 8))
    M = 무작위마코브(n, rng)                          # 성분이 전부 양수
    크기 = np.sort(np.abs(np.linalg.eigvals(M)))[::-1]
    최대둘째 = max(최대둘째, float(크기[1]))
print(f"성분이 전부 양수인 마코브 행렬 300개의 |lambda2| 중 최댓값 : {최대둘째:.6f}")
print("전부 1 보다 작은가 :", 최대둘째 < 1.0)
성분이 전부 양수인 마코브 행렬 300개의 |lambda2| 중 최댓값 : 0.777574
전부 1 보다 작은가 : True

7. 페이지랭크

작은 웹을 하나 만들자. 페이지 jj 에 링크가 djd_j 개 있으면 무작위 서퍼는 각 링크를 1/dj1/d_j 의 확률로 고른다.

링크 = {0: [1, 2], 1: [2], 2: [0], 3: [0, 1],
        4: [],                       # 막다른 페이지
        5: [4, 6], 6: [5], 7: [0, 5]}


def 링크행렬(링크):
    """A[i, j] = 1/d_j  (j 가 i 로 링크했을 때). 막다른 열은 0 으로 둔다."""
    n = len(링크)
    A = np.zeros((n, n))
    for j, 나가는곳 in 링크.items():
        for i in 나가는곳:
            A[i, j] = 1.0 / len(나가는곳)
    return A
A0 = 링크행렬(링크)
print("열의 합 :", A0.sum(axis=0))
print("마코브인가 :", 마코브인가(A0), "  <- 4번 열이 0 이라 아니다")
열의 합 : [1. 1. 1. 1. 0. 1. 1. 1.]
마코브인가 : False   <- 4번 열이 0 이라 아니다

막다른 페이지를 먼저 고친다. 나가는 링크가 없으면 아무 데로나 간다고 두면 된다.

A1 = A0.copy()
막다른 = A1.sum(axis=0) == 0
A1[:, 막다른] = 1.0 / len(링크)
print("고친 뒤 열의 합 :", A1.sum(axis=0), "  마코브인가 :", 마코브인가(A1))
값 = np.linalg.eigvals(A1)
print("고윳값의 절댓값 :", np.round(np.sort(np.abs(값))[::-1], 4))
print("|lambda2| =", np.sort(np.abs(값))[::-1][1], " <- 1 에 가까우면 느리다")
고친 뒤 열의 합 : [1. 1. 1. 1. 1. 1. 1. 1.]   마코브인가 : True
고윳값의 절댓값 : [1.     0.8394 0.7071 0.7071 0.6578 0.0566 0.     0.    ]
|lambda2| = 0.8394385518227846  <- 1 에 가까우면 느리다

이제 감쇠를 넣는다. 확률 1α1-\alpha 로 링크를 무시하고 아무 페이지로나 순간이동한다.

def 구글행렬(A, 알파=0.85):
    """G = alpha A + (1-alpha) J/n. 모든 성분이 양수가 된다."""
    n = A.shape[0]
    return 알파 * A + (1 - 알파) * np.ones((n, n)) / n
알파 = 0.85
G = 구글행렬(A1, 알파)
print("마코브인가 :", 마코브인가(G), "   모든 성분이 양수인가 :", (G > 0).all())
크기 = np.sort(np.abs(np.linalg.eigvals(G)))[::-1]
print("고윳값의 절댓값 :", np.round(크기, 6))
print(f"|lambda2| = {크기[1]:.6f}  <=  alpha = {알파} 인가 :", 크기[1] <= 알파 + 1e-9)
마코브인가 : True    모든 성분이 양수인가 : True
고윳값의 절댓값 : [1.     0.7135 0.601  0.601  0.5592 0.0481 0.     0.    ]
|lambda2| = 0.713523  <=  alpha = 0.85 인가 : True
값, V = np.linalg.eig(G)
순위 = V[:, int(np.argmin(np.abs(값 - 1)))].real
순위 = 순위 / 순위.sum()

# 고윳값 분해 대신 거듭 곱하기만 해도 같은 답이 나온다 (L22 의 지배 고윳값)
p = np.ones(len(링크)) / len(링크)
for _ in range(60):
    p = G @ p
print("고유벡터로 구한 순위 :", np.round(순위, 5))
print("거듭 곱해 구한 순위 :", np.round(p, 5))
print("최대 차이 :", np.abs(순위 - p).max())
print()
for i in np.argsort(순위)[::-1]:
    막대 = "#" * int(round(순위[i] * 200))
    print(f"  page {i} : {순위[i]:.4f}  {막대}")
고유벡터로 구한 순위 : [0.2877 0.1587 0.2827 0.0256 0.0643 0.0911 0.0643 0.0256]
거듭 곱해 구한 순위 : [0.2877 0.1587 0.2827 0.0256 0.0643 0.0911 0.0643 0.0256]
최대 차이 : 1.5103196471244473e-10

  page 0 : 0.2877  ##########################################################
  page 2 : 0.2827  #########################################################
  page 1 : 0.1587  ################################
  page 5 : 0.0911  ##################
  page 6 : 0.0643  #############
  page 4 : 0.0643  #############
  page 7 : 0.0256  #####
  page 3 : 0.0256  #####

감쇠 인자를 바꾸면 수렴 속도가 그대로 따라간다. α\alpha 가 곧 λ2|\lambda_2| 의 상한이다.

print(f"{'alpha':>8}{'|lambda2|':>12}{'60걸음 오차':>14}")
for 알파 in (0.5, 0.7, 0.85, 0.95, 0.99):
    G2 = 구글행렬(A1, 알파)
    크기 = np.sort(np.abs(np.linalg.eigvals(G2)))[::-1]
    값2, V2 = np.linalg.eig(G2)
    참 = V2[:, int(np.argmin(np.abs(값2 - 1)))].real
    참 = 참 / 참.sum()
    p = np.ones(len(링크)) / len(링크)
    for _ in range(60):
        p = G2 @ p
    print(f"{알파:>8}{크기[1]:>12.6f}{np.abs(p - 참).max():>14.2e}")
   alpha   |lambda2|       60걸음 오차
     0.5    0.419719      1.39e-16
     0.7    0.587607      1.19e-15
    0.85    0.713523      1.51e-10
    0.95    0.797467      1.89e-07
    0.99    0.831044      2.80e-06

8. 열의 합 0에서 열의 합 1로

L23의 앵커에 II 를 더하면 이번 강의의 앵커가 된다.

Ac = np.array([[-1.0, 2.0], [1.0, -2.0]])           # L23 의 앵커. 열의 합 0
print("Ac 의 열의 합 :", Ac.sum(axis=0))
print(f"{'h':>6}{'열의 합':>16}{'성분>=0':>10}{'고윳값':>24}{'기대 1+h*lambda':>20}")
for h in (0.05, 0.1, 0.25, 0.5, 0.6):
    Ad = np.eye(2) + h * Ac
    값 = np.sort(np.linalg.eigvals(Ad).real)[::-1]
    print(f"{h:>6}{str(Ad.sum(axis=0)):>16}{str((Ad>=0).all()):>10}"
          f"{str(np.round(값, 4)):>24}{str([1.0, round(1-3*h, 4)]):>20}")
print()
print("h = 0.1 이 이번 강의의 앵커인가 :", np.allclose(np.eye(2) + 0.1*Ac, A))
Ac 의 열의 합 : [0. 0.]
     h            열의 합     성분>=0                     고윳값       기대 1+h*lambda
  0.05         [1. 1.]      True             [1.   0.85]         [1.0, 0.85]
   0.1         [1. 1.]      True               [1.  0.7]          [1.0, 0.7]
  0.25         [1. 1.]      True             [1.   0.25]         [1.0, 0.25]
   0.5         [1. 1.]      True             [ 1.  -0.5]         [1.0, -0.5]
   0.6         [1. 1.]     False             [ 1.  -0.8]         [1.0, -0.8]

h = 0.1 이 이번 강의의 앵커인가 : True

h12h \le \tfrac12 까지만 성분이 음이 아니다. 대각이 1h1-h12h1-2h 이기 때문이다. 그리고 고유벡터는 hh 와 무관하게 그대로이다.

for h in (0.1, 0.3, 0.5):
    Ad = np.eye(2) + h * Ac
    for x, 이름 in ((np.array([2.0, 1.0]), "(2,1)"), (np.array([1.0, -1.0]), "(1,-1)")):
        결과 = Ad @ x
        배율 = 결과[0] / x[0]
        print(f"h={h}:  Ad {이름:>6} = {np.round(결과,4)} = {배율:>6.2f} x "
              f"  (1 + h*lambda = {1 + h*(0 if 이름=='(2,1)' else -3):.2f})")
h=0.1:  Ad  (2,1) = [2. 1.] =   1.00 x   (1 + h*lambda = 1.00)
h=0.1:  Ad (1,-1) = [ 0.7 -0.7] =   0.70 x   (1 + h*lambda = 0.70)
h=0.3:  Ad  (2,1) = [2. 1.] =   1.00 x   (1 + h*lambda = 1.00)
h=0.3:  Ad (1,-1) = [ 0.1 -0.1] =   0.10 x   (1 + h*lambda = 0.10)
h=0.5:  Ad  (2,1) = [2. 1.] =   1.00 x   (1 + h*lambda = 1.00)
h=0.5:  Ad (1,-1) = [-0.5  0.5] =  -0.50 x   (1 + h*lambda = -0.50)

마치며...

서술 파트의 내용이 노트북의 코드
확률벡터 보존확률벡터인가 가 걸음마다 참
괄호 옮기기1T(Ap)=(1TA)p\vv{1}^{\mathsf T}(A\vv{p}) = (\vv{1}^{\mathsf T}A)\vv{p}
고윳값 1무작위 500개에서 예외 0건
특성다항식이 같다np.poly(M)np.poly(M.T) 가 일치
고유벡터는 다르다A(1,1)=(1.1,0.9)A(1,1) = (1.1, 0.9)
λ1\lvert\lambda\rvert \le 1300개에서 최댓값이 정확히 1
성분 합 0λ1\lambda \neq 1 인 고유벡터에서 10-16 수준
c1=1c_1 = 1초기 상태 넷 모두 1.000000
구름의 수축슬라이더. 지름이 0.7k0.7^k
반례주기적 사슬과 II, 둘 다 0인 성분이 있다
페이지랭크막다른 페이지 고치기, 감쇠, λ2α\lvert\lambda_2\rvert \le \alpha
오일러 한 걸음I+hAcI + hA_c, 고유벡터는 그대로

더 해 볼 것

  1. 5절의 PP 를 바꿔 λ2|\lambda_2| 를 0.95쯤으로 만들어 보자. 구름이 눈에 띄게 느려지는가? λ2|\lambda_2| 가 1에 가깝다는 것은 어떤 구조를 뜻하는가?

  2. 7절의 링크를 바꿔 페이지 하나만 서로 링크하는 작은 무리를 만들어 보자. 감쇠를 빼면 순위가 그 무리로 빨려 드는가? 감쇠를 넣으면 어떻게 되는가?

  3. 6절의 주기적 사슬 BB 에 감쇠를 씌워 보자. λ2|\lambda_2| 가 얼마가 되는가?

  4. 마코브 행렬 둘을 곱하면 다시 마코브 행렬인가? 코드로 확인하고 이유를 1T\vv{1}^{\mathsf T} 로 설명해 보자.

다음 강의부터는 가장 좋은 행렬을 다룬다. S=STS = S^{\mathsf T} 인 대칭행렬이다. 고윳값은 전부 실수이고 고유벡터는 서로 직교한다. L21의 앵커 [2112]\begin{bmatrix} 2 & 1 \\ 1 & 2\end{bmatrix} 에서 이미 보았던 그 현상이다.