L1 서술 파트에서 Row picture와 Column picture를 살펴보았다.
이 노트북에서는 같은 내용을 numpy 로 직접 계산하고, 그림을 회전시켜 가며 확인해 보도록 하자.
서술 파트의 그림은 한 각도에서 찍은 정지 화면이었다. 3차원 그림은 각도에 따라 다르게 보이므로, 여기서는 마우스로 돌려 볼 수 있는 형태로 다시 그린다.
이 노트북에서 확인할 것은 다음 네 가지이다.
| 서술 파트의 내용 | 여기서 확인하는 방법 |
|---|---|
| 각 행은 직선 또는 평면이다 | 2차원 직선과 3차원 평면을 직접 그려 본다 |
| 각 열은 벡터이고, 해는 결합 계수이다 | 계수를 슬라이더로 바꿔 가며 도달점을 본다 |
| 는 열들의 선형결합이다 | 두 가지 계산 방법을 각각 함수로 짜서 대조한다 |
| 열이 한 평면에 놓이면 해가 없을 수 있다 | 특이행렬을 만들어 두 관점에서 모두 확인한다 |
셀을 하나씩 실행하면서 읽고, 숫자를 바꿔 가며 결과가 어떻게 달라지는지 확인해 보자.
0. 준비¶
모든 강의는 이 셀로 시작한다. linalg_viz 는 이 교재의 공통 시각화 모듈이며 저장소 안에 들어 있다
(pip install -r requirements.txt 가 함께 설치한다).
함수 이름은 영문이 기준이고, 한글 별칭(화살표, 평면, 무대 등)도 같이 제공한다.
같은 함수이므로 어느 쪽을 써도 된다.
import numpy as np
import plotly.graph_objects as go
from linalg_viz import (COLORS, arrow, arrow2d, plane, chain,
layout2d, layout3d, slider_figure, spin_figure)
np.set_printoptions(precision=3, suppress=True)
print("numpy", np.__version__)numpy 2.5.2
1. 2×2 시스템¶
서술 파트에서 다룬 시스템이다.
계수만 모아 행렬 A2 로, 우변은 벡터 b2 로 적는다. numpy 에서 A2[i] 는 번째 행이고
A2[:, j] 는 번째 열이다. 이 구분이 이번 강의의 전부라고 해도 된다.
A2 = np.array([[ 2, -1],
[-1, 2]], dtype=float)
b2 = np.array([0, 3], dtype=float)
x2 = np.linalg.solve(A2, b2)
print("A2 =\n", A2)
print("b2 =", b2)
print("해 =", x2)A2 =
[[ 2. -1.]
[-1. 2.]]
b2 = [0. 3.]
해 = [1. 2.]
1-1. Row picture¶
번째 행 와 우변 는 직선 를 나타낸다. 에 대해 정리하면 이므로 이대로 그리면 된다.
t = np.linspace(-2, 4, 200)
traces = []
for i, (color, name) in enumerate([(COLORS["input"], "2x - y = 0"),
(COLORS["second"], "-x + 2y = 3")]):
a, c = A2[i], b2[i] # i번째 '행' 과 우변
traces.append(go.Scatter(x=t, y=(c - a[0] * t) / a[1], mode="lines",
line=dict(color=color, width=3), name=name))
traces.append(go.Scatter(x=[x2[0]], y=[x2[1]], mode="markers+text",
marker=dict(size=12, color="red"),
text=[f"({x2[0]:.0f}, {x2[1]:.0f})"],
textposition="top center", name="해"))
go.Figure(data=traces, layout=layout2d("Row picture : 두 직선의 교점", extent=4))두 직선의 교점이 해이다. 여기까지는 서술 파트의 첫 번째 그림과 같은 내용이다.
1-2. Column picture¶
이번에는 같은 행렬을 열로 나눈다. 열벡터에 계수를 곱해 꼬리에 꼬리를 물고 이어 붙이면 어디에 도달하는지 볼 수 있다.
col1, col2 = A2[:, 0], A2[:, 1]
print("col1 =", col1)
print("col2 =", col2)
print("b2 =", b2)
print()
print("해의 계수로 결합하면:", x2[0] * col1 + x2[1] * col2)col1 = [ 2. -1.]
col2 = [-1. 2.]
b2 = [0. 3.]
해의 계수로 결합하면: [0. 3.]
x, y = x2
end1 = x * col1
end2 = end1 + y * col2
traces = arrow2d([0, 0], end1, color=COLORS["input"], name=f"{x:.0f} x col1")
traces += arrow2d(end1, end2, color=COLORS["second"], name=f"{y:.0f} x col2")
traces += arrow2d([0, 0], b2, color=COLORS["output"], name="b", dashed=True)
go.Figure(data=traces, layout=layout2d("Column picture : 두 열을 결합해 b 에 도달", extent=4))파란 화살표 끝에서 초록 화살표가 출발해 주황 점선의 끝에 정확히 닿는다.
위 셀에서 x, y = x2 를 x, y = 2.0, 1.0 으로 바꿔 실행해 보자. 화살표의 끝이 에서
벗어나는 것을 볼 수 있다. 벗어난 정도가 곧 그 계수가 해가 아니라는 뜻이다.
A = np.array([[1, 2, 3],
[2, 5, 2],
[6, -3, 1]], dtype=float)
b = np.array([6, 4, 2], dtype=float)
x3 = np.linalg.solve(A, b)
print("해 =", x3)
print("det(A) =", np.linalg.det(A))
print("A @ 해 =", A @ x3, " (b 와 같아야 한다)")해 = [-0. -0. 2.]
det(A) = -77.00000000000001
A @ 해 = [6. 4. 2.] (b 와 같아야 한다)
서술 파트에서 이 시스템의 해가 라고 하였는데, 출력에는 -0. 이 보인다.
np.linalg.solve 는 소거 과정을 부동소수점으로 계산하므로 결과에 아주 작은 반올림 오차가 남는다.
잠시 뒤 3절에서는 같은 값이 -1.48e-16 처럼 보이기도 한다. 수학적으로는 0이지만 계산 결과는
정확히 0이 아니다.
따라서 두 값이 같은지 확인할 때는 == 가 아니라 np.allclose 를 쓴다. 이 교재에서 계속 그렇게 한다.
각 행 에 대해 는 법선벡터가 인 평면이다. 이 평면은 원점을 지나지 않으므로 평행이동해야 하는데, 원점에서 가장 가까운 평면 위의 점은
이다. plane(normal=..., through=...) 에 그대로 넘기면 된다.
PLANE_COLORS = ["#17becf", "#9467bd", "#8c564b"]
def three_planes(A, b, extent=4):
"""각 행을 법선벡터로 갖는 평면 세 장을 만든다."""
out = []
for i in range(3):
n = A[i] # i번째 행 = 법선벡터
p = n * b[i] / (n @ n) # 평면 위의 한 점
out.append(plane(normal=n, through=p, extent=extent,
color=PLANE_COLORS[i], opacity=0.35, name=f"식 {i+1}"))
return outtraces = three_planes(A, b)
traces.append(go.Scatter3d(x=[x3[0]], y=[x3[1]], z=[x3[2]], mode="markers+text",
marker=dict(size=7, color="red"),
text=["해"], textposition="top center", name="해"))
spin_figure(traces, title="Row picture : 세 평면의 교점", extent=4)3. 3×3 시스템의 Column picture¶
같은 시스템을 열로 묶으면 다음과 같다.
아래 슬라이더는 를 바꿔 가며 결합의 도달점을 보여 준다. 와 는 해의 값으로 고정해 두었다.
columns = [A[:, 0], A[:, 1], A[:, 2]]
candidates = np.linspace(x3[0] - 3, x3[0] + 3, 25) # 가운데(13번째)가 해
frames_data, labels = [], []
for x1 in candidates:
traces, end = chain(columns, [x1, x3[1], x3[2]])
traces += arrow([0, 0, 0], b, color=COLORS["output"], name="b")
frames_data.append(traces)
labels.append(f"{x1:.2f}")
slider_figure(frames_data, labels,
layout3d("Column picture : 열을 결합해 b 에 도달", extent=9),
prefix="x = ", initial=12)print("해의 첫 성분 x =", x3[0], " (이론값은 0)")
reached = x3[0] * columns[0] + x3[1] * columns[1] + x3[2] * columns[2]
print("그때 도달점 =", reached)
print("b =", b)
print("일치하는가 :", np.allclose(reached, b))해의 첫 성분 x = -1.4802973661668753e-16 (이론값은 0)
그때 도달점 = [6. 4. 2.]
b = [6. 4. 2.]
일치하는가 : True
서술 파트에서 본 것처럼 해는 이다. 즉 첫 번째와 두 번째 열은 쓰지 않고 세 번째 열만 2배 하면 가 된다. 아래에서 직접 확인해 보자.
print("2 x col3 =", 2 * A[:, 2])
print("b =", b)2 x col3 = [6. 4. 2.]
b = [6. 4. 2.]
4. 를 두 가지 방법으로 계산하기¶
서술 파트에서 를 보는 두 가지 방법을 정리하였다. 하나는 행과 벡터의 내적으로 성분을 하나씩 만드는 방법이고, 다른 하나는 열들의 선형결합으로 결과 벡터를 한 번에 만드는 방법이다.
두 방법을 각각 함수로 짜서 결과가 같은지 확인해 보자.
def by_columns(A, x):
"""A x = x1*a1 + x2*a2 + ... (열들의 선형결합)"""
out = np.zeros(A.shape[0])
for j in range(A.shape[1]):
out = out + x[j] * A[:, j] # j번째 '열' 에 j번째 계수
return out
def by_rows(A, x):
"""(A x)_i = i번째 행과 x의 내적"""
return np.array([A[i, :] @ x for i in range(A.shape[0])])x_test = np.array([1.0, -2.0, 0.5])
print("열 방식 :", by_columns(A, x_test))
print("행 방식 :", by_rows(A, x_test))
print("numpy :", A @ x_test)
print()
print("셋이 같은가 :",
np.allclose(by_columns(A, x_test), A @ x_test)
and np.allclose(by_rows(A, x_test), A @ x_test))열 방식 : [-1.5 -7. 12.5]
행 방식 : [-1.5 -7. 12.5]
numpy : [-1.5 -7. 12.5]
셋이 같은가 : True
결과는 같다. 같은 계산을 다르게 정리한 것이므로 당연하다.
다만 by_rows 는 결과의 성분을 하나씩 만들어 내는 반면, by_columns 는 결과 벡터를
통째로 만들어 낸다. by_columns 의 for 문 한 바퀴가 3절에서 이어 붙인 화살표 하나에
그대로 대응한다. 이 교재가 열 방식을 정의로 삼는 이유이다.
A_sing = np.array([[1, 2, 3],
[2, 5, 7],
[6, -3, 3]], dtype=float)
print("col1 + col2 =", A_sing[:, 0] + A_sing[:, 1])
print("col3 =", A_sing[:, 2])
print()
print("det =", np.linalg.det(A_sing))
print("rank =", np.linalg.matrix_rank(A_sing))col1 + col2 = [3. 7. 3.]
col3 = [3. 7. 3.]
det = 0.0
rank = 2
행렬식이 0이고 랭크가 3이 아니라 2이다. 즉 세 열이 3차원을 다 채우지 못하고 평면 하나 안에 들어 있다.
이 경우 np.linalg.solve 는 해를 구하지 못한다.
try:
np.linalg.solve(A_sing, b)
except np.linalg.LinAlgError as err:
print("풀 수 없다 :", err)풀 수 없다 : Singular matrix
5-1. 가 그 평면 위에 있는지 확인하기¶
두 열의 외적을 구하면 평면의 법선벡터가 된다. 법선과 의 내적이 0이면 는 그 평면 위에 있다.
n = np.cross(A_sing[:, 0], A_sing[:, 1]) # 평면의 법선벡터
print("법선 n =", n)
print()
for name, v in [("col1", A_sing[:, 0]), ("col2", A_sing[:, 1]),
("col3", A_sing[:, 2]), ("b", b)]:
print(f"n . {name:<5} = {n @ v:>8.2f}")법선 n = [-36. 15. 1.]
n . col1 = 0.00
n . col2 = 0.00
n . col3 = 0.00
n . b = -154.00
세 열은 모두 법선과의 내적이 0이므로 같은 평면 위에 있다. 반면 는 0이 아니므로 그 평면 밖에 있다. 그래서 해가 존재하지 않는다.
traces = [plane(spans=[A_sing[:, 0], A_sing[:, 1]], extent=7,
color=COLORS["colspace"], opacity=0.35, name="열들이 만드는 평면")]
traces += arrow([0, 0, 0], A_sing[:, 0], color=COLORS["input"], name="col1")
traces += arrow([0, 0, 0], A_sing[:, 1], color=COLORS["second"], name="col2")
traces += arrow([0, 0, 0], A_sing[:, 2], color=COLORS["third"], name="col3")
traces += arrow([0, 0, 0], b, color=COLORS["output"], name="b")
spin_figure(traces, title="열은 평면 안에, b 는 평면 밖에", extent=8)5-2. 를 평면 안으로 옮기면¶
를 평면 위의 벡터로 바꾸면 해가 생긴다. 가장 간단한 예로 첫 번째 열 자신을 우변으로 두면 이 해가 된다. 그런데 세 번째 열이 앞의 두 열로 만들어지므로, 같은 곳에 도달하는 결합 방법이 하나가 아니다.
b_in = A_sing[:, 0].copy() # b 를 col1 로 두면 평면 위에 있다
print("n . b_in =", n @ b_in, " (0 이므로 평면 위)")
print()
for x_try in ([1, 0, 0], [0, -1, 1], [2, 1, -1]):
x_try = np.array(x_try, dtype=float)
print(f"x = {x_try} -> A x = {A_sing @ x_try}")n . b_in = 0.0 (0 이므로 평면 위)
x = [1. 0. 0.] -> A x = [1. 2. 6.]
x = [ 0. -1. 1.] -> A x = [1. 2. 6.]
x = [ 2. 1. -1.] -> A x = [1. 2. 6.]
서로 다른 세 개의 가 모두 같은 를 만든다. 즉 해가 무수히 많다.
정리하면 특이행렬의 경우 가 열들이 만드는 평면 밖에 있으면 해가 없고, 평면 위에 있으면 해가 무수히 많다. 해가 정확히 하나인 경우는 없다.
마치며...¶
이번 실습에서 확인한 것을 정리하면 다음과 같다.
| 서술 파트의 내용 | 이 노트북의 코드 |
|---|---|
| 각 행은 직선 또는 평면이다 | plane(normal=A[i], through=p) |
| 각 열은 벡터이다 | A[:, j] |
| 는 열들의 선형결합이다 | by_columns(A, x) 의 for 문 |
| 해는 결합 계수이다 | 3절의 슬라이더 |
| 열이 한 평면에 있으면 해가 없을 수 있다 | 5절의 n @ b |
더 해 볼 것¶
A의 세 번째 행(열이 아니라)을 앞의 두 행의 합으로 바꿔 보자. 행렬식과 랭크는 어떻게 되는가? Row picture와 Column picture 중 어느 쪽이 먼저 이상해 보이는가?2×2로 돌아가
A2 = [[2, -1], [-4, 2]]로 바꿔 보자. 두 직선은 어떻게 배치되는가? 두 열벡터는 어떤 관계인가?by_columns를for문 없이 한 줄로 써 보자.A @ x는 쓰지 않는다. 힌트:A * x가 무엇을 하는지 먼저 확인해 보자.
다음 강의에서는 연립방정식을 기계적으로 푸는 방법인 소거법(Elimination)을 다룬다.