Skip to content

机器学习:从基础建模到工程交付 ​

机器学习的目标不是在训练集上拿到高分,而是在真实约束下,对未见数据做出可靠预测。本手册以 scikit-learn 为主线,把任务定义、评估设计、数据处理、算法选择和部署串成可复用的闭环。

阅读方式

初学者按第一至第三篇顺序学习,再进入算法进阶;已有建模经验时,优先复查数据划分、流水线和部署契约。除特别说明的部署配套代码外,每个 Python 代码块都自带数据与导入,可单独运行;合成数据只验证方法,不代表业务收益。

导航目录 ​

第一篇:入门——先跑通正确闭环 ​

第二篇:基础——让评估值得相信 ​

第三篇:核心——掌握监督学习 ​

第四篇:进阶——扩展模型能力 ​

第五篇:工程——从实验到服务 ​

第六篇:实战——验收与持续迭代 ​


1.1 先定义预测时刻,再选择算法 ​

把业务需求写成四句话:对谁预测、在什么时候预测、预测什么、预测结果触发什么动作。

例如“流失预测”需要明确:在每周一,用此前已经产生的行为,预测未来 30 天是否流失,再决定是否进行挽留。未来退款记录、事后关闭账户状态不能作为当时的特征。

学习范式数据与目标典型任务验收重点
监督学习特征和已知标签回归、分类、排序未见样本上的误差与业务成本
无监督学习不依赖目标标签聚类、降维、结构探索稳定性、结构合理性、业务解释
半监督学习少量标签和大量未标注数据标注稀缺任务伪标签错误传播与分布差异
自监督学习从数据本身构造学习目标表征预训练下游任务收益,而非只看预训练损失
强化学习状态、动作、奖励和环境交互序贯决策、控制累积回报、探索成本与安全约束

回归输出连续量,分类输出类别或类别概率。“逻辑回归”虽然名字含回归,却用于分类。多输出预测也不自动等于采用共享表征的多任务学习。

1.2 理解训练与泛化 ​

  • 样本是一条观测,特征是预测时可获得的输入,标签是要学习的目标。
  • 参数由训练学习,例如权重;超参数由实验选择,例如树深度和正则化强度。
  • 损失函数指导优化;评估指标衡量任务表现,两者不必相同。
  • 过拟合通常表现为训练误差低、验证误差明显高;欠拟合可能表现为两者都高,也需排查标签噪声。
  • 泛化依赖未来数据与评估设计的匹配,不是训练分数的同义词。
text
定义任务与预测时刻
        |
        v
审计数据 -> 留出最终测试集
        |
        v
开发集:划分训练/验证 -> 拟合处理与模型 -> 比较基线与候选
        |                                  |
        +---------- 修改假设与重试 ---------+
        |
        v
冻结方案 -> 最终测试 -> 部署 -> 监控与反馈

不需要一开始推导所有公式,但应理解矩阵维度、均值方差、条件概率、梯度和正则化。若可靠规则已足够、标签无法定义或预测时拿不到所需数据,先不要增加模型复杂度。

2.1 依赖分层 ​

建议使用 Python 3.11;本文按 scikit-learn 1.5 ~ 1.6 接口组织。以下是兼容范围,不是已在所有平台验证的锁文件,正式项目应固定实际验证过的精确版本。

bash
python -m venv .venv
# Windows cmd 中激活:.venv\Scripts\activate.bat
python -m pip install "numpy>=1.26,<3" "pandas>=2.2,<3" "scikit-learn>=1.5,<1.7" "joblib>=1.3,<2"
工具职责依赖层次
NumPy / Pandas数值数组 / 带字段的数据表基础
scikit-learn预处理、建模、评估、搜索基础
Matplotlib分布、残差、学习曲线可视化按需
XGBoost / LightGBM / CatBoost外部梯度提升实现按需
FastAPI / Pydantic / Uvicorn校验、HTTP 服务部署章节
joblib可信模型持久化部署章节

示例默认使用内置或合成数据,不隐式下载数据集。外部提升库的早停参数随版本变化,应查对应版本文档,不直接拼接不同版本教程。

2.2 检查二维特征和一维标签 ​

python
import numpy as np
import pandas as pd
import sklearn

frame = pd.DataFrame({"age": [20, 30, 40], "visits": [3, 6, 2]})
X = frame[["age", "visits"]].to_numpy(dtype=float)
y = np.array([0, 1, 0])
print("sklearn:", sklearn.__version__)
print("X:", X.shape, "y:", y.shape)
assert X.shape == (3, 2)
assert y.shape == (3,)

单个样本也保留二维结构。字段顺序、单位、类型、缺失约定是数据契约的一部分;数字形式的地区编号不代表连续数值。Pandas 用于审计和整理,依赖样本统计量的填补与缩放应交给训练流水线。

3.1 从基线开始 ​

先留出测试集,再把缩放和分类器封装为整体;如果继续调参,应在训练部分内部做验证,而不是反复查看测试分数。

python
from sklearn.datasets import make_classification
from sklearn.dummy import DummyClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, balanced_accuracy_score
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

