linear_model.SGDClassifier.decision_function()

2025-05-26

scikit-learnlinear_model.SGDClassifier.decision_function() は、確率的勾配降下法 (SGD) を用いた線形分類器が、入力された各サンプルに対して、分類の確信度スコアまたは決定関数値を返すメソッドです。

SGDClassifier は、線形分類器(例えば、線形SVMやロジスティック回帰など)をSGDという最適化手法を使って訓練します。このモデルは、データ空間内に決定境界(超平面)を学習します。この決定境界は、異なるクラスのサンプルを分離する線(2次元の場合)や平面(3次元の場合)です。

decision_function(X) メソッドは、入力データ X の各サンプルが、この学習された決定境界からどれだけ離れているかを示す「符号付き距離」を返します。

  • 値の解釈
    • 正の大きな値
      サンプルが、あるクラス(多くの場合、ポジティブクラス)に非常に高い確信度で属していることを示します。決定境界からそのクラス側に大きく離れています。
    • 負の大きな値
      サンプルが、別のクラス(多くの場合、ネガティブクラス)に非常に高い確信度で属していることを示します。決定境界からそのクラス側に大きく離れています。
    • 0に近い値
      サンプルが決定境界の近くにあることを示します。これは、モデルがどちらのクラスに属するかを判断するのに「迷っている」状態であり、確信度が低いことを意味します。

具体的な計算式

線形モデルの場合、decision_function() が返す値は基本的に以下の線形結合で計算されます。

score=w⋅x+b

ここで:

  • ⋅ はドット積(内積)
  • b はモデルの切片clf.intercept_ 属性で取得できる)
  • x は入力サンプル(特徴量ベクトル)
  • w はモデルの係数ベクトルclf.coef_ 属性で取得できる)

このスコアは、決定境界からの符号付き距離を表しています。

predict() との違い

  • decision_function(X)
    これは、最終的なクラスラベルではなく、生の確信度スコアを返します。このスコアの絶対値が大きいほど、モデルはその予測に自信があることを示します。
  • predict(X)
    これは、decision_function() の出力に基づいて、最終的なクラスラベル(例:0または1)を返します。通常、decision_function() の値が正であればクラス1、負であればクラス0のように分類します。

マルチクラス分類の場合

SGDClassifier がマルチクラス分類(3つ以上のクラスを分類する場合)を行う場合、通常は「One-vs-Rest (OvR)」戦略が採用されます。これは、各クラスに対して他のすべてのクラスと区別する二項分類器を学習する方式です。

この場合、decision_function(X) は、各サンプルに対して、各クラスに対応するスコアの配列(shape: (n_samples, n_classes)) を返します。最も高いスコアを持つクラスが、そのサンプルの予測クラスとなります。

from sklearn.linear_model import SGDClassifier
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split

# サンプルデータの生成
X, y = make_classification(n_samples=100, n_features=2, n_redundant=0, n_informative=2,
                           n_clusters_per_class=1, random_state=42)

# データを訓練セットとテストセットに分割
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)

# SGDClassifierのインスタンス化と訓練
# loss="log_loss" はロジスティック回帰に相当します
clf = SGDClassifier(loss="log_loss", random_state=42)
clf.fit(X_train, y_train)

# テストデータでdecision_functionを実行
decision_scores = clf.decision_function(X_test)

print("テストデータの最初の5つのサンプルの決定関数値:")
print(decision_scores[:5])

# 予測結果も確認
predictions = clf.predict(X_test)
print("\nテストデータの最初の5つのサンプルの予測クラス:")
print(predictions[:5])

# 決定関数値と予測クラスの関係を見てみる
# 例えば、decision_scoresが正のとき、predictionsは1になっているはず
for i in range(5):
    print(f"サンプル {i}: decision_function={decision_scores[i]:.2f}, predict={predictions[i]}")



モデルが訓練されていない (NotFittedError)

