零基础也可以实现“机器同传翻译”!

同传翻译的“前世今生”

同声传译,简称“同传”,是指译员在不打断讲话者讲话的情况下,不间断地将内容口译给听众的一种翻译方式,同声传译员通过专用的设备提供即时的翻译,这种方式适用于大型的研讨会和国际会议,同声传译效率高,能保证演讲或会议的流畅进行。

零基础也可以实现“机器同传翻译”!_第1张图片

同声传译员一般收入较高,但是成为同声传译的门槛也很高。当前,世界上95%的国际高端会议都采用同声传译的方式。第二次世界大战结束后,设立在德国的纽伦堡国际军事法庭在审判法西斯战犯时,首次采用同声传译,这也是世界上第一次在大型国际活动中采用同声传译。

不过目前人工同传翻译存在着以下局限之处:

  • 精力体力的挑战:与交替传译不同的是,同传需要边听、边记、边翻,同步进行,对译员的要求极高。由于需要高度集中注意力,人类同传一般两人一组,且每隔20多分钟就要换人休息,对人的精力、体力都是极大的挑战。
  • 译出率不高:据统计,同传译员的译出率一般在60%-70%左右。译出率不高的原因,一般由于未听清或者难翻译,人类译员通常会选择性的忽略某些句子,保证总体上的准确率和实时性。
  • 全球同传译员稀缺:由于苛刻的要求,全球同传译员稀缺,只有几千人。与巨大的市场需求相比,人才严重短缺。

相比之下机器同声传译的优势有:机器最大的优势是不会因为疲倦而导致译出率下降,能将所有“听到”的句子全部翻译出来,这使得机器的“译出率”可以达到100%,远高于人类译员的60%-70%。同时,在价格上也占有优势。

本期项目我们PaddleNLP团队为大家带来一个机器同传翻译demo,它的翻译效果如何呢?让我们先睹为快吧!

零基础也可以实现“机器同传翻译”!_第2张图片

语音同传Demo

零基础也可以实现“机器同传翻译”!_第3张图片

文本同传Demo

是不是看起来效果很不错!或许大家会问,实现起来复杂吗?这里小编隆重给大家推荐一个好用的工具——PaddleNLP!即使是零基础课程学员,通过PaddleNLP,只需经过简单的一些操作也能够轻松将它实现,如果你也感兴趣,那就赶快来试试吧!

机器同传demo教程:
https://github.com/PaddlePaddle/PaddleNLP/blob/develop/education/day09.md

零基础也可以实现“机器同传翻译”!_第4张图片

本项目是基于机器翻译领域主流模型 Transformer网络结构的同传模型STACL的PaddlePaddle 实现,包含模型训练,预测以及使用自定义数据等内容。用户可以基于发布的内容搭建自己的同传翻译模型。

《STACL: Simultaneous Translation with Implicit Anticipation and Controllable Latency using Prefix-to-Prefix Framework》 提出适用于同传场景的翻译架构,该架构基于Transformer实现。

STACL 主要具有以下优势:

  • Implicit Anticipation(隐式的预测能力):
    Prefix-to-Prefix架构拥有预测能力,即在未看到源词的情况下仍然可以翻译出对应的目标词,克服了SOV→SVO等词序差异;

零基础也可以实现“机器同传翻译”!_第5张图片

图1:Implicit Anticipation

  • Controllable Latency(可控的延迟):
    Wait-k策略可以不需要全句的源句,直接预测目标句,可以实现任意的字级延迟,同时保持较高的翻译质量。

零基础也可以实现“机器同传翻译”!_第6张图片

图2:Controllable Latency (Wait-k)

Wait-k策略首先等待源端读入k个词后开始进行翻译。上图2中,k=1,第一个目标词在读入第一个1个源词后翻译,第二个目标词在读入前2个源词后翻译,以此类推,所以当源端读入“他 还 说”3个词后,目标端就已经翻译出“he also said”。当k=3,第一个目标词在读入前3个源词后翻译,所以当源端读入“他 还 说”后,目标端翻译出第一个词”he“。

