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 13. 중간 정리 — 파이썬 실습

행렬 해부 도구 만들기

L13 서술 파트에서 소거 한 번으로 얻는 rr 이 나머지를 전부 결정하는 것을 보았고, 그것을 A=CRA = CR 이라는 등식 하나로 묶었다.

이 노트북은 새 개념을 배우는 자리가 아니라 도구를 만드는 자리이다. 행렬을 넣으면 L1부터 L13까지의 모든 정보를 한 번에 돌려주는 행렬해부(A, b) 를 조립하고, 앞으로의 강의에서 계속 꺼내 쓰도록 하자.

만들 것하는 일
기약사다리꼴(A)정확한 RR 과 피벗 열 번호
랭크분해(A)A=CRA = CR 의 두 조각
영공간행렬(A)특수해를 열로 세운 NN
행렬해부(A, b)위의 것을 전부 묶은 최종 도구

0. 준비

import numpy as np
import sympy as sp
from scipy.linalg import null_space

from linalg_viz import show_matrix

np.set_printoptions(precision=3, suppress=True)
rng = np.random.default_rng(13)
print("numpy", np.__version__, " sympy", sp.__version__)
numpy 2.5.2  sympy 1.14.0

L7부터 계속 쓰던 행렬을 그대로 가져온다. 서술 파트의 예와 같아야 대조하기 쉽다.

A = np.array([[1.0, 2, 2, 2],
              [2.0, 4, 6, 8],
              [3.0, 6, 8, 10]])
b = np.array([1.0, 5, 6])

print(show_matrix(A, "A ="))
print("크기 :", A.shape)
A =
[   1    2    2    2 ]
[   2    4    6    8 ]
[   3    6    8   10 ]
크기 : (3, 4)

1. 소거 한 번으로 얻는 것

sympy 는 분수를 그대로 다루므로 소거 과정에 반올림 오차가 끼지 않는다. 피벗 열 번호까지 함께 돌려주는 것이 요점이다.

def 기약사다리꼴(A):
    """정확한 기약 사다리꼴 R 과 피벗 열 번호를 돌려준다."""
    M = sp.Matrix(np.asarray(A, dtype=float).tolist()).applyfunc(sp.Rational)
    R_sym, 피벗 = M.rref()
    return np.array(R_sym, dtype=float), list(피벗)
R, 피벗 = 기약사다리꼴(A)
r = len(피벗)
n = A.shape[1]
자유 = [j for j in range(n) if j not in 피벗]

print(show_matrix(R, "R (기약 사다리꼴)"))
print("피벗열 :", [j + 1 for j in 피벗], "   자유열 :", [j + 1 for j in 자유])
print("랭크 r :", r)
R (기약 사다리꼴)
[   1    2    0   -2 ]
[   0    0    1    2 ]
[   0    0    0    0 ]
피벗열 : [1, 3]    자유열 : [2, 4]
랭크 r : 2

서술 파트 1절에서 이 rr 이 세 가지를 동시에 센다고 했다. 하나씩 확인해 보자.

독립인행 = np.linalg.matrix_rank(A)          # = 행공간의 차원
독립인열 = np.linalg.matrix_rank(A.T)        # = 열공간의 차원
피벗변수 = len(피벗)

print("독립인 행의 개수 :", 독립인행)
print("독립인 열의 개수 :", 독립인열)
print("피벗 변수의 개수 :", 피벗변수)
print("셋이 모두 같은가 :", 독립인행 == 독립인열 == 피벗변수 == r)
print()
m = A.shape[0]
print(f"네 부분공간의 차원 : {r}, {n - r}, {r}, {m - r}")
독립인 행의 개수 : 2
독립인 열의 개수 : 2
피벗 변수의 개수 : 2
셋이 모두 같은가 : True

네 부분공간의 차원 : 2, 2, 2, 1

2. A=CRA = CR

CCAA 피벗 열, RR 은 기약 사다리꼴에서 0인 행을 잘라 낸 조각이다.

