TransFormer介绍及框架搭建总结

一. 介绍

2017年, Google推出的Transformer框架

原论文地址: https://papers.neurips.cc/paper/7181-attention-is-all-you-need.pdf

Transformer的优势

相比之前占领市场的LSTM和GRU模型,Transformer有两个显著的优势:

1、Transformer能够利用分布式GPU进行并行训练,提升模型训练效率.    
2、在分析预测更长的文本时, 捕捉间隔较长的语义关联效果更好.  

Transformer的作用

基于seq2seq架构的transformer模型可以完成NLP领域研究的典型任务, 如机器翻译, 文本生成等. 同时又可以构建预训练语言模型,用于不同任务的迁移学习.

二. Transformer架构

★总体框架

三. 输入部分实现

文本嵌入层的作用

无论是源文本嵌入还是目标文本嵌入,都是为了将文本中词汇的数字表示转变为向量表示, 希望在这样的高维空间捕捉词汇间的关系.

文本嵌入代码实现

import torch
import torch.nn as nn
import torch.nn.functional as F
import math
from torch.autograd import Variable
​
​
class Embedding(nn.Module):
    def __init__(self, d_model, vocab_size):
        super(Embedding, self).__init__()
        self.lcut = nn.Embedding(vocab_size, d_model)
        self.d_model = d_model
​
    def forward(self, x):       # x表示在词表中的id
        # 乘以根号下d_model增加词嵌入编码的影响
        return self.lcut(x) * math.sqrt(self.d_model)
    
if __name__ == '__main__':
    d_model = 512
    vocab_size = 1000
    x = torch.LongTensor([
        [1, 2, 3, 5],
        [6, 7, 8, 10]
    ])
​
    emb = Embedding(d_model, vocab_size)
    embr = emb(x)
    print(embr)
    print(embr.shape)
    
    # padding_idx = 0: 表示: 0位置的词向量用0填充, 等于1则将1位置的词向量用0填充
    ceshi_emb = nn.Embedding(10, 5, padding_idx=0)
    ceshi_x = torch.LongTensor([[1, 0, 3, 5]])
    print(ceshi_emb(ceshi_x))

位置编码的作用

面试题位置编码

为什么用 sin cos
a 绝对位置编码(0-512):冲淡文本嵌入信息,模型外推性(ROPE alibi)
模型外推性解释: 模型训练时规定max_len=512, 预测时, 用了超出max_len的句子, 用作生成任务, 模型效果非常差
​
b 对绝对位置编码进行归一化:512/512=1  511/512约=1,相邻token位置信息区别不大
c 为什么不单独用一个三角函数:让每个token的位置独一无二
三角函数的周期,周期大,相邻token位置信息区别不大,周期小,间隔的token可能位置信息相似甚至相等

RNN的时间步可以看作是做了位置信息

因为在Transformer的编码器结构中, 并没有针对词汇位置信息的处理,因此需要在Embedding层后加入位置编码器,将词汇位置不同可能会产生不同语义的信息加入到词嵌入张量中, 以弥补位置信息的缺失.

位置编码器类PositionalEncoding 实现思路分析 1 init函数 (self, d_model, dropout, max_len=5000) super()函数 定义层self.dropout 定义位置编码矩阵pe 定义位置列-矩阵position 定义变化矩阵div_term 套公式div_term = torch.exp(torch.arange(0, d_model, 2) * -(math.log(10000.0)/d_model)) 位置列-矩阵 * 变化矩阵 阿达码积my_matmulres 给pe矩阵偶数列奇数列赋值 pe[:, 0::2] pe[:, 1::2] pe矩阵注册到模型缓冲区 pe.unsqueeze(0)三维 self.register_buffer('pe', pe) 2 forward(self, x) 返回self.dropout(x) 给x数据添加位置特征信息 x = x + Variable( self.pe[:,:x.size()[1]], requires_grad=False)

位置编码变换过程

position / _2i 的广播过程:
1. 维度相等, _2i.shape(1, d_model / 2), 1行, d_model/2列
2. 广播中'1'维度是可变的
position.shape(max_len, d_model / 2) == 将列复制d_model / 2 份 == [[0,0,0,,,],[1,1,1,,,],,,[max_len-1,,,]]
position:[[0, 0, 0, 0, 0, ,,,] --> len(position[0][0]) = d_model/2
           [1, 1, 1, 1, 1, ,,,]
           ...
           [max_len - 1, max_len -1 ,,,,,,]]
_2i.shape(max_len, d_model / 2) == 将行复制max_len份 == [[0, 2, 4, ,,, d_model / 2], [0, 2, 4, ,,, d_model / 2],,,]
_2i: [[0, 2, 4, ,,, d_model / 2]
       [0, 2, 4, ,,, d_model / 2]
       ...
       [0, 2, 4, ,,, d_model / 2]]  --> len(position[0]) = max_len

位置编码代码实现

