Python 人工智能实战:机器翻译

128 阅读14分钟

1.背景介绍

语言是现代人类交流的工具之一,世界上有上百亿种语言,但仅有极少数能够被人类精确理解。另一方面,由于电子计算机的迅速普及,越来越多的人都选择了使用计算机进行各种工作。然而,计算机只能理解二进制的数字信号,没有能力处理文本形式的自然语言,因此需要人工智能(AI)对语言进行理解、翻译和生成。语言是一种信息载体,其复杂性和多样性带来了人工智能领域巨大的挑战。机器翻译(MT),或称语言识别与合成(LTR),是人工智能的一个重要方向。它可以帮助用户更加高效地沟通、学习、阅读和聆听。本文将介绍机器翻译的基本概念,以及如何通过开源的深度学习库TensorFlow实现一个简单的机器翻译系统。

2.核心概念与联系

机器翻译是指将一种自然语言从一种语言环境转换为另一种语言环境的过程。人工智能领域对于机器翻译的研究从20世纪70年代就开始了,在过去的几十年中,机器翻译已经取得了惊人的进步,特别是在以英语为源语言的机器翻译任务上取得了惊人的成绩。在过去的两三年里,国际认知计算联盟(ICC)、台湾国立大学语言学系、加州大学圣巴巴拉分校等机构相继开设了机器翻译课程,这些课程大都是基于最新的机器翻译技术。本节将简要介绍相关的核心概念,并阐述它们之间的联系。

2.1 MT基础概念

机器翻译(Machine Translation,MT)是由计算机自动将一种语言的语句自动转化为另一种语言的过程。简单来说,就是根据人类的语言语法、词汇习惯以及对话语境,用计算机程序把输入的句子变换为目标语言。机器翻译是人工智能领域的一个热门方向,它的研究主要集中在两方面:一方面,如何利用计算机自动识别输入语句中的内容,并用机器自然语言生成相应的输出;另一方面,如何将源语言的句子正确转换为目标语言的句子。为了实现这一目标,需要解决两个关键问题:词法分析和句法分析。词法分析即将输入的语句切分成词素(wordpiece)或单词,句法分析则确定每个词素的上下文关系,以便生成正确的翻译结果。

词法分析(Lexical Analysis)

词法分析是将输入语句划分成多个词组成的一个个词素,每个词素对应着输入语句的一个特定元素或符号。例如,给定一个英语语句“The quick brown fox jumps over the lazy dog”,词法分析的输出可能是:["the", "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog"]。在中文中,词法分析可以借助于分词工具完成,也可采用基于规则的分割方法,如将连续的中文字符视作一个词素,数字和标点符号视作独立的词素等。

句法分析(Syntactic Analysis)

句法分析是确定每个词素的上下文关系,以便生成正确的翻译结果。一般情况下,句法分析通过考虑语法规则和语义规则实现,语法规则定义了句法结构,语义规则指定了各个词素之间的关联关系。句法分析的输出通常是一个树形结构,表示输入语句的语法分析树。

2.2 模型概览

图1:MT系统的模型概览

本文所述的MT系统由三个主要模块组成:语言模型(Language Model)、翻译模型(Translation Model)和优化器(Optimizer)。其中,语言模型用于预测输入序列的下一个单词,翻译模型用于生成目标语言的序列,优化器负责更新模型参数,使得翻译质量不断提升。整个MT系统由数据集、训练、推断、评估等环节构成,接下来将详细介绍这三个模块的功能和作用。

2.2.1 语言模型(Language Model)

语言模型是机器翻译领域中的一个基础模块,用于表示输入语句的概率分布。语言模型通常由一系列的词嵌入向量和概率分布函数组成。词嵌入向量是一个向量空间,用于编码输入语句中的每个词。概率分布函数是语言模型在给定当前词时,预测下一个词出现的概率。一般情况下,语言模型会考虑到上下文信息,假设词的邻近词对生成当前词的影响较大,通过语言模型可以准确预测出上下文,增强生成的效果。语言模型的作用如下:

  1. 对齐句子:语言模型能够识别出输入语句中的歧义词,消除错误的翻译结果。
  2. 概括生成:语言模型可以用来帮助生成的文章保持一致性,尽量避免生成的句子太长。
  3. 改善生成:语言模型能够提供更多的参考信息,帮助生成更好的翻译结果。

2.2.2 翻译模型(Translation Model)

