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 9. 독립, 기저, 차원 — 파이썬 실습

Independence, Basis, Dimension — 실습

L9 서술 파트에서 독립, 생성, 기저, 차원을 정의하고 차원이 유일하다는 것을 증명하였다. 이 노트북에서는 그 결과들을 코드로 확인해 보도록 하자.

서술 파트의 내용여기서 확인하는 방법
독립 \Leftrightarrow N(A)={0}N(A) = \{\vv{0}\}영공간의 차원으로 판정하는 함수를 만든다
Rm\R^m 안에서 mm 개보다 많으면 종속무작위 벡터의 개수를 늘려 가며 확인한다
기저는 여러 개, 개수는 하나같은 공간의 기저를 여러 가지로 만들어 본다
종속 \Leftrightarrow 상자가 납작해진다부피를 슬라이더로 0까지 줄여 본다
랭크 == dimC(A)\dim C(A)피벗 열이 기저인지 확인한다

0. 준비

import numpy as np
import plotly.graph_objects as go

from linalg_viz import COLORS, arrow, layout3d, slider_figure, show_matrix

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

1. 독립성 판정하기

서술 파트에서 독립성이 영공간의 문제라는 것을 보았다. 벡터들을 열로 세운 행렬의 영공간이 원점뿐이면 독립이다. 랭크로 말하면 랭크가 열의 개수와 같으면 독립이다.

from scipy.linalg import null_space


def 독립인가(*벡터들):
    """벡터들을 열로 세운 행렬의 영공간이 원점뿐인지 본다."""
    A = np.column_stack(벡터들)
    return null_space(A).shape[1] == 0
사례 = [
    ("독립이어야 함", [[2.0, 1], [-1.0, 2]]),
    ("평행 (종속)",   [[2.0, 1], [-4.0, -2]]),
    ("3차원 셋",      [[1.0, 0, 0], [0.0, 1, 0], [0.0, 0, 1]]),
    ("한 평면 위",    [[1.0, 2, 6], [2.0, 5, -3], [3.0, 7, 3]]),
]

for 이름, 벡터목록 in 사례:
    벡터들 = [np.array(v) for v in 벡터목록]
    A = np.column_stack(벡터들)
    N = null_space(A)
    판정 = "독립" if N.shape[1] == 0 else "종속"
    print(f"{이름:<14} rank={np.linalg.matrix_rank(A)}  "
          f"열 개수={A.shape[1]}  dim N={N.shape[1]}  ->  {판정}")
독립이어야 함        rank=2  열 개수=2  dim N=0  ->  독립
평행 (종속)        rank=1  열 개수=2  dim N=1  ->  종속
3차원 셋          rank=3  열 개수=3  dim N=0  ->  독립
한 평면 위         rank=2  열 개수=3  dim N=1  ->  종속

마지막 예제는 L1에서 다룬 특이행렬의 세 열이다. 종속이라면 어떤 결합이 영벡터가 되는지도 영공간에서 읽을 수 있다.

A_dep = np.column_stack([np.array([1.0, 2, 6]), np.array([2.0, 5, -3]),
                         np.array([3.0, 7, 3])])
c = null_space(A_dep)[:, 0]
c = c / c[0]                      # 첫 계수를 1로 맞춘다

print("계수 c =", np.round(c, 10) + 0.0)
print("c1 v1 + c2 v2 + c3 v3 =", np.round(A_dep @ c, 10) + 0.0)
print()
print("즉  v1 + v2 - v3 = 0,  다시 말해  v3 = v1 + v2")
계수 c = [ 1.  1. -1.]
c1 v1 + c2 v2 + c3 v3 = [0. 0. 0.]

즉  v1 + v2 - v3 = 0,  다시 말해  v3 = v1 + v2

2. 개수가 많으면 반드시 종속이다

R3\R^3 안에서 벡터를 무작위로 뽑아 개수를 늘려 보자. 넷 이상이 되면 아무리 뽑아도 종속이다.