X, y = make_classification(
    n_samples=800, n_features=8, n_informative=5,
    weights=[0.7, 0.3], random_state=42,
)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, stratify=y, random_state=42,
)
models = {
    "baseline": DummyClassifier(strategy="most_frequent"),
    "logistic": make_pipeline(StandardScaler(), LogisticRegression(max_iter=1000)),
}
for name, model in models.items():
    model.fit(X_train, y_train)
    pred = model.predict(X_test)
    print(name, "accuracy:", accuracy_score(y_test, pred),
          "balanced accuracy:", balanced_accuracy_score(y_test, pred))

3.2 三个接口贯穿全文 ​

操作意义可使用的数据
fit学习统计量、特征映射或模型参数当前训练子集
transform使用已学习的转换规则训练、验证、测试、线上输入
predict / predict_proba输出类别、连续预测或类别概率输入契约一致的新样本

流水线训练时依次拟合转换器和最终模型;预测时只转换并预测。它防止处理步骤脱离验证流程,但不能自动识别“事后信息”或重复用户造成的泄露。

4.1 划分方式模拟上线场景 ​

场景推荐策略常见错误
近似独立的分类样本分层留出、StratifiedKFold小类在某一折消失
同一用户或设备多条记录GroupKFold / 分组留出同一实体同时出现在训练和验证
按未来时间预测时间留出、TimeSeriesSplit随机打散,把未来用于预测过去
多机构或跨地区推广按机构或地区留出只验证已见场景内部表现

分组与时间要求可能同时存在,需要定制划分。时间序列先排序,并根据标签窗口、特征窗口设置间隔;普通时间切分不会自动消除窗口重叠泄露。

4.2 交叉验证的正确边界 ​

text
最终测试集:隔离,不参加拟合、选特征、调参或选阈值

开发集:第 1 折训练 -> 拟合填补/缩放/选特征/模型 -> 第 1 折验证
        第 2 折训练 -> 重新拟合全部步骤          -> 第 2 折验证
        第 3 折训练 -> 重新拟合全部步骤          -> 第 3 折验证
                        |
                        v
                 聚合指标并选择方案

先在全部开发集上标准化,再交叉验证,仍会泄露折间统计量。交叉验证应接收完整流水线;有监督特征选择、目标编码、采样也必须处于相应训练折内部。

4.3 泄露审计清单 ​

分数异常高时,先怀疑信息边界

检查特征是否在预测时刻可得;是否存在重复样本、用户交叉或未来标签;填补和选择是否见过验证数据;测试集是否被反复用来选择模型。固定随机种子只能复现划分,不能证明划分正确。

开发集的交叉验证用于选择方案,最终测试用于报告冻结方案的表现。最优交叉验证分数有选择偏差;需要更稳健估计时考虑嵌套交叉验证,并报告计算成本。折间标准差只是波动描述,不直接等于置信区间。

5.1 回归指标 ​

指标含义边界
MAE绝对误差平均值与标签同单位,便于解释
MSE / RMSE平方误差均值 / 其平方根大误差惩罚更强,RMSE 与标签同单位
R²相对以评估集均值为常数预测的解释程度可为负;标签常量时需单独处理
MAPE相对绝对误差平均值真实值接近零时不稳定

训练集均值或中位数构成可部署的常数基线。不能把 R² 理解成准确率,也不能直接比较不同目标尺度的 RMSE。做过对数目标变换时,应同时在业务原始单位上评估误差。

5.2 分类指标 ​

以标签 1 为正类:TP 是正确识别的正类,FP 是误报,FN 是漏报。

指标关注点使用建议
Accuracy总体判对比例类别严重不平衡时容易误导
Precision = TP / (TP + FP)发出告警有多少是真的误报成本高时重点观察
Recall = TP / (TP + FN)真正目标找回多少漏报成本高时重点观察
F1Precision 与 Recall 的调和平均不直接表达业务成本,也不考虑 TN
Balanced accuracy各类别召回率平均减弱大类对准确率的支配
ROC-AUC正负样本排序能力输入连续分数,不能用硬类别替代
Average precision(AP)精确率-召回率曲线的阶梯加权汇总稀有正类常用;不等同于梯形积分 PR-AUC
Log loss / Brier score概率预测质量需要概率,而非只比较排名

多分类需声明 macro、weighted 或 micro 汇总方式,并保留各类指标。概率列必须对应选定正类,类别顺序来自模型的 classes_,不能永远假定业务正类在第二列。

5.3 用验证集选阈值,测试集只验收 ​

此例用合成不平衡数据演示:假设漏掉一个正类的成本为误报的五倍。在验证集扫描阈值,再冻结模型和阈值,一次评估测试集。

python
import numpy as np
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import average_precision_score, classification_report, roc_auc_score
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

X, y = make_classification(
    n_samples=2400, n_features=12, n_informative=7,
    weights=[0.9, 0.1], random_state=42,
)
X_dev, X_test, y_dev, y_test = train_test_split(
    X, y, test_size=0.2, stratify=y, random_state=42,
)
X_train, X_valid, y_train, y_valid = train_test_split(
    X_dev, y_dev, test_size=0.25, stratify=y_dev, random_state=43,
)
model = make_pipeline(StandardScaler(), LogisticRegression(max_iter=1000))
model.fit(X_train, y_train)
positive_column = list(model.classes_).index(1)
p_valid = model.predict_proba(X_valid)[:, positive_column]
thresholds = np.linspace(0, 1, 101)
costs = []
for threshold in thresholds:
    predicted = p_valid >= threshold
    fp = np.sum(predicted & (y_valid == 0))
    fn = np.sum(~predicted & (y_valid == 1))
    costs.append(fp + 5 * fn)
