模型训练、调参与保存

1. 最短训练路径

import pandas as pd
from hscredit import LightGBM

features = ["衡枢鉴真分老客版", "近六个月非银多头机构数", "青云24"]
df = pd.read_excel("examples/hscredit_yyp.xlsx")
model = LightGBM(
    n_estimators=300,
    learning_rate=0.05,
    random_state=42,
    n_jobs=4,
    target="FPD",
    eval_metric=["auc", "ks"],
    early_stopping_rounds=20,
)
model.fit(df[features + ["FPD"]])
probability = model.predict_proba(df[features])[:, 1]
score = model.predict_score(df[features])

这段代码演示接口,效果评估应另留测试集。标签必须同时包含 0 和 1,1 表示坏样本。 也可以使用 fit(X, y, sample_weight=weights);显式 y 优先,并从特征中移除 target 列。 DataFrame 预测和验证集会按训练字段重新排序;缺字段、重名列、标签或权重长度不符会直接报中文错误。

这一输入契约也适用于概率校准器、概率评分卡、传统评分卡和调参器:显式 y 优先, 但 target 列始终剔除;样本参数按位置而不是 pandas 索引标签对齐。 正类概率按真实 classes_ 查找,ProbabilityScoreCard(positive_class=1) 默认选择坏样本类别 1, 不能以“最后一列”代替正类。失败的重复训练不可继续使用旧的预测状态。

ScoreCard.fit 会重新训练自身创建的 LR,并按当前 PDO 等配置刷新刻度;显式提供的预训练 LR 仍可复用。 ScoreCard(pipeline=...) 要求 LR 前的输入确实是相同的 WOE;若 Pipeline 使用分箱索引、缩放或其他变换, 请用 ProbabilityScoreCard(model=pipeline) 保留完整预处理,不能把它静默替换为 WOE。

早停需要验证数据。包装模型未收到 eval_set 时,会按 validation_fraction 从训练输入划出验证集; 不启用早停时使用全部输入。训练权重、XGBoost base_margin、LightGBM init_score、 CatBoost baseline 会跟随自动划分同步切分。

类别字段保留原来的 DataFrame 类型:LightGBM 可使用 pandas category,XGBoost 使用其原生 enable_categorical=True 配置;CatBoost 可传 cat_features=["商品类别"]。 包装模型以 DataFrame、二维数组或框架支持的稀疏矩阵为输入;框架专属容器需按该框架支持情况使用。

2. 参数如何复用

from sklearn.base import clone
from xgboost import XGBClassifier
from hscredit import XGBoost

native_params = XGBClassifier(
    n_estimators=50, max_depth=3, max_bin=64, n_jobs=2, random_state=42,
).get_params()
source = XGBoost(**native_params, target="FPD")
model = clone(source)                   # 保留 target、原生扩展参数和自定义配置
model.set_params(max_bin=128)           # 可用于 Pipeline / GridSearchCV
model.fit(df[features], df["FPD"])
actual_params = model.get_native_params()
native_model = model.get_native_model()
  • get_params() 返回构造配置,适合克隆、搜索和重新配置。

  • get_native_params() 返回实际训练使用的原生构造参数;自动类别权重等计算值在这里检查。

  • get_native_model() 返回已训练的底层对象,可以直接使用原生方法。

  • params={...} 继续支持。初始化时保留历史规则:该字典优先于同名关键字;后续 set_params() 的显式更新优先于旧字典,不会修改调用者传入的字典。

  • 常用别名会被规范化,例如 CatBoost 的 n_estimators、max_depth、reg_lambda, LightGBM 的 num_iterations、feature_fraction,XGBoost 的 eta、lambda。

  • 各框架的专属参数、损失函数和评估函数保留原生语义。XGBoost 的自定义评估函数返回数值, LightGBM 返回 (名称, 数值, 是否越大越好),CatBoost 接收原生评估对象。 predict() / predict_proba() 的额外预测参数也会传递给原生模型。

特征重要性有两个用途:feature_importances_ 始终与输入字段顺序一致,供 sklearn 筛选器使用; get_feature_importances(importance_type=...) 返回排序后的具名 Series,供查看和报告使用。

3. 保存完整模型与训练过程

model.save("artifacts/model.joblib")
restored = type(model).load("artifacts/model.joblib")

restored.training_summary_              # 最近一次训练的状态、参数、用时、依赖版本等
restored.training_history_              # 历次训练,包括失败和中断
restored.evals_result_                  # 框架实际产生的逐轮指标
restored.best_iteration_
restored.predict_score(df[features])

