Press "Enter" to skip to content

【深度学习】(3) Transformer 中的 Encoder 机制,附Pytorch完整代码

大家好,今天和各位分享一下 Transformer 中的 Encoder 部分涉及到的知识点: Word Embedding、Position Embedding、self_attention_Mask

 

本篇博文是对上一篇 《Transformer代码复现》的解析, 强烈建议大家先看一下 : https://blog.csdn.net/dgvv4/article/details/125491693

 

由于 Transformer 中涉及的知识点比较多,之后的几篇会介绍 Decoder 机制、损失计算、实战案例等。

 

1. Word Embedding

 

Word Embedding 可以理解为让句子中的每个单词都能狗对应一个特征向量。

 

该部分的代码如下:

 

首先指定特征序列和目标序列的长度, src_len=[2, 4] 代表特征序列中包含 2 个句子,第一个句子中有 2 个单词,第二个句子中有 4 个单词 。

 

指定 序列的单词库大小为 8 ,即 序列中所有的单词都是在 1~8 之间选取 。接下来随机生成每个句子中包含的单词,得到特征序列 src_seq 和目标序列 tgt_seq。

 

由于每个句子的长度不一样,比如特征序列 src_seq 中第一个句子有 2 个单词,第二个句子有 4 个单词。在送入至 Word Embedding 之前, 需要把所有句子的长度给统一 , 在第一个句子后面填充 2 个 0,使得特征序列中的两个句子等长 。

 

import torch
from torch import nn
from torch.nn import functional as F
import numpy as np
max_word_idx = 8  # 特征序列和目标序列的单词库由8种单词组成
model_dim = 6  # wordembedding之后,每个单词用长度为6的向量来表示
# ------------------------------------------------------ #
#(1)构建序列,序列的字符以索引的形式表示
# ------------------------------------------------------ #
# 指定序列长度
src_len = torch.Tensor([2, 4]).to(torch.int32)  # 特征序列的长度为2
tgt_len = torch.Tensor([4, 3]).to(torch.int32)  # 目标序列的长度为2
# 特征序列种有2个句子,第一个句子包含2个单词,第二个句子有4个单词
print(src_len, tgt_len)  # tensor([2, 4]) tensor([4, 3])
# 创建序列,句子由八种单词构成,用1~8来表示
src_seq = [ torch.randint(1, max_word_idx, (L,)) for L in src_len ]  # 创建特征序列
tgt_seq = [ torch.randint(1, max_word_idx, (L,)) for L in tgt_len ]  # 创建目标序列
print(src_seq, tgt_seq)
# [tensor([6, 4]), tensor([6, 4, 1, 7])]    # 特征序列,第一个句子有2个单词,第二个句子有4个单词
# [tensor([4, 2, 1, 3]), tensor([6, 5, 1])]  # 目标特征,第一个句子有4个单词,第二个句子有3个单词
# 每个句子的长度都不一样,需要填充0变成相同长度
new_seq = []  # 保存padding后的序列
for seq in src_seq:  # 遍历特征序列中的每个句子
    sent = F.pad(seq, pad=(0, max(src_len)-len(seq)))  # 右侧填充0保证所有句子等长
    sent = torch.unsqueeze(sent, dim=0)  # 变成二维张量[max_src_len]==>[1, max_src_len]
    new_seq.append(sent)  # 保存padding后的序列
for seq in tgt_seq:  # 遍历目标序列中的每个句子
    sent = F.pad(seq, pad=(0, max(tgt_len)-len(seq)))
    sent = torch.unsqueeze(sent, dim=0)  # 变成二维张量[max_tgt_len]==>[1, max_tgt_len]
    new_seq.append(sent)  # 保存padding后的序列
