WHY
Ich denke, dass viele Menschen an Deep Learning interessiert sind, daher werde ich die Implementierung von Deep Learning im Dialog beschreiben.
Da die Chat-Antwort Chainer verwendet, werde ich mich auf diesen Teil konzentrieren. Bitte beachten Sie jedoch, dass die Version alt ist.
Die Version, deren Funktion bestätigt wurde, ist 1.5.1.
Es kann einige Fehler geben. Es gab einen Teil, den ich tief verstehen wollte, also folge ich einem Chainer-Code. Wir entschuldigen uns für die Unannehmlichkeiten, würden uns aber freuen, wenn Sie auf Fehler hinweisen könnten.
――Der auf der PyCon 2016 angekündigte Inhalt ist eher eine Konzept- und Gliederungsbasis, daher gibt es keine Erklärung für den tatsächlich implementierten Code, daher ist es besser, sich selbst zu reflektieren.
――So habe ich diesen Artikel geschrieben, weil ich möchte, dass mehr Menschen ihn verstehen und verwenden, indem sie eine Code-Erklärung hinzufügen. (Ich hoffe, wenn möglich, gibt es mehr Sterne auf Github)
Docker Hub
https://hub.docker.com/r/masayaresearch/dialogue/
github
https://github.com/SnowMasaya/Chainer-Slack-Twitter-Dialogue
Es gibt viele andere Bereiche wie Frage und Antwort, Themenklassifizierung und Parallelisierung der Datenerfassung, daher werde ich diesen Teil auf Anfrage schreiben.
WHAT
Wir trainieren an den klassifizierten Daten. Das Aufmerksamkeitsmodell wird auch beim tiefen Lernen verwendet. Was ist ein Aufmerksamkeitsmodell?
Bei der maschinellen Übersetzung neuronaler Netze hatte das Sequenz-zu-Sequenz-Modell das Problem, dass die Bedeutung des ersten Wortes durch die Akkumulation von Differenzierung verringert wurde, wenn es bei der Eingabe eines langen Satzes zu einem Vektor aggregiert wurde. Besonders auf Englisch wird das erste Wort wichtiger.
Um dies zu lösen, wurde in der Vergangenheit die Übersetzungsgenauigkeit durch Eingabe in die entgegengesetzte Richtung verbessert. Im Fall von Japanisch und Chinesisch ist im Gegenteil das letzte Wort wichtig, so dass es keine wesentliche Lösung ist.
Daher wurde das Aufmerksamkeitsmodell als ein Modell vorgeschlagen, das die Ausgabe jeder Decodierung durch Gewichtsmittelung der verborgenen Schicht und der Eingabe der Codierung, die der Decodierung entsprechen, vorhersagt, ohne die Eingabe separat zu codieren und zu decodieren. Ursprünglich war es auf dem Gebiet der Bilder erfolgreich, aber jetzt liefert es Ergebnisse in den Aufgaben der maschinellen Übersetzung und Satzzusammenfassung.
Bild
Um "mo" vorherzusagen, ist es die Nachwahrscheinlichkeit, wenn "I" eingegeben wird ("Ich bin Ingenieur"). Die hintere Wahrscheinlichkeit ist die Punktzahl des vorherigen Wortes (I), der Zustand der verborgenen Schicht und der Kontextvektor ("Ich bin ein Ingenieur"). Ignorieren Sie vorerst den Kontextvektor. Ich werde es später erklären. Die Funktion g ist im Allgemeinen eine Softmax-Funktion
Wie in der obigen Abbildung gezeigt, lautet die Formel, die bei der Vorhersage des aktuellen Zustands und Kontexts unter Berücksichtigung der vorherigen Ausgabe verwendet wird, wie folgt.
p(y_i|y_1,...y_{i_1}, \vec{x}) = g(y_{i-1}, s_i, c_i)
Hier kann der Zustand der verborgenen Schicht zum Zeitpunkt t wie folgt sein. (Zustand zur Vorhersage von "mo") Dies wird durch den Kontextvektor des vorherigen Wortes "I", des vorherigen Zustands und des vorherigen "Ich bin ein Ingenieur" bestimmt. Die Funktion f ist im Allgemeinen sigmoid
s_i=f(s_{i-1}, y_{i-1},c_i)
Der Kontextvektor wird durch die Summe der verborgenen Schicht des Encoderteils ("Ich bin ein Ingenieur") und des Gewichts $ a $ bestimmt.
c_{i} = \sum^{T_x}_{j=1}\alpha_{ij}h_{j}
Wie man dann das zuvor definierte Gewicht findet, wird das Gewicht, das aus der verborgenen Schicht h mit der Bezeichnung e und dem vorherigen Zustand s auf der Ausgabeseite ("I" im Fall von "") erhalten wird, als Punktzahl verwendet. Diese Form ist, weil h im Encoderteil eine spezielle Form hat. Dieser Punkt wird später beschrieben. Die Punktzahl e ist aufgrund der Wahrscheinlichkeit ein kleiner Wert. Sie wird durch die exp-Funktion zu einem großen Wert gemacht und durch alle Eingabeteile geteilt, um das Gewicht zu berechnen, das dem Paar von Eingabe und Ausgabe entspricht.
\alpha_{ij} = \frac{exp(e_{ij})}{\sum_{k=1}^{T_x}exp(e_{ik})} \\
e_{ij} = a(s_{i-1}, h_j)
Was ist das Besondere an der verborgenen Schicht h? Tatsächlich unterscheidet es sich von einer normalen Sequenz zu einer Sequenz darin, dass es vorwärts und rückwärts kombiniert. Definieren Sie vorwärts und rückwärts wie unten gezeigt und drücken Sie sie verkettet aus. Dies ist die verborgene Ebene der Codierungseingabe "Ich bin Ingenieur".
(\vec{h_1},...\vec{h_{T_x}})\\
(\overleftarrow{h_1},...\overleftarrow{h_{T_x}})\\
h_j = [\vec{h_j^T};\overleftarrow{h_j^T}]^T
Folgen wir, wie diese Formel tatsächlich in der Codebasis realisiert wird.
src_embed.py
Dies ist der Teil, der die Sprachdaten in den Raum des neuronalen Netzes verschiebt.
attention_encoder.py
Dies ist der Teil, der die Informationen weitergibt, die in den Raum des neuronalen Netzes der eingabeseitigen Sprache übertragen werden. (Es entspricht dem Sprachteil des Benutzers im Dialog)
attention.py
Der Teil, der Kontextinformationen erstellt
attention_decoder.py
Dies ist der neuronale Netzteil der Ausgabesprache. Es gibt sogar Kontextinformationen und Zielsprache aus und verbreitet verborgene Ebenen.
attention_dialogue.py
Modell laden
Modell speichern
Gewichtsinitialisierung
Einbetten von Gewichten --Codiervorgang --Decodierungsprozess
Es besteht aus den oben genannten fünf.
HOW
src_embed.py
Ich werde aus dem Teil erklären, der die Informationen der Eingabesprache einbettet. Legt das Vokabular der Eingabesprache und die Anzahl der eingebetteten Ebenen im neuronalen Netz fest. Das eingegebene Sprachvokabular ist die Sprache des Benutzers im Falle eines Dialogs.
def __init__(self, vocab_size, embed_size):
super(SrcEmbed, self).__init__(
weight_xi=links.EmbedID(vocab_size, embed_size),
)
Es wird der Inhalt einer bestimmten Verarbeitung sein.
W (~ chainer.Variable)
ist eine eingebettete Matrix von chainer.Variable.
Verwendet Anfangsgewichte, die aus einer Normalverteilung mit einem Mittelwert von 0 und einer Varianz von 1,0 generiert wurden.
def __init__(self, in_size, out_size, initialW=None, ignore_label=None):
super(EmbedID, self).__init__(W=(in_size, out_size))
if initialW is None:
initialW = initializers.Normal(1.0)
initializers.init_weight(self.W.data, initialW)
self.ignore_label = ignore_label
Insbesondere ist es der Teil, der Daten aus der Normalverteilung generiert. Da der "xp" -Teil bei der Verwendung von "gpu" verwendet wird, verwenden wir "xp.random.normal" anstelle von "numpy.random.normal".
Referenz https://docs.scipy.org/doc/numpy/reference/generated/numpy.random.normal.html
class Normal(initializer.Initializer):
def __init__(self, scale=0.05, dtype=None):
self.scale = scale
super(Normal, self).__init__(dtype)
def __call__(self, array):
xp = cuda.get_array_module(array)
array[...] = xp.random.normal(
loc=0.0, scale=self.scale, size=array.shape)
Das hier zurückzugebende Anfangsgewicht ist unten festgelegt. Die Daten von "Initialisierer" werden durch die Daten oder die Klasse von "numpy.ndarray" oder die Klasse von "cupy.ndarray" festgelegt.
def init_weight(weights, initializer, scale=1.0):
if initializer is None:
initializer = HeNormal(1 / numpy.sqrt(2))
elif numpy.isscalar(initializer):
initializer = Constant(initializer)
elif isinstance(initializer, numpy.ndarray):
initializer = Constant(initializer)
assert callable(initializer)
initializer(weights)
weights *= scale
Wenn initializer
nicht None
ist, wird ein Array im GPU-Format oder ein normales Array zurückgegeben.
class Constant(initializer.Initializer):
def __init__(self, fill_value, dtype=None):
self.fill_value = fill_value
super(Constant, self).__init__(dtype)
def __call__(self, array):
if self.dtype is not None:
assert array.dtype == self.dtype
xp = cuda.get_array_module(array)
array[...] = xp.asarray(self.fill_value)
Die Teile, die speziell beurteilt und zurückgegeben werden, sind wie folgt.
def get_array_module(*args):
if available:
return cupy.get_array_module(*args)
else:
return numpy
Die Funktion __call__
ruft src_embed auf, um die Eingabesprache in den Raum des neuronalen Netzes einzubetten.
Es wird mit einer bipolaren Funktion in functions.tanh
auf einen teilbaren Raum abgebildet. Wenn es sich um einen teilbaren Raum handelt, ist das Lernen durch Fehlerrückübertragung möglich.
def __call__(self, source):
return functions.tanh(self.weight_xi(source))
attention_encoder.py
Die im Raum des neuronalen Netzes abgebildete Eingangsschicht wird an die verborgene Schicht weitergeleitet. Warum 4 mal
Eingangstor Vergessenheitstor Ausgangsgatter Gate, das die vorherige Eingabe berücksichtigt
Dies liegt daran, dass die obigen vier berücksichtigt werden. Ich werde nicht ins Detail gehen, warum es notwendig ist, weil es in anderen Materialien erwähnt wird, aber dieses Gerät verhindert Überlernen.
def __init__(self, embed_size, hidden_size):
super(AttentionEncoder, self).__init__(
source_to_hidden=links.Linear(embed_size, 4 * hidden_size),
hidden_to_hidden=links.Linear(hidden_size, 4 * hidden_size),
)
Es ist ein spezifischer Links.Liner-Prozess.
def __init__(self, in_size, out_size, wscale=1, bias=0, nobias=False,
initialW=None, initial_bias=None):
super(Linear, self).__init__()
self.initialW = initialW
self.wscale = wscale
self.out_size = out_size
self._W_initializer = initializers._get_initializer(initialW, math.sqrt(wscale))
if in_size is None:
self.add_uninitialized_param('W')
else:
self._initialize_params(in_size)
if nobias:
self.b = None
else:
if initial_bias is None:
initial_bias = bias
bias_initializer = initializers._get_initializer(initial_bias)
self.add_param('b', out_size, initializer=bias_initializer)
def _initialize_params(self, in_size):
self.add_param('W', (self.out_size, in_size), initializer=self._W_initializer)
Dies ist ein spezifischer Initialisierungsprozess. Da die Skalierung standardmäßig 1 ist, multiplizieren Sie sie, um ein Array zu erstellen. Mit der zuvor veröffentlichten "Konstante" wird der Anfangswert mit einem festen Wert initialisiert und skaliert.
class _ScaledInitializer(initializer.Initializer):
def __init__(self, initializer, scale=1.0):
self.initializer = initializer
self.scale = scale
dtype = getattr(initializer, 'dtype', None)
super(Identity, self).__init__(dtype)
def __call__(self, array):
self.initializer(array)
array *= self.scale
def _get_initializer(initializer, scale=1.0):
if initializer is None:
return HeNormal(scale / numpy.sqrt(2))
if numpy.isscalar(initializer):
return Constant(initializer * scale)
if isinstance(initializer, numpy.ndarray):
return Constant(initializer * scale)
assert callable(initializer)
if scale == 1.0:
return initializer
return _ScaledInitializer(initializer, scale)
Wir übergeben den aktuellen Status, den Wert der vorherigen ausgeblendeten Ebene und den Wert der Eingabeebene.
def __call__(self, source, current, hidden):
return functions.lstm(current, self.source_to_hidden(source) + self.hidden_to_hidden(hidden))
Die Verarbeitung, die bei der Weiterleitung in der obigen lstm aufgerufen wird, ist wie folgt.
Die Datei lautet "chainer / functions / activity / lstm.py".
Der Eingang ist in vier Gates von lstm unterteilt.
len (x)
: Zeilenlänge abrufen
x.shape [1]
: Spaltenlänge abrufen
x.shape [2:]
: Wird für Daten mit 3 oder mehr Dimensionen verwendet
def _extract_gates(x):
r = x.reshape((len(x), x.shape[1] // 4, 4) + x.shape[2:])
return [r[:, :, i] for i in six.moves.range(4)]
CPU-Verarbeitung
--Eingabe und Status erhalten
Die Verarbeitung von gpu ist dieselbe. Da jedoch C ++ verwendet wird, wird es mit der folgenden Definition gelesen. Es ist dasselbe wie lstm, das in Python definiert ist, aber es ist für die Verarbeitung in C ++ geschrieben.
_preamble = '''
template <typename T> __device__ T sigmoid(T x) {
const T half = 0.5;
return tanh(x * half) * half + half;
}
template <typename T> __device__ T grad_sigmoid(T y) { return y * (1 - y); }
template <typename T> __device__ T grad_tanh(T y) { return 1 - y * y; }
#define COMMON_ROUTINE \
T aa = tanh(a); \
T ai = sigmoid(i_); \
T af = sigmoid(f); \
T ao = sigmoid(o);
'''
def forward(self, inputs):
c_prev, x = inputs
a, i, f, o = _extract_gates(x)
batch = len(x)
if isinstance(x, numpy.ndarray):
self.a = numpy.tanh(a)
self.i = _sigmoid(i)
self.f = _sigmoid(f)
self.o = _sigmoid(o)
c_next = numpy.empty_like(c_prev)
c_next[:batch] = self.a * self.i + self.f * c_prev[:batch]
h = self.o * numpy.tanh(c_next[:batch])
else:
c_next = cuda.cupy.empty_like(c_prev)
h = cuda.cupy.empty_like(c_next[:batch])
cuda.elementwise(
'T c_prev, T a, T i_, T f, T o', 'T c, T h',
'''
COMMON_ROUTINE;
c = aa * ai + af * c_prev;
h = ao * tanh(c);
''',
'lstm_fwd', preamble=_preamble)(
c_prev[:batch], a, i, f, o, c_next[:batch], h)
c_next[batch:] = c_prev[batch:]
self.c = c_next[:batch]
return c_next, h
Die Verarbeitung von GPU ist wie folgt. Der Prozess zum Aufrufen des Inhalts von cuda ist wie folgt. Ich benutze Cupy. Über Cupy
http://docs.chainer.org/en/stable/cupy-reference/overview.html
Erstellen Sie unten eine Kernelfunktion, speichern Sie sie im Speicher von cuda und verknüpfen Sie das Ergebnis mit dem Gerät von cuda. Im Folgenden finden Sie die Gründe, warum die im GPU-Speicher berechneten Werte verknüpft werden müssen.
http://www.nvidia.com/docs/io/116711/sc11-cuda-c-basics.pdf
@memoize(for_each_device=True)
def elementwise(in_params, out_params, operation, name, **kwargs):
check_cuda_available()
return cupy.ElementwiseKernel(
in_params, out_params, operation, name, **kwargs)
Im Fall von Rückwärts ist die Verarbeitung wie folgt.
Dies ist hilfreich, da der Kettenhändler die Verarbeitung hier versteckt.
Wie bei der Vorwärtsverarbeitung, jedoch besteht der Unterschied darin, dass nicht nur die Eingabe, sondern auch die Gradientenausgabe verwendet wird.
Mit gc_prev [: batch]
wird das Produkt der verborgenen Schicht und der Ausgabeschicht aktualisiert, indem der Verlauf zur Stapelgröße hinzugefügt wird.
Der Gradient wird mit "_grad_tanh" und "_grad_sigmoid" berechnet und aktualisiert.
co = numpy.tanh(self.c)
gc_prev = numpy.empty_like(c_prev)
# multiply f later
gc_prev[:batch] = gh * self.o * _grad_tanh(co) + gc_update
gc = gc_prev[:batch]
ga[:] = gc * self.i * _grad_tanh(self.a)
gi[:] = gc * self.a * _grad_sigmoid(self.i)
gf[:] = gc * c_prev[:batch] * _grad_sigmoid(self.f)
go[:] = gh * co * _grad_sigmoid(self.o)
gc_prev[:batch] *= self.f # multiply f here
gc_prev[batch:] = gc_rest
Es ist der Verarbeitungsteil von GPU. Entspricht der Behandlung von CPU, wird jedoch mit "cuda.elementwise" berechnet, um C ++ zu übergeben.
a, i, f, o = _extract_gates(x)
gc_prev = xp.empty_like(c_prev)
cuda.elementwise(
'T c_prev, T c, T gc, T gh, T a, T i_, T f, T o',
'T gc_prev, T ga, T gi, T gf, T go',
'''
COMMON_ROUTINE;
T co = tanh(c);
T temp = gh * ao * grad_tanh(co) + gc;
ga = temp * ai * grad_tanh(aa);
gi = temp * aa * grad_sigmoid(ai);
gf = temp * c_prev * grad_sigmoid(af);
go = gh * co * grad_sigmoid(ao);
gc_prev = temp * af;
''',
'lstm_bwd', preamble=_preamble)(
c_prev[:batch], self.c, gc_update, gh, a, i, f, o,
gc_prev[:batch], ga, gi, gf, go)
gc_prev[batch:] = gc_rest
attention.py
Dies ist der Teil, der die Kontextinformationen enthält.
--annotion_weight
ist das Gewicht des vorderen Teils
--back_weight
ist das Gewicht des rückwärtigen Teils,
--pw
ist das Gewicht der aktuellen Schicht
--Einstellungen, damit weight_exponential
die exp-Funktion im neuronalen Netz verarbeiten kann
def __init__(self, hidden_size):
super(Attention, self).__init__(
annotion_weight=links.Linear(hidden_size, hidden_size),
back_weight=links.Linear(hidden_size, hidden_size),
pw=links.Linear(hidden_size, hidden_size),
weight_exponential=links.Linear(hidden_size, 1),
)
self.hidden_size = hidden_size
Eine Liste von Wörtern, deren "annotion_list" vorwärts ist
back_word_list
ist eine Liste von rückwärts gerichteten Wörtern
"p" ist das Gewicht der aktuellen Schicht
def __call__(self, annotion_list, back_word_list, p):
Initialisierung für die Stapelverarbeitung
batch_size = p.data.shape[0]
exponential_list = []
sum_exponential = XP.fzeros((batch_size, 1))
Erstellen Sie eine Gewichtung, die die Vorwärtswortliste, die Rückwortwortliste und den aktuellen Ebenenstatus kombiniert Entspricht dem Folgenden
e_{ij} = a(s_{i-1}, h_j)
Listen Sie jeden Wert auf, indem Sie der exp-Funktion erlauben, die dort erhaltenen Werte zu verarbeiten. Berechnen Sie auch den Gesamtwert
\alpha_{ij} = \frac{\exp(e_{ij})}{\sum_{k=1}^{T_x}\exp(e_{ik})} \\
Da die Verarbeitung aus beiden Richtungen erfolgt, werden die Anmerkungsliste aus der vorderen Richtung und die rückwärtige Liste aus der hinteren Richtung erfasst und das Gewicht einschließlich des aktuellen Gewichts berechnet. Erstellen Sie eine Gewichtsliste für die exp-Funktion. Berechnen Sie die Summe der Exp-Funktionen
for annotion, back_word in zip(annotion_list, back_word_list):
weight = functions.tanh(self.annotion_weight(annotion) + self.back_weight(back_word) + self.pw(p))
exponential = functions.exp(self.weight_exponential(weight))
exponential_list.append(exponential)
sum_exponential += exponential
Die Initialisierung wird durchgeführt, die Vorwärts- und Rückwärtsgewichte werden berechnet und die von der Vorwärts- und Rückwärtsmatrix berechneten Werte werden für die Chargengröße vorbereitet und zurückgegeben. Die Matrixberechnung erfolgt mit functions.batch_matmul
.
"a" ist die linke Matrix
b
ist die richtige Matrix
Wenn es eine "Transa" gibt, wird die linke Matrix transponiert.
Wenn es ein "transb" gibt, transponieren Sie die richtige Matrix
def batch_matmul(a, b, transa=False, transb=False):
return BatchMatMul(transa=transa, transb=transb)(a, b)
Inhalt der eigentlichen Matrixberechnung
a = a.reshape(a.shape[:2] + (-1,))
Wenn es eine Linie wie die folgende gibt
array([[1, 2, 3],
[4, 5, 6],
[3, 4, 5]])
Es wird wie folgt konvertiert.
array([[[1],
[2],
[3]],
[[4],
[5],
[6]],
[[3],
[4],
[5]]])
--Wenn eine Translokation erforderlich ist, verarbeiten Sie diese.
matmul
, um die Matrix zu berechnen. matmul
erlaubt keine skalaren Berechnungen und Prozesse durch Stapeln von Matrizen auf einem Stapel.def _batch_matmul(a, b, transa=False, transb=False, transout=False):
a = a.reshape(a.shape[:2] + (-1,))
b = b.reshape(b.shape[:2] + (-1,))
trans_axis = (0, 2, 1)
if transout:
transa, transb = not transb, not transa
a, b = b, a
if transa:
a = a.transpose(trans_axis)
if transb:
b = b.transpose(trans_axis)
xp = cuda.get_array_module(a)
if xp is numpy:
ret = numpy.empty(a.shape[:2] + b.shape[2:], dtype=a.dtype)
for i in six.moves.range(len(a)):
ret[i] = numpy.dot(a[i], b[i])
return ret
return xp.matmul(a, b)
Initialisiert mit einer Nullmatrix für die Stapelgröße und die Matrixgröße und gibt die durch "annotion" und "back_word" berechnete Summe zurück.
ZEROS = XP.fzeros((batch_size, self.hidden_size))
annotion_value = ZEROS
back_word_value = ZEROS
# Calculate the Convolution Value each annotion and back word
for annotion, back_word, exponential in zip(annotion_list, back_word_list, exponential_list):
exponential /= sum_exponential
annotion_value += functions.reshape(functions.batch_matmul(annotion, exponential), (batch_size, self.hidden_size))
back_word_value += functions.reshape(functions.batch_matmul(back_word, exponential), (batch_size, self.hidden_size))
return annotion_value, back_word_value
attention_decoder.py
Es wird der Ausgabeteil sein. Im Falle eines Dialogs ist es die Antwort des Systems.
Es ist komplizierter als das Tippen.
embedded_vocab
: Der Teil, der die Ausgabesprache dem Raum des neuronalen Netzes zuordnet
embedded_hidden
: Der Teil, der den Wert des neuronalen Netzes an LSTM weitergibt
hidden_hidden
: Ausbreitungsteil der versteckten Ebene
annotation_hidden
: Kontextvektor vom Forward-Typ
back_word_hidden
: Kontextvektor vom Typ Backword
hidden_embed
: Ausbreitung von der versteckten Schicht zur Ausgabeschicht (entsprechend der Systemantwort)
embded_target
: Ausbreitung von der Ausgabeschicht zur Systemausgabe (entsprechend der Systemantwort)
super(AttentionDecoder, self).__init__(
embed_vocab=links.EmbedID(vocab_size, embed_size),
embed_hidden=links.Linear(embed_size, 4 * hidden_size),
hidden_hidden=links.Linear(hidden_size, 4 * hidden_size),
annotation_hidden=links.Linear(embed_size, 4 * hidden_size),
back_word_hidden=links.Linear(hidden_size, 4 * hidden_size),
hidden_embed=links.Linear(hidden_size, embed_size),
embded_target=links.Linear(embed_size, vocab_size),
)
Verwenden Sie eine teilbare bipolare Funktion, die das Ausgabewort einer verborgenen Ebene zuordnet Prognostizieren Sie den Zustand und die verborgene Ebene, indem Sie lsm die Summe der verborgenen Ebenen, verborgenen Ebenen, Kontextvektoren vorwärts und Kontextvektoren rückwärts der Ausgabewörter geben Vorhersage der verborgenen Schicht für die Ausgabe mit einer teilbaren bipolaren Funktion unter Verwendung der zuvor vorhergesagten verborgenen Schicht Prognostizieren Sie Ausgabewörter mithilfe der verborgenen Ebene für die Ausgabe, geben Sie den aktuellen Status und die verborgene Ebene zurück
embed = functions.tanh(self.embed_vocab(target))
current, hidden = functions.lstm(current, self.embed_hidden(embed) + self.hidden_hidden(hidden) +
self.annotation_hidden(annotation) + self.back_word_hidden(back_word))
embed_hidden = functions.tanh(self.hidden_embed(hidden))
return self.embded_target(embed_hidden), current, hidden
attention_dialogue.py
Dies ist der Teil, der eine bestimmte Dialogverarbeitung durchführt.
Wir werden die vier zuvor beschriebenen Modelle verwenden.
Verwenden Sie "emb", um die Eingabesprache in den Raum des neuronalen Netzes abzubilden.
forward_encode
: Codierung weiterleiten und den Kontextvektor für die Erstellung vorbereiten.
back_encdode
: Rückwärtscodiert, um den Kontextvektor für die Erstellung vorzubereiten.
Aufmerksamkeit
: Auf Aufmerksamkeit vorbereitet
dec
: Vorbereitet für Wörter zur Ausgabe
Es bestimmt die Größe des Vokabulars, die Größe, die dem Raum des neuronalen Netzes zugeordnet werden soll, die Größe der verborgenen Ebene und ob GPU in XP verwendet werden soll.
super(AttentionDialogue, self).__init__(
emb=SrcEmbed(vocab_size, embed_size),
forward_encode=AttentionEncoder(embed_size, hidden_size),
back_encdode=AttentionEncoder(embed_size, hidden_size),
attention=Attention(hidden_size),
dec=AttentionDecoder(vocab_size, embed_size, hidden_size),
)
self.vocab_size = vocab_size
self.embed_size = embed_size
self.hidden_size = hidden_size
self.XP = XP
Es wird auf einen Gradienten von Null initialisiert.
def reset(self):
self.zerograds()
self.source_list = []
Es enthält die Eingabesprache (Sprache des Benutzers) als Wortliste.
def embed(self, source):
self.source_list.append(self.emb(source))
encode
Dies ist der Teil für die Verarbeitung. Nur der eindimensionale Teil der Eingabesprache wird verwendet, um die Stapelgröße zu erhalten.
Zahl
Ich initialisiere, aber da der Initialisierungswert zwischen gpu und cpu unterschiedlich ist, verwende ich self.XP.fzeros
.
Ich erhalte eine Liste von Weiterleitungen, um einen Vorwärtskontextvektor zu erstellen.
Rückwärts macht das gleiche.
def encode(self):
batch_size = self.source_list[0].data.shape[0]
ZEROS = self.XP.fzeros((batch_size, self.hidden_size))
context = ZEROS
annotion = ZEROS
annotion_list = []
# Get the annotion list
for source in self.source_list:
context, annotion = self.forward_encode(source, context, annotion)
annotion_list.append(annotion)
context = ZEROS
back_word = ZEROS
back_word_list = []
# Get the back word list
for source in reversed(self.source_list):
context, back_word = self.back_encdode(source, context, back_word)
back_word_list.insert(0, back_word)
self.annotion_list = annotion_list
self.back_word_list = back_word_list
self.context = ZEROS
self.hidden = ZEROS
Holen Sie sich den Kontextvektor für jede der vorwärts, rückwärts und verborgenen Aufmerksamkeitsebenen. Gibt das Ausgabewort mit dem Zielwort, dem Kontext (erhalten durch dec), dem Wert der verborgenen Ebene, dem Vorwärtswert und dem Rückwärtswert zurück.
def decode(self, target_word):
annotion_value, back_word_value = self.attention(self.annotion_list, self.back_word_list, self.hidden)
target_word, self.context, self.hidden = self.dec(target_word, self.context, self.hidden, annotion_value, back_word_value)
return target_word
Modell speichern Es speichert die Vokabulargröße, die Zuordnungsgröße der latenten Ebene und die Größe der verborgenen Ebene.
def save_spec(self, filename):
with open(filename, 'w') as fp:
print(self.vocab_size, file=fp)
print(self.embed_size, file=fp)
print(self.hidden_size, file=fp)
Der Ladeteil des Modells. Der hier gelesene Wert wird erfasst und an das Modell übergeben.
def load_spec(filename, XP):
with open(filename) as fp:
vocab_size = int(next(fp))
embed_size = int(next(fp))
hidden_size = int(next(fp))
return AttentionDialogue(vocab_size, embed_size, hidden_size, XP)
EncoderDecoderModelAttention.py
Verwenden Sie tatsächlich das zuvor in diesem Teil erläuterte Modul Es werden verschiedene Parameter eingestellt.
def __init__(self, parameter_dict):
self.parameter_dict = parameter_dict
self.source = parameter_dict["source"]
self.target = parameter_dict["target"]
self.test_source = parameter_dict["test_source"]
self.test_target = parameter_dict["test_target"]
self.vocab = parameter_dict["vocab"]
self.embed = parameter_dict["embed"]
self.hidden = parameter_dict["hidden"]
self.epoch = parameter_dict["epoch"]
self.minibatch = parameter_dict["minibatch"]
self.generation_limit = parameter_dict["generation_limit"]
self.word2vec = parameter_dict["word2vec"]
self.word2vecFlag = parameter_dict["word2vecFlag"]
self.model = parameter_dict["model"]
self.attention_dialogue = parameter_dict["attention_dialogue"]
XP.set_library(False, 0)
self.XP = XP
Implementierung der Vorwärtsverarbeitung. Es erhält die Größe des Ziels und der Quelle und erhält den Index von jedem.
def forward_implement(self, src_batch, trg_batch, src_vocab, trg_vocab, attention, is_training, generation_limit):
batch_size = len(src_batch)
src_len = len(src_batch[0])
trg_len = len(trg_batch[0]) if trg_batch else 0
src_stoi = src_vocab.stoi
trg_stoi = trg_vocab.stoi
trg_itos = trg_vocab.itos
attention.reset()
Die Eingabesprache wird aus der entgegengesetzten Richtung eingegeben. Wenn Sie aus der entgegengesetzten Richtung eingeben, wird das Ergebnis der maschinellen Übersetzung verbessert, sodass der Dialog dieselbe Form hat, aber ich denke, dass er keine Auswirkungen hat.
x = self.XP.iarray([src_stoi('</s>') for _ in range(batch_size)])
attention.embed(x)
for l in reversed(range(src_len)):
x = self.XP.iarray([src_stoi(src_batch[k][l]) for k in range(batch_size)])
attention.embed(x)
attention.encode()
Initialisieren Sie die Zielsprachenzeichenfolge, die Sie erhalten möchten, mit .
t = self.XP.iarray([trg_stoi('<s>') for _ in range(batch_size)])
hyp_batch = [[] for _ in range(batch_size)]
Dies ist der Lernteil. Sprachinformationen können nur gelernt werden, wenn es sich um Indexinformationen handelt. Verwenden Sie daher "stoi", um die Sprache in Indexinformationen zu ändern. Holen Sie sich das Ziel (in diesem Fall die Ausgabe des Dialogs) und vergleichen Sie es mit den richtigen Daten, um die Kreuzentropie zu berechnen. Da die Kreuzentropie den Abstand zwischen den Wahrscheinlichkeitsverteilungen angibt, ist ersichtlich, dass das Ausgabeergebnis umso näher am Ziel liegt, je kleiner der Verlust dieser Berechnung ist. Es gibt hypothetische Kandidaten und berechnete Verluste zurück.
if is_training:
loss = self.XP.fzeros(())
for l in range(trg_len):
y = attention.decode(t)
t = self.XP.iarray([trg_stoi(trg_batch[k][l]) for k in range(batch_size)])
loss += functions.softmax_cross_entropy(y, t)
output = cuda.to_cpu(y.data.argmax(1))
for k in range(batch_size):
hyp_batch[k].append(trg_itos(output[k]))
return hyp_batch, loss
Dies ist der Testteil. Das neuronale Netz kann unendliche Kandidaten erzeugen, und insbesondere im Fall des lstm-Modells kann es, da es den vergangenen Zustand verwendet, in eine Endlosschleife eintreten, so dass es begrenzt ist. Ausgabe mit der initialisierten Zielwortzeichenfolge. Der Maximalwert der Ausgabedaten wird ausgegeben und "t" wird aktualisiert. Die für die Stapelgröße ausgegebenen Kandidaten werden von Indexinformationen in Sprachinformationen konvertiert. Unterbrechen Sie den Prozess, wenn alle Kandidaten mit einem Kündigungssymbol enden.
else:
while len(hyp_batch[0]) < generation_limit:
y = attention.decode(t)
output = cuda.to_cpu(y.data.argmax(1))
t = self.XP.iarray(output)
for k in range(batch_size):
hyp_batch[k].append(trg_itos(output[k]))
if all(hyp_batch[k][-1] == '</s>' for k in range(batch_size)):
break
return hyp_batch
Es ist die Verarbeitung des gesamten Lernens.
Initialisiert Eingabe- und Ausgabeäußerungen.
self.vocab
generiert einen Generator mit gens.word_list
im gesamten Vokabular.
src_vocab = Vocabulary.new(gens.word_list(self.source), self.vocab)
trg_vocab = Vocabulary.new(gens.word_list(self.target), self.vocab)
Ich erstelle Vokabularinformationen für Eingabe- und Ausgabeäußerungen mit Vocabulary.new ()
.
Erstellen Sie den folgenden "Generator" mit "gens.word_list (self.source)". Der Name der Eingabedatei wird in "self.source" angegeben.
def word_list(filename):
with open(filename) as fp:
for l in fp:
yield l.split()
Das Konvertieren von Vokabularinformationen in Indexinformationen wird im folgenden Teil durchgeführt.
<Unk>
es ist 0 in dem unbekannten Wort, <s>
1 mit dem Präfix </ s>
hat die 2 am Ende des Satzes gesetzt.
Da die Werte im Voraus festgelegt werden, wird +3 hinzugefügt, sodass der Index hinter dem reservierten Wort steht.
@staticmethod
def new(list_generator, size):
self = Vocabulary()
self.__size = size
word_freq = defaultdict(lambda: 0)
for words in list_generator:
for word in words:
word_freq[word] += 1
self.__stoi = defaultdict(lambda: 0)
self.__stoi['<unk>'] = 0
self.__stoi['<s>'] = 1
self.__stoi['</s>'] = 2
self.__itos = [''] * self.__size
self.__itos[0] = '<unk>'
self.__itos[1] = '<s>'
self.__itos[2] = '</s>'
for i, (k, v) in zip(range(self.__size - 3), sorted(word_freq.items(), key=lambda x: -x[1])):
self.__stoi[k] = i + 3
self.__itos[i + 3] = k
return self
Ein Aufmerksamkeitsmodell erstellen. Gibt Vokabeln, eingebettete Ebenen, versteckte Ebenen und "XP". "XP" ist der Teil, der CPU- und GPU-Berechnungen durchführt.
trace('making model ...')
self.attention_dialogue = AttentionDialogue(self.vocab, self.embed, self.hidden, self.XP)
Es wird Teil des Transferlernens sein. Hier wird das von word2vec erzeugte Gewicht übertragen. Da der Name des Gewichts des mit word2vec erstellten Modells "weight_xi" lautet, wird die Eingabeäußerung vereinheitlicht, der Teil der Ausgabeäußerung unterscheidet sich jedoch für "embded_target", sodass die folgende Verarbeitung enthalten ist. Der [0] Teil ist der Name des Gewichts Der [1] Teil ist der Wert.
if dst["embded_target"] and child.name == "weight_xi" and self.word2vecFlag:
for a, b in zip(child.namedparams(), dst["embded_target"].namedparams()):
b[1].data = a[1].data
Dies ist eine Kopie des Gewichts.
Drehen Sie die Iteration des Originalteils und kopieren Sie die Gewichte, wenn die Bedingungen erfüllt sind.
Bedingung 1: Es gibt etwas, das mit dem Namen des Gewichts übereinstimmt
Bedingung 2: Die Gewichtsarten sind gleich
Bedingung 3: Der Teil von link.Link
, dh der Modellteil, wurde erreicht.
Bedingung 4: Die Länge der Modellgewichtsmatrix ist gleich
def copy_model(self, src, dst, dec_flag=False):
print("start copy")
for child in src.children():
if dec_flag:
if dst["embded_target"] and child.name == "weight_xi" and self.word2vecFlag:
for a, b in zip(child.namedparams(), dst["embded_target"].namedparams()):
b[1].data = a[1].data
print('Copy weight_jy')
if child.name not in dst.__dict__: continue
dst_child = dst[child.name]
if type(child) != type(dst_child): continue
if isinstance(child, link.Chain):
self.copy_model(child, dst_child)
if isinstance(child, link.Link):
match = True
for a, b in zip(child.namedparams(), dst_child.namedparams()):
if a[0] != b[0]:
match = False
break
if a[1].data.shape != b[1].data.shape:
match = False
break
if not match:
print('Ignore %s because of parameter mismatch' % child.name)
continue
for a, b in zip(child.namedparams(), dst_child.namedparams()):
b[1].data = a[1].data
print('Copy %s' % child.name)
if self.word2vecFlag:
self.copy_model(self.word2vec, self.attention_dialogue.emb)
self.copy_model(self.word2vec, self.attention_dialogue.dec, dec_flag=True)
Erstellen Sie einen Generator für Eingabe- und Ausgabeäußerungen.
gen1 = gens.word_list(self.source)
gen2 = gens.word_list(self.target)
gen3 = gens.batch(gens.sorted_parallel(gen1, gen2, 100 * self.minibatch), self.minibatch)
Erstellen Sie beide für die Stapelgröße. Erstellen Sie die Stapelgröße im folgenden Tapple-Format.
def batch(generator, batch_size):
batch = []
is_tuple = False
for l in generator:
is_tuple = isinstance(l, tuple)
batch.append(l)
if len(batch) == batch_size:
yield tuple(list(x) for x in zip(*batch)) if is_tuple else batch
batch = []
if batch:
yield tuple(list(x) for x in zip(*batch)) if is_tuple else batch
Eingabe- und Ausgabeäußerungen werden erstellt und nach Chargengrößen sortiert.
def sorted_parallel(generator1, generator2, pooling, order=1):
gen1 = batch(generator1, pooling)
gen2 = batch(generator2, pooling)
for batch1, batch2 in zip(gen1, gen2):
#yield from sorted(zip(batch1, batch2), key=lambda x: len(x[1]))
for x in sorted(zip(batch1, batch2), key=lambda x: len(x[order])):
yield x
Adagrad wird zur Optimierung verwendet. Bei dieser Methode wird die Aktualisierungsbreite kleiner, wenn sich die Anzahl der Aktualisierungen ansammelt.
r ← r + g^2_{\vec{w}}\\
w ← w - \frac{\alpha}{r + }g^2_{\vec{w}}
optimizer.GradientClipping (5)
verwendet die L2-Regularisierung, um den Gradienten innerhalb eines bestimmten Bereichs zu halten.
opt = optimizers.AdaGrad(lr = 0.01)
opt.setup(self.attention_dialogue)
opt.add_hook(optimizer.GradientClipping(5))
Im Folgenden werden eingegebene Benutzeräußerungen und entsprechende Benutzeräußerungen mit *
von fill_batch
gefüllt, um tiefes Lernen zu ermöglichen.
def fill_batch(batch, token='</s>'):
max_len = max(len(x) for x in batch)
return [x + [token] * (max_len - len(x) + 1) for x in batch]
Die Rückwärtsverarbeitung wird unter Verwendung des bei der Vorwärtsverarbeitung erhaltenen Verlusts durchgeführt, und das Gewicht wird aktualisiert. Die Rückwärtsverarbeitung hängt von der Aktivierungsfunktion ab. Der aktualisierte Teil lautet wie folgt. Ändern Sie, ob die Daten von GPU oder CPU verarbeitet werden Die Optimierung durch die Verlustfunktion wird geändert, indem die Art und Weise der Datenübertragung in "Tupel", "Dikt" und anderen geändert wird.
def update_core(self):
batch = self._iterators['main'].next()
in_arrays = self.converter(batch, self.device)
optimizer = self._optimizers['main']
loss_func = self.loss_func or optimizer.target
if isinstance(in_arrays, tuple):
in_vars = tuple(variable.Variable(x) for x in in_arrays)
optimizer.update(loss_func, *in_vars)
elif isinstance(in_arrays, dict):
in_vars = {key: variable.Variable(x)
for key, x in six.iteritems(in_arrays)}
optimizer.update(loss_func, **in_vars)
else:
in_var = variable.Variable(in_arrays)
optimizer.update(loss_func, in_var)
for src_batch, trg_batch in gen3:
src_batch = fill_batch(src_batch)
trg_batch = fill_batch(trg_batch)
K = len(src_batch)
hyp_batch, loss = self.forward_implement(src_batch, trg_batch, src_vocab, trg_vocab, self.attention_dialogue, True, 0)
loss.backward()
opt.update()
Speichern des trainierten Modells save und save_spec sind im Chainer-Standard nicht vorhanden, werden jedoch separat erstellt, um Informationen zur Sprache zu speichern.
save
speichert Sprachdateninformationen
save_spec
speichert die Vokabulargröße, die Größe der eingebetteten Ebene und die Größe der versteckten Ebene
save_hdf5
speichert das Modell im hdf5-Format
trace('saving model ...')
prefix = self.model
model_path = APP_ROOT + "/model/" + prefix
src_vocab.save(model_path + '.srcvocab')
trg_vocab.save(model_path + '.trgvocab')
self.attention_dialogue.save_spec(model_path + '.spec')
serializers.save_hdf5(model_path + '.weights', self.attention_dialogue)
Dies ist der Testteil. Die Modellausgabe während des Trainings wird gelesen und der Äußerungsinhalt des Benutzers für die Eingabeäußerung wird ausgegeben.
def test(self):
trace('loading model ...')
prefix = self.model
model_path = APP_ROOT + "/model/" + prefix
src_vocab = Vocabulary.load(model_path + '.srcvocab')
trg_vocab = Vocabulary.load(model_path + '.trgvocab')
self.attention_dialogue = AttentionDialogue.load_spec(model_path + '.spec', self.XP)
serializers.load_hdf5(model_path + '.weights', self.attention_dialogue)
trace('generating translation ...')
generated = 0
with open(self.test_target, 'w') as fp:
for src_batch in gens.batch(gens.word_list(self.source), self.minibatch):
src_batch = fill_batch(src_batch)
K = len(src_batch)
trace('sample %8d - %8d ...' % (generated + 1, generated + K))
hyp_batch = self.forward_implement(src_batch, None, src_vocab, trg_vocab, self.attention_dialogue, False, self.generation_limit)
source_cuont = 0
for hyp in hyp_batch:
hyp.append('</s>')
hyp = hyp[:hyp.index('</s>')]
print("src : " + "".join(src_batch[source_cuont]).replace("</s>", ""))
print('hyp : ' +''.join(hyp))
print(' '.join(hyp), file=fp)
source_cuont = source_cuont + 1
generated += K
trace('finished.')
Der Inhalt wurde auf der PyCon 2016 angekündigt, aber wenn Sie der Meinung sind, dass es sich immer noch um einen Teil handelt, scheint es ein langer Weg zu sein, wenn Sie die Erklärung anderer Teile hinzufügen. Gegenwärtig ist der Bereich, der durch einfaches tiefes Lernen bewältigt werden kann, begrenzt, daher verwenden wir mehrere Technologien. Da es beim Deep Learning viele Modelle für den Dialog gibt, denke ich, dass dies zu einer Leistungsverbesserung führen wird, indem der Bewertungsindex ermittelt und das Modell des Deep Learning geändert wird.