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

강의 시간 5:50–6:40

연립방정식 Ax = b

"두 수를 더하면 3이고, 빼면 1이다. 두 수는 무엇일까?" 중학교에서 풀던 연립방정식입니다. 선형대수가 가장 많이 쓰이는 곳이 바로 이런 문제를 푸는 일입니다. 다만 실제로는 식이 두 개가 아니라 수천, 수백만 개입니다.

개발자 주변에도 연립방정식은 많습니다. 게임에서 두 광선이 어디서 만나는지 구하기, 회로나 물리 시뮬레이션에서 힘의 균형 맞추기, 이미지 처리에서 필터를 되돌리기, 머신러닝에서 가중치를 구하기가 모두 "미지수 여러 개, 식 여러 개"를 한꺼번에 푸는 일입니다.

이번 교시에는 연립방정식을 두 가지 그림으로 봅니다. 식 하나를 직선 하나로 보는 행 그림, 그리고 4교시의 일차결합으로 보는 열 그림입니다. 7교시의 행렬식이 "해가 하나뿐인가"를 알려 주는 신호라는 것도 확인하고, 마지막으로 NumPy의 np.linalg.solve로 직접 풉니다.

8-1. 식 하나 = 직선 하나 (10분)

식 x + y = 3을 만족하는 점 (x, y)를 모두 찍어 보면 (3, 0), (2, 1), (0, 3), (−1, 4)…처럼 한 직선 위에 늘어섭니다. 일반적으로 a x + b y = c 꼴의 식은 평면 위의 직선 하나입니다. x와 y가 제곱되거나 서로 곱해지지 않고 숫자배로만 더해지기 때문에 "선형(linear)"이라는 이름이 붙었습니다.

식이 두 개면 직선이 두 개입니다. 두 식을 동시에 만족하는 점은 두 직선 위에 모두 있는 점, 즉 교점입니다. 연립방정식을 푼다는 것은 그림으로 보면 두 직선이 만나는 점을 찾는 일입니다. 이렇게 식(행렬의 행) 하나하나를 그림으로 보는 방식을 행 그림이라고 부릅니다.

아래 실습대의 처음 모습은 x + y = 3(청록)과 x − y = 1(진홍)입니다. 두 직선이 만나는 검정 점이 (2, 1)이고, 실제로 2 + 1 = 3, 2 − 1 = 1이니 두 식을 모두 만족합니다.

(2, 1)

첫째 식: 1x + 1y = 3

둘째 식: 1x − 1y = 1

해가 하나: 두 직선이 한 점에서 만납니다

미리 만든 식

NumPy로 확인하기

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

>>> A = np.array([[1, 1], [1, -1]])
>>> b = np.array([3, 1])
>>> np.round(np.linalg.det(A), 10)
np.float64(-2.0)
>>> x = np.linalg.solve(A, b)
>>> x
array([2., 1.])
>>> A @ x
array([3., 1.])
둘째 식(진홍)의 c 슬라이더를 1에서 3으로 옮겨 보세요. 진홍 직선이 기울기는 그대로 둔 채 평행하게 움직이고, 교점이 따라 미끄러집니다. 이어서 둘째 식의 a, b를 첫째 식과 같게(1, 1) 만들면 무슨 일이 생기는지도 보세요.

실습대 오른쪽의 슬라이더 a, b, c는 각 식 a x + b y = c의 숫자입니다. 몇 가지를 직접 확인해 보세요.

  • c만 바꾸면 직선이 평행하게 움직입니다. 기울어진 정도는 a와 b가 정합니다.
  • a나 b를 바꾸면 직선이 돌아갑니다. b가 0이면 a x = c라 세로선, a가 0이면 가로선이 됩니다.
  • 교점 옆에 적힌 좌표가 해입니다. 슬라이더가 0.5 간격이라 해가 1.67처럼 반올림되어 보일 때도 있습니다.

슬라이더 이름 b는 "y 앞의 숫자"라는 뜻이고, 잠시 뒤에 나올 오른쪽 변의 벡터 b와는 다른 것입니다. 책도 이 두 가지에 같은 글자를 쓰는 경우가 많으니 문맥으로 구별하세요.

8-2. 해가 하나, 없음, 무수히 (7분)

직선 두 개가 놓이는 방법은 세 가지뿐입니다. 실습대의 '미리 만든 식'에 세 경우가 모두 들어 있습니다.

