深度学习 Transformer 中的 Encoder 机制,附Pytorch完整代码
Posted 立Sir
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深度学习 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]]])
以上是关于深度学习 Transformer 中的 Encoder 机制,附Pytorch完整代码的主要内容,如果未能解决你的问题,请参考以下文章