L9 서술 파트에서 독립, 생성, 기저, 차원을 정의하고 차원이 유일하다는 것을 증명하였다. 이 노트북에서는 그 결과들을 코드로 확인해 보도록 하자.
| 서술 파트의 내용 | 여기서 확인하는 방법 |
|---|---|
| 독립 | 영공간의 차원으로 판정하는 함수를 만든다 |
| 안에서 개보다 많으면 종속 | 무작위 벡터의 개수를 늘려 가며 확인한다 |
| 기저는 여러 개, 개수는 하나 | 같은 공간의 기저를 여러 가지로 만들어 본다 |
| 종속 상자가 납작해진다 | 부피를 슬라이더로 0까지 줄여 본다 |
| 랭크 | 피벗 열이 기저인지 확인한다 |
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. 개수가 많으면 반드시 종속이다¶
안에서 벡터를 무작위로 뽑아 개수를 늘려 보자. 넷 이상이 되면 아무리 뽑아도 종속이다.
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 종속
이면 랭크가 많아야 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. 기저는 여러 개, 개수는 하나¶
의 기저를 무작위로 만들어 보자. 정방행렬이 가역이면 그 열들이 의 기저이다.
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. 상자가 납작해지는 순간¶
, 을 고정하고 의 를 움직여 보자. 이면 세 벡터가 모두 평면에 눕는다.
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)부피와 판정을 숫자로도 확인해 보자.
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 독립
부피가 정확히 이다. 일 때 가 되어 종속이 된다. 납작한 정도를 재는 이 숫자는 후반부에서 다시 다룬다.
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. ¶
여러 크기의 행렬로 확인해 보자.
예제들 = {
"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
마지막 열이 언제나 과 같다. 열의 개수가 랭크와 영공간 차원으로 나뉘는 셈이다.
마치며...¶
| 서술 파트의 내용 | 이 노트북의 코드 |
|---|---|
| 독립 | null_space(A).shape[1] == 0 |
| 종속이면 결합 계수를 읽을 수 있다 | null_space(A)[:, 0] |
| 개보다 많으면 종속 | 2절의 1000번 시험 |
| 기저는 여러 개, 개수는 하나 | 3절의 세 가지 기저 |
| 종속 부피가 0 | 4절의 슬라이더 |
| 자유 열은 피벗 열의 결합 | lstsq(B, A[:, j]) |
| 6절의 표 |
더 해 볼 것¶
독립인가를null_space대신matrix_rank로 판정하도록 바꿔 보자. 두 방법의 결과가 다른 경우를 만들 수 있는가? 어떤 행렬에서 그런 일이 생기는가?3절에서 2열과 4열도 기저였다. 1열과 2열은 어떤가? 왜 그런지 설명해 보자.
4절의 를 대신 로 두고 부피를 구해 보자. 부피가 와 에 어떻게 의존하는가?
무작위 행렬의 랭크는 거의 언제나 이다. 랭크가 그보다 작은 행렬을 무작위로 뽑으려면 어떻게 만들어야 하는가?
다음 강의에서는 와 에서 나오는 네 개의 부분공간을 함께 다룬다.