본문 바로가기
AI 컴퓨터 비전프로젝트/[ML,DL]머신러닝,딥러닝

[DL] AlexNet을 활용한 <깔끔한 방 VS 지저분한 방> 분류하기

by 바다의 공간 2024. 8. 26.

[DL] 컴퓨터 비전 데이터셋 활용링크 (tistory.com)

를 이용해서 데이터셋을 활용할 수 있지만

 

AlexNet을 이용하면 연습해보고 공부하기 좋은 데이터셋을 이용해서 프로젝트를 해보려고 합니다.

 

각 train, test, validation 데이터들을 강사님이 주셨고, 파일을 나눠서 작업을 했습니다.

AlexNet자체로는 전이학습이 가능하니 다음으로 덧데어서 받은 데이터로 예측을 해보려고 합니다.


 

필요한 라이브러리 임포트

import os
import glob
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision.transforms as transforms
import torchvision.models as models
from torch.utils.data import DataLoader, Dataset
from torchvision.datasets import ImageFolder
from PIL import Image

데이터를 구글코랩에서 가져올 수 있도록 폴더를 만들어놓았으므로 데이터 경로를 저장해놓겠습니다.

#데이터 경로 저장
data_root = '/content/drive/MyDrive/경로2'

 

 

이미지 출력해주는 함수를 만들려고합니다.

#이미지 출력 함수
def display_images(image_paths, title, max_images=4):
    plt.figure(figsize=(12, 3))
    for i, image_path in enumerate(image_paths[:max_images]):
        img = plt.imread(image_path)
        plt.subplot(1, max_images, i+1)
        plt.imshow(img)
        plt.title(title)
        plt.axis('off')
    plt.show()

 

각 코드가 중첩 for문을 이용해서 상항 애매했었는데 지금제대로 정리를 하려고 합니다.

 

def display_images(image_paths, title, max_images=4):
이 줄은 display_images라는 함수를 정의합니다. 이 함수는 세 가지 인자를 받습니다:

  • image_paths: 출력할 이미지 파일들의 경로가 담긴 리스트입니다.
  • title: 출력할 이미지들의 제목(타이틀)입니다.
  • max_images: 한 번에 출력할 최대 이미지 개수를 지정하며, 기본값은 4입니다.

 

 

그런데 여기서 image_paths는 경로가 담긴 리스트라고 하는데 왜 위에서 지정한 data_root로 하지않을까?

라는 의문이 생겨서 알아보니

data_root는 보통 이미지 파일의 위치! 만알려주고

image_paths 는 이미지 경로를 리스트로 담고있습니다. 예를들어서 ["C:/images/img1.jpg", "C:/images/img2.jpg", ...]

이런식으로 파일 하나하나를 리스트로 담고있습니다.


 

for i, image_path in enumerate(image_paths[]):
이 줄은 루프를 시작합니다. enumerate 함수는 image_paths 리스트에서 이미지 경로를 하나씩 가져오며, 동시에 해당 이미지의 인덱스(i)도 함께 제공합니다.

 

image_paths[:max_images]는 리스트의 처음부터 max_images만큼의 이미지들만 가져오는 슬라이싱 연산입니다.

이 코드에서는 최대 max_images 개의 이미지까지만 출력합니다.


img = plt.imread(image_path):
이 줄은 지정된 이미지 경로(image_path)에서 이미지를 읽어옵니다. plt.imread 함수는 Matplotlib에서 이미지를 읽어오는 함수로, 이미지 데이터를 img 변수에 저장합니다.


plt.subplot(1, max_images, i+1)

이 줄은 도화지에서 현재 이미지를 출력할 위치를 설정합니다. plt.subplot은 여러 이미지나 그래프를 한 도화지에 나누어 표시할 수 있게 하는 함수입니다. 여기서 1은 한 줄에 표시할 이미지를 의미하고, max_images는 한 줄에 몇 개의 이미지를 나란히 배치할 것인지를 지정합니다. i+1은 현재 이미지가 몇 번째 위치에 놓일지를 나타냅니다. 예를 들어, i가 0이면 첫 번째 위치에 이미지가 배치됩니다.


 

다음은 카테고리를 정해줍니다.

#카테고리 정하기
categories = ['Train Clean', 'Train Messy', 'Val Clean', 'Val Messy', 'Test Clean','Test Messy']

 

