8 October 2019

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

PythonProgrammingMachine learningPopular scienceArtificial Intelligence
Привет, Хабр.

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



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

Примечание: данный проект использования нейронной сети для перевода исключительно учебный, поэтому вопрос «зачем» не рассматривается. Просто для интереса. Я не ставлю целью доказать что тот или иной метод лучше или хуже, просто интересно было проверить, что получится. Метод, используемый ниже, разумеется, упрощенный, но надеюсь никто и не надеется, что мы напишем второй 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(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')

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



Процесс, как можно видеть, не быстрый, и занимает порядка получаса на 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(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()


Сам словарь слишком большой, чтобы приаттачить к статье, ссылка в комментариях.

Как обычно, всем удачных экспериментов.
Tags:pythonkerasнейронные сетимашинное обучение
Hubs: Python Programming Machine learning Popular science Artificial Intelligence
+16
9.8k 154
Comments 21