def 랭크분해(A):
    """A = C R 로 쪼갠다. C 는 열공간의 기저, R 은 행공간의 기저이다."""
    A = np.asarray(A, dtype=float)
    Rref, 피벗 = 기약사다리꼴(A)
    C = A[:, 피벗]                  # m x r : A 에서 그대로 떼어 온 피벗 열
    R = Rref[:len(피벗)]            # r x n : 0 인 행을 잘라 낸 것
    return C, R
C, R행 = 랭크분해(A)

print(show_matrix(C, "C  (m x r)"))
print(show_matrix(R행, "R  (r x n)"))
print("C R 이 A 와 같은가 :", np.allclose(C @ R행, A))
C  (m x r)
[  1   2 ]
[  2   6 ]
[  3   8 ]
R  (r x n)
[   1    2    0   -2 ]
[   0    0    1    2 ]
C R 이 A 와 같은가 : True

서술 파트에서 이 등식 하나가 네 가지를 말해 준다고 했다. 차례로 확인하자.

첫째, CC 의 열이 열공간의 기저이다. AA 의 열과 CC 의 열을 나란히 붙여도 랭크가 늘지 않으면 같은 공간이다.

print("rank(C)        :", np.linalg.matrix_rank(C))
print("rank([A | C])  :", np.linalg.matrix_rank(np.column_stack([A, C])),
      "  -> 같은 열공간")
rank(C)        : 2
rank([A | C])  : 2   -> 같은 열공간

둘째, RR 의 행이 행공간의 기저이다. 행끼리 붙여서 같은 검사를 한다.

print("rank(R)             :", np.linalg.matrix_rank(R행))
print("rank([A ; R] 세로) :", np.linalg.matrix_rank(np.vstack([A, R행])),
      "  -> 같은 행공간")
rank(R)             : 2
rank([A ; R] 세로) : 2   -> 같은 행공간

셋째, 행랭크와 열랭크가 같다. CC 의 열 개수와 RR 의 행 개수가 같은 수라는 것이 등식의 모양에서 바로 보인다.

print("C 의 열 개수 :", C.shape[1])
print("R 의 행 개수 :", R행.shape[0])
print("두 수가 같은가 :", C.shape[1] == R행.shape[0])
C 의 열 개수 : 2
R 의 행 개수 : 2
두 수가 같은가 : True

넷째, AA 가 랭크 1 행렬 rr 개의 합이다. CC 의 열과 RR 의 행을 짝지어 바깥곱을 만들고 더하면 된다.

벽돌 = [np.outer(C[:, k], R행[k]) for k in range(r)]

for k, 조각 in enumerate(벽돌, 1):
    print(show_matrix(조각, f"벽돌 {k}  (rank {np.linalg.matrix_rank(조각)})"))

print("벽돌 합이 A 와 같은가 :", np.allclose(sum(벽돌), A))
벽돌 1  (rank 1)
[   1    2    0   -2 ]
[   2    4    0   -4 ]
[   3    6    0   -6 ]
벽돌 2  (rank 1)
[   0    0    2    4 ]
[   0    0    6   12 ]
[   0    0    8   16 ]
벽돌 합이 A 와 같은가 : True

벽돌이 rr 개보다 적으면 AA 를 만들 수 없다. 랭크 1 행렬 kk 개의 합은 랭크가 kk 를 넘지 못하기 때문이다.

print("벽돌 1개만  :", np.linalg.matrix_rank(벽돌[0]))
print("벽돌 2개 합 :", np.linalg.matrix_rank(sum(벽돌)), "  (= r =", r, ")")
벽돌 1개만  : 1
벽돌 2개 합 : 2   (= r = 2 )

저장 공간

AA 를 그대로 적으면 mnmn 개, CCRR 로 나눠 적으면 r(m+n)r(m+n) 개이다. rr 이 작으면 차이가 커진다.

큰A = rng.integers(-4, 5, (200, 3)) @ rng.integers(-4, 5, (3, 200))
큰A = 큰A.astype(float)

