En avril de cette année, ** "Deep Learning3 Framework from scratch" ** est sorti. J'ai lu ** Zero work ** comme ** 1.2 ** et j'ai beaucoup appris, alors j'ai décidé de contester l'édition du framework cette fois.
J'ai donc récemment acheté un livre, mais avant de commencer à étudier étape par étape, j'ai décidé d'écrire du code pour avoir un aperçu rapide du ** framework **.
Reportez-vous à la ** bibliothèque ** et à ** l'exemple ** de ** DeZero ** sur Github. J'ai écrit un code simple pour le traitement du langage naturel sur google colab en parcourant ** this **, donc je le laisserai comme mémorandum.
Le ** code google colab ** que j'ai créé est publié sur ** Github **, donc si vous aimez ** ce lien ** Cliquez sur .ipynb) pour le déplacer.
Quand j'ai essayé de faire du traitement du langage naturel du japonais, j'ai pensé que ce serait pratique s'il y avait quelque chose de facile à utiliser, comme ** MNIST ** pour le traitement d'image, alors j'ai fait quelque chose de similaire.
Pour la ** classe de jeu de données ** à créer, téléchargez ** "Je suis un chat" ** d'Aozora Bunko. Ensuite, après avoir supprimé la partie inutile, écrivez dans la division avec ** janome **, créez le dictionnaire et le corpus, puis créez les ** données de série chronologique ** et ** les données de réponse correctes suivantes **.
En guise de préparation préliminaire, installez le ** framework dezero ** avec ! Pip install dezero
, et installez la ** bibliothèque d'analyse morphologique janome ** avec! Pip install janome
.
Le nom de classe de l'ensemble de données est ** Neko **, et selon la méthode de dezero, ** hérite de la classe Dataset **, écrivez ** le contenu de traitement ** dans def prepare ()
, puis ** process. Écrivez la fonction requise ** dans.
import numpy as np
import dezero
from dezero.datasets import Dataset
from dezero.utils import get_file, cache_dir
import zipfile
import re
from janome.tokenizer import Tokenizer
class Neko(Dataset):
def prepare(self):
url = 'https://www.aozora.gr.jp/cards/000148/files/789_ruby_5639.zip'
file = get_file(url)
data = self.unzip(cache_dir + '/' + '789_ruby_5639.zip')
self.text = self.preprocess(cache_dir + '/' + 'wagahaiwa_nekodearu.txt')
self.wakati = self.keitaiso(self.text)
self.corpus, self.word_to_id, self.id_to_word = self.process(self.wakati)
self.data = np.array(self.corpus[:-1])
self.label = np.array(self.corpus[1:])
def unzip(self, file_path):
with zipfile.ZipFile(file_path) as existing_zip:
existing_zip.extractall(cache_dir)
def preprocess(self, file_path):
binarydata = open(file_path, 'rb').read()
text = binarydata.decode('shift_jis')
text = re.split(r'\-{5,}', text)[2] #Supprimer l'en-tête
text = re.split('Livre du bas:',text)[0] #Supprimer le pied de page
text = re.sub('|', '', text) # |Effacer
text = re.sub('[.+?]', '', text) #Supprimer la note d'entrée
text = re.sub(r'《.+?》', '', text) #Élimination du rubis
text = re.sub(r'\u3000', '', text) #Supprimer les blancs
text = re.sub(r'\r\n', '', text) #Supprimer les sauts de ligne
text = text[1:] #Supprimer le premier caractère (ajustement)
return text
def keitaiso(self, text):
t = Tokenizer()
output = t.tokenize(text, wakati=True)
return output
def process(self, text):
# 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 corpus, word_to_id, id_to_word
Le ** constructeur ** de la ** classe héritée ** Dataset ** (à def __init __ ()
) dit self.prepare ()
, donc la classe Neko est une ** instance. Ensuite, def prepare ()
fonctionnera **.
def prepare ()
utilise get_file (url) ʻ dans la bibliothèque dezero pour télécharger le fichier à partir de l''url
spécifiée et le sauvegarder dans cache_dir
. Pour google colab, cache_dir
est / root / .dezero
.
Après cela, quatre fonctions sont appelées en séquence pour effectuer le traitement. Enfin, remplacez ** corpus ** par «self.data» (données de séries chronologiques) et «self.label» (prochaine réponse correcte) selon la méthode.
Les variables text, wakati, corpus, word_to_id, id_to_word
sont chacune données self.
afin qu'elles puissent être appelées comme ** attributs ** une fois que la classe Neko est ** instanciée **. ..
def unzip ()
est une fonction qui décompresse le ** fichier zip ** téléchargé. def preprocess ()
est une fonction qui lit le fichier décompressé et renvoie le texte avec ** les parties inutiles telles que ruby et les sauts de ligne ** supprimées. def keitaiso ()
est une fonction qui analyse morphologiquement le texte et renvoie ** fractionnement **. def process ()
est une fonction qui crée des ** dictionnaires ** et ** corpus ** à partir de fractions.
Déplaçons-le réellement.
** Instanciez ** la classe Neko avec neko = Neko ()
pour télécharger le fichier et ** démarrer le processus **. Cela prend quelques dizaines de secondes, car il faut un peu de temps pour traiter la division de janome. Lorsque vous avez terminé, utilisons-le tout de suite.
neko.text
peut afficher du ** texte **, neko.wakati
peut afficher des ** fractions ** et neko.corpus
peut afficher ** corpus **. Le texte est dit solide, la division est une liste mot par mot et le corpus est le numéro depuis le début du mot de division (pas de duplication). Au fait, jetons un œil au dictionnaire.
neko.waord_to_id []
est un dictionnaire qui ** convertit les mots en mots **, et neko.id_to_word []
est un dictionnaire qui convertit les nombres ** en mots **. Regardons les données d'entraînement.
Vous pouvez voir que «neko.data» et «neko.label» sont décalés de un. Enfin, regardons la longueur des données et le nombre de mots dans le dictionnaire.
La ** longueur des données ** est de 205 815 et le nombre de mots dans le dictionnaire ** vovab_size ** est de 13 616.
Maintenant, écrivons le code du corps principal.
import numpy as np
import dezero
from dezero import Model
from dezero import SeqDataLoader
import dezero.functions as F
import dezero.layers as L
import random
from dezero import cuda
import textwrap
max_epoch = 70
batch_size = 30
vocab_size = len(neko.word_to_id)
wordvec_size = 650
hidden_size = 650
bptt_length = 30
class Lstm_nlp(Model):
def __init__(self, vocab_size, wordvec_size, hidden_size, out_size):
super().__init__()
self.embed = L.EmbedID(vocab_size, wordvec_size)
self.rnn = L.LSTM(hidden_size)
self.fc = L.Linear(out_size)
def reset_state(self): #Réinitialisation de l'état
self.rnn.reset_state()
def __call__(self, x): #Décrire le contenu de la connexion de la couche
y = self.embed(x)
y = self.rnn(y)
y = self.fc(y)
return y
Le modèle a une structure simple de ** couche d'intégration + couche LSTM + couche linéaire **. Entrez le EmbedID sous forme de nombre de mots (entier).
La taille de la matrice d'incorporation de mots pour EmbedID est ** vocal_size x wordvec_size **, donc elle est de 13616 x 650. La hidden_size
de LSTM est 650, ce qui est identique à wordvec_size. Et la taille de sortie de Linear ʻout_size` est 13616, ce qui est identique à vocab_size.
Décrivez ** le contenu de la connexion de chaque couche ** dans def __call __ ()
. Le contenu décrit ici peut être appelé en donnant des arguments à l'instance créée comme une fonction. Par exemple, si vous instanciez avec model = Lstm_nlp (....)
, vous pouvez déplacer la partie de def __call __ ()
avec y = model (x)
. En d'autres termes, ce que l'on appelle prédire peut être réalisé avec cela. C'est intelligent.
model = Lstm_nlp(vocab_size, wordvec_size, hidden_size, vocab_size) #Génération de modèle
dataloader = SeqDataLoader(neko, batch_size=batch_size) #Génération de chargeur de données
seqlen = len(neko)
optimizer = dezero.optimizers.Adam().setup(model) #La méthode d'optimisation est Adam
#Jugement de présence / absence et traitement du GPU
if dezero.cuda.gpu_enable: #Si le GPU est activé, procédez comme suit
dataloader.to_gpu() #Chargeur de données vers GPU
model.to_gpu() #Modèle vers GPU
Le chargeur de données utilise «SeqDataLoader» pour les données de séries chronologiques. Étant donné que l'ordre des données de séries chronologiques change lorsqu'elles sont mélangées, la méthode d'extraction de plusieurs données en divisant les données de séries chronologiques à intervalles réguliers est adoptée.
Si le GPU est disponible, ʻif dezero.cuda.gpu_enable: `sera True, auquel cas il enverra le chargeur de données et le modèle au GPU.
#Boucle d'apprentissage
for epoch in range(max_epoch):
model.reset_state()
loss, count = 0, 0
for x, t in dataloader:
y = model(x) #Propagation vers l'avant
#Degré d'apparition du mot suivant y(vocab_vecteur dimensionnel de taille)Softmax est-il traité et correct(Un vecteur chaud)Calcul des pertes avec
#Cependant, l'entrée t est le numéro d'index dans lequel 1 du vecteur chaud se trouve.(entier)
loss += F.softmax_cross_entropy_simple(y, t)
count += 1
if count % bptt_length == 0 or count == seqlen:
model.cleargrads() #Initialisation de la différenciation
loss.backward() #Rétropropagation
loss.unchain_backward() #Revenir au graphique de calcul et rompre la connexion
optimizer.update() #Mise à jour du poids
avg_loss = float(loss.data) / count
print('| epoch %d | loss %f' % (epoch + 1, avg_loss))
#Génération de phrases
model.reset_state() #Réinitialiser l'état
with dezero.no_grad(): #Ne pas mettre à jour les poids
text = []
x = random.randint(0,vocab_size) #Choisissez au hasard le premier numéro de mot
while len(text) < 100: #Répétez jusqu'à 100 mots
x = np.array(int(x))
y = model(x) #y est le degré d'apparition du mot suivant(vocab_vecteur dimensionnel de taille)
p = F.softmax_simple(y, axis=0) #Multipliez par softmax pour obtenir la probabilité d'apparition
xp = cuda.get_array_module(p) #XP avec GPU=xp sans cp=np
sampled = xp.random.choice(len(p.data), size=1, p=p.data) #Nombres tenant compte de la probabilité d'apparition(indice)Choisir
word = neko.id_to_word[int(sampled)] #Convertir des nombres en mots
text.append(word) #Ajouter un mot au texte
x = sampled #Définir échantillonné sur l'entrée suivante
text = ''.join(text)
print(textwrap.fill(text, 60)) #Affichage avec une pause à 60 caractères
C'est une boucle d'apprentissage. ** Propagation vers l'avant ** avec y = model (x)
et calculer la perte avec loss + = F.softmax_cross_entropy_simple (y, t)
.
À ce stade, y est un ** vecteur ** (dimension vocab_size) représentant le ** degré d'apparence ** du mot suivant, qui est multiplié par softmax pour obtenir la ** probabilité d'apparition **, et ** un chaud La perte est calculée à partir des données de réponse correctes **. Cependant, l'entrée t est le ** nombre (entier) ** du vecteur one-hot dans lequel 1 se trouve.
ʻSi count% bptt_length == 0 ou count == seqlen: ʻSi count est un multiple entier de bptt_length ou va à la fin, rétropropagez et mettez à jour le poids.
Ensuite, 100 mots sont générés pour chaque eopch. Tout d'abord, utilisez model.reset_state ()
pour réinitialiser l'état, et avec dezero.no_grad ():
pour garder les poids inchangés. Ensuite, avec x = random.randint (0, vocal_size)
, la valeur initiale du mot est déterminée aléatoirement à partir d'un entier de 0 à vocal_size, et le mot suivant est prédit. Une phrase est générée en répétant une autre prédiction basée sur le mot prédit.
p = F.softmax_simple (y, axis = 0)
multiplie y par softmax pour trouver la probabilité d'occurrence du mot suivant, etxp.random.choice ()
est un mot aléatoire selon cette probabilité. Est sélectionné.
La raison pour laquelle xp.random.choice ()
commence par ** xp ** est ** np ** (numpy) lorsque le premier caractère est déplacé par le CPU, et ** cp lorsqu'il est déplacé par le GPU. C'est parce qu'il doit être changé en ** (cupy). Par conséquent, jugez par xp = cuda.get_array_module (p)
et remplacez xp = np par CPU et xp = cp par GPU.
Maintenant, déplaçons l'unité principale.
Lorsque vous exécutez le code du corps principal, il apprend l'ordre des mots «Je suis un chat» et génère une phrase pour chaque époque. Cela prend environ 1 à 2 minutes par époque. Après avoir appris dans une certaine mesure, cela ressemble à ceci. C'est aussi amusant de voir les phrases devenir plus comme ça petit à petit.
L'impression d'écrire le code en l'imitant est qu'il s'agit d'un ** framework simple ** écrit en ** tout python **, donc le contenu est facile à comprendre ** et le code est facile à écrire, mais le degré de liberté est élevé * J'ai eu une bonne impression de *. Pendant cette période, j'aimerais étudier le contenu du framework DeZero.
Recommended Posts