的完整閉環(huán))
做AI工程這行久了經(jīng)常有朋友問我“怎么從零開始入門”。我一開始沒太當(dāng)回事覺得照著教程跑通幾個模型就算入門了。直到我親眼看到不少科班出身、算法調(diào)參很熟練的同學(xué)在把一個模型真正變成線上服務(wù)時被各種工程問題捶得沒脾氣才意識到所謂的“從零開始”指的根本不是會跑通一個notebook而是能在真實(shí)業(yè)務(wù)環(huán)境里把模型從想法變成持續(xù)穩(wěn)定運(yùn)行的系統(tǒng)。今天這篇內(nèi)容就是基于“從零開始搭建AI工程能力”這個主題把我在實(shí)際項(xiàng)目中反復(fù)踩過、也反復(fù)優(yōu)化過的一套方法論整理出來。它不教你怎么推導(dǎo)Transformer公式也不講怎么刷LeetCode只關(guān)注一件事一個非AI背景的工程師或者一個算法基礎(chǔ)不錯但工程經(jīng)驗(yàn)為零的人要沿著怎樣一條路線才能把一個AI項(xiàng)目從實(shí)驗(yàn)推到生產(chǎn)并且持續(xù)迭代。1. 從零起步AI工程與算法實(shí)驗(yàn)的本質(zhì)差別很多人會把“AI工程”和“機(jī)器學(xué)習(xí)算法”混為一談這是第一個需要掰開揉碎講清楚的問題。我做過的項(xiàng)目里花在模型結(jié)構(gòu)設(shè)計(jì)上的時間通常只占不到30%剩下70%都在處理數(shù)據(jù)質(zhì)量、管道調(diào)度、接口延遲、資源成本、模型版本回滾這些“不入流”的臟活。如果一開始就奔著“我要寫一個高級模型”去大概率會在真實(shí)系統(tǒng)的復(fù)雜面前被徹底淹沒。算法實(shí)驗(yàn)是一次性的邏輯驗(yàn)證AI工程是算法系統(tǒng)的全生命周期管理。你可以把算法實(shí)驗(yàn)想象成在廚房里研究一道新菜鍋碗瓢盆隨你折騰做壞了重來一鍋成本極低。而AI工程是開一家餐廳菜譜再好也得考慮供應(yīng)鏈、冷藏保鮮、出菜速度、服務(wù)員培訓(xùn)和顧客口味變化。同樣的菜譜在前者的實(shí)驗(yàn)室環(huán)境里能得滿分放進(jìn)后者的營業(yè)場景里可能連及格都難。所以“從零開始”的第一課不是去找算力買顯卡而是建立一套規(guī)模化的工程思維。我在實(shí)際項(xiàng)目中總結(jié)過AI工程能力的最低可行閉環(huán)包括五個環(huán)節(jié)需求拆解把一個模糊的業(yè)務(wù)問題翻譯成可優(yōu)化的機(jī)器學(xué)習(xí)目標(biāo)數(shù)據(jù)閉環(huán)構(gòu)建訓(xùn)練數(shù)據(jù)、驗(yàn)證數(shù)據(jù)、線上反饋數(shù)據(jù)的流動管道訓(xùn)練管理可復(fù)現(xiàn)的實(shí)驗(yàn)記錄、模型版本、超參數(shù)配置服務(wù)化部署模型上線成API、定時任務(wù)或嵌入式模塊監(jiān)控回歸持續(xù)觀察線上指標(biāo)自動發(fā)現(xiàn)模型衰減。這個閉環(huán)缺了任何一環(huán)后面都會付出代價。我見過很多團(tuán)隊(duì)號稱在“用AI”實(shí)際上就是離線跑一次腳本把預(yù)測結(jié)果倒成Excel發(fā)出去。這連AI工程的邊都沒摸到最多叫數(shù)據(jù)加工。在這個環(huán)節(jié)里我強(qiáng)烈建議初學(xué)者打破一個心理定勢不要把所有問題都當(dāng)成一個“端到端深度學(xué)習(xí)”問題。從零開始學(xué)AI工程反而是先學(xué)會權(quán)衡邏輯規(guī)則能解決的不用模型線性模型能解決的不用GBDTGBDT能解決的別上來就上大模型。工程上每多一分模型復(fù)雜度就多十分維護(hù)成本。2. 搭建第一套可復(fù)用的AI工程腳手架我以前也經(jīng)歷過“隨便開個文件夾寫代碼”的階段。直到有一次需要回滾到一周前的模型卻發(fā)現(xiàn)當(dāng)時的訓(xùn)練腳本已經(jīng)改得面目全非連自己都分不清哪份代碼產(chǎn)出過哪份模型我才意識到AI工程的第一步必須是腳手架而不是模型。一套真正可復(fù)用的AI工程腳手架至少要覆蓋三個層面的能力代碼層面的模塊邊界、數(shù)據(jù)層面的版本管理、實(shí)驗(yàn)層面的指標(biāo)追蹤。先說代碼模塊邊界。我建議一個最小項(xiàng)目這樣劃分目錄project/ ├── data/ # 原始數(shù)據(jù)與中間產(chǎn)物 │ ├── raw/ │ └── processed/ ├── src/ │ ├── features/ # 特征工程 │ ├── models/ # 模型定義與訓(xùn)練 │ ├── serving/ # 模型服務(wù)化接口 │ └── utils/ # 公共工具 ├── configs/ # 配置文件超參、路徑、環(huán)境 ├── experiments/ # 每次實(shí)驗(yàn)的記錄 ├── notebooks/ # 探索性分析 └── tests/ # 單元測試和冒煙測試這個劃分不是拍腦袋定的它的核心原則是“配置與代碼分離、數(shù)據(jù)與邏輯分離、實(shí)驗(yàn)與源碼分離”。你寫一個訓(xùn)練腳本不應(yīng)在腳本里寫死一個文件路徑而應(yīng)該通過config文件讀取。這樣同一個代碼就能跑不同的數(shù)據(jù)集不同超參組合也能追溯。然后是數(shù)據(jù)版本管理。代碼可以進(jìn)Git但數(shù)據(jù)往往幾個G甚至幾個T不適合直接塞進(jìn)Git。我實(shí)際用的是DVC的輕量思路——把元數(shù)據(jù)哈希值、文件地址交給Git數(shù)據(jù)本體放在共享存儲。這是一個小到可以手工實(shí)現(xiàn)的方案每次數(shù)據(jù)更新記錄下數(shù)據(jù)快照hash作為實(shí)驗(yàn)配置的一部分。這樣做的好處是任何一次訓(xùn)練都能明確回答“我用的是哪份數(shù)據(jù)訓(xùn)練出來的”。實(shí)驗(yàn)追蹤我用的是MLflow它給我?guī)淼暮诵膬r值不是漂亮的后臺而是“可復(fù)現(xiàn)”的保障。每次訓(xùn)練啟動時自動把以下內(nèi)容記入實(shí)驗(yàn)記錄代碼版本Git commit hash數(shù)據(jù)版本數(shù)據(jù)目錄hash完整超參數(shù)通過命令行或配置文件注入評估指標(biāo)訓(xùn)練集、驗(yàn)證集、測試集產(chǎn)物路徑模型二進(jìn)制、預(yù)處理pipeline文件一套腳手架跑起來的標(biāo)志是你訓(xùn)練完一個模型一周后哪怕是別人也能不看任何解釋只靠README和實(shí)驗(yàn)記錄完整重建整個訓(xùn)練過程和產(chǎn)物。如果你的項(xiàng)目達(dá)不到這個程度它還不能被稱為“工程”只是腳本。這里我想額外提一句關(guān)于環(huán)境依賴的管理。Python的依賴地獄是每個AI工程師躲不掉的。不要相信requirements.txt寫到“tensorflow2.0”這種寫法我踩過不少坑發(fā)現(xiàn)就算同一個大版本不同小版本在GPU算子上的行為都會有差異。建議直接鎖定子版本更穩(wěn)妥的是用Docker鏡像鎖定整個操作系統(tǒng)、CUDA、Python和依賴庫的組合。這相當(dāng)于你給訓(xùn)練任務(wù)買了一份“環(huán)境保險(xiǎn)”。3. 訓(xùn)練迭代中被低估的數(shù)據(jù)與評估問題訓(xùn)練誰都會跑幾個epoch誰都能跑。但真正拉開差距的是兩個“隱形問題”數(shù)據(jù)質(zhì)量怎么保障模型好不好怎么度量。先聊數(shù)據(jù)。很多教程用現(xiàn)成的DataLoader糊弄過去現(xiàn)實(shí)中的數(shù)據(jù)往往都是臟的、亂的、不平衡的。我見過一個項(xiàng)目線上收益一直上不去最后發(fā)現(xiàn)訓(xùn)練數(shù)據(jù)里有大量重復(fù)樣本同一個用戶的行為被抽樣了多次導(dǎo)致模型嚴(yán)重偏置。那之后我把“數(shù)據(jù)畫像”作為訓(xùn)練前強(qiáng)制環(huán)節(jié)。所謂數(shù)據(jù)畫像就是在特征訓(xùn)練前用腳本產(chǎn)出數(shù)據(jù)分布報(bào)告包括每列特征的缺失率、均值、方差、分位數(shù)目標(biāo)變量在訓(xùn)練集和驗(yàn)證集的分布對比特征與目標(biāo)之間的簡單相關(guān)性樣本時間戳的跨度與間隔分布。這一步看起來基礎(chǔ)但能防住很多奇怪的模型行為。舉個例子如果訓(xùn)練集的時間范圍是1月到5月驗(yàn)證集是6月這兩個月里數(shù)據(jù)分布發(fā)生了明顯漂移那你做出來的指標(biāo)再高也只是歷史擬合上線后照樣崩。數(shù)據(jù)畫像能讓你提前看到“訓(xùn)練和驗(yàn)證來自不同世界”的警告。再說評估。我自己吃過最大的虧是只盯著單一指標(biāo)做優(yōu)化。分類任務(wù)就只看AUC回歸任務(wù)就只看MAE最后模型上線業(yè)務(wù)方跟我說“你這個預(yù)測完全沒用”。后來我學(xué)乖了嗎其實(shí)沒有。模型離線指標(biāo)與線上業(yè)務(wù)指標(biāo)之間存在一條無法完全抹平的鴻溝但可以用一套“分層評估”把它們拉近。我的做法是把評估指標(biāo)分成三層第一層是算法指標(biāo)AUC、LogLoss、召回率、精確率這些用于快速比較不同版本模型的優(yōu)劣第二層是業(yè)務(wù)代理指標(biāo)比如推薦場景的點(diǎn)擊率預(yù)估離線算一下預(yù)測值和真實(shí)點(diǎn)擊的相關(guān)性、AUC分段收益曲線等第三層是灰度指標(biāo)上線后通過A/B實(shí)驗(yàn)直接看業(yè)務(wù)KPI如成交轉(zhuǎn)化、停留時長、投訴率。這三層不是互相替代而是從不同層面給模型做體檢。很多從零開始的初學(xué)者眼里只有第一層指標(biāo)所以模型“看著好”卻“用著差”。當(dāng)你把評估體系搭建完整后你就不會再被一個高AUC沖昏頭腦了——因?yàn)槟阒浪芙忉屖裁床荒芙忉屖裁?。在?xùn)練迭代策略上還有一條實(shí)用的經(jīng)驗(yàn)不要試圖一次性把模型調(diào)到完美而是固定好基線做增量改進(jìn)。我習(xí)慣的做法是先把最簡單的邏輯回歸或線性模型跑通作為baseline然后一步步增加特征和模型復(fù)雜度。每一步都保留到實(shí)驗(yàn)記錄里這樣你能隨時判斷新加的東西到底是正向還是負(fù)向貢獻(xiàn)。沒有baseline的優(yōu)化都是在裸泳。4. 部署上線從離線實(shí)驗(yàn)到生產(chǎn)推理的最后一公里模型練出來了離線指標(biāo)很不錯接下來就是AI工程里最容易翻車的地方——部署上線。離線環(huán)境訓(xùn)練用的Python版本、依賴庫、系統(tǒng)環(huán)境相對干凈但生產(chǎn)環(huán)境可沒那么聽話。本地上跑得飛快的推理代碼放到線上容器里可能連路徑都找不到。部署方式的選擇要基于你的實(shí)際需求而不是追新。我總結(jié)了四種常見的模型上線形態(tài)以及它們的適用場景形態(tài)適用場景延遲要求實(shí)現(xiàn)復(fù)雜度離線批處理用戶分群、批量推薦、定時報(bào)表分鐘級到小時級低在線API實(shí)時推薦、風(fēng)控決策、聊天助手毫秒級到百毫秒級中高嵌入式推理移動端、邊緣設(shè)備、IoT毫秒級高流式處理實(shí)時事件流、即刻策略響應(yīng)秒級高從零開始我建議先掌握離線批處理和在線API兩者。離線批處理最簡單本質(zhì)上是寫好一個推理腳本在特定時間點(diǎn)對一批數(shù)據(jù)運(yùn)行輸出結(jié)果到數(shù)據(jù)庫或文件。這個過程中最關(guān)鍵的是冪等性設(shè)計(jì)同一時刻跑兩次、或者重跑同一個數(shù)據(jù)批次結(jié)果必須是一樣的。不然調(diào)度系統(tǒng)一重試就會出現(xiàn)重復(fù)數(shù)據(jù)。在線API則涉及到更多的工程細(xì)節(jié)。我拿一個實(shí)際的FastAPI推理服務(wù)來說明。假設(shè)你訓(xùn)練好了一個用于預(yù)測用戶點(diǎn)擊概率的XGBoost模型模型的輸入需要經(jīng)過特征工程轉(zhuǎn)換。你不能讓線上服務(wù)每次請求都從頭做一遍特征處理這樣延遲會很高而且容易和訓(xùn)練時的特征處理不一致。正確做法是把特征處理Pipeline和模型一起打包上線。操作上我把所有特征變換邏輯封裝成一個transform函數(shù)然后把這個函數(shù)所在的模塊和模型文件一起保存。服務(wù)啟動時加載模型和管線推理時先走變換函數(shù)再喂給模型。偽代碼如下import os import pickle import numpy as np from fastapi import FastAPI from pydantic import BaseModel app FastAPI() # 啟動時加載整體Pipeline with open(os.getenv(MODEL_PATH, /models/pipeline_and_model.pkl), rb) as f: pipeline pickle.load(f) class PredictRequest(BaseModel): user_id: str features: list app.post(/predict) def predict(req: PredictRequest): # 做數(shù)據(jù)校驗(yàn)和特征補(bǔ)全 x np.array(req.features).reshape(1, -1) prob pipeline.predict_proba(x)[0][1] return {user_id: req.user_id, click_prob: round(float(prob), 6)}這段代碼雖然小但避開了幾個大坑模型和特征轉(zhuǎn)換打包在同一個pickle文件里線上和訓(xùn)練時用的是完全一致的邏輯請求里的features由上游按約定順序傳入模型API不負(fù)責(zé)解析復(fù)雜的業(yè)務(wù)字段進(jìn)一步還能通過gRPC或者ONNX Runtime來降低預(yù)測延遲初期先不用強(qiáng)求。服務(wù)寫好后千萬不能不壓測就直接上線。我用Locust做簡單的并發(fā)請求測試服務(wù)的吞吐量和P99延遲。注意一個細(xì)節(jié)很多人的模型推理時間看起來不慢但整個HTTP服務(wù)延遲高是因?yàn)檎埱罄飵Я舜蠖蜫SON、做了不必要的日志打印、或者模型對象沒預(yù)熱。有一次我發(fā)現(xiàn)首請求延遲高達(dá)2秒原因就是模型在進(jìn)程啟動后第一次預(yù)測時才進(jìn)行XGBoost的線程池初始化。解決方案很簡單加載完模型后先用一條假數(shù)據(jù)“預(yù)熱”一次讓底層資源就緒。再談容器化部署。Docker是線上部署繞不開的一環(huán)但不要只是在鏡像里裝一個Python解釋器和依賴庫。生產(chǎn)鏡像必須做到無入侵、無獨(dú)立寫權(quán)限、時區(qū)正確、日志輸出到標(biāo)準(zhǔn)輸出。我踩過一個很不值錢的坑Dockerfile里忘了設(shè)置時區(qū)導(dǎo)致線上服務(wù)出的時間戳全部是UTC業(yè)務(wù)查對賬的時候數(shù)據(jù)差了8小時被運(yùn)維同學(xué)噴了一整天。這里給出一個我實(shí)際使用的小型API鏡像Dockerfile片段它不復(fù)雜但每個指令都針對一個真實(shí)事故FROM python:3.10-slim ENV PYTHONUNBUFFERED1 \ PYTHONDONTWRITEBYTECODE1 \ TZAsia/Shanghai WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY ./app /app/app # 使用非root用戶運(yùn)行降低安全風(fēng)險(xiǎn) RUN useradd --create-home --shell /bin/bash appuser USER appuser EXPOSE 8080 CMD [uvicorn, app.main:app, --host, 0.0.0.0, --port, 8080]在部署這一節(jié)最后必須強(qiáng)調(diào)一個理念部署不等于上線上線不等于完成。真正嚴(yán)謹(jǐn)?shù)牧鞒淌恰安渴?→ 灰度 → 切量 → 監(jiān)控”。我經(jīng)歷過一次事故模型在離線驗(yàn)證集上F1很高灰度給1%流量時看著也正常等慢慢放到30%后才發(fā)現(xiàn)特定用戶群的預(yù)測結(jié)果異常。原因是灰度樣本分布存在偏差前20%流量恰好掩蓋了某些用戶特征。從那以后我再也不信“放量穩(wěn)步提升”這句話沒有監(jiān)控兜底。5. 模型上線后的維護(hù)與演進(jìn)建立持續(xù)監(jiān)控和快速迭代機(jī)制模型部署上去這塊工作就結(jié)束了嗎遠(yuǎn)遠(yuǎn)沒有?,F(xiàn)實(shí)中模型上線后基本就進(jìn)入了一個不斷壞死的過程。數(shù)據(jù)變了、用戶行為變了、外部環(huán)境變了模型預(yù)測的準(zhǔn)確性就會逐漸下降。AI工程中很多團(tuán)隊(duì)都不重視這部分導(dǎo)致業(yè)務(wù)初期效果好幾個月后莫名其妙變差還找不到原因。我做維護(hù)介入的第一件事就是給模型建立監(jiān)控面板。監(jiān)控指標(biāo)分兩類一類是無法避免的技術(shù)指標(biāo)服務(wù)本身的QPS、延遲、報(bào)錯率、超時率。另一類是模型相關(guān)的業(yè)務(wù)指標(biāo)預(yù)測分?jǐn)?shù)分布、平均值、方差、特征覆蓋率、輸出結(jié)果與正負(fù)樣本的比例。這里有個非常實(shí)用的技巧模型分?jǐn)?shù)分布漂移往往比真實(shí)業(yè)務(wù)指標(biāo)下降更早暴露問題。比如一個用戶點(diǎn)擊率預(yù)測模型正常情況下預(yù)測概率均值在0.3到0.4之間某天突然變成0.1即使目前線上業(yè)務(wù)指標(biāo)還沒變化也要引起警惕因?yàn)槟P涂赡芤呀?jīng)在面對分布完全不同的數(shù)據(jù)了。我在監(jiān)控面板里專門畫了“預(yù)測分?jǐn)?shù)分布日環(huán)比變化”和“特征缺失率趨勢”兩個圖表故障發(fā)現(xiàn)速度比只看業(yè)務(wù)KPI快得多。用于監(jiān)控模型是否衰減的另一個手段是留存樣本回放。定期抽樣一部分線上真實(shí)請求樣本保存下來注意脫敏和合規(guī)然后離線用當(dāng)前最新模型和歷史模型同時進(jìn)行預(yù)測對比兩者在留存樣本上的分?jǐn)?shù)變化。這相當(dāng)于給模型做了一個“歷史考卷”可以量化模型衰減的幅度。當(dāng)發(fā)現(xiàn)模型衰減后就要啟動新一輪的迭代。這里最忌諱的是把線上模型直接拿下來重新訓(xùn)練然后一把梭替換。正確做法是建立模型版本管理機(jī)制線上有一個穩(wěn)定的生產(chǎn)版本同時有一個正在訓(xùn)練的候選版本。候選版本在離線評估和影子模式下表現(xiàn)穩(wěn)定后才進(jìn)入灰度發(fā)布。所謂影子模式Shadow Mode就是讓新模型上線后和舊模型同步接收真實(shí)請求但新模型的結(jié)果僅記錄不外發(fā)。跑一段時間后用真實(shí)的線上數(shù)據(jù)對比新舊模型的表現(xiàn)這個環(huán)節(jié)非常能說明問題。我見過不少離線指標(biāo)提升很猛的模型在影子模式下被現(xiàn)實(shí)教育得老老實(shí)實(shí)。版本管理上我建議每個模型都遵循一套命名和元信息規(guī)范否則時間長了根本分不清歷史模型的含義。我的規(guī)范是“模型類型_業(yè)務(wù)場景_序號_日期”比如“xgboost_ctr_v3_20240120”表示2024年1月20日訓(xùn)練的第3版CTR預(yù)測模型。同時每版模型在倉庫里記錄四個必填信息訓(xùn)練代碼commit、訓(xùn)練數(shù)據(jù)集hash、訓(xùn)練配置、評估報(bào)告。這套規(guī)范和前面說的實(shí)驗(yàn)追蹤是一脈相承的。一個很多人忽視的維護(hù)點(diǎn)是特征對齊。訓(xùn)練時的特征構(gòu)造代碼和線上推理時的特征構(gòu)造代碼如果分別維護(hù)在兩處幾乎一定會因?yàn)椤案牧艘恍袇s忘了改另一邊”而漂移。我在工具鏈上強(qiáng)制要求訓(xùn)練特征和線上特征必須復(fù)用一個特征函數(shù)庫新特征上線前必須跑一遍“訓(xùn)練時同一份樣本的推理分?jǐn)?shù)一致性校驗(yàn)”。這個校驗(yàn)不復(fù)雜就是對同一批歷史數(shù)據(jù)用訓(xùn)練時的代碼構(gòu)造特征再用線上的代碼構(gòu)造特征計(jì)算兩組特征的最大差異。如果差異超過閾值堅(jiān)決不讓上線。到了這個階段你會發(fā)現(xiàn)從零開始的AI工程逐步形成了一個良性循環(huán)穩(wěn)定的腳手架幫助你高效實(shí)驗(yàn)實(shí)時的監(jiān)控讓你了解線上模型狀態(tài)自動化的指標(biāo)評估幫你作出決策安全的版本管理使你可以快速迭代。這是一條沒有終點(diǎn)的路但走通它你就不再是“會用框架的人”而是真正能夠駕馭AI系統(tǒng)的工程師。最后分享一條私人經(jīng)驗(yàn)不要給自己規(guī)定“必須學(xué)完什么才能開始”。我學(xué)習(xí)AI工程的過程極其雜亂一開始連Linux常用命令都生疏就敢去改服務(wù)端推理邏輯結(jié)果被進(jìn)程崩潰按在地上摩擦了幾次才回頭老老實(shí)實(shí)補(bǔ)基礎(chǔ)。如果你也打算從零開始我建議你直接選一個真實(shí)業(yè)務(wù)場景哪怕只是做一個“商品評論情感分析”小程序然后順著這條鏈路往下走數(shù)據(jù)處理→模型訓(xùn)練→構(gòu)建API→部署上線→監(jiān)控日志。走完這一趟你踩下的每一個坑都會成為你和別人介紹AI工程時最生動的素材。