seminar

키워드

  • train_test_split
  • PyTorch Dataset and DataLoader
  • PyTorch Optimizer

Train / Test / Validation

보통 ML/DL 모델을 학습시키고 성능을 평가하는 과정은 한 번의 학습으로 이뤄지지 않는다. ML/DL의 목표는 존재하는 소량의 데이터셋으로 앞으로 마주할 대량의 미지의 데이터셋에서 높은 성능으로 예측하는 것이다. 그래서 ML/DL은 데이터셋을 Train, Test, Validation으로 나누어 모델을 학습시킨다.

Train, Test, Validation을 왜 나누는지 그리고 어떤 목적으로 사용되는지, K-fold Cross Validation 등등에 대해서는 이미 알고 있을 거라고 생각하고 따로 설명하지 않겠다. 혹시 모른다면 아래의 아티클들을 참고하라.

일단 저번 세미나에서 nn.Linear()를 사용해 구축했던 Linear Regression 코드에서 시작하자.

!wget https://raw.githubusercontent.com/mahesh147/Multiple-Linear-Regression/master/50_Startups.csv
import torch
import torch.nn as nn
import pandas as pd
from sklearn.preprocessing import MinMaxScaler

data = pd.read_csv('50_Startups.csv')
X = data[['R&D Spend', 'Administration', 'Marketing Spend']]
y = data[['Profit']]

transformer = MinMaxScaler()
transformer.fit(X)
X = transformer.transform(X)

transformer = MinMaxScaler()
transformer.fit(y)
y = transformer.transform(y)

X_data = torch.FloatTensor(X)
y_data = torch.FloatTensor(y)
layer = nn.Linear(in_features=3, out_features=1)

for _ in range(10):
  y_pred = layer(X_data)
  loss = ((y_pred - y_data)**2).sum()
  print(loss)

  loss.backward()

  eta = 1e-2
  with torch.no_grad():
    for p in layer.parameters():
        p.sub_(eta * p.grad)
        p.grad.zero_()

우리는 skicit-learn의 train_test_split() 함수를 이용해 데이터셋을 분리할 것이다. 코드를 먼저 살펴보자.

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
X_train, X_val, y_train, y_val = train_test_split(X_train, y_train, test_size=0.2)

print("train:", len(X_train))
print("val:", len(X_val))
print("test:", len(X_test))

보통 Train/Test를 0.8/0.2 정도로 나누고, Train/Val도 0.8/0.2 정도로 나눈다. train_test_split() 함수 자체를 외울 필욘 없고, train set을 분리하려고 할 때 train_test_split()를 쓴다 정도만 기억하면 된다.

이제 Train/Val/Test를 쓰도록 코드를 수정해보자.

transformer = MinMaxScaler()
transformer.fit(X_train)
X_train = transformer.transform(X_train)
X_val = transformer.transform(X_val)
X_test = transformer.transform(X_test)

transformer = MinMaxScaler()
transformer.fit(y_train)
y_train = transformer.transform(y_train)
y_val = transformer.transform(y_val)
y_test = transformer.transform(y_test)

X_train = torch.FloatTensor(X_train)
X_val = torch.FloatTensor(X_val)
X_test = torch.FloatTensor(X_test)

y_train = torch.FloatTensor(y_train)
y_val = torch.FloatTensor(y_val)
y_test = torch.FloatTensor(y_test)

layer = nn.Linear(in_features=3, out_features=1)

for i in range(101):
  y_pred = layer(X_train)
  loss = ((y_pred - y_train)**2).sum()

  loss.backward()

  if i % 10 == 0:
    print(f"===== iter: {i} =====")
    print(f"[train]: {loss:.2f}")

    with torch.no_grad():
      y_pred = layer(X_val)
      loss = ((y_pred - y_val)**2).sum()

      print(f"[val]: {loss:.2f}")

  eta = 1e-2
  with torch.no_grad():
    for p in layer.parameters():
        p.sub_(eta * p.grad)
        p.grad.zero_()


y_pred = layer(X_test)
loss = ((y_pred - y_test)**2).sum()

print(f"===== final test loss =====")
print(f"[test]: {loss:.2f}")

