首先在阅读源码的过程之中,发现充斥着一些未定义的变量
比如加载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
)
进入到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 =
[, ]
首先
(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(dkQ∗K)
之后,加上对应的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} zt∈B,I,O
输出3个标签的对应内容,而最后输出的这部分是sequence-to-sequence用于预测的内容
引用大佬原文的内容
接下来添加一波数据
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损失函数调用的公式
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为人工定义)
也就是说,softmax只选取前面最多的k个值保留概率,后面的概率值一律置为0,这样操作能够有效地防止模型过拟合
首先我们查看输入的内容
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)
接下来进行一波平移(感觉这波平移可以取消掉)
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:]这个标志
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)
模型的调用这一块我认为没有必要完全非常详细地阅读,只需要注意输入哪些内容,输出哪些内容,如果计算损失函数即可
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子类继承父类的继承过程,以及子类重新定义父类出现的情况
class data1:
def prints(self):
print('1')
class data2(data1):
def prints(self):
print('2')
newdata = data2()
newdata.prints()
此时输出结果会单独地输出一个2的数值
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=β1mt−1+(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=β2vt−1+(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=θt−vt^+ϵηmt^
解读这一段adam梯度优化策略的对应内容
m t = β 1 m t − 1 + ( 1 − β 1 ) g t m_{t} = \beta_{1}m_{t-1}+(1-\beta_{1})g_{t} mt=β1mt−1+(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=β2vt−1+(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)
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=θt−vt^+ϵη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+平均池化拿出来的向量效果还可以,说明平均池化在比赛之中效果更佳