エラー
NotFittedError: This SGDClassifier instance is not fitted yet. Call 'fit' with appropriate arguments before using this estimator.

原因
decision_function() は、モデルが訓練された後にのみ使用できます。つまり、fit() メソッドを呼び出してモデルをデータに適合させていない状態で decision_function() を呼び出すとこのエラーが発生します。

トラブルシューティング
decision_function() を呼び出す前に、必ず clf.fit(X_train, y_train) のようにモデルを訓練してください。

from sklearn.linear_model import SGDClassifier
import numpy as np

clf = SGDClassifier(random_state=42)

# エラーの例: 訓練せずにdecision_functionを呼び出す
# try:
#     clf.decision_function(np.array([[1, 2]]))
# except Exception as e:
#     print(f"エラー: {e}")

# 解決策: モデルを訓練する
X = np.array([[0, 0], [1, 1], [2, 2], [3, 3]])
y = np.array([0, 0, 1, 1])
clf.fit(X, y)

# 訓練後であれば、decision_functionを使用できる
scores = clf.decision_function(np.array([[0.5, 0.5]]))
print(f"決定関数値: {scores}")

入力データの形状が正しくない (ValueError: Expected 2D array, got 1D array instead)

エラー
ValueError: Expected 2D array, got 1D array instead: ... または、特徴量の数がモデル訓練時の数と異なる場合。

原因
decision_function() は、複数のサンプル(行)と複数の特徴量(列)を持つ2次元配列(またはそれに類似する構造)を期待します。単一のサンプルを渡す場合でも、その形状を (1, n_features) のように2次元にする必要があります。また、訓練時と予測時で特徴量の数が一致している必要があります。

トラブルシューティング

  • 特徴量の不一致
    モデルを訓練した時の特徴量の数と、decision_function() に渡すデータの特徴量の数が一致していることを確認します。データの整形や特徴量エンジニアリングのステップを見直してください。
  • 単一サンプル
    np.array([sample_data]) または sample_data.reshape(1, -1) のようにして、単一のサンプルを2次元配列に変換します。
from sklearn.linear_model import SGDClassifier
import numpy as np

X = np.array([[0, 0], [1, 1], [2, 2], [3, 3]])
y = np.array([0, 0, 1, 1])
clf = SGDClassifier(random_state=42)
clf.fit(X, y)

# エラーの例: 1次元配列を渡す
# try:
#     clf.decision_function(np.array([0.5, 0.5])) # 1次元配列
# except Exception as e:
#     print(f"エラー: {e}")

# 解決策: 2次元配列にする
scores = clf.decision_function(np.array([[0.5, 0.5]])) # 2次元配列 (1サンプル, 2特徴量)
print(f"単一サンプルの決定関数値: {scores}")

# 特徴量数の不一致の例 (存在しない特徴量を渡す)
# try:
#     clf.decision_function(np.array([[0.5, 0.5, 0.5]])) # 3特徴量
# except Exception as e:
#     print(f"エラー: {e}")

loss パラメータと decision_function() の出力の解釈

問題
decision_function() の出力が、期待する確率値ではない、または解釈が難しい。

