静かなる名辞

pythonとプログラミングのこと



【python】分類タスクの評価指標の解説とsklearnでの計算方法

 分類結果の評価指標として、混同行列(confusion matrix)、適合率(precision)、再現率(recall)、F1値(F1-measure)*1などがあります。

 分類の評価をやるときはとりあえずこれらを出せば良い、ということで日常的に用いられるものですが、意外とまとまった解説をネット上で見かけません。私もこれまでなんとなく使っていましたが、それじゃいかんなぁ、とずっと思っていました。

 この記事はこれらの評価指標について解説します。ついでにsklearnでの計算方法も書いておきます。

 目次

スポンサーリンク



理論の解説編

基本編(二値分類)

混同行列の話

 混同行列は分類結果を表、というか行列の形で表したものです。混同行列はすべての評価指標の基本となります。

 一つの数値として指標が得られる訳ではない、という意味では厳密には評価指標には含まれないかもしれませんが、これを理解しておくと、適合率・再現率・F1値もとても理解しやすくなります(というか混同行列がわからない状態で、この3つを理解するのは困難)。なので、最初は混同行列から理解するのが良いでしょう。

 混同行列とは、こんな奴のことを指します。
f:id:hayataka2049:20180314112047p:plain
Excelで5分で描いた画像なので見栄えが寂しいのは許してください)

 重要なのは中央のTP, TN, FP, FNという4つの記号です。これはそれぞれTrue Positive, True Negative, False Positive, False Negativeの略です。

 TP, TNは「正しくPositive or Negativeに分類されました(本当にPositive or Negative)」という意味です。同様に、FP, FNは「間違ってPositive or Negativeになっちゃいました(本当はNegative or Positive)」という意味だと覚えておけば良いと思います*2

 さて、実際にはTP, TN, FP, FNには、それぞれ対応する数字(条件に当てはまるデータの件数)が入ります。400件のデータを分類して混同行列を得たら、TP, TN, FP, FNをぜんぶ足し合わせるとちゃんと400になる、という具合です。

 評価指標を用いなくても、この混同行列を見て分類結果の良し悪しを判断することも(主観的になりそうですが)可能です。

 たとえば、400件の分類結果に対して、TP=150, TN=100, FP=50, FN=100という結果を得たとしましょう。まずTP+FN、TN+FPで真値がPositive, Negativeなデータの件数を計算すると、250、150という数字が出ます。400件の中にはPositiveが多め、ということがこれでわかります。次に正解率(accuracyと言います)を出してみましょう。これはTP+TNを全データ数400で割れば良く、つまり250/400=0.625という数字になります。あまり良くなさそうです。ではどんな風に良くないのかというと、FPは50、FNは100ということで、間違ってPositiveになってしまうことは相対的に少ないけど、間違ってNegativeを出している率が相対的に高いことがこれでわかります。

 このように、混同行列を見ることでだいたいどんな分類結果なのかを理解できます。また、他の評価指標には含まれない情報も含むので(データ、予測結果に実際にどんな比率でP/Nが含まれているのか、多クラス分類の場合はどのクラス同士で混同が起きやすいかなど)、けっこう重宝します。

