Skip to content
psymon-ai
Go back

1. 지도 학습의 기초

1

Table of contents

Open Table of contents

들어가는 글

블로그를 시작하며 쓴 글에서 밝혔듯 나는 AI 연구자 출신이 아니다. LLaMA를 처음 맥북에서 돌리고 감탄하던 시절부터 지금까지, 나는 주로 모델을 ‘쓰는’ 쪽에 있었지 ‘만드는’ 쪽에 있지 않았다. 파인튜닝을 해보고 LoRA를 돌려보고 토크나이저를 뜯어보는 일은 해왔지만, 이 모든 작업을 관통하는 수학적 뼈대는 늘 흐릿했다.

첫 창업한 회사를 정리하고 다시 이 분야로 돌아오며 결심한 것 중 하나가 기초를 처음부터 다시 쌓기였다. 실용 중심으로만 공부하다 보면 어느 순간 벽에 부딪힌다. 논문을 읽어도 수식에서 막히고, 모델의 특정 행동을 설명하려 해도 말문이 막힌다. 어떤 문제를 근본부터 이해하려면 결국 그 아래 깔린 수학과 통계로 내려가야 한다.

이 시리즈는 그 다짐의 기록이다. 서울대학교 이준석 교수의 “머신러닝 및 딥러닝” 강의를 중심으로, 전공 수준의 내용을 내 언어로 재정리하는 노트다. 블로그의 다른 글이 현재 진행 중인 연구나 실험 기록이라면, 이 시리즈는 그 연구들을 받치는 지반 공사에 가깝다.

첫 주제는 가장 기본이 되는 지도 학습(Supervised Learning)이다. 이름만 들으면 별것 아닌 것 같지만 현대 딥러닝의 거의 모든 기법이 이 프레임워크의 확장이다. 최신 GPT 계열 모델도, BERT도, 이미지 분류기도, 그 뿌리는 지도 학습의 4단계 절차에 있다. 뿌리를 단단히 잡아놓자.

머신러닝이란 무엇인가

‘머신러닝’이라는 단어는 너무 익숙해서 오히려 정의가 흐릿하다. 일단 가장 큰 개념부터 정리해두자.

인공지능(AI, Artificial Intelligence)은 기계가 지능적으로 행동하도록 만드는 모든 시도를 포괄한다. 1950년대 튜링 테스트부터 시작된 이 아이디어는 규칙 기반 시스템, 전문가 시스템, 기호 처리 등 다양한 형태로 구현되어 왔다. 머신러닝이 등장하기 전에도 AI는 있었다는 뜻이다.

머신러닝(Machine Learning)은 그중 데이터 기반으로 지능을 구현하는 방법론이다. 사람이 명시적 규칙을 코딩하는 대신, 데이터를 주고 기계가 그 안의 패턴을 스스로 학습하게 만든다. 1990년대 이후 컴퓨팅 자원과 데이터가 풍부해지면서 AI의 주류로 올라섰다.

딥러닝(Deep Learning)은 머신러닝의 한 갈래로, 여러 층으로 이루어진 신경망(Neural Network)을 활용한다. 2012년 AlexNet 이후 폭발적으로 발전해 지금은 사실상 AI의 대명사처럼 쓰이지만, 엄밀히 말하면 머신러닝의 한 부분집합이다.

포함 관계로 정리하면 이렇다.

AI ⊃ 머신러닝 ⊃ 딥러닝

그렇다면 머신러닝의 본질은 무엇인가. 핵심은 규칙을 누가 쓰느냐에 있다. 전통적 프로그래밍에서는 사람이 규칙을 명시적으로 코딩한다. “신호등이 빨간색이면 멈춰라”처럼, 입력 조건과 그에 따른 행동을 일일이 손으로 작성한다. 이 방식은 규칙이 명확하고 유한한 영역에서 잘 작동한다. 회계 소프트웨어, 컴파일러, 운영체제 스케줄러 같은 것들이 그렇다.

현실의 많은 문제는 그렇지 않다. 스팸 메일을 판단하는 문제를 생각해보자. “‘공짜’라는 단어가 들어 있으면 스팸”이라는 규칙은 금방 뚫린다. 광고 업자는 곧 ‘공↗짜’처럼 특수문자를 끼워 넣는다. ‘당첨’, ‘축하합니다’, ‘한정수량’ 등 규칙을 계속 추가해도 끝이 없다. 스팸 발신자가 끊임없이 진화하기 때문이다.

사진 속 동물이 개인지 고양이인지 판단하는 문제는 더 어렵다. “뾰족한 귀가 있으면 고양이”라는 규칙을 쓸 수 있을까? 귀가 접힌 스코티시폴드는? 귀가 보이지 않는 각도로 찍힌 사진은? 이런 문제는 규칙이 아니라 패턴에서 답을 찾아야 한다. 사람이 직접 작성한 규칙 대신, 사진 수 천 장에서 기계가 스스로 패턴을 찾도록 하는 것이 머신러닝의 접근이다.

머신러닝은 크게 두 범주로 나뉜다.

지도 학습(Supervised Learning)은 정답이 붙어 있는 데이터, 즉 레이블(label)이 있는 데이터로 학습한다. “이 이미지는 고양이”, “이 이메일은 스팸”, “이 집의 가격은 8억”처럼 입력과 그에 대응하는 정답을 짝지어 준다. 모델의 목표는 이 쌍들로부터 입력에서 출력으로 가는 함수를 학습하는 것이다.

