仮想通貨の自動取引入門 ~機械学習の特徴量剪定とパラメータ調整~

Yuya Sugano
Nov 3 · 40 min read

仮想通貨に限らず自動取引は取引時に人手を介さず、リスクをコントロールしつつコストの削減およびリターンを機械的に追求することであると考える。前回の記事では、Ta-Libを使用したテクニカル指標を機械学習の特徴量としてその相関関係を確認し、層化抽出法による交差検証を行うことでモデルの性能を検証した。『分足のOHLCVデータから7個のテクニカル指標を抽出することで構築したモデルは終値変化率の3値分類問題に対して極めて高い性能を示す』と結論付けたが、本稿ではデータ処理において発見された誤りを示し、再度特徴量の選出と剪定を行うことと、各アルゴリズムにおけるパラメータ調整について考察してみたい。

Technical Indexes from OHLCV data btc/jpy

  • 自動取引とは(再掲)
  • データ処理における致命的な誤り
  • 機械学習モデルのパラメータ調整
  • 特徴量の選出と剪定
  • グリッドサーチの適用

自動取引とは

金融商品における自動取引とはシステムトレードの非裁量取引を、特にプログラムを用いて自動化しまた定例化することでリスクのコントロールもしくはリターンの追求を省力化した上で達成することであると考える。

システムトレードやアルゴリズム取引という語には使用する組織や文脈に応じて複数の意味があること、また意味の混ざることがあるためにここでは単純に自動取引という用語のみ使用する。[1]

アルゴリズムを使用しないものも含め、自動取引という用語の意味はより広義に捉えて問題ない。プログラムを介して、取引機会を発見し、それを自動化しているものを自動取引と呼びたいと思う。非裁量性や高頻度取引(High Frequency Trading)など様々な利点のある自動取引であるが、まず大分類としてコストの削減を目指すものとリターンの追求を実現するものの2種類がある。[2]


  • コストの削減
    ・取引コストの削減(執行系アルゴリズム、VAMPなどのベンチマーク系アルゴリズム)
    ・マーケット・メイクやバスケット取引などの定型的な業務の自動化による内部コストの削減
    ・マーケット・インパクトによるコストの削減
    ・最適な市場の選択をすることによる手数料の削減
  • リターンの追求
    ・収益機会の発見(裁定アルゴリズム、ディレクショナル・アルゴリズムなど)
    ・リベートの獲得(メイカー・テイカー手数料)
    ・スプレッド収益の発見(マーケット・メイキング・アルゴリズム)

個人投資家についてはよほど大口でない限り、リターン追求のために自動取引を導入すると考えられることから、ここでは収益機会の発見を達成する自動取引をプログラムでどのように記述していくかを考えていく。主に裁定アルゴリズム、ディレクショナルアルゴリズム、マーケット・メイキング・アルゴリズムを個人投資家は利用する。自動取引に限らないが以下のPDCAを回して行くことで(自動取引においては自動的に行うことが好ましい)、効率よく収益機会の発見および収益の発生を実現することが理想である。

1.情報の収集および整理
2.自動取引と約定の確認
3.バックテスト(ベンチマーク)


データ処理における致命的な誤り

仮想通貨の自動取引を行うにあたり、bitbank.cc APIから得られる分足のOHLCVデータの分析を行い、次点の終値が上昇するか下降するかを機械学習によってモデル化する試みを行ってきた。[1]

OHLCVデータから7種のテクニカル指標を特徴量を抽出することで 勾配ブースティングの機械学習のモデルを構築し、10分割の層化抽出法を用いた交差検証によってその性能の相加平均が99.9%以上になることを確認した。複数の日付のデータで全ての分足1440行からモデルを構築し、その性能が同等であることを確認できた。前回既に高い性能を示していたため、アルゴリズムのパラメータ調整やグリッドサーチは行っていなかった。

from sklearn.model_selection import cross_val_score, StratifiedKFold
cv = cross_val_score(pipe_gb, X_, y_.values.ravel(), cv=StratifiedKFold(n_splits=10, shuffle=True, random_state=39))
print('Cross Validation with StratifiedKFold mean: {}'.format(cv.mean()))Cross Validation with StratifiedKFold mean: 0.9993055555555556

