Python + Keras + LSTM: делаем переводчик текстов за полчаса

Привет Хабр.

В предыдущей части я рассматривал создание несложной распознавалки текста, основанной на нейронной сети. Сегодня мы применим аналогичный подход, и напишем автоматический переводчик текстов с английского на немецкий.

Python + Keras + LSTM: делаем переводчик текстов за полчаса

Для тех, кому интересно как это работает, подробности под катом.

Примечание: данный проект использования нейронной сети для перевода исключительно учебный, поэтому вопрос «зачем» не рассматривается. Просто для интереса. Я не ставлю целью доказать что тот или иной метод лучше или хуже, просто интересно было проверить, что получится. Метод, используемый ниже, разумеется, упрощенный, но надеюсь никто и не надеется, что мы напишем второй Lingvo за полчаса.

Сбор данных

В качестве исходного датасета был использован найденный в сети файл, содержащий английские и немецкие фразы, разделенные табуляцией. Набор фраз выглядит примерно так:

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

Файл содержит 192тыс строк и имеет размер 13МБайт. Загружаем текст в память и разбиваем данные на два блока, для английских и немецких слов.

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

Также мы перевели все слова в нижний регистр и убрали знаки препинания.

Следующим шагом мы должны подготовить данные для нейронной сети. Сеть не знает что такое слова, и работает исключительно с цифрами. К счастью для нас, в keras уже встроен класс Tokenizer, который заменяет слова в предложениях их цифровыми кодами.

Его использование просто проиллюстрировать примером:

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) 

Фраза «to be or not to be» будет заменена массивом [1 2 3 4 1 2 0 0], где как не сложно догадаться, 1=to, 2=be, 3=or, 4=not. Эти данные мы уже можем подавать на нейросеть.

Обучение нейронной сети

Наши данные в цифровом виде готовы. Разобьем массив на два блока для входных (английские строки) и выходных (немецкие строки) данных. Также подготовим отдельный блок для валидации процесса обучения.

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

Теперь мы можем создать модель нейронной сети и запустить её обучение. Как можно видеть, нейронная сеть содержит слои LSTM, имеющие ячейки памяти. Хотя возможно, заработало бы и на «обычной» сети, желающие могут проверить самостоятельно.

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(RepeatVector(out_timesteps))     model.add(LSTM(n, return_sequences=True))     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') 

Само обучение выглядит примерно так:

Процесс, как можно видеть, не быстрый, и занимает порядка получаса на Core i7 + GeForce 1060 для набора из 30тыс строк. По окончании обучения (его нужно сделать всего один раз) модель сохраняется в файл, и дальше её можно использовать повторно.

Для получения перевода воспользуемся функцией predict_classes, на вход которой подадим несколько несложных фраз. Функция get_word используется для обратного преобразования слов в числа.

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

Результаты

Теперь собственно, самое любопытное — результаты. Интересно посмотреть, как обучается нейронная сеть и «запоминает» соответствия между английскими и немецкими фразами. Я специально взял 2 фразы попроще и 2 посложнее, чтобы увидеть разницу.

5 минут обучения
«the weather is nice today» — «das ist ist tom»
«my name is tom» — «wie für tom tom»
«how old are you» — «wie geht ist es»
«where is the nearest shop» — «wo ist der»

Как можно видеть, пока «попаданий» немного. Фрагмент фразы «how old are you» нейросеть «спутала» с фразой «how are you» и выдала перевод «wie geht ist es» (как дела?). Во фразе «where is …» нейросеть определила только глагол where и выдала перевод «wo ist der» (где это?), что в принципе, не лишено смысла. В общем, примерно также переводит на немецкий новичок в группе А1 😉

10 минут обучения
«the weather is nice today» — «das haus ist bereit»
«my name is tom» — «mein heiße heiße tom»
«how old are you» — «wie alt sind sie»
«where is the nearest shop» — «wo ist paris»

Виден некий прогресс. Первая фраза совсем невпопад. Во второй фразе нейросеть «выучила» глагол heißen (называться), но «mein heiße heiße tom» все равно некорректно, хотя о смысле уже можно догадаться. Третья фраза уже правильная. В четвертой правильная первая часть «wo ist», но nearest shop почему-то было заменено на paris.

30 минут обучения
«the weather is nice today» — «das ist ist aus»
«my name is tom» — ««tom» ist mein name»
«how old are you» — «wie alt sind sie»
«where is the nearest shop» — «wo ist der»

Как можно видеть, вторая фраза стала правильной, хотя конструкция выглядит несколько непривычно. Третья фраза правильная, ну а 1я и 4я фразы пока так и не были «выучены». На этом я с целью экономии электроэнергии закончил процесс.

Заключение

Как можно видеть, в принципе это работает. Хотел бы я с такой скоростью запоминать новый язык 🙂 Конечно, результат пока что не идеален, но обучение на полном наборе в 190тыс строк заняло бы не один час.

Для желающих поэкспериментировать самостоятельно, исходный код под спойлером. Программа теоретически может использовать любую пару языков, не только английский и немецкий (файл должен быть в кодировке UTF-8). Вопрос качества перевода тоже остается открытым, здесь есть что потестировать.

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(RepeatVector(out_timesteps))     model.add(LSTM(n, return_sequences=True))     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() 

Как обычно, всем удачных экспериментов.

 
Источник

Читайте также