열심히 코딩 하숭!

11. 사전 학습 모델 이용하기 | 파이토치 딥러닝 프로그래밍 본문

프로그래밍 책/파이토치 딥러닝 프로그래밍

11. 사전 학습 모델 이용하기 | 파이토치 딥러닝 프로그래밍

채숭이 2024. 5. 16. 00:30

11 사전 학습 모델 활용하기

11.2 사전 학습 모델

사전 학습 모델

  • 실제 실무에서는 학습을 밑바닥부터 시작하는 경우는 오히려 드물다
  • 미리 학습이 되어있는 사전 학습 모델을 활용하여 정확도가 더욱 높은 모델을 만든다

파이토치에서 이미지 분류 모델로 사용 가능한 사전 학습 모델

  • 아래의 모델들은 모두 224 x 224 화소의 이미지 데이터로 학습되었음
  • 클래스명: 파이토치에서 실제 모델을 읽어올 때 사용하는 클래스명
  • Top-1 에러: ILSVRC에서 과제를 위해 공개한 ImageNet이라는 1000개의 클래스를 분류하는 문제에서 검증에 대한 에러 비율
  • Top-5에러: 정답이 모델의 예측 결과의 Top-5 안에 들지 못한 비율

너무 많아서 사진으로 대체

 

  • 사전 학습 모델 호출 예시
from torchvision import models

net = models.resnet18(pretrained = True)

 

11.3 파인 튜닝과 전이 학습

파인튜닝 (Fine tuning)

  • 사전 학습 모델의 파라미터를 초깃값으로 사용
  • 모든 레이어 함수를 사용해 학습하는 방법
  • 학습 데이터를 대량으로 확보하고 있을 때 유리함

전이 학습 (Transfer Learning)

  • 사전 학습 모델의 파라미터 중에 입력에 가까운 부분의 레이어 함수는 모두 고정
  • 출력에 가까운 부분만 학습하는 방법
  • 학습 데이터가 소량일 경우 유리함

 

11.4 적응형 평균 풀링 함수 (nn.AdaptiveAvgPool2d)

nn.AdaptiveAvgPool2d

  • 이미지 화소 수에 의존하지 않고 이미지를 입력받을 수 있도록 한다
  • Avg이므로 평균 값을 취함
  • 아래의 그림과 같이
    • 보통 풀링 함수) 파라미터로 ‘커널 사이즈’를 지정해줌
    • Adaptive 함수) 파라미터로 ‘변환 후의 화소 수’를 지정해줌

  • 모델 구현 코드
# nn.AdaptiveAvgPool2d 함수 정의
p = nn.AdaptiveAvgPool2d**((1,1))**
print(p)

# 선형 함수 정의
l1 = nn.Linear(32,10)
print(l1)

## 결과 ##
AdaptiveAvgPool2d(output_size=(1,1))
Linear(in_features=32, out_features=10, bias=True)

# 사전 학습 모델 시뮬레이션
inputs = torch.randn(100, 32, 16, 16)
m1 = p(inputs) # Adaptive 함수
m2 = m1.view(m1.shape[0], -1) # view 함수
m3 = l1(m2) # 선형 함수

# shape 확인
print(m1.shape)
print(m2.shape)
print(m3.shape)

## 결과 ##
torch.Size([100, 32, 1, 1])
torch.Size([100, 32])
torch.Size([100, 10])
  • [100,32,16,16]라는 4계 텐서가 AdaptiveAvgPool2d((1,1)) 함수로 인해 [100,32,1,1]로 변환됨
  • [100,32,1,1]라는 4계 텐서에 view(100, -1) 함수를 적용하면 (데이터 개수: 100 x 32개) [100, 32]라는 2계 텐서로 변환됨
  • AdaptiveAvgPool2d에 의해 inputs의 shape이 아래와 같더라도 동일한 선형 함수(Linear(32, 10))를 사용할 수 있다
    • [100, 32, 8, 8]
    • [100, 32, 4, 4]
  • 이를 통해 모델에서 첫번째 선형함수의 입력 차원수를 꼭 계산해야 하는 문제를 해결할 수 있다
  • 사전 학습 모델의 입력 화소 수에 상관없이 사용할 수 있게 된다

 

11.5 데이터 준비

Transforms 정의

  • 학습용과 테스트용의 Tranforms 정의가 다르다는 것 주의
  • Resize(112): 원본 이미지를 해당 사이즈로 확대 또는 축소 변환하는 기능
  • 32 x 32 사이즈인 CIFAR10에 대해 데이터를 확대하면 모델의 정확도가 높아진다는 사실 때문에 Resize를 구현한 것임
