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와 함께
5교시

강의 시간 3:20–4:10

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

게임에서 캐릭터 그림을 90° 돌리거나, 그림판에서 사진을 비스듬히 기울이거나, 지도 앱에서 화면을 확대하면 화면 위의 모든 점이 한꺼번에 새 자리로 옮겨 갑니다. 점이 수백만 개라도 컴퓨터는 이 일을 순식간에 해냅니다. 점마다 따로 규칙을 정해 두지 않고, 숫자 네 개짜리 규칙 하나로 모든 점을 옮기기 때문입니다.

그 숫자 네 개짜리 규칙이 행렬(matrix) 입니다. 책에서는 행렬을 "숫자를 직사각형으로 늘어놓은 표"라고 먼저 소개하지만, 이 강의에서는 다르게 봅니다. 행렬은 공간 전체를 옮기는 규칙이고, 그 규칙을 적어 둔 메모가 숫자 표입니다.

이번 교시가 강의 전체의 중심입니다. 여기서 잡은 그림 하나("행렬의 열은 î와 ĵ가 도착한 곳")가 행렬곱, 행렬식, 역행렬, 고유값, SVD까지 계속 쓰입니다. 서두르지 말고 실습대를 충분히 만져 보세요.

5-1. 공간을 옮긴다는 것 (6분)

변환(transformation) 은 벡터를 넣으면 벡터가 나오는 함수입니다. 파이썬으로 치면 def T(v): return 새_벡터 같은 것입니다. 평면 위의 점 하나하나를 화살표 끝이라고 생각하면, 변환은 평면 위의 모든 점을 새 자리로 옮기는 규칙이 됩니다.

변환은 아무렇게나 만들 수 있습니다. 평면을 물결처럼 구부릴 수도 있고, 한 점으로 빨아들일 수도 있습니다. 그중 선형대수가 다루는 것은 아주 얌전한 변환, 선형변환(linear transformation) 뿐입니다. 모눈종이 위에 그린 격자를 떠올리면 조건을 눈으로 확인할 수 있습니다.

선형변환이 지키는 것눈으로 보면
원점은 제자리에 있다격자의 중심 (0, 0)이 움직이지 않는다
직선은 직선으로 간다격자선이 휘지 않는다
평행한 선은 평행하게, 간격은 고르게격자 칸이 모두 똑같은 평행사변형이 된다

돌리기, 늘이기, 비스듬히 밀기, 거울처럼 뒤집기는 모두 이 조건을 지킵니다. 반대로 "모든 점을 오른쪽으로 1칸 옮기기(평행이동)"는 원점이 (1, 0)으로 움직이므로 선형변환이 아닙니다. 격자선을 구부리는 변환도 아닙니다.

조건이 이렇게 까다로운 대신, 선형변환에는 놀라운 성질이 하나 있습니다. 점 두 개가 어디로 가는지만 알면 나머지 모든 점이 어디로 가는지 저절로 정해집니다. 다음 절에서 그 두 점을 만나 봅니다.

5-2. î와 ĵ만 따라가면 된다 (10분)

4교시에서 평면의 모든 벡터를 표준기저 ı^=(1,0)\hat{\imath} = (1, 0)과 ȷ^=(0,1)\hat{\jmath} = (0, 1)의 일차결합으로 쓸 수 있다고 했습니다. 예를 들어 v=(2,1)\mathbf{v} = (2, 1)은 "ı^\hat{\imath} 두 번, ȷ^\hat{\jmath} 한 번"입니다.

v=[21]=2 ı^+1 ȷ^\mathbf{v} = \begin{bmatrix} 2 \\ 1 \end{bmatrix} = 2\,\hat{\imath} + 1\,\hat{\jmath}

이제 선형변환 TT로 평면 전체를 옮겼다고 해 봅시다. 격자선이 평행하고 간격이 고르게 유지되므로, "ı^\hat{\imath} 두 번, ȷ^\hat{\jmath} 한 번"이라는 관계도 그대로 유지됩니다. 달라지는 것은 ı^\hat{\imath}와 ȷ^\hat{\jmath}가 가리키는 곳뿐입니다.

T(v)=2 T(ı^)+1 T(ȷ^)T(\mathbf{v}) = 2\,T(\hat{\imath}) + 1\,T(\hat{\jmath})

