week4(下)

预训练语言模型 --- BERT(预训练思想 · BERT架构 · MLM/NSP · Embedding)

transformer的论文是attention is all you need,出现比bert更早。

bert的架构其实就是transformer结构。

每个模型都是随机初始化。

有任务a,做语种分类,随机初始化一个模型。

有任务b,做用户评论情感分类,收集数据,重新训练模型。

任务c,判断两句话是否矛盾,准备大量训练模型,也是从头开始训练模型。

每个都是从0开始,各训练各自的。

有些事情是新的,但是中文是早就会的,相当于中文是基座。

机器学习中便引入这份思想:能不能不谈具体任务,让他先熟悉语言。等要做具体任务的时候,就拿少量的样本,稍微的调整下模型。

这样我们所有的任务A,任务B和任务C,都有共同的基座模型。A、B、C相当于是从基座模型引申出分支,在这个基座模型的基础上,少量的微调。这样A、B、C就不是从0开始的任务,就都是基于一个共同的基座开始。这种思想,就叫做预训练思想。

也就是先预训练基座,然后进行迁移学习,在预训练的基础上少量的去改。

NLP 的困境(BERT 之前)

每个任务从头训练,依赖大量标注数据

词向量静态,无法感知上下文语义变化

任务间知识无法复用

迁移学习:以 CV 经验为例(在NLP之前)

ImageNet 大规模预训练 → 迁移各类视觉任务

微调只需少量标注数据即可取得好效果

图像领域有个非常大的数据集,叫做ImageNet,里面是有各类图像标注的类别,有猫的图像,狗的图像等。

他们设计了预训练任务,训练了图像的基座模型,然后在后续做基于图像的分类,基于图像的理解等,都以基座模型为基础,就不再从头训练了。

预训练 + 微调范式

语言模型非常适合做基座模型,因为语言模型是自监督的,不需要标注,(需要标注的话,注定不会有太多数据),

我们是用前n个字预测错一位的下面n+1个字。所有的文本数据天生自己就是自己的标注。

无需标注即可学习通用语言知识(自监督学习)

语料来源:所有网站、电子书、代码、论文。所有能找到的文本资料收集起来,做预训练。这样会得到通用的语言模型。

你输入前文,就有相当强的预测后面的文字的能力。也有对多个文字序列排序,看哪个打分高。但是他不是能直接完成任务的模型。用于下游任务的时候,我们会对他再补充少量的标注数据,针对某个特定任务的标注数据。

原来我们想从头训练分类模型的话,可能要准备1000条样本,如果用基座训练加上标注的方法,也许只需要100条样本就够了。用少量样本在模型的基础上增加分类层,整体带着模型微调下。

和原来的方式的区别是,原来是每次从头训练新模型,现在是通过特别大规模的语料,先预训练一个我们称之为基座的模型,然后再做。

NLP 预训练演进路径

BERT 概览与影响

nlp榜单的任务,如果想刷出好的成绩,都是基于如下思想去做的:

两阶段范式

核心创新

双向上下文:同时利用左侧和右侧词的语义信息

预训练 + 微调:通用模型 + 轻量任务适配头

规模优势:亿级参数,海量语料预训练

‌新发布的AI模型,在11项不同的自然语言处理公开评测任务上,全部刷新了当前公开的最高性能纪录‌。

在这之后,就没有非预训练的做法能上榜了。

预训练任务一:Masked Language Model(完形填空)

自回归语言模型:通过 第1-第n位的文字,错位预测第2-第n+1位的文字。不断预测下一个的字的做法,叫做自回归语言模型。

但是bert当年没有用自回归语言模型,而是做了修改,将预测下一字改成了预测某一个字。

输入一个序列,将中间的某一个词替换成mask token,训练目标是将token预测回原来的这个字。

这个字不一定出现在句子最后,如果这个mask永远在最后,那么它就是自回归语言模型。

如果mask出现在第三个字,那么我们是通过第一、第二、第四个字去预测第三个字。

任务设计

随机遮盖输入序列中 15% 的 Token,

让模型预测被遮盖的原始词:

输入:我 喜欢 MASK 苹果

目标:预测 MASK = 「吃」

这个任务也被称为完形填空任务。

