Focal Lossは物体検出(RetinaNetなど)で「背景クラスが圧倒的に多い」という極端なクラス不均衡に対処するために考案された損失関数です。では、CIFAR-10のように10クラスがほぼ均等なデータセットにFocal Lossを使うとどうなるでしょうか? gamma値(γ)を0・1・2・5と変化させながら、全体精度だけでなくクラスごとのrecallまで踏み込んで検証します。
- Focal Lossのgamma値が全体精度に与える影響(CIFAR-10・クラス不均衡なし)
- 「全体精度は変わらないのにクラスごとのrecallは変化する」現象の有無
- alpha=1.0(重み付けなし)でgammaの効果だけを切り出す実験設計
- Focal Lossを不均衡データ以外で使う際の注意点
Focal Lossの数式とgammaの役割
Focal Lossは通常のCross Entropyに「易しいサンプルの損失を減衰させる」項を掛け合わせた形をしています。
\[ FL(p_t) = -\alpha (1-p_t)^{\gamma} \log(p_t) \]
ここで p_t は正解クラスの予測確率です。p_tが大きい(=モデルが自信を持って正解している「易しいサンプル」)ほど (1-p_t)^γ は小さくなり、損失への寄与が抑えられます。γ=0のときは通常のCross Entropyと一致します。
| gamma | 意味 |
|---|---|
| γ = 0 | 通常のCross Entropyと同一(Focal Lossの効果なし) |
| γ = 1 | 易しいサンプルの損失を穏やかに減衰 |
| γ = 2 | 論文(RetinaNet)で推奨されるデフォルト値 |
| γ = 5 | 易しいサンプルをほぼ無視し、難しいサンプルに極端に集中 |
今回は alpha=1.0(クラス重み付けなし)に固定します。Focal Lossのデフォルトであるalpha=0.25は「背景クラスが75%を占める」ような極端な不均衡を前提にしたチューニング値であり、CIFAR-10のような均等データにそのまま持ち込むと逆に不利になる可能性が高いため、今回はgamma単体の効果を切り出す設計にしています。
実験コード
ベースラインは本シリーズ標準構成(Conv2D×2・GAP・Dropout=0.2・Adam lr=0.001・batch_size=64・30エポック)です。
① 環境準備(最初に一度だけ実行)
!apt-get -y install fonts-ipafont-gothic
!rm -rf /root/.cache/matplotlib
!pip install -q japanize_matplotlib
print("環境準備完了")
実行結果をクリックして内容を開く
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following additional packages will be installed:
fonts-ipafont-mincho
The following NEW packages will be installed:
fonts-ipafont-gothic fonts-ipafont-mincho
0 upgraded, 2 newly installed, 0 to remove and 53 not upgraded.
Need to get 8,237 kB of archives.
After this operation, 28.7 MB of additional disk space will be used.
Get:1 http://archive.ubuntu.com/ubuntu jammy/universe amd64 fonts-ipafont-gothic all 00303-21ubuntu1 [3,513 kB]
Get:2 http://archive.ubuntu.com/ubuntu jammy/universe amd64 fonts-ipafont-mincho all 00303-21ubuntu1 [4,724 kB]
Fetched 8,237 kB in 0s (27.9 MB/s)
Selecting previously unselected package fonts-ipafont-gothic.
(Reading database ... 122403 files and directories currently installed.)
Preparing to unpack .../fonts-ipafont-gothic_00303-21ubuntu1_all.deb ...
Unpacking fonts-ipafont-gothic (00303-21ubuntu1) ...
Selecting previously unselected package fonts-ipafont-mincho.
Preparing to unpack .../fonts-ipafont-mincho_00303-21ubuntu1_all.deb ...
Unpacking fonts-ipafont-mincho (00303-21ubuntu1) ...
Setting up fonts-ipafont-mincho (00303-21ubuntu1) ...
update-alternatives: using /usr/share/fonts/opentype/ipafont-mincho/ipam.ttf to provide /usr/share/fonts/truetype/fonts-japanese-mincho.ttf (fonts-japanese-mincho.ttf) in auto mode
Setting up fonts-ipafont-gothic (00303-21ubuntu1) ...
update-alternatives: using /usr/share/fonts/opentype/ipafont-gothic/ipag.ttf to provide /usr/share/fonts/truetype/fonts-japanese-gothic.ttf (fonts-japanese-gothic.ttf) in auto mode
Processing triggers for fontconfig (2.13.1-4.2ubuntu5) ...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.1/4.1 MB 54.1 MB/s eta 0:00:00
Preparing metadata (setup.py) ... done
Building wheel for japanize_matplotlib (setup.py) ... done
環境準備完了
② import・データ準備・Focal Loss実装・モデル構築関数
import numpy as np
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
import matplotlib.pyplot as plt
import japanize_matplotlib
SEED = 42
tf.random.set_seed(SEED)
np.random.seed(SEED)
CLASS_NAMES = ["airplane", "automobile", "bird", "cat", "deer",
"dog", "frog", "horse", "ship", "truck"]
# データ読み込み・正規化
(x_train, y_train), (x_test, y_test) = keras.datasets.cifar10.load_data()
x_train = x_train.astype("float32") / 255.0
x_test = x_test.astype("float32") / 255.0
y_train = y_train.flatten()
y_test = y_test.flatten()
EPOCHS = 30
BATCH_SIZE = 64
def sparse_categorical_focal_loss(gamma=2.0, alpha=1.0):
"""
sparseなラベル(整数)に対応したFocal Loss。
gamma=0のとき、通常のCross Entropyと一致する。
"""
def loss_fn(y_true, y_pred):
y_true = tf.cast(tf.reshape(y_true, [-1]), tf.int32)
y_pred = tf.clip_by_value(y_pred, 1e-7, 1.0 - 1e-7)
y_true_onehot = tf.one_hot(y_true, depth=tf.shape(y_pred)[-1])
p_t = tf.reduce_sum(y_true_onehot * y_pred, axis=-1)
focal_weight = alpha * tf.pow(1.0 - p_t, gamma)
ce = -tf.math.log(p_t)
return tf.reduce_mean(focal_weight * ce)
return loss_fn
def build_model(gamma):
tf.random.set_seed(SEED)
inputs = keras.Input(shape=(32, 32, 3))
x = layers.Conv2D(32, 3, padding="same", activation="relu")(inputs)
x = layers.MaxPooling2D()(x)
x = layers.Conv2D(64, 3, padding="same", activation="relu")(x)
x = layers.MaxPooling2D()(x)
x = layers.GlobalAveragePooling2D()(x)
x = layers.Dropout(0.2)(x)
outputs = layers.Dense(10, activation="softmax")(x)
model = keras.Model(inputs, outputs)
model.compile(
optimizer=keras.optimizers.Adam(learning_rate=0.001),
loss=sparse_categorical_focal_loss(gamma=gamma, alpha=1.0),
metrics=["accuracy"],
)
return model
実行結果をクリックして内容を開く
Downloading data from https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz 170498071/170498071 ━━━━━━━━━━━━━━━━━━━━ 1977s 12us/step
③ gamma=0, 1, 2, 5 で学習実行
gammas = [0.0, 1.0, 2.0, 5.0]
results = []
histories = {}
predictions = {}
for gamma in gammas:
print(f"\n=== gamma={gamma} ===")
model = build_model(gamma)
history = model.fit(
x_train, y_train,
validation_split=0.2,
epochs=EPOCHS,
batch_size=BATCH_SIZE,
verbose=0,
)
score = model.evaluate(x_test, y_test, verbose=0)
y_pred = np.argmax(model.predict(x_test, verbose=0), axis=1)
results.append({
"gamma": gamma,
"test_accuracy": score[1],
"test_loss": score[0],
"val_accuracy_final": history.history["val_accuracy"][-1],
})
histories[gamma] = history
predictions[gamma] = y_pred
print(f"test_accuracy: {score[1]:.4f}")
実行結果をクリックして内容を開く
=== gamma=0.0 === test_accuracy: 0.5755 === gamma=1.0 === test_accuracy: 0.5668 === gamma=2.0 === test_accuracy: 0.5769 === gamma=5.0 === test_accuracy: 0.5679
④ 全体精度の比較とクラスごとのrecall分析
import pandas as pd
from sklearn.metrics import recall_score
df = pd.DataFrame(results).sort_values("test_accuracy", ascending=False)
print("===== gamma別 全体精度 =====")
print(df.to_string(index=False))
# ── クラスごとのrecallを算出 ─────────────────────────
recall_table = {}
for gamma in gammas:
recalls = recall_score(y_test, predictions[gamma], average=None)
recall_table[f"gamma={gamma}"] = recalls
recall_df = pd.DataFrame(recall_table, index=CLASS_NAMES)
print("\n===== クラスごとのrecall =====")
print(recall_df.round(4))
# 特に紛らわしいとされるcat/dogに注目
print("\n===== cat / dog のrecall比較 =====")
print(recall_df.loc[["cat", "dog"]].round(4))
# ── 学習曲線(val_accuracy)の比較 ───────────────────
plt.figure(figsize=(8, 5))
for gamma in gammas:
plt.plot(histories[gamma].history["val_accuracy"], label=f"gamma={gamma}")
plt.xlabel("epoch")
plt.ylabel("val_accuracy")
plt.title("gamma別のval_accuracy推移")
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig("focal_loss_gamma_curves.png", dpi=150)
plt.show()
# ── クラスごとrecallの棒グラフ ─────────────────────
recall_df.plot(kind="bar", figsize=(10, 5))
plt.ylabel("recall")
plt.title("クラスごとのrecall(gamma別)")
plt.xticks(rotation=45)
plt.tight_layout()
plt.savefig("focal_loss_gamma_recall_bar.png", dpi=150)
plt.show()
実行結果をクリックして内容を開く
===== gamma別 全体精度 =====
gamma test_accuracy test_loss val_accuracy_final
2.0 0.5769 0.744106 0.5763
0.0 0.5755 1.208866 0.5724
5.0 0.5679 0.449096 0.5717
1.0 0.5668 0.963021 0.5664
===== クラスごとのrecall =====
gamma=0.0 gamma=1.0 gamma=2.0 gamma=5.0
airplane 0.664 0.620 0.602 0.668
automobile 0.793 0.745 0.768 0.744
bird 0.431 0.422 0.418 0.483
cat 0.239 0.301 0.309 0.332
deer 0.478 0.471 0.479 0.445
dog 0.500 0.437 0.491 0.507
frog 0.666 0.679 0.690 0.585
horse 0.678 0.637 0.675 0.654
ship 0.704 0.710 0.707 0.685
truck 0.602 0.646 0.630 0.576
===== cat / dog のrecall比較 =====
gamma=0.0 gamma=1.0 gamma=2.0 gamma=5.0
cat 0.239 0.301 0.309 0.332
dog 0.500 0.437 0.491 0.507
実験結果
全体精度の比較
| gamma | test_accuracy | test_loss | val_accuracy(最終) |
|---|---|---|---|
| 0(通常のCE相当) | 57.55% | 1.209 | 57.24% |
| 1 | 56.68% | 0.963 | 56.64% |
| 2 | 57.69% | 0.744 | 57.63% |
| 5 | 56.79% | 0.449 | 57.17% |
test_accuracyは56.68〜57.69%の範囲に収まり、gammaを上げても単調な改善・悪化どちらの傾向も見られませんでした(差はわずか1.01pt)。一方test_lossはgammaが大きいほど小さくなっていますが、これは後述する通り数式上の見かけ上の現象で、精度改善を意味しません。
学習曲線(val_accuracy推移)
クラスごとのrecall(特にcat/dog)
| クラス | gamma=0 | gamma=1 | gamma=2 | gamma=5 | gamma5-gamma0 |
|---|---|---|---|---|---|
| cat | 23.9% | 30.1% | 30.9% | 33.2% | +9.3pt |
| dog | 50.0% | 43.7% | 49.1% | 50.7% | +0.7pt |
| frog | 66.6% | 67.9% | 69.0% | 58.5% | −8.1pt |
| automobile | 79.3% | 74.5% | 76.8% | 74.4% | −4.9pt |
10クラス全体で見ると、gammaが大きくなるほどクラス間recallの標準偏差(ばらつき)が単調に縮小しています。
| gamma | 0 | 1 | 2 | 5 |
|---|---|---|---|---|
| クラス間recallの標準偏差 | 15.50pt | 14.02pt | 13.90pt | 12.01pt |
今回の結果でtest_lossがgamma=0の1.209からgamma=5の0.449まで単調に下がっていますが、これは学習が進んだからではありません。Focal Lossの式
(1-p_t)^γ はgammaが大きいほど損失値そのもののスケールを縮小させるため、gammaが異なる設定同士でtest_lossの数値を直接比較することはできません。比較には必ずtest_accuracyやrecallなど、スケールに依存しない指標を使ってください。
考察
① 全体精度への影響
予想通り、全体精度(test_accuracy)はgamma=0の57.55%からgamma=5の56.79%まで、明確な単調傾向のないまま1.01pt以内に収まりました。CIFAR-10は10クラスがほぼ均等(各クラス1,000枚)であり、Focal Lossが本来解決しようとしている「圧倒的なクラス不均衡」が存在しないため、全体精度としての恩恵はほぼ見られないという結果は理論的にも整合的です。
② クラスごとのrecallは変化したか:cat/dogは改善、frog/automobileは悪化
もっとも顕著だったのはcatで、gamma=0の23.9%からgamma=5の33.2%まで+9.3ptと一貫して単調に改善しました。CIFAR-10のcatクラスはdogとの混同が多い「難しいクラス」として知られており、Focal Lossが「予測確信度が低い(p_tが低い)サンプル」の損失を相対的に強調する性質と合致する結果です。一方でfrog(−8.1pt)やautomobile(−4.9pt)のように、gamma=0では高いrecallを持っていた「得意クラス」がgamma=5で明確に悪化しています。これはFocal Lossが「易しいサンプルの損失をほぼ無視する」ことの裏返しで、モデルが得意クラスへの最適化を弱め、その分を苦手クラスに再配分している可能性を示唆します。
③ クラス間recallのばらつきが単調に縮小
10クラスのrecallの標準偏差を計算すると、gamma=0で15.50pt、gamma=1で14.02pt、gamma=2で13.90pt、gamma=5で12.01ptと、gammaを上げるほど単調に縮小していました。全体精度(平均recall)はほぼ変わらないにもかかわらず、クラスごとの得意・不得意の差は着実に均されています。つまりFocal Lossは、クラスの出現頻度が均等なデータであっても、「モデルにとっての難易度」に基づいてクラス間の性能格差を縮小する効果を持つと言えそうです。
④ gamma=5という極端な設定でどうなったか
gamma=5でも学習曲線自体が発散・停滞するような不安定さは見られず、val_accuracyはgamma=0とほぼ同水準(57.17% vs 57.24%)で着地しています。ただしクラスごとの内訳を見ると、frogやautomobileのように明確に悪化するクラスが出てくるため、「gammaを上げるほど無条件に良い」というわけではなく、どのクラスの性能を優先したいかによってトレードオフが生じる点には注意が必要です。
実務での推奨
| 状況 | 推奨 | 理由 |
|---|---|---|
| クラス分布がほぼ均等なデータ(CIFAR-10など) | 全体精度の向上目的では採用しない/クラス間の性能格差を均したい場合はgamma=2〜5を検討 | 全体精度への寄与は誤差範囲(1pt以内)だが、苦手クラスのrecallを底上げしつつ得意クラスを多少犠牲にする効果は確認できたため |
| クラス分布が極端に偏っているデータ | alpha・gammaともに元論文の推奨値(alpha=0.25, gamma=2)から試す | Focal Lossは本来この状況のために設計されているため |
| 「頻度は均等だが混同しやすいクラス」を改善したい場合 | gamma=2〜5を試し、必ずクラスごとのrecallまで確認する | 全体精度だけを見ると効果が見えないため、recall_score(average=None)などクラス別の指標が必須 |
- 全体精度はgamma=0の57.55%〜gamma=5の56.79%で、明確な傾向のないまま1.01pt以内に収まった(CIFAR-10にはFocal Lossが想定する頻度不均衡が存在しないため)
- catのrecallはgamma=0の23.9%からgamma=5の33.2%まで+9.3ptと一貫して改善。一方frog(−8.1pt)やautomobileなどの「得意クラス」はgamma上昇で悪化した
- クラス間recallの標準偏差は15.50pt→14.02pt→13.90pt→12.01ptとgammaに対して単調に縮小しており、全体精度は変わらなくても「クラスごとの得意・不得意の格差」は着実に均されていた
- test_lossはgammaが大きいほど数式上小さくなる(0.449 vs 1.209)ため、gamma同士のtest_loss比較には使えない点に注意
関連記事もあわせてどうぞ:



0 件のコメント:
コメントを投稿