Ich versuche, eine neuronale Netzwerkarchitektur in Haskell zu implementieren und auf MNIST zu verwenden.
Ich benutze das hmatrix
Paket für die lineare Algebra. Mein Trainingsrahmen wird mit dem pipes
Paket erstellt.
Mein Code wird kompiliert und stürzt nicht ab. Das Problem ist jedoch, dass bestimmte Kombinationen von Schichtgröße (z. B. 1000), Minibatch-Größe und Lernrate zu NaN
Werten in den Berechnungen führen. Nach einiger Inspektion sehe ich, dass extrem kleine Werte (Reihenfolge 1e-100
) schließlich in den Aktivierungen erscheinen. Aber selbst wenn das nicht passiert, funktioniert das Training immer noch nicht. Es gibt keine Verbesserung gegenüber dem Verlust oder der Genauigkeit.
Ich habe meinen Code überprüft und erneut überprüft und bin mir nicht sicher, wo die Ursache des Problems liegen könnte.
Hier ist das Backpropagation-Training, das die Deltas für jede Schicht berechnet:
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
ist die Verlustfunktion, n
ist das Netzwerk ( weight
Matrix und bias
Vektor für jede Schicht) out
und tar
ist die tatsächliche Ausgabe des Netzwerks und die target
(gewünschte) Ausgabe und das
sind die Aktivierungsableitungen jeder Schicht.
Im Batch - Modus out
, tar
sind Matrizen (Zeilen Ausgangsvektoren), und das
eine Liste der Matrizen.
Hier ist die eigentliche Gradientenberechnung:
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)
Hier lf
und n
wie oben i
ist die Eingabe und t
die Zielausgabe (beide in Stapelform als Matrizen).
squeeze
transformiert eine Matrix in einen Vektor durch Summieren über jede Zeile. Das heißt, es ds
handelt sich um eine Liste von Matrizen von Deltas, wobei jede Spalte den Deltas für eine Zeile des Minibatch entspricht. Die Gradienten für die Verzerrungen sind also der Durchschnitt der Deltas über das gesamte Minibatch. Das gleiche für gs
, was den Verläufen für die Gewichte entspricht.
Hier ist der eigentliche Update-Code:
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
ist die Lernrate. FC
ist der Ebenenkonstruktor und af
die Aktivierungsfunktion für diese Ebene.
Der Gradientenabstiegsalgorithmus stellt sicher, dass ein negativer Wert für die Lernrate übergeben wird. Der eigentliche Code für den Gradientenabstieg ist einfach eine Schleife um eine Zusammensetzung von grad
und move
mit einer parametrisierten Stoppbedingung.
Schließlich ist hier der Code für eine mittlere quadratische Fehlerverlustfunktion:
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
bündelt nur eine Verlustfunktion und ihre Ableitung (zur Berechnung des Deltas der Ausgangsschicht).
Der Rest des Codes ist auf GitHub: NeuralNetwork verfügbar .
Wenn also jemand einen Einblick in das Problem hat oder nur eine Überprüfung der Gesundheit, ob ich den Algorithmus korrekt implementiere, wäre ich dankbar.