27  项目: 开发神经机器翻译模型

机器翻译是一项具有挑战性的任务,传统上涉及使用高度复杂的语言知识开发的大型统计模型。神经机器翻译是利用深度神经网络解决机器翻译问题。在本教程中,您将了解如何开发用于将德语短语翻译成英语的神经机器翻译系统。完成本教程后,您将了解:

  • 如何清理和准备数据训练神经机器翻译系统。
  • 如何开发机器翻译的编码器-解码器模型。
  • 如何使用已训练的模型推断新的输入短语并评估模型性能。

27.1 教程概述

本教程分为以下几部分:

  1. 德语到英语翻译数据集
  2. 准备文本数据
  3. 训练神经翻译模型
  4. 评估神经翻译模型

27.2 德语到英语翻译数据集

在本教程中,我们将使用德语到英语术语的数据集作为语言学习的抽认卡的基础。该数据集可从ManyThings.org网站获得,其中的示例来自Tatoeba项目。该数据集由德语短语及其英语对应词组成,旨在与Anki闪卡软件一起使用。

将数据集下载到当前工作目录并解压缩;例如:

unzip deu-eng.zip

代码清单27.1:解压缩数据集

您将拥有一个名为deu.txt的文件,其中包含176,692对英语到德语阶段,每行一对,使用tab制表符作为分割器。例如,文件的前5行如下所示:

Hi. Hallo!

Hi. Grüß Gott!

Run! Lauf!

Wow! Potzdonner!

Wow! Donnerwetter!

代码清单27.2:原始数据集的示例(Unicode字符规范化)

我们将预测问题框定为德语中的一系列单词作为输入,翻译或预测英语单词的序列。我们将开发的模型将适用于一些德语初学者短语。

27.3 准备文本数据

下一步是准备好文本数据以进行建模。查看原始数据并记下您在数据清理操作中可能需要处理的内容。例如,以下是我在审查原始数据时注意到的一些观察结果:

  • 有标点符号。
  • 该文本包含大写和小写。
  • 德语中有特殊字符。
  • 英语中有重复的短语,德语中有不同的翻译。
  • 该文件按句子长度排序,在文件末尾有非常长的句子。

一个好的文本清理过程可以处理这些观察的一部分或全部内容。数据准备分为两部分:

  • 清洗文本
  • 分割文本

27.3.1 清洗文字

首先,我们必须以保留Unicode德语字符的方式加载数据。下面的函数load_doc()将把文件以文本块方式加载。

# load doc into memory
def load_doc(filename):
   # open the file as read only
   file = open(filename, mode='rt', encoding='utf-8')
   # read all text
   text = file.read()
   # close the file
   file.close()
   return text

代码清单27.3:将文件加载到内存的函数

每行包含一对短语,首先是英语,然后是德语,由制表符分隔。我们必须逐行拆分加载的文本,然后按短语拆分。下面的to_pairs()函数将拆分加载的文本。

# split a loaded document into sentences
def to_pairs(doc):
   lines = doc.strip().split('\n')
   pairs = [line.split('\t') for line in  lines]
   return pairs

代码清单27.4:将行拆分成对的函数

我们现在准备清洗每一句话。我们将执行的具体清洗操作如下:

  • 删除所有不可打印的字符。
  • 删除所有标点字符。
  • 将所有Unicode字符规范化为ASCII(例如拉丁字符)。
  • 将案例规范化为小写。
  • 删除任何非字母的剩余令牌。

我们将对加载的数据集中每对的每个短语执行这些操作。Clean_pair()函数实现了这些操作。

# clean a list of lines
def clean_pairs(lines):
   cleaned = list()
   # prepare regex for char filtering
   re_print = re.compile('[^%s]' % re.escape(string.printable))
   # prepare translation table for removing punctuation
   table = str.maketrans('', '', string.punctuation)
   for pair in lines:
      clean_pair = list()
      for line in pair:
         # normalize unicode characters
         line = normalize('NFD', line).encode('ascii', 'ignore')
         line = line.decode('UTF-8')
         # tokenize on white space
         line = line.split()
         # convert to lowercase
         line = [word.lower() for word in line]
         # remove punctuation from each token
         line = [word.translate(table) for word in line]
         # remove non-printable chars form each token
         line = [re_print.sub('', w) for w in line]
         # remove tokens with numbers in them
         line = [word for word in line if word.isalpha()]
         # store as string
         clean_pair.append(' '.join(line))
      cleaned.append(clean_pair)
   return array(cleaned)

