월간 보관물: 2018 2월

MLPClassifier의 다중 레이블 분류

이 포스트는 “나이브”님이 메일로 문의 주신 내용을 바탕으로 작성되었습니다. “파이썬 라이브러리를 활용한 머신러닝” 2.3.8절의 신경망에 소개된 MLPClassifier 모델이 다중 분류Multi-class Classification, 다중 레이블 분류Multi-label Classification가 가능한지에 대해 문의를 주셨습니다. 책을 보니 공교롭게도 예제가 모두 이진 분류로 나와 있네요. 🙂

MLPClassifier는 다중 분류, 다중 레이블 분류를 지원합니다. 일반적으로 신경망에서 다중 분류를 구현하려면 출력층의 뉴런을 2개 이상 놓아야 합니다. MLPClassifier 클래스는 겉으로 드러나 있지는 않지만 타깃 배열 y의 차원을 보고 출력 뉴런의 개수를 자동으로 결정합니다. 간단한 예를 만들어 확인해 보겠습니다. 먼저 책에서 사용한 moons 데이터셋을 억지로 다중 분류를 위한 데이터셋으로 변경해 보겠습니다. 즉1차원 배열인 타깃값 y를 (100, 2) 2차원 배열로 만들어 사용합니다.

print(Y_train[:10])
array([[ 0.,  1.],
        [ 0.,  1.],
        [ 1.,  0.],
        [ 0.,  1.],
        [ 0.,  1.],
        [ 1.,  0.],
        [ 0.,  1.],
        [ 1.,  0.],
        [ 1.,  0.],
        [ 0.,  1.]])

그런 다음 책의 예제와 동일한 옵션으로 신경망을 학습시켜 보겠습니다. 타깃을 2차원 배열로 변형시켰기 때문에 y_train이 아니라 Y_train 처럼 대문자를 사용했습니다.

mlp_multi = MLPClassifier(solver='lbfgs', random_state=0).fit(X_train, Y_train)

mlp_multi

이 그래프를 아래 이진 분류의 경우와 비교해 보면 결정 경계가 조금 다른 것을 확인할 수 있습니다.

mlp_binary

MLPClassifier의 기본값은 100개의 뉴런을 가진 은닉층 하나를 사용합니다. 그림 2-47과 같은 신경망 구조를 상상해 보면, 마지막 출력층과 은닉층 사이의 연결(가중치)이 출력층의 뉴런의 개수가 하나일때와 두 개일때 달라질 것이라는 것을 눈치챌 수 있습니다. 이런 차이 때문에 결정 경계가 조금 달라졌습니다. 하지만 우리가 사용한 샘플 데이터는 그렇게 조밀하지 않으므로 변화된 결정 경계에 영향을 받지 않아 테스트 점수가 동일합니다.

mlp_multi.score(X_test, Y_test)
0.88

이번에는 다중 레이블 분류를 위해 ClassifierChain 예제에서 사용했던 Yeast 데이터셋을 이용해 보겠습니다. 이 데이터의 타깃값은 확실히 다중 레이블입니다.

Y_train[:10]
array([[ 1.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  1.,
          0.],
        [ 1.,  1.,  0.,  0.,  0.,  0.,  0.,  1.,  1.,  0.,  0.,  0.,  0.,
          0.],
        [ 0.,  0.,  1.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  1.,
          0.],
        [ 1.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  1.,
          0.],
        [ 0.,  1.,  1.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  1.,
          0.],
        [ 1.,  1.,  0.,  0.,  0.,  1.,  1.,  1.,  0.,  0.,  0.,  1.,  1.,
          0.],
        [ 0.,  1.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
          0.],
        [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  1.,  0.,  0.,  0.,  0.,
          0.],
        [ 1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  1.,
          0.],
        [ 1.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  1.,
          0.]])

입력의 특성이 103개이므로 뉴런과 은닉층의 개수를 조금 늘려 보겠습니다(300, 100). 그리고 기본 solver인 Adam 알고리즘을 사용하므로 최대 반복횟수(max_iter)를 기본값인 200에서 크게 증가시켜 주었습니다.

mlp_multilabel = MLPClassifier(hidden_layer_sizes=(300,100), max_iter=10000, 
                               random_state=42).fit(X_train, Y_train)
mlp_multilabel.score(X_test, Y_test)
0.16115702479338842

분류 모델의 score 메서드는 다중 레이블 분류를 지원하지 않습니다. 즉 행의 전체 원소가 모두 정확히 맞았을 때를 카운트합니다. ClassifierChain에서 처럼 자카드 유사도를 사용할 수 있지만 여기서는 하나 원소라도 맞았을 때를 수동으로 확인해 보겠습니다.

