Je lis un chef-d'œuvre, ** "Deep Learning from Zero 2" **. Cette fois, c'est un mémo du chapitre 5. Pour exécuter le code, téléchargez le code complet depuis Github et utilisez le notebook jupyter dans ch05.
2.RNNLM Tout d'abord, essayez d'exécuter le code suivant, ch05 / train_custom_loop.py, qui apprend l'ordre des mots de l'ensemble de données PTB.
import sys
sys.path.append('..')
import matplotlib.pyplot as plt
import numpy as np
from common.optimizer import SGD
from dataset import ptb
from simple_rnnlm import SimpleRnnlm
#Paramètres des hyper paramètres
batch_size = 10
wordvec_size = 100
hidden_size = 100
time_size = 5  #Durée de déploiement du BPTT tronqué
lr = 0.1
max_epoch = 100
#Lire les données d'entraînement (réduire le jeu de données)
corpus, word_to_id, id_to_word = ptb.load_data('train')
corpus_size = 1000
corpus = corpus[:corpus_size]
vocab_size = int(max(corpus) + 1)
xs = corpus[:-1]  #contribution
ts = corpus[1:]  #Sortie (étiquette de l'enseignant)
data_size = len(xs)
print('corpus size: %d, vocabulary size: %d' % (corpus_size, vocab_size))
#Variables utilisées lors de l'apprentissage
max_iters = data_size // (batch_size * time_size)
time_idx = 0
total_loss = 0
loss_count = 0
ppl_list = []
#Génération de modèle
model = SimpleRnnlm(vocab_size, wordvec_size, hidden_size)
optimizer = SGD(lr)
#Calculer la position de départ de chargement de chaque échantillon dans le mini-lot
jump = (corpus_size - 1) // batch_size
offsets = [i * jump for i in range(batch_size)]
for epoch in range(max_epoch):
    for iter in range(max_iters):
        #Obtenez un mini lot
        batch_x = np.empty((batch_size, time_size), dtype='i')
        batch_t = np.empty((batch_size, time_size), dtype='i')
        for t in range(time_size):
            for i, offset in enumerate(offsets):
                batch_x[i, t] = xs[(offset + time_idx) % data_size]
                batch_t[i, t] = ts[(offset + time_idx) % data_size]
            time_idx += 1
        #Trouvez le dégradé et mettez à jour les paramètres
        loss = model.forward(batch_x, batch_t)
        model.backward()
        optimizer.update(model.params, model.grads)
        total_loss += loss
        loss_count += 1
    #Évaluation de la perplexité pour chaque époque
    ppl = np.exp(total_loss / loss_count)
    print('| epoch %d | perplexity %.2f'
          % (epoch+1, ppl))
    ppl_list.append(float(ppl))
    total_loss, loss_count = 0, 0
#Dessiner un graphique
x = np.arange(len(ppl_list))
plt.plot(x, ppl_list, label='train')
plt.xlabel('epochs')
plt.ylabel('perplexity')
plt.show()
 L'axe vertical du graphique est un index appelé ** perplexité ** qui prédit la probabilité du mot suivant, et $ perplexité = e ^ L \ (L = - \ frac {1} {N} \ sum_n \ sum_k t_ { Il est exprimé par la formule nk} log y_ {nk}) $. Plus la valeur de perplexité est proche de 1, plus la précision de prédiction est élevée. En termes simples, la perplexité est le nombre de choix pour le mot suivant. Jetons un coup d'œil à la partie qui prépare les données.
L'axe vertical du graphique est un index appelé ** perplexité ** qui prédit la probabilité du mot suivant, et $ perplexité = e ^ L \ (L = - \ frac {1} {N} \ sum_n \ sum_k t_ { Il est exprimé par la formule nk} log y_ {nk}) $. Plus la valeur de perplexité est proche de 1, plus la précision de prédiction est élevée. En termes simples, la perplexité est le nombre de choix pour le mot suivant. Jetons un coup d'œil à la partie qui prépare les données.

