Post

Happy LLM - Datawhale

Happy LLM - Datawhale

Happy LLM - Datawhale

第一章:NLP 基础概念

  • NLP 核⼼任务是通过计算机程序来模拟⼈类对语⾔的认知和使⽤过程

  • 从符号主义与统计方法 到 机器学习与深度学习

  • 任务包括但不限于中文分词、子词切分、词性标注、文本分类、实体识别、关系抽取、文本摘要、机器翻译以及自动问答系统的开发。

第二章:Transformer 架构

2.1 注意力机制

理解注意力机制

  • 从 CV 为起源发展起来的神经网络,其核心架构有三种:

    • 前馈神经网络 Feedforward Neural Network, FNN;每层神经元之间完全链接;

    • 卷积神经网络 Convolutional Neural Network, CNN:用训练参数量远小于 FNN 的卷积层来进行特征提取和学习;

    • 循环神经网络 Recurrent Neural Network, RNN:能够用历史信息作为输入,包含环和自重复的网络;

  • 过去常用 RNN,但 RNN 限制了计算机并行计算的能力,难以捕捉长序列的相关关系;

  • 于是借鉴 CV 的注意力机制,有了 Transformer:

    • 将重点注意力集中在一个或几个 token;

    • 注意力机制有三个核心变量:Query(查询值)、Key(键值)和 Value(真值)。

    • 注意力机制的特点是通过计算 Query 与 Key 的相关性为真值加权求和(注意力分数),从而拟合序列中每个词同其他词的相关关系。

实现注意力机制

\[\begin{equation} attention(Q, K, V) = \text{softmax}\left( \frac{QK^T}{\sqrt{d_k}} \right) V \label{eq:attention} \end{equation}\]
  • 其中,$ Q $ 与 $ K $ 相乘的结果反映了 $ query $ 与每一个 $ key $ 的相似程度,经 $ softmax $ 归一化,得到注意力分数。将其乘上 $ V $,得到最终值。

  • 除以 $ \sqrt{d_k} $ 是为了避免 $ Q $ 与 $ K $ 对应的维度 $ d_k $ 较大,使得 $ softmax $ 放缩时受过多影响,使不同值之间的差异较大,影响梯度的稳定性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import torch
import math

def attention(query, key, value, dropout = None):

    # query  [batch, seq_len_q, dim]
    # key    [batch, seq_len_k, dim]
    d_k = query.size(-1) # 获取特征 dim

    # matmul 矩阵计算
    # transpose 使得 q 与 k 能相乘
    scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)

    # scores [batch, seq_len_q, seq_len_k]
    # softmax 使得 seq_len_k 的值归一化
    p_attn = scores.softmax(dim = -1)
    
    if dropout is not None:
        p_attn = dropout(p_attn)

    return torch.matmul(p_attn, value), p_attn

自注意力机制 Self-attention

  • 在 Transformer 的 Encoder 的,我们希望获取每一个 token 对其它 token 的注意力分布,就有了自注意力机制(Self-attention)
1
attention(x, x, x)

掩码自注意力机制 Mask Self-attention

  • 让模型只能看到历史信息而看不到未来信息,需要用 [mask] 来遮蔽未来的信息
1
2
3
4
5
<BOS> [MASK] [MASK] [MASK] [MASK],
<BOS>   I     [MASK] [MASK] [MASK],
<BOS>  I      like  [MASK] [MASK],
<BOS>  I      like   you   [MASK],
<BOS>  I      like   you   </EOS>
  • 掩码矩阵就是一个跟文本序列等长的上三角矩阵,当输入维度为 (batch_size, seq_len, hidden_size)时,我们的 Mask 矩阵维度一般为 (1, seq_len, seq_len)(通过广播实现同一个 batch 中不同样本的计算)。
1
2
3
4
5
6
7
8
9
10
11
# 掩码矩阵
mask = torch.full((1, args.max_seq_len, args.max_seq_len), float("-inf"))

# diagonal = 1 使得只保留对角线上的元素,若 = 0 则保留对角线元素;除上三角地区为 -inf 外,其它元素均为 0
mask = torch.triu(mask, diagonal = 1)

# 把负无穷掩码加上 softmax 对极大极小值敏感,-inf 概率会趋近于 0,实现屏蔽未来位置的注意力
scores = scores + mask[:, :seqlen, :seqlen]

# 最后一维归一化 使得类型同 xq
scores = F.softmax(scores.float(), dim = -1).type_as(xq)

多头注意力机制 Multi-Head attention

  • 一次注意力计算只能拟合一种相关关系,单一的注意力机制很难全面拟合语句序列里的相关关系。因此 Transformer 使用了多头注意力机制,同时对一个语料进行多次注意力计算,每次注意力计算都能拟合不同的关系,将最后的多次结果拼接起来作为最后的输出
\[\begin{equation} \text{MultiHead}(Q, K, V) = \text{Concat}(\text{head}_1, \ldots, \text{head}_h) W^O \label{eq:multi_head} \end{equation} where \(\text{head}_i = \text{Attention}(QW_i^Q, KW_i^K, VW_i^V)\)\]
  • 我们可以通过矩阵运算巧妙地实现并行的多头计算,其核心逻辑在于使用三个组合矩阵来代替了 n 个参数矩阵的组合,也就是矩阵内积再拼接其实等同于拼接矩阵再内积。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
