[ML] Linear Regression (선형 회귀) - [2] 여러 구현 방법 및 해석
Linear Regression Series
- [1] Linear Regression - 수학적 증명과 이론
- [2] Linear Regression - 여러 구현 방법 및 해석
Introduction
Linear Regression 1편에서는 선형 회귀의 수학적 기초와 최적화 과정을 다루었다. 우선, 해당 내용을 간단하게 살펴보자.
Linear Model Representation
선형 회귀는 입력 변수 $X$, 출력 변수 $y$ 사이의 선형 관계를 학습한다.
\[\hat{y} = X\theta\]- $\hat{y}$: 예측값 벡터
- $X$: 입력 데이터의 디자인 행렬 $(n \times (p+1))$
- $\theta$: 모델 파라미터 벡터 $((p+1) \times 1)$
예측 변수 $\hat{y}$가 독립 변수 $X$와 모델 파라미터 $\theta$의 선형 결합으로 포함된다.
Loss Function
선형 회귀는 실제값 $y$과 예측값 $\hat{y}$ 간의 차이를 최소화하며, 손실 함수 $R(\theta)$는 다음과 같이 정의된다.
\[R(\theta) = \frac{1}{n} \| y - X\theta \|_2^2\]평균 제곱 오차 MSE 을 최소화하는 최적화 문제로 구성된다.
Normal Equation
손실 함수를 최소화하는 $\theta$는 다음 정규 방정식을 통해 계산된다.
\[\theta = (X^T X)^{-1} X^T y\]본 포스팅은 선형 회귀의 여러 구현 방법을 다루며, 각 방법에 대한 이론적 내용과 장단점도 함께 알아본다.
Normal Equation을 통한 구현
정규 방정식 Normal Equation 을 통해 손실 함수를 최소화하는 결과를 바로 얻을 수 있으며, 이는 Numpy를 통해 구현할 수 있다.
먼저, 예시 데이터를 생성한다.
1
2
3
4
5
6
import numpy as np
np.random.seed(42)
n = 100 # 샘플 개수
X = 2 * np.random.rand(n, 1)
y = 4 + 3 * X + np.random.randn(n, 1)
다음으로, 정규 방정식을 통해 $\hat{\theta}$를 계산한다.
1
2
3
4
5
# 선형 회귀 파라미터 계산
X_b = np.c_[np.ones((X.shape[0], 1)), X] # 절편 추가 (bias term)
theta_best = np.linalg.inv(X_b.T @ X_b) @ X_b.T @ y
print("Optimal Parameters (theta):", theta_best)
np.linalg: 넘파이 선형대수 모듈inv(): 역행렬 계산@: 행렬곱 수행
계산된 $\hat{\theta}$를 통해 예측을 수행하고, 시각화한다.
1
2
3
4
5
6
7
import matplotlib.pyplot as plt
y_pred = X_b @ theta_best
plt.plot(X, y_pred, "r-")
plt.plot(X, y, "b.")
plt.show()
그러나, 정규 방정식을 활용한 선형 회귀 구현은 $O(p^{2.4})$에서 $O(p^3)$ 사이의 매우 높은 복잡도를 가진다.
SVD를 통한 구현
다음은 Scikit-learn을 통해 보다 간단하게 선형 회귀를 구현할 수 있다.
1
2
3
4
5
6
7
8
from sklearn.linear_model import LinearRegression
lr = LinearRegression()
lr.fit(X, y)
print(f"Intercept: {lr.intercept_}, coefficient: {lr.coef_}")
X_new = np.array([[0], [2]])
lr.predict(X_new)
Scikit-learn의 LinearRegression()는 scipy.linalg.lstsq()를 기반으로 하는데,
이 함수는 $X$의 유사역행렬 $X^+$을 통해 $\hat\theta = X^+y$를 계산한다.
유사역행렬은 특잇값 분해 Singular Value Decomposition 라고 부르는 표준 행렬 분해 기법을 사용한다.
SVD는 입력 행렬 $X$를 3개의 행렬 곱셈 $U \Sigma V^T$로 분해하며, 파라미터는 다음과 같이 계산된다.
- $\hat\theta = X^+y = U \Sigma^+ V^Ty$
이를 통해 정규 방정식보다 훨씬 효율적으로 $\hat\theta$를 계산할 수 있다.
또한, 기존 정규 방정식은 $X^T X$의 역행렬이 없다면 작동하지 않지만, 유사역행렬은 항상 구할 수 있다는 장점이 있다.
그러나, Scikit-learn의 LinearRegression 클래스가 활용하는 SVD 방법의 계산 복잡도는 $O(p^2)$으로 여전히 매우 높은 편이며, 특성의 개수가 늘어나면 계산 시간은 배로 늘어난다.
훈련 세트의 샘플 수에 대해서는 $O(n)$의 복잡도로 선형적으로 증가한다.
경사하강법
경사하강법 Gradient Descent 은 손실 함수를 최소화하기 위해 반복하여 파라미터를 조정하는 방법이다.
파라미터 $\theta$에 대해 손실 함수의 그래디언트가 0이 되면 손실 함수가 최솟값에 도달한 것이다.
경사 하강법에서 가장 중요한 파라미터는 스텝의 크기로, 학습률 하이퍼파라미터로 결정된다.
효울적 학습을 위해 적절한 학습률을 찾는 것이 중요하며, 모든 손실 함수가 위의 볼록 함수를 갖지 않아 지역 최솟값 Local Minimum 에 빠질 위험이 존재한다.
다행히, MSE 함수는 볼록 함수 Convex Function 이므로, 지역 최솟값에 빠질 우려는 없다.
손실 함수는 그릇 모양을 갖고 있지만, 특성들의 스케일이 매우 다르면 더 길쭉한 모양이 되어 최소값으로 진행하는 데 더 오랜 시간이 걸린다.
특성 스케일을 적용한 경사하강법(왼쪽), 적용하지 않은 경사하강법(오른쪽)
따라서, 경사 하강법을 사용할 땐 반드시 모든 특성의 스케일을 동일하게 만들어야 한다. :
StandardScaler(),MinMaxScaler()등
배치 경사하강법을 통한 구현
경사하강법을 구현하기 위해 각 모델 파라미터 $\theta_j$에 대해 손실 함수의 그래디언트를 계산해야 한다.
즉, 편도함수 Partial Derivative $\frac{\partial }{\partial \theta_j} R(\theta)$를 계산한다.
배치 경사하강법 Batch Gradient Descent 은 전체 훈련 데이터셋에 대해 손실 함수의 기울기를 계산하는 방식이며, 편도함수를 한꺼번에 계산한다.
\[\frac{\partial }{\partial \theta} R(\theta) = -\frac{2}{n} X^T (y - X\theta)\]훈련 데이터 전체를 사용하므로, 매우 큰 훈련 세트에서는 적합하지 않지만, 경사하강법은 특성 수에 민감하지 않아 정규방정식, SVD보다는 빠르다.
배치 경사하강법의 업데이트 규칙은 다음과 같이 정의되며, 방향은 손실 함수가 감소하는 쪽으로 정해지고, 크기는 학습률 $\eta$에 의해 정해진다.
\[\theta^{(t+1)} = \theta^{(t)} - \eta \nabla R(\theta^{(t)})\]배치 경사하강법을 간단하게 구현할 수 있다.
1
2
3
4
5
6
7
8
9
10
eta = 0.1 # 학습률
epochs = 1000 # 훈련 세트 반복 횟수
n = len(X_b)
np.random.seed(42)
theta = np.random.randn(2, 1) # 모델 파라미터 랜덤 초기화
for epoch in range(epochs):
gradients = 2 / n * X_b.T @ (X_b @ theta - y)
theta = theta - eta * gradients
위의 코드에서는 0.1로 학습률을 지정했지만, 적절한 학습률을 찾기 위해 그리드 서치를 활용하는 것이 좋다.
에포크는 허용 오차보다 작아지면 경사 하강법이 최솟값에 거의 도달한 것이므로 알고리즘을 중지할 수 있다.
확률적 경사하강법을 통한 구현
확률적 경사하강법 SGD 은 매 스텝에서 한 개의 샘플을 랜덤 선택하고, 그에 대한 그래디언트를 계산한다.
\[\theta^{(t+1)} = \theta^{(t)} - \eta \nabla R(\theta^{(t)}; x^{(i)}, y^{(i)})\]전체 데이터셋을 다루는 배치 경사하강법에 비해 매우 효율적인 반면, 확률적이므로 배치 경사하강법보다 불안정해 시간이 지나도 최솟값에 안착하지 못한다.
그러나, 손실 함수가 규칙적인 선형 회귀와 달리, 규칙적이지 않은 손실 함수의 경우, 전역 최솟값을 찾을 가능성을 높일 수 있다.
이 과정에서 전역 최솟값에 도달하기 위해 중요한 것은 매 스텝마다 적절한 학습률을 찾는 것이며, 시작할 때는 큰 학습률로 빠른 수렴을 진행한 후, 점차 작게 줄여서 전역 최솟값에 도달하게 한다.
이렇게 매 반복에서 학습률을 결정하는 함수를 학습 스케줄 Learning Schedule 이라고 하며, 이를 활용해 확률적 경사하강법을 구현한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
epochs = 1000 # 훈련 세트 반복 횟수
t0, t1 = 5, 50 # 학습 스케줄 하이퍼파라미터
n = len(X_b)
def learning_schedule(t):
return t0 / (t + t1)
np.random.seed(42)
theta = np.random.randn(2, 1) # 모델 파라미터 랜덤 초기화
for epoch in range(epochs):
for iteration in range(n):
random_index = np.random.randint(n)
xi = X_b[random_index:random_index + 1]
yi = y[random_index: random_index + 1]
gradients = 2 * xi.T @ (xi @ theta - yi)
eta = learning_schedule(epoch * n + iteration)
theta = theta - eta * gradients
확률적 경사하강법을 사용할 때, 훈련 샘플이 독립 동일 분포 Independent and Identically Distributed 를 만족해야 파라미터가 전역 최솟값에 도달할 수 있다. 즉, 각 샘플을 랜덤하게 선택해야 한다.
Scikit-learn에서 SVD가 아닌 SGD 방식으로 선형 회귀를 구현할 수 있다. SGDRegressor() 클래스를 활용한다.
1
2
3
4
from sklearn.linear_model import SGDRegressor
sgd_lr = SGDRegressor(max_iter=1000, tol=1e-5, penalty=None, eta0=0.01, n_iter_no_change=100, random_state=42)
sgd_lr.fit(X, y.ravel())
- 총 1000번의 에포크 동안, 100번의 에포크에서 손실이 $10^{-5}$보다 작아질 때까지 실행되며, 학습률은 기본 학습 스케줄을 사용함
미니배치 경사하강법을 통한 구현
미니배치 경사하강법 Mini-batch Gradient Descent 은 미니배치라 부르는 임의의 작은 샘플 세트에 대해 그래디언트를 계산한다.
\[\theta^{(t+1)} = \theta^{(t)} - \eta \nabla R(\theta^{(t)}; \text{mini-batch})\]즉, 미니배치 경사하강법은 효율적이지만 불안정적인 확률적 경사하강법, 비효율적이지만 안정적인 배치 경사하강법의 트레이드오프를 적절히 조정할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
eta = 0.1 # 학습률
epochs = 1000 # 훈련 세트 반복 횟수
batch_size = 16 # 미니배치 크기
t0, t1 = 5, 50 # 학습 스케줄 하이퍼파라미터
n = len(X_b) # 데이터 샘플 수
def learning_schedule(t):
return t0 / (t + t1) # 학습 스케줄 함수
np.random.seed(42)
theta = np.random.randn(2, 1) # 모델 파라미터 랜덤 초기화
for epoch in range(epochs):
shuffled_indices = np.random.permutation(n) # 데이터 인덱스 셔플
X_b_shuffled = X_b[shuffled_indices]
y_shuffled = y[shuffled_indices]
for i in range(0, n, batch_size):
xi = X_b_shuffled[i:i + batch_size]
yi = y_shuffled[i:i + batch_size]
gradients = 2 / len(xi) * xi.T @ (xi @ theta - yi) # 미니배치 기울기 계산
eta = learning_schedule(epoch * n + i)
theta = theta - eta * gradients # 파라미터 업데이트
statsmodels를 통한 구현 및 해석
추가적으로, statsmodels는 선형 회귀를 통계적 관점에서 구현하고, 결과를 해석할 수 있는 도구이다.
statsmodels는 정규 방정식을 기반으로 작동하며, 역행렬 계산이 불가능할 때는 SVD를 활용한다.
회귀 계수를 추정하는 데 그치지 않고, 통계적 검정, 신뢰 구간 계산, 결정 계수 등의 정보를 제공하며, 데이터의 분포와 모델의 가정을 검증할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
import statsmodels.api as sm
# 절편 추가
X_b = sm.add_constant(X) # X에 절편 추가
# OLS 모델 생성 및 적합
model = sm.OLS(y, X_b)
results = model.fit()
# 결과 출력
print(results.summary())
1
2
3
4
5
6
7
8
9
10
11
12
OLS Regression Results
==============================================================================
Dep. Variable: y R-squared: 0.962
Model: OLS Adj. R-squared: 0.961
Method: Least Squares F-statistic: 1274.
Date: Wed, 10 Dec 2024 Prob (F-statistic): 1.47e-58
==============================================================================
coef std err t P>|t| [0.025 0.975]
------------------------------------------------------------------------------
const 4.2369 0.204 20.755 0.000 3.833 4.641
x1 2.8975 0.081 35.705 0.000 2.737 3.058
==============================================================================
주요 정보
const: $\hat{\theta_0} = 4.2369$x1: $\hat{\theta_1} = 2.8975$R-squared: $R^2 = 0.962$는 모델이 데이터의 96.2% 변동성을 설명할 수 있음을 의미함p-value: $0.000$으로 각 계수가 통계적으로 유의미함을 나타냄 $(P < 0.05)$
결론
이번 포스팅에서는 선형 회귀의 다양한 구현 방법을 다루었으며, 총 5가지 알고리즘에 대해 비교하였다.
| 알고리즘 | n이 클 때 | p가 클 때 | 하이퍼파라미터 수 | 스케일 조정 필요 | Scikit-learn |
|---|---|---|---|---|---|
| 정규 방정식 | 빠름 | 느림 | 0 | No | N/A |
| SVD | 빠름 | 느림 | 0 | No | LinearRegression() |
| 배치 경사하강법 | 느림 | 빠름 | 2 | Yes | N/A |
| 확률적 경사하강법 | 빠름 | 빠름 | >= 2 | Yes | SGDRegressor() |
| 미니배치 경사하강법 | 빠름 | 빠름 | >= 2 | Yes | N/A |