print(f"{'개수':>4}{'rank':>7}{'dim N':>8}   판정")
for k in (1, 2, 3, 4, 5, 6):
    V = rng.standard_normal((3, k))
    r = np.linalg.matrix_rank(V)
    dimN = null_space(V).shape[1]
    print(f"{k:>4}{r:>7}{dimN:>8}   {'독립' if dimN == 0 else '종속'}")
  개수   rank   dim N   판정
   1      1       0   독립
   2      2       0   독립
   3      3       0   독립
   4      3       1   종속
   5      3       2   종속
   6      3       3   종속

k>3k > 3 이면 랭크가 많아야 3이므로 자유 변수가 반드시 생긴다. 무작위로 몇 번을 뽑아도 결과는 같다.

횟수 = 200
독립이었던_경우 = sum(np.linalg.matrix_rank(rng.standard_normal((3, 4))) == 4
                  for _ in range(횟수))
print(f"R^3 안의 벡터 4개를 {횟수}번 뽑았을 때 독립이었던 횟수 : {독립이었던_경우}")
R^3 안의 벡터 4개를 200번 뽑았을 때 독립이었던 횟수 : 0

3. 기저는 여러 개, 개수는 하나

R3\R^3 의 기저를 무작위로 만들어 보자. 정방행렬이 가역이면 그 열들이 R3\R^3 의 기저이다.

for k in range(5):
    B = rng.standard_normal((3, 3))
    print(f"기저 {k + 1} :  det = {np.linalg.det(B):+.3f},"
          f"  rank = {np.linalg.matrix_rank(B)},  벡터 개수 = {B.shape[1]}")
기저 1 :  det = +0.498,  rank = 3,  벡터 개수 = 3
기저 2 :  det = -0.807,  rank = 3,  벡터 개수 = 3
기저 3 :  det = +2.840,  rank = 3,  벡터 개수 = 3
기저 4 :  det = -1.055,  rank = 3,  벡터 개수 = 3
기저 5 :  det = -0.325,  rank = 3,  벡터 개수 = 3

서로 완전히 다른 기저들인데 개수는 모두 3이다. 부분공간에 대해서도 확인해 보자. 랭크 2인 행렬의 열공간은 평면이므로 기저가 두 개짜리여야 한다.

A = np.array([[1, 2, 2,  2],
              [2, 4, 6,  8],
              [3, 6, 8, 10]], dtype=float)     # L7, L8 에서 쓴 행렬

r = np.linalg.matrix_rank(A)
print("rank(A) =", r)
print()

from scipy.linalg import orth

기저들 = {
    "피벗 열 (1, 3열)": A[:, [0, 2]],
    "다른 두 열 (2, 4열)": A[:, [1, 3]],
    "scipy.linalg.orth": orth(A),
}

for 이름, B in 기저들.items():
    print(f"{이름:<22} 크기 {B.shape}  rank {np.linalg.matrix_rank(B)}")
rank(A) = 2

피벗 열 (1, 3열)           크기 (3, 2)  rank 2
다른 두 열 (2, 4열)         크기 (3, 2)  rank 2
scipy.linalg.orth      크기 (3, 2)  rank 2

세 가지 모두 벡터가 두 개이고 랭크가 2이다. 정말 같은 공간을 만드는지는 붙여서 랭크를 재면 확인된다.

이름들 = list(기저들)
for i in range(len(이름들)):
    for j in range(i + 1, len(이름들)):
        붙임 = np.column_stack([기저들[이름들[i]], 기저들[이름들[j]]])
        print(f"{이름들[i]:<22} 와 {이름들[j]:<22} -> 붙인 랭크 "
              f"{np.linalg.matrix_rank(붙임)}  (같은 공간: "
              f"{np.linalg.matrix_rank(붙임) == r})")
피벗 열 (1, 3열)           와 다른 두 열 (2, 4열)         -> 붙인 랭크 2  (같은 공간: True)
피벗 열 (1, 3열)           와 scipy.linalg.orth      -> 붙인 랭크 2  (같은 공간: True)
다른 두 열 (2, 4열)         와 scipy.linalg.orth      -> 붙인 랭크 2  (같은 공간: True)

4. 상자가 납작해지는 순간

