LoginSignup
star_jun
@star_jun

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

CNNを用いたクラス分類においてtrainの正解率がvalidの正解率を下回る。

解決したいこと

機械学習の初学者です。Keras上でCNNを用いた6クラスの分類に取り組んでいます。
CNNのクラス分類として他で見たことのない問題に直面しており、調べ方も分からないので質問させていただきます。
次の2点についてご意見をお聞きしたいです。よろしくお願いします。

  1. 以下のような問題は起こりうるのか(見たこと、聞いたことがあるか)
  2. 問題の原因の特定、解消

発生している問題

学習曲線においてtrainの正解率がvalidの正解率を下回ります(train:valid:test=6:1:3の割合でデータを分割しています)。
画像ではグラフの凡例がtestとなっていますが、これはvalidのことだと思ってください。testは学習に使用していません。
このようなことは起きても問題ないのでしょうか。それともやはり何か様子がおかしいでしょうか。
元の問題.png

該当するソースコード

Kerasで記述しているCNNモデルの実装部分について載せさせていただきます。

BATCH_SIZE = 128
image_size_x = 25 #画像サイズ
image_size_y = 15
epochnumber = 50
L2weight = 0.001

#CNNアーキテクチャの設計
model = Sequential()

model.add(BatchNormalization(input_shape=(image_size_x, image_size_y, color_setting)))
model.add(Activation('relu'))
model.add(Conv2D(16, (3, 3), padding='same'))

model.add(BatchNormalization())
model.add(Activation('relu'))
model.add(Conv2D(32, (3, 3), padding='same'))

model.add(BatchNormalization())
model.add(Activation('relu'))
model.add(Conv2D(32, (3, 3), padding='same'))

model.add(BatchNormalization())
model.add(Activation('relu'))
model.add(Conv2D(64, (3, 3), padding='same'))

model.add(GlobalAveragePooling2D())

model.add(Dense(class_number, activation='softmax', kernel_regularizer=regularizers.l2(L2weight)))

model.summary()

#学習率減衰の設定
def step_decay(epoch):
    x = 0.01
    if epoch >= 20: x = 0.001
    if epoch >= 40: x = 0.0001
    return x
lr_decay = LearningRateScheduler(step_decay)

model.compile(loss='categorical_crossentropy', #損失関数の設定
              optimizer=Adam(learning_rate=0.01), #最適化アルゴリズムの設定
              metrics=['accuracy'] #評価関数の設定
              )

start_time = time.time()

#可視化
# ここを変更。必要に応じて「batch_size=」「epochs=」の数字を変更してみてください。
history = model.fit(x_train,y_train, 
                    batch_size=BATCH_SIZE, 
                    epochs=epochnumber, verbose=1, 
                    validation_data=(x_valid, y_valid), 
                    callbacks=[lr_decay] #学習率減衰の設定
                    )

自分で試したこと

まず、データセットに問題がないかは確認しました。ソフトを使ってデータセットに重複等の不備がないかを確認しました。
その結果、おそらくデータセットは問題なく用意できていそうだと結論づけています。

次に、上のグラフから20epochの学習率減衰の際に挙動が大きく変わっているように感じたので、学習率減衰の部分をコメントアウトして学習させてみました(上で示したCNNの実装コードでは20epochと40epochの段階で学習率減衰を導入しています)。
その結果が以下のグラフです。
学習率減衰を除いた場合.png

結果としては、最初に示したような露骨な結果にはなりませんでした。しかし、trainがvalidの正解率を下回るepochも多少みられることから問題の解消には至っていないんじゃないかと思っています。

また、ここでは原因究明のためにコメントアウトしたデータを用意しましたが、学習率減衰は最終的には実装したいと考えています。

余談

昨日もQiitaで別の質問をさせていただき色々とご回答いただいたのですが、Qiitaに不慣れなために質問を誤って削除してしまいました。昨日ご助言くださった方々が見ているはずもありませんが、ここで謝罪させていただきます(その後問題は解決しました)。

追記(12月13日)

cross validationしたらどうかというご助言をいただいたので、train:test=7:3で分割した後trainデータを7等分してcross validationしました。7回分の学習結果を平均した学習曲線を載せさせていただきます。

crossvalidの結果.png

結果としてはやはり平均してvalidデータのほうが正解率が高くなりました(問題は解決しませんでした)。

追記(12月17日)

BNや入力データの正規化に関するご助言をもとに、2つの実験を行いました。

