irpas技术客

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

网络投稿 4197

大家好,今天和各位分享一下 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]]])


1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,会注明原创字样,如未注明都非原创,如有侵权请联系删除!;3.作者投稿可能会经我们编辑修改或补充;4.本站不提供任何储存功能只提供收集或者投稿人的网盘链接。

标签: #深度学习encoder #大家好今天和各位分享一下 #transformer #中的 #Encoder #部分涉及到的知识点Word