LoginSignup
kobaq
@kobaq (masa koba)

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!

MNISTの0〰9数字を分類で学習がうまくいかないです。

解決したいこと

Python3でDL(おそらく初歩的な)をスクラッチで書いています。
softmax関数のコードが間違っているのか、学習が進みません。
どなたか教えていただけませんでしょうか。
初心者ですので、まるまるコードを載せます。

発生している問題・エラー

RuntimeWarning: invalid value encountered in subtract  x = x-np.max(x,axis=0)

該当するソースコード

Python3.9
import csv
import os
import pickle
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import cv2
from sklearn import datasets
from sklearn.model_selection import train_test_split
%matplotlib inline
np.random.seed(seed=0)


#ここでエラーが出ているようです。
def softmax(x):
    x = x.T
    x = x-np.max(x,axis=0)
    x = np.exp(x)/np.sum(np.exp(x))
    return x.T

# mnistデータセット
mnist = datasets.fetch_openml('mnist_784', as_frame=False)
# 画像とラベルを取得
X, T = mnist.data, mnist.target
# 訓練データとテストデータに分割
X_train, X_test, T_train, T_test = train_test_split(X, T, test_size=0.2)

T_train = np.eye(10)[T_train.astype("int")]
T_test = np.eye(10)[T_test.astype("int")]

def cross_entropy_error(t, y):
    delta = 1e-8
    error = -np.mean(t * np.log(y + delta))
    return error

class SoftmaxCrossEntropyLoss():
    def __init__(self):
        self.y = None
        self.t = None
        self.loss = None
        
    def __call__(self, t, y):
        self.y = softmax(y)
        self.t = t.copy()
        self.loss = cross_entropy_error(self.t, self.y)
        return self.loss
    
    def backward(self):
        batch_size = self.t.shape[0]
        dy = self.y - self.t
        dy /= batch_size
        return dy

class FullyConnectedLayer():
    def __init__(self, input_shape, output_shape):
        self.w = np.random.randn(input_shape, output_shape) * 0.01
        self.b = np.zeros(output_shape, dtype=np.float)
        self.x = None
        self.dw = None
        self.db = None

       
    def __call__(self, x):
        self.x = x
        out = np.dot(x,self.w)+self.b
        return out
    
    def backward(self, dout):
        dx = np.dot(dout,np.transpose(self.w))
        batch_size = dx.shape[0]
        self.dw = np.dot(np.transpose(self.x),dout)
        self.db =  np.sum(dout,axis=0)
        return dx

class ReLU():
    def __init__(self):
        self.mask = None

    def __call__(self, x):
        self.mask = (x <= 0)
        out = x.copy() 
        out[self.mask]=0 
        return out

    def backward(self, dout):
        dout[self.mask]=0 
        dx = dout

        return dx

class MLP_classifier():

    def __init__(self):
        '''
        
        x -> fc(784, 256) -> relu -> fc(256, 256) -> relu -> fc(256, 10) -> out
        '''
        
        # 層
        self.fc1 = FullyConnectedLayer(784, 256)
        self.relu1 = ReLU()
        self.fc2 = FullyConnectedLayer(256, 256)
        self.relu2 = ReLU()
        self.fc3 = FullyConnectedLayer(256, 10)
        self.out = None
        
        # 損失関数の定義
        self.criterion = SoftmaxCrossEntropyLoss()

    def forward(self, x):
        '''
        順伝播
        '''
        
        x = self.relu1(self.fc1(x))
        x = self.relu2(self.fc2(x))
        self.out = self.fc3(x)
        

        return self.out

    def backward(self, t):
        '''
        逆伝播
        '''
        
        # 誤差を計算
        loss = self.criterion(t, self.out)
        # 勾配を逆伝播
        d = self.criterion.backward()
        d = self.fc3.backward(d)
        d = self.relu2.backward(d)
        d = self.fc2.backward(d)
        d = self.relu1.backward(d)
        d = self.fc1.backward(d)
        
        return loss

    def optimize_GradientDecent(self, lr):
        '''
        勾配降下法による全層のパラメータの更新
        '''
        for fc in [self.fc1, self.fc2, self.fc3]:
            fc.w -= lr * fc.dw
            fc.b -= lr * fc.db

# モデルの宣言
model = MLP_classifier()

# 学習率
lr = 0.005
# 学習エポック数
n_epoch = 20


