2017年04月09日

 ROC曲線を理解する

2値の予測(判別、識別、…)に用いる特徴量の良し悪しを評価する1つの方法として、ROC曲線というものがある。
実際は正で予測も正であるデータの数をTP(True Positive)、
実際は負で予測も負であるデータの数をTN(True Negative)、
実際は負で予測は正であるデータの数をFP(False Positive)、
実際は正で予測は負であるデータの数をFN(False Negative)、
と呼ぶ時、
TPR(True Positive Ratio)=TP/(TP+FN)を縦軸、
FPR(False Positive Ratio)=FP/(FP+TN)を横軸、
としたグラフである。

ROC曲線の例
ROC curve sample

ROC曲線は、必ず(0,0)から始まって(1,1)で終わる。
特徴量が全く予測の役に立たない、ランダムな値であれば、TPR=FPRの線になる。
ROC曲線より下の面積、AUR(Area Under ROC curve)(または単にAUC(Area Under the Curve))が大きいほど、特徴量の値の全域に渡って良い特徴量だとされる。 理想的な特徴量だと、ROC曲線はFPR=0とTPR=1の線になる。

ROC曲線は機械学習で識別器の評価によく用いられるらしいので、とりあえず覚えておこうと思ったのだが、筆者はこれの理解にえらく苦労したので、調べたことや考えたことをメモする。
統計学の検定と同様、こういう確率と論理を組み合わせたものは、人によって向き不向きがあるのだと思いたい。

TP,TN,FP,FNの関係を再度整理すると、次のようになる。

予測

TPFN (Type II error)
FP (Type I error)TN
PやNは予測がPositiveかNegativeかであり、TやFはそれが正解かどうかである。
FPは誤検出のことであり、統計学の検定でも使われる「第一種の過誤」(検定では帰無仮説を棄却できないのに棄却する条件に誤ってヒットしてしまうこと)である。
FNは検出不能であり、「第二種の過誤」(検定では帰無仮説が誤りなのに棄却する条件にヒットしないこと)である。

予測の精度に関する尺度としては、Accuracy, Presicision, Recall, F値があり、それぞれ次のように定義される。
Accuracy = (TP+TN) / (TP+TN+FP+FN)
予測の正解率。
Precision = TP / (TP+FP)
Positiveと予測される中の正解率。
Recall = TP / (TP+FN)
実際にPositiveの内、Positiveと予測される割合。検出力、Sensitivity。
F値(F-measure, F1 score) = ((Precision-1 + Recall-1)/2)-1
PrecisionとRecallの調和平均。統計学のF分布に従うF値とは関係ない。
PrecisionとRecallはトレードオフの関係にあるので、それらをバランス良く合成した尺度。

予測の精度はAccuracyで評価するのが簡単だが、実際の正のデータ数と負のデータ数に偏りがあると、データ数が少ない方の正解率が低くても、データ数が多い方の正解率が高ければAccuracyが高くなってしまうので、Accuracyだけでは適切に評価できない。
そのような場合にPrecisionやRecallが用いられるが、これらは一般に特徴量の閾値によってトレードオフの関係があり、セットで評価しないといけないので、単純比較には向かない。そこで用いられるスカラー値が、F値や、ROC曲線のAUCである。
Precision-Recall曲線のAUCも使われることがあるが、Presicionは実際の正のデータの割合に依存するので、正のデータの割合が同じでないと比較には使えない。実際の正のデータの割合が極端に小さい場合など、Precisionが大きな意味を持つ場合にはPrecision-Recall曲線が用いられる。

ROC曲線は、TPR=TP/(TP+FN)とFPR=FP/(FP+TN)のグラフである。TPRを陽性率、FPRを偽陽性率と呼ぶこともある。TPRはRecallと同じである。FPRはfall-out(副産物)と呼ばれることもある。

次の図の3つのROC曲線が、正のデータと負のデータがどのように分布する特徴量に対応するかを考えてみる。
ROC curve sample 2

例えば、次のような分布になる特徴量だと、青いROC曲線になる。
distribution of totally independent feature
横軸は特徴量、縦軸は赤い部分が正のデータの分布、青い部分が負のデータの分布を表している。このグラフでは、正のデータも負のデータも一様分布している。閾値tより右ならPositive、左ならNegativeと予測する時、tを右端から左に動かすと、TPRもFPRも0から1に向かって増大するが、常にTPR=FPRである。正のデータと負のデータの割合はグラフの形状には関係しない。

次のような、正のデータと負のデータが完全に分かれる理想的な特徴量だと、緑のROC曲線になる。
distribution of ideal feature
tを右端から左に動かすと、FPR=0のままTPRが0から1に変化し、青のゾーンに入ると、FPRが0から1に変化する。

