☰
从零开始AI工程:数据到模型部署的完整链路实践
2026/10/5 5:37:50 网站建设 项目流程

1. 为什么选"从零开始",而不是直接上手现成框架

先交代一下背景:我在这里说的"ai-engineering-from-scratch"并不是让你去从零实现一个Transformer或重写CUDA算子,而是指——不借助那些帮你屏蔽细节的完整脚手架,从最底层的数据准备、模型训练脚本、评估逻辑到服务部署,一步一步亲手搭起一条AI工程链路。

如果你在搜索引擎里查"AI工程入门",跳出来的绝大多数教程都是这么教的:装好PyTorch,加载一个预训练模型,跑通一个inference脚本,然后说"你已经完成了一个AI项目"。这话没错,但也很坑——因为它跳过了所有真正会在实际工作中绊倒你的环节。等你去公司实习或者接手真实业务,发现数据不是下载一个CSV就能用的,模型跑完不是输出一个精度数字就结束的,你要把模型包成接口、监控它的漂移、处理它上线后每天收到的几十种异常输入,这个时候"从脚手架学会的东西"就完全不顶用了。

所以"from-scratch"的定位,是用一种笨但扎实的方式,把AI工程里每一个被黑盒化的环节亲手做一遍。哪怕你最后还是会回到PyTorch、回到HuggingFace、回到Docker和K8s,但你回去之后的心态完全不同——因为你亲手写过那些已经被封装成三行API调用的底层逻辑,"会用"和"懂为什么这么用"是两种体验。

如果你正处在下面某个阶段,这篇文章大概率帮得上忙:

  • 已经能跑通教程里的模型训练,但换一个数据集就手足无措;
  • 想转行做AI工程或算法工程,但简历上只有调库经验,心里发虚;
  • 自己做了一个小模型demo,想上线却完全不知道从哪一步开始;
  • 纯粹好奇AI工程完整链路长什么样,想找个不靠"项目模板"的路径。

我接下来要讲的不是一套噱头,而是我按"从零开始"的方式完整走了一遍AI工程全流程之后沉淀下来的实操记录。每一章都对应了一个真实环节,按顺序读下来,再按顺序做一遍,你会比"背熟了三个框架的API"的人更接近"工程"这个词的本质。

2. 动手前先想清楚:从零到一的工程骨架长什么样

很多人的问题不是不懂得努力,而是不知道一条完整的AI工程链路到底由哪些环节组成。结果就是东一榔头西一棒子,今天学个网络结构,明天看一篇损失函数优化的文章,后天复制一段数据增强代码,半年过去还是拼不出一个完整闭环。

2.1 一条最小可用链路的六个环节

我把自己从零搭建AI工程的过程画成了一根主线:数据→特征→模型→训练→评估→服务化。这六个环节看起来平淡无奇,很多人甚至觉得"就这?"。但如果你真的亲手走一遍,会发现每个环节内部都藏着足够让人崩溃半天的细节。

以我自己做的一个文本分类项目为例:任务是给定一段用户对产品的评价,判断情感极性(正向、负向、中性)。这是个再经典不过的入门任务,但一旦不从Kaggle的干净CSV出发,而是从模拟的真实业务环境出发,事情很快就变了。

下面这张表是我在动手前整理的最小链路清单,每一项对应了什么交付物、什么验收标准,目标是一天之内跑通一个不需要任何框架封装的最小闭环:

环节具体要做什么验收标准
数据采集、清洗、标注、切分拿到可训练的干净数据集,分布基本合理
特征文本分词、去停用词、向量化或序列化模型能读入张量,维度与词表一致
模型构建一个结构完整可解释的模型输入一个batch能前向传播,输出维度正确
训练损失计算、梯度回传、参数更新loss曲线下降,指标逐步上升
评估在验证集和测试集上计算指标指标能反映真实业务水平,不止看准确率
服务化封装接口、部署运行、监控日志能用HTTP调用模型,返回有意义的预测结果

你可能注意到了,这张表里没有提到"用哪个框架"。这是从零开始练习时最重要的一条原则:别让框架替你决策。在这个阶段,PyTorch、TensorFlow这些工具都可以用,但默认配置之外的每一步——数据集怎么切、batch怎么设、评价指标选什么、模型参数怎么初始化——都要自己问自己一声"为什么"。真正动手做的时候就明白了,框架帮你省掉的是重复劳动,不是理解成本。