큰m, 큰n = 큰A.shape
큰r = np.linalg.matrix_rank(큰A)
print(f"크기 {큰m} x {큰n},  랭크 {큰r}")
print(f"A 그대로     : {큰m * 큰n:,} 개")
print(f"C 와 R 로    : {큰r * (큰m + 큰n):,} 개")
print(f"줄어든 비율  : {큰m * 큰n / (큰r * (큰m + 큰n)):.1f} 배")
크기 200 x 200,  랭크 3
A 그대로     : 40,000 개
C 와 R 로    : 1,200 개
줄어든 비율  : 33.3 배

3. 영공간의 기저

L7에서 만든 N=[FI]N = \begin{bmatrix} -F \\ I \end{bmatrix} 를 함수로 굳혀 둔다. 피벗 행과 자유 행을 제자리에 꽂아 넣으면 열 교환을 따로 되돌릴 필요가 없다.

def 영공간행렬(A):
    """특수해를 열로 세운 n x (n-r) 행렬을 돌려준다."""
    A = np.asarray(A, dtype=float)
    n = A.shape[1]
    R, 피벗 = 기약사다리꼴(A)
    r = len(피벗)
    자유 = [j for j in range(n) if j not in 피벗]

    F = R[:r][:, 자유]                    # r x (n-r)
    N = np.zeros((n, n - r))
    N[피벗, :] = -F                        # 피벗 변수는 끌려온다
    N[자유, :] = np.eye(n - r)             # 자유 변수에 1 을 하나씩
    return N + 0.0                         # -0.0 을 0.0 으로 정리
N = 영공간행렬(A)
print(show_matrix(N, "N (열이 특수해)"))
print("A N 이 0 인가 :", np.allclose(A @ N, 0))
print("열의 개수 :", N.shape[1], "  (n - r =", n - r, ")")
N (열이 특수해)
[  -2    2 ]
[   1    0 ]
[   0   -2 ]
[   0    1 ]
A N 이 0 인가 : True
열의 개수 : 2   (n - r = 2 )

L7에서 손으로 구한 특수해와 비교해 보자. s1=(2,1,0,0)\vv{s}_1 = (-2, 1, 0, 0), s2=(2,0,2,1)\vv{s}_2 = (2, 0, -2, 1) 이었다.

s1 = np.array([-2.0, 1, 0, 0])
s2 = np.array([2.0, 0, -2, 1])

print("N 의 열 :", N[:, 0] + 0.0, "/", N[:, 1] + 0.0)
print("손으로  :", s1, "/", s2)
print("같은가 :", np.allclose(N[:, 0], s1) and np.allclose(N[:, 1], s2))
N 의 열 : [-2.  1.  0.  0.] / [ 2.  0. -2.  1.]
손으로  : [-2.  1.  0.  0.] / [ 2.  0. -2.  1.]
같은가 : True

scipynull_space 는 정규직교 기저를 주므로 벡터 자체는 다르다. 같은 공간인지는 랭크로 확인해야 한다.

scipy기저 = null_space(A)
print(show_matrix(scipy기저, "scipy 의 기저"))
print("붙여도 랭크가 그대로인가 :",
      np.linalg.matrix_rank(np.column_stack([N, scipy기저])) == N.shape[1])
scipy 의 기저
[    0.907     0.069 ]
[   -0.409     0.378 ]
[  -0.0893    -0.826 ]
[   0.0446     0.413 ]
붙여도 랭크가 그대로인가 : True

4. 행렬해부 조립

지금까지 만든 것을 하나로 묶는다. 이 강의의 산출물이다.

