본문 바로가기
카테고리 없음

[파이토치] 3. 학습

by W_log 2023. 12. 29.
model.eval()  # 모델을 평가 모드로 설정
  valid_loss = 0.0
  valid_accuracy = 0

  with torch.no_grad(): # model의 업데이트 막기

전체 개요

 

지난 시간에 모델에 대해서 공부했다면 오늘은 모델에 학습시킬 데이터를 처리하는 부분에 대해서 정리해보았습니다.

 

1. 모델 : 우리가 어떤 신경망 구조로 만들지에 대해서 설계하는 방식으로 이건 설계, 창작의 영역이라고 보면 될것 같다.DNN으로 보면, 각 layer 내부 뉴런의 갯수, CNN에서는 Kernel의 갯수 등을 여기서 설정한다.

 

2. 데이터 : 크게 데이터 전처리와 데이터 로더 작업으로 나눌 수 있으며, 전처리는 우리가 학습시키려는 데이터를 모델에 들어갈 수 있게 텐서를 조절하거나 데이터를 클리닝하는 작업에 해당하며, 데이터 로더는 3번의 학습에 맞게 데이터를 배치 형태로 전달하는 역할을 한다.

*데이터 로더가 왜 필요한지? 처음 공부할 때 이게 왜 필요하지? 생각했는데 공부하면서 이렇게 이해했다. 효율적인 관점이 큰데 학습하는 과정에서 병렬 처리, 배치 처리, 셔플링 등의 작업을 진행해준다.

 

3. 학습 : 이제 1과 2에 정의된 모델과 데이터를 정의한 방식대로 학습을 시킨다. 크게 3가지의 학습 관련 함수가 있는데, training(training 측정), evaluation(validation 측정), test(최종 결과 도출) 함수를 각각 정의한다.

 

 

학습 : 모델과 데이터를 넣어서 가중치를 조정하는 코드

 

1. training code : train_data를 모델에 넣어서 가중치를 학습하는 코드

전체 흐름은 아래와 같다.

 

 

1) model을 train모드로 전환한다.

2) 데이터 로더에서 나온 데이터를 받아서 model에 통과시켜서 output을 뽑아낸다.

3) 정의한 loss function인 criterion으로 output과 정답인 label을 비교해서 loss를 계산한다.

4) 파이토치의 backward와 optimizer.step 메소드를 활용해서 가중치를 업데이트한다.

 

 

 

1) model 모드 설정 & 초기 loss 정의

 

딥러닝 모델을 훈련과 평가 시점에 동작하는게 다르기 때문에 명시적으로 훈련 상태라는 것을 model.train()을 통해서 정의해준다. 주요하게 시점마다 다르게 동작하는건 가중치를 업데이트하는지 안하는지라고 볼 수 있다. 

 

이후, train_loss와 train_accuracy를 계산하기 위해서 0으로 초기화시켜주는 것이 이 작업이다.

 

 

2) dataloader를 활용한 for문 구성 & output 계산

 

  tbar = tqdm(dataloader)
  for images, labels in tbar:
    images = images.to(device)
    labels = labels.to(device)


    outputs = model(images)

 

tqdm은 현재 진행상태를 학습하기 위한 라이브러리이고 tqdm 안에 dataloader를 넣어주고 For문을 통해서 선언하면 어느 정도 쯤에 for문에 도달했는지를 명시해준다. 크게 중요한 영역이 아니기도 하고, 사용법이 간단해서 참고만 해주시면 된다.

 

현재의 dataloader는 CNN 모델을 위한 데이터로더이므로 배치당 이미지와 라벨을 for문을 돌면서 내뱉는다.

병렬 학습을 위해 .to(device)를 통해서 gpu로 넘겨주고, model(images)를 넣어서 output에 넣는다.

 

3) loss 계산 : loss = criterion(outputs, labels)

 

위 한줄의 코드로 loss를 계산할 수 있다. criterion은 추후에 Training_loop를 돌릴 때 정의한다. 

 

 

4) backward & weight update : 가중치를 업데이트하는 작업

    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

 

정의한 optimizer에서 우선 가중치를 초기화하고, loss 기준으로 역전파를 통해서 gradient를 계산하고 그에 따라 parameter들을 업데이트해준다.

 

 