2.2 环境搭建的省力思路

我见过太多人卡在环境搭建这一步,装了删、删了装,最后在群里问"为什么我import torch一直报错"。

我的建议是,既然目标是练习工程能力,就用一套主流的Python环境管理方案搭底,越简单越好。我用的是conda加venv的组合:用conda管理Python版本,用venv隔离项目依赖。不要一上来就学Docker,那是后面服务化阶段的事,现在给自己增加负担没有意义。

具体步骤不复杂:

conda create -n ai-eng python=3.10 conda activate ai-eng pip install numpy pandas scikit-learn torch --index-url https://download.pytorch.org/whl/cpu

第一周不需要GPU,CPU已经完全能跑通文本分类的练手项目。等到你真的进到训练大模型阶段再配CUDA,那时候你已经知道自己需要什么显卡、什么驱动版本、什么算力服务了——每一步都在需要时才引入新复杂度,这才是从零开始的正确节奏。

2.3 数据先行:没有好数据,后面全是白干

做AI工程和做算法竞赛最大的不同是:竞赛的数据集是别人准备好的,工程里的数据集是你要自己养大的孩子。

我模拟了一个非常现实的数据收集场景:产品评价分散在几个渠道里,格式不统一,有的带评分、有的纯文字、有的夹杂大量表情符号和错别字。为了不让第一天就卡死在数据清洗上,我做了三件事:写一个统一的读取脚本把不同格式拉平、写一份简单的标注规范文档、用程序自动做初版标签再利用人工校验。

这套流程跑下来,我才真正理解为什么前辈常说"数据和特征决定了模型的上限,算法只是逼近这个上限"。我当时用的模型非常简单——就是一个词向量加权平均之后接一个逻辑回归,但经过了认真清洗和合理的停顿词/标点处理之后,准确率直接干到了86%。而如果我什么都不洗,把原始文本扔给一个LSTM,准确率就只有72%左右。

所以从零开始练习时,请把至少一半的时间花在数据上。别嫌枯燥,这是AI工程这条路上性价比最高的时间投入。

3. 用"最小手写模型"理解训练机制:前向、反向与损失的本质

很多教程一上来就让用nn.Linear、nn.Embedding搭模型,三个类拼起来就号称"搭建了一个神经网络"。初看没什么问题,但直到某天面试官问我"梯度更新时为什么要把梯度清零",我才发现自己对训练的理解全是断层的。

3.1 既然要"从零",就试着手写一个MLP

为了不在工程的起步阶段就被框架的抽象吓退,我做的第一个模型不靠任何深度学习框架的自动层封装,而是用一个纯NumPy实现的多层感知机(MLP)来做情感分类的baseline。

这个选择看起来"倒退"了,但对理解训练的本质帮助极大。我不需要处理什么复杂的东西,只需要实现三部分:

  • 前向传播:线性变换加激活函数逐层计算;
  • 反向传播:按链式法则把误差从输出层传回输入层;
  • 梯度下降:用梯度更新每层的权重和偏置。

给你看一下我当时写的关键片段(示意,非完整代码):

import numpy as np def relu(x): return np.maximum(0, x) def softmax(x): e_x = np.exp(x - x.max(axis=1, keepdims=True)) return e_x / e_x.sum(axis=1, keepdims=True) def cross_entropy_loss(y_pred, y_true): m = y_true.shape[0] p = np.clip(y_pred, 1e-12, 1.0) return -np.sum(y_true * np.log(p)) / m # 前向:输入 [batch, input_dim] → 隐层 → 输出 z1 = np.dot(X_batch, W1) + b1 a1 = relu(z1) z2 = np.dot(a1, W2) + b2 a2 = softmax(z2) # 反向(第2层到第1层的误差传播简化版) dz2 = a2 - y_batch dW2 = np.dot(a1.T, dz2) / batch_size # 更新 W2 -= lr * dW2

当时跑通这个代码让我想明白了很多"原来如此"的事:

  • 为什么softmax后面要跟log损失——因为它们两个配合起来,梯度形式极其干净,就是预测概率减真实标签;
  • 为什么激活函数选ReLU而不是sigmoid——因为链式法则传回来之后sigmoid的导数在两端接近0,梯度会消失,网络更新不动;
  • 为什么训练的时候每个epoch要把数据打乱——因为如果每轮都按固定顺序喂数据,模型会记住顺序里的规律而不是数据本身的规律。