代码清单27.5:清理文本的函数

最后,既然已经清理了数据,我们可以将短语对列表保存到准备使用的文件中。函数save_clean_data()使用pickle API将干净文本列表保存到文件中。将所有这些结合在一起,下面列出了完整的示例。

import string
import re
from pickle import dump
from unicodedata import normalize
from numpy import array

# load doc into memory
def load_doc(filename):
   # open the file as read only
   file = open(filename, mode='rt', encoding='utf-8')
   # read all text
   text = file.read()
   # close the file
   file.close()
   return text

# split a loaded document into sentences
def to_pairs(doc):
   lines = doc.strip().split('\n')
   pairs = [line.split('\t') for line in  lines]
   return pairs

# clean a list of lines
def clean_pairs(lines):
   cleaned = list()
   # prepare regex for char filtering
   re_print = re.compile('[^%s]' % re.escape(string.printable))
   # prepare translation table for removing punctuation
   table = str.maketrans('', '', string.punctuation)
   for pair in lines:
      clean_pair = list()
      for line in pair:
         # normalize unicode characters
         line = normalize('NFD', line).encode('ascii', 'ignore')
         line = line.decode('UTF-8')
         # tokenize on white space
         line = line.split()
         # convert to lowercase
         line = [word.lower() for word in line]
         # remove punctuation from each token
         line = [word.translate(table) for word in line]
         # remove non-printable chars form each token
         line = [re_print.sub('', w) for w in line]
         # remove tokens with numbers in them
         line = [word for word in line if word.isalpha()]
         # store as string
         clean_pair.append(' '.join(line))
      cleaned.append(clean_pair)
   return array(cleaned)

# save a list of clean sentences to file
def save_clean_data(sentences, filename):
   dump(sentences, open(filename, 'wb'))
   print('Saved: %s' % filename)

# load dataset
filename = 'deu.txt'
doc = load_doc(filename)
# split into english-german pairs
pairs = to_pairs(doc)
# clean sentences
clean_pairs = clean_pairs(pairs)
# save clean pairs to file
save_clean_data(clean_pairs, 'english-german.pkl')
# spot check
for i in range(100):
   print('[%s] => [%s]' % (clean_pairs[i,0], clean_pairs[i,1]))

代码清单27.6:完整的文本数据准备示例

运行该示例将在当前工作目录中创建一个新文件,其中包含名为 english-german.pkl的已清理文本。打印清洁文本的一些示例供我们在运行结束时进行评估,以确认清洁操作是按预期执行的。

[hi] => [hallo]

[hi] => [gru gott]

[run] => [lauf]

[wow] => [potzdonner]

[wow] => [donnerwetter]

[fire] => [feuer]

[help] => [hilfe]

[help] => [zu hulf]

[stop] => [stopp]

[wait] => [warte]

27.3.2 拆分文字

干净的数据包含超过170,000个短语对,并且文件末尾的一些对非常长,这是开发小型翻译模型的大量示例。模型的复杂性随着示例的数量、短语的长度和词汇的大小而增加,虽然我们有一个很好的数据集用于建模翻译,但我们会略微简化问题,以大幅减少所需模型的大小,进而缩短拟合模型所需的训练时间。

您可以尝试在更全面的数据集上开发模型作为扩展。我们将通过将数据集减少到文件中的前10,000个样本来简化问题;这些将是数据集中最短的短语。此外,我们将把前9,000个作为训练样本,其余1,000个例子用于测试拟合模型。下面是加载干净数据,拆分数据并将数据拆分部分保存到新文件的完整示例。

from pickle import load
from pickle import dump
from numpy.random import rand
from numpy.random import shuffle

# load a clean dataset
def load_clean_sentences(filename):
   return load(open(filename, 'rb'))

# save a list of clean sentences to file
def save_clean_data(sentences, filename):
   dump(sentences, open(filename, 'wb'))
   print('Saved: %s' % filename)

# load dataset
raw_dataset = load_clean_sentences('english-german.pkl')

# reduce dataset size
n_sentences = 10000
dataset = raw_dataset[:n_sentences, :]
# random shuffle
shuffle(dataset)
# split into train/test
train, test = dataset[:9000], dataset[9000:]
# save
save_clean_data(dataset, 'english-german-both.pkl')
save_clean_data(train, 'english-german-train.pkl')
save_clean_data(test, 'english-german-test.pkl')