# 由于特征序列和目标序列都保存在list中,变成tensor类型,在axis=0维度堆叠
src_seq = torch.cat(new_seq[:2], dim=0)  # 特征序列
tgt_seq = torch.cat(new_seq[2:], dim=0)  # 目标序列
print(src_seq, src_seq.shape)  # 查看特征序列 shape=[2,4], 序列中有2个句子,每个句子4个单词
print(tgt_seq, tgt_seq.shape)  # 目标序列同上
'''
src_seq = [[6, 4, 0, 0], [6, 4, 1, 7]]
src_seq.shape = [2, 4]
tgt_seq = [[4, 2, 1, 3], [6, 5, 1, 0]]
tgt_seq.shape = [2, 4]
'''
# ------------------------------------------------------ #
#(2)word-embadding
# ------------------------------------------------------ #
# 实例化embedding类, 一共8种单词,考虑到padding填充的0,因此单词表一共9种, 每种单词的特征向量长度为6
src_embedding_tabel = nn.Embedding(num_embeddings=max_word_idx+1, embedding_dim=model_dim)  # 特征序列的Embedding
tgt_embedding_tabel = nn.Embedding(num_embeddings=max_word_idx+1, embedding_dim=model_dim)  # 目标序列的Embedding
print(src_embedding_tabel.weight)  # shape=[9,6], 第一行是分配给padding=0,剩下的八行分类给8种单词
print(tgt_embedding_tabel)
# 从embedding表中获取每个单词的特征向量表示,单词0的特征向量为[-1.1004, -1.4062,  1.1152,  0.9054,  1.0759,  1.1679]
src_embedding = src_embedding_tabel(src_seq)  # ()代表使用该实例的前向传播方法
tgt_embedding = src_embedding_tabel(tgt_seq)
# 打印每个句子对应的embedding张量,每一行代表句子中每个单词对应的embedding
print(src_embedding)
# shape=[2,4,6] 代表目标序列由2个句子,每个句子有4个单词,每个单词用长度为6的向量表示
print(src_embedding.shape)

 

首先我们的单词库是由 1~8 组成的,后面又多了 padding 的 0 填充,因此 现在单词库中一共有 9 种 ,通过 nn.Embedding() 为 9 种单词分别构建一个长度为 model_dim=6 的特征向量 。如下面的第一个矩阵,单词 0 用向量 [-1.1004, -1.4062, 1.1152, 0.9054, 1.0759, 1.1679] 来表示。

 

接下来 通过前向传播为序列中的每个单词编码 ,见下面的第二个矩阵,如:src_seq = [[6, 4, 0, 0], [6, 4, 1, 7]] 中,第一个单词 6 用向量 [-0.9194, 0.3338, 0.7215, -1.2306, 0.9512, -0.1863] 来表示。

 

特征序列的 shape 由原来的 [2, 4] 变成 [2, 4, 6] ,即 特征序列中有 2 个句子,每个句子包含 4 个单词,每个单词用长度为 6 的向量来表示 。

 

# src_embedding_tabel.weight
Parameter containing:
tensor([[-1.1004, -1.4062,  1.1152,  0.9054,  1.0759,  1.1679],
        [-0.0360, -1.6144,  0.9804,  0.4482,  1.8510,  0.3860],
        [ 0.2041,  0.1746,  0.4676, -1.3600,  0.3034,  1.7780],
        [ 0.5122, -1.3473, -0.2934, -0.7200,  1.9156, -1.5741],
        [ 0.7404, -1.1773,  1.3077, -0.7012,  1.9886, -1.3895],
        [-1.8221, -0.7920,  0.9091,  0.4478, -0.3373, -1.5661],
        [-0.9194,  0.3338,  0.7215, -1.2306,  0.9512, -0.1863],
        [-1.3199, -1.4841,  1.0171,  0.8665,  0.3624,  0.4318],
        [-1.7603, -0.5641,  0.3106, -2.7896,  1.6406,  1.9038]],
       requires_grad=True)
