加具留矢流余

かぐるやるよ

過去に計算したDataFrameと内容が同じか高速に判定したい

同じ前処理を実行するたびに数十分取られるのが非常に辛く感じる。 特に同じ入力を入れているのに実行するたびに計算をしなおすのが馬鹿らしく感じる。 かといってDataFrameを丸々キャッシュして、関数呼び出すたびに前回のDataFrameと同じか検証するものアホらしい。

もやもやしつつfeatherとhashでうまいことやれば高速に検証できね?って思ったので頑張ってみた。

コンセプトは以下の通り 1. featherを使ってDataFrameをbytesに変換 2. 高速なハッシュアルゴリズムxxhashを使ってbytesのハッシュ値を計算。 3. 初回はハッシュ値とDataFrameを何かしらの形でファイルに保存する。 4. 2回目以降はハッシュ値を計算して、前回のハッシュ値と同じだったら保存されている計算結果を読み込み。

pyarrowを使うとfeatherの出力先にfile-like objectが使えるので、シリアライズしたDataFrameをメモリ上(io.BytesIO)に吐き出せて良い。

以下実装

import io
import pyarrow.feather
import xxhash
import os
import gc
import pandas as pd

def get_hash(df):
    fp = io.BytesIO()
    pyarrow.feather.write_feather(df, fp)
    b = fp.getvalue()
    fp.close()
    h = xxhash.xxh64(b).hexdigest()
    return h

def dfcacher(func, df, name, path="./", verbose=False):
    h = get_hash(df)
    h_path = os.path.join(path, name+".xxhash")
    f_path = os.path.join(path, name+".feather")
    
    # 前回のハッシュ値を読み込み
    try:
        with open(h_path, "r") as fp:
            saved_h = fp.read().rstrip('\n')
    except FileNotFoundError:
        saved_h = ""
        
    if h == saved_h:
        # 前回とハッシュ値が変わらないのであれば計算結果をキャッシュから読み込み
        if verbose: print(f"Hash value of  {name} is not changed. Load dataframe from cache.")
        del(df)
        gc.collect()
        df = pyarrow.feather.read_feather(f_path)
    else:
        if verbose: print(f"Hash value of  {name} is changed.")
        # ハッシュ値が変わっていれば計算して計算結果を保存
        df = func(df)
        with open(h_path, "w") as fp:
            fp.write(h)
        df.to_feather(f_path)
    return df

# テストケース1
df1 = pd.DataFrame([[1, 2, 3], [4, 5, 6]])
df2 = pd.DataFrame([[1, 2, 3], [4, 5, 6]])
assert get_hash(df1) == get_hash(df2), "ハッシュ値が異なります"

# テストケース2
df1 = pd.DataFrame([[1, 2, 3], [4, 5, 6]])
df2 = pd.DataFrame([[1, 2, 4], [4, 5, 6]])
assert get_hash(df1) != get_hash(df2), "ハッシュ値が等しいです"

# テストケース3
def test_func1(df):
    df = df.apply(lambda x: x+2)
    return df

df1 = pd.DataFrame([[1, 2, 3], [4, 5, 6]], columns=["c1", "c2", "c3"])
df2 = pd.DataFrame([[1, 2, 4], [4, 5, 6]], columns=["c1", "c2", "c3"])
df = dfcacher(test_func1, df1, "test_func1", verbose=True)
df = dfcacher(test_func1, df1, "test_func1", verbose=True)
df = dfcacher(test_func1, df2, "test_func1", verbose=True)

問題点としては高速にシリアライズするためにfeatherを使ってるので、DataFrameがfeatherフォーマットの条件を満たしてないとうまく動かない。 あと余りしっかり検証していないこと。まだ実際に使ってないので使ってる途中になにか問題が出てきそうな気がする。 ただ体感としてhash値の計算はかなり高速に動作している。今度なにか機会があったら速度の検証をやろうと思っている。

深層学習+Hough変換で紙の領域検出

kaggleやるよ何て名前をつけておいて1年半くらいkaggleやってませんでした。