threshold = float(thresholds[np.argmin(costs)])
p_test = model.predict_proba(X_test)[:, positive_column]
print("threshold:", threshold)
print("ROC-AUC:", roc_auc_score(y_test, p_test))
print("AP:", average_precision_score(y_test, p_test))
print(classification_report(y_test, p_test >= threshold, zero_division=0))

成本比例仅为教学假设。阈值受正类比例、容量和干预成本影响,不存在通用最优值。此例没有选完阈值再重训模型,因为重训可能改变分数分布;需要重训时应设计折外预测或独立校准、验证流程,重新验证模型和阈值这对组合。

6.1 每种处理解决不同问题 ​

问题常用方法注意事项
数值缺失中位数填补、缺失指示器统计量只来自当前训练折
数值尺度差异标准化、稳健缩放正则化线性模型、SVM、KNN 常需要;树通常不需要
无序类别One-hot 编码设置未知类别策略,避免把编号当大小
高基数类别频次、目标编码或原生类别模型目标编码需折外训练编码,避免标签泄露
长尾和异常值合理变换、截断、稳健模型先检查业务含义,不机械删除极端样本
文本TF-IDF、嵌入词表和 IDF 只能从训练数据学习

标准化不会把任意分布变成正态分布。稀疏矩阵做中心化可能导致密集化,应设置不中心化或选择合适处理器。

6.2 混合字段的完整示例 ​

ColumnTransformer 按字段分工,Pipeline 绑定处理与模型。生成足够多的本地样本,先切分,再拟合填补和编码,避免小表上先填补后切分的错误示范。

python
import numpy as np
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import balanced_accuracy_score
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler

rng = np.random.default_rng(42)
size = 600
frame = pd.DataFrame({
    "age": rng.integers(18, 70, size).astype(float),
    "visits": rng.poisson(5, size).astype(float),
    "city": rng.choice(["BJ", "SH", "SZ"], size),
})
y = (frame["visits"] + rng.normal(0, 2, size) > 6).astype(int)
frame.loc[rng.choice(size, 50, replace=False), "age"] = np.nan
X_train, X_test, y_train, y_test = train_test_split(
    frame, y, test_size=0.2, stratify=y, random_state=42,
)
numeric = Pipeline([
    ("imputer", SimpleImputer(strategy="median", add_indicator=True)),
    ("scaler", StandardScaler()),
])
categorical = Pipeline([
    ("imputer", SimpleImputer(strategy="most_frequent")),
    ("encoder", OneHotEncoder(handle_unknown="ignore")),
])
preprocessor = ColumnTransformer([
    ("numeric", numeric, ["age", "visits"]),
    ("category", categorical, ["city"]),
])
model = Pipeline([
    ("preprocess", preprocessor),
    ("classifier", LogisticRegression(max_iter=1000)),
])
model.fit(X_train, y_train)
print("balanced accuracy:", balanced_accuracy_score(y_test, model.predict(X_test)))
print("new city:", model.predict(pd.DataFrame([
    {"age": 30.0, "visits": 4.0, "city": "GZ"},
])))

未知类别映射为对应字段的全零向量,不意味着模型理解新城市,仍应监控未知类别比例。训练与推理保持列名、类型、单位一致,部署时保存整条流水线,不能只保存最后的分类器。

7.1 从线性模型理解复杂度 ​

线性回归预测为“特征与权重的加权和 + 截距”,普通最小二乘最小化残差平方和。“线性”指对参数线性,输入可以包含平方项或交互项。

模型核心变化适用与代价
LinearRegression最小化残差平方和清晰基线;共线性使系数不稳定
Ridge加入 L2 惩罚收缩权重,改善共线性问题,通常不产生精确零系数
Lasso加入 L1 惩罚可产生稀疏系数,相关特征之间选择可能不稳定
多项式回归展开平方、交互特征再拟合表达非线性,但维度和外推风险迅速上升

正则化强度越大,约束通常越强;特征尺度影响惩罚,所以 Ridge/Lasso 通常需要缩放。正则化不保证每次都提升测试指标,也不把相关关系变成因果关系。

7.2 完整回归比较 ​

在开发集上比较基线、线性和二次特征模型,按交叉验证 MAE 选择,再在最终测试集报告一次。数据只含三个特征,以控制多项式展开规模。

python
import numpy as np
from sklearn.dummy import DummyRegressor
from sklearn.linear_model import Lasso, LinearRegression, Ridge
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
from sklearn.model_selection import KFold, cross_val_score, train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import PolynomialFeatures, StandardScaler