翻译模型用于实现从源语言到目标语言的句子翻译。最简单的翻译模型直接采用词级的双向LSTM网络,通过编码输入语句的词嵌入,输入LSTM,获取每个词的上下文信息,生成翻译序列。但是,这种模型在翻译质量上存在很大的限制。因此,本文采用基于注意力机制的翻译模型,它可以捕捉不同位置上的依赖关系,改善生成的质量。具体来说,注意力机制包括两种不同的层次:词级别和句子级别。词级别的注意力机制通过注意力矩阵决定词是否被翻译,句子级别的注意力机制则通过考虑整个句子而不是每个词来对整个句子进行翻译。

2.2.3 优化器(Optimizer)

优化器用于更新模型参数,使得翻译质量不断提升。传统的机器翻译方法包括基于最大似然估计的方法、基于梯度上升方法以及基于非监督学习的方法。本文采用基于条件随机场的方法,在MT模型的输入层、输出层和隐藏层之间加入条件随机场层,以对输入输出之间的关系进行建模。同时,还使用反向传播算法进行参数更新。

2.3 数据集简介

机器翻译的数据集是MT领域的一个重要资源,主要用于训练、验证、测试模型。目前,公共数据集大致有以下几个方面:

  1. 平行语料库:平行语料库是MT领域的核心资源。它包含了大量的源语言句子和对应的目标语言句子,可以作为训练数据集、开发数据集或者测试数据集。

  2. 有监督训练数据集:有监督训练数据集是由机器翻译专业人员标注的源语言句子和目标语言句子,经过人工筛选后成为训练数据集。

  3. 自动机翻语料库:自动机翻语料库是由翻译系统产生的机器翻译结果,并且可以作为训练数据集。

  4. 测试数据集:测试数据集包含了不同领域的句子对,可作为模型的泛化性能验证。

2.4 Tensorflow框架

Tensorflow是一个开源的深度学习框架,广泛应用于机器学习和图像识别等领域。本文采用Tensorflow实现MT模型,并使用开源的Tensor2tensor库实现MT模型。Tensor2tensor是Tensorflow的一个扩展项目,用于实现不同类型NLP任务的模型。在这里,我们只需要关注翻译模型即可。

3.核心算法原理和具体操作步骤

3.1 数据准备

首先,收集并整理好机器翻译训练集、开发集和测试集。本文使用了OpenSubtitles数据集作为示例,该数据集由人工注释的儿童英语对照片组成。具体的,我们下载了该数据集的英语、德语、法语、西班牙语版本,共计约41万条对照图片。然后,我们将这些图片分别标记好,并按照开发集/测试集的比例拆分成训练集和验证集。由于OpenSubtitles数据集比较小,因此无需使用外部数据扩充。

3.2 文件格式转换

第二步是将OpenSubtitles数据集的文件格式转换为标准的txt文件。该数据集原本是图片格式,我们需要先将其转换为txt格式。我们可以使用图像处理库PIL对图片进行读取、缩放、裁剪等操作,得到的每张图片的像素矩阵转为字符串保存。这样,我们可以获得一批大小可控的txt文件。

3.3 数据清洗

第三步是对数据进行清洗。由于原始数据中的英语、德语、法语、西班牙语版本图片数量差异很大,为了统一语料库规模,我们取出它们的均值作为所有语言的训练集。然后,删除不需要的符号、标点符号和空格,最后将所有文本转为小写,以便进行统一。

3.4 数据集拆分

第四步是对数据集进行拆分。在机器翻译过程中,我们往往需要将数据集拆分成训练集、验证集和测试集。在本文中,我们使用70%的训练集、10%的开发集、20%的测试集进行训练。

3.5 词表建立

第五步是建立词表。词表是机器翻译中用于编码输入语句、输出序列的稀疏矩阵。我们可以使用python中的collections.Counter()函数统计每句话中单词的频率,选择频率最高的n个单词组成词表。我们可以设置参数n的大小为30K。

3.6 数据加载

第六步是数据加载。我们可以通过python中的tensorflow.data API将数据加载至内存中。Tensorflow的输入API支持大量的数据集,包括tf.data.Dataset对象,可以让我们方便地进行数据加载、分批处理等操作。另外,我们也可以将数据加载进内存中,并进行数据预处理。

3.7 数据处理

第七步是数据处理。在机器翻译任务中,数据预处理主要包括:

  1. 数据增强:通过随机操作,生成新的数据,扩充数据集的规模。常用的增强方法有插入、交换、删除等。

  2. 数据归一化:将数据的特征缩放到相同的范围,减少特征的大小差异。

  3. 数据切分:将数据集拆分成训练集、验证集和测试集,以防止过拟合。

3.8 源语言数据处理

第八步是源语言数据处理。源语言数据处理主要包括词表建立、字符映射、数据集拆分等步骤。

3.9 目标语言数据处理