利用双向信息,预测中间的词。

如果是预测下一个词,那么就是利用单侧的信息。

自回归只关注上文。

但是现在主流的是单向的做法,从前文预测后文的做法。原因如下:

bert时代,人们做的很少是生成式任务。

生成式任务:

模型输入一段话后,预测下一个字的概率,预测出概率最高的字补到你原始的句子后面,然后再进行下一次预测,再补到你句子的后面,再进行下一次的预测。不断的这样,一个字一个字的向后生成。现在的大模型原理是这样。这种做法适合做单向的预训练。

使用的时候也是从前预测后面,做生成式任务。这样是比较适配的。预训练时候的目标和你最终使用时候的目标是接近的。这就是为什么现在的主流是从前向后预测,因为现在主流的用法也是从前向后预测。

18年的时候,生成式任务做的很少,nlp那时候主要是用分类一类的方法去解决的。分类你可以看到整句话在做分类。这样双向的比较有优势。因为双向的是看到整句话再下判断。由于那时候绝大多数的榜单是偏分类类型的任务,所以使用的预训练目标更合适一些。就是使用bert,把某个词替换为mask token去做学习。

预训练任务二:Next Sentence Prediction

将两句话拼接起来送入模型,进行分类,如果两句话是上下文关系,那么认为是正类,如果没有上下文关系,那么认为是负类。

BERT 输入格式

从很长的文本中截取连续的上下文作为正样本,从很长文本中截取相隔很远的非连续上下文为负样本。

所以它也是自监督任务,不需要标注,和前面的完形填空任务一样。

BERT 整体模型结构

预训练目标要设置很合理,预训练数据能较好的吸收,模型结构要有强拟合能力。

transformer层堆叠12层,其实是相同的形状和结构,只要了解其中一层即可。

标签需要人工生成-有监督任务。

标签可以自动生成-自监督任务。

预训练任务一:Masked Language Model(完形填空)和预训练任务二:Next Sentence Prediction

是同步做的。

相当于拿两个任务,一起去训练这个模型。

BERT Embedding 层

token embedding是传统的embedding,就是每个字符对应一个向量。

position embedding是可学习的位置编码,设定了文本最大长度,在bert中是512,在这一层准备了512个向量,第0个位置专门对应了一个可学习的向量,第1个位置也对应一个可学习的向量。以此类推2,3,4,5,一直到512,相当于对应512个可学习向量。对于你任意输入的向量,这层总是先取第0个向量,再取第1个向量,和你的token embedding对位相加,

"我"这个字符 - 蓝色v2(768维度的向量),p1也是768维度的向量,这两个embedding层最终会相加。因为都是768维,可以对位相加,结果还是768维。向量的对位相加不会改变形状。就这样一一对位相加起来。

position embedding只不过这一层与字符无关,只和位置有关。

如果"我"和"爱"位置交换了,p1 和p2这两个向量还是不变的。p1还是第一个位置的向量,p2还是第二个位置的向量。与字符无关,只和顺序有关。

预训练的时候,加了任务,是两句话拼成的。给第一句话所对应的字符都打上0对应的向量,给第二句话对应的字符都打上1对应的向量,让这个模型比较容易区分这两句话。拼接的时候,会往前面默认增加句首的token(CLS).

两句话拼接的时候,中间会有连接专用的token(SEP),句尾会有额外的token(SEP).这样就能区分两句话

python 复制代码
[CLS] Sentence_A [SEP] Sentence_B [SEP]

所以这样一来,本质上分为了三层。第一层是我们正常的字符对应向量,token embedding.第二层是纯看位置position embedding,第三层是用于看是第一句话还是第二句话的,segment embedding.

这里我们就可以计算每一层可训练的参数量。

token embedding可训练参数取决于词表大小(20000)乘以每个词的维度(可能是768维度h)

position embedding可训练参数。指定的最大位置(512 L乘以 768维度h)

segment embedding (2 乘以 768维度,因为它只有两个可训练的向量,要么是第一句话,要么是第二句话)

如果出现第三句话,它可以交替,也就是 000 111 000。 第一个000是第一句话,第二个111是第二句话,第三个000是第三句话。本质上只有两个可训练的向量。