def 행렬해부(A, b=None):
    """행렬 하나를 L1~L13 의 도구로 끝까지 분해해 사전으로 돌려준다."""
    A = np.asarray(A, dtype=float)
    m, n = A.shape
    Rref, 피벗 = 기약사다리꼴(A)
    r = len(피벗)
    자유 = [j for j in range(n) if j not in 피벗]

    정보 = {
        "크기": (m, n), "랭크": r, "피벗열": 피벗, "자유열": 자유,
        "차원": (r, n - r, r, m - r),
        "열공간기저": A[:, 피벗],            # C — A 에서 가져온다
        "행공간기저": Rref[:r],              # R — 기약 사다리꼴에서
        "영공간기저": 영공간행렬(A),
        "좌영공간기저": null_space(A.T),
        "모든b에해": r == m,
        "해가유일": r == n,
    }

    if b is not None:
        b = np.asarray(b, dtype=float)
        Rb, 피벗b = 기약사다리꼴(np.column_stack([A, b]))
        if n in 피벗b:                       # 마지막 열이 피벗 -> 0 = 1 인 행
            정보["해"] = "없음"
        else:
            특수해 = np.zeros(n)
            for i, j in enumerate(피벗b):
                특수해[j] = Rb[i, n]
            정보["특수해"] = 특수해
            정보["해"] = "유일" if r == n else "무한히 많음"
    return 정보
def 해부출력(A, b=None, 이름="A"):
    """행렬해부 의 결과를 사람이 읽기 좋게 찍는다."""
    정보 = 행렬해부(A, b)
    m, n = 정보["크기"]
    r = 정보["랭크"]
    d행, d영, d열, d좌영 = 정보["차원"]

    print(f"[{이름}]  {m} x {n},  랭크 {r}")
    print(f"  피벗열 {[j + 1 for j in 정보['피벗열']]}"
          f"   자유열 {[j + 1 for j in 정보['자유열']]}")
    print(f"  차원   행공간 {d행} | 영공간 {d영} | 열공간 {d열} | 좌영공간 {d좌영}")
    print(f"  모든 b 에 해가 있는가 : {정보['모든b에해']}   (r={r}, m={m})")
    print(f"  해가 있다면 유일한가  : {정보['해가유일']}   (r={r}, n={n})")
    for 이름칸 in ("열공간기저", "행공간기저", "영공간기저", "좌영공간기저"):
        기저 = 정보[이름칸]
        개수 = 기저.shape[0] if 이름칸 == "행공간기저" else 기저.shape[1]
        if 개수 == 0:
            print(f"  {이름칸} : 없음 (차원 0)")
        else:
            print(show_matrix(기저, f"  {이름칸}  ({개수}개)"))
    if "해" in 정보:
        print(f"  Ax = b 의 해 : {정보['해']}")
        if "특수해" in 정보:
            print(f"    특수해 x_p = {정보['특수해'] + 0.0}")
            print(f"    완전해     = x_p + (영공간의 아무 원소)")
    return 정보
_ = 해부출력(A, b, "L7 의 행렬")
[L7 의 행렬]  3 x 4,  랭크 2
  피벗열 [1, 3]   자유열 [2, 4]
  차원   행공간 2 | 영공간 2 | 열공간 2 | 좌영공간 1
  모든 b 에 해가 있는가 : False   (r=2, m=3)
  해가 있다면 유일한가  : False   (r=2, n=4)
  열공간기저  (2개)
[  1   2 ]
[  2   6 ]
[  3   8 ]
  행공간기저  (2개)
[   1    2    0   -2 ]
[   0    0    1    2 ]
  영공간기저  (2개)
[  -2    2 ]
[   1    0 ]
[   0   -2 ]
[   0    1 ]
  좌영공간기저  (1개)
[  -0.577 ]
[  -0.577 ]
[   0.577 ]
  Ax = b 의 해 : 무한히 많음
    특수해 x_p = [-2.   0.   1.5  0. ]
    완전해     = x_p + (영공간의 아무 원소)

b\vv{b} 를 하나만 바꾸면 해가 사라진다. 서술 파트에서 열공간 밖으로 나간 경우이다.

_ = 해부출력(A, np.array([1.0, 5, 7]), "같은 A, b 만 바꿈")
[같은 A, b 만 바꿈]  3 x 4,  랭크 2
  피벗열 [1, 3]   자유열 [2, 4]
  차원   행공간 2 | 영공간 2 | 열공간 2 | 좌영공간 1
  모든 b 에 해가 있는가 : False   (r=2, m=3)
  해가 있다면 유일한가  : False   (r=2, n=4)
  열공간기저  (2개)
