ハイパーパラメータ調整:ファインチューニング新章(2)

Date:

Share post:

前回の投稿(ResNet50からEfficientNetV2Lへ!ファインチューニングの新章)では、ベースモデルをEfficientNetV2Lに変更するという大きな方針転換に関して解説した上で、直近の課題はハイパーパラメータ(margin, pair_loss_weight, dropout_rate など)の調整であることに触れました。

今回は、その結果に関して書いてみます。

Fβスコア

ハイパーパラメータの調整に際しては、各設定での学習結果であるモデルの精度を評価する指標が必要になります。
前回の投稿においては、この指標としてF1スコアを採用するとの方針を示しましたが、モデルの用途がラベル画像からワインを特定することである点を考えると、適合率の方をより重視すべきではないかとの疑念もありました。

この点に関して、改めてGemini先生に相談したところ、以下のような回答をいただきました。

ラベル画像からワインの商品識別という目的において、適合率(Precision)を重視するという考え方は、非常に合理的かつ適切です。

これは、システムが提供する「信頼性」と、ユーザーの「許容されるリスク」を考慮した、実用的な判断と言えます。

再現率が低い(FNが多い)場合、「どのワインとも識別できませんでした」という結果になります。これはユーザーが望む結果ではありませんが、再撮影や別の角度でのリトライで解決できるため、致命的なエラーではありません。

適合率が低い(FPが多い)場合、システムがAというワインを「これはBというワインです」と間違って識別してしまい、ユーザーは誤った情報を得ます。ユーザーがその誤りに気づかなければリカバリが困難であり、システムへの信頼が失墜します。これは、識別の失敗(リトライで解決可能)よりも重大なサービス上の問題です。

この観点から、「確実性の高い結果を提供すること(高適合率)」が、「可能な限り多くの結果を提供すること(高再現率)」よりもビジネス価値が高いという判断は妥当です。

適合率を重視するという方針を精度の数値化に反映させるには、F1スコアではなく、以下の適合率を強調した指標を使用します。

ということで、紹介されたのが「Fβスコア」でした。

Fβスコアは、適合率と再現率のどちらに重み(β)を置くかを調整できる指標です。
具体的な計算式は以下になります。

Fβスコア = (1 + β^2) * (適合率 * 再現率) / ((β^2 * 適合率) + 再現率)

βを1.0未満にすれば適合率が重視され、1.0以上にすれば再現率が重視されることになります。
β=1の場合がF1スコアです。

Gemini先生お勧めのβは0.5(β^2 = 0.25)でしたので、まずは本値を採用してFβスコアを算出し、それによってハイパーパラメータの調整を行なってみます。

トリプレットの取得方法変更

ハイパーパラメータの調整というテーマからは若干外れるのですが、トリプレットの取得方法に関しても見直しています。

今までの処理内容は以下の通り。

import random
import itertools

def get_triplet_combinations(anchor_info, max_combinations=15):
    anchor_path = anchor_info[0]
    positives = anchor_info[1]
    negatives = anchor_info[2]
    
    # 可能な組み合わせをすべて生成
    all_combinations = []
    for positive_path in positives:
        for negative_path in negatives:
            all_combinations.append((anchor_path, positive_path, negative_path))
    
    random.shuffle(all_combinations)
    
    # max_combinationsの上限で組み合わせを選択
    selected_combinations = all_combinations[:max_combinations]
    
    for combination in selected_combinations:
        yield combination

def triplet_generator(triplet_data, max_combinations=15):
    # エポックごとにデータをシャッフルして多様な組み合わせを生成
    random.shuffle(triplet_data)
    
    for anchor_info in triplet_data:
        # 各アンカー情報から組み合わせジェネレータを取得
        for triplet in get_triplet_combinations(anchor_info, max_combinations):
            yield triplet

前回の投稿でも触れましたが、学習効率を考慮して、1つのアンカーを含むトリプレット数に関して、所定の数を超えないように制限しています。
それ上限を指定する引数がmax_combinationsです。

普段はmax_combinations=5として学習を実施していますが、ある程度ハイパーパラメータの値が確定してきた段階で、より多くのデータを使用して学習を行なうよう、デフォルト値(max_combinations=15)で学習を行いました。
その結果、同じハイパーパラメータを指定しながら、なぜかmax_combinationsを大きくした方がF1スコア・Fβスコアともに悪くなるという傾向が見受けられました。

