今まで食わず嫌いというかなんというか、興味はあるのに挑戦を先延ばしに伸ばしてきたKaggleに初挑戦してみました。その時の四苦八苦ぶりを赤裸々にお伝えします。
きっかけ
新年早々、「Kaggleで始める機械学習入門」という、素晴らしい記事を読んだからです。今までは、なんとなく「Kaggleに興味はあるけど、コンペのテーマや開催スケジュールが自分にマッチしているのを探すのが面倒」で、ついつい挑戦を先延ばしにしていました。そして、Kaggleの「Titanic」の記事を見るたび、「いや~、Titanicの生存者の推定とか興味ないし」「Leadersboardの上位は1.0とか並んでて、データリークしてるっぽいし」と感じて、手を動かさずにいたのです。
しかし、上記の記事では、Kaggleのユーザー登録、Titanicコンペへの参加方法、Kaggle Notebookを使った開発方法、予測した結果のSubmission方法など、一連の流れがとても簡潔に説明されていたおかげで、「ああ、これはチュートリアルなんだ」ということで合点がいったわけです。
読んでいると、自分にもできそうな気がしてきたので、さっそく挑戦してみました。
記事の執筆者の@k-ysdさん、良いきっかけを与えていただき、ありがとうございます!
最終結果はスコア0.79904、954位(上位6%)
1日に結果をSubmit可能な回数が10回で、その10回で到達できた最高スコアが0.79904(954位)で上位6%くらい(954位/15898人中)に入り込めました。内心目標にしていた0.8を超えることはできなかったのが残念ですが、チュートリアルとしては十分楽しんだかな、という感じです。今後は、Titanic以外のコンペもやってみようと思っています。
参考までに、1回目の投稿から10回目の投稿まで、どんな感じで開発を進めていったかの足跡を残しておきます。
初手:記事のコピペ(スコア:0.77751 = baseline)
「チュートリアル」として、ユーザ登録、開発、結果の投稿&確認の一連の流れをサクッと進めます。上の記事のソースコードをまるっとそのままコピペしてKaggle Notebookに貼り付け、そのまま結果を投稿したところ結果は0.77751。使っているモデルはTensorFlowデシジョンフォレストのGradientBoostedTreesモデル。初手としてはなかなかのスコアだと思いました。(私は何も考えてませんが!)
2手目:pycaretでモデルの比較(スコア0.7799 ↑up)
デフォルトパラメーターをいろいろといじって、精度向上にいそしんでも良かったのですが、なんとなく、データの中身を見ながら特徴量エンジニアリングっぽいこともしたかったので、前から気になっていたPyCaretを使ってみることにしました。
PyCaretは、Pythonで実装されたオープンソースの機械学習ライブラリーで、機械学習の一連の作業を自動化してくれる便利な仕組みを備えています。データの前処理や、種類の異なる複数のモデルの精度比較や、特定モデルのチューニングなど、個別に書くと何百行も必要になる作業を、数行のコードで簡単に書くことができます。
しかし、このPyCaretをKaggle Notebookにインストールするところで、つまずいてしまいました。普通に
! pip install pycaret
としただけではエラーが出てインストールされないのです。結局Kaggle Notebookでpip installするには、
(1) アカウントに携帯電話の番号を登録する
(2) NotebookのSettingで「Internet」をONにする
という2つの作業が必要になることが分かりました。
上記の作業で一応、インストールはできたのですが、今度はソースコードの
from pycaret.classification import *
で、「ImportError: cannot import name 'prod' from 'scipy._lib._util'」というエラーが出てしまいます。いろいろと調べてみたのですが、直接の解決策がみつからず・・・結局、「【徹底解説】機械学習「PyCaret」でKaggle Titanic にチャレンジ」という記事を参考に、Kaggle APIキーを発行して、Google Colabと連携させて開発を進めることにしました。Google ColabだとPyCaretは問題なく使えますし、Kaggleコマンドを使ってラインから一発投稿も可能なので、結果的には充実した開発環境になったと思います。
2手目も基本的に、上記記事の通りにコードをコピペしてポチポチと進めていったのですが、私の場合はなぜかLogistic Regressionが一番成績が良かったので、そちらをチューニングして結果を出して投稿しました。その結果のスコアが0.7799。初手0.7751から微増しました。
3~5手目:先人の知恵①敬称に着目(スコア0.79425 ↑up)
さて、モデルの比較やチューニングも簡単にできるようになったので、いよいよデータの中身を見ながら精度向上に効きそうな特徴量をアレコレしていきます。まずは、Discussionをのぞいて、どんなトピックが議論されているか見てみました。「Why is there a barrier to 80% correct?」「Going Beyond 0.80」といったトピックが投稿されているあたり、80%あたりに目標精度の壁がありそうな雰囲気です。
さらに、「Kaggle Titanic 目標精度」などでググってみたところ、「KaggleチュートリアルTitanicで上位3%以内に入るには。(0.82297)」という記事が見つかりました。これは素晴らしい!
中身を読んでみると、
- Mr., Miss., Mrs. などの敬称
- チケットの記号と番号
- キャビンの記号と番号
- 一緒に乗船した家族の人数
などが精度に効きそうなことがわかりました。
まずは、「Mr., Miss., Mrs. などの敬称」を特徴量として独立して取り出すことにしました。ソースコードはこんな感じです。
def add_title_features(df):
df["isMr"] = ["Mr." in name for name in df["Name"]]
df["isMrs"] = ["Mrs." in name for name in df["Name"]]
df["isMiss"] = ["Miss." in name for name in df["Name"]]
df["isMaster"] = ["Master." in name for name in df["Name"]]
df["isDr"] = ["Dr." in name for name in df["Name"]]
df["isRev"] = ["Rev." in name for name in df["Name"]]
add_title_features(train)
add_title_features(test)
これで、名前にそれぞれの敬称がついている人はTrue、ついてない人はFalseになる特徴量が追加できました。当初は「Mr.」「Mrs.」「Miss.」だけを特徴量に追加したところ、それだけでスコアが0.79186にサクッと上がってくれたので、「これは0.8を超えるのもすぐだな」と思っていたのですが・・・そこからの道のりが長かった。結局、「Master.」「Dr.」「Rev.」といった敬称も追加して0.79425まで、じわりじわりスコアアップができました。
6~8手目:先人の知恵②チケットとキャビン番号(スコア0.79665)
さて、次に参考にしたのは、チケット番号とキャビン番号に含まれる文字と数字の並びです。チケット番号は「A/5 21171」や「PC 17599」「STON/O2. 3101282」「113803」のように、よく分からない記号があったりなかったりします。チケットの値段と相関がありそうなので、「お金持ちほど助かりやすかった」という事実と相関がある可能性があります。
また、キャビン番号は、欠損地が多いのですが「C85」「C123」「E46」のようなアルファベット+数字の形式でデータが入っており、船内で過ごす場所に関係していると思われます。Wikipediaによると、左舷と右舷の救命ボートの担当者の間で「婦女子優先」の考え方にギャップがあり、それが男性/女性の生存率に影響を与えたといわれています。残念ながらどのキャビンが左舷か右舷かの情報はわかりませんが、番号の1文字目のアルファベットが同じだったり、番号が近い場所にいた人は、似たような運命をたどっている可能性があります。
そこで、チケット番号は「A/5 21171」を「A/5」と「21171」に、「PC 17599」を「PC」と「17599」に分けて特徴量として追加しました。また、キャビン番号は「C85」を「C」と「85」に、「E46」を「E」と「46」に分けて特徴量として追加しました。
ソースコードは下記の通り。
def split_ticket_number(df):
df["TicketSymbol"] = df["Ticket"].apply(lambda x: x.split(" ")[0] if len(x.split(" ")) > 1 else "")
df["TicketNumber"] = df["Ticket"].apply(lambda x: x.split(" ")[1] if len(x.split(" ")) > 1 else x)
def split_cabin_number(df):
df["CabinSymbol"] = df["Cabin"].apply(lambda x: re.sub("[\d]+", "", str(x)))
df["CabinNumber"] = df["Cabin"].apply(lambda x: re.sub("[^\d]", "", str(x)))
split_ticket_number(train)
split_ticket_number(test)
split_cabin_number(train)
split_cabin_number(test)
実は、この特徴量、最初はチケット番号だけ分離して追加、次にキャビン番号だけ分離して追加してみたのですが、スコアは上がるどころか下がってしまいました(0.78708)。しかし、チケット番号、キャビン番号を両方分離して特徴量に追加したデータでは、0.79665まで向上させることができました。
9手目:多数決(スコア0.79904 ↑up)
スコア0.8までもうあと一息のところに来たので、ちょっとサボり心が芽生えてしまって、「小手先で精度向上できないか?」と思い、複数モデルの結果を多数決で決めてみることにしました。正式な技術としては「アンサンブル学習」という優れたやり方があるのですが、ここではそれ以前の「投票による多数決」で決めるやり方を実行してみます。
やり方は簡単、モデル生成のパラメータをちょっとだけ変えて作った3つのモデルで生存者ラベルを予測した結果を3つ出力し、3モデル中2モデル以上が同じラベルだったものを最終結果として出力する、というものです。私の場合は、Logistic RegressionのパラメータCを0.95、1.0、1.05に設定して生成した3つのモデルを使って多数決をとりました。結果は0.79904。惜しい!
10手目:先人の知恵③家族の人数(スコア0.79186 ↓down)
スコア0.8まで、残り0.001ポイントを稼ぐためにどうするか・・・。先人の知恵の中でも、手つかずだった家族の人数を特徴量に追加することにしてみました。ソースコードはとても単純。下記の通りです。
def add_family_size(df):
df["FamilySize"] = df["Parch"] + df["SibSp"] + 1
add_family_size(train)
add_family_size(test)
この特徴量を追加後、9手目と同様に3つのモデルによる投票の結果を投稿しましたが・・・スコアは0.79186に下がってしまいました。。。まあ、機械学習ではあるあるですね。そして、これが本日最後の10回目の投稿ということで、ちょっと歯切れが悪いですが、これにて打ち止めと相成りました。
今後について
今はお正月なので、時間がたっぷりありますが、仕事が始まったらどうかな・・・
とはいえ、せっかくチュートリアルを終えられたので、「Kaggleでのメダル獲得」を今年の目標にがんばっていきます!