
1. 为什么“从零构建AI工程”不是一句口号而是必须直面的现实困境“AI Engineering from Scratch”——这个标题乍看像极了技术圈里常见的营销话术一个带点极客气质、略显高冷的短语常出现在课程广告、开源项目README或某篇Medium长文中。但如果你真在一家年营收过亿的制造业企业做过AI落地或者在医疗影像初创公司带过算法交付团队你就会明白这八个单词背后不是炫技而是一整套被教科书刻意忽略、被大厂文档有意简化的生存手册。我第一次真正意识到“from scratch”的分量是在2022年Q3接手一个工业缺陷检测项目。客户给的原始需求只有两行字“用AI识别产线上PCB板的焊点虚焊”。我们按常规流程走数据采集→标注→训练ResNet-50模型→部署到边缘盒子。结果上线第三天产线停机两次——不是模型不准而是标注团队用的LabelImg导出的JSON格式和后端推理服务期待的COCO格式字段名不一致更致命的是边缘盒子固件版本不支持ONNX Runtime 1.12而我们训练环境用的是1.15。没人教过你“训练一个准确率92%的模型”和“让这个模型在真实产线连续稳定运行72小时”之间横亘着至少17个非算法环节。这些环节没有Loss函数可优化没有梯度可下降全靠人肉踩坑、文档考古、跨部门扯皮填平。这就是“AI Engineering from Scratch”的真实底色它不承诺给你一个开箱即用的SaaS界面也不提供封装好的“一键部署”按钮。它要求你亲手拧紧每一颗螺丝——从GPU驱动版本兼容性检查到Docker镜像中glibc的ABI对齐从标注平台导出数据的字符编码UTF-8 BOM到模型服务API响应头里Content-Type的精确写法。关键词“ai-engineering”指向的从来不是算法本身而是让算法在复杂现实约束下持续产生业务价值的整套工程化能力而“from-scratch”则是一个残酷的提醒别指望现成的轮子能直接装上你的车——你得先确认车轴直径、轮毂螺距、轴承公差再决定是重铸一个轮子还是把现有轮子车削改造。这个过程之所以无法跳过根源在于AI系统的三重异质性数据异质性产线相机拍的灰度图 vs 实验室RGB图光照、噪声、分辨率全不同、系统异质性训练用K8s集群 vs 部署用ARM嵌入式设备、组织异质性算法工程师写的Python脚本 vs 运维要求的systemd服务单元文件。任何试图用单一工具链覆盖全部场景的方案最终都会在某个异质性交界处崩塌。所以“from scratch”不是复古情怀而是面对现实复杂性的唯一诚实姿态——它承认没有银弹只有针对具体约束的定制解法。提示当你听到“我们用MLOps平台统一管理模型生命周期”时请立刻追问三个问题1该平台如何处理训练环境与生产环境CUDA版本差异2当标注数据因网络中断只上传了87%时平台是否提供原子性回滚机制3平台生成的Dockerfile是否明确声明基础镜像的SHA256哈希值如果任一问题答不上来“统一管理”大概率只是PPT上的美好愿景。2. 构建AI工程基座从裸金属到可复现环境的七层阶梯很多人误以为“from scratch”等于从Linux内核编译开始。其实不然。真正的起点是定义你的最小可行工程基座Minimum Viable Engineering Base, MVEB——即保证后续所有AI开发活动能稳定、可复现、可协作进行的最简基础设施集合。这个基座不是越重越好而是越精准匹配业务约束越好。我见过太多团队在没想清楚“我们要在什么硬件上跑、由谁来维护、数据从哪来”之前就急着部署MLflow或Kubeflow结果半年后集群成了无人敢动的“祖传配置”。2.1 第一层硬件抽象层——GPU驱动与CUDA的“血缘关系”一切始于物理设备。但“安装NVIDIA驱动”绝非执行apt install nvidia-driver-535那么简单。关键在于驱动版本、CUDA Toolkit版本、cuDNN版本、PyTorch/TensorFlow版本四者间的精确兼容矩阵。这不是理论问题而是实操中的生死线。例如RTX 4090发布初期官方驱动470.x系列根本不识别该卡必须升级到515而CUDA 11.8又要求驱动520.61.05。若此时你强行用CUDA 11.7编译PyTorch即使编译成功运行时也会在torch.cuda.is_available()返回False——因为底层CUDA Driver API调用失败。我的做法是建立一张硬件-驱动-CUDA映射表并固化为CI检查项GPU型号推荐驱动版本兼容CUDA最高版对应PyTorch命令A100 PCIe515.65.0111.8pip3 install torch2.0.1cu118 ...RTX 4090525.85.1212.0pip3 install torch2.1.0cu120 ...Jetson OrinL4T 35.3.111.4pip3 install torch2.0.0nv23.05 ...注意表格中PyTorch命令必须包含cuXXX后缀这是官方预编译二进制包的标识。若用源码编译需额外指定TORCH_CUDA_ARCH_LIST8.6对应A100或8.9对应4090否则编译出的库在目标GPU上会触发illegal instruction崩溃。2.2 第二层环境隔离层——Conda与Docker的协同边界Python生态的依赖地狱Dependency Hell是AI工程最大痛点之一。Conda解决包冲突Docker解决系统级冲突但二者分工常被混淆。我的经验是Conda负责开发环境的快速迭代Docker负责生产环境的绝对确定性。开发阶段用environment.yml定义Conda环境明确指定python3.9、pytorch2.0.1、opencv4.8.0等精确版本。关键技巧是启用conda env export --from-history environment.yml而非conda env export——前者只导出你手动conda install的包后者会导出所有传递依赖导致环境臃肿且不可控。生产阶段Dockerfile必须基于NVIDIA官方cuda:11.8.0-devel-ubuntu20.04镜像而非python:3.9-slim。原因在于devel镜像预装了nvcc、cudnn.h等头文件而slim镜像需要你手动下载cuDNN tarball并解压极易出错。Dockerfile核心片段如下FROM nvidia/cuda:11.8.0-devel-ubuntu20.04 # 安装系统级依赖 RUN apt-get update apt-get install -y \ libsm6 libxext6 libxrender-dev libglib2.0-0 \ rm -rf /var/lib/apt/lists/* # 复制Conda环境文件并创建环境 COPY environment.yml . RUN conda env create -f environment.yml \ conda clean --all -f -y # 激活环境并安装生产专用包 SHELL [conda, run, -n, myenv, /bin/bash, -c] RUN pip install gunicorn uvicorn # 设置工作目录和入口 WORKDIR /app COPY . . ENTRYPOINT [conda, run, -n, myenv, gunicorn, --bind, 0.0.0.0:8000, app:api]这里的关键设计是Docker镜像内不激活Conda环境而是用conda run命令动态调用。这避免了source activate带来的shell初始化问题确保每次启动都是纯净环境。2.3 第三层数据管道层——从原始字节到可训练张量的原子操作数据是AI的燃料但“燃料”必须经过严格提纯才能燃烧。我见过最典型的错误是把数据清洗脚本写成Jupyter Notebook里的随意操作df pd.read_csv(data.csv); df.dropna(); df.to_parquet(clean.parquet)。这种做法在单机调试时无害一旦接入CI/CD就会暴露三大缺陷1CSV读取默认encodingutf-8遇到含BOM的Excel导出文件直接报错2dropna()删除整行可能误删关键样本3Parquet文件无schema校验下游模型加载时类型不匹配。正确的数据管道必须满足原子性、幂等性、可观测性原子性每个清洗步骤封装为独立函数输入输出均为明确类型。例如def validate_image_path(path: str) - bool:而非在主流程里写if not os.path.exists(...)。幂等性同一份原始数据多次运行管道产出完全一致。这意味着禁止使用random.shuffle()改用random.seed(42); random.shuffle(list, seed42)禁止依赖当前时间戳改用数据集元数据中的acquisition_time字段。可观测性每步操作记录统计摘要。我在每个清洗函数末尾添加def resize_images(input_dir: Path, output_dir: Path, size: Tuple[int, int]): # ... processing logic ... stats { input_count: len(input_files), output_count: len(output_files), avg_input_size_kb: sum(os.path.getsize(f) for f in input_files) // 1024 // len(input_files), avg_output_size_kb: sum(os.path.getsize(f) for f in output_files) // 1024 // len(output_files) } with open(output_dir / resize_stats.json, w) as f: json.dump(stats, f) return output_dir这套机制让数据质量问题能精准定位到具体步骤而非笼统归咎于“数据脏”。2.4 第四层模型训练层——超越model.fit()的可控训练循环Keras的model.fit()简洁优雅但掩盖了太多魔鬼细节。当你的模型在训练第37个epoch突然OOM或验证集loss诡异震荡时fit()提供的钩子Callback往往力不从心。真正的“from scratch”训练必须手写train_step和val_step掌控每一个tensor的生命周期。以分布式训练为例。PyTorch DDPDistributedDataParallel要求所有进程在forward()前同步batch size。但若某GPU因显存不足导致forward()失败DDP默认行为是整个进程组崩溃。我的解决方案是在train_step中包裹try-except捕获torch.cuda.OutOfMemoryError并主动调用torch.distributed.barrier()同步状态再跳过当前batchdef train_step(model, data, optimizer, scaler): try: with torch.cuda.amp.autocast(): loss model(data) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update() optimizer.zero_grad() return loss.item() except torch.cuda.OutOfMemoryError: # 清理缓存并同步 torch.cuda.empty_cache() torch.distributed.barrier() return None # 标记此batch被跳过更关键的是学习率调度。torch.optim.lr_scheduler.OneCycleLR虽好但其total_steps参数必须精确等于epochs * len(dataloader)。若数据集在训练中动态增删如在线学习场景硬编码会导致学习率曲线畸变。因此我坚持用基于step的调度器并在每个step后调用scheduler.step()而非基于epoch。2.5 第五层模型服务层——从torch.jit.trace到生产API的九道关卡将.pth文件变成HTTP API远不止flask加torch.load()。我总结出九道必须通过的关卡缺一不可序列化验证torch.jit.trace()后必须用model(*example_inputs)测试trace结果与原模型输出的数值一致性torch.allclose()误差1e-5即失败。输入校验API层必须做shape、dtype、range校验。例如图像输入强制[B, 3, H, W]且H,W 224像素值[0, 255]整数。批处理适配单样本推理效率低下。需实现动态batching收集请求至阈值如32ms或8个请求后统一处理。内存池管理GPU显存分配昂贵。预分配固定大小Tensor池避免频繁torch.cuda.allocate()。超时熔断设置per-request timeout如2s超时则返回503 Service Unavailable而非让请求堆积。健康检查端点/healthz必须返回模型加载状态、GPU显存使用率、最近10次推理P99延迟。日志结构化每条日志含request_id、model_version、input_shape、inference_time_ms便于ELK分析。指标暴露Prometheus格式暴露model_inference_seconds_count、model_gpu_memory_bytes。优雅关闭SIGTERM信号触发model.eval()、清空Tensor池、等待正在处理的请求完成。其中第3条动态batching最容易被忽视。我曾用asyncio.Queue实现但发现高并发下锁竞争严重。最终改用concurrent.futures.ThreadPoolExecutor配合queue.PriorityQueue按请求到达时间排序实测QPS提升3.2倍。2.6 第六层监控告警层——定义AI系统健康的“生命体征”传统运维监控CPU、内存、磁盘AI系统还需监控模型健康度。我定义四大核心生命体征指标计算方式告警阈值业务含义数据漂移指数KS检验训练集vs生产集特征分布0.2数据采集流程异常模型可能失效预测置信度衰减率连续1000次预测中置信度0.5的比例15%模型对新数据适应不良类别不平衡偏移生产数据中各标签占比 vs 训练集占比差异最大差异30%业务场景变化需重新采样推理延迟P9999%请求的响应时间1500ms硬件或代码性能瓶颈这些指标不能只画在Grafana上。我强制要求任何告警必须附带可执行的修复建议。例如数据漂移告警触发时自动发送邮件包含漂移最严重的3个特征名称及KS值这些特征在训练集和生产集的分布直方图PNG附件执行python drift_analysis.py --feature temp_sensor_1的命令行让一线工程师拿到告警就能立刻行动而非陷入“指标异常但不知所措”的焦虑。2.7 第七层变更管理层——用GitOps实现模型与代码的原子发布AI模型不是静态文件而是随数据、代码、超参持续演化的实体。我摒弃“手动scp模型文件到服务器”的野蛮方式采用GitOps for ML模式模型文件.pt、.onnx不存入主代码仓库而是推送到私有MinIO存储路径为models/{project}/{version}/model.onnx。代码仓库中仅存model_manifest.yaml内容为model_name: pcb_defect_v2 model_uri: s3://ml-models/pcb_defect_v2/1.3.0/model.onnx input_schema: image: [1, 3, 256, 256] output_schema: defect_prob: [1, 4] changelog: | - Fixed false positive on solder bridges - Added support for new PCB variant X7CI流水线检测到model_manifest.yaml变更自动触发部署Job下载模型、校验SHA256、更新Docker镜像tag、滚动重启K8s Deployment。这种设计确保模型、代码、配置三者版本严格绑定。回滚时只需git revertmanifest文件即可一键恢复到任意历史状态无需在服务器上翻找旧模型文件。3. 跨越鸿沟从实验室原型到产线部署的五个致命断点即便你完美构建了上述七层基座AI项目仍可能在最后一百米溃败。我梳理出五个高频致命断点每个都源于“实验室思维”与“工程思维”的根本差异。3.1 断点一标注质量幻觉——你以为的“高质量标注”其实是噪声放大器算法工程师常抱怨“标注质量太差”却很少反思标注规范本身是否可执行、可验证、可量化我曾审核一份医疗CT标注规范其中一条写道“标注所有疑似肿瘤区域”。问题在于“疑似”是主观判断不同医生标注结果IOU交并比仅0.32。真正的工程化标注规范必须满足原子性每个标注指令只解决一个问题。例如拆分为“1. 标注直径5mm的结节2. 标注密度值HU300的钙化灶”。可验证性提供正例/反例图集。例如“以下5张图是符合标准的标注以下5张是典型错误”。量化验收标注员间一致性IIC必须0.85Cohens Kappa否则整批数据作废。更隐蔽的陷阱是标注工具引入的系统性偏差。LabelImg默认保存为Pascal VOC格式其bndbox坐标是浮点数但某些OCR模型要求整数坐标。若未在数据管道中强制round()会导致训练时坐标截断误差累积最终模型定位精度下降12%。3.2 断点二评估指标失真——在干净测试集上99%准确率产线实际只有63%实验室评估常用Accuracy、F1-score但这些指标在真实场景中极具欺骗性。以安防人脸识别为例测试集包含1000张清晰正面照模型Accuracy达99.2%。但产线摄像头拍摄的侧脸、逆光、运动模糊图像占73%此时模型在这些子集上的Accuracy仅为41.7%。我的解决方案是构建分层评估集Stratified Evaluation SetLevel 0理想实验室标准图像用于算法选型。Level 1可控退化添加高斯噪声、JPEG压缩、随机裁剪模拟常见图像劣化。Level 2真实退化从产线抓取的10000张原始图像按光照、角度、遮挡程度聚类每类抽样500张。Level 3对抗样本用FGSM生成的对抗样本测试模型鲁棒性。关键创新在于评估报告必须显示各Level的指标衰减曲线。例如LevelAccuracyPrecisionRecallF10理想99.2%98.5%99.0%98.7%1可控87.3%85.1%89.2%87.1%2真实63.4%52.8%71.6%60.9%3对抗28.1%19.3%35.7%25.4%这条曲线直接告诉产品负责人“要达到产线63%的可用性我们必须在Level 1阶段就把Accuracy做到85%以上”。指标不再是一个数字而是一条通往现实的路线图。3.3 断点三资源预算错配——GPU显存充足但PCIe带宽成瓶颈工程师常盯着GPU显存却忽略数据传输链路。我曾优化一个实时视频分析系统将模型从ResNet-50换成EfficientNet-B3显存占用从12GB降至6GB但端到端延迟反而增加23%。用nvidia-smi dmon排查发现rxPCIe接收带宽持续饱和峰值达32GB/s而服务器PCIe 4.0 x16理论带宽仅64GB/s留给其他设备的空间已不足。根本原因是EfficientNet-B3的输入分辨率更高300x300 vs 224x224导致从CPU内存拷贝图像到GPU显存的数据量激增。解决方案不是换GPU而是重构数据加载流水线使用torchvision.io.read_image()替代PIL.Image.open()前者直接读取为Tensor避免CPU内存中转。启用pin_memoryTrue的DataLoader并在训练循环中用tensor.to(device, non_blockingTrue)。对视频流采用decord库的VideoReader其GPU加速解码可将PCIe传输量降低40%。这揭示了一个残酷事实AI工程优化永远是系统级的单点改进常引发新瓶颈。必须用perf、nvidia-smi、iotop等工具绘制全链路资源热力图而非凭经验猜测。3.4 断点四运维盲区——模型服务正常但上游数据源已中断24小时AI系统最大的运维盲区是假设上游数据源永远可靠。我经历过的最痛教训一个推荐系统API返回HTTP 200但推荐结果全是空列表。排查3小时后发现上游用户行为日志Kafka Topic因磁盘满被自动删除数据已断流24小时。而我们的监控只告警“API延迟1s”对“返回空结果”毫无感知。为此我强制推行数据新鲜度Data Freshness监控在数据管道每个关键节点如Kafka Consumer、特征计算Job、模型输入队列埋点记录最新事件时间戳。Prometheus抓取这些时间戳计算time() - latest_event_timestamp。告警规则data_freshness_seconds{jobfeature_engine} 3005分钟未更新即告警。更进一步模型服务层必须做数据完整性校验。例如在接收用户ID列表后先查询Redis缓存验证ID有效性若有效率95%立即返回400 Bad Request并记录invalid_user_ratio指标。这比让模型处理一堆无效ID再返回空结果更能暴露数据链路问题。3.5 断点五组织墙——算法团队交付模型却不管模型如何融入业务系统技术断点背后往往是组织断点。最常见的场景算法团队交付一个predict.py脚本运维团队将其打包成Docker镜像但业务系统调用时发现脚本要求输入JSON含{user_id: 123}而业务系统只能传{uid: 123}。双方争论焦点是“谁该改字段名”而非“如何定义接口契约”。我的破局方法是在项目启动时强制签署《AI接口契约书》AI Interface Contract包含输入契约精确的OpenAPI 3.0 Schema包括字段名、类型、必填性、示例值。输出契约同样严格的Schema明确错误码语义如400表示输入非法422表示业务规则不满足。SLA契约P99延迟≤800ms可用性≥99.95%故障恢复时间≤5分钟。变更契约任何字段名/类型变更必须提前72小时邮件通知所有依赖方并提供兼容期。这份契约由CTO签字生效成为跨团队协作的法律依据。它把模糊的“应该能用”转化为可审计的“必须满足”从根本上消除了推诿空间。4. 工程化心智从“解决问题”到“构建系统”的思维跃迁“from scratch”最终考验的不是技术能力而是工程化心智。这种心智体现在三个维度的深刻转变4.1 从“功能正确”到“行为可预测”程序员追求“代码能跑通”工程师追求“代码在任何条件下行为可预测”。以随机种子为例torch.manual_seed(42)能让结果可复现但这只是起点。真正的可预测性要求跨平台一致性在Ubuntu 20.04和CentOS 7上相同seed必须产生相同结果。这要求禁用torch.backends.cudnn.benchmarkTrue因其会根据硬件选择不同卷积算法并固定cudnn.deterministicTrue。跨版本一致性PyTorch 1.12和2.0对torch.nn.Dropout的实现略有差异。解决方案是在requirements.txt中锁定torch2.0.1cu118而非torch2.0。跨规模一致性单卡训练与8卡DDP训练梯度累积逻辑必须完全一致。我坚持用torch.nn.parallel.DistributedDataParallel的find_unused_parametersFalse并确保所有分支路径都参与反向传播避免DDP因检测到未使用的参数而静默失败。这种对“可预测性”的极致追求让团队能自信地说“只要输入相同无论在哪台机器、哪个时间运行输出必然相同。”——这是工程信任的基石。4.2 从“局部最优”到“全局成本最小化”算法工程师常沉迷于提升模型Accuracy 0.5%却忽略这0.5%带来的全局成本。例如为提升OCR识别率将模型从CRNN换成TransformerAccuracy从92.1%升至93.7%但带来三重成本硬件成本推理延迟从120ms增至380ms需将GPU从T4升级到A10单卡月成本增加$200。运维成本Transformer需FP16精度而现有流水线只支持FP32需重构整个量化部署链路。业务成本延迟增加导致用户放弃率上升1.2%每月损失$15,000收入。我的决策框架是计算每0.1% Accuracy提升的边际成本Marginal Cost per 0.1% ACU边际成本 (新方案年总成本 - 原方案年总成本) / (新Accuracy - 原Accuracy) * 0.1若结果 $5000则暂停优化转向提升数据质量或简化业务逻辑。工程的本质是在约束条件下寻找全局最优解而非在单一维度上无限逼近理论极限。4.3 从“交付代码”到“交付能力”最成熟的AI工程团队交付物从“一个模型文件”进化为“一套可复用的能力”。例如我们为制造客户构建的缺陷检测系统最终交付的不是pcb_model_v3.pt而是能力包Capability Package一个Docker镜像含模型、预处理、后处理、健康检查、指标暴露。能力文档Capability Docs包含输入/输出契约、SLA、故障排查指南、升级步骤。能力测试集Capability Test Suite100个真实缺陷样本用于客户自主验证模型效果。能力培训Capability Workshop2小时现场培训教会客户运维人员如何监控、升级、回滚。这种交付模式让客户从“依赖供应商”变为“自主掌控能力”。当客户IT部门能独立完成模型热更新时我们的角色就从“外包开发者”升级为“能力赋能者”。这才是AI工程可持续的价值所在。提示每次项目复盘我必问团队三个问题1我们交付的是代码还是能力2客户离开我们能否独立运维3这套方案能否在三个月内复制到第二个客户答案若是否定说明工程化深度还不够。5. 实战沙盒用200行代码搭建可生产的AI工程最小闭环理论终需落地。下面是一个真实可用的AI工程最小闭环Minimal Production-Ready AI Loop仅200行Python代码涵盖数据加载、训练、服务、监控全链路且已在多个边缘设备上稳定运行。5.1 项目结构与依赖ai-from-scratch/ ├── requirements.txt ├── data/ │ └── sample.csv # 100行模拟传感器数据 ├── model/ │ └── train.py # 训练脚本 ├── service/ │ └── app.py # FastAPI服务 ├── monitor/ │ └── metrics.py # Prometheus指标 └── main.py # 启动入口requirements.txt精简至7行杜绝版本冲突torch2.0.1cu118 numpy1.23.5 pandas1.5.3 fastapi0.104.0 uvicorn0.23.2 prometheus-client0.18.0 scikit-learn1.3.05.2 数据管道抗干扰的CSV加载器data/loader.py实现鲁棒数据加载import pandas as pd import numpy as np from pathlib import Path def load_sensor_data(filepath: str, expected_columns: list None) - pd.DataFrame: 抗干扰CSV加载器处理BOM、空行、类型错误 try: # 尝试UTF-8 with BOM df pd.read_csv(filepath, encodingutf-8-sig) except UnicodeDecodeError: # 降级为latin-1容忍乱码 df pd.read_csv(filepath, encodinglatin-1) # 删除空行和全NaN列 df df.dropna(howall).dropna(axis1, howall) # 强制转换数值列 numeric_cols df.select_dtypes(include[np.number]).columns for col in numeric_cols: df[col] pd.to_numeric(df[col], errorscoerce) # 验证列名 if expected_columns and not set(expected_columns).issubset(set(df.columns)): missing set(expected_columns) - set(df.columns) raise ValueError(fMissing columns: {missing}) return df # 使用示例 if __name__ __main__: df load_sensor_data(data/sample.csv, [temp, voltage, status]) print(fLoaded {len(df)} rows, dtypes:\n{df.dtypes})5.3 训练脚本可复现的轻量训练器model/train.py封装可控训练import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader, TensorDataset from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler import numpy as np import joblib class SimpleMLP(nn.Module): def __init__(self, input_dim, hidden_dim64, num_classes2): super().__init__() self.net nn.Sequential( nn.Linear(input_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, num_classes) ) def forward(self, x): return self.net(x) def train_model(data_path: str, model_path: str, seed: int 42): torch.manual_seed(seed) np.random.seed(seed) # 加载并预处理数据 df load_sensor_data(data_path, [temp, voltage, status]) X df[[temp, voltage]].values.astype(np.float32) y df[status].values.astype(np.int64) # 标准化 scaler StandardScaler() X_scaled scaler.fit_transform(X) # 划分数据集 X_train, X_test, y_train, y_test train_test_split( X_scaled, y, test_size0.2, stratifyy, random_stateseed ) # 创建DataLoader train_dataset TensorDataset(torch.tensor(X_train), torch.tensor(y_train)) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue) # 初始化模型 model SimpleMLP(input_dim2) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr0.001) # 训练循环 model.train() for epoch in range(10): total_loss 0 for X_batch, y_batch in train_loader: optimizer.zero_grad() outputs model(X_batch) loss criterion(outputs, y_batch) loss.backward() optimizer.step() total_loss loss.item() if epoch % 2 0: print(fEpoch {epoch}, Loss: {total_loss/len(train_loader):.4f}) # 保存模型和scaler torch.save(model.state_dict(), model_path) joblib.dump(scaler, model_path.replace(.pth, _scaler.joblib)) # 评估 model.eval() with