ファインチューニング(2)

Date:

Share post:

前回の投稿(ファインチューニング(1))で、以下の2つの改善点を上げておきました。

  • 勾配計算にコサイン距離を使用
  • ネガティブにはアンカーと似たもの(ハードネガティブ)を指定

今回は上記に対応していきますが、それ以前に、もっと影響のありそうな問題点がありましたので、その点に関しても併せて対応します。

学習用画像データの改修

前回の投稿において、学習に使用するデータは「ラベル画像のトリミング(4)」で用意した200点の画像であることに触れつつ、背景等の余計な情報を排除するため、上記投稿時に併せて生成したマスク画像を使用して、ラベル部分以外を黒く塗りつぶしておくことを説明しました。

言い方を変えれば、セマンティックセグメンテーションの学習用に用意した画像の、ラベル以外の部分を黒く塗りつぶしただけのものを前回の学習に使用していた訳です。

前述の投稿内でも触れていますが、セマンティックセグメンテーションの学習の特性上、学習に使用する画像は「画像全体に対するラベル部分の占有面積が30%(最大でも50%)程度」に調整してありました。
つまり、意図的にラベルが小さめに写るように調整した画像だったと言うことです。
ResNet50のファインチューニングに際しては上記画像を224×224にリサイズしますので、ラベル部分のサイズは一辺が120〜130程度しかないかもしれません。
これで、ラベルの内容を識別するように学習し直すと言うのは、かなり無理な話だったかと思います(その割には、一応の成果が出ていた点に、逆に驚きますが)。

と言うことで、「ラベル画像の切り出し」で実施したように、マスク後の画像からラベル部分を切り出して、それを224×224にリサイズしたものを使用して、再度学習を行なってみました。

結果は以下の通り。

まずは、学習の推移から。

val_lossに減衰傾向があるかに関しては相変わらず怪しいところですが、少なくとも値的にはかなり小さくなりました。

val_lossが最小となった状況を前回の結果と比較してみます。

前回今回
epoch4149
loss0.00360.0017
val_loss0.02240.0034

比較的早い段階で最小値を記録してしまう(その後、40epochの間に改善なし)と言う点は大きくは変わっていませんが、loss, val_lossの数値はともに、かなり良くなっています。

今回も、ポジティブ(pos)、ネガティブ(neg)のそれぞれに関して、0.05単位で区切った各範囲に何件のケースが存在するかをグラフ化してみます(この方法が一番直感的に分かりやすいので)。

ラベル切り抜き後

前回の結果と比較して、ネガティブ側がさらに全体的に0.1程度後ろにずれて、ポジティブとネガティブの間の隙間が0.2以上に広がりました。
良い傾向です。

改めて、ResNet50オリジナルと比較してみましょう。
オリジナルのポジティブ(pos-1)とネガティブ(neg-1)に対して、今回のチューニング結果におけるポジティブ(pos-2)とネガティブ(neg-2)の分布がどう変化したかをプロットしてみます。

オリジナルとファインチューニング後の比較

特にネガティブの分布に関して、かなり顕著に学習の効果が出ていることが分かります。

コサイン距離での損失計算

次は、損失計算をユークリッド距離(正確にはその二乗ですが)からコサイン距離を使用するように変更してみます。

ユークリッド距離での損失計算は以下のように行なっていました。

        pos_dist = tf.reduce_sum(tf.square(anchor_embedding - positive_embedding), axis=1)
        neg_dist = tf.reduce_sum(tf.square(anchor_embedding - negative_embedding), axis=1)
        triplet_loss_value = tf.maximum(0.0, pos_dist - neg_dist + self.margin)
        total_loss = tf.reduce_mean(triplet_loss_value)

上記をコサイン距離を使用するように変更すると、以下のようになります(例によって、実装はGemini先生にお願いしました)。

        sim_ap = tf.reduce_sum(anchor_embedding * positive_embedding, axis=1)
        sim_an = tf.reduce_sum(anchor_embedding * negative_embedding, axis=1)
        pos_dist_cos = 1.0 - sim_ap
        neg_dist_cos = 1.0 - sim_an
        triplet_loss_value = tf.maximum(0.0, pos_dist_cos - neg_dist_cos + self.margin)
        total_loss = tf.reduce_mean(triplet_loss_value)

