目录
从整体网路结构来看,分为三个部分:编码层,解码层,输出层
1、编码层
位置编码实现
attention mask实现
Encoder layer:
1. 多头注意力
2、前馈神经网络
transformer的encoder和decoder的差别
为什么需要Mask处理
如何进行Mask处理
四、Transformer的特点
4.1 并行处理
4.2Encoder与Decoder的联系
面试题
WordPiece子字级标记算法
BPE(Basic Periodontal Examination)
BERT
BERT的三个Embedding直接相加会对语义有影响吗?
Pytorch:
view(参数a,参数b,…):
主要用于Tensor维度的重构,即返回一个有相同数据但不同维度的Tensor如果某个参数为-1,则表示该维度取决于其它维度,由Pytorch自己补充
repeat
1、x.repeat(a)
列数乘以a倍,对x进行横向赋值
2、x.reshape(a,b)
列数先乘以b倍,再行数乘以a倍。即对x先横向复制b倍,再纵向复制a倍
expand:
扩展某个维度,返回当前张量在某维扩展更大后的张量。扩展(expand)张量不会分配新的内存,只是在存在的张量上创建一个新的视图(view),一个大小(size)等于1的维度扩展到更大的尺寸。
unsqueeze(dim):
起升维的作用,参数表示在哪个地方加一个维度
(179条消息) 【Pytorch】unsqueeze() 方法_pytorch unsqueeze_想变厉害的大白菜的博客-CSDN博客
参考:(179条消息) Transformer源码详解(Pytorch版本)逐行讲解_transformer代码pytorch_Queen_sy的博客-CSDN博客
(179条消息) Transformer模型入门详解及代码实现_transformer模型代码-CSDN博客
超细节的BERT/Transformer知识点 - 知乎 (zhihu.com)
class Transformer(nn.Module):
def __init__(self):
super(Transformer, self).__init__()
self.encoder = Encoder() ## 编码层
self.decoder = Decoder() ## 解码层
self.projection = nn.Linear(d_model, tgt_vocab_size, bias=False) ## 输出层 d_model 是我们解码层每个token输出的维度大小,之后会做一个 tgt_vocab_size 大小的softmax
def forward(self, enc_inputs, dec_inputs):
## 这里有两个数据进行输入,一个是enc_inputs 形状为[batch_size, src_len],主要是作为编码段的输入,一个dec_inputs,形状为[batch_size, tgt_len],主要是作为解码端的输入
## enc_inputs作为输入 形状为[batch_size, src_len],输出由自己的函数内部指定,想要什么指定输出什么,可以是全部tokens的输出,可以是特定每一层的输出;也可以是中间某些参数的输出;
## enc_outputs就是主要的输出,enc_self_attns这里没记错的是QK转置相乘之后softmax之后的矩阵值,代表的是每个单词和其他单词相关性;
enc_outputs, enc_self_attns = self.encoder(enc_inputs)
## dec_outputs 是decoder主要输出,用于后续的linear映射; dec_self_attns类比于enc_self_attns 是查看每个单词对decoder中输入的其余单词的相关性;dec_enc_attns是decoder中每个单词对encoder中每个单词的相关性;
dec_outputs, dec_self_attns, dec_enc_attns = self.decoder(dec_inputs, enc_inputs, enc_outputs)
## dec_outputs做映射到词表大小
dec_logits = self.projection(dec_outputs) # dec_logits : [batch_size x src_vocab_size x tgt_vocab_size]
return dec_logits.view(-1, dec_logits.size(-1)), enc_self_attns, dec_self_attns, dec_enc_attns
每个编码器层由两个子层连接结构组成:
第一个子层包括一个多头自注意力层和规范化层以及一个残差连接;
第二个子层包括一个前馈全连接层和规范化层以及一个残差连接;
Encoder 部分包含三个部分: 1. 词向量embedding 2. 位置编码部分 3. 注意力层及后续的前馈神经网络
class Encoder(nn.Module):
def __init__(self):
super(Encoder, self).__init__()
## 这个其实就是去定义生成一个矩阵,大小是 src_vocab_size * d_model
self.src_emb = nn.Embedding(src_vocab_size, d_model)
## 位置编码情况,这里是固定的正余弦函数,也可以使用类似词向量的nn.Embedding获得一个可以更新学习的位置编码
self.pos_emb = PositionalEncoding(d_model)
## 使用ModuleList对多个encoder进行堆叠,因为后续的encoder并没有使用词向量和位置编码,所以抽离出来;
self.layers = nn.ModuleList([EncoderLayer() for _ in range(n_layers)])
def forward(self, enc_inputs):
# enc_inputs: [batch_size x source_len]
# 通过src_emb,进行索引定位,enc_outputs输出形状是[batch_size, src_len, d_model]
enc_outputs = self.src_emb(enc_inputs)
# 位置编码,把两者相加放入到这个函数
enc_outputs = self.pos_emb(enc_outputs.transpose(0, 1)).transpose(0, 1)
# get_attn_pad_mask是为了得到句子中pad的位置信息,在计算自注意力和交互注意力的时候去掉pad符号的影响
enc_self_attn_mask = get_attn_pad_mask(enc_inputs, enc_inputs)
enc_self_attns = []
for layer in self.layers:
enc_outputs, enc_self_attn = layer(enc_outputs, enc_self_attn_mask)
enc_self_attns.append(enc_self_attn)
return enc_outputs, enc_self_attns
位置编码的实现其实很简单,直接对照着公式去敲代码就可以,下面这个代码只是其中一种实现方式; 从理解来讲,需要注意的就是偶数和奇数在公式上有一个共同部分,我们使用log函数把次方拿下来,方便计算; pos代表的是单词在句子中的索引,这点需要注意;比如max_len是128个,那么索引就是从0,1,2,...,127 假设我的demodel是512,2i那个符号中i从0取到了255,那么2i对应取值就是0,2,4...510
## 3. PositionalEncoding 代码实现
class PositionalEncoding(nn.Module):
def __init__(self, d_model, dropout=0.1, max_len=5000):
super(PositionalEncoding, self).__init__()
## 位置编码的实现其实很简单,直接对照着公式去敲代码就可以,下面这个代码只是其中一种实现方式;
## 从理解来讲,需要注意的就是偶数和奇数在公式上有一个共同部分,我们使用log函数把次方拿下来,方便计算;
## pos代表的是单词在句子中的索引,这点需要注意;比如max_len是128个,那么索引就是从0,1,2,...,127
##假设我的demodel是512,2i那个符号中i从0取到了255,那么2i对应取值就是0,2,4...510
self.dropout = nn.Dropout(p=dropout)
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
pe[:, 0::2] = torch.sin(position * div_term)## 这里需要注意的是pe[:, 0::2]这个用法,就是从0开始到最后面,补长为2,其实代表的就是偶数位置
pe[:, 1::2] = torch.cos(position * div_term)##这里需要注意的是pe[:, 1::2]这个用法,就是从1开始到最后面,补长为2,其实代表的就是奇数位置
## 上面代码获取之后得到的pe:[max_len*d_model]
## 下面这个代码之后,我们得到的pe形状是:[max_len*1*d_model]
pe = pe.unsqueeze(0).transpose(0, 1)
self.register_buffer('pe', pe) ## 定一个缓冲区,其实简单理解为这个参数不更新就可以
def forward(self, x):
"""
x: [seq_len, batch_size, d_model]
"""
x = x + self.pe[:x.size(0), :]
return self.dropout(x)
比如说,我现在的句子长度是5,在后面注意力机制的部分,我们在计算出来QK转置除以根号之后,softmax之前,我们得到的形状len_input * len*input 代表每个单词对其余包含自己的单词的影响力 所以这里我需要有一个同等大小形状的矩阵,告诉我哪个位置是PAD部分,之后在计算计算softmax之前会把这里置为无穷大; 一定需要注意的是这里得到的矩阵形状是batch_size x len_q x len_k,我们是对k中的pad符号进行标识,并没有对k中的做标识,因为没必要 seq_q 和 seq_k 不一定一致,在交互注意力,q来自解码端,k来自编码端,所以告诉模型编码这边pad符号信息就可以,解码端的pad信息在交互注意力层是没有用到的;
def get_attn_pad_mask(seq_q, seq_k):
batch_size, len_q = seq_q.size()
batch_size, len_k = seq_k.size()
# eq: equal函数,eq(0)当输入等于0时,该位置为1,否则为0,eq(zero) is PAD token
# [batch_size, 1, len_k], one is masking
pad_attn_mask = seq_k.data.eq(0).unsqueeze(1)
# [batch_size, len_q, len_k]
return pad_attn_mask.expand(batch_size, len_q, len_k)
class EncoderLayer(nn.Module):
def __init__(self):
super(EncoderLayer, self).__init__()
self.enc_self_attn = MultiHeadAttention()
self.pos_ffn = PoswiseFeedForwardNet()
def forward(self, enc_inputs, enc_self_attn_mask):
# 自注意力层,输入是enc_inputs,形状是[batch_size, seq_len_q, d_model]
enc_outputs, attn = self.enc_self_attn(enc_inputs, enc_inputs, enc_inputs, enc_self_attn_mask)
# enc_outputs: [batch_size x len_q x d_model]
enc_outputs = self.pos_ffn(enc_outputs)
return enc_outputs, attn
1. QKV映射
2. 计算多头attention矩阵
3. 获得attention输出
4. layerNorm + shortcut,残差连接,为了避免梯度消失
class MultiHeadAttention(nn.Module):
def __init__(self):
super(MultiHeadAttention, self).__init__()
self.W_Q = nn.Linear(d_model, d_q * n_heads)
self.W_K = nn.Linear(d_model, d_k * n_heads)
self.W_V = nn.Linear(d_model, d_v * n_heads)
self.linear = nn.Linear(n_heads * d_v, d_model)
self.layer_norm = nn.LayerNorm(d_model)
def forward(self, Q, K, V, attn_mask):
## 这个多头分为这几个步骤,首先映射分头,然后计算atten_scores,然后计算atten_value;
##输入进来的数据形状: Q: [batch_size x len_q x d_model], K: [batch_size x len_k x d_model], V: [batch_size x len_k x d_model]
residual, batch_size = Q, Q.size(0)
# (B, S, D) -proj-> (B, S, D) -split-> (B, S, H, W) -trans-> (B, H, S, W)
# [batch_size x n_heads x len_q x d_k]
q_s = self.W_Q(Q).view(batch_size, -1, n_heads, d_k).transpose(1,2)
k_s = self.W_K(K).view(batch_size, -1, n_heads, d_k).transpose(1,2)
v_s = self.W_V(V).view(batch_size, -1, n_heads, d_v).transpose(1,2)
# 将attn_mask重复n_heads次
# [batch_size, len_q, len_k],然后经过下面这个代码得到 新的attn_mask : [batch_size x n_heads x len_q x len_k],就是把pad信息重复了n个头上
attn_mask = attn_mask.unsqueeze(1).repeat(1, n_heads, 1, 1)
# 得到的结果有两个:context: [batch_size, n_heads, len_q, d_v], attn: [batch_size, n_heads, len_q, len_k]
context, attn = ScaledDotProductAttention()(q_s, k_s, v_s, attn_mask)
# context: [batch_size x len_q x n_heads * d_v]
context = context.transpose(1, 2).contiguous().view(batch_size, -1, n_heads * d_v)
output = self.linear(context)
# output: [batch_size x len_q x d_model]
# LayerNorm + shortcut
return self.layer_norm(output + residual), attn
注意力具体实现部分
attn_mask,把被mask的地方置为无限小,softmax之后基本就是0,对q的单词不起作用。
然后相似度矩阵score 经过Softmax后为attn矩阵 ,他的每一行之和等于1,再与Value矩阵相乘,得到权重矩阵context。
class ScaledDotProductAttention(nn.Module):
def __init__(self):
super(ScaledDotProductAttention, self).__init__()
def forward(self, Q, K, V, attn_mask):
# Q: [batch_size, n_heads, len_q, d_k]
# K: [batch_size, n_heads, len_k, d_k]
# V: [batch_size, n_heads, len_k, d_v]
# score: [batch_size, n_heads, len_q, len_k]
scores = torch.matmul(Q, K.transpose(-1, -2)) / np.sqrt(d_k)
# attn_mask,把被mask的地方置为无限小, 1e9,softmax之后基本就是0,对q的单词不起作用
# Fills elements of self tensor with value where mask is one.
scores.masked_fill_(attn_mask, -1e9)
attn = nn.Softmax(dim=-1)(scores)
# [batch_size, n_heads, len_q, len_k]*[batch_size, n_heads, len_k, d_v]=[batch_size, n_heads, len_q, d_v]
context = torch.matmul(attn, V)
return context, attn
class PoswiseFeedForwardNet(nn.Module):
def __init__(self):
super(PoswiseFeedForwardNet, self).__init__()
self.dense1 = nn.Linear(ffn_num_inputs, ffn_num_hiddens)
self.relu = nn.ReLU()
self.dense2 = nn.Linear(ffn_num_hiddens, ffn_num_outputs)
def forward(self, inputs):
residual = inputs # inputs : [batch_size, len_q, d_model]
output = self.dense2(self.relu(self.dense1(inputs)))
return self.layer_norm(output + residual)
1. decoder包含两个 Multi-Head Attention 层。
相当于每个位置获得encoder所有输入信息和前面已经翻译部分的信息
2. decoder最后有一个 Softmax 层计算下一个翻译单词的概率。
并行是相对而言的,对比rnn一次只能处理一个字符,transformer中的注意力机制enocder一次可以处理一整句话,不过decoder的时候还是一次处理一个字符。
decoder预测时一次一个字,训练的时候可以一次性计算从头到当前字符的向量。
注意encoder的输出并没直接作为decoder的直接输入。
encoder的输出包括隐向量以及K /Q /V。
decoders层中相较encoders层多了一个encoder-decoder attention模块,其计算跟多头自注意力计算也类似,只是它的Q是前一个decoder层的输出乘上新的参数矩阵进行转换得来的,K, V 则来自于与encoder的输出乘上新的参数矩阵进行转换得来的。
仔细想想其实可以发现,这里的交互模块就跟seq2seq with attention中的机制一样,目的就在于让Decoder端的单词(token)给予Encoder端对应的单词(token)“更多的关注(attention weight)”
4.3 Decoder的输入与输出
1.Self-Attention 的时间复杂度是怎么计算的?
Self-Attention时间复杂度:O(n^2,d) ,这里,n是序列的长度,d是embedding的维度。
2.Transformer为什么Q和K使用不同的权重矩阵生成,为何不能使用同一个值进行自身的点乘?
使用Q,K两个向量,而不是只用一个Q向量,这样引入非对称性,更具健壮性(Attention对角元素值不一定是最大的,也就是说当前位置对自身的注意力得分不一定最高)
3.Transformer计算attention的时候为何选择点乘而不是加法?两者计算复杂度和效果上有什么区别?
答:K和Q的点乘是为了得到一个attention score 矩阵,用来对V进行提纯。K和Q使用了不同的W_k, W_Q来计算,可以理解为是在不同空间上的投影。正因为 有了这种不同空间的投影,增加了表达能力,这样计算得到的attention score矩阵的泛化能力更高。
4.为什么在进行softmax之前需要对attention进行scaled(为什么除以dk的平方根)
当维度很大时,点积结果会很大,会导致softmax的梯度很小。
假设向量q,k满足各分量独立同分布,方差为1。它们的矩阵乘积将有均值为0,方差为dk,因此使用dk的平方根被用于缩放,因为,Q 和 K 的矩阵乘积的均值本应该为 0,方差本应该为1,这样可以获得更平缓的softmax。
5. 在计算attention score的时候如何对padding做mask操作?
对需要mask的位置设为负无穷,再与attention score进行相加,softmax时负无穷的位置score为0
6. 为什么在进行多头注意力的时候需要对每个head进行降维?
将原有的高维空间转化为多个低维空间并再最后进行拼接,形成同样维度的输出,借此丰富特性信息,降低了计算量
7.大概讲一下Transformer的Encoder模块?
答:输入嵌入-加上位置编码-多个编码器层(每个编码器层包含全连接层,多头注意力层和点式前馈网络层(包含激活函数层))
8.为何在获取输入词向量之后需要对矩阵乘以embedding size的开方?
embedding matrix的初始化方式是xavier init,这种方式的方差是1/embedding size,因此乘以embedding size的开方,使得embedding matrix的方差是1,在这个scale下可能更有利于embedding matrix的收敛。
Xavier init:
初始化为全0,当有隐藏层时,使用relu等激活函数,会导致梯度梯度为0,参数无法更新
理想的参数分布应该使得,输入输出的空间比较一致,输入稠密,输出稀疏,导致参数更新慢,输入稀疏,输出稠密,导致梯度爆炸,所以要使得输入输出方差尽可能相等
10.你还了解哪些关于位置编码的技术,各自的优缺点是什么?
1.在计算attention score和weighted value时各加入一个可训练的表示相对位置的参数。
2.在生成多头注意力时,把对key来说将绝对位置转换为相对query的位置
3.复数域函数,已知一个词在某个位置的词向量表示,可以计算出它在任何位置的词向量表示。前两个方法是词向量+位置编码,属于亡羊补牢,复数域是生成词向量的时候即生成对应的位置信息。
Transformer和Bert位置编码的区别
Transformer的位置编码是一个固定值,因此只能标记位置,但是不能标记这个位置有什么用。
BERT的位置编码是可学习的Embedding,因此不仅可以标记位置,还可以学习到这个位置有什么用。
BERT选择这么做的原因可能是,相比于Transformer,BERT训练所用的数据量充足,完全可以让模型自己学习。
11.简单讲一下Transformer中的残差结构以及意义。
答:encoder和decoder的self-attention层和ffn层都有残差连接。反向传播的时候缓解造成梯度消失。
12.为什么transformer块使用LayerNorm而不是BatchNorm?LayerNorm 在Transformer的位置是哪里?
答:多头注意力层和激活函数层之间。nlp领域认为句子长度不一致,并且各个batch的信息没什么关系,因此只考虑句子内信息的归一化,也就是LN。
13.简答讲一下BatchNorm技术,以及它的优缺点。
答:批归一化是对每一批的数据在进入激活函数前/后进行归一化,可以提高收敛速度,防止过拟合,防止梯度消失,增加网络对数据的敏感度。
优点
1.把输入强行拉到均值0,方差1的标准正态分布区间,使得非线性变换函数的输入值落到输入比较敏感区域,缓解梯度消失问题(因为对于sigmoid和softmax这类激活函数,输入过大或者过小,都会导致梯度很小)
2. 随着网络训练,每层参数变化后,导致后一层的输入发生变化,使得网络在训练中需要不停拟合不同分布的数据,训练难度加大
缺点
batch非常小的情况不适用
不等句长输入不适用
什么时候用?
收敛速度很慢,梯度消失/爆炸无法训练时
也可以在平时用来加速网络训练
BN训练和测试时的参数是一样的嘛?
训练时:是对每一批的训练数据进行归一化,也即用每一批数据的均值和方差。
测试时:比如进行一个样本的预测,就并没有batch的概念,因此,这个时候用的均值和方差是全量训练数据的均值和方差,这个可以通过移动平均法求得。
对于BN,当一个模型训练完成之后,它的所有参数都确定了,包括均值和方差,gamma和bata。
BN训练时为什么不用全量训练集的均值和方差呢?
因为用全量训练集的均值和方差容易过拟合,对于BN,其实就是对每一批数据进行归一化到一个相同的分布,而每一批数据的均值和方差会有一定的差别,而不是用固定的值,这个差别实际上能够增加模型的鲁棒性,也会在一定程度上减少过拟合。
也正是因此,BN一般要求将训练集完全打乱,并用一个较大的batch值,否则,一个batch的数据无法较好得代表训练集的分布,会影响模型训练的效果。
BN的参数和公式
增加两个可训练参数,让本层输入和上一层解耦
14.简单描述一下Transformer中的前馈神经网络?使用了什么激活函数?相关优缺点?
答:输入嵌入-加上位置编码-多个编码器层(每个编码器层包含全连接层,多头注意力层和点式前馈网络层(包含激活函数层))-多个解码器层(每个编码器层包含全连接层,多头注意力层和点式前馈网络层)-全连接层,使用了relu激活函数
15.Encoder端和Decoder端是如何进行交互的?
答:通过转置encoder_ouput的seq_len维与depth维,进行矩阵两次乘法,即q*kT*v输出即可得到target_len维度的输出
16.Decoder阶段的多头自注意力和encoder的多头自注意力有什么区别?
答:Decoder有两层多头注意力,encoder有一层多头注意力,
Decoder的第一层注意力是带mask的注意力,第二层多头注意力与encoder进行交互
17.Transformer的并行化提现在哪个地方?
答:Transformer的并行化主要体现在self-attention模块,在Encoder端Transformer可以并行处理整个序列,并得到整个输入序列经过Encoder端的输出,但是rnn只能从前到后的执行
18.Decoder端可以做并行化吗?
训练的时候可以,预测的时候不可以
19.简单描述一下wordpiece model 和 byte pair encoding,有实际应用过吗?
而现在大火的bert框架就是使用的WordPiece算法。WordPiece( Schuster和Nakajima,2012年 )最初用于解决日文和韩文语音问题,目前因在BERT中使用而闻名;它在许多方面与BPE相似,不同之处在于它基于似然而不是下一个最高频率对形成一个新的子字,这个似然值表示来两个子字的相关性,WordPiece会合并相关性最高的两个子字;WordPiece底层算法和代码尚未公开,这里简单叙述以下实现步骤。
1. 获得足够大的语料库。
2. 定义所需的子词词汇量。
3. 将单词拆分为字符序列。
4. 用文本中的所有字符初始化词汇表。
5. 根据词汇建立语言模型。
6. 通过将当前词汇表中的两个单元组合以将词汇表增加一个来生成新的子词单元。 从所有可能性中选择新的子词单位,这会在添加到模型时最大程度地增加训练数据的可能性。
7. 重复第5步,直到达到子词词汇量(在第2步中定义),或者似然性增加降至某个阈值以下。
由Sennrich等人介绍。 在2015年 ,它迭代地合并最频繁出现的字符或字符序列。 该算法大致是这样工作的:
1. 获得足够大的语料库。
2. 定义所需的子词词汇量。
3. 将单词拆分为字符序列,并附加一个特殊的标记,分别显示单词的开头或单词的结尾词缀/后缀。
4. 计算文本中的序列对及其频率。 例如,('t','h')具有频率X,('h','e')具有频率Y。
5. 根据最频繁出现的序列对生成一个新的子词。 例如,如果('t','h')在该对对中具有最高的频率,则新的子字单元将变为'th'。
6. 从第3步开始重复,直到达到子词词汇量(在第2步中定义)或下一个最高频率对为1。在示例中,语料库中的“ t”,“ h”将替换为“ th”,再次计算,最频繁的对再次获得,并再次合并。
BPE是一种贪婪的确定性算法,不能提供多个细分。 也就是说,对于给定的文本标记化文本始终是相同的。
两者主要区别:
BPE和WordPiece的区别在于如何选择两个子词进行合并。
BPE的词表创建过程:
WordPiece是贪心的最长匹配搜索算法。基本流程:
优点:
答“传统词表示方法无法很好的处理未知或罕见的词汇(OOV问题)
传统词tokenization方法不利于模型学习词缀之间的关系
BPE(字节对编码)或二元编码是一种简单的数据压缩形式,其中最常见的一对连续字节数据被替换为该数据中不存在的字节。后期使用时需要一个替换表来重建原始数据。
优点:可以有效地平衡词汇表大小和步数(编码句子所需的token次数)。
缺点:基于贪婪和确定的符号替换,不能提供带概率的多个分片结果。
源码实现
def tokenize(self, text):
"""Tokenizes a piece of text into its word pieces.
This uses a greedy longest-match-first algorithm to perform tokenization
using the given vocabulary.
For example:
input = "unaffable"
output = ["un", "##aff", "##able"]
Args:
text: A single token or whitespace separated tokens. This should have
already been passed through `BasicTokenizer`.
Returns:
A list of wordpiece tokens.
"""
def whitespace_tokenize(text):
"""Runs basic whitespace cleaning and splitting on a peice of text."""
text = text.strip()
if not text:
return []
tokens = text.split()
return tokens
output_tokens = []
for token in whitespace_tokenize(text):
chars = list(token)
if len(chars) > self.max_input_chars_per_word: #max_input_chars_per_word 默认为 100
output_tokens.append(self.unk_token) # 单词长度大于最大长度,用[UNK]表示单词
continue
is_bad = False
start = 0
sub_tokens = []
while start < len(chars):
end = len(chars)
cur_substr = None
while start < end: # 贪心的最长匹配搜索 end从最后一位往前遍历,每移动一位,判断start:end是否存在于词表中
substr = "".join(chars[start:end])
if start > 0: # 若子词不是从位置0开始,前面要加“##”
substr = "##" + substr
if substr in self.vocab:
cur_substr = substr
break
end -= 1
if cur_substr is None: #没有在词表中出现的子词,break
is_bad = True
break
sub_tokens.append(cur_substr)
start = end # 从上一子词的后一位开始下一轮遍历
if is_bad: #没有在词表中出现的子词(单词中的任何区域),用[unk]表示该词:比如“wordfi”,首先确定“word”为子词,后发现“fi”不存在在词表中,则最终用[UNK]表示“wordfi”
output_tokens.append(self.unk_token)
else:
output_tokens.extend(sub_tokens)
return output_tokens
20.Transformer训练的时候学习率是如何设定的?Dropout是如何设定的,位置在哪里?Dropout 在测试的需要有什么需要注意的吗?
LN是为了解决梯度消失的问题,dropout是为了解决过拟合的问题。在embedding后面加LN有利于embedding matrix的收敛。
21.bert的mask为何不学习transformer在attention处进行屏蔽score的技巧?
答:BERT和transformer的目标不一致,bert是语言的预训练模型,需要充分考虑上下文的关系,而transformer主要考虑句子中第i个元素与前i-1个元素的关系。
22.Transformer的非线性来自于哪里?
23.Transformer在两个地方进行了权重共享:
(1)Encoder和Decoder间的Embedding层权重共享
《Attention is all you need》中Transformer被应用在机器翻译任务中,源语言和目标语言是不一样的,但它们可以共用一张大词表,对于两种语言中共同出现的词(比如:数字,标点等等)可以得到更好的表示,而且对于Encoder和Decoder,嵌入时都只有对应语言的embedding会被激活,因此是可以共用一张词表做权重共享的。
论文中,Transformer词表用了bpe来处理,所以最小的单元是subword。英语和德语同属日耳曼语族,有很多相同的subword,可以共享类似的语义。而像中英这样相差较大的语系,语义共享作用可能不会很大。
但是,共用词表会使得词表数量增大,增加softmax的计算时间,因此实际使用中是否共享可能要根据情况权衡。
(2)Decoder中Embedding层和FC层权重共享。
Embedding层可以说是通过onehot去取到对应的embedding向量,FC层可以说是相反的,通过向量(定义为 x)去得到它可能是某个词的softmax概率,取概率最大(贪婪情况下)的作为预测值。
那哪一个会是概率最大的呢?在FC层的每一行量级相同的前提下,理论上和 x 相同的那一行对应的点积和softmax概率会是最大的(可类比本文问题1)。
因此,Embedding层和FC层权重共享,Embedding层中和向量 x 最接近的那一行对应的词,会获得更大的预测概率。实际上,Decoder中的Embedding层和FC层有点像互为逆过程。
通过这样的权重共享可以减少参数的数量,加快收敛。
24.不考虑多头的原因,self-attention中词向量不乘QKV参数矩阵,会有什么问题?
Self-Attention的核心是用文本中的其它词来增强目标词的语义表示,从而更好的利用上下文的信息。
self-attention中,sequence中的每个词都会和sequence中的每个词做点积去计算相似度,也包括这个词本身。
对于 self-attention,一般会说它的 q=k=v,这里的相等实际上是指它们来自同一个基础向量,而在实际计算时,它们是不一样的,因为这三者都是乘了QKV参数矩阵的。那如果不乘,每个词对应的q,k,v就是完全一样的。
在相同量级的情况下,qi与ki点积的值会是最大的(可以从“两数和相同的情况下,两数相等对应的积最大”类比过来)。
那在softmax后的加权平均中,该词本身所占的比重将会是最大的,使得其他词的比重很少,无法有效利用上下文信息来增强当前词的语义表示。
而乘以QKV参数矩阵,会使得每个词的q,k,v都不一样,能很大程度上减轻上述的影响。
当然,QKV参数矩阵也使得多头,类似于CNN中的多核,去捕捉更丰富的特征/信息成为可能。
25.Transformer训练的时候学习率是如何设定的?Dropout是如何设定的,位置在哪里?Dropout 在测试的需要有什么需要注意的吗?
学习率:warm up
dropout:
使用Dropout的模型在测试的时候是无需Dropout的。我们在建立模型的阶段是通过设置一个概率值P,把一部分节点dropout,但是在测试阶段,我们是无需再去跑模型的,没有Dropout,所有节点都在,这就和建立模型的阶段不一样,会有一定误差,所以在测试阶段我们会对模型的权重进行调整,让它乘上P,让它的性能和模型建立阶段相似。
但是这里会有一个问题,在测试阶段还有权重的计算量,一般我们是不希望在测试阶段还有太多的计算量。所以我们会做如下的变化,在模型建立阶段就让权重除于P,P是0-1的值,相当于乘一个倍数。这样去达到我们想要的效果。这里无论是去乘还是去除,都是为了保证使用或者不使用Dropout的期望值一样。使用了Dropout,其输出激活值的期望变为 p*a+(1-p)*0=pa,所以可以除p。
如果使用 Transformer 对不同类别的数据进行训练,数据集有些类别的数据量很大
(例如有 10 亿条),而大多数类别的数据量特别小(例如可能只有 100 条),此时如何训练
出一个相对理想的 Transformer 模型来对处理不同类别的任务?
重新选择评价指标
准确度在类别均衡的分类任务中并不能有效地评价分类器模型,造成模型失效,甚至会误导业务,造成较大损失。
最典型的评价指标即混淆矩阵Confusion Matrix
:使用一个表格对分类器所预测的类别与其真实的类别的样本统计,分别为:TP、FN、FP、TN
。包括精确度Precision、召回率Recall、F1得分F1-Score
等。
重采样数据集
使用采样sampling策略该减轻数据的不平衡程度。主要有两种方法
over-sampling
under-sampling
采样算法往往很容易实现,并且其运行速度快,并且效果也不错。在使用采样策略时,可以考虑:
Embedding的数学本质,就是以one hot为输入的单层全连接,也就是说,世界上本没什么Embedding,有的只是one hot。我们将token,position,segment三者都用one hot表示,然后concat起来,然后才去过一个单层全连接,等价的效果就是三个Embedding相加。原文链接:词向量与Embedding究竟是怎么回事?
在这里想用一个例子再尝试解释一下:
假设 token Embedding 矩阵维度是 [4,768];position Embedding 矩阵维度是 [3,768];segment Embedding 矩阵维度是 [2,768]。假设它的 token one-hot 是[1,0,0,0];它的 position one-hot 是[1,0,0];它的 segment one-hot 是[1,0]。
那这个字最后的 word Embedding,就是上面三种 Embedding 的加和。如此得到的 word Embedding,和concat后的特征:[1,0,0,0,1,0,0,1,0],再过维度为 [4+3+2,768] = [9, 768] 的全连接层,得到的向量其实就是一样的。
再换一个角度理解:
直接将三个one-hot 特征 concat 起来得到的 [1,0,0,0,1,0,0,1,0] 不再是one-hot了,但可以把它映射到三个one-hot 组成的特征空间,空间维度是 432=24 ,那在新的特征空间,这个字的one-hot就是[1,0,0,0,0…] (23个0)。
此时,Embedding 矩阵维度就是 [24,768],最后得到的 word Embedding 依然是和上面的等效,但是三个小Embedding 矩阵的大小会远小于新特征空间对应的Embedding 矩阵大小。
当然,在相同初始化方法前提下,两种方式得到的 word Embedding 可能方差会有差别,但是,BERT还有Layer Norm,会把 Embedding 结果统一到相同的分布。
BERT的三个Embedding相加,本质可以看作一个特征的融合,强大如 BERT 应该可以学到融合后特征的语义信息的。