Y_pred = mlp_multilabel.predict(X_test)
np.sum(np.sum(Y_test.astype(int) & Y_pred, axis=1) > 0)/Y_test.shape[0]
0.85330578512396693

이 코드는 예측(Y_pred)을 만들어 테스트 데이터(Y_test)의 각 원소에 대해 논리 곱(AND) 연산을 합니다. 즉 두 행렬의 같은 위치의 원소가 모두 True일 때만 True가 됩니다. 그리고 난 후 True의 개수가 0 보다 큰 행의 개수를 카운트했습니다. 테스트 세트의 85%는 최소한 하나의 레이블 이상 맞았네요. 🙂

 

이 글의 샘플 코드는 ‘파이썬 라이브러리를 활용한 머신러닝‘ 깃허브(https://github.com/rickiepark/introduction_to_ml_with_python/blob/master/MLP_Multilabel.ipynb)에서 확인할 수 있습니다.

분류기 체인: ClassifierChain

Scikit-Learn 0.19 버전에 추가된 기능으로 이번에 소개할 모델은 ClassifierChain입니다.

다중 레이블(Multi-Label) 문제를 직접 다룰 수 있는 모델도 있지만(가령, 랜덤 포레스트), 이진 분류기를 사용하여 다중 레이블 분류기를 구현하는 간단한 방법은 One-Vs-All 방식을 사용하는 것입니다. 이 예에서 사용할 데이터는 Yeast 데이터셋으로 2,417개의 샘플과 103개의 특성, 14개의 타깃 레이블(즉, 다중 레이블)을 가지고 있습니다.

import numpy as np
from sklearn.datasets import fetch_mldata

yeast = fetch_mldata('yeast')

X = yeast['data']
Y = yeast['target']

Y = Y.transpose().toarray()

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())
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입니다.

jaccard_bar1

어떤 순서로 레이블을 사용할지 사전에 알 수 없으므로 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이며 마지막 막대 그래프가 앙상블의 결과입니다.

jaccard_bar2

이 글의 샘플 코드는 ‘파이썬 라이브러리를 활용한 머신러닝‘ 깃허브(https://github.com/rickiepark/introduction_to_ml_with_python/blob/master/ClassifierChain.ipynb)에서 확인할 수 있습니다.

PyTorch 0.3.1 Release

파이토치PyTorch 0.3.1 버전이 릴리즈되었습니다. 주로 버그 수정과 성능 향상을 포함하고 있습니다. 자세한 변경사항은 릴리즈 노트를 참고하세요.

파이토치 0.3.1 부터는 CUDA 3.0, 5.0과의 호환성이 삭제되었고, CUDA 7.5를 위한 바이너리 설치가 제외되었습니다. 대신 CUDA 8, 9, 9.1용 바이너리 설치가 제공됩니다. 지원하는 파이썬 버전은 2.7, 3.5, 3.6 입니다.

# macOS (no GPU)
$ conda install pytorch torchvision -c pytorch

# Linux CUDA 8.0
$ conda install pytorch torchvision -c pytorch
# Linux CUDA 9.0
$ conda install pytorch torchvision cuda90 -c pytorch
# Linux CUDA 9.1
$ conda install pytorch torchvision cuda91 -c pytorch
# Linux CPU
$ conda install pytorch-cpu torchvision -c pytorch

지난 달 말에는 마이크로소프트의 CNTK 2.4 버전이 릴리즈 되었습니다. 자세한 사항은 릴리즈 노트를 참고하세요.

TensorFlow 1.6.0 RC0 Release

텐서플로 1.6.0 RC0 버전이 릴리즈되었습니다. 1.5.0 버전이 나온지 채 한달도 되지 않았는데 잉크가 마르기전이라는 말이 실감이 나네요. 이번 버전에는 이전에 예고한 대로 CPU 사용자들을 위한 AVX 명령이 활성화되어 바이너리가 제공됩니다. CUDA 9.0에 대한 버그 안내도 있네요. 조금 더 자세한 내용은 릴리즈 노트를 참고해 주세요. 1.6.0 RC0 버전은 다음과 같이 pip 명령으로 설치할 수 있습니다.

$ pip install --upgrade --pre tensorflow
$ pip install --upgrade --pre tensorflow-gpu

(업데이트) 텐서플로 1.6.0 RC1 버전이 릴리즈되었습니다.