# 학습 데이터용
transform_train = transforms.Compose([
	**transforms.Resize(112),**
	transforms.RandomHorizontalFlip(p=0.5),
	transforms.ToTensor(),
	transforms.Normalize(0.5, 0.5),
	transforms.RandomErasing(p=0.5, scale=(0.02, 0.33), ratio=(0.3, 3.3), value=0, inplace=False)
])

# 검증 데이터용
transform = transforms.Compose([
	**transforms.Resize(112),**
	transforms.ToTensor(),
	transforms.Normalize(0.5, 0.5),
])

데이터로더 정의

  • batch_size = 50 : 이전보다 거대한 신경망이기 때문에, GPU 메모리가 부족해지는 문제 발생
  • 구글 코랩의 경우 배치 사이즈를 작게 설정하여 문제를 해결할 수 있다
# 배치사이즈 지정
batch_size = 50

# 훈련용 데이터로더
# 훈련용이기 때문에 셔플 = True
train_loader = DataLoader(train_set, batch_size=batch_size, shuffle=True)

# 검증용 데이터로더
# 검증용이기 때문에 셔플은 굳이 필요하지 않음 = False
test_loader = DataLoader(test_set, batch_size=batch_size, shuffle=False)

 

11.6 ResNet-18 불러오기

ResNet-18 불러오기

  • pretrained=True : 학습을 마친 파라미터도 동시에 불러오기
from torchvision import models

# 사전 학습 모델 불러오기
net = models.resnet18(pretrained=True) # pretrained=True: 학습을 마친 파라미터도 동시에 불러오기

모델 개요 1

print(net)

결과