第九步是目标语言数据处理。目标语言数据处理主要包括词表建立、字符映射、标签映射、数据集拆分等步骤。

3.10 数据批处理

第十步是数据批处理。数据批处理是指将数据集按固定长度切分为批次,在训练时从批次中随机抽取批次进行训练。

3.11 训练集处理

第十一步是训练集处理。训练集处理主要包括:

  1. 文本编码:将文本转换为词索引序列。

  2. 句子排序:按照源句子长度进行排序。

  3. 创建数据迭代器:创建训练数据迭代器,用于从数据集中按批次抽取训练数据。

3.12 模型构建

第十二步是模型构建。模型构建是指构造机器翻译模型,包括编码器、解码器以及优化器。

3.13 损失函数

第十三步是损失函数。损失函数是指用于衡量模型输出与真实值的距离程度。在机器翻译中,我们采用基于注意力机制的损失函数,该函数可以捕捉不同位置上的依赖关系,抓住关键词之间的关联关系。

3.14 优化器设置

第十四步是优化器设置。优化器设置是指设置模型训练时的优化策略,如Adam、Adagrad、SGD等。

3.15 训练模型

第十五步是训练模型。在训练模型时,我们逐步更新模型参数,使得损失函数最小化。模型训练可以分为若干轮次,每轮次数目固定,当一轮训练结束后,我们评价模型的性能。如果模型的性能不佳,可以调整参数,重新训练。

3.16 模型评估

第十六步是模型评估。模型评估主要是指评估模型在开发集和测试集上的性能。模型在开发集上的性能往往决定是否将模型部署到生产环境。模型在测试集上的性能会给予模型更准确的反映。

4.具体代码实例和详细解释说明

4.1 TensorFlow安装

我们使用Anaconda这个开源的Python发行版来安装TensorFlow。Anaconda是一个基于Python的数据科学包管理系统和环境管理系统,用于轻松安装各类数据科学软件,包括TensorFlow。

conda create -n tf_env python=3.7 tensorflow numpy scipy scikit-learn matplotlib ipykernel notebook
activate tf_env # activate conda environment

4.2 数据准备

import os

def download_dataset(url):
    filename = url.split("/")[-1]
    if not os.path.exists(filename):
       !wget $url
    
download_dataset("http://www.manythings.org/anki/deu-eng.zip")

!unzip deu-eng.zip && rm deu-eng.zip
os.rename('deu', 'opensubtitles')

4.3 文件格式转换

from PIL import Image
import numpy as np

images = []
for file in os.listdir("./opensubtitles"):
    with open('./opensubtitles/' + file, encoding='utf-8') as f:
        text = ''
        for line in f:
            text += line.strip('\n').lower().replace('-',' ')
            
        w, h = img.size
        
        pixels = list(img.getdata())
        pixel_matrix = [pixels[i*w:(i+1)*w] for i in range(h)]
        images.append((text,np.array(pixel_matrix)))
        
with open('train.txt', 'w', encoding='utf-8') as fw:
    for image in images:
        fw.write(' '.join([str(x) for x in image[1].flatten()])+'\t'+image[0]+'\n')

4.4 数据清洗

import string

all_lines = []
with open('train.txt', encoding='utf-8') as fr:
    lines = fr.readlines()[::3]
    for line in lines:
        all_lines.extend([line[:64], line[64:]])
        
new_lines = []        
for line in all_lines:
    new_line = ''.join([char for char in line if char not in string.punctuation and 
                        char!='']).encode('ascii', errors='ignore').decode('ascii').lower()
    
    if len(new_line)>0:
        new_lines.append(new_line)
        
unique_words = set(['<unk>'] + sorted(list(set(' '.join(new_lines).split()))))    

vocab_size = min(len(unique_words), 30000)

word2idx = {'<pad>':0}
idx2word = {0:'<pad>'}
for word in unique_words:
    idx = len(idx2word)
    if vocab_size==None or idx < vocab_size:
        word2idx[word] = idx
        idx2word[idx] = word
        
with open('vocab.txt', 'w', encoding='utf-8') as fw:
    fw.write('<pad>\t0\n<unk>\t1\n'+'\n'.join([str(k)+ '\t'+ str(v) for k, v in word2idx.items()]))

4.5 数据集拆分

import random

random.seed(0)
train_lines = random.sample(range(len(new_lines)), int(len(new_lines)*0.7))
dev_lines   = random.sample(list(set(range(len(new_lines)))-set(train_lines)),int(len(new_lines)*0.1))
test_lines  = list(set(range(len(new_lines)))-set(train_lines)-set(dev_lines))

print(len(train_lines))
print(len(dev_lines))
print(len(test_lines))

