2021-08-22

bert代码

原始链接 作者看的特别详细,是我楷模。
BERT Tokenization 分词模型(BertTokenizer)

BERT Model 本体模型(BertModel)

  • BertEmbeddings
  • BertEncoder
    • BertLayer
      • BertAttention
      • BertIntermediate
      • BertOutput
    • BertPooler
BERT Tokenization 分词模型(BertTokenizer)

BertTokenizer 是基于BasicTokenizerWordPieceTokenizer的分词器:

  • BasicTokenizer负责处理的第一步——按标点、空格等分割句子,并处理是否统一小写,以及清理非法字符。

    • 对于中文字符,通过预处理(加空格)来按字分割;
    • 同时可以通过never_split指定对某些词不进行分割;
    • 这一步是可选的(默认执行)。
  • WordPieceTokenizer在词的基础上,进一步将词分解为子词(subword)

    • subword 介于 char 和 word 之间,既在一定程度保留了词的含义,又能够照顾到英文中单复数、时态导致的词表爆炸和未登录词的 OOV(Out-Of-Vocabulary)问题,将词根与时态词缀等分割出来,从而减小词表,也降低了训练难度;
    • tokenizer = “token”+ “##izer”

BertTokenizer 有常用方法如下:

  • from_pretrained:从包含词表文件(vocab.txt)的目录中初始化一个分词器;
  • tokenize:将文本(词或者句子)分解为子词列表;
  • convert_tokens_to_ids:将子词列表转化为子词对应下标的列表;
  • convert_ids_to_tokens :与上一个相反;
  • convert_tokens_to_string:将 subword 列表按“##”拼接回词或者句子;
  • encode:对于单个句子输入,分解词并加入特殊词形成“[CLS], x, [SEP]”的结构并转换为词表对应下标的列表;对于两个句子输入(多个句子只取前两个),分解词并加入特殊词形成“[CLS], x1, [SEP], x2, [SEP]”的结构并转换为下标列表;
  • decode:可以将 encode 方法的输出变为完整句子。

使用


2-Model-BertModel