実運用を行うにあたり致命的な欠陥が存在することに気が付いた。ラベル付けの計算の中では shift(-1)によって、1分後から現在までの終値の変化率で現在のラベルを付与している。同時に説明変数の diffにこの1分後から現在までの変化率をそのまま適用していた。ところがこの変化率は1分後の未来の終値が分からなければ計算できない。つまり diffは説明変数としてそのまま使用できず、 shift(-1)を伴わない現在の終値から1分前の終値の変化率としてしか diffと同様の説明変数は与えられないことになる。機械学習のモデリングには引き続き同じ計算手法を採用するが、説明変数・特徴量を再検討する必要がある。少なくとも現在計算している diff は未来の終値がなければ計算できないため使用できない。 diff を現在の終値から1分前の終値の変化率として構築した学習モデルのスコアを以下に示す。データは同様に10月17日のものである。正答率(Accuracy)は他の日次データにおいてもそれほど違いはなかった。層化抽出法を用いた10分割交差検証による結果の相加平均が0.74程度である。

(1440, 5)
Index(['open', 'high', 'low', 'close', 'volume'], dtype='object')
----------------------------------------------------------------------------------------
X shape: (1440,5)
y shape: (1440,1)
----------------------------------------------------------------------------------------
y
-1 194
0 1041
1 205
dtype: int64
y=1 up, y=0 stay, y=-1 down
----------------------------------------------------------------------------------------
X_train shape: (964, 7)
X_test shape: (476, 7)
y_train shape: (964, 1)
y_test shape: (476, 1)
KNN Train Accuracy: 0.834
KNN Test Accuracy: 0.679
KNN Train F1 Score: 0.834
KNN Test F1 Score: 0.679
Logistic Train Accuracy: 0.733
Logistic Test Accuracy: 0.725
Logistic Train F1 Score: 0.733
Logistic Test F1 Score: 0.725
RandomForest Train Accuracy: 0.987
RandomForest Test Accuracy: 0.679
RandomForest Train F1 Score: 0.987
RandomForest Test F1 Score: 0.679
GradientBoosting Train Accuracy: 0.900
GradientBoosting Test Accuracy: 0.702
GradientBoosting Train F1 Score: 0.900
GradientBoosting Test F1 Score: 0.702
KNN Confusion Matrix
[[ 17 46 8]
[ 28 295 18]
[ 10 46 8]]
Logistic Confusion Matrix
[[ 2 63 6]
[ 1 338 2]
[ 2 57 5]]
RandomForest Confusion Matrix
[[ 11 48 12]
[ 17 306 18]
[ 9 43 12]]
GradientBoosting Confusion Matrix
[[ 17 44 10]
[ 7 317 17]
[ 6 47 11]]
Cross Validation with StratifiedKFold mean: 0.7401376469538963

同様に各アルゴリズムにおける混合行列を図示した。

Confusion Matrix for 4 algorithms

以下はテスト用データセットから予測した結果のラベル『1』に対するROC曲線で、AUC値は0.76であった。

Receiver operating characteristic for label 1

モデルの性能が悪いため改善する必要があるが、このケースで取れる対策は以下の3つである。3つのうちアルゴリズムの変更は、サポートベクターマシンやXG Boostなど、今まで使用していないアルゴリズムを導入するか、スタッキングなどの手法を使用することが考えられる。アルゴリズムの変更などによる大幅なスコア値の改善が見られることは考えにくい。従って本稿では説明変数を変更することとモデルを最適化することに焦点を当てていきたい。

  • アルゴリズムを変更する
  • 説明変数を変更する(次元削減などの処理も含める)
  • モデルを最適化する(パラメータ調整など)

次節ではまず構築した機械学習モデルのパラメータ調整を行ってみる。


機械学習モデルのパラメータ調整

モデルが高い性能を示していたため、当初パラメータ調整やグリッドサーチによるパラメータの探索は行っていなかった。アルゴリズムのハイパーパラメータを変更することで性能の改善が行えないか検討する。各4つのアルゴリズムの正答率のスコア結果に注目すると、ロジスティック回帰以外は、訓練データに対する当てはまりが強いことに反してテストデータへの予測結果が弱い。

