부스트캠프 AI Tech 4기

Transfer Learning + Hyper Parameter Tuning

쉬엄쉬엄블로그 2023. 5. 26. 14:18
728x90

이 색깔은 주석이라 무시하셔도 됩니다.

Transfer Learning and Hyperparameter Tuning

  • Convolutional Neural Networks에서 Transfer Learning (Fine-Tune, torchvision)과 Hyperparameter Tuning을 위한 pytorch 및 추가 라이브러리(Ray Tune) 사용법 학습

Transfer Learning

  • 현실의 문제를 다룰 때, 정제된 충분한 데이터를 수집하는 일은 어려움
  • 이에 따라 적은 데이터로 좋은 모델을 만드는 방법론이 다양하게 개발되었는데, 그 중 하나가 Transfer Learning 방법론
  • Transfer Learning은 지식 전이(Knowledge Transfer)를 위한 방법론으로, “Source Tasks”에서 학습된 지식을 “Target Task”로 전이하는 절차 및 방법론을 의미
  • Source Tasks와 Target Tasks에 정답(Label)의 유무에 따라 다양한 Transfer Learning 방법론이 사용될 수 있음
  • Source Tasks와 Target Tasks에 모두 정답이 있는 상황에서 사용될 수 있는 “Fine-Tuning”을 실습

