L30 서술 파트의 문장 하나가 이 노트북 전체를 굴린다.
행렬의 번째 열은 번째 기저벡터가 어디로 가는지를 적은 것이다.
이 문장이 그대로 코드가 된다. 기저벡터를 하나씩 넣어 보고 답을 열에 세우는 함수를 열 줄 안에 짜서, 그것으로 미분과 적분의 행렬을 만들고 같은 변환을 여러 기저에서 적어 본다. 그리고 미분한 뒤 적분하면 무엇을 잃는지 확인한다.
| 서술 파트의 내용 | 여기서 확인하는 방법 |
|---|---|
| 선형이면 기저의 행선지가 전부 | 행렬만들기 열 줄 |
| 평행이동은 선형이 아니다 | 과 동차좌표 |
| 번째 열 = 번째 기저벡터의 행선지 | 표준기저를 넣어 본다 |
| 합성이 곱 | 두 번 따라간 것과 를 대조 |
| 미분은 행렬이다 | , 영공간이 상수 |
| 인데 | 상수항이 사라진다 |
| 가 투영 | , |
| 의 정체 | np.linalg.pinv 와 같다 |
| 투영의 고윳값 | 좋은 기저에서 |
| 의 | 기저 변환 행렬 |
| 대각화 = 좌표계 선택 | 고유기저에서 행렬을 만들면 |
| 좋은 기저가 없을 수도 | 회전은 어떤 실수 기저에서도 안 된다 |
0. 준비¶
import numpy as np
import plotly.graph_objects as go
from scipy.linalg import null_space
from linalg_viz import COLORS, layout2d, show_matrix, slider_figure
np.set_printoptions(precision=4, suppress=True)
rng = np.random.default_rng(30)
print("numpy", np.__version__)numpy 2.5.2
1. 정의를 그대로 코드로¶
서술 파트의 정의는 이랬다. 입력 기저를 하나씩 에 넣고, 나온 것을 출력 기저로 전개해, 그 계수를 열에 세운다. 열 줄이면 된다.
def 행렬만들기(T, 입력기저, 출력기저):
"""선형변환 T 의 행렬을 정의대로 만든다.
입력기저 : 열이 기저벡터인 행렬 (n 개)
출력기저 : 열이 기저벡터인 행렬 (m 개)
"""
입력기저 = np.asarray(입력기저, dtype=float)
출력기저 = np.asarray(출력기저, dtype=float)
열들 = []
for j in range(입력기저.shape[1]):
간곳 = T(입력기저[:, j]) # j 번째 기저벡터가 가는 곳
좌표, *_ = np.linalg.lstsq(출력기저, 간곳, rcond=None) # 출력 기저로 전개
열들.append(좌표)
return np.column_stack(열들)
def 깔끔(M, 눈감아=1e-10):
"""눈으로 볼 때 방해가 되는 아주 작은 값을 0 으로 눌러 준다.
lstsq 는 실수 나눗셈을 하므로 결과가 정확히 0 이 되는 일은 드물다.
1e-16 같은 값은 '0 인데 반올림 오차가 남은 것' 이라고 읽으면 된다.
"""
M = np.asarray(M, dtype=float).copy()
M[np.abs(M) < 눈감아] = 0.0
return M표준기저를 넣으면 우리가 아는 행렬이 그대로 나와야 한다.
A = np.array([[2.0, -1.0], [1.0, 1.0]])
만든것 = 행렬만들기(lambda v: A @ v, np.eye(2), np.eye(2))
print(show_matrix(A, "원래 A"))
print(show_matrix(만든것, "기저벡터에게 물어서 만든 것"))
print("같은가 :", np.allclose(A, 만든것))
print()
print("e1 이 가는 곳 :", A @ np.array([1.0, 0.0]), " = A 의 1열 :", A[:, 0])
print("e2 이 가는 곳 :", A @ np.array([0.0, 1.0]), " = A 의 2열 :", A[:, 1])원래 A
[ 2 -1 ]
[ 1 1 ]
기저벡터에게 물어서 만든 것
[ 2 -1 ]
[ 1 1 ]
같은가 : True
e1 이 가는 곳 : [2. 1.] = A 의 1열 : [2. 1.]
e2 이 가는 곳 : [-1. 1.] = A 의 2열 : [-1. 1.]
선형인지 아닌지 판정하기¶
정의는 하나뿐이다. 무작위로 두들겨 보자.
def 선형인가(T, n, 횟수=500, 눈감아=1e-9):
"""무작위 v, w, c, d 로 선형성을 두들겨 본다."""
for _ in range(횟수):
v, w = rng.normal(size=n), rng.normal(size=n)
c, d = rng.normal(), rng.normal()
왼 = T(c * v + d * w)
오 = c * np.asarray(T(v), float) + d * np.asarray(T(w), float)
if not np.allclose(왼, 오, atol=눈감아):
return False
return True회전 = np.array([[0.0, -1.0], [1.0, 0.0]])
후보 = (("행렬 곱하기", lambda v: A @ v),
("90도 회전", lambda v: 회전 @ v),
("y=x 로 투영", lambda v: np.array([v.sum()/2, v.sum()/2])),
("평행이동 +(1,2)", lambda v: v + np.array([1.0, 2.0])),
("길이 재기", lambda v: np.array([np.linalg.norm(v), 0.0])),
("제곱하기", lambda v: v**2))
print(f"{'':>18}{'T(0) = 0 인가':>16}{'선형인가':>12}")
for 이름, T in 후보:
영 = np.allclose(T(np.zeros(2)), 0.0)
print(f"{이름:>18}{str(영):>16}{str(선형인가(T, 2)):>12}") T(0) = 0 인가 선형인가
행렬 곱하기 True True
90도 회전 True True
y=x 로 투영 True True
평행이동 +(1,2) False False
길이 재기 True False
제곱하기 True False
v, w = np.array([3.0, 0.0]), np.array([0.0, 4.0])
평행 = lambda x: x + np.array([1.0, 2.0])
print("T(v+w) =", 평행(v + w))
print("T(v)+T(w) =", 평행(v) + 평행(w), " <- 이동벡터를 두 번 더했다")
print()
print("차원을 하나 올려 보자 (동차좌표)")
H = np.array([[1.0, 0.0, 1.0],
[0.0, 1.0, 2.0],
[0.0, 0.0, 1.0]])
print(show_matrix(H, "H"))
print("(3,0,1) -> ", H @ np.array([3.0, 0.0, 1.0]), " = (4,2) 로 옮겨졌다")
print("R^3 에서는 선형인가 :", 선형인가(lambda x: H @ x, 3))
print()
print("-> 평행이동을 선형으로 만든 것이 아니라,")
print(" 한 차원 위에서 선형인 것을 z=1 평면에서 잘라 본 것이다.")T(v+w) = [4. 6.]
T(v)+T(w) = [5. 8.] <- 이동벡터를 두 번 더했다
차원을 하나 올려 보자 (동차좌표)
H
[ 1 0 1 ]
[ 0 1 2 ]
[ 0 0 1 ]
(3,0,1) -> [4. 2. 1.] = (4,2) 로 옮겨졌다
R^3 에서는 선형인가 : True
-> 평행이동을 선형으로 만든 것이 아니라,
한 차원 위에서 선형인 것을 z=1 평면에서 잘라 본 것이다.
2. 합성은 곱이다¶
의 행렬을 정의대로 만들면 가 나와야 한다.
B = np.array([[0.0, 1.0], [1.0, 0.0]]) # x, y 맞바꾸기
합성 = 행렬만들기(lambda v: A @ (B @ v), np.eye(2), np.eye(2))
print(show_matrix(합성, "합성을 정의대로 만든 것"))
print(show_matrix(A @ B, "A @ B"))
print("같은가 :", np.allclose(합성, A @ B))
print()
print("순서를 바꾸면 :", np.allclose(행렬만들기(lambda v: B @ (A @ v),
np.eye(2), np.eye(2)), B @ A))
print("A@B 와 B@A 가 같은가 :", np.allclose(A @ B, B @ A),
" <- 합성은 순서를 탄다")합성을 정의대로 만든 것
[ -1 2 ]
[ 1 1 ]
A @ B
[ -1 2 ]
[ 1 1 ]
같은가 : True
순서를 바꾸면 : True
A@B 와 B@A 가 같은가 : False <- 합성은 순서를 탄다
3. 미분과 적분의 행렬¶
숫자가 하나도 안 나오는 공간에서 행렬을 만들어 보자. 다항식은 계수 벡터로 나타낸다. 이 이다.
def 미분행렬(n):
"""P_(n-1) -> P_(n-2) 의 미분 행렬. 기저는 1, x, ..., x^(n-1)."""
D = np.zeros((n - 1, n))
for j in range(1, n):
D[j - 1, j] = j # d/dx x^j = j x^(j-1)
return D
def 적분행렬(n):
"""P_(n-2) -> P_(n-1) 의 적분 행렬. 적분상수는 붙이지 않는다."""
S = np.zeros((n, n - 1))
for j in range(n - 1):
S[j + 1, j] = 1.0 / (j + 1) # int x^j dx = x^(j+1)/(j+1)
return SD, S = 미분행렬(4), 적분행렬(4)
print(show_matrix(D, "D (3x4) 미분"))
print(show_matrix(S * 6, "6 S (4x3) 적분 (6배 해서 정수로 보기)"))
print()
p = np.array([5.0, 3.0, 0.0, 2.0]) # 5 + 3x + 2x^3
print("p =", p, " -> 5 + 3x + 2x^3")
print("Dp =", D @ p, " -> 3 + 6x^2 (p' = 3 + 6x^2 이 맞는가)")D (3x4) 미분
[ 0 1 0 0 ]
[ 0 0 2 0 ]
[ 0 0 0 3 ]
6 S (4x3) 적분 (6배 해서 정수로 보기)
[ 0 0 0 ]
[ 6 0 0 ]
[ 0 3 0 ]
[ 0 0 2 ]
p = [5. 3. 0. 2.] -> 5 + 3x + 2x^3
Dp = [3. 0. 6.] -> 3 + 6x^2 (p' = 3 + 6x^2 이 맞는가)
정의대로 만들어도 같은가¶
행렬만들기 로도 만들어 보자. 이번에는 다항식을 실제로 미분하는 함수를 넘긴다.
def 다항식미분(계수):
"""계수 벡터를 미분한다. (numpy 의 polyder 를 쓰지 않고 직접)"""
계수 = np.asarray(계수, dtype=float)
return np.array([j * 계수[j] for j in range(1, len(계수))])정의대로 = 행렬만들기(다항식미분, np.eye(4), np.eye(3))
print(show_matrix(정의대로, "기저 1, x, x^2, x^3 에게 물어 만든 행렬"))
print("D 와 같은가 :", np.allclose(정의대로, D))
print()
print("각 기저벡터가 가는 곳을 하나씩 :")
for j, 이름 in enumerate(["1", "x", "x^2", "x^3"]):
기저 = np.eye(4)[:, j]
print(f" d/dx {이름:>4} -> 계수 {다항식미분(기저)} = D 의 {j+1}열 {D[:, j]}")기저 1, x, x^2, x^3 에게 물어 만든 행렬
[ 0 1 0 0 ]
[ 0 0 2 0 ]
[ 0 0 0 3 ]
D 와 같은가 : True
각 기저벡터가 가는 곳을 하나씩 :
d/dx 1 -> 계수 [0. 0. 0.] = D 의 1열 [0. 0. 0.]
d/dx x -> 계수 [1. 0. 0.] = D 의 2열 [1. 0. 0.]
d/dx x^2 -> 계수 [0. 2. 0.] = D 의 3열 [0. 2. 0.]
d/dx x^3 -> 계수 [0. 0. 3.] = D 의 4열 [0. 0. 3.]
영공간이 상수함수다¶
영 = null_space(D)
print("D 의 영공간 차원 :", 영.shape[1])
print("기저 :", np.round(영.ravel(), 6), " -> 상수함수")
print("정말 죽는가 :", D @ 영.ravel())
print()
print(f"{'공간':>8}{'D 의 크기':>12}{'랭크':>6}{'영공간 차원':>12}")
for n in (4, 6, 8, 12):
Dn = 미분행렬(n)
r = np.linalg.matrix_rank(Dn)
print(f"{'P_' + str(n-1):>8}{str(Dn.shape):>12}{r:>6}{n - r:>12}")
print()
print("-> 차원을 아무리 올려도 죽는 것은 상수 하나뿐이다.")
print(" 그래서 미분 행렬은 어느 차원에서도 정방이 될 수 없다.")D 의 영공간 차원 : 1
기저 : [-1. 0. 0. 0.] -> 상수함수
정말 죽는가 : [0. 0. 0.]
공간 D 의 크기 랭크 영공간 차원
P_3 (3, 4) 3 1
P_5 (5, 6) 5 1
P_7 (7, 8) 7 1
P_11 (11, 12) 11 1
-> 차원을 아무리 올려도 죽는 것은 상수 하나뿐이다.
그래서 미분 행렬은 어느 차원에서도 정방이 될 수 없다.
4. 한쪽으로만 역행렬¶
이 절이 L33의 복선이다.
print(show_matrix(D @ S, "D S"))
print("I_3 인가 :", np.allclose(D @ S, np.eye(3)))
print()
print(show_matrix(S @ D, "S D"))
print("I_4 인가 :", np.allclose(S @ D, np.eye(4)))
print("어디가 다른가 :", np.argwhere(~np.isclose(S @ D, np.eye(4))).tolist(),
" <- 왼쪽 위 한 칸")D S
[ 1 0 0 ]
[ 0 1 0 ]
[ 0 0 1 ]
I_3 인가 : True
S D
[ 0 0 0 0 ]
[ 0 1 0 0 ]
[ 0 0 1 0 ]
[ 0 0 0 1 ]
I_4 인가 : False
어디가 다른가 : [[0, 0]] <- 왼쪽 위 한 칸
print("p 를 미분했다가 적분해 보자")
print(" p =", p)
print(" D p =", D @ p)
print(" S D p =", S @ (D @ p), " <- 상수 5 가 사라졌다")
print()
print("적분했다가 미분하면")
q = np.array([3.0, 0.0, 6.0]) # 3 + 6x^2
print(" q =", q)
print(" S q =", S @ q)
print(" D S q =", D @ (S @ q), " <- 그대로 돌아왔다")p 를 미분했다가 적분해 보자
p = [5. 3. 0. 2.]
D p = [3. 0. 6.]
S D p = [0. 3. 0. 2.] <- 상수 5 가 사라졌다
적분했다가 미분하면
q = [3. 0. 6.]
S q = [0. 3. 0. 2.]
D S q = [3. 0. 6.] <- 그대로 돌아왔다
는 투영행렬이다¶
P = S @ D
print("P^2 = P 인가 :", np.allclose(P @ P, P))
print("P^T = P 인가 :", np.allclose(P.T, P))
print("고윳값 :", np.round(np.linalg.eigvalsh(P), 12), " <- 0 과 1 뿐 (L21)")
print("랭크 :", np.linalg.matrix_rank(P), " = D 의 랭크 :",
np.linalg.matrix_rank(D))
print()
print("무엇으로의 투영인가 : 상수항이 없는 다항식들")
print(" (0,3,0,2) 를 넣으면 :", P @ np.array([0.0, 3.0, 0.0, 2.0]), " 그대로")
print(" (5,0,0,0) 을 넣으면 :", P @ np.array([5.0, 0.0, 0.0, 0.0]), " 통째로 죽는다")P^2 = P 인가 : True
P^T = P 인가 : True
고윳값 : [0. 1. 1. 1.] <- 0 과 1 뿐 (L21)
랭크 : 3 = D 의 랭크 : 3
무엇으로의 투영인가 : 상수항이 없는 다항식들
(0,3,0,2) 를 넣으면 : [0. 3. 0. 2.] 그대로
(5,0,0,0) 을 넣으면 : [0. 0. 0. 0.] 통째로 죽는다
print(show_matrix(np.linalg.pinv(D) * 6, "6 * pinv(D)"))
print(show_matrix(S * 6, "6 * S (우리가 만든 적분 행렬)"))
print("같은가 :", np.allclose(np.linalg.pinv(D), S))
print()
print(f"{'n':>4}{'pinv(D) == S 인가':>20}")
for n in (4, 6, 8, 12):
print(f"{n:>4}{str(np.allclose(np.linalg.pinv(미분행렬(n)), 적분행렬(n))):>20}")
print()
print("L33 에서 이 행렬에 이름을 붙인다.")6 * pinv(D)
[ 0 0 0 ]
[ 6 0 0 ]
[ 0 3 0 ]
[ 0 0 2 ]
6 * S (우리가 만든 적분 행렬)
[ 0 0 0 ]
[ 6 0 0 ]
[ 0 3 0 ]
[ 0 0 2 ]
같은가 : True
n pinv(D) == S 인가
4 True
6 True
8 True
12 True
L33 에서 이 행렬에 이름을 붙인다.
5. 같은 변환, 여러 기저¶
이제 로의 투영을 여러 기저에서 적어 본다.
투영 = lambda v: np.array([v.sum()/2, v.sum()/2])
기저들 = (("표준기저 e1, e2", np.eye(2)),
("직선에 맞춘 (1,1), (1,-1)", np.array([[1.0, 1.0], [1.0, -1.0]])),
("순서를 바꾼 (1,-1), (1,1)", np.array([[1.0, 1.0], [-1.0, 1.0]])),
("길이를 바꾼 (2,2), (3,-3)", np.array([[2.0, 3.0], [2.0, -3.0]])),
("비스듬한 (1,0), (1,1)", np.array([[1.0, 1.0], [0.0, 1.0]])))
for 이름, M in 기저들:
표현 = 행렬만들기(투영, M, M)
print(show_matrix(깔끔(표현), 이름))표준기저 e1, e2
[ 0.5 0.5 ]
[ 0.5 0.5 ]
직선에 맞춘 (1,1), (1,-1)
[ 1 0 ]
[ 0 0 ]
순서를 바꾼 (1,-1), (1,1)
[ 0 0 ]
[ 0 1 ]
길이를 바꾼 (2,2), (3,-3)
[ 1 0 ]
[ 0 0 ]
비스듬한 (1,0), (1,1)
[ 0 0 ]
[ 0.5 1 ]
다섯 개의 행렬이 전부 다르다. 그런데 전부 같은 투영이다. 변하지 않는 것이 무엇인지 보자.
print(f"{'':>28}{'대각합':>10}{'행렬식':>10}{'랭크':>8}{'고윳값':>16}")
for 이름, M in 기저들:
표현 = 행렬만들기(투영, M, M)
고 = np.sort(np.linalg.eigvals(표현).real)
print(f"{이름:>28}{np.trace(표현):>10.4f}{np.linalg.det(표현):>10.4f}"
f"{np.linalg.matrix_rank(표현):>8}{str(np.round(고, 4)):>16}")
print()
print("-> 대각합, 행렬식, 랭크, 고윳값은 좌표계를 타지 않는다.")
print(" 이것들은 변환의 성질이지 기저의 성질이 아니다.") 대각합 행렬식 랭크 고윳값
표준기저 e1, e2 1.0000 0.0000 1 [0. 1.]
직선에 맞춘 (1,1), (1,-1) 1.0000 0.0000 1 [0. 1.]
순서를 바꾼 (1,-1), (1,1) 1.0000 0.0000 1 [0. 1.]
길이를 바꾼 (2,2), (3,-3) 1.0000 0.0000 1 [0. 1.]
비스듬한 (1,0), (1,1) 1.0000 0.0000 1 [-0. 1.]
-> 대각합, 행렬식, 랭크, 고윳값은 좌표계를 타지 않는다.
이것들은 변환의 성질이지 기저의 성질이 아니다.
print("대칭성은 어떤가")
for 이름, M in 기저들:
표현 = 행렬만들기(투영, M, M)
print(f" {이름:>28} : 대칭인가 {np.allclose(표현, 표현.T)}")
print()
print("-> 비스듬한 기저에서는 대칭성이 깨진다.")
print(" '대칭' 은 기저가 직교라는 약속 위에서만 뜻이 있다.")대칭성은 어떤가
표준기저 e1, e2 : 대칭인가 True
직선에 맞춘 (1,1), (1,-1) : 대칭인가 True
순서를 바꾼 (1,-1), (1,1) : 대칭인가 True
길이를 바꾼 (2,2), (3,-3) : 대칭인가 True
비스듬한 (1,0), (1,1) : 대칭인가 False
-> 비스듬한 기저에서는 대칭성이 깨진다.
'대칭' 은 기저가 직교라는 약속 위에서만 뜻이 있다.
6. 의 은 기저 변환 행렬이다¶
Pl = np.array([[0.5, 0.5], [0.5, 0.5]]) # 표준기저에서의 투영
M = np.array([[1.0, 1.0], [1.0, -1.0]]) # 새 기저를 열에 세운다
print("새 좌표 (3,1) 은 어떤 벡터인가")
print(" 3*(1,1) + 1*(1,-1) =", 3*M[:, 0] + 1*M[:, 1])
print(" M @ (3,1) =", M @ np.array([3.0, 1.0]), " 같다")
print()
print("표준 좌표 (4,2) 를 새 좌표로 :", np.linalg.inv(M) @ np.array([4.0, 2.0]))
print()
print(show_matrix(np.linalg.inv(M) @ Pl @ M, "M^-1 P M"))
print(show_matrix(행렬만들기(투영, M, M), "정의대로 만든 것"))
print("같은가 :", np.allclose(np.linalg.inv(M) @ Pl @ M, 행렬만들기(투영, M, M)))새 좌표 (3,1) 은 어떤 벡터인가
3*(1,1) + 1*(1,-1) = [4. 2.]
M @ (3,1) = [4. 2.] 같다
표준 좌표 (4,2) 를 새 좌표로 : [3. 1.]
M^-1 P M
[ 1 0 ]
[ 0 0 ]
정의대로 만든 것
[ 1 0 ]
[ 0 0 ]
같은가 : True
세 단계를 따라가 보자¶
c = np.array([3.0, 1.0]) # 새 좌표
단계 = (("① 새 좌표", c),
("② M 을 곱해 표준 좌표로", M @ c),
("③ P 로 변환 (표준 좌표에서)", Pl @ (M @ c)),
("④ M^-1 로 다시 새 좌표로", np.linalg.inv(M) @ (Pl @ (M @ c))))
for 이름, 값 in 단계:
print(f" {이름:>28} : {np.round(값, 6)}")
print()
print("한 번에 :", np.round((np.linalg.inv(M) @ Pl @ M) @ c, 6)) ① 새 좌표 : [3. 1.]
② M 을 곱해 표준 좌표로 : [4. 2.]
③ P 로 변환 (표준 좌표에서) : [3. 3.]
④ M^-1 로 다시 새 좌표로 : [3. 0.]
한 번에 : [3. 0.]
7. 대각화는 좌표계 선택이었다¶
고유벡터를 기저로 넣으면 행렬만들기 가 대각행렬을 뱉어야 한다.
Ssym = np.array([[3.0, 1.0], [1.0, 3.0]])
값, 벡 = np.linalg.eigh(Ssym)
print("고윳값 :", 값)
print(show_matrix(벡, "고유벡터를 열에 세운 것"))
print()
print(show_matrix(행렬만들기(lambda v: Ssym @ v, np.eye(2), np.eye(2)),
"표준기저에서"))
print(show_matrix(깔끔(행렬만들기(lambda v: Ssym @ v, 벡, 벡)), "고유기저에서"))
print("대각인가 :", np.allclose(행렬만들기(lambda v: Ssym @ v, 벡, 벡),
np.diag(값)))고윳값 : [2. 4.]
고유벡터를 열에 세운 것
[ -0.707 0.707 ]
[ 0.707 0.707 ]
표준기저에서
[ 3 1 ]
[ 1 3 ]
고유기저에서
[ 2 0 ]
[ 0 4 ]
대각인가 : True
# 3x3 로도. L25 의 앵커를 다시 쓴다.
S3 = np.array([[6.0, 0.0, -3.0], [0.0, 6.0, -3.0], [-3.0, -3.0, 9.0]])
값3, 벡3 = np.linalg.eigh(S3)
print("고윳값 :", 값3)
print(show_matrix(깔끔(행렬만들기(lambda v: S3 @ v, 벡3, 벡3)),
"고유기저에서의 S3"))
print()
# 평면 x+y+z=0 으로의 투영도 같은 방법으로
b1, b2, n = (np.array([1.0, -1.0, 0.0]), np.array([1.0, 1.0, -2.0]),
np.array([1.0, 1.0, 1.0]))
Pm = np.eye(3) - np.outer(n, n) / 3.0
좋은기저 = np.column_stack([b1, b2, n])
print(show_matrix(Pm * 3, "표준기저에서의 P (3배)"))
print(show_matrix(깔끔(행렬만들기(lambda v: Pm @ v, 좋은기저, 좋은기저)),
"b1, b2, n 기저에서의 P"))
print("diag(1,1,0) 인가 :",
np.allclose(행렬만들기(lambda v: Pm @ v, 좋은기저, 좋은기저),
np.diag([1.0, 1.0, 0.0])))
print("고윳값 :", np.sort(np.linalg.eigvalsh(Pm)), " <- 살렸다, 살렸다, 죽였다")
print()
민것 = 행렬만들기(lambda v: Pm @ v, 좋은기저, 좋은기저)
print("깔끔() 을 씌우기 전의 (3,3) 성분 :", 민것[2, 2])
print("-> 0 이어야 하는데 1e-16 쯤 남는다. lstsq 가 실수 나눗셈을 하기 때문이다.")
print(" L28 에서 본 그대로다. 컴퓨터의 0 은 '충분히 작은 것' 이다.")고윳값 : [ 3. 6. 12.]
고유기저에서의 S3
[ 3 0 0 ]
[ 0 6 0 ]
[ 0 0 12 ]
표준기저에서의 P (3배)
[ 2 -1 -1 ]
[ -1 2 -1 ]
[ -1 -1 2 ]
b1, b2, n 기저에서의 P
[ 1 0 0 ]
[ 0 1 0 ]
[ 0 0 0 ]
diag(1,1,0) 인가 : True
고윳값 : [0. 1. 1.] <- 살렸다, 살렸다, 죽였다
깔끔() 을 씌우기 전의 (3,3) 성분 : 1.1102230246251563e-16
-> 0 이어야 하는데 1e-16 쯤 남는다. lstsq 가 실수 나눗셈을 하기 때문이다.
L28 에서 본 그대로다. 컴퓨터의 0 은 '충분히 작은 것' 이다.
8. 좋은 기저가 없을 수도 있다¶
"어려운 행렬은 없고 나쁜 기저가 있을 뿐"이라는 말은 절반만 맞다.
R = np.array([[0.0, -1.0], [1.0, 0.0]]) # 90도 회전
print("고윳값 :", np.linalg.eigvals(R), " <- 실수가 아니다")
print()
print("실수 고유벡터가 정말 하나도 없는지 두들겨 보자.")
최대 = 0.0
for _ in range(200000):
x = rng.normal(size=2)
x /= np.linalg.norm(x)
최대 = max(최대, abs(float(x @ (R @ x)))) # 같은 방향이면 |cos| = 1
print(f"단위벡터 20만개 중 |x . Rx| 의 최댓값 : {최대:.3e}")
print("-> 정확히 0 이다. 90도 회전은 모든 벡터를 자기 자신과 수직으로 보낸다.")
print(" 자기 방향을 지키는 벡터가 하나도 없으니 대각이 될 수가 없다.")고윳값 : [0.+1.j 0.-1.j] <- 실수가 아니다
실수 고유벡터가 정말 하나도 없는지 두들겨 보자.
단위벡터 20만개 중 |x . Rx| 의 최댓값 : 0.000e+00
-> 정확히 0 이다. 90도 회전은 모든 벡터를 자기 자신과 수직으로 보낸다.
자기 방향을 지키는 벡터가 하나도 없으니 대각이 될 수가 없다.
print("무작위 기저 2000개로 회전을 적어 봐도 대각이 되는 일은 없다.")
가장대각 = np.inf
for _ in range(2000):
Mr = rng.normal(size=(2, 2))
if abs(np.linalg.det(Mr)) < 1e-6:
continue
표현 = np.linalg.inv(Mr) @ R @ Mr
비대각 = abs(표현[0, 1]) + abs(표현[1, 0])
가장대각 = min(가장대각, 비대각)
print(f"비대각 성분 크기의 최솟값 : {가장대각:.4f} (0 이면 대각화 성공)")
print()
결함 = np.array([[3.0, 1.0], [0.0, 3.0]])
print("결함 행렬 [[3,1],[0,3]] 도 마찬가지다.")
print(" 고윳값 :", np.linalg.eigvals(결함))
print(" 독립인 고유벡터 개수 :",
np.linalg.matrix_rank(np.linalg.eig(결함)[1], tol=1e-8), " (2 여야 대각화 가능)")무작위 기저 2000개로 회전을 적어 봐도 대각이 되는 일은 없다.
비대각 성분 크기의 최솟값 : 2.0008 (0 이면 대각화 성공)
결함 행렬 [[3,1],[0,3]] 도 마찬가지다.
고윳값 : [3.+0.j 3.+0.j]
독립인 고유벡터 개수 : 1 (2 여야 대각화 가능)
9. 눈으로 — 기저를 돌려 가며¶
같은 투영을 기저 각도를 바꿔 가며 적어 보자. 그림의 변환은 하나도 변하지 않는데 숫자만 바뀐다.
각도들 = np.arange(0, 91, 7.5)
프레임, 이름표 = [], []
for 각 in 각도들:
t = np.radians(각)
b1v = np.array([np.cos(t), np.sin(t)])
b2v = np.array([-np.sin(t), np.cos(t)])
Mb = np.column_stack([b1v, b2v])
표현 = np.linalg.inv(Mb) @ Pl @ Mb
s = np.linspace(-2.0, 2.0, 2)
프레임.append([
go.Scatter(x=s, y=s, mode="lines", name="y = x",
line=dict(color="#bbbbbb", width=2)),
go.Scatter(x=[0, b1v[0]], y=[0, b1v[1]], mode="lines+markers",
name="b1", line=dict(color=COLORS["input"], width=5)),
go.Scatter(x=[0, b2v[0]], y=[0, b2v[1]], mode="lines+markers",
name="b2", line=dict(color=COLORS["output"], width=5)),
go.Scatter(x=[0], y=[-1.72], mode="text",
text=[f"[[{표현[0,0]:+.2f}, {표현[0,1]:+.2f}], "
f"[{표현[1,0]:+.2f}, {표현[1,1]:+.2f}]]"],
textfont=dict(size=18), showlegend=False),
])
이름표.append(f"{각:.0f}")
배치 = layout2d("같은 투영, 기저만 돌린다", extent=2.05)
배치["height"] = 600
slider_figure(프레임, 이름표, 배치, prefix="기저 각도 ")슬라이더를 끝까지 밀어 보자. 에서는 표준기저라 이고, 에서는 이다. 그 사이 어디에서도 대각합은 1, 행렬식은 0 이다.
print(f"{'각도':>8}{'대각합':>10}{'행렬식':>10}{'대칭인가':>10}")
for 각 in (0, 15, 30, 45, 60, 90):
t = np.radians(각)
Mb = np.array([[np.cos(t), -np.sin(t)], [np.sin(t), np.cos(t)]])
표현 = np.linalg.inv(Mb) @ Pl @ Mb
print(f"{각:>8}{np.trace(표현):>10.4f}{np.linalg.det(표현):>10.4f}"
f"{str(np.allclose(표현, 표현.T)):>10}")
print()
print("직교 기저만 쓰면 대칭성도 지켜진다. 5절에서 깨졌던 것은 비스듬한 기저였다.") 각도 대각합 행렬식 대칭인가
0 1.0000 0.0000 True
15 1.0000 0.0000 True
30 1.0000 0.0000 True
45 1.0000 -0.0000 True
60 1.0000 0.0000 True
90 1.0000 -0.0000 True
직교 기저만 쓰면 대칭성도 지켜진다. 5절에서 깨졌던 것은 비스듬한 기저였다.
마치며...¶
| 서술 파트의 내용 | 이 노트북의 코드 |
|---|---|
| 번째 열 = 번째 기저벡터의 행선지 | 행렬만들기 열 줄 |
| 선형성의 정의 | 선형인가 로 두들겨 보기 |
| 은 필요조건일 뿐 | 제곱하기가 반례 |
| 평행이동은 아핀 | 동차좌표로 차원을 올리면 선형 |
| 합성이 곱 | 정의대로 만들면 |
| 미분은 행렬 | 행렬만들기(다항식미분, ...) |
| 영공간이 상수 | 차원을 올려도 늘 1차원 |
| , | 상수 5가 사라진다 |
| 가 투영 | , 고윳값 |
| 의 정체 | pinv(D) 와 정확히 같다 |
| 같은 변환, 다른 행렬 | 기저 다섯 벌로 다섯 행렬 |
| 변하지 않는 것 | 대각합·행렬식·랭크·고윳값 |
| 대칭성은 좌표계를 탄다 | 비스듬한 기저에서 깨진다 |
| 은 기저 변환 행렬 | 세 단계를 따라가 보기 |
| 대각화 = 좌표계 선택 | 고유기저를 넣으면 |
| 좋은 기저가 없을 수도 | 회전은 무작위 2000벌로도 실패 |
더 해 볼 것¶
행렬만들기에 정규직교가 아닌 기저를 넣으면lstsq가 무엇을 하는가?np.linalg.solve로 바꿔도 같은 답이 나오는가? 기저가 정방이 아닐 때는 어떤가?의 기저를 대신 르장드르 다항식으로 바꿔 미분 행렬을 만들어 보자. 모양이 달라지는가? 영공간의 차원은?
도 선형변환이다. 에서 행렬을 만들어 보자. 이 행렬의 고윳값은 무엇이고 고유벡터는 어떤 다항식인가?
8절의 회전을 복소수 기저로 적어 보자.
np.linalg.eig가 준 복소 고유벡터를 기저로 넣으면 대각이 되는가? 실수 세계에서만 안 되는 것이었나?
다음 강의에서는 기저를 잘 고르면 무슨 이득이 있는지 본다. 답은 압축이다.