麻雀搜索算法优化XGBoost:多特征分类调参实战指南
简介一份基于麻雀搜索算法SSA优化极端梯度提升XGBoost的多特征分类预测完整项目实例面向具备机器学习基础、希望掌握群体智能优化与XGBoost结合应用的研发人员和数据科学家可应用于金融风控、工业设备预测性维护、医疗辅助决策等场景。资源为单个docx文档大小约90KB以章节形式完整组织从项目背景、目标与意义出发详细说明了高维参数空间探索、不均衡数据评估偏差、搜索稳定性等挑战及解决方案并系统拆解了数据层与特征工程、模型层、优化层、评估层与早停机制、可解释性与监控层、工程与持久化层等模块。文档同时给出从环境准备、数据准备、算法设计、模型构建、参数调整、训练预测到性能评估、GUI界面设计的完整代码与实现细节便于按需查阅和复现。目前已有67人学习下载适合需要系统掌握自动化超参数寻优框架并落地多特征分类实践的数据科学从业者。1. SSA-XGBoost 是什么麻雀搜索算法给多特征分类预测减负的落地思路SSA-XGBoost 这个名字最近在多特征分类预测项目里出现得越来越频繁。它把麻雀搜索算法和 XGBoost 绑在一起用麻雀种群中发现者、加入者、警戒者三种角色的协作搜索自动帮你把 XGBoost 那组超参数找出来。说白了这就是一个给 xgboost 二分类模型做自动调参的方案你准备好特征矩阵和标签SSA 自己迭代几十代返回一组 n_estimators、max_depth、learning_rate 等参数的组合再用这组参数去训练最终模型。适合谁已经按 python 安装教程配好环境、能在 vscode python 环境配置里跑通 import xgboost但面对 7 个超参数不知道怎么组合的人。它解决的是调参像个黑匣子的问题不负责特征工程和业务理解。下面按一条完整路线展开为什么选 SSA 调 XGBoost、数据怎么准备、代码怎么落地、踩过哪些坑、最后怎么收进 GUI。2. 麻雀搜索算法为什么适合给 XGBoost 调参网格搜索到 SSA 的选型逻辑先想清楚一个问题XGBoost 默认参数在很多数据集上已经能跑出 0.85 的 AUC手动调参可能调到 0.87但时间花了两天。SSA 优化的目标是把这最后的 0.02~0.05 找出来同时不让你手动一遍遍试。为什么偏偏是麻雀算法而不是网格搜索、贝叶斯优化或者遗传算法这章把选型逻辑讲透。2.1 XGBoost 多特征分类要调的 7 个超参数范围、影响与编码方式XGBoost 分类模型的超参数可以分三组树结构、行与列采样、正则化。多特征场景下colsample_bytree 这类参数直接决定模型对特征维度的利用方式比在低维数据上更敏感。以下是实际项目中最常放进搜索空间的 7 个参数超参数常见范围类型对模型的影响SSA 中编码方式n_estimators50~500整数树越多拟合能力越强过多必过拟合整数编码解包时取整max_depth3~10整数控制树的复杂度越大交互项越多整数编码解包时取整learning_rate0.01~0.3连续越小越稳但需要更多树补偿连续编码subsample0.5~1.0连续行采样比例抗过拟合主力连续编码colsample_bytree0.5~1.0连续每棵树用多少比例特征多特征时关键连续编码reg_alpha0~1连续L1 正则把不重要特征的权重压向 0连续编码reg_lambda0~1连续L2 正则默认是 1搜索时从 0 起步连续编码这里有一个容易忽略的细节max_depth 和 n_estimators 是整数参数而 SSA 里每个个体的位置是浮点向量。如果解码时不取整XGBoost 会直接报错或者更麻烦——有些版本会静默截断让你以为 5.7 的深度有效实际上用的是 5。后文代码里 decode_position 函数专门处理这件事。选择这三个组的依据是它们对多特征分类的贡献不同树结构组决定拟合上限采样组决定泛化能力正则组决定特征稀疏性。实际调参时不要只动 n_estimators 和 max_depth多特征数据里 colsample_bytree 和 reg_alpha 的收益往往更大SSA 的价值就是把这几个参数一起搜而不是一个一个试。2.2 SSA 的三个种群角色与迭代流程发现者、加入者、警戒者怎么协作麻雀搜索算法是 2020 年前后提出的群智能优化算法模拟麻雀觅食和反捕食行为。它的核心是把种群分成三种角色各司其职发现者负责全局探索占据种群前 20% 左右。位置越靠前的发现者步长越大按 exp(-i / (alpha * T)) 衰减后期自动收敛。如果随机值超过预警阈值说明附近有危险发现者会放弃当前食物飞到一个全新的随机位置——这个机制让算法在后期仍然保留跳出去的勇气。加入者跟随发现者占剩余 80%。位置在后一半的个体会飞到随机区域重新碰运气位置尚可的个体则围绕当前全局最优位置做扰动步长用 L1 距离决定。警戒者随机抽出 10%~20%它们观察整体环境如果自己不是最优个体就往最优位置靠拢如果自己已经是最优就向远离最差个体的方向躲开防止整个种群扎堆。整个迭代流程可以写成五步初始化种群并计算适应度按适应度排序并区分角色更新发现者位置更新加入者位置最后更新警戒者位置做完边界检查后进入下一代。适应度就是下一章要讲的交叉验证 AUC。为什么选 SSA 而不是别的方法网格搜索在 7 个参数下组合爆炸6 个参数各取 5 档就是 15625 次实验乘上 5 折交叉验证实际训练量接近 8 万次随机搜索只做几百次抽样效率高但纯靠运气重复性差贝叶斯优化省训练次数但要维护高斯过程代理模型参数多时代理本身拟合成本不低调它的 acquisition function 又是一层玄学。SSA 的优势在于无模型、参数少。只需要设置种群大小、迭代次数、发现者比例、警戒者比例不容易出现调优化器的参比调模型还累的情况。以 pop_size30、max_iter50 为例总评估次数是 1500 次乘上 5 折交叉验证是 7500 次单模型训练在中小规模数据集上完全可接受。如果你只有单机 CPU 数据量又大就把 pop_size 降下来或者把交叉验证从 5 折改成 3 折。3. 多特征分类预测的数据准备分层划分、标准化与评估指标怎么定SSA 再强也是建立在干净数据上的。多特征分类项目最常见的问题不是算法跑不通而是数据划分和评估指标选错导致优化过程自欺欺人。这一章给出一套可以直接照抄的数据准备流程。3.1 用 StandardScaler 和 stratify 处理多特征数据最小可跑代码先从 sklearn 自带数据演示方便你完整复现。乳腺癌数据集 30 个特征、二分类正好是典型的多特征分类预测场景import numpy as np import pandas as pd from sklearn.datasets import load_breast_cancer from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler # 常见做法先用自带数据把流程跑通再换自己的 CSV data load_breast_cancer() X data.data # 30 个连续特征shape(569, 30) y data.target # 二分类标签正负样本约 1.5:1 # 多特征场景下先标准化SSA 的搜索稳定性依赖特征量纲统一 scaler StandardScaler() X_scaled scaler.fit_transform(X) # 分层划分保证训练集和测试集正负比例一致 X_train, X_test, y_train, y_test train_test_split( X_scaled, y, test_size0.2, stratifyy, random_state42 ) print(X_train.shape, X_test.shape) print(训练集标签分布:, np.bincount(y_train))逻辑说明StandardScaler 把每个特征变成均值 0、方差 1。多特征数据里如果同时存在年龄这种几十量级的特征和收入这种几万量级的特征不做标准化后续特征重要性分析和扰动都会偏向大数特征。stratifyy 是第二个关键数据不平衡时不做分层运气差会把某一类全切进测试集SSA 优化出来的参数在测试集上表现全是虚的。如果是自己的 CSV 文件替换方式很简单df pd.read_csv(your_dataset.csv) feature_cols [c for c in df.columns if c ! label] X df[feature_cols].to_numpy() y df[label].to_numpy()注意检查标签列的取值是否连续整数XGBoost 二分类要求标签是 0 和 1如果 CSV 里是字符串分类先做 LabelEncoder。random_state42 这一步不能省数据划分每次不一致后面 SSA 每次跑出来的最优参数就没有可比性这是调参项目里最常见的翻车起点。3.2 评估指标不要只盯准确率AUC、F1 与混淆矩阵的选择准确率在多特征分类里是最容易骗人的指标。正负样本比例 9:1 时一个把所有样本都判为负类的模型准确率是 0.9但对业务毫无价值。SSA 优化过程中适应度函数直接决定了搜索方向选错指标优化出来的就是偏科模型。指标关心的核心问题适合场景accuracy预测正确的比例类别均衡时可用precision判定为正的里面有多少真的为正误报代价高recall真实为正的里面召回多少漏报代价高F1precision 和 recall 的调和平均两者都想要AUC正负样本排序能力跨阈值类别不平衡推荐默认使用AUC 不依赖分类阈值它衡量的是模型把正样本排在负样本前面的能力类别不平衡时依然稳定所以 SSA 的适应度函数我一般直接用 5 折交叉验证的 AUC。先跑一个基线模型确认流程from sklearn.model_selection import StratifiedKFold, cross_val_score from xgboost import XGBClassifier model XGBClassifier( n_estimators100, max_depth5, learning_rate0.1, eval_metriclogloss, random_state42, verbosity0 ) cv StratifiedKFold(n_splits5, shuffleTrue, random_state42) scores cross_val_score(model, X_train, y_train, cvcv, scoringroc_auc) print(f默认参数 5-Fold AUC: {scores.mean():.4f} ± {scores.std():.4f})这段代码给你一个基线。之后 SSA 找到的参数如果 AUC 低于这个数说明优化逻辑有问题。交叉验证的折数在 SSA 里会放大训练成本pop_size30、max_iter50、5 折交叉验证共 7500 次模型训练几十分钟很正常。数据量大时先把折数降到 3或者把 pop_size 降到 15 跑通流程再说。4. 用 Python 实现 SSA-XGBoost麻雀优化器、适应度函数与主流程代码这一章是全文最核心的部分。代码分三段麻雀优化器类、适应度函数与参数解码、搜索空间与主流程。先把依赖装好pip install numpy pandas scikit-learn xgboost4.1 SparrowSearch 类的完整实现三种角色的位置更新麻雀优化器本身不关心你在优化什么模型它只接收一个适应度函数。这是一个最小可用的 SSA 实现保留了论文中三个角色的核心更新逻辑import numpy as np class SparrowSearch: 麻雀搜索算法默认求解最小化问题 def __init__(self, fitness_func, dim, lb, ub, pop_size30, max_iter50, PD0.2, SD0.1, seed42): self.fitness_func fitness_func self.dim dim self.lb np.asarray(lb, dtypefloat) self.ub np.asarray(ub, dtypefloat) self.pop_size pop_size self.max_iter max_iter self.PD PD # 发现者比例 self.SD SD # 警戒者比例 self.rng np.random.default_rng(seed) self.history [] # 记录每一代的最优适应度用于画收敛曲线 def _init_population(self): # 在 [lb, ub] 内均匀随机生成初始种群 return self.lb self.rng.random((self.pop_size, self.dim)) * (self.ub - self.lb) def optimize(self): pop self._init_population() fitness np.array([self.fitness_func(ind) for ind in pop]) best_idx int(np.argmin(fitness)) best_pos pop[best_idx].copy() best_fit fitness[best_idx] for t in range(self.max_iter): # 按适应度从好到差排序好个体靠前 order np.argsort(fitness) sort_pop pop[order].copy() sort_fit fitness[order] # 1) 发现者更新前 PD% 的个体做全局探索 pd_num max(1, int(self.PD * self.pop_size)) for i in range(pd_num): alpha self.rng.random() * 0.99 0.01 step np.exp(-i / (alpha * self.max_iter)) if self.rng.random() 0.8: # 预警值小于安全阈值正常局部搜索 diff self.rng.random(self.dim) * (self.ub - self.lb) self.lb sort_pop[i] step * diff else: # 发现危险飞回一个随机的安全位置 sort_pop[i] self.lb self.rng.random(self.dim) * (self.ub - self.lb) # 2) 加入者更新跟随最优个体或去随机区域 for i in range(pd_num, self.pop_size): if i self.pop_size / 2: # 位置太差去随机区域碰机会 sort_pop[i] self.lb self.rng.random(self.dim) * (self.ub - self.lb) else: # 围绕历史最优位置做 L1 步长扰动 A np.where(self.rng.random(self.dim) 0.5, 1, -1) L1 np.abs(sort_pop[i] - best_pos).sum() sort_pop[i] best_pos L1 * A # 3) 警戒者更新随机挑 SD% 的个体模拟反捕食 sd_num max(1, int(self.SD * self.pop_size)) for _ in range(sd_num): idx int(self.rng.integers(0, self.pop_size)) if sort_fit[idx] best_fit: # 边缘个体向最优位置靠拢 beta self.rng.normal(0, 1) sort_pop[idx] best_pos beta * np.abs(sort_pop[idx] - best_pos) else: # 最优个体附近向远离最差方向躲开 k self.rng.uniform(-1, 1) diff_vec sort_pop[idx] - sort_pop[-1] norm np.linalg.norm(diff_vec) 1e-12 sort_pop[idx] sort_pop[idx] k * (diff_vec / norm) # 边界处理与重新评估 pop np.clip(sort_pop, self.lb, self.ub) fitness np.array([self.fitness_func(ind) for ind in pop]) if fitness.min() best_fit: best_idx int(np.argmin(fitness)) best_pos pop[best_idx].copy() best_fit fitness[best_idx] self.history.append(best_fit) return best_pos, best_fit代码逻辑说明发现者的 step 随 i 和迭代次数衰减i 越小步长越大保证少数最优个体大步探索后续发现者逐步收紧警戒者的 beta 用标准正态随机数可以让最优个体周围的个体做出大范围的跳跃norm 加 1e-12 是防止所有个体坐标重合时除以零报错。加了 history 记录每一代最优适应度后面验证收敛和画图都用得上。实现细节上有一个选择和原论文不同这里没有为每只麻雀维护一个单独的适应度副本而是在每代更新完统一重新评估逻辑更清晰代价是每代多算若干次适应度。对 XGBoost 这种单次训练 1~2 秒的场景这点开销可以接受。4.2 适应度函数设计5 折交叉验证 AUC 与参数解码SSA 是求最小化所以适应度返回 1 - AUC。这里的关键是把浮点位置还原成真实超参数from sklearn.model_selection import StratifiedKFold, cross_val_score from xgboost import XGBClassifier def decode_position(position, param_space): 把 SSA 的归一化位置解码成 XGBoost 的超参数字典 params {} for idx, (name, (lo, hi, ptype)) in enumerate(param_space.items()): value lo position[idx] * (hi - lo) if ptype int: value int(round(value)) # 必修整数参数必须取整 params[name] value return params def make_fitness(X_train, y_train, param_space, n_splits5): 返回一个供 SSA 使用的适应度函数 cv StratifiedKFold(n_splitsn_splits, shuffleTrue, random_state42) def fitness(position): params decode_position(position, param_space) model XGBClassifier( n_estimatorsparams[n_estimators], max_depthparams[max_depth], learning_rateparams[learning_rate], subsampleparams[subsample], colsample_bytreeparams[colsample_bytree], reg_alphaparams[reg_alpha], reg_lambdaparams[reg_lambda], eval_metriclogloss, random_state42, n_jobs-1, verbosity0 ) aucs cross_val_score(model, X_train, y_train, cvcv, scoringroc_auc) return 1.0 - aucs.mean() return fitness这里 decode_position 用归一化位置的好处是SSA 内部的 lb、ub 统一设成 0 和 1搜索空间各维度的量纲一致算法不用感知不同参数的真实尺度。int 类型的参数必须用 int(round(value))否则 max_depth 会以浮点传给 XGBoost直接报错或者被静默截断。4.3 搜索空间编码与一键运行主流程把参数空间定义好封装成一个 run_ssa 函数后面 GUI 直接复用param_space { n_estimators: (50, 500, int), # 树数量 max_depth: (3, 10, int), # 最大深度 learning_rate: (0.01, 0.3, float), # 学习率 subsample: (0.5, 1.0, float), # 行采样 colsample_bytree: (0.5, 1.0, float), # 列采样 reg_alpha: (0.0, 1.0, float), # L1 正则 reg_lambda: (0.0, 1.0, float), # L2 正则 } def run_ssa(X_train, y_train, param_space, pop_size20, max_iter30, seed42): lb np.zeros(len(param_space)) ub np.ones(len(param_space)) fitness_fn make_fitness(X_train, y_train, param_space) ssa SparrowSearch( fitness_funcfitness_fn, dimlen(param_space), lblb, ubub, pop_sizepop_size, max_itermax_iter, seedseed ) best_pos, best_fit ssa.optimize() best_params decode_position(best_pos, param_space) return best_params, best_fit, ssa.history # 跑一次20 只麻雀30 代约 600 次评估 × 5 折 best_params, best_fit, history run_ssa( X_train, y_train, param_space, pop_size20, max_iter30, seed42 ) print(最优参数:, best_params) print(最优 1-AUC:, round(best_fit, 4))参数取值建议pop_size20、max_iter30 是第一档验证配置确认代码能跑通后再调成 pop_size40、max_iter60 追求更优结果。learning_rate 建议在 0.01~0.3 之间用对数均匀取值但跨度不大时线性取值也能用不会出大问题。注意SSA 优化过程中千万不要把测试集 X_test 和 y_test 丢进适应度函数否则你得到的是一个自欺欺人的高分。测试集只允许在拿到最优参数后进一次。5. SSA-XGBoost 调参常见问题排查5 个高频踩坑点与解决办法这一章写的是实际跑 SSA-XGBoost 时大概率会遇见的 5 个问题每一条都按现象、原因、解决展开。如果你按照第 4 章的代码跑出了异常结果先来这里对照。5.1 类别不平衡导致优化出偏科模型现象SSA 优化过程很顺利1-AUC 从 0.3 一路降到 0.1但拿测试集一算精度高得吓人F1 却只有 0.3。翻混淆矩阵发现正样本几乎全被吞了。原因适应度函数用了 accuracy。正负样本比 9:1 时模型全部预测负类就能拿到 0.9 的准确率SSA 的最小化方向被这个假指标带偏了。这不是 SSA 的问题是适应度函数选错了。解决适应度函数换成 5 折交叉验证的 roc_auc可以配合 F1 做最终验证。AUC 对类别比例不敏感能把正负样本的排序能力作为优化目标。正负样本比超过 5:1 时考虑对少数类做 SMOTE 过采样但要做在交叉验证的每一折内部否则会有数据泄漏。5.2 max_depth、n_estimators 类型错误与 NaN 参数现象XGBoost 报 ValueError提示 max_depth must be int或者程序不报错但训练出来的模型参数怪怪的比如 n_estimators 是 127.8 棵树。原因SSA 生成的位置是浮点向量decode_position 里没有把 int 类型参数取整就传给 XGBClassifier 了。部分版本会静默做类型转换另一部分直接抛异常表现不一致很迷惑人。解决decode_position 里对 ptypeint 的参数执行 int(round(value))并在传给模型前加一个断言assert isinstance(params[max_depth], int), max_depth 必须是整数另外在 SSA 边界处理函数里加一行把 NaN 拉回边界np.nan_to_num(pop, nanself.lb)。种群中一旦出现 NaNnp.clip 不会修复它NaN 会一路传播到适应度函数里导致模型训练出 NaN 损失。5.3 多次运行结果不一致优化不可复现现象同样的代码、同样的参数配置今天跑出的最优参数是 n_estimators320、max_depth5明天跑出来是 n_estimators180、max_depth7两个结果 AUC 差不多但你没法判断是 SSA 收敛了还是随机扰动在起作用。原因随机种子没有固定。train_test_split 的 random_state、StratifiedKFold 的 random_state、XGBClassifier 的 random_state、SSA 内部 np.random.default_rng 的 seed这四个只要有一个没固定结果就会有波动。解决四个地方全部固定。有一种情况要特别说明即使 seed 固定n_jobs-1 时 XGBoost 的多线程计算仍可能引入微不足道的浮动但主结果可复现。实际项目里多跑 3 次取最优参数中出现频率最高的组合比追求单次最高分更稳妥。5.4 种群早熟收敛1-AUC 提前卡平台现象history 曲线前 10 代下降很快到第 15 代就平了之后 30 代一动不动。打印种群位置发现所有个体的坐标几乎重合整个搜索变成了围绕同一个点的微调。原因发现者比例太小或警戒者扰动太弱种群失去了多样性。SSA 梯度信息本来就弱全靠种群分散探索一旦全部个体聚拢局部最优就再也没机会跳出去。解决把 PD 从 0.2 提高到 0.25SD 从 0.1 提高到 0.15~0.2pop_size 从 20 提到 40。另一个技巧是在每代结束后对当前最优个体加一个小的高斯扰动强迫算法在最优附近再探一步。如果确认是搜索空间边界设窄了比如 max_depth 上限 6而真实最优在 8种群会在边界处堆满先看最优参数是否顶着边界是就把边界放宽重跑。5.5 特征含缺失值适应度变成 NaN现象SSA 跑了几代之后history 里突然出现 NaN整个种群废掉或者 XGBoost 训练时损失一路是 NaN。原因多特征数据里常有缺失值StandardScaler 遇到 NaN 会直接报错或者更隐蔽地产生 NaN 特征SSA 位置更新过程中产生的新位置落在缺失值区域交叉验证评分也是一团糟。解决进入 SSA 之前做数据完整性检查print(训练集缺失值数量:, np.isnan(X_train).sum())有缺失就先用 SimpleImputer 填充from sklearn.impute import SimpleImputer imputer SimpleImputer(strategymedian) X_train imputer.fit_transform(X_train) X_test imputer.transform(X_test)用中位数而不是均值对离群值更稳健。XGBoost 本身支持用 missing 参数指定缺失值但多特征连续数据的项目里先填充再做标准化和 SSA 搜索流程更简单可控。6. 把 SSA-XGBoost 装进 GUItkinter 做可点击的多特征分类调参工具命令行跑通还不够很多从业者需要把方案交付给不会写代码的人。tkinter 是 Python 自带的界面库无额外依赖做一版最小的 SSA-XGBoost 调参工具非常合适。它的核心价值不是花哨而是把选数据、点按钮、看结果这件事固化下来。import tkinter as tk from tkinter import filedialog, scrolledtext, messagebox import threading class SSAGUI: def __init__(self, root): self.root root root.title(SSA-XGBoost 多特征分类调参工具) self.file_path tk.StringVar() self.textbox scrolledtext.ScrolledText(root, width80, height24) self.textbox.pack(padx10, pady10) tk.Button(root, text选择 CSV 数据, commandself.select_file).pack(pady4) tk.Button(root, text开始 SSA 优化, commandself.start_opt).pack(pady4) def select_file(self): self.file_path.set( filedialog.askopenfilename(filetypes[(CSV, *.csv)]) ) def start_opt(self): # 耗时任务必须放子线程否则窗口卡死几十秒 threading.Thread(targetself._run, daemonTrue).start() def _run(self): try: self.textbox.insert(tk.END, 开始读取数据...\n) # 读 CSV、取特征和标签、StandardScaler、train_test_split # 这些步骤复用第 3 章代码抽成 load_and_split(path) 函数 self.textbox.insert(tk.END, 开始 SSA 优化请耐心等待...\n) best_params, best_fit, history run_ssa( X_train, y_train, param_space, pop_size30, max_iter40 ) self.textbox.insert(tk.END, f最优参数: {best_params}\n) self.textbox.insert(tk.END, f最优 1-AUC: {best_fit:.4f}\n) # 把 history 绘制成 matplotlib 收敛曲线 messagebox.showinfo(完成, 优化结束参数已输出) except Exception as e: messagebox.showerror(错误, str(e))这个界面的关键点是 start_opt 里必须用 threading.Thread 启动训练任务。SSA 加 5 折交叉验证一次跑几分钟如果直接放在按钮回调里tkinter 主循环会被阻塞窗口显示未响应看的人会直接关掉程序。第二个值得做的事是把最优参数存成 JSON下次打开程序可以直接加载不用再跑一遍 SSAimport json with open(best_params.json, w, encodingutf-8) as f: json.dump(best_params, f, ensure_asciiFalse, indent2)GUI 的价值不完全在界面好看而是让整个调参过程可复用。我前后跑过十来个多特征分类项目最深的教训是不管用什么优化算法先固定数据划分、固定随机种子、把基线 AUC 打出来再让算法去折腾。任何优化器都救不了一个数据划分不稳定的实验。第二个教训是多跑一次 SSA 成本不高但盯着 history 曲线看它有没有收敛到平台期比盲目加大迭代次数有用得多。按照这套流程新手从 0 到跑出最优参数大概一个下午熟手可以在这个骨架上继续加特征重要性排序、阈值寻优和结果导出。希望帮到你。本文还有配套的精品资源点击获取