카테고리 없음

ResNet50를 사용하여 이미지 분류

큐트아리 2024. 3. 26. 19:24

우선, 케글에서 데이터셋을 받아옵니다.

에일리언 vs 프레데터 데이터셋

 

케글 데이터를 바로 받아오는 법

  • 케글 로그인 _> 우측 상단 계정 클릭 -> Your Profile -> API New Create Token -> kaggle.json다운
  • {"username":"본인의 아이디","key":"본인api키"}
  • 원하는 데이터의 COPY API COMMAND 한걸 !달아서 실행
import os
os.environ['KAGGLE_USERNAME'] = '아이디'
os.environ['KAGGLE_KEY'] = 'API키'
!kaggle datasets download -d pmigdal/alien-vs-predator-images

 

이렇게 되면 zip파일로 받아와지는데 zip파일을 압축 해제하는 것도 코드로 가능합니다.

!unzip -q alien-vs-predator-images.zip

 

이제 모델을 돌리기 위해 데이터 정제를 하겠습니다.

import torch
import torch.nn as nn
import torch.optim as optim
import matplotlib.pyplot as plt
from torchvision import datasets, models, transforms
from torch.utils.data import DataLoader
device = 'cuda' if torch.cuda.is_available() else 'cpu'
print(device)

이미지 증강 기법(Image Augmentation)

  • 원본 이미지 데이터를 조작하려 원본과는 다른 변화를 가진 이미지를 생성
  • 일반적으로 모델 성능이 좋아짐
  • 오버피팅을 방지

https://pytorch.org/vision/master/transforms.html

이때, train의 데이터만 증강을 거치고 test는 증강을 하지 않음으로 이 둘을 나눠서 작성해줍니다

data_transforms = {
    # Compose : 데이터에 처리하고자하는 함수들을 한번에 처리해주는 기능
    'train' : transforms.Compose([
        transforms.Resize((224,224)),
        # RandomAffine() : 데이터를 랜덤하게 뽑아서 변환시켜주겠다는 의미
        # 각도 : 0(변환x), shear(찌그러트림) , acale : 변환된 이미지의 크기는 원본 이미지 크기의 80%에서 120% 사이에서 무작위로 선택
        transforms.RandomAffine(0, shear =10, scale=(0.8,1.2)),
        # RandomHorizontalFlip 이미지를 수평으로 뒤집기
        transforms.RandomHorizontalFlip(),
        transforms.ToTensor()
    ]),
    'validation' : transforms.Compose([
        transforms.Resize((224,224)),
        transforms.ToTensor()
    ])
}

 

보통 분류문제에서는 LongTensor를 사용하나 이진분류의 경우 손실함수를 Binary Cross Entropy Loss로 사용합니다. 때문에, LongTensor로 Y값을 정할 경우 에러가 나게 됨으로 이진분류시 target은 FloatTensor로 설정하면 됩니다

def target_transforms(target):
    return torch.FloatTensor([target])

 

이터셋 객체 만들어서 가져와줌
# 이렇게 할 경우, 폴더에서 파일을 불러오면서 transform을 통해 증강 및 resize와 같은 전처리를 할 수 있음
image_datasets = {
    'train' : datasets.ImageFolder(
        root='data/train',  # 데이터셋의 루트 디렉토리
        transform = data_transforms['train'],  # 이미지에 적용할 변환
        target_transform=target_transforms  # 레이블에 적용할 변환
    ),
    'validation' : datasets.ImageFolder(
        'data/validation', # 해당 위치의 파일에서
        transform = data_transforms['validation'], # data_transforms의 train을 가져와서
        target_transform = target_transforms # 이 함수 써줌(콜백함수 개념)
    )
}
# 데이터 불러오는 것이 끝났으니 DataLoader로 데이터를 배치별로 묶어줍니다
dataloaders = {
    'train' : DataLoader(
        image_datasets['train'],
        batch_size=32,
        shuffle=True
    ),
    'validation': DataLoader(
        image_datasets['validation'],
        batch_size=32,
        shuffle=False
    )
}

print(len(image_datasets['train']),len(image_datasets['validation']))
# 694 200
imgs, labels = next(iter(dataloaders['train']))

fig, axes = plt.subplots(4,8,figsize=(16,8))

for ax, img, label in zip(axes.flatten(), imgs, labels):
    # 찍으려면 가로, 세로, 컬러 순서여야해서 permute 해줌
    ax.imshow(img.permute(1,2,0))
    ax.set_title(label.item())
    ax.axis('off')

사실 전 영화를 잘 안봐서 predator가 뭔지 잘 모릅니다. 위의 사진을 보고도 구분하기 어렵네요. 하지만 데이터가 있으니 학습을 시켜 저 대신에 모델이 구분할 수 있도록 만들어보겠습니다.

ResNet50 불러오기

model = models.resnet50(weights='IMAGENET1K_V1').to(device)
print(model)
for param in model.parameters():
    param.requires_grad = False # 가져온 파라미터 (W, b)를 업데이트하지 않음

model.fc = nn.Sequential(
    nn.Linear(2048,128),
    nn.ReLU(),
    nn.Linear(128,1), # 이진분류니까 1
    nn.Sigmoid()
).to(device)

print(model)

