Pytorch实战笔记(3)——BERT实现情感分析

  • Post author:
  • Post category:其他


本文展示的是使用 Pytorch 构建一个 BERT 来实现情感分析。本文的架构是第一章详细介绍 BERT,其中包括 Self-attention,Transformer 的 Encoder,BERT 的输入与输出,以及 BERT 的预训练和微调方式;第二章是核心代码部分。



1 BERT



1.1 self-attention

Self-attention

接受一个序列输入,并输出等长的序列

。其运行流程如下。

self-attention

上图是 self-attention 的部分实例,因为仅展示了



b

1

b_1







b










1





















的计算过程。其计算过程如下所述(这里仅说明



b

1

b_1







b










1





















的计算过程,



b

2

b_2







b










2

























b

4

b_4







b










4





















的计算方式与



b

1

b_1







b










1





















一样):

  1. 对于输入序列



    {

    a

    1

    ,

    a

    2

    ,

    a

    3

    ,

    a

    4

    }

    \{a_1, a_2, a_3, a_4\}






    {




    a










    1


















    ,





    a










    2


















    ,





    a










    3


















    ,





    a










    4


















    }





    ,当我们计算



    a

    1

    a_1







    a










    1





















    对该输入序列的注意力向量时,



    a

    1

    a_1







    a










    1





















    会经过三次不同的线性变换,得到



    q

    1

    q_1







    q










    1

























    k

    1

    k_1







    k










    1

























    v

    1

    v_1







    v










    1





















    向量,公式如下。这里的 q (query)、k (key)、v (value) 可以用数据库来理解,q 对应的就是 SQL 语句,来查询某个键,最后返回这个键的值,就比如 q 是 ‘select age from girlfriend’,这里 query 就是这个 sql 语句,key 就是 age,value 就是 18。





    q

    1

    =

    W

    q

    a

    1

    ,

    k

    1

    =

    W

    k

    a

    1

    ,

    v

    1

    =

    W

    v

    a

    1

    .

    q_1=W^qa_1, \\ k_1=W^ka_1,\\ v_1=W^va_1.







    q










    1




















    =









    W










    q










    a










    1


















    ,









    k










    1




















    =









    W










    k










    a










    1


















    ,









    v










    1




















    =









    W










    v










    a










    1


















    .





  2. 而对于



    {

    a

    2

    ,

    a

    3

    ,

    a

    4

    }

    \{a_2, a_3, a_4\}






    {




    a










    2


















    ,





    a










    3


















    ,





    a










    4


















    }





    而言,它们是被查询注意力的对象,所以只生成 k 和 v(这里需要注意的是,

    self-attention 是会计算自己对自己的注意力的,所以会有 k1 和 v1

    )。

  3. 接着



    q

    1

    q_1







    q










    1





















    会与



    {

    k

    1

    ,

    k

    2

    ,

    k

    3

    ,

    k

    4

    }

    \{k_1, k_2, k_3, k_4\}






    {




    k










    1


















    ,





    k










    2


















    ,





    k










    3


















    ,





    k










    4


















    }





    分别做一次点积操作,得到注意力权重



    {

    α

    1

    ,

    1

    ,

    α

    1

    ,

    2

    ,

    α

    1

    ,

    3

    ,

    α

    1

    ,

    4

    }

    \{\alpha_{1, 1}, \alpha_{1, 2}, \alpha_{1, 3}, \alpha_{1, 4}\}






    {




    α











    1


    ,


    1



















    ,





    α











    1


    ,


    2



















    ,





    α











    1


    ,


    3



















    ,





    α











    1


    ,


    4



















    }





    ,公式如下。这里需要注意的是,由于



    k

    k






    k





    会经过一次转置,所以注意力权重



    α

    \alpha






    α







    标量

    。同时,由于点积操作可以看做是一次

    相似度的计算

    (因为余弦相似度的计算公式是



    c

    o

    s

    θ

    =

    a

    b

    a

    b

    {\rm cos}\theta=\frac{a \cdot b}{|a||b|}








    cos




    θ




    =























    a


    ∣∣


    b



















    a





    b
























    ,即



    a

    b

    =

    a

    b

    c

    o

    s

    θ

    a \cdot b = |a||b|{\rm cos}\theta






    a













    b




    =











    a


    ∣∣


    b







    cos




    θ





    ,所以内积可以看做是计算两个向量的相似度),所以这里内积就可以理解为计算



    q

    1

    q_1







    q










    1


























    {

    k

    1

    ,

    k

    2

    ,

    k

    3

    ,

    k

    4

    }

    \{k_1, k_2, k_3, k_4\}






    {




    k










    1


















    ,





    k










    2


















    ,





    k










    3


















    ,





    k










    4


















    }





    的一次相似度权重计算

    (因为



    α

    \alpha






    α





    是标量,所以是

    相似度权重

    )。





    {

    a

    1

    ,

    1

    ,

    α

    1

    ,

    2

    ,

    α

    1

    ,

    3

    ,

    α

    1

    ,

    4

    }

    =

    q

    1

    {

    k

    1

    ,

    k

    2

    ,

    k

    3

    ,

    k

    4

    }

    T

    .

    \{a_{1,1}, \alpha_{1, 2}, \alpha_{1, 3}, \alpha_{1, 4}\} = q_1 \{k_1, k_2, k_3, k_4\}^{\rm T}.






    {




    a











    1


    ,


    1



















    ,





    α











    1


    ,


    2



















    ,





    α











    1


    ,


    3



















    ,





    α











    1


    ,


    4



















    }




    =









    q










    1


















    {




    k










    1


















    ,





    k










    2


















    ,





    k










    3


















    ,





    k










    4



















    }












    T











    .





  4. 最后,相似度权重



    {

    α

    1

    ,

    1

    ,

    α

    1

    ,

    2

    ,

    α

    1

    ,

    3

    ,

    α

    1

    ,

    4

    }

    \{\alpha_{1, 1}, \alpha_{1, 2}, \alpha_{1, 3}, \alpha_{1, 4}\}






    {




    α











    1


    ,


    1



















    ,





    α











    1


    ,


    2



















    ,





    α











    1


    ,


    3



















    ,





    α











    1


    ,


    4



















    }









    {

    v

    1

    ,

    v

    2

    ,

    v

    3

    ,

    v

    4

    }

    \{v_1, v_2, v_3, v_4\}






    {




    v










    1


















    ,





    v










    2


















    ,





    v










    3


















    ,





    v










    4


















    }





    相乘,分别得到



    a

    1

    a_1







    a










    1

























    a

    1

    a_1







    a










    1





















    的注意力向量、



    a

    1

    a_1







    a










    1

























    a

    2

    a_2







    a










    2





















    的注意力向量、



    a

    1

    a_1







    a










    1

























    a

    3

    a_3







    a










    3





















    的注意力向量、和



    a

    1

    a_1







    a










    1

























    a

    4

    a_4







    a










    4





















    的注意力向量。接着将这些向量拼起来,就得到了



    b

    1

    b_1







    b










    1

























    b

    1

    b_1







    b










    1





















    里面就包含了



    a

    1

    a_1







    a










    1





















    对整个输入序列的所有注意力向量。



