资讯动态

AI工程从零实战:手写逻辑回归到FastAPI部署全链路

发布时间:2026/10/5 5:38:04 来源:尧图企业网站定制
去年我给自己列了个项目名字就叫 ai-engineering-from-scratch。听起来像个开源仓库其实它更像一份自我训练计划不借助任何AI平台的现成能力从一条原始数据、一台普通笔记本开始把AI工程全链路亲手做一遍。为什么要这么折腾因为我在实际业务里吃过亏——直接调用现成模型框架很快但一旦线上数据长得和训练集不一样、预测结果需要解释、模型隔三差五给你出点怪问题的时候你手里全是黑盒连从哪里下手排查都不知道。这个项目做完之后回头看它解决的问题其实很清晰它让我真正理解了AI工程链条上每个环节的“为什么”也沉淀了一套可以迁移到真实业务里的最小工程模板。这篇文章不是泛泛而谈AI工程概念而是记录我实际动手时怎么选型、怎么写代码、怎么避坑希望给准备自己从零跑一遍AI项目的朋友一点参考。不管你是刚转算法的数据新人还是想补齐工程能力的后端开发者只要能装好Python都能沿着这条路线走通一遍。1. 为什么我坚持从零搭建AI工程而不是直接套用现成框架1.1 调包一时爽排障火葬场我早期做数据分析的时候处理分类问题基本就是sklearn.LogisticRegression一把梭。Pipeline一写、fit一跑、准确率一打印看起来整个流程天衣无缝。但问题出在“看起来”上。有一次模型在测试集上准确率到了88%一上线业务方隔天就跑过来说预测结果特别离谱——老客户被预测成高流失、新注册高活跃用户反而被判为要流失。我去排查时发现训练样本里的特征和时间线上的实时数据分布根本不一致但因为所有环节都藏在封装层后面我连“哪个环节出的错”都定位不到。调包本身没有错错的是我对封装层内部发生了什么一无所知。AI工程说到底是数据流、特征流、模型流、服务流四条线的组合。你跳过中间任何一环的理解遇到问题时就只能瞎试。这种时候我才下定决心要用 from scratch 的方式把整条链路自己搭一遍。先不谈性能优化至少每个环节的长相和边界要搞明白。1.2 从零做一遍才理解数据是怎么“流”起来的从零搭建之后我发现最大的收获不是“会写逻辑回归了”而是脑子里真的有了数据流的概念。原始 CSV 从data/raw.csv读进来经过清洗变成干净的 DataFrame再经过特征工程变成模型输入矩阵模型训练后产出参数文件接口服务再把“新数据”变成“新预测”。每一步的输入和输出都清清楚楚。这种认知在日常项目里非常值钱。因为大多数业务问题到最后都是数据问题而数据问题的定位方式就是沿着数据流一段一段检查。如果你从零搭过一遍你会习惯性地问新进来的数据有没有经过同样的清洗逻辑缺失值填充的规则是不是和训练时一致标准化用的均值方差是不是训练集上的这些问题不自己动手做过很容易忽略。1.3 少折腾的技术栈选型我从零搭建不是说要连 Python 解释器都自己写而是尽量用轻量、透明的工具避开“重型框架”带来的心智负担。技术栈选型如下模块选型选择理由语言Python 3.10机器学习和后端生态最全语法直观数据处理pandas 2.x表格型数据的清洗、过滤、分组都很顺手数值计算NumPy 1.24手写模型训练逻辑的基础计算库模型算法手写逻辑回归参数、梯度、正则全部可见理解最核心的训练过程特征切分scikit-learn 的 train_test_split只用它的数据切分能力不用它的训练封装接口框架FastAPI uvicorn轻量、自带请求校验和接口文档适合快速上线模型持久化joblib保存模型参数和标准化器简单可靠环境管理venv pip系统自带无额外成本这套组合就一个原则所有关键逻辑都暴露在眼前不藏黑盒。你要真想理解AI工程就不要一上来就上大而全的ML平台也别一上来就调深度学习的封装接口。先用最笨的方式把最小闭环跑通后面再换任何高级工具都来得及。2. 从零动手前建议先准备好三样东西2.1 心态把“跑通”和“跑对”分开从零搭建最大的坑是“想得太容易”。你以为一个下午就能跑通实际上光是数据清洗就可能折腾一整天。我建议把心态调整为“先跑通再跑对最后跑稳”。第一版代码哪怕很丑先让它从头走到尾能输出一个预测结果然后逐步检查每一段逻辑是否符合预期最后再考虑代码结构、并发和监控。这里有一个特别实用的习惯每一步都打印中间结果。清洗后打印df.shape特征工程后打印X.head()训练时每200个epoch打印一次 loss评估时打印混淆矩阵。这些“看得见的输出”是你定位问题的唯一线索。千万别闷头一路写完再统一调试那样出错时根本不知道是哪一步污染了数据。2.2 数据先找一个小而脏的数据集练手很多人一上来就想去跑完整版 Kaggle 竞赛数据我劝你别这么干。数据太大你的注意力全花在等待和哄GPU上反而学不到工程要点。我选的是一个客户流失预测数据集字段也就十几个正负样本比例大致在 80:20既有数值列也有类别列非常适合练习。我甚至故意往里面塞了一些脏东西重复行、年龄负数、Balance列混入字符串、数值列出现缺失。为什么这么做因为脏数据才是真实业务里的常态如果一开始就拿着干干净净的数据集你根本练不到清洗能力。数据文件建议固定放在data/raw.csv处理完的写回data/processed/。这一步看似简单但能帮你建立“原始数据永远只读、派生数据单独落盘”的习惯后面做实验对比会非常省心。2.3 工具链一台能装Python的机器就够做这种最小闭环项目不需要GPU也不需要服务器集群一台普通笔记本完全够用。唯一的要求是Python环境稳定依赖包能正常装。我的目录结构是这样的ai-engineering-from-scratch/ ├── data/ │ ├── raw.csv │ └── processed/ ├── models/ │ └── logistic_customer_churn.joblib ├── src/ │ ├── data_prep.py │ ├── features.py │ ├── train.py │ └── serve.py ├── experiments/ │ └── exp_001.yaml ├── README.md └── requirements.txt我强烈建议你不要用 Jupyter Notebook 一路写到天荒地老。Notebook适合做探索性分析但训练和服务逻辑最好尽早放进 .py 脚本里。原因很简单脚本才是工程化的最小载体能进Git、能跑测试、能被接口调用。从零练工程练的就是这种“把杂乱的探索代码变成干净模块”的能力。3. 从零跑通AI项目数据、特征、模型、服务四段式实操3.1 第一步清理数据别让脏数据毁掉后续一切数据清洗是AI工程里最枯燥但最关键的一步。我常用的几条规则重复行直接删除数值列中明显不合理的值比如年龄为负、金额字段变成字符串转成NaN再填充类别列统一大小写去除首尾空格有少量缺失时优先用中位数填充。中位数比均值稳健因为很多业务数据是偏态分布均值会被少数极端值带跑偏。实际代码大概是这样的import pandas as pd # 读取原始数据 df pd.read_csv(data/raw.csv) print(原始形状:, df.shape) # 重复行处理 df df.drop_duplicates().reset_index(dropTrue) # 明显错误值处理年龄范围控制在 18~100 df[Age] pd.to_numeric(df[Age], errorscoerce) df.loc[df[Age] 18, Age] pd.NA df.loc[df[Age] 100, Age] pd.NA # 数值列统一转类型并用中位数填充缺失 df[Balance] pd.to_numeric(df[Balance], errorscoerce) df[Balance] df[Balance].fillna(df[Balance].median()) # 类别列标准化 for col in [Gender, Geography]: df[col] df[col].astype(str).str.strip().str.lower()这里我要特别提醒填充缺失值的逻辑必须记录下来。因为在模型上线后实时进来的新数据很可能还是同一个问题——Balance为空、Age带负号这些都要走和训练时一模一样的清洗规则。我见过太多项目训练时精心清洗接口服务里却完全忘了这回事最终导致预测结果和一坨烂数据混在一起谁也说不清是谁的锅。3.2 第二步特征工程与数据集切分注意避坑特征工程这一步我的建议是先不要急着一口气做花哨特征先把最基础的处理做对。第一数值特征要做标准化。为什么不直接用原始数值因为逻辑回归这类线性模型的梯度更新对尺度非常敏感如果某个特征是“收入”量级在几万另一个特征是“活跃天数”量级在几十梯度更新时大尺度特征会主导参数方向模型收敛会变得很慢甚至不收敛。标准化公式就是(x - mean) / std但有个大坑mean 和 std 必须只用训练集来计算切分之后再计算。我在这个环节犯过一个典型的错先对全部数据计算均值方差做标准化再随机切分训练集和测试集。表面看没什么问题但测试集的均值方差已经被“偷看”了模型评估结果会偏乐观上线后一遇到新数据就露馅。正确做法是先把数据集切好再在训练集上计算标准化参数然后把这个参数保存下来应用到测试集和未来的推理数据上。from sklearn.model_selection import train_test_split features [CreditScore, Age, Balance, EstimatedSalary, IsActiveMember] X df[features] y df[Churn] # 先切分stratify保证训练集和测试集的正负比例一致 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, stratifyy, random_state42 ) # 只用训练集算均值方差 mean X_train.mean() std X_train.std() X_train_scaled (X_train - mean) / std X_test_scaled (X_test - mean) / std # 保存标准化参数供服务和上线阶段使用 import joblib joblib.dump({mean: mean, std: std}, models/scaler.joblib)另外stratifyy这个参数非常关键。客户流失数据集里流失样本通常只占20%左右如果不做分层切分随机切分可能会让测试集里几乎没有流失样本最后算出来的准确率虚高但没有业务意义。遇到分类问题我基本都会带stratify。3.3 第三步用NumPy手写逻辑回归搞懂梯度更新这一节是整个项目的灵魂。为什么非要从零写逻辑回归因为只有当你亲手写出损失函数、梯度表达式和参数更新规则你才能理解fit背后在做什么。逻辑回归的预测函数是 sigmoid 函数输出值可以理解为“正类的概率”。损失函数用二元交叉熵再加上 L2 正则避免参数过大。核心代码如下import numpy as np def sigmoid(z): # 防止指数溢出clip到合理范围 z np.clip(z, -20, 20) return 1 / (1 np.exp(-z)) def compute_loss(y, y_prob, w, reg1e-4): eps 1e-12 ce -np.mean(y * np.log(y_prob eps) (1 - y) * np.log(1 - y_prob eps)) l2 reg * np.sum(w ** 2) / 2 return ce l2 def train_logistic(X, y, lr0.1, epochs1000, reg1e-4): n_samples, n_features X.shape w np.zeros(n_features) b 0.0 for epoch in range(epochs): z X w b y_prob sigmoid(z) error y_prob - y # 梯度交叉熵损失对参数的偏导 dw (X.T error) / n_samples reg * w db np.mean(error) w - lr * dw b - lr * db if epoch % 200 0: loss compute_loss(y, y_prob, w, reg) print(fepoch {epoch}, loss {loss:.4f}) return w, b代码逻辑很直接初始化权重为0计算梯度沿负梯度方向更新参数。np.clip那行是为了防止 sigmoid 函数内部算exp(1000)得到无穷大这是数值稳定性里非常常见的处理。训练结束后记得把w和b连同前面保存的mean、std、特征列表一起打包存成一个模型文件后面服务化时只用加载这一个文件就够了。3.4 第四步评估指标与实验记录模型训练完之后别急着高兴。评估阶段最常见的误判是“准确率高 模型好”。在客户流失这种类别不平衡的场景里如果90%的样本是不流失模型只要全部预测成不流失准确率就有90%。所以我要看的是混淆矩阵、精确率、召回率、F1分数这组指标。from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, roc_auc_score y_prob sigmoid(X_test_scaled w b) y_pred (y_prob 0.5).astype(int) print(accuracy:, accuracy_score(y_test, y_pred)) print(precision:, precision_score(y_test, y_pred)) print(recall:, recall_score(y_test, y_pred)) print(f1:, f1_score(y_test, y_pred)) print(auc:, roc_auc_score(y_test, y_prob))说句实话我从零手写的逻辑回归在效果上不一定比sklearn版本强但它的价值在于我能清楚说出每个指标背后对应业务的含义。比如召回率低意味着很多真正流失的客户没被识别出来对运营来说就是“漏了该挽回的人”精确率低则意味着推送了太多无用短信打扰正常人。如果你接的是业务项目评估维度一定要结合业务成本来看而不是盯着一个数字自我满足。同时每个实验都应该留下记录。我会在experiments/exp_001.yaml里写清数据路径、随机种子、学习率、迭代次数、正则系数、最终指标。实际项目里你可能要跑几十个实验没有记录就等于白跑。3.5 第五步用FastAPI把模型包成可调用的服务模型训练完了不能锁死在Notebook里要变成别人能调用的接口。我选择 FastAPI 是因为它自带参数校验和自动生成接口文档请求和响应都能用清晰的JSON格式来定义对前后端协作非常友好。from fastapi import FastAPI from pydantic import BaseModel import joblib import numpy as np app FastAPI() artifact joblib.load(models/logistic_customer_churn.joblib) class Customer(BaseModel): CreditScore: float Age: float Balance: float EstimatedSalary: float IsActiveMember: int app.post(/predict) def predict(customer: Customer): x np.array([[ customer.CreditScore, customer.Age, customer.Balance, customer.EstimatedSalary, customer.IsActiveMember ]]) mean artifact[mean].values std artifact[std].values x_scaled (x - mean) / std z x_scaled artifact[w] artifact[b] p 1 / (1 np.exp(-z)) return { probability: float(p[0]), churn: bool(p[0] 0.5) }接口定义清楚了还要实际启动一次才能确认全链路没问题。命令很简单uvicorn src.serve:app --host 0.0.0.0 --port 8000然后可以模拟一个客户请求来测试curl -X POST http://localhost:8000/predict \ -H Content-Type: application/json \ -d {CreditScore:600,Age:35,Balance:40000,EstimatedSalary:90000,IsActiveMember:1}如果返回了概率和标签恭喜你从数据到模型再到服务的完整闭环已经通了。这一步做完你手里就是一个真正能上线的最小AI应用。4. 从零实测遇到的7类问题及排查方案从零搭项目最大的乐趣就是不断踩坑再爬出来。我把这次真实遇到的高频问题整理成三组方便你按图索骥排查。4.1 数据类问题泄漏、分布漂移、切分不当问题1数据泄漏模型指标虚高。我第一次做标准化时是先整表算 mean 和 std再切分训练测试集。结果训练和测试准确率都高得离谱但上线后立刻原形毕露。根因是测试集信息在预处理阶段就被模型“偷看”了。修复方法是先切分、后拟合标准化参数而且这个参数必须跟随模型一起固化。问题2正负样本比例失衡导致切分失效。不设置stratify时流失样本可能在训练集里只有一点点模型根本没学到流失用户的特征预测结果自然一塌糊涂。修复方法就是确保分类任务里切分时用stratifyy如果数据是时间序列那就要换成时间窗口切分不能用随机切分。问题3预测阶段出现训练时没见过的类别值。比如训练时Geography只有 “france”、 “germany”、 “spain”上线后突然来了一条 “china”。如果用的是 one-hot新类别会变成全零特征向量直接丢掉信息。稳妥的做法是在特征工程阶段先固定类别列表遇到未知类别统一映射到other或者回退到训练集中出现频率最高的那个类别。4.2 训练类问题学习率、梯度、随机种子问题4sigmoid计算溢出导致NaN。训练时如果特征没有标准化或者数值特别大np.exp(1000)直接爆成无穷大loss 变成 NaN。修复方法是给 sigmoid 的输入做np.clip(z, -20, 20)同时在交叉熵内部加一个eps1e-12防止log(0)。问题5loss剧烈震荡不收敛。通常就是学习率太大。我训练loss曲线就像心电图一样上下乱跳时第一反应是lr0.1改成lr0.01再不行就改成0.001。调整的同时也要观察训练轮数epoch太小模型还没完全收敛epoch太大会有冗余计算经验值是先用1000轮观察loss曲线趋势再决定是否提前停止。问题6模型结果无法复现。某一天跑出来的准确率是0.83隔天再跑变成0.79多半是随机种子的锅。数据切分有随机性权重初始化也带随机性。从零搭建时建议在数据切分、任何涉及随机过程的地方都明确设置random_state42并把随机种子写进实验记录文件里。严格来说这不算什么高级技巧但没有它你连自己都骗得过。4.3 工程类问题形状、编码、并发问题7NumPy数组形状不对接口报错。训练时我习惯用(n_samples, n_features)的形状但接口里传入单个样本时经常写成(n_features,)一维数组和二维数组做矩阵乘法时虽然能算但维度语义早就乱了。我现在的习惯是进入模型前先np.array([[...]])强制变成(1, n_features)并在处理完所有字段后打印x.shape做视觉校验。这个小动作能省很多调试时间。问题8类别编码在模型保存后和请求端不一致。训练时处理的是小写字符串接口端传进来的却是首字母大写。这个问题的恶心之处在于很多情况下代码不会报错只是预测结果莫名其妙。解决方案是做一个统一的转换函数训练和请求都走同一个入口绝对避免两边各写一套特征逻辑。5. 把工程化意识补上从“代码能跑”到“稳定运行”5.1 给代码分层把“探索代码”和“生产代码”分开从零搭完这个项目后我明显感觉到代码组织方式决定了你后期维护的幸福感。Notebook是探索空间适合画图、看分布、试特征但到训练和服务阶段我会把稳定逻辑抽到src/下的 .py 模块里。比如data_prep.py负责清洗features.py负责特征加工train.py负责训练和保存产物serve.py负责接口推理。每个模块的职责单一互相之间只通过明确的输入输出契约交流。这样分层的直接好处是你能对每一层单独做测试。比如接口服务出问题时先查是不是特征加工和训练时不一致再查是不是模型文件没更新而不是一头扎进几万行代码里找逻辑。5.2 模型、数据与参数做版本管理确保能回滚真实业务中“改了模型导致指标下降”这种事太常见了。所以我现在对模型产物、数据文件和实验参数都有版本意识。数据文件至少带日期后缀模型文件名包含训练日期和关键指标比如logistic_customer_churn_20250611_acc0.84.joblib。实验参数全部存到experiments/下的YAML里配合Git历史想回滚到上一版模型就只需要改一下服务里的模型路径。这里有个经验永远保留至少上一个版本的模型文件。不要每次训练完就覆盖旧的。模型回滚的成本往往比你想象的高一旦新模型上线后业务反馈异常能在十分钟内切回旧模型比现场调参救火要靠谱得多。5.3 日志、监控与可观测性不能省模型服务上线只是开始不是结束。我用 Python 内置的logging给接口增加了请求日志记录请求时间、输入特征摘要、预测概率、停留时间。刚开始你可能会觉得“多此一举”真遇到线上问题后你就会感谢这些日志。因为你需要回答的问题是“昨天下午三点到四点为什么这批客户的预测率突然偏高”没有日志你只能猜。更进一步可以用简单的统计量监控特征漂移。每隔一段时间把线上推理数据的特征分布和训练集的分布做个对比如果某个特征的均值和方差明显偏移说明线上环境的数据已经变了模型需要重新评估。这里不需要巨复杂的平台一个定时脚本加一张报表就能管事。5.4 文档和复现清单为自己留一个“逃生通道”最后一个工程化习惯是写文档。不一定要写长篇大论而是把“别人能复现这个项目”需要的东西写清楚安装步骤、数据来源、数据清洗规则、训练命令、启动服务命令、实验记录表。我在 README 里会写清完整流程在TROUBLE_SHOOTING.md里记录常见坑和对应解法。这个东西在团队协作里特别重要否则别人接手你的模型项目时光靠读代码至少要消耗两三天而且极易踩进你已经踩过的坑。写文档这件事本质上是为未来的自己留下一张逃生地图。换一个角度想AI工程里的“工程”二字很大程度就体现在这些容易被忽略的规范里。现在再回头看这个 ai-engineering-from-scratch 项目最值钱的不是那个准确率刚过0.84的模型而是我终于能在黑盒出错时按图索骥找到具体环节了。最明显的改变是遇到线上预测异常我不再一头雾水地重启服务而是能冷静地沿着“数据清洗 → 特征工程 → 标准化 → 模型参数 → 接口输入”这条链路逐段检查。如果你也想自己动手跑一遍我的建议是把目标定小一点先做出一个最小闭环让模型能训练、接口能调用再逐步加数据、加监控、加优化。踩坑的过程虽然笨拙但每一坑都会逼你看清这个环节的底层逻辑。这条路走完你学到的就不只是“怎么用AI”而是“怎么把一个AI项目稳稳立起来”。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