この記事は、kaggle Advent Calendar 2018 - Qiitaの24日目の記事です。
この記事では、先日終了したkaggleのPLAsTiCC コンペで私が行った、特徴の管理の仕方を紹介します。
コンペを約2ヶ月ほど戦い、数千の特徴を作り特徴管理が破綻しなかったので、最低限の管理はできていたとは思います。
ただし、これがベストだとは思っていなくて、強い方からマサカリ・アドバイスをもらって、より良い管理方法を模索したいという思いもあり、この記事を書いてみました。アドバイスなどあれば、是非コメントお願いします。
参考にした方法など
コンペでの特徴管理にあたり、次の2つのブログを参考にしました。
基本的にこの記事の方法は、1.のブログで紹介されている方法を前提とし、
- testデータが膨大*1であるという、今回のコンペの特性
- 私のPythonレベルが高くないため、簡単化する必要がある
という事情を考慮し、カスタマイズした内容となっています。
一般的には、1.のブログを読んで真似をしてみるのがいいと思います。
kaggleでの特徴の管理の基本方針
私の特徴の管理の仕方の基本方針は次のとおりです。
- 計算リソースの有効活用のため、同じ計算は(なるべく)2回しない。後から何をやったか分かるようにするため、コードは保存しておく。
- csvファイルの読み込みは遅いため、feather形式、npy形式、pickle形式のうち適当なものを用い、csv形式はサブミットファイルとして書き出す以外には用いない。
- 特徴は最終的にとても多く作ることになり、ひとつの特徴をひとつのファイルにしてしまうと膨大になってしまうため、グループ単位で管理することにする。
簡単に説明すると
1.は心構えのようなもので、そうできたら良いな思っています。
計算時間がかからない小さなデータなら、同じ計算をしてしまったほうが早いこともあるので、特に計算時間がかかる大きなデータの場合に気をつけたいと思っています。
2.はちょっとしたコツのようなもので、書いてあるとおりです。
コンペのデータによって、feather形式が適当な場合と、pickle形式が適当な場合があるので、適宜選択しています。
3.は、コンペを戦うとなると多くの特徴を作ることになり、その特徴をひとつずつ管理していくのは自分はムリなのでグループ単位で管理するというものです。
実際に行った方法
上記の基本方針を踏まえて、どのように特徴を管理したかということですが、以下のようなclassを作ってみました。
import pandas as pd import numpy as np from pathlib import Path import time from contextlib import contextmanager @contextmanager def timer(name, write_log=True, data_type='train'): t0 = time.time() print(f'[{name}] start') yield t1 = time.time() - t0 print(f'[{name}] done in {t1:.1f} s') if write_log is True: with open('features.log', mode='a') as f: f.write(f'{name}_{data_type}\n') f.write('[{}] done in {:.1f} s\n\n'.format(name, t1)) class Dataset(): def __init__(self, file_name=''): self.class_name = self.__class__.__name__ self.features_dir = Path(__file__).resolve().parent self.input_dir = self.features_dir / '../input' self.train_path = self.input_dir / 'training_set.ftr' self.prefix = '' self.suffix = '' self.dtypes = {'mjd': 'float32', 'passband': 'int8', 'flux': 'float32', 'flux_err': 'float32', 'detected': 'int8'} ## その他多く書いたが省略 ## def get_train(self): return pd.read_feather(self.train_path).astype(self.dtypes) def run(self, data_type, write_log, reprocess=False): self.data_type = data_type if data_type is 'train': self.feather_path = self.features_dir / 'features' / f'{self.class_name}_train.ftr' elif data_type is 'test': self.feather_path = self.features_dir / 'features' / f'{self.class_name}_test.ftr' # 既にファイルがある場合は処理をやめる if reprocess is False: # reprocess is True の場合はもう一度作る if self.feather_path.is_file(): print("ファイルがあります。class名を変更しましょう") exit(0) with timer(self.class_name, write_log, data_type): self.create_features() prefix = self.prefix+'_' if self.prefix else '' suffix = '_' + self.suffix if self.suffix else '' self.df.columns = prefix + self.df.columns + suffix self.df.reset_index(inplace=True) print(self.df.head()) self.__write_feats() return self def save(self): self.df.to_feather(str(self.feather_path))
以下、このコードについて説明します。
timer
処理時間を計測し、ログファイルに何秒かかったか書き出す関数です。
処理時間を記録しておくことで、trainデータでこのぐらいかかる処理は、testデータだとこのくらいかかるということがわかるようになります。
今回のコンペの場合で私のPCで処理すると、
trainデータで1秒かかる処理がtestデータで10分ぐらい、
trainデータで10秒かかる処理がtestデータで1時間ぐらい
の処理時間でした。
Dataset()クラス
このコンペで使うデータ周りのことを、処理するためのクラスです。
get_train()
例えばtrainデータの読み込みは、次の2行で、できるようになります。
data = Dataset() # インスタンス化 train = data.get_train() # trainの読み込み
また、pandasの読み込みは、データの型を指定しないと、整数はint64 、小数はfloat64となってしまい*2、データサイズが大きくなってしまうため、astype(self.dtypes)
によりデータの型を指定しています。
この処理は、このデータ型の処理が終わったものをpickleにしてしまったほうが良い場合も多いと思います。
今回のコンペではfeatherでやってましたが、おそらくpickleを毎回読み込んだほうが速かったのではないでしょうか。
run
- 特徴のfeatherファイルのパスを取得し
- 既にfeatherファイルがある場合は、処理をやめる。
- reprocessがTrueの場合には、再度作りなおす
- prefix、surfixが設定されている場合は、列名の頭または末尾に追加
しています。
save
- featherファイルへの書き出し
しています。
特徴の作成
作成する特徴ごとに、.py
ファイルを作成し、
- class名の変更(2箇所、今回の例ではFluxに設定している)
def __make_features(self)
内に、特徴を作成するコード
を書きます。
例えば次のようなコードになります。
import sys class Flux(Dataset): def __init__(self): super().__init__() self.file_name = pathlib.Path(__file__) def create_features(self): if self.data_type is 'train': self.df = self.get_train() self.__make_features() if self.data_type is 'test': self.df = self.get_test() self.__make_features() def __make_features(self): aggs = {'flux': ['mean', 'max', 'min', 'std']} self.df = self.df.groupby('object_id').agg(aggs) self.df.columns = pd.Index(['{}_{}'.format(i[0], i[1]) for i in self.df.columns]) self.prefix = 'prefix' def main(): args = sys.argv if len(args) == 1: data_type = 'train' if args[1] == 'test': dataset_type = 'test' Flux().run(data_type=data_type, write_log=True, reprocess=False).save() if __name__ == '__main__': main()
今回のコードは、object_idでグループ化し、fluxのmean, max, min, stdの特徴を作成するコードとなっています。
class Flux
先ほどの、Datasetクラスを継承したクラスであり、.pyを作成するたびに作成するコードに合わせて修正します。
このクラス名が、featherファイルのファイル名になります。
create_features()
今回のコンペでは、testデータが膨大であったため、trainデータとtestデータで別々に処理することにしています。
コンペ参加中、trainデータの特徴を作成し、CVを確認し、CVが良かった場合にtestデータの特徴を作成する方法にしていました。
testデータが大きくないデータでは、trainデータとtestデータの特徴を同時に作ってしまっても良いと思いますし、私もそうしています。
make_features
特徴を作るコードです。
今回は、flux列のmean, max, min, stdをobject_idごとに集計する例として書いてみました。
prefixに例としてprefixを指定しています。
main
間違ってtestデータを処理しないように、(testデータが膨大なので、誤ってtestデータの処理を始めると、メモリなどを大幅に使用してしまうことになる)
- コマンドライン引数が渡されずに、.pyが実行された場合にはtrainが指定され、trainデータが処理されるようにしています。
- ひとつ目のコマンドライン引数が'test'であった場合には、testが指定され、testデータが処理されるようにしています。
まとめ
PLAsTiCCコンペで、私が行った特徴管理について書いてみました。
まだまだ、コンペについてもpythonについても学んでいる段階なので、コンペごとに特徴管理の良いやり方を模索しています。
また、やり方がアップデートされ、この記事よりも良い形になったら、どのように特徴を管理したか書いてみたいと思います。
コンペ関連での質問などありましたら、以下からお寄せください。
twitterやブログなどから回答します。
https://marshmallow-qa.com/currypurin?utm_medium=url_text&utm_source=promotion
参考
PLAsTiCCコンペの反省会で、コンペに本気で参加した多くの方に会いましたが
- 特徴をjupyter notebookで作成されている方
- kaggleのkernelで作成されている方
- ひとつずつ.pyファイルを作成されている方
など様々でした。
長期間コンペを戦っていくので、自分が混乱しない特徴管理を行っていくのが良いと思います。