for n in range(n_epoch):
    # 訓練

    y = model.forward(X_train)
    loss = model.backward(T_train)
    model.optimize_GradientDecent(lr)
    
    # テスト
    y = model.forward(X_test)
    test_loss = model.backward(T_test)
    pred = softmax(y)
    accuracy = np.mean(np.equal(np.argmax(y, axis=1), np.argmax(T_test, axis=1)))
    print(f'EPOCH {n + 1} | TRAIN LOSS {loss:.5f} | TEST LOSS {test_loss:.5f} | ACCURACY {accuracy:.2%}')
classification_accuracy = accuracy

自分で試したこと

例題を元に作っており、正直どこをどう直していいのか分からないです。
softmaxの中は、axisを変えたり、計算を一つづつ書きくださしたりしましたが、
学習が進まないことに変わりありませんでした。

0

1Answer

考えられる原因

今回出たエラー

RuntimeWarning: invalid value encountered in subtract x = x-np.max(x,axis=0)

は翻訳してわかる通り,計算不可な値を使った減算をしようとしたことに起因するものです.実際コードを動かしてみるとnanを使った計算をしようとします.nanを使った計算の値はnanなので学習が進むわけがありません.

では,一体どこからnanが出てきたのでしょうか.

一般的に計算過程でnanが出る原因として考えられるのは,$\infty$が出た場合に多いというのがわかっています.そこで,今回のコードにおいて$\infty$が出そうなものは,指数と対数の計算が存在する箇所になると考えるのが良さそうです.

  • 指数: 肩の数字が大きすぎて$\infty$
    • ${\rm Softmax}(x_i)=\frac{e^{x_i}}{\sum_k e^{x_k}}$
    • ${\rm Sigmoid}(x)=(1+e^{-x})^{-1}$
    • etc...
  • 対数: 引数が0で$\infty$
    • Cross Entropy / Binary Cross Entropy

これに該当しない場合,

  • データセット自体に問題がある
  • メモリの範囲外アクセスによる$\infty$や$0$,巨大な値等の取得

などが挙げられます.

調査

今回,どのタイミングでnanが出るのかを知りたかったので,各epochで全結合層の重みの値域とsoftmax()の出力を観測しました.

その結果,学習が進まなくなった直後,レイヤfc1の重みwの最大値が指数関数的に上昇していくことがわかりました.$10[{\rm epoch]}$ぐらいでinfになる勢いです.

結論

入力データの一部の値の影響度が高いと判断され,入力側のレイヤfc1の重みがあらぬ方向へ学習したのが原因です.すなわち入力データをどうにかする必要があります.

今回入力に使った画像データセットXの値域が$0.0\sim255.0$となっており,とても広かったことが原因だと考えられます.

機械学習では,入力データの特徴量を機械にとって学習しやすいように加工する特徴量クレンジングや特徴量エンジニアリングといった前処理の技術が必要です.

解決策1

一般に画像処理における前処理では,画像データの値域が$[0, 255]$となっているケースがほとんどであることから,値域を$[0, 1]$の範囲に収めるような処理が欠かせません.

このような処理のことを正規化(Normalization)と言います1

特にmin-max正規化

$$
x' = \frac{x - \min(x)}{\max(x) - \min(x)}
$$

を行えば値域を$[0, 1]$にすることができます.したがって今回のコードではtrain_test_split()を行う前に

# 選択肢1: 値域を[0, 1]にする一般的なコード,今回は冗長
X = (X - X.min()) / (X.max() - X.min())

# 選択肢2: X.min()は0であることがわかっているので
X /= X.max()

# 選択肢3: X.max()は255であるのがわかっているので
X /= 255.0

# 選択肢4: [-1, 1]に正規化
X = X / 127.5 - 1

という前処理をする必要があります.値域は$[0, 1]$か$[-1, 1]$の好きな方を選んでください.

他にも標準化(Standardization/Z-score Normalization)を行うこともあります.こちらはデータ$x$の平均値$\overline{x}$と標準偏差$\sigma$を用いて

$$
x' = \frac{x - \overline{x}}{\sigma}
$$

と定義されます.今回のコードで書くと

X = (X - X.mean()) / X.std()

ですね2.平均$0$,分散$1$のデータに加工することができます.

解決策2

重みが無限大にまで増大してしまったので,重み増大を抑えるためペナルティとして損失関数に重み正則化項を加えることもできます.通常,L1正則化かL2正則化の項を損失関数に加算します.コードを大幅に変えることになるので具体例を示しません.