まず、入力直後のBN層を省いてご助言の通りに最初の2層を
model.add(Input(shape = (image_size_x, image_size_y, color_setting)))
に書き換えてみた結果が以下の通りです。
BNを省いた結果.png
10~20epochではvalidの正解率が上回っているものの、問題の解消に大きくつながる結果となりました。

次に、そもそも私がデータの準備の段階で正規化を怠っていたことに気づいたため、入力データを0~1の範囲になるよう正規化してみることにしました(追記分と比較しやすいように-1~1で正規化した結果のグラフと差し替えました。差し替える前と結果はほとんど変わりありません)
こちらの実験ではBN層はいじらずに上に示しているコードで学習しています。
1218_norm.png
元々の想定に最も近い結果が得られました。BN層についてのご助言も大変納得いくものでしたが、入力データの正規化が直面している問題の解決には最も重要だったと感じています。
結論としてはかなり初歩的なミスでしたが、ご助言いただかないと解決できなかったと思います。皆さんありがとうございました。

追記(12月18日)

またもや初歩的なミスでお恥ずかしいですが、学習率減衰を除いていたことをすっかり忘れていたため学習率減衰を改めて加えたうえで学習したところ、20epochの学習率減衰後では元々の問題が表れる結果となりました。そのため、一度は解決したと早とちりしましたが追記で質問させてください。なお、入力データの正規化についてもご助言の通り-1~1でやり直してみました。
1218_decay.png
また、ご助言いただいた通り最初のBN層を除いた場合の学習曲線が以下の通りです。こちらは以前も確認した通り問題の解消に向かっているように思います。
1218_decay_removeBN.png
実は現在取り組んでいる学習はある先行研究の追実験なので、できるだけCNNモデルをいじりたくないという事情があります。しかしご指摘いただいている通りBN層が入力の直後に来るのは不自然な気がしてきたので、他に問題の原因が見つからなければ最初のBN層は除こうと考えています。
今回の追記分で特にお伺いしたい点は、学習率減衰が質問させていただいている問題の原因になっていないかという点です。何かご存じの方がいらっしゃればぜひご助言いただきたいです。

0

3Answer

trainがvalidの正解率を下回るepochも多少みられることから問題の解消には至っていないんじゃないかと思っています。

詳細な学習の過程やログを見ていませんので断言はできませんが、trainの正答率がvalidの正答率を下回るという現象の原因を究明されているようですが、必ずしもモデルの挙動がおかしいとは断言できないと思います。機械学習においての学習(train)の最終的目標は汎化性能を上げる(新しいデータに対して高い性能を示す)ことです。多くの場合、trainの正答率がvalidの正答率を上回るという結果が観測されますが、それは汎化性能を獲得するという目標を達成しておらず、寧ろ学習データにモデルが寄ってしまっているということになります。(例えばテスト勉強のために過去問を丸暗記してその年のテストで点数が取れないといった状態です。)それが酷くなるとOver Fitting(過学習)という状態になります。あくまで機械学習は新しいデータ(ここでいう所のvalidデータ)に対して高い正答率を出すことを目指しています。よって繰り返しとなりますが、決してモデルがおかしくなっている訳ではなさそうです。
学習のlossのログを見る限り、十分にlossが減少しており、学習は正常にされている様に思われます。

1

Comments

  1. @star_jun

    Questioner
    CNNの目指すところとして誤っていたり理屈に合わない結果ではないということですね。ありがとうございます。

    ただ、一方で経験則的にはvalidの正解率はtrainのものを下回るのが普通ですよね。
    私はvalidの正解率がtrainを上回る例を見たことがないので何かしらの不備を疑っているのですが、その点はいかがでしょう…。そのような例をご存じの方はいらっしゃいますでしょうか。もしくは有識者の皆さんも見たことないでしょうか。

Cross-Validationしたらどうなりますか?
平均的にvalidの方が悪いという状態が得られると,疑問の解消になると思います.

1

