[PYTHON] Erklären des auf der PyCon 2016 angekündigten Chat-Bots aus der Codebasis (Chat-Antwort mit Chainer)

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)

Screen Shot 2016-12-26 at 8.04.52 AM.png

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

Chat-Antwort

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

image.png image.png

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

Screen Shot 2016-12-26 at 9.38.00 AM.png

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.

Es besteht aus den oben genannten fünf.

HOW

src_embed.py

Screen Shot 2016-12-26 at 9.38.00 AM.png

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

Screen Shot 2016-12-26 at 9.38.00 AM.png

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

Screen Shot 2016-12-26 at 9.38.00 AM.png

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.

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

Screen Shot 2016-12-26 at 9.38.00 AM.png

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.')

Zusammenfassung

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.

Referenz

Attention and Memory in Deep Learning and NLP

Recommended Posts

Erklären des auf der PyCon 2016 angekündigten Chat-Bots aus der Codebasis (Chat-Antwort mit Chainer)
Versuchen Sie, Code aus 1 mit dem Framework Chainer für maschinelles Lernen (Mnist Edition) zu schreiben.
Erläuterung des Konzepts der Regressionsanalyse mit Python Teil 2
[Frage] Über die API-Konvertierung von Chat-Bot mit Python
Holen Sie sich den Rückkehrcode eines Python-Skripts von bat
Erläuterung des Konzepts der Regressionsanalyse mit Python Teil 1
Lassen Sie uns die Emotionen von Tweet mit Chainer (2.) analysieren.
Erläuterung des Konzepts der Regressionsanalyse mit Python Extra 1
Studie aus Python Hour8: Verwenden von Paketen
Lassen Sie uns die Emotionen von Tweet mit Chainer (1.) analysieren.