** corpus ** utilise uniquement les 1 000 premiers mots de l'ensemble de données PTB, et ** les données de formation xs ** et ** les données de l'enseignant ts ** obtiennent 999 mots chacune dans un mot.
Ensuite, utilisez des «décalages» pour déterminer les positions de lecture (10 emplacements) pour la taille du lot, et créez un mini-lot en divisant chaque donnée pour la taille de temps (5). Lorsque ʻoffsets + time_idx` devient 999 ou plus de la taille des données, il recommence à partir de 0 et les données sont acquises.
Jetons un coup d'œil à la ** class SimpleRnnlm **, qui génère le modèle.
3.SimpleRnnlm
class SimpleRnnlm:
    def __init__(self, vocab_size, wordvec_size, hidden_size):
        V, D, H = vocab_size, wordvec_size, hidden_size
        rn = np.random.randn
        #Initialisation du poids
        embed_W = (rn(V, D) / 100).astype('f')
        rnn_Wx = (rn(D, H) / np.sqrt(D)).astype('f')
        rnn_Wh = (rn(H, H) / np.sqrt(H)).astype('f')
        rnn_b = np.zeros(H).astype('f')
        affine_W = (rn(H, V) / np.sqrt(H)).astype('f')
        affine_b = np.zeros(V).astype('f')
        #Génération de couches
        self.layers = [
            TimeEmbedding(embed_W),
            TimeRNN(rnn_Wx, rnn_Wh, rnn_b, stateful=True),
            TimeAffine(affine_W, affine_b)
        ]
        self.loss_layer = TimeSoftmaxWithLoss()
        self.rnn_layer = self.layers[1]
        #Lister tous les poids et dégradés
        self.params, self.grads = [], []
        for layer in self.layers:
            self.params += layer.params
            self.grads += layer.grads
    def forward(self, xs, ts):
        for layer in self.layers:
            xs = layer.forward(xs)
        loss = self.loss_layer.forward(xs, ts)
        return loss
    def backward(self, dout=1):
        dout = self.loss_layer.backward(dout)
        for layer in reversed(self.layers):
            dout = layer.backward(dout)
        return dout
    def reset_state(self):
        self.rnn_layer.reset_state()

** class SimpleRnnlm ** est une pile de quatre ** couches de temps **: ** TimeEmbedding, Time RNN, Time Affine et Time Softmax With Loss **. Jetons un coup d'œil à la couche Time dans l'ordre.
class TimeEmbedding:
    def __init__(self, W):
        self.params = [W]
        self.grads = [np.zeros_like(W)]
        self.layers = None
        self.W = W
    def forward(self, xs):
        N, T = xs.shape
        V, D = self.W.shape
        out = np.empty((N, T, D), dtype='f')
        self.layers = []
        for t in range(T):
            layer = Embedding(self.W)
            out[:, t, :] = layer.forward(xs[:, t])
            self.layers.append(layer)
        return out
    def backward(self, dout):
        N, T, D = dout.shape
        grad = 0
        for t in range(T):
            layer = self.layers[t]
            layer.backward(dout[:, t, :])
            grad += layer.grads[0]
        self.grads[0][...] = grad
        return None

La ** couche Time Embedding ** coupe les données une colonne à la fois à partir de xs, les entre dans la ** couche Embedding ** et stocke la sortie dans ** out (N, T, D) **. Il répète T fois en boucle.
Avant de regarder la couche TimeRNN, regardons la couche RNN utilisée pour la couche TimeRNN.
class RNN:
    def __init__(self, Wx, Wh, b):
        self.params = [Wx, Wh, b]
        self.grads = [np.zeros_like(Wx), np.zeros_like(Wh), np.zeros_like(b)]
        self.cache = None
    def forward(self, x, h_prev):
        Wx, Wh, b = self.params
        t = np.dot(h_prev, Wh) + np.dot(x, Wx) + b
        h_next = np.tanh(t)
        self.cache = (x, h_prev, h_next)
        return h_next

La couche RNN a deux poids. Ce sont le poids ** W_x ** qui prend l'entrée x_t et le produit interne (MatMul) et le poids ** W_h ** qui prend l'entrée h_prev et le produit interne (MatMUl).
    def backward(self, dh_next):
        Wx, Wh, b = self.params
        x, h_prev, h_next = self.cache
        dt = dh_next * (1 - h_next ** 2)
        db = np.sum(dt, axis=0)
        dWh = np.dot(h_prev.T, dt)
        dh_prev = np.dot(dt, Wh.T)
        dWx = np.dot(x.T, dt)
        dx = np.dot(dt, Wx.T)
        self.grads[0][...] = dWx
        self.grads[1][...] = dWh
        self.grads[2][...] = db
        return dx, dh_prev

La rétropropagation ressemble à ceci. C'est une version modifiée d'Affine, donc rien de compliqué.
class TimeRNN:
    def __init__(self, Wx, Wh, b, stateful=False):
        self.params = [Wx, Wh, b]
        self.grads = [np.zeros_like(Wx), np.zeros_like(Wh), np.zeros_like(b)]
        self.layers = None
        self.h, self.dh = None, None
        self.stateful = stateful
    def forward(self, xs):
        Wx, Wh, b = self.params
        N, T, D = xs.shape
        D, H = Wx.shape
        self.layers = []
        hs = np.empty((N, T, H), dtype='f')
        if not self.stateful or self.h is None:
            self.h = np.zeros((N, H), dtype='f')
        for t in range(T):
            layer = RNN(*self.params)
            self.h = layer.forward(xs[:, t, :], self.h)
            hs[:, t, :] = self.h
            self.layers.append(layer)
        return hs

** La couche TimeRNN ** est un réseau de couches T RNN connectées entre elles. Vous permet d'ajuster s'il faut hériter de l'état h entre les blocs avec l'argument stateful.
Pour la propagation directe, préparez d'abord le conteneur de sortie hs (N, T, H). Ensuite, en tournant la boucle for, les t-ièmes données sont coupées par xs [:, t,:] et entrées dans le RNN normal, et la sortie est le conteneur préparé par hs [:, t,:]. Tout en le stockant dans la position spécifiée de, enregistrez le calque dans les calques.
En d'autres termes, la couche TimeRNN est l'entrée / sortie de la couche RNN avec les fonctions d'extraction et de récapitulation des données.
    def backward(self, dhs):
        Wx, Wh, b = self.params
        N, T, H = dhs.shape
        D, H = Wx.shape
        dxs = np.empty((N, T, D), dtype='f')
        dh = 0
        grads = [0, 0, 0]
        for t in reversed(range(T)):
            layer = self.layers[t]
            dx, dh = layer.backward(dhs[:, t, :] + dh)  #Gradient additionné
            dxs[:, t, :] = dx
            for i, grad in enumerate(layer.grads):
                grads[i] += grad
        for i, grad in enumerate(grads):
            self.grads[i][...] = grad
        self.dh = dh
        return dxs
    def set_state(self, h):
        self.h = h
    def reset_state(self):
        self.h = None

La propagation directe TimeRNN a deux sorties, donc dans le cas de la propagation arrière, $ dh_t + dh_ {next} $, qui est la somme des deux, est entrée.
Tout d'abord, créez un conteneur dxs qui coule en aval, trouvez le gradient dx à chaque fois avec backward () de la couche RNN dans l'ordre inverse de la propagation directe, et affectez-le à l'indice correspondant de dxs. Le paramètre poids ajoute les gradients de poids pour chaque calque et écrase le résultat final dans self.grads.
class TimeAffine:
    def __init__(self, W, b):
        self.params = [W, b]
        self.grads = [np.zeros_like(W), np.zeros_like(b)]
        self.x = None
    def forward(self, x):
        N, T, D = x.shape
        W, b = self.params
        rx = x.reshape(N*T, -1)
        out = np.dot(rx, W) + b
        self.x = x
        return out.reshape(N, T, -1)
    def backward(self, dout):
        x = self.x
        N, T, D = x.shape
        W, b = self.params
        dout = dout.reshape(N*T, -1)
        rx = x.reshape(N*T, -1)
        db = np.sum(dout, axis=0)
        dW = np.dot(rx.T, dout)
        dx = np.dot(dout, W.T)
        dx = dx.reshape(*x.shape)
        self.grads[0][...] = dW
        self.grads[1][...] = db
        return dx

La ** couche Affine temporelle ** est l'entrée et la sortie de la ** couche Affine ** avec remodelage ajouté afin qu'elle puisse correspondre à T dans la direction de l'axe des temps.
8.TimeSoftmaxWithLoss
class TimeSoftmaxWithLoss:
    def __init__(self):
        self.params, self.grads = [], []
        self.cache = None
        self.ignore_label = -1
    def forward(self, xs, ts):
        N, T, V = xs.shape
        if ts.ndim == 3:  #L'étiquette de l'enseignant en est une-Pour vecteur chaud
            ts = ts.argmax(axis=2)
        mask = (ts != self.ignore_label)
        #Collecter des lots et des séries chronologiques (remodeler)
        xs = xs.reshape(N * T, V)
        ts = ts.reshape(N * T)
        mask = mask.reshape(N * T)
        ys = softmax(xs)
        ls = np.log(ys[np.arange(N * T), ts])
        ls *= mask  # ignore_Les données correspondant à l'étiquette définissent la perte sur 0
        loss = -np.sum(ls)
        loss /= mask.sum()
        self.cache = (ts, ys, mask, (N, T, V))
        return loss
    def backward(self, dout=1):
        ts, ys, mask, (N, T, V) = self.cache
        dx = ys
        dx[np.arange(N * T), ts] -= 1
        dx *= dout
        dx /= mask.sum()
        dx *= mask[:, np.newaxis]  # ignore_Mettre le dégradé à 0 pour les données correspondant à l'étiquette
        dx = dx.reshape((N, T, V))
        return dx
 ** Time Softmax with Loss Layer ** est une couche qui ajoute T de Sotmax avec perte de $ x_t et t_t $ et divise par T.
** Time Softmax with Loss Layer ** est une couche qui ajoute T de Sotmax avec perte de $ x_t et t_t $ et divise par T.
Essayons-le avec un ensemble de données japonais pour avoir une meilleure compréhension de l'ensemble. Cependant, si vous essayez de le faire mot par mot, une analyse morphologique est nécessaire, donc ** unité de caractère **. Cette fois, j'ai téléchargé "Old Man and the Sea" depuis Aozora Bunko et l'ai utilisé.
import numpy as np
import io
def load_data():
    
    # file_UTF les 1000 premiers caractères du nom-Lire en texte au format 8
    file_name = './data_rojinto_umi.txt'
    length = 1000
    with io.open(file_name, encoding='utf-8') as f:
        text = f.read().lower()
        text = text[:length]
    # word_to_id, id_to_Créer un mot
    word_to_id, id_to_word = {}, {}
    for word in text:
        if word not in word_to_id:
            new_id = len(word_to_id)
            word_to_id[word] = new_id
            id_to_word[new_id] = word
    #Créer un corpus
    corpus = np.array([word_to_id[W] for W in text])  
    
    return text, corpus, word_to_id, id_to_word
Lit 1000 caractères à partir du début du fichier texte spécifié par nom_fichier au format UTF-8. Une fonction qui renvoie du texte, du corpus, du mot_à_id, de l'id_ au mot. Bougeons un peu.
text, corpus, word_to_id, id_to_word = load_data()
print('text_length = ', len(text))
print(text)
 La longueur du "texte" est de 1000 comme spécifié. Si vous le préparez en unités de caractères, le texte est très compact.
La longueur du "texte" est de 1000 comme spécifié. Si vous le préparez en unités de caractères, le texte est très compact.
print('vocab_size = ', len(word_to_id))
print(word_to_id)

C'est word_to_id. «vocab_size» n'est pas aussi grand que je le pensais 236. Ceci est utilisé pour remplacer chaque caractère de «texte» par id pour créer un «corpus».
print('corpus_length = ', len(corpus))
print(corpus[:500])
 C'est un corpus. L'affichage est limité à 500 caractères depuis le début.
C'est un corpus. L'affichage est limité à 500 caractères depuis le début.
text2 = ''.join([id_to_word[id] for id in corpus])
print(text2)

La conversion de l'identifiant de corpus en un caractère en utilisant ʻid_to_word` retournera au premier texte comme ceci.
Maintenant, comme c'est un gros problème, j'aimerais préparer des données de test et des réponses et vérifier combien de prédictions peuvent être faites pour chaque époque.
#Échantillon de corpus
x = corpus[:50]
t = corpus[1:51]
print('x = ', x)
print('t = ', t)
#Confirmation par SMS
text_x = ''.join([id_to_word[id] for id in x])
text_t = ''.join([id_to_word[id] for id in t])
print('text_x = ', text_x)
print('text_t = ', text_t)
#Convertir au format batch
test_x = x.reshape(10, 5)
test_t = t.reshape(10, 5)
print(test_x)
print(test_t)
 Obtenez x, t de
