習(xí)實(shí)戰(zhàn):從框架選型到模型部署全攻略)
干了這么多年深度學(xué)習(xí)TensorFlow 算是我見(jiàn)面最多、也罵過(guò)最多、最后離不開(kāi)的一個(gè)框架。從早期的 1.x 版本一路用過(guò)來(lái)中間也試過(guò)別的工具但很多實(shí)際項(xiàng)目的訓(xùn)練、調(diào)優(yōu)、上線最后還是落在 TensorFlow 上。這篇文章我打算以一位從業(yè)者的視角把 TensorFlow 從框架選型、核心概念、完整建模到真實(shí)環(huán)境里的坑全部梳理一遍給正準(zhǔn)備入坑或者已經(jīng)在坑里掙扎的朋友一些能直接抄走的經(jīng)驗(yàn)。TensorFlow 能做的事太多了你能用它搭一個(gè)幾層的全連接網(wǎng)絡(luò)跑通手寫數(shù)字識(shí)別也能用它訓(xùn)練幾十億參數(shù)的稀疏模型可以在單張顯卡上調(diào)試一個(gè)圖像分類器也能把訓(xùn)練好的模型轉(zhuǎn)成移動(dòng)端格式塞進(jìn) App。它解決的是深度學(xué)習(xí)從研究到落地的完整鏈路問(wèn)題——數(shù)據(jù)送入模型、模型訓(xùn)練迭代、結(jié)果可視化、導(dǎo)出上線這一整條流水線 TensorFlow 都有對(duì)應(yīng)的官方方案。適合誰(shuí)來(lái)讀如果你是剛接觸深度學(xué)習(xí)、想正經(jīng)弄懂框架底層邏輯的學(xué)生或者已經(jīng)會(huì)用一些框架但被版本問(wèn)題、GPU 報(bào)錯(cuò)折磨到想砸電腦的工程師這篇文章都會(huì)有用。1. 為什么我還在用 TensorFlow框架選型背后的思考1.1 TensorFlow 到底解決了什么問(wèn)題很多人第一次接觸 TensorFlow 時(shí)是懵的因?yàn)樗幌駥懫胀?Python 一樣直接寫for循環(huán)就能實(shí)現(xiàn)邏輯。其實(shí) TensorFlow 要解決的核心問(wèn)題就是自動(dòng)求導(dǎo)和高效計(jì)算。你只要把神經(jīng)網(wǎng)絡(luò)的前向傳播過(guò)程描述出來(lái)框架會(huì)自動(dòng)幫你把梯度算好并且在 GPU/TPU 上把矩陣運(yùn)算并行化。這個(gè)過(guò)程在十多年前是一件極其痛苦的事情而現(xiàn)在你只需要定義模型結(jié)構(gòu)和損失函數(shù)剩下的事情框架全包了。另一個(gè)關(guān)鍵問(wèn)題是部署。做研究的人可能只關(guān)心模型的精度但工程落地的人還關(guān)心模型怎么跟業(yè)務(wù)系統(tǒng)打通。TensorFlow 在設(shè)計(jì)之初就考慮了訓(xùn)練和部署的一體化你可以把訓(xùn)練好的模型保存成一套標(biāo)準(zhǔn)的 SavedModel 格式統(tǒng)一交給不同的服務(wù)端組件去加載。這一點(diǎn)在我實(shí)際項(xiàng)目里省了太多事。以前我在某跨平臺(tái)項(xiàng)目里需要同時(shí)給 Web 端和移動(dòng)端提供推理能力如果用其他生態(tài)不完整的框架可能要單獨(dú)寫一套推理代碼而 TensorFlow 本身提供的轉(zhuǎn)換工具鏈能直接產(chǎn)出對(duì)應(yīng)的輕量模型。1.2 與主流框架橫向?qū)Ρ葹槭裁次易罱K沒(méi)換業(yè)界最常拿來(lái)跟 TensorFlow 比較的當(dāng)然是 PyTorch。我個(gè)人的感受是這樣的PyTorch 的調(diào)試體驗(yàn)確實(shí)更直觀因?yàn)樗膭?dòng)態(tài)圖機(jī)制讓模型結(jié)構(gòu)和 Python 代碼的執(zhí)行順序基本一致打印中間變量、打斷點(diǎn)非常順手。但 TensorFlow 從 2.0 開(kāi)始也默認(rèn)啟用了 Eager Execution調(diào)試體驗(yàn)差距已經(jīng)縮小了。真正拉開(kāi)差距的是生產(chǎn)環(huán)節(jié)TensorFlow Serving 對(duì)模型版本管理、并發(fā)請(qǐng)求的處理非常成熟TF Lite 在移動(dòng)端的算子覆蓋和性能優(yōu)化也比較穩(wěn)定。我自己在項(xiàng)目選型時(shí)遵循一個(gè)原則如果是在校學(xué)生或者需要大量快速實(shí)驗(yàn)、頻繁改模型結(jié)構(gòu)的場(chǎng)景PyTorch 很合適但如果你的模型最終要從離線訓(xùn)練走到線上服務(wù)需要應(yīng)對(duì)高并發(fā)推理、多版本灰度發(fā)布TensorFlow 的整套工具鏈會(huì)讓我更安心。當(dāng)然這不是說(shuō) TensorFlow 完全沒(méi)有學(xué)習(xí)成本它的版本遷移確實(shí)讓人頭疼這點(diǎn)我在后面專門講。1.3 TensorFlow 生態(tài)全景不止訓(xùn)練模型很多人以為 TensorFlow 只是一個(gè)訓(xùn)練模型的庫(kù)這其實(shí)低估了它的邊界。我梳理一下經(jīng)常用到的一套生態(tài)環(huán)境Keras從 2.0 開(kāi)始它就是 TensorFlow 的官方高階 API用幾行代碼就能搭模型。TensorBoard訓(xùn)練可視化工具看 loss 曲線、看權(quán)重直方圖、看計(jì)算圖作用怎么強(qiáng)調(diào)都不過(guò)分。TensorFlow Hub預(yù)訓(xùn)練模型倉(cāng)庫(kù)下個(gè)模型做遷移學(xué)習(xí)非???。TF Lite / TensorFlow.js分別解決移動(dòng)端和瀏覽器端的推理問(wèn)題。TFX全流程機(jī)器學(xué)習(xí)流水線框架適合超大業(yè)務(wù)場(chǎng)景。這些組件不是割裂的它們用的都是同一種模型格式和數(shù)據(jù)抽象所以你從研究和訓(xùn)練到部署的切換成本可以很低。這也是我長(zhǎng)時(shí)間沒(méi)有換框架的核心原因。2. 核心概念拆解從張量到模型的思維方式2.1 張量數(shù)據(jù)在 TensorFlow 中的存在形式TensorFlow 里的所有數(shù)據(jù)都叫張量Tensor你可以把它理解成多維數(shù)組。單純從數(shù)據(jù)結(jié)構(gòu)上說(shuō)一個(gè)標(biāo)量是 0 維張量一個(gè)向量是 1 維張量一個(gè)矩陣是 2 維張量而一張彩色圖片可以被認(rèn)為是高 × 寬 × 通道數(shù)的三維張量。如果是一次訓(xùn)練取 32 張圖那就是四維張量形狀是(32, 高, 寬, 通道數(shù))。在實(shí)際代碼里我經(jīng)常被新同學(xué)問(wèn)到Tensor 和 NumPy 的 ndarray 有什么區(qū)別最直接的區(qū)別是 Tensor 是為了 GPU 計(jì)算設(shè)計(jì)的它能在顯存里直接參與運(yùn)算并且 TensorFlow 會(huì)自動(dòng)追蹤參與梯度計(jì)算的張量。你完全可以把 Tensor 轉(zhuǎn)成 NumPy 來(lái)打印也可以反過(guò)來(lái)把 NumPy 轉(zhuǎn)成 Tensor但頻繁切換會(huì)犧牲性能因?yàn)槊恳徊角袚Q都可能涉及內(nèi)存和顯存之間的數(shù)據(jù)拷貝。import tensorflow as tf # 創(chuàng)建張量的幾種常見(jiàn)方式 a tf.constant([[1, 2], [3, 4]]) b tf.random.normal(shape(4, 4)) c tf.convert_to_tensor([[1.0, 2.0]]) print(a.shape, a.dtype) # (2, 2) dtype: int32 print(b.device) # 如果沒(méi)有 GPU這會(huì)是 /CPU:0這里要注意dtype的一致性。TensorFlow 對(duì)類型很敏感兩個(gè)張量相加如果一個(gè)是整數(shù)型一個(gè)是浮點(diǎn)型很多時(shí)候會(huì)直接報(bào)錯(cuò)而不是像 NumPy 那樣自動(dòng)提升這一點(diǎn)養(yǎng)成習(xí)慣后能幫你少踩很多坑。2.2 計(jì)算圖與 Eager Execution 的兩代變遷如果你用過(guò) TensorFlow 1.x一定忘不了session.run()那種先建圖再執(zhí)行的模式。當(dāng)時(shí)你要用占位符placeholder先把數(shù)據(jù)流定義好然后通過(guò)會(huì)話去執(zhí)行中間想打印一個(gè)中間值都得專門run一次調(diào)試體驗(yàn)很差。2.0 之后默認(rèn)改成了 Eager Execution代碼寫到哪里就執(zhí)行到哪里這對(duì)新人非常友好。那是不是計(jì)算圖就被徹底丟掉呢沒(méi)有。TensorFlow 用一種更聰明的方式——tf.function——把普通 Python 函數(shù)轉(zhuǎn)換成計(jì)算圖。當(dāng)你給函數(shù)加上tf.function裝飾器后函數(shù)內(nèi)部的 Tensor 運(yùn)算會(huì)被 trace 成圖后續(xù)執(zhí)行時(shí)直接跑優(yōu)化過(guò)的圖速度可以提升不少。我自己的經(jīng)驗(yàn)是能用tf.function包起來(lái)的地方盡量包起來(lái)尤其是訓(xùn)練步、數(shù)據(jù)預(yù)處理這些重復(fù)調(diào)用的部分。tf.function def predict_one_step(x): return model(x, trainingFalse)要注意一個(gè)常見(jiàn)坑tf.function對(duì) Python 變量和 Tensor 的處理方式不同。函數(shù)里如果用了普通的 Pythonfor循環(huán)它會(huì)在 trace 時(shí)展開(kāi)如果循環(huán)次數(shù)取決于實(shí)際 Tensor 的值比如tf.while_loop就要用 TensorFlow 自己的循環(huán)控制流否則可能達(dá)不到預(yù)期加速效果。2.3 Keras目前最順手的建模入口Keras 被集成進(jìn) TensorFlow 之后已經(jīng)成為建模的主流入口。我不太建議新同學(xué)一開(kāi)始就去研究底層 API因?yàn)?Keras 抽象已經(jīng)足夠強(qiáng)。它提供了三種建模方式Sequential 模型適合線性的層堆疊像積木一樣一行一行疊上去。Functional API適合有分支、有合并、有多輸入多輸出的模型這也是我在實(shí)際工程中用得最多的。Model Subclassing繼承tf.keras.Model自定義 forward 過(guò)程靈活性最高但坑也最多尤其是配合tf.function時(shí)容易出現(xiàn)變量創(chuàng)建時(shí)機(jī)的問(wèn)題。我舉個(gè) Functional API 的例子比如一個(gè)殘差塊inputs tf.keras.Input(shape(32, 32, 3)) x tf.keras.layers.Conv2D(32, 3, activationrelu)(inputs) x tf.keras.layers.Conv2D(32, 3, activationrelu)(x) shortcut tf.keras.layers.Conv2D(32, 1)(inputs) outputs tf.keras.layers.Add()([x, shortcut]) model tf.keras.Model(inputs, outputs)Functional API 的好處是每個(gè)中間層都會(huì)產(chǎn)出一個(gè)帶形狀信息的 Tensor你可以在串聯(lián)時(shí)很自然地檢查每一層的輸出尺寸發(fā)現(xiàn)維度不匹配時(shí)能快速定位。2.4 tf.data數(shù)據(jù)管道的細(xì)節(jié)與坑數(shù)據(jù)加載是很多時(shí)候性能瓶頸所在。GPU 算得快沒(méi)用如果 CPU 來(lái)不及喂數(shù)據(jù)訓(xùn)練就會(huì)一直等。TensorFlow 提供了tf.data.Dataset這個(gè)數(shù)據(jù)管道工具我用下來(lái)覺(jué)得最關(guān)鍵的是三件事先用.batch()把數(shù)據(jù)打包再用.prefetch()讓數(shù)據(jù)加載和模型訓(xùn)練并行運(yùn)算重的預(yù)處理用.map()并行執(zhí)行。dataset tf.data.Dataset.from_tensor_slices((images, labels)) dataset dataset.shuffle(10000).batch(128).map(preprocess_func, num_parallel_callstf.data.AUTOTUNE).prefetch(tf.data.AUTOTUNE)這里的tf.data.AUTOTUNE會(huì)讓 TensorFlow 自動(dòng)調(diào)整并行線程數(shù)一般來(lái)說(shuō)比自己拍腦袋設(shè)置一個(gè)固定值要更省心。另外要記住.shuffle()的順序必須放在.batch()前面否則你只是在一個(gè) batch 內(nèi)部打亂全局順序沒(méi)變對(duì)訓(xùn)練沒(méi)有意義。3. 完整實(shí)操?gòu)牧銟?gòu)建一個(gè)圖像分類模型3.1 環(huán)境準(zhǔn)備與版本選擇的經(jīng)驗(yàn)TensorFlow 的版本問(wèn)題可以說(shuō)是老生常談但我還是要強(qiáng)調(diào)安裝之前先確認(rèn) Python 版本、CUDA 版本、cuDNN 版本和 TensorFlow 版本之間的對(duì)應(yīng)關(guān)系。我踩過(guò)最慘的坑是自認(rèn)為裝好了 TensorFlow結(jié)果只裝了 CPU 版沒(méi)有裝 GPU 版的配套驅(qū)動(dòng)訓(xùn)練速度慢得像在爬?,F(xiàn)在官方的對(duì)應(yīng)關(guān)系寫得很清楚如果你用的是 2.10 以上的版本W(wǎng)indows 原生 GPU 支持已經(jīng)被官方移除了推薦直接用 Docker 鏡像或者 WSL2 環(huán)境。我個(gè)人現(xiàn)在的做法是用虛擬環(huán)境隔離項(xiàng)目避免全局依賴污染。安裝前先建立一個(gè)requirements.txt把tensorflow以及常用的numpy、pandas、matplotlib一起固定版本。裝完以后立刻跑一句tf.test.is_gpu_available()注意新版是tf.config.list_physical_devices(GPU)確認(rèn)是否能識(shí)別到 GPU。python -m venv tf_env source tf_env/bin/activate # Windows 下是 tf_env\Scripts\activate pip install --upgrade pip pip install tensorflow python -c import tensorflow as tf; print(tf.__version__); print(tf.config.list_physical_devices(GPU))3.2 數(shù)據(jù)準(zhǔn)備和預(yù)處理這里我用tf.keras.datasets.fashion_mnist來(lái)演示因?yàn)樗莾?nèi)置的、拿起來(lái)就能用。第一次運(yùn)行會(huì)自動(dòng)下載但如果你的網(wǎng)絡(luò)不穩(wěn)定下載可能超時(shí)失敗。我的建議是先單獨(dú)跑一段下載邏輯把數(shù)據(jù)文件保存到本地再做成統(tǒng)一的數(shù)據(jù)集接口這樣訓(xùn)練時(shí)就不受網(wǎng)絡(luò)影響。數(shù)據(jù)預(yù)處理的幾個(gè)要點(diǎn)像素值從0-255縮放到0-1是必須的這能讓梯度更穩(wěn)定。對(duì)于圖像分類任務(wù)可以做簡(jiǎn)單的數(shù)據(jù)增強(qiáng)比如隨機(jī)翻轉(zhuǎn)、隨機(jī)裁剪但這會(huì)增加訓(xùn)練時(shí)間要平衡使用。不需要把所有數(shù)據(jù)一次性放進(jìn)內(nèi)存的數(shù)據(jù)集用tf.data按批次讀取就好。(x_train, y_train), (x_test, y_test) tf.keras.datasets.fashion_mnist.load_data() x_train x_train.astype(float32) / 255.0 x_test x_test.astype(float32) / 255.0 train_ds tf.data.Dataset.from_tensor_slices((x_train, y_train)) train_ds train_ds.shuffle(5000).batch(64).prefetch(tf.data.AUTOTUNE)3.3 模型構(gòu)建與訓(xùn)練對(duì)于 fashion_mnist輸入是 28×28 的單通道灰度圖類別有 10 類。我用一個(gè)比較簡(jiǎn)單的卷積網(wǎng)絡(luò)來(lái)演示先用卷積層提取空間特征再用池化層降采樣最后展平接全連接層輸出 10 個(gè)類別的概率。model tf.keras.Sequential([ tf.keras.layers.Reshape((28, 28, 1), input_shape(28, 28)), tf.keras.layers.Conv2D(32, (3, 3), activationrelu), tf.keras.layers.MaxPooling2D((2, 2)), tf.keras.layers.Conv2D(64, (3, 3), activationrelu), tf.keras.layers.MaxPooling2D((2, 2)), tf.keras.layers.Flatten(), tf.keras.layers.Dense(128, activationrelu), tf.keras.layers.Dropout(0.3), tf.keras.layers.Dense(10, activationsoftmax) ]) model.compile( optimizeradam, losssparse_categorical_crossentropy, metrics[accuracy] )這里有兩個(gè)細(xì)節(jié)值得解釋一是損失函數(shù)為什么用sparse_categorical_crossentropy而不是categorical_crossentropy。因?yàn)槲覀兊臉?biāo)簽是整數(shù)形式而不是 one-hot 編碼所以用 sparse 版本可以直接計(jì)算省一步編碼過(guò)程。二是Dropout的作用它在訓(xùn)練時(shí)隨機(jī)讓一部分神經(jīng)元失活強(qiáng)制網(wǎng)絡(luò)學(xué)習(xí)到更魯棒的特征是輕量級(jí)防過(guò)擬合的常用手段。調(diào)用model.fit時(shí)我習(xí)慣把驗(yàn)證集一起傳進(jìn)去并且加上早停和動(dòng)態(tài)學(xué)習(xí)率callbacks [ tf.keras.callbacks.EarlyStopping(patience5, restore_best_weightsTrue), tf.keras.callbacks.ReduceLROnPlateau(factor0.5, patience3), tf.keras.callbacks.TensorBoard(log_dir./logs) ] history model.fit( train_ds, validation_data(x_test, y_test), epochs20, callbackscallbacks )EarlyStopping的patience5表示驗(yàn)證集指標(biāo)連續(xù) 5 個(gè) epoch 沒(méi)有提升就停止訓(xùn)練同時(shí)restore_best_weightsTrue會(huì)把模型恢復(fù)到歷史上最好的那一次權(quán)重。這樣既節(jié)約時(shí)間也避免訓(xùn)練過(guò)頭。3.4 訓(xùn)練監(jiān)控與調(diào)參技巧訓(xùn)練不是model.fit跑完就結(jié)束的。我會(huì)在訓(xùn)練過(guò)程中一直開(kāi)著 TensorBoard怎么看曲線是基本功訓(xùn)練 loss 不斷下降、驗(yàn)證 loss 也在下降說(shuō)明模型正常學(xué)習(xí)訓(xùn)練 loss 下降但驗(yàn)證 loss 不降反升這就是過(guò)擬合的典型信號(hào)。此時(shí)可以加大 Dropout、增加正則項(xiàng)、做數(shù)據(jù)增強(qiáng)或者減小模型容量。調(diào)學(xué)習(xí)率也有技巧。如果你的 loss 震蕩得很厲害很可能是學(xué)習(xí)率太大了。我常用的一個(gè)方法是先用較小 batch size 和默認(rèn)學(xué)習(xí)率跑十幾個(gè) epoch觀察曲線形態(tài)再用ReduceLROnPlateau讓學(xué)習(xí)率自動(dòng)下調(diào)。另一個(gè)經(jīng)驗(yàn)是不要一上來(lái)就追求大模型。先搭一個(gè)小的完整流程保證代碼、數(shù)據(jù)、評(píng)估邏輯沒(méi)有 bug再逐步擴(kuò)大模型規(guī)模所有深度學(xué)習(xí)項(xiàng)目都應(yīng)遵循這條路線。3.5 模型導(dǎo)出與部署思路訓(xùn)練完的模型不能只在 Jupyter 里展示。要用model.save(model_dir)保存成 SavedModel 格式。SavedModel 是 TensorFlow 部署的標(biāo)準(zhǔn)格式后面不管是用 TensorFlow Serving 加載還是用 TF Lite 轉(zhuǎn)換都是以它為起點(diǎn)。model.save(fashion_model) # 加載回來(lái) loaded_model tf.keras.models.load_model(fashion_model)如果你要部署到移動(dòng)端轉(zhuǎn)換成 TF Lite 格式非常直接converter tf.lite.TFLiteConverter.from_saved_model(fashion_model) tflite_model converter.convert() with open(model.tflite, wb) as f: f.write(tflite_model)在部署場(chǎng)景中有一個(gè)容易被忽略的問(wèn)題模型輸入數(shù)據(jù)的預(yù)處理必須和訓(xùn)練時(shí)保持一致。比如訓(xùn)練時(shí)做了歸一化部署時(shí)也必須在服務(wù)端做同樣的歸一化否則模型效果大打折扣。很多項(xiàng)目上線后準(zhǔn)確率驟降查到最后都是預(yù)處理不一致的問(wèn)題。4. 實(shí)戰(zhàn)中繞不開(kāi)的陷阱常見(jiàn)問(wèn)題與排查實(shí)錄4.1 版本不兼容問(wèn)題一網(wǎng)打盡TensorFlow 2.x 的代碼和 1.x 相比改動(dòng)很大網(wǎng)上很多老教程里的tf.placeholder、tf.Session已經(jīng)不存在了如果你照著敲第一行就會(huì)報(bào)AttributeError。我的建議是統(tǒng)一使用 2.x 的寫法。另一個(gè)高頻問(wèn)題是 Keras 的混用。安裝了獨(dú)立的keras包和 TensorFlow 自帶的tf.keras時(shí)兩個(gè)版本可能不一致導(dǎo)致模型保存和加載互相不認(rèn)。解決方法是始終使用tf.keras不要混用兩套 API。我現(xiàn)在遇到任何莫名其妙的加載報(bào)錯(cuò)第一反應(yīng)就是檢查環(huán)境中是否出現(xiàn)了兩套 Keras。4.2 GPU 環(huán)境常見(jiàn)的三類報(bào)錯(cuò)GPU 問(wèn)題是重災(zāi)區(qū)。第一類是CUDA/cuDNN找不到通常是因?yàn)橄到y(tǒng)里裝了不對(duì)應(yīng)版本的運(yùn)行庫(kù)。此時(shí)不要一股腦重裝最新版 CUDA應(yīng)該先查 TensorFlow 官方編譯時(shí)使用的對(duì)應(yīng)版本。第二類是顯存不足典型報(bào)錯(cuò)是ResourceExhaustedError這個(gè)可能是模型太大或者 batch size 太大適當(dāng)減小 batch size、圖片分辨率或者模型通道數(shù)即可。第三類是 GPU 顯存沒(méi)有完全釋放常見(jiàn)于在同一個(gè)進(jìn)程中多次創(chuàng)建模型我習(xí)慣在訓(xùn)練完一個(gè)模型后手動(dòng)清掉不必要的引用必要時(shí)用tf.keras.backend.clear_session()重置。還有一個(gè)很實(shí)用的小技巧是限制 TensorFlow 顯存的自增長(zhǎng)而不是一次性把所有顯存占滿gpus tf.config.list_physical_devices(GPU) if gpus: try: for gpu in gpus: tf.config.experimental.set_memory_growth(gpu, True) except RuntimeError as e: print(e)這樣其他程序還能共享顯卡資源不至于一上來(lái)就 OOM。4.3 訓(xùn)練不收斂/過(guò)擬合的排查思路如果訓(xùn)練 loss 怎么都不降我有一套固定的排查順序先檢查數(shù)據(jù)有沒(méi)有正確配對(duì)比如標(biāo)簽是否錯(cuò)位再檢查網(wǎng)絡(luò)最后一層和損失函數(shù)是否匹配比如二分類用了 softmax categorical_crossentropy、但是標(biāo)簽沒(méi)做 one-hot再檢查是不是學(xué)習(xí)率過(guò)大導(dǎo)致梯度震蕩。這些都排除后可以考慮是不是特征本身沒(méi)做歸一化。過(guò)擬合的排查更典型。如果訓(xùn)練集精度高、驗(yàn)證集精度低第一件事就是看數(shù)據(jù)量如果每類圖片只有幾百?gòu)堅(jiān)購(gòu)?fù)雜的模型也容易過(guò)擬合。解決方案優(yōu)先級(jí)從低到高增加數(shù)據(jù)增強(qiáng)、增加 Dropout 比例、縮小模型、加入 L2 正則化、用預(yù)訓(xùn)練模型做遷移學(xué)習(xí)。注意不要一上來(lái)就上大模型。4.4 內(nèi)存管理 OOM 排查除了顯存 OOMCPU 內(nèi)存也可能 OOM尤其是用tf.data加載超大數(shù)據(jù)集時(shí)。一個(gè)常見(jiàn)錯(cuò)誤是使用了.map時(shí)把數(shù)據(jù)保留在 Python 端而不是 Tensor 端。我記得有次處理上億條特征數(shù)據(jù)因?yàn)橹虚g某一步把 Tensor 轉(zhuǎn)成了 NumPy導(dǎo)致內(nèi)存瞬間爆掉。原則是不要讓 Tensor 在 Python 和 C 之間反復(fù)橫跳能用tf.data內(nèi)部算子完成的事情就不要自己用 Python 循環(huán)取數(shù)據(jù)。另一個(gè)經(jīng)驗(yàn)是使用tf.data的.cache()方法將經(jīng)過(guò)預(yù)處理的數(shù)據(jù)緩存到本地文件這樣后續(xù)每個(gè) epoch 都可以直接讀緩存而不是重新計(jì)算。4.5 實(shí)用調(diào)試工具清單最后分享幾個(gè)幫我省過(guò)很多時(shí)間的工具TensorBoard我?guī)缀趺枯唽?shí)驗(yàn)都會(huì)記錄曲線尤其是學(xué)習(xí)率、梯度和權(quán)重分布。tf.debuggingtf.debugging.assert_equal、tf.debugging.assert_shapes這類斷言工具可以在訓(xùn)練中自動(dòng)檢查張量形狀和值。模型里的summary()打印模型結(jié)構(gòu)和參數(shù)數(shù)量搭完后先跑一跑確認(rèn)輸出 shape 是否符合預(yù)期。異常捕獲把tf.errors.InternalError等異常信息打印完整堆棧不要只盯著最后一行的 error message。我個(gè)人在實(shí)際項(xiàng)目里還有一個(gè)習(xí)慣把每個(gè)可疑環(huán)節(jié)單獨(dú)寫成一個(gè)很小的可跑腳本排查哪一個(gè)環(huán)節(jié)報(bào)錯(cuò)。比如數(shù)據(jù)管道有錯(cuò)我會(huì)先用一個(gè)小數(shù)據(jù)集單步跑一次確認(rèn)無(wú)誤再回到完整訓(xùn)練流程。這種從簡(jiǎn)到繁的排查方式比面對(duì)一大段崩潰日志去猜測(cè)要高效得多。TensorFlow 這條路我走了很久踩過(guò)很多坑但每次把一個(gè)模型從數(shù)據(jù)加載一路推到線上服務(wù)那種成就感是實(shí)實(shí)在在的。如果你剛開(kāi)始學(xué)我的建議是先不要貪多把 Keras 建模流程和tf.data數(shù)據(jù)管道弄清楚再慢慢深入到底層原理。千萬(wàn)別被版本問(wèn)題的間歇性勸退因?yàn)檫@些坑都有跡可循回頭來(lái)看都是在幫你加深對(duì)框架運(yùn)行機(jī)制的理解。希望這些經(jīng)驗(yàn)?zāi)茏屇闵僮咭恍┪耶?dāng)年走過(guò)的彎路。