각각 train, val, Test의 깨끗, 지저분한 으로 구분해서 총 6개의 카테고리로 만들었습니다.


 

카테고리 이미지 가져오기

#이미지 읽어오기
for category in categories:
  image_paths = glob.glob(f'{data_root}/{category.lower().replace(" ","/")}/*')
  # print(image_paths)
  display_images(image_paths, category)
  print(f'{category} 총 이미지 수 : {len(image_paths)}')

 

glob.glob은 디렉토리를 해와서 다 리스트화 시켜주는겁니다.

 

max_images=4 로 지정을했기때문에 4개까지만 나오게 되는겁니다.

Train : 96개

Val : 10

Test : 5 로 됩니다.


나중에 실험 결과 보고서 작성할때 이미지가 몇개인지 중요하기때문에 

bar그래프로 확인해보도록 하겠습니다.

 

#bar그래프로 각 몇개인지 알아보기
plt.figure(figsize=(10, 6))
plt.bar(categories, [len(glob.glob(f'{data_root}/{category.lower().replace(" ", "/")}/*')) for category in categories], color=['blue', 'orange', 'green', 'red'])
plt.title('Number of Images per Category')
plt.xlabel('Category')
plt.ylabel('Number of Images')
plt.xticks(rotation=45)
plt.show()

 

 

 


 

이제 데이터 전처리를 해볼겁니다!Compose를 이용해서 한꺼번에 변화를 주기로 하겠습니다.

변화 항목으로는 1. 사이즈 조정  2. 텐서  3.노멀라이즈(정규화) 입니다.

사이즈를 224를 넣은 이유는 알렉스넷은 무조근 224로 크기를 맞춰줘야하기때문입니다.

또한 파이토치 모델에 넣을때는 텐서형으로 넣어야 하기때문에 ToTensor()으로 변경해줍니다.

#compose()=한꺼번에 사이즈 변환
#모델에 넣을때는 무조건 텐선형으로 넣어야함
transform = transforms.Compose([
    transforms.Resize((224,224)),
    transforms.ToTensor(),
    transforms.Normalize((0.5, 0.5,0.5), (0.5, 0.5,0.5)),
])

 


ImeageFolder을 이용해서 경로안에있는 이미지들을 리스트로 변경해주도록 하겠습니다.

데이터셋(데이터를 모아놓은 것)을 만들도록 하겠습니다.  이 데이터들이 있어야 뭘 할 수 있으니까요

#ImageFolder는 리스트로 변경해주는 코드
train_dataset = ImageFolder(f'{data_root}/train', transform=transform)
val_dataset = ImageFolder(f'{data_root}/val', transform=transform)

 


데이터로더를 만들어주려고 합니다.

데이터로더를 만드는 이유에 대해서 다시한번 알고 가야할것같은데

데이터로더 라는말은 이미지 작업에서 데이터를 효율적이고 체계적으로 처리하기 위해서 사용합니다.

 

비유를 하자면,

데이터를 그 자체로 한 번에 처리하려고 하려면 커다란 피자를 한 입에 다 먹으려는것과 똑같습니다

불가능할 뿐 아니라 위험합니다.

 

그러니 우리는 데이터를 작은 조각(batch)로 나누어야하고 

그 나눈 부분에 대해서 한번에 하나씩 또는 몇 개씩 처리하면 됩니다.

이것이 데이터로더가 하는 역할 입니다.

# 데이터로더만들기
# 학습시켜주거나 검증할때 묶어주는 단위(=데이터로더)
# 배치사이즈 맞추는 이유? 알아보기

train_loader = DataLoader(train_dataset, batch_size=8, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=8, shuffle=False)

 


Alexnet

  • 2012년 이미지 인식 경진대회(ILSVRC, ImageNet Large Scale Visual Recognition Challenge)에서 큰 성과를 거두며 컴퓨터 비전 분야에 혁신을 가져온 CNN모델
  • 224* 224 * 3 크기의 컬러 이미지를 입력
  • 이미지에서 특징을 추출하는 역할인 5개의 컨벌루션 레이어를 사용
  • Max-Pooling레이어(커널에서 최대값 뽑기)를 통해 공간 크기를 줄이면서 가장 중요한 정보를 유지합니다.
  • ReLU 함수를 활성화 함수로 사용하여 학습 속도를 크게 향상시킵니다.
  • 드롭아웃 기법을 사용하여 오버피팅을 방지
  • 마지막 두 레이어는 4096개의 파라미터를 가지고 있으며, 마지막 레이어는 1천개의 클래스로 분류합니다.
  • Max Pooling = 나온 결과에서 해당 크기만큼 안에서 가장 큰 값을 뽑아서 넣는것
  • dense = fc layer

 