評価指標の概念の説明

 混同行列から分類結果の良し悪しを判断できると言っても、上のようなことを一々考えるのはけっこう大変そうですね。そこで、数字一つで済む評価指標の出番になる訳です。

 先に言っておくと、適合率・再現率・F1値が表す情報はすべて混同行列に含まれています。実際、これらの評価指標は混同行列から計算できます。よくある説明は、混同行列の記号を用いた数式を描いて、それで終わり、というものです。

 しかし概念を理解することも重要なので、この記事では数式を出す前に言葉だけで説明してみます。数式は後ほど出します。

  • 適合率

 適合率(precision、精度と訳されることもあります)は、いわば「どれだけ嘘の分類結果が少ないか」の指標です。端的にいうと、Positiveと判定されたものの中にどれだけPositiveが含まれているかの割合です(ちなみにNegativeでも理屈の上では同様に計算できる。これについてはずっと後ろで後述する)。

 インフルエンザ検査で例えると、検査が陽性になったけど実はインフルエンザにかかっていませんでした、という人がどれだけいるかの指標になります。

  • 再現率

 再現率(rcall)は、「どれだけ取りこぼさないで分類できたか」の指標です。真値がPositiveの人の何割をPositiveと判定できたかを表します。

 インフルエンザ検査では、インフルエンザにかかってるのに検査しても陽性にならなかった、という人がどれだけいるかの指標になります。

  • F1値

 適合率や再現率はわかりやすい指標ですが、ではこれらを高めれば良いのか、というと一概にそうは言えない面があります。

 たとえば、インフルエンザ患者が50人、ただの風邪の人が50人いたとしましょう。

 100人中一番インフルエンザっぽい人に『だけ』陽性を出す検査というものがあったとして、その適合率はほぼ100%くになるに違いないでしょう。しかし、残りのインフルエンザ患者49人は陰性になってしまうのだから、ちょっと困ったものです。というか使い物になりません。

 同様に、100人全員インフルエンザ患者、という結果を出す検査があったとして、再現率は100%です。しかし、これは当然何の役にも立ちません。

 この適合率と再現率には、実はトレードオフがあります。上の例で、それぞれ適合率がほぼ100%のときの再現率、再現率が100%のときの適合率を考えてみると理解できるかと思います。そこで、中間くらいを取ってやれ、という指標を考えることができます。平均で良いじゃん、と思う訳ですが、実際には調和平均を使います。それがF1値(F1 measure)です。

 調和平均を使う理論的な根拠の説明はけっこう難しいようですが、こちらの記事が参考になります。

F値に調和平均を使う理由(再) - あらびき日記

 直感的な理解としては、調和平均は低い方に引っ張られる(それも高低差が大きければ大きいほど引っ張られる)平均なので、適合率、再現率どちらかに特化しすぎた結果はポイされることになり、とても都合が良い、と思っておけば良いかと思います。なお、調和平均については下のページが参考になります。

http://www004.upp.so-net.ne.jp/s_honma/mean/harmony2.htm

 F1値は分類結果の直接の良し悪しの指標になります。ですから、論文などではこれの改善*3を以て提案手法の有効性をアピールする、といったことがよく行われます。

評価指標を数式で書く

 お待ちかね(?)の数式です。といっても、基本的には上で書いたことを数式で表現するだけです。ここで混同行列を先に説明して、記号を導入しておいたことが生きてきます。

  • 適合率

 Precision = \frac{TP}{TP+FP}

  • 再現率

 Recall = \frac{TP}{TP+FN}
 ※FNは「間違ってNになっちゃってるけど本当はP」なので TP+FNで「本当にP」のデータの総数になる。

  • F1値

 F1-measure = \frac{2\cdot Precision \cdot Recall}{Precision+Recall}
 これは調和平均の定義通り式を書いていますが、PrecisionとRecallを展開して式を整理することができ、最終的には次のようになります。
 F1-measure = \frac{2TP}{2TP+FP+FN}
 なんとなく適合率と再現率の式を混ぜた感じに見える・・・というか分子分母をそれぞれ足した値ですね。どうしてこうなるかというと、これは調和平均を計算したので、
 \frac{1}{F1-measure} = \frac{1}{2}(\frac{TP+FP}{TP} + \frac{TP+FN}{TP})
 という式になる訳で、あとは逆数にしてやると上の式になることが理解できるかと思います。適合率、再現率でともに分子がTPなので、式が綺麗になるのがミソ。