train/val/test 데이터셋에 MinMaxScaler(), FloatTensor() 등등의 작업을 다 해주면 된다. 코드가 지저분한데 원래는 이렇게 하면 안 되고, 적절히 모듈화 해서 코드를 재사용 해야 한다.

학습된 결과를 살펴보자. 본인 노트북에서의 결과를 기준으로 리포트 하겠다.

===== fianl loss ====
[train]: 0.06
[val]: 0.07
[test]: 0.03

결과를 보면 train/val/test 모두 0에 근접한 loss 값을 가진다. 이 정도면 잘 학습 되었다고 해석할 수 있다. train/val/test의 결과값을 해석하는 것은 모델을 디자인 하는 것 만큼 중요하다. 위의 코드에서는 간단한 linear regression을 사용했다. 그러나 어떤 모델을 사용하든 train에서 converge 한다고 val/test에서도 converge 할 것임을 보장하지는 않는다. 어떤 모델은 train에서는 좋은 성능을 보이지만 val/test에서는 좋지 못한 성능을 보일 수도 있는 것이다. 그러나 이번 결과에서는 train/val/test 모두 0에 가까운 그리고 loss가 비슷한 수준으로 converge 했다. 이는 linear regression으로 모델링 하는게 꽤 그럴듯 하다는 해석을 가능케 한다.


Dataset and DataLoader

우리는 지금까지 Batch GD의 방식으로 모델을 학습했다. 그러나 Pre HW2-1에서도 다뤘듯이 Batch GD 보다는 Stochastic GD가 더 선호된다. SGD 학습 방식을 직접 코드로 구현할 수도 있겠지만, PyTorch에서는 SGD에 대한 기능도 제공한다. PyTorch로 SGD를 구현하기 위해 PyTorch의 Dataset, DataLoader에 대해 살펴보자.

from torch.utils.data import Dataset

먼저 우리는 PyTorch Dataset을 만들어야 한다. Dataset은 abstract class로 PyTorch에서의 데이터셋을 표현하는데 사용한다. (지금까지 우리는 pandas.DataFrame이나 np.array로 데이터셋을 표현했다.)

일단 아래와 같이 Dataset을 상속 받는 클래스를 만들자.

from torch.utils.data import Dataset

class MyDataset(Dataset):
  def __init__(self, df):
    self.dataset = df

PyTorch Dataset을 만들 때는 __len__(), __getitem__() 이 두 가지 메소드를 override 해줘야 한다. __len__()len(dataset)을 했을 때 데이터셋의 사이즈를 반환하는 함수고, __getitem__()dataset[i]와 같이 index로 데이터셋에 접근하는 함수이다.

class MyDataset(Dataset):
  def __init__(self, df):
    self.dataset = df

  def __len__(self):
    return len(self.dataset)

  def __getitem__(self, idx):
    return self.dataset.loc[idx]

일단은 여기까지 구현하고 데이터셋 인스턴스를 만들어보자.

dataset = MyDataset(data)

print(len(dataset))
print(dataset[0])

코드를 다듬어서 __getitem__()이 X, y의 pair를 반환하도록 바꿔보자.

class MyDataset(Dataset):
  def __init__(self, df):
    self.X = df[['R&D Spend', 'Administration', 'Marketing Spend']]
    self.y = df[['Profit']]

  def __len__(self):
    return len(self.X)

  def __getitem__(self, idx):
    return (self.X.loc[idx], self.y.loc[idx]) # 처음에는 이걸로 하고, 뒤에서는 아래의 코드로 바꾸자.
    # return (torch.FloatTensor(self.X[idx]), torch.FloatTensor([self.y[idx]]))


dataset = MyDataset(data)

print(len(dataset))
print(dataset[0])

PyTorch Dataset은 이렇게 (X, y)의 pair로 데이터 인스턴스를 반환하거나 또는 {"X": X, "y": y}와 같이 map 형태로 반환하는 게 일반적이다.

이번에는 MyDataset를 생성할 때 pre-processing을 수행할 수 있도록 MinMaxScaler를 넘겨주자. constructor와 코드를 아래와 같이 수정한다.

data = data[['R&D Spend', 'Administration', 'Marketing Spend', 'Profit']]
data_train, data_test = train_test_split(data, test_size=0.2, random_state=1)
data_train, data_val = train_test_split(data_train, test_size=0.2, random_state=1)

