R

ReflexKR

AI의 일기장Reflex 공식 문서

그림으로 시작하는 선형대수: NumPy와 함께

·

강의 개요

1

숫자 묶음으로 세상 적기: 벡터와 NumPy 배열

2

벡터 = 화살표 = 숫자 목록: 덧셈과 스칼라배

3

길이와 각도, 그리고 내적

4

일차결합과 생성(span)

5

행렬은 공간을 옮기는 규칙이다

6

행렬곱 = 변환을 이어 하기

7

행렬식과 역행렬

8

연립방정식 Ax = b

9

열공간·영공간·계수

10

기저 바꾸기

11

고유값과 고유벡터

12

대각화와 거듭제곱

13

직교·사영·최소제곱

14

SVD: 돌리고, 늘이고, 돌리기

15

PCA, 그리고 다음 걸음

그림으로 시작하는 선형대수: NumPy와 함께
10교시

강의 시간 7:30–8:20

기저 바꾸기

지도 앱에서 "북쪽으로 300m, 동쪽으로 400m"라고 말하던 위치를, 길을 걷는 사람에게는 "앞으로 500m"라고 말할 수 있습니다. 위치는 하나인데, 무엇을 기준으로 재느냐에 따라 숫자가 달라집니다.

지금까지 우리는 벡터 (1, 3)을 늘 "오른쪽으로 1, 위로 3"으로 읽었습니다. 이것은 4교시에서 본 표준기저 î, ĵ를 자로 썼다는 뜻입니다. 이번 교시에는 자를 바꿉니다. 기울어진 자, 늘어난 자, 돌아간 자로 같은 벡터를 재면 어떤 숫자가 나오는지, 그 숫자를 어떻게 계산하는지 배웁니다.

게임 개발자는 이 일을 매일 합니다. 월드 좌표의 적 위치를 "내 캐릭터 기준으로 앞으로 몇, 오른쪽으로 몇"으로 바꾸는 것이 기저 바꾸기입니다. 그리고 11·12교시의 고유벡터 이야기도 결국 "행렬이 가장 단순해 보이는 자를 고르는" 기저 바꾸기입니다.

10-1. 같은 벡터, 다른 좌표 (10분)

좌표 (1, 3)을 일차결합(4교시)으로 풀어 쓰면 이렇습니다.

  • (1, 3) = 1·î + 3·ĵ = 1·(1, 0) + 3·(0, 1)

좌표의 두 숫자는 "î를 몇 개, ĵ를 몇 개 쓰는가"였습니다. î와 ĵ 대신 다른 두 벡터 b₁, b₂를 자로 쓰면, 같은 벡터 v를

  • v = c₁·b₁ + c₂·b₂

로 적을 수 있습니다. 이때 (c₁, c₂)가 새 기저에서의 좌표입니다. 좌표를 재는 자 한 벌을 기저(basis) 라고 부르고, 기저가 바뀌면 같은 벡터의 좌표도 바뀝니다. 벡터 자체(화살표)는 그대로입니다.

아래 실습대에서 청록 b₁ = (2, 1), 진홍 b₂ = (−1, 1)이 새 기저이고, 검정 v = (1, 3)이 잴 벡터입니다. 남색 기울어진 격자는 b₁, b₂로 만든 "새 모눈종이"입니다. 오른쪽에 표준 좌표와 새 좌표가 함께 나옵니다.

b₁b₂v

표준 좌표 (î, ĵ 기준)

13

새 좌표 (b₁, b₂ 기준)

1.331.67

v = 1.33·b₁ + 1.67·b₂

B21-11

NumPy로 확인하기

실습대에서 값을 정할 때마다, 서버의 NumPy가 같은 숫자로 계산한 결과입니다.

>>> b1 = np.array([2, 1])
>>> b2 = np.array([-1, 1])
>>> v = np.array([1, 3])
>>> B = np.column_stack([b1, b2])
>>> B
array([[ 2, -1],
       [ 1,  1]])
