目录
1.3 Scaled Dot-Product Attention
1.4.4 matmul for new vector z1
1.4.6 Attention和Self-Attention区别
1.4.8 masked self-attention 实现-1
1.6 Multi-head Self-Attention (MHA)
1.6.1 multi-head self-attention algorithm
1.6.2 multi-head self-attention意义
1.6.3 pytorch实现 multi-head attention
1.7.1 positional encoding algorithm
2. Transformer(Encoder-Decoder Architecture)
2.2.2 为什么Encoder给予Decoders的是K、V矩阵
2.4.2 词向量input必须是one-hot吗?换成word2vec可以提升模型performance吗?
extension: python面向对象--封装(class类、__init__初始化函数)
2.5.4 masked multi-head self-attention
2.6 Huggingface transformers package
参考
1. Attention Mechanism
1.1 Background
1.1.1 时代背景
理论基础:类似于CNN中"感受野"receptive field的思想,attention mechanism从全部到只关注重点。
大数据背景,什么数据都有,重要的,不重要的。但model很难自己区分重要的内容和不重要的内容,--> attention mechanism.
1.1.2 模型背景
problem:在机器翻译时,RNN(极限50词)、LSTM(极限200词)存在长序列依赖问题long-term dependecies、无法做并行计算
solution:self-attention,突破RNNs等序列sequence模型的固有缺陷,捕获长距离信息依赖,生成具有句法特征和语义特征的新向量。
- 句法特征。
- 语义特征。
1.2 Attention Algorithm
本质:QKV加权求和。
权重矩阵Wq * () = Q ; Wk * () = K; Wv * () = V,这才是attention计算中的qkv。
q1通过与k1计算相似度s1,
- Note that,qk相乘最终结果得到一个一维向量!!虽然某些attention codes中直接将k定义为一个参数nn.Parameter(torch.Tensor(attn_size, 1)),但qk相乘的最终结果是要得到一个一维相似度向量,所以将k定义为参数不影响计算。
- KV一般同源
- 从dict数据结构理解,键值对key-value同源,query和key向量积得到相似度,然后用这个相似度矩阵来对value做加权求和!
softmax后得到注意力概率a1,即k1代表的单词(KV一般同源)在q1单词新的向量表示中的占比,最后实现是通过加权求和,q1新的向量表示z1=a1*v1 + a2*v2。即长距离强相关的单词信息融入到q1新向量表示中,克服了RNN长距离相关单词在q1新向量表示中信息丢失的问题。
self-attention: QKV相乘(QKV同源),QK相乘得到相似度A,AV相乘得到注意力值Z。
,我们一般使用点乘的方式。
1. 通过点乘的方式计算Q和K里的每一个元素的相似度,就可以拿到Q和k1的相似值s1,Q和k2的相似值s2,Q和kn的相似值sn。
2. 做一层softmax(s1, s2,...,sn),就可以得到概率(a1, a2,...,an)。进而就可以找出哪个对Q而言更重要
3. 还得进行一个汇总,即加权求和的过程。(a1,a2,...,an) * (v1,v2,...,vn) = (a1*v1 + a2*v2 + ...+ an*vn) a1是一个数值0.3, 0.4等、v1是一个向量哈
这样的话,就得到了一个新的矩阵V'。
一般K=V,在transformer里,K可以!= V,但是K和V之间一定具有某种联系,这样QK的点乘才能指导V哪些重要,哪些不重要。
1.3 Scaled Dot-Product Attention
- QK相乘求相似度,做一个scale放缩(未来做softmax时概率分布避免出现极端情况)
- 然后,做softmax得到概率
- 新的向量表示了K和V(K==V),然后这种表示还暗含了Q的信息(于Q而言,K里面重要信息)
1.3.1 attention scale缩放:
是K的维度
是为了防止因为数据太过离散而导致softmax计算出来的概率不稳定
softmax:
51, 49 --> 0.51, 0.4
80, 20 --> 0.999, 0.0001
a1和a2之间的差额越大,这个概率就越离谱。因为softmax在梯度更新时的缺陷。
加:80/8, 20/8 --> 0.9, 0.1
1.4 Self-Attention 自注意力机制
self-attention的关键在于,不仅仅是K≈V≈Q来源于同一个X,这三者是同源的
通过X找到X里面的关键点
1.4.1 Q、K、V的获取
并不是K=V=Q=X,而是通过三个参数
V = {v1, v2,...vn}
1.4.2 matmul for similarity
1.4.3 softmax + scale
1.4.4 matmul for new vector z1
z1表示的就是thinking新的向量表示。
- 对于thinking,初始词向量为x1
- 现在通过thinking machines这句话去查询这句话里每一个单词和thinking之间的相似度。
新的z1依然是thinking的词向量表示,只不过这个词向量的表示蕴含了thinking machines这句话对于thinking而言哪个更重要的信息。
1.4.5 self-attention summary
1.4.6 Attention和Self-Attention区别
注意力机制attention是一个很宽泛的概念,QKV相乘就是注意力,但是它没有规定QKV怎么来的。
- 通过一个查询变量Q,去找到V里面比较重要的东西。
- 假设K==V,然后QK相乘求相似度A,然后AV相乘得到注意力值Z,这个Z就是V的另一种形式表示。
- Q可以是任何一种东西,V也是任何一个东西,K往往和V同源。
- 它没有规定QKV怎么来,只规定QKV怎么做
自注意力机制self-attention是注意力机制的一个子集
- 本质上QKV可以看作是相等的,同源,来源于同一个X
- 对于一个词向量,做的是空间上的对应。乘上参数矩阵,做线性变换,依然代表X
- 词向量是不固定的,目前没有任何模型可以说一定能生活准备代表某个单词的词向量。
扩展:交叉注意力机制
Q和K不同源,但是K和V同源,同源代表着一定意义上的相等
QKV不固定,你自己定义QKV,可以自己赋予新的名字(e.g. 小猫注意力),但attention计算的本质不变。
1.4.7 Selft-attention和RNNs的区别
RNN、LSTM存在长序列依赖问题,无法做并行
self-attention得到新的词向量具有句法特征和语义特征(表征更完善)
- 句法特征
self-attention会和句子中的每一个单词做注意力计算,解决了长序列依赖问题,无论句子多长,第一个单词和最后一个单词都能产生联系。
- 语义特征
不做注意力,its的词向量就是单纯的its,没有任何附加信息。
也就是说,its有law这层意思,而通过注意力机制得到的its词向量,则会包含一定的laws信息
self-attention计算量太大,计算复杂度
1.4.8 masked self-attention 实现-1
import torch.nn.functional as F
def self-attention(query, key, value, dropout=None, mask=None):
d_k = query.size(-1)
scores = torch.matmul(query, key.transpose(-1, -2)) / math.sqrt(d_k)
# mask的操作在QK之后,softmax之前
if mask is not None:
mask.cuda()
scores = scores.masked_fill(mask == 0, -1e9)
self_attn = F.softmax(scores, dim=-1)
if dropout is not None:
self_attn = dropout(self_attn)
return torch.matmul(self_attn, value), self_attn
1.4.9 self-attention 实现-2
import torch
import torch.nn as nn
class SelfAttention(nn.Module):
def __init__(self, d_model, nhead):
super(SelfAttention, self).__init__()
self.d_model = d_model
self.nhead = nhead
self.query_linear = nn.Linear(d_model, d_model)
self.key_linear = nn.Linear(d_model, d_model)
self.value_linear = nn.Linear(d_model, d_model)
self.softmax = nn.Softmax(dim=-1)
def forward(self, input):
# input: (batch_size, sequence_length, d_model)
query = self.query_linear(input)
key = self.key_linear(input)
value = self.value_linear(input)
# compute attention scores
attention_scores = torch.matmul(query, key.transpose(-2, -1))
attention_scores = attention_scores / torch.sqrt(torch.tensor(self.d_model, dtype=attention_scores.dtype))
# apply softmax to get attention weights
attention_weights = self.softmax(attention_scores)
# compute the final attention output
attention_output = torch.matmul(attention_weights, value)
return attention_output
1.5 Masked Self-Attention
掩码自注意力
why要在self-attention上做这个改进?
生成模型,在生成单词时,是一个一个生成的。
当我们做生成任务时,我们也想对生成的这个单词做注意力计算。但是,生成的句子是一个一个单词生成的。很简单,不能用未来的预期结果预测现在的
e.g. I have a dream
1. I 第一次注意力计算,只有I
2. I have 第二次注意力计算,只有I和have
3. I have a
4. I have a dream
5. I have a dream <eos>
掩码自注意力机制
自注意力机制明确知道这句话有多少个单词,并且一次给足,就相当于预知未来指导现在穿越、降维打击?就很扯。。而masked掩码是分批次给,最后一次才给足。
- 掩码后1
- 掩码后2
1.6 Multi-head Self-Attention (MHA)
multi-head attention concanation 拼接方式其实有两种:
- 将input_dimensin/heads_num = dimension_subhead, e.g. 256/8=32,然后将32-d embedding 输入到MHA. 经过拼接后,32+32+...+32=256,再经过projection Wo: 256 ->256. 先拼接再映射!input->W_q1,W_q2-> d_q1=32, d_q2=32->d_out=64
- 将input_dimension分别输入到 MHA中,然后做一个变换unsqueeze,(batch_size, input_dim) -> (batch_size, n_heads, input_dim/n_heads),然后对each head做projection Wo,将映射后的所有head dim concatenation. 先映射再拼接!input->W_q->d_q=64 -> d_q1=32, d_q2=32 -> d_out=64
https://proceedings.neurips.cc/paper/2017/file/3f5ee243547dee91fbd053c1c4a845aa-Paper.pdf
self-attention是attention的一个具体做法。给定一个x,通过self-attention model,得到一个z,这个z就是对x的新的表征(词向量),z这个词向量相比较x拥有了句法特征和语义特征。
z相比较x有了提升,通过multi-head self-attention得到的z',相比较z又有了进一步的提升!
multi-head,多头,就是多次,一般用h=8表示。
attention: 输入x,乘以参数矩阵Wq, Wk, Wv(one linear transfermation),得到Query,Key,Value,然后输入到放缩点积attention。
multi-head, 多头,x分为heads份,维度=d_model/heads,表示并行parallel做多次。而且每次线性变换的w是不一样的。然后将h次的scaled dot-product attention结果进行拼接,虽然拼接后d_output维度为d_model,但还要再进行一次线性变换(参数矩阵矩阵为Wo=d_model*d_model),这才是multi-head attention最终的输出结果的结果。可以允许模型在不同的表示子空间里学习到相关的信息。
- 拼接方式1:dimension / heads_num
1.6.1 multi-head self-attention algorithm
1. 对于X,我们不是直接拿X得到Z,而是把X分成了8块(8头),经过8次线性变换(8个不同的Wq,Wk,Wv),得到z0-z7。Note that embed_dim
will be split across num_heads
(i.e. each head will have dimension embed_dim // num_heads
).
MultiheadAttention — PyTorch 2.0 documentation
8个不同的w会将(x0,x1,...,x7)映射到8个不同的子空间进行attention计算。
2. 然后把z0-z7拼接起来,再做一次线性变换(改变维度)得到z。
1.6.2 multi-head self-attention意义
机器学习本质:y=wx+b,线性变换。
深度学习:y=σ(wx+b),非线性。改变空间上的位置坐标,让一个维度空间上不合理的点变得合理。
非线性变换的本质:改变空间上的位置坐标,任何一个点都可以在维度空间上找到,通过某个手段,让一个不合理的点(位置不合理)变得合理。
multi-head self-attention把X切分成8块,这样一个原先在一个位置上的X,去了空间上的8个子空间subspaces,找到更合理的位置z'。
1.6.3 pytorch实现 multi-head attention
__init__()函数,声明初始化变量、模块modules和函数functions。
forward函数,放入输入数据input vector,传出输出output。
X输入的第一个维度是batch,因为训练模型时一个批度一个批度的输入数据。
这里的全连接层进行线性变换,取代w矩阵的功能,原本应该是8组不同的w矩阵,这里用一个大的全连接层进行变换,然后通过维度变换转换成8个头的形式,输入到self-attention模块里。
比如,[batch, 32, 512] # 每个batch里有32个词,每个词有512维。
将其切分成[batch, 8, 32, 64] # 将512维切分成8个头、64维
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
def self_attention(query, key, value, dropout=None, mask=None):
d_k = query.size(-1)
scores = torch.matmul(query, key.transpose(-1, -2)) / math.sqrt(d_k)
# mask的操作在QK之后,softmax之前
if mask is not None:
mask.cuda()
scores = scores.masked_fill(mask == 0, -1e9)
self_attn = F.softmax(scores, dim=-1)
if dropout is not None:
self_attn = dropout(self_attn)
return torch.matmul(self_attn, value), self_attn
class MultiHeadAttention(nn.Module):
# 声明初始化变量和函数functions
def __init__(self, head, d_model, dropout=0.1):
super(MultiHeadAttention, self).__init__()
'''
:param head: 头数,默认8
:param d_model: 输入的维度
:param query: Q
:param key: K
:param value: V
'''
assert (d_model % head ==0) # 取余
self.d_k = d_model // head # 将输入X平均分成8个头
self.head = head
self.d_model = d_model
# 自注意力机制QKV同源,线性变换
self.linear_query = nn.Linear(d_model, d_model) # linear线性层(input, output),中间暗含着w矩阵
self.linear_key = nn.Linear(d_model, d_model)
self.linear_value = nn.Linear(d_model, d_model)
self.linear_out = nn.Linear(d_model, d_model)
self.dropout = nn.Dropout(p=dropout)
self.attn = None # 用于接收self-attention function 返回的attn 概率
# forward函数传入input vector,传出输出
def forward(self, query, key, value, mask=None):
if mask is not None:
# 多头注意力机制的线性变换层是4维,是把query[batch_size, frame_num, d_model]变成[batch_size, -1, head, d_k]
# 在1,2维交换变成[batch_size, head, -1, d_k],所以mask要在第一维添加一维,与后面的self attention计算维度一样
mask = mask.unsqueeze(1)
# 输入的size(0)是batch,[batch,]
n_batch = query.size(0)
# 多头需要对输入X切分成多头, query==key==value. [batch, 32, 512] -> [batch, 8, 32, 64]
query = self.linear_query(query).view(n_batch, -1, self.head, self.d_k).transpose(1, 2) # [batch, 8, 32, 64]
key = self.linear_key(key).view(n_batch, -1, self.head, self.d_k).transpose(1, 2)
value = self.linear_value(value).view(n_batch, -1, self.head, self.d_k).transpose(1, 2) # []
x, self.attn = self_attention(query, key, value, dropout=self.dropout, mask=mask)
# 变成三维,或者说是concat head. [batch, 8, 32, 64] -> [batch, 32, 512]
x = x.transpose(1, 2).contiguous().view(n_batch, -1, self.head * self.d_k)
return self.linear_out(x)
1.7 Positional Encoding
Attention mechanism解决了长序列依赖问题、可以并行。
缺点:计算成本太大。
体现不出词与词之间存在的顺序关系order,位置编码的问题。
RNN、LSTM是包含词顺序关系的。self-attention对于每个词都是无位置关系的,把每个词的顺序打乱,得到的注意力值依然不变。
加上一个positional encoding
通过t1告诉你,x1在前面,x2在x1后面
1.7.1 positional encoding algorithm
pos代表input embedding在整体句子中的位置,i代表embedding维度里元素的位置,即2i表示维度里的偶数项,用了sin函数;2i+1表示维度里的奇数项,用了cos函数
sin和cos函数存在和差化积公式。
即sin(pos+k)=sin(pos)*cos(k) + cos(pos)*sin(k),cos(pos+k)=cos(pos)*cos(k) - sin(pos)*sin(k)
pos+k是pos位置与k位置的位置向量线性组合,2i--偶数维度用sin函数,2i+1--奇数维度用cos函数,这样的线性组合意味着位置向量中蕴含了相对位置信息。
e.g. 我爱你,现在做第三个词“你”的位置编码
pos + k = 3 = 1+2
3 = 1+2 --> 1*2+2*1
当变成你爱我,依然做第三个词“我”的位置编码
pos + k = 3 = 1+2 -->1*2+2*1
虽然都是1*2+2*1,但前面包含的单词位置信息不一样了,“我爱”、“你爱”,得到的位置编码不一样了。
实际应用中,transformer中positional encoding效果并不理想 --> bert中使用自学习的一种方式
1.7.2 positional encoding 实现
偶数项用sin函数,奇数项用cos函数,词向量维度一定是偶数维
输入X的embedding和positional encoding没有关系,比如:“我爱你”、“你爱我”,无论第一个词是啥,它的positional encoding都是一样的。
class PositionalEncoding(nn.Module):
def __init__(self, dim, dropout, max_len=5000):
super(PositionalEncoding, self).__init__()
# 偶数项用sin函数,奇数项用cos函数,位置一定是成双成对的
if dim % 2 != 0:
raise ValueError("Cannot use sin/cos positional encoding with"
"odd dim (got dim={:d})".format(dim))
"""
构建位置编码pe
pe公式为:PE(pos, 2i/2i+1) = sin/cos(pos/10000^{2i/d_{model})
"""
# max_len是解码器生成句子的最长的长度,即生成初始化一个为空的postional encoding vector
pe = torch.zeros(max_len, dim)
position = torch.arange(0, max_len).unsqueeze(1)
div_term = torch.exp((torch.arange(0, dim, 2, dtype=torch.float) *
-(math.log(10000.0) / dim)))
pe[:, 0::2] = torch.sin(position.float() * div_term) # pe第一维表示多少个词
pe[:, 1::2] = torch.cos(position.float() * div_term)
pe = pe.unsqueeze(1)
self.register_buffer('pe', pe)
self.drop_out = nn.Dropout(p=dropout)
self.dim = dim
def forward(self, emb, step=None):
emb = emb * math.sqrt(self.dim)
if step is None:
# X的embedding vector和positional vector没有关系
emb = emb + self.pe[:emb.size(0)] # 表示第几个词的embedding
else:
emb = emb + self.pe[step]
emb = self.drop_out(emb)
return emb
2. Transformer(Encoder-Decoder Architecture)
transformer其实就是attention的一个堆叠,分成两部分:Encoder和Decoder。
Nx(乘)的意思是,transformer编码器里面又有N个小编码器(默认N=6)。通过6个编码器,对词向量一步又一步的强化(增强)详见下节
seq2seq模型,序列(编码器encoder)到序列(解码器decoder),比如:一句话到一句话。
- 编码器encoder:把输入变成一个词向量(Self-Attention)
- 解码器decoder:得到编码器输出的词向量后,生成翻译结果。
机器翻译流程(transformer)
给定一个输入,给出一个输出(输出是输入的翻译结果)
“我是一个学生” --> (通过transformer) I am a student
了解transformer,就是了解transformer里的小的编码器(Encoder)和小的解码器(Decoder)。
FFN(feed Forward Network):前馈神经网络,本质就是w2(σ(w1x+b1)) + b2
编码器包括两个子层:self-attention、Feed Forward
每一个子层的传输过程都会有一个(残差网络 + 归一化)
2.1 Transformer 编码器 Encoder
thinking --> 得到green x1(词向量,可以通过one-hot、word2vec得到) + positional encoding,给x1赋予位置信息 --> 黄色x1
-->输入到Self-Attention子层中,做注意力机制(x1、x2拼接起来的一句话做),得到z1(x1与x1、x2拼接起来的句子做了自注意力机制的词向量,表征的仍然是thinking),也就是说z1拥有了位置属性、句法特征、语义特征。
--> 残差网络Add(避免梯度消失)
“梯度消失”,self-attention里面有很多wx+b,比如w3(w2(w1+b1)+b2)+b3,如果w1, w2, w3特别小,0.00...1,三者联乘,w结果基本为0,x就相当于没了 <--三阶函数的梯度是二阶,二阶函数的梯度是一阶
残差网络:加上x,让x梯度求导后不会没了。w3(w2(w1+b1)+b2)+b3 + x
--> LayerNorm归一化
做标准化,限制区间,避免梯度爆炸(如果w1,w2,w3较大)。在数据挖掘中,如果某一特征太过离散,数值分布在几十到几千之间,对参数w带来挑战,影响模型效果。
--> 得到深粉色z1
--> Feed Forward(前面每一步走在做线性变换wx+b,线性变化的叠加永远都是线性变化(线性变换就是在空间中放缩平移),激活函数非线性的提出是划时代的,通过feed forward中的Relu做多次非线性变换,这样的空间变换可以无线拟合空间中任何一种状态了),得到r1(是thinking新的表征)
2.1.1 Encoder 总结
Encoder就是在做词向量,这不过这个词向量更加优秀。因为它每一步很合理,很完美,一比之下,word2vec就是渣渣!
让这个词向量能够更加准确的表示这个单词、这句话
?未来有一个东西专门做词向量,而不是像transformer一样定位在机器翻译。
Bert
2.2 Transformer 解码器 Decoder
编码器 Encoder:转换成词向量。Encoder就是让计算机能够合理地认识人类世界客观存在的一些东西。(这个词向量不可能是100%正确的,是不确定的,只是尽可能精确的)
解码器会接收解码器生成的词向量,然后通过这个词向量去生成翻译结果。
2.2.1 为什么Decoder需要做mask
解码器的self-attention编码已经生成的单词,即masked self-attention。这是为了解决训练阶段和测试阶段不匹配
假如目标词“我是一个学生”,
训练阶段:人工标注机器翻译语料--<I'm a student, 我是一个学生>是已知的,decoder self-attention需要对“我是一个学生”做masked计算。
- 如果不做masked,每次训练阶段,decoder都会获得全部的信息。
- 如果做masked,self-attention第一次对“我”做计算;第二次对“我是”做计算,...,masked是为了训练阶段与测试阶段预测未知目标词行为保持一致。
测试阶段:目标翻译词是未知的,无法直接拿未知的翻译结果输入decoder
1. 目标词未知,假如目标词是“我是一个老师”,self-attention第一次对“我”做计算。
2. 第二次对“我是”做计算
3. .....
测试阶段:encoder每生成一个词,decoder就获得多一点
2.2.2 为什么Encoder给予Decoders的是K、V矩阵
--> Encoder-Decoder Attention中QKV不同源,交叉注意力计算
Q来自Decoder,K=V来自Encoder,Q通过对全信息KV进行查询,后面经过linear维度变换和softmax概率计算,得出下一个词。
--> Linear层映射为词表的维度。因为线性变化wx+b,神经元个数即embedding维度
--> softmax得到最大概率的单词
Q是查询变量,K=V是源语句,通过部分(生成的词)去全部(源语句)的里面挑重点。 如果反过来,Q是源语句,KV是已经生成的词,那么源语句去已经生成词里找下一个翻译词的信息,这根本是找不到的!
解决了以前seq2seq框架的问题(lstm做编码器,lstm做解码器,这种方法去生成词,每一次生成词,都是通过C的全部信息去生成)
2.3 Transformer 动态流程
- 生成一个词
- 生成全部单词
2.4 Transformer疑问解答
2.4.1 为什么用张量tensor?
因为向量是2维,张量tensor是3维以上
2.4.2 词向量input必须是one-hot吗?换成word2vec可以提升模型performance吗?
词向量不一定必须是one-hot,可以使word2vec、ELMo、Bert等其他词向量,但这不会改变模型效果,因为BP阶段会不断改变向量对应权重w,最终达到optimal,唯一区别在于BP时更新幅度和收敛熟读不一样。
e.g. 初始权重w0=5,optimal wf=10。one-hot编码不完善,需要更新的次数多一点,而bert经过预训练,可能需要的epoches更少,直接从7开始BP。
所以,input embedding可以随机初始一个向量,transformer会反向更新,它一定能将帮你更新到一个确切的位置。-->让AI领域发生翻天覆地的变化。
2.4.3 transformer强大之处
transformer的核心是self-attention、multi-head attention
生成式语言模型的核心是masked multi-head attention
lstm其实也有上述特性,但是它们模型不能做得很大、没有用self-attention;复杂的数据、大型的数据、大量的数据、没有标签的数据,lstm model无能为力。
transformer、bert 有强大的特征提取能力,不论是文本还是视频!!!
2.5 pytorch 实现Transformer
自己写transformer没有任何实际意义!!
这里只是为了熟悉transformer代码结构
实际应用请直接做调包侠!!
extension: python面向对象--封装(class类、__init__初始化函数)
面向对象,即java的编程思想。一个数据类型抽象化创建为对象(class),这个对象拥有name和property两种属性,面向对象思想编程有三大特点:
- 数据封装。在于复用
- 继承
- 多态
python编程基础(五): 面向对象--封装、继承_天狼啸月1990的博客-CSDN博客_自行封装ready.py
init函数声明初始变量和函数,
forward函数使用init变量、输入x和输出y
当前可以直接在forward函数中声明变量,但这就写死了后面应用修改太麻烦!!!
e.g.
l1 = LayerNorm()
....
l1(128)
l1(128)
l1(128)
------------------------------------------------
l1 = LayerNorm(128)
....
l1()
l1()
l1()
2.5.1 残差函数add、归一化Norm
首先有一个norm函数
class LayerNorm(nn.Module):
def __init__(self, feature, eps=1e-6):
"""
param feature: self-attention的x的大小
param eps:做平滑
"""
super(LayerNorm, self).__init__()
self.a_2 = nn.Parameter(torch.ones(feature))
self.b_2 = nn.Parameter(torch.zeros(feature))
self.eps = eps
def forward(self, x):
mean = x.mean(-1, keepdim=True)
std = x.std(-1, keepdim=True)
return self.a_2 * (x-mean)/(std+self.eps) + self.b_2
然后norm里面做残差,会输入(x和淡粉色z1,残差值),输出一个紫粉色的z1
class SublayerConnection(nn.Module):
"""
这不仅仅做了残差,这是把残差和layernorm一起做了
"""
def __init__(self, size, dropout=0.1):
super(SublayerConnection, self).__init__()
# 第一步做 layernorm
self.layer_norm = LayerNorm(size)
# 第二步做 dropout,随机置0,防止过拟合
self.dropout = nn.Dropout(p=dropout)
def forward(self, x, sublayer):
"""
param x: self-attention input
param sublayer: self-attention output
"""
return self.dropout(self.layer_norm(x + sublayer(x)))
2.5.2 Feed Forward layer(FFN)
layer_norm: 对X做标准化,深度学习中的常规操作。
w2(relu(w1x+b1))+b2。如果对线性变换或线性回归比较熟悉的话,b1和b2有时会放到w1和w2里面,默认X多增加了[1,1,1,...]这一维。
class PositionWiseFeedForward(nn.Module):
'''
layer_norm: 对X做标准化
FFN: w2(relu(w1x+b1))+b2
'''
def __init__(self, d_model, d_ff, dropout=0.1):
super(PositionWiseFeedForward, self).__init__()
self.w_1 = nn.Linear(d_model, d_ff)
self.w_2 = nn.Linear(d_ff, d_model)
self.layer_norm = nn.LayerNorm(d_model, eps=1e-6)
self.dropout_1 = nn.Dropout(dropout)
self.relu = nn.ReLU()
self.dropout_2 = nn.Dropout(dropout)
def forward(self, x):
inter = self.dropout_1(self.relu(self.w_1(self.layer_norm)))
output = self.dropout_2(self.w_2(inter))
return output
2.5.3 Linear层 + Softmax层
class Generator(nn.Module):
def __init__(self, d_model, vocab_size):
super(Generator, self).__init__()
self.linear = nn.Linear(d_model, vocab_size)
def forward(self, x):
return F.log_softmax(self.linear(x), dim=-1)
2.5.4 masked multi-head self-attention
只是对multi-head self-attention的输入做了一个masking操作
# masking multi-head attention
def pad_mask(src, r2l_trg, trg):
if trg is not None: # trg 是标签y
# 把trg丢到subsequent_mask,对它进行掩码处理,改变它的形状,只能看前几个
trg_mask = subsequent_mask(trg.size(1)).type_as(src_image_mask.data)
# 掩码处理 masking, 即生成一个上三角矩阵,只给你看到生成词的信息
def subsequent_mask(size):
"""
mask out subsequent positions.
"""
attn_shape = (1, size, size)
mask = np.triu(np.ones(attn_shape), k=1).astype('uint8')
return (torch.from_numpy(mask) == 0).cuda()
2.5.5 构建编码器 encoder
# 这里是一层编码器
class EncoderLayer(nn.Module):
def __init__(self, size, attn, feed_forward, dropout=0.1): # 实例化出来的attn, feed_forward层
super(EncoderLayer, self).__init__()
self.attn = attn
self.feed_forward = feed_forward
# 克隆,module复制n次。transformer编码器encoder里有2个残差连接
self.sublayer_connection = clones(SublayerConnection(size, dropout), 2)
def forward(self, x, mask):
x = self.sublayer_connection[0](x, lambda x: self.attn(x, x, x, mask)) # forward前向传播把数据流串起来
return self.sublayer_connection[1](x, self.feed_forward)
# transformer encoder里面有6个编码器encoderlayer
class Encoder(nn.Module):
# encoder_layer = EncoderLayer()
def __init__(self, n, encoder_layer):
super(Encoder, self).__init__()
self.encoder_layer = clones(encoder_layer, n) # 克隆n次,transformer里面是6层
def forward(self, x, src_mask):
for layer in self.encoder_layer: # 编码器前向传播,前向传播一次得到新的x,再放入下一层编码器,共有6层
x = layer(x, src_mask)
return x
2.5.6 解码器 decoder
# 首先构建一层解码器
class DecoderLayer(nn.Module):
def __init__(self, size, attn, feed_forward, sublayer_num, dropout=0.1):
super(DecoderLayer, self).__init__()
self.attn = attn # 这样的写好处是,没有写死。可以根据传入参数,确定是multi-head self-attention还是 masked multi-head self-attention
self.feed_forward = feed_forward
self.sublayer_connection = clones(SublayerConnection(size, dropout), sublayer_num)
def forward(self, x, memory, src_mask, trg_mask, r2l_memory=None, r2l_trg_mask=None):
x = self.sublayer_connection[0](x, lambda x: self.attn(x, x, x, trg_mask))
x = self.sublayer_connection[1](x, lambda x: self.attn(x, memory, memory, src_mask)) # memory是编码器输出
# 双向解码器
# if r2l_memory is not None:
# x = self.sublayer_connection[-2](x, lambda x: self.attn(x, r2l_memory, r2l_memory, r2l_trg_mask))
return self.sublayer_connection[-1](x, self.feed_forward)
# 再构建6层解码器
class R2L_Decoder(nn.Module): # 双向解码器--正向解码器
def __init__(self, n ,decoder_layer):
super(R2L_Decoder, self).__init__()
self.decoder_layer = clones(decoder_layer, n)
def forward(self, x, memory, src_mask, r2l_trg_mask):
for layer in self.decoder_layer:
x = layer(x, memory, src_mask, r2l_trg_mask)
return x
# # 双向解码器--反向解码器
# class L2R_Decoder(nn.Module):
#
# def __init__(self, n, decoder_layer):
# super(L2R_Decoder, self).__init__()
# self.decoder_layer = clones(decoder_layer, n)
#
# def forward(self, x, memory, src_mask, trg_mask, r2l_memory, r2l_trg_mask):
# for layer in self.decoder_layer:
# x = layer(x, memory, src_mask, trg_mask, r2l_memory, r2l_trg_mask)
# return x
2.5.7 整体Transformer
__init__,初始化一些小的结构layer
class ABDTransformer(nn.Module):
def __init__(self, vocab, d_feat, d_model, d_ff, n_heads, n_layers, dropout, feature_mode,
device='cuda', n_heads_big=128):
super(ABDTransformer, self).__init__()
self.vocab = vocab # 词表大小
self.device = device # 是否使用GPU
self.feature_mode = feature_mode # 多模态
c = copy.deepcopy()
# attn_no_heads = MultiHeadAttention(0, d_model, dropout) # 无头multi-head attention = 一层注意力
attn = MultiHeadAttention(n_heads, d_model, dropout)
# attn_big = MultiHeadAttention(n_heads_big, d_model, dropout) # 大头 multi-head attention
feed_forward = PositionWiseFeedForward(d_model, d_ff)
if feature_mode == 'one':
self.src_embed = FeatEmbedding(d_feat, d_model, dropout) # 视频嵌入
elif feature_mode == 'two':
pass
elif feature_mode == 'three':
pass
elif feature_mode == 'four':
pass
self.trg_embed = TextEmbedding(vocab.n_vocabs, d_model)
self.pos_embed = PositionalEncoding(d_model, dropout)
# 初始化编码器
self.encoder = Encoder(n_layers, EncoderLayer(d_model, c(attn), c(feed_forward), dropout))
# 初始化一个正向解码器
self.l2r_decoder = L2R_Decoder(n_layers, DecoderLayer(d_model, c(attn), c(feed_forward),
sublayer_num=4, dropout=dropout))
# 初始化一个反向解码器
# self.r2l_decoder = R2L_Decoder(n_layers, DecoderLayer(d_model, c(attn), c(feed_forward),
# sublayer_num=4, dropout=dropout))
# 生成器generator,就是linear + softmax layer
self.generator = Generator(d_model, vocab.n_vocabs)
# 单模态encode
def encode(self, src, src_mask, feature_mode_two=False):
x1 = self.image_src_embed(src[0])
x1 = self.pos_embed(x1)
x1 = self.encoder_big(x1, src_mask[0]) # 将 X embedding + positional encoding 输入编码器
# 正向解码器 decode
def l2r_decode(self, trg, memory, src_mask, trg_mask, r2l_memory, r2l_trg_mask):
x = self.trg_embed(x) # 解码器输入
x = self.pos_embed(x)
return self.l2r_decoder(x, memory, src_mask, trg_mask, r2l_memory, r2l_trg_mask)
# # 反向解码器
# def r2l_decode(self, trg, memory, src_mask, trg_mask, r2l_memory, r2l_trg_mask):
# pass
# 构建forward函数
def forward(self, src, r2l_trg, trg, mask):
src_mask, r2l_pad_mask, r2l_trg_mask, trg_mask = mask
if self.feature_mode == 'two' or 'three' or 'four':
encoding_outputs = self.encode(src, enc_src_mask)
l2r_outputs = self.l2r_encode(trg, encoding_outputs, dec_src_mask[1], trg_mask, r2l_outputs, r2l_pad_mask)
else:
raise "没有输出"
# r2l_pred = self.generator(r2l_outputs)
l2r_pred = self.generator(l2r_outputs)
2.6 Huggingface transformers package
2.6.1 background
Transformer,由于其优越的性能表现,在工业界使用的越来越广泛,同时,配合迁移学习理论,越来越多的Transformer预训练模型和源码库逐渐开源,Huggingface就是其中做的最为出色的一家机构。Huggingface是一家在NLP社区做出杰出贡献的纽约创业公司,其所提供的大量预训练模型和代码等资源被广泛的应用于学术研究当中。Huggingface所开源的Transformers package提供了数以千计针对于各种任务的预训练模型模型,开发者可以根据自身的需要,选择模型进行训练或微调,也可阅读api文档和源码, 快速开发新模型。
pip install transformers
2.6.2 AutoClass介绍
problem:transformers package中有上百个算法,有bert model对应的BertModel类,有bart model对应的BartModel,当我们使用对应的pre-trained model是,都必须先找到对应类名,然后进行实例化,非常麻烦!
transformers库提供了AutoClass高级对象,只需要知道预训练模型的名称name或所在目录dir,就可以快速便捷create pre-trained model。
AutoClass类只能通过from_pretrained()方法创建模型。比如:
from transformers import AutoModel
model = AutoModel.from_pretrained("./models/bert-base-chinese")
print(type(model))
2.6.3 分词器对象 AutoTokenizer
Huggingface的transformers库中提供了高级API对象--AutoTokenizer,用来加载预训练模型的分词器。
Note that: 预训练模型分词器和模型model是配套使用的!
比如:我们使用的预训练模型时"bert-base-chinese",那么,预先加载的分词器也必须使用"bert-base-chinese"
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("bert-base-chinese")
sentence = "床前明月光"
tokenizer(sentence)
-----------------------------------------------------
{'input_ids': [101, 2414, 1184, 3209, 3299, 1045, 102], 'token_type_ids': [0, 0, 0, 0, 0, 0, 0], 'attention_mask': [1, 1, 1, 1, 1, 1, 1]}
tokenizer内部还提供其他丰富的参数用于实现多种功能:
tokenizer(
["床前明月光", "床前明月光,疑是地上霜。"],
padding=True, # 长度不足max_length时是否进行填充
truncation=True, # 长度超过max_length时是否进行截断
max_length=10,
return_tensors="pt", #指定返回数据类型,pt:pytorch的张量,tf:TensorFlow的张量
)
2.6.4 配置文件 AutoConfig
problem: 每一类算法模型的框架结构不一样、超参数配置也不一样。如果每次加载预训练模型,都要用户手动找到对应的配置项,就很麻烦!
所以AutoClass提供了专门的配置管理入口--AutoConfig。
from transformers import AutoConfig
config = AutoConfig.from_pretrained("./models/bert-base-chinese")
通过config实例,我们可以对配置项进行修改。
config.num_hidden_layers=5
print(config)
修改之后的参数,如果后续需要再次使用,可以保存到本地。
config.save_pretrained("./models/bert-base-chinese")
2.6.5 创建预训练模型 AutoModel
通过AutoModel类,创建pre-trained model最简单的方法就是直接传入模型名称或本地路径。
from transformers import AutoModel
model = AutoModel.from_pretrained("./models/bert-base-chinese")
# 也可以在加载模型时,指定配置项实例
model = AutoModel.from_pretrained("./models/bert-base-chinese", config=config)
2.7 “调包侠”实现transformer、bert
2.7.1 “调包侠”实现transformer
2.7.2 “调包侠”实现bert
# -----------------------------------------Bert embeddings-----------------
from transformers import AutoTokenizer, AutoModel
tokenizer = AutoTokenizer.from_pretrained('bert-base-uncased')
bert_model = AutoModel.from_pretrained('bert-base-uncased')
df['bert_embeddings'] = df.text.apply(lambda x: bert_model(**tokenizer(x, return_tensors='pt'))[0][0][0][:128])
df_bert = df[['event_id','bert_embeddings']]
2.8 bert project application
https://www.cnblogs.com/chenhuabin/p/16997607.html
- 自定义数据集
- 自定义网络
- 训练 training
- run model
参考
推荐:
小白友好,简单直白,逻辑性强,但有好好几个点讲错了,-_-||,比如多头和解码器QKV部分
- 实战篇:03 Transformer 中的多头注意力(Multi-Head Attention)Pytorch代码实现_哔哩哔哩_bilibili
- 英文教程:Tutorial 6: Transformers and Multi-Head Attention — UvA DL Notebooks v1.2 documentation
- Huggingface:https://www.cnblogs.com/chenhuabin/p/16997607.html
66 使用注意力机制的seq2seq【动手学深度学习v2】_哔哩哔哩_bilibili
(我不推荐李沐大神的attention课程,仁者见仁智者见智。专家是指能把复杂数学概念直白讲解到老农民都能听懂的程度,虽然李沐已经简化了数学公式,还是对小白不友好;认真用心程度低,虽然沐神是卡耐基的博士,但cs高度不代表课件用心投入程度高,即便是沐神,认真投入度低,事情做出来也不好;依托RNN讲解self-attention,不理解机器翻译seq-2-seq,听attention云里雾里的。)