こしあん
2018-11-09

Kerasでランドマーク検出用の損失関数を作る上でのポイント


ランドマーク検出やオブジェクト検出では、yに最初に物体やランドマークが存在する確率をおいて、それ以降に座標を配置するというようなデータ構造を取ります。その場合、カスタム損失関数を定義する必要が出てきますが、どのように定義するれば良いでしょうか。それを見ていきます。

Kerasの損失関数

分類問題ではy(ラベル)はそのクラスに属せば0、属さなければ1という簡単な設定でした。例えば猫かどうかを分類する場合、1サンプルあたりのyは「猫ならば1、猫以外ならば0」という形になります。ところで、損失関数は

def loss_function(y_true, y_pred):
    # 損失を計算するための計算
    return loss

という形で定義されます。実際はloss=”binary_crossentropy”とか指定するため、このように損失関数を書く必要はありませんが。この猫の分類の場合、y_true, y_predに渡されるテンソルの形はどのようになるでしょうか。

Pythonのブロードキャスティングによるバグを回避するために、ランク1の行列を使わないとするなら、損失関数に渡されるy_true, y_predの形はおそらく(ミニバッチサイズ, 1)となるはずです。これはサイズ1のベクトルをミニバッチサイズ分積み重ねたもので、例えばXの行列と似ていますね。実際にy_trueの中身をprint(K.int_shape(y_true))とかで表示すると、(None, None)が出てきてデバッグに少し困ることがあるのですがそこはちょっと闇ということで…。

では、これを犬か猫かという多クラス分類にする場合、損失関数のy_true, y_predの形はどうなるでしょうか。1サンプルの場合、猫だったら[1, 0]、犬だったら[0, 1]と定義すれば、2つ目の次元が増えるだけですね。なので、(ミニバッチサイズ, 2)となるはずです。クラス数が増えればあとは同様ですね。

ランドマーク検出におけるy(ラベル)

さてここからが本題。ランドマーク検出では問題設定にもよりますが、yを次のようにおきます。

  1. 画像内にランドマークが存在するか(0なら存在しない、1なら存在する)
  2. ランドマーク1のxの座標(画像左端が0で、右端が1)
  3. ランドマーク1のyの座標(画像上橋が0で、右端が1)
  4. ランドマーク2のxの座標(以下同様)

これをランドマークの数だけつぎ込みます。ランドマークの数がLなら、1サンプルあたりのyの次元は「N=2×L+1」となります。基本的には多クラス分類と同じなのですが、確率と座標という全く2つの値を扱うため、カスタム損失関数を定義してもいいと思います。面倒だったら両方とも平均二乗誤差で計算しても大丈夫だとは思います。

ではどうカスタム損失関数を定義すればよいでしょうか?確率の部分はbinary_cross_entropy(交差エントロピー)で、座標の部分はmean_squared_error(平均二乗誤差)で計算するのが良さそうです。これを定義しましょう。

ランドマーク検出のカスタム損失関数

y_true, y_predの次元は(ミニバッチサイズ, N)であることを意識して定義します。これが一例です。

from keras.objectives import binary_crossentropy, mean_squared_error
import keras.backend as K

def loss_function(y_true, y_pred):
    bce = binary_crossentropy(K.expand_dims(y_true[:, 0]), K.expand_dims(y_pred[:, 0]))
    mse = K.sign(y_true[:,0]) * mean_squared_error(y_true[:, 1:], y_pred[:, 1:])
    return K.mean(bce + mse)

追記:最後のreturn以降のK.mean()ですが、別になくてもいいようです(むしろないほうが推奨?)。これが必要かどうかは、サンプル単位で損失の加重をつけるかどうかなので、そこらへんを気にしない場合はどっちでもいいと思います。

交差エントロピー部分

まずは交差エントロピーから。スライスしたのを直接binary_cross_entropyに放り込んではいけません。そのまま放り込むとランク1のテンソル同士の計算になり、交差エントロピーがスカラー(実数)になります。本来、サンプル間の確率の交差エントロピーを取りたいため、ここの出力はベクトルでほしいのです。奇妙なことが起こるかどうか実際に確認してみましょう。

>>> K.get_value(y_true)
array([[1. , 0.2, 0.2, 0.4, 0.5],
       [0. , 0. , 0. , 0. , 0. ],
       [1. , 0.7, 0.8, 0.3, 0.3]], dtype=float32)
>>> K.get_value(y_pred)
array([[0.9, 0.1, 0.3, 0.5, 0.7],
       [0.2, 0.7, 0.8, 0.1, 0.2],
       [0.7, 0.5, 0.8, 0.2, 0.3]], dtype=float32)

これは今説明のためにy_true, y_predに簡単な値を設定してみました。まずはダメな例から。

>>> K.get_value(binary_crossentropy(y_true[:,0], y_pred[:,0]))
0.228393

はい、ダメです。途中でランク1になってしまったので、サンプル間の確率がそのまま交差エントロピーの計算に放り込まれてしまったのです。ここはK.expand_dims()でスライスした後に次元を増やすのが正解です。

