주택 면적과 건축 연도를 기반으로 주택 가격을 예측하는 시나리오를 생각해 봅시다. 이를 위해 훈련 데이터셋을 수집해야 합니다. 이 데이터셋에는 주택 판매 가격, 건축 연도, 면적과 같은 정보가 포함됩니다. 기계 학습에서 각 데이터 행은 샘플 또는 데이터 포인트라고 하며, 예측하려는 목표(이 경우 주택 가격)는 레이블 또는 타겟이라고 합니다. 예측의 기반이 되는 입력 변수는 특징 또는 공변량으로 알려져 있습니다.
데이터셋의 샘플 수를 n이라고 할 때, i번째 샘플의 입력은 X(i) = [x1(i), x2(i)]T로 표시되며, 해당 레이블은 y(i)입니다.
선형 모델
선형 가정은 타겟 변수가 특징의 가중 합으로 표현될 수 있다는 것입니다. 예를 들어, 주택 가격 예측은 다음과 같은 식으로 나타낼 수 있습니다:
$$ \text{price} = \omega_{\text{area}} \cdot \text{area} + \omega_{\text{age}} \cdot \text{age} + b \quad (1) $$
여기서 ωarea와 ωage는 가중치(weights)이며, b는 편향(bias) 또는 절편(intercept)입니다. 가중치는 각 특징이 예측값에 미치는 영향을 결정하며, 편향은 모든 특징이 0일 때의 예측값을 나타냅니다. 실제 주택의 면적이나 건축 연도가 0인 경우는 없지만, 편향 항은 모델의 표현 능력을 향상시키기 위해 필수적입니다. 엄밀히 말해, 식 (1)은 입력 특징에 대한 아핀 변환(affine transformation)으로, 선형 변환과 평행 이동(이동)을 결합한 것입니다.
데이터셋이 주어졌을 때, 모델의 예측이 실제 데이터와 잘 맞도록 하는 가중치 W와 편향 b를 찾는 것이 목표입니다. 예측값 ŷ는 입력 특징에 대한 아핀 변환으로 결정됩니다.
특징의 수가 적은 데이터셋의 경우, 위와 같이 명시적인 형태로 모델을 표현하는 것이 일반적입니다. 하지만 기계 학습에서는 고차원 데이터셋을 다루는 경우가 많으므로, 선형대수 표기법을 사용하는 것이 더 편리합니다. 입력 특징이 d개인 경우, 예측값 ŷ (추정값을 나타내는 종종 "hat" 기호 사용)는 다음과 같이 표현됩니다:
$$ \hat{y} = \omega_1 x_1 + \dots + \omega_d x_d + b \quad (2) $$
특징 벡터를 x ∈ “Rd”로, 가중치 벡터를 w ∈ “Rd”로 정의하면, 모델은 다음과 같이 점곱(dot product) 형태로 간결하게 표현될 수 있습니다:
$$ \hat{y} = w^T x + b \quad (3) $$
여기서 x는 단일 데이터 샘플의 특징 벡터입니다. X ∈ “Rn×d” 행렬은 전체 데이터셋의 n개 샘플을 참조하는 데 유용합니다. X의 각 행은 샘플을 나타내고, 각 열은 특징을 나타냅니다.
특징 집합 X에 대한 예측값 ŷ ∈ “Rn”은 다음과 같이 행렬-벡터 곱셈으로 표현될 수 있습니다:
$$ \hat{y} = Xw + b \quad (4) $$
손실 함수
모델이 데이터를 얼마나 잘 설명하는지 측정하기 위해 손실 함수를 정의해야 합니다. 손실 함수는 실제 값과 예측값 간의 차이를 정량화합니다. 일반적으로 음수가 아닌 값을 사용하며, 값이 작을수록 손실이 적다는 것을 의미합니다. 완벽하게 일치하는 경우 손실은 0입니다. 회귀 문제에서 가장 일반적으로 사용되는 손실 함수는 제곱 오차(squared error)입니다.
i번째 샘플의 예측값 ŷ(i)와 실제 레이블 y(i)가 주어졌을 때, 제곱 오차는 다음과 같이 정의됩니다:
$$ l^{(i)}(\boldsymbol{w}, b) = \frac{1}{2} (\hat{y}^{(i)} - y^{(i)})^2 \quad (5) $$
상수 1/2은 본질적인 차이를 만들지 않지만, 손실 함수를 미분할 때 상수가 1이 되어 계산이 간결해집니다. 제곱은 큰 차이가 더 큰 손실을 초래하도록 합니다.
전체 데이터셋에 대한 모델 성능을 측정하기 위해 n개의 훈련 샘플에 대한 손실의 평균을 계산합니다:
$$ L(\boldsymbol{w}, b) = \frac{1}{n} \sum_{i=1}^{n} l^{(i)}(\boldsymbol{w}, b) = \frac{1}{2n} \sum_{i=1}^{n} (\boldsymbol{w}^T \boldsymbol{x}^{(i)} + b - y^{(i)})^2 \quad (6) $$
모델 훈련 시 목표는 모든 훈련 샘플에 대한 총 손실을 최소화하는 파라미터 집합을 찾는 것입니다:
$$ (\boldsymbol{w}^*, b^*) = \underset{\boldsymbol{w}, b}{\arg\min} L(\boldsymbol{w}, b) \quad (7) $$
해석해
선형 회귀는 해석해(analytical solution)를 갖는 간단한 최적화 문제입니다. 즉, 파라미터의 최적값을 계산하는 공식이 존재합니다. 편향 b를 파라미터 벡터 w에 통합하기 위해, 특징 행렬 X에 1로 채워진 열을 추가할 수 있습니다. 이 경우, 문제는 X의 열에 b를 포함한 새로운 가중치 벡터를 사용하여 ||y - Xw||2를 최소화하는 것이 됩니다. 이 손실 함수는 볼록(convex)하므로, 손실에 대한 w의 미분이 0이 되는 지점에서 전역 최솟값을 찾을 수 있습니다. 해석해는 다음과 같습니다:
$$ \boldsymbol{w}^* = (X^T X)^{-1} X^T y \quad (9) $$
선형 회귀와 같이 해석해를 갖는 문제는 수학적으로 분석하기 쉽다는 장점이 있습니다. 그러나 모든 최적화 문제가 해석해를 갖는 것은 아니며, 특히 딥러닝에서는 해석해가 없는 경우가 많아 다른 최적화 방법을 사용해야 합니다.
미니배치 확률적 경사 하강법
해석해를 찾을 수 없는 경우에도 모델을 효과적으로 훈련할 수 있습니다. 많은 딥러닝 모델은 해석해가 없는 경우가 많지만 성능이 뛰어납니다. 따라서 이러한 모델을 훈련하는 방법을 이해하는 것이 중요합니다.
여기서는 딥러닝 모델 최적화에 널리 사용되는 경사 하강법(gradient descent)을 사용합니다. 이 방법은 손실 함수의 기울기(gradient)를 따라 파라미터를 반복적으로 업데이트하여 오차를 줄입니다.
가장 기본적인 경사 하강법은 전체 데이터셋에 대한 손실 함수를 계산하고, 해당 손실 함수를 모델 파라미터로 미분하여 기울기를 얻는 것입니다. 그러나 전체 데이터셋을 매번 순회해야 하므로 매우 느릴 수 있습니다. 따라서 실제 구현에서는 일반적으로 한 번의 업데이트에 사용되는 훈련 샘플 수를 줄이는 미니배치 확률적 경사 하강법(minibatch stochastic gradient descent)을 사용합니다.
각 반복(iteration)에서 먼저 고정된 수의 훈련 샘플로 구성된 작은 미니배치를 무작위로 추출합니다. 그런 다음, 해당 미니배치에 대한 손실 함수의 기울기를 계산하고, 미리 결정된 학습률(learning rate) η를 곱하여 현재 파라미터 값에서 뺍니다.
데이터 생성 및 배치
가중치 w와 편향 b를 기반으로 num_examples개의 인공 데이터를 생성합니다. features는 훈련 데이터이고 labels는 타겟입니다. 노이즈는 표준 편차가 0.01인 정규 분포를 따릅니다.
import torch
import random
def synthetic_data(w, b, num_examples):
X = torch.normal(0, 1, (num_examples, len(w)))
y = torch.matmul(X, w) + b
y += torch.normal(0, 0.01, y.shape)
return X, y.reshape((-1, 1))
true_w = torch.tensor([2, -3.4])
true_b = 4.2
num_examples = 1000
features, labels = synthetic_data(true_w, true_b, num_examples)
def data_iter(batch_size, features, labels):
num_examples = len(features)
indices = list(range(num_examples))
random.shuffle(indices) # 특징 순서 섞기
for i in range(0, num_examples, batch_size):
batch_indices = torch.tensor(indices[i:min(i + batch_size, num_examples)])
yield features[batch_indices], labels[batch_indices]
파라미터 초기화
모델 파라미터(가중치 w 및 편향 b)를 초기화합니다. 가중치는 평균 0, 표준 편차 0.01의 정규 분포에서 무작위로 샘플링되고, 편향은 0으로 초기화됩니다.
batch_size = 10 # 예시 배치 크기
# requires_grad=True는 이 텐서에 대한 기울기를 계산해야 함을 PyTorch에 알립니다.
w = torch.normal(0, 0.01, size=(2, 1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)
모델 정의
선형 회귀 모델 함수:
def linreg(X, w, b):
return torch.matmul(X, w) + b
손실 함수 (제곱 오차):
def squared_loss(y_hat, y):
# y_hat과 y의 모양을 일치시키기 위해 y를 재구성합니다.
return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2
최적화 알고리즘
미니배치 확률적 경사 하강법 업데이트를 구현합니다. 이 함수는 모델 파라미터, 학습률, 배치 크기를 입력으로 받습니다. 각 단계에서의 업데이트 크기는 학습률 lr에 의해 결정됩니다. 계산된 손실은 배치 샘플의 평균이므로, 스텝 크기가 배치 크기 선택에 의존하지 않도록 배치 크기로 정규화합니다.
def sgd(params, lr, batch_size):
with torch.no_grad(): # no_grad() 컨텍스트 내에서 연산은 기울기 추적을 비활성화합니다.
for param in params:
# param.grad는 param의 현재 값에 대한 손실의 기울기입니다.
param -= lr * param.grad / batch_size
# 기울기를 0으로 재설정하여 다음 반복에서 누적되지 않도록 합니다.
param.grad.zero_()
훈련
모델 파라미터 w와 b를 최적화하는 훈련 루프를 설정합니다. 각 에포크(epoch) 동안, 데이터셋을 미니배치로 반복합니다. 각 미니배치에 대해:
- 모델을 사용하여 예측을 계산합니다.
- 예측값과 실제 레이블 간의 손실을 계산합니다.
- 역전파(backpropagation)를 수행하여 손실에 대한 각 파라미터의 기울기를 계산합니다.
sgd함수를 호출하여 파라미터를 업데이트합니다.
훈련 후, 각 에포크의 평균 훈련 손실을 출력합니다.
lr = 0.03 # 학습률
num_epochs = 3 # 훈련 에포크 수
net = linreg # 모델 함수
loss_fn = squared_loss # 손실 함수
for epoch in range(num_epochs):
# data_iter를 사용하여 미니배치 데이터를 가져옵니다.
for X, y in data_iter(batch_size, features, labels):
l = loss_fn(net(X, w, b), y) # 손실 계산
# 모든 샘플의 손실 합계에 대해 역전파를 수행합니다.
l.sum().backward()
# 모델 파라미터 업데이트
sgd([w, b], lr, batch_size)
# 에포크 종료 후 훈련 손실 출력 (평균값)
with torch.no_grad(): # 평가 중에는 기울기를 계산하지 않습니다.
train_l = loss_fn(net(features, w, b), labels)
print(f'Epoch {epoch+1}, Loss {float(train_l.mean()):f}')
# 최종 학습된 파라미터 출력 (선택 사항)
# print(f"Learned w: {w.numpy()}")
# print(f"Learned b: {b.numpy()}")