rng = np.random.default_rng(42)
X = rng.uniform(-3, 3, (600, 3))
y = 2 * X[:, 0] ** 2 + 3 * X[:, 1] + rng.normal(0, 1, 600)
X_dev, X_test, y_dev, y_test = train_test_split(X, y, random_state=42)
models = {
    "baseline": DummyRegressor(strategy="median"),
    "linear": make_pipeline(StandardScaler(), LinearRegression()),
    "ridge": make_pipeline(StandardScaler(), Ridge(alpha=1)),
    "lasso": make_pipeline(StandardScaler(), Lasso(alpha=0.02, max_iter=5000)),
    "quadratic": make_pipeline(
        PolynomialFeatures(degree=2, include_bias=False),
        StandardScaler(), Ridge(alpha=1),
    ),
}
cv = KFold(n_splits=5, shuffle=True, random_state=42)
errors = {}
for name, model in models.items():
    scores = -cross_val_score(model, X_dev, y_dev, cv=cv,
                              scoring="neg_mean_absolute_error")
    errors[name] = scores.mean()
    print(name, "CV MAE:", scores.mean(), "std:", scores.std())
best_name = min(errors, key=errors.get)
best = models[best_name].fit(X_dev, y_dev)
pred = best.predict(X_test)
print("selected:", best_name)
print("test MAE:", mean_absolute_error(y_test, pred))
print("test RMSE:", mean_squared_error(y_test, pred) ** 0.5)
print("test R2:", r2_score(y_test, pred))

构造的数据本来含二次关系,不说明多项式模型普遍优于其他算法。验收时还应画残差对预测值的散点图,检查系统性偏差和误差随尺度变化的问题;预测超出训练输入范围时要单独评估。

8.1 算法不是升级列表 ​

算法直觉缩放需求关键边界
逻辑回归线性得分经概率映射完成分类通常需要线性边界;C 越大正则越弱
决策树递归条件分割样本通常不需要深树易过拟合;限制深度和叶子样本数
随机森林多棵随机化树投票通常不需要内存、延迟随树数增加;概率不必然校准
SVM寻找间隔大的分隔边界通常需要核方法成本较高;C 与 gamma 共同影响复杂度
KNN参考附近样本类别通常需要预测需邻居检索,高维距离区分度可能下降
朴素贝叶斯给定类别下近似特征条件独立取决于变体Gaussian 用连续高斯假设;Multinomial 常用于非负文本特征

概率输出不等于概率已校准。SVC 默认不提供概率,很多排序指标可使用 decision_function;仅为 ROC-AUC 启用概率拟合会增加不必要成本。

8.2 使用相同折比较候选模型 ​

python
from sklearn.datasets import load_breast_cancer
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import balanced_accuracy_score
from sklearn.model_selection import StratifiedKFold, cross_val_score, train_test_split
from sklearn.naive_bayes import GaussianNB
from sklearn.neighbors import KNeighborsClassifier
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC
from sklearn.tree import DecisionTreeClassifier

X, y = load_breast_cancer(return_X_y=True)
X_dev, X_test, y_dev, y_test = train_test_split(X, y, stratify=y, random_state=42)
models = {
    "logistic": make_pipeline(StandardScaler(), LogisticRegression(max_iter=2000)),
    "tree": DecisionTreeClassifier(max_depth=4, random_state=42),
    "forest": RandomForestClassifier(n_estimators=100, min_samples_leaf=3, random_state=42),
    "svm": make_pipeline(StandardScaler(), SVC()),
    "knn": make_pipeline(StandardScaler(), KNeighborsClassifier(n_neighbors=7)),
    "bayes": GaussianNB(),
}
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
means = {}
for name, model in models.items():
    scores = cross_val_score(model, X_dev, y_dev, cv=cv, scoring="balanced_accuracy")
    means[name] = scores.mean()
    print(name, scores.mean(), scores.std())
name = max(means, key=means.get)
model = models[name].fit(X_dev, y_dev)
print("selected:", name)
print("test balanced accuracy:", balanced_accuracy_score(y_test, model.predict(X_test)))

该数据集仅用于演示,标签 0 为恶性、1 为良性;分析恶性召回率时必须指定正类 0。这里使用各类召回率平均值,不能把结果解释为临床诊断效力。

9.1 特征处理必须参与交叉验证 ​

特征选择减少维度,特征交叉扩展表达能力,两者不保证提高泛化。先使用业务合理且预测时可得的特征,再按同一协议做消融比较。仅凭相关系数筛选会遗漏非线性关系;高基数标识可能鼓励模型记忆用户。

将缩放、有监督选择和分类器放进搜索对象,避免在整个开发集上选完特征再交叉验证。

python
from sklearn.datasets import make_classification
from sklearn.feature_selection import SelectKBest, f_classif
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import GridSearchCV, StratifiedKFold, train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler

X, y = make_classification(n_samples=700, n_features=20,
                           n_informative=6, random_state=42)
X_dev, X_test, y_dev, y_test = train_test_split(X, y, stratify=y, random_state=42)
pipe = Pipeline([
    ("scale", StandardScaler()),
    ("select", SelectKBest(f_classif)),
    ("model", LogisticRegression(max_iter=2000)),
])
search = GridSearchCV(
    pipe, {"select__k": [5, 10, "all"], "model__C": [0.1, 1, 10]},
    scoring="roc_auc", cv=StratifiedKFold(4, shuffle=True, random_state=42),
    n_jobs=1, refit=True, error_score="raise",
)
search.fit(X_dev, y_dev)
print("parameters:", search.best_params_)
print("development CV AUC:", search.best_score_)
print("final test AUC:", roc_auc_score(y_test, search.predict_proba(X_test)[:, 1]))

9.2 搜索与诊断策略 ​

  • 小而有依据的空间用网格搜索;维度多时用随机搜索,将预算集中在重要参数,正则化强度通常按对数尺度探索。
  • 搜索评分遵循“越大越好”,负 MAE 取负还原误差。默认 refit 以最优参数在全部开发集重新训练。
  • 学习曲线比较不同训练规模下的训练/验证得分;验证曲线考察单一参数变化,辅助分析偏差、方差和数据瓶颈。
  • 排列重要性是在保留数据上打乱特征并观察性能下降;它不是因果效应,相关特征会分摊或掩盖重要性。
  • 根据残差或重要性反复改特征后,所用数据已参与选择,不再是最终测试集。
  • 固定实验预算,记录失败。不要同时让搜索和内部模型无限并行,避免 CPU 与内存过度竞争。

10.1 聚类不等于发现真实人群 ​

方法假设与能力限制
K-Means最小化到中心的平方距离需指定簇数,偏好近似球形且尺度相近的簇
层次聚类按链接准则逐步合并或拆分大样本距离计算开销高,需解释截断层级
DBSCAN密度连通区域组成簇对尺度和 eps 敏感,高维或变密度数据较难处理
PCA用正交线性方向保留较大方差无监督、线性;高方差方向未必有利于分类

簇编号是任意标识,没有天然优先级。DBSCAN 的 -1 是噪声,不当作普通簇解释。DBSCAN 与层次聚类没有通用的新样本 predict 接口,上线必须明确分配或重聚类策略。

10.2 分群与 PCA 的独立演示 ​

python
import numpy as np
from sklearn.cluster import AgglomerativeClustering, DBSCAN, KMeans
from sklearn.datasets import make_blobs
from sklearn.decomposition import PCA
from sklearn.metrics import silhouette_score
from sklearn.preprocessing import StandardScaler

X, _ = make_blobs(n_samples=500, n_features=4, centers=3,
                  cluster_std=1.2, random_state=42)
scaler = StandardScaler()
scaled = scaler.fit_transform(X)
kmeans = KMeans(n_clusters=3, n_init=10, random_state=42)
labels = kmeans.fit_predict(scaled)
print("silhouette:", silhouette_score(scaled, labels))
print("original-scale centers:", scaler.inverse_transform(kmeans.cluster_centers_))
print("hierarchical counts:", np.bincount(
    AgglomerativeClustering(n_clusters=3).fit_predict(scaled)))
noise_labels = DBSCAN(eps=0.7, min_samples=5).fit_predict(scaled)
print("DBSCAN noise ratio:", np.mean(noise_labels == -1))
pca = PCA(n_components=2)
projected = pca.fit_transform(scaled)
print("projection shape:", projected.shape)
print("retained variance:", pca.explained_variance_ratio_.sum())

轮廓系数要求至少两个簇且簇数小于样本数,偏好某些几何结构,不是业务价值的充分证据。此例是全样本描述性探索,不是泛化评估;若 PCA 服务于监督预测,必须进入交叉验证流水线。稀疏文本可考虑不中心化的 TruncatedSVD。

真实分群优先采用明确观察窗口的 RFM(最近消费、消费频次、消费金额)等特征,检查长尾和单位。比较不同窗口或重采样下的稳定性,报告原始单位的中位数和分位数,再用受控实验验证营销收益。

11.1 三种组合方式 ​

方式组合机制典型实现控制重点
Bagging重采样训练多个基模型再聚合Bagging、随机森林降低方差,不能消除数据偏差
Boosting逐步增加模型改善当前损失GBDT、直方图提升学习率、复杂度和轮数共同控制
Stacking基模型预测用于训练二层模型StackingClassifier必须使用折外预测,不以拟合预测冒充

随机森林还引入候选特征随机化。集成不一定优于简单模型,延迟、内存、可解释性和更新成本也需考虑。

11.2 原生集成模型比较 ​

python
from sklearn.datasets import make_classification
from sklearn.ensemble import (
    BaggingClassifier, GradientBoostingClassifier,
    HistGradientBoostingClassifier, RandomForestClassifier, StackingClassifier,
)
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

X, y = make_classification(n_samples=500, n_features=10,
                           n_informative=6, random_state=42)
models = {
    "bagging": BaggingClassifier(n_estimators=30, random_state=42),
    "gbdt": GradientBoostingClassifier(n_estimators=60, random_state=42),
    "hist": HistGradientBoostingClassifier(max_iter=60, random_state=42),
    "stack": StackingClassifier(
        estimators=[
            ("linear", make_pipeline(StandardScaler(), LogisticRegression(max_iter=1000))),
            ("forest", RandomForestClassifier(n_estimators=50, random_state=42)),
        ],
        final_estimator=LogisticRegression(max_iter=1000), cv=3,
    ),
}
cv = StratifiedKFold(3, shuffle=True, random_state=42)
for name, model in models.items():
    print(name, cross_val_score(model, X, y, cv=cv, scoring="roc_auc").mean())

这是开发阶段比较,不提供最终测试结果。Stacking 内部交叉验证生成二层训练输入,外层评估整个组合。时间或分组任务还需检查内外层划分,不能原样照搬普通分层折。

11.3 外部梯度提升库 ​

