Keras深度学习实战(35)——构建机器翻译模型

时间:2022-11-09 11:36:44

0. 前言

我们已经学习了多种将输入和输出进行一对一映射的模型架构,在本节中,我们将研究构建多对多模型体系结构,这种模型架构可以将所有输入数据映射到编码向量中,然后将其解码为输出向量。本节中,我们将构建神经网络模型用于将英语输入文本翻译成法语文本输出。

1. 模型与数据集分析

1.1 模型分析

在实现机器翻译模型前,我们首先定义用于执行机器翻译的体系结构:

  • 获取数据集,其中包括输入的英语句子和相应的法语翻译
  • 标记并提取英语和法语文本中常见的单词:
    • 为了识别频繁出现的单词,计算每个单词的出现频率
    • 占所有词总累计频率前80%的词被认为是常用词
    • 对于不属于常用词的单词,使用符号 (unk) 替换它们
  • 为每个单词分配一个 ID
  • 构建一个基于长短时记忆网络 (Long Short Term Memory, LSTM) 的编码器 LSTM 获取输入文本的向量
  • 编码向量通过全连接层,以便在每个时间戳提取解码文本的概率
  • 拟合模型以最小化输出损失

1.2 数据集分析

为了构建机器翻译模型,我们所使用的数据集中包含英语及其对应的法语数据,相关数据集可以在 gitcode 链接中下载,其中包含了大约 140000 个英语与法语互译的句子。

2. 实现机器翻译模型

接下来,我们将实现上一小节中所定义的策略以翻译输入文本。

2.1 预处理数据

要将输入和输出数据传递到所构建模型,我们首先对数据集进行预处理。

(1) 导入相关的库,并加载数据集:

import pandas as pd
import numpy as np
import string
import matplotlib.pyplot as plt

lines= pd.read_table('english to french.txt', names=['eng', 'fr'])
# 数据集中约有 140000 个句子,为了简单起见,在本模型中仅考虑前 100000 个句子:
lines = lines[0:100000]

(2) 将输入和输出文本转换为小写并删除标点符号:

lines.eng=lines.eng.apply(lambda x: x.lower())
lines.fr=lines.fr.apply(lambda x: x.lower())

exclude = set(string.punctuation)
lines.eng=lines.eng.apply(lambda x: ''.join(ch for ch in x if ch not in exclude))
lines.fr=lines.fr.apply(lambda x: ''.join(ch for ch in x if ch not in exclude))
# 查看数据样本示例
print(lines.head())

打印出的数据样本如下所示:

    eng         fr
0    go        va 
1   run     cours 
2   run    courez 
3   wow  ça alors 
4  fire    au feu 

(3) 将开始和结束标记添加到输出语句,即法语语句。开始标记和结束标记在编码器-解码器体系结构中非常有用,具体原因将在后续模型构建时详细讲解:

lines.fr = lines.fr.apply(lambda x : 'start '+ x + ' end')
print(lines.head())

数据样本如下所示:

    eng                   fr
0    go        start va  end
1   run     start cours  end
2   run    start courez  end
3   wow  start ça alors  end
4  fire    start au feu  end

(4) 接下来,确定常用词。如果单词属于累计构成英语单词总数的 80%,则我们将其定义为常用词:

from keras.preprocessing.text import Tokenizer
import json

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

eng_tokenizer = create_tokenizer(lines.eng)
output_dict = json.loads(json.dumps(eng_tokenizer.word_counts))
df =pd.DataFrame([output_dict.keys(), output_dict.values()]).T
df.columns = ['word','count']
df = df.sort_values(by='count',ascending = False)
df['cum_count']=df['count'].cumsum()
df['cum_perc'] = df['cum_count']/df['cum_count'].max()
final_eng_words = df[df['cum_perc']<0.8]['word'].values

在以上代码中,我们提取输入中累计构成英语单词总数的 80% 的英语单词。接下来,我们使用同样的方法提取法语单词,这些单词总数累计占输出法语单词总数的 80%

fr_tokenizer = create_tokenizer(lines.fr)
output_dict = json.loads(json.dumps(fr_tokenizer.word_counts))
df =pd.DataFrame([output_dict.keys(), output_dict.values()]).T
df.columns = ['word','count']
df = df.sort_values(by='count',ascending = False)
df['cum_count']=df['count'].cumsum()
df['cum_perc'] = df['cum_count']/df['cum_count'].max()
final_fr_words = df[df['cum_perc']<0.8]['word'].values
# 打印出不同常用词的数量
print(len(final_eng_words),len(final_fr_words))
# 398 415

(5) 过滤掉不常用的单词。如果单词不在常用词中,我们使用标记 unk 代替:

