비즈니스 문제를 해결하고 예측하는 데이터 사이언티스트가 되고 싶다면?
#인공지능 

다중 레이블 분류 (Multi-Label Classification)

다중 레이블 분류 (Multi-Label Classification)는 데이터가 동시에 여러 카테고리에 속할 수 있는 상황을 다루는 분류 문제입니다.

2024-06-18 | 김성진

다중 레이블 분류 (Multi-Label Classification)

다중 레이블 분류 (Multi-Label Classification)는 데이터가 동시에 여러 카테고리에 속할 수 있는 상황을 다루는 분류 문제입니다. 단일 레이블 분류와 달리, 하나의 데이터 포인트가 여러 개의 클래스에 할당될 수 있다는 점에서 복잡성이 증가합니다.

예를 들어, 이메일을 “스팸” 또는 “스팸 아님”으로 분류하는 것이 아니라 “스팸”, “업무”, “개인” 등 여러 카테고리 중 하나로 분류하는 것입니다. 반면, 다중 레이블 분류는 한 개의 영화가 “액션”, “코미디”, “로맨스” 등의 여러 장르에 동시에 속할 수 있는 경우입니다.

이러한 다중 레이블 분류는 텍스트 분류, 이미지 태그 지정, 음악 장르 분류, 의료 진단 등 다양한 실제 응용 분야에서 중요한 역할을 합니다. 딥러닝은 이 문제를 해결하는 데 있어 강력한 도구로 자리 잡고 있으며, 특히 신경망 구조를 활용한 접근법이 주목받고 있습니다.

각 분류의 차이점

이진 분류(Binary Classification):
이진 분류는 두 개의 클래스(예/아니오, 0/1, 참/거짓)로 데이터를 분류하는 작업을 말합니다.

예를 들어, 이메일을 스팸과 정상 메일로 분류하거나, 환자가 특정 질병을 앓고 있는지 여부를 판별하는 경우가 이에 해당합니다. 이진 분류 문제에서는 주로 로지스틱 회귀, 서포트 벡터 머신(SVM), 신경망 등의 알고리즘이 사용됩니다.

다중 클래스 분류(Multiclass Classification):
다중 클래스 분류는 상호 배타적인 여러 클래스 중 하나로 데이터를 분류하는 작업입니다.

예를 들어, 동물 이미지를 고양이, 개, 새 등의 여러 종류로 분류하거나, 색상을 빨강, 파랑, 노랑 등의 여러 색상으로 분류하는 경우가 이에 해당합니다. 다중 클래스 분류 문제에서는 소프트맥스 회귀, 결정 트리, 랜덤 포레스트, 신경망 등의 알고리즘이 사용됩니다.

다중 레이블 분류(Multilabel Classification):
다중 레이블 분류는 하나의 데이터에 여러 개의 레이블을 할당하는 작업입니다.
이는 데이터가 동시에 여러 카테고리에 속할 수 있음을 의미합니다.

예를 들어, 블로그 게시물에 여러 태그를 붙이거나, 영화에 여러 장르를 할당하는 경우가 이에 해당합니다. 다중 레이블 분류 문제에서는 각 레이블에 대해 별도의 이진 분류기를 학습시키거나, 신경망의 출력층을 다중 노드로 구성하여 각 노드가 특정 클래스에 대한 확률을 출력하도록 하는 접근 방식이 사용됩니다.

모델 훈련 기법

다중 레이블 분류 모델을 훈련하는 데는 여러 레이블을 인스턴스에 동시에 할당할 수 있도록 하는 특정 기법이 필요합니다:

  • 시그모이드 활성화(Sigmoid Activation): 신경망의 출력층에서 시그모이드 활성화 함수가 자주 사용됩니다. 다중 클래스 분류에서 사용되는 소프트맥스와 달리, 시그모이드는 각 출력 노드를 독립적으로 활성화하여 0과 1 사이의 값을 생성합니다. 이는 해당 레이블이 존재할 확률을 나타냅니다.
  • 이진 크로스 엔트로피 손실(Binary Cross-Entropy Loss): 훈련 중 이 손실 함수는 예측된 확률과 각 레이블의 실제 존재 여부 간의 차이를 측정합니다. 이는 모델이 다중 레이블 예측에서 오류를 최소화하도록 유도합니다.

평가 지표

다중 레이블 분류 모델의 성능을 평가하기 위해서는 인스턴스당 여러 레이블의 복잡성을 처리할 수 있는 특정 지표가 필요합니다:

  • 해밍 손실(Hamming Loss): 이 지표는 잘못 예측된 레이블의 비율을 계산합니다. 이는 레이블 정확도 측면에서 모델 성능을 종합적으로 측정합니다.
  • 정밀도@k(Precision at k): 이 지표는 상위 k개의 예측된 레이블의 정밀도를 평가합니다. 이는 모든 레이블을 고려할 필요가 없는 상황에서 유용하며, 가장 관련성 높은 레이블에 초점을 맞춥니다.
  • 재현율@k(Recall at k): 정밀도@k와 유사하게, 상위 k개의 예측된 레이블의 재현율을 평가합니다. 이는 상위 예측 중 관련성 높은 레이블을 포착하는 데 중점을 둡니다.