次のような分布だと、赤いROC曲線になる。
distribution of which makes ROC curve perfect arc
tを右端から左へ動かすと、FPRよりもTPRの方が早く上昇する。なるべく正のデータと負のデータの分布が分離している良い特徴量ほどFPRが上昇する前にTPRが上昇するので、AUCが大きくなることがわかる。

AUCはどれくらいだと良いか、という基準は一般的なものも色々あるようだが、大体、最低0.7は無いと有効ではないとされるようである。

なお、特徴量の最良の閾値(cut-off)はROC曲線の(0,1)に最も近い点とする、という方法を複数の箇所で目にしたが、明確な理論的根拠がある訳ではなく、必ずしもそれに限定されないようである。そもそも、(0,1)に最も近いというのがユークリッド距離で良いのかどうかがわからない。

続きを読む "ROC曲線を理解する"

2017年03月31日

 pandasでmergeせずにgroupbyで間接参照したい

この前、pandasを使っていて、次のような感じの、関連する2つのテーブル、access_logとchoice_logがある時に、結合したテーブルを作らずにchoice毎のtimestampの最小値を求めたかったのだが、どう書けば良いのかわからなかった。

import pandas as pd
import numpy as np
access_log = pd.DataFrame({'session': [100, 101, 102, 103, 104, 105, 106, 107, 108, 109],
                           'timestamp': [314, 159, 265, 358, 979, 323, 846, 264, 338, 327]})
choice_log = pd.DataFrame({'session': [100, 100, 101, 102, 102, 103, 104, 104, 105, 106, 106, 107, 108, 108, 109],
                           'choice':  ['A', 'B', 'C', 'D', 'E', 'A', 'B', 'C', 'D', 'E', 'A', 'B', 'C', 'D', 'E']})
>>> access_log
   session  timestamp
0      100        314
1      101        159
2      102        265
3      103        358
4      104        979
5      105        323
6      106        846
7      107        264
8      108        338
9      109        327
>>> choice_log
   choice  session
0       A      100
1       B      100
2       C      101
3       D      102
4       E      102
5       A      103
6       B      104
7       C      104
8       D      105
9       E      106
10      A      106
11      B      107
12      C      108
13      D      108
14      E      109
>>> 

結合テーブルを作るなら、次のように書ける。

merged = choice_log.merge(access_log, on='session', how='left')
result = merged.groupby('choice')['timestamp'].min()
>>> result
choice
A    314
B    264
C    159
D    265
E    265
Name: timestamp, dtype: int64
>>> 

実際にあったテーブルは巨大で、他にも列がたくさんあり、単純に結合テーブルを作るとRAMが足りなくなってメモリスワップが多発したので、結合テーブルを作らずにこれと同じことがしたかったのだが、その書き方がわからなかった。
結局access_log.set_index('session').to_dict()のようにして一時的なdictを作って、スワップを多発させながら処理してしまった。
それが心残りだったので、改めてpandasのドキュメントを拾い読みしながら方法を探してみた。

  1. 単純に、別のテーブルを参照する関数をSeries.mapに渡す
    def session_to_timestamp(session_series):
        return session_series.map(lambda x: access_log[access_log.session==x]['timestamp'].iat[0])
    
    result = choice_log.groupby('choice').agg(lambda x: session_to_timestamp(x).min())
    

    Seriesの先頭の要素を取り出す方法には、.iat[0]の他に.iloc[0]や.values[0]などがあり、筆者が試した所values[0]の方が速かったりしたが、pandasのドキュメントに書かれているのはilocとiatなので、ここでは添字が整数なら高速なiatを用いた。

  2. リスト内包表現(list comprehension)で別のテーブルを参照する
    def session_to_timestamp(session_series):
        return [access_log[access_log.session==x]['timestamp'].iat[0] for x in session_series]
    
    result = choice_log.groupby('choice').agg(lambda x: min(session_to_timestamp(x)))
    

    リストにはminメソッドが無いので、session_to_timestamp(x).min()とはできない。

  3. isinを使ったBoolean Indexingで別の表のサブセットを得る
    def session_to_timestamp(session_series):
        return access_log[access_log.session.isin(session_series.values)]['timestamp']
    
    result = choice_log.groupby('choice').agg(lambda x: session_to_timestamp(x).min())
    
  4. 別の表からSeriesを作ってSeries.mapに渡す
    def session_to_timestamp(session_series):
        return session_series.map(access_log.set_index('session').timestamp)
    
    result = choice_log.groupby('choice').agg(lambda x: session_to_timestamp(x).min())
    

    一見シンプルで美しそうだが、set_index()はコピーを返すので、timestampの一時的なdictを作るのと変わらない。しかも、グループ数だけ新たなテーブルを作るので、無駄である。

