天色グラフィティ

機械学習やプログラミングでいろいろ作って遊ぶブログ

コピペで使える。Kaggleでの実験を効率化する小技まとめ

f:id:ejinote:20181220184224j:plain

この記事はKaggle Advent Calendar 201820日目の記事です。当初の予定ではPLAsTiCCコンペの振り返りをするはずだったのですが、時空の狭間に吸い込まれた結果0サブミットでフィニッシュしてしまいました。何ででしょうね。 そこで、代わりにKaggleで使える便利なスニペットまとめを書くことにします。

ちなみにもうひとネタあったのでいつか書きたいですが、修論があるのでいったん見送り……

LINEに通知を送る

f:id:ejinote:20181220181419p:plain
Home Creditコンペで実際に送っていたログ

「思考は止めてもサーバーは止めるな」とはkaggler-jaの管理者であるtkmさんの名言です。サーバーを止めないためには正常・異常問わず計算終了時に通知を送ることが重要です。

import requests

def send_line_notification(message):
    line_token = 'YOUR_LINE_TOKEN'  # 終わったら無効化する
    endpoint = 'https://notify-api.line.me/api/notify'
    message = "\n{}".format(message)
    payload = {'message': message}
    headers = {'Authorization': 'Bearer {}'.format(line_token)}
    requests.post(endpoint, data=payload, headers=headers)
  1. https://notify-bot.line.me/my/ からパーソナルアクセストークンを発行する
  2. 発行したアクセストークンをYOUR_ACCESS_TOKENに入れる

これでLINE通知を送ることができます。アクセストークンはコンペが終わったら無効化すれば、GitHubにコードをそのままpushしても大丈夫です。

副作用として、計算が終了したことがわかってしまうというものがあります。

処理にかかる時間を計測する

どこまで処理が進んでいるか分からないのは気持ちの良いものではありません。 また、Kernelコンペの場合はそれぞれの部分でどの程度時間がかかっているかを把握し、制限時間内に収める必要があります。

Jupyter Notebookの場合

Jupyter Notebookの場合、計測したいセルの先頭に

%%time

というマジックコマンドを書くだけで時間を計測してくれます。

例として、0から9999までの和を10000回計算するのにかかる時間を測定してみます。

f:id:ejinote:20181220181540p:plain

Pythonスクリプトの場合

import logging
import time
from contextlib import contextmanager

@contextmanager
def timer(name, logger=None, level=logging.DEBUG):
    print_ = print if logger is None else lambda msg: logger.log(level, msg)
    t0 = time.time()
    print_(f'[{name}] start')
    yield
    print_(f'[{name}] done in {time.time() - t0:.0f} s')

使い方と出力は以下のとおりです。with文を利用し、その中の処理にかかった時間を出力します。

with timer('preprocessing'):
    # preprocessing
[preprocessing] start
(途中での出力)
[preprocessing] done in 17 s

小技としては、ロガーを受け取れるようになっています。 条件によって関数を差し替える方法を知っていると、コーディングの幅が広がると思います。

LightGBMの学習結果をログに出す

import logging
from lightgbm.callback import _format_eval_result

def log_evaluation(logger, period=1, show_stdv=True, level=logging.DEBUG):
    def _callback(env):
        if period > 0 and env.evaluation_result_list and (env.iteration + 1) % period == 0:
            result = '\t'.join([_format_eval_result(x, show_stdv) for x in env.evaluation_result_list])
            logger.log(level, '[{}]\t{}'.format(env.iteration+1, result))
    _callback.order = 10
    return _callback

log_evaluation関数にロガーを渡してあげることで、そのロガーを利用してログを吐くことができます。

clf = lgb.LGBMClassifier()
callbacks = [log_evaluation(logger, period=10)]
clf.fit(X_train, y_train, eval_set=[(X_val, y_val)], callbacks=callbacks)

これはこちらの記事でも紹介していますので、詳しくはそちらを確認してください。

amalog.hateblo.jp

Google Spreadsheetに結果を記録する

f:id:ejinote:20181220181630p:plain

Google App Scriptを利用して、実験の結果をGoogle Spreadsheetに自動で記入します。

複数人で同時に実験をする際、パラメータや結果を簡単に共有することができます。

Google Spreadsheet側の設定

まず、結果を記録したいスプレッドシートを用意します。

ツール > スクリプトエディタを開きます。

f:id:ejinote:20181220181652p:plain

以下のコードを貼り付けます。これはjavascriptで書かれており、POSTメソッドで送られてきたデータをパースし、タイムスタンプを先頭につけてスプレッドシートに書き込みます。

function doPost(e) {
  if (e==null || e.postData == null || e.postData.contents == null) {
    return;
  }
  var ss = SpreadsheetApp.getActive();
  var sheet = ss.getActiveSheet();
  var data = JSON.parse(e.postData.contents);
  var timestamp = new Date();
  data.unshift(timestamp);
  sheet.appendRow(data);
}

f:id:ejinote:20181220181717p:plain

次に、公開 > ウェブアプリケーションとして導入を選びます。

設定画面が開くので、以下のように設定してください。