import torch.nn as nn
import torch
import torch.nn.functional as F
import math

class ModelArgs:
    def __init__(self, 
                 n_embd=768, 
                 n_heads=12, 
                 dim=768, 
                 dropout=0.1, 
                 max_seq_len=2048,
                 model_parallel_size=1):
        self.n_embd = n_embd
        self.n_heads = n_heads
        self.dim = dim
        self.dropout = dropout
        self.max_seq_len = max_seq_len
        self.model_parallel_size = model_parallel_size

class MultiHeadAttention(nn.Module):

    def __init__(self, args: ModelArgs, is_causal = False):
        super().__init__()

        # 隐藏层维度必须是头数的整数倍 后面我们会将输入拆成头数个矩阵
        assert args.n_embd % args.n_heads == 0, "隐藏层维度必须是头数的整数倍"
        # 模型并行处理大小
        model_parallel_size = 1
        # 本地计算头数
        self.n_local_heads = args.n_heads // model_parallel_size
        # 每个头的维度
        self.head_dim = args.dim // args.n_heads

        # Wq Wk Wv 三个参数矩阵 [n_embd * n_embd]
        # 三个组合矩阵来代替了 n 个参数矩阵的组合,矩阵内积再拼接相当于拼接矩阵再内积
        # 将输入维度 args.dim 投影到多头维度 args.n_heads * self.head_dim
        self.wq = nn.Linear(args.dim, args.n_heads * self.n_local_heads, bias = False)
        self.wk = nn.Linear(args.dim, args.n_heads * self.n_local_heads, bias = False)
        self.wv = nn.Linear(args.dim, args.n_heads * self.n_local_heads, bias = False)
        # 输出权重矩阵 [n_embd * n_embd] head_dim = n_embds / n_heads
        self.wo = nn.Linear(args.n_heads * self.n_local_heads, args.dim, bias = False)
        # 注意力与残差连接的 dropout
        self.attn_dropout = nn.Dropout(args.dropout)
        self.resid_dropout = nn.Dropout(args.dropout)

        self.is_causal = is_causal

        # 一个上三角矩阵 mask 掩码
        # 在多头注意力下 mask 矩阵要比之前我们定义的多一个维度
        if self.is_causal:
            mask = torch.full((1, 1, args.max_seq_len, args.max_seq_len), float("-inf"))
            mask = torch.triu(mask, diagonal = 1)
            # 注册为模型的缓冲区
            # 缓冲区是一种特殊的张量,缓冲区不会被视为需要优化的参数,不会在反向传播中更新
            # 但仍随模型保存 通常用于固定数据
            self.register_buffer("mask", mask)

    def forward(self, q: torch.Tensor, k: torch.Tensor, v: torch.Tensor):

        # 获取批次大小和序列长度 [batch_size, seq_len, dim]
        batch_size, seqlen, _ = q.shape

        # 计算查询(Q)、键(K)、值(V),输⼊通过参数矩阵层,维度为 (B, T, n_embed) x (n_embed, n_embed) -> (B, T, n_embed)
        xq, xk, xv = self.wq(q), self.wk(k), self.wv(v)

        #    [batch_size, seq_len, hidden_dim] 
        # -> [batch_size, seq_len, n_local_heads, head_dim] 
        # -> [batch_size, n_local_heads, seq_len, head_dim]
        xq = xq.view(batch_size, seqlen, self.n_local_heads, self.head_dim).transpose(1, 2)
        xk = xk.view(batch_size, seqlen, self.n_local_heads, self.head_dim).transpose(1, 2)
        xv = xv.view(batch_size, seqlen, self.n_local_heads, self.head_dim).transpose(1, 2)

        # 注意力计算
        # [B, nh, T, hs] * [B, nh, hs, T] -> [B, nh, T, T]
        scores = torch.matmul(xq, xk.transpose(2, 3)) / math.sqrt(self.n_local_heads)

        # 若为掩码 有
        if self.is_causal:
            assert hasattr(self, 'mask'), "不存在掩码"
            scores = scores + self.mask[:, :, :seqlen, :seqlen]

        # softmax [B, nh, T, T] 后 dropout
        scores = F.softmax(scores.float(), dim = -1).type_as(xq)
        scores = self.attn_dropout(scores)

        # scores * V
        # [B, nh, T, T] * [B, nh, T, hs] -> [B, nh, T, hs]
        output = torch.matmul(scores, xv)

        # 恢复时间维度并合并头
        
        # 将多头的结果拼接起来 C = n_local_heads × head_dim, C 即隐藏层维度 hidden_dim

        # [B, T, n_local_heads, C // n_local_heads] -> [B, T, n_local_heads * C // n_local_heads]

        # contiguous 函数用于重新开辟一块新内存存储,因为 Pytorch 设置先 transpose 再 view 会报错,

        # 因为 view 直接基于底层存储得到,然而 transpose 并不会改变底层存储,因此需要额外存储
        
        # view(bsz, seqlen, -1) 会保持张量元素总数不变,将张量重新组织成 [bsz, seqlen, ...] 的形状:
        output = output.transpose(1, 2).contiguous().view(batch_size, seqlen, -1)

        # 投影回残差流
        output = self.wo(output)
        output = self.resid_dropout(output)
        return output
This post is licensed under CC BY 4.0 by the author.