Transformer从0理解教程(包含手撕代码)

1. 位置编码:对于输入的向量x.shape[b,s,d_model] 而言进行位置上的编码

class PositionEncoding(nn.module):
    def __init__(self,d_model,max_len):
        super.__init__()
        pe = torch.zeros(max_len,d_model)
        position = torch.arange(0,max_len)
        div_term = torch.exp(-math.log(10000)*torch.arange(0,d_model,2)/d_model)
        pe[:,0::2] = torch.sin(position*div_term)
        pe[:,1::2] = torch.cos(position*div_term)
        pe = pe.unsqueeze(0)
        self.register_buffer("pe",pe)

    def forward(self,x):
        x = x + self.pe[:,:x.size(1)]
        return x

使用位置编码可以将输入中任意某个tensor的位置量化成d_model维度的向量,编码不会重复(单通道下至少需要10000*2pi个位置才有可能重复),并且由于正弦余弦函数是周期函数,在推理时可以插入比训练序列更长的序列。

为什么要使用register_buffer?

——因为pe是常数,是不参与训练的,只需要记录就好。

2. 多头注意力机制:

class MultiHeadAttention(nn.Module):
    def __init__(self, d_model, num_heads):
        super().__init__()
        assert d_model % num_heads == 0
        self.d_k = d_model // num_heads
        self.num_heads = num_heads
        self.q_linear = nn.Linear(d_model, d_model)
        self.k_linear = nn.Linear(d_model, d_model)
        self.v_linear = nn.Linear(d_model, d_model)
        self.out = nn.Linear(d_model, d_model)
        self.dropout = nn.Dropout(0.1)

    def forward(self, q, k, v, mask=None): # 此处q k v均是x
        B, T, D = q.size()
        q = self.q_linear(q).view(B, T, self.num_heads, self.d_k).transpose(1, 2)
        k = self.k_linear(k).view(B, T, self.num_heads, self.d_k).transpose(1, 2)
        v = self.v_linear(v).view(B, T, self.num_heads, self.d_k).transpose(1, 2)

        scores = q @ k.transpose(-2, -1) / math.sqrt(self.d_k)
        if mask is not None:
            scores = scores.masked_fill(mask == 0, -1e9)

        attn = torch.softmax(scores, dim=-1)
        attn = self.dropout(attn)
        x = attn @ v 
        x = x.transpose(1, 2).contiguous().view(B, T, -1)  # 合并多头
        return self.out(x)

scores本质上是query与key的相关度,也就是某两个位置的token相关度有多高,然后attn作用于v,本质上是对某一个token处的embedding加上与之相关度高的token的embedding。

3. 前馈网络:

class FeedForward(nn.Module):
    def __init__(self, d_model, d_ff=2048):
        super().__init__()
        self.linear1 = nn.Linear(d_model, d_ff)
        self.dropout = nn.Dropout(0.1)
        self.linear2 = nn.Linear(d_ff, d_model)

    def forward(self, x):
        return self.linear2(self.dropout(torch.relu(self.linear1(x))))

4. encoder layer:一层attention一层前馈,每一层后都有残差连接和layer norm

以翻译器为例,假如输入是 “I love you”,需要对应翻译 “我爱你”,那么编码器的输入source token应该是“I love you”,此时的attention机制可以感知全局。

class EncoderLayer(nn.Module):
    def __init__(self, d_model, num_heads):
        super().__init__()
        self.self_attn = MultiHeadAttention(d_model, num_heads)
        self.ff = FeedForward(d_model)
        self.norm1 = nn.LayerNorm(d_model)
        self.norm2 = nn.LayerNorm(d_model)

    def forward(self, x, mask):
        attn = self.self_attn(x, x, x, mask)
        x = self.norm1(x + attn)
        ff = self.ff(x)
        return self.norm2(x + ff)

为什么在attention层和前馈网络后进行layer norm?

最初的transformer使用的是post norm,也就是在经过网络和残差连接之后进行layer norm,主要是由于残差直接将输入加到子层的输出中,虽然保留了底层信息但是会造成数值不稳定,并且使用norm能够有效抑制梯度爆炸和消失,同时加速训练收敛。BERT、T5 等现代 Transformer都使用pre norm,能够使得训练更加稳定,更适用于大模型。

5. decoder layer:一层self attention和cross attention以及一层前馈,每一层后都有残差连接和layer norm