그러니 T(ı^)T(\hat{\imath})와 T(ȷ^)T(\hat{\jmath}), 즉 두 기저 벡터가 도착한 곳만 알면 어떤 벡터든 옮길 수 있습니다. 도착한 곳은 숫자 두 개씩, 모두 네 개입니다. 이 네 숫자를 ı^\hat{\imath}가 도착한 곳을 첫째 열, ȷ^\hat{\jmath}가 도착한 곳을 둘째 열로 세워 적은 것이 행렬입니다.

ı^→[ac],ȷ^→[bd]⟹A=[abcd]\hat{\imath} \to \begin{bmatrix} a \\ c \end{bmatrix}, \qquad \hat{\jmath} \to \begin{bmatrix} b \\ d \end{bmatrix} \qquad \Longrightarrow \qquad A = \begin{bmatrix} a & b \\ c & d \end{bmatrix}

왼쪽 세로줄 a,ca, c가 1열(ı^\hat{\imath}가 도착한 곳), 오른쪽 세로줄 b,db, d가 2열(ȷ^\hat{\jmath}가 도착한 곳)입니다.

이 강의에서 가장 중요한 한 문장입니다. 행렬의 열은 î와 ĵ가 도착한 곳이다. 1교시에 벡터를 세로로 세워 쓰는 이유는 5교시에서 드러난다고 했는데, 바로 이것입니다. 도착한 벡터를 세로로 세워 나란히 붙이면 행렬이 됩니다.

아래 실습대에서 청록 화살표 î의 끝점과 진홍 화살표 ĵ의 끝점을 끌어 보세요. 두 화살표만 움직였는데 격자 전체가 따라 휘고, 오른쪽 행렬의 1열(청록)과 2열(진홍) 숫자가 끝점 좌표 그대로 바뀝니다. 남색으로 칠해진 칸은 처음의 정사각형 칸 하나(î와 ĵ로 만든 칸)가 옮겨 간 모습입니다.

îĵ
1001

1열은 î가, 2열은 ĵ가 도착한 곳

det A = 1
넓이는 그대로입니다

모든 방향이 고유벡터입니다 (λ = 1)

미리 만든 행렬

NumPy로 확인하기

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

>>> A = np.array([[1, 0], [0, 1]])
>>> np.round(np.linalg.det(A), 10)
np.float64(1.0)
>>> np.linalg.matrix_rank(A)
np.int64(2)
>>> vals, vecs = np.linalg.eig(A)
>>> np.round(vals, 6)
array([1.+0.j, 1.+0.j])
>>> np.round(vecs, 6)
array([[1.+0.j, 0.+0.j],
       [0.+0.j, 1.+0.j]])
>>> A @ np.array([2, 1])
array([2, 1])
청록 î의 끝점을 (1, 1)로, 진홍 ĵ의 끝점을 (−1, 1)로 끌어 보세요. 오른쪽 행렬의 1열과 2열이 끝점 좌표와 같은지 확인한 뒤, '미리 만든 행렬'의 버튼을 하나씩 눌러 보고 'I에서 A로 변환 재생'으로 격자가 움직이는 모습을 지켜보세요.

'미리 만든 행렬' 버튼을 차례로 누르며 î와 ĵ가 어디로 가는지 읽어 보면, 행렬을 외울 필요가 없다는 것을 알게 됩니다. 도착한 곳을 세로로 적기만 하면 됩니다.

버튼î가 도착한 곳 (1열)ĵ가 도착한 곳 (2열)행렬무슨 일이 일어나나
그대로(1, 0)(0, 1)[[1, 0], [0, 1]]아무것도 바뀌지 않음
90° 회전(0, 1)(−1, 0)[[0, −1], [1, 0]]시계 반대 방향으로 90° 돌기
늘이기(2, 0)(0, 0.5)[[2, 0], [0, 0.5]]가로로 2배, 세로로 절반
밀기(1, 0)(1, 1)[[1, 1], [0, 1]]바닥은 그대로, 위쪽일수록 오른쪽으로 밀림
뒤집기(−1, 0)(0, 1)[[−1, 0], [0, 1]]세로축을 거울 삼아 좌우 반전
납작하게(1, 0.5)(2, 1)[[1, 2], [0.5, 1]]평면 전체가 직선 하나로 눌림