上記観点で、改めて既存の処理内容を見てみると、triplet_dataに含まれるアンカー単位での学習順序や、あるアンカーを含むトリプレット(最大max_combinations個)に含まれるポジティブ・ネガティブの組み合わせに関してはシャッフルが適用されますが、あるアンカーを含むトリプレット全体としては連続して学習されるようになっています。
バッチサイズ=32で学習しているため、max_combinations=15とした場合、1つのバッチ内の約半分が同じアンカーに関する学習になってしまう可能性がある訳です。

上記より、現在の実装ではバッチ内の多様性が十分に確保されておらず、学習が偏ってしまうことが精度に影響しているのではないかと推測したのですが、この点に関してはCursor先生にも賛同いただきましたので、改めて以下のように改修していただきました。

import random
import itertools

def get_triplet_combinations(anchor_info, max_combinations=15):
    anchor_path = anchor_info[0]
    positives = anchor_info[1]
    negatives = anchor_info[2]
    
    # 可能な組み合わせをすべて生成
    all_combinations = []
    for positive_path in positives:
        for negative_path in negatives:
            all_combinations.append((anchor_path, positive_path, negative_path))
    
    random.shuffle(all_combinations)
    
    # max_combinationsの上限で組み合わせを選択
    selected_combinations = all_combinations[:max_combinations]
    
    for combination in selected_combinations:
        yield combination

def triplet_generator(triplet_data, max_combinations=15):
    # エポックごとにデータをシャッフルして多様な組み合わせを生成
    random.shuffle(triplet_data)
    
    # すべてのトリプレットを一度生成してリストに格納
    all_triplets = []
    for anchor_info in triplet_data:
        # 各アンカー情報から組み合わせを取得
        for triplet in get_triplet_combinations(anchor_info, max_combinations):
            all_triplets.append(triplet)
    
    # すべてのトリプレットをシャッフルして、異なるアンカーのトリプレットが混ざるようにする
    # これにより、バッチ内の多様性が向上し、学習の偏りを防ぐ
    random.shuffle(all_triplets)
    
    # シャッフルされた順序でトリプレットをyield
    for triplet in all_triplets:
        yield triplet

改修後の処理では、すべてのトリプレットを生成した後にシャッフルしているため、1つのアンカーに関連するトリプレットも分散して学習されることになります。

上記改修により、より適切にハイパーパラメータの調整が実施できるようになることが期待されます。

調整結果

最新の結果を示す前に、EfficientNetV2Lオリジナルおよび前回紹介した試験的に実施した学習結果のモデルに関して、F1スコアに加えて、Fβスコアが最大となる状況を整理しておきます。

EfficientNetV2Lオリジナル

閾値ポジティブネガティブ適合率再現率F1スコアFβスコア
0.10111818460.37720.49910.42970.3966
0.0886811920.42140.38750.40370.4141

前回の学習済モデル

閾値ポジティブネガティブ適合率再現率F1スコアFβスコア
0.1520846060.77470.93040.84540.8015
0.0916282020.88960.72680.80000.8515

いずれも、上がF1スコア、下がFβスコアが最大となった状況です。

学習済モデルに関しては、Fβスコア基準(閾値=0.09)で識別した場合、もう少しで適合率が0.9に届きそうです。
その分、再現率は7割程度になってしまいますが。

上記に対して、ハイパーパラメータの調整を行なった中で最良の結果となったケースに関して以下に示します。

最新学習済モデル

閾値ポジティブネガティブ適合率再現率F1スコアFβスコア
0.1921061740.92370.94020.93190.9269
0.121928300.98470.86070.91850.9571
0.03146401.00000.65360.79050.9042

上記モデル生成時のハイパーパラメータは、margin=0.4、pair_loss_weight=0.05、dropout_rate=0.15 でした。

Fβスコア基準(閾値=0.12)での識別結果で、適合率=0.9847、混入したネガティブが30のみ、というのは相当に良い結果だと言えます。
この時のFβスコアは0.9571で、当面の目標としていたFβスコアおよび適合率0.9越えに関しても、あっさり達成してしまいました。
再現率も0.8607と、悪くありません。

F1スコア基準(閾値=0.19)で見ても、すべての指標(適合率、再現率、F1スコア、Fβスコア)が0.9以上で、こちらも良い数値になっています。