KNN Train Accuracy: 0.834
KNN Test Accuracy: 0.679
Logistic Train Accuracy: 0.733
Logistic Test Accuracy: 0.725
RandomForest Train Accuracy: 0.987
RandomForest Test Accuracy: 0.679
GradientBoosting Train Accuracy: 0.900
GradientBoosting Test Accuracy: 0.702

ロジスティック回帰はこのデータに対して汎化性能が高いように見えるが、その他のアルゴリズムは汎化しているとは言い難い。従ってパラメータ調整によって性能を向上させる余地があると考える。まずはパラメータを調整することでどの程度汎化性能を高められるかを確認する。パラメータ調整によって汎化性能を高められる可能性があるモデルについて、後半にグリッドサーチを用いたscikit-learnの各パラメータの網羅的な走査を試すこととする。


  • k近傍法

k近傍法において最近傍点である n_neighbors の変更を検討した。1つの近傍点を使用した n_neighbors=1 の場合は、決定境界は訓練データ近くになる。逆に多くの近傍点を使用した場合には、決定境界が滑らかになることが知られている。scikit-learnにおいて n_neighbors でデフォルト値は3であるが、この値を1から11まで変化させたときに、訓練データとテストデータに対するスコアがどのように変化するかを図示した。下図より n_neighbors=6 のときに最もスコアが高くなることが分かるが、スコア自体はそれほど高くない。

KNeighbor Classification n_neighbors number
# KNeighbor classifier parameter
knn_training_accuracy = []
knn_test_accuracy = []
neighbors_settings = range(1, 11)
for n_neighbors in neighbors_settings:
knn = Pipeline([('scl', StandardScaler()), ('est', KNeighborsClassifier(n_neighbors=n_neighbors))])
knn.fit(X_train, y_train.values.ravel())
knn_training_accuracy.append(knn.score(X_train, y_train.values.ravel()))
knn_test_accuracy.append(knn.score(X_test, y_test.values.ravel()))

plt.figure(figsize=(10,5))
plt.plot(neighbors_settings, knn_training_accuracy, label="training accuracy")
plt.plot(neighbors_settings, knn_test_accuracy, label="test accuracy")
plt.ylabel("Accuracy")
plt.xlabel("n_neighbors")
plt.legend()

  • ロジスティック回帰

ロジスティックの結果は訓練データとテストデータに対するスコアが近すぎるように見える。つまり適合不足に陥っているかもしれない。scikit-learnのロジスティック回帰はデフォルトでL2正則化、ハイパーパラメータの c=1.0 として計算される。パラメータの値を大きくして正則化を弱めた場合と、小さくして逆に正則化を強め汎化能力を高めた場合を確認した。

汎化能力を高めるために c=0.01 とした場合のテストデータに対するスコアが、 c=100 の場合のテストデータに対するスコアと比べると相対的に悪化している。正則化をあまり強めないモデル(Cは大きな値)のほうがロジスティック回帰においては良さそうである。両方ともスコアとしては勾配ブースティングの値を超えている。

Training set score C=100: 0.733
Test set score C=100: 0.725
Training set score C=0.01: 0.729
Test set score C=0.01: 0.718

次に各ラベルに対して学習された係数を見ていく。正則化を強めた場合には、係数は0へ近づいていくが、L2正則化の場合には0とはならない。また興味深いのはラベルによっても係数の寄与度が異なることである。 c=0.01 におけるrsi/adx/adの係数はラベル0の c=0.01 でプラス方向へ転じている。ラベルによって同じ特徴量であっても係数の影響度が異なっている。