전체 코드는 아래와 같으며, 이 코드는 한 Epoch당 돌아가는 코드이며 최종적으로 Train_loss를 합해서 epoch당 loss를 계산해주기 위해서, 추가적인 코드가 일부 있다.

def training(model, dataloader, train_dataset, criterion, optimizer, device, epoch, num_epochs):
  model.train()
  train_loss = 0.0
  train_accuracy = 0

  tbar = tqdm(dataloader)
  for images, labels in tbar:
    images = images.to(device)
    labels = labels.to(device)


    outputs = model(images)
    loss = criterion(outputs, labels)

    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

    train_loss += loss.item()
    _, predicted = torch.max(outputs, dim = 1)
    train_accuracy += (predicted == labels).sum().item()

    tbar.set_description(f"Epoch [{epoch+1}/{num_epochs}], Train Loss: {loss.item():.4f}")

  train_loss = train_loss/len(dataloader)
  train_accuracy = train_accuracy/len(train_dataset)

  return model, train_loss, train_accuracy

 

 

2. evaluation code : valid_data를 모델에 넣어서 평가하는 코드

evaluation은 training code와 매우 유사하지만 가중치를 업데이트하지 않고 평가만 한다는 점이 다르다. 따라서 model의 설정을 평가 모드로 변경하고, 가중치 업데이트를 하지 말아달라는 의미로 with torch.no_grad():를 넣어주고 training_code에서 진행했던 output 및 loss를 계산하는 코드를 넣어준다.

model.eval()  # 모델을 평가 모드로 설정
valid_loss = 0.0
valid_accuracy = 0

with torch.no_grad(): # model의 업데이트 막기

 

아래는 위 코드를 포함한 전체 Evaluation code이다.

 

def evaluation(model, dataloader, val_dataset, criterion, device, epoch, num_epochs):
  model.eval()  # 모델을 평가 모드로 설정
  valid_loss = 0.0
  valid_accuracy = 0

  with torch.no_grad(): # model의 업데이트 막기
      tbar = tqdm(dataloader)
      for images, labels in tbar:
          images = images.to(device)
          labels = labels.to(device)

          # 순전파
          outputs = model(images)
          loss = criterion(outputs, labels)

          # 손실과 정확도 계산
          valid_loss += loss.item()
          # torch.max에서 dim 인자에 값을 추가할 경우, 해당 dimension에서 최댓값과 최댓값에 해당하는 인덱스를 반환
          _, predicted = torch.max(outputs, 1)
          valid_accuracy += (predicted == labels).sum().item()

          # tqdm의 진행바에 표시될 설명 텍스트를 설정
          tbar.set_description(f"Epoch [{epoch+1}/{num_epochs}], Valid Loss: {loss.item():.4f}")

  valid_loss = valid_loss / len(dataloader)
  valid_accuracy = valid_accuracy / len(val_dataset)

  return model, valid_loss, valid_accuracy

 

3. training_loop code : 1,2번 코드를 epoch 수만큼 학습시키고 early_stopping과 같은 작업을 정의하는 코드

앞에서 evaluation과 training은 한 에폭당 데이터를 학습하고 Loss를 계산하는 코드였다. 실제로 모델은 가중치 업데이트 과정에서 learning_rate 만큼만 업데이트하기 때문에 배치 수만큼의 업데이트를 했더라도 충분하게 optimal solution에 도달하지 않았을 수 있다.

물론 무수히 많은 훈련을 실행하면 local minima에 빠질 수도 있고 오히려 비효율적으로 학습할 수 있다.

 

이 때 몇 번 이상 loss가 증가하는 방향으로 증가하면 학습을 멈추도록 하는 Early Stopping 기법을 적용한다. 따라서 우리가 에폭수를 정의하고, 훈련 자체를 조절하는 함수를 추가한다.

 

코드 자체는 그동안 정의한 함수들을 에폭 수만큼 반복하도록 정의하고, loss가 감소할 때마다 모델을 저장해준다.

 