비지도 학습(Unsupervised Learning)은 정답 없는 데이터의 구조만 보고 패턴을 찾는다. “이 이미지를 비슷한 것끼리 묶어봐”, “이 문서의 주제를 찾아봐” 같은 문제다. 레이블 확보 비용이 크기 때문에 최근에는 자기지도학습(self-supervised learning)이 각광받는다. GPT 계열 모델이 다음 토큰을 예측하는 방식이 대표적이다.

이 외에도 강화학습(reinforcement learning), 준지도학습(semi-supervised learning) 등이 있지만, 모든 분야의 기초는 지도 학습이다. 가장 단순하고 명확하며 많이 연구되어 있다. 여기서부터 출발하자.

지도 학습의 4단계 - 간결한 프레임워크의 힘

지도 학습이 하는 일은 네 단계로 요약된다. 이 4단계를 머릿속에 확실히 새겨두면 이후 어떤 복잡한 모델을 만나도 당황하지 않는다. 이 프레임워크는 선형 회귀부터 트랜스포머까지 변하지 않기 때문이다.

모델 형태 결정 → 목표 정의 → 학습 → 예측

각 단계를 하나씩 풀어보자. 이 흐름의 체화가 이 글의 가장 중요한 목표다.

Step 1: 모델 형태 결정 (Functional Form)

지도 학습 전체 과정에서 사람이 유일하게 직접 개입하는 부분이다. 입력 xx와 출력 yy의 관계를 어떤 함수 형태로 표현할지 결정하는 단계다. 이 선택은 전적으로 개발자의 몫이고, 이후 모든 과정은 이 선택의 결과물 위에서 진행된다.

공부 시간(xx)과 시험 점수(yy)의 관계를 예측하고 싶다고 하자. 가장 단순한 가정은 선형 관계다. “공부 시간이 늘면 점수도 비례해서 늘 것이다”라는 직관을 수식으로 쓰면 이렇다.

f(x)=ax+bf(x) = ax + b

여기서 aa는 기울기, bb는 절편이다. 이 둘을 파라미터(parameter)라고 부른다. 파라미터 값을 우리는 아직 모른다. 그 값을 데이터로부터 찾아내는 일이 이후 단계다.

ml-1-2.png

선형 모델은 산점도 데이터를 직선으로 근사한다.

모델 형태가 왜 중요한가. 이 선택이 가설 공간(hypothesis space)을 결정하기 때문이다. 직선으로 가정하면 아무리 좋은 파라미터를 찾아도 결국 직선밖에 나오지 않는다. 실제 관계가 곡선이라면 선형 모델은 본질적 한계를 갖는다. 반대로 너무 복잡한 형태를 가정하면 이후 이야기할 과적합 위험이 커진다. 이 긴장 관계는 머신러닝 전반에 걸쳐 계속 등장한다.

현실에서 이 선택은 도메인 지식과 데이터 탐색을 바탕으로 이루어진다. 데이터를 먼저 산점도(scatter plot)로 그려보고, 대략 어떤 모양인지 살핀 뒤 적절한 함수 형태를 가정하는 식이다. 딥러닝 시대에는 이 선택이 ‘신경망 구조’로 옮겨갔지만, 근본적으로는 같은 일이다. 아키텍처 결정이 곧 모델 형태 결정이다.

Step 2: 목표 정의 (Objective Function)

모델 형태를 정했으면 이제 그 모델이 “좋은” 상태가 무엇인지 정의해야 한다. 모델의 예측값 f(x)f(x)가 실제 정답 yy에 가까워지기가 자연스러운 목표인데, 문제는 “가깝다”라는 말을 수학적으로 어떻게 정의하느냐다.

이 정의에는 수많은 선택지가 있다. 가장 흔히 쓰는건 평균 제곱 오차(MSE, Mean Squared Error)다.

MSE=1ni=1n(yif(xi))2\text{MSE} = \frac{1}{n}\sum_{i=1}^{n}(y_i - f(x_i))^2

각 데이터 포인트에서 예측과 실제의 차이를 구하고, 그것을 제곱해 평균 낸다. 이 값의 최소화가 학습 목표다.

이처럼 학습 과정에서 최적화하려는 함수를 목적 함수(objective function)라고 부른다. 목적 함수 중 예측값과 정답의 차이를 계산하는 함수는 손실 함수(loss function)라고 한다. 지금처럼 별도의 정규화 항이 없는 단순한 예제는 목적 함수와 손실 함수가 사실상 같은 역할을 한다.

왜 굳이 제곱인가. 이 질문에는 세 가지 대답이 있다.

첫째, 부호 상쇄를 방지한다. 그냥 차이를 더하기만 하면 양수 오차와 음수 오차가 서로 상쇄되어 평균 오차가 0이 되는 엉뚱한 상황이 생긴다. 제곱하면 모든 항이 양수가 된다.

둘째, 큰 오차에 더 큰 패널티를 준다. 1만큼 틀리면 1의 비용을 내지만 10만큼 틀리면 100의 비용을 낸다. 모델이 극단적으로 틀리는 경우를 적극적으로 피하게 만드는 효과가 있다.