표의 '행렬' 칸은 NumPy에 넣는 모양(행을 하나씩 적는 모양)으로 적었습니다. 이 점은 5-5에서 다시 다룹니다. '90° 회전'을 예로 들면, î = (1, 0)은 돌아서 위쪽 (0, 1)로, ĵ = (0, 1)은 돌아서 왼쪽 (−1, 0)으로 갑니다. 이 둘을 세로로 세워 붙이면 1열이 (0, 1), 2열이 (−1, 0)입니다.

'I에서 A로 변환 재생'은 아무것도 하지 않는 행렬 I(그대로)에서 출발해 지금의 행렬 A까지 격자를 천천히 움직여 줍니다. 재생하는 동안에도 격자선은 끝까지 곧고, 평행하고, 고르게 놓여 있습니다. 선형변환이 지키는 세 조건을 눈으로 확인하는 가장 좋은 방법입니다.

5-3. Av를 읽는 두 가지 방법 (8분)

행렬 AA로 벡터 v\mathbf{v}를 옮긴 결과를 AvA\mathbf{v}라고 쓰고 "A 곱하기 v"라고 읽습니다. 계산하는 방법은 두 가지인데, 결과는 언제나 같습니다. '밀기' 행렬과 v=(2,1)\mathbf{v} = (2, 1)로 해 봅시다.

A=[1101],v=[21]A = \begin{bmatrix} 1 & 1 \\ 0 & 1 \end{bmatrix}, \qquad \mathbf{v} = \begin{bmatrix} 2 \\ 1 \end{bmatrix}

방법 ① 열 결합. 5-2에서 본 그대로입니다. v\mathbf{v}는 "ı^\hat{\imath} 두 번, ȷ^\hat{\jmath} 한 번"이니, 옮긴 뒤에도 "1열 두 번, 2열 한 번"입니다. 4교시의 M @ [a, b]가 a·u + b·w였던 것과 똑같은 계산입니다.

Av=2[10]+1[11]=[31]A\mathbf{v} = 2 \begin{bmatrix} 1 \\ 0 \end{bmatrix} + 1 \begin{bmatrix} 1 \\ 1 \end{bmatrix} = \begin{bmatrix} 3 \\ 1 \end{bmatrix}

방법 ② 행과의 내적. 결과의 첫째 성분은 AA의 첫째 행과 v\mathbf{v}의 내적, 둘째 성분은 둘째 행과 v\mathbf{v}의 내적입니다. 3교시의 "성분끼리 곱해 더하기"입니다.

Av=[1⋅2+1⋅10⋅2+1⋅1]=[31]A\mathbf{v} = \begin{bmatrix} 1 \cdot 2 + 1 \cdot 1 \\ 0 \cdot 2 + 1 \cdot 1 \end{bmatrix} = \begin{bmatrix} 3 \\ 1 \end{bmatrix}

일반적인 2×22 \times 2 행렬로 쓰면 두 방법이 같은 식을 다른 순서로 묶었을 뿐이라는 것이 보입니다.

[abcd][xy]=x[ac]+y[bd]=[ax+bycx+dy]\begin{bmatrix} a & b \\ c & d \end{bmatrix} \begin{bmatrix} x \\ y \end{bmatrix} = x \begin{bmatrix} a \\ c \end{bmatrix} + y \begin{bmatrix} b \\ d \end{bmatrix} = \begin{bmatrix} ax + by \\ cx + dy \end{bmatrix}

두 방법은 쓰임새가 다릅니다. 그림을 떠올릴 때는 ① 열 결합("1열을 x번, 2열을 y번 이어 붙인다"), 손으로 숫자를 계산할 때는 ② 행과의 내적이 편합니다. 이 강의에서는 그림이 필요한 곳마다 ①을 씁니다.

실습대에서 '미리 만든 행렬'의 '밀기'를 누르고 'v = (2, 1) 따라가기'를 켜 보세요. 청록 점선이 1열을 두 번 간 화살표, 그 끝에 이어 붙은 진홍 점선이 2열을 한 번 간 화살표, 굵은 검정 화살표가 그 합인 Av입니다. 끝점이 (3, 1)에 놓입니다. î와 ĵ의 끝점을 끌어 행렬을 바꾸면 Av도 늘 "1열 두 번 + 2열 한 번" 자리로 따라옵니다.

책의 기호 읽는 법

