Ich interessiere mich für Recurrent Neural Network, aber es fällt mir schwer, Code zu schreiben. Gibt es nicht viele Fälle wie diesen? Es gibt mehrere Gründe, aber in meinem Fall kann ich mir Folgendes vorstellen.
Übrigens beschäftigen sich Theanos Deep Learning und TensorFlows Tutorial mit Sprachmodellen. Diejenigen, die mit Sprachmodellen vertraut sind, können schnell beginnen, aber Anfänger müssen zuerst verstehen, "was das Beispiel zu lösen versucht".
Dieses Mal habe ich ein Beispiel für eine einfachere Folge von Zahlen aufgegriffen, die kein Sprachmodell ist, und mich für die Implementierung eines einfachen wiederkehrenden neuronalen Netzwerks (RNN) entschieden.
(Die verwendete Programmierumgebung ist Python 2.7.11, Theano 0.7.0.)
Bei der Untersuchung von RNN habe ich zuerst versucht, das Tutorial (ptb_word_lm.py) von "TensorFlow" auszuführen. Es ist ersichtlich, dass die Variable "Ratlosigkeit" mit zunehmendem Wert "Epoche" abnimmt. Ich konnte jedoch die Details dessen, was es löste, nicht verstehen. Da LSTM (Long Short-Term Memory) auch als Modell für RNN verwendet wird, war die Einführung in RNN meiner Meinung nach eine hohe Schwelle.
Elman Net wird als einfaches RNN im Dokument "Deep Learning" vorgestellt. Als ich nach "Elman RNN" als Schlüsselwort suchte, fand ich einen Blog namens "Peter's note" (http://peterroelants.github.io/), der einfache RNNs als Referenz einführt, und erstellte darauf basierend ein Programm. untersucht.
Die Zahl von RNN wird von der obigen Seite zitiert.
Fig. Simple RNN structure
Daten werden von der Eingabeeinheit x eingegeben und nach Multiplikation mit dem Gewicht W_in in die verborgene Schichteinheit s eingegeben. Es gibt einen rekursiven Fluss für die Ausgabe von Einheit S, und das Ergebnis der Anwendung des Gewichts W_rec kehrt beim nächsten Mal zu Einheit s zurück. Zusätzlich ist es normalerweise notwendig, das Gewicht W_out für die Ausgabe zu berücksichtigen, aber um die Struktur zu vereinfachen, wird der Zustand der Einheit S so ausgegeben, wie er ist, wenn W_out = 1,0 fest ist.
Betrachten Sie den "erweiterten" Zustand auf der rechten Seite, um die BPTT-Methode (Back Propagation through Time) auf den links gezeigten Zustand anzuwenden. Der Zustand des Anfangswertes s_0 der verborgenen Einheit ändert sich nach rechts, während das Gewicht W_rec mit fortschreitender Zeit multipliziert wird. Außerdem wird jedes Mal [x_1, x_2, ... x_n] eingegeben. Der Zustand von s_n wird zum letzten Zeitpunkt an die Einheit y ausgegeben.
Das oben gezeigte Modell kann wie folgt in Python-Code konvertiert werden. (Zitiert aus "Peters Notiz".)
def update_state(xk, sk, wx, wRec):
return xk * wx + sk * wRec
def forward_states(X, wx, wRec):
# Initialise the matrix that holds all states for all input sequences.
# The initial state s0 is set to 0.
S = np.zeros((X.shape[0], X.shape[1]+1))
# Use the recurrence relation defined by update_state to update the
# states trough time.
for k in range(0, X.shape[1]):
# S[k] = S[k-1] * wRec + X[k] * wx
S[:,k+1] = update_state(X[:,k], S[:,k], wx, wRec)
return S
Geben Sie außerdem in Bezug auf "Welche Art von Problem wird durch das obige RNN-Modell behandelt" einen binären numerischen Wert von X_k = 0 oder 1 als Eingabe ein. Die Ausgabe ist ein Netzwerkmodell, das den Gesamtwert dieser Binärdateien ausgibt. Zum Beispiel Für X = [0.1.0.0.0.0.0.0.0.0.0.0. 1.](weil der Gesamtwert dieser Liste X 2 ist) Stellen Sie den Ausgang von Y = 2. auf den richtigen Wert ein. Natürlich besteht der Inhalt des Beispiels darin, durch RNN (einschließlich zweier Gewichtskoeffizienten) zu schätzen, ohne den "Algorithmus zum Zählen numerischer Werte" zu verwenden.
Da der Ausgabewert ein numerischer Wert ist, der kontinuierliche Werte annimmt, kann er als eine Art "Rückgabe" -Problem betrachtet werden, nicht als "Klassifizierungs" -Problem. Daher wird MSE (mittlerer quadratischer Fehler) als Kostenfunktion verwendet, und die Einheitendaten werden unverändert übergeben, ohne die Aktivierungsfunktion zu übergeben.
Zunächst wird das Training unter Verwendung der Zugdaten (im Voraus erstellt) durchgeführt und der Gewichtungskoeffizient von 2 [W_in, W_rec] wird erhalten. Er kann leicht anhand der obigen Abbildung geschätzt werden, aber die richtige Antwort lautet "[W_in, W_rec] =." [1.0, 1.0] `.
In dem Artikel "Peters Notiz", auf den ich mich bezog, habe ich Python (mit Numpy) verwendet, um ein IPython-Notizbuch zusammenzustellen, ohne die Deep Learning-Bibliothek zu verwenden. Wenn dies so kopiert wird, wie es ist, entspricht das Ergebnis dem Blog-Artikel. In Anbetracht der Entwicklung haben wir versucht, es mithilfe der Deep Learning-Bibliothek zu implementieren. Ich habe Folgendes als Optionen betrachtet.
Zuerst habe ich versucht, den ursprünglichen Python-Code "eins nach dem anderen" in die TensorFlow-Version zu verwandeln.
for k in range(0, X.shape[1]):
# S[k] = S[k-1] * wRec + X[k] * wx
S[:,k+1] = update_state(X[:,k], S[:,k], wx, wRec)
return S
Es stellte sich heraus, dass die Schleifenverarbeitung des Teils von nicht gut behoben war (auf die TensorFlow-Version). Wenn Sie sich auf den Tutorial-Code von TensorFlow (ptb_word_lm.py usw.) beziehen, sollten Sie dieses einfache RNN-Modell selbstverständlich implementieren können. Da die zugehörige Klassenbibliothek jedoch kompliziert und schwer zu verstehen war, habe ich mich dieses Mal für TensorFlow entschieden. bestanden.
Außerdem wurden Optionen 3 wie "Keras" und "Pylearn2" dieses Mal nicht ausgewählt, da sie vom Zweck des "Verständnisses der Implementierung von RNN" abweichen.
Am Ende habe ich beschlossen, die "Theano" -Version des Codes für Option 2 zu schreiben.
Was dem RNN-Code von Theano im Internet gemeinsam ist, ist, dass der größte Teil des Codes "Theano-Scan" verwendet. Der Theano-Scan ist eine Funktion zum Durchführen einer Schleifenverarbeitung (Iterationsverarbeitung) und einer Iterationsverarbeitung (Konvergenzberechnung) innerhalb des Theano-Frameworks. Die Spezifikationen sind kompliziert und es ist schwierig, sie sofort zu verstehen, selbst wenn Sie sich die Originaldokumentation (Theano-Dokumentation) ansehen. Obwohl die japanischen Informationen recht begrenzt sind, fuhr ich mit der Verhaltensuntersuchung des Theano-Scans fort, während ich einen kleinen Code mit Jupyter Notebook ausprobierte, indem ich mich auf den Blog-Artikel von Herrn Sinhrks bezog.
n = T.iscalar('n')
result, updates = theano.scan(fn=lambda prior, nonseq: prior * 2,
sequences=None,
outputs_info=a, #Siehe den Wert in der vorherigen Schleife--> prior
non_sequences=a, #Nicht-Sequenzwert--> nonseq
n_steps=n)
myfun1 = theano.function(inputs=[a, n], outputs=result, updates=updates)
myfun1(5, 3)
# array([10, 20, 40])
# return-1 = 5 * 2
# return-2 = return-1 * 2
# return-3 = return-2 * 2
Ausführungsergebnis:
>>> array([10, 20, 40], dtype=int32)
Ich kann es nicht sehr detailliert erklären, daher werde ich einige Anwendungsbeispiele behandeln. Theano.scan () akzeptiert 5 Arten von Argumenten, wie oben beschrieben.
Key Word | Inhalt | Anwendungsbeispiel |
---|---|---|
fn | Funktionen für die iterative Verarbeitung | fn=lambda prior, nonseq: prior * 2 |
sequences | Listen Sie diese Eingaben auf, während Sie die Elemente während der sequentiellen Verarbeitung weiterentwickeln,Variable vom Matrixtyp | sequences=T.arange(x) |
outputs_info | Gibt den Anfangswert der sequentiellen Verarbeitung an | outputs_info=a |
non_sequences | Festwert, der keine Sequenz ist (unveränderlich bei iterativer Verarbeitung) | non_sequences=a |
n_steps | Iterative Funktion | n_steps=n |
Im obigen Code erhält theano.scan () einen Anfangswert von 5 (keine Sequenz) und mehrmals 3, und jede Iteration wird mit 2 multipliziert, um das Ergebnis des vorherigen Prozesses zu erhalten. Es gibt. Erste Iteration: 5 x 2 = 10 Zweite Iteration: 10 x 2 = 20 Dritte Iteration: 20 x 2 = 40 Als Ergebnis wird result = [10, 20, 40] berechnet.
Das Folgende ist ein Test, der etwas RNN-bewusster ist.
v = T.matrix('v')
s0 = T.vector('s0')
result, updates = theano.scan(fn=lambda seq, prior: seq + prior * 2,
sequences=v,
outputs_info=s0,
non_sequences=None)
myfun2 = theano.function(inputs=[v, s0], outputs=result, updates=updates)
myfun2([[1., 0.], [0., 1.], [1., 1.]], [0.5, 0.5])
Ausführungsergebnis:
>>> array([[ 2., 1.],
[ 4., 3.],
[ 9., 7.]], dtype=float32)
Der Anfangswert [0,5, 0,5] wird in die Funktion eingegeben.
"theano.scan ()" ist eine Funktion, die die Flusssteuerung der von RNN benötigten Verarbeitung unterstützt. Ähnliche Funktionen werden derzeit für TensorFlow nicht unterstützt.
Our white paper mentions a number of control flow operations that we've experimented with -- I think once we're happy with its API and confident in its implementation we will try to make it available through the public API -- we're just not quite there yet. It's still early days for us :)
(Zitiert aus der Diskussion in GitHub TensorFlow, Ausgabe Nr. 208.)
Deshalb möchte ich auf zukünftige Unterstützung warten.
(Ich verstehe nicht, welche Art von Implementierung für das RNN-Modell von TensorFlow durchgeführt wird, aber die Tatsache, dass die RNN-Berechnung bereits realisiert wurde, bedeutet, dass eine solche "theano.scan ()" -ähnliche Funktion "ist". Dies bedeutet, dass es nicht "wesentlich" ist. Ich denke, dass es in diesem Fall notwendig ist, den Beispielcode von TenforFlow etwas genauer zu studieren.)
Nachdem wir Theano Scan () kennen, schauen wir uns den einfachen RNN-Code an. Definieren Sie zunächst eine simpleRNN-Klasse.
class simpleRNN(object):
# members: slen : state length
# w_x : weight of input-->hidden layer
# w_rec : weight of recurrnce
def __init__(self, slen, nx, nrec):
self.len = slen
self.w_x = theano.shared(
np.asarray(np.random.uniform(-.1, .1, (nx)),
dtype=theano.config.floatX)
)
self.w_rec = theano.shared(
np.asarray(np.random.uniform(-.1, .1, (nrec)),
dtype=theano.config.floatX)
)
def state_update(self, x_t, s0):
# this is the network updater for simpleRNN
def inner_fn(xv, s_tm1, wx, wr):
s_t = xv * wx + s_tm1 * wr
y_t = s_t
return [s_t, y_t]
w_x_vec = T.cast(self.w_x[0], 'float32')
w_rec_vec = T.cast(self.w_rec[0], 'float32')
[s_t, y_t], updates = theano.scan(fn=inner_fn,
sequences=x_t,
outputs_info=[s0, None],
non_sequences=[w_x_vec, w_rec_vec]
)
return y_t
Als Klassenmitglied wird eine Klasse definiert, indem die Länge und das Gewicht (w_x, w_rec) des Status angegeben werden. Die Klassenmethode state_update () aktualisiert den Netzwerkstatus unter Berücksichtigung des Anfangswertes s0 des Zustands und der Eingabesequenz x_t und berechnet y_t (Ausgabesequenz). y_t ist ein Vektor, aber in der Hauptverarbeitung wird nur der Endwert extrahiert und zur Berechnung der Kostenfunktion verwendet, z. B. "y = y_t [-1]".
Im Hauptprozess werden zunächst die zum Lernen verwendeten Daten erstellt. (Fast wie im Original "Peters Notiz".)
np.random.seed(seed=1)
# Create Dataset by program
num_samples = 20
seq_len = 10
trX = np.zeros((num_samples, seq_len))
for row_idx in range(num_samples):
trX[row_idx,:] = np.around(np.random.rand(seq_len)).astype(int)
trY = np.sum(trX, axis=1)
trX = trX.astype(np.float32)
trX = trX.T # need 'List of vector' shape dataset
trY = trY.astype(np.float32)
# s0 is time-zero state
s0np = np.zeros((num_samples), dtype=np.float32)
trX ist eine Serie von Daten mit einer Länge von 10 und 20 Proben. Der Punkt hier ist, dass die Matrix als "trX = trX.T" transponiert wird. Als allgemeiner Datensatz für maschinelles Lernen scheinen die Merkmalsmengen eines Datums in horizontaler Richtung (Spalte) und in vertikaler Richtung (Zeile) für die Anzahl der Abtastwerte angeordnet zu sein.
Data Set Shape
feature1 feature2 feature3 ...
sample1: - - -
sample2: - - -
sample3: - - -
.
.
Dieses Mal war es jedoch beim Aktualisieren der Zeitreihendaten mit theano.scan () erforderlich, die Daten vertikal zu gruppieren und die Daten zu übergeben.
(Durch Gruppieren wie folgt, theano.scan()Es steht im Einklang mit dem Betrieb von. )
Data Set Shape (updated)
[ time1[sample1, time2[sample1, time3[sample1 ... ]
sample2, sample2, sample2,
sample3, sample3, sample3,
... ] ... ] ... ]
Um dies leicht zu realisieren, wird die Matrix transponiert und als Eingabe für theano.scan () verarbeitet.
Danach wird der Kostenverlust aus dem Theano-Diagramm, dem Modellberechnungswert y_hypo und der Zugdatenbezeichnung y_ berechnet.
# Tensor Declaration
x_t = T.matrix('x_t')
x = T.matrix('x')
y_ = T.vector('y_')
s0 = T.vector('s0')
y_hypo = T.vector('y_hypo')
net = simpleRNN(seq_len, 1, 1)
y_t = net.state_update(x_t, s0)
y_hypo = y_t[-1]
loss = ((y_ - y_hypo) ** 2).sum()
Sobald Sie diesen Punkt erreicht haben, können Sie auf vertraute Weise mit dem Lernen fortfahren.
# Train Net Model
params = [net.w_x, net.w_rec]
optimizer = GradientDescentOptimizer(params, learning_rate=1.e-5)
train_op = optimizer.minimize(loss)
# Compile ... define theano.function
train_model = theano.function(
inputs=[],
outputs=[loss],
updates=train_op,
givens=[(x_t, trX), (y_, trY), (s0, s0np)],
allow_input_downcast=True
)
n_epochs = 2001
epoch = 0
w_x_ini = (net.w_x).get_value()
w_rec_ini = (net.w_rec).get_value()
print('Initial weights: wx = %8.4f, wRec = %8.4f' \
% (w_x_ini, w_rec_ini))
while (epoch < n_epochs):
epoch += 1
loss = train_model()
if epoch % 100 == 0:
print('epoch[%5d] : cost =%8.4f' % (epoch, loss[0]))
w_x_final = (net.w_x).get_value()
w_rec_final = (net.w_rec).get_value()
print('Final weights : wx = %8.4f, wRec = %8.4f' \
% (w_x_final, w_rec_final))
Dieses Mal haben wir zwei Optimierer vorbereitet und verwendet, GradientDecent (Gradientenabstiegsmethode) und RMSPropOptimizer (RMSProp-Methode). (Der Code für den Optimiererteil wird dieses Mal weggelassen. Informationen zur RMSProp-Methode finden Sie auf der später gezeigten Website.)
Die Beschreibung, dass "RNN im Allgemeinen schwer zu erlernen ist", ist an verschiedenen Stellen zu finden, aber das Ergebnis hat es mir klar gemacht.
Initial weights: wx = 0.0900, wRec = 0.0113
epoch[ 100] : cost =529.6915
epoch[ 200] : cost =504.5684
epoch[ 300] : cost =475.3019
epoch[ 400] : cost =435.9507
epoch[ 500] : cost =362.6525
epoch[ 600] : cost = 0.2677
epoch[ 700] : cost = 0.1585
epoch[ 800] : cost = 0.1484
epoch[ 900] : cost = 0.1389
epoch[ 1000] : cost = 0.1300
epoch[ 1100] : cost = 0.1216
epoch[ 1200] : cost = 0.1138
epoch[ 1300] : cost = 0.1064
epoch[ 1400] : cost = 0.0995
epoch[ 1500] : cost = 0.0930
epoch[ 1600] : cost = 0.0870
epoch[ 1700] : cost = 0.0813
epoch[ 1800] : cost = 0.0760
epoch[ 1900] : cost = 0.0710
epoch[ 2000] : cost = 0.0663
Final weights : wx = 1.0597, wRec = 0.9863
Als Ergebnis des Lernens konnten wir einen ungefähren Wert der richtigen Antwort erhalten [w_x, w_rec] = [1.0, 1.0]. Die folgende Abbildung zeigt, wie die Kostenfunktion reduziert wird.
Fig. Loss curve (GradientDescent)
Initial weights: wx = 0.0900, wRec = 0.0113
epoch[ 100] : cost = 5.7880
epoch[ 200] : cost = 0.3313
epoch[ 300] : cost = 0.0181
epoch[ 400] : cost = 0.0072
epoch[ 500] : cost = 0.0068
epoch[ 600] : cost = 0.0068
epoch[ 700] : cost = 0.0068
epoch[ 800] : cost = 0.0068
epoch[ 900] : cost = 0.0068
epoch[ 1000] : cost = 0.0068
epoch[ 1100] : cost = 0.0068
epoch[ 1200] : cost = 0.0068
epoch[ 1300] : cost = 0.0068
epoch[ 1400] : cost = 0.0068
epoch[ 1500] : cost = 0.0068
epoch[ 1600] : cost = 0.0068
epoch[ 1700] : cost = 0.0068
epoch[ 1800] : cost = 0.0068
epoch[ 1900] : cost = 0.0068
epoch[ 2000] : cost = 0.0068
Final weights : wx = 0.9995, wRec = 0.9993
Fig. Loss curve (RMSProp)
In diesem Modell ist die Nichtlinearität der Kostenfunktion gegenüber den Parametern sehr stark. Da der numerische Wert divergiert, sobald die Lernrate erhöht wird, war es notwendig, die Lernrate bei der Gradientenabstiegsmethode auf 1,0e-5 einzustellen, was ziemlich klein ist. Andererseits kann mit der RMSProp-Methode, die für RNN geeignet sein soll, das Lernen auch bei einer Lernrate von 0,001 problemlos fortgesetzt werden.
(Ergänzung) Das Referenzblog "Peters Notiz" enthält eine detaillierte Erläuterung des Status der Kostenfunktion und von RMSProp (im Quellblog "Rprop" genannt). Die Nichtlinearität der Kostenfunktion wird mit Farbtönen dargestellt. Wenn Sie interessiert sind, lesen Sie diese bitte. (Es wird der Link unten sein.)
Recommended Posts