Joonas' Note

Joonas' Note

[딥러닝 일지] 다른 모델도 써보기 (Transfer Learning) 본문

AI/딥러닝

[딥러닝 일지] 다른 모델도 써보기 (Transfer Learning)

2022. 3. 13. 12:47 joonas

    이전 글 - [딥러닝 일지] 학습 조기 종료 (Early Stop)

     

    [딥러닝 일지] 학습 조기 종료 (Early Stop)

    이전 글 - [딥러닝 일지] 과적합 문제, 그리고 배치 전략 [딥러닝 일지] 과적합 문제, 그리고 배치 전략 (교차 검증) 이전 글 - [딥러닝 일지] 이진 분류를 위한 CNN 모델 작성 (개 vs 고양이) [딥러닝

    blog.joonas.io


    들어가기 전에

    이번에 하려던 것을 하기 위해 검색을 많이 해봤는데, 관용적으로 부르는 건지 실제 용어 정의가 있는 지는 모르겠다.

    전이 학습(Transfer Learning)이라고도 부르고, 파인 튜닝(Fine Tuning)이라고도 부르는 것 같은 데, 찾아보기로는 다음과 같은 미묘한 차이가 있다.

    • 전이 학습(Transfer Learning)은, 기존에 누가 작성하고 그 모델이 학습한 weight를 최대한 건드리지 않는다. 즉, features를 그대로 사용한다. (feature라는 것은 어차피 각 레이어들이 곡선이나 모양, 색깔 등으로 특징을 뽑아내겠다는 것이므로..)
    • 파인 튜닝(Fine Tuning)은, 전체 레이어를 내 데이터로 다시 학습해서 weight를 재조정하는 것이다. 그러면 새로운 기준들(features)을 정의할 것이고, 가지고 있는 데이터에 따라 결과도 완전히 달라질 것이다.

    위 내용대로라면 이번에 하려고 시도한 것은, "파인 튜닝(Fine Tuning)"이었다.
    VGGNet 모델의 구조를 사용해서, 지금 가지고 있는 데이터들로 학습시킨 다음에, 개와 고양이를 분류하도록 하려했기 때문이다.

    하지만, 이번 글은 "그런데 짜잔, 전이 학습(Transfer Learning)을 해봤습니다~"로 끝난다.

     

    VGG 모델 불러오기

    모델을 불러오는 건 굉장히 쉬운데, 아래와 같다.

    model = torchvision.models.vgg16(pretrained=True)

    저 "pretrained"를 False로 두고 새로 학습시킨 결과를 보고 싶었는데, 이해한 것이 맞다면 그것이 바로 파인 튜닝(Fine Tuning)이었던 것이다.

    pretrained=False

    pretrained=False 로 두고 학습을 시작하면, 학습이 전혀 안되는 것처럼 돌아간다.
    왜냐하면, 잘 학습된 VGG 모델과 같은 성능을 내려면, 그 "잘된 학습"에 사용한 만큼의 엄청 많은 데이터를 때려부어야 할 것이기 때문이다.. (연구실의 지옥 수련을 견뎌낸 모델이겠지..)

    어쩐지 pretrained=False로 두고 학습하는 사례는 구글링을 아무리 해도 보기 힘들었다.

    결과 미리보기

    pretrained=True 인 경우의 loss과 accuracy

    시작부터 압도적인 loss과 accuracy를 보여준다. 이것이 pre-trained.

     

    이번 글은 Version 35를 기준으로 한다.

     

    Dogs vs. Cats Classification

    Explore and run machine learning code with Kaggle Notebooks | Using data from multiple data sources

    www.kaggle.com

     

    불러온 VGG16 확인

    불러온 VGG16 모델을 출력해보면 아래와 같은 구조이다.

    VGG(
        (features): Sequential(
          (0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
          (2): ReLU(inplace=True)
          (3): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (4): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
          (5): ReLU(inplace=True)
          (6): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
          (7): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (8): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
          (9): ReLU(inplace=True)
          (10): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (11): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
          (12): ReLU(inplace=True)
          (13): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
          (14): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (15): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
          (16): ReLU(inplace=True)
          (17): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (18): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
          (19): ReLU(inplace=True)
          (20): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (21): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
          (22): ReLU(inplace=True)
          (23): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
          (24): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (25): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
          (26): ReLU(inplace=True)
          (27): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (28): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
          (29): ReLU(inplace=True)
          (30): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (31): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
          (32): ReLU(inplace=True)
          (33): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
          (34): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (35): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
          (36): ReLU(inplace=True)
          (37): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (38): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
          (39): ReLU(inplace=True)
          (40): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          (41): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
          (42): ReLU(inplace=True)
          (43): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
        )
        (avgpool): AdaptiveAvgPool2d(output_size=(7, 7))
        (classifier): Sequential(
          (0): Linear(in_features=25088, out_features=4096, bias=True)
          (1): ReLU(inplace=True)
          (2): Dropout(p=0.5, inplace=False)
          (3): Linear(in_features=4096, out_features=4096, bias=True)
          (4): ReLU(inplace=True)
          (5): Dropout(p=0.5, inplace=False)
          (6): Linear(in_features=4096, out_features=1000, bias=True)
        )
      )

    그림으로 그려보면 이렇다.

    VGG16 구조

    구조가 이렇기때문에, input으로 들어가는 이미지의 크기를 (224, 244)로 맞춰줘야한다. 그리고 RGB로 입력 채널은 3개.

    All pre-trained models expect input images normalized in the same way, i.e. mini-batches of 3-channel RGB images of shape (3 x H x W), where H and W are expected to be at least 224. The images have to be loaded in to a range of [0, 1] and then normalized using mean = [0.485, 0.456, 0.406] and std = [0.229, 0.224, 0.225]
    from vgg-nets | PyTorch

    그리고 pre-train에 사용한 이미지와 동일한 정규화(normalize) 작업을 해줘야한다. 평균은 [0.485, 0.456, 0.406], 분포는 [0.229, 0.224, 0.225]를 사용했다고 하니, 아래와 같이 이미지를 전처리해서 넘겨야한다.

    mean = torch.tensor([0.485, 0.456, 0.406], dtype=torch.float32)
    std = torch.tensor([0.229, 0.224, 0.225], dtype=torch.float32)
    normalize = transforms.Normalize(mean.tolist(), std.tolist())
    unnormalize = transforms.Normalize((-mean / std).tolist(), (1.0 / std).tolist())
    
    image_to_tensor = transforms.Compose([
        transforms.Resize(256),
        transforms.CenterCrop(224),
        transforms.ToTensor(),
        normalize
    ])
    
    # Data Loader와 연결
    train_set = torchvision.datasets.ImageFolder('/train', transform=image_to_tensor)

    (224, 224) 크기로 바로 줄여도 똑같을 것이다.

    여기까지만 하면 1000개의 클래스로 분류하는 VGG16 모델을 로드한 것이다. 이것을 개와 고양이 2개의 클래스만 분류하는 것으로 바꿔야한다.

    확인한 방법은 2가지가 있다.

     

    그 전에, 해야하는 작업이 있다.

    가중치가 바뀌지 않도록 설정

    model = torchvision.models.vgg16_bn(pretrained=True)
    
    # Freeze our feature parameters
    for param in model.features.parameters():
      param.requires_grad = False

    requires_grad를 False로 설정해서, 자동 미분이 되지 않도록 한다.

    A. 모델 레이어를 직접 변경

    classifier의 Fully Connected Layer를 직접 변경

    features 쪽은 업데이트되지 않도록 한 후에, 아래처럼 모델에 먼저 정의되어있던 classifier 부분을 덮어쓰는 것이다.

    # Override classifier layer
    model.classifier = nn.Sequential(
        nn.Linear(25088, 500),   # FC 1
        nn.ReLU(),
        nn.Dropout(0.3),
        nn.Linear(500, 2)        # FC 2
    )

    그리고 학습을 시작하면, FC 1과 FC 2의 weight과 bias만 업데이트되면서 학습할 것이다.

    B. VGGNet 뒤에 레이어 추가하기

    VGGNet 뒤에 classifier를 새로 추가

    my_classifier = nn.Sequential(
        nn.Linear(1000, 500),
        nn.ReLU(),
        nn.Dropout(0.3),
        nn.Linear(500, 2)
    )
    model = nn.Sequential(collections.OrderedDict([
        ('net', model),
        ('classifier', my_classifier)
    ]))

    이번에는 모델의 save/load를 위해서 레이어들을 이름으로 묶었다.

    기존의 모델 구조를 유지하면서, 뒤에 새로 정의한 classifier 레이어를 추가한 형태이다. 출력해보면 아래와 같다.

    Sequential(
      (net): VGG(
        (features): Sequential(
          (0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
          ...
          (43): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
        )
        (avgpool): AdaptiveAvgPool2d(output_size=(7, 7))
        (classifier): Sequential(
          (0): Linear(in_features=25088, out_features=4096, bias=True)
          ...
          (6): Linear(in_features=4096, out_features=1000, bias=True)
        )
      )
      (classifier): Sequential(
        (0): Linear(in_features=1000, out_features=500, bias=True)
        (1): ReLU()
        (2): Dropout(p=0.3, inplace=False)
        (3): Linear(in_features=500, out_features=2, bias=True)
      )
    )

    이대로 저장하고, load_state_dict로 읽으려한다면 구조가 다르므로 오류가 날 것이다.

    기존의 VGG16의 구조는 건들지 않았고, 가중치는 업데이트 되지 않았으므로, 추가했던 classifier 레이어의 구조만 맞춰주면 저장했던 것 그대로 읽을 수 있을 것이다.

    # Save 하기 위해 모델 불러오기
    model = torchvision.models.vgg16(pretrained=True)
    
    # VGG 네트워크로 저장한 것이 아니기 때문에, 같은 구조로 맞춘 후 읽어야한다.
    model = nn.Sequential(collections.OrderedDict([
        ('net', model),
        ('classifier', my_classifier)
    ]))
    # 읽을 수 있는 부분만 읽도록 strict=False 설정
    model.load_state_dict(torch.load(SAVE_MODEL_PATH), strict=False)

    학습한 것을 저장하고 다시 불러와서 검증 셋으로 정확도를 계산해 본 결과는,

    train accuracy: 97.91484 %
     test accuracy: 97.86287 %

     

    다음으로는, 다른 모델들도 써보면서 하나씩 이해해봐야겠다.

    Comments