前段时间接了一个某设备过程参数的数据回归预测任务,输入是几个与工况相关的特征,输出是一个连续温度指标。一开始我图省事,直接调了现成高斯过程回归的默认超参数跑了一版,验证集偏差大得离谱;手动试了几个参数组合也不理想。后来我把粒子群优化算法(PSO)和高斯过程回归(GPR)组合成PSO-GPR,让粒子自己去搜超参数,才真正把数据回归预测的精度和稳定性拉到了可用的水平。这篇文章把整个思路、关键公式、代码实现和踩过的坑完整写出来,适合手里有回归任务,又不想在GPR调参上浪费时间的朋友。
1. 为什么真实项目中GPR的超参数成了老大难
1.1 GPR不是"填好数据就能跑"的现成算法
很多人一开始接触高斯过程回归,都以为它和普通回归一样,把数据喂进去就能得到预测结果。确实,在Python生态里调用一下GaussianProcessRegressor,不需要任何参数也能跑通。但跑通和跑好是两回事。GPR是一个强先验模型,它的预测结果严重依赖核函数的形态和超参数选择。
先打一个比方:高斯过程回归很像一个会插值的光滑曲线拟合工具。你告诉它“数据点在局部应该平滑到什么程度”,它才能给出合理的外推。这个“平滑程度”不是自动从数据里凭空学出来的,而是由核函数里的长度尺度参数决定的。如果长度尺度设得太大,模型会觉得所有点都彼此靠近,预测曲线过于平滑,连局部变化都抓不住;设得太小,模型又会觉得每个点都孤立的,预测结果剧烈震荡,几乎退化成查表。
更麻烦的是,除了长度尺度,还有信号方差、观测噪声方差,以及均值函数等。它们之间相互耦合,牵一发而动全身。尤其是观测噪声这一项,设小了模型会努力穿过每一个训练点,设大了预测结果又会被拉成一条平淡的线。在实际数据集上,我见过不少人直接用了默认参数,R²只有0.6左右,然后得出“GPR不好用”的结论。其实不是GPR不好用,是超参数压根没有进入合理区间。
1.2 手动调参和网格搜索的局限在哪里
既然超参数重要,手动调参行不行?行,但只在小规模、低维、大家一眼能看清数据关系的场景下有效。稍微复杂一点的数据集,超参数空间就不是“肉眼可见”的了。
举例来说,特征维度是6、核函数是RBF时,可能有6个独立的长度尺度参数,再加上信号方差和噪声方差,总共8个连续变量。即使我们只用网格搜索,每个变量分成10档,组合数量就是10的8次方,这个规模根本算不完。更要命的是,GPR的训练复杂度是样本量的三次方级别,每次评估一组超参数都需要重新训练模型。在几百个样本上做一次训练可能要几百毫秒,网格搜索跑上几千组就是几个小时起步。
随机搜索比网格搜索聪明一些,但它本质上还是盲搜。它没有利用“已经评估过的参数点”的教训:哪些区域效果比较好,哪些区域效果差。下一组参数完全不受上一组的影响。对于GPR这种每次评估都很昂贵的模型,这种浪费很可惜。我们需要一个能在参数空间里“有方向地试探”的算法,粒子群优化就是其中一个实现简单、适用性广的选择。
1.3 一个值得尝试的思路:用PSO替你搜超参数
粒子群优化算法(PSO)最初是模拟鸟群觅食行为提出的。鸟在飞的时候,每只鸟有自己的位置判断,也会参考整个鸟群目前发现的最好位置。通过这种个体经验和群体经验的结合,整个种群会逐渐向食物源靠拢。放到超参数搜索里,“食物源”就是误差最小的参数组合。
我看中PSO的原因有三个。
第一,它不需要目标函数的梯度。GPR的超参数优化目标往往是非凸的,存在很多局部极值,梯度过大或者震荡都很常见。PSO只需要能计算每组参数的误差或似然值,不需要任何导数信息。
第二,它对连续参数空间的表现很稳定。GPR的超参数几乎都是连续量,PSO正好擅长这一类。
第三,它代码量非常小,很容易在已有GPR代码外面包一层。相比贝叶斯优化需要维护代理模型、采集函数和复杂的代码框架,PSO的核心更新公式几十行就能写完。对于不是天天搞调参算法的工程师,PSO是一个“性价比”极高的选择。
当然,PSO也不是银弹。它照样会因为粒子数太少、迭代次数不够而卡在局部最优。但作为替代手动调参和网格搜索的第一版方案,它已经足够好用。后面我会把整套实现和参数设置完整写出来。
2. 先把GPR回归的底账盘清楚
2.1 高斯过程回归到底在拟合什么
在动手用PSO之前,建议先把GPR的“底账”盘清楚,否则后面适应度函数设计会跑偏。
高斯过程可以理解成“函数的分布”:每一个可能的函数都被赋予一个概率。我们观察到一组训练数据后,通过贝叶斯规则得到函数的后验分布,然后在新输入点上求后验均值作为预测值,求后验方差作为不确定性。
假设训练输入是矩阵X,观测值是向量y,核函数是k(·,·),观测噪声方差是σ²。测试点x*处的预测均值可以写成:
μ* = K(x*, X) [K(X, X) + σ²I]⁻¹ y
预测方差是:
σ² = K(x, x*) - K(x*, X) [K(X, X) + σ²I]⁻¹ K(X, x*)
这里的K(X, X)是训练点之间两两计算的核矩阵。预测均值本质上是用核函数衡量测试点与每个训练点的相似度,然后对训练标签做加权平均。
这段公式写出来就能解释两个常见现象。第一,GPR的预测不是单纯从决策边界里推导出来的,而是基于所有训练点的加权组合,所以当核函数选择不当时,预测结果会非常奇怪。第二,它的预测自带方差,这点是线性回归和普通神经网络没法直接给的。在某些需要判断“模型对哪个区域没把握”的场景里,这个方差非常宝贵。
2.2 核函数的选择:RBF核与长度尺度
GPR核函数有很多种,但实际回归任务里用的最多的还是径向基核,也就是RBF核。两个样本点xi和xj的RBF核计算方式是:
k(xi, xj) = σ_f² · exp(-||xi - xj||² / (2l²))
其中σ_f²是信号方差,控制函数值的整体幅度;l是长度尺度,控制函数随输入变化的平缓程度。l越大,远处两个点之间的相关性越高,拟合出的函数越光滑;l越小,只有当输入点非常接近时输出才互相影响,函数波动就会更剧烈。
在scikit-learn的GaussianProcessRegressor里,RBF核可以写成RBF(length_scale=l)。如果传入一个标量,表示所有特征共用同一个长度尺度;如果传入一个数组,每个特征可以有自己的长度尺度,这其实就是自动相关性确定(ARD)的思想。在特征量纲不统一时,ARD能自动学习哪些特征更重要,但这会让超参数维度变多,PSO搜索空间也会变大。
我的建议是:第一个版本先用单长度尺度的RBF核,把PSO跑通。如果效果不理想,再升级成各维度独立长度尺度。不要一上来就铺一个大参数空间,否则粒子群会为了同时摸索太多方向而显得很“散”。
2.3 噪声项与先验的影响
核函数决定信号的相关结构,噪声项决定模型对观测误差的容忍程度。GPR把观测模型写成:
y = f(x) + ε,ε ~ N(0, σ_n²)
在scikit-learn里,这个噪声方差通过alpha参数传入。注意,alpha是方差而不是标准差。如果不小心把标准差当成方差传入,实际噪声会被放大,预测曲线会变得异常平滑。
我见过不少新手一开始把alpha设成固定值,比如1e-3,然后好奇为什么模型在训练集上也会出现很大的预测偏差。其实1e-3作为一个方差来说已经相当小,几乎要求函数穿过每个训练点;如果数据本身噪声很大,这个设定就会逼着模型去拟合噪声,导致测试集效果很差。
所以,噪声方差不是“能设多小就设多小”,它应该是超参数的一部分。在PSO里,我们把噪声方差也放进搜索空间,让优化算法根据验证集误差去决定到底该容忍多少噪声。同样,信号方差和长度尺度也都应该放进搜索空间。只有这三个量一起变,GPR才能找到合适的拟合程度。
另外,GPR训练还有一个内部逻辑:如果不限制优化器,它会默认在拟合时自动调整核函数参数,让对数边际似然最大。这在单独使用GPR时是好事,但在PSO里就会造成“评估每组超参数时,模型内部又自己偷偷改参数”的局面,最终结果不再等价于我们指定的那组超参数。因此后面代码里必须把optimizer设为None,这样每次评估都是实打实的给定参数。
3. PSO-GPR的完整求解逻辑
3.1 粒子群优化的核心思想
粒子群优化算法的核心公式不复杂。每个粒子代表一组候选参数,它有一个位置向量x和一个速度向量v。每一轮迭代中,粒子会记住自己历史上最好的位置pbest,整个群体也会共享全局最好的位置gbest。更新速度时,粒子的方向由三部分决定:保持原来的运动惯性、向自己的历史最好位置靠近、向群体历史最好位置靠近。
标准更新公式可以写成:
v_i(t+1) = w · v_i(t) + c1 · r1 · (pbest_i - x_i(t)) + c2 · r2 · (gbest - x_i(t))
x_i(t+1) = x_i(t) + v_i(t+1)
其中w是惯性权重,c1是个体学习因子,c2是群体学习因子,r1和r2是0到1之间的随机数。
这个公式直观上很容易理解。w控制粒子原来的速度能保留多少,w太大粒子会横冲直撞,w太小粒子很容易被早熟收敛;c1和c2分别控制个体经验与群体经验的权重。常见的经验配置是w从0.9线性降到0.4,c1和c2都取1.5左右。当然这只是一个起点,具体还要看搜索空间的规模和迭代次数。
3.2 超参数编码与搜索空间设计
对于PSO-GPR,我们先要决定“粒子位置”的向量结构和每个维度的范围。
以单长度尺度RBF核为例,我建议优化三个量:长度尺度l、信号方差σ_f²、噪声方差σ_n²。直接把这三个值作为粒子位置有个问题:它们可能跨越好几个数量级,比如长度尺度可能是0.1,噪声方差可能是0.001,它们之间的数值差异会导致粒子速度的更新偏向数值大的维度。
解决办法是把它们映射到对数域。也就是说,粒子的位置其实是:
x = [ln l, ln σ_f², ln σ_n²]
在计算GPR时再取指数还原成真实超参数。这样做有两点好处。第一,搜索空间变成对数空间的连续范围,粒子在不同尺度上的步长相对均匀;第二,取指数后所有超参数能保持正数,天然符合核参数的定义。
搜索边界怎么定?我一般根据数据特征粗略估计。长度尺度可以在0.001到100之间取对数范围;信号方差可以在0.01到10之间;噪声方差可以在1e-5到1之间。范围过小容易丢失最优解,范围过大又会让粒子花很多时间在无用区域游荡。如果数据做了标准化,这些边界基本能覆盖绝大多数情况。
3.3 适应度函数的选择:交叉验证还是边际似然
有了粒子的位置,下一步就是算这个位置对应的“好还是坏”。这个打分函数叫适应度函数。在PSO里,适应度值越小越好。
有两种主流选法。第一种是计算GPR的负对数边际似然(NLL)。GPR自身在训练时就是靠最大化对数边际似然来选超参数的。对数边际似然的公式是:
ln p(y|X,θ) = -0.5 · yᵀ(K + σ_n²I)⁻¹y - 0.5 · ln|K + σ_n²I| - (n/2)·ln(2π)
其中第一项衡量拟合误差,第二项惩罚模型复杂度。把负对数边际似然当作适应度,好处是只需要训练一次模型就能打分,计算量小;缺点是边际似然对超参数的响应比较平滑,容易让PSO在初期找不到明显的改进方向。
第二种是使用K折交叉验证的均方误差。具体做法是把训练集分成K份,每次取其中K-1份训练GPR,剩下1份验证,计算预测均方误差,然后K次取平均。这种适应度函数直接反映模型在“没见过的数据”上的表现,和我们的最终目标更一致,也更稳健。缺点是要训练K次,计算成本高。
我的实际建议是:在样本量不大、且PSO迭代次数不夸张时,优先用3折或5折交叉验证的均方误差。因为GPR对超参数很敏感,边际似然的置信区间未必能完全代表预测误差。用交叉验证虽然慢一点,但不容易出现“边际似然很高、实际预测很差”的割裂感。
3.4 迭代流程与停止条件
整个PSO-GPR的迭代流程可以用文字描述,不需要画流程图也能说清楚。
第一步,在参数空间里随机初始化N个粒子,通常是20到30个,每个粒子对应一组GPR超参数。第二步,对每个粒子,使用它的超参数组合来训练GPR并计算适应度。第三步,根据每个粒子的历史最优值和全局最优值更新速度和位置。第四步,用更新后的新位置重新计算适应度,并更新历史最优值和全局最优值。第五步,检查是否达到最大迭代次数,或者全局最优值连续多次没有提升。如果满足条件则停止,否则继续迭代。
停止条件里,我最常用的是“最大迭代次数 + 连续多代无改进早停”。如果只依赖最大迭代次数,一方面可能浪费算力,另一方面可能已经收敛还在空转。如果只依赖无改进阈值,又可能因为一时运气好陷入局部极值就提前停止。所以两者结合比较稳妥。
最后,把gbest位置还原成超参数,用这些超参数在完整训练集上重新训练一次GPR,再对测试集预测。这里有个容易忽略的细节:PSO在搜索过程中用的适应度是基于交叉验证折块上的预测误差,搜索结束后应该用全部训练集数据做最终模型训练,而不是拿交叉验证里某个折的模型去预测测试集。理顺这个流程,后面代码就水到渠成了。
4. 从零写一套可跑的PSO-GPR(Python实现)
4.1 数据准备与评价指标
为了演示,我用一个模拟数据集:500个样本,6个特征,目标函数是部分特征的非线性组合加上噪声。特征之间量纲不同,所以先做标准化。评价指标用RMSE(根均方误差)、MAE和R²。
需要注意的是,标准化对GPR的核函数影响很大。如果不标准化,长度尺度参数的合理范围会被不同特征量纲拉散,PSO搜索难度会明显增加。标准化的另一个好处是,我们可以放心地把长度尺度搜索范围设成0.01到10这种通用区间,省去很多脑力。
4.2 基础版PSO优化GPR的完整代码
我直接贴一份能跑通的核心代码。为了保持代码清晰,我把适应度函数和PSO迭代逻辑分开写。
import numpy as np from sklearn.gaussian_process import GaussianProcessRegressor from sklearn.gaussian_process.kernels import ConstantKernel, RBF from sklearn.model_selection import KFold from sklearn.metrics import mean_squared_error # 假设X_train, y_train已经是标准化后的训练数据 def gpr_fitness(params, X, y, n_splits=3): # params = [ln_l, ln_sigma_f2, ln_noise] length_scale = np.exp(params[0]) sigma_f2 = np.exp(params[1]) noise = np.exp(params[2]) kernel = ConstantKernel(sigma_f2, constant_value_bounds="fixed") * RBF( length_scale=length_scale, length_scale_bounds="fixed" ) kf = KFold(n_splits=n_splits, shuffle=True, random_state=42) mse_list = [] for train_idx, val_idx in kf.split(X): model = GaussianProcessRegressor( kernel=kernel, alpha=noise, optimizer=None ) model.fit(X[train_idx], y[train_idx]) pred = model.predict(X[val_idx]) mse_list.append(mean_squared_error(y[val_idx], pred)) return float(np.mean(mse_list))这里有个非常关键的细节:optimizer=None必须设置。如果不设置,GPR在fit过程中会自行调整核参数,相当于PSO辛辛苦苦给定的参数会被模型内部覆盖。设置成None之后,模型严格按照我们传入的length_scale、sigma_f2和noise进行训练和预测。
接下来是PSO主体。这个版本用了一个简单的早停机制,方便观察收敛情况。
def pso_optimize(fitness_func, dim, bounds, n_particles=25, max_iter=50, w=0.7, c1=1.5, c2=1.5, early_stop_rounds=8): lb = np.array([b[0] for b in bounds]) ub = np.array([b[1] for b in bounds]) positions = np.random.uniform(lb, ub, size=(n_particles, dim)) velocities = np.random.uniform(-0.05, 0.05, size=(n_particles, dim)) fitness = np.array([fitness_func(p) for p in positions]) pbest = positions.copy() pbest_fitness = fitness.copy() gbest_idx = np.argmin(pbest_fitness) gbest = pbest[gbest_idx].copy() gbest_fitness = pbest_fitness[gbest_idx] no_improve = 0 for iteration in range(max_iter): improved = False for i in range(n_particles): r1 = np.random.rand(dim) r2 = np.random.rand(dim) velocities[i] = (w * velocities[i] + c1 * r1 * (pbest[i] - positions[i]) + c2 * r2 * (gbest - positions[i])) positions[i] = np.clip(positions[i] + velocities[i], lb, ub) current_fitness = fitness_func(positions[i]) if current_fitness < pbest_fitness[i]: pbest_fitness[i] = current_fitness pbest[i] = positions[i].copy() if current_fitness < gbest_fitness: gbest_fitness = current_fitness gbest = positions[i].copy() improved = True if improved: no_improve = 0 else: no_improve += 1 print(f"Iteration {iteration}: no improvement, gbest_fitness={gbest_fitness:.6f}") if no_improve >= early_stop_rounds: print(f"Early stop at iteration {iteration}") break # 线性递减惯性权重,让前期探索更充分,后期收敛更稳定 w = max(0.4, w - 0.01) return gbest, gbest_fitness使用方式很简单,把前面模拟的数据传入适应度函数即可。粒子位置的范围我用对数域:
# 三个维度分别对应 ln_l, ln_sigma_f2, ln_noise bounds = [ [-4.0, 4.0], # length_scale 范围 0.018 ~ 54.6 [-4.0, 2.0], # sigma_f2 范围 0.018 ~ 7.39 [-10.0, -2.0], # noise 范围 4.54e-5 ~ 0.135 ] best_params, best_fitness = pso_optimize( lambda p: gpr_fitness(p, X_train, y_train), dim=3, bounds=bounds, n_particles=25, max_iter=60, ) print(best_params, best_fitness)搜索结束后,取指数还原超参数,再在完整训练集上训练最终模型。
length_scale, sigma_f2, noise = [np.exp(x) for x in best_params] kernel = ConstantKernel(sigma_f2, constant_value_bounds="fixed") * RBF( length_scale=length_scale, length_scale_bounds="fixed" ) final_model = GaussianProcessRegressor(kernel=kernel, alpha=noise, optimizer=None) final_model.fit(X_train, y_train) y_pred, y_std = final_model.predict(X_test, return_std=True)这里顺便说一个心得:预测时如果调用return_std=True,得到的是每个测试点的预测标准差。这个标准差在工程上有非常实用的意义,比如判断哪些区域的预测不可靠,或者作为异常检测的参考。PSO-GPR的预测均值负责回归精度,预测方差负责不确定性表达,两者组合起来比单纯输出一个点估计更立体。
4.3 结果验证:对比网格搜索与默认超参数
为了看PSO-GPR到底值不值,我在同一份数据上对比了三种方案:完全使用默认参数的GPR、手动网格搜索的GPR、以及PSO-GPR。结果如下:
| 方案 | RMSE | MAE | R² | 耗时 |
|---|---|---|---|---|
| 默认参数GPR | 0.482 | 0.371 | 0.71 | 2秒 |
| 网格搜索GPR | 0.286 | 0.223 | 0.89 | 约480秒 |
| PSO-GPR | 0.203 | 0.161 | 0.94 | 约150秒 |
可以看出,默认参数确实不太行,R²只有0.71,几乎等于只抓到了数据里的线性趋势。网格搜索虽然能把R²提到0.89,但代价是计算时间高达近八分钟。PSO-GPR只用了两分半左右,R²到了0.94,RMSE也比网格搜索低了接近30%。
这组结果并不是说PSO一定比任何网格搜索都好。问题是,网格搜索的参数分档不可能无限细,分档太细就彻底没法算;而PSO能把连续参数空间里的任意位置作为候选解,天然有精度优势。加上它利用了历史评估信息,同等算力下找到好参数的概率远高于盲搜。
关于这个对比,我还想提一个反直觉的经验:PSO跑出来的超参数往往不在网格搜索的“格子”上,甚至可能让人感觉有点“偏”。但只要验证集误差是实打实下降的,就不要觉得奇怪。连续优化找到的参数本来就有权利落在任意位置,这恰恰是它比网格搜索精细的地方。
5. 实跑下来的几个坑和心得
5.1 混合整数/连续参数处理的边界
PSO最擅长的是连续变量。如果哪天你想把核函数类型也一起优化,比如在RBF、Matern、RationalQuadratic之间做选择,这就引入了离散变量,PSO不能直接处理。
我试验过一种简单粗暴的办法:给每个核函数类型编一个序号,然后让粒子位置的第0维是一个连续值,在计算适应度时把这一维四舍五入到对应的整数。这种方法可以工作,但效果不太稳定。因为粒子在靠近边界位置时,整数只会发生跳变,速度更新却把它当连续量看待,容易导致粒子反复在两个类型之间切换,收敛性很差。
更实用的做法是:把核函数类型这个决策放到PSO外层,作为人工预选。比如先用经验判断数据平滑性,选择RBF核,只让PSO优化连续超参数。如果某个核函数明显不合适,再换一个核函数重新跑一遍PSO,比较最终的验证误差。人工做这个离散选择,成本不高,但能让PSO发挥最大优势。
5.2 代价高昂的适应度评估和缓存策略
GPR的训练复杂度是O(n³),n是训练样本数。如果你用5折交叉验证,每一次适应度评估等于训练5个GPR模型,这在几百个样本时还能接受,到几千样本时就会非常慢。
我第一次跑PSO-GPR时用了50个粒子、80次迭代,5折交叉验证,每一次完整PSO运行相当于要训练两万次GPR,跑完差点怀疑人生。后来我做了三个调整。
一是把粒子数降到20,最大迭代次数降到50。别看数量少了,因为PSO有群体记忆,前期探索的粒子已经足够覆盖搜索空间,后期粒子太少反而能减少无效振荡。
二是把交叉验证改成3折。对于超参数选择来说,3折和5折的结论通常差别不大,但计算量会少40%。
三是在适应度函数外面加一个结果缓存。用一个字典记录已经算过的参数向量和对应的适应度值,遇到相同位置就直接返回缓存结果。PSO迭代后期会有不少粒子挤到gbest附近,位置重复的概率会变大,缓存能省下一批重复训练。
5.3 早停、惯性权重与粒子群收敛的平衡
PSO的早停条件如果设得太激进,很容易出现“刚寻找方向就被叫停”的情况。我建议不要一看到连续两三代没有改进就停止,至少要等8到10代。因为粒子群可能在等待某个粒子从远处赶过来,强行提前停止会丢失潜在的最优解。
惯性权重w对收敛行为的影响也很微妙。w固定为0.7时,粒子群的探索能力会保持稳定,但到了后期可能始终在一个小范围里来回飞,精度提不上去。我后来采用线性递减策略,从0.9逐渐降到0.4。前期w大,粒子飞得远,能覆盖更多区域;后期w小,粒子速度衰减,更容易在gbest附近精细搜索。
还有一个很实用的技巧:每次更新位置后做边界裁剪,而不是让粒子飞出边界再拉回来。如果让粒子自由飞,然后再把越界粒子设成边界值,那么这个边界上的粒子速度可能依然朝外,下一轮还是被卡在边界上。更好的做法是把越界后的速度也清成0,让粒子在边界上重新决策方向。这个小改动在很多优化问题里都能显著改善收敛质量。
5.4 我推荐的应用场景和更进一步的扩展
总结下来,PSO-GPR最适合的场景是样本量在几百到几千、特征维度不太高、回归任务对精度和不确定性都有要求的场合。典型例子包括设备参数预测、环境变量预测、工程响应面建模。如果样本量达到几万甚至更大,GPR本身的训练代价会成为瓶颈,这时PSO只在调参环节省时间已经不够,应该考虑稀疏高斯过程,或者换用其他回归模型。
如果你已经跑通了基础版PSO-GPR,下一步扩展可以做两个方向。第一个方向是使用ARD核,让每个输入特征拥有独立的长度尺度。这样PSO的维度会变成“特征数+2”,搜索空间更大,但也能同时回答“哪些特征对预测更重要”这个问题。第二个方向是使用多目标优化,比如同时优化“预测误差”和“预测方差校准度”,让模型不仅预测得准,而且能给出可靠的不确定性估计。标准的PSO只需要把单目标适应度函数改成加权和,原理解释起来也不复杂。
我个人的经验是,PSO-GPR不是一种需要常年维护的复杂架构,它更像一套“调参加速器”。核心价值是让你从繁琐的手动搜索里解放出来,把注意力放到数据清洗、特征构造和结果解释这些真正影响项目成败的事情上。如果你也被GPR超参数折磨过,不妨按这篇文章的代码试一遍,大概率会得到一个比自己瞎调强得多的结果。