[PYTHON] Seq2Seq (3) ~ CopyNet Edition ~ mit Chainer

Erläuterung von CopyNet, der dritten Version von seq2seq, und seiner Implementierung

Einführung

Synopse bis zum letzten Mal http://qiita.com/kenchin110100/items/b34f5106d5a211f4c004 http://qiita.com/kenchin110100/items/eb70d69d1d65fb451b67

Normal seq2seq, Attention Model, diesmal habe ich CopyNet implementiert.

Wir werden zuerst CopyNet erklären, dann die Implementierung und ihre Ergebnisse.

CopyNet

Was ist CopyNet?

Beginnen wir mit einer Überprüfung von Seq2Seq, um CopyNet zu erklären.

Sequence to Sequence
seq2seq.png

Seq2Seq ist eine Art Encoder-Decoder-Modell, bei dem der Encoder den gesprochenen Satz ("Wie geht es Ihnen?") In einen Vektor konvertiert und der Decoder den Antwortsatz ("Ich fühle mich gut") von diesem Vektor ausgibt.

In Seq2Seqs Encoder wurde nur der letzte Ausgangszwischenvektor berücksichtigt, aber das Aufmerksamkeitsmodell sollte mehr verschiedene Zwischenvektoren berücksichtigen.

Attention Model
attention.png

Denken Sie jetzt darüber nach, was Sie mit CopyNet tun, wenn die Äußerung "Wie geht es Ihnen?" Lautet und die Antwort "Ich fühle mich gut" lautet.

Das Wort wird sowohl in der Sprache als auch in der Antwort verwendet. Die Idee von CopyNet ist es, der Decoder-Seite das Generieren der in der Sprache verwendeten Wörter zu erleichtern.

CopyNet
copynet.png

(Die Figur ist nur ein Bild)

Der Grund, warum CopyNet gut ist, ist, dass es unbekannte Wörter verarbeiten kann. Selbst wenn Sie beim Lernen beispielsweise nicht das Wort haben, können Sie mit dem Wort antworten, indem Sie es kopieren.

Im Folgenden werde ich zwei Artikel zu CopyNet vorstellen.

Jiatao Gu et al.

Dies ist das Original-Copynet-Papier Gu, Jiatao, et al. "Incorporating copying mechanism in sequence-to-sequence learning." arXiv preprint arXiv:1603.06393 (2016).

Gu, Jiatao, et al
Screenshot 2017-03-11 17.49.04.png

Die im Papier verwendete Abbildung ist die obige, aber wenn Sie genauer hinschauen, sieht sie wie die folgende Abbildung aus.

Copy mode and StateUpdate
Gu.png

Bei dem von Gu et al. Vorgeschlagenen Verfahren gibt es zwei Hauptmechanismen, StateUpdate und CopyMode.

Wenn in StateUpdate das in Decoder eingegebene Wort ein Wort () ist, das in der Äußerung enthalten ist, wird der Zwischenvektor dieses Wortes (vom Encoder ausgegeben) eingegeben.

Wenn im CopyMode das Wort, dessen Ausgabe Sie erwarten, im gesprochenen Satz () enthalten ist, verwenden Sie einen Zwischenvektor, um die Erscheinungswahrscheinlichkeit von zu erhöhen, damit das Wort leicht ausgegeben werden kann.

(Die Erklärung ist ziemlich schlecht, aber bitte lesen Sie das Papier für Details ...)

Ziqiang Cao et al.

Ich möchte ein weiteres Papier zu CopyNet vorstellen. Genau genommen handelt es sich nicht um CopyNet, aber es gibt die folgenden Dokumente, die einen ähnlichen Mechanismus implementieren.

Cao, Ziqiang, et al. "Joint Copying and Restricted Generation for Paraphrase." arXiv preprint arXiv:1611.09235 (2016).

Ziqiang Cao et al.
Screenshot 2017-03-11 18.16.25.png

(Abbildung in der Zeitung verwendet)

Dieser ist etwas einfacher, und wenn Sie ihn kurz erklären, wird er wie folgt aussehen.

Restricted Generative Decoder
cao.png

Die Richtlinie besteht darin, das vom Aufmerksamkeitsmodell berechnete Gewicht unverändert zu verwenden.

Wenn die Eingabe kein Wort enthält, dessen Ausgabe erwartet wird, wird die Wahrscheinlichkeit des generierten Wortes unverändert verwendet. Wenn die Eingabe ein Wort enthält, dessen Ausgabe erwartet wird (), werden die Wahrscheinlichkeit des erzeugten Wortes und das vom Aufmerksamkeitsmodell berechnete Gewicht durch λ gemittelt (λ liegt zwischen 0 und 1). Skalar).

