深度学习

循环神经网络12-机器翻译与数据集

2025-11-10  本文已影响0人  R7_Perfect

一、概述

机器翻译(Machine Translation)指的是将一种语言的文本序列自动转换为另一种语言的文本序列。它不仅是自然语言处理中的一个重要应用,而且还是检验语言模型能力的经典基准。
在历史上,早期的机器翻译方法依赖统计学(称为统计机器翻译),例如著名的Brown等人提出的方法;而近年来,随着神经网络技术的发展,端到端的神经机器翻译(Neural Machine Translation)成为主流。
在神经机器翻译中,我们的任务可以看作是一个 序列转换(sequence transduction) 问题:即将输入序列(源语言)映射成输出序列(目标语言)。这与单一语言的语言模型任务不同,因为这里每个样本是一个语言对。

二、下载与预处理数据集

在本例中,我们使用 Tatoeba 项目的“英-法”双语句子对数据集。数据集中,每一行由制表符分隔,第一部分为英文文本,第二部分为翻译后的法语文本。需要注意的是,每个文本序列既可以是一个单独的句子,也可以是包含多个句子的段落。

2.1 数据下载

下面的代码展示了如何下载数据集并读取原始文本内容:

DATA_HUB['fra-eng'] = (DATA_URL + 'fra-eng.zip', '94646ad1522d915e7b0f9296181140edcf86a4f5')


def read_data_nmt():
    """载入“英语-法语”数据集"""
    data_dir = download_extract('fra-eng')
    with open(os.path.join(data_dir, 'fra.txt'), 'r', encoding='utf-8') as f:
        return f.read()


raw_text = read_data_nmt()
print(raw_text[:75])

输出:

Go. Va !
Hi. Salut !
Run!    Cours !
Run!    Courez !
Who?    Qui ?
Wow!    Ça alors !

2.2 原始数据预处理

下载后,我们需要对原始文本数据进行预处理:

预处理代码示例如下:

def preprocess_nmt(text: str):
    """预处理“英语-法语”数据集"""

    def no_space(char, prev_char):
        return char in set(',.!?') and prev_char != ' '

    # 使用空格替换不间断空格
    # 使用小写字母替换大写字母
    text = text.replace('\u202f', ' ').replace('\xa0', ' ').lower()
    # 在单词和标点符号之间插入空格
    out = [' ' + char if i > 0 and no_space(char, text[i - 1]) else char
           for i, char in enumerate(text)]
    return ''.join(out)


text = preprocess_nmt(raw_text)
print(text[:80])

通过替换为普通的空格,可以使文本在后续的处理(例如分词、标点符号处理等)时更加统一,避免因空格类型不同而引发的意外问题。

输出:

go .    va !
hi .    salut !
run !   cours !
run !   courez !
who ?   qui ?
wow !   ça alors !

三、词元化处理

在机器翻译中,我们更倾向于使用单词级词元化而非字符级词元化。这意味着我们把句子切分成单词或标点符号,而不是单个字符。下面的函数对预处理后的文本数据进行词元化,将数据分为源语言(英文)和目标语言(法语)两部分。

def tokenize_nmt(text, num_examples=None):
    """词元化“英语-法语”数据数据集"""
    source, target = [], []
    for i, line in enumerate(text.split('\n')):
        if num_examples and i > num_examples:
            break
        parts = line.split('\t')
        if len(parts) == 2:
            source.append(parts[0].split(' '))
            target.append(parts[1].split(' '))
    return source, target


source, target = tokenize_nmt(text)
pprint((source[:6], target[:6]))

输出:

([['go', '.'],
  ['hi', '.'],
  ['run', '!'],
  ['run', '!'],
  ['who', '?'],
  ['wow', '!']],
 [['va', '!'],
  ['salut', '!'],
  ['cours', '!'],
  ['courez', '!'],
  ['qui', '?'],
  ['ça', 'alors', '!']])

让我们绘制每个文本序列所包含的词元数量的直方图。在这个简单的“英-法”数据集中,大多数文本序列的词元数量少于20个。

def show_list_len_pair_hist(legend, xlabel, ylabel, xlist, ylist):
    """绘制列表长度对的直方图"""
    set_figsize((6.18, 3.82))
    _, _, patches = plt.hist([[len(l) for l in xlist], [len(l) for l in ylist]])
    plt.xlabel(xlabel)
    plt.ylabel(ylabel)
    for patch in patches[1].patches:
        patch.set_hatch('/')
    plt.legend(legend)
    plt.show()


show_list_len_pair_hist(['source', 'target'], '# tokens per sequence', 'count', source, target)

四、构建词表