原因とトラブルシューティング
SGDClassifier は、loss パラメータによって様々な線形分類器を実装できます。decision_function() の出力の解釈は、この loss の種類に依存します。

  • loss='modified_huber'
    modified_huber 損失関数は、SVMとロジスティック回帰の利点を組み合わせ、外れ値に頑健で、確率推定も可能です。decision_function() はやはりスコアを返しますが、predict_proba() も利用できます。

  • loss='log_loss' (ロジスティック回帰)
    decision_function() は、ロジスティックシグモイド関数に渡される前の**対数オッズ(log-odds)**を返します。この値は log(P(y=1∣x)/P(y=0∣x)) に相当します。正の値はポジティブクラスの確率が高いことを示し、負の値はネガティブクラスの確率が高いことを示します。

    • トラブルシューティング
      確率値が欲しい場合は、predict_proba() を使用してください。decision_function() の出力をシグモイド関数に通すことで、確率値を得ることができます。
      from scipy.special import expit # シグモイド関数
      
      # clf.loss が 'log_loss' の場合
      scores = clf.decision_function(X_test)
      probabilities = expit(scores) # P(y=1|x) の確率値に変換
      
  • loss='hinge' (デフォルト)
    線形SVMの損失関数であり、decision_function()決定境界からの符号付き距離を返します。この値は確率として解釈できません。正の値はポジティブクラス、負の値はネガティブクラスを示します。絶対値が大きいほど確信度が高いです。

    • トラブルシューティング
      確率値が必要な場合は、predict_proba() メソッドを使用できる損失関数(例: 'log_loss', 'modified_huber')を使用するか、CalibratedClassifierCV を使って確率をキャリブレーションすることを検討してください。

確認事項

  • 必要に応じて、predict_proba() が利用可能かどうかを確認し、適切な損失関数を選択してください。
  • SGDClassifierloss パラメータが何に設定されているかを確認してください。

マルチクラス分類における decision_function() の出力形状

問題
マルチクラス分類(2つ以上のクラス)の場合、decision_function() の出力形状が期待と異なる。

原因とトラブルシューティング
SGDClassifier は、マルチクラス分類にデフォルトでOne-vs-Rest (OvR) 戦略を使用します。この場合、decision_function(X)(n_samples, n_classes) の形状の配列を返します。各列は、特定のクラスに対する分類器のスコアを表します。

  • トラブルシューティング
    • 出力が2次元であることを確認してください。
    • 特定のクラスのスコアにアクセスしたい場合は、インデックスを使って scores[:, class_index] のようにアクセスします。
  • 出力の解釈
    各サンプルについて、最も高いスコアを持つクラスが予測クラスとなります。
from sklearn.linear_model import SGDClassifier
from sklearn.datasets import make_classification
import numpy as np

# 3クラス分類のデータ生成
X, y = make_classification(n_samples=100, n_features=2, n_classes=3,
                           n_informative=2, n_redundant=0, random_state=42)

clf = SGDClassifier(loss='log_loss', random_state=42)
clf.fit(X, y)

test_sample = np.array([[0.5, 0.5]])
scores = clf.decision_function(test_sample)

print(f"マルチクラス分類の決定関数値の形状: {scores.shape}") # (1, 3) となるはず
print(f"決定関数値: {scores}")

# 最も高いスコアを持つクラスが予測クラス
predicted_class = np.argmax(scores)
print(f"予測クラス (decision_functionに基づく): {predicted_class}")
print(f"predict() メソッドによる予測クラス: {clf.predict(test_sample)[0]}")

データのスケーリングの重要性

問題
decision_function() の値が非常に大きい、または小さい、あるいはモデルの性能が悪い。

原因とトラブルシューティング
SGDClassifier は勾配降下法を使用するため、特徴量のスケーリングが非常に重要です。特徴量のスケールが大きく異なる場合、最適化プロセスが不安定になったり、収束が遅くなったり、期待通りの決定境界を学習できなかったりする可能性があります。これにより、decision_function() の出力値のスケールも不適切になることがあります。

トラブルシューティング
モデルを訓練する前に、StandardScalerMinMaxScaler などを使用して、特徴量をスケーリングすることを強く推奨します。

from sklearn.linear_model import SGDClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import make_pipeline
import numpy as np

X = np.array([[0, 0], [1, 100], [2, 200], [3, 300]])
y = np.array([0, 0, 1, 1])

# スケーリングなしのモデル
clf_no_scale = SGDClassifier(random_state=42)
clf_no_scale.fit(X, y)
print(f"スケーリングなしの決定関数値: {clf_no_scale.decision_function(np.array([[0.5, 50]]))}")

