はじめに
アカネチャン...(推しの子6話見た)、nikkieです😭😭😭
機械学習モデルの評価指標の1つ F1 score について、scikit-learnの実装を使いこなすために調べたことをまとめます。
目次
- はじめに
- 目次
- scikit-learnのf1_scoreのaverage引数
- おことわり事項
- MultiLabelBinarizerでラベルを処理
- average="micro"
- average="macro"
- average="samples"
- 終わりに
scikit-learnのf1_score
のaverage引数
問題設定としてはマルチラベル分類です。
今回は3クラスとしましょう。
クラスのラベルは1,2,3とします。
正解が分かっているサンプルが5例あるとします。
>>> true_labels = [[1,2], [1], [1,2,3], [2,3], [3]]
評価対象の機械学習モデルは以下のように推論したとします。
>>> pred_labels = [[1,3], [2], [1,3], [3], [3]]
true_labels
とpred_labels
からF1 scoreを求めてモデルを性能評価したいです。
マルチラベルでは、F1 scoreの算出方法が複数通りあります。
モデルの性能評価をするために1つの値を求めるわけですが、この求め方、もっというと平均の取り方にいくつかのバリエーションがあるのです1。
おことわり事項
動作環境
- Python 3.10.9
- scikit-learn 1.2.2
- NumPy 1.24.3
参考書籍
『Kaggleで勝つデータ分析の技術』の2.3.5を参照しました。
この書籍のサンプルコードは公開されており、以下の例を自分の言葉で説明したものとなります。
https://github.com/ghmagazine/kagglebook/blob/3d8509d1c1b41a765e3f4744ba1fb226188e2b15/ch02/ch02-01-metrics.py#L90-L128
MultiLabelBinarizer
でラベルを処理
今回は3クラスですから、マルチラベルを3つの0/1の並びで表します。
ラベルとして付与されたクラスが1、付与されなかったクラスが0です。
Multi-hot encodingと呼ぶと理解しています2。
使うのはMultiLabelBinarizer
!
>>> from sklearn.preprocessing import MultiLabelBinarizer >>> mlb = MultiLabelBinarizer() >>> y_true = mlb.fit_transform(true_labels) >>> y_pred = mlb.transform(pred_labels) >>> y_true array([[1, 1, 0], [1, 0, 0], [1, 1, 1], [0, 1, 1], [0, 0, 1]]) >>> y_pred array([[1, 0, 1], [0, 1, 0], [1, 0, 1], [0, 0, 1], [0, 0, 1]])
average="micro"
scikit-learnのドキュメント(Parametersの中のaverage)より(2023/07/21 ドキュメントへのリンクを追加)
Calculate metrics globally by counting the total true positives, false negatives and false positives.
5例3クラスなのでy_true
やy_pred
のshape
は(5, 3)
ですが、これを15個の0/1の並びとして1つのF1 scoreを求める、という理解です。
>>> y_true.reshape(-1) array([1, 1, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1]) >>> y_pred.reshape(-1) array([1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1])
>>> from sklearn.metrics import f1_score >>> f1_score(y_true.reshape(-1), y_pred.reshape(-1)) 0.6250000000000001 >>> f1_score(y_true, y_pred, average="micro") 0.6250000000000001
15個の数字の並びで扱うときの混同行列
>>> from sklearn.metrics import confusion_matrix >>> confusion_matrix(y_true.reshape(-1), y_pred.reshape(-1)) array([[4, 2], [4, 5]])
見方は
# [予測がNの列, 予測がPの列] array([[TN, FP], # 正解がNの行 [FN, TP]]) # 正解がPの行
ですから3、ここからprecisionやrecallを算出すると
>>> precision = cm[1][1] / (cm[1][1] + cm[0][1]) # tp / (tp + fp) >>> precision 0.7142857142857143 >>> recall = cm[1][1] / (cm[1][1] + cm[1][0]) # tp / (tp + fn) >>> recall 0.5555555555555556
よってF1 scoreは
>>> 2 * (precision * recall) / (precision + recall) 0.6250000000000001
一致🙌
つまりf1_score(..., average="micro")
の指定はこれを全自動でやっていたってことですね!
average="macro"
scikit-learnのドキュメント(Parametersの中のaverage)より
Calculate metrics for each label, and find their unweighted mean.
今回は3クラスなので、クラスごとにF1 scoreを求め、3クラス分の平均としてF1 scoreを求めるという理解です。
✍️macro=クラスごとのF1 scoreの平均
>>> class1_f1 = f1_score(y_true[:, 0], y_pred[:, 0]) >>> class2_f1 = f1_score(y_true[:, 1], y_pred[:, 1]) >>> class3_f1 = f1_score(y_true[:, 2], y_pred[:, 2]) >>> class1_f1, class2_f1, class3_f1 (0.8, 0.0, 0.8571428571428571) >>> import numpy as np >>> np.mean([class1_f1, class2_f1, class3_f1]) 0.5523809523809523 >>> f1_score(y_true, y_pred, average="macro") 0.5523809523809523
一致してますね🙌
クラス2については1サンプルも正解していないので、その分平均が低くなっていますね。
ドキュメントには次のように続いています
This does not take label imbalance into account.
意訳 ラベルの不均衡を考慮に入れない
average=None
average
引数はNone
も取れます。
If
None
, the scores for each class are returned.
このとき、各クラスのF1 scoreが返されます。
>>> f1_score(y_true, y_pred, average=None) array([0.8 , 0. , 0.85714286])
上で見たclass1_f1, class2_f1, class3_f1
と同じですね。
(参考)クラスごとの混同行列
>>> confusion_matrix(y_true[:, 0], y_pred[:, 0]) array([[2, 0], [1, 2]]) >>> confusion_matrix(y_true[:, 1], y_pred[:, 1]) array([[1, 1], [3, 0]]) >>> confusion_matrix(y_true[:, 2], y_pred[:, 2]) array([[1, 1], [0, 3]])
- クラス1(インデックスは0)
- precision 1.0 (=2/(2+0))
- recall 0.67 (=2/(2+1))
- 👉 F1 0.8
- クラス2(インデックスは1)
- precision 0.0 (=0/(0+1))
- recall 0.0 (=0/(0+3))
- 👉 F1 0.0
- クラス3(インデックスは2)
- precision 0.75 (=3/(3+1))
- recall 1.0 (=3/(3+0))
- 👉 F1 0.8571428571428571
average="samples"
scikit-learnのドキュメント(Parametersの中のaverage)より
Calculate metrics for each instance, and find their average (only meaningful for multilabel classification where this differs from accuracy_score).
インスタンスごとにF1 scoreを計算し、それを平均するという理解です。
今回は5例なので、5つのF1 scoreの平均ということですね。
カッコ内によると マルチラベル分類でのみaccuracy_scoreと違うので意味がある ようです。
>>> sample1_f1 = f1_score(y_true[0, :], y_pred[0, :]) >>> sample2_f1 = f1_score(y_true[1, :], y_pred[1, :]) >>> sample3_f1 = f1_score(y_true[2, :], y_pred[2, :]) >>> sample4_f1 = f1_score(y_true[3, :], y_pred[3, :]) >>> sample5_f1 = f1_score(y_true[4, :], y_pred[4, :]) >>> sample1_f1, sample2_f1, sample3_f1, sample4_f1, sample5_f1 (0.5, 0.0, 0.8, 0.6666666666666666, 1.0) >>> np.mean([sample1_f1, sample2_f1, sample3_f1, sample4_f1, sample5_f1]) 0.5933333333333334 >>> f1_score(y_true, y_pred, average="samples") 0.5933333333333334
一致🙌
サンプル2で正例を外している分、平均も低くなっていますね。
(参考)サンプルごとの混同行列
>>> confusion_matrix(y_true[0, :], y_pred[0, :]) array([[0, 1], [1, 1]]) >>> confusion_matrix(y_true[1, :], y_pred[1, :]) array([[1, 1], [1, 0]]) >>> confusion_matrix(y_true[2, :], y_pred[2, :]) array([[0, 0], [1, 2]]) >>> confusion_matrix(y_true[3, :], y_pred[3, :]) array([[1, 0], [1, 1]]) >>> confusion_matrix(y_true[4, :], y_pred[4, :]) array([[2, 0], [0, 1]])
- サンプル1
- precision 0.5 (=1/(1+1))
- recall 0.5 (=1/(1+1))
- 👉 F1 0.5
- サンプル2
- precision 0.0 (=0/(0+1))
- recall 0.0 (=0/(0+1))
- 👉 F1 0.0
- サンプル3
- precision 1.0 (=2/(2+0))
- recall 0.67 (=2/(2+1))
- 👉 F1 0.8
- サンプル4
- precision 1.0 (=1/(1+0))
- recall 0.5 (=1/(1+1))
- 👉 F1 0.6666666666666666
- サンプル5
- precision 1.0 (=1/(1+0))
- recall 1.0 (=1/(1+0))
- 👉 F1 1.0
転置してaverage=None
を指定したら、サンプルごとのF1 score!
shapeが(5, 3)
のndarrayどうしなら3つのF1 score(各クラスのF1 score)が算出されるのを見ました。
気づいちゃったんですが、転置してshapeを(3, 5)
にしたら、5つのF1 scoreが算出されます!(=サンプルごとのF1 score)
>>> f1_score(y_true.T, y_pred.T, average=None) array([0.5 , 0. , 0.8 , 0.66666667, 1. ])
終わりに
scikit-learnのf1_score
でaverage
引数に"micro"
, "macro"
, "samples"
を指定したとき、どのようにF1 scoreを平均するかを見てきました。
3クラスのマルチラベル分類で、5サンプルのデータを例にすると
- micro:
y_true
とy_label
をそれぞれreshape(-1)
(1次のndarray)にして、1つのF1 scoreを算出 - macro: クラスごとにF1 scoreを算出して、それを平均
- 今回の例では3つのF1 scoreを平均
average=None
を平均
- samples: サンプルごとにF1 scoreを算出して、それを平均
- 今回の例では5つのF1 scoreを平均
- 転置して
average=None
で求まる値を平均
となります。
この記事で詳しく見ていないものは、average="weighted"
の指定です。
ドキュメントを見たところ、クラスごとのF1 scoreですが、各ラベルの正例の数(support)分の重みを付けて平均するようで、ここがmacroとの違いになるようです。
>>> f1_score(y_true, y_pred, average="macro") 0.5523809523809523 >>> f1_score(y_true, y_pred, average="weighted") 0.5523809523809524
今回の例ではほとんど変わらないですね。
また、各値はどういう目的でaverage
引数に指定するか(例:こういうときにaverage="macro"
を指定する)はモヤッとしているので、そこは『評価指標入門』にあたってみようと思います。