이진 분류
데이터를 규칙에 따라 두 그룹(참(True) 또는 거짓(False))으로 분류하는 작업.
이 작업은 결과를 참 또는 거짓으로 구분하기 때문에 논리 회귀(logistic regression)나 논리 분류(logical classification)라고도 불린다. 이진 분류를 그래프로 표현하면 다음과 같다.

그래프에서 구분하려는 값을 X, 분류된 결과를 Y라고 할 때, X 값이 3 이하이면 False로, 4 이상의 값은 True로 분류된다. 그러나 X 값이 3.6처럼 애매한 위치에 있을 경우, Y 값이 0과 1 사이의 실수가 될 수 있다.
이때, 데이터를 0 또는 1로 명확히 분류하기 위해 임계값(대개 0.5)을 설정한다. 만약 Y 값이 0.5보다 작으면 False로, 0.5보다 크면 True로 분류된다.
하지만 데이터가 항상 0과 1 사이의 값을 가지지 않을 수 있기 때문에, 이를 보정하기 위해 시그모이드 함수(Sigmoid Function)와 같은 활성화 함수를 사용한다. 활성화 함수에 대한 자세한 내용은 다음 게시글에서 다루도록 하겠다.
시그모이드 함수

시그모이드 함수(Sigmoid Function)는 S자형 곡선 모양의 그래프로, 반환값은 0 ~ 1, 또는 -1 ~ 1의 범위를 갖는다.
시그모이드 함수의 수식은 다음과 같다.
$$ Sigmoid(x) = \frac{1}{1 + e^{-x}}$$
시그모이드 함수는 계수에 따라 그래프의 경사가 달라진다.
계수가 0에 가까워질수록 그래프의 경사는 완만해지고, 계수가 0에서 멀어질수록 경사가 급격해진다.
다음 그림은 계수에 따른 시그모이드 함수의 변화를 보여준다.

시그모이드 함수의 출력값이 0.5보다 낮으면 False로, 0.5보다 크면 True로 분류된다.
시그모이드 함수는 미분값이 유연하기 때문에 입력값에 따라 값이 급격하게 변하지 않으며, 출력값의 범위가 0과 1 또는 -1과 1 사이로 제한되어 기울기 폭주(Exploding Gradient) 문제를 피할 수 있다.
또한, 미분식이 단순하여 계산이 용이하지만, 출력값이 0 또는 1에 가까워질 경우 미분값이 0에 근접하게 되어 기울기 소실(Vanishing Gradient) 문제가 발생할 수 있다는 단점도 존재한다.
이진 교차 엔트로피
이진 분류에 평균 제곱 오차 함수를 사용하면 좋은 결과를 얻기 어려운데, 평균 제곱 오차 함수는 예측값과 실젯값의 값의 차이가 작으면 계산되는 오차 크기가 작아지기 때문이다.
이를 보완하기 위해 이진 교차 엔트로피(Binary Cross Entropy, BCE)를 오차 함수로 사용한다.
이진 교차 엔트로피의 그래프와 수식은 다음과 같다.