# src_embedding
Embedding(9, 6)
tensor([[[-0.9194,  0.3338,  0.7215, -1.2306,  0.9512, -0.1863],
         [ 0.7404, -1.1773,  1.3077, -0.7012,  1.9886, -1.3895],
         [-1.1004, -1.4062,  1.1152,  0.9054,  1.0759,  1.1679],
         [-1.1004, -1.4062,  1.1152,  0.9054,  1.0759,  1.1679]],
        [[-0.9194,  0.3338,  0.7215, -1.2306,  0.9512, -0.1863],
         [ 0.7404, -1.1773,  1.3077, -0.7012,  1.9886, -1.3895],
         [-0.0360, -1.6144,  0.9804,  0.4482,  1.8510,  0.3860],
         [-1.3199, -1.4841,  1.0171,  0.8665,  0.3624,  0.4318]]],
       grad_fn=<EmbeddingBackward>)

 

2. Position Embedding

 

注意力机制更多的是关注词与词之间的重要程度,而不关心句子中词语位置的顺序关系。

 

例如:“从北京开往济南的列车”与“从济南开往北京的列车”,词向量表示并不能对两句话中的“北京”进行区分,其编码是一样的。但是在真实语境中,两个词语所表达的语义并不相同,第一个表示的是起始站,另一个表示的是终点站,两个词所表达的语义信息并不相同。

 

因此以 Attention 结构为主的大规模模型都需要位置编码来辅助学习顺序信息。

 

Transformer 模型通过对输入向量额外添加位置编码来解决这个问题 。 Transformer 模型中采用正弦位置编码 。 利用正弦和余弦函数来生成位置编码信息,将位置编码信息与词嵌入的值相加,作为输入送到下一层。

 

计算公式如下所示,其中 pos 代表行,i 代表列,d_model 代表每个位置索引用多长的向量表示。

 

偶数列:

 

奇数列:

 

代码如下:

 

import torch
from torch import nn
from torch.nn import functional as F
import numpy as np
max_word_idx = 8  # 特征序列和目标序列的单词库由8种单词组成
model_dim = 6  # wordembedding之后,每个单词用长度为6的向量来表示
# ------------------------------------------------------ #
#(1)构建序列,序列的字符以索引的形式表示
# ------------------------------------------------------ #
# 指定序列长度
src_len = torch.Tensor([2, 4]).to(torch.int32)  # 特征序列的长度为2
tgt_len = torch.Tensor([4, 3]).to(torch.int32)  # 目标序列的长度为2
# 特征序列种有2个句子,第一个句子包含2个单词,第二个句子有4个单词
print(src_len, tgt_len)  # tensor([2, 4]) tensor([4, 3])
# 创建序列,句子由八种单词构成,用1~8来表示
src_seq = [ torch.randint(1, max_word_idx, (L,)) for L in src_len ]  # 创建特征序列
tgt_seq = [ torch.randint(1, max_word_idx, (L,)) for L in tgt_len ]  # 创建目标序列
print(src_seq, tgt_seq)
# [tensor([6, 4]), tensor([6, 4, 1, 7])]    # 特征序列,第一个句子有2个单词,第二个句子有4个单词
# [tensor([4, 2, 1, 3]), tensor([6, 5, 1])]  # 目标特征,第一个句子有4个单词,第二个句子有3个单词
# 每个句子的长度都不一样,需要填充0变成相同长度
new_seq = []  # 保存padding后的序列
for seq in src_seq:  # 遍历特征序列中的每个句子
    sent = F.pad(seq, pad=(0, max(src_len)-len(seq)))  # 右侧填充0保证所有句子等长
    sent = torch.unsqueeze(sent, dim=0)  # 变成二维张量[max_src_len]==>[1, max_src_len]
    new_seq.append(sent)  # 保存padding后的序列
for seq in tgt_seq:  # 遍历目标序列中的每个句子
    sent = F.pad(seq, pad=(0, max(tgt_len)-len(seq)))
    sent = torch.unsqueeze(sent, dim=0)  # 变成二维张量[max_tgt_len]==>[1, max_tgt_len]
    new_seq.append(sent)  # 保存padding后的序列