# 2. 位置编码
class PositionalEncoding(nn.Module):
    def __init__(self, d_model, max_len=5000, dropout=0.1):
        super().__init__()
        self.dropout = nn.Dropout(p=dropout)
        # 初始化位置矩阵
        pe = torch.zeros(max_len, d_model)
        # 绝对位置矩阵, position.shape(max_len, 1), max_len行, 1列
        position = torch.arange(0, max_len).unsqueeze(1)
        # 缩放矩阵, _2i.shape(d_model / 2, )
        _2i = torch.arange(0, d_model, 2).float()
        # position / _2i 的广播过程:
        # 1. 维度相等, _2i.shape(1, d_model / 2), 1行, d_model/2列
        # 2. 广播中'1'维度是可变的
        # position.shape(max_len, d_model / 2) == 将列复制d_model / 2 份 == [[0,0,0,,,],[1,1,1,,,],,,[max_len-1,,,]]
        # position:[[0, 0, 0, 0, 0, ,,,] --> len(position[0][0]) = d_model/2
        #           [1, 1, 1, 1, 1, ,,,]
        #           ...
        #           [max_len - 1, max_len -1 ,,,,,,]]
        # _2i.shape(max_len, d_model / 2) == 将行复制max_len份 == [[0, 2, 4, ,,, d_model / 2], [0, 2, 4, ,,, d_model / 2],,,]
        # _2i: [[0, 2, 4, ,,, d_model / 2]
        #       [0, 2, 4, ,,, d_model / 2]
        #       ...
        #       [0, 2, 4, ,,, d_model / 2]]  --> len(position[0]) = max_len
        
        # _2i是缩放矩阵, 所以暂且忽略, 
        # pe[0, 0::2] = torch.sin(position[0]) --> 将pe(句子中token的位置)的第一个token的偶数位用[sin(0), sin(0), sin(0),,,]填充
        # pe[1, 0::2] = torch.sin(position[1]) --> 将pe(句子中token的位置)的第二个token的偶数位用[sin(1), sin(1), sin(1),,,]填充
        pe[:, 0::2] = torch.sin(position / 10000 ** (_2i / d_model))
        # pe[0, 1::2] = torch.cos(position[0]) --> 将pe(句子中token的位置)的第一个token的奇数位用[cos(0), cos(0), cos(0),,,]填充
        # pe[1, 0::2] = torch.cos(position[1]) --> 将pe(句子中token的位置)的第二个token的奇数位用[cos(1), cos(1), cos(1),,,]填充
        pe[:, 1::2] = torch.cos(position / 10000 ** (_2i / d_model))
​
        pe = pe.unsqueeze(0)
​
        self.register_buffer('pe', pe)
​
    def forward(self, x):
        x = x + Variable(self.pe[:, :x.size(1)], requires_grad=False)
        return self.dropout(x)

词向量特征分布曲线

Dropout=0.1

# 测试位置编码
def drop_PE_feature():
    # 创建pe
    my_pe = PositionalEncoding(20, 0.1)
    print('my_pe', my_pe.pe.shape)
    # 创建x, 传入pe
    y = my_pe(torch.zeros(1, 100, 20))
    print('y', y.shape)
​
    # 绘图
    plt.figure(figsize=(20, 20))
    plt.plot(np.arange(100), y[0, :, 4:8].numpy())
    plt.legend(['dim %d' % i for i in [4, 5, 6, 7]])
    plt.show()

Dropout=0

# 测试位置编码
def drop_PE_feature():
    # 创建pe
    my_pe = PositionalEncoding(20, 0)
    print('my_pe', my_pe.pe.shape)
    # 创建x, 传入pe
    y = my_pe(torch.zeros(1, 100, 20))
    print('y', y.shape)
​
    # 绘图
    plt.figure(figsize=(20, 20))
    plt.plot(np.arange(100), y[0, :, 4:8].numpy())
    plt.legend(['dim %d' % i for i in [4, 5, 6, 7]])
    plt.show()

总结Dropout

随机将位置信息和词嵌入信息置为0

四. 编码器部分实现

掩码张量

遮盖未来信息, 实现解码并行

基本介绍

掩码是遮盖权重矩阵:

Q * K ^T --> Q.shape(2, 4, 512)*K^T(2, 512, 4) = shape(2, 4, 4)

RNN逐时间步计算不可以并行操作, Transformer, 将词嵌入后的张量信息进行掩码操作, 方便并行操作. 一次将所有的词嵌入张量输入, 通过掩码的方式遮盖未来时间步的信息, 方便在解码器的第一个子层结构中并行

在解码器第一个子层中使用forward_mask 或 subsquent_mask

后续掩码

在解码器第二个子层中使用padding_mask

填充, 保持max_len

图解掩码张量

代码演示

# 3. 实现掩码张量
def subsquent_mask(seq_len):
    attn_shape = (1, seq_len, seq_len)
    subsquent_mask = np.triu(np.ones(attn_shape), k=1).astype('uint8')
    return torch.from_numpy(1 - subsquent_mask)

上三角矩阵triu

# 上三角矩阵:下面矩阵中0组成的形状为上三角矩阵
'''
[[[0. 1. 1. 1. 1.]
  [0. 0. 1. 1. 1.]
  [0. 0. 0. 1. 1.]
  [0. 0. 0. 0. 1.]
  [0. 0. 0. 0. 0.]]]

# nn.triu()函数功能介绍 
# def triu(m, k)
    # m:表示一个矩阵
    # K:表示对角线的起始位置(k取值默认为0)
    # return: 返回函数的上三角矩阵
'''