Der Punkt ist, wie man dieses λ ausgleicht, aber wir werden auch λ lernen. (Bitte lesen Sie das Papier für Details ...)

Implementierung

Diesmal implementierte Chainer die Methode von Ziqiang Cao et al. Es gibt nicht viele CopyNet-Implementierungen im Internet, und es tut mir leid, wenn ich einen Fehler gemacht habe ...

Encoder und Decoder verwenden das zum Zeitpunkt des Aufmerksamkeitsmodells verwendete Modell so wie es ist.

Attention

Es ist im Grunde dasselbe wie das Aufmerksamkeitsmodell, aber das Gewicht jedes Zwischenvektors wird auch geändert, um ausgegeben zu werden.

attention.py


class Copy_Attention(Attention):

    def __call__(self, fs, bs, h):
        """
Aufmerksamkeitsberechnung
        :param fs:Eine Liste von Forward-Encoder-Zwischenvektoren
        :param bs:Liste der Zwischenvektoren des Umkehrcodierers
        :param h:Zwischenvektorausgabe durch Decoder
        :return att_f:Gewichteter Durchschnitt des Zwischenvektors des Vorwärtscodierers
        :return att_b:Gewichteter Durchschnitt des Zwischenvektors des Umkehrcodierers
        :return att:Gewicht jedes Zwischenvektors
        """
        #Denken Sie an die Größe der Mini-Charge
        batch_size = h.data.shape[0]
        #Initialisieren der Liste zum Aufzeichnen von Gewichten
        ws = []
        att = []
        #Initialisieren Sie den Wert, um den Gesamtwert des Gewichts zu berechnen
        sum_w = Variable(self.ARR.zeros((batch_size, 1), dtype='float32'))
        #Gewichtsberechnung unter Verwendung des Zwischenvektors des Encoders und des Zwischenvektors des Decoders
        for f, b in zip(fs, bs):
            #Gewichtsberechnung mit Vorwärtscodierer-Zwischenvektor, Rückwärtscodierer-Zwischenvektor, Decoder-Zwischenvektor
            w = self.hw(functions.tanh(self.fh(f)+self.bh(b)+self.hh(h)))
            att.append(w)
            #Normalisieren Sie mit der Softmax-Funktion
            w = functions.exp(w)
            #Notieren Sie das berechnete Gewicht
            ws.append(w)
            sum_w += w
        #Initialisierung des ausgegebenen gewichteten Durchschnittsvektors
        att_f = Variable(self.ARR.zeros((batch_size, self.hidden_size), dtype='float32'))
        att_b = Variable(self.ARR.zeros((batch_size, self.hidden_size), dtype='float32'))
        for i, (f, b, w) in enumerate(zip(fs, bs, ws)):
            #Normalisiert, so dass die Summe der Gewichte 1 ist.
            w /= sum_w
            #Gewicht*Fügen Sie den Zwischenvektor von Encoder zum Ausgabevektor hinzu
            att_f += functions.reshape(functions.batch_matmul(f, w), (batch_size, self.hidden_size))
            att_b += functions.reshape(functions.batch_matmul(f, w), (batch_size, self.hidden_size))
        att = functions.concat(att, axis=1)
        return att_f, att_b, att

Seq2Seq with CopyNet

Das Modell, das Encoder, Decorder und Attention kombiniert, ist wie folgt.

copy_seq2seq.py