本体使用:默认当作encoder;可以当作decoder:obj:is_decoder设置为obj:True。seq2seq 模型:设置两个地方:obj:True; 和obj:encoder_hidden_statesadd_cross_attention` )

BertModel 主要为 transformer encoder 结构,包含三个部分:

  1. embeddings,即BertEmbeddings类的实体,根据单词符号获取对应的向量表示;
  2. encoder,即BertEncoder类的实体;
  3. pooler,即BertPooler类的实体,这一部分是可选的。

Bert本体输入参数:

Bert embedding:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GvKzk7S4-1629644216648)(C:\Users\朱朱\AppData\Roaming\Typora\typora-user-images\image-20210822221202679.png)]

  1. word_embeddings,上文中 subword 对应的嵌入。
  2. token_type_embeddings,用于表示当前词所在的句子,辅助区别句子与 padding、句子对间的差异。
  3. position_embeddings,句子中每个词的位置嵌入,用于区别词的顺序。和 transformer 论文中的设计不同,这一块是训练出来的,而不是通过 Sinusoidal 函数计算得到的固定嵌入。一般认为这种实现不利于拓展性(难以直接迁移到更长的句子中)。

三个 embedding 不带权重相加,并通过一层 LayerNorm+dropout 后输出,其大小为(batch_size, sequence_length, hidden_size)。

插入:为什么用layernorm,不用batch norm?

  1. layer normalization 有助于得到一个球体空间中符合0均值1方差高斯分布的 embedding, batch normalization不具备这个功能。

    解释:NLP任务真正学习的开端是从"embedding"开始的,而embedding 是学习得来的,emmbedding并不存在一个客观的分布,相反我们需要考虑的是:我们希望得到一个符合什么样分布的embedding?

    很好理解,通过layer normalization得到的embedding是 以坐标原点为中心,1为标准差,越往外越稀疏的球体空间中。

  2. layer normalization可以对transformer学习过程中由于多词条embedding累加可能带来的“尺度”问题施加约束,相当于对表达每个词一词多义的空间施加了约束,有效降低模型方差。batch normalization也不具备这个功能。

    bert词向量:每个词有一片相对独立的小空间,通过在这个小空间中产生一个小的偏移来达到表示一词多义的效果,可以表示为下面这个公式:
    E i = α E i + β E j Ei = αEi + βEj Ei=αEi+βEj
    transformer每一层都做了这件事也就是在不断调整每个词在空间中的位置。具体的Ei看成当前词,Ej可以看成所有环境词的加权和。 这个过程会有一个潜在风险:Ej是不可控的,环境词向量和环境词的数量对Ej都有影响,这就可能造成偏移量会很大,相加后的“尺度”也有可能发生较大变化,尤其是transformer的维度较高,每个维度一个很小的变化也可能引起很大的“尺度”变化。

    然后,再考虑下layer normalization,你会发现,当Ej较小时候,layer normalizaiton基本不起作用,但是当Ej较大时,layer normalization 可以将相加后得到的向量拉回到原来向量的附近,让它不至于跑太远,显然 batch normalizaiton 起不到这样的作用。

BertEncoder

包含多层 BertLayer,这一块本身没有特别需要说明的地方,不过有一个细节值得参考:利用 gradient checkpointing 技术以降低训练时的显存占用。

gradient checkpointing 即梯度检查点,通过减少保存的计算图节点压缩模型占用空间,但是在计算梯度的时候需要重新计算没有存储的值,参考论文《Training Deep Nets with Sublinear Memory Cost》。

BertAttention
class BertAttention(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.self = BertSelfAttention(config)
        #self  多头注意力
        self.output = BertSelfOutput(config)
        # output 实现attention后的全连接+dropout+residual+LayerNorm 
        self.pruned_heads = set()
        
    def prune_heads(self, heads):
        """
        find_pruneable_heads_and_indices是定位需要剪掉的 head,以及需         要保留的维度下标 index;

        prune_linear_layer则负责将 Wk/Wq/Wv 权重矩阵(连同 bias)中          按照 index 保留没有被剪枝的维度后转移到新的矩阵。 接下来就到重头戏          ——Self-Attention 的具体实现。
        """
        if len(heads) == 0:
            return
        heads, index = find_pruneable_heads_and_indices(
            heads, self.self.num_attention_heads, self.self.attention_head_size, self.pruned_heads
        )

        # Prune linear layers
        self.self.query = prune_linear_layer(self.self.query, index)
        self.self.key = prune_linear_layer(self.self.key, index)
        self.self.value = prune_linear_layer(self.self.value, index)
        self.output.dense = prune_linear_layer(self.output.dense, index, dim=1)

        # Update hyper params and store pruned heads
        self.self.num_attention_heads = self.self.num_attention_heads - len(heads)
        self.self.all_head_size = self.self.attention_head_size * self.self.num_attention_heads
        self.pruned_heads = self.pruned_heads.union(heads)
BertSelfAttention【核心区域】

首先回顾一下 multi-head self-attention 的基本公式:

M H A ( Q , K , V ) = C o n c a t ( h e a d 1 , . . . , h e a d h ) W O MHA(Q, K, V) = Concat(head_1, ..., head_h)W^O MHA(Q,K,V)=Concat(head1,...,headh)WO
h e a d i = S D P A ( Q W i Q , K W i K , V W i V ) head_i = SDPA(QW_i^Q, KW_i^K, VW_i^V) headi=SDPA(QWiQ,KWiK,VWiV)
S D P A ( Q , K , V ) = s o f t m a x ( Q K T ( d k ) ) V SDPA(Q, K, V) = softmax(\frac{QK^T}{\sqrt(d_k)})V SDPA(Q,K,V)=softmax(( dk)QKT)V

1. positional_embedding_type:
  • absolute:默认值,这部分就不用处理;
  • relative_key:对 key_layer 作处理,将其与这里的positional_embedding和 key 矩阵相乘作为 key 相关的位置编码;
  • relative_key_query:对 key 和 value 都进行相乘以作为位置编码。
2. attention_scores = attention_scores + attention_mask是在做什么?难道不应该是乘 mask 吗?
  • 因为这里的 attention_mask 已经【被动过手脚】,将原本为 1 的部分变为 0,而原本为 0 的部分(即 padding)变为一个较大的负数,这样相加就得到了一个较大的负值:
  • 至于为什么要用【一个较大的负数】?因为这样一来经过 softmax 操作以后这一项就会变成接近 0 的小数。
class BertModel(BertPreTrainedModel):
# bertmodel继承BertPreTrainedModel 的方式
3. 注意力头

众所周知是并行计算的,所以上面的 query、key、value 三个权重是唯一的——这并不是所有 heads 共享了权重,而是“拼接”起来了。

BertSelfOutput

又出现了 LayerNorm 和 Dropout 的组合,先 Dropout,进行残差连接后再进行 LayerNorm。至于为什么要做残差连接,最直接的目的就是降低网络层数过深带来的训练难度,对原始输入更加敏感~

BertIntermediate

Attention 后面还有一个全连接+激活的操作,全连接做了一个扩展,以 bert-base 为例,扩展维度为 3072,是原始维度 768 的 4 倍之多;

bertoutput

全连接+dropout+layernorm ,一个残差连接residual connect。

bertpooler

这一层只是简单地取出了句子的第一个token,即[CLS]对应的向量,然后过一个全连接层和一个激活函数后输出:(这一部分是可选的,因为pooling有很多不同的操作)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值