解码器的输入target token应该是“BOS 我 爱 你”,BOS(begin of sequence)是起始点,decoder需要进行感知。此时的每一个target token的输入都能感知到完整的encoder output的信息。

class DecoderLayer(nn.Module):
    def __init__(self, d_model, num_heads):
        super().__init__()
        self.self_attn = MultiHeadAttention(d_model, num_heads)
        self.cross_attn = MultiHeadAttention(d_model, num_heads)
        self.ff = FeedForward(d_model)
        self.norm1 = nn.LayerNorm(d_model)
        self.norm2 = nn.LayerNorm(d_model)
        self.norm3 = nn.LayerNorm(d_model)

    def forward(self, x, enc_output, src_mask, tgt_mask):
        x = self.norm1(x + self.self_attn(x, x, x, tgt_mask))
        x = self.norm2(x + self.cross_attn(x, enc_output, enc_output, src_mask))
        x = self.norm3(x + self.ff(x))
        return x

self attention和cross attention有什么不同?

self attention主要是为了让target tokens具备感知上文的能力,同时使用target mask遮盖未来token的信息。而cross attention是为了让当前的每一条target token embedding关注输入source token的全部上下文信息。在cross attention计算过程中,target token和enc_output的序列长度是不需要对齐的。

为什么cross attention不需要进行mask掩码?

回顾QKV计算,对于decoder输入target token矩阵来说,每一条query都可以完整的感知encoder的输出信息,后续参与计算的也是当前上文中的token和原文全部的位置信息。

6. encoder和decoder:

class Encoder(nn.Module):
    def __init__(self, vocab_size, d_model, N, num_heads):
        super().__init__()
        self.embed = nn.Embedding(vocab_size, d_model)
        self.pos = PositionalEncoding(d_model)
        self.layers = nn.ModuleList([EncoderLayer(d_model, num_heads) for _ in range(N)])

    def forward(self, src, mask):
        x = self.pos(self.embed(src))
        for layer in self.layers:
            x = layer(x, mask)
        return x

class Decoder(nn.Module):
    def __init__(self, vocab_size, d_model, N, num_heads):
        super().__init__()
        self.embed = nn.Embedding(vocab_size, d_model)
        self.pos = PositionalEncoding(d_model)
        self.layers = nn.ModuleList([DecoderLayer(d_model, num_heads) for _ in range(N)])

    def forward(self, tgt, enc_output, src_mask, tgt_mask):
        x = self.pos(self.embed(tgt))
        for layer in self.layers:
            x = layer(x, enc_output, src_mask, tgt_mask)
        return x

7. transformer整体结构

class Transformer(nn.Module):
    def __init__(self, src_vocab, tgt_vocab, d_model=512, N=6, heads=8):
        super().__init__()
        self.encoder = Encoder(src_vocab, d_model, N, heads)
        self.decoder = Decoder(tgt_vocab, d_model, N, heads)
        self.out = nn.Linear(d_model, tgt_vocab)

    def forward(self, src, tgt, src_mask, tgt_mask):
        enc_output = self.encoder(src, src_mask)
        dec_output = self.decoder(tgt, enc_output, src_mask, tgt_mask)
        return self.out(dec_output)  # shape: (batch, tgt_len, tgt_vocab)

8. transformer损失函数

transformer的损失函数一般使用多元交叉熵函数,也就是针对一个目标序列中的第t个token,模型预测出目标词汇表V长度的概率分布,其label是真值词的one-hot编码。

\mathcal{L}_{\mathrm{CE}} = - \sum_{t=1}^{T} \sum_{i=1}^{V} y_{t,i} \log \hat{y}_{t,i}

9. 面试中常见transformer八股

1. transformer基本架构组成

2. self attention机制的作用是什么?
    长距离依赖捕获所有位置的token的信息、实现并行计算、可以处理变长输入、权重可视化。

3. self attention如何计算?为什么要除以根号dk?
    在QKV计算中,QK点积的大小会随着dk维度的变化而变化,如果不除以dk,scores经过softmax之后的向量会接近one-hot编码,根据softmax的反函数,其梯度会变得非常小,不利于网络的反向传播。

4. multi-head attention的作用是什么?
    多个head其实相当于让模型学习到多个不同的注意力模式,捕捉更多样化的关系和语义;多个head能够实现更加稳定的梯度传播和训练;如果是并行计算,多个head比一个head计算效率更高。
    

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值