미리 만든 식두 식그림해
해 하나x + y = 3, x − y = 1한 점에서 만남(2, 1) 하나
해 없음x + 2y = 2, x + 2y = −2평행, 만나지 않음없음
해 무수히x − y = 1, 2x − 2y = 2완전히 겹침직선 위의 모든 점

'해 없음'의 두 식은 왼쪽이 똑같은데 오른쪽이 2와 −2로 다릅니다. "같은 것이 동시에 2이고 −2"일 수는 없으니 해가 없습니다. '해 무수히'의 둘째 식은 첫째 식 전체에 2를 곱한 것입니다. 새로운 정보가 없는 식이라, 사실상 식이 하나뿐인 셈입니다. 식이 하나면 직선 하나 위의 모든 점이 해입니다.

"해가 딱 두 개"나 "딱 세 개"인 경우는 없다는 점도 기억해 두세요. 두 직선은 한 점에서 만나거나, 전혀 안 만나거나, 아예 겹칩니다. 미지수가 더 많아져도 이 세 가지(하나, 없음, 무수히)뿐입니다.

해가 하나가 아닌 두 경우에는 공통점이 있습니다. 두 직선의 기울기가 같습니다. 실습대에서 '해 없음'을 고른 뒤, 아무 식이나 c 슬라이더만 움직여 보세요. 두 직선이 평행한 채로 움직이다가, 둘째 식의 c가 2가 되는 순간 겹치면서 상태 문구가 '해가 무수히 많습니다'로 바뀝니다.

8-3. 같은 식을 행렬로: Ax = b (6분)

식이 많아지면 x, y를 일일이 적기 번거롭습니다. 그래서 숫자만 뽑아 행렬과 벡터로 적습니다. 왼쪽 변의 숫자는 행렬 AA, 미지수는 벡터 x\mathbf{x}, 오른쪽 변은 벡터 b\mathbf{b}입니다.

x+y=3x−y=1⟺[111−1]⏟A[xy]⏟x=[31]⏟b\begin{aligned} x + y &= 3 \\ x - y &= 1 \end{aligned} \qquad\Longleftrightarrow\qquad \underbrace{\begin{bmatrix} 1 & 1 \\ 1 & -1 \end{bmatrix}}_{A} \underbrace{\begin{bmatrix} x \\ y \end{bmatrix}}_{\mathbf{x}} = \underbrace{\begin{bmatrix} 3 \\ 1 \end{bmatrix}}_{\mathbf{b}}

행렬의 한 행이 식 하나입니다. 5교시에서 본 "행과의 내적"으로 AxA\mathbf{x}를 계산하면, 첫째 행 (1,1)(1, 1)과 (x,y)(x, y)의 내적이 x+yx + y, 둘째 행 (1,−1)(1, -1)과의 내적이 x−yx - y가 되어 원래 식이 그대로 돌아옵니다.

책에 적힌 모양읽는 법뜻파이썬에서는
Ax=bA\mathbf{x} = \mathbf{b}에이 엑스는 비행렬 A로 x를 옮기면 b가 된다A @ x 가 b 와 같다
aija_{ij}에이 아이 제이A의 i행 j열 숫자 (1부터 셈)A[i-1, j-1]
x=A−1b\mathbf{x} = A^{-1}\mathbf{b}엑스는 에이 역 비7교시의 역행렬로 되돌리면 해가 나온다np.linalg.solve(A, b)
det⁡A≠0\det A \neq 0디터미넌트 에이는 0이 아니다평면이 납작해지지 않는다np.linalg.det(A)

Ax=bA\mathbf{x} = \mathbf{b}를 변환으로 읽으면 이런 질문이 됩니다. "변환 AA를 거친 뒤 b\mathbf{b}에 도착하는 벡터 x\mathbf{x}는 무엇인가?" 7교시에서 A−1A^{-1}은 변환을 되돌리는 행렬이었으니, b\mathbf{b}를 되돌리면 출발점 x\mathbf{x}가 나옵니다. 이 생각은 맞지만, 실제 계산에서는 역행렬을 만들지 않습니다. 이유는 8-5에서 봅니다.

8-4. 열 그림: b를 열들로 만들기 (8분)

같은 식을 이번에는 세로로 읽어 봅니다. 5교시에서 A @ v는 "x·1열 + y·2열"이라고 했습니다. 그러면 A x = b는 이런 질문입니다.