다중 레이블 분류의 이러한 세부 사항을 이해하는 것은 여러 카테고리에 동시에 속할 수 있는 작업을 수행하는 실무자들에게 필수적입니다. 이는 복잡한 실제 시나리오에서 효과적인 모델 설계와 평가를 보장합니다.

다중 레이블 분류 – 코드 연습

아래의 코드는 캐글 (kaggle) 에 공개되어 있는 다중 레이블 분류 모델입니다. Xception 과 ResNet 모델을 사용하고, 각 모델에 대한 성능을 확인합니다. 그다음 이 두 모델을 쌓아서 성능을 개선시킵니다. 여기서는 이미지를 불러오고, Xception 모델과 ResNet 모델을 구성하여 훈련하는 것까지만 소개합니다.

캐글 (kaggle) 의류 이미지 데이터셋

이 캐글에 공개되어 있는 의류 이미지 데이터셋은 다중 레이블 분류를 연습하기 위해 만들어졌으며, 누구든 연습을 원하는 사람들에게 공개되어 있습니다. 이 데이터셋은 11,385개의 의류 이미지로 구성되어 있습니다. 그리고 색상(검정색, 파란색, 갈색, 녹색, 빨간색, 흰색)과 옷의 종류(드레스, 바지, 셔츠, 반바지, 신발)에 따라 분류되어 있습니다.

F1 Score

F1 스코어는 불균형 데이터셋에서 모델의 성능을 평가하는 데 매우 유용한 지표입니다.
F1 스코어는 모델의 정밀도(precision)와 재현율(recall)을 조화 평균으로 결합하여 계산됩니다.
두 지표 간의 균형을 반영하여, 한쪽으로 치우치지 않는 성능 평가를 제공합니다.

정밀도 (Precision)
정밀도는 모델이 실제로 긍정인 샘플 중에서 얼마나 정확하게 예측했는지를 나타냅니다.
이는 모델이 예측한 긍정 결과 중에서 실제 긍정인 비율을 의미합니다.

    $$\text{Precision} = \frac{\text{True Positives}}{\text{True Positives} + \text{False Positives}}$$

재현율 (Recall)
재현율은 실제 긍정인 샘플 중에서 모델이 얼마나 많이 예측했는지를 나타냅니다.
실제 긍정 결과 중에서 모델이 올바르게 예측한 비율을 의미합니다.

    $$\text{Recall} = \frac{\text{True Positives}}{\text{True Positives} + \text{False Negatives}}$$

F1 스코어 계산
F1 스코어는 정밀도와 재현율의 조화 평균으로 계산됩니다.
두 지표 간의 균형을 맞추기 위한 방법으로, 모델의 전반적인 성능을 평가하는 데 유용합니다.

    $$\text{F1 Score} = 2 \times \frac{\text{Precision} \times \text{Recall}}{\text{Precision} + \text{Recall}}$$

F1 스코어는 특히 불균형 데이터셋에서 유용합니다.

불균형 데이터셋에서는 단순한 정확도(accuracy)가 높은 성능을 나타내는 것처럼 보일 수 있지만, 이는 대부분의 예측이 다수 클래스에 집중된 결과일 수 있습니다. 반면, F1 스코어는 정밀도와 재현율을 모두 고려하여, 모델이 실제로 얼마나 잘 예측하고 있는지를 보다 균형 있게 평가할 수 있습니다.

def recall_m(y_true, y_pred):

    y_pred = K.cast(K.greater(K.clip(y_pred, 0, 1), 0.5),K.floatx())
    true_positives = K.round(K.sum(K.clip(y_true * y_pred, 0, 1)))
    possible_positives = K.sum(K.clip(y_true, 0, 1))
    recall_ratio = true_positives / (possible_positives + K.epsilon())
    return recall_ratio

def precision_m(y_true, y_pred):

    y_pred = K.cast(K.greater(K.clip(y_pred, 0, 1), 0.5), K.floatx())
    true_positives = K.round(K.sum(K.clip(y_true * y_pred, 0, 1)))
    predicted_positives = K.sum(y_pred)
    precision_ratio = true_positives / (predicted_positives + K.epsilon())
    return precision_ratio

def f1_m(y_true, y_pred):
    
    precision = precision_m(y_true, y_pred)
    recall = recall_m(y_true, y_pred)
    return 2*((precision*recall)/(precision+recall+K.epsilon()))

이미지 전처리

from tensorflow.keras.preprocessing.image import ImageDataGenerator

train_datagen = ImageDataGenerator(rescale = 1./255,
                                   validation_split=0.15)

training_set = train_datagen.flow_from_directory('../input/apparel-images-dataset',
                                                 target_size = (img_width, img_height),
                                                 batch_size = batch_size,
                                                 subset='training')

val_set = train_datagen.flow_from_directory('../input/apparel-images-dataset',
                                                 target_size = (img_width, img_height),
                                                 batch_size = batch_size,
                                                 subset='validation')
