python3 seq2seq_model.py 对应代码解读抽取式提取+生成式提取摘要代码解读------摘要代码解读5------第一章

摘要代码阅读5

    • 未定义变量的出现
    • 精简词表操作
    • layers.py中的MultiHeadAttention网络层
    • layers.py MultiHeadAttention类别中的pay_attention_to函数的调用
    • 新的损失函数:稀疏Softmax的调用
    • seq2seq_model.py之中的CrossEntropy类compute_loss函数解读
    • 疑点1
    • 继续读loss的对应代码
    • seq2seq_model优化器部分代码解读
      • python继承父类的第一种情况
      • python继承父类的第二种情况
      • 作者实际使用的版本
      • 另一版本的讲解
      • 1.对梯度和梯度的平方进行滑动平均
      • 2.对初期滑动平均偏差的修正
    • 注意事项(在比赛中的启示)

未定义变量的出现

首先在阅读源码的过程之中,发现充斥着一些未定义的变量
比如加载nezha模型之前的操作

token_dict,keep_tokens = load_vocab(
	dict_path = nezha_dict_path,
	simpllified = True,
	startswith = ['[PAD]','[UNK]','[CLS]','[SEP]'],
)

这里的nezha_dict_path其实被定义过了,前面有加载的过程

from snippets import *

而snippets之中定义了模型加载的内容

config_path = '/home/xiaoguzai/模型/chinese_L-12_H-768_A-12/bert_config.json'
checkpoint_path = '/home/xiaoguzai/模型/chinese_L-12_H-768_A-12/bert_model.ckpt'
dict_path = '/home/xiaoguzai/模型/chinese_L-12_H-768_A-12/vocab.txt'

所以在这里面可以直接调用对应的模型结构相关文件
这里需要注意的是,如果有相应的字典json文件,则从中读取对应的词典内容,而如果没有对应字典的json文件,则自己手动写入

精简词表操作

这里的精简词表的操作感觉可有可无,但是既然作者使用到了相应的精简词表的操作,就仔细地阅读一波
首先纵观全局

# 加载并精简词表
token_dict, keep_tokens = load_vocab(
    dict_path=dict_path,
    simplified=True,
    startswith=['[PAD]', '[UNK]', '[CLS]', '[SEP]'],
    #startswith=['[UNK]','[CLS]','[SEP]'],
)

这里得到的

***token_dict = ***
{'[PAD]': 0, '[UNK]': 1, '[CLS]': 2, '[SEP]': 3, '!': 4, '"': 5, '#': 6, '$': 7, '%': 8, '&': 9, "'": 10, '(':
......
'##ル': 13574, '##ン': 13575, '##゙': 13576, '##゚': 13577, '## ̄': 13578, '##¥': 13579, '##': 13580, '##': 13581, '##': 13582, '##': 13583}

为更新之后的词表,
而对应的

keep_tokens = 
[0, 100, 101, 102, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 
......
21100, 21108, 21109, 21110, 21111, 21112, 21113, 21114, 21115, 21116, 21117, 21118, 21119, 21120, 21121, 21122, 21123, 21124, 21125, 21126, 21127]

为它在原词表中的索引的位置
这里精简词表的代码阅读先暂时跳过
注意这里的词表之中加入了作者自己新定义的词表

for w in load_user_dict(user_dict_path) + load_user_dict(user_dict_path_2):
    if w not in token_dict:
        token_dict[w] = len(token_dict)
        user_dict.append(w)

引用一下大佬论文中的内容
此外,在使用预训练模型方面,我们首创地将部分词语加入到了NEZHA模型中,改变了中文预训练模型以字为单位的通用选择,这使得模型的效果和速度都有一定的提升。
顺带加一句,其实加入词语这种操作之前阿里达摩院研究过,所以这里不能算是大佬的首创,但是在词典之中精简并加入新的词语确实对于训练有很好的帮助,这点已经得到了证实和证明。
接下来重点观察模型的构成:

model = build_transformer_model(
    config_path,
    checkpoint_path,
    model='nezha',
    application='unilm',
    with_mlm='linear',
    keep_tokens=keep_tokens,  # 只保留keep_tokens中的字,精简原字表
    compound_tokens=compound_tokens,
)

这里调用模型的时候使用bert4keras.models中的build_transformer_model函数内容

from bert4keras.models import build_transformer_model

首先这里调用nezha模型,接下来使用unilm进行扩展

if application == 'lm':
    MODEL = extend_with_language_model(MODEL)
elif application == 'unilm':
    MODEL = extend_with_unified_language_model(MODEL)

进入到extend_with_unified_language_model(BaseModel)中查看函数内容

def extend_with_unified_language_model(BaseModel):
    """添加UniLM的Attention Mask(Seq2Seq模型用)
    """
    class UnifiedLanguageModel(UniLM_Mask, BaseModel):
        """带UniLM的Attention Mask的派生模型
        UniLM: https://arxiv.org/abs/1905.03197
        """
        def __init__(self, *args, **kwargs):
            super(UnifiedLanguageModel, self).__init__(*args, **kwargs)
            self.with_mlm = self.with_mlm or True

    return UnifiedLanguageModel

这里我们再查看一下UniLM_Mask类别的定义

class UniLM_Mask(object):
    """定义UniLM的Attention Mask(Seq2Seq模型用)
    其中source和target的分区,由segment_ids来表示。
    UniLM: https://arxiv.org/abs/1905.03197
    """
    def compute_attention_bias(self, inputs=None):
        """通过idxs序列的比较来得到对应的mask
        """
        if self.attention_bias is None:
            def unilm_mask(s):
                idxs = K.cumsum(s, axis=1)
                mask = idxs[:, None, :] <= idxs[:, :, None]
                mask = K.cast(mask, K.floatx())
                return -(1 - mask[:, None]) * 1e12

            self.attention_bias = self.apply(
                inputs=self.inputs[1],
                layer=Lambda,
                function=unilm_mask,
                name='Attention-UniLM-Mask'
            )
        return self.attention_bias

这里实际上是将Base_model中的compute_attention_bias变为了UniLM_Mask类别中的compute_attention_bias函数,我们查看一下Transformer之中的compute_attention_bias函数的内容

def compute_attention_bias(self, inputs=None):
	return self.attention_bias

这里我们查看attention_mask的调用过程

if attention_mask is not None:
	arguments['a_bias'] = True
	x.append(attention_mask)

attention_mask只有在MultiHeadAttention之中才能用得到,这里我们进入MultiHeadAttention网络层之中查看attention_mask的用法
在进入到apply_main_layers之前,我们查看对应的x的值

x = 
[, 
, 
, 
, 
]

前三个为正常的q,k,v,第四个为UniLM-Mask所需要的掩码,最后一个为对应的相对位置坐标

x = self.apply(
    inputs=x,
    layer=MultiHeadAttention,
    arguments=arguments,
    heads=self.num_attention_heads,
    head_size=self.attention_head_size,
    out_dim=self.hidden_size,
    key_size=self.attention_key_size,
    kernel_initializer=self.initializer,
    name=attention_name
)

layers.py中的MultiHeadAttention网络层

进入到MultiHeadAttention的网络层之中,首先进行三个attention向量的处理

q,k,v = inputs[:3]
qw = self.q_dense(q)
kw = self.k_dense(k)
vw = self.v_dense(v)
qw = K.reshape(qw, (-1, K.shape(q)[1], self.heads, self.key_size))
kw = K.reshape(kw, (-1, K.shape(k)[1], self.heads, self.key_size))
vw = K.reshape(vw, (-1, K.shape(v)[1], self.heads, self.head_size))

接下来对于三个向量进行叠加