scaler = MinMaxScaler()
scaler.fit(data_train)

class MyDataset(Dataset):
  def __init__(self, df, scaler):
    df = scaler.transform(df)
    self.X = df[:, :3]
    self.y = df[:, -1]

  def __len__(self):
    return len(self.X)

  def __getitem__(self, idx):
    return (self.X[idx], self.y[idx])

dataset = MyDataset(data, scaler)

print(len(dataset))
print(dataset[0])

이렇게 하면 train/val/test를 운용하는 것도 한결 쉬워진다.

dataset_train = MyDataset(df_train, scaler)
dataset_val = MyDataset(df_val, scaler)
dataset_test = MyDataset(df_test, scaler)

지금은 정말 간단한 수준의 Dataset을 구현했지만 추후에 컴퓨터 비전 챕터에서 이미지 데이터셋을 만들거나 NLP 챕터에서 단어 데이터셋을 만들 때는 지금의 코드 보다 신경써야 할 부분이 더 많아진다. 👏


지금까지 살펴본 PyTorch의 Datasetdataset[idx]와 같은 방식으로 데이터를 하나씩 얻는 기능만을 제공한다. 그래서 이 녀석으로 SGD를 구현하려면 for...문을 써서 하나씩 학습시켜야 할 것이다. 🤦‍♂️ 또 mini-batch 역시 어떻게 구현해야 할지 막막하다. 그러나 PyTorch의 DataLoader를 함께 쓴다면 걱정할 것이 없다!

PyTorch의 DataLoader는 아래와 같이 Dataset 객체로 생성할 수 있다.

from torch.utils.data import DataLoader

dataloader_train = DataLoader(dataset_train, batch_size=4, shuffle=True)
print(dataloader_train)

인자로 batch_size, shuffle을 받는데, batch_size는 말 그대로 mini-batch의 사이즈이고, shuffle은 데이터셋의 순서대로 mini-batch를 얻을 것인지 아니면 shuffle된 순서로 mini-batch를 얻을 것인지에 대한 인자다.

DataLoader는 일종의 generator이기 때문에 for...에서 아래와 같이 사용한다.

for X, y in dataloader_train:
  print(X, y)

자! 이제 이 녀석을 가지고 기존의 코드를 수정해보자.

data = data[['R&D Spend', 'Administration', 'Marketing Spend', 'Profit']]
data_train, data_test = train_test_split(data, test_size=0.2, random_state=1)
data_train, data_val = train_test_split(data_train, test_size=0.2, random_state=1)

scaler = MinMaxScaler()
scaler.fit(data_train)

dataset_train = MyDataset(df_train, scaler)
dataset_val = MyDataset(df_val, scaler)
dataset_test = MyDataset(df_test, scaler)

dataloader_train = DataLoader(dataset_train, batch_size=4, shuffle=True)

layer = nn.Linear(in_features=3, out_features=1)

MAX_EPOCH = 101
for epoch in range(MAX_EPOCH):
  # SGD
  for X, y in dataloader_train:
    y_pred = layer(X)
    loss = ((y_pred - y)**2).sum()
    loss.backward()

    eta = 1e-2
    with torch.no_grad():
      for p in layer.parameters():
          p.sub_(eta * p.grad)
          p.grad.zero_()

아직은 DataLoader도 train 밖에 안 만들었다. 다음은 loss logging 부분을 구현한다. 이를 위해 evaluate() 함수를 작성하자.

def evaluate(dataloader):
  total_loss = 0

  with torch.no_grad():
    for X, y in dataloader:
      y_pred = layer(X)
      loss = ((y_pred - y)**2).sum()
      total_loss += loss
  
  print(f'{total_loss:.2f}')

이제 기존 코드에 evaluate() 함수를 끼워넣으면 된다.

MAX_EPOCH = 101
for epoch in range(MAX_EPOCH):
  # SGD
  ...
  
  # Evaluation
  if epoch % 10 == 0:
    print(f"===== epoch: {epoch} =====")
    print("[train]")
    evaluate(dataloader_train)

이제 val/test에 대한 dataloader를 작성하면 되는데, batch_size는 적당히 설정하고, shuffle을 꺼주면 된다.