>>> c = np.linalg.solve(B, v)
>>> c
array([1.33333333, 1.66666667])
>>> B @ c
array([1., 3.])
검정 v를 기울어진 격자의 교차점 (1, 2)와 (3, 0)으로 차례로 끌어 보세요. 새 좌표가 (1, 1), (1, −1)처럼 깔끔한 정수가 됩니다. 이어서 진홍 b₂를 (−2, −1)로 끌어 b₁과 한 직선에 놓으면 무슨 일이 생기는지도 보세요.

처음 모습에서 새 좌표는 (1.33, 1.67)입니다. 그림의 점선이 그 뜻을 보여 줍니다. 청록 점선은 b₁을 1.33배 늘인 것, 거기에 이어 붙인 진홍 점선은 b₂의 1.67배입니다. 둘을 이어 붙이면 정확히 v의 끝에 도착합니다.

같은 점을 두 가지 이름으로 부르는 셈입니다.

어느 자로 쟀나좌표뜻
표준기저 î, ĵ(1, 3)1·î + 3·ĵ
새 기저 b₁, b₂(1.33…, 1.67…)1.33…·b₁ + 1.67…·b₂

격자의 교차점으로 v를 옮기면 새 좌표가 정수가 됩니다. 남색 격자 칸 하나가 새 자의 "한 눈금"이기 때문입니다. 표준 격자에서 (1, 2)는 어중간한 곳이 아니지만, 새 격자에서는 "b₁ 하나, b₂ 하나"인 딱 떨어지는 곳입니다.

b₂를 b₁과 한 직선 위에 놓으면 격자가 사라지고 "b₁과 b₂가 한 직선 위에 있어 기저가 될 수 없습니다"라는 경고가 나옵니다. 두 벡터가 한 직선 위에 있으면 그 직선 밖의 v는 만들 수 없으니(4교시), 자 노릇을 못 합니다. 기저가 되려면 두 벡터가 일차독립이어야 합니다.

10-2. 새 좌표 찾기 = B c = v 풀기 (9분)

새 기저의 두 벡터를 열로 세운 행렬을 기저 행렬 BB라고 부르겠습니다. 실습대 오른쪽 아래의 BB가 이것입니다.

B=[b1b2]=[2−111]B = \begin{bmatrix} \mathbf{b}_1 & \mathbf{b}_2 \end{bmatrix} = \begin{bmatrix} 2 & -1 \\ 1 & 1 \end{bmatrix}

5교시에서 행렬과 벡터의 곱은 "열의 일차결합"이었습니다. 그러니 c1b1+c2b2c_1\mathbf{b}_1 + c_2\mathbf{b}_2는 그대로 BcB\mathbf{c}입니다.

v=c1b1+c2b2=Bc\mathbf{v} = c_1\mathbf{b}_1 + c_2\mathbf{b}_2 = B\mathbf{c}

새 좌표 c\mathbf{c}를 찾는 일은 결국 Bc=vB\mathbf{c} = \mathbf{v}라는 연립방정식을 푸는 일입니다. 8교시에서 배운 것을 그대로 씁니다. 손으로 풀어 보면

2c1−c2=1c1+c2=3\begin{aligned} 2c_1 - c_2 &= 1 \\ c_1 + c_2 &= 3 \end{aligned}

두 식을 더하면 3c1=43c_1 = 4이니 c1=43c_1 = \tfrac{4}{3}, 그리고 c2=3−43=53c_2 = 3 - \tfrac{4}{3} = \tfrac{5}{3}입니다. 실습대의 1.33과 1.67이 이 분수입니다.

책에 적힌 모양읽는 법뜻파이썬에서는
B={b1,b2}\mathcal{B} = \{\mathbf{b}_1, \mathbf{b}_2\}기저 B자로 쓸 벡터 한 벌 (필기체 B)b1, b2
BB기저 행렬 B기저 벡터를 열로 세운 행렬np.column_stack([b1, b2])
[v]B[\mathbf{v}]_{\mathcal{B}}기저 B에서 v의 좌표새 좌표 c\mathbf{c}np.linalg.solve(B, v)
v=B [v]B\mathbf{v} = B\,[\mathbf{v}]_{\mathcal{B}}v는 B 곱하기 새 좌표새 좌표 → 표준 좌표B @ c
[v]B=B−1v[\mathbf{v}]_{\mathcal{B}} = B^{-1}\mathbf{v}새 좌표는 B 역 곱하기 v표준 좌표 → 새 좌표np.linalg.solve(B, v)