どの指標を使えば良い?

 適合率・再現率・F1値を紹介しましたが、どれを重視するかはケースバイケースです。単純に考えるとF1値が良さそうですが、実際にはタスクによってどれを重視するかが変わってきます。

  • 例1:致死率が数十%の危険な感染症が海外で流行している。入国者を空港で検査し、検査が陽性なら隔離したい

 →この場合、FPを出す(偽陽性と言います)より、FNを出してすり抜けられてしまうことの方が遥かに大問題です。ですからとにかく再現率の高い検査を持ってこい、ということになると思います。適合率が10%とか、場合によっては1%であっても許容されるかもしれません(検査に引っかかった人全員を潜伏期間が終わると予想される時期まで隔離するとか、再現率99.9%、適合率1%の簡易検査で引っ掛けて精密検査でFPを弾くといった戦略もあり得る)。

  • 例2:単語で検索すると関連するWebページを出してほしい(Web検索)

 →この場合、その単語に関連するWebページがすべて表示されなくても、とりあえず5ページとか10ページ表示されれば実用上そんなに問題はないでしょう。しかし、無関係なページが結果に紛れ込んでしまうとユーザーエクスペリエンスを大きく損ないそうです。この場合、どちらかといえば再現率より適合率を重視すべき(FPを減らすべき)です。

  • 例3:ミカンとオレンジが混ざってしまったので分類したい

 →この場合、例1や例2とは違って、適合率と再現率のどちらが重要ということは特にありません。むしろ変にどちらかに偏った分類結果になる方が厄介なので、F1値で評価しておけば良いシチュエーションということになります。

 要するにFPとFNのどちらを重視するか、それともどちらも同程度に重視するかで着目する指標が変わってきます。もちろんすべて高いに越したことはないのですが、適合率と再現率にはトレードオフがある以上、最終的には「タスクに相応しい指標を重視する」ことも重要になってくると思います。

多クラス分類編

 これまではPositive or Negativeだけの二値分類について考えてきました。しかし、実際の分類問題では多クラス分類という、三値以上のクラスに分類するケースもけっこうあります。

 この場合、評価指標の計算方法が二値分類とは変わってきます。しかもややこしいことに、結果全体に対して評価指標を計算するか、複数のクラスそれぞれで評価指標を計算するか、それぞれで計算した場合はどう平均して全体の結果を出すかといった問題があります。

 このあたりの問題をちゃんと説明したWebページや書籍は、これまでほとんど見たことがありません。ある意味では適当にやってもなんとかなる*4世界だからかもしれませんが、はっきり言って問題だと思います。この記事ではできるだけ丁寧に説明してみるつもりです。

多クラス分類の混同行列

 とりあえず一例として、A,B,Cの3つのラベルを考えることにします。別にミカン、オレンジ、デコポンでも、巨峰、マスカット、デラウェアでも、なんでも構わないと言えば構わないのですが。

 多クラス分類でも二値分類の場合と同じように混同行列を描くことができます。混同行列は真のクラスラベルと予測先がわかっていれば描けます。行列の概形としてはこうなります。

f:id:hayataka2049:20180314112221p:plain

 ここでFA(B)といった記号を導入していますが、一般的に使われる記号ではないので注意してください。私が勝手に記号を付けただけです。これは「間違ってAになっちゃった(本来はB)」という意味です。以下ではこの記号を使って説明していきます。

 なお、上述の通り多クラス分類の場合では、評価指標の出し方に何通りかの方法があります。それだけ「本当のところどうなってるのよ」という疑問も出てきやすいので*5、二値分類の場合と比較して多クラス分類では混同行列による評価がより重視される傾向にあるかと思います。論文などを読み書きするときは、その辺りへの配慮も重要になってくるかもしれません*6

クラスごとの適合率・再現率・F1値

 基本的に、多クラス分類の評価指標はクラスごとに計算するのが一番簡単です。クラスAに対する評価指標、Bに対する評価指標、Cに対する評価指標という形で計算することができます。

 つまり、以下の式のようになります。

 P_A = \frac{TA}{TA+FA(B)+FA(C)}
 R_A = \frac{TA}{TA+FB(A)+FC(A)}
 F_A = H(P_A, R_A)
(P:Precision, R:Recall, F:F1-measure, H:harmonic mean)

 基本的な考え方は二値分類のときと変わりませんが、上の混同行列ではABCの3クラスがありますから、当然3つのクラスに対してそれぞれ適合率・再現率・F1値が求まります。