Coefficient Magnitude for 3 classes
# Logistic Regression parameter
pipe_logistic100 = Pipeline([('scl', StandardScaler()), ('est', LogisticRegression(C=100, solver='lbfgs', multi_class='multinomial', random_state=39))])
pipe_logistic001 = Pipeline([('scl', StandardScaler()), ('est', LogisticRegression(C=0.01, solver='lbfgs', multi_class='multinomial', random_state=39))])
pipe_logistic100.fit(X_train, y_train.values.ravel())
pipe_logistic001.fit(X_train, y_train.values.ravel())
print("Training set score C=100: {:.3f}".format(pipe_logistic100.score(X_train, y_train.values.ravel())))
print("Test set score C=100: {:.3f}".format(pipe_logistic100.score(X_test, y_test.values.ravel())))
print("Training set score C=0.01: {:.3f}".format(pipe_logistic001.score(X_train, y_train.values.ravel())))
print("Test set score C=0.01: {:.3f}".format(pipe_logistic001.score(X_test, y_test.values.ravel())))
print('Coefficiency for each label: {}'.format(pipe_logistic.named_steps['est'].coef_))
print('Classes for the model: {}'.format(pipe_logistic.named_steps['est'].classes_))
plt.figure(figsize=(15,8))
plt.plot(pipe_logistic.named_steps['est'].coef_[0].T, 'o', label='C=1, Label=-1')
plt.plot(pipe_logistic.named_steps['est'].coef_[1].T, 'o', label='C=1, Label=0')
plt.plot(pipe_logistic.named_steps['est'].coef_[2].T, 'o', label='C=1, Label=1')
plt.plot(pipe_logistic100.named_steps['est'].coef_[0].T, '^', label='C=100, Label=-1')
plt.plot(pipe_logistic100.named_steps['est'].coef_[1].T, '^', label='C=100, Label=0')
plt.plot(pipe_logistic100.named_steps['est'].coef_[2].T, '^', label='C=100, Label=1')
plt.plot(pipe_logistic001.named_steps['est'].coef_[0].T, 'v', label='C=0.01, Label=-1')
plt.plot(pipe_logistic001.named_steps['est'].coef_[1].T, 'v', label='C=0.01, Label=0')
plt.plot(pipe_logistic001.named_steps['est'].coef_[2].T, 'v', label='C=0.01, Label=1')
plt.xticks(range(X_.shape[1]), X_.columns, rotation=90)
plt.hlines(0, 0, X_.shape[1])
plt.ylim(-0.5, 0.5)
plt.xlabel("Feature")
plt.ylabel("Coefficient magnitude")
plt.legend()

ロジスティック回帰ではL1正則化を指定することもできる。L1正則化では線形関数の係数が0になる可能性があるので、その場合はモデルの解釈が容易となる。L2正則化の代わりにL1正則化で学習したモデルに対する係数を図示した。正則化を強め汎化を高めた場合には、係数が0となっていることが確認できる。

Training set score C=100: 0.733
Test set score C=100: 0.725
Training set score C=0.01: 0.728
Test set score C=0.01: 0.714
Coefficiency for each label: [
[ 0.01515947 0.01119413 0.41772055 -0.04584187 -0.05147724 0.
-0.47591533]
[-0.51789993 0. 0. 0. 0. 0.
0. ]
[ 0. -0.18761607 -0.21209732 0. 0.27883685 0.27347487
0.02747878]
]
Classes for the model: [-1 0 1]
Coefficient Magnitude for 3 classes with L1 penalty

  • ランダムフォレスト

決定木の欠点である過剰適合が個々の決定木における訓練データへの当てはまりを高めているように見えた。決定木の数は n_estimators で与えられるのでこのパラメータ値を動かした場合に、スコアがどのように変動するかを観察する(1から50まで1ずつ木の数を変更する)。特徴量のサブセットを指定する max_features は、このモデルにおいて最大値である7のとき最もスコアが良かったため、 max_features は7で固定した。

ランダムフォレストは訓練データの適合度が高かったため、汎化性能を上昇させられる期待があったが、以下のとおりテストデータのスコアから、思ったような汎化性能の向上は得られなかった。

Randomforest Classification n_estimators number
# Random forest classifier parameter
rf_training_accuracy = []
rf_test_accuracy = []
rf_settings = range(1, 50)
for n_estimators in rf_settings:
rf = Pipeline([('scl', StandardScaler()), ('est', RandomForestClassifier(n_estimators=n_estimators))])
rf.fit(X_train, y_train.values.ravel())
rf_training_accuracy.append(rf.score(X_train, y_train.values.ravel()))
rf_test_accuracy.append(rf.score(X_test, y_test.values.ravel()))

