俵言

しがない社会人が書く、勉強とかのこと。最近は機械学習や kaggle 関連がメイン。

小ネタ:pathlib.Path と Kaggle Notebooks のディレクトリ構成

誰得小ネタ第三弾です。今回は pathlib.Path の話になります。

実は知ったきっかけは ASHRAE*1 で一位を獲られた方*2 が公開した notebook で使われているのを見たからでした。 python 歴が長いくせに知ったのはずいぶん最近という。もっとはやく知りたかった... 😇

この記事では説明の都合で kaggle 上で Titanic コンペ*3 のデータを用いて例を挙げています。Public にしといたのでご参考まで。

www.kaggle.com

僕自身 pathlib の機能をちゃんと把握していなかったのですが、python 側からディレクトリを削除するといったファイル操作系がまとまっていて実はめっちゃ便利みたいです*4*5、なんだこの神がかった標準ライブラリは... 記事を書くついでに調べる内に色々知って衝撃を受けました(os.mkdir() とかを未だに使っていた顔)。

ここではあくまでディレクトリ/ファイルパスの操作について触れます。

目次

Kaggle / Titanic コンペで実行する notebook とコンペデータの位置関係

この記事を読むにあたってどういうディレクトリ構成になっているかを把握してもらう必要があるので先に示します。

Kaggle Notebooks は docker 上で動いていますが、user が意識するのは root 直下の kaggle というディレクトリになると思います。そして、 kaggle 下のディレクトリ構成は以下のようになっているはずです。

/kaggle
├── input
│   └── titanic
│       ├── gender_submission.csv
│       ├── test.csv
│       └── train.csv
├── lib
│   └── kaggle
│       └── gcp.py
└── working
    └── __notebook_source__.ipynb   <= 動かしている notebook 

因みに save すると以下のように src ディレクトリが増えるみたいです。知らなかった...

/kaggle
├── input
│   └── titanic
│       ├── gender_submission.csv
│       ├── test.csv
│       └── train.csv
├── lib
│   └── kaggle
│       └── gcp.py
├── src
│   └── script.ipynb
└── working
    └── __notebook__.ipynb

以降の話はこの構成の下で行われます。

相対 / 絶対 path の取得

上でちょろっと示していますが、 notebook は kaggle/working 下で動いています。notebook 上で pwd を打つことでも確認が可能です。

f:id:Tawara:20200506031733p:plain
notebook 上で pwd を実行

相対パスは自分のいる場所を中心に見るので、 . を指定して現在いる場所の Path オブジェクトを作成します。

>>> from pathlib import Path
>>> cur_path_rel = Path(".")
>>> cur_path_rel
PosixPath('.')

これだけだと何も情報が無いですが、.resolve メソッドによって絶対パスを取得できます。

>>> cur_path_abs = cur_path_rel.resolve()
>>> cur_path_abs
PosixPath('/kaggle/working')

先程 pwd で表示したものと同じであることが確認できますね。
また、このpathlib.PosixPath には .absolute という如何にもな名前のメソッドがあり、機能も一見同じように見えます。

>>> cur_path_abs = cur_path_rel.absolute()
>>> cur_path_abs
PosixPath('/kaggle/working')

ところが、実は冗長な path をそのままにするという性質を持っています。例えば working と同じ深さにある input相対パスを取得し、 .absolute を使用してみるとこんな感じ。

>>> Path("../working/input").absolute()
PosixPath('/kaggle/working/../working/input')

これでもファイル読み込みとかはできますけど非常に読みにくい。一方で、 .resolve を使うと綺麗にしてくれます😎

>>> Path("../working/input").resolve()
PosixPath('/kaggle/working/input')

.absolute が存在する理由はよくわかっていないのですが、「敢えて経路を残したい」場合があるのかもしれません。

また、自分のいるディレクトリの一つ上のディレクトリ(ここでは kaggle) には .parent を使うことでアクセスでき、

>>> kaggle_path = cur_path_abs.parent
>>> kaggle_path
PosixPath('/kaggle')

さらにその先を辿る場合には .parents が使えます。

>>> cur_path_abs.parents
<PosixPath.parents>

list() をかませると中身が確認できます。

>>> list(cur_path_abs.parents)
[PosixPath('/kaggle'), PosixPath('/')]

0番目が直接の親、1番目はその一つ上...という順番になっており、index を食わせることでアクセスが可能。この環境だと root (/) 直下に kaggle があるのでイマイチわかりにくいですが、階層が深ければもっと沢山出てくるのでより機能が分かりやすいと思います。

>>> cur_path_abs.parents[0]
PosixPath('/kaggle') 
>>> cur_path_abs.parents[1]
PosixPath('/')

/ による path の結合

pathlib.Path を 使いたいと思った理由は何と言ってもこれです*6/ を使った直感的結合が可能となります。

先程作った kaggle_path に結合していきましょう。

kaggle_path
>>> PosixPath('/kaggle')

