私は Haskell でニューラル ネットワーク アーキテクチャを実装し、それを MNIST で使用しようとしています。
私はhmatrix
線形代数のパッケージを使用しています。私のトレーニング フレームワークは、このpipes
パッケージを使用して構築されています。
私のコードはコンパイルされ、クラッシュしません。しかし、問題は、レイヤー サイズ (たとえば、1000)、ミニバッチ サイズ、学習率の特定の組み合わせによって、NaN
計算で値が発生することです。調べてみると、1e-100
最終的には非常に小さな値 ( のオーダー) がアクティベーションに現れることがわかりました。しかし、それが起こらない場合でも、トレーニングは機能しません。損失や精度は改善されません。
コードを何度も確認しましたが、問題の根本が何なのかわかりません。
以下は、各レイヤーのデルタを計算するバックプロパゲーション トレーニングです。
backward lf n (out,tar) das = do
let δout = tr (derivate lf (tar, out)) -- dE/dy
deltas = scanr (\(l, a') δ ->
let w = weights l
in (tr a') * (w <> δ)) δout (zip (tail $ toList n) das)
return (deltas)
lf
は損失関数、n
はネットワーク(各層のweight
行列とベクトル) 、はネットワークの実際の出力と(望ましい)出力、およびは各層の活性化導関数です。bias
out
tar
target
das
バッチモードでは、out
はtar
行列(行は出力ベクトル)であり、das
は行列のリストです。
実際の勾配計算は次のとおりです。
grad lf (n, (i,t)) = do
-- Forward propagation: compute layers outputs and activation derivatives
let (as, as') = unzip $ runLayers n i
(out) = last as
(ds) <- backward lf n (out, t) (init as') -- Compute deltas with backpropagation
let r = fromIntegral $ rows i -- Size of minibatch
let gs = zipWith (\δ a -> tr (δ <> a)) ds (i:init as) -- Gradients for weights
return $ GradBatch ((recip r .*) <$> gs, (recip r .*) <$> squeeze <$> ds)
ここで、lf
およびn
は上記と同じで、i
は入力、t
はターゲット出力です (どちらもバッチ形式で、行列として)。
squeeze
は、各行を合計して行列をベクトルに変換します。つまり、はds
デルタの行列のリストであり、各列はミニバッチの行のデルタに対応します。したがって、バイアスの勾配は、すべてのミニバッチのデルタの平均です。 についても同じことが言えgs
、これは重みの勾配に対応します。
実際の更新コードは次のとおりです。
move lr (n, (i,t)) (GradBatch (gs, ds)) = do
-- Update function
let update = (\(FC w b af) g δ -> FC (w + (lr).*g) (b + (lr).*δ) af)
n' = Network.fromList $ zipWith3 update (Network.toList n) gs ds
return (n', (i,t))
lr
は学習率です。FC
はレイヤーコンストラクタであり、af
はそのレイヤーの活性化関数です。
勾配降下法アルゴリズムでは、学習率として必ず負の値が渡されます。勾配降下法の実際のコードは、パラメータ化された停止条件を持つ、grad
との合成をループするだけです。move
最後に、平均二乗誤差損失関数のコードを示します。
mse :: (Floating a) => LossFunction a a
mse = let f (y,y') = let gamma = y'-y in gamma**2 / 2
f' (y,y') = (y'-y)
in Evaluator f f'
Evaluator
損失関数とその導関数(出力層のデルタを計算するため)をバンドルするだけです。
残りのコードは GitHub に公開されています:ニューラルネットワーク。
この問題に関する洞察力をお持ちの方、あるいは私がアルゴリズムを正しく実装しているかどうかの妥当性チェックができる方はいらっしゃいますか?
ベストアンサー1
バックプロパゲーションにおける「消失」勾配と「爆発」勾配についてご存知ですか? 私は Haskell にあまり詳しくないので、バックプロパゲーションが正確に何をしているのかは簡単にはわかりませんが、活性化関数としてロジスティック曲線を使用しているように見えます。
この関数のプロットを見ると、この関数の勾配は端でほぼ 0 であることがわかります (入力値が非常に大きくなったり非常に小さくなったりすると、曲線の傾斜はほぼ平坦になります)。そのため、バックプロパゲーション中にこれを乗算または除算すると、非常に大きな数値または非常に小さな数値になります。複数のレイヤーを通過するときにこれを繰り返し実行すると、アクティベーションが 0 または無限大に近づきます。バックプロパゲーションはトレーニング中にこれを実行することで重みを更新するため、ネットワークには多くの 0 または無限大が存在します。
解決策: 消失勾配問題を解決するために検索できる方法は多数ありますが、簡単に試せる方法の 1 つは、使用している活性化関数の種類を非飽和のものに変更することです。ReLU は、この特定の問題を軽減するため (ただし、他の問題を引き起こす可能性があります)、人気のある選択肢です。