csyhhu / MetaQuant

Codes for Accepted Paper : "MetaQuant: Learning to Quantize by Learning to Penetrate Non-differentiable Quantization" in NeurIPS 2019
55 stars 7 forks source link

Several questions about gradient backpropagation and parameter update #3

Open CharlesTao0926 opened 4 years ago

CharlesTao0926 commented 4 years ago

@csyhhu: 感谢您的杰出工作。

我对您的MetaQuant技术很感兴趣,仔细研读了您的论文和代码。我的理解,您的核心思想是要解决量化网络在梯度反传时量化函数不可导问题。解决的办法是:反向传播时,通过meta-net获得meta-grad,再结合calibration,从而获得更加准确的梯度。在训练过程中,每一次迭代的过程主要分为4步 (以dorefa和MultiFC为例): 步骤1. 对于非首次迭代,使用上次迭代产生的layer.quantized_grad和layer.pre_quantized_weight来生成meta-grad 步骤2. 执行base-net前向 步骤3. 计算base-net损失,执行base-net的反向传播 步骤4. 对于非首次迭代,a) 使用步骤1生成的meta-grad,并结合layer.calibration,更新weight梯度, b)对weight梯度执行refinement, c)用refinement后的梯度更新base-net参数。

我仔细研究了您的代码实现,代码可以正常运行,Cifar10上获得了89%的准确率。但是,我也发现了几处不太匹配的地方,分析如下。如果是我理解不当,还请您不吝赐教,非常感谢!

第一次迭代过程: 因为是第1次迭代,所以会跳过上述步骤1和步骤4,只执行步骤2和步骤3。此时,base-net完成了一次前向和反向的过程。前向过程中,由于Function_STE.apply的使用,反向结束后,layer.weight.grad保存的将是STE的结果。并且,第一次迭代结束时,不立即更新参数,从而在第二次迭代开始时base-net参数仍然是第一次迭代时的参数。 第二次迭代过程(依序执行步骤1~4): 执行步骤1:计算meta-grad:layer.pre_quantized_weight和layer.quantized_grad中分别对应于第一次迭代前向过程中保存的量化前权重和反向过程中回传的梯度,根据此两个值,计算并返回meta-grad。 执行步骤2:在base-net的前向过程中,首先根据layer.weight计算layer.calibration(该layer.calibration将用于后续的步骤4), 接着计算layer.calibrated_grad, 然后更新layer.meta_weight. 问题是,layer.meta_weight的实际计算使用的是self.weight - lr self.weight.grad, (尽管代码中使用的是 self.weight - lr (self.calibrated_grads + (self.weight.grad.data - self.calibrated_grads.data)) ), 而此时self.weight.grad中存储的是STE的梯度,并不是meta-grad。因此,第二次迭代过程中使用的layer.meta_weight是基于STE的,而不是基于meta-grad的。这是第1个与论文不匹配的地方。 执行步骤3:正常执行,没有问题。 执行步骤4:使用步骤1得到的meta-grad和第一次迭代的layer.calibration来更新layer.weight.grad,并对梯度执行refinement,最后更新网络参数。 第2次迭代结束后,网络参数对应于第一次迭代的结果,并且是基于meta-grad的。这也就是您说的“延迟更新”。然而,这带来了第2个不匹配的地方:网络存储的权重参数是基于meta-grad的,而第2次迭代过程中实际使用的meta-weight是基于STE的。 第3次迭代过程: 执行步骤1:此时,layer.pre_quantized_weight和layer.quantized_grad中分别对应于第二次迭代前向过程中保存的量化前权重和反向过程中回传的梯度,根据此两个值,计算并返回meta-grad。 执行步骤2:首先根据layer.weight计算layer.calibration,但此时的layer.weight是已经基于meta-grad更新了的参数,并不对应于第二次迭代过程中实际使用的基于STE的meta-weight,因此,这里的校准操作并不严格匹配于第二次迭代的实际情况。基于同样的原因,第3次迭代过程中用来更新layer.meta_weight的layer.weight也不对应于第二次迭代过程中实际使用的layer.meta-weight.

以上列出了我的一些分析,请指教,非常感谢!

csyhhu commented 4 years ago

@CharlesTao0926 您好!感谢您的兴趣和使用。

我先尝试解决你的第一个问题:“第二次迭代过程中使用的layer.meta_weight是基于STE的,而不是基于meta-grad的。这是第1个与论文不匹配的地方。”