셋째, 이것이 가장 깊은 이유인데, 확률론적으로 자연스러운 귀결이기 때문이다. 오차가 정규분포를 따른다고 가정하면 최대우도추정(Maximum Likelihood Estimation, MLE)의 결과가 정확히 MSE 최소화와 같아진다. 이 부분은 다음 글 선형 회귀 편에서 상세히 다룬다.

MSE만이 유일한 선택은 아니다. 이상치(outlier)에 강인한 평균 절대 오차(MAE), MSE와 MAE의 장점을 절충한 Huber loss, 분류 문제에서 정답 클래스의 확률을 높이기 위해 사용하는 크로스 엔트로피(cross-entropy), 특히 이진 분류에서 자주 쓰이는 Binary Cross-Entropy, 확률 분포 간 차이를 측정하는 KL 발산 등 다양한 손실 함수가 있다. 손실 함수 선택은 모델이 어떤 실수를 더 중요하게 여길지 결정한다. 따라서 손실 함수는 단순히 오차를 계산하는 공식이 아니라, 기술적 선택인 동시에 문제를 바라보는 방식이다.

Step 3: 학습 (Learning/Training)

모델 형태와 목적 함수가 정해졌으면, 이제 주어진 데이터 (x1,y1),(x2,y2),,(xn,yn)(x_1, y_1), (x_2, y_2), \ldots, (x_n, y_n)로부터 목적 함수를 최소화하는 파라미터를 찾아야 한다. 이 과정을 최적화(optimization)라고 부른다. 이 ‘찾는 과정’ 자체가 우리가 흔히 말하는 학습이다.

최적화 방법은 크게 두 가지로 나뉜다. 선형 회귀처럼 단순한 모델은 미분을 통해 수학적으로 정확한 해를 한 번에 구할 수 있다. 이를 닫힌 해(closed-form solution)라 한다. 수식을 풀어 바로 답을 얻는 방식이다.

더 복잡한 모델, 특히 신경망 같은 비선형 모델에는 닫힌 해가 존재하지 않는다. 이때는 경사 하강법(gradient descent) 같은 반복적 방법을 쓴다. 현재 파라미터에서 목적 함수가 감소하는 방향으로 조금씩 이동하며 최적점을 찾아간다. 이 방법은 로지스틱 회귀, 신경망 편에서 자세히 다룰 예정이니 지금은 ‘두 가지 경로가 있다’ 정도만 기억해두면 된다.

‘학습’이라는 단어가 가진 뉘앙스에 한 마디 덧붙이고 싶다. 우리는 이 단어를 인간의 학습에 비추어 해석하는 경향이 있다. 모델의 학습은 다르다. 목적 함수 값이 낮아지는 방향으로 숫자를 조정하는 수치 최적화 과정이지, 인간처럼 의미를 곱씹는 작업은 아니다. 이 차이를 잊으면 머신러닝 모델에 과도한 환상을 갖게 된다.

그러나 동시에 언어 모델을 단지 정교한 보간기(interpolator)로 보는 시각도 최신 연구 결과와 맞지 않다. 모델이 한참 동안 훈련 데이터를 외우다가 어느 시점 갑자기 일반화 능력을 갖는 그로킹(Grokking) 현상이나, 트랜스포머 내부에 구조화된 회로(circuits)가 형성된다는 사실을 볼 때 언어 모델을 단순 룩업 테이블로 환원하는 관점 역시 올바른 이해는 아니다.

Step 4: 예측 (Prediction/Inference)

학습이 끝나면 파라미터 값이 확정된다. 이제 이 모델은 새로운 xx가 들어오면 예측값 f(x)f(x)를 출력할 수 있다. 이 단계를 예측 혹은 추론(inference)이라 한다.

LLM을 돌려본 경험이 있는 사람이라면 ‘추론 비용’이라는 말이 익숙할 것이다. 바로 이 단계에서 나오는 연산 비용이다. 학습은 최소 몇 주, 보통은 몇 달이 걸리는 엄청난 작업이지만 한 번만 하면 된다. 반면 예측은 사용자가 요청할 때마다 매번 수행하므로 실제 운영 비용 대부분이 이 단계에서 나온다.

학습과 추론의 이런 비대칭성은 많은 설계 결정에 영향을 미친다. 양자화(quantization), 지식 증류(knowledge distillation), 캐싱, KV 캐시 등 모든 최적화 기법이 궁극적으로는 “학습 비용은 늘리더라도 추론을 빠르고 싸게 만들자”는 방향으로 수렴한다. 이 점을 이해하면 현대 LLM 서비스의 여러 선택이 자연스럽게 이해된다.

4단계를 코드로 따라가기

이제 이 4단계를 가장 단순한 예시로 직접 확인해보자. 공부 시간과 시험 점수 데이터에 선형 회귀를 적용하는 코드다.