这三个embedding层最终输出的格式是多少?

L x H (768)

文本长度乘以指定的单元数量

如上例子,每层都是768维度的向量,相加后就是 5 乘以 768.

bert_embedding.py

python 复制代码
#coding:utf8
import torch
import torch.nn as nn

'''
embedding层的处理
'''


#构造字符表
vocab = {
    "[pad]" : 0,
    "你" : 1,
    "你好" : 2,
    "中国" : 3,
    "好" : 4,
    "[cls]" : 5,
    "[sep]" : 6,
    "[unk]":7
}
vocab_size = len(vocab)
embedding_dim = 768
token_embedding = nn.Embedding(vocab_size, embedding_dim, padding_idx=0)
position_embedding = nn.Embedding(512, embedding_dim)
segment_embedding = nn.Embedding(2, embedding_dim)

 
#构造输入
#       [cls] 你 你好 中国 [sep] 中国 好 [sep]
token = [5,   1,  2,   3,   6,  3,  4,  6]
pos =   [0,   1,  2,   3,   4,  5,  6,  7]
seg =   [0,   0,  0,   0,   0,  1,  1,  1]


tensor_token = torch.LongTensor(token)
tensor_seg = torch.LongTensor(seg)
tensor_pos = torch.LongTensor(pos)

#计算embedding
token_emb = token_embedding(tensor_token)
print(token_emb.size())
seg_emb = segment_embedding(tensor_seg)
print(seg_emb.size())
pos_emb = position_embedding(tensor_pos)
print(pos_emb.size())

#加和输出
output = token_emb + seg_emb + pos_emb
print(output)
print(output.size())   

执行上面的代码,打印如下内容

我们发现每一层embedding的输出都是 8 乘以 768的输出。

bert embedding层有些缺陷:

比如我们设置最大长度是 512,那么如果输入文本超出了512该怎么办。

那么就没有对应的向量了。当年很少处理512长度以上的任务。现在也很少有这种情况。

当时的解决方案是,如果超出了512,只能做截断。如果截断的太多了,只能送入两次模型,再考虑设计一个方案,再把结果混合起来。

所以position embedding是很大的限制,但是为什么要加这个position embedding?

能不能没有position embedding,是可以的,现在主流的大模型中,我们可以采用相对位置编码,相对位置编码不需要可训练的向量。

之所以当时bert中需要用到position embedding是因为当时的transformer模型结构对于位置不敏感。所以加入这个position embedding,是为了强调位置信息而存在的。

比如我们交换 :"我"和"爱"的位置顺序,就会导致整个的embedding的三层加和的结果不同了。

相当于强调了它的位置不同。因为交换实际上是 v3 和v2交换位置,但是p1和p2是不动的,所以最后会形成比较大的区别。

segment embedding也是,现在也被优化掉了。在当年看来,是有用处的,可以区分两句话,但是实际上模型自己学,这个东西模型可以自己学明白。所以现在主流大模型又返璞归真了,又回到了只有token embedding的时代。

现在之所以能返璞归真,也是因为数据足够的多,大力出奇迹。

bert在很多需要做分类,对速度要求很高的情况下会用到。(10毫秒内出结果的场景,bert是稍小的模型)

大模型有个很大的弊端,就是速度太慢,需要的资源太多。

位置编码必须使用,其实和transformer的结构有关。

位置编码:Transformer 怎么知道谁在前谁在后

Transformer Block 总览

transformer这层的定位是代替rnn层/lstm层。是这两层的替代品。

如下,输入向量相当于是embedding层的输出。当你堆叠多个transformer层的时候,你也可以认为是来自上一层transformer层的输出。(第一层是embedding层,第二层是transformer层,第三层也是transformer层,那么对于这个第三层transformer层,它的输入就来自上一层,也就是第二层transformer层的输出)

输入向量的维度是什么呢?

L (文本长度)x H (768维度)

比如L是512,那么输入就是 512 x 768的张量。

为了保证我的transformer能堆叠多层,那么我们后面的形状应该始终保持这个形状。输出都保证是 512 x 768,这样能保证可以不断堆叠很多层。

multi-head self-attention 和add & layernorm(残差1)是第一大部分。

