包搭建可運(yùn)行系統(tǒng)的完整指南)
1. 從零手搓AI工程為什么我不建議你直接調(diào)包第一次看到ai-engineering-from-scratch這個(gè)項(xiàng)目名的時(shí)候我腦子里蹦出來(lái)的畫面是一個(gè)人坐在黑漆漆的終端前拒絕所有現(xiàn)成的框架從矩陣乘法開始一行一行地把一個(gè)能跑起來(lái)的AI系統(tǒng)給搭出來(lái)。這個(gè)直覺(jué)基本是對(duì)的但它比“手寫一個(gè)神經(jīng)網(wǎng)絡(luò)”要寬得多。AI工程不等于模型訓(xùn)練它是一整條鏈路——數(shù)據(jù)怎么進(jìn)來(lái)、特征怎么處理、模型怎么選、推理怎么部署、服務(wù)怎么監(jiān)控、成本怎么壓下來(lái)。這個(gè)項(xiàng)目標(biāo)題里的 “from scratch”我理解成兩層意思一層是不依賴重型框架去理解底層原理另一層是從零搭建一套可運(yùn)行的工程骨架而不是停留在 notebook 里跑個(gè) demo。我做了十多年一線見過(guò)太多團(tuán)隊(duì)在“調(diào)包”這件事上翻車。不是調(diào)包不好而是當(dāng)你不知道包里面發(fā)生了什么出了問(wèn)題你連日志都看不懂。模型輸出突然變差你懷疑是數(shù)據(jù)漂移結(jié)果發(fā)現(xiàn)是預(yù)處理里某個(gè)歸一化參數(shù)寫反了線上延遲飆高你以為是模型太大結(jié)果發(fā)現(xiàn)是 batch 拼裝邏輯在某個(gè)邊界條件下退化成了一條一條推理。這些坑只有你親手從零搭過(guò)一遍才會(huì)有肌肉記憶。這篇東西適合誰(shuí)看如果你是剛?cè)胄?、只?huì)model.fit()的算法同學(xué)它能幫你把工程這條腿補(bǔ)上如果你是后端轉(zhuǎn)AI、被各種框架繞暈的工程師它能給你一條清晰的、不依賴魔法的主線如果你是帶團(tuán)隊(duì)的技術(shù)負(fù)責(zé)人它可以當(dāng)作一份“最小可用AI系統(tǒng)”的搭建清單用來(lái)對(duì)齊團(tuán)隊(duì)認(rèn)知。我不打算寫成教科書就按我自己搭這類系統(tǒng)的真實(shí)順序來(lái)講中間會(huì)穿插大量我踩過(guò)的坑和實(shí)測(cè)參數(shù)。2. 整體架構(gòu)設(shè)計(jì)先想清楚邊界再動(dòng)手寫代碼2.1 從零不等于從原始社會(huì)開始很多人對(duì) “from scratch” 有個(gè)誤解覺(jué)得必須連 NumPy 都不用純 Python 列表推導(dǎo)式算矩陣。我明確說(shuō)沒(méi)必要也不推薦。從零的核心是“你清楚每一層的職責(zé)和數(shù)據(jù)形態(tài)”而不是“拒絕一切工具”。NumPy 是數(shù)值計(jì)算的基礎(chǔ)設(shè)施用它不丟人真正要警惕的是那種一行pipeline.fit()把數(shù)據(jù)清洗、特征工程、模型訓(xùn)練全包了、你完全不知道中間發(fā)生了什么的黑盒。我的選型原則很簡(jiǎn)單底層數(shù)值用 NumPy數(shù)據(jù)處理用原生 Python Pandas 做輕量清洗模型部分手寫核心算子服務(wù)層用 FastAPI。為什么不用 PyTorch 或 TensorFlow因?yàn)橐坏┯昧四憔秃茈y忍住不去調(diào)nn.Linear而手寫一遍前向和反向傳播你對(duì)梯度、維度、初始化的理解會(huì)上一個(gè)臺(tái)階。等你手寫完再回去用框架你會(huì)發(fā)現(xiàn)你讀源碼的速度完全不一樣了。這里有個(gè)關(guān)鍵取舍手寫模型只適合中小規(guī)模、結(jié)構(gòu)清晰的場(chǎng)景。如果你要做的是十億參數(shù)的大模型那必須用分布式框架手寫不現(xiàn)實(shí)。所以這個(gè)項(xiàng)目的定位是“教學(xué) 小規(guī)模生產(chǎn)驗(yàn)證”不是“替代工業(yè)級(jí)訓(xùn)練框架”。想清楚這一點(diǎn)后面的技術(shù)選型就不會(huì)擰巴。2.2 分層架構(gòu)與數(shù)據(jù)流設(shè)計(jì)我習(xí)慣把整個(gè)系統(tǒng)切成四層每層只跟相鄰層打交道這樣出問(wèn)題的時(shí)候能快速定位是哪一層的鍋。層級(jí)職責(zé)典型技術(shù)出問(wèn)題時(shí)的表現(xiàn)數(shù)據(jù)層采集、清洗、切分、版本管理Pandas、Parquet、DVC訓(xùn)練指標(biāo)抖動(dòng)、特征分布異常特征層歸一化、編碼、特征交叉NumPy、sklearn預(yù)處理線上線下不一致、推理結(jié)果偏移模型層前向、損失、反向、優(yōu)化手寫NumPy損失不下降、梯度爆炸/消失服務(wù)層接口、批處理、監(jiān)控、日志FastAPI、Prometheus延遲高、超時(shí)、內(nèi)存泄漏數(shù)據(jù)流是這樣的原始數(shù)據(jù)先落盤成 Parquet比 CSV 快得多列式存儲(chǔ)對(duì)特征讀取友好然后經(jīng)過(guò)清洗腳本產(chǎn)出訓(xùn)練集和驗(yàn)證集特征層把原始字段轉(zhuǎn)成模型能吃的數(shù)值矩陣模型層訓(xùn)練并保存權(quán)重服務(wù)層加載權(quán)重對(duì)外提供推理。每一層的輸出都要落盤或打日志沒(méi)有落盤的中間產(chǎn)物等于沒(méi)有這是我在生產(chǎn)環(huán)境用血換來(lái)的教訓(xùn)。2.3 為什么堅(jiān)持“可復(fù)現(xiàn)”優(yōu)先于“高性能”新手搭系統(tǒng)最容易犯的錯(cuò)是一上來(lái)就追求 QPS 和低延遲結(jié)果代碼寫得極其復(fù)雜換個(gè)數(shù)據(jù)集就跑不起來(lái)。我的建議是第一階段只追求可復(fù)現(xiàn)。什么叫可復(fù)現(xiàn)同樣的數(shù)據(jù)、同樣的隨機(jī)種子、同樣的代碼跑出來(lái)的結(jié)果必須一模一樣。為此你要做三件事固定所有隨機(jī)源NumPy、Python random、甚至哈希種子把數(shù)據(jù)切分邏輯寫成確定性的把超參數(shù)全部外置成配置文件。我實(shí)測(cè)下來(lái)一個(gè)可復(fù)現(xiàn)的樸素實(shí)現(xiàn)哪怕推理延遲是優(yōu)化后的三倍它的價(jià)值也遠(yuǎn)高于一個(gè)跑得快但結(jié)果飄忽的版本。因?yàn)橹挥锌蓮?fù)現(xiàn)你才能做 A/B 對(duì)比才能定位是哪個(gè)改動(dòng)帶來(lái)了提升。等你把基線跑穩(wěn)了再去做向量化、批處理、緩存這些優(yōu)化順序不能反。3. 核心模塊拆解手寫模型到底要寫哪些東西3.1 數(shù)據(jù)預(yù)處理最臟最累但最不能省數(shù)據(jù)預(yù)處理這塊我見過(guò)太多人草草了事。真實(shí)數(shù)據(jù)里一定有缺失值、異常值、類型不一致、時(shí)間格式混亂。我的處理順序是先做數(shù)據(jù)探查統(tǒng)計(jì)每個(gè)字段的缺失率、唯一值數(shù)量、數(shù)值分布然后做清洗缺失率超過(guò) 60% 的字段直接丟棄數(shù)值字段用中位數(shù)填充比均值抗異常值類別字段用眾數(shù)或單獨(dú)標(biāo)記為 “unknown”最后做切分按時(shí)間切分而不是隨機(jī)切分如果你的場(chǎng)景有時(shí)序性。這里有個(gè)大坑歸一化參數(shù)必須從訓(xùn)練集計(jì)算然后應(yīng)用到驗(yàn)證集和測(cè)試集。我見過(guò)有人對(duì)整個(gè)數(shù)據(jù)集做歸一化再切分這會(huì)導(dǎo)致數(shù)據(jù)泄漏驗(yàn)證集指標(biāo)虛高上線后直接崩。正確做法是訓(xùn)練集算均值和標(biāo)準(zhǔn)差存下來(lái)推理時(shí)用同一套參數(shù)。這個(gè)參數(shù)文件要跟模型權(quán)重一起版本管理缺一不可。# 訓(xùn)練集計(jì)算歸一化參數(shù) mean X_train.mean(axis0) std X_train.std(axis0) 1e-8 # 防止除零 X_train_norm (X_train - mean) / std # 驗(yàn)證集和測(cè)試集用同一套參數(shù) X_val_norm (X_val - mean) / std # 保存參數(shù) np.savez(norm_params.npz, meanmean, stdstd)那個(gè)1e-8是防止某個(gè)特征方差為零導(dǎo)致除零這種細(xì)節(jié)不寫出來(lái)線上就會(huì)給你報(bào)一堆 nan。3.2 手寫前向傳播維度對(duì)齊是永恒的主題手寫前向傳播核心就三件事矩陣乘法、激活函數(shù)、維度對(duì)齊。我建議從最簡(jiǎn)單的全連接網(wǎng)絡(luò)開始兩層隱藏層足夠驗(yàn)證整條鏈路。權(quán)重初始化用 He 初始化針對(duì) ReLU公式是std sqrt(2 / fan_in)其中fan_in是輸入維度。為什么不用全零初始化因?yàn)槿銜?huì)讓所有神經(jīng)元對(duì)稱反向傳播時(shí)梯度一樣等于白搭。為什么不用過(guò)大的隨機(jī)值因?yàn)闀?huì)導(dǎo)致激活值飽和梯度消失。def init_weights(in_dim, out_dim): std np.sqrt(2.0 / in_dim) return np.random.randn(in_dim, out_dim) * std def forward(X, W1, b1, W2, b2, W3, b3): z1 X W1 b1 a1 np.maximum(0, z1) # ReLU z2 a1 W2 b2 a2 np.maximum(0, z2) z3 a2 W3 b3 return z3, (z1, a1, z2, a2)維度對(duì)齊這塊我的經(jīng)驗(yàn)是每寫一行就打印一次 shape。別嫌麻煩等你遇到(32, 10) (64, 10)這種報(bào)錯(cuò)的時(shí)候回頭查維度能查到你懷疑人生。我習(xí)慣在開發(fā)階段加一個(gè)assert檢查比如assert X.shape[1] W1.shape[0]上線前再把這些斷言去掉或改成日志。3.3 反向傳播鏈?zhǔn)椒▌t的工程化落地反向傳播是手寫模型里最容易寫錯(cuò)的部分。我的方法是先推導(dǎo)再編碼推導(dǎo)過(guò)程寫在注釋里。以交叉熵?fù)p失 Softmax 為例輸出層的梯度有一個(gè)非常優(yōu)雅的簡(jiǎn)化形式dz y_pred - y_true。這個(gè)結(jié)論能省掉一大堆求導(dǎo)但前提是你用的是 Softmax 交叉熵的組合。如果你換成別的損失函數(shù)這個(gè)簡(jiǎn)化就不成立了必須老老實(shí)實(shí)按鏈?zhǔn)椒▌t推。def backward(X, y_true, cache, weights): z1, a1, z2, a2 cache W1, W2, W3 weights m X.shape[0] # 輸出層梯度Softmax 交叉熵的簡(jiǎn)化形式 dz3 (softmax(z3) - y_true) / m dW3 a2.T dz3 db3 dz3.sum(axis0) # 隱藏層2 da2 dz3 W3.T dz2 da2 * (z2 0) # ReLU導(dǎo)數(shù) dW2 a1.T dz2 db2 dz2.sum(axis0) # 隱藏層1 da1 dz2 W2.T dz1 da1 * (z1 0) dW1 X.T dz1 db1 dz1.sum(axis0) return dW1, db1, dW2, db2, dW3, db3注意那個(gè)/ m是對(duì) batch 求平均這樣學(xué)習(xí)率不會(huì)隨 batch size 變化而需要重新調(diào)。ReLU 的導(dǎo)數(shù)(z 0)在 z0 處不可導(dǎo)工程上直接取 0 或 1 都行實(shí)測(cè)影響可以忽略。3.4 優(yōu)化器與訓(xùn)練循環(huán)學(xué)習(xí)率是最重要的超參數(shù)優(yōu)化器我建議從最樸素的 SGD 開始然后加 Momentum最后再上 Adam。為什么因?yàn)?SGD 能讓你直觀感受到學(xué)習(xí)率的影響而 Adam 的自適應(yīng)學(xué)習(xí)率會(huì)掩蓋很多問(wèn)題。學(xué)習(xí)率的選擇有個(gè)經(jīng)驗(yàn)公式先試1e-1、1e-2、1e-3、1e-4四個(gè)量級(jí)看損失下降曲線選那個(gè)下降最快又不震蕩的。我實(shí)測(cè)下來(lái)對(duì)于中小型全連接網(wǎng)絡(luò)1e-3配合 Adam 通常是個(gè)不錯(cuò)的起點(diǎn)。訓(xùn)練循環(huán)里必須有的東西訓(xùn)練損失、驗(yàn)證損失、驗(yàn)證指標(biāo)、早停機(jī)制。早停的 patience 我一般設(shè) 5 到 10 個(gè) epoch具體看數(shù)據(jù)量。數(shù)據(jù)量大就設(shè)小一點(diǎn)因?yàn)槊總€(gè) epoch 成本高數(shù)據(jù)量小就設(shè)大一點(diǎn)給它更多機(jī)會(huì)。還有一個(gè)細(xì)節(jié)每個(gè) epoch 結(jié)束后打亂訓(xùn)練數(shù)據(jù)但驗(yàn)證集不要打亂這樣驗(yàn)證指標(biāo)才可比。4. 工程化落地從能跑到能用還差十萬(wàn)八千里4.1 配置管理別把超參數(shù)寫死在代碼里我見過(guò)太多項(xiàng)目學(xué)習(xí)率、batch size、層數(shù)全寫在代碼里想改一個(gè)參數(shù)得改代碼、重新提交、重新部署。正確做法是用配置文件YAML 或 JSON代碼只讀配置。配置里至少包含數(shù)據(jù)路徑、模型結(jié)構(gòu)參數(shù)、訓(xùn)練超參數(shù)、服務(wù)端口、日志級(jí)別。這樣你換數(shù)據(jù)集、調(diào)參、部署到不同環(huán)境都只改配置不改代碼。# config.yaml data: train_path: data/train.parquet val_path: data/val.parquet batch_size: 64 model: hidden_dims: [128, 64] dropout: 0.2 training: lr: 0.001 epochs: 100 patience: 7 serving: host: 0.0.0.0 port: 8000配置管理還有個(gè)好處實(shí)驗(yàn)可追溯。每次訓(xùn)練把配置和對(duì)應(yīng)的指標(biāo)存到一張表里回頭分析“哪個(gè)配置效果好”的時(shí)候直接查表就行不用翻聊天記錄。4.2 模型持久化權(quán)重、參數(shù)、版本一個(gè)都不能少模型保存不是只存權(quán)重就完事了。我要求保存的東西包括模型權(quán)重、歸一化參數(shù)、特征字段列表、模型版本號(hào)、訓(xùn)練時(shí)的配置、訓(xùn)練日期。為什么因?yàn)橥评淼臅r(shí)候你需要知道輸入字段的順序、需要做同樣的歸一化、需要知道這個(gè)模型是什么時(shí)候訓(xùn)練的。少任何一樣線上都可能出問(wèn)題。我習(xí)慣用目錄來(lái)組織models/v1/weights.npz、models/v1/norm_params.npz、models/v1/feature_list.json、models/v1/config.yaml。服務(wù)啟動(dòng)時(shí)加載整個(gè)目錄版本號(hào)從目錄名讀。這樣回滾的時(shí)候直接切目錄就行干凈利落。4.3 服務(wù)層設(shè)計(jì)批處理與單條推理要兼顧服務(wù)層用 FastAPI 是個(gè)務(wù)實(shí)的選擇輕量、異步、自帶文檔。接口設(shè)計(jì)上我建議同時(shí)提供兩個(gè)端點(diǎn)/predict處理單條請(qǐng)求/predict_batch處理批量請(qǐng)求。為什么因?yàn)榫€上流量往往是混合的實(shí)時(shí)請(qǐng)求走單條離線補(bǔ)數(shù)據(jù)走批量。如果只做單條批量場(chǎng)景下網(wǎng)絡(luò)開銷會(huì)拖垮性能如果只做批量實(shí)時(shí)請(qǐng)求又沒(méi)法滿足。from fastapi import FastAPI import numpy as np app FastAPI() model load_model(models/v1) app.post(/predict) def predict(item: dict): x preprocess(item) # 用保存的歸一化參數(shù) logits model.forward(x) prob softmax(logits) return {label: int(np.argmax(prob)), confidence: float(np.max(prob))} app.post(/predict_batch) def predict_batch(items: list): X np.stack([preprocess(item) for item in items]) logits model.forward(X) probs softmax(logits) return {labels: np.argmax(probs, axis1).tolist()}批處理的時(shí)候注意內(nèi)存別一次塞太多。我一般限制 batch 上限為 256超過(guò)就分片處理。還有推理的時(shí)候記得關(guān)掉梯度計(jì)算雖然手寫 NumPy 沒(méi)有自動(dòng)求導(dǎo)但如果你用了框架torch.no_grad()是必須的。4.4 監(jiān)控與日志上線才是真正的開始服務(wù)上線只是開始監(jiān)控才是保證它活著的東西。我至少要監(jiān)控四個(gè)指標(biāo)請(qǐng)求量、延遲分布P50/P95/P99、錯(cuò)誤率、輸入特征分布。前三個(gè)是常規(guī)的第四個(gè)是AI系統(tǒng)特有的。為什么監(jiān)控輸入分布因?yàn)槟P蛯?duì)訓(xùn)練時(shí)沒(méi)見過(guò)的數(shù)據(jù)分布表現(xiàn)很差如果線上輸入分布突然偏移模型輸出會(huì)悄悄變差但你從延遲和錯(cuò)誤率上看不出來(lái)。日志方面我要求每次推理都記錄請(qǐng)求ID、輸入特征摘要脫敏后、輸出結(jié)果、耗時(shí)。這樣出問(wèn)題的時(shí)候能快速定位是哪個(gè)請(qǐng)求、什么輸入導(dǎo)致的。日志量大的話采樣記錄但錯(cuò)誤請(qǐng)求必須全量記錄。5. 常見問(wèn)題與排查技巧實(shí)錄5.1 損失不下降的排查順序損失不下降是新手最常遇到的問(wèn)題。我的排查順序是先看數(shù)據(jù)再看初始化再看學(xué)習(xí)率最后看梯度。數(shù)據(jù)方面檢查標(biāo)簽有沒(méi)有對(duì)齊、有沒(méi)有全零或全一的特征、類別是否極度不平衡。初始化方面檢查權(quán)重標(biāo)準(zhǔn)差是不是太大或太小。學(xué)習(xí)率方面從1e-4到1e-1掃一遍。梯度方面打印每一層的梯度范數(shù)如果某層梯度全是零說(shuō)明那層死了ReLU 的經(jīng)典問(wèn)題可以換 LeakyReLU 或調(diào)小學(xué)習(xí)率。現(xiàn)象可能原因排查方法解決損失不變學(xué)習(xí)率過(guò)小掃學(xué)習(xí)率調(diào)大損失震蕩學(xué)習(xí)率過(guò)大看損失曲線調(diào)小或加動(dòng)量損失變 nan梯度爆炸打印梯度范數(shù)梯度裁剪驗(yàn)證損失上升過(guò)擬合對(duì)比訓(xùn)練/驗(yàn)證曲線加正則、早停某層梯度全零神經(jīng)元死亡打印激活值換激活函數(shù)5.2 線上線下不一致的經(jīng)典原因線上線下不一致我總結(jié)下來(lái)就三個(gè)原因特征處理不一致、歸一化參數(shù)不一致、字段順序不一致。特征處理不一致比如訓(xùn)練時(shí)對(duì)缺失值填中位數(shù)線上填了零歸一化參數(shù)不一致比如訓(xùn)練用訓(xùn)練集均值線上用了全量均值字段順序不一致比如訓(xùn)練時(shí)字段是 [A, B, C]線上傳進(jìn)來(lái)是 [C, A, B]。這三個(gè)問(wèn)題只要你在服務(wù)層嚴(yán)格復(fù)用訓(xùn)練時(shí)的預(yù)處理代碼和參數(shù)文件就能避免。我的做法是把預(yù)處理邏輯封裝成一個(gè)類訓(xùn)練和推理共用同一個(gè)類。訓(xùn)練時(shí)fit計(jì)算參數(shù)并保存推理時(shí)load參數(shù)并transform。這樣代碼只有一份不存在兩邊邏輯漂移的可能。5.3 性能優(yōu)化的幾個(gè)實(shí)用手段性能優(yōu)化別一上來(lái)就搞復(fù)雜的。我實(shí)測(cè)有效的順序是先向量化再批處理再緩存最后才考慮模型壓縮。向量化就是把 Python 循環(huán)換成 NumPy 操作這一步通常能帶來(lái)十倍以上的提升。批處理是把多條請(qǐng)求合并成一次矩陣運(yùn)算提升吞吐。緩存是針對(duì)重復(fù)輸入如果線上有大量重復(fù)查詢加一層 LRU 緩存能顯著降低延遲。模型壓縮量化、剪枝是最后的手段因?yàn)樗鼤?huì)損失精度需要仔細(xì)評(píng)估。提示優(yōu)化之前一定要有基線數(shù)據(jù)不然你無(wú)法判斷優(yōu)化是否有效。我習(xí)慣用time.perf_counter()在關(guān)鍵路徑打點(diǎn)記錄每個(gè)階段的耗時(shí)這樣瓶頸在哪一目了然。5.4 我踩過(guò)的三個(gè)真實(shí)坑第一個(gè)坑隨機(jī)種子沒(méi)固定全。我只固定了 NumPy 的種子忘了 Python 內(nèi)置 random 的種子結(jié)果數(shù)據(jù)打亂順序每次不一樣實(shí)驗(yàn)無(wú)法復(fù)現(xiàn)。后來(lái)我寫了個(gè)set_seed函數(shù)把所有隨機(jī)源都固定一遍。第二個(gè)坑歸一化參數(shù)保存成了 Python list加載后變成了字符串。因?yàn)橛昧?JSON 保存浮點(diǎn)數(shù)精度丟失加上類型轉(zhuǎn)換導(dǎo)致推理結(jié)果和訓(xùn)練差了一點(diǎn)點(diǎn)。后來(lái)改用np.savez保存二進(jìn)制問(wèn)題消失。第三個(gè)坑服務(wù)層沒(méi)有限制請(qǐng)求體大小有人傳了一個(gè)超大 batch直接把內(nèi)存打滿服務(wù)掛了。后來(lái)加了請(qǐng)求體大小限制和 batch 上限并且在入口做了參數(shù)校驗(yàn)。6. 從零搭建的擴(kuò)展方向與個(gè)人體會(huì)這套從零搭起來(lái)的骨架跑通之后你會(huì)發(fā)現(xiàn)它像個(gè)樂(lè)高底座往上加?xùn)|西很自然。想加文本特征就在特征層加一個(gè)分詞和 embedding 模塊想加時(shí)序特征就在數(shù)據(jù)層加滑窗邏輯想加多模型融合就在服務(wù)層加一個(gè)路由層。因?yàn)槊恳粚勇氊?zé)清晰擴(kuò)展的時(shí)候不會(huì)牽一發(fā)動(dòng)全身。我個(gè)人在實(shí)際操作中的體會(huì)是從零搭一遍最大的收獲不是那個(gè)模型本身而是你對(duì)“數(shù)據(jù)怎么流動(dòng)”的直覺(jué)。以前調(diào)包的時(shí)候數(shù)據(jù)在框架里怎么走我是不關(guān)心的手寫一遍之后我知道每一步的輸入輸出是什么形狀、什么范圍、什么含義。這種直覺(jué)在你排查線上問(wèn)題、做架構(gòu)設(shè)計(jì)、跟別人討論方案的時(shí)候價(jià)值巨大。最后分享一個(gè)小技巧如果你覺(jué)得從零寫整個(gè)系統(tǒng)太重可以先從只手寫模型層開始數(shù)據(jù)層和服務(wù)層用現(xiàn)成工具。等你對(duì)模型層有感覺(jué)了再逐步往下替換。這樣學(xué)習(xí)曲線平緩也不容易半途而廢。這個(gè)項(xiàng)目后續(xù)還可以往分布式訓(xùn)練、模型版本灰度發(fā)布、特征存儲(chǔ)這些方向擴(kuò)展但那是另一個(gè)階段的事了先把單機(jī)版跑穩(wěn)再說(shuō)。