[TensorFlow]Batch Normalization在單機多卡上的實現
前記
最近感覺Batch Normalization在實踐上還是很有搞頭的,首先在單機TensorFlow里完整實現BN演算法就有很多坑(具體可以看我的文章Michael:[論文閱讀]Batch Normalization: Accelerating Deep Netwo,裡面也有我對BN的理解),然後在單機多卡上的實現也有些坑,主要是moving mean和moving variance在單機多卡上是怎麼實現更新的(分散式的原理類似於單機多卡)。
首先做一些實驗來看看BN的運行原理吧,代碼已上傳至testBNInMultiGPU.py,我改了一些bug後可以完美運行並在實際項目中運行成功的代碼在validSyncBN.py。
單機單卡上的BN實驗
代碼為testBNInSingleGPU()。
結果:
outputArray = [0. 0. 0.]
After normalization:
moving mean = [0.1 0.1 0.1]
moving variance = [0.9 0.9 0.9]
首先看輸出全為0表示BN起到了歸一化的作用。
之後可以看moving mean和moving variance的值,參考Michael:TensorFlow源碼解讀之Batch Normalization,得知TF中moving mean和moving variance的更新方式都是momentum,即 。
因為moving mean初始化為0,moving variance初始化為1,momentum設置為0.9,代入可得:
moving mean:
moving variance:
和實驗是完美符合的。
單機多卡上的運行實驗
這裡選取了2塊卡進行實驗,代碼為testBNInMultiGPU()。
結果:
outputArray = [0. 0. 0.]
After normalization:
moving mean = [0.2 0.2 0.2]
moving variance = [0.79999995 0.79999995 0.79999995]
由於是兩塊卡,結合Michael:TensorFlow源碼解讀之Batch Normalization的分析,再根據實驗結果,我推測更新公式是這樣的:
統計每張卡上moving mean和moving variance的變化量,即 ,將各卡的變化量求和得到總變化量,進而得到更新的結果:
,這裡 表示GPU的數量。
因為moving mean初始化為0,moving variance初始化為1,momentum設置為0.9,代入可得:
moving mean:
moving variance:
和實驗是完美符合的。
另一種更新的策略是,每張卡前向執行一次,更新一次moving mean和moving variance,那麼:
moving mean:
第一次:
第二次:
這與實驗結果是不符合的。
這裡改變一下輸入的值(一個卡上是1,一個卡上是2),以驗證我的想法是不是對的:
for item in range(numberGPU):
feedDict[tf.get_default_graph().get_tensor_by_name("tower_%s/data:0" % item)]
= np.ones((3))*(item+1)
結果:
outputArray = [0. 0. 0.]
After normalization:
moving mean = [0.3 0.3 0.3]
moving variance = [0.79999995 0.79999995 0.79999995]
因為moving mean初始化為0,moving variance初始化為1,momentum設置為0.9,代入可得:
moving mean:
moving variance:
和實驗是完美符合的。
影響
對於公式 ,如果各個卡上 的分布是一致的,那麼很清晰就得到:
這個實際是會加速moving mean和moving variance的,所以可設置新的moment為: 。
第二個影響就是對訓練了,從上述的實驗可以猜測BN的訓練過程:
在每張卡上前向推導,得到BN的moving mean和moving variance的改變數,同時,反向推導,得到 和 在每張卡上的梯度。
然後,使用tensorflow 多GPU編程 完全指南中描述的方法,得到 和 的平均梯度。
所以,TensorFlow的官方方法是沒有考慮多GPU編程的,下一個步驟好好搞搞這個吧,目測比較難。
目前找到的一些比較好的資源:
batch normalization的multi-GPU版本該怎麼實現?
裡面有幾個比較好的解決方案:
1.jianlong-yuan/syncbn-tensorflow重寫了TensorFlow的官方方法,可以做個實驗驗證一下。
2.曠視科技:CVPR 2018 | 曠視科技物體檢測冠軍論文——大型Mini-Batch檢測器MegDet
tensorpack/tensorpack裡面有該論文的代碼實現。
關於第一種解決方案的實驗論證與理論解釋
jianlong-yuan/syncbn-tensorflow是源代碼,我稍微改了改代碼,並添加了我的一些注釋,代碼為sync_batch_norm()。
測試代碼為testBNInMultiGPU3()。
輸出為:
outputTuple = [array([-0.998006, -0.998006, -0.998006], dtype=float32), array([0.9980061, 0.9980061, 0.9980061], dtype=float32),
[array([0.15, 0.15, 0.15], dtype=float32), array([0.92499995, 0.92499995, 0.92499995], dtype=float32)]]
After normalization:
moving mean = [0.15 0.15 0.15]
moving variance = [0.92499995 0.92499995 0.92499995]
outputTuple一共4項,前兩項為2個GPU的輸出量,一個為-1,一個為1,後兩項分別是moving mean和moving variance的輸出。
這裡先就數據的觀點來驗證結果是否正確。
GPU_0的數據是3個1,GPU_1的數據是3個2,參考batch normalization accelerating deep network training by reducing internal covariate shift演算法1,有:
對於GPU_0的數據,有:
對於GPU_1的數據,有:
這說明輸出是符合的。
下面看moving mean和moving variance的值。
moving mean:moving variance:
也是符合的,說明程序還是很魯棒的。
這裡來分析程序的運行原理,大部分我都用注釋說明了,這裡講裡面的兩個知識點吧,一個是mean和variance是怎麼計算得到的,一個是moving mean和moving variance是怎麼更新的。
首先說下mean和variance是怎麼計算得到的,是以下的這段代碼:
# avarage moving_mean and moving_var in multi GPUs
shared_name = tf.get_variable_scope().name
batch_mean = tf.reduce_mean(inputs, axis=axis)
batch_mean = gen_nccl_ops.nccl_all_reduce(
input=batch_mean,
reduction=sum,
num_devices=num_dev,
shared_name=shared_name + _NCCL_mean) * (1.0 / num_dev)
batch_mean_square = tf.reduce_mean(tf.square(inputs), axis=axis)
batch_mean_square = gen_nccl_ops.nccl_all_reduce(
input=batch_mean_square,
reduction=sum,
num_devices=num_dev,
shared_name=shared_name + _NCCL_mean_square) * (1.0 / num_dev)
mean = batch_mean
var = batch_mean_square - tf.square(batch_mean)
假設有N塊卡,每個卡上有 個訓練樣本,且令 ,那麼有 ,其中, 是整個的樣本數。
這句代碼:
batch_mean = tf.reduce_mean(inputs, axis=axis)
其用數學表達式可以表示成:
這句代碼:
batch_mean = gen_nccl_ops.nccl_all_reduce(
input=batch_mean,
reduction=sum,
num_devices=num_dev,
shared_name=shared_name + _NCCL_mean) * (1.0 / num_dev)
其會在各卡對相應的變數進行操作,這裡進行的操作是求和,最後除N即可,用數學表達式可以表達成:
,因為有 ,那麼有:
這個表達式就與論文里的表達式一致了,以下用 代替 。
這句代碼:
batch_mean_square = tf.reduce_mean(tf.square(inputs), axis=axis)
其用數學表達式可以表示成:
這句代碼:
batch_mean_square = gen_nccl_ops.nccl_all_reduce(
input=batch_mean_square,
reduction=sum,
num_devices=num_dev,
shared_name=shared_name + _NCCL_mean_square) * (1.0 / num_dev)
其用數學表達式可以表示成:
這句代碼:
var = batch_mean_square - tf.square(batch_mean)
其用數學表達式可以表示成:
這裡利用的一個trick是 ,那麼
另外一個值得注意的代碼就是這個控制條件:
if int(outputs.device[-1]) == 0:
因為每個GPU上的moving mean和moving variance都一致了,所以沒必要每個都更新,更新GPU:0的即可。
綜上,看起來這個代碼還是不錯的,待我在實際項目中再驗證驗證吧。
在實際項目中的實驗論證
說實話我是很想在實驗中加入進去的,可惜的是,在我本地跑沒有問題,但是在伺服器上跑最終依然有問題,所以最後可能得放棄這個了。
最開始碰到的問題在於Python 2中的對str的解釋,報錯為:
Traceback (most recent call last):
File "/home/dao/anaconda2/envs/python36/lib/python3.6/site-packages/tensorflow/python/framework/ops.py", line 2349, in get_attr
c_api.TF_OperationGetAttrValueProto(self._c_op, name, buf)
tensorflow.python.framework.errors_impl.InvalidArgumentError: Operation tower_0/lstm2fc/batch_normalization_0/NcclAllReduce_1 has no attr named _XlaCompile.
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/home/dao/anaconda2/envs/python36/lib/python3.6/site-packages/tensorflow/python/ops/gradients_impl.py", line 391, in _MaybeCompile
xla_compile = op.get_attr("_XlaCompile")
File "/home/dao/anaconda2/envs/python36/lib/python3.6/site-packages/tensorflow/python/framework/ops.py", line 2353, in get_attr
raise ValueError(str(e))
ValueError: Operation tower_0/lstm2fc/batch_normalization_0/NcclAllReduce_1 has no attr named _XlaCompile.
...
在搜索解決方案的時候我碰巧發現本文所寫的第二種解決方案(即曠視論文)與第一種解決方案異曲同工(沒興趣去了解代碼的根源了)。
第二種解決方案的代碼在tensorpack/tensorpack/blob/master/tensorpack/models/batch_norm.py,首先給了我一些關於TF支持nccl的解釋:
assert six.PY2 or TF_version >= (1, 10),
"Cross-GPU BatchNorm is only supported in TF>=1.10 ."
"Upgrade TF or apply this patch manually: https://github.com/tensorflow/tensorflow/pull/20360"
鏈接的issue是Fix gradient of nccl_ops by ppwwyyxx · Pull Request #20360 · tensorflow/tensorflow,個人猜測1.10以下的版本支持Python 2.7的居多,我用Python 3.6,所以報錯了。
升級到了1.10.0的版本,發現代碼這段加上也是很不錯的:
if TF_version <= (1, 12):
try:
from tensorflow.contrib.nccl.python.ops.nccl_ops import _validate_and_load_nccl_so
except Exception:
pass
else:
_validate_and_load_nccl_so()
from tensorflow.contrib.nccl.ops import gen_nccl_ops
else:
from tensorflow.python.ops import gen_nccl_ops
弄好之後又報錯:
tensorflow.python.framework.errors_impl.NotFoundError: libnccl.so.2
參考文章tensorflow1.12 多GPU協同訓練報錯tensorflow.python.framework.errors_impl.NotFoundError: libnccl.so.2發現需要安裝nccl庫,Nvidia官方的安裝指導在Deep Learning SDK Documentation。
好歹不報錯了,但是程序似乎不運行了,也查不出原因,所以決定暫時放棄這種做法了。
後續更新:排除bug
終於搞通了,發現是代碼的bug,Tensorflow的條件控制沒有if else這麼簡單,必須寫到計算圖裡面去。
這會導致一個什麼結果呢?在推理階段,不需要更新moving mean和moving variance,所以代碼會直接推理得到結果:
outputs, _, _ = tf.nn.fused_batch_norm(inputs, gamma, beta, mean=moving_mean, variance=moving_var, epsilon=epsilon, is_training=False)
但是呢,這是一個else語句,計算圖是沒法把Python里的判斷條件放到計算圖裡去的,如果列印計算圖,會發現此時根本就沒有直接推理的結果:
print ([n.name for n in tf.get_default_graph().as_graph_def().node if "fused_batch_norm" in n.name])
使用tf.cond()寫好了判斷函數後,還發現一個問題,在我真實的項目環境中是可以使用的,但是我在寫的測試代碼中卻會報這樣的錯:
"operation has been marked as not fetchable"
參考Queue operation in conditional execution context fails with "operation has been marked as not fetchable" · Issue #4094 · tensorflow/tensorflow,控制條件不能有太多控制語句,我發現True分支里寫了moving mean和moving variance的更新語句,這個會在分支里引入過多的輸入,我猜測TensorFlow對此做了限定,在實驗中我不在sess.run()中求tf.get_collection(tf.GraphKeys.UPDATE_OPS)的值也是可以不會報錯的,綜上,我認為錯誤在於更新語句,也做了一些改進,詳細代碼見validSyncBN.py。
【已完結】
推薦閱讀: