Digits 项目工程交付与验收
按分级验收完成代码、依赖、五张图、模型、报告、预测程序和风险说明。
学习前需要掌握
背景与问题
一个高分 notebook 可能依赖隐藏单元、个人路径和当前内存对象,别人无法从干净环境复现,也无法可靠输入新样本。项目交付要把训练、评价与预测拆成明确入口,并让产物携带解释自身所需的信息。
交付是否合格取决于能否按约定运行、输入错误能否被发现,以及报告结论是否可回查,而不只是模型文件存在。
概念定义
工程交付要求训练、评价和预测逻辑分离,路径基于 pathlib,输入错误给出清晰提示,结果可复现。
直观理解
把项目当作一条可复现、可审计的实验链,而不是追求一次最高分。
核心原理
可以按四个层次验收:先完成数据审计、固定测试和基线,再完成同折候选比较与开发选择,随后锁定最终评价和图表,最后交付模型、预测程序、依赖和限制说明。后一级不能靠跳过前一级的数据边界来获得高分。
训练入口负责读取数据并输出已拟合 Pipeline 与元数据,评价入口读取固定预测和协议生成指标与图,预测入口只做契约检查、变换和预测,不能在用户每次调用时悄悄重新训练或更新缩放统计量。
产物至少关联特征数与顺序、像素范围、展平方式、类别、依赖版本和数据/划分记录。Digits 预测要求二维的每行 64 个有限像素,范围 0–16;任意图片文件到这个表示的转换需要另行实现和验证,不能只 reshape 一张大照片。
路径使用可配置输出目录与 pathlib,首次运行自动创建产物目录,缺少文件或输入不符时清楚报错。joblib/pickle 加载可能执行代码,只能加载可信来源;校验文件哈希不等于证明供应方可信。
从干净进程重载刚保存的可信模型,用固定样本核对预测一致,并测试错误形状、非有限值和越界像素。工程验收也要保留未完成项:示例通过往返不证明完整项目图表、真实照片适配或线上监控已经实现。
数学表达
项目主线
此处保留项目主线锚点,箭头仅是工程顺序示意,不构成损失函数或评分规则。验收依据是可运行入口、真实产物、输入契约和可追溯记录。
变量含义
变量定义与本页概念定义一致,代码中的同名变量保持相同语义。
计算步骤
- 1从干净环境执行固定数据审计、划分、基线、开发比较与搜索。
- 2锁定方案后评价测试,保存逐折指标、最终指标、五类图与错误说明。
- 3保存完整 Pipeline、输入/类别元数据、依赖与数据/划分记录。
- 4提供分离的训练、评价和预测入口,输出目录由配置决定并自动创建。
代码对应位置
示例 1可信临时模型的输入校验与保存恢复验收:只保存并加载本次进程创建的可信临时 Pipeline,测试固定合法输入与两种明确错误。不会读取未知模型、修改原始样本或发布模型。
案例:文件存在仍不等于预测接口合格
下方先用固定开发数据拟合一个 KNN Pipeline,保存到临时目录,再仅加载本进程刚创建的可信文件。这个模型只是交付接口示例,不宣称它是前文所有候选中的最优者。
合法的一行 64 像素输入应与保存前得到相同结果;63 列输入和包含 17 的像素则在 predict 前被拒绝。若仅检查模型文件存在,这两类输入问题会被漏掉。真实交付还需清晰命令、依赖与完整报告,不能把这个最小接口测试当成全部验收。
计算与实现步骤
- 从干净环境执行固定数据审计、划分、基线、开发比较与搜索。
- 锁定方案后评价测试,保存逐折指标、最终指标、五类图与错误说明。
- 保存完整 Pipeline、输入/类别元数据、依赖与数据/划分记录。
- 提供分离的训练、评价和预测入口,输出目录由配置决定并自动创建。
- 在预测前验证二维 64 列、实数、有限值与像素范围,给出清晰错误。
- 仅从可信来源加载产物,固定样本核对保存前后预测一致。
- 按四层交付检查报告、命令、图表和限制,未完成内容明确标注。
代码实现
示例 1
可信临时模型的输入校验与保存恢复验收
example_01.py用途:只保存并加载本次进程创建的可信临时 Pipeline,测试固定合法输入与两种明确错误。不会读取未知模型、修改原始样本或发布模型。
from pathlib import Path
from tempfile import TemporaryDirectory
import json
import joblib
import numpy as np
import sklearn
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
def validate_pixels(values):
pixels = np.asarray(values)
if pixels.ndim != 2 or pixels.shape[1] != 64:
raise ValueError("expected a 2D array with 64 columns")
if not np.issubdtype(pixels.dtype, np.number) or np.iscomplexobj(pixels):
raise ValueError("real numeric pixels required")
if not np.isfinite(pixels).all() or ((pixels < 0) | (pixels > 16)).any():
raise ValueError("pixels must be finite and in [0, 16]")
return pixels
X, y = load_digits(return_X_y=True)
train, test = train_test_split(np.arange(len(y)), test_size=0.2, stratify=y, random_state=42)
model = make_pipeline(StandardScaler(), KNeighborsClassifier(n_neighbors=5))
model.fit(X[train], y[train])
sample = validate_pixels(X[test[:1]])
with TemporaryDirectory() as temporary:
root = Path(temporary)
joblib.dump(model, root / "digits.joblib")
metadata = {"features": 64, "shape": [8, 8], "flatten_order": "C", "pixel_range": [0, 16],
"classes": model.classes_.tolist(), "sklearn": sklearn.__version__}
(root / "metadata.json").write_text(json.dumps(metadata), encoding="utf-8")
loaded = joblib.load(root / "digits.joblib")
restored_metadata = json.loads((root / "metadata.json").read_text(encoding="utf-8"))
assert loaded.n_features_in_ == restored_metadata["features"]
assert loaded.classes_.tolist() == restored_metadata["classes"]
assert np.array_equal(model.predict(sample), loaded.predict(validate_pixels(sample)))
print("round_trip_predictions_equal:", True)
print("single_prediction_count:", len(loaded.predict(sample)))
for name, bad in [("wrong_shape", np.zeros((1, 63))), ("wrong_range", np.full((1, 64), 17.0))]:
try:
loaded.predict(validate_pixels(bad))
except ValueError as error:
print(f"{name}: {error}")代码解析
解析始终位于完整代码下方,并按实际代码段逐项对应。
输入数据与任务
只保存并加载本次进程创建的可信临时 Pipeline,测试固定合法输入与两种明确错误。不会读取未知模型、修改原始样本或发布模型。
X- 完整特征矩阵,每行一个样本、每列一个特征。
y- 目标标签或连续目标值。
model- 组合预处理和估计器的模型对象。
Step 1 · 1–11 行
导入当前步骤需要的数值计算、预处理、模型或评价工具。依赖集中写在代码开头,便于复现。
from pathlib import Path
from tempfile import TemporaryDirectory
import json
import joblib
import numpy as np
import sklearn
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScalerStep 2 · 13–21 行
输入形状、实数类型、有限性和 0–16 范围都在调用模型前检查。
def validate_pixels(values):
pixels = np.asarray(values)
if pixels.ndim != 2 or pixels.shape[1] != 64:
raise ValueError("expected a 2D array with 64 columns")
if not np.issubdtype(pixels.dtype, np.number) or np.iscomplexobj(pixels):
raise ValueError("real numeric pixels required")
if not np.isfinite(pixels).all() or ((pixels < 0) | (pixels > 16)).any():
raise ValueError("pixels must be finite and in [0, 16]")
return pixelsStep 3 · 23–45 行
按固定协议切分训练与测试数据;此处的数据边界决定后续哪些样本可以参与 fit。
X, y = load_digits(return_X_y=True)
train, test = train_test_split(np.arange(len(y)), test_size=0.2, stratify=y, random_state=42)
model = make_pipeline(StandardScaler(), KNeighborsClassifier(n_neighbors=5))
model.fit(X[train], y[train])
sample = validate_pixels(X[test[:1]])
with TemporaryDirectory() as temporary:
root = Path(temporary)
joblib.dump(model, root / "digits.joblib")
metadata = {"features": 64, "shape": [8, 8], "flatten_order": "C", "pixel_range": [0, 16],
"classes": model.classes_.tolist(), "sklearn": sklearn.__version__}
(root / "metadata.json").write_text(json.dumps(metadata), encoding="utf-8")
loaded = joblib.load(root / "digits.joblib")
restored_metadata = json.loads((root / "metadata.json").read_text(encoding="utf-8"))
assert loaded.n_features_in_ == restored_metadata["features"]
assert loaded.classes_.tolist() == restored_metadata["classes"]
assert np.array_equal(model.predict(sample), loaded.predict(validate_pixels(sample)))
print("round_trip_predictions_equal:", True)
print("single_prediction_count:", len(loaded.predict(sample)))
for name, bad in [("wrong_shape", np.zeros((1, 63))), ("wrong_range", np.full((1, 64), 17.0))]:
try:
loaded.predict(validate_pixels(bad))
except ValueError as error:
print(f"{name}: {error}")代码与数学原理
减去训练均值并除以训练标准差,对应代码中的 mean/std 或 StandardScaler。
预期输出或运行结果
本地 scikit-learn 1.8.0 的核对输出;分数仅属于下方固定教学实验: round_trip_predictions_equal: True single_prediction_count: 1 wrong_shape: expected a 2D array with 64 columns wrong_range: pixels must be finite and in [0, 16]
常见错误 · 5 条
- 不要在测试集上调用 fit 或 fit_transform;测试集只能复用训练参数。
- 只交 notebook 或模型文件,缺少可独立调用的预测入口、元数据和依赖。
- 在 predict 中重新 fit 预处理,或只检查特征数而不检查像素尺度与二维形状。
- 加载未知 joblib 文件、硬编码个人绝对路径,或把最小示例通过描述成完整业务交付。
- 确认样本轴、特征轴以及 X 与 y 的第一维完全对应。
实际应用
- 本地课程项目
- 可复现实验
常见错误
- 只交 notebook 或模型文件,缺少可独立调用的预测入口、元数据和依赖。
- 在 predict 中重新 fit 预处理,或只检查特征数而不检查像素尺度与二维形状。
- 加载未知 joblib 文件、硬编码个人绝对路径,或把最小示例通过描述成完整业务交付。
推荐学习资料
参考库不会生成虚假资源或无效外部链接。