# 由于特征序列和目标序列都保存在list中,变成tensor类型,在axis=0维度堆叠
src_seq = torch.cat(new_seq[:2], dim=0)  # 特征序列
tgt_seq = torch.cat(new_seq[2:], dim=0)  # 目标序列
print(src_seq, src_seq.shape)  # 查看特征序列 shape=[2,4], 序列中有2个句子,每个句子4个单词
print(tgt_seq, tgt_seq.shape)  # 目标序列同上
'''
src_seq = [[6, 4, 0, 0], [6, 4, 1, 7]]
src_seq.shape = [2, 4]
tgt_seq = [[4, 2, 1, 3], [6, 5, 1, 0]]
tgt_seq.shape = [2, 4]
'''
# ------------------------------------------------------ #
#(2)position-embadding 奇数列使用cos,偶数列使用sin
# 正余弦位置编码的泛化能力较强、具有对称性、每个位置的embedding是确定的
# ------------------------------------------------------ #
# ==1== embedding
# 构造行矩阵, pos对应序列的长度, 特征序列中每个句子包含4个单词
pos_mat = torch.arange(max(src_len))  # 对应句子中的每个单词的位置
# 变成二维矩阵,每一行是一样的
pos_mat = torch.reshape(pos_mat, shape=[-1,1])  # shape=[4,1]
print(pos_mat)
# 构造列矩阵, 对应公式中的2i/d_model
# 每个单词用长度为6的向量来表示(d_model=6),而i代表特征向量中的每一列,2i代表偶数列
i_mat = torch.arange(0,model_dim,2).reshape(shape=(1,-1)) / model_dim
print(i_mat)  # tensor([[0.0000, 0.3333, 0.6667]])
# 公式中的10000的i_mat次方
i_mat = torch.pow(10000, i_mat)
print(i_mat)  # tensor([[  1.0000,  21.5443, 464.1590]])
# 初始化位置编码,4行6列的张量,4代表序列长度(一句话中单词个数),6代表特征列个数(一个单词用长度为6的向量表示)
pe_embedding_tabel = torch.zeros(size=(max(src_len), model_dim))
print(pe_embedding_tabel)
# 偶数列
pe_embedding_tabel[:, 0::2] = torch.sin(pos_mat / i_mat)
print(pe_embedding_tabel)
# 奇数列
pe_embedding_tabel[:, 1::2] = torch.cos(pos_mat / i_mat) 
print(pe_embedding_tabel)  # 完成正余弦位置编码
# 实例化embedding层,对每句话中的4个单词使用长度为6的向量来编码
pe_embedding = nn.Embedding(num_embeddings=max(src_len), embedding_dim=model_dim)
print(pe_embedding.weight)
# 改写embedding层的权重,并且训练过程中不更新权重
pe_embedding.weight = nn.Parameter(pe_embedding_tabel, requires_grad=False)
print(pe_embedding.weight)  # shape=[4,6]
# ==2== 位置索引
# 构建句子中每个单词的位置索引
src_pos = [torch.unsqueeze(torch.arange(max(src_len)), dim=0) for _ in src_len]
tgt_pos = [torch.unsqueeze(torch.arange(max(tgt_len)), dim=0) for _ in tgt_len]
print(src_pos,  # [tensor([[0, 1, 2, 3]]), tensor([[0, 1, 2, 3]])]
      tgt_pos)  # [tensor([[0, 1, 2, 3]]), tensor([[0, 1, 2, 3]])]
 
# 将列表类型变成tensor类型,在axis=0维度concat
src_pos = torch.cat(src_pos, dim=0)
tgt_pos = torch.cat(tgt_pos, dim=0)
print(src_pos,  # tensor([[0, 1, 2, 3], [0, 1, 2, 3]]) 
      tgt_pos)  # tensor([[0, 1, 2, 3], [0, 1, 2, 3]])
# 位置编码, 最长的一句话中有4个单词,每个单词的位置用长度为6的向量来表示
src_pe_embedding = pe_embedding(src_pos)
tgt_pe_embedding = pe_embedding(tgt_pos)
print(src_pe_embedding.shape)  # torch.Size([2, 4, 6])
print(src_pe_embedding)

 

构造特征序列和目标序列的方法和第一小节一样,就不赘述了。

 

Position Embedding 是对句子中单词的位置索引做的编码,而 Word Embedding 是对句子中的单词做编码。

 

