부스트캠프 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 등의 다양한 추가 라이브러리를 개발하여 제공하고 있음
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)
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}")
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
- Tuning의 목적 정하기 (종속변인)
- Hyper Parameter Tuning을 할 때에, 사용자가 정한 Objective가 존재해야 해당 값을 최대/최소화하는 값을 찾을 수 있음
- 본 실습에서는 Fashion-Mnist의 Test 데이터셋의 Accuracy의 "최대화"를 목표로 함
- 종속 변인인 Hyper Parameter에 대한 탐색을 수행하고자 할 때 탐색 범위를 지정하고 탐색하는 방법을 설정할 수 있음
- Tuning할 Hyper Parameter 정하기 (조작변인, 통제변인)
- 딥러닝 학습을 할 때에도 저희가 조정하며 최적 값을 찾아볼 "조작변인", 그리고 값을 고정시킬 "통제변인"이 있음
- "종속변인"은 1에서 정한 Objective가 됨
- 이번 실습에서는 "조작변인"은 빠른 실험을 위해 Epoch과 BatchSize, Learning Rate로 정하고, "통제변인"은 모델 구조 ImageNet Pretrained Resnet18, All Not-Freeze Fine Tuning으로 설정
- 딥러닝 학습을 할 때에도 저희가 조정하며 최적 값을 찾아볼 "조작변인", 그리고 값을 고정시킬 "통제변인"이 있음
- Tuning의 목적 정하기 (종속변인)
- Ray Tune 사용하기 부분만 실행하면 에러가 발생하고 1.2.3 Mnist 데이터 분류 Resnet18 모델에 학습하기 에서 ## 2. mnist train 데이터 셋을 resnet18 모델에 학습하기 까지 셀을 실행한 후 Ray Tune 사용하기를 실행해야 정상적으로 작동하는 이유를 모르겠음…
출처: 부스트캠프 AI Tech 4기(NAVER Connect Foundation)