import numpy as np
# 데이터 생성: 공부 시간 -> 시험 점수
x = np.array([1, 2, 3, 4, 5, 6, 7, 8])
y = np.array([20, 35, 45, 55, 60, 72, 80, 90])
# 평균 제곱 오차
def mse(y_true, y_pred):
return np.mean((y_true - y_pred) ** 2)
# 최소제곱법으로 직선 y = ax + b의 기울기와 절편을 계산
n = len(x)
sum_x = np.sum(x)
sum_y = np.sum(y)
sum_xy = np.sum(x * y)
sum_x2 = np.sum(x**2)
a = (n * sum_xy - sum_x * sum_y) / (n * sum_x2 - sum_x**2)
b = np.mean(y) - a * np.mean(x)
print(f"학습된 파라미터: a = {a:.2f}, b = {b:.2f}")
print(f"학습된 모델: f(x) = {a:.2f}x + {b:.2f}")
# 학습 데이터에 대한 MSE
y_pred = a * x + b
print(f"학습 MSE: {mse(y, y_pred):.2f}")
# 새로운 입력에 대한 예측
x_new = 10
y_new = a * x_new + b
print(f"공부 시간 {x_new}시간 → 예상 점수: {y_new:.1f}점")
학습된 파라미터: a = 9.54, b = 14.21
학습된 모델: f(x) = 9.54x + 14.21
학습 MSE: 4.23
공부 시간 10시간 예상 점수: 109.6점

코드로 보면 너무 단순해 보인다. 몇 줄의 수식 계산과 대입이 전부다. 그러나 이 단순함이 현대 머신러닝 토대가 된다. 파라미터 수가 2개에서 수십억 개가 되고, 함수 형태가 선형에서 깊은 신경망이 되고, 데이터가 수천 쌍에서 수조 토큰이 되더라도, 근본 구조는 그대로다. 모델 형태, 목적 함수, 학습, 예측. 이 네 단계.

하나 더 주목할 점이 있다. 예측 결과가 10시간에 109점이다. 100점이 만점인데 말이다. 이것은 모델이 현실의 제약을 모른다는 점을 드러낸다. 모델은 주어진 형태와 데이터 안에서만 최적이지 그 너머의 세계는 아무것도 알지 못한다. 이 한계에 대한 자각이 머신러닝을 현실에 적용할 때 매우 중요하다.

용어 정리 - 같은 개념에 여러 이름이 붙는 이유

머신러닝의 역사는 통계학, 컴퓨터 과학, 공학, 심리학이 뒤얽힌 역사다. 각 분야가 비슷한 개념에 서로 다른 이름을 붙여 왔고, 그 이름이 지금도 뒤섞여 쓰인다. 논문을 읽을 때 이 용어 차이가 낯설게 느껴지기 쉬우니 한 번 정리해두자.

예측하고자 하는 변수 yy타겟(target), 레이블(label), 리스폰스(response)라는 세 가지 이름으로 쓰인다. ‘target’은 공학 쪽에서, ‘label’은 분류 문제에서, ‘response’는 통계학에서 많이 쓰는 용어다. 셋 다 의미는 같다.

예측에 쓰는 변수 xx피처(feature), 인풋(input), 프레딕터(predictor), 때로는 공변량(covariate), 독립변수(independent variable)로도 불린다. ‘feature’는 머신러닝 커뮤니티에서 가장 흔하고, ‘covariate’와 ‘independent variable’은 통계학에서 주로 쓴다.

수학적 객체로 보면 이 입력 xx는 상황에 따라 세 가지 형태로 존재한다.

xx가 하나의 숫자일 때 이를 스칼라(scalar)라 부른다. 방향이 없고 크기만 있는 양이다. 숫자 여러 개를 순서대로 묶어놓으면 벡터(vector)가 된다. 벡터는 공간의 한 점 또는 한 방향으로 해석할 수 있다. 벡터를 여러 개 쌓아놓으면 행렬(matrix)이 된다. 행렬은 선형 변환을 나타내거나, 데이터 포인트 여러 개를 한꺼번에 표현하는 데 쓴다.

import numpy as np
# 스칼라: 숫자 하나
scalar = 3.14
print(f"스칼라: {scalar}, 차원: 0")
# 벡터: 숫자 여러 개를 한 줄로 나열한 값
vector = np.array([1.0, 2.0, 3.0])
print(f"벡터: {vector}, shape: {vector.shape}")
# 행렬: 벡터를 여러 줄로 쌓은 값
matrix = np.array([
[1, 2, 3],
[4, 5, 6],
])
print(f"행렬:\n{matrix}\nshape: {matrix.shape}")

이 위계가 중요한 까닭은, 실제 현업의 데이터 대부분이 행렬 혹은 그 이상의 텐서 형태이기 때문이다. 환자 1,000명에 대해 각각 20개 피처를 갖는 데이터라면 1000×201000 \times 20 크기의 행렬이 된다. 이미지 한 장은 높이 × 너비 × 채널의 3차원 텐서고, 이미지 여러 장을 배치로 묶으면 4차원 텐서다. 모든 딥러닝 연산이 텐서 위에서 일어나므로 이 구조에 익숙해져야 한다.

회귀와 분류 - 출력의 종류가 결정하는 것

지도 학습은 다시 두 갈래로 나뉜다. 기준은 예측하려는 출력 y\boldsymbol{y}가 어떤 종류의 값인가다.

회귀(Regression)yy연속적 실수일 때의 문제다. 광고비 대비 매출, 집값, 내일의 기온, 주가 예측이 여기 속한다. 출력이 이론상 무한히 많은 값 중 하나를 가질 수 있다는 점이 특징이다.