これらの処理時間を色々測ってみたが、2つのテーブルのサイズやグループの数によって変わり、どう比較すれば良いかわからなかったので、省略する。
大まかな傾向としては、1.と2.の処理時間はchoice_logのサイズに依存し、3.と4.の処理時間はsession_logのサイズに依存するようだった。4.はset_indexした中間テーブルを事前に作っておくと高速化するが、それでも、大抵の場合3.が一番速かった。
いずれの方法も最速になる場合があるようなので、場合毎に色々試してみるしかなさそうである。

肝心のメモリ使用量は、適当な測り方がわからなかった。
そもそも、スワップしながらの処理時間が問題だったので、単純にメモリ使用量では測れないと思う。

他にも、以下のような方向で書き方を考えてみたが、うまくできなかった。

  • groupbyでaggregateでなくtransformしてmin()
    transformする時に別のテーブルを参照することを考えたが、transformするとgroup解除されてしまうので、使えなかった。transformする時にmin()するのなら、min()した値を増殖させるだけ無駄なので、確実にaggregateの方が効率が良い。
  • pandas.DataFrame.lookupを使う
    引数としてindexしか受けられないので、使えなかった。
  • pandas.DataFrame.joinを使う
    pandas.DataFrame.mergeを使うのと変わらなかった。

そもそも、lambda式を使わずに書く方法は無いのだろうか。
teratailとかStack Overflowとかで聞いた方が早いか。

続きを読む "pandasでmergeせずにgroupbyで間接参照したい"

2017年03月05日

 pandas.read_csv+numpy.bincountでエラー

ちょっと前に、はやりのAIのプログラミングでよく使われるPythonとNumPyとPandasを使い始めたのだが、PandasでCSVファイルを読み込んで、各ラベルの出現回数を得る為にNumPyのbincountメソッドを使うと、次のようなエラーが出て、困った。

TypeError: Cannot cast array data from dtype('int64') to dtype('int32') according to the rule 'safe'
その時使った環境は、Windows 7(32bit版)+Python 3.5.2(Anaconda 4.1.1 (32-bit))である。Mac OS Xでは出なかった。

これは、32bitのPythonを使っていると起こることらしい。例えば、32bit Pythonで次のプログラムを実行すると、同じエラーが出る。

import numpy as np
import pandas as pd
import io

data = np.random.randint(10, size=30)
buf = io.StringIO("\n".join(str(x) for x in data))
df = pd.read_csv(buf, header=None)
x = df[0].values
print(x)
print(np.bincount(x))	#Error on 32bit Python
これは、Pandasが32bit Pythonでもint64を使うのが原因のようである。

このエラーを回避するには、bincountに渡すデータの型をint32にすれば良い。

print(np.bincount(x.astype('int32')))	#also OK on 32bit Python
出力例
[1 0 6 7 1 7 7 6 9 5 6 2 8 0 7 6 7 8 9 7 8 9 8 0 2 1 2 6 0 3]
[4 3 3 1 0 1 5 6 4 3]

または、np.unique(return_counts=True)を使う方法があり、こちらの方が、大抵の場合はnp.bincountを使うよりも好ましいとされるようである。

print(np.unique(x, return_counts=True))
出力例
(array([0, 1, 2, 3, 5, 6, 7, 8, 9]), array([4, 3, 3, 1, 1, 5, 6, 4, 3]))
確かに、np.unique(return_counts=True)の方が、データに負の値があっても使えるし、大きな値が混ざってても配列が巨大にならないので、安全そうである。

なお、今動作しているPythonが32bitか64bitかを判定する方法は、いくつかあるようである。 例1

import platform
platform.architecture()
出力(上がMacOSX、下がWin32)
('64bit', '')
('32bit', 'WindowsPE')
例2
import sys
"%x" % sys.maxsize
出力(上がMacOSX、下がWin32)
'7fffffffffffffff'
'7fffffff'
例3(platform.architecture()の実装にも使われている方法)
import struct
struct.calcsize("P") * 8
出力(上がMacOSX、下がWin32)
64
32

続きを読む "pandas.read_csv+numpy.bincountでエラー"

2017年03月01日

 Carbon EmacsにPython 3環境導入

筆者はMacで未だにEmacs 22ベースのCarbon Emacsを多用している。Emacs 24ベースのCocoa Emacsもインストールはしているのだが、色々な環境をCarbon Emacsに作ってしまっているので、移行するのが億劫なのである。

最近、Python 3を使い始めたのだが、Carbon Emacsのpython.elはPython 2にしか対応していないので、何とかPython 3に対応させる方法は無いかと探した結果、
http://www.loveshack.ukfsn.org/emacs/
から
python.el
emacs.py
sym-comp.el
の3つをダウンロードして、Emacs.app/内に置けば良いことがわかった。(python.elとemacs.pyは既にあるものを置換、sym-comp.elはpython.elと同じディレクトリに追加)
そして、Emacsを立ち上げて、M-x customize-groupとし、
"Python Default Version" = 3
"Python Python Command" = python3
にすれば完成である。