1열 (1, 1)을 x배, 2열 (1, −1)을 y배 해서 더하면 b = (3, 1)이 되게 하려면 x, y가 얼마여야 하나?

이것은 4교시의 일차결합 문제와 똑같습니다. 4교시 실습대에서 a, b 슬라이더를 움직여 목표 점 T에 도착해 보던 것이 바로 연립방정식을 푸는 일이었습니다. 이렇게 행렬의 열을 화살표로 보는 방식을 열 그림이라고 부릅니다.

위 실습대에서 '해 하나'를 고르고 '열 그림 보기'를 켜 보세요. 청록 화살표가 1열 (1, 1), 진홍 화살표가 2열 (1, −1), 굵은 검정 화살표가 b = (3, 1)입니다. 점선은 "1열을 2배 한 뒤, 2열을 1배 이어 붙이면 b에 도착한다"를 보여 줍니다. 해 (x, y) = (2, 1)이 곧 일차결합의 계수입니다.

이제 세 경우를 열 그림으로 다시 읽으면 이유가 보입니다.

  • 해 하나: 두 열이 서로 다른 방향이면 평면 전체를 생성합니다(4교시). 어떤 b든 만들 수 있고, 만드는 방법은 한 가지뿐입니다.
  • 해 없음: '해 없음'을 고르고 열 그림을 보면 두 열 (1, 1)과 (2, 2)가 한 직선 위에 있습니다. 이 둘로는 그 직선 위만 갈 수 있는데, b = (2, −2)는 직선 밖에 있습니다.
  • 해 무수히: '해 무수히'의 두 열 (1, 2)와 (−1, −2)도 한 직선 위에 있는데, 이번에는 b = (1, 2)가 그 직선 위에 있습니다. 갈 수는 있는데, 두 열이 같은 방향이라 섞는 방법이 끝없이 많습니다.

정리하면, 7교시의 행렬식과 이렇게 이어집니다.

det A두 열변환연립방정식
0이 아님일차독립되돌릴 수 있음 (역행렬 있음)어떤 b든 해가 딱 하나
0한 직선 위 (일차종속)평면이 납작해짐b에 따라 해가 없거나 무수히 많음

det = 0일 때 "없음"과 "무수히" 중 어느 쪽인지는 b가 납작해진 직선 위에 있느냐에 달려 있습니다. 이 구분은 다음 교시의 열공간에서 정확한 이름을 얻습니다.

8-5. NumPy로 풀기: np.linalg.solve (12분)

실습대 아래 "NumPy로 확인하기" 칸에 나오는 코드를 한 줄씩 따라가 봅니다. 먼저 A와 b를 만듭니다. 식 하나가 행 하나이니, 식을 위에서부터 차례로 안쪽 리스트 하나씩 적으면 됩니다.

>>> import numpy as np
>>> A = np.array([[1, 1], [1, -1]])
>>> b = np.array([3, 1])
>>> A.shape, b.shape
((2, 2), (2,))

A는 2행 2열, b는 성분 두 개짜리 벡터입니다. 식의 개수(행의 수)와 b의 성분 수가 같아야 합니다. 이제 행렬식으로 해가 하나인지 확인하고, np.linalg.solve(A, b)로 풉니다. 함수 이름의 solve는 "풀다"라는 뜻이고, 인자는 A 먼저, b 다음 순서입니다.

>>> np.linalg.det(A)
np.float64(-2.0)
>>> x = np.linalg.solve(A, b)
>>> x
array([2., 1.])

정수만 넣었는데 답은 2., 1.처럼 실수(float64)로 나옵니다. 연립방정식의 답은 보통 분수라서, solve는 늘 실수로 계산합니다. 답을 얻으면 반드시 검산합니다. A @ x가 b와 같으면 맞게 푼 것입니다.

>>> A @ x
array([3., 1.])
>>> np.allclose(A @ x, b)
True

실수 계산에는 아주 작은 오차가 끼어들 수 있으니(3교시), 검산은 == 대신 np.allclose로 합니다. 행 그림과 열 그림도 NumPy로 그대로 확인할 수 있습니다. A[0]은 첫째 행(첫째 식), A[:, 0]은 첫째 열입니다.

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

첫째 줄은 "첫째 식에 해를 넣으면 3이 나온다"(행 그림), 둘째 줄은 "1열 2배 + 2열 1배 = b"(열 그림)입니다.