由于机器翻译数据集由语言对构成,我们需要分别为源语言和目标语言构建词表。
注意: 单词级词元化生成的词表会比字符级词元化大得多。为此,我们将出现次数少于2次的词元统一视为“<unk>”(未知词元),同时还添加了以下特殊词元:

src_vocab = d2l.Vocab(source, min_freq=2, reserved_tokens=['<pad>', '<bos>', '<eos>'])
print(len(src_vocab))  # 输出:10012

五、加载数据集与批量处理

在机器翻译任务中,每个样本是一个包含源语言和目标语言的文本序列对,而且每个序列的长度通常不一致。为提高计算效率,我们需要将同一小批量内的所有序列调整为相同长度,这通常通过截断和填充实现。

5.1 截断与填充

假设我们设定固定长度为 num_steps,对于每个序列:


684eb4e8-b6a3-4909-80eb-df1be9cb3855.png

这一过程可以用下面的数学公式描述:


55802243-d33b-4a9f-a1fe-ba46f4aa71e5.png
代码实现如下:
def truncate_pad(line, num_steps, padding_token):
    """截断或填充文本序列"""
    if len(line) > num_steps:
        return line[:num_steps]
    return line + [padding_token] * (num_steps - len(line))
# 示例:对第一个英文序列进行截断或填充
print(d2l.truncate_pad(src_vocab[source[0]], 10, src_vocab['<pad>']))

输出:

[47, 4, 1, 1, 1, 1, 1, 1, 1, 1]

5.2 构造小批量数据

为了方便模型训练,我们还需要构建一个函数,将处理好的文本序列转换成小批量数据。这里的操作包括:

有效长度可以用以下公式表示:


deea831c-6a0e-4763-85b7-31ed5585f8aa.png

对应代码如下:

def build_array_nmt(lines, vocab, num_steps):
    """将机器翻译的文本序列转换成小批量"""
    lines = [vocab[line] for line in lines]
    lines = [l + [vocab['<eos>']] for l in lines]
    array = torch.tensor([truncate_pad(
        l, num_steps, vocab['<pad>']) for l in lines])
    valid_len = (array != vocab['<pad>']).type(torch.int32).sum(dim=1)
    return array, valid_len    

接下来,我们定义一个函数来加载数据并返回数据迭代器,同时输出源语言和目标语言的词表:

def load_data_nmt(batch_size, num_steps, num_examples=600):
    """返回翻译数据集的迭代器和词表"""
    text = preprocess_nmt(read_data_nmt())
    source, target = tokenize_nmt(text, num_examples)
    src_vocab = Vocab(source, min_freq=2,
                      reserved_tokens=['<pad>', '<bos>', '<eos>'])
    tgt_vocab = Vocab(target, min_freq=2,
                      reserved_tokens=['<pad>', '<bos>', '<eos>'])
    src_array, src_valid_len = build_array_nmt(source, src_vocab, num_steps)
    tgt_array, tgt_valid_len = build_array_nmt(target, tgt_vocab, num_steps)
    data_arrays = (src_array, src_valid_len, tgt_array, tgt_valid_len)
    data_iter = load_array(data_arrays, batch_size)
    return data_iter, src_vocab, tgt_vocab

读取一个小批量数据示例

train_iter, src_vocab, tgt_vocab = d2l.load_data_nmt(batch_size=2, num_steps=8)
for X, X_valid_len, Y, Y_valid_len in train_iter:
    print('X:', X.type(torch.int32))
    print('X的有效长度:', X_valid_len)
    print('Y:', Y.type(torch.int32))
    print('Y的有效长度:', Y_valid_len)
    break

输出示例可能为:

X: tensor([[  7, 101,   4,   3,   1,   1,   1,   1],
        [  7, 151,   4,   3,   1,   1,   1,   1]], dtype=torch.int32)
X的有效长度: tensor([4, 4])
Y: tensor([[  6,   7, 158,   4,   3,   1,   1,   1],
        [  6,   7,  32,  29, 108,   4,   3,   1]], dtype=torch.int32)
Y的有效长度: tensor([5, 7])

六、训练模型与小结

在完成上述数据预处理、词元化、词表构建以及批量数据加载之后,就可以利用这些数据来训练神经机器翻译模型。在训练过程中,模型会一个词一个词地生成输出序列,当生成到 <eos> 时,就认为输出完成。

通过这些步骤,我们不仅可以清晰地理解机器翻译的基本原理,也为后续模型训练打下坚实基础。希望这篇博客能帮助你快速入门神经机器翻译相关技术!

上一篇 下一篇

猜你喜欢

热点阅读