Ich versuche, eine neuronale Netzwerkarchitektur in Haskell zu implementieren und auf MNIST zu verwenden.
Ich benutze das hmatrixPaket für die lineare Algebra. Mein Trainingsrahmen wird mit dem pipesPaket 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 NaNWerten 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)
lfist die Verlustfunktion, nist das Netzwerk ( weightMatrix und biasVektor für jede Schicht) outund tarist die tatsächliche Ausgabe des Netzwerks und die target(gewünschte) Ausgabe und dassind die Aktivierungsableitungen jeder Schicht.
Im Batch - Modus out, tarsind Matrizen (Zeilen Ausgangsvektoren), und daseine 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 lfund nwie oben iist die Eingabe und tdie Zielausgabe (beide in Stapelform als Matrizen).
squeezetransformiert eine Matrix in einen Vektor durch Summieren über jede Zeile. Das heißt, es dshandelt 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))
lrist die Lernrate. FCist der Ebenenkonstruktor und afdie 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 gradund movemit 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.