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 5. 전치, 순열, 벡터공간 — 파이썬 실습

Transposes, Permutations, Vector Spaces — 실습

L5 서술 파트에서 순열행렬, 전치, 그리고 벡터공간과 부분공간을 다루었다. 이 노트북에서는 성질들을 직접 확인하고, 어떤 집합이 부분공간인지 아닌지를 무작위 점을 뽑아 판정해 보도록 하자.

서술 파트의 내용여기서 확인하는 방법
순열행렬은 n!n! 개, P1=PTP^{-1} = P^{\mathsf{T}}3×33 \times 3 여섯 개를 모두 만들어 확인한다
(AB)T=BTAT(AB)^{\mathsf{T}} = B^{\mathsf{T}}A^{\mathsf{T}}크기부터 따져 보고 값을 대조한다
RTRR^{\mathsf{T}}R 은 언제나 대칭무작위 행렬 여러 개로 확인한다
부분공간은 원점을 지난다두 평면에 무작위 점을 뽑아 닫힘을 검사한다
합집합은 부분공간이 아니다반례를 직접 만들어 본다

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 ]

3×33 \times 3 순열행렬은 3!=63! = 6 개이다. 모두 만들어서 PTP=IP^{\mathsf{T}}P = I 인지 확인해 보자.

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

PAPAAA 의 행을 재배열한 것이다. 열을 바꾸고 싶으면 오른쪽에서 곱한다.

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 ]

전치가 역행렬이라는 것을 되돌리기로도 확인할 수 있다. PP 를 곱한 뒤 PTP^{\mathsf{T}} 를 곱하면 원래대로 돌아와야 한다.

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. 전치

(AB)T=BTAT(AB)^{\mathsf{T}} = B^{\mathsf{T}}A^{\mathsf{T}} 를 확인해 보자. 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

이 예제는 m=p=3m = p = 3 이라 RTBTR^{\mathsf{T}}B^{\mathsf{T}} 도 곱해지기는 한다. 다만 결과가 2×22 \times 2 라서 (RB)T(RB)^{\mathsf{T}}3×33 \times 3 과 아예 다른 물건이다. 크기가 다르면 같은지 비교할 수조차 없다.

가로세로가 맞지 않는 경우에는 곱셈 자체가 되지 않는다.

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)

역행렬의 전치도 확인해 보자. (A1)T=(AT)1(A^{-1})^{\mathsf{T}} = (A^{\mathsf{T}})^{-1} 이다.

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. RTRR^{\mathsf{T}}R 은 언제나 대칭

서술 파트에서 (RTR)ij(R^{\mathsf{T}}R)_{ij}RRii 열과 jj 열의 내적이라고 하였다. 그래서 iijj 를 바꿔도 값이 같고, 결과가 대칭이 된다.

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  <- 같은 내적이므로 같은 값

RR 이 정방행렬이 아니어도 성립한다. 크기가 다른 무작위 행렬로 확인해 보자.

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. 어떤 집합이 부분공간인가

부분공간이 되려면 덧셈과 스칼라배에 닫혀 있어야 한다. 평면 x+y+z=cx + y + z = c 를 두고 cc 를 바꿔 가며 확인해 보자.

먼저 그 평면 위의 점을 무작위로 만드는 함수가 필요하다. xxyy 를 아무렇게나 뽑고 z=cxyz = c - x - y 로 두면 된다.

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, None
for 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

c=0c = 0 일 때만 닫혀 있다. 원점이 들어 있는지도 같은 결론을 준다.

원점 = 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)
Loading...

6. 교집합과 합집합

합집합은 부분공간이 아니다

R2\R^2 에서 xx 축과 yy 축을 생각한다. 각각은 부분공간이지만 합집합은 아니다.

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
  -> 어느 쪽에도 속하지 않으므로 합집합은 덧셈에 닫혀 있지 않다

교집합은 부분공간이다

R3\R^3 에서 원점을 지나는 두 평면의 교집합은 원점을 지나는 직선이다. 두 법선에 모두 수직인 방향을 외적으로 구하면 그 직선의 방향이 나온다.

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)
Loading...

마치며...

서술 파트의 내용이 노트북의 코드
순열행렬 만들기I3[[1, 2, 0]]
순열행렬은 n!n!permutations 로 세어 math.factorial 과 대조
P1=PTP^{-1} = P^{\mathsf{T}}P.T @ PII
(AB)T=BTAT(AB)^{\mathsf{T}} = B^{\mathsf{T}}A^{\mathsf{T}}크기 비교와 np.allclose
RTRR^{\mathsf{T}}R 이 대칭여러 크기의 무작위 행렬로 확인
부분공간의 닫힘닫혀있는가 의 무작위 검사
합집합은 부분공간이 아니다x축위(s + t)False
교집합은 부분공간이다두 법선의 외적

더 해 볼 것

  1. 4×44 \times 4 순열행렬 24개를 모두 만들고, 그중 P2=IP^2 = I 를 만족하는 것이 몇 개인지 세어 보자. 그런 순열은 어떤 모양인가?

  2. 닫혀있는가 를 평면이 아니라 x2+y2+z2=1x^2 + y^2 + z^2 = 1 인 구면에 대해 해 보자. 구면은 부분공간인가? 어떤 반례가 나오는가?

  3. R3\R^3 에서 첫 성분이 0인 벡터 전체는 부분공간인가? 성분 합이 0인 벡터 전체는 어떤가? 각각 식으로 확인하고 코드로도 검사해 보자.

  4. 무작위 행렬 MM 에 대해 M+MTM + M^{\mathsf{T}} 는 어떤 행렬인가? MMTM - M^{\mathsf{T}} 는? 임의의 정방행렬을 이 둘로 쪼갤 수 있는지 생각해 보자.

다음 강의에서는 행렬 하나에서 부분공간이 저절로 따라 나온다는 것을 본다. 그 두 공간이 열공간과 영공간이다.