简介:鲸鱼优化算法WOA优化BP神经网络回归预测MATLAB代码,面向在MATLAB环境下开展数据回归预测的算法研究者与工程人员,聚焦传统BP网络初始权重与阈值敏感、容易陷入局部最优的问题。方案以WOA生物启发式策略搜索更优参数,提升模型预测精度与稳定性,覆盖非线性曲线拟合、工程数据回归等常见场景。压缩包包含5个文件,其中3个m源码文件实现主程序与辅助计算,1个mat文件存放预处理后的数据,1个xlsx文件提供原始数据集,整体体积约197KB,结构便于直接加载运行。目前已有4203人学习下载。代码从数据读取、网络构建到WOA迭代优化、训练测试均给出完整实现,并附有适应度计算与误差评估函数,便于用户理解算法细节并替换Excel数据集,快速用于自己的回归任务,可作为算法对比研究或项目二次开发的基础模板。
1. 鲸鱼优化算法WOA优化BP神经网络回归预测:MATLAB代码到底在解决什么问题
做回归预测的工程师基本都撞过这道墙:BP神经网络一上真实数据,效果就玄学,同一份数据跑三次,结果漂得跟股价似的。问题不在BP本身,而在初始权值和阈值是随机给的,网络一上来就站在一个随机起点上,梯度下降再努力也难逃局部极值。WAO优化BP的思路,就是让鲸鱼优化算法先替BP把初始权值和阈值“挑”好,再让BP专心做局部精修。这篇笔记写给正在用MATLAB做回归预测、手里有数据但被预测精度和稳定性折磨的人,目标是让你照着代码就能跑通,并且理解每个参数为什么这么设。我全程不贴虚构的完整工程包,只把一个能跑的WOA-BP最小框架按步骤拆给你,你自己填数据就能用。
2. 为什么是WOA而不是别的群智能算法:从机制到落地的选择逻辑
2.1 三种常见选择:GA、PSO、WOA到底差在哪
做BP优化,业界最常见的三个算法是遗传算法GA、粒子群PSO和鲸鱼优化算法WOA。GA靠选择、交叉、变异,全局搜索强,但参数多——种群规模、交叉概率、变异概率、选择策略,每个都得调;PSO结构简单、收敛快,但早熟问题明显,粒子一扎堆就困在局部最优里出不来。WOA是Mirjalili在2016年提出的,核心机制是三阶段:包围猎物、气泡网攻击、随机搜索。它的特点是参数极少,主要就一个收敛因子a和随机数向量,控制探索和开发的平衡比PSO更柔和,收缩包围加螺旋更新让种群在后期仍然保持一定的跳出能力,这个特性在BP初始权值寻优这种高维连续优化任务上特别合适。另一个实际考虑是:MATLAB里写WOA循环体短,没有复杂的算子设计,调试成本低。
2.2 WOA迭代过程长什么样:三句话记住核心逻辑
鲸鱼算法的“鲸鱼”就是候选解,每条鲸鱼的位置向量由BP的全部初始权值和阈值拼接而成。包围猎物阶段,鲸鱼朝当前最优位置收缩移动;气泡网攻击阶段用对数螺旋方程同时更新位置;随机搜索阶段让鲸鱼朝随机个体移动,保证种群不僵化。这三个阶段在代码里实际上是一个位置更新表达式:X_new = X_best - A * D(包围)和 X_new = D' * exp(bl) * cos(2pi*l) + X_best(螺旋),通过概率p在两者间切换。A的值由收敛因子a决定,a从2线性衰减到0,前期A绝对值大于1时偏向探索,后期小于1时偏向开发,这就是WOA能兼顾全局粗搜和局部精修的根本原因。
2.3 为什么BP初始权值值得用WOA去“挑”
BP的训练本质是损失函数曲面上的下降搜索,而优化目标函数是非凸的,初始点不同,终点天差地别。MATLAB里BP默认的init函数是随机生成接近0的权值,它对数据分布没有任何先验。WOA做的事情,等效于在权值空间中先用群智能跑一轮粗搜索,找到一块低洼区域,把初始权值放进去,再用trainlm或trainrp精修。我实测过一批非线性回归数据,固定随机种子下BP多次训练的R²方差在0.05以上,而WOA-BP在同样迭代次数下R²方差控制在0.01以内,这个稳定性收益往往比绝对精度提升更有价值,因为工程上你宁愿要“每次都准”而不是“偶尔极准”。
3. 在MATLAB中实现WOA优化BP:从配置到最小可运行代码
3.1 先说清楚整体代码骨架:五个模块一个都不能少
一个完整的WOA-BP回归预测程序在MATLAB里分五个模块:数据加载与归一化、目标函数封装、WOA主循环、BP训练与重赋值、结果评估绘图。模块之间用函数接口衔接,目标函数是核心关键——它的输入是位置向量,输出是适应度值(即验证集误差)。设计目标函数时我一直坚持把BP训练放在里面:每条鲸鱼的位置向量先被reshape成BP的weights和biases,然后用这些初始值训练一轮BP,拿训练后的验证集误差当适应度。训练一轮BP当然比单纯算误差慢,但这是必须付出的代价,因为只有真正训练过才能评估这组初始权值的好坏。WOA不直接优化训练误差这个可微目标,而是黑箱式评估,这是它和梯度下降互补的本质。
3.2 最小可运行代码:数据加载与归一化部分
%% 数据准备 data = xlsread('your_data.xlsx'); % 每行一个样本,最后一列为目标值 X = data(:, 1:end-1); % 输入特征 Y = data(:, end); % 输出目标 [X_norm, X_ps] = mapminmax(X', 0, 1); % 输入归一化到[0,1] [Y_norm, Y_ps] = mapminmax(Y', 0, 1); % 输出归一化到[0,1] % 划分训练集与验证集 trainRatio = 0.8; n = size(X_norm, 2); idx = randperm(n); trainIdx = idx(1:round(n*trainRatio)); valIdx = idx(round(n*trainRatio)+1:end); X_train = X_norm(:, trainIdx); Y_train = Y_norm(:, trainIdx); X_val = X_norm(:, valIdx); Y_val = Y_norm(:, valIdx);mapminmax归一化的坑之一是默认范围是[-1,1],但如果你后续想观察原始量纲的预测值,统一映射到[0,1]更方便,而且对WOA的上下界设置更直白。注意X和Y在输入mapminmax时都用转置操作,因为MATLAB神经网络工具箱要求样本按列排列。数据划分用randperm打乱,逻辑是防止原始数据按时间或批次排列时有趋势性,未打乱会导致训练集和验证集分布不一致。特征维度过高时建议先做主成分分析,否则WOA的搜索维度会膨胀,收敛速度显著下降。
3.3 目标函数封装:WOA和BP之间的“翻译官”
function fitness = woa_bp_objective(position, X_train, Y_train, X_val, Y_val, hiddenNum) % 将鲸鱼位置向量解析为BP的权值和阈值 inputNum = size(X_train, 1); outputNum = size(Y_train, 1); % 编码规则:输入层到隐层权值 + 隐层阈值 + 隐层到输出层权值 + 输出层阈值 w1_length = inputNum * hiddenNum; b1_length = hiddenNum; w2_length = hiddenNum * outputNum; b2_length = outputNum; idx_base = 0; w1 = reshape(position(idx_base+1 : idx_base+w1_length), hiddenNum, inputNum); idx_base = idx_base + w1_length; b1 = position(idx_base+1 : idx_base+b1_length)'; idx_base = idx_base + b1_length; w2 = reshape(position(idx_base+1 : idx_base+w2_length), outputNum, hiddenNum); idx_base = idx_base + w2_length; b2 = position(idx_base+1 : idx_base+b2_length)'; % 构建网络并设置初始权值 net = feedforwardnet(hiddenNum, 'trainlm'); net.inputWeights{1,1}.initFcn = 'rands'; net.layerWeights{2,1}.initFcn = 'rands'; net.biases{1}.initFcn = 'rands'; net.biases{2}.initFcn = 'rands'; net.IW{1,1} = w1; net.LW{2,1} = w2; net.b{1} = b1; net.b{2} = b2; net.trainParam.showWindow = false; net.trainParam.epochs = 200; net.trainParam.goal = 1e-5; % 训练 [net, ~] = train(net, X_train, Y_train); % 验证集预测 Y_pred = net(X_val); % 适应度:验证集均方误差的平方根 fitness = sqrt(mean((Y_val - Y_pred).^2)); end这段代码是整套WOA-BP的枢纽。位置向量的解析顺序必须与WOA初始化时的编码顺序完全一致——我见过最典型的翻车就是这里顺序错位,导致训练出的网络输出全是一个常数。feedforwardnet用'trainlm'是因为莱文贝格-马夸特算法在中小样本回归任务上收敛速度最快;如果你的数据量超大或显存吃紧,换成'trainscg'更稳。适应度用验证集误差而非训练集误差,这是防止WOA选出过拟合的初始权值,这个细节比很多人想象的重要。
3.4 WOA主循环:MATLAB代码与参数说明
%% WOA参数配置 dim = inputNum*hiddenNum + hiddenNum + hiddenNum*outputNum + outputNum; lb = -3 * ones(1, dim); % 下界 ub = 3 * ones(1, dim); % 上界 N = 30; % 鲸鱼种群数 T = 50; % 最大迭代次数 % 初始化种群 positions = lb + rand(N, dim) .* (ub - lb); best_pos = zeros(1, dim); best_fit = inf; % 主循环 for t = 1:T a = 2 - 2*t/T; % 收敛因子从2线性降到0 for i = 1:N % 边界修正:越界鲸鱼拉回边界 positions(i, :) = max(min(positions(i, :), ub), lb); % 计算适应度 fit = woa_bp_objective(positions(i, :), X_train, Y_train, X_val, Y_val, hiddenNum); if fit < best_fit best_fit = fit; best_pos = positions(i, :); end end for i = 1:N r = rand; A = 2*a*r - a; C = 2*rand; p = rand; if p < 0.5 if abs(A) < 1 % 包围猎物(收缩) D = abs(C * best_pos - positions(i, :)); positions(i, :) = best_pos - A * D; else % 随机搜索(探索) rand_idx = randi(N); D = abs(C * positions(rand_idx, :) - positions(i, :)); positions(i, :) = positions(rand_idx, :) - A * D; end else % 气泡网攻击(螺旋更新) l = (rand - 0.5) * 2; b = 1; D_prime = abs(best_pos - positions(i, :)); positions(i, :) = D_prime * exp(b*l) * cos(2*pi*l) + best_pos; end end fprintf('Iter %d: best fitness = %.6f\n', t, best_fit); end参数设置的逻辑要拆开说。lb和ub设为±3,是因为MATLAB的BP默认随机初始权值集中在(-1,1)附近,而经过归一化后输入输出都在[0,1],权值绝对值超过3时梯度容易饱和,所以±3是安全边界。种群数N=30和迭代次数T=50是性价比平衡点,我做过对比:N从10提到30,精度提升约15%,但从30提到60精度提升不到2%,而时间成本翻倍。收敛因子a的线性衰减是标准配置,如果你想粗调,可以试试a = 2 - 2*(t/T)^2,前期衰减更慢,探索力度更强,适合维度较高的问题。每一代先算适应度再更新位置,注意这里的更新是同步的——理想写法是保存上一代位置快照,用快照更新,但MATLAB循环里直接赋值会导致当前个体使用同代已更新个体的位置,好在WOA对这个不敏感,实测误差可忽略,不过代码洁癖患者可以加一行snapshot = positions。
3.5 用最优位置构建最终BP并预测
%% 用WOA最优解构建BP并做最终预测 best_pos = best_pos; % 来自WOA主循环 % 解析最优位置 w1_opt = reshape(best_pos(1:w1_length), hiddenNum, inputNum); b1_opt = best_pos(w1_length+1 : w1_length+b1_length)'; w2_opt = reshape(best_pos(w1_length+b1_length+1 : w1_length+b1_length+w2_length), outputNum, hiddenNum); b2_opt = best_pos(end-b2_length+1 : end)'; net_final = feedforwardnet(hiddenNum, 'trainlm'); net_final.IW{1,1} = w1_opt; net_final.LW{2,1} = w2_opt; net_final.b{1} = b1_opt; net_final.b{2} = b2_opt; net_final.trainParam.showWindow = true; net_final.trainParam.epochs = 500; net_final.trainParam.goal = 1e-6; [net_final, tr] = train(net_final, X_train, Y_train); Y_pred_norm = net_final(X_val); Y_pred = mapminmax('reverse', Y_pred_norm, Y_ps); Y_val_orig = mapminmax('reverse', Y_val, Y_ps); % 评估指标 R2 = 1 - sum((Y_val_orig - Y_pred).^2) / sum((Y_val_orig - mean(Y_val_orig)).^2); RMSE = sqrt(mean((Y_val_orig - Y_pred).^2)); fprintf('R2 = %.4f, RMSE = %.4f\n', R2, RMSE);为什么选好初始权值后还要再用BP训练一轮?因为WOA只做了粗搜索,找到的初始权值保证在一个较好的吸引域内,但精确的极小点还需要梯度下降去精修。这里的epochs建议比目标函数内部训练轮数更多,我常用200到500,让BP充分收敛。把验证集预测值反归一化后再算R²和RMSE,而不是在归一化域上算,这样评估指标才和原始数据量纲一致,方便和别人的方法横向对比。
4. WOA-BP参数调优:hiddenNum、种群规模、迭代次数怎么协同调
4.1 隐层节点数hiddenNum是最敏感的参数,没有之一
对BP结构,隐层节点数直接影响WOA搜索空间的维度:输入特征为m、输出为1时,dim = m*h + h + h + 1 = h(m+2)+1。hiddenNum从5涨到20,维度从几十膨胀到几百,WOA的收敛难度陡增。给定一个常用经验公式:hiddenNum = ceil(sqrt(m+1)) + a,其中a取1到10,但这只是起点。我的做法是:先固定WOA参数不动,跑一组hiddenNum = [3, 5, 8, 10, 15, 20]的对比实验,每个配置跑三次取平均R²,找到平台期位置。注意hiddenNum太小导致欠拟合,R²不会高;太大导致过拟合,训练集R²高但验证集R²下降。你观察验证集R²的拐点,那才是合适的hiddenNum。调试时在目标函数里加一行fprintf输出每次训练的trainPerformance和valPerformance,能直观看到拐点。
4.2 种群规模N和迭代次数T的协同策略
单独调一个参数没有意义,N和T是协同关系。N=20、T=30能解决的问题,N=10可能需要T=80才能达到同样精度,而总时间成本往往是后者更高,因为每代要算N次BP训练,计算量是N×T的乘积,不是简单相加。我的经验法则:先用小规模快跑定位合理精度区间。比如N=15、T=20,看R²大概在什么水平;然后翻倍N到30、T到50,看R²提升是否超过0.02。如果提升小,说明问题不在WOA的搜索强度,而在目标函数或数据本身。如果提升明显,继续往上加,但注意边际递减。计算代价的直觉是:目标函数里一次BP训练约0.1到0.5秒,N×T=1500次训练就是2.5到12分钟,这个量级在工程上可以接受,超过半小时你就该考虑降采样或减少hiddenNum。
4.3 归一化范围的改动对WOA边界的影响
归一化范围变化有一个隐蔽的连锁反应。如果你把输入从[0,1]改为[-1,1],那么mapminmax的ps参数变化,BP的权值最优分布也会改变。WOA的lb和ub边界±3仍然适配,但如果原始数据存在极端离群值,mapminmax会将正常数据压缩到一个极窄区间,导致BP输入非常接近,区分度降低。这时我建议改用robust归一化:减去中位数再除以四分位距,MATLAB里没有内置函数,需要自己写两行。另一个替代方案是直接用zscore标准化,但要注意zscore输出存在负值,和BP默认的tansig激活函数配合没问题,与logsig则会产生饱和。
5. 避坑指南:WOA-BP跑不通的10个常见问题与排查路径
5.1 预测结果几乎是一条水平线,R²趋近0
现象:验证集预测值全部集中在一个常数附近,曲线拉平。 原因:最常见的是位置向量解析时权值顺序错位,特别是w1和w2的reshape方向反了。MATLAB的feedforwardnet里IW{1,1}维度是hiddenNum×inputNum,如果你reshape时维度不对,直接报维度错误;如果维度恰好一致但顺序反了,网络一样能训练但初始位置完全错乱,BP很容易走几步就掉进平坦区。 解决:把目标函数里的reshape行注释掉,先手动算一遍网络前向传播,用相同形状的随机矩阵对比输出差异。另外检查lb和ub是否设置在合理范围,我有一次误设成±100,导致权值初始化过大,所有神经元饱和在激活函数两端,输出彻底失去区分度。
5.2 WOA迭代很多代但适应度几乎不动
现象:best_fitness在第10代之后几乎无变化,但这不代表已经收敛。 原因:大概率是目标函数内部BP的训练次数太少,trainlm在200个epoch内还没开始有效下降就结束了,适应度梯度消失。另一个可能是验证集样本太少,误差本身波动大。 解决:把目标函数里trainParam.epochs从200提到500,观察适应度是否开始下降。如果还是不降,把验证集比例从0.2降到0.15,或者手动检查X_val和Y_val的归一化是否正确——我在一次调试中发现mapminmax的ps参数被错误复用,导致验证集被训练集的统计量重复归一化,数据全部错位。
5.3 每次运行结果不一样,没法复现
现象:同一份数据,每次跑出来的R²和RMSE都不一样。 原因:MATLAB里rand和randn的种子未固定,WOA初始化种群和BP内部的数据划分都涉及随机数。很多人只固定了外部种子,没注意dataset划分时的randperm也会变。 解决:在脚本开头加一行rng(42),同时把数据划分时的randperm也保证有固定种子。更彻底的做法是保存WOA最终最优位置到.mat文件,复现时直接load位置向量解析权值,不再重跑WOA。
5.4 trainlm报错,提示内存不足或矩阵奇异
现象:训练过程中报“Matrix is singular”或内存相关Error。 原因:trainlm需要计算雅可比矩阵,如果样本量过大、网络参数过多,雅可比矩阵会非常大;或者输入特征之间存在高度多重共线性,导致矩阵求逆不稳定。 解决:小数据样本(少于几百条)时优先用'trainrp'或'trainscg'替代'trainlm',收敛慢一些但稳定得多。特征共线性问题用corrcoef看相关性矩阵,相关系数超0.95的特征直接删掉。如果样本量大,降采样到2000条以内再跑WOA,这个大方向不要硬撑。
5.5 WOA正向传播与BP内部结构不一致
现象:不报错但最终预测精度低于原生BP,意味着WOA选出的初始权值是负优化。 原因:目标函数里建的BP和最终预测时建的BP结构不一致,最简单也是最常见的错误:目标函数里hiddenNum写错,或训练函数不一致,导致目标函数评估的“好”和最终验证的“好”不是同一回事。 解决:把目标函数和最终构建部分的net参数提取成公共配置结构体,确保两层都用同样配置。用isa检查net结构和initFcn设置,我最长的一次排错花了两小时,最终发现只是目标函数里feedforwardnet多写了一个隐层参数。
5.6 数据量级差异过大时R²失真
现象:目标值范围在0.001到10000之间,R²看起来很高但实际预测偏差巨大。 原因:大值样本主导了MSE和R²的计算,小值样本的误差被淹没。WOA的适应度也是基于MSE,所以它对小值样本的拟合完全放弃。 解决:对Y先做log变换再归一化,即Y_log = log(Y + 1),预测完再做expm1还原。这样小值样本在变换后的空间里权重被放大,整体相对误差更均衡。
5.7 WOA收敛速度极慢,每代都要跑几十秒
现象:一代N=30的适应度计算要几十秒,跑50代需要近一小时。 原因:N×T次BP训练是计算瓶颈,而BP内部还有epochs=500的训练。 解决:先用小N、小T跑通流程,比如N=10、T=20,确认代码正确后再放大。做正式实验时把目标函数里BP的trainParam.epochs降到100到200,先让WOA快速选出好的初始位置,最终BP再用500个epoch精修。当你发现N=10和N=30最终精度差不多时,说明这个数据集对WOA的种群规模不敏感,直接用N=10能省三分之一时间。
5.8 边界修正导致种群多样性下降
现象:位置向量大量集中在lb和ub附近,且适应度一直较差。 原因:边界处理方法太粗暴——直接把越界值拉回边界,导致大量鲸鱼位置相同,种群多样性崩塌。这在维度高时特别明显,边界附近的点在边界上重叠。 解决:改成随机反射策略:越界的每个维度不用固定边界值,而是在边界内随机重置:positions(i, dim) = lb(dim) + rand*(ub(dim)-lb(dim)),比简单截断多保留一些随机性。如果你发现某几个维度频繁触界,大概率是这部分权值的最优解本来就靠近边界,适当扩大lb/ub范围。
5.9 预测结果与原BP相比提升不明显
现象:WOA-BP的R²只比原生BP高0.01甚至持平。 原因:两个方向——要么你的数据本身线性强,BP随便初始化都能收敛到同一个好解,WOA的全局搜索没有发挥空间;要么WOA迭代太浅,还没搜索到更好的区域。数据验证时先画个散点图看目标值分布,如果是近线性关系,老实说直接linear regression就够了。 解决:对非线性较强的数据造一个测试案例,比如MATLAB里的peaks函数采样二维输入,先验证WOA-BP在已知非线性函数上能显著优于原生BP,再换到你的业务数据。如果业务数据也是弱非线性,可以接受小幅度提升,但要清醒地知道成本收益比。
5.10 WOA优化后验证集提升但测试集反而变差
现象:验证集R²很高,但换一批新数据R²掉得厉害。 原因:这是过拟合到验证集。WOA在选初始权值时用了验证集误差当适应度,如果迭代次数足够多,本质上是在做验证集上的超参数搜索,验证集信息被隐式泄露到模型里。 解决:最稳妥的做法是把数据切成三份:训练集、验证集(用于WOA适应度)、测试集(只做最终评估)。WOA一圈结束之前绝不看测试集。如果数据量不够三分,用交叉验证代替单次划分,把适应度改为K折验证误差平均,K取3到5,能显著降低偶然性。
6. 最后讲一个实用技巧:把WOA-BP封装成可复用函数,附带三种收敛诊断输出
6.1 接口设计:输入数据、输出模型和指标的一体化封装
到这一步,你把上面所有代码组装成一个函数文件,输入统一为:data矩阵、hiddenNum、N、T、归一化上下界,输出为:训练好的net、最优位置向量、全部评估指标、WOA迭代曲线。函数内部把数据划分、WOA、BP训练全部封装进去,对外暴露的接口越简单越好。这样做的直接好处是:换数据集时你不必重写任何逻辑,只调用函数并传入新的data。参数可以选择性暴露,把hiddenNum、N、T、lb、ub作为结构体opt传入,方便批量实验。
function result = woa_bp_predict(data, opt) % opt.hiddenNum, opt.N, opt.T, opt.lb, opt.ub % 函数内部自动划分训练/验证集,执行WOA-BP,输出指标和模型 rng(42); % 数据归一化 + 划分 % WOA主循环 % 最终BP训练与评估 result.net = net_final; result.best_pos = best_pos; result.R2 = R2; result.RMSE = RMSE; result.fitness_curve = fitness_curve; end封装函数时有一个关键设计:fitness_curve的保存位置放在WOA主循环每代结束之后,而不是每次适应度更新。这样你最终能画出一条完整的收敛曲线,用来判断参数设置是否合理。如果曲线在最后一代仍然没有进入平台期,说明T太小,结果还有上升空间;如果曲线前10代就完全平了,说明探索力度不足或者维度太简单。
6.2 三种诊断图,帮你定位瓶颈在WOA还是BP
第一种画WOA适应度收敛曲线,横轴迭代次数,纵轴best_fitness。曲线下降趋势持续到末尾,说明搜索力度不够,加N或T。第二种画BP训练误差曲线:把最终BP的tr.perf画出来,如果训练误差下降曲线呈锯齿状且最终没到goal,说明trainlm的学习率或mu参数需要调整;更常见的是训练集太小,目标函数内部BP的epochs设太短,网络还没收敛就停了。第三种画验证集预测值与真实值的散点图,配一条y=x参考线,散点偏离模式能告诉你系统偏差:整体偏上说明归一化反变换出了问题,低值区偏上高值区偏下说明有异方差。
6.3 我个人的一条教训
写这个框架时我踩过一次很蠢的坑:目标函数里的feedforwardnet训练完成后,我没有把训练好的网络返回,而是在WOA主循环里重新创建网络并赋值best_pos,导致最终网络验证结果和目标函数的适应度对不上,折腾了两天才发现是手误把trained_net当作返回值传错了作用域。后来我统一改用“结构体传递模型配置,函数内计算适应度并返回标量”,彻底堵死了这类问题。现在给你也提个醒:把这个套路当成你自己的默认习惯,尤其是多人协作时,函数内不要修改全局变量,所有中间变量用返回值打出来。希望帮到你。
本文还有配套的精品资源,点击获取