qkv_inputs = [qw,kw,vw]+inputs[3:]

这里放入的数据[qw,kw,vw]对应值为

[qw,kw,vw] = 
[, , ]

inputs[3:]对应值为

inputs[3:] = 
[, ]

两个list相加拼接在一起,得到的qkv_inputs的值为

qkv_inputs = 
[, 
, 
, 
, 
]
qv_masks = [q_mask,v_mask]

得到对应的qv_masks的值

qv_masks = 
[, ]

layers.py MultiHeadAttention类别中的pay_attention_to函数的调用

首先

(qw, kw, vw), n = inputs[:3], 3
q_mask, v_mask = mask
a_bias, p_bias = kwargs.get('a_bias'), kwargs.get('p_bias')

这里调用出来的a_bias = True之后,抽取出对应的unilm_mask的内容

if a_bias:
    a_bias = inputs[n]
    n += 1

最后在计算完成

s o f t m a x ( Q ∗ K d k ) softmax(\frac{Q*K}{\sqrt{d_{k}}}) softmax(dk QK)
之后,加上对应的unilm_mask掩码内容

if self.attention_scale:
    a = a / self.key_size**0.5
if a_bias is not None:
    a = a + a_bias

接下来调用MLM-Norm网络层

output = model.get_layer('MLM-Norm').output

要想找到这一层的相应的结构,关键是找到MLM-Norm的对应内容
在keras中,要想获取层的输出的各种信息,可以先获取层对象,再通过层对象的属性output或者output_shape获取层输出的其他特性
观察Transformer之中call函数的结构

def call(self,inputs):
	outputs = self.apply_embeddings(inputs)
  for i in range(self.num_hidden_layers):
     outputs = self.apply_main_layers(outputs, i)
  # Final
  outputs = self.apply_final_layers(outputs)
  return outputs

这里关键的MLM-Norm网络层的内容,需要到最后一个apply_final_layers函数之中去寻找

outputs = self.apply_final_layers(outputs)

BERT类别之中的apply_final_layers函数中,有着调用MLM-Dense和MLM-Norm的过程

if self.with_mlm:
    # Masked Language Model部分
    x = outputs[0]
    x = self.apply(
        inputs=x,
        layer=Dense,
        units=self.embedding_size,
        activation=self.hidden_act,
        kernel_initializer=self.initializer,
        name='MLM-Dense'
    )
    x = self.apply(
        inputs=self.simplify([x, z]),
        layer=LayerNormalization,
        conditional=(z is not None),
        hidden_units=self.layer_norm_conds[1],
        hidden_activation=self.layer_norm_conds[2],
        hidden_initializer=self.initializer,
        name='MLM-Norm'
    )

也就是说这里在原始bert的后面加上一个对应的Dense和LayerNormalization的网络层
接下来的内容比较关键,为两个网络层的结果相加

output = model.get_layer('MLM-Norm').output
output = Dense(3,activation='softmax')(output)
outputs = model.outputs+[output]

这里的结果为两个部分叠在一块

output = (?,?,3)
model.outputs = []

正经的model.outputs在经历过上面的dense网络层和layer_normalization网络层之后,还需要经历一波用来预测的网络层结构

x = self.apply(
    inputs=x,
    layer=Embedding,
    arguments={'mode': 'dense'},
    name='Embedding-Token'
)
x = self.apply(inputs=x, layer=BiasAdd, name='MLM-Bias')
#特别指出一下,这里常规的MLM-Bias初始化的参数为全零矩阵
mlm_activation = 'softmax' if self.with_mlm is True else self.with_mlm
x = self.apply(
    inputs=x,
    layer=Activation,
    activation=mlm_activation,
    name='MLM-Activation'
)
#使用softmax类似于预训练的过程那样进行每一个单词的训练

经过最后一波叠加

outputs = model.outputs+[output]

之后

outputs = 
[, ]

