L10 서술 파트에서 네 부분공간과 차원의 기본정리를 다루었다. 이 노트북에서는 네 기저를 모두 구해 보고, 가 행공간을 열공간으로 옮기고 영공간을 0으로 뭉갠다는 것을 분해로 확인해 보도록 하자.
| 서술 파트의 내용 | 여기서 확인하는 방법 |
|---|---|
| 네 부분공간의 기저 | 네 가지를 모두 구해 차원을 센다 |
| 차원의 기본정리 | , 을 확인한다 |
| 행랭크 열랭크 | 무작위 행렬로 여러 번 시험한다 |
| 열공간 기저를 에서 가져오면 안 된다 | 잘못 가져왔을 때 무엇이 깨지는지 본다 |
| 는 행공간을 옮기고 영공간을 뭉갠다 | 를 두 조각으로 분해한다 |
0. 준비¶
import numpy as np
import sympy as sp
import plotly.graph_objects as go
from scipy.linalg import null_space
from linalg_viz import COLORS, plane, spin_figure, show_matrix
np.set_printoptions(precision=3, suppress=True)
rng = np.random.default_rng(0)
print("numpy", np.__version__, " sympy", sp.__version__)numpy 2.5.2 sympy 1.14.0
A = np.array([[1, 2, 2, 2],
[2, 4, 6, 8],
[3, 6, 8, 10]], dtype=float)
m, n = A.shape
r = np.linalg.matrix_rank(A)
print(show_matrix(A, "A ="))
print(f"m = {m}, n = {n}, r = {r}")A =
[ 1 2 2 2 ]
[ 2 4 6 8 ]
[ 3 6 8 10 ]
m = 3, n = 4, r = 2
1. 네 부분공간의 기저 구하기¶
각 공간마다 구하는 방법이 다르다. 서술 파트의 표를 그대로 코드로 옮긴다.
R_sym, 피벗 = sp.Matrix(A.astype(int)).rref()
피벗열 = list(피벗)
R = np.array(R_sym.tolist(), dtype=float)
print(show_matrix(R, "R (RREF) ="))
print("피벗 열 :", 피벗열)R (RREF) =
[ 1 2 0 -2 ]
[ 0 0 1 2 ]
[ 0 0 0 0 ]
피벗 열 : [0, 2]
행공간기저 = R[:r].T # R 의 0이 아닌 행을 열로 세운다
열공간기저 = A[:, 피벗열] # 원래 A 의 피벗 열
영공간기저 = null_space(A)
좌영공간기저 = null_space(A.T)
for 이름, B, 사는곳 in [
("행공간 C(A^T)", 행공간기저, f"R^{n}"),
("영공간 N(A)", 영공간기저, f"R^{n}"),
("열공간 C(A)", 열공간기저, f"R^{m}"),
("좌영공간 N(A^T)", 좌영공간기저, f"R^{m}"),
]:
print(f"{이름:<16} {사는곳:>4} 안, 기저 {B.shape[1]}개, 각 벡터 길이 {B.shape[0]}")행공간 C(A^T) R^4 안, 기저 2개, 각 벡터 길이 4
영공간 N(A) R^4 안, 기저 2개, 각 벡터 길이 4
열공간 C(A) R^3 안, 기저 2개, 각 벡터 길이 3
좌영공간 N(A^T) R^3 안, 기저 1개, 각 벡터 길이 3
print(show_matrix(행공간기저, "행공간 기저 (열마다 하나)"))
print(show_matrix(열공간기저, "열공간 기저"))
print(show_matrix(영공간기저, "영공간 기저"))
print(show_matrix(좌영공간기저, "좌영공간 기저"))행공간 기저 (열마다 하나)
[ 1 0 ]
[ 2 0 ]
[ 0 1 ]
[ -2 2 ]
열공간 기저
[ 1 2 ]
[ 2 6 ]
[ 3 8 ]
영공간 기저
[ 0.907 0.069 ]
[ -0.409 0.378 ]
[ -0.0893 -0.826 ]
[ 0.0446 0.413 ]
좌영공간 기저
[ -0.577 ]
[ -0.577 ]
[ 0.577 ]
좌영공간 기저를 첫 성분이 -1 이 되도록 맞추면 L8에서 손으로 구한 이 된다.
y = 좌영공간기저[:, 0]
y = y / y[0] * -1
print("y =", np.round(y, 10) + 0.0)
print("y^T A =", np.round(y @ A, 10) + 0.0, " (영벡터)")y = [-1. -1. 1.]
y^T A = [0. 0. 0. 0.] (영벡터)
2. 차원의 기본정리¶
dim_행 = 행공간기저.shape[1]
dim_영 = 영공간기저.shape[1]
dim_열 = 열공간기저.shape[1]
dim_좌영 = 좌영공간기저.shape[1]
print(f"정의역 쪽 : dim C(A^T) + dim N(A) = {dim_행} + {dim_영} = {dim_행 + dim_영}"
f" (n = {n})")
print(f"공역 쪽 : dim C(A) + dim N(A^T) = {dim_열} + {dim_좌영} = {dim_열 + dim_좌영}"
f" (m = {m})")
print()
print("네 차원 :", dim_행, dim_영, dim_열, dim_좌영, " (r, n-r, r, m-r 과 같아야 한다)")
print("예상값 :", r, n - r, r, m - r)정의역 쪽 : dim C(A^T) + dim N(A) = 2 + 2 = 4 (n = 4)
공역 쪽 : dim C(A) + dim N(A^T) = 2 + 1 = 3 (m = 3)
네 차원 : 2 2 2 1 (r, n-r, r, m-r 과 같아야 한다)
예상값 : 2 2 2 1
여러 크기의 행렬로도 확인해 보자. 랭크를 원하는 값으로 만들려면 얇은 두 행렬을 곱하면 된다. 가 이고 가 이면 의 랭크는 보통 이다.
print(f"{'m':>3}{'n':>4}{'r':>4}{'dimC^T':>9}{'dimN':>7}{'dimC':>7}{'dimN^T':>9}"
f"{'합 = n':>9}{'합 = m':>9}")
for m_i, n_i, k in [(3, 4, 2), (5, 3, 2), (2, 6, 2), (4, 4, 4), (6, 2, 1)]:
M = rng.standard_normal((m_i, k)) @ rng.standard_normal((k, n_i))
r_i = np.linalg.matrix_rank(M)
a, b = null_space(M).shape[1], null_space(M.T).shape[1]
print(f"{m_i:>3}{n_i:>4}{r_i:>4}{r_i:>9}{a:>7}{r_i:>7}{b:>9}"
f"{r_i + a:>9}{r_i + b:>9}") m n r dimC^T dimN dimC dimN^T 합 = n 합 = m
3 4 2 2 2 2 1 4 3
5 3 2 2 1 2 3 3 5
2 6 2 2 4 2 0 6 2
4 4 4 4 0 4 0 4 4
6 2 1 1 1 1 5 2 6
3. 행랭크와 열랭크는 같다¶
서술 파트에서 증명한 것이다. 자명하지 않은 사실이므로 여러 행렬로 확인해 보자.
다른경우 = 0
for _ in range(300):
m_i, n_i = rng.integers(1, 7, size=2)
k = rng.integers(1, min(m_i, n_i) + 1)
M = rng.standard_normal((m_i, k)) @ rng.standard_normal((k, n_i))
if np.linalg.matrix_rank(M) != np.linalg.matrix_rank(M.T):
다른경우 += 1
print(f"무작위 행렬 300개 중 rank(A) != rank(A^T) 인 경우 : {다른경우}")무작위 행렬 300개 중 rank(A) != rank(A^T) 인 경우 : 0
소거로도 확인할 수 있다. 피벗 열의 개수와 의 0이 아닌 행의 개수가 같아야 한다.
피벗개수 = len(피벗열)
영아닌행수 = int(np.sum(np.abs(R).sum(axis=1) > 1e-10))
print(f"피벗 열의 개수 : {피벗개수}")
print(f"R 의 0이 아닌 행 개수 : {영아닌행수}")
print(f"같은가 : {피벗개수 == 영아닌행수}")피벗 열의 개수 : 2
R 의 0이 아닌 행 개수 : 2
같은가 : True
4. 열공간의 기저를 에서 가져오면 어떻게 되는가¶
서술 파트에서 경고한 함정이다. 작은 예로 먼저 보자.
A2 = np.array([[1.0, 2], [3.0, 6]])
R2_sym, 피벗2 = sp.Matrix(A2.astype(int)).rref()
R2 = np.array(R2_sym.tolist(), dtype=float)
print(show_matrix(A2, "A ="))
print(show_matrix(R2, "R ="))
print()
print("C(A) 의 기저 (A 의 1열) :", A2[:, 0])
print("C(R) 의 기저 (R 의 1열) :", R2[:, 0])A =
[ 1 2 ]
[ 3 6 ]
R =
[ 1 2 ]
[ 0 0 ]
C(A) 의 기저 (A 의 1열) : [1. 3.]
C(R) 의 기저 (R 의 1열) : [1. 0.]
b = np.array([2.0, 6.0]) # A 의 2열, 당연히 C(A) 안에 있다
def 열공간에_있는가(B, v):
return np.linalg.matrix_rank(B) == np.linalg.matrix_rank(np.column_stack([B, v]))
print("b =", b)
print(" C(A) 안에 있는가 :", 열공간에_있는가(A2, b))
print(" C(R) 안에 있는가 :", 열공간에_있는가(R2, b), " <- 잘못된 기저로는 만들 수 없다")b = [2. 6.]
C(A) 안에 있는가 : True
C(R) 안에 있는가 : False <- 잘못된 기저로는 만들 수 없다
큰 예제에서도 확인해 보자. 의 피벗 열을 열공간의 기저로 잘못 쓰면 원래 열들을 만들지 못한다.
올바른기저 = A[:, 피벗열]
잘못된기저 = R[:, 피벗열]
for 이름, B in [("A 의 피벗 열 (올바름)", 올바른기저), ("R 의 피벗 열 (잘못됨)", 잘못된기저)]:
붙임 = np.column_stack([B, A])
같은공간 = np.linalg.matrix_rank(붙임) == np.linalg.matrix_rank(B)
print(f"{이름:<22} rank {np.linalg.matrix_rank(B)},"
f" A 의 모든 열을 만들 수 있는가 : {같은공간}")A 의 피벗 열 (올바름) rank 2, A 의 모든 열을 만들 수 있는가 : True
R 의 피벗 열 (잘못됨) rank 2, A 의 모든 열을 만들 수 있는가 : False
피벗이 어느 열에 있는지는 에서 읽어도 되지만, 벡터 자체는 에서 가져와야 한다. 열들 사이의 종속 관계가 보존되는 것은 영공간이 같기 때문이다.
print("N(A) 와 N(R) 이 같은 공간인가 :",
np.linalg.matrix_rank(np.column_stack([null_space(A), null_space(R)]))
== null_space(A).shape[1])N(A) 와 N(R) 이 같은 공간인가 : True
5. 좌영공간을 소거에서 읽기¶
를 소거하면 가 되고 이다. 의 행이 0인 자리의 의 행이 좌영공간에 들어간다.
확장 = np.hstack([A, np.eye(m)]).astype(int)
RE_sym, _ = sp.Matrix(확장).rref()
RE = np.array(RE_sym.tolist(), dtype=float)
R_부분, E_부분 = RE[:, :n], RE[:, n:]
print(show_matrix(R_부분, "왼쪽 블록 (= R)"))
print(show_matrix(E_부분, "오른쪽 블록 (= E)"))
print()
print("E A 가 R 과 같은가 :", np.allclose(E_부분 @ A, R_부분))왼쪽 블록 (= R)
[ 1 2 0 -2 ]
[ 0 0 1 2 ]
[ 0 0 0 0 ]
오른쪽 블록 (= E)
[ 0 -4 3 ]
[ 0 1.5 -1 ]
[ 1 1 -1 ]
E A 가 R 과 같은가 : True
영인행 = [i for i in range(m) if np.allclose(R_부분[i], 0)]
print("R 에서 0이 된 행 :", 영인행)
for i in 영인행:
행 = E_부분[i]
print(f" E 의 {i + 1}행 = {행} -> (그 행) A = {np.round(행 @ A, 10) + 0.0}")R 에서 0이 된 행 : [2]
E 의 3행 = [ 1. 1. -1.] -> (그 행) A = [0. 0. 0. 0.]
null_space(A.T) 가 준 기저와 같은 공간을 만드는지 확인해 보자.
E기저 = E_부분[영인행].T
붙임 = np.column_stack([E기저, 좌영공간기저])
print("같은 공간인가 :",
np.linalg.matrix_rank(붙임) == 좌영공간기저.shape[1])같은 공간인가 : True
6. 는 행공간을 옮기고 영공간을 뭉갠다¶
서술 파트의 결론이다. 정의역의 아무 벡터나 잡아 행공간 조각과 영공간 조각으로 나눠 보자.
행공간 기저와 영공간 기저를 나란히 붙이면 전체의 기저가 된다. 차원의 합이 이고 두 공간이 원점만 공유하기 때문이다. 그러므로 어떤 든 두 조각의 합으로 쓸 수 있다.
전체기저 = np.column_stack([행공간기저, 영공간기저])
print("붙인 행렬의 크기 :", 전체기저.shape)
print("랭크 :", np.linalg.matrix_rank(전체기저), f" (n = {n} 이면 R^{n} 전체의 기저)")붙인 행렬의 크기 : (4, 4)
랭크 : 4 (n = 4 이면 R^4 전체의 기저)
x = rng.standard_normal(n)
계수, *_ = np.linalg.lstsq(전체기저, x, rcond=None)
x_행 = 행공간기저 @ 계수[:r] # 행공간 조각
x_영 = 영공간기저 @ 계수[r:] # 영공간 조각
print("x =", x)
print("x_행 =", x_행)
print("x_영 =", x_영)
print()
print("x_행 + x_영 = x 인가 :", np.allclose(x_행 + x_영, x))
print("A x_영 = 0 인가 :", np.allclose(A @ x_영, 0))
print("A x_행 = A x 인가 :", np.allclose(A @ x_행, A @ x))x = [-0.736 1.205 0.554 -0.907]
x_행 = [ 0.428 0.855 0.09 -0.675]
x_영 = [-1.164 0.35 0.464 -0.232]
x_행 + x_영 = x 인가 : True
A x_영 = 0 인가 : True
A x_행 = A x 인가 : True
영공간 조각은 통째로 사라지고, 행공간 조각만 를 통과한다. 서술 파트에서 말한 "행공간을 열공간으로 옮기고 영공간을 0으로 뭉갠다"가 이것이다.
행공간의 서로 다른 두 벡터가 서로 다른 곳으로 가는지도 확인해 보자.
for _ in range(3):
c1, c2 = rng.standard_normal(r), rng.standard_normal(r)
u, v = 행공간기저 @ c1, 행공간기저 @ c2
print(f"두 입력이 다른가 : {not np.allclose(u, v)},"
f" 두 출력이 다른가 : {not np.allclose(A @ u, A @ v)},"
f" 거리 {np.linalg.norm(A @ u - A @ v):.3f}")두 입력이 다른가 : True, 두 출력이 다른가 : True, 거리 139.275
두 입력이 다른가 : True, 두 출력이 다른가 : True, 거리 57.332
두 입력이 다른가 : True, 두 출력이 다른가 : True, 거리 85.216
반대로 영공간 방향으로만 다른 두 벡터는 같은 곳으로 간다.
x2 = x + 영공간기저 @ rng.standard_normal(영공간기저.shape[1])
print("x 와 x2 가 다른가 :", not np.allclose(x, x2))
print("A x 와 A x2 가 같은가 :", np.allclose(A @ x, A @ x2))x 와 x2 가 다른가 : True
A x 와 A x2 가 같은가 : True
7. 네 부분공간을 그려 보기¶
정사각 행렬을 쓰면 네 공간이 모두 안에 있어 그릴 수 있다. L1에서 다룬 특이행렬을 쓴다.
A3 = np.array([[1.0, 2, 3],
[2.0, 5, 7],
[6.0, -3, 3]]) # rank 2
r3 = np.linalg.matrix_rank(A3)
행3 = null_space(A3.T).shape[1]
print("rank =", r3)
print(f"정의역 R^3 : 행공간 {r3}차원 + 영공간 {null_space(A3).shape[1]}차원")
print(f"공역 R^3 : 열공간 {r3}차원 + 좌영공간 {행3}차원")rank = 2
정의역 R^3 : 행공간 2차원 + 영공간 1차원
공역 R^3 : 열공간 2차원 + 좌영공간 1차원
정의역 쪽 — 행공간과 영공간¶
from scipy.linalg import orth
행3기저 = orth(A3.T)
영3기저 = null_space(A3)
d = 영3기저[:, 0]
traces = [
plane(spans=[행3기저[:, 0], 행3기저[:, 1]], extent=2.2,
color=COLORS["colspace"], opacity=0.4, name="행공간 (2차원)"),
go.Scatter3d(x=[-2.4 * d[0], 2.4 * d[0]], y=[-2.4 * d[1], 2.4 * d[1]],
z=[-2.4 * d[2], 2.4 * d[2]], mode="lines",
line=dict(color=COLORS["nullspace"], width=7), name="영공간 (1차원)"),
go.Scatter3d(x=[0], y=[0], z=[0], mode="markers",
marker=dict(size=7, color="red"), name="원점"),
]
spin_figure(traces, title="정의역 R^3 = 행공간 + 영공간", extent=2.6)공역 쪽 — 열공간과 좌영공간¶
열3기저 = orth(A3)
좌영3기저 = null_space(A3.T)
e = 좌영3기저[:, 0]
traces = [
plane(spans=[열3기저[:, 0], 열3기저[:, 1]], extent=2.2,
color=COLORS["colspace"], opacity=0.4, name="열공간 (2차원)"),
go.Scatter3d(x=[-2.4 * e[0], 2.4 * e[0]], y=[-2.4 * e[1], 2.4 * e[1]],
z=[-2.4 * e[2], 2.4 * e[2]], mode="lines",
line=dict(color=COLORS["nullspace"], width=7), name="좌영공간 (1차원)"),
go.Scatter3d(x=[0], y=[0], z=[0], mode="markers",
marker=dict(size=7, color="red"), name="원점"),
]
spin_figure(traces, title="공역 R^3 = 열공간 + 좌영공간", extent=2.6)직선 방향과 평면의 두 기저 벡터의 내적을 재 보면 미리 엿볼 수는 있다.
print("영공간 방향과 행공간 기저의 내적 :")
for j in range(행3기저.shape[1]):
print(f" {d @ 행3기저[:, j]:+.2e}")
print()
print("좌영공간 방향과 열공간 기저의 내적 :")
for j in range(열3기저.shape[1]):
print(f" {e @ 열3기저[:, j]:+.2e}")영공간 방향과 행공간 기저의 내적 :
-5.55e-17
+2.95e-16
좌영공간 방향과 열공간 기저의 내적 :
+1.53e-16
-1.04e-17
마치며...¶
| 서술 파트의 내용 | 이 노트북의 코드 |
|---|---|
| 행공간 기저 | R[:r] (RREF 의 0이 아닌 행) |
| 열공간 기저 | A[:, 피벗열] (원래 에서) |
| 영공간 기저 | null_space(A) |
| 좌영공간 기저 | null_space(A.T) 또는 소거의 |
| 차원의 기본정리 | 2절의 표 |
| 행랭크 열랭크 | 무작위 300개 시험 |
| 열공간 기저의 함정 | 4절의 열공간에_있는가 |
| 의 작용 | 6절의 “행공간 조각 영공간 조각” 분해 |
더 해 볼 것¶
6절의 분해에서
lstsq대신np.linalg.solve를 쓸 수 있는가? 전체기저가 정사각행렬인 경우와 아닌 경우를 나눠 생각해 보자.7절 마지막의 내적이 전부 10-16 수준이었다. 우연인가? 다른 행렬로도 해 보자.
와 의 네 부분공간을 비교해 보자. 어느 것과 어느 것이 짝이 되는가?
랭크가 인 행렬을 만들어 보자. 좌영공간은 어떻게 되는가? 그때 는 모든 에 대해 풀리는가?
다음 강의에서는 랭크가 1인 행렬을 다룬다. 네 부분공간이 가장 단순해지는 경우이고, 모든 행렬을 쌓아 올리는 벽돌이 된다.