这些东西在PyTorch里都被封装得看不见摸不着,但你要真出了训练不收敛的问题,回头排查的时候脑子里有没有这个图景,决定你是"看报错猜原因"还是"顺着计算链路直接定位"。

3.2 从手写模型切回现代框架的正确姿势

有了一个手写MLP的体验之后,再回到PyTorch会顺手得多。你不必把框架里的每个算子都手动实现一遍,但从此你会带着一种"我知道你底层想干什么"的状态去读错误信息、去设计网络结构。

第一次切回PyTorch时我刻意对比了一下,同样是实现一个带embedding的文本分类网络,框架里要建的东西变成了三块:词表映射、Embedding层、分类头。看似多了东西,但核心思路一点没变,还是"把文本变成向量,把向量过几层,把结果变成概率"。只是现在每一层都有人给你写好了前向和反向,你只需要决定层与层怎么组合。

这样做的意义在于,你不再把模型当作一个"魔法黑箱"来看待,而是当作一套你亲手装起来过、现在只是换用标准零件重装一次的机器。遇到问题的时候,你会拆到具体某个零件去排查。

4. 从Notebook到训练脚本:我踩过的工程化深坑

模型能在Jupyter Notebook里训起来了,只是热身完成。AI工程真正的分水岭出现在"把Notebook改写成训练脚本"这一步——大量数据、多轮运行、需要稳定复现的结果。我在这段吃过不少苦头,挑两个最典型的说。

4.1 第一个坑:固定随机种子比你想的更复杂

一开始我把Notebook里的代码原封不动搬进训练脚本,设置了一个random.seed(42)就以为万事大吉。结果发现每次运行出来的指标都在小幅波动,甚至在数据加载逻辑不变的情况下,两次结果相差能到1.5个百分点。

排查了老半天,终于找到三个"漏网之鱼":NumPy有自己的随机数生成器,PyTorch有自己的torch.manual_seed,Python内置random是三套独立状态;其次数据要做shuffle,用的还是Python内置random,而跨进程数据加载的时候还要额外设PyTorch的worker_seed。总之,要真正可复现,需要在脚本开头把所有随机源都固定一次:

import random import numpy as np import torch def set_all_seeds(seed): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed)

这个坑很小,但它让我意识到什么叫"训练脚本的工程素养":你不仅要图模型跑通,还要保证任何人在任何环境里跑你的代码,都能得到同一份结果。没有可复现性的实验脚本,本质上是还没进入工程状态。

4.2 第二个坑:早停、学习率调度与"训练完"的定义

在Notebook里训练模型,结束的标准往往是"loss已经很低了"或者"我不想等了"。但工程里的训练需要有明确终止条件,否则你没法自动化、没法对比不同实验。

我当时的处理是在训练脚本里加入三项机制:

  • 验证集早停:每个epoch结束在验证集上算一次指标,如果连续若干个epoch没有改善,就提前停止并保存最佳模型权重;
  • 学习率调度:训练初期学习率较高快速下降,后期降低学习率做精细收敛,用的是常见的阶梯式衰减或余弦退火;
  • 检查点保存:每个epoch都保存一份最新的权重文件,文件名带上epoch数和验证指标,防止中途断掉后从头再来。

这三个机制的实现并不难,但每加一个都会逼你思考一个问题:你评估一个模型好坏的信号到底是什么?是训练loss还是验证集上的某项指标?一旦你把"好"的定义从"loss很低"细化成了"在分布接近真实场景的验证集上F1分数最高",你就会开始认真认识评估环节的价值。

4.3 训练基础设施的轻量搭建

我不会一上来就让你接触分布式训练、MLflow、Kubeflow这些重型武器。轻量级的方案完全够用来理解工程化的核心:脚本化、可配置、可追踪。我的做法是:

  • 训练参数全部用argparse或简单的配置文件管理,不硬编码在代码里;
  • 每一轮实验的日志写到一个固定的logs目录,标注时间戳;
  • 最终指标记录到一个CSV文件里,方便横向比较不同参数组合的效果。

