Python + Keras + LSTM: faites un traducteur de texte en une demi-heure

Salut, Habr.

Dans la partie précédente, j'ai envisagé de créer une simple reconnaissance de texte basée sur un réseau neuronal. Aujourd'hui, nous utiliserons une approche similaire et rédigerons un traducteur automatique de textes de l'anglais vers l'allemand.



Pour ceux qui sont intéressés par la façon dont cela fonctionne, les détails sont sous la coupe.

Remarque : ce projet d'utilisation d'un réseau de neurones pour la traduction est exclusivement éducatif, donc la question «pourquoi» n'est pas considérée. Juste pour le plaisir. Je ne cherche pas à prouver que telle ou telle méthode est meilleure ou pire, c'était juste intéressant de vérifier ce qui se passe. La méthode utilisée ci-dessous est bien sûr simplifiée, mais j'espère que personne n'espère que nous écrirons un deuxième Lingvo dans une demi-heure.

Collecte de données


Un fichier trouvé sur le réseau contenant des phrases anglaises et allemandes séparées par des tabulations a été utilisé comme ensemble de données source. Un ensemble de phrases ressemble à ceci:

Hi. Hallo! Hi. Grüß Gott! Run! Lauf! Wow! Potzdonner! Wow! Donnerwetter! Fire! Feuer! Help! Hilfe! Help! Zu Hülf! Stop! Stopp! Wait! Warte! Go on. Mach weiter. Hello! Hallo! I ran. Ich rannte. I see. Ich verstehe. ... 

Le fichier contient 192 000 lignes et a une taille de 13 Mo. Nous chargeons le texte en mémoire et divisons les données en deux blocs, pour les mots anglais et allemands.

 def read_text(filename): with open(filename, mode='rt', encoding='utf-8') as file: text = file.read() sents = text.strip().split('\n') return [i.split('\t') for i in sents] data = read_text("deutch.txt") deu_eng = np.array(data) deu_eng = deu_eng[:30000,:] print("Dictionary size:", deu_eng.shape) # Remove punctuation deu_eng[:,0] = [s.translate(str.maketrans('', '', string.punctuation)) for s in deu_eng[:,0]] deu_eng[:,1] = [s.translate(str.maketrans('', '', string.punctuation)) for s in deu_eng[:,1]] # convert text to lowercase for i in range(len(deu_eng)): deu_eng[i,0] = deu_eng[i,0].lower() deu_eng[i,1] = deu_eng[i,1].lower() 

Nous avons également converti tous les mots en minuscules et supprimé les signes de ponctuation.

L'étape suivante consiste à préparer les données pour le réseau neuronal. Le réseau ne sait pas ce que sont les mots et fonctionne exclusivement avec des nombres. Heureusement pour nous, keras a déjà intégré la classe Tokenizer, qui remplace les mots dans les phrases par des codes numériques.

Son utilisation est simplement illustrée par un exemple:

 from keras.preprocessing.text import Tokenizer from keras.preprocessing.sequence import pad_sequences s = "To be or not to be" eng_tokenizer = Tokenizer() eng_tokenizer.fit_on_texts([s]) seq = eng_tokenizer.texts_to_sequences([s]) seq = pad_sequences(seq, maxlen=8, padding='post') print(seq) 

L'expression «être ou ne pas être» sera remplacée par le tableau [1 2 3 4 1 2 0 0], où il n'est pas difficile de deviner, 1 = à, 2 = être, 3 = ou, 4 = pas. Nous pouvons déjà soumettre ces données au réseau neuronal.

Formation au réseau de neurones


Nos données sont prêtes numériquement. Nous divisons le tableau en deux blocs pour les données d'entrée (lignes anglaises) et de sortie (lignes allemandes). Nous préparerons également une unité distincte pour valider le processus d'apprentissage.

 # split data into train and test set train, test = train_test_split(deu_eng, test_size=0.2, random_state=12) # prepare training data trainX = encode_sequences(eng_tokenizer, eng_length, train[:, 0]) trainY = encode_sequences(deu_tokenizer, deu_length, train[:, 1]) # prepare validation data testX = encode_sequences(eng_tokenizer, eng_length, test[:, 0]) testY = encode_sequences(deu_tokenizer, deu_length, test[:, 1]) 