def training_loop(model, train_dataloader, val_dataloader, train_dataset, val_dataset, criterion, optimizer, device, num_epochs, patience, model_name):
    best_valid_loss = float('inf')  # 가장 좋은 validation loss를 저장
    early_stop_counter = 0  # 카운터
    valid_max_accuracy = -1

    for epoch in range(num_epochs):
        model, train_loss, train_accuracy = training(model, train_dataloader, train_dataset, criterion, optimizer, device, epoch, num_epochs)
        model, valid_loss, valid_accuracy = evaluation(model, val_dataloader, val_dataset, criterion, device, epoch, num_epochs)

        if valid_accuracy > valid_max_accuracy:
          valid_max_accuracy = valid_accuracy

        # validation loss가 감소하면 모델 저장 및 카운터 리셋
        if valid_loss < best_valid_loss:
            best_valid_loss = valid_loss
            torch.save(model.state_dict(), f"./model_{model_name}.pt")
            early_stop_counter = 0

        # validation loss가 증가하거나 같으면 카운터 증가
        else:
            early_stop_counter += 1

        print(f"Epoch [{epoch + 1}/{num_epochs}], Train Loss: {train_loss:.4f}, Train Accuracy: {train_accuracy:.4f} Valid Loss: {valid_loss:.4f}, Valid Accuracy: {valid_accuracy:.4f}")

        # 조기 종료 카운터가 설정한 patience를 초과하면 학습 종료
        if early_stop_counter >= patience:
            print("Early stopping")
            break

    return model, valid_max_accuracy

 

 

4. 훈련 동작 : 위에 정의한 훈련이 실제 진행될 수 있도록 동작시키는 코드

사용자가 직접 지정해줘야하는 Hyperparameter들을 정의해주고 training_loop를 실행하면 학습이 실행된다.

num_epochs = 100
patience = 3
model_name = 'exp1'

lr = 1e-3
criterion = nn.NLLLoss() # NLL loss 후 softmax 를 취하면 CrossEntropy Loss 를 취한 것과 동일하게 됨
optimizer = optim.Adam(model.parameters(), lr = lr)
model, valid_max_accuracy = training_loop(model, train_dataloader, val_dataloader, train_dataset, val_dataset, criterion, optimizer, device, num_epochs, patience, model_name)
print('Valid max accuracy : ', valid_max_accuracy)

 

5. test_code : 최종적으로 training_loop에서 저장된 최적 모델을 불러와서 테스트 데이터셋에 넣어서 score를 출력하는 코드

파이토치의 모델 클래스는 모델을 저장하고 불러오는 함수를 지원해주며, 3번에서 저장된 파일을 불러들이고, 최종적으로 test 데이터를 통해서 결과를 채점하는 코드이다. 

 

evaluation과 동일하게 가중치가 업데이트되면 안되기 때문에 model.eval()과 torch.no_grad()를 실행시켜준다.

model.load_state_dict(torch.load("./model_exp1.pt")) # 모델 불러오기
model = model.to(device)
model.eval()
total_labels = []
total_preds = []
with torch.no_grad():
    for images, labels in tqdm(test_dataloader):
        images = images.to(device)
        labels = labels

        outputs = model(images)
        # torch.max에서 dim 인자에 값을 추가할 경우, 해당 dimension에서 최댓값과 최댓값에 해당하는 인덱스를 반환
        _, predicted = torch.max(outputs.data, 1)

        total_preds.extend(predicted.detach().cpu().tolist())
        total_labels.extend(labels.tolist())

total_preds = np.array(total_preds)
total_labels = np.array(total_labels)
custom_cnn_acc = accuracy_score(total_labels, total_preds) # 정확도 계산
print("Custom CNN model accuracy : ", custom_cnn_acc)

 

이제 진짜 전체 코드는 아래와 같습니다.

def training(model, dataloader, train_dataset, criterion, optimizer, device, epoch, num_epochs):
  model.train()
  train_loss = 0.0
  train_accuracy = 0

  tbar = tqdm(dataloader)
  for images, labels in tbar:
    images = images.to(device)
    labels = labels.to(device)


    outputs = model(images)
    loss = criterion(outputs, labels)

    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

    train_loss += loss.item()
    _, predicted = torch.max(outputs, dim = 1)
    train_accuracy += (predicted == labels).sum().item()

    tbar.set_description(f"Epoch [{epoch+1}/{num_epochs}], Train Loss: {loss.item():.4f}")

  train_loss = train_loss/len(dataloader)
  train_accuracy = train_accuracy/len(train_dataset)

  return model, train_loss, train_accuracy