这套轻量方案让我在一周内跑了上百组小实验,养成了"每次修改都有记录,每个结果都可回溯"的工作习惯。后来看到别人用重量级平台做的事情,本质上就是把这套手工流程做成了系统和可视化——但如果你连手工流程都没走通过,那些平台在你手里也只是徒增概念负担。

5. 评估指标里最容易骗人的东西:我为什么不再只看准确率

训练跑通了,脚本稳定了,模型的准确率到了86%左右,当时我觉得这个项目已经成功了大半。直到我把预测结果按真实标签逐条打开看,才出了一身冷汗。

5.1 类别不平衡会怎样玩弄你的准确率

我那个情感分类数据集里,正向评价占了大约60%,负向和中性的加起来只有40%。一个"永远预测正向"的傻瓜模型,准确率可以到60%。而我的模型虽然整体准确率86%,看起来不错,但仔细一拆混淆矩阵:

  • 正向的召回率接近95%,但负向的召回率只有68%,中性的更是掉到55%;
  • 也就是说,用户在一条极其不满的评价里用了比较隐晦的表达,我的模型很可能把它归成"中性"甚至"正向";
  • 这在业务上是无法接受的,因为愤怒的用户需要被优先发现、优先响应。

这是AI工程里最经典的评估误区:只看准确率。准确率只在类别分布均衡、且各类别错误代价相同的时候才有参考价值,而真实业务几乎永远不满足这两个条件。

我最后改用了三个指标的组合:精确率、召回率、F1,并且分别算每个类别的宏平均和加权平均。在此之上还额外记录了一份"严重错误率"——把真正的负向评价预测成正向的比例——这个指标直接反映业务上最不想看到的情况。

5.2 训练集、验证集、测试集这样切才是对的

另一个评估相关的坑是数据切分。我最初用train_test_split一股脑切了80/20,训练过程中又截了一部分当验证集做早停。结果问题马上就来了:因为验证集是从测试集里"偷"的,我相当于拿着考试题当模拟题反复练习,最后的测试指标虚高。

正确的做法是:先切出一块完全不见过的测试集,把剩下部分再切出验证集。整个过程应该是数据一加载就先完成,中间不能有任何代码"看"到测试集的信息。文本类数据还需要额外考虑一点——如果同一用户的多条评价有相关性,还需要按用户ID分组切分,防止数据泄漏。我一开始没注意,后来发现同一用户的评价同时出现在训练集和测试集里,模型指标虚高了好几个点。所以数据切分不是随便切两份就完了,先想清楚你的数据里什么单位是"不可切割的原子",再动手。

6. 服务化上线:模型能跑只是开始,能一直跑才是工程

到这一步,模型已经过了评估这一关,理论上可以上线了。但"上线"这个词在AI工程里,含义比大多数人想象的大得多。你要从一个model.pt权重文件出发,让一个完全不懂模型的Web服务能稳定地加载它、接收请求、返回结果,并在线上环境持续运行不崩溃。

6.1 API服务的三种实现路径,我的选型和理由

我实际尝试过三种方式把模型变成接口:

方式优点缺点适合场景
Flask/FastAPI 直接写Web接口简单直接,快速跑通需要自己管并发、生命周期、模型预热极小流量或内部工具
模型服务框架(如TorchServe、Triton)自带并发、批处理、监控配置复杂,定制不灵活生产环境中等以上流量
ONNX Runtime + FastAPI推理性能好,跨环境兼容需要转模型,调试维度单一对延迟和硬件兼容有要求

我自己的练习项目流量不大,也不追求极限性能,选了第一条路径——用FastAPI做服务封装。这不代表我否定另外两条路径,而是在"练习"这个阶段,最简单的方案能暴露最多工程细节,不会用框架的默认行为掩盖掉问题。

当时封装服务的核心代码逻辑大致是这样:加载权重 → 建立词表映射 → 接收请求文本 → 预处理 → 预测 → 构造响应。

from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() model, vocab = load_model_and_vocab() class PredictRequest(BaseModel): text: str @app.post("/predict") def predict(req: PredictRequest): vec = text_to_tensor(req.text, vocab) prob = model.predict_proba(vec) label = int(prob.argmax()) return {"label": label, "confidence": float(prob[label])}

这段代码看着简单,但上线后冒出来的问题全在细节里:请求偶尔带有非法字符导致预处理报错、模型首次加载需要好几秒导致第一个请求超时、日志里没有任何关于预测内容的信息导致出了问题没法复盘。

