轉自 https://zhuanlan.zhihu.com/p/77248274
Inside tf.estimator(2) 使用記錄
0. 前言
- 基本使用請參考:Inside tf.estimator(1) 基本使用
- 目標:記錄使用
tf.estimator
時碰到的問題。 - 感想:
- 近來在努力搬磚,終於像個真正的算法工程師了。
- 使用的解決方案不一定最優,但都能解決自己的問題。
- 如果有更好的解決方案,請告訴我……
- 本文內容
tf.keras.layers.BatchNormalization
采坑tf.keras.layers.BatchNormalization
不會將修改mean/var/beta等操作自動添加到UPDATE_OPS中。- 使用
model.updates
也存在問題,需要使用model.get_updates_for
才行。 - 單機多卡訓練/預測/評估
- 通過 MirroredStrategy 實現。
- 導入 fine-tune 模型
- 導入fine-tune模型,不能使用
model.load_weights
實現,真的坑。 - 可以通過
tf.train.init_from_checkpoints
/tf.train.Scaffold
/tf.estimator.WarmStartSettings
實現。本文介紹后兩種。 - 保存驗證集誤差最小的模型
- 通過
tf.train.SessionRunHook
實現。
1. tf.keras.layers.BatchNormalization
采坑
- 問題描述:
- 使用
tf.keras
構建模型,通過自定義tf.estimator.EstimatorSpec
,構建了tf.estimator.Estimator
對象。 - 在創建 train_op 時,在
optimizer.minimize
之前使用了with tf.control_dependencies(update_ops)
。 - 當使用 vgg16 作為backend時,模型能夠正常;當使用 resnet50, xception 等作為backend時,效果賊差。
- 發現問題:
- 檢查保存下來的ckpt文件時發現,所有bn的mean為0,var為1,也就是說UPDATE_OPS里沒有添加BN的更新操作。
- 也就是說
tf.keras.layers.BatchNormalization
沒有默認向UPDATE_OPS
中添加默認操作,而tensorflow其他的bn實現都這么做了…… - 解決:
- 獲取所有BN的OPS,添加到
tf.GraphKeys.UPDATE_OPS
。 - 方法一(參考issue):
- 又采坑(這個方法不行):在通過keras的方法獲取,即
model.updates
,是不行的。 - 應該使用
model.get_updates_for()
. - 方法二:參考這篇博客,通過OPS的名稱尋找所有符合要求的OPS。
ops = tf.get_default_graph().get_operations() update_ops = [x for x in ops if ("AssignMovingAvg" in x.name and x.type=="AssignSubVariableOp")] tf.add_to_collection(tf.GraphKeys.UPDATE_OPS, update_ops)
- 感想
- ………………………………………………………………
- 查了下issue,一年多前就有人問了……大哥,好歹文檔里說明下。啊…………………………………………
- 我一直比較抵觸使用 tf.keras,但官方就是推薦使用,那就用吧,還能咋辦呢。但這個BUG表明,官方的意思是使用全套keras……
- keras的作者還在 issue 19643 中宣稱:
- 就是這么設計的。
- 想解決,要么自己手動處理,想要方便就直接用全套 keras API好啦(言下之意,要官方修復是不可能的)……
That is by design. Global collections (and global states in general) are prone to various issues (like most global variables in software engineering) and should be avoided.
...
If you are writing your own custom training loops, you will have to run these updates as part of the call to sess.run(). But do note that tf.keras provides a built-in training loop in the form of model.fit(), as well as primitive methods for building your own custom training loops (in particular model.train_on_batch() and model.predict_on_batch()). If you are using Keras you should generally never have to manually manage your model's updates.
2. 單機多卡
- 目標:單機多卡訓練/預測/評估。
2.1. 多GPU訓練的第一種實現方式
- 最基本、底層的實現方式,不推薦使用。
- 思路:
- 使用
tf.variable_scpe
、tf.device
等對同一個計算圖使用多個gpu創建。 - 分別計算每個GPU中的梯度值,最終匯總求平均,最為最終目標梯度值。
- 實例:官方cifar10示例
2.2. 多GPU訓練的第二種方式
- 使用
MirroredStrategy
實現,強烈推薦。 - 在tf1中,該類位於
tensorflow.contrib.distribute
. - tf1.13后可以直接使用
tf.distribute.MirroredStrategy
來調用,但API有變動…… - 准備工作:安裝NCCL
- 必須安裝nccl,否則會報錯
libnccl.so.2: cannot open shared object file: No such file or directory
。 - 相關issue:issue 22899
- 安裝參考資料:
- 自己的總結(在ubuntu16.04中為cuda9.0安裝nccl):
- 建議直接看文檔。
- 首先下載nccl安裝文件,地址。(像下載cudnn一樣,需要登錄,並選擇合適的版本)