...
dataloader_val = DataLoader(dataset_val, batch_size=4, shuffle=False)
dataloader_test = DataLoader(dataset_test, batch_size=4, shuffle=False)
...

for epoch in range(MAX_EPOCH):
  # SGD
  ...

  # Evaluation
  if epoch % 10 == 0:
    print(f"===== epoch: {epoch} =====")
    print("[train]")
    evaluate(dataloader_train)
    print("[val]")
    evaluate(dataloader_val)
    print("[test]")
    evaluate(dataloader_test)

Optimizer

지금까지 우리는 Batch GD와 SGD를 직접 구현했다.

for X, y in dataloader_train:
  y_pred = layer(X)
  loss = ((y_pred - y)**2).sum()
  loss.backward()

  eta = 1e-2
  with torch.no_grad():
    for p in layer.parameters():
        p.sub_(eta * p.grad)
        p.grad.zero_()

그러나! PyTorch에서는 이미 SGD를 구현 해둔 모듈이 있다! 그것이 바로 PyTorch Optimizer다.

import torch.optim as optim

optimizer = optim.SGD(layer.parameters(), lr=0.01)

optimizer는 parameter와 lr, momentum 등등의 hyper-parameter를 인자로 받는다. 이때, parameter란 딥러닝 모델에서 학습의 대상이 되는 녀석으로 weight, bias를 생각하면 된다.

이제 이 녀석을 활용해 기존 코드를 수정해보자.

layer = nn.Linear(in_features=3, out_features=1)
optimizer = optim.SGD(layer.parameters(), lr=0.01)

MAX_EPOCH = 101
for epoch in range(MAX_EPOCH):
  # SGD
  for X, y in dataloader_train:
    y_pred = layer(X)
    loss = ((y_pred - y)**2).sum()

    # Back-prop
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
  
  # Evaluation
  ...

와우! GD를 구현한 부분이 optimizer.step() 한줄로 바뀌었다!! 👏 이전과 비교해 loss.backward() 전후로 optimzer의 코드가 들어왔는데, optimizer.zero_grad()는 optimizer에 등록된 모델의 parameter의 gradient 값을 0으로 초기화 해주는 함수고, optimizer.step()backward() 단계에서 계산된 grad를 parameter에 적용하는 함수다.

PyTorch Optimizer에는 SGD 말고도 다양한 종류의 최적화 Iterative 한 최적화 방법들이 구현되어 있다. 자세한 내용은 torch.optim 문서에서 확인할 수 있다. 다만, 보통은 optim.SGD() 또는 optim.Adam()을 Optimizer로 사용한다 ✨


맺음말

오늘 진행한 내용까지 잘 이해했다면 PyTorch로 딥러닝 모델을 구축하기 위한 최소한의 준비는 된 것이다! 🙌 보통읜 딥러닝 모델 학습은

# prepare dataset
data = df.read_csv(...)

# create custom dataset class
class MyDataset(Dataset):
  ...

# build DL model
model = nn.Linear(...)

# prepare dataloader
dl_train = DataLoader(...)

# prepare optimizer
optimizer = optim.SGD(...)

# Do SGD
for epoch in range(MAX_EPOCH):
  # train phase
  for X, y in dl_train:
    y_pred = model(X)
    loss = ...

    # Back-prop
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

  # eval phase
  evaluate(dl_train)
  evaluate(dl_val)
  evaluate(dl_test)

와 같은 형식으로 모델 학습이 진행된다. 학습이 완료되면 loss 값 또는 $R^2$을 바탕으로 성능을 평가하고, lr, MAX_EPOCH, DL model, train-test split 비율 등등을 조정하면서 학습을 계속해 가장 좋은 성능을 내는 모델을 찾는다. 이것이 딥러닝 모델 학습이다. 👏


Tip & Tricks

  • val/test와 같이 forward feed만 사용하는 경우에는 batch_size를 train 때보다 조금 더 크게 잡아도 된다. backward feed가 없어 GPU memory를 덜 쓰기 때문. 단, 케바케다.
  • 딥러닝을 정말 제대로 배우고 싶다면, 본 세미나와 함께 다른 강좌를 병행해서 듣는 것을 추천한다.
  • 혹시 이번 학기에 컴공과 인공지능(CSED442)이나 ML/DL 관련 강의를 듣고 있는 분??