def evaluation(model, dataloader, val_dataset, criterion, device, epoch, num_epochs):
  model.eval()  # 모델을 평가 모드로 설정
  valid_loss = 0.0
  valid_accuracy = 0

  with torch.no_grad(): # model의 업데이트 막기
      tbar = tqdm(dataloader)
      for images, labels in tbar:
          images = images.to(device)
          labels = labels.to(device)

          # 순전파
          outputs = model(images)
          loss = criterion(outputs, labels)

          # 손실과 정확도 계산
          valid_loss += loss.item()
          # torch.max에서 dim 인자에 값을 추가할 경우, 해당 dimension에서 최댓값과 최댓값에 해당하는 인덱스를 반환
          _, predicted = torch.max(outputs, 1)
          valid_accuracy += (predicted == labels).sum().item()

          # tqdm의 진행바에 표시될 설명 텍스트를 설정
          tbar.set_description(f"Epoch [{epoch+1}/{num_epochs}], Valid Loss: {loss.item():.4f}")

  valid_loss = valid_loss / len(dataloader)
  valid_accuracy = valid_accuracy / len(val_dataset)

  return model, valid_loss, valid_accuracy


def training_loop(model, train_dataloader, val_dataloader, train_dataset, val_dataset, criterion, optimizer, device, num_epochs, patience, model_name):
    best_valid_loss = float('inf')  # 가장 좋은 validation loss를 저장
    early_stop_counter = 0  # 카운터
    valid_max_accuracy = -1

    for epoch in range(num_epochs):
        model, train_loss, train_accuracy = training(model, train_dataloader, train_dataset, criterion, optimizer, device, epoch, num_epochs)
        model, valid_loss, valid_accuracy = evaluation(model, val_dataloader, val_dataset, criterion, device, epoch, num_epochs)

        if valid_accuracy > valid_max_accuracy:
          valid_max_accuracy = valid_accuracy

        # validation loss가 감소하면 모델 저장 및 카운터 리셋
        if valid_loss < best_valid_loss:
            best_valid_loss = valid_loss
            torch.save(model.state_dict(), f"./model_{model_name}.pt")
            early_stop_counter = 0

        # validation loss가 증가하거나 같으면 카운터 증가
        else:
            early_stop_counter += 1

        print(f"Epoch [{epoch + 1}/{num_epochs}], Train Loss: {train_loss:.4f}, Train Accuracy: {train_accuracy:.4f} Valid Loss: {valid_loss:.4f}, Valid Accuracy: {valid_accuracy:.4f}")

        # 조기 종료 카운터가 설정한 patience를 초과하면 학습 종료
        if early_stop_counter >= patience:
            print("Early stopping")
            break

    return model, valid_max_accuracy

num_epochs = 100
patience = 3
model_name = 'exp1'

lr = 1e-3
criterion = nn.NLLLoss() # NLL loss 후 softmax 를 취하면 CrossEntropy Loss 를 취한 것과 동일하게 됨
optimizer = optim.Adam(model.parameters(), lr = lr)
model, valid_max_accuracy = training_loop(model, train_dataloader, val_dataloader, train_dataset, val_dataset, criterion, optimizer, device, num_epochs, patience, model_name)
print('Valid max accuracy : ', valid_max_accuracy)


model.load_state_dict(torch.load("./model_exp1.pt")) # 모델 불러오기
model = model.to(device)
model.eval()
total_labels = []
total_preds = []
with torch.no_grad():
    for images, labels in tqdm(test_dataloader):
        images = images.to(device)
        labels = labels

        outputs = model(images)
        # torch.max에서 dim 인자에 값을 추가할 경우, 해당 dimension에서 최댓값과 최댓값에 해당하는 인덱스를 반환
        _, predicted = torch.max(outputs.data, 1)

        total_preds.extend(predicted.detach().cpu().tolist())
        total_labels.extend(labels.tolist())

total_preds = np.array(total_preds)
total_labels = np.array(total_labels)
custom_cnn_acc = accuracy_score(total_labels, total_preds) # 정확도 계산
print("Custom CNN model accuracy : ", custom_cnn_acc)

 

 

 

여기까지가 기본적으로 파이토치를 활용해 모델을 만드는 코드였다. 실제로 파이토치 코드 자체를 공부할 때 배우는 함수보다 훨씬 더 간단한 함수들을 사용하고 있고, 파이토치를 익숙하게 하기위해 코드 자체를 사용하기 보다는 직접 이렇게 모델을 구현해보면서 배우는게 기억에 더 잘 남고 효율적인 것 같다.