선형 회귀 및 선형 분류
AI-PyTorch

서론

전체 코드는 Colab에서 확인할 수 있다.

선형 회귀

데이터 적재하기

본격적으로 신경망을 학습하기 전에, 선형 회귀 문제를 해결하는 간단한 맛보기 모델을 제작해 본다.

넘파이 어레이를 사용해 샘플(X_train) 및 레이블(y_train)을 생성한다.

1
2
X_train = np.arange(10, dtype="float32").reshape((10, 1))
y_train = np.array([1.0, 1.3, 3.1, 2.0, 5.0, 6.3, 6.6, 7.4, 8.0, 9.0], dtype="float32")

데이터를 시각화하면 다음과 같다.

X_train을 표준화하고, torch.from_numpy를 사용해 샘플과 레이블을 텐서로 변환한다.

1
2
3
X_train_norm = (X_train - np.mean(X_train)) / np.std(X_train)
X_train_norm = torch.from_numpy(X_train_norm)
y_train = torch.from_numpy(y_train)

데이터 적재와 순회를 위해 TensorDataset(torch.utils.data.TensorDataset)와 DataLoader(torch.utils.data.DataLoader)를 사용한다.

TensorDataset은 Dataset(torch.utils.data.Dataset)의 하위 클래스로, 샘플 X와 레이블 Y를 묶어 놓는 컨테이너이다.

DataLoader은 TensorDataset을 순환 가능(Iterable)하게 만든다. 즉, for 루프를 사용해 쉽게 접근할 수 있게 된다. 조정 가능한 하이퍼파라미터는 다음과 같다.

  • batch_size

    배치의 크기를 지정한다. 현재 데이터셋에는 10개의 데이터가 있고, batch_size를 1로 지정하면 10/1 = 10번의 반복이 발생한다.

  • shuffle

    데이터를 섞어서 사용할지 결정한다.

1
2
3
4
from torch.utils.data import TensorDataset, DataLoader

train_ds = TensorDataset(X_train_norm, y_train)
train_dl = DataLoader(train_ds, batch_size=1, shuffle=True)

Note

DataLoader 클래스의 조정 가능한 모든 하이퍼파라미터는 다음과 같다.

1
2
3
4
5
DataLoader(dataset, batch_size=1, shuffle=False, sampler=None,
        batch_sampler=None, num_workers=0, collate_fn=None,
        pin_memory=False, drop_last=False, timeout=0,
        worker_init_fn=None, *, prefetch_factor=2,
        persistent_workers=False)

출처: https://pytorch.org/docs/stable/data.html

모델 훈련하기

선형 회귀 모델을 사용하며, 이는 다음과 같은 선형 방정식으로 나타낼 수 있다.

\[z = wx + b\]

가중치(w) 및 편향(b)에 주목해야 한다. 이는 각각 선형 방정식의 기울기와 절편에 대응하며, 그래프의 모양을 결정한다. 가중치와 편향은 임의의 값에서 출발하고, 확률적 경사 하강법을 사용해 비용(손실 함수의 결과)이 최소화되는 방향으로 이동한다.

파이토치에서는 자동 미분(Autograd)을 지원하므로, 경사 하강법 알고리즘을 직접 구현하지 않아도 된다. 자동 미분은 텐서를 생성할 때 requires_grad=True로 설정하거나, 나중에 requires_grad_()로 활성화한다.

1
2
3
4
5
6
7
8
torch.manual_seed(1)

# 가중치
weight = torch.randn(1)
weight.requires_grad_()                     # 자동 미분 활성화

# 편향
bias = torch.zeros(1, requires_grad=True)   # 자동 미분 활성화

모델은 선형 회귀 모델, 손실 함수는 MSE(평균 제곱 오차)를 사용한다.

1
2
3
4
5
6
7
# 선형 회귀 모델
def model(xb):
    return xb @ weight + bias

# 손실 함수: MSE | 평균 제곱 오차
def loss_fn(input, target):
    return (input-target).pow(2).mean()

모델, 가중치, 편향, 손실 함수의 설정이 끝났다면, 학습률과 에포크를 설정하고 모델 훈련을 시행한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
learning_rate = 0.001
num_epochs = 200
log_epochs = 10
for epoch in range(num_epochs):
    for x_batch, y_batch in train_dl:
        pred = model(x_batch)           # 예측값
        loss = loss_fn(pred, y_batch)   # 오차
        loss.backward()                 # 변화도 계산
    with torch.no_grad():
        # 계산한 변화도를 사용해 파라미터 업데이트
        weight -= weight.grad * learning_rate
        bias -= bias.grad * learning_rate
        # 변화도 초기화
        weight.grad.zero_()
        bias.grad.zero_()
    if epoch % log_epochs == 0:
        print(f"Epoch {epoch} Loss {loss.item():.4f}")

