
1、算法原理1GBDT1概述GBDTGradient Boosting Decision Tree梯度提升决策树是Boosting集成学习算法。通俗描述串行训练多棵若决策树每一棵拟合前面所有树的负梯度残差近似累加所有树输出得到最终预测。2完整原理GBDT 加法模型 前向分步 负梯度拟合 构造回归树【加法模型】【前向分布】一次只学一个前面所有全部固定不变。【负梯度拟合】当第m步时目标是对于每一个样本i损失函数为一元函数泰勒一阶展开公式上面的损失函数转为第一项是常数所以优化损失函数就是最小化第二项。目标变为我们希望f和g尽量符号相反这样变小等价于让定位残差对于MSE损失、为了限制过大不符合泰勒展开所以假如学习率【构造回归树】第m轮得到的残差数据集分两步1. 树的分裂生长选择特征作为分裂点划分叶子区域分裂准则为平方误差最小2.叶子节点赋值求导过程2LightGBM1原理LightGBM在GBDT梯度提升数学框架之上通过Leaf-wise 直方图做工程加速。【Leaf-wise】GBDTLevel‑wise按层生长一层层全部节点都分裂无效计算多。LightGBMLeaf‑wise按叶子生长每次只分裂增益最大的叶子收敛快但更容易过拟合靠num_leaves约束。【直方图】GBDT预排序遍历全部特征值找分裂点速度慢、内存大。LightGBM把连续特征离散成 bin 桶只在桶边界分裂直方图做差加速速度大幅提升微小精度损失2多特征的支持对当前待分裂叶子节点遍历全部特征连续/类别每个特征构建直方图计算该特征所有候选分裂点的分裂增益选出全局最大增益的【特征分裂点】用这个分裂把叶子切成左右两个叶子。3LR逻辑回归在线性回归的输出外面套一个sigmoid函数把实数映射到[0,1]表示正类概率。1模型形式2损失函数二元交叉熵推导过程3训练流程2、实操代码1训练代码import pandas as pd import json import lightgbm as lgb import numpy as np from sklearn.linear_model import LogisticRegression from scipy.sparse import lil_matrix # 1.配置参数 CONTINUOUS_FEATURES [fI{i} for i in range(1, 14)] CATEGORY_FEATURES [fC{i} for i in range(1, 27)] # 博文给出clip阈值95分位数截断 continous_clip [20, 600, 100, 50, 64000, 500, 100, 50, 500, 10, 10, 10, 50] cutoff 200 # 类别特征出现次数200才保留否则unk train_path train_sub100w.txt # 2.读取原始数据 names [label] CONTINUOUS_FEATURES CATEGORY_FEATURES df_raw pd.read_csv(train_path, sep\t, namesnames) # 划分前80w训练后20w验证时序切分不shuffle df_train df_raw.iloc[:800000].copy() df_val df_raw.iloc[800000:].copy() # 3.处理连续特征 I1‑I13clip min‑max归一化 class ContinuousFeatureGenerator: def __init__(self, clip_list): self.clip_list clip_list self.minv None self.maxv None def fit(self, df): arr df[CONTINUOUS_FEATURES].fillna(0).to_numpy(dtypenp.float32) # clip截断 for i in range(len(CONTINUOUS_FEATURES)): arr[:, i] np.clip(arr[:, i], 0, self.clip_list[i]) self.minv np.min(arr, axis0) self.maxv np.max(arr, axis0) def transform(self, df): arr df[CONTINUOUS_FEATURES].fillna(0).to_numpy(dtypenp.float32) for i in range(len(CONTINUOUS_FEATURES)): arr[:, i] np.clip(arr[:, i], 0, self.clip_list[i]) # min‑max归一化 [0,1] denom self.maxv - self.minv denom[denom 1e-8] 1.0 norm (arr - self.minv) / denom return norm cont_gen ContinuousFeatureGenerator(continous_clip) cont_gen.fit(df_train) X_train_cont cont_gen.transform(df_train) X_val_cont cont_gen.transform(df_val) # 4.处理类别特征 C1‑C26每个field内部独立编码cutoff过滤低频 class CategoryDictGenerator: def __init__(self): self.field_vocab dict() # key:col_name, value: {str:id} def fit(self, df): 只在训练集fit统计频次cutoff过滤 for col in CATEGORY_FEATURES: cnt_series df[col].fillna(unk).value_counts() valid_items cnt_series[cnt_series cutoff].index.tolist() vocab {} vocab[unk] 0 for idx, item in enumerate(valid_items): vocab[item] idx 1 self.field_vocab[col] vocab def transform(self, df): out np.zeros((len(df), len(CATEGORY_FEATURES)), dtypenp.int32) for fi, col in enumerate(CATEGORY_FEATURES): vocab self.field_vocab[col] vals df[col].fillna(unk).tolist() for ri, v in enumerate(vals): out[ri, fi] vocab.get(v, vocab[unk]) return out cate_gen CategoryDictGenerator() cate_gen.fit(df_train) X_train_cate cate_gen.transform(df_train) X_val_cate cate_gen.transform(df_val) # 保存映射元数据Java推理要用 with open(cate_field_vocab.json, w, encodingutf‑8) as f: json.dump(cate_gen.field_vocab, f, ensure_asciiFalse) y_train df_train[label].to_numpy() y_val df_val[label].to_numpy() # GBDT完整输入归一化连续特征 field内编码类别特征 X_train_gbdt np.hstack([X_train_cont, X_train_cate]) X_val_gbdt np.hstack([X_val_cont, X_val_cate]) # 5.训练LightGBM只用于产出叶子不直接用预测分数 lgb_train lgb.Dataset(X_train_gbdt, labely_train) lgb_valid lgb.Dataset(X_val_gbdt, labely_val, referencelgb_train) params { objective: binary, metric: binary_logloss,auc, num_leaves: 32, learning_rate: 0.05, verbose: 1 } def print_metrics_callback(env): iter_num env.iteration eval_results env.evaluation_result_list out fIter {iter_num:2d} # 元组(数据集名,指标名,数值,是否越大越好) for ds_name, metric, val, _ in eval_results: out f{ds_name}_{metric}:{val:.6f} print(out) gbm lgb.train( params, lgb_train, num_boost_round20, valid_sets[lgb_valid], callbacks[print_metrics_callback] ) gbm.save_model(gbdt_model.txt) # 获取每棵树叶子id shape:[样本数,树数量] train_leaf gbm.predict(X_train_gbdt, pred_leafTrue) val_leaf gbm.predict(X_val_gbdt, pred_leafTrue) num_trees gbm.num_trees() # 6.构造叶子→LR全局下标映射非常关键 # 每棵树局部叶子id映射为LR稀疏矩阵全局index leaf_mapping {} cur_idx 0 for tree_no in range(num_trees): max_leaf int(np.max(train_leaf[:, tree_no])) for local_leaf in range(0, max_leaf 1): leaf_mapping[f{tree_no}_{local_leaf}] cur_idx cur_idx 1 leaf_feature_size cur_idx # 类别特征继续追加到后面 FIELD_MAX_SIZE 1000 cate_feature_offset leaf_feature_size # 加上26个字段预留空间 total_feature_size leaf_feature_size len(CATEGORY_FEATURES) * FIELD_MAX_SIZE meta_leaf { num_trees: num_trees, leaf_mapping: leaf_mapping, cate_feature_offset: cate_feature_offset, field_max_size: FIELD_MAX_SIZE } with open(leaf_mapping.json, w) as f: json.dump(meta_leaf, f) # 7.构造LR稀疏输入叶子one‑hot 原始类别特征 def build_sparse_matrix(leaf_arr, cate_arr): n_sample leaf_arr.shape[0] mat lil_matrix((n_sample, total_feature_size), dtypenp.float32) for s in range(n_sample): # GBDT叶子特征 for t in range(num_trees): local_id int(leaf_arr[s, t]) key f{t}_{local_id} gid leaf_mapping[key] mat[s, gid] 1.0 # 原始C类别特征偏移之后写入 for fi in range(len(CATEGORY_FEATURES)): cid int(cate_arr[s, fi]) mat[s, cate_feature_offset fi * 1000 cid] 1.0 return mat.tocsr() X_lr_train build_sparse_matrix(train_leaf, X_train_cate) X_lr_val build_sparse_matrix(val_leaf, X_val_cate) # 训练LR lr LogisticRegression(penaltyl2, solversaga, max_iter600, verbose2) lr.fit(X_lr_train, y_train) # 验证集评估 from sklearn.metrics import roc_auc_score y_pred_val lr.predict_proba(X_lr_val)[:, 1] print(Val AUC , roc_auc_score(y_val, y_pred_val)) # 在训练脚本最后添加 cont_meta { minv: cont_gen.minv.tolist(), maxv: cont_gen.maxv.tolist(), clip: continous_clip } with open(cont_norm_meta.json, w, encodingutf-8) as f: json.dump(cont_meta, f) # 导出LR权重、bias lr_meta { lr_weights: lr.coef_[0].tolist(), lr_bias: float(lr.intercept_[0]), total_feature_size: total_feature_size } with open(lr_meta.json, w) as f: json.dump(lr_meta, f) print(全部导出物料) print(gbdt_model.txt) print(cate_field_vocab.json) print(leaf_mapping.json) print(cont_norm_meta.json) print(lr_meta.json)2部署服务的代码import json import numpy as np import lightgbm as lgb from fastapi import FastAPI from pydantic import BaseModel import uvicorn import os # 配置 CONTINUOUS_FEATURES [fI{i} for i in range(1, 14)] CATEGORY_FEATURES [fC{i} for i in range(1, 27)] continous_clip [20, 600, 100, 50, 64000, 500, 100, 50, 500, 10, 10, 10, 50] FIELD_MAX_SIZE 1000 GBDT_MODEL_PATH ./model/gbdt_model.txt LEAF_MAPPING_PATH ./model/leaf_mapping.json LR_META_PATH ./model/lr_meta.json CATE_VOCAB_PATH ./model/cate_field_vocab.json # 全局加载资源 # 1.GBDT gbdt_booster lgb.Booster(model_fileGBDT_MODEL_PATH) # 2.叶子映射 with open(LEAF_MAPPING_PATH, r, encodingutf-8) as f: leaf_meta json.load(f) leaf_mapping leaf_meta[leaf_mapping] num_trees leaf_meta[num_trees] cate_feature_offset leaf_meta[cate_feature_offset] # 3.LR权重 with open(LR_META_PATH, r, encodingutf-8) as f: lr_meta json.load(f) lr_weight np.array(lr_meta[lr_weights], dtypenp.float64) lr_bias float(lr_meta[lr_bias]) total_feature_size lr_meta[total_feature_size] # 4.类别词典 with open(CATE_VOCAB_PATH, r, encodingutf-8) as f: cate_field_vocab json.load(f) # 5.预存训练集统计min/max with open(./model/cont_norm_meta.json, r, encodingutf-8) as f: cont_norm_meta json.load(f) cont_min np.array(cont_norm_meta[minv]) cont_max np.array(cont_norm_meta[maxv]) app FastAPI(titleGBDTLR 推理服务) # 请求模型 # 单条原始特征 class RawFeatureRequest(BaseModel): I1: float | None None I2: float | None None I3: float | None None I4: float | None None I5: float | None None I6: float | None None I7: float | None None I8: float | None None I9: float | None None I10: float | None None I11: float | None None I12: float | None None I13: float | None None C1: str | None None C2: str | None None C3: str | None None C4: str | None None C5: str | None None C6: str | None None C7: str | None None C8: str | None None C9: str | None None C10: str | None None C11: str | None None C12: str | None None C13: str | None None C14: str | None None C15: str | None None C16: str | None None C17: str | None None C18: str | None None C19: str | None None C20: str | None None C21: str | None None C22: str | None None C23: str | None None C24: str | None None C25: str | None None C26: str | None None # 批量请求结构 class BatchRawFeatureRequest(BaseModel): samples: list[RawFeatureRequest] # 特征预处理函数单条 def preprocess_raw_features(req: RawFeatureRequest): # 1.连续特征处理 clip minmax cont_vals [] for idx, col in enumerate(CONTINUOUS_FEATURES): v getattr(req, col, 0.0) or 0.0 v np.clip(v, 0, continous_clip[idx]) cont_vals.append(v) cont_arr np.array(cont_vals, dtypenp.float64) denom cont_max - cont_min denom[denom 1e-8] 1.0 cont_norm (cont_arr - cont_min) / denom # 2.类别特征编码 cate_ids [] for col in CATEGORY_FEATURES: raw_val getattr(req, col) val str(raw_val) if raw_val is not None else unk vocab cate_field_vocab[col] cid vocab.get(val, vocab[unk]) cate_ids.append(cid) gbdt_row np.hstack([cont_norm, np.array(cate_ids, dtypenp.float64)]) return gbdt_row, cate_ids # 批量推理核心 def batch_predict(sample_list: list[RawFeatureRequest]): batch_gbdt_rows [] batch_cate_ids [] for s in sample_list: row, cids preprocess_raw_features(s) batch_gbdt_rows.append(row) batch_cate_ids.append(cids) batch_arr np.array(batch_gbdt_rows, dtypenp.float64) # LightGBM批量预测叶子 leaf_batch gbdt_booster.predict(batch_arr, pred_leafTrue) result_list [] for idx in range(len(sample_list)): leaf_indexes leaf_batch[idx] cate_ids batch_cate_ids[idx] lr_input np.zeros(total_feature_size, dtypenp.float64) # 填充GBDT叶子onehot for tree_idx in range(num_trees): local_leaf int(leaf_indexes[tree_idx]) key f{tree_idx}_{local_leaf} if key in leaf_mapping: pos leaf_mapping[key] lr_input[pos] 1.0 # 填充原始类别onehot for fi, cid in enumerate(cate_ids): pos cate_feature_offset fi * FIELD_MAX_SIZE cid lr_input[pos] 1.0 logit float(np.dot(lr_input, lr_weight) lr_bias) prob 1.0 / (1.0 np.exp(-logit)) result_list.append({logit: logit, prob: prob}) return result_list # 接口 app.post(/predict) def inference(req: RawFeatureRequest): gbdt_input, cate_ids preprocess_raw_features(req) res batch_predict([req])[0] return { code: 0, msg: success, logit: res[logit], prob: res[prob] } app.post(/batch_predict) def batch_inference(req: BatchRawFeatureRequest): try: res_list batch_predict(req.samples) return { code: 0, msg: success, result: res_list } except Exception as e: return { code: -1, msg: str(e), result: [] } app.get(/health) def health(): return {status: ok} if __name__ __main__: uvicorn.run( main:app, host0.0.0.0, port8000, workers1, log_levelinfo )3代码总结1核心目的GBDT负责特征交叉LR负责线性打分2GBDTLR的逻辑链路原始特征 → LightGBM 树分裂 → 样本落到叶子 →pred_leaf拿到叶子 ID → leaf_mapping 映射全局 id → one‑hot 稀疏特征 → LogisticRegression 训练。3为什么GBDT叶子ID可以当作特征样本落到哪个叶子就代表满足该叶子路径上一整套特征分裂条件把叶子ID转one-hot就把GBDT学到的非线性交叉特征转成线性模型可以用到的离散特征。4推理计算逻辑LightGBM 支持批量输出样本叶子 IDLR 不调用 sklearn 库遍历每个样本手动构造 0‑1 特征数组直接通过公式logitWx b 做向量点积计算再 sigmoid 得到概率。5推理依赖训练导出物料服务启动时全局加载训练阶段输出的全部静态文件GBDT 模型、叶子映射表、LR 权重偏置、类别词表、连续特征归一化统计 (min/max、clip 阈值)保证推理预处理逻辑与训练完全一致3、服务请求示例1测试响应时间一批300个请求时间curl.exe -o $null -s -w 总耗时%{time_total}sn -X POST http://127.0.0.1:8000/batch_predict -H Content-Type: application/json -d {samples:[ ((, {I1:10.0,I2:20.0,I3:5.0,I4:1.0,I5:1000.0,I6:100.0,I7:10.0,I8:5.0,I9:100.0,I10:2.0,I11:3.0,I12:1.0,I13:10.0,C1:abc,C2:def,C3:xyz,C4:null,C5:aaa,C6:bbb,C7:ccc,C8:ddd,C9:eee,C10:fff,C11:ggg,C12:hhh,C13:iii,C14:jjj,C15:kkk,C16:lll,C17:mmm,C18:nnn,C19:ooo,C20:ppp,C21:qqq,C22:rrr,C23:sss,C24:ttt,C25:uuu,C26:vvv},{I1:5.0,I2:10.0,I3:2.0,I4:3.0,I5:500.0,I6:50.0,I7:5.0,I8:2.0,I9:50.0,I10:1.0,I11:1.0,I12:2.0,I13:5.0,C1:a1,C2:b1,C3:c1,C4:d1,C5:e1,C6:f1,C7:g1,C8:h1,C9:i1,C10:j1,C11:k1,C12:l1,C13:m1,C14:n1,C15:o1,C16:p1,C17:q1,C18:r1,C19:s1,C20:t1,C21:u1,C22:v1,C23:w1,C24:x1,C25:y1,C26:z1})*150].TrimStart(,) ]}请求时间16ms左右2测试返回值import time import requests URL http://127.0.0.1:8000/batch_predict # 两套样本模板 sample_a { I1: 10.0, I2: 20.0, I3: 5.0, I4: 1.0, I5: 1000.0, I6: 100.0, I7: 10.0, I8: 5.0, I9: 100.0, I10: 2.0, I11: 3.0, I12: 1.0, I13: 10.0, C1: abc, C2: def, C3: xyz, C4: None, C5: aaa, C6: bbb, C7: ccc, C8: ddd, C9: eee, C10: fff, C11: ggg, C12: hhh, C13: iii, C14: jjj, C15: kkk, C16: lll, C17: mmm, C18: nnn, C19: ooo, C20: ppp, C21: qqq, C22: rrr, C23: sss, C24: ttt, C25: uuu, C26: vvv } sample_b { I1: 5.0, I2: 10.0, I3: 2.0, I4: 3.0, I5: 500.0, I6: 50.0, I7: 5.0, I8: 2.0, I9: 50.0, I10: 1.0, I11: 1.0, I12: 2.0, I13: 5.0, C1: a1, C2: b1, C3: c1, C4: d1, C5: e1, C6: f1, C7: g1, C8: h1, C9: i1, C10: j1, C11: k1, C12: l1, C13: m1, C14: n1, C15: o1, C16: p1, C17: q1, C18: r1, C19: s1, C20: t1, C21: u1, C22: v1, C23: w1, C24: x1, C25: y1, C26: z1 } # 构造300条A B 循环150次 samples [] for _ in range(150): samples.append(sample_a) samples.append(sample_b) payload {samples: samples} headers {Content-Type: application/json} if __name__ __main__: start time.perf_counter() resp requests.post(URL, jsonpayload, headersheaders) cost time.perf_counter() - start print(f请求耗时: {cost:.4f} 秒) print(fHTTP状态码: {resp.status_code}) if resp.ok: data resp.json() print(f返回样本数量: {len(data[result])}) # 打印前5条结果预览避免刷屏 print(前5条预测结果) for idx, item in enumerate(data[result][:5]): print(f[{idx}] prob{item[prob]:.6f}, logit{item[logit]:.4f}) else: print(响应异常) print(resp.text)返回结果88ms。请求耗时: 0.0886 秒 HTTP状态码: 200 返回样本数量: 300 前5条预测结果 [0] prob0.088884, logit-2.3273 [1] prob0.094043, logit-2.2652 [2] prob0.088884, logit-2.3273 [3] prob0.094043, logit-2.2652 [4] prob0.088884, logit-2.3273如果是200条就是62ms。我在linux服务器上试了下性能和本机的差不多。4、CTR指标1AUCArea Under ROC CurveROC曲线下的面积面积的计算方法把每一个样本预测分数都当做候选阈值得到一系列 FPR、TPR 点将点按横轴排序用梯形法累加得到曲线下面积。含义随机抽一个正样本、一个负样本模型给正样本打高于负样本负数的概率特点- 不受分类阈值影响只看样本之间的相对排序CTR 预估关心排序- 对样本不平衡不敏感正负样本差距很大广告点击点击很少绝大部分不点击AUC 依然可用2LogLoss含义衡量预估概率 p 和真实标签 y 之间的差距评估概率值准不准3Accuracy准确率含义全局预测正确占比。几乎不用不平衡下有欺骗性。4Precision精确率判为正的里面真实正的比例。多用于召回一般是Top-K的召回。5Recall召回率把多少真正样本找出来。多用于召回一般是Top-K的召回。