默认保存使用 cloudpickle,支持 Notebook 中定义的函数、损失和回调,原有 joblib/pickle/dill 引擎选项与旧文件加载方式保留。完整模型文件写入成功后才替换目标,序列化失败不会破坏原文件。 未实际计算的指标不会伪造为训练曲线;查看逐轮验证曲线时,应配置 eval_set 或启用自动早停。

保存方式

内容与用途

save("model.joblib")

完整 Python 对象:模型、评分转换器、训练历史及关联调参器

save_artifact(...)

既有统一制品接口,可显式指定 engine="cloudpickle"

save("model.json")

支持框架的 JSON 清单,配套原生模型、评分转换器和 .state.pkl 完整状态文件;需一起移动

save_model(...)

原生模型文件与评分转换器附属文件,保留已有接口

get_native_model()

直接调用底层框架的导出、树结构、叶节点、预测或其他专属能力

save_inference("model.joblib")

风险模型/LR 的轻量推理对象:移除 tuner、训练历史和划分索引,保留预测、评分和字段契约;不修改原对象

输入预处理也是复用的一部分:推荐把编码、标准化与模型放进 sklearn Pipeline 一起保存; 如果预处理在外部完成,必须同时保存已拟合的预处理器和字段顺序。 训练记录保存配置和模型过程,不自动复制作为位置参数传入的原始训练矩阵。 CatBoost 验证集 Pool 的原生句柄不能深拷贝,训练记录保留其标签、权重、基线与字段结构。

Python 完整对象不承诺任意跨版本加载。版本迁移时,先在新环境运行验证脚本; 若原框架的对象结构变化,使用该框架支持的原生导出和加载方式。记录中的 依赖版本 用于定位来源环境。

4. 在 tune 中直接使用 Optuna

tune() 返回最佳模型;原始模型和返回模型的 .tuner 指向同一调参器。 原始模型本身不会被替换为最佳模型。

import optuna
from pathlib import Path
from hscredit import RandomForest

Path("artifacts").mkdir(exist_ok=True)
study = optuna.create_study(
    direction="maximize",
    study_name="FPD随机森林",
    storage="sqlite:///artifacts/tuning.db",
    load_if_exists=True,
    sampler=optuna.samplers.TPESampler(seed=42),
    pruner=optuna.pruners.MedianPruner(n_startup_trials=5),
)

def search(trial):
    depth = trial.suggest_int("max_depth", 2, 8)
    return {
        "max_depth": depth,
        "min_samples_leaf": trial.suggest_int("min_samples_leaf", 5, 30),
        # 可继续调用 suggest_float、suggest_categorical,或用 if 定义条件空间。
    }

def on_trial(study, trial):
    print(trial.number, trial.state, trial.values)

model = RandomForest(n_estimators=100, target="FPD", random_state=42, n_jobs=4)
best = model.tune(
    df[features + ["FPD"]],
    search_space=search,
    study=study,
    callbacks=[on_trial],
    metric="auc",
    cv=3,
    n_trials=20,
    catch=(ValueError,),
    gc_after_trial=True,
    artifact_dir="artifacts/trials",
)

先创建数据库父目录。传入已有 Study 时,采样器、剪枝器与持久化连接由该 Study 管理; 不要在包装器上重复配置另一套 Study 设置。direction 必须与已有 Study 一致。 已有字典搜索空间、Optuna 分布对象、多目标、手工点、Hyperopt/skopt 风格声明和各类可视化保持可用。

需求

入口

原生 Trial 条件采样

search_space=lambda trial: {...}

已有 Study、采样器、剪枝器

study=study;自行创建 Study 时也可用 sampler=、pruner=

Trial 完成后的回调

callbacks=[fn],签名与 Study.optimize 一致

异常策略、垃圾回收

catch=(...)、gc_after_trial=True

底层模型 fit 参数

fit_params={"callbacks": [...], ...}

每折创建原生训练回调

fit_params_factory(trial, fold_index) -> dict

分组、时间、重复验证

cv=GroupKFold(...)、cv=TimeSeriesSplit(...) 或索引迭代器;必要时 groups=

原生完整目标函数

trial_objective=fn,fn(trial) 完全接管试验;与既有 objective(y, p) 评分函数区分

原生高级操作

model.tuner.study_.enqueue_trial(...)、stop()、ask()、tell() 等原生 Study 方法

fit_params_factory 每折调用一次;最终重训会以 (None, None) 调用,应在此分支返回最终训练参数, 避免把绑定某个 Trial 的剪枝回调复用到最终模型上。已知样本级参数如 sample_weight、base_margin、 init_score、baseline 及 Pipeline 中对应的 步骤名__参数 会按折同步切分。 外部固定 eval_set 会按原生语义传递,应由调用者确保它不包含外层验证或最终测试样本。