# スケーリングありのモデル (パイプラインを使用)
clf_scaled = make_pipeline(StandardScaler(), SGDClassifier(random_state=42))
clf_scaled.fit(X, y)
print(f"スケーリングありの決定関数値: {clf_scaled.decision_function(np.array([[0.5, 50]]))}")
# スケーリングありの方が、より妥当な決定関数値を示すことが多い

問題
スパースデータ(多くの値が0であるデータ)を使用している場合、decision_function() の呼び出しが遅い。

原因とトラブルシューティング
SGDClassifier はスパースデータに効率的に対応していますが、内部的な計算によっては、特に大規模なスパースデータに対してdecision_function()が遅くなる場合があります。これは、SciPyのスパース行列操作のオーバーヘッドや、メモリレイアウトの最適化に関する問題に起因することがあります。

トラブルシューティング

  • モデルの簡素化
    モデルが複雑すぎる場合(例えば、非常に多数の特徴量がある場合)、特徴量選択や次元削減を検討してください。
  • n_jobs の調整
    SGDClassifiern_jobs パラメータは、特にマルチクラス分類におけるOne-vs-Rest戦略で並列計算を有効にするために使用できます。しかし、常にパフォーマンスを向上させるとは限りません。試してみて、改善が見られるか確認してください。
  • データ形式の確認
    データを scipy.sparse 形式(例: csr_matrix)で保持していることを確認します。SGDClassifier は、高密度のNumPy配列よりもスパース行列の方が効率的に処理できる場合があります。

SGDClassifier.decision_function() の利用で問題が発生した場合、以下の点を順に確認することが重要です。

  1. モデルが訓練されているか? (fit() を呼び出したか)
  2. 入力データの形状は正しいか? ((n_samples, n_features) の2次元配列になっているか)
  3. loss パラメータは何か? (decision_function() の出力の解釈は loss によって異なる)
  4. マルチクラス分類の場合の出力形状は理解しているか?
  5. 特徴量は適切にスケーリングされているか?


以下に、いくつかの使用例と、その結果の解釈について解説します。

例1: 2クラス分類における decision_function() の基本

この例では、2つの明確に分離されたクラスを持つ簡単なデータセットを作成し、SGDClassifier を使用して分類を行います。decision_function() の出力がどのように解釈されるかを見てみましょう。

import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import SGDClassifier
from sklearn.datasets import make_blobs # データを生成するための関数
from sklearn.preprocessing import StandardScaler # 特徴量スケーリングのため

# 1. データ生成
# 2つのクラスに属する2次元データを生成
X, y = make_blobs(n_samples=100, centers=2, random_state=0, cluster_std=0.8)

# SGDClassifierは特徴量のスケーリングに敏感なため、StandardScalerを使用
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# 2. SGDClassifierのインスタンス化と訓練
# loss='hinge' は線形SVMに相当(デフォルト)
clf = SGDClassifier(loss='hinge', random_state=42)
clf.fit(X_scaled, y)

# 3. decision_function() の使用
# テストデータ(ここでは訓練データと同じものを利用)に対する決定関数値を取得
scores = clf.decision_function(X_scaled)

# 4. 結果の表示と解釈
print("最初の5つのサンプルの決定関数値と実際のクラス:")
for i in range(5):
    print(f"サンプル {i}: スコア={scores[i]:.2f}, 実際のクラス={y[i]}")

# 予測クラスも確認
predictions = clf.predict(X_scaled)
print("\n最初の5つのサンプルの予測クラス:")
print(predictions[:5])

# スコアと予測クラスの関係をプロット
plt.figure(figsize=(10, 6))
plt.scatter(X_scaled[:, 0], X_scaled[:, 1], c=y, cmap='viridis', edgecolors='k', s=80)
ax = plt.gca()
xlim = ax.get_xlim()
ylim = ax.get_ylim()

