はじめに
みなさま、こんにちは @cp20 です。最近暑い日とちょっと寒い日があって体を壊しやすいですね。お体には気を付けましょう、本当に。
最近Kaggle班による機械学習講習会 (全7回) がありまして、それの一環として (比較的) 簡単なコンペティションが開催されました。そのコンペで自分たちのチーム「ZeroWidthSpace」が優勝したので、その解法を含めた参加記的なことを書いていこうと思います。
初心者の集まりのチームなのであんまりすごいことはやってないですが (予防線) コンペに参加した人を満足させられるぐらいの内容になっていれば嬉しいです。
やったこと
@ayana
正直最初は何も分かっていなくて、第6回の講習会資料をコピペしてsubmitファイルを作るので必死でした。(途中できなすぎてコード書くのを諦めて二人のやっていることを理解するために単語等を調べるフェーズに入ったりしていました...)最初のSubmitはほとんど第6回の講習会のコードをそのまま使い、スコアは0.96ちょっとくらいだったと思います。そのあと、@kantoku くんに教えてもらって活性化関数をReLUに変えたのと欠損値を-1で埋めるようにしたところ、0.97ちょっとくらいまで上がりました。
途中まで NN で頑張っていたのですが、@cp20 さんから LightGBM が素晴らしいと聞いてやってみたくなったので、一旦 @cp20 さんのコードをお借りして手元で実行しました。真似したから当たり前なんですが、高いスコア(0.98646)が出て嬉しくなって、自分の力でも改善したいと思い、ネットやDiscussionを参考にしながら改善ポイントを探したりしたのですが、なかなかうまく改善できず...。一旦他の方法も試してみようと、XGBoostとかも調べて試してみたりもしましたが精度がそんなに高く出なかった(0.96ちょっとくらい)のでこれは諦めちゃいました。このあたりからハイパーパラメータチューニングの計算を回し続けてる手伝いをしたのですが、いくら回し続けても、回し直してもBest TrialのAccuracyが0.9857とか09859とかあたり以上はなかなか伸びませんでした。
(力学の時間中とかにも回してたのでパソコンがシュワーってそこそこ大きな音で鳴っててごめんなさいと思っていました)
最後の方はパラメータをいじったりしながら高くならないかな〜〜とやっていましたが、オリジナルでコードを書こうとしたら謎の処理が行われたりして、あまり意味ないことと格闘していました。貢献はできていないです。
そして提出締め切り直前、どのファイルを提出するかという二人の議論からようやくコンペのスコアの仕組みをしっかりと理解し、(ほとんど興味本意で) @cp20 さんのsubmitファイルを(本当にゴリ押しで)少しだけ変えました。全然提出ファイルに出すつもりはなかったのですが、物は試しということで結局これを最終ファイルの二つ目に採用してもらいました。もう少し詳しくは下の方の最終submitのセクションに記載しています。
@cp20
最初のサンプル実装(?)では NN が与えられていましたが、それを全力で無視して LightGBM のモデルを初手で組みました。とりあえず過去のコンペで利用した LightGBM の実装を丸々コピペしてきて、文字列のカテゴリカル変数を one-hot encoding しただけのデータをそのまま突っ込んでスコア 0.98562 ぐらいが出ました。(7/12 夕方ぐらい) この時点でだいたいの NN よりは精度が良いと思うので、やっぱ GDBT は偉大だなぁと思うなど。
アンサンブル用に NN とかランダムフォレストとか他の方法も一応構築したんですが、あんまりスコアが伸びなかったので結局辞めてしまいました。でも終わった後に話を聞くと多少 Public スコアが下がったとしてもアンサンブルはやるべきだし、NN やランダムフォレストでも十分高いスコアは出るっぽいです。
そこから Optuna を使ったハイパーパラメータチューニングをちょっとだけ (数時間ぐらい?) して 0.98661 ぐらいが出ました。(7/13 夕方ぐらい)
さらに one-hot encoding の代わりにラベルエンコーディングを行うようにして、LightGBM の categorical_feature パラメータ にそれらのカテゴリカル変数を入れるようにしたら 0.98671 まで伸びました。(7/13 夕方ぐらい) この時点で Public LB 1位になって、2日間ぐらい1位を保ってました。(そのあと Deep Dreamers に抜かされてからコンペ終了までずっと2位でした)
そこからターゲットエンコーディングとか特徴量エンジニアリングとかを試すもあまり伸びず、結局無限にパソコンを稼働させてハイパーパラメータチューニングをする人になっていました。が、最初に出した 0.98671 を超えられず結局無意味な電力消費を行ってしまいました、、、地球、ごめん。。
@kantoku
初めはデータ分析のコンペに関して無知だったので、薦められていた『Kaggleで勝つデータ分析の技術』という本を読んでました。ただ、量が多すぎてとてもコンペが終わるまでに読み切れないと思ったので使えそうなところだけピックアップして実際に実装しながら読み進めました。
データの前処理では、欠損値の補完をいろいろ試して最終的に-1で全部埋めました。あとはCategoricalデータの処理なのですが、はじめNNでLabelエンコーディングを行っていたのをOne-Hotエンコーディングに変えたら良い結果が出ました。本に書いてあったのですが、LabelエンコーディングはNNには不向きらしいです。
それと、データについてちゃんと理解しないといけないと思い、ヒストグラムとかを出してEDA(データ理解)をやりました。見やすい感じに出力するのに思いのほか時間がかかりました。ヒストグラムとひたすらにらめっこしましたが、初めはだから何という感じであんまり理解できませんでした。しかし、コンペ終盤でいいアイデアを思い付いたので実装したら精度が上がりました。何をやったかについては下に書いてます。
NNのモデリングについては@abap34さんが講習会の中で原点を通る活性化関数が良いと言っていたのを思い出して中間層にReLU関数を使いました。Tanhも試したのですが体感ReLUの方が良い結果が出ました。ハイパーパラメータチューニングではOptunaというPythonの自動最適化ライブラリを使ったのですが、めちゃめちゃ時間がかかったので途中の時点で最適なものを取り出してそこの周りを手動で調整してました。
こんな感じでNNの実装を頑張っていた訳なのですが、最終的にはNNの限界を感じLightGBMに乗り換えました。GBDTは欠損値を処理しなくてよかったり、NNよりも調整するところが少なくて済んだり、学習がめちゃ速いというのに加えて精度もめちゃいいのでほんと感無量でした。最終submitはハイパーパラメータチューニングしたのをちょっと調整したやつと@cp20さんの最高スコアが出たやつをアンサンブルしたものを1つとして提出しました。ただ、LightGBM同士を混ぜ合わせただけなので上で@cp20さんも言っていたように、NNとLightGBMをアンサンブルして提出した方が良かったのかもしれません。
解法
基本はNN (ニューラルネットワーク) とGDBT (勾配ブースティング木) の2つのモデルを構築しました。が、最終的にはGDBTのみを採用しました。
NN (ニューラルネットワーク)
最初は欠損値をすべて0で補完し、CategoricalデータをLabelエンコーディングしたものを講習会で扱ったNNのサンプルモデルに入れて回してましたが、良くても0.94くらいしか出なかったので欠損値を-1で埋めてみたら0.97弱出ました。平均値や最頻値などの代表値や9999での補完も試してみたのですが、-1で埋めた時が一番精度が良かったです。勝手な憶測ですが今回の特徴量は全て0以上だったので負の数を採用することで新たに欠損しているという情報を盛り込めたのではないでしょうか。(ほんとただの憶測です)
データをlog1pで非線形変換してから学習したら0.983くらいは安定して出るようになりました。また、データをいろいろ見ているとNumericalデータのふりをしているだけで実はCategoricalデータとして扱えそうなものが`service`, `flag`, `protocol_type`以外にもあることに気づいた(`num_root`や`num_access_files`など回数を表している特徴量に多い傾向がある気がする)のでエンコーディングしてみました。(下)これで0.984くらい。その他ClipやBinningも試してみたものの、あまり変化がなかったので諦めました。
# Boolにしてエンコード
train_x['wrong_fragment'] = (train_x['wrong_fragment' != 0])
test['wrong_fragment'] = (test['wrong_fragment'] != 0)
train_x['su_attempted'] = (train_x['su_attempted'] == 0)
test['su_attempted'] = (test['su_attempted'] == 0)
train_x['num_root'] = (train_x['num_root'] == 0)
test['num_root'] = (test['num_root'] == 0)
train_x['num_access_files'] = (train_x['num_access_files'] == 0)
test['num_access_files'] = (test['num_access_files'] == 0)
ハイパーパラメータチューニングを行った結果、中間層は2つにしました。1つ目の中間層のノード数を入力数のプラス5~10くらいにして2つ目の中間層のノード数は1つ目の約2倍にすると学習速度と結果がいい感じのバランスになりました。
上で言ったように活性化関数はReLUを使っています。OptimizerはSGDが一番速くて正解率も大きくなってくれました。
GDBT (勾配ブースティング木)
GDBT は特に何もしなくてもそこそこ精度が出て、しかも欠損値があっても学習・推論ができるので便利です。強いです。今回のコンペは最終的にこれだけを利用しているので、本当に強いです。Kaggle班長であるところの @abap34 さんによると「そんなにカラムが多くないベタなテーブルデータだと NN が GBDT に勝つことはそうそうない」 らしいです。今回は初期で30ちょいぐらいしかカラムがないのでGDBTの方がまぁまぁ有利ということですね。
GDBT の具体的な実装には CatBoost や XGBoost、LightGBM などがありますが、この中でも推論が速くて精度がそれなりに良い LightGBM を使いました。噂によると CatBoost とかの方が学習時間は長いけど精度が良いみたいな話があるのでそっちを使った方が良かったかもしれません。
LightGBM 特有のデータの前処理として categorical_feature パラメータ を使いました。
あとハイパーパラメータチューニングは Optuna を使ってチューニングして、最終サブミットでは (たぶん) 次のパラメータを使いました。
{
'num_leaves': 86,
'learning_rate': 0.026306284024472916,
'n_estimators': 747,
'max_depth': 15,
'subsample': 0.5274186828044589,
'colsample_bytree': 0.5866746558989268,
'reg_alpha': 1.5411829712150768e-06,
'reg_lambda': 1.8359119265751425e-06,
}
謎に24時間とか回してましたが、別に数時間やれば十分だと思います。それよりも特徴量の改善に力を注いだ方が良いのではないかと...
そのほか
ターゲットエンコーディング
@cp20 が過去のコンペで使用した実装をほぼそのままコピペして使いました。ただ結論から言えばほぼ精度は伸びませんでした。理由は分からないのでもっと詳しい人に聞いてください。
最終サブミット
by @kantoku
最終サブミットは@cp20さんと@kantokuのsubmitの中でPablicスコアが一番高かったものをアンサンブルしました。アンサンブルといってもお互いの予測で一致していない箇所(34項目だった)をランダムな予測にするということをやっただけです。下のコードでは@cp20さんと@kantokuの予測を採用する確率をそれぞれ0.55,0.45として@cp20さんの方を少し高くしています。これを何回か実行したらPablicスコアが0.0001上がりました。ですが、コンペが終わってからPrivateスコアを確認してみると特別高いわけではなかったのでPBを信頼しすぎるのは良くないと実感しました。本来であればNNとのアンサンブルをするべきだったと思います。
import numpy as np
import pandas as pd
pred_kantoku = pd.read_csv('submit_kantoku.csv')
pred_cp = pd.read_csv('submit_cp.csv')
pred_kantoku = pred_kantoku.to_numpy()
pred_cp = pred_cp.to_numpy()
pred = np.array([pred_kantoku, pred_cp])
count = 0
data = pred_cp.copy()
for i in range(pred_kantoku.shape[0]):
pk = pred_kantoku[i, 1]
pc = pred_cp[i, 1]
data[i, 1] = np.random.choice([pk, pc], p=[0.45, 0.55]) if pc != pk else pc
count += 1 if pk != pc else 0
print('不一致数:', count)
submission = pd.DataFrame(data={'id': data[:, 0], 'pred': data[:, 1]})
submission.to_csv('submit.csv', index=False)
by @ayana
by @ayana とはなっていますが、ほとんど @cp20 さんのsubmitです。。。 LightGBM のカテゴリカル変数を扱う用のオプションを使った(らしい) @cp20 さんのsubmitファイルを少し改善したものです。
具体的には、精度が低かったモデルと精度が高かったモデルの結果を複数用意して、
「精度が低いモデルでは多数派だったけど精度が高いモデルでは少数派」
の結果は間違えている可能性が高いと判断し、一部改変することでほんの少しだけ精度を高めようと頑張りました。本当にゴリ押しです。。これでPrivateのスコアは1個分くらいしか上がらなかったのですが、おそらく2個くらい上がって1個間違えたりした(結果+1とかになった)んじゃないかなと思っています。
Publicボードの最終順位が数チームほぼ同じくらいで、どのsubmitファイルを最終版にするかも含めて完全な運頼みになってしまうのがちょっと悲しいなと思ったので、なんとかPrivateスコアもあげられないかなと考えた結果です。
(意味があったのかと、機械学習として適切かは分からないですが...)
感想
@ayana
一言で言うと楽しかったです。チームメンバーに感謝しかないです。本当に。。
初回顔合わせのとき、 @cp20 さんがつよいのは分かっていたけど @kantoku くんも分厚い本を持ってきていてスラスラとコードを書いていたので正直ちょっとだけ萎縮していました。第6回講習会のコード内容もコピペで動いただけで、理解するフェーズまでいけてなかったので、エラーになった時も対処法がいまいち分からず二人の会話にもついていけずごめんなさいの状態になっていました(ごめんなさい)。
そこからあまり動けていなかったのですが、@cp20 さんがもう一度対面で集まろうと声をかけてくださり、そこでとりあえず一個submitファイルを提出するところまでいこう、とサポートしてくださって、無事first submitができたときは正直結構うれしかったです(アップロード直前、めちゃくちゃ低いスコアとか出たらどうしようと内心ビクビクしてました)。ちょくちょく明らかに置いてかれているのを察して声かけてくださってめちゃくちゃ感謝です。。。。
そのあと、@kantoku くんが NN の改善ポイントをいくつか教えてくれたので私も「上がった!」の瞬間を楽しむことができました。そして、結局あまり上がらなかったものの、そこから自分なりになんとかスコアを上げられないかと試してみるのも楽しかったです(最初の成功体験大事ですね)。二人が議論しながらずっと格闘しているのを見て自分も何かしたい、頑張ろうと思えました。
今回はもう完全についていくだけみたいな感じになってしまったので、次回コンペがあれば積極的に貢献したいなと思っています。そして今回は「あまり理解できてないけど試したらうまくいった」ということも結構あったので、今後はしっかり理解しながらもう少し理論的な改善がしていけたらいいなと思いました。
機械学習という概念自体はずっと好きだったのですが、実際にやってみる機会は全くなかったので、今回1から学べてとても楽しかったです。運営してくださった @abap34 さん、いろいろ教えてくださった @cp20 さん、 @kantoku くん本当にありがとうございました。
@cp20
一応去年機械学習講習会には出ていたので、今年はコンペだけ参加するかーみたいな軽い気持ちで出ていました。なので優勝するとはまさか思ってませんでした、びっくり。でもそれも他のチームメイトのおかげ (謙遜とかではなく、マジで) だと思っているので、本当に感謝しています。
感謝の気持ちを込めて、具体的なチームメイトの活躍を挙げて褒めたたえていこうと思います。@kantoku は本当に強かったです。最初に顔合わせしたときに『Kaggleで勝つデータ分析の技術』(技術評論社) を持っていたのがかなり印象的でした。1人で勝手に進めてくれていて、気づいたらモデルが改善されていて良いスコアが出ていて勝手にびっくりしていました。勝利に貢献したサブミット (最終2サブミットの高い方) は @kantoku が作ってくれたものなので実質優勝は @kantoku のおかげといっても過言ではないという見方もありますね(?) 今回は競技性低め (取っつきやすさを上げようとした結果だと思うのでそれ自体に文句はないです) でしたが、夏にあるであろうコンペでは圧倒的な活躍を見せてくれるんじゃないでしょうか。今後に大きく (勝手に) 期待しています。
@ayana も未経験とは思えないほど頑張ってくれました。ちゃんと講習会の内容を飲みこんでちゃんとモデルを構築するところまでできるだけでも結構すごい (あたかも普通に感じますが、これ結構すごいことです) んですが、最後には自分で改善の方法を考えてスコア向上に貢献してくれて流石や...って気持ちです。最終2サブミットは @cp20 と @kantoku のものの予定だったんですが、@ayana が @cp20 のサブミットの改善版を作ってくれたのでそれを採用しました。Public スコアだけじゃなくて Private スコアもちゃんと高かったので目の付け所はちゃんと良かったということでしょう。コンペの期間に圧倒的成長を見せてくれたのでかなりのポテンシャルを秘めているのではないかと (勝手に) 期待しています。
その一方でボクは、実は LightGBM のモデルを作ってひたすらハイパーパラメータチューニングをやっていた人でしかないです。ボクの唯一のお仕事は LightGBM はいいぞと布教することだけでした。簡単なお仕事ですね。でもそれで勝ったという説もあるのでボクも功労者ということにしておきます。みんな頑張ってみんな偉いの精神 (大事)
@kantoku
NNについては高校の時少しかじっていたので導入はスムーズに行えました。しかし、データ分析はやったことがなかったので始めは不安が大きかったです。
@cp20さんとは初対面だったのですが、対面で活動したおかげでいろいろ質問しやすかったのがよかったです!それに、@cp20さんが一番最初のsubmitで2位をたたき出したのでこのチームならいけそうだと自信がついて不安も吹っ飛びました。それと、チームメンバー全員が本気で取り組んでいたのがよかったです。そのおかげで、もっと頑張ろう!とやる気が出てました。Discordのサーバーも活発に動いていて、情報共有と役割分担がうまくできてたのも勝因のひとつだと思います。
このコンペで学べたことは、ひたすらパラメータチューニングするよりも、まずデータの前処理を真面目にやるべきだということです。ちょっとパラメータを調整するよりも前処理を少し工夫するだけで予測精度がかなり変わってくるということを実感しました。また、GBDTについてもっと学んでみたいと思いました。
とにかく、優勝できたのもそうですが、このコンペめちゃめちゃ楽しかったです!
企画してくれた@abap34さんたちに感謝です。
おわりに
結論: LightGBMが強い
というのは冗談で (そんなに冗談でもないけど) Kaggleをはじめとするコンペティションで重要なことを身をもって体感することができてかなり良かったと思います。賞品がついていたからなのかかなりみんな気合を入れて挑んでいたのが印象的でした。
最後にはなりますが、主催の @abap34 さんをはじめとした運営の方々に Special Thanks を述べておきたいと思います。本当に!ありがとう!ございました!! あとごちそうさまでした!