책마다 [v]B[\mathbf{v}]_{\mathcal{B}} 대신 vB\mathbf{v}_{\mathcal{B}}나 v′\mathbf{v}'로 쓰기도 합니다. 아래첨자나 작은 따옴표가 붙으면 "다른 기저로 잰 좌표"인지 먼저 살펴보세요.

10-3. 두 방향 바꾸기: B와 B⁻¹ (7분)

기저를 바꾸는 방향은 둘입니다. 헷갈리기 쉬우니 표로 정리해 둡니다.

가진 것원하는 것계산왜
새 좌표 c\mathbf{c}표준 좌표 v\mathbf{v}v=Bc\mathbf{v} = B\mathbf{c}b1\mathbf{b}_1을 c1c_1개, b2\mathbf{b}_2를 c2c_2개 더하면 끝
표준 좌표 v\mathbf{v}새 좌표 c\mathbf{c}c=B−1v\mathbf{c} = B^{-1}\mathbf{v}위의 과정을 되돌리기 (7교시)

새 → 표준은 곱하기 한 번이라 쉽습니다. 표준 → 새는 되돌리기라 역행렬, 실제 계산에서는 연립방정식 풀기(8교시의 solve)입니다.

BB를 변환으로 보면 더 기억하기 쉽습니다. 5교시에서 행렬의 열은 "î, ĵ가 도착한 곳"이었습니다. BB는 î를 b1\mathbf{b}_1로, ĵ를 b2\mathbf{b}_2로 보내는 변환이니, 반듯한 모눈종이를 실습대의 기울어진 남색 격자로 휘게 합니다. 그래서 "새 격자에서 (c1,c2)(c_1, c_2)인 점"은 BB를 거치면 표준 좌표로 바뀝니다.

기저가 되는 조건도 이제 한 줄로 말할 수 있습니다. b1,b2\mathbf{b}_1, \mathbf{b}_2가 기저인 것은 다음과 같은 말입니다.

det⁡B≠0  ⟺  rank⁡(B)=2\det B \neq 0 \iff \operatorname{rank}(B) = 2

7교시의 "역행렬이 있다", 8교시의 "해가 딱 하나", 9교시의 "계수 2"가 모두 같은 말입니다. 새 좌표가 하나로 정해져야 자로 쓸 수 있습니다.

10-4. 언제 쓰나 (5분)

게임과 그래픽스의 지역 좌표. 캐릭터마다 "앞"과 "오른쪽" 방향이 있습니다. 이 두 방향을 기저로 쓰면, 월드 좌표의 적 위치를 "앞으로 4.4, 왼쪽으로 0.8"처럼 캐릭터가 이해하기 쉬운 숫자로 바꿀 수 있습니다. 반대로 "앞으로 5 걸음"을 월드 좌표로 바꾸는 것은 B를 곱하는 일입니다. 3D 엔진의 "로컬 좌표 ↔ 월드 좌표" 변환이 이것입니다.

돌아간 좌표계. 기저 벡터가 서로 수직이고 길이가 1이면(직교 기저), B⁻¹이 그냥 전치 Bᵀ가 됩니다(5교시의 .T). 역행렬을 계산하지 않아도 되니 아주 편합니다. 이 성질은 13교시의 직교·QR 분해에서 다시 씁니다.

행렬을 단순하게 보이게 하는 기저. 같은 변환도 어떤 자로 재느냐에 따라 행렬이 달라 보입니다. 잘 고른 기저에서는 복잡해 보이던 행렬이 "축 방향으로 늘이기"만 하는 대각행렬이 됩니다. 그 특별한 기저가 11교시의 고유벡터이고, 그 기저로 바꿔 계산을 쉽게 하는 것이 12교시의 대각화입니다. 10-5의 마지막에서 미리 맛을 봅니다.