# 決定境界の描画
# w[0]*x + w[1]*y + intercept = 0 を解く
# y = -(w[0]/w[1])*x - (intercept/w[1])
w = clf.coef_[0]
b = clf.intercept_[0]
xx = np.linspace(xlim[0], xlim[1], 30)
yy = (-w[0] / w[1]) * xx - (b / w[1])
plt.plot(xx, yy, 'k--', label='決定境界 (score=0)')

# スコアの等高線を描画
Z = clf.decision_function(np.c_[xx.ravel(), yy.ravel()]).reshape(xx.shape)
plt.contour(xx, yy, Z, colors='k', levels=[-1, 0, 1], alpha=0.5,
            linestyles=['--', '-', '--'])
plt.scatter(X_scaled[:, 0], X_scaled[:, 1], c=scores, cmap='RdBu', edgecolors='k', s=100, alpha=0.7)
plt.colorbar(label='decision_function() Score')

plt.title('SGDClassifierの決定境界とdecision_function()の値')
plt.xlabel('特徴量1 (スケーリング済み)')
plt.ylabel('特徴量2 (スケーリング済み)')
plt.legend()
plt.grid(True)
plt.show()

# 解釈:
# - loss='hinge'の場合、decision_function()は決定境界からの符号付き距離を表します。
# - 決定関数値が正の値は、通常、ポジティブクラス(この場合はクラス1)に属すると予測される確信度が高いことを示します。
# - 決定関数値が負の値は、通常、ネガティブクラス(この場合はクラス0)に属すると予測される確信度が高いことを示します。
# - 決定関数値が0に近いサンプルは、決定境界の近くにあり、モデルの確信度が低いことを意味します。
# - 絶対値が大きいほど、モデルはその予測に強い確信を持っています。

出力と解釈のポイント

  • predict() は、このスコアが0より大きいか小さいか(バイナリ分類の場合)に基づいて、最終的なクラスラベル(0または1)を返します。
  • 例えば、-0.58 は決定境界からクラス0の方向に0.58離れていることを意味し、1.23 は決定境界からクラス1の方向に1.23離れていることを意味します。
  • scores は、各サンプルが決定境界からどれだけ離れているかを示す数値です。

例2: loss='log_loss' (ロジスティック回帰) と確率の変換

loss='log_loss' を指定すると、SGDClassifier はロジスティック回帰のように振る舞います。この場合、decision_function() の出力は対数オッズ(log-odds)であり、scipy.special.expit(シグモイド関数)を使って確率に変換できます。

import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import SGDClassifier
from sklearn.datasets import make_classification
from sklearn.preprocessing import StandardScaler
from scipy.special import expit # シグモイド関数

# 1. データ生成
X, y = make_classification(n_samples=100, n_features=2, n_informative=2,
                           n_redundant=0, n_clusters_per_class=1, random_state=42)

scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# 2. SGDClassifierのインスタンス化と訓練 (loss='log_loss'を指定)
clf_logloss = SGDClassifier(loss='log_loss', random_state=42)
clf_logloss.fit(X_scaled, y)

# 3. decision_function() の使用
scores_logloss = clf_logloss.decision_function(X_scaled)

# 4. decision_function() の出力を確率に変換
# ロジスティック回帰の場合、decision_function()の出力は対数オッズなので、
# シグモイド関数を適用することで確率に変換できる
probabilities = expit(scores_logloss) # P(y=1|x) の確率

# 5. 結果の表示と比較
print("ロジスティック回帰の場合の最初の5つのサンプルのスコア、確率、予測、実際のクラス:")
for i in range(5):
    predicted_class = 1 if probabilities[i] >= 0.5 else 0
    print(f"サンプル {i}: スコア={scores_logloss[i]:.2f}, 確率={probabilities[i]:.2f}, 予測={predicted_class}, 実際={y[i]}")

# predict_proba() メソッドも利用可能
# predict_proba() はクラスごとの確率を直接返します
if hasattr(clf_logloss, 'predict_proba'):
    proba_method_output = clf_logloss.predict_proba(X_scaled)
    print("\npredict_proba() メソッドの出力(最初の5サンプル、クラス0とクラス1の確率):")
    print(proba_method_output[:5])

