[PyTorch] torchmetrics: Multiclass vs. Multilabel F1-score
Multiclass vs. Multilabel
분류문제에서 multiclass와 multilabel 문제는 종종 혼용되는 용어지만, 분명 다른 의미를 갖습니다. 먼저 multiclass의 경우, 이진 분류가 아닌 일반적인 분류 문제를 생각하시면 됩니다. 예를들어, 고양이, 개 그리고 닭의 이미지를 구별하는 문제가 이에 해당됩니다. 각각의 샘플은 무조건 하나의 라벨을 갖는다는 특징이 있으며, 이를 조금 어려워 보이는 말로 `상호 배타적인 (Mutually exclusive)` 라벨을 갖는다고 합니다. 반대로 multilabel의 경우, 마찬가지로 고양이, 개 그리고 닭의 이미지를 구분하는 상황이지만, 하나의 이미지에 여러 라벨이 등장 가능한 경우입니다. 위의 예시로 보자면, 고양이, 개 그리고 닭이 동시에 등장하는 이미지를 여러 라벨로 동시에 표현 가능한 상황입니다.
F1 score
Multiclass
F1 score는 multiclass의 경우, 각 클래스 마다 결과를 이진화 시켜 TP, TN, FP, FN를 구합니다. 전체 데이터 셋에서 모든 TP, TN, FP, FN 값들을 통해 구하는 F1 score를 `Micro F1 score`라고 합니다. 그리고 각 class마다의 F1 score를 구한 뒤, 단순 평균내는 방식을 `Macro F1 score`, 샘플의 수에 따라 평균내는 방식을 `Weighted F1 score`라고 합니다.
이렇게 나누어 보는 의미는, F1 metrics는 보통 class imbalance 상태에서 합리적인 성능을 측정하기 때문에 있습니다. 데이터셋 전체의 TP, TN, FP, FN를 보는 micro F1 score는 모델의 전반적인 성능을 보고싶을 때 확인 가능하며, macro F1 score의 경우 majority class 뿐 아니라 minority에 있는 class도 잘 맞추는지 보기위해, 즉 모든 클래스가 동등하게 중요한 경우 확인합니다. Weighted F1 score의 경우 이러한 표본수의 불균형을 고려하여 F1 score가 잘 나오는지 확인하기 좋습니다. Class imbalance가 심한 경우에는, 경험적으로 `Weighted > Micro > Macro` 순으로 성능이 잘 나옵니다.
Multilabel
그렇다면, multilabel의 경우 어찌 F1 을 확인할까요? Multiclass의 경우 샘플마다 상호 배타적인라벨을 갖게되어, label을 하나의 vector로 갖습니다. 하지만, multilabel의 경우 여러 라벨을 하나의 샘플이 동시에 갖을 수 있습니다. 때문에 2차원의 행렬을 label로 갖으며, 이 행렬은 하나의 행이 샘플에 해당하며, 클래스에 해당하는 열을 갖습니다. Multilabel을 예측하는 모델에 의해 이런 행렬의 원소는 각각 0~1의 probability를 갖습니다. 각각의 class별로 (column-wise) TP, TN, FP, FN를 계산하여, F1 score를 계산합니다. 상호배타성을 제외하고는 multiclass f1 score를 구하는 방식과 동일합니다.
torchmetrics
이를 인지하고 `torchmetrics`를 통해 multiclass와 multilabel을 계산해 보았습니다.
import numpy as np
import pandas as pd
import torch
from torch import tensor
from sklearn.metrics import precision_score, recall_score, f1_score
from torchmetrics.classification import MultilabelF1Score, MulticlassF1Score
def generate_random_integers(n, m, N):
return np.random.randint(n, m+1, N)
def one_hot_encoding(integers, num_classes):
return np.eye(num_classes)[integers]
def assign_probabilities(one_hot_array):
probabilities = np.random.rand(*one_hot_array.shape)
max_indices = np.argmax(one_hot_array, axis=1)
for i, max_index in enumerate(max_indices):
probabilities[i, :] = probabilities[i, :] / np.sum(probabilities[i, :]) # 먼저 확률을 0~1 사이로 맞춤
temp = probabilities[i].copy()
maxidx = np.argmax(probabilities[i])
temp[max_index] = probabilities[i][maxidx]
temp[maxidx] = probabilities[i][max_index]
probabilities[i] = temp
return probabilities
def assign_probabilities_threshold(one_hot_array, threshold = 0.5):
probabilities = np.random.rand(*one_hot_array.shape)
max_indices = np.argmax(one_hot_array, axis=1)
num_label = one_hot_array.shape[1]
for i, max_index in enumerate(max_indices):
probabilities[i, max_index] = threshold / (2 - threshold) * 3 * (np.sum(probabilities[i, :])-probabilities[i, max_index]) + 0.0001# 정답 레이블을 다른 레이블에 대한 모든 확률값을 더한거의 threshold배 하면 0~1 로 변환시 최소 threshold이상은 나옴
probabilities[i, :] = probabilities[i, :] / np.sum(probabilities[i, :]) # 확률을 0~1 사이로 맞춤
return probabilities
위의 함수를 통해 class5 개인 상호 배타적인 라벨을 100개 생성하고,
random_integers = generate_random_integers(0, 4, 100)
print(random_integers)
[4 3 4 2 1 4 0 3 3 1 0 4 1 1 4 3 0 4 1 4 4 3 4 0 2 3 2 3 0 3 4 4 2 1 4 3 0
4 2 3 2 2 2 3 4 1 4 3 1 2 0 2 0 3 3 1 4 1 3 4 0 2 1 1 4 2 0 2 2 3 3 2 1 4
2 3 4 2 0 0 1 4 3 4 1 2 3 4 0 3 1 3 1 1 3 0 3 3 4 0]
원핫인코딩한 뒤,
one_hot = one_hot_encoding(random_integers, 5)
print(one_hot[:5,:])
[[0. 0. 0. 0. 1.]
[0. 0. 0. 1. 0.]
[0. 0. 0. 0. 1.]
[0. 0. 1. 0. 0.]
[0. 1. 0. 0. 0.]]
머신러닝의 아웃풋처럼 상황을 연출하기위해 정답 라벨에 가장 큰 값을 주고, 샘플마다의 라벨의 예측값 총합이 1이 되도록 만들었습니다.
probabilities = assign_probabilities(one_hot)
print(probabilities[:5,:])
[[0.12783913 0.21261925 0.22371256 0.19388086 0.2419482 ]
[0.32742592 0.09817918 0.05733956 0.34157258 0.17548275]
[0.17536185 0.16354045 0.25521722 0.06998232 0.33589815]
[0.28299039 0.0825619 0.31421498 0.05561604 0.26461668]
[0.19875125 0.34519277 0.21694464 0.00326659 0.23584475]]
문제상황
이렇게 셋팅된 상황에서 f1 score는 다음과 같이 무조건 1이 나와합니다.
probabilities = torch.from_numpy(probabilities)
random_integers = torch.from_numpy(random_integers)
one_hot = torch.from_numpy(one_hot)
f1_macro_desc1 = MulticlassF1Score(num_classes=5, average=None)
f1_macro_desc1(probabilities, random_integers)
>>> tensor([1., 1., 1., 1., 1.])
그런데, 막상 두가지로 모두 계산해 보니, multilable에서는 다음과 같은 결과가 나왔습니다.
f1_macro_desc2 = MultilabelF1Score(num_labels=5, average=None)
f1_macro_desc2(probabilities, one_hot)
>>> tensor([0.0000, 0.1818, 0.1111, 0.1905, 0.1739])
무엇이 문제일까요?
분명 계산되는 F1 score의 수식도 같고, 별다른 문제도 없어보입니다.
원인
앞서 살펴본 multilabel F1 score에서 상호배타성이 없기 때문에, multiclass classification 처럼 `argmax` 연산으로 정답을 정하는것이 아니라, 각각의 probability가 특정 threshold를 넘으면 정답이라고 여기기 때문입니다. 실제로 아래 GitHub에 구현되어있는 코드의 핵심을 살펴보면, `MulticlassFBetaScore`에는 threshold 파라미터가 없으며, 반대로 `MultilabelFBetaScore`의 경우는 존재하며, 디폴트값으로 0.5를 갖습니다.
torchmetrics/src/torchmetrics/classification/f_beta.py at v1.4.0 · Lightning-AI/torchmetrics
Torchmetrics - Machine learning metrics for distributed, scalable PyTorch applications. - Lightning-AI/torchmetrics
github.com
이를 고려하여, 정답 probability 값에 0.5 이상을 넣도록 생성해주는 함수하나 만들고,
def assign_probabilities_threshold(one_hot_array):
threshold = 0.5
probabilities = np.random.rand(*one_hot_array.shape)
max_indices = np.argmax(one_hot_array, axis=1)
num_label = one_hot_array.shape[1]
for i, max_index in enumerate(max_indices):
probabilities[i, max_index] = threshold / (2 - threshold) * 3 * (np.sum(probabilities[i, :])-probabilities[i, max_index]) + 0.0001# 정답 레이블을 다른 레이블에 대한 모든 확률값을 더한거의 threshold배 하면 0~1 로 변환시 최소 threshold이상은 나옴
probabilities[i, :] = probabilities[i, :] / np.sum(probabilities[i, :]) # 확률을 0~1 사이로 맞춤
return probabilities
결과를 확인하면, multiclass의 경우와 같은 결과가 나오게 됩니다.
probabilities_thre = assign_probabilities_threshold(one_hot)
probabilities_thre = torch.from_numpy(probabilities_thre)
f1_macro_desc = MultilabelF1Score(num_labels=5, average=None)
f1_macro_desc(probabilities_thre, one_hot)
>>> tensor([1., 1., 1., 1., 1.])
참고로 `MulticlassFBetaScore` 또는 `MultilabelFBetaScore`에서 $\beta$는 precision recall의 조화 평균에서 가중치를 줄 때 사용합니다. 아래의 식을 보면 명확해집니다. $\beta$가 1보다 크면 recall에 힘을 실어주며, 1보다 작으면 precision에 힘을 실어줍니다.
Outro
저는 별다른 주의 없이 Multiclass classification 상황에서 Multilabel classification을 사용하였고, 결과 비교시 sklearn의 사용한 F1 결과와는 조금 다른 값이 나와서 찾아보았습니다. 감사하게도(?) 근대의 머신러닝 모델은 결과에 대해 과신하는 경향이 있어 probability 값이 0.5를 대부분 크게 넘어 아주 큰 결과의 차이는 없었습니다.
참조
https://en.wikipedia.org/wiki/F-score
F-score - Wikipedia
From Wikipedia, the free encyclopedia Statistical measure of a test's accuracy For the significance test, see F-test. Precision and recall In statistical analysis of binary classification and information retrieval systems, the F-score or F-measure is a mea
en.wikipedia.org