nikkie-ftnextの日記

イベントレポートや読書メモを発信

sklearn.metrics.f1_scoreのUndefinedMetricWarningとzero_division引数

はじめに

7月はナイスなstapyです。みんな来てね! nikkieです。

機械学習ではおなじみのライブラリscikit-learn。
親の顔を見た回数よりインストールしているフシがあります。
そんなscikit-learnのf1_score関数で見かけることのあるwarningについて、最近理解したことをまとめます。

目次

まとめ

  • サンプルがtrue negative(TN)のみのとき、F1スコアは未定義となる
  • このときsklearn.metrics.f1_scoreUndefinedMetricWarningを送出
  • 未定義の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のときということです。
すなわち、混同行列1TNだけ

>>> 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_scoreUndefinedMetricWarningとzero_division引数について見てきました。

  • TNのみのとき、F1スコアは未定義
  • デフォルトではUndefinedMetricWarningを送出し、0.0として扱う
  • zero_division引数で未定義のF1スコアの値を指定できる(1.0またはnp.nan
    • 見当違いな推論結果でF1スコアが(未定義ではなく)0となるときと区別できる

これまでは見かけるたびに「ちょっと何言ってるかわからない」ものでしたが、腰を据えて理解を深めたことで読み解けるようになった感覚です。
scikit-learnはaverage引数もそうでしたが、1つの引数の裏に多くの知識が潜んでいることが多く、習熟の道は長いですが知らなかったことを知るのは楽しいとも感じます。


  1. 自分用混同行列チートシート
  2. https://scikit-learn.org/stable/whats_new/v1.3.html#sklearn-metrics averageをとるときに効いてきそうですね