两种常见的预训练任务:

因果语言建模(来源于NNLM)

遮盖语言建模(来源于CBOW)

预训练没有先验知识,需要海量训练数据,时间和经济成本都很高。

因此,使用标注好的任务语料对模型进行二次训练(迁移学习),这个过程称为微调。

微调充分利用了预训练的知识,需要的数据量很少。

1. 总体结构:编解码结构:编码将输入序列变成等长的连续表示,自回归解码将连续表示变成一个输出序列;编解码都使用堆叠的自注意力机制和逐点全连接层

2. 编码层:6层相同的层,每一层有两个子层,第一个子层是多头自注意力层,第二个子层是逐点全连接前馈层。两个子层都包括一个残差连接和层归一化,即每个子层的输出是layernorm(x+sublayer(x))

3. 解码层:6层相同的层,每一层有三个子层,第一个子层是掩码多头注意力层,指的是因果掩码,预测不能依赖未来信息,第二个子层是多头自注意力层,其中query来自第一个子层,key value来自于编码器,第三个子层是逐点全连接前馈层。三个子层都包括一个残差连接和层归一化,即每个子层的输出是layernorm(x+sublayer(x))。

4. 注意力机制:将一个query和一组key-value对映射为一个output,其中query,keys,values,output都是向量。output等于value的加权和,每个value的权重由query和相应的key计算得来(query通过和key内积并softmax的方式,得到query和各个value的相似度,然后加权求和,得到一个dv维的向量)

K 是为了被检索而存在的特征,V 是为了被阅读而存在的内容。必须用 Q 去匹配 K,才能决定去读取哪个 V。

5. 缩放点积注意力:query和key的维度是dk,value的维度是dv。将一组queries组合成一个矩阵Q,同理得到K, V

为什么除以根号dk,因为q k 都是dk维度,

防止注意力分数值过大,落入softmax的饱和区,导致梯度消失

注意mask的位置在scale和softmax之间

6. 多头注意力:

b batch_size

n sequence_length

d hidden_size

h num_heads

k head_dim

输入张量x为:batch_size*sequence_length*hidden_size

1)线性投影

x*[hidden_size*hidden_size] 得到 Q K V (K和V的sequence_length要相等)

形状为batch_size*sequence_length*hidden_size

2)分头

hidden_size=num_heads*head_dim

对Q K V进行reshape和transpose操作,将注意力头的数量提前,成为批处理维度的一部分

形状为batch_size*num_heads*sequence_length*head_dim

3)计算注意力得分并加attention mask

关键:只有在 输入 softmax 之前 把无效位置设为 -inf,才能让它们的权重变成 0。如果在 softmax 之后再 mask,就无法保证概率和为 1,且无法真正“忽略”这些位置。

Q*K.T=batch_size*num_heads*sequence_length*sequence_length

4)加权求和

weight*V=batch_size*num_heads*sequence_length*head_dim

5)合并头并最终投影

output: batch_size*sequence_length*num_heads*head_dim-->batch_size*sequence_length*hidden_size

final_output: batch_size*sequence_length*hidden_size

多头注意力有三种应用:(1)kv来自编码器,q来自解码器 (2)自注意力中qkv来自同一个地方 (3)自回归解码中加入掩码机制(另外attention mask可以遮盖padding等防止模型关注)

import torch
import torch.nn as nn
import torch.nn.functional as F
import math
 
class mha(nn.Module):
    def __init__(self,config):
        super().__init__()
        self.emb_dim=config.hidden_size
        self.num_attention_heads=config.num_attention_heads
        self.head_dim=self.emb_dim//self.num_attention_heads
        self.q_proj=nn.Linear(self.emb_dim,self.emb_dim)
        self.k_proj=nn.Linear(self.emb_dim,self.emb_dim)
        self.v_proj=nn.Linear(self.emb_dim,self.emb_dim)
        self.o_proj=nn.Linear(self.emb_dim,self.emb_dim)
 
    def forward(self,x,attn_mask=None):
        q=self.q_proj(x)
        k=self.k_proj(x)
        v=self.v_proj(x)
        batch_size,seq_len,_=q.size()
        q=q.view(batch_size,seq_len,self.num_attention_heads,self.head_dim).transpose(1,2)
        k=k.view(batch_size,seq_len,self.num_attention_heads,self.head_dim).transpose(1,2)
        v=v.view(batch_size,seq_len,self.num_attention_heads,self.head_dim).transpose(1,2)
        # batch_size,self.num_attention_heads,seq_len,self.head_dim
        attn_score=torch.matmul(q,k.transpose(-1,-2))
        # batch_size,self.num_attention_heads,seq_len,seq_len
        dk=k.size(-1)
        attn_score=attn_score/math.sqrt(dk)
        if attn_mask is not None:
            attn_score=attn_score.masked_fill(attn_mask==0,-float('inf'))
        weights=F.softmax(attn_score,dim=-1)
        output=torch.matmul(weights,v)
        # batch_size,self.num_attention_heads,seq_len,self.head_dim
        output=output.transpose(1,2).contiguous().view(batch_size,seq_len,self.emb_dim)
        output=self.o_proj(output)
        return output

7. 逐点前馈网络

编码器和解码器都包括一个逐点前馈网络,包括一个线性层,一个RELU激活函数,一个线性层

8. 嵌入层和softmax

编码器和解码器的嵌入层都是将token映射为dmodel维向量

在解码器最后,对每个位置的输出向量(维度dmodel​)施加一个线性变换(全连接层),将其映射到词汇表大小∣V∣的 logits 空间,再通过Softmax得到下一个 token 的概率分布:

