李沐动手学深度学习V2-机器翻译和数据集

一. 机器翻译和数据集

1. 介绍

机器翻译的数据集是由源语言和目标语言的文本序列对组成的,因此需要一种完全不同的方法来预处理机器翻译数据集, 而不是复用语言模型的预处理程序。

2. 下载和预处理数据集

首先,下载一个由Tatoeba项目的双语句子对组成的“英-法”数据集,数据集中的每一行都是制表符分隔的文本序列对,序列对由英文文本序列和翻译后的法语文本序列组成。注意每个文本序列可以是一个句子,也可以是包含多个句子的一个段落。在将英语翻译成法语的机器翻译问题中,英语是源语言(source language),法语是目标语言(target language)。

import torch
import os
import d2l.torch
d2l.torch.DATA_HUB['fra-eng'] = (d2l.torch.DATA_URL + 'fra-eng.zip',
                           '94646ad1522d915e7b0f9296181140edcf86a4f5')
"""载入“英语-法语”数据集"""
def read_data_nmt():
    data_dir = d2l.torch.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() #注意raw_text是一个String类型
print(raw_text[:80])
'''
输出结果如下:
Go.	Va !
Hi.	Salut !
Run!	Cours !
Run!	Courez !
Who?	Qui ?
Wow!	Ça alors !
Fire!
'''

下载数据集后,原始文本数据需要经过几个预处理步骤,例如用空格代替不间断空格(non-breaking space), 使用小写字母替换大写字母,并在单词和标点符号之间插入空格。

# 将字符和标点符号,.!?以空格方式分隔开,同时将不间断空格用空格符替换,用于下面tokenize_nmt()处理
def process_nmt(text):
    def no_space(char,pre_char):
        return char in set(',.!?') and pre_char != ' '
    #text就是一个String类型,将不间断空格用空格符替换
    # 使用空格替换不间断空格
    # 使用小写字母替换大写字母
    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 = process_nmt(raw_text)
print(text[:80])

3. 词元化

在机器翻译中,将单词级词元化,而不是将字符级词元化,下面的tokenize_nmt()函数对前num_examples个文本序列对进行词元化, 其中每个词元要么是一个词,要么是一个标点符号,此函数返回两个词元列表:source和target: source[i]是源语言(这里是英语)第 个文本序列的词元列表, target[i]是目标语言(这里是法语)第 个文本序列的词元列表

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里面每个元素是一个列表,代表每行数据英文那部分,英文那部分是通过空白字符分隔形成的列表
            source.append(parts[0].split(' '))
            # target里面每个元素是一个列表,代表每行数据法语那部分,法语那部分是通过空白字符分隔形成的列表
            target.append(parts[1].split(' '))
    return source,target
'''
source,target是一个list of list ,例如:
[['go', '.'],
  ['hi', '.'],
  ['run', '!'],
  ['run', '!'],
  ['who', '?'],
  ['wow', '!']]
[['va', '!'],
 ['salut', '!'],
 ['cours', '!'],
 ['courez', '!'],
 ['qui', '?'],
 ['ça', 'alors', '!']]
'''
source,target = tokenize_nmt(text)
source[:6],target[:6]
'''
输出结果如下:
([['go', '.'],
  ['hi', '.'],
  ['run', '!'],
  ['run', '!'],
  ['who', '?'],
  ['wow', '!']],
 [['va', '!'],
  ['salut', '!'],
  ['cours', '!'],
  ['courez', '!'],
  ['qui', '?'],
'''

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

def show_list_len_pair_hist(legend,xlabel,ylabel,xlist,ylist):
    """绘制列表长度对的直方图"""
    d2l.torch.set_figsize()
    d2l.torch.plt.xlabel(xlabel)
    d2l.torch.plt.ylabel(ylabel)
    _,_,patches = d2l.torch.plt.hist([[len(l) for l in xlist],[len(l) for l in ylist]])
    for patch in patches[1].patches:
        patch.set_hatch('/')
    d2l.torch.plt.legend(legend)
show_list_len_pair_hist(['source','target'],'# tokens per sequence','count',source,target)

李沐动手学深度学习V2-机器翻译和数据集_第1张图片

4. 词表

