很久以前我在团队里带过一批实习生,大家简历上都写着"熟悉机器学习",但真正新项目立项以后,能独立把数据、训练、评估、部署整个链路走通的人,一只手数得过来。不是他们理论不行,而是大多数教程都在教"某个模型怎么调",没人教"一个AI项目从零到上线需要经历什么"。所以当我看到"ai-engineering-from-scratch"这个题目时,反而觉得这是一个特别清醒的起点:把工程拆开,一步步亲手搭起来,而不是站在别人的Demo上假装自己会了。
这篇文章就是围绕"从零开始做AI工程"这个主题展开的。我会沿着一条真实可落地的学习路径,讲清楚AI工程和AI研究的区别、一个完整项目的工作流、环境搭建的关键决策点,再用一个文本分类项目串起数据处理、基线模型、预训练模型和部署上线的全过程,最后整理我这些年踩过的高频问题和排查思路。无论你是刚接触AI的学生,还是想转行做AI工程师的开发,这篇文章都适合拿来当第一份"从零开始"的路线图——因为这里的每一个环节,我都用实际项目验证过。
1. 先想清楚:AI工程到底在解决什么问题
1.1 AI工程和AI研究是两条不同的路
很多新人会把"AI工程"和"AI研究"混在一起,上来就去啃论文、复现SOTA模型,结果几个月过去,模型还没跑通过一次。我的看法是:AI研究的核心是探索"模型能不能做得更好",它的产出是论文、算法和实验结论;而AI工程的核心是把模型变成稳定、可复现、可维护的系统,它的产出是一个能服务用户的接口、一条能自动更新的数据链路、一套能监控模型效果的平台。
打个比方,研究是"发明一道新菜谱",工程是"开一家能稳定出餐的餐厅"。研究只需要在实验室环境里证明这道菜好吃,工程却要考虑食材供应、厨房流程、出餐速度、口味一致性。回到AI项目上,如果没人关心数据从哪来、模型怎么上线、效果怎么监控,那再好的模型也落不了地。
所以"from scratch"的第一步,不是选模型,而是建立工程视角:把一个AI项目拆成数据、训练、评估、部署四个连续环节,每一环都有独立的质量标准。有了这个框架,你会发现后续的所有学习都是在往这四个环节里填充具体工具和技巧。
1.2 一条完整AI项目的流水线长什么样
一个典型的AI项目,从需求到上线,基本会经历下面这些环节:
- 业务与问题定义:明确要解决什么问题,分类、回归、检索还是生成?成功的标准是什么?
- 数据采集与清洗:确定数据来源,处理缺失、重复、噪音,做好标注和格式统一。
- 特征工程/模型选择:根据数据规模和任务类型,判断用传统机器学习还是深度学习,需要做哪些预处理。
- 训练与调优:划分训练/验证/测试集,选择损失函数、优化器、学习率,跑基线再逐步迭代。
- 评估与验证:用准确率、精确率、召回率、F1等指标衡量效果,并检查是否存在数据泄漏或过拟合。
- 部署与监控:把模型封装成服务,做好并发、延迟、日志和版本管理,持续追踪线上表现。
很多新人容易栽在"业务定义"和"数据清洗"上。他们觉得这两步不酷,一上来就想训练模型,结果用了几天时间调试模型,却发现是标注数据本身错了。我在实际项目中见过太多这样的案例:数据里标签错了一半,再好的模型也救不回来。所以在这条流水线里,数据环节的地位一点都不比模型环节低。
1.3 为什么"先跑通一个小项目"比"学完所有理论"更重要
如果让我给一个从零开始的人定优先级,我的答案很明确:用最小成本跑通一个完整项目,比什么都重要。理论可以边做边补,但完整闭环带来的"全局感"是看再多教程都换不来的。
我当年入门的时候,花了三周看机器学习理论,决策树、SVM、神经网络全都背得滚瓜烂熟,但第一次独立做项目还是懵:数据要归一化吗?类别不均衡怎么办?模型保存成什么格式?训练完怎么调用?这些问题没有一个是课本直接讲的,全是在项目里踩坑踩出来的。
所以我的建议是,选一个小数据集、做一个简单任务,先跑通"数据→模型→预测"的最小闭环。哪怕模型效果只有60%准确率,你也已经超越了"只会调API"的阶段。之后再去深入每个环节,效率会高很多。这个理念也是整篇文章的组织思路:先有骨架,再填血肉。
2. 动手第一步:把开发环境配到"一次到位"
2.1 Python版本与虚拟环境是第一个坑
环境配置看起来是杂活,但它决定了你后面能不能顺利复现别人的代码。Python的版本差异(3.8、3.10、3.12)会影响很多依赖库的兼容性,尤其是PyTorch、TensorFlow这类重型库,版本不对轻则警告,重则直接装不上。
我建议从第一天就养成两个习惯:
- 用版本管理工具管理Python版本,比如
pyenv或conda,不要直接在系统里装一个全局Python就开干。 - 每个项目创建独立的虚拟环境,把依赖隔离在项目内部。这样项目A升级了某个库,不会影响项目B的运行。
虚拟环境的原理其实很简单:每个环境是一个独立的目录,里面放着属于这个环境的Python解释器和已安装的库。用venv创建环境只需要一条命令:
python -m venv .venv source .venv/bin/activate # 在Windows上是 .venv\Scripts\activate激活后,你用pip install装的库都会被安装到.venv里,不会污染全局。团队协作时,把依赖导出到requirements.txt,别人就能一键复现你的环境:
pip freeze > requirements.txt2.2 显卡不是刚需,但你要知道自己卡在哪
很多新手一上来就问"要不要买4090",我的回答通常是"先别买"。因为学习阶段跑的都是小数据集、小模型,CPU完全能应付。等你真正开始训练BERT级别以上的模型,或者处理百万级数据的时候,再考虑GPU也不迟。
不过你要理解GPU和CPU的本质区别:CPU擅长串行任务,适合数据预处理、逻辑控制;GPU有几千个计算核心,适合并行的大规模矩阵运算,而神经网络训练本质上就是海量矩阵乘法。所以如果你用PyTorch训练一个ResNet,GPU比CPU快几十倍都不夸张。
判断自己是否需要GPU,看两件事:数据量多大、模型多深。如果你的数据是几万条级别、模型是逻辑回归或浅层神经网络,CPU足够;如果数据是百万级、模型要跑深度学习,那就需要GPU了。
即便没有本地GPU,也可以用云GPU服务或者免费的Colab跑深度学习实验。训练代码不用改,只需要把数据传到云端、在笔记本里指定GPU运行时即可。这一点对从零开始的人来说非常友好:你不需要一开始就花大价钱买硬件,先把流程学会,再决定要不要添设备。
2.3 一张表看清AI工程的常用工具栈
我把自己做AI工程最常用的工具按环节整理成了一张表,方便你对照着准备:
| 环节 | 常用工具 | 作用 |
|---|---|---|
| 开发语言 | Python | AI生态最丰富的语言,没有之一 |
| 数据处理 | Pandas、NumPy | 表格数据清洗、统计、变换 |
| 可视化 | Matplotlib、Seaborn | 数据分布、训练曲线、混淆矩阵 |
| 机器学习 | scikit-learn | 传统算法、特征工程、评估指标 |
| 深度学习 | PyTorch、TensorFlow | 神经网络模型的训练与推理 |
| 预训练模型 | Hugging Face Transformers | 加载BERT、GPT等预训练模型 |
| 实验管理 | MLflow、W&B | 记录参数、指标、模型产物 |
| 模型部署 | FastAPI、Flask、ONNX | 把模型封装成HTTP服务 |
对于从零开始的人,我建议优先掌握Pandas、scikit-learn、PyTorch、FastAPI这四样。它们分别对应数据、传统模型、深度模型、部署四个核心场景,学完就能覆盖绝大多数项目的需要。其他工具在遇到具体问题时再学,效率更高。
3. 完整实操:从零搭建一个文本分类项目
3.1 数据先行:加载数据、查看分布、切分数据集
理论讲得再多,不如亲手做一遍。下面我用一个文本分类任务来演示完整流程。为了让你能快速复现,我选用的是一个公开可得的新闻分类数据集,并聚焦在数据准备、模型训练和部署三个关键环节。
第一步永远是看数据长什么样。不要急着训练,先加载数据,查看类别分布、文本长度、是否有缺失值:
import pandas as pd df = pd.read_csv("news.csv") print(df.head()) print(df["category"].value_counts()) print("缺失值数量:", df.isnull().sum().sum())这一步的目的有三个:确认数据能正常读取、了解类别是否均衡、判断是否需要处理缺失值。我在真实项目中见过有人跳过这一步,直接训练,结果发现某个类别只有几十条样本,训练出来的模型对这个类别完全失效。
确认数据质量后,用train_test_split划分数据集。这里有一个容易忽略的细节:如果数据带有时间属性(比如新闻按日期排列),应该按时间切分而不是随机切分,否则会引入数据泄漏——模型在训练时"见过"未来的信息,线上表现会打折扣。
from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split( df["text"], df["category"], test_size=0.2, random_state=42, stratify=df["category"] )注意这里用了stratify参数做分层采样,保证训练集和测试集里各个类别的比例一致。对于类别不均衡的数据,这一步非常重要。
3.2 基线模型:用TF-IDF加逻辑回归跑通第一个版本
我强烈建议每个项目的第一步都先做一个"笨模型"基线,而不是直接上深度学习。基线的意义在于:它给你一个最低效果参考,后面所有复杂模型的效果都要跟它比;同时它跑起来快,几十秒就能让你看到完整流程。
文本分类最简单的基线是TF-IDF加逻辑回归。TF-IDF(词频-逆文档频率)把每篇文本转成一个向量,表达每个词在文档中的重要性;逻辑回归在这个向量上做分类。代码很简单:
from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.pipeline import make_pipeline model = make_pipeline( TfidfVectorizer(max_features=5000, ngram_range=(1, 2)), LogisticRegression(max_iter=1000) ) model.fit(X_train, y_train) print("测试集准确率:", model.score(X_test, y_test))这里有一个参数值得说明:max_features=5000表示只保留出现频率最高的5000个特征。为什么不是全部词汇?因为中文文本的词表动辄上十万,但大部分词只出现一两次,对分类没有帮助,反而会稀释有效特征。限制特征数量既加快训练,又能减少过拟合。ngram_range=(1, 2)表示同时考虑单个词和相邻两个词的组合,能捕捉一些简单的短语语义。
跑完基线之后,不要只看准确率,还要看每类的精确率和召回率:
from sklearn.metrics import classification_report print(classification_report(y_test, model.predict(X_test)))这一步能告诉你模型具体在哪些类别上表现差。如果是类别不均衡导致的,后面可以考虑做类别加权或数据增强。
3.3 进阶:用预训练模型替代手工特征
基线跑通之后,接下来可以引入预训练模型。很多人以为"from scratch"意味着每个模型都要从头训练,这其实是一个误解。在现代AI工程里,绝大多数场景不会从头训练一个大模型——那样需要海量数据和算力,得不偿失。更常见的做法是加载别人已经预训练好的模型,在自己的数据上做微调。
用Hugging Face的Transformers库,加载一个文本分类模型只需要几行代码。以中文场景为例:
from transformers import AutoTokenizer, AutoModelForSequenceClassification, Trainer, TrainingArguments model_name = "bert-base-chinese" tokenizer = AutoTokenizer.from_pretrained(model_name) model = AutoModelForSequenceClassification.from_pretrained( model_name, num_labels=len(df["category"].unique()) )这里的关键点在于"预训练"和"微调"的关系。预训练模型在海量通用文本上学会了语言的语法和语义,相当于一个读了万卷书的"通才";微调则是在你的具体任务数据上再学一遍,让它变成这个领域的"专家"。相比从头训练,微调需要的样本量少、训练时间短、效果还好,这是工程上的最优解。
训练数据需要先经过Tokenizer编码成模型能理解的格式:
def tokenize_function(texts): return tokenizer(list(texts), padding=True, truncation=True, max_length=128) train_encodings = tokenize_function(X_train) test_encodings = tokenize_function(X_test)max_length=128表示每条文本最多截取128个token。这个值不是越大越好,太长会拖慢训练速度,太短会丢失信息。我一般会看文本长度的分布,选择能覆盖90%样本的长度即可。
随后用Trainer训练,它会自动处理batch、梯度更新、日志记录等繁琐环节:
training_args = TrainingArguments( output_dir="./results", num_train_epochs=3, per_device_train_batch_size=16, evaluation_strategy="epoch", save_strategy="epoch", ) trainer = Trainer( model=model, args=training_args, train_dataset=train_encodings, eval_dataset=test_encodings, ) trainer.train()这里per_device_train_batch_size=16表示每批16条样本。如果显存不够,可以调小;如果训练很慢,可以调大。num_train_epochs=3是常见的初始值,太多会让模型过拟合。
3.4 部署:把模型变成一个可调用的服务
模型训练完只完成了一半工作,另一半是让其他人或系统能用上它。最简单的部署方式是用FastAPI封装一个HTTP接口,让前端、后端或测试脚本通过POST请求发送文本,返回分类结果。
先保存训练好的模型:
model.save_pretrained("./news_classifier") tokenizer.save_pretrained("./news_classifier")然后写一个FastAPI服务:
from fastapi import FastAPI from pydantic import BaseModel from transformers import AutoTokenizer, AutoModelForSequenceClassification import torch app = FastAPI() model = AutoModelForSequenceClassification.from_pretrained("./news_classifier") tokenizer = AutoTokenizer.from_pretrained("./news_classifier") class Item(BaseModel): text: str @app.post("/predict") def predict(item: Item): inputs = tokenizer(item.text, return_tensors="pt", truncation=True, max_length=128) with torch.no_grad(): outputs = model(**inputs) pred_id = torch.argmax(outputs.logits, dim=-1).item() return {"prediction": pred_id}这里有一个工程要点:模型在服务启动时加载一次,之后每个请求都复用同一个模型实例,而不是每次请求都重新加载。加载一个大模型可能要几秒甚至几十秒,如果每个请求都加载一次,服务根本没法用。
启动服务:
uvicorn app:app --host 0.0.0.0 --port 8000用curl测试:
curl -X POST http://localhost:8000/predict \ -H "Content-Type: application/json" \ -d '{"text": "这是一条测试新闻"}'到这里,一个完整的AI工程闭环就成立了:从数据到模型到服务,每个环节都亲手跑了一遍。这时候你对"from scratch"的理解,会比看十篇教程都深。
4. 训练过程的真实战场:高频问题与排查技巧
4.1 数据问题:90%的模型效果差都是数据惹的祸
我先说一个可能让新手意外的事实:在工业界,模型效果不达预期时,我第一个怀疑的不是模型结构,而是数据。数据里的问题常见的有三类:
第一类是标签噪声。标注人员标准不一致,或者标注工具出错,导致同类文本被标成不同类别。我遇到过有项目里"科技"和"体育"两个标签混了一批新闻,模型怎么调都分不清,最后发现是标注时把"体育科技"类的文章随手归错了类。排查方法很简单:把训练集中模型预测错且置信度高的样本打印出来,人眼扫一遍,往往能发现标签问题。
第二类是类别不均衡。有的类别样本上千条,有的只有几十条。模型为了降低整体损失,会倾向于把样本都预测为大类,小类完全被忽略。解决办法有三种:用class_weight给少数类更大权重、对少数类做过采样/对多数类做欠采样、或者用F1等均衡指标代替准确率评估。
第三类是文本清洗不彻底。HTML标签、多余空格、特殊符号、全角半角混用,都会干扰模型。比如爬虫抓下来的文本经常带\u3000全角空格,看起来没区别,但在分词阶段会变成两个不同的token。我的习惯是在数据准备阶段做一个统一的清洗函数,把所有文本过一遍:去HTML、去不可见字符、统一大小写、统一全角半角。
4.2 训练问题:loss不下降、显存溢出、训练崩溃怎么办
训练过程中最常见的三个问题,我一个个说。
loss不下降。先检查学习率是不是太大或太小。学习率太大会导致loss震荡甚至发散,太小则收敛极慢。我建议用学习率预热加衰减的策略:前几百步用一个较小学习率让模型稳定起步,之后逐步增大到设定值,再按余弦曲线衰减。很多框架都内置了这个功能,比如Hugging Face的Trainer里有get_linear_schedule_with_warmup。
再检查数据是不是没做shuffle。如果不打乱数据顺序,模型会集中在连续几个batch里见到同一类样本,梯度方向被带偏,loss会在一个区间反复横跳。把DataLoader的shuffle=True打开,很多时候问题就消失了。
显存溢出。PyTorch会报CUDA out of memory。最直接的解决办法是把batch_size调小,比如从16降到8或4。如果batch太小效果不稳定,可以开启梯度累积:每跑几个小batch才做一次梯度更新,等效于大batch的效果:
accumulation_steps = 4 optimizer.zero_grad() for i, batch in enumerate(dataloader): loss = model(batch) loss = loss / accumulation_steps # 归一化 loss.backward() if (i + 1) % accumulation_steps == 0: optimizer.step() optimizer.zero_grad()另外可以用混合精度训练(AMP)。FP16比FP32省一半显存,在NVIDIA GPU上速度还更快。PyTorch里的实现非常简洁:
from torch.cuda.amp import autocast, GradScaler scaler = GradScaler() with autocast(): loss = model(batch) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()训练崩溃,比如loss变成NaN。这通常是因为学习率太大导致的梯度爆炸,调小学习率一般能解决。如果是文本任务,还可能是因为embedding层出现了极端值,给输入做一下标准化或者增加梯度裁剪clip_grad_norm_会有帮助。
4.3 评估陷阱:别让准确率骗了你
我见过太多团队在演示的时候报喜:"我们的模型准确率95%!"结果上线后发现完全没法用。为什么?因为准确率在类别不均衡时是一个极具欺骗性的指标。
举个例子,一个二分类任务里90%是负样本,你什么都不做、全部预测为负类,准确率就有90%。但这个模型没有任何实用价值。所以评估分类模型时,一定要看混淆矩阵和精确率、召回率、F1值:
- 精确率:预测为某类的样本里,有多少是真的。
- 召回率:某类的真实样本里,有多少被找出来了。
- F1值:精确率和召回率的调和平均,兼顾两者。
不同业务场景对精确率和召回率的偏好不同。垃圾邮件拦截更看重精确率——误杀正常邮件比漏拦垃圾邮件代价更高;癌症筛查则更看重召回率——宁可多检查一些人,也不放过一个真病人。
我在评估时还有两个习惯:一是把预测错误的样本打印出来逐条看,分析错误模式比只看数字更有价值;二是用置信度阈值调整来控制精确率和召回率的平衡,大多数模型都提供概率输出,可以按业务需求选择阈值。
5. 从"跑通项目"到"工程化"的几个关键习惯
5.1 实验可复现:固定随机种子和记录配置
代码能跑通只是第一步,别人甚至三个月后的你能否重新跑出同样的结果,才是工程化的分水岭。机器学习里充满了随机性:数据洗牌、权重初始化、dropout,每一步都可能导致结果波动。如果不做控制,你可能昨天跑出85%准确率,今天跑出82%,却说不清差异来自代码改动还是随机波动。
最简单的做法是固定随机种子:
import random import numpy as np import torch def set_seed(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed)但这只是基础。更工程化的做法是用配置文件把每次实验的所有参数记录下来:学习率、batch size、epoch数、数据版本、模型结构、随机种子。用yaml或json保存一份配置,训练时输出到日志目录。这样你随时能回答一个问题:"这个效果好的模型,是用什么配置跑出来的?"
我见过很多团队用"实验记录.xlsx"来登记实验,但Excel在并发记录和数据量大时会很痛苦。用MLflow这样的实验管理工具可以自动记录参数、指标和模型文件,后续对比实验也会很方便。
5.2 项目结构:训练代码和推理代码分离
初学者的项目里,最常见的状态是训练和推理都堆在一个Jupyter Notebook里:前面是数据处理,中间是模型训练,最后是几个预测示例。自己调试的时候还行,但要部署上线、要给别人维护,就是一场灾难。
我建议从项目一开始就建立清晰的结构:
project/ ├── configs/ # 配置文件 ├── data/ # 数据文件 ├── src/ │ ├── data.py # 数据加载与预处理 │ ├── train.py # 训练脚本 │ ├── infer.py # 推理脚本 │ └── utils.py # 常用工具函数 ├── models/ # 保存训练好的模型 └── requirements.txt训练代码和推理代码分离,是因为它们的生命周期完全不同。训练代码改得频繁,但只在训练时运行;推理代码相对稳定,但需要长期在线服务。把它们混在一起,任何一处改动都可能影响线上服务稳定。
infer.py里只做一件事:加载模型、接收输入、返回预测。它不依赖训练时的数据清洗细节,只需要把输入转换成模型能理解的格式。做到这一点,部署和维护的成本会直线下降。
5.3 工程思维:能用规则不用模型,能用小模型不用大模型
最后分享一个可能不太"AI味"的经验:工程的核心是解决问题,不是堆模型。很多场景用不上深度学习,甚至用不上机器学习。
我接手过一个文本分类任务,需求是判断用户反馈是否属于"退款问题"。最初的技术方案是训练一个BERT分类模型,一轮调优下来效果也还可以。但后来我们分析了一下数据,发现这类反馈有极强的关键词特征,一条包含"退款""退钱""申请退款"的规则就能覆盖八成以上的场景,准确率接近99%,延迟几乎为零,维护成本远远低于训练一个BERT模型。
这不是说模型没用,而是说在工程上,简单方案能解决就不上复杂方案。模型是工具,不是目的。当你的简单规则覆盖不了、或者场景需要泛化的时候,再上机器学习;当小模型效果不够、大数据量需要更强表示的时候,再上大模型。每一步决策都应该有成本收益分析。
这个思维是从零开始做AI工程最容易忽略、也最值钱的一课。学会了它,你已经不只是"会用AI工具的人",而是一个真正能对系统负责的工程师。
我在实际带项目时还发现一个规律:凡是能坚持从简单到复杂、一步一步亲手做完完整项目的同学,后面成长速度都非常快。相反,那些一开始就盯着最前沿模型、急着复现最复杂算法的同学,反而容易在某个环节卡住后彻底放弃。所以,如果你正走在"ai-engineering-from-scratch"的路上,我的建议很简单:先选一个几万条数据的小任务,把数据清洗、基线模型、评估、部署整条链路跑通,再回头补理论。这一轮下来,你对AI工程的理解,会超过大多数停留在教程里的人。