Obtenez x, t de corpus en décalant 50 caractères d'un caractère. Pour le moment, je le convertis en caractères et vérifie le contenu. Ensuite, il est converti en la forme (10, 5) utilisée dans le modèle, et les données de prédiction test_x et test_t pour les tests sont créées.
    def generate(self, xs):
        for layer in self.layers:
            xs = layer.forward(xs)
        return xs
Plus tard, j'ajouterai ce code à la fin de simple_rnnlm.py pour faire des prédictions pour chaque époque.
Ensuite, en vous basant sur le code ch05 / train_custom_loop.py qui a été exécuté en premier, modifiez et ajoutez les deux parties de ** lecture des données d'apprentissage et exécution de l'inférence des données de test **. Le nombre d'époques est de 1000 fois.
import sys
sys.path.append('..')
import matplotlib.pyplot as plt
import numpy as np
from common.optimizer import SGD
from dataset import ptb
from simple_rnnlm import SimpleRnnlm    
#Paramètres des hyper paramètres
batch_size = 10
wordvec_size = 100
hidden_size = 100
time_size = 5  #Durée de déploiement du BPTT tronqué
lr = 0.1
max_epoch = 1000  
# -----------Lecture des données d'entraînement-------------
text, corpus, word_to_id, id_to_word = load_data()
corpus_size = 1000
vocab_size = int(max(corpus) + 1)
# ----------------------------------------------
xs = corpus[:-1]  #contribution
ts = corpus[1:]  #Sortie (étiquette de l'enseignant)
data_size = len(xs)
print('corpus size: %d, vocabulary size: %d' % (corpus_size, vocab_size))
#Variables utilisées lors de l'apprentissage
max_iters = data_size // (batch_size * time_size)  
time_idx = 0
total_loss = 0
loss_count = 0
ppl_list = []
#Génération de modèle
model = SimpleRnnlm(vocab_size, wordvec_size, hidden_size)
optimizer = SGD(lr)
#Calculer la position de départ de chargement de chaque échantillon dans le mini-lot
jump = (corpus_size - 1) // batch_size
offsets = [i * jump for i in range(batch_size)]
for epoch in range(max_epoch):
    for iter in range(max_iters):
        #Obtenez un mini lot
        batch_x = np.empty((batch_size, time_size), dtype='i')
        batch_t = np.empty((batch_size, time_size), dtype='i')
        for t in range(time_size):
            for i, offset in enumerate(offsets):
                batch_x[i, t] = xs[(offset + time_idx) % data_size]
                batch_t[i, t] = ts[(offset + time_idx) % data_size]
            time_idx += 1
        #Trouvez le dégradé et mettez à jour les paramètres
        loss = model.forward(batch_x, batch_t)
        model.backward()
        optimizer.update(model.params, model.grads)
        total_loss += loss
        loss_count += 1
    #Évaluation de la perplexité pour chaque époque
    ppl = np.exp(total_loss / loss_count)
    print('| epoch %d | perplexity %.2f'
          % (epoch+1, ppl))
    ppl_list.append(float(ppl))
    total_loss, loss_count = 0, 0
    
    # ----------Prédiction avec les données de test------------
    pred= model.generate(test_x) 
    predict = np.argmax(pred, axis = 2) 
    print(predict)
    # ------------------------------------------------
    