feed-forward network和add & layernorm(残差2)是第二大部分。

最重要的就是multi-head self-attention

Attention 张量维度变化详解

逐步维度变化(BERT-base 示例,L=128)(multi-head self-attention过程)

d_model其实就是h维度,768

L是文本长度,128

128 * 768 是embedding层的输出。

也就是transformer层的输入。

输入之后,我们做三个线性层投影,三个线性层分别称之为Q、K、V。

三个线性层的维度都是 768 乘以768的。

128 * 768 和 768 * 768 ,结果仍然是。128 * 768.

相当于过了三个线性层,形状没有变化。

过了三个线性层,会得到三个128 * 768的东西。这三个 128 * 768的东西,我们分别称之为 Q、K、V。

三者值不相同,权重不相同,但是形状是相同的,都是 128 * 768.

然后我们将他们分成多个头。变成12个 64维度的矩阵。

比如,对于Q,横向 768个数,纵向 128个数的矩阵,将768竖着切分成12份,那么每一份就是128 * 64。

这样我们就有了 12 * 128 * 64的张量,也就是12个 128 * 64的张量。

我们对Q、K、V都做这件事,也就是Q、K、V都变成了12个 128 * 64的形状,这个动作我们就把它叫做分成12个头。

其实就是将原始的张量切分成了12块。

现在我们有了12个Q、12个K、12个V,形状都是一样的,都是128 * 64.

接下来,我们拿出一个Q(形状是128 * 64,也就是L×d_k)和一个K ,对K我们做一个转置,变成(64 * 128 ,也就是d_k×L),然后这两个矩阵我们相乘,128 * 64和64 * 128 相乘,输出的形状是 128 * 128 (L×L)。

因为我们有12个Q和12个K,我可以得到12个方阵 128 * 128 (L×L)

然后我们要对 128 * 128 (L×L)做逐行的softmax,向量归一化,每一行加和为1,每个数都不小于0.

除以根号下的d_k,也就是 64的开根号,8.

相当于每个数除以8后,再过softmax.

V 也是12个 128 * 64。和Q、K形状一样。

我们用 128 * 128(L×L)方阵和V的 128 *64相乘,Weights( L×L ) × V( L×d_k ) → Output: L×d_k

128 * 64 ,最终我们得到12个 128 * 64 。

这个结果就是Q、K、V都参与运算过后的了。

然后我们再将这个12个 128 * 64 结果拼回128 * 768 。

后面会接一个线性层是768 * 768的,也就是输出层,不改变形状。

这样multi-head self-attention的计算就算完成了。

可以看出形状没有变化,输入的是128 * 768,经过这么一通计算,结果仍然是 128 * 768.

也就是说这里面只有线性层,没有其他层。Q、K、V是线性计算,切分是矩阵乘法。

multi-head self-attention中本质只有线性层。

那么为什么要这么计算呢?

上图Scores (L × L)这个方阵是最重要的部分,是attention.它反映的是文本里每两个字符之间的关系。

纵向是和文本长度一致L,相当于是第一个字符到最后一个字符,横向也是第一个字符到最后一个字符。

Q、K的运算本身就来自于你输入的整体的文本。只是整体的文本过一个线性层后的结果。所以他们的横向纵向都是文本。

Scores (L × L)这个方阵实际上是文本中的某一个字在和文本中的另一个字计算一定的关系,计算一定的相关性。

attention_lab.html

cls是特殊字符,代表句首。

12个头,每个头潜在的关系的意义可能不一样。每个头计算得到的attention在学习不同的东西。

每一行都在和其他的字计算相关性。每个格子是数值,计算相关性,越大颜色越深。这就是真实bert训练好后输出的值。为什么逐行去计算:将某个字分配在所有字上的注意力做了归一化,它与这句话中哪个字的关系最密切,体现在了自己的向量中,向句子里的每个字分配相关性的权重,这种相关性的权重,我们就称之为注意力。

也就是通过L * L的矩阵,计算了每个字符和其他字符之间的关系。然后每个头,12个头中每个头潜在的关系意义不同,有些头可能是从距离角度,语义角度,语法角度去考虑,我们假想每个头学习的内容不同。