Found 9686 images belonging to 24 classes.
Found 1699 images belonging to 24 classes.

이미지 데이터셋 플롯

fig, ax = plt.subplots(nrows=6, ncols=4,figsize = (15,20))

for X_batch, y_batch in training_set:
    i=0
    for row in ax:
        for col in row:
            col.imshow(X_batch[i])
            i+=1
    break
plt.show()

 

 

Xception Net

Xception 모델 구성

텐서플로 케라스 (tensorflow keras) 를 이용하여 Xception 모델을 구성합니다.

model = tf.keras.Sequential()

model.add( 
    
    tf.keras.applications.Xception(
        input_shape = input_shape,
        include_top = True,
        weights     = None,
        pooling     = 'max'
        )
)

model.add(Dense(512,activation='relu'))
model.add(Dense(64,activation='relu'))
model.add(Dense(24,activation='softmax'))

model.compile(
    optimizer = 'adam', 
    loss = 'categorical_crossentropy',               
    metrics=['accuracy',f1_m]
)
model.summary()
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
xception (Functional)        (None, 1000)              22910480  
_________________________________________________________________
dense (Dense)                (None, 512)               512512    
_________________________________________________________________
dense_1 (Dense)              (None, 64)                32832     
_________________________________________________________________
dense_2 (Dense)              (None, 24)                1560      
=================================================================
Total params: 23,457,384
Trainable params: 23,402,856
Non-trainable params: 54,528
_________________________________________________________________

모델 훈련

history = model.fit(training_set,
                    validation_data = val_set,
                    epochs=epochs,
                    callbacks = [c],
                    verbose = 1)
Epoch 1/25
76/76 [==============================] - 116s 1s/step - loss: 2.9810 - accuracy: 0.1674 - f1_m: 0.0065 - val_loss: 3.0719 - val_accuracy: 0.0765 - val_f1_m: 0.0000e+00
Epoch 2/25
76/76 [==============================] - 69s 909ms/step - loss: 2.0346 - accuracy: 0.3322 - f1_m: 0.1073 - val_loss: 3.2720 - val_accuracy: 0.0412 - val_f1_m: 0.0000e+00
Epoch 3/25
76/76 [==============================] - 69s 904ms/step - loss: 1.7787 - accuracy: 0.3755 - f1_m: 0.1868 - val_loss: 5.2617 - val_accuracy: 0.0865 - val_f1_m: 0.0968
...(생략)

결과

print(f"Max F1-score in validation dataset = {max(history.history['val_f1_m'])}")
Max F1-score in validation dataset = 0.8710781335830688

F1 점수는 0.87로 나왔습니다.

ResNet50V2

ResNet50V2 모델 구성

model = tf.keras.Sequential()

model.add( 
    tf.keras.applications.ResNet50V2(
        input_shape = input_shape,
        include_top = True,
        weights     = None,
        pooling     = 'max'
        )
)

model.add(Dense(512,activation='relu'))
model.add(Dense(64,activation='relu'))
model.add(Dense(24,activation='softmax'))

model.compile(
    optimizer = 'adam', 
    loss = 'categorical_crossentropy',               
    metrics=['accuracy',f1_m]
)

model.summary()
Model: "sequential_1"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
resnet50v2 (Functional)      (None, 1000)              25613800  
_________________________________________________________________
dense_3 (Dense)              (None, 512)               512512    
_________________________________________________________________
dense_4 (Dense)              (None, 64)                32832     
_________________________________________________________________
dense_5 (Dense)              (None, 24)                1560      
=================================================================
Total params: 26,160,704
Trainable params: 26,115,264
Non-trainable params: 45,440
_________________________________________________________________

모델 훈련

history = model.fit(training_set,
                    validation_data = val_set,
                    epochs=epochs,
                    callbacks = [c],
                    verbose = 1)
Epoch 1/25
76/76 [==============================] - 65s 777ms/step - loss: 3.0599 - accuracy: 0.1000 - f1_m: 0.0000e+00 - val_loss: 4.1324 - val_accuracy: 0.0665 - val_f1_m: 0.0000e+00
Epoch 2/25
76/76 [==============================] - 57s 749ms/step - loss: 2.3934 - accuracy: 0.2776 - f1_m: 0.0032 - val_loss: 2.7723 - val_accuracy: 0.1919 - val_f1_m: 0.0000e+00
Epoch 3/25
76/76 [==============================] - 57s 750ms/step - loss: 1.9038 - accuracy: 0.3421 - f1_m: 0.1196 - val_loss: 3.0617 - val_accuracy: 0.2254 - val_f1_m: 0.1858
...(생략)

결과

print(f"Max F1-score in validation dataset = {max(history.history['val_f1_m'])}")
Max F1-score in validation dataset = 0.8500279188156128

F1 점수는 0.85로 나왔습니다.

캐글의 출처 페이지로 가면, 이 두 모델을 쌓아서 0.91점까지 개선시킨 결과를 볼 수 있습니다.