由于机器翻译数据集由语言对组成, 因此可以分别为源语言和目标语言构建两个词表。 使用单词级词元化时,词表大小将明显大于使用字符级词元化时的词表大小。 为了缓解这一问题,这里将出现次数少于2次的低频率词元视为相同的未知(“”)词元。 除此之外还指定了额外的特定词元, 例如在小批量时用于将序列填充到相同长度的填充词元(“”), 以及序列的开始词元(“”)和结束词元(“”),这些特殊词元在自然语言处理任务中比较常用。

#source 为list of list
src_vocab = d2l.torch.Vocab(source,min_freq=2,reserved_tokens=['','','']) #''对应id为0,''对应id为1,''对应id为2,''对应id为3
len(src_vocab) #词表中唯一词的个数,对应生成对应的id个数

5. 加载数据集

语言模型中的序列样本都有一个固定的长度, 无论这个样本是一个句子的一部分还是跨越了多个句子的一个片断。 这个固定长度是由 num_steps(时间步数或词元数量)参数指定的。 在机器翻译中,每个样本都是由源和目标组成的文本序列对, 其中的每个文本序列可能具有不同的长度。
为了提高计算效率,可以通过截断(truncation)和 填充(padding)方式将文本序列变成一个固定num_steps长度的样本序列,从而有利于网络一次性加载一个批量的样本数据。 假设同一个小批量中的每个序列都应该具有相同的长度num_steps, 那么如果文本序列的词元数目少于num_steps时, 将继续在其末尾添加特定的“”词元, 直到其长度达到num_steps; 反之将截断文本序列时,只取其前num_steps 个词元, 并且丢弃剩余的词元。这样每个文本序列将具有相同的长度, 以便以相同形状的小批量进行加载。

#将每个样本序列采取填充或者截断方式变成一个固定长度的序列用于下面数据集加载,网络训练
def truncate_padding(line,num_steps,padding_token):
    """截断或填充文本序列"""
    if len(line)>num_steps:
        return line[:num_steps]  # 截断
    return line+[padding_token]*(num_steps-len(line)) # 填充
truncate_padding(src_vocab[source[0]],10,src_vocab[''])

定义一个函数,将文本序列转换成小批量数据集用于训练。将特定的“”词元添加到所有序列的末尾, 用于表示序列的结束,当模型通过一个词元接一个词元地生成序列进行预测时, 生成的“”词元说明完成了序列预测输出工作,此外记录了每个文本序列的真实长度, 统计长度时排除了填充词元。

def build_array_nmt(lines,vocab,num_steps):
    """将机器翻译的文本序列转换成小批量"""
    # vocab[data]:无论data是一个词,或者是一个列表,或者是一个list of list 类型,返回的都是一个list,list里面是这些词对应的id
    # lines是一个list of list类型,里面每个元素是一个list,代表一个英文序列,也即是txt文件中每行数据中英文那部分序列,list中每个元素是这个英文序列的每个词的token
    lines = [vocab[line] for line in lines]
    lines = [line+[vocab['']] for line in lines] #给每个英文序列(一个英文句子)添加一个结束符''
    # array 是一个list of list ,里面每个元素是一个list,代表每个英文序列的token
    array = torch.tensor([truncate_padding(line,num_steps,vocab['']) for line in lines]) #表明并不是每一个英文序列token都会以''结尾,需要看这个英文序列的长度和num_steps
    valid_len = (array != vocab['']).type(torch.int32).sum(1) #sum(dim=1)表示求出每个序列样本的真实长度,除开填充长度,为一个list列表,里面每个元素代码一个英文样本序列真是长度真实
    return array,valid_len

6. 定义数据集迭代器

定义load_data_nmt()函数来返回数据迭代器, 以及源语言和目标语言的两种词表。