plt.figure(figsize=(15,8))
plt.plot(rf_settings, rf_training_accuracy, label="training accuracy")
plt.plot(rf_settings, rf_test_accuracy, label="test accuracy")
plt.ylabel("Accuracy")
plt.xlabel("n_estimators")
plt.legend()

各特徴量の重要度はモデルに対する feature_importances_ で得られる。特徴量の重要度は合計で1.0となる値で、値が大きいほうが重要となる。重要であるとはラベルが正例なのか負例(いまは3値分類問題だが)なのかをを示しているわけではない点は注意が必要である。

Feature importances with n_estimator=10

  • 勾配ブースティング

弱学習機を順番に作成し、次の学習機である決定木がその誤りを修正する形でモデルを構築していく。調整されるパラメータとしては、ランダムフォレストと同様に決定木の数である n_estimators と、個々の決定木の過ちをどれぐらい強く補正するかを制御する learning_rate が挙げられる。

勾配ブースティングにおいても、ランダムフォレストと同様に n_estimators のパラメータ値を動かした場合に、スコアがどのように変動するかを観察する(1から50まで1ずつ木の数を変更する)。 learning_rate はデフォルト値である0.1を使用している。値を低減させたほうが、モデルの複雑度は低下し汎化性能が高まることがある。 n_estimators を増加させると訓練データへの適合は上昇するが、テストデータに対する性能は逆に低下することが判明した。

Gradientboost Classification n_estimators number
# GradientBoost classifier parameter n_estimators
gb_training_accuracy = []
gb_test_accuracy = []
gb_settings = range(1, 50)
for n_estimators in gb_settings:
gb = Pipeline([('scl', StandardScaler()), ('est', GradientBoostingClassifier(n_estimators=n_estimators))])
gb.fit(X_train, y_train.values.ravel())
gb_training_accuracy.append(gb.score(X_train, y_train.values.ravel()))
gb_test_accuracy.append(gb.score(X_test, y_test.values.ravel()))

plt.figure(figsize=(15,8))
plt.plot(gb_settings, gb_training_accuracy, label="training accuracy")
plt.plot(gb_settings, gb_test_accuracy, label="test accuracy")
plt.ylabel("Accuracy")
plt.xlabel("n_estimators")
plt.legend()

各特徴量の重要度がランダムフォレストと大きく異なる。ここでは diffおよび volumeの重要度が際立っているように見える。ロジスティック回帰やランダムフォレストの結果からも言えるが、使用するアルゴリズムやそのパラメータ値によって説明変数のラベル判定への寄与度は異なっている。原因として指定した特徴量が想定するラベルに対してロバストなデータでないことが考えられる。使用する特徴量がラベルを説明できないのであれば、次元削減や特徴量の交互作用を加えることにもあまり意味はない。そこで次節では特徴量自体を見直すことにする。前回同様にTa-Libからテクニカル指標を複数計算し、モデルを上手に説明する特徴量の剪定を機械学習のモデルを用いて実施する。

Feature importances with n_estimator=10

特徴量の選出と剪定

適用したアルゴリズムに対してパラメータの調整を試みたが、汎化能力が十分に上昇しなかった。これまでTa-Libを用いて投資家に比較的よく使用されるテクニカル指標を特徴量として算出したが、この特徴量が被説明変数を説明していないことが問題である。今回はテクニカル指標を、可能な限り多く計算し、逆に自動特徴量選択によってより汎化能力が高くなるような特徴量を選出し剪定(プルーニング)する。それでも汎化能力に上昇がみられない場合は、交互作業特徴量、多項式特徴量を加えることも検討したい。

Ta-Libはテクニカル指標を計算する関数群が10種類ある。それぞれに相当数のテクニカル指標がグルーピングされていることが確認できる。今回は全てのグループのテクニカル指標を可能な限り計算し、特徴量として加た。[2]

個別の指標は列挙しないが、Overlap Studiesから14個、Momemtum Indicatorsから19個、Volume Indicatorsから3個、Volatility Indicatorsから3個、Price Transformから4個、Cycle Indicatorsから2個、Statistic Functionsから6個の51個のテクニカル指標と、現在と1分前の終値の差分である diff 、OHLCVの元データにあった volume の計53個を特徴量として選出した。