f:id:ejinote:20181220181742p:plain

導入を選択すると、アプリケーションを承認する画面が出るので、承認してください。

するとウェブアプリケーションとして公開され、URLが表示されます。 これをコピーしておきましょう。

f:id:ejinote:20181220181755p:plain

これでGoogle Spreadsheet側の設定は終了です。

Pythonからアクセスする

Google Spreadsheetに登録したスクリプトは、json形式でリストを送れば、その各要素をセルに書き込むというものになっています。

Pythonから扱うには、以下のような関数を用意すると便利です。 YOUR_ENDPOINT_URLには、先程コピーしたURLを入れてください。

import json
import requests

def write_spreadsheet(*args):
    endpoint = 'YOUR_ENDPOINT_URL'
    requests.post(endpoint, json.dumps(args))

これを使えば、書き込みたいものを関数に渡してあげるだけで良いです。 引数として渡した各要素をそれぞれ別のセルに入れてくれます。

クロスバリデーションの結果などはリストやnumpyのarrayに入っていることが多いですが、その場合は先頭に*をつけることでばらばらにして引数に入れてくれます。

# scores = np.array([0.8, 0.7, 0.75, 0.8]) とする
# write_spreadsheet('baseline', 0.8, 0.7, 0.75, 0.8) は以下のように書ける
write_spreadsheet('baseline', *scores)

実行結果は以下のとおりです。先頭にタイムスタンプが付き、CVの結果がそれぞれ別のセルに入っていることが分かるかと思います。

f:id:ejinote:20181220181827p:plain

Notebook上でライブラリを毎回再読込する

Kaggle用に自作ライブラリを使っている場合、コンペをやりながら自作ライブラリも書き換えていくことが多いと思います。

Jupyter Notebookでライブラリを読み込む際、普通は1回importした時点で中身は固定されてしまいます。 そこで、ライブラリを書き換えた時点でリロードする必要が出てきます。

% reload_ext autoreload
% autoreload 2

これをノートブックの先頭に書いておくと、関数を実行するたびにリロードが走るようになります。

より詳しくはこちらのQiitaをご覧ください。

DataFrameのメモリを節約する

pandasのDataFrameはintならnp.int64に、floatならnp.float64がデフォルトで使われます。 しかし、ある程度データセットが大きくなってくると、DataFrameがメモリを圧迫して学習を思うように進めることができなかったりします。

解決方法のひとつはpd.read_csv()などをする際にdtypesを指定して読み込むことです。 この方法は最初のデータセットは適切な精度で読み込むことができる反面、途中で作成した特徴量などには一切タッチできません。

そこで、各列の値の範囲を参照し、適切な型に変換します。

def reduce_mem_usage(df, logger=None, level=logging.DEBUG):
    print_ = print if logger is None else lambda msg: logger.log(level, msg)
    start_mem = df.memory_usage().sum() / 1024**2
    print_('Memory usage of dataframe is {:.2f} MB'.format(start_mem))

    for col in df.columns:
        col_type = df[col].dtype
        if col_type != 'object' and col_type != 'datetime64[ns]':
            c_min = df[col].min()
            c_max = df[col].max()
            if str(col_type)[:3] == 'int':
                if c_min > np.iinfo(np.int8).min and c_max < np.iinfo(np.int8).max:
                    df[col] = df[col].astype(np.int8)
                elif c_min > np.iinfo(np.int16).min and c_max < np.iinfo(np.int16).max:
                    df[col] = df[col].astype(np.int16)
                elif c_min > np.iinfo(np.int32).min and c_max < np.iinfo(np.int32).max:
                    df[col] = df[col].astype(np.int32)
                elif c_min > np.iinfo(np.int64).min and c_max < np.iinfo(np.int64).max:
                    df[col] = df[col].astype(np.int64)
            else:
                if c_min > np.finfo(np.float16).min and c_max < np.finfo(np.float16).max:
                    df[col] = df[col].astype(np.float32)  # feather-format cannot accept float16
                elif c_min > np.finfo(np.float32).min and c_max < np.finfo(np.float32).max:
                    df[col] = df[col].astype(np.float32)
                else:
                    df[col] = df[col].astype(np.float64)

    end_mem = df.memory_usage().sum() / 1024**2
    print_('Memory usage after optimization is: {:.2f} MB'.format(end_mem))
    print_('Decreased by {:.1f}%'.format(100 * (start_mem - end_mem) / start_mem))
    return df

オリジナルはKaggleで見たのですが、3つカスタマイズを加えてあります。

  1. ロガーを受け入れるようにした
  2. objectdatetimeは変換させない
  3. np.float16はfeather形式で書き出せないため、np.float16を使わない

追記:オリジナルはこちらのKernelだそうです。教えてくださったupuraさんありがとうございます!

まとめ

以上、Kaggle Advent Calendar 2018 20日目の記事でした。

昨日の記事は、カレーちゃんさんのkaggleコンペのディスカッションの情報をメールで通知する方法【2018年12月版】 - kaggle全力でやりますでした。

明日はnamakemonoさんのBERTでKaggleの過去問Quora Question Pairsを解いてみるです。(公開されました!)