harugonos
@harugonos

Are you sure you want to delete the question?

If your question is resolved, you may close it.

Leaving a resolved question undeleted may help others!

We hope you find it useful!

[ArcFace] 推論結果がクラスラベルに依存しすぎてしまう

前提

Qiitaのサイトと,GitHubのコードを参考にしResNet50v2+ArcFaceのモデルを構築しました.
画像は物体の輪郭内を白塗りした白黒画像を使用しています.

発生している問題・エラーメッセージ

こちらのモデルは,学習時に画像とラベルの二つの情報を学習データとして与えて学習させたため,推論時には次のように画像とそのクラスラベル(1~12)の情報を同時に与えてあげる必要があります.

arcface_model = create_arcface_with_resnet50v2(input_shape, s=s_value, m=m_value) # モデル生成

predictions = arcface_model.predict([img, cls]) # softmaxの値を推論(画像(img)とそのクラスラベル(cls)を入力)

こちらに例えばクラス1の画像を1枚入力した際の出力は↓

[1.0000000e+00 0.0000000e+00 0.0000000e+00
 2.3753611e-32 0.0000000e+00 0.0000000e+00 
 0.0000000e+00 0.0000000e+00 2.8887461e-28 
 0.0000000e+00 0.0000000e+00 1.3241292e-28 ]

期待通り,クラス1に該当する確率が1になります.

しかし,これをクラス1の画像であるのにクラスラベルを2(cls=2)として入力すると↓

[0.0000000e+00 1.0000000e+00 0.0000000e+00
 1.1876997e-18 0.0000000e+00 4.6460828e-18 
 0.0000000e+00 0.0000000e+00 3.9981019e-35 
 0.0000000e+00 0.0000000e+00 0.0000000e+00 ]

とクラス1の画像なのにクラス2の確率が1になってしまします.

この現象は,画像はクラス1のままでクラスラベルを他の値にしても必ずそのクラスの確率が1になります.

本来であれば,画像の特徴に基づいて確率を出してほしいのに全てclsの値に依存したような結果が出力されてしまうので疑問に思いました.

いったいなぜこのような現象が起きてしまうのでしょうか.

追記

以下に,現在使用しているモデルの構造を示します.

class Arcfacelayer(Layer):
    # s:softmaxの温度パラメータ, m:margin
    def __init__(self, output_dim, s, m, easy_margin=False):
        self.output_dim = output_dim
        self.s = s
        self.m = m
        self.easy_margin = easy_margin
        super(Arcfacelayer, self).__init__()
    
    # モデル保存に必要
    def get_config(self):
        config = {
            "output_dim" : self.output_dim,
            "s" : self.s,
            "m" : self.m,
            "easy_margin" : self.easy_margin
        }
        base_config = super().get_config()
        return dict(list(base_config.items()) + list(config.items()))
    

    # 重みの作成
    def build(self, input_shape):
        # Create a trainable weight variable for this layer.
        self.kernel = self.add_weight(name='kernel',
                                        shape=(input_shape[0][1], self.output_dim),
                                        initializer='uniform',
                                        trainable=True)
        super(Arcfacelayer, self).build(input_shape)


    # mainの処理 
    def call(self, x):
        y = x[1]
        x_normalize = tf.math.l2_normalize(x[0]) # x = x'/ ||x'||2
        k_normalize = tf.math.l2_normalize(self.kernel) # Wj = Wj' / ||Wj'||2

        cos_m = K.cos(self.m)
        sin_m = K.sin(self.m)
        th = K.cos(np.pi - self.m)
        mm = K.sin(np.pi - self.m) * self.m

        cosine = K.dot(x_normalize, k_normalize) # W.Txの内積
        sine = K.sqrt(1.0 - K.square(cosine))

        phi = cosine * cos_m - sine * sin_m #cos(θ+m)の加法定理

        if self.easy_margin:
            phi = tf.where(cosine > 0, phi, cosine) 

        else:
            phi = tf.where(cosine > th, phi, cosine - mm) 

        # 正解クラス:cos(θ+m) 他のクラス:cosθ 
        output = (y * phi) + ((1.0 - y) * cosine) 
        output *= self.s

        return output

    def compute_output_shape(self, input_shape):
        return (input_shape[0][0], self.output_dim) #入力[x,y]のためx[0]はinput_shape[0][0]
# ResNet50v2 + ArcFace定義
# 学習に使用
def create_arcface_with_resnet50v2(input_shape, s, m):
    # ResNet50V2の入力層の前に独自の入力層を追加
    input_tensor = input_shape
    
    input_model = Sequential()
    input_model.add(InputLayer(input_shape=input_tensor))
    input_model.add(Conv2D(3, (7, 7), padding='same'))
    input_model.add(BatchNormalization())
    input_model.add(Activation('relu'))
    
    resnet50v2 = ResNet50V2(include_top=False, weights=None, input_tensor=input_model.output)

    resnet50v2.load_weights('save_model(weights_imagenet)/weights_imagenet.hdf5', by_name=True)

    flat = Flatten()(resnet50v2.layers[-1].output)
    dense = Dense(512, activation="relu", name="hidden")(flat)
    
    x = BatchNormalization()(dense)
    
    yinput = Input(shape=(num_classes,)) #ArcFaceで使用
    
    s_cos = Arcfacelayer(num_classes, s, m)([x,yinput]) #outputをクラス数と同じ数に

    prediction = Dense(num_classes, activation="softmax")(s_cos)

    model = Model(inputs=[resnet50v2.input,yinput], outputs=prediction)
    
    return model

