超参数搜索:入门、指标、损失与结果复盘
ModelTuner 使用 Optuna 采样候选参数,在外层交叉验证中评估候选,保留试验过程,再用选中的参数训练最终模型。已有 HSCredit 模型时,优先用 model.tune(...),直接得到可预测的最佳模型。
完整可运行演示:tuning_workflow.ipynb。所有代码格使用仓库真实放款数据;模型数量和训练轮数控制在演示规模。损失选择与框架回调详见 损失指南 和 losses_workflow.ipynb。
先选对入口
需求 |
推荐入口 |
返回内容 |
|---|---|---|
已有 HSCredit 模型,想得到最优模型 |
|
已重训的模型,搜索器在 |
要管理 Study、逐折结果或 sklearn Pipeline |
|
|
已有参数化评估函数 |
|
可离线评估、训练监控及调参的 |
已有 BaseLoss |
|
用该损失训练,按其同口径指标搜索 |
loss 与业务评价不同 |
|
训练优化代理损失,搜索按业务口径选优 |
搜索需要关联/条件参数 |
|
每个 trial 的模型参数字典 |
完全自定义训练和评分循环 |
|
原始单目标数值或多目标序列,由函数自行管理训练过程 |
区分训练目标、搜索指标与试验函数
名称 |
接收内容 |
用途 |
|---|---|---|
|
|
注入 HSCredit XGBoost、LightGBM、CatBoost 的训练目标 |
|
字符串、 |
决定 trial 的优劣 |
模型 |
对应模型支持的评估配置 |
训练曲线与早停;不自动等于搜索 metric |
|
历史评分目标名/函数 |
保留兼容的 |
|
|
完全接管一轮搜索,可用于外部训练流程 |
loss.metric() 是同口径训练代理损失,始终最小化;loss.business_metric() 是对应真实业务指标,方向随指标变化。不要把训练 loss 的低值直接当作 AUC、利润或坏账率的改进。
环境与数据准备
安装 pip install -e ".[boost,tune,dev]",从仓库根目录或 examples 目录执行。下文代码按顺序复用变量。
数据约定:三项特征为 衡枢鉴真分老客版、近六个月非银多头机构数、青云24;FPD=1 表示坏样本。先划出独立测试集,再在训练数据内做搜索和早停。示例使用 LightGBM 原生缺失值处理,Pipeline 示例则逐折拟合预处理器。
from pathlib import Path
from tempfile import mkdtemp
import sys
repository_root = next(
(path for path in (Path.cwd(), Path.cwd().parent) if (path / "hscredit").is_dir()),
None,
)
if repository_root is None:
raise FileNotFoundError("请从仓库根目录或 examples 目录启动 Notebook")
if str(repository_root) not in sys.path:
sys.path.insert(0, str(repository_root))
import numpy as np
import pandas as pd
import optuna
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split, TimeSeriesSplit
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from hscredit import LightGBM, ModelTuner
from hscredit.core.models.tuning import Integer, Real, Categorical
from hscredit.core.models.losses import (
BaseLoss, BaseMetric, FocalLoss, WeightedBCELoss, AUCMetric, make_metric,
)
optuna.logging.set_verbosity(optuna.logging.WARNING)
FEATURES = ["衡枢鉴真分老客版", "近六个月非银多头机构数", "青云24"]
data = pd.read_excel(repository_root / "examples" / "hscredit_yyp.xlsx")
data = data.loc[data["FPD"].isin([0, 1])].copy()
X = data[FEATURES].apply(pd.to_numeric, errors="coerce").replace([np.inf, -np.inf], np.nan)
y = data["FPD"].astype(int)
amount = pd.to_numeric(data["放款金额"], errors="raise")
X_train, X_test, y_train, y_test, amount_train, amount_test = train_test_split(
X, y, amount, test_size=0.3, stratify=y, random_state=42,
)
print(f"训练样本数:{len(X_train)};测试样本数:{len(X_test)}")
1. 已有模型:用 .tune() 获取最佳模型
最少只需模型、搜索空间和评估指标。metric="auc" 自动最大化,metric="log_loss" 自动最小化,不必重复写方向。已有模型的其他设置继续保留;搜索参数覆盖原配置,显式 fixed_params 的优先级最高。
tune() 返回已经重训好的最佳模型。原模型与返回模型均通过 .tuner 查看同一次搜索,原对象本身不会被替换为最佳模型。
model = LightGBM(n_estimators=30, num_leaves=7, n_jobs=1, random_state=42)
best = model.tune(
X_train, y_train,
search_space={"max_depth": [3, 4], "learning_rate": [0.03, 0.08]},
metric="auc", cv=3, n_trials=3, n_jobs=1,
retention="predictions", show_progress_bar=False,
)
tuner = best.tuner
print("最佳参数:", tuner.best_params_)
print(f"CV 平均 AUC:{tuner.best_score_:.6f}")
print(f"独立测试 AUC:{AUCMetric().evaluate(y_test, best.predict_proba(X_test)):.6f}")
assert tuner.get_best_model() is best
2. 独立调参器:理解 fit 的返回值
ModelTuner.fit() 为兼容旧 API 返回最佳参数字典,不是调参器或模型。先保存 tuner,再调用 get_best_model()。首次取模型会在全部输入训练数据上重训并缓存,后续调用复用缓存;refit=True 显式要求再训练。
以下辅助函数仅减少本 Notebook 的重复配置。每次都新建调参器;不同数据、指标或 CV 口径使用不同 Study。
def make_tuner(metric="auc", **kwargs):
"""创建小规模演示搜索器,默认不做内部早停。"""
options = dict(
model_class=LightGBM,
search_space={"max_depth": [3, 4]},
fixed_params={"n_estimators": 25, "num_leaves": 7, "validation_fraction": 0.0},
metric=metric, cv=3, n_jobs=1, random_state=42,
early_stopping_rounds=None, retention="predictions",
)
options.update(kwargs)
return ModelTuner(**options)
loss_score_tuner = make_tuner(metric="log_loss")
best_params = loss_score_tuner.fit(X_train, y_train, n_trials=2, show_progress_bar=False)
assert isinstance(best_params, dict)
logloss_best = loss_score_tuner.get_best_model()
print(f"CV 平均交叉熵:{loss_score_tuner.best_score_:.6f};越小越好")
3. 搜索空间:先学三种声明,再学条件空间
Integer 是整数范围,Real 是连续范围,Categorical 是离散选项。列表也是离散候选,列表声明不意味着穷举所有组合;默认仍由 TPE 采样。固定参数放入 fixed_params,避免当作搜索维度。
Integer(3, 4) 包含两端;学习率跨数量级时可用 Real(..., prior="log-uniform")。调参器的 n_jobs 主要控制模型并行,trial 按顺序运行。
space = {
"max_depth": Integer(3, 4),
"learning_rate": Real(0.02, 0.1, prior="log-uniform"),
"reg_lambda": Categorical([0.0, 1.0, 3.0]),
}
space_tuner = make_tuner(search_space=space)
space_tuner.enqueue_trial({"max_depth": 3, "learning_rate": 0.05, "reg_lambda": 1.0})
space_tuner.fit(X_train, y_train, n_trials=2, show_progress_bar=False)
print("搜索空间示例最佳参数:", space_tuner.best_params_)
复杂关联用 search_space(trial) -> dict:例如叶子数不能超过 2 ** max_depth,或某模型参数仅在另一参数取特定值时出现。返回的键必须是模型构造参数;Trial 内部参数名也会保留在 Study 中。
def conditional_space(trial):
depth = trial.suggest_int("max_depth", 2, 4)
return {
"max_depth": depth,
"num_leaves": trial.suggest_int("num_leaves", 3, min(15, 2 ** depth)),
"learning_rate": trial.suggest_float("learning_rate", 0.02, 0.1, log=True),
}
conditional_tuner = make_tuner(
search_space=conditional_space,
fixed_params={"n_estimators": 25, "validation_fraction": 0.0},
)
conditional_tuner.fit(X_train, y_train, n_trials=2, show_progress_bar=False)
print("条件空间最佳参数:", conditional_tuner.best_params_)
4. 自定义评估指标:普通函数只需加名称和方向
评估函数签名是 (y_true, y_prob, sample_weight=None, **固定参数) -> float;收到的 y_prob 是一维坏样本概率,不是 logits,也不是两列概率。
用 make_metric 包装后,离线评估、训练监控、ModelTuner 和 sklearn scorer 都可复用同一个对象。没有额外参数时可以直接使用 sklearn 的 brier_score_loss 等概率指标。
def probability_error(y_true, y_prob, sample_weight=None, scale=1.0):
"""均方概率误差,scale 是固定业务系数。"""
error = (np.asarray(y_prob) - np.asarray(y_true)) ** 2
return float(scale * np.average(error, weights=sample_weight))
brier = make_metric(
probability_error, name="均方概率误差", greater_is_better=False, scale=1.0,
)
function_tuner = make_tuner(metric=brier)
function_tuner.fit(X_train, y_train, n_trials=2, show_progress_bar=False)
function_model = function_tuner.get_best_model()
print(f"自定义指标 CV:{function_tuner.best_score_:.6f}")
print(f"自定义指标测试值:{brier.evaluate(y_test, function_model.predict_proba(X_test)):.6f}")
assert brier.direction == "minimize"
# 也可直接传裸函数,但必须显式声明方向,避免误把误差最大化。
raw_function_tuner = make_tuner(metric=probability_error, direction="minimize")
raw_function_tuner.fit(X_train, y_train, n_trials=1, show_progress_bar=False)
5. 需要封装状态:继承 BaseMetric
指标需要多个参数、方法或业务语义时再定义类。实现 __call__ 即可,基类 evaluate() 会校验输入、支持两列概率及显式原始分数转换。额外权重要在 __call__ 中明确接收并使用,不能静默忽略。
class ProbabilityError(BaseMetric):
"""可复用的均方概率误差。"""
def __init__(self):
super().__init__(name="均方概率误差", greater_is_better=False)
def __call__(self, y_true, y_pred, sample_weight=None):
error = (np.asarray(y_pred) - np.asarray(y_true)) ** 2
return float(np.average(error, weights=sample_weight))
custom_metric = ProbabilityError()
class_tuner = make_tuner(metric=custom_metric)
class_tuner.fit(X_train, y_train, n_trials=1, show_progress_bar=False)
assert np.isclose(custom_metric.evaluate(y_test, best.predict_proba(X_test)), brier.evaluate(y_test, best.predict_proba(X_test)))
print("BaseMetric 自动采用方向:", class_tuner.directions)
6. 自定义 loss 训练,固定 metric 选择模型
loss=... 便捷参数支持 HSCredit LightGBM / XGBoost / CatBoost 包装器。设 metric=None 会使用 loss.metric();设 metric="auc" 则训练优化该 loss、搜索最大化 AUC。
ModelTuner(objective=...) 是历史上的搜索评分目标别名,不是模型训练 loss。原生 sklearn estimator 请在模型构造器或 fixed_params 中按该框架签名配置训练目标。
loss = FocalLoss(alpha=0.75, gamma=2.0)
focal_tuner = make_tuner(loss=loss, metric=None)
focal_tuner.fit(X_train, y_train, n_trials=2, show_progress_bar=False)
focal_model = focal_tuner.get_best_model()
print(f"Focal CV 损失:{focal_tuner.best_score_:.6f}")
print(f"Focal 测试损失:{loss.evaluate(y_test, focal_model.predict_proba(X_test)):.6f}")
assert focal_tuner.directions == ["minimize"]
搜索 loss 自身参数时,在条件搜索函数返回模型的 objective 对象。评估指标必须固定:下例搜索 alpha/gamma,但始终用 AUC 比较。不能让每个 trial 的 loss 参数同时改变评价标尺。
def loss_parameter_space(trial):
return {
"objective": FocalLoss(
alpha=trial.suggest_float("alpha", 0.4, 0.8),
gamma=trial.suggest_float("gamma", 0.5, 2.0),
),
"max_depth": trial.suggest_int("max_depth", 3, 4),
}
loss_parameter_tuner = make_tuner(metric=AUCMetric(), search_space=loss_parameter_space)
loss_parameter_tuner.fit(X_train, y_train, n_trials=2, show_progress_bar=False)
print(f"搜索 loss 参数后的 CV AUC:{loss_parameter_tuner.best_score_:.6f}")
7. 从零实现一个自定义训练 loss
Brier loss 对概率求导,适配器负责转换为原始分数导数。逐样本贡献的均值与 __call__ 相同,gradient / hessian 是“样本数 × 平均损失”的导数;is_additive=True 表示允许分批与外部权重。
class BrierLoss(BaseLoss):
"""均方概率误差训练目标。"""
is_additive = True
def __init__(self):
super().__init__("均方概率损失")
def loss_values(self, y_true, y_pred):
return (np.asarray(y_pred) - np.asarray(y_true)) ** 2
def __call__(self, y_true, y_pred):
return float(np.mean(self.loss_values(y_true, y_pred)))
def gradient(self, y_true, y_pred):
return 2 * (np.asarray(y_pred) - np.asarray(y_true))
def hessian(self, y_true, y_pred):
return np.full(len(y_true), 2.0)
custom_loss_tuner = make_tuner(loss=BrierLoss(), metric=brier)
custom_loss_tuner.fit(X_train, y_train, n_trials=2, show_progress_bar=False)
custom_loss_model = custom_loss_tuner.get_best_model()
print(f"自定义 loss 模型测试 Brier:{brier.evaluate(y_test, custom_loss_model.predict_proba(X_test)):.6f}")
8. 金额加权:区分训练权重和评估权重
sample_weight 控制训练;evaluation_weight 控制 CV 选优。只传训练权重不会暗中改变评估口径。两者都应与 X_train 按行位置对齐,调参器按训练/验证折分别切分。
需要金额加权 BCE 时,使用普通 WeightedBCELoss 加权即可。不把全量金额数组绑定进跨折复用的 AmountWeightedLoss;绑定数组需要手动逐折创建损失和指标。
weighted_loss = WeightedBCELoss()
weighted_tuner = make_tuner(loss=weighted_loss, metric=weighted_loss.metric())
weight_train = amount_train.to_numpy(dtype=float)
weighted_tuner.fit(
X_train, y_train, sample_weight=weight_train, evaluation_weight=weight_train,
n_trials=2, show_progress_bar=False,
)
weighted_model = weighted_tuner.get_best_model()
weighted_test = weighted_loss.evaluate(
y_test, weighted_model.predict_proba(X_test), sample_weight=amount_test.to_numpy(dtype=float),
)
print(f"CV 金额加权 BCE:{weighted_tuner.best_score_:.6f};测试值:{weighted_test:.6f}")
9. sklearn Pipeline:预处理在每折内部拟合
模型可传 estimator 实例,Pipeline 内的参数用 步骤名__参数。使用自定义空间,不依赖 HSCredit 特定模型的默认空间。不要先对全量数据拟合填充器/标准化器再做 CV。
pipeline = make_pipeline(SimpleImputer(strategy="median"), StandardScaler(), LogisticRegression(max_iter=500))
pipeline_tuner = ModelTuner(
pipeline, search_space={"logisticregression__C": Real(0.01, 10.0, prior="log-uniform")},
metric=brier, cv=3, n_jobs=1, random_state=42, retention="predictions",
)
pipeline_tuner.fit(X_train, y_train, n_trials=2, show_progress_bar=False)
pipeline_best = pipeline_tuner.get_best_model()
print(f"Pipeline 测试 Brier:{brier.evaluate(y_test, pipeline_best.predict_proba(X_test)):.6f}")
10. 多目标:保留所有不能同时被超越的候选
metric=[...] 声明多个目标,各指标自动给出方向。这里同时最大化 AUC、最小化 Brier。get_pareto_front() 返回 Pareto 候选;默认 get_best_model() 按指标列表顺序优先第一个指标,同分时看下一个,不能代替业务权衡。
multi_tuner = make_tuner(metric=[AUCMetric(), brier], sampler="nsgaii")
multi_tuner.fit(X_train, y_train, n_trials=3, show_progress_bar=False)
pareto = pd.DataFrame([
{"试验编号": trial.number, "AUC": trial.values[0], "均方概率误差": trial.values[1]}
for trial in multi_tuner.get_pareto_front()
])
print(pareto.to_string(index=False))
print("默认最优试验:", multi_tuner.best_trial_.number)
assert multi_tuner.directions == ["maximize", "minimize"]
11. 时间交叉验证:按放款时间模拟向未来预测
按 放款时间 排序,最后 20% 保留作时间外测试。TimeSeriesSplit 只用过去训练、未来验证,不打乱时间。AUC 要求每个验证折同时含好坏两类,先验证这个条件。时间 CV 的最早一段不会获得 OOF 预测,输出保留缺失值是正常现象。
客户/门店分组场景则使用 cv=GroupKFold(...) 并在 fit(..., groups=分组数组) 中传入真实组标识。
time_data = data.assign(放款时间=pd.to_datetime(data["放款时间"], errors="coerce"))
time_data = time_data.dropna(subset=["放款时间"]).sort_values("放款时间", kind="stable")
cut = int(len(time_data) * 0.8)
time_train, time_test = time_data.iloc[:cut], time_data.iloc[cut:]
X_time = time_train[FEATURES].apply(pd.to_numeric, errors="coerce")
y_time = time_train["FPD"].astype(int)
time_cv = TimeSeriesSplit(n_splits=3)
assert all(y_time.iloc[valid].nunique() == 2 for _, valid in time_cv.split(X_time))
time_tuner = make_tuner(cv=time_cv)
time_tuner.fit(X_time, y_time, n_trials=2, show_progress_bar=False)
time_model = time_tuner.get_best_model()
time_probability = time_model.predict_proba(time_test[FEATURES].apply(pd.to_numeric, errors="coerce"))
print(f"时间外测试 AUC:{AUCMetric().evaluate(time_test['FPD'], time_probability):.6f}")
12. 结果复盘、OOF 和复训
best_score_ 是外层 CV 各折指标均值,不是最终模型在训练集或测试集的得分。get_trial_result() 查看逐折记录,get_oof_predictions() 查看折外预测;OOF 只用于训练样本的折外估计。
get_optimization_history() 保留兼容的 Optuna 历史字段;retention="predictions" 保存预测但不保留各折模型,适合本例。最终模型另有缓存。
record = tuner.get_trial_result(tuner.best_trial_.number)
print("最佳试验状态:", record["状态"], ";折数:", len(record["各折"]))
oof = tuner.get_oof_predictions()
print("OOF 列:", list(oof.columns), ";行数:", len(oof))
assert len(oof) == len(X_train)
print(tuner.get_optimization_history().head().to_string(index=False))
assert tuner.get_best_model() is best
refitted = tuner.get_best_model(refit=True)
print(f"显式复训后的测试 AUC:{AUCMetric().evaluate(y_test, refitted.predict_proba(X_test)):.6f}")
13. 保存与恢复:完整对象、SQLite 和推理模型
完整 tuner.save() 包含搜索配置、原始训练数据、指标函数、Study 和当前保留的结果,适合继续同一个任务。SQLite 保存 Optuna 的 trial 状态,不自动保存 Python 模型和预测;需要跨进程读取折结果时同时设置 artifact_dir。
此例在新临时目录写文件,不覆盖现有结果。恢复后传入相同数据和 CV/指标配置再 fit(n_trials=1) 表示追加 1 次 trial。数据、行顺序或评估口径发生变化,应新建调参器与 Study。
output_dir = Path(mkdtemp(prefix="hscredit_tuning_workflow_"))
storage_url = "sqlite:///" + (output_dir / "search.db").as_posix()
persistent_tuner = make_tuner(
metric=brier, storage=storage_url, study_name="FPD概率误差演示",
artifact_dir=str(output_dir / "trials"), retention="disk",
)
persistent_tuner.fit(X_train, y_train, n_trials=2, show_progress_bar=False)
persistent_best = persistent_tuner.get_best_model()
tuner_path = output_dir / "tuner.joblib"
persistent_tuner.save(tuner_path)
restored = ModelTuner.load(tuner_path)
np.testing.assert_allclose(restored.get_best_model().predict_proba(X_test), persistent_best.predict_proba(X_test))
old_count = len(restored.study_.trials)
restored.fit(X_train, y_train, n_trials=1, show_progress_bar=False)
assert len(restored.study_.trials) == old_count + 1
restored.get_best_model().save_inference(output_dir / "最佳预测模型.joblib")
print("验证输出目录:", output_dir)
print("恢复后试验总数:", len(restored.study_.trials))
14. 可视化与扩展入口
完整搜索过程保留在 tuner.get_study()。下列方法生成图对象;在 Jupyter 末行展示或调用 .show() 即可。少量 trial 的参数重要性不稳定,本例只构造历史图并核验对象。
更复杂的训练逻辑可使用 fit_params_factory(trial, fold_index) 每折创建训练回调,或 trial_objective(trial) 完全接管试验。后者需要自己管理切分、训练、评价与记录,不享有自动逐折保存。
figure = tuner.visualization.plot_optimization_history()
assert len(figure.data) > 0
print("已生成搜索历史图,可运行 figure.show() 查看。")
15. 常见误用检查
调参结果“反着选”:裸函数必须声明方向;指标对象通过
greater_is_better声明一次。metric="log_loss"是正的交叉熵,越小越好;旧"logloss"/"neg_log_loss"是负值,越大越好,保留作兼容。loss决定训练,metric决定候选优劣,模型的eval_metric决定训练监控/早停;三者可以不同。ModelTuner(objective=...)是旧评分入口;自定义训练用loss=...,完全接管 trial 才用trial_objective=...。自定义空间的名字必须是模型参数名;Pipeline 用
步骤名__参数;列表空间不是自动穷举。不同 loss 参数的损失数值不可直接比大小;调 loss 参数时使用固定 AUC、固定 Brier 或固定业务指标。
加权训练和加权评价分别传
sample_weight、evaluation_weight;自定义指标必须支持权重。测试集不传入搜索/早停;绑定金额等行级数组不能直接跨折复用。
只保存 SQLite 不能恢复 Python 模型;完整制品、折制品目录、外部数据库各自保留。
16. 常用参数按职责查阅
参数 |
位置 |
说明 |
|---|---|---|
|
|
模型类或可克隆 estimator 实例;Pipeline 用实例 |
|
构造器 / |
字典、声明对象或 |
|
构造器 |
模型基准配置; |
|
构造器 / |
固定模型配置,覆盖同名采样值;避免在两处定义同名参数 |
|
构造器 / |
单目标或列表; |
|
构造器 / |
默认从内置指标或指标对象推断;裸函数必须指定 |
|
构造器 |
需要覆盖指标显示名时提供同长度列表,不得重名 |
|
构造器 / |
整数分层 K 折、splitter 对象或 |
|
|
分组 splitter 需要的行级组标签;不属于模型参数 |
|
|
本次新增 trial 数,不是整个 Study 的累计上限 |
|
|
本次优化的总时间预算;正在运行的模型 fit 不保证即时被打断 |
|
构造器 / |
模型侧并行预算;不代表并行 trial 数 |
|
构造器 |
默认 TPE,可传 |
|
构造器 / 方法 |
优先评估已有经验参数;部分键未提供时其余维度仍采样 |
|
构造器 |
支持早停模型的默认监控轮数;监控只使用外层训练折内部数据 |
|
|
训练样本权重,按训练折切分 |
|
|
显式评估权重,按训练/验证评分对应数据切分 |
|
构造器 |
传给底层模型 fit 的固定参数;支持已识别的样本级参数切分 |
|
构造器 |
每折创建新训练回调;最终重训传 |
|
构造器 |
Optuna 完成回调与剪枝器;区别于模型训练回调 |
|
|
允许某类异常记作 trial 失败后继续;默认让错误暴露 |
|
构造器 |
Optuna 存储和名字,支持 SQLite |
|
构造器 / |
直接使用原生 Study,方向、采样器和存储以该对象为准 |
|
构造器 / |
持久化逐折 Python 制品,不等同于 Optuna storage |
|
构造器 / |
|
默认空间与跨库写法
初学时为 1–3 个关键参数给出小范围、固定其他配置。已知 HSCredit 模型可省略空间使用自适应默认值;未知模型或 Pipeline 应显式给空间。
内部统一使用 Optuna 搜索。接收 sklearn 列表、skopt、Hyperopt、Bayesian Optimization 等声明,表示兼容空间写法,不表示切换到那些库的搜索算法。sampler 才决定 Optuna 如何采样。
# 等价表达:整数区间包含两端,列表表示离散候选。
spaces = {
"简写": {"max_depth": (3, 5), "learning_rate": [0.03, 0.05, 0.1]},
"声明对象": {"max_depth": Integer(3, 5), "learning_rate": Categorical([0.03, 0.05, 0.1])},
"字典": {"max_depth": {"type": "int", "low": 3, "high": 5},
"learning_rate": {"type": "categorical", "choices": [0.03, 0.05, 0.1]}},
}
# 确实需要完整网格时,显式入队所有点,并给足 trial 数。
grid_tuner = make_tuner(search_space=spaces["简写"])
grid_tuner.enqueue_trials(param_grid={"max_depth": [3, 4, 5], "learning_rate": [0.03, 0.05, 0.1]})
grid_tuner.fit(X_train, y_train, n_trials=9, show_progress_bar=False)
分组验证
以下 商品类别 仅用于演示按类别隔离,不是通用的客户防泄漏分组。实际客户有多条记录时应传真实客户标识;至少有 n_splits 个独立组,并检查每个验证折的标签。
from sklearn.model_selection import GroupKFold
groups = data.loc[X_train.index, "商品类别"].fillna("未标注类别").astype(str).to_numpy()
group_cv = GroupKFold(n_splits=3)
if len(np.unique(groups)) < 3:
raise ValueError("可用分组不足3个,请减少折数或选择合适的真实分组标识")
group_tuner = make_tuner(cv=group_cv, metric="log_loss")
group_tuner.fit(X_train, y_train, groups=groups, n_trials=2, show_progress_bar=False)
每折独立生成原生回调
import lightgbm as lgb
def fit_options(trial, fold_index):
if trial is None: # 最终全数据重训,没有内部早停监控集
return {}
return {"callbacks": [lgb.log_evaluation(period=0)]}
callback_tuner = make_tuner(fit_params_factory=fit_options)
callback_tuner.fit(X_train, y_train, n_trials=2, show_progress_bar=False)
固定外部 eval_set 会按模型原生语义传递;调用方必须确保它不包含外层验证或最终测试样本。普通使用无需手动提供,包装模型会在外层训练折内部切出监控集。最终重训默认结合各折最佳轮数,在全部输入训练数据上拟合;get_best_model(refit=True, full_data=False) 显式保留内部验证流程。
内置指标与优化方向
指标 |
方向 |
数值含义 |
|---|---|---|
|
最大化 |
风险方向 AUC;反向预测不会自动翻转为高 AUC |
|
最大化 |
好坏风险分布区分度 |
|
最小化 |
训练与验证 KS 差异;建议与区分度搭配,单独低差异不保证有效 |
|
最小化 |
正的二元交叉熵;新代码优先用此名称 |
|
最大化 |
负的二元交叉熵;保留旧比较方向 |
|
对象声明 |
原始数值;不会自动取负 |
|
最小化 |
固定损失参数的同口径代理损失 |
裸函数 |
必须指定 |
返回一个有限标量;不猜测业务方向 |
仅 sklearn 的 metric.to_scorer() 将最小化指标取负,不能把负 scorer 直接当作 ModelTuner 的概率评估函数。ModelTuner 评分函数签名是 (y_true, y_prob),sklearn scorer 签名是 (estimator, X, y)。
17. 留存、恢复与适用边界
retention |
保存内容 |
适合 |
|---|---|---|
|
各折模型、预测、完整训练记录 |
小规模详细复盘,默认 |
|
预测和指标,不留各折模型 |
OOF 分析,本教程默认 |
|
指标、曲线、样本数、错误摘要 |
较大搜索;不提供 OOF |
|
当前最佳试验完整结果,其他仅摘要 |
控制内存并重点复盘最佳候选 |
|
内存留摘要,完整折结果写入 artifact_dir |
大模型,需要保留磁盘目录 |
get_trial_result(number, load=False) 读取摘要;disk 默认 load=True 按需读取完整折结果。get_oof_predictions() 会合并重复验证预测,未验证的位置保留缺失值。evaluate_trials / evaluate_study_trials 会重新训练评估,不是只读查询;换数据时传入新的 X/y/groups/evaluation_weight,不会复用旧 CV 行位置。
release_training_data() 释放输入与行级训练参数后,缓存模型仍可预测;最终重训和完整 OOF 需要重新提供训练数据。save_inference() 保存不带整个搜索历史的预测模型。完整 save() 适合复盘、继续同一个任务,并会保留训练数据。
自动续跑会验证数据、CV 和评分配置的一致性,避免把不同实验的分数混进同一 Study。已存在的 Study 必须与当前指标方向一致;没有可确认上下文的外部历史,不要当作当前实验的可续跑记录。数据、顺序、标签、权重、CV 或指标参数更改时,新建 Study 最清晰。
Optuna 4.9 已弃用 terminator 模块,普通调参不会再自动调用它来记录专用 CV 数据;
各折指标、中间值、中文指标名称和常规可视化仍然保留。只有需要旧版终止改进诊断时,
才显式使用 record_terminator_scores=True,并接受上游对该功能的弃用提示。
指标名称仍通过公开的 set_metric_names 接口设置;包装器只在这一调用内接管其自身的实验性提示,
不会屏蔽用户函数、训练异常或其它库的警告。
18. 本次整理后的使用约定
指标方向从内置名称或对象声明推断;普通函数须明确方向,输入或指标错误不再用零分代表成功。
自定义评估使用
make_metric,需要完整类型封装时使用BaseMetric;自定义训练使用BaseLoss。loss=明确区分训练目标与历史objective=评分入口;收到混淆用法会说明正确参数。外层 CV 验证数据用于搜索评分,内部监控只来自训练折;训练权重与评价权重独立指定。
保留原有空间声明和历史
logloss负值语义;推荐新代码统一Integer/Real/Categorical与log_loss。Notebook 使用少量 trial 验证接口与产物,不声称此参数组合已达到最佳业务效果。