Python + Keras + LSTM: haz un traductor de texto en media hora

Hola Habr

En la parte anterior, busqué crear un reconocimiento de texto simple basado en una red neuronal. Hoy utilizaremos un enfoque similar y escribiremos un traductor automático de textos del inglés al alemán.



Para aquellos que estén interesados ​​en cómo funciona esto, los detalles están debajo del corte.

Nota : este proyecto de utilizar una red neuronal para la traducción es exclusivamente educativo, por lo tanto, no se considera la pregunta "por qué". Solo por diversión. No me propongo demostrar que este o aquel método es mejor o peor, solo fue interesante verificar lo que sucede. El método utilizado a continuación es, por supuesto, simplificado, pero espero que nadie espere que escribamos un segundo Lingvo en media hora.

Recogida de datos


Se utilizó un archivo encontrado en la red que contenía frases en inglés y alemán separadas por pestañas como el conjunto de datos de origen. Un conjunto de frases se parece a esto:

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

El archivo contiene 192 mil líneas y tiene un tamaño de 13 MB. Cargamos el texto en la memoria y dividimos los datos en dos bloques, para palabras en inglés y alemán.

 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() 

También convertimos todas las palabras a minúsculas y eliminamos los signos de puntuación.

El siguiente paso es preparar los datos para la red neuronal. La red no sabe qué son las palabras y trabaja exclusivamente con números. Afortunadamente para nosotros, Keras ya tiene incorporada la clase Tokenizer, que reemplaza las palabras en oraciones con códigos digitales.

Su uso se ilustra simplemente con un ejemplo:

 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) 

La frase "ser o no ser" será reemplazada por la matriz [1 2 3 4 1 2 0 0], donde no es difícil adivinar, 1 = a, 2 = ser, 3 = o, 4 = no. Ya podemos enviar estos datos a la red neuronal.

Entrenamiento de redes neuronales


Nuestros datos están listos digitalmente. Dividimos la matriz en dos bloques para los datos de entrada (líneas inglesas) y de salida (líneas alemanas). También prepararemos una unidad separada para validar el proceso de aprendizaje.

 # 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]) 

Ahora podemos crear un modelo de red neuronal y comenzar su entrenamiento. Como puede ver, la red neuronal contiene capas LSTM que tienen celdas de memoria. Aunque probablemente funcionaría en una red "regular", aquellos que lo deseen pueden verificar por su cuenta.

 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') 

El entrenamiento en sí se parece a esto:



El proceso, como puede ver, no es rápido, y toma aproximadamente media hora en un Core i7 + GeForce 1060 para un conjunto de 30 mil líneas. Al final del entrenamiento (solo debe hacerse una vez), el modelo se guarda en un archivo y luego se puede reutilizar.

Para obtener la traducción, utilizamos la función predic_classes, cuya entrada enviamos algunas frases simples. La función get_word se usa para invertir palabras en números.

 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)) 

Resultados


Ahora, en realidad, lo más curioso son los resultados. Es interesante ver cómo la red neuronal aprende y "recuerda" la correspondencia entre las frases en inglés y alemán. Específicamente tomé 2 frases más fáciles y 2 más difíciles de ver la diferencia.

5 minutos de entrenamiento

"Hoy hace buen tiempo" - "das ist ist tom"
"Mi nombre es tom" - "wie für tom tom"
"Cuántos años tienes" - "wie geht ist es"
"Dónde está la tienda más cercana" - "wo ist der"

Como puede ver, hasta ahora hay pocos "golpes". Un fragmento de la frase "cuántos años tienes" confundió la red neuronal con la frase "cómo estás" y produjo la traducción "wie geht ist es" (¿cómo estás?). En la frase “dónde está ...” la red neuronal identificó solo el verbo dónde y produjo la traducción “wo ist der” (¿dónde está?), Que, en principio, no carece de significado. En general, también se traduce en un recién llegado alemán en el grupo A1;)

10 minutos de entrenamiento

“Hoy hace buen tiempo” - “das haus ist bereit”
"Mi nombre es tom" - "mein heiße heiße tom"
"Cuántos años tienes" - "wie alt sind sie"
"Dónde está la tienda más cercana" - "wo ist paris"

Algún progreso es visible. La primera frase está completamente fuera de lugar. En la segunda frase, la red neuronal "aprendió" el verbo heißen (llamado), pero "mein heiße heiße tom" sigue siendo incorrecto, aunque ya puede adivinar el significado. La tercera frase ya es correcta. En el cuarto, la primera parte correcta es "wo ist", pero la tienda más cercana fue reemplazada por alguna razón por París.

30 minutos de entrenamiento

“Hoy hace buen tiempo” - “das ist ist aus”
"Mi nombre es tom" - "" tom "es mi nombre"
"Cuántos años tienes" - "wie alt sind sie"
"Dónde está la tienda más cercana" - "wo ist der"

Como puede ver, la segunda frase se ha vuelto correcta, aunque el diseño parece algo inusual. La tercera frase es correcta, pero las frases primera y cuarta aún no se han "aprendido". Con esto para ahorrar electricidad, terminé el proceso.

Conclusión


Como puede ver, en principio, esto funciona. Me gustaría memorizar un nuevo idioma con tanta velocidad :) Por supuesto, el resultado no es perfecto hasta ahora, pero entrenar en un conjunto completo de 190 mil líneas tomaría más de una hora.

Para aquellos que quieran experimentar por su cuenta, el código fuente está bajo el spoiler. El programa teóricamente puede usar cualquier par de idiomas, no solo inglés y alemán (el archivo debe estar en codificación UTF-8). El tema de la calidad de la traducción también permanece abierto, hay algo que probar.

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() 


El diccionario en sí es demasiado grande para adjuntarlo al artículo, el enlace está en los comentarios.

Como de costumbre, todos los experimentos exitosos.

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


All Articles