越学越有趣:『手把手带你学NLP』系列项目07 ——机器翻译的那些事儿

点击左上方蓝字关注我们

课程简介

“手把手带你学NLP”是基于飞桨PaddleNLP的系列实战项目。本系列由百度多位资深工程师精心打造,提供了从词向量、预训练语言模型,到信息抽取、情感分析、文本问答、结构化数据问答、文本翻译、机器同传、对话系统等实践项目的全流程讲解,旨在帮助开发者更全面清晰地掌握百度飞桨框架在NLP领域的用法,并能够举一反三、灵活使用飞桨框架和PaddleNLP进行NLP深度学习实践。

6月,百度飞桨 & 自然语言处理部携手推出了12节NLP视频课,课程中详细讲解了本实践项目。

观看课程回放请戳:https://aistudio.baidu.com/aistudio/course/introduce/24177

欢迎来课程QQ群(群号:758287592)交流吧~~


背景介绍

机器翻译是利用计算机将一种自然语言(源语言)转换为另一种自然语言(目标语言)的过程。

本项目是机器翻译领域主流模型 Transformer 的 PaddlePaddle 实现,快来基于此项目搭建自己的翻译模型吧。

Transformer 是论文《 Attention Is All You Need 》中提出的用以完成机器翻译(Machine Translation)等序列到序列(Seq2Seq)学习任务的一种全新网络结构,其完全使用注意力(Attention)机制来实现序列到序列的建模。

图1:Transformer 网络结构图

相较于此前 Seq2Seq 模型中广泛使用的循环神经网络(Recurrent Neural Network, RNN),使用Self Attention进行输入序列到输出序列的变换主要具有以下优势:

  • 计算复杂度小

特征维度为 d 、长度为 n 的序列,在 RNN 中计算复杂度为 O(n * d * d) (n 个时间步,每个时间步计算 d 维的矩阵向量乘法),在Transformer中计算复杂度为 O(n * n * d) (n 个时间步两两计算 d 维的向量点积或其他相关度函数),n 通常要小于 d 。

  • 计算并行度高

RNN 中当前时间步的计算要依赖前一个时间步的计算结果;Self-Attention 中各时间步的计算只依赖输入,不依赖之前时间步输出,各时间步可以完全并行。

  • 容易学习长距离依赖(long-range dependencies)

RNN 中相距为 n 的两个位置间的关联需要 n 步才能建立;Self-Attention 中任何两个位置都直接相连;路径越短信号传播越容易。Transformer 结构已被广泛应用在 Bert 等语义表示模型中,取得了显著效果。


快速实践

本示例展示了以Transformer为代表的预训练模型如何Finetune完成机器翻译任务。

项目基于飞桨PaddleNLP编写,GitHub地址:

https://github.com/PaddlePaddle/PaddleNLP


PaddleNLP官方文档:

https://paddlenlp.readthedocs.io

完整代码请戳:

https://github.com/PaddlePaddle/PaddleNLP/tree/develop/examples/machine_translation/transformer

深度学习任务Pipeline

图2:深度学习任务Pipeline


2.1 数据预处理

本教程使用CWMT数据集中的中文英文的数据作为训练语料, CWMT数据集包含900万+样本,质量较高,非常适合来训练Transformer机器翻译模型。


中文需要Jieba+BPE,英文需要BPE。

BPE(Byte Pair Encoding)

BPE优势:

  • 压缩词表;

  • 一定程度上缓解OOV(out of vocabulary)问题。

图3:learn BPE

图4:Apply BPE

# 自定义读取本地数据的方法
def read(src_path, tgt_path, is_predict=False):
   # 是否为测试集,测试集tgt为空
    if is_predict:
        with open(src_path, 'r', encoding='utf8') as src_f:
            for src_line in src_f.readlines():
                src_line = src_line.strip()
                if not src_line:
                    continue
                yield {'src':src_line, 'tgt':''}
    else:
        with open(src_path, 'r', encoding='utf8') as src_f, open(tgt_path, 'r', encoding='utf8') as tgt_f:
            for src_line, tgt_line in zip(src_f.readlines(), tgt_f.readlines()):
                src_line = src_line.strip()
                if not src_line:
                    continue
                tgt_line = tgt_line.strip()
                if not tgt_line:
                    continue
                yield {'src':src_line, 'tgt':tgt_line}

# 过滤掉长度 ≤min_len或者≥max_len 的数据            
def min_max_filer(data, max_len, min_len=0):
    # 获取每条src和tgt的最小长度和最大长度(+1是为了<s>或者<e>),过滤掉不满足长度范围的样本.
    data_min_len = min(len(data[0]), len(data[1])) + 1
    data_max_len = max(len(data[0]), len(data[1])) + 1
    return (data_min_len >= min_len) and (data_max_len <= max_len)
