zoukankan      html  css  js  c++  java
  • 常用优化器及tf源码解析

    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

  • 相关阅读:
    Oracle VM VirtualBox 虚拟机中桥接模式一直不能用 ,需要安装 VBoxNetLwf.inf
    Windows7窗口跑到屏幕外面
    手动操作群晖蜂鸣器指示灯方法
    DHT11温湿度传感器接入HomeBridge
    DIY树莓派Homebridge智能台灯
    群晖NAS局域网无法跑满千兆排查
    虚拟机性能监控与故障处理工具------JDK的命令行工具
    垃圾收集器与内存分配策略-内存分配与回收策略
    Minor GC 与Full GC有什么不一样
    java的关键字与保留字
  • 原文地址:https://www.cnblogs.com/zhanxiage1994/p/15211124.html
Copyright © 2011-2022 走看看