代码清单27.7:拆分文本数据的完整示例

运行该示例将创建三个新文件:english-german-both.pkl,其中包含我们可用于定义问题参数的所有训练和测试示例,例如最大短语长度和词汇,以及用于训练和测试数据集的german-train.pklenglish-german-test.pkl文件。我们现在准备开始开发我们的翻译模型。

27.4 训练神经翻译模型

在本节中,我们将开发翻译模型。这包括加载和准备准备好进行建模的清洁文本数据,以及在准备好的数据上定义和训练模型。让我们从加载数据集开始,以便我们可以准备数据。下面命名为load_clean_sentences()的函数可用于依次加载traintest和全部数据集。

# load a clean dataset
def load_clean_sentences(filename):
   return load(open(filename, 'rb'))

# load datasets
dataset = load_clean_sentences('english-german-both.pkl')
train = load_clean_sentences('english-german-train.pkl')
test = load_clean_sentences('english-german-test.pkl')

代码清单27.8:从文件加载清理的数据

为了简单起见,我们可以使用训练和测试数据集的两者或组合来定义问题的最大长度和词汇。或者,我们可以单独从训练数据集定义这些属性,并使用这些属性截断测试集中的样本,以剔除太长样本或者不在词汇表中的词汇。根据建模需要,我们使用Keras Tokenize类将单词映射到整数,我们将为英语序列和德语序列使用单独的标记化器。下面名为create_tokenizer()的函数将在短语列表上训练一个标记化器。

# fit a tokenizer
def create_tokenizer(lines):
   tokenizer = Tokenizer()
   tokenizer.fit_on_texts(lines)
   return tokenizer

代码清单27.9:在干净的文本数据上创建一个tokenizer

类似地,下面名为max_length()的函数将计算短语列表中最长序列的长度。

# max sentence length
def max_length(lines):
   return max(len(line.split()) for line in lines)

代码清单27.10:计算最大序列长度

我们调用这些函数处理组合数据集,为英语和德语短语准备标记器、词汇表大小和最大长度。

# prepare english tokenizer
eng_tokenizer = create_tokenizer(dataset[:, 0])
eng_vocab_size = len(eng_tokenizer.word_index) + 1
eng_length = max_length(dataset[:, 0])
print('English Vocabulary Size: %d' % eng_vocab_size)
print('English Max Length: %d' % (eng_length))
# prepare german tokenizer
ger_tokenizer = create_tokenizer(dataset[:, 1])
ger_vocab_size = len(ger_tokenizer.word_index) + 1
ger_length = max_length(dataset[:, 1])
print('German Vocabulary Size: %d' % ger_vocab_size)
print('German Max Length: %d' % (ger_length))

代码清单27.11:为源序列和目标序列准备Tokenizer

我们现在准备准备训练数据集。每个输入和输出序列必须编码为整数并填充到最大短语长度。这是因为我们将对输入序列使用字嵌入,并对输出序列进行one-hot编码。下面命名为encode_sequences()的函数将执行这些操作并返回结果。

# encode and pad sequences
def encode_sequences(tokenizer, length, lines):
   # integer encode sequences
   X = tokenizer.texts_to_sequences(lines)
   # pad sequences with 0 values
   X = pad_sequences(X, maxlen=length, padding='post')
   return X

代码清单27.12:编码和填充序列的函数

输出序列需要是一个热编码。这是因为模型将预测词汇表中每个单词的概率作为输出。下面的函数encode_output()将是一个热编码英文输出序列。

# one hot encode target sequence
def encode_output(sequences, vocab_size):
   ylist = list()
   for sequence in sequences:
      encoded = to_categorical(sequence, num_classes=vocab_size)
      ylist.append(encoded)
   y = array(ylist)
   y = y.reshape(sequences.shape[0], sequences.shape[1], vocab_size)
   return y

代码清单27.13:一个热编码输出序列

我们可以利用这两个函数,准备好训练模型的训练和测试数据集。

# prepare training data
trainX = encode_sequences(ger_tokenizer, ger_length, train[:, 1])
trainY = encode_sequences(eng_tokenizer, eng_length, train[:, 0])
trainY = encode_output(trainY, eng_vocab_size)
# prepare validation data
testX = encode_sequences(ger_tokenizer, ger_length, test[:, 1])
testY = encode_sequences(eng_tokenizer, eng_length, test[:, 0])
testY = encode_output(testY, eng_vocab_size)

