データ分析でコードをクリーンに保つ技術
こんにちは、ひぐです。
最近データサイエンティストのための良いコーディング習慣という記事を読みました。
www.thoughtworks.com
こうした方がいいよなという自分の経験則が綺麗に言語化されていてよかったです。
ここではデータ分析でコードをクリーンに保つ技術について、記事の内容と自分の取り組みを合わせて紹介したいと思います。
自分はまだチームでの開発経験などが浅いため、間違っている部分もあるかもしれません。
あらかじめご了承ください汗
コードが汚くなる要因
コードが解くべき問題の複雑さを増長させている時、そのコードは汚いと言えます。
汚いコードは汚い部屋で探し物をする時などと同じく、簡単な作業を困難にしてしまいます。
では、どのような書き方をするとコードが汚くなるのでしょうか。
元記事には下記のような例が記載されています。
部屋で例えると、
- 一つの収納箱にあれこれ詰め込む
- 物の定位置を決めず、空いているところに収納する
- 整理してない収納箱を全てひっくり返して、再配置する
といった振る舞いと似てそうです。
処理が1箇所に纏まっていないことや、
どこに何が書いてあるかわからないことが複雑さを冗長させていると言えます。
jupyter notebookはコードを煩雑にしやすい
さらにデータ分析でおなじみのjupyter notebookは
- df.head()/describe()などデータの内部を確認できる機能が豊富
- 上下のセルから変数の中身が引き継がれる
といった特徴から、プロジェクト序盤は素早いフィードバックを得られて便利ですが、
これらの特徴は裏を返せば
- 変数の影響範囲が広くなりやすい
- 処理に影響を及ぼさないコードが増えやすい
とも言え、コード量が増えると急速に煩雑になってしまいます。
インテリアデザイナーには「平たい場所は乱雑さを蓄積しやすい」 という通説があるそうですが、
何でも1箇所に書けてしまう"notebook"は、データ分析における平たい場所であると言えます。
良いコードにする振る舞い
では良いコードにするにはどのようにすれば良いのでしょうか。
元記事では下記の5点が紹介されていました。
コードを綺麗に保つ
データ分析に限らず、綺麗なコードを書くセオリーがあります。
たとえば
- DEAD CODEを消す
- 処理の内容が明快にわかる変数名をつける
- 似た記述はまとめる(DRYである)
データ分析も例外ではなく、これらのセオリーには従うべきです。
リーダブルコードなどの書籍にまとめられていて、目を通しておくべきでしょう。
リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)
- 作者:Dustin Boswell,Trevor Foucher
- 発売日: 2012/06/23
- メディア: 単行本(ソフトカバー)
関数を使って実装を抽象化する
一つの関心ごとに対しては一つの関数でまとめ、処理を抽象化するべきです。
そうすることで、以下のメリットが得られます。
- 読みやすい
- テストしやすい
- 再利用しやすい(引数を与えて、ハードコーディングを防げる)
これは例を見てみるとわかりやすいです。
# bad example pd.qcut(df['Fare'], q=4, retbins=True)[1] # returns array([0., 7.8958, 14.4542, 31.275, 512.3292]) df.loc[ df['Fare'] <= 7.90, 'Fare'] = 0 df.loc[(df['Fare'] > 7.90) & (df['Fare'] <= 14.454), 'Fare'] = 1 df.loc[(df['Fare'] > 14.454) & (df['Fare'] <= 31), 'Fare'] = 2 df.loc[ df['Fare'] > 31, 'Fare'] = 3 df['Fare'] = df['Fare'].astype(int) df['FareBand'] = df['Fare'] # good example (after refactoring into functions) df['FareBand'] = categorize_column(df['Fare'], num_bins=4)
good exampleの例であれば、pd.qcutの意味や引数を覚えていなくても、
連続値の'Fare'列をbin詰めする処理ができます。
イメージはこんな感じです笑
引用元(?):
ヘヴィメタバンド、スティール・パンサーのサッチェル氏→機材紹介がハードロック過ぎる - Togetter
なるべく早い段階でjupyter notebookを.pyに移行する
上で言及したように、notebookはコード量に伴い煩雑さが急速に増してしまいます。
したがって、コード量が増えてきたらなるべく早く.pyコードに書き換えるべきです。
notebookからpyファイルに書き換える手順は元記事で下記のように紹介されています。 引用元: clean-code-ml/refactoring-process.md at master · davified/clean-code-ml · GitHub
- notebookが正しく動くか確認する
- 自動変換でpyファイルに出力する
- debugコードを取り除く(df.head()など)
- Code smell(直したい部分)をリストアップする
- 一纏めにしたい部分を特定する
- ユニットテストを書く
- テストを通すように記述する
- importを整理する
- 動作を確認し、commitする
- 繰り返す
テスト駆動開発で行う
データ分析業務もソフトウェア開発の例外にもれず、テストを書くべきです。
例えばモデルの挙動を調べるテストでは、
ベースラインで想定想定スコアを超えない場合はエラーとみなすコードを書くと良いそうです。
テストコードについては自分の知識も浅いので、またいつか改めて記事を書きたいと思います。
こまめなコミットをする
コミットを小さく頻繁に行うことで、下記のメリットが得られます。
- 自分がどの問題に取り組んでいるか簡単に理解できる
- 処理のロールバックが簡単にできる
自分なりの工夫点
最後に自分なりのコードを綺麗にする工夫点をいくつか紹介します。
業務ごとにコードをまとめスニペット化する
データ分析では、タスクが変わっても似たような処理を書くことが多いです。
コードをスニペットとして保存しておくと、似たような処理が必要になった時少ない作業量で書き終えることができます。
また、スニペットにすることを意識しながらコードを書くことで
自然と汎用性の高いコードが書けるようになります。
自分はGitHub GistとDashを使ってスニペットを保存しています。
自分なりのルールを設ける
自分なりのルールを設けて、いつも似たコードを書くようにしています。
そうすることで他のスニペットとの互換性を良くしたり、素早くコードを書くことができます。
また自分はNN系のコードを書くときはhydraとpytorch-lightningを
使うことでいつも同じステップで書けるようにしています。
データ分析のコードはあまり高級なラッパーを使うと、すぐ破綻してしまうので
その塩梅が難しいですが、うまく使えば綺麗にかけるでしょう。
メソッド名、I/Oなどを組み込み関数や有名ライブラリに近づける
sklearnやpandasなどの有名なライブラリの入出力と対応させてコードを書くことで、
他者とのコミニュケーションコストを抑えることができます。
綺麗な人のコードを見る
Kaggleなどデータ分析コンペティションでは、上位の人が解法を公開していることが多いので、
それを眺めると良いと思います。
他には、nyanpさんのnyaggleなど参考にさせていただいていますm(__)m GitHub - nyanp/nyaggle: Code for Kaggle and Offline Competitions
まとめ
以上です!
振り返ってみると当たり前のことばかりですが、全部を常に実践するのは難しい...!
綺麗なコードが書けるということはエンジニアの技量としてかなり本質的なものだと思っているので、
今後も頑張っていきたいと思います。
久しぶりにブログを書いたら、文章書くのが難しすぎてびっくりしました。
こっちも頑張っていきたいです。では〜
2019年を振り返りと2020年の目標
こんにちは、ひぐです。
もう年の瀬ですね〜
今年のトピックは大きく分けて、研究、就活、プログラミングの勉強の3つという感じでした。
今年のよかったこと、反省点を踏まえて来年も頑張りたいので、
それぞれまとめていきたいと思います!
概要
自分を4行でまとめる
2019年4月に工学系学部を卒業して同大学大学院に進学。
プログラミングは1年半前に始める。
研究内容は、学部:政治と自然言語処理→院生:深層学習を用いた医療画像における腫瘍の自動識別
21卒で就活中
今年をざっくりまとめる
- 1~3月
卒論/言語処理学会/長期インターン先での開発/卒業旅行 - 4~6月
入学/授業/新規研究のスタート - 7月~9月
選挙学会/サマーインターン/研究 - 10~12月
SIGNATE参加/研究/就活悩む/Atcoder始める
就活
インターンシップ
下記インターンシップに参加しました。
- Wantedly(3weeks)
- 日本経済新聞社(5days)
- SoftBank(2days)
- Gunosy(2days)
- CyberAgent(2days)
参加することで、企業の風土や開発環境を知れるだけでなく、
自分の目標となる先輩や、優秀な同期と出会うことができました。
1月からCAのAI事業本部で長期インターンをさせていただけることになったので、こちらも頑張りたいです。
自己分析
企業との面接でどういうエンジニアになりたい?という質問になかなか答えられず大変でした。 そのため、10月以降は特に自分の将来を真剣に考えました。
多動な人間なのでその時その時でやりたいことは常にたくさんあるのですが、
もっと軸足を定めて考えるべきだったと反省してます。
ざっくりですが、
- 社会的に正しいことをする
- 頭とコミニュケーション両方を使う
- 自分の学びを同業者・同期に還元していく
この3つは自分の中で大きな軸だなぁと思っています。
面接で取り繕って話すことは絶対したくないので、
ちゃんと考えて言葉にできるようにしていきたいです。
研究
学会発表
- 言語処理学会
- 選挙学会 にてポスター発表をしました。
新規研究
4年次の研究を経て、もっと深層学習ちゃんと勉強したくてテーマを変えました。 大変ですが、後悔はしてないです。
プログラミングの勉強・成果
長期インターン中にAidemyの講座を作った
premium.aidemy.netSIGNATE Stududent CUPで9位になった
zerebom.hatenablog.comGunosyデータ分析コンペ準優勝した
SoftBankデータ分析コンペ特別賞もらった
ソフトバンクAIハッカソン
— ひぐ (@zerebom_3) 2019年10月20日
5/14位で特別賞でした😭
チームのみんなと協力できて楽しかったです
パーカー(かわいい)とKindle Oasisなんて豪華な景品ありがとうございます
マイナビコンペも頑張りたい pic.twitter.com/NcNdTzQn3mCSの基礎を勉強・Atcoderを始めた
その他
- TOEIC 755点になった
- Twitterのフォロワーが610人になった
- ブログの月間PVが1500~2500くらいになった
- 筋トレが4ヶ月くらい続いた、10回*3セットで上がる重量が増えた
ベンチプレス30->45kg
デッドリフト40kg->65kg
スクワット60kg->75kg
今年できたこと
- たくさん行動する経験する
- 尊敬できる人に会いにいく
- 規則正しい生活
- 人を傷つけない
今年できなかったこと
- 目標に到達する前にやめてしまった(KaggleとかKaggleとか)
- ブログ以外のアウトプット(LT/論文投稿など)
- 部屋をきれいに保つこと
- 一つ一つを丁寧にこなすこと(手広くやりすぎた)
来年の目標(抽象)
来年の目標(具体)
- 論文投稿
- LT登壇
- TOEIC 850以上
- Kaggle Master
- 統計検定1級
まとめ
今年1年間は自分に対して向き合って、たくさん勉強できたなと思っています。
勉強や就活をする上でいろんな人に話を聞きに行ったり、新たな友達ができたのも大きな成果でした。
その一方でエンジニア・院生以外の人とは殆ど会わず、
世間一般から遠ざかったような気持ちもしました。
今年は就活、学校、研究全部あったのでしょうがない部分もありますが、
小粒の成果がいっぱいって感じになってしまいました。
来年以降は一つの目標に対してじっくり取り組んで大きな成果を出していきたいです。
勉強のための勉強ではなく、
なんのために勉強するかも今まで以上にしっかり考えていきたいです。
来年もよろしくお願いします〜
おわり
マイナビ × SIGNATE Student Cup2019に参加して9位でした
こんにちは、ひぐです。
先日マイナビ × SIGNATE Student Cupに参加し、9/342位になりました!
この記事ではどんな取り組みを行ったかを書きたいと思います。
なるべく本コンペに参加してない人にも内容がわかる記事にしたいと思います。
本コンペの基礎情報
マイナビ × SIGNATE Student Cupとは年に1度開かれる、学生のみが参加できるデータ分析のオンライン大会です。
お題とデータが渡され、機械学習を用いて目的変数を予測し精度を競う大会です。
本コンペのテーマは「東京都23区の賃貸物件の家賃予測」です。
各物件に対し、「面積、方角、所在階」などの情報が与えれ、そのデータを元に家賃を予測する、といった内容でした。
引用:https://signate.jp/competitions/182
データ量はTrain,Testどちらとも3万程度でした。
本コンペの特徴
本コンペのデータは以下4つの特徴があり、これらをうまく取り扱うことが精度向上の鍵になったかと思います。
外れ値がある上に、評価指標がRMSE
目的変数である賃料は非常に右に裾が長い分布であり、最も高い物件の賃料は250万円もするものでした。
評価指標がRMSE(二乗平均平方根誤差)であるため、これらの高級物件の誤差をいかに小さくするかが重要でした。データが汚い&数値ではなく文字データとして与えられている
データがすぐに使える形で与えられていなかったため、正規表現等を駆使して情報を取り出す必要がありました。
また、欠損値や書き間違いも多く含まれ、丁寧に処理する必要がありました。Train,Testで同じようなデータが含まれている
物件データの中には、同じアパートの別の部屋などが含まれており、 普通に学習するより、学習データと同じ値で埋めるほうが精度が高くなりました。
外部データ使用可能
このコンペでは外部データの使用が認められていました。
土地データのオープンデータは非常に多く、どれをどのように使うかが大事になったかと思います。
弊チームの取り組み
最終的なPipelineは以下の通りです。
基本的に「各物件のデータから推論より、同じ・似た物件データの賃料からキャリブレーションする」
というつもりで進めていました。
コンペ全期間の大まかな流れと精度の変化は以下の通りです。
- 3人とも個別で学習(17000~16000程度)
- チームマージしアンサンブル。LogをとってMAEで学習(15000程度)
- K-meansで近傍データの作成(14500程度)
- 住所の修正、単位面積あたりの賃料を推定(13000程度)
- パラメータ調整、SeedAverage(12400程度)
特に住所の修正、単位面積あたりの賃料の推定が大きかったと思います。
順を追って説明します。
Plotlyを用いて予測誤差の原因を追求
簡単に予測モデルを作ってからは、出力誤差をPlotlyを用いて地図上にMapして、どういった物件が誤差が大きいか確認しました。
Plotlyはインタラクティブに描画されるため、ズームしながら一つ一つ確認できました。
詳しくはQiitaに記事を書いたので良ければ見てください↓
qiita.com
同じ住所なのに、違うアパートが含まれていること、
またそういった物件の誤差が大きいことを確認しました。
欠損値、異常値補完
今回配布されたデータの物件の所在地には
A:「東京都〇〇区××n丁目x-yy」と正確に記載されているデータもあれば、
B:「東京都〇〇区××n丁目」と丁までしか含まれていないデータも多くありました。
これらを注意深く観察すると、同じアパート(賃料・面積などから判断)でも
Aの形で所在地が埋められてるデータもあればBの形のデータもあることがわかりました。
そこで、面積、所在階、室内設備などの複数条件が同じであれば、同一アパートとみなし、
Bの形で所在地が記載されているデータを同じ物件のAの形の所在地に変換しました。
図にするとこんな感じです。
こうすることで住所や緯度経度をkeyとした集約特徴量が正確な値になり、精度が向上しました。
外部データの使用
今回収集した外部データは以下の通りです。
- 地価データ
- 駅データ
- 路線数
- 1日の利用者数
- 緯度経度
これらから作成した以下のデータは精度向上に寄与しました。
- 物件とその物件から最も近い駅の距離
- 物件から最も近い距離にある公開されている地価
- 上記の地価の2012年から2017年の変化率
- 六本木ヒルズからの距離
K-meansを用いた近傍データの使用
地域によって賃料が全然違うことから近傍データが効くと考えられました。
そこで、緯度、経度、築年数を元にK-meansでクラスタリングし、
このCategorycal変数から以下のような特徴量を作成しました。
差分や比率を入れることで、各物件がクラスタの中でどのような位置付けがわかります。
前処理を丁寧に行なったこともあり、強力な特徴量となりました。 築年数をk-meansの判断材料に入れることにより、より似た性質の物件を同じクラスタに入れることができました。
外れ値に強いモデルの作成
外れ値も外れ値でない値も正確に予測したかったため、
Logをとってmaeで学習をしました。
また、賃料は面積との相関が強かったため、単位面積あたりの賃料を予測するモデルも作成しました。
面積で割り、さらに築年数を考慮したクラスタリングで特徴量を作ることで、
賃料という立地×築年数×面積×その他要員という複雑な変数をモデルに理解させることができたと思っています。
最終予測結果はlightgbm、Kfold、k-meansのシードを1ずつ変えて
30シード×2モデル×10Foldの600個のモデルから作成しました。
k-meansのSeedによって大きく精度が変わってしまっていたのですが平均をとることで
大きくshakedownすることのない頑健な出力結果となりました。
チームでのコミニュケーション方法
チームメンバーはそれぞれ就活や修論で忙しかったため、
それぞれが進められるときに進めて行きました。
Github,Line,Trelloでやりとりを進めていたのですが、特にTrelloが便利でした。 25MB以下のファイルはほぼ無制限に共有できること、各人の取り組んでる内容、進捗状況がすぐにわかったので、非常にスムーズにコミニュケーションを取れました。
参考にしたサイト
飯田コンペ上位手法 signate.jp
Lightgbmのパラメータ調整 nykergoto.hatenablog.jp
Kaggle本 Stacking, Validationの考えをしっかり学べました
感想・まとめ
良かった取り組み
チームを想定してコードを書いた
早い段階でチームで関数の書き方にルールを作ったのでコードのマージが楽だった。(引数も返り値もtrain,testをまとめたDataFrameにする等)
前処理担当、モデル担当、外部データ担当と分けることで責任感を持ちつつ作業ができた。一度使ったらおしまいのコードを書かないようにした。
よく使う関数はクラス、関数化した(target_encoding,save_data,lgb_predictorなど)Lightgbmのバージョンを上げる
なんと精度が上がります
改善するべき取り組み
- どんなコンペにも対応できる柔軟なPipelineコードを作っておく。
- 実験のログをもっと綺麗にとる
- lightgbmに詳しくなる(最後まで気づかなくて、max_depth=8,num_leaves=31とかだった)
まとめ
今までこれほど良い順位でコンペを終えられたことがなかったので嬉しい反面、
入賞する気概で取り組んでいたので9位という結果は非常に悔しいです。
個人で取り組むと、だれてしまったり諦めてしまいがちなコンペもチームでやればモチベーションも上がる上に、
他の人のアイデアから異なるアイデアが浮かんだりと、アンサンブル学習の威力を実感でき、非常に楽しかったです。
最後3日で順位が20位くらい上がったこともあり、停滞期で諦めないことも大事だなと思いました。 (とはいえ、上位の人たちはずっと上位だったので地力の差も感じました)
今後は今回学んだことをしっかり復習してKaggleでメダルを取れるように頑張っていきたいと思います。 また研究や、企業でデータ分析を生かして社会に貢献できるようにも頑張りたいです。
それでは最後までご覧いただきありがとうございました!
よければTwitterのフォローもよろしくお願いします( ^ω^ )
Pythonにおける勾配ブースティング予測モデルをラクチンに作成するラッパーを公開しました
こんばんは、ひぐです。
今回はPythonの勾配ブースティングライブラリを使いやすくしたラッパーについて紹介します。 今回紹介するラッパーを使うと以下のメリットがあります。
- PandasのDataFrameといくつかの引数を渡すだけで予測結果が返ってくる
- 本来はそれぞれ使い方が微妙に異なるライブラリを、全く同じ記法で使える
- 出力した予測値を正解データとすぐに比較できる、可視化メソッドが使える
- パラメータやValidationの分け方を簡単に指定できる
- ターゲットエンコーディングが必要な場合、列と関数を渡すことで自動でリークしないように計算してくれる
機械学習を用いたデータ分析で必ず必要になる予測モデルを作成するプロセスですが、
これらをいつも同じ使い方で使用できるのは大きなメリットだと思います!
よければ是非使ってください!
使い方
用意するもの
- train/testデータ(DataFrame)
- ハイパーパラメータ(dict)
まず使用するハイパーパラメータを定義します。
from script import RegressionPredictor cat_params = { 'loss_function': 'RMSE', 'num_boost_round': 5000, 'early_stopping_rounds': 100, } xgb_params = { 'num_boost_round':5000, 'early_stopping_rounds':100, 'objective': 'reg:linear', 'eval_metric': 'rmse', } lgbm_params = { 'num_iterations': 5000, 'learning_rate': 0.05, 'objective': 'regression', 'metric': 'rmse', 'early_stopping_rounds': 100}
そしてインスタンスの呼び出し、学習します。
catPredictor = RegressionPredictor(train_df, train_y, test_df, params=cat_params,n_splits=10, clf_type='cat') catoof, catpreds, catFIs=catPredictor.fit() xgbPredictor = RegressionPredictor(train_df, train_y, test_df, params=xgb_params,n_splits=10, clf_type='xgb') xgboof, xgbpreds, xgbFIs = xgbPredictor.fit() lgbPredictor = RegressionPredictor(train_df, train_y, test_df, params=lgbm_params,n_splits=10, clf_type='lgb') lgboof, lgbpreds, lgbFIs = lgbPredictor.fit() rfPredictor = RegressionPredictor(train_df, train_y, test_df, sk_model=RandomForestRegressor(rf_params), n_splits=10, clf_type='sklearn') rfoof, rfpreds, rfFIs = rfPredictor.fit()
これだけです!
fitの返り値はそれぞれ、trainの予測データ、testの予測データ、Feature Importanceのnumpy arrayです 。
Kaggleなどのデータ分析の場合、これらをcsvにするだけですぐに提出できるようになります。
予測値についてデータ可視化したい場合は以下のようにします。
lgbPredictor.plot_FI(50)
lgbPredictor.plot_pred_dist()
ソースコード
class RegressionPredictor(object): ''' 回帰をKfoldで学習するクラス。 TODO:分類、多クラス対応/Folderを外部から渡す/predictのプロット/できれば学習曲線のプロット ''' def __init__(self, train_X, train_y, split_y, test_X, params=None, Folder=None, sk_model=None, n_splits=5, clf_type='xgb'): self.kf = Folder(n_splits=n_splits) self.columns = train_X.columns.values self.train_X = train_X self.train_y = train_y self.test_X = test_X self.params = params self.oof = np.zeros((len(self.train_X),)) self.preds = np.zeros((len(self.test_X),)) if clf_type == 'xgb': self.FIs = {} else: self.FIs = np.zeros(self.train_X.shape[1], dtype=np.float) self.sk_model = sk_model self.clf_type = clf_type @staticmethod def merge_dict_add_values(d1, d2): return dict(Counter(d1) + Counter(d2)) def rmse(self): return int(np.sqrt(mean_squared_error(self.oof, self.train_y))) def get_model(self): return self.model def _get_xgb_callbacks(self): '''nround,early_stopをparam_dictから得るためのメソッド''' nround=1000 early_stop_rounds=10 if self.params['num_boost_round']: nround=self.params['num_boost_round'] del self.params['num_boost_round'] if self.params['early_stopping_rounds']: early_stop_rounds=self.params['early_stopping_rounds'] del self.params['early_stopping_rounds'] return nround ,early_stop_rounds def _get_cv_model(self, tr_X, val_X, tr_y, val_y, val_idx): if self.clf_type == 'cat': clf_train =Pool(tr_X,tr_y) clf_val =Pool(val_X,val_y) clf_test =Pool(self.test_X) self.model=CatBoost(params=self.params) self.model.fit(clf_train,eval_set=[clf_val]) self.oof[val_idx]=self.model.predict(clf_val) self.preds += self.model.predict(clf_test) / self.kf.n_splits self.FIs += self.model.get_feature_importance() elif self.clf_type == 'lgb': clf_train = lgb.Dataset(tr_X, tr_y) clf_val = lgb.Dataset(val_X, val_y, reference=lgb.train) self.model = lgb.train(self.params, clf_train, valid_sets=clf_val) self.oof[val_idx] = self.model.predict(val_X, num_iteration=self.model.best_iteration) self.preds += self.model.predict(self.test_X, num_iteration=self.model.best_iteration) / self.kf.n_splits self.FIs += self.model.feature_importance() elif self.clf_type == 'xgb': clf_train = xgb.DMatrix(tr_X, label=tr_y, feature_names=self.columns) clf_val = xgb.DMatrix(val_X, label=val_y, feature_names=self.columns) clf_test = xgb.DMatrix(self.test_X, feature_names=self.columns) evals = [(clf_train, 'train'), (clf_val, 'eval')] evals_result = {} nround,early_stop_rounds= self._get_xgb_callbacks() self.model = xgb.train(self.params, clf_train, num_boost_round=nround, early_stopping_rounds=early_stop_rounds, evals=evals, evals_result=evals_result) self.oof[val_idx] = self.model.predict(clf_val) self.preds += self.model.predict(clf_test) / self.kf.n_splits self.FIs = self.merge_dict_add_values(self.FIs, self.model.get_fscore()) elif self.clf_type == 'sklearn': self.model = self.sk_model self.model.fit(tr_X, tr_y) self.oof[val_idx] = self.model.predict(val_X) self.preds += self.model.predict(self.test_X) / self.kf.n_splits self.FIs += self.model.feature_importances_ else: raise ValueError('clf_type is wrong.') def fit(self): for train_idx, val_idx in self.kf.split(self.train_X, self.train_y): X_train = self.train_X.iloc[train_idx, :] X_val = self.train_X.iloc[val_idx, :] y_train = self.train_y[train_idx] y_val = self.train_y[val_idx] self._get_cv_model(X_train, X_val, y_train, y_val, val_idx) print('this self.model`s rmse:',self.rmse()) return self.oof, self.preds, self.FIs def plot_FI(self, max_row=50): plt.figure(figsize=(10, 20)) if self.clf_type == 'xgb': df = pd.DataFrame.from_dict(self.FIs, orient='index').reset_index() df.columns = ['col', 'FI'] else: df = pd.DataFrame({'FI': self.FIs, 'col': self.columns}) df = df.sort_values('FI', ascending=False).reset_index(drop=True).iloc[:max_row, :] sns.barplot(x='FI', y='col', data=df) plt.show() def plot_pred_dist(self): fig, axs = plt.subplots(1, 2, figsize=(18, 5)) sns.distplot(self.oof, ax=axs[1], label='oof') sns.distplot(self.train_y, ax=axs[0], label='train_y') sns.distplot(self.preds, ax=axs[0], label='test_preds') plt.show()
以上です!
未実装な部分はいっぱいあるので逐次修正していきたいと思います!
ゆくゆくは親クラスを作って、分類回帰でクラスを分けて継承していくみたいにしたいと思います。
こういうふうに実装した方がいいよなど知見があればコメント頂けたら幸いです。
最後まで読んでいただきありがとうございました~
Wantedlyの機械学習エンジニアインターンに3週間いってきました
ひぐです!8/19~9/6の期間にWantedly社でMLエンジニアコースで働かせていただきました!
楽しかったのでブログを書きたいと思います。
志望動機と選考
魔法のスプレッドシートでやれること・日程・給与などなどを見比べながら決めました。
2019夏のITエンジニアインターンの情報が集まる魔法のスプレッドシート - Google スプレッドシート
メンターさんが1on1で付いてくれること、サービスを知っているからこそ裏側を知るのが楽しそうと言った理由も大きかったです。
選考はES->コーディングテスト->Skype面接でした。
コーディングテストはAtCorder ABCのBくらい?の難易度でした。
何をしたか
Wanteldy Peopleのユーザにタグをつける。
Wantedly Peopleという名刺管理アプリケーションの改善するためDeepLearningでなんとかすることになりました。
はじめに抽象度の高い課題をいくつか提示していただいて、 サーバーからデータをクエリで拾い、データを見ながら、実現可能性がありそうなアプローチを考えていくという段階からスタートしました。
僕が取り組んだタスクは、ユーザの職業欄からタグをつけるというものです。
例えば職業欄が
[取締役執行委員 社長]/[CEO]/[社長 取締役]⇨[社長]タグを付与
[〇〇営業所 部長]/[セールスエンジニア]⇨[営業]タグを付与
のようになります。要は名寄せとかカテゴリ付与みたいなタスクです。
名寄せができれば以下のメリットがあります
- ユーザがフォロワーを検索するときの足がかりになる
- 広告などのレコメンドのターゲッティングに使える指標となる
しかし、この課題には以下のような障害がありました
ユーザ一人当たりの情報が少ない
読み込まれた名刺の持ち主はPeopleのユーザではないことが殆ど、プロフィール文やフォロワーの分布から予測はできない表記揺れがマジで多い
社長を表す表現一つにとっても、「社長」「取締役代表」「代表取締役社長」「CEO」みたいに色々な種類がある
「取締役代表 補佐」とか「取締役 秘書」とかは取り除かないといけないなどの問題も
最終的には
「事前学習モデルから分散表現を獲得し、名刺に含まれる単語の平均ベクトルを学習して予測を立てる」
というアプローチになりました。
簡単にいうと、単語の意味を表すベクトルを使って、そのベクトルの近さとかでグループ分けをしようって感じです。
分散表現についてはこちらのサイトでわかりやすく説明されていました。
deepage.net
職業が同じ単語(「社長」や「代表取締役」など)は意味空間でのベクトルが近いので同じタグを貼れるだろうというアプローチです。
前処理はこんな感じです。
今回はアノテーションはルールベースで行いました。 例えば名刺に「医」「療」という単語が含まれていれば医療ラベルを付与といった感じです。
結果としては予測精度は高かったのですが、精度の高さ=ルールをどれだけ守っているかになってしまい、 オフラインでの評価は難しかったです。
一応ルールベースでのアノテーションに変わる方法を何個か提示しました。 時間があれば半教師あり学習とかもできたらなぁって感じで終わりました。
機械学習エンジニアとして働くことを体験して
Kaggleなどでは決められたタスクに対して高速で実装していく力などが求められますが、
実務では、どんなアプローチがあって、どのデータなら使えるかなど、課題設定から始めなくてはいけないと改めて気づかされました。
また評価指標やアノテーションの仕方も考える必要があって、やりがいがありました。
そういった意味ではアイデア力も必要ですし、アイデアを産むために数理的な知識も、ビジネスの知識も必要だなぁと痛感しました。
こう言ったことを知れたのはメンターの縣さんが何を持ち帰ってもらおうか考えてインターン生をサポートしてくださったからこそだと思います。
本当にありがとうございました!
できるようになったこと・学んだこと
GItHubを綺麗に使えるようになった
レポジトリを見てWantedly社は相当GItHubを使いこなしているなという印象を受けました。 他のレポジトリなどを見ながら、コードレビューしてもらいやすいプルリクの作り方や、後世に遺産が伝わりやすいIssueの書き方などを知れました。便利なオープンソースコードを知れた インターンでFastTextやBERTなどの事前学習モデルをクローンして使うことがあったのですが、あんなにお手軽に再学習や、分散表現を手に入れらるとは思いませんでした。 特にFastTextはgensimのWord2Vecクラスは便利なメソッドが多くて、今後も使おうと思います。
またBigQueryとかPlotlyとかも書けるようになりました。機械学習エンジニアのリアルを知れた
上記に書いたとおりです。
Wantedlyのインターンの印象
たしかに就業型インターンでした
取り組んだ課題がインターン生のために用意された課題ではなく、インターン生を一人の社員として見てくださっている感じがありました。 給料をいただいているし、メンターの方の時間も割いていただきながら仕事をするので、進捗産まなきゃ。。。みたいな緊張感はありましたが、詰まったらいつでも聞いてねという感じだったのでありがたかったです。
オフィスも街も綺麗
オフィスが白金台だしピカピカだしテンション上がりました。
あと会議室の名前が全部ジョジョのスタンドの名前でウケました。 「ザ・ワールドで話そうか」みたいな。出てこれなくなっちゃいそう。
業務後は卓球もできました。
同じタームの子と仲良くなった
席が近い子とは毎日昼ごはんに行ったり、シャッフルランチが毎週あったりで仲良くなれました。
ただインターン生全員と席が近いわけではないので、全員と話すには割と能動的に行動しないといけないかもです。
インターンの選考対策にしたこと
自分も去年はこういうブログを見て、インターン行きてえ。。。!ってなってたのですが、
「おすすめなので是非行ってみてください!」
的な文章を見るたびに、いや受かる前提やんけ!みたいな気持ちになってました。
誰得だよって感じですが、自分なりにES/面接で気をつけたことを書きたいと思います。
成果ベースで話す
〇〇を勉強中です!みたいなことを推すのは弱いと思ったので、なるべく成果ベースで話しました。 アウトプットが社外に公開されるような会社で長期インターンをしつつ、大きな仕事があれば積極的に関われるようにコミニュケーションを取るととっかかりやすいかもです。 あとはKaggleとかもメダルは取れたわけではないのですが、ちゃんとSubmitして順位が出ているので伝えました。自分の関心領域と業務内容が近い(近そう)ということを語る
当たり前ですが、業務内容のミスマッチは企業側も避けたいと思っているので、
自分の経験や興味が企業に取り組む内容に近いことを推していくのは大事だと思います。
株主総会向けのプレゼン資料とかに、企業のビジョンや今後注力する領域がわかりやすくまとめてあるのでオススメです。やる気を見せる
記述式の問題や、ESの文字数制限ないところは人よりたくさん書いたと思います。最強の技術力があれば関係ない
それはそう応募しないと絶対受からないので、いっぱい出す
それはそう
終わり
こういったインターン行くと成長の糧になることをいっぱい学べるし、
仲間やメンターさんとのつながりもできるし、本当に参加できてよかったと思ってます!
今回学んだことを研究や将来の職業でも活かせるように今後も頑張ります!
3週間ありがとうございましたー!
GitHubに画像解析用のKerasディレクトリを公開しました。
お久しぶりです、ひぐです!
大学院に入ってからニューラルネットワークを使って、医療画像の補完を行う研究をするようになりました。
そこで今日は自分が普段使ってる研究用のコードを紹介したいと思います!
結構KaggleやQiitaとかでNNライブラリ用のソースコードを検索すると、使い切りなコードが多くないですか?
今回は繰り返し実験できるようにディレクトリごとコードを公開しました!
モデルの保存や、出力結果の記録をクラス単位で実装しています。よかったら参考にしてください。
なかなか上手に書けない部分もあるので、ご教授いただければ幸いです。。。笑
というかまだまだ絶賛修正中なので温かく見守ってくださいw
URLはこちら github.com
コードの概要
全体的にはこんなイメージです。
主な機能は以下のようになっています。
* main.pyを走らせると自動で、loss関数のグラフ、出力画像を自動作成
* batch size training rateなどのハイパーパラメータははmain.py の引数で渡せる
* データをジェネレーターで読み込むのでデータ量が多くなってもメモリエラーにならない
* 結果を出力するディレクトリに使用したModelの名前とlossの値を使用する(ので見やすい)
main.py
import いろいろ(省略) INPUT_SIZE = (256, 256) CONCAT_LEFT_RIGHT=True CHANGE_SLIDE2_FILL = True def train(parser): configs = json.load(open('./settings.json')) reporter = Reporter(parser=parser) loader = Loader(configs['dataset_path2'], parser.batch_size) if CHANGE_SLIDE2_FILL: loader.change_slide2fill() reporter.add_log_documents('Done change_slide2fill.') if CONCAT_LEFT_RIGHT: loader.concat_left_right() reporter.add_log_documents('Done concat_left_right.') train_gen, valid_gen, test_gen = loader.return_gen() train_steps, valid_steps, test_steps = loader.return_step() # ---------------------------model---------------------------------- input_channel_count = parser.input_channel output_channel_count = 3 first_layer_filter_count = 32 network = UNet(input_channel_count, output_channel_count, first_layer_filter_count) model = network.get_model() model.compile(optimizer='adam', loss='mse') model.summary() # ---------------------------training---------------------------------- batch_size = parser.batch_size epochs = parser.epoch config = tf.ConfigProto() config.gpu_options.per_process_gpu_memory_fraction = 0.9 config.gpu_options.allow_growth = True sess = tf.Session(config=config) # fit_generatorのコールバック関数の指定・TensorBoardとEarlyStoppingの指定 logdir = os.path.join('./logs', dt.today().strftime("%Y%m%d_%H%M")) os.makedirs(logdir, exist_ok=True) tb_cb = TensorBoard(log_dir=logdir, histogram_freq=1, write_graph=True, write_images=True) es_cb = EarlyStopping(monitor='val_loss', patience=parser.early_stopping, verbose=1, mode='auto') print("start training.") # Pythonジェネレータ(またはSequenceのインスタンス)によりバッチ毎に生成されたデータでモデルを訓練します. history = model.fit_generator( generator=train_gen, steps_per_epoch=train_steps, epochs=epochs, validation_data=valid_gen, validation_steps=valid_steps, # use_multiprocessing=True, callbacks=[es_cb, tb_cb]) print("finish training. And start making predict.") train_preds = model.predict_generator(train_gen, steps=train_steps, verbose=1) valid_preds = model.predict_generator(valid_gen, steps=valid_steps, verbose=1) test_preds = model.predict_generator(test_gen, steps=test_steps, verbose=1) print("finish making predict. And render preds.") # ==========================report==================================== reporter.add_val_loss(history.history['val_loss']) reporter.add_model_name(network.__class__.__name__) reporter.generate_main_dir() reporter.plot_history(history) reporter.save_params(parser, history) input_img_list = [] # reporter.plot_predict(train_list, Left_RGB, Right_RGB, train_preds, INPUT_SIZE, save_folder='train') reporter.plot_predict(loader.train_list, loader.Left_slide, loader.Left_RGB, train_preds, INPUT_SIZE, save_folder='train') reporter.plot_predict(loader.valid_list, loader.Left_slide, loader.Left_RGB, valid_preds, INPUT_SIZE, save_folder='valid') reporter.plot_predict(loader.test_list, loader.Left_slide, loader.Left_RGB, test_preds, INPUT_SIZE, save_folder='test') model.save("model.h5") def get_parser(): parser = argparse.ArgumentParser( prog='generate parallax image using U-Net', usage='python main.py', description='This module generate parallax image using U-Net.', add_help=True ) parser.add_argument('-e', '--epoch', type=int, default=200, help='Number of epochs') parser.add_argument('-b', '--batch_size', type=int, default=32, help='Batch size') parser.add_argument('-t', '--trainrate', type=float, default=0.85, help='Training rate') parser.add_argument('-es', '--early_stopping', type=int, default=20, help='early_stopping patience') parser.add_argument('-i', '--input_channel', type=int, default=7, help='input_channel') parser.add_argument('-a', '--augmentation', action='store_true', help='Number of epochs') return parser if __name__ == '__main__': parser = get_parser().parse_args() train(parser)
ディレクトリのパスはsetting.jsonで一括管理しています。
trainという巨大な関数にargparserで引数を渡して、ハイパーパラメータを用いています。
自分の研究では、入力、教師データどちらにも画像を使うため独自のジェネレータを作成しています。
repoter.py
importあれこれ class Reporter: ROOT_DIR = "Result" IMAGE_DIR = "image" LEARNING_DIR = "learning" INFO_DIR = "info" MODEL_DIR = "model" PARAMETER = "parameter.txt" IMAGE_PREFIX = "epoch_" IMAGE_EXTENSION = ".png" def __init__(self, result_dir=None, parser=None): self._root_dir = self.ROOT_DIR self.create_dirs() self.parameters = list() # def make_main_dir(self): def add_model_name(self, model_name): if not type(model_name) is str: raise ValueError('model_name is not str.') self.model_name = model_name def add_val_loss(self, val_loss): self.val_loss = str(round(min(val_loss))) def generate_main_dir(self): main_dir = self.val_loss + '_' + dt.today().strftime("%Y%m%d_%H%M") + '_' + self.model_name self.main_dir = os.path.join(self._root_dir, main_dir) os.makedirs(self.main_dir, exist_ok=True) def create_dirs(self): os.makedirs(self._root_dir, exist_ok=True) def plot_history(self,history,title='loss'): # 後でfontsize変える plt.rcParams['axes.linewidth'] = 1.0 # axis line width plt.rcParams["font.size"] = 24 # 全体のフォントサイズが変更されます。 plt.rcParams['axes.grid'] = True # make grid plt.plot(history.history['loss'], linewidth=1.5, marker='o') plt.plot(history.history['val_loss'], linewidth=1., marker='o') plt.tick_params(labelsize=20) plt.title('model loss') plt.xlabel('epoch') plt.ylabel('loss') plt.legend(['loss', 'val_loss'], loc='upper right', fontsize=18) plt.tight_layout() plt.savefig(os.path.join(self.main_dir, title + self.IMAGE_EXTENSION)) if len(history.history['val_loss'])>=10: plt.xlim(10, len(history.history['val_loss'])) plt.ylim(0, int(history.history['val_loss'][9]*1.1)) plt.savefig(os.path.join(self.main_dir, title +'_remove_outlies_'+ self.IMAGE_EXTENSION)) def add_log_documents(self, add_message): self.parameters.append(add_message) def save_params(self,parser,history): #early_stoppingを考慮 self.parameters.append("Number of epochs:" + str(len(history.history['val_loss']))) self.parameters.append("Batch size:" + str(parser.batch_size)) self.parameters.append("Training rate:" + str(parser.trainrate)) self.parameters.append("Augmentation:" + str(parser.augmentation)) self.parameters.append("input_channel:" + str(parser.input_channel)) self.parameters.append("min_val_loss:" + str(min(history.history['val_loss']))) self.parameters.append("min_loss:" + str(min(history.history['loss']))) # self.parameters.append("L2 regularization:" + str(parser.l2reg)) output = "\n".join(self.parameters) filename=os.path.join(self.main_dir,self.PARAMETER) with open(filename, mode='w') as f: f.write(output) @staticmethod def get_concat_h(im1, im2): dst = Image.new('RGB', (im1.width + im2.width, im1.height)) dst.paste(im1, (0, 0)) dst.paste(im2, (im1.width, 0)) return dst def plot_predict(self, img_num_list, Left_RGB, Right_RGB, preds, INPUT_SIZE, max_output=20,save_folder='train'): if len(img_num_list) > max_output: img_num_list=img_num_list[:max_output] for i, num in enumerate(img_num_list): if i == 1: print(preds[i].astype(np.uint8)) pred_img = array_to_img(preds[i].astype(np.uint8)) train_img = load_img(Left_RGB[num], target_size=INPUT_SIZE) teach_img = load_img(Right_RGB[num], target_size=INPUT_SIZE) concat_img = self.get_concat_h(train_img, pred_img) concat_img = self.get_concat_h(concat_img, teach_img) os.makedirs(os.path.join(self.main_dir,save_folder), exist_ok=True) array_to_img(concat_img).save(os.path.join(self.main_dir, save_folder, f'pred_{num}.png'))
データの保存を担っています。
Kerasではfit関数を動かすとその返り値にhistoryオブジェクトという出力のログが入ったインスタンスを返します。
これと、parserをmain.pyから受け取ってデータを保存しています。
保存先はResult dir以下に、使用パラメータ・出力結果・lossグラフなどをまとめて格納します。
loader.py
import あれこれ config = json.load(open('./settings.json')) class Loader(object): # コンストラクタ def __init__(self, json_paths, batch_size, init_size=(256, 256)): self.size = init_size self.DATASET_PATH = json_paths self.add_member() self.batch_size = batch_size # def define_IO(self): def add_member(self): """ jsonファイルに記載されている、pathをクラスメンバとして登録する。 self.Left_RGBとかが追加されている。 """ for key, path in self.DATASET_PATH.items(): setattr(self, key, glob.glob(os.path.join(path, '*png'))) #左右の画像を結合してデータを二倍にする def concat_left_right(self): self.Left_slide += self.Right_slide self.Left_RGB += self.Right_RGB self.Left_disparity += self.Left_disparity self.Right_disparity += self.Right_disparity self.Left_bin += self.Left_bin self.Right_bin += self.Right_bin print('Done concat_left_right.') #入力で使う画像を平均値で埋めた画像にする def change_slide2fill(self): self.Left_slide = self.Left_fill self.Right_slide = self.Right_fill def return_gen(self): self.imgs_length = len(self.Left_RGB) # self.train_paths = (self.Left_slide, self.Right_disparity, self.Left_disparity) # sel = self.Left_RGB self.train_list, self.valid_list, self.test_list = self.train_valid_test_splits(self.imgs_length) self.train_steps = math.ceil(len(self.train_list) / self.batch_size) self.valid_steps = math.ceil(len(self.valid_list) / self.batch_size) self.test_steps = math.ceil(len(self.test_list) / self.batch_size) self.train_gen = self.generator_with_preprocessing(self.train_list, self.batch_size) self.valid_gen = self.generator_with_preprocessing(self.valid_list, self.batch_size) self.test_gen = self.generator_with_preprocessing(self.test_list, self.batch_size) return self.train_gen, self.valid_gen, self.test_gen def return_step(self): return self.train_steps, self.valid_steps, self.test_steps @staticmethod def train_valid_test_splits(imgs_length: 'int', train_rate=0.8, valid_rate=0.1, test_rate=0.1): data_array = list(range(imgs_length)) tr = math.floor(imgs_length * train_rate) vl = math.floor(imgs_length * (train_rate + valid_rate)) random.shuffle(data_array) train_list = data_array[:tr] valid_list = data_array[tr:vl] test_list = data_array[vl:] return train_list, valid_list, test_list def load_batch_img_array(self, batch_list, prepro_callback=False,use_bin=True): teach_img_list = [] input_img_list = [] for i in batch_list: LS_img = img_to_array( load_img(self.Left_slide[i], color_mode='rgb', target_size=self.size)).astype(np.uint8) LD_img = img_to_array( load_img(self.Left_disparity[i], color_mode='grayscale', target_size=self.size)).astype(np.uint8) RD_img = img_to_array( load_img(self.Right_disparity[i], color_mode='grayscale', target_size=self.size)).astype(np.uint8) if use_bin: LB_img = img_to_array( load_img(self.Left_bin[i], color_mode='grayscale', target_size=self.size)).astype(np.uint8) # LB_img=np.where(LB_img==255,1,LB_img) RB_img = img_to_array( load_img(self.Right_bin[i], color_mode='grayscale', target_size=self.size)).astype(np.uint8) # RB_img = np.where(RB_img == 255, 1, RB_img) input_img = np.concatenate((LS_img, LD_img, RD_img, LB_img, RB_img), 2).astype(np.uint8) else: input_img=np.concatenate((LS_img, LD_img, RD_img), 2).astype(np.uint8) teach_img = img_to_array( load_img(self.Left_RGB[i], color_mode='rgb', target_size=self.size)).astype(np.uint8) input_img_list.append(input_img) teach_img_list.append(teach_img) return np.stack(input_img_list), np.stack(teach_img_list) def generator_with_preprocessing(self, img_id_list, batch_size):#, *input_paths while True: for i in range(0, len(img_id_list), batch_size): batch_list = img_id_list[i:i + batch_size] batch_input, batch_teach = self.load_batch_img_array(batch_list) yield(batch_input, batch_teach) class DataSet: pass
Data dirからデータ(画像)を読み取ってmain.pyにジェネレータ形式で渡します。
このコードは特にまだまだ改善の余地があります...
実験ごとに入力チャンネル数を変えたいのですが、
ジェネレータに読みだした後、それらを結合するとことが難しく、悩んでいます。
jsonからディレクトリのパスを受け取って、その直下の画像ファイルをすべてクラスメンバにして
読み込むようにしているのがおしゃれポイントです
for key, path in self.DATASET_PATH.items(): setattr(self, key, glob.glob(os.path.join(path, '*png')))
おわりに
ザックリですが以上になります!
わからないところや修正したほうがいいと思う部分がありましたら、連絡いただけたら幸いです!
今後は他の人でも使えるように、どんなタスクでも使えるように、調整して再度公開したいです。
就職したときにも、恥ずかしくないようにきれいで再利用性の高いコードをかけるように頑張っていきたいです!
では~
Santanderコンペで学ぶ、EDA(探索的データ解析)の手法
お久しぶりです、ひぐです!
先日、Kaggleの Santander Customer Transaction Predictionコンペティション(以下、Santanderコンペ)
に参加したのですが、凄惨な結果に終わってしまいました。。。
そこで今回の記事では自分の復習もかねて、上位の方の解法をまとめつつ
データ分析に必要なEDAの手法を紹介していきたいと思います!
この記事を読むのに必要な前提知識
くわしくは↓
www.codexa.net
- EDAとは?
Explanatory Data Analysis の略。日本語で言うと、探索的データ解析。
データの特徴や構造を理解するためにグラフを作成し、
特徴量の相関やターゲットとの関係性を調べることです。
くわしくは↓
Santanderコンペってどんなコンペ?
Santanderというアメリカの銀行が開いたコンペ。
トレーニング・テストデータどちらも
20万行、かつ200列の特徴量から構成されている。
特定の取引を行った行にtarget==1が付与され、それ以外にtarget==0が付与されている。
全ての特徴量がfloat型かつvar_iと命名されていて、どんな特徴量か分からない様に匿名化されている。
どんなことに気づけばスコアが伸びたのか
このコンペでは大きく分けて、
- testデータにある、計算に用いられない偽データの存在
- 逆向きになっている列の存在(0行目の値が200000行目の値、1行目→199999,2→199998...)
- 値が重複しているかどうかの特徴量
- 各列が独立であるということ
これらに気づけた場合、精度があげることが出来たそうです(どうやってわかるんだよ)
これらを踏まえてEDAしていきましょう。
1位の方の解法はこちら↓
#1 Solution | Kaggle
EDAの手法紹介
基礎編(どんなコンペでも用いるEDA)
基礎編ではこの方のKernelを参考にしました。ありがとうございます。
https://www.kaggle.com/gpreda/santander-eda-and-prediction
0.ライブラリimport
import gc import os import logging import datetime import warnings import numpy as np import pandas as pd import seaborn as sns import lightgbm as lgb from tqdm import tqdm_notebook import matplotlib.pyplot as plt from sklearn.metrics import mean_squared_error from sklearn.metrics import roc_auc_score, roc_curve from sklearn.model_selection import StratifiedKFold warnings.filterwarnings('ignore')
1.データ読み込み
import feather as ftr train = ftr.read_dataframe("../data/input/train.feather") test = ftr.read_dataframe("../data/input/test.feather")
read_csvより早いので、feather形式で読み込みます。
こちらを参考にさせていただきました↓
Kaggleで使えるFeather形式を利用した特徴量管理法 - 天色グラフィティ
2.データの大きさ、最初の数行確認
print(train.shape)
train.head()
3.各列の欠損値を確認
def missing_data(data): #欠損値を含む行の合計数 total = data.isnull().sum() #欠損値を含む行の合計(%表示) percent = (data.isnull().sum()/data.isnull().count()*100) tt = pd.concat([total, percent], axis=1, keys=['Total', 'Percent']) types = [] for col in data.columns: dtype = str(data[col].dtype) types.append(dtype) #各列の型を確認 tt['Types'] = types #行列を入れ替える missing_data(train)
結果↓
欠損値は全くなく、すべての特徴量がfloat64です。
4.列ごとの統計量の確認
train.describe()
結果
target=1が全体の10%ほどという、不均衡データであることを確認しておきましょう。
5.各特徴量のtarget==0の行,target==1の行の分布比較
def plot_feature_distribution(df1, df2, label1, label2, features): i = 0 sns.set_style('whitegrid') plt.figure() fig, ax = plt.subplots(10,10,figsize=(18,22)) for feature in features: i += 1 plt.subplot(10,10,i) sns.kdeplot(df1[feature], bw=0.5,label=label1) sns.kdeplot(df2[feature], bw=0.5,label=label2) plt.xlabel(feature, fontsize=9) locs, labels = plt.xticks() plt.tick_params(axis='x', which='major', labelsize=6, pad=-6) plt.tick_params(axis='y', which='major', labelsize=6) plt.show(); t0 = train.loc[train['target'] == 0] t1 = train.loc[train['target'] == 1] features = train.columns.values[2:102] plot_feature_distribution(t0, t1, '0', '1', features)
結果↓
こちらで使っているseabornのkdeplotはカーネル密度推定と呼ばれるものです。
分布の密度関数推定に使われる、ノンパラメトリック推定の一種です。
簡単に説明すると、ヒストグラムと同じような分布の推定方法だけど、滑らかになるように操作を加えています(雑)
詳しくはこちらなどを参考にしてください。
↓
http://www.ocw.titech.ac.jp/index.php?module=General&action=DownLoad&file=200927244-21-0-19.pdf&type=cal&JWC=200927244
6.trainとtestデータの各行の平均値の分布を比較
plt.figure(figsize=(16,6)) features = train_df.columns.values[2:202] plt.title("Distribution of mean values per row in the train and test set") sns.distplot(train_df[features].mean(axis=1),color="green", kde=True,bins=120, label='train') sns.distplot(test_df[features].mean(axis=1),color="blue", kde=True,bins=120, label='test') plt.legend() plt.show()
結果↓
train,testの分布がほとんど同じということが分かります。
.mean(axis=1)
の部分を.mean(axis=0)
にすれば列の平均値を集約し、分布の推定が行われます。
他にも、std,min,max,skew,kurtosis
などとすることで、標準偏差、最小値、最大値、尖度、歪度などの分布も確認できます。
train,とtestの分布が異なる場合は、適切にバリデーションを行わないと、
過学習を起こしてしまうので要注意です。
u++さんの記事が非常にわかりやすいのでよかったら参考にしてください。
upura.hatenablog.com
これを応用して、trainデータをtarget==0,target==1で分けて同様に分布を確認することが出来ます。
#分け方↓ t0 = train.loc[train['target'] == 0] t1 = train.loc[train['target'] == 1]
7.重複の存在する特徴量の確認
features = train.columns.values[2:202] unique_max_train = [] unique_max_test = [] for feature in features: values = train[feature].value_counts() unique_max_train.append([feature, values.max(), values.idxmax()]) values = test[feature].value_counts() unique_max_test.append([feature, values.max(), values.idxmax()]) np.transpose((pd.DataFrame(unique_max_train, columns=['Feature', 'Max duplicates', 'Value'])).\ sort_values(by = 'Max duplicates', ascending=False).head(15))
結果↓
これが非常に大切だったのです...(のちに紹介)
8.各列ごとの相関係数確認
#各列の相関係数を求め、絶対値を取り、行列を1列に直し、相関係数の大きさで並べ替え、indexを再度付与 correlations = train[features].corr().abs().unstack().sort_values(kind="quicksort").reset_index() #同じ列同士の行は消去 correlations = correlations[correlations['level_0'] != correlations['level_1']] correlations.head(10)
結果↓
非常に相関係数が小さいことが分かります。
このことから、それぞれの特徴量は独立に扱ってよいという考えに至れると、
新たな学習方法を行うことが出来ます。
9.各特徴量毎のtarget==0,target==1の散布図。
train.loc[train['target']==0]['var_180'].plot(figsize=(30,30),style='.') train.loc[train['target']==1]['var_180'].plot(figsize=(30,30),style='.')
結果↓
このKernelを参考にしました。
In Search of Weirdness | Kaggle
横軸が行番号、縦列が各要素の値です。
青がtarget==0,オレンジがtarget==1です。
自分は各特徴量毎にこの散布図を作成し、なめるように見ていました。
ここでは、0.48付近にtarget==1が多く含まれていて、効きそうな特徴量が作れそうですが、
この図だけでは青がどれだけ密集しているか詳しくはわからないので要注意です。
手法5で確認を行えばよいですが、
plt.figure(figsize=(30,30)) sns.kdeplot(train.loc[train['target']==1]['var_108'], bw=0.2) sns.kdeplot(train.loc[train['target']==0]['var_108'], bw=0.2)
とすることで、特徴量毎に大きく表示し、確認することもできます。
応用編(Santanderで役に立つEDA)
それぞれ長くなりそうなので、別の記事にしました。
- 各特徴量を独立と仮定した修正ナイーブベイズ法によるtarget確率分布推定
初めて、本気で参加したコンペについての感想
Santanderコンペについて
欠損値もなく、データも大きくなく、すべて数値型だったので取り組みやすい!
と思って始めたのですが、実際は人と差をつけることが難しく、スコアも伸びず大変でした…
LBスコアが非常に密集していて、
kernelで公開されている、0.901スコア代の人が3000人くらいいたのが特徴的でした。
自分の足りなかった部分
- 着手し始めるのが遅すぎた。
- NNライブラリを扱えず、アンサンブルができなかった
- モデルの理解度が低く、LGBMでは効かない特徴量を作ってしまった。
- 英語力が足りず、discussionから必要な情報を収集しきれなかった。
- discussionに書いてあることをデータにうまく落とし込めなかった。
EX)各列が無相関→各列を縦に連結し、(4000万行,2列)のデータセットにするなど
参加して得られたこと
- Kaggleにしっかり参加するときの流れがつかめた。
u++さんが公開してくださった、ディレクトリを用いて
デバッグ、特徴量の追加削除を自由にできる環境でのKaggleを行うことが出来るようになった。
のちのコンペに生かせそうな関数を知れた
EDA等、dfを引数にした汎用的な関数をいくつか知ることが出来たので、今後のコンペに対してスムーズに動生きだせそう。やるき
何の成果も得られず悔しかったので、次はやってやるぞという気持ちになれた。