ResNet50v2の構造自体は以下の写真を参考にしていただければと思います.
image.png

補足情報

一枚に対応するimg, clsのshape
img.shape → (1, 110, 110, 1)
cls.shape → (1, 29) # one-hot

ツールのバージョン

ubuntu 20.04
Python 3.8.10

tensorflow-gpu 2.5.3
keras 2.8.0
numpy 1.19.5

jupyter lab 2.3.2

0

1Answer

create_arcface_with_resnet50v2 の中身が分からないので何とも言えないのですが、predictions には予測したラベルではなく、真値のラベルが出力されていないでしょうか?

0Like

Comments

  1. @harugonos

    Questioner

    ご回答ありがとうございます.

    create_arcface_with_resnet50v2 については質問内に追記させていただきました.
    また,predictonsは12クラスに対するsoftmaxの確率値なので,これが予測値だと思っていたのですが,違うということでしょうか.
  2. >こちらのモデルは,学習時に画像とラベルの二つの情報を学習データとして与えて学習させたため,推論時には次のように画像とそのクラスラベル(1~12)の情報を同時に与えてあげる必要があります.

    とありますが、参考にされたリポジトリでは、推論時にクラスラベルは与えておらず、画像だけを入力しています。

    https://github.com/4uiiurz1/keras-arcface#test

    クラスラベルを入力することで、本来予測値が出力されるはずのpredictionsに何らかの影響が出ていないでしょうか?
  3. @harugonos

    Questioner

    学習時のモデルとは別に,Arcfacelayer以降の層を取り除くことで,次のように画像のみ1入力の推論モデルの構築は可能なのですが,層を取り除いたことでこれは最終的に512次元の特徴ベクトルを出力するモデルになってしまいます.
    ```python
    embedded_features_model = Model(arcface_model.get_layer(index=0).input, arcface_model.get_layer(index=-5).output)
    ```
    しかしArcFaceは分類タスクに対応しているため,ある未知画像を入力した際にそれが分類されるクラスに加え,その確率値をarcface_modelの出力層でsoftmaxの値として取得したいと考えております.

    質問内容に記述した通り,特定のクラスXの一枚の画像(img)とそれ以外のクラスラベル(cls≠X)の組み合わせで入力して得られるpredictionsの出力は,必ずclsのクラスに対応するsoftmaxの値が1になり,入力画像のクラスXに関わらずclsのクラスに依存するような出力となってしまいます.
  4. @harugonos

    Questioner

    embedded_features_modelから512次元の特徴ベクトルを取得し,cos類似度によって画像間の類似度を求めたところ,すべてのデータ間距離が0.99以上と極めて大きい値を出力しました.

    ArcFaceはデータ間の距離を近づけることはできても遠ざける働きはないため,使用したデータがもとから極めて類似していると捉えることができる?

    そのため,arcface_modelに[img, cls]を入力しても画像の特徴ベクトルの判断よりも,入力クラスでの判断が重要視されているのではないかと考えたのですが,この考察は見当違いでしょうか.
  5. >ArcFaceはデータ間の距離を近づけることはできても遠ざける働きはない
    これは誤りです。ArcFaceで利用しているlossを下げるためには、特徴ベクトルについて、同じラベルのデータ同士は近づけ、異なるラベルのデータ同士は遠ざける必要があるからです。

    >arcface_modelに[img, cls]を入力しても画像の特徴ベクトルの判断よりも,入力クラスでの判断が重要視されているのではないかと考えた
    推論時にarcface_modelにclsを入力する必要があることと、その入力clsによって推論結果が変わってしまうこと自体から、そもそもバグや実装ミスがあると考えます。
    ちなみに、未学習のモデルでも、このような状況は起こるでしょうか。そうであれば、バグや実装ミスである裏付けが更に増します。
  6. また、そもそも単に分類問題を解きたいのであれば、ArcFaceではなく通常の分類モデルを使えば良いと思いますが、それでは不都合でしょうか?
  7. @harugonos

    Questioner

    調べたところ,実装ミスがございました.

    そもそもResNet50v2 + ArcFace定義時に
    出力層を上記では
    prediction = Activation('softmax')(s_cos)
    として設定しているはずが,本問題では
    prediction = Dense(num_classes, activation="softmax")(s_cos)
    としていたため,予測値がそのままSoftmaxに渡せずに学習が思うようにいっていなかったと考えております.
    そのため,現在は前者の実装にて再学習を行っています.
  8. @harugonos

    Questioner

    修正後のコードを用いて学習を行った結果,新たな問題としてval_accが100であるのにも関わらず,val_lossが2以上の値を取りそれよりも値が小さくならなくなってしましました.

    普通ですともう少しval_lossは小さくなるのではと思うのですが,これは学習がうまくいっていないと捉えるべきなのでしょうか.
  9. val_lossの絶対値が大きいとのことですが、学習開始から下がっているのであれば問題ないと思います。val_accが高いということは(不均衡データでなければ)学習はできているのではないでしょうか。train_lossとも比べてみるといいと思います。

    私の経験則ですが、lossの特性上、例えばBinary Cross Entropy lossなど他のlossと比べると、lossそのものの値は大きくなったと記憶しています。
  10. @harugonos

    Questioner

    なるほど,そこまで神経質にならなくても良いということですね.

    この度は,親身にご対応くださり大変助かりました.

    厚かましいお願いであることは重々承知なのですが,こちらの質問内容にもお答えいただけますと幸いです.
    お手すきの際で構いません.よろしくお願いします.
    https://qiita.com/harugonos/questions/c7571e1f7c1d9c340a65

Your answer might help someone💌