P(wt​∣w<t​)=Softmax(Wout​⋅ht​)

transformer在三处共享权重矩阵:输入嵌入层,输出嵌入层,预softmax线性变换层,显著提升性能并减少参数量

在 Transformer 模型中,“在嵌入层将权重乘以根号dmodel​​”是一个关键的缩放技巧,其主要目的是平衡词嵌入(token embeddings)与位置编码(positional encodings)的尺度(scale),从而确保模型训练的稳定性。PyTorch 的 embedding 初始化使其向量长度与维度无关(恒定小值),而 PE 长度随 dmodel​​增长。通过 ×dmodel​​将 embedding 的尺度提升到与 PE 匹配,确保两者在相加时贡献均衡。

层后归一化:原使用方式

层后归一化:归一化层在跳跃连接外,在跳跃连接后

层前归一化:归一化层在跳跃连接内,在内部最前面

9. 位置编码

内部式子相同,只是sin和cos的区别

d代表位置编码向量的总维度数

对于同一个词的不同维度,2i代表偶数维,2i+1代表奇数维

由于有足够的数据,足够深的网络,所以模型可以训练整合位置和词向量的信息

由于词向量维度够高,所以'碰撞'发生的可能性很低

10 teacher forcing

训练解码器时,使用真实的目标序列前一个词作为输入,而不是模型预测的词

  1. 加速训练,稳定收敛
  2. 支持并行计算(通过掩码防止信息泄露)

11 编码器模型(Auto-Encoding)

适合理解整个句子语义的任务,包括分类,NER,抽取式问答等

包括BERT DistilBERT(更小的模型) RoBERTa(更大数据量,更长时间) ModernBERT(2万亿个次元)

12 解码器模型(Auto-Regressive)

适合文本生成任务

13 编码器-解码器模型

适合给定输入生成新文本的任务,包括摘要,翻译,生成式问答等

T5(将所有的NLU NLG任务转换成seq2seq形式解决)

BART (同时结合了BERT和GPT的预训练过程)

M2M-100(语言对之间可能存在共享知识来处理小众语言之间的翻译)

BIGBIRD(通过线性扩展的稀疏注意力形式扩展可处理的文本长度)

14 两阶段推理过程

预填充:分词(词元);嵌入转换;初始处理

解码:注意力计算(回顾所有先前的词元以理解上下文);概率计算;词元选择;持续性检查

需要考虑的关键指标:1 首次响应时间TTFT 2 输出每词元的时间 TPOT 3 吞吐量 同时处理的请求数量 4 显存使用情况

15 一些新型模型结构

Mamba;RWKV;RetNet;Hyena

16 MoE结构

将特定FFN替换成MoE,使得模型可以在推理过程中仅激活部分参数,从而在不显著提升计算成本的同时实现对模型参数的扩展。

每个MoE包含K个专家,每个专家是一个FFN,对于输入,模型首先通过一个路由网络来计算各专家的权重,然后选择概率最高的k个激活,最后通过softmax计算出k个专家权重,没选中的权重置为0

17 手撕GQA

import math
import torch
import torch.nn as nn
 
class GQA(nn.Module):
    def __init__(self,hidden_dim=128,head_size=8,key_head_size=4,dropout_rate=0.1):
        super().__init__()
        self.hidden_dim=hidden_dim
        self.head_size=head_size
        self.key_head_size=key_head_size
        # key_head_size=1时就是MQA
        self.head_dim=self.hidden_dim//self.head_size
        self.q_proj=nn.Linear(hidden_dim,hidden_dim)
        self.k_proj=nn.Linear(hidden_dim,self.key_head_size*self.head_dim)
        self.v_proj=nn.Linear(hidden_dim,self.key_head_size*self.head_dim)
        #这边注意
        self.dropout=nn.Dropout(dropout_rate)
        self.output_proj=nn.Linear(hidden_dim,hidden_dim)
 
    def forward(self,X,attention_mask=None):
        b,s,_=X.shape
        Q=self.q_proj(X)
        K=self.k_proj(X)
        V=self.v_proj(X)
        Q=Q.view(b,s,self.head_size,self.head_dim).transpose(1,2)
        K=K.view(b,s,self.key_head_size,self.head_dim).transpose(1,2)
        V=V.view(b,s,self.key_head_size,self.head_dim).transpose(1,2)
        K=K.repeat_interleave(repeats=self.head_size//self.key_head_size,dim=1)
        V=V.repeat_interleave(repeats=self.head_size//self.key_head_size,dim=1)
        #torch.repeat_interleave(input, repeats, dim=None) 会对张量 input 中的每个元素进行指定次数的重复,并将结果拼接成一个新的张量。
        #input:输入张量
        #repeats:每个元素的重复次数(可以是整数或与 input 维度匹配的张量)
        #dim:指定重复的维度(不指定时会先将张量展平)
        # 把维度转换一下
        attention_value=Q @ K.transpose(-1,-2)
        attention_weight=attention_value/math.sqrt(self.head_dim)
        if attention_mask is not None:
            attention_weight=attention_weight.masked_fill(attention_mask==0,-float('inf'))
        attention_weight=torch.softmax(attention_weight,-1)
        attention_weight=self.dropout(attention_weight)
        output=attention_weight @ V
        output=output.transpose(1,2).contiguous()
        #contiguous() 方法的作用是确保张量(tensor)在内存中是连续存储的
        #某些操作(如 view())只支持连续张量,对不连续张量使用会报错
        #b,s,head_num,head_dim
        output=output.view(b,s,-1)
        # 再把维度转换回来
        output=self.output_proj(output)
        return output
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