以下の論文を読みます。
CatBoost…直訳すると「猫が励ます/後押しする」…なるほど、ツイッターなどをみると猫に励まされている方が大勢いますものね。
違うよ?
はい。この論文は CatBoost という新しい「勾配ブースティング(gradient boosting)」のツールについて解説するものであるようです。こ、勾配ブースティングとは? 勾配が励ますということですか? 猫ちゃんが励ましてくれるのではなく?
「弱学習器」を組み合わせて「強学習器」にする、ですか…でも、適当にその辺の弱い人々を100人集めてチームを組ませたところで藤井聡太七段に将棋で勝つことはできませんよね?
弱学習器に「訓練されていない」って意味はないからね!? ちゃんと訓練して!? というか弱学習器たちをどう訓練するべきかって手法が「勾配ブースティング」だからね。それでその勾配ブースティングでは、以下のように弱学習器 を足し上げて強学習器 にするらしい。各 ステップでは1時点前の強学習器 の残差 を埋めるような弱学習器 を得ようとするイメージだと紹介されているね。
- で与えられる がよい予測値になるような を得る。
- で与えられる がよい予測値になるような を得る。
- で与えられる がよい予測値になるような を得る。
- で与えられる がよい予測値になるような を得る。
え、えっと? 要するに、2人目以降は前の人までの判断のミスを埋めることを目指して学習するんですね? そうですね…例えばスパムメール分類器を学習するとして、5つの訓練データの正解ラベルを とでもしましょう。1人目が訓練データの特徴からこの正解ラベルを目指して学習した結果が であったとします。この場合、2人目は1人目のミスを埋めるように学習しなければならないので、2人目にとっての正解ラベルは ですね、おそらく(符号はなんとでもなるのでどちら向きでもいいですが)。さらに3人目は1人目と2人目の判断を最適に合体させたような強学習器のミスを埋めることを目指すことになります…でもこれ、ミスを埋めていくといわれると確かに強学習器がどんどん改善されていきそうですが、1時点前までの強学習器の損失っぷりが「弱学習器で埋められるようなもの」でなければなりませんよね…? 例えば個々の弱学習器が線形回帰モデルで損失関数が2乗誤差の和だったら、1人目ができる限り頑張って学習した時点で残差は平均ゼロのノイズであるはずです。平均ゼロのノイズに線形回帰モデルをフィッティングしても傾きもバイアスもゼロですから、この残差を2人目が埋め合わせようというのはできないのでは…?
1人目と2人目が参照する訓練データが全く同じなら損失が2乗誤差の線形回帰モデルではそうなりそうだね。でもロジスティック回帰による線形分類なら違いそうかな。ロジスティック回帰結果の真のラベルとの誤差を再びロジスティック回帰することはできそうだから。もっとも、こういう組み合わせて強くする学習に向いてるモデルの最たる例が「決定木」らしい。ウィキペディアの「勾配ブースティング」にも「勾配ブースティングは典型的には固定サイズの決定木を弱学習器として使用される」とあるね。
決定木? 何ですかそれは? ご神木から得た神託を予測値とするのですか?
違うかな。ウィキペディアに Decision tree learning という項目があるね。
上の記事の最初の図に決定木の例があるよ。"is sex male?" を根ノードにした木構造をしているね。これは「タイタニックの乗客が生存したか否か」の2値分類だ。各乗客の特徴として「性別」「年齢」「ともに乗船した配偶者または兄弟の人数」が与えられているらしい(この木で使用されていないだけで他の特徴もあるかもしれないけど)。各乗客は根ノードの質問から出発して、各ノードの質問の答えに応じていずれかの終端ノードに振り分けられる。緑色の終端ノードに振り分けられたら「生存」、赤色の終端ノードに振り分けられたら「死亡」と判定する。例えば女性の乗客なら真っ先に一番右の終端ノードに振り分けられて「生存」と判定されるけど、実際女性の乗客の生存率は 73% みたいだね。女性ならば「生存」といっておけば 73% は当たる。27% は外してしまうけど。というか図のたった3つの内部ノードだけで全乗客に対して 80% 以上の正解率が出るんだね( 0.36 * 0.73 + 0.02 * 0.89 + 0.02 * (1 - 0.05) + 0.61 * (1 - 0.17) = 0.8059 )。タイタニックの乗客で生存したのは雑にいうと「女性か、または9歳以下でかつともに乗船した兄弟が2人以下の男児」だったらしい。…それで、この図のような決定木をつくる方法だけど、まず根ノードでは最も説明力の高い特徴の閾値によって分岐させて、そこから先は分岐させたデータの中で最も説明力の高い特徴の閾値によって分岐させて…といった手続きによるみたいだね。そうすればなるべくバランスのよい木はつくれるけど、データを決定付けるよい閾値を拾いきれるとは限らない。だから、いくつかの異なる木を組み合わせたくなってくる。組み合わせる方法の1つは訓練データの1部ずつをつかっていくつも木をつくって多数決させるというもので、この手法は「バギング」といってランダムフォレストもこの一種だ。組み合わせる別の方法は、バギングのように並列に木を組み合わせるのではなくて、直列に組み合わせる。それぞれの木はそれまでの木のミスを補うように学習する。これが「ブースティング」で、その具体的な強力なアルゴリズムの一つが「勾配ブースティング」だね。
なるほど。先ほどの「勾配ブースティング」の手続きを、決定木を弱学習器として実行するわけですか。決定木であったとしても、各弱学習器 はそれまでの強学習器の残差を正解ラベルとして学習し、得られた を損失が最小になるような係数 で強学習器に足し合わせるというのは変わりませんね。
なんだけど、決定木の場合はもう少し改良できるってかいてある。決定木って特徴空間を直方体の領域に分割するよね。この直方体ごとに を変えても構わない。というか、 の領域分割さえわかれば、 が各領域に割り当てた値に興味もなくて、各領域にどれだけの定数を足し合わせれば強学習器の損失が最小になるかを探索するだけだね。
…確かに全ての領域について同じ係数で足してやらなければならないということもありませんね。新しい弱学習器は、特徴空間内の一部の領域の判定精度を向上させるが、その領域の外はむしろそれまでの強学習器のままでよく余計に何か足さないでほしい、ということもあるでしょうし。…しかし、こうウィキペディアだけ読んでいると雲をつかむようです。実際に何か決定木を学習してみることはできないでしょうか。
決定木というかむしろ勾配ブースティング決定木のパッケージはいくつもあるから、何か動かしてみればいいんじゃないかな。
そうですね、では xgboost というパッケージのR版をつかってみましょう(スクリプトは記事の下部)。データは…タイタニックのデータを拾ってくるのも面倒なので iris でいいです。木の数は指定できるのですね。しかし木を2つに指定しても6つできてしまうようですが…ああ、3クラス分類だから弱学習器1つにつき木が3つなのですね。面倒なので2クラスに絞りましょう。しかし、2つ目の弱学習器によって1つ目の弱学習器単体よりも分類精度がよくなる特徴と木の深さの組み合わせがなかなかないですね…特徴をがく片の幅と長さだけに絞り、木の深さを4にすると申し訳程度に訓練誤差が減少しますね。
[1] train-error:0.260000 [2] train-error:0.240000 pred y 0 1 0 34 16 1 8 42 Feature Gain Cover Frequency 1: Sepal.Length 0.94330762 0.7858799 0.6666667 2: Sepal.Width 0.05669238 0.2141201 0.3333333
こうなると特徴空間の分割もみたいよね。
特徴空間の分割ですか…これでどうでしょう。白丸が "versicolor"、黒丸が "virginica" のデータで、破線が各学習器の分割境界、青が濃いほど "versicolor" と判定、赤が濃いほど "virginica" と判定という意味です。赤と青の濃さは上のグラフの Value に比例しています。一番左の図が1つ目の弱学習器、真ん中の図が2つ目の弱学習器で、一番右の図は結果の強学習器です。2つの弱学習器の図を単純に重ね合わせたら赤と青が相殺しないのがいけてないですね…。
それどうやって描いたの??
上のグラフを見て自力で長方形を描きましたね…。
そっか…。
スクリプト
library(xgboost) max.depth <- 4 # 木の最大深さ nrounds <- 2 # 木の数 ### iris データの準備と特徴カラムと損失関数の定義 df <- data.frame(iris) # いきなり3クラス分類だとわかりづらいので setosa を除外して2クラス分類にする target_labels <- c("versicolor", "virginica") # "setosa" num_class <- 1 # 3 # 3クラス以上でない場合は1を指定する objective <- "binary:logistic" # "multi:softmax" df <- df[df$Species %in% target_labels, ] df$Species <- factor(df$Species, levels=target_labels) # クラスを絞ったので factor 型を修正 # 特徴も2つのみに絞る feats <- c("Sepal.Length", "Sepal.Width") # "Petal.Length", "Petal.Width" # 説明変数と目標ラベル x <- as.matrix(df[,feats]) y <- as.integer(df[,"Species"]) - 1 # 訓練する model <- xgboost(data=x, label=y, max.depth=max.depth, nrounds=nrounds, objective=objective, num_class=num_class, verbose=1) # 訓練したモデルの判定結果(訓練データ上) pred <- predict(model, x) if (length(target_labels) == 2) { # 2クラス分類にした場合は予測結果が 0~1 なので閾値で 0 or 1 に pred <- ifelse(pred < 0.5, 0, 1) } print(table(y, pred)) # 混同行列 # 各特徴の重要度を取得する imp <- xgb.importance(feats, model) print(imp) # 重要度 # モデルを図示 library(DiagrammeR) print(xgb.plot.tree(feature_names=feats, model=model)) print(xgb.dump(model))