这里叠加的中间的这个dense3是形成相应的序列标注的
标签分布
z t ∈ B , I , O z_{t} \in {B,I,O} ztB,I,O
输出3个标签的对应内容,而最后输出的这部分是sequence-to-sequence用于预测的内容
引用大佬原文的内容
python3 seq2seq_model.py 对应代码解读抽取式提取+生成式提取摘要代码解读------摘要代码解读5------第一章_第1张图片接下来添加一波数据

y_in = Input(shape=(None,))
l_in = Input(shape=(None,))
outputs = [y_in, model.inputs[1], l_in] + outputs

得到的结果

outputs = 
[, , , , ]

然后调用了一个对应的CrossEntropy的交叉熵损失

outputs = CrossEntropy([3,4])(outputs)

新的损失函数:稀疏Softmax的调用

首先我们来看一下原版的softmax损失函数调用的公式
p i = e s i ∑ j = 1 n e s j p_{i} = \frac{e^{s_{i}}}{\sum_{j=1}^{n}e^{s_{j}}} pi=j=1nesjesi
而新版的稀疏softmax只取出了前面的k个值(k为人工定义)
python3 seq2seq_model.py 对应代码解读抽取式提取+生成式提取摘要代码解读------摘要代码解读5------第一章_第2张图片也就是说,softmax只选取前面最多的k个值保留概率,后面的概率值一律置为0,这样操作能够有效地防止模型过拟合

seq2seq_model.py之中的CrossEntropy类compute_loss函数解读

首先我们查看输入的内容

inputs = [, 
, 
, 
,
 ]

从上面的输入可以看出来,第0和第3组数据构成第一波训练,第1和第4组数据构成了第二波训练
接下来我们从inputs中取出对应的数值,先看compute_seq2seq_loss函数的调用过程

y_true,y_mask,_,y_pred,_ = inputs

获得的内容

y_true = 
Tensor("input_1:0", shape=(?, ?), dtype=float32)
y_mask = 
Tensor("Input-Segment:0", shape=(?, ?), dtype=float32)
y_pred = 
Tensor("MLM-Activation/Identity:0", shape=(?, ?, 16153), dtype=float32)

疑点1

接下来进行一波平移(感觉这波平移可以取消掉)

y_true = y_true[:, 1:]  # 目标token_ids
y_mask = y_mask[:, :-1] * y_mask[:, 1:]  # segment_ids,刚好指示了要预测的部分
y_pred = y_pred[:, :-1]  # 预测序列,错开一位

[1:]应该想要去除的是y_true之中的[cls]标志,而y_pred[:,:-1]这个没看懂,感觉可以去除y_pred[:,1:]这个标志

继续读loss的对应代码

pos_loss = batch_gather(y_pred,y_true[...,None])[...,0]

首先这里需要理解batch_gather函数的用法

import tensorflow as tf
tensor_a = tf.Variable([[1,2,3],[4,5,6],[7,8,9]])
tensor_b = tf.Variable([1,2,0],dtype=tf.int32)
tensor_c = tf.Variable([0,0],dtype=tf.int32)
with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())
    print(sess.run(tf.gather(tensor_a,tensor_b)))
    print(sess.run(tf.gather(tensor_a,tensor_c)))

上个例子tf.gather(tensor_a,tensor_b) 的值为[[4,5,6],[7,8,9],[1,2,3]],tf.gather(tensor_a,tensor_b) 的值为[[1,2,3],[1,2,3]]

对于tensor_a,其第1个元素为[4,5,6],第2个元素为[7,8,9],第0个元素为[1,2,3],所以以[1,2,0]为索引的返回值是[[4,5,6],[7,8,9],[1,2,3]],同样的,以[0,0]为索引的值为[[1,2,3],[1,2,3]]。
经历过截取之后

