Pytorch

[파이토치] Multi Label Classification with CNN

왕초보코딩러 2025. 1. 10. 17:45
728x90

https://github.com/stonegyoung/Pytorch_Study/blob/master/multi_label.ipynb

 

Pytorch_Study/multi_label.ipynb at master · stonegyoung/Pytorch_Study

Pytorch 공부. Contribute to stonegyoung/Pytorch_Study development by creating an account on GitHub.

github.com


멀티 레이블이란?

정답(라벨)이 여러 개 존재하며, 각 라벨에 대해 맞는 경우만 1로 표시하고, 아닌 경우 0으로 표시하는 리스트 형태   

 

ex) black dress가 정답인 경우

 

 

이를 위해 출력층에서 Sigmoid 활성화 함수를 통과하며, 결과가 각 클래스(라벨)만큼 나오고, 클래스 별로 독립적인 확률(0~1)을 가진다.

각 클래스의 확률값에 대해 특정 임계값(예: 0.5)을 기준으로 1 또는 0으로 예측한다.

 


Sigmoid와 Softmax의 차이

- sigmoid
멀티 레이블 분류에서 많이 씀. 각 클래스에 대한 독립적인(0~1 사이의) 확률값
- softmax
단일 레이블 분류에서 많이 씀. 전체의 값을 더하면 1(여러 값 중 한 값만 뽑을 때. 가장 높은 확률의 것 뽑을 때)

 

sigmoid

sigmoid = nn.Sigmoid()

# 카테고리 0일 확률이 0.2689
result = torch.tensor([-1.,-2., 10., 1.])
sigmoid(result), sum(sigmoid(result))

 

각 클래스마다 0~1 확률값을 가짐

softmax

softmax = nn.Softmax()

# 총 합이 1
result = torch.tensor([-1.,-2., 10., 1.])
softmax(result), sum(softmax(result))

각 클래스의 확률 값을 더하면 1

 

이진 분류에서의 Sigmoid
이진 분류는 두 클래스(예: 0과 1) 중 하나를 선택하는 문제

- 이진 분류에서 Sigmoid는 출력 노드가 한 개 
Sigmoid 함수는 단일 로짓 값을 확률로 변환하며,
이 값은 클래스 1에 속할 확률을 의미
클래스 0에 속할 확률 = (1 - 클래스 1에 속할 확률)

+ Softmax를 사용해서 출력층 2개로 이진 분류를 할 수는 있지만, Sigmoid가 더 간단하고 계산 효율적, 이진 분류에서는 두 클래스 확률의 합이 1임을 보장할 필요 없이, 하나의 확률 값만으로 충분히 문제를 해결할 수 있음(p와 1-p)
Sigmoid 함수에서 출력층 노드가 2개 이상이라면 이는 멀티 레이블 분류 문제로 간주

데이터셋

Kaggle Apparel image dataset 2

https://www.kaggle.com/datasets/airplane2230/apparel-image-dataset-2

 

Apparel image dataset 2

11385 apparel images for multi-label/class classification

www.kaggle.com

fashion_data 폴더 안 / clothes_dataset 폴더 안

 

 

데이터셋 csv 확인

 

필요 라이브러리 임포트

import pandas as pd
from PIL import Image

 

데이터프레임으로

train_df = pd.read_csv('fashion_data/train.csv', index_col=0)
test_df = pd.read_csv('fashion_data/test.csv', index_col=0)

 

데이터 확인

train_df.head()

image 컬럼: 이미지의 경로가 적혀 있음

나머지 컬럼: 정답 라벨(정답이면 1, 아니면 0)

 

train_df[0]을 확인하면, blue shorts가 정답 라벨이네요.

 

train_df[0]의 이미지 경로 확인

image 컬럼을 사용합니다.

f"fashion_data/{train_df['image'][0][2:]}"

 

train_df[0]의 이미지 출력

아까 이미지 경로를 사용합니다.

Image.open(f"fashion_data/{train_df['image'][0][2:]}")

blue shorts

 

라벨 리스트 확인

iloc를 사용하여 1번 컬럼부터 마지막 컬럼(0번인 image 컬럼 제외 모두)까지 가져옵니다

train_df.iloc[:, 1:]

 

 


필요 라이브러리 임포트

import torch
import torch.nn as nn
from torch.optim import Adam # 최적화함수
from torch.utils.data import Dataset, DataLoader # 데이터셋 만들기
from torchvision import transforms, models # 전처리, pretrained 모델 가져오기
import numpy as np
import matplotlib.pyplot as plt
import tqdm

 

csv를 이용해 custom dataset 만들기