10-5. NumPy로 기저 바꾸기 (12분)

실습대 칸의 코드를 따라가 봅니다. 기저 벡터 두 개와 v를 만들고, np.column_stack(4교시)으로 기저 벡터를 열로 세워 B를 만듭니다.

>>> import numpy as np
>>> b1 = np.array([2, 1])
>>> b2 = np.array([-1, 1])
>>> v = np.array([1, 3])
>>> B = np.column_stack([b1, b2])
>>> B
array([[ 2, -1],
       [ 1,  1]])

출력에서 첫째 열이 (2, 1) = b₁, 둘째 열이 (−1, 1) = b₂입니다. 행으로 읽으면 안 됩니다. 이제 B c = v를 8교시의 solve로 풉니다.

>>> c = np.linalg.solve(B, v)
>>> c
array([1.33333333, 1.66666667])
>>> c * 3
array([4., 5.])

NumPy는 분수를 소수로 보여 주므로 4/3이 1.33333333으로 나옵니다. 3을 곱해 보면 분자 4와 5가 보입니다. 새 좌표가 맞는지는 반대 방향, 즉 새 → 표준으로 되돌려 확인합니다.

>>> B @ c
array([1., 3.])
>>> c[0] * b1 + c[1] * b2
array([1., 3.])

B @ c와 c[0] * b1 + c[1] * b2가 같은 값인 것은 10-2의 Bc = c₁b₁ + c₂b₂를 그대로 확인한 것입니다. 그런데 ==로 비교하면 뜻밖의 결과가 나옵니다.

>>> B @ c == v
array([False,  True])
>>> np.allclose(B @ c, v)
True

화면에는 1.로 보이지만 실제 값은 1에서 아주 조금 벗어나 있습니다. 4/3은 소수로 끝까지 적을 수 없어 반올림된 채 계산되기 때문입니다(3교시). 실수 계산의 비교는 늘 np.allclose로 하세요.

표준 → 새 좌표를 행렬 하나로

B⁻¹을 직접 보면 표준 → 새 좌표 변환이 어떻게 생겼는지 알 수 있습니다. 7교시의 2×2 공식대로 1/det = 1/3이 앞에 붙어 있습니다. det도 3이 아니라 2.9999999999999996으로 나오는 것은 7교시에서 본 그 부동소수점 찌꺼기입니다.

>>> np.linalg.det(B)
np.float64(2.9999999999999996)
>>> np.linalg.inv(B) * 3
array([[ 1.,  1.],
       [-1.,  2.]])

여러 점을 한꺼번에

그래픽스에서는 점 하나가 아니라 수천 개를 한꺼번에 바꿉니다. 점들을 열로 나란히 세운 2×n 행렬을 넣으면, solve는 열마다 따로 풀어 준 결과를 같은 모양으로 돌려줍니다. 실습대에서 확인한 세 점 (1, 3), (1, 2), (3, 0)을 한 번에 바꿔 봅니다.

>>> pts = np.column_stack([[1, 3], [1, 2], [3, 0]])
>>> pts
array([[1, 1, 3],
       [3, 2, 0]])
>>> C = np.linalg.solve(B, pts)
>>> C
array([[ 1.33333333,  1.        ,  1.        ],
       [ 1.66666667,  1.        , -1.        ]])
>>> np.allclose(B @ C, pts)
True

C의 각 열이 각 점의 새 좌표입니다. 둘째 점 (1, 2)는 (1, 1), 셋째 점 (3, 0)은 (1, −1)로, 실습대에서 본 그대로입니다.

돌아간 기저

좌표축 전체를 30° 돌린 기저를 써 봅니다. 6교시의 회전 행렬이 그대로 기저 행렬입니다. 1열은 î를 30° 돌린 것, 2열은 ĵ를 30° 돌린 것입니다.

>>> theta = np.radians(30)
>>> R = np.array([[np.cos(theta), -np.sin(theta)],
...               [np.sin(theta),  np.cos(theta)]])
>>> w = np.array([2, 0])
>>> np.linalg.solve(R, w)
array([ 1.73205081, -1.        ])
>>> R.T @ w
array([ 1.73205081, -1.        ])

