静かなる名辞

pythonとプログラミングのこと

2019/03/22:TechAcademyがteratailの質問・回答を盗用していた件
2019/03/26:TechAcademy盗用事件 公式発表と深まる疑念


【python】sklearnのAdaBoostをデフォルトパラメータで使ってはいけない

はじめに

 sklearnのAdaBoostを使う機会がありましたが、デフォルトパラメータのまま使ってみたら性能が悪すぎて驚きました。

 対策を書きます。

症状

 とりあえずデフォルトパラメータで動かしてみて、様子を見るというシチュエーションはたくさんあると思います。しかし、AdaBoostはいまいちそういう用途には向いていません。

 何も考えずに使うとこんな感じです。

from sklearn.datasets import load_digits
from sklearn.ensemble import AdaBoostClassifier
from sklearn.metrics import classification_report
from sklearn.model_selection import train_test_split

def main():
    digits = load_digits()
    ada = AdaBoostClassifier()
    
    X_train, X_test, y_train, y_test = train_test_split(
        digits.data, digits.target, stratify=digits.target)
    ada.fit(X_train, y_train)
    pred = ada.predict(X_test)
    print(classification_report(y_test, pred))

if __name__ == "__main__":
    main()

""" => 
UndefinedMetricWarning: Precision and F-score are ill-defined and being set to 0.0 in labels with no predicted samples.
  'precision', 'predicted', average, warn_for)
              precision    recall  f1-score   support

           0       0.92      0.98      0.95        45
           1       0.13      1.00      0.23        46
           2       0.00      0.00      0.00        44
           3       0.00      0.00      0.00        46
           4       0.00      0.00      0.00        45
           5       0.00      0.00      0.00        46
           6       0.59      0.44      0.51        45
           7       0.00      0.00      0.00        45
           8       0.00      0.00      0.00        43
           9       0.50      0.24      0.33        45

   micro avg       0.27      0.27      0.27       450
   macro avg       0.21      0.27      0.20       450
weighted avg       0.21      0.27      0.20       450
"""

 さすがにひどい。どうしてなんでしょうね。

原因と対処

 ドキュメントをしっかり読んでみます。するとここが気になります。

base_estimator : object, optional (default=None)
The base estimator from which the boosted ensemble is built. Support for sample weighting is required, as well as proper classes_ and n_classes_ attributes. If None, then the base estimator is DecisionTreeClassifier(max_depth=1)

sklearn.ensemble.AdaBoostClassifier — scikit-learn 0.21.2 documentation

 深さ1の決定木はさすがに駄目なことが予想されます。特徴量を1つ取って真ん中で分けるだけか……

 ベース分類器をもっと優秀なものにしてあげます。とりあえず、max_depth=Noneにした(伸び放題の)決定木。

from sklearn.datasets import load_digits
from sklearn.ensemble import AdaBoostClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import classification_report
from sklearn.model_selection import train_test_split

def main():
    digits = load_digits()
    ada = AdaBoostClassifier(DecisionTreeClassifier(max_depth=None))
    
    X_train, X_test, y_train, y_test = train_test_split(
        digits.data, digits.target, stratify=digits.target)
    ada.fit(X_train, y_train)
    pred = ada.predict(X_test)
    print(classification_report(y_test, pred))

if __name__ == "__main__":
    main()

""" => 
              precision    recall  f1-score   support

           0       1.00      0.96      0.98        45
           1       0.73      0.89      0.80        46
           2       0.92      0.82      0.87        44
           3       0.89      0.85      0.87        46
           4       0.85      0.91      0.88        45
           5       1.00      0.85      0.92        46
           6       0.98      0.93      0.95        45
           7       0.87      0.89      0.88        45
           8       0.72      0.79      0.76        43
           9       0.82      0.82      0.82        45

   micro avg       0.87      0.87      0.87       450
   macro avg       0.88      0.87      0.87       450
weighted avg       0.88      0.87      0.87       450
"""

 大幅に改善。要するに、性能を上げたければベースを良くする必要があるということですね。

 わかったら、あとはパラメータチューニングして追い込みます。

from sklearn.datasets import load_digits
from sklearn.ensemble import AdaBoostClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import classification_report
from sklearn.model_selection import GridSearchCV, train_test_split

def main():
    digits = load_digits()
    ada = AdaBoostClassifier(n_estimators=100) # ついでに増やす
    params = {"base_estimator" : [DecisionTreeClassifier(max_depth=x) 
                                  for x in range(5, 10)],
              "learning_rate" : [0.5, 1.0, 1.5]
    }
    cv = GridSearchCV(ada, params, cv=3, n_jobs=-1, verbose=2)

    X_train, X_test, y_train, y_test = train_test_split(
        digits.data, digits.target, stratify=digits.target)
    cv.fit(X_train, y_train)
    print(cv.best_params_)
    pred = cv.predict(X_test)
    print(classification_report(y_test, pred))

if __name__ == "__main__":
    main()
""" =>
{'learning_rate': 1.0, 'base_estimator': DecisionTreeClassifier(class_weight=None, criterion='gini', max_depth=8,
            max_features=None, max_leaf_nodes=None,
            min_impurity_decrease=0.0, min_impurity_split=None,
            min_samples_leaf=1, min_samples_split=2,
            min_weight_fraction_leaf=0.0, presort=False, random_state=None,
            splitter='best')}
              precision    recall  f1-score   support

           0       1.00      1.00      1.00        45
           1       0.98      1.00      0.99        46
           2       1.00      0.98      0.99        44
           3       1.00      0.96      0.98        46
           4       1.00      1.00      1.00        45
           5       0.98      0.91      0.94        46
           6       1.00      0.98      0.99        45
           7       0.98      0.98      0.98        45
           8       0.91      1.00      0.96        43
           9       0.91      0.96      0.93        45

   micro avg       0.98      0.98      0.98       450
   macro avg       0.98      0.98      0.98       450
weighted avg       0.98      0.98      0.98       450

"""

 最大深さ8の決定木で、データの限界に近いであろう数字が出ました。これで満足しておきます。AdaBoostやればできる子ですね。

まとめ

 base_estimatorをいじって多少パラメータチューニングすれば性能が出るので、デフォルトのまま使って使えない子だと判断しないように。アダブーストは十分強いです。