6.2 被夸大的部署:容器化第一次给了我正反馈

说实话,在动手之前我一直觉得Docker是一个"很重"的东西,学起来肯定要命。但真正给AI服务写了个Dockerfile、把镜像跑起来之后,才发现它其实是帮你从"本机能跑"走向"别的地方也能跑"的最短路径。

Dockerfile本身简单得让人意外:

FROM python:3.10-slim WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY ./app ./app COPY ./weights ./weights CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

写到这里其实我只花了一上午。真正改变我工作方式的是那个晚上:我在本地跑得好好的服务,部署到一个只有2GB内存的服务器上之后,直接内存溢出崩了。一查原因,是模型加载的时候向量矩阵太占内存,而我测试环境的内存是开发机的四倍。Docker在这里最大的价值不是"更方便地发版",而是能用一行命令在模仿生产环境的容器里提前暴露"环境差异"问题。从那以后我再也不敢只在本机对着Notebook说"跑通了"。

6.3 日志和监控:没有它们,线上事故等于盲人摸象

模型进入线上服务之后,最常被忽略的工程环节就是日志。线上一旦出了问题,你手里没有任何信息:是谁的请求、什么文本内容、模型输出了什么、耗时多少?这种时候你只能去猜,而AI应用的玄学问题又多,猜是猜不出来的。

我后来给自己的服务加了三套日志:

  • 访问日志:记录每个请求的文本长度、处理耗时、预测结果;
  • 数据分布日志:定期统计线上输入的文本长度分布、高频词,和训练集做对比,用来发现数据漂移;
  • 异常日志:记录所有预处理报错和未知输入形态。

这让我从一个"上线即焦虑"的状态变成了"出问题能在一小时内定位大概率方向"的状态。特别是数据漂移这个点,真实业务里的输入会随着季节、活动、用户群体变化而改变分布,模型用久了自然会变钝。如果没有数据分布日志,你只能等用户投诉变多了才发现模型出了问题;有了日志,你能在指标还没明显下滑的时候就意识到"线上文本的长度分布已经和训练集差了一大截"。

7. 给同样想从零开始的人:我的路径复盘和避坑清单

写到这里,整个从零开始的AI工程链路已经串完了一遍。如果你按顺序把我前面说的六个环节都亲手做过一遍,最短一个周末可以跑通最小闭环,认认真真做一周可以做到服务化。如果只挑一个最重要的心得来说,那就是——不要贪快,每个环节亲手做一遍,远远胜过把所有环节用框架黑盒跑十遍。

我把这一路踩过的最有价值的坑整理成了一份清单,方便你实操前先扫一眼:

  • 犹豫时先不要引入新工具:能用内存加载解决的就别急着上数据库,能用脚本解决的就别急着上框架,等瓶颈真实出现再做技术升级;
  • 任何环节先定义"什么叫完成"再动手:数据的完成是清洗后统计分布可解释,模型的完成是可复现的实验记录加指标,服务的完成是持续稳定处理请求而不是"能够返回一次结果";
  • 指标只信拆分后的明细,不信聚合后的数字:整体准确率会骗你,按类别拆开的精确率和召回率不会;
  • 给日志留一席之地:没有日志的服务就像一个没有仪表盘的驾驶舱——不出事故一切都好,一出事故你连从哪开始排查都不知道;
  • 环境差异永远是事故高发区:本地跑通不是真跑通,在一个干净的环境里重新部署一次才算。

我个人的体会是,AI工程这个领域最大的门槛不是数学,不是编程,甚至不是算法,而是对"完整链路"的掌控感。很多人卡在某个环节学不下去,往往是因为他只盯着眼前这一小段,看不到它在整条链路里的位置。而"from-scratch"的这轮练习,恰恰把你强行按在那条链路的起点,逼着你一路走到终点——这个过程产生的掌控感,值得每一个想入行AI工程的人亲自体会一次。

最后多分享一个我当时收尾时做的小事:把训练、评估、服务三段脚本分别用三个README串起来,让一个完全没看过我代码的人照着文档能完整复现整条链路。写文档的过程又一次暴露了我代码里的含糊之处,而修完之后我才敢说这个项目真正"从零开始走完了"。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询