분류(Classification)yy이산적 범주일 때의 문제다. 스팸이냐 정상이냐, 개냐 고양이냐, 숫자 0부터 9 중 어느 것이냐 같은 문제다. 가능한 출력 값이 유한하게 정해져 있다.

모델 쪽 용어도 이에 맞게 달라진다. 회귀 모델을 리그레서(regressor), 분류 모델을 분류기(classifier)라고 부른다. 평가 방식도 차이가 있다.

  • 회귀 평가: MSE, RMSE, MAE, R² 같은 지표. 예측과 실제 사이의 거리를 잰다. 작을수록 좋다.
  • 분류 평가: 정확도(Accuracy)가 가장 기본. 전체 중 맞게 분류한 비율이다. 클수록 좋다. 나중에는 정밀도(precision), 재현율(recall), F1 스코어, AUC 같은 더 정교한 지표도 다룬다.
import numpy as np
# 실제 레이블과 모델 예측
y_true = np.array([0, 1, 1, 0, 1, 0, 1, 1])
y_pred = np.array([0, 1, 0, 0, 1, 1, 1, 1])
# 전체 중 맞게 예측한 비율
accuracy = np.mean(y_true == y_pred)
print(f"정확도: {accuracy:.2%}")
# 8개 중 6개 맞춤 → 75%

주의할 점은 회귀와 분류의 경계가 생각보다 모호하다는 것이다. ‘내일 비가 올 확률’을 예측하는 문제는 출력이 0과 1 사이의 실수이므로 회귀처럼 보이지만, 사실은 이진 분류 문제를 확률로 표현한 것이다. 로지스틱 회귀라는 이름에 ‘회귀’가 들어 있지만 실제로는 분류 모델인 것도 같은 맥락이다. 이 둘은 서로 변환 가능하며, 실제로 많은 모델이 내부적으로는 회귀처럼 연속값을 뽑고 마지막에 분류로 변환하는 구조를 가진다.

분류는 클래스 수에 따라 다시 나뉜다. 두 클래스만 있으면 이진 분류(binary classification), 세 개 이상이면 다중 클래스 분류(multiclass classification)다. 이미지 한 장에 여러 레이블이 동시에 붙을 수 있는 경우는 다중 레이블 분류(multilabel classification)라 하며, 훨씬 까다로운 문제다.

파라메트릭 vs 논파라메트릭 - 모델의 두 가지 철학

머신러닝 모델은 또 다른 기준으로도 나눌 수 있다. 함수 형태를 미리 가정하느냐 아니냐다.

파라메트릭 모델(Parametric Model)은 함수 형태를 먼저 가정하고, 그 안의 파라미터를 데이터에서 학습한다. 앞서 본 선형 회귀가 대표적이다.

y=β0+β1x1+β2x2++βpxpy = \beta_0 + \beta_1 x_1 + \beta_2 x_2 + \cdots + \beta_p x_p

이 식에서 우리는 이미 ‘yyxx들의 선형 결합으로 표현된다’는 형태를 고정했다. 학습 과정에서 바뀌는 값은 β\beta뿐이다. 데이터가 아무리 많아져도 파라미터 개수는 변하지 않는다. 피처가 pp개면 언제나 p+1p+1개의 파라미터를 가진다.

파라메트릭 모델의 장점은 두 가지다. 첫째, 해석이 명확하다. “광고비를 1만원 늘리면 매출이 β1\beta_1만큼 오른다”라는 식으로 각 계수의 의미를 인간의 언어로 번역할 수 있다. 이는 의사결정이나 정책 수립이 필요한 분야(의료, 금융, 사회과학)에서 큰 장점이 된다. 둘째, 데이터가 적어도 작동한다. 가정이 맞다면 적은 데이터로도 안정적인 추정이 가능하다.

단점은 명확하다. 가정이 틀리면 성능이 무너진다. 현실이 곡선인데 직선으로 끼워 맞추면, 아무리 좋은 파라미터를 찾아도 근본적 한계가 있다. 이를 모델의 편향(bias)이라 한다.

논파라메트릭 모델(Non-parametric Model)은 함수 형태를 미리 가정하지 않는다. 대신 데이터 자체를 ‘외우고’ 있다가 새 입력이 들어오면 주변 데이터를 참고해 답을 낸다. 이름이 ‘파라미터가 없는’처럼 들리지만, 실제로는 파라미터 수가 데이터 크기에 따라 늘어난다는 뜻이다. 파라미터가 고정된 것이 아니라 유연하다는 뜻이지, 아예 없다는 뜻이 아니다.

가장 단순한 예가 K-최근접 이웃(K-Nearest Neighbors, KNN)이다. 새 데이터가 들어오면 학습 데이터에서 가장 가까운 K개를 찾고, 다수결로 분류한다. 회귀라면 평균을 낸다.

ml-1-3.png