실습대 칸은 화면을 깔끔하게 하려고 행렬식을 np.round(np.linalg.det(A), 10), 곧 소수 10자리에서 반올림해 보여 줍니다. 직접 계산하면 -1.9999999999999996처럼 끝자리가 조금 다른 값이 나올 수 있습니다(7교시).

미지수가 셋이어도 똑같다

식이 세 개, 미지수가 세 개(x, y, z)여도 코드는 그대로입니다. 그림으로는 평면 세 장이 한 점에서 만나는 문제지만, NumPy에게는 3×3 행렬일 뿐입니다.

 2x +  y -  z =   8
-3x -  y + 2z = -11
-2x +  y + 2z =  -3
>>> A3 = np.array([[2, 1, -1],
...                [-3, -1, 2],
...                [-2, 1, 2]])
>>> b3 = np.array([8, -11, -3])
>>> x3 = np.linalg.solve(A3, b3)
>>> x3
array([ 2.,  3., -1.])
>>> np.allclose(A3 @ x3, b3)
True

해는 x = 2, y = 3, z = −1입니다. 손으로 풀면 식을 더하고 빼며 몇 분은 걸리는 문제입니다. 미지수가 1000개여도 np.linalg.solve는 같은 한 줄로 풉니다.

해가 하나가 아니면 오류가 난다

'해 없음'이나 '해 무수히'를 고르면 실습대 칸에 오류가 나옵니다. det가 0인 행렬을 NumPy는 특이 행렬(singular matrix) 이라고 부르고, solve는 답을 하나로 정할 수 없어 LinAlgError를 냅니다.

>>> A_none = np.array([[1, 2], [1, 2]])
>>> b_none = np.array([2, -2])
>>> np.linalg.det(A_none)
np.float64(0.0)
>>> np.linalg.solve(A_none, b_none)
Traceback (most recent call last):
  ...
numpy.linalg.LinAlgError: Singular matrix

'해 무수히'의 A = [[1, -1], [2, -2]], b = [1, 2]를 넣어도 똑같은 오류가 납니다. solve는 "해가 딱 하나인 경우"만 다루기 때문에, 없음과 무수히를 구별해 주지 않습니다. 둘을 구별하는 법은 9교시에서, 해가 없을 때 "가장 가까운 답"을 구하는 법은 13교시에서 배웁니다.

프로그램에서는 오류가 날 수 있는 곳을 try로 감싸 둡니다.

>>> try:
...     np.linalg.solve(A_none, b_none)
... except np.linalg.LinAlgError:
...     print("해가 하나로 정해지지 않습니다")
해가 하나로 정해지지 않습니다

inv(A) @ b 대신 solve를 쓰는 이유

8-3에서 해는 A⁻¹b라고 했으니 np.linalg.inv(A) @ b로도 같은 답이 나옵니다.

>>> np.linalg.inv(A) @ b
array([2., 1.])

하지만 실무에서는 거의 언제나 solve를 씁니다. 이유는 두 가지입니다.

  1. 느립니다. 역행렬을 만드는 일은 연립방정식을 열 개수만큼(n×n이면 n번) 푸는 것과 비슷한 일입니다. 우리는 b 하나에 대한 답만 필요한데, 쓰지도 않을 정보까지 계산하는 셈입니다.
  2. 오차가 커지기 쉽습니다. 역행렬을 만들 때 생긴 반올림 오차가 곱셈을 거치며 한 번 더 쌓입니다. solve는 A를 바로 분해해서 푸니 계산 단계가 적습니다.

직접 비교해 보고 싶다면 아래 코드를 실행해 보세요. 걸리는 시간은 컴퓨터마다 다르니 결과는 적어 두지 않았습니다. 보통 inv 쪽이 더 오래 걸리고, 남는 오차(A @ x - b의 길이)도 solve 쪽이 작거나 비슷합니다.

import time
import numpy as np

rng = np.random.default_rng(0)
A = rng.standard_normal((2000, 2000))   # 2000×2000 무작위 행렬
b = rng.standard_normal(2000)

t = time.perf_counter(); x1 = np.linalg.solve(A, b); print("solve:", time.perf_counter() - t)
t = time.perf_counter(); x2 = np.linalg.inv(A) @ b;  print("inv  :", time.perf_counter() - t)

print(np.allclose(x1, x2))                                   # 답은 거의 같다
print(np.linalg.norm(A @ x1 - b), np.linalg.norm(A @ x2 - b))  # 남은 오차 비교