マクロ平均

 上の方法で各クラスごとに評価指標を計算した場合、各クラスごとの分類の状況はわかるものの、分類結果全体の良し悪しの判断は難しくなります。そこで、なんとか全体で一つの評価指標にまとめたい、というニーズが出てきます。

 もっとも単純なのがマクロ平均(macro average)です。大げさな名前が付いていますが、単なる全クラスの結果の平均に他なりません。適合率、再現率、F1値をそれぞれのクラスごとに求め、それを平均するだけです。

 あと、これは私の長年の疑問で、蛇足なので読み飛ばして頂いても構わないんですが、「各クラスごとにF1値を求めてからF1値のマクロ平均を計算する」方法だと、F1値のマクロ平均は「適合率のマクロ平均と再現率のマクロ平均の調和平均」と一致しない気がするんですが、それで良いんですかね・・・。で、恐らくこの事態を避けるために「適合率のマクロ平均と再現率のマクロ平均の調和平均」をF1値として評価指標に用いている論文も読んだことがあるんですが、どっちが妥当なんでしょうか。ご存知の方は教えて頂けると助かります。ちなみに後述するsklearnの関数はaverage="macro"を指定すると「各クラスごとにF1値を求めてからF1値のマクロ平均を計算する」でやってくれるみたいですが、そっちの方が良いのかな・・・。

重み付き平均

 マクロ平均には各クラスごとのデータ件数の偏りが反映されないという欠点、というか特性があります。1000件のデータがあるクラスも10件のデータがあるクラスも一緒くたに同じ重みで扱われる、ということで、1000件の方の評価指標は0.95なのに10件の方が0.6になってくれたせいで0.775くらいにされちゃったとか、逆に1000件の方が0.3でも10件の方が0.9なら0.6扱いになるなど、理不尽というか「それで良いの?」という結果が出ることがあります。

 これを改善したのが重み付き平均で、これも基本的にはデータ件数の比率で重み付き平均を計算してやるというわかりやすい処理をするだけですから、数式で説明するほどのことはありません。

 ただ、真のクラスの比率に基づいた重みを使うか、予測ラベルに基づいた重みを使うかという微妙な問題が、あるといえばあるような気はします。まあ、普通は真のクラスの方でやるんだと思いますが。

 ちなみにこの重み付き平均が論文などで使われているのは、ほぼ見たことがありません。上述のマクロ平均か、後述のマイクロ平均が使われていることが多いという印象です。

マイクロ平均

 マイクロ平均(micro average*7)は、「クラスごとに計算してどうにか平均する」という方針を取りません。

 ではどうやるかというと、まず混同行列全体でTP, FP, FNを集計し、それに基いて二値分類と同様に評価指標を算出します。

 TP = TA+TB+TC
(TNはどうせ使わないので省略)
 FP = FP_A+FP_B+FP_C = FA(B)+FA(C)+FB(A)+FB(C)+FC(A)+FC(B)
 FN = FN_A+FN_B+FN_C = FB(A)+FC(A)+FA(B)+FC(B)+FA(C)+FB(C)

 まあ、簡単な話ですね。

 これには当然データ件数が効いてくるので、重み付き平均と似たような性質があります。じゃあ重み付き平均と一致するのかというと、上でちろっと書いた「真のクラスの比率に基づいた重みを使うか、予測ラベルに基づいた重みを使うか」の問題が首をもたげてきます。要するに「適合率に対しては予測ラベルの比率」、「再現率に対しては真のラベルの比率」で重み付き平均を計算すればマイクロ平均と同じ結果になるんだと思いますが*8、そんな面倒くさいことするくらいなら素直にマイクロ平均って言えば済む話ですね。なんとなくこちらの方が理論的に妥当そうな気もする、ということがあり、重み付き平均よりマイクロ平均が積極的に使われる結果を招いています。