책에 적힌 모양읽는 법뜻파이썬에서는
AA (대문자)행렬 A행렬에는 보통 대문자 이름을 붙임A = np.array([[1, 1], [0, 1]])
2×22 \times 2이 곱하기 이행 2개, 열 2개짜리 행렬A.shape == (2, 2)
a12a_{12}에이 일이1행 2열 성분 (행 번호가 먼저)A[0, 1]
AvA\mathbf{v}A 곱하기 v행렬 A로 벡터 v를 옮긴 결과A @ v
T(v)T(\mathbf{v})T 오브 v변환 T에 v를 넣은 결과T(v) (함수 호출)
ı^, ȷ^\hat{\imath},\ \hat{\jmath}아이 햇, 제이 햇표준기저 (1, 0), (0, 1). 책에 따라 e1, e2\mathbf{e}_1,\ \mathbf{e}_2np.array([1, 0])
ATA^{\mathsf{T}}A 전치행과 열을 맞바꾼 행렬A.T
II아이, 항등행렬아무것도 바꾸지 않는 행렬np.eye(2) (6교시)

성분 번호는 언제나 행이 먼저, 열이 나중입니다. 그리고 1교시에서 말했듯 책은 1부터, 파이썬은 0부터 셉니다. 책의 a12a_{12}가 파이썬의 A[0, 1]입니다.

5-4. 실습대의 나머지 숫자들 (4분)

실습대 오른쪽에는 행렬 말고도 숫자와 문구가 몇 개 더 있습니다. 지금은 뜻만 가볍게 알아 두고, 자세한 내용은 뒤 교시에서 다룹니다.

det A와 넓이 배율. 남색 칸은 원래 넓이가 1인 정사각형 칸이 옮겨 간 모습입니다. 이 칸의 넓이가 몇 배가 되었는지가 행렬식(det) 입니다. 격자 칸은 모두 똑같이 변하므로, 평면의 어떤 도형이든 넓이가 det배가 됩니다. 버튼을 눌러 보면 몇 가지가 눈에 띕니다.

  • '늘이기'는 가로로 2배, 세로로 절반이라 넓이는 2 × 0.5 = 1, 그대로입니다.
  • '밀기'도 넓이가 그대로입니다. 칸이 기울기만 하고 밑변과 높이는 변하지 않기 때문입니다.
  • '뒤집기'는 det가 −1이고 칸이 노랑으로 바뀝니다. 음수는 "앞뒤가 뒤집혔다"는 뜻입니다.
  • '납작하게'는 det가 0입니다. 1열 (1, 0.5)와 2열 (2, 1)이 한 직선 위에 있어서(4교시의 일차종속), 평면 전체가 직선 하나로 눌려 버립니다.

행렬식을 계산하는 법과, det가 0이면 왜 되돌릴 수 없는지는 7교시에서 자세히 다룹니다.

고유벡터 점선. '고유벡터 보기'가 켜져 있으면 회색 점선이 보일 때가 있습니다. 변환을 해도 제 방향을 잃지 않고 같은 직선 위에 머무는 방향입니다. '늘이기'에서는 가로축과 세로축이 점선이 됩니다. '90° 회전'처럼 모든 방향이 돌아가 버리면 그런 방향이 없다는 문구가 나옵니다. 이 방향들은 11교시의 주인공인 고유벡터이니, 지금은 "그런 것이 있구나" 하고 넘어가면 됩니다.

5-5. NumPy로 행렬 다루기 (12분)

실습대 아래 "NumPy로 확인하기" 칸에는 A = np.array([[1, 0], [0, 1]])처럼 행렬을 만드는 줄이 맨 먼저 나옵니다. 이 줄부터 하나씩 읽어 봅시다.

2차원 배열 만들기: 행을 하나씩 적는다

NumPy에서 행렬은 리스트의 리스트로 만듭니다. 안쪽 리스트 하나가 행 하나입니다.

>>> import numpy as np
>>> A = np.array([[1, 1], [0, 1]])
>>> A
array([[1, 1],
       [0, 1]])
>>> A.shape
(2, 2)
>>> A.ndim
2

shape의 (2, 2)는 "행 2개, 열 2개"입니다. 1교시의 벡터는 (2,)처럼 숫자가 하나였고 ndim이 1이었는데, 행렬은 축이 두 개(행 방향, 열 방향)라 ndim이 2입니다.