实现常见特点重点参数与边界
XGBoost多种树构建方式、正则化和生态工具树深、学习率、轮数、行列采样;原生类别处理需明确类型与配置
LightGBM直方图算法、叶子优先生长num_leaves、min_child_samples 与深度共同控制复杂度
CatBoost类别处理和有序提升机制显式声明类别字段,并非任何数据都无需预处理

它们是可选工具,不是必经的高级等级。早停集属于选择数据,不能使用最终测试集;其编码和缩放需与训练一致,普通流水线不一定自动转换传给内部模型的评估集。记录最佳轮数与版本,明确重训与导出策略。

12.1 先修改评估设计,再增加算法 ​

场景优先动作禁止的捷径
类别不平衡分层验证、PR 指标、成本阈值、类别权重只看准确率;切分前 SMOTE
时间预测滚动回测、滞后特征用未来填补过去;随机划分重叠窗口
高维稀疏文本稀疏流水线、正则化、TF-IDF全量拟合词表或强行转密集矩阵
分布漂移区分数据、标签比例与概念漂移把输入变化直接等同于效果下降
多输出明确每个目标指标与联合约束把独立多输出称为共享表征多任务学习

SMOTE 等采样只作用于训练折,通常使用 imbalanced-learn 的采样流水线;验证和测试保持真实分布。类别权重和采样会影响概率含义,需要独立评估校准,不能只看 F1。

12.2 Q-Learning 是另一条学习路径 ​

强化学习优化序贯动作的累积回报,不是监督分类的最后一个算法。表格 Q-Learning 用“即时奖励 + 折扣后的下一状态最大价值”修正状态-动作价值;真正终止状态不引入下一状态价值。

下面是六格走廊玩具环境,0 向左、1 向右。增加每回合步数上限防止无限循环,随机打破价值相同的动作平局。

python
import numpy as np

rng = np.random.default_rng(42)
n_states, n_actions = 6, 2
Q = np.zeros((n_states, n_actions))
alpha, gamma, epsilon = 0.1, 0.9, 0.2
successes = 0
for episode in range(500):
    state = 0
    for step in range(100):
        if rng.random() < epsilon:
            action = int(rng.integers(n_actions))
        else:
            tied = np.flatnonzero(Q[state] == Q[state].max())
            action = int(rng.choice(tied))
        next_state = int(np.clip(state + (1 if action == 1 else -1), 0, n_states - 1))
        terminated = next_state == n_states - 1
        reward = 1.0 if terminated else -0.01
        target = reward if terminated else reward + gamma * Q[next_state].max()
        Q[state, action] += alpha * (target - Q[state, action])
        state = next_state
        if terminated:
            successes += 1
            break
print("successful training episodes:", successes)
print("Q-table:", Q)
print("greedy actions for nonterminal states:", Q[:-1].argmax(axis=1))

步数上限是人为截断,不等同于环境终止,所以只在到达终点时清除自举项。固定探索率和有限回合不构成收敛保证,训练成功次数也不是独立策略评估。真实任务需要状态和奖励设计、独立评估、安全探索;大状态空间需函数逼近,超出此处范围。

13.1 随机种子不是全部 ​

实验至少记录数据快照标识、划分规则、特征可用时刻、代码和依赖版本、完整参数、指标和资源。含个人信息的原始样本不要直接写入日志,应做脱敏、访问控制与保留期限管理。

text
数据版本 + 划分规则 + 特征契约 + 代码/依赖版本
                        |
                        v
               训练流水线与实验记录
                        |
                        v
模型制品 + 阈值/类别映射 + 评估报告 + 回滚版本
                        |
                        v
             上线监控 -> 标签回流 -> 再评估

相同种子不保证跨平台、跨版本、不同并行实现逐位一致。可复现要求固定环境和数据,稳健性则需要换种子或重采样观察波动,而不是挑选最好的一次。

13.2 遵循 sklearn 契约 ​

优先组合现有组件。自定义时,构造函数只保存超参数,fit 学习带下划线的状态并返回自身,predict 验证拟合状态和维度;分类器保存真实类别映射。不能忽略标签却声称实现监督分类器。

下面用最近类别中心演示,Mixin 在 BaseEstimator 左边。校验辅助方法按 1.5 ~ 1.6 基线使用,迁移新版需检查 validate_data 等接口变化。

python
import numpy as np
from sklearn.base import BaseEstimator, ClassifierMixin
from sklearn.datasets import load_iris
from sklearn.model_selection import cross_val_score
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.utils.validation import check_is_fitted

class NearestCentroidDemo(ClassifierMixin, BaseEstimator):
    def fit(self, X, y):
        X, y = self._validate_data(X, y, dtype=float)
        self.classes_, encoded = np.unique(y, return_inverse=True)
        self.centers_ = np.vstack([
            X[encoded == index].mean(axis=0)
            for index in range(len(self.classes_))
        ])
        return self

    def predict(self, X):
        check_is_fitted(self, ["centers_", "classes_"])
        X = self._validate_data(X, reset=False, dtype=float)
        distances = ((X[:, None, :] - self.centers_[None, :, :]) ** 2).sum(axis=2)
        return self.classes_[distances.argmin(axis=1)]

X, y = load_iris(return_X_y=True)
model = make_pipeline(StandardScaler(), NearestCentroidDemo())
print("CV accuracy:", cross_val_score(model, X, y, cv=5).mean())