首先初始化一个 4 行 6 列的矩阵,其中行代表位置索引,列代表每个位置用多少长的向量来表示 。根据公式, 奇数列用 cos 函数代替,偶数列用 sin 函数代替 。得到正余弦编码后的张量。接下来 实例化 nn.Embedding(), 将随机初始化的 embedding 层的权重矩阵换成正余弦位置编码后的权重 , 并且在训练过程中不更新位置权重 。如下面第一个矩阵所示。

 

然后 构造特征序列中句子的每个单词的位置索引 src_pos ,每个句子包含 4个单词,因此单词位置索引就是 [0,1,2,3],其中 src_pos.shape = [2, 4] 代表特征序列有 2 个句子,每个句子有 4 个单词位置索引 。 经过 Position Embedding 层之后,shape 变成 [2, 4, 6] ,代 表特征序列中有 2 个句子,每个句子包含 4 个单词位置,每个单词位置由长度为 6 的特征向量来表示 。如下面第二个矩阵所示。

 

# pe_embedding.weight (正余弦位置编码)
tensor([[ 0.0000,  1.0000,  0.0000,  1.0000,  0.0000,  1.0000],
        [ 0.8415,  0.5403,  0.0464,  0.9989,  0.0022,  1.0000],
        [ 0.9093, -0.4161,  0.0927,  0.9957,  0.0043,  1.0000],
        [ 0.1411, -0.9900,  0.1388,  0.9903,  0.0065,  1.0000]])
# src_pe_embedding
tensor([[[ 0.0000,  1.0000,  0.0000,  1.0000,  0.0000,  1.0000],
         [ 0.8415,  0.5403,  0.0464,  0.9989,  0.0022,  1.0000],
         [ 0.9093, -0.4161,  0.0927,  0.9957,  0.0043,  1.0000],
         [ 0.1411, -0.9900,  0.1388,  0.9903,  0.0065,  1.0000]],
        [[ 0.0000,  1.0000,  0.0000,  1.0000,  0.0000,  1.0000],
         [ 0.8415,  0.5403,  0.0464,  0.9989,  0.0022,  1.0000],
         [ 0.9093, -0.4161,  0.0927,  0.9957,  0.0043,  1.0000],
         [ 0.1411, -0.9900,  0.1388,  0.9903,  0.0065,  1.0000]]])

 

3. self_attention_Mask

 

这里介绍 Encoder 中 Muti_head_attention 中的 mask 方法

 

由于每个特征句子的长度不同,经过 padding 之后每个句子的长度一致。在特征序列中,第一个句子只包含 2 个单词,用 1 来表示,后两个填充的位置用 0 值来表示。因此将特征序列表示为 [[1, 1, 0, 0], [1, 1, 1, 1]],其 shape=[2, 4]

 

接下来 构建邻接矩阵 shape=[2, 4, 4] ,其中有 4 行和 4 列的单词, 邻接矩阵中每个元素代表两两单词之间的对应关系 ,若为 1 则代表有效单词,若为 0 则代表无效单词,是通过 padding 得到的。

 

接下来只要 将邻接矩阵中所有元素为 0 的区域都打上掩码, 将该位置的元素值变得非常小 。

 

代码如下:

 

import torch
from torch.nn import functional as F
# ------------------------------------------------------ #
# 构造一个mask shape=[batch, max_src_len, max_src_len], 值为1或负无穷
# ------------------------------------------------------ #
# 指定序列长度
src_len = torch.Tensor([2, 4]).to(torch.int32)  # 特征序列的长度为2
# 特征序列有2个句子,第一个句子的长度为2,第二个句子的长度为4
print(src_len)  # tensor([2, 4])
# 构建有效编码器的位置, 如:第一句话只包含2个单词,那幺只有前2个元素的值为1
valid_encoder_pos = [torch.ones(L) for L in src_len]
print(valid_encoder_pos)  # [tensor([1., 1.]), tensor([1., 1., 1., 1.])]
# 由于在训练时要求每个句子包含的单词数量相同,因此通过padding将所有特征句子的长度都变成最大有效句子长度
new_encoder_pos = []  # 保存padding后的句子
for sent in valid_encoder_pos:  # 遍历每个句子
    sent = F.pad(sent, pad=(0, max(src_len)-len(sent)))  # 右侧填充0保持序列长为4
    sent = torch.unsqueeze(sent, dim=0)  # 变成二维张量[max_src_len]==>[1, max_src_len]
    new_encoder_pos.append(sent)  # 保存padding后的序列
