https://github.com/stonegyoung/Pytorch_Study/blob/master/multi_label.ipynb
멀티 레이블이란?
정답(라벨)이 여러 개 존재하며, 각 라벨에 대해 맞는 경우만 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))
softmax
softmax = nn.Softmax()
# 총 합이 1
result = torch.tensor([-1.,-2., 10., 1.])
softmax(result), sum(softmax(result))
이진 분류에서의 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
데이터셋 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:]}")
라벨 리스트 확인
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 만들기
전처리
2024.12.28 - [Pytorch] - [파이토치] 이미지 데이터 전처리/증강
(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}')
'Pytorch' 카테고리의 다른 글
[파이토치] Custom Dataset 만들기 (0) | 2025.01.08 |
---|---|
[파이토치] CNN 모델 학습, 평가, 추론 (0) | 2025.01.06 |
[파이토치] CNN Lenet5 구현 (1) | 2025.01.04 |
[파이토치] MNIST로 이미지 데이터 처리 (1) | 2024.12.31 |
[파이토치] 이미지 데이터 전처리/증강 (1) | 2024.12.28 |