どれを使うか

 クラスごと、マクロ平均、マイクロ平均の3択で考えることにします。重み付き平均は、選択肢に載せないことにしましょう。

  • クラスごとの評価指標

 ある意味これを見るのが一番間違いがない。ただし、クラス数と同じ個数出てくるので(適合率、再現率、F1値をそれぞれ計算するとクラス数*3個ですね!)、クラス数が多いと評価が困難になる。

  • マクロ平均

 クラスごとのデータ件数の比率を考慮しないのは駄目では? と思われがちだが、クラスごとにデータ件数にばらつきがないケースではこれを使っても問題は起きない。また、データ件数が少ないクラスでひどい結果になっていたりした場合、マイクロ平均では埋もれてしまうがマクロ平均では敏感に(1/クラス数の比率で)評価指標に反映されるので、一概にそれが悪いとは言えない。むしろ「どのクラスも同じ価値がある」とみなすマクロ平均は、分類結果の良し悪しをストレートに反映するという考え方もできる。ただし、データ件数10件のクラスとかあったらそのクラスの評価指標はざっと0.1刻みになってしまう訳で、1000件のクラスと10件のクラスでマクロ平均するとほぼ10件のクラスに引っ張られて使い物にならん・・・という考え方もできる。要するにケースバイケース。

  • マイクロ平均

 クラスごとのデータ件数の比率が、実際に使ったりするときとだいたい同じであれば、全体的な良し悪しの指標として妥当そう。ただし、小さいクラスで結果が悪い場合は普通に埋もれるので、そういった観点で細かくケアしてあげないといけない気がする。

 正直、間違いがないのはクラスごと、マクロ平均とマイクロ平均は率直に言って甲乙つけがたいというか、どっちもどっちという感想です。紙幅に制限がなければ、マクロ平均、マイクロ平均、クラスごとの評価指標、混同行列をすべて示すのが無難というか誠実な気がしますが、そうも行かない場合が多いので、実際は「えいや」でマクロ平均かマイクロ平均のどちらかを選ぶ、といった感じになることが多いと思います。「結果が酷くない」「クラスごとのデータ件数に酷いばらつきがない」という条件なら、マクロ平均とマイクロ平均はどちらを選んでも大差はないですし・・・。

二値分類の取り扱いについて

 これまで多クラス分類について説明してきましたが、実は二値分類を「2クラスの多クラス分類」と解釈することも可能です。そしてPositiveとNegativeそれぞれで評価指標を計算したり、マクロ平均やマイクロ平均を計算することも当然可能です。

 二値分類のところで示した評価指標の計算式はけっきょくPositiveについてだけ計算していると解釈することができますが、Negativeについても同様に計算すると当然異なった数字が出てきます。そしてPositiveとNegativeでマクロ平均やマイクロ平均を取ってやると、当然Positiveだけで出した数値とは異なる結果になります。

 何が言いたいかというと、ミカンとオレンジを分類するときなどは本来「2クラスの多クラス分類」とみなしてマクロ平均やマイクロ平均で評価するのが妥当なはず、ということです。インフルエンザ検査は「ポジネガの結果になる二値分類」とみなしても構わないかもしれませんが・・・。

 この辺りのことをちゃんと説明してくれた資料にはこれまで出会ったことがありませんし、実用上これをどこまで厳密に使い分ける必要性があるのかもなんとも言えませんが、理屈の上では上で書いた通りになると思っています。ですから、二値分類の評価指標の算出は意外と気楽にはできないと思っています。「本当にその評価指標は妥当なの?」という質問は、実験結果を根底から否定されかねないとても厄介なものです。皆さんにはしっかり対策して挑んでほしいところです。

スポンサーリンク


実践編(sklearnでの計算方法)

 おまたせしました、pythonで計算する方法です。手っ取り早くやるためにsklearnを使います。

 評価指標の計算にはsklearn.metricsというモジュールを使います。便利な評価指標計算関数がたくさん入っているので、これを活用して計算していくことになります。

API Reference — scikit-learn 0.20.0 documentation

 基本的な使い方はどの関数も大体同じで、真のラベル、予測ラベルの順に引数を渡してあげると評価指標が返ります(返り値の形式は関数によって色々ある)。

混同行列の計算

 次のように計算することができます。

>>> true = [0,0,0,0,0,1,1,1,1,1,2,2,2,2,2,3,3,3,3,3]
>>> pred = [0,0,1,1,1,0,1,1,1,1,2,1,1,2,2,3,2,1,3,3]
>>> from sklearn.metrics import confusion_matrix
>>> confusion_matrix(true, pred)
array([[2, 3, 0, 0],
       [1, 4, 0, 0],
       [0, 2, 3, 0],
       [0, 1, 1, 3]])

 結果はnumpy配列で返りますので、画像形式で出力したい場合は別途matplotlibなどで描画する必要があります。

 seabornを使うのが簡単です。これについては過去に記事を書いたので、ご参考にどうぞ。
hayataka2049.hatenablog.jp

評価指標の計算

 適合率、再現率、F1値を計算する関数はそれぞれ存在しています。

  • sklearn.metrics.precision_score
  • sklearn.metrics.recall_score
  • sklearn.metrics.f1_score

 しかし、これらを利用するより、一括で結果を計算してくれる「precision_recall_fscore_support」を利用した方が簡単です。