今回のエンベディング結果は正規化される(ベクトルの長さが1になる)ため、コサイン類似度(2つのベクトルがどの程度近いか)は単純に2つのベクトルの各要素の積を合計した値(内積)で示せるようです。
コサイン類似度は1(一致)から-1(真逆)の間の値になります。

コサイン距離は上記コサイン類似度を距離っぽく(近いほど数値が小さく)表現できるよう変換したものと言って良く、計算方法は単に1からコサイン類似度を引くだけです。
その結果、コサイン距離は0(一致)から2(真逆)の間の値となります。

なお、上記に併せて margin も1.0から0.5に変更していますが、この辺は話が複雑になりそうなので略。

上記改修を行なった上で、再度学習を実施してみました。
結果は以下の通り。

まずは、ポジティブとネガティブの距離の分布から。

損失計算にコサイン距離採用後

ぱっと見では、ユークリッド距離と大差ありません。

ポイントとなるデータを見てみましょう。

ユークリッド距離コサイン距離
ポジティブ最小距離0.026148483062386596(41-43)0.02662279963858527(41-43)
ポジティブ最大距離0.17715173619226599(34-35)0.19114278475724944(34-35)
ポジティブ最大距離(35を除く)0.1337833385611581(32-33)0.14774973285180026(32-33)
ネガティブ最小距離0.447228579061297(35-45)0.423604784323777(35-45)

ごく僅かですが、精度が落ちている(ポジティブ側の距離が大きくなり、ネガティブ側の距離が小さくなっている)ようです。
この結果だけ見ると、ユークリッド距離を使って損失計算した方が良いようにも思いますが、1回の学習結果のみで結論を出すのも早計かと思いますし、Gemini先生の指導を信じて、今後もコサイン距離を使っていくことにします。

ハードネガティブ

最後に、ネガティブをアンカー以外のものからランダムに選択するのではなく、よりアンカーと似たもの(ハードネガティブ)を利用するように変更します。

まずは、学習用データ(200点)の各画像間の距離を算出し、値の小さいものから20個を各アンカーのネガティブリストとして管理するようにします。
距離算出の対象となるベクトルには前述した損失計算方法改修までを実施したモデルでのエンベディング結果を使用し、各アンカーに対する他ベクトルとのコサイン距離を算出した上で、小さいものから20個を抽出します(この辺はPostgreSQL任せです)。

トリプレットの動的生成時には、上記リストの中からランダムにネガティブを採用します。
なお、ネガティブリストに含める対象の数が20個であるのは、学習がバッチサイズ32で100回、つまり3200トリプレットに対して行なわれることを考慮した結果です。
学習用データ全200点のうち実際の訓練に使用されるのが160点であるため、各アンカーに対して20個のネガティブが存在すれば、全ての組み合わせが3200となり、先の3200トリプレットを組み合わせの重複なく生成できるためです(実際にはネガティブの抽出はランダムなので、1epochの中で組み合わせの重複は発生していると思いますが)。

実は、最初は最も距離が近い1つのみをネガティブとして持つようにしたのですが、学習精度がかなり悪くなってしまいました。
学習におけるデータの多様性は、かなり重要っぽいです。

と言うことで、まずは学習用データ200点のそれぞれに関して他画像との距離を算出しました。
以下が、各アンカーに対して最短距離がどの程度であったかをグラフ化したものです。
比較のため、先に示したポジティブ、ネガティブの結果と併せてプロットしています(h-negが200点の最短距離の分布です)。

ハードネガティブの分布

限られたサンプル(4つのラベルに関する各5点の画像)での比較ではポジティブとネガティブが明確に分離できていましたが、200点の画像で確認すると、やはり別ラベルながら「近い」と判断されてしまうものも多々あるようです。

参考までに、最も距離が近かったものから5セットを、それぞれのコサイン距離と併せて以下に示します(上から近い順です)。

0.0364172767525095
0.0417696151162118
0.0530420668225296
0.0620070358543874
0.069015017865298

2番目、4番目はラベル自体の形状が特殊で、それだけで「似ている」との判断になってしまうかもしれません。
ラベル内の文字の大まかな配置も似ていますし、書かれた文字の違いをどの程度認識できるかが勝負ですかね。

