考えられる原因
今回出たエラー
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)
とすると適用できます.弊環境で入力の値域と重み初期値の違いによる学習速度を比較してみたので参考にしてください.
重み初期値の違いだけで学習速度が全然違うことがわかると思います.原理等は下のリンクにある論文を参照してください.
nan
以外にも学習が進まない原因は重み初期値にあるかも.と考え,余談とさせていただきます.
学習曲線がおかしいので,まだまだ改善の余地はありそうです.
-
正規化はバッチ方向に各ピクセルでやらないといけない処理です.手を抜きました. ↩