y_true = Tensor("input_1:0", shape=(?, ?), dtype=float32)
y_mask = Tensor("Input-Segment:0", shape=(?, ?), dtype=float32)
y_pred = Tensor("MLM-Activation/Identity:0", shape=(?, ?, 16153), dtype=float32)

接下来调用

y_true[...,None]

得到对应的结果

y_true = (?,?,1)

接下来调用的pos_loss较为关键

pos_loss = batch_gather(y_pred,y_true[...,None])[...,0]

重点分析一下这句
首先调用

batch_gather(y_pred,y_true[...,None])

是从y_pred = (?,?,16153)之中调用出真实的那个id对应的概率值
接下来调用

batch_gather(y_pred,y_true[...,None])[...,None]

这里外面加上了[…,None]使得对应的数据由(?,?)变为(?,?,1)
这里之所以要算正确的数值对应的pos_loss,是因为这部分不应该出现在交叉熵的计算之中
接下来计算对应的前10个数值相应的loss

y_pred = tf.nn.top_k(y_pred, k=k_sparse)[0]
neg_loss = K.logsumexp(y_pred, axis=-1)

这里的k_sparse = 10,也就是这里取出前面的10个数值计算相应的loss。
计算完对应loss之后需要把原装的loss去除掉(原装的loss属于预测对的,肯定不能在真实的loss之中出现)

loss = neg_loss - pos_loss
loss = K.sum(loss*y_mask)/K.sum(y_mask)

接下来我们查看一下compute_copy_loss函数,这里是计算真实的标签与B、I、O三个标签对应的crossentropy的损失,总共就3个标签,就不需要k_sparse进行标记了

def compute_copy_loss(self,inputs,mask=None):
    _, y_mask, y_true, _, y_pred = inputs
    y_mask = K.cumsum(y_mask[:, ::-1], axis=1)[:, ::-1]
    y_mask = K.cast(K.greater(y_mask, 0.5), K.floatx())
    y_mask = y_mask[:, 1:]  # mask标记,减少一位
    y_pred = y_pred[:, :-1]  # 预测序列,错开一位
    loss = K.sparse_categorical_crossentropy(y_true, y_pred)
    loss = K.sum(loss * y_mask) / K.sum(y_mask)
    return loss

这里的

y_mask = Tensor("Input-Segment:0", shape=(?, ?), dtype=float32)
y_true = Tensor("input_2:0", shape=(?, ?), dtype=float32)
y_pred = Tensor("dense_73/truediv:0", shape=(?, ?, 3), dtype=float32) 

其中y_true为真实的B、I、O的标签,y_pred为预测出来的三个标签的概率,最后计算相应的loss值

loss = K.sparse_categorical_crossentropy(y_true,y_pred)
loss = K.sum(loss*y_mask)/K.sum(y_mask)

seq2seq_model优化器部分代码解读

模型的调用这一块我认为没有必要完全非常详细地阅读,只需要注意输入哪些内容,输出哪些内容,如果计算损失函数即可

train_model = Model(model.inputs+[y_in,l_in],outputs)

所以这一块主要阅读的内容是AdamEMA,即Adam加上EMA权重滑动平均优化器的解读

AdamEMA = extend_with_exponential_moving_average(Adam,name='AdamEMA')
optimizer = AdamEMA(learning_rate=2e-5,ema_momentum=0.9999)

查看extend_with_exponential_moving_average调用过程

from bert4keras.optimizers import Adam,extend_with_exponential_moving_average

需要注意的是,这里作者采用了Adam优化器和EMA都重写的方式,将Adam与优化器配合使用
这里首先我们学习一下python子类继承父类的继承过程,以及子类重新定义父类出现的情况

python继承父类的第一种情况

class data1:
    def prints(self):
        print('1')

class data2(data1):
    def prints(self):
        print('2')
        
newdata = data2()
newdata.prints()

此时输出结果会单独地输出一个2的数值

python继承父类的第二种情况

class data1:
    def prints(self):
        print('1')