valid_encoder_pos = torch.cat(new_encoder_pos, dim=0)
print(valid_encoder_pos)  # tensor([[1., 1., 0., 0.],[1., 1., 1., 1.]])
# [2,4] ==> [2,4,1]
valid_encoder_pos = torch.unsqueeze(valid_encoder_pos, dim=-1)
# 邻接矩阵得到矩阵之间的对应关系 [2,4,1]@[2,1,4]==>[2,4,4]
valid_encoder_pos_matrix = torch.bmm(valid_encoder_pos, valid_encoder_pos.transpose(1,2))
print(valid_encoder_pos_matrix)  # 第一个句子只有两个有效单词,后面两个单词都是padding,
# 得到无效矩阵, 为1的位置都是padding得到的, 是无效的
invalid_encoder_pos_matrix = 1 - valid_encoder_pos_matrix
# 变成布尔类型, True代表无效区域,需要mask
mask_encoder_self_attention = invalid_encoder_pos_matrix.to(torch.bool)
print(mask_encoder_self_attention)
# 构造输入特征,2个句子,每个句子4个单词,每个单词用长度为4的向量表示
score = torch.randn(2, 4, 4)
# 对mask中为True的地方,对应score中的元素都变成很小的负数
masked_score = score.masked_fill(mask_encoder_self_attention, -1e10)
print(score)
print(masked_score)

 

下面的第一个矩阵是经过 padding 后的特征序列的邻接矩阵;第二个矩阵是随机生成的输入序列; 第三个矩阵是经过掩码后的序列 , 将 mask 的元素值变得非常小,这样在计算交叉熵损失时,经过 softmax 函数后这些做过 padding 的元素变得非常小,在反向传播过程中对模型的整体影响较小。

 

# 邻接矩阵,0代表是是经过padding后的区域
tensor([[[1., 1., 0., 0.],
         [1., 1., 0., 0.],
         [0., 0., 0., 0.],
         [0., 0., 0., 0.]],
        [[1., 1., 1., 1.],
         [1., 1., 1., 1.],
         [1., 1., 1., 1.],
         [1., 1., 1., 1.]]])
# 随机构造的输入特征score, shape=[2,4,4]
tensor([[[-0.1509, -0.2514, -0.5393,  2.0241],
         [-0.1525, -1.9199,  0.6847, -1.8795],
         [ 1.0322,  0.0772,  0.9992, -0.1082],
         [ 1.4347,  1.4084, -0.6897, -0.2518]],
        [[-0.0109,  0.0328,  1.5458,  0.9872],
         [ 0.0314, -1.3659, -0.6441, -1.6444],
         [-0.0487,  0.0438,  0.0576, -1.1691],
         [ 0.3475, -0.1329, -1.0455, -0.9671]]])
# 打上 mask 之后的 score
tensor([[[-1.5094e-01, -2.5137e-01, -1.0000e+10, -1.0000e+10],
         [-1.5255e-01, -1.9199e+00, -1.0000e+10, -1.0000e+10],
         [-1.0000e+10, -1.0000e+10, -1.0000e+10, -1.0000e+10],
         [-1.0000e+10, -1.0000e+10, -1.0000e+10, -1.0000e+10]],
        [[-1.0883e-02,  3.2843e-02,  1.5458e+00,  9.8725e-01],
         [ 3.1395e-02, -1.3659e+00, -6.4410e-01, -1.6444e+00],
         [-4.8689e-02,  4.3825e-02,  5.7644e-02, -1.1691e+00],
         [ 3.4751e-01, -1.3290e-01, -1.0455e+00, -9.6713e-01]]])

Be First to Comment

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注