ResNet(
  (conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
  (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
  (relu): ReLU(inplace=True)
  (maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
  (layer1): Sequential(
    (0): BasicBlock(
      (conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    )
    (1): BasicBlock(
      (conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    )
  )
  (layer2): Sequential(
    (0): BasicBlock(
      (conv1): Conv2d(64, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (downsample): Sequential(
        (0): Conv2d(64, 128, kernel_size=(1, 1), stride=(2, 2), bias=False)
        (1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (1): BasicBlock(
      (conv1): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    )
  )
  (layer3): Sequential(
    (0): BasicBlock(
      (conv1): Conv2d(128, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (downsample): Sequential(
        (0): Conv2d(128, 256, kernel_size=(1, 1), stride=(2, 2), bias=False)
        (1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (1): BasicBlock(
      (conv1): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    )
  )
  (layer4): Sequential(
    (0): BasicBlock(
      (conv1): Conv2d(256, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (downsample): Sequential(
        (0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)
        (1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (1): BasicBlock(
      (conv1): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    )
  )
  (avgpool): AdaptiveAvgPool2d(output_size=(1, 1))
  (fc): Linear(in_features=512, out_features=1000, bias=True)
)
  • avgpool / fc 함수가 정의되어 있는 것을 확인할 수 있다
📍 개요에 대한 해석
* layer 1, 2, 3, 4 … → Residual Block (조사 필요)
* (0) (1) (2) … → Sequential 컨테이너 내의 모듈의 위치

모델 개요 2 - summary

net = net.to(device)
summary(net, (100, 3, 112, 112))

결과

==========================================================================================
Layer (type:depth-idx)                   **Output Shape**              Param #
==========================================================================================
ResNet                                   [100, 1000]               --
├─Conv2d: 1-1                            [100, 64, 56, 56]         9,408
├─BatchNorm2d: 1-2                       [100, 64, 56, 56]         128
├─ReLU: 1-3                              [100, 64, 56, 56]         --
├─MaxPool2d: 1-4                         [100, 64, 28, 28]         --
├─Sequential: 1-5                        [100, 64, 28, 28]         --
│    └─BasicBlock: 2-1                   [100, 64, 28, 28]         --
│    │    └─Conv2d: 3-1                  [100, 64, 28, 28]         36,864
│    │    └─BatchNorm2d: 3-2             [100, 64, 28, 28]         128
│    │    └─ReLU: 3-3                    [100, 64, 28, 28]         --
│    │    └─Conv2d: 3-4                  [100, 64, 28, 28]         36,864
│    │    └─BatchNorm2d: 3-5             [100, 64, 28, 28]         128
│    │    └─ReLU: 3-6                    [100, 64, 28, 28]         --
│    └─BasicBlock: 2-2                   [100, 64, 28, 28]         --
│    │    └─Conv2d: 3-7                  [100, 64, 28, 28]         36,864
│    │    └─BatchNorm2d: 3-8             [100, 64, 28, 28]         128
│    │    └─ReLU: 3-9                    [100, 64, 28, 28]         --
│    │    └─Conv2d: 3-10                 [100, 64, 28, 28]         36,864
│    │    └─BatchNorm2d: 3-11            [100, 64, 28, 28]         128
│    │    └─ReLU: 3-12                   [100, 64, 28, 28]         --
├─Sequential: 1-6                        [100, 128, 14, 14]        --
│    └─BasicBlock: 2-3                   [100, 128, 14, 14]        --
│    │    └─Conv2d: 3-13                 [100, 128, 14, 14]        73,728
│    │    └─BatchNorm2d: 3-14            [100, 128, 14, 14]        256
│    │    └─ReLU: 3-15                   [100, 128, 14, 14]        --
│    │    └─Conv2d: 3-16                 [100, 128, 14, 14]        147,456
│    │    └─BatchNorm2d: 3-17            [100, 128, 14, 14]        256
│    │    └─Sequential: 3-18             [100, 128, 14, 14]        8,448
│    │    └─ReLU: 3-19                   [100, 128, 14, 14]        --
│    └─BasicBlock: 2-4                   [100, 128, 14, 14]        --
│    │    └─Conv2d: 3-20                 [100, 128, 14, 14]        147,456
│    │    └─BatchNorm2d: 3-21            [100, 128, 14, 14]        256
│    │    └─ReLU: 3-22                   [100, 128, 14, 14]        --
│    │    └─Conv2d: 3-23                 [100, 128, 14, 14]        147,456
│    │    └─BatchNorm2d: 3-24            [100, 128, 14, 14]        256
│    │    └─ReLU: 3-25                   [100, 128, 14, 14]        --
├─Sequential: 1-7                        [100, 256, 7, 7]          --
│    └─BasicBlock: 2-5                   [100, 256, 7, 7]          --
│    │    └─Conv2d: 3-26                 [100, 256, 7, 7]          294,912
│    │    └─BatchNorm2d: 3-27            [100, 256, 7, 7]          512
│    │    └─ReLU: 3-28                   [100, 256, 7, 7]          --
│    │    └─Conv2d: 3-29                 [100, 256, 7, 7]          589,824
│    │    └─BatchNorm2d: 3-30            [100, 256, 7, 7]          512
│    │    └─Sequential: 3-31             [100, 256, 7, 7]          33,280
│    │    └─ReLU: 3-32                   [100, 256, 7, 7]          --
│    └─BasicBlock: 2-6                   [100, 256, 7, 7]          --
│    │    └─Conv2d: 3-33                 [100, 256, 7, 7]          589,824
│    │    └─BatchNorm2d: 3-34            [100, 256, 7, 7]          512
│    │    └─ReLU: 3-35                   [100, 256, 7, 7]          --
│    │    └─Conv2d: 3-36                 [100, 256, 7, 7]          589,824
│    │    └─BatchNorm2d: 3-37            [100, 256, 7, 7]          512
│    │    └─ReLU: 3-38                   [100, 256, 7, 7]          --
├─Sequential: 1-8                        [100, 512, 4, 4]          --
│    └─BasicBlock: 2-7                   [100, 512, 4, 4]          --
│    │    └─Conv2d: 3-39                 [100, 512, 4, 4]          1,179,648
│    │    └─BatchNorm2d: 3-40            [100, 512, 4, 4]          1,024
│    │    └─ReLU: 3-41                   [100, 512, 4, 4]          --
│    │    └─Conv2d: 3-42                 [100, 512, 4, 4]          2,359,296
│    │    └─BatchNorm2d: 3-43            [100, 512, 4, 4]          1,024
│    │    └─Sequential: 3-44             [100, 512, 4, 4]          132,096
│    │    └─ReLU: 3-45                   [100, 512, 4, 4]          --
│    └─BasicBlock: 2-8                   [100, 512, 4, 4]          --
│    │    └─Conv2d: 3-46                 [100, 512, 4, 4]          2,359,296
│    │    └─BatchNorm2d: 3-47            [100, 512, 4, 4]          1,024
│    │    └─ReLU: 3-48                   [100, 512, 4, 4]          --
│    │    └─Conv2d: 3-49                 [100, 512, 4, 4]          2,359,296
│    │    └─BatchNorm2d: 3-50            [100, 512, 4, 4]          1,024
│    │    └─ReLU: 3-51                   [100, 512, 4, 4]          --
├─AdaptiveAvgPool2d: 1-9                 **[100, 512, 1, 1]**          --
├─Linear: 1-10                           **[100, 1000]**               513,000
==========================================================================================
Total params: 11,689,512
Trainable params: 11,689,512
Non-trainable params: 0
Total mult-adds (G): 48.54
==========================================================================================
Input size (MB): 15.05
Forward/backward pass size (MB): 1009.64
Params size (MB): 46.76
Estimated Total Size (MB): 1071.46
==========================================================================================
  • AdaptiveAvgPool2d 함수로 인해, [100, 512, 4, 4] 였던 shape이 [100, 512, 1, 1]로 변했다는 것을 알 수 있음

선형 함수 fc 확인

print(net.fc)
print(net.fc.in_features)
Linear(in_features=512, out_features=1000, bias=True)
512
  • 입력: 512 / 출력 1000

 

11.7 최종 레이어 함수 교체하기

최종 레이어 함수 교체

  • 파인 튜닝과 전이 학습 모두, 불러온 사전 학습 모델의 마지막 단을 목적에 맞는 선형 함수로 교체할 필요가 있음
  • 현재 사전 학습 모델의 마지막 단의 선형 함수 출력은 1000차원으로 설정돼 있음 → 10차원으로 교체
torch_seed() # 난수 고정

# 최종 레이어 함수의 입력 차원수 확인
fc_in_features = net.fc.in_features

# 최종 레이어 교체 함수
**net.fc = nn.Linear(fc_in_features, n_output)**
  • fc_in_features → 원래의 선형 함수 입력 차원수
  • n_output → 10으로 지정하고 대입
# 최종으로 작성된 모델을 사용해 손실 정의하고 그래프 시각화

criterion = nn.CrossEntropyLoss()
loss = eval_loss(test_loader, device, net, criterion)
g = make_dot(loss, params=dict(net.named_parameters())
display(g)
  • 의도한대로 클래스 수가 10으로 설정됨

11.8 학습과 결과 평가

초기 설정

# 난수 고정
torch_seed()

# 사전 학습 모델 불러오기
# pretraind = True로 **학습을 마친 파라미터도 함께 불러오기**
net = models.resnet18(pretrained = True)

# 최종 레이어 함수 입력 차원수 확인
fc_in_features = net.fc.in_features

# 최종 레이어 함수 교체
net.fc = nn.Linear(fc_in_features, n_output)

# GPU 사용
net = net.to(device)

# 학습률
lr = 0.001

# 손실 함수 정의
criterion = nn.CrossEntropyLoss()

# 최적화 함수 정의
optimizer = optim.SGD(net.parameters(), lr=lr, momentum=0.9)

# history 파일 초기화
history = np.zeros((0, 5))
  • 사전 학습 모델을 사용한다면, 각 레이어 함수의 파라미터는 어느정도 자리 잡은 상태임

⇒ 이런 조건에서는, Adam같이 복잡한 최적화 함수보다는 비교적 간단한 알고리즘을 사용하는 것이 좋다

  • 그래서 이번 코드에서는 momentum 옵션이 추가된 optim.SGD 함수를 최적화에 사용!

학습

  • 반복 횟수는 5회로 설정
# 학습
num_epochs = 5
history = fit(net, optimizer, criterion, num_epochs, 
			        train_loader, test_loader, device, history)

평가

# 결과 요약
evaluate_history(history)
  • 훈련 - 파란색 / 검증 - 검은색
  • 5회 반복만으로 94%의 정확도를 달성

11.9 VGG-19-BN 활용하기

위에 있는 코드랑 거의 비슷

모델 불러오기

from torchvision import models

net = models.vvg19_bn(pretrained=True)

모델 구조

... 생략 ...
  (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)
  )
)
  • 선형 함수는 총 3개
print(net.classifier[6])

 

  • 맨 마지막 선형 함수임

최종 레이어 함수 교체

# 난수 고정
torch_seed()

# 최종 레이어 함수 교체
in_features = net.classifier[6].in_features
**net.classifier[6] = nn.Linear(in_features, n_output)**

# features 마지막의 MaxPool2d 제거
net.features = net.features[:-1]

# AdaptiveAvgPool2d 제거
net.avgpool = nn.Identity()

=>
features 마지막의 MaxPool2d 제거: 
net.features = net.features[:-1]는 모델의 특성 추출 부분(features)에서 
***마지막 MaxPool2d 레이어를 제거합니다.*** 
이는 모델의 아키텍처를 조정하여 특성 맵의 크기를 조절하거나, 
다른 특성 처리 방식을 적용하고자 할 때 유용합니다.

AdaptiveAvgPool2d 제거: 
net.avgpool = nn.Identity()는 평균 풀링(AdaptiveAvgPool2d) 레이어를 
신원 함수(Identity)로 교체합니다. 
nn.Identity 레이어는 입력을 그대로 출력으로 전달합니다. 
***이 변경은 모델에서 평균 풀링 단계를 건너뛰고 싶을 때 사용됩니다.*** 
평균 풀링을 제거함으로써, 모델은 풀링에 의해 감소되기 전의 더 높은 차원의 
특성 맵을 유지할 수 있습니다.

 

학습 및 평가

num_epochs = 5
history = fit(net, optimizer, criterion, num_epochs,
          train_loader, test_loader, device, history)
          
# 결과 요약
evaluate_history(history)
  • 정확도 95.7%

# 이미지와 정답, 예측 결과를 함께 표시
show_images_labels(test_loader, classes, net, device)