class Copy_Seq2Seq(Chain):
    def __init__(self, vocab_size, embed_size, hidden_size, batch_size, flag_gpu=True):
        super(Copy_Seq2Seq, self).__init__(
            #Vorwärtscodierer
            f_encoder = LSTM_Encoder(vocab_size, embed_size, hidden_size),
            #Rückwärtsgeber
            b_encoder = LSTM_Encoder(vocab_size, embed_size, hidden_size),
            # Attention Model
            attention=Copy_Attention(hidden_size, flag_gpu),
            # Decoder
            decoder=Att_LSTM_Decoder(vocab_size, embed_size, hidden_size),
            #Netzwerk zur Berechnung des Gewichts von λ
            predictor=links.Linear(hidden_size, 1)
        )
        self.vocab_size = vocab_size
        self.embed_size = embed_size
        self.hidden_size = hidden_size
        self.batch_size = batch_size
        if flag_gpu:
            self.ARR = cuda.cupy
        else:
            self.ARR = np

        #Initialisieren Sie die Liste, um den Vorwärtscodierer-Zwischenvektor und den Rückwärtscodierer-Zwischenvektor zu speichern
        self.fs = []
        self.bs = []

    def encode(self, words):
        """
Geberberechnung
        :param words:Eine aufgezeichnete Liste von Wörtern, die für Ihre Eingabe verwendet werden sollen
        :return:
        """
        #Interner Speicher, Initialisierung des Zwischenvektors
        c = Variable(self.ARR.zeros((self.batch_size, self.hidden_size), dtype='float32'))
        h = Variable(self.ARR.zeros((self.batch_size, self.hidden_size), dtype='float32'))
        #Berechnen Sie zunächst den Vorwärtscodierer
        for w in words:
            c, h = self.f_encoder(w, c, h)
            #Notieren Sie den berechneten Zwischenvektor
            self.fs.append(h)

        #Interner Speicher, Initialisierung des Zwischenvektors
        c = Variable(self.ARR.zeros((self.batch_size, self.hidden_size), dtype='float32'))
        h = Variable(self.ARR.zeros((self.batch_size, self.hidden_size), dtype='float32'))
        #Reverse Encoder Berechnung
        for w in reversed(words):
            c, h = self.b_encoder(w, c, h)
            #Notieren Sie den berechneten Zwischenvektor
            self.bs.insert(0, h)

        #Interner Speicher, Initialisierung des Zwischenvektors
        self.c = Variable(self.ARR.zeros((self.batch_size, self.hidden_size), dtype='float32'))
        self.h = Variable(self.ARR.zeros((self.batch_size, self.hidden_size), dtype='float32'))


    def decode(self, w):
        """
Decoderberechnung
        :param w:Mit Decoder einzugebende Wörter
        :return t:Voraussagendes Wort
        :return att:Aufmerksamkeitsgewicht für jedes Wort
        :return lambda_:Das Gewicht zum Bestimmen, ob Kopieren wichtig oder Generieren wichtig ist
        """
        #Berechnen Sie den Eingabevektor mit dem Aufmerksamkeitsmodell
        att_f, att_b, att = self.attention(self.fs, self.bs, self.h)
        #Eingabevektor in Decoder
        t, self.c, self.h = self.decoder(w, self.c, self.h, att_f, att_b)
        #Berechnung von λ unter Verwendung des berechneten Zwischenvektors
        lambda_ = self.predictor(self.h)
        return t, att, lambda_

Tatsächlich unterscheidet sich dies nicht wesentlich vom Aufmerksamkeitsmodell. Die Änderung besteht darin, dass auch das Aufmerksamkeitsgewicht ausgegeben wird, das λ berechnet, um den Kopiermodus und den generativen Modus auszugleichen.

forward

Die große Veränderung liegt in der Vorwärtsfunktion. Die Vorwärtsfunktion überprüft den Eingabesatz und das Wort, das Sie ausgeben möchten, um festzustellen, ob der Kopiermodus berechnet werden soll.

forward.py


