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.
quelle
ce = x_j - log(sum_i(exp(x)))
Berechnung von hier, damit Sie nicht das Log des Exponentials (das häufig NaNs erzeugt) nehmenAntworten:
Kennen Sie "verschwindende" und "explodierende" Gradienten in der Backpropagation? Ich bin mit Haskell nicht allzu vertraut, daher kann ich nicht leicht erkennen, was genau Ihr Backprop tut, aber es sieht so aus, als würden Sie eine Logistikkurve als Aktivierungsfunktion verwenden.
Wenn Sie sich das Diagramm dieser Funktion ansehen, werden Sie feststellen, dass der Gradient dieser Funktion an den Enden nahezu 0 beträgt (da die Eingabewerte sehr groß oder sehr klein werden, ist die Steigung der Kurve fast flach), also multiplizieren oder dividieren Dies führt während der Backpropagation zu einer sehr großen oder sehr kleinen Anzahl. Wenn Sie dies wiederholt tun, während Sie mehrere Ebenen durchlaufen, nähern sich die Aktivierungen Null oder Unendlich. Da Backprop Ihre Gewichte während des Trainings aktualisiert, haben Sie viele Nullen oder Unendlichkeiten in Ihrem Netzwerk.
Lösung: Es gibt eine Vielzahl von Methoden, nach denen Sie suchen können, um das Problem des verschwindenden Gradienten zu lösen. Eine einfache Möglichkeit besteht darin, die Art der von Ihnen verwendeten Aktivierungsfunktion in eine nicht gesättigte zu ändern. ReLU ist eine beliebte Wahl, da es dieses spezielle Problem mildert (aber möglicherweise andere einführt).
quelle