dukim's blog

[WK03-Day013][21.08.19.Thu] PyTorch Project Template 뜯어보기, Transfer Learning & Hyper Parameter Search 본문

Boostcamp AI Tech 2th

[WK03-Day013][21.08.19.Thu] PyTorch Project Template 뜯어보기, Transfer Learning & Hyper Parameter Search

eliza.dukim 2021. 8. 20. 02:00

Intro

  • 오늘 피어세션 발표차례라 발표 내용 준비하면서 강의 때 소개되었던 프로젝트 템플릿을 CIFAR10 Tutorial code에 맞게 수정해보았다.
  • CV 분야의 transfer learning 방법 및 Hyperparameter Search 도구인 Ray Tune을 선택과제로 학습하였다.

강의 내용 복습

PyTorch 프로젝트 구조 이해

Tip: import 순서

  • python 내장 모듈 or 라이브러리 -> 오픈소스 -> 사용자정의 모듈 순으로

조각코드: random seed 고정

  SEED = 123
  torch.manual_seed(SEED)
  torch.backends.cudnn.deterministic = True
  torch.backends.cudnn.benchmark = False
  np.random.seed(SEED)

getattr의 사용

  • getattr을 지금까지 클래스 객체에 대해서 attribute 접근하는 함수로만 알고 있었다.

  • template 코드에서는 .py로 정의된 모듈 파일이 포함하고 있는 클래스나 함수를 선택할 때 사용 가능하다. 특히 factory pattern을 만들때 유용하게 사용된다. 예를 들어 아래와 같은 directory로 구성되어 있고

    pytorch-template/
    │
    ├── train.py - main script to start training
    │
    ├── model/ - models, losses, and metrics
    │   ├── model.py
    │   ├── metric.py
    │   └── loss.py
    │...

    model.loss.py가 다음과 같이 작성되어 있을 때

    import torch.nn.functional as F
    
    def nll_loss(output, target):
        return F.nll_loss(output, target)

    trian.py 내부에서 model.loss의 nll_loss 함수 객체에 접근해 변수에 할당하거나

    ...
    import model.loss as module_loss
    ...
    criterion = getattr(module_loss, 'nll_loss')

    다음처럼 특정 라이브러리의 모듈내에서 원하는 클래스에 접근해 원하는 인스턴스를 만드는데 사용된다.

    import torch
    def get_optimizer_by_lr(model, optm_name:str, lr:float):
        return getattr(torch.optim, optm_name)(model.parameters(), lr=lr)

pathlib: 경로관리를 편리하게하는 built-in module

  • 템플릿 코드를 보다보면 string type끼리 /로 나누는 이상한 코드가 보이는데, 이건 pathlib.Path 객체와 string type 객체 간의 연산으로, 경로 구분자(/)로 합쳐진 pathlib.Path 객체를 리턴하는 것.

    # template code
    # set save_dir where trained model and log will be saved.
    save_dir = Path(self.config['trainer']['save_dir'])
    
    exper_name = self.config['name']
    if run_id is None: # use timestamp as default run-id
        run_id = datetime.now().strftime(r'%m%d_%H%M%S')
    self._save_dir = save_dir / 'models' / exper_name / run_id
    self._log_dir = save_dir / 'log' / exper_name / run_id
    # Example usage of pathlib.Path
    from pathlib import Path
    base_dir = Path('data/')
    print(base_dir / 'exp01' / 'log')

collections.namedtuple

  • tuple 형태로 Data 구조체 저장
  • 저장되는 Data의 variable을 사전에 지정해 저장
    # template code
    ...
    from collections import namedtuple
    ...
    # 구조체의 이름: 'CustomArgs'
    CustomArgs = collections.namedtuple('CustomArgs', 'flags type target')
    custom_arg1 = CustomArgs(['--lr', '--learning_rate'], type=float, target='optimizer;args;lr')
    custom_arg2 = CustomArgs(['--bs', '--batch_size'], type=int, target='data_loader;args;batch_size')
    options = [custom_arg1, custom_arg2]