Nous pouvons maintenant créer un modèle de réseau neuronal et commencer sa formation. Comme vous pouvez le voir, le réseau neuronal contient des couches LSTM ayant des cellules mémoire. Bien que cela fonctionnerait probablement sur un réseau «régulier», ceux qui le souhaitent peuvent vérifier par eux-mêmes.

 def make_model(in_vocab, out_vocab, in_timesteps, out_timesteps, n): model = Sequential() model.add(Embedding(in_vocab, n, input_length=in_timesteps, mask_zero=True)) model.add(LSTM(n)) model.add(Dropout(0.3)) model.add(RepeatVector(out_timesteps)) model.add(LSTM(n, return_sequences=True)) model.add(Dropout(0.3)) model.add(Dense(out_vocab, activation='softmax')) model.compile(optimizer=optimizers.RMSprop(lr=0.001), loss='sparse_categorical_crossentropy') return model eng_vocab_size = len(eng_tokenizer.word_index) + 1 deu_vocab_size = len(deu_tokenizer.word_index) + 1 eng_length, deu_length = 8, 8 model = make_model(eng_vocab_size, deu_vocab_size, eng_length, deu_length, 512) num_epochs = 40 model.fit(trainX, trainY.reshape(trainY.shape[0], trainY.shape[1], 1), epochs=num_epochs, batch_size=512, validation_split=0.2, callbacks=None, verbose=1) model.save('en-de-model.h5') 

La formation elle-même ressemble à ceci:



Le processus, comme vous pouvez le voir, n'est pas rapide et prend environ une demi-heure sur un Core i7 + GeForce 1060 pour un ensemble de 30 000 lignes. À la fin de la formation (elle ne doit être effectuée qu'une seule fois), le modèle est enregistré dans un fichier, puis il peut être réutilisé.

Pour obtenir la traduction, nous utilisons la fonction Predict_classes, dont nous soumettons quelques phrases simples en entrée. La fonction get_word est utilisée pour inverser les mots en nombres.

 model = load_model('en-de-model.h5') def get_word(n, tokenizer): if n == 0: return "" for word, index in tokenizer.word_index.items(): if index == n: return word return "" phrs_enc = encode_sequences(eng_tokenizer, eng_length, ["the weather is nice today", "my name is tom", "how old are you", "where is the nearest shop"]) preds = model.predict_classes(phrs_enc) print("Preds:", preds.shape) print(preds[0]) print(get_word(preds[0][0], deu_tokenizer), get_word(preds[0][1], deu_tokenizer), get_word(preds[0][2], deu_tokenizer), get_word(preds[0][3], deu_tokenizer)) print(preds[1]) print(get_word(preds[1][0], deu_tokenizer), get_word(preds[1][1], deu_tokenizer), get_word(preds[1][2], deu_tokenizer), get_word(preds[1][3], deu_tokenizer)) print(preds[2]) print(get_word(preds[2][0], deu_tokenizer), get_word(preds[2][1], deu_tokenizer), get_word(preds[2][2], deu_tokenizer), get_word(preds[2][3], deu_tokenizer)) print(preds[3]) print(get_word(preds[3][0], deu_tokenizer), get_word(preds[3][1], deu_tokenizer), get_word(preds[3][2], deu_tokenizer), get_word(preds[3][3], deu_tokenizer)) 

Résultats


Maintenant, en fait, la chose la plus curieuse, ce sont les résultats. Il est intéressant de voir comment le réseau neuronal apprend et «se souvient» de la correspondance entre les phrases anglaises et allemandes. J'ai spécifiquement pris 2 phrases plus faciles et 2 plus difficiles à voir la différence.

5 minutes de formation

«Il fait beau aujourd'hui» - «das ist ist tom»
"Mon nom est tom" - "wie für tom tom"
"Quel âge avez-vous" - "wie geht ist es"
"Où est le magasin le plus proche" - "wo ist der"

Comme vous pouvez le voir, il n'y a pour l'instant que peu de «hits». Un fragment de l'expression «quel âge avez-vous» a confondu le réseau neuronal avec l'expression «comment allez-vous» et a produit la traduction «wie geht ist es» (comment allez-vous?). Dans la phrase «où est ...», le réseau neuronal n'a identifié que le verbe où et a produit la traduction «wo ist der» (où est-il?), Qui, en principe, n'est pas dénué de sens. En général, à peu près la même chose que traduit en allemand un nouveau venu dans le groupe A1;)

10 minutes de formation

«Il fait beau aujourd'hui» - «das haus ist bereit»
«Je m'appelle Tom» - «mein heiße heiße tom»
"Quel âge avez-vous" - "wie alt sind sie"
"Où est le magasin le plus proche" - "wo ist paris"

Des progrès sont visibles. La première phrase est complètement hors de propos. Dans la deuxième phrase, le réseau neuronal a «appris» le verbe heißen (appelé), mais «mein heiße heiße tom» est toujours incorrect, bien que vous puissiez déjà en deviner le sens. La troisième phrase est déjà correcte. Dans le quatrième, la première partie correcte est «wo ist», mais le magasin le plus proche a été pour une raison quelconque remplacé par paris.

30 minutes de formation

«Il fait beau aujourd'hui» - «das ist ist aus»
"Je m'appelle Tom" - "" Tom "ist mein name"
"Quel âge avez-vous" - "wie alt sind sie"
"Où est le magasin le plus proche" - "wo ist der"

Comme vous pouvez le voir, la deuxième phrase est devenue correcte, bien que le design semble quelque peu inhabituel. La troisième phrase est correcte, mais les première et quatrième phrases n'ont pas encore été «apprises». Avec cela afin d'économiser de l'électricité, j'ai terminé le processus.

Conclusion


Comme vous pouvez le voir, cela fonctionne en principe. Je voudrais mémoriser une nouvelle langue avec une telle vitesse :) Bien sûr, le résultat n'est pas parfait jusqu'à présent, mais la formation sur un ensemble complet de 190 mille lignes prendrait plus d'une heure.

Pour ceux qui veulent expérimenter par eux-mêmes, le code source est sous le spoiler. Le programme peut théoriquement utiliser n'importe quelle paire de langues, pas seulement l'anglais et l'allemand (le fichier doit être au format UTF-8). La question de la qualité de la traduction reste également ouverte, il y a quelque chose à tester.