- 如果CUDA版本高於9.2,則可以下載 O/S agnostic local installer,可以直接下載后解壓、將相關文件復制到
/usr/local/cuda
的對應文件夾中,方便很多,更多請參考上面參考資料中的鏈接。 - 如果是cuda9,只能下載deb安裝包,並下載。
- 下載deb包后,要進行安裝:
- 如果是local版,則通過
sudo dpkg -i nccl-repo-<version>.deb
安裝。 - 如果是network版,則通過
sudo dpkg -i nvidia-machine-learning-repo-<version>.deb
安裝。 - 更新APT數據庫:
sudo apt update
- 通過apt安裝libnccl2庫,命令在下載nccl文件的網址中有,大概形式就是:
sudo apt install libnccl2=2.4.7-1+cuda9.0 libnccl-dev=2.4.7-1+cuda9.0
。 - 實現介紹
- 參考資料:Multi-GPU training with Estimators, tf.keras and tf.data
- 在創建
tf.estimator.Estimator
的tf.estimator.RunConfig
中指定train_distribute
。 - 其他不需要任何修改……真的方便……
- 參考代碼如下:
NUM_GPUS = 2 strategy = tf.contrib.distribute.MirroredStrategy(num_gpus=NUM_GPUS) config = tf.estimator.RunConfig(train_distribute=strategy) estimator = tf.keras.estimator.model_to_estimator(model, config=config)
2.3. 方法二的一些采坑以及使用記錄
- 使用多GPU時調用
train/predict/evaluate
方法時,init_fn
參數注意事項 - 返回的必須是
tf.data.Dataset
對象。 - 創建
tf.data.Dataset
對象的過程必須在init_fn
函數中。 - 否則會報類似
ValueError: Tensor(xxx) must be from the same graph as Tensor(xxx).
的錯誤 - 參考資料:issue 20784,官方回復是說:
One of the key features of estimator is that it performs graph and session management for you.
,大概意思是,所有的創建計算圖的過程,最好都封裝在init_fn
和model_fn
中,等Estimator調用時在創建計算圖。 - 其他:
- 假設創建
tf.data.Dataset
中有指定batch_size
,且確定GPU數量為num_gpus
,則多GPU訓練時,數據集實際batch size為batch_size * num_gpus
。 - 建議在創建
tf.data.Dataset
對象時引入prefetch
。 - 在未使用
MirroredStrategy
時,可以在init_fn
外創建好tf.data.Dataset
,而在init_fn
中只創建iterator並調用get_next
函數。 - 若使用
slim
構建模型,很可能會報錯 - 根據issue 27392,問題可能出在
ExponentialMovingAverage
操作上。 - 根據issue 20874,當模型中使用
slim.batch_norm
時會與MirroredStrategy
沖突。 - 個人看法:
- 如果沒有
batch_norm
,可能模型可以正常運行。但有幾個模型是完全不包括batch_norm
的…… - 建議使用
tf.keras
構建模型;如果硬要使用 slim 模型,可以將slim.batch_norm
改為tf.keras.layers.BatchNormalization
。 - slim模型 + tf.estimator + MirroredStrategy 這個組合目前來看應該必須放棄,因為tensorflow的Member已經表態 issue23770 I don't think we have any current effort to add distribute strategy support to slim, and it seems unlikely to become a priority due to the general TF move away from contrib.
3. 導入 fine-tune 模型
- 目標:在訓練之前,導入部分模型的pre-trained model。
3.1. 使用 slim 搭建模型時
- 基本步驟:
- 第一步:定義
init_fn
函數,用於構建tf.train.Scaffold
對象。 - 函數基本形式:
init_fn(scaffold, session)
,用於初始化模型參數。 - 構建
init_fn
可以用到slim.assign_from_checkpoint_fn
,細節就不描述了。 - 第二步:將
tf.train.Scaffold
對象,作為mode == tf.estimator.ModeKeys.PREDICT
時tf.estimator.EstimatorSpec
對象的初始化參數。 - 采坑,曾經想用
tf.train.SessionRunHook
來實現這個功能: - 實現方法(最終失敗):構建子類,在
def after_create_session(session, coord)
方法中將finetune模型參數導入,並將該hook添加到tf.estimator.EstimatorSpec
中。 - 問題:
- 如果訓練中止、再啟動時,需要導入訓練過程中保存的模型文件,即導入
model_dir
中最新的模型參數。 - 導入fine-tune模型參數的操作在每次重起訓練時都會調用,而且調用順序在導入
model_dir
模型參數之后……
3.2 使用 tf.keras 搭建模型時
3.2.1 問題描述:
- 在使用
tf.keras.applications
中的模型都有對應的預訓練模型,想通過這些h5文件進進行fine-tune。 - 采坑:不能直接使用
load_weights
,原因如下: load_weights
的最后一步是通過keras.backend.get_session()
執行 assign 操作,也就是說,會自動創建tf.Session
對象。tf.estimator
的目標是自動管理tf.Session
的生命周期,會創建新的Session
對象,而不會使用keras.backend.get_session()
中的對象。- TensorFlow模型中不能直接使用 h5 文件,所以需要先將h5文件轉換為ckpt。
- 在
tf.estimator
中導入fine-tune模型的方法有很多,可以使用以下幾種: tf.train.init_from_checkpoint
:改變的是變量的 initializer,看issue里有個Member提到這事推薦方法,不過我沒試過。tf.train.Scaffold
:定義其中的init_fn
,將scaffold對象傳入tf.estimator.Estimator
的初始化方法或train
方法。- 使用
tf.estimator.WarmStartSettings
對象,導入tf.estimator.Estimator
初始化方法中。
3.2.2. h5 to ckpt
- 參考keras issue9040,fchollet給出了方法。
- 我自己寫的測試樣例
import tensorflow as tf import os os.environ["CUDA_DEVICE_ORDER"]="PCI_BUS_ID" # see issue #152 os.environ["CUDA_VISIBLE_DEVICES"]="1" config = tf.ConfigProto() config.gpu_options.allow_growth = True tf.keras.backend.set_session(tf.Session(config=config)) path_to_save_ckpt = '/path/to/keras-ckpt' model = tf.keras.applications.VGG16() model_name = 'vgg16' var_list = slim.get_variables_to_restore(include=None, exclude=['predictions']) saver = tf.train.Saver(var_list) saver.save(tf.keras.backend.get_session(), os.path.join(path_to_save_ckpt, model_name + '.ckpt'))
3.2.3 tf.train.Scaffold
對象導入 ckpt 文件實現思路
tf.train.Scaffold
對象中init_fn
方法的定義為:def init_fn(scaffold, session)
。- 通過
slim.get_variables_to_restore(include=[], exclude=[])
獲取對應的變量。 - 通過
slim.assign_from_checkpoint_fn
獲取def init_fn(session)
方法。 - 該方法可將ckpt文件中tensor和當前模型中實際tensor對應起來。
- var_list 可以是list,也可以是dict。
- dict是 ckpt name to tf.Variable。
- list則表明ckpt文件與當前模型中tensor name是一一對應的,即
{var.op.name: var for var in var_list}
- 有了
init_fn(session)
再自己創建一個_init_fn(scaffold, session)
wrap 一下就行了。
3.2.4 tf.estimator.WarmStartSettings
對象導入 ckpt 文件實現思路:
- 該類就是為了實現部分導入模型參數的功能,該類的注釋中有幾個簡單的使用
- 定義該類時有以下幾個參數,現在分別說明:
ckpt_to_initialize_from
:必須填寫,ckpt文件路徑。vars_to_warm_start
:需要導入的參數。- 默認是使用所有 trainable 參數。
- 如果設置為
.*
,則表示所有 trainable 參數。 - 如果設置為
[.*]
,則表示所有參數(包括non-trainable)。 - 可以設置為[string1, string2],分別獲取。
- 小技巧,如果想要設置以XXX結尾,則使用
*xxx[^/]
。 var_name_to_vocab_info
:不知道是啥玩意,沒仔細研究。var_name_to_prev_var_name
:如果ckpt name和當前模型tensor name不對應,可以在這里設置。- 需要注意的是,
var_name_to_prev_var_name
不用於過濾參數。 - 換句話說,如果ckpt name和當前模型的tensor name相同時,可以不在
var_name_to_prev_var_name
中進行配置。 - 采坑:如果選擇的當前模型參數不存在於ckpt文件中,則會報錯。
- 如所有以
Momentum
結尾的參數。 - 解決方法:設置正則表達式,例如設置以
kernel
或bias
結尾的所有變量。 - 舉個例子:
- 對於 keras vgg16 網絡,若想所有卷基層的tensor name和當前模型的var name相同,但全連接層不相同,則
ws = tf.estimator.WarmStartSettings( # ckpt 文件路徑 ckpt_to_initialize_from=os.path.join(ckpt_root_path, ckpt_name), # 獲取所有block開頭、kernel/bias結尾的當前模型var # 獲取所有original_ranking/rank_fc開頭、kernel/bias結尾的當前模型var vars_to_warm_start=['block.+kernel[^/]', 'block.+bias[^/]', 'original_ranking/rank_fc.+kernel[^/]', 'original_ranking/rank_fc.+bias[^/]'], # 若當前模型var name與ckpt name不匹配,則在這里進行處理 # 當前模型 var name to ckpt name var_name_to_prev_var_name={ 'original_ranking/rank_fc1/bias': 'fc1/bias', 'original_ranking/rank_fc1/kernel': 'fc1/kernel', 'original_ranking/rank_fc2/bias': 'fc2/bias', 'original_ranking/rank_fc2/kernel': 'fc2/kernel', } )

4. 保存驗證集誤差最小的模型
tf.estimator.Estimator
中,基本的訓練過程是類似於以下代碼
for i in range(args.num_epochs): # train estimator.train(_train_input_fn, hooks=train_hooks, steps=args.train_steps) if i % args.validation_step == 0: # val every {validation_step} steps estimator.evaluate(_val_input_fn, args.val_steps, hooks=evaluate_hooks)
- 目標:在模型訓練過程中,保留驗證集誤差最小的模型。
- 換句話說:希望根據
evaluate
的結果保存 loss 最小或 accuracy 最大的模型。 - 實現思路:
- 構建hook,每次
evaluate
結束后記錄loss/accuracy的平均數,如果loss/accuracy比之前記錄的對應數值小/大,則保存該模型。 - 難點:由於
tf.estimator.Estimator
封裝了evaluate
的過程,無法直接獲取loss/accuracy的平均值。 - 解決方案:
- 猜測
evaluate
中使用了類似tf.metrics.mean
的操作,用於保存驗證集中的平均loss/accuracy。 - 找到 loss/accuracy 平均數對應的tensor name。
- 我使用的方法是,在tensorboard中找到該名稱。
- 使用
tf.get_default_graph().get_tensor_by_name(tensor_name)
來獲取tf.Tensor
對象。 - 構建hook對象,實現上述思路,並將該hook添加到
evaluate
方法中。 - 源碼示例(保存loss最小的模型):
class EvaluateCheckpointHook(tf.train.SessionRunHook): def __init__(self, eval_dir, # 模型保存路徑 tensor_name='mean/value:0', # 需要對比的tensor名稱 ): self._cur_min_value = 1e8 # 記錄當前最小值 self._tensor_name = tensor_name self._eval_dir = eval_dir def begin(self): self._saver = tf.train.Saver() # saver 必須在 begin 方法中創建 def end(self, session): target_tensor = tf.get_default_graph().get_tensor_by_name( self._tensor_name) # 獲取tensor cur_value = session.run(target_tensor) if self._cur_min_value > cur_value: self._cur_min_value = cur_value self._saver.save(session, self._eval_dir, global_step=tf.train.get_global_step())
編輯於 2019-08-22