이렇게 모델을 프린트하면 엄청나게 복잡하고 층층이 쌓인 모델 구조를 볼 수 있는데요. 이 모델은 다른 데이터로 이미 학습된 전이학습 모델이기에 그냥 써서는 우리가 원하는 분류를 할 수 없습니다.

모델 구조 맨 아래를 보시면 fc라고 보이지죠 거기가 저희가 입맛대로, 데이터대로 고쳐야하는 곳입니다.

지금은 2048이 들어와 1000개의 아웃풋으로 나오는데 2048은 모델의 피처맵수이고 1000은 이 모델이 최종적으로 1000개의 데이터를 구분할 수 있다는 뜻입니다.

하지만 우리는 이제 외계인과 수호자를 구분하니 저 곳의 아웃풋을 1로 만들어줘야 합니다.

2048개의 특징을 가지고 바로 이진분류를 하면 오류가 많이 생길 수 있고 성능이 잘 안나올 수 있으니 한줄한줄 줄여가면서 만들어보겠습니다.

모델 수정

5. Freeze Layers

  • 특징을 Qhqdksosms CNN의 앞쪽 컨볼루션 레이어들은 학습을하지 않도록 설정
  • 출력부분의 레이어(fc)를 다시 설정하며 분류에 맞게 변경
for param in model.parameters():
    param.requires_grad = False # 가져온 파라미터 (W, b)를 업데이트하지 않음

model.fc = nn.Sequential(
    nn.Linear(2048,128),
    nn.ReLU(),
    nn.Linear(128,1), # 이진분류니까 1
    # 이진분류니까 Sigmoid 사용 사실 여기가 아니라 모델 밖에서 사용해도됨
    nn.Sigmoid()
).to(device)

print(model)

이제 모델이 저희 데이터에 맞는 출력값을 뱉어낼 수 있게 되었습니다.

# 학습
optimizer = optim.Adam(model.fc.parameters(), lr=0.001)
epochs = 10
for epoch in range(epochs):
    # 이렇게 되면 학습이 끝날때 모델이 자동으로 model.eval()가 된 상태로 나옴
    for phase in ['train', 'validation']:
        if phase == 'train':
            model.train()
        else:
            model.eval()
        sum_losses = 0
        sum_accs = 0
        for x_batch, y_batch in dataloaders[phase]:
            x_batch = x_batch.to(device)
            y_batch = y_batch.to(device)
            y_pred = model(x_batch)
            loss = nn.BCELoss()(y_pred, y_batch)
            if phase == 'train':
                optimizer.zero_grad()
                loss.backward()
                optimizer.step()
            sum_losses = sum_losses + loss
            y_bool = (y_pred >= 0.5).float()
            acc = (y_batch == y_bool).float().sum() / len(y_batch) * 100
            sum_accs = sum_accs + acc
        avg_loss = sum_losses / len(dataloaders[phase])
        avg_acc = sum_accs / len(dataloaders[phase])
        print(f'{phase:10s}: Epoch {epoch+1:4d}/{epochs} Loss: {avg_loss:.4f} Accuracy: {avg_acc: .2f}%')

 

※참고 : 이미지데이터 열기

from PIL import Image
 
img1 = Image.open('./data/validation/alien/19.jpg')
img2 = Image.open('./data/validation/predator/20.jpg')

fig, axes = plt.subplots(1,2,figsize=(12,6))
axes[0].imshow(img1)
axes[0].axis('off')
axes[1].imshow(img2)
axes[1].axis('off')
plt.show()

# data_transforms는 내가 위에서 설정한 애로 사이즈 224,224로 재설정하고 텐서형태로 변환되어있음
# 그럼으로 새로 불러온 img1과 img2 둘다 같은 전처리를 거쳐야함
# data_transforms에는 Compose클래스가 가진 __call__이라는 스페셜 메서드가 있음으로 함수로 정하지 않았지만 ()를 붙여서 이미지를 넣으면 함수처럼 이미지가 data_transforms안에 설정된 것들이 적용됨

img1_input = data_transforms['validation'](img1)
img2_input = data_transforms['validation'](img2)

print(img1_input.shape,img2_input.shape)
# torch.Size([3, 224, 224]) torch.Size([3, 224, 224])
# 배치로 묶어서 모델에 연결
# 2,3,224,224 차원으로 img1_input와 img2_input를 쌓음
# stack : 차원을 쌓아주는 함수
test_batch = torch.stack([img1_input,img2_input])
test_batch = test_batch.to(device)
test_batch.shape
torch.Size([2, 3, 224, 224])
y_pred = model(test_batch)
y_pred
tensor([[0.1115],
        [0.9516]], device='cuda:0', grad_fn=<SigmoidBackward0>)
fig, axes = plt.subplots(1, 2, figsize=(12, 6))
axes[0].set_title(f'{(1-y_pred[0, 0])*100:.2f}% Alien, {(y_pred[0, 0])*100:.2f}% Predator')
axes[0].imshow(img1)
axes[0].axis('off')
axes[1].set_title(f'{(1-y_pred[1, 0])*100:.2f}% Alien, {(y_pred[1, 0])*100:.2f}% Predator')
axes[1].imshow(img2)
axes[1].axis('off')
plt.show()