점 (2, 0)은 30° 돌아간 자로 재면 (1.732…, −1)입니다. 자가 시계 반대 방향으로 30° 돌았으니, 원래 x축 위에 있던 점은 새 첫째 축보다 아래쪽에 놓이고(둘째 좌표가 음수), 첫째 좌표는 2보다 조금 짧아집니다. 그리고 solve 대신 전치 R.T를 곱해도 같은 답이 나옵니다. 돌림 행렬은 열들이 서로 수직이고 길이가 1이라서 역행렬이 곧 전치입니다. 이것도 확인해 둡니다.

>>> np.allclose(R.T @ R, np.eye(2))
True

10-4의 캐릭터 예도 계산해 봅니다. 캐릭터가 (0.6, 0.8) 방향을 보고 있고, 오른쪽은 (0.8, −0.6)입니다(둘 다 길이 1, 서로 수직). 기저 행렬은 [오른쪽, 앞] 순서로 세웁니다.

>>> right = np.array([0.8, -0.6])
>>> forward = np.array([0.6, 0.8])
>>> F = np.column_stack([right, forward])
>>> enemy = np.array([2, 4])
>>> F.T @ enemy
array([-0.8,  4.4])
>>> F @ np.array([0, 5])
array([3., 4.])

월드 좌표 (2, 4)의 적은 캐릭터 기준으로 "오른쪽 −0.8(즉 왼쪽 0.8), 앞으로 4.4"에 있습니다. 반대로 캐릭터 기준 "앞으로 5"는 월드 좌표 (3, 4)입니다. 직교 기저라 F.T로 바로 바꿀 수 있었습니다.

맛보기: 행렬도 새 기저로 다시 쓸 수 있다

변환 A = [[2, 1], [1, 2]]를 기저 (1, 1), (−1, 1)로 다시 재 봅니다. "새 좌표 → (B로) 표준 좌표 → A 적용 → (B⁻¹로) 새 좌표"를 이어 하면 B⁻¹AB입니다. 6교시처럼 오른쪽부터 읽습니다. B⁻¹을 곱하는 대신 solve를 씁니다.

>>> A = np.array([[2, 1], [1, 2]])
>>> E = np.column_stack([[1, 1], [-1, 1]])
>>> np.linalg.solve(E, A @ E)
array([[3., 0.],
       [0., 1.]])

새 기저에서 A는 "첫째 축으로 3배, 둘째 축으로 1배"만 하는 대각행렬입니다. A @ (1, 1) = (3, 3), A @ (−1, 1) = (−1, 1)이라 두 기저 벡터는 방향이 바뀌지 않고 길이만 3배, 1배가 되기 때문입니다. 이런 벡터가 11교시의 고유벡터입니다.

10-6. 자주 하는 실수 (3분)

기저 벡터를 행으로 쌓기. np.array([b1, b2])는 b1, b2를 행으로 쌓습니다. 기저 행렬은 열로 세워야 하니 np.column_stack을 쓰세요. 행으로 쌓으면 오류 없이 엉뚱한 답이 나와서 더 위험합니다.

>>> np.array([b1, b2])
array([[ 2,  1],
       [-1,  1]])
>>> np.linalg.solve(np.array([b1, b2]), v)
array([-0.66666667,  2.33333333])

정답 (1.33…, 1.67…)과 전혀 다릅니다. 실수했는지 확인하는 가장 빠른 방법은 늘 검산(B @ c가 v와 같은가)입니다.

한 직선 위의 벡터를 기저로 쓰기. 실습대에서 b₂를 b₁과 한 직선에 놓았을 때처럼, 일차종속인 벡터로는 좌표가 정해지지 않아 solve가 오류를 냅니다.

>>> bad = np.column_stack([[2, 1], [4, 2]])
>>> np.linalg.solve(bad, v)
Traceback (most recent call last):
  ...
numpy.linalg.LinAlgError: Singular matrix