def dm_test_nptriu():
    # 测试产生上三角矩阵
    print(np.triu([[1, 1, 1, 1, 1],
                   [2, 2, 2, 2, 2],
                   [3, 3, 3, 3, 3],
                   [4, 4, 4, 4, 4],
                   [5, 5, 5, 5, 5]], k=1))
    --->[[0 1 1 1 1]
 		[0 0 2 2 2]
 		[0 0 0 3 3]
 		[0 0 0 0 4]
 		[0 0 0 0 0]]
    
    
    print(np.triu([[1, 1, 1, 1, 1],
                   [2, 2, 2, 2, 2],
                   [3, 3, 3, 3, 3],
                   [4, 4, 4, 4, 4],
                   [5, 5, 5, 5, 5]], k=0))
    --->[[1 1 1 1 1]
 		[0 2 2 2 2]
 		[0 0 3 3 3]
 		[0 0 0 4 4]
 		[0 0 0 0 5]]
    
    
    print(np.triu([[1, 1, 1, 1, 1],
                   [2, 2, 2, 2, 2],
                   [3, 3, 3, 3, 3],
                   [4, 4, 4, 4, 4],
                   [5, 5, 5, 5, 5]], k=-1))
    --->[[1 1 1 1 1]
 		[2 2 2 2 2]
 		[0 3 3 3 3]
 		[0 0 4 4 4]
 		[0 0 0 5 5]]

可视化

plt.figure(figsize=(5, 5))
plt.imshow(subsquent_mask(20)[0])
plt.show()

image-20241130104359327

掩码张量作用

在transformer中, 掩码张量的主要作用在应用attention(将在下一小节讲解)时,有一些生成的attetion张量中的值计算有可能已知量未来信息而得到的,未来信息被看到是因为训练时会把整个输出结果都一次性进行Embedding,但是理论上解码器的的输出却不是一次就能产生最终结果的,而是一次次通过上一次结果综合得出的,因此,未来的信息可能被提前利用. 所以,我们会进行遮掩. 关于解码器的有关知识将在后面的章节中讲解.

注意力机制

计算规则

根号dk

为什么除以根号下dk?
输入x (2,4,512),分别经过三个线性层,Q、K、V (2,4,512)---> Q@K^T (2,4,4)
词嵌入维度 dk=512
前提假设 Q、K 满足标准正态分布,(均值)期望0,方差1, E(QK)=0, Var(QK)=dk, 把方差dk拉回到1,除以标准差(根号下dk)
可以解决:
1、内部协变量偏移问题  输入输出最好是同分布的,好处是: 需要的data更少,训练时间更短
2、f = softmax(xi),f‘ = f(1-f)(i=j);f’ = -fi*fj (j!=i),dk越大,方差越大,数据越分散,大的很大,小的很小,再经过softmax,
大的变成1,小的变成了0,类似onehot编码形式,把这些值带入softmax导数公式,梯度接近为0,梯度消失问题,模型可训练性非常差

假设: Q和K满足标准正太分布, 均值 为0, 方差1,. 因此Q*K^T均值为0, 方差为dk

将方差dk缩放回1, 所以要除以标准差:根号dk

单头注意力

# 4. 实现单头注意力
def attention(query, key, value, dropout=None, mask=None):
    # 1. 获取dk
    d_k = query.size(-1)
    
    # 2. 缩放点积
    scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)
    
    # 3. mask
    if mask is not None:
        scores = scores.masked_fill(mask == 0, -1e9)
        
    # 4. softmax
    p_attn = F.softmax(scores, dim=-1)
    
    # 5. dropout
    if dropout is not None:
        p_attn = dropout(p_attn)
    
    # 6. 返回注意attn
    return torch.matmul(p_attn, value), p_attn

多头注意力

多头注意力机制的理解:1、代码;2、原理
a、注意力计算公式,(2,4,512)---》(2,4,8,64)---》(2,8,4,64)
b、512,划分成了8个细分子空间,每个子空间就是一个观察角度,特征抽取会更丰富更全面,盲人摸象
c、降低了计算复杂度,4*512*4*512,变成了 8 个 4*64*4*64

原理
  1. 代码:

    a. 注意力计算公式softmax(Q @ K^T/根号dk) @ V

    维度变换: (2, 4, 512) --> (2, 4, 8, 64) --> (2, 8, 4, 64)

    b. 将整个512维度划分为8个细分子空间, 每个子空间都是一个观察角度, 特征抽取会更丰富更全面, 例如: 盲人摸象, 8个角度更全面

    c. 计算复杂度

    4*512@512*4  --> 8*4*64@64*4
代码
# 5. 实现多头注意力
# 5.1 定义克隆方法, 用于克隆线性层
def clones(module, N):
    return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])


# 5.2 多头注意力机制类
class MutiHeadAtten(nn.Module):
    def __init__(self, head, embedding_dim, dropout=0.1):
        super().__init__()
        # 断言, 确定词嵌入维度一定能够整除头数
        assert embedding_dim % head == 0
        # 每个头的维度
        self.d_k = embedding_dim // head
        self.head = head
        self.attn = None
        self.dropout = nn.Dropout(p=dropout)
        # 初始化4个线性层, q, k, v分别用一个, 最后的x用一个
        self.linears = clones(nn.Linear(embedding_dim, embedding_dim), 4)

    def forward(self, query, key, value, mask=None):
        if mask is not None:
            mask.unsqueeze(0)
        batch_size = query.size(0)

        # q, k, v 三者的维度都是(batch_size, head, seq_len, d_k), 其中d_k是分为多头后的词嵌入维度
        # zip: 将model和q, k , v分别对应返回
        # transpose(1, 2): 是为了让seq_len(token数量)与dk相邻 
        query, key, value = [model(x).view(batch_size, -1, self.head, self.d_k).transpose(1, 2) for model, x in zip(self.linears, (query, key, value))]

        x, self.attn = attention(query, key, value, dropout=self.dropout, mask=mask)

        # 将多头进行拼接, 维度为(batch_size, seq_len, embedding_dim) -->(2, 4, 512)
        x = x.transpose(1, 2).contiguous().view(batch_size, -1, self.head * self.d_k)

        return self.linears[-1](x)