import numpy as np
# 학습 데이터
x_train = np.array([
[1, 2],
[2, 3],
[3, 1],
[6, 5],
[7, 7],
[8, 6],
])
y_train = np.array([0, 0, 0, 1, 1, 1])
# 새로운 데이터
x_new = np.array([5, 5])
k = 3
# 거리 계산
distances = np.sqrt(np.sum((x_train - x_new) ** 2, axis=1))
print("각 학습 데이터까지의 거리:")
for i, (distance, label) in enumerate(zip(distances, y_train)):
print(f" 데이터 {i}: 거리={distance:.2f}, 클래스={label}")
# 가장 가까운 k개 선택
nearest_indices = np.argsort(distances)[:k]
nearest_labels = y_train[nearest_indices]
print(f"\n가장 가까운 {k}개의 클래스: {nearest_labels}")
# 다수결
prediction = np.bincount(nearest_labels).argmax()
print(f"예측 결과: 클래스 {prediction}")
학습 데이터까지의 거리:
데이터 0: 거리=5.00, 클래스=0
데이터 1: 거리=3.61, 클래스=0
데이터 2: 거리=4.47, 클래스=0
데이터 3: 거리=1.00, 클래스=1
데이터 4: 거리=2.83, 클래스=1
데이터 5: 거리=3.16, 클래스=1
가장 가까운 3개의 클래스: [1 1 1]
예측 결과: 클래스 1

KNN의 장점은 어떤 형태의 관계든 표현할 수 있다는 점이다. 데이터만 충분하면 아무리 복잡한 결정 경계도 근사할 수 있다. 또 학습 과정이 따로 없다. 새 데이터가 들어올 때 계산이 일어날 뿐이다.

단점도 명확하다. 모든 학습 데이터를 메모리에 가지고 있어야 하고, 예측 시 모든 학습 데이터와의 거리를 계산해야 하므로 추론이 느리다. 차원의 저주(curse of dimensionality)라는 악명 높은 문제도 있다. 피처 수가 늘어나면 ‘가깝다’는 개념이 의미를 잃고, 모든 점이 비슷하게 멀리 떨어진다. 따라서 KNN은 저차원 문제에서 주로 쓰고, 고차원 이미지나 텍스트에는 잘 맞지 않다.

현대 딥러닝 모델은 이 두 범주의 경계 위에 있다. 신경망은 이론상으로는 파라미터 수가 고정된 파라메트릭 모델이지만, 파라미터가 수억에서 수조 개까지 가능하므로 실용상 거의 논파라메트릭에 가까운 유연성을 가진다. 이런 초대형 모델의 등장이 전통적 파라메트릭/논파라메트릭 구분을 어느 정도 흐려놓았지만, 그 바탕이 된 직관은 여전히 유효하다.

과적합과 과소적합 - 일반화라는 진짜 목표

이제 머신러닝의 가장 본질적 문제로 넘어간다. 모델을 학습할 때 초보자가 가장 먼저 마주치는 당황스러운 현상, 바로 과적합(Overfitting)이다.

과적합은 모델이 학습 데이터에는 완벽하게 맞지만, 처음 보는 데이터에서는 엉터리 예측을 하는 현상이다. 시험 족보만 달달 외운 학생이 막상 시험장에서 새 문제를 받으면 풀지 못하는 것과 같다. 모델이 학습 데이터의 일반적 패턴을 학습한 것이 아니라, 그 특정 데이터 세트에 있는 우연한 노이즈까지 외워버린 상태다.

반대로 과소적합(Underfitting)은 모델이 너무 단순해서 학습 데이터의 기본 패턴조차 제대로 잡지 못하는 경우다. 데이터 속에 명백한 곡선 관계가 있는데 직선으로 끼워 맞추려 하면 양쪽 모두에서 실패한다.

두 현상 모두 결국 일반화(generalization) 성능의 실패다. 머신러닝의 진짜 목표는 학습 데이터에서의 성능이 아니다. 학습에 쓰지 않은 새로운 데이터에서의 성능이다. 반드시 기억해야 한다.

ml-1-4.png

데이터를 가장 잘 따라간 것처럼 보이는 초록색 모델은 과적합으로 오히려 성능이 떨어진다.

예시로 확인해보자. 사인 곡선을 따르는 데이터에 노이즈를 약간 섞어놓고, 세 가지 차수의 다항 회귀를 적용한 결과다.

import numpy as np
# 학습 데이터 생성
np.random.seed(1)
x_train = np.linspace(0, 1, 10)
y_true = np.sin(2 * np.pi * x_train)
y_train = y_true + np.random.randn(10) * 0.3
# 과소적합: 1차 다항식
coeff_1 = np.polyfit(x_train, y_train, 1)
train_pred_1 = np.polyval(coeff_1, x_train)
# 적절한 모델: 3차 다항식
coeff_3 = np.polyfit(x_train, y_train, 3)
train_pred_3 = np.polyval(coeff_3, x_train)
# 과적합: 9차 다항식
coeff_9 = np.polyfit(x_train, y_train, 9)
train_pred_9 = np.polyval(coeff_9, x_train)
# 평균 제곱 오차
def mse(y_true, y_pred):
return np.mean((y_true - y_pred) ** 2)
print(f"1차 (과소적합) 학습 MSE: {mse(y_train, train_pred_1):.4f}")
print(f"3차 (적절) 학습 MSE: {mse(y_train, train_pred_3):.4f}")
print(f"9차 (과적합) 학습 MSE: {mse(y_train, train_pred_9):.4f}")
# 테스트 데이터로 평가
x_test = np.linspace(0, 1, 100)
y_test = np.sin(2 * np.pi * x_test)
test_pred_1 = np.polyval(coeff_1, x_test)
test_pred_3 = np.polyval(coeff_3, x_test)
test_pred_9 = np.polyval(coeff_9, x_test)
print(f"\n1차 (과소적합) 테스트 MSE: {mse(y_test, test_pred_1):.4f}")
print(f"3차 (적절) 테스트 MSE: {mse(y_test, test_pred_3):.4f}")
print(f"9차 (과적합) 테스트 MSE: {mse(y_test, test_pred_9):.4f}")
1차 (과소적합) 학습 MSE: 0.2598
3차 (적절) 학습 MSE: 0.1104
9차 (과적합) 학습 MSE: 0.0000
1차 (과소적합) 테스트 MSE: 0.2178
3차 (적절) 테스트 MSE: 0.0361
9차 (과적합) 테스트 MSE: 3.9560

