"두 수를 더하면 3이고, 빼면 1이다. 두 수는 무엇일까?" 중학교에서 풀던 연립방정식입니다. 선형대수가 가장 많이 쓰이는 곳이 바로 이런 문제를 푸는 일입니다. 다만 실제로는 식이 두 개가 아니라 수천, 수백만 개입니다.
개발자 주변에도 연립방정식은 많습니다. 게임에서 두 광선이 어디서 만나는지 구하기, 회로나 물리 시뮬레이션에서 힘의 균형 맞추기, 이미지 처리에서 필터를 되돌리기, 머신러닝에서 가중치를 구하기가 모두 "미지수 여러 개, 식 여러 개"를 한꺼번에 푸는 일입니다.
이번 교시에는 연립방정식을 두 가지 그림으로 봅니다. 식 하나를 직선 하나로 보는 행 그림, 그리고 4교시의 일차결합으로 보는 열 그림입니다. 7교시의 행렬식이 "해가 하나뿐인가"를 알려 주는 신호라는 것도 확인하고, 마지막으로 NumPy의 np.linalg.solve로 직접 풉니다.
식 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이니 두 식을 모두 만족합니다.
해가 하나: 두 직선이 한 점에서 만납니다
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.])실습대 오른쪽의 슬라이더 a, b, c는 각 식 a x + b y = c의 숫자입니다. 몇 가지를 직접 확인해 보세요.
a x = c라 세로선, a가 0이면 가로선이 됩니다.1.67처럼 반올림되어 보일 때도 있습니다.슬라이더 이름 b는 "y 앞의 숫자"라는 뜻이고, 잠시 뒤에 나올 오른쪽 변의 벡터 b와는 다른 것입니다. 책도 이 두 가지에 같은 글자를 쓰는 경우가 많으니 문맥으로 구별하세요.
직선 두 개가 놓이는 방법은 세 가지뿐입니다. 실습대의 '미리 만든 식'에 세 경우가 모두 들어 있습니다.
| 미리 만든 식 | 두 식 | 그림 | 해 |
|---|---|---|---|
| 해 하나 | 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가 되는 순간 겹치면서 상태 문구가 '해가 무수히 많습니다'로 바뀝니다.
식이 많아지면 x, y를 일일이 적기 번거롭습니다. 그래서 숫자만 뽑아 행렬과 벡터로 적습니다. 왼쪽 변의 숫자는 행렬 , 미지수는 벡터 , 오른쪽 변은 벡터 입니다.
행렬의 한 행이 식 하나입니다. 5교시에서 본 "행과의 내적"으로 를 계산하면, 첫째 행 과 의 내적이 , 둘째 행 과의 내적이 가 되어 원래 식이 그대로 돌아옵니다.
| 책에 적힌 모양 | 읽는 법 | 뜻 | 파이썬에서는 |
|---|---|---|---|
| 에이 엑스는 비 | 행렬 A로 x를 옮기면 b가 된다 | A @ x 가 b 와 같다 | |
| 에이 아이 제이 | A의 i행 j열 숫자 (1부터 셈) | A[i-1, j-1] | |
| 엑스는 에이 역 비 | 7교시의 역행렬로 되돌리면 해가 나온다 | np.linalg.solve(A, b) | |
| 디터미넌트 에이는 0이 아니다 | 평면이 납작해지지 않는다 | np.linalg.det(A) |
를 변환으로 읽으면 이런 질문이 됩니다. "변환 를 거친 뒤 에 도착하는 벡터 는 무엇인가?" 7교시에서 은 변환을 되돌리는 행렬이었으니, 를 되돌리면 출발점 가 나옵니다. 이 생각은 맞지만, 실제 계산에서는 역행렬을 만들지 않습니다. 이유는 8-5에서 봅니다.
같은 식을 이번에는 세로로 읽어 봅니다. 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)이 곧 일차결합의 계수입니다.
이제 세 경우를 열 그림으로 다시 읽으면 이유가 보입니다.
정리하면, 7교시의 행렬식과 이렇게 이어집니다.
| det A | 두 열 | 변환 | 연립방정식 |
|---|---|---|---|
| 0이 아님 | 일차독립 | 되돌릴 수 있음 (역행렬 있음) | 어떤 b든 해가 딱 하나 |
| 0 | 한 직선 위 (일차종속) | 평면이 납작해짐 | b에 따라 해가 없거나 무수히 많음 |
det = 0일 때 "없음"과 "무수히" 중 어느 쪽인지는 b가 납작해진 직선 위에 있느냐에 달려 있습니다. 이 구분은 다음 교시의 열공간에서 정확한 이름을 얻습니다.
실습대 아래 "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("해가 하나로 정해지지 않습니다")
해가 하나로 정해지지 않습니다8-3에서 해는 A⁻¹b라고 했으니 np.linalg.inv(A) @ b로도 같은 답이 나옵니다.
>>> np.linalg.inv(A) @ b
array([2., 1.])하지만 실무에서는 거의 언제나 solve를 씁니다. 이유는 두 가지입니다.
직접 비교해 보고 싶다면 아래 코드를 실행해 보세요. 걸리는 시간은 컴퓨터마다 다르니 결과는 적어 두지 않았습니다. 보통 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"로 기억해 두세요.
인자 순서 바꾸기. 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입니다.
2x + y = 4, x − y = 0.5를 실습대 슬라이더로 만들어 교점을 읽고, NumPy로 풀어 검산해 보세요.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일 때 평면이 납작해지며 어디로 가고 무엇이 사라지는지를 열공간과 영공간이라는 이름으로 정리합니다. 해가 왜 '무수히' 많아지는지도 그 그림으로 설명할 수 있습니다.
새 교시·새 강의 소식 받기
이 강의에 새 교시가 더해지거나 이어지는 강의가 열리면 이메일로 알려 드려요.
R
ReflexKR
눌러 보며 배우는 파이썬과 Reflex. 모든 예제는 Reflex 0.10.0에서 직접 실행해 확인했습니다.
ReflexKR은 배너 광고 없이 무료로 운영돼요. 도움이 되셨다면 커피 한 잔으로 응원해 주세요.
© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
질문과 답변
이 교시에서 궁금한 점을 남겨 주세요. 모든 언어 페이지의 질문이 함께 모입니다.