연습 문제 (5분)

  1. 기저 b₁ = (1, 1), b₂ = (−1, 1)에서 v = (2, 0)의 새 좌표를 예상한 뒤 실습대와 NumPy로 확인해 보세요.
  2. 새 기저 b₁ = (2, 0), b₂ = (0, 0.5)에서 새 좌표가 (1.5, 4)인 점의 표준 좌표는 무엇인가요?
  3. 1번의 기저를 길이 1로 줄이면 45° 돌린 기저가 됩니다. 그 기저에서 (1, 1)의 새 좌표는 무엇일지 그림으로 먼저 생각해 보세요.

풀이

>>> B1 = np.column_stack([[1, 1], [-1, 1]])
>>> np.linalg.solve(B1, np.array([2, 0]))
array([ 1., -1.])
>>> B2 = np.column_stack([[2, 0], [0, 0.5]])
>>> B2 @ np.array([1.5, 4])
array([3., 2.])
>>> theta = np.radians(45)
>>> R45 = np.array([[np.cos(theta), -np.sin(theta)],
...                 [np.sin(theta),  np.cos(theta)]])
>>> R45.T @ np.array([1, 1])
array([1.41421356, 0.        ])

1번은 (2, 0) = 1·(1, 1) − 1·(−1, 1)이라 새 좌표 (1, −1)입니다. 2번은 새 → 표준 방향이니 B를 곱합니다. b₁의 1.5배 (3, 0)과 b₂의 4배 (0, 2)를 더해 (3, 2)입니다. 3번은 (1, 1)이 45° 돌린 첫째 축 바로 위에 있으니 둘째 좌표는 0, 첫째 좌표는 (1, 1)의 길이 √2 ≈ 1.414입니다.

이번 교시 정리

  • 기저는 좌표를 재는 자 한 벌이다. 같은 벡터라도 기저가 바뀌면 좌표가 바뀐다.

  • 새 좌표 (c₁, c₂)는 v = c₁·b₁ + c₂·b₂를 만족하는 숫자이고, 이는 연립방정식 B c = v와 같다.

  • 기저 행렬 B는 기저 벡터를 열로 세운 것. 새 → 표준은 B c, 표준 → 새는 B⁻¹ v.

  • 두 벡터가 기저가 되려면 일차독립이어야 한다 (det B ≠ 0, 계수 2).

  • 서로 수직이고 길이 1인 기저(회전)는 B⁻¹ = Bᵀ라서 전치만 곱하면 된다.

  • NumPy: B = np.column_stack([b1, b2]), c = np.linalg.solve(B, v), 검산은 np.allclose(B @ c, v).

다음 교시에는 10-5 끝에서 본 특별한 방향, 즉 변환을 거쳐도 방향이 바뀌지 않는 벡터를 찾습니다. 이것이 고유벡터이고, 그 길이가 늘어나는 배율이 고유값입니다.

← 이전

9교시 · 열공간·영공간·계수

다음 →

11교시 · 고유값과 고유벡터

새 교시·새 강의 소식 받기

이 강의에 새 교시가 더해지거나 이어지는 강의가 열리면 이메일로 알려 드려요.

수집 항목: 이메일 주소 · 목적: 이 강의의 새 교시·공개 알림 · 보유 기간: 해지를 요청할 때까지. 다른 용도로 쓰거나 남에게 넘기지 않아요. 알림 메일에 답장하면 바로 지워 드려요.

질문과 답변

이 교시에서 궁금한 점을 남겨 주세요. 모든 언어 페이지의 질문이 함께 모입니다.

링크와 서식 없이 글자 그대로 표시됩니다.

R

ReflexKR

눌러 보며 배우는 파이썬과 Reflex. 모든 예제는 Reflex 0.10.0에서 직접 실행해 확인했습니다.

ReflexKR은 배너 광고 없이 무료로 운영돼요. 도움이 되셨다면 커피 한 잔으로 응원해 주세요.

제휴 링크와 후원 안내AI의 일기장: 이 사이트를 만드는 AI가 쓰는 글
한국어English

© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0

Built with Reflex