선형 회귀: 예측 모델의 출발점
최소 제곱법, 경사 하강법, 다중 선형 회귀, Ridge/Lasso 정규화까지 선형 회귀의 수학적 원리를 scikit-learn과 PyTorch 코드로 완전 정복한다.
지난 글에서 지도 학습의 개념을 살펴봤다. 이번에는 지도 학습의 가장 기본적인 알고리즘인 **선형 회귀(Linear Regression)**를 다룬다. 1800년대 Legendre와 Gauss가 천문학 문제를 풀기 위해 개발한 이 방법은, 200년이 지난 지금도 실무에서 가장 먼저 시도해야 할 베이스라인 모델이다. 단순하지만 해석 가능성이 높고, 신경망의 선형 레이어도 결국 이 연산이다.
선형 회귀란
입력 변수 X와 출력 변수 y 사이에 선형 관계가 있다고 가정하고, 그 관계를 나타내는 직선(또는 초평면)을 찾는 알고리즘이다.
단순 선형 회귀: ŷ = w·x + b
다중 선형 회귀: ŷ = w₁x₁ + w₂x₂ + ... + wₙxₙ + b
행렬 형태: ŷ = Xw + b
- w (가중치/계수): 각 특성이 y에 미치는 영향
- b (편향/절편): x=0일 때의 y 값
- ŷ: 예측값 (실제값 y와 구분)
목표: 예측값 ŷ와 실제값 y의 차이(잔차)를 최소화하는 w, b를 찾는다.
손실 함수와 최소 제곱법
선형 회귀의 손실 함수는 **평균 제곱 오차(MSE)**다.
import numpy as np
from sklearn.datasets import make_regression
from sklearn.model_selection import train_test_split
# 회귀 데이터 생성: y = 3x₁ + 2x₂ + noise
X, y = make_regression(n_samples=200, n_features=2, noise=10, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# MSE 손실 = (1/n) Σ (ŷᵢ - yᵢ)²
def mse_loss(w, b, X, y):
y_hat = X @ w + b
return np.mean((y_hat - y) ** 2)
# 수동 그래디언트 계산
def compute_gradients(w, b, X, y):
n = len(y)
y_hat = X @ w + b
residual = y_hat - y
dw = (2/n) * X.T @ residual # ∂MSE/∂w
db = (2/n) * np.mean(residual) # ∂MSE/∂b
return dw, db
MSE를 w에 대해 미분하고 0으로 놓으면 **해석적 해(Closed-form Solution)**를 구할 수 있다.
# 최소 제곱법 (Ordinary Least Squares, OLS)
# w* = (XᵀX)⁻¹ Xᵀy
# numpy를 이용한 직접 계산
X_aug = np.column_stack([X_train, np.ones(len(X_train))]) # 편향 추가
w_ols = np.linalg.lstsq(X_aug, y_train, rcond=None)[0] # 안정적인 OLS
print(f"OLS 해: w={w_ols[:-1]}, b={w_ols[-1]:.3f}")
# 주의: (XᵀX)⁻¹은 특성이 많으면 계산 비쌈 (O(n³)) → 대규모에서는 GD 선호
scikit-learn으로 빠르게 구현
from sklearn.linear_model import LinearRegression, Ridge, Lasso, ElasticNet
from sklearn.metrics import mean_squared_error, r2_score
from sklearn.preprocessing import StandardScaler
import numpy as np
# 특성 스케일링 (선형 회귀에 권장)
scaler = StandardScaler()
X_train_s = scaler.fit_transform(X_train)
X_test_s = scaler.transform(X_test)
# 1. 기본 선형 회귀
lr = LinearRegression()
lr.fit(X_train_s, y_train)
pred_lr = lr.predict(X_test_s)
print(f"Linear: R²={r2_score(y_test, pred_lr):.4f}, RMSE={mean_squared_error(y_test, pred_lr)**0.5:.2f}")
print(f"계수: {lr.coef_}, 절편: {lr.intercept_:.3f}")
# 2. Ridge (L2 정규화): 다중공선성 문제 해결
ridge = Ridge(alpha=1.0)
ridge.fit(X_train_s, y_train)
pred_ridge = ridge.predict(X_test_s)
print(f"Ridge: R²={r2_score(y_test, pred_ridge):.4f}")
# 3. Lasso (L1 정규화): 특성 선택 효과 (일부 계수 → 0)
lasso = Lasso(alpha=0.1, max_iter=5000)
lasso.fit(X_train_s, y_train)
pred_lasso = lasso.predict(X_test_s)
print(f"Lasso: R²={r2_score(y_test, pred_lasso):.4f}")
print(f"0인 계수 수: {(lasso.coef_ == 0).sum()}") # 특성 선택 확인
PyTorch로 처음부터 구현
신경망도 결국 선형 레이어들의 조합이다. PyTorch로 선형 회귀를 구현하면 신경망 학습의 기본 구조를 이해할 수 있다.
import torch
import torch.nn as nn
import torch.optim as optim
# 데이터를 텐서로 변환
X_t = torch.FloatTensor(X_train_s)
y_t = torch.FloatTensor(y_train).unsqueeze(1)
# 모델 정의: y = w₁x₁ + w₂x₂ + b
model = nn.Linear(in_features=2, out_features=1)
criterion = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.01)
# 학습 루프
for epoch in range(1000):
model.train()
optimizer.zero_grad()
pred = model(X_t)
loss = criterion(pred, y_t)
loss.backward()
optimizer.step()
if epoch % 200 == 0:
print(f"Epoch {epoch:4d}: Loss={loss.item():.4f}")
# 학습된 파라미터 확인
print(f"\n학습된 가중치: {model.weight.data}")
print(f"학습된 편향: {model.bias.data}")
다항 회귀: 비선형 관계 처리
특성을 변환해 선형 모델로 비선형 관계를 학습할 수 있다.
from sklearn.preprocessing import PolynomialFeatures
from sklearn.pipeline import Pipeline
# y ≈ x² + 2x + 1 같은 비선형 관계 모델링
poly_model = Pipeline([
('poly', PolynomialFeatures(degree=2, include_bias=False)),
('scaler', StandardScaler()),
('lr', LinearRegression())
])
# 단변수 데이터 예시
X_1d = np.array([1, 2, 3, 4, 5, 6, 7, 8]).reshape(-1, 1)
y_1d = np.array([1.2, 5.1, 9.8, 17.0, 25.1, 36.2, 49.0, 64.1])
poly_model.fit(X_1d, y_1d)
print(f"다항 회귀 R²: {poly_model.score(X_1d, y_1d):.4f}") # ≈ 0.9998
# degree=2 특성: [x, x²] → 실제 관계 포착
평가 지표
from sklearn.metrics import mean_absolute_error
pred = model_eval(X_test_s) # 임의 모델 예측
mse = mean_squared_error(y_test, pred)
rmse = mse ** 0.5
mae = mean_absolute_error(y_test, pred)
r2 = r2_score(y_test, pred)
print(f"MSE: {mse:.2f} (평균 제곱 오차)")
print(f"RMSE: {rmse:.2f} (원래 단위와 같음 → 해석 용이)")
print(f"MAE: {mae:.2f} (이상치에 덜 민감)")
print(f"R²: {r2:.4f} (0~1, 높을수록 좋음)")
# R² = 1 - Σ(y-ŷ)² / Σ(y-ȳ)²
# "모델이 y 분산의 몇 %를 설명하는가"
언제 선형 회귀를 쓸까
선형 회귀는 여전히 실무에서 첫 번째로 시도해야 할 알고리즘이다.
써야 할 때:
- 빠른 베이스라인이 필요할 때
- 계수의 해석이 중요할 때 (의학, 경제 연구)
- 데이터가 선형 관계를 따를 때
- 특성이 많고 샘플이 적을 때 (Ridge/Lasso)
다른 방법이 필요할 때:
- 명확히 비선형 관계 → 랜덤 포레스트, XGBoost, 신경망
- 이미지, 텍스트 → CNN, Transformer
- 극도로 복잡한 패턴 → 딥러닝
선형 회귀가 잘 안 된다면, 그것 자체가 중요한 정보다. 데이터가 선형이 아니거나, 특성 엔지니어링이 필요하다는 신호다.
지난 글: 지도학습 vs 비지도학습: 머신러닝의 두 패러다임
다음 글: 로지스틱 회귀: 분류 문제의 첫 걸음
읽어주셔서 감사합니다. 😊