class data2(data1):
    def prints(self):
        super(data2,self).prints()
        print('2')
        
newdata = data2()
newdata.prints()

这里的

super(data2,self).prints()

会调用之前的父类,输出1,而

print('2')

接着继续输出2
可见继承类可以调用父类的方法,但是必须加上对应的语句

super(...,self)....()

的函数
这里的优化器过程作者使用了一个比较特殊的方法进行实现,首先放上对应的代码

class Adam(keras.optimizers.Optimizer):
    """重新定义Adam优化器,便于派生出新的优化器
    (tensorflow的optimizer_v2类)
    """
    def __init__(
        self,
        learning_rate=0.001,
        beta_1=0.9,
        beta_2=0.999,
        epsilon=1e-6,
        bias_correction=True,
        **kwargs
    ):
        kwargs['name'] = kwargs.get('name') or 'Adam'
        super(Adam, self).__init__(**kwargs)
        self._set_hyper('learning_rate', learning_rate)
        self._set_hyper('beta_1', beta_1)
        self._set_hyper('beta_2', beta_2)
        self.epsilon = epsilon or K.epislon()
        self.bias_correction = bias_correction

    def _create_slots(self, var_list):
        for var in var_list:
            self.add_slot(var, 'm')
            self.add_slot(var, 'v')

    def _resource_apply(self, grad, var, indices=None):
        # 准备变量
        var_dtype = var.dtype.base_dtype
        lr_t = self._decayed_lr(var_dtype)
        m = self.get_slot(var, 'm')
        v = self.get_slot(var, 'v')
        beta_1_t = self._get_hyper('beta_1', var_dtype)
        beta_2_t = self._get_hyper('beta_2', var_dtype)
        epsilon_t = K.cast(self.epsilon, var_dtype)
        local_step = K.cast(self.iterations + 1, var_dtype)
        beta_1_t_power = K.pow(beta_1_t, local_step)
        beta_2_t_power = K.pow(beta_2_t, local_step)

        # 更新公式
        if indices is None:
            m_t = K.update(m, beta_1_t * m + (1 - beta_1_t) * grad)
            v_t = K.update(v, beta_2_t * v + (1 - beta_2_t) * grad**2)
        else:
            mv_ops = [K.update(m, beta_1_t * m), K.update(v, beta_2_t * v)]
            with tf.control_dependencies(mv_ops):
                m_t = self._resource_scatter_add(
                    m, indices, (1 - beta_1_t) * grad
                )
                v_t = self._resource_scatter_add(
                    v, indices, (1 - beta_2_t) * grad**2
                )

        # 返回算子
        with tf.control_dependencies([m_t, v_t]):
            if self.bias_correction:
                m_t = m_t / (1.0 - beta_1_t_power)
                v_t = v_t / (1.0 - beta_2_t_power)
            var_t = var - lr_t * m_t / (K.sqrt(v_t) + self.epsilon)
            return K.update(var, var_t)

    def _resource_apply_dense(self, grad, var):
        return self._resource_apply(grad, var)

    def _resource_apply_sparse(self, grad, var, indices):
        return self._resource_apply(grad, var, indices)

    def get_config(self):
        config = {
     
            'learning_rate': self._serialize_hyperparameter('learning_rate'),
            'beta_1': self._serialize_hyperparameter('beta_1'),
            'beta_2': self._serialize_hyperparameter('beta_2'),
            'epsilon': self.epsilon,
            'bias_correction': self.bias_correction,
        }
        base_config = super(Adam, self).get_config()
        return dict(list(base_config.items()) + list(config.items()))

奇怪的是,我在中间加入各种输出运行的过程中是一次都没有被调用过,这里不得不佩服tensorflow1的静态图模式,什么输出也没有
后来发现这里调用错了,总共有两个平均权重的版本,我只读了其中一个版本,有extend_with_weight_decay和extend_with_weight_decay_v2两个对应的版本,实际上使用的是extend_with_weight_decay的版本