这是数值输入、多类别标签的教学组件,未覆盖稀疏输入、样本权重、概率及全部估计器检查;正式封装需使用 sklearn 估计器检查工具验证。持久化自定义类还需稳定可导入的模块路径,避免只存在于交互会话。

14.1 保存推理契约 ​

模型加载属于信任边界

joblib / pickle 加载可能执行任意代码,只加载受信任且来源验证过的制品,不接受用户上传路径或远程任意模型。跨 scikit-learn 版本加载不受支持,应在相同锁定环境验证;版本检查不能替代来源校验。

下面两个代码块配套使用:先训练生成本地制品,再将服务块作为启动模块。四个合成传感器字段用于模拟告警,不是生产告警系统。训练块写入当前目录,重复运行会覆盖同名制品。

14.2 训练并保存完整流水线 ​

python
from pathlib import Path
import joblib
import pandas as pd
import sklearn
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

fields = ["signal_a", "signal_b", "signal_c", "signal_d"]
X, y = make_classification(n_samples=800, n_features=4, n_informative=3,
                           n_redundant=0, random_state=42)
frame = pd.DataFrame(X, columns=fields)
X_train, X_test, y_train, y_test = train_test_split(
    frame, y, stratify=y, random_state=42,
)
model = make_pipeline(StandardScaler(), LogisticRegression(max_iter=1000))
model.fit(X_train, y_train)
print("test AUC:", roc_auc_score(y_test, model.predict_proba(X_test)[:, 1]))
artifact = {
    "model": model, "fields": fields, "positive_label": 1,
    "threshold": 0.5, "version": "sensor-demo-v1",
    "sklearn_version": sklearn.__version__,
}
path = Path("sensor_model.joblib")
joblib.dump(artifact, path)
loaded = joblib.load(path)
assert (loaded["model"].predict(X_test) == model.predict(X_test)).all()
print("saved:", path.resolve())

0.5 是固定教学阈值,不是业务最优阈值。模型版本应绑定字段定义、单位、数据快照与环境锁文件;真实项目应补齐元数据与制品校验,而非只记录 sklearn 版本。

14.3 输入校验与启动生命周期 ​

部署额外依赖:

bash
python -m pip install "fastapi>=0.110,<1" "pydantic>=2,<3" "uvicorn>=0.29,<1"

把下一块保存为本地服务模块,在包含制品的目录启动。生命周期加载一次模型,具名字段避免位置数组错位;严格类型、有限值检查和额外字段禁止用于拒绝错误输入。

python
import os
from contextlib import asynccontextmanager
from pathlib import Path
import joblib
import pandas as pd
import sklearn
from fastapi import FastAPI
from pydantic import BaseModel, ConfigDict, FiniteFloat

FIELDS = ["signal_a", "signal_b", "signal_c", "signal_d"]

class SensorInput(BaseModel):
    model_config = ConfigDict(extra="forbid", strict=True)
    signal_a: FiniteFloat
    signal_b: FiniteFloat
    signal_c: FiniteFloat
    signal_d: FiniteFloat

@asynccontextmanager
async def lifespan(app):
    # 路径只由受信任的部署配置提供,不来自请求参数。
    path = Path(os.environ.get("MODEL_PATH", "sensor_model.joblib"))
    artifact = joblib.load(path)
    if artifact["sklearn_version"] != sklearn.__version__:
        raise RuntimeError("model and runtime sklearn versions differ")
    if artifact["fields"] != FIELDS:
        raise RuntimeError("feature schema mismatch")
    model = artifact["model"]
    if list(model.feature_names_in_) != FIELDS:
        raise RuntimeError("model feature names mismatch")
    app.state.artifact = artifact
    app.state.positive_column = list(model.classes_).index(artifact["positive_label"])
    yield

app = FastAPI(title="Sensor Demo API", lifespan=lifespan)

@app.post("/predict")
def predict(data: SensorInput):
    artifact = app.state.artifact
    frame = pd.DataFrame([data.model_dump()], columns=FIELDS)
    probability = float(artifact["model"].predict_proba(frame)[0, app.state.positive_column])
    return {
        "is_alert": probability >= artifact["threshold"],
        "probability": probability,
        "model_version": artifact["version"],
    }

假设服务模块命名为 main.py,在相同目录执行下面命令。默认仅监听本机,生产环境不要开启开发热重载。

bash
python -m uvicorn main:app --host 127.0.0.1 --port 8000

向本地 /predict 发送 POST 请求,请求体如下:

json
{ "signal_a": 0.2, "signal_b": -0.4, "signal_c": 1.0, "signal_d": 0.1 }

验收:合法输入返回布尔告警、概率、版本;缺少或额外字段、字符串冒充数值返回 422;非有限值被解析或校验拒绝;制品缺失时启动失败,而非带着空模型提供服务。

14.4 到生产系统的差距 ​

  • 服务保护:认证、授权、限流、请求大小限制、超时和隐私保护;示例不应直接暴露公网。
  • 资源治理:同步预测避免直接阻塞异步事件循环,但仍需限制线程、内部并行和并发;多个进程通常各持一份模型。
  • 发布治理:制品签名/哈希、环境锁、启动探针、灰度和回滚;新模型校验通过再切流,不在请求中训练。
  • 一致性:离线在线特征语义和窗口一致;概率不是置信区间,字段合法不意味着属于训练分布。
  • 监控闭环:监控输入缺失、未知类别、漂移、延迟、错误和输出分布;标签到达后评估效果与分群误差,再决定重训。