これで、C-c TABとM-TABを駆使すればシンボルの補完ができて、まあまあコーディングが楽になるのだが、やっぱり補完機能は便利にしたいので、
Pythonの補完をEmacsでシンプルに最小労力で手早く使えるようにする - 牌語備忘録 -pygo
を参考にして、auto-complete.el + ac-python.elをインストールした。
auto-complete.elは、Emacs 22で動作実績のある、auto-complete-1.3.1.tar.bz2をどこかから入手した。

そして、.emacsに次の4行を書けば完成である。

(require 'auto-complete)
(require 'auto-complete-config)
(ac-config-default)
(require 'ac-python)

これで、python-modeにして、import mathと書いてC-c TABして、math.と打つと、math.sqrtやらmath.piやらが補完候補として自動的に現れるようになった。

続きを読む "Carbon EmacsにPython 3環境導入"

2016年11月27日

 対石田流の定跡で悩む

筆者は石田流が苦手である。後手になって、初手から▲7六歩△3四歩▲7五歩とされると、これは大変なことになったな、と思ってしまう。同じぐらいの実力(アマ三段程度)同士だと、石田流にされると3局に2局は負けてると思う。
先週は将棋大会があり、石田流対策としては、▲7六歩△3四歩▲7五歩には△4二玉、下図の局面から仕掛けるなら△9二飛▲8五桂△8二飛▲8六歩△8四飛、の2つだけを覚えて臨んだら、予選の1局目から▲7六歩△3四歩▲7五歩とされ、次の図の局面を迎えてしまった。

このような局面から△9二飛▲8五桂△8二飛▲8六歩△8四飛と飛車を端に振ってすぐ戻して浮くのは、過去に何かの本で読んだもので、多分定跡だと思う。もし△9二飛▲8五桂△8二飛の次に▲8六飛とされたらどうすればいいんだろう、と不安に思っていたが、他に何も思い付かなかったので、定跡と信じて△9二飛、▲8五桂、△8二飛と手早く指した。

すると、ノータイムで実際に▲8六飛とされてしまい、長考に沈んでしまった。
後で調べると、ここまでは2004年のNHK杯の三浦−行方戦と全く同じ進行であり、その対局ではやはり▲8六歩と指されていた。他にも部分的に同じ形から△9二飛▲8五桂△8二飛としたプロの棋譜を4つぐらい見つけたが、次の手は全て▲8六歩△8四飛だったし、激指14で分析しても▲8六飛は評価が低いので、良くない手なのだと思うが、これの咎め方がよくわからない。

次に何をやっても▲7三桂成がある。それには△8六飛(次図)とする一手だが、▲同角でも△7三桂の後に先攻されてすぐ桂損を回復されるし、▲6三成桂と踏み込まれる手も気になる。

筆者の実戦は、▲8六飛の後、△7四歩▲7三桂成△8六飛▲同角△7三桂▲7四歩△同銀▲7一飛(次図)と先着され、△6二金と受けたら▲9一飛成から大暴れされ、なす術無く、十数手後には激指の先手の評価値が+1000という必敗の形勢になってしまった。

激指によると、△6二金が悪手で、ここまで来たら桂取りが銀取りにならないように△6三銀と戻るのが良いらしいが、そこまで後手を引いて駒損を回復されて龍まで作られてしまうのでは、何をやってるのかわからないと思う。
ただ、△7四歩では△6五歩の方が良さそうである。▲7三桂成と歩を取られるのなら逃げておこうと思って△7四歩と避けたのだが、△7三桂と取り返した後に▲7四歩と取る手が桂取りの先手になるし、△7四歩が無くても△7三桂には▲7四歩と突き出す所なので、意味が無かった。▲8六飛の後、△6五歩▲7三桂成△8六飛▲同角△7三桂▲7四歩△同銀▲7一飛△5五角というのが激指の推奨手順である。

激指はこれで-400(後手少しリード)くらいの評価を示しているが、筆者にはこれで後手が指せる理由がわからない。次に△3六桂とやっても▲3九玉と引かれるので攻め方がわからないし、やはり▲9一飛成から暴れられて、筆者の実力ではその勢いを止められないように思う。
ただ、もし△5五角に▲5六歩とされれば、△3六桂で一気に後手勝勢になりそうである。△3六桂に▲3九玉だと△6六角が王手で危険なので▲1七玉しか無いが、△1五歩とすれば受けが難しいと思う。

▲5五歩なら△1六歩▲2六玉△2八桂成である。
こうなることを期待して、▲8六飛には△6五歩とするべきなのだろうか。