はじめに
7月はナイスなstapyです。みんな来てね! nikkieです。
機械学習ではおなじみのライブラリscikit-learn。
親の顔を見た回数よりインストールしているフシがあります。
そんなscikit-learnのf1_score関数で見かけることのあるwarningについて、最近理解したことをまとめます。
目次
- はじめに
- 目次
- まとめ
- これまでのnikkieとf1_score
- 動作環境
- f1_scoreを使っていて見かけるUndefinedMetricWarning
- なぜUndefinedMetricWarningが送出される?
- f1_scoreのzero_division引数
- (参考)未定義ではなく、F1スコアが0となるとき
- 終わりに
まとめ
- サンプルがtrue negative(TN)のみのとき、F1スコアは未定義となる
- このとき
sklearn.metrics.f1_score
はUndefinedMetricWarning
を送出 - 未定義のF1スコアは0.0として扱われるが、
zero_division
引数を使って1.0やnp.nan
として扱うように調整できる
これまでのnikkieとf1_score
scikit-learnのドキュメントはよく見ます。
最初はわからない箇所が多すぎましたが、繰り返しあたる中で使いこなしtipsが拾えるようになってきました。
これまでにはaverage引数について理解を深めましたね(これが分かって楽しくなってきた!)
動作環境
Python 3.11.4
pip install scikit-learn
したところ、以下が入りました。
joblib==1.3.1 numpy==1.25.1 scikit-learn==1.3.0 scipy==1.11.1 threadpoolctl==3.1.0
f1_scoreを使っていて見かけるUndefinedMetricWarning
以下のことです。
/.../venv/lib/python3.11/site-packages/sklearn/metrics/_classification.py:1757: UndefinedMetricWarning: F-score is ill-defined and being set to 0.0 due to no true nor predicted samples. Use `zero_division` parameter to control this behavior. _warn_prf(average, "true nor predicted", "F-score is", len(true_sum))
意訳
true samplesもpredicted samplesもないため、F-scoreが不明確であり、0.0に設定されました。
この振る舞いを制御するにはzero_division引数を使ってください。
- true samples:正解ラベルが正例のサンプル
- predicted samples:機械学習モデルが正例と推論したサンプル
このwarningを発生させるコード例がf1_scoreのドキュメントにあります。
>>> from sklearn.metrics import f1_score >>> y_true_empty = [0, 0, 0, 0, 0, 0] >>> y_pred_empty = [0, 0, 0, 0, 0, 0] >>> f1_score(y_true_empty, y_pred_empty) # UndefinedMetricWarning 0.0
なぜUndefinedMetricWarningが送出される?
f1_scoreのドキュメントのNotesを見ます。
When true positive + false positive == 0, precision is undefined. When true positive + false negative == 0, recall is undefined. In such cases, by default the metric will be set to 0, as will f-score, and UndefinedMetricWarning will be raised.
- TP(true positive)とFP(false positive)の和が0に等しいとき、precisionは未定義となる
- true positive:モデルが正例と推論し、正解したサンプルの数
- false positive:モデルが正例と推論し、間違えたサンプルの数
- precisionはモデルが正例と推論したサンプルに興味がある指標
- 定義は TP / (TP + FP)
- 正例という推論が1件もないならば定義できない(分母が0)
- TP(true positive)とFN(false negative)の和が0に等しいとき、recallは未定義となる
- false negative:モデルが負例と推論し、間違えた(つまり、実際は正例だった)サンプルの数
- recallは正解ラベルが正例のサンプルに興味がある指標
- 定義は TP / (TP + FN)
- 正解ラベル正例が1件もないならば定義できない(分母が0)
- そのような場合(=precisionもrecallも未定義の場合)、デフォルトではその指標は0に設定される
- f-scoreも0に設定される
- UndefinedMetricWarningが送出される
F1スコアの定義は以下です。
F1 = 2 * (precision * recall) / (precision + recall)
precisionもrecallも(未定義で)0と扱われるとき、分母が0なのでF1スコアも未定義となります。
scikit-learnのデフォルトでは(warningを送出しつつ)0と扱われます。
先のサンプルコードでも示されていますね。
precisionもrecallも未定義のとき、これはTPもFPもFNも全部0のときということです。
すなわち、混同行列1はTNだけ。
>>> from sklearn.metrics import confusion_matrix >>> confusion_matrix(y_true_empty, y_pred_empty) array([[6]])
2x2で書いてみるとこうですね(2値分類と仮定しています)。
array([[6, 0], [0, 0]])
f1_scoreのzero_division引数
サンプルがTNだけであればF1スコアが0という振る舞いは(warningで案内されるように)カスタマイズできます。
それに使うのがzero_division
引数。
ドキュメントを参照すると、この引数が取りうる値は
- 文字列の
"warn"
(デフォルト値) - 0.0(0)
- 1.0(1)
np.nan
(scikit-learn 1.3.0から)
です。
- デフォルトの
"warn"
または0.0を指定したとき、未定義のF1スコアは0となります - 1.0を指定したとき、未定義のF1スコアは1.0となります
np.nan
を指定したとき、未定義のF1スコアはnp.nan
となります2
>>> f1_score(y_true_empty, y_pred_empty, zero_division=0.0) 0.0 >>> f1_score(y_true_empty, y_pred_empty, zero_division=0) 0.0 >>> f1_score(y_true_empty, y_pred_empty, zero_division=1.0) 1.0 >>> f1_score(y_true_empty, y_pred_empty, zero_division=1) 1.0 >>> import numpy as np >>> f1_score(y_true_empty, y_pred_empty, zero_division=np.nan) nan
F1スコアが未定義になるとき、デフォルトでは0.0として扱い、UndefinedMetricWarning
を送出します。
未定義の時のF1スコアを1.0やnp.nan
にするようにzero_division
引数で指定できるわけですね。
(参考)未定義ではなく、F1スコアが0となるとき
>>> confusion_matrix([1, 0, 0], [0, 1, 0]) array([[1, 1], [1, 0]]) >>> f1_score([1, 0, 0], [0, 1, 0]) 0.0
- precisionは 0 / (0+1) = 0
- recallは 0 / (0+1) = 0
- TNだけではなく、FPやFNがあるケース(TPのみ0件)
- precisionもrecallも未定義ではなく0
- このときF1スコアも(未定義ではなく)0となります
デフォルト値のzero_division="warn"
はTNだけのときに、F1スコアを0にします。
これはF1スコアだけを見るとprecisionもrecallも0という見当違いな場合と区別できません。
正解ラベルが負例だけで、モデルの予測も負例だけならば、間違えてはいないわけですよね。
このケースを見当違いなF1スコアと同じとする扱いに違和感があるのなら、zero_division=1.0
と指定するというのが現時点の私の理解です。
終わりに
scikit-learnのf1_score
のUndefinedMetricWarning
とzero_division引数について見てきました。
- TNのみのとき、F1スコアは未定義
- デフォルトでは
UndefinedMetricWarning
を送出し、0.0として扱う zero_division
引数で未定義のF1スコアの値を指定できる(1.0またはnp.nan
)- 見当違いな推論結果でF1スコアが(未定義ではなく)0となるときと区別できる
これまでは見かけるたびに「ちょっと何言ってるかわからない」ものでしたが、腰を据えて理解を深めたことで読み解けるようになった感覚です。
scikit-learnはaverage
引数もそうでしたが、1つの引数の裏に多くの知識が潜んでいることが多く、習熟の道は長いですが知らなかったことを知るのは楽しいとも感じます。
- 自分用混同行列チートシート ↩
- https://scikit-learn.org/stable/whats_new/v1.3.html#sklearn-metrics averageをとるときに効いてきそうですね↩