快速实践

本项目基于飞桨PaddleNLP完成,记得给PaddleNLP点个小小的Star⭐
开源不易,希望大家多多支持~

GitHub地址:
https://github.com/PaddlePaddle/PaddleNLP

PaddleNLP文档:
https://paddlenlp.readthedocs.io

完整代码请戳:
https://github.com/PaddlePaddle/PaddleNLP/tree/develop/examples/simultaneous_translation/stacl

深度学习任务Pipeline

在这里插入图片描述

图3:深度学习任务Pipeline

2.1 数据预处理

本项目展示的训练数据为NIST的中英demo数据(1000条中英文本对),同时提供基于全量NIST中英数据训练的预训练模型下载。

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

BPE(Byte Pair Encoding)

BPE优势:

  • 压缩词表;
  • 一定程度上缓解OOV(out of vocabulary)问题

零基础也可以实现“机器同传翻译”!_第7张图片

图4:learn BPE

零基础也可以实现“机器同传翻译”!_第8张图片

图5:Apply BPE

零基础也可以实现“机器同传翻译”!_第9张图片

图6:Jieba+BPE

数据格式

兵营 是 双@@ 枪 老@@ 大@@ 爷 的 前提 建筑 之一 。it serves as a prerequisite for Re@@ apers to be built at the Bar@@ rac@@ ks .

2.2 构造Dataloader

构造DataLoader过程,与上一篇项目类似:越学越有趣:『手把手带你学NLP』系列项目07 ——机器翻译的那些事儿。

同样使用paddlenlp.data和paddle.io.DataLoader进行数据处理和Dataloder的构造。

零基础也可以实现“机器同传翻译”!_第10张图片

图7:构造Dataloader的流程

零基础也可以实现“机器同传翻译”!_第11张图片

图8:Dataloader细节

2.3 搭建模型

基于飞桨框架API,包括:

  • paddle.nn.TransformerEncoderLayer:Transformer编码器层
  • paddle.nn.TransformerEncoder:Transformer编码器
  • paddle.nn.TransformerDecoderLayer:Transformer解码器层
  • paddle.nn.TransformerDecoder:Transformer解码器

零基础也可以实现“机器同传翻译”!_第12张图片

图9:模型搭建

Encoder层
采用Transformer的编码结构。

Decoder层
基于paddle.nn.TransformerDecoderLayer加入Wait-k策略。

模型主结构
与Transformer基本一致,具体细节可参考:
paddlenlp.transformers.TransformerModel
SimultaneousTransformer:Encoder+Decoder(wait-k 策略)

零基础也可以实现“机器同传翻译”!_第13张图片
零基础也可以实现“机器同传翻译”!_第14张图片
零基础也可以实现“机器同传翻译”!_第15张图片
零基础也可以实现“机器同传翻译”!_第16张图片
零基础也可以实现“机器同传翻译”!_第17张图片
图10:wait-k策略示例

