기존의 trainer에 문제가 있어 원인을 찾고 수정한다.
dataset은 STL-10 = classification with 10 classess
기존 code
device = 'cuda:1'
def train_val(model, params):
num_epochs=params['num_epochs']
loss_func=params["loss_func"]
opt=params["optimizer"]
train_dl=params["train_dl"]
val_dl=params["val_dl"]
lr_scheduler=params["lr_scheduler"]
path2weights=params["path2weights"]
loss_history = {'train': [], 'val': []}
metric_history = {'train': [], 'val': []}
best_model_weight = copy.deepcopy(model.state_dict())
# 아래에서 best parameter저장할때를 대비하여 미리 모양만 만들어 둔다. 공식문서에서 clone 대신 이거 사용함.
# https://discuss.pytorch.org/t/copy-deepcopy-vs-clone/55022
best_loss = float('inf')
# best model을 저장할때 기준이 loss value 이므로 미리 큰 값으로 설정 해 둔다.(작으면 좋은 거니까)
start_time = time.time()
for epoch in range(num_epochs):
# 1-epoch이다.
current_lr = get_lr(opt)
print('Epoch {}/{}, current lr={}'.format(epoch, num_epochs-1, current_lr))
#-------------------------------------------------------------------------------------------------------
model.train()
# nn.Module에 있다. 하는 역할이 크지는 않다. 하지만 필수 적인데 일단 이는 train과 validation을 구분하게 해준다.
# parameter를 계산안하고 하고를 결정하는 것은 아니고, dropout같이 train과 validation에서
# 다른 연산을 하는 layer들에게 지금 뭘 하고 있는지 알려준다.
running_loss = 0.0
running_metric = 0.0
# epoch 마다 0으로 만들어 준다.
len_data_train = len(train_dl.dataset)
# 이는 전체 data의 개수이다.
for inputs, targets in tqdm(train_dl):
# 1-batch train
inputs , targets = inputs.to(device) , targets.to(device)
outputs = model(inputs)
loss_batch = loss_func(outputs, targets)
metric_batch = metric_function(outputs, targets)
opt.zero_grad() # 이미 저장되어 있는 grad를 없애준다.
loss_batch.backward() # autograd = True 되어있는 parameter들의 위치에서 grad를 계산한다.
opt.step() # update한다.
running_loss += loss_batch.item() # 이는 1-minibatch의 value이고 epoch이 될때까지 누적합을 계산한다.
running_metric += metric_batch
train_loss = running_loss / len_data_train # 따라서 이 값이 1-epoch당 loss와 metric이다. 이때 굳이 loss에서 sum을 하였는데
train_metric = running_metric / len_data_train # 이는 batch 별로 다 더하고 여기서 한번에 다음과 같이 나누는게 편해서이다.
loss_history['train'].append(train_loss) # 매 epoch당 저장해 둔다.
metric_history['train'].append(train_metric)
#-------------------------------------------------------------------------------------------------------
model.eval() # nn.Module에 있다
with torch.no_grad():
# 이렇게 하여 with문 아래에서는 autograd = False가 되는데 이는 with문 아래에서만 일시적으로 그렇하다.
# 아니면 transfer에서 사용하는 방법처럼 layer마다 grad를 off 해줘도 되는데 그럼 또 다시 켜줘야 하니까 이렇게 하는 것이 합리적이다.
running_loss = 0.0
running_metric = 0.0
len_data_val = len(val_dl.dataset)
for inputs, targets in val_dl:
inputs , targets = inputs.to(device) , targets.to(device)
outputs = model(inputs)
loss_batch = loss_func(outputs, targets)
metric_batch = metric_function(outputs, targets)
running_loss += loss_batch.item()
running_metric += metric_batch
val_loss = running_loss / len_data_val
val_metric = running_metric / len_data_val
loss_history['val'].append(val_loss)
metric_history['val'].append(val_metric)
# Best model을 판단하고 저장하고 불러온다.
if val_loss < best_loss:
best_loss = val_loss
best_model_weight = copy.deepcopy(model.state_dict()) # 앞에서와 마찬가지로 parameter를 복사한다.
torch.save(model.state_dict(), path2weights)
print('Copied best model weights!')
print('Get best val_loss')
lr_scheduler.step(val_loss)
print('train loss: %.6f, train accuracy: %.2f' %(train_loss, 100*train_metric))
print('val loss: %.6f, val accuracy: %.2f' %(val_loss, 100*val_metric))
print('time: %.4f min' %((time.time()-start_time)/60))
print('-'*50)
model.load_state_dict(best_model_weight)
######### 항상 마지막 epovc이 best가 아니므로 test를 위해 best의 parameter를 불러준다.
# 이때 불러줄때 이미 짜여진 model의 class에 저렇게 불러줘야 한다.
# 그러면 parameter 자리에 알아서 잘 들어간다.
return model, loss_history, metric_history
물론 학습이 되는데는 전혀 지장이 없으나 살짝 애매한 부분이 있다.
문제점은 batch학습에서 일어 났다.
일단 mini-batch(원래는 mini가 없으면 다르지만 요즘은 그냥 생략해서 사용함.)학습을 다시 생각해보자.
본 dataset에는 5,000개의 train image가 있다. 이에 batch size를 50으로 준다고 해보자.
그러면 5,000개의 loss를 한번에 계산하여 update하는 것이 naive한 방법이나 이는 어떠한 datset이든 memory의 문제가 생길 것이다. 따라서 이를 50개씩 묶어서 총 100번을 계산한다. 이때 50을 1-batch train이라고 하고 이를 100번 즉, 총5,000장의 image를 다 학습하면 이를 1-epoch이라고 한다. 여기서 헷갈렸던건 1-epoch당 총 100번의 update가 된다는 것이다. 즉, 1-epoch의 loss와 metric은 한번의 update로 인한 값이 아닌 총 100번 각각의 값들의 평균이 되어 버린다.
실제 위 code가 그렇게 계산된다.
Trian쪽을 보게 되면 loss와 metric이 0번(학습전) ~ 99번학습의 값들의 평균이 된다. 다음 validation을 보게 되면 100번째 update를 마친다음의 loss와 value이다. 이때또 batch로 계산하기는 하지만, update가 일어나지 않기 때문에 정확히 100번째 학습 후 의 값이 된다.
즉, train과 validation이 다른 시점의 값이며 심지어 1-epoch을 옮긴다고 해도 같아질수가 없다.
따라서 이 둘의 시점을 맞춰보고자 한다.
참고로 위에서 0~99가 나왔던 이유는 loss를 계산해야지 update가 가능했기 때문이다.
몇가지의 방법이 떠오른다.
첫번째
trian과 validation의 batch를 동시에 계산하는 것이다.
편의상 위의 예시를 그대로 사용하고 epoch당 1번 .... 100번째 batch학습이 있다고 하자.
위의 code는 trian일때는 0~99의 평균을 validation일때는 100을 계산 하므로 train을 계산할때 validation도 동시에 계산해서 시점을 맞춰 주는 것이다.
두번째
일단 epoch 학습 후의 loss와 metric이 정확히 뭘 의미하는지를 잘 모르겠다. 그래서 한번의 epoch이 끝난후 loss와 metric을 다시 계산하는 거다. 즉, 위의 예시라면 batch로 본다면 100n의 값들만을 계산한다는 거다.
애매하긴 하다 이게 쫌 그렇다.