作者实际使用的版本

实际上使用的是keras之中的keras.optimizers.Adam优化器加上extend_with_weight_decay函数内容,这点取值在最后面有讲述到

if is_tf_keras:
    extend_with_weight_decay = extend_with_weight_decay_v2
    extend_with_layer_adaptation = extend_with_layer_adaptation_v2
    extend_with_piecewise_linear_lr = extend_with_piecewise_linear_lr_v2
    extend_with_gradient_accumulation = extend_with_gradient_accumulation_v2
    extend_with_lookahead = extend_with_lookahead_v2
    extend_with_lazy_optimization = extend_with_lazy_optimization_v2
    extend_with_exponential_moving_average = extend_with_exponential_moving_average_v2
    extend_with_parameter_wise_lr = extend_with_parameter_wise_lr_v2
    AdaFactor = AdaFactorV2
else:
    Adam = keras.optimizers.Adam
    AdaFactor = AdaFactorV1

AdaFactor.__name__ = 'AdaFactor'
custom_objects = {
     
    'Adam': Adam,
    'AdaFactor': AdaFactor,
}
keras.utils.get_custom_objects().update(custom_objects)

实际上调用的是keras之中的Adam优化器内容,所以后面定义的新的Adam优化器完全没有用上

另一版本的讲解

于是,想要看明白优化器的实现原理,不得不回到keras的源代码部分
keras源代码部分网址
首先我们查看_resource_apply_dense和_resource_apply_sparse这两个对应的函数以及相应的注释

def _resource_apply_dense(self,grad,handle,apply_state):
	r"""
  Args:
   grad: a `Tensor` representing the gradient.
   handle: a `Tensor` of dtype `resource` which points to the variable to be
     updated.
   apply_state: A dict which is used across multiple apply calls.
 Returns:
   An `Operation` which updates the value of the variable.
	"""
	raise NotImplementedError()

raise NotImplementedError()如果没有实现会报错
解读一下上面的内容,grad为计算出来的梯度,handle为现在的变量,现在需要使用grad将handle变为新的变量
结合adam优化器之中具体实现的代码谈一谈:

def _resource_apply(self, grad, var, indices=None):
    # 准备变量
    print('Adam _resorce_apply')
    var_dtype = var.dtype.base_dtype
    lr_t = self._decayed_lr(var_dtype)
    m = self.get_slot(var, 'm')
    v = self.get_slot(var, 'v')
    beta_1_t = self._get_hyper('beta_1', var_dtype)
    beta_2_t = self._get_hyper('beta_2', var_dtype)
    epsilon_t = K.cast(self.epsilon, var_dtype)
    local_step = K.cast(self.iterations + 1, var_dtype)
    beta_1_t_power = K.pow(beta_1_t, local_step)
    beta_2_t_power = K.pow(beta_2_t, local_step)

    # 更新公式
    if indices is None:
        m_t = K.update(m, beta_1_t * m + (1 - beta_1_t) * grad)
        v_t = K.update(v, beta_2_t * v + (1 - beta_2_t) * grad**2)
    else:
        mv_ops = [K.update(m, beta_1_t * m), K.update(v, beta_2_t * v)]
        with tf.control_dependencies(mv_ops):
            m_t = self._resource_scatter_add(
                m, indices, (1 - beta_1_t) * grad
            )
            v_t = self._resource_scatter_add(
                v, indices, (1 - beta_2_t) * grad**2
            )

    # 返回算子
    with tf.control_dependencies([m_t, v_t]):
        if self.bias_correction:
            m_t = m_t / (1.0 - beta_1_t_power)
            v_t = v_t / (1.0 - beta_2_t_power)
        var_t = var - lr_t * m_t / (K.sqrt(v_t) + self.epsilon)
        return K.update(var, var_t)

可以看出来,grad为计算出来的梯度,var为现在的变量,计算完成之后

