태그 보관물: 0.19

분류기 체인: 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_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입니다.

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)에서 확인할 수 있습니다.

QuantileTransformer

Scikit-Learn 0.19 버전에 추가된 기능 중 오늘은 QuantileTransformer() 변환기를 살펴 보겠습니다. 먼저 ‘파이썬 라이브러리를 활용한 머신러닝‘ 3장에 있는 스케일 조정의 예제와 비슷한 데이터 셋을 make_blobs 함수로 만들겠습니다. 여기서는 샘플 개수가 어느 정도 되어야 눈으로 확인하기 좋으므로 500개를 만들겠습니다.

X, y = make_blobs(n_samples=500, centers=2, random_state=4)
plt.scatter(X[:, 0], X[:, 1], c=y, edgecolors='black')

quantile-1

QuantileTransformer 는 지정된 분위수에 맞게 데이터를 변환합니다. 기본 분위수는 1,000개이며 n_quantiles 매개변수에서 변경할 수 있습니다. 여기서는 100개 정도로 지정해 보겠습니다.

quan = QuantileTransformer(n_quantiles=100)

fit() 메서드에서 입력 데이터의 범위를 분위수에 맞게 나누어 quantiles_ 속성에 저장합니다. 이를 위해 넘파이 percentile() 함수를 사용하여 분위에 해당하는 값을 손쉽게 구할 수 있습니다. QuantileTransformer 에는 기본값이 100,000인 subsample 매개변수가 있습니다.  만약 입력 데이터 개수가 subsample 보다 크면 계산량을 줄이기 위해 subsample 개수만큼 데이터를 샘플링하여 percentile() 함수를 적용합니다. percentile() 함수는 특성마다 각각 적용되므로 quantiles_ 속성의 크기는 [n_quantiles, X.shape[1]] 이 됩니다.

quan.fit(X)
print(quan.quantiles_.shape)
(100, 2)

transform() 메서드에서는 데이터를 quantiles_를 기준으로 하여 0~1 사이로 매핑합니다. 이를 위해 넘파이 interp() 함수를 사용합니다. 두 개의 특성이 모두 0~1 사이로 균등하게 나뉘어진 것을 그래프로 확인할 수 있습니다.

X_quan = quan.transform(X)
plt.scatter(X_quan[:, 0], X_quan[:, 1], c=y, edgecolors='black')

quantile-2

이런 변환은 RobustScaler와 비슷하게 이상치에 민감하지 않게 됩니다. 하지만 균등 분포라서 무조건 [0, 1] 사이로 클리핑합니다.

사실 transform() 메서드에서는 scipy.stats.uniform.ppf() 함수를 사용하여 균등 분포로 변환합니다. 하지만 interp() 함수에서 동일한 변환을 이미 하고 있기 때문에 효과가 없습니다. QuantileTransformer 에서 output_distribution=’normal’ 로 지정하면 scipy.stats.norm.ppf() 함수를 사용하여 정규 분포로 변환합니다.

quan = QuantileTransformer(output_distribution='normal', n_quantiles=100)
X_quan = quan.fit_transform(X)
plt.scatter(X_quan[:, 0], X_quan[:, 1], c=y, edgecolors='black')

quantile-3

변환된 데이터는 평균이 0, 표준편차가 1인 정규 분포임을 확인할 수 있습니다.

X_quan.mean(axis=0), X_quan.std(axis=0)
(array([-0.00172502, -0.00134149]), array([ 1.0412595 ,  1.03818794]))

StandardScaler 와 비교해 보면 평균 과 표준편차는 같지만 정규 분포를 따르지 않는 분포에서 차이를 확인할 수 있습니다. 🙂

from sklearn.preprocessing import StandardScaler
X_std = StandardScaler().fit_transform(X)
plt.scatter(X_std[:, 0], X_std[:, 1], c=y, edgecolors='black')

quantile-4

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

New SAGA solver

scikit-learn 0.19 버전에서 추가된 기능 중 SAGA 알고리즘에 대해 살펴 보겠습니다. SAGA는 SAGStochastic Average Gradient 알고리즘의 변종, 혹은 개선 버전입니다(SAGA의 A가 특별한 의미가 있는 것은 아닙니다). SAG 알고리즘은 이전 타임스텝에서 계산된 그래디언트를 모두 평균내어 적용하는 알고리즘입니다. 기본 공식은 일반적인 SGDStochastic Gradient Descent과 비슷합니다. 다만 이전에 계산했던 그래디언트를 저장하고 다시 활용한다는 측면에서 SGD와 배치 그래디언트 디센트의 장점을 취하고 있습니다.