# 数据预处理过程,包括jieba分词、bpe分词和词表。
!bash preprocess.sh

2.2 构造Dataloader

我们定义create_data_loader函数,用来创建训练集、验证集所需要的DataLoader对象。


DataLoader对象用于产生一个个batch的数据。下面对函数中调用的PaddleNLP内置函数作简单说明:

  • paddlenlp.data.Vocab.load_vocabulary:Vocab词表类,集合了一系列文本token与ids之间映射的一系列方法,支持从文件、字典、json等一系方式构建词表

  • paddlenlp.datasets.load_dataset:从本地文件创建数据集时,推荐根据本地数据集的格式给出读取function并传入 load_dataset()中创建数据集

  • paddlenlp.data.Pad:padding 操作,用于对齐同一batch内的句子长度。


图6:构造Dataloader的流程

图7:Dataloader细节
# 创建训练集、验证集的dataloader。测试集的dataloader类似。
def create_data_loader(args):
    # 通过paddlenlp.datasets.load_dataset从本地文件创建数据集:根据本地数据集的格式给出读取function并传入 load_dataset()中创建数据集
    train_dataset = load_dataset(read, src_path=args.training_file.split(',')[0], tgt_path=args.training_file.split(',')[1], lazy=False)
    dev_dataset = load_dataset(read, src_path=args.training_file.split(',')[0], tgt_path=args.training_file.split(',')[1], lazy=False)
    # 通过Paddlenlp.data.Vocab.load_vocabulary从本地创建词表
    src_vocab = Vocab.load_vocabulary(
        args.src_vocab_fpath,
        bos_token=args.special_token[0],
        eos_token=args.special_token[1],
        unk_token=args.special_token[2])
    trg_vocab = Vocab.load_vocabulary(
        args.trg_vocab_fpath,
        bos_token=args.special_token[0],
        eos_token=args.special_token[1],
        unk_token=args.special_token[2])
    # 将词表的大小补足为pad_factor的倍数,为了Tranformer的加速。
    padding_vocab = (
        lambda x: (x + args.pad_factor - 1) // args.pad_factor * args.pad_factor
    )
    args.src_vocab_size = padding_vocab(len(src_vocab))
    args.trg_vocab_size = padding_vocab(len(trg_vocab))

def convert_samples(sample):
        source = sample['src'].split()
        target = sample['tgt'].split()
        # 将tokens转化为词表对应的ids
        source = src_vocab.to_indices(source)
        target = trg_vocab.to_indices(target)
        return source, target
    # 训练集dataloader和验证集dataloader
    data_loaders = []
for i, dataset in enumerate([train_dataset, dev_dataset]):
    # 通过Dataset的map方法将样本token转换为id;通过Dataset的filter方法过滤掉不符合条件的样本
        dataset = dataset.map(convert_samples, lazy=False).filter(
            partial(min_max_filer, max_len=args.max_length))
        # 批采样器BatchSampler组batch
        batch_sampler = BatchSampler(dataset,batch_size=args.batch_size, shuffle=True,drop_last=False)
      # 构造Dataloader用于后续迭代取数据进行训练/验证/测试
        data_loader = DataLoader(
            dataset=dataset,
            batch_sampler=batch_sampler,
            collate_fn=partial(
                prepare_train_input,
                bos_idx=args.bos_idx,
                eos_idx=args.eos_idx,
                pad_idx=args.bos_idx),
                num_workers=0,
                return_list=True)
        data_loaders.append(data_loader)
    return data_loaders

def prepare_train_input(insts, bos_idx, eos_idx, pad_idx):
   # 通过paddlenlp.data.Pad来padding,用于对齐同一batch中样本的长度
    word_pad = Pad(pad_idx)
    src_word = word_pad([inst[0] + [eos_idx] for inst in insts])
    trg_word = word_pad([[bos_idx] + inst[1] for inst in insts])
    # 扩展维度用于后续计算Loss
    lbl_word = np.expand_dims(
        word_pad([inst[1] + [eos_idx] for inst in insts]), axis=2)

    data_inputs = [src_word, trg_word, lbl_word]
    return data_inputs

2.3 搭建模型

PaddleNLP提供Transformer API供调用:

  • paddlenlp.transformers.TransformerModel:Transformer模型的实现

  • paddlenlp.transformers.InferTransformerModel:Transformer模型用于生成任务

  • paddlenlp.transformers.CrossEntropyCriterion:计算交叉熵损失

  • paddlenlp.transformers.position_encoding_init:Transformer 位置编码的初始化

图8:模型搭建

图9:Encoder-Decoder示意图


2.4 训练模型

运行do_train函数, 在do_train函数中,配置优化器、损失函数,以及评价指标Perplexity;

Perplexity,即困惑度,常用来衡量语言模型优劣,即句子的通顺度,一般用于机器翻译和文本生成等领域。Perplexity越小,句子越通顺,该语言模型越好。