2025.01.06 - [Pytorch] - [파이토치] Custom Dataset 만들기

 

 
class Fashion(Dataset):
  def __init__(self, df, transform=None): # 이미지 경로와 정답이 있는 df, 전처리 transfrom 받기
    self.transform = transform
    self.data_list = df['image'].values
    self.label_list = df.iloc[:, 1:].values

  def __len__(self):
    return len(self.data_list)

  def __getitem__(self, idx):
    img_path = self.data_list[idx]
    img = Image.open(f'fashion_data/{img_path[2:]}')
    label = self.label_list[idx]

    if self.transform is not None:
      img = self.transform(img)

    return img, label
 

 

 

전처리

2024.12.28 - [Pytorch] - [파이토치] 이미지 데이터 전처리/증강

 

[파이토치] 이미지 데이터 전처리/증강

데이터 전처리/증강torchvision의 transforms를 사용이미지 데이터 전처리데이터 다양성을 높이기 위해 증강 https://github.com/stonegyoung/Pytorch_Study/blob/master/%EB%8D%B0%EC%9D%B4%ED%84%B0%20%EC%A0%84%EC%B2%98%EB%A6%AC.ipy

dogfoot1.tistory.com

 

(224, 224) 크기로 변환, 텐서로 변환

data_transform = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.ToTensor(),
])

 

데이터셋 만들기

train_data = Fashion(train_df, transform=data_transform)
test_data = Fashion(test_df, transform=data_transform)

 

 

데이터를 이미지로 출력하는 함수

category = train_df.columns[1:] # 클래스 이름 리스트로

def imgshow(data, labels, transform=True): # transform이 True면 tensor 형태, 아니면 PIL 형태
  if transform:
    numpy_array = data.numpy() # tensor -> numpy
    img = numpy_array.transpose((1, 2, 0)) # (channel, h, w) -> (h, w, channel)
  else:
    img = np.array(data) # PIL -> numpy
  
  st = ''
  for i in range(11): # 클래스 이름 돌면서
    if labels[i] == 1: # 정답이면
      st += category[i] + ' ' # 추가
  plt.title(st)
  plt.axis('off')
  plt.imshow(img)

 

transform 했을 때(아까 train_data 사용)

data, label = train_data[1]

 

imgshow(data, label)

 


transform 안했을 때(transform = None)

train_no_transform = Fashion(train_df)

d, l = train_no_transform[0]
imgshow(d,l, transform=False)

원본 크기

 

데이터 로더 만들기

train_loader = DataLoader(train_data, batch_size=16, shuffle=True)
test_loader = DataLoader(train_data, batch_size=16)

 

데이터 로더 확인

앞에 배치 사이즈가 추가 되었습니다.

 

첫 번째 미니 배치의 첫 번째 데이터를 이미지로 확인해보겠습니다.

data[0].shape

imgshow(data[0], label[0], True)

 


pretrained 모델 feature extraction 하기

 

torchvision의 models를 사용하여 pretrained 모델을 가져오겠습니다.

ResNet34 사용하겠습니다.

model = models.resnet34(pretrained=True)

 

model

 

저는 분류기쪽만 재학습하고, 나머지는 프리징하는 Feature Extraction을 사용할 것입니다.

 

프리징

- 모델이 가진 파라미터 뽑기 -> model.parameters()
- 모델 이름이랑 파라미터 뽑기 -> model.named_parameters()

 

프리징 상태 보기: requires_grad

 

for param in model.parameters():
  print(param.requires_grad)

현재는 모두 가중치가 업데이트 되도록 설정되어 있습니다.

저는 모든 layer를 False로 바꿔주겠습니다.

# 프리징
for parm in model.parameters():
  parm.requires_grad = False

 

Feature Extraction

분류기 레이어인 model.fc를 내 데이터에 맞게(출력층이 11개), 가중치가 업데이트 되도록 바꿔주겠습니다.

 

현재 모델의 분류기 상태

출력 노드 : 1000개

model.fc

 

내 데이터에 맞게 분류기 아키텍처 생성

출력 노드 : 11개

# 내 데이터에 맞게
fc_layer = nn.Linear(512, 11)

 

model.fc를 내가 만든 레이어로 바꾸기

model.fc = fc_layer

 

확인

model

 

requires_grad를 확인해보겠습니다.

# fc layer만 True(Feature extraction)
for name, param in model.named_parameters():
  print(name, param.requires_grad)

 


모델 학습

 

gpu 확인

device = 'cuda' if torch.cuda.is_available() else 'cpu'
device

 

gpu를 쓴다면 model을 gpu로 옮기기

model.to(device)

모델이 잘 되는 지 확인

(모델이 gpu에 있기 때문에 데이터로 gpu로 보내야 합니다)

data = torch.rand((16, 3, 224, 224))
data.to(device)

model(data).shape

이렇게 나온다면 완료~

 

 

 

손실함수

이진분류 손실함수

