赞
踩
大家好,今天和各位分享一下 Transformer 中的 Encoder 部分涉及到的知识点:Word Embedding、Position Embedding、self_attention_Mask
本篇博文是对上一篇 《Transformer代码复现》的解析,强烈建议大家先看一下:https://blog.csdn.net/dgvv4/article/details/125491693
由于 Transformer 中涉及的知识点比较多,之后的几篇会介绍 Decoder 机制、损失计算、实战案例等。
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]]])
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。