通俗易懂系列机器学习之手撕bert

优质的预训练模型
啥意思,举个例子,你看了二十四史、资治通鉴、史记,你对历史知识以及发展规律有很多了解,但是直接让你去高考文科历史,估计难以拿高分,但是给你学习一下考纲,针对性训练下,你就可以拿很高分,估计很多应试教育的人都pk不过你。这样可以通俗理解预训练和fine-tune

bert的预训练貌似是维基百科啥的语料,这个模型会看很多文字资料,这样会把一些语言规律、语言的语义、上下位概念之类的信息集成到这个大模型的参数之中。

那么第一步,我们先看看模型长啥样(细节上小问题先忽略哈,注重整体理解,不拘泥于一招一式的剑法,而要懂得剑意精髓)

拿好纸和笔,边看边画效果更佳哦。

1.bert模型长啥样

1.1前面说过,看模型先看输入

括号里的进阶部分,初看时可以不看。

1.1.1举个栗子:I like strawberries

tokenization:[CLS] I like straw ##berries [SEP] 其中[CLS]和[SEP]为填充字符,至于为什么可以先不管,看到最后就知道了。
向量化:
(1)将上述每一个token embedding为一个向量(进阶了解,初始化后lookup table查找也可以索引向量与embedding矩阵相乘得到),假设embedding都为128维,则得到6 * 128向量/tensor
(2)将上述每个token的position位置0 1 2 3 4 5,也各自embedding为一个向量,同上,得到6 * 128向量/tensor
(3)上述整体为一个句子,于是为0 0 0 0 0 0,如果为A、B句pairs,则前半句每个token对应这部分为0,后半句对应1;与上述token方式一样embedding,得到6 * 128向量/tensor

对于两个序列:
 tokens:   [CLS] is this jack ##son ##ville ? [SEP] no it is not . [SEP]
 type_ids/Segment:    0   0  0    0    0      0     0   0   1  1  1  1   1   1
对于一个序列:
 tokens:   [CLS] the dog is hairy . [SEP]
 type_ids/Segment:   0    0   0   0  0    0  0

(4)将上述暴力相加得到6 * 128向量/tensor(进阶探索,不暴力相加也行,还可以拼接为6 * 384向量/tensor然后全连接为6 * 128向量/tensor,不过既然bert他们相加就可以,估计无所谓)

1.1.2三个向量字典:

token向量字典(key为token,value为向量,随机初始化然后训练进行优化,字典长度一般几万吧);
position向量字典(key为position,value为向量,随机初始化然后训练进行优化,字典长度取决于预训练时设置的最大token序列长度,一般也就几百);
Segment 句向量字典(key为0、1,value为向量,bert是随机初始化,然后训练进行优化,字典长度为2),只有对句pairs前句都为0,后句都为1,单句情况下每个token的Segment都为0

如果没看懂,可以看下这位同学的画图,很清晰https://www.cnblogs.com/d0main/p/10447853.html。

是不是发现同样一个token,位置不一样,生成的向量可能不一样了,这才符合我们实际语言的认知啊

1.1.3源码进阶延伸

延伸1:
会设置一个input的最大字符数长度,比如200这种,多的截断,少的用某个字符补上,bert对于补上的字符做了标记,在后续transform结构中会处理为近似为0的权重。其实我觉得,只要是专门的填充字符,不用专门做这个权重处理吧。