前馈全连接层

在Transformer中前馈全连接层就是具有两层线性层的全连接网络.

前馈全连接作用

  • 考虑注意力机制可能对复杂过程的拟合程度不够, 通过增加两层网络来增强模型的能力.

代码

# 6. ffn前馈全连接层
class PositionwiseFeedForward(nn.Module):
    def __init__(self, d_model, d_ff, dropout=0.1):
        super().__init__()
        self.w1 = nn.Linear(d_model, d_ff)
        self.w2 = nn.Linear(d_ff, d_model)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x):
        return self.w2(self.dropout(F.relu(self.w1(x))))

规范化层

作用

它是所有深层网络模型都需要的标准网络层,因为随着网络层数的增加,通过多层的计算后参数可能开始出现过大或过小的情况,这样可能会导致学习过程出现异常,模型可能收敛非常的慢. 因此都会在一定层数后接规范化层进行数值的规范化,使其特征数值在合理范围内.

  1. 解决内部协变量偏移问题

  2. 把数据分布拉回到激活函数的非饱和区

norm不会丢失网络学到的参数

norm改变的是数据的空间分布, 不会改变数据内部的相对大小

增加了两个可学习参数: 缩放系数, 偏移系数, 分别决定方差, 期望. 让模型自行决定缩放偏移多少.

BN和LN区别

BN

在embedding_dim中的某一维度求均值和方差, 包含batch_size中每个句子的每个token, 会造成padding不规则

LN

在batch_size中的某句话中求每个token分别求embedding_dim的均值和方差, padding规则, 方便处理

代码

# 7. layer_norm
class LayerNorm(nn.Module):
    def __init__(self, features, eps=1e-6):
        super().__init__()
        self.a2 = nn.Parameter(torch.ones(features))
        self.b2 = nn.Parameter(torch.zeros(features))
        self.eps = eps

    def forward(self, x):
        # 均值
        mean = x.mean(-1, keepdim=True)
        # 方差
        std = x.std(-1, keepdim=True)
        return self.a2 * (x - mean) / (std + self.eps) + self.b2

子层(残差)链接结构

残差链接作用: 缓解梯度消失, 提高模型的可训练性

概念原理

如图所示,输入到每个子层以及规范化层的过程中,还使用了残差链接(跳跃连接),因此我们把这一部分结构整体叫做子层连接(代表子层及其链接结构),在每个编码器层中,都有两个子层,这两个子层加上周围的链接结构就形成了两个子层连接结构.

  • 什么是子层连接结构:

    • 如图所示,输入到每个子层以及规范化层的过程中,还使用了残差链接(跳跃连接),因此我们把这一部分结构整体叫做子层连接(代表子层及其链接结构), 在每个编码器层中,都有两个子层,这两个子层加上周围的链接结构就形成了两个子层连接结构.

  • 学习并实现了子层连接结构的类: SublayerConnection

    • 类的初始化函数输入参数是size, dropout, 分别代表词嵌入大小和置零比率.

    • 它的实例化对象输入参数是x, sublayer, 分别代表上一层输出以及子层的函数表示.

    • 它的输出就是通过子层连接结构处理的输出.

代码

# 8. 子层链接结构
class SublayerConnection(nn.Module):
    def __init__(self, size, dropout=0.1):
        super().__init__()
        self.norm = LayerNorm(size)
        self.dropout = nn.Dropout(p=dropout)

    def forward(self, x, sublayer):
        res = x + self.dropout(sublayer(self.norm(x)))
        return res
    
    
if __name__ == '__main__':	
    # 测试子层链接(多头自注意力)
    x = pe_res
    self_attn = MutiHeadAtten(8, d_model)
    sublayer = lambda x: self_attn(x, x, x, sm)
    sc = SublayerConnection(d_model)
    sc_res = sc(x, sublayer)
    # print(sc_res.shape)

    # 测试子层链接结构(前馈全连接层)
    f_x = pe_res
    ff = PositionwiseFeedForward(d_model, 1024)
    ff_sc = SublayerConnection(d_model)
    res_ff_sc = ff_sc(f_x, ff)
    # print(res_ff_sc.shape)

编码器层

作用

  • 作为编码器的组成单元, 每个编码器层完成一次对输入的特征提取过程, 即编码过程.

代码

# 9. 编码器层
class EncoderLayer(nn.Module):
    def __init__(self, size, self_attn, feed_forward, dropout=0.1):
        super().__init__()
        self.size = size
        # 多头注意力机制对象
        self.self_attn = self_attn
        # 前馈全连接层对象
        self.feed_forward = feed_forward
        # 克隆两个子层链接结构
        self.sublayers = clones(SublayerConnection(size), 2)

    def forward(self, x, mask=None):
        # x: 位置编码结果
        # mask: 解码器时的掩码
        x = self.sublayers[0](x,lambda x: self.self_attn(x, x, x, mask))
        x = self.sublayers[1](x, self.feed_forward)
        return x
    
