Python + Keras + LSTM: faça um tradutor de texto em meia hora

Oi Habr.

Na parte anterior, observei a criação de um reconhecimento de texto simples baseado em uma rede neural. Hoje vamos usar uma abordagem semelhante e escrever um tradutor automático de textos de inglês para alemão.



Para aqueles que estão interessados ​​em como isso funciona, os detalhes estão ocultos.

Nota : este projeto de utilização de uma rede neural para tradução é exclusivamente educacional, portanto a questão “por que” não é considerada. Apenas por diversão. Não pretendo provar que esse ou aquele método seja melhor ou pior, apenas foi interessante verificar o que acontece. O método usado abaixo é, obviamente, simplificado, mas espero que ninguém espere escrever um segundo Lingvo em meia hora.

Coleta de dados


Um arquivo encontrado na rede contendo frases em inglês e alemão separadas por guias foi usado como o conjunto de dados de origem. Um conjunto de frases é mais ou menos assim:

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

O arquivo contém 192 mil linhas e um tamanho de 13 MB. Carregamos o texto na memória e dividimos os dados em dois blocos, para palavras em inglês e alemão.

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

Também convertemos todas as palavras para minúsculas e removemos os sinais de pontuação.

O próximo passo é preparar os dados para a rede neural. A rede não sabe o que são palavras e trabalha exclusivamente com números. Felizmente para nós, o keras já possui a classe Tokenizer incorporada, que substitui palavras em frases por códigos digitais.

Seu uso é simplesmente ilustrado com um exemplo:

 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) 

A frase “ser ou não ser” será substituída pela matriz [1 2 3 4 1 2 0 0], onde não é difícil adivinhar, 1 = para, 2 = ser, 3 = ou, 4 = não. Já podemos enviar esses dados para a rede neural.

Treinamento em redes neurais


Nossos dados estão prontos digitalmente. Dividimos a matriz em dois blocos para dados de entrada (linhas em inglês) e saída (linhas em alemão). Também prepararemos uma unidade separada para validar o processo de aprendizado.

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

Agora podemos criar um modelo de rede neural e iniciar seu treinamento. Como você pode ver, a rede neural contém camadas LSTM com células de memória. Embora provavelmente funcione em uma rede "regular", quem desejar pode verificar por conta própria.

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

O treinamento em si é mais ou menos assim:



O processo, como você pode ver, não é rápido e leva cerca de meia hora em uma Core i7 + GeForce 1060 para um conjunto de 30 mil linhas. No final do treinamento (ele precisa ser feito apenas uma vez), o modelo é salvo em um arquivo e pode ser reutilizado.

Para obter a tradução, usamos a função predict_classes, cuja entrada enviamos algumas frases simples. A função get_word é usada para inverter palavras em 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


Agora, na verdade, o mais curioso são os resultados. É interessante ver como a rede neural aprende e "lembra" a correspondência entre frases em inglês e alemão. Eu especificamente tomei 2 frases mais fáceis e 2 mais difíceis de ver a diferença.

5 minutos de treinamento

“O tempo está bom hoje” - “das ist ist tom”
"Meu nome é tom" - "wie für tom tom"
"Quantos anos você tem" - "wie geht ist es"
"Onde fica a loja mais próxima" - "wo ist der"

Como você pode ver, até agora existem poucos "hits". Um fragmento da frase “quantos anos você tem” confundiu a rede neural com a frase “como você está” e produziu a tradução “wie geht ist es” (como você está?). Na frase “onde está ...”, a rede neural identificou apenas o verbo where e produziu a tradução “wo ist der” (onde está?). Que, em princípio, não deixa de ter significado. Em geral, aproximadamente o mesmo que traduz para o alemão um recém-chegado ao grupo A1;)

10 minutos de treinamento

“O tempo está bom hoje” - “das haus ist bereit”
"Meu nome é tom" - "mein heiße heiße tom"
"Quantos anos você tem" - "wie alt sind sie"
"Onde fica a loja mais próxima" - "wo ist paris"

Algum progresso é visível. A primeira frase está completamente fora de lugar. Na segunda frase, a rede neural “aprendeu” o verbo heißen (chamado), mas “mein heiße heiße tom” ainda está incorreta, embora você já possa adivinhar o significado. A terceira frase já está correta. Na quarta, a primeira parte correta é “wo ist”, mas a loja mais próxima foi substituída por algum motivo por paris.

30 minutos de treinamento

“O tempo está bom hoje” - “das ist ist aus”
"Meu nome é tom" - "" tom "é meu nome"
"Quantos anos você tem" - "wie alt sind sie"
"Onde fica a loja mais próxima" - "wo ist der"

Como você pode ver, a segunda frase se tornou correta, embora o design pareça um tanto incomum. A terceira frase está correta, mas a primeira e a quarta frases ainda não foram "aprendidas". Com isso para economizar eletricidade, terminei o processo.

Conclusão


Como você pode ver, em princípio, isso funciona. Gostaria de memorizar um novo idioma com tanta velocidade :) Obviamente, o resultado não é perfeito até agora, mas o treinamento em um conjunto completo de 190 mil linhas levaria mais de uma hora.

Para aqueles que querem experimentar por conta própria, o código-fonte está sob o spoiler. Teoricamente, o programa pode usar qualquer par de idiomas, não apenas inglês e alemão (o arquivo deve estar na codificação UTF-8). A questão da qualidade da tradução também permanece em aberto, há algo a ser testado.

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


O dicionário em si é muito grande para anexar ao artigo, o link está nos comentários.

Como sempre, todas as experiências bem-sucedidas.

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


All Articles