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.
** 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.
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.
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.
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 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.
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