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 使用了多头注意力机制,同时对一个语料进行多次注意力计算,每次注意力计算都能拟合不同的关系,将最后的多次结果拼接起来作为最后的输出
- 我们可以通过矩阵运算巧妙地实现并行的多头计算,其核心逻辑在于使用三个组合矩阵来代替了 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.