if __name__ == '__main__':
    # 测试encoder layer层
    x = pe_res
    attn = MutiHeadAtten(8, d_model)
    ffn = PositionwiseFeedForward(d_model, 1024)
    el = EncoderLayer(d_model, attn, ffn)
    el_res = el(x)
    print(el_res.shape)

编码器

作用

  • 编码器用于对输入进行指定的特征提取过程, 也称为编码, 由N个编码器层堆叠而成.

代码

# 10. 编码器类
class Encoder(nn.Module):
    def __init__(self, layer, N):
        super().__init__()
        # 克隆N个编码器层
        self.layers = clones(layer, N)
        # 经过编码后再次经过norm层输出编码器的输出结果
        self.norm = LayerNorm(layer.size)

    def forward(self, x, mask=None):
        for layer in self.layers:
            x = layer(x, mask)
        return self.norm(x)
    
    
if __name__ == '__main__':
	muti_attn = MutiHeadAtten(8, d_model)
    feed_forward = PositionwiseFeedForward(d_model, 1024)
    encoder_layer = EncoderLayer(d_model, muti_attn, feed_forward)
    en = Encoder(encoder_layer, 6)
    res_en = en(pe_res, sm)
    print(res_en.shape)

五. 解码器部分实现

解码器部分:

  • 由N个解码器层堆叠而成

  • 每个解码器层由三个子层连接结构组成

  • 第一个子层连接结构包括一个多头自注意力子层和规范化层以及一个残差连接

  • 第二个子层连接结构包括一个多头注意力子层和规范化层以及一个残差连接

  • 第三个子层连接结构包括一个前馈全连接子层和规范化层以及一个残差连接

解码器层

  • 作为解码器的组成单元, 每个解码器层根据给定的输入向目标方向进行特征提取操作,即解码过程.

# 11. 解码器层
class DecoderLayer(nn.Module):
    def __init__(self, size, self_attn, src_attn, ffn, dropout=0.1):
        super().__init__()
        self.size = size
        # 自注意力
        self.self_attn = self_attn
        # 普通注意力
        self.src_attn = src_attn
        self.ffn = ffn
        self.sublayers = clones(SublayerConnection(size), 3)

    def forward(self, x, memory, source_mask, target_mask):
        # memory: 中间语义张量C
        # target_mask: 掩码未来信息
        # source_mask: 掩码padding
        m = memory
        x = self.sublayers[0](x, lambda x:self.self_attn(x, x, x, target_mask))
        # x->Q, m->K,V  ===> 信息融合
        x = self.sublayers[1](x, lambda x:self.src_attn(x, m, m, source_mask))
        x = self.sublayers[2](x, self.ffn)
        return x
    
    
if __name__ == '__main__':
    memory = res_en
    x = pe_res
    source_mask = target_mask = sm
    dl = DecoderLayer(d_model, muti_attn, src_attn, feed_forward)
    res_dl = dl(x, memory, target_mask, source_mask)
    print(res_dl.shape)

解码器

  • 根据编码器的结果以及上一次预测的结果, 对下一次可能出现的'值'进行特征表示.

# 12. 解码器
class Decoder(nn.Module):
    def __init__(self, layer, N):
        super().__init__()
        self.layers = clones(layer, N)
        self.norm = LayerNorm(layer.size)

    def forward(self, x, memory, source_mask, target_mask):
        for layer in self.layers:
            x = layer(x, memory, source_mask, target_mask)
        return self.norm(x)

if __name__ == '__main__':
	# 测试decoder
    layer = dl
    de = Decoder(layer, 6)
    res_de = de(x, memory, source_mask, target_mask)
    print(res_de.shape)

总结:

将实例化后的解码器层对象, 传递给解码器, 让其循环创建6次,

解码器前向传播参数说明:

x: 目标词经过输入部分的结果

memory: 编码器输出结果

source_mask: 中间语义张量C的掩码

target_mask: 目标seq_len的掩码结果

六. 输出部分

线性层+ softmax

# 13. 输出部分
class Generator(nn.Module):
    def __init__(self, d_model, vocab_size):
        super().__init__()
        self.project = nn.Linear(d_model, vocab_size)

    def forward(self, x):
        return F.log_softmax(self.project(x), dim=-1)


gen = Generator(d_model, vocab_size=1000)
res_gen = gen(res_de)
print(res_gen.shape)

七. 模型构建

模型框架组装

  • 类的初始化函数传入5个参数, 分别是编码器对象, 解码器对象, 源数据嵌入函数, 目标数据嵌入函数, 以及输出部分的类别生成器对象.

  • 类中共实现三个函数, forward, encode, decode

  • forward是主要逻辑函数, 有四个参数, source代表源数据, target代表目标数据, source_mask和target_mask代表对应的掩码张量.

  • encode是编码函数, 以source和source_mask为参数.

  • decode是解码函数, 以memory即编码器的输出, source_mask, target, target_mask为参数

