当前位置: 首页 > news >正文

《动手学深度学习》读书笔记—9.5机器翻译与数据集

本文记录了自己在阅读《动手学深度学习》时的一些思考,仅用来作为作者本人的学习笔记,不存在商业用途。
语言模型是自然语言处理的关键, 而机器翻译是语言模型最成功的基准测试。 因为机器翻译正是将输入序列转换成输出序列的 序列转换模型(sequence transduction)的核心问题。 序列转换模型在各类现代人工智能应用中发挥着至关重要的作用, 本节将介绍机器翻译问题及其后文需要使用的数据集。
机器翻译(machine translation)指的是 将序列从一种语言自动翻译成另一种语言。 事实上,这个研究领域可以追溯到数字计算机发明后不久的20世纪40年代, 特别是在第二次世界大战中使用计算机破解语言编码。 几十年来,在使用神经网络进行端到端学习的兴起之前, 统计学方法在这一领域一直占据主导地位 (Brown et al., 1990, Brown et al., 1988)。 因为统计机器翻译(statistical machine translation)涉及了 翻译模型和语言模型等组成部分的统计分析, 因此基于神经网络的方法通常被称为 神经机器翻译(neural machine translation), 用于将两种翻译模型区分开来。
本书的关注点是神经网络机器翻译方法,强调的是端到端的学习。 机器翻译的数据集是由源语言和目标语言的文本序列对组成的。 因此,我们需要一种完全不同的方法来预处理机器翻译数据集。

import os
import torch
from d2l import torch as d2l

9.5.1 下载和预处理数据集

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

#@save
d2l.DATA_HUB['fra-eng'] = (d2l.DATA_URL + 'fra-eng.zip','94646ad1522d915e7b0f9296181140edcf86a4f5')#@save
def read_data_nmt():"""载入“英语-法语”数据集"""data_dir = d2l.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()
# 打印前面75个字符(0到74)
print(raw_text[:75])

🏷运行结果

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

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

#@save
def preprocess_nmt(text):"""预处理“英语-法语”数据集"""def no_space(char, prev_char):# 如果当前字符是特殊符号且上一个字符不是空格, 说明当前字符是单词后面跟着的特殊符号, 则该字符前应该添加空格# 返回布尔变量, 需要添加空格时则返回True, 否则返回Falsereturn char in set(',.!?') and prev_char != ' '# 使用空格替换不间断空格# text.replace('\u202f', ' ')使用标准空格替换Unicode编码中的窄空格'\u202f'# text.replace('\xa0', ' ')使用标准空格替换Unicode编码中的不换行空格'\xa0'# 使用小写字母替换大写字母text = text.replace('\u202f', ' ').replace('\xa0', ' ').lower()# 在单词和标点符号之间插入空格# for i, char in enumerate(text)返回字符串text中的每个字符以及其对应的索引# if i > 0 and no_space(char, text[i - 1])如果当前字符不是首字符并且当前字符是特殊字符(,.!?), 则说明当前字符是单词后面的特殊字符, 在中间插入空格# else如果当前字符是首字符或不满足上述条件, 则当前字符是单词内的字符, 直接返回即可out = [' ' + char if i > 0 and no_space(char, text[i - 1]) else charfor i, char in enumerate(text)]# out是数据集raw_text经过字符修正后的结果, 是一个列表, 列表中每个元素是修正后的数据集中的字符# ''.join(out)表示利用列表out中的元素组建字符串, 其中不使用任何额外字符填充# ''.join(["A","B","C"])	"ABC"# ' '.join(["A","B","C"])	"A B C"# '-'.join(["A","B","C"])	"A-B-C"return ''.join(out)# 处理数据集并打印前80个字符
text = preprocess_nmt(raw_text)
print(text[:80])

🏷预处理后的数据集如下

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

9.5.2 词元化

在机器翻译中,我们更喜欢单词级词元化 (最先进的模型可能使用更高级的词元化技术)。 下面的tokenize_nmt函数对前num_examples个文本序列对进行词元, 其中每个词元要么是一个词,要么是一个标点符号。此函数返回两个词元列表:source和target: sourcei是源语言(这里是英语)第iii个文本序列的词元列表, targeti是目标语言(这里是法语)第iii个文本序列的词元列表。

#@save
def tokenize_nmt(text, num_examples=None):"""词元化“英语-法语”数据数据集"""# 创建两个空列表source和targetsource, target = [], []# text.split('\n')按换行符拆分修正后的数据集text,得到每个英语-法语单词对(一行是一个)# for i, line in enumerate(text.split('\n'))返回每行line及行索引ifor i, line in enumerate(text.split('\n')):# 如果num_examples存在值(正整数)并且当前处理的行超过了num_examples时则终止(当前处理的是第i个单词对, 但设置了只处理前面num_examples个单词对)if num_examples and i > num_examples:break# line.split('\t')按照空格拆分, parts[0]是英语部分, parts[1]是法语部分parts = line.split('\t')# 如果拆分不出来两个部分, 说明当前行不是英语-法语单词对if len(parts) == 2:# 按照空格拆分英语部分(单词直接有空格, 单词和特殊符号之间有空格)source.append(parts[0].split(' '))# 按照空格拆分法语部分(单词直接有空格, 单词和特殊符号之间有空格)target.append(parts[1].split(' '))# 返回英语列表source和法语列表targetreturn source, target# 打印前6个英语单词和对应的法语单词
source, target = tokenize_nmt(text)
source[:6], target[:6]

🏷运行结果如下

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

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

#@save
def show_list_len_pair_hist(legend, xlabel, ylabel, xlist, ylist):"""绘制列表长度对的直方图"""# 设置图表大小d2l.set_figsize()# for l in xlist, for l in ylist遍历source和target中的每个元素(就是原始数据text的每一行)# len(l)获得source和target中包含的词元数量, 比如英语的['go','.']是两个词元, 对应法语的['va','!']也是两个词元_, _, patches = d2l.plt.hist([[len(l) for l in xlist], [len(l) for l in ylist]])d2l.plt.xlabel(xlabel)d2l.plt.ylabel(ylabel)# 设置target的直方图样式为带斜线for patch in patches[1].patches:patch.set_hatch('/')d2l.plt.legend(legend)show_list_len_pair_hist(['source', 'target'], '# tokens per sequence','count', source, target);

统计图

9.5.3 词表

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

# 具体可以参考8.2文本预处理中的代码
# 以['go', '.']为例, 在d2l.Vocab中调用collection.Counter时会分别统计'go'和'.'出现的频率
src_vocab = d2l.Vocab(source, min_freq=2,reserved_tokens=['<pad>', '<bos>', '<eos>'])
len(src_vocab)

🏷运行结果显示英语词表共包含10012个词元

10012

9.5.4 加载数据集

在机器翻译中,每个样本都是由源和目标组成的文本序列对, 其中的每个文本序列可能具有不同的长度。为了提高计算效率,我们仍然可以通过截断(truncation)和 填充(padding)方式实现一次只处理一个小批量的文本序列。 假设同一个小批量中的每个序列都应该具有相同的长度num_steps, 那么如果文本序列的词元数目少于num_steps时, 我们将继续在其末尾添加特定的“”词元, 直到其长度达到num_steps; 反之,我们将截断文本序列时,只取其前num_steps 个词元, 并且丢弃剩余的词元。这样,每个文本序列将具有相同的长度, 以便以相同形状的小批量进行加载。

#@save
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))  # 填充# 根据source[0], 即['go', '.'], 去词表src_vocab中查找对应的词元索引
# 由于source[0] = ['go','.']共包含两个词元, 而num_steps设置了10, 即每行应包括10个词元,
# 所以需要填充8个'<pad>', 去词表src_vocab中查找对应的词元索引后拼接成包含10个词元索引的向量
truncate_pad(src_vocab[source[0]], 10, src_vocab['<pad>'])

🏷运行结果如下所示

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

现在我们定义一个函数,可以将文本序列转换成小批量数据集用于训练。 我们将特定的“”词元添加到所有序列的末尾, 用于表示序列的结束。 当模型通过一个词元接一个词元地生成序列进行预测时, 生成的“”词元说明完成了序列输出工作。 此外,我们还记录了每个文本序列的长度, 统计长度时排除了填充词元, 在稍后将要介绍的一些模型会需要这个长度信息。