延伸2:
有没有看到strawberries被切开了,这里可以看下bert源码WordpieceTokenizer:This uses a greedy longest-match-first algorithm to perform tokenization using the given vocabulary.
input = “unaffable”
output = [“un”, “##aff”, “##able”]
简单来说,有些词不存在词典vocab里(OOV问题),那么切开匹配,怎么切,longest-match-first algorithm

延伸3:
那么词典怎么生成,当然对应英文单词来说空格切开,中文就是一个汉字,但是还记得延伸1中的"##aff"这种么,这就涉及到Subword策略,可阅读链接https://plmsmile.github.io/2017/10/19/subword-units/进阶了解,这种策略的好处就是减小词典、一定程度解决OOV问题。那为什么英文情况下词典不直接就用26个英文字母呢,我觉得理论上效果应该没问题,可是这样变成token序列之后就很长了,效率降低很多吧?

延伸4:
源码里的一些小trick,比如去除利用unicode码来判断是否是中文、检测控制字符(’\n’这种)、大小写是否归一。

延伸5:
感觉那个Segment向量理论上是不是可以不要啊,[SEP]貌似隐式的确定了它们的边界。对于分类任务,[CLS]对应的transformer后面的向量可以被看成 “sentence vector”,Fine-Tuning之后才有意义。

延伸6:
相加以后的向量还要经过一个layer_norm_and_dropout,对最后一维做norm,即只对embedding_size这一维做norm,这个好理解,原本3组向量假设都是均值0标准差1的截断正态分布初始化,那么相加以后标准差应该是 3 \sqrt{3} 3 ,normalization下合情合理;关于这个dropout,没事drop一下房过拟合?

1.1.4 至此,任何一句话/一个样本都变成了一个n * m的向量/tensor输入模型,n表示多少个token,m表示embedding的维度,可以进入后续模型了
1.2 再看模型结构:中间的encode层,transformer

首先啰嗦下,经过1.1的处理,文本/字符串序列数据被转化为一个n * m的向量/tensor,n表示多少个token,m表示embedding的维度。

接下来transformer:
其实很简单,self-attention嘛,来,建议边看边在草稿纸上画写,应该很轻松。

1.2.1 step1:

上面的例子,输入为一个6*128维的向量(6个128维向量) [ v e c 1 , v e c 2 , v e c 3 , v e c 4 , v e c 5 , v e c 6 ] [vec_1,vec_2,vec_3,vec_4,vec_5,vec_6] [vec1,vec2,vec3,vec4,vec5,vec6] ,然后对于每一个128向量设置一个对应3个全连接层 f 1 − q 、 f 1 − k 、 f 1 − v f_{1-q}、f_{1-k}、f_{1-v} f1qf1kf1v(带激活函数) ,暂且都设置(这不影响理解)为128 * hidden_size大小,假设hidden_size为512,那么每个128维向量都可以映射为3个512维度的向量,即得到 [ ( q 1 , k 1 , v 1 ) , ( q 2 , k 2 , v 2 ) , ( q 3 , k 3 , v 3 ) , ( q 4 , k 4 , v 4 ) , ( q 5 , k 5 , v 5 ) , ( q 6 , k 6 , v 6 ) ] [(q_1,k_1,v_1),(q_2,k_2,v_2),(q_3,k_3,v_3),(q_4,k_4,v_4),(q_5,k_5,v_5),(q_6,k_6,v_6)] [(q1,k1,v1),(q2,k2,v2),(q3,k3,v3),(q4,k4,v4),(q5,k5,v5),(q6,k6,v6)]

然后就是一个公式 A t t e n t i o n ( Q , K , V ) = s o f t m a x ( Q ∗ K T / d k ) ∗ V Attention(Q,K,V)=softmax(Q*K^T/\sqrt{d_k})*V Attention(Q,K,V)=softmax(QKT/dk )V

1.2.2 step2:

我知道这个公式有的人看起来难受,继续那上面的例子来画,用 q 1 q_1 q1分别与 k 1 、 k 2 、 k 3 、 k 4 、 k 5 、 k 6 k_1、k_2、k_3、k_4、k_5、k_6 k1k2k3k4k5k6向量点乘得到一个6维数组,每个元素为浮点数,将6维浮点数向量除以 d k \sqrt{d_k} dk 即向量 k k k的维度(这里就是512)的平方根,然后softamx(带dropout,这里有细节见后面的进阶延伸1),得到一个6维向量 [ a 1 , a 2 , a 3 , a 4 , a 5 , a 6 ] [a_1,a_2,a_3,a_4,a_5,a_6] [a1,a2,a3,a4,a5,a6],然后 v f 1 − 1 = a 1 ∗ v 1 + a 2 ∗ v 2 + a 3 ∗ v 3 + a 4 ∗ v 4 + a 5 ∗ v 5 + a 6 ∗ v 6 v_{f1-1} = a_1*v_1+a_2*v_2+a_3*v_3+a_4*v_4+a_5*v_5+a_6*v_6 vf11=a1v1+a2v2+a3v3+a4v4+a5v5+a6v6得到一个512维度的向量 v f 1 − 1 v_{f1-1} vf11,与 v e c 1 vec_1 vec1对应;同理得到512维度的 v f 1 − 2 、 v f 1 − 3 、 v f 1 − 4 、 v f 1 − 5 、 v f 1 − 6 v_{f1-2}、v_{f1-3}、v_{f1-4}、v_{f1-5}、v_{f1-6} vf12vf13vf14vf15vf16 v e c 2 、 v e c 3 、 v e c 4 、 v e c 5 、 v e c 6 vec_2、vec_3、vec_4、vec_5、vec_6 vec2vec3vec4vec5vec6对应。

1.2.3 step3:

整理一下,上述输入为一个6*128维的向量 [ v e c 1 , v e c 2 , v e c 3 , v e c 4 , v e c 5 , v e c 6 ] [vec_1,vec_2,vec_3,vec_4,vec_5,vec_6] [vec1,vec2,vec3,vec4,vec5,vec6],经过这个套路可以得到一个6*512维度的向量 [ v f 1 − 1 、 v f 1 − 2 、 v f 1 − 3 、 v f 1 − 4 、 v f 1 − 5 、 v f 1 − 6 ] [v_{f1-1}、v_{f1-2}、v_{f1-3}、v_{f1-4}、v_{f1-5}、v_{f1-6}] [vf11vf12vf13vf14vf15vf16],这就是self-Attention,不是self的可以延伸想一下,并不难。

1.2.4 step4:

multihead,很简单。
上述1组全连接层为 f 1 q 、 f 1 k 、 f 1 v f_{1q}、f_{1k}、f_{1v} f1qf1kf1v,将 [ v e c 1 , v e c 2 , v e c 3 , v e c 4 , v e c 5 , v e c 6 ] [vec_1,vec_2,vec_3,vec_4,vec_5,vec_6] [vec1,vec2,vec3,vec4,vec5,vec6]转化为 [ v f 1 − 1 、 v f 1 − 2 、 v f 1 − 3 、 v f 1 − 4 、 v f 1 − 5 、 v f 1 − 6 ] [v_{f1-1}、v_{f1-2}、v_{f1-3}、v_{f1-4}、v_{f1-5}、v_{f1-6}] [vf11vf12vf13vf14vf15vf16],那么对于另一组全连接层 f 2 q 、 f 2 k 、 f 2 v f_{2q}、f_{2k}、f_{2v} f2qf2kf2v同理可得到 [ v f 2 − 1 、 v f 2 − 2 、 v f 2 − 3 、 v f 2 − 4 、 v f 2 − 5 、 v f 2 − 6 ] [v_{f2-1}、v_{f2-2}、v_{f2-3}、v_{f2-4}、v_{f2-5}、v_{f2-6}] [vf21vf22vf23vf24vf25vf26],以此类推…
有多少组全连接层,就是多少个head。

1.2.5 step5:

假设10 heads,即10组全连接层,那么可以得到10组6*512维度的向量,将对应位置的向量concat得到,得到一个6*5120维度的向量,即将上述的 v f 1 − 1 、 v f 2 − 1 、 v f 3 − 1 、 . . . 、 v f 10 − 1 v_{f1-1}、v_{f2-1}、v_{f3-1}、...、v_{f10-1} vf11vf21vf31...vf101concat为一个5120向量与 v e c 1 vec_1 vec1对应

1.2.6 step6:

输入一个6*128维的向量经过得到一个10 heads attention操作得到6*5120维度的向量,然后经过一个全连接层 f s f_s fs 大小为 5120 * 128(带dropout、layernorm、residential之类),映射为一个6*128维的向量,当然我这里简化,实际上你可以多来几层全连接啥啥啥的,这不重要。
重要的是,你可以看到输入一个6*128维的向量,经过一系列骚操作得到一个6*128维的向量,那把这套骚操作复制下去不就行了,那就是transformer的层数,比如bert精简版好像是12层。

1.2.7 进阶延伸1

对于mask掉的位置和长度不足pad的位置,在上文进入softmax之前,会把对应的位置那个点乘浮点数置换为一个很大的负数,以至于softmax之后接近0,这个很好理解啦,就是这个位置的信息被mask掉。这个也和预训练的设置有关系。

1.2.8 至此:一个6 * 128维的向量经过多头、多层的self-attention转变为一个6*128维的向量,整体上不过是一个encoder而已。当然hidden-size什么的维度问题自己控制就好,毕竟加个全连接层可以轻松的变换向量维度(size)。
1.3 看完模型输入、结构,再看loss怎么构建的

fine-tune的时候和具体的任务相关,此处以pre-train和文本分类fine-tune为例介绍:

1.3.1 pre-train的loss:本质上多分类loss+2分类loss

预训练的loss = mask词的预测loss1 + 两句话是否是上下文预测 loss2
mask词的预测loss1:

step1:因为都是矩阵操作,所以源码对新同学看起来可能有点吃力,我这里简要描述下。上文transformer将输入的6 * 128维的向量encoder为6 * 128维,再经过一个全连接层,将维度转化为和词表的embedding size一样,比如这里的全连接层为128*128,由此得到6 * 128维度向量(你可能觉得这里多此一举,实际上当transformer输出不是128维,是1280维的时候,这个全连接层就有用了)

step2:假设6个token的第2个词和第4个词被mask掉了,那么需要对这两个地方的token进行预测loss计算。对于第二个token位置,将step1得到6*128向量的第二个128维向量与词表中每一个词的embedding向量做点乘(这就是step1全连接层的目的),加上一个bais,(假设词典有50000个)这个时候会得到一个50000维度的实数向量,然后softmax,然后与一个50000维的0、1向量点乘(1的位置代表mask掉的词在词典中的索引),得到loss,取负号(预测越准,loss越小)。举个例子其实很简单,假设softmax之后的向量如下[0.001, 0.003, 0.009, 0.012, 0.0001,…0.023](50000维),上文被mask掉的第二个词在词典vocab中位置是第5个,那么这个0、1向量为[0, 0, 0, 0, 1, … ,0](50000维),对于第二个词的loss为-0.0001
step3:同理得到第4个词的预测loss,假设为-0.0003,两者取平均值得到-0.0002为这个样本的整体预测词loss。一个batch的loss,再对batch取个平均呗。

两句是否是上下文的预测loss2:
还记得第一个token是“[CLS]”么,和他对应的那个encoder之后的向量也就是那个输出6*128向量的第一个,然后接个全连接层(+bais),映射为一个2维向量,二分类问题,就不多说了。

1.3.2 loss解释

总的来说,很简单,对于mask词的loss,就是讲对应位置的encode 向量(上文是128维)做一个vocab size(上文是50000)的分类,取loss,然后对多个词、多个样本平均;对于是否是上下文,就是将“[CLS]”的encode 向量做一个2分类,求loss

1.3.2 文本多分类fine-tune的loss

参考两句是否是上下文的预测loss2,比如你的分类是20分类,将“[CLS]”的encode 向量映射为一个20维度向量,然后去取loss(交叉熵什么的都随意)

2.bert的其他

思考1:本质上是有监督loss优化,但是语料是不需要人工标注的,word2vec不也是这个逻辑么。

经验2:做fine-tune的时候,注意学习率的设置,很容易就飞了。显卡尽量大一点,模型不小,我用的2080ti,还行。

思考3:感觉bert这种mask预测、上下文预测构建损失、transformer结构之前其他论文多多少少都有,所以google这是将有用的套路放在一起,然后怼上计算力和庞大语料开干

思考4:其实pre-train上finetune,比如你做的是豆瓣上评论相关的事情,是不是可以搞一批语料在pre-trained的bert基础上再pre-train一词,然后fine-tune具体的业务

先就到这把,感冒了发烧了。。。。

你可能感兴趣的:(机器学习)