每行的值,经过softmax后,加和是1.相当于这个字在像其他的字分配相关性的权重,这种相关性的权重,我们就称之为注意力。有很多字,我把我的注意力放在哪里,哪个字的值就高一些。

所以它称之为自注意力机制。就是这个句子自身的每个字到这个句子自身每个其他字的注意力。这就是自身到自身的注意力。

我的第一个字符和自身计算注意力就和第一个字符和最后一个字符计算注意力差不多,没什么区别。

因为都在一个方阵中解决了。不会因为第一个字符和最后一个字符距离很远,就自然的值变得很低。完全有可能最后一个值是这一行里最高的。

这算是无视距离的相关性计算的性质。

LSTM是顺序的计算,当我们传入小,传入明,到传入最后一个字兴的时候,小这个字的信息就非常淡了。记忆中造成的影响就非常的不明显了。

而transformer中是一个方阵,在这个方阵中,第一个字和最后一个字计算相关性,没有任何压力,和第一个字和第一个字计算相关性本质上是一样的。哪怕两个字距离很远,依然可能给出这行最高的分数。

这就是自注意力机制的优势:当它想计算每个字和其他字的相关性的时候,它无视距离的影响,这就是为什么它处理长文本的情况下比LSTM要强。因为它从计算方式上决定了这点。解决长距离依赖问题的能力强于LSTM。

为什么准备12个头?

它认为它切成多个头,让模型在多个不同的子空间去学习,学习不一样的文本中的规律,字符和字符之间语法规律,拼音规律等 ,每个头去学习不同的东西,使得模型的综合效果比单个头能学习到更复杂的规律。

每个头计算得到的注意力各不相同。

(1)为什么做softmax?

一行代表着这个字分配在整句话上的权重注意力,这个注意力理论上是加和为1的,这个字总共1这么多的精力,精力的分配有高有低,如果不做归一化,可能所有的数都很高,不容易区分高低。

做softmax更容易看出高低。

(2)为什么要先除以根号下d_k后,再做softmax呢?

这里的d_k是64,根号下d_k是8。

softmax本质是一个指数函数,在高区间的增长速度较快,低区间增长速度相对较慢。

如果不除以根号下d_k,会导致高区间的都趋近于1,低区间的都趋近于0,注意力的分配过于集中,高低过于分明了。比如 softmax上x的数值10和100,过完softmax后,10的结果是0,100的结果是1.

但是如果都除以8,两者的x会回到一个小区间来,这样两者差距没那么大,分配注意力会相对均匀一些。总之就是除以8之后,使得两者绝对值缩小了,那么softmax后两者的差距也会缩小。这样注意力就不会分配的很极端。

这个是softmax函数可能出现的常见问题。

(3)为什么会有Q\K\V三个不同的线性层?

主要是希望增加更多的可训练参数,

Q\K\V是各自是一个矩阵,Q和K在学习一定的特定特征。Q和K相乘就代表了一定的信息。再和V相乘,就形成了注意力的加权。Q和K的作用是算出注意力。注意力还是要施加在原始的句子上,V就代表原始的句子,(其实Q\K也可以代表原始的句子,因为三者本质是等价的,虽然权重不同,但是计算方式是相同的,都是线性层)

Q\K两个线性层后,相当于得到了两个对句子的完整表示,用这两个表示来计算注意力。算完注意力后,这个注意力还是要体现在句子本身上,所以将注意力乘回句子的表示上,乘回原来的Q\K不太合适,所以乘在了单独的V上。注意力是一种分配。

有A\B\C三个字符,注意力相当于学到了A\B\C的分配。将分配的系数再乘回A\B\C上。(乘回的过程由V来代表,Q\K用来计算分配)

如上整体便是multi-head self-attention的机制。

(4)为什么是除以根号下d_k,而不是其他的数字。

这本质是实验的结果。论文作者通过实验,认为这个值是一个差不多的值。但其实是可以调整的。

包括12头、768维都是可以调整的。不过总维度越高,头就可以越多,因为头是从总维度切出来的。不可以每个头的维度太小,不然作为一个头没有价值,这样学习能力就很弱。每个头的大小要保持一定的值,

这些值都是可配的。