#@save
def build_array_nmt(lines, vocab, num_steps):"""将机器翻译的文本序列转换成小批量"""# for l in lines, 从lines中遍历每行l# vocab[l]获得该行中的词元在词表中的索引# lines是一个列表, 每个元素也是列表, 包含该行中的所有词元在词表中索引, 比如[[47, 4], [12, 4]]lines = [vocab[l] for l in lines]# 遍历lines中的每个元素l, 向里面加入终止符'<eos>'在词表中对应的索引, 于是lines变成类似[[47, 4, 0], [12, 4, 0]]lines = [l + [vocab['<eos>']] for l in lines]# 遍历lines中的每个元素, 如果不满足num_steps则填充'<pad>'在词表中的索引# 转成torch.tensor返回# array类似tensor([[  9,   4,   3,  ...,   1,   1,   1],#                  [113,   4,   3,  ...,   1,   1,   1]])array = torch.tensor([truncate_pad(l, num_steps, vocab['<pad>']) for l in lines])# array的每个元素就是一行, array != vocab['<pad>']逐元素比较是否为填充词元'<pad>'的索引, 获得布尔值# sum(1)按照列求和, 于是可以得到一行中有多少不是填充词元'<pad>'的词元, 即获得一行的长度valid_len = (array != vocab['<pad>']).type(torch.int32).sum(1)# 返回填充后的每行索引以及每行的长度(包含的词元数)return array, valid_len

9.5.5 训练模型

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

#@save
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 = d2l.Vocab(source, min_freq=2,reserved_tokens=['<pad>', '<bos>', '<eos>'])# 根据法语获得法语词表tgt_vocab = d2l.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)# 返回数据集迭代器, d2l.load_array需要元组型数据和批量大小, 元组型数据格式是(数据特征, 标签)data_iter = d2l.load_array(data_arrays, batch_size)return data_iter, src_vocab, tgt_vocab

读取"英语-法语"数据集中的第一个小批量数据

# 批量大小为2, 时间长度为8(每行应该包括8个词元)
# 返回训练集迭代器, 英语词表, 法语词表
train_iter, src_vocab, tgt_vocab = 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

🏷运行结果,注意每个批量使用了两行(batch_size = 2),每行需要8个词元(num_step = 8)

X: tensor([[ 7, 43,  4,  3,  1,  1,  1,  1],[44, 23,  4,  3,  1,  1,  1,  1]], dtype=torch.int32)
X的有效长度: tensor([4, 4])
Y: tensor([[ 6,  7, 40,  4,  3,  1,  1,  1],[ 0,  5,  3,  1,  1,  1,  1,  1]], dtype=torch.int32)
Y的有效长度: tensor([5, 3])
http://www.lryc.cn/news/611648.html

相关文章:

  • miniExcel一个对象加一个对象列表导出
  • 前端全栈修炼手册:从 Vue3 到工程化的进阶之路
  • 线上Linux服务器的优化设置、系统安全与网络安全策略
  • 移动商城平台适配:ZKmall开源商城鸿蒙 / 小程序端开发要点
  • django permission_classes = [AllowAny] 如何限制到具体接口
  • 时间轮算法
  • Java学习第一百一十一部分——Jenkins(二)
  • docker-compose快速部署启动file beat+ELK
  • Git 分支管理:从新开发分支迁移为主分支的完整指南
  • Agent安全机制:权限控制与风险防范
  • 商派小程序商城(小程序/官网/APP···)的范式跃迁与增长再想象
  • C语言基础_排序算法和二分法查找
  • GROUP BY与ORDER BY的索引优化方法
  • 脑洞大开——AI流程图如何改变思维?
  • 深入解析Java NIO在高并发场景下的性能优化实践指南
  • 企业网络安全中人工智能(AI)的影响
  • 使用MatterJs物理2D引擎实现重力和鼠标交互等功能,有点击事件(盒子堆叠效果)
  • HTML应用指南:利用GET请求获取全国OPPO官方授权体验店门店位置信息
  • nlp-词汇分析
  • easyExcel 读取有合并单元格数据
  • EasyExcel高效工具类:简化Excel导入导出,支持多Sheet与枚举转换
  • QT----QAxObject在子线程中调用,发现excel指针为空
  • Excel制作尖刀图,直观展示业绩涨跌
  • EXCEL-业绩、目标、达成、同比、环比一图呈现
  • 从“T+1”到“T+0”:基于SQL构建MES到数据仓库的数据采集通道
  • OpenGL VBO:顶点缓冲对象的深度解析
  • 点穴式优化:用DeepSeek精准识别关键节点的产品体验突破法
  • PostgreSQL报错“maximum number of prepared transactions reached”原因及高效解决方案解析
  • 推荐一款优质的开源博客与内容管理系统
  • Mac安装WebStorm