代码清单27.14:为建模准备训练和测试数据

我们现在准备定义模型,我们将在此问题上使用编码器-解码器架构。在这种架构中,输入序列由称为编码器的前端模型编码,然后由称为解码器的后端模型逐字解码,下面的函数define_model()定义这个具体的模型,并采用了许多用于配置模型的参数,例如输入和输出词汇表的大小、输入和输出短语的最大长度以及模型的记忆单元数。

该模型训练使用Adam优化算法,因为我们将预测问题框定为多类分类,使用categorical_crossentropy作为损失函数。声明一下,本模型配置未针对此问题进行优化,这意味着您有足够的机会对其进行调整并提升翻译性能。

# define NMT model
def define_model(src_vocab, tar_vocab, src_timesteps, tar_timesteps, n_units):
   model = Sequential()
   model.add(Embedding(src_vocab, n_units, input_length=src_timesteps, mask_zero=True))
   model.add(LSTM(n_units))
   model.add(RepeatVector(tar_timesteps))
   model.add(LSTM(n_units, return_sequences=True))
   model.add(TimeDistributed(Dense(tar_vocab, activation='softmax')))
   return model

# define model
model = define_model(ger_vocab_size, eng_vocab_size, ger_length, eng_length, 256)
model.compile(optimizer='adam', loss='categorical_crossentropy')
# summarize defined model
print(model.summary())
plot_model(model, to_file='model.png', show_shapes=True)

代码清单27.15:定义和显示模型结构

最后,我们可以训练模型,训练30个迭代的模型和的批量大小为64个样本。

我们使用检查点来确保每次测试集上的模型性能得到改进时,模型都会保存到文件中。

# fit model
filename = 'model.h5'
checkpoint = ModelCheckpoint(filename, monitor='val_loss', verbose=1, save_best_only=True, mode='min')
model.fit(trainX, trainY, epochs=30, batch_size=64, validation_data=(testX, testY), callbacks=[checkpoint], verbose=2)

代码清单27.16:使用检查点拟合定义的模型并保存模型。

我们可以将所有这些结合在一起并适合神经翻译模型。完整的工作示例如下所示。

from pickle import load
from numpy import array
from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
from keras.utils import to_categorical
from keras.utils.vis_utils import plot_model
from keras.models import Sequential
from keras.layers import LSTM
from keras.layers import Dense
from keras.layers import Embedding
from keras.layers import RepeatVector
from keras.layers import TimeDistributed
from keras.callbacks import ModelCheckpoint

# load a clean dataset
def load_clean_sentences(filename):
   return load(open(filename, 'rb'))

# fit a tokenizer
def create_tokenizer(lines):
   tokenizer = Tokenizer()
   tokenizer.fit_on_texts(lines)
   return tokenizer

# max sentence length
def max_length(lines):
   return max(len(line.split()) for line in lines)

# encode and pad sequences
def encode_sequences(tokenizer, length, lines):
   # integer encode sequences
   X = tokenizer.texts_to_sequences(lines)
   # pad sequences with 0 values
   X = pad_sequences(X, maxlen=length, padding='post')
   return X

# one hot encode target sequence
def encode_output(sequences, vocab_size):
   ylist = list()
   for sequence in sequences:
      encoded = to_categorical(sequence, num_classes=vocab_size)
      ylist.append(encoded)
   y = array(ylist)
   y = y.reshape(sequences.shape[0], sequences.shape[1], vocab_size)
   return y

# define NMT model
def define_model(src_vocab, tar_vocab, src_timesteps, tar_timesteps, n_units):
   model = Sequential()
   model.add(Embedding(src_vocab, n_units, input_length=src_timesteps, mask_zero=True))
   model.add(LSTM(n_units))
   model.add(RepeatVector(tar_timesteps))
   model.add(LSTM(n_units, return_sequences=True))
   model.add(TimeDistributed(Dense(tar_vocab, activation='softmax')))
   return model

# load datasets
dataset = load_clean_sentences('english-german-both.pkl')
train = load_clean_sentences('english-german-train.pkl')
test = load_clean_sentences('english-german-test.pkl')

