a.Self-Attention表达式
$$ Q = xW_Q, K = xW_K, V= xW_V\ Attention(Q,K,V) = softmax(\frac{QK^T}{\sqrt{d_k}})V
$$
b.为何对QK进行scaling
让softmax输入的数据分布变好,数值进入梯度敏感区间,能防止梯度消失,让模型好训练。
c.self-attention一定要这样表达吗?
不需要,能刻画相关性,相似性等建模方式都可以。最好速度快,模型好学,表达能力够。
d.有其他方法不用除根号dk吗?
有,同上,只要能做到每层参数的梯度保持在训练敏感的范围内,不要太大,不要太小。那么这个网络就比较好训练。方式有,比较好的初始化方法,类似于google的T5模型,就在初始化把这个事情干了。
e.为什么transformer用Layer Norm?有什么用?
八股,让神经网络各层参数输入的数据分布变好,数值进入梯度敏感区间,能防止梯度消失,让模型好训练
f.为什么不用BN?
1)NLP不定长,好多位置填0,影响其他样本非0参数的计算。
2)Transformer的模型比较大,BS拉不大,容易变得不稳定.
3)BN无物理意义
g.Bert为什么要搞一个position embedding?
八股,增强表达能力(位置上的)。因为transformer对位置不敏感,需要显示标示
h.Bert为什么三个embedding可以相加?
1)transformer不具有很好的上下文感知能力,这里的相加,是特征交叉的表达方法。可以带来上下文的语义信息。
2)Bert输入的token是BPE和中文char。这个要比单词,和中文词粒度更粗,分布更均匀一些。加上位置的Embedding能把这些粒度更粗的token带来个性化的表达能力。
3)因为表征空间含义的不同,这里的加算不上池化(avg pooling*3),只能是为了带入位置信息的交叉操作。
i.transformer为什么要用三个不一样的QKV?
八股,增强网络的容量和表达能力。
j.为什么要多头?举例说明多头相比单头注意力的优势
八股,增强网络的容量和表达能力。
k.为什么Bert中要用WordPiece/BPE这样的subword Token?
能很好的解决单词上的OOV,在语义粒度是比较合适的表达。
l.Bert中为什么要在开头加个[CLS]?
为了拿到更好的句子语义表示,更好意味着这个表征能够照顾大局,不对任何一个位置过分偏心。selfattenruin的计算,对所有词都是等价的。
m.不用[CLS]的语义输出,有其他方式可以代替吗?
可以用他的maxpooling和avgpooling进行拼接(实践经验,前Bert时代的套路,面试官可能不认)
n.Bert中有哪些地方用到了mask?
预训练任务,selfattention计算,下游任务的decoder
o.预训练阶段的mask有什么用?
构造完形填空的预训练任务
p.attention中的mask有什么用?(BERT中)
处理掉padding部分带来的无效信息,实际在softmax之前填上-inf就可以实现了。
q.decoder中的mask有什么用?
防止语言模型利用了leak未来信息
r.Bert中self attention 计算复杂度如何?
s.有什么技术降低复杂度提升输入长度的?
Sparse attention,sparse attention放弃对全文的关注,只关心局部的语义组合,在整个计算矩阵上挖了空格子。这样做的好处是,下游任务的语义关联性的体现往往是局部的。
t.Bert是如何处理传统方法难以搞定的溢出词表词(oov)的语义学习的?
Bert使用subword,把词拆碎,常见的typo或者语言特殊表达,都能有一部分照顾到。
u.中文是如何处理溢出词表词(oov)的语义学习的?
中文是字级别,词级别的oov,在字级别上解决。
v.为什么以前char level/subword level的NLP模型表现一般都比较差,但是到了bert这里就比较好?
以前NLP模型没办法做到很深,两层lstm基本就到极限了,非线性成长路线过分陡峭。增加网络容量的时候,降低了泛化能力。
Bert降低了输入的复杂度,提升了模型的复杂度。模型多层产生的非线性增长平滑,可以加大网络容量,同时增强容量和泛化能力。
w.Bert为什么要使用warmup的学习率trick
八股,为了让开始阶段的数据分布更好,更容易训练,防止过拟合到少数几个batch上。
x.为什么说GPT是单向的Bert是双向的?
双向指的是语义上的双向,GPT模仿了语言模型屏蔽了序列后面的位置。Bert没有这样做,在self attention上都可以彼此交互,但是GPT就不行。
y.Bert如何处理一词多义?
一句话,利用self-attention中词和词的交互。
z.Bert中的transformer和原生的transformer有什么区别?
原生用的是周期函数对相对位置进行编码,Bert换成了position embedding,后面也有模型
z+.Albert是通过什么方法压缩网络参数的?有什么问题?
多层transformer共享参数,推断需要重复使用这些参数,时间没有减少
1)普通attention
$$ Q = xW_Q, K = xW_K, V= xW_V\ Attention(Q,K,V) = softmax(\frac{QK^T}{\sqrt{d_k}})V
$$
2)multi-head attention
$$ O = MultiHead(Q,K,V) = W^Oconcat(\left{\begin{matrix} softmax(\frac{Q_1K_1}{\sqrt{d_m}})V_1 \ softmax(\frac{Q_2K_2}{\sqrt{d_m}})V_2 \end{matrix}\right.)
$$
self-attention块的模型参数有Q、K、V的权重矩阵
上面是一个简化的算法,multi-head attention也符合这个参数量,但实际上需要分head计算再组合。
这里值得关注的点是:
- 偏置矩阵有没有用?
在实现Q、K、V矩阵时是有一个参数bias的。
不同模型设置不同,从代码观察到,LLaMA没有用(划重点),ChatGLM v1用了,这里参数量不同,但因为是1次项,所以几乎不影响参数量。
- 一般hidden size是多少?
LLaMA从4096到8192。可以记住7B模型是,32个头,每个小矩阵 [128*4096]
from torch import nn
class LlamaAttention(nn.Module):
"""Multi-headed attention from 'Attention Is All You Need' paper"""
def __init__(self, config: LlamaConfig):
super().__init__()
self.config = config
self.hidden_size = config.hidden_size
self.num_heads = config.num_attention_heads
self.head_dim = self.hidden_size // self.num_heads
self.max_position_embeddings = config.max_position_embeddings
self.q_proj = nn.Linear(self.hidden_size, self.num_heads * self.head_dim, bias=False)
self.k_proj = nn.Linear(self.hidden_size, self.num_heads * self.head_dim, bias=False)
self.v_proj = nn.Linear(self.hidden_size, self.num_heads * self.head_dim, bias=False)
self.o_proj = nn.Linear(self.num_heads * self.head_dim, self.hidden_size, bias=False)
def forward(
self,
hidden_states: torch.Tensor,
attention_mask: Optional[torch.Tensor] = None,
position_ids: Optional[torch.LongTensor] = None,
past_key_value: Optional[Tuple[torch.Tensor]] = None,
output_attentions: bool = False,
use_cache: bool = False,
) -> Tuple[torch.Tensor, Optional[torch.Tensor], Optional[Tuple[torch.Tensor]]]:
bsz, q_len, _ = hidden_states.size()
# 获得qkv向量
query_states = self.q_proj(hidden_states).view(bsz, q_len, self.num_heads, self.head_dim).transpose(1, 2)
key_states = self.k_proj(hidden_states).view(bsz, q_len, self.num_heads, self.head_dim).transpose(1, 2)
value_states = self.v_proj(hidden_states).view(bsz, q_len, self.num_heads, self.head_dim).transpose(1, 2)
# 拼接kvcache
kv_seq_len = key_states.shape[-2]
if past_key_value is not None:
kv_seq_len += past_key_value[0].shape[-2]
if past_key_value is not None:
# reuse k, v, self_attention
key_states = torch.cat([past_key_value[0], key_states], dim=2)
value_states = torch.cat([past_key_value[1], value_states], dim=2)
past_key_value = (key_states, value_states) if use_cache else None
# 计算attention权重
attn_weights = torch.matmul(query_states, key_states.transpose(2, 3)) / math.sqrt(self.head_dim)
# 加入mask矩阵,decoder-only为下三角
if attention_mask is not None:
attn_weights = attn_weights + attention_mask
dtype_min = torch.tensor(
torch.finfo(attn_weights.dtype).min, device=attn_weights.device, dtype=attn_weights.dtype
)
attn_weights = torch.max(attn_weights, dtype_min)
# 计算softmax,这里需要从fp16升为fp32
# upcast attention to fp32
attn_weights = nn.functional.softmax(attn_weights, dim=-1, dtype=torch.float32).to(query_states.dtype)
attn_output = torch.matmul(attn_weights, value_states)
attn_output = attn_output.transpose(1, 2)
attn_output = attn_output.reshape(bsz, q_len, self.hidden_size)
attn_output = self.o_proj(attn_output)
if not output_attentions:
attn_weights = None
return attn_output, attn_weights, past_key_value
NLU是自然语言理解,NLG是自然语言生成。
NLG生成的是文本,而NLU通常输出的是标签。
文本分类、意图识别、实体识别、关系抽取、事件抽取、阅读理解等
机器翻译、文本摘要、生成式对话系统、故事续写、生成式问答
基础知识见“02.04.Tokenizer.md”,这里仅讨论llama的tokenizer
LLaMA基于BBPE实现。
首先,分词要面对 OOV (out-of-vocabulary)问题,比如在英文上训练,如果词表里面没有中文,那么对于一个汉字,是无法识别出token的,在BERT中会被识别为[UNK]。
为了解决这个问题,需要把类似中文或者日文的字符都塞进去。这带来了新的问题,很多字符出现的频率很低频,但是 词表会变得很大 ,以至于存储和训练的成本都很高,并且稀有词往往很难学好。
Q1)如何解决OOV问题?
每个计算机能表示的字符总能用Unicode表示,可以给 所有可能的字符都编码。 。
Unicode 的全称是 Universal Multiple-Octet Coded Character Set(通用多八位字符集,简称 UCS)。Unicode 在一个字符集中包含了世界上所有文字和符号,统一编码,来终结不同编码产生乱码的问题。
Unicode 统一了所有字符的编码,是一个 Character Set,也就是字符集,字符集只是给所有的字符一个唯一编号,但是却没有规定如何存储。UTF-8其实只是unicode的一种存储方式。
用基于UTF-8的BBPE,词表可以相比于BPE减少到 1/8 (来自原论文的摘要,划重点)
Q2)为什么LLaMA词表里只有700+的汉字,但是也能表示中文?
因为使用BBPE,根据上面的原理,不存在的汉字用字节合成就可以了。
Q3)既然LLaMA的分词器是基于UTF-8的BBPE,是不是所有的模型通用一个分词器即可,不需额外训练?
是不可以的。LLaMA尽管能支持中文,但是表示效率很低,一个中文字符在UTF-8表示为3个字节,意味着:最差情况下,1个汉字要编码成3个token。
但是如果在中文语料上训练分词器,很多2个汉字组成的词会被编码为1个token。大模型是逐个token生成,推理速度都是按token计算的,所以如果推理速度相同,LLaMA生成中文的速度会远慢于在专门在中文上训练的GLM系列模型。
这就能解释为什么羊驼家族有一个流派在中文上扩充词表再继续预训练。这种情况下,中文效果提升未必是词表扩充导致的,但是生成中文的速度变快就是因为扩充词表。
LLaMA使用pre RMSNorm,是比较主流的方式。
GLM使用post DeepNorm。DEEPNORM在进行layer-norm之前会扩大残差连接。
$$ LayerNorm(x * \alpha + f(x))
$$
RMSNorm是对LayerNorm的一个改进,没有做re-center操作(移除了其中的均值项),可以看作LayerNorm在均值为0时的一个特例。
简单结论:
- 现在都是fp16或者bf16训练和推理,那么如果是1个100亿参数量的模型(也就是储存了100亿个参数),它其实是一个10B大小的模型。(1Billion等于10的9次方,也就是10亿)
- 1个字节占8bits,那么fp16就是占2个字节(Byte),那么10B模型的模型大小是20GB,是*2的关系。
那么对于nB 模型:
- 推理时显存的下限是 2n GB ,至少要把模型加载完全。
- 训练时,如果用Adam优化器,有个2+2+12的公式, 训练时显存下限是16n GB ,需要把模型参数、梯度和优化器状态(4+4+4),保持在显存,具体可以参考微软的ZeRO论文。
详细信息见“02.06.模型参数计算.md”
$$ Swish_\beta (x) = x\sigma (\beta x)
$$
其中,$\sigma$是softmax函数
$$ SwiGLU(x,W,V,b,c) = Swish_1 (xW + b) \otimes (xV + c)
$$