keras_translate.py
 import os # os.environ["CUDA_VISIBLE_DEVICES"] = "-1" # Force CPU os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3' # 0 = all messages are logged, 3 - INFO, WARNING, and ERROR messages are not printed import string import re import numpy as np import pandas as pd from keras.models import Sequential from keras.layers import Dense, LSTM, Embedding, RepeatVector from keras.preprocessing.text import Tokenizer from keras.callbacks import ModelCheckpoint from keras.preprocessing.sequence import pad_sequences from keras.models import load_model from keras import optimizers from sklearn.model_selection import train_test_split import matplotlib.pyplot as plt pd.set_option('display.max_colwidth', 200) # Read raw text file def read_text(filename): with open(filename, mode='rt', encoding='utf-8') as file: text = file.read() sents = text.strip().split('\n') return [i.split('\t') for i in sents] data = read_text("deutch.txt") deu_eng = np.array(data) deu_eng = deu_eng[:30000,:] print("Dictionary size:", deu_eng.shape) # Remove punctuation deu_eng[:,0] = [s.translate(str.maketrans('', '', string.punctuation)) for s in deu_eng[:,0]] deu_eng[:,1] = [s.translate(str.maketrans('', '', string.punctuation)) for s in deu_eng[:,1]] # Convert text to lowercase for i in range(len(deu_eng)): deu_eng[i,0] = deu_eng[i,0].lower() deu_eng[i,1] = deu_eng[i,1].lower() # Prepare English tokenizer eng_tokenizer = Tokenizer() eng_tokenizer.fit_on_texts(deu_eng[:, 0]) eng_vocab_size = len(eng_tokenizer.word_index) + 1 eng_length = 8 # Prepare Deutch tokenizer deu_tokenizer = Tokenizer() deu_tokenizer.fit_on_texts(deu_eng[:, 1]) deu_vocab_size = len(deu_tokenizer.word_index) + 1 deu_length = 8 # Encode and pad sequences def encode_sequences(tokenizer, length, lines): # integer encode sequences seq = tokenizer.texts_to_sequences(lines) # pad sequences with 0 values seq = pad_sequences(seq, maxlen=length, padding='post') return seq # Split data into train and test set train, test = train_test_split(deu_eng, test_size=0.2, random_state=12) # Prepare training data trainX = encode_sequences(eng_tokenizer, eng_length, train[:, 0]) trainY = encode_sequences(deu_tokenizer, deu_length, train[:, 1]) # Prepare validation data testX = encode_sequences(eng_tokenizer, eng_length, test[:, 0]) testY = encode_sequences(deu_tokenizer, deu_length, test[:, 1]) # Build NMT model def make_model(in_vocab, out_vocab, in_timesteps, out_timesteps, n): model = Sequential() model.add(Embedding(in_vocab, n, input_length=in_timesteps, mask_zero=True)) model.add(LSTM(n)) model.add(Dropout(0.3)) model.add(RepeatVector(out_timesteps)) model.add(LSTM(n, return_sequences=True)) model.add(Dropout(0.3)) model.add(Dense(out_vocab, activation='softmax')) model.compile(optimizer=optimizers.RMSprop(lr=0.001), loss='sparse_categorical_crossentropy') return model print("deu_vocab_size:", deu_vocab_size, deu_length) print("eng_vocab_size:", eng_vocab_size, eng_length) # Model compilation (with 512 hidden units) model = make_model(eng_vocab_size, deu_vocab_size, eng_length, deu_length, 512) # Train model num_epochs = 250 history = model.fit(trainX, trainY.reshape(trainY.shape[0], trainY.shape[1], 1), epochs=num_epochs, batch_size=512, validation_split=0.2, callbacks=None, verbose=1) # plt.plot(history.history['loss']) # plt.plot(history.history['val_loss']) # plt.legend(['train','validation']) # plt.show() model.save('en-de-model.h5') # Load model model = load_model('en-de-model.h5') def get_word(n, tokenizer): if n == 0: return "" for word, index in tokenizer.word_index.items(): if index == n: return word return "" phrs_enc = encode_sequences(eng_tokenizer, eng_length, ["the weather is nice today", "my name is tom", "how old are you", "where is the nearest shop"]) print("phrs_enc:", phrs_enc.shape) preds = model.predict_classes(phrs_enc) print("Preds:", preds.shape) print(preds[0]) print(get_word(preds[0][0], deu_tokenizer), get_word(preds[0][1], deu_tokenizer), get_word(preds[0][2], deu_tokenizer), get_word(preds[0][3], deu_tokenizer)) print(preds[1]) print(get_word(preds[1][0], deu_tokenizer), get_word(preds[1][1], deu_tokenizer), get_word(preds[1][2], deu_tokenizer), get_word(preds[1][3], deu_tokenizer)) print(preds[2]) print(get_word(preds[2][0], deu_tokenizer), get_word(preds[2][1], deu_tokenizer), get_word(preds[2][2], deu_tokenizer), get_word(preds[2][3], deu_tokenizer)) print(preds[3]) print(get_word(preds[3][0], deu_tokenizer), get_word(preds[3][1], deu_tokenizer), get_word(preds[3][2], deu_tokenizer), get_word(preds[3][3], deu_tokenizer)) print() 


Le dictionnaire lui-même est trop volumineux pour être attaché à l'article, le lien est dans les commentaires.

Comme d'habitude, toutes les expériences réussies.

Source: https://habr.com/ru/post/fr470706/


All Articles