AI工程化Notebook实战:从能跑到可复用 作为 AI 工程师我们几乎每天都要和 Jupyter Notebook 打交道探索数据、跑 baseline、对比实验、画图表分析结果。但很多人用着用着本地目录里就堆满了untitled_final_v2.ipynb、test_final_真的不改了.ipynb这种文件。代码逻辑散落在各个 cell 里换一台电脑跑不起来同事想复用也只能“抄 cell”。最近我复盘了calmrocks/ai-engineer-notebooks这个仓库所代表的 AI 工程化 notebooks 工作流结合自己踩过的坑整理出一套从“能跑”到“能复用”的完整实践笔记。这篇文章会从 notebook 的结构设计、环境管理、代码复用、可复现性、常见问题排查等方面展开并且会给出一个完整的端到端案例从数据探索到模型训练到模块化重构。无论你是刚接触 AI 工程的初学者还是已经写了很久 notebook 但想改善组织方式的开发者这篇都应该能帮到你。1. 背景为什么 AI 工程师需要一份“工程化”的 notebook1.1 AI 工程师 notebook 的常见形态AI 工程师的日常工作流通常包括读数据、清洗、特征工程、训练模型、评估结果、画图总结。这些步骤天然适合用 notebook 来承载因为每一步都能看到中间输出可以边写边调。常见的 notebook 使用方式大概是这样的所有逻辑一个文件写到底从上往下依次执行。每个 cell 都直接修改全局变量cell 之间通过隐式状态传递数据。训练参数、文件路径、模型超参数全部写死在代码里。换环境时要手动重装依赖经常出现 “在我电脑上是好的” 的问题。这种方式在临时探索阶段效率很高但一旦进入协作、项目交付、模型迭代阶段就会暴露很多问题。1.2 notebook 的天然问题Notebook 本身作为一种交互式文档优点是直观、反馈快。但它也是工程化的重灾区执行顺序混乱不按顺序执行 cell后面的 cell 可能引用到不存在的变量。状态隐式依赖数据清洗结果保存在内存里一旦 kernel 重启全部得重新跑。低复用性函数定义、训练逻辑散落在 cell 里别的地方想直接用非常困难。版本管理困难ipynb本质是 JSON合并冲突是常态代码 review 也困难。可复现性差依赖版本不锁定、随机种子不确定、路径写死导致结果无法复现。难以测试notebook 里的函数逻辑很少写单元测试回归风险高。1.3calmrocks/ai-engineer-notebooks想解决什么calmrocks/ai-engineer-notebooks这类项目所代表的思路是把 notebook 当作 AI 工程工作流中的“入口”和“可视化报告”而不是把所有代码都塞进 cell 里。它强调的是一种结构化的 notebook 组织方式每个 notebook 负责一条清晰的分析主线而不是一个大杂烩。可复用的数据读写、特征处理、模型封装逻辑尽量放到.py模块中。参数、路径、配置集中管理notebook 里只保留业务分析流程。通过环境锁定和随机种子控制保证结果可复现。我在看过这类思路之后把自己工作里的所有 notebook 都做了一次重构最大的感受是同一套代码从“只能自己看”变成了“能给别人用、能上生产测试线”。2. 环境准备搭建一个可复现的 AI 工程环境在动手写 notebook 之前先解决环境问题。很多项目跑不起来第一原因不是代码错而是环境不统一。2.1 Python 与深度学习环境本文示例以 Python 3.9 为主深度学习框架可以根据你的实际需求选择 PyTorch 或 TensorFlow。建议先确认好以下信息操作系统Windows / Linux / macOSPython 版本建议 3.9 或 3.10CUDA 版本如果本机有 NVIDIA GPU需要确认 nvidia-smi 显示的 CUDA 版本深度学习框架版本例如 PyTorch 2.x包管理工具pip 或 conda需要注意不同环境的版本号差异很大下面命令中的版本号只是示例思路实际安装时请根据你的项目情况调整。2.2 使用虚拟环境与依赖锁定推荐的做法是每个 AI 项目单独建一个虚拟环境然后把依赖锁定到requirements.txt。在项目根目录执行python -m venv venv source venv/bin/activate # Windows 使用 venv\Scripts\activate安装基础依赖pip install --upgrade pip pip install jupyter pip install pandas numpy matplotlib scikit-learn训练完模型后把当前的准确依赖版本导出pip freeze requirements.txt但pip freeze会包含大量传递依赖建议在提交项目时额外维护一个requirements-base.txt只记录直接依赖便于他人理解。例如pandas2.0 numpy1.24 scikit-learn1.3 matplotlib3.7 jupyter1.02.3 目录规划一个工程化的 AI 项目目录通常长这样ai-engineer-notebooks/ ├── data/ │ ├── raw/ # 原始数据 │ ├── processed/ # 清洗后数据 │ └── output/ # 模型输出、图表 ├── notebooks/ │ ├── 01-eda.ipynb # 探索性数据分析 │ ├── 02-feature-engineering.ipynb │ └── 03-train-evaluate.ipynb ├── src/ │ ├── config.py # 配置管理 │ ├── data_loader.py # 数据读取 │ ├── preprocessing.py # 预处理 │ └── models.py # 模型训练与评估 ├── tests/ │ └── test_preprocessing.py ├── requirements.txt └── README.md这样的分层设计核心思想是notebook 负责“讲业务故事”src负责“沉淀可复用代码”。3. 核心拆解一份工程化 notebook 应该包含什么在重构 notebook 时可以按下面的标准 cell 结构来组织而不是想到哪里写到哪里。3.1 标准 cell 结构无论是探索性分析、特征工程还是模型训练一份规范的 AI notebook 建议包含以下几类 cell说明性 cell用 Markdown 说明本 notebook 要解决什么问题输出什么结论。导入 cell统一导入所有依赖库放在最前面。配置 cell集中定义路径、参数、常量。数据读取 cell调用src里的数据加载函数。核心分析 cell对应业务主线例如特征分布、相关性分析、模型训练。结果展示 cell用表格、图表展示结果。保存与记录 cell把结果图片、指标、模型参数保存到指定目录。这里的关键是配置不要散落在多个 cell 里更不要藏在代码深处。3.2 参数与配置分离很多 notebook 跑两次结果不一样就是因为参数被改来改去但又没有记录。更好的做法是使用一个集中的配置模块比如src/config.py# 文件路径src/config.py from pathlib import Path # 项目根目录 BASE_DIR Path(__file__).resolve().parent.parent # 数据目录 RAW_DATA_DIR BASE_DIR / data / raw PROCESSED_DATA_DIR BASE_DIR / data / processed OUTPUT_DIR BASE_DIR / data / output # 模型参数 MODEL_PARAMS { random_state: 42, n_estimators: 200, max_depth: 8, test_size: 0.2, } # 随机种子 SEED 42然后在 notebook 中这样使用# 文件路径notebooks/03-train-evaluate.ipynb第 3 个 cell import sys from pathlib import Path # 将项目根目录加入模块搜索路径 sys.path.append(str(Path.cwd().parent)) from src.config import RAW_DATA_DIR, PROCESSED_DATA_DIR, OUTPUT_DIR, MODEL_PARAMS, SEED print(原始数据目录:, RAW_DATA_DIR) print(输出目录:, OUTPUT_DIR)这样做的好处是修改参数时只改config.pynotebook 的逻辑保持稳定。3.3 可复用代码抽取判断一段逻辑要不要抽成.py函数可以用一个简单的标准你是否有超过一次的机会用同一段逻辑数据加载、重命名列、统一日期格式基本都要抽出来。缺失值统计、异常值处理抽出来。模型训练、评价指标计算抽出来。只在本 notebook 里一次性画图的代码可以留在 notebook 里。举个典型的坏味道在 notebook 里反复出现这样的 cell——# 坏味道复制粘贴的加载逻辑 import pandas as pd df_train pd.read_csv(../data/raw/train.csv) df_train[date] pd.to_datetime(df_train[date]) df_train df_train.drop_duplicates()如果第二个 notebook 也需要同样的预处理又要复制一遍。应该抽成一个函数# 文件路径src/data_loader.py import pandas as pd from pathlib import Path def load_clean_data(file_path): 加载 CSV 数据并做基础清洗。 参数 file_path: str 或 PathCSV 文件路径。 返回 pandas.DataFrame清洗后的数据。 df pd.read_csv(file_path) df[date] pd.to_datetime(df[date]) df df.drop_duplicates().reset_index(dropTrue) return df然后在 notebook 中这样调用from src.data_loader import load_clean_data df load_clean_data(RAW_DATA_DIR / train.csv) print(df.shape) print(df.dtypes)3.4 随机种子与可复现性模型训练涉及随机过程时一定要设置随机种子。否则同一个 notebook同样的数据、同样的代码两次训练出的指标可能差很多。在训练前统一设置import numpy as np import random import torch def set_seed(seed: int): 固定随机种子保证实验可复现。 random.seed(seed) np.random.seed(seed) if torch.cuda.is_available(): torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) set_seed(SEED)这里把SEED放在config.py中统一管理避免每次写 notebook 时都重复定义也能防止某个 cell 忘了设置导致结果漂移。4. 完整实战案例从数据探索到模型训练的 notebook 重构接下来我们用一份简单的房价预测数据作为案例展示一个工程化 notebook 的完整流程。案例数据是模拟生成的重点是演示结构而不是追求模型精度。4.1 创建项目结构先按照上面的目录规划创建项目mkdir -p ai-engineer-notebooks/{data/{raw,processed,output},notebooks,src,tests} cd ai-engineer-notebooks touch README.md为了快速演示我们先生成一份模拟数据。你也可以把自己手头的数据集放到data/raw目录下。# 生成模拟房价数据仅用于演示 import pandas as pd import numpy as np np.random.seed(42) n 1000 df pd.DataFrame({ area: np.random.normal(120, 30, n), bedrooms: np.random.randint(1, 5, n), age: np.random.randint(0, 50, n), price: np.random.normal(300, 80, n) }) df.to_csv(data/raw/house.csv, indexFalse) print(模拟数据已生成shape:, df.shape)4.2 编写可复用模块在src目录下把数据读取、特征处理、模型训练拆成独立模块。首先是数据加载模块# 文件路径src/data_loader.py import pandas as pd from pathlib import Path def load_dataset(file_path: str, **kwargs) - pd.DataFrame: 加载 CSV 数据。 参数 file_path: CSV 文件路径。 **kwargs: 传给 pd.read_csv 的其他参数。 返回 pd.DataFrame return pd.read_csv(file_path, **kwargs) def save_processed_data(df: pd.DataFrame, output_path: Path) - None: 保存处理后的数据。 参数 df: 待保存的数据框。 output_path: 保存路径。 output_path.parent.mkdir(parentsTrue, exist_okTrue) df.to_csv(output_path, indexFalse)然后是特征处理模块# 文件路径src/preprocessing.py import pandas as pd import numpy as np def fill_missing_values(df: pd.DataFrame, columns: list) - pd.DataFrame: 使用中位数填充指定列的缺失值。 参数 df: 输入数据框。 columns: 需要填充的列名列表。 返回 填充后的数据框。 for col in columns: if col in df.columns and df[col].isnull().any(): median_val df[col].median() df[col] df[col].fillna(median_val) return df def add_room_density(df: pd.DataFrame) - pd.DataFrame: 添加卧室密度特征表示单位面积内的卧室数量。 参数 df: 输入数据框。 返回 新增 room_density 列后的数据框。 df df.copy() df[room_density] df[bedrooms] / df[area] return df最后是模型训练模块# 文件路径src/models.py from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score import numpy as np def train_random_forest(X_train, y_train, model_params: dict): 训练随机森林回归模型。 参数 X_train: 训练特征。 y_train: 训练标签。 model_params: 模型参数字典。 返回 训练好的模型。 model RandomForestRegressor(**model_params) model.fit(X_train, y_train) return model def evaluate_model(model, X_test, y_test): 评估回归模型。 参数 model: 训练好的模型。 X_test: 测试特征。 y_test: 测试标签。 返回 包含 MAE、RMSE、R2 的字典。 y_pred model.predict(X_test) mae mean_absolute_error(y_test, y_pred) rmse np.sqrt(mean_squared_error(y_test, y_pred)) r2 r2_score(y_test, y_pred) return {MAE: mae, RMSE: rmse, R2: r2}4.3 编写主 notebook打开 notebook按照下面结构编写 cell。Cell 1说明# 房价预测 - 模型训练与评估 目标基于面积、卧室数、房龄训练随机森林回归模型并输出评估指标。 数据来源data/raw/house.csv 输出模型指标字典、预测结果图。Cell 2导入与配置import sys from pathlib import Path import pandas as pd import matplotlib.pyplot as plt sys.path.append(str(Path.cwd().parent)) from src.config import RAW_DATA_DIR, MODEL_PARAMS, SEED, OUTPUT_DIR from src.data_loader import load_dataset, save_processed_data from src.preprocessing import fill_missing_values, add_room_density from src.models import train_random_forest, evaluate_model import numpy as np import random def set_seed(seed: int): random.seed(seed) np.random.seed(seed) set_seed(SEED)Cell 3数据读取df load_dataset(RAW_DATA_DIR / house.csv) print(数据形状:, df.shape) print(df.head())Cell 4特征处理# 模拟部分缺失值用于演示填充逻辑 df.loc[0, area] np.nan df_clean fill_missing_values(df, columns[area, bedrooms]) df_feat add_room_density(df_clean) print(df_feat.head())Cell 5训练集与测试集划分from sklearn.model_selection import train_test_split X df_feat[[area, bedrooms, age, room_density]] y df_feat[price] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_stateSEED ) print(训练集样本数:, X_train.shape[0]) print(测试集样本数:, X_test.shape[0])Cell 6训练与评估model train_random_forest(X_train, y_train, MODEL_PARAMS) metrics evaluate_model(model, X_test, y_test) print(模型评估指标:) for metric_name, metric_value in metrics.items(): print(f{metric_name}: {metric_value:.4f})Cell 7可视化与保存y_pred model.predict(X_test) plt.figure(figsize(6, 6)) plt.scatter(y_test, y_pred, alpha0.6) plt.xlabel(真实房价) plt.ylabel(预测房价) plt.title(真实值 vs 预测值) plt.plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], r--) OUTPUT_DIR.mkdir(parentsTrue, exist_okTrue) plt.savefig(OUTPUT_DIR / prediction_result.png, dpi150) plt.show() # 保存模型评估指标方便后续对比实验 import json with open(OUTPUT_DIR / metrics.json, w, encodingutf-8) as f: json.dump(metrics, f, ensure_asciiFalse, indent2)4.4 运行与验证在项目根目录启动 Jupyterjupyter notebook打开notebooks/03-train-evaluate.ipynb按顺序执行所有 cell。预期结果数据读取成功输出 1000 行数据。特征处理完成后新增room_density列。模型训练完成后输出 MAE、RMSE、R2 三个指标。在data/output目录下生成prediction_result.png图和metrics.json文件。模拟数据的指标会比较一般因为数据本身没有强规律但整个流程跑通是有价值的。4.5 结果说明与版本管理跑通之后有两个动作非常重要更新依赖清单pip freeze requirements.txt给 notebook 加 Cell 元信息。在 notebook 最后的 Markdown cell 里记录实验日志## 实验记录 - 日期2025-XX-XX - 数据模拟生成的 house.csv - 模型RandomForestRegressorn_estimators200max_depth8 - 指标MAExx.xxRMSExx.xxR2xx.xx - 备注新增 room_density 特征后R2 略有提升这样再看 notebook 时不需要翻代码也能知道这次实验做了什么。5. 常见问题与排查思路在实际操作中最容易遇到的问题集中在环境、路径和 kernel 状态上。我整理了下面这个排查表问题现象常见原因解决思路notebook 里 import 不到src模块Python 模块搜索路径没有包含项目根目录在 notebook 开头执行sys.path.append(str(Path.cwd().parent))Kernel 重启后变量全部丢失notebook 默认不持久化内存变量按 cell 顺序执行把耗时结果提前保存到磁盘换电脑后 notebook 跑不起来依赖版本不一致、路径写死使用requirements.txt锁定依赖不要在代码里写绝对路径同样代码两次结果不一样未设置随机种子、不同库版本行为不同在入口统一set_seed并固定数据划分的 random_state项目里 notebook 太多找不到对应关系文件命名混乱使用01-eda、02-feature、03-train的编号命名git diff 里看到大段 JSONipynb包含输出内容安装nbstripout提交前清理输出数据文件路径中有中文或空格配置文件转义问题统一使用英文路径用pathlib.Path代替字符串拼接这里重点说一下最常遇到的模块导入问题。如果在 notebook 中执行from src.data_loader import load_dataset报错ModuleNotFoundError: No module named src大概率是因为 notebook 的工作目录和项目根目录不一致。在 notebook 第一个 cell 里加上以下代码即可import sys from pathlib import Path project_root Path.cwd().parent if str(project_root) not in sys.path: sys.path.append(str(project_root))如果你对项目根目录的判定有更严格的需求也可以从config.py中导入BASE_DIRimport sys from pathlib import Path sys.path.append(str(Path(__file__).resolve().parent.parent)) # 这是 .py 文件里的做法但注意__file__这种写法只适用于.py文件在 notebook 中要用Path.cwd()或Path().resolve()来推测。另外一个高频问题是notebook 没按顺序执行。明明上面的 cell 里定义了变量下面的 cell 却报错NameError。这通常是因为中间的 cell 执行失败或者之前跳过了某个 cell。排查方式很简单点击菜单Kernel - Restart Run All看是否全流程都能跑通。如果全流程能跑通说明代码没问题只是执行顺序乱了。如果全流程也报错说明有隐性依赖需要检查变量定义是否放在引用之前。6. 最佳实践与工程建议结合calmrocks/ai-engineer-notebooks所代表的工程化思路我总结了下面几条建议可以直接用在自己的项目里。6.1 Notebook 命名与组织每个 notebook 只做一件事对应一个编号。命名中带上序号例如01-data-exploration.ipynb方便浏览。用 README 记录每个 notebook 的用途和依赖关系。提交到 Git 前用nbstripout清理输出结果减少 diff 噪声。安装方式pip install nbstripout nbstripout --install6.2 配置管理路径、超参数、随机种子统一放在config.py。不要在 notebook 中硬编码绝对路径。涉及敏感信息如数据库连接串时不要提交到仓库使用环境变量或.env文件并加入.gitignore。如果同一个项目要跑多组实验可以使用类似于config dataclass的方式管理实验参数。简单版本from dataclasses import dataclass dataclass class ExperimentConfig: model_type: str random_forest n_estimators: int 200 max_depth: int 8 test_size: float 0.2 random_state: int 426.3 异常处理与数据安全在处理真实数据时要特别注意边界情况数据读取时判断文件是否存在避免下次路径改了直接报一个难懂的 FileNotFoundError。特征处理后检查样本量是否仍合理。涉及删除数据、覆盖文件的步骤先备份或先写入临时文件。如果需要访问数据库或其他敏感数据源务必使用最小权限账号并在测试环境验证。示例from pathlib import Path file_path RAW_DATA_DIR / train.csv if not file_path.exists(): raise FileNotFoundError(f未找到数据文件{file_path}请检查原始数据目录。)6.4 测试与质量保障不要觉得 notebook 里的代码就不用测试。可复用的函数一旦抽到src中就应该为它们编写单元测试。比如src/preprocessing.py中的fill_missing_values测试可以这样写# 文件路径tests/test_preprocessing.py import pandas as pd import numpy as np from src.preprocessing import fill_missing_values def test_fill_missing_values(): df pd.DataFrame({area: [100, np.nan, 140], price: [300, 320, 280]}) df_result fill_missing_values(df, columns[area]) assert df_result[area].isnull().sum() 0 # 中位数填充100、120、140 的中位数是 120 assert abs(df_result.loc[1, area] - 120) 1e-6在项目根目录执行pytest tests/ -v虽然一个小函数也写测试看起来有点“重”但对于 AI 工程化项目来说数据预处理逻辑一旦出错模型指标再高都没有意义。6.5 性能与资源管理大数据集不要轻易pd.read_csv全量读入优先用pd.read_csv(..., usecols[...])只读所需列或使用分块读取。耗时的预处理结果保存为中间文件避免每次重启 kernel 都重跑。在 GPU 上训练大模型时注意监控显存占用用完及时释放不必要的大张量。不要把大模型的权重文件直接放进 Git 仓库可以使用 dvc 等方式管理数据与模型版本。6.6 协作与 ReviewPull Request 中提交 notebook 时可以在描述中说明改动了哪个 cell、改了什么参数。多人在同一 notebook 上工作时优先拆分到不同 notebook而不是大家一起改同一个文件。Review 代码时重点关注src下的.py文件notebook 更多看结论和可视化是否合理。7. 总结与下一步这份笔记的核心就八个字逻辑下沉配置集中notebook 留主线。通过把数据加载、预处理、模型训练等通用逻辑沉淀到src目录把路径和参数集中到config.py再让 notebook 专注于分析和展示整个 AI 工程项目的可维护性会明显提升。calmrocks/ai-engineer-notebooks这类项目给我们的最大启示并不是某一个具体函数而是一种把 notebook 从“个人草稿本”变成“团队协作工具”的思路。下一步你可以做三件事把自己最近一个 notebook 项目按文中的目录结构重构一遍重点是把重复出现的代码抽成函数。给已经稳定的数据预处理逻辑补上单元测试用 pytest 跑通一条最小用例。尝试使用nbstripout和requirements.txt管理项目版本让你的 notebook 真正变成可交付、可复现的工程资产。如果你想进一步提升可以继续学习 dvc 的数据版本管理、mlflow 的实验追踪以及模型服务化部署。它们和工程化 notebook 结合之后就能形成一套完整的 AI 工程闭环。