def load_data_nmt(batch_size,num_steps,num_examples=600):
    """返回翻译数据集的迭代器和词表"""
    #读取txt文件
    raw_text = read_data_nmt()
    # 将字符和标点符号,.!?以空格方式分隔开,同时将不间断空格用空格符替换,用于下面tokenize_nmt()处理
    text = process_nmt(raw_text)
    # 将每行英文和法语句子分隔开,同时将英文句子以空格分隔开,将法语句子以空格分隔开,source,target都是list of list类型,num_examples表示读取文本数据最大行数是多少
    source,target = tokenize_nmt(text,num_examples)
    # 建立英文词表vocab,法语词表vocab
    src_vocab = d2l.torch.Vocab(source,min_freq=2,reserved_tokens=['','',''])
    tgt_vocab = d2l.torch.Vocab(target,min_freq=2,reserved_tokens=['','',''])
    # 将source,target转变成对应src_vocab,tgt_vocab里面的词表id索引,同时将每个句子样本序列通过裁剪或者填充成一个固定num_steps长度的句子
    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是四个:批量X(批量序列数据集), 批量X_valid_len(批量中每个序列样本的长度),
    # 批量Y(批量标签lable), 批量Y_valid_len(批量中每个目标序列样本的长度)
    #返回值data_iter是一个元组,有四个值:批量X(批量序列数据集), 批量X_valid_len(批量中每个序列样本的长度), 批量Y(批量标签lable), 批量Y_valid_len(批量中每个目标序列样本的长度)
    data_iter = d2l.torch.load_array(data_arrays,batch_size)#生成数据集迭代器是将所有样本数据随机打乱然后再把batch_size个样本数据组合在一起生成一个批量样本数据
    #返回数据迭代器,src_vocab,tgt_vocab
    return data_iter,src_vocab,tgt_vocab

读出“英语-法语”数据集中的第一个小批量数据:

data_iter,src_vocab,tgt_vocab = load_data_nmt(batch_size=2,num_steps=8)
for X,X_valid_len,Y,Y_valid_len in data_iter:
    print('X: ',X.type(torch.int32))
    print('X_valid_len: ',X_valid_len.type(torch.int32))
    print('Y: ',Y.type(torch.int32))
    print('Y_valid_len: ',Y_valid_len.type(torch.int32))
    break #只读出“英语-法语”数据集中的第一个小批量数据
'''
输出结果如下:
X:  tensor([[41, 11,  3,  1,  1,  1,  1,  1],
        [36, 12,  4,  3,  1,  1,  1,  1]], dtype=torch.int32)
X_valid_len:  tensor([3, 4], dtype=torch.int32)
Y:  tensor([[ 24,   9,   3,   1,   1,   1,   1,   1],
        [131,   4,   3,   1,   1,   1,   1,   1]], dtype=torch.int32)
Y_valid_len:  tensor([3, 3], dtype=torch.int32)
'''

7. 小结

  • 机器翻译指的是将文本序列从一种语言自动翻译成另一种语言。
  • 使用单词级词元化时的词表大小,将明显大于使用字符级词元化时的词表大小,为了缓解这一问题,可以将低频词元视为相同的未知词元。
  • 通过截断和填充文本序列,可以保证所有的文本序列都具有相同的长度,以便以小批量的方式加载。

8. 全部代码

import torch
import os
import d2l.torch

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


def read_data_nmt():
    """载入“英语-法语”数据集"""
    data_dir = d2l.torch.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()  #注意raw_text是一个String类型
print(raw_text[:80])


# 将字符和标点符号,.!?以空格方式分隔开,同时将不间断空格用空格符替换,用于下面tokenize_nmt()处理
def process_nmt(text):
    def no_space(char, pre_char):
        return char in set(',.!?') and pre_char != ' '

    #text就是一个String类型,将不间断空格用空格符替换
    # 使用空格替换不间断空格
    # 使用小写字母替换大写字母
    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 = process_nmt(raw_text)
print(text[:80])


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里面每个元素是一个列表,代表每行数据英文那部分,英文那部分是通过空白字符分隔形成的列表
            source.append(parts[0].split(' '))
            # target里面每个元素是一个列表,代表每行数据法语那部分,法语那部分是通过空白字符分隔形成的列表
            target.append(parts[1].split(' '))
    return source, target


'''
source,target是一个list of list ,例如:
[['go', '.'],
  ['hi', '.'],
  ['run', '!'],
  ['run', '!'],
  ['who', '?'],
  ['wow', '!']]
[['va', '!'],
 ['salut', '!'],
 ['cours', '!'],
 ['courez', '!'],
 ['qui', '?'],
 ['ça', 'alors', '!']]
'''
source, target = tokenize_nmt(text)
source[:6], target[:6]


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


show_list_len_pair_hist(['source', 'target'], '# tokens per sequence', 'count', source, target)
#source 为list of list
src_vocab = d2l.torch.Vocab(source, min_freq=2, reserved_tokens=['', '',
                                                                 ''])  #''对应id为0,''对应id为1,''对应id为2,''对应id为3
len(src_vocab)  #词表中唯一词的个数,对应生成对应的id个数