각 학습마다 다음 작업을 수행한다.

  • 모든 데이터셋에 대해

    • DataLoader에서 x_batch 및 y_batch를 가져온다.
    • 모델(선형 방정식)의 인자로 x_batch를 사용해 예측값을 구한다.
    • 예측값과 실제값(y_batch)의 오차를 구한다.
    • backward()는 자동 미분이 활성화된 모든 텐서들(weight, bias)에 대한 gradient(변화도)을 추적하고, 이를 각 텐서의 grad 속성에 저장한다.
  • no_grad()with문과 같이 사용하면 Pytorch는 autograd engine을 끄고, 변화도 추적을 중지한다. 이 동안 메모리 사용량을 줄이고, 연산 속도를 높일 수 있다.
    • 계산한 변화도를 사용해 가중치와 편향을 갱신한다.
    • grad_zero_()를 사용해 변화도를 초기화한다. 만약 초기화하지 않는다면 값은 그대로 남아 있으며, 다음 학습에서 변화도를 추적할 때 grad에 값이 축적되는 일이 발생한다.
  • 10번마다 오차를 출력한다.

훈련 결과는 다음과 같다.

torch.nn 활용하기

PyTorch가 제공하는 클래스 및 함수를 사용하면 앞서 구현한 내용을 보다 간단하게 작성할 수 있다.

1
import torch.nn as nn

torch.nn 은 모델 및 손실 함수를 제공하며, 선형 회귀 모델(torch.nn.Linear) 및 MSE 손실 함수(torch.nn.MSELoss)를 사용한다. 선형 회귀 모델의 조정 가능한 하이퍼파라미터는 다음과 같다.

  • in_features

    입력의 크기를 정의한다.

  • out_features

    출력의 크기를 정의한다.

  • bias

    False로 설정하면, 신경망의 레이어는 편향을 추가로 학습하지 않는다. (Default: True)

torch.optim은 옵티마이저(최적화) 알고리즘을 제공하며, 확률적 경사 하강법 옵티마이저(torch.optim.SGD) 를 사용한다.

1
2
3
4
5
input_size = 1
output_size = 1
model = nn.Linear(input_size, output_size)
loss_fn = nn.MSELoss(reduction="mean")
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)

동일한 학습률과 에포크를 사용해 모델 훈련을 시행한다.

1
2
3
4
5
6
7
8
9
for epoch in range(num_epochs):
    for x_batch, y_batch in train_dl:
        pred = model(x_batch)[:, 0]     # 예측값
        loss = loss_fn(pred, y_batch)   # 오차
        loss.backward()                 # 변화도 계산
        optimizer.step()                # 계산한 변화도를 사용해 파라미터 초기화
        optimizer.zero_grad()           # 변화도 초기화
    if epoch % log_epochs == 0:
        print(f"Epoch {epoch} Loss {loss.item():.4f}")

옵티마이저를 사용해 역전파 과정을 다음과 같이 간단하게 나타낼 수 있다.

1
2
3
loss.backward()                 # 변화도 계산
optimizer.step()                # 계산한 변화도를 사용해 파라미터 초기화
optimizer.zero_grad()           # 변화도 초기화

선형 분류

사전 작업하기

NN을 구현하고, 이를 사용해 붓꽃 데이터셋을 분류한다. sklearn.datasets을 사용해 데이터셋을 가져오고, train_test_split을 사용해 훈련셋과 데이터셋으로 나눈다.

1
2
3
4
5
6
7
8
9
10
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split

iris = load_iris()
X = iris["data"]
y = iris["target"]
X_train, X_test, y_train, y_test = train_test_split(
    # 훈련셋:테스트셋 = 2:1
    X, y, test_size=1./3, random_state=1    
)

X를 표준화하고 샘플과 레이블을 텐서로 변환한 후, 배치 사이즈가 2인 DataLoader를 생성한다.

1
2
3
4
5
6
7
X_train_norm = (X_train - np.mean(X_train)) / np.std(X_train)
X_train_norm = torch.from_numpy(X_train_norm).float()
y_train = torch.from_numpy(y_train)
train_ds = TensorDataset(X_train_norm, y_train)
torch.manual_seed(1)
batch_size = 2
train_dl = DataLoader(train_ds, batch_size, shuffle=True)   # 배치 사이즈 2인 DataLoader

신경망 구현하기

앞서 학습한 선형 회귀 모델은 입력과 출력의 크기를 매개변수로 가지는, PyTorch가 기본적으로 제공하는 모델 중 하나이다.