# 定义SimultaneousTransformer,这里给出和nn.TransformerDecoderLayer不一致地方的注释
class SimultaneousTransformer(nn.Layer):
    def __init__(self,
                 src_vocab_size,
                 trg_vocab_size,
                 max_length,
                 n_layer,
                 n_head,
                 d_model,
                 d_inner_hid,
                 dropout,
                 weight_sharing,
                 bos_id=0,
                 eos_id=1,
                 waitk=-1):
        super(SimultaneousTransformer, self).__init__()
        self.trg_vocab_size = trg_vocab_size
        self.emb_dim = d_model
        self.bos_id = bos_id
        self.eos_id = eos_id
        self.dropout = dropout
        self.waitk = waitk
        self.n_layer = n_layer
        self.n_head = n_head
        self.d_model = d_model

        # 声明WordEmbedding
        self.src_word_embedding = WordEmbedding(
            vocab_size=src_vocab_size, emb_dim=d_model, bos_id=self.bos_id)

        # 声明PositionalEmbedding
        self.src_pos_embedding = PositionalEmbedding(
            emb_dim=d_model, max_length=max_length)

        # 判断target是否要和source共享WordEmbedding
        if weight_sharing:
            assert src_vocab_size == trg_vocab_size, (
                "Vocabularies in source and target should be same for weight sharing."
            )
            self.trg_word_embedding = self.src_word_embedding
            self.trg_pos_embedding = self.src_pos_embedding
        else:
            self.trg_word_embedding = WordEmbedding(
                vocab_size=trg_vocab_size, emb_dim=d_model, bos_id=self.bos_id)
            self.trg_pos_embedding = PositionalEmbedding(
                emb_dim=d_model, max_length=max_length)

        # 声明Encoder层
        encoder_layer = nn.TransformerEncoderLayer(
            d_model=d_model,
            nhead=n_head,
            dim_feedforward=d_inner_hid,
            dropout=dropout,
            activation='relu',
            normalize_before=True,
            bias_attr=[False, True])
        encoder_norm = nn.LayerNorm(d_model)
        # 声明Encoder
        self.encoder = nn.TransformerEncoder(
            encoder_layer=encoder_layer, num_layers=n_layer, norm=encoder_norm)

        # 声明Decoder层
        decoder_layer = DecoderLayer(
            d_model=d_model,
            nhead=n_head,
            dim_feedforward=d_inner_hid,
            dropout=dropout,
            activation='relu',
            normalize_before=True,
            bias_attr=[False, False, True])
        decoder_norm = nn.LayerNorm(d_model)
        # 声明Decoder
        self.decoder = Decoder(
            decoder_layer=decoder_layer, num_layers=n_layer, norm=decoder_norm)

        if weight_sharing:
            self.linear = lambda x: paddle.matmul(
                x=x, y=self.trg_word_embedding.word_embedding.weight, transpose_y=True)
        else:
            self.linear = nn.Linear(
                in_features=d_model,
                out_features=trg_vocab_size,
                bias_attr=False)

    def forward(self, src_word, trg_word):
        src_max_len = paddle.shape(src_word)[-1]
        trg_max_len = paddle.shape(trg_word)[-1]
        base_attn_bias = paddle.cast(
            src_word == self.bos_id,
            dtype=paddle.get_default_dtype()).unsqueeze([1, 2]) * -1e9
        # 计算source端的attention mask
        src_slf_attn_bias = base_attn_bias
        src_slf_attn_bias.stop_gradient = True
        # 计算target端的attention mask
        trg_slf_attn_bias = paddle.tensor.triu(
            (paddle.ones(
                (trg_max_len, trg_max_len),
                dtype=paddle.get_default_dtype()) * -np.inf),
            1)
        trg_slf_attn_bias.stop_gradient = True
        # 计算encoder-decoder的attention mask
        trg_src_attn_bias = paddle.tile(base_attn_bias, [1, 1, trg_max_len, 1])
        src_pos = paddle.cast(
            src_word != self.bos_id, dtype="int64") * paddle.arange(
                start=0, end=src_max_len)
        trg_pos = paddle.cast(
            trg_word != self.bos_id, dtype="int64") * paddle.arange(
                start=0, end=trg_max_len)
        # 计算source的word embedding
        src_emb = self.src_word_embedding(src_word)
        # 计算source的position embedding
        src_pos_emb = self.src_pos_embedding(src_pos)
        # 得到最终Embedding:word embedding + position embedding
        src_emb = src_emb + src_pos_emb
        enc_input = F.dropout(
            src_emb, p=self.dropout,
            training=self.training) if self.dropout else src_emb
        with paddle.static.amp.fp16_guard():
            # 下面是添加了waitk策略的部分
            if self.waitk >= src_max_len or self.waitk == -1:
                # 整句模型,和API一致
                enc_outputs = [
                    self.encoder(
                        enc_input, src_mask=src_slf_attn_bias)
                ]
            else:
                # Wait-k策略
                enc_outputs = []
                for i in range(self.waitk, src_max_len + 1):
                    # 分别将子句送入encoder
                    enc_output = self.encoder(
                        enc_input[:, :i, :],
                        src_mask=src_slf_attn_bias[:, :, :, :i])
                    enc_outputs.append(enc_output)
            # 计算target的word embedding
            trg_emb = self.trg_word_embedding(trg_word)
            # 计算target的position embedding
            trg_pos_emb = self.trg_pos_embedding(trg_pos)
            # 得到最终Embedding:word embedding + position embedding
            trg_emb = trg_emb + trg_pos_emb
            dec_input = F.dropout(
                trg_emb, p=self.dropout,
                training=self.training) if self.dropout else trg_emb
            # 送入Decoder,拿到输出
            dec_output = self.decoder(
                dec_input,
                enc_outputs,
                tgt_mask=trg_slf_attn_bias,
                memory_mask=trg_src_attn_bias)
            # 经过全连接层拿到最终输出
            predict = self.linear(dec_output)

        return predict