なお、適合率=1.0000(100%)となる最大の閾値を確認したところ0.03で、その際の再現率は0.6536です。
つまり、本モデルを用いたエンベディング結果の距離が0.03未満であれば同じワインである可能性が極めて高く、そのように判断できるケースが(今回学習に用いたデータの範囲では)全体の65%程度存在したと言うことです。

最新学習済モデルのハードネガティブ

前述のように、最新学習済モデルは精度面でかなりの向上が見られるのですが、それでもFβスコア最良となる閾値=0.12未満に含まれるアンカー・ネガティブの組み合わせが30組存在します(アンカーとネガティブを逆にしても同じ結果が得られるため、実際の画像の組み合わせとしては15組と言うことになりますが)。

どのようなものであったのか、代表的なものを見てみます。

アンカーネガティブ距離
0.07183635234826824
0.0759878207484378
0.10494493187130305
0.03592628454721125

1番目は、真ん中左に書かれているブドウの品種が違っています。アンカーが「PINOT GRIS」で、ネガティブが「RIESLING」です。
2番目は、上の方に書かれているビンテージが違っています。アンカーが「2020」、ネガティブが「2019」です。
3番目は、少なくとも人間の目で見る限り、全くの別物です。色合いが若干似ていますが、酷似しているレベルでもありません。
4番目は、下の方に書かれている文字列(何を意味するかは不明)が違っています。アンカーが「SUR LA CÔTE」で、ネガティブが「INFUSION」です。なお、本ケースが、現時点のデータ+最新学習済モデルにおいて、アンカー・ネガティブの距離が最も近い組み合わせです。

1,2番目の識別難易度が高い点は理解できます。
4番目に関しては、識別のヒントとなる文字列の大きさで言えば1,2番目よりも大きいので識別しやすそうなのですが、結果としては、最も難易度が高い識別に位置付けられています。
実は4番目の画像は両方ともテスト用データに属するもので、学習(訓練・検証)には全く関与していません(ちなみに、1,2番目の画像は学習用データ)。
つまり、現在の学習方法における汎用性の限界を示した例だと言えます。

最も不思議なのは3番目で、適合率=0.9847のモデルとして、本組み合わせを「近い」と判断してしまう理由が全く想像できません。
Gemini先生に聞いてみましたが、(途中のややこしい説明は割愛して)最終的な結論は以下のようなものでした。

この誤認は、この2枚の画像のみが共有する、背景のテクスチャや撮影条件由来のノイズパターンといった、人間が見過ごす「非ラベル領域の特徴」を、モデルが誤って識別を支配する「決定的な特徴」として学習してしまった、非常に局所的な失敗であると推測されます。

結局…良くわかりませんね。

まとめ

前回の投稿以降、ハイパーパラメータの調整を行なってきました。
その成果として、良好な結果につながる値が大まかに絞り込めてきましたし、その値を使って学習されたモデルのFβスコア(+適合率)も格段に向上しました。

ただ、今回の精度向上の最大の要因は、実はトリプレットの取得方法変更の方にあるようです。
同じハイパーパラメータを指定しているにも関わらず、変更前はFβスコア平均=0.8555(最大=0.8719、最小=0.8205)であったのに対し、変更後は平均=0.9519(最大=0.9571、最小=0.9365)になっていますので。

つまり、「ハイパーパラメータを調整して精度を上げる」という趣旨で作業をしていたのが、「ハイパーパラメータも調整したし、精度も上がった」という、想定していたものとは若干違った形になってしまいましたが、目的自体は達成できたので結果オーライと言うことにします。

なお、大した根拠なく目標にしてきた適合率0.9越えですが、改めて考えてみると適合率=0.9847でも100回に1〜2回程度の誤認が発生することを意味する訳で、ソフトウェアの精度としては手放しに喜べる状況でもないような気がしてきました。
引き続きモデルの精度向上は模索していきたいですが、もっと別のアプローチでも誤認を減らす方法を考えていきたいと思います。

Related articles

ResNet50からEfficientNetV2L...

約三ヶ月ぶりの投稿です。前回の投稿(...

使ってよかったおやじの独り言 Part1

足の匂い気になりませんかw同じ靴を毎...

NFCとSAKUTTO lpでデジタル名刺

NFC安くなったな〜。一枚安いと70...

MacBookとThunderboltとデュアルデ...

「部屋とYシャツと私」みたいなタイト...