
메타 디스크립션: AI 입문 강의 day2 공부 기록. 선형회귀와 로지스틱회귀를 NumPy로 직접 구현하며 경사하강법의 원리를 이해하는 과정을 정리했습니다.
키워드: 선형회귀 NumPy 구현, 로지스틱회귀 경사하강법, 지도학습 기초
예상 읽기 시간: 7분
카테고리: AI 입문 / AI Math
태그: AI수학, 선형회귀, 로지스틱회귀, 경사하강법, 머신러닝, 공부기록
AI 강의 day2 내용을 정리한 공부 기록입니다.
day1에서 NumPy 기초를 익혔는데, day2에서는 그 NumPy를 직접 써서 머신러닝 알고리즘을 구현합니다. 라이브러리 한 줄로 끝낼 수 있는 걸 굳이 밑바닥부터 짜는 이유는 "왜 이렇게 동작하는지"를 모르면 나중에 모델이 이상하게 동작할 때 손도 못 댄다는 걸 알기 때문인 것 같습니다. 직접 짜보고 나서 경사하강법이 머릿속에 훨씬 잘 남는 것 같아서 이번 글도 코드 중심으로 정리해 둡니다.
대상 독자는 머신러닝 개념은 들어봤지만 수식이 어렵게 느껴지는 분, 혹은 Scikit-learn만 써봐서 내부 동작이 궁금한 분입니다.
지도학습은 정답(label)이 있는 데이터를 학습해서 새 데이터의 정답을 예측하는 방식입니다. 구조는 아주 단순합니다.
입력(x) → 함수(f) → 예측값(ŷ)
이 "함수 f를 어떻게 잘 만들 것인가"가 머신러닝의 핵심입니다. 강의에서는 seaborn에 내장된 tips 데이터셋을 사용했습니다. 식당에서 전체 금액(total_bill)을 알 때 팁(tip)이 얼마일지 예측하는 문제입니다.
import seaborn as sns
df = sns.load_dataset("tips")
💡 핵심 포인트: 지도학습은 입력과 정답 쌍을 대량으로 보여주면서 함수를 조금씩 다듬는 과정입니다. 선형회귀와 로지스틱회귀 모두 이 틀 안에 있습니다.
선형회귀는 y = ax + b 형태의 직선을 데이터에 가장 잘 맞추는 a와 b를 찾는 문제입니다. 전체 금액이 늘어날수록 팁도 늘어나는 경향이 있다면, 그 경향을 직선 하나로 표현하는 것입니다.
모델이 얼마나 틀렸는지 측정하는 지표가 두 가지 있습니다.
# MAE (Mean Absolute Error) — 오차 절댓값의 평균
error = np.sum(np.abs(y - y_hat))
# MSE (Mean Squared Error) — 오차 제곱의 평균
error = np.mean((y - y_hat) ** 2)
두 지표의 차이는 큰 오차를 얼마나 중하게 볼 것인가입니다. MSE는 오차를 제곱하기 때문에 크게 틀린 예측에 더 강한 패널티를 줍니다. 선형회귀에서는 보통 MSE를 사용합니다.
💡 핵심 포인트: MAE는 이상치(outlier)에 덜 민감하고, MSE는 큰 오차에 더 민감합니다. 데이터 성격에 따라 선택이 달라집니다.
a와 b를 어떻게 찾을지에 대한 접근법이 세 가지 나왔습니다.
방법 1 — 그리드 서치(Grid Search)
a, b 값의 범위를 정해두고 모든 조합을 직접 계산해서 오차가 가장 작은 조합을 고릅니다. 직관적이지만 탐색 범위가 넓어질수록 계산량이 폭발적으로 늘어나서 실전에서는 쓰기 어렵습니다.
방법 2 — 정규방정식(Normal Equation)
수학적으로 MSE를 최소화하는 a와 b를 해석적으로 한 번에 구합니다.
X_b = np.c_[np.ones((X.shape[0], 1)), X] # bias 항 추가
b, a = np.linalg.inv(X_b.T @ X_b) @ (X_b.T) @ Y
반복 계산 없이 단 한 번의 계산으로 정답을 냅니다. 단, 행렬의 역행렬을 직접 구하기 때문에 데이터가 매우 크거나 변수가 많아지면 계산 비용이 급격히 올라가서 한계가 있습니다.
방법 3 — 경사하강법(Gradient Descent)
실제로 가장 많이 쓰이는 방법입니다. 오차를 줄이는 방향으로 a와 b를 조금씩 반복 업데이트합니다.
for epoch in range(epochs):
y_hat = a * X + b
error = y_hat - y
grad_a = np.mean(error * X)
grad_b = np.mean(error)
a -= lr * grad_a
b -= lr * grad_b
lr은 learning rate(학습률)입니다. 한 번에 얼마나 크게 이동할지를 결정하는 값인데, 너무 크면 오차가 발산하고 너무 작으면 학습이 너무 느립니다. 잘 조절하는 것이 중요합니다.
[이미지 위치: 경사하강법 개념 — 오차 곡선에서 기울기 방향으로 이동하는 모습 — alt: "경사하강법 그래프, 손실함수 최솟값을 향해 반복 이동"]
로지스틱 회귀는 이름에 "회귀"가 붙어 있지만 실제로는 분류(Classification) 문제를 푸는 알고리즘입니다. "이 사람이 팁을 많이 줄 사람인가, 아닌가" 같은 예/아니오 형태의 질문에 답하는 것입니다.
선형회귀의 출력은 연속적인 숫자지만, 분류 문제는 0~1 사이의 확률이 필요합니다. 여기서 시그모이드(Sigmoid) 함수가 등장합니다.
def sigmoid(z):
return 1 / (1 + np.exp(-z))
어떤 실수 값이 들어와도 0과 1 사이로 압축해 줍니다. 출력값을 "이 클래스일 확률"로 해석할 수 있게 되는 것입니다.
💡 핵심 포인트: 시그모이드 함수 하나로 선형회귀의 출력이 확률로 바뀝니다. 딥러닝 활성화 함수의 시작점이기도 합니다.
분류 문제에서는 MSE 대신 크로스 엔트로피를 씁니다.
eps = 1e-10 # log(0) 방지용 작은 값
loss = -np.mean(y * np.log(y_hat + eps) + (1 - y) * np.log(1 - y_hat + eps))
MSE를 분류 문제에 쓰면 경사하강법이 잘 작동하지 않는 문제가 있습니다. 크로스 엔트로피는 틀린 예측에 로그 스케일로 강한 패널티를 주기 때문에 분류 문제에 더 적합합니다. eps를 더하는 이유는 log(0)이 수학적으로 정의되지 않기 때문입니다.
선형회귀와 구조가 거의 같습니다. 차이는 예측값을 구할 때 시그모이드를 씌우는 것, 그리고 손실 함수가 크로스 엔트로피로 바뀌는 것뿐입니다.
for epoch in range(epochs):
y_hat = sigmoid(a * X + b)
eps = 1e-10
loss = -np.mean(y * np.log(y_hat + eps) + (1 - y) * np.log(1 - y_hat + eps))
error = y_hat - y
grad_a = np.mean(error * X)
grad_b = np.mean(error)
a -= lr * grad_a
b -= lr * grad_b
경사하강법의 그래디언트 업데이트 구조 자체는 선형회귀와 동일합니다. 이 구조가 나중에 딥러닝에서도 그대로 이어진다는 게 흥미로운 점인 것 같습니다.
[이미지 위치: 시그모이드 함수 그래프 — S자 곡선으로 실수 전체를 0~1 사이로 압축 — alt: "시그모이드 함수 그래프, S자 형태"]
| 항목 | 선형회귀 | 로지스틱 회귀 |
|---|---|---|
| 문제 유형 | 연속값 예측 (회귀) | 범주 예측 (분류) |
| 출력 형태 | 임의의 실수 | 0~1 확률값 |
| 변환 함수 | 없음 | 시그모이드 |
| 손실 함수 | MSE | 크로스 엔트로피 |
| 최적화 | 경사하강법 / 정규방정식 / 그리드 서치 | 경사하강법 |
직접 경사하강법을 구현해 보니 "학습"이라는 게 결국 오차를 줄이는 방향으로 파라미터를 반복해서 조금씩 이동하는 것이라는 게 체감으로 와닿았습니다. 라이브러리가 내부적으로 하는 일을 손으로 짜보는 경험이 꽤 가치 있는 것 같습니다.
다음은 EDA + Scikit-learn 파이프라인입니다. 실제 데이터를 탐색하고 전처리하는 과정, 그리고 Scikit-learn으로 머신러닝 파이프라인을 구성하는 방법을 다룹니다. day2까지는 NumPy로 직접 구현했다면, day3부터는 실전에 가까운 라이브러리 활용으로 넘어갈 것 같습니다.
이 글이 도움이 됐다면 댓글이나 공감 부탁드립니다. 틀린 내용이 있으면 알려주세요!
#AI수학 #선형회귀 #로지스틱회귀 #경사하강법 #머신러닝 #공부기록