def forward(enc_words, dec_words, model, ARR):
    """
Funktion zur Vorwärtsberechnung
    :param enc_words:Eingabeanweisung
    :param dec_words:Ausgabeanweisung
    :param model:Modell-
    :param ARR:numpy oder cuda.Entweder Cupy
    :return loss:Verlust
    """
    #Chargengröße aufzeichnen
    batch_size = len(enc_words[0])
    #Setzen Sie den im Modell aufgezeichneten Gradienten zurück
    model.reset()
    #Bereiten Sie eine Liste vor, um die im Eingabesatz verwendeten Wörter zu überprüfen
    enc_key = enc_words.T
    #Ändern Sie die in Encoder eingegebene Anweisung in Variablentyp
    enc_words = [Variable(ARR.array(row, dtype='int32')) for row in enc_words]
    #Geberberechnung
    model.encode(enc_words)
    #Verlustinitialisierung
    loss = Variable(ARR.zeros((), dtype='float32'))
    # <eos>Zum Decoder
    t = Variable(ARR.array([0 for _ in range(batch_size)], dtype='int32'))
    #Decoderberechnung
    for w in dec_words:
        #Wort für Wort dekodieren
        y, att, lambda_ = model.decode(t)
        #Konvertieren Sie das richtige Wort in den Variablentyp
        t = Variable(ARR.array(w, dtype='int32'))

        #Protokoll der vom generativen Modus berechneten Wörter_Nimm Softmax
        s = functions.log_softmax(y)
        #Achtung Gewichtsprotokoll_Nimm Softmax
        att_s = functions.log_softmax(att)
        #Durch Multiplikation von Lambda mit Sigmoidfunktion wird 0~Ändern Sie auf einen Wert von 1
        lambda_s = functions.reshape(functions.sigmoid(lambda_), (batch_size,))
        #Generative Modusverlustinitialisierung
        Pg = Variable(ARR.zeros((), dtype='float32'))
        #Initialisierung des Verlusts des Kopiermodus
        Pc = Variable(ARR.zeros((), dtype='float32'))
        #Verlustinitialisierung zum Erlernen des Lambda-Gleichgewichts
        epsilon = Variable(ARR.zeros((), dtype='float32'))
        #Von hier aus wird der Verlust jedes Wortes im Stapel berechnet und die for-Anweisung wird umgedreht ...
        counter = 0
        for i, words in enumerate(w):
            # -1 ist die Bezeichnung für das Wort, das Sie nicht lernen. Ignoriere das.
            if words != -1:
                #Berechnung des generativen Modusverlusts
                Pg += functions.get_item(functions.get_item(s, i), words) * functions.reshape((1.0 - functions.get_item(lambda_s, i)), ())
                counter += 1
                #Wenn es ein Wort gibt, das Sie im Eingabesatz ausgeben möchten
                if words in enc_key[i]:
                    #Kopiermodus berechnen
                    Pc += functions.get_item(functions.get_item(att_s, i), list(enc_key[i]).index(words)) * functions.reshape(functions.get_item(lambda_s, i), ())
                    #Lernen Sie, Lambda besser zu machen als den Kopiermodus
                    epsilon += functions.log(functions.get_item(lambda_s, i))
                #Wenn es kein Wort gibt, das Sie im Eingabesatz ausgeben möchten
                else:
                    #Lernen Sie, Lambda besser zu machen als den generativen Modus
                    epsilon += functions.log(1.0 - functions.get_item(lambda_s, i))
        #Teilen Sie jeden Verlust durch die Chargengröße und addieren Sie ihn
        Pg *= (-1.0 / np.max([1, counter]))
        Pc *= (-1.0 / np.max([1, counter]))
        epsilon *= (-1.0 / np.max([1, counter]))
        loss += Pg + Pc + epsilon
    return loss

Im Code werden drei Verluste, Pg, Pc und epsilon, definiert und berechnet, um jeweils den generativen Modus, den Kopiermodus und λ zu lernen.

Es geht darum, functions.log_softmax zu verwenden. Wenn Sie log (softmax (x)) einstellen, tritt ein Fehler auf, wenn die Berechnung von softmax 0 wird, aber diese Funktion macht es gut (wie es funktioniert, ist ein Rätsel ...).

Wenn Sie die Funktion functions.softmax_cross_entropy verwenden, benötigen Sie keine so mühsame Berechnung, aber dieses Mal möchte ich den Verlust des Kopiermodus und den Verlust des generativen Modus mit λ ausgleichen, also verwende ich die Funktionen functions.get_items und functions.log_softmax, um den Verlust zu machen. Wird berechnet.

Wenn Sie eine bessere Implementierung kennen, lassen Sie es mich bitte wissen ...

Der erstellte Code ist https://github.com/kenchin110100/machine_learning/blob/master/sampleCopySeq2Seq.py Es ist in.

Experiment

Korpus

Ich habe den Dialogfehlerkorpus wie zuvor verwendet. https://sites.google.com/site/dialoguebreakdowndetection/chat-dialogue-corpus

Versuchsergebnis

Die folgenden 4 Arten von Äußerungen

Wir werden uns die Antwortergebnisse für jede Epoche ansehen.

Epoch 1

Sprechen:Guten Morgen=>Antwort:  ['Guten Morgen', '</s>'] ['copy', 'copy']
Sprechen:Wie geht's?=>Antwort:  ['Bedingung', 'Ist', 'Ist', 'ist', 'ist', '</s>'] ['copy', 'copy', 'copy', 'copy', 'copy', 'copy']
Sprechen:Ich bin hungrig=>Antwort:  ['Bauch', 'Aber', 'Aber', 'Aber', 'Ta', 'Ta', 'ist', '</s>'] ['copy', 'copy', 'copy', 'copy', 'copy', 'copy', 'gen', 'copy']
Sprechen:Es ist heiß heute=>Antwort:  ['heute', 'Ist', 'Ist', 'ist', 'ist', '</s>'] ['copy', 'copy', 'copy', 'copy', 'copy', 'copy']

