L14 서술 파트에서 내적을 도입하자 L10의 지도에 직각이 나타났다. 이 노트북에서는 그 직각을 실제로 재 본다. 네 부분공간의 기저를 뽑아 내적표를 만들면 전부 0이 나와야 하고, 무작위 행렬 수십 개를 넣어도 한 번도 어긋나지 않아야 한다.
| 서술 파트의 내용 | 여기서 확인하는 방법 |
|---|---|
| 피타고라스 내적 0 | 두 값의 차가 정확히 인지 본다 |
| 바닥과 벽 | 두 기저의 내적표에 0이 아닌 칸이 남는다 |
| 행공간 영공간 | orth(A.T).T @ null_space(A) 가 영행렬 |
| 직교여공간 | 차원의 합과 유일한 분해 |
| 무작위 행렬로 랭크까지 대조 |
0. 준비¶
import numpy as np
import plotly.graph_objects as go
from scipy.linalg import null_space, orth
from linalg_viz import (COLORS, arrow, layout3d, line, plane, show_matrix,
spin_figure)
np.set_printoptions(precision=3, suppress=True)
rng = np.random.default_rng(14)
print("numpy", np.__version__)numpy 2.5.2
1. 내적, 길이, 피타고라스¶
내적은 @ 하나로 끝난다. 길이는 자기 자신과의 내적의 제곱근이다.
x = np.array([3.0, 0.0])
y = np.array([0.0, 4.0])
print("x . y =", x @ y)
print("y . x =", y @ x, " (순서를 바꿔도 같다)")
print("x . x =", x @ x, " = |x|^2")
print("|x| =", np.linalg.norm(x))
print("numpy 와 일치 :", np.isclose(x @ x, np.linalg.norm(x) ** 2))x . y = 0.0
y . x = 0.0 (순서를 바꿔도 같다)
x . x = 9.0 = |x|^2
|x| = 3.0
numpy 와 일치 : True
서술 파트에서 를 유도했다. 어긋나는 양이 정확히 내적의 두 배인지 확인해 보자.
def 피타고라스검사(x, y, 이름=""):
"""두 변의 제곱합과 빗변의 제곱을 비교하고, 그 차를 내적과 대조한다."""
왼쪽 = x @ x + y @ y
오른쪽 = (x + y) @ (x + y)
print(f"{이름}")
print(f" |x|^2 + |y|^2 = {왼쪽:.4f}")
print(f" |x + y|^2 = {오른쪽:.4f}")
print(f" 차이 = {오른쪽 - 왼쪽:.4f}")
print(f" 2 (x . y) = {2 * (x @ y):.4f} -> 같은가 "
f"{np.isclose(오른쪽 - 왼쪽, 2 * (x @ y))}")피타고라스검사(np.array([3.0, 0]), np.array([0.0, 4]), "직교하는 경우")
print()
피타고라스검사(np.array([3.0, 0]), np.array([2.0, 3]), "직교하지 않는 경우")직교하는 경우
|x|^2 + |y|^2 = 25.0000
|x + y|^2 = 25.0000
차이 = 0.0000
2 (x . y) = 0.0000 -> 같은가 True
직교하지 않는 경우
|x|^2 + |y|^2 = 22.0000
|x + y|^2 = 34.0000
차이 = 12.0000
2 (x . y) = 12.0000 -> 같은가 True
그림을 그릴 수 없는 차원에서도 똑같다. 직교는 각도가 아니라 등식이기 때문이다.
n = 50
u = rng.normal(size=n)
v = rng.normal(size=n)
v직교 = v - (v @ u) / (u @ u) * u # u 성분을 빼면 u 와 직교한다
for 이름, 짝 in [("무작위 두 벡터", (u, v)), ("u 성분을 뺀 뒤", (u, v직교))]:
a, b = 짝
print(f"{이름:>16} : x . y = {a @ b:+.3e}, "
f"차이 - 2(x.y) = {(a + b) @ (a + b) - a @ a - b @ b - 2 * (a @ b):+.2e}") 무작위 두 벡터 : x . y = -3.459e+00, 차이 - 2(x.y) = -2.75e-14
u 성분을 뺀 뒤 : x . y = -1.998e-15, 차이 - 2(x.y) = -1.02e-14
2. 바닥과 벽¶
두 부분공간이 직교하는지 보려면 양쪽 기저의 모든 짝을 내적해 보면 된다. 기저끼리 전부 0이면 그 결합끼리도 전부 0이기 때문이다.
def 내적표(V, W):
"""열이 기저인 두 행렬을 받아 기저끼리의 내적표를 만든다."""
return V.T @ W바닥 = np.array([[1.0, 0], [0, 1], [0, 0]]) # z = 0 : 열이 (1,0,0), (0,1,0)
벽 = np.array([[1.0, 0], [0, 0], [0, 1]]) # y = 0 : 열이 (1,0,0), (0,0,1)
print(show_matrix(내적표(바닥, 벽), "바닥 기저 . 벽 기저"))
print("전부 0 인가 :", np.allclose(내적표(바닥, 벽), 0))
print()
print("겹치는 벡터 v = (1,0,0) 이 양쪽에 있다.")
v = np.array([1.0, 0, 0])
print(" v . v =", v @ v, " -> 0 이 아니므로 직교 부분공간일 수 없다")바닥 기저 . 벽 기저
[ 1 0 ]
[ 0 0 ]
전부 0 인가 : False
겹치는 벡터 v = (1,0,0) 이 양쪽에 있다.
v . v = 1.0 -> 0 이 아니므로 직교 부분공간일 수 없다
정말 직교하는 짝과 비교해 보자. 바닥과 축은 직교한다.
z축 = np.array([[0.0], [0.0], [1.0]])
print(show_matrix(내적표(바닥, z축), "바닥 기저 . z축"))
print("전부 0 인가 :", np.allclose(내적표(바닥, z축), 0))
print("공유하는 벡터가 있는가 :",
np.linalg.matrix_rank(np.column_stack([바닥, z축])) < 3)바닥 기저 . z축
[ 0 ]
[ 0 ]
전부 0 인가 : True
공유하는 벡터가 있는가 : False
3. 행공간과 영공간¶
서술 파트의 예 로 시작한다.
A = np.array([[1.0, 1, 1],
[1.0, 2, 3]])
행공간 = orth(A.T) # 열이 행공간의 정규직교 기저
영공간 = null_space(A) # 열이 영공간의 정규직교 기저
print(show_matrix(행공간, "행공간의 기저"))
print(show_matrix(영공간, "영공간의 기저"))
print("차원 :", 행공간.shape[1], "+", 영공간.shape[1], "=",
행공간.shape[1] + 영공간.shape[1], " (n =", A.shape[1], ")")행공간의 기저
[ -0.323 0.854 ]
[ -0.548 0.183 ]
[ -0.772 -0.487 ]
영공간의 기저
[ 0.408 ]
[ -0.816 ]
[ 0.408 ]
차원 : 2 + 1 = 3 (n = 3 )
print(show_matrix(내적표(행공간, 영공간), "행공간 기저 . 영공간 기저"))
print("전부 0 인가 :", np.allclose(내적표(행공간, 영공간), 0, atol=1e-12))행공간 기저 . 영공간 기저
[ -5.55e-17 ]
[ -8.33e-17 ]
전부 0 인가 : True
서술 파트의 증명은 을 행별로 읽은 것이 전부였다.
그러니 사실 A @ 영공간 자체가 이미 그 내적표이다. 각 성분이 (행) (영공간 벡터)이다.
print(show_matrix(A @ 영공간, "A @ 영공간 = (각 행) . (영공간 기저)"))
print("전부 0 인가 :", np.allclose(A @ 영공간, 0, atol=1e-12))
print()
print("영공간 기저를 정수로 보면 :", np.round(영공간[:, 0] / 영공간[0, 0], 6))A @ 영공간 = (각 행) . (영공간 기저)
[ 0 ]
[ 5.55e-17 ]
전부 0 인가 : True
영공간 기저를 정수로 보면 : [ 1. -2. 1.]
행공간에서 아무 벡터나, 영공간에서 아무 벡터나 뽑아 내적해도 0이어야 한다. 계수를 무작위로 바꿔 가며 여러 번 해 보자.
최대 = 0.0
for _ in range(2000):
v = 행공간 @ rng.normal(size=행공간.shape[1])
w = 영공간 @ rng.normal(size=영공간.shape[1])
최대 = max(최대, abs(v @ w))
print(f"2000번 중 가장 큰 내적의 절댓값 : {최대:.2e}")
print("0 이라고 봐도 되는가 :", 최대 < 1e-12)2000번 중 가장 큰 내적의 절댓값 : 6.66e-16
0 이라고 봐도 되는가 : True
4. 열공간과 좌영공간¶
증명은 자리에 를 넣은 것뿐이었다. 코드도 그렇다.
def 직교확인(A, 이름="A"):
"""네 부분공간의 기저를 뽑아 두 쌍의 내적표가 영행렬인지 확인한다."""
m, n = A.shape
r = np.linalg.matrix_rank(A)
쌍 = {
"행공간 . 영공간": (orth(A.T), null_space(A)),
"열공간 . 좌영공간": (orth(A), null_space(A.T)),
}
print(f"[{이름}] {m} x {n}, rank {r}")
for 이름칸, (V, W) in 쌍.items():
if V.size == 0 or W.size == 0:
print(f" {이름칸:>18} : 한쪽이 비어 있다 (차원 0)")
continue
표 = V.T @ W
print(f" {이름칸:>18} : 크기 {표.shape}, "
f"가장 큰 절댓값 {np.abs(표).max():.2e}, 전부 0 {np.allclose(표, 0, atol=1e-10)}")
return r_ = 직교확인(A, "L14 의 예")
print()
B = np.array([[1.0, 2, 2, 2],
[2.0, 4, 6, 8],
[3.0, 6, 8, 10]])
_ = 직교확인(B, "L7 의 행렬")[L14 의 예] 2 x 3, rank 2
행공간 . 영공간 : 크기 (2, 1), 가장 큰 절댓값 8.33e-17, 전부 0 True
열공간 . 좌영공간 : 한쪽이 비어 있다 (차원 0)
[L7 의 행렬] 3 x 4, rank 2
행공간 . 영공간 : 크기 (2, 2), 가장 큰 절댓값 4.31e-16, 전부 0 True
열공간 . 좌영공간 : 크기 (2, 1), 가장 큰 절댓값 2.22e-16, 전부 0 True
쪽에서 두 번째 쌍이 비어 있다고 나온다. 가 이고 랭크가 2라 이고 이기 때문이다. 직교할 상대가 영벡터뿐이면 확인할 것도 없다.
무작위 행렬 여러 개에 넣어도 한 번도 어긋나지 않아야 한다.
print(f"{'m x n':>8}{'rank':>6}{'행 . 영':>12}{'열 . 좌영':>12}")
for _ in range(10):
m_ = int(rng.integers(2, 7))
n_ = int(rng.integers(2, 7))
k = int(rng.integers(1, min(m_, n_) + 1))
M = (rng.integers(-3, 4, (m_, k)) @ rng.integers(-3, 4, (k, n_))).astype(float)
if np.linalg.matrix_rank(M) == 0:
continue
쌍1 = (orth(M.T), null_space(M))
쌍2 = (orth(M), null_space(M.T))
값 = []
for V, W in (쌍1, 쌍2):
값.append("(빈 공간)" if V.size == 0 or W.size == 0
else f"{np.abs(V.T @ W).max():.1e}")
print(f"{f'{m_} x {n_}':>8}{np.linalg.matrix_rank(M):>6}{값[0]:>12}{값[1]:>12}") m x n rank 행 . 영 열 . 좌영
2 x 4 1 9.7e-17 5.6e-17
6 x 2 1 0.0e+00 5.6e-17
4 x 2 1 1.1e-16 1.4e-16
3 x 6 2 4.2e-17 5.6e-17
4 x 5 1 4.1e-16 1.1e-16
3 x 2 2 (빈 공간) 0.0e+00
2 x 3 1 7.5e-17 5.6e-17
5 x 2 2 (빈 공간) 1.3e-16
6 x 3 1 1.9e-16 4.3e-16
2 x 5 2 2.4e-16 (빈 공간)
5. 직교여공간과 유일한 분해¶
행공간의 기저와 영공간의 기저를 한데 모으면 의 기저가 된다. 그러면 아무 벡터나 두 조각으로 갈리고, 갈리는 방법은 하나뿐이다.
def 쪼개기(A, x):
"""x 를 행공간 성분과 영공간 성분으로 나눈다.
두 기저를 이어 붙여 만든 정사각행렬로 좌표를 풀면 된다.
(다음 강의에서 이보다 직접적인 공식을 배운다.)
"""
Q, N = orth(A.T), null_space(A)
B = np.column_stack([Q, N]) # n x n, 가역이다
좌표 = np.linalg.solve(B, x)
r = Q.shape[1]
return Q @ 좌표[:r], N @ 좌표[r:]x = np.array([0.0, -2.0, 2.0])
x_r, x_n = 쪼개기(A, x)
print("x =", x)
print("x_r =", x_r, " (행공간 성분)")
print("x_n =", x_n, " (영공간 성분)")
print()
print("합이 x 인가 :", np.allclose(x_r + x_n, x))
print("x_n 이 영공간인가 :", np.allclose(A @ x_n, 0))
print("x_r 이 영공간과 직교 :", np.allclose(x_r @ 영공간, 0, atol=1e-12))
print("서로 직교한가 :", np.isclose(x_r @ x_n, 0, atol=1e-12))x = [ 0. -2. 2.]
x_r = [-1. -0. 1.] (행공간 성분)
x_n = [ 1. -2. 1.] (영공간 성분)
합이 x 인가 : True
x_n 이 영공간인가 : True
x_r 이 영공간과 직교 : True
서로 직교한가 : True
서술 파트에서 , 이라 했다. 맞는지 보자.
print("손으로 : x_r = [-1 0 1] , x_n = [ 1 -2 1]")
print("코드로 : x_r =", np.round(x_r, 10) + 0.0,
", x_n =", np.round(x_n, 10) + 0.0)
print("같은가 :", np.allclose(x_r, [-1, 0, 1]) and np.allclose(x_n, [1, -2, 1]))손으로 : x_r = [-1 0 1] , x_n = [ 1 -2 1]
코드로 : x_r = [-1. 0. 1.] , x_n = [ 1. -2. 1.]
같은가 : True
유일성을 확인해 보자. 행공간의 기저를 다른 것으로 바꿔도 나오는 조각은 같아야 한다.
M = rng.normal(size=(2, 2))
while abs(np.linalg.det(M)) < 0.3:
M = rng.normal(size=(2, 2))
다른기저 = orth(A.T) @ M # 같은 행공간, 다른 기저
B2 = np.column_stack([다른기저, 영공간])
좌표2 = np.linalg.solve(B2, x)
x_r2 = 다른기저 @ 좌표2[:2]
print(show_matrix(다른기저, "행공간의 다른 기저"))
print("이 기저로 구한 x_r :", np.round(x_r2, 10) + 0.0)
print("처음 것과 같은가 :", np.allclose(x_r, x_r2))행공간의 다른 기저
[ -0.904 -0.53 ]
[ 0.108 0.201 ]
[ 1.12 0.933 ]
이 기저로 구한 x_r : [-1. 0. 1.]
처음 것과 같은가 : True
는 영공간 성분을 보지 않는다. 그래서 이다.
print("A x =", A @ x)
print("A x_r =", A @ x_r)
print("A x_n =", A @ x_n, " (영벡터)")
print("A x = A x_r 인가 :", np.allclose(A @ x, A @ x_r))A x = [0. 2.]
A x_r = [-0. 2.]
A x_n = [0. 0.] (영벡터)
A x = A x_r 인가 : True
6. ¶
는 언제나 대칭이고, 영공간과 랭크가 와 같다.
G = A.T @ A
print(show_matrix(G, "A^T A"))
print("대칭인가 :", np.allclose(G, G.T))
print("rank(A) :", np.linalg.matrix_rank(A))
print("rank(A^T A) :", np.linalg.matrix_rank(G))
print()
print("두 영공간이 같은 공간인가 :",
np.linalg.matrix_rank(np.column_stack([null_space(A), null_space(G)]))
== null_space(A).shape[1])A^T A
[ 2 3 4 ]
[ 3 5 7 ]
[ 4 7 10 ]
대칭인가 : True
rank(A) : 2
rank(A^T A) : 2
두 영공간이 같은 공간인가 : True
증명의 핵심은 이었다. 숫자로 확인해 보자.
for _ in range(5):
z = rng.normal(size=3)
왼쪽 = z @ (A.T @ A) @ z
오른쪽 = np.linalg.norm(A @ z) ** 2
print(f"x^T A^T A x = {왼쪽:8.4f} |A x|^2 = {오른쪽:8.4f} "
f"같은가 {np.isclose(왼쪽, 오른쪽)}")x^T A^T A x = 2.1802 |A x|^2 = 2.1802 같은가 True
x^T A^T A x = 2.9629 |A x|^2 = 2.9629 같은가 True
x^T A^T A x = 9.7339 |A x|^2 = 9.7339 같은가 True
x^T A^T A x = 0.3655 |A x|^2 = 0.3655 같은가 True
x^T A^T A x = 60.1133 |A x|^2 = 60.1133 같은가 True
무작위 행렬로 랭크와 영공간 차원이 언제나 맞아떨어지는지 본다.
print(f"{'m x n':>8}{'rank A':>8}{'rank A^T A':>12}{'dim N(A)':>10}"
f"{'dim N(A^T A)':>14}{'가역':>7}")
for _ in range(10):
m_ = int(rng.integers(2, 8))
n_ = int(rng.integers(2, 6))
k = int(rng.integers(1, min(m_, n_) + 1))
M = (rng.integers(-3, 4, (m_, k)) @ rng.integers(-3, 4, (k, n_))).astype(float)
G_ = M.T @ M
rM, rG = np.linalg.matrix_rank(M), np.linalg.matrix_rank(G_)
print(f"{f'{m_} x {n_}':>8}{rM:>8}{rG:>12}"
f"{null_space(M).shape[1]:>10}{null_space(G_).shape[1]:>14}"
f"{str(rG == n_):>7}") m x n rank A rank A^T A dim N(A) dim N(A^T A) 가역
3 x 4 3 3 1 1 False
7 x 2 2 2 0 0 True
5 x 2 2 2 0 0 True
4 x 5 2 2 3 3 False
7 x 4 1 1 3 3 False
6 x 5 4 4 1 1 False
2 x 5 2 2 3 3 False
4 x 4 4 4 0 0 True
6 x 2 2 2 0 0 True
3 x 3 2 2 1 1 False
가역인 줄은 rank A^T A 가 과 같은 줄, 곧 의 열이 독립인 줄뿐이다.
열이 종속이면 도 특이하다.
나쁜A = np.array([[1.0, 1], [1.0, 1]])
나쁜G = 나쁜A.T @ 나쁜A
print(show_matrix(나쁜A, "A (두 열이 같다)"))
print(show_matrix(나쁜G, "A^T A"))
print("rank(A) :", np.linalg.matrix_rank(나쁜A))
print("rank(A^T A) :", np.linalg.matrix_rank(나쁜G))
print("det(A^T A) :", np.linalg.det(나쁜G))
print("가역인가 :", np.linalg.matrix_rank(나쁜G) == 나쁜A.shape[1])A (두 열이 같다)
[ 1 1 ]
[ 1 1 ]
A^T A
[ 2 2 ]
[ 2 2 ]
rank(A) : 1
rank(A^T A) : 1
det(A^T A) : 0.0
가역인가 : False
7. 직각을 여러 각도에서 보기¶
3차원 그림은 보는 방향에 따라 직각이 직각으로 보이지 않는다. 돌려 가며 확인해 보자. 평면이 행공간, 회색 직선이 영공간이다.
법선 = np.array([1.0, -2.0, 1.0]) # 영공간의 방향
그림요소 = [
plane(normal=법선, extent=2.4, color=COLORS["colspace"],
opacity=0.35, name="행공간"),
line(법선, extent=1.05, color=COLORS["nullspace"], name="영공간", dashed=False),
]
for 벡터, 색, 이름 in [(A[0], COLORS["colspace"], "행 1"),
(A[1] / 1.5, COLORS["second"], "행 2"),
(법선, COLORS["nullspace"], "N(A) 방향")]:
그림요소 += arrow([0, 0, 0], 벡터, 색, 이름)
spin_figure(그림요소, title="행공간(평면)과 영공간(직선)은 직각으로 만난다",
extent=2.6, n_frames=48, radius=2.4)5절의 분해도 그려 보자. 주황이 , 파랑이 행공간 성분, 회색이 영공간 성분이다. 평행사변형이 닫히는 것이 분해가 존재한다는 뜻이고, 닫히는 방법이 하나뿐인 것이 유일성이다.
분해요소 = [plane(normal=법선, extent=2.4, color=COLORS["colspace"],
opacity=0.28, name="행공간")]
분해요소 += arrow([0, 0, 0], x, COLORS["output"], "x")
분해요소 += arrow([0, 0, 0], x_r, COLORS["input"], "x_r")
분해요소 += arrow([0, 0, 0], x_n, COLORS["nullspace"], "x_n")
분해요소 += arrow(x_r, x, COLORS["nullspace"], "", dashed=True, legend=False)
분해요소 += arrow(x_n, x, COLORS["input"], "", dashed=True, legend=False)
go.Figure(data=분해요소, layout=layout3d("x = x_r + x_n", extent=2.6))마치며...¶
| 서술 파트의 내용 | 이 노트북의 코드 |
|---|---|
| 내적과 길이 | x @ y, np.linalg.norm(x) |
| 피타고라스 내적 0 | 피타고라스검사(x, y) |
| 바닥과 벽 | 내적표(바닥, 벽) 에 1 이 남는다 |
| 행공간 영공간 | A @ null_space(A) 가 영행렬 |
| 열공간 좌영공간 | 직교확인(A) |
| 유일한 분해 | 쪼개기(A, x) |
| 무작위 행렬로 랭크 대조 | |
| 가역 조건 | 열이 독립인 줄만 가역 이 참 |
더 해 볼 것¶
쪼개기는 두 기저를 붙여 연립방정식을 푼다. 3절에서 본 대로 영공간 기저는 정규직교이니, 훨씬 짧은 식으로 을 구할 수 있다. 어떤 식일지 짐작해 보자. 다음 강의에서 그 식이 나온다.직교확인에 세 번째 쌍 “행공간 . 열공간” 을 넣어 보자. 0이 나오는가? 왜 그런가, 혹은 왜 그렇지 않은가?7절의 그림에서
법선을 바꿔 가며 평면과 직선이 함께 돌아가는지 보자.대신 를 보자. 랭크는 얼마인가? 영공간은 의 어느 부분공간과 같은가?
다음 강의에서는 을 실제로 구하는 방법을 배운다. 그리고 같은 방법이 가 열공간 밖에 있어 해가 없을 때 무엇을 할 것인가에도 답한다.