15.1 明确项目产物,而不是重复堆示例 ​

项目可运行起点从教学到业务需补齐验收标准
房价回归第七章回归比较本地房源数据、地理/时间留出、时刻可用特征优于常数基线,报告原单位及区域误差
用户流失第五章阈值示例标签定义、观察/预测窗口、挽留成本与容量冻结阈值后的召回、误报、成本
用户分群第十章聚类示例RFM、窗口稳定性、原单位画像稳定可解释,独立营销实验验证收益
特征工程优化第九章选择与搜索固定划分、原模型对照、消融和成本记录同一协议下改善且无泄露
本地推理服务第十四章配套块环境锁、制品安全、鉴权、监控、回滚错误字段拒绝、加载前后预测一致、版本可定位

房价任务如采用 California Housing,首次获取可能需联网,目标单位为十万美元且有顶端截断,它不是单套房交易数据。不能把合成数据分数当作真实房价效果,也不要无条件给树模型添加标准化。

15.2 文本分类补充:最小完整例子 ​

先划分原始文本,再在训练集拟合 TF-IDF 和朴素贝叶斯。中文需合适的分词或字符 n-gram;此处短英文样本只为了便于检查流程。

python
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics import classification_report
from sklearn.model_selection import train_test_split
from sklearn.naive_bayes import MultinomialNB
from sklearn.pipeline import make_pipeline

messages = [
    "win a free prize now", "claim free money today", "exclusive cash offer",
    "free reward click now", "win cash bonus today", "claim your prize offer",
    "meeting agenda for tomorrow", "project review notes attached", "team lunch next week",
    "please review the report", "tomorrow project meeting", "team schedule update",
]
labels = [1] * 6 + [0] * 6
X_train, X_test, y_train, y_test = train_test_split(
    messages, labels, test_size=0.33, stratify=labels, random_state=42,
)
model = make_pipeline(TfidfVectorizer(ngram_range=(1, 2)), MultinomialNB(alpha=1.0))
model.fit(X_train, y_train)
print(classification_report(y_test, model.predict(X_test), zero_division=0))
print("new message:", model.predict(["please review tomorrow meeting agenda"]))

四条测试样本不足以判断模型好坏。真实项目需要更多独立邮件,去重并按发送者、模板或时间划分,审计敏感信息,报告误拦正常邮件成本;不能对全部邮件拟合词表再交叉验证。

15.3 项目完成的统一定义 ​

交付包括任务定义、数据契约、划分依据、基线与候选实验、冻结结果、误差分析、环境和制品版本、推理约定、风险和回滚策略,而非只有模型文件。

上线后分布变化,优先检查数据链路和特征语义,再决定重训。A/B 实验关注干预效果,不仅是离线排序;高风险领域需要人工复核、合规评估和部署边界。

16.1 从症状定位问题 ​

症状先查什么再考虑什么
训练高分、验证低分重复实体、划分方式、数据量、标签质量正则化、降低复杂度、增加有效数据
训练验证都差标签定义、特征、实现错误和基线增强表达能力、改善优化
分数好得不合理事后字段、全量处理、重复样本独立留出与泄露审计
逻辑回归不收敛尺度、异常值、共线性、求解器兼容性调整正则化和迭代数,不屏蔽警告
搜索拟合失败折内类别、特征数、缺失、参数组合明确报错定位,不静默忽略
线上字段数不同是否只存分类器、列名/顺序/编码保存完整流水线和契约
未知类别导致退化编码策略、类别分布回退、告警与再训练
内存暴涨稀疏转密集、特征交叉、并行复制降维、控制并发、批预测
AUC 高但告警不可用正类、概率列、阈值、容量PR 指标、校准、成本分析

16.2 参数速查 ​

对象参数记忆要点
划分test_size、stratify、random_state分层不能代替分组或时间隔离
Ridge / Lassoalpha越大约束越强,不同模型数值不可直接横比
LogisticRegression / SVCC越大正则越弱,迭代上限不是复杂度开关
树与森林max_depth、min_samples_leaf、n_estimators前两项控制树复杂度,树数增加计算和内存
KNNn_neighbors、weights、metric与尺度和局部密度共同影响结果
K-Meansn_clusters、n_init、random_state多次初始化降低坏局部解风险,不证明簇数合理
DBSCANeps、min_samples依赖尺度和密度,-1 表示噪声
PCAn_components、whiten保留方差不等于保留预测能力,白化改变尺度
搜索cv、scoring、refit、n_jobs先设计正确协议,再增加预算
流水线步骤名加双下划线参数名搜索覆盖每折需要学习的处理步骤

16.3 官方资源与学习路线 ​

官方 stable 页面会更新,复现时切换到安装版本对应文档。建议路线:独立数据上的基线 → 无泄露流水线 → 评估和阈值 → 算法与特征对照 → 复现和部署 → 监控与业务验收。

结语

机器学习的高级能力,不是使用更多算法,而是能解释数据从哪里来、指标为什么可信、预测在什么条件下有效,以及模型失效时如何发现和回滚。