def filter_eng_words(x):
    t = []
    x = x.split()
    for i in range(len(x)):
        if x[i] in final_eng_words:
            t.append(x[i])
        else:
            t.append('unk')
    x3 = ''
    for i in range(len(t)):
        x3 = x3+t[i]+' '
    return x3

filter_eng_words 函数以一个句子为输入,提取其中不同的单词,如果其不属于常用英语单词 final_eng_words,则将其替换为 unk。接下来,我们以类似的方式定义函数 filter_fr_words,其将法语句子作为输入,提取其中不同的单词,如果其不属于常用法语单词 final_fr_words,则将其替换为 unk

def filter_fr_words(x):
    t = []
    x = x.split()
    for i in range(len(x)):
        if x[i] in final_fr_words:
            t.append(x[i])
        else:
            t.append('unk')
    x3 = ''
    for i in range(len(t)):
        x3 = x3+t[i]+' '
    return x3

使用以上定义的函数,在一个包含常用单词和不常用单词的句子中,替换后的输出如下:

print(filter_eng_words('he is extremely good'))
# he is unk good

(6) 在了解了 filter_en_wordsfilter_fr_words 函数的用法后,我们将根据定义的函数处理所有英语和法语句子:

lines['fr']=lines['fr'].apply(filter_fr_words)
lines['eng']=lines['eng'].apply(filter_eng_words)

(7) 为英语(输入)和法语(输出)句子中的每个单词分配不同 ID。分别存储英语和法语句子数据中所有不重复单词的集合,并将它们转换为列表:

all_eng_words=set()
for eng in lines.eng:
    for word in eng.split():
        if word not in all_eng_words:
            all_eng_words.add(word)
    
all_french_words=set()
for fr in lines.fr:
    for word in fr.split():
        if word not in all_french_words:
            all_french_words.add(word)

input_words = sorted(list(all_eng_words))
target_words = sorted(list(all_french_words))
num_encoder_tokens = len(all_eng_words)
num_decoder_tokens = len(all_french_words)

创建输入词及其对应索引的字典:

input_token_index = dict([(word, i+1) for i, word in enumerate(input_words)])
target_token_index = dict([(word, i+1) for i, word in enumerate(target_words)])

计算输入和目标句子的最大长度,以便所有句子都可以填充为相同长度:

lenght_list=[]
for l in lines.fr:
    lenght_list.append(len(l.split(' ')))
fr_max_length = np.max(lenght_list)

lenght_list=[]
for l in lines.eng:
    lenght_list.append(len(l.split(' ')))
eng_max_length = np.max(lenght_list)

至此,我们已经完成了对数据集的预处理,接下来,我们使用多种模型架构处理数据集以比较不同模型性能。

2.2 传统多对多架构

在传统体系结构中,我们将每个输入单词编码为 128 维嵌入向量,从而得到形状为 (batch_size, 128, 18) 的输出向量,最后一个维度 18 是由于输入数据具有 18 个时间戳而输出数据集也具有 18 个时间戳。然后通过 LSTM 将每个输入时间戳连接到输出时间戳,最后使用 softmax 函数得到输出结果。

(1) 根据预处理得到的 decoder_input_datadecoder_target_data 创建输入和输出数据集,创建 decoder_input_data 存储目标句子单词相对应的单词 IDdecoder_target_data 是目标句子中起始标记单词后所有单词的独热编码:

encoder_input_data = np.zeros(
    (len(lines.eng), fr_max_length),
    dtype='float32')
decoder_input_data = np.zeros(
    (len(lines.fr), fr_max_length),
    dtype='float32')
decoder_target_data = np.zeros(
    (len(lines.fr), fr_max_length, num_decoder_tokens+1),
    dtype='float32')

在以上代码中,我们为 num_decodder_tokens1,其中加 1 用于对应索引为 0 的单词,用于填充句子:

for i, (input_text, target_text) in enumerate(zip(lines.eng, lines.fr)):
    for t, word in enumerate(input_text.split()):
        encoder_input_data[i, t] = input_token_index[word]
    for t, word in enumerate(target_text.split()):
        # decoder_target_data 领先于decoder_input_data 一个时间戳
        decoder_input_data[i, t] = target_token_index[word]
        if t>0:          
            # decoder_target_data 将提前一个时间戳,且不包括起始字符
            decoder_target_data[i, t - 1, target_token_index[word]] = 1.
        if t== len(target_text.split())-1:
            decoder_target_data[i, t:, target_token_index['end']] = 1

在以上代码中,我们遍历输入文本和目标文本,以将英语或法语中的句子替换为其对应的英语和法语中的单词 ID
此外,我们对目标数据进行独热编码,以便将其传递给模型。此外,由于我们已经将所有句子填充为相同长度,在 for 循环中超过句子长度之后,我们将目标数据的值替换为结束标注单词 end