# prepare english tokenizer
eng_tokenizer = create_tokenizer(dataset[:, 0])
eng_vocab_size = len(eng_tokenizer.word_index) + 1
eng_length = max_length(dataset[:, 0])
print('English Vocabulary Size: %d' % eng_vocab_size)
print('English Max Length: %d' % (eng_length))
# prepare german tokenizer
ger_tokenizer = create_tokenizer(dataset[:, 1])
ger_vocab_size = len(ger_tokenizer.word_index) + 1
ger_length = max_length(dataset[:, 1])
print('German Vocabulary Size: %d' % ger_vocab_size)
print('German Max Length: %d' % (ger_length))

# prepare training data
trainX = encode_sequences(ger_tokenizer, ger_length, train[:, 1])
trainY = encode_sequences(eng_tokenizer, eng_length, train[:, 0])
trainY = encode_output(trainY, eng_vocab_size)
# prepare validation data
testX = encode_sequences(ger_tokenizer, ger_length, test[:, 1])
testY = encode_sequences(eng_tokenizer, eng_length, test[:, 0])
testY = encode_output(testY, eng_vocab_size)

# define model
model = define_model(ger_vocab_size, eng_vocab_size, ger_length, eng_length, 256)
model.compile(optimizer='adam', loss='categorical_crossentropy')
# summarize defined model
print(model.summary())
plot_model(model, to_file='model.png', show_shapes=True)
# fit model
filename = 'model.h5'
checkpoint = ModelCheckpoint(filename, monitor='val_loss', verbose=1, save_best_only=True, mode='min')
model.fit(trainX, trainY, epochs=30, batch_size=64, validation_data=(testX, testY), callbacks=[checkpoint], verbose=2)

代码清单27.17:训练神经机器翻译模型的完整示例

首先运行该示例将打印数据集参数的摘要,例如词汇表大小和最大短语长度。

English Vocabulary Size: 2309

English Max Length: 5

German Vocabulary Size: 3657

German Max Length: 10

代码清单27.18:加载数据的摘要

接下来,打印已定义模型的摘要,允许我们确认模型配置。

_________________________________________________________________

Layer (type)                 Output Shape              Param #   

=================================================================

embedding_1 (Embedding)      (None, 10, 256)           936192    

_________________________________________________________________

lstm_1 (LSTM)                (None, 256)               525312    

_________________________________________________________________