# 14. 实现模型整体构建
class EncoderDecoder(nn.Module):
    def __init__(self, encoder, decoder, source_embd, target_embd, generator):
        super().__init__()
        self.encoder = encoder
        self.decoder = decoder
        self.source_embd = source_embd
        self.target_embd = target_embd
        self.generator = generator

    def forward(self, source, target, source_mask, target_mask):
        return self.generator(self.decode(self.encode(source, source_mask), source_mask, target, target_mask))

    def encode(self, source, source_mask):
        return self.encoder(self.source_embd(source), source_mask)

    def decode(self, memory, source_mask, target, target_mask):
        return self.decoder(self.target_embd(target), memory, source_mask, target_mask)

构建模型

# 15. 组建模型
def make_model(source_vocab, target_vocab, d_model=512, d_ff=2048, head=8, N=6, dropout=0.1):
    c = copy.deepcopy
    attn = MutiHeadAtten(head=head, embedding_dim=d_model)
    ffn = PositionwiseFeedForward(d_model=d_model, d_ff=d_ff, dropout=dropout)
    position = PositionalEncoding(d_model=d_model, dropout=dropout)
    model = EncoderDecoder(
        encoder=Encoder(EncoderLayer(d_model, c(attn), c(ffn), dropout=dropout), N=N),
        decoder=Decoder(DecoderLayer(d_model, c(attn), c(attn), c(ffn), dropout=dropout), N=N),
        source_embd=nn.Sequential(Embedding(d_model=d_model, vocab_size=source_vocab), c(position)),
        target_embd=nn.Sequential(Embedding(d_model=d_model, vocab_size=target_vocab), c(position)),
        generator=Generator(d_model=d_model, vocab_size=target_vocab)
    )
    # 参数初始化
    for p in model.parameters():
        if p.dim() > 1:
            nn.init.xavier_uniform_(p)
​
    return model
​
​
source_vocab = 1000
target_vocab = 5000
model = make_model(source_vocab=source_vocab, target_vocab=target_vocab)
print(model)

八.  整体代码

import copy
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.autograd import Variable
import numpy as np


# 1. 词嵌入层
class Embedding(nn.Module):
    def __init__(self, vocab_size, d_model):
        super().__init__()
        self.embd = nn.Embedding(vocab_size, d_model)
        self.d_model = d_model

    def forward(self, x):
        x = self.embd(x)
        return x


x = torch.LongTensor([
    [1, 2, 3, 4],
    [5, 6, 7, 8]
])
eb = Embedding(1000, 512)
eb_res = eb(x)
print(eb_res.shape)


# 2. 位置编码
class PositionalEncoding(nn.Module):
    def __init__(self, d_model, max_len=5000, dropout=0.1):
        super().__init__()
        self.dropout = nn.Dropout(p=dropout)
        # 初始化位置矩阵
        pe = torch.zeros(max_len, d_model)

        # 绝对位置矩阵, position.shape(max_len, 1), max_len行, 1列
        position = torch.arange(0, max_len).unsqueeze(1)

        # 缩放矩阵写法1
        # div_term = torch.exp(torch.arange(0, d_model, 2) * (-math.log(10000.0) / d_model))

        # 缩放矩阵写法2, _2i.shape(d_model / 2, )
        _2i = torch.arange(0, d_model, 2).float()

        # position / _2i 的广播过程:
        # 1. 维度相等, _2i.shape(1, d_model / 2), 1行, d_model/2列
        # 2. 广播中'1'维度是可变的
        # position.shape(max_len, d_model / 2) == 将列复制d_model / 2 份 == [[0,0,0,,,],[1,1,1,,,],,,[max_len-1,,,]]
        # position:[[0, 0, 0, 0, 0, ,,,] --> len(position[0][0]) = d_model/2
        #           [1, 1, 1, 1, 1, ,,,]
        #           ...
        #           [max_len - 1, max_len -1 ,,,,,,]]
        # _2i.shape(max_len, d_model / 2) == 将行复制max_len份 == [[0, 2, 4, ,,, d_model / 2], [0, 2, 4, ,,, d_model / 2],,,]
        # _2i: [[0, 2, 4, ,,, d_model / 2]
        #       [0, 2, 4, ,,, d_model / 2]
        #       ...
        #       [0, 2, 4, ,,, d_model / 2]]  --> len(position[0]) = max_len

        # _2i是缩放矩阵, 所以暂且忽略,
        # pe[0, 0::2] = torch.sin(position[0]) --> 将pe(句子中token的位置)的第一个token的偶数位用[sin(0), sin(0), sin(0),,,]填充
        # pe[1, 0::2] = torch.sin(position[1]) --> 将pe(句子中token的位置)的第二个token的偶数位用[sin(1), sin(1), sin(1),,,]填充
        pe[:, 0::2] = torch.sin(position / 10000 ** (_2i / d_model))
        # pe[0, 1::2] = torch.cos(position[0]) --> 将pe(句子中token的位置)的第一个token的奇数位用[cos(0), cos(0), cos(0),,,]填充
        # pe[1, 0::2] = torch.cos(position[1]) --> 将pe(句子中token的位置)的第二个token的奇数位用[cos(1), cos(1), cos(1),,,]填充
        pe[:, 1::2] = torch.cos(position / 10000 ** (_2i / d_model))

        pe = pe.unsqueeze(0)

        self.register_buffer('pe', pe)

    def forward(self, x):
        x = x + Variable(self.pe[:, :x.size(1)], requires_grad=False)
        return self.dropout(x)


pe = PositionalEncoding(512)
pe_res = pe(eb_res)
print(pe_res.shape)