모델만들기

#pretrained=True (전이학습을 가지고 있기에 어느정도  신뢰성이 있음)
model = models.alexnet(pretrained=True)
model

 

알렉스 함수로 써서 PRETRAINED=TRUE를 하게되면 껍데기뿐만아니라 이미지로 학습된 전이학습을 가지고 있고 효과는 어느정도 검증이 되어있는걸 확인할 수 있습니다.

model을 확인하면 

이렇게 나오는데 3으로 들어갔다가 64로 나오는 이유는 224를 맞추기 위해서 이렇게 크기가 나오게 됩니다.

박스친 부분말고 

classifier은 우리가 수정을 하고, 이 부분만 이용해보도록 하겠습니다.

그래서 이 앞쪽은 학습을 하지 않게 얼리기로 하겠습니다.

for param in model.parameters():
  param.requires_grad = False

 

기억하기로는 앞쪽 학습을 하지않는건 frozen을 사용하지만 classifier부분만 수정하려면 classifier부분을 새로 정의하고 그 부분만 학습될 수 있도록 requiers_grad = True로 설정하면 됩니다.

그러면 코드가 고정되어서 학습되지않도록 할 수 있습니다. 

그렇게 하면 AlexNet의 가중치는 고정이 되고 새로운 classifier부분만 학습됩니다.

#알렉스 다운된 이름이랑 맞춰야함
model.classifier[6] = nn.Linear(4096, 2)

model.classifier[5].requires_grad = True
model.classifier[6].requires_grad = True

숫자로 된 부분을 써주고 이름을 맞춰줍니다.

4096을 받아서 2개로 내보낸다는 뜻입니다.

requies_grad = True는 그 부분만 학습이 된다는 뜻입니다.

loss_func = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.0001)

bce를 쓰려면 1개의 클래스로 내보내고 시그모이드를 함수를 써야하지만

저희는 4096개를 받아서 2개의 클래스로 내보내니 BCE를 사용하지 않습니다.(소프트맥스를 사용해야하기에)

그래서 크로스앤트로피를 사용합니다.

optimizer은 사용하고싶은거 쓰면되는데 강의에서는 adam을 사용했습니다.

 


#정확도 계산하는 함수
def calculate_accuracy(loader, model):
  model.eval()
  correct = 0
  total = 0
  with torch.no_grad():
    for data in loader: #8바퀴 돌게됨
        images, labels = data  #labels = 깔끔,지저분 방
        outputs = model(images)
        _, predicted = torch.max(outputs.data, 1) #1=열
        total += labels.size(0)
        correct +=(predicted == labels).sum().item()
  return 100 * correct / total

 

calculate_accuracy 라는 함수가 하는 일은 모델이 얼마나 잘 예측하는지 측정하는 정확도  계산 함수입니다.

 

model.eval() = 시험모드로 전환

- 이 부분에서는 시험모드(평가)모드로 변경해서 이 모델에서는 예측만 하고 배우지 못합니다.

 

correct=0, total=0 을 이용해서 

맞춘 문제와 전체 문제를 수는 상자를 준비합니다.

 

wtth torch.no_grad():는 이건 시험볼때 니가 계산하지말고 그냥 예측만 해! 라는 뜻입니다.

모델이 공부하면서 계산하는게 아니라 정말 말 그대로 알고있는걸로만 예측하게 만들어요.

 

for data in loader:

    images, labels = data

:이 부분을 이용해서는 loader에서이미지와 정답(이미지가 깔끔한지 지저분한지)를 가져옵니다.

모델은 이 이미지를 보고 예측해요.

 

outputs = model(images)

_, predicted = torch.max(outputs.data, 1)

모델이 이 이미지를 보고 깔끔한방 or 지저분한 방 이라고 예측합니다

predicted에는 모델이 예측한 결과가 담겨있습니다.

 

total += labels.size(0)

correct += (predicted == labels).sum().item()

total:전체 몇 문제를 봤는지 세요

correct : 모델이 맞춘 문제 수를 셉니다. 모델이 예측한것(predicted)와 실제 정답(labels)이 일치하는지 확인합니다.

 

 

return 100 * correct / total

모델이 맞춘 문제수를 전체 문제 수로 나누고 100을 곱해서 정확도를 계산합니다.


만든 값들을 저장할 변수를 만들어줍니다.

train_losses = []
val_losses = []
val_accuracies = []

 

에포크를  주고 5바퀴정도 학습을 돌릴겁니다.

num_epochs = 5
for epoch in range(num_epochs):
    model.train()
    running_loss = 0.0
    for i, data in enumerate(train_loader):
        inputs, labels = data
        optimizer.zero_grad()
        outputs = model(inputs)
        loss = loss_func(outputs, labels)
        loss.backward()
        optimizer.step()
        running_loss += loss.item()
    train_loss = running_loss / len(train_loader)
    train_losses.append(train_loss)
    val_loss = 0.0
    model.eval()
    with torch.no_grad():
        for inputs, labels in val_loader:
            outputs = model(inputs)
            loss = loss_func(outputs, labels)
            val_loss += loss.item()
    val_loss /= len(val_loader)
    val_losses.append(val_loss)
    val_accuracy = calculate_accuracy(val_loader, model)
    val_accuracies.append(val_accuracy)
    print(f'Epoch {epoch + 1}, Train Loss: {train_loss: .6f}, Val Loss: {val_loss: .6f}, Val Accuracy: {val_accuracy: .2f}%')

 

정확도는 100%가 나옵니다. 그럼 모든 값들을 다 맞춘다는~이야기죠.

 

 

 


 

보기좋게 그래프로 넘겨보겠습니다.

plt.figure(figsize=(15, 5))
# 학습 손실 그래프
plt.subplot(1, 3, 1)
plt.plot(train_losses, label='Train Loss')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('Training Loss Over Epochs')
plt.legend()
# 검증 정확도 그래프
plt.subplot(1, 3, 2)
plt.plot(val_losses, label='Validation Loss')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('Validation Loss Over Epochs')
plt.legend()
# 검증 정확도 그래프
plt.subplot(1, 3, 3)
plt.plot(val_accuracies, label='Validation Accuracy', color='orange')
plt.xlabel('Epoch')
plt.ylabel('Accuracy (%)')
plt.title('Validation Accuracy Over Epochs')
plt.legend()
plt.tight_layout()
plt.show()

 

 

그래프로 확인할 수 있는건 로스(loss)값 이 훅 떨어진것을 볼 수 있고 정확도는 한바퀴 돌리자마자 100%로 올라갔고 

학습을 잘 맞춘다는것을 확인할 수 있습니다.

이제 실제로 라벨링이 잘 되는지를 확인해야합니다.

 


실제로 검증하는 트렌스폼을 만드려고합니다.

def load_and_transform_image(image_path, transform):
    image = Image.open(image_path).convert('RGB')
    return transform(image).unsqueeze(0)

convert를 RGB로 불러옵니다. 이상태로 이미지 넘파이로 가져오게되면 컬러채널의 가로세로로 가져오게되니까 3,244,244 뭐 이런식으로 됩니다. 그런데 모델에 넣으려면 앞에 배치사이즈를 넣어야되고 즉 쉐이프 모양이 다르게되니까 

return.transform(image).unsqueeze(0)를 해주게됩니다. 

이 뜻은 맨 처음에다가 배치사이즈를 넣어준다는 뜻입니다.

class_folders = {
    'clean': f'{data_root}/test/clean',
    'messy': f'{data_root}/test/messy'
}

이렇게 넣어서 클래스 폴더를 지정해줍니다. 그 다음 시각화를 해줍니다.

plt.figure(figsize=(20, 8))
counter = 1

for class_name, folder_path in class_folders.items():
    image_paths = glob.glob(os.path.join(folder_path, "*"))
    selected_paths = image_paths[:5]
    
    for image_path in selected_paths:
        image = load_and_transform_image(image_path, transform)
        model.eval()
        with torch.no_grad():
            outputs = model(image)
            _, predicted = torch.max(outputs, 1)
        prediction = 'clean' if predicted.item() == 0 else 'messy'
        
        plt.subplot(2, 5, counter)
        plt.imshow(Image.open(image_path))
        plt.title(f'True: {class_name}, Pred: {prediction}')
        plt.axis('off')
        counter += 1
plt.tight_layout()
plt.show()