自动评分采用外层交叉验证;包装模型内部早停和原生提升器的内部分割均只使用训练折。 单目标试验每完成一折执行一次 trial.report() / should_prune()。 多目标继续返回各目标结果与 Pareto 前沿,不调用 Optuna 不支持的多目标 report()。

为了维持现有 CPU 预算和可中断行为,Trial 依旧顺序执行,n_jobs 分配给当前模型。 这不是将 n_jobs 直接当成 Study.optimize 的并行试验数。

5. 保留每折内容、继续搜索、复用最佳模型

tuner = best.tuner
tuner.get_trial_result(0)                # 参数、每折模型、训练/验证预测、指标、曲线和异常
tuner.get_oof_predictions()              # 样本位置、真实标签、折外预测概率、验证次数
tuner.get_optimization_history()         # 保留原有 Optuna 风格历史表
tuner.get_best_model() is best           # True:不会再次训练

tuner.save("artifacts/tuner.joblib")
restored = type(tuner).load("artifacts/tuner.joblib")
restored.enqueue_trial({"max_depth": 4})
restored.fit(df[features + ["FPD"]], n_trials=10)
new_best = restored.get_best_model()

完整调参制品包括输入数据、标签和权重,适合继续原任务;换数据集或改变验证口径时,应创建新的 Study。 SQLite 保留 Optuna 试验状态;artifact_dir 额外逐折写入模型过程,可在新调参器中通过 Study 记录的路径恢复。 只使用 SQLite、不保存完整对象或配置制品目录,不能恢复已丢失的 Python 模型和预测数组。 同样,只调用原生 trial_objective 时,内部训练过程由用户函数管理,自动逐折记录不适用。

通过 ModelTuner(..., retention=...) 或 model.tune(..., retention=...) 选择结果保留策略。 所有策略都保留完整 Optuna Study、各试验指标、搜索参数、状态与最佳模型缓存,不改变训练和搜索计算。

retention

常驻结果

OOF 与折模型

full

所有折模型、训练/验证预测、完整训练记录

全部可用,默认行为

predictions

训练/验证预测、指标、精简训练记录

OOF 可用,不保留折模型

summary

指标、曲线、样本数、早停轮数、异常

不保留逐样本数组;请求 OOF 会明确报错

best

当前最佳试验完整结果;其余仅摘要

最佳试验可用;其他试验的 OOF 不再保留

disk

逐折制品索引与摘要

必须设置 artifact_dir,逐折写盘、按需读取

未指定 retention 时继续兼容 store_models=True/False,分别对应 full/predictions。 显式 retention 优先。disk 与“full 加 artifact_dir”不同:每折写入成功后就释放其常驻模型和数组, get_trial_result(n, load=False) 只返回摘要;默认 load=True 返回完整结果但不把折模型放回常驻缓存。 get_oof_predictions() 逐折读取并聚合,重复验证仍取均值,未参加验证的样本保留缺失值。 磁盘档的完整制品依赖其折文件,须同时保留 artifact_dir;缺失文件会明确报错,不重新训练补造结果。 将整个折制品目录复制到其他位置后,可用 ModelTuner.load(path, artifact_dir="新目录") 恢复读取。 若使用 SQLite 等外部 Study 存储,还需独立维护对应数据库的可访问性。

输入数据默认仍保留以支持最终重训与续跑。取得最佳模型后可调用 tuner.release_training_data(), 释放调参器持有的输入、标签、权重、CV 索引和已知样本级 fit 参数;该操作不删除折结果或磁盘制品。 缓存最佳模型仍可预测;重训及完整标签的 OOF 需要重新 fit,续跑时也必须重新提供所需样本参数。 save_inference 不携带整个搜索历史,适合部署;完整 save 继续满足复盘与续跑用途。 上述清理仅针对库管理的字段,不会任意删除用户函数闭包捕获的数据或底层模型推理所需的状态。 自定义函数返回不能放入 Optuna JSON 属性的模型对象或损失对象时,完整配置保存在调参制品中。

最佳模型只训练一次并缓存。Boosting 最终默认使用各折最佳轮数的中位数关闭内部早停,在全部输入上训练; 可用 get_best_model(refit=True, full_data=False) 显式重训并保留内部验证。 中断会继续抛出 KeyboardInterrupt,不会悄悄开始最终重训,但已经完成的试验和历史仍可检查、保存。

6. 两种调参入口都能查看完整搜索过程

两种方式均保留同一个原生 Optuna Study,不只保留最佳参数。训练完成后, 既可以通过 visualization 自动绑定 Study,也可以把 get_study() 的返回值直接交给 Optuna。

from hscredit import RandomForest, ModelTuner