直下への path は/` 一個で繋げればOK。

>>> kaggle_path / "working"
PosixPath('/kaggle/working')

/ を複数繋げて path を作ることも可能です。

kaggle_path / "input" / "titanic" / "train.csv"
PosixPath('/kaggle/input/titanic/train.csv')

「ここで扱っているのは pathlib.PosixPath だ」とちゃんと認識していないと割り算に誤解されるかもしれないのがちょっと良くなさそうな部分ですが、めちゃくちゃスッキリかけるので最高です😎 もう .format+os.path でやらなくてもええんや...

ファイルの読み込み / 書き込み

さて、ここまで pathlib.Path の簡単な操作を書いたのですが、これを使うシーンはもちろんファイルの読み込み・書き込みです。 具体例を出す前に、僕が kaggle notebook 上でやる path のおまじないを書きます。

>>> COMPETITION_NAME = "titanic"
>>> ROOT = Path(".").resolve().parent
>>> INPUT_ROOT = ROOT / "input"
>>> RAW_DATA = INPUT_ROOT / COMPETITION_NAME
>>> WORK_DIR = ROOT / "working"
>>> # OUTPUT_ROOT = ROOT / "output"
>>> OUTPUT_ROOT = WORK_DIR / "output"
>>> PROC_DATA = ROOT / "processed_data"

各種ディレクトリに楽にアクセスできるように、グローバル変数的に扱う Path オブジェクトを用意しました。この時点では kaggle/working/outputkaggle/processed_data は存在しないので、.mkdir で生成します*7

>>> OUTPUT_ROOT.mkdir()
>>> PROC_DATA.mkdir()

そうすると、ディレクトリ構成は以下のようになります。

/kaggle
├── input
│   └── titanic
│       ├── gender_submission.csv
│       ├── test.csv
│       └── train.csv
├── lib
│   └── kaggle
│       └── gcp.py
├── processed_data
└── working
    ├── __notebook_source__.ipynb
    └── output

おまじないを唱え終わったので、コンペデータ(ここでは csv ファイル)を読んでみましょう。
みんな大好き pandas.read_csvpathlib.PosixPath を渡すと読んでくれます。便利!

>>> train = pd.read_csv(RAW_DATA / "train.csv")
>>> test = pd.read_csv(RAW_DATA / "test.csv")
>>> sample_sub = pd.read_csv(RAW_DATA / "gender_submission.csv")

ただ、(特に third party library では) pathlib.PosixPath を渡しても対応していなくて error が発生する場合もあるようです。
そんなときには、以下のように str に変換して上げれば OK.

>>> str(RAW_DATA / "train.csv")
'/kaggle/input/titanic/train.csv'
>>> (RAW_DATA / "train.csv").as_posix()
'/kaggle/input/titanic/train.csv'

尚、どのライブラリが対応しててどのライブラリが対応していいみたいな話は結構需要があるらしいです。誰か書いてくれないかな~(チラッチラッ


話を戻しまして、次は書き込みです。とはいっても pathlib.PosixPath に対応しているライブラリであれば渡すだけでいいのであまり語ることはありません。

>>> train.to_pickle(PROC_DATA / "proc_train.pkl")
>>> sample_sub.to_csv(OUTPUT_ROOT / "submission.csv", index=False)

以下のようにちゃんと出力されています。

/kaggle
├── input
│   └── titanic
│       ├── gender_submission.csv
│       ├── test.csv
│       └── train.csv
├── lib
│   └── kaggle
│       └── gcp.py
├── processed_data
│   └── proc_train.pkl  <= 出力された
└── working
    ├── __notebook_source__.ipynb
    └── output
        └── submission.csv  <= 出力された


ここで、 Kaggle Notebooks ならではの注意点があります。実は、この二つのファイルのうち commit 後に出力としてOutput Files に出現するのは submission.csv のみです。

f:id:Tawara:20200506141036p:plain
commit 後の Output Files

どうやら kaggle/working 下のファイルのみを保存する仕様になっている模様。これを意識してないと「ある notebook で特徴量をまとめて作成して保存 => 別の notebook で読み込んでモデリング」みたいなことをしたいときに事故るので気を付けましょう。

一方で、一時的に保存する必要があるけど Output Files が散らかるから最終的に保存したくはないファイルとかを kaggle/working の外に保存する、という利用の仕方が出来ると思います。

もう一つ注意点と言うか tips として、kaggle/input/ 下は読み込み専用となっているのでファイルの書き込みが出来ません。これはこのあとのおまけに少し関わる話です。

>>> train.to_pickle(RAW_DATA / "train.pkl")
---------------------------------------------------------------------------
OSError                                   Traceback (most recent call last)

(中略)

OSError: [Errno 30] Read-only file system: '/kaggle/input/titanic/train.pkl'

おわりに /

小ネタ記事はさっくり書く方針だったのですが何か長くなってしまいました。次から気を付けます。

この記事では pathlib.Path を単純にファイルパスとしてしか扱ってなかったですが、ちょっとだけ触れたようにファイルやディレクトリを生成したり、削除したり、存在を確認したり、読み込んだりとかなり便利に使えます。

一度も使ってみたこと無いって方は触ってみると何かしら楽になる可能性を秘めているので是非どうぞ。

おまけ / Kaggle Notebooks に合わせてローカルのディレクトリ構造を作る

余談なのですが、僕自身のローカル環境はコンペごとのディレクトリを上の Notebooks の構成に合わせています*8

コンペ用の大元のディレクトリの下にコンペごとのディレクトリを作り、それぞれについて Kaggle Notebooks に合わせた形でディレクトリ構成をするという方針です。

COMPETITION_ROOT
├── atma-cup-03
├── bengaliai-cv19
├── data-science-bowl-2019
├── kaggle-days-tokyo
├── m5-forecasting-uncertainty
├── trends-assessment-prediction
├── youtube-view-count
...

色々と具体例が挙げやすいので、ここでは僕が以前参加していたベンガル語コンペ*9を例として使います。このコンペは

  1. 画像コンペなので学習済みモデル(e.g. on ImageNet) はとりあえず使いたい
  2. 元の画像ファイルが .parquet ファイルなので前処理の必要あり
  3. code competition だったため 提出は Kaggle Notebooks で行う
    • 更に言うと、全 test data にアクセスできるのは 提出の瞬間のみ

といった特徴があり、特に 3. があるからこそ構成を合わせることに意味がありました。

実際のディレクトリ構成は以下です。

bengaliai-cv19
├── input
│   ├── bengaliai-cv19  <= コンペで与えられるデータを格納
│   │   ├── class_map.csv
│   │   ├── class_map_corrected.csv
│   │   ├── sample_submission.csv
│   │   ├── test.csv
│   │   ├── test_image_data_0.parquet
│   │   ├── test_image_data_1.parquet
│   │   ├── test_image_data_2.parquet
│   │   ├── test_image_data_3.parquet
│   │   ├── train.csv
│   │   ├── train_image_data_0.parquet
│   │   ├── train_image_data_1.parquet
│   │   ├── train_image_data_2.parquet
│   │   ├── train_image_data_3.parquet
│   │   └── train_multi_diacritics.csv
│   └── chainercv-seresnext   <= 学習済みモデルを格納
│
│
├── output
├── processed_data
│   ├── test  <= .png に変換した test 画像を格納
│   └── train  <= .png に変換した train 画像を格納
├── src  <= 学習などのためのスクリプトファイルを格納
└── working  <= notebook を起動する場所

ポイントとしては

  1. input の下に bengaliai-cv19 というディレクトリを作ってその中にファイルを入れる
  2. 学習済みモデルの入ったディレクトリ(ここではchainercv-seresnext) を input 下に置く
  3. root(bengaliai-cv19) の下に processed_data を作り、その中に前処理済みファイルを入れる
  4. src は root(bengaliai-cv19) の下に置き、実行は src 直下に移動して行う

1 は純粋にパスの名前を合わせるためです。名前が被るからちょっと冗長に見えますが仕方ない😅

2 がここにあるのは、notebook に pre-trained model を attach した際の場所が input 直下であるため。学習済みモデル等を upload した際も同様の位置になります。インターネットアクセスが許されるコンペなら意識しなくてもいいかもしれないです(その場で有名モデルが load できるため)。

3 は前述した「Kaggle Notebooks 上では input に書き込みが出来ない」という事情もあってこうなっています*10。また、ファイル名をミスって元データを上書きするなんてミスを無くすためにもやった方が良いです。テーブルコンペだったら作成済みの特徴を置いたりもします。
ただ、全 test data に最初からアクセスできるコンペなら事前に前処理を行って dataset として attach 出来る(つまり notebook上では input 下に入る) ので、この場合は ローカルでも processed_datainput の下に置いても良さそうです。

4 はまあ好みかもしれないですが、srcworking の root (bengaliai-cv19) から見た深さを一致させておくと、Notebook 上で実行 & 提出を行う場合に便利かなーと言うお気持ちでやっています。

実際の所各々のプロジェクトの作り方の好みとかあるので無理にやる必要は無いですが、code competition のときは構造が一致している方が明らかに楽できるのでおススメです😃

*1:https://www.kaggle.com/c/ashrae-energy-prediction

*2:https://www.kaggle.com/yamsam

*3:https://www.kaggle.com/c/titanic

*4:Python, pathlibの使い方(パスをオブジェクトとして操作・処理): https://note.nkmk.me/python-pathlib-usage/

*5:Pathlibチートシート: https://qiita.com/meznat/items/a1cc61edb1e340d0b1a2

*6:なのでこれ以外の盛りだくさんの便利機能を全然把握していなかった

*7:個人的にはオブジェクトのメソッドで生成できることに感動しました

*8:多分多くの人がこれをやっているとは思いますが。

*9:https://www.kaggle.com/c/bengaliai-cv19

*10:ベンガル語コンペは Notebook 上で submit した場合のみ test 全体にアクセスできる仕様だったため、尚更このやり方が必要でした。