每个头都是单独的子空间。这些头互不影响,各算各的。整体都是以梯度为指引,去追寻最低的loss.所以其实不需要这些头有什么明确的解释,实在要解释的话,就说这些头在学习不同的东西。

如上就是transformer中最重要的多头自注意力机制。

如果句子很长,那么L X L 的矩阵也会很大,这确实会带来问题,后面有很多优化就是围绕着优化计算来的。

bert中L X L 的矩阵 就是固定 512 X 512大小的。

BERT模型相较LSTM还有一个优点在于它的并行能力。Q\K\V三者可以并行计算,因为他们都是一个线性层和x进行计算,x是确定的。Q\K\V是三个矩阵和X去进行计算,是可以并行计算的。同时12个头的计算也是并行的(12个头之间互相没有依赖,Q\K\V之间也没有依赖)。并行完后再计算后面的。计算量虽然很大,但是运算效率高,可以充分利用GPU进行并行。

所以不需要像LSTM那样,算完第一个字后算第二个字。

(5)为什么transformer这种模型结构比较依赖位置编码?

线性层做矩阵乘法,交互字的顺序的时候,本质只是行的交换。

如果没有位置编码的话,输入的序列X是 128 (L)X 768(d_model)维度的,当我们交换某两个字的位置的时候,其实只是交互了两行的位置,数值并没有发生变化。

Q\K\V也是128 (L)X 768(d_model)矩阵,也一样,只是行列发生顺序交换,但是数值没有变化的,不影响输出的整体数值。交换语序后,信息是一样的,这样是不利于学习的,所以额外添加position embedding强调位置信息。

而LSTM如果某两个字交互顺序,那么记忆会完全不一样。因为每一轮的记忆都和当前输入的值有关系。

加和归一化

残差层

经过Muti-head self-attention,输出128(L) * 768(h),然后进行加和归一化Add & LayerNorm (残差①)

输入前的X和Muti-head self-attention这层处理后的Sublayer(x)两者相加,由于X 和 经过Muti-head self-attention这层处理后的形状都是128(L) X 768(h),形状相同的矩阵是可以对位相加的,加完后形状仍为128(L) X 768(h)。相加完后再过LayerNorm,也就是层归一化。

python 复制代码
Output = LayerNorm( x + Sublayer(x) )

层归一化不改变张量形状。

x + Sublayer(x) 这个动作叫做 残差,可以避免梯度消失。

图像的神经网络经常会使用残差这个方法,残差在越深的网络中,有越好的效果,有避免梯度消失的效果。

如果不加x,那么就相当于盖楼的时候,楼歪了点,持续往下堆很多层,那么这个楼就越来越歪。那么我们把x加回去,一定程度上是把楼往原始的位置拽一拽。残差加了原始数值x,相当于往原始数值上靠,给你的积木搭了一堵墙,让你可以比较好的贴着墙去盖这个积木。这样楼就不容易塌。越深层的网络,越依赖这种机制。

残差作用:尽量保持原始信息不偏离太多。将原始信息加入到了映射后的信息之内。

残差处理完后,结果仍然是 128 X 768的形状。

前馈网络(FFN)Feed-Forward Network

本质上是两个线性层,中间夹一个激活层。

x是输入经过multi-head self-attention后再残差归一化的输出。

xW₁ + b₁ 是过线性层

GELU 是激活函数

在transformer刚提出的时候,这层激活函数用的是RELU,后面实验结果更好的是GELU.

在激活函数后,再过一个线性层 * W₂ + b₂

W₁的维度是768 x. 3072

3072是768的四倍。

也就是 L X 768 乘以 768 X 3072 ,这就使得第一层线性层过后,输出为 L. X 3072

第二个线性层会负责从 3072 映射回 768.

也就是是W₂是3072 x 768, 那么经过第二层线性层过后( L. X 3072与3072 x 768相乘),结果输出 L X 768.

为什么这么操作呢?想多增加一些可训练的参数,让这个模型的拟合能力更强。提供更多的可训练的空间,模型能拟合更为复杂的规律。

python 复制代码
FFN(x) = GELU(xW₁ + b₁)W₂ + b₂

Transformer Block 完整公式:

① z = LayerNorm( x + MHA(x) )

② output = LayerNorm( z + FFN(z) )

multi-head attention

输入x ,与multi-head attention 处理过的x 相加,这叫做残差,然后归一化,归一化后的z再做残差(和两个线性层和一个激活函数处理过的z做残差),最后再做归一化。最后的输出就是一层transformer block的输出output.

这样的transformer block我们堆叠若干层,比如说bert堆叠12层,就是完整的模型结构。

这就是transformer完整的模型结构。

最核心的部分是 L x L scores解决了lstm的长文本之间的上下文依赖问题。

注意力是两个字符之间的相关性,要承载在能表达原始文本的张量上(V)

Q\K相当于算出来的是系数。比如你有40块钱,早饭、午饭、晚饭的比例是10、20、10块,比例是1:2:1,那么Q、K算出来的是这个比例,你得乘上40块钱,才知道每顿饭分配了多少钱。attention提供了原始输入的系数,系数得乘载在原始输入上。

Transformer 高频追问

为什么要除以 √d_k?

目的是不希望softmax变得很极端,也就是非1即0,除以√d_k,将softmax拉到比较小的区间后,softmax对注意力的分配会更加均匀。很极端的情况是不太利于学习的,因为一个字很难和其他字没有关系。

点积的方差随维度 d_k 线性增大,不缩放会让 softmax 落进饱和区、梯度几乎为零。除以 √d_k 把方差拉回 1,训练才稳定。

多头是不是「拆成几份省参数」?

多头是将Q\K\V计算的结果进行切块,没有省参数。计算完还有一个输出的W_O ,也就是一个单独的线性层。回到L x 768(张量的形状的整个过程最好要清楚)

不是。每个头有独立的 W_Q/W_K/W_V,拼接后再乘 W_O 回到 d_model,参数量和单头同一量级(约 4d²)。目的是让不同头在不同子空间学不同关系。

Add & Norm 放在子层前还是后?

求和和归一化,在bert中是先做残差,再做归一化。

BERT 是 Post-LN:先 x + Sublayer(x),再 LayerNorm。层数一深就不太好训,GPT-2 之后普遍改成 Pre-LN ------ 两种都能用,位置不同。

后面改成把归一化Pre-LN 放在Sublayer也就是multi-head attention之前,先做归一化,再进multi-head attention.

为什么 BERT 不能直接做生成?

双向注意力能看到「后面」的词,预训练目标是填空(MLM)而不是续写;自回归生成需要因果掩码,把右侧的注意力屏蔽掉。

bert的预训练机制是完形填空,看到1、2、4,猜测3是什么.不是传统的自回归模型,从1,2,3去猜测4

生成的话都是从前面预测后面,没法先知道后面。

所以bert的预测方式不适合做生成。

注意力的代价是什么?

注意力很大的代价在L x L 的注意力矩阵上,是文本的平方。

时间和显存都是 O(L²):句长翻一倍,注意力矩阵大 4 倍。FlashAttention、稀疏/线性注意力、GQA 都是在打这一项。

参数量怎么估算?

可以自行计算参数量的大小,

W₁ 是 h x 4h ,W₂ 是 4h x h

python 复制代码
FFN(x) = GELU(xW₁ + b₁)W₂ + b₂

Q \ K \V 都是 768 x 768. h. x h

之后有个output. 是 h x h

每层约 12d²(注意力 4d² + FFN 8d²)。BERT-base:12 层 × 12×768² ≈ 85M,加上词表 embedding 共约 110M。

diy_bert.py

https://www.modelscope.cn/models/google-bert/bert-base-chinese

下载模型文件

pytorch_model.bin是开源的bert的真实权重,已经预训练好了的 。经过了语言模型训练的模型了。输入一句话,它输出的向量是确定的,不是随机初始化的模型。所以可以加载它的权重,把权重里的每个w,b拿出来,然后自行实现它的embedding,线性层,self-attention的过程,diy_bert.py做的就是这个事情。模型在hugging-face下载也可以。

2:04:00

gelu激活函数的作用:为模型添加非线性能力。

BertModel官方库

from transformers import BertModel

面试:手动实现bert attention的计算过程,用pytorch实现