v1=(1,0,0)\vv{v}_1 = (1,0,0), v2=(0,1,0)\vv{v}_2 = (0,1,0) 을 고정하고 v3=(1,1,t)\vv{v}_3 = (1, 1, t)tt 를 움직여 보자. t=0t = 0 이면 세 벡터가 모두 xyxy평면에 눕는다.

CUBE = np.array([[i, j, k] for i in (0, 1) for j in (0, 1) for k in (0, 1)], float)
EDGES = [(a, b) for a in range(8) for b in range(a + 1, 8)
         if np.sum(np.abs(CUBE[a] - CUBE[b])) == 1]


def box_trace(v1, v2, v3, color):
    """세 벡터가 만드는 평행육면체의 모서리를 선분 하나로 묶는다."""
    P = CUBE @ np.array([v1, v2, v3])
    xs, ys, zs = [], [], []
    for a, b in EDGES:
        xs += [P[a, 0], P[b, 0], None]
        ys += [P[a, 1], P[b, 1], None]
        zs += [P[a, 2], P[b, 2], None]
    return go.Scatter3d(x=xs, y=ys, z=zs, mode="lines",
                        line=dict(color=color, width=4), name="평행육면체")
v1 = np.array([1.0, 0.0, 0.0])
v2 = np.array([0.0, 1.0, 0.0])

후보 = np.linspace(1.0, -0.6, 17)          # 가운데(11번째)가 t = 0
frames_data, labels = [], []

for t in 후보:
    v3 = np.array([1.0, 1.0, t])
    부피 = abs(np.linalg.det(np.column_stack([v1, v2, v3])))
    색 = COLORS["output"] if 부피 > 1e-9 else COLORS["error"]

    traces = [box_trace(v1, v2, v3, 색)]
    traces += arrow([0, 0, 0], v1, color=COLORS["input"], name="v1")
    traces += arrow([0, 0, 0], v2, color=COLORS["second"], name="v2")
    traces += arrow([0, 0, 0], v3, color=COLORS["third"], name="v3")

    frames_data.append(traces)
    labels.append(f"{t:.1f}")

slider_figure(frames_data, labels,
              layout3d("t 를 내리면 상자가 납작해진다", extent=2.2),
              prefix="t = ", initial=0)
Loading...

부피와 판정을 숫자로도 확인해 보자.

print(f"{'t':>6}{'부피':>10}   판정")
for t in (1.0, 0.5, 0.1, 0.0, -0.3):
    v3 = np.array([1.0, 1.0, t])
    부피 = abs(np.linalg.det(np.column_stack([v1, v2, v3])))
    print(f"{t:>6.1f}{부피:>10.3f}   {'독립' if 부피 > 1e-9 else '종속'}")
     t        부피   판정
   1.0     1.000   독립
   0.5     0.500   독립
   0.1     0.100   독립
   0.0     0.000   종속
  -0.3     0.300   독립

부피가 정확히 t|t| 이다. t=0t = 0 일 때 v3=v1+v2\vv{v}_3 = \vv{v}_1 + \vv{v}_2 가 되어 종속이 된다. 납작한 정도를 재는 이 숫자는 후반부에서 다시 다룬다.

5. 피벗 열이 열공간의 기저이다

서술 파트에서 자유 열이 피벗 열들의 결합으로 쓰인다는 것을 보였다. 실제로 그 계수를 구해 보자.

import sympy as sp

_, 피벗 = sp.Matrix(A.astype(int)).rref()
피벗열 = list(피벗)
자유열 = [j for j in range(A.shape[1]) if j not in 피벗열]

print("피벗 열 :", 피벗열, "   자유 열 :", 자유열)

B = A[:, 피벗열]
print("피벗 열들의 랭크 :", np.linalg.matrix_rank(B), " (독립이다)")
피벗 열 : [0, 2]    자유 열 : [1, 3]
피벗 열들의 랭크 : 2  (독립이다)
for j in 자유열:
    계수, *_ = np.linalg.lstsq(B, A[:, j], rcond=None)
    오차 = np.linalg.norm(B @ 계수 - A[:, j])
    # 반올림 찌꺼기(1e-15 수준)는 빼고 보여 준다
    항 = "  +  ".join(f"{c:g} (col {피벗열[k] + 1})"
                     for k, c in enumerate(np.round(계수, 10))
                     if abs(c) > 1e-9)
    print(f"col {j + 1}  =  {항}     (오차 {오차:.1e})")
