唯物是真 @Scaled_Wurm

プログラミング(主にPython2.7)とか機械学習とか

scikit-learnでMean Average Precisionを計算しようと思ったら混乱した話

複数ラベルの分類問題を評価しようと思ってMean Average Precisionを計算しようと思ったが、Pythonの機械学習ライブラリのscikit-learn(sklearn)にはaverage_precision_score()関数とlabel_ranking_average_precision_score()関数があってどういう違いがあるのかドキュメントを読んでもいまいちよくわからなかったので調べました
とりあえず最初に結論を書いておくと、複数ラベルの分類問題でよく使われるMean Average Precisionの計算にはlabel_ranking_average_precision_score()関数を使えばよさそう

Mean Average Precision (MAP) とは

情報検索(IR)や機械学習の分野で使われる評価尺度。スコア付きの出力について、正解のものが上位のスコアであるほど大きな値になる(0から1の値を取る)
機械学習では一つの事例に複数の正解ラベルがつけられているmulti labelのタスクで用いられているのを見かける

PrecisionとRecall

最初にPrecisionとは何かについて軽く説明します
機械学習などのシステムでデータを目的のラベル(正例)とその他のラベル(負例)に分類する時、正例と判定したもののうち実際に正例だったものの割合がPrecisionです
$$\text{precision} = \frac{|\{\text{正例と判定されたもののうち実際に正例であったもの}\}|}{|\{\text{正例と判定されたもの}\}|}$$
単純な正解率ではなくPrecisionを使って嬉しい時は、例えばデータ中の目的でないラベル(負例)の比率が非常に大きい場合に全部を負例と分類しても高い正解率になってしまう場合などがあります

逆にシステムが正例と判定したものが、評価用のデータセットの正例のうちどれぐらいの割合をカバーしているのかを示す評価尺度もあり、これはRecallと呼ばれます
$$\text{recall} = \frac{|\{\text{正例と判定されたもの}\}|}{|\{\text{データセット中に含まれる正例}\}|}$$

Precisionが正確さ、Recallがカバー率的なものを表しています

Average Precision

次にAverage Precision (AP) について説明します

Average PrecisionはPrecisionをRecallについて平均をとったものです
PrecisionとRecallを軸にしてPrecision-Recall curveのグラフを書いたときの曲線以下の部分の面積(AUC, Area Under the Curve)がAverage Precisionになります(以下のグラフだと折れ線ですが)
f:id:sucrose:20170225225304p:plain

ちゃんと計算するには積分的な計算が必要になる(?)っぽいですが、近似値の計算として(?)以下のような式が使われているのをよく見かけます
出力をスコア上位順に並べた時にk番目の出力が正例なら1、負例なら0になる関数を\(I(k)\)と定義した時
$$\text{Average Precision} = \frac{\sum_k \text{k番目までの出力で計算したPrecision} \times I(k)}{|\{\text{データセット中の正例}\}|}$$

複数ラベルの分類の評価ではデータの各事例ごとに、正解のラベルを正例それ以外を負例とみなしてAverage Precisionを計算します
簡単な例として以下の表のデータのAverage Precisionを計算してみます

ラベルの種類 ラベルがついているか(ついていれば1) 出力されたスコア
赤い 1 1
丸い 0 0.8
重い 1 0.6
辛い 0 0.4

既にスコアの大きい順に並んでいるので上から順番に見ていきます
正例なのは1行目と3行目です
1行目の時点では\(\text{Precision} = \frac{1}{1}\)
3行目の時点では\(\text{Precision} = \frac{2}{3}\)
なので\(\text{Average Precision} = \frac{1}{2}(\frac{1}{1} + \frac{2}{3}) = \frac{5}{6}\)となります

Mean Average PrecisionはAverage Precisionをそれぞれの事例ごとに計算して平均を取ったものです
$$\text{Mean Average Precision} = \frac{1}{N}\sum_{i=1}^{N} \text{Average Precision}(data_i)$$

また出力のうち上位\(k\)件までしか見ないで評価尺度を計算する時もあります。このときはPrecision@k、MAP@kなどと呼ばれます

scikit-learnの関数

ようやく本題に入ります
sklearnにはaverage_precision_score()関数とlabel_ranking_average_precision_score()関数という似たような名前の関数があり、どちらを使えばよいのかちょっと混乱しました
ドキュメントを見ると、両方共適切な引数を渡せばMAPが計算できそうですが実行してみると違う値になります

from sklearn.metrics import label_ranking_average_precision_score, average_precision_score
average_precision_score([1, 0, 1, 0], [1, 0.8, 0.6, 0.4], average='samples')
#0.79166666666666663
label_ranking_average_precision_score([[1, 0, 1, 0]], [[1, 0.8, 0.6, 0.4]])
#0.83333333333333326

よくわからなくなってきたのでソースコードを読んで調べてみました
すると以下の式の定義と等しい計算をしていそうなのはlabel_ranking_average_precision_score()関数の方でした
$$\text{Average Precision} = \frac{\sum_k \text{k番目までの出力で計算したPrecision} \times I(k)}{|\{\text{データセット中の正例}\}|}$$
上の式では実は面積を以下の画像のように長方形で近似して計算しています
f:id:sucrose:20170226205115p:plain

逆にaverage_precision_score()関数の方では、numpy.trapz()関数を使って面積の計算を台形で近似して計算していました
つまりある意味average_precision_score()関数の方が正確な計算をしているということになります
不勉強のためAverage Precisionの定義として以下のような定義しか知らなかったのですが、実際には以下の式は近似値の計算をしているということっぽいです
$$\text{Average Precision} = \frac{\sum_k \text{k番目までの出力で計算したPrecision} \times I(k)}{|\{\text{データセット中の正例}\}|}$$

まとめ

multi label classificationの評価尺度としてMean Average Precisionを計算したいときはlabel_ranking_average_precision_score()関数を使えばよい(上位k件の出力までしか見ない@kの機能は今はついてなさそう)
scikit-learnのaverage_precision_score()は台形で計算しているので、よく使われるAverage Precisionの式とは少し違う値になる

ちなみに完全に今更ですが、scikit-learnもnumpyもドキュメントにソースコードへのリンクがついていて便利なことに気づきました
f:id:sucrose:20170226205729p:plain
f:id:sucrose:20170226205839p:plain

このはてなブログの記事がGoogleで「モバイルフレンドリーではありません」と表示されるようになっていた

たまたま自分のブログの記事をGoogleで検索したらいつの間にか「モバイルフレンドリーではありません」と表示されるようになっていた
このブログの記事が全滅というわけでもないし、記事に原因があるのかGoogleかはてなブログの問題なのかもよくわからない
f:id:sucrose:20170218152541p:plain
以下のGoogleの公式のツールでモバイルフレンドリーかチェックできるのですが、こちらではなぜかOKで謎
https://search.google.com/search-console/mobile-friendly
f:id:sucrose:20170218152709p:plain
よくわからないのでとりあえず「GOOGLEに送信」を押しておきました
上だけでよさそうな気もするけど念のためGoogleウェブマスターツールの「Fetch as Google」でインデックス登録のリクエストをやっておく

その後、確認したらいつの間にか直っていました(上でやったことに効果があったのかは不明
f:id:sucrose:20170219232755p:plain