第二次迭代的时候,meta_grad_dict 已经算出来了,所以这段代码已经被执行:https://github.com/csyhhu/MetaQuant/blob/50694404fa010958aa24c96b4c870d5e9442e849/meta-quantize.py#L239 for layer_info in net.layer_name_list: layer_name = layer_info[0] layer_idx = layer_info[1] layer = get_layer(net, layer_idx) layer.weight.grad.data = ( layer.calibration * meta_grad_dict[layer_name][1].data ) 因而此时layer.weight.grad已经是meta-grad的了,所以第二次forward的时候是基于meta-grad的。

不知道我有没有理解清您的问题呢?

CharlesTao0926 commented 4 years ago

@csyhhu: 感谢您的回复。

我理解您的意思。但是,我觉得问题似乎仍然存在。因为在执行https://github.com/csyhhu/MetaQuant/blob/50694404fa010958aa24c96b4c870d5e9442e849/meta-quantize.py#L239 之前,实际上第2次迭代的前向过程已经完成了https://github.com/csyhhu/MetaQuant/blob/50694404fa010958aa24c96b4c870d5e9442e849/meta-quantize.py#L220 也就是说L220执行在先,而L239执行在后。 因此,第2次迭代的前向过程中实际使用的layer.meta_weight是在执行L220的过程中,由https://github.com/csyhhu/MetaQuant/blob/50694404fa010958aa24c96b4c870d5e9442e849/meta_quantized_module.py#L87 计算的,而此时的self.weight.grad还没被L239更新。

另外,虽然在https://github.com/csyhhu/MetaQuant/blob/50694404fa010958aa24c96b4c870d5e9442e849/meta_quantized_module.py#L87 使用了如下的方式更新self.meta_weight,但此处的self.calibrated_grads一加一减被抵消,实际的梯度使用的是self.weight.grad。我不太理解此处对self.calibrated_grads一加一减操作的作用,请您赐教。 self.meta_weight = self.weight - \ lr * (self.calibrated_grads \

不知我的理解,是否有误。请指教,多谢!

csyhhu commented 4 years ago

@CharlesTao0926 ,你说的是对的,应该要等到第三次iteration的时候,forward才会用上meta_grad_dict里面的信息,然而此时meta_grad_dict是由第一次iteration产生的梯度生成的,这里存在两个iteration的延迟。你的困惑应该跟 #1 这里的问题是类似的。我之后尝试换一种框架来实现,目前的问题的确难以避免。但high-level idea是一样的,都是通过梯度学习梯度。

一加一减的操作是因为要考虑到optimizer(主要是Adam)的影响以及要把calibrated_grads引入计算图。calibrated_grads是不经过optimizer改变的,但是weights的更新应该需要用上不同的optimizer. 因此我先把calibrated_grads赋值给了weight.grad,然后在https://github.com/csyhhu/MetaQuant/blob/50694404fa010958aa24c96b4c870d5e9442e849/meta-quantize.py#L244这步中更新,这样weight.grad就是用optimizer更新后的calibrated_grads, 然后再放入下一次iteration的forward. 但因为L244切断了calibrated_grads和计算图的连接,所以我在self.meta_weight的更新里用了一加一减的方式完成:1.把calibrated_grads引入计算图。2.weight的更新是来自optimizer更新后的梯度。

CharlesTao0926 commented 4 years ago

@csyhhu: 感谢您的回复。

谢谢您的解释,我基本明白了。还有两个问题,想继续追问一下:

  1. 如何理解您在回复中提到的 “因为L244切断了calibrated_grads和计算图的连接”?optimizee.get_refine_gradient()为何会切断meta-net和计算图的连接呢?
  2. 您在 #1 问题的回复中提到,“正确的做法应该是把line 239-249整体提前到line 226上面,这样就能解决mismatch的问题。” 这里的行号是不是对应于以前版本的meta-quantize.py,好像和现在版本的行号对应不上?

多谢!

csyhhu commented 4 years ago

@CharlesTao0926 您好,

  1. 切断的意思是,因为我在把calibrated_grads的值付给weight.grad, 再由optimizee.get_refine_gradient()去更新weight.grad. 因此在后面forward中用上的weight.grad.data无法在计算图上和calibrated_grads相连,因此称之为切断。
  2. 我尝试把line 239-249整体提前到line 226上面,但发现会有别的问题,还没解决,因此我先保留目前的版本。
CharlesTao0926 commented 4 years ago

@csyhhu 明白了,谢谢您的指教。