col 2  =  2 (col 1)     (오차 2.2e-15)
col 4  =  -2 (col 1)  +  2 (col 3)     (오차 8.6e-15)

서술 파트의 그림에 적힌 관계와 같다. 자유 열이 피벗 열의 결합이므로 열공간을 넓히지 못한다.

그러면 피벗 열만으로 만든 공간과 전체 열공간이 같아야 한다.

print("rank(A)          =", np.linalg.matrix_rank(A))
print("rank(피벗 열들)  =", np.linalg.matrix_rank(B))
print("둘을 붙인 랭크   =", np.linalg.matrix_rank(np.column_stack([A, B])))
print("같은 공간인가    :",
      np.linalg.matrix_rank(np.column_stack([A, B])) == np.linalg.matrix_rank(A))
rank(A)          = 2
rank(피벗 열들)  = 2
둘을 붙인 랭크   = 2
같은 공간인가    : True

6. dimC(A)+dimN(A)=n\dim C(A) + \dim N(A) = n

여러 크기의 행렬로 확인해 보자.

예제들 = {
    "L7 의 A (3x4)": A,
    "3x3 rank 2": np.array([[1., 2, 3], [2, 5, 7], [6, -3, 3]]),
    "2x3 rank 1": np.array([[1., 2, 3], [2, 4, 6]]),
    "4x2 rank 2": np.array([[1., 0], [0, 1], [1, 1], [2, -1]]),
    "무작위 5x3": rng.standard_normal((5, 3)),
}

print(f"{'행렬':<16}{'m':>3}{'n':>3}{'r = dim C':>11}{'dim N':>8}{'r + dim N':>11}")
for 이름, M in 예제들.items():
    m, n = M.shape
    r_i = np.linalg.matrix_rank(M)
    dimN = null_space(M).shape[1]
    print(f"{이름:<16}{m:>3}{n:>3}{r_i:>11}{dimN:>8}{r_i + dimN:>11}")
행렬                m  n  r = dim C   dim N  r + dim N
L7 의 A (3x4)      3  4          2       2          4
3x3 rank 2        3  3          2       1          3
2x3 rank 1        2  3          1       2          3
4x2 rank 2        4  2          2       0          2
무작위 5x3           5  3          3       0          3

마지막 열이 언제나 nn 과 같다. 열의 개수가 랭크와 영공간 차원으로 나뉘는 셈이다.

마치며...

서술 파트의 내용이 노트북의 코드
독립 \Leftrightarrow N(A)={0}N(A) = \{\vv{0}\}null_space(A).shape[1] == 0
종속이면 결합 계수를 읽을 수 있다null_space(A)[:, 0]
mm 개보다 많으면 종속2절의 1000번 시험
기저는 여러 개, 개수는 하나3절의 세 가지 기저
종속 \Leftrightarrow 부피가 04절의 슬라이더
자유 열은 피벗 열의 결합lstsq(B, A[:, j])
r+dimN=nr + \dim N = n6절의 표

더 해 볼 것

  1. 독립인가null_space 대신 matrix_rank 로 판정하도록 바꿔 보자. 두 방법의 결과가 다른 경우를 만들 수 있는가? 어떤 행렬에서 그런 일이 생기는가?

  2. 3절에서 2열과 4열도 기저였다. 1열과 2열은 어떤가? 왜 그런지 설명해 보자.

  3. 4절의 v3\vv{v}_3(1,1,t)(1, 1, t) 대신 (a,b,t)(a, b, t) 로 두고 부피를 구해 보자. 부피가 aabb 에 어떻게 의존하는가?

  4. 무작위 m×nm \times n 행렬의 랭크는 거의 언제나 min(m,n)\min(m, n) 이다. 랭크가 그보다 작은 행렬을 무작위로 뽑으려면 어떻게 만들어야 하는가?

다음 강의에서는 AAATA^{\mathsf{T}} 에서 나오는 네 개의 부분공간을 함께 다룬다.