图10:训练模型
def do_train(args):
    random_seed = eval(str(args.random_seed))
    if random_seed is not None:
        paddle.seed(random_seed)
    # 获取Dataloader
    (train_loader), (eval_loader) = create_data_loader(args)

    # 声明模型
    transformer = TransformerModel(
        src_vocab_size=args.src_vocab_size,
        trg_vocab_size=args.trg_vocab_size,
        max_length=args.max_length + 1,
        n_layer=args.n_layer,
        n_head=args.n_head,
        d_model=args.d_model,
        d_inner_hid=args.d_inner_hid,
        dropout=args.dropout,
        weight_sharing=args.weight_sharing,
        bos_id=args.bos_idx,
        eos_id=args.eos_idx)

    # 定义Loss
    criterion = CrossEntropyCriterion(args.label_smooth_eps, args.bos_idx)
   # 定义学习率的衰减策略
    scheduler = paddle.optimizer.lr.NoamDecay(
        args.d_model, args.warmup_steps, args.learning_rate, last_epoch=0)

    # 定义优化器
    optimizer = paddle.optimizer.Adam(
        learning_rate=scheduler,
        beta1=args.beta1,
        beta2=args.beta2,
        epsilon=float(args.eps),
        parameters=transformer.parameters())

    step_idx = 0

    # 按epoch迭代训练
    for pass_id in range(args.epoch):
        batch_id = 0
        for input_data in train_loader:
             # 从训练集Dataloader按batch取数据
            (src_word, trg_word, lbl_word) = input_data
            # 获得模型输出的logits 
            logits = transformer(src_word=src_word, trg_word=trg_word)
        # 计算loss
            sum_cost, avg_cost, token_num = criterion(logits, lbl_word)

            # 计算梯度
            avg_cost.backward() 
            # 更新参数
            optimizer.step() 
            # 梯度清零
            optimizer.clear_grad() 

            batch_id += 1
            step_idx += 1
            scheduler.step()

do_train(args)
[2021-06-18 22:38:55,597] [    INFO] - step_idx: 0, epoch: 0, batch: 0, avg loss: 10.513082,  ppl: 36793.687500 
[2021-06-18 22:38:56,783] [    INFO] - step_idx: 9, epoch: 0, batch: 9, avg loss: 10.506249,  ppl: 36543.164062 
[2021-06-18 22:38:58,032] [    INFO] - step_idx: 19, epoch: 0, batch: 19, avg loss: 10.464736,  ppl: 35057.187500 
[2021-06-18 22:38:59,032] [    INFO] - validation, step_idx: 19, avg loss: 10.454649,  ppl: 34705.347656


2.5 预测和评估

模型最终训练的效果一般可通过测试集来进行测试,机器翻译领域一般计算BLEU值。

预测结果中每行输出是对应行输入的得分最高的翻译,对于使用 BPE 的数据,预测出的翻译结果也将是 BPE 表示的数据,要还原成原始的数据(这里指 tokenize 后的数据)才能进行正确的评估。

图11:预测和评估


动手试一试

是不是觉得很有趣呀。小编强烈建议初学者参考上面的代码亲手敲一遍,因为只有这样,才能加深你对代码的理解呦。

本次项目对应的代码:

https://aistudio.baidu.com/aistudio/projectdetail/1918692

来定制自己的翻译系统吧。

更多PaddleNLP信息,欢迎访问GitHub点star收藏后体验:

https://github.com/PaddlePaddle/PaddleNLP


加入交流群,一起学习吧

如果你在学习过程中遇到任何问题或疑问,欢迎加入PaddleNLP的QQ技术交流群!

回顾往期

越学越有趣:『手把手带你学NLP』系列项目01 ——词向量应用的那些事儿

越学越有趣:『手把手带你学NLP』系列项目02 ——语义相似度计算的那些事儿

越学越有趣:『手把手带你学NLP』系列项目03 ——快递单信息抽取的那些事儿

越学越有趣:『手把手带你学NLP』系列项目04 ——实体关系抽取的那些事儿

越学越有趣:『手把手带你学NLP』系列项目05 ——文本情感分析的那些事儿

越学越有趣:『手把手带你学NLP』系列项目06 ——机器阅读理解的那些事儿

飞桨(PaddlePaddle)以百度多年的深度学习技术研究和业务应用为基础,集深度学习核心训练和推理框架、基础模型库、端到端开发套件和丰富的工具组件于一体,是中国首个开源最早、技术领先的产业级深度学习平台。飞桨企业版针对企业级需求增强了相应特性,包含零门槛AI开发平台EasyDL和全功能AI开发平台BML。EasyDL主要面向中小企业,提供零门槛、预置丰富网络和模型、便捷高效的开发平台;BML是为大型企业提供的功能全面、可灵活定制和被深度集成的开发平台。

END

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值