【深度学习】(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=)

2. Position Embedding

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

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

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

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

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

偶数列:PE(pos, 2i) = sin(pos/10000^{2i/d_{model}})

奇数列:PE(pos, 2i+1) = cos(pos/10000^{2i/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]]])

你可能感兴趣的:(pyTorch深度学习,pytorch,深度学习,神经网络,transformer,nlp)