踐指南與踩坑記錄)
老實(shí)說《ai-engineering-from-scratch》這個(gè)標(biāo)題看起來像是一個(gè)短期突擊計(jì)劃但真正把它做完之后我覺得它更像一面鏡子——照出一個(gè)新手在AI工程這條路上一路踩坑、填坑、重新爬起來的全過程。過去大半年我基本就是這個(gè)狀態(tài)零基礎(chǔ)起步不靠現(xiàn)成的高層封裝一步步把數(shù)據(jù)、模型、訓(xùn)練、評估、部署、監(jiān)控這些環(huán)節(jié)整個(gè)過了一遍?,F(xiàn)在回頭看最值錢的東西反而不是跑了多少模型而是那些失敗和調(diào)試經(jīng)歷幫我建立的底層判斷力。這篇文章我盡量把整個(gè)過程還原出來包括思路、實(shí)操、踩過的坑和沉淀下來的方法希望能給也想走這條路的人一些參考。這篇文章適合誰如果你正處在裝好環(huán)境但不知道下一步干什么的階段或者已經(jīng)能跑通demo但總覺得理解很淺那這篇文章應(yīng)該對你有用。我會從AI工程到底是什么講起再拆解具體的實(shí)操路線最后把我遇到的典型問題和解決方案整理成速查表。不繞彎子都是實(shí)際跑過的經(jīng)驗(yàn)。1. 先搞清楚AI工程到底在做什么很多人提起AI工程第一反應(yīng)就是用Python調(diào)一下模型接口。這確實(shí)是最外層的形態(tài)但工程實(shí)踐遠(yuǎn)不止這一步。我得先把這個(gè)概念剝開看它底下到底壓著哪些東西因?yàn)槔斫獠粚竺嫠械膶W(xué)習(xí)路徑都會跟著歪掉。1.1 AI工程不是調(diào)接口那么簡單如果只調(diào)用現(xiàn)成API你實(shí)際接觸的不過是AI系統(tǒng)的最后一層。真正的AI工程至少包含數(shù)據(jù)采集與清洗、特征處理、模型選型、訓(xùn)練調(diào)優(yōu)、推理優(yōu)化、服務(wù)部署、監(jiān)控告警、迭代更新這幾個(gè)環(huán)節(jié)。每一個(gè)環(huán)節(jié)單獨(dú)拿出來都可以寫一本書但工程實(shí)踐要求的是把它們串成一個(gè)閉環(huán)。我從零開始折騰第一個(gè)完整項(xiàng)目時(shí)才發(fā)現(xiàn)一個(gè)殘酷現(xiàn)實(shí)模型訓(xùn)練只占整個(gè)時(shí)間的大概三成剩下的時(shí)間幾乎都花在數(shù)據(jù)處理、環(huán)境配置、調(diào)試問題和服務(wù)化封裝上。提示這個(gè)比例不是隨口說的是我?guī)讉€(gè)項(xiàng)目實(shí)測下來的平均結(jié)果。如果你也在AI工程入門建議先做好心理預(yù)期——不是只有訓(xùn)練才叫AI工程前面后面那些臟活累活才是工程的主體。我把AI工程拆成四個(gè)輪子數(shù)據(jù)、模型、算力、評估。數(shù)據(jù)對應(yīng)輸入質(zhì)量模型對應(yīng)算法能力算力對應(yīng)資源邊界評估對應(yīng)質(zhì)量底線。任何一個(gè)輪子扁平了整個(gè)系統(tǒng)就跑不順暢。新手最容易犯的錯(cuò)就是把所有精力押在模型上結(jié)果數(shù)據(jù)一團(tuán)糟、評估靠肉眼最后模型上線了根本不敢用。1.2 為什么強(qiáng)調(diào)from scratchfrom scratch不是要求你從反向傳播手寫一個(gè)Transformer才算數(shù)——當(dāng)然如果你想深挖底層手寫是很好的練習(xí)——但它確實(shí)強(qiáng)調(diào)一個(gè)核心態(tài)度不要只滿足于能用要明白為什么這么用。舉個(gè)例子。用Hugging Face的pipeline接口加載一個(gè)BERT模型做情感分類三行代碼就跑通了。但如果你的任務(wù)不是通用的情感分類而是某個(gè)特定行業(yè)里很冷門的文本判斷任務(wù)你會發(fā)現(xiàn)預(yù)訓(xùn)練模型直接遷移的效果常常不理想。這時(shí)候如果你懂?dāng)?shù)據(jù)采樣、tokenizer詞表擴(kuò)展、微調(diào)策略這幾個(gè)底層環(huán)節(jié)你就知道問題出在哪詞表里沒有領(lǐng)域詞匯、數(shù)據(jù)分布和預(yù)訓(xùn)練語料差異過大、超參數(shù)沒有適配任務(wù)規(guī)模。這就是from scratch的價(jià)值——它培養(yǎng)的是排查鏈路問題的能力而不是復(fù)制Demo的能力。我自己實(shí)踐下來如果只看教程跟著跑確實(shí)很快但只要環(huán)境和數(shù)據(jù)稍作變化馬上就會卡住。而一旦卡住才是學(xué)習(xí)真正開始的地方。2. 從零開始的第一步環(huán)境與工具鏈搭建標(biāo)題里既然寫了from scratch那環(huán)境搭建自然是繞不開的苦功。這一節(jié)我把我的搭建過程和遇到的取舍寫清楚特別是那些別人很少提但很重要的細(xì)節(jié)。2.1 硬件配置與開發(fā)環(huán)境的取舍我入手時(shí)沒有太多預(yù)算手頭只有一臺普通游戲本CPU是i7-12700HGPU是RTX 3060 Laptop6GB顯存內(nèi)存32GB。在此之前我一度覺得沒A100就學(xué)不了AI但事實(shí)是6GB顯存在入門階段完全夠用前提是你得知道怎么在資源邊界內(nèi)工作。如果你有獨(dú)立顯卡哪怕顯存只有4GB也建議優(yōu)先把CUDA環(huán)境配好因?yàn)楹芏嗾{(diào)試問題只有在真實(shí)GPU上跑過才會理解。如果是純CPU環(huán)境照樣能學(xué)但推理和訓(xùn)練的速度會慢到令人崩潰所以我建議至少有一塊支持CUDA的NVIDIA顯卡顯存6GB以上是舒適區(qū)。開發(fā)環(huán)境方面我強(qiáng)烈建議用LinuxUbuntu 22.04 LTS原因不是Windows跑不了而是大量模型倉庫、部署腳本和運(yùn)維工具都是優(yōu)先支持Linux的。如果你只有Windows機(jī)器也不用急著裝雙系統(tǒng)Windows Subsystem for LinuxWSL2是一個(gè)很實(shí)用的過渡方案CUDA支持也成熟了。我是直接在Windows上用的WSL2日常開發(fā)體驗(yàn)和Linux基本一致。2.2 版本匹配問題Python、CUDA、PyTorch三方博弈整個(gè)入門過程中最折磨我的不是模型不是算法是版本匹配。PyTorch、CUDA、Python三者的對應(yīng)關(guān)系非常嚴(yán)格。裝錯(cuò)一版要么檢測不到GPU要么運(yùn)行時(shí)報(bào)錯(cuò)要么性能莫名其妙下降。我的建議是不要追求最新要追求穩(wěn)定組合。我目前在用的一個(gè)穩(wěn)定組合供參考組件版本選擇說明Ubuntu22.04 LTS長期支持官方文檔示例最多的版本Python3.10絕大多數(shù)庫兼容性最好的版本CUDA11.8或12.1看PyTorch官方對應(yīng)表PyTorch2.x穩(wěn)定版安裝時(shí)用官方命令生成器選對應(yīng)版本cuDNN與CUDA配套用pip安裝時(shí)一般自動(dòng)處理我踩過最慘的一次坑是直接把CUDA升級到12.4結(jié)果項(xiàng)目里一個(gè)老版本的TensorFlow直接無法調(diào)用GPU最后只能花時(shí)間重建環(huán)境。從那以后我立了一個(gè)規(guī)矩——任何大版本升級前先看依賴庫的官方兼容性聲明再決定動(dòng)還是不動(dòng)。還有一個(gè)實(shí)用習(xí)慣每個(gè)項(xiàng)目開一個(gè)獨(dú)立的Python虛擬環(huán)境。我用的是conda因?yàn)樗诠芾鞵ython版本和CUDA依賴方面更順手。每開一個(gè)項(xiàng)目第一件事就是conda create -n project_name python3.10然后在虛擬環(huán)境里裝依賴。這樣項(xiàng)目之間互不污染即使某個(gè)環(huán)境被我搞壞了也不會影響其他項(xiàng)目。3. 從零到第一個(gè)AI模型完整實(shí)操路徑環(huán)境準(zhǔn)備好以后就可以開始正經(jīng)的項(xiàng)目實(shí)操。我把路徑拆成三個(gè)階段跑通推理、訓(xùn)練小模型、服務(wù)化封裝。每階段都卡過也都有值得說的細(xì)節(jié)。3.1 第一步跑通一個(gè)開源模型的推理我選的首個(gè)模型是bert-base-chinese——一個(gè)中文預(yù)訓(xùn)練模型用途是文本分類。選擇它的理由很樸素體量小約110M參數(shù)推理快能在CPU上跑且中文生態(tài)資料多出了問題好排查。跑通推理之前先要理解tokenizer和模型輸入的套路。文本不能直接以字符串形式喂給模型必須經(jīng)過tokenizer轉(zhuǎn)成張量。這個(gè)轉(zhuǎn)換過程包含分詞、切subword、加special token、截?cái)嗷騪adding。很多新手上來就報(bào)錯(cuò)dimension mismatch多半就是沒搞明白padding和truncation的作用。我的實(shí)操流程是用Hugging Face加載模型和tokenizer對輸入文本做tokenization得到input_ids、attention_mask把張量喂給模型拿到logits對logits做softmax得到概率分布按索引取標(biāo)簽代碼如下from transformers import AutoTokenizer, AutoModelForSequenceClassification import torch model_name bert-base-chinese tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModelForSequenceClassification.from_pretrained(model_name, num_labels2) text 這家餐廳的菜很好吃但服務(wù)態(tài)度一般。 inputs tokenizer(text, truncationTrue, paddingTrue, return_tensorspt) with torch.no_grad(): outputs model(**inputs) logits outputs.logits probs torch.softmax(logits, dim-1) print(probs)看起來簡單但這一步卡住我的點(diǎn)在于很多人會忽略return_tensorspt這個(gè)參數(shù)。如果不加返回的是Python列表沒法直接喂給PyTorch模型。這種細(xì)節(jié)教程里不一定提但實(shí)際跑的時(shí)候都會遇到。跑通這一步之后我對模型推理建立了完整的直覺模型不神秘它就是一個(gè)接收張量、輸出張量的函數(shù)tokenizer是文本與張量之間的橋梁。3.2 第二步自己訓(xùn)練一個(gè)文本分類小模型跑通別人的模型只是熱身真正價(jià)值出現(xiàn)在自己動(dòng)手微調(diào)模型。我做的第一個(gè)訓(xùn)練項(xiàng)目是一個(gè)酒店評論情感分類任務(wù)正負(fù)樣本各兩千條。數(shù)據(jù)是從一個(gè)公開的評論集里整理出來的先做了去重、清理特殊字符、統(tǒng)一格式然后按7:2:1切分為訓(xùn)練集、驗(yàn)證集、測試集。訓(xùn)練代碼的核心骨架from datasets import Dataset from transformers import TrainingArguments, Trainer train_dataset Dataset.from_pandas(train_df) val_dataset Dataset.from_pandas(val_df) training_args TrainingArguments( output_dir./results, evaluation_strategyepoch, save_strategyepoch, learning_rate2e-5, per_device_train_batch_size16, per_device_eval_batch_size32, num_train_epochs3, weight_decay0.01, logging_dir./logs, ) trainer Trainer( modelmodel, argstraining_args, train_datasettrain_dataset, eval_datasetval_dataset, ) trainer.train()這里有個(gè)關(guān)鍵問題learning_rate2e-5這個(gè)值不是隨便拍的。微調(diào)預(yù)訓(xùn)練模型時(shí)學(xué)習(xí)率通常要比從頭訓(xùn)練小一個(gè)量級因?yàn)轭A(yù)訓(xùn)練權(quán)重已經(jīng)學(xué)到了通用特征學(xué)率太大會導(dǎo)致災(zāi)難性遺忘——模型把之前學(xué)到的通用知識全忘掉。我一開始試過1e-4明顯看到驗(yàn)證集loss劇烈波動(dòng)改回2e-5以后才穩(wěn)定下降。另一個(gè)容易被忽略的參數(shù)是evaluation_strategyepoch它代表每個(gè)訓(xùn)練epoch結(jié)束后用驗(yàn)證集評估一次。千萬不要只在所有訓(xùn)練結(jié)束后評估一次那樣中間出現(xiàn)的問題沒法及時(shí)發(fā)現(xiàn)。我后來甚至把日志頻率調(diào)到每一步都記錄loss可以實(shí)時(shí)觀察訓(xùn)練狀態(tài)。訓(xùn)練結(jié)束以后我做了兩件新手常忽略的事用專門的測試集做最終評估以及把模型保存到本地并用它對真實(shí)樣本做推理驗(yàn)證。測試集的作用是模擬模型在沒見過的數(shù)據(jù)上的表現(xiàn)這才是真實(shí)場景下的效果而不是訓(xùn)練集上的自我感覺良好。3.3 第三步把模型封裝成一個(gè)可用服務(wù)訓(xùn)練完模型之后下一步是部署。如果不做這一步模型永遠(yuǎn)只是筆記本里的一個(gè)產(chǎn)物不算一個(gè)工程。我用FastAPI封裝成了RESTful接口代碼很簡單from fastapi import FastAPI, Request from pydantic import BaseModel from transformers import pipeline app FastAPI() classifier pipeline(text-classification, model./results/checkpoint-xxx) class Item(BaseModel): text: str app.post(/predict) async def predict(item: Item): result classifier(item.text)[0] return {label: result[label], score: result[score]}這里最需要注意的是模型加載時(shí)機(jī)。如果每次請求都加載一次模型服務(wù)和死掉差不多。正確的做法是在服務(wù)啟動(dòng)時(shí)把模型加載進(jìn)內(nèi)存然后接口只做推理。這個(gè)細(xì)節(jié)雖然小但我在頭幾次部署時(shí)確實(shí)犯過——在接口函數(shù)里加載模型結(jié)果第一個(gè)請求花了十幾秒后面的請求報(bào)告內(nèi)存一直在漲。服務(wù)化還有一個(gè)現(xiàn)實(shí)問題并發(fā)。如果只處理單條請求用上面的寫法完全夠用。但一旦有并發(fā)請求就需要考慮批量推理、異步任務(wù)隊(duì)列、負(fù)載均衡。入門階段建議先把單服務(wù)跑通理解整個(gè)調(diào)用鏈路再考慮更復(fù)雜的架構(gòu)。4. 從零學(xué)習(xí)中最容易踩的坑承諾一下這一節(jié)的東西幾乎都是我從實(shí)際報(bào)錯(cuò)和反復(fù)排查里攢下來的比看十篇教程都有用。我按出現(xiàn)頻率排了序。4.1 版本地獄庫、驅(qū)動(dòng)、系統(tǒng)的三角關(guān)系這個(gè)問題幾乎人人會碰。我的經(jīng)歷是這樣的某次升級依賴之后PyTorch開始報(bào)CUDA error: no kernel image is available for execution on the device查了半天發(fā)現(xiàn)是CUDA版本和顯卡驅(qū)動(dòng)不匹配。報(bào)錯(cuò)信息大概率原因解決方向CUDA error: no kernel image...CUDA版本和驅(qū)動(dòng)不匹配查驅(qū)動(dòng)支持的CUDA版本重裝對應(yīng)CUDA或PyTorchlibcudnn.so.8: cannot open shared object filecuDNN缺失或版本不對在conda環(huán)境里安裝匹配的cuDNNRuntimeError: CUDA out of memory顯存不足減小batch size、用梯度累積、換半精度Expected all tensors to be on the same device張量在CPU/GPU混用顯式調(diào)用.to(device)統(tǒng)一設(shè)備ImportError: cannot import name x from transformerstransformers版本過舊升級或按官方文檔回退到指定版本建議所有初學(xué)者建立起一個(gè)意識每一次環(huán)境變動(dòng)都做記錄。我后來把所有依賴寫進(jìn)requirements.txt而且固定版本號不寫范圍版本這樣環(huán)境才能可復(fù)現(xiàn)。尤其是多人協(xié)作或者隔一段時(shí)間重新跑項(xiàng)目時(shí)這個(gè)習(xí)慣能救人一命。4.2 數(shù)據(jù)問題比模型問題更致命而且更隱蔽我在訓(xùn)練第一個(gè)模型時(shí)犯過一個(gè)很經(jīng)典的錯(cuò)誤數(shù)據(jù)沒做shuffle。數(shù)據(jù)里前2000條都是正樣本后2000條都是負(fù)樣本訓(xùn)練時(shí)模型先大量看到正樣本loss震蕩很厲害最后模型嚴(yán)重偏向預(yù)測正類。后來我總結(jié)出一個(gè)規(guī)律數(shù)據(jù)層面出問題往往模型輸出的整體表現(xiàn)看起來還行但細(xì)看各類別的指標(biāo)就會露餡。比如準(zhǔn)確率看著有85%一看precision和recall負(fù)類的recall只有30%——這樣的模型上線之后就只能不停地誤判。處理數(shù)據(jù)的基本流程我整理為五步去重去掉完全重復(fù)或近似重復(fù)的文本清洗去掉HTML標(biāo)簽、噪聲符號、亂碼字符標(biāo)準(zhǔn)化統(tǒng)一大小寫中文不需要、統(tǒng)一全半角符號標(biāo)簽校驗(yàn)檢查標(biāo)簽分布是否有錯(cuò)標(biāo)漏標(biāo)劃分按類別分層采樣確保訓(xùn)練、驗(yàn)證、測試集中各類別比例一致這五步看著簡單實(shí)際做下來每一步都需要親手寫代碼檢查我甚至專門寫了幾個(gè)小函數(shù)來統(tǒng)計(jì)標(biāo)簽分布、文本長度分布、重復(fù)率確保數(shù)據(jù)的每個(gè)維度都心里有數(shù)。4.3 顯存不夠時(shí)的幾種現(xiàn)實(shí)解法6GB顯存做小模型微調(diào)還算夠用但稍微模型大一點(diǎn)就尷尬了。我剛?cè)腴T時(shí)想試試更大的模型結(jié)果直接被OOMOut Of Memory勸退。后來學(xué)會了幾招現(xiàn)在整理出來方法思路代價(jià)減小batch size一次喂給模型更少樣本訓(xùn)練變慢需配合梯度累積梯度累積每N步更新一次參數(shù)訓(xùn)練時(shí)間變長混合精度訓(xùn)練用FP16代替FP32精度略降但訓(xùn)練加速模型量化用INT8/INT4代替FP16精度損失需驗(yàn)證清理緩存torch.cuda.empty_cache()在中間步驟清理治標(biāo)不治本我用的最順手的方案是batch size設(shè)為8梯度累積步數(shù)設(shè)為2這樣等效到16的批量大小顯存卻不爆。如果你也想優(yōu)化顯存第一條先改batch size很多情況下就能解決。注意顯存不夠不要急著買新顯卡先把batch size降下來試試。很多人習(xí)慣網(wǎng)上抄一個(gè)batch size值就直接跑跑掛了還以為是模型問題。實(shí)際上batch size是訓(xùn)練里最不敏感的參數(shù)量級只要不是太大了對效果影響有限。5. 我沉淀下來的幾條經(jīng)驗(yàn)法則最后分享幾個(gè)經(jīng)過反復(fù)驗(yàn)證的經(jīng)驗(yàn)不是套話是每個(gè)決策背后都有實(shí)際教訓(xùn)才總結(jié)出來的。5.1 用項(xiàng)目驅(qū)動(dòng)代替漫無目的地刷課說真的AI工程這個(gè)方向信息量太密如果沒有明確目標(biāo)很容易陷入看視頻一時(shí)爽合上電腦全忘光的假學(xué)習(xí)循環(huán)。我的建議是一開始就定一個(gè)可以交付的項(xiàng)目哪怕是做一個(gè)小模型走完整條鏈路——數(shù)據(jù)、訓(xùn)練、評估、部署——做完一遍之后再去補(bǔ)理論你會發(fā)現(xiàn)自己吸收知識的效率完全不同。我見過不少朋友花兩三個(gè)月看深度學(xué)習(xí)理論課代碼一行沒寫最后讓他跑個(gè)模型完全懵。反過來先動(dòng)手把流程跑通再回頭補(bǔ)為什么梯度下降收斂為什么激活函數(shù)要選ReLU這些理論理解深度完全不一樣因?yàn)槟阌姓鎸?shí)體驗(yàn)打底了。第一個(gè)項(xiàng)目不要選太復(fù)雜。我最推薦的入門項(xiàng)目是中文文本分類數(shù)據(jù)好找模型不重鏈路完整踩坑密度剛剛好。圖像方向可以選CIFAR-10做分類但環(huán)境依賴要多一些上手成本更高。如果能堅(jiān)持做完一個(gè)文本分類項(xiàng)目AI工程的基礎(chǔ)框架就有了。5.2 記錄實(shí)驗(yàn)日志養(yǎng)成工程習(xí)慣剛開始訓(xùn)練模型時(shí)我試了很多組超參數(shù)但全憑印象過一周之后根本想不起來哪組參數(shù)好、為什么好。吃了不少虧之后我建了一個(gè)實(shí)驗(yàn)記錄表每跑一次實(shí)驗(yàn)記一行實(shí)驗(yàn)編號與目標(biāo)數(shù)據(jù)集版本與切分方式模型結(jié)構(gòu)與參數(shù)數(shù)量超參數(shù)配置學(xué)習(xí)率、batch size、epochs等訓(xùn)練結(jié)果loss曲線、評估指標(biāo)復(fù)現(xiàn)命令或代碼位置備注當(dāng)時(shí)的異常表現(xiàn)偶然發(fā)現(xiàn)這個(gè)習(xí)慣的收益是滾雪球式的。三個(gè)月以后我翻記錄還能準(zhǔn)確知道當(dāng)時(shí)某個(gè)實(shí)驗(yàn)為什么失敗哪些參數(shù)組合值得延續(xù)。這比任何AI輔助調(diào)參工具都更貼近工程實(shí)踐本質(zhì)——可復(fù)現(xiàn)、可追溯、可信賴。5.3 學(xué)習(xí)節(jié)奏與心態(tài)建議AI工程是一條很長很長的路入門期45天到90天不等取決于基礎(chǔ)和時(shí)間投入。如果之前寫過Python過程會順一大截如果完全零編程基礎(chǔ)建議先花一兩周熟悉Python語法和基礎(chǔ)庫再正式進(jìn)入AI工程環(huán)節(jié)。心態(tài)上最重要的一個(gè)建議報(bào)錯(cuò)不是災(zāi)難而是線索。我最早遇到報(bào)錯(cuò)就想放棄后來逐漸把它當(dāng)成排查游戲。每個(gè)報(bào)錯(cuò)都在告訴你系統(tǒng)內(nèi)部某個(gè)環(huán)節(jié)的狀態(tài)順著報(bào)錯(cuò)追下去你會對框架的理解越來越深。到現(xiàn)在遇見沒見過的報(bào)錯(cuò)我甚至有點(diǎn)興奮——因?yàn)槲抑澜鉀Q它之后認(rèn)知又會提升一截。最后一個(gè)小建議多在社區(qū)寫分享、看別人的提問。我很多經(jīng)驗(yàn)其實(shí)來源于幫別人排查問題普適性極強(qiáng)的問題往往意味著很多人都會踩。試著答一道新區(qū)塊鏈跑出來的問題你就知道自己是否真正理解了一個(gè)知識點(diǎn)。從零開始這條路不短但它帶來的底層判斷力是不可替代的。前期慢一點(diǎn)沒關(guān)系持續(xù)跑總會到達(dá)自己最初想去的地方。