# pytorch에서는 keras와는 달리 compile, fit으로 쉽게 사용하는 부분을 전부 직접 코딩해야 한다고 한다.
# 이것도 모르고 인터넷에서 찾은 자료만 보고 compile을 못 찾고 있었다.
# compile, fit을 직접 만들어야 하기 때문에 실제 모델이 돌아가면서 해야할 일을 직접 코딩해 내 입맛에 맞출 수 있나보다.
# 신경망의 구조를 모르면 구현하기 힘들 것 같다.
# 아 그냥 keras로 할걸, 그래도 좋은 경험 했다 생각하자.
1. compile & fit & evaluate
- 이 notebook에는 compiling과 fit, model evaluate이 한 function에 있다. 그래서 처음 딥러닝을 접하거나, 어색한 사람은 보다가 너무 길어 살짝 멘붕이 올 수도 있겠다.(내가 그랬거든...)
● optimizer
avg_val_loss = 0.
optimizer = getattr(torch.optim, optimizer)(model.parameters(), lr=lr)
● DataLoader
# DataLoaders
train_loader = DataLoader(
train_dataset,
batch_size = batch_size,
shuffle = True,
drop_last = True,
num_workers = NUM_WORKERS,
pin_memory = True,
worker_init_fn = worker_init_fn
)
val_loader = DataLoader(
val_dataset,
batch_size = val_bs,
shuffle = False,
num_workers = NUM_WORKERS,
pin_memory = True
)
- 데이터를 불러오는 generator
- dataset에서 데이터를 가져오는 방식을 설정한다.
- batch_size, shuffle(섞을지 말지) 등등을 설정해 주면 알아서 데이터를 뽑아온다.
- return value는 전에 정의한 get_item의 return value로 반환된다.
class VentilatorDataset(Dataset):
...
def __getitem__(self, idx):
data = {
'input': torch.tensor(self.inputs[idx], dtype=torch.float),
'u_out': torch.tensor(self.u_outs[idx], dtype=torch.float),
'p': torch.tensor(self.pressures[idx], dtype=torch.float)
}
def __len__(self):
return self.df_shape[0]
- 마지막 return value의 batch는 __len__을 batch로 나눈 나머지값이 됨
- 보통 for문을 통해 반복시키나, iterator를 만들고 next를 사용해 수동으로 가져올 수 도 있다.
● loss function
#Loss function
loss_fct = VentilatorLoss()
● loss function 정의
# metric & loss
import torch.nn as nn
#metric 계산
def compute_metric(df, preds):
y = np.array(df['pressure'].values.tolist())
w = 1 - np.array(df['u_out'].values.tolist())
assert y.shape == preds.shape and w.shape == y.shape, (y.shape, preds.shape, w.shape)
# assert(가정 설정문) : 조건문 : y.shape == preds.shape and w.shape == y.shape 가 False면 AssertError가 발생
# 에러가 발생하면서 (y.shape, preds.shape, w.shape)를 출력
# 단순히 에러를 찾을 뿐만 아니라 값을 보증하기 위해 사용.
# Q. 왜 u_out이 0인 애들만 기록할까?
# A. the competition will be scored as the mean absolute error between predicted and actual pressures during the inspiratory phase of each breath.
# inspiratory phase일 때 predict와 actual pressure와의 MAE만 보고 점수를 매긴다.
# 오늘의 교훈: 문제를 잘 읽자.
mae = w*np.abs(y - preds)
mae = mae.sum() / w.sum()
return mae
# Loss 계산
class VentilatorLoss(nn.Module):
def __call__(self, preds, u_out):
w = 1 - u_out
mae = w*(y-preds).abs()
mae = mae.sum(-1) / w.sum(-1) # sum(-1) : shape가 (a0, a1, a2)일 때, a2끼리 더한다는 뜻
return mae
- metric을 mae로 했으며, u_out = 0인 부분만 계산했다.
● Scheduler
: learning reate를 지속적으로 변경시켜줘서 학습 성능을 높여준다.
# Scheduler : lr를 실시간으로 변경시켜줌
num_warmup_steps = int(warmup_prop * epochs * len(train_loader)) # int( 0.1 * 50 * len(train_loader))
num_training_steps = int(epochs * len(train_loader)) # 50 * len(train_loader)
scheduler = get_linear_schedule_with_warmup(
optimizer, num_warmup_steps, num_training_steps # num_warmup_steps = num_training_steps = 0.1
)
- lr이 너무 크면 predict가 너무 튀어 학습이 산으로 가고, lr이 너무 작으면 천년만년 시간이 걸릴 수 있으니, 지속적으로 lr을 변경시켜줘서 학습의 효율과 성능을 함께 잡는다는 아이디어
- get_linear_schedule_with_warmup : warmup step동안에 lr이 linear하게 늘어나고, 그 후 lr이 linear하게 줄어드는 scheduler이다.
- optimizer : scheduler에 넣을 optimizer 객체
- num_warmup_steps : 얼마나 warm up 시킬 건지 ( training step의 1/10 )
- num_training_steps : training step 크기
● training & evaluation
for epoch in range(epochs):
model.train() # train mode로 model을 전환 <--> model.eval()
model.zero_grad() # back_prop시 필요
# 매번 loss.backward()를 호출할 때 초기 설정은 매번 gradient를 더해 주는 것으로 설정되어 있다.
# 따라서 학습 loop을 돌 떄 이상적으로 학습이 이루어지기 위해선 한번의 학습이 완료되어 지면
# 즉, Iteration이 한 번 끝나면, gradients를 항상 0으로 만들어 주어야 한다.
start_time = time.time()
# model training
avg_loss = 0
for data in train_loader:
pred = model(data['input'].to(device)).squeeze(-1)
loss = loss_fct(
pred,
data['p'].to(device),
data['u_out'].to(device),
).mean() # custom loss function에서 평균을 구해주기 때문에, 꼭 필요한 건 아닌 것 같다.
loss.backward()
avg_loss += loss.item() / len(train_loader)
optimizer.step()
scheduler.step() # lr을 update
for param in model.parameters(): # parameter gradient 초기화
param.grad = None
# model evaluation 시작 (with validation data)
model.eval()
mae, avg_val_loss = 0, 0
preds = []
with torch.no_grad():
for data in val_loader:
pred = model(data['input'].to(device)).squeeze(-1)
loss = loss_fct(
pred.detach(),
data['p'].to(device),
data['u_out'].to(device),
).mean()
avg_val_loss += loss.item() / len(val_loader)
preds.append(pred.detach().cpu().numpy())
preds = np.concatenate(preds, 0)
mae = compute_metric(val_dataset.df, preds)
# print
elapsed_time = time.time() - start_time
if (epoch + 1) % verbose == 0:
elapsed_time = elapsed_time * verbose
lr = scheduler.get_last_lr()[0]
print(
f"Epoch {epoch + 1:02d}/{epochs:02d} \t lr={lr:.1e}\t t={elapsed_time:.0f}s \t"
f"loss={avg_loss:.3f}",
end="\t",
)
if (epoch + 1 >= first_epoch_eval) or (epoch + 1 == epochs):
print(f"val_loss={avg_val_loss:.3f}\tmae={mae:.3f}")
else:
print("")
del (val_loader, train_loader, loss, data, pred)
gc.collect()
torch.cuda.empty_cache()
return preds
- Torch.no_grad()
- gradient 계산을 안하게끔 만드는 context-manager
- model 추론 ( validation OR test data로 하는 과정들 ) 과정에서 사용한다. 쓸데없는 메모리 소비를 줄인다.
- 다른 thread에 영향을 미치지 않는다(thread local)
- with ~~:
- 자원을 획득하고 사용 후 반납해야 하는 경우 사용
- e.g. : with open([file]): file을 열었을 때 닫아주어야 하는데, 보통 close()를 써도 되는데, 파일 처리를 수행하는 도중 오류가 발생하면 close()가 실행이 안될 수도 있음. 이럴 때, with statement안에 넣어줬으면, 에러가 발생하든 안하든 close()가 실행됨
- with Torch.no_grad():
model training과 evaluation을 함께 진행한다.
● 전체 코드
import gc
import time
import torch
import numpy as np
from torch.utils.data import DataLoader
from transformers import get_linear_schedule_with_warmup
def fit(
model,
train_dataset,
val_dataset,
loss_name = 'L1loss',
optimizer = 'Adam',
epochs = 50,
batch_size = 32,
val_vs = 32,
warmup_prop = 0.1,
lr = 1e-3, # 0.001
num_classes = 1,
verbose = 1,
first_eopch_eval = 0,
device = 'cuda'
):
avg_val_loss = 0.
optimizer = getattr(torch.optim, optimizer)(model.parameters(), lr=lr)
# == torch.optim.Adam(model.parameters(), lr=lr)
# DataLoaders
train_loader = DataLoader(
train_dataset,
batch_size = batch_size,
shuffle = True,
drop_last = True,
num_workers = NUM_WORKERS,
pin_memory = True,
worker_init_fn = worker_init_fn
)
val_loader = DataLoader(
val_dataset,
batch_size = val_bs,
shuffle = False,
num_workers = NUM_WORKERS,
pin_memory = True
)
#Loss function
loss_fct = VentilatorLoss()
# Scheduler : lr를 실시간으로 변경시켜줌
num_warmup_steps = int(warmup_prop * epochs * len(train_loader)) # int( 0.1 * 50 * len(train_loader))
num_training_steps = int(epochs * len(train_loader)) # 50 * len(train_loader)
scheduler = get_linear_schedule_with_warmup(
optimizer, num_warmup_steps, num_training_steps # num_warmup_steps = num_training_steps = 0.1
)
for epoch in range(epochs):
model.train() # model를 train mode로 전환 ( <-> model.eval())
model.zero_grad() # back_prop시 필요
# TODO. 조큼 이해가 안간다.
# 매번 loss.backward()를 호출할 떄 초기 설정은 매번 gradient를 더해 주는 것으로 설정되어 있다.
# 따라서 학습 loop을 돌 떄 이상적으로 학습이 이루어지기 위해선 한번의 학습이 완료되어 지면
# 즉, Iteration이 한 번 끝나면, gradients를 항상 0으로 만들어 주어야 한다.
start_time = time.time()
avg_loss = 0
for data in train_loader:
pred = model(data['input'].to(device)).squeeze(-1)
# squeeze() - shape(2, 1, 2, 1, 2)에서 1인 애들을 다 삭제해서 shape가 (2, 2, 2)가 되도록
# squeeze(-1) 맨 마지막 dimension에서만 squeeze 진행
loss = loss_fct(
pred,
data['p'].to(device), # 원리가 무얼까. GPU로 보낸다는 건 알겠는데
data['u_out'].to(device)
).mean()
# 각 parameter들의 .grad 값에 변화도가 저장이 된다.
# 이 후 다음 루프에서 zero_grad()를 하지 않고, back_prop을 시키면
# 이전 loop에서 .grad에 저장된 값이 다음 loop에도 간섭이 되어서 원하는 방향으로 학습이 안됨
loss.backward()
avg_loss += loss.item() / len(train_loader)
optimizer.step() # TODO. model parameter를 update https://anweh.tistory.com/22
scheduler.step() # TODO. learning rate를 update
for param in model.parameters():
param.grad = None
# evalutation mode로 변경: evaluation 과정에서 사용하지 않아야 하는 layer들을 알아서 off시키도록 함.
# https://bluehorn07.github.io/2021/02/27/model-eval-and-train.html
model.eval()
mae, avg_val_loss = 0, 0
preds = []
##
with torch.no_grad():
for data in val_loader:
pred = model(data['input'].to(device).squeeze(-1))
loss = loff_fct(
pred.detach(),
data['p'].to(device),
data['u_out'].to(device)
).mean()
avg_val_loss += loss.item() / len(val_loader)
preds.append(pred.detach().cpu().numpy()) # pred를 numpy로 만들기 위해서 detach()
preds = np.concatenate(preds, 0)
mae = compute_metric(val_dataset.df, preds)
elapsed_time = time.time() - start_time
if (epoch + 1) % verbose -- 0:
elapsed_time = elapsed_time * verbose
lr = scheduler.get_last_lr()[0]
print(
f'Epoch {epoch + 1:02d}/{epochs:02d} \t lr={lr:.1e}\t t={elapsed_time:.0f}s \t loss={avg_loss:.3f}'
)
if (epoch + 1 >= first_epoch_eval) or (epoch + 1 == epochs):
print(f'val_loss={avg_val_loss:.3f}\tmae{mae:.3f}')
else:
print('')
del (val_loader, train_loader, loss, data, pred)
gc.collect()
torch.cuda.empty_cache()
return preds
자료 출처 : https://aruie.github.io/2020/01/12/torch.html
https://huggingface.co/transformers/main_classes/optimizer_schedules.html