1.2 multi-head self-attention

多头自注意力机制实际上就是计算多次 self-attention。如下图所示。

multi-head self-attention

multi-head self-attention 就是输入的向量会经过



h

h






h





个不同的线性变换,得到



h

h






h





个 q、k、v。比如



h

=

2

h=2






h




=








2





的时候,



a

1

a_1







a










1





















会通过以下公式得到



q

1

1

q_1^1







q










1








1

























k

1

1

k_1^1







k










1








1

























v

1

1

v_1^1







v










1








1

























q

1

2

q_1^2







q










1








2

























k

1

2

k_1^2







k










1








2

























v

1

2

v_1^2







v










1








2



























q

1

1

=

W

1

q

a

1

,

k

1

1

=

W

1

k

a

1

,

v

1

1

=

W

1

v

a

1

,

q

1

2

=

W

2

q

a

1

,

k

1

2

=

W

2

k

a

1

,

v

1

2

=

W

2

v

a

1

.

q_1^1=W^q_1a_1, \\ k_1^1=W^k_1a_1,\\ v_1^1=W^v_1a_1,\\ q_1^2=W^q_2a_1, \\ k_1^2=W^k_2a_1,\\ v_1^2=W^v_2a_1.







q










1








1




















=









W










1








q



















a










1


