# 3. 实现掩码张量
def subsquent_mask(seq_len):
    mask_shape = (1, seq_len, seq_len)
    mask = np.triu(np.ones(mask_shape), k=1).astype('uint8')
    return torch.from_numpy(1 - mask)


sm = subsquent_mask(4)
print(sm)


# 4. 单头注意力机制
def attention(query, key, value, mask=None, dropout=None):
    attn_score = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(query.size(-1))
    if mask is not None:
        # masked_fill: 实际上是哈达玛积, 按位相乘, mask中等于0的位置, 填充非常小的负数来遮盖未来信息
        attn_score = attn_score.masked_fill(mask == 0, -1e9)
    p_attn = F.softmax(attn_score, dim=-1)
    if dropout is not None:
        p_attn = dropout(p_attn)
    return torch.matmul(p_attn, value), p_attn


attn_x, p_attn = attention(pe_res, pe_res, pe_res, mask=sm)
print(attn_x.shape)


# 5. 多头注意力机制
# 5.1 辅助函数, 克隆模型
def clone(layer, N):
    layers = nn.ModuleList([copy.deepcopy(layer) for _ in range(N)])
    return layers


# 5.2 多头注意力机制
class MultiHeadAttention(nn.Module):
    def __init__(self, head, d_model, dropout=0.1):
        super().__init__()
        assert d_model % head == 0
        self.d_k = d_model // head
        self.head = head
        self.attn = None
        self.dropout = nn.Dropout(p=dropout)
        self.layers = clone(nn.Linear(d_model, d_model), 4)

    def forward(self, query, key, value, mask=None):
        if mask is not None:
            mask = mask.unsqueeze(0)
        bc = query.size(0)
        query, key, value = [model(x).view(bc, -1, self.head, self.d_k).transpose(1, 2) for model, x in
                             zip(self.layers, (query, key, value))]
        x, self.attn = attention(query, key, value, mask=mask, dropout=self.dropout)
        x = x.transpose(1, 2).contiguous().view(bc, -1, self.head * self.d_k)
        return self.layers[-1](x)


mha = MultiHeadAttention(8, 512)
mha_res = mha(pe_res, pe_res, pe_res, mask=sm)
print(mha_res.shape)


# 6. 前馈全连接层
class PositionFeedForward(nn.Module):
    def __init__(self, d_model, dff, dropout=0.1):
        super().__init__()
        self.d_model = d_model
        self.w1 = nn.Linear(d_model, dff)
        self.w2 = nn.Linear(dff, d_model)
        self.dropout = nn.Dropout(p=dropout)

    def forward(self, x):
        return self.w2(self.dropout(F.relu(self.w1(x))))


ffn = PositionFeedForward(512, 1024)
ffn_res = ffn(mha_res)
print(ffn_res.shape)


# 7. 规范化层Layer Norm --> 解决内部协变量偏移问题
class LayerNorm(nn.Module):
    def __init__(self, size, esp=1e-6):
        super().__init__()
        self.size = size
        self.a2 = nn.Parameter(torch.ones(self.size))
        self.b2 = nn.Parameter(torch.zeros(self.size))
        self.esp = esp

    def forward(self, x):
        mean = x.mean(-1, keepdim=True)
        std = x.std(-1, keepdim=True)
        return self.a2 * (x - mean) / (std + self.esp) + self.b2


ln = LayerNorm(512)
ln_res = ln(ffn_res)
print(ln_res.shape)


# 8. 子层链接链接结构
class SubLayerConnection(nn.Module):
    def __init__(self, d_model, dropout=0.1):
        super().__init__()
        self.norm = LayerNorm(d_model)
        self.dropout = nn.Dropout(p=dropout)

    def forward(self, x, sublayer):
        return x + self.dropout(sublayer(self.norm(x)))


sc = SubLayerConnection(512)
sl = MultiHeadAttention(8, 512)
sl2 = PositionFeedForward(512, 1024)
sublayer_mha = lambda x: sl(x, x, x, mask=sm)
sc_res = sc(pe_res, sublayer_mha)
sc2_res = sc(pe_res, sl2)
print('sc_res', sc_res.shape)
print('sc2_res', sc2_res.shape)


# 9. 编码器层
class EncoderLayer(nn.Module):
    def __init__(self, d_model, self_attn, feed_forward):
        super().__init__()
        self.size = d_model
        self.multiattn = self_attn
        self.ffn = feed_forward
        self.sub_layers = clone(SubLayerConnection(d_model), 2)

    def forward(self, x, mask=None):
        encoder_mha = lambda x: self.multiattn(x, x, x, mask=mask)
        x = self.sub_layers[0](x, encoder_mha)
        x = self.sub_layers[1](x, self.ffn)
        return x


self_attn = MultiHeadAttention(8, 512)
ffn = PositionFeedForward(512, 1024)
enl = EncoderLayer(512, self_attn, ffn)
enl_res = enl(pe_res, sm)
print('enl_shape: ', enl_res.shape)


# 10. 编码器
class Encoder(nn.Module):
    def __init__(self, layers, N):
        # layers: 编码器层对象
        super().__init__()
        self.norm = LayerNorm(layers.size)
        self.layers = clone(layers, N)

    def forward(self, x, mask=None):
        for layer in self.layers:
            x = layer(x, mask)
        return self.norm(x)