[  1   2 ]
[  2   6 ]
[  3   8 ]
  행공간기저  (2개)
[   1    2    0   -2 ]
[   0    0    1    2 ]
  영공간기저  (2개)
[  -2    2 ]
[   1    0 ]
[   0   -2 ]
[   0    1 ]
  좌영공간기저  (1개)
[  -0.577 ]
[  -0.577 ]
[   0.577 ]
  Ax = b 의 해 : 없음

5. 네 가지 경우

L8에서 쓴 네 예제를 그대로 넣어 본다. 판정이 서술 파트의 표와 맞아야 한다.

경우들 = {
    "r = m = n":    np.array([[1.0, 2], [3, 4]]),
    "r = n < m":    np.array([[1.0, 0], [0, 1], [1, 1]]),
    "r = m < n":    np.array([[1.0, 0, 1], [0, 1, 1]]),
    "r < m, r < n": np.array([[1.0, 2, 3], [2, 4, 6]]),
}

print(f"{'경우':>14}{'크기':>8}{'r':>4}{'차원 (행,영,열,좌영)':>22}{'해의 개수':>14}")
for 이름, M in 경우들.items():
    정보 = 행렬해부(M)
    m_, n_ = 정보["크기"]
    판정 = ("언제나 1개" if 정보["모든b에해"] and 정보["해가유일"] else
           "0개 또는 1개" if 정보["해가유일"] else
           "언제나 무한히" if 정보["모든b에해"] else "0개 또는 무한히")
    print(f"{이름:>14}{f'{m_}x{n_}':>8}{정보['랭크']:>4}"
          f"{str(정보['차원']):>22}{판정:>14}")
            경우      크기   r         차원 (행,영,열,좌영)         해의 개수
     r = m = n     2x2   2          (2, 0, 2, 0)        언제나 1개
     r = n < m     3x2   2          (2, 0, 2, 1)      0개 또는 1개
     r = m < n     2x3   2          (2, 1, 2, 0)       언제나 무한히
  r < m, r < n     2x3   1          (1, 2, 1, 1)     0개 또는 무한히

각 경우의 기약 사다리꼴 모양도 확인해 보자. r=nr = n 이면 FF 가 없고, r=mr = m 이면 0인 행이 없다.

for 이름, M in 경우들.items():
    Rref, 피벗 = 기약사다리꼴(M)
    m_, n_ = M.shape
    r_ = len(피벗)
    print(f"{이름} :  F 가 있는가 {r_ < n_},   0 인 행이 있는가 {r_ < m_}")
    print(show_matrix(Rref, "  R"))
r = m = n :  F 가 있는가 False,   0 인 행이 있는가 False
  R
[  1   0 ]
[  0   1 ]
r = n < m :  F 가 있는가 False,   0 인 행이 있는가 True
  R
[  1   0 ]
[  0   1 ]
[  0   0 ]
r = m < n :  F 가 있는가 True,   0 인 행이 있는가 False
  R
[  1   0   1 ]
[  0   1   1 ]
r < m, r < n :  F 가 있는가 True,   0 인 행이 있는가 True
  R
[  1   2   3 ]
[  0   0   0 ]

6. 자주 하는 실수를 코드로 확인

서술 파트 5절의 다섯 가지를 하나씩 반증해 보자.

(1) 열공간의 기저를 RR 에서 가져오면 안 된다.

A1 = np.array([[1.0, 2], [3, 6]])
R1, 피벗1 = 기약사다리꼴(A1)

print(show_matrix(A1, "A"))
print(show_matrix(R1, "R"))
print("A 의 피벗열 :", A1[:, 피벗1].ravel(), "  -> C(A) 의 방향")
print("R 의 피벗열 :", R1[:, 피벗1].ravel(), "  -> C(R) 의 방향")
print("두 공간이 같은가 :",
      np.linalg.matrix_rank(np.column_stack([A1[:, 피벗1], R1[:, 피벗1]])) == 1)
A
[  1   2 ]
[  3   6 ]
R
[  1   2 ]
[  0   0 ]
A 의 피벗열 : [1. 3.]   -> C(A) 의 방향
R 의 피벗열 : [1. 0.]   -> C(R) 의 방향
두 공간이 같은가 : False

(2) (AB)1=B1A1(AB)^{-1} = B^{-1}A^{-1} 이지 A1B1A^{-1}B^{-1} 이 아니다.

P = np.array([[1.0, 2], [3, 5]])
Q = np.array([[2.0, 1], [1, 1]])
정답 = np.linalg.inv(Q) @ np.linalg.inv(P)
오답 = np.linalg.inv(P) @ np.linalg.inv(Q)

print("inv(PQ) == inv(Q) inv(P) :", np.allclose(np.linalg.inv(P @ Q), 정답))
print("inv(PQ) == inv(P) inv(Q) :", np.allclose(np.linalg.inv(P @ Q), 오답))
inv(PQ) == inv(Q) inv(P) : True
inv(PQ) == inv(P) inv(Q) : False

(3) 특수해는 유일하지 않다. 자유 변수에 0을 넣는 것은 관례일 뿐이다.

정보 = 행렬해부(A, b)
xp = 정보["특수해"]
다른xp = xp + 2.0 * N[:, 0] - 1.0 * N[:, 1]      # 영공간을 타고 옮긴 것

print("xp      =", xp, "  A xp =", A @ xp)
print("다른 xp =", 다른xp, "  A xp =", A @ 다른xp)
print("둘 다 해인가 :", np.allclose(A @ xp, b) and np.allclose(A @ 다른xp, b))
print("차가 영공간에 있는가 :", np.allclose(A @ (다른xp - xp), 0))
xp      = [-2.   0.   1.5  0. ]   A xp = [1. 5. 6.]
다른 xp = [-8.   2.   3.5 -1. ]   A xp = [1. 5. 6.]
둘 다 해인가 : True
차가 영공간에 있는가 : True

(4) 랭크는 더해지지 않는다. 부등식이지 등식이 아니다.

단위 = np.eye(4)
print("가장 극단적인 예")
print("  rank(I)      :", np.linalg.matrix_rank(단위))
print("  rank(-I)     :", np.linalg.matrix_rank(-단위))
print("  rank(I + -I) :", np.linalg.matrix_rank(단위 - 단위), " (합이 0 행렬)")
print()

print("무작위로 여러 번")
print(f"{'rank P':>8}{'rank Q':>8}{'rank(P+Q)':>11}{'합 이하인가':>13}")
for _ in range(6):
    P_ = rng.integers(-3, 4, (4, 4)).astype(float)
    Q_ = rng.integers(-3, 4, (4, 4)).astype(float)
    rP, rQ = np.linalg.matrix_rank(P_), np.linalg.matrix_rank(Q_)
    rS = np.linalg.matrix_rank(P_ + Q_)
    print(f"{rP:>8}{rQ:>8}{rS:>11}{str(rS <= rP + rQ):>13}")
가장 극단적인 예
  rank(I)      : 4
  rank(-I)     : 4
  rank(I + -I) : 0  (합이 0 행렬)

무작위로 여러 번
  rank P  rank Q  rank(P+Q)       합 이하인가
       4       4          4         True
       4       4          4         True
       3       4          4         True
       4       3          4         True
       4       4          4         True
       4       4          4         True

(5) 해집합은 부분공간이 아니다. 두 해를 더하면 해가 아니다.

x1 = xp
x2 = xp + N[:, 0]

print("A x1     =", A @ x1)
print("A x2     =", A @ x2)
print("A(x1+x2) =", A @ (x1 + x2), "   b =", b)
print("x1 + x2 도 해인가 :", np.allclose(A @ (x1 + x2), b))
print("영벡터는 해인가   :", np.allclose(A @ np.zeros(4), b))
A x1     = [1. 5. 6.]
A x2     = [1. 5. 6.]
A(x1+x2) = [ 2. 10. 12.]    b = [1. 5. 6.]
x1 + x2 도 해인가 : False
영벡터는 해인가   : False

