2021SC@SDUSC
lingvo.core.learner.py
learner根据损失来优化变量的子集。
它包括一个learning rate schedule,一个优化器,和gradient clipping机制。一个BaseTask可以有多个learner,每个learner优化 变量的一个子集(通常是不连接的)。
关于 gradient clipping
1.梯度爆炸的影响
在一个只有一个隐藏节点的网络中,损失函数和权值w偏置b构成error surface,其中有一堵墙,如下所示
2.解决梯度爆炸问题的方法
通常会使用一种叫”clip gradients “的方法. 它能有效地权重控制在一定范围之内.
算法步骤如下。
- 首先设置一个梯度阈值:clip_gradient
- 在后向传播中求出各参数的梯度,这里我们不直接使用梯度进去参数更新,我们求这些梯度的l2范数
- 然后比较梯度的l2范数||g||与clip_gradient的大小
- 如果前者大,求缩放因子clip_gradient/||g||, 由缩放因子可以看出梯度越大,则缩放因子越小,这样便很好地控制了梯度的范围
- 最后将梯度乘上缩放因子便得到最后所需的梯度
关于 Learning Rate Scheduler 自适应学习率
学习率(Learning Rate,LR)是深度学习训练中非常重要的超参数。同样的模型和数据下,不同的LR将直接影响模型何时能够收敛到预期的准确率。
随机梯度下降SGD算法中,每次从训练数据中随机选择一批样本,样本数为Batch Size。很多实验都证明了,在LR不变的情况下,Batch Size越大,模型收敛效果越差。
Linear Scale
随着Batch Size增大,一个Batch Size内样本的方差变小;也就是说越大的Batch Size,意味着这批样本的随机噪声越小。那么我们可以更加相信这批样本所产生的梯度,可以增大LR,在负梯度方向更快下降。有人提出了根据Batch Size的大小,线性地调整LR,在ResNet50上实验有效果。例如,ResNet50论文中使用的Batch Size为256,LR为0.1,那么对于更大的Batch Size bs
:
Warmup
在训练最开始,模型中绝大多数参数都是随机初始化的,与最终模型很远。一开始就使用一个很大的LR,会增加不确定性。所以在训练最开始,先使用一个较小的LR,训练几轮Epoch后,再使用较大的LR
Decay
一直使用较大的LR也有问题,在训练中后期,过大的LR可能导致模型在最优解附近震荡,无法快速收敛。所以,在中后期,需要将LR进行一些衰减(Decay)。ResNet论文中最初使用的Step Decay:每训练30个Epoch,LR衰减为刚才的0.1倍。还有Cosine衰减[^3]:最初的LR为 [公式] ,整个训练的Step数目为[公式](steps_per_epoch * total_epochs),当前Step为[公式],当前的[公式]由下面公式计算得到。
Step Decay和Cosine Decay的LR随着训练Epoch变化如下
Linear + Warmup + Decay
将以上三种LR策略组合起来,可以形成一个完整的LR策略:根据Batch Size大小,线性地缩放LR基准值,前几个Epoch使用较小的LR先进行Warmup,之后从LR基准值开始,逐渐对LR进行衰减。比如,ResNet50 ImageNet下Batch Size为1024,LR基准值为0.4 = 0.1 * (1024 / 256),整个训练过程中的LR策略如下图所示:
参考文章——Learning Rate Schedule:CNN学习率调整策略
读代码
类class Learner(base_layer.BaseLayer)
training program层。该层以损失张量作为输入,并输出一个trainer op。
类初始化
def __init__(self, params):
super().__init__(params)
p = self.params
self._var_grads = None
self._eval_metrics = {}
不能在推理中创建多余的变量。
is_training = not (self.do_eval or p.is_inference)
if p.grad_norm_tracker and is_training:
self.CreateChild('grad_norm_tracker', p.grad_norm_tracker)
不能在推理中创建优化器和lr_schedule。
self.CreateChild('optimizer', p.optimizer)
self.CreateChild('lr_schedule', p.lr_schedule)
if isinstance(p.loss_name, (list, tuple)):
assert p.gradient_combiner
self.CreateChild('gradient_combiner', p.gradient_combiner)
else:
assert p.gradient_combiner is None
方法def _CreateChildrenVariables(self)
在传统模式下,multi learner由于ValueError不能使用。
向后兼容:在tf.variable_scope(p.name)之外手动调用child.InstantiateVariables()。
def _CreateChildrenVariables(self):
p = self.params
if not p.learner_use_variable_scope:
if 'grad_norm_tracker' in self.children:
self.grad_norm_tracker.InstantiateVariables()
self.lr_schedule.InstantiateVariables()
self.optimizer.InstantiateVariables()
super()._CreateChildrenVariables()
方法def Apply(self, metrics, vmap, gradient_mask=None, gradient_adjuster=None)
方法作用:计算“vmap”上的更新以优化损失
参数
- metrics: 字典[str, (value, weight)],可以根据p.loss_name计算损失。
- vmap:
.NestedMap
对象,包含要优化的变量。 - gradient_mask: 如果not None,则字典映射变量名到0/1标量。
- gradient_adjuster: 如果not None,则为一个函数,该函数会改变给定的var_gradient_梯度。
返回值
(losses, op, eval_metrics),其中
- losses:标量张量的列表;
- op:tf.Operation要更新变量;
- eval_metrics:是一个Dict[str, (value, weight)],其中每个value/weight是一个标量张量。
源码:
在name_scope之外应用梯度以保持self.optimizer.Apply()创建的变量的向后兼容性。
def Apply(self, metrics, vmap, gradient_mask=None, gradient_adjuster=None):
losses, var_grads, eval_metrics = self._ComputeLossesAndGradients(
metrics, vmap)
if 'tpu_embedding_var_grads' in var_grads:
tpu_embedding_var_grads = var_grads.tpu_embedding_var_grads
del var_grads.tpu_embedding_var_grads
tpu_embedding_collection = py_utils.GetTpuEmbeddingGraphCollection()[0]
assert tpu_embedding_collection
tpu_emb_update_op, stats = tpu_embedding_collection.ApplyGradients(
py_utils.GetTaskCallScope(),
tpu_embedding_var_grads.Transform(lambda var_grad: var_grad.grad))
eval_metrics.update(stats)
else:
tpu_emb_update_op = tf.no_op()
assert py_utils.GetGlobalStep() is not None
lr = self.LearningRate()
var_grads, stats = self.AdjustGradients(
var_grads,
gradient_mask=gradient_mask,
gradient_adjuster=gradient_adjuster)
eval_metrics.update(stats)
self._var_grads = var_grads
eval_metrics['learning_rate'] = (tf.convert_to_tensor(lr),
tf.convert_to_tensor(1.))
var_update_op = tf.group(
[tpu_emb_update_op,
self.optimizer.Apply(lr, var_grads)])
return losses, var_update_op, eval_metrics
计算有效梯度方法ComputeActivationGradients
源码
def ComputeActivationGradients(self, activations, activations_grad, vmap):
p = self.params
vmap = self.GetTrainableVariables(vmap)
for v in vmap.Flatten():
tf.logging.info('%s: bprop variable: %s', p.name, v.name)
return self.optimizer.ComputeGradients(
activations,
vmap,
p.grad_aggregation_method,
p.colocate_gradients_with_ops,
p.gate_gradients,
compute_gradients_fn=self._CustomComputeGradientsFn(),
skip_zero_gradients=p.skip_zero_gradients,
skip_none_gradients=False,
activations_grad=activations_grad,
is_activations=True)
计算损失方法ComputeLosses
源码
def ComputeLosses(self, metrics):
p = self.params
def _Loss(metric_name):
metric = metrics.get(metric_name, None)
if metric is None:
raise ValueError('Loss %s not found in metrics %s' %
(metric_name, list(metrics.keys())))
return metric
loss_name = p.loss_name or p.name
losses = []
if isinstance(loss_name, (list, tuple)):
for metric_name in loss_name:
loss_metric = _Loss(metric_name)
losses.append(loss_metric[0])
else:
loss_metric = _Loss(loss_name)
losses.append(loss_metric[0])
return losses