# 確率と決定関数値の関係を視覚化 (クラス1の確率)
plt.figure(figsize=(10, 6))
plt.scatter(X_scaled[:, 0], X_scaled[:, 1], c=probabilities, cmap='coolwarm', edgecolors='k', s=80)
plt.colorbar(label='P(Class=1|x)')
plt.title('SGDClassifier (log_loss) の決定関数値と確率')
plt.xlabel('特徴量1 (スケーリング済み)')
plt.ylabel('特徴量2 (スケーリング済み)')
plt.grid(True)
plt.show()

# 解釈:
# - scoreが0であれば、確率は0.5になります(expit(0) = 0.5)。
# - scoreが大きくなると確率は1に近づき、小さくなると確率は0に近づきます。
# - predict_proba() はこの計算を内部で行って、より使いやすい形で確率を返してくれます。

SGDClassifier は、デフォルトでOne-vs-Rest (OvR) 戦略を使用してマルチクラス分類を処理します。この場合、decision_function() は各サンプルに対して、各クラスの分類器が返すスコアの配列を返します。

import numpy as np
from sklearn.linear_model import SGDClassifier
from sklearn.datasets import make_classification
from sklearn.preprocessing import StandardScaler

# 1. データ生成 (3クラス分類)
X, y = make_classification(n_samples=100, n_features=2, n_informative=2,
                           n_redundant=0, n_clusters_per_class=1, n_classes=3, random_state=42)

scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# 2. SGDClassifierのインスタンス化と訓練
# マルチクラス分類の場合、loss='log_loss' や 'modified_huber' が確率出力に役立つ
clf_multiclass = SGDClassifier(loss='log_loss', random_state=42)
clf_multiclass.fit(X_scaled, y)

# 3. decision_function() の使用
# 各サンプルに対して、クラス数分のスコアが返される
scores_multiclass = clf_multiclass.decision_function(X_scaled[:5]) # 最初の5サンプルを見る

# 4. 結果の表示と解釈
print("最初の5つのサンプルの決定関数値(マルチクラス):")
print(scores_multiclass)
print(f"形状: {scores_multiclass.shape}") # (n_samples, n_classes) となる

print("\n最初の5つのサンプルの予測クラスと実際のクラス:")
predictions_multiclass = clf_multiclass.predict(X_scaled[:5])
for i in range(5):
    # 各サンプルのスコア配列の中から最大値のインデックスが予測クラス
    predicted_from_scores = np.argmax(scores_multiclass[i])
    print(f"サンプル {i}: スコア={scores_multiclass[i].round(2)}, "
          f"予測クラス (decision_functionから)={predicted_from_scores}, "
          f"予測クラス (predictから)={predictions_multiclass[i]}, "
          f"実際={y[i]}")

# 解釈:
# - 各行は1つのサンプルに対応し、各列は各クラス(クラス0, クラス1, クラス2)に対するスコアです。
# - `np.argmax()` を使って行ごとに最大値のインデックスを見つけると、それが `predict()` が返す予測クラスと一致します。
# - スコアが高いほど、そのクラスに属する確信度が高いことを示します。

これらの例からわかるように、linear_model.SGDClassifier.decision_function() は、モデルの予測が単なるクラスラベルだけでなく、その裏にある数値的な確信度を理解するために非常に重要です。

  • predict_proba() とは異なり、decision_function() の出力は必ずしも確率にスケールされているわけではないため、解釈には注意が必要です。特に loss='hinge' の場合は、0から1の間の確率に変換する直接的な方法は提供されません。
  • マルチクラス分類の場合
    各クラスに対する個別のスコアの配列を返します。
  • バイナリ分類の場合
    スコアは決定境界からの符号付き距離(loss='hinge')または対数オッズ(loss='log_loss')を表します。