境搭建到模型部署的完整指南)
1. 別急著寫代碼想清楚從零開始到底意味著什么我見過太多人栽在這一步。打開GitHub看到一個叫ai-engineering-from-scratch的倉庫熱血上頭克隆下來就開始跑訓(xùn)練腳本結(jié)果卡在環(huán)境依賴上折騰了一整天最后連數(shù)據(jù)集長什么樣都沒看清楚。這幾乎是每個剛接觸AI工程的初學(xué)者都會踩的坑——拿到了倉庫地址卻不知道自己究竟要在這個工程里做什么。先把這個概念拆開AI工程不是寫模型而是用工程化的方法把模型變成可持續(xù)運行、可維護(hù)、可迭代的系統(tǒng)。一個合格的AI工程至少包含四件套數(shù)據(jù)管線、訓(xùn)練實驗、部署服務(wù)、監(jiān)控運維。絕大多數(shù)初學(xué)者只盯著訓(xùn)練這一環(huán)然后被環(huán)境配置、數(shù)據(jù)清洗、模型存儲、接口封裝這些問題反復(fù)摩擦。所以如果你想認(rèn)真做ai-engineering-from-scratch這個項目我建議你第一步不是看代碼而是畫一張圖把下面這幾件事理清楚這個工程要解決什么問題分類、生成、還是檢索數(shù)據(jù)的來源是什么原始格式長什么樣需要哪些預(yù)處理步驟模型選多大、選什么架構(gòu)訓(xùn)練需要什么樣的GPU資源和顯存訓(xùn)練完成后怎么提供服務(wù)需要多高的并發(fā)和延遲要求模型漂移了怎么辦人工反饋的通道在哪里只有把這些想明白了你才知道倉庫里哪些代碼是核心、哪些是輔助、哪些可以直接跳過不用管。從零開始做AI工程本質(zhì)上是從業(yè)務(wù)問題開始倒推技術(shù)方案而不是從代碼開始正推能跑起來什么。2. 環(huán)境搭建的坑遠(yuǎn)比你想的多得多2.1 Python環(huán)境從Virtualenv到Conda的取舍在正式開始跑任何AI項目之前環(huán)境是第一道關(guān)卡。你可能會覺得用pip install裝幾個包有什么難的但真到了深度學(xué)習(xí)項目里坑馬上就會冒出來CUDA版本對不上、PyTorch和TensorFlow沖突、Python 3.10某庫不兼容、protobuf版本互相打架……我甚至見過有人在Mac上用pip install tensorflow裝上之后發(fā)現(xiàn)只能跑CPU還以為是代碼寫錯了。我的建議是如果你不是只做純Python的原型驗證直接用Conda。不光是虛擬環(huán)境隔離更重要的是它能同時管理Python版本和CUDA依賴尤其是你用NVIDIA GPU做深度學(xué)習(xí)訓(xùn)練的時候。Conda環(huán)境可以做到每個項目一套獨立體系互不污染。用conda create -n ai-eng python3.10創(chuàng)建環(huán)境然后按需安裝PyTorch或TensorFlow對應(yīng)的CUDA版本踩坑概率驟降。2.2 GPU驅(qū)動與CUDA版本的匹配邏輯這是整個環(huán)境搭建過程中最容易讓人崩潰的一段。很多人照著網(wǎng)上的教程裝了CUDA結(jié)果torch.cuda.is_available()返回False怎么排查都找不到問題。核心原因通常是裝了新的CUDA Toolkit但顯卡驅(qū)動太老或者驅(qū)動版本支持的最高CUDA版本低于你安裝的版本。這里有一個基本匹配邏輯顯卡驅(qū)動是底層CUDA Toolkit是上層上層不能超過下層的上限。用nvidia-smi查看驅(qū)動支持的CUDA版本然后裝一個不高于這個上限的CUDA版本再用pip裝對應(yīng)編譯好的PyTorch版本基本就不會出錯。另外我強(qiáng)烈建議在項目根目錄寫一個environment.yml或者至少寫清楚依賴列表版本。不做版本鎖定的AI工程等于給自己埋雷——三個月后你打開之前的項目發(fā)現(xiàn)依賴全變了代碼跑不起來了那種痛苦相信很多人都有體會。2.3 Docker從能跑到可復(fù)現(xiàn)的關(guān)鍵一步到了真的要把項目給別人復(fù)現(xiàn)、或者部署到服務(wù)器上的時候Docker絕對繞不開。如果你只是在自己電腦上調(diào)代碼Conda就夠用但只要涉及團(tuán)隊協(xié)作、服務(wù)器部署、或者你想讓這個從零開始的項目具備工程化的底子一定要一開始就把Dockerfile寫好。FROM pytorch/pytorch:2.1.0-cuda11.8-cudnn8-runtime WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD [python, train.py]這份Dockerfile雖然簡單但它體現(xiàn)了工程化最基本的要求環(huán)境可重復(fù)構(gòu)建。在寫Dockerfile時有幾個細(xì)節(jié)需要注意盡量選擇官方發(fā)布的帶CUDA的鏡像作為基礎(chǔ)避免重復(fù)裝驅(qū)動和CUDA的煩惱把requirements.txt放在源碼之前單獨COPY這樣能充分利用Docker緩存后續(xù)改代碼不會重新安裝依賴構(gòu)建速度快好幾倍。注意國內(nèi)網(wǎng)絡(luò)環(huán)境下拉取Docker鏡像和pip依賴都可能遇到連接問題建議提前配置好鏡像源。這不是小問題很多人的AI工程就是從下載依賴失敗這一步開始放棄的。3. 數(shù)據(jù)管線的優(yōu)先級沒數(shù)據(jù)模型就是花架子3.1 先解決數(shù)據(jù)從哪來再到數(shù)據(jù)怎么喂很多從零開始的AI工程卡住的核心往往不是模型太復(fù)雜而是根本沒有高質(zhì)量數(shù)據(jù)。很多人習(xí)慣先寫好訓(xùn)練代碼然后才去找數(shù)據(jù)集最后發(fā)現(xiàn)格式對不上、標(biāo)簽質(zhì)量差、數(shù)量不夠又回頭改代碼浪費大量時間。我的建議是數(shù)據(jù)先行。不管你是爬公開數(shù)據(jù)集、從業(yè)務(wù)系統(tǒng)導(dǎo)出還是自己設(shè)計標(biāo)注方案一定要在寫模型代碼前先把數(shù)據(jù)流程跑通原始數(shù)據(jù) → 清洗 → 預(yù)處理 → 切分訓(xùn)練/驗證/測試集 → 構(gòu)建DataLoader。這一步走完了后續(xù)訓(xùn)練模型時就會非常順暢。3.2 數(shù)據(jù)清洗的常規(guī)操作清單數(shù)據(jù)清洗聽起來不酷但它是決定模型效果上限的隱形要素。我總結(jié)了一套常規(guī)操作你在項目里直接照著做就可以去重。文本數(shù)據(jù)要處理完全重復(fù)的樣本圖像數(shù)據(jù)檢查完全相同的圖片這一步看似簡單但能有效防止驗證集數(shù)據(jù)泄漏讓模型效果虛高。處理缺失值。圖像中缺樣本可以直接丟棄文本中缺標(biāo)簽可以考慮規(guī)則補(bǔ)全表格數(shù)據(jù)則可以填充均值或眾數(shù)具體策略取決于業(yè)務(wù)場景。標(biāo)準(zhǔn)化/歸一化。文本轉(zhuǎn)小寫、去除停用詞圖像做灰度歸一化數(shù)值特征縮放到0到1區(qū)間。不同模型對此敏感度差異很大但標(biāo)準(zhǔn)做法一定不會錯。標(biāo)簽分布檢查。做一個簡單的統(tǒng)計直方圖看類別是否極度不均衡。如果發(fā)生嚴(yán)重不均衡在訓(xùn)練前就想好要不要做重采樣否則后面模型會被多數(shù)類帶跑偏。這些步驟不需要都用上重武器很多情況下其實就是幾行Python的事。比如用pandas做標(biāo)簽分布檢查import pandas as pd df pd.read_csv(data/labels.csv) value_counts df[label].value_counts() print(value_counts) # 輸出類別分布比例判斷是否需要重采樣 class_ratios value_counts / len(df) print(class_ratios)3.3 數(shù)據(jù)集切分容易被忽視的三個細(xì)節(jié)切訓(xùn)練集、驗證集、測試集人人都知道8:1:1但實際操作里有三個細(xì)節(jié)特別容易被忽略。第一類別比例要保持。如果原始數(shù)據(jù)集里A類占80%、B類占20%那你切出來的訓(xùn)練集也保持這個比例不能切完發(fā)現(xiàn)訓(xùn)練集里全是A類。用sklearn的train_test_split時別忘設(shè)置stratifydf[label]這個參數(shù)。第二驗證集和測試集必須保持未來感。如果數(shù)據(jù)帶時間順序比如新聞文本、交易記錄一定要按時間切分不能隨機(jī)打亂否則就是讓模型用未來的信息預(yù)測過去你得到的是一個看起來95%準(zhǔn)確率但在真實場景下崩得一塌糊涂的模型。第三切分完固定下來。把切分后的索引存成文件或者設(shè)置固定隨機(jī)種子保證每次復(fù)現(xiàn)時訓(xùn)練集、驗證集完全一致。沒有這一步你的實驗對比就沒有公平性可言——每次訓(xùn)練的數(shù)據(jù)都不一樣你根本無法判斷模型效果提升是因為改進(jìn)了模型還是因為換了一批訓(xùn)練數(shù)據(jù)。4. 訓(xùn)練實驗管理你以為的跑通只是萬里長征第一步4.1 實驗記錄為什么比代碼本身更值錢到了訓(xùn)練階段一個典型的錯誤是改一改參數(shù)跑一遍訓(xùn)練看一眼loss下降沒有然后繼續(xù)改——沒有任何記錄。跑了幾十次實驗后看到結(jié)果最好的是第三天的某一次但你完全記不清當(dāng)時用了什么參數(shù)、什么數(shù)據(jù)版本、什么隨機(jī)種子。AI工程和寫普通軟件最大的區(qū)別就在于它的行為是概率性的實驗的復(fù)現(xiàn)和追蹤極其重要。如果你從零開始做工程化我建議在第一天就把下面這些東西記錄下來模型結(jié)構(gòu)的具體配置隱藏層維度、層數(shù)、激活函數(shù)、dropout率訓(xùn)練超參數(shù)學(xué)習(xí)率、batch size、epoch數(shù)、優(yōu)化器及參數(shù)數(shù)據(jù)版本用哪份清洗后的數(shù)據(jù)、預(yù)處理腳本的版本訓(xùn)練環(huán)境PyTorch版本、CUDA版本、GPU型號隨機(jī)種子固定種子保證可復(fù)現(xiàn)關(guān)鍵指標(biāo)train loss、val loss、accuracy、F1等這些信息可以先用一個簡單的CSV表格記錄也可以直接引入MLflow或者Weights and Biases這類實驗管理平臺。我對從零開始的項目的建議是先用CSV手動記錄把習(xí)慣養(yǎng)成等實驗數(shù)量多了再上平臺。4.2 一個最小可用的訓(xùn)練循環(huán)下面這段代碼是一個結(jié)構(gòu)清晰的訓(xùn)練循環(huán)框架你拿到任何入門深度學(xué)習(xí)項目里都可以直接改改就用import torch from torch.utils.data import DataLoader def train_one_epoch(model, dataloader, optimizer, criterion, device): model.train() total_loss 0.0 correct 0 total 0 for inputs, labels in dataloader: inputs, labels inputs.to(device), labels.to(device) optimizer.zero_grad() outputs model(inputs) loss criterion(outputs, labels) loss.backward() optimizer.step() total_loss loss.item() * inputs.size(0) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() avg_loss total_loss / total accuracy correct / total return avg_loss, accuracy def validate(model, dataloader, criterion, device): model.eval() total_loss 0.0 correct 0 total 0 with torch.no_grad(): for inputs, labels in dataloader: inputs, labels inputs.to(device), labels.to(device) outputs model(inputs) loss criterion(outputs, labels) total_loss loss.item() * inputs.size(0) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() avg_loss total_loss / total accuracy correct / total return avg_loss, accuracy如果你觀察足夠仔細(xì)會發(fā)現(xiàn)一個關(guān)鍵點訓(xùn)練時用optimizer.zero_grad()清空梯度這是初學(xué)者最容易忘的。如果忘了梯度會跨batch累加模型行為完全不可控。另外model.train()和model.eval()的切換也不能省它控制著Dropout和BatchNorm在不同階段的行為差別省了它你看到的驗證集指標(biāo)基本沒有參考價值。4.3 從過擬合一個batch到真正收斂我這里有一個實戰(zhàn)中非常有效的經(jīng)驗希望盡早傳授給你正式開始大規(guī)模訓(xùn)練之前先取一兩個batch的數(shù)據(jù)跑一次完整的前向和反向傳播看模型能否把loss降到接近0。這個過程俗稱過擬合一個小batch它的意義在于用最短的時間確認(rèn)整條鏈路是通的——從數(shù)據(jù)加載到模型前向、從loss計算到梯度回傳、從參數(shù)更新到指標(biāo)統(tǒng)計每一個環(huán)節(jié)都沒有bug。很多初學(xué)者跳過了這一步直接讓模型在全部數(shù)據(jù)上訓(xùn)練結(jié)果發(fā)現(xiàn)36個小時跑完loss就是不降再回頭排查發(fā)現(xiàn)是數(shù)據(jù)標(biāo)簽錯位了。這時已經(jīng)浪費了整整一天半的GPU時間。等小batch過擬合測試通過再回到完整訓(xùn)練流程上這就到了策略取舍的地方學(xué)習(xí)率設(shè)多少合適。一個經(jīng)驗是3e-4到5e-5區(qū)間對大多數(shù)Transformer類模型來說是一個安全起點CNN模型可以放寬到1e-3。如果你開啟了學(xué)習(xí)率預(yù)熱和衰減策略通常訓(xùn)練表現(xiàn)會更穩(wěn)定。4.4 早停法與模型保存別把GPU燒到最后訓(xùn)練過程中我比較推薦使用早停法。設(shè)置一個耐心值比如連續(xù)5個epoch驗證集指標(biāo)沒有提升就停止訓(xùn)練把模型恢復(fù)到驗證集表現(xiàn)最好的那一步。這樣做的原因很簡單深度神經(jīng)網(wǎng)絡(luò)的訓(xùn)練曲線往往是鋸齒狀上升的后期可能出現(xiàn)局部波動、過擬合和指標(biāo)回退繼續(xù)燒GPU除了傷害錢包和耐心沒有多少收益。模型保存同樣有講究。我建議每一輪都保存包含訓(xùn)練信息的完整checkpointtorch.save({ epoch: epoch, model_state_dict: model.state_dict(), optimizer_state_dict: optimizer.state_dict(), valid_loss: best_valid_loss, config: model_config, }, fcheckpoints/checkpoint_{epoch:03d}.pt)這樣你在后續(xù)任何時候都可以精確恢復(fù)訓(xùn)練或者進(jìn)行模型評估不用從頭再來。只保存model.state_dict()的做法雖然方便加載但想繼續(xù)訓(xùn)練或者復(fù)盤實驗時就會很被動。從工程角度講一個checkpoint如果能告訴你這個模型在哪個epoch、什么數(shù)據(jù)、什么超參數(shù)下訓(xùn)練的它的價值會超出模型權(quán)重本身。5. 模型部署這最后一公里決定項目能否真正落地5.1 TorchScript、ONNX還是一種簡單服務(wù)化封裝模型訓(xùn)練完怎么給別人用是很多從零開始項目最容易掉鏈子的一步。你有可能把訓(xùn)練好的模型寫成model.pt文件放在Git倉庫里就完事了但其他人想跑起來就得把整個訓(xùn)練代碼拉下來環(huán)境全部裝一遍再調(diào)用一些模糊不清的函數(shù)。這不是工程化的做法。生產(chǎn)環(huán)境里常見的方式有三種TorchScript、ONNX和純服務(wù)化封裝。TorchScript是PyTorch官方提供的模型序列化格式好處是它把模型結(jié)構(gòu)和權(quán)重打包成單一文件不依賴原始Python類定義。導(dǎo)出方式很簡單model.eval() scripted_model torch.jit.script(model.cpu()) scripted_model.save(models/model_scripted.pt)ONNX則更進(jìn)一步把模型轉(zhuǎn)換成與框架無關(guān)的中間格式可以直接用ONNX Runtime在CPU上達(dá)到遠(yuǎn)超PyTorch原生的推理速度。但ONNX對某些動態(tài)結(jié)構(gòu)兼容性不夠理想如果你的模型里包含循環(huán)和條件控制可能需要額外處理。對初學(xué)者來說我建議的路線是先不用管TorchScript和ONNX這些繁瑣格式先寫一個標(biāo)準(zhǔn)的推理腳本封裝把模型的加載和預(yù)測封裝成干凈的predict(input)接口。這段代碼才是工程化的第一步import torch from PIL import Image import torchvision.transforms as transforms class ModelInference: def __init__(self, model_path, devicecpu): self.device torch.device(device) self.model torch.jit.load(model_path, map_locationself.device) self.model.eval() self.transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) def predict(self, image_path): image Image.open(image_path).convert(RGB) tensor self.transform(image).unsqueeze(0).to(self.device) with torch.no_grad(): output self.model(tensor) prob torch.softmax(output, dim1) return prob5.2 FastAPI是當(dāng)前比較穩(wěn)妥的服務(wù)化選擇如果你想把這個推理腳本變成真正的線上服務(wù)我推薦用FastAPI它相比Flask有幾個更貼合AI項目的特點原生支持異步、自動生成API文檔、基于Pydantic做請求參數(shù)校驗尤其適合深度學(xué)習(xí)模型的在線推理場景。一個最小可用的服務(wù)只需要幾十行代碼from fastapi import FastAPI, UploadFile, File import uvicorn import torch app FastAPI() inferer ModelInference(models/model_scripted.pt, devicecpu) app.post(/predict) async def predict(file: UploadFile File(...)): contents await file.read() with open(/tmp/upload.jpg, wb) as f: f.write(contents) probabilities inferer.predict(/tmp/upload.jpg) top3_indices torch.topk(probabilities, 3).indices[0].tolist() return {predictions: [{class_id: idx, probability: float(probabilities[0, idx])} for idx in top3_indices]} if __name__ __main__: uvicorn.run(app, host0.0.0.0, port8000)這個服務(wù)看起來簡陋但它完整地做到了模型推理對外提供HTTP接口這件事。你可以先用uvicorn main:app --reload在本地起服務(wù)然后用curl測一下curl -X POST http://localhost:8000/predict -F filetest.jpg如果一切正常返回的JSON里就是模型的預(yù)測結(jié)果。到了這一步模型能跑和模型能用之間的距離就被拉近了。5.3 部署到云服務(wù)器時的幾個實操建議本地跑通只是第一步真正部署到云服務(wù)器的過程中還有幾個注意點值得提前準(zhǔn)備。第一個是模型文件的分發(fā)和加載路徑。不要把大模型文件直接放進(jìn)Git倉庫Github有100MB的文件大小限制而且push幾十MB的權(quán)重文件會造成倉庫膨脹日后拉取代碼非常痛苦。正規(guī)做法是使用模型專用的對象存儲服務(wù)如S3、OSS或者用Git LFS來管理。第二個是GPU和CPU之間的選擇。很多推理場景其實CPU就夠用。特別是用的預(yù)訓(xùn)練模型參數(shù)量在億級以下且沒有大量請求并發(fā)時開一個好消息是即使不上GPU優(yōu)化過的CPU推理也能滿足一般業(yè)務(wù)需求。更重要的是CPU服務(wù)器通常便宜得多從成本角度考慮非常劃算。第三個是服務(wù)自動重啟和健康檢查。用systemd或Docker Compose管理服務(wù)進(jìn)程讓它崩潰后自動拉起。加上一個/health端點方便負(fù)載均衡和監(jiān)控系統(tǒng)定期檢查服務(wù)是否存活。6. 上線的終點是監(jiān)控模型漂移和效果衰減防不勝防如果你以為模型部署上線后就萬事大吉那我只能說你還沒經(jīng)歷過真正的AI工程。模型跑在生產(chǎn)環(huán)境里數(shù)據(jù)分布每天都在變用戶的輸入模式和訓(xùn)練集有差異模型的效果可能在你不知不覺中持續(xù)下降——這就是所謂的模型漂移問題。它通常發(fā)生在兩種情況下一是數(shù)據(jù)漂移即輸入數(shù)據(jù)分布發(fā)生變化二是概念漂移即輸入到輸出的映射規(guī)則發(fā)生了變化。針對這些問題我建議你從第一天就建立以下三個監(jiān)控習(xí)慣輸入數(shù)據(jù)分布監(jiān)控對每一批真實請求的特征做統(tǒng)計和訓(xùn)練集分布對比發(fā)現(xiàn)異常偏離能及時預(yù)警。預(yù)測結(jié)果質(zhì)量抽檢人工抽樣檢查模型輸出記錄錯誤率和置信度分布變化。延遲和負(fù)載監(jiān)控記錄服務(wù)的推理延遲、并發(fā)請求量、內(nèi)存和GPU利用率確保系統(tǒng)穩(wěn)定性。舉個很直觀的例子一個在晴天拍攝街景下訓(xùn)練出來的分類模型部署后遇到連續(xù)陰雨天氣輸入圖片的像素分布發(fā)生明顯偏移模型準(zhǔn)確率可能從95%直接跌到70%。如果沒有數(shù)據(jù)分布監(jiān)控你可能要等用戶大量投訴后才發(fā)現(xiàn)問題。而有了監(jiān)控告警你可以第一時間發(fā)現(xiàn)分布偏離及時收集新數(shù)據(jù)、重新訓(xùn)練模型把影響控制在最小范圍。7. 從跑通一個倉庫到做出自己的AI工程最后階段的路徑建議說實話ai-engineering-from-scratch這類項目之所以有吸引力恰恰在于它給了你一個從混沌到清晰的成長路徑。但我要提醒你完全照著倉庫跑一遍你的收獲非常有限。真正的成長發(fā)生在你改這個倉庫的那一刻——換一個數(shù)據(jù)集、加一個新功能、換一種模型架構(gòu)、修復(fù)一個bug、把單機(jī)訓(xùn)練改成分布式、把同步推理改成異步隊列。每一次改變都會逼你深入理解一個原本一知半解的環(huán)節(jié)。在這里我給你三條可以具體執(zhí)行的方向選一個小而完整的問題場景比如文本分類或圖像分類從數(shù)據(jù)采集到服務(wù)上線全流程親手走一遍不依賴任何端到端的AutoML工具。把這個項目的某一環(huán)徹底做深例如把數(shù)據(jù)管線切換到流式處理或者把模型推理用ONNX Runtime重寫體會不同技術(shù)選擇之間的差異。給自己設(shè)定一個交付標(biāo)準(zhǔn)Git倉庫里有清晰的README、能一鍵復(fù)現(xiàn)的環(huán)境配置、完整的實驗記錄、標(biāo)準(zhǔn)化的模型推理接口。做到這些你的工程化意識就已經(jīng)比絕大多數(shù)教程搬運工強(qiáng)了。我在做類似的從零項目時最大的感受是AI工程的知識密度其實被遠(yuǎn)遠(yuǎn)低估了。很多人覺得我懂機(jī)器學(xué)習(xí)算法就夠了但實際上數(shù)據(jù)清洗、依賴管理、訓(xùn)練調(diào)度、模型服務(wù)化、監(jiān)控告警、實驗追蹤這些工程能力才是真正區(qū)分能做Demo和能落地的分界線。而且這些能力沒有任何捷徑只能在項目中一個一個坑踩過來踩得越多下一次就越穩(wěn)。