여기서 꼭 짚고 넘어갈 함정이 있습니다. 실습대는 열로 생각하라고 했는데(1열 = î가 도착한 곳), NumPy에는 행을 적습니다. '밀기'는 î가 (1, 0)으로, ĵ가 (1, 1)로 가는 변환이지만, NumPy에는 [[1, 1], [0, 1]]이라고 적습니다. 첫 행 [1, 1]은 두 열의 윗부분을 가로로 읽은 것입니다. 출력된 모양을 세로로 읽으면 열이 보입니다. 왼쪽 세로줄 1, 0이 î가 도착한 곳, 오른쪽 세로줄 1, 1이 ĵ가 도착한 곳입니다.

도착한 곳을 그대로 써서 만들고 싶다면 4교시의 np.column_stack이 가장 안전합니다. 이름 그대로 벡터를 열로 쌓아 줍니다.

>>> i_to = np.array([1, 0])
>>> j_to = np.array([1, 1])
>>> np.column_stack([i_to, j_to])
array([[1, 1],
       [0, 1]])

성분, 행, 열 꺼내기

4교시에서 본 인덱싱을 행렬의 뜻과 연결해 봅시다. A[행, 열]이 성분 하나, A[:, 0]이 첫째 열, A[1]이 둘째 행입니다. :는 "그 축은 전부"라는 뜻입니다.

>>> A[0, 1]
np.int64(1)
>>> A[:, 0]
array([1, 0])
>>> A[:, 1]
array([1, 1])
>>> A[1]
array([0, 1])

A[:, 0]은 "모든 행의 0번 칸", 즉 첫째 열입니다. 이것이 î가 도착한 곳 (1, 0)이고, A[:, 1]이 ĵ가 도착한 곳 (1, 1)입니다. 반면 A[1]은 둘째 행이라 도착한 곳과는 상관이 없는 숫자 묶음입니다. 열을 꺼낼 때는 꼭 A[:, 열]을 쓰세요.

A @ v: 행렬로 벡터 옮기기

행렬과 벡터의 곱은 3교시의 내적과 같은 @ 연산자로 합니다. 실습대 NumPy 칸의 마지막 줄 A @ np.array([2, 1])이 바로 'v = (2, 1) 따라가기'의 검정 화살표 Av입니다.

>>> v = np.array([2, 1])
>>> A @ v
array([3, 1])

5-3의 두 방법을 NumPy로 그대로 옮겨 적어도 같은 답이 나옵니다.

>>> v[0] * A[:, 0] + v[1] * A[:, 1]
array([3, 1])
>>> np.array([A[0] @ v, A[1] @ v])
array([3, 1])

첫 줄은 ① "1열을 2번, 2열을 1번 더하기", 둘째 줄은 ② "각 행과 v의 내적"입니다. 실제로 A @ v를 쓸 때 이렇게 풀어 쓸 일은 없지만, @가 속에서 무슨 일을 하는지 한 번 확인해 두면 결과를 믿고 쓸 수 있습니다.

여러 점을 한 번에 옮기기

게임에서 사각형 스프라이트를 기울이려면 꼭짓점 네 개를 모두 옮겨야 합니다. 꼭짓점을 열로 늘어놓은 행렬을 만들면 @ 한 번에 전부 옮길 수 있습니다.

>>> square = np.array([[0, 1, 1, 0],
...                    [0, 0, 1, 1]])
>>> square.shape
(2, 4)
>>> A @ square
array([[0, 1, 2, 1],
       [0, 0, 1, 1]])

square의 열 네 개가 정사각형의 꼭짓점 (0, 0), (1, 0), (1, 1), (0, 1)이고, 결과의 열 네 개가 옮겨 간 꼭짓점 (0, 0), (1, 0), (2, 1), (1, 1)입니다. 실습대에서 '밀기'를 눌렀을 때 남색 칸의 네 꼭짓점과 같습니다. 행렬에 행렬을 곱하는 이 계산이 6교시의 주제입니다.

A * v는 Av가 아니다

NumPy에서 *는 2교시에서 본 원소별 곱입니다. 행렬에 벡터를 *로 곱하면 브로드캐스팅이 일어나 v가 각 행에 원소별로 곱해집니다.

>>> A * v
array([[2, 1],
       [0, 1]])