repeat_vector_1 (RepeatVecto (None, 5, 256)            0         

_________________________________________________________________

lstm_2 (LSTM)                (None, 5, 256)            525312    

_________________________________________________________________

time_distributed_1 (TimeDist (None, 5, 2309)           593413    

=================================================================

Total params: 2,580,229

Trainable params: 2,580,229

Non-trainable params: 0

_________________________________________________________________

代码清单27.19:已定义模型的摘要

27.1:定义的神经机器翻译模型的图

 

接下来,训练模型。在现代CPU硬件上,每个时期大约需要30;不需要GPU。在运行期间,模型将保存到文件model.h5中,准备在下一步中进行推理。

......

Epoch 00024: val_loss improved from 2.01333 to 1.99557, saving model to model.h5

Epoch 25/30

 - 2s - loss: 0.7012 - val_loss: 1.9764

 

Epoch 00025: val_loss improved from 1.99557 to 1.97643, saving model to model.h5

Epoch 26/30

 - 2s - loss: 0.6409 - val_loss: 1.9747

 

Epoch 00026: val_loss improved from 1.97643 to 1.97466, saving model to model.h5

Epoch 27/30

 - 2s - loss: 0.5865 - val_loss: 1.9716

 

Epoch 00027: val_loss improved from 1.97466 to 1.97158, saving model to model.h5

Epoch 28/30

 - 2s - loss: 0.5387 - val_loss: 1.9557

 

Epoch 00028: val_loss improved from 1.97158 to 1.95568, saving model to model.h5

Epoch 29/30

 - 2s - loss: 0.4908 - val_loss: 1.9453

 

Epoch 00029: val_loss improved from 1.95568 to 1.94527, saving model to model.h5

Epoch 30/30

 - 2s - loss: 0.4528 - val_loss: 1.9555

 

Epoch 00030: val_loss did not improve from 1.94527

 

代码清单27.20:训练神经机器翻译模型的摘要输出

27.5 评估神经翻译模型

我们将在训练和测试数据集上评估模型,该模型应该在训练数据集上表现很好,理想情况下在测试数据集上表现也应该良好。理想情况下,我们应该使用一个单独的验证数据集来帮助在训练期间选择最优模型,而不是使用测试集。你可以试试这个扩展练习必须像以前一样加载和准备干净的数据集。

...
# load datasets
dataset = load_clean_sentences('english-german-both.pkl')
train = load_clean_sentences('english-german-train.pkl')
test = load_clean_sentences('english-german-test.pkl')
# prepare english tokenizer
eng_tokenizer = create_tokenizer(dataset[:, 0])
eng_vocab_size = len(eng_tokenizer.word_index) + 1
eng_length = max_length(dataset[:, 0])
# prepare german tokenizer
ger_tokenizer = create_tokenizer(dataset[:, 1])
ger_vocab_size = len(ger_tokenizer.word_index) + 1
ger_length = max_length(dataset[:, 1])
# prepare data
trainX = encode_sequences(ger_tokenizer, ger_length, train[:, 1])
testX = encode_sequences(ger_tokenizer, ger_length, test[:, 1])

代码清单27.21:加载和准备数据

接下来,必须加载训练期间保存的最佳模型。

# load model
model = load_model('model.h5')

代码清单27.22:加载和保存的模型

评估涉及两个步骤:首先生成翻译的输出序列,然后针对许多输入示例重复此过程,并在多个案例中总结模型的性能。从推理开始,模型可以以一次性方式预测整个输出序列。

translation = model.predict(source, verbose=0)

代码清单27.23:给定源序列预测目标序列。

这将是一个整数序列,我们可以在tokenizer中枚举和查找以映射回单词。下面的函数word_for_id()将执行此反向映射。

# map an integer to a word
def word_for_id(integer, tokenizer):
   for word, index in tokenizer.word_index.items():
      if index == integer:
         return word
   return None

代码清单27.24:将预测的单词索引映射到词汇表中的单词。

我们可以为翻译中的每个整数执行此映射,并将结果作为一个单词串返回。函数predict_sequence()对单个编码的源短语执行此操作。

# generate target given source sequence
def predict_sequence(model, tokenizer, source):
   prediction = model.predict(source, verbose=0)[0]
   integers = [argmax(vector) for vector in prediction]
   target = list()
   for i in integers:
      word = word_for_id(i, tokenizer)
      if word is None:
         break
      target.append(word)
   return ' '.join(target)

代码清单27.25:预测并解释目标序列

接下来,我们可以对数据集中的每个源短语重复此操作,并将预测结果与英语中的预期目标短语进行比较。我们可以将这些比较中的一些信息打印到屏幕上,以了解模型在实践中的表现。我们还将计算BLEU分数,以获得模型表现良好的定量概念。下面的evaluate_model()函数实现了这一点,为提供的数据集中的每个短语调用上面的predict_sequence()函数。

# evaluate the skill of the model
def evaluate_model(model, tokenizer, sources, raw_dataset):
   actual, predicted = list(), list()
   for i, source in enumerate(sources):
      # translate encoded source text
      source = source.reshape((1, source.shape[0]))
      translation = predict_sequence(model, eng_tokenizer, source)
      raw_target, raw_src = raw_dataset[i]
      if i < 10:
         print('src=[%s], target=[%s], predicted=[%s]' % (raw_src, raw_target, translation))
      actual.append(raw_target.split())
      predicted.append(translation.split())
   # calculate BLEU score
   print('BLEU-1: %f' % corpus_bleu(actual, predicted, weights=(1.0, 0, 0, 0)))
   print('BLEU-2: %f' % corpus_bleu(actual, predicted, weights=(0.5, 0.5, 0, 0)))
   print('BLEU-3: %f' % corpus_bleu(actual, predicted, weights=(0.3, 0.3, 0.3, 0)))
   print('BLEU-4: %f' % corpus_bleu(actual, predicted, weights=(0.25, 0.25, 0.25, 0.25)))

代码清单27.26:评估拟合模型的函数

我们可以将所有这些结合在一起,并在训练和测试数据集上评估加载的模型。完整代码如下:

from pickle import load
from numpy import array
from numpy import argmax
from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
from keras.models import load_model
from nltk.translate.bleu_score import corpus_bleu

# load a clean dataset
def load_clean_sentences(filename):
   return load(open(filename, 'rb'))

# fit a tokenizer
def create_tokenizer(lines):
   tokenizer = Tokenizer()
   tokenizer.fit_on_texts(lines)
   return tokenizer

# max sentence length
def max_length(lines):
   return max(len(line.split()) for line in lines)

# encode and pad sequences
def encode_sequences(tokenizer, length, lines):
   # integer encode sequences
   X = tokenizer.texts_to_sequences(lines)
   # pad sequences with 0 values
   X = pad_sequences(X, maxlen=length, padding='post')
   return X

# map an integer to a word
def word_for_id(integer, tokenizer):
   for word, index in tokenizer.word_index.items():
      if index == integer:
         return word
   return None

# generate target given source sequence
def predict_sequence(model, tokenizer, source):
   prediction = model.predict(source, verbose=0)[0]
   integers = [argmax(vector) for vector in prediction]
   target = list()
   for i in integers:
      word = word_for_id(i, tokenizer)
      if word is None:
         break
      target.append(word)
   return ' '.join(target)

# evaluate the skill of the model
def evaluate_model(model, tokenizer, sources, raw_dataset):
   actual, predicted = list(), list()
   for i, source in enumerate(sources):
      # translate encoded source text
      source = source.reshape((1, source.shape[0]))
      translation = predict_sequence(model, eng_tokenizer, source)
      raw_target, raw_src = raw_dataset[i]
      if i < 10:
         print('src=[%s], target=[%s], predicted=[%s]' % (raw_src, raw_target, translation))
      actual.append(raw_target.split())
      predicted.append(translation.split())
   # calculate BLEU score
   print('BLEU-1: %f' % corpus_bleu(actual, predicted, weights=(1.0, 0, 0, 0)))
   print('BLEU-2: %f' % corpus_bleu(actual, predicted, weights=(0.5, 0.5, 0, 0)))
   print('BLEU-3: %f' % corpus_bleu(actual, predicted, weights=(0.3, 0.3, 0.3, 0)))
   print('BLEU-4: %f' % corpus_bleu(actual, predicted, weights=(0.25, 0.25, 0.25, 0.25)))

# load datasets
dataset = load_clean_sentences('english-german-both.pkl')
train = load_clean_sentences('english-german-train.pkl')
test = load_clean_sentences('english-german-test.pkl')
# prepare english tokenizer
eng_tokenizer = create_tokenizer(dataset[:, 0])
eng_vocab_size = len(eng_tokenizer.word_index) + 1
eng_length = max_length(dataset[:, 0])
# prepare german tokenizer
ger_tokenizer = create_tokenizer(dataset[:, 1])
ger_vocab_size = len(ger_tokenizer.word_index) + 1
ger_length = max_length(dataset[:, 1])
# prepare data
trainX = encode_sequences(ger_tokenizer, ger_length, train[:, 1])
testX = encode_sequences(ger_tokenizer, ger_length, test[:, 1])

# load model
model = load_model('model.h5')
# test on some training sequences
print('train')
evaluate_model(model, eng_tokenizer, trainX, train)
# test on some test sequences
print('test')
evaluate_model(model, eng_tokenizer, testX, test)

代码清单27.27:使用拟合神经机器翻译模型翻译文本的完整示例

首先运行该示例打印源文本,预期和预测翻译的示例,以及训练数据集的分数,然后是测试数据集。鉴于数据集的随机改组和神经网络的随机性,您的具体结果会有所不同。首先查看测试数据集的结果,我们可以看到翻译是可读的并且大部分都是正确的。例如:'ich liebe dich'被正确翻译为'i love you'

意:鉴于神经网络的随机性,您的具体结果可能会有所不同。考虑运行几次示例。

train

src=[tu ihm nicht weh], target=[dont hurt him], predicted=[dont hurt him]

src=[tom wird nicht antworten], target=[tom wont answer], predicted=[tom wont respond]

src=[ich gab tom ein buch], target=[i gave tom a book], predicted=[i gave tom a book]

src=[ich muss rennen], target=[i have to run], predicted=[i have to run]

src=[wie heit du], target=[what is your name], predicted=[what is your name]

src=[sie ist meine kommilitonin], target=[shes my classmate], predicted=[shes my classmate]

src=[sei still und hor zu], target=[be quiet and listen], predicted=[shut up and listen]

src=[tom ist im zweiten studienjahr], target=[tom is a sophomore], predicted=[tom is a sophomore]

src=[toms gesichtsfarbe anderte sich], target=[tom changed color], predicted=[tom changed that]

src=[du kannst dich auf mich verlassen], target=[you can lean on me], predicted=[you can lean on me]

/home/oliverbak/anaconda3/lib/python3.6/site-packages/nltk/translate/bleu_score.py:503: UserWarning:

The hypothesis contains 0 counts of 3-gram overlaps.

Therefore the BLEU score evaluates to 0, independently of

how many N-gram overlaps of lower order it contains.

Consider using lower n-gram order or use SmoothingFunction()

  warnings.warn(_msg)

/home/oliverbak/anaconda3/lib/python3.6/site-packages/nltk/translate/bleu_score.py:503: UserWarning:

The hypothesis contains 0 counts of 4-gram overlaps.

Therefore the BLEU score evaluates to 0, independently of

how many N-gram overlaps of lower order it contains.

Consider using lower n-gram order or use SmoothingFunction()

  warnings.warn(_msg)

BLEU-1: 0.069963

BLEU-2: 0.001763

BLEU-3: 0.000000

BLEU-4: 0.000000

代码清单27.28:训练数据集上的示例输出转换

查看测试集上的结果,确实看到可读的翻译,这不是一件容易的事。例如,我们看到 'ich habe kurze haare'正确翻译为'i have short hair'。我们还看到一些糟糕的翻译和一个很好的案例,该模型可以支持进一步调整,例如'tom schreit viel'翻译为'tom travels a lot' 而不是预期的’tom shouts a lot’BLEU-4得分为0.0000,而且2元以上函数有错误信息,显然是越界了,我们还应该进一步改进模型。

test

src=[nur wasser bitte], target=[just water please], predicted=[just wait please please]

src=[steh nicht auf], target=[dont stand up], predicted=[dont get up]

src=[tom war es egal], target=[tom didnt care], predicted=[tom didnt it]

src=[das ist nicht fur mich], target=[this isnt for me], predicted=[this is for for me]

src=[tom schreit viel], target=[tom shouts a lot], predicted=[tom travels a lot]

src=[du siehst gut aus], target=[youre looking good], predicted=[you look good]

src=[tom rannte nach drauen], target=[tom ran outside], predicted=[tom ran out]

src=[wir werden angreifen], target=[well attack], predicted=[well cooperate]

src=[lassen sie mich mal sehen], target=[let me see], predicted=[let me see that]

src=[ich habe kurze haare], target=[i have short hair], predicted=[i have short hair]

/home/oliverbak/anaconda3/lib/python3.6/site-packages/nltk/translate/bleu_score.py:503: UserWarning:

The hypothesis contains 0 counts of 2-gram overlaps.

Therefore the BLEU score evaluates to 0, independently of

how many N-gram overlaps of lower order it contains.

Consider using lower n-gram order or use SmoothingFunction()

  warnings.warn(_msg)

BLEU-1: 0.067721

BLEU-2: 0.000000

BLEU-3: 0.000000

BLEU-4: 0.000000

代码清单27.29:测试数据集上的示例输出转换

27.6 扩展

本节列出了一些扩展您可以做如下方面的尝试。

据清理。可以对数据执行不同的数据清理操作,例如不删除标点符号或规范化大小写,或者可能删除重复的英语短语。

汇。可以改进词汇表,也许删除在数据集中使用少于510次的单词并用unk替换。

多数据。用于拟合模型的数据集可以扩展到50,000,1000个短语或更多。

入顺序。输入短语的顺序可以颠倒,据说可以提升模型性能,或者可以使用双向输入层。

。编码器和/或解码器模型可以通过附加层进行扩展,并训练更多的迭代,从而为模型提供更多的代表性能力。

记忆单元。可以增加编码器和解码器中的记忆单元的数量,从而为模型提供更多的代表性容量。

正则。 该模型可以使用正则化,例如权重或激活正则化,或在LSTM层上使用dropout

训练的单词向量。可以在模型中使用预训练的单词向量。

代性能评估指标。探索BLEU旁边的其他性能指标,例如ROGUE。比较相同翻译的分数,以便对性能指标在实践中的差异有直觉。

归模型。可以递归使用模型,其中输出序列中的下一个字,可以以输入序列和到目前为止生成的输出序列为条件。


0 条 查看最新 评论

没有评论
暂时无法发表评论