Scikit-Learn 0.19 버전에 추가된 기능으로 이번에 소개할 모델은 ClassifierChain입니다.
다중 레이블(Multi-Label) 문제를 직접 다룰 수 있는 모델도 있지만(가령, 랜덤 포레스트), 이진 분류기를 사용하여 다중 레이블 분류기를 구현하는 간단한 방법은 One-Vs-All 방식을 사용하는 것입니다. 이 예에서 사용할 데이터는 Yeast 데이터셋으로 2,417개의 샘플과 103개의 특성, 14개의 타깃 레이블(즉, 다중 레이블)을 가지고 있습니다.
import numpy as np from sklearn.datasets import fetch_openml yeast = fetch_openml('yeast', version=4) X = yeast['data'] Y = yeast['target'] Y = Y == 'TRUE'
X와 행의 차원을 맞추기 위해 희소 행렬 Y를 전치하고 밀집 행렬로 변환합니다. 그러면 X는 (2417×103)인 행렬이고 Y는 (2417×14)인 행렬이 됩니다. 그 다음 훈련 데이터와 테스트 데이터로 나눕니다.
from sklearn.model_selection import train_test_split X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=0.2, random_state=42)
먼저 로지스틱 회귀을 사용해 OneVsRestClassifier 모델을 훈련시켜 보겠습니다.
from sklearn.linear_model import LogisticRegression from sklearn.multiclass import OneVsRestClassifier ovr = OneVsRestClassifier(LogisticRegression(solver='liblinear')) ovr.fit(X_train, Y_train) pred_ovr = ovr.predict(X_test)
OneVsRestClassifier는 타깃 레이블의 개수 만큼 로지스틱 회귀 모델을 만들어 각 레이블을 타깃으로 하는 분류기를 학습시킵니다. 따라서 pred_ovr의 차원은 (2417×14)이 됩니다. 다중 레이블 분류에 사용되는 대표적인 측정 방법은 자카드 유사도(jaccard_similarity_score)입니다. 이 함수에 진짜 타깃 레이블과 예측 결과를 전달하면 0~1 사이의 값을 반환합니다. 1에 가까울수록 두 값이 비슷한 것입니다.
from sklearn.metrics import jaccard_similarity_score ovr_score = jaccard_similarity_score(Y_test, pred_ovr) ovr_score
0.50828086055358779
이번에는 ClassifierChain을 사용해 보겠습니다. 이 (메타) 분류기는 OneVsRestClassifier처럼 레이블마다 하나의 모델을 학습시키지만 타깃 레이블을 특성으로 사용합니다. 다중 레이블의 문제에서 종종 타깃은 서로에게 상관관계를 가집니다. 이런 경우 분류기 체인 방식을 사용하면 One-Vs-All 전략 보다 더 좋은 결과를 얻을 수 있습니다.
ClassifierChain은 먼저 첫 번째 레이블에 대해 이진 분류기를 학습시킵니다. 그 다음 두 번째 분류기를 학습시킬 때 첫 번째 레이블을 특성으로 포함시킵니다. 그 다음엔 첫 번째와 두 번째 레이블을 특성으로 포함시켜 세 번째 분류기를 학습시키는 식입니다. 이렇다 보니 레이블의 순서에 따라 결과가 달라질 수 있습니다. ClassifierChain의 order 매개변수 기본값은 Y 타깃값을 순서대로 사용하므로 이보다는 ‘random’으로 주는 것이 일반적입니다.
from sklearn.multioutput import ClassifierChain cc = ClassifierChain(LogisticRegression(), order='random', random_state=42) cc.fit(X_train, Y_train) pred_cc = cc.predict(X_test) cc_score = jaccard_similarity_score(Y_test, pred_cc) cc_score
0.52203774760592925
예측을 만들 때는 체인 순서대로 첫 번째 분류기의 예측을 만들어 다음 분류기에서 특성으로 사용하고, 두 번째 분류기의 예측을 만들어 첫 번째 예측 결과와 함께 세 번째 분류기의 특성으로 사용하는 식으로 진행됩니다.
이번에는 random_state를 바꾸어 가면서 여러개의 ClassifierChain을 만들어 order=’random’일 때 미치는 영향을 살펴 보겠습니다. 다음 코드는 리스트 내포(list comprehension)를 사용하여 random_state 값을 바꾸면서 10개의 ClassifierChain을 만들고 자카드 점수를 계산하지만 기본적으로 위의 코드와 동일합니다.
chains = [ClassifierChain(LogisticRegression(), order='random', random_state=42+i) for i in range(10)] for chain in chains: chain.fit(X_train, Y_train) pred_chains = np.array([chain.predict(X_test) for chain in chains]) chain_scores = [jaccard_similarity_score(Y_test, pred_chain) for pred_chain in pred_chains]
[0.52203774760592925, 0.50759953430407978, 0.54071149809786168, 0.51879427390791022, 0.51900088547815826, 0.51148445792040831, 0.52014626787354057, 0.50362964056145876, 0.50333366128820667, 0.47443673750491933]
확실히 random_state에 따라 타깃값을 사용하는 순서가 바뀌기 때문에 성능이 들쭉 날쭉합니다. 다음 그래프에서 첫 번째 막대 그래프는 ovr_score이고 그 다음 10개의 그래프는 chain_scores입니다.
어떤 순서로 레이블을 사용할지 사전에 알 수 없으므로 ClassifierChain의 앙상블을 만들어 사용하는 것이 좋습니다. 앙상블을 만들 때는 각 분류기 체인의 값을 평균내어 사용합니다. 분류기 체인의 예측 결과를 앙상블하는 것보다 예측 확률을 앙상블하는 것이 조금 더 안정적인 결과를 제공할 것으로 기대할 수 있습니다. 로지스틱 회귀는 predict_proba() 메서드를 제공하므로 간단하게 체인의 확률값을 얻을 수 있습니다. 앙상블의 평균을 낸 후에 이를 예측값으로 바꾸어 Y_test와 유사도를 측정하려면 앙상블의 평균 확률이 0.5 보다 큰지를 판단하면 됩니다.
proba_chains = np.array([chain.predict_proba(X_test) for chain in chains]) proba_ensemble = proba_chains.mean(axis=0) ensemble_score = jaccard_similarity_score(Y_test, proba_ensemble >= 0.5)
0.51958792470156101
앙상블의 결과는 0.5196로 첫 번째 체인보다는 점수가 낮지만 가장 나쁜 체인의 점수에 비하면 비교적 높은 점수를 얻었습니다. 또 OneVsRestClassifier 보다 높은 점수를 얻었습니다. 다음 그래프의 첫 번째 막대 그래프는 ovr_score이고 그 다음 10개의 그래프는 chain_scores이며 마지막 막대 그래프가 앙상블의 결과입니다.
이 글의 샘플 코드는 ‘파이썬 라이브러리를 활용한 머신러닝‘ 깃허브(https://github.com/rickiepark/introduction_to_ml_with_python/blob/master/ClassifierChain.ipynb)에서 확인할 수 있습니다.
핑백: 분류기 체인: ClassifierChain | it 사람들의 오늘 이야기