ta-lib function groups

データフレームとして10行表示した。ラベルである y_を加えて54列のデータフレームになっていることが分かる。

Technical Indexes from OHLCV data btc/jpy

https://gist.github.com/yuyasugano/aa1510b82efbcff11a2a3281522c07e2

パラメータ調整を加えずにこの53個の特徴量を使用して、4つのアルゴリズムでモデリングを行うと以下の結果が得らる。いずれも先に採用していた7個の特徴量による機械学習モデルのスコアよりも劣っている。

KNN Train Accuracy: 0.821
KNN Test Accuracy: 0.660
KNN Train F1 Score: 0.821
KNN Test F1 Score: 0.660
Logistic Train Accuracy: 0.744
Logistic Test Accuracy: 0.710
Logistic Train F1 Score: 0.744
Logistic Test F1 Score: 0.710
RandomForest Train Accuracy: 0.988
RandomForest Test Accuracy: 0.649
RandomForest Train F1 Score: 0.988
RandomForest Test F1 Score: 0.649
GradientBoosting Train Accuracy: 0.930
GradientBoosting Test Accuracy: 0.672
GradientBoosting Train F1 Score: 0.930
GradientBoosting Test F1 Score: 0.672

53個の特徴量のほとんどは意味をなさないと考えて、有用な特徴量を残し必要のない残りを切り捨てたい。特徴量を何らかの基準に基づいて選択する技法を特徴量選択と呼び、scikit-learnには教師あり学習の手法に対して、自動的に特徴量を選択する自動特徴量選択のライブラリがある。scikit-learnに実装されているものの中では、基本的な戦略として以下の3種類の使用が考えられる。[3]

  • 単変量統計(Univariate Statistics)
  • モデルベース選択(Model-based Selection)
  • 反復選択(Iterative Selection)

scikit-learnの自動特徴量選択は、機械学習のモデルに基づいておりこれらは特徴量選択の手法の中で一般的にラッパー法(Wrapper Method)と呼ばれる。


単変量統計(Univariate Statistics)

個々の特徴量に対して(単変量)統計的な関係があるかどうかを計算する手法である。個別の特徴量とラベルとの関係しか考慮されないため、交互作用など複数の特徴量の組み合わせに対する効果は得られない。残す特徴量の数を指定する SelectKBest があるが、ここでは残す特徴量の割合数を設定する SelectPercentile を使用する。

アルゴリズムは勾配ブースティングを採用し、25%の特徴量を残す設定をしたところ13種類が選択された。以下は、単変量統計を使用した場合と、使用しない場合におけるスコアだが、単変量統計を使用した場合のほうが多少汎化性能に上昇が見られる。

------------------Without Univariate Statistics---------------------
Train Accuracy: 0.930
Test Accuracy: 0.672
Train F1 Score: 0.930
Test F1 Score: 0.672
--------------------With Univariate Statistics----------------------
Train Accuracy: 0.895
Test Accuracy: 0.674
Train F1 Score: 0.895
Test F1 Score: 0.674
Univariate Statistics

モデルベース選択(Model-based Selection)

教師あり学習モデルを1つ用いて個々の特徴量の重要性を判断し、重要と判断されたものを残す手法である。決定木の特性を説明する feature_importances_ や、L1正則化を使用した線形モデルによって一部の係数を0にすることで特徴量を自動選択する評価をする。単変量統計の場合とは異なり、モデルベース選択は全ての特徴量を同時に考慮するため、説明変数間の交互作用を捉えることができる。ここでは SelectFromModel を使用する。

特徴量選択のアルゴリズムとしてランダムフォレストを使用し、しきい値として1.25*mean以上の重要度を持つ特徴量を残す設定をしたところ15種類が選択された。選択された特徴量に対して単変量統計の場合と同様に、勾配ブースティングでモデルを構築したところ以下の結果が得られた。スコアは単変量統計による選択と大差ない。

-------------------Without Model-based Selection--------------------
Train Accuracy: 0.930
Test Accuracy: 0.672
Train F1 Score: 0.930
Test F1 Score: 0.672
--------------------With Model-based Selection----------------------
Train Accuracy: 0.916
Test Accuracy: 0.674
Train F1 Score: 0.916
Test F1 Score: 0.674
Model-based Selection

