1. 从零开始构建AI工程体系:这不是写几个模型脚本,而是重建你的技术认知坐标系
“AI Engineering from Scratch”——这个标题乍看像是一门课程名,或是某本新书的副标题。但如果你在一线做过三年以上AI相关落地项目,看到这八个单词的第一反应不会是“哦,又一个教人调包的教程”,而是心里一紧:终于有人把这件事拎出来单独讲了。它不是“用PyTorch训练一个MNIST分类器”,也不是“十分钟部署一个LangChain聊天机器人”。它是当你手头只有一台刚重装系统的笔记本、一个空的GitHub仓库、一份模糊的业务需求文档时,如何从第一行代码开始,搭建起一套能长期演进、可协作、可监控、可回滚、能经受住生产环境压力的AI系统骨架。核心关键词里反复出现的python、typescript、rust、julia,绝非随意堆砌——它们各自承担着不可替代的工程角色:Python是算法实验与数据胶水的母语;TypeScript是前端交互、API编排与工程化约束的守门人;Rust是高性能推理服务、内存敏感模块与基础设施层的压舱石;Julia则是科学计算密集型任务(如微分方程求解、物理仿真耦合、高维优化)中越来越不可忽视的加速引擎。我带过6个AI产品团队,每次新项目启动,最耗时、最易返工、最让初级工程师崩溃的,从来不是模型精度调优,而是这四类语言如何在同一个CI/CD流水线里和平共处、如何定义跨语言的数据契约、如何让Rust写的推理服务被TypeScript前端稳定调用、又如何让Julia生成的数值解能被Python训练流程无缝消费。这篇文章不讲“怎么安装Python”,而是讲清楚:当你敲下mkdir ai-engineering-from-scratch那一刻起,接下来72小时里,你该优先建立哪5个目录结构、为什么pyproject.toml比requirements.txt更适合现代Python工程、TypeScript的tsconfig.json里哪3个配置项直接决定你未来半年的调试效率、Rust的Cargo.toml中[profile.release]的lto = "fat"设置如何影响GPU推理延迟、Julia的Project.toml与Manifest.toml双文件机制怎样避免“在我机器上跑得好好的”这类经典灾难。这不是语言对比评测,而是一份面向真实战场的工程基建地图。
2. 整体架构设计:为什么必须放弃“单语言单仓库”幻觉
2.1 真实AI产品的技术栈从来不是扁平的
很多初学者误以为AI工程就是“Python写模型+Flask搭API+Vue做前端”,这种理解在Kaggle竞赛或课程作业中尚可运转,但一旦进入真实业务场景——比如为一家区域电网公司开发负荷预测系统,或为医疗器械厂商构建影像辅助诊断模块——就会立刻暴露致命缺陷。我亲身经历过的最典型反例,是一个医疗NLP项目:团队用Python训练BERT微调模型,用FastAPI暴露REST接口,前端用Vue调用。上线后发现,当并发请求超过200QPS时,API响应延迟从200ms飙升至3s以上,CPU使用率持续98%,日志里全是OSError: [Errno 24] Too many open files。排查发现,问题根源不在模型本身,而在FastAPI默认的异步事件循环与Python的GIL(全局解释器锁)在高并发IO场景下的协同失效;更深层的是,整个服务缺乏对GPU显存的精细化管理,多个请求排队等待同一块显卡,而Python生态中缺乏真正可靠的显存隔离机制。最终解决方案不是“换框架”,而是将核心推理逻辑下沉到Rust层,用tract库加载ONNX模型,通过tokio实现真正的异步GPU调度,再用TypeScript封装成gRPC客户端供前端调用。这个过程暴露出一个根本事实:AI工程的本质是多语言协同的系统工程,而非单一语言的算法实现。Python擅长快速验证想法,但它不是为高吞吐、低延迟、强一致性的生产服务而生;TypeScript能提供卓越的类型安全和前端体验,但它无法直接操作GPU内存;Rust能提供极致性能和内存安全,但它缺乏成熟的科学计算生态;Julia在数值计算上快如闪电,但它的包管理器和IDE支持仍处于追赶阶段。因此,“from scratch”的第一步,不是选语言,而是画出一张清晰的能力-语言映射图。
2.2 四语言分工的底层逻辑与边界划定
我们来拆解这四种语言在AI工程中的不可替代性,以及它们之间必须划清的“责任边界”:
Python:作为“胶水层”与“实验中枢”
它的核心价值不在于速度,而在于生态密度与迭代速度。scikit-learn、pandas、matplotlib、transformers、lightning这些库构成了AI研发的“乐高积木”。但关键在于,Python应严格限定在离线训练、数据预处理、模型验证、实验记录(MLflow/W&B)这些“非实时、非高并发、允许分钟级延迟”的环节。一旦涉及在线服务、流式推理、资源强约束场景,就必须将其剥离。我见过太多团队把joblib并行训练代码直接扔进Flask路由里,结果服务器每小时重启一次——这不是代码bug,而是架构误判。TypeScript:作为“契约层”与“交互中枢”
它的使命是定义清晰的接口契约、保障前后端数据一致性、提供可维护的UI逻辑。在AI工程中,它绝不只是“写页面”。一个典型的TypeScript模块可能包含:1)基于OpenAPI规范自动生成的API客户端(用swagger-typescript-api),确保前端调用参数与后端Swagger文档100%同步;2)用Zod库定义的严格数据校验Schema,拦截90%以上的无效输入;3)基于React Query的状态管理,自动处理模型推理请求的loading、error、stale状态。更重要的是,TypeScript的declare module语法能让你为Python后端生成的.pyi类型存根文件提供前端映射,实现真正的“全栈类型安全”。这远比“用JSON传数据”可靠得多。Rust:作为“性能层”与“基础设施层”
它解决的是Python和TypeScript无法逾越的物理极限:毫秒级延迟、确定性内存占用、无GC停顿、跨平台二进制分发。在AI工程中,Rust的典型落点包括:1)模型推理服务(用burn或tract加载ONNX/TensorRT模型);2)实时数据流处理(用polars的Rust内核做流式特征计算);3)硬件抽象层(如与CUDA驱动、OPC UA工业协议、嵌入式传感器的直接通信)。一个关键经验是:Rust代码不应直接暴露给业务逻辑,而应通过FFI(Foreign Function Interface)或gRPC提供标准化服务。我们曾用Rust重写一个Python的实时异常检测模块,性能提升17倍,内存峰值下降63%,但更重要的是,它让整个服务的P99延迟从1.2s稳定在87ms以内——这对工业预测性维护场景是生死线。Julia:作为“计算层”与“数学建模层”
它的价值在于将数学公式直接翻译为高效代码的能力。当你的问题本质是求解偏微分方程、进行大规模稀疏矩阵迭代、或执行蒙特卡洛路径积分时,Julia的多重分派(Multiple Dispatch)和JIT编译器能让你写出接近伪代码的简洁表达,却获得C/Fortran级别的性能。例如,在金融衍生品定价中,用Julia的DifferentialEquations.jl求解Heston模型,代码行数只有同等Python+NumPy方案的1/3,运行速度却是其5倍。但必须清醒:Julia不是通用Web开发语言,它的强项是“计算密集型子任务”,应通过PyCall.jl或HTTP.jl暴露为独立微服务,由Python主流程调用,而非试图用它重构整个应用。
提示:四种语言的协作不是“谁取代谁”,而是“谁负责哪一段物理世界”。Python管“想得对不对”,TypeScript管“用户看得清不清”,Rust管“跑得快不快”,Julia管“算得准不准”。任何试图用单一语言覆盖全部环节的方案,终将在规模增长时付出指数级的维护成本。
2.3 目录结构即架构宣言:从第一天就拒绝混乱
一个健康的AI工程仓库,其目录结构本身就是一份无声的架构说明书。我们摒弃传统的src/单目录模式,采用领域驱动的分层结构:
ai-engineering-from-scratch/ ├── docs/ # 架构决策记录(ADR)、接口文档、部署手册 ├── infra/ # IaC代码(Terraform/Pulumi)、Dockerfile、K8s manifest ├── python/ # Python核心:训练脚本、数据管道、实验管理 │ ├── src/ # 可安装包(pyproject.toml定义) │ ├── notebooks/ # Jupyter实验记录(禁止提交数据文件) │ └── tests/ # pytest测试,覆盖数据加载、特征工程、模型评估 ├── typescript/ # TypeScript核心:前端应用、API客户端、类型定义 │ ├── apps/ # Next.js/Vite应用 │ ├── packages/ # 可复用的UI组件库、工具函数库 │ └── proto/ # Protocol Buffers定义(用于gRPC) ├── rust/ # Rust核心:推理服务、CLI工具、性能关键模块 │ ├── inference-service/ # 基于Axum/Tokio的gRPC服务 │ ├── cli-tools/ # 数据校验、模型转换等命令行工具 │ └── crates/ # 可复用的Rust库(如自定义张量操作) ├── julia/ # Julia核心:数值仿真、优化求解、物理建模 │ ├── models/ # 微分方程求解器、随机过程模拟器 │ └── interfaces/ # 通过HTTP或ZeroMQ与Python/Rust通信 ├── scripts/ # 跨语言协作脚本(如:用Python生成Julia输入,调用Rust服务) └── .github/ # 统一的CI/CD工作流(触发多语言测试)这个结构的关键在于物理隔离与逻辑耦合的平衡。每个语言子目录都是独立的Git子模块(可选),拥有自己的Cargo.toml、pyproject.toml、package.json和Project.toml,但通过scripts/中的协调脚本和infra/中的统一部署配置实现集成。例如,python/src/train.py训练完模型后,会调用scripts/export-to-onnx.sh将模型转为ONNX格式,再触发rust/inference-service的CI构建;而typescript/apps/dashboard则通过packages/api-client调用该服务的gRPC接口。这种设计让新人入职第一天就能看清:“我要改的是哪个部分?它依赖什么?会影响什么?”——这才是工程化的起点。
3. 核心细节解析:从环境初始化到跨语言数据流
3.1 Python工程化:告别pip install,拥抱现代Python包管理
很多团队还在用pip install -r requirements.txt,这在2024年已是重大技术债。requirements.txt本质是“已安装包的快照”,它无法解决依赖冲突、无法声明可选依赖、无法指定构建后端。现代Python工程必须以pyproject.toml为唯一真相源。
我们以一个典型的AI训练模块为例,其pyproject.toml核心配置如下:
[build-system] requires = ["setuptools>=45", "wheel", "setuptools_scm[toml]>=6.2"] build-backend = "setuptools.build_meta" [project] name = "ai-engineering-core" version = "0.1.0" description = "Core training and evaluation logic" authors = [{name = "Your Name", email = "you@example.com"}] readme = "README.md" requires-python = ">=3.10" dependencies = [ "torch>=2.0.0", "transformers>=4.30.0", "datasets>=2.14.0", "mlflow>=2.8.0", "pandas>=2.0.0", "numpy>=1.24.0", ] optional-dependencies = { dev = ["pytest>=7.0.0", "black>=23.0.0", "mypy>=1.0.0"], viz = ["matplotlib>=3.7.0", "seaborn>=0.12.0"], } [project.optional-dependencies] dev = ["pytest>=7.0.0", "black>=23.0.0", "mypy>=1.0.0"] viz = ["matplotlib>=3.7.0", "seaborn>=0.12.0"] [project.urls] Homepage = "https://github.com/your-org/ai-engineering-from-scratch" Repository = "https://github.com/your-org/ai-engineering-from-scratch" [tool.setuptools] packages = ["ai_engineering_core"] [tool.black] line-length = 88 skip-string-normalization = true [tool.mypy] python_version = "3.10" disallow_untyped_defs = true disallow_incomplete_defs = true disallow_untyped_decorators = true warn_return_any = true warn_unused_configs = true关键点解析:
build-system.requires指定了构建所需的最小工具集,确保任何机器都能正确构建。project.dependencies声明了运行时必需依赖,optional-dependencies则将开发和可视化依赖分离,避免生产环境安装matplotlib。tool.black和tool.mypy将代码风格和类型检查固化为项目标准,而非个人习惯。setuptools_scm启用基于Git标签的版本管理,git tag v0.1.0后,pip install .会自动识别版本号,无需手动修改__version__。
实操心得:我强制团队所有Python包都采用src/布局(即源码在src/目录下),这能彻底避免“当前目录即包”的陷阱。pip install -e .安装后,无论你在仓库哪个目录下运行python -c "import ai_engineering_core",都能成功导入。这是保证CI/CD中路径无关性的基础。
3.2 TypeScript工程化:类型即文档,接口即契约
TypeScript的威力不在语法糖,而在它能把“口头约定”变成可执行的机器校验。一个AI前端项目,其tsconfig.json必须包含以下核心配置:
{ "compilerOptions": { "target": "ES2020", "lib": ["DOM", "ES2020"], "module": "ESNext", "skipLibCheck": false, "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "resolveJsonModule": true, "isolatedModules": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "moduleResolution": "node", "baseUrl": ".", "paths": { "@/*": ["src/*"], "@api/*": ["src/api/*"], "@components/*": ["src/components/*"] }, "plugins": [ { "name": "typescript-plugin-css-modules", "options": { "classNameConvention": "camelCase" } } ] }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] }最关键的三个配置项:
"strict": true:开启所有严格检查,包括noImplicitAny、strictNullChecks等。这是TypeScript价值的基石,关闭它等于放弃类型安全。"paths":定义路径别名,避免../../../../utils这种反人类导入。配合VS Code的jsconfig.json,能实现完美的路径跳转。"plugins":引入CSS Modules插件,让.module.css文件的类名自动转为类型安全的对象,杜绝className="button"这种硬编码错误。
更进一步,我们用Zod定义所有API响应类型:
// src/api/schemas/inference.ts import { z } from 'zod'; export const InferenceRequestSchema = z.object({ model_id: z.string().min(1), input_data: z.array(z.number()).min(10).max(1000), temperature: z.number().min(0).max(2).default(0.7), }); export const InferenceResponseSchema = z.object({ prediction: z.array(z.number()), confidence: z.number().min(0).max(1), latency_ms: z.number().int(), model_version: z.string(), }); export type InferenceRequest = z.infer<typeof InferenceRequestSchema>; export type InferenceResponse = z.infer<typeof InferenceResponseSchema>;然后在API客户端中强制使用:
// src/api/client.ts import axios from 'axios'; import { InferenceRequestSchema, InferenceResponseSchema } from './schemas/inference'; export const inferenceApi = { predict: async (req: InferenceRequest) => { // 先校验输入 const validatedReq = InferenceRequestSchema.parse(req); const res = await axios.post('/api/v1/predict', validatedReq); // 再校验输出 const validatedRes = InferenceResponseSchema.parse(res.data); return validatedRes; }, };这样做的好处是:1)前端调用inferenceApi.predict({model_id: "v1", input_data: [1,2,3]})时,如果input_data长度不足10,TypeScript会在编辑器里立刻报错;2)后端返回的latency_ms如果是字符串,Zod校验会失败并抛出明确错误,而不是让前端拿到NaN后默默崩溃。这就是“类型即文档”的力量。
3.3 Rust工程化:从Cargo.toml到生产级服务配置
Rust的Cargo.toml不仅是依赖管理,更是性能调优的入口。一个用于AI推理的Rust服务,其Cargo.toml需重点关注:
[package] name = "inference-service" version = "0.1.0" edition = "2021" [dependencies] axum = { version = "0.6", features = ["full"] } tokio = { version = "1.0", features = ["full"] } tract-onnx = "0.24" ndarray = "0.15" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "1.0" tracing = "0.1" tracing-subscriber = "0.3" hyper = "1.0" [dev-dependencies] tokio = { version = "1.0", features = ["test-util"] } [profile.release] opt-level = 3 lto = "fat" # 启用全链接时优化,显著降低二进制大小和提升性能 codegen-units = 1 # 强制单线程编译,牺牲编译速度换取最佳优化 panic = "abort" # 生产环境禁用栈展开,减少二进制体积 strip = true # 自动剥离调试符号lto = "fat"是关键:它让链接器对整个程序进行跨crate优化,对于tract-onnx这种重度依赖底层向量指令的库,能带来15%-20%的性能提升。codegen-units = 1虽会让cargo build --release变慢,但生成的二进制在ARM64服务器上启动时间缩短了300ms——这对需要快速扩缩容的K8s环境至关重要。
服务启动代码需集成tracing进行结构化日志:
// src/main.rs use axum::{Router, routing::get, Json}; use serde::{Deserialize, Serialize}; use tracing::{info, error}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; #[derive(Deserialize)] struct PredictRequest { model_id: String, input_data: Vec<f32>, } #[derive(Serialize)] struct PredictResponse { prediction: Vec<f32>, latency_ms: f64, } async fn predict(Json(req): Json<PredictRequest>) -> Result<Json<PredictResponse>, axum::http::StatusCode> { info!("Received prediction request for model {}", req.model_id); // 模拟模型推理(实际调用tract) let start = std::time::Instant::now(); let result = vec![0.1, 0.2, 0.3]; // placeholder let latency = start.elapsed().as_millis() as f64; Ok(Json(PredictResponse { prediction: result, latency_ms: latency, })) } #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { // 初始化tracing tracing_subscriber::fmt() .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) .init(); let app = Router::new().route("/predict", get(predict)); let addr = std::net::SocketAddr::from(([0, 0, 0, 0], 3000)); info!("Starting inference service on {}", addr); axum::Server::bind(addr) .serve(app.into_make_service()) .await .unwrap(); Ok(()) }实操心得:Rust服务必须通过tracing而非println!打日志。tracing的结构化日志能被loki或datadog直接解析,info!(?req)这样的语法能自动序列化整个请求对象,极大提升线上问题排查效率。我们曾用此方案将一次GPU显存泄漏的定位时间从8小时缩短到22分钟。
3.4 Julia工程化:Project.toml与跨语言桥接
Julia的包管理是其最大优势,也是最大陷阱。Project.toml和Manifest.toml双文件机制确保了环境的绝对可重现性,但新手常因忽略Manifest.toml而陷入“在我机器上跑得好好的”困境。
一个Julia数值求解模块的Project.toml示例:
[deps] DifferentialEquations = "0c46a032-eb83-5123-abe9-813cca98fedf" Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" DataFrames = "a93c6f00-e57d-5684-b7b6-82b956931c18" HTTP = "cd3b7a88-5590-5517-8580-45245224144a" JSON = "682c06a0-6aef-4fcd-a109-2ce362c15740" [compat] DifferentialEquations = "7.0" Plots = "1.30" DataFrames = "1.5"关键原则:
Project.toml声明语义化版本范围(如DifferentialEquations = "7.0"表示兼容7.x.x),这是人类可读的依赖策略。Manifest.toml记录精确的commit hash和完整依赖树,这是机器可执行的环境快照。它必须提交到Git,否则CI无法复现环境。
跨语言调用是Julia工程化的难点。我们采用HTTP REST作为最简单可靠的桥接方式:
# julia/models/heat_equation.jl using DifferentialEquations, Plots, JSON, HTTP function solve_heat_equation(; dx=0.1, dt=0.01, tspan=(0.0, 1.0)) # ... 数值求解逻辑 ... return solution end # 启动一个轻量HTTP服务 HTTP.serve() do req if req.method == "POST" && req.target == "/solve" try data = JSON.parse(String(req.body)) dx = get(data, "dx", 0.1) dt = get(data, "dt", 0.01) tspan = (get(data, "t0", 0.0), get(data, "t1", 1.0)) result = solve_heat_equation(dx=dx, dt=dt, tspan=tspan) response = Dict( "status" => "success", "solution" => Array(result.u), # 转为普通数组 "times" => Array(result.t) ) HTTP.Response(200, JSON.json(response)) catch e HTTP.Response(400, JSON.json(Dict("error" => string(e)))) end else HTTP.Response(404, "Not Found") end endPython端调用:
# python/src/integrate_julia.py import requests import numpy as np def call_julia_heat_solver(dx=0.1, dt=0.01, t0=0.0, t1=1.0): url = "http://localhost:8080/solve" payload = {"dx": dx, "dt": dt, "t0": t0, "t1": t1} response = requests.post(url, json=payload) response.raise_for_status() data = response.json() if data["status"] != "success": raise RuntimeError(f"Julia solver failed: {data['error']}") return np.array(data["solution"]), np.array(data["times"])注意:Julia的
Array直接转为JSON时会丢失维度信息,因此必须用Array(result.u)强制降维。这是跨语言数据序列化的常见坑,务必在文档中明确标注。
4. 实操过程:72小时构建可运行的AI工程骨架
4.1 第1小时:初始化仓库与基础环境
目标:创建空仓库,完成四种语言的基础环境验证。
步骤:
mkdir ai-engineering-from-scratch && cd ai-engineering-from-scratchgit init && echo "# AI Engineering from Scratch" > README.md && git add . && git commit -m "init repo"- 创建各语言子目录:
mkdir python typescript rust julia docs infra scripts - Python环境验证:
cd python && python -m venv .venv && source .venv/bin/activate(macOS/Linux)或.venv\Scripts\activate.bat(Windows)pip install --upgrade pip setuptools wheelpip install hatch(现代Python项目管理工具)hatch new ai-engineering-core→ 自动生成pyproject.toml和src/结构hatch run test:pytest --version验证pytest可用
- TypeScript环境验证:
cd ../typescript && npm init -y && npm install -D typescript ts-node @types/nodenpx tsc --init生成tsconfig.json,按前文配置修改echo 'console.log("TypeScript OK");' > index.ts && npx ts-node index.ts
- Rust环境验证:
cd ../rust && curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh(如未安装)rustc --version && cargo --versioncargo new inference-service --bin && cd inference-service && cargo run
- Julia环境验证:
cd ../julia && julia --version(如未安装,从julialang.org下载)julia -e 'using Pkg; Pkg.generate("HeatSolver")'cd HeatSolver && julia --project -e 'using .HeatSolver; println("Julia OK")'
避坑指南:
- Windows用户注意:PowerShell默认执行策略禁止运行脚本,需先执行
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser。 - Julia首次启动会预编译大量包,可能耗时5-10分钟,耐心等待,勿强行中断。
- 所有环境验证必须在各自子目录下进行,避免路径污染。这是后续CI/CD能正常工作的前提。
4.2 第2-12小时:构建Python训练流水线与数据契约
目标:完成一个端到端的模型训练脚本,输出标准化模型文件,并生成ONNX格式。
我们以一个简化的时序预测任务为例(预测未来24小时电力负荷):
数据准备:
- 在
python/data/raw/下放置load_history.csv(含timestamp,load_kw两列,1年历史数据) - 编写
python/scripts/preprocess.py进行标准化、滑动窗口切片:
# python/scripts/preprocess.py import pandas as pd import numpy as np from sklearn.preprocessing import StandardScaler import torch def create_sequences(data, seq_len=168, pred_len=24): """将时间序列转为监督学习格式:[seq_len] -> [pred_len]""" X, y = [], [] for i in range(len(data) - seq_len - pred_len + 1): X.append(data[i:i+seq_len]) y.append(data[i+seq_len:i+seq_len+pred_len]) return np.array(X), np.array(y) def main(): df = pd.read_csv("data/raw/load_history.csv", parse_dates=["timestamp"]) df = df.set_index("timestamp").resample("1H").mean().fillna(method="ffill") scaler = StandardScaler() scaled_data = scaler.fit_transform(df[["load_kw"]]).flatten() X, y = create_sequences(scaled_data) # 保存处理后的数据和scaler np.save("data/processed/X.npy", X) np.save("data/processed/y.npy", y) import joblib joblib.dump(scaler, "data/processed/scaler.joblib") print(f"Preprocessing done: {X.shape[0]} samples") if __name__ == "__main__": main()模型训练:
python/src/models/lstm.py定义PyTorch LSTM模型:
# python/src/models/lstm.py import torch import torch.nn as nn class LoadPredictor(nn.Module): def __init__(self, input_size=1, hidden_size=64, num_layers=2, output_size=24): super().__init__() self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True) self.fc = nn.Linear(hidden_size, output_size) def forward(self, x): # x: [batch, seq_len, features] lstm_out, _ = self.lstm(x) # lstm_out: [batch, seq_len, hidden_size] # 取最后一个时间步的输出 last_output = lstm_out[:, -1, :] # [batch, hidden_size] return self.fc(last_output) # [batch, output_size]python/src/train.py执行训练:
# python/src/train.py import torch import torch.optim as optim from torch.utils.data import DataLoader, TensorDataset from sklearn.metrics import mean_absolute_error import numpy as np from models.lstm import LoadPredictor def train(): X = np.load("data/processed/X.npy") y = np.load("data/processed/y.npy") # 转为tensor X_tensor = torch.tensor(X, dtype=torch.float32).unsqueeze(-1) # [N, seq_len, 1] y_tensor = torch.tensor(y, dtype=torch.float32) dataset = TensorDataset(X_tensor, y_tensor) train_loader = DataLoader(dataset, batch_size=32, shuffle=True) model = LoadPredictor() criterion = nn.MSELoss() optimizer = optim.Adam(model.parameters(), lr=0.001) for epoch in range(10): model.train() total_loss = 0 for X_batch, y_batch in train_loader: optimizer.zero_grad() y_pred = model(X_batch) loss = criterion(y_pred, y_batch) loss.backward() optimizer.step() total_loss += loss.item() if epoch % 2 == 0: print(f"Epoch {epoch}, Loss: {total_loss/len(train_loader):.4f}") # 保存模型 torch.save(model.state_dict(), "models/lstm_state_dict.pth") # 导出ONNX dummy_input = torch.randn(1, 168, 1) torch.onnx.export( model, dummy_input, "models/lstm.onnx", input_names=["input"], output_names=["output"], dynamic_axes={"input": {0: "batch_size"}, "output": {0: "batch_size"}}, opset_version=12 ) print("Model exported to ONNX") if __name__ == "__main__": train()关键产出:
models/lstm_state_dict.pth:PyTorch原生模型,用于继续训练或研究。models/lstm.onnx:标准化中间表示,可被Rust/Python/Java等多种语言加载。data/processed/scaler.joblib:标准化器,必须与模型一起部署,用于输入预处理和输出反标准化。
4.3 第13-36小时:Rust推理服务与gRPC接口
目标:用Rust加载ONNX模型,提供gRPC服务,接受标准化输入,返回预测结果。
步骤:
cd rust && cargo new inference-service --bin && cd inference-service- 修改
Cargo.toml,添加依赖:
[dependencies] tonic = "0.10" prost = "0.12" tokio = { version = "1.0", features = ["full"] } tract-onnx = "0.24" ndarray = "0.15" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "1.0" tracing = "0.1" tracing-subscriber = "0.3"- 定义Protocol Buffers:在
rust/proto/inference.proto中:
syntax = "proto3"; package inference; service PredictionService { rpc Predict (PredictRequest) returns (PredictResponse