과제 수행 / 결과물

Transfer Learning & Hyperparameter Search(Ray Tune)

ImageNet Pretrained Modeld을 torchvision에서 불러오기(PyTorch Hub)

  import torchvision
  import torch
  import np 
  imagenet_resnet18 = torchvision.models.resnet18(pretrained=True)

불러온 모델을 현재 데이터에 맞게 수정하기

  • input 수정(1 channel gray scale -> 3 channels RGB)에는 2가지 선택지가 있다
    1. 모델의 input_layer를 수정하거나
    2. 입력 데이터를 transform하거나
# 0. check model input, output shape
print("# of input channels", mnist_resnet18.conv1.weight.shape[1])
print("# of labels", mnist_resnet18.fc.weight.shape[0])

# 1. 모델의 input_layer를 수정
mnist_input_num = 1
mnist_label_num = 10
mnist_resnet18.conv1 = torch.nn.Conv2d(mnist_input_num, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
mnist_resnet18.fc = torch.nn.Linear(in_features=512, out_features=mnist_label_num, bias=True)

# weight initialization
torch.nn.init.xavier_uniform_(target_model.conv1.weight)
torch.nn.init.xavier_uniform_(target_model.fc.weight)
stdv = 1/np.sqrt(FASHION_CLASS_NUM) 
target_model.fc.bias.data.uniform_(-stdv, stdv)

# 2. 입력 데이터를 Transform(1 channel grayscale -> 3 channel grayscale)
common_transform = torchvision.transforms.Compose(
  [
    torchvision.transforms.Grayscale(num_output_channels=3), # grayscale의 1채널 영상을 3채널로 동일한 값으로 확장함
    torchvision.transforms.ToTensor() # PIL Image를 Tensor type로 변경함
  ]
)

tqdm에서 progress bar에 정보 표시하기(ipython notebook)

  from tqdm.notebook import tqdm

  for epoch in range(n_epoch):
      with tqdm(dataloaders) as pbar:
          for ind, (data, labels) in enmerate(pbar):
              data = data.to(device)
              labels = labels.to(device)
              ...
              cur_loss = loss.item() / data.size(0)
              cur_acc = torch.sum(preds == labels.data) / data.size(0)
              pbar.set_description(f'Epoch : {epoch} / Step : {i} / Loss : {cur_loss:.4f} / Acc : {cur_acc:.4f}')

Ray Tune을 이용한 Hyper Parameter Tuning

코드 작성 전에 결정할 내용

  • 최대/최소화 해야할 대상이 되는 값(e.g. Accuracy)이 무엇인지
  • 어떤 hyper parameter(e.g. epoch, batch size)를 최적화할 것인지를 정하고 Search Space를 설정

Ray Tune 설치

  $pip uninstall -y -q pyarrow
  $pip install -q -U ray[tune]
  $pip install -q ray[debug]

training setup:

  • 매 실험을 돌릴 때마다 초기화해줘야하므로 함수에서 출력하는 형태로 작성

    ...
    import numpy as np
    import torch
    from torch.utils.data import Subset, DataLoader
    import torchvision as tv
    from sklearn.model_selection import train_test_split
    
    # helper function : Stratified Random Split
    def _balanced_val_split(dataset, val_split=0.2):
      targets = np.array(dataset.targets)
      train_idxs, val_idxs = train_test_split(
          np.arange(targets.shape[0]),
          test_size=val_split,
          stratify=targets
      )
      train_dataset = Subset(dataset, indices=train_idxs)
      val_dataset = Subset(dataset, indices=val_idxs)
      return train_dataset, val_dataset
    
    # set up function & datasets
    def get_pretrained_model(model_name:str):
      model = getattr(tv.models, model_name)(pretrained=True)
      FASHION_INPUT_NUM = 1
      FASHION_CLASS_NUM = 10
    
      model.conv1 = torch.nn.Conv2d(FASHION_INPUT_NUM, 64,
                                                kernel_size=(7, 7),
                                                stride=(2, 2), padding=(3, 3),
                                                bias=False)
      model.fc = torch.nn.Linear(in_features=512,
                                        out_features=FASHION_CLASS_NUM, bias=True)
    
      torch.nn.init.xavier_uniform_(model.conv1.weight)
      torch.nn.init.xavier_uniform_(model.fc.weight)
      stdv = 1/np.sqrt(FASHION_CLASS_NUM)
      model.fc.bias.data.uniform_(-stdv, stdv)
      return model
    
    def get_optimizer_by_lr(model, optm_name:str, lr:float):
      return getattr(torch.optim, optm_name)(model.parameters(), lr=lr)
    
    def get_epch_by_epch(epch:int):
      return epch
    
    basic_transform = tv.transform.Compose([tv.transforms.ToTensor()])
    train_transformed = tv.datasets.MNIST(root='./fashion', train=True, download=True, transform=basic_transform)
    train_transformed, val_transformed = _balanced_val_split(train_transformed)
    
    def get_dataloaders_by_bs(bs:int):
      batch_size= bs
      train_dataloader = DataLoader(train_transformed, batch_size=batch_size, shuffle=True, num_workers=2)
      val_dataloader = DataLoader(val_transformed, batch_size=batch_size, shuffle=False, num_workers=2)
      dataloaders = {
          "train" : train_dataloader,
          "val" : val_dataloader
      }
      return dataloaders

Search space 설정

  from ray import tune

  config_space = {
      "n_epoch" : tune.choice([7,8,9]), 
      "learning_rate" : tune.uniform(1e-5, 1e-4),
      "batch_size" : tune.choice([32,64,128]),
  }

training function 설정

  def training(config):
      # controlled variables: model architecture, non-freeze, loss function
      model = get_pretrained_model('resnet18') # fix architecture set up

      device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
      model.to(device)
      criterion = torch.nn.CrossEntropyLoss()

      # manipulated variables: n_epoch, learning_rate, batch_size
      n_epoch = config["n_epoch"]
      optimizer = get_optimizer_by_lr(model, config["learning_rate"])
      dataloaders = get_dataloaders_by_bs(config["batch_size"])

      # training
      best_test_accuracy = 0.
      best_test_loss = 0.
      for epoch in range(n_epoch):
          for phase in ["train", "test"]:
          running_loss = 0.
          running_acc = 0.
          if phase == "train":model.train()        
          elif phase == "test":model.eval()

          for ind, (images, labels) in enumerate(tqdm(dataloaders[phase])):
              images = images.to(device)
              labels = labels.to(device)

              optimizer.zero_grad()

              with torch.set_grad_enabled(phase == "train"):
              logits = model(images)
              _, preds = torch.max(logits, 1)
              loss = criterion(logits, labels)

              if phase == "train":
                  loss.backward()
                  optimizer.step()

              running_loss += loss.item() * images.size(0)
              running_acc += torch.sum(preds == labels.data)

          epoch_loss = running_loss / len(dataloaders[phase].dataset)
          epoch_acc = running_acc / len(dataloaders[phase].dataset)

          if phase == "test":
              if best_test_accuracy < epoch_acc:
                  best_test_accuracy = epoch_acc
              if best_test_loss < epoch_loss:
                  best_test_loss = epoch_loss

      tune.report(accuracy=best_test_accuracy.item(), loss=best_test_loss)

Search Algorithm 선택: HyperOptSearch 모듈

Hyper parameter search 실행

  • *Reporter: 진행 상황을 관리하는 클래스로,CLIReporter,JupyterNotebookReporter` 두 가지가 있다.

  • tune.run: hyper parameter search 실행하는 함수로, 분석 결과를 담은 객체를 리턴함

  • num_samples 파라미터의 의미: 각각의 hyper-parameter 조합을 몇 번 반복할 것인지(e.g. Gride Search를 할 경우 각 Grid 지점에서 num_samples에 지정된 값 만큼의 실험을 반복함. search space가 총 10가지 조합이고 num_samples=10 이면 10 * 10 = 100번의 학습을 실행함.

    from ray.tune import CLIReporter
    import ray
    
    n_trial = 2
    
    reporter = CLIReporter(
      parameter_columns=["n_epoch", "learning_rate", "batch_size"],
      metric_columns=["accuracy", "loss"])
    
    ray.shutdown() # ray 초기화
    
    analysis = tune.run(
      training,
      config=config_space,
      search_alg=optim,
      #verbose=1,
      progress_reporter=reporter,
      num_samples=n_trial,
      resources_per_trial={'gpu': 1}
    )
  • 참고: Ray 공식 문서 - Execution (tune.run, tune.Experiment)

피어세션

역할

  • 모더레이터: 준수
  • 발표자: 준수, 대웅
  • 회의록: 창한

발표

준수

  • Back of Tricks for image classification with convolutional neural networks(https://arxiv.org/abs/1812.01187)에서 소개되었던 기법들을 간략하게 소개
    1. Cosine Learning Rate Decay
    2. Label Smoothing
    3. Knowledge Distillation
    4. Mixup Training

대웅

  • 프로젝트를 위한 베이스라인 코드 및 실행 방법 제시
  • 강의에서 소개되었던 PyTorch project template에 PyTorch Image Classification Tutorial을 적용하며 일부 수정
  • 여기에 각자가 시도해볼 모델 및 실험 조건을 각자 시도해 볼 수 있을 것
  • TODO: transforms 관리 기능 추가, Weights & Biases 기능 추가, 실험 관리 및 Hyper parameter search에 대한 고민

내일 회의

  • 모더레이터: 대웅
  • 발표자: 창한, 한진
  • 회의록: 한진

마스터 클래스 내용 요약

  • ML/DL 엔지니어의 필요성이 증대되고 있다. 따라서 단순 ML/DL 코드 작성을 넘어야 한다
    • 자동화하고, 데이터와 연계, 실험 결과를 기반으로 설득, 시스템화
  • 좋은 엔지니어이자 좋은 기획자적인 요소들이 필요하다(아직 AI화 되지 않은 영역의 AI화, 데이터를 어떻게 먹일? 것인가))
  • ML/DL 과정의 주변에 있는 에코시스템에 대한 지식과 경험이 필요
    • Linux shell script
    • DB
    • Cloud
    • Deply 관련 툴(e.g. Docker)
    • 대용량 데이터 처리: Multi-node(Spark 코드를 읽고 이해할 수 있는 능력), Multi-Processing

멘토링 요약

  • 이번 P-stage에서는 기본에 충실하자.
  • 나만의 Template, BoilerPlate 를 만들자
  • 각 과정의 요소들(실험 결과 정리 및 기록, 문서화, 토의, 깃 & 깃헙사용, 커밋 메시지 습관 들이기 등)에 익숙해지고 습관을 들이자.

학습 회고

  • 학습 내용 정리 시간을 더 줄일 수 있는 방법 없을까
  • 오늘 최성철 교수님의 마스터 클래스는 CS지식 학습에 대한 가이드라인을 얻을 수 있었다.
  • 멘토님께서 강조하신 템플릿 코드의 중요성에 동의한다. 각 태스크별 상황별 바로 적용해 볼 수 있는 템플릿 코드를 만들어 둬야겠다.
  • Ray Tune에 대한 사용법을 그동안 어렴풋이 알고만 있었는데, 선택과제를 하면서 내용과 사용법이 정리 되었다.

밀린 학습 내용

  • 오늘은 없음
Comments