Source Task 모델 생성

  • Fashion-Mnist (Target Task)를 분류하는 모델을 만들기 위해, 지식을 전이할 Source Task 모델을 만듬
  • Pytorch 개발팀에서는 Tensor 연산을 위한 pytorch 라이브러리 외에 음성 데이터 및 모델을 다루기 위한 torchaudio, 영상 데이터 및 모델을 위한 torchvision, 모델 서빙을 위한 torchserve 등의 다양한 추가 라이브러리를 개발하여 제공하고 있음
  1. ImageNet Pretrained Model을 torchvision에서 불러오기

     import torchvision
     import torch
    
     import numpy as np
    
     # ImageNet에서 학습된 ResNet 18 딥러닝 모델을 불러옴
     imagenet_resnet18 = torchvision.models.resnet18(pretrained=True)
     print("네트워크 필요 입력 채널 개수", imagenet_resnet18.conv1.weight.shape[1])
     print("네트워크 출력 채널 개수 (예측 class type 개수)", imagenet_resnet18.fc.weight.shape[0])
     print(imagenet_resnet18)
  2. MNIST Pretrained Model 만들기

    • Mnist Dataset 불러오기

        # mnist train 데이터와 test 데이터 불러오기 - Mnist 데이터셋은 0부터 9까지 손으로 쓰인 10가지의 클래스가 있는 데이터셋
        # ref : http://yann.lecun.com/exdb/mnist/
        mnist_train = torchvision.datasets.MNIST(root='./mnist', train=True, download=True)
        mnist_test = torchvision.datasets.MNIST(root='./mnist', train=False, download=True)
    • Mnist를 학습할 CNN 모델 생성하기 (Resnet18)

        mnist_resnet18 = torchvision.models.resnet18(pretrained=False)
        print("네트워크 필요 입력 채널 개수", mnist_resnet18.conv1.weight.shape[1])
        print("네트워크 출력 채널 개수 (예측 class type 개수)", mnist_resnet18.fc.weight.shape[0])
        print("네트워크 구조", mnist_resnet18)
    • Mnist 데이터 분류 Resnet18 모델에 학습하기

        # torchvision.datasets.mnist의 데이터 타입은 PIL Image이고, 학습 때는 torch로 type 변경이 필요함
        # + 원본 영상은 grayscale이라서 채널이 한개뿐인 입력이지만 모델은 3채널이기 때문에 grayscale을 RGB로 변경해주어야함
        # (참고) grayscale의 입력을 모델에 넣고 싶으면, 모델 입력을 channel 1개만 받도록 변경할 수도 있음 (hint - `mnist_resnet18.conv1 = nn.Conv2d(...)`)
        print('원본', type(mnist_train[0][0]), np.array(mnist_train[0][0]).shape)
      
        common_transform = torchvision.transforms.Compose(
          [
            torchvision.transforms.Grayscale(num_output_channels=3), # grayscale의 1채널 영상을 3채널로 동일한 값으로 확장함
            torchvision.transforms.ToTensor() # PIL Image를 Tensor type로 변경함
          ]
        )
        # 앞서 선언한 데이터셋에 transform 인자를 넘겨주자
        mnist_train_transformed = torchvision.datasets.MNIST(root='./mnist', train=True, download=True, transform=common_transform)
        mnist_test_transformed = torchvision.datasets.MNIST(root='./mnist', train=False, download=True, transform=common_transform)
      
        print('변경됨', type(mnist_train_transformed[0][0]), np.array(mnist_train_transformed[0][0]).shape)
      
        import math
      
        # Mnist Dataset을 DataLoader에 붙이기
        BATCH_SIZE = 64
        mnist_train_dataloader = torch.utils.data.DataLoader(mnist_train_transformed, batch_size=BATCH_SIZE, shuffle=True, num_workers=2)
        mnist_test_dataloader = torch.utils.data.DataLoader(mnist_test_transformed, batch_size=BATCH_SIZE, shuffle=False, num_workers=2)
      
        # mnist_resnet18 분류 모델을 학습하기
        ## 1. 분류 모델의 output 크기가 1000개로 되어 있음으로 mnist data class 개수로 나올 수 있도록 Fully Connected Layer를 변경하고 xavier uniform으로 weight 초기화
        # # print(mnist_train_dataloader.dataset.classes)
        # MNIST_CLASS_NUM = len(mnist_train_dataloader.dataset.classes)
        # mnist_resnet18.fc = torch.nn.Linear(in_features=512, out_features=MNIST_CLASS_NUM, bias=True)
        # print(mnist_resnet18)
        # torch.nn.init.xavier_uniform_(mnist_resnet18.fc.weight)
        # print(mnist_resnet18.fc.weight.size())
        # stdv = 1 / mnist_resnet18.fc.weight.size(1) ** 0.5 # fully connected layer의 bias를 resnet18.fc in_feature의 크기의 1/root(n) 크기의 uniform 분산 값 중 하나로 설정 - Why? https://stackoverflow.com/questions/49433936/how-to-initialize-weights-in-pytorch
        # mnist_resnet18.fc.bias.data.uniform_(-stdv, stdv)
      
        # ################ 레이어 변경 대신 레이어 추가해보기1 ################
        # # print(mnist_train_dataloader.dataset.classes)
        # MNIST_CLASS_NUM = len(mnist_train_dataloader.dataset.classes)
        # # mnist_resnet18.fc = torch.nn.Linear(in_features=512, out_features=MNIST_CLASS_NUM, bias=True)
        # mnist_resnet18 = torch.nn.Sequential(mnist_resnet18, torch.nn.Linear(in_features=1000, out_features=MNIST_CLASS_NUM, bias=True))
        # print(mnist_resnet18)
        # torch.nn.init.xavier_uniform_(mnist_resnet18[1].weight)
        # print(mnist_resnet18[1].weight)
        # stdv = 1 / mnist_resnet18[1].weight.size(1) ** 0.5 # fully connected layer의 bias를 resnet18.fc in_feature의 크기의 1/root(n) 크기의 uniform 분산 값 중 하나로 설정 - Why? https://stackoverflow.com/questions/49433936/how-to-initialize-weights-in-pytorch
        # mnist_resnet18[1].bias.data.uniform_(-stdv, stdv)
        # ################ 레이어 변경 대신 레이어 추가해보기1 ################
      
        ################ 레이어 변경 대신 레이어 추가해보기2 ################
        # print(mnist_train_dataloader.dataset.classes)
        MNIST_CLASS_NUM = len(mnist_train_dataloader.dataset.classes)
        # mnist_resnet18.fc = torch.nn.Linear(in_features=512, out_features=MNIST_CLASS_NUM, bias=True)
        mnist_resnet18.add_module('output_layer', torch.nn.Linear(512, MNIST_CLASS_NUM-3, bias=True))
        print(mnist_resnet18)
        torch.nn.init.xavier_uniform_(mnist_resnet18.output_layer.weight)
        print(mnist_resnet18.output_layer.weight)
        stdv = 1 / mnist_resnet18.output_layer.weight.size(1) ** 0.5 # fully connected layer의 bias를 resnet18.fc in_feature의 크기의 1/root(n) 크기의 uniform 분산 값 중 하나로 설정 - Why? https://stackoverflow.com/questions/49433936/how-to-initialize-weights-in-pytorch
        mnist_resnet18.output_layer.bias.data.uniform_(-stdv, stdv)
        ################ 레이어 변경 대신 레이어 추가해보기2 ################
      
        # """""""""""""""""""""""""""""""""""""""""
      
        # print("네트워크 필요 입력 채널 개수", mnist_resnet18.conv1.weight.shape[1])
        # print("네트워크 출력 채널 개수 (예측 class type 개수)", mnist_resnet18.fc.weight.shape[0])
        # print("네트워크 필요 입력 채널 개수", mnist_resnet18[0].conv1.weight.shape[1])
        # print("네트워크 출력 채널 개수 (예측 class type 개수)", mnist_resnet18[1].weight.shape[0])
        print("네트워크 필요 입력 채널 개수", mnist_resnet18.conv1.weight.shape[1])
        print("네트워크 출력 채널 개수 (예측 class type 개수)", mnist_resnet18.output_layer.weight.shape[0])
      
        from tqdm.notebook import tqdm # tqdm이라는 "반복문"의 현재 진행 상태를 progress-bar로 보여주는 라이브러리. 자세한 것은 (참고) : https://github.com/tqdm/tqdm
      
        ## 2. mnist train 데이터 셋을 resnet18 모델에 학습하기
      
        device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") # 학습 때 GPU 사용여부 결정. Colab에서는 "런타임"->"런타임 유형 변경"에서 "GPU"를 선택할 수 있음
      
        print(f"{device} is using!")
      
        mnist_resnet18.to(device) # Resnent 18 네트워크의 Tensor들을 GPU에 올릴지 Memory에 올릴지 결정함
      
        LEARNING_RATE = 0.0001 # 학습 때 사용하는 optimizer의 학습률 옵션 설정
        NUM_EPOCH = 5 # 학습 때 mnist train 데이터 셋을 얼마나 많이 학습할지 결정하는 옵션
      
        loss_fn = torch.nn.CrossEntropyLoss() # 분류 학습 때 많이 사용되는 Cross entropy loss를 objective function으로 사용 - https://en.wikipedia.org/wiki/Cross_entropy
        optimizer = torch.optim.Adam(mnist_resnet18.parameters(), lr=LEARNING_RATE) # weight 업데이트를 위한 optimizer를 Adam으로 사용함
      
        dataloaders = {
            "train" : mnist_train_dataloader,
            "test" : mnist_test_dataloader
        }
      
        ### 학습 코드 시작
        best_test_accuracy = 0.
        best_test_loss = 9999.
      
        for epoch in range(NUM_EPOCH):
          for phase in ["train", "test"]:
            running_loss = 0.
            running_acc = 0.
            if phase == "train":
              mnist_resnet18.train() # 네트워크 모델을 train 모드로 두어 gradient을 계산하고, 여러 sub module (배치 정규화, 드롭아웃 등)이 train mode로 작동할 수 있도록 함
            elif phase == "test":
              mnist_resnet18.eval() # 네트워크 모델을 eval 모드 두어 여러 sub module들이 eval mode로 작동할 수 있게 함
      
            with tqdm(dataloaders[phase], unit='iter') as pbar:
                for ind, (images, labels) in enumerate(pbar):
                    images = images.to(device)
                    labels = labels.to(device)
      
                    optimizer.zero_grad() # parameter gradient를 업데이트 전 초기화함
      
                    with torch.set_grad_enabled(phase == "train"): # train 모드일 시에는 gradient를 계산하고, 아닐 때는 gradient를 계산하지 않아 연산량 최소화
                        logits = mnist_resnet18(images)
                        _, preds = torch.max(logits, 1) # 모델에서 linear 값으로 나오는 예측 값 ([0.9,1.2, 3.2,0.1,-0.1,...])을 최대 output index를 찾아 예측 레이블([2])로 변경함  
                        loss = loss_fn(logits, labels)
      
                        if phase == "train":
                            loss.backward() # 모델의 예측 값과 실제 값의 CrossEntropy 차이를 통해 gradient 계산
                            optimizer.step() # 계산된 gradient를 가지고 모델 업데이트
      
                    running_loss += loss.item() * images.size(0) # 한 Batch에서의 loss 값 저장
                    running_acc += torch.sum(preds == labels.data) # 한 Batch에서의 Accuracy 값 저장
                    pbar.set_postfix(running_loss=loss.item() * images.size(0), running_accuracy=torch.sum(preds == labels.data).item() / len(labels) * 100.)
      
            # 한 epoch이 모두 종료되었을 때,
            epoch_loss = running_loss / len(dataloaders[phase].dataset)
            epoch_acc = running_acc / len(dataloaders[phase].dataset)
      
            print(f"현재 epoch-{epoch}의 {phase}-데이터 셋에서 평균 Loss : {epoch_loss:.3f}, 평균 Accuracy : {epoch_acc:.3f}")
            if phase == "test" and best_test_accuracy < epoch_acc: # phase가 test일 때, best accuracy 계산
              best_test_accuracy = epoch_acc
            if phase == "test" and best_test_loss > epoch_loss: # phase가 test일 때, best loss 계산
              best_test_loss = epoch_loss
        print("학습 종료!")
        print(f"최고 accuracy : {best_test_accuracy}, 최고 낮은 loss : {best_test_loss}")
  3. Target Task 모델 학습하기

    • Fashion-Mnist Dataset 불러오기

        # fashion-mnist train 데이터와 test 데이터 불러오기 : Fashion Mnist는 10가지의 옷 종류가 있는 데이터 셋
        # ref - https://github.com/zalandoresearch/fashion-mnist
        fashion_train = torchvision.datasets.FashionMNIST(root='./fashion', train=True, download=True)
        fashion_test = torchvision.datasets.FashionMNIST(root='./fashion', train=False, download=True)
    • Fashion Mnist를 학습할 Source Task 모델 가져오기

        print("네트워크 필요 입력 채널 개수", imagenet_resnet18.conv1.weight.shape[1])
        print("네트워크 출력 채널 개수 (예측 class type 개수)", imagenet_resnet18.fc.weight.shape[0])
      • 현재 Imagenet pretrained model 구조는 필요 입력 채널 개수가 3개이고, 예측하는 클래스 종류 개수는 1000가지임

      • 풀고자 하는 Fashion Mnist 데이터의 입력 크기는 Grayscale로 1채널 뿐이고, 레이블은 10가지 타입만 존재함

      • 이를 위해 Fine-Tuning을 하기 전에 모델 구조를 변경해야 함

          target_model = imagenet_resnet18
          FASHION_INPUT_NUM = 1
          FASHION_CLASS_NUM = 10
        
          # target model의 입력 크기와 출력 크기를 변경, 새로운 네트워크 가중치를 만들어서 기존 부분 중 일부를 변경
          target_model.conv1 = torch.nn.Conv2d(FASHION_INPUT_NUM, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
          target_model.fc = torch.nn.Linear(in_features=512, out_features=FASHION_CLASS_NUM, bias=True)
        
          # 새롭게 넣은 네트워크 가중치를 xavier uniform으로 초기화
          # (참고) 왜 xavier uniform으로 초기화해줄까? - 관련 논문(https://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf)
          torch.nn.init.xavier_uniform_(target_model.fc.weight)
          print(target_model.fc.weight.size())
          stdv = 1 / target_model.fc.weight.size(1) ** 0.5 # fully connected layer의 bias를 resnet18.fc in_feature의 크기의 1/root(n) 크기의 uniform 분산 값 중 하나로 설정 - Why? https://stackoverflow.com/questions/49433936/how-to-initialize-weights-in-pytorch
          target_model.fc.bias.data.uniform_(-stdv, stdv)
        
          print("네트워크 필요 입력 채널 개수", target_model.conv1.weight.shape[1])
          print("네트워크 출력 채널 개수 (예측 class type 개수)", target_model.fc.weight.shape[0])
    • Fashion-Mnist 데이터 분류 학습하기

        # torchvision.datasets.fasionmnist의 데이터 타입은 PIL Image이고, 학습 때는 torch로 type 변경이 필요함
        print('원본', type(fashion_train[0][0]))
      
        common_transform = torchvision.transforms.Compose(
          [
            torchvision.transforms.ToTensor() # PIL Image를 Tensor type로 변경함
          ]
        )
        # 앞서 선언한 데이터셋에 transform 인자를 넘겨주자
        fashion_train_transformed = torchvision.datasets.FashionMNIST(root='./fashion', train=True, download=True, transform=common_transform)
        fashion_test_transformed = torchvision.datasets.FashionMNIST(root='./fashion', train=False, download=True, transform=common_transform)
      
        print('변경됨', type(fashion_train_transformed[0][0]))
      
        # Mnist Dataset을 DataLoader에 붙이기
        BATCH_SIZE = 64
        fashion_train_dataloader = torch.utils.data.DataLoader(fashion_train_transformed, batch_size=BATCH_SIZE, shuffle=True, num_workers=2)
        fashion_test_dataloader = torch.utils.data.DataLoader(fashion_test_transformed, batch_size=BATCH_SIZE, shuffle=False, num_workers=2)
      
        from tqdm.notebook import tqdm # tqdm이라는 "반복문"의 현재 진행 상태를 progress-bar로 보여주는 라이브러리
      
        ## 2. mnist train 데이터 셋을 resnet18 모델에 학습하기
      
        device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") # 학습 때 GPU 사용여부 결정. Colab에서는 "런타임"->"런타임 유형 변경"에서 "GPU"를 선택할 수 있음
      
        print(f"{device} is using!")
      
        target_model.to(device) # Resnent 18 네트워크의 Tensor들을 GPU에 올릴지 Memory에 올릴지 결정함
      
        LEARNING_RATE = 0.0001 # 학습 때 사용하는 optimizer의 학습률 옵션 설정
        NUM_EPOCH = 5 # 학습 때 mnist train 데이터 셋을 얼마나 많이 학습할지 결정하는 옵션
      
        loss_fn = torch.nn.CrossEntropyLoss() # 분류 학습 때 많이 사용되는 Cross entropy loss를 objective function으로 사용 - https://en.wikipedia.org/wiki/Cross_entropy
        optimizer = torch.optim.Adam(target_model.parameters(), lr=LEARNING_RATE) # weight 업데이트를 위한 optimizer를 Adam으로 사용함
      
        dataloaders = {
            "train" : fashion_train_dataloader,
            "test" : fashion_test_dataloader
        }
      
        ### 학습 코드 시작
        best_test_accuracy = 0.
        best_test_loss = 9999.
        for epoch in range(NUM_EPOCH):
          for phase in ["train", "test"]:
            running_loss = 0.
            running_acc = 0.
            if phase == "train":
              target_model.train() # 네트워크 모델을 train 모드로 두어 gradient을 계산하고, 여러 sub module (배치 정규화, 드롭아웃 등)이 train mode로 작동할 수 있도록 함
            elif phase == "test":
              target_model.eval() # 네트워크 모델을 eval 모드 두어 여러 sub module들이 eval mode로 작동할 수 있게 함
      
            with tqdm(dataloaders[phase], unit='iter') as pbar:
                for ind, (images, labels) in enumerate(pbar):
                    pbar.set_description(f'Epoch {epoch}')
                    images = images.to(device)
                    labels = labels.to(device)
      
                    optimizer.zero_grad() # parameter gradient를 업데이트 전 초기화함
      
                    with torch.set_grad_enabled(phase == "train"): # train 모드일 시에는 gradient를 계산하고, 아닐 때는 gradient를 계산하지 않아 연산량 최소화
                        logits = target_model(images)
                        _, preds = torch.max(logits, 1) # 모델에서 linear 값으로 나오는 예측 값 ([0.9,1.2, 3.2,0.1,-0.1,...])을 최대 output index를 찾아 예측 레이블([2])로 변경함  
                        loss = loss_fn(logits, labels)
      
                        if phase == "train":
                            loss.backward() # 모델의 예측 값과 실제 값의 CrossEntropy 차이를 통해 gradient 계산
                            optimizer.step() # 계산된 gradient를 가지고 모델 업데이트
      
                running_loss += loss.item() * images.size(0) # 한 Batch에서의 loss 값 저장
                running_acc += torch.sum(preds == labels.data) # 한 Batch에서의 Accuracy 값 저장
                pbar.set_postfix(running_loss=loss.item() * images.size(0), running_accuracy=torch.sum(preds == labels.data).item() / len(labels) * 100.)
      
            # 한 epoch이 모두 종료되었을 때,
            epoch_loss = running_loss / len(dataloaders[phase].dataset)
            epoch_acc = running_acc / len(dataloaders[phase].dataset)
      
            print(f"현재 epoch-{epoch}의 {phase}-데이터 셋에서 평균 Loss : {epoch_loss:.3f}, 평균 Accuracy : {epoch_acc:.3f}")
            if phase == "test" and best_test_accuracy < epoch_acc: # phase가 test일 때, best accuracy 계산
              best_test_accuracy = epoch_acc
            if phase == "test" and best_test_loss > epoch_loss: # phase가 test일 때, best loss 계산
              best_test_loss = epoch_loss
        print("학습 종료!")
        print(f"최고 accuracy : {best_test_accuracy}, 최고 낮은 loss : {best_test_loss}")
      

Hyper Parameter Tuning

Ray Tune 사용하기

  • Ray는 Distributed application을 만들기 위한 프레임워크
  • 분산 컴퓨팅 환경에서 많이 사용되고 있음
  • Ray 프레임워크 안에 있는 Tune이라는 라이브러리를 통해 간단하게 학습 파라미터 튜닝을 하는 사용법을 확인해봄
  • 2가지 progress
    1. Tuning의 목적 정하기 (종속변인)
      • Hyper Parameter Tuning을 할 때에, 사용자가 정한 Objective가 존재해야 해당 값을 최대/최소화하는 값을 찾을 수 있음
      • 본 실습에서는 Fashion-Mnist의 Test 데이터셋의 Accuracy의 "최대화"를 목표로 함
      • 종속 변인인 Hyper Parameter에 대한 탐색을 수행하고자 할 때 탐색 범위를 지정하고 탐색하는 방법을 설정할 수 있음
    2. Tuning할 Hyper Parameter 정하기 (조작변인, 통제변인)
      • 딥러닝 학습을 할 때에도 저희가 조정하며 최적 값을 찾아볼 "조작변인", 그리고 값을 고정시킬 "통제변인"이 있음
        • "종속변인"은 1에서 정한 Objective가 됨
      • 이번 실습에서는 "조작변인"은 빠른 실험을 위해 Epoch과 BatchSize, Learning Rate로 정하고, "통제변인"은 모델 구조 ImageNet Pretrained Resnet18, All Not-Freeze Fine Tuning으로 설정
  • Ray Tune 사용하기 부분만 실행하면 에러가 발생하고 1.2.3 Mnist 데이터 분류 Resnet18 모델에 학습하기 에서 ## 2. mnist train 데이터 셋을 resnet18 모델에 학습하기 까지 셀을 실행한 후 Ray Tune 사용하기를 실행해야 정상적으로 작동하는 이유를 모르겠음…

출처: 부스트캠프 AI Tech 4기(NAVER Connect Foundation)