Optimizer
深度学习中,无法直接找到模型解析解,通常是利用梯度对权重参数做迭代式优化。
经典综述,paper link: https://arxiv.org/pdf/1609.04747.pdf
定义后续提到的相关符号,待优化参数 heta ,目标函数 (J( heta)) ,学习率 lr ,当前epoch/step=t,则当前时刻的参数梯度 (g_t= riangledown _{ heta_t} J( heta_t))
后续详细算法前,需要两块知识点为基础,指数加权平均和tensorflow代码结构,放在了最后。
SGD(Stochastic Gradient Descent)
更新公式:( heta_t= heta_{t-1}-lr * g_t)
关于梯度下降方法,会有三种叫法,BGD/minibatchGD/SGD,区别就是计算 g_t时用多少条样本,全局/小批量/单条。当前DeepLearning任务巨量训练数据默认采用minibatch训练策略,SGD同样默认时minibatchGD。
全局模式每次能够保证梯度都是全局最优方向,缺点就是梯度成本高;单条模式每次更新参数只需要一条样本即可,导致每次迭代不都是朝整体最优方向,波动较大容易在局部最优间跳跃,如果lr 足够小,SGD& BGD会有相同的收敛效果;minibatch结合了前两者在梯度获取上的优势。
但GD系列本身存在固有缺点,1. lr的选择,太小导致收敛速度慢,太大导致局部震荡;2.对于非凸函数容易陷入局部极值点或鞍点,困在梯度为0附近;
class GradientDescentOptimizer():
def _prepare():
_learning_rate_tensor = ops.convert_to_tensor(lr)
def _apply_dense(grad, var):
struct ApplyGradientDescent<CPUDevice, T>
var.device(d) -= grad * lr();
def _apply_sparse_duplicate_indices(grad, var):
delta = ops.IndexedSlices(grad.values * _learning_rate_tensor, grad.indices, grad.dense_shape)
return var.scatter_sub(delta)
Momentum
更新公式:( heta_t= heta_{t-1}-lr * m_t; m_t=eta_1*m_{t-1}+(1-eta_1)*g_t)
tf代码实现公式:( heta_t= heta_{t-1}-lr * m_t; m_t=eta_1*m_{t-1}+g_t)
既然minibatch相比于batch仍少了数据,自然想到继续计算梯度的数据量,结合指数加权平均思想,使用历史窗口内的数据做平均提高梯度质量。这个历史窗口内的平均,称为梯度的一阶动量 m_t。即保留历史更新方向的同时,利用当前梯度微调最终方向,由于eta=0.9的默认取值,下降方向主要为积累方向略微偏向当前时刻。
优势在于动量与梯度同向时加速下降,反向时降低速度减少震荡;
缺点在于盲目加速,在接近极值点下降时由于同向加速会造成大幅度跨过极值点。
NAG(Nesterov Accelerated Gradient)
更新公式:( heta_t= heta_{t-1}-lr * m_t; m_t=eta_1*m_{t-1}+(1-eta_1)* riangledown _{ heta_t} J( heta_t - eta_1 *m_{t-1}))
tf代码实现公式,在Momentum公式中,将m_{t-1}替换为m_t,模拟投影梯度。
使用先前动量对当前权重更新为投影权重,重新计算前向传播获得投影梯度,用于新动量的计算中。当梯度出现大的跳跃时,通过校正因子对方向进行修正,通过提前预判避免前进的太快,提高灵敏度;
但此方式在随机梯度的情况下,对收敛作用有限。
class MomentumOptimizer():
def __init__():
_lr, _momentum, _use_nesterov
def _create_slots(var_list):
for v in var_list:
momentum -> _zeros_slot(name=v.name+'/momentum')
def _prepare():
_lr_t, _momentum_t <- ops.convert_to_tensor(lr, _momentum)
def _apply_dense(grad, var):
accum = self.get_slot(var, "momentum")
training_ops.apply_momentum()
struct ApplyMomentum<CPUDevice, T> {};
accum.device(d) = accum * momentum() + grad;
if (use_nesterov) {
var.device(d) -= grad * lr() + accum * momentum() * lr();
} else {
var.device(d) -= accum * lr();
}
AdaGrad
更新公式:( heta_t= heta_{t-1}-frac{lr}{sqrt {V_t+epsilon }} * g_t; V_t=sum_{i=1}^t g_t^2)
从参数更新频率角度出发,高频更新的参数经过大量数据的迭代已经得到较高的优化,因此希望降低单条样本对参数的影响,而低频更新参数则反之,由于迭代次数较低希望单条样本对参数的影响更大些,显然只有一个共享标量学习率是不满足需求的。因此引入二阶动量V_t,即所有梯度值的平方和。
缺点是,当 t 越大,分母上的V_t 越大,使得学习率趋于0,导致训练提前结束。
class AdagradOptimizer():
def __init__(lr, initial_accumulator_value=0.1):
epsilon: tf2 1e-8
initial_accumulator_value: tf2 allow zero
def _create_slots(var_list):
for v in var_list:
init = init_ops.constant_initializer(_initial_accumulator_value)
self.add_slot(var, 'accumulator', init)
def _apply_dense(grad, var):
acc = self.get_slot(var, 'accumulator')
training_ops.apply_adagrad()
struct ApplyAdagradV2<CPUDevice, T> {};
accum.device(d) += grad.square();
var.device(d) -= grad * lr() / (accum.sqrt() + epsilon());
RMSprop
更新公式:( heta_t= heta_{t-1}-frac{lr}{sqrt {V_t+epsilon }} * g_t; V_t=eta_2*V_{t-1}+(1-eta_2) g_t^2)
思想很直接,既然AdaGrad在t 较大时会出问题,那就改变下V_t 的计算公式,不用累计的全部历史梯度而是用近期的部分梯度,结合前边指数加权平均的思想,得到新的窗口V_t 计算方式;默认值 lr=0.001,eta_2=0.9,epsilon=10e-6
缺点是,手工设置 lr 对更新影响仍然较大;
class RMSPropOptimizer():
def __init(lr, decay=0.9, momentum=0):
epsilon: tf1->tf2, 1e-10->1e-07
tf1: decay, tf2: rho
centered=False
def _create_slots(var_list):
for var in var_list:
self.add_slot(var, "rms")
self.add_slot(var, "momentum")
if centered:
self.add_slot(var, "mg")
def _prepare():
_lr, _decay, _momentum, _epsilon <- ops.convert_to_tensor
def _apply_dense(grad, var):
rms = self.get_slot(var, "rms")
mom = self.get_slot(var, "momentum")
if not center:
training_ops.apply_rms_prop()
struct ApplyRMSProp<CPUDevice, T> {}
rms.device(d) += (grad.square() - rms) * (1 - rho());
mom.device(d) = mom * momentum() + (grad * lr()) / ((rms + epsilon()).sqrt());
var.device(d) -= mom;
else:
# gradients are normalized by the estimated variance
mg = self.get_slot(var, "mg")
training_ops.apply_centered_rms_prop()
struct ApplyCenteredRMSProp<CPUDevice, T> {}
rms.device(d) += (grad.square() - rms) * (1 - rho());
mg.device(d) += (grad - mg) * (1 - rho());
auto denom = (rms - mg.square()) + epsilon();
mom.device(d) = mom * momentum() + (grad * lr()) / denom.sqrt();
var.device(d) -= mom;
Adadelta
更新公式:( heta_t= heta_{t-1}-lr * riangledown heta_{t}; riangledown heta_{t}=frac{sqrt{D_{t-1}+epsilon}} {sqrt{V_t+epsilon}} g_t; V_t=eta_2V_{t-1}+(1-eta_2)g_t^2; D_t=eta_2 D_{t-1}+(1-eta_2) riangledown heta_{t}^2)
消除了手工设置 lr 的麻烦。
class AdadeltaOptimizer():
def __init__(learning_rate=0.001, rho=0.95):
epsilon: tf1->tf2, 1e-8 -> 1e-7
def _create_slots(self, var_list):
for v in var_list:
self._zeros_slot(v, "accum", self._name)
self._zeros_slot(v, "accum_update", self._name)
def _prepare():
lr, rho, epsilon <- ops.convert_to_tensor
def _apply_dense(grad, var):
accum = self.get_slot(var, "accum")
accum_update = self.get_slot(var, "accum_update")
training_ops.apply_adadelta()
struct ApplyAdadelta<CPUDevice, T> {}
accum.device(d) = accum * rho() + grad.square() * (1 - rho());
const auto update = (accum_update + epsilon()).sqrt() * (accum + epsilon()).rsqrt() * grad;
var.device(d) -= update * lr();
accum_update = accum_update * rho() + update.square() * (1 - rho());
Adam
更新公式:( heta_t= heta_{t-1}- frac{lr}{sqrt{hat V_t + epsilon} }*hat m_t)
(hat m_t=m_t/(1-eta_1^t); m_t=eta_1 m_{t-1}+(1-eta_1)g_t=m_{t-1}+(g_t-m_{t-1})(1-eta_1))
(hat V_t=V_t/(1-eta_2^t); V_t=eta_2 V_{t-1}+(1-eta_2)g_t^2=V_{t-1}+(g_t^2-m_{t-1})(1-eta_2))
tf代码实现公式:对m_t/V_t做无偏修正时并没有使用幂指 t
在经典GD公式中,参数变化项有两个因素,lr & g_t,Momentum和NAG是在g_t上的工作,AdaGrad/RMSprop/Adadelta是在 lr 上的工作,adam则将两部分改进结合起来,吸收了两侧的优势,虽然仍需要给定 lr 超参,但影响已经很微弱了。默认值,eta_1=0.9, eta_2=0.999, epsilon=10e-8。
class AdamOptimizer():
def __init__(lr=0.001,beta1=0.9,beta2=0.999,epsilon=1e-8):
# epsilon: Default value is 1e-08 in TF1, but 1e-07 in TF2.
self._name = 'Adam'
self.argv = argv
def _create_slots(var_list):
_beta1,_beta2 -> variable(trainable=False) ->self._non_slot_dict
for v in var_list:
m, v -> _zeros_slot(name=v.name+'/Adam') -> self._slots['m'/'v']
def _prepare(self):
_lr_t,_beta1_t,_beta2_t,_epsilon_t <-
ops.convert_to_tensor(lr,beta1,beta2,epsilon)
def _apply_dense(grad, var):
m,v = self.get_slot(var, 'm'/'v')
beta1_power, beta2_power = self._get_beta_accumulators()
struct ApplyAdam<CPUDevice, T> : ApplyAdamNonCuda<CPUDevice, T> {};
const T alpha = lr() * Eigen::numext::sqrt(T(1) - beta2_power()) / (T(1) - beta1_power());
m += (g - m) * (T(1) - beta1());
v += (g.square() - v) * (T(1) - beta2());
var -= (m * alpha) / (v.sqrt() + epsilon());
def _apply_sparse(grad, var):
m,v = self.get_slot(var, 'm'/'v')
beta1_power, beta2_power = self._get_beta_accumulators()
lr = (lr_t * math_ops.sqrt(1 - beta2_power) / (1 - beta1_power))
m_scaled_g_values = grad * (1 - beta1_t)
m = m * beta1_t
m_t = scatter_add(m, grad.indices, m_scaled_g_values) #sparse add
v_scaled_g_values = (grad * grad) * (1 - beta2_t)
v = v * beta2_t
v_t = scatter_add(v, indices, v_scaled_g_values)
v_sqrt = math_ops.sqrt(v_t)
var_update = state_ops.assign_sub(var, lr * m_t / (v_sqrt + epsilon_t))
NAdam
更新公式:( heta_t= heta_{t-1}- frac{lr}{sqrt{hat V_t + epsilon} }* M_t)
(M_t=eta_1 hat m_t + frac{1-eta_1}{1-eta_1^t}g_t; hat m_t=m_t/(1-eta_1^t); m_t=eta_1 m_{t-1}+(1-eta_1)g_t)
(hat V_t=V_t/(1-eta_2^t); V_t=eta_2 V_{t-1}+(1-eta_2)g_t^2=V_{t-1}+(g_t^2-m_{t-1})(1-eta_2))
在adam基础上增加了Nesterov ,算是集前人所有方法于一身了。一般而言,在使用带动量的RMSprop或Adam的问题上,使用Nadam可以取得更好的结果。
class Nadam(optimizer_v2.OptimizerV2):
def __init__(lr=0.001,beta_1=0.9,beta_2=0.999,epsilon=1e-7):
self._m_cache
def _create_slots(var_list):
self._m_cache = add_weight('momentum_cache',initializer='ones',trainable=False,)
for var in var_list:
self.add_slot(var, 'm')
self.add_slot(var, 'v')
def _prepare(var_list):
_m_cache_read = tf.identity(self._m_cache)
lr_t, beta_1_t, beta_2_t
local_step, next_step = self.iterations +1, +2
m_t, m_t_1 = beta_1_t
# m_schedule_new = pow(eta_1, local_step)
m_schedule_new = _m_cache_read * m_t
m_schedule_new = identity(assign(self._m_cache, m_schedule_new))
m_schedule_next = m_schedule_new * m_t_1
def _resource_apply_dense(grad, var):
m = self.get_slot(var, 'm')
v = self.get_slot(var, 'v')
g_prime = grad / (1. - m_schedule_new)
m_t = beta_1_t * m + (1 - beta_1_t) * grad
m_t = state_ops.assign(m, m_t)
m_t_prime = m_t / (1. - m_schedule_next)
m_t_bar = (1 - beta_1_t) * g_prime + m_t_1 * m_t_prime
v_t = beta_2_t * v + (1 - beta_2_t) * square(grad)
v_t = state_ops.assign(v, v_t)
v_t_prime = v_t / (1. - pow(beta_2_t, local_step))
var_t = var - lr * m_t_bar / (sqrt(v_t_prime) + epsilon)
state_ops.assign(var, var_t)
Adamax
更新公式:(hat V_t=eta_2^ infty V_{t-1}+(1-eta_2^infty)|g_t|^infty=max(eta_2 V_{t-1},|g_t|))
Adam的升级,将V_t计算中的l2 norm推广到了lp norm形式,用于收敛到更稳定的状态;
class Adamax(optimizer_v2.OptimizerV2):
def __init__():
lr=0.001, beta_1=0.9, beta_2=0.999, epsilon=1e-7
def _create_slots(var_list):
for var in var_list:
self.add_slot(var, 'm')
self.add_slot(var, 'v')
def _prepare_local():
local_step = self.iterations + 1
beta_1_t, beta_2_t
beta_1_power = pow(beta_1_t, local_step)
def _resource_apply_dense(grad, var):
m = self.get_slot(var, 'm')
v = self.get_slot(var, 'v')
gen_training_ops.ResourceApplyAdaMax()
struct ApplyAdaMax<CPUDevice, T> : ApplyAdaMaxNonCuda<CPUDevice, T> {};
m.device(d) += (grad - m) * (T(1) - beta1());
// eigen_cwiseMax: an expression of the coefficient-wise max of *this and other
v.device(d) = (beta2() * v).cwiseMax(grad.abs());
var.device(d) -= lr() / (T(1) - beta1_power()) * (m / (v + epsilon()));
AMSGrad
更新公式:( heta_t= heta_{t-1}- frac{lr}{sqrt{hat V_t + epsilon} }*m_t)
(m_t=eta_1 m_{t-1}+(1-eta_1)g_t=m_{t-1}+(g_t-m_{t-1})(1-eta_1))
(hat V_t=max(hat V_{t-1},V_t); V_t=eta_2 V_{t-1}+(1-eta_2)g_t^2=V_{t-1}+(g_t^2-m_{t-1})(1-eta_2))
论文是ICLR18的bestpaper,表示指数加权的假发使得梯度只有短期记忆,并设计了理论实验证明Adam的收敛失败。但后续的多篇分析表示,虽然论文中表述的确实是Adam存在的问题,但AMSGrad并没有解决。在tf中是一个flag开关控制,集成在了Adam中。
class Adam(optimizer_v2.OptimizerV2):
def __init__(amsgrad=False):
self.amsgrad = amsgrad
def _create_slots(var_list):
if self.amsgrad:
for var in var_list:
self.add_slot(var, 'vhat')
def _resource_apply_dense(grad, var):
vhat = self.get_slot(var, 'vhat')
gen_training_ops.ResourceApplyAdamWithAmsgrad()
class ApplyAdamWithAmsgradOp : public OpKernel {}
functor::ApplyAdamWithAmsgrad<Device, T>()
struct ApplyAdamWithAmsgrad<CPUDevice, T> {}
const T alpha = lr() * sqrt(T(1) - beta2_power()) / (T(1) - beta1_power());
m.device(d) += (grad - m) * (T(1) - beta1());
v.device(d) += (grad.square() - v) * (T(1) - beta2());
vhat.device(d) = vhat.cwiseMax(v);
var.device(d) -= (m * alpha) / (vhat.sqrt() + epsilon());
总结
深度学习可堪称可归结为优化问题,最小化目标函数(J( heta)),首先求解目标梯度(g_t= riangledown J( heta)),将参数向负梯度方向更新,( heta_t= heta_{t-1}-lr * g_t),lr为学习率表明前进幅度,梯度表示前进方向。
由公式可看出,参数更新项是两部分决定,因此优化器的发展也分成了为两条思路,最终是Adam将两侧收入麾下。Adam之后的工作,主要集中在m_t和v_t计算方式的改进。
无论怎样发展,所有的改进项都是基于梯度的,一条样本得到的唯一数值只有梯度。直观的,只用当前数值不够好,就需要历史数据的辅助,此时m_t就诞生了;对于较大梯度希望适当缩小,直接操作就是归一化除梯度的模,同样希望历史数据到辅助,就诞生了v_t;具体的区别就是怎样辅助了。
至于哪种优化器效果更好,貌似并没有唯一的结论。大多数的论文中,很少有使用Adam的,而是SGD更多,主要是lr可精细化调整取得最终的那几个小数点的收益。个人觉得,任何时候都可以直接用Adam,跑出baseline后再改动NAdam或 lr+RMSprop 的模式,而非上手就是SGD。Adam不一定是很好的上限,但一定是个不错的下限。
另,对于巨大的稀疏矩阵,尤其是推荐领域EmbeddingDict,tf 中实现Adam是跑不动的,要用LazyAdam,对应pytorch中的SparseAdam。原因在于,单条样本涉及到的embedding在Dict中非常非常少,而m_t& v_t 对于不涉及本次inference的参数也会更新,同时由于Dict size巨大,所以速度就非常慢。sparse模式下的Adam能够反向只更新涉及到的部分embedding行,但对效果可能是有损的。正是由于每次更新所有参数的原因,导致使用Adam的优化算法的GPU利用率会明显高于其他算法,但并没有训练速度上的提升。
准备知识点
指数加权平均
N个数(x_1,x_2,...,x_N) , 算术平均 (ar{x}=frac{1}{N}sum_{i=1}^N x_i) 当数字存在其他含义时就会存在重要程度的差异(用w表示),对应的加权平均 (ar{x}=frac{1}{N}sum_{i=1}^N w_ix_i) 。可以将后者理解为前者的通用式,w=1是退化为前者。
移动窗口策略
随时间变化的数据中,可以使用平均值描述数值的变化趋势。
当数据相对平稳没有剧烈波动时,计算均值可以选取近期部分数据代表整体数据,得到近似平均。
近期数据取多少可以用窗口n表示。n取小值时,均值跟随观测数据较实时,波动较剧烈;n取大值时,均值对波动的平滑效果较好,但需要记录的历史数据较多。因此n的选取,通常使用经验法或试数法,两个极端情况n=1 or n=N。
指数平均加权
指数加权平均是对移动窗口策略的改进,好处在于只需要记录一个数值,去掉了窗口内所有时刻值的保留,节省了内存空间。
记,Vt表示t 时刻的均值, heta_t 表示t 时刻的观测值,eta 超参数,则计算公式为,
(V_t=eta V_{t-1}+(1-eta) heta_t)
eta 取值通常默认0.9,详细分析下上边的公式,取 eta=0.9, t=100;
(V_{100}=0.9V_{99}+0.1 heta_{100}=0.9(0.9V_{98}+0.1 heta_{99})+0.1 heta_{100}=0.9^2 (0.9V_{97}+0.1 heta_{98})+0.9*0.1 heta_{99}+0.1 heta_{100})
(=0.9^3V_{97}+0.1*0.9^2 heta_{98}+0.1*0.9 heta_{99}+0.1 heta_{100})
指数式递减加权的移动平均,从指数系数观察,0.1*0.9^10=0.03486,普适看来这个值够小可以作为忽略项。从极限公式出发(limit_{x->0}(1-x)^{1/x}=e) ,e的倒数约等于0.35,因此可将 1/(1-eta) 作为忽略项分界点,即指数加权平均可近似为 $ frac{1}{1-eta} $ 个数据的窗口平均。
当eta 越小,越重视当前时刻的观测值,越能够跟随系统瞬间突发变化,时效性强,相对的平稳性稍差。
但此公式存在一个明显的问题点,初始时刻由于积累数据较少导致V的偏差较大,因此存在一种修正偏差方案,(V_t'=V_t/(1-eta^t)) .但在实际使用中,一般不做初期修正,采取忍受熬过;
tensorflow源码
版本TF2.6
class Optimizer
各种具体的优化器,均继承自class Optimizer;暂时只关注单级单卡模式,不涉及分布式;只留核心代码片段和相关参数,去掉变量创建、校验、信息获取,scope命名等;去掉冗余的if 判断,关闭eager模式;
# tensorflow/python/training/optimizer.py
class Optimizer():
def __init__():
self._slots = {}
self._non_slot_dict = {}
def minimize(loss, global_step, var_list):
grads_and_vars = self.compute_gradients(loss, var_list) # type:list(tuple)
return self.apply_gradients(grads_and_vars, global_step)
def apply_gradients(grads_and_vars, global_step):
# g类型规范化;根据v的类型调用不同的变量更新方法;统一收录到converted_grads_and_vars
for g, v in grads_and_vars:
g = ops.convert_to_tensor_or_indexed_slices(g) #type:Tensor/IndexedSlices
p = _get_processor(v)
converted_grads_and_vars.append((g, v, p))
# 创建优化器自带的参数;以及apply梯度前创建好所有必须的tensors
self._create_slots(var_list)
self._prepare()
# 获取参数更新操作op,colocate_with保证op与var在同一设备执行,返回op 集合
for grad, var, processor in converted_grads_and_vars:
with ops.colocate_with(var):
update_ops.append(processor.update_op(self, grad))
apply_updates = control_flow_ops.group(*update_ops)
with ops.control_dependencies([apply_updates]):
apply_updates = state_ops.assign_add(global_step, 1)
return apply_updates
def _get_processor(v):
# resource_variabel类型变量, 调用_resource_apply_dense, _resource_apply_sparse;
# 其余变量, 调用_apply_dense, _apply_sparse
if v.op.type == "VarHandleOp":
return _DenseResourceVariableProcessor(v)
if isinstance(v, variables.Variable):
return _RefVariableProcessor(v)
从基类中可以看出,具体优化器方法实现中,子类至少要实现6个函数,_create_slots, _prepare, _apply_dense, _apply_sparse, _resource_apply_dense, _resource_apply_sparse。
从逻辑上看,_resource_apply_dense & _apply_dense,_resource_apply_sparse & _apply_sparse是相同的。只是获取入参的操作不同,区别并不影响理解逻辑。
resource variabel
关于resource_variable和variable的区别,tf 官方并没有太多的解释,只是在def enable_resource_variables() 中提到了一段,简单总结就是,前者是后者的改进版本,具有内存资源的占用,并且能够保证读写的顺序;
Resource variables are improved versions of TensorFlow variables with a well-defined memory model. Accessing a resource variable reads its value, and all ops which access a specific read value of the variable are guaranteed to see the same value for that tensor. Writes which happen after a read (by having a control or data dependency on the read) are guaranteed not to affect the value of the read tensor, and similarly writes which happen before a read are guaranteed to affect the value. No guarantees are made about unordered read/write pairs.
看完这段并没有什么理解,在variables.py里翻看,看到官方给的demo,并且结合运行结果,有点懂了。
import tensorflow as tf
# import tensorflow.compat.v1 as tf
# tf.disable_eager_execution()
# tf.disable_resource_variables()
# tf.disable_control_flow_v2()
tf.reset_default_graph()
print(tf.__version__)
def my_false_fn():
return 0
v = tf.Variable(5)
res = tf.cond(v>4, lambda: v.assign(3), my_false_fn)
# from tensorflow.python.ops import resource_variable_ops
# resource_variable_ops.is_resource_variable(v)
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
print(sess.run([res,v]))
在 tf1.9 版本中,output: [3, 3];在tf2.x中,output: [3, 5]
首先明确一点,sess.run的顺序并不影响graph流的先后,因此[res, v]还是[v, res],结果是完全一样的。
举个例子,1+1=10,这在二进制下是正确,但看到之后要多反应一下。类比到demo,个人认为,3/5 都有各自的逻辑,5 更合理些但3也不是完全错,只要有规则可循提前定义好就行,但偏偏 tf 把这块模糊了,在 tf1.get_variable中有一个参数就是使用use_resource。
tensorflow在2.x后将ResourceVariable作为默认项,Variable是一个resource,继承自ResourceBase并由ResourceMgr管理。
对应到optimizer里,就是为啥会有apply_dense和resource_apply_dense两个看起来没啥区别的函数实现。
Adam文件查找
在tensorflow/python/training下集成了官方提供的一众优化器实现,但具体看进去就发现,有点头疼。
以adam.py为例,_apply_dense()调用了training_ops.apply_adam,但training_ops.py算上注释也就26行内容,又是一个import嵌套。from tensorflow.python.ops import gen_training_ops
,在源码中找到ops文件夹,发现并没有gen_training_ops.py文件,一脸懵逼!
在编译安装的机器上搜索,居然存在这个文件。前两行表明这是个生成文件,并且来源于cpp。
This file is MACHINE GENERATED! Do not edit.
Original C++ source file: training_ops.cc
tensorflow使用bazel编译器,编译依赖关系在BUILD文件中,虽然知道了来源,但还是尝试从源码中查找下流程。
首先搜索training_ops_gen,在tensorflow/python/BUILD中发现编译输出,
tf_gen_op_wrapper_private_py(
name = "training_ops_gen",
out = "training/gen_training_ops.py",
)
跟进tf_gen_op_wrapper_private_py,发现调用tf_gen_op_wrapper_py接口
tf_gen_op_wrapper_py(
name = "training_ops,
out = "training/gen_training_ops.py",
require_shape_functions = True,
generated_target_name = name,
api_def_srcs = [
"//tensorflow/core/api_def:base_api_def",
"//tensorflow/core/api_def:python_api_def",
],
)
deps = ["//tensorflow/core:" + name + "_op_lib"]
继续进入def定义
if not deps:
deps = [str(Label("//tensorflow/core:" + name + "_op_lib"))]
转到core/BUILD中查找training_ops,在tf_gen_op_libs下一坨,节选
tf_gen_op_libs(
is_external = False,
op_lib_names = ["training_ops",],
deps = [
":lib",
":protos_all_cc",
],
)
进入def tf_gen_op_libs执行
n=training_ops
native.cc_library(
name = n + "_op_lib",
copts = tf_copts(is_external = is_external),
srcs = ["ops/" + n + ".cc"],
deps = deps + [clean_dep("//tensorflow/core:framework")],
visibility = ["//visibility:public"],
alwayslink = 1,
linkstatic = 1,
)
至此找到,src: ops/training_ops.cc。
由tf 模式可知,ops对应定义,具体实现在kernel中去找。
kernel中有eigen库版本的cpu版本,以及cuda写的GPU版本,能够起到任务加速。
Reference
指数移动平均:https://zhuanlan.zhihu.com/p/32335746 http://shichaoxin.com/2020/02/25/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0%E5%9F%BA%E7%A1%80-%E7%AC%AC%E5%8D%81%E5%85%AD%E8%AF%BE-%E6%8C%87%E6%95%B0%E5%8A%A0%E6%9D%83%E5%B9%B3%E5%9D%87/
Opt: https://www.cnblogs.com/guoyaohua/p/8542554.html https://zhuanlan.zhihu.com/p/58236906 https://mp.weixin.qq.com/s/EOVvSPeEMbcj2tJnOk1zTA https://cloud.tencent.com/developer/article/1468547?from=article.detail.1183236
Tf: https://zhuanlan.zhihu.com/p/87348147 https://www.cnblogs.com/littleorange/p/13168159.html https://blog.51cto.com/u_15179348/2734068