역행렬 자체가 필요한 경우(같은 변환을 여러 번 되돌려야 하고 행렬이 작을 때 등)가 아니라면, "A x = b를 푼다 = solve"로 기억해 두세요.

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

인자 순서 바꾸기. np.linalg.solve(b, A)처럼 순서를 바꾸면, 첫 인자가 1차원이라 행렬로 볼 수 없다는 오류가 납니다.

>>> np.linalg.solve(b, A)
Traceback (most recent call last):
  ...
numpy.linalg.LinAlgError: 1-dimensional array given. Array must be at least two-dimensional

식의 개수와 b의 길이가 다름. 식이 두 개인데 오른쪽 변에 숫자를 세 개 넣으면 모양이 맞지 않는다는 오류가 납니다. 오류 문장이 길지만 마지막 괄호 (size 3 is different from 2)만 읽으면 됩니다.

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

식을 정리하지 않고 숫자 옮기기. x = 3 − y처럼 적힌 식은 먼저 x + y = 3으로 고쳐 "미지수는 왼쪽, 상수는 오른쪽"을 맞춘 뒤 행렬에 옮기세요. 빠진 미지수는 0으로 적습니다. 예를 들어 2x = 4는 [2, 0]행, 오른쪽 4입니다.

연습 문제 (5분)

  1. 연립방정식 2x + y = 4, x − y = 0.5를 실습대 슬라이더로 만들어 교점을 읽고, NumPy로 풀어 검산해 보세요.
  2. 실습대에서 '해 없음'을 고른 뒤 슬라이더 하나만 움직여 해가 하나가 되게 해 보세요. 어느 슬라이더를 움직였고, 그때 det는 어떻게 바뀌었나요?
  3. x + 2y = 2, 2x + 4y = c에서 해가 무수히 많아지는 c는 얼마인가요? 열 그림으로 설명해 보세요.

풀이

>>> A1 = np.array([[2, 1], [1, -1]])
>>> b1 = np.array([4, 0.5])
>>> x1 = np.linalg.solve(A1, b1)
>>> x1
array([1.5, 1. ])
>>> A1 @ x1
array([4. , 0.5])

2번은 c가 아닌 a나 b 슬라이더를 움직여야 합니다. c는 직선을 평행하게 옮길 뿐이라 기울기가 같은 채로 남습니다. 예를 들어 둘째 식의 b를 2에서 1.5로 바꾸면 det = 1×1.5 − 2×1 = −0.5가 되어 0이 아니게 되고, 두 직선이 한 점에서 만납니다.

3번은 c = 4입니다. 두 열 (1, 2)와 (2, 4)는 한 직선 위에 있고, 갈 수 있는 곳은 (1, 2) 방향의 직선뿐입니다. b = (2, c)가 그 직선 위에 있으려면 (2, 4)여야 합니다. 이때 둘째 식은 첫째 식의 두 배라 새 정보가 없습니다.

이번 교시 정리

  • 식 a x + b y = c 하나는 평면 위의 직선 하나, 연립방정식의 해는 두 직선의 교점이다 (행 그림).

  • 해는 하나, 없음, 무수히 셋 중 하나다. 하나가 아닌 두 경우는 두 직선의 기울기가 같다.

  • 연립방정식은 Ax = b로 쓴다. A의 한 행이 식 하나, x는 미지수, b는 오른쪽 변.

  • 열 그림: Ax = b는 'A의 열들을 몇 배씩 더해 b를 만드는' 일차결합 문제다.

  • det A ≠ 0이면 어떤 b든 해가 딱 하나, det A = 0이면 b에 따라 없거나 무수히 많다.

  • NumPy에서는 x = np.linalg.solve(A, b)로 풀고 np.allclose(A @ x, b)로 검산한다. inv(A) @ b보다 빠르고 정확하다.

다음 교시에는 det = 0일 때 평면이 납작해지며 어디로 가고 무엇이 사라지는지를 열공간과 영공간이라는 이름으로 정리합니다. 해가 왜 '무수히' 많아지는지도 그 그림으로 설명할 수 있습니다.

← 이전

7교시 · 행렬식과 역행렬

다음 →

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

새 교시·새 강의 소식 받기

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

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

질문과 답변

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

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

R

ReflexKR

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

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

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

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

Built with Reflex