결과가 (2, 2) 행렬이니 Av가 아니라는 것은 바로 보입니다. 각 행 [1, 1], [0, 1]에 [2, 1]을 원소별로 곱한 것입니다. 세로로 보면 1열이 2배, 2열이 1배가 된 모양이라 ① 열 결합에서 "더하기 직전"의 조각들이 나온 셈입니다. 두 열을 더해야 비로소 Av가 됩니다.

>>> (A * v)[:, 0] + (A * v)[:, 1]
array([3, 1])

행렬로 벡터를 옮길 때는 @, 칸끼리 곱할 때는 *입니다. 이 구분은 6교시에서 한 번 더 강조합니다.

A.T: 전치

행과 열을 맞바꾼 행렬을 전치(transpose) 라고 하고, 책에서는 Aᵀ, NumPy에서는 A.T로 씁니다. 첫째 행이 첫째 열이 되고, 첫째 열이 첫째 행이 됩니다.

>>> A.T
array([[1, 0],
       [1, 1]])
>>> np.array([[1, 0], [1, 1]]).T
array([[1, 1],
       [0, 1]])

둘째 예처럼, 도착한 곳 (1, 0)과 (1, 1)을 행으로 적은 뒤 .T로 뒤집어도 같은 행렬을 얻습니다. 전치는 13교시(최소제곱)와 15교시(PCA)에서 본격적으로 쓰입니다.

NumPy 칸의 나머지 줄

실습대 NumPy 칸에는 아직 배우지 않은 줄도 있습니다. 미리 이름만 알아 두세요.

줄하는 일자세히는
np.round(np.linalg.det(A), 10)넓이 배율(행렬식)을 소수점 아래 10자리로 반올림7교시
np.linalg.matrix_rank(A)열이 펼치는 공간의 차원(계수). 4교시처럼 2면 평면 전체, 1이면 직선9교시
vals, vecs = np.linalg.eig(A)고유값과 고유벡터를 구해 vals, vecs 두 이름에 나눠 담기11교시
np.round(vals, 6), np.round(vecs, 6)그 둘을 소수점 아래 6자리로 반올림해 보기. 1.+0.j처럼 복소수 모양으로 나옴11교시
A @ np.array([2, 1])'v = (2, 1) 따라가기'의 Av이번 교시

np.linalg는 NumPy 안의 선형대수(linear algebra) 도구 모음입니다. 앞으로 나오는 함수 대부분이 여기 들어 있습니다.

np.round(값, 자릿수)는 숫자나 배열의 모든 칸을 소수점 아래 그 자릿수까지 반올림합니다. 실습대는 계산 결과에 섞이는 아주 작은 오차(6교시와 7교시에서 자세히 봅니다)를 숨기려고 몇몇 값을 이렇게 반올림해 보여 주고, 반올림했다는 사실을 줄에 그대로 적어 둡니다. 직접 따라 할 때 np.round(...)를 빼면 끝자리가 조금 다른 숫자가 나올 수 있습니다.

>>> np.round(3.14159, 2)
np.float64(3.14)

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

열을 행으로 적기. "î는 (1, 0)으로, ĵ는 (1, 1)로"라는 말을 듣고 np.array([[1, 0], [1, 1]])이라고 적으면 '밀기'가 아니라 전치된, 전혀 다른 행렬이 됩니다. 도착한 곳을 그대로 쓰고 싶을 때는 np.column_stack을 쓰거나, 적은 뒤 A[:, 0]으로 1열을 꺼내 확인하는 습관을 들이세요.

@ 대신 * 쓰기. 5-5에서 본 것처럼 A * v도 오류 없이 무언가를 돌려주기 때문에 실수를 알아채기 어렵습니다. 결과의 shape이 (2,)가 아니라 (2, 2)라면 의심해 보세요.

모양이 맞지 않는 벡터 곱하기. 열이 2개인 행렬에는 성분이 2개인 벡터만 곱할 수 있습니다. "1열을 몇 번, 2열을 몇 번"에 해당하는 숫자가 정확히 두 개 필요하기 때문입니다.

>>> A @ np.array([1, 2, 3])
Traceback (most recent call last):
  ...
ValueError: matmul: Input operand 1 has a mismatch in its core dimension 0, with gufunc signature (n?,k),(k,m?)->(n?,m?) (size 3 is different from 2)

