参数与特征基础:参数类型、特征分类、优化问题定义

聊参数和特征之前,我想先说说这两者的关系。

你可能觉得参数就是超参数,特征就是数据列。嗯,大体没错。但真正做协同优化时,如果不把它们放在一起看,很容易掉坑里。我早期做过一个推荐系统项目,单独调参数能到AUC 0.78,加上特征筛选反而掉到0.72——后来发现是参数和特征在相互“打架”。

所以这一章,我们先打好地基。把参数是什么、特征是什么、优化问题怎么定义,一件一件理清楚。

参数类型:不只是“调个数字”

先问个问题:你手头要调的东西,到底属于哪一类参数?

我个人习惯把参数分成三种:

  • 连续参数:取值范围是实数区间。比如学习率0.001~0.1,Dropout概率0.2~0.8。
  • 离散参数:取值来自有限集合。比如树的最大深度{3,5,7,9},优化器选择{Adam,SGD,RMSprop}。
  • 条件参数:只在其他参数取特定值时才生效。比如用了Adam优化器,才有β1、β2参数;用RAdam就多一个稳定期参数。

关键点:条件参数经常被忽略,导致搜索空间里产生大量无效组合。我建议在定义参数空间时,显式写出依赖关系。

举个例子,一个简单的XGBoost参数配置,JSON形式是这样的:

{
  "n_estimators": {"type": "int", "range": [50, 500]},
  "max_depth": {"type": "int", "range": [3, 15]},
  "learning_rate": {"type": "float", "range": [0.01, 0.3]},
  "subsample": {"type": "float", "range": [0.5, 1.0]},
  "grow_policy": {"type": "categorical", "values": ["depthwise", "lossguide"]},
  "max_leaves": {
    "type": "int",
    "range": [8, 256],
    "condition": {"param": "grow_policy", "value": "lossguide"}
  }
}

看到没?max_leaves 只有在 grow_policy 为 lossguide 时才需要。如果不加条件,搜索算法可能在 depthwise 模式下也调 max_leaves,白白浪费计算。

一个小技巧:用配置管理器(比如OmegaConf、Hydra)定义参数层级,可以自动处理条件逻辑。我曾经一个项目用纯字典定义,后来条件嵌套多了维护成本暴涨,换成Hydra后清爽很多。

特征分类:模型吃的“食材”

特征决定了模型的上限,参数只是逼近这个上限。我见过太多人花80%时间调参,却只在原始特征上折腾,效果自然有限。

从工程角度看,特征大概分这几类:

类型例子常用处理
数值特征年龄、收入、温度标准化、对数变换、分箱
类别特征城市、部门、颜色One-hot、Label Encoding、Target Encoding
时序特征时间戳、星期、季节性滞后特征、滚动统计、差分
文本特征评论、描述、日志TF-IDF、Word2Vec、大模型Embedding
交互特征年龄×收入、城市×类别多项式组合、笛卡尔积(谨慎使用)

你想想看,如果特征数量多但噪声大,参数再优也没用。反过来,特征质量高,简单模型+默认参数都可能碾压复杂模型。

避坑指南:我曾经在风控项目里做特征工程,贪心加了200多个交互特征,导致模型方差飞升,上线后bad rate反而恶化。后来用特征重要性筛选降到30个,再配合正则化参数,效果才稳住。特征不是越多越好。

优化问题定义:把业务放到数学框里

参数和特征都准备好了,接下来要回答:我们到底要优化什么?

一个完整的优化问题定义,包含三要素:

  1. 决策变量——参数和特征选择(哪些特征要保留,哪些参数要调)
  2. 目标函数——比如AUC、MSE、业务ROI
  3. 约束条件——训练时间不超过1小时,模型大小小于100MB,推理延迟低于50ms

协同优化的难点在于,参数和特征会互相影响。举个例子,增加一个高基数类别特征后,树的深度参数可能要从6调到10才能充分利用,但训练时间却翻了一倍。

用数学语言表达,大概是这样的:

Minimize    f(θ, F) = -AUC(模型(θ, F))
Subject to  g1(θ, F) = 训练时间(θ, F) ≤ T_max
            g2(θ, F) = 模型大小(θ, F) ≤ S_max
            θ ∈ Θ,  F ⊆ 原始特征集

这里 θ 是参数空间,F 是特征子集选择。注意F是离散的,θ里有连续有离散——这就成了混合整数非线性优化问题。嗯,并不简单。

我的经验:在定义目标函数时,尽量用单一指标。如果必须多目标(比如AUC+推理延迟),可以加权求和或设置帕累托前沿。我习惯在探索阶段先跑一个纯性能目标,选定候选后再加上约束。

知识结构图:参数与特征协同优化的全局脉络

下面这张图把这章的核心关系画出来了。你看一眼就能知道每个部分的位置。

参数与特征协同优化基础 参数类型 特征分类 优化问题定义 连续参数 离散参数 条件参数 数值特征 类别特征 时序特征 文本/交互 决策变量(θ, F) 目标 + 约束 核心关系 参数调优与特征选择相互依赖,需在同一个优化框架内协同搜索

用代码感受一下

光说不练假把式。下面是一个用Optuna定义协同优化空间的Python片段,把参数和特征选择放在一起:

import optuna

def objective(trial):
    # 参数部分
    lr = trial.suggest_float("lr", 1e-4, 1e-1, log=True)
    n_layers = trial.suggest_int("n_layers", 1, 5)
    optimizer_name = trial.suggest_categorical("optimizer", ["Adam", "SGD"])

    # 条件参数
    if optimizer_name == "Adam":
        beta1 = trial.suggest_float("beta1", 0.8, 0.999)
    else:
        beta1 = None  # 用默认值

    # 特征选择部分(假设原有50个特征)
    feature_mask = []
    for i in range(50):
        use = trial.suggest_categorical(f"feat_{i}", [True, False])
        feature_mask.append(use)

    # 训练模型并返回指标
    model = train_model(lr, n_layers, optimizer_name, beta1, feature_mask)
    return model.validation_auc()

注意这里特征是否入选也成了优化变量。但50个特征意味着50个二值变量,搜索空间瞬间变成2^50。所以实际项目中我会先用特征重要性或者Filter方法预筛,只留20~30个候选,再和参数一起搜。

实用建议:如果想快速跑第一轮,可以用随机搜索或TPE采样。我习惯先跑50组随机,看参数和特征的大致影响方向,再切到贝叶斯优化。

这一章的核心你已经抓住:参数类型决定了搜索空间的设计,特征分类影响模型的性能天花板,而优化问题定义把这些串成一个可计算的数学问题。后面的章节,我们会一步步在这个基础上叠加技巧。