2.4 训练模型

配置优化器、损失函数,以及评价指标(Perplexity,即困惑度,常用来衡量语言模型优劣,也可用于机器翻译、文本生成等任务)。

零基础也可以实现“机器同传翻译”!_第18张图片
图11:训练模型

def do_train(args):
    # 设置在GPU/CPU/XPU上运行
    paddle.set_device(args.device)

    # 设置随机种子
    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, places=paddle.get_device())

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


    print('waitk=', args.waitk)

    # 定义Loss
    criterion = CrossEntropyCriterion(args.label_smooth_eps, args.bos_idx)

    # 定义学习率的衰减策略
    scheduler = paddle.optimizer.lr.NoamDecay(args.d_model, args.warmup_steps,
                                              args.learning_rate)
    # 定义优化器
    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() 

            if (step_idx + 1) % args.print_step == 0 or step_idx == 0:
                total_avg_cost = avg_cost.numpy()
                # 打印log
                logger.info(
                    "step_idx: %d, epoch: %d, batch: %d, avg loss: %f, "
                    " ppl: %f " %
                    (step_idx, pass_id, batch_id, total_avg_cost,
                        np.exp([min(total_avg_cost, 100)])))

            if (step_idx + 1) % args.save_step == 0:
                # 验证
                transformer.eval()
                total_sum_cost = 0
                total_token_num = 0
                with paddle.no_grad():
                    for input_data in eval_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)
                        total_sum_cost += sum_cost.numpy()
                        total_token_num += token_num.numpy()
                        total_avg_cost = total_sum_cost / total_token_num
[2021-06-17 22:03:51,772] [    INFO] - step_idx: 0, epoch: 0, batch: 0, avg loss: 9.260654,  ppl: 10516.013672 
[2021-06-17 22:04:14,491] [    INFO] - step_idx: 9, epoch: 0, batch: 9, avg loss: 9.239330,  ppl: 10294.142578 
[2021-06-17 22:04:40,302] [    INFO] - step_idx: 19, epoch: 0, batch: 19, avg loss: 9.196883,  ppl: 9866.330078 
[2021-06-17 22:04:53,412] [    INFO] - validation, step_idx: 19, avg loss: 9.171905,  ppl: 9622.934570

2.5 预测和评估

模型最终训练的效果一般可通过测试集来进行测试,同传类似机器翻译场景,一般计算BLEU值。

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

零基础也可以实现“机器同传翻译”!_第19张图片

图12:预测和评估

动手试一试

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

本次项目对应的代码:
https://aistudio.baidu.com/aistudio/projectdetail/1926754

更多PaddleNLP信息,欢迎访问GitHub点star收藏后体验:
https://github.com/PaddlePaddle/PaddleNLP

推荐阅读
全新的安全漏洞扫描解决方案,百度MTC让APP安全防护能力更强

你可能感兴趣的:(飞桨PaddlePaddle,人工智能,深度学习,技术实践,开源项目,飞浆)