: 처음에 가장 부담없는 REGRESSION 문제를 풀어보려고 한다.
: Import -> Data -> Dataset -> DataLoader -> Model -> Loss Function and Optimizer -> train_one_epoch() and valid_one_epoch() -> run_train()
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
: nn을 이용해서 nn.Module을 상속받아서 다음 예시처럼 Model을 빌드한다. 그래서 필요하다.
class Model(nn.Module):
def __init__(self, input_dim = 8, output_dim = 1):
super().__init__()
self.fc1 = nn.Linear()
...
: Dataset과 DataLoader는 데이터를 배치 단위로 학습할 데이터(x)와 정답 데이터(y)를 묶어서 뱉어주는 역할을 한다.
: SCIKIT-LEARN에서 CALIFORNIA HOUSING 데이터이다. EDA, PREPROCESSING 모두 생략한다.
from sklearn.datasets import fetch_california_housing
ch = fetch_california_housing()
df = pd.DataFrame(ch.data, columns = ch.feature_names)
df['target'] = ch.target
print(df.shape)
df.head()
: Dataset을 상속받아서 MyDataset 클래스를 만들어준다.
class MyDataset(Dataset):
def __init__(self, df = df):
self.df = df
self.x = df.iloc[:, :-1].values
# numpy
# df.loc[:, "MedInc":"target"]
self.y = df.iloc[:, -1:].values
# numpy
def __len__(self):
# 전체 데이터의 길이 정보(총 row 수) 반환
return self.x.shape[0]
def __getitem__(self, index):
# index로 row 하나를 특정합니다.
x = self.x[index] # numpy # Shape: [8]
y = self.y[index] # numpy # Shape: [1]
## 우리는 토치의 민족이기 때문에 위 x, y를 토치 텐서로 반환하여 반환해준다.
return torch.tensor(x, dtype = torch.float), torch.tensor(y, dtype = torch.float)
# Shape: [8] # Shape: [1]
## 다른 데서 코드 가져옴
for fold, ( _, val_) in enumerate(skf.split(X=df, y=df['target'])):
df.loc[val_ , "kfold"] = int(fold)
# _, val_ : index 넘버
return torch.tensor(x, dtype = torch.float), torch.tensor(y, dtype = torch.float)
: 여기서는 학습에 사용할 데이터와 성능 검증에 필요한 데이터로 쪼개준다. 그리고 각 데이터에 대해서 배치 단위로 뱉어줄 수 있도록 DataLoader 객체를 각각 만들어준다. 이 과정을 prepare_loaders() 함수에 담았다.
def prepare_loaders(df = df, index_num = 14000, bs =256):
# train, valid split - 학습을 위한 train과 성능 검증을 위한 valid로 쪼개준다.
train = df[:index_num].reset_index(drop = True)
valid = df[index_num:].reset_index(drop = True) # 혹시 몰라서 하는 index 초기화
# train_ds, valid_ds
# 위애서 선언한 MyDataset으로 train, valid에 대한 Pytorch Dataset 객체를 만든다.
# --> train_ds, valid_ds
train_ds = MyDataset(df = train)
valid_ds = MyDataset(df = valid)
# train_loader, valid_loader
# Batch 단위로 데이터들을 묶어서 뱉어주는 DataLoader로 train_loader, valid_loader 객체를 만든다.
train_loader = DataLoader(train_ds, # Pytorch Dataset
shuffle = True, # 섞을래 말래?
batch_size = bs, # 배치 단위 사이즈 크기 현재 256으로 디폴트
# pin_memory = True, num_workers = 2,
# > 필자는 '내 컴이 아니면 쓴다.'라는 공식으로 사용여부를 결정한다.
)
# 이 외에도 drop_last, collate_fn, sampler 등 여러 아규먼트들이 있다.
valid_loader = DataLoader(valid_ds, shuffle = False, batch_size = bs)
return train_loader, valid_loader
train_loader, valid_loader = prepare_loaders()
: Data의 Shape 추적이 가장 중요하다. 그래서 여기서도 확인해볼 필요가 있다. 아마 x에 해당하는 부분의 Shape은 [256, 8], y에 해당하는 부분의 Shape은 [256, 1] 으로 나올 것이다.
## train_loader가 뱉는 배치 크기 확인해보자
data = next(iter(train_loader))
data[0].shape, data[1].shape
: torch.tensor를 비롯해서 train_loader, valid_loader에서 나오는 배치들과 Model의 layer들을 모두 GPU로 보내기 위한 코드이다. M1, Colab에서 적용할 수 있는 코드이며, '지금 GPU 쓸 수 있니? 없니?' 라고 확인 후, if else 조건문을 통해 GPU로 보내는 코드이다.
# Colab
# device = torch.device("cuda:0") if torch.cuda.is_available() else torch.device("cpu")
# M1 버전
device = torch.device("mps") if torch.backends.mps.is_available() else torch.device("cpu")
: nn.Module을 상속받아서 Model 클래스를 빌드한다.
class Model(nn.Module):
def __init__(self,):
super().__init__()
# nn.Module 상속시 반드시 같이 해야 에러가 나지 않음
# nn.Module에서 선언된 변수들이 선언되어있는데 땡겨오는 것
# 참고: https://velog.io/@minchoul2/nn.Module-init시-super.init-해야하는-이유
# input data(=x)'s shape: [bs, 8]
# input data(=x)는 배치단위로 들어오는 배치 덩어리. bs는 배치단위의 크기를 의미
# Feature Extraction 과정
# : [bs, 8] -> [bs, 3] -> [bs, 1]
self.fc1 = nn.Linear(8, 3)
# [bs, 8] -> [bs, 3]
self.relu = nn.ReLU()
# [bs, 3] -> [bs, 3]
# 활성화 함수를 지날 때에는 Shape 변화가 없다. (단순히 비선형만 만들기 때문)
self.fc2 = nn.Linear(3, 1)
# [bs, 3] -> [bs, 1]
# [bs, 1]이 최종적으로 model이 예측한 y_pred(=predicted)가 된다.
def forward(self, x):
# x: input_data - batch 단위
# 실질적으로 연산이 이루어지는 곳 (함수 형태로 엮어줌(= 넣어줌))
# x: [bs, 8]
x = self.fc1(x) # [bs, 8] -> [bs, 3]
x = self.relu(x) # [bs, 3] -> [bs, 3]
y_pred = self.fc2(x) # [bs, 3] -> [bs, 1]
return y_pred
# y_pred's shape: [bs, 1]
model = Model().to(device) # GPU로 보내준다.
: Loss Function과 Optimizer를 선언한다.
# loss_fn도 to(device)가 가능하다. (= GPU로 보내줄 수 있다.)
loss_fn = nn.MSELoss().to(device)
# optimizer는 to(device)가 불가능하다. (= GPU로 보내줄 수 없다.)
optimizer = torch.optim.Adam(model.parameters(), lr = 5e-3) # 수강생분이 이걸로 하자고 하셔서 이걸로 해 보았다.
: model이 한 epoch를 도는 동안, model이 학습되는 과정과 train_epoch_loss(=epoch당 평균 train loss)를 return 시키는 함수이다.
def train_one_epoch(model = model,
dataloader = train_loader,
loss_fn = loss_fn,
optimizer = optimizer,
device = device):
# model을 학습시키겠다고 선언
model.train()
train_loss = 0
for data in dataloader:
# train_loader = [([bs, 8], [bs, 1]), ([bs, 8], [bs, 1]), .... ]
x = data[0].to(device) # [bs, 8] # GPU로 보내준다.
y = data[1].to(device) # [bs, 1] # GPU로 보내준다.
y_pred = model(x) # [bs, 1] # model을 통해 예측한 값
loss = loss_fn(y_pred, y) # MSE Loss
optimizer.zero_grad() # Gradient 초기화
loss.backward() # 역전파할 Gradient 구함
optimizer.step() # 역전파할 Gradient 토대로 Weight 업데이트
train_loss += loss.item()
train_epoch_loss = train_loss / len(dataloader) # epoch 당 train loss 평균값
return train_epoch_loss
: model이 한 epoch를 도는 동안, valid_epoch_loss(=epoch당 평균 valid loss)를 return 시키는 함수이다.
# @torch.no_grad() : 데코레이터 형태의 'model을 학습 시키지 않겠다는 필수 의지 표명 2'
# --> 근데 함수형일 때 쓸 수 있는 것으로 알고 있다.
# --> model을 학습 시키지 않겠다는 필수 의지 표명 1과 2 중 하나만 써도 무방하다.
@torch.no_grad()
def valid_one_epoch(model = model,
dataloader = valid_loader,
loss_fn = loss_fn,
device = device):
# 여기서는 optimizer가 필요없다. 학습을 하지 않기 때문
# model을 학습 안 시키겠다고 선언
model.eval()
valid_loss = 0
# model을 학습 시키지 않겠다는 필수 의지 표명 1
with torch.no_grad():
for data in dataloader:
# valid_loader = [([bs, 8], [bs, 1]), ([bs, 8], [bs, 1]), .... ]
x = data[0].to(device) # [bs, 8] # GPU로 보내준다.
y = data[1].to(device) # [bs, 1] # GPU로 보내준다.
y_pred = model(x) # [bs, 1] # model을 통해 예측한 값
loss = loss_fn(y_pred, y) # MSE Loss - 성능평가
valid_loss += loss.item()
valid_epoch_loss = valid_loss / len(dataloader) # epoch 당 valid loss 평균값
return valid_epoch_loss
: 여기서는 전반적인 train 과정을 담았다.
def run_train(model = model,
n_epochs = 150,
train_loader = train_loader,
valid_loader = valid_loader,
loss_fn = loss_fn,
optimizer = optimizer,
device = device):
# 시각화를 위해서 줍줍할 list
train_hs, valid_hs = [], []
# monitoring을 위한 주기
print_iter= 20
# Lowest Loss 갱신할 때 필요
lowest_loss, lowest_epoch = np.inf, np.inf
# 오버 피팅 방지
early_stop = 20
for epoch in range(n_epochs):
# epoch 당 평균 train loss: train_epoch_loss
train_loss = train_one_epoch(model = model,
dataloader = train_loader,
loss_fn = loss_fn,
optimizer = optimizer,
device = device)
# epoch 당 평균 valid loss: valid_epoch_loss
valid_loss = valid_one_epoch(model = model,
dataloader = valid_loader,
loss_fn = loss_fn,
device = device)
# train_epoch_loss, valid_epoch_loss를 위에서 선언한 빈 리스트들에 담아둔다. (시각화)
train_hs.append(train_loss)
valid_hs.append(valid_loss)
# monitoring
# : 특정 주기(=print_iter)마다 Train Loss, Valid Loss, Lowest Loss를 찍어준다.
if (epoch + 1) % print_iter == 0:
print(f"Epoch{epoch}|TL:{train_loss:.3e}|VL:{valid_loss:.3e}|LL:{lowest_loss:.3e}|")
# Lowest Loss 갱신 - Valid Loss 기준
if valid_loss < lowest_loss:
# = valid loss가 기존의 Lowest Loss보다 작을 때
# = Lowest Loss가 갱신되었을 때
lowest_loss = valid_loss # 이 때 valid loss를 Lowest loss로 새로 재임명
lowest_epoch= epoch # Lowest Loss가 갱신되었을 때의 epoch를
# lowest_epoch 변수에 저장
# Lowest Loss가 갱신되었을 때의 model 저장
torch.save(model.state_dict(), './model_regression.bin')
# pth, pt 확장자로도 저장 가능!
else:
# else: valid loss가 기존의 Lowest Loss보다 여전히 커서, Lowest Loss가 갱신되지 않는 상황
if early_stop > 0 and lowest_epoch+ early_stop < epoch +1:
# Lowest Loss가 나왔던 지점(lowest_epoch) 보다
# early_stop만큼의 epoch 수만큼 train이 진행되었는데도 여전히 같은 상황이라면
print("삽 질 중")
# 원래는 print("There is no improvement during %d epochs" % early_stop)
break
# 중단
print()
print("The Best Validation Loss=%.3e at %d Epoch" % (lowest_loss, lowest_epoch))
# model load : Lowest Loss가 마지막으로 갱신되 지점에서 저장된 모델을 불러온다.
model.load_state_dict(torch.load('./model_regression.bin'))
result = dict()
# train_epoch_loss가 담긴 train_hs와
# valid_epoch_loss가 담긴 valid_hs를
# result라는 dictionary에 담아서 깔끔하게 반환
result["Train Loss"] = train_hs
result["Valid Loss"] = valid_hs
return model, result
: 다음 코드로 학습 시작!
model, result = run_train()
"삽 질 중"이라는 문구가 떴다면, early stop이 적용된 것을 알 수 있다.
## Train/Valid History
plot_from = 0
plt.figure(figsize=(20, 10))
plt.title("Train/Valid Loss History", fontsize = 20)
plt.plot(
range(0, len(result['Train Loss'][plot_from:])),
result['Train Loss'][plot_from:],
label = 'Train Loss'
)
plt.plot(
range(0, len(result['Valid Loss'][plot_from:])),
result['Valid Loss'][plot_from:],
label = 'Valid Loss'
)
plt.legend()
plt.yscale('log')
plt.grid(True)
plt.show()