#将每个样本序列采取填充或者截断方式变成一个固定长度的序列用于下面数据集加载,网络训练
def truncate_padding(line, num_steps, padding_token):
    """截断或填充文本序列"""
    if len(line) > num_steps:
        return line[:num_steps]  # 截断
    return line + [padding_token] * (num_steps - len(line))  # 填充


truncate_padding(src_vocab[source[0]], 10, src_vocab[''])


def build_array_nmt(lines, vocab, num_steps):
    """将机器翻译的文本序列转换成小批量"""
    # vocab[data]:无论data是一个词,或者是一个列表,或者是一个list of list 类型,返回的都是一个list,list里面是这些词对应的id
    # lines是一个list of list类型,里面每个元素是一个list,代表一个英文序列,也即是txt文件中每行数据中英文那部分序列,list中每个元素是这个英文序列的每个词的token
    lines = [vocab[line] for line in lines]
    lines = [line + [vocab['']] for line in lines]  #给每个英文序列(一个英文句子)添加一个结束符''
    # array 是一个list of list ,里面每个元素是一个list,代表每个英文序列的token
    array = torch.tensor([truncate_padding(line, num_steps, vocab['']) for line in
                          lines])  #表明并不是每一个英文序列token都会以''结尾,需要看这个英文序列的长度和num_steps
    valid_len = (array != vocab['']).type(torch.int32).sum(
        1)  #sum(dim=1)表示求出每个序列样本的真实长度,除开填充长度,为一个list列表,里面每个元素代码一个英文样本序列真是长度真实
    return array, valid_len


def load_data_nmt(batch_size, num_steps, num_examples=600):
    """返回翻译数据集的迭代器和词表"""
    #读取txt文件
    raw_text = read_data_nmt()
    # 将字符和标点符号,.!?以空格方式分隔开,同时将不间断空格用空格符替换,用于下面tokenize_nmt()处理
    text = process_nmt(raw_text)
    # 将每行英文和法语句子分隔开,同时将英文句子以空格分隔开,将法语句子以空格分隔开,source,target都是list of list类型,num_examples表示读取文本数据最大行数是多少
    source, target = tokenize_nmt(text, num_examples)
    # 建立英文词表vocab,法语词表vocab
    src_vocab = d2l.torch.Vocab(source, min_freq=2, reserved_tokens=['', '', ''])
    tgt_vocab = d2l.torch.Vocab(target, min_freq=2, reserved_tokens=['', '', ''])
    # 将source,target转变成对应src_vocab,tgt_vocab里面的词表id索引,同时将每个句子样本序列通过裁剪或者填充成一个固定num_steps长度的句子
    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是四个:批量X(批量序列数据集), 批量X_valid_len(批量中每个序列样本的长度),
    # 批量Y(批量标签lable), 批量Y_valid_len(批量中每个目标序列样本的长度)
    #返回值data_iter是一个元组,有四个值:批量X(批量序列数据集), 批量X_valid_len(批量中每个序列样本的长度), 批量Y(批量标签lable), 批量Y_valid_len(批量中每个目标序列样本的长度)
    data_iter = d2l.torch.load_array(data_arrays, batch_size)  #生成数据集迭代器是将所有样本数据随机打乱然后再把batch_size个样本数据组合在一起生成一个批量样本数据
    #返回数据迭代器,src_vocab,tgt_vocab
    return data_iter, src_vocab, tgt_vocab


data_iter, src_vocab, tgt_vocab = load_data_nmt(batch_size=2, num_steps=8)
for X, X_valid_len, Y, Y_valid_len in data_iter:
    print('X: ', X.type(torch.int32))
    print('X_valid_len: ', X_valid_len.type(torch.int32))
    print('Y: ', Y.type(torch.int32))
    print('Y_valid_len: ', Y_valid_len.type(torch.int32))
    break

8. 相关链接

机器翻译第一篇:李沐动手学深度学习V2-机器翻译和数据集
机器翻译第二篇:李沐动手学深度学习V2-Encoder-Decoder编码器和解码器架构
机器翻译第三篇:李沐动手学深度学习V2-seq2seq和代码实现
机器翻译第四篇:李沐动手学深度学习V2-基于注意力机制的seq2seq

你可能感兴趣的:(李沐动手学深度学习笔记,深度学习,机器翻译,seq2seq,auto-encoder,自然语言处理)