바로 앞에서 심층신경망을 활용하여 이진 분류를 수행하는 방법에 대해 글을 올렸다.
이진분류를 위해서 우리는 심층신경망의 가장 마지막 계층 이후에
시그모이드 활성 함수를 넣어주어 확률값이 반환되도록 구현하였다.
>하지만 우리가 분류하고자 하는 클래스가 2개가 아닌 여러 개라면 어떻게 할까?<
즉, 이진 분류와 달리 정답 레이블이 2개가 아니라 3개,4개 등 다양한 클래스가 존재할 때
우리는 모델의 출력 벡터는 각 후보 클래스에 대한 조건부 확률 값을 요소로 가지고 있도록 구현해야 한다.
그리고 이를 다중 클래스 분류라고 칭한다.
소프트맥스(Softmax)
이진 분류에서는 심층신경망 마지막에 시그모이드 함수를 활용한 반면,
일반 분류 문제를 위해서는 심층신경망 마지막에 소프트맥스 함수를 활용한다.
소프트맥스 함수는 임의의 벡터를 입력을 받아 이산 확률 분포의 형태로 출력을 반환한다.
즉, 각각의 입력벡터에 대한 출력값의 모든 벡터 요소의 합이 1이 되도록
0에서 1사이의 실수 값을 반환한다는 말이다.
다시 말해 소프트맥스 함수의 결과는
입력벡터별로 각 클래스에 대한 확률 값이 들어가 있다고 이해하면 쉽다.
아래 사진을 통해 쉽게 이해해보자!
위 사진에서 z값을 보자
z값은 입력 벡터 x가 심층신경망을 통과하고, 소프트맥스 함수를 통과하기 직전의 데이터 벡터를 의미한다.
이때, z의 크기는 클래스 종류의 개수와 동일해야 한다.
예를 들어, 0,1,2의 클래스로 예측해야 하는 문제라면 z는 3차원 벡터이여야 한다.
소프트 맥스는 이 z값들을 이용하여 결과를 이용하여
각 클래스별 확률값을 반환해주는데, 이때 y1에 대한 확률을 계산하기 위해서는
z1,z2,z3의 값을 모두 이용해야 한다. y2,y3도 마찬가지이다.
즉, 소프트맥스 함수의 경우에는 벡터 내의 다른 차원과 상호작용하여 해당 차원의 값이 결정된다.
교차엔트로피
이진 분류에 대해서 배웠을 때,
회귀에서 사용하던 MSE 손실 함수를 사용하지 않고, BCE손실 함수를 사용한다고 이야기하였다.
다중 클래스 분류 문제는 이진 분류 문제의 일반화 버전이기 때문에,
마찬가지로 BCE 손실 함수의 일반 버전인 교차 엔트로피, CE 손실 함수를 사용한다.
수식은 건너뛰겠다.
로그소프트맥스
파이토치에서는 음의 가능도, NLL 손실 함수를 제공한다.
또한 소프트맥스 함수와 같이 로그소프트맥스 함수도 제공하는데,
소프트맥스에 그냥 로그를 취한 형태이며 수식은 아래와 같다.
정말로 그냥 소프트맥스 함수에 로그만 취한 것이다.
따라서 소프트맥스가 아닌 로그소프트맥스 함수를 사용할 경우,
우리는 각 클래스별 확률이 아닌 "로그 확률 값"을 얻을 수 있는 것이다.
그리고 이때 손실함수는 교차 엔트로피가 아닌, NLL 손실 함수를 사용해야 한다.
이말은 즉, 소프트맥스 함수에 CE 손실 함수를 사용하는 것은
로그소프트맥스 함수에 NLL 손실 함수를 사용하는 것과 같은 결과를 얻을 수 있다는 말을 뜻한다.
코드구현
MNIST데이터를 활용하여 다중클래스 분류 문제를 코드로 구현해보고자 한다.
MNIST 데이터셋은 한 샘플당 한 개의 0에서부터 9까지의 손 글씨 숫자로 구성되어 있다.
따라서 우리는 딥러닝을 통해 각 샘플이 0에서부터 9 사이에 어떤 클래스에 속하는지 분류해야한다.
MNIST 샘플의 각 픽셀은 0에서부터 255까지의 숫자로 구성되어 있다.
따라서 255로 각 픽셀 값을 나눠주면, 0에서 1까지의 값으로 정규화를 해줄 수 있다.
현재 우리의 신경망은 선형 계층으로만 이루어질 것이기 때문에, 2D 이미지도 1차원 벡터로 flatten하여 나타내야 한다.
MNIST 하나의 샘플은 크기의 픽셀들로 이루어져 있기 때문에
이 2차원 행렬을 1차원 벡터로 flatten할 경우, 784 크기의 벡터가 될 것이다.
#MNIST 샘플의 각 픽셀은 0에서 255까지 숫자로 이루어짐. 따라서 각 픽셀을 255로 나눠주면 정규화 가능
x=train.data.float() / 255 #정규화
y=train.targets
#view는 텐서의 크기를 변형 ( 단 데이터의 개수는 유지하되 size만 변경 -> ex) 2x8 -> 2x2x4 )
#-1을 넘겨주면 자동으로 데이터의 개수에 맞게끔 변형 (ex 2x6짜리 데이터에 대해서 view(3,-1)해주면 3x4로 바꿔줌)
x=x.view(x.size(0),-1)
#사진 1개에 대한 28x28개의 픽셀 값이 하나의 행으로 들어감
#즉 행 하나가 하나의 사진을 의미, 열 한개가 픽셀을 의미
print(x.shape, y.shape)
그리고 input과 output size를 따로 설정해준다.
모델은 784 크기의 입력을 받아, 10 개의 확률 값을 뱉어낼 것이다.
#어떤 크기의 흑백 사진이 들어오더라도 동작하도록 변수 사용
input_size=x.size(-1) #사진 하나당 픽셀의 개수가 784개 -> 따라서 입력값은 784
output_size=int(max(y))+1 #0~9사이의 숫자를 뱉어내야 하므로 클래스는 10개 (즉 숫자 10개에 대한 확률을 뱉어내야 함)
print(input_size, output_size)
MNIST는 테스트 데이터셋을 따로 제공하므로, 학습 데이터와 검증 데이터를 나누는 작업만 수행하면 된다.
따라서 이번에는 8:2의 비율로 학습/검증 데이터셋을 나누도록 하겠다.
#train / valid -> 8:2
ratios=[.8, .2]
train_cnt=int(x.size(0)*ratios[0])
valid_cnt=int(x.size(0)*ratios[1])
test_cnt=len(test.data)
cnt=[train_cnt, valid_cnt]
indices=torch.randperm(x.size(0))
x = torch.index_select(x, index=indices, dim=0)
y = torch.index_select(y, index=indices, dim=0)
x=list(x.split(cnt,dim=0))
y=list(y.split(cnt,dim=0))
#x에 test데이터도 추가 (현재 x에는 train과 valid밖에 없음)
x+=[(test.data.float()/255.).view(test_cnt,-1)]
y+=[test.targets]
for x_i, y_i in zip(x,y):
print(x_i.size(), y_i.size())
이번에도 nn.Sequential을 활용하여 MNIST 이미지를 분류하기 위한 모델을 구현해보고자 한다.
한 개의 MNIST 이미지는 변환을 통해 784개의 요소를 갖는 1차원 벡터가 될 것이다.
그리고 우리의 모델은 784차원 벡터를 입력을 받아, 10개 클래스에 속할 확률 값을 각각 출력해야 한다.
즉, 입력의 크기는 784, 출력의 크기는 10이 된다는 말이다.
# 학습코드 구현
#심층신경망 구현
model=nn.Sequential(nn.Linear(input_size,500),nn.LeakyReLU(),
nn.Linear(500,400),nn.LeakyReLU(),
nn.Linear(400,300),nn.LeakyReLU(),
nn.Linear(300,200),nn.LeakyReLU(),
nn.Linear(200,100),nn.LeakyReLU(),
nn.Linear(100,50),nn.LeakyReLU(),
nn.Linear(50,output_size),
nn.LogSoftmax(dim=-1) #로그 소프트맥스 활성화 함수 사용 -> 손실함수는 자동으로 NLL함수
)
print(model)
마지막에 Softmax함수 또는 LogSoftmax함수 사용하는 것 잊지말기!
본 예제에서는 로그소프트맥스 함수를 사용했기 때문에
손실함수는 NLL함수를 사용해야 한다.
optimizer=optim.Adam(model.parameters())
#손실함수
crit=nn.NLLLoss()
앞에서 구현했던 문제들보다 입력 크기가 훨씬 크기 때문에
이번 예제는 GPU에서 학습을 해보자!
GPU에서 학습하기 위해서는 아래 코드를 작성해주면 된다.
#이미지라는 용량이 큰 데이터셋 이용 -> GPU에서 학습
device=torch.device('cpu')
if torch.cuda.is_available():
device=torch.device('cuda')
model=model.to(device)
x=[x_i.to(device) for x_i in x]
y=[y_i.to(device) for y_i in y]
이제 구현한 모델을 가지고 학습시켜보자!
코드는 앞에서 했던 코드와 거의 동일한 코드이며
손실함수가 NLL함수인 것만 유의하면 된다.
n_epoch=1000
batch_size=32
print_interval=10
lowest_loss=np.inf #최저 검증 손실을 추적하기 위한 변수
best_model=None # 최저 검증 손실을 뱉어낸 모델을 저장하기 위한 변수
early_stop=50 #조기종료
lowest_epoch=np.inf #최저 검증 손실 값을 뱉어낸 에포크를 저장하기 위한 변수
train_history, valid_history=[], []
for i in range(n_epoch):
indices=torch.randperm(x[0].size(0)) #인덱스를 섞어줌
x_=torch.index_select(x[0], dim=0, index=indices) #섞어진 인덱스 순으로 재배열
y_=torch.index_select(y[0], dim=0, index=indices)
x_=x_.split(batch_size, dim=0) #미니배치 생성
y_=y_.split(batch_size, dim=0)
train_loss,valid_loss=0,0
y_hat=[]
#Train Data에 대한 작업 수행
for x_i, y_i in zip(x_,y_):
y_hat_i=model(x_i)
loss=crit(y_hat_i,y_i) #이진 분류이기 때문에 손실함수는 이진크로스엔트로피 사용!
optimizer.zero_grad()
loss.backward()
optimizer.step()
train_loss+=float(loss) #하나의 epoch에서 나오는 모든 iteration의 loss를 더함
train_loss=train_loss/len(x_) #그리고 평균을 냄 -> 1epoch당 loss
#검증 데이터셋을 활용하여 검증 작업 수행
with torch.no_grad():#그래디언트 계산할 필요없음
#섞을 필요 x
x_=x[1].split(batch_size,dim=0)
y_=y[1].split(batch_size,dim=0)
valid_loss=0
for x_i, y_i in zip(x_,y_):
y_hat_i=model(x_i)
loss=crit(y_hat_i,y_i)
valid_loss+=loss
y_hat += [y_hat_i]
valid_loss=valid_loss/len(x_)
train_history+=[train_loss]
valid_history+=[valid_loss]
if (i+1)% print_interval==0:
print('Epoch %d : train_loss= %.4e valid_loss= %.4e lowest_loss= %.4e' % (i+1, train_loss, valid_loss, lowest_loss))
#학습과 검증이 끝난 후에는 검증 손실 값을 기준으로 모델 저장 여부 판단
#lowest_loss와 현재 검증 손실 값인 valid_loss를 비교하여 갱신될 경우 현재 에포크의 모델을 best_model에 저장
if valid_loss <= lowest_loss:
lowest_loss=valid_loss
lowest_epoch=i
#state_dict(): 현재 모델에 저장되어 있던 key_value들을 반환
best_model=deepcopy(model.state_dict())
else:
if early_stop > 0 and lowest_epoch + early_stop < i+1:
print('There is no improvement during last %d epochs' % (early_stop))
break
print('The best validation loss from epoch %d : %.4e' %(lowest_epoch+1, lowest_loss))
#Load best epoch's model.
model.load_state_dict(best_model)
테스트 데이터 셋에 대한 예측 코드 또한 동일하다
#테스트 데이터셋에 대해서도 확인
test_loss=0
y_hat=[]
with torch.no_grad():
x_=x[2].split(batch_size,dim=0)
y_=y[2].split(batch_size,dim=0)
for x_i, y_i in zip(x_,y_):
y_hat_i=model(x_i)
loss=crit(y_hat_i,y_i)
test_loss+=loss
y_hat+=[y_hat_i]
test_loss=test_loss/len(x_)
y_hat=torch.cat(y_hat,dim=0)
print('Test loss : %.4e'%test_loss)
정확도를 구해보자.
다중분류 결과값은 아래 사진과 같이 입력벡터별 각 클래스에 대한 확률값을 반환해주기 때문에
따라서 마지막 계층의 출력값 중에서
가장 높은 값을 가지고 있는 클래스 인덱스가
(어차피 인덱스도 0부터 시작하고, 숫자(클래스)도 0부터 시작하기 때문에 동일)
모델이 예측한 클래스의 인덱스라고 볼 수 있다.
이는 파이토치의 argmax 함수를 통해 구현할 수 있다.
#정확도 구하기
#y_hat은 각 클래스, 즉 숫자에 대한 확률값을 가지고 있음 -> 따라서 가장 높은 값을 가지고 있는 클래스 인덱스가 모델이 예측한 숫자와 동일
#만약 0번째 인덱스 확률이 가장 높았으면 숫자 0으로 예측
correct_cnt=(y[-1]==torch.argmax(y_hat,dim=-1)).sum()
total_cnt=float(y[-1].size(0))
print('Test Accuracy : %.4f' % (correct_cnt / total_cnt))
test accuracy는 약 97%로 매우 높은 수치를 보였다.
보통 분류 문제는 정확도를 통해서 모델의 성능을 평가할 수 있다.
하지만 서비스를 위해서는 단순히 정확도만 살펴보기보단,
어떤 케이스에서 모델이 약하고 쉽게 틀릴 수 있는지 파악할 수 있어야 한다.
또한 모델의 단점에 대한 면밀한 분석이 있어야지만,
이후 모델의 개선에 있어서도 올바른 방향으로 효율적인 개선을 수행할 수 있을 것이다.
이런 의미에서 confusion matrix은 매우 유용하게 활용될 수 있다.
sklearn에서 제공하는 confusion_matrix 함수를 통해 우리는 쉽게 혼동 행렬을 계산할 수 있다.
(혼동 행렬 : TP, FP, TN, FN에 대한 Table)
인수로는 실제값과 예측값을 넘겨주면 된다.
#혼동 행렬
#TP, TN ,FP, FN에 대한 값을 표로 나타낸 것
import pandas as pd
from sklearn.metrics import confusion_matrix
pd.DataFrame(confusion_matrix(y[-1], torch.argmax(y_hat,dim=-1)),
index=['true_%d'%i for i in range(10)],
columns=['pred_%d'%i for i in range(10)])
행렬 표의 대각 성분은 정답을 맞춘 갯수를 의미한다.
또한 표에서 ‘true_4’ 행의 ‘pred_9' 열에 위치한 값이 12으로 되어 있는데,
이는 실제 4이 정답인데, 9로 예측해서 틀린 샘플의 숫자가 12개라는 의미이다.
실제로 4와 9는 비슷한 모양을 가지고 있으므로 헷갈릴 여지가 있어 보인다.
따라서 만약 배포를 염두에 두고 모델 개선을 하고자 한다면,
4와 9에 대한 데이터를 더 모으거나, 4와 9를 구별하기 위한 추가적인 조치를 고민해야한다.
'✍️ STUDY > DeepLearning' 카테고리의 다른 글
[DL] 정규화 (Regularization) (0) | 2023.05.23 |
---|---|
[DL] 심층신경망을 활용한 이진분류, 평가지표 (0) | 2023.05.22 |
[DL] 오버피팅(Over Fitting) , 검증(Validation) (1) | 2023.05.20 |
[DL] 적응형 학습률, Optimizer (1) | 2023.05.20 |
[DL] 심층신경망 Basic (0) | 2023.05.19 |