Es ist völlig kaputt ...

Epoch 3

Sprechen:Guten Morgen=>Antwort:  ['Guten Morgen', '</s>'] ['copy', 'copy']
Sprechen:Wie geht's?=>Antwort:  ['Bedingung', 'Ist', '</s>'] ['copy', 'gen', 'copy']
Sprechen:Ich bin hungrig=>Antwort:  ['Bauch', '</s>'] ['copy', 'copy']
Sprechen:Es ist heiß heute=>Antwort:  ['heiß', 'Ist', 'Mögen', 'ist', 'Hallo', '</s>'] ['copy', 'copy', 'gen', 'gen', 'gen', 'copy']

Epoch 5

Sprechen:Guten Morgen=>Antwort:  ['Guten Morgen', '</s>'] ['copy', 'copy']
Sprechen:Wie geht's?=>Antwort:  ['Bedingung', 'Ist', 'Mögen', 'ist', 'Oder', '</s>'] ['copy', 'copy', 'gen', 'copy', 'gen', 'copy']
Sprechen:Ich bin hungrig=>Antwort:  ['Bauch', '</s>'] ['copy', 'copy']
Sprechen:Es ist heiß heute=>Antwort:  ['heiß', 'ist', '</s>'] ['copy', 'gen', 'copy']

Auch wenn Sie für hungrig erklärt werden ...

Epoch 7

Sprechen:Guten Morgen=>Antwort:  ['Guten Morgen', 'Vielen Dank', 'Masu', '</s>'] ['copy', 'gen', 'gen', 'copy']
Sprechen:Wie geht's?=>Antwort:  ['Bedingung', 'Ist', '</s>'] ['copy', 'gen', 'copy']
Sprechen:Ich bin hungrig=>Antwort:  ['Bauch', 'Aber', 'Frei', 'Besser', 'Ta', '</s>'] ['copy', 'gen', 'copy', 'copy', 'gen', 'gen']
Sprechen:Es ist heiß heute=>Antwort:  ['heiß', 'ist', '</s>'] ['copy', 'gen', 'copy']

Das Wort ist nicht im Lernen enthalten, daher kann gesagt werden, dass es in dieser Hinsicht gut kopiert wird.

Ehrlich gesagt möchte ich Sie jedoch bitten, etwas besser zu antworten.

Es lernt sowohl den Kopiermodus als auch den Generierungsmodus, so dass der Decoder das Sprachmodell anscheinend nicht vollständig trainiert hat.

Dies kann damit zusammenhängen, dass das Papier nicht von der Dialogaufgabe, sondern von der Zusammenfassungsaufgabe bewertet wurde. (Nun, die Hauptursache kann die Implementierung sein ...)

Fazit

Ich habe CopyNet mit Chainer implementiert. Ich habe das Dialogmodell dreimal gemacht, also bin ich schon voll lol Nächstes Mal mache ich etwas anderes.

Recommended Posts

Seq2Seq (3) ~ CopyNet Edition ~ mit Chainer
Seq2Seq (1) mit Chainer
Seq2Seq (2) ~ Achtung Model Edition ~ mit Chainer
Verwenden Sie Tensorboard mit Chainer
Versuchen Sie, RBM mit Chainer zu implementieren.
Lernen Sie mit Chainer elliptische Bahnen
Verwendung von Chainer mit Jetson TK1
Neuronales Netz beginnend mit Chainer
Bedingte GAN mit Chainer implementiert
Bildunterschriftengenerierung mit Chainer
SmoothGrad mit Chainer v2 implementiert
Deep Embedded Clustering mit Chainer 2.0
Ein bisschen im Kettenschiff stecken
Mehrschichtiges Perzeptron mit Kette: Funktionsanpassung
Versuchen Sie, Pferderennen mit Chainer vorherzusagen
Ich habe versucht, Attention Seq2Seq mit PyTorch zu implementieren
[Chainer] Lernen von XOR mit mehrschichtigem Perzeptron
Erste Anime-Gesichtserkennung mit Chainer
Führen Sie eine Inferenz mit dem Chainer 2.0 MNIST-Beispiel durch
Verwenden von Chainer mit CentOS7 [Umgebungskonstruktion]
Versuchen Sie Common Representation Learning mit Chainer
Feature Quantity Engineering auf Reisen mit Pokemon-Numerical Version-