可以了解下BertModel的实现:bertattention、bertcrossattention

完整的NLP模型是有embedding层的,有embedding层意味着词表已经固定了。当使用bert时候,要同时使用bert的词表。bert的词表也同时开源出来。

如下modelscope中bert-base-chinese下的vocab.txt就是bert的词表。

这个词表的顺序是不可以改动的,因为embedding层已经与这个词表绑定。

embedding层的5848行对应的位置的向量,必定是这个字的向量,你给他换了,模型就不认了。使用bert的时候,要同时使用bert的词表。

tokenizer.py

tokenizer.py中的BertTokenizer 是分词器。是transformers这个库提供的。BertTokenizer 对一句话进行自动的切分。它会默认为你添加 CLSSEP这些特殊符号,是为了和它预训练阶段保持一致,这样它应用的效果会更好一点。编码完后,就可以送入bert embedding层。

跑deepseek模型也要使用它的词表,词表和模型是绑定的,否则你的输入,它不认可。

作业:训练基于transformer的模型(参考language_model.py),可以使用pytorch.nn.transformer

重点:transformer去训练语言模型,有坑。在预测下一个字的时候,用RNN这个方法,用第一个字计算出来第二个字的结果,用第二个字预测第三个字。但是如果将RNN层改成transformer,那么它其实第一个字会看到第二个字,第二个字会看到第三个字,那么它实际上是抄了,不是在学,会学出一个伪规律。所以transformer要训练单向的模型,需要加一个mask,来对它的注意力做一个遮蔽。可以让AI加MASK和不加MASK看有什么区别。用我们提供的预料训练一个语言模型,让它做文本生成。输入'今天',让它自己预测概率最高的字,补在'今天'后面,然后再将这个三个字输入预测下一个字,再补回去成四个字的输入,再预测第五个字。

建议用transformer去替换LSTM做,用BERT结构也可以,但是会比较慢。bert是12层的模型,用自己的transformer两层即可。可以做输入法,也可以生成文本。

预训练的embedding还会被反向传播更新吗?在微调阶段是会的。

BERT使用的模型结构是TRANSFORMER

句子的结束怎么训练?用终止符训练。

transformers是模型结构,但是确实有个框架也叫这个,但是我们所说transformer是模型结构。transformer block是一个transformer 包,bert用了12层transformer包。

微调改哪些参数?全部都调。

预训练的embedding权重会被微调更新的话,预训练是否也只能一次性使用 ?

预训练是基础,有新的任务,就拿着这个预训练模型为基础,再去做一个新的模型出来,相当于fork出了一个新的分支。预训练模型是develop,其他各新任务,都拉取基于develop分支的新分支。

CPU & GPU

笔记本有8G左右的显存是可以训练bert的。

相关推荐
田里的水稻5 天前
FA_CNN卷积神经网络二十七---填充与步幅PyTorch
人工智能·pytorch·cnn
田里的水稻5 天前
FA_CNN卷积神经网络二十六---从全连接到卷积PyTorch
人工智能·pytorch·cnn
梦想不只是梦与想7 天前
PyTorch、CPU、GPU、CUDA
人工智能·pytorch·python·深度学习·机器学习·cuda
田里的水稻7 天前
FA_CNN卷积神经网络二十四---层\块\参数管理PyTorch
人工智能·pytorch·cnn
NJPJI_Yang7 天前
鸿蒙 HarmonyOS 7 应用内存优化:appMemoryOptimizer 释放 so 页缓存实战
pytorch·深度学习·harmonyos
ZZHow10247 天前
神经网络与深度学习笔记_Day1
人工智能·pytorch·笔记·python·深度学习·神经网络·机器学习
盼小辉丶8 天前
PyTorch强化学习实战——从零开始实现魔方求解器
pytorch·深度学习·强化学习
计算机毕业编程指导师8 天前
基于深度学习的花卉图像分类识别系统(PyTorch + ResNet50 + Django)
pytorch·python·深度学习·分类·毕业设计·迁移学习·图像分类
NJPJI_Yang8 天前
鸿蒙操作系统小艺 Skill 开发实战:让应用被 HarmonyOS 7 一句话召唤
pytorch·深度学习·harmonyos