,









k










1








1




















=









W










1








k



















a










1


















,









v










1








1




















=









W










1








v



















a










1


















,









q










1








2




















=









W










2








q



















a










1


















,









k










1








2




















=









W










2








k



















a










1


















,









v










1








2




















=









W










2








v



















a










1


















.







接着,每个 self-attention 后的输出,会拼在一起,再通过一个线性转换,得到 multi-head self-attention 的输出。设第一个头的输出为



h

e

a

d

1

head_1






h


e


a



d










1





















,第二个头的输出为



h

e

a

d

2

head_2






h


e


a



d










2





















,最后的输出为



O

O






O





,则其计算公式为:





O

=

c

o

n

c

a

t

(

h

e

a

d

1

,

h

e

a

d

2

)

W

o

.

O = {\rm concat}(head_1, head_2)W^o.






O




=










concat




(


h


e


a



d










1


















,




h


e


a



d










2


















)



W










o









.







1.3 Encoder

这里的 Encoder 特指的是 Transformer[1] 中的 Encoder(左边是 Transformer 的 Encoder,右边是 Decoder),其模型结构如下:

Transformer

Encoder 中一共有以下几个部分:


  • Multi-head self-attention

    :在前面已介绍过了。

  • 残差连接 (Residual connection)

    [2]:对应的是图中的

    Add

    。残差连接如下图所示。简单来说,

    残差连接就是将一个模块的输入与其输出相加

    ,通常使用在

    层次较深的结构当中

    。那么为什么残差连接在深层次模型中有效?具体而言,如果不采用残差连接,那么前向传播为



    F

    (

    x

    )

    F(x)






    F


    (


    x


    )





    ,反向传播的时候,求梯度就为



    (

    F

    (

    x

    )

    )

    x

    \frac{\partial (F(x))}{\partial x}





















    x



















    (


    F


    (


    x


    ))
























    ,当梯度消失的时候,



    (

    F

    (

    x

    )

    )

    x

    \frac{\partial (F(x))}{\partial x}





















    x



















    (


    F


    (


    x


    ))
























    就为0,就无法回传梯度。而当采用了残差连接后,前向传播变为



    F

    (

    x

    )

    +

    x

    F(x) + x






    F


    (


    x


    )




    +








    x





    。从直觉上来讲,这样能够让模型更关注于经过了这个模块后变化的部分;而从数学上来将,在反向传播的时候会变成



    (

    F

    (

    x

    )

    +

    x

    )

    x

    =

    (

    F

    (

    x

    )

    )

    x

    +

    1

    \frac{\partial (F(x)+x)}{\partial x}=\frac{\partial (F(x))}{\partial x}+1





















    x



















    (


    F


    (


    x


    )


    +


    x


    )























    =























    x



















    (


    F


    (


    x


    ))























    +








    1





    。当梯度消失后,那么



    (

    F

    (

    x

    )

    )

    x

    \frac{\partial (F(x))}{\partial x}





















    x



















    (


    F


    (


    x


    ))
























    趋近于0,所以



    (

    F

    (

    x

    )

    +

    x

    )

    x

    \frac{\partial (F(x)+x)}{\partial x}





















    x



















    (


    F


    (


    x


    )


    +


    x


    )
























    趋近于1,使得梯度无法消失,始终能够回传。

    残差连接


  • 层归一化 (layer norm)

    [3]:对应的是图中的

    Norm

    。层归一化的公式如下所示。其中,



    m

    m






    m





    是向量



    x

    i

    x_i







    x










    i





















    的均值,



    σ

    \sigma






    σ





    是向量



    x

    i

    x_i







    x










    i





















    的标准差。层归一化的示例图如下所示。具体而言,如果数据不做归一化,有可能在某些方向上梯度下降很快(从左下到右上),这样会导致越过最优点;有的方向上下降很慢(从右下到左上),这样会导致半天收敛不到最优点。而通过层归一化后,能够使得数据在各个方向上都能够下降的一样快,使得能够更快收敛。





    x

    i

    =

    x

    i

    m

    σ

    x_i’=\frac{x_i-m}{\sigma}







    x










    i































    =



















    σ















    x










    i

























    m

























    layer norm


  • 位置嵌入 (Positional Encoding)

    :对应的是图中最下面的

    Positional Encoding

    。为什么要位置嵌入?由于 self-attention 可以看做是下图这样,两个位置之间间隔为

    1

    。如果不能理解为什么是

    1

    ,可以再回过头看看上面那个 gif。那么这样会导致一个问题,对于自然语言处理的任务而言,词语的先后顺序肯定是很重要的,就比如我现在这里写到了 positional encoding,那么和第一小节写的 self-attention 关联就很弱了,所以需要通过位置嵌入来控制词语与词语之间的位置。

    self-attention

  • 全连接层

    :对应图中的

    Feed forward

    ,没什么好说的,唯一要注意的是,这里是

    两层全连接层

    ,公式如下:





    F

    F

    N

    (

    x

    )

    =

    W

    2

    (

    R

    e

    L

    U

    (

    W

    1

    x

    +

    b

    1

    )

    )

    +

    b

    2

    FFN(x)=W_2({\rm ReLU}(W_1x+b_1))+b_2






    FFN


    (


    x


    )




    =









    W










    2


















    (




    ReLU




    (



    W










    1


















    x




    +









    b










    1


















    ))




    +









    b










    2























