Higu`s diary

新米データサイエンティストのブログ。技術についてゆるく書きます〜

データ分析でコードをクリーンに保つ技術

こんにちは、ひぐです。
最近データサイエンティストのための良いコーディング習慣という記事を読みました。
www.thoughtworks.com

こうした方がいいよなという自分の経験則が綺麗に言語化されていてよかったです。
ここではデータ分析でコードをクリーンに保つ技術について、記事の内容と自分の取り組みを合わせて紹介したいと思います。

自分はまだチームでの開発経験などが浅いため、間違っている部分もあるかもしれません。
あらかじめご了承ください汗

コードが汚くなる要因

f:id:zerebom:20200610210812p:plain:w300
コードが解くべき問題の複雑さを増長させている時、そのコードは汚いと言えます。
汚いコードは汚い部屋で探し物をする時などと同じく、簡単な作業を困難にしてしまいます。

では、どのような書き方をするとコードが汚くなるのでしょうか。

元記事には下記のような例が記載されています。

  1. 関数やクラスを使って処理を抽象化しない
  2. 一つの関数に長く複数の処理を書く
  3. ユニットテストを書かず、リファクタリング時に1から書き直す

部屋で例えると、

  • 一つの収納箱にあれこれ詰め込む
  • 物の定位置を決めず、空いているところに収納する
  • 整理してない収納箱を全てひっくり返して、再配置する

といった振る舞いと似てそうです。

処理が1箇所に纏まっていないことや、
どこに何が書いてあるかわからないことが複雑さを冗長させていると言えます。

jupyter notebookはコードを煩雑にしやすい

さらにデータ分析でおなじみのjupyter notebookは

  1. df.head()/describe()などデータの内部を確認できる機能が豊富
  2. 上下のセルから変数の中身が引き継がれる

といった特徴から、プロジェクト序盤は素早いフィードバックを得られて便利ですが、
これらの特徴は裏を返せば

  1. 変数の影響範囲が広くなりやすい
  2. 処理に影響を及ぼさないコードが増えやすい

とも言え、コード量が増えると急速に煩雑になってしまいます。

インテリアデザイナーには「平たい場所は乱雑さを蓄積しやすい」 という通説があるそうですが、
何でも1箇所に書けてしまう"notebook"は、データ分析における平たい場所であると言えます。

良いコードにする振る舞い

では良いコードにするにはどのようにすれば良いのでしょうか。
元記事では下記の5点が紹介されていました。

コードを綺麗に保つ

データ分析に限らず、綺麗なコードを書くセオリーがあります。
たとえば

  • DEAD CODEを消す
  • 処理の内容が明快にわかる変数名をつける
  • 似た記述はまとめる(DRYである)

データ分析も例外ではなく、これらのセオリーには従うべきです。
リーダブルコードなどの書籍にまとめられていて、目を通しておくべきでしょう。

関数を使って実装を抽象化する

一つの関心ごとに対しては一つの関数でまとめ、処理を抽象化するべきです。
そうすることで、以下のメリットが得られます。

  • 読みやすい
  • テストしやすい
  • 再利用しやすい(引数を与えて、ハードコーディングを防げる)

これは例を見てみるとわかりやすいです。

# 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詰めする処理ができます。

イメージはこんな感じです笑
f:id:zerebom:20200611102441j:plain:w300 f:id:zerebom:20200611102458j:plain:w300 f:id:zerebom:20200611102508j:plain:w300

引用元(?):
ヘヴィメタバンド、スティール・パンサーのサッチェル氏→機材紹介がハードロック過ぎる - Togetter

なるべく早い段階でjupyter notebookを.pyに移行する

上で言及したように、notebookはコード量に伴い煩雑さが急速に増してしまいます。
したがって、コード量が増えてきたらなるべく早く.pyコードに書き換えるべきです。

notebookからpyファイルに書き換える手順は元記事で下記のように紹介されています。 f:id:zerebom:20200611102550p:plain 引用元: clean-code-ml/refactoring-process.md at master · davified/clean-code-ml · GitHub

  1. notebookが正しく動くか確認する
  2. 自動変換でpyファイルに出力する
  3. debugコードを取り除く(df.head()など)
  4. Code smell(直したい部分)をリストアップする
  5. 一纏めにしたい部分を特定する
  6. ユニットテストを書く
  7. テストを通すように記述する
  8. importを整理する
  9. 動作を確認し、commitする
  10. 繰り返す

テスト駆動開発で行う

データ分析業務もソフトウェア開発の例外にもれず、テストを書くべきです。

例えばモデルの挙動を調べるテストでは、
ベースラインで想定想定スコアを超えない場合はエラーとみなすコードを書くと良いそうです。

テストコードについては自分の知識も浅いので、またいつか改めて記事を書きたいと思います。

こまめなコミットをする

コミットを小さく頻繁に行うことで、下記のメリットが得られます。

  • 自分がどの問題に取り組んでいるか簡単に理解できる
  • 処理のロールバックが簡単にできる

自分なりの工夫点

最後に自分なりのコードを綺麗にする工夫点をいくつか紹介します。

業務ごとにコードをまとめスニペット化する

データ分析では、タスクが変わっても似たような処理を書くことが多いです。
コードをスニペットとして保存しておくと、似たような処理が必要になった時少ない作業量で書き終えることができます。

また、スニペットにすることを意識しながらコードを書くことで
自然と汎用性の高いコードが書けるようになります。

自分はGitHub GistとDashを使ってスニペットを保存しています。

gist.github.com

https://kapeli.com/dash

自分なりのルールを設ける

自分なりのルールを設けて、いつも似たコードを書くようにしています。
そうすることで他のスニペットとの互換性を良くしたり、素早くコードを書くことができます。

また自分はNN系のコードを書くときはhydraとpytorch-lightningを
使うことでいつも同じステップで書けるようにしています。

github.com
hydra.cc

データ分析のコードはあまり高級なラッパーを使うと、すぐ破綻してしまうので
その塩梅が難しいですが、うまく使えば綺麗にかけるでしょう。

メソッド名、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卒で就活中

今年をざっくりまとめる

就活

インターンシップ

下記インターンシップに参加しました。

zerebom.hatenablog.com

参加することで、企業の風土や開発環境を知れるだけでなく、 自分の目標となる先輩や、優秀な同期と出会うことができました。
1月からCAのAI事業本部で長期インターンをさせていただけることになったので、こちらも頑張りたいです。

自己分析

企業との面接でどういうエンジニアになりたい?という質問になかなか答えられず大変でした。 そのため、10月以降は特に自分の将来を真剣に考えました。

多動な人間なのでその時その時でやりたいことは常にたくさんあるのですが、
もっと軸足を定めて考えるべきだったと反省してます。

ざっくりですが、
- 社会的に正しいことをする
- 頭とコミニュケーション両方を使う
- 自分の学びを同業者・同期に還元していく

この3つは自分の中で大きな軸だなぁと思っています。 面接で取り繕って話すことは絶対したくないので、
ちゃんと考えて言葉にできるようにしていきたいです。

研究

学会発表

新規研究

4年次の研究を経て、もっと深層学習ちゃんと勉強したくてテーマを変えました。 大変ですが、後悔はしてないです。

プログラミングの勉強・成果

その他

  • TOEIC 755点になった
  • Twitterのフォロワーが610人になった
  • ブログの月間PVが1500~2500くらいになった
  • 筋トレが4ヶ月くらい続いた、10回*3セットで上がる重量が増えた
    ベンチプレス30->45kg
    デッドリフト40kg->65kg
    スクワット60kg->75kg

今年できたこと

  • たくさん行動する経験する
  • 尊敬できる人に会いにいく
  • 規則正しい生活
  • 人を傷つけない

今年できなかったこと

  • 目標に到達する前にやめてしまった(KaggleとかKaggleとか)
  • ブログ以外のアウトプット(LT/論文投稿など)
  • 部屋をきれいに保つこと
  • 一つ一つを丁寧にこなすこと(手広くやりすぎた)

来年の目標(抽象)

  • 将来の夢を考える(人生の目標)
  • 基礎を固める(線形代数統計学・CS)
  • 専門分野を深く学ぶ

来年の目標(具体)

  • 論文投稿
  • LT登壇
  • TOEIC 850以上
  • Kaggle Master
  • 統計検定1級

まとめ

今年1年間は自分に対して向き合って、たくさん勉強できたなと思っています。
勉強や就活をする上でいろんな人に話を聞きに行ったり、新たな友達ができたのも大きな成果でした。

その一方でエンジニア・院生以外の人とは殆ど会わず、
世間一般から遠ざかったような気持ちもしました。

今年は就活、学校、研究全部あったのでしょうがない部分もありますが、
小粒の成果がいっぱいって感じになってしまいました。 来年以降は一つの目標に対してじっくり取り組んで大きな成果を出していきたいです。

勉強のための勉強ではなく、
なんのために勉強するかも今まで以上にしっかり考えていきたいです。

来年もよろしくお願いします〜

おわり

マイナビ × SIGNATE Student Cup2019に参加して9位でした

こんにちは、ひぐです。

先日マイナビ × SIGNATE Student Cupに参加し、9/342位になりました!
この記事ではどんな取り組みを行ったかを書きたいと思います。
なるべく本コンペに参加してない人にも内容がわかる記事にしたいと思います。

本コンペの基礎情報

マイナビ × SIGNATE Student Cupとは年に1度開かれる、学生のみが参加できるデータ分析のオンライン大会です。
お題とデータが渡され、機械学習を用いて目的変数を予測し精度を競う大会です。

本コンペのテーマは「東京都23区の賃貸物件の家賃予測」です。
各物件に対し、「面積、方角、所在階」などの情報が与えれ、そのデータを元に家賃を予測する、といった内容でした。

f:id:zerebom:20191108215858p:plain
引用:https://signate.jp/competitions/182

データ量はTrain,Testどちらとも3万程度でした。

本コンペの特徴

本コンペのデータは以下4つの特徴があり、これらをうまく取り扱うことが精度向上の鍵になったかと思います。

  • 外れ値がある上に、評価指標がRMSE
    目的変数である賃料は非常に右に裾が長い分布であり、最も高い物件の賃料は250万円もするものでした。
    評価指標がRMSE(二乗平均平方根誤差)であるため、これらの高級物件の誤差をいかに小さくするかが重要でした。

    f:id:zerebom:20191108224222p:plain
    目的変数の分布

  • データが汚い&数値ではなく文字データとして与えられている
    データがすぐに使える形で与えられていなかったため、正規表現等を駆使して情報を取り出す必要がありました。
    また、欠損値や書き間違いも多く含まれ、丁寧に処理する必要がありました。

  • Train,Testで同じようなデータが含まれている
    物件データの中には、同じアパートの別の部屋などが含まれており、 普通に学習するより、学習データと同じ値で埋めるほうが精度が高くなりました。

    f:id:zerebom:20191108224133p:plain
    同じようなデータ群

  • 外部データ使用可能
    このコンペでは外部データの使用が認められていました。
    土地データのオープンデータは非常に多く、どれをどのように使うかが大事になったかと思います。

弊チームの取り組み

最終的なPipelineは以下の通りです。

f:id:zerebom:20191109000508p:plain
Pipeline

基本的に「各物件のデータから推論より、同じ・似た物件データの賃料からキャリブレーションする」 というつもりで進めていました。

コンペ全期間の大まかな流れと精度の変化は以下の通りです。

  • 3人とも個別で学習(17000~16000程度)
  • チームマージしアンサンブル。LogをとってMAEで学習(15000程度)
  • K-meansで近傍データの作成(14500程度)
  • 住所の修正、単位面積あたりの賃料を推定(13000程度)
  • パラメータ調整、SeedAverage(12400程度)

特に住所の修正、単位面積あたりの賃料の推定が大きかったと思います。
順を追って説明します。

Plotlyを用いて予測誤差の原因を追求

簡単に予測モデルを作ってからは、出力誤差をPlotlyを用いて地図上にMapして、どういった物件が誤差が大きいか確認しました。
Plotlyはインタラクティブに描画されるため、ズームしながら一つ一つ確認できました。
詳しくはQiitaに記事を書いたので良ければ見てください↓
qiita.com

同じ住所なのに、違うアパートが含まれていること、
またそういった物件の誤差が大きいことを確認しました。

f:id:zerebom:20191108225251p:plain

欠損値、異常値補完

今回配布されたデータの物件の所在地には
A:「東京都〇〇区××n丁目x-yy」と正確に記載されているデータもあれば、
B:「東京都〇〇区××n丁目」と丁までしか含まれていないデータも多くありました。

これらを注意深く観察すると、同じアパート(賃料・面積などから判断)でも
Aの形で所在地が埋められてるデータもあればBの形のデータもあることがわかりました。

そこで、面積、所在階、室内設備などの複数条件が同じであれば、同一アパートとみなし、
Bの形で所在地が記載されているデータを同じ物件のAの形の所在地に変換しました。
図にするとこんな感じです。
f:id:zerebom:20191109115533p:plain

こうすることで住所や緯度経度を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以下のファイルはほぼ無制限に共有できること、各人の取り組んでる内容、進捗状況がすぐにわかったので、非常にスムーズにコミニュケーションを取れました。

f:id:zerebom:20191108233928p:plain
使い倒されるtrello

参考にしたサイト

飯田コンペ上位手法 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エンジニアコースで働かせていただきました!
f:id:zerebom:20190908180454j:plain
楽しかったのでブログを書きたいと思います。

志望動機と選考

魔法のスプレッドシートでやれること・日程・給与などなどを見比べながら決めました。

2019夏のITエンジニアインターンの情報が集まる魔法のスプレッドシート - Google スプレッドシート

メンターさんが1on1で付いてくれること、サービスを知っているからこそ裏側を知るのが楽しそうと言った理由も大きかったです。

選考はES->コーディングテスト->Skype面接でした。
コーディングテストはAtCorder ABCのBくらい?の難易度でした。

何をしたか

Wanteldy Peopleのユーザにタグをつける。

Wantedly Peopleという名刺管理アプリケーションの改善するためDeepLearningでなんとかすることになりました。

people.wantedly.com

はじめに抽象度の高い課題をいくつか提示していただいて、 サーバーからデータをクエリで拾い、データを見ながら、実現可能性がありそうなアプローチを考えていくという段階からスタートしました。

僕が取り組んだタスクは、ユーザの職業欄からタグをつけるというものです。 例えば職業欄が
[取締役執行委員 社長]/[CEO]/[社長 取締役]⇨[社長]タグを付与
[〇〇営業所 部長]/[セールスエンジニア]⇨[営業]タグを付与

のようになります。要は名寄せとかカテゴリ付与みたいなタスクです。

名寄せができれば以下のメリットがあります

  • ユーザがフォロワーを検索するときの足がかりになる
  • 広告などのレコメンドのターゲッティングに使える指標となる

しかし、この課題には以下のような障害がありました

  • ユーザ一人当たりの情報が少ない
    読み込まれた名刺の持ち主はPeopleのユーザではないことが殆ど、プロフィール文やフォロワーの分布から予測はできない

  • 表記揺れがマジで多い
    社長を表す表現一つにとっても、「社長」「取締役代表」「代表取締役社長」「CEO」みたいに色々な種類がある
    「取締役代表 補佐」とか「取締役 秘書」とかは取り除かないといけないなどの問題も

最終的には
事前学習モデルから分散表現を獲得し、名刺に含まれる単語の平均ベクトルを学習して予測を立てる」 というアプローチになりました。

簡単にいうと、単語の意味を表すベクトルを使って、そのベクトルの近さとかでグループ分けをしようって感じです。

分散表現についてはこちらのサイトでわかりやすく説明されていました。
deepage.net

職業が同じ単語(「社長」や「代表取締役」など)は意味空間でのベクトルが近いので同じタグを貼れるだろうというアプローチです。
前処理はこんな感じです。
f:id:zerebom:20190907193758p:plain

今回はアノテーションはルールベースで行いました。 例えば名刺に「医」「療」という単語が含まれていれば医療ラベルを付与といった感じです。

結果としては予測精度は高かったのですが、精度の高さ=ルールをどれだけ守っているかになってしまい、 オフラインでの評価は難しかったです。

一応ルールベースでのアノテーションに変わる方法を何個か提示しました。 時間があれば半教師あり学習とかもできたらなぁって感じで終わりました。

機械学習エンジニアとして働くことを体験して

Kaggleなどでは決められたタスクに対して高速で実装していく力などが求められますが、
実務では、どんなアプローチがあって、どのデータなら使えるかなど、課題設定から始めなくてはいけないと改めて気づかされました。
また評価指標やアノテーションの仕方も考える必要があって、やりがいがありました。 そういった意味ではアイデア力も必要ですし、アイデアを産むために数理的な知識も、ビジネスの知識も必要だなぁと痛感しました。

こう言ったことを知れたのはメンターの縣さんが何を持ち帰ってもらおうか考えてインターン生をサポートしてくださったからこそだと思います。
本当にありがとうございました!

できるようになったこと・学んだこと

  • GItHubを綺麗に使えるようになった
    レポジトリを見てWantedly社は相当GItHubを使いこなしているなという印象を受けました。 他のレポジトリなどを見ながら、コードレビューしてもらいやすいプルリクの作り方や、後世に遺産が伝わりやすいIssueの書き方などを知れました。

  • 便利なオープンソースコードを知れた インターンでFastTextやBERTなどの事前学習モデルをクローンして使うことがあったのですが、あんなにお手軽に再学習や、分散表現を手に入れらるとは思いませんでした。 特にFastTextはgensimのWord2Vecクラスは便利なメソッドが多くて、今後も使おうと思います。
    またBigQueryとかPlotlyとかも書けるようになりました。

  • 機械学習エンジニアのリアルを知れた
    上記に書いたとおりです。

Wantedlyインターンの印象

たしかに就業型インターンでした

取り組んだ課題がインターン生のために用意された課題ではなく、インターン生を一人の社員として見てくださっている感じがありました。 給料をいただいているし、メンターの方の時間も割いていただきながら仕事をするので、進捗産まなきゃ。。。みたいな緊張感はありましたが、詰まったらいつでも聞いてねという感じだったのでありがたかったです。

オフィスも街も綺麗

オフィスが白金台だしピカピカだしテンション上がりました。   f:id:zerebom:20190908180530j:plain

あと会議室の名前が全部ジョジョのスタンドの名前でウケました。 「ザ・ワールドで話そうか」みたいな。出てこれなくなっちゃいそう。

業務後は卓球もできました。
f:id:zerebom:20190909084544j:plain

同じタームの子と仲良くなった

席が近い子とは毎日昼ごはんに行ったり、シャッフルランチが毎週あったりで仲良くなれました。
ただインターン生全員と席が近いわけではないので、全員と話すには割と能動的に行動しないといけないかもです。

f:id:zerebom:20190909083540j:plain
打ち上げのボドゲ大会

インターンの選考対策にしたこと

自分も去年はこういうブログを見て、インターン行きてえ。。。!ってなってたのですが、
「おすすめなので是非行ってみてください!」
的な文章を見るたびに、いや受かる前提やんけ!みたいな気持ちになってました。

誰得だよって感じですが、自分なりにES/面接で気をつけたことを書きたいと思います。

  • 成果ベースで話す
    〇〇を勉強中です!みたいなことを推すのは弱いと思ったので、なるべく成果ベースで話しました。 アウトプットが社外に公開されるような会社で長期インターンをしつつ、大きな仕事があれば積極的に関われるようにコミニュケーションを取るととっかかりやすいかもです。 あとはKaggleとかもメダルは取れたわけではないのですが、ちゃんとSubmitして順位が出ているので伝えました。

  • 自分の関心領域と業務内容が近い(近そう)ということを語る
    当たり前ですが、業務内容のミスマッチは企業側も避けたいと思っているので、
    自分の経験や興味が企業に取り組む内容に近いことを推していくのは大事だと思います。
    株主総会向けのプレゼン資料とかに、企業のビジョンや今後注力する領域がわかりやすくまとめてあるのでオススメです。

  • やる気を見せる
    記述式の問題や、ESの文字数制限ないところは人よりたくさん書いたと思います。

  • 最強の技術力があれば関係ない
    それはそう

  • 応募しないと絶対受からないので、いっぱい出す
    それはそう

終わり

こういったインターン行くと成長の糧になることをいっぱい学べるし、
仲間やメンターさんとのつながりもできるし、本当に参加できてよかったと思ってます!

今回学んだことを研究や将来の職業でも活かせるように今後も頑張ります!
3週間ありがとうございましたー!

GitHubに画像解析用のKerasディレクトリを公開しました。

お久しぶりです、ひぐです!

大学院に入ってからニューラルネットワークを使って、医療画像の補完を行う研究をするようになりました。

そこで今日は自分が普段使ってる研究用のコードを紹介したいと思います!
結構KaggleやQiitaとかでNNライブラリ用のソースコードを検索すると、使い切りなコードが多くないですか?

今回は繰り返し実験できるようにディレクトリごとコードを公開しました!
モデルの保存や、出力結果の記録をクラス単位で実装しています。よかったら参考にしてください。

なかなか上手に書けない部分もあるので、ご教授いただければ幸いです。。。笑
というかまだまだ絶賛修正中なので温かく見守ってくださいw

URLはこちら github.com

コードの概要

全体的にはこんなイメージです。 f:id:zerebom:20190518110310j:plain

主な機能は以下のようになっています。
* 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グラフなどをまとめて格納します。

f:id:zerebom:20190518112328p:plain

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(探索的データ解析)の手法

お久しぶりです、ひぐです!

f:id:zerebom:20190412103328p:plain  

先日、Kaggleの Santander Customer Transaction Predictionコンペティション(以下、Santanderコンペ)
に参加したのですが、凄惨な結果に終わってしまいました。。。

  そこで今回の記事では自分の復習もかねて、上位の方の解法をまとめつつ
データ分析に必要なEDAの手法を紹介していきたいと思います!  

この記事を読むのに必要な前提知識

  • Kaggleとは?
    インターネット上で開催されているデータ分析コンペティション
    企業が公開したデータセットを前処理、モデル構築を行い、
    予測データを作成し、その精度で競います。

くわしくは↓
www.codexa.net

  • EDAとは?
    Explanatory Data Analysis の略。日本語で言うと、探索的データ解析。
    データの特徴や構造を理解するためにグラフを作成し、
    特徴量の相関やターゲットとの関係性を調べることです。

くわしくは↓

www.codexa.net

Santanderコンペってどんなコンペ?

f:id:zerebom:20190412105118p:plain

Santanderというアメリカの銀行が開いたコンペ。

レーニング・テストデータどちらも
20万行、かつ200列の特徴量から構成されている。
特定の取引を行った行にtarget==1が付与され、それ以外にtarget==0が付与されている。

全ての特徴量がfloat型かつvar_iと命名されていて、どんな特徴量か分からない様に匿名化されている。
f:id:zerebom:20190412105125p:plain

どんなことに気づけばスコアが伸びたのか

このコンペでは大きく分けて、

  • 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)

結果↓
f:id:zerebom:20190412193310p:plain
欠損値は全くなく、すべての特徴量がfloat64です。

4.列ごとの統計量の確認

train.describe()

結果
f:id:zerebom:20190412193812p:plain

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)

結果↓
f:id:zerebom:20190412194118p:plain

こちらで使っている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()

結果↓
f:id:zerebom:20190412200923p:plain

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))

結果↓
f:id:zerebom:20190412202108p:plain

これが非常に大切だったのです...(のちに紹介)

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)

結果↓
f:id:zerebom:20190412202851p:plain

非常に相関係数が小さいことが分かります。
このことから、それぞれの特徴量は独立に扱ってよいという考えに至れると、
新たな学習方法を行うことが出来ます。

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='.')

結果↓
f:id:zerebom:20190412203426p:plain

この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)

とすることで、特徴量毎に大きく表示し、確認することもできます。
f:id:zerebom:20190412204322p:plain

応用編(Santanderで役に立つEDA)

それぞれ長くなりそうなので、別の記事にしました。

  1. 各特徴量を独立と仮定した修正ナイーブベイズ法によるtarget確率分布推定

初めて、本気で参加したコンペについての感想

Santanderコンペについて

欠損値もなく、データも大きくなく、すべて数値型だったので取り組みやすい!
と思って始めたのですが、実際は人と差をつけることが難しく、スコアも伸びず大変でした…

LBスコアが非常に密集していて、
kernelで公開されている、0.901スコア代の人が3000人くらいいたのが特徴的でした。

自分の足りなかった部分

  • 着手し始めるのが遅すぎた。
  • NNライブラリを扱えず、アンサンブルができなかった
  • モデルの理解度が低く、LGBMでは効かない特徴量を作ってしまった。
  • 英語力が足りず、discussionから必要な情報を収集しきれなかった。
  • discussionに書いてあることをデータにうまく落とし込めなかった。
    EX)各列が無相関→各列を縦に連結し、(4000万行,2列)のデータセットにするなど

参加して得られたこと

  • Kaggleにしっかり参加するときの流れがつかめた。

u++さんが公開してくださった、ディレクトリを用いて
デバッグ、特徴量の追加削除を自由にできる環境でのKaggleを行うことが出来るようになった。

upura.hatenablog.com

  • のちのコンペに生かせそうな関数を知れた
    EDA等、dfを引数にした汎用的な関数をいくつか知ることが出来たので、今後のコンペに対してスムーズに動生きだせそう。

  • やるき
    何の成果も得られず悔しかったので、次はやってやるぞという気持ちになれた。

   

google-site-verification: google1c6f931fc8723fac.html