train_file = './train.txt'
dev_file   = './dev.txt'
test_file  = './test.txt'

with open(train_file, 'w', encoding='utf-8') as fw:
    for idx in train_lines:
        fw.write(new_lines[idx]+'\t'+new_lines[idx+1][:-1]+'\n')

with open(dev_file, 'w', encoding='utf-8') as fw:
    for idx in dev_lines:
        fw.write(new_lines[idx]+'\t'+new_lines[idx+1][:-1]+'\n')

with open(test_file, 'w', encoding='utf-8') as fw:
    for idx in test_lines:
        fw.write(new_lines[idx]+'\t'+new_lines[idx+1][:-1]+'\n')

4.6 数据加载

import tensorflow as tf


class TextDataGenerator(object):

    def __init__(self, data_path, batch_size):

        self._batch_size = batch_size
        input_files = ['./train.txt']
        target_files = ['./dev.txt', './test.txt']

        dataset = tf.data.Dataset.from_tensor_slices((input_files,target_files))
        dataset = dataset.repeat().shuffle(buffer_size=len(input_files)).interleave(lambda x: tf.data.TextLineDataset(x), cycle_length=len(input_files), block_length=1)

        # preprocess function
        def _preprocess(src, trg):

            src = tf.strings.split(src).to_tensor()
            src = tf.keras.preprocessing.sequence.pad_sequences(src, maxlen=64, padding='post')
            src = tf.one_hot(src, depth=len(word2idx), dtype=tf.float32)[..., :-1]
            
            trg = tf.strings.split(trg).to_tensor()
            trg = tf.keras.preprocessing.sequence.pad_sequences(trg, maxlen=64, padding='post')
            trg = tf.concat(([word2idx['<start>']] * (trg.shape[0]-1)), axis=-1)
            trg = tf.concat(([[word2idx.get(token.numpy(), word2idx['<unk>'])] for token in trg[:-1]], [[word2idx['<end>']]]), axis=-1)

            return {"inputs": src}, {"outputs": trg}

        # apply preprocessing to dataset
        dataset = dataset.map(_preprocess)
        dataset = dataset.batch(batch_size, drop_remainder=True)

        # prefetch data into buffer
        dataset = dataset.prefetch(buffer_size=tf.data.AUTOTUNE)

        self._dataset = dataset


    @property
    def steps_per_epoch(self):
        num_samples = sum([sum(1 for _ in open(f,'r'))//2 for f in ['./train.txt']])
        return num_samples // self._batch_size
    

    @property
    def total_steps(self):
        num_samples = sum([sum(1 for _ in open(f,'r'))//2 for f in ['./train.txt', './dev.txt', './test.txt']])
        return num_samples // self._batch_size

    
    def get_generator(self):
        return iter(self._dataset)

4.7 数据处理

import tensorflow as tf
import numpy as np

max_len = 64
num_examples = None


def preprocess_sentence(sent):
    """
    sent : sentence containing space separated words
    """
    sent = '<start>'+sent+'<end>'
    return sent


def load_dataset(path, num_examples=None):
  """Load sentences and labels from path."""

  sentences = []
  labels = []
  lang_labels=[]
  
  with open(path, encoding="utf-8") as f:
      lines = f.read().strip().split("\n")
      
      count = 0
      for line in lines:
          source, target = line.split("\t")
          
          source = preprocess_sentence(source)
          target = preprocess_sentence(target)
          
          if len(source.split()) > max_len or len(target.split()) > max_len:
              continue
          
          sentences.append(source)
          labels.append(target)
          lang_labels.append(count % 4)
          
          if num_examples is not None:
              count += 1
              if count >= num_examples:
                  break
                    
  print(sentences[0])
  print(labels[0])
  return sentences, labels,lang_labels

sentences_train, labels_train,lang_labels_train=load_dataset('./train.txt', num_examples)
sentences_val, labels_val,lang_labels_val=load_dataset('./dev.txt', num_examples)
sentences_test, labels_test,lang_labels_test=load_dataset('./test.txt', num_examples)
print(len(sentences_train))
print(len(sentences_val))
print(len(sentences_test))

4.8 源语言数据处理

from collections import Counter
import re

vocab_size = 30000
tokenizer = Tokenizer(num_words=vocab_size, oov_token="<unk>", lower=False)
tokenizer.fit_on_texts(sentences_train)

train_seq = tokenizer.texts_to_sequences(sentences_train)
train_seq = pad_sequences(train_seq, maxlen=max_len)
train_labels = tokenizer.texts_to_sequences(labels_train)
train_labels = pad_sequences(train_labels, maxlen=max_len)

word_index = tokenizer.word_index

embedding_dim = 256
"""Creating an embedding matrix"""
num_tokens = len(word_index) + 1
hits = 0
misses = 0

embedding_matrix = np.zeros((num_tokens, embedding_dim))

for word, i in word_index.items():
    embedding_vector = embeddings_index.get(word)
    if embedding_vector is not None:
        # Words not found in embedding index will be all-zeros.
        # This includes the representation for "padding" and "OOV"
        embedding_matrix[i] = embedding_vector
        hits += 1
    else:
        misses += 1

print("Converted {} words ({:.2f}% of tokens)".format(hits, 100.0 * hits / num_tokens))
print("Missed {}".format(misses))

del embeddings_index

4.9 目标语言数据处理

from keras_preprocessing.text import Tokenizer
from keras_preprocessing.sequence import pad_sequences

tokenizer = Tokenizer(oov_token='<unk>', filters='')
tokenizer.fit_on_texts(labels_train)
label_word_index = tokenizer.word_index

train_label_seq = tokenizer.texts_to_sequences(labels_train)
train_label_seq = pad_sequences(train_label_seq, padding='post', maxlen=max_len)

reverse_word_index = dict([(value, key) for (key, value) in label_word_index.items()]) 

def decode_review(text):
    return''.join([reverse_word_index.get(i, '?') for i in text])
  
print(decode_review(train_label_seq[0]))

4.10 数据批处理

BATCH_SIZE = 32
BUFFER_SIZE = 10000

train_dataset = tf.data.Dataset.from_tensor_slices((train_seq, train_label_seq))
train_dataset = train_dataset.cache()
train_dataset = train_dataset.shuffle(BUFFER_SIZE)
train_dataset = train_dataset.padded_batch(BATCH_SIZE, padded_shapes=([-1],[max_len]))
train_dataset = train_dataset.prefetch(tf.data.experimental.AUTOTUNE)

4.11 模型构建

from tensorflow.keras.layers import Input, Embedding, LSTM, Dense, Concatenate
from tensorflow.keras.models import Model
from tensorflow.keras.optimizers import Adam
from sklearn.model_selection import KFold

num_layers = 2
units = 256

def build_model():
    
    inputs = Input(shape=(None,))
    embedding_layer = Embedding(vocab_size, embedding_dim, mask_zero=True)(inputs)
    
    encoder = LSTM(units, return_state=True, name='encoder')
    enc_output, enc_hidden, c_state = encoder(embedding_layer)
    enc_states = [enc_hidden, c_state]
    
    decoder_inputs = Input(shape=(None,), name='decoder_inputs')
    dec_embedding = Embedding(vocab_size, embedding_dim, mask_zero=True)(decoder_inputs)
    
    decoder_lstm = LSTM(units, return_sequences=True, return_state=True, name='decoder_lstm')
    
    dec_outputs, _, _ = decoder_lstm(dec_embedding, initial_state=enc_states)
    
    outputs = Dense(vocab_size, activation='softmax')(dec_outputs)
    model = Model([inputs, decoder_inputs], outputs)
    
    optimizer = Adam(lr=0.001)
    loss = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)
    
    model.compile(optimizer=optimizer, loss=[loss])
    
    return model
    
model = build_model()
print(model.summary())

4.12 损失函数

loss_object = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)

def loss_function(real, pred):
    mask = tf.math.logical_not(tf.math.equal(real, 0))
    loss_ = loss_object(real, pred)

    mask = tf.cast(mask, dtype=loss_.dtype)
    loss_ *= mask

    return tf.reduce_mean(loss_)

4.13 优化器设置

optimizer = tf.keras.optimizers.Adam()

4.14 训练模型

history = model.fit(train_dataset, epochs=NUM_EPOCHS, validation_data=val_dataset, verbose=1)

4.15 模型评估

loss = history.history['loss']
val_loss = history.history['val_loss']

plt.plot(loss, label='Training Loss')
plt.plot(val_loss, label='Validation Loss')
plt.legend(loc='upper right')
plt.title('Training and Validation Loss')
plt.show()

4.16 模型预测

def predict_sentence(sentence):
    sentence = preprocess_sentence(sentence)
    sequence = tokenizer.texts_to_sequences([sentence])[0][:max_len]
    sequence = pad_sequences([sequence], maxlen=max_len)
        
    translation = translate_model.predict([sequence])
    
    predicted_sentence = tokenizer.sequences_to_texts(translation)[0]
    final_sentence = re.sub("<.*?>","",predicted_sentence)
    return final_sentence
    
translate_sentence = predict_sentence(sentences[0])
print(translate_sentence)