transformer模型,文本生成任务。 self-attention结构

前言

大家好,好久不见,终于又有机会来学习深度学习了, 这次我们通过全球人工智能大赛算法挑战赛的赛道一来学习。

2023全球人工智能技术创新大赛 - 算法挑战赛

赛道一的任务是 通过医疗图像的文字描述来生成医生的诊断描述。这是一个典型的文本生成任务, 因此transformer终于可以要打破bert在我心中的地位, 第一次登上历史舞台了。 

下图很好的展现了transformer,bert和GPT的关系。 transformer包括了一个encoder和一个decoder。 bert因为只需要取特征, 因此用的是encoder这部分, gpt要生成就取了decoder这部分。 事实上, 我认为decoder是更难懂的部分, 超级麻烦。 所以还是得通过推文。 我们这篇文章研究的就是transformer 本身, 即transformer是如何处理一个文字序列的。

transformer的输入: 

transformer是做翻译任务起家的, 所以我们的数据需要一段输入,和对应的文字输出。 

transformer和bert一样都是把序列作为输入, 最典型的就是一段文字。 这段文字会被分词,之后映射到 input_ids 和 output_ids。 这是我们输入的一段文字和输出的一段文字经过分词和tokenizer后的结果。  (可以先读bert的编码层)。 

一文读懂bert结构。_bert模型结构_亮子李的博客-CSDN博客

如果举个例子就是:  i love you  ->  我爱你

       i love you    -------- >  “i” “love” “you”  -------- > "999"   ,  "777",  "456"   

        我爱你  -------- >“我”, “爱”, “你”   ------> "100"   ,  "256",  "333"  

即先分词, 后用一个固定的数字来替换文字。 之后输出需要加上一些特殊的token,也就是特殊的id。 

如: 

sos_id ,  eos_id,   pad_id。 

sos_id: 输出的起始字符, 比如可以用1表示。 

eos_id: 输出的结束字符, 比如可以用2表示。 

pad_id: 填充id。 如果输入输出长度不够规定的标准长度, 就填充为0 。 (一个数据集一般会规定一个统一的处理长度。)

如假如我们要求处理长度 输入长度为8, 输出长度为6

那么:

 i love you    -------- > "999"   ,  "777",  "456"   --- "999"   ,  "777",  "456",  "0",  "0",  "0",  "0",  "0"

我爱你  -------- >“我”, “爱”, “你”   ------> "1", "100" , "256",  "333" ,"2" ,"0"

这就是transformer的输入。 为什么输出要这样处理, 具体请搜索李宏毅老师的课程。 self-attention。

从这里代码可以看出, transformer就包含两个结构 ,encoder和decoder。 

transformer的encoder: 

        关于self-attention 机制 ,你需要先读下面这篇博客。

一文读懂bert结构。_bert模型结构_亮子李的博客-CSDN博客

transformer的encoder基本和bert差不多。 我们象征性走一下流程。 

我们首先要对输入进行嵌入embedding。   

看forward 即运行的第一句。 

posit_index  即位置编码。 要转为位置embedding。 这里是可学习的全连接层: nn.linear(句子长度, 编码长度)。 有的时候是固定函数算的,而不是可学习的。 
padding_mask   即 输入的有效位置, 为1的地方表示无效。
token_embedding 即词embedding。 
attn_mask  全为0 . 大小为(输入长度, 输入长度) 一个方阵。 

 处理后变为embedding, 之后把他们全部输入到encoder中去。

值得注意的是与bert的不同之处:

        1,  这里要把输入转换维度 不是很懂。 后面感觉确实没啥用意义。在multi attention head那里同意变不就行了? 

        2, 没有用nn.linear来创建全连接, 而是定义了参数w和b直接用计算来做。 

q, k, v = linear(query, in_proj_weight, in_proj_bias).chunk(3, dim=-1)

 q,k,v用chunk函数创建。  注意 这里的q大小是(  length* batch* dim) 即长度,batch大小,编码维度。而w大小为 (3*dim,dim)  w和q相乘后会变为 (  length* batch* 3×dim) 

.chunk表示分块。 沿着dim 也就是最后一维 3*dim这一维。 分成三块 也就是Q,K,V。 真是麻烦这样子写, 

transformer的decoder: 

 transformer 输入有两个 , 一个是encoder输出的特征,一个是目标target , 也就是输出。 

看看decoder定义了哪些东西:

前几个说过了。  

d 就是编码维度。

n_token 是输入的值,即词表的大小。 

同样有解码器的位置编码 posit_embedding, 和编码器不共享。  

还附带输入的位置编码。 source_posit_embedding。 这个等会看看什么作用, 

定义了输出的linear

self.output = nn.Linear(d, n_token)

然后看前向。 

 先来个arange的下标列表, 之后用linear得到位置编码。 再加上token编码。 这里caption就是输出。 

attn_mask = self.generate_square_subsequent_mask(caption.shape[1]).to(caption.device)

 这一句用来生成一个对角矩阵, 大小为输出的 length*length  一个阵。 A[i][j]位置的数字可以理解为第i个token 对第j个token的注意力, 也就是i有多么关注j。  这里设计了一个下三角矩阵,例:

        1,-inf,-inf,-inf

        1,  1, -inf,-inf

        1,  1,   1, -inf

        1,  1,   1,   1

也就是说 第一个token 对后面的token都不关注, 这是为了完成输出时,每个token只看前面的token,不看后面的这个功能。 因为生成任务,只能根据前面生成的来生成后面的。  

经过两次维度转换进入decoder。 维度转换的必要性没那么强。

注意和encoder的区别。  encoder只需要输入src 这里加了target, 

这里是decoder的核心。 看到一个norm1 是让输出tgt通过自注意力层进行编码。 其他的与普通注意力都一样。 

attn_mask = attn_mask.masked_fill(key_padding_mask, float("-inf"))

 注意这一句, 表示的是将attn_mask 中那些  key_padding_mask为1的位置下标对应的值都改为“-“inf” 主要是因为 key_padding_mask为1的地方,表示是实际上没有输出 ,全是我们填充的,所以变为负无穷,这样softmax后就为0了。 

所以第一句就是用self-attention提取一下target的特征。 而且是遮盖上三角的方式, 

来到第二句

 这一句同样是自注意力机制处理, 不过不同的是要同时输入 src和tgt的特征。 

 

 看sa和mha的区别。   self_attn 和multihead_attn是一模一样的。 主要在于是输入换了。 

前三个参数对应的是q,k,v 也就是说, sa是全部用 tgt来产生 q,k,v

而mha是由tgt产生 q ,用输入的特征src来产生k,和v

也就是用tgt来询问输入的特征 问应该给每个输入的token'多少注意力。 

加上注意力是下三角形式的。  这样就可以做到, 用第一个词来预测下一个, 用前两个预测第三个。 

 来到第三句, 就是一个mlp。 之后经过多层att 得到输出。 

output即一个全连接层。 从特征长度到词表长度。 

 至此得到decoder也就是transformer的输出吗, 也就是预测的结果。 

预测时。 首先通过encoder得到输入的特征。 

在预测时, 是没有目标值的。 也就是target。 这里的caption就是本来output的位置, 

 可以看到forward直接走的infer函数。 我们也进入infer。 

创建两个矩阵, 第一个是batch*1  第二个就是batch。 看看干嘛的。 

 这句的意思是 从infer在返回forward。 但是不同的是outputs只是一个256*1的矩阵。 也就是只有一个token。 里面的值全部为1, 也就是说代表着sos_id 。  即意思就是用开始字符推断第一个字。 

当长度为1的target进入decoder后。

 这些的长度全部变为1。  也就是说,假设某轮target的长度为 t, 位置坐标只产生输入长度为t的位置。 编码只编t个token的。 

在进入 decoder后与之前一样的三步。

可以看下在交叉注意力时的维度变化。 

 因为这里的q是tgt提供的, 而k和v是输入提供的, 所以要走下面了。 

先把计算qkv的矩阵w拆为两个, 第一个是dim*dim 第二个是2×dim*dim。 dim就是编码维度。 

 看返回是两个相加。 看第一个。 用q对应的linaer权重q_w去处理q。 因为w是方阵, 所以q保持原来的shape不变。  依然为batch* t* dim 。  (不过这里batch和t在代码里换了位置,我们不管)。  所以前面这个是返回去是q, 后面chunk出来k和v。

现在q大小为  batch* t* dim ,k和v的大小为 batch*l_src*dim。  

q和K的转置乘得到 batch* t*l_src  再用这个 去乘以V得到 batch* t* dim 正好可以作为输出。

 得到输出后,取最后一个的输出。 

 这一句就是要来判断是否输出结束了。 

首先看这个batch的每一个预测是否和结束id eos相等。 用

torch.ne(outputs[:,-1], eos_id).long() 这个函数。

所以这句代码的意思就是 保持前面每一轮是否结束的标签, 判断新的一轮是否结束了。 取minimum相当于一个和以前的合并。 

这里取这个如果not over 全为0 说明全部结束了 就结束这一轮的预测。  

这样就完成预测了。 但是没有截断过程么 。 我真的好奇。 

好了 原来截断在后处理这里。 这里有一个转换函数, 后处理时, 如果遇到pad或者截止符号, 就停止输出, 这样就对了。

猜你喜欢

转载自blog.csdn.net/YI_SHU_JIA/article/details/129822429
今日推荐