>>> K.get_value(binary_crossentropy(K.expand_dims(y_true[:,0]), K.expand_dims(y
pred[:,0])))
array([0.10536053, 0.22314355, 0.35667494], dtype=float32)

これが本来欲しかった、サンプル別の確率の交差エントロピーです。非常に間違いやすいところなのですが、ブロードキャスティングで値が出る場合は、こういうケースでも実行時に警告一切出してくれないのですよね。なので、しっかり別立てで確認していかないと思わぬ落とし穴になると思います。

ちなみにブロードキャスティングのバグを直したら訓練が1~2割高速化しました。Numpyのブロードキャスティングは気づく場合があっても、Kerasのテンソルのブロードキャスティングは知らないまま動かしていて、闇が深いような感じがします。これが原因で訓練結果がおかしかったら普通はデータやモデルのほうを疑ってしまうので、なかなかここまで気づけない。

平均二乗誤差部分

さて、損失関数の続きを。こちらはブロードキャスティングの問題がないので、比較的わかりやすいと思います。

    mse = K.sign(y_true[:,0]) * mean_squared_error(y_true[:, 1:], y_pred[:, 1:])

最初のsign関数ですが、ランドマークがない場合は続く平均二乗誤差を無視しますよ、という意味です。K.signは要素別の計算なので、ランク1のテンソルを代入してもスカラーになることはありません。

>>> K.get_value(K.sign(y_true[:,0]))
array([1., 0., 1.], dtype=float32)

続く平均二乗誤差ですが、スライスはしているものの複数列のスライスなのでランク2のテンソルのままです。したがって、集計機能のある(ランクが落ちる)mean_squared_errorに代入してもスカラーになることはありません。ランク1のテンソル(ベクトル)になります。

>>> K.get_value(mean_squared_error(y_true[:, 1:], y_pred[:, 1:]))
array([0.0175, 0.295 , 0.0125], dtype=float32)

交差エントロピーと平均二乗誤差を足す

あとは簡単ですね。最後にサンプル別のベクトルを何らかの集計関数にかけましょう。合計でも平均でもどっちでもいいと思うのですが、一応平均にしてみました(ミニバッチサイズが変わったときに統一性を出すため)。

    return K.mean(bce + mse)

一応数値例でも確認してみましょう。

>>> bce = binary_crossentropy(K.expand_dims(y_true[:,0]), K.expand_dims(y_pred
,0]))
>>> bce
<tf.Tensor 'Mean_22:0' shape=(3,) dtype=float32>
>>> mse = mean_squared_error(y_true[:, 1:], y_pred[:, 1:])
>>> mse
<tf.Tensor 'Mean_23:0' shape=(3,) dtype=float32>
>>> error = K.mean(bce + mse)
>>> error
<tf.Tensor 'Mean_24:0' shape=() dtype=float32>
>>> K.get_value(error)
0.33672634

うまくいきましたね。たった3行のコードでしたが、ランク1のテンソルという思わぬ落とし穴があり、テンソル演算の部分はきちんとデバッグしないといけないという闇の深さを思い知らされました。

Related Posts

Kerasで複数のラベル(出力)があるモデルを訓練する... Kerasで複数のラベル(出力)のあるモデルを訓練することを考えます。ここでの複数のラベルとは、あるラベルとそれに付随する情報が送られてきて、それを同時に損失関数で計算する例です。これを見ていきましょう。 問題設定 MNISTの分類で、ラベルが奇数のときだけ損失を評価し(categorical...
Kerasで評価関数にF1スコアを使う方法 Kerasで訓練中の評価関数(metrics)にF1スコアを使う方法を紹介します。Kerasのmetricsに直接F1スコアの関数を入れると、バッチ間の平均計算により、調和平均であるF1スコアは正しい値が計算されません。そこだけ注意が必要です。 F1スコアをmetricsに入れるときは要注意 ...
Python(Numpy)で画像を水平反転する方法:Data Augmentation向け... OpenCVを使わずに単純に画像を左右反転(水平反転)する方法を考えます。ディープラーニングでデータのジェネレーターを自分で実装した場合、Data Augmentationを組み込む際にも必要になります。それを見ていきましょう。 左右反転自体は実は簡単 例えばNumpyの行列を左右反転させてみ...
Google ColabのTPU環境でmodel.fitのhistoryが消える現象... Google ColabのTPU環境でmodel.fitしたときに、通常の環境で得られるhistory(誤差や精度のログ)が消えていることがあります。その対応法を示します。 原因はTPU用のモデルに変換したから まず結論からいうとこの現象はCPU/GPU環境では再発しません。TPU環境特有の現...
TensorFlow/Kerasでの分散共分散行列・相関行列、テンソル主成分分析の実装... TensorFlowでは分散共分散行列や主成分分析用の関数が用意されていません。訓練を一切せずにTensorFlowとKeras関数だけを使って、分散共分散行列、相関行列、主成分分析を実装します。最終的にはカテゴリー別のテンソル主成分分析を作れるようにします。 何らかの論文でこれらのテクニックを...

Add a Comment

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です