1.4 BERT 的输入与输出



1.4.1 BERT 的输入

BERT 的输入与传统的语言模型输入不同,传统的语言模型的输入就只是整个句子,而 BERT 在输入中还加入了几个特殊的字符。其中包括:


  • [CLS]

    :[CLS] 一定出现在句首,这个特殊字符

    通过 BERT 后得到的隐藏状态代表了该句子的句向量。

    [CLS] 是

    一定会有

    的。

  • [SEP]

    :[SEP] 一定出现在句子的结尾。由于 BERT 支持

    单句和两句话输入

    ,所以用 [SEP] 来区分哪句话是哪句话。[SEP] 是

    一定会有

    的。

  • [MASK]

    :[MASK] 会出现在 [CLS] 与 [SEP] 中的任意位置,该特殊字符是让 BERT 去

    预测这个位置是什么词语

    。[MASK]

    不一定会有

以以下两句话为例

练习时长两年半



唱跳 rap 打篮球

,那么输入进 BERT 后会变成以下这样:

[CLS] 练习时长两年半 [SEP] 唱跳 rap 打篮球 [SEP]

;如果只有前一句话输入,并且掩盖掉



的话,那么是如下这样:

[CLS] 练习时长两年[MASK] [SEP]



1.4.2 BERT 的输出

与传统的序列模型一样,BERT 的输出有两部分:


  • 句向量

    :通过模型后,[CLS] 的隐藏状态即句向量。

    如果是一句话输入,那么就是这句话的句向量;如果是两句话输入,那么就是这两句话的句向量。

  • 每个词语的隐藏状态

    :和 LSTM 一样,BERT 也会输出每个词语的隐藏状态。这里特别需要注意的是,

    所谓的 BERT 的词嵌入,实际上指的就是这个通过 BERT 后的隐藏状态,而非 BERT 的嵌入层。

    这是因为 BERT 是

    基于上下文的词嵌入(contextualized word embedding)

    ,你得有上下文信息,才能叫词嵌入。