余談

関数FullyConnectedLayer()における重み初期値

$$
\mathbf{w}\sim\mathcal{N}(0, 0.01)
$$

では学習の収束に影響が出ます.今回,活性化関数に${\rm ReLU}(x)$を使っているので,活性化されるレイヤにおける入力数$n_\textrm{in}$を用いたHeの初期値3

$$
\mathbf{w}\sim\mathcal{N}\left(0, \sqrt{\frac{2}{n_\textrm{in}}}\right)
$$

を使った方が収束が早いです.

関数FullyConnectedLayer()における重み初期化を

-  self.w = np.random.randn(input_shape, output_shape) * 0.01
+  self.w = np.random.randn(input_shape, output_shape) * np.sqrt(2 / input_shape)

とすると適用できます.弊環境で入力の値域と重み初期値の違いによる学習速度を比較してみたので参考にしてください.

スクリーンショット 2022-09-18 5.59.02.png

重み初期値の違いだけで学習速度が全然違うことがわかると思います.原理等は下のリンクにある論文を参照してください.

nan以外にも学習が進まない原因は重み初期値にあるかも.と考え,余談とさせていただきます.

学習曲線がおかしいので,まだまだ改善の余地はありそうです.

  1. https://en.wikipedia.org/wiki/Feature_scaling

  2. 正規化はバッチ方向に各ピクセルでやらないといけない処理です.手を抜きました.

  3. http://arxiv.org/abs/1502.01852

2

Comments

  1. @kobaq

    Questioner
    ご教授ありがとうございます。質問サイトへの投稿は初めてで、ご回答とても嬉しいです。
    私もいつか回答する側になりたいです。

    いわゆる計算がオーバーフローという状態でしょうか。
    正規化はどこかで勉強したつもりでした、、、
    正則化はまた別の機会に勉強しなおします。アドバイスありがとうございます。

    教えて頂いた数値が大きくなり過ぎる事象については、x = x-np.max(x,axis=0)にて、
    最大値を0にしているつもりでした。expはマイナス側に大きい値は処理できるため、こちらで対処できていると考えておりました。
    質問:x-np.max できちんと0以下に出来ていないということでしょうか。

    余談について:Heの初期値 使ってみます。ありがとうございます。
  2. > 計算がオーバーフローという状態でしょうか。

    そういうことです.

    > x-np.max できちんと0以下に出来ていないということでしょうか。

    できています.今回の問題点はfowardingの際に出力側に与えられる値が大きくなったのではなくて,backwardingの際に入力層側の重みが増大したことが問題であるというだけです.
  3. @kobaq

    Questioner
    x-np.max できちんと0以下に出来ていない のではと考えて、
    ↓を参考にnp.max( , keepdims=True) を追加して次元が減らないように変更した所、
    問題なく学習できました。

    ご指摘頂いた所、もう一度勉強してみます。
    基礎からやり直した方が良さそうですね。色々教えて頂きありがとうございました。
  4. なるほど,バッチ全体の最大値じゃなくて,各バッチの最大値になるようにaxis=-1にしてkeepdimsあったほうが確かにいい感じになりそうですね,

    ただ,こちらで参考記事の記述を上のコードに反映させて,
    x = x - np.max(x, axis=-1, keepdims=True)
    と変更しただけでは変わらずnanになりました.オーバーフロー対策としているものの,値が小さすぎて単精度浮動小数点数で表現可能な3e-38辺りよりも小さくなった可能性があります.アンダーフローが起きる可能性だってあります(記事に対して「アンダーフローはいいのか?」とツッコみたいです).

    アンダーフローが起きている状態でbackpropagationで傾きを計算した場合,0.0と同じ意味を持つ5e-38や1e-40などの数値を使って差分を取っても0.0です.そのせいでSoftmaxCrossEntropyLoss()のbackward()のときのdy = self.y - self.tが0.0のままとなり,そうでない箇所が活性化されまくって入力側の重みが指数関数的に無限大まで増加する.といった感じだと考えられます.

    nanにならないためにはやはり,参考記事でもやっている通り,load_mnistのときにnormalizeしてることから,正規化周りの手順が大事そうですね,

    活性化関数の直後にBatchNormalizationを挿入するのも,本件に対する改善になるはずです.なんならnp.max()を使った0以下の数字に抑えるなどという動作も外せるはずです.

    勉強頑張ってください.本質問をクローズにしていただければ終了になります.

Your answer might help someone💌