小ネタ:pathlib.Path と Kaggle Notebooks のディレクトリ構成
誰得小ネタ第三弾です。今回は pathlib.Path
の話になります。
実は知ったきっかけは ASHRAE*1 で一位を獲られた方*2 が公開した notebook で使われているのを見たからでした。 python 歴が長いくせに知ったのはずいぶん最近という。もっとはやく知りたかった... 😇
この記事では説明の都合で kaggle 上で Titanic コンペ*3 のデータを用いて例を挙げています。Public にしといたのでご参考まで。
僕自身 pathlib
の機能をちゃんと把握していなかったのですが、python 側からディレクトリを削除するといったファイル操作系がまとまっていて実はめっちゃ便利みたいです*4*5、なんだこの神がかった標準ライブラリは... 記事を書くついでに調べる内に色々知って衝撃を受けました(os.mkdir()
とかを未だに使っていた顔)。
ここではあくまでディレクトリ/ファイルパスの操作について触れます。
目次
- Kaggle / Titanic コンペで実行する notebook とコンペデータの位置関係
- 相対 / 絶対 path の取得
- / による path の結合
- ファイルの読み込み / 書き込み
- おわりに /
- おまけ / Kaggle Notebooks に合わせてローカルのディレクトリ構造を作る
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
を打つことでも確認が可能です。
相対パスは自分のいる場所を中心に見るので、 .
を指定して現在いる場所の 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/output
と kaggle/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_csv
は pathlib.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'
尚、どのライブラリが対応しててどのライブラリが対応していいみたいな話は結構需要があるらしいです。誰か書いてくれないかな~(チラッチラッ
標準ライブラリの全てに対応してるかは自分も知らなかったです…
— ふぁむたろう (@fam_taro) May 5, 2020
とりあえず今でも open() には str にして突っ込んでました…
あー、pillow は OK だけど cv2 は str でしたね…あと pydicom も str なんですよねー
めっちゃまとめて欲しい(
話を戻しまして、次は書き込みです。とはいっても 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
のみです。
どうやら 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を例として使います。このコンペは
- 画像コンペなので学習済みモデル(e.g. on ImageNet) はとりあえず使いたい
- 元の画像ファイルが
.parquet
ファイルなので前処理の必要あり - 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 を起動する場所
ポイントとしては
input
の下にbengaliai-cv19
というディレクトリを作ってその中にファイルを入れる- 学習済みモデルの入ったディレクトリ(ここでは
chainercv-seresnext
) をinput
下に置く - root(
bengaliai-cv19
) の下にprocessed_data
を作り、その中に前処理済みファイルを入れる 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_data
を input
の下に置いても良さそうです。
4 はまあ好みかもしれないですが、src
と working
の 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 全体にアクセスできる仕様だったため、尚更このやり方が必要でした。