지도 앱에서 "북쪽으로 300m, 동쪽으로 400m"라고 말하던 위치를, 길을 걷는 사람에게는 "앞으로 500m"라고 말할 수 있습니다. 위치는 하나인데, 무엇을 기준으로 재느냐에 따라 숫자가 달라집니다.
지금까지 우리는 벡터 (1, 3)을 늘 "오른쪽으로 1, 위로 3"으로 읽었습니다. 이것은 4교시에서 본 표준기저 î, ĵ를 자로 썼다는 뜻입니다. 이번 교시에는 자를 바꿉니다. 기울어진 자, 늘어난 자, 돌아간 자로 같은 벡터를 재면 어떤 숫자가 나오는지, 그 숫자를 어떻게 계산하는지 배웁니다.
게임 개발자는 이 일을 매일 합니다. 월드 좌표의 적 위치를 "내 캐릭터 기준으로 앞으로 몇, 오른쪽으로 몇"으로 바꾸는 것이 기저 바꾸기입니다. 그리고 11·12교시의 고유벡터 이야기도 결국 "행렬이 가장 단순해 보이는 자를 고르는" 기저 바꾸기입니다.
좌표 (1, 3)을 일차결합(4교시)으로 풀어 쓰면 이렇습니다.
좌표의 두 숫자는 "î를 몇 개, ĵ를 몇 개 쓰는가"였습니다. î와 ĵ 대신 다른 두 벡터 b₁, b₂를 자로 쓰면, 같은 벡터 v를
로 적을 수 있습니다. 이때 (c₁, c₂)가 새 기저에서의 좌표입니다. 좌표를 재는 자 한 벌을 기저(basis) 라고 부르고, 기저가 바뀌면 같은 벡터의 좌표도 바뀝니다. 벡터 자체(화살표)는 그대로입니다.
아래 실습대에서 청록 b₁ = (2, 1), 진홍 b₂ = (−1, 1)이 새 기저이고, 검정 v = (1, 3)이 잴 벡터입니다. 남색 기울어진 격자는 b₁, b₂로 만든 "새 모눈종이"입니다. 오른쪽에 표준 좌표와 새 좌표가 함께 나옵니다.
v = 1.33·b₁ + 1.67·b₂
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.])처음 모습에서 새 좌표는 (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교시), 자 노릇을 못 합니다. 기저가 되려면 두 벡터가 일차독립이어야 합니다.
새 기저의 두 벡터를 열로 세운 행렬을 기저 행렬 라고 부르겠습니다. 실습대 오른쪽 아래의 가 이것입니다.
5교시에서 행렬과 벡터의 곱은 "열의 일차결합"이었습니다. 그러니 는 그대로 입니다.
새 좌표 를 찾는 일은 결국 라는 연립방정식을 푸는 일입니다. 8교시에서 배운 것을 그대로 씁니다. 손으로 풀어 보면
두 식을 더하면 이니 , 그리고 입니다. 실습대의 1.33과 1.67이 이 분수입니다.
| 책에 적힌 모양 | 읽는 법 | 뜻 | 파이썬에서는 |
|---|---|---|---|
| 기저 B | 자로 쓸 벡터 한 벌 (필기체 B) | b1, b2 | |
| 기저 행렬 B | 기저 벡터를 열로 세운 행렬 | np.column_stack([b1, b2]) | |
| 기저 B에서 v의 좌표 | 새 좌표 | np.linalg.solve(B, v) | |
| v는 B 곱하기 새 좌표 | 새 좌표 → 표준 좌표 | B @ c | |
| 새 좌표는 B 역 곱하기 v | 표준 좌표 → 새 좌표 | np.linalg.solve(B, v) |
책마다 대신 나 로 쓰기도 합니다. 아래첨자나 작은 따옴표가 붙으면 "다른 기저로 잰 좌표"인지 먼저 살펴보세요.
기저를 바꾸는 방향은 둘입니다. 헷갈리기 쉬우니 표로 정리해 둡니다.
| 가진 것 | 원하는 것 | 계산 | 왜 |
|---|---|---|---|
| 새 좌표 | 표준 좌표 | 을 개, 를 개 더하면 끝 | |
| 표준 좌표 | 새 좌표 | 위의 과정을 되돌리기 (7교시) |
새 → 표준은 곱하기 한 번이라 쉽습니다. 표준 → 새는 되돌리기라 역행렬, 실제 계산에서는 연립방정식 풀기(8교시의 solve)입니다.
를 변환으로 보면 더 기억하기 쉽습니다. 5교시에서 행렬의 열은 "î, ĵ가 도착한 곳"이었습니다. 는 î를 로, ĵ를 로 보내는 변환이니, 반듯한 모눈종이를 실습대의 기울어진 남색 격자로 휘게 합니다. 그래서 "새 격자에서 인 점"은 를 거치면 표준 좌표로 바뀝니다.
기저가 되는 조건도 이제 한 줄로 말할 수 있습니다. 가 기저인 것은 다음과 같은 말입니다.
7교시의 "역행렬이 있다", 8교시의 "해가 딱 하나", 9교시의 "계수 2"가 모두 같은 말입니다. 새 좌표가 하나로 정해져야 자로 쓸 수 있습니다.
게임과 그래픽스의 지역 좌표. 캐릭터마다 "앞"과 "오른쪽" 방향이 있습니다. 이 두 방향을 기저로 쓰면, 월드 좌표의 적 위치를 "앞으로 4.4, 왼쪽으로 0.8"처럼 캐릭터가 이해하기 쉬운 숫자로 바꿀 수 있습니다. 반대로 "앞으로 5 걸음"을 월드 좌표로 바꾸는 것은 B를 곱하는 일입니다. 3D 엔진의 "로컬 좌표 ↔ 월드 좌표" 변환이 이것입니다.
돌아간 좌표계. 기저 벡터가 서로 수직이고 길이가 1이면(직교 기저), B⁻¹이 그냥 전치 Bᵀ가 됩니다(5교시의 .T). 역행렬을 계산하지 않아도 되니 아주 편합니다. 이 성질은 13교시의 직교·QR 분해에서 다시 씁니다.
행렬을 단순하게 보이게 하는 기저. 같은 변환도 어떤 자로 재느냐에 따라 행렬이 달라 보입니다. 잘 고른 기저에서는 복잡해 보이던 행렬이 "축 방향으로 늘이기"만 하는 대각행렬이 됩니다. 그 특별한 기저가 11교시의 고유벡터이고, 그 기저로 바꿔 계산을 쉽게 하는 것이 12교시의 대각화입니다. 10-5의 마지막에서 미리 맛을 봅니다.
실습대 칸의 코드를 따라가 봅니다. 기저 벡터 두 개와 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)
TrueC의 각 열이 각 점의 새 좌표입니다. 둘째 점 (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))
True10-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교시의 고유벡터입니다.
기저 벡터를 행으로 쌓기. 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>>> 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 끝에서 본 특별한 방향, 즉 변환을 거쳐도 방향이 바뀌지 않는 벡터를 찾습니다. 이것이 고유벡터이고, 그 길이가 늘어나는 배율이 고유값입니다.
새 교시·새 강의 소식 받기
이 강의에 새 교시가 더해지거나 이어지는 강의가 열리면 이메일로 알려 드려요.
R
ReflexKR
눌러 보며 배우는 파이썬과 Reflex. 모든 예제는 Reflex 0.10.0에서 직접 실행해 확인했습니다.
ReflexKR은 배너 광고 없이 무료로 운영돼요. 도움이 되셨다면 커피 한 잔으로 응원해 주세요.
© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
질문과 답변
이 교시에서 궁금한 점을 남겨 주세요. 모든 언어 페이지의 질문이 함께 모입니다.