결과를 천천히 살펴보자. 학습 데이터에서의 MSE만 보면 9차 다항식이 압도적으로 우수하다. 거의 0에 가까운 값이다. 초보자라면 “이 모델이 가장 좋구나”라고 결론 내리기 쉽다. 테스트 데이터로 가면 상황이 완전히 뒤바뀐다. 9차 다항식의 MSE는 3.9560이다. 3차 다항식의 테스트 MSE인 0.0361보다 훨씬 크다.

왜 이런 일이 벌어졌을까. 9차 다항식은 10개의 데이터 포인트를 거의 정확히 지나가는 곡선을 만들 만큼 유연하다. 그 유연함 때문에 데이터에 섞인 노이즈까지 외워버렸다. 노이즈는 말 그대로 무작위이므로, 새 데이터에서는 전혀 다른 방향의 노이즈가 있을 것이다. 과적합된 모델이 그 방향을 맞출 리 없다.

반면 3차 다항식은 학습 MSE는 9차보다 높지만 테스트 MSE는 훨씬 낮다. 이 모델이 사인 곡선이라는 진짜 패턴을 잘 포착한 덕이다. 1차 모델은 너무 단순해서 사인 곡선을 직선으로 근사하려다 양쪽 모두에서 실패했다. 과소적합의 전형이다.

이 예시가 주는 교훈은 명확하다. 모델의 성능은 반드시 학습에 쓰지 않은 데이터로 평가해야 한다. 그래서 데이터를 훈련 세트와 테스트 세트로 나누기가 표준 절차가 되었다. 모델을 선택할 때도 훈련 정확도가 아니라 검증(validation) 정확도를 기준으로 삼아야 한다. 이 원칙이 지켜지지 않으면 겉으로는 아름답지만 실전에서는 쓸모없는 모델이 나온다.

과적합을 줄이는 방법은 여러 가지다. 모델 복잡도를 낮추기가 가장 직관적이다. 데이터를 더 많이 모으기도 좋은 방법이다. 정규화(regularization)로 파라미터가 너무 커지지 않도록 제약을 거는 방법, 드롭아웃(dropout)처럼 학습 중 일부 뉴런을 무작위로 꺼버리는 방법, 조기 종료(early stopping)로 학습이 너무 오래 진행되지 않도록 하는 방법 등이 있다. 이후 시리즈에서 하나씩 다룬다.

바이어스-분산 트레이드오프: 모든 것의 뿌리

과적합과 과소적합의 뿌리를 더 깊이 들여다보면 바이어스-분산 트레이드오프(Bias-Variance Tradeoff)라는 개념에 도달한다. 이 개념은 머신러닝 전반을 관통하는 핵심 원리이므로, 이번 글의 마지막 주제로 다룬다.

모델의 예측 오차는 수학적으로 세 항으로 분해된다.

총 오차=Bias2+Variance+노이즈\text{총 오차} = \text{Bias}^2 + \text{Variance} + \text{노이즈}

각 항의 의미를 하나씩 살펴보자.

바이어스(Bias)는 모델이 가진 단순한 가정에서 비롯되는 오차다. 현실은 곡선인데 모델은 직선만 그릴 수 있다면, 아무리 좋은 데이터와 학습 알고리즘을 써도 이 곡선을 완벽히 잡아낼 수 없다. 이 ‘근본적 한계’가 바이어스다. 바이어스가 높으면 과소적합이 생긴다.

분산(Variance)은 모델이 학습 데이터의 변화에 얼마나 민감한가를 나타낸다. 학습 데이터가 조금만 바뀌어도 모델이 크게 달라지면 분산이 높은 것이다. 분산이 높으면 과적합이 생긴다. 9차 다항식이 노이즈에 휩쓸려 매번 다른 곡선을 그리는 것은 분산이 높기 때문이다.

노이즈는 데이터 자체에 내재된 무작위성이다. 아무리 좋은 모델을 써도 제거할 수 없는 환원 불가능한(irreducible) 오차다. 같은 공부 시간이라도 시험 당일 컨디션에 따라 점수가 달라지는 부분이 여기 해당한다.

트레이드오프의 본질은 이것이다. 단순한 모델은 바이어스가 높고 분산이 낮다. 복잡한 모델은 바이어스가 낮고 분산이 높다. 한쪽을 줄이려 하면 다른 한쪽이 늘어난다. 이상적인 모델은 둘 다 낮은 지점이지만, 그 지점을 찾기가 쉽지 않다.

