L5 서술 파트에서 순열행렬, 전치, 그리고 벡터공간과 부분공간을 다루었다. 이 노트북에서는 성질들을 직접 확인하고, 어떤 집합이 부분공간인지 아닌지를 무작위 점을 뽑아 판정해 보도록 하자.
| 서술 파트의 내용 | 여기서 확인하는 방법 |
|---|---|
| 순열행렬은 개, | 여섯 개를 모두 만들어 확인한다 |
| 크기부터 따져 보고 값을 대조한다 | |
| 은 언제나 대칭 | 무작위 행렬 여러 개로 확인한다 |
| 부분공간은 원점을 지난다 | 두 평면에 무작위 점을 뽑아 닫힘을 검사한다 |
| 합집합은 부분공간이 아니다 | 반례를 직접 만들어 본다 |
0. 준비¶
import numpy as np
import plotly.graph_objects as go
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__)numpy 2.5.2
1. 순열행렬¶
단위행렬의 행 순서를 바꾸면 순열행렬이 된다. numpy 에서는 행 번호 목록으로 인덱싱하면
그대로 만들어진다.
I3 = np.eye(3)
P = I3[[1, 2, 0]] # 행을 2, 3, 1 번째 순서로 가져온다
print(show_matrix(P, "P ="))P =
[ 0 1 0 ]
[ 0 0 1 ]
[ 1 0 0 ]
순열행렬은 개이다. 모두 만들어서 인지 확인해 보자.
from itertools import permutations
모든순열 = [I3[list(순서)] for 순서 in permutations(range(3))]
print("3x3 순열행렬의 개수 :", len(모든순열))
print()
for 순서, Pk in zip(permutations(range(3)), 모든순열):
행순서 = [i + 1 for i in 순서]
print(f" 행 순서 {행순서} -> P^T P = I 인가 : {np.allclose(Pk.T @ Pk, I3)}")3x3 순열행렬의 개수 : 6
행 순서 [1, 2, 3] -> P^T P = I 인가 : True
행 순서 [1, 3, 2] -> P^T P = I 인가 : True
행 순서 [2, 1, 3] -> P^T P = I 인가 : True
행 순서 [2, 3, 1] -> P^T P = I 인가 : True
행 순서 [3, 1, 2] -> P^T P = I 인가 : True
행 순서 [3, 2, 1] -> P^T P = I 인가 : True
import math
for n in (2, 3, 4, 5):
개수 = len(list(permutations(range(n))))
print(f"n = {n} : 순열행렬 {개수}개, n! = {math.factorial(n)}")n = 2 : 순열행렬 2개, n! = 2
n = 3 : 순열행렬 6개, n! = 6
n = 4 : 순열행렬 24개, n! = 24
n = 5 : 순열행렬 120개, n! = 120
는 의 행을 재배열한 것이다. 열을 바꾸고 싶으면 오른쪽에서 곱한다.
A = np.array([[1, 2, 1],
[3, 8, 1],
[0, 4, 1]], dtype=float)
print(show_matrix(P @ A, "P A (행을 2, 3, 1 순서로)"))
print(show_matrix(A @ P, "A P (열의 순서가 바뀐다)"))P A (행을 2, 3, 1 순서로)
[ 3 8 1 ]
[ 0 4 1 ]
[ 1 2 1 ]
A P (열의 순서가 바뀐다)
[ 1 1 2 ]
[ 1 3 8 ]
[ 1 0 4 ]
전치가 역행렬이라는 것을 되돌리기로도 확인할 수 있다. 를 곱한 뒤 를 곱하면 원래대로 돌아와야 한다.
print(show_matrix(P.T @ (P @ A), "P^T (P A) ="))
print("A 로 돌아왔는가 :", np.allclose(P.T @ (P @ A), A))P^T (P A) =
[ 1 2 1 ]
[ 3 8 1 ]
[ 0 4 1 ]
A 로 돌아왔는가 : True
2. 전치¶
를 확인해 보자. L3에서 쓴 두 행렬을 그대로 쓴다.
R = np.array([[1, 2],
[3, 4],
[5, 6]], dtype=float) # 3 x 2
B = np.array([[7, 8, 9],
[10, 11, 12]], dtype=float) # 2 x 3
print("(R B)^T 의 크기 :", (R @ B).T.shape)
print("B^T R^T 의 크기 :", (B.T @ R.T).shape, " <- 맞다")
print("R^T B^T 의 크기 :", (R.T @ B.T).shape, " <- 크기부터 다르다")
print()
print("(R B)^T == B^T R^T :", np.allclose((R @ B).T, B.T @ R.T))(R B)^T 의 크기 : (3, 3)
B^T R^T 의 크기 : (3, 3) <- 맞다
R^T B^T 의 크기 : (2, 2) <- 크기부터 다르다
(R B)^T == B^T R^T : True
이 예제는 이라 도 곱해지기는 한다. 다만 결과가 라서 의 과 아예 다른 물건이다. 크기가 다르면 같은지 비교할 수조차 없다.
가로세로가 맞지 않는 경우에는 곱셈 자체가 되지 않는다.
C = rng.standard_normal((2, 5)) # R 은 3x2 이므로 R C 는 3x5
print("(R C)^T 의 크기 :", (R @ C).T.shape)
try:
R.T @ C.T
except ValueError as err:
print("R^T C^T 는 곱할 수 없다 :", err)(R C)^T 의 크기 : (5, 3)
R^T C^T 는 곱할 수 없다 : matmul: Input operand 1 has a mismatch in its core dimension 0, with gufunc signature (n?,k),(k,m?)->(n?,m?) (size 5 is different from 3)
역행렬의 전치도 확인해 보자. 이다.
print("(A^-1)^T == (A^T)^-1 :",
np.allclose(np.linalg.inv(A).T, np.linalg.inv(A.T)))(A^-1)^T == (A^T)^-1 : True
3. 은 언제나 대칭¶
서술 파트에서 가 의 열과 열의 내적이라고 하였다. 그래서 와 를 바꿔도 값이 같고, 결과가 대칭이 된다.
S = R.T @ R
print(show_matrix(S, "R^T R ="))
print("대칭인가 :", np.allclose(S, S.T))
print()
print("R 의 1열 . 2열 =", R[:, 0] @ R[:, 1])
print("(R^T R)[0,1] =", S[0, 1])
print("(R^T R)[1,0] =", S[1, 0], " <- 같은 내적이므로 같은 값")R^T R =
[ 35 44 ]
[ 44 56 ]
대칭인가 : True
R 의 1열 . 2열 = 44.0
(R^T R)[0,1] = 44.0
(R^T R)[1,0] = 44.0 <- 같은 내적이므로 같은 값
이 정방행렬이 아니어도 성립한다. 크기가 다른 무작위 행렬로 확인해 보자.
for 크기 in [(2, 5), (5, 2), (4, 4), (1, 6), (7, 3)]:
M = rng.standard_normal(크기)
좌 = M.T @ M
우 = M @ M.T
print(f"M {str(크기):>8} -> M^T M {str(좌.shape):>8} 대칭 {np.allclose(좌, 좌.T)}"
f" M M^T {str(우.shape):>8} 대칭 {np.allclose(우, 우.T)}")M (2, 5) -> M^T M (5, 5) 대칭 True M M^T (2, 2) 대칭 True
M (5, 2) -> M^T M (2, 2) 대칭 True M M^T (5, 5) 대칭 True
M (4, 4) -> M^T M (4, 4) 대칭 True M M^T (4, 4) 대칭 True
M (1, 6) -> M^T M (6, 6) 대칭 True M M^T (1, 1) 대칭 True
M (7, 3) -> M^T M (3, 3) 대칭 True M M^T (7, 7) 대칭 True
4. 어떤 집합이 부분공간인가¶
부분공간이 되려면 덧셈과 스칼라배에 닫혀 있어야 한다. 평면 를 두고 를 바꿔 가며 확인해 보자.
먼저 그 평면 위의 점을 무작위로 만드는 함수가 필요하다. 와 를 아무렇게나 뽑고 로 두면 된다.
def 평면위의점(c, 개수=1):
"""x + y + z = c 를 만족하는 점을 무작위로 만든다."""
xy = rng.standard_normal((개수, 2))
z = c - xy.sum(axis=1)
return np.column_stack([xy, z])점 = 평면위의점(1.0, 4)
print(show_matrix(점, "x + y + z = 1 위의 점 네 개"))
print("각 점의 성분 합 :", 점.sum(axis=1))x + y + z = 1 위의 점 네 개
[ -0.618 1.82 -0.204 ]
[ -1.32 -0.662 2.98 ]
[ 0.935 0.0491 0.0159 ]
[ 2 0.189 -1.19 ]
각 점의 성분 합 : [1. 1. 1. 1.]
이제 두 점을 더하거나 스칼라를 곱했을 때 여전히 그 평면 위에 남는지 검사한다.
def 닫혀있는가(c, 횟수=500):
"""평면 x+y+z=c 가 덧셈과 스칼라배에 닫혀 있는지 무작위로 검사한다.
반례를 찾으면 그것을 함께 돌려준다."""
점 = 평면위의점(c, 2 * 횟수)
for i in range(횟수):
u, v = 점[2 * i], 점[2 * i + 1]
if not np.isclose((u + v).sum(), c):
return False, ("덧셈", u, v, u + v)
s = rng.standard_normal()
if not np.isclose((s * u).sum(), c):
return False, ("스칼라배", u, s, s * u)
return True, Nonefor c in (0.0, 1.0, -2.5):
통과, 반례 = 닫혀있는가(c)
if 통과:
print(f"x + y + z = {c:>4g} : 닫혀 있다")
else:
연산 = 반례[0]
print(f"x + y + z = {c:>4g} : 닫혀 있지 않다 ({연산}에서 실패)")
print(f" {반례[1]} 와 {반례[2]}")
print(f" -> 결과의 성분 합이 {반례[3].sum():.3f}")x + y + z = 0 : 닫혀 있다
x + y + z = 1 : 닫혀 있지 않다 (덧셈에서 실패)
[ 0.079 1.269 -0.348] 와 [0.583 0.124 0.293]
-> 결과의 성분 합이 2.000
x + y + z = -2.5 : 닫혀 있지 않다 (덧셈에서 실패)
[ 0.157 0.859 -3.516] 와 [ 0.513 0.621 -3.635]
-> 결과의 성분 합이 -5.000
일 때만 닫혀 있다. 원점이 들어 있는지도 같은 결론을 준다.
원점 = np.zeros(3)
print("원점의 성분 합 :", 원점.sum())
print(" x + y + z = 0 위에 있는가 :", np.isclose(원점.sum(), 0))
print(" x + y + z = 1 위에 있는가 :", np.isclose(원점.sum(), 1))원점의 성분 합 : 0.0
x + y + z = 0 위에 있는가 : True
x + y + z = 1 위에 있는가 : False
서술 파트에서 든 구체적인 반례도 확인해 보자.
u = np.array([1.0, 0.0, 0.0])
v = np.array([0.0, 1.0, 0.0])
for 이름, 점 in [("u", u), ("v", v), ("u + v", u + v), ("2u", 2 * u)]:
print(f"{이름:>6} = {점} 성분 합 = {점.sum():g}")
print()
print("u 와 v 는 평면 위에 있지만 u+v 와 2u 는 벗어난다.") u = [1. 0. 0.] 성분 합 = 1
v = [0. 1. 0.] 성분 합 = 1
u + v = [1. 1. 0.] 성분 합 = 2
2u = [2. 0. 0.] 성분 합 = 2
u 와 v 는 평면 위에 있지만 u+v 와 2u 는 벗어난다.
5. 두 평면을 나란히 보기¶
원점을 지나는 평면과 그것을 밀어낸 평면을 함께 그려 보자. 돌려 보면 한쪽만 원점을 지난다는 것이 보인다.
n = [1.0, 1.0, 1.0]
traces = [
plane(normal=n, extent=2.2, color=COLORS["colspace"], opacity=0.4,
name="x + y + z = 0 (부분공간)"),
plane(normal=n, through=np.array([1.0, 1.0, 1.0]) / 3, extent=2.2,
color=COLORS["error"], opacity=0.3,
name="x + y + z = 1 (부분공간 아님)"),
go.Scatter3d(x=[0], y=[0], z=[0], mode="markers",
marker=dict(size=8, color="red"), name="원점"),
]
spin_figure(traces, title="원점을 지나는 평면과 지나지 않는 평면", extent=2.5)def x축위(p):
return np.isclose(p[1], 0)
def y축위(p):
return np.isclose(p[0], 0)s = np.array([1.0, 0.0])
t = np.array([0.0, 1.0])
합 = s + t
print(f"s = {s} x축 위 : {x축위(s)}")
print(f"t = {t} y축 위 : {y축위(t)}")
print()
print(f"s + t = {합}")
print(f" x축 위 : {x축위(합)}")
print(f" y축 위 : {y축위(합)}")
print(" -> 어느 쪽에도 속하지 않으므로 합집합은 덧셈에 닫혀 있지 않다")s = [1. 0.] x축 위 : True
t = [0. 1.] y축 위 : True
s + t = [1. 1.]
x축 위 : False
y축 위 : False
-> 어느 쪽에도 속하지 않으므로 합집합은 덧셈에 닫혀 있지 않다
교집합은 부분공간이다¶
에서 원점을 지나는 두 평면의 교집합은 원점을 지나는 직선이다. 두 법선에 모두 수직인 방향을 외적으로 구하면 그 직선의 방향이 나온다.
n1 = np.array([1.0, 1.0, 1.0])
n2 = np.array([1.0, -1.0, 0.0])
방향 = np.cross(n1, n2)
print("두 평면의 교선 방향 :", 방향)
print()
print("n1 . 방향 =", n1 @ 방향, " -> 첫 번째 평면 위에 있다")
print("n2 . 방향 =", n2 @ 방향, " -> 두 번째 평면 위에도 있다")
print()
for t in (-2.0, 0.0, 3.5):
점 = t * 방향
print(f"t = {t:>4} : {점} n1 . 점 = {n1 @ 점:.1f}, n2 . 점 = {n2 @ 점:.1f}")두 평면의 교선 방향 : [ 1. 1. -2.]
n1 . 방향 = 0.0 -> 첫 번째 평면 위에 있다
n2 . 방향 = 0.0 -> 두 번째 평면 위에도 있다
t = -2.0 : [-2. -2. 4.] n1 . 점 = 0.0, n2 . 점 = 0.0
t = 0.0 : [ 0. 0. -0.] n1 . 점 = 0.0, n2 . 점 = 0.0
t = 3.5 : [ 3.5 3.5 -7. ] n1 . 점 = 0.0, n2 . 점 = 0.0
직선 위의 어느 점을 잡아도 두 평면 위에 있다. 교집합이 부분공간이라는 것을 확인한 셈이다.
traces = [
plane(normal=n1, extent=2.4, color=COLORS["colspace"], opacity=0.35, name="평면 1"),
plane(normal=n2, extent=2.4, color=COLORS["third"], opacity=0.35, name="평면 2"),
go.Scatter3d(x=[-2.4 * 방향[0] / np.linalg.norm(방향), 2.4 * 방향[0] / np.linalg.norm(방향)],
y=[-2.4 * 방향[1] / np.linalg.norm(방향), 2.4 * 방향[1] / np.linalg.norm(방향)],
z=[-2.4 * 방향[2] / np.linalg.norm(방향), 2.4 * 방향[2] / np.linalg.norm(방향)],
mode="lines", line=dict(color="red", width=7), name="교집합 (직선)"),
]
spin_figure(traces, title="두 평면의 교집합은 원점을 지나는 직선", extent=2.6)마치며...¶
| 서술 파트의 내용 | 이 노트북의 코드 |
|---|---|
| 순열행렬 만들기 | I3[[1, 2, 0]] |
| 순열행렬은 개 | permutations 로 세어 math.factorial 과 대조 |
P.T @ P 가 | |
크기 비교와 np.allclose | |
| 이 대칭 | 여러 크기의 무작위 행렬로 확인 |
| 부분공간의 닫힘 | 닫혀있는가 의 무작위 검사 |
| 합집합은 부분공간이 아니다 | x축위(s + t) 가 False |
| 교집합은 부분공간이다 | 두 법선의 외적 |
더 해 볼 것¶
순열행렬 24개를 모두 만들고, 그중 를 만족하는 것이 몇 개인지 세어 보자. 그런 순열은 어떤 모양인가?
닫혀있는가를 평면이 아니라 인 구면에 대해 해 보자. 구면은 부분공간인가? 어떤 반례가 나오는가?에서 첫 성분이 0인 벡터 전체는 부분공간인가? 성분 합이 0인 벡터 전체는 어떤가? 각각 식으로 확인하고 코드로도 검사해 보자.
무작위 행렬 에 대해 는 어떤 행렬인가? 는? 임의의 정방행렬을 이 둘로 쪼갤 수 있는지 생각해 보자.
다음 강의에서는 행렬 하나에서 부분공간이 저절로 따라 나온다는 것을 본다. 그 두 공간이 열공간과 영공간이다.