하지만 신경망을 구현하기 위해서는 추가로 은닉층을 포함시켜야 하며, 각 뉴런에 대한 활성화 함수를 지정해주어야 한다. 따라서 사용자 정의 모델을 작성하는 방법이 필요하다.

사용자 정의 모델은 torch.nn.Module을 상속해 간단하게 생성할 수 있다. Model 클래스는 두 개의 선형 회귀 모델을 은닉층으로 사용한다. 첫 번째 은닉층은 입력을 바탕으로 은닉층 뉴런을 생성하며, 두 번째 은닉층은 은닉층 뉴런을 바탕으로 출력을 생성한다.

1
2
3
4
5
6
7
class Model(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super().__init__()
        # 선형회귀 모델을 사용한 분류
        self.layer1 = nn.Linear(input_size, hidden_size)    # 은닉층: 입력층 뉴런 -> 은닉층 뉴런
        self.layer2 = nn.Linear(hidden_size, output_size)   # 출력층: 은닉층 뉴런 -> 출력층 뉴런
    ...

순전파를 수행하는 forward 메서드를 정의한다. 은닉층과 출력층을 연결하고, 각 층 사이에 활성화 함수를 추가한다.

활성화 함수로는 시그모이드(torch.nn.Sigmoid) 및 소프트맥스(torch.nn.Softmax)를 사용한다.

1
2
3
4
5
6
7
    ...
    def forward(self, x):
        x = self.layer1(x)
        x = nn.Sigmoid()(x)         # 시그모이드
        x = self.layer2(x)
        x = nn.Softmax(dim=1)(x)    # 소프트맥스
        return x

Note

실제 신경망의 층 개수를 셀 때 입력층은 생략한다.

Note

각 층의 결과를 다음 층에 그대로 사용해서는 안 된다. 층에 음수가 많이 입력될 경우 신경망 학습이 매우 더디게 진행되며, 입력 값이 최종 레이어에서 미치는 영향이 작아질 수(Vanishing Gradient Problem) 있기 때문이다. 따라서 활성화 함수를 사용해 음수를 처리하는 과정이 필요하다.

이번 분류 모델의 목적은, 붓꽃 데이터셋의 4개의 특성을 바탕으로 붓꽃을 3가지로 분류하는 것이다. 따라서 입력층의 크기(뉴런의 수)는 4, 출력층의 크기는 3으로 설정한다. 은닉층의 크기는 16으로 설정한다.

손실 함수로 CrossEntropyLoss, 옵티마이저로 Adam을 사용한다. 이 둘은 다중 분류에 흔히 사용된다.

1
2
3
4
5
6
7
input_size = X_train_norm.shape[1]  # 4
hidden_size = 16
output_size = 3
model = Model(input_size, hidden_size, output_size)
loss_fn = nn.CrossEntropyLoss()                                     # CrossEntropyLoss
learning_rate = 0.001
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)  # Adam

에포크를 설정하고 모델 훈련을 시행한다.

손실 함수가 backward 메서드를 호출하면서 역전파가 시작된다. SGD 옵티마이저는 입력층 방향으로 이동하며 연쇄 미분 법칙을 통해 가중치와 편향의 변화도를 업데이트한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
num_epochs = 100
loss_hist = [0] * num_epochs
accuracy_hist = [0] * num_epochs
for epoch in range(num_epochs):
    for x_batch, y_batch in train_dl:
        
        pred = model(x_batch)
        loss = loss_fn(pred, y_batch)
        loss.backward()
        optimizer.step()
        optimizer.zero_grad()
        
        # update hist
        loss_hist[epoch] += loss.item() * y_batch.size(0)
        is_correct = (torch.argmax(pred, dim=1) == y_batch).float() # True:1, False:0
        accuracy_hist[epoch] += is_correct.mean()
    loss_hist[epoch] /= len(train_dl.dataset)                       # 각 에포크의 오차
    accuracy_hist[epoch] /= len(train_dl.dataset)                   # 각 에포크의 정답률
    accuracy_hist[epoch] *= batch_size

학습하는 동안 오차는 꾸준히 감소했으며, 정확도는 약 30번째 에포크에서 급증함을 확인할 수 있다.

훈련이 끝난 모델을 테스트셋에 대해 평가한다.

1
2
3
4
5
6
7
X_test_norm = (X_test - np.mean(X_train)) / np.std(X_train)
X_test_norm = torch.from_numpy(X_test_norm).float()
y_test = torch.from_numpy(y_test)
pred_test = model(X_test_norm)
correct = (torch.argmax(pred_test, dim=1) == y_test).float()
accuracy = correct.mean()
print(f"Test Acc.: {accuracy:.4f}")
1
Test Acc.: 0.9800

📌REF

  • Machine Learning with PyTorch and Scikit-Learn