Comments

  1. @star_jun

    Questioner
    試してみます!
  2. はい,現状のvalidがtrainよりも高精度になっている,いわゆる過学習の状態が治らなければモデルの性能を下げるようにしてみてください.普通は,Cross-Validationしてハイパーパラメータをチューニングするような箇所です.
  3. @star_jun

    Questioner
    過学習というとvalidがtrainの結果に及ばない状態を指すイメージがあるのですが、今回の問題もいわゆる過学習の状態と呼ぶのでしょうか?
    また、Cross Validationの結果を追記しました。問題の解決に至る結果にはなりませんでしたが、ご確認してご助言いただければ幸いです。
  4. そうですね,過学習とはちょっと性質が違うので誤発言でした.

    ただ,BatchNormalizationが入力側にあることで,train時にはその時のデータを元に正規化され,valid時にもtrainデータを元にした正規化がされるという動作の差によって,train-valid逆転が生じているように見えます. おそらく後者の方がlossが少なく済むのでしょう. https://keras.io/api/layers/normalization_layers/batch_normalization/ 詳しくは During training の項を確認してください.

    普通BNは何らかの非線形変換後に挿入されるべきもので,データの入力そのものに利用するのは今回初めて見ました.BNによって分散1,平均0に正規化された後に,ReLUを通すと0以上の値しか使われなくなってしまい,Loss増大に繋がると考えます.入力に与えた画像の値域次第では顕著に差が出そうですね.MNISTで実験しても逆転は起きませんでしたが,悪影響はあると考えます.

    念の為,最初の2レイヤを削除して,代わりにInputレイヤを
    model.add(Input(shape = (image_size_x, image_size_y, color_setting)))
    として与えてください.
  5. @star_jun

    Questioner
    >普通BNは何らかの非線形変換後に挿入されるべきもの
    確かにその通りですね。浅学なために疑問を持たずに用いていましたが、BNの特性として認識しておくべきですね。

    @PondVillege様のご助言を受けて、入力データの正規化の重要性を軽視していたことに気づきました。おかげさまで問題を解決できました。問題解決について追記したのでもしよかったらご覧ください。
    具体的かつ的確なアドバイスありがとうございます。大変勉強になりました!
  6. 追記の方,拝見いたしました.
    そもそも正規化されてなかったのですね...
    特にニューラルネットにおいて正規化されてないデータは悪影響の一因であることが多いですね.入力値が平均0,分散1であることを想定して色々開発が続けられているのもあると思います.だからといって入力直後にBNがあるのはあまりしっくりきませんが,うまくいったのなら良しとしましょう!

    入力データの正規化には,BNのように入力データ全部の平均と分散を利用して平均0,分散1に標準化する方法もあれば,値域を[-1, 1]にすることもあります.

    https://datascience.stackexchange.com/questions/54296/should-input-images-be-normalized-to-1-to-1-or-0-to-1
    一般には値域を[-1, 1]にする方が多いです(私もそうしています).

    また,言い忘れていましたが活性化関数をReLUにする場合は,Heの初期値を利用すると学習が高速で推移することが知られています. https://arxiv.org/abs/1502.01852
    上のコードに適用するには
    Conv2D(16, (3, 3), padding='same', kernel_initializer = 'he_normal')
    のようにしてください.デフォルトで活性化関数がtanh(というか原点対象)である関数に有効なglorot_normalが設定されています.
  7. @star_jun

    Questioner
    正規化は0~1というイメージがありましたが-1~1の場合も多いんですね。早速導入してみます。Heの初期値も初めて知りました。参考のリンクまでいただきありがとうございます。

    ところで当初質問した内容ですが、解決までは至っていませんでした。大変厚かましいお願いで恐縮ですがまたお知恵をお借りしたいです。質問にさらに追記しましたのでご覧いただければ幸いです。
  8. train-validで反転してはいるものの,もう十分に学習率に追従していることから正しい動作として検討を打ち切るのも良いと思います.

    LearningRateSchedulerを使って急激に学習率減少させるのがダメそうなら, ReduceLROnPlateau https://keras.io/api/callbacks/reduce_lr_on_plateau/ を利用して損失をモニタリングして動的に学習率を徐々に減少させるのはどうでしょうか.factor = 0.5とかにすると,学習停滞したら学習率を半分にできます.
  9. @star_jun

    Questioner
    学習率減衰にも動的な実装方法があるんですね。勉強になります。
    ただ、おっしゃる通り今回の問題は12月18日の2パターンのどちらかで満足しようと思います。

    正規化の重要性も知らないほどの初学者に丁寧に付き合っていただきありがとうございました!おかげさまで問題の解決に近づくことができましたし、それ以外にも色々と勉強させていただきました。

ただ、一方で経験則的にはvalidの正解率はtrainのものを下回るのが普通ですよね。

データやパラメータによりけりだと思います。
@PondVillege さんがご指摘のようにCross Validationを試してみて下さい。

1

Your answer might help someone💌