Seminar 2
키워드
train_test_split
- PyTorch
Dataset
andDataLoader
- 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의 Dataset
은 dataset[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를 덜 쓰기 때문. 단, 케바케다.
- 딥러닝을 정말 제대로 배우고 싶다면, 본 세미나와 함께 다른 강좌를 병행해서 듣는 것을 추천한다.
- Stanford - CS229
- Stanford - CS231n
- 2019 KAIST 딥러닝 홀로서기
- 본인은 딥러닝 처음 배울 때 PyTorch Tutorial / CS229 / CS231n 3개를 병행하면서 공부했다.
- 혹시 이번 학기에 컴공과 인공지능(CSED442)이나 ML/DL 관련 강의를 듣고 있는 분??