#Dessiner un graphique
x = np.arange(len(ppl_list))
plt.plot(x, ppl_list, label='train')
plt.xlabel('epochs')
plt.ylabel('perplexity')
plt.show()
 Après 1000 époques, la perplexité est tombée à 1,08. Puisque le résultat de la prédiction basé sur les données de test est affiché pour chaque époque, si vous regardez le résultat de la prédiction après 1 époque, il est de 5 ("ta" de Hiragana). Quand vous venez d'apprendre, cela ressemble à ceci. Ceci, le résultat de la prédiction après 1000 époques est tout à fait comme ça. Vérifions maintenant le résultat final de la prédiction.
Après 1000 époques, la perplexité est tombée à 1,08. Puisque le résultat de la prédiction basé sur les données de test est affiché pour chaque époque, si vous regardez le résultat de la prédiction après 1 époque, il est de 5 ("ta" de Hiragana). Quand vous venez d'apprendre, cela ressemble à ceci. Ceci, le résultat de la prédiction après 1000 époques est tout à fait comme ça. Vérifions maintenant le résultat final de la prédiction.

La bonne réponse est entourée de rouge après la correspondance des résultats de prédiction. Puisqu'il est 24/50, le taux de réponse correcte est de 48%, ce qui n'est pas aussi élevé que prévu. Est-ce parce que le taux de réponse correcte dans la seconde moitié des cinq caractères est élevé parce qu'il peut être prédit en fonction des caractères de la première moitié? Si vous essayez d'exprimer le résultat de la prédiction de la première ligne avec un modèle,

Je vois. Avez-vous fait une erreur comme celle-ci?
Recommended Posts