return K.update(var,var_t)

将旧的变量替换成为新的变量
这里查看对应的公式
m t = β 1 m t − 1 + ( 1 − β 1 ) g t m_{t} = \beta_{1}m_{t-1}+(1-\beta_{1})g_{t} mt=β1mt1+(1β1)gt
v t = β 2 v t − 1 + ( 1 − β 2 ) g t 2 v_{t} = \beta_{2}v_{t-1}+(1-\beta_{2})g_{t}^{2} vt=β2vt1+(1β2)gt2
m t ^ = m t 1 − β 1 t \hat{m_{t}} = \frac{m_{t}}{1-\beta_{1}^{t}} mt^=1β1tmt
v t ^ = v t 1 − β 2 t \hat{v_{t}} = \frac{v_{t}}{1-\beta_{2}^{t}} vt^=1β2tvt
θ t + 1 = θ t − η v t ^ + ϵ m t ^ \theta_{t+1} = \theta_{t} - \frac{\eta}{\sqrt{\hat{v_{t}}}+\epsilon}\hat{m_{t}} θt+1=θtvt^ +ϵηmt^
解读这一段adam梯度优化策略的对应内容

1.对梯度和梯度的平方进行滑动平均

m t = β 1 m t − 1 + ( 1 − β 1 ) g t m_{t} = \beta_{1}m_{t-1}+(1-\beta_{1})g_{t} mt=β1mt1+(1β1)gt
v t = β 2 v t − 1 + ( 1 − β 2 ) g t 2 v_{t} = \beta_{2}v_{t-1}+(1-\beta_{2})g_{t}^{2} vt=β2vt1+(1β2)gt2
对梯度和梯度的平方进行滑动平均,使得每次更新都和历史有关
实现更新的代码:

if indices is None:
    m_t = K.update(m, beta_1_t * m + (1 - beta_1_t) * grad)
    v_t = K.update(v, beta_2_t * v + (1 - beta_2_t) * grad**2)

2.对初期滑动平均偏差的修正

m t ^ = m t 1 − β 1 t \hat{m_{t}} = \frac{m_{t}}{1-\beta_{1}^{t}} mt^=1β1tmt
v t ^ = v t 1 − β 2 t \hat{v_{t}} = \frac{v_{t}}{1-\beta_{2}^{t}} vt^=1β2tvt
这是对初期滑动平均偏差较大的一个修正,叫做bias correction。
当t越来越大时,1- β 1 t \beta_{1}^{t} β1t和1- β 2 t \beta_{2}^{t} β2t都趋近于1,这时bias correction完成任务。
最后使用参数更新
θ t + 1 = θ t − η v t ^ + ϵ m t ^ \theta_{t+1} = \theta_{t} - \frac{\eta}{\sqrt{\hat{v_{t}}}+\epsilon}\hat{m_{t}} θt+1=θtvt^ +ϵηmt^
这是参数更新公式,当学习率为 η v t ^ + ϵ \frac{\eta}{\sqrt{\hat{v_{t}}}+\epsilon} vt^ +ϵη,每轮学习率不再保持不变。
实现更新的代码

with tf.control_dependencies([m_t, v_t]):
    if self.bias_correction:
        m_t = m_t / (1.0 - beta_1_t_power)
        v_t = v_t / (1.0 - beta_2_t_power)

最后参数更新的过程:

var_t = var - lr_t * m_t / (K.sqrt(v_t) + self.epsilon)
return K.update(var, var_t)

注意事项(在比赛中的启示)

EMA(权重滑动平均)可以使得训练过程更加稳定,甚至可能提升模型效果。
bert的cls向量拿出来的效果很差,但是bert+平均池化拿出来的向量效果还可以,说明平均池化在比赛之中效果更佳

你可能感兴趣的:(文本摘要抽取代码解读,python,开发语言,后端,1024程序员节)