その間何やってたかって言うとAI使ったWebサービスを作ろうとして挫折してを繰り返してました。 今のところ何の生産物も無いっていう悲惨な状況なんですけど、しょうもないノウハウ達は手に入れたのでせめてもの手向けとして公開しようかなと思う。

① 紙の領域検出

画像の中に含まれる領域を検出するメジャーな方法としてはCanny特徴量でエッジ検出+Hough変換で直線の検出っていう組み合わせがメジャーだと思います。 ただCanny特徴量は紙以外の物体が入っていたり紙面に色々描かれていたりすると、余分なエッジまでも検出しちゃってその後の工程が失敗しちゃう可能性があったりします。

f:id:theflyingcat28:20191109214911j:plain
Cannyでのエッジ検出の例
なので深層学習とかで都合よく検出できたりしないかなーって探していたら、偉大なDropboxが技術ブログを公開してました。

blogs.dropbox.com

今のChromeには素晴らしい翻訳機能があるので紹介するまでもないとは思いますが、彼らがやってることとしては、

  1. 何らかの深層学習の方法(各ピクセルのエッジらしさを0~1の小数で返す)でエッジを検出
  2. 検出したエッジに対してHough変換で直線を検出
  3. 得られた直線から構成可能な四角形の候補をリストアップ
  4. DNNの出力(0~1の実数)を四角形の辺に沿って平均してスコアとする。
  5. スコアの最も高かった四角形を、紙の領域として提案

というような感じでした。 ただ肝心な「何からの深層学習の方法」ということが示されていなかったので適当にCNNを使ったエッジ検出方法を見ていたら何個か見つかりました。

HED(Hostically-Nested Edge Detection)

画像をCNNで畳み込んで行き、各レイヤーの出力をSide Layerと呼ばれる層で逆畳み込みしてまとめて結合して出力という方法。画像中に含まれるエッジのピクセルの数は非エッジのピクセルに比べて圧倒的に少ないので、エッジピクセルの数に応じて誤差関数を重み付けしたりエッジ検出に特化した工夫がなされている。詳細は参考URLを見てください。この手法のメリットはVGG16を転移学習して使うので学習コストが軽い点だと思います。逆にデメリットはネットワークの構造的に画像の情報を余り深く理解できていなさそうなとこ。ただ紙の認識では雑に紙のエッジを検出してくれれば大丈夫なので、そんなデメリットは気にしなくていい気もします。高みを目指したい人はネットワークの構造をM2DetみたいにPyramid構造とかに変更したら精度あがるんじゃないかと思う(妄想)。

f:id:theflyingcat28:20191014235129p:plain
HEDの論文から引用

論文URL: https://arxiv.org/abs/1504.06375

参考URL: http://blog.livedoor.jp/tmako123-programming/archives/51956636.html

CASENet

HEDは画像の各ピクセルに対してエッジかそうでないかを学習する手法でしたが、CASENetはラベルが付けられた複数種類のエッジを検出するための手法です(Semantic Edge Detection)。HEDは二値分類問題を前提としているので、複数種類の矩形物体を検知したい場合にはこちらを検討することになると思います。自分では試してないので何とも得いませんが、論文を読む感じ基本的なネットワークのコンセプトはHEDに近しいものを感じます(当然細部は全く異っていますが)。 参考URL: https://arxiv.org/abs/1705.09759

HEDは2015年、CASENetは2017年の論文なので最新の論文を漁ればもっと良さげな手法もあるかと思いますが、とりあえず紙を検出するくらいであれば、この辺の手法で行けるのではと思います(CASENetの方は試してないので何とも言えませんが)。

試してみた

HEDの中間層を適当に変えたり出力層のチャネル数変えたり色々改造したもの(細かい条件は忘れた)を使って実際の紙を認識できるか試してみました。 f:id:theflyingcat28:20191109215322p:plain Cannyは紙だけでなくペンのエッジも拾ってるのがわかります。 一方、HED使って予測したものはペンのエッジは検出されず、紙の輪郭だけをしっかり拾っているのが確認できます(ペンの部分を補完することはできていませんが)。今回はシンプルな画像で試しましたが色々写り込んでいてCannyでうまくエッジの矩形の検出ができないときは、深層学習+Hough変換でもいいかもねっていう記事でした。