간단한 비유를 들어보자. 다트 판에 다트를 열 번 던진다고 상상해보자. 바이어스가 높고 분산이 낮은 상태는 다트가 한쪽에 몰려 있지만 과녁 중심에서 멀리 떨어진 경우다. 일관되게 빗나간다. 바이어스가 낮고 분산이 높은 상태는 평균적으로 과녁 중심 근처에 떨어지지만 여기저기 흩어진 경우다. 가끔은 잘 맞히지만 예측이 어렵다. 이상은 중심 가까이에 촘촘히 모이는 상태다. 이게 바이어스와 분산이 모두 낮은 상태다.

import numpy as np
# 바이어스-분산 트레이드오프를 확인해보는 예시
import numpy as np
# 바이어스-분산 트레이드오프 시뮬레이션
def simulate_models(n_trials=200, n_samples=50, seed=42):
np.random.seed(seed)
def true_fn(x):
return np.sin(2 * np.pi * x)
# 테스트 데이터
x_test = np.linspace(0, 1, 200)
y_test = true_fn(x_test)
simple_predictions = []
complex_predictions = []
for _ in range(n_trials):
# 매번 다른 학습 데이터 생성
x_train = np.random.uniform(0, 1, n_samples)
y_train = true_fn(x_train) + np.random.normal(0, 0.2, n_samples)
# 단순 모델: 1차 다항식
coeff_1 = np.polyfit(x_train, y_train, 1)
simple_predictions.append(np.polyval(coeff_1, x_test))
# 복잡 모델: 9차 다항식
coeff_9 = np.polyfit(x_train, y_train, 9)
complex_predictions.append(np.polyval(coeff_9, x_test))
simple_predictions = np.array(simple_predictions)
complex_predictions = np.array(complex_predictions)
# 바이어스: 평균 예측과 진짜 함수의 차이
bias2_simple = np.mean((simple_predictions.mean(axis=0) - y_test) ** 2)
bias2_complex = np.mean((complex_predictions.mean(axis=0) - y_test) ** 2)
# 분산: 학습 데이터가 바뀔 때 예측이 흔들리는 정도
var_simple = np.mean(simple_predictions.var(axis=0))
var_complex = np.mean(complex_predictions.var(axis=0))
print(f"단순 모델(1차): Bias² = {bias2_simple:.4f}, Variance = {var_simple:.4f}")
print(f"복잡 모델(9차): Bias² = {bias2_complex:.4f}, Variance = {var_complex:.4f}")
simulate_models()
단순 모델(1차): Bias² = 0.1997, Variance = 0.0104
복잡 모델(9차): Bias² = 0.0037, Variance = 0.3903

시뮬레이션을 돌려보면, 1차 다항식은 바이어스가 높고 분산이 낮고, 9차 다항식은 그 반대라는 점이 수치로 드러난다. 실제로 어느 쪽이 총 오차가 더 낮은지는 데이터와 상황에 따라 다르다. 이 트레이드오프를 의식하며 모델을 선택하는 것이 머신러닝 엔지니어의 핵심 역량 중 하나다.

덧붙이고 싶은 이야기가 있다. No Free Lunch 정리라는 유명한 이론이다. “모든 문제에 대해 다른 모든 알고리즘보다 평균적으로 우수한 알고리즘은 없다”는 내용이다. 다시 말해 만능 알고리즘은 없다. 어떤 알고리즘이 특정 문제에서 잘 작동하려면, 그 문제의 구조에 맞는 가정(inductive bias)을 잘 담고 있어야 한다. 선형 회귀는 선형 관계에, CNN은 이미지의 국소성과 평행이동 불변성에, 트랜스포머는 시퀀스 간 관계에 잘 맞는 귀납적 편향을 가진다. ‘어떤 모델이 가장 좋은가’라는 질문은 ‘어떤 문제에 대해?‘라는 반문 없이 답할 수 없다.

나가는 글 - 단순함의 미학

지도 학습의 핵심은 놀라울 정도로 단순하다.

모델 형태를 정하고, 오차를 어떻게 계산할지 정한 뒤 오차를 줄이는 파라미터를 찾아 새로운 데이터에 적용한다.

이 한 문장이 선형 회귀부터 트랜스포머까지 이어지는 모든 머신러닝의 뼈대다. 위로 올라갈수록 모델이 복잡해지고 파라미터가 늘어나고 목적 함수가 정교해지지만, 근본 구조는 변하지 않는다. 4단계 프레임워크, 회귀와 분류의 구분, 파라메트릭과 논파라메트릭의 대조, 바이어스-분산의 긴장 관계. 이 개념들이 이후 마주칠 모든 모델을 이해하는 토대가 된다.

물론, 4단계의 뼈대를 아는 것과, 그 뼈대 위에서 실제 문제를 푸는 것은 완전히 다른 수준의 일이다. 그 간극을 메우는 게 이 시리즈의 목표다. 다음 글에서는 이 프레임워크를 가장 구체적으로 실현하는 첫 번째 모델, 선형 회귀(Linear Regression)를 본격적으로 파헤쳐 본다. 닫힌 해의 수학적 유도, 최대우도추정과의 연결, 행렬 표기로의 일반화까지 머신러닝의 가장 기본이 되는 도구를 천천히 들여다보자.






이미지 출처: [ML/DL] Lecture 2. Introduction to Supervised Learning