反復選択(Iterative Selection)

モデルベース選択では、指定したモデルを1つ使用して特徴量を自動選択した。反復選択では、異なる特徴量を用いた一連のモデルを作成して、そこから重要度の高い特徴量を選択していく手法である。特徴量を使用しないところから、特徴量を積み上げていくやり方と、全特徴量を使用する状態から1つずつ重要度の低い特徴量を減らしていくやり方があるが、ここでは後者の方法の1つである再帰的特徴量削減(Recursive Feature Elimination)を適用する。

特徴量選択のアルゴリズムとしてランダムフォレストを使用し、15個の特徴量を残す設定を行った。選択された特徴量に対して単変量統計の場合と同様に、勾配ブースティングでモデルを構築したところ以下の結果が得られた。単変量統計の場合と同程度ではあるが汎化能力が上昇しているように見える。

------------------Without Recursive Feature Elimination-------------
Train Accuracy: 0.930
Test Accuracy: 0.672
Train F1 Score: 0.930
Test F1 Score: 0.672
-------------------With Recursive Feature Elimination---------------
Train Accuracy: 0.916
Test Accuracy: 0.674
Train F1 Score: 0.916
Test F1 Score: 0.674
Recursive Feature Elimination

53種の特徴量をテクニカル指標から算出し、特徴量選択によって最適な特徴量を選択することを試みたが、汎化性能が十分に確保できているとは言い難い。7個の特徴量に勾配ブースティングを適用し、交差検証を行った0.74というスコアのほうが数値としては勝っている。そこで自動特徴量選択は一旦止め、元の7個の特徴量から勾配ブースティングで使用するパラメータを最適化する方向へ舵を切る。


グリッドサーチの適用

汎化性能を向上させその性能評価を行うために、scikit-learnではメタEstimatorの形でグリッドサーチの手法を実装したGridSearchCVクラスが用意されている。pythonのディクショナリを用いて探索したいハイパーパラメータのCやgamma、alphaを設定することで、パラメータの組み合わせのモデル総当たりで学習した精度を評価できる。基本的に総当たりで順番にモデル構築と評価を行っていくだけなので、採用するモデルやデータセットの大きさ、交差検証の指定によってコンピューティングに大きな計算資源を消費する点は注意が必要である。今回は勾配ブースティングにおいて影響の大きいと考えられる n_estimatorslearning_ratemax_depth のパラメータをグリッドサーチで探索してみた。

param_grid のキーにアルゴリズムのパラメータ名を、値に試したい値をリストで入力してディクショナリを作る。 learning_rate に4つ、 1n_estimatorsに5つ、 max_depthに5つのパラメータを設定し、4×5×5の計100個のモデルを総当たりで検証する。また cvというオプションをグリッドサーチで指定することで、訓練データに対する交差検証の分割器を変更することができる。 stratifiedcvという変数を作成し層化10分割の交差検証分割器をグリッドサーチへ渡した。したがって各モデルにおいて10回の交差検証が行われるため、100個のモデルと10回の検証となり、計1000通りのモデルを評価する設定となった。グリッドサーチにおいて fit メソッドは、最適なパラメータ探索と、訓練データに対するモデルの学習を行ってくれる。作成したインスタンスに対して今までと同様に predictscore の呼び出しが可能である。

# GridSearch
from sklearn.model_selection import GridSearchCV
n_features = len(df.columns)
param_grid = {
'learning_rate': [0.01, 0.1, 1, 10],
'n_estimators': [1, 10, 100, 200, 300],
'max_depth': [1, 2, 3, 4, 5]
}
stratifiedcv = StratifiedKFold(n_splits=10, shuffle=True, random_state=39)
X_train, X_test, y_train, y_test = train_test_split(X_, y_, test_size=0.33, random_state=42)
grid_search = GridSearchCV(GradientBoostingClassifier(), param_grid, cv=stratifiedcv)
grid_search.fit(X_train, y_train.values.ravel())
print('GridSearch Train Accuracy: {:.3f}'.format(accuracy_score(y_train.values.ravel(), grid_search.predict(X_train))))
print('GridSearch Test Accuracy: {:.3f}'.format(accuracy_score(y_test.values.ravel(), grid_search.predict(X_test))))
print('GridSearch Train F1 Score: {:.3f}'.format(f1_score(y_train.values.ravel(), grid_search.predict(X_train), average='micro')))
print('GridSearch Test F1 Score: {:.3f}'.format(f1_score(y_test.values.ravel(), grid_search.predict(X_test), average='micro')))