Docker使って手軽にGCEで機械学習できる環境を整えた

最近、強化学習的なとこに手を出してみたけど手元のMacbook AirじゃメモリやCPU的に厳しいのでクラウドで実行することを検討した。 最初はAmazon EC2借りようかなーと考えていたけれど、Google Compute Engine(GCE) だとプリエンプティブという格安実行モードがあることを知り、GCEで実行することにした。プリエンプティブモードで実行すると、1日しかインスタンスを実行できない、突然インスタンスが停止するなどのデメリットがある代わりに、通常価格より50%以上安くインスタンスを利用できる。

しかし毎回ファイルを転送してsshで繋いで実行するのは正直面倒くさい。そこでDockerを使って効率的にプログラムをGCEで実行する環境を整えた。 具体的には機械学習用イメージのビルド→イメージのアップロード→コンテナの起動・実行をコマンドラインからささっと実行できるようにした。

環境構築にあたって以下の2つのサービスを使用した。

  • Google Compute Engine
  • Google Container Registry(Dockerイメージのアップロード先)

Google Container Registryは使用したネットワークとストレージ分だけ課金されるので、少しでも課金額を抑えたい場合には1レジストリだけ無料で使えるDocker Hubなどを使った方がいいかもしれない。

注意: 今回使用するスクリプトGoogleクラウドサービスを使用するので、当然のことながら課金が必要になります。特にコンテナの実行までシェルスクリプトで実行してしまうので、変な設定で実行すると青天井の請求が来る可能性があります。ここに載っている内容を実行する場合には自己責任で実行してください。

Step.0 前準備

gcloudのインストール

Google Compute Engineをコマンドラインから使うためにglcoudをインストールする・ ここでは導入方法の説明は省くがGCPのgcloudコマンドをインストールする - Qiitaを参考にすればインストールできると思う。

プロジェクトの作成・Google Container RegistryのAPI有効化

Google Cloud Platformにアクセスして新しいプロジェクトを作成する。 左のサイドーからContainer Registryを選択してAPIを有効化する。(Google Container Registry以外のサービスを使う人、既に有効化している人はこの作業をする必要はありません)

スクリプトで使用する用の環境変数の設定

今回の説明ではプロジェクト名や、イメージの名前を環境変数に設定して使用する(スクリプトにベタ打ちしてGithubにアップしちゃうのが怖いので)。 CLOUDML_HOST_REGIONにはasia.gcr.ioeu.gcr.ious.gcr.ioなどから選べる(参考:Pushing and Pulling Images  |  Container Registry  |  Google Cloud)。CLOUDML_PROJECTIDには作成した自分のプロジェクトIDを、CLOUDML_IMAGE_NAMEには適当にイメージの名前を設定する。

export CLOUDML_HOST_REGION="REGION TO UPLOAD"
export CLOUDML_PROJECT_ID="YOUR PROJECT ID"
export CLOUDML_IMAGE_NAME="YOUR IMAGE NAME"

Step1. Dockerfileの準備

普通にDockerfileを作成する。今回はtensorflow、keras、lightgbmのCPU版をインストールしている。また実行するファイルをビルドするときに変えられるように実行ファイル名を変数として持たせている。注意点としてpythonの引数に-uを指定しないと出力がフラッシュされなくて、Stack Driverにログが正しく表示されないのでちゃんと指定する。

Step2. アップロード用のスクリプトの準備

DockerfileからイメージをビルドしてGoogle Container Repositoryにアップロードするスクリプトを作る。

実行したいファイルを引数に渡すとそのファイルを実行するイメージをビルドしてくれる。main.pyというスクリプトを実行するイメージを作成したい場合には以下のように実行する。

./deploy.sh main.py

実行に成功すると最後にレポジトリの名前を出力してくれる。

Step3. 実行用のスクリプトの準備

使用するイメージの名前、使うインスタンスの名前を指定するとGCE上で実行してくれるスクリプトを作る。 細かいインスタンスの設定はgcloud beta compute instances create-with-containerの引数で行う。 今回は4コアCPU、メモリ20GB、ディスクサイズ50GB、プリエンプティブ有効の設定で実行した。 他の設定で実行したい場合には一度手動でインスタンスを作成して課金額を確認したあとに https://cloud.google.com/sdk/gcloud/reference/beta/compute/instances/create-with-containerを参考にして引数を決定するのが良いと思う。

2018年11月4日現在gcloud beta compute instances create-with-containerは実行することができるが、ベータ版のコマンドなので今後のアップデートで実行できなくなる可能性がある。

実行してみる

今回作成したスクリプトを使用してkerasのmnistを実行してみる。今回作成したスクリプト類はGitHub - mikebird28/cloudml-templateに置いてある。環境変数などを設定したあとにdeploy.shを実行すると長々とビルドメッセージが流れたあと最後にレポジトリの名前を教えてくれる。

./deploy.sh main.py

deploy.shの実行が終わったら次のようにしてコンテナを実行する。今回はインスタンス名としてtest-instance1を指定したが、自分で実行するときは好きな名前を使用して問題ない。

./exec_container.sh test-instance1 レポジトリ名

コンテナの実行に成功するとインスタンスIPアドレス、CPU数などの詳細が出力される。ブラウザ上でGCEのインスタンス一覧を確認するとコンテナが実行されていることが確認できる。またインスタンスを選んでログを表示すればプログラムが実行されていることが確認できる。

f:id:theflyingcat28:20181104212909p:plain f:id:theflyingcat28:20181105020524p:plain

今回はCPUしか使用してないがDockerfileとexec_container.shを修正すればGPUも使用できるようになるはず。現状exec_container.pyにバグがあるみたいで、処理が一通り終わるともう一度コンテナがはじめから実行されてしまう。時間があったら今後修正する予定。

参考

機械学習の前処理を効率的にするPreprepっていうライブラリを作った

最近kaggleは放置して機械学習を使った競馬の予想をやってるんですけど、少しパラメータを変えただけのコードを実行するだけなのに、毎回一から前処理をしていると時間がかかりすぎて死ぬという問題が発生していました。

そんな悩みを解決するために、実行するときに前処理をキャッシュしておいて、次に実行するときには以前と変化した部分だけを計算するPreprepというライブラリを作りました。

GitHub - mikebird28/preprep

よく何言ってるかわからないと思うので実際に例を見てもらったほうが早い気がします。

import preprep
import pandas as pd


def func1(df):
    return df*2

def func2(df):
    return df*3

def func3(df):
    return df*4

def func4(df):
    return df*5

def func5(df,n = 1):
    return df*n



df = pd.DataFrame([[1,2,3],[4,5,6]])

#キャッシュするディレクトリを指定してインスタンスを作成
p = preprep.Preprep("./cache_files")

#前処理の関数を登録 func1からfunc3を順番に実行していく
p = p.add(func1,name = "func1")
p = p.add(func2,name = "func2")
p = p.add(func3,name = "func3")

#fit_geneで実際に前処理を実行
df = p.fit_gene(df,verbose = True)
print(df.head())

1回目の出力

[*] start running graph
[*] no cache exists for func1, calculate
[*] no cache exists for func2, calculate
[*] no cache exists for func3, calculate
    0    1    2
0  24   48   72
1  96  120  144

2回目の出力

[*] start running graph
[*] available cache for func1 exists, skip calculation
[*] available cache for func2 exists, skip calculation
[*] available cache for func3 exists, skip calculation
    0    1    2
0  24   48   72
1  96  120  144

1回目に実行したときはキャッシュが存在していないので全ての前処理を計算していますが、2回目に実行するときにはキャッシュが存在しているので計算をスキップしています。キャッシュが存在している状態で、fit_geneに渡すDataFrameが変化すると次のようになります。

#DataFrameの値を変更
#df = pd.DataFrame([[1,2,3],[4,5,6]])
df = pd.DataFrame([[1,2,3],[4,5,7]])

#キャッシュするディレクトリを指定してインスタンスを作成
p = preprep.Preprep("./cache_files")

#前処理の関数を登録
p = p.add(func1,name = "func1")
p = p.add(func2,name = "func2")
p = p.add(func3,name = "func3")

#fit_geneで実際に前処理を実行
df = p.fit_gene(df,verbose = True)
print(df.head())
[*] start running graph
[*] saved cache for func1 exists, but dataset hash value has changed, calculate
[*] saved cache for func2 exists, but dataset hash value has changed, calculate
[*] saved cache for func3 exists, but dataset hash value has changed, calculate
    0    1    2
0  24   48   72
1  96  120  168

ちゃんと入力されたDataFrameが変化していることを検出して計算をやり直しています。またfunc3中身を書き換えたり、他の関数に変えたりすると、func3以前はキャッシュから読み込んで、func3以降だけ計算をやり直してくれます。ちゃんと内部で依存グラフを作ってそれを解決しているので、今回のような関数を順番に実行していくだけでなく、枝分かれしたり、結合したりみたいな複雑なものでもちゃんと必要なとこだけ計算をやり直してくれます。


df = pd.DataFrame([[1,2,3],[4,5,7]])

p = preprep.Preprep("./cache_files")

#func3をfunc4に変更
p = p.add(func1,name = "func1")
p = p.add(func2,name = "func2")
#p = p.add(func3,name = "func3")
p = p.add(func4,name = "func3")

#fit_geneで実際に前処理を実行
df = p.fit_gene(df,verbose = True)
print(df.head())

出力

[*] start running graph
[*] available cache for func1 exists, skip calculation
[*] available cache for func2 exists, skip calculation
[*] saved cache for func3 exists, but op hash value has changed, calculate
     0    1    2
0   30   60   90
1  120  150  210

実際に自分で使っていますが、以前に比べて飛躍的に実験を回すスピードが上がったと思います。これまで前処理に毎回30分くらいかかっていましたが、preprepのおかげで変化したとこだけ実行するので今ではほとんど前処理に時間を取られません(入力するDataFrameが変わったりすると、1から計算しなおさないといけないのでやっぱり時間がかかりますが)。

インストールの方法ですがpipで配布しているので下記のコマンドでインストールできる(はず)です。

pip install preprep

まだ生まれたばっかのライブラリなのでバグが多かったりドキュメントが存在してなかったりするので今後その辺はこつこつ直していこうかなと思います。バグとか見つけたらここかgithubでぜひ教えていください。

TargetEncodingのスムーシング

はじめてのFeature Engineering で紹介したTargetEncoding, LikelihoodEncodingはカテゴリ変数の前処理としては非常に強力な手法だが、あるカテゴリに所属するデータの数が少ないと過学習の原因になってしまう可能性がある。データ数が少ないカテゴリを持つデータセットに対して効果的にTragetEncodingを使うために、この論文ではTargetEncodingのスムージングという手法を提案している。

まず通常のTargetEncoding、LikelihoodEncodingは以下のような式で表現できる。

{
\displaystyle
S_i = \frac{n_{iy}}{n_i}
}

ここで \displaystyle n_iクラスタi に所属しているデータの数、 n_{iy}クラスタiに所属していて目的変数が1の数を表している。 \displaystyle n_i  \displaystyle n_{iy} の数が十分に大きければ過学習を気にする必要は無いかもしれないが、これらの値が小さいと間接的に目的変数の値を予想できるようになってしまうので過学習の要因になってしまう。この問題を解決するために以下のように改良された式が提案されている。

{ \displaystyle
S_i = \lambda (n_i) \frac{n_{iy}}{n_i} + (1 - \lambda (n_i) ) \frac{n_y}{n_{tr}}
}

上で説明した式に加えて \displaystyle \lambda (n_i) \displaystyle n_i \displaystyle {n_{tr}} という変数が追加されている。、 \displaystyle {n_{tr}} はデータの総数、 \displaystyle n_y はデータセット全体の中で目的変数が1の数を表している。また \displaystyle \lambda (n_i) \displaystyle n_iに 対して単調増加で、0〜1の範囲を取る関数を表している。

この式は \displaystyle n_i の数が十分に大きければ第一項の影響が大きくなり普通のTargetEncodingと同じような値を示す。一方で  \displaystyle n_iの数が小さいと第二項の影響が大きくなりデータセット全体の目的変数が1の割合に近づく。このようにデータ数が少ないクラスタに対してはデータセット全体の値を用いることで、機械学習モデルが過学習することを防いでいる。

上の式で出てきた \displaystyle \lambda (n_i)には次のような関数を用いることが多いようだ。

{
\displaystyle
\lambda (n_i) = \frac {1}{1 + e^{- \frac{n_i - k} {f}}}
}

この関数はシグモイド関数的な特徴(k = 0、f = 1とすれば標準シグモイド関数)を示し、k、fの値を調整することでチューニングすることができる。

参考

はじめてのFeature Engineering

先日KaggleのAdTracking Competition に参加したが惨敗したため、特訓することにした。 Ad Tracking Competition で4位の人がFeature Engineeringについて非常にまとまった資料を公開していたので、その要約をまとめた。 勉強がてらPythonでサンプルコードを書いたので一緒においておく。

h2o-meetups/Feature Engineering.pdf at master · h2oai/h2o-meetups · GitHub

何故Feature Engineeringが必要なのか

価値のある特徴量を生成し、ノイズの原因となる特徴量を除去することでシンプルで精度のいいモデルを実現する。

たとえば下の図で、左の状態だと円形にプロットが分布していて、ニューラルネットワークSVMなどの非線形モデルでないと分類することができない。 しかし各点を極座標で表して右のように変換すれば、非線形の手法を使わなくても決定木や線形分類器などのシンプルな手法で識別できるようになる。

f:id:theflyingcat28:20180512214941p:plainf:id:theflyingcat28:20180512214951p:plain
座標変換の効果(上記の資料より引用)

具体的な手法(カテゴリ変数の場合)

多くの機械学習手法(ニューラルネットワークsvm)はカテゴリ変数を直接学習させることが出来ない。これらのモデルにカテゴリ変数を学習させたい場合には、カテゴリ変数を数値に変換する必要がある。よりモデルがカテゴリ変数の意味を理解しやすいように様々な手法が考案されている。カテゴリ変数の特徴(値に大小関係があるか、疎な入力でも問題がないか、特定の値だけが重要なのか)などを考えてどのような前処理を施すか決定することになる。

Label Encoding

単純にラベルを数字に置き換えるだけの処理。 多くの人が使ったことがあるのではないかと思う。下の例だとA→0、B→1、C→2という様に置き換えてる。pythonだとsklearnのLabelEncoderを使うのが楽。

idx category encoded_feature
0 A 0
1 A 0
2 A 0
3 B 1
4 B 1
5 C 2
import pandas as pd
from sklearn.preprocessing import LabelEncoder

df = pd.DataFrame({
    "category" : ["A","A","A","B","B","C"],
})

le = LabelEncoder()
df["encoded_feature"] = le.fit_transform(df["category"])
df.head(10)

One Hot Encoding

カテゴリ変数の各値ごとにカラムを分けて0、1で表現する方法。下の例だとcategory_A, category_B, category_Cという列を新しく作って、Aの場合には(1, 0, 0)、Bの場合には(0, 1 ,0)、Cの場合には(0, 0, 1)と表現するニューラルネットワークでカテゴリ変数を表すときはOne Hot Encodingが第一選択肢になる気がする。

LabelEncodingのときと同様、One Hot Encoderがsklearnに収録されているので、それを使うのが楽。pandasにはget_dummiesというメソッドが収録されているので、それを使っても同じことが出来る。

category category_A category_B category_C
0 A 1 0 0
1 A 1 0 0
2 A 1 0 0
3 B 0 1 0
4 B 0 1 0
5 C 0 0 1
#one hot  encoding sample
import pandas as pd
from sklearn.preprocessing import OneHotEncoder

df = pd.DataFrame({
    "category" : ["A","A","A","B","B","C"],
})

#文字をLabel Encodingして数値に変換
le = LabelEncoder()
df["encoded_feature"] = le.fit_transform(df["category"])

#One Hot Encoding
ohe = OneHotEncoder()
ohe_array = ohe.fit_transform(df[["encoded_feature"]])
df.drop("encoded_feature", axis = 1, inplace = True)

#DataFrameに変換
ohe_df = pd.DataFrame(ohe_array.todense()).astype("uint8")
ohe_df.columns = ["category_A","category_B","category_C"]

#元のDataFrameと結合
df = pd.concat([df,ohe_df], axis = 1)
df.head(10)

Frequency Encoding

各値の出現頻度で表現する方法。例えば以下の表におけるAの値は、3(Aの出現回数)/ 6(全体の行数)で0.5となる。pandasを使えばgroupbyとmergeを駆使して比較的に簡単に書くことが出来る。

category category_counts frequency_encoding
0 A 3 0.500000
1 A 3 0.500000
2 A 3 0.500000
3 B 2 0.333333
4 B 2 0.333333
5 C 1 0.166667
#Frequency Encoding Sample
import pandas as pd
from sklearn.preprocessing import OneHotEncoder

df = pd.DataFrame({
    "category" : ["A","A","A","B","B","C"],
})

#各カテゴリーの出現回数を計算
grouped = df.groupby("category").size().reset_index(name='category_counts')

#元のデータセットにカテゴリーをキーとして結合
df = df.merge(grouped, how = "left", on = "category")
df["frequency"] = df["category_counts"]/df["category_counts"].count()

df.head(10)

Target Mean Encoding

Likelihood Encodingとも呼ばれる。Google検索でのヒット数的にはTarget Mean Encodingの方がメジャー。あるカテゴリ変数の値を持ち目的変数が1の個数を、その値の出現回数で割ったもの。 下の例だと、Aの出現回数は3回、カテゴリーがAで目的変数が1を持つものの出現回数は2回なのでTarget Mean Encodingの値は2/3 = 0.666 となる。

category outcome category_counts outcome_counts target_mean_encoding
0 A 1 3 2 0.666667
1 A 1 3 2 0.666667
2 A 0 3 2 0.666667
3 B 1 2 1 0.500000
4 B 0 2 1 0.500000
5 C 1 1 1 1.000000

 

#Target Mean Sample
import pandas as pd
from sklearn.preprocessing import OneHotEncoder

df = pd.DataFrame({
    "category" : ["A","A","A","B","B","C"],
    "outcome" : [1,1,0,1,0,1],
})

単純に計算すれば次のようなコードになる。

#各カテゴリーともう一つの特徴量の出現回数を計算
grouped_category = df.groupby("category")["category"].count().reset_index(name='category_counts')
grouped_outcome = df.groupby("category")["outcome"].sum().reset_index(name='outcome_counts')

#元のデータセットにカテゴリーをキーとして結合
df = df.merge(grouped_category, how = "left", on = "category")
df = df.merge(grouped_outcome, how = "left", on = "category")

df["target_mean_encoding"] = df["outcome_counts"]/df["category_counts"]

df.head(10)

Target Mean Encodingには過学習を防ぐための leave one out schemaという手法も提案されている。これは上の表で1列目のAに対してTarget Mean Encodingを計算するときに、自分自身の値を除いて計算する方法だ。この方法を用いて1列目のAに対してTarget Mean Encoding を計算すると(自分を除いたカテゴリーがAで目的変数が1の総数) / (自分を除いたAの出現回数)= 0.5 となる。

category outcome category_counts outcome_counts target_mean_encoding
0 A 1 3 2 0.5
1 A 1 3 2 0.5
2 A 0 3 2 1.0
3 B 1 2 1 0.0
4 B 0 2 1 1.0
5 C 1 2 2 1.0
6 C 1 2 2 1.0
#Target Mean Sample with leave one out scheme
import pandas as pd
from sklearn.preprocessing import OneHotEncoder

df = pd.DataFrame({
    "category" : ["A","A","A","B","B","C","C"],
    "outcome" : [1,1,0,1,0,1,1],
})

#各カテゴリーの出現回数を計算
grouped_category = df.groupby("category")["category"].count().reset_index(name='category_counts')
grouped_outcome = df.groupby("category")["outcome"].sum().reset_index(name='outcome_counts')

#元のデータセットにカテゴリーをキーとして結合
df = df.merge(grouped_category, how = "left", on = "category")
df = df.merge(grouped_outcome, how = "left", on = "category")

#計算する際に自分自身の値は除く
df["target_mean_encoding"] = (df["outcome_counts"] - df["outcome"])/(df["category_counts"] - 1)

df.head(10)

おわりに

4種類のカテゴリ変数の数値化に関する方法を紹介した。「カテゴリー変数に対する手法」という目次をつけておいて、時間が無くて数値変数に対する手法の紹介まで出来なかったのが申し訳ない。 もとのスライドには、Weight of Evidecneと呼ばれる手法や、具体的な特徴量抽出の手順などが記載されているので時間があったら追記するかもしれない。

feather - pandasの読み書きを高速化

kaggleに参加して巨大なCSVファイルを読み書きしていると、それだけで数分近くかかってしまうことがある。 そこで紹介されていたfeatherというDataFrame保存用ライブラリを使ってみた。 featherはpandasのDataFrameを高速に保存、ロードするために開発されたApache Arrowpythonラッパーだ。

Apache Arrow特有のカラム型メモリ管理をすることで、データの読み書きを高速化しているそうだ。 ベースになっているApache Arrowが他言語や他オープンソース(SparkやHadoop)との互換性を重視しているために、feather形式で保存すればRなどでも読み込める。

featherのインストールはpipから簡単にできる。

pip install feather-format

使い方も簡単でwrite_dataframe、read_dataframeを呼ぶだけ。

import feather
import pandas as pd

#データフレームの読み込み
df = feather.read_dataframe(path_feather)

#データフレームの書き込み
feather.write_dataframe(df, path_feather)

またpandasのバージョン0.20.0からpandas.DataFrame.to_featherpandas.read_featherというメソッドが追加されているので、以下のような方法でも使える。

import pandas as pd

#データフレームの読み込み
df = pd.read_feather(path_feather)

#データフレームの書き込み
df.to_feather(path_feather)

約18列、150000行のデータのデータセットで、csvとfeatherそれぞれの読み書きの速度を計測すると、

読み込み速度

# csvからの読み込み
%time df = pd.read_csv(path_csv)

# feather formatからの読み込み
%time df = pd.read_feather(path_feather)
%time df = feather.read_dataframe(path_feather)
CPU times: user 29.1 s, sys: 27.7 s, total: 56.8 s
Wall time: 1min 38s
CPU times: user 5.53 s, sys: 10.1 s, total: 15.6 s
Wall time: 37.7 s
CPU times: user 5.46 s, sys: 11.1 s, total: 16.5 s
Wall time: 35.7 s

書き込み速度

# csvへの書き込み
%time df.to_csv(path_csv)
# featherへの書き込み
%time df.to_feather(path_feather)
%time feather.write_dataframe(df, path_feather)
CPU times: user 53 s, sys: 11.4 s, total: 1min 4s
Wall time: 1min 30s
CPU times: user 18.3 s, sys: 10.1 s, total: 28.4 s
Wall time: 39 s
CPU times: user 18.8 s, sys: 8.76 s, total: 27.5 s
Wall time: 35.5 s

実時間で読み書きともに約2.5倍程度と非常に高速に動作することが確認できた。データセットの保存や読み込みに使えば大幅な時間の節約になる。 長期のデータには向いていない(破損とかに弱い?)そうなので、あくまで前処理したデータのキャッシュなどに使うのがいいと思う。