- torch.nn.BCELoss()  
sigmoid 적용이 안돼서 출력 값에 직접 torch.sigmoid() / nn.Sigmoid()()로 sigmoid 적용해야 함
- torch.nn.BCEWithLogitsLoss()  
내부적으로 sigmoid 연산 포함

 

 

y = torch.tensor([1., 2., -3])
target = torch.tensor([0.0, 1.0, 1.0])

bceloss = nn.BCELoss()
bcewithlogitloss = nn.BCEWithLogitsLoss()

print(bceloss(torch.sigmoid(y), target)) # sigmoid로 0~1 범위로 바꿔야 함
print(bcewithlogitloss(y, target))

 

 

1. BCELoss로 모델 학습

# 최적화함수, 손실함수, 에폭 정의
optimizer = Adam(model.parameters(), lr=0.0002)

criterion = nn.BCELoss() # 클래스 각각에 대한 확률값

epochs = 10

 

model.train()
loss_list = []
min_loss = int(1e9)

for epoch in range(epochs):
  epoch_loss = 0
  total_samples = 0

  for images, labels in tqdm.tqdm(train_loader):
    optimizer.zero_grad()

    y_pred = model(images.to(device))
    output = torch.sigmoid(y_pred).float() # sigmoid 적용

    loss = criterion(output, labels.float().to(device))

    # 마지막 배치 사이즈가 다를 때
    epoch_loss += loss.item() * images.size(0) # 배치에 대한 평균 손실 * 배치 크기
    total_samples += images.size(0) # 배치 크기 누적

    loss.backward()
    optimizer.step()

  avg_loss = epoch_loss/total_samples # 한 에폭에 대한 평균 손실
  loss_list.append(avg_loss)
  print(f'epoch: {epoch}, loss: {avg_loss}')

  if avg_loss < min_loss:
    torch.save(model.state_dict(), f'multi_fashion_epoch_{epochs}.pth')
    min_loss = avg_loss

 

2. BCEWithLogitLoss로 모델 학습

# 최적화함수, 손실함수, 에폭 정의
optimizer = Adam(model.parameters(), lr=0.0002)

criterion = nn.BCEWithLogitsLoss() # 클래스 각각에 대한 확률값

epochs = 10
model.train()
loss_list = []
min_loss = int(1e9)

for epoch in range(epochs):
  epoch_loss = 0
  total_samples = 0

  for images, labels in tqdm.tqdm(train_loader):
    optimizer.zero_grad()

    y_pred = model(images.to(device))
    output = torch.sigmoid(y_pred).float()

    loss = criterion(output, labels.float().to(device))

    # 마지막 배치 사이즈가 다를 때
    epoch_loss += loss.item() * images.size(0) # 배치에 대한 평균 손실 * 배치 크기
    total_samples += images.size(0) # 배치 크기 누적

    loss.backward()
    optimizer.step()

  avg_loss = epoch_loss/total_samples # 한 에폭에 대한 평균 손실
  loss_list.append(avg_loss)
  print(f'epoch: {epoch}, loss: {avg_loss}')

  if avg_loss < min_loss:
    torch.save(model.state_dict(), f'multi_fashion_epoch_{epochs}.pth')
    min_loss = avg_loss

 

loss_list에 넣은 에폭마다의 loss값 시각화

plt.plot(range(epochs),loss_list)
plt.show()

 


모델 평가

저는 BCELoss()로 한 모델을 평가해보겠습니다.

 

절차

datas, targets = next(iter(test_loader)) # 배치 하나 (16,3,224,224), (16,11)
res = model(datas.to(device)) # 배치를 모델에 넣었을 때 예측값 (16,11)
result = nn.Sigmoid()(res) # 출력값에 sigmoid 적용

# True와 False로 이루어진 리스트
y_pred = result > 0.5 # 임계값 0.5보다 크면 True, 작으면 False 
y = targets.to(device) == 1 # 1이면 True 아니면 False

correct = 0 # 맞춘 개수
for ouput, target in zip(y_pred, y): # 배치사이즈 16을 하나씩 돌면서
  # 다 같으면 True -> 정답
  if torch.equal(ouput, target):
    correct += 1

print(correct) # 16개 중 맞춘 개수

 

모델 평가

모델 평가 모드로 전환, 기울기 계산 방지

model.eval()
with torch.no_grad():
    correct = 0
    total = 0
    for img,label in test_loader:
        pred = model(img.to(device))
        # 임계값(0.5) 이상인 것만
        result = nn.Sigmoid()(pred) > 0.5 # True, False 식으로

        targets = label.to(device) == 1 # True, False 식으로

        for output,target in zip(result,targets):
            if torch.equal(output,target): # 멀티 레이블이 다 일치하면 정답
                correct += 1

            total += 1 # 전체 개수

    print(f'acc {correct/total}')