# 方式一:model.tune。原模型和最佳模型均关联同一个搜索器。
model = RandomForest(n_estimators=30, random_state=42, n_jobs=2)
best = model.tune(
    df[features], df["FPD"], search_space={"max_depth": [2, 3, 4, 5]},
    n_trials=8, cv=3, n_jobs=2,
)
assert model.tuner is best.tuner
best.tuner.visualization.plot_optimization_history().show()
best.tuner.visualization.plot_intermediate_values().show()

# 方式二:直接使用 ModelTuner,fit 后立即可分析,不需要先调用 get_best_model。
tuner = ModelTuner(
    RandomForest(n_estimators=30, random_state=42, n_jobs=2),
    search_space={"max_depth": [2, 3, 4, 5]}, metric="auc", cv=3, n_jobs=2,
)
tuner.fit(df[features], df["FPD"], n_trials=8)
tuner.visualization.plot_timeline().show()
tuner.visualization.plot_rank(params=["max_depth"]).show()

# 与原生 Optuna 完全互通。
import optuna
study = tuner.get_study()                 # 与 tuner.study_ 是同一对象
optuna.visualization.plot_param_importances(study).show()
study.trials_dataframe()                  # 全部试验的参数、状态、得分、时间及属性
study.trials[0].intermediate_values        # 每折结束时的累计平均目标值
study.trials[0].user_attrs["各折指标"]     # 每折、每个目标的实际分数
study.sampler                            # 本次搜索的原生采样器
study.pruner                             # 本次搜索的原生剪枝器

visualization 根据当前安装的 Optuna 动态提供全部公开入口,包括其后续新增的绘图函数。 用 dir(tuner.visualization) 或 dir(tuner.visualization.matplotlib) 可查看当前版本提供的函数。 原生参数和返回对象不变,只自动传入当前 Study;显式传 study=[study1, study2] 可比较多个搜索。

可查看的内容

原生函数

搜索得分历史、中间值、时间轴

plot_optimization_history、plot_intermediate_values、plot_timeline

参数重要性、切片、等高线、排名、平行坐标

plot_param_importances、plot_slice、plot_contour、plot_rank、plot_parallel_coordinate

得分分布

plot_edf

多目标 Pareto 前沿与超体积历史

plot_pareto_front、plot_hypervolume_history

搜索终止改进诊断

plot_terminator_improvement

Matplotlib 后端也完整开放,返回原生 Axes 或 Axes 数组:

import matplotlib.pyplot as plt

ax = tuner.visualization.matplotlib.plot_optimization_history()
ax.figure.savefig("search-history.png", dpi=150, bbox_inches="tight")
plt.show()

这些图仍遵循 Optuna 原生绘图条件: 多目标单曲线图的 target 是函数,例如 target=lambda trial: trial.values[1]; 超体积图需要多目标 Study 和显式 reference_point。 终止改进图按 Optuna 版本和 evaluator 要求使用相应依赖与足够的试验; Optuna 低于 4.9 时,自动单目标交叉验证会通过官方接口记录专用 CV 分数。 Optuna 4.9 起弃用了 terminator,默认不再为此调用弃用接口;常规各折指标、 中间值和曲线仍完整保留。若确需旧终止改进图,可显式配置 ModelTuner(..., record_terminator_scores=True),此时保留上游弃用提示。 只有一折或用户完全接管的 trial_objective,需按原生要求提供适用的误差估计或记录。

plot_intermediate_values 默认显示各交叉验证折结束时的累计平均目标值。 每个模型的逐轮学习曲线在 tuner.get_trial_result(编号)["各折"][折编号]["评估曲线"] 中; 失败、剪枝的 Trial、时间和已上报的中间值也保留在 Study,不会为绘图而过滤。 多目标的逐折分数保存在 各折指标,不伪造 Optuna 不支持的多目标 intermediate_values。

原有 tuner.plot_* 便捷方法继续支持整数 target 与 HSCredit 搜索空间参数名转换; visualization 则使用 Optuna 原生 params / target 语义。 保存最佳模型或调参器后,重新加载仍可通过相同入口画图;访问绘图入口本身不会重新训练模型。

7. 可执行 Notebook 演示

python scripts/validate_examples.py --pattern 28_model_workflow.ipynb

在 Jupyter 中打开 模型工作流 Notebook,按单元格顺序执行即可。 它优先使用 examples/hscredit_yyp.xlsx 的指定三项特征和 FPD;没有工作簿时使用固定种子的模拟数据。 覆盖双 API 防泄漏、字段顺序、概率校准、五档结果保留的数值等价、按需读取、 完整与推理制品保存恢复、续跑和显式资源释放。缺失值只使用训练集统计量处理。 输出默认位于独立临时目录,可在配置单元格修改路径。示例不再依赖独立 .py 演示脚本。