w^{k+1} = w^k - \dfrac{\alpha}{n} \left( f'_j(w^k)-f'_j(\theta_j^k)+\sum_{i=1}^{n}f'_i(\theta_i^k) \right)

첨자가 조금 장황해 보일 수 있지만 사실 특별한 내용은 아닙니다. 이전까지의 그래디언트를 모두 누적하고 있는 항이 \sum_{i=1}^{n}f'_i(\theta_i^k)입니다. 그리고 현재 스텝에서 구한 그래디언트를 f'_j(w^k) 더하되 혹시 이전에 누적된 것에 포함이 되어 있다면, 즉 랜덤하게 추출한 적이 있는 샘플이라면 이전의 그래디언트 값을 f'_j(\theta_j^k) 빼 줍니다. 혼돈을 줄이기 위해 현재의 스텝의 파라미터와 이전의 스텝의 파라미터를 w\theta로 구별하였습니다.

SAGA 알고리즘은 여기에서 과거 그래디언트에만 평균을 적용하는 방식입니다. 위 공식과 비교해 보시면 금방 눈치챌 수 있습니다.

w^{k+1} = w^k - \alpha \left( f'_j(w^k)-f'_j(\theta_j^k)+\dfrac{1}{n}\sum_{i=1}^{n}f'_i(\theta_i^k) \right)

SAGA 알고리즘은 Ridge, RidgeClassifier, LogisticRegression 등에 solver 매개변수를 ‘saga’로 설정하여 사용할 수 있습니다. 이 모델들은 대량의 데이터셋에서 SAG 알고리즘을 사용할 수 있었는데 SAGA가 SAG 보다 성능이 좋으므로 데이터셋이 클 경우 SAGA를 항상 사용하는 것이 좋을 것 같습니다. 그럼 예를 살펴 보겠습니다.

먼저 익숙한 cancer 데이터셋에서 로지스특회귀로 SAG와 SAGA를 비교해 보겠습니다.

logreg_sag = LogisticRegression(solver='sag', max_iter=10000)
logreg_sag.fit(X_train, y_train)
print("훈련 세트 점수: {:.3f}".format(logreg_sag.score(X_train, y_train)))
print("테스트 세트 점수: {:.3f}".format(logreg_sag.score(X_test, y_test)))
훈련 세트 점수: 0.927
테스트 세트 점수: 0.930

다음은 SAGA solver일 경우입니다.

logreg_saga = LogisticRegression(solver='saga', max_iter=10000)
logreg_saga.fit(X_train, y_train)
print("훈련 세트 점수: {:.3f}".format(logreg_saga.score(X_train, y_train)))
print("테스트 세트 점수: {:.3f}".format(logreg_saga.score(X_test, y_test)))
훈련 세트 점수: 0.920
테스트 세트 점수: 0.937

둘이 거의 비슷하지만 SAGA의 테스트 점수가 조금 더 좋습니다. 좀 더 큰 데이터셋에 적용해 보기 위해서 캘리포니아 주택 가격 데이터셋을 사용해 보겠습니다. 이 데이터셋은 8개의 특성을 가지고 있고 2만개가 넘는 샘플을 가지고 있습니다. 타깃값은 평균 주택 가격입니다. scikit-learn에 이 데이터를 다운 받아 로드할 수 있는 함수가 있습니다.

from sklearn.datasets import fetch_california_housing
housing = fetch_california_housing()

housing도 scikit-learn의 다른 샘플 데이터와 동일하게 Bunch 클래스의 객체입니다. 데이터를 분할하고 릿지 회귀를 사용하여 이전과 마찬가지로 ‘sag’와 ‘saga’를 비교해 보겠습니다.

X_train, X_test, y_train, y_test = train_test_split(
    housing.data, housing.target, random_state=42)
ridge = Ridge(solver='sag').fit(X_train, y_train)
print("훈련 세트 점수: {:.3f}".format(ridge.score(X_train, y_train)))
print("테스트 세트 점수: {:.3f}".format(ridge.score(X_test, y_test)))
훈련 세트 점수: 0.061
테스트 세트 점수: 0.062

이번에는 SAGA solver 입니다.

ridge_saga = Ridge(solver='saga').fit(X_train, y_train)
print("훈련 세트 점수: {:.3f}".format(ridge_saga.score(X_train, y_train)))
print("테스트 세트 점수: {:.3f}".format(ridge_saga.score(X_test, y_test)))
훈련 세트 점수: 0.035
테스트 세트 점수: 0.036

여기에서는 SAGA solver의 R2 스코어가 더 낮게 나왔네요. 🙂

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

참고 자료

scikit-learn 0.19 Release

파이썬의 대표적인 머신 러닝 라이브러리인 scikit-learn 0.19 버전이 릴리즈 되었습니다. 0.19 버전에는 여러가지 새로운 기능과 버그 수정들이 포함되었습니다. 대표적으로는 이상치 탐지를 위한 sklearn.neighbors.LocalOutlierFactor, 분위 값을 사용하는 sklearn.preprocessing.QuantileTransformer, 이진 분류기를 엮어 앙상블 시킬 수 있는 sklearn.multioutput.ClassifierChain,  교차 검증에서 훈련 세트와 테스트 세트의 점수를 모두 리턴해 주는 sklearn.model_selection.cross_validate가 추가되었습니다.

sklearn.decomposition.NMF의 solver 매개변수에 ‘mu'(Multiplicative Update)가 추가 되었고 sklearn.linear_model.LogisticRegression에 L1 규제를 사용한 SAGA 알고리즘의 구현인 ‘saga’ 옵션이 solver 매개변수에 추가되었습니다. 또 cross_val_score와 GridSearchCV, RandomizedSearchCV의 scoring 매개변수에 복수개의 스코어 함수를 지정할 수 있게 되었고 Pipeline 클래스에 memory 매개변수가 추가되어 그리드 서치 안에서 반복적으로 수행될 때 전처리 작업을 캐싱할 수 있게 되었습니다. 이 외에도 많은 버그가 수정되고 기능이 향상되었습니다. 자세한 내용은 릴리즈 노트를 참고하세요.

scikit-learn 0.19 버전은 pip 나 conda 를 이용하여 손쉽게 설치가 가능합니다.

$ conda install scikit-learn

$ pip install --upgrade scikit-learn