1,3,5番目は間違い探しのような内容ですが、特に3番目はラベル左下にある年表記(ヴィンテージ)が違っているだけなので、分かっていても区別が難しいです。

これらを識別できるようにするのは、かなりハードルが高いように思いますが…

で、学習結果が以下です。

学習の推移としては、ランダムにネガティブを選択していた段階と大きな違いはないように見えます。

最小val_lossの記録は以下になります。

epoch90
loss0.0006
val_loss0.0025

epochが増えていたり、loss,val_lossともに若干小さくなっていますが、この辺は学習ごとにかなり振れ幅があるので、このデータだけから言えることは、あまりなさそうです。

ポイントとなるデータも見てみましょう。

ランダムネガティブハードネガティブ
ポジティブ最小距離0.02662279963858527(41-43)0.03188617177705588(41-43)
ポジティブ最大距離0.19114278475724944(34-35)0.18784141902879992(34-35)
ポジティブ最大距離(35を除く)0.14774973285180026(32-33)0.1406749314672997(32-33)
ネガティブ最小距離0.423604784323777(35-45)0.42069447985953334(35-45)

ここも、数値的には大きな違いはないです。

ポジティブ、ネガティブの距離の分布を見てみましょう。

ポジティブの方は大きな変化はありませんが、ネガティブの方に変化があるようです。

比較のためランダムネガティブ段階(pos-1, neg-1)と今回の結果(pos-2, neg-2)を併せてプロットしてみます。

ランダムネガティブでは分布が比較的安定的(山型)であるのに対し、ハードネガティブ側は0.5〜0.6辺りも増えている一方で、0.7以上も増えていて、逆に途中の0.6〜0.7辺りは減っています。

200点の各アンカーに対する最短距離の変化に関しても見てみましょう。
ランダムネガティブ(r-neg)とハードネガティブ(h-neg)それぞれに関する分布をプロットしてみます。

分布している範囲(0〜0.5)としては大きく変わりませんが、0.2未満(現在の一致判定域)の数がハードネガティブでは少なくなっています。
この点は良い傾向です。

先に上げた、最短距離トップ5に関して、距離の変化を見てみます。

0.03641727675250950.06749018021064912
0.04176961511621180.10060575304605435
0.05304206682252960.06311497873952499
0.06200703585438740.14821802910018855
0.0690150178652980.11029192476594318

一番右にハードネガティブの結果を記載しました。
いずれもハードネガティブの方が距離が大きくなっています。
2,4番目などは倍以上あります。
ただ、相変わらず一致判定域(0.2未満)です。
全体的に、文字の違いでの判断は、まだまだ苦戦している感じなのかもしれません。

なお、ハードネガティブの結果において、最も距離が小さくなったのは、上記3番目のケース(約0.063)です。
このケースが一番区別が難しいと判断されたのは、ある意味納得の結果なのですが…

まとめ

今回の対処により、ラベルの識別精度がかなり向上したことは間違いないでしょう。
ただ、十分かどうかで言えば、もう一声と言ったところです。
ポジティブをもう少し近づける(0.1〜0.15未満くらいにしたい)、ネガティブをもう少し遠ざける(0.1〜0.15以上にしたい)両面で更なる精度向上を目指したいです。

と、まとめたかったのですが、実は今回のデータから除外した組み合わせが1つあります。
それが以下です。

ボトルの色を見ると、両者の区別は比較的簡単にできます。
しかし、ラベルだけを抜き出してみると…違いがないんですよね。
こんなん、どないせいっちゅーねん!って話です。
まぁ、こんなパターンもありそうだと予想はしていましたが。

と言うことで、ラベルによるワインの識別は引き続き目指したいテーマですが、それだけでワインの識別が100%可能になると言うことは絶対にないということだけ分かりましたので、別のアプローチも考えていきたいと思います。

Related articles

ファインチューニング(1)

前回の投稿(ラベル画像のエンベディン...

ラベル画像のエンベディング

前回の投稿(ラベル画像の切り出し)で...

ラベル画像の切り出し

前回の投稿(Mac+PostgreS...

フロント担当のTIL 2025/06

「Today I Learned(今日学んだこと)」を記録するTIL、6月は新マシンが納品され、GPU仮想化からローカルLLM環境の構築まで、いろいろ試行錯誤しました。