>>> true = [0,0,0,0,0,1,1,1,1,1,2,2,2,2,2,3,3,3,3,3]
>>> pred = [0,0,1,1,1,0,1,1,1,1,2,1,1,2,2,3,2,1,3,3]
>>> from sklearn.metrics import precision_recall_fscore_support
>>> precision_recall_fscore_support(true, pred)
(array([0.66666667, 0.4       , 0.75      , 1.        ]),
 array([0.4, 0.8, 0.6, 0.6]),
 array([0.5       , 0.53333333, 0.66666667, 0.75      ]),
 array([5, 5, 5, 5])) # 結果は見やすいよう整形しています

 ここでは返り値はtupleになっています。このtupleの中身は、先頭から順に適合率(precision)、再現率(recall)、F1値(fscore)、クラスごとのデータの件数(support)を各分類クラスごとに計算して格納したnumpy配列となっています。つまり「クラスごとの評価指標」がこれで得られたことになります。

 この関数の重要な引数としてaverageがあります。これを指定することで、理論の解説編で説明したマクロ平均、重み付き平均、マイクロ平均を計算できます*9

>>> precision_recall_fscore_support(true, pred, average="macro")
(0.7041666666666666, 0.6000000000000001, 0.6124999999999999, None)
>>> precision_recall_fscore_support(true, pred, average="weighted")
(0.7041666666666666, 0.6, 0.6124999999999999, None)
>>> precision_recall_fscore_support(true, pred, average="micro")
(0.6, 0.6, 0.6, None)

 つまり何も悩まなくても結果は出してくれるということで、非常に心強いです。ただし、内部でどんなことをしているのかは使う人間がしっかり把握する必要があります。何もソースを読め等と言うつもりはありませんが、解説編で書いた程度の内容は理解して使うべきものです。

まとめ

 軽い気持ちで書き始めた記事ですが、ちゃんと説明しようとすると意外と内容が膨れ上がってしまい、長い記事になってしまいました。評価指標はそれだけ濃いテーマだ、ということを実感しました。

 結果の評価というのは非常に大切な部分で、この数字(評価指標)を良くすることを目標に頑張るシチュエーションというのはとても多いです。なので、その基本的な仕組みはしっかりと理解しておくべきでしょう。

 なお、間違いの指摘や質問などは大歓迎です。遠慮なくコメントして頂けると幸いです。

*1:F値と表記されることも多いですが、この記事では敢えてF1値で通します

*2:ところで、PositiveとNegativeというのは、日本語に訳すと陽性、陰性になります。インフルエンザ検査のときのあの陽性、陰性です。

*3:改善前と改善後で統計的有意差があることを検定で示せば良い。ということは、当然データを入れ替えたりして複数の試行を行う必要がある。余談だが、この改善がせいぜい数%の差しかなかったりすると検定のために何十回も試行してデータを取る必要があり、処理が重かったりするとけっこう大変。私が文書分類のテーマで卒論を書いたときは二日二晩パソコンをぶん回したが、それでも軽い方という業界もあるくらいだと思う(ディープラーニングで交差検証を10回やるのに二週間かかるとか・・・そういう業界はどうやってるんだろう?)

*4:最悪比較対象にする数字で評価方法が一貫していればなんとかなる。更にクラス数に極端な偏りがなければどの方法で計算しても問題は起きづらい

*5:有利な指標の計算方法を恣意的に選んでないか? とか問題になる可能性がある

*6:書く場合は、評価指標だけ示すのではなく混同行列も載せるようにする。読むときは混同行列もじっくり見て、著者の導いている結論が妥当かどうかを確認する

*7:日本語ではマイクロ平均と読み、ミクロ平均と呼ばれることはありません。ドイツ語読みだとミクロで学術用語ではこちらを使うことが多い(日本の学問の多くは明治期以降にドイツから入ってきた)気がするんですが、統計とかデータサイエンスは英語圏から入ってきたので、英語読みが定着してるのか・・・

*8:数式で確認すれば良いんだろうけどやってない。もし間違ってたらごめんなさい

*9:なお、precision_scoreなどの関数も同様にaverageを指定して使うことができます。まあ、普通はprecision_recall_fscore_supportを使えば事足りるでしょう。