前言:
最近在工作中遇到一个结构化数据回归预测的问题,用到了很多回归算法(如多元线性回归等)都没有很好的效果,于是使用了XGBoost,自己也冲三个特征参数人为的增加来几个,训练出来的效果还是很不错的,XGBoost还是比较擅长对结构化数据的预测分析。希望对你有帮助
一、XGBoost 的基本介绍
XGBoost(eXtreme Gradient Boosting)是一种基于梯度提升树(GBDT)的高效机器学习算法,由陈天奇等人提出。它在 GBDT 的基础上进行了诸多优化,具有速度快、性能好、泛化能力强等特点,在各类机器学习竞赛和实际工业应用中都有着广泛的应用。
二、XGBoost 的核心原理
(一)梯度提升框架
XGBoost 遵循梯度提升的思想,通过迭代地构建一系列决策树来进行预测。每一棵新的树都是为了纠正前面所有树的预测误差而生成的。具体来说,假设已经有了 t-1 棵树,对于每一个样本,这些树的预测结果之和为(\hat{y}^{(t-1)}_i),那么第 t 棵树的目标就是使得预测误差尽可能减小。
(二)正则化
XGBoost 引入了正则化项来防止过拟合。正则化项包括树的叶子节点数量和叶子节点权重的 L2 范数。其目标函数可以表示为:
(Obj(\theta) = \sum{i=1}^n l(y_i, \hat{y}i) + \sum_{k=1}^K \Omega(f_k))
其中,(l(y_i, \hat{y}i))是损失函数,用于衡量预测值(\hat{y}i)与真实值(y_i)之间的差异;(\Omega(f_k))是正则化项,(f_k)表示第 k 棵树,(K)是树的数量。
(三)树的构建
XGBoost 在构建树的过程中,采用了贪心算法。对于每个特征,它会尝试不同的分割点,计算分割后的增益,选择增益最大的分割点进行分裂。为了提高计算效率,XGBoost 还采用了直方图优化等技术,将连续特征的取值离散化为若干个直方图 bins,从而减少分割点的搜索时间。
三、XGBoost 的核心特性
(一)高效性
-
支持并行计算:在树的构建过程中,特征的分裂可以并行处理,大大提高了算法的运行速度。
-
直方图优化:如前面所述,通过将连续特征离散化,减少了分割点的搜索时间,提高了计算效率。
(二)灵活性
-
支持多种损失函数:可以根据不同的任务(如分类、回归)选择合适的损失函数,如平方损失、逻辑损失等。
-
可以处理多种类型的数据:包括数值型数据和类别型数据,对于类别型数据,需要进行适当的编码处理。
(三)鲁棒性
-
对缺失值不敏感:XGBoost 可以自动处理缺失值,在训练过程中会学习缺失值的处理方式。
-
内置正则化:通过正则化项可以有效防止过拟合,提高模型的泛化能力。
四、XGBoost 的使用步骤
(一)数据准备
-
数据收集:获取用于训练和测试的数据集。
-
数据清洗:处理数据中的缺失值、异常值等。对于缺失值,可以采用均值填充、中位数填充、众数填充等方法;对于异常值,可以根据实际情况进行删除或修正。
-
特征工程:对数据进行特征选择、特征转换等操作,以提高模型的性能。例如,可以进行标准化、归一化处理,或者构建新的特征。
(二)模型训练
-
导入 XGBoost 库:在 Python 中,可以使用import xgboost as xgb来导入 XGBoost 库。
-
划分训练集和测试集:可以使用train_test_split函数将数据集划分为训练集和测试集,一般按照 7:3 或 8:2 的比例进行划分。
-
定义参数:设置 XGBoost 的相关参数,如学习率(learning_rate)、树的数量(n_estimators)、最大深度(max_depth)等。
-
训练模型:使用XGBClassifier(分类任务)或XGBRegressor(回归任务)构建模型,并使用fit方法进行训练。
(三)模型评估
-
预测:使用训练好的模型对测试集进行预测,得到预测结果。
-
评估指标:根据不同的任务选择合适的评估指标。对于分类任务,可以使用准确率、精确率、召回率、F1 值、ROC 曲线等;对于回归任务,可以使用均方误差(MSE)、均方根误差(RMSE)、平均绝对误差(MAE)等。
(四)模型调优
-
网格搜索:通过设置不同的参数组合,使用网格搜索来寻找最优的参数。
-
随机搜索:与网格搜索类似,但随机选择参数组合进行搜索,在一定程度上可以提高搜索效率。
五、详细示例
(一)分类任务示例(基于鸢尾花数据集)
-
数据准备
from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split from xgboost import XGBClassifier from sklearn.metrics import accuracy_score, classification_report # 加载数据集 iris = load_iris() X = iris.data y = iris.target # 划分训练集和测试集 X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
-
模型训练
# 定义模型 model = XGBClassifier(learning_rate=0.1,n_estimators=100,max_depth=3,objective='multi:softmax', # 多分类任务num_class=3, # 类别数量random_state=42 ) # 训练模型 model.fit(X_train, y_train)
-
模型评估
# 预测 y_pred = model.predict(X_test) # 计算准确率 accuracy = accuracy_score(y_test, y_pred) print(f"准确率:{accuracy:.2f}") # 详细评估报告 print(classification_report(y_test, y_pred))
运行结果:
准确率:1.00precision recall f1-score support 0 1.00 1.00 1.00 141 1.00 1.00 1.00 152 1.00 1.00 1.00 11 accuracy 1.00 40macro avg 1.00 1.00 1.00 40 weighted avg 1.00 1.00 1.00 40
从结果可以看出,使用 XGBoost 模型在鸢尾花数据集上的分类准确率达到了 100%,效果非常好。
(二)回归任务示例(基于波士顿房价数据集)
-
数据准备
from sklearn.datasets import load_boston from sklearn.model_selection import train_test_split from xgboost import XGBRegressor from sklearn.metrics import mean_squared_error, mean_absolute_error # 注意:sklearn 1.2.0版本后移除了boston数据集,这里使用其他方式获取(如从mlxtend库) # 假设已获取数据 X, y = load_boston(return_X_y=True) # 划分训练集和测试集 X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
-
模型训练
# 定义模型 model = XGBRegressor(learning_rate=0.1,n_estimators=100,max_depth=3,objective='reg:squarederror', # 回归任务,平方误差损失random_state=42 ) # 训练模型 model.fit(X_train, y_train)
-
模型评估
# 预测 y_pred = model.predict(X_test) # 计算评估指标 mse = mean_squared_error(y_test, y_pred) rmse = mse **0.5 mae = mean_absolute_error(y_test, y_pred) print(f"均方误差(MSE):{mse:.2f}") print(f"均方根误差(RMSE):{rmse:.2f}") print(f"平均绝对误差(MAE):{mae:.2f}")
运行结果(示例):
均方误差(MSE):10.23 均方根误差(RMSE):3.20 平均绝对误差(MAE):2.35
从结果可以看出,模型的预测效果较好,各项误差指标都处于较低水平。
六、XGBoost 与其他算法的对比
(一)与 GBDT 的对比
-
相同点:两者都属于梯度提升树算法,都是通过迭代构建决策树来进行预测。
-
不同点
-
正则化:XGBoost 引入了更严格的正则化项,包括树的叶子节点数量和叶子节点权重的 L2 范数,而 GBDT 没有明确的正则化项,容易过拟合。
-
计算效率:XGBoost 支持并行计算和直方图优化,计算速度比 GBDT 快很多。
-
对缺失值的处理:XGBoost 可以自动处理缺失值,而 GBDT 需要手动处理。
(二)与随机森林的对比
-
相同点:两者都由多棵决策树组成,都可以用于分类和回归任务。
-
不同点
-
构建方式:随机森林是通过 bootstrap 抽样构建多棵决策树,然后进行投票或平均得到结果,树之间是独立的;XGBoost 是迭代地构建树,每一棵新的树都依赖于前面的树。
-
偏差与方差:随机森林通过集成多棵树来降低方差,XGBoost 通过梯度提升来降低偏差。
-
性能:在很多任务中,XGBoost 的性能优于随机森林,但随机森林的训练速度可能更快一些。
(三)与支持向量机(SVM)的对比
-
相同点:都可以用于分类和回归任务。
-
不同点
-
处理数据规模:SVM 在处理大规模数据时效率较低,而 XGBoost 可以较好地处理大规模数据。
-
核函数:SVM 需要选择合适的核函数来处理非线性问题,而 XGBoost 通过决策树的组合可以自然地处理非线性问题。
-
解释性:XGBoost 的模型解释性相对较强,可以通过特征重要性等指标了解特征的影响;SVM 的解释性相对较弱。
七、我的训练脚本(仅供参考)
-
max_depth
:树的深度,影响模型复杂度和过拟合风险。 -
learning_rate
(或eta
):学习率,控制每次迭代的步长。 -
n_estimators
:提升树的数量,即训练的轮数。
代码:
import json import numpy as np import pandas as pd import matplotlib.pyplot as plt import seaborn as sns from sklearn.model_selection import train_test_split, cross_val_score, KFold from sklearn.preprocessing import OneHotEncoder, StandardScaler from sklearn.linear_model import Ridge from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_squared_error, r2_score import xgboost as xgb from matplotlib.font_manager import FontProperties import joblib font_path = r"simhei.ttf" font = FontProperties(fname=font_path) with open(r'test.json', 'r', encoding='utf-8') as f:data = json.load(f) #数据处理,将嵌套列表展开为适合建模的表格 rows = [] for item in data:gcbh = item['gcbh']jxtbhj = item['jxtbhj']for debh, sl, dj,dwsl in zip(item['debh'], item['sl'], item['dj'],item['dwsl']):rows.append([gcbh, debh, sl, dj,dwsl, jxtbhj]) df = pd.DataFrame(rows, columns=['gcbh', 'debh', 'sl', 'dj', "dwsl",'jxtbhj']) # 新增特征 df['single_item_total'] = df['sl'] * df['dj'] * df['dwsl'] # 在DataFrame创建后,添加更多特征 # 1. 工程级别的统计特征 df['total_by_gcbh'] = df.groupby('gcbh')['single_item_total'].transform('sum') df['items_count'] = df.groupby('gcbh')['debh'].transform('count') df['avg_price'] = df.groupby('gcbh')['dj'].transform('mean') df['price_std'] = df.groupby('gcbh')['dj'].transform('std') df['total_quantity'] = df.groupby('gcbh')['sl'].transform('sum') # 2. 项目级别的统计特征 df['price_ratio'] = df['dj'] / df['avg_price'] df['quantity_ratio'] = df['sl'] / df['total_quantity'] df['value_ratio'] = df['single_item_total'] / df['total_by_gcbh'] # 修改特征列表 cat_features = ['debh'] num_features = ['sl', 'dj', 'single_item_total', # 基础特征'total_by_gcbh', 'items_count','avg_price','price_std','total_quantity', 'price_ratio','quantity_ratio', 'value_ratio' ] # 3. 处理缺失值 print("处理缺失值前的数据形状:", df.shape) print("缺失值统计:") print(df[num_features].isnull().sum()) # 处理缺失值 - 使用中位数填充数值特征 for col in num_features:if df[col].isnull().sum() > 0:median_val = df[col].median()df[col].fillna(median_val, inplace=True)print(f"列 {col} 使用中位数 {median_val:.2f} 填充了 {df[col].isnull().sum()} 个缺失值") # 处理分类特征的缺失值 for col in cat_features:if df[col].isnull().sum() > 0:mode_val = df[col].mode()[0]df[col].fillna(mode_val, inplace=True)print(f"列 {col} 使用众数 {mode_val} 填充了 {df[col].isnull().sum()} 个缺失值") print("处理缺失值后的数据形状:", df.shape) print("缺失值统计:") print(df[num_features + cat_features].isnull().sum()) # 4. 处理异常值 def handle_outliers(df, columns, n_sigmas=3):for col in columns:mean = df[col].mean()std = df[col].std()df[col] = df[col].clip(mean - n_sigmas * std, mean + n_sigmas * std)return df # 处理数值特征的异常值 df = handle_outliers(df, num_features) # 目标值不做对数变换,因为看起来对数变换效果不好 y = df['jxtbhj'].values # 检查目标值是否有缺失值 if np.isnan(y).any():print("警告:目标值包含缺失值,将删除这些行")mask = ~np.isnan(y)df = df[mask]y = y[mask]print(f"删除缺失值后的数据形状: {df.shape}") # 工程编号独热编码 encoder = OneHotEncoder(sparse_output=False, handle_unknown='ignore') X_cat = encoder.fit_transform(df[cat_features]) X_num = df[num_features].values # 最终检查确保没有NaN值 print("最终数据检查:") print(f"X_cat 包含 NaN: {np.isnan(X_cat).any()}") print(f"X_num 包含 NaN: {np.isnan(X_num).any()}") print(f"y 包含 NaN: {np.isnan(y).any()}") X = np.hstack([X_cat, X_num]) # 特征缩放 scaler = StandardScaler() X_scaled = scaler.fit_transform(X) # 4. 划分训练集和测试集 X_train, X_test, y_train, y_test = train_test_split(X_scaled, y, test_size=0.2, random_state=42) # 在数据处理后,模型训练前添加这段代码 plt.figure(figsize=(12, 4)) # 原始值分布 plt.subplot(121) plt.hist(df['jxtbhj'], bins=50) plt.title('原始 jxtbhj 分布', fontproperties=font) # 对数变换后的分布 plt.subplot(122) plt.hist(np.log1p(df['jxtbhj']), bins=50) plt.title('log(jxtbhj) 分布', fontproperties=font) plt.tight_layout() plt.show() # 打印一些基本统计信息 print("原始 jxtbhj 统计信息:") print(df['jxtbhj'].describe()) # 5. 建立多种模型 models = {# '岭回归': Ridge(# alpha=0.5, # 适中的正则化# fit_intercept=True# ),# '随机森林': RandomForestRegressor(# n_estimators=300, # 增加树的数量# max_depth=8, # 控制过拟合# min_samples_leaf=5,# max_features='sqrt',# random_state=42# ),'XGBoost': xgb.XGBRegressor(n_estimators=1500,#900max_depth=8,learning_rate=0.1, # 降低学习率0.09subsample=0.8, # 随机采样colsample_bytree=0.8,# 特征采样reg_alpha=0.1, # L1正则化reg_lambda=1.0, # L2正则化random_state=42,verbosity=0) } results = {} kf = KFold(n_splits=5, shuffle=True, random_state=42) for name, model in models.items():cv_scores = cross_val_score(model, X_scaled, y, cv=kf, scoring='r2',error_score='raise')print(f"\n模型: {name}")print(f"交叉验证 R² 分数: {cv_scores.mean():.4f} (+/- {cv_scores.std() * 2:.4f})") # 在全部数据上训练和评估model.fit(X_train, y_train)y_pred = model.predict(X_test)mse = mean_squared_error(y_test, y_pred)r2 = r2_score(y_test, y_pred)results[name] = {'model': model, 'mse': mse, 'r2': r2, 'y_pred': y_pred}print(f"测试集 MSE: {mse:.2f}")print(f"测试集 R²: {r2:.4f}") # 在模型训练后添加def analyze_predictions(y_true, y_pred, name):# 计算预测误差百分比error_percent = np.abs((y_pred - y_true) / y_true) * 100 print(f"\n{name} 预测分析:")print(f"平均百分比误差: {error_percent.mean():.2f}%")print(f"中位数百分比误差: {np.median(error_percent):.2f}%")print(f"90%预测的误差在 {np.percentile(error_percent, 90):.2f}% 以内") # 计算不同误差范围的预测比例for threshold in [10, 20, 30, 50]:accuracy = (error_percent <= threshold).mean() * 100print(f"误差在{threshold}%以内的预测比例: {accuracy:.2f}%") analyze_predictions(y_test, y_pred, name) # 6. 选择最佳模型用于解释性分析 best_model_name = max(results, key=lambda k: results[k]['r2']) best_model = results[best_model_name]['model'] y_pred = results[best_model_name]['y_pred'] print(f"最佳模型: {best_model_name}") # 保存XGBoost模型 if best_model_name == 'XGBoost':best_model.save_model('xgb_model.json')print('XGBoost模型已保存到 xgb_model.json')# 保存encoder和scalerjoblib.dump(encoder, 'encoder.joblib')joblib.dump(scaler, 'scaler.joblib')print('encoder和scaler已保存') # 7. 特征重要性分析 feature_names = list(encoder.get_feature_names_out(cat_features)) + num_features if hasattr(best_model, 'coef_'):importances = best_model.coef_ elif hasattr(best_model, 'feature_importances_'):importances = best_model.feature_importances_ else:importances = np.zeros(len(feature_names)) plt.figure(figsize=(12, 6)) sns.barplot(x=feature_names, y=importances) plt.title(f'{best_model_name} 特征重要性', fontproperties=font) plt.xlabel('特征', fontproperties=font) plt.ylabel('重要性', fontproperties=font) plt.xticks(rotation=90) plt.tight_layout() plt.show() # 8. 预测值与真实值对比图 plt.figure(figsize=(8, 8)) plt.scatter(y_test, y_pred, alpha=0.6) plt.plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], 'r--', lw=2) plt.title(f'{best_model_name} 真实值 vs 预测值', fontproperties=font) plt.xlabel('真实值', fontproperties=font) plt.ylabel('预测值', fontproperties=font) plt.tight_layout() plt.show() # 9. 残差分析 residuals = y_test - y_pred plt.figure(figsize=(8, 5)) sns.histplot(residuals, bins=30, kde=True) plt.title(f'{best_model_name} 残差分布', fontproperties=font) plt.xlabel('残差', fontproperties=font) plt.tight_layout() plt.show() plt.figure(figsize=(8, 5)) plt.scatter(y_pred, residuals, alpha=0.6) plt.axhline(0, color='red', linestyle='--') plt.title(f'{best_model_name} 预测值与残差', fontproperties=font) plt.xlabel('预测值', fontproperties=font) plt.ylabel('残差', fontproperties=font) plt.tight_layout() plt.show()
八、安装与资源
-
安装:
pip install xgboost
或conda install -c conda-forge xgboost
-
官方文档:XGBoost Documentation
-
GitHub 仓库:dmlc/xgboost