en_mha = MultiHeadAttention(8, 512)
en_ffn = PositionFeedForward(512, 1024)
en_enl = EncoderLayer(d_model=512, self_attn=en_mha, feed_forward=en_ffn)
en = Encoder(en_enl, 6)
en_res = en(pe_res, mask=sm)
print('en_res: ', en_res.shape)


# 11. 解码器层
class DecoderLayer(nn.Module):
    def __init__(self, d_model, target_attn_obj, source_attn_obj, ffn_obj):
        super().__init__()
        self.size = d_model
        self.target_attn_obj = target_attn_obj
        # source_attn_obj: 用来做信息融合, 融合编码器和当前解码器层的信息
        self.source_attn_obj = source_attn_obj
        self.ffn_obj = ffn_obj
        self.sub_layers = clone(SubLayerConnection(d_model), 3)

    def forward(self, x, memery, source_mask, target_mask):
        m = memery
        x = self.sub_layers[0](x, lambda x: self.target_attn_obj(x, x, x, target_mask))
        x = self.sub_layers[1](x, lambda x: self.source_attn_obj(x, m, m, source_mask))
        x = self.sub_layers[2](x, self.ffn_obj)
        return x


del_src_attn = MultiHeadAttention(8, 512)
del_tar_attn = MultiHeadAttention(8, 512)
del_ffn = PositionFeedForward(512, 1024)
del_source_mask = del_target_mask = sm
memery = en_res
de_l = DecoderLayer(d_model=512, ffn_obj=del_ffn, target_attn_obj=del_tar_attn, source_attn_obj=del_src_attn)
del_res = de_l(x=pe_res, memery=memery, source_mask=del_source_mask, target_mask=del_target_mask)
print('del_res: ', del_res.shape)


# 12. 解码器
class Decoder(nn.Module):
    def __init__(self, layer, N):
        super().__init__()
        self.norm = LayerNorm(layer.size)
        self.layers = clone(layer, N)

    def forward(self, x, memery, source_mask, target_mask):
        for layer in self.layers:
            x = layer(x, memery, source_mask, target_mask)
        return self.norm(x)


de_src_attn = MultiHeadAttention(8, 512)
de_tar_attn = MultiHeadAttention(8, 512)
de_ffn = PositionFeedForward(512, 1024)
de_l = DecoderLayer(512, ffn_obj=de_ffn, source_attn_obj=de_src_attn, target_attn_obj=de_tar_attn)
de = Decoder(de_l, 6)
de_res = de(pe_res, en_res, sm, sm)
print('de_res: ', de_res.shape)


# 13. 输出部分
class Generator(nn.Module):
    def __init__(self, vocab_size, d_model):
        super().__init__()
        self.gen = nn.Linear(d_model, vocab_size)

    def forward(self, x):
        return F.log_softmax(self.gen(x), dim=-1)


gen = Generator(d_model=512, vocab_size=5000)
gen_res = gen(de_res)
print('gen_res: ', gen_res.shape)


# 14. 组装模型-编码器解码器
class EncoderDecoder(nn.Module):
    def __init__(self, source_embed, target_embed, encoder, decoder, generator):
        # source_embed: 源语言的文本嵌入曾的对象, (包含词嵌入和位置编码)
        # target_embed: 目标语言的文本嵌入曾的对象, (包含词嵌入和位置编码)
        # encoder: 编码器对象
        # decoder: 解码器对象
        # generator: 输出层对象
        super().__init__()
        self.source_embed = source_embed
        self.encoder = encoder
        self.target_embed = target_embed
        self.decoder = decoder
        self.generator = generator

    def forward(self, source, target, source_mask, target_mask):
        # source: 源语言的语料(word2index的结果表示内容)
        # target: 目标语言的语料(word2index的结果表示内容)
        return self.generator(self.decode(self.encode(source, source_mask), target, source_mask, target_mask))

    def encode(self, source, source_mask):
        return self.encoder(self.source_embed(source), source_mask)

    def decode(self, memory, target, source_mask, target_mask):
        return self.decoder(self.target_embed(target), memory, source_mask, target_mask)


# 15. 实例化模型
def make_model(source_vocab, target_vocab, dff=2048, head=8, d_model=512, N=6, dropout=0.1):
    c = copy.deepcopy
    attn = MultiHeadAttention(head=head, d_model=d_model)
    ffn = PositionFeedForward(d_model=d_model, dff=dff)
    position = PositionalEncoding(d_model=d_model, dropout=dropout)
    model = EncoderDecoder(
        # nn.Sequential用于保存简单的模型, 前向传播时按顺序执行
        source_embed=nn.Sequential(Embedding(source_vocab, d_model), c(position)),
        target_embed=nn.Sequential(Embedding(target_vocab, d_model), c(position)),
        encoder=Encoder(EncoderLayer(d_model=d_model, self_attn=c(attn), feed_forward=c(ffn)), N),
        decoder=Decoder(DecoderLayer(d_model=d_model, source_attn_obj=c(attn), target_attn_obj=c(attn), ffn_obj=c(ffn)),
                        N),
        generator=Generator(target_vocab, d_model)
    )

    # 初始化
    for p in model.parameters():
        if p.dim() > 1:
            nn.init.xavier_uniform_(p)

    return model


model = make_model(1000, 5000, 1024)
model_res = model(x, x, sm, sm)
print('model_res: ', model_res.shape)
print('*' * 50)
print(model)


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值