1.5 BERT 预训练

BERT 预训练有两个部分,第一个部分是

masked language model (MLM)

,第二部分是

next sentence prediction (NSP)

  • MLM 简单来说就是随机将输入文本中

    15%

    的词语给提取出来,然后进行以下处理:1.

    80%

    的可能,将词语替换为

    [MASK]

    ,这是让模型通过上下文来预测这

    [MASK]

    是什么词语;

    10%

    的可能,将词语随机替换为另外一个词语;

    10%

    的可能,保持词语不变。MLM 如下图所示。MLM 是个



    V

    V






    V





    分类任务,其中



    V

    V






    V





    是词表大小。

    MLM

  • NSP 简单来说就是输入两句话到模型中,让模型判断后一句话是否与前一句话有关联。NSP 如下图所示。NSP 是个二分类任务,其中 1 代表上下两句话有关联,0 代表没有关联。

    NSP



1.6 BERT 微调

微调阶段,首先 BERT 会先加载预训练好的参数,并额外添加上部分随机初始化的参数。如下图所示。图中,用橙色标出来的全连接层,就是在微调阶段随机初始化的参数。所以微调阶段,训练的为两部分内容:


  • 模型本身

    :这部分是可以不参与训练的,因为模型已经在预训练阶段训练好了,不是必须训练的。

  • 随机初始化的参数

    :这部分是必须训练的参数。

    BERT 微调



2 BERT 实现情感分析

具体的模型代码如下:

import torch
import torch.nn as nn
from transformers import BertTokenizer, BertConfig, BertForSequenceClassification


class Config:
    def __init__(self):
        # 训练配置
        self.seed = 22
        self.batch_size = 64
        self.lr = 1e-5
        self.weight_decay = 1e-4
        self.num_epochs = 100
        self.early_stop = 512
        self.max_seq_length = 128
        self.save_path = '../model_parameters/BERT_SA.bin'

        # 模型配置
        self.bert_hidden_size = 768
        self.model_path = 'bert-base-uncased'
        self.num_outputs = 2


class Model(nn.Module):
    def __init__(self, config, device):
        super().__init__()
        self.config = config
        self.device = device
        tokenizer_class, bert_class, model_path = BertTokenizer, BertForSequenceClassification, config.model_path
        bert_config = BertConfig.from_pretrained(model_path, num_labels=config.num_outputs)
        self.tokenizer = tokenizer_class.from_pretrained(model_path)
        self.bert = bert_class.from_pretrained(model_path, config=bert_config).to(device)

    def forward(self, inputs):
        tokens = self.tokenizer.batch_encode_plus(inputs,
                                                  add_special_tokens=True,
                                                  max_length=self.config.max_seq_length,
                                                  padding='max_length',
                                                  truncation='longest_first')

        input_ids = torch.tensor(tokens['input_ids']).to(self.device)
        att_mask = torch.tensor(tokens['attention_mask']).to(self.device)

        logits = self.bert(input_ids, attention_mask=att_mask).logits

        return logits

实验结果如下:

test loss 0.281900 | test accuracy 0.878846 | test precision 0.853424 | test recall 0.915280 | test F1 0.883270



参考

[1] Ashish Vaswani, Noam Shazeer, Niki Parmar, et al. Attention is all you need [EB/OL].

https://arxiv.org/abs/1706.03762

, 2017.

[2] Kaiming He, Xiangyu Zhang, Shaoqing Ren, et al. Deep Residual Learning for Image Recognition [EB/OL].

https://arxiv.org/abs/1512.03385

, 2015.

[3] Jimmy Lei Ba, Jamie Ryan Kiros, Geoffrey E. Hinton. Layer Normalization [EB/OL].

https://arxiv.org/abs/1607.06450

, 2016.



版权声明:本文为qq_35357274原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。