for i in range(decoder_input_data.shape[0]):
    for j in range(decoder_input_data.shape[1]):
        if(decoder_input_data[i][j]==0):
            decoder_input_data[i][j] = target_token_index['end']

在以上代码中,我们将解码器输入数据中的 0 值替换为结束标记,0 在我们创建的单词索引中没有任何与之关联的单词,我们创建的三个数据集的形状如下:

print(decoder_input_data.shape,encoder_input_data.shape,decoder_target_data.shape)

数据集形状的输出如下:

(100000, 18) (100000, 18) (100000, 18, 417)

(2) 构建模型,编译并拟合模型:

model = Sequential()
model.add(Embedding(len(input_words)+1, 128, input_length=fr_max_length, mask_zero=True))
model.add((Bidirectional(LSTM(256, return_sequences = True))))
#model.add(RepeatVector(fr_max_length))
model.add((LSTM(256, return_sequences=True)))
model.add((Dense(len(target_token_index)+1, activation='softmax')))
model.summary()
model.compile(optimizer='adam', loss='categorical_crossentropy',metrics=['acc'])

history = model.fit(encoder_input_data, decoder_target_data,
          batch_size=64,
          epochs=10,
          validation_split=0.1,
          verbose=1)

构建的模型简要信息如下所示:

Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
embedding (Embedding)        (None, 18, 128)           51200     
_________________________________________________________________
bidirectional (Bidirectional (None, 18, 512)           788480    
_________________________________________________________________
lstm_1 (LSTM)                (None, 18, 256)           787456    
_________________________________________________________________
dense (Dense)                (None, 18, 417)           107169    
=================================================================
Total params: 1,734,305
Trainable params: 1,734,305
Non-trainable params: 0
_________________________________________________________________

在模型训练过程中,模型的损失和准确率变化如下:

Keras深度学习实战(35)——构建机器翻译模型

(3) 计算正确翻译的单词数,由于我们使用 90% 的数据进行训练,因此接下来,我们使用最后的 10% 的数据作为验证集进行计算:

count = 0
correct_count = 0
pred = model.predict(encoder_input_data[90000:])
for i in range(10000):
    t = np.argmax(pred[i], axis=1)
    act = np.argmax(decoder_target_data[90000],axis=1)
    correct_count += np.sum((act==t) & (act!=target_token_index['end']))
    count += np.sum(act!=target_token_index['end'])
print(correct_count/count)

计算完成后,我们可以看到大约有 14.02% 的单词可以被正确翻译。

2.3 使用具有多个隐藏层的模型架构

传统体系结构的缺点之一是,我们必须人为地将输入中的时间戳增加到指定数量,即使我们知道在某些输入中最多有 10 个时间戳。接下来,我们构建具有多个隐藏层的模型在输入的最后一个时间戳提取网络中间状态值;此外,它会将网络中间状态值复制 18 次,因为输出中有 18 个时间戳。然后,通过 Dense 层传递复制的状态值,并最终提取输出中可能的类别。

(1) 重新创建输入和输出数据集,这样我们在输入中有 8 个时间戳(而在上一小节介绍的传统网络架构中,输入中有 18 个时间戳),在输出中有 18 个时间戳:

encoder_input_data = np.zeros(
    (len(lines.eng), eng_max_length),
    dtype='float32')
decoder_input_data = np.zeros(
    (len(lines.fr), fr_max_length),
    dtype='float32')
decoder_target_data = np.zeros(
    (len(lines.fr), fr_max_length, num_decoder_tokens+1),
    dtype='float32')

for i, (input_text, target_text) in enumerate(zip(lines.eng, lines.fr)):
    for t, word in enumerate(input_text.split()):
        encoder_input_data[i, t] = input_token_index[word]
    for t, word in enumerate(target_text.split()):
        # decoder_target_data is ahead of decoder_input_data by one timestep
        decoder_input_data[i, t] = target_token_index[word]
        if t>0: 
            # decoder_target_data will be ahead by one timestep
            # and will not include the start character.
          decoder_target_data[i, t - 1, target_token_index[word]] = 1.
          if t== len(target_text.split())-1:
            decoder_target_data[i, t:, target_token_index['end']] = 1
      
for i in range(decoder_input_data.shape[0]):
    for j in range(decoder_input_data.shape[1]):
        if(decoder_input_data[i][j]==0):
            decoder_input_data[i][j] = target_token_index['end'] 

(2) 建立模型,使用 RepeatVector 层将 双向 LSTM 层的输出复制 18 次:

from keras.models import Sequential, Model
from keras.layers import Input, Embedding
from keras.layers import LSTM, Dense, Bidirectional, RepeatVector

model2 = Sequential()
model2.add(Embedding(len(input_words)+1, 128, input_length=eng_max_length, mask_zero=True))
model2.add((Bidirectional(LSTM(256))))
model2.add(RepeatVector(fr_max_length))
model2.add((LSTM(256, return_sequences=True)))
model2.add((Dense(len(target_token_index)+1, activation='softmax')))

model2.summary()

模型简要信息输入如下:

Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
embedding (Embedding)        (None, 10, 128)           51200     
_________________________________________________________________
bidirectional (Bidirectional (None, 512)               788480    
_________________________________________________________________
repeat_vector (RepeatVector) (None, 18, 512)           0         
_________________________________________________________________
lstm_1 (LSTM)                (None, 18, 256)           787456    
_________________________________________________________________
dense (Dense)                (None, 18, 417)           107169    
=================================================================
Total params: 1,734,305
Trainable params: 1,734,305
Non-trainable params: 0
_________________________________________________________________

(2) 编译并拟合模型:

model2.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['acc'])
history = model2.fit(encoder_input_data, decoder_target_data,
                batch_size=64,
                epochs=10,
                validation_split=0.1,
                verbose=1)

在模型训练过程中,模型的损失和准确率变化如下:

Keras深度学习实战(35)——构建机器翻译模型

(4) 计算正确翻译的单词占测试单词总数的百分比:

count = 0
correct_count = 0
pred = model2.predict(encoder_input_data[90000:])
for i in range(10000):
    t = np.argmax(pred[i], axis=1)
    act = np.argmax(decoder_target_data[90000],axis=1)
    correct_count += np.sum((act==t) & (act!=target_token_index['end']))
    count += np.sum(act!=target_token_index['end'])
print(correct_count/count)
# 0.14138

计算完成后,我们可以看到大约有 14.14% 的单词可以被正确翻译,与传统模型相比几乎相等。这样的结果是可以预料的,因为当所有输入时间戳的信息仅存储在最后一个网络中间状态值中时,我们往往会丢失大量信息。同样由于没有使用包含大量信息的单元状态,往往这些信息包含了一些有用的内容。我们将在后续的学习中进一步改善该机器翻译模型,以获得更加优秀的性能。

小结

机器翻译,是利用计算机将一种自然语言(源语言)转换为另一种自然语言(目标语言)的过程,是人工智能的终极目标之一,具有重要的科学研究与实用价值,机器翻译技术在促进经济、文化交流等方面起到越来越重要的作用。本文中,首先介绍了机器翻译模型的基本原理,然后构建了两种不同的模型实现了机器翻译模型。

系列链接

Keras深度学习实战(1)——神经网络基础与模型训练过程详解
Keras深度学习实战(2)——使用Keras构建神经网络
Keras深度学习实战(3)——神经网络性能优化技术
Keras深度学习实战(4)——深度学习中常用激活函数和损失函数详解
Keras深度学习实战(5)——批归一化详解
Keras深度学习实战(6)——深度学习过拟合问题及解决方法
Keras深度学习实战(7)——卷积神经网络详解与实现
Keras深度学习实战(8)——使用数据增强提高神经网络性能
Keras深度学习实战(9)——卷积神经网络的局限性
Keras深度学习实战(10)——迁移学习详解
Keras深度学习实战(11)——可视化神经网络中间层输出
Keras深度学习实战(12)——面部特征点检测
Keras深度学习实战(13)——目标检测基础详解
Keras深度学习实战(14)——从零开始实现R-CNN目标检测
Keras深度学习实战(15)——从零开始实现YOLO目标检测
Keras深度学习实战(16)——自编码器详解
Keras深度学习实战(17)——使用U-Net架构进行图像分割
Keras深度学习实战(18)——语义分割详解
Keras深度学习实战(19)——使用对抗攻击生成可欺骗神经网络的图像
Keras深度学习实战(20)——DeepDream模型详解
Keras深度学习实战(21)——神经风格迁移详解
Keras深度学习实战(22)——生成对抗网络详解与实现
Keras深度学习实战(23)——DCGAN详解与实现
Keras深度学习实战(24)——从零开始构建单词向量
Keras深度学习实战(25)——使用skip-gram和CBOW模型构建单词向量
Keras深度学习实战(26)——文档向量详解
Keras深度学习实战(27)——循环神经详解与实现
Keras深度学习实战(28)——利用单词向量构建情感分析模型
Keras深度学习实战(29)——长短时记忆网络详解与实现
Keras深度学习实战(30)——使用文本生成模型进行文学创作
Keras深度学习实战(31)——构建电影推荐系统
Keras深度学习实战(32)——基于LSTM预测股价
Keras深度学习实战(33)——基于LSTM的序列预测模型
Keras深度学习实战(34)——构建聊天机器人