$$ \begin{align*}
BCE \ \#1 &= -Y_{i} \cdot log(\hat{Y_{l}}) \\
BCE \ \#2 &= -\left(1 - Y_{i} \right) \cdot log(1 - \hat{Y_{l}}) \\ \\
\end{align*} $$
$$ \begin{align*}
BCE &= BCE \ \#1 + BCE \ \#2 \\
&= -\left(Y_{i} \cdot log(\hat{Y_{l}}) + \left(1 - Y_{i} \right) \cdot log(1 - \hat{Y_{l}}) \right)
\end{align*}$$
BCE #1은 실젯값($Y_{i} = 1$)이 1일 때의 수식, BCE #2는 실젯값($Y_{i} = 0$)이 0일 때의 수식이다.
로그 함수는 값이 불일치하는 비중이 높을수록 높은 손실 값을 반환하는 특성을 가진다.
로그 함수의 양끝은 0과 무한대이기 때문에, 이진 교차 엔트로피는 두 가지 로그 함수를 하나로 합쳐 기울기가 0이 되는 지점을 찾는다.
$$ BCE = -\frac{1}{n} \sum_{i = 1}^n \left(Y_{i} \cdot log(\hat{Y_{l}}) + \left(1 - Y_{i} \right) \cdot log(1 - \hat{Y_{l}}) \right)$$
이진 분류 구현
파이토치에서 이중 분류를 구현하기 위해, 아래와 같은 형태의 데이터를 사용한다.
| x | y | z | pass |
|---|---|---|---|
| 86 | 22 | 1 | False |
| 81 | 75 | 91 | True |
| 54 | 85 | 78 | True |
| 5 | 58 | 4 | False |
| ... | ... | ... | ... |
# 사용자 정의 데이터 세트
class CustomDataset(Dataset):
def __init__(self, file_path):
df = pd.read_csv(file_path)
self.x1 = df.iloc[:, 0].values
self.x2 = df.iloc[:, 1].values
self.x3 = df.iloc[:, 2].values
self.y = df.iloc[:, 3].values
self.length = len(df)
def __getitem__(self, index):
x = torch.FloatTensor([self.x1[index], self.x2[index], self.x3[index]])
y = torch.FloatTensor([int(self.y[index])])
return x, y
def __len__(self):
return self.length
이번 모델은 여러 레이어로 구성되어 있으므로, Sequential을 사용해 이들을 하나로 묶는다. 이렇게 묶인 레이어들은 순차적으로 실행된다.
# 사용자 정의 모델
class CustomModel(nn.Module):
def __init__(self):
super().__init__()
self.layer = nn.Sequential(
nn.Linear(3, 1),
nn.Sigmoid()
)
def forward(self, x):
x = self.layer(x)
return x
데이터세트와 모델 구현이 완료됐으면, 이진 교차 엔트로피 클래스(nn.BCELoss)로 오차 함수를 정의한다.
이진 교차 엔트로피 클래스는 기존에 사용했던 평균 제곱 오차 클래스(nn.MSELoss)와 동일한 방식으로 적용한다.
criterion = nn.BCELoss().to(device)
# 전체 코드
import pandas as pd
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader, random_split
class CustomDataset(Dataset):
def __init__(self, file_path):
df = pd.read_csv(file_path)
self.x1 = df.iloc[:, 0].values
self.x2 = df.iloc[:, 1].values
self.x3 = df.iloc[:, 2].values
self.y = df.iloc[:, 3].values
self.length = len(df)
def __getitem__(self, index):
x = torch.FloatTensor([self.x1[index], self.x2[index], self.x3[index]])
y = torch.FloatTensor([int(self.y[index])])
return x, y
def __len__(self):
return self.length
class CustomModel(nn.Module):
def __init__(self):
super().__init__()
self.layer = nn.Sequential(
nn.Linear(3, 1),
nn.Sigmoid()
)
def forward(self, x):
x = self.layer(x)
return x
dataset = CustomDataset("datasets/binary.csv")
dataset_size = len(dataset)
train_size = int(dataset_size * 0.8)
val_size = int(dataset_size * 0.1)
test_size = dataset_size - train_size - val_size
train_dataset, val_dataset, test_dataset = random_split(
dataset, [train_size, val_size, test_size], torch.manual_seed(4)
)
train_dataloader = DataLoader(train_dataset, batch_size=64, shuffle=True, drop_last=True)
val_dataloader = DataLoader(val_dataset, batch_size=4, shuffle=True, drop_last=True)
test_dataloader = DataLoader(test_dataset, batch_size=4, shuffle=True, drop_last=True)
device = "cuda" if torch.cuda.is_available() else "cpu"
model = CustomModel().to(device)
criterion = nn.BCELoss().to(device)
optimizer = optim.SGD(model.parameters(), lr=1e-4)
for epoch in range(10000):
cost = 0
for x, y in train_dataloader:
x = x.to(device)
y = y.to(device)
output = model(x)
loss = criterion(output, y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
cost += loss
cost /= len(train_dataloader)
if (epoch + 1) % 1000 == 0:
print(f"Epoch: {epoch + 1:4d}, Model: {list(model.parameters())}, Cost: {cost:.3f}")
with torch.no_grad():
model.eval()
for x, y in val_dataloader:
x = x.to(device)
y = y.to(device)
outputs = model(x)
print(outputs)
print(outputs >= torch.FloatTensor([0.5]).to(device))
print("----------------------------")
Epoch: 1000, Model: [Parameter containing:
tensor([[ 0.0028, -0.0006, 0.0036]], device='cuda:0', requires_grad=True), Parameter containing:
tensor([0.0949], device='cuda:0', requires_grad=True)], Cost: 0.680
(생략)
Epoch: 10000, Model: [Parameter containing:
tensor([[0.0089, 0.0070, 0.0096]], device='cuda:0', requires_grad=True), Parameter containing:
tensor([-0.9644], device='cuda:0', requires_grad=True)], Cost: 0.576
(생략)
tensor([[0.7563],
[0.6743],
[0.3967],
[0.7840]], device='cuda:0')
tensor([[ True],
[ True],
[False],
[ True]], device='cuda:0')
----------------------------
tensor([[0.5797],
[0.7276],
[0.6039],
[0.5545]], device='cuda:0')
tensor([[True],
[True],
[True],
[True]], device='cuda:0')
----------------------------'ML · DL > Pytorch 공부' 카테고리의 다른 글
| [파이토치 기초] 순전파와 역전파 (0) | 2025.02.26 |
|---|---|
| [파이토치 기초] 모델 저장/불러오기 (0) | 2025.01.09 |
| [파이토치 기초] 데이터세트 분리 (0) | 2025.01.07 |
| [파이토치 기초] 모듈 (0) | 2025.01.04 |
| [파이토치 기초] 데이터세트 & 데이터로더 (0) | 2024.12.30 |