메시지가 길지만 끝부분 (size 3 is different from 2)만 읽으면 됩니다. "벡터 길이 3이 행렬의 열 개수 2와 다르다"는 뜻입니다.

연습 문제 (7분)

  1. 위아래를 뒤집는 변환(가로축을 거울로 삼는 반전)은 î와 ĵ를 어디로 보낼까요? 행렬을 NumPy 배열 F로 만들고, F @ np.array([2, 1])의 결과를 먼저 예상한 뒤 확인해 보세요.
  2. B = np.array([[2, -1], [1, 1]])에서 î와 ĵ가 도착한 곳은 각각 어디인가요? B @ np.array([1, 2])를 ① 열 결합과 ② 행과의 내적으로 손으로 계산한 뒤 NumPy로 확인해 보세요.
  3. 실습대에서 î를 (0, 2)로, ĵ를 (1, 0)으로 끌어 보세요. NumPy에는 어떻게 적어야 할까요? 칸이 노랑으로 바뀌는 이유도 생각해 보세요.
  4. "모든 점을 2배로 늘린 뒤 위로 1칸 옮기기"는 선형변환일까요?

풀이

>>> F = np.array([[1, 0], [0, -1]])
>>> F @ np.array([2, 1])
array([ 2, -1])
>>> B = np.array([[2, -1], [1, 1]])
>>> B[:, 0], B[:, 1]
(array([2, 1]), array([-1,  1]))
>>> B @ np.array([1, 2])
array([0, 3])
>>> np.column_stack([[0, 2], [1, 0]])
array([[0, 1],
       [2, 0]])

1번은 î = (1, 0)은 그대로, ĵ = (0, 1)은 (0, −1)로 갑니다. 출력의 [ 2, -1]에서 2 앞의 빈칸은 −1과 자리를 맞추려고 NumPy가 넣은 것입니다.

2번은 î가 (2, 1)로, ĵ가 (−1, 1)로 갑니다. ① 1·(2, 1) + 2·(−1, 1) = (0, 3), ② (2·1 + (−1)·2, 1·1 + 1·2) = (0, 3)입니다. 튜플로 두 열을 한꺼번에 보여 주었기 때문에 출력도 괄호에 싸여 나왔습니다.

3번은 도착한 곳이 열이니 [[0, 1], [2, 0]]입니다. 원래는 ĵ가 î에서 시계 반대 방향으로 90° 돈 쪽에 있는데, 여기서는 ĵ(오른쪽)가 î(위쪽)에서 시계 방향 쪽에 놓여 앞뒤가 뒤집혔기 때문에 칸이 노랑이 되고 det가 −2가 됩니다. 넓이가 2배가 되고 방향이 뒤집힌 것입니다(7교시).

4번은 아닙니다. 원점 (0, 0)이 (0, 1)로 움직이기 때문입니다. 2배로 늘리기만 하는 부분은 선형변환 [[2, 0], [0, 2]]이지만, 위로 옮기는 부분이 원점을 움직입니다.

이번 교시 정리

  • 선형변환은 원점을 고정하고, 격자선을 곧고 평행하고 고르게 유지한 채 공간 전체를 옮긴다.

  • 행렬의 1열은 î가, 2열은 ĵ가 도착한 곳이다. 이 두 곳만 알면 모든 벡터의 행선지가 정해진다.

  • Av는 ① 열 결합 x·(1열) + y·(2열)로 그림을 그리고, ② 각 행과 v의 내적으로 숫자를 계산한다.

  • det는 넓이 배율(음수면 뒤집힘, 0이면 납작), 고유벡터는 방향을 잃지 않는 방향: 7교시와 11교시에서 자세히.

  • NumPy의 np.array([[a, b], [c, d]])는 행을 적는다. 열은 A[:, 0], 벡터 옮기기는 A @ v, A * v는 원소별 곱, A.T는 전치.

다음 교시에는 변환 두 개를 차례로 하면 무엇이 되는지 봅니다. "먼저 돌리고 그다음 민다"를 행렬 하나로 적는 법, 그것이 바로 행렬곱입니다.

← 이전

4교시 · 일차결합과 생성(span)

다음 →

6교시 · 행렬곱 = 변환을 이어 하기

새 교시·새 강의 소식 받기

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

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

질문과 답변

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

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

R

ReflexKR

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

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

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

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

Built with Reflex