コードを実行した結果は以下の通りで、テストデータに対する汎化能力がわずかだが上昇している(0.702より0.01の上昇)。

GridSearch Train Accuracy: 0.769
GridSearch Test Accuracy: 0.712
GridSearch Train F1 Score: 0.769
GridSearch Test F1 Score: 0.712

発見された最適パラメータはbest_params_で、またそのパラメータに対する交差検証のベストスコアはbest_score_に格納されている。

print("Best params:\n{}".format(grid_search.best_params_))
print("Best cross-validation score: {:.2f}".format(grid_search.best_score_))
Best params:
{'learning_rate': 0.01, 'max_depth': 2, 'n_estimators': 300}
Best cross-validation score: 0.74

さらに最適と判断されたEstimatorはbest_estimator_属性でアクセスができる。ただし前述したとおり predictscore メソッドが実装されているため、予測や評価についてbest_estimator_を用いる必要はない。

best_params_やbest_score_の結果で注意が必要な点は、ここに格納されている値が、訓練データに対する交差検証を用いたモデルの評価結果である点である。best_score_に格納されている値は、訓練データに対する交差検証の平均交差検証精度となる。訓練データとテストデータの分割を複数回実施して、それぞれの分割に対してグリッドサーチを適用する手法を『ネストした交差検証』と呼ぶ。ネストした交差検証では、1次段階のループでデータを訓練データとテストデータに分割し、2次段階でそれぞれの分割に対してグリッドサーチを行う。各グリッドサーチの結果について、最適なパラメータによるモデルをテストセットに適用し、評価した結果が報告される。

ネストした交差検証は、前回層化抽出法を用いたk分割交差検証で使用した cross_val_score に、グリッドサーチのインスタンスを渡すだけで実行できる。また cv に交差検証の分割器のインスタンスを渡すことができる。分類問題の場合はデフォルトで層化抽出法のk分割交差検証でここでは cv=3 を指定した。2次段階のグリッドサーチが1種の訓練データとテストデータの分割に対して計1000通りのモデルを構築し、1次段階の3分割交差検証でこのセットが3回繰り返される。したがってこの処理には3000個のモデル評価と同様の計算資源が必要である。

from sklearn.model_selection import cross_val_score, StratifiedKFold
cv_gb = cross_val_score(grid_search, X_, y_.values.ravel(), cv=StratifiedKFold(n_splits=3, shuffle=True, random_state=39))
print('Grid Search with nested cross validation scores: {}'.format(cv_gb))
print('Grid Search with nested cross validation mean: {}'.format(cv_gb.mean()))

『ネストした交差検証』はpythonで以下のように記述できる。

Nested Cross-validation with iris data-set

今回実装したコードは以下のリポジトリへ保存した。[5]

https://github.com/yuyasugano/ml-classifier-Ta-Lib-grid

以上から、特徴量の候補数を増やし自動特徴量選択によって特徴量を剪定する手法はテクニカル指標にはそれほど有効でないことが判明した。元の特徴量と勾配ブースティングのモデルに対してグリッドサーチを行うことで汎化性能がわずかながら上昇することが確認できた。スタッキングを用いてこのモデル評価精度にさらに磨きをかけることもできるかもしれないが、特徴量やラベリングなど前提を見直したほうがより良い問題設定となるかもしれない。


Yuya Sugano

Written by

@HashHub_Tokyo — 機械学習講座メンター、ブロックチェーン・セキュリティ関連の記事を執筆。バックパッカーとしてユーラシア大陸を陸路横断するなど旅が趣味。Vinyl DJ, Backpacker, Technology Orchestrator. https://twitter.com/SuganoYuya

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade