机器学习深度学习——transformer(机器翻译的再实现)

👨‍🎓作者简介:一位即将上大四,正专攻机器学习的保研er
🌌上期文章:机器学习&&深度学习——自注意力和位置编码(数学推导+代码实现)
📚订阅专栏:机器学习&&深度学习
希望文章对你们有所帮助

transformer(机器翻译的再实现)

  • 引入
  • 模型
  • 基于位置的前馈网络
  • 残差连接和层规范化
    • 为何使用层规范化
  • 编码器
  • 解码器
  • 训练
  • 小结

引入

上一节比较了CNN、RNN和self-attention。容易知道,自注意力同时具有并行计算力强最大路径长度的两个有点,因此使用self-attention来设计模型架构是很有吸引力的。
而transformer模型就是完全基于注意力机制,且其没有任何的卷积层和循环神经网络层。transformer最初是应用在文本数据上的序列到序列学习,现在也已经推广到了语言、视觉、语音和强化学习等领域。
而在之前,我们已经使用了seq2seq来实现了英语到法语的翻译,那时候发现训练后的效果并不是很好,接下来将边讲解transformer边用transformer来进行机器翻译。

模型

transformer是编码器-解码器架构的一个实例,整体实例的架构图如下所示:
在这里插入图片描述
可以看出transformer的编码器和解码器时基于自注意力的模块叠加而成的,源(输入)序列和目标(输出)序列的嵌入(embedding)表示将加上位置编码,再分别输入到编码器和解码器中。
宏观角度上进行结构上的解释,transformer的编码器是由多个相同的层叠加而成的,每个层都有两个子层(sublayer)。第一个子层是多头注意力(multi-head self-attention)池化;第二个子层是基于位置的前馈网络(positionwise feed-forward network(FFT))。也就是说,计算编码器的自注意力时,查询、键和值都来自前一个编码器层的输出。
受残差网络ResNet的启发(具体可以自行去了解一下原理,主要优点是残差网络的使用使得模型更容易优化),每个子层都使用了残差连接。在transformer中,对于序列中任何位置的任何输入x∈Rd,都要求满足sublayer(x)∈Rd,一遍残差连接满足x+sublayer(x)∈Rd。在残差连接的加法运算后,要紧接着进行应用层规范化(layer normalization)。因此,输入序列对应的每个位置,编码器都将输出一个d维表示向量。
transformer的解码器也是由多个相同的层叠加而成的,也使用了残差连接和层规范化。除了编码器中描述的两个子层之外,解码器还在这两个子层之间插入了第三个子层,称为编码器-解码器注意力层。这个层的query为前一解码器层的输出,而key和value都是来自编码器的输出。在解码器自注意力中,query、key、value都来自上一个解码器层的输出。但是解码器中的每个位置只能考虑该位置之前的所有位置,这和我们之前见过的掩蔽很相似,事实上这种掩蔽注意力就保留了自回归属性,确保预测仅仅依赖于已生成的输出词元。

import math
import pandas as pd
import torch
from torch import nn
from d2l import torch as d2l

基于位置的前馈网络

基于位置的前馈网络对序列中的所有位置的表示进行变换时使用的是同一个多层感知机(MLP),这就是称前馈网络是基于位置的原因。下面进行实现,下面代码中,输入X的形状为(批量大小,时间步数或序列长度,隐单元数或特征维度)将被一个两层感知机转换成形状为(批量大小,时间步数,ffn_num_outputs)的输出张量。

#@save
class PositionWiseFFN(nn.Module):
    """基于位置的前馈网络"""
    def __init__(self, ffn_num_input, ffn_num_hiddens, ffn_num_outputs,
                 **kwargs):
        super(PositionWiseFFN, self).__init__(**kwargs)
        self.dense1 = nn.Linear(ffn_num_input, ffn_num_hiddens)
        self.relu = nn.ReLU()
        self.dense2 = nn.Linear(ffn_num_hiddens, ffn_num_outputs)

    def forward(self, X):
        return self.dense2(self.relu(self.dense1(X)))

残差连接和层规范化

现在关注加法和规范化(add&norm)组件,如前面所说,这是由残差连接和紧随其后的层规范化组成的,这两个都是关键。
规范化也叫归一化,他将其改变成均值为0方差为1。
在计算机视觉中,常用批量规范化,也就是在一个小批量的样本内基于批量规范化对数据进行重新中心化和重新缩放的调整。层规范化是基于特征维度进行规范化。而在NLP(输入通常是变长序列)中通常使用层规范化。

为何使用层规范化

也就是为啥使用LayerNorm而不使用BatchNorm呢?如下图:
在这里插入图片描述
其中,b为batch_size也就是批量大小,len是指代序列的长度。
那么我们知道,批量规范化是对每个特征/通道里的元素进行归一化,把d规范化了,len留着,但是对于序列长度会变的NLP并不适合,这样就会不稳定。
而层规范化则是对每个样本里面的元素进行规范化,相对更稳定。
通过以下的代码来对比不同维度的层规范化和批量规范化的效果:

ln = nn.LayerNorm(2)
bn = nn.BatchNorm1d(2)
X = torch.tensor([[1, 2], [2, 3]], dtype=torch.float32)
# 在训练模式下计算X的均值和方差
print('layer norm:', ln(X), '\nbatch norm:', bn(X))

运行结果:
在这里插入图片描述
可以很清楚看出来,使用层规范化以后,会对每一个批量中的每一个样本的不同特征来进行规范化;使用批量规范化以后,会对每一个批量中的每个样本的同一特征来进行规范化。
接着就可以使用残差连接和层规范化来实现AddNorm类,dropout也被作为正则化方法使用:

#@save
class AddNorm(nn.Module):
    """残差连接后进行层规范化"""
    def __init__(self, normalized_shape, dropout, **kwargs):
        super(AddNorm, self).__init__(**kwargs)
        self.dropout = nn.Dropout(dropout)
        self.ln = nn.LayerNorm(normalized_shape)

    def forward(self, X, Y):
        return self.ln(self.dropout(Y) + X)

残差连接要求两个输入的形状相同,以便加法操作后输出张量的形状相同。

add_norm = AddNorm([3, 4], 0.5)
add_norm.eval()
print(add_norm(torch.ones((2, 3, 4)), torch.ones((2, 3, 4))).shape)

输出结果:

torch.Size([2, 3, 4])

编码器

有了组成Transformer编码器的基础组件,现在可以先实现编码器中的一个层。下面的EncoderBlock类包含两个子层:多头自注意力和基于位置的前馈网络,这两个子层都使用了残差连接和紧随的层规范化。

#@save
class EncoderBlock(nn.Module):
    """Transformer编码器块"""
    def __init__(self, key_size, query_size, value_size, num_hiddens,
                 norm_shape, ffn_num_input, ffn_num_hiddens, num_heads,
                 dropout, use_bias=False, **kwargs):
        super(EncoderBlock, self).__init__(**kwargs)
        self.attention = d2l.MultiHeadAttention(
            key_size, query_size, value_size, num_hiddens, num_heads, dropout,
            use_bias)
        self.addnorm1 = AddNorm(norm_shape, dropout)
        self.ffn = PositionWiseFFN(
            ffn_num_input, ffn_num_hiddens, num_hiddens)
        self.addnorm2 = AddNorm(norm_shape, dropout)

    def forward(self, X, valid_lens):
        Y = self.addnorm1(X, self.attention(X, X, X, valid_lens))
        return self.addnorm2(Y, self.ffn(Y))

下面实现的Transformer编码器的代码中,堆叠了num_layers个EncoderBlock类的实例。由于这里使用的是值范围在-1和1之间的固定位置编码,因此通过学习得到的输入的嵌入表示的值需要先乘以嵌入维度的平方根进行重新缩放,然后再与位置编码相加。

#@save
class TransformerEncoder(d2l.Encoder):
    """Transformer编码器"""
    def __init__(self, vocab_size, key_size, query_size, value_size,
                 num_hiddens, norm_shape, ffn_num_input, ffn_num_hiddens,
                 num_heads, num_layers, dropout, use_bias=False, **kwargs):
        super(TransformerEncoder, self).__init__(**kwargs)
        self.num_hiddens = num_hiddens
        self.embedding = nn.Embedding(vocab_size, num_hiddens)
        self.pos_encoding = d2l.PositionalEncoding(num_hiddens, dropout)
        self.blks = nn.Sequential()
        for i in range(num_layers):
            self.blks.add_module("block"+str(i),
                EncoderBlock(key_size, query_size, value_size, num_hiddens,
                             norm_shape, ffn_num_input, ffn_num_hiddens,
                             num_heads, dropout, use_bias))

    def forward(self, X, valid_lens, *args):
        # 因为位置编码值在-1和1之间,
        # 因此嵌入值乘以嵌入维度的平方根进行缩放,
        # 然后再与位置编码相加。
        X = self.pos_encoding(self.embedding(X) * math.sqrt(self.num_hiddens))
        self.attention_weights = [None] * len(self.blks)
        for i, blk in enumerate(self.blks):
            X = blk(X, valid_lens)
            self.attention_weights[
                i] = blk.attention.attention.attention_weights
        return X

解码器

Transformer解码器也是由多个相同的层组成,在DecoderBlock类中实现的每个层包含了三个子层:解码器自注意力、“编码器-解码器”注意力和基于位置的前馈网络。这些子层也都被残差连接和紧随的层规范化围绕。
在掩蔽多头解码器自注意力层(第一个子层)中,查询、键和值都来自上一个解码器层的输出。关于序列到序列模型(sequence-to-sequence model),在训练阶段,其输出序列的所有位置(时间步)的词元都是已知的;然而,在预测阶段,其输出序列的词元是逐个生成的。
因此,在任何解码器时间步中,只有生成的词元才能用于解码器的自注意力计算中。为了在解码器中保留自回归的属性,其掩蔽自注意力设定了参数dec_valid_lens,以便任何查询都只会与解码器中所有已经生成词元的位置(即直到该查询位置为止)进行注意力计算。

class DecoderBlock(nn.Module):
    """解码器中第i个块"""
    def __init__(self, key_size, query_size, value_size, num_hiddens,
                 norm_shape, ffn_num_input, ffn_num_hiddens, num_heads,
                 dropout, i, **kwargs):
        super(DecoderBlock, self).__init__(**kwargs)
        self.i = i
        self.attention1 = d2l.MultiHeadAttention(
            key_size, query_size, value_size, num_hiddens, num_heads, dropout)
        self.addnorm1 = AddNorm(norm_shape, dropout)
        self.attention2 = d2l.MultiHeadAttention(
            key_size, query_size, value_size, num_hiddens, num_heads, dropout)
        self.addnorm2 = AddNorm(norm_shape, dropout)
        self.ffn = PositionWiseFFN(ffn_num_input, ffn_num_hiddens,
                                   num_hiddens)
        self.addnorm3 = AddNorm(norm_shape, dropout)

    def forward(self, X, state):
        enc_outputs, enc_valid_lens = state[0], state[1]
        # 训练阶段,输出序列的所有词元都在同一时间处理,
        # 因此state[2][self.i]初始化为None。
        # 预测阶段,输出序列是通过词元一个接着一个解码的,
        # 因此state[2][self.i]包含着直到当前时间步第i个块解码的输出表示
        if state[2][self.i] is None:
            key_values = X
        else:
            key_values = torch.cat((state[2][self.i], X), axis=1)
        state[2][self.i] = key_values
        if self.training:
            batch_size, num_steps, _ = X.shape
            # dec_valid_lens的开头:(batch_size,num_steps),
            # 其中每一行是[1,2,...,num_steps]
            dec_valid_lens = torch.arange(
                1, num_steps + 1, device=X.device).repeat(batch_size, 1)
        else:
            dec_valid_lens = None

        # 自注意力
        X2 = self.attention1(X, key_values, key_values, dec_valid_lens)
        Y = self.addnorm1(X, X2)
        # 编码器-解码器注意力。
        # enc_outputs的开头:(batch_size,num_steps,num_hiddens)
        Y2 = self.attention2(Y, enc_outputs, enc_outputs, enc_valid_lens)
        Z = self.addnorm2(Y, Y2)
        return self.addnorm3(Z, self.ffn(Z)), state

现在我们构建了由num_layers个DecoderBlock实例组成的完整的Transformer解码器。最后,通过一个全连接层计算所有vocab_size个可能的输出词元的预测值。解码器的自注意力权重和编码器解码器注意力权重都被存储下来,方便日后可视化的需要。

class TransformerDecoder(d2l.AttentionDecoder):
    def __init__(self, vocab_size, key_size, query_size, value_size,
                 num_hiddens, norm_shape, ffn_num_input, ffn_num_hiddens,
                 num_heads, num_layers, dropout, **kwargs):
        super(TransformerDecoder, self).__init__(**kwargs)
        self.num_hiddens = num_hiddens
        self.num_layers = num_layers
        self.embedding = nn.Embedding(vocab_size, num_hiddens)
        self.pos_encoding = d2l.PositionalEncoding(num_hiddens, dropout)
        self.blks = nn.Sequential()
        for i in range(num_layers):
            self.blks.add_module("block"+str(i),
                DecoderBlock(key_size, query_size, value_size, num_hiddens,
                             norm_shape, ffn_num_input, ffn_num_hiddens,
                             num_heads, dropout, i))
        self.dense = nn.Linear(num_hiddens, vocab_size)

    def init_state(self, enc_outputs, enc_valid_lens, *args):
        return [enc_outputs, enc_valid_lens, [None] * self.num_layers]

    def forward(self, X, state):
        X = self.pos_encoding(self.embedding(X) * math.sqrt(self.num_hiddens))
        self._attention_weights = [[None] * len(self.blks) for _ in range (2)]
        for i, blk in enumerate(self.blks):
            X, state = blk(X, state)
            # 解码器自注意力权重
            self._attention_weights[0][
                i] = blk.attention1.attention.attention_weights
            # “编码器-解码器”自注意力权重
            self._attention_weights[1][
                i] = blk.attention2.attention.attention_weights
        return self.dense(X), state

    @property
    def attention_weights(self):
        return self._attention_weights

训练

依照Transformer架构来实例化编码器-解码器模型。在这里,指定Transformer的编码器和解码器都是2层,都使用4头注意力。为了进行序列到序列的学习,下面将在之前所说的机器翻译数据集上进行transformer模型的训练。

num_hiddens, num_layers, dropout, batch_size, num_steps = 32, 2, 0.1, 64, 10
lr, num_epochs, device = 0.005, 200, d2l.try_gpu()
ffn_num_input, ffn_num_hiddens, num_heads = 32, 64, 4
key_size, query_size, value_size = 32, 32, 32
norm_shape = [32]

train_iter, src_vocab, tgt_vocab = d2l.load_data_nmt(batch_size, num_steps)

encoder = TransformerEncoder(
    len(src_vocab), key_size, query_size, value_size, num_hiddens,
    norm_shape, ffn_num_input, ffn_num_hiddens, num_heads,
    num_layers, dropout)
decoder = TransformerDecoder(
    len(tgt_vocab), key_size, query_size, value_size, num_hiddens,
    norm_shape, ffn_num_input, ffn_num_hiddens, num_heads,
    num_layers, dropout)
net = d2l.EncoderDecoder(encoder, decoder)
d2l.train_seq2seq(net, train_iter, lr, num_epochs, tgt_vocab, device)
d2l.plt.show()

运行输出:

loss 0.032, 5786.9 tokens/sec on cpu

运行图片:
在这里插入图片描述

训练完后,我们可以将一些英语句子翻译成法语,并且计算翻译后的BLEU分数(和之前的seq2seq实现的训练模型一样的计算方式):

engs = ['go .', "i lost .", 'he\'s calm .', 'i\'m home .']
fras = ['va !', 'j\'ai perdu .', 'il est calme .', 'je suis chez moi .']
for eng, fra in zip(engs, fras):
    translation, dec_attention_weight_seq = d2l.predict_seq2seq(
        net, eng, src_vocab, tgt_vocab, num_steps, device, True)
    print(f'{eng} => {translation}, ',
          f'bleu {d2l.bleu(translation, fra, k=2):.3f}')

运行结果:

go . => va !, bleu 1.000
i lost . => j’ai perdu ., bleu 1.000
he’s calm . => il est mouillé ., bleu 0.658
i’m home . => je suis chez moi ., bleu 1.000

这次的效果比起上一次明显还是好了很多的。

小结

1、Transformer是编码器-解码器架构的一个实践,尽管在实际情况中编码器或解码器可以单独使用。
2、在Transformer中,多头自注意力用于表示输入序列和输出序列,不过解码器必须通过掩蔽机制来保留自回归属性。
3、Transformer中的残差连接和层规范化是训练非常深度模型的重要工具。
4、Transformer模型中基于位置的前馈网络使用同一个多层感知机,作用是对所有序列位置的表示进行转换。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/82920.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Flink 数据集成服务在小红书的降本增效实践

摘要:本文整理自实时引擎研发工程师袁奎,在 Flink Forward Asia 2022 数据集成专场的分享。本篇内容主要分为四个部分: 小红书实时服务降本增效背景Flink 与在离线混部实践实践过程中遇到的问题及解决方案未来展望 点击查看原文视频 & 演…

[oneAPI] 手写数字识别-LSTM

[oneAPI] 手写数字识别-LSTM 手写数字识别参数与包加载数据模型训练过程结果 oneAPI 比赛:https://marketing.csdn.net/p/f3e44fbfe46c465f4d9d6c23e38e0517 Intel DevCloud for oneAPI:https://devcloud.intel.com/oneapi/get_started/aiAnalyticsToolk…

八种架构演进

日升时奋斗,日落时自省 目录 1、单机架构 2、应用数据分离架构 3、应用服务集群架构 4、读写分离/主从分离架构 5、冷热分离架构 6、垂直分库架构 7、微服务架构 8、容器编排架构 9、小结 1、单机架构 特征:应用服务和数据库服务器公用一台服务…

LVS负载均衡群-DR模式

目录 1、lvs-dr数据包流向分析 2、lvs-dr中ARP问题 3、lvs-dr特性 4、lvs-dr集群-构建 1、lvs-dr数据包流向分析 (1)客户端发送请求到 Director Server(负载均衡器),请求的数据报文(源 IP 是 CIP,目标 IP …

【校招VIP】CSS校招考点之选择器优先级

考点介绍: 选择器是CSS的基础,也是校招中的高频考点,特别是复合选择器的执行优先级,同时也是实战中样式不生效的跟踪依据。 因为选择器的种类较多,很难直接记忆,可以考虑选择一个相对值,比如id类…

回归预测 | MATLAB实现BO-SVM贝叶斯优化支持向量机多输入单输出回归预测(多指标,多图)

回归预测 | MATLAB实现BO-SVM贝叶斯优化支持向量机多输入单输出回归预测(多指标,多图) 目录 回归预测 | MATLAB实现BO-SVM贝叶斯优化支持向量机多输入单输出回归预测(多指标,多图)效果一览基本介绍程序设计…

Java自学到什么程度就可以去找工作了?

引言 Java作为一门广泛应用于软件开发领域的编程语言,对于初学者来说,了解到什么程度才能开始寻找实习和入职机会是一个常见的问题。 本文将从实习和入职这两个方面,分点详细介绍Java学习到什么程度才能够开始进入职场。并在文章末尾给大家安…

k8s集群部署vmalert和prometheusalert实现钉钉告警

先决条件 安装以下软件包:git, kubectl, helm, helm-docs,请参阅本教程。 1、安装 helm wget https://xxx-xx.oss-cn-xxx.aliyuncs.com/helm-v3.8.1-linux-amd64.tar.gz tar xvzf helm-v3.8.1-linux-amd64.tar.gz mv linux-amd64/helm /usr/local/bin…

网络编程(TCP和UDP的基础模型)

一、TCP基础模型&#xff1a; tcp Ser&#xff1a; #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #include <string.h> #include <head.h>#define PORT 88…

无涯教程-Perl - wait函数

描述 该函数等待子进程终止,返回已故进程的进程ID。进程的退出状态包含在$?中。 语法 以下是此函数的简单语法- wait返回值 如果没有子进程,则此函数返回-1,否则将显示已故进程的进程ID Perl 中的 wait函数 - 无涯教程网无涯教程网提供描述该函数等待子进程终止,返回已故…

Java 项目日志实例:LogBack

点击下方关注我&#xff0c;然后右上角点击...“设为星标”&#xff0c;就能第一时间收到更新推送啦~~~ LogBack 和 Log4j 都是开源日记工具库&#xff0c;LogBack 是 Log4j 的改良版本&#xff0c;比 Log4j 拥有更多的特性&#xff0c;同时也带来很大性能提升。LogBack 官方建…

Pytorch建立MyDataLoader过程详解

简介 torch.utils.data.DataLoader(dataset, batch_size1, shuffleNone, samplerNone, batch_samplerNone, num_workers0, collate_fnNone, pin_memoryFalse, drop_lastFalse, timeout0, worker_init_fnNone, multiprocessing_contextNone, generatorNone, *, prefetch_factorN…

TensorFlow2.1 模型训练使用

文章目录 1、环境安装搭建2、神经网络2.1、解决线性问题2.2、FAshion MNIST数据集使用 3、卷积神经网络3.1、卷积神经网络使用3.2、ImageDataGenerator使用3.3、猫狗识别案例3.4、参数优化 3.5、石头剪刀布案例4、词条化4.1、讽刺数据集的词条化和序列化4.2、词嵌入 1、环境安装…

[保研/考研机试] KY11 二叉树遍历 清华大学复试上机题 C++实现

题目链接&#xff1a; 二叉树遍历_牛客题霸_牛客网编一个程序&#xff0c;读入用户输入的一串先序遍历字符串&#xff0c;根据此字符串建立一个二叉树&#xff08;以指针方式存储&#xff09;。题目来自【牛客题霸】https://www.nowcoder.com/share/jump/43719512169254700747…

实数信号的傅里叶级数研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

(2)、将SpringCache扩展功能封装为starter

(2)、将SpringCache扩展功能封装为starter 1、准备工作 前面我们写了一个common-cache模块,尽可能的将自定义的RedisConnectionFactory, RedisTemplate, RedisCacheManager等Bean封装了起来。 就是为了方便我们将其封装为一个Starter。 我们这里直接《SpringCache+Redis实…

微信个人号开发,实现机器人辅助社群操作

微信 iPad 协议是指用于在 iPad 设备上使用微信应用的技术协议。一般来说&#xff0c;通过该协议可以将微信账号同步到 iPad 设备上&#xff0c;并且可以在 iPad 上发送和接收微信消息&#xff0c;查看好友列表、聊天记录等功能。微信 iPad 协议是通过私有API实现的。 需要一定…

嵌入式:ARM Day6

作业:完成cortex-A7核UART总线实验 目的&#xff1a;1.输入a,显示b&#xff0c;将输入的字符的ASCII码下一位字符输出 2.原样输出输入的字符串 源码&#xff1a; uart4.h #ifndef __UART4_H__ #define __UART4_H__#include "stm32mp1xx_rcc.h" #incl…

九、Linux下,如何在命令行进入文本编辑页面?

1、文本编辑基础 说到文本编辑页面&#xff0c;那就必须提到vi和vim&#xff0c;两者都是Linux系统中&#xff0c;常用的文本编辑器 2、三种工作模式 3、使用方法 &#xff08;1&#xff09;在进入Linux系统&#xff0c;在输入vim text.txt之后&#xff0c;会进入文本编辑中&…

Python多组数据三维绘图系统

文章目录 增添和删除坐标数据更改绘图逻辑源代码 Python绘图系统&#xff1a; 基础&#xff1a;将matplotlib嵌入到tkinter &#x1f4c8;简单的绘图系统 &#x1f4c8;数据导入&#x1f4c8;三维绘图系统自定义控件&#xff1a;坐标设置控件&#x1f4c9;坐标列表控件 增添和…