7. 무작위 행렬로 검산

랭크를 정해 놓고 행렬을 만들면 네 관계가 언제나 성립하는지 확인할 수 있다.

print(f"{'m x n':>8}{'심은 r':>8}{'잰 r':>7}{'C R = A':>10}"
      f"{'A N = 0':>10}{'차원 합':>9}{'랭크 대칭':>11}")
for _ in range(10):
    m_ = int(rng.integers(2, 7))
    n_ = int(rng.integers(2, 7))
    k = int(rng.integers(0, min(m_, n_) + 1))
    M = (rng.integers(-3, 4, (m_, k)) @ rng.integers(-3, 4, (k, n_))).astype(float)

    정보 = 행렬해부(M)
    r_ = 정보["랭크"]
    C_, R_ = 정보["열공간기저"], 정보["행공간기저"]
    N_ = 정보["영공간기저"]

    검사 = (
        np.allclose(C_ @ R_, M),
        np.allclose(M @ N_, 0),
        정보["차원"][0] + 정보["차원"][1] == n_ and 정보["차원"][2] + 정보["차원"][3] == m_,
        np.linalg.matrix_rank(M) == np.linalg.matrix_rank(M.T) == r_,
    )
    print(f"{f'{m_} x {n_}':>8}{k:>8}{r_:>7}"
          f"{str(검사[0]):>10}{str(검사[1]):>10}{str(검사[2]):>9}{str(검사[3]):>11}")
   m x n    심은 r    잰 r   C R = A   A N = 0     차원 합      랭크 대칭
   6 x 5       1      1      True      True     True       True
   5 x 5       4      4      True      True     True       True
   4 x 4       1      1      True      True     True       True
   4 x 3       2      2      True      True     True       True
   5 x 6       2      2      True      True     True       True
   4 x 6       2      2      True      True     True       True
   6 x 5       2      2      True      True     True       True
   4 x 2       0      0      True      True     True       True
   3 x 4       1      1      True      True     True       True
   4 x 2       1      1      True      True     True       True

심은 r잰 r 이 다를 때가 있다. 무작위 정수 두 조각을 곱하다 우연히 열이 겹치면 랭크가 심은 것보다 작아지기 때문이다. 어느 쪽이든 네 검사는 전부 통과한다.

마치며...

서술 파트의 내용이 노트북의 코드
피벗이 세 가지를 동시에 센다matrix_rank(A), matrix_rank(A.T), len(피벗)
A=CRA = CR랭크분해(A)
CC 가 열공간의 기저column_stack([A, C]) 의 랭크가 그대로
RR 이 행공간의 기저vstack([A, R]) 의 랭크가 그대로
행랭크 == 열랭크C.shape[1] == R.shape[0]
랭크 1 벽돌 rrnp.outer 의 합
영공간 행렬영공간행렬(A)
해부 절차행렬해부(A, b)

더 해 볼 것

  1. 행렬해부 에 좌영공간의 기저를 null_space(A.T) 대신 소거로 구하도록 고쳐 보자. [AI][A \mid I] 를 소거하면 0이 된 행에 대응하는 오른쪽 조각이 답이다.

  2. 랭크분해 로 얻은 CCRR 을 곱해 AA 를 복원할 때 소수점 오차가 얼마나 나는지 큰 행렬로 재 보자.

  3. 7절의 검사에 “CC 의 열이 독립인가”, “RR 의 행이 독립인가” 를 추가해 보자.

  4. 랭크 1 벽돌의 순서를 바꿔서 더해도 AA 가 나오는가? 벽돌 하나를 빼면 랭크가 얼마가 되는가?

다음 강의에서 내적이 등장한다. 두 벡터가 직각인지 물을 수 있게 되고, L10에서 그려 놓고 설명하지 못했던 직각의 이유를 확인하게 된다.